@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.
- package/dist/{chunk-V2FNIPZJ.cjs → chunk-3B43AHYE.cjs} +2 -2
- package/dist/{chunk-TMD44IKJ.js.map → chunk-3B43AHYE.cjs.map} +1 -1
- package/dist/{chunk-TMD44IKJ.js → chunk-55HF462A.js} +2 -2
- package/dist/chunk-55HF462A.js.map +1 -0
- package/dist/{chunk-C3M6R6JH.cjs → chunk-DMUFJO4C.cjs} +797 -792
- package/dist/chunk-DMUFJO4C.cjs.map +1 -0
- package/dist/{chunk-ZOM75JOY.js → chunk-UZ2FOWW3.js} +4191 -4186
- package/dist/chunk-UZ2FOWW3.js.map +1 -0
- package/dist/components/features/index.cjs +13 -3
- package/dist/components/features/index.cjs.map +1 -1
- package/dist/components/features/index.d.ts +1 -1
- package/dist/components/features/index.d.ts.map +1 -1
- package/dist/components/features/index.js +16 -6
- package/dist/components/index.cjs +15 -3
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +16 -4
- package/dist/components/layout/title-block.d.ts.map +1 -1
- package/dist/components/navigation/index.cjs +3 -3
- package/dist/components/navigation/index.js +2 -2
- package/dist/components/providers/theme-provider.d.ts +69 -0
- package/dist/components/providers/theme-provider.d.ts.map +1 -0
- package/dist/components/ui/entity-image.d.ts +9 -0
- package/dist/components/ui/entity-image.d.ts.map +1 -0
- package/dist/components/ui/file-manager/index.cjs +50 -50
- package/dist/components/ui/file-manager/index.js +1 -1
- package/dist/components/ui/index.cjs +5 -3
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.d.ts +1 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +4 -2
- package/dist/components/ui/organization-card.d.ts.map +1 -1
- package/dist/index.cjs +15 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +16 -4
- package/package.json +1 -1
- package/src/components/features/index.ts +15 -1
- package/src/components/layout/title-block.tsx +6 -30
- package/src/components/providers/theme-provider.tsx +130 -0
- package/src/components/ui/button/button.tsx +1 -1
- package/src/components/ui/checkbox-block.tsx +13 -13
- package/src/components/ui/entity-image.tsx +56 -0
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/organization-card.tsx +4 -8
- package/src/stories/CheckboxBlock.stories.tsx +1 -3
- package/src/stories/OrganizationCard.stories.tsx +14 -0
- package/src/stories/Theme.stories.tsx +350 -0
- package/src/styles/README.md +271 -174
- package/src/styles/dark_theme.tokens.json +982 -0
- package/src/styles/light_theme.tokens.json +982 -0
- package/src/styles/ods-colors.css +225 -146
- package/dist/chunk-C3M6R6JH.cjs.map +0 -1
- package/dist/chunk-V2FNIPZJ.cjs.map +0 -1
- package/dist/chunk-ZOM75JOY.js.map +0 -1
- package/dist/components/features/organization-icon.d.ts +0 -80
- package/dist/components/features/organization-icon.d.ts.map +0 -1
- package/src/components/features/organization-icon.tsx +0 -175
- 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-
|
|
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-
|
|
34
|
-
"
|
|
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:
|
|
37
|
-
disabled && "opacity-50 cursor-not-allowed hover:
|
|
38
|
-
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
|
|
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 {
|
|
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
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
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">
|
|
@@ -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 & 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 & 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"><html></code> flips every{' '}
|
|
261
|
+
<code className="text-ods-accent">--ods-*</code> primitive. Components below don't
|
|
262
|
+
know — and don'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
|
+
}
|