From 4f078e1ee453ca0d222911d49aeff32e17aa097d Mon Sep 17 00:00:00 2001
From: kame <63849460+kamegoro@users.noreply.github.com>
Date: Tue, 7 Nov 2023 12:35:05 +0900
Subject: [PATCH] Migrated Storybook notation from CSF2 to CSF3 (#3412)

* Migrate web action-buttons directory to CSF3 notation

* Migrate web chat directory to CSF3 notation

* Migrate web common directory to CSF3 notation

* Migrate web layout directory to CSF3 notation

* Migrate web modals directory to CSF3 notation

* Migrate web ui directory to CSF3 notation

* Migrate web video directory to CSF3 notation

* Migrate web stories directory to CSF3 notation
---
 .../ActionButton/ActionButton.stories.tsx     |  54 +++----
 .../ActionButtonMenu.stories.tsx              |  55 +++++---
 .../ActionButtonRow.stories.tsx               |  23 +--
 .../ChatActionMessage.stories.tsx             |  16 +--
 .../ChatContainer/ChatContainer.stories.tsx   |  75 +++++-----
 .../ChatJoinMessage.stories.tsx               |  31 ++--
 .../ChatModerationActionMenu.stories.tsx      |  17 +--
 .../ChatModerationDetailsModal.stories.tsx    |  17 +--
 .../ChatModeratorNotification.stories.tsx     |  15 +-
 .../ChatNameChangeMessage.stories.tsx         |  26 ++--
 .../ChatPartMessage.stories.tsx               |  31 ++--
 .../ChatSocialMessage.stories.tsx             |  94 +++++++------
 .../ChatSystemMessage.stories.tsx             |  26 ++--
 .../ChatTextField/ChatTextField.stories.tsx   |  62 ++++----
 .../ChatUserBadge/ChatUserBadge.stories.tsx   |  56 ++++----
 .../ChatUserMessage.stories.tsx               |  96 ++++++++-----
 .../ContentHeader/ContentHeader.stories.tsx   | 133 +++++++++---------
 .../OwncastLogo/OwncastLogo.stories.tsx       |  25 ++--
 .../UserDropdown/UserDropdown.stories.tsx     |  21 +--
 web/components/layouts/Main/Main.stories.tsx  | 106 ++++++++------
 .../modals/AuthModal/AuthModal.stories.tsx    |  17 ++-
 .../BrowserNotifyModal.stories.tsx            |  17 +--
 .../modals/ChatModal/ChatModal.stories.tsx    |  41 +++---
 .../FatalErrorStateModal.stories.tsx          |  22 ++-
 .../FediAuthModal/FediAuthModal.stories.tsx   |  31 ++--
 .../FollowModal/FollowModal.stories.tsx       |  17 +--
 .../IndieAuthModal/IndieAuthModal.stories.tsx |  17 +--
 .../NameChangeModal.stories.tsx               |  16 ++-
 .../ComponentError/ComponentError.stories.tsx |  47 ++++---
 .../CustomPageContent.stories.tsx             |  38 +++--
 web/components/ui/Footer/Footer.stories.tsx   |  21 +--
 web/components/ui/Header/Header.stories.tsx   |  33 +++--
 web/components/ui/Modal/Modal.stories.tsx     |  37 +++--
 .../NotifyReminderPopup.stories.tsx           |  39 ++---
 .../OfflineBanner/OfflineBanner.stories.tsx   |  97 ++++++++-----
 .../ui/SocialLinks/SocialLinks.stories.tsx    |  57 ++++----
 .../ui/Statusbar/StatusBar.stories.tsx        |  29 ++--
 .../FollowerCollection.stories.tsx            |  29 ++--
 .../SingleFollower/SingleFollower.stories.tsx |  26 ++--
 .../OwncastPlayer/OwncastPlayer.stories.tsx   |  24 ++--
 .../video/VideoPoster/VideoPoster.stories.tsx |  44 +++---
 web/stories/PageLogo.stories.tsx              |  32 ++---
 web/stories/ReadonlyChat.stories.tsx          |  18 ++-
 web/stories/ReadwriteChat.stories.tsx         |  18 ++-
 44 files changed, 966 insertions(+), 780 deletions(-)

diff --git a/web/components/action-buttons/ActionButton/ActionButton.stories.tsx b/web/components/action-buttons/ActionButton/ActionButton.stories.tsx
index e174782dc..9e8023129 100644
--- a/web/components/action-buttons/ActionButton/ActionButton.stories.tsx
+++ b/web/components/action-buttons/ActionButton/ActionButton.stories.tsx
@@ -1,9 +1,8 @@
-import React from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { StoryFn, Meta } from '@storybook/react';
 import { action } from '@storybook/addon-actions';
 import { ActionButton } from './ActionButton';
 
-export default {
+const meta = {
   title: 'owncast/Components/Action Buttons/Single button',
   component: ActionButton,
   parameters: {
@@ -13,38 +12,45 @@ export default {
       },
     },
   },
-} as ComponentMeta<typeof ActionButton>;
+} satisfies Meta<typeof ActionButton>;
+
+export default meta;
 
 const itemSelected = a => {
   console.log('itemSelected', a);
   action(a.title);
 };
 
