@santi020k/lumen-astro 3.0.0 → 4.0.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 +403 -7
- package/components/AmountField.astro +21 -0
- package/components/AnimatedNumber.astro +1 -1
- package/components/ApprovalCard.astro +30 -0
- package/components/AttachmentList.astro +15 -0
- package/components/AttachmentPreview.astro +51 -0
- package/components/Avatar.astro +2 -0
- package/components/BarChart.astro +95 -31
- package/components/BoxPlot.astro +77 -0
- package/components/Breadcrumb.astro +2 -1
- package/components/BulletChart.astro +64 -0
- package/components/Button.astro +1 -1
- package/components/Calendar.astro +54 -65
- package/components/CalendarHeatmap.astro +76 -0
- package/components/Card.astro +4 -0
- package/components/ChangeSummary.astro +36 -0
- package/components/ChartDatumActions.astro +24 -0
- package/components/ChartMotion.astro +11 -0
- package/components/Code.astro +18 -3
- package/components/CodeTabs.astro +12 -0
- package/components/ComboChart.astro +93 -11
- package/components/CountryFlag.astro +23 -0
- package/components/DataTable.astro +15 -7
- package/components/DatePicker.astro +24 -17
- package/components/DescriptionDetail.astro +18 -0
- package/components/DescriptionItem.astro +18 -0
- package/components/DescriptionTerm.astro +18 -0
- package/components/DeviceFrame.astro +79 -0
- package/components/DialogBody.astro +20 -0
- package/components/DialogClose.astro +21 -0
- package/components/DialogFooter.astro +20 -0
- package/components/DialogHeader.astro +20 -0
- package/components/DialogTitle.astro +21 -0
- package/components/DumbbellChart.astro +8 -0
- package/components/FileUpload.astro +3 -1
- package/components/FilterBar.astro +25 -0
- package/components/FunnelChart.astro +50 -0
- package/components/Grid.astro +2 -2
- package/components/Heatmap.astro +61 -12
- package/components/Histogram.astro +30 -0
- package/components/Icon.astro +7 -3
- package/components/ImageComparison.astro +74 -0
- package/components/Input.astro +2 -2
- package/components/LineChart.astro +127 -67
- package/components/LollipopChart.astro +8 -0
- package/components/MediaFilmstrip.astro +19 -0
- package/components/MediaThumbnail.astro +29 -0
- package/components/MediaViewport.astro +38 -0
- package/components/Mentions.astro +2 -1
- package/components/MessageScroller.astro +6 -0
- package/components/MotionGroup.astro +19 -0
- package/components/NativeSelect.astro +2 -2
- package/components/Pagination.astro +2 -1
- package/components/PasswordField.astro +1 -1
- package/components/PhoneInput.astro +60 -9
- package/components/PhoneNumber.astro +19 -0
- package/components/PieChart.astro +26 -3
- package/components/PromptComposer.astro +36 -0
- package/components/RangeChart.astro +38 -8
- package/components/ScatterChart.astro +62 -14
- package/components/Segmented.astro +5 -5
- package/components/Select.astro +8 -8
- package/components/SourceCitation.astro +18 -0
- package/components/Sparkline.astro +9 -10
- package/components/Stack.astro +2 -2
- package/components/StreamMessage.astro +26 -0
- package/components/Tabs.astro +3 -0
- package/components/ToolActivity.astro +17 -0
- package/components/TreeSelect.astro +1 -0
- package/components/VirtualList.astro +5 -2
- package/components/VisualEffect.astro +21 -0
- package/components/WaterfallChart.astro +27 -0
- package/components/WorldMap.astro +213 -0
- package/forms.ts +17 -4
- package/index.ts +37 -0
- package/internal/ChartInspection.astro +27 -0
- package/internal/ComparisonChart.astro +69 -0
- package/internal/IntervalChart.astro +66 -0
- package/package.json +6 -4
- package/runtime/UIPrimitives.astro +670 -647
- package/runtime/chart-actions.ts +19 -0
- package/runtime/controllers/amount-field.ts +12 -0
- package/runtime/controllers/attachments.ts +13 -0
- package/runtime/controllers/chart-activation.ts +33 -0
- package/runtime/controllers/charts.ts +33 -0
- package/runtime/controllers/combobox.ts +39 -0
- package/runtime/controllers/comparison-reset.ts +20 -0
- package/runtime/controllers/data-table.ts +142 -0
- package/runtime/controllers/dialogs.ts +32 -32
- package/runtime/controllers/document-navigation.ts +54 -22
- package/runtime/controllers/file-upload.ts +89 -0
- package/runtime/controllers/image-comparison.ts +53 -0
- package/runtime/controllers/media-selection.ts +20 -0
- package/runtime/controllers/media-viewport.ts +54 -0
- package/runtime/controllers/media-workspace.ts +8 -0
- package/runtime/controllers/message-scroller.ts +13 -0
- package/runtime/controllers/motion.ts +15 -16
- package/runtime/controllers/optional-media.ts +29 -0
- package/runtime/controllers/phone-input.ts +128 -39
- package/runtime/controllers/visual-interactions.ts +75 -0
- package/runtime/controllers/world-map.ts +154 -0
- package/styles/critical.css +634 -61
- package/styles/lumen.css +101 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { LumenChartDatumActivationDetail, LumenChartLabels } from '@santi020k/lumen-core'
|
|
2
|
+
|
|
3
|
+
export interface ChartDatumAction {
|
|
4
|
+
detail: LumenChartDatumActivationDetail
|
|
5
|
+
label: string
|
|
6
|
+
serialized: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const createChartDatumAction = (
|
|
10
|
+
detail: LumenChartDatumActivationDetail | null,
|
|
11
|
+
context: string,
|
|
12
|
+
labels: Readonly<LumenChartLabels>
|
|
13
|
+
): ChartDatumAction | null => detail ?
|
|
14
|
+
{
|
|
15
|
+
detail,
|
|
16
|
+
label: labels.formatDatumAction(context),
|
|
17
|
+
serialized: JSON.stringify(detail)
|
|
18
|
+
} :
|
|
19
|
+
null
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createLumenAmountFieldController, type LumenAmountFieldController } from '@santi020k/lumen-core'
|
|
2
|
+
|
|
3
|
+
const enhanced = new WeakMap<HTMLElement, LumenAmountFieldController>()
|
|
4
|
+
|
|
5
|
+
export const initAmountFields = (scope: ParentNode): void => {
|
|
6
|
+
for (const root of scope.querySelectorAll<HTMLElement>('[data-ui-amount-field]')) {
|
|
7
|
+
const controller = enhanced.get(root)
|
|
8
|
+
|
|
9
|
+
if (controller) controller.refresh()
|
|
10
|
+
else enhanced.set(root, createLumenAmountFieldController(root))
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createLumenAttachmentPreviewController } from '@santi020k/lumen-core'
|
|
2
|
+
|
|
3
|
+
const enhanced = new WeakSet<HTMLElement>()
|
|
4
|
+
|
|
5
|
+
export const initAttachmentPreviewControllers = (scope: ParentNode): void => {
|
|
6
|
+
for (const root of scope.querySelectorAll<HTMLElement>('[data-ui-attachment-preview]')) {
|
|
7
|
+
if (enhanced.has(root)) continue
|
|
8
|
+
|
|
9
|
+
createLumenAttachmentPreviewController(root)
|
|
10
|
+
|
|
11
|
+
enhanced.add(root)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createLumenChartActivationController } from '@santi020k/lumen-core'
|
|
2
|
+
|
|
3
|
+
const controllers = new Map<HTMLElement, ReturnType<typeof createLumenChartActivationController>>()
|
|
4
|
+
|
|
5
|
+
export const initChartActivationControllers = (scope: ParentNode): void => {
|
|
6
|
+
for (const [root, controller] of controllers) {
|
|
7
|
+
if (root.isConnected) continue
|
|
8
|
+
|
|
9
|
+
controller.destroy()
|
|
10
|
+
|
|
11
|
+
root.removeAttribute('data-ui-chart-activation-bound')
|
|
12
|
+
|
|
13
|
+
controllers.delete(root)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
for (const root of scope.querySelectorAll<HTMLElement>('[data-ui-chart-activation]:not([data-ui-chart-adapter="react"]):not([data-ui-chart-adapter="elements"])')) {
|
|
17
|
+
if (!controllers.has(root)) {
|
|
18
|
+
controllers.set(root, createLumenChartActivationController(root))
|
|
19
|
+
|
|
20
|
+
root.setAttribute('data-ui-chart-activation-bound', 'true')
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
document.addEventListener('astro:before-swap', () => {
|
|
26
|
+
for (const [root, controller] of controllers) {
|
|
27
|
+
controller.destroy()
|
|
28
|
+
|
|
29
|
+
root.removeAttribute('data-ui-chart-activation-bound')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
controllers.clear()
|
|
33
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createLumenChartInteractionController } from '@santi020k/lumen-core'
|
|
2
|
+
|
|
3
|
+
interface BoundChart {
|
|
4
|
+
controller: ReturnType<typeof createLumenChartInteractionController>
|
|
5
|
+
document: Document
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const controllers = new Map<HTMLElement, BoundChart>()
|
|
9
|
+
|
|
10
|
+
export const initChartControllers = (scope: ParentNode): void => {
|
|
11
|
+
for (const [root, binding] of controllers) {
|
|
12
|
+
if (!root.isConnected || root.ownerDocument !== binding.document) {
|
|
13
|
+
binding.controller.destroy()
|
|
14
|
+
|
|
15
|
+
controllers.delete(root)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
for (const root of scope.querySelectorAll<HTMLElement>('[data-ui-chart-interactive]')) {
|
|
20
|
+
if (!controllers.has(root)) {
|
|
21
|
+
controllers.set(root, {
|
|
22
|
+
controller: createLumenChartInteractionController(root),
|
|
23
|
+
document: root.ownerDocument
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
document.addEventListener('astro:before-swap', () => {
|
|
30
|
+
for (const { controller } of controllers.values()) controller.destroy()
|
|
31
|
+
|
|
32
|
+
controllers.clear()
|
|
33
|
+
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createLumenComboboxController, type LumenComboboxController } from '@santi020k/lumen-core'
|
|
2
|
+
|
|
3
|
+
interface BoundCombobox {
|
|
4
|
+
controller: LumenComboboxController
|
|
5
|
+
document: Document
|
|
6
|
+
eventRoot: Node
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const controllers = new Map<HTMLElement, BoundCombobox>()
|
|
10
|
+
|
|
11
|
+
export const closeCombobox = (root: HTMLElement): void => {
|
|
12
|
+
controllers.get(root)?.controller.close()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const clearComboboxes = (): void => {
|
|
16
|
+
for (const { controller } of controllers.values()) controller.destroy()
|
|
17
|
+
|
|
18
|
+
controllers.clear()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const initComboboxes = (scope: ParentNode): void => {
|
|
22
|
+
for (const [root, binding] of controllers) {
|
|
23
|
+
if (!root.isConnected || root.ownerDocument !== binding.document || root.getRootNode() !== binding.eventRoot) {
|
|
24
|
+
binding.controller.destroy()
|
|
25
|
+
|
|
26
|
+
controllers.delete(root)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (const root of scope.querySelectorAll<HTMLElement>('[data-ui-combobox]')) {
|
|
31
|
+
if (!controllers.has(root)) {
|
|
32
|
+
controllers.set(root, {
|
|
33
|
+
controller: createLumenComboboxController(root),
|
|
34
|
+
document: root.ownerDocument,
|
|
35
|
+
eventRoot: root.getRootNode()
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const roots = new WeakSet<Node>()
|
|
2
|
+
const isRoot = (node: Node): node is ParentNode => 'querySelectorAll' in node
|
|
3
|
+
|
|
4
|
+
export const bindComparisonReset = (node: Node, syncs: WeakMap<HTMLInputElement, () => number>): void => {
|
|
5
|
+
const scope = node.getRootNode()
|
|
6
|
+
|
|
7
|
+
if (!isRoot(scope) || roots.has(scope)) return
|
|
8
|
+
|
|
9
|
+
roots.add(scope)
|
|
10
|
+
|
|
11
|
+
scope.addEventListener('reset', event => {
|
|
12
|
+
setTimeout(() => {
|
|
13
|
+
if (event.defaultPrevented) return
|
|
14
|
+
|
|
15
|
+
for (const input of scope.querySelectorAll<HTMLInputElement>('[data-ui-image-comparison-input]')) {
|
|
16
|
+
if (input.isConnected && input.form === event.target) syncs.get(input)?.()
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
}, true)
|
|
20
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/* cspell:ignore Datatable datatable */
|
|
2
|
+
const nextSortDirection = (current: string | null): 'ascending' | 'descending' | 'none' => {
|
|
3
|
+
if (current === 'ascending') return 'descending'
|
|
4
|
+
|
|
5
|
+
if (current === 'descending') return 'none'
|
|
6
|
+
|
|
7
|
+
return 'ascending'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const getDataTableSortValue = (
|
|
11
|
+
cell: HTMLTableCellElement | undefined
|
|
12
|
+
): string => cell?.dataset.sortValue ?? cell?.textContent.trim() ?? ''
|
|
13
|
+
|
|
14
|
+
const compareDataTableValues = (value: string, other: string, sortType: string | undefined): number => {
|
|
15
|
+
const numericValue = Number(value.replaceAll(',', ''))
|
|
16
|
+
const numericOther = Number(other.replaceAll(',', ''))
|
|
17
|
+
|
|
18
|
+
const useNumeric = sortType === 'number' || (
|
|
19
|
+
sortType !== 'string' &&
|
|
20
|
+
value.trim() !== '' &&
|
|
21
|
+
other.trim() !== '' &&
|
|
22
|
+
Number.isFinite(numericValue) &&
|
|
23
|
+
Number.isFinite(numericOther)
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
if (useNumeric) {
|
|
27
|
+
return numericValue - numericOther
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return value.localeCompare(other, undefined, { numeric: true, sensitivity: 'base' })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const sortDataTableRows = (
|
|
34
|
+
table: HTMLTableElement,
|
|
35
|
+
header: HTMLTableCellElement,
|
|
36
|
+
columnIndex: number,
|
|
37
|
+
direction: 'ascending' | 'descending' | 'none'
|
|
38
|
+
): void => {
|
|
39
|
+
const body = table.tBodies[0]
|
|
40
|
+
|
|
41
|
+
if (!body) return
|
|
42
|
+
|
|
43
|
+
const rows = [...body.rows]
|
|
44
|
+
const sortType = header.dataset.uiDatatableSortType
|
|
45
|
+
|
|
46
|
+
rows.sort((row, other) => {
|
|
47
|
+
if (direction === 'none') {
|
|
48
|
+
return Number(row.dataset.uiDatatableIndex ?? '0') - Number(other.dataset.uiDatatableIndex ?? '0')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const result = compareDataTableValues(
|
|
52
|
+
getDataTableSortValue(row.cells[columnIndex]), getDataTableSortValue(other.cells[columnIndex]), sortType
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return (direction === 'ascending' ? result : -result) ||
|
|
56
|
+
Number(row.dataset.uiDatatableIndex ?? '0') - Number(other.dataset.uiDatatableIndex ?? '0')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
rows.forEach(row => {
|
|
60
|
+
body.append(row)
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const updateDataTableSort = (
|
|
65
|
+
root: HTMLElement,
|
|
66
|
+
table: HTMLTableElement,
|
|
67
|
+
header: HTMLTableCellElement,
|
|
68
|
+
direction: 'ascending' | 'descending' | 'none'
|
|
69
|
+
): void => {
|
|
70
|
+
for (const item of table.querySelectorAll<HTMLTableCellElement>('thead th[aria-sort]')) {
|
|
71
|
+
item.setAttribute('aria-sort', item === header ? direction : 'none')
|
|
72
|
+
|
|
73
|
+
item.dataset.sort = item === header ? direction : 'none'
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
root.dataset.uiDatatableSortDirection = direction
|
|
77
|
+
|
|
78
|
+
if (direction === 'none') {
|
|
79
|
+
delete root.dataset.uiDatatableSortColumn
|
|
80
|
+
} else {
|
|
81
|
+
root.dataset.uiDatatableSortColumn = header.dataset.uiDatatableColumn ?? ''
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const initDataTableSorting = (root: HTMLElement, table: HTMLTableElement): void => {
|
|
86
|
+
const headers = [...table.querySelectorAll<HTMLTableCellElement>('thead th')].filter(
|
|
87
|
+
header => header.dataset.uiDatatableSortable === 'true' ||
|
|
88
|
+
header.dataset.sortable === 'true' ||
|
|
89
|
+
header.hasAttribute('data-sortable')
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
for (const header of headers) {
|
|
93
|
+
if (header.dataset.uiDatatableSortBound === 'true') continue
|
|
94
|
+
|
|
95
|
+
const row = header.parentElement
|
|
96
|
+
const columnIndex = row ? [...row.children].indexOf(header) : -1
|
|
97
|
+
|
|
98
|
+
if (columnIndex < 0) continue
|
|
99
|
+
|
|
100
|
+
header.dataset.uiDatatableSortBound = 'true'
|
|
101
|
+
|
|
102
|
+
header.dataset.uiDatatableColumn = String(columnIndex)
|
|
103
|
+
|
|
104
|
+
header.setAttribute('aria-sort', header.getAttribute('aria-sort') ?? 'none')
|
|
105
|
+
|
|
106
|
+
const button = document.createElement('button')
|
|
107
|
+
|
|
108
|
+
button.type = 'button'
|
|
109
|
+
|
|
110
|
+
button.className = 'ui-data-table__sort'
|
|
111
|
+
|
|
112
|
+
button.dataset.uiDatatableSort = ''
|
|
113
|
+
|
|
114
|
+
while (header.firstChild) {
|
|
115
|
+
button.append(header.firstChild)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
header.append(button)
|
|
119
|
+
|
|
120
|
+
button.addEventListener('click', () => {
|
|
121
|
+
const currentDirection = header.getAttribute('aria-sort')
|
|
122
|
+
const nextDirection = nextSortDirection(currentDirection)
|
|
123
|
+
|
|
124
|
+
updateDataTableSort(root, table, header, nextDirection)
|
|
125
|
+
|
|
126
|
+
if (root.dataset.uiDatatableSortMode !== 'manual') sortDataTableRows(table, header, columnIndex, nextDirection)
|
|
127
|
+
|
|
128
|
+
root.dispatchEvent(new CustomEvent('ui:data-table-sort-change', {
|
|
129
|
+
bubbles: true,
|
|
130
|
+
detail: { key: header.dataset.uiDatatableSortKey ?? String(columnIndex), columnIndex, direction: nextDirection }
|
|
131
|
+
}))
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const body = table.tBodies[0]
|
|
136
|
+
|
|
137
|
+
if (!body) return
|
|
138
|
+
|
|
139
|
+
;[...body.rows].forEach((row, index) => {
|
|
140
|
+
row.dataset.uiDatatableIndex = row.dataset.uiDatatableIndex ?? String(index)
|
|
141
|
+
})
|
|
142
|
+
}
|
|
@@ -1,21 +1,13 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
typeof element.checkVisibility === 'function' ?
|
|
12
|
-
element.checkVisibility() :
|
|
13
|
-
element.offsetParent !== null
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
const getFocusable = (root: ParentNode): HTMLElement[] => [
|
|
17
|
-
...root.querySelectorAll<HTMLElement>(focusableSelector)
|
|
18
|
-
].filter(element => !element.hasAttribute('hidden') && isElementVisible(element))
|
|
1
|
+
const dialogTriggers = new WeakMap<HTMLDialogElement, HTMLElement>()
|
|
2
|
+
|
|
3
|
+
const isBackdropPoint = (dialog: HTMLDialogElement, event: MouseEvent): boolean => {
|
|
4
|
+
if (event.target !== dialog) return false
|
|
5
|
+
|
|
6
|
+
const rect = dialog.getBoundingClientRect()
|
|
7
|
+
|
|
8
|
+
return event.clientX < rect.left || event.clientX > rect.right ||
|
|
9
|
+
event.clientY < rect.top || event.clientY > rect.bottom
|
|
10
|
+
}
|
|
19
11
|
|
|
20
12
|
export const initDialogControllers = (scope: ParentNode): void => {
|
|
21
13
|
const dialogSelector =
|
|
@@ -32,17 +24,27 @@ export const initDialogControllers = (scope: ParentNode): void => {
|
|
|
32
24
|
'role', dialog.hasAttribute('data-ui-alert-dialog') ? 'alertdialog' : 'dialog'
|
|
33
25
|
)
|
|
34
26
|
|
|
27
|
+
let pressStartedOutside = false
|
|
28
|
+
|
|
29
|
+
dialog.addEventListener('pointerdown', event => {
|
|
30
|
+
pressStartedOutside = isBackdropPoint(dialog, event)
|
|
31
|
+
})
|
|
32
|
+
|
|
35
33
|
dialog.addEventListener('click', event => {
|
|
36
|
-
|
|
34
|
+
const dismiss = pressStartedOutside && event.detail > 0 &&
|
|
35
|
+
!event.defaultPrevented && isBackdropPoint(dialog, event)
|
|
36
|
+
|
|
37
|
+
pressStartedOutside = false
|
|
38
|
+
|
|
39
|
+
if (dismiss && !dialog.hasAttribute('data-ui-alert-dialog')) {
|
|
37
40
|
dialog.close()
|
|
38
41
|
}
|
|
39
42
|
})
|
|
40
43
|
|
|
41
44
|
dialog.addEventListener('close', () => {
|
|
42
|
-
const
|
|
43
|
-
const trigger = triggerId ? document.getElementById(triggerId) : null
|
|
45
|
+
const trigger = dialogTriggers.get(dialog)
|
|
44
46
|
|
|
45
|
-
if (trigger
|
|
47
|
+
if (trigger?.isConnected) {
|
|
46
48
|
trigger.focus({ preventScroll: true })
|
|
47
49
|
}
|
|
48
50
|
})
|
|
@@ -62,7 +64,9 @@ export const initDialogControllers = (scope: ParentNode): void => {
|
|
|
62
64
|
|
|
63
65
|
trigger.setAttribute('aria-haspopup', 'dialog')
|
|
64
66
|
|
|
65
|
-
trigger.addEventListener('click',
|
|
67
|
+
trigger.addEventListener('click', event => {
|
|
68
|
+
if (event.defaultPrevented || trigger.matches(':disabled, [aria-disabled="true"]')) return
|
|
69
|
+
|
|
66
70
|
const targetId =
|
|
67
71
|
trigger.dataset.uiDialogTrigger ??
|
|
68
72
|
trigger.dataset.uiAlertDialogTrigger ??
|
|
@@ -73,17 +77,11 @@ export const initDialogControllers = (scope: ParentNode): void => {
|
|
|
73
77
|
|
|
74
78
|
const dialog = document.getElementById(targetId)
|
|
75
79
|
|
|
76
|
-
if (!(dialog instanceof HTMLDialogElement)) return
|
|
77
|
-
|
|
78
|
-
if (!trigger.id) {
|
|
79
|
-
trigger.id = `ui-trigger-${crypto.randomUUID()}`
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
dialog.dataset.uiLastTrigger = trigger.id
|
|
80
|
+
if (!(dialog instanceof HTMLDialogElement) || dialog.open) return
|
|
83
81
|
|
|
84
82
|
dialog.showModal()
|
|
85
83
|
|
|
86
|
-
|
|
84
|
+
if (dialog.hasAttribute('open')) dialogTriggers.set(dialog, trigger)
|
|
87
85
|
})
|
|
88
86
|
}
|
|
89
87
|
|
|
@@ -99,7 +97,9 @@ export const initDialogControllers = (scope: ParentNode): void => {
|
|
|
99
97
|
|
|
100
98
|
closeButton.dataset.uiBound = 'true'
|
|
101
99
|
|
|
102
|
-
closeButton.addEventListener('click',
|
|
100
|
+
closeButton.addEventListener('click', event => {
|
|
101
|
+
if (event.defaultPrevented || closeButton.matches(':disabled, [aria-disabled="true"]')) return
|
|
102
|
+
|
|
103
103
|
closeButton.closest<HTMLDialogElement>('dialog')?.close()
|
|
104
104
|
})
|
|
105
105
|
}
|
|
@@ -1,15 +1,63 @@
|
|
|
1
|
+
const decodeFragment = (fragment: string): string => {
|
|
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
|
+
}
|
|
9
|
+
|
|
10
|
+
interface AnchorDispatcher {
|
|
11
|
+
callbacks: WeakMap<HTMLElement, () => void>
|
|
12
|
+
frame: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const anchorDispatchers = new WeakMap<Document, AnchorDispatcher>()
|
|
16
|
+
|
|
17
|
+
const getAnchorDispatcher = (ownerDocument: Document): AnchorDispatcher => {
|
|
18
|
+
const existing = anchorDispatchers.get(ownerDocument)
|
|
19
|
+
|
|
20
|
+
if (existing) return existing
|
|
21
|
+
|
|
22
|
+
const dispatcher: AnchorDispatcher = { callbacks: new WeakMap(), frame: 0 }
|
|
23
|
+
|
|
24
|
+
const runUpdates = (): void => {
|
|
25
|
+
dispatcher.frame = 0
|
|
26
|
+
|
|
27
|
+
for (const root of ownerDocument.querySelectorAll<HTMLElement>('[data-ui-anchor]')) {
|
|
28
|
+
dispatcher.callbacks.get(root)?.()
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const requestUpdates = (): void => {
|
|
33
|
+
if (dispatcher.frame) return
|
|
34
|
+
|
|
35
|
+
dispatcher.frame = requestAnimationFrame(runUpdates)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
ownerDocument.defaultView?.addEventListener('resize', requestUpdates, { passive: true })
|
|
39
|
+
|
|
40
|
+
ownerDocument.defaultView?.addEventListener('scroll', requestUpdates, { passive: true })
|
|
41
|
+
|
|
42
|
+
anchorDispatchers.set(ownerDocument, dispatcher)
|
|
43
|
+
|
|
44
|
+
return dispatcher
|
|
45
|
+
}
|
|
46
|
+
|
|
1
47
|
const initAnchors = (scope: ParentNode): void => {
|
|
2
48
|
for (const root of scope.querySelectorAll<HTMLElement>('[data-ui-anchor]')) {
|
|
3
49
|
if (root.dataset.uiBound === 'true') continue
|
|
4
50
|
|
|
5
51
|
root.dataset.uiBound = 'true'
|
|
6
52
|
|
|
53
|
+
const ownerDocument = root.ownerDocument
|
|
7
54
|
const links = [...root.querySelectorAll<HTMLAnchorElement>('a[href^="#"]')]
|
|
8
55
|
const targets: { link: HTMLAnchorElement, target: HTMLElement }[] = []
|
|
9
56
|
|
|
10
57
|
for (const link of links) {
|
|
11
|
-
const
|
|
12
|
-
const
|
|
58
|
+
const fragment = link.getAttribute('href')?.slice(1) ?? ''
|
|
59
|
+
const id = decodeFragment(fragment)
|
|
60
|
+
const target = id ? ownerDocument.getElementById(id) : null
|
|
13
61
|
|
|
14
62
|
if (target) {
|
|
15
63
|
targets.push({ link, target })
|
|
@@ -33,11 +81,9 @@ const initAnchors = (scope: ParentNode): void => {
|
|
|
33
81
|
}
|
|
34
82
|
|
|
35
83
|
const update = (): void => {
|
|
36
|
-
const scrollingElement =
|
|
37
|
-
|
|
38
|
-
const atEnd = scrollingElement.scrollTop
|
|
39
|
-
scrollingElement.scrollHeight - 1
|
|
40
|
-
|
|
84
|
+
const scrollingElement = ownerDocument.scrollingElement ?? ownerDocument.documentElement
|
|
85
|
+
const maximum = scrollingElement.scrollHeight - scrollingElement.clientHeight
|
|
86
|
+
const atEnd = maximum > 0 && scrollingElement.scrollTop >= maximum - 1
|
|
41
87
|
const offset = Number(root.dataset.uiAnchorOffset) || 0
|
|
42
88
|
let active = targets[0]?.link
|
|
43
89
|
|
|
@@ -54,27 +100,13 @@ const initAnchors = (scope: ParentNode): void => {
|
|
|
54
100
|
if (active) setActive(active)
|
|
55
101
|
}
|
|
56
102
|
|
|
57
|
-
let frame = 0
|
|
58
|
-
|
|
59
|
-
const requestUpdate = (): void => {
|
|
60
|
-
if (frame) return
|
|
61
|
-
|
|
62
|
-
frame = requestAnimationFrame(() => {
|
|
63
|
-
frame = 0
|
|
64
|
-
|
|
65
|
-
update()
|
|
66
|
-
})
|
|
67
|
-
}
|
|
68
|
-
|
|
69
103
|
for (const { link } of targets) {
|
|
70
104
|
link.addEventListener('click', () => {
|
|
71
105
|
setActive(link)
|
|
72
106
|
})
|
|
73
107
|
}
|
|
74
108
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
window.addEventListener('scroll', requestUpdate, { passive: true })
|
|
109
|
+
getAnchorDispatcher(ownerDocument).callbacks.set(root, update)
|
|
78
110
|
|
|
79
111
|
update()
|
|
80
112
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const renderers = new WeakMap<HTMLInputElement, () => void>()
|
|
2
|
+
const resetRoots = new WeakSet<Node>()
|
|
3
|
+
|
|
4
|
+
const isQueryRoot = (root: Node): root is Document | DocumentFragment | Element => (
|
|
5
|
+
'querySelectorAll' in root && typeof root.querySelectorAll === 'function'
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
const bindResets = (input: HTMLInputElement): void => {
|
|
9
|
+
const scope = input.getRootNode()
|
|
10
|
+
|
|
11
|
+
if (!isQueryRoot(scope) || resetRoots.has(scope)) return
|
|
12
|
+
|
|
13
|
+
resetRoots.add(scope)
|
|
14
|
+
|
|
15
|
+
scope.addEventListener('reset', event => {
|
|
16
|
+
// Wait for the native reset default action.
|
|
17
|
+
setTimeout(() => {
|
|
18
|
+
if (event.defaultPrevented) return
|
|
19
|
+
|
|
20
|
+
for (const current of scope.querySelectorAll<HTMLInputElement>('[data-ui-file-upload-input]')) {
|
|
21
|
+
if (current.isConnected && current.form === event.target) renderers.get(current)?.()
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
}, { capture: true })
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const initFileUploadControllers = (scope: ParentNode): void => {
|
|
28
|
+
for (const root of scope.querySelectorAll<HTMLElement>('[data-ui-file-upload]')) {
|
|
29
|
+
const input = root.querySelector<HTMLInputElement>('[data-ui-file-upload-input]')
|
|
30
|
+
|
|
31
|
+
if (!input) continue
|
|
32
|
+
|
|
33
|
+
bindResets(input)
|
|
34
|
+
|
|
35
|
+
if (root.dataset.uiBound === 'true') continue
|
|
36
|
+
|
|
37
|
+
root.dataset.uiBound = 'true'
|
|
38
|
+
|
|
39
|
+
const files = root.querySelector<HTMLElement>('[data-ui-file-upload-files]')
|
|
40
|
+
|
|
41
|
+
const renderFiles = (): void => {
|
|
42
|
+
const selectedFiles = input.files ? [...input.files] : []
|
|
43
|
+
|
|
44
|
+
root.dataset.state = selectedFiles.length > 0 ? 'selected' : 'idle'
|
|
45
|
+
|
|
46
|
+
if (!files) return
|
|
47
|
+
|
|
48
|
+
if (selectedFiles.length === 1) {
|
|
49
|
+
files.textContent = selectedFiles[0]?.name ?? ''
|
|
50
|
+
} else {
|
|
51
|
+
files.textContent = selectedFiles.length > 1 ?
|
|
52
|
+
(root.dataset.uiSelectedFilesLabel ?? '{count} files selected').replaceAll('{count}', String(selectedFiles.length)) :
|
|
53
|
+
''
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
input.addEventListener('change', renderFiles)
|
|
58
|
+
|
|
59
|
+
renderers.set(input, renderFiles)
|
|
60
|
+
|
|
61
|
+
root.addEventListener('dragover', event => {
|
|
62
|
+
if (input.matches(':disabled')) return
|
|
63
|
+
|
|
64
|
+
event.preventDefault()
|
|
65
|
+
|
|
66
|
+
root.dataset.state = 'drag-over'
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
root.addEventListener('dragleave', event => {
|
|
70
|
+
if (event.relatedTarget instanceof Node && root.contains(event.relatedTarget)) return
|
|
71
|
+
|
|
72
|
+
root.dataset.state = input.files?.length ? 'selected' : 'idle'
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
root.addEventListener('drop', event => {
|
|
76
|
+
if (input.matches(':disabled')) return
|
|
77
|
+
|
|
78
|
+
event.preventDefault()
|
|
79
|
+
|
|
80
|
+
if (event.dataTransfer) {
|
|
81
|
+
input.files = event.dataTransfer.files
|
|
82
|
+
|
|
83
|
+
input.dispatchEvent(new Event('change', { bubbles: true }))
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
renderFiles()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { formatLumenImageComparisonValue, normalizeLumenImageComparisonMode, normalizeLumenImageComparisonValue, syncLumenImageComparisonMode } from '@santi020k/lumen-core'
|
|
2
|
+
|
|
3
|
+
import { bindComparisonReset } from './comparison-reset.js'
|
|
4
|
+
|
|
5
|
+
const syncs = new WeakMap<HTMLInputElement, () => number>()
|
|
6
|
+
|
|
7
|
+
export const initImageComparisonControllers = (scope: ParentNode): void => {
|
|
8
|
+
for (const root of scope.querySelectorAll<HTMLElement>('[data-ui-image-comparison]')) {
|
|
9
|
+
const input = root.querySelector<HTMLInputElement>('[data-ui-image-comparison-input]')
|
|
10
|
+
const frame = root.querySelector<HTMLElement>('.ui-image-comparison__frame')
|
|
11
|
+
|
|
12
|
+
if (!input || !frame) continue
|
|
13
|
+
|
|
14
|
+
bindComparisonReset(input, syncs)
|
|
15
|
+
|
|
16
|
+
if (syncs.has(input)) continue
|
|
17
|
+
|
|
18
|
+
const update = () => {
|
|
19
|
+
syncLumenImageComparisonMode(root, normalizeLumenImageComparisonMode(root.dataset.mode))
|
|
20
|
+
|
|
21
|
+
input.disabled = root.dataset.disabled === 'true' || root.dataset.mode !== 'reveal'
|
|
22
|
+
|
|
23
|
+
const value = normalizeLumenImageComparisonValue(input.valueAsNumber)
|
|
24
|
+
|
|
25
|
+
input.value = `${value}`
|
|
26
|
+
|
|
27
|
+
input.ariaValueText = formatLumenImageComparisonValue(value, root.dataset.afterLabel ?? 'After', root.dataset.locale || root.closest('[lang]')?.getAttribute('lang') || undefined)
|
|
28
|
+
|
|
29
|
+
frame.style.setProperty('--ui-image-comparison-position', `${value}%`)
|
|
30
|
+
|
|
31
|
+
return value
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
input.addEventListener('input', () => {
|
|
35
|
+
if (input.disabled) return
|
|
36
|
+
|
|
37
|
+
root.dispatchEvent(new CustomEvent('ui:image-comparison-change', {
|
|
38
|
+
bubbles: true,
|
|
39
|
+
detail: { value: update() }
|
|
40
|
+
}))
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
input.addEventListener('change', update)
|
|
44
|
+
|
|
45
|
+
const observer = new MutationObserver(update)
|
|
46
|
+
|
|
47
|
+
observer.observe(root, { attributes: true, attributeFilter: ['data-mode', 'data-disabled'] })
|
|
48
|
+
|
|
49
|
+
syncs.set(input, update)
|
|
50
|
+
|
|
51
|
+
update()
|
|
52
|
+
}
|
|
53
|
+
}
|