@rokkit/core 1.0.5 → 1.1.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/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from "./types.js";
2
2
  export * from "./constants.js";
3
+ export * from "./named-tokens.ts";
3
4
  export * from "./utils.js";
4
5
  export * from "./nested.js";
5
6
  export * from "./string.js";
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The 24 canonical named tokens that constitute the "core" emit vocabulary.
3
+ * Order is significant for tests and for predictable CSS-var output ordering.
4
+ */
5
+ export declare const NAMED_TOKENS: readonly ["paper", "paper-soft", "paper-mute", "paper-edge", "ink", "ink-mute", "ink-soft", "ink-faint", "primary", "on-primary", "accent", "accent-soft", "success", "success-soft", "warning", "warning-soft", "danger", "danger-soft", "error", "error-soft", "info", "info-soft", "focus-ring", "shadow-tint"];
6
+ export type NamedToken = (typeof NAMED_TOKENS)[number];
7
+ export type SkinRole = 'surface' | 'ink' | 'primary' | 'accent' | 'success' | 'warning' | 'danger' | 'error' | 'info';
8
+ export type ZSlot = 'z0' | 'z1' | 'z2' | 'z3' | 'z4' | 'z5' | 'z6' | 'z7' | 'z8' | 'z9' | 'z10';
9
+ export declare const Z_SLOTS: ZSlot[];
10
+ /**
11
+ * Maps each named token to the palette shade index that backs it.
12
+ * 'derived' indicates the token is computed from another palette (e.g., on-primary
13
+ * picks the paper shade of the surface palette by default for white-on-primary).
14
+ *
15
+ * paper: 50 (lightest surface tone, canvas)
16
+ * paper-soft: 100 (card background)
17
+ * paper-mute: 200 (subdued panel)
18
+ * paper-edge: 400 (hairline border tone)
19
+ * ink: 900 (primary text)
20
+ * ink-mute: 700 (secondary text)
21
+ * ink-soft: 500 (placeholder)
22
+ * ink-faint: 300 (disabled)
23
+ * primary: 500 (CTA fill)
24
+ * on-primary: derived — surface.50 default (white-on-primary)
25
+ * accent / status: 500 (solid)
26
+ * *-soft: 100 (tinted-bg companion)
27
+ * focus-ring: 500 of accent
28
+ * shadow-tint: 900 of ink
29
+ */
30
+ export declare const NAMED_TOKEN_SHADE_MAP: Record<NamedToken, number | 'derived'>;
31
+ /**
32
+ * Maps each named token to the skin role whose palette it draws from.
33
+ */
34
+ export declare const NAMED_TOKEN_ROLE_MAP: Record<NamedToken, SkinRole>;
35
+ /**
36
+ * Surface z-slot → named slot. Used to emit back-compat aliases like
37
+ * `--color-surface-z1: var(--paper-soft);` in core mode.
38
+ *
39
+ * z2/z3 collapse to paper-mute, z5/z6 to ink-soft, z7/z8 to ink-mute,
40
+ * z9/z10 to ink. This collapse is the contract: 4 surface tones in core mode.
41
+ */
42
+ export declare const Z_COLLAPSE_MAP_SURFACE: Record<ZSlot, NamedToken>;
43
+ /**
44
+ * Ink role uses the inverted z-scale (z0 = darkest = primary text).
45
+ * In core mode, the same 4+4 named ladder is reused; this table just inverts.
46
+ */
47
+ export declare const Z_COLLAPSE_MAP_INK: Record<ZSlot, NamedToken>;
48
+ /**
49
+ * Returns true when the role has a `-soft` companion in the named vocabulary.
50
+ * Used to decide whether the role's tinted-background slot collapses to
51
+ * `var(--{role}-soft)` or to `var(--{role})`.
52
+ */
53
+ export declare function hasSoftCompanion(role: string): boolean;
54
+ export declare function shadeForNamedToken(name: string): number | 'derived' | undefined;
55
+ export declare function roleForNamedToken(name: string): SkinRole | undefined;
package/dist/theme.d.ts CHANGED
@@ -80,6 +80,60 @@ export declare class Theme {
80
80
  mapVariant(color: any, variant: any): {};
81
81
  getColorRules(mapping?: null): {};
82
82
  getPalette(mapping?: null): {};
83
+ /**
84
+ * Returns the named-token layer as a map of `--{name}: <resolved value>` entries.
85
+ *
86
+ * Palette values are inlined — there is no `var(--color-{role}-{shade})` indirection.
87
+ *
88
+ * `on-primary` is derived: it picks the paper shade (50) of the surface palette so the
89
+ * default contrast pair is "primary fill + white-ish text." Skins that need a different
90
+ * pair should override via custom tokens.
91
+ *
92
+ * `*-soft` variants resolve to shade 100 of their role's palette.
93
+ * `shadow-tint` resolves to shade 900 of the ink palette.
94
+ *
95
+ * @param {'light' | 'dark'} _mode — accepted for symmetry with future
96
+ * dark-aware derivations. Today the Theme is constructed with a mode-resolved
97
+ * mapping, so this parameter is informational.
98
+ * @param perRoleModes — optional map of role → 'core' | 'extended'. When a token's
99
+ * role is 'extended', the value is a `var(--color-{role}-{shade})` alias instead
100
+ * of the inlined palette value. Defaults to 'core' for all unspecified roles
101
+ * (and for the whole map when undefined).
102
+ */
103
+ getNamedTokens(_mode?: string, perRoleModes?: Record<string, 'core' | 'extended'>): Record<string, string>;
104
+ /**
105
+ * Returns z-alias CSS-var assignments for back-compat in core mode.
106
+ * `--color-{role}-z{n}` → `var(--{namedSlot})`.
107
+ *
108
+ * - surface: uses Z_COLLAPSE_MAP_SURFACE (z2/z3 → paper-mute, z9/z10 → ink, etc.)
109
+ * - ink: uses Z_COLLAPSE_MAP_INK (inverted scale)
110
+ * - primary/accent/status: tint-vs-solid 2-state collapse.
111
+ * z0–z2 → role-soft (if it exists) else role; z3+ → role.
112
+ *
113
+ * This is the back-compat layer: existing `@apply bg-surface-z3` etc. resolves
114
+ * through these aliases until consumers migrate to the named vocabulary.
115
+ */
116
+ /** Accepts SkinRole and any custom skin roles beyond the SkinRole literal union. */
117
+ getZAliasesForCore(role: string): Record<string, string>;
118
+ /**
119
+ * Returns named tokens as CSS-var aliases pointing at the palette layer.
120
+ * Used in extended mode where the preset emits the full --color-{role}-{shade}
121
+ * palette; named tokens become thin syntactic aliases over the palette.
122
+ *
123
+ * `on-primary` aliases to `var(--color-surface-50)` (derived: paper of surface,
124
+ * matching the core-mode resolution).
125
+ */
126
+ getZAliasesForExtended(): Record<string, string>;
127
+ /**
128
+ * Emit the full --color-{role}-{shade} palette CSS vars for a single role.
129
+ * Mirrors what getPalette does for all roles, but scoped to one.
130
+ */
131
+ getPaletteForRole(role: string): Record<string, string>;
132
+ /**
133
+ * Emit z-aliases for one role in extended mode — they point at the palette vars,
134
+ * using TONE_MAP for surface/non-inverted roles and the inverted map for ink.
135
+ */
136
+ getZAliasesForRoleExtended(role: string): Record<string, string>;
83
137
  getShortcuts(name: any): (string[] | (RegExp | (([, variant, end]: [any, any, any]) => string))[] | (RegExp | (([, end]: [any, any]) => string))[])[];
84
138
  /**
85
139
  * Generates CSS custom property declarations for the semantic z0–z10 tone scale.
package/dist/utils.d.ts CHANGED
@@ -101,9 +101,16 @@ export function hex2oklch(hex: string): string;
101
101
  */
102
102
  export function oklch2hex(L: number, C: number, H: number): string;
103
103
  /**
104
- * Detects whether an icon value is a CSS class (i-*) or literal text (kanji, emoji, etc.)
104
+ * Detects whether an icon value is a CSS class or a literal character
105
+ * (kanji, emoji, single letter, etc.).
106
+ *
107
+ * Anything more than one grapheme is treated as a class — including bare
108
+ * semantic names like `file-svelte` which UnoCSS expands to the configured
109
+ * icon collection via `iconShortcuts(DEFAULT_ICONS, …)`. Single characters
110
+ * (kanji, emoji, single letters) render as literal text.
111
+ *
105
112
  * @param {string | null | undefined} icon
106
- * @returns {boolean} true if icon is a CSS class string
113
+ * @returns {boolean} true if icon should render as a CSS class
107
114
  */
108
115
  export function isIconClass(icon: string | null | undefined): boolean;
109
116
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rokkit/core",
3
- "version": "1.0.5",
3
+ "version": "1.1.0",
4
4
  "description": "Contains core utility functions and classes that can be used in various components.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -46,6 +46,6 @@
46
46
  "ramda": "^0.32.0"
47
47
  },
