@santi020k/lumen-astro 1.4.0 → 1.7.0-rc.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 +16 -1
- package/components/BarChart.astro +5 -1
- package/components/Chart.astro +3 -0
- package/components/ComboChart.astro +118 -0
- package/components/Heatmap.astro +32 -0
- package/components/Illustration.astro +8 -30
- package/components/Image.astro +8 -1
- package/components/LineChart.astro +5 -1
- package/components/PhoneInput.astro +70 -7
- package/components/PieChart.astro +11 -0
- package/components/RangeChart.astro +58 -0
- package/components/ScatterChart.astro +78 -0
- package/index.ts +4 -0
- package/package.json +6 -6
- package/runtime/UIPrimitives.astro +92 -14
package/README.md
CHANGED
|
@@ -60,6 +60,15 @@ incremental migrations, but new code should keep native behavior and visual styl
|
|
|
60
60
|
<NativeSelect size={8} visualSize="lg" />
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
+
`PhoneInput` generates localized country names, supplementary flags, and calling codes when custom
|
|
64
|
+
`countries` are not provided. Mount `UIPrimitives` once to enable as-you-type formatting,
|
|
65
|
+
metadata-backed validation, automatic country detection for pasted international numbers, and the
|
|
66
|
+
`ui:phone-change` event with a `LumenPhoneNumber` detail.
|
|
67
|
+
|
|
68
|
+
```astro
|
|
69
|
+
<PhoneInput countryValue="CO" locale="en-US" name="hospitalPhone" />
|
|
70
|
+
```
|
|
71
|
+
|
|
63
72
|
## Compound interactive components
|
|
64
73
|
|
|
65
74
|
Popover, dropdown menu, tabs, and tooltip parts expose the DOM and ARIA contracts consumed by the
|
|
@@ -216,7 +225,9 @@ non-code content `Code` semantics.
|
|
|
216
225
|
## Data visualization
|
|
217
226
|
|
|
218
227
|
Use `Sparkline` beside a metric, `BarChart` for categorical comparison, `LineChart` for ordered
|
|
219
|
-
trends,
|
|
228
|
+
trends, `PieChart` for a small part-to-whole breakdown, `ScatterChart` for numeric relationships,
|
|
229
|
+
`Heatmap` for a matrix, `RangeChart` for intervals, and `ComboChart` for a shared-domain mix of bars,
|
|
230
|
+
lines, and areas. The chart components accept
|
|
220
231
|
already-aggregated series, use Lumen data-color tokens, and include a revealable semantic data
|
|
221
232
|
table by default.
|
|
222
233
|
|
|
@@ -237,6 +248,10 @@ const series = [{
|
|
|
237
248
|
`PieChart` accepts one `LumenChartSeries`; its data points become slices. It defaults to
|
|
238
249
|
`variant="donut"` and also supports `variant="pie"`.
|
|
239
250
|
|
|
251
|
+
Every chart includes a factual screen-reader summary by default and accepts `summary` for more
|
|
252
|
+
useful domain context. See [data visualization](../../docs/data-visualization.md) for selection,
|
|
253
|
+
missing-data, live-data, and accessibility guidance.
|
|
254
|
+
|
|
240
255
|
Lucide intentionally excludes brand marks. For GitHub and other project-specific icons, put the
|
|
241
256
|
SVG in the default slot instead of passing an unsupported `name`; Astro warns about unknown named
|
|
242
257
|
icons during development.
|
|
@@ -3,6 +3,7 @@ import type { HTMLAttributes } from 'astro/types'
|
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
createLumenBarGeometry,
|
|
6
|
+
formatLumenChartSummary,
|
|
6
7
|
getLumenChartTicks,
|
|
7
8
|
getLumenChartToneClassName,
|
|
8
9
|
hasLumenChartData,
|
|
@@ -31,6 +32,7 @@ interface Props extends HTMLAttributes<'figure'> {
|
|
|
31
32
|
series: readonly LumenChartSeries[]
|
|
32
33
|
showLegend?: boolean
|
|
33
34
|
showTable?: boolean
|
|
35
|
+
summary?: string
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
const {
|
|
@@ -49,6 +51,7 @@ const {
|
|
|
49
51
|
series,
|
|
50
52
|
showLegend = series.length > 1,
|
|
51
53
|
showTable = true,
|
|
54
|
+
summary = formatLumenChartSummary(series, formatValue),
|
|
52
55
|
...rest
|
|
53
56
|
} = Astro.props
|
|
54
57
|
|
|
@@ -72,7 +75,7 @@ const labelFor = (item: LumenChartSeries, category: number | string) => (
|
|
|
72
75
|
const formattedValueFor = (item: LumenChartSeries, category: number | string) => {
|
|
73
76
|
const value = valueFor(item, category)
|
|
74
77
|
|
|
75
|
-
return value === null ? 'Not available' : formatValue(value)
|
|
78
|
+
return value === null || !Number.isFinite(value) ? 'Not available' : formatValue(value)
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
const categoryLabelFor = (category: number | string) => {
|
|
@@ -92,6 +95,7 @@ const margin = orientation === 'horizontal' ?
|
|
|
92
95
|
{description}
|
|
93
96
|
{glass}
|
|
94
97
|
{heading}
|
|
98
|
+
{summary}
|
|
95
99
|
{...rest}
|
|
96
100
|
>
|
|
97
101
|
{
|
package/components/Chart.astro
CHANGED
|
@@ -11,6 +11,7 @@ interface Props extends HTMLAttributes<'figure'> {
|
|
|
11
11
|
glass?: boolean | 'subtle' | 'strong'
|
|
12
12
|
heading?: string
|
|
13
13
|
presentation?: 'bare' | 'default'
|
|
14
|
+
summary?: string
|
|
14
15
|
value?: string
|
|
15
16
|
}
|
|
16
17
|
|
|
@@ -22,6 +23,7 @@ const {
|
|
|
22
23
|
glass = false,
|
|
23
24
|
heading,
|
|
24
25
|
presentation = 'default',
|
|
26
|
+
summary,
|
|
25
27
|
value,
|
|
26
28
|
...rest
|
|
27
29
|
} = Astro.props
|
|
@@ -53,6 +55,7 @@ const { classList, passthrough } = resolveLumenAstroProps(rest, [
|
|
|
53
55
|
</header>
|
|
54
56
|
)
|
|
55
57
|
}
|
|
58
|
+
{summary && <p class="ui-sr-only" data-ui-chart-summary>{summary}</p>}
|
|
56
59
|
<slot />
|
|
57
60
|
{caption && <figcaption>{caption}</figcaption>}
|
|
58
61
|
</figure>
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from 'astro/types'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
alignLumenChartSeries,
|
|
6
|
+
createLumenBarGeometry,
|
|
7
|
+
createLumenLineGeometry,
|
|
8
|
+
formatLumenChartSummary,
|
|
9
|
+
getLumenChartCategories,
|
|
10
|
+
getLumenChartDomain,
|
|
11
|
+
getLumenChartToneClassName,
|
|
12
|
+
hasLumenChartData,
|
|
13
|
+
type LumenChartDatum,
|
|
14
|
+
type LumenComboSeries,
|
|
15
|
+
resolveLumenChartTone } from '@santi020k/lumen-core'
|
|
16
|
+
|
|
17
|
+
import Chart from './Chart.astro'
|
|
18
|
+
|
|
19
|
+
interface Props extends HTMLAttributes<'figure'> {
|
|
20
|
+
caption?: string
|
|
21
|
+
class?: string | null
|
|
22
|
+
className?: string
|
|
23
|
+
description?: string
|
|
24
|
+
formatValue?: (value: number) => string
|
|
25
|
+
heading?: string
|
|
26
|
+
series: readonly LumenComboSeries[]
|
|
27
|
+
showLegend?: boolean
|
|
28
|
+
showTable?: boolean
|
|
29
|
+
summary?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const { caption, class: classProp = '', className = '', description, formatValue = String, heading, series, showLegend = true, showTable = true, summary = formatLumenChartSummary(series, formatValue), ...rest } = Astro.props
|
|
33
|
+
const width = 640
|
|
34
|
+
const height = 320
|
|
35
|
+
const padding = 44
|
|
36
|
+
const categories = getLumenChartCategories(series)
|
|
37
|
+
|
|
38
|
+
const alignedSeries = series.map(item => ({
|
|
39
|
+
...item,
|
|
40
|
+
data: alignLumenChartSeries(item, categories).data
|
|
41
|
+
}))
|
|
42
|
+
|
|
43
|
+
const domain = getLumenChartDomain(alignedSeries.flatMap(item => item.data.map(datum => datum.y)))
|
|
44
|
+
const barSeries = alignedSeries.filter(item => item.mark === 'bar')
|
|
45
|
+
const lineSeries = alignedSeries.filter(item => item.mark !== 'bar')
|
|
46
|
+
const bars = createLumenBarGeometry(barSeries, { domain, height, width })
|
|
47
|
+
|
|
48
|
+
const categoryPositions = new Map(
|
|
49
|
+
bars.categories.map(category => [`${typeof category.category}:${String(category.category)}`, category.x])
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const drawableWidth = width - padding * 2
|
|
53
|
+
|
|
54
|
+
const alignComboLineDatum = (datum: LumenChartDatum): LumenChartDatum => {
|
|
55
|
+
if (barSeries.length === 0) return datum
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
...datum,
|
|
59
|
+
x: ((categoryPositions.get(`${typeof datum.x}:${String(datum.x)}`) ?? padding) - padding) / drawableWidth
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const lineGeometryOptions = {
|
|
64
|
+
domain,
|
|
65
|
+
height,
|
|
66
|
+
padding,
|
|
67
|
+
width,
|
|
68
|
+
...(barSeries.length === 0 ?
|
|
69
|
+
{} :
|
|
70
|
+
{
|
|
71
|
+
xDomain: { max: 1, min: 0 },
|
|
72
|
+
xScale: 'linear' as const
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const lines = lineSeries.map(item => createLumenLineGeometry(
|
|
77
|
+
item.data.map(alignComboLineDatum),
|
|
78
|
+
lineGeometryOptions
|
|
79
|
+
))
|
|
80
|
+
|
|
81
|
+
const hasData = hasLumenChartData(series)
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
<Chart class:list={['ui-combo-chart', classProp, className]} {caption} {description} {heading} {summary} {...rest}>
|
|
85
|
+
{showLegend && hasData && <ul class="ui-chart__legend" aria-label="Chart legend">{series.map((item, index) => <li class={getLumenChartToneClassName(item.tone, index)}><span aria-hidden="true" />{item.label}</li>)}</ul>}
|
|
86
|
+
{!hasData && <p class="ui-chart__empty" role="status">No chart data available.</p>}
|
|
87
|
+
<div class="ui-chart__plot" hidden={!hasData}><svg aria-hidden="true" viewBox={`0 0 ${width} ${height}`}><g class="
|
|
88
|
+
ui-bar-chart__marks
|
|
89
|
+
"
|
|
90
|
+
>{bars.marks.map(mark => <rect class={getLumenChartToneClassName(mark.tone)} height={mark.height} rx="4" width={mark.width} x={mark.x} y={mark.y}><title>{`${mark.seriesLabel}: ${formatValue(mark.value)}`}</title></rect>)}</g>{lines.map((geometry, index) => {
|
|
91
|
+
const item = lineSeries[index]
|
|
92
|
+
|
|
93
|
+
if (!item) return null
|
|
94
|
+
|
|
95
|
+
const tone = resolveLumenChartTone(item.tone, index + barSeries.length)
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<g class:list={['ui-line-chart__series', getLumenChartToneClassName(tone)]}>{item.mark === 'area' && geometry.areaPaths.map(path => (
|
|
99
|
+
<path
|
|
100
|
+
class="ui-line-chart__area"
|
|
101
|
+
d={path}
|
|
102
|
+
/>
|
|
103
|
+
)) }<path
|
|
104
|
+
class="ui-line-chart__line"
|
|
105
|
+
d={geometry.path}
|
|
106
|
+
/></g>
|
|
107
|
+
)
|
|
108
|
+
})}</svg></div>
|
|
109
|
+
{showTable && hasData && (
|
|
110
|
+
<details class="ui-chart__data"><summary>View chart data</summary><div><table><thead><tr><th scope="col">Category</th>{series.map(item => <th scope="col">{item.label}</th>)}</tr></thead><tbody>{categories.map(category => (
|
|
111
|
+
<tr><th scope="row">{String(category)}</th>{series.map(item => {
|
|
112
|
+
const datum = item.data.find(point => point.x === category)
|
|
113
|
+
|
|
114
|
+
return <td>{datum?.label ?? (datum?.y === undefined || datum.y === null || !Number.isFinite(datum.y) ? 'Not available' : formatValue(datum.y))}</td>
|
|
115
|
+
})}</tr>
|
|
116
|
+
))}</tbody></table></div></details>
|
|
117
|
+
)}
|
|
118
|
+
</Chart>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from 'astro/types'
|
|
3
|
+
|
|
4
|
+
import { createLumenHeatmapGeometry, type LumenHeatmapDatum } from '@santi020k/lumen-core'
|
|
5
|
+
|
|
6
|
+
import Chart from './Chart.astro'
|
|
7
|
+
|
|
8
|
+
interface Props extends HTMLAttributes<'figure'> {
|
|
9
|
+
caption?: string
|
|
10
|
+
class?: string | null
|
|
11
|
+
className?: string
|
|
12
|
+
data: readonly LumenHeatmapDatum[]
|
|
13
|
+
description?: string
|
|
14
|
+
formatValue?: (value: number) => string
|
|
15
|
+
heading?: string
|
|
16
|
+
showTable?: boolean
|
|
17
|
+
summary?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { caption, class: classProp = '', className = '', data, description, formatValue = String, heading, showTable = true, summary = `${data.filter(item => item.value !== null && Number.isFinite(item.value)).length} available heatmap cells.`, ...rest } = Astro.props
|
|
21
|
+
const geometry = createLumenHeatmapGeometry(data)
|
|
22
|
+
const hasData = geometry.cells.some(cell => cell.value !== null && Number.isFinite(cell.value))
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
<Chart class:list={['ui-heatmap', classProp, className]} {caption} {description} {heading} {summary} {...rest}>
|
|
26
|
+
{!hasData && <p class="ui-chart__empty" role="status">No chart data available.</p>}
|
|
27
|
+
<div class="ui-chart__plot" hidden={!hasData}><svg aria-hidden="true" viewBox={`0 0 ${geometry.width} ${geometry.height}`}><g class="
|
|
28
|
+
ui-heatmap__cells
|
|
29
|
+
"
|
|
30
|
+
>{geometry.cells.filter(cell => cell.value !== null && Number.isFinite(cell.value)).map(cell => <rect height={Math.max(0, cell.height - 2)} opacity={Math.max(0.12, cell.ratio)} width={Math.max(0, cell.width - 2)} x={cell.xCoordinate + 1} y={cell.yCoordinate + 1}><title>{`${cell.xLabel ?? cell.x} · ${cell.yLabel ?? cell.y}: ${cell.label ?? formatValue(cell.value ?? 0)}`}</title></rect>)}</g></svg></div>
|
|
31
|
+
{showTable && <details class="ui-chart__data"><summary>View chart data</summary><div><table><thead><tr><th scope="col">Column</th><th scope="col">Row</th><th scope="col">Value</th></tr></thead><tbody>{data.map(cell => <tr><th scope="row">{cell.xLabel ?? cell.x}</th><td>{cell.yLabel ?? cell.y}</td><td>{cell.label ?? (cell.value === null || !Number.isFinite(cell.value) ? 'Not available' : formatValue(cell.value))}</td></tr>)}</tbody></table></div></details>}
|
|
32
|
+
</Chart>
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
import type { HTMLAttributes } from 'astro/types'
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
renderLumenIllustrationSvg,
|
|
6
|
+
resolveLumenAstroProps
|
|
7
|
+
} from '@santi020k/lumen-core'
|
|
5
8
|
|
|
6
9
|
interface Props extends HTMLAttributes<'span'> {
|
|
7
10
|
class?: string | null
|
|
@@ -28,6 +31,8 @@ const { classList, passthrough } = resolveLumenAstroProps(rest, [
|
|
|
28
31
|
`ui-illustration--${tone}`,
|
|
29
32
|
`ui-illustration--${size}`
|
|
30
33
|
], classProp, className)
|
|
34
|
+
|
|
35
|
+
const artwork = renderLumenIllustrationSvg(variant)
|
|
31
36
|
---
|
|
32
37
|
|
|
33
38
|
<span
|
|
@@ -36,32 +41,5 @@ const { classList, passthrough } = resolveLumenAstroProps(rest, [
|
|
|
36
41
|
class:list={classList}
|
|
37
42
|
role={label ? 'img' : undefined}
|
|
38
43
|
{...passthrough}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
<circle class="ui-illustration__wash" cx="60" cy="60" r="48" />
|
|
42
|
-
{variant === 'empty' && (
|
|
43
|
-
<g>
|
|
44
|
-
<path d="M32 48h56l-7 35H39l-7-35Z" />
|
|
45
|
-
<path d="M43 48 49 36h22l6 12M47 66h26" />
|
|
46
|
-
</g>
|
|
47
|
-
)}
|
|
48
|
-
{variant === 'success' && (
|
|
49
|
-
<g>
|
|
50
|
-
<circle cx="60" cy="60" r="28" />
|
|
51
|
-
<path d="m45 60 10 10 21-23" />
|
|
52
|
-
</g>
|
|
53
|
-
)}
|
|
54
|
-
{variant === 'error' && (
|
|
55
|
-
<g>
|
|
56
|
-
<circle cx="60" cy="60" r="28" />
|
|
57
|
-
<path d="m49 49 22 22m0-22L49 71" />
|
|
58
|
-
</g>
|
|
59
|
-
)}
|
|
60
|
-
{variant === 'offline' && (
|
|
61
|
-
<g>
|
|
62
|
-
<path d="M37 74h44a14 14 0 0 0 1-28 23 23 0 0 0-42-4 16 16 0 0 0-3 32Z" />
|
|
63
|
-
<path d="m38 34 44 52" />
|
|
64
|
-
</g>
|
|
65
|
-
)}
|
|
66
|
-
</svg>
|
|
67
|
-
</span>
|
|
44
|
+
set:html={artwork}
|
|
45
|
+
/>
|
package/components/Image.astro
CHANGED
|
@@ -10,25 +10,32 @@ import { resolveLumenAstroProps } from '@santi020k/lumen-core'
|
|
|
10
10
|
type Props = (LocalImageProps | RemoteImageProps) & {
|
|
11
11
|
class?: string | null
|
|
12
12
|
className?: string
|
|
13
|
+
fit?: 'contain' | 'cover'
|
|
13
14
|
invertOnDark?: boolean
|
|
15
|
+
radius?: 'full' | 'lg' | 'md' | 'none' | 'sm'
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
const {
|
|
17
19
|
class: classProp = '',
|
|
18
20
|
className = '',
|
|
21
|
+
fit = 'cover',
|
|
19
22
|
invertOnDark = false,
|
|
20
23
|
loading = 'lazy',
|
|
24
|
+
radius = 'lg',
|
|
21
25
|
...rest
|
|
22
26
|
} = Astro.props
|
|
23
27
|
|
|
24
28
|
const { classList, passthrough } = resolveLumenAstroProps(rest, [
|
|
25
29
|
'ui-image',
|
|
26
|
-
invertOnDark && 'ui-image--invert-dark'
|
|
30
|
+
invertOnDark && 'ui-image--invert-dark',
|
|
31
|
+
`ui-image--fit-${fit}`,
|
|
32
|
+
`ui-image--radius-${radius}`
|
|
27
33
|
], classProp, className)
|
|
28
34
|
---
|
|
29
35
|
|
|
30
36
|
<AstroImage
|
|
31
37
|
loading={loading}
|
|
32
38
|
class:list={classList}
|
|
39
|
+
fit={fit}
|
|
33
40
|
{...passthrough}
|
|
34
41
|
/>
|
|
@@ -4,6 +4,7 @@ import type { HTMLAttributes } from 'astro/types'
|
|
|
4
4
|
import {
|
|
5
5
|
alignLumenChartSeries,
|
|
6
6
|
createLumenLineGeometry,
|
|
7
|
+
formatLumenChartSummary,
|
|
7
8
|
getLumenChartCategories,
|
|
8
9
|
getLumenChartDomain,
|
|
9
10
|
getLumenChartTicks,
|
|
@@ -33,6 +34,7 @@ interface Props extends HTMLAttributes<'figure'> {
|
|
|
33
34
|
series: readonly LumenChartSeries[]
|
|
34
35
|
showLegend?: boolean
|
|
35
36
|
showTable?: boolean
|
|
37
|
+
summary?: string
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
const {
|
|
@@ -51,6 +53,7 @@ const {
|
|
|
51
53
|
series,
|
|
52
54
|
showLegend = series.length > 1,
|
|
53
55
|
showTable = true,
|
|
56
|
+
summary = formatLumenChartSummary(series, formatValue),
|
|
54
57
|
...rest
|
|
55
58
|
} = Astro.props
|
|
56
59
|
|
|
@@ -99,6 +102,7 @@ const referenceY =
|
|
|
99
102
|
{description}
|
|
100
103
|
{glass}
|
|
101
104
|
{heading}
|
|
105
|
+
{summary}
|
|
102
106
|
{...rest}
|
|
103
107
|
>
|
|
104
108
|
{
|
|
@@ -227,7 +231,7 @@ const referenceY =
|
|
|
227
231
|
return (
|
|
228
232
|
<td>
|
|
229
233
|
{datum?.label ??
|
|
230
|
-
(datum?.y ===
|
|
234
|
+
(datum?.y === undefined || datum.y === null || !Number.isFinite(datum.y) ?
|
|
231
235
|
'Not available' :
|
|
232
236
|
formatValue(datum.y))}
|
|
233
237
|
</td>
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
import type { HTMLAttributes } from 'astro/types'
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
getLumenPhoneCountries,
|
|
6
|
+
getLumenPhoneCountry,
|
|
7
|
+
type LumenPhoneCountry,
|
|
8
|
+
type LumenPhoneCountryOptions,
|
|
9
|
+
type LumenPhoneNumber,
|
|
10
|
+
resolveLumenAstroProps,
|
|
11
|
+
resolveLumenPhoneNumber
|
|
12
|
+
} from '@santi020k/lumen-core'
|
|
5
13
|
|
|
6
14
|
interface CountryOption {
|
|
7
15
|
disabled?: boolean
|
|
@@ -11,13 +19,20 @@ interface CountryOption {
|
|
|
11
19
|
|
|
12
20
|
type Country = CountryOption | string
|
|
13
21
|
|
|
22
|
+
interface NormalizedCountryOption extends CountryOption {
|
|
23
|
+
regionCode?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
14
26
|
interface Props extends Omit<HTMLAttributes<'div'>, 'size'> {
|
|
15
27
|
class?: string | null
|
|
16
28
|
className?: string
|
|
17
29
|
countries?: Country[]
|
|
30
|
+
countryOptions?: readonly LumenPhoneCountry[]
|
|
18
31
|
countryLabel?: string
|
|
19
32
|
countryName?: string
|
|
20
33
|
countryValue?: string
|
|
34
|
+
invalidNumberMessage?: string
|
|
35
|
+
locale?: LumenPhoneCountryOptions['locale']
|
|
21
36
|
name?: string
|
|
22
37
|
placeholder?: string
|
|
23
38
|
size?: 'default' | 'lg' | 'sm'
|
|
@@ -28,9 +43,12 @@ const {
|
|
|
28
43
|
class: classProp = '',
|
|
29
44
|
className = '',
|
|
30
45
|
countries = [],
|
|
46
|
+
countryOptions,
|
|
31
47
|
countryLabel = 'Country code',
|
|
32
48
|
countryName = 'country',
|
|
33
|
-
countryValue,
|
|
49
|
+
countryValue = 'US',
|
|
50
|
+
invalidNumberMessage = 'Enter a complete phone number.',
|
|
51
|
+
locale,
|
|
34
52
|
name = 'phone',
|
|
35
53
|
placeholder = 'Phone number',
|
|
36
54
|
size = 'default',
|
|
@@ -38,8 +56,45 @@ const {
|
|
|
38
56
|
...rest
|
|
39
57
|
} = Astro.props
|
|
40
58
|
|
|
41
|
-
const
|
|
42
|
-
const
|
|
59
|
+
const phoneOptions = locale === undefined ? {} : { locale }
|
|
60
|
+
const metadataCountries = countryOptions ?? getLumenPhoneCountries(phoneOptions)
|
|
61
|
+
const usesMetadata = countries.length === 0
|
|
62
|
+
|
|
63
|
+
const normalizedCountries: NormalizedCountryOption[] = usesMetadata ?
|
|
64
|
+
metadataCountries.map(country => ({
|
|
65
|
+
label: country.pickerLabel,
|
|
66
|
+
regionCode: country.regionCode,
|
|
67
|
+
value: country.regionCode
|
|
68
|
+
})) :
|
|
69
|
+
countries.map(country => typeof country === 'string' ? { label: country, value: country } : country)
|
|
70
|
+
|
|
71
|
+
const selectedCountry = metadataCountries.find(country => (
|
|
72
|
+
country.regionCode === countryValue || country.callingCode === countryValue
|
|
73
|
+
)) ?? metadataCountries[0] ?? getLumenPhoneCountry('US', phoneOptions)
|
|
74
|
+
|
|
75
|
+
const detectedPhoneValue = selectedCountry && value ?
|
|
76
|
+
resolveLumenPhoneNumber(selectedCountry, value, phoneOptions) :
|
|
77
|
+
null
|
|
78
|
+
|
|
79
|
+
const normalizePhoneValue = (detectedValue: LumenPhoneNumber | null): LumenPhoneNumber | null => {
|
|
80
|
+
if (!detectedValue || !selectedCountry) return detectedValue
|
|
81
|
+
|
|
82
|
+
const detectedCountryIsAllowed = metadataCountries.some(country => (
|
|
83
|
+
country.regionCode === detectedValue.country.regionCode
|
|
84
|
+
))
|
|
85
|
+
|
|
86
|
+
if (detectedCountryIsAllowed) return detectedValue
|
|
87
|
+
|
|
88
|
+
const allowedCountryValue = detectedValue.nationalNumber.startsWith('+') ?
|
|
89
|
+
detectedValue.nationalNumber.slice(1) :
|
|
90
|
+
detectedValue.nationalNumber
|
|
91
|
+
|
|
92
|
+
return resolveLumenPhoneNumber(selectedCountry, allowedCountryValue, phoneOptions)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const phoneValue = normalizePhoneValue(detectedPhoneValue)
|
|
96
|
+
const selectedCountryValue = usesMetadata ? phoneValue?.country.regionCode ?? selectedCountry?.regionCode : countryValue
|
|
97
|
+
const renderedValue = usesMetadata ? phoneValue?.nationalNumber ?? value : value
|
|
43
98
|
|
|
44
99
|
const { classList, passthrough } = resolveLumenAstroProps(rest, [
|
|
45
100
|
'ui-phone-input',
|
|
@@ -47,7 +102,14 @@ const { classList, passthrough } = resolveLumenAstroProps(rest, [
|
|
|
47
102
|
], classProp, className)
|
|
48
103
|
---
|
|
49
104
|
|
|
50
|
-
<div
|
|
105
|
+
<div
|
|
106
|
+
class:list={classList}
|
|
107
|
+
data-e164={phoneValue?.e164}
|
|
108
|
+
data-invalid-number-message={invalidNumberMessage}
|
|
109
|
+
data-ui-phone-input={usesMetadata ? '' : undefined}
|
|
110
|
+
data-valid={phoneValue ? String(phoneValue.isValid) : undefined}
|
|
111
|
+
{...passthrough}
|
|
112
|
+
>
|
|
51
113
|
<select
|
|
52
114
|
aria-label={countryLabel}
|
|
53
115
|
class:list={[
|
|
@@ -61,7 +123,8 @@ const { classList, passthrough } = resolveLumenAstroProps(rest, [
|
|
|
61
123
|
{normalizedCountries.map(country => (
|
|
62
124
|
<option
|
|
63
125
|
disabled={country.disabled}
|
|
64
|
-
|
|
126
|
+
data-region={country.regionCode}
|
|
127
|
+
selected={country.value === selectedCountryValue}
|
|
65
128
|
value={country.value}
|
|
66
129
|
>{country.label}
|
|
67
130
|
</option>
|
|
@@ -79,6 +142,6 @@ const { classList, passthrough } = resolveLumenAstroProps(rest, [
|
|
|
79
142
|
name={name}
|
|
80
143
|
placeholder={placeholder}
|
|
81
144
|
type="tel"
|
|
82
|
-
value={
|
|
145
|
+
value={renderedValue}
|
|
83
146
|
/>
|
|
84
147
|
</div>
|
|
@@ -3,6 +3,7 @@ import type { HTMLAttributes } from 'astro/types'
|
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
createLumenPieGeometry,
|
|
6
|
+
formatLumenChartSummary,
|
|
6
7
|
getLumenChartToneClassName,
|
|
7
8
|
getLumenPieChartVariantClassName,
|
|
8
9
|
hasLumenPieData,
|
|
@@ -24,6 +25,7 @@ interface Props extends HTMLAttributes<'figure'> {
|
|
|
24
25
|
series: LumenChartSeries
|
|
25
26
|
showLegend?: boolean
|
|
26
27
|
showTable?: boolean
|
|
28
|
+
summary?: string
|
|
27
29
|
valueFormatter?: (value: number) => string
|
|
28
30
|
variant?: LumenPieChartVariant
|
|
29
31
|
}
|
|
@@ -41,6 +43,7 @@ const {
|
|
|
41
43
|
showLegend = true,
|
|
42
44
|
showTable = true,
|
|
43
45
|
valueFormatter = String,
|
|
46
|
+
summary: summaryProp,
|
|
44
47
|
variant = 'donut',
|
|
45
48
|
...rest
|
|
46
49
|
} = Astro.props
|
|
@@ -48,6 +51,13 @@ const {
|
|
|
48
51
|
const geometry = createLumenPieGeometry(series.data, { variant })
|
|
49
52
|
const hasData = hasLumenPieData(series.data)
|
|
50
53
|
|
|
54
|
+
const renderedSeries = {
|
|
55
|
+
...series,
|
|
56
|
+
data: series.data.filter(datum => datum.y !== null && Number.isFinite(datum.y) && datum.y > 0)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const summary = summaryProp ?? formatLumenChartSummary([renderedSeries], valueFormatter)
|
|
60
|
+
|
|
51
61
|
const percentageFor = (percentage: number) => new Intl.NumberFormat(undefined, {
|
|
52
62
|
maximumFractionDigits: percentage < 0.01 ? 1 : 0,
|
|
53
63
|
style: 'percent'
|
|
@@ -60,6 +70,7 @@ const percentageFor = (percentage: number) => new Intl.NumberFormat(undefined, {
|
|
|
60
70
|
{description}
|
|
61
71
|
{glass}
|
|
62
72
|
{heading}
|
|
73
|
+
{summary}
|
|
63
74
|
{...rest}
|
|
64
75
|
>
|
|
65
76
|
{
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from 'astro/types'
|
|
3
|
+
|
|
4
|
+
import { createLumenRangeGeometry, getLumenChartTicks, getLumenChartToneClassName, type LumenChartTone, type LumenRangeDatum, scaleLumenChartValue } from '@santi020k/lumen-core'
|
|
5
|
+
|
|
6
|
+
import Chart from './Chart.astro'
|
|
7
|
+
|
|
8
|
+
interface Props extends HTMLAttributes<'figure'> {
|
|
9
|
+
caption?: string
|
|
10
|
+
class?: string | null
|
|
11
|
+
className?: string
|
|
12
|
+
data: readonly LumenRangeDatum[]
|
|
13
|
+
description?: string
|
|
14
|
+
formatValue?: (value: number) => string
|
|
15
|
+
heading?: string
|
|
16
|
+
showTable?: boolean
|
|
17
|
+
summary?: string
|
|
18
|
+
tone?: LumenChartTone
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { caption, class: classProp = '', className = '', data, description, formatValue = String, heading, showTable = true, summary: summaryProp, tone = 'series-1', ...rest } = Astro.props
|
|
22
|
+
const width = 640
|
|
23
|
+
const height = 320
|
|
24
|
+
const padding = 44
|
|
25
|
+
const geometry = createLumenRangeGeometry(data, { height, padding, width })
|
|
26
|
+
|
|
27
|
+
const summary = summaryProp ?? (
|
|
28
|
+
geometry.points.length === 0 ?
|
|
29
|
+
'No chart data available.' :
|
|
30
|
+
`${geometry.points.length} available ranges.`
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
const ticks = getLumenChartTicks(geometry.domain)
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
<Chart class:list={['ui-range-chart', getLumenChartToneClassName(tone), classProp, className]} {caption} {description} {heading} {summary} {...rest}>
|
|
37
|
+
{geometry.points.length === 0 && <p class="ui-chart__empty" role="status">No chart data available.</p>}
|
|
38
|
+
<div class="ui-chart__plot" hidden={geometry.points.length === 0}><svg aria-hidden="true" viewBox={`0 0 ${width} ${height}`}><g class="
|
|
39
|
+
ui-chart__grid
|
|
40
|
+
"
|
|
41
|
+
>{ticks.map(tick => {
|
|
42
|
+
const y = scaleLumenChartValue(tick, geometry.domain, height - padding, padding)
|
|
43
|
+
|
|
44
|
+
return <><line x1={padding} x2={width - padding} y1={y} y2={y}/><text x={padding - 8} y={y}>{formatValue(tick)}</text></>
|
|
45
|
+
})}</g><path
|
|
46
|
+
class="ui-range-chart__area"
|
|
47
|
+
d={geometry.areaPath}
|
|
48
|
+
/>{geometry.points.map(point => (
|
|
49
|
+
<line
|
|
50
|
+
class="ui-range-chart__interval"
|
|
51
|
+
x1={point.xCoordinate}
|
|
52
|
+
x2={point.xCoordinate}
|
|
53
|
+
y1={point.highCoordinate}
|
|
54
|
+
y2={point.lowCoordinate}
|
|
55
|
+
><title>{`${point.xLabel ?? point.x}: ${point.label ?? `${formatValue(point.low ?? 0)}–${formatValue(point.high ?? 0)}`}`}</title></line>
|
|
56
|
+
))}</svg></div>
|
|
57
|
+
{showTable && <details class="ui-chart__data"><summary>View chart data</summary><div><table><thead><tr><th scope="col">Category</th><th scope="col">Low</th><th scope="col">High</th></tr></thead><tbody>{data.map(item => <tr><th scope="row">{item.xLabel ?? item.x}</th><td>{item.low === null || !Number.isFinite(item.low) ? 'Not available' : formatValue(item.low)}</td><td>{item.high === null || !Number.isFinite(item.high) ? 'Not available' : formatValue(item.high)}</td></tr>)}</tbody></table></div></details>}
|
|
58
|
+
</Chart>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from 'astro/types'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
createLumenScatterGeometry,
|
|
6
|
+
formatLumenChartSummary,
|
|
7
|
+
getLumenChartTicks,
|
|
8
|
+
getLumenChartToneClassName,
|
|
9
|
+
type LumenChartScaleType,
|
|
10
|
+
type LumenChartSeries,
|
|
11
|
+
scaleLumenChartValue } from '@santi020k/lumen-core'
|
|
12
|
+
|
|
13
|
+
import Chart from './Chart.astro'
|
|
14
|
+
|
|
15
|
+
interface Props extends HTMLAttributes<'figure'> {
|
|
16
|
+
caption?: string
|
|
17
|
+
class?: string | null
|
|
18
|
+
className?: string
|
|
19
|
+
description?: string
|
|
20
|
+
emptyLabel?: string
|
|
21
|
+
formatValue?: (value: number) => string
|
|
22
|
+
heading?: string
|
|
23
|
+
series: readonly LumenChartSeries[]
|
|
24
|
+
showLegend?: boolean
|
|
25
|
+
showTable?: boolean
|
|
26
|
+
summary?: string
|
|
27
|
+
xScale?: Exclude<LumenChartScaleType, 'categorical'>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const {
|
|
31
|
+
caption,
|
|
32
|
+
class: classProp = '',
|
|
33
|
+
className = '',
|
|
34
|
+
description,
|
|
35
|
+
emptyLabel = 'No chart data available.',
|
|
36
|
+
formatValue = String,
|
|
37
|
+
heading,
|
|
38
|
+
series,
|
|
39
|
+
showLegend = series.length > 1,
|
|
40
|
+
showTable = true,
|
|
41
|
+
summary: summaryProp,
|
|
42
|
+
xScale = 'linear',
|
|
43
|
+
...rest
|
|
44
|
+
} = Astro.props
|
|
45
|
+
|
|
46
|
+
const geometry = createLumenScatterGeometry(series, { xScale })
|
|
47
|
+
|
|
48
|
+
const projectedSeries = series.map(item => ({
|
|
49
|
+
...item,
|
|
50
|
+
data: geometry.points.filter(point => point.seriesId === item.id)
|
|
51
|
+
})).filter(item => item.data.length > 0)
|
|
52
|
+
|
|
53
|
+
const hasData = geometry.points.length > 0
|
|
54
|
+
const summary = summaryProp ?? formatLumenChartSummary(projectedSeries, formatValue)
|
|
55
|
+
const xTicks = getLumenChartTicks(geometry.xDomain)
|
|
56
|
+
const yTicks = getLumenChartTicks(geometry.domain)
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
<Chart class:list={['ui-scatter-chart', classProp, className]} {caption} {description} {heading} {summary} {...rest}>
|
|
60
|
+
{showLegend && hasData && <ul class="ui-chart__legend" aria-label="Chart legend">{series.map((item, index) => <li class={getLumenChartToneClassName(item.tone, index)}><span aria-hidden="true" />{item.label}</li>)}</ul>}
|
|
61
|
+
{!hasData && <p class="ui-chart__empty" role="status">{emptyLabel}</p>}
|
|
62
|
+
<div class="ui-chart__plot" hidden={!hasData}>
|
|
63
|
+
<svg aria-hidden="true" viewBox={`0 0 ${geometry.width} ${geometry.height}`}>
|
|
64
|
+
<g class="ui-chart__grid">{yTicks.map(tick => {
|
|
65
|
+
const y = scaleLumenChartValue(tick, geometry.domain, geometry.height - 44, 44)
|
|
66
|
+
|
|
67
|
+
return <><line x1="44" x2={geometry.width - 44} y1={y} y2={y}/><text x="36" y={y}>{formatValue(tick)}</text></>
|
|
68
|
+
})}</g>
|
|
69
|
+
<g class="ui-chart__axis-labels">{xTicks.map(tick => {
|
|
70
|
+
const x = scaleLumenChartValue(tick, geometry.xDomain, 44, geometry.width - 44)
|
|
71
|
+
|
|
72
|
+
return <text text-anchor="middle" x={x} y={geometry.height - 14}>{formatValue(tick)}</text>
|
|
73
|
+
})}</g>
|
|
74
|
+
<g class="ui-scatter-chart__marks">{geometry.points.map(point => <circle class={getLumenChartToneClassName(point.tone)} cx={point.xCoordinate} cy={point.yCoordinate} r={point.radius}><title>{`${point.xLabel ?? point.x} · ${point.seriesLabel}: ${point.label ?? formatValue(point.y ?? 0)}`}</title></circle>)}</g>
|
|
75
|
+
</svg>
|
|
76
|
+
</div>
|
|
77
|
+
{showTable && hasData && <details class="ui-chart__data"><summary>View chart data</summary><div><table><thead><tr><th scope="col">X</th><th scope="col">Series</th><th scope="col">Value</th><th scope="col">Size</th></tr></thead><tbody>{geometry.points.map(point => <tr><th scope="row">{point.xLabel ?? point.x}</th><td>{point.seriesLabel}</td><td>{point.label ?? formatValue(point.y ?? 0)}</td><td>{point.size === undefined || point.size === null || !Number.isFinite(point.size) ? 'Not available' : formatValue(point.size)}</td></tr>)}</tbody></table></div></details>}
|
|
78
|
+
</Chart>
|
package/index.ts
CHANGED
|
@@ -38,6 +38,7 @@ export { default as CodeTabs } from './components/CodeTabs.astro'
|
|
|
38
38
|
export { default as Collapsible } from './components/Collapsible.astro'
|
|
39
39
|
export { default as ColorPicker } from './components/ColorPicker.astro'
|
|
40
40
|
export { default as Combobox } from './components/Combobox.astro'
|
|
41
|
+
export { default as ComboChart } from './components/ComboChart.astro'
|
|
41
42
|
export { default as Command } from './components/Command.astro'
|
|
42
43
|
export { default as Container } from './components/Container.astro'
|
|
43
44
|
export { default as ContextMenu } from './components/ContextMenu.astro'
|
|
@@ -68,6 +69,7 @@ export { default as FormattedDate } from './components/FormattedDate.astro'
|
|
|
68
69
|
export { default as GradientDivider } from './components/GradientDivider.astro'
|
|
69
70
|
export { default as Graphic } from './components/Graphic.astro'
|
|
70
71
|
export { default as Grid } from './components/Grid.astro'
|
|
72
|
+
export { default as Heatmap } from './components/Heatmap.astro'
|
|
71
73
|
export { default as HoverCard } from './components/HoverCard.astro'
|
|
72
74
|
export { default as Icon } from './components/Icon.astro'
|
|
73
75
|
export { default as Illustration } from './components/Illustration.astro'
|
|
@@ -108,10 +110,12 @@ export { default as Progress } from './components/Progress.astro'
|
|
|
108
110
|
export { default as Prose } from './components/Prose.astro'
|
|
109
111
|
export { default as QRCode } from './components/QRCode.astro'
|
|
110
112
|
export { default as RadioGroup } from './components/RadioGroup.astro'
|
|
113
|
+
export { default as RangeChart } from './components/RangeChart.astro'
|
|
111
114
|
export { default as Rating } from './components/Rating.astro'
|
|
112
115
|
export { default as Resizable } from './components/Resizable.astro'
|
|
113
116
|
export { default as RevealGroup } from './components/RevealGroup.astro'
|
|
114
117
|
export { default as RichTextEditor } from './components/RichTextEditor.astro'
|
|
118
|
+
export { default as ScatterChart } from './components/ScatterChart.astro'
|
|
115
119
|
export { default as Schedule } from './components/Schedule.astro'
|
|
116
120
|
export { default as ScrollArea } from './components/ScrollArea.astro'
|
|
117
121
|
export { default as ScrollProgress } from './components/ScrollProgress.astro'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@santi020k/lumen-astro",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0-rc.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -37,14 +37,14 @@
|
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"uqr": "^0.1.3",
|
|
40
|
-
"@santi020k/lumen": "^1.
|
|
41
|
-
"@santi020k/lumen
|
|
40
|
+
"@santi020k/lumen-core": "^1.7.0-rc.0",
|
|
41
|
+
"@santi020k/lumen": "^1.7.0-rc.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@astrojs/ts-plugin": "^1.10.
|
|
45
|
-
"astro": "^7.2.
|
|
44
|
+
"@astrojs/ts-plugin": "^1.10.11",
|
|
45
|
+
"astro": "^7.2.6",
|
|
46
46
|
"typescript": "^6.0.3",
|
|
47
|
-
"vitest": "^4.1.
|
|
47
|
+
"vitest": "^4.1.11"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"astro": ">=5"
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
exportThemeFigmaVariables,
|
|
18
18
|
formatLumenLanguageLabel,
|
|
19
19
|
getLumenLocalePair,
|
|
20
|
+
getLumenPhoneCountry,
|
|
20
21
|
getLumenRichTextShortcut,
|
|
21
22
|
isLumenRichTextToggleCommand,
|
|
22
23
|
type LumenLocaleOption,
|
|
@@ -24,7 +25,8 @@ import {
|
|
|
24
25
|
type LumenRichTextCommandDetail,
|
|
25
26
|
type LumenThemeTokens,
|
|
26
27
|
lumenTokenNames,
|
|
27
|
-
normalizeLumenLocales
|
|
28
|
+
normalizeLumenLocales,
|
|
29
|
+
resolveLumenPhoneNumber } from '@santi020k/lumen-core'
|
|
28
30
|
|
|
29
31
|
/* eslint-disable complexity */
|
|
30
32
|
/* Stateful DOM handlers stay local so Astro consumers do not need a framework runtime. */
|
|
@@ -71,6 +73,74 @@ const formControlSelector = [
|
|
|
71
73
|
'textarea'
|
|
72
74
|
].join(',')
|
|
73
75
|
|
|
76
|
+
const phoneInputSelector = '[data-ui-phone-input]'
|
|
77
|
+
|
|
78
|
+
const initPhoneInputs = (scope: ParentNode): void => {
|
|
79
|
+
for (const root of scope.querySelectorAll<HTMLElement>(phoneInputSelector)) {
|
|
80
|
+
if (root.dataset.uiPhoneBound === 'true') continue
|
|
81
|
+
|
|
82
|
+
const countrySelect = root.querySelector<HTMLSelectElement>('.ui-phone-input__country')
|
|
83
|
+
const numberInput = root.querySelector<HTMLInputElement>('.ui-phone-input__number')
|
|
84
|
+
|
|
85
|
+
if (!countrySelect || !numberInput) continue
|
|
86
|
+
|
|
87
|
+
root.dataset.uiPhoneBound = 'true'
|
|
88
|
+
|
|
89
|
+
const locale = root.lang || document.documentElement.lang || undefined
|
|
90
|
+
const phoneOptions = locale ? { locale } : {}
|
|
91
|
+
|
|
92
|
+
const commit = (): void => {
|
|
93
|
+
const selectedOption = countrySelect.selectedOptions[0]
|
|
94
|
+
const regionCode = selectedOption?.dataset.region ?? countrySelect.value
|
|
95
|
+
const country = getLumenPhoneCountry(regionCode, phoneOptions)
|
|
96
|
+
|
|
97
|
+
if (!country) return
|
|
98
|
+
|
|
99
|
+
const detectedPhoneNumber = resolveLumenPhoneNumber(country, numberInput.value, phoneOptions)
|
|
100
|
+
|
|
101
|
+
const detectedCountryIsAllowed = Array.from(countrySelect.options).some(option => (
|
|
102
|
+
option.value === detectedPhoneNumber.country.regionCode
|
|
103
|
+
))
|
|
104
|
+
|
|
105
|
+
const phoneNumber = detectedCountryIsAllowed ?
|
|
106
|
+
detectedPhoneNumber :
|
|
107
|
+
resolveLumenPhoneNumber(
|
|
108
|
+
country,
|
|
109
|
+
detectedPhoneNumber.nationalNumber.startsWith('+') ?
|
|
110
|
+
detectedPhoneNumber.nationalNumber.slice(1) :
|
|
111
|
+
detectedPhoneNumber.nationalNumber,
|
|
112
|
+
phoneOptions
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
const hasInput = phoneNumber.nationalNumber.length > 0
|
|
116
|
+
const invalidMessage = root.dataset.invalidNumberMessage ?? 'Enter a complete phone number.'
|
|
117
|
+
|
|
118
|
+
numberInput.value = phoneNumber.nationalNumber
|
|
119
|
+
|
|
120
|
+
countrySelect.value = phoneNumber.country.regionCode
|
|
121
|
+
|
|
122
|
+
numberInput.setCustomValidity(hasInput && !phoneNumber.isValid ? invalidMessage : '')
|
|
123
|
+
|
|
124
|
+
numberInput.setAttribute('aria-invalid', String(hasInput && !phoneNumber.isValid))
|
|
125
|
+
|
|
126
|
+
root.dataset.e164 = phoneNumber.e164 ?? ''
|
|
127
|
+
|
|
128
|
+
root.dataset.valid = String(phoneNumber.isValid)
|
|
129
|
+
|
|
130
|
+
root.dispatchEvent(new CustomEvent('ui:phone-change', {
|
|
131
|
+
bubbles: true,
|
|
132
|
+
detail: phoneNumber
|
|
133
|
+
}))
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
countrySelect.addEventListener('change', commit)
|
|
137
|
+
|
|
138
|
+
numberInput.addEventListener('input', commit)
|
|
139
|
+
|
|
140
|
+
if (numberInput.value) commit()
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
74
144
|
const validityMessageAttributes = [
|
|
75
145
|
['valueMissing', 'data-error-required'],
|
|
76
146
|
['typeMismatch', 'data-error-type'],
|
|
@@ -3375,13 +3445,13 @@ const kanbanColumnSelector = '[data-ui-kanban-column]'
|
|
|
3375
3445
|
|
|
3376
3446
|
const initKanbanBoards = (scope: ParentNode): void => {
|
|
3377
3447
|
for (const root of scope.querySelectorAll<HTMLElement>('[data-ui-kanban]')) {
|
|
3448
|
+
for (const handle of root.querySelectorAll<HTMLElement>('[data-ui-kanban-handle]'))
|
|
3449
|
+
handle.draggable = true
|
|
3450
|
+
|
|
3378
3451
|
if (root.dataset.uiKanbanBound === 'true') continue
|
|
3379
3452
|
|
|
3380
3453
|
root.dataset.uiKanbanBound = 'true'
|
|
3381
3454
|
|
|
3382
|
-
for (const handle of root.querySelectorAll<HTMLElement>('[data-ui-kanban-handle]'))
|
|
3383
|
-
handle.draggable = true
|
|
3384
|
-
|
|
3385
3455
|
let pointer: {
|
|
3386
3456
|
active: boolean
|
|
3387
3457
|
handle: HTMLElement
|
|
@@ -3391,13 +3461,21 @@ const initKanbanBoards = (scope: ParentNode): void => {
|
|
|
3391
3461
|
startY: number
|
|
3392
3462
|
} | undefined
|
|
3393
3463
|
|
|
3394
|
-
const itemFor = (target: EventTarget | null): HTMLElement | null =>
|
|
3395
|
-
|
|
3396
|
-
|
|
3464
|
+
const itemFor = (target: EventTarget | null): HTMLElement | null => {
|
|
3465
|
+
const item = target instanceof Element ?
|
|
3466
|
+
target.closest<HTMLElement>('[data-ui-kanban-item]') :
|
|
3467
|
+
null
|
|
3468
|
+
|
|
3469
|
+
return item?.closest<HTMLElement>('[data-ui-kanban]') === root ? item : null
|
|
3470
|
+
}
|
|
3397
3471
|
|
|
3398
|
-
const columnFor = (target: EventTarget | null): HTMLElement | null =>
|
|
3399
|
-
target
|
|
3400
|
-
|
|
3472
|
+
const columnFor = (target: EventTarget | null): HTMLElement | null => {
|
|
3473
|
+
const column = target instanceof Element ?
|
|
3474
|
+
target.closest<HTMLElement>(kanbanColumnSelector) :
|
|
3475
|
+
null
|
|
3476
|
+
|
|
3477
|
+
return column?.closest<HTMLElement>('[data-ui-kanban]') === root ? column : null
|
|
3478
|
+
}
|
|
3401
3479
|
|
|
3402
3480
|
const clearState = (): void => {
|
|
3403
3481
|
pointer = undefined
|
|
@@ -3580,8 +3658,7 @@ const initKanbanBoards = (scope: ParentNode): void => {
|
|
|
3580
3658
|
|
|
3581
3659
|
pointer.item.dataset.state = 'dragging'
|
|
3582
3660
|
|
|
3583
|
-
const target = document.elementFromPoint(event.clientX, event.clientY)
|
|
3584
|
-
?.closest<HTMLElement>(kanbanColumnSelector)
|
|
3661
|
+
const target = columnFor(document.elementFromPoint(event.clientX, event.clientY))
|
|
3585
3662
|
|
|
3586
3663
|
for (const column of root.querySelectorAll<HTMLElement>(kanbanColumnSelector)) {
|
|
3587
3664
|
if (column === target) column.dataset.state = 'drop-target'
|
|
@@ -3592,8 +3669,7 @@ const initKanbanBoards = (scope: ParentNode): void => {
|
|
|
3592
3669
|
root.addEventListener('pointerup', event => {
|
|
3593
3670
|
if (!pointer || pointer.pointerId !== event.pointerId) return
|
|
3594
3671
|
|
|
3595
|
-
const target = document.elementFromPoint(event.clientX, event.clientY)
|
|
3596
|
-
?.closest<HTMLElement>(kanbanColumnSelector)
|
|
3672
|
+
const target = columnFor(document.elementFromPoint(event.clientX, event.clientY))
|
|
3597
3673
|
|
|
3598
3674
|
if (pointer.active && target) requestMove(pointer.item, target, 'pointer')
|
|
3599
3675
|
|
|
@@ -5577,6 +5653,8 @@ const initUiPrimitives = (scope: ParentNode = document): void => {
|
|
|
5577
5653
|
|
|
5578
5654
|
initFields(scope)
|
|
5579
5655
|
|
|
5656
|
+
initPhoneInputs(scope)
|
|
5657
|
+
|
|
5580
5658
|
initForms(scope)
|
|
5581
5659
|
|
|
5582
5660
|
initPasswordFields(scope)
|