@talelk/newui-tokens 0.0.0-stage → 0.1.1
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 +12 -2
- package/package.json +30 -4
- package/src/css.d.ts +2 -0
- package/src/css.js +64 -0
- package/src/index.d.ts +67 -0
- package/src/index.js +413 -0
- package/src/tokens/colors.js +1176 -0
- package/src/tokens/effects.js +44 -0
- package/src/tokens/layers.js +35 -0
- package/src/tokens/provenance.json +11 -0
- package/src/tokens/radius.js +17 -0
- package/src/tokens/typography.js +471 -0
package/README.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @talelk/newui-tokens
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Framework-neutral token data defines colour, typography, spacing, radii, focus,
|
|
4
|
+
motion and elevation. `radiusRoles` maps control/card/overlay/pill onto the
|
|
5
|
+
primitive radius scale, and `layoutSpacing` names the page, section, card,
|
|
6
|
+
field and control rhythm used by components. The root export supplies token
|
|
7
|
+
values; `@talelk/newui-tokens/css` provides CSS generation helpers. These functions do
|
|
8
|
+
not own DOM or application state. Semantic surfaces, inks and outlines are the
|
|
9
|
+
component vocabulary.
|
|
10
|
+
|
|
11
|
+
Run `pnpm css` after modifying token data, then inspect light and dark rendered
|
|
12
|
+
surfaces and run contrast, CSS emission and generated-token checks. Applications
|
|
13
|
+
retune runtime custom properties rather than changing component recipes.
|
package/package.json
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@talelk/newui-tokens",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Framework-neutral design tokens (colors, radius, typography, spacing, elevation, focus).",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./src/index.d.ts",
|
|
9
|
+
"default": "./src/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./css": {
|
|
12
|
+
"types": "./src/css.d.ts",
|
|
13
|
+
"default": "./src/css.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"src"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
21
|
+
},
|
|
22
|
+
"sideEffects": false,
|
|
23
|
+
"license": "Apache-2.0",
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/talelk/newui.git",
|
|
30
|
+
"directory": "packages/tokens"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/css.d.ts
ADDED
package/src/css.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime CSS custom properties, generated from the token data.
|
|
3
|
+
*
|
|
4
|
+
* `light` goes on `:root`, `dark` on `[data-theme="dark"]`. This is how the
|
|
5
|
+
* theme flips without a single `dark:` variant: utilities resolve
|
|
6
|
+
* `var(--surface-*)` / `var(--ink-*)` / `var(--outline-*)`, and this file is
|
|
7
|
+
* what re-values those under the dark selector.
|
|
8
|
+
*
|
|
9
|
+
* Radius, typography, spacing and shadow are intentionally NOT emitted here:
|
|
10
|
+
* those are static and owned by Tailwind's `@theme` block. Emitting them twice
|
|
11
|
+
* would only create duplicate declarations.
|
|
12
|
+
*/
|
|
13
|
+
import { cssVariables } from './index.js'
|
|
14
|
+
|
|
15
|
+
/** Categorical series colours for charts, light and dark. */
|
|
16
|
+
const CHART_SERIES = [
|
|
17
|
+
'blue',
|
|
18
|
+
'green',
|
|
19
|
+
'amber',
|
|
20
|
+
'red',
|
|
21
|
+
'violet',
|
|
22
|
+
'cyan',
|
|
23
|
+
'teal',
|
|
24
|
+
'pink',
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
// The ramps are keyed by Tailwind's `50…950` scale, so a series pointer must
|
|
28
|
+
// name the three-digit step (`--blue-600`). A one-digit suffix (`--blue-6`)
|
|
29
|
+
// matches nothing: the custom property is guaranteed-invalid, every
|
|
30
|
+
// `var(--chart-*)` consumer falls back to `transparent`, and charts render
|
|
31
|
+
// invisible without any error.
|
|
32
|
+
function chartVariables(mode) {
|
|
33
|
+
const out = {}
|
|
34
|
+
CHART_SERIES.forEach((family, index) => {
|
|
35
|
+
const ramp = mode === 'dark' ? '--dark-' : '--'
|
|
36
|
+
out[`--chart-${index + 1}`] =
|
|
37
|
+
`var(${ramp}${family}-${mode === 'dark' ? '200' : '600'})`
|
|
38
|
+
})
|
|
39
|
+
return out
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function block(selector, vars) {
|
|
43
|
+
const body = Object.entries(vars)
|
|
44
|
+
.map(([name, value]) => ` ${name}: ${value};`)
|
|
45
|
+
.join('\n')
|
|
46
|
+
return `${selector} {\n${body}\n}`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function tokensCss() {
|
|
50
|
+
const light = Object.fromEntries(
|
|
51
|
+
Object.entries(cssVariables.light).filter(
|
|
52
|
+
([name]) => !name.startsWith('--radius-'),
|
|
53
|
+
),
|
|
54
|
+
)
|
|
55
|
+
return [
|
|
56
|
+
'/* Generated by @talelk/newui-tailwind `build:css`. Do not edit. */',
|
|
57
|
+
block(':root', { ...light, ...chartVariables('light') }),
|
|
58
|
+
block('[data-theme="dark"]', {
|
|
59
|
+
...cssVariables.dark,
|
|
60
|
+
...chartVariables('dark'),
|
|
61
|
+
}),
|
|
62
|
+
'',
|
|
63
|
+
].join('\n')
|
|
64
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type surface for the framework-neutral token data.
|
|
3
|
+
*
|
|
4
|
+
* The values themselves live in `index.js` (ported verbatim from frappe-ui so
|
|
5
|
+
* the token vocabulary and resolved values stay identical). This file only
|
|
6
|
+
* declares their shapes for TypeScript consumers.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type ColorRamp = Record<string, string>
|
|
10
|
+
export type SemanticLayer = Record<string, ColorRamp>
|
|
11
|
+
|
|
12
|
+
export interface CssVariableLayers {
|
|
13
|
+
light: Record<string, string>
|
|
14
|
+
dark: Record<string, string>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const colors: {
|
|
18
|
+
light: Record<string, ColorRamp>
|
|
19
|
+
dark: Record<string, ColorRamp>
|
|
20
|
+
overlay: Record<string, ColorRamp | string>
|
|
21
|
+
neutral: Record<string, string>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const semanticColors: {
|
|
25
|
+
light: SemanticLayer
|
|
26
|
+
dark: SemanticLayer
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const radius: Record<string, string>
|
|
30
|
+
export const radiusRoles: Record<
|
|
31
|
+
'control' | 'card' | 'overlay' | 'pill',
|
|
32
|
+
string
|
|
33
|
+
>
|
|
34
|
+
|
|
35
|
+
export const shadows: Record<string, string>
|
|
36
|
+
export const shadowRoles: Record<
|
|
37
|
+
'card' | 'raised' | 'popover' | 'dialog' | 'toast' | 'drag',
|
|
38
|
+
string
|
|
39
|
+
>
|
|
40
|
+
|
|
41
|
+
export const focusRing: {
|
|
42
|
+
light: Record<string, string>
|
|
43
|
+
dark: Record<string, string>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const fontSize: Record<
|
|
47
|
+
string,
|
|
48
|
+
{
|
|
49
|
+
fontSize: string
|
|
50
|
+
lineHeight: string
|
|
51
|
+
letterSpacing: string
|
|
52
|
+
fontWeight: number
|
|
53
|
+
}
|
|
54
|
+
>
|
|
55
|
+
|
|
56
|
+
export const fontWeight: Record<string, number>
|
|
57
|
+
export const fontFamily: Record<string, string>
|
|
58
|
+
export const tracking: Record<string, Record<string, Record<string, string>>>
|
|
59
|
+
export const spacing: Record<string, string>
|
|
60
|
+
export const layoutSpacing: Record<string, string>
|
|
61
|
+
export const screens: Record<string, string>
|
|
62
|
+
|
|
63
|
+
export const cssVariables: CssVariableLayers
|
|
64
|
+
|
|
65
|
+
export const zIndex: Record<string, number>
|
|
66
|
+
export const duration: Record<string, string>
|
|
67
|
+
export const easing: Record<string, string>
|
package/src/index.js
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The design tokens, as data.
|
|
3
|
+
*
|
|
4
|
+
* This is the one module that turns `./tokens/*.js` into shaped values, and
|
|
5
|
+
* the only one a consumer outside frappe-ui should read. Everything here is
|
|
6
|
+
* framework-neutral: resolved `oklch(...)` strings, plain px, plain numbers.
|
|
7
|
+
* Nothing carries a Tailwind sentinel.
|
|
8
|
+
*
|
|
9
|
+
* That last rule is load-bearing. `colorPalette.js` wraps these same values in
|
|
10
|
+
* `oklch(L C H / <alpha-value>)` and `color-mix(... calc(<alpha-value> *
|
|
11
|
+
* 100%) ...)` so Tailwind's `/50` opacity modifier works. Those strings mean
|
|
12
|
+
* nothing anywhere else — a colour picker fed `<alpha-value>` renders an empty
|
|
13
|
+
* swatch. The Tailwind-only shaping lives one layer up, in `colorPalette.js`
|
|
14
|
+
* and `plugin.js`, and never leaks back down here.
|
|
15
|
+
*
|
|
16
|
+
* Exported as `frappe-ui/tailwind/tokens`, its own entry point. It imports
|
|
17
|
+
* nothing but the four data modules beside it, so a Node script (codegen,
|
|
18
|
+
* docs, a design tool) can read tokens with no bundler and no component tree.
|
|
19
|
+
* The preset at `frappe-ui/tailwind` cannot: it statically imports
|
|
20
|
+
* `tailwindcss/plugin`, which plain Node does not resolve.
|
|
21
|
+
*
|
|
22
|
+
* Those four are plain modules, not JSON. Reading JSON from an ES module
|
|
23
|
+
* needs an import attribute, and the oldest config loaders in the supported
|
|
24
|
+
* peer range cannot parse one. See `tokens/build.js`.
|
|
25
|
+
*
|
|
26
|
+
* Per ADR-0010 this surface is additive-only until 2.0.0: names may be added,
|
|
27
|
+
* none may be renamed or removed.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import colorsData from './tokens/colors.js'
|
|
31
|
+
import effectsData from './tokens/effects.js'
|
|
32
|
+
import radiusTokens from './tokens/radius.js'
|
|
33
|
+
import typographyTokens from './tokens/typography.js'
|
|
34
|
+
|
|
35
|
+
// ---------- COLORS ----------
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Primitive colour ramps, split by theme. These are the raw scales, not the
|
|
39
|
+
* semantic vocabulary — `colors.light.gray[500]`, not `surface-gray-2`.
|
|
40
|
+
* Reach for `semanticColors` unless you specifically want a ramp step.
|
|
41
|
+
*/
|
|
42
|
+
export const colors = {
|
|
43
|
+
light: colorsData.lightMode,
|
|
44
|
+
dark: colorsData.darkMode,
|
|
45
|
+
overlay: colorsData.overlay,
|
|
46
|
+
neutral: colorsData.neutral,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// `themedVariables` stores each semantic entry as an unresolved pointer into
|
|
50
|
+
// the ramps above (`"lightMode/gray/50"`, `"neutral/white"`). Follow it.
|
|
51
|
+
function resolveColorReference(reference) {
|
|
52
|
+
const [mode, color, shade] = reference.split('/')
|
|
53
|
+
if (mode === 'lightMode') return colorsData.lightMode[color][shade]
|
|
54
|
+
if (mode === 'darkMode') return colorsData.darkMode[color][shade]
|
|
55
|
+
if (mode === 'overlay') return colorsData.overlay[color][shade]
|
|
56
|
+
if (mode === 'neutral') return colorsData.neutral[color]
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function resolveThemedLayer(layer) {
|
|
61
|
+
return Object.fromEntries(
|
|
62
|
+
Object.entries(layer).map(([category, entries]) => [
|
|
63
|
+
category,
|
|
64
|
+
Object.fromEntries(
|
|
65
|
+
Object.entries(entries).map(([name, reference]) => [
|
|
66
|
+
name,
|
|
67
|
+
resolveColorReference(reference),
|
|
68
|
+
]),
|
|
69
|
+
),
|
|
70
|
+
]),
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The semantic vocabulary — `surface`, `surface-alpha`, `ink`, `outline`,
|
|
76
|
+
* `outline-alpha` — resolved to real colours, per theme:
|
|
77
|
+
*
|
|
78
|
+
* semanticColors.light.surface['gray-2'] // 'oklch(0.964 0 0)'
|
|
79
|
+
* semanticColors.dark.ink.base // …
|
|
80
|
+
*
|
|
81
|
+
* Split by theme rather than theme-agnostic, because a consumer outside a
|
|
82
|
+
* frappe-ui page has no `[data-theme]` to resolve against and needs to pick a
|
|
83
|
+
* side. Inside a themed page, prefer the `--<category>-<name>` variables from
|
|
84
|
+
* `cssVariables`, which flip on their own.
|
|
85
|
+
*/
|
|
86
|
+
export const semanticColors = {
|
|
87
|
+
light: resolveThemedLayer(colorsData.themedVariables.light),
|
|
88
|
+
dark: resolveThemedLayer(colorsData.themedVariables.dark),
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Mode-flipping scrim (alias layer — the generated `colors.js` stays
|
|
92
|
+
// untouched). One `--surface-scrim` runtime var replaces every hand-copied
|
|
93
|
+
// `bg-black-overlay-200 dark:bg-black-overlay-700` backdrop pair, so overlays
|
|
94
|
+
// name the role and the theme flip lives here, never in a `dark:` variant.
|
|
95
|
+
semanticColors.light.surface.scrim = colorsData.overlay.black[200]
|
|
96
|
+
semanticColors.dark.surface.scrim = colorsData.overlay.black[700]
|
|
97
|
+
|
|
98
|
+
// ---------- RADIUS ----------
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Numbered radius scale `0`–`9` plus `none` and `full`, in px, extended with
|
|
102
|
+
* the semantic `control`, `card`, `overlay` and `pill` roles. The roles are the
|
|
103
|
+
* component vocabulary; the numbered steps remain for specialized geometry.
|
|
104
|
+
*/
|
|
105
|
+
/**
|
|
106
|
+
* Stable component roles mapped onto the primitive radius scale. Components
|
|
107
|
+
* use these names so changing a surface family does not require editing every
|
|
108
|
+
* recipe independently.
|
|
109
|
+
*/
|
|
110
|
+
export const radiusRoles = {
|
|
111
|
+
control: radiusTokens[3],
|
|
112
|
+
card: radiusTokens[4],
|
|
113
|
+
overlay: radiusTokens[6],
|
|
114
|
+
pill: radiusTokens.full,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const radius = { ...radiusTokens, ...radiusRoles }
|
|
118
|
+
|
|
119
|
+
// ---------- SHADOWS ----------
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The box-shadows frappe-ui renders, as one flat map of composed
|
|
123
|
+
* `box-shadow` strings. Keyed the way the `shadow-*` utilities are: `none`,
|
|
124
|
+
* the six elevation steps, and `DEFAULT` for the bare `shadow` class.
|
|
125
|
+
*
|
|
126
|
+
* Real values, not `var(--elevation-*)` references, so a consumer that has no
|
|
127
|
+
* frappe-ui stylesheet can still paint the shadow. `plugin.js` reads the key
|
|
128
|
+
* list from here and points each Tailwind key at its variable.
|
|
129
|
+
*
|
|
130
|
+
* This flat map carries the light steps (a consumer with no stylesheet picks
|
|
131
|
+
* a side itself); the runtime `--elevation-*` variables in `cssVariables`
|
|
132
|
+
* additionally ship `elevation.dark` under the dark selector, so a themed
|
|
133
|
+
* page gets dark-tuned depth. Focus is a separate shape, and a different CSS
|
|
134
|
+
* property: see `focusRing`.
|
|
135
|
+
*/
|
|
136
|
+
export const shadows = buildShadows()
|
|
137
|
+
|
|
138
|
+
// `DEFAULT` sits right after `base`, where the theme literal always had it.
|
|
139
|
+
// `plugin.js` builds `theme.boxShadow` in this key order, and theme key order
|
|
140
|
+
// is CSS source order: with `DEFAULT` last, `class="shadow shadow-xl"` would
|
|
141
|
+
// resolve to `.shadow`, not `.shadow-xl`.
|
|
142
|
+
function buildShadows() {
|
|
143
|
+
const out = { none: 'none' }
|
|
144
|
+
for (const [step, value] of Object.entries(effectsData.elevation.light)) {
|
|
145
|
+
out[step] = value
|
|
146
|
+
if (step === 'base') out.DEFAULT = value
|
|
147
|
+
}
|
|
148
|
+
return { ...out, ...effectsData.elevation.custom }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Stable component roles mapped onto the primitive elevation steps, the
|
|
153
|
+
* shadow counterpart of `radiusRoles`. Components name the role
|
|
154
|
+
* (`shadow-popover`) so retuning a surface family's depth is one edit here,
|
|
155
|
+
* not a sweep of every recipe's step pick.
|
|
156
|
+
*/
|
|
157
|
+
export const shadowRoles = {
|
|
158
|
+
card: 'sm',
|
|
159
|
+
raised: 'base',
|
|
160
|
+
popover: 'md',
|
|
161
|
+
dialog: 'lg',
|
|
162
|
+
toast: 'xl',
|
|
163
|
+
drag: '2xl',
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Focus tokens arrive as single-layer `0 0 0 <spread> <color>` shadows.
|
|
167
|
+
// Re-express as an `outline` shorthand so the focus ring never collides with
|
|
168
|
+
// a `shadow-*` utility on the same element and survives forced-colors mode
|
|
169
|
+
// (ADR-0005). Only the outline form is emitted; nothing reads `--focus-<name>`.
|
|
170
|
+
function shadowToOutline(shadow) {
|
|
171
|
+
const parts = shadow.trim().split(/\s+/)
|
|
172
|
+
return `${parts[3]} solid ${parts.slice(4).join(' ')}`
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function toOutlines(layer) {
|
|
176
|
+
return Object.fromEntries(
|
|
177
|
+
Object.entries(layer).map(([name, value]) => [
|
|
178
|
+
name,
|
|
179
|
+
shadowToOutline(value),
|
|
180
|
+
]),
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* The focus ring per theme, one entry per colour (`default`, `red`, `green`,
|
|
186
|
+
* `amber`, `blue`, `violet`).
|
|
187
|
+
*
|
|
188
|
+
* Each value is an `outline` shorthand — `'2px solid #c9c9c9e5'` — not a
|
|
189
|
+
* box-shadow. frappe-ui draws focus with `outline` (ADR-0005), and these are
|
|
190
|
+
* the exact strings behind `--focus-outline-<name>`. Light rings are 2px,
|
|
191
|
+
* dark ones 3px, which is why this one does mode-swap and `shadows` does not.
|
|
192
|
+
*/
|
|
193
|
+
export const focusRing = {
|
|
194
|
+
light: toOutlines(effectsData.focus.light),
|
|
195
|
+
dark: toOutlines(effectsData.focus.dark),
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ---------- TYPOGRAPHY ----------
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* One entry per size, in both families. `fontSize.base` is the text family;
|
|
202
|
+
* `fontSize['p-base']` is the paragraph family, same size with a looser
|
|
203
|
+
* line-height and its own letter-spacing.
|
|
204
|
+
*
|
|
205
|
+
* Objects, not Tailwind's `[size, meta]` tuple form — the tuple is a Tailwind
|
|
206
|
+
* convention and this module does not speak Tailwind. `plugin.js` converts.
|
|
207
|
+
*
|
|
208
|
+
* `letterSpacing` here is the regular weight's. Tracking varies by weight and
|
|
209
|
+
* CSS letter-spacing cannot follow `font-weight`, so per-weight values live in
|
|
210
|
+
* `tracking` and ship as self-contained `text-<size>-<weight>` classes.
|
|
211
|
+
*/
|
|
212
|
+
export const fontSize = buildFontSize()
|
|
213
|
+
|
|
214
|
+
function buildFontSize() {
|
|
215
|
+
const out = {}
|
|
216
|
+
for (const [key, [size, meta]] of Object.entries(typographyTokens.fontSize)) {
|
|
217
|
+
out[key] = { fontSize: size, ...meta }
|
|
218
|
+
}
|
|
219
|
+
// Only these two. The paragraph family is the text family at a looser
|
|
220
|
+
// line-height with its own tracking, and nothing else. Spreading the whole
|
|
221
|
+
// paragraph entry would let a third key added to the Figma export reshape
|
|
222
|
+
// every `p-*` style with no code change and no review.
|
|
223
|
+
for (const [key, p] of Object.entries(typographyTokens.paragraph || {})) {
|
|
224
|
+
if (!out[key]) continue
|
|
225
|
+
out[`p-${key}`] = {
|
|
226
|
+
...out[key],
|
|
227
|
+
lineHeight: p.lineHeight,
|
|
228
|
+
letterSpacing: p.letterSpacing,
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return out
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* `regular` is 420, not 400. Inter's 400 reads too light at frappe-ui's sizes,
|
|
236
|
+
* and 420 is the one real customisation in the scale — the rest are stock
|
|
237
|
+
* Inter weights.
|
|
238
|
+
*/
|
|
239
|
+
export const fontWeight = typographyTokens.fontWeight
|
|
240
|
+
|
|
241
|
+
export const fontFamily = typographyTokens.fontFamily
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Letter-spacing per (size, weight), for the `text` and `paragraph` families
|
|
245
|
+
* separately. The only token property that varies by weight.
|
|
246
|
+
*/
|
|
247
|
+
export const tracking = typographyTokens.tracking
|
|
248
|
+
|
|
249
|
+
// ---------- SIZING ----------
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Every integer 1–128 plus every half step 0.5–19.5, at the canonical 0.25rem
|
|
253
|
+
* step. Stock Tailwind's numeric scale has gaps above 12 (13, 15, 17… are
|
|
254
|
+
* undefined), so `h-17` silently compiles to nothing. Values on the keys
|
|
255
|
+
* Tailwind already defines match its own formula, so filling them is a no-op;
|
|
256
|
+
* the win is the in-between steps.
|
|
257
|
+
*
|
|
258
|
+
* This is the ONE place sizing is declared. Tailwind reads `theme('spacing')`
|
|
259
|
+
* for width, height, size, min/max of both, which is why nothing else
|
|
260
|
+
* declares a sizing block.
|
|
261
|
+
*/
|
|
262
|
+
export const spacing = Object.fromEntries(
|
|
263
|
+
[
|
|
264
|
+
...Array.from({ length: 20 }, (_, i) => i + 0.5),
|
|
265
|
+
...Array.from({ length: 128 }, (_, i) => i + 1),
|
|
266
|
+
]
|
|
267
|
+
.sort((a, b) => a - b)
|
|
268
|
+
.map((n) => [n, `${n * 0.25}rem`]),
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Semantic layout rhythm over the numeric 4px spacing scale. These roles are
|
|
273
|
+
* the shared vocabulary for page gutters, sections, cards, fields and controls.
|
|
274
|
+
*/
|
|
275
|
+
export const layoutSpacing = {
|
|
276
|
+
'control-gap': '0.5rem',
|
|
277
|
+
'control-pad': '0.5rem',
|
|
278
|
+
'field-gap': '0.75rem',
|
|
279
|
+
'card-gap': '1rem',
|
|
280
|
+
'card-pad-compact': '0.75rem',
|
|
281
|
+
'card-pad': '1rem',
|
|
282
|
+
'card-pad-wide': '1.25rem',
|
|
283
|
+
'section-gap': '1.5rem',
|
|
284
|
+
'page-gutter-compact': '1rem',
|
|
285
|
+
'page-gutter': '1.5rem',
|
|
286
|
+
'page-gutter-wide': '2rem',
|
|
287
|
+
'page-stack': '2rem',
|
|
288
|
+
// Region roles codified from the values menus, dialogs, toolbars and data
|
|
289
|
+
// tables already repeat numerically, so density retunes in one place.
|
|
290
|
+
'menu-pad': '0.25rem',
|
|
291
|
+
'dialog-pad': '1rem',
|
|
292
|
+
'table-cell-x': '1rem',
|
|
293
|
+
'table-cell-y': '0.5rem',
|
|
294
|
+
'toolbar-pad': '0.5rem',
|
|
295
|
+
stack: '1rem',
|
|
296
|
+
inline: '0.5rem',
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Breakpoint minimums. Not in the Figma export — decided in code. */
|
|
300
|
+
export const screens = {
|
|
301
|
+
xs: '360px',
|
|
302
|
+
sm: '640px',
|
|
303
|
+
md: '768px',
|
|
304
|
+
lg: '1024px',
|
|
305
|
+
xl: '1280px',
|
|
306
|
+
'2xl': '1536px',
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ---------- CSS VARIABLES ----------
|
|
310
|
+
|
|
311
|
+
function mergeLayers(...layers) {
|
|
312
|
+
const out = { light: {}, dark: {} }
|
|
313
|
+
for (const layer of layers) {
|
|
314
|
+
for (const [theme, vars] of Object.entries(layer)) {
|
|
315
|
+
out[theme] = { ...out[theme], ...vars }
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return out
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function colorVariables() {
|
|
322
|
+
const light = {}
|
|
323
|
+
const dark = {}
|
|
324
|
+
|
|
325
|
+
for (const [category, entries] of Object.entries(semanticColors.light)) {
|
|
326
|
+
for (const [name, value] of Object.entries(entries)) {
|
|
327
|
+
light[`--${category}-${name}`] = value
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
for (const [category, entries] of Object.entries(semanticColors.dark)) {
|
|
331
|
+
for (const [name, value] of Object.entries(entries)) {
|
|
332
|
+
dark[`--${category}-${name}`] = value
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
// Every ramp step is also addressable directly, so a component can reach a
|
|
336
|
+
// primitive the semantic vocabulary doesn't name.
|
|
337
|
+
for (const [family, shades] of Object.entries(colors.light)) {
|
|
338
|
+
for (const [shade, value] of Object.entries(shades)) {
|
|
339
|
+
light[`--${family}-${shade}`] = value
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
for (const [family, shades] of Object.entries(colors.dark)) {
|
|
343
|
+
for (const [shade, value] of Object.entries(shades)) {
|
|
344
|
+
dark[`--dark-${family}-${shade}`] = value
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return { light, dark }
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function effectVariables() {
|
|
352
|
+
const light = {}
|
|
353
|
+
const dark = {}
|
|
354
|
+
|
|
355
|
+
// Off `effectsData`, not off `shadows`: `shadows` carries `none` and
|
|
356
|
+
// `DEFAULT`, which are Tailwind key names with no variable of their own.
|
|
357
|
+
for (const [step, value] of Object.entries(effectsData.elevation.light)) {
|
|
358
|
+
light[`--elevation-${step}`] = value
|
|
359
|
+
}
|
|
360
|
+
for (const [name, value] of Object.entries(effectsData.elevation.custom)) {
|
|
361
|
+
light[`--elevation-${name}`] = value
|
|
362
|
+
}
|
|
363
|
+
// Dark elevation ships: `--elevation-*` re-values under the dark selector
|
|
364
|
+
// (deeper ambient + key shadows, fainter inset highlight), so dark mode no
|
|
365
|
+
// longer renders the light shadow stack unchanged.
|
|
366
|
+
for (const [step, value] of Object.entries(effectsData.elevation.dark)) {
|
|
367
|
+
dark[`--elevation-${step}`] = value
|
|
368
|
+
}
|
|
369
|
+
for (const [name, value] of Object.entries(focusRing.light)) {
|
|
370
|
+
light[`--focus-outline-${name}`] = value
|
|
371
|
+
}
|
|
372
|
+
for (const [name, value] of Object.entries(focusRing.dark)) {
|
|
373
|
+
dark[`--focus-outline-${name}`] = value
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return { light, dark }
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function radiusVariables() {
|
|
380
|
+
const vars = {}
|
|
381
|
+
for (const [key, value] of Object.entries(radius)) {
|
|
382
|
+
vars[`--radius-${key}`] = value
|
|
383
|
+
}
|
|
384
|
+
return { light: vars }
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Every token as a CSS custom property, keyed by theme:
|
|
389
|
+
*
|
|
390
|
+
* cssVariables.light['--surface-base']
|
|
391
|
+
* cssVariables.dark['--surface-base']
|
|
392
|
+
*
|
|
393
|
+
* `light` goes on `:root`, `dark` on `[data-theme="dark"]`. Neither is a
|
|
394
|
+
* subset of the other. `dark` re-values the semantic, elevation and focus
|
|
395
|
+
* properties that `light` already declares, and adds the dark ramps under
|
|
396
|
+
* their own `--dark-*` names. Those are the only keys it has that `light`
|
|
397
|
+
* lacks. `light` additionally carries the light ramps (`--gray-500`) and
|
|
398
|
+
* radius, which do not flip by theme, so a property `dark` leaves out keeps
|
|
399
|
+
* its `:root` value.
|
|
400
|
+
*
|
|
401
|
+
* `plugin.js` emits both into the base layer, which is how a frappe-ui page
|
|
402
|
+
* gets them. Read this map directly when you need a token's value somewhere
|
|
403
|
+
* that does not load frappe-ui's stylesheet, such as exported markup or a
|
|
404
|
+
* canvas renderer.
|
|
405
|
+
*/
|
|
406
|
+
export const cssVariables = mergeLayers(
|
|
407
|
+
colorVariables(),
|
|
408
|
+
effectVariables(),
|
|
409
|
+
radiusVariables(),
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
/** Stacking and motion scales — the semantic layers components name. */
|
|
413
|
+
export { duration, easing, zIndex } from './tokens/layers.js'
|