@santi020k/lumen-astro 4.0.0 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -1
- package/components/ListBox.astro +2 -1
- package/components/Progress.astro +2 -4
- package/components/Skeleton.astro +46 -5
- package/components/Spinner.astro +4 -4
- package/package.json +8 -5
- package/runtime/UIPrimitives.astro +72 -67
- package/runtime/controllers/document-navigation.ts +6 -24
- package/runtime/controllers/list-box-proxy.ts +209 -0
- package/runtime/controllers/progress.ts +52 -0
- package/styles/critical.css +66 -5
- package/styles/tailwind.css +1 -0
package/README.md
CHANGED
|
@@ -103,12 +103,15 @@ The stylesheet defaults `--ui-font` to `"Montserrat", "Avenir Next", "Segoe UI",
|
|
|
103
103
|
It declares the family stack but does not bundle or load font files. Load Montserrat once through
|
|
104
104
|
your preferred delivery path, or override `--ui-font` in application CSS.
|
|
105
105
|
|
|
106
|
-
With Tailwind, keep the Lumen
|
|
106
|
+
With Tailwind CSS v4, keep the Lumen imports in the same shared CSS entry. The optional
|
|
107
|
+
`tailwind.css` bridge (Lumen 4.3+) maps semantic colors to utilities such as `bg-surface` and
|
|
108
|
+
`text-ink`, including nested themes. See the [Tailwind guide](https://lumen.santi020k.com/docs/web/tailwind):
|
|
107
109
|
|
|
108
110
|
```css
|
|
109
111
|
@import "@santi020k/lumen-astro/layers.css";
|
|
110
112
|
@import "tailwindcss";
|
|
111
113
|
@import "@santi020k/lumen-astro/styles.css";
|
|
114
|
+
@import "@santi020k/lumen-astro/tailwind.css";
|
|
112
115
|
```
|
|
113
116
|
|
|
114
117
|
```astro
|
|
@@ -161,6 +164,9 @@ Set `data-lumen-preset="studio"` and an optional `data-lumen-scheme="light|dark"
|
|
|
161
164
|
|
|
162
165
|
## Compound interactive components
|
|
163
166
|
|
|
167
|
+
DatePicker, Calendar, and InputOTP enhancement skips markup marked `data-ui-owner`, such as the
|
|
168
|
+
matching React controls, so those islands keep their ids, labels, and state before and after hydration.
|
|
169
|
+
|
|
164
170
|
Popover, dropdown menu, tabs, and tooltip parts expose the DOM and ARIA contracts consumed by the
|
|
165
171
|
shared progressive-enhancement runtime. Prefer these parts over recreating roles and `data-ui-*`
|
|
166
172
|
attributes by hand.
|
|
@@ -321,6 +327,9 @@ import { Button, Icon } from '@santi020k/lumen-astro'
|
|
|
321
327
|
|
|
322
328
|
`Anchor` accepts heading `depth`, `index`, and `description` metadata plus an `activationOffset`.
|
|
323
329
|
Its current link stays synchronized on click and scroll, including at the end of the document.
|
|
330
|
+
The activation line uses the greater of `activationOffset` and the root `scroll-padding-top` plus
|
|
331
|
+
each heading's `scroll-margin-top`, with a one CSS pixel rounding tolerance. Styles are read live so
|
|
332
|
+
text resizing and responsive headers keep native fragment links and the current section aligned.
|
|
324
333
|
`ScrollProgress` derives reading progress from the document and pins the indicator to the top or
|
|
325
334
|
bottom viewport edge. Both use the single root `UIPrimitives` runtime.
|
|
326
335
|
|
|
@@ -336,6 +345,24 @@ import { Anchor, CopyButton, ScrollProgress } from '@santi020k/lumen-astro'
|
|
|
336
345
|
]} />
|
|
337
346
|
```
|
|
338
347
|
|
|
348
|
+
Enhanced `DatePicker` controls mirror native input `aria-invalid`, `aria-describedby`, and
|
|
349
|
+
`aria-errormessage` attributes onto the visible trigger, including later attribute changes.
|
|
350
|
+
Valid updates and uncanceled resets clear generated validation feedback while preserving explicit
|
|
351
|
+
application errors.
|
|
352
|
+
|
|
353
|
+
`Skeleton` accepts `shape` (`rectangle`, `text`, `circle`), `width`, `height`, and `animation`
|
|
354
|
+
(`shimmer`, `pulse`, `none`). Numeric sizes are pixels; CSS lengths, percentages, and math or
|
|
355
|
+
custom-property expressions also work. Explicit styles take precedence. Empty placeholders default
|
|
356
|
+
to 1rem high, while text and element content can grow naturally. Reduced motion stops animations.
|
|
357
|
+
Unnamed Skeletons and Spinners are decorative. Give a standalone indicator an accessible name, or
|
|
358
|
+
wrap a group in one named status region with `VisuallyHidden` text.
|
|
359
|
+
|
|
360
|
+
```astro
|
|
361
|
+
<Skeleton shape="circle" width={40} />
|
|
362
|
+
<Skeleton shape="text" width="80%" />
|
|
363
|
+
<Skeleton animation="pulse" height={8} width="60%" />
|
|
364
|
+
```
|
|
365
|
+
|
|
339
366
|
`Progress` can be updated after hydration by dispatching `ui:progress-change` from its root with
|
|
340
367
|
`{ value, max? }`. Use `CopyButton` to copy arbitrary text or a referenced element without giving
|
|
341
368
|
non-code content `Code` semantics.
|
package/components/ListBox.astro
CHANGED
|
@@ -63,7 +63,8 @@ if (Array.isArray(value)) initialValues = value
|
|
|
63
63
|
else if (value !== undefined) initialValues = [value]
|
|
64
64
|
|
|
65
65
|
const selectedValues = new Set(initialValues)
|
|
66
|
-
|
|
66
|
+
// Named lists keep their stable id; unnamed lists need a unique one so labels and options never collide.
|
|
67
|
+
const controlId = id ?? (name ? `ui-list-box-${name}` : `ui-list-box-${Math.random().toString(36).slice(2)}`)
|
|
67
68
|
const listId = `${controlId}-list`
|
|
68
69
|
const { classList, passthrough } = resolveLumenAstroProps(rest, ['ui-list-box'], classProp, className)
|
|
69
70
|
---
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
import type { HTMLAttributes } from 'astro/types'
|
|
3
3
|
|
|
4
|
-
import { resolveLumenAstroProps } from '@santi020k/lumen-core'
|
|
4
|
+
import { normalizeLumenProgress, resolveLumenAstroProps } from '@santi020k/lumen-core'
|
|
5
5
|
|
|
6
6
|
interface Props extends HTMLAttributes<'div'> {
|
|
7
7
|
class?: string | null
|
|
@@ -11,9 +11,7 @@ interface Props extends HTMLAttributes<'div'> {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
const { class: classProp = '', className = '', max = 100, value = 0, ...rest } = Astro.props
|
|
14
|
-
const safeMax
|
|
15
|
-
const safeValue = Math.min(safeMax, Math.max(0, value))
|
|
16
|
-
const percentage = (safeValue / safeMax) * 100
|
|
14
|
+
const { max: safeMax, percentage, value: safeValue } = normalizeLumenProgress(max, value)
|
|
17
15
|
|
|
18
16
|
const { classList, passthrough } = resolveLumenAstroProps(rest, [
|
|
19
17
|
'ui-progress'
|
|
@@ -1,36 +1,77 @@
|
|
|
1
1
|
---
|
|
2
2
|
import type { HTMLAttributes } from 'astro/types'
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
type LumenSkeletonAnimation,
|
|
6
|
+
type LumenSkeletonShape,
|
|
7
|
+
type LumenSkeletonSize,
|
|
8
|
+
resolveLumenAstroProps,
|
|
9
|
+
resolveLumenLoadingSemantics,
|
|
10
|
+
resolveLumenSkeletonAnimation,
|
|
11
|
+
resolveLumenSkeletonShape,
|
|
12
|
+
resolveLumenSkeletonSize
|
|
13
|
+
} from '@santi020k/lumen-core'
|
|
5
14
|
|
|
6
15
|
interface Props extends HTMLAttributes<'div'> {
|
|
16
|
+
/** `shimmer` (default) keeps the existing sweep; `pulse` fades; `none` is static. */
|
|
17
|
+
animation?: LumenSkeletonAnimation
|
|
7
18
|
class?: string | null
|
|
8
19
|
className?: string
|
|
20
|
+
/** Pixels for numbers, or a CSS length such as `0.75rem` or `60%`. */
|
|
21
|
+
height?: LumenSkeletonSize
|
|
22
|
+
shape?: LumenSkeletonShape
|
|
23
|
+
width?: LumenSkeletonSize
|
|
9
24
|
}
|
|
10
25
|
|
|
11
26
|
const {
|
|
27
|
+
animation,
|
|
12
28
|
'aria-hidden': ariaHidden,
|
|
13
29
|
'aria-label': ariaLabel,
|
|
14
30
|
'aria-labelledby': ariaLabelledby,
|
|
15
31
|
class: classProp = '',
|
|
16
32
|
className = '',
|
|
33
|
+
height,
|
|
17
34
|
role,
|
|
35
|
+
shape,
|
|
36
|
+
style,
|
|
37
|
+
width,
|
|
18
38
|
...rest
|
|
19
39
|
} = Astro.props
|
|
20
40
|
|
|
21
|
-
const
|
|
41
|
+
const semantics = resolveLumenLoadingSemantics({ ariaHidden, ariaLabel, ariaLabelledby, role })
|
|
42
|
+
const resolvedShape = resolveLumenSkeletonShape(shape)
|
|
43
|
+
const resolvedAnimation = resolveLumenSkeletonAnimation(animation)
|
|
44
|
+
const sizeVariables: Record<string, string> = {}
|
|
45
|
+
const resolvedHeight = resolveLumenSkeletonSize(height)
|
|
46
|
+
const resolvedWidth = resolveLumenSkeletonSize(width)
|
|
47
|
+
|
|
48
|
+
if (resolvedHeight) sizeVariables['--ui-skeleton-height'] = resolvedHeight
|
|
49
|
+
|
|
50
|
+
if (resolvedWidth) sizeVariables['--ui-skeleton-width'] = resolvedWidth
|
|
51
|
+
|
|
52
|
+
// Consumer styles follow the size variables so explicit declarations still win.
|
|
53
|
+
const skeletonStyle = typeof style === 'object' && style !== null ?
|
|
54
|
+
{ ...sizeVariables, ...style } :
|
|
55
|
+
[
|
|
56
|
+
...Object.entries(sizeVariables).map(([name, value]) => `${name}:${value}`),
|
|
57
|
+
typeof style === 'string' ? style : ''
|
|
58
|
+
].filter(Boolean).join(';')
|
|
22
59
|
|
|
23
60
|
const { classList, passthrough } = resolveLumenAstroProps(rest, [
|
|
24
|
-
'ui-skeleton'
|
|
61
|
+
'ui-skeleton',
|
|
62
|
+
resolvedShape === 'rectangle' ? '' : `ui-skeleton--${resolvedShape}`,
|
|
63
|
+
resolvedAnimation === 'pulse' ? 'ui-skeleton--pulse' : '',
|
|
64
|
+
resolvedAnimation === 'none' ? 'ui-skeleton--static' : ''
|
|
25
65
|
], classProp, className)
|
|
26
66
|
---
|
|
27
67
|
|
|
28
68
|
<div
|
|
29
|
-
aria-hidden={ariaHidden ??
|
|
69
|
+
aria-hidden={ariaHidden ?? semantics.ariaHidden}
|
|
30
70
|
aria-label={ariaLabel}
|
|
31
71
|
aria-labelledby={ariaLabelledby}
|
|
32
72
|
class:list={classList}
|
|
33
|
-
role={role ??
|
|
73
|
+
role={role ?? semantics.role}
|
|
74
|
+
style={skeletonStyle || undefined}
|
|
34
75
|
{...passthrough}
|
|
35
76
|
>
|
|
36
77
|
<slot />
|
package/components/Spinner.astro
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
import type { HTMLAttributes } from 'astro/types'
|
|
3
3
|
|
|
4
|
-
import { resolveLumenAstroProps } from '@santi020k/lumen-core'
|
|
4
|
+
import { resolveLumenAstroProps, resolveLumenLoadingSemantics } from '@santi020k/lumen-core'
|
|
5
5
|
|
|
6
6
|
interface Props extends HTMLAttributes<'span'> {
|
|
7
7
|
class?: string | null
|
|
@@ -18,7 +18,7 @@ const {
|
|
|
18
18
|
...rest
|
|
19
19
|
} = Astro.props
|
|
20
20
|
|
|
21
|
-
const
|
|
21
|
+
const semantics = resolveLumenLoadingSemantics({ ariaHidden, ariaLabel, ariaLabelledby, role })
|
|
22
22
|
|
|
23
23
|
const { classList, passthrough } = resolveLumenAstroProps(rest, [
|
|
24
24
|
'ui-spinner'
|
|
@@ -26,11 +26,11 @@ const { classList, passthrough } = resolveLumenAstroProps(rest, [
|
|
|
26
26
|
---
|
|
27
27
|
|
|
28
28
|
<span
|
|
29
|
-
aria-hidden={ariaHidden ??
|
|
29
|
+
aria-hidden={ariaHidden ?? semantics.ariaHidden}
|
|
30
30
|
aria-label={ariaLabel}
|
|
31
31
|
aria-labelledby={ariaLabelledby}
|
|
32
32
|
class:list={classList}
|
|
33
|
-
role={role ??
|
|
33
|
+
role={role ?? semantics.role}
|
|
34
34
|
{...passthrough}
|
|
35
35
|
>
|
|
36
36
|
<slot />
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@santi020k/lumen-astro",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
"sideEffects": [
|
|
10
10
|
"./styles/lumen.css",
|
|
11
11
|
"./styles/critical.css",
|
|
12
|
-
"./styles.css"
|
|
12
|
+
"./styles.css",
|
|
13
|
+
"./styles/tailwind.css",
|
|
14
|
+
"./styles/layers.css"
|
|
13
15
|
],
|
|
14
16
|
"types": "./index.ts",
|
|
15
17
|
"exports": {
|
|
@@ -26,7 +28,8 @@
|
|
|
26
28
|
"./layers.css": "./styles/layers.css",
|
|
27
29
|
"./styles.css": "./styles/lumen.css",
|
|
28
30
|
"./styles/critical.css": "./styles/critical.css",
|
|
29
|
-
"./package.json": "./package.json"
|
|
31
|
+
"./package.json": "./package.json",
|
|
32
|
+
"./tailwind.css": "./styles/tailwind.css"
|
|
30
33
|
},
|
|
31
34
|
"files": [
|
|
32
35
|
"bin",
|
|
@@ -43,8 +46,8 @@
|
|
|
43
46
|
"lumen": "./bin/lumen.mjs"
|
|
44
47
|
},
|
|
45
48
|
"dependencies": {
|
|
46
|
-
"@santi020k/lumen": "^4.
|
|
47
|
-
"@santi020k/lumen-core": "^4.
|
|
49
|
+
"@santi020k/lumen": "^4.3.0",
|
|
50
|
+
"@santi020k/lumen-core": "^4.3.0",
|
|
48
51
|
"uqr": "^0.1.3"
|
|
49
52
|
},
|
|
50
53
|
"devDependencies": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
<script>
|
|
12
|
-
import { isLumenDateBoundsValid, parseLumenDate as parseCalendarDate, resolveLumenDateLabels, resolveLumenDateLocale } from '@santi020k/lumen-core'
|
|
12
|
+
import { connectLumenFloatingPanel, isLumenDateBoundsValid, parseLumenDate as parseCalendarDate, resolveLumenDateLabels, resolveLumenDateLocale } from '@santi020k/lumen-core'
|
|
13
13
|
import {
|
|
14
14
|
coerceThemePreset,
|
|
15
15
|
createLumenKanbanMoveDetail,
|
|
@@ -36,8 +36,10 @@ import {
|
|
|
36
36
|
import { initAmountFields } from './controllers/amount-field'
|
|
37
37
|
import { clearComboboxes, closeCombobox, initComboboxes } from './controllers/combobox'
|
|
38
38
|
import { initDataTableSorting } from './controllers/data-table'
|
|
39
|
+
import { bindListBoxProxy } from './controllers/list-box-proxy'
|
|
39
40
|
import { initMessageFeeds } from './controllers/message-scroller'
|
|
40
41
|
import { initOptionalMediaControllers } from './controllers/optional-media'
|
|
42
|
+
import { initProgress } from './controllers/progress'
|
|
41
43
|
|
|
42
44
|
/* eslint-disable complexity */
|
|
43
45
|
/* Stateful DOM handlers stay local so Astro consumers do not need a framework runtime. */
|
|
@@ -773,9 +775,15 @@ const getListBoxSelectedValues = (select: HTMLSelectElement): string[] => [
|
|
|
773
775
|
...select.selectedOptions
|
|
774
776
|
].map(option => option.value)
|
|
775
777
|
|
|
778
|
+
const isOwnedByAnotherAdapter = (root: Element): boolean => root.hasAttribute('data-ui-owner')
|
|
779
|
+
// A bound root that was removed and inserted again follows its external labels once reinitialized.
|
|
780
|
+
const listBoxResyncs = new WeakMap<HTMLElement, () => void>()
|
|
781
|
+
|
|
776
782
|
const initListBoxes = (scope: ParentNode): void => {
|
|
777
783
|
for (const root of scope.querySelectorAll<HTMLElement>('[data-ui-list-box]')) {
|
|
778
|
-
if (root.dataset.uiListBoxBound === 'true')
|
|
784
|
+
if (root.dataset.uiListBoxBound === 'true') listBoxResyncs.get(root)?.()
|
|
785
|
+
|
|
786
|
+
if (root.dataset.uiListBoxBound === 'true' || isOwnedByAnotherAdapter(root)) continue
|
|
779
787
|
|
|
780
788
|
const select = root.querySelector<HTMLSelectElement>('[data-ui-list-box-native]')
|
|
781
789
|
const list = root.querySelector<HTMLElement>('[data-ui-list-box-list]')
|
|
@@ -789,6 +797,8 @@ const initListBoxes = (scope: ParentNode): void => {
|
|
|
789
797
|
|
|
790
798
|
list.hidden = false
|
|
791
799
|
|
|
800
|
+
const optionIdPrefix = select.id || nextRuntimeId('ui-list-box', root.ownerDocument)
|
|
801
|
+
|
|
792
802
|
let activeIndex = Math.max(
|
|
793
803
|
0, options.findIndex(option => option.getAttribute('aria-disabled') !== 'true')
|
|
794
804
|
)
|
|
@@ -798,7 +808,7 @@ const initListBoxes = (scope: ParentNode): void => {
|
|
|
798
808
|
|
|
799
809
|
const syncActive = (): void => {
|
|
800
810
|
options.forEach((option, index) => {
|
|
801
|
-
option.id ||= `${
|
|
811
|
+
option.id ||= `${optionIdPrefix}-option-${index}`
|
|
802
812
|
|
|
803
813
|
option.dataset.active = index === activeIndex ? 'true' : 'false'
|
|
804
814
|
})
|
|
@@ -929,19 +939,17 @@ const initListBoxes = (scope: ParentNode): void => {
|
|
|
929
939
|
})
|
|
930
940
|
})
|
|
931
941
|
|
|
932
|
-
|
|
942
|
+
listBoxResyncs.set(root, bindListBoxProxy(root, select, list, () => nextRuntimeId('ui-list-box-label', root.ownerDocument), () => {
|
|
943
|
+
if (!options.length) return
|
|
933
944
|
|
|
934
|
-
|
|
935
|
-
window.setTimeout(() => {
|
|
936
|
-
syncSelected()
|
|
945
|
+
syncSelected()
|
|
937
946
|
|
|
938
|
-
|
|
939
|
-
0, options.findIndex(option => option.getAttribute('aria-selected') === 'true')
|
|
940
|
-
)
|
|
947
|
+
activeIndex = Math.max(0, options.findIndex(option => option.getAttribute('aria-selected') === 'true'))
|
|
941
948
|
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
949
|
+
syncActive()
|
|
950
|
+
}))
|
|
951
|
+
|
|
952
|
+
select.addEventListener('change', syncSelected)
|
|
945
953
|
|
|
946
954
|
syncSelected()
|
|
947
955
|
|
|
@@ -1618,50 +1626,6 @@ const initCopyButtons = (scope: ParentNode): void => {
|
|
|
1618
1626
|
}
|
|
1619
1627
|
}
|
|
1620
1628
|
|
|
1621
|
-
interface ProgressChangeDetail {
|
|
1622
|
-
max?: number
|
|
1623
|
-
value?: number
|
|
1624
|
-
}
|
|
1625
|
-
|
|
1626
|
-
const updateProgress = (
|
|
1627
|
-
root: HTMLElement,
|
|
1628
|
-
{ max, value }: ProgressChangeDetail
|
|
1629
|
-
): void => {
|
|
1630
|
-
const currentMax = Number(root.getAttribute('aria-valuemax')) || 100
|
|
1631
|
-
const safeMax = max && max > 0 ? max : currentMax
|
|
1632
|
-
const currentValue = Number(root.getAttribute('aria-valuenow')) || 0
|
|
1633
|
-
|
|
1634
|
-
const safeValue = Math.min(
|
|
1635
|
-
safeMax, Math.max(0, Number.isFinite(value) ? (value ?? 0) : currentValue)
|
|
1636
|
-
)
|
|
1637
|
-
|
|
1638
|
-
const indicator = root.querySelector<HTMLElement>(
|
|
1639
|
-
'[data-slot="progress-indicator"]'
|
|
1640
|
-
)
|
|
1641
|
-
|
|
1642
|
-
root.setAttribute('aria-valuemax', String(safeMax))
|
|
1643
|
-
|
|
1644
|
-
root.setAttribute('aria-valuenow', String(safeValue))
|
|
1645
|
-
|
|
1646
|
-
if (indicator) indicator.style.width = `${(safeValue / safeMax) * 100}%`
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
const initProgress = (scope: ParentNode): void => {
|
|
1650
|
-
for (const root of scope.querySelectorAll<HTMLElement>('[data-ui-progress]')) {
|
|
1651
|
-
if (root.dataset.uiProgressBound === 'true') continue
|
|
1652
|
-
|
|
1653
|
-
root.dataset.uiProgressBound = 'true'
|
|
1654
|
-
|
|
1655
|
-
root.addEventListener('ui:progress-change', event => {
|
|
1656
|
-
const detail = event instanceof CustomEvent ?
|
|
1657
|
-
(event.detail as ProgressChangeDetail) :
|
|
1658
|
-
{}
|
|
1659
|
-
|
|
1660
|
-
updateProgress(root, detail)
|
|
1661
|
-
})
|
|
1662
|
-
}
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
1629
|
const filterItems = (input: HTMLInputElement, items: HTMLElement[]): void => {
|
|
1666
1630
|
const query = input.value.trim().toLowerCase()
|
|
1667
1631
|
|
|
@@ -2248,9 +2212,10 @@ const sanitizeOtpValue = (input: HTMLInputElement, value: string, length: number
|
|
|
2248
2212
|
return normalized.slice(0, length)
|
|
2249
2213
|
}
|
|
2250
2214
|
|
|
2215
|
+
// Markup whose behavior another adapter owns, such as a React island, is never enhanced here.
|
|
2251
2216
|
const initInputOtpFields = (scope: ParentNode): void => {
|
|
2252
2217
|
for (const root of scope.querySelectorAll<HTMLElement>('[data-ui-input-otp]')) {
|
|
2253
|
-
if (root.dataset.uiBound === 'true') continue
|
|
2218
|
+
if (root.dataset.uiBound === 'true' || isOwnedByAnotherAdapter(root)) continue
|
|
2254
2219
|
|
|
2255
2220
|
const input = root.querySelector<HTMLInputElement>('[data-ui-input-otp-native]')
|
|
2256
2221
|
const segments = [...root.querySelectorAll<HTMLButtonElement>('[data-ui-input-otp-segment]')]
|
|
@@ -2867,7 +2832,7 @@ const moveCalendarFocus = (root: HTMLElement, currentDate: Date, key: string): v
|
|
|
2867
2832
|
|
|
2868
2833
|
const initCalendars = (scope: ParentNode): void => {
|
|
2869
2834
|
for (const root of scope.querySelectorAll<HTMLElement>('[data-ui-calendar]')) {
|
|
2870
|
-
if (root.dataset.uiBound === 'true') continue
|
|
2835
|
+
if (root.dataset.uiBound === 'true' || isOwnedByAnotherAdapter(root)) continue
|
|
2871
2836
|
|
|
2872
2837
|
const input = root.querySelector<HTMLInputElement>('[data-ui-calendar-input]')
|
|
2873
2838
|
|
|
@@ -3011,12 +2976,22 @@ const formatDatePickerValue = (value: string, placeholder: string, locale: strin
|
|
|
3011
2976
|
}).format(date)
|
|
3012
2977
|
}
|
|
3013
2978
|
|
|
2979
|
+
// Each open picker's shared floating-panel connection; closing disconnects it.
|
|
2980
|
+
const datePickerPanels = new WeakMap<HTMLElement, () => void>()
|
|
2981
|
+
|
|
3014
2982
|
const closeDatePicker = (root: HTMLElement, restoreFocus = false): void => {
|
|
2983
|
+
if (isOwnedByAnotherAdapter(root)) return
|
|
2984
|
+
|
|
3015
2985
|
const trigger = root.querySelector<HTMLButtonElement>('[data-ui-date-picker-trigger]')
|
|
3016
2986
|
const popover = root.querySelector<HTMLElement>('[data-ui-date-picker-popover]')
|
|
3017
2987
|
|
|
3018
2988
|
if (!trigger || !popover) return
|
|
3019
2989
|
|
|
2990
|
+
// Leave the top layer and restore the popover's inline styles before hiding it.
|
|
2991
|
+
datePickerPanels.get(root)?.()
|
|
2992
|
+
|
|
2993
|
+
datePickerPanels.delete(root)
|
|
2994
|
+
|
|
3020
2995
|
popover.hidden = true
|
|
3021
2996
|
|
|
3022
2997
|
popover.dataset.state = 'closed'
|
|
@@ -3026,6 +3001,24 @@ const closeDatePicker = (root: HTMLElement, restoreFocus = false): void => {
|
|
|
3026
3001
|
if (restoreFocus) trigger.focus()
|
|
3027
3002
|
}
|
|
3028
3003
|
|
|
3004
|
+
const invalidDatePickerInputs = new WeakSet<HTMLInputElement>()
|
|
3005
|
+
|
|
3006
|
+
const syncDatePickerValidity = (native: HTMLInputElement, trigger: HTMLElement): void => {
|
|
3007
|
+
if (native.validity.valid) invalidDatePickerInputs.delete(native)
|
|
3008
|
+
|
|
3009
|
+
const invalid = native.getAttribute('aria-invalid') ?? (invalidDatePickerInputs.has(native) ? 'true' : null)
|
|
3010
|
+
|
|
3011
|
+
if (invalid === null) trigger.removeAttribute('aria-invalid')
|
|
3012
|
+
else trigger.setAttribute('aria-invalid', invalid)
|
|
3013
|
+
|
|
3014
|
+
for (const attribute of ['aria-label', 'aria-labelledby', 'aria-describedby', 'aria-errormessage']) {
|
|
3015
|
+
const value = native.getAttribute(attribute)
|
|
3016
|
+
|
|
3017
|
+
if (value === null) trigger.removeAttribute(attribute)
|
|
3018
|
+
else trigger.setAttribute(attribute, value)
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3029
3022
|
const syncDatePickerValue = (root: HTMLElement): void => {
|
|
3030
3023
|
const native = root.querySelector<HTMLInputElement>('[data-ui-date-picker-native]')
|
|
3031
3024
|
const value = root.querySelector<HTMLElement>('[data-ui-date-picker-value]')
|
|
@@ -3043,9 +3036,9 @@ const syncDatePickerValue = (root: HTMLElement): void => {
|
|
|
3043
3036
|
root.dataset.placeholder = 'true'
|
|
3044
3037
|
}
|
|
3045
3038
|
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3039
|
+
const trigger = root.querySelector<HTMLElement>('[data-ui-date-picker-trigger]')
|
|
3040
|
+
|
|
3041
|
+
if (trigger) syncDatePickerValidity(native, trigger)
|
|
3049
3042
|
}
|
|
3050
3043
|
|
|
3051
3044
|
const openDatePicker = (root: HTMLElement): void => {
|
|
@@ -3081,6 +3074,14 @@ const openDatePicker = (root: HTMLElement): void => {
|
|
|
3081
3074
|
|
|
3082
3075
|
popover.dataset.state = 'open'
|
|
3083
3076
|
|
|
3077
|
+
datePickerPanels.get(root)?.()
|
|
3078
|
+
|
|
3079
|
+
datePickerPanels.set(root, connectLumenFloatingPanel(popover, trigger, {
|
|
3080
|
+
onDismiss: () => {
|
|
3081
|
+
closeDatePicker(root)
|
|
3082
|
+
}
|
|
3083
|
+
}))
|
|
3084
|
+
|
|
3084
3085
|
trigger.setAttribute('aria-expanded', 'true')
|
|
3085
3086
|
|
|
3086
3087
|
window.requestAnimationFrame(() => {
|
|
@@ -3095,7 +3096,7 @@ const openDatePicker = (root: HTMLElement): void => {
|
|
|
3095
3096
|
|
|
3096
3097
|
const initDatePickers = (scope: ParentNode): void => {
|
|
3097
3098
|
for (const root of scope.querySelectorAll<HTMLElement>('[data-ui-date-picker]')) {
|
|
3098
|
-
if (root.dataset.uiDatePickerBound === 'true') continue
|
|
3099
|
+
if (root.dataset.uiDatePickerBound === 'true' || isOwnedByAnotherAdapter(root)) continue
|
|
3099
3100
|
|
|
3100
3101
|
const native = root.querySelector<HTMLInputElement>('[data-ui-date-picker-native]')
|
|
3101
3102
|
const control = root.querySelector<HTMLElement>('[data-ui-date-picker-control]')
|
|
@@ -3147,13 +3148,13 @@ const initDatePickers = (scope: ParentNode): void => {
|
|
|
3147
3148
|
|
|
3148
3149
|
syncState()
|
|
3149
3150
|
|
|
3150
|
-
const observer = new MutationObserver(
|
|
3151
|
-
closeDatePicker(root)
|
|
3151
|
+
const observer = new MutationObserver(mutations => {
|
|
3152
|
+
if (mutations.some(mutation => !mutation.attributeName?.startsWith('aria-'))) closeDatePicker(root)
|
|
3152
3153
|
|
|
3153
3154
|
syncState()
|
|
3154
3155
|
})
|
|
3155
3156
|
|
|
3156
|
-
observer.observe(native, { attributes: true, attributeFilter: ['disabled', 'readonly', 'required', 'value', 'min', 'max', 'placeholder'] })
|
|
3157
|
+
observer.observe(native, { attributes: true, attributeFilter: ['disabled', 'readonly', 'required', 'value', 'min', 'max', 'placeholder', 'aria-invalid', 'aria-errormessage', 'aria-describedby', 'aria-label', 'aria-labelledby'] })
|
|
3157
3158
|
|
|
3158
3159
|
observer.observe(root, { attributes: true, attributeFilter: ['locale', 'lang', 'data-locale', 'data-choose-date-label'] })
|
|
3159
3160
|
|
|
@@ -3207,7 +3208,9 @@ const initDatePickers = (scope: ParentNode): void => {
|
|
|
3207
3208
|
native.addEventListener('invalid', event => {
|
|
3208
3209
|
event.preventDefault()
|
|
3209
3210
|
|
|
3210
|
-
|
|
3211
|
+
invalidDatePickerInputs.add(native)
|
|
3212
|
+
|
|
3213
|
+
syncDatePickerValidity(native, trigger)
|
|
3211
3214
|
|
|
3212
3215
|
trigger.focus()
|
|
3213
3216
|
})
|
|
@@ -3216,6 +3219,8 @@ const initDatePickers = (scope: ParentNode): void => {
|
|
|
3216
3219
|
globalThis.setTimeout(() => {
|
|
3217
3220
|
if (event.defaultPrevented || !root.isConnected) return
|
|
3218
3221
|
|
|
3222
|
+
invalidDatePickerInputs.delete(native)
|
|
3223
|
+
|
|
3219
3224
|
syncDatePickerValue(root)
|
|
3220
3225
|
|
|
3221
3226
|
closeDatePicker(root)
|
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
try {
|
|
3
|
-
return decodeURIComponent(fragment)
|
|
4
|
-
} catch {
|
|
5
|
-
// A literal percent sign can still identify a section; isolate malformed escapes.
|
|
6
|
-
return fragment
|
|
7
|
-
}
|
|
8
|
-
}
|
|
1
|
+
import { decodeLumenAnchorFragment, getLumenActiveAnchorIndex } from '@santi020k/lumen-core'
|
|
9
2
|
|
|
10
3
|
interface AnchorDispatcher {
|
|
11
4
|
callbacks: WeakMap<HTMLElement, () => void>
|
|
@@ -56,7 +49,7 @@ const initAnchors = (scope: ParentNode): void => {
|
|
|
56
49
|
|
|
57
50
|
for (const link of links) {
|
|
58
51
|
const fragment = link.getAttribute('href')?.slice(1) ?? ''
|
|
59
|
-
const id =
|
|
52
|
+
const id = decodeLumenAnchorFragment(fragment)
|
|
60
53
|
const target = id ? ownerDocument.getElementById(id) : null
|
|
61
54
|
|
|
62
55
|
if (target) {
|
|
@@ -66,6 +59,8 @@ const initAnchors = (scope: ParentNode): void => {
|
|
|
66
59
|
|
|
67
60
|
if (!targets.length) continue
|
|
68
61
|
|
|
62
|
+
const headings = targets.map(({ target }) => target)
|
|
63
|
+
|
|
69
64
|
const setActive = (active: HTMLAnchorElement): void => {
|
|
70
65
|
for (const link of links) {
|
|
71
66
|
const isActive = link === active
|
|
@@ -81,21 +76,8 @@ const initAnchors = (scope: ParentNode): void => {
|
|
|
81
76
|
}
|
|
82
77
|
|
|
83
78
|
const update = (): void => {
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
const atEnd = maximum > 0 && scrollingElement.scrollTop >= maximum - 1
|
|
87
|
-
const offset = Number(root.dataset.uiAnchorOffset) || 0
|
|
88
|
-
let active = targets[0]?.link
|
|
89
|
-
|
|
90
|
-
if (atEnd) {
|
|
91
|
-
active = targets.at(-1)?.link
|
|
92
|
-
} else {
|
|
93
|
-
for (const item of targets) {
|
|
94
|
-
if (item.target.getBoundingClientRect().top > offset) break
|
|
95
|
-
|
|
96
|
-
active = item.link
|
|
97
|
-
}
|
|
98
|
-
}
|
|
79
|
+
const index = getLumenActiveAnchorIndex(headings, Number(root.dataset.uiAnchorOffset))
|
|
80
|
+
const active = targets[index]?.link
|
|
99
81
|
|
|
100
82
|
if (active) setActive(active)
|
|
101
83
|
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
const forwardedAttributes = ['aria-label', 'aria-labelledby', 'aria-describedby', 'aria-errormessage'] as const
|
|
2
|
+
// A new select id changes which labels name it.
|
|
3
|
+
const observedAttributes = [...forwardedAttributes, 'aria-invalid', 'disabled', 'form', 'id', 'required']
|
|
4
|
+
|
|
5
|
+
// The first invalid control owns focus so several invalid fields do not race for it.
|
|
6
|
+
const isFirstInvalidControl = (select: HTMLSelectElement): boolean => {
|
|
7
|
+
const form = select.form
|
|
8
|
+
|
|
9
|
+
if (!form) return true
|
|
10
|
+
|
|
11
|
+
for (const element of form.elements) {
|
|
12
|
+
if (element.matches(':invalid') && element.tagName !== 'FIELDSET') return element === select
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return true
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const setOptionalAttribute = (element: HTMLElement, name: string, value: string | null): void => {
|
|
19
|
+
if (value === null) element.removeAttribute(name)
|
|
20
|
+
else element.setAttribute(name, value)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const syncListBoxName = (
|
|
24
|
+
root: HTMLElement,
|
|
25
|
+
select: HTMLSelectElement,
|
|
26
|
+
list: HTMLElement,
|
|
27
|
+
listOwned: ReadonlySet<string>,
|
|
28
|
+
createLabelId: () => string
|
|
29
|
+
): void => {
|
|
30
|
+
for (const name of forwardedAttributes) {
|
|
31
|
+
if (listOwned.has(name)) continue
|
|
32
|
+
|
|
33
|
+
setOptionalAttribute(list, name, select.getAttribute(name) ?? (name === 'aria-label' ? root.getAttribute('aria-label') : null))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (listOwned.has('aria-labelledby') || list.hasAttribute('aria-label') || list.hasAttribute('aria-labelledby')) return
|
|
37
|
+
|
|
38
|
+
const labelIds = [...select.labels].map(label => {
|
|
39
|
+
label.id ||= createLabelId()
|
|
40
|
+
|
|
41
|
+
return label.id
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
if (labelIds.length) list.setAttribute('aria-labelledby', labelIds.join(' '))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const mayHoldListBoxLabel = (node: Node, root: HTMLElement): boolean => {
|
|
48
|
+
// Adoption preserves a node's original realm, so instanceof the destination Element is insufficient.
|
|
49
|
+
if (node.nodeType !== 1) return false
|
|
50
|
+
|
|
51
|
+
const element = node as Element
|
|
52
|
+
|
|
53
|
+
return ['LABEL', 'FORM'].includes(element.nodeName) || element.contains(root) || element.querySelector('label, form') !== null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Only label insertions, removals, `for` or `id` changes, or moving the list itself can change its labels.
|
|
57
|
+
const affectsListBoxLabels = (
|
|
58
|
+
records: readonly MutationRecord[],
|
|
59
|
+
root: HTMLElement
|
|
60
|
+
): boolean => (
|
|
61
|
+
records.some(record => record.type === 'attributes' ?
|
|
62
|
+
['LABEL', 'FORM', 'SELECT'].includes(record.target.nodeName) :
|
|
63
|
+
[...record.addedNodes, ...record.removedNodes].some(node => mayHoldListBoxLabel(node, root)))
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Once a list box is enhanced the visible list is its only focus stop and accessible control, while the
|
|
68
|
+
* native select keeps form submission, validation, and reset. Naming, description, error, required, and
|
|
69
|
+
* disabled semantics follow the select as they change, focus that reaches the select moves to the list,
|
|
70
|
+
* and native invalid events mark the list invalid until a valid change or an accepted reset. Consumer
|
|
71
|
+
* `aria-invalid` on the select always wins over generated state. External labels are followed while the root
|
|
72
|
+
* is connected; the returned function resynchronizes a root that was removed and inserted again.
|
|
73
|
+
*/
|
|
74
|
+
export const bindListBoxProxy = (
|
|
75
|
+
root: HTMLElement,
|
|
76
|
+
select: HTMLSelectElement,
|
|
77
|
+
list: HTMLElement,
|
|
78
|
+
createLabelId: () => string,
|
|
79
|
+
onResetAccepted?: () => void
|
|
80
|
+
): (() => void) => {
|
|
81
|
+
// Attributes authored on the list itself stay under the author's control.
|
|
82
|
+
const listOwned = new Set<string>(forwardedAttributes.filter(name => list.hasAttribute(name)))
|
|
83
|
+
let generatedInvalid = false
|
|
84
|
+
let resetForm: HTMLFormElement | null = null
|
|
85
|
+
|
|
86
|
+
const syncState = (): void => {
|
|
87
|
+
select.tabIndex = -1
|
|
88
|
+
|
|
89
|
+
select.setAttribute('aria-hidden', 'true')
|
|
90
|
+
|
|
91
|
+
syncListBoxName(root, select, list, listOwned, createLabelId)
|
|
92
|
+
|
|
93
|
+
setOptionalAttribute(list, 'aria-required', select.required ? 'true' : null)
|
|
94
|
+
|
|
95
|
+
// A select that is valid again or barred from validation, such as disabled or no longer required, is not invalid.
|
|
96
|
+
if (!select.willValidate || select.validity.valid) generatedInvalid = false
|
|
97
|
+
|
|
98
|
+
setOptionalAttribute(list, 'aria-invalid', select.getAttribute('aria-invalid') ?? (generatedInvalid ? 'true' : null))
|
|
99
|
+
|
|
100
|
+
list.tabIndex = select.disabled ? -1 : 0
|
|
101
|
+
|
|
102
|
+
setOptionalAttribute(list, 'aria-disabled', select.disabled ? 'true' : null)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const onReset = (event: Event): void => {
|
|
106
|
+
const form = event.currentTarget
|
|
107
|
+
|
|
108
|
+
globalThis.setTimeout(() => {
|
|
109
|
+
if (event.defaultPrevented || select.form !== form) return
|
|
110
|
+
|
|
111
|
+
generatedInvalid = false
|
|
112
|
+
|
|
113
|
+
syncState()
|
|
114
|
+
|
|
115
|
+
onResetAccepted?.()
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const followForm = (): void => {
|
|
120
|
+
const next = root.isConnected ? select.form : null
|
|
121
|
+
|
|
122
|
+
if (next === resetForm) return
|
|
123
|
+
|
|
124
|
+
resetForm?.removeEventListener('reset', onReset)
|
|
125
|
+
|
|
126
|
+
resetForm = next
|
|
127
|
+
|
|
128
|
+
resetForm?.addEventListener('reset', onReset)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const sync = (): void => {
|
|
132
|
+
followForm()
|
|
133
|
+
|
|
134
|
+
syncState()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let labelObserver: MutationObserver | undefined
|
|
138
|
+
let observedDocument: Document | undefined
|
|
139
|
+
|
|
140
|
+
// The document observer stops once the root leaves, so a removed list box is not kept alive by it.
|
|
141
|
+
const followLabels = (): void => {
|
|
142
|
+
if (observedDocument !== root.ownerDocument) {
|
|
143
|
+
labelObserver?.disconnect()
|
|
144
|
+
|
|
145
|
+
labelObserver = undefined
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const view = root.ownerDocument.defaultView
|
|
149
|
+
|
|
150
|
+
if (labelObserver || !view || !root.isConnected) return
|
|
151
|
+
|
|
152
|
+
observedDocument = root.ownerDocument
|
|
153
|
+
|
|
154
|
+
labelObserver = new view.MutationObserver(records => {
|
|
155
|
+
if (observedDocument !== root.ownerDocument) {
|
|
156
|
+
followLabels()
|
|
157
|
+
|
|
158
|
+
sync()
|
|
159
|
+
} else if (!root.isConnected) {
|
|
160
|
+
followForm()
|
|
161
|
+
|
|
162
|
+
labelObserver?.disconnect()
|
|
163
|
+
|
|
164
|
+
labelObserver = undefined
|
|
165
|
+
} else if (affectsListBoxLabels(records, root)) {
|
|
166
|
+
sync()
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
labelObserver.observe(root.ownerDocument, {
|
|
171
|
+
attributeFilter: ['for', 'form', 'id'], attributes: true, childList: true, subtree: true
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
sync()
|
|
176
|
+
|
|
177
|
+
followLabels()
|
|
178
|
+
|
|
179
|
+
new MutationObserver(sync).observe(select, { attributeFilter: observedAttributes, attributes: true })
|
|
180
|
+
|
|
181
|
+
select.addEventListener('focus', () => {
|
|
182
|
+
// Only real select focus, such as a label click, moves to the list; focus moved elsewhere stays put.
|
|
183
|
+
if (select.ownerDocument.activeElement === select && !select.disabled) list.focus({ preventScroll: true })
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
select.addEventListener('invalid', event => {
|
|
187
|
+
event.preventDefault()
|
|
188
|
+
|
|
189
|
+
generatedInvalid = true
|
|
190
|
+
|
|
191
|
+
sync()
|
|
192
|
+
|
|
193
|
+
if (isFirstInvalidControl(select) && !select.disabled) list.focus()
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
select.addEventListener('change', () => {
|
|
197
|
+
if (!select.validity.valid) return
|
|
198
|
+
|
|
199
|
+
generatedInvalid = false
|
|
200
|
+
|
|
201
|
+
sync()
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
return () => {
|
|
205
|
+
followLabels()
|
|
206
|
+
|
|
207
|
+
sync()
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { normalizeLumenProgress } from '@santi020k/lumen-core'
|
|
2
|
+
|
|
3
|
+
interface ProgressChangeDetail {
|
|
4
|
+
max?: number | undefined
|
|
5
|
+
value?: number | undefined
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const readProgressChangeDetail = (detail: unknown): ProgressChangeDetail => {
|
|
9
|
+
if (typeof detail !== 'object' || detail === null) return {}
|
|
10
|
+
|
|
11
|
+
const max = 'max' in detail && typeof detail.max === 'number' ? detail.max : undefined
|
|
12
|
+
const value = 'value' in detail && typeof detail.value === 'number' ? detail.value : undefined
|
|
13
|
+
|
|
14
|
+
return { max, value }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const updateProgress = (
|
|
18
|
+
root: HTMLElement,
|
|
19
|
+
{ max, value }: ProgressChangeDetail
|
|
20
|
+
): void => {
|
|
21
|
+
const current = normalizeLumenProgress(
|
|
22
|
+
root.getAttribute('aria-valuemax'), root.getAttribute('aria-valuenow')
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
// Omitted or invalid fields keep the current state instead of resetting it.
|
|
26
|
+
const next = normalizeLumenProgress(
|
|
27
|
+
max !== undefined && Number.isFinite(max) && max > 0 ? max : current.max,
|
|
28
|
+
value !== undefined && Number.isFinite(value) ? value : current.value
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
const indicator = root.querySelector<HTMLElement>(
|
|
32
|
+
'[data-slot="progress-indicator"]'
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
root.setAttribute('aria-valuemax', String(next.max))
|
|
36
|
+
|
|
37
|
+
root.setAttribute('aria-valuenow', String(next.value))
|
|
38
|
+
|
|
39
|
+
if (indicator) indicator.style.width = `${next.percentage}%`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const initProgress = (scope: ParentNode): void => {
|
|
43
|
+
for (const root of scope.querySelectorAll<HTMLElement>('[data-ui-progress]')) {
|
|
44
|
+
if (root.dataset.uiProgressBound === 'true') continue
|
|
45
|
+
|
|
46
|
+
root.dataset.uiProgressBound = 'true'
|
|
47
|
+
|
|
48
|
+
root.addEventListener('ui:progress-change', event => {
|
|
49
|
+
updateProgress(root, readProgressChangeDetail(event instanceof CustomEvent ? event.detail : undefined))
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
}
|
package/styles/critical.css
CHANGED
|
@@ -172,6 +172,17 @@
|
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
@keyframes ui-skeleton-pulse {
|
|
176
|
+
0%,
|
|
177
|
+
100% {
|
|
178
|
+
opacity: 1;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
50% {
|
|
182
|
+
opacity: 0.55;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
175
186
|
@keyframes ui-spin {
|
|
176
187
|
to {
|
|
177
188
|
transform: rotate(360deg);
|
|
@@ -790,15 +801,21 @@
|
|
|
790
801
|
background-image:
|
|
791
802
|
linear-gradient(45deg, transparent 50%, hsl(var(--ink-muted)) 50%),
|
|
792
803
|
linear-gradient(135deg, hsl(var(--ink-muted)) 50%, transparent 50%);
|
|
793
|
-
background-position:
|
|
794
|
-
calc(100% - 1rem) 50%,
|
|
795
|
-
calc(100% - 0.72rem) 50%;
|
|
804
|
+
background-position: var(--_ui-select-chevron, calc(100% - 1rem) 50%, calc(100% - 0.72rem) 50%);
|
|
796
805
|
background-size:
|
|
797
806
|
0.32rem 0.32rem,
|
|
798
807
|
0.32rem 0.32rem;
|
|
799
808
|
background-repeat: no-repeat;
|
|
800
809
|
}
|
|
801
810
|
|
|
811
|
+
[dir="rtl"] {
|
|
812
|
+
--_ui-select-chevron: 0.72rem 50%, 1rem 50%;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
[dir="ltr"] {
|
|
816
|
+
--_ui-select-chevron: calc(100% - 1rem) 50%, calc(100% - 0.72rem) 50%;
|
|
817
|
+
}
|
|
818
|
+
|
|
802
819
|
.ui-label {
|
|
803
820
|
color: hsl(var(--ink));
|
|
804
821
|
font-size: 0.875rem;
|
|
@@ -820,6 +837,10 @@
|
|
|
820
837
|
width: min(100%, 34rem);
|
|
821
838
|
}
|
|
822
839
|
|
|
840
|
+
lumen-date-picker.ui-date-picker-field {
|
|
841
|
+
display: block;
|
|
842
|
+
}
|
|
843
|
+
|
|
823
844
|
.ui-combobox > .ui-input {
|
|
824
845
|
width: 100%;
|
|
825
846
|
}
|
|
@@ -1007,7 +1028,9 @@
|
|
|
1007
1028
|
}
|
|
1008
1029
|
|
|
1009
1030
|
.ui-skeleton {
|
|
1010
|
-
|
|
1031
|
+
/* A default height, not a minimum, lets explicit styles and classes size placeholders below 1rem. */
|
|
1032
|
+
width: var(--ui-skeleton-width, auto);
|
|
1033
|
+
height: var(--ui-skeleton-height, 1rem);
|
|
1011
1034
|
border-radius: var(--ui-radius-sm);
|
|
1012
1035
|
background: linear-gradient(
|
|
1013
1036
|
90deg,
|
|
@@ -1019,6 +1042,42 @@
|
|
|
1019
1042
|
animation: ui-shimmer 2.2s linear infinite;
|
|
1020
1043
|
}
|
|
1021
1044
|
|
|
1045
|
+
.ui-skeleton:where(:not(:empty)) {
|
|
1046
|
+
height: var(--ui-skeleton-height, auto);
|
|
1047
|
+
min-height: 1rem;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
lumen-skeleton.ui-skeleton,
|
|
1051
|
+
lumen-progress.ui-progress {
|
|
1052
|
+
display: block;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
.ui-skeleton--text {
|
|
1056
|
+
height: var(--ui-skeleton-height, 0.75rem);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
.ui-skeleton--circle {
|
|
1060
|
+
flex: 0 0 auto;
|
|
1061
|
+
width: var(--ui-skeleton-width, var(--ui-skeleton-height, 2.5rem));
|
|
1062
|
+
height: var(--ui-skeleton-height, var(--ui-skeleton-width, 2.5rem));
|
|
1063
|
+
border-radius: 50%;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
.ui-skeleton--pulse {
|
|
1067
|
+
background: hsl(var(--surface-strong));
|
|
1068
|
+
animation: ui-skeleton-pulse 1.6s ease-in-out infinite;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
.ui-skeleton--static {
|
|
1072
|
+
animation: none;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1076
|
+
.ui-skeleton {
|
|
1077
|
+
animation: none;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1022
1081
|
.ui-spinner {
|
|
1023
1082
|
box-sizing: border-box;
|
|
1024
1083
|
display: inline-block;
|
|
@@ -1586,7 +1645,9 @@ lumen-dialog.ui-dialog:not([hidden]):has(> [data-slot="dialog-body"]) {
|
|
|
1586
1645
|
|
|
1587
1646
|
.ui-input-otp__native[data-ui-enhanced="true"] {
|
|
1588
1647
|
position: absolute;
|
|
1589
|
-
|
|
1648
|
+
inset-block-start: 0;
|
|
1649
|
+
inset-inline-start: 0;
|
|
1650
|
+
width: 100%;
|
|
1590
1651
|
height: 1px;
|
|
1591
1652
|
overflow: hidden;
|
|
1592
1653
|
clip-path: inset(50%);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import "@santi020k/lumen/tailwind.css";
|