@santi020k/lumen-astro 3.0.1 → 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 +122 -66
- 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
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
import type { HTMLAttributes } from 'astro/types'
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { alignLumenChartSeries,
|
|
5
5
|
createLumenBarGeometry,
|
|
6
|
+
createLumenChartDatumActivation,
|
|
6
7
|
formatLumenChartSummary,
|
|
8
|
+
getLumenChartCategories,
|
|
9
|
+
getLumenChartCategoryLabel,
|
|
10
|
+
getLumenChartCategoryTicks,
|
|
11
|
+
getLumenChartMotionKey,
|
|
7
12
|
getLumenChartTicks,
|
|
8
13
|
getLumenChartToneClassName,
|
|
9
14
|
hasLumenChartData,
|
|
@@ -15,7 +20,10 @@ import {
|
|
|
15
20
|
scaleLumenChartValue
|
|
16
21
|
} from '@santi020k/lumen-core'
|
|
17
22
|
|
|
23
|
+
import { createChartDatumAction } from '../runtime/chart-actions'
|
|
24
|
+
|
|
18
25
|
import Chart from './Chart.astro'
|
|
26
|
+
import ChartDatumActions from './ChartDatumActions.astro'
|
|
19
27
|
|
|
20
28
|
interface Props extends HTMLAttributes<'figure'> {
|
|
21
29
|
caption?: string
|
|
@@ -23,6 +31,7 @@ interface Props extends HTMLAttributes<'figure'> {
|
|
|
23
31
|
class?: string | null
|
|
24
32
|
className?: string
|
|
25
33
|
description?: string
|
|
34
|
+
drilldown?: boolean
|
|
26
35
|
emptyLabel?: string
|
|
27
36
|
formatCategory?: (category: number | string) => string
|
|
28
37
|
formatValue?: (value: number) => string
|
|
@@ -44,8 +53,9 @@ const {
|
|
|
44
53
|
class: classProp = '',
|
|
45
54
|
className = '',
|
|
46
55
|
description,
|
|
56
|
+
drilldown = false,
|
|
47
57
|
emptyLabel,
|
|
48
|
-
formatCategory
|
|
58
|
+
formatCategory,
|
|
49
59
|
formatValue = String,
|
|
50
60
|
glass = false,
|
|
51
61
|
heading,
|
|
@@ -60,11 +70,41 @@ const {
|
|
|
60
70
|
} = Astro.props
|
|
61
71
|
|
|
62
72
|
const resolvedLabels = resolveLumenChartLabels(labels)
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
73
|
+
const categories = getLumenChartCategories(series)
|
|
74
|
+
const alignedSeries = series.map(item => alignLumenChartSeries(item, categories))
|
|
75
|
+
|
|
76
|
+
const motionKeys = new Map(alignedSeries.flatMap(item => item.data.map(point => [
|
|
77
|
+
JSON.stringify([item.id, point.x]), getLumenChartMotionKey(item.id, point)
|
|
78
|
+
] as const)))
|
|
79
|
+
|
|
80
|
+
const summary = summaryProp ?? formatLumenChartSummary(alignedSeries, formatValue, resolvedLabels)
|
|
81
|
+
|
|
82
|
+
const geometry = createLumenBarGeometry(alignedSeries, {
|
|
83
|
+
width: 480,
|
|
84
|
+
height: 240,
|
|
85
|
+
categoryWidth: categoryWidth ?? 160,
|
|
86
|
+
...(formatCategory === undefined ? {} : { formatCategory }),
|
|
87
|
+
formatValue,
|
|
88
|
+
layout,
|
|
89
|
+
orientation
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const hasData = hasLumenChartData(alignedSeries)
|
|
66
93
|
const ticks = getLumenChartTicks(geometry.domain)
|
|
67
|
-
const
|
|
94
|
+
const margin = geometry.margin
|
|
95
|
+
|
|
96
|
+
const categoryTicks = getLumenChartCategoryTicks(geometry.categories.map(category => String(category.label)), {
|
|
97
|
+
end: geometry.width - margin.right,
|
|
98
|
+
positions: geometry.categories.map(category => category.x),
|
|
99
|
+
start: margin.left
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
const valueTicks = getLumenChartCategoryTicks(ticks.map(tick => formatValue(tick)), {
|
|
103
|
+
end: geometry.width - margin.right,
|
|
104
|
+
minimumGap: 48,
|
|
105
|
+
positions: ticks.map(tick => scaleLumenChartValue(tick, geometry.domain, margin.left, geometry.width - margin.right)),
|
|
106
|
+
start: margin.left
|
|
107
|
+
})
|
|
68
108
|
|
|
69
109
|
const datumFor = (item: LumenChartSeries, category: number | string) => (
|
|
70
110
|
item.data.find(datum => datum.x === category)
|
|
@@ -84,15 +124,23 @@ const formattedValueFor = (item: LumenChartSeries, category: number | string) =>
|
|
|
84
124
|
return value === null || !Number.isFinite(value) ? resolvedLabels.notAvailable : formatValue(value)
|
|
85
125
|
}
|
|
86
126
|
|
|
87
|
-
const categoryLabelFor = (category: number | string) =>
|
|
88
|
-
const label = series.flatMap(item => item.data).find(datum => datum.x === category)?.xLabel
|
|
127
|
+
const categoryLabelFor = (category: number | string) => getLumenChartCategoryLabel(alignedSeries, category, formatCategory, 'detail')
|
|
89
128
|
|
|
90
|
-
|
|
91
|
-
|
|
129
|
+
const datumActions = drilldown ?
|
|
130
|
+
geometry.marks.map(mark => {
|
|
131
|
+
const item = alignedSeries.find(candidate => candidate.id === mark.seriesId)
|
|
132
|
+
const datum = item?.data.find(candidate => candidate.x === mark.category)
|
|
133
|
+
|
|
134
|
+
return item && datum ?
|
|
135
|
+
createChartDatumAction(
|
|
136
|
+
createLumenChartDatumActivation(item.id, datum),
|
|
137
|
+
`${categoryLabelFor(mark.category)} · ${mark.seriesLabel}: ${formatValue(mark.value)}`,
|
|
138
|
+
resolvedLabels
|
|
139
|
+
) :
|
|
140
|
+
null
|
|
141
|
+
}) :
|
|
142
|
+
[]
|
|
92
143
|
|
|
93
|
-
const margin = orientation === 'horizontal' ?
|
|
94
|
-
{ bottom: 24, left: Math.max(64, Math.min(240, categoryWidth ?? 112)), right: 20, top: 16 } :
|
|
95
|
-
{ bottom: 52, left: 52, right: 16, top: 16 }
|
|
96
144
|
---
|
|
97
145
|
|
|
98
146
|
<Chart
|
|
@@ -103,6 +151,7 @@ const margin = orientation === 'horizontal' ?
|
|
|
103
151
|
{heading}
|
|
104
152
|
{summary}
|
|
105
153
|
{...rest}
|
|
154
|
+
data-ui-chart-activation={drilldown || undefined}
|
|
106
155
|
>
|
|
107
156
|
{
|
|
108
157
|
showLegend && hasData && (
|
|
@@ -117,7 +166,7 @@ const margin = orientation === 'horizontal' ?
|
|
|
117
166
|
)
|
|
118
167
|
}
|
|
119
168
|
{!hasData && <p class="ui-chart__empty" role="status">{emptyLabel ?? resolvedLabels.empty}</p>}
|
|
120
|
-
<div class="ui-chart__plot" hidden={!hasData}>
|
|
169
|
+
<div aria-label={resolvedLabels.chartData} class="ui-chart__plot" role="region" tabindex="0" hidden={!hasData}>
|
|
121
170
|
<svg
|
|
122
171
|
aria-hidden="true"
|
|
123
172
|
preserveAspectRatio="xMidYMid meet"
|
|
@@ -145,36 +194,40 @@ const margin = orientation === 'horizontal' ?
|
|
|
145
194
|
/>
|
|
146
195
|
) :
|
|
147
196
|
(
|
|
148
|
-
|
|
149
|
-
x1={margin.left}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
y2={coordinate}
|
|
153
|
-
/>
|
|
197
|
+
<>
|
|
198
|
+
<line x1={margin.left} x2={geometry.width - margin.right} y1={coordinate} y2={coordinate} />
|
|
199
|
+
<text x={margin.left - 8} y={coordinate}>{formatValue(tick)}</text>
|
|
200
|
+
</>
|
|
154
201
|
)
|
|
155
202
|
})
|
|
156
203
|
}
|
|
157
204
|
</g>
|
|
158
205
|
<g class="ui-chart__axis-labels">
|
|
159
206
|
{
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
x={category.x}
|
|
165
|
-
y={category.y}
|
|
166
|
-
>
|
|
167
|
-
{String(category.label)}
|
|
207
|
+
orientation === 'horizontal' ?
|
|
208
|
+
geometry.categories.map(category => (
|
|
209
|
+
<text dominant-baseline="middle" text-anchor="end" x={category.x} y={category.y}>
|
|
210
|
+
{getLumenChartCategoryTicks([String(category.label)], { end: margin.left - 16, start: 0 })[0]?.label}
|
|
168
211
|
</text>
|
|
212
|
+
)) :
|
|
213
|
+
categoryTicks.map(tick => (
|
|
214
|
+
<text text-anchor={tick.textAnchor} x={tick.position} y={geometry.height - 20}>{tick.label}</text>
|
|
169
215
|
))
|
|
170
216
|
}
|
|
171
217
|
</g>
|
|
218
|
+
{orientation === 'horizontal' && (
|
|
219
|
+
<g class="ui-chart__axis-labels">{valueTicks.map(tick => (
|
|
220
|
+
<text text-anchor={tick.textAnchor} x={tick.position} y={geometry.height - 6}>{tick.label}</text>
|
|
221
|
+
))}</g>
|
|
222
|
+
)}
|
|
172
223
|
<g class="ui-bar-chart__marks">
|
|
173
224
|
{
|
|
174
|
-
geometry.marks.map(mark => (
|
|
225
|
+
geometry.marks.map((mark, index) => (
|
|
175
226
|
<rect
|
|
176
227
|
class={getLumenChartToneClassName(mark.tone)}
|
|
228
|
+
data-ui-chart-motion-key={motionKeys.get(JSON.stringify([mark.seriesId, mark.category]))}
|
|
177
229
|
height={mark.height}
|
|
230
|
+
data-ui-chart-datum={datumActions[index]?.serialized}
|
|
178
231
|
rx="4"
|
|
179
232
|
width={mark.width}
|
|
180
233
|
x={mark.x}
|
|
@@ -186,6 +239,16 @@ const margin = orientation === 'horizontal' ?
|
|
|
186
239
|
</rect>
|
|
187
240
|
))
|
|
188
241
|
}
|
|
242
|
+
{drilldown && geometry.marks.map((mark, index) => (
|
|
243
|
+
<rect
|
|
244
|
+
class="ui-chart__datum-hit"
|
|
245
|
+
width={Math.max(12, mark.width)}
|
|
246
|
+
height={Math.max(12, mark.height)}
|
|
247
|
+
x={mark.x - Math.max(0, 12 - mark.width) / 2}
|
|
248
|
+
y={mark.y - Math.max(0, 12 - mark.height) / 2}
|
|
249
|
+
data-ui-chart-datum={datumActions[index]?.serialized}
|
|
250
|
+
/>
|
|
251
|
+
))}
|
|
189
252
|
</g>
|
|
190
253
|
</svg>
|
|
191
254
|
</div>
|
|
@@ -193,7 +256,7 @@ const margin = orientation === 'horizontal' ?
|
|
|
193
256
|
showTable && hasData && (
|
|
194
257
|
<details class="ui-chart__data">
|
|
195
258
|
<summary>{resolvedLabels.viewData}</summary>
|
|
196
|
-
<div>
|
|
259
|
+
<div role="group" tabindex="0" aria-label={resolvedLabels.chartData}>
|
|
197
260
|
<table>
|
|
198
261
|
<thead>
|
|
199
262
|
<tr>
|
|
@@ -205,7 +268,7 @@ const margin = orientation === 'horizontal' ?
|
|
|
205
268
|
{categories.map(category => (
|
|
206
269
|
<tr>
|
|
207
270
|
<th scope="row">{categoryLabelFor(category)}</th>
|
|
208
|
-
{
|
|
271
|
+
{alignedSeries.map(item => (
|
|
209
272
|
<td>
|
|
210
273
|
{labelFor(item, category) ?? formattedValueFor(item, category)}
|
|
211
274
|
</td>
|
|
@@ -218,4 +281,5 @@ const margin = orientation === 'horizontal' ?
|
|
|
218
281
|
</details>
|
|
219
282
|
)
|
|
220
283
|
}
|
|
284
|
+
{drilldown && <ChartDatumActions actions={datumActions} label={resolvedLabels.exploreData} />}
|
|
221
285
|
</Chart>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from 'astro/types'
|
|
3
|
+
|
|
4
|
+
import { createLumenBoxPlotGeometry, getLumenChartToneClassName, type LumenBoxPlotDatum, type LumenBoxPlotOptions, type LumenBoxPlotStatisticLabels, type LumenChartLabels, resolveLumenChartLabels } from '@santi020k/lumen-core'
|
|
5
|
+
|
|
6
|
+
import Chart from './Chart.astro'
|
|
7
|
+
|
|
8
|
+
interface Props extends HTMLAttributes<'figure'>, LumenBoxPlotOptions {
|
|
9
|
+
data: readonly LumenBoxPlotDatum[]
|
|
10
|
+
className?: string
|
|
11
|
+
heading?: string
|
|
12
|
+
description?: string
|
|
13
|
+
caption?: string
|
|
14
|
+
presentation?: 'bare' | 'default'
|
|
15
|
+
summary?: string
|
|
16
|
+
formatValue?: (value: number) => string
|
|
17
|
+
labels?: Partial<LumenChartLabels>
|
|
18
|
+
statisticLabels?: Partial<LumenBoxPlotStatisticLabels>
|
|
19
|
+
showTable?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { data, domain, formatValue = String, labels, statisticLabels, showTable = true, summary, class: classProp, className, ...props } = Astro.props
|
|
23
|
+
const text = resolveLumenChartLabels(labels)
|
|
24
|
+
const stats = { min: 'Lower whisker', q1: 'First quartile', median: 'Median', q3: 'Third quartile', max: 'Upper whisker', outliers: 'Outliers', ...statisticLabels }
|
|
25
|
+
const columns = ['min', 'q1', 'median', 'q3', 'max'] as const
|
|
26
|
+
const model = createLumenBoxPlotGeometry(data, { domain })
|
|
27
|
+
const format = (value: number | null) => value === null ? text.notAvailable : formatValue(value)
|
|
28
|
+
const fallback = model.valid ? text.empty : text.invalidData
|
|
29
|
+
|
|
30
|
+
const factual = !model.valid || model.rows.length === 0 ?
|
|
31
|
+
fallback :
|
|
32
|
+
model.rows.map(row => (
|
|
33
|
+
`${row.label}. ${columns.map(column => `${stats[column]}: ${format(row[column])}`).join('. ')}. ${stats.outliers}: ${(row.outliers ?? []).map(formatValue).join(', ') || '—'}.`
|
|
34
|
+
)).join(' ')
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
<Chart {...props} class:list={['ui-box-plot', classProp, className]} summary={summary ?? factual}>
|
|
38
|
+
{!model.valid || model.rows.length === 0 ?
|
|
39
|
+
<p class="ui-chart__empty" role="status">{model.valid ? text.empty : text.invalidData}</p> :
|
|
40
|
+
(
|
|
41
|
+
<>
|
|
42
|
+
<ul class="ui-box-plot__rows">{model.rows.map((row, index) => (
|
|
43
|
+
<li class={getLumenChartToneClassName(row.tone, index)}><div class="
|
|
44
|
+
ui-box-plot__label
|
|
45
|
+
"
|
|
46
|
+
><span>{row.label}</span><span>{stats.median}: <strong>{format(row.median)}</strong></span></div><div
|
|
47
|
+
class="ui-box-plot__track"
|
|
48
|
+
aria-hidden="true"
|
|
49
|
+
>
|
|
50
|
+
{row.minPosition !== null && row.maxPosition !== null && (
|
|
51
|
+
<><span
|
|
52
|
+
class="ui-box-plot__whisker"
|
|
53
|
+
style={{ left: `${row.minPosition * 100}%`, width: `${(row.maxPosition - row.minPosition) * 100}%` }}
|
|
54
|
+
/><span
|
|
55
|
+
class="ui-box-plot__cap"
|
|
56
|
+
style={{ left: `${row.minPosition * 100}%` }}
|
|
57
|
+
/><span
|
|
58
|
+
class="ui-box-plot__cap"
|
|
59
|
+
style={{ left: `${row.maxPosition * 100}%` }}
|
|
60
|
+
/></>
|
|
61
|
+
)}
|
|
62
|
+
{row.q1Position !== null && row.q3Position !== null && (
|
|
63
|
+
<span
|
|
64
|
+
class="ui-box-plot__box"
|
|
65
|
+
style={{ left: `${row.q1Position * 100}%`, width: `${(row.q3Position - row.q1Position) * 100}%` }}
|
|
66
|
+
/>
|
|
67
|
+
)}
|
|
68
|
+
{row.medianPosition !== null && <span class="ui-box-plot__median" style={{ left: `${row.medianPosition * 100}%` }} />}
|
|
69
|
+
{row.outlierPositions.map(position => <span class="ui-box-plot__outlier" style={{ left: `${position * 100}%` }} />)}
|
|
70
|
+
</div></li>
|
|
71
|
+
))}</ul>
|
|
72
|
+
<div class="ui-bullet-chart__ticks" aria-hidden="true">{model.ticks.map(tick => <span>{formatValue(tick)}</span>)}</div>
|
|
73
|
+
{!showTable && <ul class="ui-sr-only">{model.rows.map(row => <li>{row.label}. {columns.map(column => `${stats[column]}: ${format(row[column])}`).join('. ')}. {stats.outliers}: {(row.outliers ?? []).map(formatValue).join(', ') || '—'}.</li>)}</ul>}
|
|
74
|
+
{showTable && <details class="ui-chart__data"><summary>{text.viewData}</summary><div role="group" tabindex="0" aria-label={text.chartData}><table><thead><tr><th scope="col">{text.category}</th>{columns.map(column => <th scope="col">{stats[column]}</th>)}<th scope="col">{stats.outliers}</th></tr></thead><tbody>{model.rows.map(row => <tr><th scope="row">{row.label}</th>{columns.map(column => <td>{format(row[column])}</td>)}<td>{(row.outliers ?? []).map(formatValue).join(', ') || '—'}</td></tr>)}</tbody></table></div></details>}
|
|
75
|
+
</>
|
|
76
|
+
)}
|
|
77
|
+
</Chart>
|
|
@@ -9,6 +9,7 @@ interface Props extends HTMLAttributes<'nav'> {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
const {
|
|
12
|
+
'aria-label': ariaLabel = 'Breadcrumb',
|
|
12
13
|
class: classProp = '',
|
|
13
14
|
className = '',
|
|
14
15
|
...rest
|
|
@@ -20,7 +21,7 @@ const { classList, passthrough } = resolveLumenAstroProps(rest, [
|
|
|
20
21
|
---
|
|
21
22
|
|
|
22
23
|
<nav
|
|
23
|
-
aria-label=
|
|
24
|
+
aria-label={ariaLabel}
|
|
24
25
|
class:list={classList}
|
|
25
26
|
{...passthrough}
|
|
26
27
|
>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from 'astro/types'
|
|
3
|
+
|
|
4
|
+
import { createLumenBulletGeometry, getLumenChartToneClassName, type LumenBulletOptions, type LumenChartLabels, type LumenChartTone, resolveLumenChartLabels } from '@santi020k/lumen-core'
|
|
5
|
+
|
|
6
|
+
import Chart from './Chart.astro'
|
|
7
|
+
|
|
8
|
+
interface Props extends HTMLAttributes<'figure'>, LumenBulletOptions {
|
|
9
|
+
caption?: string
|
|
10
|
+
className?: string
|
|
11
|
+
description?: string
|
|
12
|
+
formatValue?: (value: number) => string
|
|
13
|
+
heading?: string
|
|
14
|
+
labels?: Partial<LumenChartLabels>
|
|
15
|
+
presentation?: 'bare' | 'default'
|
|
16
|
+
showTable?: boolean
|
|
17
|
+
summary?: string
|
|
18
|
+
target: number
|
|
19
|
+
targetLabel?: string
|
|
20
|
+
tone?: LumenChartTone
|
|
21
|
+
value: number | null
|
|
22
|
+
valueLabel?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const { class: classProp, className, domain, formatValue = String, labels, ranges, showTable = true, summary, target, targetLabel = 'Target', tone = 'series-1', value, valueLabel, ...props } = Astro.props
|
|
26
|
+
const text = resolveLumenChartLabels(labels)
|
|
27
|
+
const model = createLumenBulletGeometry(value, target, { domain, ranges })
|
|
28
|
+
const title = valueLabel ?? text.value
|
|
29
|
+
const actual = model.valid && value !== null ? formatValue(value) : text.notAvailable
|
|
30
|
+
const factual = model.valid ? `${title}: ${actual}. ${targetLabel}: ${formatValue(target)}.` : text.invalidData
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
<Chart {...props} class:list={['ui-bullet-chart', classProp, className]} summary={summary ?? factual}>
|
|
34
|
+
{model.valid ?
|
|
35
|
+
(
|
|
36
|
+
<>
|
|
37
|
+
<div class="ui-bullet-chart__values">
|
|
38
|
+
<div><span>{title}</span><strong>{actual}</strong></div>
|
|
39
|
+
<p><span class="ui-bullet-chart__target-key" aria-hidden="true" />{targetLabel}<strong>{formatValue(target)}</strong></p>
|
|
40
|
+
</div>
|
|
41
|
+
<div aria-hidden="true" class="ui-bullet-chart__plot">
|
|
42
|
+
<div class="ui-bullet-chart__track">
|
|
43
|
+
{model.ranges.map((range, index) => (
|
|
44
|
+
<span
|
|
45
|
+
class:list={['ui-bullet-chart__range', getLumenChartToneClassName(range.tone ?? 'neutral')]}
|
|
46
|
+
style={{ left: `${range.startRatio * 100}%`, width: `${(range.endRatio - range.startRatio) * 100}%`, opacity: 0.08 + index / Math.max(1, model.ranges.length - 1) * 0.14 }}
|
|
47
|
+
/>
|
|
48
|
+
))}
|
|
49
|
+
{value !== null && <span class:list={['ui-bullet-chart__bar', getLumenChartToneClassName(tone)]} style={{ left: `${model.valueStartRatio * 100}%`, width: `${model.valueWidthRatio * 100}%` }} />}
|
|
50
|
+
<span class="ui-bullet-chart__target" style={{ left: `${model.targetRatio * 100}%` }} />
|
|
51
|
+
</div>
|
|
52
|
+
<div class="ui-bullet-chart__ticks">{model.ticks.map(tick => <span>{formatValue(tick.value)}</span>)}</div>
|
|
53
|
+
</div>
|
|
54
|
+
{model.ranges.length > 0 && <ul class="ui-bullet-chart__ranges">{model.ranges.map(range => <li class={getLumenChartToneClassName(range.tone ?? 'neutral')}><span>{range.label}</span><span>{formatValue(range.start)}–{formatValue(range.end)}</span></li>)}</ul>}
|
|
55
|
+
{showTable && (
|
|
56
|
+
<details class="ui-chart__data"><summary>{text.viewData}</summary><div role="group" tabindex="0" aria-label={text.chartData}><table>
|
|
57
|
+
<thead><tr><th scope="col">{text.category}</th><th scope="col">{text.value}</th></tr></thead>
|
|
58
|
+
<tbody><tr><th scope="row">{title}</th><td>{actual}</td></tr><tr><th scope="row">{targetLabel}</th><td>{formatValue(target)}</td></tr>{model.ranges.map(range => <tr><th scope="row">{range.label}</th><td>{formatValue(range.start)}–{formatValue(range.end)}</td></tr>)}</tbody>
|
|
59
|
+
</table></div></details>
|
|
60
|
+
)}
|
|
61
|
+
</>
|
|
62
|
+
) :
|
|
63
|
+
<p class="ui-chart__empty" role="status">{text.invalidData}</p>}
|
|
64
|
+
</Chart>
|
package/components/Button.astro
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
---
|
|
2
2
|
import type { HTMLAttributes } from 'astro/types'
|
|
3
3
|
|
|
4
|
-
import { resolveLumenAstroProps } from '@santi020k/lumen-core'
|
|
4
|
+
import { isLumenDateBoundsValid, type LumenDateLabels, parseLumenDate, resolveLumenAstroProps, resolveLumenDateLabels, resolveLumenDateLocale } from '@santi020k/lumen-core'
|
|
5
5
|
|
|
6
6
|
interface Props extends Omit<HTMLAttributes<'div'>, 'value'> {
|
|
7
7
|
class?: string | null
|
|
8
8
|
className?: string
|
|
9
9
|
disabled?: boolean
|
|
10
10
|
glass?: boolean | 'subtle' | 'strong'
|
|
11
|
+
labels?: Partial<Pick<LumenDateLabels, 'previousMonth' | 'nextMonth'>>
|
|
12
|
+
locale?: string
|
|
13
|
+
readOnly?: boolean
|
|
11
14
|
max?: string
|
|
12
15
|
min?: string
|
|
13
16
|
month?: string
|
|
@@ -15,66 +18,31 @@ interface Props extends Omit<HTMLAttributes<'div'>, 'value'> {
|
|
|
15
18
|
value?: string
|
|
16
19
|
}
|
|
17
20
|
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const weekdayFormatter = new Intl.DateTimeFormat('en', { timeZone: 'UTC', weekday: 'short' })
|
|
21
|
-
const monthFormatter = new Intl.DateTimeFormat('en', { month: 'long', timeZone: 'UTC', year: 'numeric' })
|
|
22
|
-
|
|
23
|
-
const dayFormatter = new Intl.DateTimeFormat('en', {
|
|
24
|
-
day: 'numeric',
|
|
25
|
-
month: 'long',
|
|
26
|
-
timeZone: 'UTC',
|
|
27
|
-
weekday: 'long',
|
|
28
|
-
year: 'numeric'
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
const parseIsoDate = (date: string | undefined): Date | undefined => {
|
|
32
|
-
if (!date || !isoDatePattern.test(date)) return undefined
|
|
33
|
-
|
|
34
|
-
const [year, month, day] = date.split('-').map(Number)
|
|
35
|
-
const parsed = new Date(Date.UTC(year, month - 1, day))
|
|
36
|
-
|
|
37
|
-
return parsed.getUTCFullYear() === year &&
|
|
38
|
-
parsed.getUTCMonth() === month - 1 &&
|
|
39
|
-
parsed.getUTCDate() === day ?
|
|
40
|
-
parsed :
|
|
41
|
-
undefined
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const parseIsoMonth = (date: string | undefined): Date | undefined => {
|
|
45
|
-
if (!date || !isoMonthPattern.test(date)) return undefined
|
|
46
|
-
|
|
47
|
-
const [year, month] = date.split('-').map(Number)
|
|
48
|
-
const parsed = new Date(Date.UTC(year, month - 1, 1))
|
|
49
|
-
|
|
50
|
-
return parsed.getUTCFullYear() === year && parsed.getUTCMonth() === month - 1 ?
|
|
51
|
-
parsed :
|
|
52
|
-
undefined
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const formatIsoDate = (date: Date): string => date.toISOString().slice(0, 10)
|
|
56
|
-
const formatIsoMonth = (date: Date): string => date.toISOString().slice(0, 7)
|
|
21
|
+
const createDate = (year: number, month: number, day: number): Date => {
|
|
22
|
+
const date = new Date(0)
|
|
57
23
|
|
|
58
|
-
|
|
59
|
-
const utc = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() + days)
|
|
24
|
+
date.setUTCFullYear(year, month, day)
|
|
60
25
|
|
|
61
|
-
return
|
|
26
|
+
return date
|
|
62
27
|
}
|
|
63
28
|
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
)
|
|
29
|
+
const parseIsoDate = (date: string | undefined): Date | undefined => parseLumenDate(date) ?? undefined
|
|
30
|
+
const parseIsoMonth = (date: string | undefined): Date | undefined => date ? parseIsoDate(`${date}-01`) : undefined
|
|
31
|
+
const formatIsoDate = (date: Date): string => date.toISOString().split('T')[0] ?? ''
|
|
32
|
+
const formatIsoMonth = (date: Date): string => formatIsoDate(date).slice(0, -3)
|
|
33
|
+
const addDays = (date: Date, days: number): Date => createDate(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() + days)
|
|
34
|
+
const isBefore = (date: Date, limit: Date | undefined): boolean => Boolean(limit && date < limit)
|
|
35
|
+
const isAfter = (date: Date, limit: Date | undefined): boolean => Boolean(limit && date > limit)
|
|
71
36
|
|
|
72
37
|
const {
|
|
73
38
|
class: classProp = '',
|
|
74
39
|
className = '',
|
|
75
|
-
disabled = false,
|
|
40
|
+
disabled: requestedDisabled = false,
|
|
76
41
|
glass = false,
|
|
77
42
|
id,
|
|
43
|
+
labels,
|
|
44
|
+
locale,
|
|
45
|
+
readOnly = false,
|
|
78
46
|
max,
|
|
79
47
|
min,
|
|
80
48
|
month,
|
|
@@ -83,16 +51,26 @@ const {
|
|
|
83
51
|
...rest
|
|
84
52
|
} = Astro.props
|
|
85
53
|
|
|
54
|
+
const disabled = requestedDisabled || !isLumenDateBoundsValid(min, max)
|
|
55
|
+
const resolvedLocale = resolveLumenDateLocale(locale)
|
|
56
|
+
const resolvedLabels = { ...resolveLumenDateLabels(resolvedLocale), ...labels }
|
|
57
|
+
const weekdayFormatter = new Intl.DateTimeFormat(resolvedLocale, { timeZone: 'UTC', weekday: 'short' })
|
|
58
|
+
const monthFormatter = new Intl.DateTimeFormat(resolvedLocale, { month: 'long', timeZone: 'UTC', year: 'numeric' })
|
|
59
|
+
|
|
60
|
+
const dayFormatter = new Intl.DateTimeFormat(resolvedLocale, {
|
|
61
|
+
day: 'numeric', month: 'long', timeZone: 'UTC', weekday: 'long', year: 'numeric'
|
|
62
|
+
})
|
|
63
|
+
|
|
86
64
|
const today = new Date()
|
|
87
|
-
const todayUtc =
|
|
65
|
+
const todayUtc = createDate(today.getFullYear(), today.getMonth(), today.getDate())
|
|
88
66
|
const selectedDate = parseIsoDate(value)
|
|
89
|
-
const minDate = parseIsoDate(min)
|
|
90
|
-
const maxDate = parseIsoDate(max)
|
|
67
|
+
const minDate = parseIsoDate(min ?? '0001-01-01')
|
|
68
|
+
const maxDate = parseIsoDate(max ?? '9999-12-31')
|
|
91
69
|
const selectedIso = selectedDate ? formatIsoDate(selectedDate) : ''
|
|
92
70
|
|
|
93
71
|
const monthDate = parseIsoMonth(month) ??
|
|
94
|
-
(selectedDate ?
|
|
95
|
-
|
|
72
|
+
(selectedDate ? createDate(selectedDate.getUTCFullYear(), selectedDate.getUTCMonth(), 1) : undefined) ??
|
|
73
|
+
createDate(todayUtc.getUTCFullYear(), todayUtc.getUTCMonth(), 1)
|
|
96
74
|
|
|
97
75
|
const calendarId = id ?? `ui-calendar-${Math.random().toString(36).slice(2)}`
|
|
98
76
|
const labelId = `${calendarId}-label`
|
|
@@ -108,8 +86,9 @@ const firstSelectableDate = visibleDates.find(date => date.getUTCMonth() === mon
|
|
|
108
86
|
|
|
109
87
|
let focusIso = formatIsoDate(monthDate)
|
|
110
88
|
|
|
111
|
-
if (selectedDate && formatIsoMonth(selectedDate) === monthIso) focusIso = selectedIso
|
|
89
|
+
if (selectedDate && !isBefore(selectedDate, minDate) && !isAfter(selectedDate, maxDate) && formatIsoMonth(selectedDate) === monthIso) focusIso = selectedIso
|
|
112
90
|
else if (
|
|
91
|
+
!isBefore(todayUtc, minDate) && !isAfter(todayUtc, maxDate) &&
|
|
113
92
|
todayUtc.getUTCMonth() === monthDate.getUTCMonth() &&
|
|
114
93
|
todayUtc.getUTCFullYear() === monthDate.getUTCFullYear()
|
|
115
94
|
) focusIso = todayIso
|
|
@@ -131,12 +110,16 @@ const { classList, passthrough } = resolveLumenAstroProps(rest, [
|
|
|
131
110
|
aria-disabled={disabled ? 'true' : undefined}
|
|
132
111
|
class:list={classList}
|
|
133
112
|
data-ui-calendar
|
|
113
|
+
data-locale={locale}
|
|
114
|
+
data-readonly={readOnly ? 'true' : undefined}
|
|
115
|
+
data-previous-month-label={labels?.previousMonth}
|
|
116
|
+
data-next-month-label={labels?.nextMonth}
|
|
134
117
|
data-ui-calendar-initial-month={monthIso}
|
|
135
|
-
data-ui-calendar-max={
|
|
136
|
-
data-ui-calendar-min={
|
|
118
|
+
data-ui-calendar-max={max}
|
|
119
|
+
data-ui-calendar-min={min}
|
|
137
120
|
data-ui-calendar-month={monthIso}
|
|
138
121
|
data-ui-calendar-value={selectedIso || undefined}
|
|
139
|
-
data-disabled={
|
|
122
|
+
data-disabled={requestedDisabled ? 'true' : undefined}
|
|
140
123
|
data-ui-glass-track={glass ? true : undefined}
|
|
141
124
|
id={calendarId}
|
|
142
125
|
{...passthrough}
|
|
@@ -151,27 +134,33 @@ const { classList, passthrough } = resolveLumenAstroProps(rest, [
|
|
|
151
134
|
|
|
152
135
|
<div class="ui-calendar__header">
|
|
153
136
|
<button
|
|
154
|
-
aria-label=
|
|
137
|
+
aria-label={resolvedLabels.previousMonth}
|
|
155
138
|
class="ui-calendar__nav"
|
|
156
139
|
data-ui-calendar-prev
|
|
157
|
-
disabled={disabled || undefined}
|
|
140
|
+
disabled={disabled || isBefore(addDays(monthDate, -1), minDate) || undefined}
|
|
158
141
|
type="button"
|
|
159
142
|
>
|
|
160
143
|
<span aria-hidden="true">‹</span>
|
|
161
144
|
</button>
|
|
162
145
|
<strong class="ui-calendar__label" data-ui-calendar-label id={labelId}>{monthFormatter.format(monthDate)}</strong>
|
|
163
146
|
<button
|
|
164
|
-
aria-label=
|
|
147
|
+
aria-label={resolvedLabels.nextMonth}
|
|
165
148
|
class="ui-calendar__nav"
|
|
166
149
|
data-ui-calendar-next
|
|
167
|
-
disabled={disabled || undefined}
|
|
150
|
+
disabled={disabled || isAfter(createDate(monthDate.getUTCFullYear(), monthDate.getUTCMonth() + 1, 1), maxDate) || undefined}
|
|
168
151
|
type="button"
|
|
169
152
|
>
|
|
170
153
|
<span aria-hidden="true">›</span>
|
|
171
154
|
</button>
|
|
172
155
|
</div>
|
|
173
156
|
|
|
174
|
-
<table
|
|
157
|
+
<table
|
|
158
|
+
aria-readonly={readOnly ? 'true' : undefined}
|
|
159
|
+
aria-labelledby={labelId}
|
|
160
|
+
class="ui-calendar__grid"
|
|
161
|
+
data-ui-calendar-grid
|
|
162
|
+
role="grid"
|
|
163
|
+
>
|
|
175
164
|
<thead>
|
|
176
165
|
<tr role="row">
|
|
177
166
|
{weekdays.map(weekday => <th role="columnheader" scope="col">{weekday}</th>)}
|