@flamingo-stack/openframe-frontend-core 0.0.200 → 0.0.201-snapshot.20260521140839

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 (57) hide show
  1. package/dist/{chunk-V2FNIPZJ.cjs → chunk-3B43AHYE.cjs} +2 -2
  2. package/dist/{chunk-TMD44IKJ.js.map → chunk-3B43AHYE.cjs.map} +1 -1
  3. package/dist/{chunk-TMD44IKJ.js → chunk-55HF462A.js} +2 -2
  4. package/dist/chunk-55HF462A.js.map +1 -0
  5. package/dist/{chunk-C3M6R6JH.cjs → chunk-DMUFJO4C.cjs} +797 -792
  6. package/dist/chunk-DMUFJO4C.cjs.map +1 -0
  7. package/dist/{chunk-ZOM75JOY.js → chunk-UZ2FOWW3.js} +4191 -4186
  8. package/dist/chunk-UZ2FOWW3.js.map +1 -0
  9. package/dist/components/features/index.cjs +13 -3
  10. package/dist/components/features/index.cjs.map +1 -1
  11. package/dist/components/features/index.d.ts +1 -1
  12. package/dist/components/features/index.d.ts.map +1 -1
  13. package/dist/components/features/index.js +16 -6
  14. package/dist/components/index.cjs +15 -3
  15. package/dist/components/index.cjs.map +1 -1
  16. package/dist/components/index.js +16 -4
  17. package/dist/components/layout/title-block.d.ts.map +1 -1
  18. package/dist/components/navigation/index.cjs +3 -3
  19. package/dist/components/navigation/index.js +2 -2
  20. package/dist/components/providers/theme-provider.d.ts +69 -0
  21. package/dist/components/providers/theme-provider.d.ts.map +1 -0
  22. package/dist/components/ui/entity-image.d.ts +9 -0
  23. package/dist/components/ui/entity-image.d.ts.map +1 -0
  24. package/dist/components/ui/file-manager/index.cjs +50 -50
  25. package/dist/components/ui/file-manager/index.js +1 -1
  26. package/dist/components/ui/index.cjs +5 -3
  27. package/dist/components/ui/index.cjs.map +1 -1
  28. package/dist/components/ui/index.d.ts +1 -0
  29. package/dist/components/ui/index.d.ts.map +1 -1
  30. package/dist/components/ui/index.js +4 -2
  31. package/dist/components/ui/organization-card.d.ts.map +1 -1
  32. package/dist/index.cjs +15 -3
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.js +16 -4
  35. package/package.json +1 -1
  36. package/src/components/features/index.ts +15 -1
  37. package/src/components/layout/title-block.tsx +6 -30
  38. package/src/components/providers/theme-provider.tsx +130 -0
  39. package/src/components/ui/button/button.tsx +1 -1
  40. package/src/components/ui/checkbox-block.tsx +13 -13
  41. package/src/components/ui/entity-image.tsx +56 -0
  42. package/src/components/ui/index.ts +1 -0
  43. package/src/components/ui/organization-card.tsx +4 -8
  44. package/src/stories/CheckboxBlock.stories.tsx +1 -3
  45. package/src/stories/OrganizationCard.stories.tsx +14 -0
  46. package/src/stories/Theme.stories.tsx +350 -0
  47. package/src/styles/README.md +271 -174
  48. package/src/styles/dark_theme.tokens.json +982 -0
  49. package/src/styles/light_theme.tokens.json +982 -0
  50. package/src/styles/ods-colors.css +225 -146
  51. package/dist/chunk-C3M6R6JH.cjs.map +0 -1
  52. package/dist/chunk-V2FNIPZJ.cjs.map +0 -1
  53. package/dist/chunk-ZOM75JOY.js.map +0 -1
  54. package/dist/components/features/organization-icon.d.ts +0 -80
  55. package/dist/components/features/organization-icon.d.ts.map +0 -1
  56. package/src/components/features/organization-icon.tsx +0 -175
  57. package/src/styles/ods_color_tokens.json +0 -302
@@ -28,14 +28,15 @@ const CheckboxBlock = React.forwardRef<
28
28
  <label
29
29
  htmlFor={id}
