@rokkit/core 1.0.0-next.125 → 1.0.0-next.129

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/utils.d.ts CHANGED
@@ -1,3 +1,13 @@
1
+ /**
2
+ * Detects text direction based on HTML lang attribute
3
+ * @returns {'ltr' | 'rtl'}
4
+ */
5
+ export function detectDirection(): "ltr" | "rtl";
6
+ /**
7
+ * Checks if current document direction is RTL
8
+ * @returns {boolean}
9
+ */
10
+ export function isRTL(): boolean;
1
11
  /**
2
12
  * Finds the closest ancestor of the given element that has the given attribute.
3
13
  *
@@ -67,6 +77,21 @@ export function getPathFromKey(key: string): string[];
67
77
  * @returns {Function|undefined}
68
78
  */
69
79
  export function getSnippet(obj: Object, key: string, defaultSnippet?: null | Function): Function | undefined;
80
+ /**
81
+ * Resolve which snippet to render for a proxy item.
82
+ *
83
+ * Checks proxy.snippet for a per-item named override first (e.g. item.snippet = 'highlighted').
84
+ * Falls back to the component-level fallback snippet name (e.g. 'itemContent' / 'groupContent').
85
+ * Returns null if neither is found.
86
+ *
87
+ * @param {Record<string, unknown>} snippets - snippets passed to the component
88
+ * @param {{ snippet?: string | null }} proxy - any object with an optional .snippet property
89
+ * @param {string} [fallback] - fallback snippet name; defaults to ITEM_SNIPPET ('itemContent')
90
+ * @returns {Function | null}
91
+ */
92
+ export function resolveSnippet(snippets: Record<string, unknown>, proxy: {
93
+ snippet?: string | null;
94
+ }, fallback?: string): Function | null;
70
95
  /**
71
96
  * convert hex string to `{r} {g} {b}`
72
97
  * @param {string} hex
@@ -80,4 +105,3 @@ export function hex2rgb(hex: string): string;
80
105
  * @returns {string|null} - Returns the original string if it's an image URL or image data, otherwise null
81
106
  */
82
107
  export function getImage(str: string): string | null;
83
- export function importIcons(icons: Object): Object;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Creates icon collection loaders for UnoCSS presetIcons from a simple config.
3
+ *
4
+ * This function transforms a map of collection names to JSON package paths
5
+ * into the format expected by UnoCSS's presetIcons.
6
+ *
7
+ * Supports three types of paths:
8
+ * - Package paths (e.g., '@rokkit/icons/ui.json') - resolved via require
9
+ * - Absolute paths (e.g., '/path/to/icons.json') - used directly
10
+ * - Relative paths (e.g., './static/icons.json') - resolved from process.cwd()
11
+ *
12
+ * @example
13
+ * // uno.config.js
14
+ * import { iconCollections } from '@rokkit/core/vite'
15
+ *
16
+ * export default defineConfig({
17
+ * presets: [
18
+ * presetIcons({
19
+ * collections: iconCollections({
20
+ * rokkit: '@rokkit/icons/ui.json',
21
+ * logo: '@rokkit/icons/auth.json',
22
+ * solar: '@iconify-json/solar/icons.json',
23
+ * custom: './static/icons/custom.json'
24
+ * })
25
+ * })
26
+ * ]
27
+ * })
28
+ *
29
+ * @param {Record<string, string>} config - Map of collection alias to JSON path
30
+ * @returns {Record<string, () => any>} Collections object for presetIcons
31
+ */
32
+ export function iconCollections(config: Record<string, string>): Record<string, () => any>;
@@ -0,0 +1 @@
1
+ export { iconCollections } from "./icon-collections.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rokkit/core",
3
- "version": "1.0.0-next.125",
3
+ "version": "1.0.0-next.129",
4
4
  "description": "Contains core utility functions and classes that can be used in various components.",
5
5
  "author": "Jerry Thomas <me@jerrythomas.name>",
6
6
  "license": "MIT",
@@ -27,10 +27,18 @@
27
27
  "types": "./dist/index.d.ts",
28
28
  "import": "./src/index.js",
29
29
  "svelte": "./src/index.js"
30
+ },
31
+ "./vite": {
32
+ "types": "./dist/vite/index.d.ts",
33
+ "import": "./src/vite/index.js"
30
34
  }
