diff --git a/packages/component-library/.storybook/main.ts b/packages/component-library/.storybook/main.ts
index 77afada247..5a5fece876 100644
--- a/packages/component-library/.storybook/main.ts
+++ b/packages/component-library/.storybook/main.ts
@@ -13,7 +13,8 @@ function getAbsolutePath(value: string) {
}
const config: StorybookConfig = {
stories: [
- '../src/Introduction.mdx',
+ '../src/Concepts/*.mdx',
+ '../src/Themes/*.mdx',
'../src/**/*.mdx',
'../src/**/*.stories.@(js|jsx|mjs|ts|tsx)',
],
diff --git a/packages/component-library/.storybook/manager-head.html b/packages/component-library/.storybook/manager-head.html
new file mode 100644
index 0000000000..5e01a16d90
--- /dev/null
+++ b/packages/component-library/.storybook/manager-head.html
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/component-library/.storybook/public/favicon.ico b/packages/component-library/.storybook/public/favicon.ico
new file mode 100644
index 0000000000..a88b044eb0
Binary files /dev/null and b/packages/component-library/.storybook/public/favicon.ico differ
diff --git a/packages/component-library/.storybook/public/global-styles.css b/packages/component-library/.storybook/public/global-styles.css
new file mode 100644
index 0000000000..d63ee6204e
--- /dev/null
+++ b/packages/component-library/.storybook/public/global-styles.css
@@ -0,0 +1 @@
+/* Custom Storybook Styling */
diff --git a/packages/component-library/.storybook/public/og.webp b/packages/component-library/.storybook/public/og.webp
new file mode 100644
index 0000000000..a98c0f43aa
Binary files /dev/null and b/packages/component-library/.storybook/public/og.webp differ
diff --git a/packages/component-library/src/AlignedText.stories.tsx b/packages/component-library/src/AlignedText.stories.tsx
index 595eacba3e..c9ac70d2eb 100644
--- a/packages/component-library/src/AlignedText.stories.tsx
+++ b/packages/component-library/src/AlignedText.stories.tsx
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite';
import { AlignedText } from './AlignedText';
const meta = {
- title: 'AlignedText',
+ title: 'Components/AlignedText',
component: AlignedText,
parameters: {
layout: 'centered',
diff --git a/packages/component-library/src/Block.stories.tsx b/packages/component-library/src/Block.stories.tsx
index 1da39fa88a..be3693004a 100644
--- a/packages/component-library/src/Block.stories.tsx
+++ b/packages/component-library/src/Block.stories.tsx
@@ -4,7 +4,7 @@ import { Block } from './Block';
import { theme } from './theme';
const meta = {
- title: 'Block',
+ title: 'Components/Block',
component: Block,
parameters: {
layout: 'centered',
diff --git a/packages/component-library/src/Button.stories.ts b/packages/component-library/src/Button.stories.ts
index 9325f4b6d5..aafe00b08b 100644
--- a/packages/component-library/src/Button.stories.ts
+++ b/packages/component-library/src/Button.stories.ts
@@ -4,7 +4,7 @@ import { fn } from 'storybook/test';
import { Button } from './Button';
const meta = {
- title: 'Button',
+ title: 'Components/Button',
component: Button,
parameters: {
layout: 'centered',
diff --git a/packages/component-library/src/Card.stories.tsx b/packages/component-library/src/Card.stories.tsx
index edc3221bbf..6cac8b1c6d 100644
--- a/packages/component-library/src/Card.stories.tsx
+++ b/packages/component-library/src/Card.stories.tsx
@@ -6,7 +6,7 @@ import { Paragraph } from './Paragraph';
import { theme } from './theme';
const meta = {
- title: 'Card',
+ title: 'Components/Card',
component: Card,
parameters: {
layout: 'centered',
diff --git a/packages/component-library/src/ColorPicker.stories.tsx b/packages/component-library/src/ColorPicker.stories.tsx
index a838e64447..0d96d9adb2 100644
--- a/packages/component-library/src/ColorPicker.stories.tsx
+++ b/packages/component-library/src/ColorPicker.stories.tsx
@@ -8,7 +8,7 @@ import { Button } from './Button';
import { ColorPicker } from './ColorPicker';
const meta = {
- title: 'ColorPicker',
+ title: 'Components/ColorPicker',
component: ColorPicker,
parameters: {
layout: 'centered',
diff --git a/packages/component-library/src/Introduction.mdx b/packages/component-library/src/Concepts/Introduction.mdx
similarity index 95%
rename from packages/component-library/src/Introduction.mdx
rename to packages/component-library/src/Concepts/Introduction.mdx
index 8954862d7f..616486e9cf 100644
--- a/packages/component-library/src/Introduction.mdx
+++ b/packages/component-library/src/Concepts/Introduction.mdx
@@ -1,6 +1,6 @@
import { Meta } from '@storybook/addon-docs/blocks';
-
+
# Actual Budget Component Library
diff --git a/packages/component-library/src/FormError.stories.tsx b/packages/component-library/src/FormError.stories.tsx
index a76ff9054f..d03c19abc2 100644
--- a/packages/component-library/src/FormError.stories.tsx
+++ b/packages/component-library/src/FormError.stories.tsx
@@ -5,7 +5,7 @@ import { Input } from './Input';
import { View } from './View';
const meta = {
- title: 'FormError',
+ title: 'Components/FormError',
component: FormError,
parameters: {
layout: 'centered',
diff --git a/packages/component-library/src/InitialFocus.stories.tsx b/packages/component-library/src/InitialFocus.stories.tsx
new file mode 100644
index 0000000000..b92008e14d
--- /dev/null
+++ b/packages/component-library/src/InitialFocus.stories.tsx
@@ -0,0 +1,86 @@
+import { type Ref } from 'react';
+
+import type { Meta, StoryObj } from '@storybook/react-vite';
+
+import { InitialFocus } from './InitialFocus';
+import { Input } from './Input';
+import { View } from './View';
+
+const meta = {
+ title: 'Components/InitialFocus',
+ component: InitialFocus,
+ parameters: {
+ layout: 'centered',
+ },
+ tags: ['autodocs'],
+} satisfies Meta;
+
+export default meta;
+
+type Story = StoryObj;
+
+export const WithInput: Story = {
+ args: {
+ children: ,
+ },
+ render: args => (
+
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'InitialFocus automatically focuses its child element when the component mounts. The input will receive focus and have its text selected.',
+ },
+ },
+ },
+};
+
+export const WithFunctionChild: Story = {
+ args: {
+ children: ,
+ },
+ render: () => (
+
+
+ {ref => (
+ }
+ placeholder="Focused via function child"
+ />
+ )}
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'InitialFocus can accept a function as child for components that need custom ref handling.',
+ },
+ },
+ },
+};
+
+export const MultipleInputsOnlyFirstFocused: Story = {
+ args: {
+ children: ,
+ },
+ render: args => (
+
+
+
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'When multiple inputs are present, only the one wrapped in InitialFocus will receive initial focus.',
+ },
+ },
+ },
+};
diff --git a/packages/component-library/src/InlineField.stories.tsx b/packages/component-library/src/InlineField.stories.tsx
new file mode 100644
index 0000000000..5fee031416
--- /dev/null
+++ b/packages/component-library/src/InlineField.stories.tsx
@@ -0,0 +1,101 @@
+import type { Meta, StoryObj } from '@storybook/react-vite';
+
+import { InlineField } from './InlineField';
+import { Input } from './Input';
+import { View } from './View';
+
+const meta = {
+ title: 'Components/InlineField',
+ component: InlineField,
+ parameters: {
+ layout: 'centered',
+ },
+ tags: ['autodocs'],
+} satisfies Meta;
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ label: 'Name',
+ width: 300,
+ children: ,
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'InlineField displays a label and input side-by-side in a horizontal layout.',
+ },
+ },
+ },
+};
+
+export const WithCustomLabelWidth: Story = {
+ args: {
+ label: 'Email Address',
+ labelWidth: 120,
+ width: 400,
+ children: ,
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Custom label width can be specified to accommodate longer labels.',
+ },
+ },
+ },
+};
+
+export const MultipleFields: Story = {
+ args: {
+ label: 'First Name',
+ width: 300,
+ },
+ render: args => (
+
+
+
+
+
+
+
+
+
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Multiple InlineFields stack vertically with consistent label alignment.',
+ },
+ },
+ },
+};
+
+export const WithPercentageWidth: Story = {
+ args: {
+ label: 'Description',
+ width: '100%',
+ children: ,
+ },
+ decorators: [
+ Story => (
+
+
+
+ ),
+ ],
+ parameters: {
+ docs: {
+ description: {
+ story: 'Width can be specified as a percentage for responsive layouts.',
+ },
+ },
+ },
+};
diff --git a/packages/component-library/src/Input.stories.tsx b/packages/component-library/src/Input.stories.tsx
new file mode 100644
index 0000000000..fa6f673cea
--- /dev/null
+++ b/packages/component-library/src/Input.stories.tsx
@@ -0,0 +1,215 @@
+import { useState } from 'react';
+
+import type { Meta, StoryObj } from '@storybook/react-vite';
+
+import { Input } from './Input';
+import { View } from './View';
+
+const meta = {
+ title: 'Components/Input',
+ component: Input,
+ parameters: {
+ layout: 'centered',
+ },
+ tags: ['autodocs'],
+} satisfies Meta;
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ placeholder: 'Enter text...',
+ },
+ decorators: [
+ Story => (
+
+
+
+ ),
+ ],
+ parameters: {
+ docs: {
+ description: {
+ story: 'A basic input field with placeholder text.',
+ },
+ },
+ },
+};
+
+export const WithValue: Story = {
+ args: {
+ defaultValue: 'Hello World',
+ },
+ decorators: [
+ Story => (
+
+
+
+ ),
+ ],
+ parameters: {
+ docs: {
+ description: {
+ story: 'Input with a pre-filled value.',
+ },
+ },
+ },
+};
+
+export const Disabled: Story = {
+ args: {
+ defaultValue: 'Disabled input',
+ disabled: true,
+ },
+ decorators: [
+ Story => (
+
+
+
+ ),
+ ],
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Disabled inputs prevent user interaction and display muted text.',
+ },
+ },
+ },
+};
+
+export const WithOnEnter: Story = {
+ render: function Render() {
+ const [submittedValue, setSubmittedValue] = useState('');
+
+ return (
+
+ setSubmittedValue(value)}
+ />
+ {submittedValue && Submitted: {submittedValue}}
+
+ );
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: 'The onEnter callback is triggered when the user presses Enter.',
+ },
+ },
+ },
+};
+
+export const WithOnEscape: Story = {
+ render: function Render() {
+ const [escaped, setEscaped] = useState(false);
+
+ return (
+
+ setEscaped(true)}
+ />
+ {escaped && Escape pressed!}
+
+ );
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'The onEscape callback is triggered when the user presses Escape.',
+ },
+ },
+ },
+};
+
+export const WithOnChangeValue: Story = {
+ render: function Render() {
+ const [value, setValue] = useState('');
+
+ return (
+
+ setValue(newValue)}
+ />
+ Current value: {value}
+
+ );
+ },
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'The onChangeValue callback provides the new value on each keystroke.',
+ },
+ },
+ },
+};
+
+export const NumberInput: Story = {
+ args: {
+ type: 'number',
+ placeholder: '0',
+ },
+ decorators: [
+ Story => (
+
+
+
+ ),
+ ],
+ parameters: {
+ docs: {
+ description: {
+ story: 'Input configured for numeric values.',
+ },
+ },
+ },
+};
+
+export const PasswordInput: Story = {
+ args: {
+ type: 'password',
+ placeholder: 'Enter password',
+ },
+ decorators: [
+ Story => (
+
+
+
+ ),
+ ],
+ parameters: {
+ docs: {
+ description: {
+ story: 'Password input masks the entered text.',
+ },
+ },
+ },
+};
diff --git a/packages/component-library/src/Label.stories.tsx b/packages/component-library/src/Label.stories.tsx
new file mode 100644
index 0000000000..a14df89b66
--- /dev/null
+++ b/packages/component-library/src/Label.stories.tsx
@@ -0,0 +1,97 @@
+import type { Meta, StoryObj } from '@storybook/react-vite';
+
+import { Input } from './Input';
+import { Label } from './Label';
+import { View } from './View';
+
+const meta = {
+ title: 'Components/Label',
+ component: Label,
+ parameters: {
+ layout: 'centered',
+ },
+ tags: ['autodocs'],
+} satisfies Meta;
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ title: 'Username',
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: 'A basic label component for form fields.',
+ },
+ },
+ },
+};
+
+export const WithInput: Story = {
+ args: {
+ title: 'Email Address',
+ },
+ render: args => (
+
+
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: 'Label used with an input field in a vertical layout.',
+ },
+ },
+ },
+};
+
+export const MultipleLabels: Story = {
+ args: {
+ title: 'First Name',
+ },
+ render: args => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story: 'Multiple labels and inputs in a form layout.',
+ },
+ },
+ },
+};
+
+export const CustomStyle: Story = {
+ args: {
+ title: 'Custom Styled Label',
+ style: {
+ fontSize: 16,
+ color: '#007bff',
+ textAlign: 'left',
+ },
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: 'Label with custom styling applied.',
+ },
+ },
+ },
+};
diff --git a/packages/component-library/src/Menu.stories.tsx b/packages/component-library/src/Menu.stories.tsx
new file mode 100644
index 0000000000..491aff0dcd
--- /dev/null
+++ b/packages/component-library/src/Menu.stories.tsx
@@ -0,0 +1,243 @@
+import { useState } from 'react';
+
+import type { Meta, StoryObj } from '@storybook/react-vite';
+
+import { SvgAdd, SvgTrash } from './icons/v1';
+import { SvgPencil1 } from './icons/v2';
+import { Menu, type MenuItem } from './Menu';
+import { Text } from './Text';
+import { View } from './View';
+
+const meta = {
+ title: 'Components/Menu',
+ component: Menu,
+ parameters: {
+ layout: 'centered',
+ },
+ tags: ['autodocs'],
+} satisfies Meta;
+
+export default meta;
+
+type Story = StoryObj;
+
+const basicItems: Array