@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.
- package/README.md +121 -1
- package/package.json +26 -34
- package/src/colors/extra.json +16 -0
- package/src/colors/index.ts +24 -0
- package/src/colors/syntax.json +4 -4
- package/src/colors/tailwind.json +0 -14
- package/src/connector.js +28 -23
- package/src/constants.js +113 -24
- package/src/events.js +32 -0
- package/src/field-mapper.js +147 -0
- package/src/index.js +13 -11
- package/src/key-event-map.js +35 -0
- package/src/mapped-items.js +22 -0
- package/src/mapping.js +5 -122
- package/src/nested.js +3 -31
- package/src/string.js +11 -11
- package/src/theme.ts +243 -0
- package/src/types.js +45 -0
- package/src/utils.js +203 -3
- package/src/vite/icon-collections.js +73 -0
- package/src/vite/index.js +1 -0
- package/LICENSE +0 -21
- package/src/colors/index.js +0 -19
- package/src/mapped-list.js +0 -233
- package/src/theme.js +0 -197
|
@@ -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 './
|
|
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 './
|
|
8
|
+
export * from './nested.js'
|
|
14
9
|
// skipcq: JS-E1004 - Needed for exposing all functions
|
|
15
|
-
export * from './
|
|
10
|
+
export * from './string.js'
|
|
16
11
|
// skipcq: JS-E1004 - Needed for exposing all functions
|
|
17
|
-
export * from './
|
|
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 {
|
|
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
|
-
*
|
|
14
|
+
* Fetches the fieldmapping for a child node
|
|
128
15
|
*
|
|
129
|
-
* @param {Array<*>} items
|
|
130
16
|
* @param {import('./types').FieldMapping} fields
|
|
131
|
-
* @returns {
|
|
17
|
+
* @returns {import('./types').FieldMapping}
|
|
132
18
|
*/
|
|
133
|
-
export function
|
|
134
|
-
|
|
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 {
|
|
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 =
|
|
13
|
-
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
|
+
}
|