@nebutra/tokens 0.1.1 → 0.1.3
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/CHANGELOG.md +23 -0
- package/LICENSE +21 -676
- package/README.md +41 -8
- package/brands/gsap/brand.json +133 -0
- package/brands/linear/brand.json +163 -0
- package/brands/notion/brand.json +145 -0
- package/brands/raycast/brand.json +120 -0
- package/brands/stripe/brand.json +127 -0
- package/brands/vanta/brand.json +151 -0
- package/brands/vercel/brand.json +173 -0
- package/dist/brand-package/index.d.ts +123 -0
- package/dist/brand-package/index.js +61 -0
- package/dist/brand-package/index.js.map +1 -0
- package/dist/brand-package/use-brand.d.ts +2 -0
- package/dist/brand-package/use-brand.js +12 -0
- package/dist/brand-package/use-brand.js.map +1 -0
- package/dist/chunk-ALJTP5HL.js +881 -0
- package/dist/chunk-ALJTP5HL.js.map +1 -0
- package/dist/chunk-RGUJQOLH.js +1582 -0
- package/dist/chunk-RGUJQOLH.js.map +1 -0
- package/dist/index.d.ts +81 -0
- package/dist/index.js +187 -0
- package/dist/index.js.map +1 -0
- package/dist/use-brand-C8d4DPqE.d.ts +371 -0
- package/package.json +46 -12
- package/recipe.css +435 -0
- package/skins/README.md +149 -0
- package/skins/gsap.css +159 -0
- package/skins/linear.css +198 -0
- package/skins/notion.css +158 -0
- package/skins/raycast.css +185 -0
- package/skins/stripe.css +146 -0
- package/skins/vanta.css +160 -0
- package/skins/vercel.css +202 -0
- package/styles.css +528 -169
- package/.turbo/turbo-build.log +0 -5
- package/.turbo/turbo-typecheck.log +0 -4
- package/AGENTS.md +0 -42
- package/DESIGN.md +0 -335
- package/src/index.ts +0 -37
- package/src/theme-provider.tsx +0 -244
- package/tsconfig.json +0 -11
package/.turbo/turbo-build.log
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
> @nebutra/tokens@0.1.1 build /home/runner/work/Nebutra-Sailor/Nebutra-Sailor/packages/design/tokens
|
|
3
|
-
> node -e "const fs=require('node:fs'),src='../design-tokens/build/css/styles.generated.css';if(fs.existsSync(src)){fs.copyFileSync(src,'./styles.css');console.log('styles.css refreshed from @nebutra/design-tokens')}else{console.log('styles.css kept (committed copy used; design-tokens build/ absent — likely Turbo cache restore without files)')}"
|
|
4
|
-
|
|
5
|
-
styles.css refreshed from @nebutra/design-tokens
|
package/AGENTS.md
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
# AGENTS.md — packages/tokens
|
|
2
|
-
|
|
3
|
-
Execution contract for Nebutra's runtime design-token package.
|
|
4
|
-
|
|
5
|
-
## Scope
|
|
6
|
-
|
|
7
|
-
Applies to everything under `packages/design/tokens/`.
|
|
8
|
-
|
|
9
|
-
This package is the canonical runtime token layer for Nebutra apps. It owns the
|
|
10
|
-
base semantic CSS variables and the light/dark theme-provider surface.
|
|
11
|
-
|
|
12
|
-
## Source Of Truth
|
|
13
|
-
|
|
14
|
-
- Public package surface and exports: `package.json`, `src/index.ts`
|
|
15
|
-
- Canonical runtime token definitions and semantic CSS variables: `styles.css`
|
|
16
|
-
|
|
17
|
-
## Contract Boundaries
|
|
18
|
-
|
|
19
|
-
- Treat `styles.css` as the single source of truth for runtime token names,
|
|
20
|
-
semantic aliases, and Tailwind-consumable CSS variables. Do not redefine core
|
|
21
|
-
token names in apps or sibling packages.
|
|
22
|
-
- Keep `THEME_IDS` and `DEFAULT_THEME` in `src/index.ts` aligned with the
|
|
23
|
-
actual light/dark behavior implemented in `styles.css`.
|
|
24
|
-
- This package owns the base light/dark token system only. Do not move named
|
|
25
|
-
product-theme presets here; those belong in `@nebutra/theme`.
|
|
26
|
-
- Preserve the package boundary between CSS tokens and component usage.
|
|
27
|
-
`@nebutra/ui` and apps should consume the exported CSS surface rather than
|
|
28
|
-
importing token internals.
|
|
29
|
-
- Because `./styles.css` is an exported side-effect file, treat renames or
|
|
30
|
-
variable removals as cross-repo breaking changes even if TypeScript passes.
|
|
31
|
-
|
|
32
|
-
## Generated And Derived Files
|
|
33
|
-
|
|
34
|
-
- `tsconfig.tsbuildinfo` and compiler artifacts are derived files.
|
|
35
|
-
- Consumer app compiled CSS output is derived from `styles.css`.
|
|
36
|
-
|
|
37
|
-
## Validation
|
|
38
|
-
|
|
39
|
-
- Token or export changes:
|
|
40
|
-
`pnpm --filter @nebutra/tokens typecheck`
|
|
41
|
-
- When token names or theme IDs change, verify at least one consuming package
|
|
42
|
-
updates with the same change instead of relying on stale compiled CSS.
|
package/DESIGN.md
DELETED
|
@@ -1,335 +0,0 @@
|
|
|
1
|
-
# `@nebutra/tokens` — Design Spec
|
|
2
|
-
|
|
3
|
-
> Runtime token layer of the Nebutra-Sailor design system.
|
|
4
|
-
> Part of the [root DESIGN.md](../../DESIGN.md). Spec format: `design-md@2026.05`.
|
|
5
|
-
|
|
6
|
-
| Field | Value |
|
|
7
|
-
|------|------|
|
|
8
|
-
| Package | `@nebutra/tokens` |
|
|
9
|
-
| Status | **★ Source of truth for runtime apps** |
|
|
10
|
-
| Source file | `packages/design/tokens/styles.css` |
|
|
11
|
-
| Tailwind integration | Tailwind v4 `@theme inline` block in same file |
|
|
12
|
-
| Re-exports | `ThemeProvider`, `useTheme` from `next-themes` |
|
|
13
|
-
|
|
14
|
-
---
|
|
15
|
-
|
|
16
|
-
## 1. Identity
|
|
17
|
-
|
|
18
|
-
This package emits the **runtime CSS variables** consumed by every Nebutra app. Apps import via:
|
|
19
|
-
|
|
20
|
-
```css
|
|
21
|
-
/* apps/{app}/src/app/globals.css */
|
|
22
|
-
@import "tailwindcss";
|
|
23
|
-
@import "@nebutra/tokens/styles.css";
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
Tokens are organized in three concentric rings:
|
|
27
|
-
|
|
28
|
-
```
|
|
29
|
-
Brand scales (raw) → --nebutra-blue-{50..950}, --nebutra-cyan-{50..950}, --nebutra-neutral-{50..950}
|
|
30
|
-
12-step semantic scales → --neutral-{1..12}, --blue-{1..12}, --cyan-{1..12}
|
|
31
|
-
Aliased semantic tokens → --background, --foreground, --primary, --brand-primary, --status-*
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
---
|
|
35
|
-
|
|
36
|
-
## 2. Tokens
|
|
37
|
-
|
|
38
|
-
### 2.1 Brand color scales (locked anchors)
|
|
39
|
-
|
|
40
|
-
#### 云毓蓝 (`--nebutra-blue-*`)
|
|
41
|
-
|
|
42
|
-
| Step | Hex |
|
|
43
|
-
|------|-----|
|
|
44
|
-
| 50 | `#F0F4FF` |
|
|
45
|
-
| 100 | `#DBE4FF` |
|
|
46
|
-
| 200 | `#BAC8FF` |
|
|
47
|
-
| 300 | `#91A7FF` |
|
|
48
|
-
| 400 | `#5C7CFA` |
|
|
49
|
-
| **500** | **`#0033FE`** (base) |
|
|
50
|
-
| 600 | `#002AD4` |
|
|
51
|
-
| 700 | `#0021AB` |
|
|
52
|
-
| 800 | `#001882` |
|
|
53
|
-
| 900 | `#000F59` |
|
|
54
|
-
| 950 | `#000830` |
|
|
55
|
-
|
|
56
|
-
#### 云毓青 (`--nebutra-cyan-*`)
|
|
57
|
-
|
|
58
|
-
| Step | Hex |
|
|
59
|
-
|------|-----|
|
|
60
|
-
| 50 | `#E6FFF8` |
|
|
61
|
-
| 100 | `#B3FFEC` |
|
|
62
|
-
| 200 | `#80FFE0` |
|
|
63
|
-
| 300 | `#4DFCD4` |
|
|
64
|
-
| 400 | `#1AF7C8` |
|
|
65
|
-
| **500** | **`#0BF1C3`** (base) |
|
|
66
|
-
| 600 | `#09C9A3` |
|
|
67
|
-
| 700 | `#07A183` |
|
|
68
|
-
| 800 | `#057963` |
|
|
69
|
-
| 900 | `#035143` |
|
|
70
|
-
| 950 | `#012923` |
|
|
71
|
-
|
|
72
|
-
#### Neutral (blue-undertone slate; `--nebutra-neutral-*`)
|
|
73
|
-
|
|
74
|
-
| Step | Hex |
|
|
75
|
-
|------|-----|
|
|
76
|
-
| 50 | `#F8FAFC` |
|
|
77
|
-
| 100 | `#F1F5F9` |
|
|
78
|
-
| 200 | `#E2E8F0` |
|
|
79
|
-
| 300 | `#CBD5E1` |
|
|
80
|
-
| 400 | `#94A3B8` |
|
|
81
|
-
| 500 | `#64748B` |
|
|
82
|
-
| 600 | `#475569` |
|
|
83
|
-
| 700 | `#334155` |
|
|
84
|
-
| 800 | `#1E293B` |
|
|
85
|
-
| 900 | `#0F172A` |
|
|
86
|
-
| 950 | `#020617` |
|
|
87
|
-
|
|
88
|
-
> Wide-gamut: when the display supports `display-p3`, `--nebutra-brand-blue` and `--nebutra-brand-cyan` upgrade automatically to richer P3 values (`color(display-p3 …)` syntax).
|
|
89
|
-
|
|
90
|
-
### 2.2 12-step functional scales — semantic ladder
|
|
91
|
-
|
|
92
|
-
Each scale obeys this Geist-style mapping:
|
|
93
|
-
|
|
94
|
-
| Steps | Role |
|
|
95
|
-
|-------|------|
|
|
96
|
-
| 1–2 | Backgrounds (app, subtle) |
|
|
97
|
-
| 3–5 | Component bg (default / hover / active) |
|
|
98
|
-
| 6–8 | Borders (subtle / default / hover) |
|
|
99
|
-
| 9–10 | Solid fills (default / hover) |
|
|
100
|
-
| 11 | Low-contrast / secondary text |
|
|
101
|
-
| 12 | High-contrast / primary text |
|
|
102
|
-
|
|
103
|
-
Scales: `--neutral-{1..12}`, `--blue-{1..12}`, `--cyan-{1..12}`. Light values are derived from the brand scales above; dark values invert the order so `step 1 = darkest, step 12 = lightest`. Full mapping table is in [root DESIGN.md §2.1](../../DESIGN.md#21-color--12-step-functional-scales).
|
|
104
|
-
|
|
105
|
-
### 2.3 Brand aliases (canonical for app code)
|
|
106
|
-
|
|
107
|
-
| Token | Resolves to | Use |
|
|
108
|
-
|-------|-------------|-----|
|
|
109
|
-
| `--brand-primary` | `var(--blue-9)` → `#0033FE` | Primary brand |
|
|
110
|
-
| `--brand-accent` | `var(--cyan-9)` → `#0BF1C3` | Accent |
|
|
111
|
-
| `--brand-tertiary` | `#8B5CF6` | Data viz / infra tags only |
|
|
112
|
-
| `--brand-gradient` | `linear-gradient(135deg, #254BFA → #057963)` | UI usage gradient for CTAs, badges, hero text |
|
|
113
|
-
| `--brand-gradient-logo` | `linear-gradient(135deg, #0033FE → #0BF1C3)` | VI/logo assets only |
|
|
114
|
-
|
|
115
|
-
### 2.4 Semantic theme tokens (HSL triplets — Tailwind compatible)
|
|
116
|
-
|
|
117
|
-
These are stored as space-separated HSL triplets so Tailwind v4 can consume them via `hsl(var(--token))`.
|
|
118
|
-
|
|
119
|
-
| Token | Light value | Dark value | Tailwind class |
|
|
120
|
-
|------|-------------|-----------|---------------|
|
|
121
|
-
| `--background` | `0 0% 100%` | `222 47% 4%` | `bg-background` |
|
|
122
|
-
| `--foreground` | `222 47% 11%` | `210 40% 98%` | `text-foreground` |
|
|
123
|
-
| `--card` | `0 0% 100%` | `222 47% 7%` | `bg-card` |
|
|
124
|
-
| `--popover` | `0 0% 100%` | `222 47% 7%` | `bg-popover` |
|
|
125
|
-
| `--primary` | `228 85% 56%` | `228 90% 72%` | `bg-primary` |
|
|
126
|
-
| `--secondary` | `210 40% 96%` | `217 33% 17%` | `bg-secondary` |
|
|
127
|
-
| `--muted` | `210 40% 96%` | `217 33% 17%` | `bg-muted` |
|
|
128
|
-
| `--accent` | `228 100% 97%` | `228 100% 12%` | `bg-accent` |
|
|
129
|
-
| `--destructive` | `0 84% 45%` | `0 63% 31%` | `bg-destructive` |
|
|
130
|
-
| `--success` | `142 71% 29%` | `142 76% 36%` | `bg-success` |
|
|
131
|
-
| `--warning` | `38 92% 50%` | `38 80% 45%` | `bg-warning` |
|
|
132
|
-
| `--info` | `228 100% 50%` | `228 95% 67%` | `bg-info` |
|
|
133
|
-
| `--border` | `240 5.9% 90%` | `217 33% 17%` | `border-border` |
|
|
134
|
-
| `--ring` | `228 100% 50%` | `228 95% 67%` | `ring-ring` |
|
|
135
|
-
|
|
136
|
-
### 2.5 Status colors (direct hex — for SVG, charts, inline)
|
|
137
|
-
|
|
138
|
-
| Token | Hex |
|
|
139
|
-
|-------|-----|
|
|
140
|
-
| `--status-danger` | `#EF4444` |
|
|
141
|
-
| `--status-warning` | `#F59E0B` |
|
|
142
|
-
| `--status-success` | `#10B981` |
|
|
143
|
-
| `--status-info` | `var(--brand-primary)` |
|
|
144
|
-
|
|
145
|
-
### 2.6 Geist DS scale (interop with 21st.dev / Geist components)
|
|
146
|
-
|
|
147
|
-
`--ds-blue-{200,700,900}`, `--ds-red-*`, `--ds-amber-*`, `--ds-green-*`, `--ds-teal-*`, `--ds-purple-*`, `--ds-pink-*`, `--ds-gray-{100..1000}`, `--ds-background-100`, plus accent gradients `--ds-trial-*`, `--ds-turbo-*`. Stored as oklch (with sRGB hsla fallback for legacy engines).
|
|
148
|
-
|
|
149
|
-
### 2.7 Layout containers
|
|
150
|
-
|
|
151
|
-
| Token | Value |
|
|
152
|
-
|-------|-------|
|
|
153
|
-
| `--container-text` | `896px` |
|
|
154
|
-
| `--container-content` | `1152px` |
|
|
155
|
-
| `--container-wide` | `1400px` |
|
|
156
|
-
|
|
157
|
-
### 2.8 Radius scale
|
|
158
|
-
|
|
159
|
-
```
|
|
160
|
-
--radius-none: 0
|
|
161
|
-
--radius-sm: 0.25rem (4px)
|
|
162
|
-
--radius-md: 0.375rem (6px) ← default --radius
|
|
163
|
-
--radius-lg: 0.5rem (8px)
|
|
164
|
-
--radius-xl: 0.75rem (12px)
|
|
165
|
-
--radius-2xl: 1rem (16px)
|
|
166
|
-
--radius-3xl: 1.5rem (24px)
|
|
167
|
-
--radius-full: 9999px
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
### 2.9 Elevation / shadow
|
|
171
|
-
|
|
172
|
-
| Token | Light | Dark |
|
|
173
|
-
|------|-------|------|
|
|
174
|
-
| `--elevation-xs` | `0 1px 2px 0 rgb(0 0 0 / 0.05)` | `0 1px 2px 0 rgb(0 0 0 / 0.3)` |
|
|
175
|
-
| `--elevation-sm` | small card shadow | higher-contrast variant |
|
|
176
|
-
| `--elevation-md` | dropdown shadow | — |
|
|
177
|
-
| `--elevation-lg` | floating panel | — |
|
|
178
|
-
| `--elevation-xl` | modal | — |
|
|
179
|
-
| `--elevation-2xl` | top overlay | — |
|
|
180
|
-
| `--elevation-brand` | `0 0 0 1px rgb(0 51 254 / 0.15), 0 4px 20px -2px rgb(0 51 254 / 0.2)` | uses `#5C7CFA` glow |
|
|
181
|
-
| `--elevation-brand-lg` | hero CTA | hero CTA dark |
|
|
182
|
-
|
|
183
|
-
Aliased to Tailwind: `shadow-{xs,sm,md,lg,xl,2xl,brand,brand-lg}`.
|
|
184
|
-
|
|
185
|
-
### 2.10 Motion
|
|
186
|
-
|
|
187
|
-
Easing:
|
|
188
|
-
|
|
189
|
-
```
|
|
190
|
-
--ease-in: cubic-bezier(0.4, 0, 1, 1)
|
|
191
|
-
--ease-out: cubic-bezier(0, 0, 0.2, 1)
|
|
192
|
-
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1)
|
|
193
|
-
--ease-spring: cubic-bezier(0.175, 0.885, 0.32, 1.275)
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
Duration:
|
|
197
|
-
|
|
198
|
-
```
|
|
199
|
-
--duration-micro: 100ms /* hover, focus, toggle, button press */
|
|
200
|
-
--duration-flow: 200ms /* modal, dropdown, tab — default */
|
|
201
|
-
--duration-reveal: 300ms /* slide, expand, accordion, drawer */
|
|
202
|
-
--duration-cinematic: 500ms /* hero entrance, big delight */
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
Reduced motion: a global `@media (prefers-reduced-motion: reduce)` block in `styles.css` collapses all animation/transition durations to `0.01ms`.
|
|
206
|
-
|
|
207
|
-
### 2.11 Typography utilities
|
|
208
|
-
|
|
209
|
-
Geist-style named utilities defined via Tailwind v4 `@utility`. Naming pattern: `text-{role}-{px}[-strong|-mono|-tabular]`.
|
|
210
|
-
|
|
211
|
-
| Role | Sizes (px) |
|
|
212
|
-
|------|-----------|
|
|
213
|
-
| `text-heading-{px}` | 72, 64, 56, 48, 40, 32, 24, 20, 16, 14 |
|
|
214
|
-
| `text-button-{px}` | 16, 14, 12 |
|
|
215
|
-
| `text-label-{px}[-strong/-mono/-tabular]` | 20, 18, 16, 14, 13, 12 |
|
|
216
|
-
| `text-copy-{px}[-strong/-mono]` | 24, 20, 18, 16, 14, 13 |
|
|
217
|
-
|
|
218
|
-
Plus `font-cn` (CJK utility, `line-height: 1.75`, word-break-aware). Auto-activates on `<html lang="zh|ja|ko">`.
|
|
219
|
-
|
|
220
|
-
### 2.12 Chart colors
|
|
221
|
-
|
|
222
|
-
`--chart-{1..5}` — light: blue-500, cyan-500, purple, cyan-300, blue-700. Dark: brighter equivalents.
|
|
223
|
-
|
|
224
|
-
---
|
|
225
|
-
|
|
226
|
-
## 3. Patterns
|
|
227
|
-
|
|
228
|
-
### 3.1 Tailwind class usage (canonical)
|
|
229
|
-
|
|
230
|
-
```tsx
|
|
231
|
-
// 12-step scale classes
|
|
232
|
-
<div className="bg-neutral-3 text-neutral-12 border-neutral-7" />
|
|
233
|
-
<div className="bg-blue-9 text-cyan-11" />
|
|
234
|
-
|
|
235
|
-
// Semantic Tailwind classes
|
|
236
|
-
<div className="bg-primary text-primary-foreground" />
|
|
237
|
-
<div className="bg-destructive text-destructive-foreground" />
|
|
238
|
-
|
|
239
|
-
// Brand aliases
|
|
240
|
-
<div className="bg-brand-primary text-brand-accent" />
|
|
241
|
-
|
|
242
|
-
// Inline CSS variables (for SVG, recharts, dynamic values)
|
|
243
|
-
<stop stopColor="var(--brand-primary)" />
|
|
244
|
-
<Cell fill="var(--brand-accent)" />
|
|
245
|
-
<div style={{ background: "var(--brand-gradient)" }} />
|
|
246
|
-
|
|
247
|
-
// Arbitrary Tailwind for non-scale tokens
|
|
248
|
-
<div className="text-[color:var(--status-warning)]" />
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
### 3.2 ThemeProvider wiring (per app)
|
|
252
|
-
|
|
253
|
-
```tsx
|
|
254
|
-
// apps/{app}/src/app/layout.tsx
|
|
255
|
-
import { ThemeProvider } from "@nebutra/tokens";
|
|
256
|
-
|
|
257
|
-
<html lang="en" suppressHydrationWarning>
|
|
258
|
-
<body>
|
|
259
|
-
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
|
260
|
-
{children}
|
|
261
|
-
</ThemeProvider>
|
|
262
|
-
</body>
|
|
263
|
-
</html>
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
---
|
|
267
|
-
|
|
268
|
-
## 4. Imports & Conventions
|
|
269
|
-
|
|
270
|
-
### Allowed
|
|
271
|
-
|
|
272
|
-
```ts
|
|
273
|
-
import { ThemeProvider, useTheme } from "@nebutra/tokens";
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
```css
|
|
277
|
-
@import "@nebutra/tokens/styles.css";
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
### Forbidden
|
|
281
|
-
|
|
282
|
-
```tsx
|
|
283
|
-
// ❌ Hardcoded brand hex
|
|
284
|
-
<div style={{ color: "#0033FE" }} />
|
|
285
|
-
|
|
286
|
-
// ❌ Importing JS color tokens in runtime
|
|
287
|
-
import { colors } from "@nebutra/ui/theme"; // deprecated
|
|
288
|
-
|
|
289
|
-
// ❌ Adding raw hex into Tailwind arbitrary classes for brand colors
|
|
290
|
-
<div className="bg-[#0a0a0a]" /> // → bg-neutral-1
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
---
|
|
294
|
-
|
|
295
|
-
## 5. Theming
|
|
296
|
-
|
|
297
|
-
Light/dark switching: handled by `next-themes` via the `class` attribute. The `.dark` selector inside `styles.css` overrides every semantic token plus inverts the 12-step scales (step 1 = darkest, step 12 = lightest in dark mode).
|
|
298
|
-
|
|
299
|
-
For the **multi-theme product feature** (6 oklch presets), see [`packages/design/theme/DESIGN.md`](../theme/DESIGN.md).
|
|
300
|
-
|
|
301
|
-
---
|
|
302
|
-
|
|
303
|
-
## 6. Versioning & Governance
|
|
304
|
-
|
|
305
|
-
| Surface | Status |
|
|
306
|
-
|--------|--------|
|
|
307
|
-
| Brand color hex anchors (`#0033FE`, `#0BF1C3`, `--nebutra-neutral-{50..950}`) | **Locked** |
|
|
308
|
-
| UI brand gradient (`--brand-gradient`) | Extensible — may tune saturation/contrast while preserving locked brand anchors |
|
|
309
|
-
| 12-step semantic ladder positions (1–12 meaning) | **Locked** |
|
|
310
|
-
| Container widths | **Locked** |
|
|
311
|
-
| Radius scale values | **Locked** |
|
|
312
|
-
| Status color hex | **Locked** |
|
|
313
|
-
| Adding a new semantic token | Extensible — must come with both light + dark values and Tailwind binding |
|
|
314
|
-
| Adding a new typography utility | Extensible — must follow `text-{role}-{px}` naming |
|
|
315
|
-
| Removing/renaming any token | **Forbidden** without a codemod (`scripts/codemod-tokens.ts`) |
|
|
316
|
-
|
|
317
|
-
### Governance scripts
|
|
318
|
-
|
|
319
|
-
```bash
|
|
320
|
-
pnpm --filter @nebutra/tokens typecheck
|
|
321
|
-
node scripts/codemod-tokens.ts # migrate hex → tokens
|
|
322
|
-
pnpm tsx scripts/validate-ui-governance-policy.ts
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
---
|
|
326
|
-
|
|
327
|
-
## 7. Open questions / review notes
|
|
328
|
-
|
|
329
|
-
- The HSL semantic tokens (`--primary` etc.) and the 12-step scales (`--blue-9` etc.) have separate value definitions. The `--primary` light value `228 85% 56%` (#254bfa) is intentionally *softer* than `--blue-9` (#0033FE) for Shadcn-style emphasis. Document this rationale once more in §2.4.
|
|
330
|
-
- `--ds-*` Geist colors duplicate some 12-step scales — verify whether they can be consolidated when 21st.dev components are migrated.
|
|
331
|
-
|
|
332
|
-
---
|
|
333
|
-
|
|
334
|
-
← back to [root DESIGN.md](../../DESIGN.md) ·
|
|
335
|
-
peer specs: [brand](../brand/DESIGN.md) · [theme](../theme/DESIGN.md) · [ui](../ui/DESIGN.md)
|
package/src/index.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @nebutra/tokens — Runtime theme tokens & theme switching
|
|
3
|
-
*
|
|
4
|
-
* This package is the SINGLE SOURCE OF TRUTH for runtime design tokens.
|
|
5
|
-
*
|
|
6
|
-
* CSS tokens: @import "@nebutra/tokens/styles.css"
|
|
7
|
-
* → Brand color scales (--nebutra-blue-*, --nebutra-cyan-*)
|
|
8
|
-
* → 12-step functional scales (--neutral-1..12, --blue-1..12, --cyan-1..12)
|
|
9
|
-
* → Semantic variables (--primary, --background, --border, etc.)
|
|
10
|
-
* → Light/dark mode via :root / .dark
|
|
11
|
-
* → Display-P3 wide gamut with sRGB fallback
|
|
12
|
-
* → Tailwind v4 @theme integration
|
|
13
|
-
*
|
|
14
|
-
* JS exports: ThemeProvider, useTheme, THEME_STORAGE_KEY (custom — no next-themes)
|
|
15
|
-
* → App-level light/dark mode switching
|
|
16
|
-
* → ThemeProvider writes BOTH localStorage AND a cookie of the same name.
|
|
17
|
-
* Server Components read the cookie via `next/headers` cookies() and
|
|
18
|
-
* inject the resolved class directly into <html> — zero inline script,
|
|
19
|
-
* zero React 19 "script in component" warning, zero FOUC risk.
|
|
20
|
-
*
|
|
21
|
-
* Related packages:
|
|
22
|
-
* @nebutra/brand → brand primitives (color definitions, motion language)
|
|
23
|
-
* @nebutra/theme → multi-theme presets (6 oklch variants for SaaS product)
|
|
24
|
-
* @nebutra/ui → component library (consumes tokens via CSS variables)
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
export {
|
|
28
|
-
THEME_STORAGE_KEY,
|
|
29
|
-
ThemeProvider,
|
|
30
|
-
type ThemeProviderProps,
|
|
31
|
-
useTheme,
|
|
32
|
-
} from "./theme-provider";
|
|
33
|
-
|
|
34
|
-
export const THEME_IDS = ["light", "dark"] as const;
|
|
35
|
-
export type ThemeId = (typeof THEME_IDS)[number];
|
|
36
|
-
|
|
37
|
-
export const DEFAULT_THEME: ThemeId = "dark";
|
package/src/theme-provider.tsx
DELETED
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Custom ThemeProvider — replaces the upstream `next-themes` re-export.
|
|
5
|
-
*
|
|
6
|
-
* Why we own this: `next-themes@0.4.x` renders an inline `<script>` element
|
|
7
|
-
* inside its Client-Component provider for FOUC prevention. React 19 +
|
|
8
|
-
* Next.js 16 (Turbopack) now emit a console error when scripts appear
|
|
9
|
-
* inside Client Components ("Scripts inside React components are never
|
|
10
|
-
* executed when rendering on the client"). The script DID do its job at
|
|
11
|
-
* SSR time, but the warning pollutes the dev console on every page load.
|
|
12
|
-
*
|
|
13
|
-
* Our architecture splits the two concerns cleanly:
|
|
14
|
-
* - {@link ThemeProvider} (Client) — runtime state, OS sync, transitions
|
|
15
|
-
* - {@link ThemeScript} (Server) — synchronous FOUC-prevention inline
|
|
16
|
-
* script rendered into the SSR HTML
|
|
17
|
-
*
|
|
18
|
-
* The API surface mirrors next-themes so the migration is a drop-in for
|
|
19
|
-
* callers (same prop names: `attribute`, `defaultTheme`, `enableSystem`,
|
|
20
|
-
* `disableTransitionOnChange`, `storageKey`, `nonce`).
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import {
|
|
24
|
-
createContext,
|
|
25
|
-
type ReactNode,
|
|
26
|
-
useCallback,
|
|
27
|
-
useContext,
|
|
28
|
-
useEffect,
|
|
29
|
-
useMemo,
|
|
30
|
-
useState,
|
|
31
|
-
} from "react";
|
|
32
|
-
|
|
33
|
-
type Theme = "light" | "dark" | "system";
|
|
34
|
-
type ResolvedTheme = "light" | "dark";
|
|
35
|
-
|
|
36
|
-
const THEMES = ["light", "dark", "system"] as const satisfies readonly Theme[];
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Shared default storage key. Matches what `next-themes` uses, so any user
|
|
40
|
-
* who already has a saved preference keeps it across the migration.
|
|
41
|
-
*/
|
|
42
|
-
export const THEME_STORAGE_KEY = "theme";
|
|
43
|
-
|
|
44
|
-
interface ThemeContextValue {
|
|
45
|
-
/** Whether a real ThemeProvider is present above the consumer. */
|
|
46
|
-
isProviderBound: boolean;
|
|
47
|
-
/** The raw user preference. May be "system". */
|
|
48
|
-
theme: Theme;
|
|
49
|
-
/** Provider-level override for demos, locked routes, or embedded previews. */
|
|
50
|
-
forcedTheme?: Theme | undefined;
|
|
51
|
-
/** The concretely-applied theme — always "light" or "dark". */
|
|
52
|
-
resolvedTheme: ResolvedTheme;
|
|
53
|
-
/** The OS-level preference (always concrete). */
|
|
54
|
-
systemTheme: ResolvedTheme;
|
|
55
|
-
setTheme: (theme: Theme) => void;
|
|
56
|
-
themes: readonly Theme[];
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Read the active theme state. Safe to call outside a provider — returns
|
|
63
|
-
* a sane default in that case so consumers don't need to null-check.
|
|
64
|
-
*/
|
|
65
|
-
export function useTheme(): ThemeContextValue {
|
|
66
|
-
const ctx = useContext(ThemeContext);
|
|
67
|
-
if (ctx) return ctx;
|
|
68
|
-
return {
|
|
69
|
-
isProviderBound: false,
|
|
70
|
-
theme: "system",
|
|
71
|
-
forcedTheme: undefined,
|
|
72
|
-
resolvedTheme: "light",
|
|
73
|
-
systemTheme: "light",
|
|
74
|
-
setTheme: () => {
|
|
75
|
-
// no-op outside provider
|
|
76
|
-
},
|
|
77
|
-
themes: THEMES,
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function resolveSystemTheme(): ResolvedTheme {
|
|
82
|
-
if (typeof window === "undefined") return "light";
|
|
83
|
-
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function readStoredTheme(storageKey: string, fallback: Theme): Theme {
|
|
87
|
-
if (typeof window === "undefined") return fallback;
|
|
88
|
-
try {
|
|
89
|
-
const v = window.localStorage.getItem(storageKey);
|
|
90
|
-
if (v === "light" || v === "dark" || v === "system") return v;
|
|
91
|
-
} catch {
|
|
92
|
-
// localStorage may be disabled (Safari private mode, etc.)
|
|
93
|
-
}
|
|
94
|
-
return fallback;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** 1 year — long-lived because theme is a deliberate user choice. */
|
|
98
|
-
const THEME_COOKIE_MAX_AGE = 365 * 24 * 60 * 60;
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Mirror the resolved theme into a cookie so the next request can be
|
|
102
|
-
* server-rendered with the correct `<html>` class — eliminating the
|
|
103
|
-
* need for an inline FOUC-prevention `<script>` (which React 19 warns
|
|
104
|
-
* about). The cookie value is always concrete ("light" | "dark"), never
|
|
105
|
-
* "system" — Server Components shouldn't try to guess the OS preference.
|
|
106
|
-
*/
|
|
107
|
-
function writeThemeCookie(resolved: ResolvedTheme, cookieName: string) {
|
|
108
|
-
if (typeof document === "undefined") return;
|
|
109
|
-
document.cookie = `${cookieName}=${resolved}; Max-Age=${THEME_COOKIE_MAX_AGE}; Path=/; SameSite=Lax`;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function applyThemeToDom(
|
|
113
|
-
resolved: ResolvedTheme,
|
|
114
|
-
attribute: "class" | "data-theme",
|
|
115
|
-
disableTransitionOnChange: boolean,
|
|
116
|
-
) {
|
|
117
|
-
if (typeof document === "undefined") return;
|
|
118
|
-
const root = document.documentElement;
|
|
119
|
-
|
|
120
|
-
// Suppress CSS transitions during the swap so the change appears instant.
|
|
121
|
-
if (disableTransitionOnChange) {
|
|
122
|
-
const suppressor = document.createElement("style");
|
|
123
|
-
suppressor.appendChild(
|
|
124
|
-
document.createTextNode(
|
|
125
|
-
"*,*::before,*::after{transition:none!important;animation-duration:0s!important}",
|
|
126
|
-
),
|
|
127
|
-
);
|
|
128
|
-
document.head.appendChild(suppressor);
|
|
129
|
-
// Force a reflow so the rule applies before we swap classes
|
|
130
|
-
void window.getComputedStyle(suppressor).opacity;
|
|
131
|
-
// Remove the rule on the next tick; transitions resume afterwards
|
|
132
|
-
setTimeout(() => {
|
|
133
|
-
if (suppressor.parentNode) suppressor.parentNode.removeChild(suppressor);
|
|
134
|
-
}, 1);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (attribute === "class") {
|
|
138
|
-
root.classList.remove("light", "dark");
|
|
139
|
-
root.classList.add(resolved);
|
|
140
|
-
} else {
|
|
141
|
-
root.setAttribute("data-theme", resolved);
|
|
142
|
-
}
|
|
143
|
-
root.style.colorScheme = resolved;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export interface ThemeProviderProps {
|
|
147
|
-
children: ReactNode;
|
|
148
|
-
/** Which DOM attribute to set on `<html>`. Default: `"class"`. */
|
|
149
|
-
attribute?: "class" | "data-theme";
|
|
150
|
-
/** Default theme when no preference is stored. Default: `"system"`. */
|
|
151
|
-
defaultTheme?: Theme;
|
|
152
|
-
/** Whether `"system"` is a valid theme that tracks `prefers-color-scheme`. */
|
|
153
|
-
enableSystem?: boolean;
|
|
154
|
-
/** Lock the rendered theme and expose read-only state to controls. */
|
|
155
|
-
forcedTheme?: Theme | undefined;
|
|
156
|
-
/** Suppress CSS transitions during the swap. Default: `true`. */
|
|
157
|
-
disableTransitionOnChange?: boolean;
|
|
158
|
-
/** Storage key under which the preference is persisted. */
|
|
159
|
-
storageKey?: string;
|
|
160
|
-
/** CSP nonce — accepted for API compatibility, currently unused by client logic. */
|
|
161
|
-
nonce?: string;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export function ThemeProvider({
|
|
165
|
-
children,
|
|
166
|
-
attribute = "class",
|
|
167
|
-
defaultTheme = "system",
|
|
168
|
-
enableSystem = true,
|
|
169
|
-
forcedTheme,
|
|
170
|
-
disableTransitionOnChange = true,
|
|
171
|
-
storageKey = THEME_STORAGE_KEY,
|
|
172
|
-
}: ThemeProviderProps) {
|
|
173
|
-
const [theme, setThemeState] = useState<Theme>(() => readStoredTheme(storageKey, defaultTheme));
|
|
174
|
-
const [systemTheme, setSystemTheme] = useState<ResolvedTheme>(() => resolveSystemTheme());
|
|
175
|
-
|
|
176
|
-
const effectiveTheme = forcedTheme ?? theme;
|
|
177
|
-
const resolvedTheme: ResolvedTheme =
|
|
178
|
-
effectiveTheme === "system" ? (enableSystem ? systemTheme : "light") : effectiveTheme;
|
|
179
|
-
|
|
180
|
-
const setTheme = useCallback(
|
|
181
|
-
(next: Theme) => {
|
|
182
|
-
if (forcedTheme !== undefined) return;
|
|
183
|
-
setThemeState(next);
|
|
184
|
-
try {
|
|
185
|
-
if (next === "system") {
|
|
186
|
-
window.localStorage.removeItem(storageKey);
|
|
187
|
-
} else {
|
|
188
|
-
window.localStorage.setItem(storageKey, next);
|
|
189
|
-
}
|
|
190
|
-
} catch {
|
|
191
|
-
// localStorage may be disabled — accept the loss
|
|
192
|
-
}
|
|
193
|
-
},
|
|
194
|
-
[forcedTheme, storageKey],
|
|
195
|
-
);
|
|
196
|
-
|
|
197
|
-
// Apply the resolved theme to the DOM AND sync the cookie so the next
|
|
198
|
-
// SSR can render the correct <html> class directly (no inline script,
|
|
199
|
-
// no React 19 "script in component" warning).
|
|
200
|
-
useEffect(() => {
|
|
201
|
-
applyThemeToDom(resolvedTheme, attribute, disableTransitionOnChange);
|
|
202
|
-
writeThemeCookie(resolvedTheme, storageKey);
|
|
203
|
-
}, [resolvedTheme, attribute, disableTransitionOnChange, storageKey]);
|
|
204
|
-
|
|
205
|
-
// Follow OS preference changes when in `system` mode
|
|
206
|
-
useEffect(() => {
|
|
207
|
-
if (!enableSystem) return;
|
|
208
|
-
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
209
|
-
const handler = (event: MediaQueryListEvent) =>
|
|
210
|
-
setSystemTheme(event.matches ? "dark" : "light");
|
|
211
|
-
mq.addEventListener("change", handler);
|
|
212
|
-
return () => mq.removeEventListener("change", handler);
|
|
213
|
-
}, [enableSystem]);
|
|
214
|
-
|
|
215
|
-
// Sync across tabs / windows
|
|
216
|
-
useEffect(() => {
|
|
217
|
-
const handler = (event: StorageEvent) => {
|
|
218
|
-
if (event.key !== storageKey) return;
|
|
219
|
-
const next = event.newValue;
|
|
220
|
-
if (next === "light" || next === "dark" || next === "system") {
|
|
221
|
-
setThemeState(next);
|
|
222
|
-
} else if (next === null) {
|
|
223
|
-
setThemeState(defaultTheme);
|
|
224
|
-
}
|
|
225
|
-
};
|
|
226
|
-
window.addEventListener("storage", handler);
|
|
227
|
-
return () => window.removeEventListener("storage", handler);
|
|
228
|
-
}, [storageKey, defaultTheme]);
|
|
229
|
-
|
|
230
|
-
const value = useMemo<ThemeContextValue>(
|
|
231
|
-
() => ({
|
|
232
|
-
isProviderBound: true,
|
|
233
|
-
theme,
|
|
234
|
-
forcedTheme,
|
|
235
|
-
resolvedTheme,
|
|
236
|
-
systemTheme,
|
|
237
|
-
setTheme,
|
|
238
|
-
themes: THEMES,
|
|
239
|
-
}),
|
|
240
|
-
[theme, forcedTheme, resolvedTheme, systemTheme, setTheme],
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
|
|
244
|
-
}
|
package/tsconfig.json
DELETED