@rokkit/core 1.0.0-next.98 → 1.0.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.
@@ -0,0 +1,147 @@
1
+ import { isNil, has, omit } from 'ramda'
2
+ import { DEFAULT_FIELDS } from './constants.js'
3
+ import { isObject } from './utils.js'
4
+
5
+ export class FieldMapper {
6
+ #fields = { ...DEFAULT_FIELDS }
7
+ #childMapper = null
8
+
9
+ constructor(fields = DEFAULT_FIELDS) {
10
+ this.#updateFields(fields)
11
+ }
12
+
13
+ #updateFields(fields) {
14
+ Object.keys(fields).forEach((key) => {
15
+ this.#fields[key] = fields[key]
16
+ })
17
+ this.hasIcon = has(this.#fields.icon)
18
+ this.hasImage = has(this.#fields.image)
19
+ this.hasValue = has(this.#fields.value)
20
+ this.hasLabel = has(this.#fields.label)
21
+ this.hasComponent = has(this.#fields.component)
22
+ this.hasCurrency = has(this.#fields.currency)
23
+ this.withPrefix = (x) => [this.#fields.iconPrefix, x].join('-').replace(/^-+/g, '')
24
+ this.excludeFlags = omit([
25
+ this.#fields.isDeleted,
26
+ this.#fields.isHidden,
27
+ this.#fields.isSelected,
28
+ this.#fields.isFiltered,
29
+ this.#fields.isOpen
30
+ ])
31
+ }
32
+
33
+ getChildMapper() {
34
+ if (!this.#childMapper) {
35
+ this.#childMapper = new FieldMapper(this.fields.fields ?? this.fields)
36
+ }
37
+ return this.#childMapper
38
+ }
39
+ /**
40
+ * @private
41
+ */
42
+ prop(fieldName, value) {
43
+ if (isNil(value)) return null
44
+
45
+ if (typeof value === 'object') {
46
+ return value[this.fields[fieldName]]
47
+ }
48
+
49
+ return fieldName === 'label' ? value : null
50
+ }
51
+
52
+ /**
53
+ * Gets a mapped attribute from the original item
54
+ *
55
+ * @param {string} fieldName - Name of the field to get
56
+ * @returns {any|null} - The attribute value or null if not found
57
+ */
58
+ get(fieldName, value, defaultValue = null) {
59
+ return this.prop(fieldName, value) ?? defaultValue
60
+ }
61
+
62
+ get fields() {
63
+ return this.#fields
64
+ }
65
+
66
+ set fields(fields) {
67
+ this.#updateFields(fields)
68
+ }
69
+
70
+ getIcon(value) {
71
+ if (!this.hasIcon(value)) return null
72
+ const icon = value[this.fields.icon]
73
+ if (isObject(icon)) return this.withPrefix(icon[value[this.fields.state]])
74
+ return this.withPrefix(icon)
75
+ }
76
+
77
+ getValue(value) {
78
+ if (this.hasValue(value)) {
79
+ return value[this.fields.value]
80
+ }
81
+ return value
82
+ }
83
+
84
+ getFormattedText(value, formatter) {
85
+ const text = this.get('label', value)
86
+
87
+ if (isNil(text)) return ''
88
+
89
+ if (typeof formatter !== 'function') return text.toString()
90
+
91
+ if (this.hasCurrency(value)) {
92
+ return formatter(text, this.get('currency', value))
93
+ }
94
+ return formatter(text)
95
+ }
96
+
97
+ hasChildren(item) {
98
+ return (
99
+ !isNil(item) &&
100
+ has(this.fields.children, item) &&
101
+ Array.isArray(item[this.fields.children]) &&
102
+ item[this.fields.children].length > 0
103
+ )
104
+ }
105
+
106
+ isNested(items) {
107
+ return Array.isArray(items) && items.some((item) => this.hasChildren(item))
108
+ }
109
+
110
+ getChildren(item) {
111
+ return this.hasChildren(item) ? item[this.fields.children] : []
112
+ }
113
+
114
+ /**
115
+ * Finds children by an index path
116
+ *
117
+ * @param {Array<Object>} items
118
+ * @param {Array<number>} path
119
+ * @returns {Array<Object>}
120
+ */
121
+ getChildrenByPath(items, path = []) {
122
+ const result = path.reduce(
123
+ (children, index) => children?.[index]?.[this.fields.children],
124
+ items
125
+ )
126
+
127
+ if (result === undefined) throw new Error('Invalid path')
128
+ return result
129
+ }
130
+
131
+ /**
132
+ * Finds an item by an index path
133
+ *
134
+ * @param {Array<Object>} items
135
+ * @param {Array<number>} path
136
+ * @returns {Object|null}
137
+ */
138
+ getItemByPath(items, path = []) {
139
+ const result = path.reduce(
140
+ (item, index, i) => (i === 0 ? items?.[index] : item?.[this.fields.children]?.[index]),
141
+ /* skipcq: JS-W1042 default undefined is needed */ undefined
142
+ )
143
+
144
+ if (result === undefined) throw new Error('Invalid path')
145
+ return result
146
+ }
147
+ }
package/src/index.js CHANGED
@@ -1,17 +1,19 @@
1
1
  // skipcq: JS-E1004 - Needed for exposing JS Doc types
2
- export * from './types'
2
+ export * from './types.js'
3
3
  // skipcq: JS-E1004 - Needed for exposing all constants
4
- export * from './constants'
5
- export { flattenNestedList, findValueFromPath } from './nested'
4
+ export * from './constants.js'
6
5
  // skipcq: JS-E1004 - Needed for exposing all functions
7
- export * from './mapping'
8
- export { mappedList } from './mapped-list'
9
- export { getLineTypes } from './connector'
10
- export { generateTicks } from './ticks'
11
- export { weekdays, getCalendarDays } from './calendar'
6
+ export * from './utils.js'
12
7
  // skipcq: JS-E1004 - Needed for exposing all functions
13
- export * from './utils'
8
+ export * from './nested.js'
14
9
  // skipcq: JS-E1004 - Needed for exposing all functions
15
- export * from './theme'
10
+ export * from './string.js'
16
11
  // skipcq: JS-E1004 - Needed for exposing all functions
17
- export * from './string'
12
+ export * from './theme.js'
13
+ export { FieldMapper } from './field-mapper.js'
14
+ export { getItemAtIndex, getIndexForItem } from './mapped-items.js'
15
+ export { createEmitter } from './events.js'
16
+ export { getLineTypes } from './connector.js'
17
+ export { weekdays, getCalendarDays } from './calendar.js'
18
+ export { generateTicks } from './ticks.js'
19
+ export { getNestedFields } from './mapping.js'
@@ -0,0 +1,35 @@
1
+ import { find, toPairs } from 'ramda'
2
+
3
+ /**
4
+ * Class to manage key event mappings.
5
+ */
6
+ export class KeyEventMap {
7
+ constructor() {
8
+ this.mapping = {}
9
+ }
10
+
11
+ /**
12
+ * Add a new key mapping.
13
+ * @param {string} eventName - The event name.
14
+ * @param {Array<string>|RegExp} keys - The keys to match.
15
+ */
16
+ add(eventName, keys) {
17
+ if (!Array.isArray(keys) && !(keys instanceof RegExp)) {
18
+ throw new Error('Keys must be an array or a regular expression')
19
+ }
20
+ this.mapping[eventName] = keys
21
+ }
22
+
23
+ /**
24
+ * Get the event name for a given key.
25
+ * @param {string} key - The key to match.
26
+ * @returns {string|null} - The event name or null if no match is found.
27
+ */
28
+ getEventForKey(key) {
29
+ const matchEvent = ([_, keys]) =>
30
+ (Array.isArray(keys) && keys.includes(key)) || (keys instanceof RegExp && keys.test(key))
31
+
32
+ const event = find(matchEvent, toPairs(this.mapping))
33
+ return event ? event[0] : null
34
+ }
35
+ }
@@ -0,0 +1,22 @@
1
+ import { equals, isNil } from 'ramda'
2
+
3
+ /**
4
+ * Get an item at a specific index
5
+ * @param {Array<any>} items
6
+ * @param {any} index
7
+ * @returns
8
+ */
9
+ export function getItemAtIndex(items, index) {
10
+ if (isNil(index)) return null
11
+ return index >= 0 && index < items.length ? items[index] : null
12
+ }
13
+
14
+ /**
15
+ * Get the index for an item in an array
16
+ * @param {Array<any} items
17
+ * @param {any} item
18
+ * @returns
19
+ */
20
+ export function getIndexForItem(items, item) {
21
+ return items.findIndex((i) => equals(i, item))
22
+ }
package/src/mapping.js CHANGED
@@ -1,87 +1,5 @@
1
- import { defaultFields } from './constants'
2
- import { toString, isObject } from './utils'
1
+ import { DEFAULT_FIELDS } from './constants.js'
3
2
 
4
- /**
5
- * Get the component to be used to render the item.
6
- * If the component is null or undefined, it will return the default component.
7
- *
8
- * @param {object|string} value
9
- * @param {import('./types.js').FieldMapping} fields
10
- * @param {import('./types.js').ComponentMap} using
11
- */
12
- export function getComponent(value, fields, using) {
13
- return fields.component && isObject(value)
14
- ? using[value[fields.component]] ?? using.default
15
- : using.default
16
- }
17
-
18
- /**
19
- * Get the icon for the item. If the icon is an object, it will use the state to determine which icon to use.
20
- *
21
- * @param {object|string} value
22
- * @param {import('./types.js').FieldMapping} fields
23
- * @returns {string}
24
- */
25
- export function getIcon(value, fields = defaultFields) {
26
- if (fields.icon === undefined || typeof (value ?? '') !== 'object') return null
27
-
28
- const name = getIconFromObject(value, fields)
29
- return fields.iconPrefix ? [fields.iconPrefix, name].join('-') : name
30
- }
31
-
32
- /**
33
- * Get the icon for the item. If the icon is an object, it will use the state to determine which icon to use.
34
- *
35
- * @param {object} value
36
- * @param {import('./types.js').FieldMapping} fields
37
- * @returns {string}
38
- */
39
- function getIconFromObject(value, fields) {
40
- if (typeof value[fields.icon] === 'object') return value[fields.icon][value[fields.state]]
41
- return value[fields.icon]
42
- }
43
-
44
- /**
45
- * Get the value for the item. If the value is an object,
46
- * it will use the field mapping to determine which attribute to get.
47
- *
48
- * @param {*} node
49
- * @param {import('./types').FieldMapping} fields
50
- * @returns {*}
51
- */
52
- export function getValue(node, fields = defaultFields) {
53
- return typeof node === 'object' && node !== null ? node[fields.value] ?? node[fields.text] : node
54
- }
55
-
56
- /**
57
- * Get the text for the item. If the text is an object,
58
- * it will use the field mapping to determine which attribute to get.
59
- *
60
- * @param {*} node
61
- * @param {import('./types').FieldMapping} fields
62
- * @returns {string}
63
- */
64
- export function getText(node, fields = defaultFields) {
65
- const value = isObject(node) ? node[fields.text] : node
66
- return value
67
- }
68
-
69
- /**
70
- * Get the formatted text for the item. If the text is an object, use the field mapping to determine
71
- * which attribute to get currency. Use the formatter or identity function to format the text.
72
- *
73
- * @param {*} node
74
- * @param {import('./types').FieldMapping} fields
75
- * @param {Function} formatter
76
- * @returns {Function}
77
- */
78
- export function getFormattedText(node, fields = defaultFields, formatter = toString) {
79
- const value = isObject(node) ? node[fields.text] : node
80
- const currency = getAttribute(node, fields.currency)
81
- const formatValue = typeof formatter === 'function' ? formatter : toString
82
-
83
- return currency ? formatValue(value, currency) : formatValue(value)
84
- }
85
3
  /**
86
4
  * Gets the attribute from the node
87
5
  * @param {*} node
@@ -91,48 +9,13 @@ export function getFormattedText(node, fields = defaultFields, formatter = toStr
91
9
  export function getAttribute(node, attr) {
92
10
  return typeof node === 'object' && node !== null && attr !== null ? node[attr] : null
93
11
  }
94
- /**
95
- * Check if the current item is a parent
96
- *
97
- * @param {*} item
98
- * @param {import('./types').FieldMapping} fields
99
- * @returns {boolean}
100
- */
101
- export function hasChildren(item, fields) {
102
- return (
103
- item !== null &&
104
- typeof item === 'object' &&
105
- fields.children in item &&
106
- Array.isArray(item[fields.children])
107
- )
108
- }
109
-
110
- /**
111
- * Check if the current item is a parent and is expanded
112
- *
113
- * @param {*} item
114
- * @param {import('./types').FieldMapping} fields
115
- * @returns {boolean}
116
- */
117
- export function isExpanded(item, fields) {
118
- if (item === null) return false
119
- if (!hasChildren(item, fields)) return false
120
- if (fields.isOpen in item) {
121
- return item[fields.isOpen]
122
- }
123
- return false
124
- }
125
12
 
126
13
  /**
127
- * Verify if at least one item has children
14
+ * Fetches the fieldmapping for a child node
128
15
  *
129
- * @param {Array<*>} items
130
16
  * @param {import('./types').FieldMapping} fields
131
- * @returns {boolean}
17
+ * @returns {import('./types').FieldMapping}
132
18
  */
133
- export function isNested(items, fields) {
134
- for (let i = 0; i < items.length; i++) {
135
- if (hasChildren(items[i], fields)) return true
136
- }
137
- return false
19
+ export function getNestedFields(fields) {
20
+ return { ...DEFAULT_FIELDS, ...(fields.fields ?? fields) }
138
21
  }
package/src/nested.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { omit } from 'ramda'
2
- import { defaultFields } from './constants'
2
+ import { DEFAULT_FIELDS } from './constants.js'
3
3
 
4
4
  /**
5
5
  * Flattens a nested list of items
@@ -9,8 +9,8 @@ import { defaultFields } from './constants'
9
9
  * @param {number} level
10
10
  * @returns {Array}
11
11
  */
12
- export function flattenNestedList(items, fields = defaultFields, level = 0) {
13
- fields = { ...defaultFields, ...fields }
12
+ export function flattenNestedList(items, fields = DEFAULT_FIELDS, level = 0) {
13
+ fields = { ...DEFAULT_FIELDS, ...fields }
14
14
  let data = []
15
15
  items.forEach((item) => {
16
16
  const children = item[fields.children] ?? []
@@ -26,31 +26,3 @@ export function flattenNestedList(items, fields = defaultFields, level = 0) {
26
26
  })
27
27
  return data
28
28
  }
29
-
30
- /**
31
- * Matches a path slug to a value in the menu
32
- *
33
- * @param {string} slug
34
- * @param {Array} data
35
- * @param {import('./types').FieldMapping} fields
36
- * @returns {any}
37
- */
38
- export function findValueFromPath(slug, data, fields) {
39
- fields = { ...defaultFields, ...fields }
40
- const keys = slug.split('/')
41
- let items = data
42
- let value = null
43
-
44
- keys.forEach((key, index) => {
45
- const match = items.find((item) => item[fields.key] === key)
46
- if (match) {
47
- if (index < keys.length - 1) {
48
- match[fields.isOpen] = true
49
- items = match[fields.children]
50
- } else {
51
- value = match
52
- }
53
- }
54
- })
55
- return value
56
- }
package/src/string.js CHANGED
@@ -35,6 +35,17 @@ export function toHyphenCase(text) {
35
35
  .replace(/^-/, '')
36
36
  }
37
37
 
38
+ /**
39
+ * Simple comparison for two strings
40
+ *
41
+ * @param {String} a
42
+ * @param {String} b
43
+ * @returns
44
+ */
45
+ export function compareStrings(a, b) {
46
+ return a > b ? 1 : a < b ? -1 : 0
47
+ }
48
+
38
49
  /**
39
50
  * Sort by splitting hyphen separated strings while keeping strings with same number of parts together
40
51
  *
@@ -53,17 +64,6 @@ export function sortByParts(a, b, separator = '-') {
53
64
  return result
54
65
  }
55
66
 
56
- /**
57
- * Simple comparison for two strings
58
- *
59
- * @param {String} a
60
- * @param {String} b
61
- * @returns
62
- */
63
- export function compareStrings(a, b) {
64
- return a > b ? 1 : a < b ? -1 : 0
65
- }
66
-
67
67
  /**
68
68
  * Generates a unique id from current timestamp
69
69
  *
package/src/theme.ts ADDED
@@ -0,0 +1,243 @@
1
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
2
+ // @ts-nocheck
3
+ import { DEFAULT_THEME_MAPPING, defaultColors, TONE_MAP } from './constants'
4
+ import { shades } from './colors/index'
5
+ import { colorToRgb } from './utils'
6
+
7
+ const modifiers = {
8
+ hsl: (value) => `hsl(${value} / <alpha-value>)`,
9
+ rgb: (value) => `rgb(${value} / <alpha-value>)`,
10
+ none: (value) => value
11
+ }
12
+
13
+ /**
14
+ * Generate shades for a color using css varuable
15
+ *
16
+ * @param {string} name
17
+ * @param {string} modifier
18
+ * @returns
19
+ */
20
+ export function shadesOf(name, modifier = 'none') {
21
+ const fn = modifier in modifiers ? modifiers[modifier] : modifiers.none
22
+
23
+ return shades.reduce(
24
+ (result, shade) => ({
25
+ ...result,
26
+ [shade]: fn(`var(--color-${name}-${shade})`)
27
+ }),
28
+ {
29
+ DEFAULT: fn(`var(--color-${name}-500)`)
30
+ }
31
+ )
32
+ }
33
+
34
+ /**
35
+ * Generates color rules for a specific theme variant, for both light and dark modes.
36
+ *
37
+ * @param {string} variant - The name of the variant to generate rules for.
38
+ * @param {Object} colors - The object containing color definitions.
39
+ * @param {Object} mapping - An object that maps variant names to color property names.
40
+ * @returns {import('./types').ShadeMappings} An array containing the color rules for both light and dark modes.
41
+ */
42
+ function generateColorRules(variant, colors, mapping) {
43
+ return ['DEFAULT', ...shades].flatMap((shade) => [
44
+ {
45
+ key: shade === 'DEFAULT' ? `--color-${variant}` : `--color-${variant}-${shade}`,
46
+ value: colorToRgb(colors[mapping[variant]][`${shade}`])
47
+ }
48
+ ])
49
+ }
50
+
51
+ /**
52
+ * Constructs and returns the light and dark theme variants based on provided color mapping and color definitions.
53
+ *
54
+ * @param {Object} [mapping=DEFAULT_THEME_MAPPING] - An object mapping variant names to color property names.
55
+ * @param {Object} [colors=defaultColors] - The object containing default color definitions.
56
+ * @returns {Array<Array>} An array containing two arrays, one for the light theme variant and another for the dark theme.
57
+ */
58
+ export function themeRules(mapping = DEFAULT_THEME_MAPPING, colors = defaultColors) {
59
+ mapping = { ...DEFAULT_THEME_MAPPING, ...mapping }
60
+ colors = { ...defaultColors, ...colors }
61
+ const variants = Object.keys(mapping)
62
+ const rules = variants
63
+ .flatMap((variant) => generateColorRules(variant, colors, mapping))
64
+ .reduce((acc, { key, value }) => ({ ...acc, [key]: value }), {})
65
+
66
+ return rules
67
+ }
68
+
69
+ const SEMANTIC_PREFIXES = [
70
+ 'bg',
71
+ 'border',
72
+ 'border-l',
73
+ 'border-r',
74
+ 'border-t',
75
+ 'border-b',
76
+ 'text',
77
+ 'ring',
78
+ 'outline',
79
+ 'from',
80
+ 'to',
81
+ 'divide',
82
+ 'stroke',
83
+ 'fill'
84
+ ]
85
+
86
+ /**
87
+ * @param {{ prefix: string, name: string, toneName: string, lightValue: number, darkValue: number }} opts
88
+ * @returns {Array}
89
+ */
90
+ function toneShortcuts({ prefix, name, toneName, lightValue, darkValue }) {
91
+ const variantPattern = new RegExp(`^(.+):${prefix}-${name}-${toneName}(\\/\\d+)?$`)
92
+ const opacityPattern = new RegExp(`^${prefix}-${name}-${toneName}(\\/\\d+)?$`)
93
+ const exactPattern = `${prefix}-${name}-${toneName}`
94
+ return [
95
+ [
96
+ variantPattern,
97
+ ([, variant, end]) =>
98
+ `${variant}:${prefix}-${name}-${lightValue}${end || ''} ${variant}:dark:${prefix}-${name}-${darkValue}${end || ''}`
99
+ ],
100
+ [
101
+ opacityPattern,
102
+ ([, end]) =>
103
+ `${prefix}-${name}-${lightValue}${end || ''} dark:${prefix}-${name}-${darkValue}${end || ''}`
104
+ ],
105
+ [exactPattern, `${prefix}-${name}-${lightValue} dark:${prefix}-${name}-${darkValue}`]
106
+ ]
107
+ }
108
+
109
+ /**
110
+ * Generates UnoCSS shortcut definitions for semantic tones with bg, border, text.
111
+ * @param {string} name - Color name (e.g., 'primary')
112
+ * @returns {Array} Array of shortcut definitions
113
+ */
114
+ export function semanticShortcuts(name) {
115
+ const shortcuts = []
116
+ for (const [toneName, lightValue] of Object.entries(TONE_MAP)) {
117
+ const darkValue = 1000 - lightValue
118
+ for (const prefix of SEMANTIC_PREFIXES) {
119
+ shortcuts.push(...toneShortcuts({ prefix, name, toneName, lightValue, darkValue }))
120
+ }
121
+ }
122
+ return shortcuts
123
+ }
124
+
125
+ /**
126
+ * Generates "on-color" text shortcuts for readable text on colored backgrounds.
127
+ *
128
+ * - `text-on-{name}` → high contrast text for use on z5+ backgrounds (always light text)
129
+ * - `text-on-{name}-muted` → slightly muted but still readable on z5+ backgrounds
130
+ *
131
+ * @param {string} name - Color name (e.g., 'primary', 'surface')
132
+ * @returns {Array} Array of shortcut definitions
133
+ */
134
+ export function contrastShortcuts(name) {
135
+ return [
136
+ [
137
+ new RegExp(`^text-on-${name}(\\/\\d+)?$`),
138
+ ([, end]) => `text-${name}-50${end || ''} dark:text-${name}-50${end || ''}`
139
+ ],
140
+ [
141
+ new RegExp(`^text-on-${name}-muted(\\/\\d+)?$`),
142
+ ([, end]) => `text-${name}-100${end || ''} dark:text-${name}-200${end || ''}`
143
+ ]
144
+ ]
145
+ }
146
+
147
+ /**
148
+ * Theme class for managing color palettes, mappings, and semantic shortcuts.
149
+ */
150
+ export class Theme {
151
+ #colors
152
+ #mapping
153
+
154
+ /**
155
+ *
156
+ * @param {import('./types.js').ColorTheme} param0
157
+ */
158
+ constructor({ colors = defaultColors, mapping = DEFAULT_THEME_MAPPING } = {}) {
159
+ this.#colors = { ...defaultColors, ...colors }
160
+ this.#mapping = { ...DEFAULT_THEME_MAPPING, ...mapping }
161
+ }
162
+
163
+ get colors() {
164
+ return this.#colors
165
+ }
166
+ set colors(colors) {
167
+ this.#colors = { ...colors }
168
+ }
169
+
170
+ get mapping() {
171
+ return this.#mapping
172
+ }
173
+ set mapping(mapping) {
174
+ this.#mapping = { ...mapping }
175
+ }
176
+
177
+ mapVariant(color, variant) {
178
+ return Object.keys(color).reduce(
179
+ (acc, key) => ({
180
+ ...acc,
181
+ [key]:
182
+ key === 'DEFAULT'
183
+ ? `rgba(var(--color-${variant}),<alpha-value>)`
184
+ : `rgba(var(--color-${variant}-${key}),<alpha-value>)`
185
+ }),
186
+ {}
187
+ )
188
+ }
189
+
190
+ getColorRules(mapping = null) {
191
+ const variants = Object.entries({ ...this.#mapping, ...mapping })
192
+ return variants.reduce(
193
+ (acc, [variant, key]) => ({
194
+ ...acc,
195
+ [variant]: this.mapVariant(this.#colors[key], variant)
196
+ }),
197
+ {}
198
+ )
199
+ }
200
+
201
+ getPalette(mapping = null) {
202
+ const useMapping = { ...this.#mapping, ...mapping }
203
+ const useColors = { ...defaultColors, ...this.#colors }
204
+ const variants = Object.keys(useMapping)
205
+ const rules = variants
206
+ .flatMap((variant) => generateColorRules(variant, useColors, useMapping))
207
+ .reduce((acc, { key, value }) => ({ ...acc, [key]: value }), {})
208
+ return rules
209
+ }
210
+
211
+ getShortcuts(name) {
212
+ return [...semanticShortcuts(name), ...contrastShortcuts(name)]
213
+ }
214
+
215
+ /**
216
+ * Generates CSS custom property declarations for the semantic z0–z10 tone scale.
217
+ *
218
+ * Returns a CSS string with two blocks:
219
+ * - `:root` — light-mode aliases (z0=50 … z10=950)
220
+ * - `[data-mode="dark"]` — dark-mode aliases (z0=950 … z10=50)
221
+ *
222
+ * These let consumers write `var(--color-primary-z5)` instead of hardcoding
223
+ * numeric shades, and the value automatically adapts to the active mode.
224
+ */
225
+ getZScaleCSS() {
226
+ const names = Object.keys(this.#mapping)
227
+
228
+ const lightLines = names.flatMap((name) =>
229
+ Object.entries(TONE_MAP).map(
230
+ ([zone, light]) => ` --color-${name}-${zone}: var(--color-${name}-${light});`
231
+ )
232
+ )
233
+
234
+ const darkLines = names.flatMap((name) =>
235
+ Object.entries(TONE_MAP).map(([zone, light]) => {
236
+ const dark = 1000 - light
237
+ return ` --color-${name}-${zone}: var(--color-${name}-${dark});`
238
+ })
239
+ )
240
+
241
+ return `:root {\n${lightLines.join('\n')}\n}\n[data-mode="dark"] {\n${darkLines.join('\n')}\n}`
242
+ }
243
+ }