48
48
  "devDependencies": {
49
- "@rokkit/icons": "1.0.5"
49
+ "@rokkit/icons": "1.1.0"
50
50
  }
51
51
  }
package/src/constants.js CHANGED
@@ -79,6 +79,7 @@ export const DEFAULT_ICONS = [
79
79
  'action-close',
80
80
  'action-copy',
81
81
  'action-copysuccess',
82
+ 'action-download',
82
83
  'action-check',
83
84
  'action-pin',
84
85
  'action-save',
@@ -87,6 +88,20 @@ export const DEFAULT_ICONS = [
87
88
  'node-closed',
88
89
  'folder-opened',
89
90
  'folder-closed',
91
+ 'doc-default',
92
+ 'doc-svelte',
93
+ 'doc-js',
94
+ 'doc-ts',
95
+ 'doc-css',
96
+ 'doc-html',
97
+ 'doc-json',
98
+ 'doc-md',
99
+ 'doc-text',
100
+ 'doc-pdf',
101
+ 'doc-archive',
102
+ 'media-image',
103
+ 'media-video',
104
+ 'media-audio',
90
105
  'selector-opened',
91
106
  'selector-closed',
92
107
  'menu-opened',
@@ -133,7 +148,9 @@ export const DEFAULT_ICONS = [
133
148
  'align-vertical-bottom',
134
149
  'align-vertical-middle',
135
150
  'view-code',
136
- 'view-chart'
151
+ 'view-chart',
152
+ 'view-preview',
153
+ 'view-off'
137
154
  ]
138
155
 
139
156
  export const DEFAULT_OPTIONS = {
package/src/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  export * from './types.js'
3
3
  // skipcq: JS-E1004 - Needed for exposing all constants
4
4
  export * from './constants.js'
5
+ export * from './named-tokens.ts'
5
6
  // skipcq: JS-E1004 - Needed for exposing all functions
6
7
  export * from './utils.js'
7
8
  // skipcq: JS-E1004 - Needed for exposing all functions
@@ -0,0 +1,161 @@
1
+ /**
2
+ * The 24 canonical named tokens that constitute the "core" emit vocabulary.
3
+ * Order is significant for tests and for predictable CSS-var output ordering.
4
+ */
5
+ export const NAMED_TOKENS = [
6
+ 'paper', 'paper-soft', 'paper-mute', 'paper-edge',
7
+ 'ink', 'ink-mute', 'ink-soft', 'ink-faint',
8
+ 'primary', 'on-primary',
9
+ 'accent', 'accent-soft',
10
+ 'success', 'success-soft',
11
+ 'warning', 'warning-soft',
12
+ 'danger', 'danger-soft',
13
+ 'error', 'error-soft',
14
+ 'info', 'info-soft',
15
+ 'focus-ring', 'shadow-tint'
16
+ ] as const
17
+
18
+ export type NamedToken = (typeof NAMED_TOKENS)[number]
19
+
20
+ export type SkinRole =
21
+ | 'surface' | 'ink' | 'primary' | 'accent'
22
+ | 'success' | 'warning' | 'danger' | 'error' | 'info'
23
+
24
+ export type ZSlot = 'z0' | 'z1' | 'z2' | 'z3' | 'z4' | 'z5' | 'z6' | 'z7' | 'z8' | 'z9' | 'z10'
25
+
26
+ export const Z_SLOTS: ZSlot[] = ['z0', 'z1', 'z2', 'z3', 'z4', 'z5', 'z6', 'z7', 'z8', 'z9', 'z10']
27
+
28
+ /**
29
+ * Maps each named token to the palette shade index that backs it.
30
+ * 'derived' indicates the token is computed from another palette (e.g., on-primary
31
+ * picks the paper shade of the surface palette by default for white-on-primary).
32
+ *
33
+ * paper: 50 (lightest surface tone, canvas)
34
+ * paper-soft: 100 (card background)
35
+ * paper-mute: 200 (subdued panel)
36
+ * paper-edge: 400 (hairline border tone)
37
+ * ink: 900 (primary text)
38
+ * ink-mute: 700 (secondary text)
39
+ * ink-soft: 500 (placeholder)
40
+ * ink-faint: 300 (disabled)
41
+ * primary: 500 (CTA fill)
42
+ * on-primary: derived — surface.50 default (white-on-primary)
43
+ * accent / status: 500 (solid)
44
+ * *-soft: 100 (tinted-bg companion)
45
+ * focus-ring: 500 of accent
46
+ * shadow-tint: 900 of ink
47
+ */
48
+ export const NAMED_TOKEN_SHADE_MAP: Record<NamedToken, number | 'derived'> = {
49
+ paper: 50,
50
+ 'paper-soft': 100,
51
+ 'paper-mute': 200,
52
+ 'paper-edge': 400,
53
+ ink: 900,
54
+ 'ink-mute': 700,
55
+ 'ink-soft': 500,
56
+ 'ink-faint': 300,
57
+ primary: 500,
58
+ 'on-primary': 'derived',
59
+ accent: 500,
60
+ 'accent-soft': 100,
61
+ success: 500,
62
+ 'success-soft': 100,
63
+ warning: 500,
64
+ 'warning-soft': 100,
65
+ danger: 500,
66
+ 'danger-soft': 100,
67
+ error: 500,
68
+ 'error-soft': 100,
69
+ info: 500,
70
+ 'info-soft': 100,
71
+ 'focus-ring': 500,
72
+ 'shadow-tint': 900
73
+ }
74
+
75
+ /**
76
+ * Maps each named token to the skin role whose palette it draws from.
77
+ */
78
+ export const NAMED_TOKEN_ROLE_MAP: Record<NamedToken, SkinRole> = {
79
+ paper: 'surface',
80
+ 'paper-soft': 'surface',
81
+ 'paper-mute': 'surface',
82
+ 'paper-edge': 'surface',
83
+ ink: 'ink',
84
+ 'ink-mute': 'ink',
85
+ 'ink-soft': 'ink',
86
+ 'ink-faint': 'ink',
87
+ primary: 'primary',
88
+ 'on-primary': 'primary',
89
+ accent: 'accent',
90
+ 'accent-soft': 'accent',
91
+ success: 'success',
92
+ 'success-soft': 'success',
93
+ warning: 'warning',
94
+ 'warning-soft': 'warning',
95
+ danger: 'danger',
96
+ 'danger-soft': 'danger',
97
+ error: 'error',
98
+ 'error-soft': 'error',
99
+ info: 'info',
100
+ 'info-soft': 'info',
101
+ 'focus-ring': 'accent',
102
+ 'shadow-tint': 'ink'
103
+ }
104
+
105
+ /**
106
+ * Surface z-slot → named slot. Used to emit back-compat aliases like
107
+ * `--color-surface-z1: var(--paper-soft);` in core mode.
108
+ *
109
+ * z2/z3 collapse to paper-mute, z5/z6 to ink-soft, z7/z8 to ink-mute,
110
+ * z9/z10 to ink. This collapse is the contract: 4 surface tones in core mode.
111
+ */
112
+ export const Z_COLLAPSE_MAP_SURFACE: Record<ZSlot, NamedToken> = {
113
+ z0: 'paper',
114
+ z1: 'paper-soft',
115
+ z2: 'paper-mute',
116
+ z3: 'paper-mute',
117
+ z4: 'paper-edge',
118
+ z5: 'ink-soft',
119
+ z6: 'ink-soft',
120
+ z7: 'ink-mute',
121
+ z8: 'ink-mute',
122
+ z9: 'ink',
123
+ z10: 'ink'
124
+ }
125
+
126
+ /**
127
+ * Ink role uses the inverted z-scale (z0 = darkest = primary text).
128
+ * In core mode, the same 4+4 named ladder is reused; this table just inverts.
129
+ */
130
+ export const Z_COLLAPSE_MAP_INK: Record<ZSlot, NamedToken> = {
131
+ z0: 'ink',
132
+ z1: 'ink-mute',
133
+ z2: 'ink-mute',
134
+ z3: 'ink-soft',
135
+ z4: 'ink-soft',
136
+ z5: 'paper-edge',
137
+ z6: 'paper-edge',
138
+ z7: 'paper-mute',
139
+ z8: 'paper-mute',
140
+ z9: 'paper-soft',
141
+ z10: 'paper'
142
+ }
143
+
144
+ const SOFT_TOKENS: Set<string> = new Set(NAMED_TOKENS.filter((t) => t.endsWith('-soft')))
145
+
146
+ /**
147
+ * Returns true when the role has a `-soft` companion in the named vocabulary.
148
+ * Used to decide whether the role's tinted-background slot collapses to
149
+ * `var(--{role}-soft)` or to `var(--{role})`.
150
+ */
151
+ export function hasSoftCompanion(role: string): boolean {
152
+ return SOFT_TOKENS.has(`${role}-soft`)
153
+ }
154
+
155
+ export function shadeForNamedToken(name: string): number | 'derived' | undefined {
156
+ return NAMED_TOKEN_SHADE_MAP[name as NamedToken]
157
+ }
158
+
159
+ export function roleForNamedToken(name: string): SkinRole | undefined {
160
+ return NAMED_TOKEN_ROLE_MAP[name as NamedToken]
161
+ }
package/src/theme.ts CHANGED
@@ -3,6 +3,15 @@
3
3
  import { DEFAULT_THEME_MAPPING, defaultColors, TONE_MAP, INVERTED_ROLES } from './constants'
4
4
  import { shades } from './colors/index'
5
5
  import { ColorSpace } from './color-space'
6
+ import {
7
+ NAMED_TOKENS,
8
+ NAMED_TOKEN_SHADE_MAP,
9
+ NAMED_TOKEN_ROLE_MAP,
10
+ Z_COLLAPSE_MAP_SURFACE,
11
+ Z_COLLAPSE_MAP_INK,
12
+ Z_SLOTS,
13
+ hasSoftCompanion
14
+ } from './named-tokens'
6
15
 
7
16
  /**
8
17
  * Generate shades for a color using css variable and a ColorSpace adapter.
@@ -109,9 +118,11 @@ function toneShortcuts({ prefix, name, toneName, lightValue, darkValue }) {
109
118
  * @returns {Array} Array of shortcut definitions
110
119
  */
111
120
  export function semanticShortcuts(name) {
121
+ const inverted = INVERTED_ROLES.has(name)
112
122
  const shortcuts = []
113
- for (const [toneName, lightValue] of Object.entries(TONE_MAP)) {
114
- const darkValue = 1000 - lightValue
123
+ for (const [toneName, toneValue] of Object.entries(TONE_MAP)) {
124
+ const lightValue = inverted ? 1000 - toneValue : toneValue
125
+ const darkValue = inverted ? toneValue : 1000 - toneValue
115
126
  for (const prefix of SEMANTIC_PREFIXES) {
116
127
  shortcuts.push(...toneShortcuts({ prefix, name, toneName, lightValue, darkValue }))
117
128
  }
@@ -241,6 +252,191 @@ export class Theme {
241
252
  return rules
242
253
  }
243
254
 
255
+ /**
256
+ * Returns the named-token layer as a map of `--{name}: <resolved value>` entries.
257
+ *
258
+ * Palette values are inlined — there is no `var(--color-{role}-{shade})` indirection.
259
+ *
260
+ * `on-primary` is derived: it picks the paper shade (50) of the surface palette so the
261
+ * default contrast pair is "primary fill + white-ish text." Skins that need a different
262
+ * pair should override via custom tokens.
263
+ *
264
+ * `*-soft` variants resolve to shade 100 of their role's palette.
265
+ * `shadow-tint` resolves to shade 900 of the ink palette.
266
+ *
267
+ * @param {'light' | 'dark'} _mode — accepted for symmetry with future
268
+ * dark-aware derivations. Today the Theme is constructed with a mode-resolved
269
+ * mapping, so this parameter is informational.
270
+ * @param perRoleModes — optional map of role → 'core' | 'extended'. When a token's
271
+ * role is 'extended', the value is a `var(--color-{role}-{shade})` alias instead
272
+ * of the inlined palette value. Defaults to 'core' for all unspecified roles
273
+ * (and for the whole map when undefined).
274
+ */
275
+ getNamedTokens(_mode = 'light', perRoleModes?: Record<string, 'core' | 'extended'>) {
276
+ const colors = { ...defaultColors, ...this.#colors }
277
+ const result: Record<string, string> = {}
278
+ for (const name of NAMED_TOKENS) {
279
+ const value = this.#resolveNamedToken(name, colors, perRoleModes)
280
+ if (value !== undefined) result[`--${name}`] = value
281
+ }
282
+ return result
283
+ }
284
+
285
+ /**
286
+ * Resolves a single named token to its CSS value, respecting per-role mode.
287
+ * Returns undefined when the role's palette is missing.
288
+ */
289
+ #resolveNamedToken(name: string, colors: Record<string, Record<string, string>>, perRoleModes?: Record<string, 'core' | 'extended'>): string | undefined {
290
+ const role = NAMED_TOKEN_ROLE_MAP[name]
291
+ const shadeOrDerived = NAMED_TOKEN_SHADE_MAP[name]
292
+ const paletteName = this.#mapping[role]
293
+ if (!paletteName || !colors[paletteName]) return undefined
294
+ if (shadeOrDerived === 'derived') return this.#resolveDerivedToken(name, colors, perRoleModes)
295
+ return this.#resolveShadeToken(role, shadeOrDerived as number, colors[paletteName], perRoleModes)
296
+ }
297
+
298
+ /**
299
+ * Resolves a shade-based named token value in either core (inline) or extended (alias) mode.
300
+ */
301
+ #resolveShadeToken(role: string, shade: number, palette: Record<string, string>, perRoleModes?: Record<string, 'core' | 'extended'>): string | undefined {
302
+ const roleMode = perRoleModes?.[role] ?? 'core'
303
+ if (roleMode === 'extended') return `var(--color-${role}-${shade})`
304
+ const raw = palette[String(shade)]
305
+ return raw !== undefined ? this.#adapter.wrap(raw) : undefined
306
+ }
307
+
308
+ /**
309
+ * Resolves a 'derived' named token. Today only `on-primary` is derived — it picks
310
+ * shade 50 from the surface palette for the default white-on-primary contrast pair.
311
+ */
312
+ #resolveDerivedToken(name: string, colors: Record<string, Record<string, string>>, perRoleModes?: Record<string, 'core' | 'extended'>): string | undefined {
313
+ if (name === 'on-primary') {
314
+ const surfacePaletteName = this.#mapping['surface']
315
+ const surfacePalette = colors[surfacePaletteName]
316
+ if (!surfacePalette?.['50']) return undefined
317
+ const surfaceMode = perRoleModes?.['surface'] ?? 'core'
318
+ if (surfaceMode === 'extended') {
319
+ return `var(--color-surface-50)`
320
+ }
321
+ return this.#adapter.wrap(surfacePalette['50'])
322
+ }
323
+ return undefined
324
+ }
325
+
326
+ /**
327
+ * Generate z-aliases for surface or ink role using the provided map.
328
+ */
329
+ #getZAliasesFromMap(role: 'surface' | 'ink', map: Record<ZSlot, NamedToken>): Record<string, string> {
330
+ const result: Record<string, string> = {}
331
+ // Bare `--color-{role}` alias — points at the role's primary named
332
+ // slot. Lets preset-wind3 color utilities (bg-surface, from-surface,
333
+ // to-surface, etc.) resolve in core mode without forcing the full
334
+ // palette emit. Surface → paper, Ink → ink.
335
+ result[`--color-${role}`] = role === 'surface' ? 'var(--paper)' : 'var(--ink)'
336
+ for (const z of Z_SLOTS) {
337
+ result[`--color-${role}-${z}`] = `var(--${map[z]})`
338
+ }
339
+ return result
340
+ }
341
+
342
+ /**
343
+ * Generate z-aliases for other roles using tint-vs-solid 2-state collapse.
344
+ */
345
+ #getZAliasesOther(role: string): Record<string, string> {
346
+ const result: Record<string, string> = {}
347
+ const hasSoft = hasSoftCompanion(role)
348
+ const softTarget = hasSoft ? `${role}-soft` : role
349
+ // Bare `--color-{role}` alias points at the named token so
350
+ // preset-wind3 color utilities (from-{role}, to-{role}, divide-{role},
351
+ // etc.) resolve in core mode — they read `--color-{role}` and we'd
352
+ // otherwise leave it undefined since core mode skips the palette emit.
353
+ result[`--color-${role}`] = `var(--${role})`
354
+ for (const z of Z_SLOTS) {
355
+ const zNum = parseInt(z.slice(1), 10)
356
+ const target = zNum <= 2 ? softTarget : role
357
+ result[`--color-${role}-${z}`] = `var(--${target})`
358
+ }
359
+ return result
360
+ }
361
+
362
+ /**
363
+ * Returns z-alias CSS-var assignments for back-compat in core mode.
364
+ * `--color-{role}-z{n}` → `var(--{namedSlot})`.
365
+ *
366
+ * - surface: uses Z_COLLAPSE_MAP_SURFACE (z2/z3 → paper-mute, z9/z10 → ink, etc.)
367
+ * - ink: uses Z_COLLAPSE_MAP_INK (inverted scale)
368
+ * - primary/accent/status: tint-vs-solid 2-state collapse.
369
+ * z0–z2 → role-soft (if it exists) else role; z3+ → role.
370
+ *
371
+ * This is the back-compat layer: existing `@apply bg-surface-z3` etc. resolves
372
+ * through these aliases until consumers migrate to the named vocabulary.
373
+ */
374
+ /** Accepts SkinRole and any custom skin roles beyond the SkinRole literal union. */
375
+ getZAliasesForCore(role: string): Record<string, string> {
376
+ if (role === 'surface') {
377
+ return this.#getZAliasesFromMap('surface', Z_COLLAPSE_MAP_SURFACE)
378
+ }
379
+ if (role === 'ink') {
380
+ return this.#getZAliasesFromMap('ink', Z_COLLAPSE_MAP_INK)
381
+ }
382
+ return this.#getZAliasesOther(role)
383
+ }
384
+
385
+ /**
386
+ * Returns named tokens as CSS-var aliases pointing at the palette layer.
387
+ * Used in extended mode where the preset emits the full --color-{role}-{shade}
388
+ * palette; named tokens become thin syntactic aliases over the palette.
389
+ *
390
+ * `on-primary` aliases to `var(--color-surface-50)` (derived: paper of surface,
391
+ * matching the core-mode resolution).
392
+ */
393
+ getZAliasesForExtended(): Record<string, string> {
394
+ const result: Record<string, string> = {}
395
+ for (const name of NAMED_TOKENS) {
396
+ const role = NAMED_TOKEN_ROLE_MAP[name]
397
+ const shadeOrDerived = NAMED_TOKEN_SHADE_MAP[name]
398
+ if (shadeOrDerived === 'derived') {
399
+ // on-primary → surface.50
400
+ result[`--${name}`] = `var(--color-surface-50)`
401
+ continue
402
+ }
403
+ result[`--${name}`] = `var(--color-${role}-${shadeOrDerived})`
404
+ }
405
+ return result
406
+ }
407
+
408
+ /**
409
+ * Emit the full --color-{role}-{shade} palette CSS vars for a single role.
410
+ * Mirrors what getPalette does for all roles, but scoped to one.
411
+ */
412
+ getPaletteForRole(role: string): Record<string, string> {
413
+ const colors = { ...defaultColors, ...this.#colors }
414
+ const paletteName = this.#mapping[role]
415
+ if (!paletteName || !colors[paletteName]) return {}
416
+ const palette = colors[paletteName]
417
+ const result: Record<string, string> = {}
418
+ for (const shade of shades) {
419
+ if (palette[shade] !== undefined) {
420
+ result[`--color-${role}-${shade}`] = this.#adapter.wrap(palette[shade])
421
+ }
422
+ }
423
+ return result
424
+ }
425
+
426
+ /**
427
+ * Emit z-aliases for one role in extended mode — they point at the palette vars,
428
+ * using TONE_MAP for surface/non-inverted roles and the inverted map for ink.
429
+ */
430
+ getZAliasesForRoleExtended(role: string): Record<string, string> {
431
+ const result: Record<string, string> = {}
432
+ const isInverted = INVERTED_ROLES.has(role)
433
+ for (const [zone, light] of Object.entries(TONE_MAP)) {
434
+ const shade = isInverted ? 1000 - light : light
435
+ result[`--color-${role}-${zone}`] = `var(--color-${role}-${shade})`
436
+ }
437
+ return result
438
+ }
439
+
244
440
  getShortcuts(name) {
245
441
  return [...semanticShortcuts(name), ...contrastShortcuts(name)]
246
442
  }
package/src/utils.js CHANGED
@@ -318,13 +318,22 @@ export function oklch2hex(L, C, H) {
318
318
  }
319
319
 
320
320
  /**
321
- * Detects whether an icon value is a CSS class (i-*) or literal text (kanji, emoji, etc.)
321
+ * Detects whether an icon value is a CSS class or a literal character
322
+ * (kanji, emoji, single letter, etc.).
323
+ *
324
+ * Anything more than one grapheme is treated as a class — including bare
325
+ * semantic names like `file-svelte` which UnoCSS expands to the configured
326
+ * icon collection via `iconShortcuts(DEFAULT_ICONS, …)`. Single characters
327
+ * (kanji, emoji, single letters) render as literal text.
328
+ *
322
329
  * @param {string | null | undefined} icon
323
- * @returns {boolean} true if icon is a CSS class string
330
+ * @returns {boolean} true if icon should render as a CSS class
324
331
  */
325
332
  export function isIconClass(icon) {
326
333
  if (!icon || typeof icon !== 'string') return false
327
- return icon.startsWith('i-')
334
+ // Spread to count graphemes, not UTF-16 code units, so multi-codepoint
335
+ // emoji (e.g. 👨‍👩‍👧‍👦) and single CJK characters don't get misread.
336
+ return [...icon].length > 1
328
337
  }
329
338
 
330
339
  /**