30
30
  className={cn(
31
- "flex items-center gap-[var(--spacing-system-s)] rounded-[6px] border w-full",
31
+ "flex items-center gap-[var(--spacing-system-s)] rounded-md ring-1 ring-inset w-full",
32
32
  "p-[var(--spacing-system-sf)]",
33
- !description && "h-11 md:h-12",
34
- "bg-ods-card border-ods-border",
33
+ !description && "min-h-[44px] md:min-h-[48px]",
34
+ description && "min-h-[60px] md:min-h-[64px]",
35
+ "bg-ods-card ring-ods-border",
35
36
  "cursor-pointer transition-colors duration-200",
36
- "hover:border-ods-accent/30",
37
- disabled && "opacity-50 cursor-not-allowed hover:border-ods-border",
38
- error && "border-ods-error",
37
+ "hover:ring-ods-accent/30",
38
+ disabled && "opacity-50 cursor-not-allowed hover:ring-ods-border",
39
+ error && "ring-ods-error",
39
40
  )}
40
41
  >
41
42
  <CheckboxPrimitive.Root
@@ -46,7 +47,7 @@ const CheckboxBlock = React.forwardRef<
46
47
  onCheckedChange={onCheckedChange}
47
48
  disabled={disabled}
48
49
  className={cn(
49
- "h-6 w-6 shrink-0",
50
+ "h-4 w-4 md:h-6 md:w-6 shrink-0",
50
51
  "rounded-[6px] border-2",
51
52
  error ? "border-ods-error" : "border-[var(--color-border-strong)]",
52
53
  "bg-ods-card",
@@ -58,21 +59,20 @@ const CheckboxBlock = React.forwardRef<
58
59
  <CheckboxPrimitive.Indicator
59
60
  className="flex items-center justify-center text-ods-text-on-accent"
60
61
  >
61
- <CheckboxCheckmarkIcon size={10} />
62
+ <CheckboxCheckmarkIcon className="w-2 h-2 md:w-2.5 md:h-2.5" />
62
63
  </CheckboxPrimitive.Indicator>
63
64
  </CheckboxPrimitive.Root>
64
65
  <div className="flex flex-1 flex-col justify-center min-w-0">
65
66
  <span className={cn(
66
- "text-h4",
67
- "text-ods-text-primary select-none",
68
- !description && "truncate"
67
+ "text-h4 !leading-5 md:!leading-6",
68
+ "text-ods-text-primary select-none break-words"
69
69
  )}>
70
70
  {label}
71
71
  </span>
72
72
  {description && (
73
73
  <span className={cn(
74
- "text-h6",
75
- "text-ods-text-secondary select-none"
74
+ "text-h6 !leading-4",
75
+ "text-ods-text-secondary select-none break-words"
76
76
  )}>
77
77
  {description}
78
78
  </span>
@@ -0,0 +1,56 @@
1
+ 'use client'
2
+
3
+ import React from 'react'
4
+ import { cn } from '../../utils/cn'
5
+
6
+ function getInitials(name?: string): string {
7
+ if (!name) return ''
8
+ const words = name.trim().split(/\s+/)
9
+ if (words.length === 1) return words[0].charAt(0).toUpperCase()
10
+ return (words[0].charAt(0) + words[words.length - 1].charAt(0)).toUpperCase()
11
+ }
12
+
13
+ export interface EntityImageProps {
14
+ src?: string | null
15
+ alt?: string
16
+ /** Overrides the initials source. Defaults to `alt`. */
17
+ fallbackText?: string
18
+ className?: string
19
+ }
20
+
21
+ export function EntityImage({ src, alt, fallbackText, className }: EntityImageProps) {
22
+ const [imageFailed, setImageFailed] = React.useState(false)
23
+
24
+ React.useEffect(() => {
25
+ setImageFailed(false)
26
+ }, [src])
27
+
28
+ const showFallback = imageFailed || !src
29
+ const initials = getInitials(fallbackText ?? alt)
30
+
31
+ if (showFallback) {
32
+ return (
33
+ <div
34
+ aria-label={alt}
35
+ className={cn(
36
+ 'size-[52px] md:size-[60px] shrink-0 rounded-md border border-ods-border bg-ods-bg flex items-center justify-center text-ods-text-secondary text-h4 select-none',
37
+ className,
38
+ )}
39
+ >
40
+ {initials || '?'}
41
+ </div>
42
+ )
43
+ }
44
+
45
+ return (
46
+ <img
47
+ src={src ?? undefined}
48
+ alt={alt ?? ''}
49
+ onError={() => setImageFailed(true)}
50
+ className={cn(
51
+ 'size-[52px] md:size-[60px] shrink-0 rounded-md border border-ods-border object-cover',
52
+ className,
53
+ )}
54
+ />
55
+ )
56
+ }
@@ -72,6 +72,7 @@ export { CheckIcon, CheckCircleIcon as LucideCheckCircleIcon, XIcon as LucideXIc
72
72
  export * from './dashboard-info-card'
73
73
  export * from './device-card'
74
74
  export * from './device-card-compact'
75
+ export * from './entity-image'
75
76
  export * from './feature-card'
76
77
  export * from './feature-list'
77
78
  export { FloatingTooltip } from './floating-tooltip'
@@ -4,7 +4,7 @@ import React from "react"
4
4
  import Link from "next/link"
5
5
  import { Monitor } from "lucide-react"
6
6
  import { cn } from "../../utils/cn"
7
- import { OrganizationIcon } from "../features/organization-icon"
7
+ import { EntityImage } from "./entity-image"
8
8
 
9
9
  export interface Organization {
10
10
  id: string
@@ -98,13 +98,9 @@ export function OrganizationCard({
98
98
 
99
99
  {/* Header */}
100
100
  <div className="flex items-start gap-3 w-full">
101
- <OrganizationIcon
102
- imageUrl={fetchedImageUrl || organization.imageUrl}
103
- organizationName={organization.name}
104
- size="xl"
105
- backgroundStyle="dark"
106
- showBackground={true}
107
- className="w-[60px] h-[60px]"
101
+ <EntityImage
102
+ src={fetchedImageUrl || organization.imageUrl}
103
+ alt={organization.name}
108
104
  />
109
105
 
110
106
  <div className="flex-1 flex flex-col justify-center py-2 min-w-0">
@@ -186,9 +186,7 @@ export const FlamingoError: Story = {
186
186
  },
187
187
  decorators: [
188
188
  (Story) => (
189
- <div data-app-type="flamingo" style={{ width: '320px' }}>
190
- <Story />
191
- </div>
189
+ <Story />
192
190
  ),
193
191
  ],
194
192
  };
@@ -59,6 +59,20 @@ export const Default: Story = {
59
59
  },
60
60
  }
61
61
 
62
+ /**
63
+ * Card with an organization logo image.
64
+ */
65
+ export const WithImage: Story = {
66
+ args: {
67
+ organization: {
68
+ ...baseOrg,
69
+ imageUrl: 'https://picsum.photos/seed/acme/120/120',
70
+ },
71
+ href: '/organizations/details/1',
72
+ deviceCount: 142,
73
+ },
74
+ }
75
+
62
76
  /**
63
77
  * Card as a clickable link.
64
78
  */
@@ -0,0 +1,350 @@
1
+ import type { Meta, StoryObj } from '@storybook/nextjs-vite'
2
+ import { Moon, Sun } from 'lucide-react'
3
+ import React from 'react'
4
+ import { Alert, AlertDescription, AlertTitle } from '../components/ui/alert'
5
+ import { Badge } from '../components/ui/badge'
6
+ import { Button } from '../components/ui/button'
7
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../components/ui/card'
8
+ import { Input } from '../components/ui/input'
9
+ import { ThemeProvider, useThemeToggle } from '../components/providers/theme-provider'
10
+
11
+ const meta = {
12
+ title: 'Foundations/Theme',
13
+ parameters: {
14
+ layout: 'fullscreen',
15
+ docs: {
16
+ description: {
17
+ component:
18
+ 'Demonstrates the ODS light/dark theme system. The `ThemeProvider` (wrapping `next-themes`) sets `data-theme="light|dark"` on `<html>`, and `src/styles/ods-colors.css` swaps the `--ods-*` primitives accordingly. Use `useThemeToggle()` to build your own toggle UI.',
19
+ },
20
+ },
21
+ },
22
+ } satisfies Meta
23
+
24
+ export default meta
25
+ type Story = StoryObj<typeof meta>
26
+
27
+ /* ------------------------------------------------------------------ */
28
+ /* Helpers */
29
+ /* ------------------------------------------------------------------ */
30
+
31
+ function ThemeToggleButton() {
32
+ const { isDark, toggle, mounted } = useThemeToggle()
33
+ return (
34
+ <Button
35
+ variant="outline"
36
+ onClick={toggle}
37
+ leftIcon={mounted ? (isDark ? <Sun /> : <Moon />) : undefined}
38
+ aria-label={isDark ? 'Switch to light theme' : 'Switch to dark theme'}
39
+ >
40
+ {mounted ? (isDark ? 'Switch to light' : 'Switch to dark') : 'Toggle theme'}
41
+ </Button>
42
+ )
43
+ }
44
+
45
+ function ThemeStatusBar() {
46
+ const { theme, isDark, setTheme, toggle, mounted } = useThemeToggle()
47
+ return (
48
+ <div className="flex flex-wrap items-center gap-3 rounded-lg border border-ods-border bg-ods-card p-4">
49
+ <span className="text-body-sm text-ods-text-secondary">Current theme:</span>
50
+ <Badge variant={isDark ? 'secondary' : 'outline'} className="uppercase">
51
+ {mounted ? theme : '…'}
52
+ </Badge>
53
+ <div className="ml-auto flex flex-wrap gap-2">
54
+ <Button size="small" variant="outline" onClick={() => setTheme('light')}>
55
+ Set light
56
+ </Button>
57
+ <Button size="small" variant="outline" onClick={() => setTheme('dark')}>
58
+ Set dark
59
+ </Button>
60
+ <Button size="small" variant="accent" onClick={toggle}>
61
+ Toggle
62
+ </Button>
63
+ </div>
64
+ </div>
65
+ )
66
+ }
67
+
68
+ interface SwatchProps {
69
+ name: string
70
+ /** Tailwind class that consumes the token (e.g. `bg-ods-bg`). */
71
+ className: string
72
+ /** Render dark text instead of light (for very light tokens). */
73
+ darkLabel?: boolean
74
+ }
75
+
76
+ function Swatch({ name, className, darkLabel }: SwatchProps) {
77
+ return (
78
+ <div className="flex flex-col gap-1">
79
+ <div
80
+ className={`${className} h-16 w-full rounded-md border border-ods-border flex items-end justify-start p-2`}
81
+ >
82
+ <span
83
+ className={`text-caption font-mono ${
84
+ darkLabel ? 'text-black/70' : 'text-white/80'
85
+ }`}
86
+ >
87
+ {name.replace(/^bg-|^text-|^border-/, '')}
88
+ </span>
89
+ </div>
90
+ <span className="text-caption text-ods-text-secondary font-mono">{name}</span>
91
+ </div>
92
+ )
93
+ }
94
+
95
+ function TokenGrid() {
96
+ return (
97
+ <section className="space-y-6">
98
+ <div>
99
+ <h3 className="text-h5 text-ods-text-primary mb-3">Surfaces</h3>
100
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
101
+ <Swatch name="bg-ods-bg" className="bg-ods-bg" />
102
+ <Swatch name="bg-ods-card" className="bg-ods-card" />
103
+ <Swatch name="bg-ods-bg-surface" className="bg-ods-bg-surface" />
104
+ <Swatch name="bg-ods-bg-hover" className="bg-ods-bg-hover" />
105
+ </div>
106
+ </div>
107
+
108
+ <div>
109
+ <h3 className="text-h5 text-ods-text-primary mb-3">Text</h3>
110
+ <div className="rounded-lg border border-ods-border bg-ods-card p-4 space-y-1">
111
+ <p className="text-ods-text-primary">text-ods-text-primary — primary text</p>
112
+ <p className="text-ods-text-secondary">text-ods-text-secondary — secondary text</p>
113
+ <p className="text-ods-text-tertiary">text-ods-text-tertiary — tertiary text</p>
114
+ <p className="text-ods-text-muted">text-ods-text-muted — muted text</p>
115
+ <p className="text-ods-text-disabled">text-ods-text-disabled — disabled text</p>
116
+ </div>
117
+ </div>
118
+
119
+ <div>
120
+ <h3 className="text-h5 text-ods-text-primary mb-3">Accent &amp; status</h3>
121
+ <div className="grid grid-cols-2 md:grid-cols-5 gap-3">
122
+ <Swatch name="bg-ods-accent" className="bg-ods-accent" darkLabel />
123
+ <Swatch name="bg-ods-success" className="bg-ods-success" />
124
+ <Swatch name="bg-ods-error" className="bg-ods-error" />
125
+ <Swatch name="bg-ods-warning" className="bg-ods-warning" darkLabel />
126
+ <Swatch name="bg-ods-info" className="bg-ods-info" darkLabel />
127
+ </div>
128
+ </div>
129
+
130
+ <div>
131
+ <h3 className="text-h5 text-ods-text-primary mb-3">Borders</h3>
132
+ <div className="grid grid-cols-2 md:grid-cols-3 gap-3">
133
+ <div className="h-16 rounded-md border-2 border-ods-border bg-ods-card flex items-center justify-center text-caption font-mono text-ods-text-secondary">
134
+ border-ods-border
135
+ </div>
136
+ <div className="h-16 rounded-md border-2 border-ods-border-hover bg-ods-card flex items-center justify-center text-caption font-mono text-ods-text-secondary">
137
+ border-ods-border-hover
138
+ </div>
139
+ <div className="h-16 rounded-md border-2 border-ods-border-focus bg-ods-card flex items-center justify-center text-caption font-mono text-ods-text-secondary">
140
+ border-ods-border-focus
141
+ </div>
142
+ </div>
143
+ </div>
144
+ </section>
145
+ )
146
+ }
147
+
148
+ function ComponentsShowcase() {
149
+ return (
150
+ <section className="space-y-6">
151
+ <div>
152
+ <h3 className="text-h5 text-ods-text-primary mb-3">Buttons</h3>
153
+ <div className="flex flex-wrap gap-3">
154
+ <Button variant="accent">Accent</Button>
155
+ <Button variant="outline">Outline</Button>
156
+ <Button variant="transparent">Transparent</Button>
157
+ <Button variant="destructive">Destructive</Button>
158
+ <Button variant="outline" disabled>
159
+ Disabled
160
+ </Button>
161
+ <Button variant="accent" loading>
162
+ Loading
163
+ </Button>
164
+ </div>
165
+ </div>
166
+
167
+ <div>
168
+ <h3 className="text-h5 text-ods-text-primary mb-3">Inputs</h3>
169
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3 max-w-xl">
170
+ <Input placeholder="Default input" />
171
+ <Input placeholder="With value" defaultValue="user@flamingo.cx" />
172
+ <Input placeholder="Invalid" invalid error="Required" />
173
+ <Input placeholder="Disabled" disabled />
174
+ </div>
175
+ </div>
176
+
177
+ <div>
178
+ <h3 className="text-h5 text-ods-text-primary mb-3">Badges</h3>
179
+ <div className="flex flex-wrap gap-2">
180
+ <Badge>Default</Badge>
181
+ <Badge variant="secondary">Secondary</Badge>
182
+ <Badge variant="outline">Outline</Badge>
183
+ <Badge variant="success">Success</Badge>
184
+ <Badge variant="destructive">Destructive</Badge>
185
+ </div>
186
+ </div>
187
+
188
+ <div>
189
+ <h3 className="text-h5 text-ods-text-primary mb-3">Cards</h3>
190
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
191
+ <Card>
192
+ <CardHeader>
193
+ <CardTitle>Card title</CardTitle>
194
+ <CardDescription>
195
+ Card surface and text adapt to the active theme.
196
+ </CardDescription>
197
+ </CardHeader>
198
+ <CardContent>
199
+ <p className="text-body-sm text-ods-text-secondary">
200
+ All colors come from <code className="text-ods-accent">--ods-*</code> tokens — the
201
+ same markup renders correctly in both themes.
202
+ </p>
203
+ </CardContent>
204
+ </Card>
205
+ <Card>
206
+ <CardHeader>
207
+ <CardTitle>Inputs &amp; actions</CardTitle>
208
+ <CardDescription>Try interacting — focus rings flip too.</CardDescription>
209
+ </CardHeader>
210
+ <CardContent className="space-y-3">
211
+ <Input placeholder="Type something…" />
212
+ <div className="flex gap-2">
213
+ <Button size="small" variant="accent">
214
+ Save
215
+ </Button>
216
+ <Button size="small" variant="outline">
217
+ Cancel
218
+ </Button>
219
+ </div>
220
+ </CardContent>
221
+ </Card>
222
+ </div>
223
+ </div>
224
+
225
+ <div>
226
+ <h3 className="text-h5 text-ods-text-primary mb-3">Alerts</h3>
227
+ <div className="space-y-3 max-w-2xl">
228
+ <Alert>
229
+ <AlertTitle>Informational</AlertTitle>
230
+ <AlertDescription>
231
+ Default alert surface — uses card background and primary text tokens.
232
+ </AlertDescription>
233
+ </Alert>
234
+ </div>
235
+ </div>
236
+ </section>
237
+ )
238
+ }
239
+
240
+ /* ------------------------------------------------------------------ */
241
+ /* Stories */
242
+ /* ------------------------------------------------------------------ */
243
+
244
+ /**
245
+ * Full showcase — toggle the theme and watch every primitive flip in place.
246
+ *
247
+ * The whole story is wrapped in `<ThemeProvider>`. `useThemeToggle()` is used
248
+ * to drive the toggle button (and `setTheme('light' | 'dark')` is wired to the
249
+ * explicit "Set light / Set dark" buttons). All visible color comes from ODS
250
+ * tokens, so nothing is hardcoded.
251
+ */
252
+ export const Showcase: Story = {
253
+ render: () => (
254
+ <ThemeProvider>
255
+ <div className="min-h-screen bg-ods-bg text-ods-text-primary p-6 md:p-10 space-y-8 transition-colors">
256
+ <header className="space-y-2">
257
+ <h1 className="text-h2 text-ods-text-primary">ODS theme switching</h1>
258
+ <p className="text-body text-ods-text-secondary max-w-2xl">
259
+ One <code className="text-ods-accent">data-theme</code> attribute on{' '}
260
+ <code className="text-ods-accent">&lt;html&gt;</code> flips every{' '}
261
+ <code className="text-ods-accent">--ods-*</code> primitive. Components below don&apos;t
262
+ know — and don&apos;t care — which theme is active; they read tokens.
263
+ </p>
264
+ </header>
265
+
266
+ <ThemeStatusBar />
267
+
268
+ <TokenGrid />
269
+ <ComponentsShowcase />
270
+ </div>
271
+ </ThemeProvider>
272
+ ),
273
+ }
274
+
275
+ /**
276
+ * Minimal example: just the toggle button and a single card.
277
+ *
278
+ * Useful as a copy-paste reference for what consumer apps need to do to add a
279
+ * theme switch: wrap once in `<ThemeProvider>`, then build any button you like
280
+ * around `useThemeToggle()`.
281
+ */
282
+ export const ToggleOnly: Story = {
283
+ render: () => (
284
+ <ThemeProvider>
285
+ <div className="min-h-screen bg-ods-bg text-ods-text-primary p-10 flex items-center justify-center transition-colors">
286
+ <Card className="w-full max-w-md">
287
+ <CardHeader>
288
+ <CardTitle>Theme toggle</CardTitle>
289
+ <CardDescription>
290
+ Click the button below — the entire surface, text and border swap themes.
291
+ </CardDescription>
292
+ </CardHeader>
293
+ <CardContent className="flex justify-center">
294
+ <ThemeToggleButton />
295
+ </CardContent>
296
+ </Card>
297
+ </div>
298
+ </ThemeProvider>
299
+ ),
300
+ }
301
+
302
+ /**
303
+ * Side-by-side: force light and dark on two halves of the screen at once using
304
+ * the `.theme-light` / `.theme-dark` class escape hatches (no provider needed
305
+ * for these — they directly scope the primitive overrides). Handy for visual
306
+ * diffing without flipping the document.
307
+ */
308
+ export const SideBySide: Story = {
309
+ render: () => (
310
+ <div className="grid grid-cols-1 md:grid-cols-2 min-h-screen">
311
+ {(['light', 'dark'] as const).map((mode) => (
312
+ <div
313
+ key={mode}
314
+ className={`theme-${mode} bg-ods-bg text-ods-text-primary p-6 space-y-4 border-r border-ods-border`}
315
+ >
316
+ <div className="flex items-center gap-2">
317
+ <Badge variant="outline" className="uppercase">
318
+ {mode}
319
+ </Badge>
320
+ <span className="text-body-sm text-ods-text-secondary">
321
+ scoped via <code className="text-ods-accent">.theme-{mode}</code>
322
+ </span>
323
+ </div>
324
+ <Card>
325
+ <CardHeader>
326
+ <CardTitle>Same component, different theme</CardTitle>
327
+ <CardDescription>
328
+ Both halves render identical JSX — only the wrapping class differs.
329
+ </CardDescription>
330
+ </CardHeader>
331
+ <CardContent className="space-y-3">
332
+ <Input placeholder="Email" defaultValue="hello@flamingo.cx" />
333
+ <div className="flex gap-2 flex-wrap">
334
+ <Button size="small" variant="accent">
335
+ Primary
336
+ </Button>
337
+ <Button size="small" variant="outline">
338
+ Secondary
339
+ </Button>
340
+ <Badge>Default</Badge>
341
+ <Badge variant="success">Success</Badge>
342
+ <Badge variant="destructive">Error</Badge>
343
+ </div>
344
+ </CardContent>
345
+ </Card>
346
+ </div>
347
+ ))}
348
+ </div>
349
+ ),
350
+ }