@flamingo-stack/openframe-frontend-core 0.0.523 → 0.0.524-1675.4405.1
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/{chunk-CE4CBCKX.cjs → chunk-25JYFZ6D.cjs} +7 -7
- package/dist/{chunk-CE4CBCKX.cjs.map → chunk-25JYFZ6D.cjs.map} +1 -1
- package/dist/{chunk-3OFRFRBW.cjs → chunk-2JMWI7RE.cjs} +26 -26
- package/dist/{chunk-3OFRFRBW.cjs.map → chunk-2JMWI7RE.cjs.map} +1 -1
- package/dist/{chunk-MYQAF43S.js → chunk-2WNONXFZ.js} +4 -4
- package/dist/{chunk-6MW6ELZQ.cjs → chunk-56NIEVZ4.cjs} +37 -37
- package/dist/{chunk-6MW6ELZQ.cjs.map → chunk-56NIEVZ4.cjs.map} +1 -1
- package/dist/{chunk-M7FGOI4V.cjs → chunk-6CTX5BLD.cjs} +31 -31
- package/dist/{chunk-M7FGOI4V.cjs.map → chunk-6CTX5BLD.cjs.map} +1 -1
- package/dist/{chunk-2ALPHSGU.js → chunk-6S5E5RLR.js} +4 -4
- package/dist/{chunk-TPIF252F.js → chunk-CELZDZP2.js} +2 -2
- package/dist/{chunk-C7T363HO.js → chunk-DFZTO3NG.js} +2 -2
- package/dist/{chunk-OPPGJ4PY.cjs → chunk-DTETRKV6.cjs} +87 -87
- package/dist/{chunk-OPPGJ4PY.cjs.map → chunk-DTETRKV6.cjs.map} +1 -1
- package/dist/{chunk-HM53VV7K.cjs → chunk-H635T5R2.cjs} +1316 -1079
- package/dist/chunk-H635T5R2.cjs.map +1 -0
- package/dist/{chunk-MVYTOP3B.js → chunk-HI7LXHWR.js} +5 -5
- package/dist/{chunk-KSZPPVXT.cjs → chunk-HMYZM3SS.cjs} +11 -11
- package/dist/{chunk-KSZPPVXT.cjs.map → chunk-HMYZM3SS.cjs.map} +1 -1
- package/dist/{chunk-YTLLRLMU.cjs → chunk-I5RARV6I.cjs} +14 -14
- package/dist/{chunk-YTLLRLMU.cjs.map → chunk-I5RARV6I.cjs.map} +1 -1
- package/dist/{chunk-TH4BQCOD.cjs → chunk-JFGAVBWJ.cjs} +26 -26
- package/dist/{chunk-TH4BQCOD.cjs.map → chunk-JFGAVBWJ.cjs.map} +1 -1
- package/dist/{chunk-E2W2XMX2.js → chunk-JHEUPQAF.js} +2 -2
- package/dist/{chunk-JBCNOJMX.cjs → chunk-LEN53CEM.cjs} +3 -3
- package/dist/{chunk-JBCNOJMX.cjs.map → chunk-LEN53CEM.cjs.map} +1 -1
- package/dist/{chunk-QCWVGCT3.js → chunk-MAAN76UX.js} +6 -6
- package/dist/{chunk-HYVYAWUO.js → chunk-N3XPBO6W.js} +2 -2
- package/dist/{chunk-QRYJYZTW.cjs → chunk-NFTLCOJF.cjs} +61 -61
- package/dist/{chunk-QRYJYZTW.cjs.map → chunk-NFTLCOJF.cjs.map} +1 -1
- package/dist/{chunk-4DJL34HL.js → chunk-Q3DK4P3F.js} +2 -2
- package/dist/{chunk-ARCH6RK6.cjs → chunk-RLAS623H.cjs} +4 -4
- package/dist/{chunk-ARCH6RK6.cjs.map → chunk-RLAS623H.cjs.map} +1 -1
- package/dist/{chunk-CBD3IHPF.js → chunk-RNAXQVBK.js} +2 -2
- package/dist/{chunk-2MPS2QY5.cjs → chunk-STJB3XH6.cjs} +5 -5
- package/dist/{chunk-2MPS2QY5.cjs.map → chunk-STJB3XH6.cjs.map} +1 -1
- package/dist/{chunk-LEMU4JMX.js → chunk-WR2XVJ6K.js} +2 -2
- package/dist/{chunk-43MPUCDL.js → chunk-XMROOTCF.js} +2 -2
- package/dist/{chunk-INTFGXHO.js → chunk-Y2BLPCJZ.js} +4979 -4742
- package/dist/chunk-Y2BLPCJZ.js.map +1 -0
- package/dist/{chunk-ZYBBV6CK.js → chunk-Y5MDIJHH.js} +2 -2
- package/dist/{chunk-S4BAM7EC.cjs → chunk-YLWWP46N.cjs} +9 -9
- package/dist/{chunk-S4BAM7EC.cjs.map → chunk-YLWWP46N.cjs.map} +1 -1
- package/dist/components/case-studies/index.cjs +8 -8
- package/dist/components/case-studies/index.js +2 -2
- package/dist/components/chat/index.cjs +2 -2
- package/dist/components/chat/index.js +1 -1
- package/dist/components/contact/index.cjs +3 -3
- package/dist/components/contact/index.js +2 -2
- package/dist/components/docs/index.cjs +5 -5
- package/dist/components/docs/index.js +4 -4
- package/dist/components/embeds/index.cjs +3 -3
- package/dist/components/embeds/index.js +2 -2
- package/dist/components/faq/index.cjs +3 -3
- package/dist/components/faq/index.js +2 -2
- package/dist/components/features/index.cjs +2 -2
- package/dist/components/features/index.js +1 -1
- package/dist/components/help-center-pages/index.cjs +22 -22
- package/dist/components/help-center-pages/index.js +13 -13
- package/dist/components/index.cjs +144 -132
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +23 -11
- package/dist/components/index.js.map +1 -1
- package/dist/components/meeting-scheduler/index.cjs +34 -34
- package/dist/components/meeting-scheduler/index.js +3 -3
- package/dist/components/navigation/index.cjs +2 -2
- package/dist/components/navigation/index.js +1 -1
- package/dist/components/onboarding-guides/index.cjs +5 -5
- package/dist/components/onboarding-guides/index.js +4 -4
- package/dist/components/related-content/index.cjs +3 -3
- package/dist/components/related-content/index.js +2 -2
- package/dist/components/tickets/index.cjs +6 -6
- package/dist/components/tickets/index.js +5 -5
- package/dist/components/ui/dropdown-button.d.ts +42 -11
- package/dist/components/ui/dropdown-button.d.ts.map +1 -1
- package/dist/components/ui/field.d.ts +38 -0
- package/dist/components/ui/field.d.ts.map +1 -0
- package/dist/components/ui/index.cjs +14 -2
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.d.ts +4 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +13 -1
- package/dist/components/ui/info-hint.d.ts +23 -0
- package/dist/components/ui/info-hint.d.ts.map +1 -0
- package/dist/components/ui/json-detail-list.d.ts +9 -0
- package/dist/components/ui/json-detail-list.d.ts.map +1 -0
- package/dist/components/ui/modal-guarded-close.d.ts +50 -0
- package/dist/components/ui/modal-guarded-close.d.ts.map +1 -0
- package/dist/components/ui/modal-v2.d.ts.map +1 -1
- package/dist/components/ui/tabs.d.ts +19 -5
- package/dist/components/ui/tabs.d.ts.map +1 -1
- package/dist/index.cjs +14 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +13 -1
- package/package.json +3 -2
- package/src/components/ui/dropdown-button.tsx +109 -46
- package/src/components/ui/field.tsx +74 -0
- package/src/components/ui/index.ts +4 -0
- package/src/components/ui/info-hint.tsx +74 -0
- package/src/components/ui/json-detail-list.tsx +112 -0
- package/src/components/ui/modal-guarded-close.tsx +133 -0
- package/src/components/ui/modal-v2.tsx +12 -5
- package/src/components/ui/tabs.tsx +75 -20
- package/src/stories/DropdownButton.stories.tsx +109 -0
- package/src/stories/Field.stories.tsx +70 -0
- package/src/stories/InfoHint.stories.tsx +65 -0
- package/src/stories/JsonDetailList.stories.tsx +73 -0
- package/src/stories/ModalGuardedClose.stories.tsx +82 -0
- package/src/stories/Tabs.stories.tsx +101 -0
- package/dist/chunk-HM53VV7K.cjs.map +0 -1
- package/dist/chunk-INTFGXHO.js.map +0 -1
- /package/dist/{chunk-MYQAF43S.js.map → chunk-2WNONXFZ.js.map} +0 -0
- /package/dist/{chunk-2ALPHSGU.js.map → chunk-6S5E5RLR.js.map} +0 -0
- /package/dist/{chunk-TPIF252F.js.map → chunk-CELZDZP2.js.map} +0 -0
- /package/dist/{chunk-C7T363HO.js.map → chunk-DFZTO3NG.js.map} +0 -0
- /package/dist/{chunk-MVYTOP3B.js.map → chunk-HI7LXHWR.js.map} +0 -0
- /package/dist/{chunk-E2W2XMX2.js.map → chunk-JHEUPQAF.js.map} +0 -0
- /package/dist/{chunk-QCWVGCT3.js.map → chunk-MAAN76UX.js.map} +0 -0
- /package/dist/{chunk-HYVYAWUO.js.map → chunk-N3XPBO6W.js.map} +0 -0
- /package/dist/{chunk-4DJL34HL.js.map → chunk-Q3DK4P3F.js.map} +0 -0
- /package/dist/{chunk-CBD3IHPF.js.map → chunk-RNAXQVBK.js.map} +0 -0
- /package/dist/{chunk-LEMU4JMX.js.map → chunk-WR2XVJ6K.js.map} +0 -0
- /package/dist/{chunk-43MPUCDL.js.map → chunk-XMROOTCF.js.map} +0 -0
- /package/dist/{chunk-ZYBBV6CK.js.map → chunk-Y5MDIJHH.js.map} +0 -0
|
@@ -11,37 +11,92 @@ import { cn } from "../../utils/cn"
|
|
|
11
11
|
* shadows). This Radix-based `Tabs` set (a duplicate of `components/tabs.tsx`)
|
|
12
12
|
* styles itself with legacy shadcn tokens (`bg-muted`, `text-foreground`,
|
|
13
13
|
* `ring-ring`) that don't exist in ODS and is kept only for un-migrated call
|
|
14
|
-
* sites. Do not use in new code
|
|
14
|
+
* sites. Do not use in new code — EXCEPT `variant="admin-rail"`, the sanctioned
|
|
15
|
+
* ODS underline-tab styling for admin config surfaces (see {@link TabsList}).
|
|
15
16
|
*/
|
|
16
17
|
const Tabs = TabsPrimitive.Root
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
export type TabsVariant = 'default' | 'admin-rail'
|
|
20
|
+
|
|
21
|
+
const TABS_LIST_BASE =
|
|
22
|
+
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground"
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The admin underline-tab styling, baked in as a variant so call sites never
|
|
26
|
+
* stack these classes themselves: transparent list on a bordered, scrollable
|
|
27
|
+
* rail; accent underline on the active trigger; secondary → primary text on
|
|
28
|
+
* hover. Set `variant="admin-rail"` on BOTH `TabsList` and each `TabsTrigger`.
|
|
29
|
+
*/
|
|
30
|
+
const tabsListVariantClasses: Record<TabsVariant, string> = {
|
|
31
|
+
default: TABS_LIST_BASE,
|
|
32
|
+
// Applied ON TOP of the base (cn resolves the conflicts) so the variant
|
|
33
|
+
// renders exactly what admin call sites produced by stacking these classes.
|
|
34
|
+
'admin-rail': cn(TABS_LIST_BASE, 'inline-flex justify-start rounded-none bg-transparent h-auto p-0'),
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const TABS_TRIGGER_BASE =
|
|
38
|
+
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-h6 ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm"
|
|
39
|
+
|
|
40
|
+
const tabsTriggerVariantClasses: Record<TabsVariant, string> = {
|
|
41
|
+
default: TABS_TRIGGER_BASE,
|
|
42
|
+
'admin-rail': cn(
|
|
43
|
+
TABS_TRIGGER_BASE,
|
|
44
|
+
'rounded-none border-b-2 border-transparent',
|
|
45
|
+
'data-[state=active]:border-ods-accent data-[state=active]:bg-transparent',
|
|
46
|
+
'px-[var(--spacing-system-l)] py-[var(--spacing-system-sf)] text-h4 whitespace-nowrap',
|
|
47
|
+
'text-ods-text-secondary data-[state=active]:text-ods-text-primary',
|
|
48
|
+
'hover:text-ods-text-primary transition-colors',
|
|
49
|
+
// Disabled tabs stay visible but inert.
|
|
50
|
+
'disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:text-ods-text-secondary',
|
|
51
|
+
),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The bordered rail `admin-rail` lists sit on, scrollable when tabs overflow. */
|
|
55
|
+
const ADMIN_RAIL_WRAPPER_CLASS = 'w-full border-b border-ods-border overflow-x-auto'
|
|
56
|
+
|
|
57
|
+
interface TabsListProps
|
|
58
|
+
extends React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> {
|
|
59
|
+
/**
|
|
60
|
+
* `admin-rail` renders the ODS underline-tab list AND wraps it in the
|
|
61
|
+
* bordered, horizontally-scrollable rail — no extra wrapper div needed at
|
|
62
|
+
* the call site.
|
|
63
|
+
*/
|
|
64
|
+
variant?: TabsVariant
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** @deprecated for `variant="default"` — use `TabNavigation`. `variant="admin-rail"` is the sanctioned admin tabs styling. See {@link Tabs}. */
|
|
19
68
|
const TabsList = React.forwardRef<
|
|
20
69
|
React.ElementRef<typeof TabsPrimitive.List>,
|
|
21
|
-
|
|
22
|
-
>(({ className, ...props }, ref) =>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
70
|
+
TabsListProps
|
|
71
|
+
>(({ className, variant = 'default', ...props }, ref) => {
|
|
72
|
+
const list = (
|
|
73
|
+
<TabsPrimitive.List
|
|
74
|
+
ref={ref}
|
|
75
|
+
className={cn(tabsListVariantClasses[variant], className)}
|
|
76
|
+
{...props}
|
|
77
|
+
/>
|
|
78
|
+
)
|
|
79
|
+
if (variant === 'admin-rail') {
|
|
80
|
+
return <div className={ADMIN_RAIL_WRAPPER_CLASS}>{list}</div>
|
|
81
|
+
}
|
|
82
|
+
return list
|
|
83
|
+
})
|
|
32
84
|
TabsList.displayName = TabsPrimitive.List.displayName
|
|
33
85
|
|
|
34
|
-
|
|
86
|
+
interface TabsTriggerProps
|
|
87
|
+
extends React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> {
|
|
88
|
+
/** Match the `variant` set on the surrounding `TabsList`. */
|
|
89
|
+
variant?: TabsVariant
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** @deprecated for `variant="default"` — use `TabNavigation`. `variant="admin-rail"` is the sanctioned admin tabs styling. See {@link Tabs}. */
|
|
35
93
|
const TabsTrigger = React.forwardRef<
|
|
36
94
|
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
|
37
|
-
|
|
38
|
-
>(({ className, ...props }, ref) => (
|
|
95
|
+
TabsTriggerProps
|
|
96
|
+
>(({ className, variant = 'default', ...props }, ref) => (
|
|
39
97
|
<TabsPrimitive.Trigger
|
|
40
98
|
ref={ref}
|
|
41
|
-
className={cn(
|
|
42
|
-
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-h6 ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
|
43
|
-
className
|
|
44
|
-
)}
|
|
99
|
+
className={cn(tabsTriggerVariantClasses[variant], className)}
|
|
45
100
|
{...props}
|
|
46
101
|
/>
|
|
47
102
|
))
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
|
|
2
|
+
import { fn } from 'storybook/test';
|
|
3
|
+
import {
|
|
4
|
+
Copy01Icon,
|
|
5
|
+
Download01Icon,
|
|
6
|
+
PencilIcon,
|
|
7
|
+
Settings01Icon,
|
|
8
|
+
TrashBlankIcon,
|
|
9
|
+
} from '../components/icons-v2-generated';
|
|
10
|
+
import { DropdownButton } from '../components/ui/dropdown-button';
|
|
11
|
+
|
|
12
|
+
const meta = {
|
|
13
|
+
title: 'UI/DropdownButton',
|
|
14
|
+
component: DropdownButton,
|
|
15
|
+
parameters: {
|
|
16
|
+
layout: 'centered',
|
|
17
|
+
},
|
|
18
|
+
argTypes: {
|
|
19
|
+
variant: {
|
|
20
|
+
control: 'select',
|
|
21
|
+
options: [undefined, 'outline', 'accent', 'transparent', 'destructive', 'warning'],
|
|
22
|
+
},
|
|
23
|
+
align: {
|
|
24
|
+
control: 'select',
|
|
25
|
+
options: ['start', 'center', 'end'],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
decorators: [
|
|
29
|
+
(Story) => (
|
|
30
|
+
<div className="flex min-h-[320px] items-start justify-center p-8">
|
|
31
|
+
<Story />
|
|
32
|
+
</div>
|
|
33
|
+
),
|
|
34
|
+
],
|
|
35
|
+
} satisfies Meta<typeof DropdownButton>;
|
|
36
|
+
|
|
37
|
+
export default meta;
|
|
38
|
+
type Story = StoryObj<typeof meta>;
|
|
39
|
+
|
|
40
|
+
const ITEMS = [
|
|
41
|
+
{ id: 'edit', label: 'Edit', icon: <PencilIcon />, onClick: fn() },
|
|
42
|
+
{ id: 'duplicate', label: 'Duplicate', icon: <Copy01Icon />, onClick: fn() },
|
|
43
|
+
{ id: 'delete', label: 'Delete', icon: <TrashBlankIcon />, danger: true, onClick: fn() },
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The default card-colored seam trigger — label and chevron separated by a
|
|
48
|
+
* divider, whole surface one click target, chevron rotates while open.
|
|
49
|
+
*/
|
|
50
|
+
export const Default: Story = {
|
|
51
|
+
args: {
|
|
52
|
+
label: 'Actions',
|
|
53
|
+
items: ITEMS,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The Button-styled trigger: `variant="outline"` renders a standard `Button`
|
|
59
|
+
* (with leading icon and trailing chevron) so the dropdown matches sibling
|
|
60
|
+
* buttons in the same row. `groups` gives separated item groups; the danger
|
|
61
|
+
* row demonstrates the destructive treatment.
|
|
62
|
+
*/
|
|
63
|
+
export const ButtonTrigger: Story = {
|
|
64
|
+
args: {
|
|
65
|
+
label: 'Manage repo',
|
|
66
|
+
variant: 'outline',
|
|
67
|
+
icon: <Settings01Icon />,
|
|
68
|
+
groups: [
|
|
69
|
+
{
|
|
70
|
+
id: 'primary',
|
|
71
|
+
separator: true,
|
|
72
|
+
items: [
|
|
73
|
+
{ id: 'edit', label: 'Edit rules', icon: <PencilIcon />, onClick: fn() },
|
|
74
|
+
{ id: 'export', label: 'Export config', icon: <Download01Icon />, onClick: fn() },
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: 'danger',
|
|
79
|
+
items: [{ id: 'remove', label: 'Remove repo', icon: <TrashBlankIcon />, danger: true, onClick: fn() }],
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
onCloseAutoFocus: (e: Event) => e.preventDefault(),
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Loading state (Button-styled trigger only): the spinner replaces the
|
|
88
|
+
* content in place and the menu cannot open while loading.
|
|
89
|
+
*/
|
|
90
|
+
export const Loading: Story = {
|
|
91
|
+
args: {
|
|
92
|
+
label: 'Manage repo',
|
|
93
|
+
variant: 'outline',
|
|
94
|
+
icon: <Settings01Icon />,
|
|
95
|
+
loading: true,
|
|
96
|
+
items: ITEMS,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Disabled — both trigger looks support it.
|
|
102
|
+
*/
|
|
103
|
+
export const Disabled: Story = {
|
|
104
|
+
args: {
|
|
105
|
+
label: 'Actions',
|
|
106
|
+
disabled: true,
|
|
107
|
+
items: ITEMS,
|
|
108
|
+
},
|
|
109
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
|
|
2
|
+
import { Field } from '../components/ui/field';
|
|
3
|
+
import { Input } from '../components/ui/input';
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'UI/Field',
|
|
7
|
+
component: Field,
|
|
8
|
+
} satisfies Meta<typeof Field>;
|
|
9
|
+
|
|
10
|
+
export default meta;
|
|
11
|
+
type Story = StoryObj<typeof meta>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The full contract in one field: label, required `*`, hover InfoHint, and a
|
|
15
|
+
* `role="alert"` error. The render-prop spreads `id` / `aria-required` /
|
|
16
|
+
* `aria-invalid` / `aria-describedby` onto the control so the label and error
|
|
17
|
+
* are actually announced.
|
|
18
|
+
*/
|
|
19
|
+
export const Default: Story = {
|
|
20
|
+
args: {
|
|
21
|
+
label: 'Repository name',
|
|
22
|
+
hint: 'The GitHub repository this rule set applies to. Owner/name format.',
|
|
23
|
+
required: true,
|
|
24
|
+
error: 'A repository with this name already exists.',
|
|
25
|
+
children: () => null,
|
|
26
|
+
},
|
|
27
|
+
render: args => (
|
|
28
|
+
<div className="max-w-md">
|
|
29
|
+
<Field {...args}>
|
|
30
|
+
{f => <Input {...f} placeholder="flamingo-stack/openframe" defaultValue="flamingo-stack/openframe" />}
|
|
31
|
+
</Field>
|
|
32
|
+
</div>
|
|
33
|
+
),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Clean state — no hint, no error, not required.
|
|
38
|
+
*/
|
|
39
|
+
export const Plain: Story = {
|
|
40
|
+
args: {
|
|
41
|
+
label: 'Display name',
|
|
42
|
+
children: () => null,
|
|
43
|
+
},
|
|
44
|
+
render: args => (
|
|
45
|
+
<div className="max-w-md">
|
|
46
|
+
<Field {...args}>{f => <Input {...f} placeholder="Enter a name..." />}</Field>
|
|
47
|
+
</div>
|
|
48
|
+
),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Fields in a grid row top-align naturally because labels are a single line —
|
|
53
|
+
* hints live in the hover icon, never in flow.
|
|
54
|
+
*/
|
|
55
|
+
export const GridRow: Story = {
|
|
56
|
+
args: {
|
|
57
|
+
label: 'Model',
|
|
58
|
+
children: () => null,
|
|
59
|
+
},
|
|
60
|
+
render: () => (
|
|
61
|
+
<div className="grid max-w-2xl grid-cols-2 gap-[var(--spacing-system-mf)]">
|
|
62
|
+
<Field label="Model" hint="The LLM used for judged rules." required>
|
|
63
|
+
{f => <Input {...f} placeholder="claude-sonnet" />}
|
|
64
|
+
</Field>
|
|
65
|
+
<Field label="Threshold" error="Must be between 0 and 100.">
|
|
66
|
+
{f => <Input {...f} defaultValue="150" />}
|
|
67
|
+
</Field>
|
|
68
|
+
</div>
|
|
69
|
+
),
|
|
70
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
|
|
2
|
+
import { InfoHint } from '../components/ui/info-hint';
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'UI/InfoHint',
|
|
6
|
+
component: InfoHint,
|
|
7
|
+
argTypes: {
|
|
8
|
+
side: {
|
|
9
|
+
control: 'select',
|
|
10
|
+
options: ['top', 'right', 'bottom', 'left'],
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
parameters: {
|
|
14
|
+
layout: 'centered',
|
|
15
|
+
},
|
|
16
|
+
} satisfies Meta<typeof InfoHint>;
|
|
17
|
+
|
|
18
|
+
export default meta;
|
|
19
|
+
type Story = StoryObj<typeof meta>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The "what does this mean?" affordance — hover the ⓘ to reveal the
|
|
23
|
+
* definition. The same text is exposed to assistive tech via
|
|
24
|
+
* `aria-describedby`, so keyboard users are not left out by the hover-only
|
|
25
|
+
* tooltip.
|
|
26
|
+
*/
|
|
27
|
+
export const Default: Story = {
|
|
28
|
+
args: {
|
|
29
|
+
label: 'Judged rules',
|
|
30
|
+
side: 'top',
|
|
31
|
+
children: 'Judged rules are evaluated by an LLM against the diff; deterministic rules run as code.',
|
|
32
|
+
},
|
|
33
|
+
render: args => (
|
|
34
|
+
<div className="flex min-h-[160px] items-center justify-center">
|
|
35
|
+
<span className="inline-flex items-center gap-1.5 text-h6 text-ods-text-primary">
|
|
36
|
+
Judged rules
|
|
37
|
+
<InfoHint {...args} />
|
|
38
|
+
</span>
|
|
39
|
+
</div>
|
|
40
|
+
),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Inline in a row of labels — the `icon-inline` Button variant keeps the
|
|
45
|
+
* target small enough to sit beside a badge without swamping it.
|
|
46
|
+
*/
|
|
47
|
+
export const InALabelRow: Story = {
|
|
48
|
+
args: {
|
|
49
|
+
children: 'Placeholder',
|
|
50
|
+
},
|
|
51
|
+
render: () => (
|
|
52
|
+
<div className="flex min-h-[160px] flex-col items-start justify-center gap-[var(--spacing-system-sf)]">
|
|
53
|
+
<span className="inline-flex items-center gap-1.5 text-h6 text-ods-text-primary">
|
|
54
|
+
Observe
|
|
55
|
+
<InfoHint label="Observe">Runs the rule and records the outcome without blocking the PR.</InfoHint>
|
|
56
|
+
</span>
|
|
57
|
+
<span className="inline-flex items-center gap-1.5 text-h6 text-ods-text-primary">
|
|
58
|
+
Error
|
|
59
|
+
<InfoHint label="Error" side="right">
|
|
60
|
+
A failing rule blocks the merge until the finding is resolved or waived.
|
|
61
|
+
</InfoHint>
|
|
62
|
+
</span>
|
|
63
|
+
</div>
|
|
64
|
+
),
|
|
65
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
|
|
2
|
+
import { JsonDetailList } from '../components/ui/json-detail-list';
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'UI/JsonDetailList',
|
|
6
|
+
component: JsonDetailList,
|
|
7
|
+
} satisfies Meta<typeof JsonDetailList>;
|
|
8
|
+
|
|
9
|
+
export default meta;
|
|
10
|
+
type Story = StoryObj<typeof meta>;
|
|
11
|
+
|
|
12
|
+
const SAMPLE_PAYLOAD = {
|
|
13
|
+
rule_id: 'no-hardcoded-colors',
|
|
14
|
+
status: 'failed',
|
|
15
|
+
severity: 'error',
|
|
16
|
+
matched_count: 3,
|
|
17
|
+
is_blocking: true,
|
|
18
|
+
created_at: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(),
|
|
19
|
+
affected_files: ['components/admin/foo.tsx', 'components/admin/bar.tsx'],
|
|
20
|
+
evidence: {
|
|
21
|
+
firstMatch: {
|
|
22
|
+
file: 'components/admin/foo.tsx',
|
|
23
|
+
line: 42,
|
|
24
|
+
snippet: 'className="bg-[#212121] text-white"',
|
|
25
|
+
},
|
|
26
|
+
reviewerNote: 'Hex colors must be replaced with ODS tokens.\nSee the token rules doc.',
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A nested machine payload rendered as readable prose: keys humanised,
|
|
32
|
+
* ISO dates as relative times (hover for the raw timestamp), nested objects
|
|
33
|
+
* indented behind a border, scalar arrays joined inline.
|
|
34
|
+
*/
|
|
35
|
+
export const Default: Story = {
|
|
36
|
+
args: {
|
|
37
|
+
data: SAMPLE_PAYLOAD,
|
|
38
|
+
},
|
|
39
|
+
render: args => (
|
|
40
|
+
<div className="max-w-xl">
|
|
41
|
+
<JsonDetailList {...args} />
|
|
42
|
+
</div>
|
|
43
|
+
),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A JSON string parses transparently — the common case when the payload comes
|
|
48
|
+
* straight out of a text column.
|
|
49
|
+
*/
|
|
50
|
+
export const FromJsonString: Story = {
|
|
51
|
+
args: {
|
|
52
|
+
data: JSON.stringify(SAMPLE_PAYLOAD),
|
|
53
|
+
},
|
|
54
|
+
render: args => (
|
|
55
|
+
<div className="max-w-xl">
|
|
56
|
+
<JsonDetailList {...args} />
|
|
57
|
+
</div>
|
|
58
|
+
),
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Unparseable input falls back to a faithful raw render instead of blowing up.
|
|
63
|
+
*/
|
|
64
|
+
export const RawFallback: Story = {
|
|
65
|
+
args: {
|
|
66
|
+
data: 'plain text note: retried 3 times, gave up',
|
|
67
|
+
},
|
|
68
|
+
render: args => (
|
|
69
|
+
<div className="max-w-xl">
|
|
70
|
+
<JsonDetailList {...args} />
|
|
71
|
+
</div>
|
|
72
|
+
),
|
|
73
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { UnsavedChangesChip, useGuardedClose } from '../components/ui/modal-guarded-close';
|
|
4
|
+
import { Button } from '../components/ui/button';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Stories for `modal-guarded-close` — the unified dirty-state contract for
|
|
8
|
+
* modals. The chip is the visual affordance; the `useConfirm` /
|
|
9
|
+
* `useGuardedClose` hooks are exercised by the interactive story below (the
|
|
10
|
+
* chip story file covers all three exports of the module).
|
|
11
|
+
*/
|
|
12
|
+
const meta = {
|
|
13
|
+
title: 'UI/UnsavedChangesChip',
|
|
14
|
+
component: UnsavedChangesChip,
|
|
15
|
+
} satisfies Meta<typeof UnsavedChangesChip>;
|
|
16
|
+
|
|
17
|
+
export default meta;
|
|
18
|
+
type Story = StoryObj<typeof meta>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The standing "Unsaved changes" signal for a dirty modal's footer —
|
|
22
|
+
* `role="status"`, warning color, with the WHAT-is-dirty detail on hover.
|
|
23
|
+
*/
|
|
24
|
+
export const Default: Story = {
|
|
25
|
+
args: {
|
|
26
|
+
detail: '2 rules changed, 1 rule removed',
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* In its natural habitat: a form-modal footer between the status text and the
|
|
32
|
+
* Cancel/Save buttons.
|
|
33
|
+
*/
|
|
34
|
+
export const InAFooterRow: Story = {
|
|
35
|
+
args: {
|
|
36
|
+
detail: 'Name and threshold edited',
|
|
37
|
+
},
|
|
38
|
+
render: args => (
|
|
39
|
+
<div className="flex w-full max-w-2xl items-center justify-between gap-[var(--spacing-system-xsf)] rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-mf)]">
|
|
40
|
+
<span className="text-h6 text-ods-text-secondary">3 rules selected</span>
|
|
41
|
+
<div className="flex items-center gap-[var(--spacing-system-xsf)]">
|
|
42
|
+
<UnsavedChangesChip {...args} />
|
|
43
|
+
<Button variant="outline" size="small-legacy">
|
|
44
|
+
Cancel
|
|
45
|
+
</Button>
|
|
46
|
+
<Button size="small-legacy">Save</Button>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* `useGuardedClose` end-to-end: while dirty, Close asks the house confirm
|
|
54
|
+
* (`useConfirm` + ModalV2) before discarding; while clean, it closes
|
|
55
|
+
* immediately. Toggle the checkbox and hit Close to see both paths.
|
|
56
|
+
*/
|
|
57
|
+
export const GuardedCloseFlow: Story = {
|
|
58
|
+
args: {},
|
|
59
|
+
render: () => <GuardedCloseDemo />,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function GuardedCloseDemo() {
|
|
63
|
+
const [dirty, setDirty] = useState(true);
|
|
64
|
+
const [lastOutcome, setLastOutcome] = useState<string>('never closed');
|
|
65
|
+
const { guardedClose, dialog } = useGuardedClose(dirty, () => setLastOutcome('closed (edits discarded)'));
|
|
66
|
+
return (
|
|
67
|
+
<div className="flex max-w-md flex-col items-start gap-[var(--spacing-system-mf)]">
|
|
68
|
+
<label className="flex items-center gap-[var(--spacing-system-xsf)] text-h6 text-ods-text-primary">
|
|
69
|
+
<input type="checkbox" checked={dirty} onChange={e => setDirty(e.target.checked)} />
|
|
70
|
+
Form has unsaved edits
|
|
71
|
+
</label>
|
|
72
|
+
<div className="flex items-center gap-[var(--spacing-system-xsf)]">
|
|
73
|
+
{dirty && <UnsavedChangesChip detail="Demo edits" />}
|
|
74
|
+
<Button variant="outline" size="small-legacy" onClick={guardedClose}>
|
|
75
|
+
Close
|
|
76
|
+
</Button>
|
|
77
|
+
</div>
|
|
78
|
+
<span className="text-h6 text-ods-text-secondary">Last outcome: {lastOutcome}</span>
|
|
79
|
+
{dialog}
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
|
|
2
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../components/ui/tabs';
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'UI/Tabs',
|
|
6
|
+
component: Tabs,
|
|
7
|
+
parameters: {
|
|
8
|
+
docs: {
|
|
9
|
+
description: {
|
|
10
|
+
component:
|
|
11
|
+
'Radix-based Tabs primitives. The default variant is legacy (deprecated — use `TabNavigation` in new code); `variant="admin-rail"` is the sanctioned ODS underline-tab styling for admin config surfaces: transparent list on a bordered scrollable rail, accent underline on the active trigger. Set the variant on BOTH `TabsList` and each `TabsTrigger`.',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
} satisfies Meta<typeof Tabs>;
|
|
16
|
+
|
|
17
|
+
export default meta;
|
|
18
|
+
type Story = StoryObj<typeof meta>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Default (legacy) look — kept for un-migrated call sites.
|
|
22
|
+
*/
|
|
23
|
+
export const Default: Story = {
|
|
24
|
+
render: () => (
|
|
25
|
+
<Tabs defaultValue="overview" className="max-w-xl">
|
|
26
|
+
<TabsList>
|
|
27
|
+
<TabsTrigger value="overview">Overview</TabsTrigger>
|
|
28
|
+
<TabsTrigger value="settings">Settings</TabsTrigger>
|
|
29
|
+
<TabsTrigger value="history">History</TabsTrigger>
|
|
30
|
+
</TabsList>
|
|
31
|
+
<TabsContent value="overview">
|
|
32
|
+
<p className="text-h6 text-ods-text-secondary">Overview content.</p>
|
|
33
|
+
</TabsContent>
|
|
34
|
+
<TabsContent value="settings">
|
|
35
|
+
<p className="text-h6 text-ods-text-secondary">Settings content.</p>
|
|
36
|
+
</TabsContent>
|
|
37
|
+
<TabsContent value="history">
|
|
38
|
+
<p className="text-h6 text-ods-text-secondary">History content.</p>
|
|
39
|
+
</TabsContent>
|
|
40
|
+
</Tabs>
|
|
41
|
+
),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The `admin-rail` variant: `TabsList variant="admin-rail"` self-wraps in the
|
|
46
|
+
* bordered, horizontally-scrollable rail; triggers get the accent underline
|
|
47
|
+
* when active and secondary → primary text on hover. Disabled tabs stay
|
|
48
|
+
* visible but inert.
|
|
49
|
+
*/
|
|
50
|
+
export const AdminRail: Story = {
|
|
51
|
+
render: () => (
|
|
52
|
+
<Tabs defaultValue="rules" className="max-w-2xl">
|
|
53
|
+
<TabsList variant="admin-rail">
|
|
54
|
+
<TabsTrigger variant="admin-rail" value="rules">
|
|
55
|
+
Rules
|
|
56
|
+
</TabsTrigger>
|
|
57
|
+
<TabsTrigger variant="admin-rail" value="repos">
|
|
58
|
+
Repositories
|
|
59
|
+
</TabsTrigger>
|
|
60
|
+
<TabsTrigger variant="admin-rail" value="runs">
|
|
61
|
+
Runs
|
|
62
|
+
</TabsTrigger>
|
|
63
|
+
<TabsTrigger variant="admin-rail" value="billing" disabled>
|
|
64
|
+
Billing
|
|
65
|
+
</TabsTrigger>
|
|
66
|
+
</TabsList>
|
|
67
|
+
<TabsContent value="rules">
|
|
68
|
+
<p className="pt-[var(--spacing-system-mf)] text-h6 text-ods-text-secondary">Rule configuration lives here.</p>
|
|
69
|
+
</TabsContent>
|
|
70
|
+
<TabsContent value="repos">
|
|
71
|
+
<p className="pt-[var(--spacing-system-mf)] text-h6 text-ods-text-secondary">Repository bindings live here.</p>
|
|
72
|
+
</TabsContent>
|
|
73
|
+
<TabsContent value="runs">
|
|
74
|
+
<p className="pt-[var(--spacing-system-mf)] text-h6 text-ods-text-secondary">Sweep runs live here.</p>
|
|
75
|
+
</TabsContent>
|
|
76
|
+
</Tabs>
|
|
77
|
+
),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The rail scrolls horizontally when tabs overflow a narrow container —
|
|
82
|
+
* the border stays full-width under the scrolled list.
|
|
83
|
+
*/
|
|
84
|
+
export const AdminRailOverflow: Story = {
|
|
85
|
+
render: () => (
|
|
86
|
+
<div className="max-w-sm">
|
|
87
|
+
<Tabs defaultValue="t1">
|
|
88
|
+
<TabsList variant="admin-rail">
|
|
89
|
+
{['General', 'Data Sources', 'Executors', 'Prompts', 'Retrieval', 'Transcripts'].map((label, i) => (
|
|
90
|
+
<TabsTrigger key={label} variant="admin-rail" value={`t${i + 1}`}>
|
|
91
|
+
{label}
|
|
92
|
+
</TabsTrigger>
|
|
93
|
+
))}
|
|
94
|
+
</TabsList>
|
|
95
|
+
<TabsContent value="t1">
|
|
96
|
+
<p className="pt-[var(--spacing-system-mf)] text-h6 text-ods-text-secondary">General settings.</p>
|
|
97
|
+
</TabsContent>
|
|
98
|
+
</Tabs>
|
|
99
|
+
</div>
|
|
100
|
+
),
|
|
101
|
+
};
|