@titan-design/react-ui 0.1.1 → 0.2.6

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 (67) hide show
  1. package/README.md +47 -1
  2. package/dist/{chunk-MGW37MPO.mjs → chunk-UXVRPOME.mjs} +173 -3
  3. package/dist/chunk-UXVRPOME.mjs.map +1 -0
  4. package/dist/index.d.mts +101 -12
  5. package/dist/index.d.ts +101 -12
  6. package/dist/index.js +1267 -468
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +734 -112
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/theme/index.d.mts +86 -1
  11. package/dist/theme/index.d.ts +86 -1
  12. package/dist/theme/index.js +166 -0
  13. package/dist/theme/index.js.map +1 -1
  14. package/dist/theme/index.mjs +1 -1
  15. package/docs/WEB_SETUP.md +217 -0
  16. package/package.json +14 -2
  17. package/src/components/custom/Workout/RestTimer.stories.tsx +71 -0
  18. package/src/components/custom/Workout/RestTimer.test.tsx +145 -0
  19. package/src/components/custom/Workout/RestTimer.tsx +172 -0
  20. package/src/components/custom/Workout/SupersetWrapper.stories.tsx +144 -0
  21. package/src/components/custom/Workout/SupersetWrapper.test.tsx +130 -0
  22. package/src/components/custom/Workout/SupersetWrapper.tsx +63 -0
  23. package/src/components/custom/Workout/index.ts +2 -0
  24. package/src/components/custom/index.ts +1 -0
  25. package/src/components/custom/stepper/index.ts +2 -2
  26. package/src/components/ui/avatar/Avatar.test.tsx +17 -0
  27. package/src/components/ui/avatar/Avatar.tsx +13 -5
  28. package/src/components/ui/badge/Badge.test.tsx +17 -0
  29. package/src/components/ui/badge/Badge.tsx +14 -0
  30. package/src/components/ui/badge/index.ts +1 -0
  31. package/src/components/ui/button/Button.tsx +55 -0
  32. package/src/components/ui/card/Card.test.tsx +29 -0
  33. package/src/components/ui/card/Card.tsx +32 -11
  34. package/src/components/ui/index.ts +2 -0
  35. package/src/components/ui/indicator/Indicator.stories.tsx +74 -0
  36. package/src/components/ui/indicator/Indicator.test.tsx +55 -0
  37. package/src/components/ui/indicator/Indicator.tsx +73 -0
  38. package/src/components/ui/indicator/index.ts +2 -0
  39. package/src/components/ui/pill/Pill.stories.tsx +71 -0
  40. package/src/components/ui/pill/Pill.test.tsx +69 -0
  41. package/src/components/ui/pill/Pill.tsx +104 -0
  42. package/src/components/ui/pill/index.ts +2 -0
  43. package/src/components/ui/popover/Popover.test.tsx +144 -2
  44. package/src/components/ui/popover/Popover.tsx +81 -6
  45. package/src/components/ui/progress/Progress.test.tsx +29 -0
  46. package/src/components/ui/progress/Progress.tsx +54 -35
  47. package/src/components/ui/select/Select.test.tsx +20 -0
  48. package/src/components/ui/select/Select.tsx +9 -2
  49. package/src/components/ui/toast/index.ts +1 -0
  50. package/src/components/ui/tooltip/Tooltip.test.tsx +83 -0
  51. package/src/components/ui/tooltip/Tooltip.tsx +117 -26
  52. package/src/stories/PresetShowcase.stories.tsx +462 -0
  53. package/src/stories/ThemePresets.stories.tsx +85 -0
  54. package/src/theme/global.css +129 -1
  55. package/src/theme/index.ts +1 -0
  56. package/src/theme/presets/apply.ts +97 -0
  57. package/src/theme/presets/audiobook.ts +93 -0
  58. package/src/theme/presets/default.ts +6 -0
  59. package/src/theme/presets/index.ts +4 -0
  60. package/src/theme/presets/presets.test.ts +51 -0
  61. package/src/theme/presets/types.ts +76 -0
  62. package/src/utils/avatar-color.test.ts +30 -0
  63. package/src/utils/avatar-color.ts +20 -0
  64. package/src/web-jsx/jsx-dev-runtime.ts +28 -0
  65. package/src/web-jsx/jsx-runtime.ts +41 -0
  66. package/tailwind.config.js +53 -3
  67. package/dist/chunk-MGW37MPO.mjs.map +0 -1