-const Template: ComponentStory<typeof ActionButton> = args => (
+const Template: StoryFn<typeof ActionButton> = args => (
   <ActionButton externalActionSelected={itemSelected} {...args} />
 );
 
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-export const Example1 = Template.bind({});
-Example1.args = {
-  action: {
-    url: 'https://owncast.online/docs',
-    title: 'Documentation',
-    description: 'Owncast Documentation',
-    icon: 'https://owncast.online/images/logo.svg',
-    color: '#5232c8',
-    openExternally: false,
+export const Example1 = {
+  render: Template,
+
+  args: {
+    action: {
+      url: 'https://owncast.online/docs',
+      title: 'Documentation',
+      description: 'Owncast Documentation',
+      icon: 'https://owncast.online/images/logo.svg',
+      color: '#5232c8',
+      openExternally: false,
+    },
   },
 };
 
-export const Example2 = Template.bind({});
-Example2.args = {
-  action: {
-    url: 'https://opencollective.com/embed/owncast/donate',
-    title: 'Support Owncast',
-    description: 'Contribute to Owncast',
-    icon: 'https://opencollective.com/static/images/opencollective-icon.svg',
-    color: '#2b4863',
-    openExternally: false,
+export const Example2 = {
+  render: Template,
+
+  args: {
+    action: {
+      url: 'https://opencollective.com/embed/owncast/donate',
+      title: 'Support Owncast',
+      description: 'Contribute to Owncast',
+      icon: 'https://opencollective.com/static/images/opencollective-icon.svg',
+      color: '#2b4863',
+      openExternally: false,
+    },
   },
 };
diff --git a/web/components/action-buttons/ActionButtonMenu/ActionButtonMenu.stories.tsx b/web/components/action-buttons/ActionButtonMenu/ActionButtonMenu.stories.tsx
index ee65d20b4..12769546f 100644
--- a/web/components/action-buttons/ActionButtonMenu/ActionButtonMenu.stories.tsx
+++ b/web/components/action-buttons/ActionButtonMenu/ActionButtonMenu.stories.tsx
@@ -1,20 +1,21 @@
-import React from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { StoryFn, Meta } from '@storybook/react';
 import { action } from '@storybook/addon-actions';
 import { ActionButtonMenu } from './ActionButtonMenu';
 
-export default {
+const meta = {
   title: 'owncast/Components/Action Buttons/Action Menu',
   component: ActionButtonMenu,
   parameters: {},
-} as ComponentMeta<typeof ActionButtonMenu>;
+} satisfies Meta<typeof ActionButtonMenu>;
+
+export default meta;
 
 const itemSelected = a => {
   console.log('itemSelected', a);
   action(a.title);
 };
 
-const Template: ComponentStory<typeof ActionButtonMenu> = args => (
+const Template: StoryFn<typeof ActionButtonMenu> = args => (
   <ActionButtonMenu {...args} externalActionSelected={a => itemSelected(a)} />
 );
 
@@ -37,26 +38,38 @@ const actions = [
   },
 ];
 
-export const Example = Template.bind({});
-Example.args = {
-  actions,
+export const Example = {
+  render: Template,
+
+  args: {
+    actions,
+  },
 };
 
-export const ShowFollowExample = Template.bind({});
-ShowFollowExample.args = {
-  actions,
-  showFollowItem: true,
+export const ShowFollowExample = {
+  render: Template,
+
+  args: {
+    actions,
+    showFollowItem: true,
+  },
 };
 
-export const ShowNotifyExample = Template.bind({});
-ShowNotifyExample.args = {
-  actions,
-  showNotifyItem: true,
+export const ShowNotifyExample = {
+  render: Template,
+
+  args: {
+    actions,
+    showNotifyItem: true,
+  },
 };
 
-export const ShowNotifyAndFollowExample = Template.bind({});
-ShowNotifyAndFollowExample.args = {
-  actions,
-  showNotifyItem: true,
-  showFollowItem: true,
+export const ShowNotifyAndFollowExample = {
+  render: Template,
+
+  args: {
+    actions,
+    showNotifyItem: true,
+    showFollowItem: true,
+  },
 };
diff --git a/web/components/action-buttons/ActionButtonRow/ActionButtonRow.stories.tsx b/web/components/action-buttons/ActionButtonRow/ActionButtonRow.stories.tsx
index d0afab5fd..4a549dd09 100644
--- a/web/components/action-buttons/ActionButtonRow/ActionButtonRow.stories.tsx
+++ b/web/components/action-buttons/ActionButtonRow/ActionButtonRow.stories.tsx
@@ -1,10 +1,9 @@
-import React from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { StoryFn, Meta } from '@storybook/react';
 import { action } from '@storybook/addon-actions';
 import { ActionButtonRow } from './ActionButtonRow';
 import { ActionButton } from '../ActionButton/ActionButton';
 
-export default {
+const meta = {
   title: 'owncast/Components/Action Buttons/Buttons Row',
   component: ActionButtonRow,
   parameters: {
@@ -15,15 +14,15 @@ export default {
       },
     },
   },
-} as ComponentMeta<typeof ActionButtonRow>;
+} satisfies Meta<typeof ActionButtonRow>;
 
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-const Template: ComponentStory<typeof ActionButtonRow> = args => {
+export default meta;
+
+const Template: StoryFn<typeof ActionButtonRow> = args => {
   const { buttons } = args as any;
   return <ActionButtonRow>{buttons}</ActionButtonRow>;
 };
 
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
 const actions = [
   {
     url: 'https://owncast.online/docs',
@@ -49,7 +48,11 @@ const itemSelected = a => {
 };
 
 const buttons = actions.map(a => <ActionButton externalActionSelected={itemSelected} action={a} />);
-export const Example1 = Template.bind({});
-Example1.args = {
-  buttons,
+
+export const Example1 = {
+  render: Template,
+
+  args: {
+    buttons,
+  },
 };
diff --git a/web/components/chat/ChatActionMessage/ChatActionMessage.stories.tsx b/web/components/chat/ChatActionMessage/ChatActionMessage.stories.tsx
index 74c8c71b1..f1e78105b 100644
--- a/web/components/chat/ChatActionMessage/ChatActionMessage.stories.tsx
+++ b/web/components/chat/ChatActionMessage/ChatActionMessage.stories.tsx
@@ -1,9 +1,8 @@
-import React from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { Meta } from '@storybook/react';
 import { ChatActionMessage } from './ChatActionMessage';
 import Mock from '../../../stories/assets/mocks/chatmessage-action.png';
 
-export default {
+const meta = {
   title: 'owncast/Chat/Messages/Chat action',
   component: ChatActionMessage,
   parameters: {
@@ -17,11 +16,12 @@ export default {
       },
     },
   },
-} as ComponentMeta<typeof ChatActionMessage>;
+} satisfies Meta<typeof ChatActionMessage>;
 
-const Template: ComponentStory<typeof ChatActionMessage> = args => <ChatActionMessage {...args} />;
+export default meta;
 
-export const Basic = Template.bind({});
-Basic.args = {
-  body: 'This is a basic action message.',
+export const Basic = {
+  args: {
+    body: 'This is a basic action message.',
+  },
 };
diff --git a/web/components/chat/ChatContainer/ChatContainer.stories.tsx b/web/components/chat/ChatContainer/ChatContainer.stories.tsx
index a9b034fea..57f25b7a0 100644
--- a/web/components/chat/ChatContainer/ChatContainer.stories.tsx
+++ b/web/components/chat/ChatContainer/ChatContainer.stories.tsx
@@ -1,10 +1,10 @@
-import React, { useState } from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { useState } from 'react';
+import { StoryFn, Meta } from '@storybook/react';
 import { RecoilRoot } from 'recoil';
 import { ChatContainer } from './ChatContainer';
 import { ChatMessage } from '../../../interfaces/chat-message.model';
 
-export default {
+const meta = {
   title: 'owncast/Chat/Chat messages container',
   component: ChatContainer,
   parameters: {
@@ -19,7 +19,9 @@ export default {
       },
     },
   },
-} as ComponentMeta<typeof ChatContainer>;
+} satisfies Meta<typeof ChatContainer>;
+
+export default meta;
 
 const testMessages = `[
 		{
@@ -586,37 +588,46 @@ const AddMessagesChatExample = args => {
   );
 };
 
-const Template: ComponentStory<typeof ChatContainer> = args => <AddMessagesChatExample {...args} />;
+const Template: StoryFn<typeof ChatContainer> = args => <AddMessagesChatExample {...args} />;
+
+export const Example = {
+  render: Template,
 
-export const Example = Template.bind({});
-Example.args = {
-  loading: false,
-  messages,
-  usernameToHighlight: 'testuser',
-  chatUserId: 'testuser',
-  isModerator: true,
-  showInput: true,
-  chatAvailable: true,
+  args: {
+    loading: false,
+    messages,
+    usernameToHighlight: 'testuser',
+    chatUserId: 'testuser',
+    isModerator: true,
+    showInput: true,
+    chatAvailable: true,
+  },
 };
 
-export const ChatDisabled = Template.bind({});
-ChatDisabled.args = {
-  loading: false,
-  messages,
-  usernameToHighlight: 'testuser',
-  chatUserId: 'testuser',
-  isModerator: true,
-  showInput: true,
-  chatAvailable: false,
+export const ChatDisabled = {
+  render: Template,
+
+  args: {
+    loading: false,
+    messages,
+    usernameToHighlight: 'testuser',
+    chatUserId: 'testuser',
+    isModerator: true,
+    showInput: true,
+    chatAvailable: false,
+  },
 };
 
-export const SingleMessage = Template.bind({});
-SingleMessage.args = {
-  loading: false,
-  messages: [messages[0]],
-  usernameToHighlight: 'testuser',
-  chatUserId: 'testuser',
-  isModerator: true,
-  showInput: true,
-  chatAvailable: true,
+export const SingleMessage = {
+  render: Template,
+
+  args: {
+    loading: false,
+    messages: [messages[0]],
+    usernameToHighlight: 'testuser',
+    chatUserId: 'testuser',
+    isModerator: true,
+    showInput: true,
+    chatAvailable: true,
+  },
 };
diff --git a/web/components/chat/ChatJoinMessage/ChatJoinMessage.stories.tsx b/web/components/chat/ChatJoinMessage/ChatJoinMessage.stories.tsx
index 474931fb0..31d5888a4 100644
--- a/web/components/chat/ChatJoinMessage/ChatJoinMessage.stories.tsx
+++ b/web/components/chat/ChatJoinMessage/ChatJoinMessage.stories.tsx
@@ -1,9 +1,8 @@
-import React from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { Meta } from '@storybook/react';
 import { ChatJoinMessage } from './ChatJoinMessage';
 import Mock from '../../../stories/assets/mocks/chatmessage-action.png';
 
-export default {
+const meta = {
   title: 'owncast/Chat/Messages/Chat Join',
   component: ChatJoinMessage,
   argTypes: {
@@ -23,20 +22,22 @@ export default {
       },
     },
   },
-} as ComponentMeta<typeof ChatJoinMessage>;
+} satisfies Meta<typeof ChatJoinMessage>;
 
-const Template: ComponentStory<typeof ChatJoinMessage> = args => <ChatJoinMessage {...args} />;
+export default meta;
 
-export const Regular = Template.bind({});
-Regular.args = {
-  displayName: 'RandomChatter',
-  isAuthorModerator: false,
-  userColor: 3,
+export const Regular = {
+  args: {
+    displayName: 'RandomChatter',
+    isAuthorModerator: false,
+    userColor: 3,
+  },
 };
 
-export const Moderator = Template.bind({});
-Moderator.args = {
-  displayName: 'RandomChatter',
-  isAuthorModerator: true,
-  userColor: 2,
+export const Moderator = {
+  args: {
+    displayName: 'RandomChatter',
+    isAuthorModerator: true,
+    userColor: 2,
+  },
 };
diff --git a/web/components/chat/ChatModerationActionMenu/ChatModerationActionMenu.stories.tsx b/web/components/chat/ChatModerationActionMenu/ChatModerationActionMenu.stories.tsx
index 80a24162a..249160b68 100644
--- a/web/components/chat/ChatModerationActionMenu/ChatModerationActionMenu.stories.tsx
+++ b/web/components/chat/ChatModerationActionMenu/ChatModerationActionMenu.stories.tsx
@@ -1,5 +1,4 @@
-import React from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { StoryFn, Meta } from '@storybook/react';
 import { RecoilRoot } from 'recoil';
 import { ChatModerationActionMenu } from './ChatModerationActionMenu';
 
@@ -64,7 +63,7 @@ const mocks = {
   ],
 };
 
-export default {
+const meta = {
   title: 'owncast/Chat/Moderation menu',
   component: ChatModerationActionMenu,
   parameters: {
@@ -79,10 +78,11 @@ export default {
       },
     },
   },
-} as ComponentMeta<typeof ChatModerationActionMenu>;
+} satisfies Meta<typeof ChatModerationActionMenu>;
 
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-const Template: ComponentStory<typeof ChatModerationActionMenu> = () => (
+export default meta;
+
+const Template: StoryFn<typeof ChatModerationActionMenu> = () => (
   <RecoilRoot>
     <ChatModerationActionMenu
       accessToken="abc123"
@@ -93,5 +93,6 @@ const Template: ComponentStory<typeof ChatModerationActionMenu> = () => (
   </RecoilRoot>
 );
 
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-export const Basic = Template.bind({});
+export const Basic = {
+  render: Template,
+};
diff --git a/web/components/chat/ChatModerationDetailsModal/ChatModerationDetailsModal.stories.tsx b/web/components/chat/ChatModerationDetailsModal/ChatModerationDetailsModal.stories.tsx
index 85d102d84..3b44e9ba6 100644
--- a/web/components/chat/ChatModerationDetailsModal/ChatModerationDetailsModal.stories.tsx
+++ b/web/components/chat/ChatModerationDetailsModal/ChatModerationDetailsModal.stories.tsx
@@ -1,5 +1,4 @@
-import React from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { StoryFn, Meta } from '@storybook/react';
 import { RecoilRoot } from 'recoil';
 import { ChatModerationDetailsModal } from './ChatModerationDetailsModal';
 
@@ -64,7 +63,7 @@ const mocks = {
   ],
 };
 
-export default {
+const meta = {
   title: 'owncast/Chat/Moderation modal',
   component: ChatModerationDetailsModal,
   parameters: {
@@ -79,14 +78,16 @@ export default {
       },
     },
   },
-} as ComponentMeta<typeof ChatModerationDetailsModal>;
+} satisfies Meta<typeof ChatModerationDetailsModal>;
 
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-const Template: ComponentStory<typeof ChatModerationDetailsModal> = () => (
+export default meta;
+
+const Template: StoryFn<typeof ChatModerationDetailsModal> = () => (
   <RecoilRoot>
     <ChatModerationDetailsModal userId="testuser123" accessToken="fakeaccesstoken4839" />
   </RecoilRoot>
 );
 
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-export const Example = Template.bind({});
+export const Example = {
+  render: Template,
+};
diff --git a/web/components/chat/ChatModeratorNotification/ChatModeratorNotification.stories.tsx b/web/components/chat/ChatModeratorNotification/ChatModeratorNotification.stories.tsx
index 297f93c61..7cb1dec69 100644
--- a/web/components/chat/ChatModeratorNotification/ChatModeratorNotification.stories.tsx
+++ b/web/components/chat/ChatModeratorNotification/ChatModeratorNotification.stories.tsx
@@ -1,17 +1,12 @@
-import React from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { Meta } from '@storybook/react';
 import { ChatModeratorNotification } from './ChatModeratorNotification';
 
-export default {
+const meta = {
   title: 'owncast/Chat/Messages/Moderation Role Notification',
   component: ChatModeratorNotification,
   parameters: {},
-} as ComponentMeta<typeof ChatModeratorNotification>;
+} satisfies Meta<typeof ChatModeratorNotification>;
 
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-const Template: ComponentStory<typeof ChatModeratorNotification> = (args: object) => (
-  <ChatModeratorNotification {...args} />
-);
+export default meta;
 
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-export const Basic = Template.bind({});
+export const Basic = {};
diff --git a/web/components/chat/ChatNameChangeMessage/ChatNameChangeMessage.stories.tsx b/web/components/chat/ChatNameChangeMessage/ChatNameChangeMessage.stories.tsx
index e778cc46b..e32c38b37 100644
--- a/web/components/chat/ChatNameChangeMessage/ChatNameChangeMessage.stories.tsx
+++ b/web/components/chat/ChatNameChangeMessage/ChatNameChangeMessage.stories.tsx
@@ -1,23 +1,21 @@
-import React from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { Meta } from '@storybook/react';
 import { ChatNameChangeMessage } from './ChatNameChangeMessage';
 
-export default {
+const meta = {
   title: 'owncast/Chat/Messages/Chat name change',
   component: ChatNameChangeMessage,
-} as ComponentMeta<typeof ChatNameChangeMessage>;
+} satisfies Meta<typeof ChatNameChangeMessage>;
 
-const Template: ComponentStory<typeof ChatNameChangeMessage> = args => (
-  <ChatNameChangeMessage {...args} />
-);
+export default meta;
 
-export const Basic = Template.bind({});
-Basic.args = {
-  message: {
-    oldName: 'JohnnyOldName',
-    user: {
-      displayName: 'JohnnyNewName',
-      displayColor: '3',
+export const Basic = {
+  args: {
+    message: {
+      oldName: 'JohnnyOldName',
+      user: {
+        displayName: 'JohnnyNewName',
+        displayColor: '3',
+      },
     },
   },
 };
diff --git a/web/components/chat/ChatPartMessage/ChatPartMessage.stories.tsx b/web/components/chat/ChatPartMessage/ChatPartMessage.stories.tsx
index 92cbb9a0c..de62a4bbe 100644
--- a/web/components/chat/ChatPartMessage/ChatPartMessage.stories.tsx
+++ b/web/components/chat/ChatPartMessage/ChatPartMessage.stories.tsx
@@ -1,9 +1,8 @@
-import React from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { Meta } from '@storybook/react';
 import { ChatPartMessage } from './ChatPartMessage';
 import Mock from '../../../stories/assets/mocks/chatmessage-action.png';
 
-export default {
+const meta = {
   title: 'owncast/Chat/Messages/Chat Part',
   component: ChatPartMessage,
   argTypes: {
@@ -23,20 +22,22 @@ export default {
       },
     },
   },
-} as ComponentMeta<typeof ChatPartMessage>;
+} satisfies Meta<typeof ChatPartMessage>;
 
-const Template: ComponentStory<typeof ChatPartMessage> = args => <ChatPartMessage {...args} />;
+export default meta;
 
-export const Regular = Template.bind({});
-Regular.args = {
-  displayName: 'RandomChatter',
-  isAuthorModerator: false,
-  userColor: 3,
+export const Regular = {
+  args: {
+    displayName: 'RandomChatter',
+    isAuthorModerator: false,
+    userColor: 3,
+  },
 };
 
-export const Moderator = Template.bind({});
-Moderator.args = {
-  displayName: 'RandomChatter',
-  isAuthorModerator: true,
-  userColor: 2,
+export const Moderator = {
+  args: {
+    displayName: 'RandomChatter',
+    isAuthorModerator: true,
+    userColor: 2,
+  },
 };
diff --git a/web/components/chat/ChatSocialMessage/ChatSocialMessage.stories.tsx b/web/components/chat/ChatSocialMessage/ChatSocialMessage.stories.tsx
index 51c51a938..39273d88f 100644
--- a/web/components/chat/ChatSocialMessage/ChatSocialMessage.stories.tsx
+++ b/web/components/chat/ChatSocialMessage/ChatSocialMessage.stories.tsx
@@ -1,66 +1,70 @@
-import React from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { Meta } from '@storybook/react';
 import { ChatSocialMessage } from './ChatSocialMessage';
 
-export default {
+const meta = {
   title: 'owncast/Chat/Messages/Social-fediverse event',
   component: ChatSocialMessage,
   parameters: {},
-} as ComponentMeta<typeof ChatSocialMessage>;
+} satisfies Meta<typeof ChatSocialMessage>;
 
-const Template: ComponentStory<typeof ChatSocialMessage> = args => <ChatSocialMessage {...args} />;
+export default meta;
 
-export const Follow = Template.bind({});
-Follow.args = {
-  message: {
-    type: 'FEDIVERSE_ENGAGEMENT_FOLLOW',
-    body: '<p>james followed this live stream.</p>',
-    title: 'james@mastodon.social',
-    image: 'https://mastodon.social/avatars/original/missing.png',
-    link: 'https://mastodon.social/@james',
+export const Follow = {
+  args: {
+    message: {
+      type: 'FEDIVERSE_ENGAGEMENT_FOLLOW',
+      body: '<p>james followed this live stream.</p>',
+      title: 'james@mastodon.social',
+      image: 'https://mastodon.social/avatars/original/missing.png',
+      link: 'https://mastodon.social/@james',
+    },
   },
 };
 
-export const Like = Template.bind({});
-Like.args = {
-  message: {
-    type: 'FEDIVERSE_ENGAGEMENT_LIKE',
-    body: '<p>james liked that this stream went live.</p>',
-    title: 'james@mastodon.social',
-    image: 'https://mastodon.social/avatars/original/missing.png',
-    link: 'https://mastodon.social/@james',
+export const Like = {
+  args: {
+    message: {
+      type: 'FEDIVERSE_ENGAGEMENT_LIKE',
+      body: '<p>james liked that this stream went live.</p>',
+      title: 'james@mastodon.social',
+      image: 'https://mastodon.social/avatars/original/missing.png',
+      link: 'https://mastodon.social/@james',
+    },
   },
 };
 
-export const Repost = Template.bind({});
-Repost.args = {
-  message: {
-    type: 'FEDIVERSE_ENGAGEMENT_REPOST',
-    body: '<p>james shared this stream with their followers.</p>',
-    title: 'james@mastodon.social',
-    image: 'https://mastodon.social/avatars/original/missing.png',
-    link: 'https://mastodon.social/@james',
+export const Repost = {
+  args: {
+    message: {
+      type: 'FEDIVERSE_ENGAGEMENT_REPOST',
+      body: '<p>james shared this stream with their followers.</p>',
+      title: 'james@mastodon.social',
+      image: 'https://mastodon.social/avatars/original/missing.png',
+      link: 'https://mastodon.social/@james',
+    },
   },
 };
 
-export const LongAccountName = Template.bind({});
-LongAccountName.args = {
-  message: {
-    type: 'FEDIVERSE_ENGAGEMENT_REPOST',
-    body: '<p>james shared this stream with their followers.</p>',
-    title: 'littlejimmywilliams@technology.biz.net.org.technology.gov',
-    image: 'https://mastodon.social/avatars/original/missing.png',
-    link: 'https://mastodon.social/@james',
+export const LongAccountName = {
+  args: {
+    message: {
+      type: 'FEDIVERSE_ENGAGEMENT_REPOST',
+      body: '<p>james shared this stream with their followers.</p>',
+      title: 'littlejimmywilliams@technology.biz.net.org.technology.gov',
+      image: 'https://mastodon.social/avatars/original/missing.png',
+      link: 'https://mastodon.social/@james',
+    },
   },
 };
 
-export const InvalidAvatarImage = Template.bind({});
-InvalidAvatarImage.args = {
-  message: {
-    type: 'FEDIVERSE_ENGAGEMENT_REPOST',
-    body: '<p>james shared this stream with their followers.</p>',
-    title: 'james@mastodon.social',
-    image: 'https://xx.xx/avatars/original/missing.png',
-    link: 'https://mastodon.social/@james',
+export const InvalidAvatarImage = {
+  args: {
+    message: {
+      type: 'FEDIVERSE_ENGAGEMENT_REPOST',
+      body: '<p>james shared this stream with their followers.</p>',
+      title: 'james@mastodon.social',
+      image: 'https://xx.xx/avatars/original/missing.png',
+      link: 'https://mastodon.social/@james',
+    },
   },
 };
diff --git a/web/components/chat/ChatSystemMessage/ChatSystemMessage.stories.tsx b/web/components/chat/ChatSystemMessage/ChatSystemMessage.stories.tsx
index b0c207ace..580f5f420 100644
--- a/web/components/chat/ChatSystemMessage/ChatSystemMessage.stories.tsx
+++ b/web/components/chat/ChatSystemMessage/ChatSystemMessage.stories.tsx
@@ -1,10 +1,9 @@
-import React from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { Meta } from '@storybook/react';
 import { ChatSystemMessage } from './ChatSystemMessage';
 import Mock from '../../../stories/assets/mocks/chatmessage-system.png';
 import { ChatMessage } from '../../../interfaces/chat-message.model';
 
-export default {
+const meta = {
   title: 'owncast/Chat/Messages/System',
   component: ChatSystemMessage,
   parameters: {
@@ -18,9 +17,9 @@ export default {
       },
     },
   },
-} as ComponentMeta<typeof ChatSystemMessage>;
+} satisfies Meta<typeof ChatSystemMessage>;
 
-const Template: ComponentStory<typeof ChatSystemMessage> = args => <ChatSystemMessage {...args} />;
+export default meta;
 
 const message: ChatMessage = JSON.parse(`{
   "type": "SYSTEM",
@@ -34,14 +33,15 @@ const message: ChatMessage = JSON.parse(`{
   },
   "body": "Test system message from the chat server."}`);
 
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-export const Basic = Template.bind({});
-Basic.args = {
-  message,
+export const Basic = {
+  args: {
+    message,
+  },
 };
 
-export const HighlightExample = Template.bind({});
-HighlightExample.args = {
-  message,
-  highlightString: 'chat',
+export const HighlightExample = {
+  args: {
+    message,
+    highlightString: 'chat',
+  },
 };
diff --git a/web/components/chat/ChatTextField/ChatTextField.stories.tsx b/web/components/chat/ChatTextField/ChatTextField.stories.tsx
index 5f1c7aea7..eb328f78a 100644
--- a/web/components/chat/ChatTextField/ChatTextField.stories.tsx
+++ b/web/components/chat/ChatTextField/ChatTextField.stories.tsx
@@ -1,5 +1,4 @@
-import React from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { StoryFn, Meta } from '@storybook/react';
 import { RecoilRoot } from 'recoil';
 import { ChatTextField } from './ChatTextField';
 import Mockup from '../../../stories/assets/mocks/chatinput-mock.png';
@@ -28,7 +27,7 @@ const mocks = {
   ],
 };
 
-export default {
+const meta = {
   title: 'owncast/Chat/Input text field',
   component: ChatTextField,
   parameters: {
@@ -48,43 +47,54 @@ export default {
       },
     },
   },
-} as ComponentMeta<typeof ChatTextField>;
+} satisfies Meta<typeof ChatTextField>;
 
-const Template: ComponentStory<typeof ChatTextField> = args => (
+export default meta;
+
+const Template: StoryFn<typeof ChatTextField> = args => (
   <RecoilRoot>
     <ChatTextField {...args} />
   </RecoilRoot>
 );
 
-export const Example = Template.bind({});
-Example.args = {
-  enabled: true,
-};
+export const Example = {
+  render: Template,
 
-export const LongerMessage = Template.bind({});
-LongerMessage.args = {
-  enabled: true,
-  defaultText:
-    'Lorem ipsum dolor sit amet,  consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
+  args: {
+    enabled: true,
+  },
 };
 
-LongerMessage.parameters = {
-  docs: {
-    description: {
-      story: 'Should display two lines of text and scroll to display more.',
+export const LongerMessage = {
+  render: Template,
+
+  args: {
+    enabled: true,
+    defaultText:
+      'Lorem ipsum dolor sit amet,  consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
+  },
+
+  parameters: {
+    docs: {
+      description: {
+        story: 'Should display two lines of text and scroll to display more.',
+      },
     },
   },
 };
 
-export const DisabledChat = Template.bind({});
-DisabledChat.args = {
-  enabled: false,
-};
+export const DisabledChat = {
+  render: Template,
+
+  args: {
+    enabled: false,
+  },
 
-DisabledChat.parameters = {
-  docs: {
-    description: {
-      story: 'Should not allow you to type anything and should state that chat is disabled.',
+  parameters: {
+    docs: {
+      description: {
+        story: 'Should not allow you to type anything and should state that chat is disabled.',
+      },
     },
   },
 };
diff --git a/web/components/chat/ChatUserBadge/ChatUserBadge.stories.tsx b/web/components/chat/ChatUserBadge/ChatUserBadge.stories.tsx
index 640c931dd..eaa7e495f 100644
--- a/web/components/chat/ChatUserBadge/ChatUserBadge.stories.tsx
+++ b/web/components/chat/ChatUserBadge/ChatUserBadge.stories.tsx
@@ -1,11 +1,10 @@
-import React from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { StoryFn, Meta } from '@storybook/react';
 import { ChatUserBadge } from './ChatUserBadge';
 import { ModerationBadge } from './ModerationBadge';
 import { AuthedUserBadge } from './AuthedUserBadge';
 import { BotUserBadge } from './BotUserBadge';
 
-export default {
+const meta = {
   title: 'owncast/Chat/Messages/User Flag',
   component: ChatUserBadge,
   argTypes: {
@@ -14,36 +13,43 @@ export default {
       control: { type: 'select' },
     },
   },
-} as ComponentMeta<typeof ChatUserBadge>;
+} satisfies Meta<typeof ChatUserBadge>;
 
-const Template: ComponentStory<typeof ChatUserBadge> = args => <ChatUserBadge {...args} />;
-const ModerationTemplate: ComponentStory<typeof ModerationBadge> = args => (
-  <ModerationBadge {...args} />
-);
+export default meta;
 
-const AuthedTemplate: ComponentStory<typeof ModerationBadge> = args => (
-  <AuthedUserBadge {...args} />
-);
+const ModerationTemplate: StoryFn<typeof ModerationBadge> = args => <ModerationBadge {...args} />;
 
-const BotTemplate: ComponentStory<typeof BotUserBadge> = args => <BotUserBadge {...args} />;
+const AuthedTemplate: StoryFn<typeof ModerationBadge> = args => <AuthedUserBadge {...args} />;
 
-export const Authenticated = AuthedTemplate.bind({});
-Authenticated.args = {
-  userColor: '3',
+const BotTemplate: StoryFn<typeof BotUserBadge> = args => <BotUserBadge {...args} />;
+
+export const Authenticated = {
+  render: AuthedTemplate,
+
+  args: {
+    userColor: '3',
+  },
 };
 
-export const Moderator = ModerationTemplate.bind({});
-Moderator.args = {
-  userColor: '5',
+export const Moderator = {
+  render: ModerationTemplate,
+
+  args: {
+    userColor: '5',
+  },
 };
 
-export const Bot = BotTemplate.bind({});
-Bot.args = {
-  userColor: '7',
+export const Bot = {
+  render: BotTemplate,
+
+  args: {
+    userColor: '7',
+  },
 };
 
-export const Generic = Template.bind({});
-Generic.args = {
-  badge: '?',
-  userColor: '6',
+export const Generic = {
+  args: {
+    badge: '?',
+    userColor: '6',
+  },
 };
diff --git a/web/components/chat/ChatUserMessage/ChatUserMessage.stories.tsx b/web/components/chat/ChatUserMessage/ChatUserMessage.stories.tsx
index 09829c166..22a9fdf48 100644
--- a/web/components/chat/ChatUserMessage/ChatUserMessage.stories.tsx
+++ b/web/components/chat/ChatUserMessage/ChatUserMessage.stories.tsx
@@ -1,11 +1,10 @@
-import React from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { StoryFn, Meta } from '@storybook/react';
 import { RecoilRoot } from 'recoil';
 import { ChatUserMessage } from './ChatUserMessage';
 import { ChatMessage } from '../../../interfaces/chat-message.model';
 import Mock from '../../../stories/assets/mocks/chatmessage-user.png';
 
-export default {
+const meta = {
   title: 'owncast/Chat/Messages/Standard user',
   component: ChatUserMessage,
   parameters: {
@@ -20,9 +19,11 @@ export default {
       },
     },
   },
-} as ComponentMeta<typeof ChatUserMessage>;
+} satisfies Meta<typeof ChatUserMessage>;
 
-const Template: ComponentStory<typeof ChatUserMessage> = args => (
+export default meta;
+
+const Template: StoryFn<typeof ChatUserMessage> = args => (
   <RecoilRoot>
     <ChatUserMessage {...args} />
   </RecoilRoot>
@@ -105,48 +106,69 @@ const botUserMessage: ChatMessage = JSON.parse(`{
 				},
 				"body": "I am a bot."}`);
 
-export const WithoutModeratorMenu = Template.bind({});
-WithoutModeratorMenu.args = {
-  message: standardMessage,
-  showModeratorMenu: false,
+export const WithoutModeratorMenu = {
+  render: Template,
+
+  args: {
+    message: standardMessage,
+    showModeratorMenu: false,
+  },
 };
 
-export const WithLinkAndCustomEmoji = Template.bind({});
-WithLinkAndCustomEmoji.args = {
-  message: messageWithLinkAndCustomEmoji,
-  showModeratorMenu: false,
+export const WithLinkAndCustomEmoji = {
+  render: Template,
+
+  args: {
+    message: messageWithLinkAndCustomEmoji,
+    showModeratorMenu: false,
+  },
 };
 
-export const WithModeratorMenu = Template.bind({});
-WithModeratorMenu.args = {
-  message: standardMessage,
-  showModeratorMenu: true,
+export const WithModeratorMenu = {
+  render: Template,
+
+  args: {
+    message: standardMessage,
+    showModeratorMenu: true,
+  },
 };
 
-export const FromModeratorUser = Template.bind({});
-FromModeratorUser.args = {
-  message: moderatorMessage,
-  showModeratorMenu: false,
-  isAuthorModerator: true,
+export const FromModeratorUser = {
+  render: Template,
+
+  args: {
+    message: moderatorMessage,
+    showModeratorMenu: false,
+    isAuthorModerator: true,
+  },
 };
 
-export const FromAuthenticatedUser = Template.bind({});
-FromAuthenticatedUser.args = {
-  message: authenticatedUserMessage,
-  showModeratorMenu: false,
-  isAuthorAuthenticated: true,
+export const FromAuthenticatedUser = {
+  render: Template,
+
+  args: {
+    message: authenticatedUserMessage,
+    showModeratorMenu: false,
+    isAuthorAuthenticated: true,
+  },
 };
 
-export const FromBotUser = Template.bind({});
-FromBotUser.args = {
-  message: botUserMessage,
-  showModeratorMenu: false,
-  isAuthorBot: true,
+export const FromBotUser = {
+  render: Template,
+
+  args: {
+    message: botUserMessage,
+    showModeratorMenu: false,
+    isAuthorBot: true,
+  },
 };
 
-export const WithStringHighlighted = Template.bind({});
-WithStringHighlighted.args = {
-  message: standardMessage,
-  showModeratorMenu: false,
-  highlightString: 'message',
+export const WithStringHighlighted = {
+  render: Template,
+
+  args: {
+    message: standardMessage,
+    showModeratorMenu: false,
+    highlightString: 'message',
+  },
 };
diff --git a/web/components/common/ContentHeader/ContentHeader.stories.tsx b/web/components/common/ContentHeader/ContentHeader.stories.tsx
index dd313ffb2..b98496f09 100644
--- a/web/components/common/ContentHeader/ContentHeader.stories.tsx
+++ b/web/components/common/ContentHeader/ContentHeader.stories.tsx
@@ -1,76 +1,77 @@
-import React from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { Meta } from '@storybook/react';
 import { ContentHeader } from './ContentHeader';
 
-export default {
+const meta = {
   title: 'owncast/Components/Content Header',
   component: ContentHeader,
   parameters: {},
-} as ComponentMeta<typeof ContentHeader>;
+} satisfies Meta<typeof ContentHeader>;
 
-const Template: ComponentStory<typeof ContentHeader> = args => <ContentHeader {...args} />;
+export default meta;
 
-export const Example = Template.bind({});
-Example.args = {
-  name: 'My Awesome Owncast Stream',
-  summary: 'A calvacade of glorious sights and sounds',
-  tags: ['word', 'tag with spaces', 'music'],
-  logo: 'https://watch.owncast.online/logo',
-  links: [
-    {
-      platform: 'github',
-      url: 'https://github.com/owncast/owncast',
-      icon: 'https://watch.owncast.online/img/platformlogos/github.svg',
-    },
-    {
-      platform: 'Documentation',
-      url: 'https://owncast.online',
-      icon: 'https://watch.owncast.online/img/platformlogos/link.svg',
-    },
-    {
-      platform: 'mastodon',
-      url: 'https://fosstodon.org/users/owncast',
-      icon: 'https://watch.owncast.online/img/platformlogos/mastodon.svg',
-    },
-  ],
+export const Example = {
+  args: {
+    name: 'My Awesome Owncast Stream',
+    summary: 'A calvacade of glorious sights and sounds',
+    tags: ['word', 'tag with spaces', 'music'],
+    logo: 'https://watch.owncast.online/logo',
+    links: [
+      {
+        platform: 'github',
+        url: 'https://github.com/owncast/owncast',
+        icon: 'https://watch.owncast.online/img/platformlogos/github.svg',
+      },
+      {
+        platform: 'Documentation',
+        url: 'https://owncast.online',
+        icon: 'https://watch.owncast.online/img/platformlogos/link.svg',
+      },
+      {
+        platform: 'mastodon',
+        url: 'https://fosstodon.org/users/owncast',
+        icon: 'https://watch.owncast.online/img/platformlogos/mastodon.svg',
+      },
+    ],
+  },
 };
 
-export const LongContent = Template.bind({});
-LongContent.args = {
-  name: 'My Awesome Owncast Stream, streaming the best of streams and some lorem ipsum too',
-  summary:
-    'A calvacade of glorious sights and sounds. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
-  tags: [
-    'word',
-    'tag with spaces',
-    'music',
-    'more tags',
-    'a bunch',
-    'keep going',
-    'and more',
-    'just a few more',
-    'video games',
-    'things',
-    'stuff',
-    'ok some more',
-    'this should do it',
-  ],
-  logo: 'https://watch.owncast.online/logo',
-  links: [
-    {
-      platform: 'github',
-      url: 'https://github.com/owncast/owncast',
-      icon: 'https://watch.owncast.online/img/platformlogos/github.svg',
-    },
-    {
-      platform: 'Documentation',
-      url: 'https://owncast.online',
-      icon: 'https://watch.owncast.online/img/platformlogos/link.svg',
-    },
-    {
-      platform: 'mastodon',
-      url: 'https://fosstodon.org/users/owncast',
-      icon: 'https://watch.owncast.online/img/platformlogos/mastodon.svg',
-    },
-  ],
+export const LongContent = {
+  args: {
+    name: 'My Awesome Owncast Stream, streaming the best of streams and some lorem ipsum too',
+    summary:
+      'A calvacade of glorious sights and sounds. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
+    tags: [
+      'word',
+      'tag with spaces',
+      'music',
+      'more tags',
+      'a bunch',
+      'keep going',
+      'and more',
+      'just a few more',
+      'video games',
+      'things',
+      'stuff',
+      'ok some more',
+      'this should do it',
+    ],
+    logo: 'https://watch.owncast.online/logo',
+    links: [
+      {
+        platform: 'github',
+        url: 'https://github.com/owncast/owncast',
+        icon: 'https://watch.owncast.online/img/platformlogos/github.svg',
+      },
+      {
+        platform: 'Documentation',
+        url: 'https://owncast.online',
+        icon: 'https://watch.owncast.online/img/platformlogos/link.svg',
+      },
+      {
+        platform: 'mastodon',
+        url: 'https://fosstodon.org/users/owncast',
+        icon: 'https://watch.owncast.online/img/platformlogos/mastodon.svg',
+      },
+    ],
+  },
 };
diff --git a/web/components/common/OwncastLogo/OwncastLogo.stories.tsx b/web/components/common/OwncastLogo/OwncastLogo.stories.tsx
index 3ceaabb4e..3481f26ad 100644
--- a/web/components/common/OwncastLogo/OwncastLogo.stories.tsx
+++ b/web/components/common/OwncastLogo/OwncastLogo.stories.tsx
@@ -1,25 +1,24 @@
-import React from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { Meta } from '@storybook/react';
 import { OwncastLogo } from './OwncastLogo';
 
-export default {
+const meta = {
   title: 'owncast/Components/Header Logo',
   component: OwncastLogo,
   parameters: {
     chromatic: { diffThreshold: 0.8 },
   },
-} as ComponentMeta<typeof OwncastLogo>;
+} satisfies Meta<typeof OwncastLogo>;
 
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-const Template: ComponentStory<typeof OwncastLogo> = args => <OwncastLogo {...args} />;
+export default meta;
 
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-export const Logo = Template.bind({});
-Logo.args = {
-  url: '/logo',
+export const Logo = {
+  args: {
+    url: '/logo',
+  },
 };
 
-export const DemoServer = Template.bind({});
-DemoServer.args = {
-  url: 'https://watch.owncast.online/logo',
+export const DemoServer = {
+  args: {
+    url: 'https://watch.owncast.online/logo',
+  },
 };
diff --git a/web/components/common/UserDropdown/UserDropdown.stories.tsx b/web/components/common/UserDropdown/UserDropdown.stories.tsx
index 019452b83..6c907f295 100644
--- a/web/components/common/UserDropdown/UserDropdown.stories.tsx
+++ b/web/components/common/UserDropdown/UserDropdown.stories.tsx
@@ -1,15 +1,17 @@
-import React, { useEffect } from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { useEffect } from 'react';
+import { StoryFn, Meta } from '@storybook/react';
 import { RecoilRoot, useSetRecoilState } from 'recoil';
 import { UserDropdown } from './UserDropdown';
 import { CurrentUser } from '../../../interfaces/current-user';
 import { currentUserAtom } from '../../stores/ClientConfigStore';
 
-export default {
+const meta = {
   title: 'owncast/Components/User settings menu',
   component: UserDropdown,
   parameters: {},
-} as ComponentMeta<typeof UserDropdown>;
+} satisfies Meta<typeof UserDropdown>;
+
+export default meta;
 
 // This component uses Recoil internally so wrap it in a RecoilRoot.
 const Example = args => {
@@ -29,13 +31,16 @@ const Example = args => {
   return <UserDropdown id="user-menu" {...args} />;
 };
 
-const Template: ComponentStory<typeof UserDropdown> = args => (
+const Template: StoryFn<typeof UserDropdown> = args => (
   <RecoilRoot>
     <Example {...args} />
   </RecoilRoot>
 );
 
-export const ChatEnabled = Template.bind({});
-ChatEnabled.args = {
-  username: 'test-user',
+export const ChatEnabled = {
+  render: Template,
+
+  args: {
+    username: 'test-user',
+  },
 };
diff --git a/web/components/layouts/Main/Main.stories.tsx b/web/components/layouts/Main/Main.stories.tsx
index 417dcae6c..fccf6329c 100644
--- a/web/components/layouts/Main/Main.stories.tsx
+++ b/web/components/layouts/Main/Main.stories.tsx
@@ -1,4 +1,4 @@
-import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { Meta, StoryFn, StoryObj } from '@storybook/react';
 import { MutableSnapshot, RecoilRoot } from 'recoil';
 import { makeEmptyClientConfig } from '../../../interfaces/client-config.model';
 import { ServerStatus, makeEmptyServerStatus } from '../../../interfaces/server-status.model';
@@ -30,12 +30,14 @@ import videoSettingsServiceMockOf from '../../../services/video-settings-service
 import { spidermanUser } from '../../../interfaces/user.fixture';
 import { exampleChatHistory } from '../../../interfaces/chat-message.fixture';
 
-export default {
+const meta = {
   title: 'owncast/Layout/Main',
   parameters: {
     layout: 'fullscreen',
   },
-} satisfies ComponentMeta<typeof Main>;
+} satisfies Meta<typeof Main>;
+
+export default meta;
 
 // mock the Websocket to prevent ani webhook calls from being made in storybook
 // @ts-ignore
@@ -92,7 +94,7 @@ const DefaultServerStatusServiceMock = serverStatusServiceMockOf(defaultServerSt
 const OnlineServerStatusServiceMock = serverStatusServiceMockOf(onlineServerStatus);
 const VideoSettingsServiceMock = videoSettingsServiceMockOf([]);
 
-const Template: ComponentStory<typeof Main> = ({
+const Template: StoryFn<typeof Main> = ({
   initializeState,
   ServerStatusServiceMock = DefaultServerStatusServiceMock,
   ...args
@@ -113,57 +115,79 @@ const Template: ComponentStory<typeof Main> = ({
   </RecoilRoot>
 );
 
-export const OfflineDesktop: typeof Template = Template.bind({});
-OfflineDesktop.parameters = {
-  chromatic: { diffThreshold: 0.88 },
-};
+export const OfflineDesktop: StoryObj<typeof Template> = {
+  render: Template,
 
-export const OfflineMobile: typeof Template = Template.bind({});
-OfflineMobile.args = {
-  initializeState: (mutableState: MutableSnapshot) => {
-    mutableState.set(isMobileAtom, true);
+  parameters: {
+    chromatic: { diffThreshold: 0.88 },
   },
 };
-OfflineMobile.parameters = {
-  viewport: {
-    defaultViewport: 'mobile1',
+
+export const OfflineMobile: StoryObj<typeof Template> = {
+  render: Template,
+
+  args: {
+    initializeState: (mutableState: MutableSnapshot) => {
+      mutableState.set(isMobileAtom, true);
+    },
   },
-};
 
-export const OfflineTablet: typeof Template = Template.bind({});
-OfflineTablet.parameters = {
-  viewport: {
-    defaultViewport: 'tablet',
+  parameters: {
+    viewport: {
+      defaultViewport: 'mobile1',
+    },
   },
 };
 
-export const Online: typeof Template = Template.bind({});
-Online.args = {
-  ServerStatusServiceMock: OnlineServerStatusServiceMock,
-};
-Online.parameters = {
-  chromatic: { diffThreshold: 0.88 },
-};
+export const OfflineTablet: StoryObj<typeof Template> = {
+  render: Template,
 
-export const OnlineMobile: typeof Template = Online.bind({});
-OnlineMobile.args = {
-  ServerStatusServiceMock: OnlineServerStatusServiceMock,
-  initializeState: (mutableState: MutableSnapshot) => {
-    mutableState.set(isMobileAtom, true);
+  parameters: {
+    viewport: {
+      defaultViewport: 'tablet',
+    },
   },
 };
-OnlineMobile.parameters = {
-  viewport: {
-    defaultViewport: 'mobile1',
+
+export const Online: StoryObj<typeof Template> = {
+  render: Template,
+
+  args: {
+    ServerStatusServiceMock: OnlineServerStatusServiceMock,
+  },
+
+  parameters: {
+    chromatic: { diffThreshold: 0.88 },
   },
 };
 
-export const OnlineTablet: typeof Template = Online.bind({});
-OnlineTablet.args = {
-  ServerStatusServiceMock: OnlineServerStatusServiceMock,
+export const OnlineMobile: StoryObj<typeof Template> = {
+  render: Template,
+
+  args: {
+    ServerStatusServiceMock: OnlineServerStatusServiceMock,
+    initializeState: (mutableState: MutableSnapshot) => {
+      mutableState.set(isMobileAtom, true);
+    },
+  },
+
+  parameters: {
+    viewport: {
+      defaultViewport: 'mobile1',
+    },
+  },
 };
-OnlineTablet.parameters = {
-  viewport: {
-    defaultViewport: 'tablet',
+
+export const OnlineTablet: StoryObj<typeof Template> = {
+  render: Template,
+
+  args: {
+    ServerStatusServiceMock: OnlineServerStatusServiceMock,
+  },
+
+  parameters: {
+    viewport: {
+      defaultViewport: 'tablet',
+    },
   },
 };
diff --git a/web/components/modals/AuthModal/AuthModal.stories.tsx b/web/components/modals/AuthModal/AuthModal.stories.tsx
index 5906a2faa..ed5c252fa 100644
--- a/web/components/modals/AuthModal/AuthModal.stories.tsx
+++ b/web/components/modals/AuthModal/AuthModal.stories.tsx
@@ -1,5 +1,5 @@
-import React, { useEffect } from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { useEffect } from 'react';
+import { StoryFn, Meta } from '@storybook/react';
 import { RecoilRoot, useSetRecoilState } from 'recoil';
 import { AuthModal } from './AuthModal';
 import { currentUserAtom } from '../../stores/ClientConfigStore';
@@ -26,17 +26,20 @@ const Example = () => {
   );
 };
 
-export default {
+const meta = {
   title: 'owncast/Modals/Auth',
   component: AuthModal,
   parameters: {},
-} as ComponentMeta<typeof AuthModal>;
+} satisfies Meta<typeof AuthModal>;
 
-const Template: ComponentStory<typeof AuthModal> = () => (
+export default meta;
+
+const Template: StoryFn<typeof AuthModal> = () => (
   <RecoilRoot>
     <Example />
   </RecoilRoot>
 );
 
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-export const Basic = Template.bind({});
+export const Basic = {
+  render: Template,
+};
diff --git a/web/components/modals/BrowserNotifyModal/BrowserNotifyModal.stories.tsx b/web/components/modals/BrowserNotifyModal/BrowserNotifyModal.stories.tsx
index 15110182f..090db1247 100644
--- a/web/components/modals/BrowserNotifyModal/BrowserNotifyModal.stories.tsx
+++ b/web/components/modals/BrowserNotifyModal/BrowserNotifyModal.stories.tsx
@@ -1,5 +1,4 @@
-import React from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { StoryFn, Meta } from '@storybook/react';
 import { RecoilRoot } from 'recoil';
 import { BrowserNotifyModal } from './BrowserNotifyModal';
 import BrowserNotifyModalMock from '../../../stories/assets/mocks/notify-modal.png';
@@ -10,7 +9,7 @@ const Example = () => (
   </div>
 );
 
-export default {
+const meta = {
   title: 'owncast/Modals/Browser Notifications',
   component: BrowserNotifyModal,
   parameters: {
@@ -31,14 +30,16 @@ export default {
       },
     },
   },
-} as ComponentMeta<typeof BrowserNotifyModal>;
+} satisfies Meta<typeof BrowserNotifyModal>;
 
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-const Template: ComponentStory<typeof BrowserNotifyModal> = () => (
+export default meta;
+
+const Template: StoryFn<typeof BrowserNotifyModal> = () => (
   <RecoilRoot>
     <Example />
   </RecoilRoot>
 );
 
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-export const Basic = Template.bind({});
+export const Basic = {
+  render: Template,
+};
diff --git a/web/components/modals/ChatModal/ChatModal.stories.tsx b/web/components/modals/ChatModal/ChatModal.stories.tsx
index bcf7e31e2..50f3f8e70 100644
--- a/web/components/modals/ChatModal/ChatModal.stories.tsx
+++ b/web/components/modals/ChatModal/ChatModal.stories.tsx
@@ -1,21 +1,21 @@
-/* eslint-disable object-shorthand */
-
-import React, { useEffect } from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { useEffect } from 'react';
+import { StoryFn, Meta } from '@storybook/react';
 import { RecoilRoot, useSetRecoilState } from 'recoil';
 import { ChatModal, ChatModalProps } from './ChatModal';
 import { ChatMessage } from '../../../interfaces/chat-message.model';
 import { CurrentUser } from '../../../interfaces/current-user';
 import { currentUserAtom } from '../../stores/ClientConfigStore';
 
-export default {
+const meta = {
   title: 'owncast/Chat/Chat modal',
   component: ChatModal,
   parameters: {
     chromatic: { diffThreshold: 0.8 },
     docs: {},
   },
-} as ComponentMeta<typeof ChatModal>;
+} satisfies Meta<typeof ChatModal>;
+
+export default meta;
 
 const testMessages = `[
 		{
@@ -578,21 +578,24 @@ const Component = args => {
   return <ChatModal {...args} />;
 };
 
-const Template: ComponentStory<typeof ChatModal> = args => (
+const Template: StoryFn<typeof ChatModal> = args => (
   <RecoilRoot>
     <Component {...args} />
   </RecoilRoot>
 );
 
-export const Example = Template.bind({});
-Example.args = {
-  loading: false,
-  messages: messages,
-  usernameToHighlight: 'testuser',
-  chatUserId: 'testuser',
-  isModerator: true,
-  showInput: true,
-  chatAvailable: true,
-  handleClose: () => {},
-  currentUser: currentUser,
-} as ChatModalProps;
+export const Example = {
+  render: Template,
+
+  args: {
+    loading: false,
+    messages,
+    usernameToHighlight: 'testuser',
+    chatUserId: 'testuser',
+    isModerator: true,
+    showInput: true,
+    chatAvailable: true,
+    handleClose: () => {},
+    currentUser,
+  } as ChatModalProps,
+};
diff --git a/web/components/modals/FatalErrorStateModal/FatalErrorStateModal.stories.tsx b/web/components/modals/FatalErrorStateModal/FatalErrorStateModal.stories.tsx
index c68817e56..6a0d592f0 100644
--- a/web/components/modals/FatalErrorStateModal/FatalErrorStateModal.stories.tsx
+++ b/web/components/modals/FatalErrorStateModal/FatalErrorStateModal.stories.tsx
@@ -1,21 +1,17 @@
-import React from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { Meta } from '@storybook/react';
 import { FatalErrorStateModal } from './FatalErrorStateModal';
 
-export default {
+const meta = {
   title: 'owncast/Modals/Global error state',
   component: FatalErrorStateModal,
   parameters: {},
-} as ComponentMeta<typeof FatalErrorStateModal>;
+} satisfies Meta<typeof FatalErrorStateModal>;
 
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-const Template: ComponentStory<typeof FatalErrorStateModal> = args => (
-  <FatalErrorStateModal {...args} />
-);
+export default meta;
 
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-export const Example = Template.bind({});
-Example.args = {
-  title: 'Example error title',
-  message: 'Example error message',
+export const Example = {
+  args: {
+    title: 'Example error title',
+    message: 'Example error message',
+  },
 };
diff --git a/web/components/modals/FediAuthModal/FediAuthModal.stories.tsx b/web/components/modals/FediAuthModal/FediAuthModal.stories.tsx
index b6274c500..ffde12fdd 100644
--- a/web/components/modals/FediAuthModal/FediAuthModal.stories.tsx
+++ b/web/components/modals/FediAuthModal/FediAuthModal.stories.tsx
@@ -1,9 +1,8 @@
-import React from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { Meta } from '@storybook/react';
 import { FediAuthModal } from './FediAuthModal';
 import FediAuthModalMock from '../../../stories/assets/mocks/fediauth-modal.png';
 
-export default {
+const meta = {
   title: 'owncast/Modals/FediAuth',
   component: FediAuthModal,
   parameters: {
@@ -13,20 +12,22 @@ export default {
       scale: 0.5,
     },
   },
-} as ComponentMeta<typeof FediAuthModal>;
+} satisfies Meta<typeof FediAuthModal>;
 
-const Template: ComponentStory<typeof FediAuthModal> = args => <FediAuthModal {...args} />;
+export default meta;
 
-export const NotYetAuthenticated = Template.bind({});
-NotYetAuthenticated.args = {
-  displayName: 'fake-user',
-  authenticated: false,
-  accessToken: '',
+export const NotYetAuthenticated = {
+  args: {
+    displayName: 'fake-user',
+    authenticated: false,
+    accessToken: '',
+  },
 };
 
-export const Authenticated = Template.bind({});
-Authenticated.args = {
-  displayName: 'fake-user',
-  authenticated: true,
-  accessToken: '',
+export const Authenticated = {
+  args: {
+    displayName: 'fake-user',
+    authenticated: true,
+    accessToken: '',
+  },
 };
diff --git a/web/components/modals/FollowModal/FollowModal.stories.tsx b/web/components/modals/FollowModal/FollowModal.stories.tsx
index d535ec5ac..001b7d6a9 100644
--- a/web/components/modals/FollowModal/FollowModal.stories.tsx
+++ b/web/components/modals/FollowModal/FollowModal.stories.tsx
@@ -1,5 +1,4 @@
-import React from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { StoryFn, Meta } from '@storybook/react';
 import { FollowModal } from './FollowModal';
 import FollowModalMock from '../../../stories/assets/mocks/follow-modal.png';
 
@@ -9,7 +8,7 @@ const Example = () => (
   </div>
 );
 
-export default {
+const meta = {
   title: 'owncast/Modals/Follow',
   component: FollowModal,
   parameters: {
@@ -29,10 +28,12 @@ export default {
       },
     },
   },
-} as ComponentMeta<typeof FollowModal>;
+} satisfies Meta<typeof FollowModal>;
 
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-const Template: ComponentStory<typeof FollowModal> = () => <Example />;
+export default meta;
 
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-export const Basic = Template.bind({});
+const Template: StoryFn<typeof FollowModal> = () => <Example />;
+
+export const Basic = {
+  render: Template,
+};
diff --git a/web/components/modals/IndieAuthModal/IndieAuthModal.stories.tsx b/web/components/modals/IndieAuthModal/IndieAuthModal.stories.tsx
index a4618fc93..a006667dc 100644
--- a/web/components/modals/IndieAuthModal/IndieAuthModal.stories.tsx
+++ b/web/components/modals/IndieAuthModal/IndieAuthModal.stories.tsx
@@ -1,5 +1,4 @@
-import React from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { StoryFn, Meta } from '@storybook/react';
 import { IndieAuthModal } from './IndieAuthModal';
 import Mock from '../../../stories/assets/mocks/indieauth-modal.png';
 
@@ -9,7 +8,7 @@ const Example = () => (
   </div>
 );
 
-export default {
+const meta = {
   title: 'owncast/Modals/IndieAuth',
   component: IndieAuthModal,
   parameters: {
@@ -19,10 +18,12 @@ export default {
       scale: 0.5,
     },
   },
-} as ComponentMeta<typeof IndieAuthModal>;
+} satisfies Meta<typeof IndieAuthModal>;
 
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-const Template: ComponentStory<typeof IndieAuthModal> = () => <Example />;
+export default meta;
 
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-export const Basic = Template.bind({});
+const Template: StoryFn<typeof IndieAuthModal> = () => <Example />;
+
+export const Basic = {
+  render: Template,
+};
diff --git a/web/components/modals/NameChangeModal/NameChangeModal.stories.tsx b/web/components/modals/NameChangeModal/NameChangeModal.stories.tsx
index ce9175f2b..1630d8864 100644
--- a/web/components/modals/NameChangeModal/NameChangeModal.stories.tsx
+++ b/web/components/modals/NameChangeModal/NameChangeModal.stories.tsx
@@ -1,15 +1,17 @@
-import React, { useEffect } from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { useEffect } from 'react';
+import { StoryFn, Meta } from '@storybook/react';
 import { RecoilRoot, useSetRecoilState } from 'recoil';
 import { NameChangeModal } from './NameChangeModal';
 import { CurrentUser } from '../../../interfaces/current-user';
 import { currentUserAtom } from '../../stores/ClientConfigStore';
 
-export default {
+const meta = {
   title: 'owncast/Modals/Name Change',
   component: NameChangeModal,
   parameters: {},
-} as ComponentMeta<typeof NameChangeModal>;
+} satisfies Meta<typeof NameChangeModal>;
+
+export default meta;
 
 const Example = () => {
   const setCurrentUser = useSetRecoilState<CurrentUser>(currentUserAtom);
@@ -32,10 +34,12 @@ const Example = () => {
   );
 };
 
-const Template: ComponentStory<typeof NameChangeModal> = () => (
+const Template: StoryFn<typeof NameChangeModal> = () => (
   <RecoilRoot>
     <Example />
   </RecoilRoot>
 );
 
-export const Basic = Template.bind({});
+export const Basic = {
+  render: Template,
+};
diff --git a/web/components/ui/ComponentError/ComponentError.stories.tsx b/web/components/ui/ComponentError/ComponentError.stories.tsx
index 7087e82d5..8f011bda8 100644
--- a/web/components/ui/ComponentError/ComponentError.stories.tsx
+++ b/web/components/ui/ComponentError/ComponentError.stories.tsx
@@ -1,8 +1,7 @@
-import React from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { Meta } from '@storybook/react';
 import { ComponentError } from './ComponentError';
 
-export default {
+const meta = {
   title: 'owncast/Components/Component Error',
   component: ComponentError,
   parameters: {
@@ -12,31 +11,35 @@ export default {
       },
     },
   },
-} as ComponentMeta<typeof ComponentError>;
+} satisfies Meta<typeof ComponentError>;
 
-const Template: ComponentStory<typeof ComponentError> = args => <ComponentError {...args} />;
+export default meta;
 
-export const DefaultMessage = Template.bind({});
-DefaultMessage.args = {
-  componentName: 'Test Component',
+export const DefaultMessage = {
+  args: {
+    componentName: 'Test Component',
+  },
 };
 
-export const Error1 = Template.bind({});
-Error1.args = { message: 'This is a test error message.', componentName: 'Test Component' };
+export const Error1 = {
+  args: { message: 'This is a test error message.', componentName: 'Test Component' },
+};
 
-export const WithDetails = Template.bind({});
-WithDetails.args = {
-  message: 'This is a test error message.',
-  componentName: 'Test Component',
-  details: 'Here are some additional details about the error.',
+export const WithDetails = {
+  args: {
+    message: 'This is a test error message.',
+    componentName: 'Test Component',
+    details: 'Here are some additional details about the error.',
+  },
 };
 
-export const CanRetry = Template.bind({});
-CanRetry.args = {
-  message: 'This is a test error message.',
-  componentName: 'Test Component',
-  details: 'Here are some additional details about the error.',
-  retryFunction: () => {
-    console.log('retrying');
+export const CanRetry = {
+  args: {
+    message: 'This is a test error message.',
+    componentName: 'Test Component',
+    details: 'Here are some additional details about the error.',
+    retryFunction: () => {
+      console.log('retrying');
+    },
   },
 };
diff --git a/web/components/ui/CustomPageContent/CustomPageContent.stories.tsx b/web/components/ui/CustomPageContent/CustomPageContent.stories.tsx
index 22abfdda1..d50269630 100644
--- a/web/components/ui/CustomPageContent/CustomPageContent.stories.tsx
+++ b/web/components/ui/CustomPageContent/CustomPageContent.stories.tsx
@@ -1,32 +1,42 @@
 /* eslint-disable no-useless-escape */
-import React from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { StoryFn, Meta } from '@storybook/react';
 import { RecoilRoot } from 'recoil';
 import { CustomPageContent } from './CustomPageContent';
 
-export default {
+const meta = {
   title: 'owncast/Components/Custom page content',
   component: CustomPageContent,
   parameters: {},
-} as ComponentMeta<typeof CustomPageContent>;
+} satisfies Meta<typeof CustomPageContent>;
 
-const Template: ComponentStory<typeof CustomPageContent> = args => (
+export default meta;
+
+const Template: StoryFn<typeof CustomPageContent> = args => (
   <RecoilRoot>
     <CustomPageContent {...args} />
   </RecoilRoot>
 );
 
-export const Example1 = Template.bind({});
-Example1.args = {
-  content: `\u003cp\u003eOwncast TV is a 24/7 live stream run by the Owncast project as an example of the software in use. Learn more about how you can have your own live stream that you completely control at \u003ca href=\"https://owncast.online\"\u003eowncast.online\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eThis example instance shows how you can customize the page by changing things like fonts and colors as well as how you can add custom action buttons such as a donation button.\u003c/p\u003e\n\u003cp\u003eStay tuned in to learn about Owncast, hear from some streamers about their experiences using it, some bits and pieces of Owncast promo material, and highlights from other projects that are pretty cool.\u003c/p\u003e\n\u003cp\u003eBut when you've seen what we have to share with you, do yourself a favor and visit the \u003ca href=\"https://directory.owncast.online\"\u003eOwncast Directory\u003c/a\u003e and find an awesome stream to check out!\u003c/p\u003e\n\u003chr\u003e\n\u003ch2\u003eLinks to content seen in this stream\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://owncast.online/quickstart/\"\u003eOwncast Install Quickstart\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://directory.owncast.online\"\u003eOwncast Directory\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://creativecommons.org\"\u003eCreative Commons\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tilvids.com\"\u003eTILVids\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://studio.blender.org/\"\u003eBlender Studio\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://archive.org/details/computerchronicles\"\u003eComputer Chronicles\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://joinmastodon.org\"\u003eMastodon\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e",`,
+export const Example1 = {
+  render: Template,
+
+  args: {
+    content: `\u003cp\u003eOwncast TV is a 24/7 live stream run by the Owncast project as an example of the software in use. Learn more about how you can have your own live stream that you completely control at \u003ca href=\"https://owncast.online\"\u003eowncast.online\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eThis example instance shows how you can customize the page by changing things like fonts and colors as well as how you can add custom action buttons such as a donation button.\u003c/p\u003e\n\u003cp\u003eStay tuned in to learn about Owncast, hear from some streamers about their experiences using it, some bits and pieces of Owncast promo material, and highlights from other projects that are pretty cool.\u003c/p\u003e\n\u003cp\u003eBut when you've seen what we have to share with you, do yourself a favor and visit the \u003ca href=\"https://directory.owncast.online\"\u003eOwncast Directory\u003c/a\u003e and find an awesome stream to check out!\u003c/p\u003e\n\u003chr\u003e\n\u003ch2\u003eLinks to content seen in this stream\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"https://owncast.online/quickstart/\"\u003eOwncast Install Quickstart\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://directory.owncast.online\"\u003eOwncast Directory\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://creativecommons.org\"\u003eCreative Commons\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://tilvids.com\"\u003eTILVids\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://studio.blender.org/\"\u003eBlender Studio\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://archive.org/details/computerchronicles\"\u003eComputer Chronicles\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://joinmastodon.org\"\u003eMastodon\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e",`,
+  },
 };
 
-export const Example2 = Template.bind({});
-Example2.args = {
-  content: `<h1>WHAT IS HAPPENING HERE</h1>\n<p>Game That Tune Radio is live with fantastic video game music streaming around the clock! We've got music from NES, SNES, Sega Genesis, Nintendo 64, Playstation, PC, and more coming all the time! If it's been featured on our podcast, it's gonna be on this stream! We only play three songs from each game on our podcast, and we decided that everyone needs more tunes!</p>\n<p>We'll be updating this livestream with new games as they're played on the show, including your requests! To get priority in requesting games for the show, check out <a href=\"https://www.patreon.com/GameThatTune\">https://www.patreon.com/GameThatTune</a></p>\n<p>Be sure to check out our live recordings of the Game That Tune podcast! We broadcast every Wednesday night at 9 PM EST on our YouTube channel as well as <a href=\"https://www.twitch.tv/GameThatTune\">https://www.twitch.tv/GameThatTune</a> and <a href=\"https://www.facebook.com/GameThatTune\">https://www.facebook.com/GameThatTune</a>\nTune in and join in on the fun! Find the podcast in iTunes every Wednesday morning or head to <a href=\"https://www.gamethattune.com\">https://www.gamethattune.com</a>!</p>\n<p>Visit <a href=\"https://www.patreon.com/GameThatTune\">https://www.patreon.com/GameThatTune</a> to help us keep up this live stream and upgrade our equipment for the live show! We've got exclusive mixtapes for our patrons, and lots more stuff planned for the future, so consider helping us out!</p>\n<h1>HOW IT WORKS</h1>\n<p>Featuring music from over 1000 games! Check out <a href=\"https://music.gamethattune.com/songs\">https://music.gamethattune.com/songs</a> for the full list and make a request from your favorite game!</p>\n<p>Now that you've seen the list of games, make a request in the chat!</p>\n<p><code>!sr</code> + anything = general search<br>\n<code>!gr</code> + game title = random song from matching game<br>\n<code>!cr</code> + composer name = random song from matching composer<br>\n<code>!tr</code> + anything = random result only searching song titles<br>\n<code>!rr</code> + anything = random result from all searchable fields<br>\n<code>!game gtt</code> = starts a round of our guessing game for bonus points!</p>\n<p>We have gifs!</p>\n<p>Wanna see your favorite gif on screen? type <code>!summon</code> followed by the gif name! Want your favorite gif to take over the video? Type <code>!spawn</code> followed by the gif name!</p>\n<p>Still have questions? Ask the chatbot! type <code>!info</code> to...wait for it...get more info!</p>\n<p>Thanks for listening!</p>"`,
+export const Example2 = {
+  render: Template,
+
+  args: {
+    content: `<h1>WHAT IS HAPPENING HERE</h1>\n<p>Game That Tune Radio is live with fantastic video game music streaming around the clock! We've got music from NES, SNES, Sega Genesis, Nintendo 64, Playstation, PC, and more coming all the time! If it's been featured on our podcast, it's gonna be on this stream! We only play three songs from each game on our podcast, and we decided that everyone needs more tunes!</p>\n<p>We'll be updating this livestream with new games as they're played on the show, including your requests! To get priority in requesting games for the show, check out <a href=\"https://www.patreon.com/GameThatTune\">https://www.patreon.com/GameThatTune</a></p>\n<p>Be sure to check out our live recordings of the Game That Tune podcast! We broadcast every Wednesday night at 9 PM EST on our YouTube channel as well as <a href=\"https://www.twitch.tv/GameThatTune\">https://www.twitch.tv/GameThatTune</a> and <a href=\"https://www.facebook.com/GameThatTune\">https://www.facebook.com/GameThatTune</a>\nTune in and join in on the fun! Find the podcast in iTunes every Wednesday morning or head to <a href=\"https://www.gamethattune.com\">https://www.gamethattune.com</a>!</p>\n<p>Visit <a href=\"https://www.patreon.com/GameThatTune\">https://www.patreon.com/GameThatTune</a> to help us keep up this live stream and upgrade our equipment for the live show! We've got exclusive mixtapes for our patrons, and lots more stuff planned for the future, so consider helping us out!</p>\n<h1>HOW IT WORKS</h1>\n<p>Featuring music from over 1000 games! Check out <a href=\"https://music.gamethattune.com/songs\">https://music.gamethattune.com/songs</a> for the full list and make a request from your favorite game!</p>\n<p>Now that you've seen the list of games, make a request in the chat!</p>\n<p><code>!sr</code> + anything = general search<br>\n<code>!gr</code> + game title = random song from matching game<br>\n<code>!cr</code> + composer name = random song from matching composer<br>\n<code>!tr</code> + anything = random result only searching song titles<br>\n<code>!rr</code> + anything = random result from all searchable fields<br>\n<code>!game gtt</code> = starts a round of our guessing game for bonus points!</p>\n<p>We have gifs!</p>\n<p>Wanna see your favorite gif on screen? type <code>!summon</code> followed by the gif name! Want your favorite gif to take over the video? Type <code>!spawn</code> followed by the gif name!</p>\n<p>Still have questions? Ask the chatbot! type <code>!info</code> to...wait for it...get more info!</p>\n<p>Thanks for listening!</p>"`,
+  },
 };
 
-export const Example3 = Template.bind({});
-Example3.args = {
-  content: `<hr>\n<h3>The upcoming streams</h3>\n<h4>Thursday, Apr. 28:</h4>\n<ul>\n<li>22:00 CEST / 20:00 UTC / 4:00 pm EDT<br>\n<strong>Thursday Dragons:</strong> „<a href=\"https://www.gog.com/game/dragon_age_origins\">Dragon Age: Origins UE</a>“ (english)</li>\n</ul>\n<h4>Friday, Apr. 29:</h4>\n<ul>\n<li>22:00 CEST / 20:00 UTC / 4:00 pm EDT<br>\n<strong>Trucker Hat:</strong> „<a href=\"https://eurotrucksimulator2.com\">Euro Truck Simulator 2</a>“ (english)</li>\n</ul>\n<h4>Saturday, Apr. 30:</h4>\n<ul>\n<li>19:00 CEST / 17:00 UTC / 1:00 pm EDT<br>\n<strong>Immer Samstags:</strong> „<a href=\"https://www.gog.com/game/syberia_2\">Syberia II</a>“ (deutsch)</li>\n<li>23:30 CEST / 21:30 UTC / 5:30 pm EDT<br>\n<strong>Night Walk:</strong> „<a href=\"https://itch.io/jam/linux-game-jam-2022\">Linux Game Jam 2022</a>“ (english)</li>\n</ul>\n<h4>Sunday, May 1:</h4>\n<ul>\n<li>22:00 CEST / 20:00 UTC / 4:00 pm EDT<br>\n<strong>Spacy Sunday:</strong> „<a href=\"https://www.ea.com/games/mass-effect/mass-effect-legendary-edition\">Mass Effect 2 LE</a>“ (english)</li>\n</ul>\n<h4>Monday, May 2:</h4>\n<ul>\n<li>22:00 CEST / 20:00 UTC / 4:00 pm EDT<br>\n<strong>Monday Tactics:</strong> „<a href=\"https://harebrained-schemes.com/battletech/\">BATTLETECH</a>“ (english)</li>\n</ul>\n<h4>Tuesday, May 3:</h4>\n<ul>\n<li>22:00 CEST / 20:00 UTC / 4:00 pm EDT<br>\n<strong>Epic Tuesday:</strong> „<a href=\"https://terraformers-game.com\">Terraformers</a>“ (english)</li>\n</ul>\n<h4>Wednesday, May 4:</h4>\n<ul>\n<li>22:00 CEST / 20:00 UTC / 4:00 pm EDT<br>\n<strong>Narrative Wednesday:</strong> „<a href=\"https://zidandzniw.pl\">Zniw Adventure</a>“ (english)</li>\n</ul>\n<p><br>\n<em>I plan a duration of about 4 to 5 hours per stream.</em></p>\n<p><strong>The VODs</strong> of my streams will be available on <a href=\"https://youtube.com/hatniX\">YouTube channel</a> and on my <a href=\"https://diode.zone/c/hatvods\">hatVODs PeerTube channel</a> (the last 14 days). Some additional videos can also be found on my <a href=\"https://tube.tchncs.de/c/hatube\">haTube PeerTube channel</a>.</p>\n<p>If this channel is offline you might want to checkout other great Owncast streams at the <a href=\"https://directory.owncast.online/\">Owncast Directory</a>.</p>\n<hr>\n<h3>Pro-tips for new viewers</h3>\n<h4>Change chat name</h4>\n<p>When you visit an Owncast channel for the first time then you've been given a random chat name. You see this name on the top right above the chat box. Click on that name in order to pick your own preferred chat name.</p>\n<h4>Chat formatting</h4>\n<p>The chat supports some basic Markdown, like <code>*Italic*</code>, <code>**Bold**</code> and <code>\`Code blocks\`</code> (from the <a href=\"https://owncast.online/docs/website/#chat\">Owncast docs</a>)</p>\n<h4>Player shortcuts</h4>\n<p>The web video player has some keyboard shortcuts for you to use:</p>\n<ul>\n<li>Play/Pause - Spacebar</li>\n<li>Volume up - 0</li>\n<li>Volume down - 9</li>\n<li>Mute - m</li>\n<li>Toggle fullscreen - f</li>\n<li>Toggle chat - c</li>\n</ul>\n<p>(from the <a href=\"https://owncast.online/docs/website/#player\">Owncast docs</a>)</p>\n<h4>Watch Owncast via IPTV</h4>\n<p>If you have a tv platform/set top box (apple, amazon, roku) that you can install IPTV apps on then you can add the url <a href=\"https://directory.owncast.online/api/iptv\">https://directory.owncast.online/api/iptv</a> to watch this and other Owncast channels on the big screen.</p>\n<h4>Watch the stream in a media player</h4>\n<p>The stream is using HLS standard, so you can use your favorite media player to watch the stream using the url <a href=\"https://live.hatnix.net/hls/stream.m3u8\">https://live.hatnix.net/hls/stream.m3u8</a></p>\n<h4>Joining chat-only</h4>\n<p>In case you want to join only the chat (because you're watching via IPTV?) you can do that using this url: <a href=\"https://live.hatnix.net/embed/chat/readwrite\">https://live.hatnix.net/embed/chat/readwrite</a></p>\n<hr>\n<h3>About me and this channel</h3>\n<p>My name is hatniX (or Frank), 48 years old. I'm a PC gamer for over 30 years now, playing games on Linux since 1998. And this is what you'll see here on this channel, me playing games on Linux. Preferable with a focus on storytelling and atmosphere, rather than &quot;pro-gaming&quot;.</p>\n<h4>Fediverse</h4>\n<p>You can follow this server in the Fediverse: <code>@hatnix@live.hatnix.net</code>, in order to get announcements when the stream starts.</p>\n<h4>Owncast</h4>\n<p>This server is powered by <a href=\"https://owncast.online/\">Owncast</a>. Check out the Owncast-Browser-Extension <a href=\"https://addons.mozilla.org/en-US/firefox/addon/owncast-extension/\">for Firefox</a> or <a href=\"https://chrome.google.com/webstore/detail/owncast-extension/djgneammmklaajinkihpibdpaflehgio\">for Chrome browsers</a> to see whether I'm live. Another great way to see when I (or other Owncast streamers) go online, is to follow the Owncast bots on <a href=\"https://botsin.space/@owncast\">Mastodon</a> or <a href=\"https://twitter.com/owncastlive\">Twitter</a>.</p>\n<h4>Chat bot</h4>\n<p>I'm using an experimantal chat bot here, which I have written specifically for Owncast. Use <code>!help</code> in chat to get a list of the available commands. The source code of the bot is available at <a href=\"https://github.com/hatniX/hatbot\">github.com/hatniX/hatbot</a> (Public Domain).</p>\n<p>In case you wonder how I display the chat onscreen, incl. the fade effect, check out <a href=\"https://raw.githubusercontent.com/hatniX/Owncast-tweaks/main/Owncast_onscreen_chat_fade.css\">my CSS file</a>.</p>\n<h4>Music</h4>\n<p>The music here, if not part of the game I'm playing, is provided by <a href=\"https://www.jamendo.com/start\">Jamando</a>. I've got a <a href=\"https://licensing.jamendo.com/en/livestream/gaming\">license for livestream music for gaming</a>.</p>\n<hr>\n<h3>How you can support me</h3>\n<p>If you want to support me, visit my streams, either lurk or be active in chat. You can also help me getting more attention, by telling others about my channel, or just boosting my stream announcements on <a href=\"https://social.tchncs.de/@hatnix\">Mastodon</a> or retweeting them on <a href=\"https://twitter.com/hatniKS\">Twitter</a>.</p>\n<h4>Liberapay</h4>\n<p>Liberapay is a non-profit subscription platform, which means that they don't take a share. If you like, you can <a href=\"https://liberapay.com/hatnix\">support me</a> there, either with a one-time or a regular donation. Thank you so much!</p>\n<h4>Ko-fi</h4>\n<p>In case you want to buy me a coffee then you can do so at <a href=\"https://ko-fi.com/hatnix\">ko-fi</a>. I appreciate that a lot!</p>\n<p>Support is never required, but always much appreciated. Thank you very much! &lt;3</p>\n<hr>\n<h3>My tech specs</h3>\n<h4>PC Hardware:</h4>\n<ul>\n<li><strong>CPU:</strong> AMD Ryzen 9 5900X</li>\n<li><strong>RAM:</strong> 32 GB</li>\n<li><strong>GPU:</strong> AMD Radeon RX 6900 XT (16 GB)</li>\n<li><strong>Keyboard:</strong> Logitech K295</li>\n<li><strong>Trackball:</strong> Kensington Expert Mouse Trackball</li>\n<li><strong>Mouse 1:</strong> Logitech M220</li>\n<li><strong>Mouse 2:</strong> Razer Naga Left Handed Edition</li>\n<li><strong>Microphone:</strong> Blue Yeti</li>\n<li><strong>Earbuds:</strong> Linklike Classic 2</li>\n<li><strong>Headphone:</strong> beyerdynamic DT 990 Pro</li>\n<li><strong>Webcam(s):</strong> Vitade 960A Pro</li>\n</ul>\n<h4>PC Software:</h4>\n<ul>\n<li><strong>OS:</strong> Manjaro Linux</li>\n<li><strong>Kernel:</strong> 5.17.1-3-MANJARO x86_64</li>\n<li><strong>Desktop:</strong> Xfce 4.16.0</li>\n<li><strong>Mesa:</strong> 21.3.8</li>\n<li><strong>Broadcaster software:</strong> OBS Studio 27.2.4</li>\n</ul>\n<h4>Stream Server: <a href=\"https://www.hetzner.com/cloud\">Hetzner Cloud</a> „CPX11“</h4>\n<ul>\n<li>2 vCPU AMD</li>\n<li>2 GB RAM</li>\n<li>40 GB disc</li>\n<li>20TB Traffic</li>\n<li><a href=\"https://owncast.online/\">Owncast</a> v0.0.11<br></li>\n</ul>`,
+export const Example3 = {
+  render: Template,
+
+  args: {
+    content: `<hr>\n<h3>The upcoming streams</h3>\n<h4>Thursday, Apr. 28:</h4>\n<ul>\n<li>22:00 CEST / 20:00 UTC / 4:00 pm EDT<br>\n<strong>Thursday Dragons:</strong> „<a href=\"https://www.gog.com/game/dragon_age_origins\">Dragon Age: Origins UE</a>“ (english)</li>\n</ul>\n<h4>Friday, Apr. 29:</h4>\n<ul>\n<li>22:00 CEST / 20:00 UTC / 4:00 pm EDT<br>\n<strong>Trucker Hat:</strong> „<a href=\"https://eurotrucksimulator2.com\">Euro Truck Simulator 2</a>“ (english)</li>\n</ul>\n<h4>Saturday, Apr. 30:</h4>\n<ul>\n<li>19:00 CEST / 17:00 UTC / 1:00 pm EDT<br>\n<strong>Immer Samstags:</strong> „<a href=\"https://www.gog.com/game/syberia_2\">Syberia II</a>“ (deutsch)</li>\n<li>23:30 CEST / 21:30 UTC / 5:30 pm EDT<br>\n<strong>Night Walk:</strong> „<a href=\"https://itch.io/jam/linux-game-jam-2022\">Linux Game Jam 2022</a>“ (english)</li>\n</ul>\n<h4>Sunday, May 1:</h4>\n<ul>\n<li>22:00 CEST / 20:00 UTC / 4:00 pm EDT<br>\n<strong>Spacy Sunday:</strong> „<a href=\"https://www.ea.com/games/mass-effect/mass-effect-legendary-edition\">Mass Effect 2 LE</a>“ (english)</li>\n</ul>\n<h4>Monday, May 2:</h4>\n<ul>\n<li>22:00 CEST / 20:00 UTC / 4:00 pm EDT<br>\n<strong>Monday Tactics:</strong> „<a href=\"https://harebrained-schemes.com/battletech/\">BATTLETECH</a>“ (english)</li>\n</ul>\n<h4>Tuesday, May 3:</h4>\n<ul>\n<li>22:00 CEST / 20:00 UTC / 4:00 pm EDT<br>\n<strong>Epic Tuesday:</strong> „<a href=\"https://terraformers-game.com\">Terraformers</a>“ (english)</li>\n</ul>\n<h4>Wednesday, May 4:</h4>\n<ul>\n<li>22:00 CEST / 20:00 UTC / 4:00 pm EDT<br>\n<strong>Narrative Wednesday:</strong> „<a href=\"https://zidandzniw.pl\">Zniw Adventure</a>“ (english)</li>\n</ul>\n<p><br>\n<em>I plan a duration of about 4 to 5 hours per stream.</em></p>\n<p><strong>The VODs</strong> of my streams will be available on <a href=\"https://youtube.com/hatniX\">YouTube channel</a> and on my <a href=\"https://diode.zone/c/hatvods\">hatVODs PeerTube channel</a> (the last 14 days). Some additional videos can also be found on my <a href=\"https://tube.tchncs.de/c/hatube\">haTube PeerTube channel</a>.</p>\n<p>If this channel is offline you might want to checkout other great Owncast streams at the <a href=\"https://directory.owncast.online/\">Owncast Directory</a>.</p>\n<hr>\n<h3>Pro-tips for new viewers</h3>\n<h4>Change chat name</h4>\n<p>When you visit an Owncast channel for the first time then you've been given a random chat name. You see this name on the top right above the chat box. Click on that name in order to pick your own preferred chat name.</p>\n<h4>Chat formatting</h4>\n<p>The chat supports some basic Markdown, like <code>*Italic*</code>, <code>**Bold**</code> and <code>\`Code blocks\`</code> (from the <a href=\"https://owncast.online/docs/website/#chat\">Owncast docs</a>)</p>\n<h4>Player shortcuts</h4>\n<p>The web video player has some keyboard shortcuts for you to use:</p>\n<ul>\n<li>Play/Pause - Spacebar</li>\n<li>Volume up - 0</li>\n<li>Volume down - 9</li>\n<li>Mute - m</li>\n<li>Toggle fullscreen - f</li>\n<li>Toggle chat - c</li>\n</ul>\n<p>(from the <a href=\"https://owncast.online/docs/website/#player\">Owncast docs</a>)</p>\n<h4>Watch Owncast via IPTV</h4>\n<p>If you have a tv platform/set top box (apple, amazon, roku) that you can install IPTV apps on then you can add the url <a href=\"https://directory.owncast.online/api/iptv\">https://directory.owncast.online/api/iptv</a> to watch this and other Owncast channels on the big screen.</p>\n<h4>Watch the stream in a media player</h4>\n<p>The stream is using HLS standard, so you can use your favorite media player to watch the stream using the url <a href=\"https://live.hatnix.net/hls/stream.m3u8\">https://live.hatnix.net/hls/stream.m3u8</a></p>\n<h4>Joining chat-only</h4>\n<p>In case you want to join only the chat (because you're watching via IPTV?) you can do that using this url: <a href=\"https://live.hatnix.net/embed/chat/readwrite\">https://live.hatnix.net/embed/chat/readwrite</a></p>\n<hr>\n<h3>About me and this channel</h3>\n<p>My name is hatniX (or Frank), 48 years old. I'm a PC gamer for over 30 years now, playing games on Linux since 1998. And this is what you'll see here on this channel, me playing games on Linux. Preferable with a focus on storytelling and atmosphere, rather than &quot;pro-gaming&quot;.</p>\n<h4>Fediverse</h4>\n<p>You can follow this server in the Fediverse: <code>@hatnix@live.hatnix.net</code>, in order to get announcements when the stream starts.</p>\n<h4>Owncast</h4>\n<p>This server is powered by <a href=\"https://owncast.online/\">Owncast</a>. Check out the Owncast-Browser-Extension <a href=\"https://addons.mozilla.org/en-US/firefox/addon/owncast-extension/\">for Firefox</a> or <a href=\"https://chrome.google.com/webstore/detail/owncast-extension/djgneammmklaajinkihpibdpaflehgio\">for Chrome browsers</a> to see whether I'm live. Another great way to see when I (or other Owncast streamers) go online, is to follow the Owncast bots on <a href=\"https://botsin.space/@owncast\">Mastodon</a> or <a href=\"https://twitter.com/owncastlive\">Twitter</a>.</p>\n<h4>Chat bot</h4>\n<p>I'm using an experimantal chat bot here, which I have written specifically for Owncast. Use <code>!help</code> in chat to get a list of the available commands. The source code of the bot is available at <a href=\"https://github.com/hatniX/hatbot\">github.com/hatniX/hatbot</a> (Public Domain).</p>\n<p>In case you wonder how I display the chat onscreen, incl. the fade effect, check out <a href=\"https://raw.githubusercontent.com/hatniX/Owncast-tweaks/main/Owncast_onscreen_chat_fade.css\">my CSS file</a>.</p>\n<h4>Music</h4>\n<p>The music here, if not part of the game I'm playing, is provided by <a href=\"https://www.jamendo.com/start\">Jamando</a>. I've got a <a href=\"https://licensing.jamendo.com/en/livestream/gaming\">license for livestream music for gaming</a>.</p>\n<hr>\n<h3>How you can support me</h3>\n<p>If you want to support me, visit my streams, either lurk or be active in chat. You can also help me getting more attention, by telling others about my channel, or just boosting my stream announcements on <a href=\"https://social.tchncs.de/@hatnix\">Mastodon</a> or retweeting them on <a href=\"https://twitter.com/hatniKS\">Twitter</a>.</p>\n<h4>Liberapay</h4>\n<p>Liberapay is a non-profit subscription platform, which means that they don't take a share. If you like, you can <a href=\"https://liberapay.com/hatnix\">support me</a> there, either with a one-time or a regular donation. Thank you so much!</p>\n<h4>Ko-fi</h4>\n<p>In case you want to buy me a coffee then you can do so at <a href=\"https://ko-fi.com/hatnix\">ko-fi</a>. I appreciate that a lot!</p>\n<p>Support is never required, but always much appreciated. Thank you very much! &lt;3</p>\n<hr>\n<h3>My tech specs</h3>\n<h4>PC Hardware:</h4>\n<ul>\n<li><strong>CPU:</strong> AMD Ryzen 9 5900X</li>\n<li><strong>RAM:</strong> 32 GB</li>\n<li><strong>GPU:</strong> AMD Radeon RX 6900 XT (16 GB)</li>\n<li><strong>Keyboard:</strong> Logitech K295</li>\n<li><strong>Trackball:</strong> Kensington Expert Mouse Trackball</li>\n<li><strong>Mouse 1:</strong> Logitech M220</li>\n<li><strong>Mouse 2:</strong> Razer Naga Left Handed Edition</li>\n<li><strong>Microphone:</strong> Blue Yeti</li>\n<li><strong>Earbuds:</strong> Linklike Classic 2</li>\n<li><strong>Headphone:</strong> beyerdynamic DT 990 Pro</li>\n<li><strong>Webcam(s):</strong> Vitade 960A Pro</li>\n</ul>\n<h4>PC Software:</h4>\n<ul>\n<li><strong>OS:</strong> Manjaro Linux</li>\n<li><strong>Kernel:</strong> 5.17.1-3-MANJARO x86_64</li>\n<li><strong>Desktop:</strong> Xfce 4.16.0</li>\n<li><strong>Mesa:</strong> 21.3.8</li>\n<li><strong>Broadcaster software:</strong> OBS Studio 27.2.4</li>\n</ul>\n<h4>Stream Server: <a href=\"https://www.hetzner.com/cloud\">Hetzner Cloud</a> „CPX11“</h4>\n<ul>\n<li>2 vCPU AMD</li>\n<li>2 GB RAM</li>\n<li>40 GB disc</li>\n<li>20TB Traffic</li>\n<li><a href=\"https://owncast.online/\">Owncast</a> v0.0.11<br></li>\n</ul>`,
+  },
 };
diff --git a/web/components/ui/Footer/Footer.stories.tsx b/web/components/ui/Footer/Footer.stories.tsx
index 40eb48975..42a0604f4 100644
--- a/web/components/ui/Footer/Footer.stories.tsx
+++ b/web/components/ui/Footer/Footer.stories.tsx
@@ -1,22 +1,25 @@
-import React from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { StoryFn, Meta } from '@storybook/react';
 import { RecoilRoot } from 'recoil';
 import { Footer } from './Footer';
 
-export default {
+const meta = {
   title: 'owncast/Layout/Footer',
   component: Footer,
   parameters: {},
-} as ComponentMeta<typeof Footer>;
+} satisfies Meta<typeof Footer>;
 
-const Template: ComponentStory<typeof Footer> = args => (
+export default meta;
+
+const Template: StoryFn<typeof Footer> = args => (
   <RecoilRoot>
     <Footer {...args} />
   </RecoilRoot>
 );
 
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-export const Example = Template.bind({});
-Example.args = {
-  version: 'v1.2.3',
+export const Example = {
+  render: Template,
+
+  args: {
+    version: 'v1.2.3',
+  },
 };
diff --git a/web/components/ui/Header/Header.stories.tsx b/web/components/ui/Header/Header.stories.tsx
index d23eee05a..45b0519e1 100644
--- a/web/components/ui/Header/Header.stories.tsx
+++ b/web/components/ui/Header/Header.stories.tsx
@@ -1,30 +1,37 @@
-import React from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { StoryFn, Meta } from '@storybook/react';
 import { RecoilRoot } from 'recoil';
 import { Header } from './Header';
 
-export default {
+const meta = {
   title: 'owncast/Layout/Header',
   component: Header,
   parameters: {
     chromatic: { diffThreshold: 0.75 },
   },
-} as ComponentMeta<typeof Header>;
+} satisfies Meta<typeof Header>;
 
-const Template: ComponentStory<typeof Header> = args => (
+export default meta;
+
+const Template: StoryFn<typeof Header> = args => (
   <RecoilRoot>
     <Header {...args} />
   </RecoilRoot>
 );
 
-export const ChatAvailable = Template.bind({});
-ChatAvailable.args = {
-  name: 'Example Stream Name',
-  chatAvailable: true,
+export const ChatAvailable = {
+  render: Template,
+
+  args: {
+    name: 'Example Stream Name',
+    chatAvailable: true,
+  },
 };
 
-export const ChatNotAvailable = Template.bind({});
-ChatNotAvailable.args = {
-  name: 'Example Stream Name',
-  chatAvailable: false,
+export const ChatNotAvailable = {
+  render: Template,
+
+  args: {
+    name: 'Example Stream Name',
+    chatAvailable: false,
+  },
 };
diff --git a/web/components/ui/Modal/Modal.stories.tsx b/web/components/ui/Modal/Modal.stories.tsx
index cacf58317..3cf4e4996 100644
--- a/web/components/ui/Modal/Modal.stories.tsx
+++ b/web/components/ui/Modal/Modal.stories.tsx
@@ -1,8 +1,7 @@
-import React from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { StoryFn, Meta } from '@storybook/react';
 import { Modal } from './Modal';
 
-export default {
+const meta = {
   title: 'owncast/Modals/Container',
   component: Modal,
   parameters: {
@@ -12,23 +11,31 @@ export default {
       },
     },
   },
-} as ComponentMeta<typeof Modal>;
+} satisfies Meta<typeof Modal>;
 
-const Template: ComponentStory<typeof Modal> = args => {
+export default meta;
+
+const Template: StoryFn<typeof Modal> = args => {
   const { children } = args;
   return <Modal {...args}>{children}</Modal>;
 };
 
-export const Example = Template.bind({});
-Example.args = {
-  title: 'Modal example with content nodes',
-  visible: true,
-  children: <div>Test 123</div>,
+export const Example = {
+  render: Template,
+
+  args: {
+    title: 'Modal example with content nodes',
+    visible: true,
+    children: <div>Test 123</div>,
+  },
 };
 
-export const UrlExample = Template.bind({});
-UrlExample.args = {
-  title: 'Modal example with URL',
-  visible: true,
-  url: 'https://owncast.online',
+export const UrlExample = {
+  render: Template,
+
+  args: {
+    title: 'Modal example with URL',
+    visible: true,
+    url: 'https://owncast.online',
+  },
 };
diff --git a/web/components/ui/NotifyReminderPopup/NotifyReminderPopup.stories.tsx b/web/components/ui/NotifyReminderPopup/NotifyReminderPopup.stories.tsx
index ba2ce94ed..6d99fc355 100644
--- a/web/components/ui/NotifyReminderPopup/NotifyReminderPopup.stories.tsx
+++ b/web/components/ui/NotifyReminderPopup/NotifyReminderPopup.stories.tsx
@@ -1,6 +1,5 @@
 /* eslint-disable no-alert */
-import React from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { StoryFn, Meta } from '@storybook/react';
 import { NotifyReminderPopup } from './NotifyReminderPopup';
 import Mock from '../../../stories/assets/mocks/notify-popup.png';
 
@@ -12,7 +11,7 @@ const Example = args => (
   </div>
 );
 
-export default {
+const meta = {
   title: 'owncast/Components/Notify Reminder',
   component: NotifyReminderPopup,
   parameters: {
@@ -27,22 +26,30 @@ Clicking it will make the notification modal display. Clicking the "X" will hide
       },
     },
   },
-} as ComponentMeta<typeof NotifyReminderPopup>;
+} satisfies Meta<typeof NotifyReminderPopup>;
 
-const Template: ComponentStory<typeof NotifyReminderPopup> = args => <Example {...args} />;
+export default meta;
 
-export const Active = Template.bind({});
-Active.args = {
-  open: true,
-  notificationClicked: () => {
-    alert('notification clicked');
-  },
-  notificationClosed: () => {
-    alert('notification closed');
+const Template: StoryFn<typeof NotifyReminderPopup> = args => <Example {...args} />;
+
+export const Active = {
+  render: Template,
+
+  args: {
+    open: true,
+    notificationClicked: () => {
+      alert('notification clicked');
+    },
+    notificationClosed: () => {
+      alert('notification closed');
+    },
   },
 };
 
-export const InActive = Template.bind({});
-InActive.args = {
-  open: false,
+export const InActive = {
+  render: Template,
+
+  args: {
+    open: false,
+  },
 };
diff --git a/web/components/ui/OfflineBanner/OfflineBanner.stories.tsx b/web/components/ui/OfflineBanner/OfflineBanner.stories.tsx
index d414efad4..1b1b73ab5 100644
--- a/web/components/ui/OfflineBanner/OfflineBanner.stories.tsx
+++ b/web/components/ui/OfflineBanner/OfflineBanner.stories.tsx
@@ -1,10 +1,9 @@
-import React from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { StoryFn, Meta } from '@storybook/react';
 import { RecoilRoot } from 'recoil';
 import { OfflineBanner } from './OfflineBanner';
 import OfflineState from '../../../stories/assets/mocks/offline-state.png';
 
-export default {
+const meta = {
   title: 'owncast/Layout/Offline Banner',
   component: OfflineBanner,
   parameters: {
@@ -19,56 +18,76 @@ export default {
       },
     },
   },
-} as ComponentMeta<typeof OfflineBanner>;
+} satisfies Meta<typeof OfflineBanner>;
 
-const Template: ComponentStory<typeof OfflineBanner> = args => (
+export default meta;
+
+const Template: StoryFn<typeof OfflineBanner> = args => (
   <RecoilRoot>
     <OfflineBanner {...args} />
   </RecoilRoot>
 );
 
-export const ExampleDefaultWithNotifications = Template.bind({});
-ExampleDefaultWithNotifications.args = {
-  streamName: 'Cool stream 42',
-  notificationsEnabled: true,
-  lastLive: new Date(),
+export const ExampleDefaultWithNotifications = {
+  render: Template,
+
+  args: {
+    streamName: 'Cool stream 42',
+    notificationsEnabled: true,
+    lastLive: new Date(),
+  },
 };
 
-export const ExampleDefaultWithDateAndFediverse = Template.bind({});
-ExampleDefaultWithDateAndFediverse.args = {
-  streamName: 'Dull stream 31337',
-  lastLive: new Date(),
-  notificationsEnabled: false,
-  fediverseAccount: 'streamer@coolstream.biz',
+export const ExampleDefaultWithDateAndFediverse = {
+  render: Template,
+
+  args: {
+    streamName: 'Dull stream 31337',
+    lastLive: new Date(),
+    notificationsEnabled: false,
+    fediverseAccount: 'streamer@coolstream.biz',
+  },
 };
 
-export const ExampleCustomWithDateAndNotifications = Template.bind({});
-ExampleCustomWithDateAndNotifications.args = {
-  streamName: 'Dull stream 31337',
-  customText:
-    'This is some example offline text that a streamer can leave for a visitor of the page.',
-  lastLive: new Date(),
-  notificationsEnabled: true,
+export const ExampleCustomWithDateAndNotifications = {
+  render: Template,
+
+  args: {
+    streamName: 'Dull stream 31337',
+    customText:
+      'This is some example offline text that a streamer can leave for a visitor of the page.',
+    lastLive: new Date(),
+    notificationsEnabled: true,
+  },
 };
 
-export const ExampleDefaultWithNotificationsAndFediverse = Template.bind({});
-ExampleDefaultWithNotificationsAndFediverse.args = {
-  streamName: 'Cool stream 42',
-  notificationsEnabled: true,
-  fediverseAccount: 'streamer@coolstream.biz',
-  lastLive: new Date(),
+export const ExampleDefaultWithNotificationsAndFediverse = {
+  render: Template,
+
+  args: {
+    streamName: 'Cool stream 42',
+    notificationsEnabled: true,
+    fediverseAccount: 'streamer@coolstream.biz',
+    lastLive: new Date(),
+  },
 };
 
-export const ExampleDefaultWithoutNotifications = Template.bind({});
-ExampleDefaultWithoutNotifications.args = {
-  streamName: 'Cool stream 42',
-  notificationsEnabled: false,
-  lastLive: new Date(),
+export const ExampleDefaultWithoutNotifications = {
+  render: Template,
+
+  args: {
+    streamName: 'Cool stream 42',
+    notificationsEnabled: false,
+    lastLive: new Date(),
+  },
 };
 
-export const ExampleCustomTextWithoutNotifications = Template.bind({});
-ExampleCustomTextWithoutNotifications.args = {
-  streamName: 'Dull stream 31337',
-  customText:
-    'This is some example offline text that a streamer can leave for a visitor of the page.',
+export const ExampleCustomTextWithoutNotifications = {
+  render: Template,
+
+  args: {
+    streamName: 'Dull stream 31337',
+    customText:
+      'This is some example offline text that a streamer can leave for a visitor of the page.',
+  },
 };
diff --git a/web/components/ui/SocialLinks/SocialLinks.stories.tsx b/web/components/ui/SocialLinks/SocialLinks.stories.tsx
index 58e928196..e44846d93 100644
--- a/web/components/ui/SocialLinks/SocialLinks.stories.tsx
+++ b/web/components/ui/SocialLinks/SocialLinks.stories.tsx
@@ -1,39 +1,38 @@
-import React from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { Meta } from '@storybook/react';
 import { SocialLinks } from './SocialLinks';
 
-export default {
+const meta = {
   title: 'owncast/Components/Social links',
   component: SocialLinks,
   parameters: {},
-} as ComponentMeta<typeof SocialLinks>;
+} satisfies Meta<typeof SocialLinks>;
 
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-const Template: ComponentStory<typeof SocialLinks> = args => <SocialLinks {...args} />;
+export default meta;
 
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-export const Populated = Template.bind({});
-Populated.args = {
-  links: [
-    {
-      platform: 'github',
-      url: 'https://github.com/owncast/owncast',
-      icon: '/img/platformlogos/github.svg',
-    },
-    {
-      platform: 'Documentation',
-      url: 'https://owncast.online',
-      icon: '/img/platformlogos/link.svg',
-    },
-    {
-      platform: 'mastodon',
-      url: 'https://fosstodon.org/users/owncast',
-      icon: '/img/platformlogos/mastodon.svg',
-    },
-  ],
+export const Populated = {
+  args: {
+    links: [
+      {
+        platform: 'github',
+        url: 'https://github.com/owncast/owncast',
+        icon: '/img/platformlogos/github.svg',
+      },
+      {
+        platform: 'Documentation',
+        url: 'https://owncast.online',
+        icon: '/img/platformlogos/link.svg',
+      },
+      {
+        platform: 'mastodon',
+        url: 'https://fosstodon.org/users/owncast',
+        icon: '/img/platformlogos/mastodon.svg',
+      },
+    ],
+  },
 };
 
-export const Empty = Template.bind({});
-Empty.args = {
-  links: [],
+export const Empty = {
+  args: {
+    links: [],
+  },
 };
diff --git a/web/components/ui/Statusbar/StatusBar.stories.tsx b/web/components/ui/Statusbar/StatusBar.stories.tsx
index 633e88759..3aa1d82dd 100644
--- a/web/components/ui/Statusbar/StatusBar.stories.tsx
+++ b/web/components/ui/Statusbar/StatusBar.stories.tsx
@@ -1,25 +1,26 @@
-import React from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { Meta } from '@storybook/react';
 import { subHours } from 'date-fns';
 import { Statusbar } from './Statusbar';
 
-export default {
+const meta = {
   title: 'owncast/Player/Status bar',
   component: Statusbar,
   parameters: {},
-} as ComponentMeta<typeof Statusbar>;
+} satisfies Meta<typeof Statusbar>;
 
-const Template: ComponentStory<typeof Statusbar> = args => <Statusbar {...args} />;
+export default meta;
 
-export const Online = Template.bind({});
-Online.args = {
-  online: true,
-  viewerCount: 42,
-  lastConnectTime: subHours(new Date(), 3),
+export const Online = {
+  args: {
+    online: true,
+    viewerCount: 42,
+    lastConnectTime: subHours(new Date(), 3),
+  },
 };
 
-export const Offline = Template.bind({});
-Offline.args = {
-  online: false,
-  lastDisconnectTime: subHours(new Date(), 3),
+export const Offline = {
+  args: {
+    online: false,
+    lastDisconnectTime: subHours(new Date(), 3),
+  },
 };
diff --git a/web/components/ui/followers/FollowerCollection/FollowerCollection.stories.tsx b/web/components/ui/followers/FollowerCollection/FollowerCollection.stories.tsx
index 19c920287..59386906d 100644
--- a/web/components/ui/followers/FollowerCollection/FollowerCollection.stories.tsx
+++ b/web/components/ui/followers/FollowerCollection/FollowerCollection.stories.tsx
@@ -1,5 +1,4 @@
-import React from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { StoryFn, Meta } from '@storybook/react';
 import { RecoilRoot } from 'recoil';
 import { action } from '@storybook/addon-actions';
 import { FollowerCollection } from './FollowerCollection';
@@ -258,15 +257,17 @@ const noFollowersMock = {
   ],
 };
 
-export default {
+const meta = {
   title: 'owncast/Components/Followers/Followers collection',
   component: FollowerCollection,
   parameters: {
     chromatic: { diffThreshold: 0.86 },
   },
-} as ComponentMeta<typeof FollowerCollection>;
+} satisfies Meta<typeof FollowerCollection>;
 
-const Template: ComponentStory<typeof FollowerCollection> = (args: object) => (
+export default meta;
+
+const Template: StoryFn<typeof FollowerCollection> = (args: object) => (
   <RecoilRoot>
     <FollowerCollection
       onFollowButtonClick={() => {
@@ -278,12 +279,18 @@ const Template: ComponentStory<typeof FollowerCollection> = (args: object) => (
   </RecoilRoot>
 );
 
-export const NoFollowers = Template.bind({});
-NoFollowers.parameters = {
-  fetchMock: noFollowersMock,
+export const NoFollowers = {
+  render: Template,
+
+  parameters: {
+    fetchMock: noFollowersMock,
+  },
 };
 
-export const Example = Template.bind({});
-Example.parameters = {
-  fetchMock: mocks,
+export const Example = {
+  render: Template,
+
+  parameters: {
+    fetchMock: mocks,
+  },
 };
diff --git a/web/components/ui/followers/SingleFollower/SingleFollower.stories.tsx b/web/components/ui/followers/SingleFollower/SingleFollower.stories.tsx
index c1dc5a681..ea9ddb636 100644
--- a/web/components/ui/followers/SingleFollower/SingleFollower.stories.tsx
+++ b/web/components/ui/followers/SingleFollower/SingleFollower.stories.tsx
@@ -1,9 +1,8 @@
-import React from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { Meta } from '@storybook/react';
 import { SingleFollower } from './SingleFollower';
 import SingleFollowerMock from '../../../../stories/assets/mocks/single-follower.png';
 
-export default {
+const meta = {
   title: 'owncast/Components/Followers/Single Follower',
   component: SingleFollower,
   parameters: {
@@ -17,17 +16,18 @@ export default {
       },
     },
   },
-} as ComponentMeta<typeof SingleFollower>;
+} satisfies Meta<typeof SingleFollower>;
 
-const Template: ComponentStory<typeof SingleFollower> = args => <SingleFollower {...args} />;
+export default meta;
 
-export const Example = Template.bind({});
-Example.args = {
-  follower: {
-    name: 'John Doe',
-    description: 'User',
-    username: '@account@domain.tld',
-    image: 'https://avatars0.githubusercontent.com/u/1234?s=460&v=4',
-    link: 'https://yahoo.com',
+export const Example = {
+  args: {
+    follower: {
+      name: 'John Doe',
+      description: 'User',
+      username: '@account@domain.tld',
+      image: 'https://avatars0.githubusercontent.com/u/1234?s=460&v=4',
+      link: 'https://yahoo.com',
+    },
   },
 };
diff --git a/web/components/video/OwncastPlayer/OwncastPlayer.stories.tsx b/web/components/video/OwncastPlayer/OwncastPlayer.stories.tsx
index ac27bafd9..f76fbf716 100644
--- a/web/components/video/OwncastPlayer/OwncastPlayer.stories.tsx
+++ b/web/components/video/OwncastPlayer/OwncastPlayer.stories.tsx
@@ -1,5 +1,4 @@
-import React from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { StoryFn, Meta } from '@storybook/react';
 import { RecoilRoot } from 'recoil';
 import { OwncastPlayer } from './OwncastPlayer';
 
@@ -9,7 +8,7 @@ const streams = {
   localhost: `http://localhost:8080/hls/stream.m3u8`,
 };
 
-export default {
+const meta = {
   title: 'owncast/Player/Player',
   component: OwncastPlayer,
   argTypes: {
@@ -22,17 +21,22 @@ export default {
     },
   },
   parameters: {},
-} as ComponentMeta<typeof OwncastPlayer>;
+} satisfies Meta<typeof OwncastPlayer>;
 
-const Template: ComponentStory<typeof OwncastPlayer> = args => (
+export default meta;
+
+const Template: StoryFn<typeof OwncastPlayer> = args => (
   <RecoilRoot>
     <OwncastPlayer {...args} />
   </RecoilRoot>
 );
 
-export const LiveDemo = Template.bind({});
-LiveDemo.args = {
-  online: true,
-  source: 'https://watch.owncast.online/hls/stream.m3u8',
-  title: 'Stream title',
+export const LiveDemo = {
+  render: Template,
+
+  args: {
+    online: true,
+    source: 'https://watch.owncast.online/hls/stream.m3u8',
+    title: 'Stream title',
+  },
 };
diff --git a/web/components/video/VideoPoster/VideoPoster.stories.tsx b/web/components/video/VideoPoster/VideoPoster.stories.tsx
index 090837aed..1c8711b2c 100644
--- a/web/components/video/VideoPoster/VideoPoster.stories.tsx
+++ b/web/components/video/VideoPoster/VideoPoster.stories.tsx
@@ -1,8 +1,7 @@
-import React from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { Meta } from '@storybook/react';
 import { VideoPoster } from './VideoPoster';
 
-export default {
+const meta = {
   title: 'owncast/Player/Video poster',
   component: VideoPoster,
   parameters: {
@@ -17,29 +16,30 @@ export default {
       },
     },
   },
-} as ComponentMeta<typeof VideoPoster>;
+} satisfies Meta<typeof VideoPoster>;
 
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-const Template: ComponentStory<typeof VideoPoster> = args => <VideoPoster {...args} />;
+export default meta;
 
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-export const Example1 = Template.bind({});
-Example1.args = {
-  initialSrc: 'https://watch.owncast.online/logo',
-  src: 'https://watch.owncast.online/thumbnail.jpg',
-  online: true,
+export const Example1 = {
+  args: {
+    initialSrc: 'https://watch.owncast.online/logo',
+    src: 'https://watch.owncast.online/thumbnail.jpg',
+    online: true,
+  },
 };
 
-export const Example2 = Template.bind({});
-Example2.args = {
-  initialSrc: 'https://listen.batstationrad.io/logo',
-  src: 'https://listen.batstationrad.io//thumbnail.jpg',
-  online: true,
+export const Example2 = {
+  args: {
+    initialSrc: 'https://listen.batstationrad.io/logo',
+    src: 'https://listen.batstationrad.io//thumbnail.jpg',
+    online: true,
+  },
 };
 
-export const Offline = Template.bind({});
-Offline.args = {
-  initialSrc: 'https://watch.owncast.online/logo',
-  src: 'https://watch.owncast.online/thumbnail.jpg',
-  online: false,
+export const Offline = {
+  args: {
+    initialSrc: 'https://watch.owncast.online/logo',
+    src: 'https://watch.owncast.online/thumbnail.jpg',
+    online: false,
+  },
 };
diff --git a/web/stories/PageLogo.stories.tsx b/web/stories/PageLogo.stories.tsx
index 79782e00e..1e4c50ef4 100644
--- a/web/stories/PageLogo.stories.tsx
+++ b/web/stories/PageLogo.stories.tsx
@@ -1,30 +1,30 @@
-import React from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { Meta } from '@storybook/react';
 import { Logo } from '../components/ui/Logo/Logo';
 
-export default {
+const meta = {
   title: 'owncast/Components/Page Logo',
   component: Logo,
   parameters: {
     chromatic: { diffThreshold: 0.8 },
   },
-} as ComponentMeta<typeof Logo>;
+} satisfies Meta<typeof Logo>;
 
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-const Template: ComponentStory<typeof Logo> = args => <Logo {...args} />;
+export default meta;
 
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-export const LocalServer = Template.bind({});
-LocalServer.args = {
-  src: 'http://localhost:8080/logo',
+export const LocalServer = {
+  args: {
+    src: 'http://localhost:8080/logo',
+  },
 };
 
-export const DemoServer = Template.bind({});
-DemoServer.args = {
-  src: 'https://watch.owncast.online/logo',
+export const DemoServer = {
+  args: {
+    src: 'https://watch.owncast.online/logo',
+  },
 };
 
-export const NotSquare = Template.bind({});
-NotSquare.args = {
-  src: 'https://via.placeholder.com/150x325/FF0000/FFFFFF?text=Rectangle',
+export const NotSquare = {
+  args: {
+    src: 'https://via.placeholder.com/150x325/FF0000/FFFFFF?text=Rectangle',
+  },
 };
diff --git a/web/stories/ReadonlyChat.stories.tsx b/web/stories/ReadonlyChat.stories.tsx
index 66ffffc1b..4c24ff35f 100644
--- a/web/stories/ReadonlyChat.stories.tsx
+++ b/web/stories/ReadonlyChat.stories.tsx
@@ -1,15 +1,17 @@
-import React, { useEffect } from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { useEffect } from 'react';
+import { StoryFn, Meta } from '@storybook/react';
 import { RecoilRoot, useSetRecoilState } from 'recoil';
 import ReadOnlyPage from '../pages/embed/chat/readonly/index';
 import { ChatMessage } from '../interfaces/chat-message.model';
 import { chatMessagesAtom } from '../components/stores/ClientConfigStore';
 
-export default {
+const meta = {
   title: 'owncast/Chat/Embeds/Read-only chat',
   component: ReadOnlyPage,
   parameters: {},
-} as ComponentMeta<typeof ReadOnlyPage>;
+} satisfies Meta<typeof ReadOnlyPage>;
+
+export default meta;
 
 const testMessages =
   '[{"type":"CHAT","id":"wY-MEXwnR","timestamp":"2022-04-28T20:30:27.001762726Z","user":{"id":"h_5GQ6E7R","displayName":"UserDisplayName42","displayColor":329,"createdAt":"2022-03-24T03:52:37.966584694Z","previousNames":["gifted-nobel","EliteMooseTaskForce"],"nameChangedAt":"2022-04-26T23:56:05.531287897Z","scopes":[""]},"body":"this is a test message"},{"type":"CHAT","id":"VhLGEXwnR","timestamp":"2022-04-28T20:30:28.806999545Z","user":{"id":"h_5GQ6E7R","displayName":"EliteMooseTaskForce","displayColor":329,"createdAt":"2022-03-24T03:52:37.966584694Z","previousNames":["gifted-nobel","EliteMooseTaskForce"],"nameChangedAt":"2022-04-26T23:56:05.531287897Z","scopes":[""]},"body":"Hit 3"},{"type":"CHAT","id":"GguMEuw7R","timestamp":"2022-04-28T20:30:34.500150601Z","user":{"id":"h_5GQ6E7R","displayName":"EliteMooseTaskForce","displayColor":329,"createdAt":"2022-03-24T03:52:37.966584694Z","previousNames":["gifted-nobel","EliteMooseTaskForce"],"nameChangedAt":"2022-04-26T23:56:05.531287897Z","scopes":[""]},"body":"Jkjk"},{"type":"CHAT","id":"y_-VEXwnR","timestamp":"2022-04-28T20:31:32.695583044Z","user":{"id":"h_5GQ6E7R","displayName":"EliteMooseTaskForce","displayColor":329,"createdAt":"2022-03-24T03:52:37.966584694Z","previousNames":["gifted-nobel","EliteMooseTaskForce"],"nameChangedAt":"2022-04-26T23:56:05.531287897Z","scopes":[""]},"body":"I\u0026#39;m doing alright. How about you Hatnix?"},{"type":"CHAT","id":"qAaKEuwng","timestamp":"2022-04-28T20:34:16.22275314Z","user":{"id":"h_5GQ6E7R","displayName":"EliteMooseTaskForce","displayColor":329,"createdAt":"2022-03-24T03:52:37.966584694Z","previousNames":["gifted-nobel","EliteMooseTaskForce"],"nameChangedAt":"2022-04-26T23:56:05.531287897Z","scopes":[""]},"body":"Oh shiet I didn\u0026#39;t think you would kill him"},{"type":"CHAT","id":"8wUFEuwnR","timestamp":"2022-04-28T20:34:21.624898714Z","user":{"id":"h_5GQ6E7R","displayName":"EliteMooseTaskForce","displayColor":329,"createdAt":"2022-03-24T03:52:37.966584694Z","previousNames":["gifted-nobel","EliteMooseTaskForce"],"nameChangedAt":"2022-04-26T23:56:05.531287897Z","scopes":[""]},"body":"Hahaha, ruthless"},{"type":"CHAT","id":"onYcPuQnR","timestamp":"2022-04-28T20:34:50.671024312Z","user":{"id":"h_5GQ6E7R","displayName":"EliteMooseTaskForce","displayColor":329,"createdAt":"2022-03-24T03:52:37.966584694Z","previousNames":["gifted-nobel","EliteMooseTaskForce"],"nameChangedAt":"2022-04-26T23:56:05.531287897Z","scopes":[""]},"body":"I\u0026#39;ve never played it before"},{"type":"CHAT","id":"kORyEXQ7R","timestamp":"2022-04-28T20:40:29.761977233Z","user":{"id":"h_5GQ6E7R","displayName":"EliteMooseTaskForce","displayColor":329,"createdAt":"2022-03-24T03:52:37.966584694Z","previousNames":["gifted-nobel","EliteMooseTaskForce"],"nameChangedAt":"2022-04-26T23:56:05.531287897Z","scopes":[""]},"body":"brb real quick"},{"type":"CHAT","id":"F3DvsuQ7g","timestamp":"2022-04-28T20:50:29.451341783Z","user":{"id":"h_5GQ6E7R","displayName":"EliteMooseTaskForce","displayColor":329,"createdAt":"2022-03-24T03:52:37.966584694Z","previousNames":["gifted-nobel","EliteMooseTaskForce"],"nameChangedAt":"2022-04-26T23:56:05.531287897Z","scopes":[""]},"body":"I\u0026#39;m back"},{"type":"CHAT","id":"AH2vsXwnR","timestamp":"2022-04-28T20:50:33.872156152Z","user":{"id":"h_5GQ6E7R","displayName":"EliteMooseTaskForce","displayColor":329,"createdAt":"2022-03-24T03:52:37.966584694Z","previousNames":["gifted-nobel","EliteMooseTaskForce"],"nameChangedAt":"2022-04-26T23:56:05.531287897Z","scopes":[""]},"body":"Whoa what happened here?"},{"type":"CHAT","id":"xGkOsuw7R","timestamp":"2022-04-28T20:50:53.202147658Z","user":{"id":"h_5GQ6E7R","displayName":"EliteMooseTaskForce","displayColor":329,"createdAt":"2022-03-24T03:52:37.966584694Z","previousNames":["gifted-nobel","EliteMooseTaskForce"],"nameChangedAt":"2022-04-26T23:56:05.531287897Z","scopes":[""]},"body":"Your dwarf was half naked."},{"type":"CHAT","id":"opIdsuw7g","timestamp":"2022-04-28T20:50:59.631595947Z","user":{"id":"h_5GQ6E7R","displayName":"EliteMooseTaskForce","displayColor":329,"createdAt":"2022-03-24T03:52:37.966584694Z","previousNames":["gifted-nobel","EliteMooseTaskForce"],"nameChangedAt":"2022-04-26T23:56:05.531287897Z","scopes":[""]},"body":"lol"},{"type":"CHAT","id":"JpwdsuQnR","timestamp":"2022-04-28T20:51:18.065535459Z","user":{"id":"vbh9gtPng","displayName":"𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™","displayColor":276,"createdAt":"2022-03-16T21:02:32.009965702Z","previousNames":["goth-volhard","𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™","𝒽𝒶𝓅𝓅𝓎 𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™","𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™","𝓈𝓉𝒶𝒶𝓇𝒻𝒶𝒶𝓇𝑒𝑒𝓇™","𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™"],"nameChangedAt":"2022-04-14T21:51:50.97992512Z","scopes":[""]},"body":"evening did i just see you running around in... nothing"},{"type":"CHAT","id":"R4WKsXw7R","timestamp":"2022-04-28T20:51:28.064914803Z","user":{"id":"vbh9gtPng","displayName":"𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™","displayColor":276,"createdAt":"2022-03-16T21:02:32.009965702Z","previousNames":["goth-volhard","𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™","𝒽𝒶𝓅𝓅𝓎 𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™","𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™","𝓈𝓉𝒶𝒶𝓇𝒻𝒶𝒶𝓇𝑒𝑒𝓇™","𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™"],"nameChangedAt":"2022-04-14T21:51:50.97992512Z","scopes":[""]},"body":"^^"},{"type":"CHAT","id":"g-PKyXw7g","timestamp":"2022-04-28T20:51:47.936500772Z","user":{"id":"h_5GQ6E7R","displayName":"EliteMooseTaskForce","displayColor":329,"createdAt":"2022-03-24T03:52:37.966584694Z","previousNames":["gifted-nobel","EliteMooseTaskForce"],"nameChangedAt":"2022-04-26T23:56:05.531287897Z","scopes":[""]},"body":"Lol Starfarer, so my eyes didnt deceive me."},{"type":"CHAT","id":"fV8Ksuw7R","timestamp":"2022-04-28T20:51:49.588744112Z","user":{"id":"h_5GQ6E7R","displayName":"EliteMooseTaskForce","displayColor":329,"createdAt":"2022-03-24T03:52:37.966584694Z","previousNames":["gifted-nobel","EliteMooseTaskForce"],"nameChangedAt":"2022-04-26T23:56:05.531287897Z","scopes":[""]},"body":"hahahaha"},{"type":"CHAT","id":"TaStyuwnR","timestamp":"2022-04-28T20:52:38.127528579Z","user":{"id":"vbh9gtPng","displayName":"𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™","displayColor":276,"createdAt":"2022-03-16T21:02:32.009965702Z","previousNames":["goth-volhard","𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™","𝒽𝒶𝓅𝓅𝓎 𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™","𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™","𝓈𝓉𝒶𝒶𝓇𝒻𝒶𝒶𝓇𝑒𝑒𝓇™","𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™"],"nameChangedAt":"2022-04-14T21:51:50.97992512Z","scopes":[""]},"body":"lol sounds nice"},{"type":"CHAT","id":"JGposuwng","timestamp":"2022-04-28T20:53:49.329567087Z","user":{"id":"GCa3J9P7R","displayName":"(ghost of)^10  * toudy49","displayColor":147,"createdAt":"2022-03-22T21:49:25.284237821Z","previousNames":["lucid-pike","toudy49","ghost of toudy49","ghost of ghost of toudy49","ghost of ghost of ghost of toudy49","ghost of ghost of ghost of ghost of toudy49","ghost of ghost of ghost of ghost of ghost of toudy49","ghost ofghost of ghost of ghost of ghost of ghost of toudy49","ghostof ghost of ghost of ghost of ghost of ghost of toudy49","(ghost of)^6  * toudy49","(ghost of)^7  * toudy49","(ghost of)^8  * toudy49","(ghost of)^9  * toudy49","(ghost of)^10  * toudy49"],"nameChangedAt":"2022-04-11T21:01:19.938445828Z","scopes":[""]},"body":"!hydrate"},{"type":"CHAT","id":"T4tTsuwng","timestamp":"2022-04-28T20:53:49.391636551Z","user":{"id":"fKINHKpnR","displayName":"hatnixbot","displayColor":325,"createdAt":"2021-11-24T08:11:32Z","previousNames":["hatnixbot"],"scopes":["CAN_SEND_SYSTEM_MESSAGES","CAN_SEND_MESSAGES","HAS_ADMIN_ACCESS"]},"body":"test 123"},{"type":"CHAT","id":"wUJTsuw7R","timestamp":"2022-04-28T20:53:54.073218761Z","user":{"id":"GCa3J9P7R","displayName":"(ghost of)^10  * toudy49","displayColor":147,"createdAt":"2022-03-22T21:49:25.284237821Z","previousNames":["lucid-pike","toudy49","ghost of toudy49","ghost of ghost of toudy49","ghost of ghost of ghost of toudy49","ghost of ghost of ghost of ghost of toudy49","ghost of ghost of ghost of ghost of ghost of toudy49","ghost ofghost of ghost of ghost of ghost of ghost of toudy49","ghostof ghost of ghost of ghost of ghost of ghost of toudy49","(ghost of)^6  * toudy49","(ghost of)^7  * toudy49","(ghost of)^8  * toudy49","(ghost of)^9  * toudy49","(ghost of)^10  * toudy49"],"nameChangedAt":"2022-04-11T21:01:19.938445828Z","scopes":[""]},"body":"!stretch"},{"type":"CHAT","id":"S_Joyuw7R","timestamp":"2022-04-28T20:53:54.119778013Z","user":{"id":"fKINHKpnR","displayName":"hatnixbot","displayColor":325,"createdAt":"2021-11-24T08:11:32Z","previousNames":["hatnixbot"],"scopes":["CAN_SEND_SYSTEM_MESSAGES","CAN_SEND_MESSAGES","HAS_ADMIN_ACCESS"]},"body":"blah blah"},{"type":"CHAT","id":"MtYTyXwnR","timestamp":"2022-04-28T20:53:57.796985761Z","user":{"id":"vbh9gtPng","displayName":"𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™","displayColor":276,"createdAt":"2022-03-16T21:02:32.009965702Z","previousNames":["goth-volhard","𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™","𝒽𝒶𝓅𝓅𝓎 𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™","𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™","𝓈𝓉𝒶𝒶𝓇𝒻𝒶𝒶𝓇𝑒𝑒𝓇™","𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™"],"nameChangedAt":"2022-04-14T21:51:50.97992512Z","scopes":[""]},"body":"heyy toudy"}]';
@@ -24,11 +26,13 @@ const Page = () => {
   return <ReadOnlyPage />;
 };
 
-const Template: ComponentStory<typeof ReadOnlyPage> = () => (
+const Template: StoryFn<typeof ReadOnlyPage> = () => (
   <RecoilRoot>
     <Page />
   </RecoilRoot>
 );
 
-export const Example = Template.bind({});
-Example.args = {};
+export const Example = {
+  render: Template,
+  args: {},
+};
diff --git a/web/stories/ReadwriteChat.stories.tsx b/web/stories/ReadwriteChat.stories.tsx
index 6788a9c03..7eb0f4ab6 100644
--- a/web/stories/ReadwriteChat.stories.tsx
+++ b/web/stories/ReadwriteChat.stories.tsx
@@ -1,5 +1,5 @@
-import React, { useEffect } from 'react';
-import { ComponentStory, ComponentMeta } from '@storybook/react';
+import { useEffect } from 'react';
+import { StoryFn, Meta } from '@storybook/react';
 import { RecoilRoot, useRecoilState, useSetRecoilState } from 'recoil';
 import ReadWritePage from '../pages/embed/chat/readwrite/index';
 import { ChatMessage } from '../interfaces/chat-message.model';
@@ -10,13 +10,15 @@ import {
 } from '../components/stores/ClientConfigStore';
 import { ClientConfig } from '../interfaces/client-config.model';
 
-export default {
+const meta = {
   title: 'owncast/Chat/Embeds/Read-write chat',
   component: ReadWritePage,
   parameters: {
     chromatic: { diffThreshold: 0.8 },
   },
-} as ComponentMeta<typeof ReadWritePage>;
+} satisfies Meta<typeof ReadWritePage>;
+
+export default meta;
 
 const testMessages =
   '[{"type":"CHAT","id":"wY-MEXwnR","timestamp":"2022-04-28T20:30:27.001762726Z","user":{"id":"h_5GQ6E7R","displayName":"UserDisplayName42","displayColor":329,"createdAt":"2022-03-24T03:52:37.966584694Z","previousNames":["gifted-nobel","EliteMooseTaskForce"],"nameChangedAt":"2022-04-26T23:56:05.531287897Z","scopes":[""]},"body":"this is a test message"},{"type":"CHAT","id":"VhLGEXwnR","timestamp":"2022-04-28T20:30:28.806999545Z","user":{"id":"h_5GQ6E7R","displayName":"EliteMooseTaskForce","displayColor":329,"createdAt":"2022-03-24T03:52:37.966584694Z","previousNames":["gifted-nobel","EliteMooseTaskForce"],"nameChangedAt":"2022-04-26T23:56:05.531287897Z","scopes":[""]},"body":"Hit 3"},{"type":"CHAT","id":"GguMEuw7R","timestamp":"2022-04-28T20:30:34.500150601Z","user":{"id":"h_5GQ6E7R","displayName":"EliteMooseTaskForce","displayColor":329,"createdAt":"2022-03-24T03:52:37.966584694Z","previousNames":["gifted-nobel","EliteMooseTaskForce"],"nameChangedAt":"2022-04-26T23:56:05.531287897Z","scopes":[""]},"body":"Jkjk"},{"type":"CHAT","id":"y_-VEXwnR","timestamp":"2022-04-28T20:31:32.695583044Z","user":{"id":"h_5GQ6E7R","displayName":"EliteMooseTaskForce","displayColor":329,"createdAt":"2022-03-24T03:52:37.966584694Z","previousNames":["gifted-nobel","EliteMooseTaskForce"],"nameChangedAt":"2022-04-26T23:56:05.531287897Z","scopes":[""]},"body":"I\u0026#39;m doing alright. How about you Hatnix?"},{"type":"CHAT","id":"qAaKEuwng","timestamp":"2022-04-28T20:34:16.22275314Z","user":{"id":"h_5GQ6E7R","displayName":"EliteMooseTaskForce","displayColor":329,"createdAt":"2022-03-24T03:52:37.966584694Z","previousNames":["gifted-nobel","EliteMooseTaskForce"],"nameChangedAt":"2022-04-26T23:56:05.531287897Z","scopes":[""]},"body":"Oh shiet I didn\u0026#39;t think you would kill him"},{"type":"CHAT","id":"8wUFEuwnR","timestamp":"2022-04-28T20:34:21.624898714Z","user":{"id":"h_5GQ6E7R","displayName":"EliteMooseTaskForce","displayColor":329,"createdAt":"2022-03-24T03:52:37.966584694Z","previousNames":["gifted-nobel","EliteMooseTaskForce"],"nameChangedAt":"2022-04-26T23:56:05.531287897Z","scopes":[""]},"body":"Hahaha, ruthless"},{"type":"CHAT","id":"onYcPuQnR","timestamp":"2022-04-28T20:34:50.671024312Z","user":{"id":"h_5GQ6E7R","displayName":"EliteMooseTaskForce","displayColor":329,"createdAt":"2022-03-24T03:52:37.966584694Z","previousNames":["gifted-nobel","EliteMooseTaskForce"],"nameChangedAt":"2022-04-26T23:56:05.531287897Z","scopes":[""]},"body":"I\u0026#39;ve never played it before"},{"type":"CHAT","id":"kORyEXQ7R","timestamp":"2022-04-28T20:40:29.761977233Z","user":{"id":"h_5GQ6E7R","displayName":"EliteMooseTaskForce","displayColor":329,"createdAt":"2022-03-24T03:52:37.966584694Z","previousNames":["gifted-nobel","EliteMooseTaskForce"],"nameChangedAt":"2022-04-26T23:56:05.531287897Z","scopes":[""]},"body":"brb real quick"},{"type":"CHAT","id":"F3DvsuQ7g","timestamp":"2022-04-28T20:50:29.451341783Z","user":{"id":"h_5GQ6E7R","displayName":"EliteMooseTaskForce","displayColor":329,"createdAt":"2022-03-24T03:52:37.966584694Z","previousNames":["gifted-nobel","EliteMooseTaskForce"],"nameChangedAt":"2022-04-26T23:56:05.531287897Z","scopes":[""]},"body":"I\u0026#39;m back"},{"type":"CHAT","id":"AH2vsXwnR","timestamp":"2022-04-28T20:50:33.872156152Z","user":{"id":"h_5GQ6E7R","displayName":"EliteMooseTaskForce","displayColor":329,"createdAt":"2022-03-24T03:52:37.966584694Z","previousNames":["gifted-nobel","EliteMooseTaskForce"],"nameChangedAt":"2022-04-26T23:56:05.531287897Z","scopes":[""]},"body":"Whoa what happened here?"},{"type":"CHAT","id":"xGkOsuw7R","timestamp":"2022-04-28T20:50:53.202147658Z","user":{"id":"h_5GQ6E7R","displayName":"EliteMooseTaskForce","displayColor":329,"createdAt":"2022-03-24T03:52:37.966584694Z","previousNames":["gifted-nobel","EliteMooseTaskForce"],"nameChangedAt":"2022-04-26T23:56:05.531287897Z","scopes":[""]},"body":"Your dwarf was half naked."},{"type":"CHAT","id":"opIdsuw7g","timestamp":"2022-04-28T20:50:59.631595947Z","user":{"id":"h_5GQ6E7R","displayName":"EliteMooseTaskForce","displayColor":329,"createdAt":"2022-03-24T03:52:37.966584694Z","previousNames":["gifted-nobel","EliteMooseTaskForce"],"nameChangedAt":"2022-04-26T23:56:05.531287897Z","scopes":[""]},"body":"lol"},{"type":"CHAT","id":"JpwdsuQnR","timestamp":"2022-04-28T20:51:18.065535459Z","user":{"id":"vbh9gtPng","displayName":"𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™","displayColor":276,"createdAt":"2022-03-16T21:02:32.009965702Z","previousNames":["goth-volhard","𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™","𝒽𝒶𝓅𝓅𝓎 𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™","𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™","𝓈𝓉𝒶𝒶𝓇𝒻𝒶𝒶𝓇𝑒𝑒𝓇™","𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™"],"nameChangedAt":"2022-04-14T21:51:50.97992512Z","scopes":[""]},"body":"evening did i just see you running around in... nothing"},{"type":"CHAT","id":"R4WKsXw7R","timestamp":"2022-04-28T20:51:28.064914803Z","user":{"id":"vbh9gtPng","displayName":"𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™","displayColor":276,"createdAt":"2022-03-16T21:02:32.009965702Z","previousNames":["goth-volhard","𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™","𝒽𝒶𝓅𝓅𝓎 𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™","𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™","𝓈𝓉𝒶𝒶𝓇𝒻𝒶𝒶𝓇𝑒𝑒𝓇™","𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™"],"nameChangedAt":"2022-04-14T21:51:50.97992512Z","scopes":[""]},"body":"^^"},{"type":"CHAT","id":"g-PKyXw7g","timestamp":"2022-04-28T20:51:47.936500772Z","user":{"id":"h_5GQ6E7R","displayName":"EliteMooseTaskForce","displayColor":329,"createdAt":"2022-03-24T03:52:37.966584694Z","previousNames":["gifted-nobel","EliteMooseTaskForce"],"nameChangedAt":"2022-04-26T23:56:05.531287897Z","scopes":[""]},"body":"Lol Starfarer, so my eyes didnt deceive me."},{"type":"CHAT","id":"fV8Ksuw7R","timestamp":"2022-04-28T20:51:49.588744112Z","user":{"id":"h_5GQ6E7R","displayName":"EliteMooseTaskForce","displayColor":329,"createdAt":"2022-03-24T03:52:37.966584694Z","previousNames":["gifted-nobel","EliteMooseTaskForce"],"nameChangedAt":"2022-04-26T23:56:05.531287897Z","scopes":[""]},"body":"hahahaha"},{"type":"CHAT","id":"TaStyuwnR","timestamp":"2022-04-28T20:52:38.127528579Z","user":{"id":"vbh9gtPng","displayName":"𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™","displayColor":276,"createdAt":"2022-03-16T21:02:32.009965702Z","previousNames":["goth-volhard","𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™","𝒽𝒶𝓅𝓅𝓎 𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™","𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™","𝓈𝓉𝒶𝒶𝓇𝒻𝒶𝒶𝓇𝑒𝑒𝓇™","𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™"],"nameChangedAt":"2022-04-14T21:51:50.97992512Z","scopes":[""]},"body":"lol sounds nice"},{"type":"CHAT","id":"JGposuwng","timestamp":"2022-04-28T20:53:49.329567087Z","user":{"id":"GCa3J9P7R","displayName":"(ghost of)^10  * toudy49","displayColor":147,"createdAt":"2022-03-22T21:49:25.284237821Z","previousNames":["lucid-pike","toudy49","ghost of toudy49","ghost of ghost of toudy49","ghost of ghost of ghost of toudy49","ghost of ghost of ghost of ghost of toudy49","ghost of ghost of ghost of ghost of ghost of toudy49","ghost ofghost of ghost of ghost of ghost of ghost of toudy49","ghostof ghost of ghost of ghost of ghost of ghost of toudy49","(ghost of)^6  * toudy49","(ghost of)^7  * toudy49","(ghost of)^8  * toudy49","(ghost of)^9  * toudy49","(ghost of)^10  * toudy49"],"nameChangedAt":"2022-04-11T21:01:19.938445828Z","scopes":[""]},"body":"!hydrate"},{"type":"CHAT","id":"T4tTsuwng","timestamp":"2022-04-28T20:53:49.391636551Z","user":{"id":"fKINHKpnR","displayName":"hatnixbot","displayColor":325,"createdAt":"2021-11-24T08:11:32Z","previousNames":["hatnixbot"],"scopes":["CAN_SEND_SYSTEM_MESSAGES","CAN_SEND_MESSAGES","HAS_ADMIN_ACCESS"]},"body":"test 123"},{"type":"CHAT","id":"wUJTsuw7R","timestamp":"2022-04-28T20:53:54.073218761Z","user":{"id":"GCa3J9P7R","displayName":"(ghost of)^10  * toudy49","displayColor":147,"createdAt":"2022-03-22T21:49:25.284237821Z","previousNames":["lucid-pike","toudy49","ghost of toudy49","ghost of ghost of toudy49","ghost of ghost of ghost of toudy49","ghost of ghost of ghost of ghost of toudy49","ghost of ghost of ghost of ghost of ghost of toudy49","ghost ofghost of ghost of ghost of ghost of ghost of toudy49","ghostof ghost of ghost of ghost of ghost of ghost of toudy49","(ghost of)^6  * toudy49","(ghost of)^7  * toudy49","(ghost of)^8  * toudy49","(ghost of)^9  * toudy49","(ghost of)^10  * toudy49"],"nameChangedAt":"2022-04-11T21:01:19.938445828Z","scopes":[""]},"body":"!stretch"},{"type":"CHAT","id":"S_Joyuw7R","timestamp":"2022-04-28T20:53:54.119778013Z","user":{"id":"fKINHKpnR","displayName":"hatnixbot","displayColor":325,"createdAt":"2021-11-24T08:11:32Z","previousNames":["hatnixbot"],"scopes":["CAN_SEND_SYSTEM_MESSAGES","CAN_SEND_MESSAGES","HAS_ADMIN_ACCESS"]},"body":"blah blah"},{"type":"CHAT","id":"MtYTyXwnR","timestamp":"2022-04-28T20:53:57.796985761Z","user":{"id":"vbh9gtPng","displayName":"𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™","displayColor":276,"createdAt":"2022-03-16T21:02:32.009965702Z","previousNames":["goth-volhard","𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™","𝒽𝒶𝓅𝓅𝓎 𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™","𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™","𝓈𝓉𝒶𝒶𝓇𝒻𝒶𝒶𝓇𝑒𝑒𝓇™","𝓈𝓉𝒶𝓇𝒻𝒶𝓇𝑒𝓇™"],"nameChangedAt":"2022-04-14T21:51:50.97992512Z","scopes":[""]},"body":"heyy toudy"}]';
@@ -58,11 +60,13 @@ const Page = () => {
   return <ReadWritePage />;
 };
 
-const Template: ComponentStory<typeof ReadWritePage> = () => (
+const Template: StoryFn<typeof ReadWritePage> = () => (
   <RecoilRoot>
     <Page />
   </RecoilRoot>
 );
 
-export const Example = Template.bind({});
-Example.args = {};
+export const Example = {
+  render: Template,
+  args: {},
+};