31
35
  },
32
36
  "dependencies": {
37
+ "@unocss/preset-mini": "^66.6.1",
33
38
  "date-fns": "^4.1.0",
34
- "ramda": "^0.31.3"
39
+ "ramda": "^0.32.0"
40
+ },
41
+ "devDependencies": {
42
+ "@rokkit/icons": "1.0.0-next.129"
35
43
  }
36
44
  }
package/src/constants.js CHANGED
@@ -1,9 +1,17 @@
1
- export { defaultColors, syntaxColors, shades, defaultPalette } from './colors/index.js'
1
+ export { defaultColors, syntaxColors, shades, defaultPalette } from './colors/index'
2
2
  export const DATA_IMAGE_REGEX = /^data:image\/(jpeg|png|gif|bmp|webp|svg\+xml)/i
3
+
4
+ // ─── Snippet names ────────────────────────────────────────────────────────────
5
+
6
+ export const ITEM_SNIPPET = 'itemContent'
7
+ export const GROUP_SNIPPET = 'groupContent'
3
8
  /**
9
+ * @deprecated Use BASE_FIELDS from @rokkit/core instead.
10
+ * Retained for legacy ListController/FieldMapper/Proxy consumers (Toolbar, Table).
11
+ * Will be removed when those components migrate to Wrapper+Navigator.
4
12
  * @type {import('./types).FieldMapping} Fields
5
13
  */