@@ -1,7 +1,19 @@
1
- import React, { useState, useRef } from 'react'
2
- import { View, Text, Pressable, Modal, type ViewProps } from 'react-native'
1
+ import React, { useState, useRef, useEffect } from 'react'
2
+ import { View, Text, Pressable, Platform, type ViewProps } from 'react-native'
3
3
  import { cn } from '../../../utils/cn'
4
4
 
5
+ let createPortal: typeof import('react-dom').createPortal | undefined
6
+ if (Platform.OS === 'web') {
7
+ try {
8
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
9
+ createPortal = require('react-dom').createPortal
10
+ } catch {
11
+ // react-dom unavailable
12
+ }
13
+ }
14
+
15
+ type PortalPosition = { top: number; left: number; transform: string }
16
+
5
17
  export type TooltipPlacement =
6
18
  | 'top'
7
19
  | 'top-start'
@@ -13,8 +25,10 @@ export type TooltipPlacement =
13
25
  | 'right'
14
26
 
15
27
  export interface TooltipProps extends ViewProps {
16
- /** Tooltip content */
17
- label: string
28
+ /** Plain-text tooltip content */
29
+ label?: string
30
+ /** Rich tooltip content (takes precedence over label) */
31
+ content?: React.ReactNode
18
32
  /** Trigger element */
19
33
  children: React.ReactNode
20
34
  /** Tooltip placement */
@@ -29,6 +43,8 @@ export interface TooltipProps extends ViewProps {
29
43
  isDisabled?: boolean
30
44
  /** Additional className for tooltip */
31
45
  className?: string
46
+ /** Render tooltip via portal to escape overflow:hidden ancestors (web only) */
47
+ usePortal?: boolean
32
48
  }
33
49
 
34
50
  /**
@@ -37,12 +53,19 @@ export interface TooltipProps extends ViewProps {
37
53
  * Note: On native, tooltips appear on long press. On web, they appear on hover.
38
54
  *
39
55
  * @example
56
+ * // String-only tooltip
40
57
  * <Tooltip label="This is a tooltip">
41
58
  * <Button><ButtonText>Hover me</ButtonText></Button>
42
59
  * </Tooltip>
60
+ *
61
+ * // Rich content tooltip
62
+ * <Tooltip content={<View><Text>Bold tip</Text><Text>with details</Text></View>}>
63
+ * <Button><ButtonText>Hover me</ButtonText></Button>
64
+ * </Tooltip>
43
65
  */
44
66
  export function Tooltip({
45
67
  label,
68
+ content,
46
69
  children,
47
70
  placement = 'top',
48
71
  openDelay = 0,
@@ -50,11 +73,52 @@ export function Tooltip({
50
73
  hasArrow = true,
51
74
  isDisabled = false,
52
75
  className,
76
+ usePortal: usePortalProp = false,
53
77
  ...props
54
78
  }: TooltipProps) {
55
79
  const [isVisible, setIsVisible] = useState(false)
80
+ const [portalPos, setPortalPos] = useState<PortalPosition | null>(null)
56
81
  const openTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
57
82
  const closeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
83
+ const triggerRef = useRef<View>(null)
84
+
85
+ const isPortalMode = usePortalProp && Platform.OS === 'web' && !!createPortal
86
+
87
+ useEffect(() => {
88
+ if (!isVisible || !isPortalMode || !triggerRef.current) return
89
+ const node = triggerRef.current as unknown as HTMLElement
90
+ const id = requestAnimationFrame(() => {
91
+ const rect = node.getBoundingClientRect()
92
+ const gap = 8
93
+ const cardinal = placement.split('-')[0] as 'top' | 'bottom' | 'left' | 'right'
94
+
95
+ const positions: Record<string, PortalPosition> = {
96
+ top: {
97
+ top: rect.top - gap,
98
+ left: rect.left + rect.width / 2,
99
+ transform: 'translate(-50%, -100%)',
100
+ },
101
+ bottom: {
102
+ top: rect.bottom + gap,
103
+ left: rect.left + rect.width / 2,
104
+ transform: 'translate(-50%, 0%)',
105
+ },
106
+ left: {
107
+ top: rect.top + rect.height / 2,
108
+ left: rect.left - gap,
109
+ transform: 'translate(-100%, -50%)',
110
+ },
111
+ right: {
112
+ top: rect.top + rect.height / 2,
113
+ left: rect.right + gap,
114
+ transform: 'translate(0%, -50%)',
115
+ },
116
+ }
117
+
118
+ setPortalPos(positions[cardinal] ?? positions.top)
119
+ })
120
+ return () => cancelAnimationFrame(id)
121
+ }, [isVisible, isPortalMode, placement])
58
122
 
59
123
  const clearTimeouts = () => {
60
124
  if (openTimeoutRef.current) clearTimeout(openTimeoutRef.current)
@@ -104,8 +168,43 @@ export function Tooltip({
104
168
  right: 'left-full top-1/2 -translate-y-1/2 ml-2',
105
169
  }
106
170
 
171
+ const tooltipContent = (
172
+ <View className="bg-neutral-800 px-3 py-2 rounded-md shadow-lg max-w-xs">
173
+ {content ?? <Text className="text-white text-sm">{label}</Text>}
174
+ {hasArrow && (
175
+ <View
176
+ className={cn(
177
+ 'absolute w-0 h-0 border-4',
178
+ arrowStyles[placement]
179
+ )}
180
+ />
181
+ )}
182
+ </View>
183
+ )
184
+
185
+ const renderPortalTooltip = () => {
186
+ if (!isVisible || !isPortalMode || !createPortal) return null
187
+ return createPortal(
188
+ <div
189
+ style={{
190
+ position: 'fixed',
191
+ top: portalPos?.top ?? 0,
192
+ left: portalPos?.left ?? 0,
193
+ transform: portalPos?.transform ?? 'translate(-50%, -100%)',
194
+ zIndex: 10000,
195
+ pointerEvents: 'none',
196
+ }}
197
+ data-testid="tooltip-portal"
198
+ className={className}
199
+ >
200
+ {tooltipContent}
201
+ </div>,
202
+ document.body
203
+ )
204
+ }
205
+
107
206
  return (
108
- <View className="relative" {...props}>
207
+ <View className="relative" ref={triggerRef} {...props}>
109
208
  <Pressable
110
209
  onHoverIn={show}
111
210
  onHoverOut={hide}
@@ -116,28 +215,20 @@ export function Tooltip({
116
215
  {children}
117
216
  </Pressable>
118
217
 
119
- {isVisible && (
120
- <View
121
- className={cn(
122
- 'absolute z-50',
123
- tooltipPositionStyles[placement],
124
- className
218
+ {isPortalMode
219
+ ? renderPortalTooltip()
220
+ : isVisible && (
221
+ <View
222
+ className={cn(
223
+ 'absolute z-50 web:animate-fade-in',
224
+ tooltipPositionStyles[placement],
225
+ className
226
+ )}
227
+ pointerEvents="none"
228
+ >
229
+ {tooltipContent}
230
+ </View>
125
231
  )}
126
- pointerEvents="none"
127
- >
128
- <View className="bg-neutral-800 px-3 py-2 rounded-md shadow-lg max-w-xs">
129
- <Text className="text-white text-sm">{label}</Text>
130
- {hasArrow && (
131
- <View
132
- className={cn(
133
- 'absolute w-0 h-0 border-4',
134
- arrowStyles[placement]
135
- )}
136
- />
137
- )}
138
- </View>
139
- </View>
140
- )}
141
232
  </View>
142
233
  )
143
234
  }
@@ -0,0 +1,462 @@
1
+ import React, { useEffect, useState } from 'react'
2
+ import type { Meta, StoryObj } from '@storybook/react-vite'
3
+ import { View, Text, Pressable } from 'react-native'
4
+ import { Button, ButtonText } from '../components/ui/button'
5
+ import { Card } from '../components/ui/card'
6
+ import { Input } from '../components/ui/input'
7
+ import { Progress, CircularProgress } from '../components/ui/progress'
8
+ import { Badge } from '../components/ui/badge'
9
+ import { Toast } from '../components/ui/toast'
10
+ import { Switch } from '../components/ui/switch'
11
+ import { applyThemePreset, audiobookPreset, defaultPreset } from '../theme/presets'
12
+ import type { ThemePreset } from '../theme/presets'
13
+
14
+ const presets: Record<string, ThemePreset> = {
15
+ default: defaultPreset,
16
+ audiobook: audiobookPreset,
17
+ }
18
+
19
+ function PresetSwitcher({
20
+ activePreset,
21
+ onSwitch,
22
+ }: {
23
+ activePreset: string
24
+ onSwitch: (name: string) => void
25
+ }) {
26
+ return (
27
+ <View className="flex-row items-center gap-3 p-4 bg-surface-elevated rounded-lg border border-border-default">
28
+ <Text className="text-text-secondary text-sm font-medium">Active Preset:</Text>
29
+ {Object.keys(presets).map((name) => (
30
+ <Pressable
31
+ key={name}
32
+ onPress={() => onSwitch(name)}
33
+ className={`px-4 py-2 rounded-md border ${
34
+ activePreset === name
35
+ ? 'bg-brand-primary border-brand-primary'
36
+ : 'bg-surface-base border-border-default web:hover:bg-interactive-hover'
37
+ }`}
38
+ >
39
+ <Text
40
+ className={`text-sm font-semibold capitalize ${
41
+ activePreset === name ? 'text-on-brand-primary' : 'text-text-primary'
42
+ }`}
43
+ >
44
+ {name}
45
+ </Text>
46
+ </Pressable>
47
+ ))}
48
+ </View>
49
+ )
50
+ }
51
+
52
+ function usePresetToggle(initial = 'default') {
53
+ const [activePreset, setActivePreset] = useState(initial)
54
+
55
+ useEffect(() => {
56
+ const cleanup = applyThemePreset(presets[activePreset])
57
+ return cleanup
58
+ }, [activePreset])
59
+
60
+ return { activePreset, setActivePreset }
61
+ }
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // SideBySide Story
65
+ // ---------------------------------------------------------------------------
66
+
67
+ function SideBySideShowcase() {
68
+ const { activePreset, setActivePreset } = usePresetToggle()
69
+
70
+ return (
71
+ <View className="gap-6 p-6 bg-background-default min-h-screen">
72
+ <View className="gap-2">
73
+ <Text className="text-3xl font-bold text-text-primary font-heading">
74
+ Preset Showcase
75
+ </Text>
76
+ <Text className="text-text-secondary text-base">
77
+ Toggle between presets to see how the entire design system adapts.
78
+ </Text>
79
+ </View>
80
+
81
+ <PresetSwitcher activePreset={activePreset} onSwitch={setActivePreset} />
82
+
83
+ {/* Typography */}
84
+ <Section title="Typography">
85
+ <Text className="text-3xl font-bold text-text-primary font-heading">
86
+ Heading (font-heading)
87
+ </Text>
88
+ <Text className="text-xl font-semibold text-text-primary font-heading">
89
+ Subheading (font-heading)
90
+ </Text>
91
+ <Text className="text-base text-text-primary font-body">
92
+ Body text using font-body. The quick brown fox jumps over the lazy dog. This
93
+ demonstrates how paragraph text renders under the current preset.
94
+ </Text>
95
+ <Text className="text-sm text-text-secondary font-body">
96
+ Secondary body text for captions and supporting content.
97
+ </Text>
98
+ <Text className="text-sm text-text-primary font-mono">
99
+ {'const monospace = \'font-mono rendering\''}
100
+ </Text>
101
+ </Section>
102
+
103
+ {/* Buttons */}
104
+ <Section title="Buttons">
105
+ <View className="gap-3">
106
+ <Text className="text-xs font-semibold text-text-tertiary uppercase tracking-wider">
107
+ Primary
108
+ </Text>
109
+ <View className="flex-row gap-3 flex-wrap">
110
+ <Button variant="solid" color="primary">
111
+ <ButtonText>Solid</ButtonText>
112
+ </Button>
113
+ <Button variant="outline" color="primary">
114
+ <ButtonText>Outline</ButtonText>
115
+ </Button>
116
+ <Button variant="ghost" color="primary">
117
+ <ButtonText>Ghost</ButtonText>
118
+ </Button>
119
+ </View>
120
+ <Text className="text-xs font-semibold text-text-tertiary uppercase tracking-wider">
121
+ Secondary
122
+ </Text>
123
+ <View className="flex-row gap-3 flex-wrap">
124
+ <Button variant="solid" color="secondary">
125
+ <ButtonText>Solid</ButtonText>
126
+ </Button>
127
+ <Button variant="outline" color="secondary">
128
+ <ButtonText>Outline</ButtonText>
129
+ </Button>
130
+ <Button variant="ghost" color="secondary">
131
+ <ButtonText>Ghost</ButtonText>
132
+ </Button>
133
+ </View>
134
+ </View>
135
+ </Section>
136
+
137
+ {/* Cards with Badges */}
138
+ <Section title="Cards">
139
+ <View className="flex-row gap-4 flex-wrap">
140
+ <Card className="p-4 gap-3 flex-1 min-w-[260px]">
141
+ <Text className="text-lg font-semibold text-text-primary font-heading">
142
+ Elevated Card
143
+ </Text>
144
+ <Text className="text-text-secondary text-sm">
145
+ Surface elevation and border colors adapt to the active preset.
146
+ </Text>
147
+ <View className="flex-row gap-2 flex-wrap">
148
+ <Badge color="success">Complete</Badge>
149
+ <Badge color="warning">Pending</Badge>
150
+ <Badge color="error">Failed</Badge>
151
+ </View>
152
+ </Card>
153
+ <Card className="p-4 gap-3 flex-1 min-w-[260px]">
154
+ <Text className="text-lg font-semibold text-text-primary font-heading">
155
+ Content Card
156
+ </Text>
157
+ <Text className="text-text-secondary text-sm">
158
+ Brand secondary and info badges shown below for comparison.
159
+ </Text>
160
+ <View className="flex-row gap-2 flex-wrap">
161
+ <Badge color="info">Info</Badge>
162
+ <Badge color="secondary">Secondary</Badge>
163
+ </View>
164
+ </Card>
165
+ </View>
166
+ </Section>
167
+
168
+ {/* Input + Switch */}
169
+ <Section title="Inputs">
170
+ <View className="gap-3 max-w-[400px]">
171
+ <Input placeholder="Default input..." />
172
+ <Input placeholder="Disabled input..." isDisabled />
173
+ <View className="flex-row items-center gap-3">
174
+ <Switch />
175
+ <Text className="text-text-primary text-sm">Toggle switch</Text>
176
+ </View>
177
+ </View>
178
+ </Section>
179
+
180
+ {/* Progress */}
181
+ <Section title="Progress">
182
+ <View className="gap-4">
183
+ <Progress value={65} label="Primary" showValue color="primary" />
184
+ <Progress value={40} label="Secondary" showValue color="secondary" />
185
+ <Progress value={100} label="Success" showValue color="success" />
186
+ <View className="flex-row gap-4 items-center flex-wrap">
187
+ <CircularProgress value={25} size={48} showValue color="primary" />
188
+ <CircularProgress value={50} size={48} showValue color="secondary" />
189
+ <CircularProgress value={75} size={48} showValue color="success" />
190
+ <CircularProgress value={100} size={48} showValue color="error" />
191
+ </View>
192
+ </View>
193
+ </Section>
194
+
195
+ {/* Toasts */}
196
+ <Section title="Toasts">
197
+ <View className="gap-3">
198
+ <Toast title="Operation successful" description="Your changes have been saved." status="success" />
199
+ <Toast title="Something went wrong" description="Please try again later." status="error" />
200
+ <Toast title="Heads up" description="This action cannot be undone." status="warning" />
201
+ <Toast title="New update available" description="Version 2.0 is ready to install." status="info" />
202
+ </View>
203
+ </Section>
204
+
205
+ {/* Status Colors */}
206
+ <Section title="Status Colors">
207
+ <View className="flex-row gap-3 flex-wrap">
208
+ <Badge color="success" size="lg">Success</Badge>
209
+ <Badge color="error" size="lg">Error</Badge>
210
+ <Badge color="warning" size="lg">Warning</Badge>
211
+ <Badge color="info" size="lg">Info</Badge>
212
+ </View>
213
+ </Section>
214
+ </View>
215
+ )
216
+ }
217
+
218
+ // ---------------------------------------------------------------------------
219
+ // Typography Story
220
+ // ---------------------------------------------------------------------------
221
+
222
+ function TypographyShowcase() {
223
+ const { activePreset, setActivePreset } = usePresetToggle()
224
+
225
+ return (
226
+ <View className="gap-6 p-6 bg-background-default min-h-screen">
227
+ <View className="gap-2">
228
+ <Text className="text-3xl font-bold text-text-primary font-heading">
229
+ Typography
230
+ </Text>
231
+ <Text className="text-text-secondary text-base">
232
+ How fonts and text styles change across presets.
233
+ </Text>
234
+ </View>
235
+
236
+ <PresetSwitcher activePreset={activePreset} onSwitch={setActivePreset} />
237
+
238
+ <Section title="Heading Font (font-heading)">
239
+ <Text className="text-4xl font-bold text-text-primary font-heading">
240
+ H1 - The Art of Design Systems
241
+ </Text>
242
+ <Text className="text-3xl font-bold text-text-primary font-heading">
243
+ H2 - Component Architecture
244
+ </Text>
245
+ <Text className="text-2xl font-semibold text-text-primary font-heading">
246
+ H3 - Token-Driven Theming
247
+ </Text>
248
+ <Text className="text-xl font-semibold text-text-primary font-heading">
249
+ H4 - Cross-Platform Consistency
250
+ </Text>
251
+ </Section>
252
+
253
+ <Section title="Body Font (font-body)">
254
+ <Text className="text-base text-text-primary font-body">
255
+ Regular body text. Design systems provide a shared language for product teams.
256
+ By codifying decisions into reusable components and tokens, teams move faster
257
+ while maintaining visual consistency across every surface.
258
+ </Text>
259
+ <Text className="text-base font-semibold text-text-primary font-body">
260
+ Bold body text. Emphasis and hierarchy within paragraph content.
261
+ </Text>
262
+ <Text className="text-base italic text-text-primary font-body">
263
+ Italic body text. Used for quotations, citations, and emphasis.
264
+ </Text>
265
+ <Text className="text-sm text-text-secondary font-body">
266
+ Small / secondary text. Captions, labels, and supporting information
267
+ that supplements the main content without competing for attention.
268
+ </Text>
269
+ <Text className="text-xs text-text-tertiary font-body">
270
+ Extra-small / tertiary text. Timestamps, metadata, fine print.
271
+ </Text>
272
+ </Section>
273
+
274
+ <Section title="Monospace Font (font-mono)">
275
+ <View className="bg-surface-elevated rounded-lg p-4 border border-border-default">
276
+ <Text className="text-sm text-text-primary font-mono">
277
+ {'const theme = applyThemePreset(audiobookPreset)'}
278
+ </Text>
279
+ <Text className="text-sm text-text-secondary font-mono">
280
+ {'// Overrides CSS custom properties on :root'}
281
+ </Text>
282
+ <Text className="text-sm text-text-primary font-mono">
283
+ {'export type ThemePreset = { name: string }'}
284
+ </Text>
285
+ </View>
286
+ </Section>
287
+
288
+ <Section title="Mixed Typography">
289
+ <Card className="p-5 gap-3">
290
+ <Text className="text-2xl font-bold text-text-primary font-heading">
291
+ Chapter One
292
+ </Text>
293
+ <Text className="text-base text-text-secondary font-body">
294
+ It was a bright cold day in April, and the clocks were striking thirteen.
295
+ Winston Smith, his chin nuzzled into his breast in an effort to escape the
296
+ vile wind, slipped quickly through the glass doors of Victory Mansions,
297
+ though not quickly enough to prevent a swirl of gritty dust from entering
298
+ along with him.
299
+ </Text>
300
+ <Text className="text-xs text-text-tertiary font-mono">
301
+ George Orwell, 1984
302
+ </Text>
303
+ </Card>
304
+ </Section>
305
+ </View>
306
+ )
307
+ }
308
+
309
+ // ---------------------------------------------------------------------------
310
+ // ColorPalette Story
311
+ // ---------------------------------------------------------------------------
312
+
313
+ function ColorSwatch({
314
+ label,
315
+ className,
316
+ textClass = 'text-white',
317
+ }: {
318
+ label: string
319
+ className: string
320
+ textClass?: string
321
+ }) {
322
+ return (
323
+ <View className={`rounded-lg p-3 min-w-[120px] min-h-[60px] justify-end ${className}`}>
324
+ <Text className={`text-xs font-mono font-medium ${textClass}`}>{label}</Text>
325
+ </View>
326
+ )
327
+ }
328
+
329
+ function ColorPaletteShowcase() {
330
+ const { activePreset, setActivePreset } = usePresetToggle()
331
+
332
+ return (
333
+ <View className="gap-6 p-6 bg-background-default min-h-screen">
334
+ <View className="gap-2">
335
+ <Text className="text-3xl font-bold text-text-primary font-heading">
336
+ Color Palette
337
+ </Text>
338
+ <Text className="text-text-secondary text-base">
339
+ Semantic color tokens rendered as swatches. Toggle presets to compare palettes.
340
+ </Text>
341
+ </View>
342
+
343
+ <PresetSwitcher activePreset={activePreset} onSwitch={setActivePreset} />
344
+
345
+ <Section title="Brand Colors">
346
+ <View className="flex-row gap-3 flex-wrap">
347
+ <ColorSwatch label="brand-primary" className="bg-brand-primary" />
348
+ <ColorSwatch label="brand-primary-light" className="bg-brand-primary-light" />
349
+ <ColorSwatch label="brand-primary-dark" className="bg-brand-primary-dark" />
350
+ <ColorSwatch label="brand-primary-subtle" className="bg-brand-primary-subtle" textClass="text-text-primary" />
351
+ </View>
352
+ <View className="flex-row gap-3 flex-wrap mt-2">
353
+ <ColorSwatch label="brand-secondary" className="bg-brand-secondary" />
354
+ <ColorSwatch label="brand-secondary-light" className="bg-brand-secondary-light" />
355
+ <ColorSwatch label="brand-secondary-dark" className="bg-brand-secondary-dark" />
356
+ <ColorSwatch label="brand-secondary-subtle" className="bg-brand-secondary-subtle" textClass="text-text-primary" />
357
+ </View>
358
+ </Section>
359
+
360
+ <Section title="Status Colors">
361
+ <View className="flex-row gap-3 flex-wrap">
362
+ <ColorSwatch label="status-success" className="bg-status-success" />
363
+ <ColorSwatch label="status-error" className="bg-status-error" />
364
+ <ColorSwatch label="status-warning" className="bg-status-warning" />
365
+ <ColorSwatch label="status-info" className="bg-status-info" />
366
+ </View>
367
+ </Section>
368
+
369
+ <Section title="Text Colors">
370
+ <View className="flex-row gap-3 flex-wrap">
371
+ <View className="rounded-lg p-3 min-w-[120px] min-h-[60px] justify-end bg-surface-elevated border border-border-default">
372
+ <Text className="text-xs font-mono font-medium text-text-primary">text-primary</Text>
373
+ </View>
374
+ <View className="rounded-lg p-3 min-w-[120px] min-h-[60px] justify-end bg-surface-elevated border border-border-default">
375
+ <Text className="text-xs font-mono font-medium text-text-secondary">text-secondary</Text>
376
+ </View>
377
+ <View className="rounded-lg p-3 min-w-[120px] min-h-[60px] justify-end bg-surface-elevated border border-border-default">
378
+ <Text className="text-xs font-mono font-medium text-text-tertiary">text-tertiary</Text>
379
+ </View>
380
+ </View>
381
+ </Section>
382
+
383
+ <Section title="Surface Colors">
384
+ <View className="flex-row gap-3 flex-wrap">
385
+ <ColorSwatch label="surface-base" className="bg-surface-base border border-border-default" textClass="text-text-primary" />
386
+ <ColorSwatch label="surface-elevated" className="bg-surface-elevated border border-border-default" textClass="text-text-primary" />
387
+ <ColorSwatch label="surface-raised" className="bg-surface-raised border border-border-default" textClass="text-text-primary" />
388
+ <ColorSwatch label="surface-overlay" className="bg-surface-overlay border border-border-default" textClass="text-text-primary" />
389
+ <ColorSwatch label="surface-input" className="bg-surface-input border border-border-default" textClass="text-text-primary" />
390
+ </View>
391
+ </Section>
392
+
393
+ <Section title="Background Colors">
394
+ <View className="flex-row gap-3 flex-wrap">
395
+ <ColorSwatch label="background-base" className="bg-background-base border border-border-default" textClass="text-text-primary" />
396
+ <ColorSwatch label="background-default" className="bg-background-default border border-border-default" textClass="text-text-primary" />
397
+ <ColorSwatch label="background-subtle" className="bg-background-subtle border border-border-default" textClass="text-text-primary" />
398
+ </View>
399
+ </Section>
400
+
401
+ <Section title="Border Colors">
402
+ <View className="flex-row gap-3 flex-wrap">
403
+ <View className="rounded-lg p-3 min-w-[120px] min-h-[60px] justify-end bg-surface-base border-2 border-border-default">
404
+ <Text className="text-xs font-mono font-medium text-text-primary">border-default</Text>
405
+ </View>
406
+ <View className="rounded-lg p-3 min-w-[120px] min-h-[60px] justify-end bg-surface-base border-2 border-border-subtle">
407
+ <Text className="text-xs font-mono font-medium text-text-primary">border-subtle</Text>
408
+ </View>
409
+ <View className="rounded-lg p-3 min-w-[120px] min-h-[60px] justify-end bg-surface-base border-2 border-border-strong">
410
+ <Text className="text-xs font-mono font-medium text-text-primary">border-strong</Text>
411
+ </View>
412
+ <View className="rounded-lg p-3 min-w-[120px] min-h-[60px] justify-end bg-surface-base border-2 border-border-focus">
413
+ <Text className="text-xs font-mono font-medium text-text-primary">border-focus</Text>
414
+ </View>
415
+ </View>
416
+ </Section>
417
+ </View>
418
+ )
419
+ }
420
+
421
+ // ---------------------------------------------------------------------------
422
+ // Shared Section Component
423
+ // ---------------------------------------------------------------------------
424
+
425
+ function Section({ title, children }: { title: string; children: React.ReactNode }) {
426
+ return (
427
+ <View className="gap-3">
428
+ <Text className="text-lg font-semibold text-text-primary font-heading border-b border-border-subtle pb-2">
429
+ {title}
430
+ </Text>
431
+ {children}
432
+ </View>
433
+ )
434
+ }
435
+
436
+ // ---------------------------------------------------------------------------
437
+ // Storybook Meta & Exports
438
+ // ---------------------------------------------------------------------------
439
+
440
+ const meta: Meta = {
441
+ title: 'Theme/Preset Showcase',
442
+ parameters: {
443
+ layout: 'fullscreen',
444
+ },
445
+ }
446
+
447
+ export default meta
448
+
449
+ export const SideBySide: StoryObj = {
450
+ render: () => <SideBySideShowcase />,
451
+ name: 'Component Showcase',
452
+ }
453
+
454
+ export const Typography: StoryObj = {
455
+ render: () => <TypographyShowcase />,
456
+ name: 'Typography',
457
+ }
458
+
459
+ export const ColorPalette: StoryObj = {
460
+ render: () => <ColorPaletteShowcase />,
461
+ name: 'Color Palette',
462
+ }