@sovereignfs/ui 0.40.0 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Accordion.module.css +3 -0
- package/dist/Alert.module.css +62 -0
- package/dist/AspectRatio.module.css +15 -0
- package/dist/Badge.module.css +75 -9
- package/dist/BalanceChip.module.css +32 -0
- package/dist/Breadcrumb.module.css +49 -0
- package/dist/Button.module.css +12 -3
- package/dist/ButtonGroup.module.css +29 -0
- package/dist/Card.module.css +6 -0
- package/dist/Checkbox.module.css +3 -1
- package/dist/Collapsible.module.css +70 -0
- package/dist/Combobox.module.css +122 -0
- package/dist/Command.module.css +80 -0
- package/dist/ConfirmDialog.module.css +5 -1
- package/dist/DataTable.module.css +28 -0
- package/dist/DatePicker.module.css +6 -2
- package/dist/Dialog.module.css +3 -0
- package/dist/HoverCard.module.css +3 -0
- package/dist/Input.module.css +6 -1
- package/dist/Item.module.css +70 -0
- package/dist/Kbd.module.css +14 -0
- package/dist/Label.module.css +11 -0
- package/dist/Marker.module.css +33 -0
- package/dist/MemberMultiSelect.module.css +27 -0
- package/dist/Menu.module.css +8 -1
- package/dist/Message.module.css +99 -0
- package/dist/MessageScroller.module.css +54 -0
- package/dist/NavigationMenu.module.css +53 -0
- package/dist/Pagination.module.css +98 -0
- package/dist/Progress.module.css +20 -0
- package/dist/RadioGroup.module.css +88 -0
- package/dist/Resizable.module.css +70 -0
- package/dist/ScrollArea.module.css +20 -0
- package/dist/Select.module.css +7 -3
- package/dist/Slider.module.css +95 -0
- package/dist/Table.module.css +45 -0
- package/dist/Tabs.module.css +6 -1
- package/dist/Toggle.module.css +6 -1
- package/dist/Typography.module.css +47 -0
- package/dist/index.d.ts +698 -5
- package/dist/index.js +1938 -482
- package/dist/tokens/primitives.css +22 -8
- package/package.json +1 -1
- package/src/components/Accordion/Accordion.module.css +3 -0
- package/src/components/Accordion/Accordion.stories.tsx +110 -0
- package/src/components/Accordion/Accordion.tsx +53 -0
- package/src/components/Accordion/__tests__/Accordion.test.tsx +61 -0
- package/src/components/Alert/Alert.module.css +62 -0
- package/src/components/Alert/Alert.stories.tsx +96 -0
- package/src/components/Alert/Alert.tsx +58 -0
- package/src/components/Alert/__tests__/Alert.test.tsx +75 -0
- package/src/components/AspectRatio/AspectRatio.module.css +15 -0
- package/src/components/AspectRatio/AspectRatio.stories.tsx +78 -0
- package/src/components/AspectRatio/AspectRatio.tsx +21 -0
- package/src/components/AspectRatio/__tests__/AspectRatio.test.tsx +27 -0
- package/src/components/Badge/Badge.module.css +75 -9
- package/src/components/Badge/Badge.stories.tsx +109 -1
- package/src/components/Badge/Badge.tsx +30 -2
- package/src/components/Badge/__tests__/Badge.test.tsx +26 -2
- package/src/components/BalanceChip/BalanceChip.module.css +32 -0
- package/src/components/BalanceChip/BalanceChip.stories.tsx +45 -0
- package/src/components/BalanceChip/BalanceChip.tsx +33 -0
- package/src/components/Breadcrumb/Breadcrumb.module.css +49 -0
- package/src/components/Breadcrumb/Breadcrumb.stories.tsx +47 -0
- package/src/components/Breadcrumb/Breadcrumb.tsx +57 -0
- package/src/components/Breadcrumb/__tests__/Breadcrumb.test.tsx +52 -0
- package/src/components/Button/Button.module.css +12 -3
- package/src/components/Button/Button.stories.tsx +3 -2
- package/src/components/Button/Button.tsx +1 -1
- package/src/components/Button/__tests__/Button.test.tsx +10 -0
- package/src/components/ButtonGroup/ButtonGroup.module.css +29 -0
- package/src/components/ButtonGroup/ButtonGroup.stories.tsx +57 -0
- package/src/components/ButtonGroup/ButtonGroup.tsx +27 -0
- package/src/components/ButtonGroup/__tests__/ButtonGroup.test.tsx +29 -0
- package/src/components/Card/Card.module.css +6 -0
- package/src/components/Checkbox/Checkbox.module.css +3 -1
- package/src/components/Collapsible/Collapsible.module.css +70 -0
- package/src/components/Collapsible/Collapsible.stories.tsx +62 -0
- package/src/components/Collapsible/Collapsible.tsx +66 -0
- package/src/components/Collapsible/__tests__/Collapsible.test.tsx +71 -0
- package/src/components/Combobox/Combobox.module.css +122 -0
- package/src/components/Combobox/Combobox.stories.tsx +79 -0
- package/src/components/Combobox/Combobox.tsx +186 -0
- package/src/components/Combobox/__tests__/Combobox.test.tsx +158 -0
- package/src/components/Command/Command.module.css +80 -0
- package/src/components/Command/Command.stories.tsx +104 -0
- package/src/components/Command/Command.tsx +168 -0
- package/src/components/Command/__tests__/Command.test.tsx +162 -0
- package/src/components/ConfirmDialog/ConfirmDialog.module.css +5 -1
- package/src/components/ContextMenu/ContextMenu.stories.tsx +81 -0
- package/src/components/ContextMenu/ContextMenu.tsx +100 -0
- package/src/components/ContextMenu/__tests__/ContextMenu.test.tsx +87 -0
- package/src/components/CurrencyInput/CurrencyInput.stories.tsx +50 -0
- package/src/components/CurrencyInput/CurrencyInput.tsx +60 -0
- package/src/components/DataTable/DataTable.module.css +28 -0
- package/src/components/DataTable/DataTable.stories.tsx +74 -0
- package/src/components/DataTable/DataTable.tsx +154 -0
- package/src/components/DataTable/__tests__/DataTable.test.tsx +123 -0
- package/src/components/DatePicker/DatePicker.module.css +6 -2
- package/src/components/Dialog/Dialog.module.css +3 -0
- package/src/components/HoverCard/HoverCard.module.css +3 -0
- package/src/components/HoverCard/HoverCard.stories.tsx +66 -0
- package/src/components/HoverCard/HoverCard.tsx +111 -0
- package/src/components/HoverCard/__tests__/HoverCard.test.tsx +99 -0
- package/src/components/Icon/icons/circle-check.tsx +21 -0
- package/src/components/Icon/icons/circle-x.tsx +22 -0
- package/src/components/Icon/icons/index.ts +8 -0
- package/src/components/Input/Input.module.css +6 -1
- package/src/components/Item/Item.module.css +70 -0
- package/src/components/Item/Item.stories.tsx +62 -0
- package/src/components/Item/Item.tsx +54 -0
- package/src/components/Item/__tests__/Item.test.tsx +39 -0
- package/src/components/Kbd/Kbd.module.css +14 -0
- package/src/components/Kbd/Kbd.stories.tsx +32 -0
- package/src/components/Kbd/Kbd.tsx +12 -0
- package/src/components/Kbd/__tests__/Kbd.test.tsx +14 -0
- package/src/components/Label/Label.module.css +11 -0
- package/src/components/Label/Label.stories.tsx +40 -0
- package/src/components/Label/Label.tsx +24 -0
- package/src/components/Label/__tests__/Label.test.tsx +32 -0
- package/src/components/Marker/Marker.module.css +33 -0
- package/src/components/Marker/Marker.stories.tsx +39 -0
- package/src/components/Marker/Marker.tsx +34 -0
- package/src/components/Marker/__tests__/Marker.test.tsx +23 -0
- package/src/components/MemberMultiSelect/MemberMultiSelect.module.css +27 -0
- package/src/components/MemberMultiSelect/MemberMultiSelect.stories.tsx +74 -0
- package/src/components/MemberMultiSelect/MemberMultiSelect.tsx +67 -0
- package/src/components/Menu/Menu.module.css +8 -1
- package/src/components/Menu/Menu.tsx +30 -8
- package/src/components/Menubar/Menubar.stories.tsx +52 -0
- package/src/components/Menubar/Menubar.tsx +42 -0
- package/src/components/Menubar/__tests__/Menubar.test.tsx +45 -0
- package/src/components/Message/Message.module.css +99 -0
- package/src/components/Message/Message.stories.tsx +79 -0
- package/src/components/Message/Message.tsx +41 -0
- package/src/components/Message/__tests__/Message.test.tsx +46 -0
- package/src/components/MessageScroller/MessageScroller.module.css +54 -0
- package/src/components/MessageScroller/MessageScroller.stories.tsx +84 -0
- package/src/components/MessageScroller/MessageScroller.tsx +72 -0
- package/src/components/MessageScroller/__tests__/MessageScroller.test.tsx +105 -0
- package/src/components/NavigationMenu/NavigationMenu.module.css +53 -0
- package/src/components/NavigationMenu/NavigationMenu.stories.tsx +58 -0
- package/src/components/NavigationMenu/NavigationMenu.tsx +125 -0
- package/src/components/NavigationMenu/__tests__/NavigationMenu.test.tsx +93 -0
- package/src/components/Pagination/Pagination.module.css +98 -0
- package/src/components/Pagination/Pagination.stories.tsx +49 -0
- package/src/components/Pagination/Pagination.tsx +87 -0
- package/src/components/Pagination/__tests__/Pagination.test.tsx +56 -0
- package/src/components/Progress/Progress.module.css +20 -0
- package/src/components/Progress/Progress.stories.tsx +40 -0
- package/src/components/Progress/Progress.tsx +35 -0
- package/src/components/Progress/__tests__/Progress.test.tsx +31 -0
- package/src/components/RadioGroup/RadioGroup.module.css +88 -0
- package/src/components/RadioGroup/RadioGroup.stories.tsx +69 -0
- package/src/components/RadioGroup/RadioGroup.tsx +99 -0
- package/src/components/RadioGroup/__tests__/RadioGroup.test.tsx +59 -0
- package/src/components/Resizable/Resizable.module.css +70 -0
- package/src/components/Resizable/Resizable.stories.tsx +82 -0
- package/src/components/Resizable/Resizable.tsx +313 -0
- package/src/components/Resizable/__tests__/Resizable.test.tsx +128 -0
- package/src/components/ScrollArea/ScrollArea.module.css +20 -0
- package/src/components/ScrollArea/ScrollArea.stories.tsx +57 -0
- package/src/components/ScrollArea/ScrollArea.tsx +27 -0
- package/src/components/ScrollArea/__tests__/ScrollArea.test.tsx +39 -0
- package/src/components/Select/Select.module.css +7 -3
- package/src/components/Slider/Slider.module.css +95 -0
- package/src/components/Slider/Slider.stories.tsx +64 -0
- package/src/components/Slider/Slider.tsx +77 -0
- package/src/components/Slider/__tests__/Slider.test.tsx +45 -0
- package/src/components/SplitMethodSelector/SplitMethodSelector.stories.tsx +33 -0
- package/src/components/SplitMethodSelector/SplitMethodSelector.tsx +43 -0
- package/src/components/Table/Table.module.css +45 -0
- package/src/components/Table/Table.stories.tsx +125 -0
- package/src/components/Table/Table.tsx +51 -0
- package/src/components/Table/__tests__/Table.test.tsx +50 -0
- package/src/components/Tabs/Tabs.module.css +6 -1
- package/src/components/Toggle/Toggle.module.css +6 -1
- package/src/components/Typography/Typography.module.css +47 -0
- package/src/components/Typography/Typography.stories.tsx +62 -0
- package/src/components/Typography/Typography.tsx +30 -0
- package/src/components/Typography/__tests__/Typography.test.tsx +34 -0
- package/src/index.ts +92 -1
- package/src/stories/DesignSystemOverview.stories.tsx +706 -2
- package/src/stories/MobilePatterns.stories.tsx +8 -2
- package/src/stories/TokenGallery.stories.tsx +112 -16
- package/src/tokens/primitives.css +22 -8
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import styles from './AspectRatio.module.css';
|
|
3
|
+
|
|
4
|
+
export interface AspectRatioProps {
|
|
5
|
+
/** Width / height, e.g. 16 / 9 or 1. */
|
|
6
|
+
ratio: number;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** AspectRatio — constrains content (image, video, embed) to a fixed ratio. */
|
|
12
|
+
export function AspectRatio({ ratio, children, className }: AspectRatioProps) {
|
|
13
|
+
return (
|
|
14
|
+
<div
|
|
15
|
+
className={[styles.root, className].filter(Boolean).join(' ')}
|
|
16
|
+
style={{ aspectRatio: ratio }}
|
|
17
|
+
>
|
|
18
|
+
{children}
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
+
import { cleanup, render, screen } from '@testing-library/react';
|
|
4
|
+
import { AspectRatio } from '../AspectRatio';
|
|
5
|
+
|
|
6
|
+
afterEach(cleanup);
|
|
7
|
+
|
|
8
|
+
describe('AspectRatio', () => {
|
|
9
|
+
it('applies the ratio as inline CSS aspect-ratio', () => {
|
|
10
|
+
render(
|
|
11
|
+
<AspectRatio ratio={16 / 9}>
|
|
12
|
+
<img src="/x.png" alt="Preview" />
|
|
13
|
+
</AspectRatio>,
|
|
14
|
+
);
|
|
15
|
+
const root = screen.getByRole('img').parentElement;
|
|
16
|
+
expect(root?.style.aspectRatio).toContain(String(16 / 9));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('renders children', () => {
|
|
20
|
+
render(
|
|
21
|
+
<AspectRatio ratio={1}>
|
|
22
|
+
<div>content</div>
|
|
23
|
+
</AspectRatio>,
|
|
24
|
+
);
|
|
25
|
+
expect(screen.getByText('content')).toBeDefined();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -4,27 +4,28 @@
|
|
|
4
4
|
gap: var(--sv-space-1);
|
|
5
5
|
border-radius: var(--sv-radius-sm);
|
|
6
6
|
font-family: var(--sv-font-family);
|
|
7
|
-
font-size: var(--sv-font-size-xs);
|
|
8
7
|
line-height: 1;
|
|
9
8
|
white-space: nowrap;
|
|
10
9
|
user-select: none;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/* Applied by default (uppercase=true) — opt out for a title-case badge that
|
|
13
|
+
renders children exactly as passed in. */
|
|
14
|
+
.uppercase {
|
|
11
15
|
text-transform: uppercase;
|
|
12
16
|
letter-spacing: 0.05em;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
|
-
/* role — neutral fill, border
|
|
19
|
+
/* role — neutral fill, border */
|
|
16
20
|
.role {
|
|
17
|
-
padding: 2px var(--sv-space-2);
|
|
18
21
|
background: var(--sv-color-surface-sunken);
|
|
19
22
|
border: 1px solid var(--sv-color-border);
|
|
20
23
|
color: var(--sv-color-text-primary);
|
|
21
|
-
font-size: var(--sv-font-size-label);
|
|
22
24
|
font-weight: var(--sv-font-weight-semibold);
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
/* status — tinted chip with leading colour dot; colour set by chip* classes below */
|
|
26
28
|
.status {
|
|
27
|
-
padding: 2px var(--sv-space-2) 2px var(--sv-space-1);
|
|
28
29
|
font-weight: var(--sv-font-weight-medium);
|
|
29
30
|
}
|
|
30
31
|
|
|
@@ -54,23 +55,88 @@
|
|
|
54
55
|
|
|
55
56
|
/* mono — monospace font, neutral surface, for type tags / versions */
|
|
56
57
|
.mono {
|
|
57
|
-
padding: 2px var(--sv-space-2);
|
|
58
58
|
background: var(--sv-color-surface-sunken);
|
|
59
59
|
border: 1px solid var(--sv-color-border);
|
|
60
60
|
color: var(--sv-color-text-muted);
|
|
61
61
|
font-family: var(--sv-font-family-mono);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* Size × variant — font-size and padding are the two axes that change
|
|
65
|
+
together, so each combination is one compound rule rather than two
|
|
66
|
+
composed partials fighting over the same properties. `.role.md`,
|
|
67
|
+
`.status.md`, and `.mono.md` reproduce the badge's original (and, until
|
|
68
|
+
now, only) sizing exactly, so the default (no `size` prop) renders
|
|
69
|
+
pixel-identical to before this scale existed. */
|
|
70
|
+
.role.sm {
|
|
71
|
+
padding: 1px var(--sv-space-1);
|
|
72
|
+
font-size: var(--sv-font-size-label);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.role.md {
|
|
76
|
+
padding: 2px var(--sv-space-2);
|
|
77
|
+
font-size: var(--sv-font-size-label);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.role.lg {
|
|
81
|
+
padding: 3px var(--sv-space-3);
|
|
82
|
+
font-size: var(--sv-font-size-sm);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.mono.sm {
|
|
86
|
+
padding: 1px var(--sv-space-1);
|
|
87
|
+
font-size: var(--sv-font-size-label);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.mono.md {
|
|
91
|
+
padding: 2px var(--sv-space-2);
|
|
62
92
|
font-size: var(--sv-font-size-xs);
|
|
63
93
|
}
|
|
64
94
|
|
|
65
|
-
|
|
95
|
+
.mono.lg {
|
|
96
|
+
padding: 3px var(--sv-space-3);
|
|
97
|
+
font-size: var(--sv-font-size-sm);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* status keeps a tighter left edge than right at every size — the dot +
|
|
101
|
+
gap already read as space before the text, so a full padding-inline-start
|
|
102
|
+
would double it up. */
|
|
103
|
+
.status.sm {
|
|
104
|
+
padding: 1px var(--sv-space-1) 1px 3px;
|
|
105
|
+
font-size: var(--sv-font-size-label);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.status.md {
|
|
109
|
+
padding: 2px var(--sv-space-2) 2px var(--sv-space-1);
|
|
110
|
+
font-size: var(--sv-font-size-xs);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.status.lg {
|
|
114
|
+
padding: 3px var(--sv-space-3) 3px var(--sv-space-2);
|
|
115
|
+
font-size: var(--sv-font-size-sm);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* Status dot — scales with size */
|
|
66
119
|
.dot {
|
|
67
120
|
display: inline-block;
|
|
68
|
-
width: 6px;
|
|
69
|
-
height: 6px;
|
|
70
121
|
border-radius: var(--sv-radius-full);
|
|
71
122
|
flex-shrink: 0;
|
|
72
123
|
}
|
|
73
124
|
|
|
125
|
+
.sm .dot {
|
|
126
|
+
width: 5px;
|
|
127
|
+
height: 5px;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.md .dot {
|
|
131
|
+
width: 6px;
|
|
132
|
+
height: 6px;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.lg .dot {
|
|
136
|
+
width: 8px;
|
|
137
|
+
height: 8px;
|
|
138
|
+
}
|
|
139
|
+
|
|
74
140
|
.dotGreen {
|
|
75
141
|
background: var(--sv-color-success-text);
|
|
76
142
|
}
|
|
@@ -10,16 +10,18 @@ const meta = {
|
|
|
10
10
|
docs: {
|
|
11
11
|
description: {
|
|
12
12
|
component:
|
|
13
|
-
'Compact label for roles (`role`), lifecycle states (`status`), and type/version tags (`mono`). RSC-safe — no state.',
|
|
13
|
+
'Compact label for roles (`role`), lifecycle states (`status`), and type/version tags (`mono`). Three sizes (`sm`/`md`/`lg`); ALL CAPS by default, or set `uppercase={false}` for title case. RSC-safe — no state.',
|
|
14
14
|
},
|
|
15
15
|
},
|
|
16
16
|
},
|
|
17
17
|
argTypes: {
|
|
18
18
|
variant: { control: 'select', options: ['role', 'status', 'mono'] },
|
|
19
|
+
size: { control: 'select', options: ['sm', 'md', 'lg'] },
|
|
19
20
|
status: {
|
|
20
21
|
control: 'select',
|
|
21
22
|
options: ['active', 'enabled', 'deactivated', 'failed', 'invited', 'pending', 'neutral'],
|
|
22
23
|
},
|
|
24
|
+
uppercase: { control: 'boolean' },
|
|
23
25
|
},
|
|
24
26
|
args: { children: 'Badge' },
|
|
25
27
|
} satisfies Meta<typeof Badge>;
|
|
@@ -54,6 +56,84 @@ export const MonoPlatform: Story = { args: { variant: 'mono', children: 'platfor
|
|
|
54
56
|
export const MonoCommunity: Story = { args: { variant: 'mono', children: 'community' } };
|
|
55
57
|
export const MonoVersion: Story = { args: { variant: 'mono', children: 'v0.1.0' } };
|
|
56
58
|
|
|
59
|
+
/** sm / md (default) / lg side by side, across all three variants. */
|
|
60
|
+
export const Sizes: Story = {
|
|
61
|
+
render: () => {
|
|
62
|
+
const rowStyle: React.CSSProperties = { display: 'flex', alignItems: 'center', gap: 8 };
|
|
63
|
+
const sectionStyle: React.CSSProperties = { display: 'flex', flexDirection: 'column', gap: 8 };
|
|
64
|
+
return (
|
|
65
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
66
|
+
<div style={sectionStyle}>
|
|
67
|
+
<div style={rowStyle}>
|
|
68
|
+
<Badge variant="role" size="sm">
|
|
69
|
+
Owner
|
|
70
|
+
</Badge>
|
|
71
|
+
<Badge variant="role" size="md">
|
|
72
|
+
Owner
|
|
73
|
+
</Badge>
|
|
74
|
+
<Badge variant="role" size="lg">
|
|
75
|
+
Owner
|
|
76
|
+
</Badge>
|
|
77
|
+
</div>
|
|
78
|
+
<div style={rowStyle}>
|
|
79
|
+
<Badge variant="status" status="active" size="sm">
|
|
80
|
+
Active
|
|
81
|
+
</Badge>
|
|
82
|
+
<Badge variant="status" status="active" size="md">
|
|
83
|
+
Active
|
|
84
|
+
</Badge>
|
|
85
|
+
<Badge variant="status" status="active" size="lg">
|
|
86
|
+
Active
|
|
87
|
+
</Badge>
|
|
88
|
+
</div>
|
|
89
|
+
<div style={rowStyle}>
|
|
90
|
+
<Badge variant="mono" size="sm">
|
|
91
|
+
v0.1.0
|
|
92
|
+
</Badge>
|
|
93
|
+
<Badge variant="mono" size="md">
|
|
94
|
+
v0.1.0
|
|
95
|
+
</Badge>
|
|
96
|
+
<Badge variant="mono" size="lg">
|
|
97
|
+
v0.1.0
|
|
98
|
+
</Badge>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/** uppercase (default) vs. title case — same text, `uppercase={false}` on the right. */
|
|
107
|
+
export const TitleCase: Story = {
|
|
108
|
+
render: () => {
|
|
109
|
+
const rowStyle: React.CSSProperties = { display: 'flex', alignItems: 'center', gap: 8 };
|
|
110
|
+
return (
|
|
111
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
112
|
+
<div style={rowStyle}>
|
|
113
|
+
<Badge variant="role">Owner</Badge>
|
|
114
|
+
<Badge variant="role" uppercase={false}>
|
|
115
|
+
Owner
|
|
116
|
+
</Badge>
|
|
117
|
+
</div>
|
|
118
|
+
<div style={rowStyle}>
|
|
119
|
+
<Badge variant="status" status="active">
|
|
120
|
+
Active
|
|
121
|
+
</Badge>
|
|
122
|
+
<Badge variant="status" status="active" uppercase={false}>
|
|
123
|
+
Active
|
|
124
|
+
</Badge>
|
|
125
|
+
</div>
|
|
126
|
+
<div style={rowStyle}>
|
|
127
|
+
<Badge variant="mono">Serve Route</Badge>
|
|
128
|
+
<Badge variant="mono" uppercase={false}>
|
|
129
|
+
Serve Route
|
|
130
|
+
</Badge>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
57
137
|
/** All variants and statuses at a glance. */
|
|
58
138
|
export const AllVariants: Story = {
|
|
59
139
|
render: (_args) => {
|
|
@@ -112,6 +192,34 @@ export const AllVariants: Story = {
|
|
|
112
192
|
))}
|
|
113
193
|
</div>
|
|
114
194
|
</div>
|
|
195
|
+
<div style={sectionStyle}>
|
|
196
|
+
<span style={headingStyle}>Sizes (sm / md / lg)</span>
|
|
197
|
+
<div style={{ ...groupStyle, alignItems: 'center' }}>
|
|
198
|
+
<Badge variant="role" size="sm">
|
|
199
|
+
Owner
|
|
200
|
+
</Badge>
|
|
201
|
+
<Badge variant="role" size="md">
|
|
202
|
+
Owner
|
|
203
|
+
</Badge>
|
|
204
|
+
<Badge variant="role" size="lg">
|
|
205
|
+
Owner
|
|
206
|
+
</Badge>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
<div style={sectionStyle}>
|
|
210
|
+
<span style={headingStyle}>Title case (uppercase=false)</span>
|
|
211
|
+
<div style={{ ...groupStyle, alignItems: 'center' }}>
|
|
212
|
+
<Badge variant="role" uppercase={false}>
|
|
213
|
+
Owner
|
|
214
|
+
</Badge>
|
|
215
|
+
<Badge variant="status" status="active" uppercase={false}>
|
|
216
|
+
Active
|
|
217
|
+
</Badge>
|
|
218
|
+
<Badge variant="mono" uppercase={false}>
|
|
219
|
+
Serve Route
|
|
220
|
+
</Badge>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
115
223
|
</div>
|
|
116
224
|
);
|
|
117
225
|
},
|
|
@@ -2,6 +2,7 @@ import type { ReactNode } from 'react';
|
|
|
2
2
|
import styles from './Badge.module.css';
|
|
3
3
|
|
|
4
4
|
export type BadgeVariant = 'role' | 'status' | 'mono';
|
|
5
|
+
export type BadgeSize = 'sm' | 'md' | 'lg';
|
|
5
6
|
export type BadgeStatus =
|
|
6
7
|
| 'active'
|
|
7
8
|
| 'enabled'
|
|
@@ -13,8 +14,16 @@ export type BadgeStatus =
|
|
|
13
14
|
|
|
14
15
|
export interface BadgeProps {
|
|
15
16
|
variant?: BadgeVariant;
|
|
17
|
+
/** 'sm' | 'md' (default) | 'lg'. 'md' is the badge's original — and until
|
|
18
|
+
* now, only — size, so the default renders unchanged. */
|
|
19
|
+
size?: BadgeSize;
|
|
16
20
|
/** Only relevant when variant="status" — determines the dot colour. */
|
|
17
21
|
status?: BadgeStatus;
|
|
22
|
+
/** Forces ALL CAPS display regardless of the case `children` is passed
|
|
23
|
+
* in. Defaults to `true` — every existing badge in the app relies on
|
|
24
|
+
* this. Set `false` for a title-case badge that renders exactly the text
|
|
25
|
+
* passed in (e.g. "Owner" instead of "OWNER"). */
|
|
26
|
+
uppercase?: boolean;
|
|
18
27
|
children: ReactNode;
|
|
19
28
|
}
|
|
20
29
|
|
|
@@ -44,14 +53,33 @@ const STATUS_CHIP_CLASS: Record<BadgeStatus, string> = {
|
|
|
44
53
|
* - `role` neutral surface + border, semibold — for Owner / Admin / User
|
|
45
54
|
* - `status` tinted chip with leading colour dot — for Active / Deactivated / etc.
|
|
46
55
|
* - `mono` monospace font, neutral surface — for platform / community / v0.1.0
|
|
56
|
+
*
|
|
57
|
+
* Three sizes (`sm`/`md`/`lg`, default `md`). ALL CAPS by default
|
|
58
|
+
* (`uppercase`) — set `uppercase={false}` for a title-case badge instead.
|
|
47
59
|
*/
|
|
48
|
-
export function Badge({
|
|
60
|
+
export function Badge({
|
|
61
|
+
variant = 'role',
|
|
62
|
+
size = 'md',
|
|
63
|
+
status = 'neutral',
|
|
64
|
+
uppercase = true,
|
|
65
|
+
children,
|
|
66
|
+
}: BadgeProps) {
|
|
49
67
|
const isStatus = variant === 'status';
|
|
50
68
|
const dotClass = isStatus ? STATUS_DOT_CLASS[status] : undefined;
|
|
51
69
|
const chipClass = isStatus ? STATUS_CHIP_CLASS[status] : undefined;
|
|
52
70
|
|
|
53
71
|
return (
|
|
54
|
-
<span
|
|
72
|
+
<span
|
|
73
|
+
className={[
|
|
74
|
+
styles.badge,
|
|
75
|
+
styles[variant],
|
|
76
|
+
styles[size],
|
|
77
|
+
chipClass,
|
|
78
|
+
uppercase && styles.uppercase,
|
|
79
|
+
]
|
|
80
|
+
.filter(Boolean)
|
|
81
|
+
.join(' ')}
|
|
82
|
+
>
|
|
55
83
|
{isStatus && <span className={[styles.dot, dotClass].join(' ')} aria-hidden />}
|
|
56
84
|
{children}
|
|
57
85
|
</span>
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
+
import { cleanup, render, screen } from '@testing-library/react';
|
|
4
4
|
import { Badge } from '../Badge';
|
|
5
5
|
|
|
6
|
+
afterEach(cleanup);
|
|
7
|
+
|
|
6
8
|
describe('Badge', () => {
|
|
7
9
|
it('renders its children', () => {
|
|
8
10
|
render(<Badge>Owner</Badge>);
|
|
@@ -61,4 +63,26 @@ describe('Badge', () => {
|
|
|
61
63
|
const { container } = render(<Badge variant="role">Owner</Badge>);
|
|
62
64
|
expect(container.querySelectorAll('[aria-hidden]').length).toBe(0);
|
|
63
65
|
});
|
|
66
|
+
|
|
67
|
+
it('defaults to md size', () => {
|
|
68
|
+
render(<Badge>Admin</Badge>);
|
|
69
|
+
expect(screen.getByText('Admin').closest('span')?.className).toContain('md');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('applies sm and lg size classes', () => {
|
|
73
|
+
const { rerender } = render(<Badge size="sm">Admin</Badge>);
|
|
74
|
+
expect(screen.getByText('Admin').closest('span')?.className).toContain('sm');
|
|
75
|
+
rerender(<Badge size="lg">Admin</Badge>);
|
|
76
|
+
expect(screen.getByText('Admin').closest('span')?.className).toContain('lg');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('defaults to uppercase display', () => {
|
|
80
|
+
render(<Badge>Owner</Badge>);
|
|
81
|
+
expect(screen.getByText('Owner').closest('span')?.className).toContain('uppercase');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('omits the uppercase class when uppercase is false', () => {
|
|
85
|
+
render(<Badge uppercase={false}>Owner</Badge>);
|
|
86
|
+
expect(screen.getByText('Owner').closest('span')?.className).not.toContain('uppercase');
|
|
87
|
+
});
|
|
64
88
|
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
.chip {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
max-width: 100%;
|
|
5
|
+
min-height: 24px;
|
|
6
|
+
padding: 2px var(--sv-space-2);
|
|
7
|
+
border: 1px solid var(--sv-color-border);
|
|
8
|
+
border-radius: var(--sv-radius-full);
|
|
9
|
+
font-family: var(--sv-font-family);
|
|
10
|
+
font-size: var(--sv-font-size-xs);
|
|
11
|
+
font-weight: var(--sv-font-weight-semibold);
|
|
12
|
+
line-height: 1.2;
|
|
13
|
+
white-space: nowrap;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.owed {
|
|
17
|
+
color: var(--sv-color-success-text);
|
|
18
|
+
background: var(--sv-color-success-surface);
|
|
19
|
+
border-color: var(--sv-color-success-border);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.owes {
|
|
23
|
+
color: var(--sv-color-error-text);
|
|
24
|
+
background: var(--sv-color-error-surface);
|
|
25
|
+
border-color: var(--sv-color-error-border);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.settled {
|
|
29
|
+
color: var(--sv-color-text-muted);
|
|
30
|
+
background: var(--sv-color-surface-sunken);
|
|
31
|
+
border-color: var(--sv-color-border);
|
|
32
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { BalanceChip } from './BalanceChip';
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Components/BalanceChip',
|
|
6
|
+
component: BalanceChip,
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: 'centered',
|
|
9
|
+
docs: {
|
|
10
|
+
description: {
|
|
11
|
+
component:
|
|
12
|
+
'Inline net-balance indicator — green when owed to them, red when they owe, neutral when settled. Not tied to expense-splitting specifically: any plugin tracking a signed balance between people can use it.',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
args: {
|
|
17
|
+
amountCents: 2500,
|
|
18
|
+
currency: 'USD',
|
|
19
|
+
},
|
|
20
|
+
} satisfies Meta<typeof BalanceChip>;
|
|
21
|
+
|
|
22
|
+
export default meta;
|
|
23
|
+
type Story = StoryObj<typeof meta>;
|
|
24
|
+
|
|
25
|
+
export const Owed: Story = {
|
|
26
|
+
args: { amountCents: 2500, currency: 'USD' },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const Owes: Story = {
|
|
30
|
+
args: { amountCents: -1350, currency: 'USD' },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const SettledUp: Story = {
|
|
34
|
+
args: { amountCents: 0, currency: 'USD' },
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const AllStates: Story = {
|
|
38
|
+
render: () => (
|
|
39
|
+
<div style={{ display: 'flex', gap: 'var(--sv-space-2)' }}>
|
|
40
|
+
<BalanceChip amountCents={2500} currency="USD" />
|
|
41
|
+
<BalanceChip amountCents={-1350} currency="USD" />
|
|
42
|
+
<BalanceChip amountCents={0} currency="USD" />
|
|
43
|
+
</div>
|
|
44
|
+
),
|
|
45
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import styles from './BalanceChip.module.css';
|
|
2
|
+
|
|
3
|
+
export interface BalanceChipProps {
|
|
4
|
+
/** Smallest currency unit (cents). Positive = owed to them, negative = they owe, zero = settled. */
|
|
5
|
+
amountCents: number;
|
|
6
|
+
/** ISO 4217 currency code, e.g. "USD". */
|
|
7
|
+
currency: string;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function formatAbsCents(cents: number): string {
|
|
12
|
+
return (Math.abs(cents) / 100).toFixed(2);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* BalanceChip — inline balance indicator (green = owed to them, red = they
|
|
17
|
+
* owe, neutral = settled up). For any plugin tracking who-owes-whom — not
|
|
18
|
+
* Tally-specific despite the naming; "amountCents" is a generic signed net
|
|
19
|
+
* balance, not tied to expense-splitting semantics.
|
|
20
|
+
*/
|
|
21
|
+
export function BalanceChip({ amountCents, currency, className }: BalanceChipProps) {
|
|
22
|
+
const direction = amountCents > 0 ? 'owed' : amountCents < 0 ? 'owes' : 'settled';
|
|
23
|
+
const text =
|
|
24
|
+
direction === 'settled'
|
|
25
|
+
? 'Settled up'
|
|
26
|
+
: `${direction === 'owed' ? 'Owed' : 'Owes'} ${currency} ${formatAbsCents(amountCents)}`;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<span className={[styles.chip, styles[direction], className].filter(Boolean).join(' ')}>
|
|
30
|
+
{text}
|
|
31
|
+
</span>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
.nav {
|
|
2
|
+
min-width: 0;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.list {
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-wrap: wrap;
|
|
8
|
+
align-items: center;
|
|
9
|
+
gap: var(--sv-space-1);
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 0;
|
|
12
|
+
list-style: none;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.item {
|
|
16
|
+
display: inline-flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
gap: var(--sv-space-1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.link {
|
|
22
|
+
font-size: var(--sv-font-size-sm);
|
|
23
|
+
color: var(--sv-color-text-muted);
|
|
24
|
+
text-decoration: none;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@media (hover: hover) {
|
|
28
|
+
.link:hover {
|
|
29
|
+
color: var(--sv-color-text-primary);
|
|
30
|
+
text-decoration: underline;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.link:focus-visible {
|
|
35
|
+
outline: 2px solid var(--sv-color-focus-ring);
|
|
36
|
+
outline-offset: 2px;
|
|
37
|
+
border-radius: var(--sv-radius-sm);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.current {
|
|
41
|
+
font-size: var(--sv-font-size-sm);
|
|
42
|
+
font-weight: var(--sv-font-weight-medium);
|
|
43
|
+
color: var(--sv-color-text-primary);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.separator {
|
|
47
|
+
color: var(--sv-color-text-muted);
|
|
48
|
+
flex-shrink: 0;
|
|
49
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { Breadcrumb } from './Breadcrumb';
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Components/Breadcrumb',
|
|
6
|
+
component: Breadcrumb,
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: 'padded',
|
|
9
|
+
docs: {
|
|
10
|
+
description: {
|
|
11
|
+
component:
|
|
12
|
+
'Link trail. The last item (no href) renders as plain text with aria-current="page". Pass renderLink to use a client-side router Link instead of a plain <a> — matters inside overlay-shell plugins, where a full page reload breaks the dismiss flow.',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
args: {
|
|
17
|
+
items: [
|
|
18
|
+
{ label: 'Console', href: '/console' },
|
|
19
|
+
{ label: 'Plugins', href: '/console/plugins' },
|
|
20
|
+
{ label: 'sovereign-tasks' },
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
} satisfies Meta<typeof Breadcrumb>;
|
|
24
|
+
|
|
25
|
+
export default meta;
|
|
26
|
+
type Story = StoryObj<typeof meta>;
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
export const Default: Story = {};
|
|
31
|
+
|
|
32
|
+
export const TwoLevels: Story = {
|
|
33
|
+
args: {
|
|
34
|
+
items: [{ label: 'Console', href: '/console' }, { label: 'Settings' }],
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const WithRenderLink: Story = {
|
|
39
|
+
name: 'With custom renderLink',
|
|
40
|
+
args: {
|
|
41
|
+
renderLink: (item, children) => (
|
|
42
|
+
<a href={item.href} data-client-side-nav="true">
|
|
43
|
+
{children}
|
|
44
|
+
</a>
|
|
45
|
+
),
|
|
46
|
+
},
|
|
47
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { Icon } from '../Icon/Icon';
|
|
3
|
+
import styles from './Breadcrumb.module.css';
|
|
4
|
+
|
|
5
|
+
export interface BreadcrumbItem {
|
|
6
|
+
label: string;
|
|
7
|
+
/** Omit for the current page — rendered as plain text with aria-current. */
|
|
8
|
+
href?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface BreadcrumbProps {
|
|
12
|
+
items: BreadcrumbItem[];
|
|
13
|
+
/** Renders a linked item. Defaults to a plain `<a href>`, which causes a
|
|
14
|
+
* full page reload — pass Next's `<Link>` (or equivalent) to keep
|
|
15
|
+
* navigation client-side, which matters inside overlay-shell plugins
|
|
16
|
+
* where a full reload breaks the router.back()-driven dismiss flow. */
|
|
17
|
+
renderLink?: (item: BreadcrumbItem, children: ReactNode) => ReactNode;
|
|
18
|
+
className?: string;
|
|
19
|
+
'aria-label'?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function Breadcrumb({
|
|
23
|
+
items,
|
|
24
|
+
renderLink,
|
|
25
|
+
className,
|
|
26
|
+
'aria-label': ariaLabel = 'Breadcrumb',
|
|
27
|
+
}: BreadcrumbProps) {
|
|
28
|
+
return (
|
|
29
|
+
<nav aria-label={ariaLabel} className={[styles.nav, className].filter(Boolean).join(' ')}>
|
|
30
|
+
<ol className={styles.list}>
|
|
31
|
+
{items.map((item, index) => {
|
|
32
|
+
const isLast = index === items.length - 1;
|
|
33
|
+
return (
|
|
34
|
+
<li key={item.label + index} className={styles.item}>
|
|
35
|
+
{item.href ? (
|
|
36
|
+
renderLink ? (
|
|
37
|
+
renderLink(item, item.label)
|
|
38
|
+
) : (
|
|
39
|
+
<a href={item.href} className={styles.link}>
|
|
40
|
+
{item.label}
|
|
41
|
+
</a>
|
|
42
|
+
)
|
|
43
|
+
) : (
|
|
44
|
+
<span className={styles.current} aria-current="page">
|
|
45
|
+
{item.label}
|
|
46
|
+
</span>
|
|
47
|
+
)}
|
|
48
|
+
{!isLast && (
|
|
49
|
+
<Icon name="chevron-right" size="xs" aria-hidden className={styles.separator} />
|
|
50
|
+
)}
|
|
51
|
+
</li>
|
|
52
|
+
);
|
|
53
|
+
})}
|
|
54
|
+
</ol>
|
|
55
|
+
</nav>
|
|
56
|
+
);
|
|
57
|
+
}
|