6
- export const defaultFields = {
14
+ export const DEFAULT_FIELDS = {
7
15
  id: 'id',
8
16
  href: 'href',
9
17
  icon: 'icon',
@@ -24,26 +32,83 @@ export const defaultFields = {
24
32
  label: 'label',
25
33
  component: 'component', // to be deprecated in favour of snippet
26
34
  snippet: 'snippet',
35
+ disabled: 'disabled',
27
36
  /* flag fields */
28
37
  deleted: '_deleted',
29
38
  expanded: '_expanded',
30
39
  selected: '_selected'
31
40
  }
32
41
 
33
- export const defaultIcons = [
42
+ // ─── BASE_FIELDS ─────────────────────────────────────────────────────────────
43
+ // Canonical field mapping for ProxyItem and all Wrapper+Navigator components.
44
+ // Semantic keys map to common raw data keys for backward compatibility.
45
+
46
+ export const BASE_FIELDS = {
47
+ // Identity
48
+ id: 'id',
49
+ value: 'value',
50
+ // Display
51
+ label: 'label',
52
+ icon: 'icon',
53
+ avatar: 'image',
54
+ subtext: 'description',
55
+ tooltip: 'title',
56
+ badge: 'badge',
57
+ shortcut: 'shortcut',
58
+ // Structure
59
+ children: 'children',
60
+ type: 'type',
61
+ snippet: 'snippet',
62
+ href: 'href',
63
+ hrefTarget: 'target',
64
+ // State
65
+ disabled: 'disabled',
66
+ expanded: 'expanded',
67
+ selected: 'selected',
68
+ }
69
+
70
+ const LEGACY_KEY_MAP = { description: 'subtext', title: 'tooltip', image: 'avatar', target: 'hrefTarget' }
71
+
72
+ /**
73
+ * Remap legacy field-override keys to their BASE_FIELDS semantic equivalents.
74
+ * e.g. { text: 'name' } → { label: 'name' }
75
+ *
76
+ * @param {Record<string, string> | null | undefined} fields
77
+ * @returns {Record<string, string>}
78
+ */
79
+ export function normalizeFields(fields) {
80
+ if (!fields || typeof fields !== 'object') return {}
81
+ const result = {}
82
+ for (const [key, value] of Object.entries(fields)) {
83
+ result[LEGACY_KEY_MAP[key] ?? key] = value
84
+ }
85
+ return result
86
+ }
87
+
88
+ export const DEFAULT_ICONS = [
34
89
  'accordion-opened',
35
90
  'accordion-closed',
36
91
  'action-remove',
92
+ 'action-cancel',
93
+ 'action-retry',
37
94
  'action-add',
38
95
  'action-clear',
39
96
  'action-search',
40
97
  'action-close',
41
98
  'action-copy',
42
99
  'action-copysuccess',
100
+ 'action-check',
101
+ 'action-pin',
102
+ 'action-save',
103
+ 'action-unpin',
43
104
  'node-opened',
44
105
  'node-closed',
106
+ 'folder-opened',
107
+ 'folder-closed',
45
108
  'selector-opened',
46
109
  'selector-closed',
110
+ 'menu-opened',
111
+ 'menu-closed',
47
112
  'checkbox-checked',
48
113
  'checkbox-unchecked',
49
114
  'checkbox-unknown',
@@ -54,10 +119,13 @@ export const defaultIcons = [
54
119
  'radio-on',
55
120
  'mode-dark',
56
121
  'mode-light',
122
+ 'mode-system',
57
123
  'navigate-left',
58
124
  'navigate-right',
59
125
  'navigate-up',
60
126
  'navigate-down',
127
+ 'palette-hex',
128
+ 'palette-presets',
61
129
  'state-error',
62
130
  'state-warning',
63
131
  'state-success',
@@ -84,14 +152,14 @@ export const defaultIcons = [
84
152
  'align-vertical-middle'
85
153
  ]
86
154
 
87
- export const defaultOptions = {
155
+ export const DEFAULT_OPTIONS = {
88
156
  id: 'id',
89
157
  label: 'label',
90
158
  value: 'value',
91
159
  checked: 'checked'
92
160
  }
93
161
 
94
- export const defaultKeyMap = {
162
+ export const DEFAULT_KEYMAP = {
95
163
  ArrowRight: 'open',
96
164
  ArrowLeft: 'close',
97
165
  ArrowDown: 'down',
@@ -100,7 +168,7 @@ export const defaultKeyMap = {
100
168
  Escape: 'deselect'
101
169
  }
102
170
 
103
- export const defaultThemeMapping = {
171
+ export const DEFAULT_THEME_MAPPING = {
104
172
  surface: 'slate',
105
173
  primary: 'orange',
106
174
  secondary: 'pink',
@@ -124,15 +192,6 @@ export const TONE_MAP = {
124
192
  z8: 800,
125
193
  z9: 900,
126
194
  z10: 950
127
- // base: 50,
128
- // inset: 100,
129
- // subtle: 200,
130
- // muted: 300,
131
- // raised: 400,
132
- // elevated: 600,
133
- // floating: 700,
134
- // contrast: 800,
135
- // overlay: 900
136
195
  }
137
196
  /**
138
197
  * Splits an icon name into its group and key components.
@@ -161,4 +220,4 @@ export function stateIconsFromNames(icons) {
161
220
  )
162
221
  }
163
222
 
164
- export const defaultStateIcons = stateIconsFromNames(defaultIcons)
223
+ export const DEFAULT_STATE_ICONS = stateIconsFromNames(DEFAULT_ICONS)
@@ -1,12 +1,12 @@
1
1
  import { isNil, has, omit } from 'ramda'
2
- import { defaultFields } from './constants.js'
2
+ import { DEFAULT_FIELDS } from './constants.js'
3
3
  import { isObject } from './utils.js'
4
4
 
5
5
  export class FieldMapper {
6
- #fields = { ...defaultFields }
6
+ #fields = { ...DEFAULT_FIELDS }
7
7
  #childMapper = null
8
8
 
9
- constructor(fields = defaultFields) {
9
+ constructor(fields = DEFAULT_FIELDS) {
10
10
  this.#updateFields(fields)
11
11
  }
12
12
 
@@ -16,7 +16,6 @@ export class FieldMapper {
16
16
  })
17
17
  this.hasIcon = has(this.#fields.icon)
18
18
  this.hasImage = has(this.#fields.image)
19
- this.hasText = has(this.#fields.text)
20
19
  this.hasValue = has(this.#fields.value)
21
20
  this.hasLabel = has(this.#fields.label)
22
21
  this.hasComponent = has(this.#fields.component)
@@ -47,7 +46,7 @@ export class FieldMapper {
47
46
  return value[this.fields[fieldName]]
48
47
  }
49
48
 
50
- return fieldName === 'text' ? value : null
49
+ return fieldName === 'label' ? value : null
51
50
  }
52
51
 
53
52
  /**
@@ -83,7 +82,7 @@ export class FieldMapper {
83
82
  }
84
83
 
85
84
  getFormattedText(value, formatter) {
86
- const text = this.get('text', value)
85
+ const text = this.get('label', value)
87
86
 
88
87
  if (isNil(text)) return ''
89
88
 
package/src/index.js CHANGED
@@ -16,4 +16,4 @@ export { createEmitter } from './events.js'
16
16
  export { getLineTypes } from './connector.js'
17
17
  export { weekdays, getCalendarDays } from './calendar.js'
18
18
  export { generateTicks } from './ticks.js'
19
- export { getValue, getNestedFields, hasChildren, isExpanded } from './mapping.js'
19
+ export { getNestedFields } from './mapping.js'
@@ -26,7 +26,7 @@ export class KeyEventMap {
26
26
  * @returns {string|null} - The event name or null if no match is found.
27
27
  */
28
28
  getEventForKey(key) {
29
- // eslint-disable-next-line no-unused-vars
29
+
30
30
  const matchEvent = ([_, keys]) =>
31
31
  (Array.isArray(keys) && keys.includes(key)) || (keys instanceof RegExp && keys.test(key))
32
32
 
package/src/mapping.js CHANGED
@@ -1,73 +1,4 @@
1
- import { defaultFields } from './constants.js'
2
- import { toString, isObject } from './utils.js'
3
- import { has } from 'ramda'
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} value
22
- * @param {import('./types.js').FieldMapping} fields
23
- * @returns {string}
24
- */
25
- function getIconFromObject(value, fields) {
26
- if (!value) return null
27
- if (typeof value[fields.icon] === 'object') return value[fields.icon][value[fields.state]]
28
- return value[fields.icon]
29
- }
30
-
31
- /**
32
- * Get the icon for the item. If the icon is an object, it will use the state to determine which icon to use.
33
- *
34
- * @param {object|string} value
35
- * @param {import('./types.js').FieldMapping} fields
36
- * @returns {string}
37
- */
38
- export function getIcon(value, fields = defaultFields) {
39
- if (fields.icon === undefined || typeof value !== 'object') return null
40
-
41
- const name = getIconFromObject(value, fields)
42
- return name ? [fields.iconPrefix, name].join('-').replace(/^-+/g, '') : null
43
- }
44
-
45
- /**
46
- * Get the value for the item. If the value is an object,
47
- * it will use the field mapping to determine which attribute to get.
48
- *
49
- * @param {*} node
50
- * @param {import('./types').FieldMapping} fields
51
- * @returns {*}
52
- */
53
- export function getValue(node, fields = defaultFields) {
54
- return typeof node === 'object' && node !== null
55
- ? (node[fields.value] ?? node[fields.text])
56
- : node
57
- }
58
-
59
- /**
60
- * Get the text for the item. If the text is an object,
61
- * it will use the field mapping to determine which attribute to get.
62
- *
63
- * @param {*} node
64
- * @param {import('./types').FieldMapping} fields
65
- * @returns {string}
66
- */
67
- export function getText(node, fields = defaultFields) {
68
- const value = isObject(node) ? node[fields.text] : node
69
- return value
70
- }
1
+ import { DEFAULT_FIELDS } from './constants.js'
71
2
 
72
3
  /**
73
4
  * Gets the attribute from the node
@@ -79,50 +10,6 @@ export function getAttribute(node, attr) {
79
10
  return typeof node === 'object' && node !== null && attr !== null ? node[attr] : null
80
11
  }
81
12
 
82
- /**
83
- * Get the formatted text for the item. If the text is an object, use the field mapping to determine
84
- * which attribute to get currency. Use the formatter or identity function to format the text.
85
- *
86
- * @param {*} node
87
- * @param {import('./types').FieldMapping} fields
88
- * @param {Function} formatter
89
- * @returns {Function}
90
- */
91
- export function getFormattedText(node, fields = defaultFields, formatter = toString) {
92
- const value = getText(node, fields)
93
- const currency = getAttribute(node, fields.currency)
94
- const formatValue = typeof formatter === 'function' ? formatter : toString
95
-
96
- return currency ? formatValue(value, currency) : formatValue(value)
97
- }
98
-
99
- /**
100
- * Check if the current item is a parent
101
- *
102
- * @param {*} item
103
- * @param {import('./types').FieldMapping} fields
104
- * @returns {boolean}
105
- */
106
- export function hasChildren(item, fields) {
107
- return has(fields.children, item) && Array.isArray(item[fields.children])
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 (has(fields.expanded, item)) {
121
- return item[fields.expanded]
122
- }
123
- return false
124
- }
125
-
126
13
  /**
127
14
  * Fetches the fieldmapping for a child node
128
15
  *
@@ -130,5 +17,5 @@ export function isExpanded(item, fields) {
130
17
  * @returns {import('./types').FieldMapping}
131
18
  */
132
19
  export function getNestedFields(fields) {
133
- return { ...defaultFields, ...(fields.fields ?? fields) }
20
+ return { ...DEFAULT_FIELDS, ...(fields.fields ?? fields) }
134
21
  }
package/src/nested.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { omit } from 'ramda'
2
- import { defaultFields } from './constants.js'
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.js'
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/utils.js CHANGED
@@ -1,7 +1,54 @@
1
1
  import { has, isNil } from 'ramda'
2
- import { DATA_IMAGE_REGEX } from './constants'
2
+ import { DATA_IMAGE_REGEX, ITEM_SNIPPET } from './constants'
3
3
 
4
4
  let idCounter = 0
5
+
6
+ /**
7
+ * RTL language codes (ISO 639-1)
8
+ * @type {string[]}
9
+ */
10
+ const RTL_LANGUAGES = [
11
+ 'ar', // Arabic
12
+ 'he', // Hebrew
13
+ 'fa', // Persian/Farsi
14
+ 'ur', // Urdu
15
+ 'yi', // Yiddish
16
+ 'ps', // Pashto
17
+ 'sd', // Sindhi
18
+ 'ug', // Uyghur
19
+ 'ku', // Kurdish (Sorani)
20
+ 'dv' // Divehi/Maldivian
21
+ ]
22
+
23
+ /**
24
+ * Detects text direction based on HTML lang attribute
25
+ * @returns {'ltr' | 'rtl'}
26
+ */
27
+ export function detectDirection() {
28
+ if (typeof document === 'undefined') return 'ltr'
29
+
30
+ // Check dir attribute first (explicit override)
31
+ const htmlDir = document.documentElement.getAttribute('dir')
32
+ if (htmlDir === 'rtl' || htmlDir === 'ltr') return htmlDir
33
+
34
+ // Detect from lang attribute
35
+ const lang = document.documentElement.getAttribute('lang')
36
+ if (lang) {
37
+ // Extract primary language code (e.g., 'ar-SA' -> 'ar')
38
+ const primaryLang = lang.split('-')[0].toLowerCase()
39
+ if (RTL_LANGUAGES.includes(primaryLang)) return 'rtl'
40
+ }
41
+
42
+ return 'ltr'
43
+ }
44
+
45
+ /**
46
+ * Checks if current document direction is RTL
47
+ * @returns {boolean}
48
+ */
49
+ export function isRTL() {
50
+ return detectDirection() === 'rtl'
51
+ }
5
52
  /**
6
53
  * Finds the closest ancestor of the given element that has the given attribute.
7
54
  *
@@ -123,19 +170,22 @@ export function getSnippet(obj, key, defaultSnippet = null) {
123
170
  }
124
171
 
125
172
  /**
126
- * Creates a collection of icon import functions based on an icons dictionary
173
+ * Resolve which snippet to render for a proxy item.
127
174
  *
128
- * @param {Object} icons - Dictionary mapping collection names to file paths
129
- * @returns {Object} Collections object with functions that dynamically import icon files
175
+ * Checks proxy.snippet for a per-item named override first (e.g. item.snippet = 'highlighted').
176
+ * Falls back to the component-level fallback snippet name (e.g. 'itemContent' / 'groupContent').
177
+ * Returns null if neither is found.
178
+ *
179
+ * @param {Record<string, unknown>} snippets - snippets passed to the component
180
+ * @param {{ snippet?: string | null }} proxy - any object with an optional .snippet property
181
+ * @param {string} [fallback] - fallback snippet name; defaults to ITEM_SNIPPET ('itemContent')
182
+ * @returns {Function | null}
130
183
  */
131
- export const importIcons = (icons) => {
132
- if (!icons) return {}
133
-
134
- return Object.entries(icons).reduce((acc, [key, value]) => {
135
- acc[key] = () =>
136
- import(/* @vite-ignore */ value, { with: { type: 'json' } }).then((i) => i.default)
137
- return acc
138
- }, {})
184
+ export function resolveSnippet(snippets, proxy, fallback = ITEM_SNIPPET) {
185
+ const name = proxy?.snippet
186
+ if (name && typeof snippets[name] === 'function') return snippets[name]
187
+ const fb = snippets[fallback]
188
+ return typeof fb === 'function' ? fb : null
139
189
  }
140
190
 
141
191
  /**
@@ -178,8 +228,8 @@ function isImageUrl(str) {
178
228
 
179
229
  // Fallback if URL constructor is not available
180
230
  return fallbackValidation()
181
- // eslint-disable-next-line no-unused-vars
182
- } catch (e) {
231
+
232
+ } catch {
183
233
  // Fallback if URL constructor fails
184
234
  return fallbackValidation()
185
235
  }
@@ -0,0 +1,73 @@
1
+ import { createRequire } from 'module'
2
+ import { resolve, isAbsolute } from 'path'
3
+ import { readFileSync } from 'fs'
4
+
5
+ // Two require contexts: CWD (for site-specific packages like @iconify-json/*)
6
+ // and the module's own location (for workspace packages like @rokkit/icons).
7
+ const requireFromCwd = createRequire(resolve(process.cwd(), 'package.json'))
8
+ const requireFromModule = createRequire(import.meta.url)
9
+
10
+ function requirePackage(jsonPath) {
11
+ try {
12
+ return requireFromCwd(jsonPath)
13
+ } catch {
14
+ return requireFromModule(jsonPath)
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Creates icon collection loaders for UnoCSS presetIcons from a simple config.
20
+ *
21
+ * This function transforms a map of collection names to JSON package paths
22
+ * into the format expected by UnoCSS's presetIcons.
23
+ *
24
+ * Supports three types of paths:
25
+ * - Package paths (e.g., '@rokkit/icons/ui.json') - resolved via require
26
+ * - Absolute paths (e.g., '/path/to/icons.json') - used directly
27
+ * - Relative paths (e.g., './static/icons.json') - resolved from process.cwd()
28
+ *
29
+ * @example
30
+ * // uno.config.js
31
+ * import { iconCollections } from '@rokkit/core/vite'
32
+ *
33
+ * export default defineConfig({
34
+ * presets: [
35
+ * presetIcons({
36
+ * collections: iconCollections({
37
+ * rokkit: '@rokkit/icons/ui.json',
38
+ * logo: '@rokkit/icons/auth.json',
39
+ * solar: '@iconify-json/solar/icons.json',
40
+ * custom: './static/icons/custom.json'
41
+ * })
42
+ * })
43
+ * ]
44
+ * })
45
+ *
46
+ * @param {Record<string, string>} config - Map of collection alias to JSON path
47
+ * @returns {Record<string, () => any>} Collections object for presetIcons
48
+ */
49
+ export function iconCollections(config) {
50
+ if (!config || typeof config !== 'object') {
51
+ return {}
52
+ }
53
+
54
+ return Object.entries(config).reduce((acc, [alias, jsonPath]) => {
55
+ acc[alias] = () => {
56
+ // Check if it's a relative path (starts with ./ or ../)
57
+ if (jsonPath.startsWith('./') || jsonPath.startsWith('../')) {
58
+ // Resolve relative to current working directory (where the config is)
59
+ const absolutePath = resolve(process.cwd(), jsonPath)
60
+ return JSON.parse(readFileSync(absolutePath, 'utf-8'))
61
+ }
62
+
63
+ // Check if it's an absolute path
64
+ if (isAbsolute(jsonPath)) {
65
+ return JSON.parse(readFileSync(jsonPath, 'utf-8'))
66
+ }
67
+
68
+ // Otherwise treat as a package path; try CWD first (site packages), then module location
69
+ return requirePackage(jsonPath)
70
+ }
71
+ return acc
72
+ }, {})
73
+ }
@@ -0,0 +1 @@
1
+ export { iconCollections } from './icon-collections.js'