@rokkit/ui 1.0.0-next.136 → 1.0.0-next.138
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/components/Carousel.svelte +0 -1
- package/src/components/FloatingAction.svelte +66 -37
- package/src/components/FloatingNavigation.svelte +78 -68
- package/src/components/LazyTree.svelte +1 -2
- package/src/components/List.svelte +0 -1
- package/src/components/Menu.svelte +0 -1
- package/src/components/MultiSelect.svelte +30 -43
- package/src/components/Range.svelte +52 -62
- package/src/components/Rating.svelte +26 -16
- package/src/components/SearchFilter.svelte +1 -1
- package/src/components/Select.svelte +72 -52
- package/src/components/Switch.svelte +10 -14
- package/src/components/Table.svelte +10 -13
- package/src/components/Tabs.svelte +0 -2
- package/src/components/Toolbar.svelte +9 -13
- package/src/components/Tree.svelte +0 -1
- package/src/components/UploadTarget.svelte +13 -15
- package/src/utils/palette.ts +38 -55
- package/src/utils/shiki.ts +16 -24
- package/src/utils/upload.js +52 -38
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
// @ts-nocheck
|
|
41
41
|
import type { ProxyItem } from '@rokkit/states'
|
|
42
42
|
import { Wrapper, ProxyTree } from '@rokkit/states'
|
|
43
|
+
import { SvelteSet } from 'svelte/reactivity'
|
|
43
44
|
import { Navigator, Trigger } from '@rokkit/actions'
|
|
44
45
|
import { DEFAULT_STATE_ICONS, resolveSnippet, ITEM_SNIPPET, GROUP_SNIPPET } from '@rokkit/core'
|
|
45
46
|
import ItemContent from './ItemContent.svelte'
|
|
@@ -105,27 +106,33 @@
|
|
|
105
106
|
const textField = $derived(fields?.label || 'label')
|
|
106
107
|
const childrenField = $derived(fields?.children || 'children')
|
|
107
108
|
|
|
109
|
+
function childMatchesQuery(child: unknown, query: string): boolean {
|
|
110
|
+
return String((child as Record<string, unknown>)[textField] ?? '').toLowerCase().includes(query)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function filterGroupChildren(
|
|
114
|
+
asRecord: Record<string, unknown>,
|
|
115
|
+
query: string
|
|
116
|
+
): unknown | null {
|
|
117
|
+
const children = asRecord[childrenField] as unknown[]
|
|
118
|
+
const matching = children.filter((child: unknown) => childMatchesQuery(child, query))
|
|
119
|
+
return matching.length > 0 ? { ...asRecord, [childrenField]: matching } : null
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function filterItem(item: unknown, query: string): unknown | null {
|
|
123
|
+
const asRecord = item as Record<string, unknown>
|
|
124
|
+
const children = asRecord[childrenField]
|
|
125
|
+
if (Array.isArray(children) && children.length > 0) {
|
|
126
|
+
return filterGroupChildren(asRecord, query)
|
|
127
|
+
}
|
|
128
|
+
const text = String(asRecord[textField] ?? '').toLowerCase()
|
|
129
|
+
return text.includes(query) ? item : null
|
|
130
|
+
}
|
|
131
|
+
|
|
108
132
|
const filteredItems = $derived.by(() => {
|
|
109
133
|
if (!filterable || !filterQuery) return items
|
|
110
134
|
const query = filterQuery.toLowerCase()
|
|
111
|
-
return items
|
|
112
|
-
.map((item) => {
|
|
113
|
-
const children = item[childrenField]
|
|
114
|
-
if (Array.isArray(children) && children.length > 0) {
|
|
115
|
-
const matching = children.filter((child) =>
|
|
116
|
-
String(child[textField] ?? '')
|
|
117
|
-
.toLowerCase()
|
|
118
|
-
.includes(query)
|
|
119
|
-
)
|
|
120
|
-
return matching.length > 0 ? { ...item, [childrenField]: matching } : null
|
|
121
|
-
}
|
|
122
|
-
return String(item[textField] ?? '')
|
|
123
|
-
.toLowerCase()
|
|
124
|
-
.includes(query)
|
|
125
|
-
? item
|
|
126
|
-
: null
|
|
127
|
-
})
|
|
128
|
-
.filter(Boolean)
|
|
135
|
+
return items.map((item) => filterItem(item, query)).filter(Boolean)
|
|
129
136
|
})
|
|
130
137
|
|
|
131
138
|
// Pre-process: force groups expanded + disabled (non-navigable labels)
|
|
@@ -176,13 +183,23 @@
|
|
|
176
183
|
|
|
177
184
|
// ─── Selected proxy for trigger display ───────────────────────────────────
|
|
178
185
|
|
|
179
|
-
|
|
180
|
-
|
|
186
|
+
function isValueUnset(): boolean {
|
|
187
|
+
return value === undefined || value === null
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function isMatchingLeaf(proxy: ProxyItem): boolean {
|
|
191
|
+
return !proxy.hasChildren && proxy.value === value
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function findSelectedProxy(): ProxyItem | null {
|
|
195
|
+
if (isValueUnset()) return null
|
|
181
196
|
for (const [, proxy] of wrapper.lookup) {
|
|
182
|
-
if (
|
|
197
|
+
if (isMatchingLeaf(proxy)) return proxy
|
|
183
198
|
}
|
|
184
199
|
return null
|
|
185
|
-
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const selectedProxy = $derived.by(findSelectedProxy)
|
|
186
203
|
|
|
187
204
|
// Sync selected raw item
|
|
188
205
|
$effect(() => {
|
|
@@ -214,15 +231,18 @@
|
|
|
214
231
|
return () => t.destroy()
|
|
215
232
|
})
|
|
216
233
|
|
|
217
|
-
function
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
return
|
|
223
|
-
}
|
|
234
|
+
function moveToSelectedValue(): boolean {
|
|
235
|
+
for (const node of wrapper.flatView) {
|
|
236
|
+
if (!node.proxy.disabled && node.proxy.value === value) {
|
|
237
|
+
wrapper.moveTo(node.key)
|
|
238
|
+
return true
|
|
224
239
|
}
|
|
225
240
|
}
|
|
241
|
+
return false
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function focusSelectedOrFirst() {
|
|
245
|
+
if (value !== undefined && value !== null && moveToSelectedValue()) return
|
|
226
246
|
wrapper.first(null)
|
|
227
247
|
}
|
|
228
248
|
|
|
@@ -250,36 +270,38 @@
|
|
|
250
270
|
|
|
251
271
|
// ─── Filter keyboard (native listener, fires before Navigator) ───────────
|
|
252
272
|
|
|
273
|
+
function selectFocused() {
|
|
274
|
+
if (wrapper.focusedKey) wrapper.select(null)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function handleFilterKeyDown(event: KeyboardEvent) {
|
|
278
|
+
if (event.key === 'ArrowDown') {
|
|
279
|
+
event.preventDefault()
|
|
280
|
+
event.stopPropagation()
|
|
281
|
+
wrapper.first(null)
|
|
282
|
+
} else if (event.key === 'Escape' && filterQuery) {
|
|
283
|
+
event.preventDefault()
|
|
284
|
+
event.stopPropagation()
|
|
285
|
+
filterQuery = ''
|
|
286
|
+
} else if (event.key === 'Enter') {
|
|
287
|
+
event.preventDefault()
|
|
288
|
+
event.stopPropagation()
|
|
289
|
+
selectFocused()
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
253
293
|
$effect(() => {
|
|
254
294
|
if (!isOpen || !filterable || !filterInputRef) return
|
|
255
295
|
const el = filterInputRef
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
event.preventDefault()
|
|
259
|
-
event.stopPropagation()
|
|
260
|
-
wrapper.first(null)
|
|
261
|
-
} else if (event.key === 'Escape') {
|
|
262
|
-
if (filterQuery) {
|
|
263
|
-
event.preventDefault()
|
|
264
|
-
event.stopPropagation()
|
|
265
|
-
filterQuery = ''
|
|
266
|
-
}
|
|
267
|
-
// Empty filter: let event bubble to Navigator/Trigger for close
|
|
268
|
-
} else if (event.key === 'Enter') {
|
|
269
|
-
event.preventDefault()
|
|
270
|
-
event.stopPropagation()
|
|
271
|
-
if (wrapper.focusedKey) wrapper.select(null)
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
el.addEventListener('keydown', handler)
|
|
275
|
-
return () => el.removeEventListener('keydown', handler)
|
|
296
|
+
el.addEventListener('keydown', handleFilterKeyDown)
|
|
297
|
+
return () => el.removeEventListener('keydown', handleFilterKeyDown)
|
|
276
298
|
})
|
|
277
299
|
|
|
278
300
|
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
279
301
|
|
|
280
302
|
/** Set of group keys that need a divider before them (not the first group) */
|
|
281
303
|
const groupDividers = $derived.by(() => {
|
|
282
|
-
const set = new
|
|
304
|
+
const set = new SvelteSet<string>()
|
|
283
305
|
let foundFirst = false
|
|
284
306
|
for (const node of wrapper.flatView) {
|
|
285
307
|
if (node.hasChildren) {
|
|
@@ -334,11 +356,9 @@
|
|
|
334
356
|
</button>
|
|
335
357
|
|
|
336
358
|
{#if isOpen}
|
|
337
|
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
338
359
|
<div bind:this={dropdownRef} data-select-dropdown role="listbox" aria-orientation="vertical">
|
|
339
360
|
{#if filterable}
|
|
340
361
|
<div data-select-filter>
|
|
341
|
-
<!-- svelte-ignore a11y_autofocus -->
|
|
342
362
|
<input
|
|
343
363
|
bind:this={filterInputRef}
|
|
344
364
|
type="text"
|
|
@@ -28,22 +28,18 @@
|
|
|
28
28
|
onchange?.(nextValue, next.original as SwitchItem)
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
function shouldToggle(key: string): boolean {
|
|
32
|
+
if (key === ' ' || key === 'Enter') return true
|
|
33
|
+
if (key === 'ArrowRight') return !isChecked
|
|
34
|
+
if (key === 'ArrowLeft') return isChecked
|
|
35
|
+
return false
|
|
36
|
+
}
|
|
37
|
+
|
|
31
38
|
function handleKeyDown(event: KeyboardEvent) {
|
|
32
39
|
if (disabled) return
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
event.preventDefault()
|
|
37
|
-
toggle()
|
|
38
|
-
break
|
|
39
|
-
case 'ArrowRight':
|
|
40
|
-
event.preventDefault()
|
|
41
|
-
if (!isChecked) toggle()
|
|
42
|
-
break
|
|
43
|
-
case 'ArrowLeft':
|
|
44
|
-
event.preventDefault()
|
|
45
|
-
if (isChecked) toggle()
|
|
46
|
-
break
|
|
40
|
+
if (shouldToggle(event.key)) {
|
|
41
|
+
event.preventDefault()
|
|
42
|
+
toggle()
|
|
47
43
|
}
|
|
48
44
|
}
|
|
49
45
|
</script>
|
|
@@ -56,27 +56,25 @@
|
|
|
56
56
|
|
|
57
57
|
// ─── Focus management ───────────────────────────────────────────
|
|
58
58
|
|
|
59
|
+
function focusByKey(el: HTMLElement, key: string) {
|
|
60
|
+
const target = el.querySelector(`[data-path="${key}"]`) as HTMLElement | null
|
|
61
|
+
if (target && target !== document.activeElement) {
|
|
62
|
+
target.focus()
|
|
63
|
+
target.scrollIntoView({ block: 'nearest', inline: 'nearest' })
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
59
67
|
$effect(() => {
|
|
60
68
|
if (!tableRef) return
|
|
61
69
|
const el = tableRef
|
|
62
70
|
|
|
63
71
|
function onAction(event: Event) {
|
|
64
72
|
const detail = (event as CustomEvent).detail
|
|
65
|
-
|
|
66
73
|
if (detail.name === 'move') {
|
|
67
74
|
const key = controller.focusedKey
|
|
68
|
-
if (key)
|
|
69
|
-
const target = el.querySelector(`[data-path="${key}"]`) as HTMLElement | null
|
|
70
|
-
if (target && target !== document.activeElement) {
|
|
71
|
-
target.focus()
|
|
72
|
-
target.scrollIntoView({ block: 'nearest', inline: 'nearest' })
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (detail.name === 'select') {
|
|
78
|
-
handleSelectAction()
|
|
75
|
+
if (key) focusByKey(el, key)
|
|
79
76
|
}
|
|
77
|
+
if (detail.name === 'select') handleSelectAction()
|
|
80
78
|
}
|
|
81
79
|
|
|
82
80
|
el.addEventListener('action', onAction)
|
|
@@ -134,7 +132,6 @@
|
|
|
134
132
|
}
|
|
135
133
|
</script>
|
|
136
134
|
|
|
137
|
-
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
138
135
|
<div
|
|
139
136
|
bind:this={tableRef}
|
|
140
137
|
data-table
|
|
@@ -92,7 +92,6 @@
|
|
|
92
92
|
{/if}
|
|
93
93
|
<span data-tabs-label>{proxy.label}</span>
|
|
94
94
|
{#if editable}
|
|
95
|
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
96
95
|
<span
|
|
97
96
|
data-tabs-remove
|
|
98
97
|
role="button"
|
|
@@ -125,7 +124,6 @@
|
|
|
125
124
|
No tabs available.
|
|
126
125
|
{/snippet}
|
|
127
126
|
|
|
128
|
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
129
127
|
<div
|
|
130
128
|
bind:this={containerRef}
|
|
131
129
|
data-tabs
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { getSnippet } from '../types/menu.js'
|
|
9
9
|
import { ProxyItem, messages } from '@rokkit/states'
|
|
10
10
|
import { ListController } from '@rokkit/states'
|
|
11
|
+
import { SvelteMap } from 'svelte/reactivity'
|
|
11
12
|
import { navigator } from '@rokkit/actions'
|
|
12
13
|
import { untrack } from 'svelte'
|
|
13
14
|
|
|
@@ -51,7 +52,7 @@
|
|
|
51
52
|
|
|
52
53
|
/** Map from item → its data-path key (index within interactive items) */
|
|
53
54
|
const itemPathMap = $derived.by(() => {
|
|
54
|
-
const map = new
|
|
55
|
+
const map = new SvelteMap<unknown, string>()
|
|
55
56
|
let idx = 0
|
|
56
57
|
for (const item of items) {
|
|
57
58
|
if (isInteractive(item)) {
|
|
@@ -94,6 +95,11 @@
|
|
|
94
95
|
return () => el.removeEventListener('focusin', onFocusIn)
|
|
95
96
|
})
|
|
96
97
|
|
|
98
|
+
function focusKeyedItem(el: HTMLElement, key: string) {
|
|
99
|
+
const target = el.querySelector(`[data-path="${key}"]`) as HTMLElement | null
|
|
100
|
+
if (target && target !== document.activeElement) target.focus()
|
|
101
|
+
}
|
|
102
|
+
|
|
97
103
|
// Focus the item matching controller.focusedKey on navigator action events
|
|
98
104
|
$effect(() => {
|
|
99
105
|
if (!containerRef) return
|
|
@@ -101,20 +107,11 @@
|
|
|
101
107
|
|
|
102
108
|
function onAction(event: Event) {
|
|
103
109
|
const detail = (event as CustomEvent).detail
|
|
104
|
-
|
|
105
110
|
if (detail.name === 'move') {
|
|
106
111
|
const key = controller.focusedKey
|
|
107
|
-
if (key)
|
|
108
|
-
const target = el.querySelector(`[data-path="${key}"]`) as HTMLElement | null
|
|
109
|
-
if (target && target !== document.activeElement) {
|
|
110
|
-
target.focus()
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (detail.name === 'select') {
|
|
116
|
-
handleSelectAction()
|
|
112
|
+
if (key) focusKeyedItem(el, key)
|
|
117
113
|
}
|
|
114
|
+
if (detail.name === 'select') handleSelectAction()
|
|
118
115
|
}
|
|
119
116
|
|
|
120
117
|
el.addEventListener('action', onAction)
|
|
@@ -250,7 +247,6 @@
|
|
|
250
247
|
{/if}
|
|
251
248
|
{/snippet}
|
|
252
249
|
|
|
253
|
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
254
250
|
<div
|
|
255
251
|
bind:this={containerRef}
|
|
256
252
|
data-toolbar
|
|
@@ -38,22 +38,21 @@
|
|
|
38
38
|
let inputRef = $state<HTMLInputElement | null>(null)
|
|
39
39
|
let dragging = $state(false)
|
|
40
40
|
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
function validateFile(file: File): 'type' | 'size' | null {
|
|
42
|
+
if (accept && !matchesAccept(file, accept)) return 'type'
|
|
43
|
+
if (file.size > maxSize) return 'size'
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (file.size > maxSize) {
|
|
51
|
-
onerror?.({ file, reason: 'size' })
|
|
52
|
-
continue
|
|
53
|
-
}
|
|
54
|
-
valid.push(file)
|
|
55
|
-
}
|
|
47
|
+
function processFile(file: File, valid: File[]) {
|
|
48
|
+
const reason = validateFile(file)
|
|
49
|
+
if (reason) onerror?.({ file, reason })
|
|
50
|
+
else valid.push(file)
|
|
51
|
+
}
|
|
56
52
|
|
|
53
|
+
function handleFiles(fileList: FileList | File[]) {
|
|
54
|
+
const valid: File[] = []
|
|
55
|
+
for (const file of Array.from(fileList)) processFile(file, valid)
|
|
57
56
|
if (valid.length > 0) onfiles?.(valid)
|
|
58
57
|
}
|
|
59
58
|
|
|
@@ -86,7 +85,6 @@
|
|
|
86
85
|
}
|
|
87
86
|
</script>
|
|
88
87
|
|
|
89
|
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
90
88
|
<div
|
|
91
89
|
data-upload-target
|
|
92
90
|
data-disabled={disabled || undefined}
|
package/src/utils/palette.ts
CHANGED
|
@@ -83,38 +83,45 @@ export function rgbToHex(rgb: RGB): string {
|
|
|
83
83
|
/**
|
|
84
84
|
* Convert RGB to HSL
|
|
85
85
|
*/
|
|
86
|
+
interface NormalizedRGB { r: number; g: number; b: number }
|
|
87
|
+
|
|
88
|
+
function computeHue(norm: NormalizedRGB, max: number, d: number): number {
|
|
89
|
+
const { r, g, b } = norm
|
|
90
|
+
if (max === r) return ((g - b) / d + (g < b ? 6 : 0)) / 6
|
|
91
|
+
if (max === g) return ((b - r) / d + 2) / 6
|
|
92
|
+
return ((r - g) / d + 4) / 6
|
|
93
|
+
}
|
|
94
|
+
|
|
86
95
|
export function rgbToHsl(rgb: RGB): HSL {
|
|
87
|
-
const
|
|
88
|
-
const g = rgb.g / 255
|
|
89
|
-
const b = rgb.b / 255
|
|
96
|
+
const norm: NormalizedRGB = { r: rgb.r / 255, g: rgb.g / 255, b: rgb.b / 255 }
|
|
90
97
|
|
|
91
|
-
const max = Math.max(r, g, b)
|
|
92
|
-
const min = Math.min(r, g, b)
|
|
98
|
+
const max = Math.max(norm.r, norm.g, norm.b)
|
|
99
|
+
const min = Math.min(norm.r, norm.g, norm.b)
|
|
93
100
|
const l = (max + min) / 2
|
|
94
101
|
|
|
95
|
-
if (max === min) {
|
|
96
|
-
return { h: 0, s: 0, l: l * 100 }
|
|
97
|
-
}
|
|
102
|
+
if (max === min) return { h: 0, s: 0, l: l * 100 }
|
|
98
103
|
|
|
99
104
|
const d = max - min
|
|
100
105
|
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
|
|
101
|
-
|
|
102
|
-
let h = 0
|
|
103
|
-
switch (max) {
|
|
104
|
-
case r:
|
|
105
|
-
h = ((g - b) / d + (g < b ? 6 : 0)) / 6
|
|
106
|
-
break
|
|
107
|
-
case g:
|
|
108
|
-
h = ((b - r) / d + 2) / 6
|
|
109
|
-
break
|
|
110
|
-
case b:
|
|
111
|
-
h = ((r - g) / d + 4) / 6
|
|
112
|
-
break
|
|
113
|
-
}
|
|
106
|
+
const h = computeHue(norm, max, d)
|
|
114
107
|
|
|
115
108
|
return { h: h * 360, s: s * 100, l: l * 100 }
|
|
116
109
|
}
|
|
117
110
|
|
|
111
|
+
function normalizeT(t: number): number {
|
|
112
|
+
if (t < 0) return t + 1
|
|
113
|
+
if (t > 1) return t - 1
|
|
114
|
+
return t
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function hue2rgb(p: number, q: number, t: number): number {
|
|
118
|
+
const tt = normalizeT(t)
|
|
119
|
+
if (tt < 1 / 6) return p + (q - p) * 6 * tt
|
|
120
|
+
if (tt < 1 / 2) return q
|
|
121
|
+
if (tt < 2 / 3) return p + (q - p) * (2 / 3 - tt) * 6
|
|
122
|
+
return p
|
|
123
|
+
}
|
|
124
|
+
|
|
118
125
|
/**
|
|
119
126
|
* Convert HSL to RGB
|
|
120
127
|
*/
|
|
@@ -128,15 +135,6 @@ export function hslToRgb(hsl: HSL): RGB {
|
|
|
128
135
|
return { r: val, g: val, b: val }
|
|
129
136
|
}
|
|
130
137
|
|
|
131
|
-
const hue2rgb = (p: number, q: number, t: number) => {
|
|
132
|
-
if (t < 0) t += 1
|
|
133
|
-
if (t > 1) t -= 1
|
|
134
|
-
if (t < 1 / 6) return p + (q - p) * 6 * t
|
|
135
|
-
if (t < 1 / 2) return q
|
|
136
|
-
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6
|
|
137
|
-
return p
|
|
138
|
-
}
|
|
139
|
-
|
|
140
138
|
const q = l < 0.5 ? l * (1 + s) : l + s - l * s
|
|
141
139
|
const p = 2 * l - q
|
|
142
140
|
|
|
@@ -546,39 +544,24 @@ export function applyPalette(
|
|
|
546
544
|
}
|
|
547
545
|
}
|
|
548
546
|
|
|
547
|
+
const DEFAULT_PALETTE_ROLES: ColorRole[] = [
|
|
548
|
+
'primary', 'secondary', 'accent', 'surface', 'success', 'warning', 'danger', 'info'
|
|
549
|
+
]
|
|
550
|
+
|
|
551
|
+
const ALL_SHADE_KEYS: ShadeKey[] = [
|
|
552
|
+
'50', '100', '200', '300', '400', '500', '600', '700', '800', '900', '950'
|
|
553
|
+
]
|
|
554
|
+
|
|
549
555
|
/**
|
|
550
556
|
* Remove custom palette CSS variables (reset to theme defaults)
|
|
551
557
|
*/
|
|
552
558
|
export function resetPalette(
|
|
553
|
-
roles: ColorRole[] =
|
|
554
|
-
'primary',
|
|
555
|
-
'secondary',
|
|
556
|
-
'accent',
|
|
557
|
-
'surface',
|
|
558
|
-
'success',
|
|
559
|
-
'warning',
|
|
560
|
-
'danger',
|
|
561
|
-
'info'
|
|
562
|
-
],
|
|
559
|
+
roles: ColorRole[] = DEFAULT_PALETTE_ROLES,
|
|
563
560
|
element: HTMLElement = document.documentElement
|
|
564
561
|
): void {
|
|
565
|
-
const shadeKeys: ShadeKey[] = [
|
|
566
|
-
'50',
|
|
567
|
-
'100',
|
|
568
|
-
'200',
|
|
569
|
-
'300',
|
|
570
|
-
'400',
|
|
571
|
-
'500',
|
|
572
|
-
'600',
|
|
573
|
-
'700',
|
|
574
|
-
'800',
|
|
575
|
-
'900',
|
|
576
|
-
'950'
|
|
577
|
-
]
|
|
578
|
-
|
|
579
562
|
for (const role of roles) {
|
|
580
563
|
element.style.removeProperty(`--color-${role}`)
|
|
581
|
-
for (const shade of
|
|
564
|
+
for (const shade of ALL_SHADE_KEYS) {
|
|
582
565
|
element.style.removeProperty(`--color-${role}-${shade}`)
|
|
583
566
|
}
|
|
584
567
|
}
|
package/src/utils/shiki.ts
CHANGED
|
@@ -70,36 +70,28 @@ export interface HighlightOptions {
|
|
|
70
70
|
* @param options - Highlighting options
|
|
71
71
|
* @returns The highlighted code as HTML
|
|
72
72
|
*/
|
|
73
|
+
function resolveTheme(themeOption: string | undefined): BundledTheme {
|
|
74
|
+
if (themeOption === 'light') return 'github-light'
|
|
75
|
+
if (themeOption === 'dark' || !themeOption) return 'github-dark'
|
|
76
|
+
return themeOption as BundledTheme
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isValidCode(code: unknown): code is string {
|
|
80
|
+
return Boolean(code) && typeof code === 'string'
|
|
81
|
+
}
|
|
82
|
+
|
|
73
83
|
export async function highlightCode(code: string, options: HighlightOptions = {}): Promise<string> {
|
|
74
|
-
if (!code
|
|
75
|
-
throw new Error('Invalid code provided for highlighting')
|
|
76
|
-
}
|
|
84
|
+
if (!isValidCode(code)) throw new Error('Invalid code provided for highlighting')
|
|
77
85
|
|
|
78
86
|
const hl = await initializeHighlighter()
|
|
79
|
-
const lang = (options.lang
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (options.theme === 'light') {
|
|
83
|
-
theme = 'github-light'
|
|
84
|
-
} else if (options.theme === 'dark') {
|
|
85
|
-
theme = 'github-dark'
|
|
86
|
-
} else if (options.theme) {
|
|
87
|
-
theme = options.theme
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Check if language is supported, fallback to text
|
|
87
|
+
const lang = (options.lang ?? 'text') as BundledLanguage
|
|
88
|
+
const theme = resolveTheme(options.theme)
|
|
91
89
|
const loadedLangs = hl.getLoadedLanguages()
|
|
92
90
|
const effectiveLang = loadedLangs.includes(lang) ? lang : 'text'
|
|
93
91
|
|
|
94
|
-
return
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
lang: effectiveLang,
|
|
98
|
-
theme
|
|
99
|
-
})
|
|
100
|
-
// Remove inline styles from pre tag to allow CSS theming
|
|
101
|
-
.replace(/(<pre[^>]+) style="[^"]*"/, '$1')
|
|
102
|
-
)
|
|
92
|
+
return hl
|
|
93
|
+
.codeToHtml(code, { lang: effectiveLang, theme })
|
|
94
|
+
.replace(/(<pre[^>]+) style="[^"]*"/, '$1')
|
|
103
95
|
}
|
|
104
96
|
|
|
105
97
|
/**
|