@talelk/newui-tokens 0.1.0 → 0.2.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@talelk/newui-tokens",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "Framework-neutral design tokens (colors, radius, typography, spacing, elevation, focus).",
6
6
  "exports": {
package/src/index.d.ts CHANGED
@@ -33,6 +33,10 @@ export const radiusRoles: Record<
33
33
  >
34
34
 
35
35
  export const shadows: Record<string, string>
36
+ export const shadowRoles: Record<
37
+ 'card' | 'raised' | 'popover' | 'dialog' | 'toast' | 'drag',
38
+ string
39
+ >
36
40
 
37
41
  export const focusRing: {
38
42
  light: Record<string, string>
package/src/index.js CHANGED
@@ -27,11 +27,28 @@
27
27
  * none may be renamed or removed.
28
28
  */
29
29
 
30
- import colorsData from './tokens/colors.js'
30
+ import exportedColors from './tokens/colors.js'
31
31
  import effectsData from './tokens/effects.js'
32
+ import * as instrument from './tokens/instrument.js'
32
33
  import radiusTokens from './tokens/radius.js'
33
34
  import typographyTokens from './tokens/typography.js'
34
35
 
36
+ // The Figma export stays untouched; the instrument-panel layer replaces ramp
37
+ // steps on a copy, before anything resolves against them (ADR-0016).
38
+ const colorsData = {
39
+ ...exportedColors,
40
+ lightMode: withRamps(exportedColors.lightMode, instrument.ramps.lightMode),
41
+ darkMode: withRamps(exportedColors.darkMode, instrument.ramps.darkMode),
42
+ }
43
+
44
+ function withRamps(palette, overrides) {
45
+ const out = { ...palette }
46
+ for (const [family, steps] of Object.entries(overrides)) {
47
+ out[family] = { ...palette[family], ...steps }
48
+ }
49
+ return out
50
+ }
51
+
35
52
  // ---------- COLORS ----------
36
53
 
37
54
  /**
@@ -88,6 +105,23 @@ export const semanticColors = {
88
105
  dark: resolveThemedLayer(colorsData.themedVariables.dark),
89
106
  }
90
107
 
108
+ // Mode-flipping scrim (alias layer — the generated `colors.js` stays
109
+ // untouched). One `--surface-scrim` runtime var replaces every hand-copied
110
+ // `bg-black-overlay-200 dark:bg-black-overlay-700` backdrop pair, so overlays
111
+ // name the role and the theme flip lives here, never in a `dark:` variant.
112
+ semanticColors.light.surface.scrim = colorsData.overlay.black[200]
113
+ semanticColors.dark.surface.scrim = colorsData.overlay.black[700]
114
+
115
+ // Instrument-panel semantic entries: the Mist frame (`surface-canvas`) behind
116
+ // sheets, shared by the sidebar.
117
+ for (const theme of ['light', 'dark']) {
118
+ for (const [category, entries] of Object.entries(
119
+ instrument.semantic[theme],
120
+ )) {
121
+ Object.assign(semanticColors[theme][category], entries)
122
+ }
123
+ }
124
+
91
125
  // ---------- RADIUS ----------
92
126
 
93
127
  /**
@@ -101,9 +135,9 @@ export const semanticColors = {
101
135
  * recipe independently.
102
136
  */
103
137
  export const radiusRoles = {
104
- control: radiusTokens[3],
105
- card: radiusTokens[4],
106
- overlay: radiusTokens[6],
138
+ control: instrument.radiusRoles.control,
139
+ card: instrument.radiusRoles.card,
140
+ overlay: instrument.radiusRoles.overlay,
107
141
  pill: radiusTokens.full,
108
142
  }
109
143
 
@@ -120,10 +154,11 @@ export const radius = { ...radiusTokens, ...radiusRoles }
120
154
  * frappe-ui stylesheet can still paint the shadow. `plugin.js` reads the key
121
155
  * list from here and points each Tailwind key at its variable.
122
156
  *
123
- * Only the light steps ship. Espresso 2.0 references `elevation/light/*` on
124
- * its dark-mode page too, so `elevation.dark` in `effects.js` is not what
125
- * frappe-ui renders and is not exported. Focus is a separate shape, and a
126
- * different CSS property: see `focusRing`.
157
+ * This flat map carries the light steps (a consumer with no stylesheet picks
158
+ * a side itself); the runtime `--elevation-*` variables in `cssVariables`
159
+ * additionally ship `elevation.dark` under the dark selector, so a themed
160
+ * page gets dark-tuned depth. Focus is a separate shape, and a different CSS
161
+ * property: see `focusRing`.
127
162
  */
128
163
  export const shadows = buildShadows()
129
164
 
@@ -140,6 +175,21 @@ function buildShadows() {
140
175
  return { ...out, ...effectsData.elevation.custom }
141
176
  }
142
177
 
178
+ /**
179
+ * Stable component roles mapped onto the primitive elevation steps, the
180
+ * shadow counterpart of `radiusRoles`. Components name the role
181
+ * (`shadow-popover`) so retuning a surface family's depth is one edit here,
182
+ * not a sweep of every recipe's step pick.
183
+ */
184
+ export const shadowRoles = {
185
+ card: 'sm',
186
+ raised: 'base',
187
+ popover: 'md',
188
+ dialog: 'lg',
189
+ toast: 'xl',
190
+ drag: '2xl',
191
+ }
192
+
143
193
  // Focus tokens arrive as single-layer `0 0 0 <spread> <color>` shadows.
144
194
  // Re-express as an `outline` shorthand so the focus ring never collides with
145
195
  // a `shadow-*` utility on the same element and survives forced-colors mode
@@ -168,8 +218,8 @@ function toOutlines(layer) {
168
218
  * dark ones 3px, which is why this one does mode-swap and `shadows` does not.
169
219
  */
170
220
  export const focusRing = {
171
- light: toOutlines(effectsData.focus.light),
172
- dark: toOutlines(effectsData.focus.dark),
221
+ light: toOutlines({ ...effectsData.focus.light, ...instrument.focus.light }),
222
+ dark: toOutlines({ ...effectsData.focus.dark, ...instrument.focus.dark }),
173
223
  }
174
224
 
175
225
  // ---------- TYPOGRAPHY ----------
@@ -191,7 +241,7 @@ export const fontSize = buildFontSize()
191
241
  function buildFontSize() {
192
242
  const out = {}
193
243
  for (const [key, [size, meta]] of Object.entries(typographyTokens.fontSize)) {
194
- out[key] = { fontSize: size, ...meta }
244
+ out[key] = { fontSize: size, ...meta, ...instrument.fontSize[key] }
195
245
  }
196
246
  // Only these two. The paragraph family is the text family at a looser
197
247
  // line-height with its own tracking, and nothing else. Spreading the whole
@@ -203,6 +253,7 @@ function buildFontSize() {
203
253
  ...out[key],
204
254
  lineHeight: p.lineHeight,
205
255
  letterSpacing: p.letterSpacing,
256
+ ...instrument.fontSize[`p-${key}`],
206
257
  }
207
258
  }
208
259
  return out
@@ -215,13 +266,34 @@ function buildFontSize() {
215
266
  */
216
267
  export const fontWeight = typographyTokens.fontWeight
217
268
 
218
- export const fontFamily = typographyTokens.fontFamily
269
+ export const fontFamily = {
270
+ ...typographyTokens.fontFamily,
271
+ ...instrument.fontFamily,
272
+ }
219
273
 
220
274
  /**
221
275
  * Letter-spacing per (size, weight), for the `text` and `paragraph` families
222
276
  * separately. The only token property that varies by weight.
223
277
  */
224
- export const tracking = typographyTokens.tracking
278
+ export const tracking = retrack(typographyTokens.tracking)
279
+
280
+ // Plex wants the size's own tracking at every weight: the export's per-weight
281
+ // spread was tuned for Inter.
282
+ function retrack(table) {
283
+ const out = {}
284
+ for (const [group, sizes] of Object.entries(table)) {
285
+ out[group] = {}
286
+ for (const [size, weights] of Object.entries(sizes)) {
287
+ const key = group === 'paragraph' ? `p-${size}` : size
288
+ const value = instrument.trackingFor(key)
289
+ out[group][size] =
290
+ value === undefined
291
+ ? weights
292
+ : Object.fromEntries(Object.keys(weights).map((w) => [w, value]))
293
+ }
294
+ }
295
+ return out
296
+ }
225
297
 
226
298
  // ---------- SIZING ----------
227
299
 
@@ -262,14 +334,25 @@ export const layoutSpacing = {
262
334
  'page-gutter': '1.5rem',
263
335
  'page-gutter-wide': '2rem',
264
336
  'page-stack': '2rem',
337
+ // Region roles codified from the values menus, dialogs, toolbars and data
338
+ // tables already repeat numerically, so density retunes in one place.
339
+ 'menu-pad': '0.25rem',
340
+ 'dialog-pad': '1rem',
341
+ 'table-cell-x': '0.625rem',
342
+ 'table-cell-y': '0.25rem',
343
+ 'toolbar-pad': '0.5rem',
344
+ stack: '1rem',
345
+ inline: '0.5rem',
265
346
  }
266
347
 
267
348
  /** Breakpoint minimums. Not in the Figma export — decided in code. */
268
349
  export const screens = {
350
+ xs: '360px',
269
351
  sm: '640px',
270
352
  md: '768px',
271
353
  lg: '1024px',
272
354
  xl: '1280px',
355
+ '2xl': '1536px',
273
356
  }
274
357
 
275
358
  // ---------- CSS VARIABLES ----------
@@ -326,6 +409,12 @@ function effectVariables() {
326
409
  for (const [name, value] of Object.entries(effectsData.elevation.custom)) {
327
410
  light[`--elevation-${name}`] = value
328
411
  }
412
+ // Dark elevation ships: `--elevation-*` re-values under the dark selector
413
+ // (deeper ambient + key shadows, fainter inset highlight), so dark mode no
414
+ // longer renders the light shadow stack unchanged.
415
+ for (const [step, value] of Object.entries(effectsData.elevation.dark)) {
416
+ dark[`--elevation-${step}`] = value
417
+ }
329
418
  for (const [name, value] of Object.entries(focusRing.light)) {
330
419
  light[`--focus-outline-${name}`] = value
331
420
  }
@@ -351,12 +440,12 @@ function radiusVariables() {
351
440
  * cssVariables.dark['--surface-base']
352
441
  *
353
442
  * `light` goes on `:root`, `dark` on `[data-theme="dark"]`. Neither is a
354
- * subset of the other. `dark` re-values the semantic and focus properties
355
- * that `light` already declares, and adds the dark ramps under their own
356
- * `--dark-*` names. Those are the only keys it has that `light` lacks.
357
- * `light` additionally carries the light ramps (`--gray-500`), elevation and
358
- * radius. None of those three flip by theme, so a property `dark` leaves out
359
- * keeps its `:root` value.
443
+ * subset of the other. `dark` re-values the semantic, elevation and focus
444
+ * properties that `light` already declares, and adds the dark ramps under
445
+ * their own `--dark-*` names. Those are the only keys it has that `light`
446
+ * lacks. `light` additionally carries the light ramps (`--gray-500`) and
447
+ * radius, which do not flip by theme, so a property `dark` leaves out keeps
448
+ * its `:root` value.
360
449
  *
361
450
  * `plugin.js` emits both into the base layer, which is how a frappe-ui page
362
451
  * gets them. Read this map directly when you need a token's value somewhere
@@ -0,0 +1,155 @@
1
+ /**
2
+ * The instrument-panel layer: the hand-authored retune that sits on top of the
3
+ * Figma export (`colors.js`, `radius.js`, `typography.js` stay generated and
4
+ * untouched — see ADR-0016).
5
+ *
6
+ * The kit is for people who spend the day inside records: dense tables,
7
+ * filters, forms. This layer encodes that brief as data:
8
+ *
9
+ * - a cool gray family (Mist frame, white Sheet for data, Graphite ink, Pewter
10
+ * secondary ink, Rule hairlines) instead of the export's near-neutral gray;
11
+ * - the `blue` theme retuned to Petrol — the kit's one accent: primary
12
+ * actions, focus, the active row;
13
+ * - IBM Plex Sans (one variable family; its condensed width serves narrow
14
+ * columns, its default figures are tabular, so numbers align in columns);
15
+ * - a type scale one step tighter (13px interface text) with no positive
16
+ * tracking, which suited Inter and only loosens Plex;
17
+ * - radius by role: 4px controls, 6px sheets, 8px overlays.
18
+ *
19
+ * Every value is a replacement for a name that already exists, except
20
+ * `surface.canvas` (the frame behind sheets) — the token surface stays
21
+ * additive-only (ADR-0010).
22
+ */
23
+
24
+ /** Ramp steps replaced in the exported palettes, per theme. */
25
+ export const ramps = {
26
+ lightMode: {
27
+ gray: {
28
+ 50: 'oklch(0.976 0.003 264.5)', // #F6F7F9
29
+ 100: 'oklch(0.954 0.005 247.9)', // #EDF0F3 Mist
30
+ 200: 'oklch(0.923 0.008 253.9)', // #E2E6EB
31
+ 300: 'oklch(0.883 0.012 252.1)', // #D3D9E0 Rule
32
+ 400: 'oklch(0.791 0.017 250.9)', // #B3BCC6
33
+ 500: 'oklch(0.665 0.023 252.6)', // #8A95A2
34
+ 600: 'oklch(0.612 0.025 254.5)', // #7A8593 muted tier, kept under AA
35
+ 700: 'oklch(0.489 0.028 256.2)', // #566170 Pewter
36
+ 800: 'oklch(0.364 0.024 255.7)', // #363F4B
37
+ 900: 'oklch(0.241 0.024 256.4)', // #18202B Graphite
38
+ 950: 'oklch(0.189 0.019 255.8)', // #0E141C
39
+ },
40
+ blue: {
41
+ 50: 'oklch(0.968 0.009 214.3)', // #EEF6F8
42
+ 100: 'oklch(0.933 0.018 218.4)', // #DCECF1
43
+ 200: 'oklch(0.869 0.034 217.8)', // #BCDAE3
44
+ 300: 'oklch(0.778 0.056 218.7)', // #8FC0CF
45
+ 400: 'oklch(0.668 0.076 219.7)', // #5BA0B5
46
+ 500: 'oklch(0.558 0.087 221.8)', // #2C7F98
47
+ 600: 'oklch(0.492 0.085 223.3)', // #176B84
48
+ 700: 'oklch(0.443 0.079 224.9)', // #0E5C74 Petrol
49
+ 800: 'oklch(0.389 0.069 224.6)', // #0B4C60
50
+ 900: 'oklch(0.333 0.057 224.7)', // #0A3C4C
51
+ 950: 'oklch(0.267 0.044 225.3)', // #072A36
52
+ },
53
+ },
54
+ darkMode: {
55
+ gray: {
56
+ 50: 'oklch(0.966 0.005 247.9)', // #F1F4F7
57
+ 100: 'oklch(0.926 0.009 247.9)', // #E2E7EC
58
+ 200: 'oklch(0.848 0.015 251.2)', // #C6CED7
59
+ 300: 'oklch(0.744 0.021 252.9)', // #A3ADB9
60
+ 400: 'oklch(0.589 0.026 256.2)', // #737E8D muted tier, kept under AA
61
+ 450: 'oklch(0.562 0.025 254.5)', // #6B7684
62
+ 500: 'oklch(0.459 0.025 252.7)', // #4E5966
63
+ 600: 'oklch(0.375 0.025 253.1)', // #38424E
64
+ 700: 'oklch(0.317 0.023 253.6)', // #2A333E
65
+ 800: 'oklch(0.266 0.021 257.3)', // #1F2630
66
+ 900: 'oklch(0.233 0.020 258.4)', // #181E27
67
+ 950: 'oklch(0.203 0.016 256.8)', // #12171E sheet
68
+ },
69
+ blue: {
70
+ 50: 'oklch(0.953 0.018 214.4)', // #E3F3F7
71
+ 100: 'oklch(0.897 0.038 215.5)', // #C2E4ED
72
+ 200: 'oklch(0.819 0.065 216.2)', // #93CFDF
73
+ 300: 'oklch(0.732 0.087 216.9)', // #62B6CC Petrol, lit
74
+ 400: 'oklch(0.642 0.093 219.1)', // #3F9AB3
75
+ 500: 'oklch(0.557 0.087 220.5)', // #2A7F97
76
+ 600: 'oklch(0.476 0.077 221.3)', // #1D667B
77
+ 700: 'oklch(0.410 0.066 222.2)', // #165264
78
+ 800: 'oklch(0.344 0.054 222.1)', // #113F4D
79
+ 900: 'oklch(0.290 0.044 222.4)', // #0D303B
80
+ 950: 'oklch(0.241 0.034 222.0)', // #0A232B
81
+ },
82
+ },
83
+ }
84
+
85
+ /**
86
+ * Semantic entries set after the ramps resolve. `canvas` is the Mist frame the
87
+ * app shell paints behind its sheets; the sidebar shares it so the frame reads
88
+ * as one continuous surface.
89
+ */
90
+ export const semantic = {
91
+ light: {
92
+ surface: {
93
+ canvas: 'oklch(0.954 0.005 247.9)', // Mist
94
+ sidebar: 'oklch(0.954 0.005 247.9)',
95
+ },
96
+ },
97
+ dark: {
98
+ surface: {
99
+ canvas: 'oklch(0.171 0.012 254.2)', // #0C1015, below the sheet
100
+ sidebar: 'oklch(0.171 0.012 254.2)',
101
+ },
102
+ },
103
+ }
104
+
105
+ /** The interface family. Declared by `@talelk/newui-tailwind`'s font CSS. */
106
+ export const fontFamily = { text: 'IBM Plex Sans' }
107
+
108
+ /**
109
+ * Type sizes replaced in the exported scale: 13px interface text, quieter
110
+ * headings that lean on weight rather than size.
111
+ */
112
+ export const fontSize = {
113
+ interface: { fontSize: '13px', lineHeight: '18px', letterSpacing: '0em' },
114
+ caption: { fontSize: '12px', lineHeight: '16px', letterSpacing: '0em' },
115
+ section: { fontSize: '15px', lineHeight: '20px', letterSpacing: '-0.005em' },
116
+ heading: { fontSize: '20px', lineHeight: '26px', letterSpacing: '-0.015em' },
117
+ metric: { fontSize: '22px', lineHeight: '28px', letterSpacing: '-0.02em' },
118
+ hero: { fontSize: '32px', lineHeight: '38px', letterSpacing: '-0.025em' },
119
+ base: { fontSize: '13px', lineHeight: '18px', letterSpacing: '0em' },
120
+ sm: { fontSize: '12px', lineHeight: '16px', letterSpacing: '0em' },
121
+ xs: { fontSize: '11px', lineHeight: '14px', letterSpacing: '0.005em' },
122
+ '2xs': { fontSize: '10.5px', lineHeight: '13px', letterSpacing: '0.01em' },
123
+ md: { fontSize: '14px', lineHeight: '20px', letterSpacing: '0em' },
124
+ lg: { fontSize: '15px', lineHeight: '20px', letterSpacing: '0em' },
125
+ 'p-base': { fontSize: '13px', lineHeight: '20px', letterSpacing: '0em' },
126
+ 'p-sm': { fontSize: '12px', lineHeight: '18px', letterSpacing: '0em' },
127
+ }
128
+
129
+ /** Plex needs no positive tracking at text sizes; weights share the size's. */
130
+ export function trackingFor(size) {
131
+ return fontSize[size]?.letterSpacing
132
+ }
133
+
134
+ /** Radius by role: 4px controls, 6px sheets and cards, 8px overlays. */
135
+ export const radiusRoles = {
136
+ control: '4px',
137
+ card: '6px',
138
+ overlay: '8px',
139
+ }
140
+
141
+ /**
142
+ * Focus rings, as the export's `0 0 0 <spread> <color>` shapes. The export's
143
+ * default ring is light gray (about 1.6:1 on white); here focus is the accent,
144
+ * solid, 3:1 or better against both the sheet and the frame.
145
+ */
146
+ export const focus = {
147
+ light: {
148
+ default: '0px 0px 0px 2px #0E5C74',
149
+ blue: '0px 0px 0px 2px #0E5C74',
150
+ },
151
+ dark: {
152
+ default: '0px 0px 0px 2px #62B6CC',
153
+ blue: '0px 0px 0px 2px #62B6CC',
154
+ },
155
+ }
@@ -4,7 +4,8 @@
4
4
  * Components never hard-code a z-index or a transition duration: they name the
5
5
  * layer's role or the speed's role, and this scale owns the number. Six stacked
6
6
  * layers cover the kit — content, raised chrome, floating chrome, overlays and
7
- * the topmost hover layer — and three durations cover transition speeds.
7
+ * the topmost hover layer — and the named durations cover every transition and
8
+ * animation speed a component may use.
8
9
  */
9
10
  export const zIndex = {
10
11
  raised: 10,
@@ -14,12 +15,21 @@ export const zIndex = {
14
15
  }
15
16
 
16
17
  export const duration = {
18
+ instant: '75ms',
17
19
  fast: '100ms',
18
20
  base: '150ms',
21
+ moderate: '200ms',
19
22
  slow: '300ms',
23
+ progress: '660ms',
20
24
  }
21
25
 
22
26
  export const easing = {
23
27
  /** The kit's standard ease-in-out curve (progress fills, drawers). */
24
28
  standard: 'cubic-bezier(0.65, 0, 0.35, 1)',
29
+ /** Enter/disclosure: fast arrival, gentle settle (disclosure-panel, overlays). */
30
+ out: 'cubic-bezier(0.23, 1, 0.32, 1)',
31
+ /** Exit: slow departure, fast finish. */
32
+ in: 'cubic-bezier(0.5, 0, 0.75, 0)',
33
+ /** The standard curve under Tailwind's conventional name. */
34
+ 'in-out': 'cubic-bezier(0.65, 0, 0.35, 1)',
25
35
  }