@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.
- package/README.md +47 -1
- package/dist/{chunk-MGW37MPO.mjs → chunk-UXVRPOME.mjs} +173 -3
- package/dist/chunk-UXVRPOME.mjs.map +1 -0
- package/dist/index.d.mts +101 -12
- package/dist/index.d.ts +101 -12
- package/dist/index.js +1267 -468
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +734 -112
- package/dist/index.mjs.map +1 -1
- package/dist/theme/index.d.mts +86 -1
- package/dist/theme/index.d.ts +86 -1
- package/dist/theme/index.js +166 -0
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/index.mjs +1 -1
- package/docs/WEB_SETUP.md +217 -0
- package/package.json +14 -2
- package/src/components/custom/Workout/RestTimer.stories.tsx +71 -0
- package/src/components/custom/Workout/RestTimer.test.tsx +145 -0
- package/src/components/custom/Workout/RestTimer.tsx +172 -0
- package/src/components/custom/Workout/SupersetWrapper.stories.tsx +144 -0
- package/src/components/custom/Workout/SupersetWrapper.test.tsx +130 -0
- package/src/components/custom/Workout/SupersetWrapper.tsx +63 -0
- package/src/components/custom/Workout/index.ts +2 -0
- package/src/components/custom/index.ts +1 -0
- package/src/components/custom/stepper/index.ts +2 -2
- package/src/components/ui/avatar/Avatar.test.tsx +17 -0
- package/src/components/ui/avatar/Avatar.tsx +13 -5
- package/src/components/ui/badge/Badge.test.tsx +17 -0
- package/src/components/ui/badge/Badge.tsx +14 -0
- package/src/components/ui/badge/index.ts +1 -0
- package/src/components/ui/button/Button.tsx +55 -0
- package/src/components/ui/card/Card.test.tsx +29 -0
- package/src/components/ui/card/Card.tsx +32 -11
- package/src/components/ui/index.ts +2 -0
- package/src/components/ui/indicator/Indicator.stories.tsx +74 -0
- package/src/components/ui/indicator/Indicator.test.tsx +55 -0
- package/src/components/ui/indicator/Indicator.tsx +73 -0
- package/src/components/ui/indicator/index.ts +2 -0
- package/src/components/ui/pill/Pill.stories.tsx +71 -0
- package/src/components/ui/pill/Pill.test.tsx +69 -0
- package/src/components/ui/pill/Pill.tsx +104 -0
- package/src/components/ui/pill/index.ts +2 -0
- package/src/components/ui/popover/Popover.test.tsx +144 -2
- package/src/components/ui/popover/Popover.tsx +81 -6
- package/src/components/ui/progress/Progress.test.tsx +29 -0
- package/src/components/ui/progress/Progress.tsx +54 -35
- package/src/components/ui/select/Select.test.tsx +20 -0
- package/src/components/ui/select/Select.tsx +9 -2
- package/src/components/ui/toast/index.ts +1 -0
- package/src/components/ui/tooltip/Tooltip.test.tsx +83 -0
- package/src/components/ui/tooltip/Tooltip.tsx +117 -26
- package/src/stories/PresetShowcase.stories.tsx +462 -0
- package/src/stories/ThemePresets.stories.tsx +85 -0
- package/src/theme/global.css +129 -1
- package/src/theme/index.ts +1 -0
- package/src/theme/presets/apply.ts +97 -0
- package/src/theme/presets/audiobook.ts +93 -0
- package/src/theme/presets/default.ts +6 -0
- package/src/theme/presets/index.ts +4 -0
- package/src/theme/presets/presets.test.ts +51 -0
- package/src/theme/presets/types.ts +76 -0
- package/src/utils/avatar-color.test.ts +30 -0
- package/src/utils/avatar-color.ts +20 -0
- package/src/web-jsx/jsx-dev-runtime.ts +28 -0
- package/src/web-jsx/jsx-runtime.ts +41 -0
- package/tailwind.config.js +53 -3
- package/dist/chunk-MGW37MPO.mjs.map +0 -1
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import React, { useEffect } from 'react'
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
3
|
+
import { View, Text } 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 { applyThemePreset, audiobookPreset, defaultPreset } from '../theme/presets'
|
|
10
|
+
|
|
11
|
+
function PresetShowcase({ preset }: { preset: typeof audiobookPreset }) {
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const cleanup = applyThemePreset(preset)
|
|
14
|
+
return cleanup
|
|
15
|
+
}, [preset])
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<View className="gap-6 p-6 bg-background-default min-h-screen">
|
|
19
|
+
<Text className="text-2xl font-bold text-text-primary font-heading">
|
|
20
|
+
{preset.name} preset
|
|
21
|
+
</Text>
|
|
22
|
+
|
|
23
|
+
{/* Buttons */}
|
|
24
|
+
<View className="flex-row gap-3 flex-wrap">
|
|
25
|
+
<Button variant="solid" color="primary">
|
|
26
|
+
<ButtonText>Primary</ButtonText>
|
|
27
|
+
</Button>
|
|
28
|
+
<Button variant="solid" color="secondary">
|
|
29
|
+
<ButtonText>Secondary</ButtonText>
|
|
30
|
+
</Button>
|
|
31
|
+
<Button variant="outline" color="primary">
|
|
32
|
+
<ButtonText>Outline</ButtonText>
|
|
33
|
+
</Button>
|
|
34
|
+
<Button variant="ghost" color="primary">
|
|
35
|
+
<ButtonText>Ghost</ButtonText>
|
|
36
|
+
</Button>
|
|
37
|
+
</View>
|
|
38
|
+
|
|
39
|
+
{/* Card */}
|
|
40
|
+
<Card className="p-4 gap-3">
|
|
41
|
+
<Text className="text-lg font-semibold text-text-primary">Sample Card</Text>
|
|
42
|
+
<Text className="text-text-secondary">
|
|
43
|
+
This card demonstrates surface elevation and border colors with the current preset.
|
|
44
|
+
</Text>
|
|
45
|
+
<View className="flex-row gap-2">
|
|
46
|
+
<Badge color="success">Complete</Badge>
|
|
47
|
+
<Badge color="warning">Pending</Badge>
|
|
48
|
+
<Badge color="error">Failed</Badge>
|
|
49
|
+
</View>
|
|
50
|
+
</Card>
|
|
51
|
+
|
|
52
|
+
{/* Input */}
|
|
53
|
+
<Input placeholder="Type something..." />
|
|
54
|
+
|
|
55
|
+
{/* Progress */}
|
|
56
|
+
<View className="gap-3">
|
|
57
|
+
<Progress value={65} label="Processing" showValue color="primary" />
|
|
58
|
+
<View className="flex-row gap-4 items-center">
|
|
59
|
+
<CircularProgress value={25} size={48} showValue color="primary" />
|
|
60
|
+
<CircularProgress value={75} size={48} showValue color="secondary" />
|
|
61
|
+
<CircularProgress value={100} size={48} showValue color="success" />
|
|
62
|
+
</View>
|
|
63
|
+
</View>
|
|
64
|
+
</View>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const meta: Meta = {
|
|
69
|
+
title: 'Theme/Presets',
|
|
70
|
+
parameters: {
|
|
71
|
+
layout: 'fullscreen',
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export default meta
|
|
76
|
+
|
|
77
|
+
export const Default: StoryObj = {
|
|
78
|
+
render: () => <PresetShowcase preset={defaultPreset} />,
|
|
79
|
+
name: 'Default Preset',
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const Audiobook: StoryObj = {
|
|
83
|
+
render: () => <PresetShowcase preset={audiobookPreset} />,
|
|
84
|
+
name: 'Audiobook Preset',
|
|
85
|
+
}
|
package/src/theme/global.css
CHANGED
|
@@ -116,6 +116,29 @@
|
|
|
116
116
|
|
|
117
117
|
--color-avatar-background: #4B5563;
|
|
118
118
|
--color-avatar-text: #FFFFFF;
|
|
119
|
+
|
|
120
|
+
/* RGB decomposed values for glow shadows */
|
|
121
|
+
--color-brand-primary-rgb: 255, 121, 0;
|
|
122
|
+
--color-brand-secondary-rgb: 64, 109, 135;
|
|
123
|
+
--color-status-success-rgb: 20, 184, 166;
|
|
124
|
+
--color-status-error-rgb: 209, 67, 67;
|
|
125
|
+
--color-status-warning-rgb: 255, 176, 32;
|
|
126
|
+
--color-status-info-rgb: 33, 150, 243;
|
|
127
|
+
--color-background-base-rgb: 16, 16, 16;
|
|
128
|
+
|
|
129
|
+
/* Font families */
|
|
130
|
+
--font-family-sans: 'Inter';
|
|
131
|
+
--font-family-body: 'Nunito Sans';
|
|
132
|
+
--font-family-heading: 'Space Grotesk';
|
|
133
|
+
--font-family-mono: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
134
|
+
|
|
135
|
+
/* Animation tokens */
|
|
136
|
+
--ease-out: cubic-bezier(0.22, 1, 0.36, 1);
|
|
137
|
+
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
|
|
138
|
+
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
139
|
+
--duration-fast: 150ms;
|
|
140
|
+
--duration-normal: 250ms;
|
|
141
|
+
--duration-slow: 400ms;
|
|
119
142
|
}
|
|
120
143
|
|
|
121
144
|
/* Light mode */
|
|
@@ -222,11 +245,34 @@
|
|
|
222
245
|
|
|
223
246
|
--color-avatar-background: #6B7280;
|
|
224
247
|
--color-avatar-text: #FFFFFF;
|
|
248
|
+
|
|
249
|
+
/* RGB decomposed values for glow shadows */
|
|
250
|
+
--color-brand-primary-rgb: 255, 121, 0;
|
|
251
|
+
--color-brand-secondary-rgb: 64, 109, 135;
|
|
252
|
+
--color-status-success-rgb: 20, 184, 166;
|
|
253
|
+
--color-status-error-rgb: 209, 67, 67;
|
|
254
|
+
--color-status-warning-rgb: 255, 176, 32;
|
|
255
|
+
--color-status-info-rgb: 33, 150, 243;
|
|
256
|
+
--color-background-base-rgb: 16, 16, 16;
|
|
257
|
+
|
|
258
|
+
/* Font families */
|
|
259
|
+
--font-family-sans: 'Inter';
|
|
260
|
+
--font-family-body: 'Nunito Sans';
|
|
261
|
+
--font-family-heading: 'Space Grotesk';
|
|
262
|
+
--font-family-mono: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
263
|
+
|
|
264
|
+
/* Animation tokens */
|
|
265
|
+
--ease-out: cubic-bezier(0.22, 1, 0.36, 1);
|
|
266
|
+
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
|
|
267
|
+
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
268
|
+
--duration-fast: 150ms;
|
|
269
|
+
--duration-normal: 250ms;
|
|
270
|
+
--duration-slow: 400ms;
|
|
225
271
|
}
|
|
226
272
|
|
|
227
273
|
/* Base styles */
|
|
228
274
|
html {
|
|
229
|
-
font-family:
|
|
275
|
+
font-family: var(--font-family-sans), -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
|
230
276
|
-webkit-font-smoothing: antialiased;
|
|
231
277
|
-moz-osx-font-smoothing: grayscale;
|
|
232
278
|
}
|
|
@@ -248,3 +294,85 @@
|
|
|
248
294
|
color: var(--color-text-primary);
|
|
249
295
|
}
|
|
250
296
|
}
|
|
297
|
+
|
|
298
|
+
@layer utilities {
|
|
299
|
+
/* Opt-in styled scrollbar — apply to scrollable containers */
|
|
300
|
+
.scrollbar-styled::-webkit-scrollbar {
|
|
301
|
+
width: 8px;
|
|
302
|
+
height: 8px;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.scrollbar-styled::-webkit-scrollbar-track {
|
|
306
|
+
background: transparent;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.scrollbar-styled::-webkit-scrollbar-thumb {
|
|
310
|
+
background: var(--color-border-strong);
|
|
311
|
+
border-radius: 4px;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.scrollbar-styled::-webkit-scrollbar-thumb:hover {
|
|
315
|
+
background: var(--color-text-tertiary);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/* Thin variant */
|
|
319
|
+
.scrollbar-thin::-webkit-scrollbar {
|
|
320
|
+
width: 4px;
|
|
321
|
+
height: 4px;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.scrollbar-thin::-webkit-scrollbar-track {
|
|
325
|
+
background: transparent;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.scrollbar-thin::-webkit-scrollbar-thumb {
|
|
329
|
+
background: var(--color-border-strong);
|
|
330
|
+
border-radius: 2px;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
|
|
334
|
+
background: var(--color-text-tertiary);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/* Hide scrollbar but keep scrollable */
|
|
338
|
+
.scrollbar-none::-webkit-scrollbar {
|
|
339
|
+
display: none;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.scrollbar-none {
|
|
343
|
+
-ms-overflow-style: none;
|
|
344
|
+
scrollbar-width: none;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/* Atmosphere: subtle radial gradient washes for ambient color */
|
|
348
|
+
.atmosphere-warm {
|
|
349
|
+
position: relative;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.atmosphere-warm::before {
|
|
353
|
+
content: '';
|
|
354
|
+
position: fixed;
|
|
355
|
+
inset: 0;
|
|
356
|
+
pointer-events: none;
|
|
357
|
+
z-index: 0;
|
|
358
|
+
background:
|
|
359
|
+
radial-gradient(ellipse 140% 100% at 15% 5%, rgba(var(--color-brand-primary-rgb, 255, 121, 0), 0.06) 0%, transparent 45%),
|
|
360
|
+
radial-gradient(ellipse 120% 80% at 85% 95%, rgba(var(--color-brand-secondary-rgb, 64, 109, 135), 0.04) 0%, transparent 45%);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/* Film grain overlay for vintage texture */
|
|
364
|
+
.grain {
|
|
365
|
+
position: relative;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.grain::after {
|
|
369
|
+
content: '';
|
|
370
|
+
position: fixed;
|
|
371
|
+
inset: 0;
|
|
372
|
+
pointer-events: none;
|
|
373
|
+
z-index: 1;
|
|
374
|
+
opacity: 0.015;
|
|
375
|
+
mix-blend-mode: overlay;
|
|
376
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
|
|
377
|
+
}
|
|
378
|
+
}
|
package/src/theme/index.ts
CHANGED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { ThemePreset } from './types'
|
|
2
|
+
|
|
3
|
+
function colorKeyToVar(key: string): string {
|
|
4
|
+
return `--color-${key}`
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function applyColorOverrides(colors: Record<string, string | undefined>, root: HTMLElement) {
|
|
8
|
+
for (const [key, value] of Object.entries(colors)) {
|
|
9
|
+
if (value !== undefined) {
|
|
10
|
+
root.style.setProperty(colorKeyToVar(key), value)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function applyFontOverrides(fonts: Record<string, string | undefined>, root: HTMLElement) {
|
|
16
|
+
const fontVarMap: Record<string, string> = {
|
|
17
|
+
sans: '--font-family-sans',
|
|
18
|
+
body: '--font-family-body',
|
|
19
|
+
heading: '--font-family-heading',
|
|
20
|
+
mono: '--font-family-mono',
|
|
21
|
+
}
|
|
22
|
+
for (const [key, value] of Object.entries(fonts)) {
|
|
23
|
+
if (value !== undefined && fontVarMap[key]) {
|
|
24
|
+
root.style.setProperty(fontVarMap[key], value)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function injectFontImport(url: string) {
|
|
30
|
+
if (typeof document === 'undefined') return
|
|
31
|
+
const existing = document.querySelector(`link[href="${url}"]`)
|
|
32
|
+
if (existing) return
|
|
33
|
+
const link = document.createElement('link')
|
|
34
|
+
link.rel = 'stylesheet'
|
|
35
|
+
link.href = url
|
|
36
|
+
document.head.appendChild(link)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function applyThemePreset(preset: ThemePreset): () => void {
|
|
40
|
+
if (typeof document === 'undefined') {
|
|
41
|
+
return () => {}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const root = document.documentElement
|
|
45
|
+
const removals: (() => void)[] = []
|
|
46
|
+
|
|
47
|
+
// Inject font import if specified
|
|
48
|
+
if (preset.fontImport) {
|
|
49
|
+
injectFontImport(preset.fontImport)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Determine current theme mode
|
|
53
|
+
const isLight = root.classList.contains('light')
|
|
54
|
+
const modeColors = isLight ? preset.colors?.light : preset.colors?.dark
|
|
55
|
+
|
|
56
|
+
// Apply color overrides
|
|
57
|
+
if (modeColors) {
|
|
58
|
+
const previousValues: Record<string, string | null> = {}
|
|
59
|
+
for (const key of Object.keys(modeColors)) {
|
|
60
|
+
previousValues[colorKeyToVar(key)] = root.style.getPropertyValue(colorKeyToVar(key)) || null
|
|
61
|
+
}
|
|
62
|
+
applyColorOverrides(modeColors, root)
|
|
63
|
+
removals.push(() => {
|
|
64
|
+
for (const [varName, prev] of Object.entries(previousValues)) {
|
|
65
|
+
if (prev === null) {
|
|
66
|
+
root.style.removeProperty(varName)
|
|
67
|
+
} else {
|
|
68
|
+
root.style.setProperty(varName, prev)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Apply font overrides
|
|
75
|
+
if (preset.fonts) {
|
|
76
|
+
applyFontOverrides(preset.fonts as Record<string, string | undefined>, root)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Apply root classes
|
|
80
|
+
if (preset.rootClasses) {
|
|
81
|
+
for (const cls of preset.rootClasses) {
|
|
82
|
+
root.classList.add(cls)
|
|
83
|
+
}
|
|
84
|
+
removals.push(() => {
|
|
85
|
+
for (const cls of preset.rootClasses!) {
|
|
86
|
+
root.classList.remove(cls)
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Return cleanup function
|
|
92
|
+
return () => {
|
|
93
|
+
for (const remove of removals) {
|
|
94
|
+
remove()
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { ThemePreset } from './types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Audiobook Studio preset.
|
|
5
|
+
*
|
|
6
|
+
* Warm copper/patina aesthetic inspired by vintage audio equipment
|
|
7
|
+
* and reading lounges. Dark-mode only design with editorial typography.
|
|
8
|
+
*/
|
|
9
|
+
export const audiobookPreset: ThemePreset = {
|
|
10
|
+
name: 'audiobook',
|
|
11
|
+
description: 'Warm copper/patina theme with editorial typography',
|
|
12
|
+
fontImport:
|
|
13
|
+
"https://fonts.googleapis.com/css2?family=Newsreader:ital,wght@0,400;0,600;0,700;1,400&family=Outfit:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap",
|
|
14
|
+
fonts: {
|
|
15
|
+
sans: "'Outfit', 'Avenir', 'Avenir Next', system-ui, sans-serif",
|
|
16
|
+
body: "'Outfit', 'Avenir', 'Avenir Next', system-ui, sans-serif",
|
|
17
|
+
heading: "'Newsreader', 'Libre Baskerville', Georgia, serif",
|
|
18
|
+
mono: "'JetBrains Mono', 'SF Mono', 'Fira Code', monospace",
|
|
19
|
+
},
|
|
20
|
+
colors: {
|
|
21
|
+
dark: {
|
|
22
|
+
// Brand: Copper accent
|
|
23
|
+
'brand-primary': '#d4782a',
|
|
24
|
+
'brand-primary-light': '#e8944a',
|
|
25
|
+
'brand-primary-dark': '#9a4a15',
|
|
26
|
+
'brand-primary-subtle': 'rgba(212, 120, 42, 0.12)',
|
|
27
|
+
'brand-primary-hover': '#e8944a',
|
|
28
|
+
'brand-primary-active': '#f5b070',
|
|
29
|
+
|
|
30
|
+
// Brand secondary: Patina/verdigris
|
|
31
|
+
'brand-secondary': '#3a9b8a',
|
|
32
|
+
'brand-secondary-light': '#55baa7',
|
|
33
|
+
'brand-secondary-dark': '#1f5c52',
|
|
34
|
+
'brand-secondary-subtle': 'rgba(58, 155, 138, 0.12)',
|
|
35
|
+
'brand-secondary-hover': '#55baa7',
|
|
36
|
+
'brand-secondary-active': '#7ed4c4',
|
|
37
|
+
|
|
38
|
+
'on-brand-primary': '#FFFFFF',
|
|
39
|
+
'on-brand-secondary': '#FFFFFF',
|
|
40
|
+
|
|
41
|
+
// Status colors (audiobook's warmer status palette)
|
|
42
|
+
'status-success': '#5fb870',
|
|
43
|
+
'status-error': '#d85c4a',
|
|
44
|
+
'status-warning': '#d4a43a',
|
|
45
|
+
'status-info': '#5a8fd4',
|
|
46
|
+
|
|
47
|
+
// Text: warm off-whites
|
|
48
|
+
'text-primary': '#f4f3f1',
|
|
49
|
+
'text-secondary': '#b5b3af',
|
|
50
|
+
'text-tertiary': '#7a7875',
|
|
51
|
+
|
|
52
|
+
// Surfaces: warm charcoals (not pure gray)
|
|
53
|
+
'surface-base': '#101114',
|
|
54
|
+
'surface-elevated': '#15171c',
|
|
55
|
+
'surface-raised': '#1a1d24',
|
|
56
|
+
'surface-overlay': '#15171c',
|
|
57
|
+
'surface-input': '#15171c',
|
|
58
|
+
|
|
59
|
+
// Backgrounds
|
|
60
|
+
'background-base': '#0a0b0d',
|
|
61
|
+
'background-default': '#101114',
|
|
62
|
+
'background-subtle': '#1a1d24',
|
|
63
|
+
|
|
64
|
+
// Borders
|
|
65
|
+
'border-default': 'rgba(255, 255, 255, 0.06)',
|
|
66
|
+
'border-subtle': 'rgba(255, 255, 255, 0.03)',
|
|
67
|
+
'border-strong': '#2d323d',
|
|
68
|
+
'border-focus': '#d4782a',
|
|
69
|
+
'border-input': '#2d323d',
|
|
70
|
+
'border-input-hover': '#454c5c',
|
|
71
|
+
'border-input-focus': '#d4782a',
|
|
72
|
+
'border-input-error': '#d85c4a',
|
|
73
|
+
|
|
74
|
+
// Interactive
|
|
75
|
+
'interactive-hover': 'rgba(255, 255, 255, 0.04)',
|
|
76
|
+
'interactive-focus': 'rgba(255, 255, 255, 0.12)',
|
|
77
|
+
'interactive-active': 'rgba(255, 255, 255, 0.16)',
|
|
78
|
+
'interactive-selected': 'rgba(255, 255, 255, 0.08)',
|
|
79
|
+
|
|
80
|
+
'divider': 'rgba(255, 255, 255, 0.06)',
|
|
81
|
+
|
|
82
|
+
// Glow RGB values for shadow utilities
|
|
83
|
+
'brand-primary-rgb': '212, 120, 42',
|
|
84
|
+
'brand-secondary-rgb': '58, 155, 138',
|
|
85
|
+
'status-success-rgb': '95, 184, 112',
|
|
86
|
+
'status-error-rgb': '216, 92, 74',
|
|
87
|
+
'status-warning-rgb': '212, 164, 58',
|
|
88
|
+
'status-info-rgb': '90, 143, 212',
|
|
89
|
+
'background-base-rgb': '10, 11, 13',
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
rootClasses: ['atmosphere-warm', 'grain'],
|
|
93
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest'
|
|
2
|
+
import { applyThemePreset } from './apply'
|
|
3
|
+
import type { ThemePreset } from './types'
|
|
4
|
+
|
|
5
|
+
describe('applyThemePreset', () => {
|
|
6
|
+
let cleanup: () => void
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
cleanup?.()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('applies color overrides to document root', () => {
|
|
13
|
+
const preset: ThemePreset = {
|
|
14
|
+
name: 'test',
|
|
15
|
+
colors: {
|
|
16
|
+
dark: {
|
|
17
|
+
'brand-primary': '#FF0000',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
cleanup = applyThemePreset(preset)
|
|
22
|
+
const value = document.documentElement.style.getPropertyValue('--color-brand-primary')
|
|
23
|
+
expect(value).toBe('#FF0000')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('returns cleanup function that removes overrides', () => {
|
|
27
|
+
const preset: ThemePreset = {
|
|
28
|
+
name: 'test',
|
|
29
|
+
colors: {
|
|
30
|
+
dark: {
|
|
31
|
+
'brand-primary': '#FF0000',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
cleanup = applyThemePreset(preset)
|
|
36
|
+
cleanup()
|
|
37
|
+
const value = document.documentElement.style.getPropertyValue('--color-brand-primary')
|
|
38
|
+
expect(value).toBe('')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('adds root classes', () => {
|
|
42
|
+
const preset: ThemePreset = {
|
|
43
|
+
name: 'test',
|
|
44
|
+
rootClasses: ['atmosphere-warm'],
|
|
45
|
+
}
|
|
46
|
+
cleanup = applyThemePreset(preset)
|
|
47
|
+
expect(document.documentElement.classList.contains('atmosphere-warm')).toBe(true)
|
|
48
|
+
cleanup()
|
|
49
|
+
expect(document.documentElement.classList.contains('atmosphere-warm')).toBe(false)
|
|
50
|
+
})
|
|
51
|
+
})
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export interface ThemePresetColors {
|
|
2
|
+
'brand-primary'?: string
|
|
3
|
+
'brand-primary-light'?: string
|
|
4
|
+
'brand-primary-dark'?: string
|
|
5
|
+
'brand-primary-subtle'?: string
|
|
6
|
+
'brand-primary-hover'?: string
|
|
7
|
+
'brand-primary-active'?: string
|
|
8
|
+
'brand-secondary'?: string
|
|
9
|
+
'brand-secondary-light'?: string
|
|
10
|
+
'brand-secondary-dark'?: string
|
|
11
|
+
'brand-secondary-subtle'?: string
|
|
12
|
+
'brand-secondary-hover'?: string
|
|
13
|
+
'brand-secondary-active'?: string
|
|
14
|
+
'on-brand-primary'?: string
|
|
15
|
+
'on-brand-secondary'?: string
|
|
16
|
+
'status-success'?: string
|
|
17
|
+
'status-error'?: string
|
|
18
|
+
'status-warning'?: string
|
|
19
|
+
'status-info'?: string
|
|
20
|
+
'text-primary'?: string
|
|
21
|
+
'text-secondary'?: string
|
|
22
|
+
'text-tertiary'?: string
|
|
23
|
+
'surface-base'?: string
|
|
24
|
+
'surface-elevated'?: string
|
|
25
|
+
'surface-raised'?: string
|
|
26
|
+
'surface-overlay'?: string
|
|
27
|
+
'surface-input'?: string
|
|
28
|
+
'background-base'?: string
|
|
29
|
+
'background-default'?: string
|
|
30
|
+
'background-subtle'?: string
|
|
31
|
+
'border-default'?: string
|
|
32
|
+
'border-subtle'?: string
|
|
33
|
+
'border-strong'?: string
|
|
34
|
+
'border-focus'?: string
|
|
35
|
+
'divider'?: string
|
|
36
|
+
[key: `${string}`]: string | undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ThemePresetFonts {
|
|
40
|
+
sans?: string
|
|
41
|
+
body?: string
|
|
42
|
+
heading?: string
|
|
43
|
+
mono?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ThemePresetRadii {
|
|
47
|
+
sm?: string
|
|
48
|
+
DEFAULT?: string
|
|
49
|
+
md?: string
|
|
50
|
+
lg?: string
|
|
51
|
+
xl?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ThemePresetShadows {
|
|
55
|
+
sm?: string
|
|
56
|
+
DEFAULT?: string
|
|
57
|
+
md?: string
|
|
58
|
+
lg?: string
|
|
59
|
+
xl?: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ThemePreset {
|
|
63
|
+
name: string
|
|
64
|
+
description?: string
|
|
65
|
+
colors?: {
|
|
66
|
+
dark?: ThemePresetColors
|
|
67
|
+
light?: ThemePresetColors
|
|
68
|
+
}
|
|
69
|
+
fonts?: ThemePresetFonts
|
|
70
|
+
radii?: ThemePresetRadii
|
|
71
|
+
shadows?: ThemePresetShadows
|
|
72
|
+
/** Additional CSS classes to add to root element */
|
|
73
|
+
rootClasses?: string[]
|
|
74
|
+
/** Google Fonts import URL (appended to existing imports) */
|
|
75
|
+
fontImport?: string
|
|
76
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { avatarColor, getInitials, AVATAR_COLORS } from './avatar-color'
|
|
3
|
+
|
|
4
|
+
describe('avatarColor', () => {
|
|
5
|
+
it('returns a color from the palette', () => {
|
|
6
|
+
expect(AVATAR_COLORS).toContain(avatarColor('Alice'))
|
|
7
|
+
})
|
|
8
|
+
it('is deterministic', () => {
|
|
9
|
+
expect(avatarColor('Bob')).toBe(avatarColor('Bob'))
|
|
10
|
+
})
|
|
11
|
+
it('varies across names', () => {
|
|
12
|
+
const colors = new Set(['Alice', 'Bob', 'Charlie', 'Dave'].map(avatarColor))
|
|
13
|
+
expect(colors.size).toBeGreaterThan(1)
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
describe('getInitials', () => {
|
|
18
|
+
it('returns first+last initials', () => {
|
|
19
|
+
expect(getInitials('John Doe')).toBe('JD')
|
|
20
|
+
})
|
|
21
|
+
it('handles single name', () => {
|
|
22
|
+
expect(getInitials('Alice')).toBe('A')
|
|
23
|
+
})
|
|
24
|
+
it('handles empty string', () => {
|
|
25
|
+
expect(getInitials('')).toBe('?')
|
|
26
|
+
})
|
|
27
|
+
it('handles multi-part names', () => {
|
|
28
|
+
expect(getInitials('Mary Jane Watson')).toBe('MW')
|
|
29
|
+
})
|
|
30
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const AVATAR_COLORS = [
|
|
2
|
+
'#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8'
|
|
3
|
+
]
|
|
4
|
+
|
|
5
|
+
export function avatarColor(name: string): string {
|
|
6
|
+
let hash = 0
|
|
7
|
+
for (let i = 0; i < name.length; i++) {
|
|
8
|
+
hash = name.charCodeAt(i) + ((hash << 5) - hash)
|
|
9
|
+
}
|
|
10
|
+
return AVATAR_COLORS[Math.abs(hash) % AVATAR_COLORS.length]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getInitials(name: string): string {
|
|
14
|
+
const parts = name.trim().split(/\s+/)
|
|
15
|
+
if (parts.length === 0) return '?'
|
|
16
|
+
if (parts.length === 1) return parts[0][0]?.toUpperCase() ?? '?'
|
|
17
|
+
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { AVATAR_COLORS }
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Development-mode custom JSX runtime for titan-design's web distribution.
|
|
3
|
+
* See jsx-runtime-web.ts for details on the $$css conversion approach.
|
|
4
|
+
*/
|
|
5
|
+
import { jsxDEV as reactJsxDEV, Fragment } from 'react/jsx-dev-runtime'
|
|
6
|
+
import type { StyleProp } from 'react-native'
|
|
7
|
+
|
|
8
|
+
type JsxDevFn = typeof reactJsxDEV
|
|
9
|
+
|
|
10
|
+
function wrapJsxDev(fn: JsxDevFn): JsxDevFn {
|
|
11
|
+
return function (type, props: Record<string, any>, ...rest) {
|
|
12
|
+
if (props && typeof props.className === 'string' && props.className) {
|
|
13
|
+
const cn = props.className
|
|
14
|
+
const cssStyle = { $$css: true, [cn]: cn } as unknown as StyleProp<any>
|
|
15
|
+
const existing = props.style
|
|
16
|
+
props = {
|
|
17
|
+
...props,
|
|
18
|
+
style: existing ? [cssStyle, existing] : cssStyle,
|
|
19
|
+
}
|
|
20
|
+
delete props.className
|
|
21
|
+
}
|
|
22
|
+
return fn(type, props, ...rest)
|
|
23
|
+
} as JsxDevFn
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const jsxDEV = wrapJsxDev(reactJsxDEV)
|
|
27
|
+
|
|
28
|
+
export { jsxDEV, Fragment }
|