@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.
Files changed (124) hide show
  1. package/dist/{chunk-CE4CBCKX.cjs → chunk-25JYFZ6D.cjs} +7 -7
  2. package/dist/{chunk-CE4CBCKX.cjs.map → chunk-25JYFZ6D.cjs.map} +1 -1
  3. package/dist/{chunk-3OFRFRBW.cjs → chunk-2JMWI7RE.cjs} +26 -26
  4. package/dist/{chunk-3OFRFRBW.cjs.map → chunk-2JMWI7RE.cjs.map} +1 -1
  5. package/dist/{chunk-MYQAF43S.js → chunk-2WNONXFZ.js} +4 -4
  6. package/dist/{chunk-6MW6ELZQ.cjs → chunk-56NIEVZ4.cjs} +37 -37
  7. package/dist/{chunk-6MW6ELZQ.cjs.map → chunk-56NIEVZ4.cjs.map} +1 -1
  8. package/dist/{chunk-M7FGOI4V.cjs → chunk-6CTX5BLD.cjs} +31 -31
  9. package/dist/{chunk-M7FGOI4V.cjs.map → chunk-6CTX5BLD.cjs.map} +1 -1
  10. package/dist/{chunk-2ALPHSGU.js → chunk-6S5E5RLR.js} +4 -4
  11. package/dist/{chunk-TPIF252F.js → chunk-CELZDZP2.js} +2 -2
  12. package/dist/{chunk-C7T363HO.js → chunk-DFZTO3NG.js} +2 -2
  13. package/dist/{chunk-OPPGJ4PY.cjs → chunk-DTETRKV6.cjs} +87 -87
  14. package/dist/{chunk-OPPGJ4PY.cjs.map → chunk-DTETRKV6.cjs.map} +1 -1
  15. package/dist/{chunk-HM53VV7K.cjs → chunk-H635T5R2.cjs} +1316 -1079
  16. package/dist/chunk-H635T5R2.cjs.map +1 -0
  17. package/dist/{chunk-MVYTOP3B.js → chunk-HI7LXHWR.js} +5 -5
  18. package/dist/{chunk-KSZPPVXT.cjs → chunk-HMYZM3SS.cjs} +11 -11
  19. package/dist/{chunk-KSZPPVXT.cjs.map → chunk-HMYZM3SS.cjs.map} +1 -1
  20. package/dist/{chunk-YTLLRLMU.cjs → chunk-I5RARV6I.cjs} +14 -14
  21. package/dist/{chunk-YTLLRLMU.cjs.map → chunk-I5RARV6I.cjs.map} +1 -1
  22. package/dist/{chunk-TH4BQCOD.cjs → chunk-JFGAVBWJ.cjs} +26 -26
  23. package/dist/{chunk-TH4BQCOD.cjs.map → chunk-JFGAVBWJ.cjs.map} +1 -1
  24. package/dist/{chunk-E2W2XMX2.js → chunk-JHEUPQAF.js} +2 -2
  25. package/dist/{chunk-JBCNOJMX.cjs → chunk-LEN53CEM.cjs} +3 -3
  26. package/dist/{chunk-JBCNOJMX.cjs.map → chunk-LEN53CEM.cjs.map} +1 -1
  27. package/dist/{chunk-QCWVGCT3.js → chunk-MAAN76UX.js} +6 -6
  28. package/dist/{chunk-HYVYAWUO.js → chunk-N3XPBO6W.js} +2 -2
  29. package/dist/{chunk-QRYJYZTW.cjs → chunk-NFTLCOJF.cjs} +61 -61
  30. package/dist/{chunk-QRYJYZTW.cjs.map → chunk-NFTLCOJF.cjs.map} +1 -1
  31. package/dist/{chunk-4DJL34HL.js → chunk-Q3DK4P3F.js} +2 -2
  32. package/dist/{chunk-ARCH6RK6.cjs → chunk-RLAS623H.cjs} +4 -4
  33. package/dist/{chunk-ARCH6RK6.cjs.map → chunk-RLAS623H.cjs.map} +1 -1
  34. package/dist/{chunk-CBD3IHPF.js → chunk-RNAXQVBK.js} +2 -2
  35. package/dist/{chunk-2MPS2QY5.cjs → chunk-STJB3XH6.cjs} +5 -5
  36. package/dist/{chunk-2MPS2QY5.cjs.map → chunk-STJB3XH6.cjs.map} +1 -1
  37. package/dist/{chunk-LEMU4JMX.js → chunk-WR2XVJ6K.js} +2 -2
  38. package/dist/{chunk-43MPUCDL.js → chunk-XMROOTCF.js} +2 -2
  39. package/dist/{chunk-INTFGXHO.js → chunk-Y2BLPCJZ.js} +4979 -4742
  40. package/dist/chunk-Y2BLPCJZ.js.map +1 -0
  41. package/dist/{chunk-ZYBBV6CK.js → chunk-Y5MDIJHH.js} +2 -2
  42. package/dist/{chunk-S4BAM7EC.cjs → chunk-YLWWP46N.cjs} +9 -9
  43. package/dist/{chunk-S4BAM7EC.cjs.map → chunk-YLWWP46N.cjs.map} +1 -1
  44. package/dist/components/case-studies/index.cjs +8 -8
  45. package/dist/components/case-studies/index.js +2 -2
  46. package/dist/components/chat/index.cjs +2 -2
  47. package/dist/components/chat/index.js +1 -1
  48. package/dist/components/contact/index.cjs +3 -3
  49. package/dist/components/contact/index.js +2 -2
  50. package/dist/components/docs/index.cjs +5 -5
  51. package/dist/components/docs/index.js +4 -4
  52. package/dist/components/embeds/index.cjs +3 -3
  53. package/dist/components/embeds/index.js +2 -2
  54. package/dist/components/faq/index.cjs +3 -3
  55. package/dist/components/faq/index.js +2 -2
  56. package/dist/components/features/index.cjs +2 -2
  57. package/dist/components/features/index.js +1 -1
  58. package/dist/components/help-center-pages/index.cjs +22 -22
  59. package/dist/components/help-center-pages/index.js +13 -13
  60. package/dist/components/index.cjs +144 -132
  61. package/dist/components/index.cjs.map +1 -1
  62. package/dist/components/index.js +23 -11
  63. package/dist/components/index.js.map +1 -1
  64. package/dist/components/meeting-scheduler/index.cjs +34 -34
  65. package/dist/components/meeting-scheduler/index.js +3 -3
  66. package/dist/components/navigation/index.cjs +2 -2
  67. package/dist/components/navigation/index.js +1 -1
  68. package/dist/components/onboarding-guides/index.cjs +5 -5
  69. package/dist/components/onboarding-guides/index.js +4 -4
  70. package/dist/components/related-content/index.cjs +3 -3
  71. package/dist/components/related-content/index.js +2 -2
  72. package/dist/components/tickets/index.cjs +6 -6
  73. package/dist/components/tickets/index.js +5 -5
  74. package/dist/components/ui/dropdown-button.d.ts +42 -11
  75. package/dist/components/ui/dropdown-button.d.ts.map +1 -1
  76. package/dist/components/ui/field.d.ts +38 -0
  77. package/dist/components/ui/field.d.ts.map +1 -0
  78. package/dist/components/ui/index.cjs +14 -2
  79. package/dist/components/ui/index.cjs.map +1 -1
  80. package/dist/components/ui/index.d.ts +4 -0
  81. package/dist/components/ui/index.d.ts.map +1 -1
  82. package/dist/components/ui/index.js +13 -1
  83. package/dist/components/ui/info-hint.d.ts +23 -0
  84. package/dist/components/ui/info-hint.d.ts.map +1 -0
  85. package/dist/components/ui/json-detail-list.d.ts +9 -0
  86. package/dist/components/ui/json-detail-list.d.ts.map +1 -0
  87. package/dist/components/ui/modal-guarded-close.d.ts +50 -0
  88. package/dist/components/ui/modal-guarded-close.d.ts.map +1 -0
  89. package/dist/components/ui/modal-v2.d.ts.map +1 -1
  90. package/dist/components/ui/tabs.d.ts +19 -5
  91. package/dist/components/ui/tabs.d.ts.map +1 -1
  92. package/dist/index.cjs +14 -2
  93. package/dist/index.cjs.map +1 -1
  94. package/dist/index.js +13 -1
  95. package/package.json +3 -2
  96. package/src/components/ui/dropdown-button.tsx +109 -46
  97. package/src/components/ui/field.tsx +74 -0
  98. package/src/components/ui/index.ts +4 -0
  99. package/src/components/ui/info-hint.tsx +74 -0
  100. package/src/components/ui/json-detail-list.tsx +112 -0
  101. package/src/components/ui/modal-guarded-close.tsx +133 -0
  102. package/src/components/ui/modal-v2.tsx +12 -5
  103. package/src/components/ui/tabs.tsx +75 -20
  104. package/src/stories/DropdownButton.stories.tsx +109 -0
  105. package/src/stories/Field.stories.tsx +70 -0
  106. package/src/stories/InfoHint.stories.tsx +65 -0
  107. package/src/stories/JsonDetailList.stories.tsx +73 -0
  108. package/src/stories/ModalGuardedClose.stories.tsx +82 -0
  109. package/src/stories/Tabs.stories.tsx +101 -0
  110. package/dist/chunk-HM53VV7K.cjs.map +0 -1
  111. package/dist/chunk-INTFGXHO.js.map +0 -1
  112. /package/dist/{chunk-MYQAF43S.js.map → chunk-2WNONXFZ.js.map} +0 -0
  113. /package/dist/{chunk-2ALPHSGU.js.map → chunk-6S5E5RLR.js.map} +0 -0
  114. /package/dist/{chunk-TPIF252F.js.map → chunk-CELZDZP2.js.map} +0 -0
  115. /package/dist/{chunk-C7T363HO.js.map → chunk-DFZTO3NG.js.map} +0 -0
  116. /package/dist/{chunk-MVYTOP3B.js.map → chunk-HI7LXHWR.js.map} +0 -0
  117. /package/dist/{chunk-E2W2XMX2.js.map → chunk-JHEUPQAF.js.map} +0 -0
  118. /package/dist/{chunk-QCWVGCT3.js.map → chunk-MAAN76UX.js.map} +0 -0
  119. /package/dist/{chunk-HYVYAWUO.js.map → chunk-N3XPBO6W.js.map} +0 -0
  120. /package/dist/{chunk-4DJL34HL.js.map → chunk-Q3DK4P3F.js.map} +0 -0
  121. /package/dist/{chunk-CBD3IHPF.js.map → chunk-RNAXQVBK.js.map} +0 -0
  122. /package/dist/{chunk-LEMU4JMX.js.map → chunk-WR2XVJ6K.js.map} +0 -0
  123. /package/dist/{chunk-43MPUCDL.js.map → chunk-XMROOTCF.js.map} +0 -0
  124. /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
- /** @deprecated Use `TabNavigation` from `components/ui/tab-navigation`. See {@link Tabs}. */
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
- React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
22
- >(({ className, ...props }, ref) => (
23
- <TabsPrimitive.List
24
- ref={ref}
25
- className={cn(
26
- "inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
27
- className
28
- )}
29
- {...props}
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
- /** @deprecated Use `TabNavigation` from `components/ui/tab-navigation`. See {@link Tabs}. */
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
- React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
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
+ };