@santi020k/lumen-astro 3.0.1 → 4.1.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.
Files changed (109) hide show
  1. package/README.md +424 -7
  2. package/components/AmountField.astro +21 -0
  3. package/components/AnimatedNumber.astro +1 -1
  4. package/components/ApprovalCard.astro +30 -0
  5. package/components/AttachmentList.astro +15 -0
  6. package/components/AttachmentPreview.astro +51 -0
  7. package/components/Avatar.astro +2 -0
  8. package/components/BarChart.astro +95 -31
  9. package/components/BoxPlot.astro +77 -0
  10. package/components/Breadcrumb.astro +2 -1
  11. package/components/BulletChart.astro +64 -0
  12. package/components/Button.astro +1 -1
  13. package/components/Calendar.astro +54 -65
  14. package/components/CalendarHeatmap.astro +76 -0
  15. package/components/Card.astro +4 -0
  16. package/components/ChangeSummary.astro +36 -0
  17. package/components/ChartDatumActions.astro +24 -0
  18. package/components/ChartMotion.astro +11 -0
  19. package/components/Code.astro +18 -3
  20. package/components/CodeTabs.astro +12 -0
  21. package/components/ComboChart.astro +93 -11
  22. package/components/CountryFlag.astro +23 -0
  23. package/components/DataTable.astro +15 -7
  24. package/components/DatePicker.astro +24 -17
  25. package/components/DescriptionDetail.astro +18 -0
  26. package/components/DescriptionItem.astro +18 -0
  27. package/components/DescriptionTerm.astro +18 -0
  28. package/components/DeviceFrame.astro +79 -0
  29. package/components/DialogBody.astro +20 -0
  30. package/components/DialogClose.astro +21 -0
  31. package/components/DialogFooter.astro +20 -0
  32. package/components/DialogHeader.astro +20 -0
  33. package/components/DialogTitle.astro +21 -0
  34. package/components/DumbbellChart.astro +8 -0
  35. package/components/FileUpload.astro +3 -1
  36. package/components/FilterBar.astro +25 -0
  37. package/components/FunnelChart.astro +50 -0
  38. package/components/Grid.astro +2 -2
  39. package/components/Heatmap.astro +61 -12
  40. package/components/Histogram.astro +30 -0
  41. package/components/Icon.astro +7 -3
  42. package/components/ImageComparison.astro +74 -0
  43. package/components/Input.astro +2 -2
  44. package/components/LineChart.astro +122 -66
  45. package/components/ListBox.astro +2 -1
  46. package/components/LollipopChart.astro +8 -0
  47. package/components/MediaFilmstrip.astro +19 -0
  48. package/components/MediaThumbnail.astro +29 -0
  49. package/components/MediaViewport.astro +38 -0
  50. package/components/Mentions.astro +2 -1
  51. package/components/MessageScroller.astro +6 -0
  52. package/components/MotionGroup.astro +19 -0
  53. package/components/NativeSelect.astro +2 -2
  54. package/components/Pagination.astro +2 -1
  55. package/components/PasswordField.astro +1 -1
  56. package/components/PhoneInput.astro +60 -9
  57. package/components/PhoneNumber.astro +19 -0
  58. package/components/PieChart.astro +26 -3
  59. package/components/Progress.astro +2 -4
  60. package/components/PromptComposer.astro +36 -0
  61. package/components/RangeChart.astro +38 -8
  62. package/components/ScatterChart.astro +62 -14
  63. package/components/Segmented.astro +5 -5
  64. package/components/Select.astro +8 -8
  65. package/components/Skeleton.astro +46 -5
  66. package/components/SourceCitation.astro +18 -0
  67. package/components/Sparkline.astro +9 -10
  68. package/components/Spinner.astro +4 -4
  69. package/components/Stack.astro +2 -2
  70. package/components/StreamMessage.astro +26 -0
  71. package/components/Tabs.astro +3 -0
  72. package/components/ToolActivity.astro +17 -0
  73. package/components/TreeSelect.astro +1 -0
  74. package/components/VirtualList.astro +5 -2
  75. package/components/VisualEffect.astro +21 -0
  76. package/components/WaterfallChart.astro +27 -0
  77. package/components/WorldMap.astro +213 -0
  78. package/forms.ts +17 -4
  79. package/index.ts +37 -0
  80. package/internal/ChartInspection.astro +27 -0
  81. package/internal/ComparisonChart.astro +69 -0
  82. package/internal/IntervalChart.astro +66 -0
  83. package/package.json +6 -4
  84. package/runtime/UIPrimitives.astro +739 -711
  85. package/runtime/chart-actions.ts +19 -0
  86. package/runtime/controllers/amount-field.ts +12 -0
  87. package/runtime/controllers/attachments.ts +13 -0
  88. package/runtime/controllers/chart-activation.ts +33 -0
  89. package/runtime/controllers/charts.ts +33 -0
  90. package/runtime/controllers/combobox.ts +39 -0
  91. package/runtime/controllers/comparison-reset.ts +20 -0
  92. package/runtime/controllers/data-table.ts +142 -0
  93. package/runtime/controllers/dialogs.ts +32 -32
  94. package/runtime/controllers/document-navigation.ts +54 -22
  95. package/runtime/controllers/file-upload.ts +89 -0
  96. package/runtime/controllers/image-comparison.ts +53 -0
  97. package/runtime/controllers/list-box-proxy.ts +209 -0
  98. package/runtime/controllers/media-selection.ts +20 -0
  99. package/runtime/controllers/media-viewport.ts +54 -0
  100. package/runtime/controllers/media-workspace.ts +8 -0
  101. package/runtime/controllers/message-scroller.ts +13 -0
  102. package/runtime/controllers/motion.ts +15 -16
  103. package/runtime/controllers/optional-media.ts +29 -0
  104. package/runtime/controllers/phone-input.ts +128 -39
  105. package/runtime/controllers/progress.ts +52 -0
  106. package/runtime/controllers/visual-interactions.ts +75 -0
  107. package/runtime/controllers/world-map.ts +154 -0
  108. package/styles/critical.css +696 -66
  109. 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 = String,
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 summary = summaryProp ?? formatLumenChartSummary(series, formatValue, resolvedLabels)
64
- const geometry = createLumenBarGeometry(series, { categoryWidth, layout, orientation })
65
- const hasData = hasLumenChartData(series)
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 categories = geometry.categories.map(category => category.category)
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
- return label ?? formatCategory(category)
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
- <line
149
- x1={margin.left}
150
- x2={geometry.width - margin.right}
151
- y1={coordinate}
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
- geometry.categories.map(category => (
161
- <text
162
- dominant-baseline={orientation === 'horizontal' ? 'middle' : undefined}
163
- text-anchor={orientation === 'horizontal' ? 'end' : 'middle'}
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
- {series.map(item => (
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="Breadcrumb"
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>
@@ -50,5 +50,5 @@ const { classList, passthrough } = resolveLumenAstroProps(rest, [
50
50
  {...passthrough}
51
51
  >
52
52
  {loading && <span aria-hidden="true" class="ui-spinner"></span>}
53
- <slot />
53
+ <span class="ui-button__content"><slot /></span>
54
54
  </button>
@@ -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 isoDatePattern = /^\d{4}-\d{2}-\d{2}$/
19
- const isoMonthPattern = /^\d{4}-\d{2}$/
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
- const addDays = (date: Date, days: number): Date => {
59
- const utc = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() + days)
24
+ date.setUTCFullYear(year, month, day)
60
25
 
61
- return new Date(utc)
26
+ return date
62
27
  }
63
28
 
64
- const isBefore = (date: Date, limit: Date | undefined): boolean => (
65
- Boolean(limit && formatIsoDate(date) < formatIsoDate(limit))
66
- )
67
-
68
- const isAfter = (date: Date, limit: Date | undefined): boolean => (
69
- Boolean(limit && formatIsoDate(date) > formatIsoDate(limit))
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 = new Date(Date.UTC(today.getFullYear(), today.getMonth(), today.getDate()))
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 ? new Date(Date.UTC(selectedDate.getUTCFullYear(), selectedDate.getUTCMonth(), 1)) : undefined) ??
95
- new Date(Date.UTC(todayUtc.getUTCFullYear(), todayUtc.getUTCMonth(), 1))
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={maxDate ? formatIsoDate(maxDate) : undefined}
136
- data-ui-calendar-min={minDate ? formatIsoDate(minDate) : undefined}
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={disabled ? 'true' : undefined}
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="Previous month"
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">&lsaquo;</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="Next month"
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">&rsaquo;</span>
171
154
  </button>
172
155
  </div>
173
156
 
174
- <table aria-labelledby={labelId} class="ui-calendar__grid" data-ui-calendar-grid role="grid">
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>)}