@santi020k/lumen-astro 2.0.0 → 3.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 CHANGED
@@ -253,9 +253,17 @@ non-code content `Code` semantics.
253
253
 
254
254
  ```astro
255
255
  <p id="invite">Join the Lumen workspace</p>
256
- <CopyButton target="#invite" toast>Copy invite</CopyButton>
256
+ <CopyButton size="sm" target="#invite" toast variant="default">
257
+ Copy invite
258
+ <span slot="copied">Invite copied</span>
259
+ <span slot="error">Copy failed</span>
260
+ </CopyButton>
257
261
  ```
258
262
 
263
+ `CopyButton` accepts the same presentation `variant` and `size` vocabulary as `Button`. Its stable
264
+ `copy-idle`, `copy-copied`, and `copy-error` slots keep visible feedback in
265
+ sync with the accessible label; free-form default children remain the idle presentation.
266
+
259
267
  ## Data visualization
260
268
 
261
269
  Use `Sparkline` beside a metric, `BarChart` for categorical comparison, `LineChart` for ordered
@@ -374,14 +382,14 @@ animates a formatted metric while preserving a stable accessible value. All thre
374
382
  content readable without JavaScript and honor reduced-motion preferences.
375
383
 
376
384
  ```astro
377
- <ScrollReveal animation="slide-up" duration="slow">
385
+ <ScrollReveal animation="slide-up" as="section" duration="slow">
378
386
  <h2>Release highlights</h2>
379
387
  </ScrollReveal>
380
388
 
381
- <RevealGroup stagger={80}>
382
- <Card>Plan</Card>
383
- <Card>Build</Card>
384
- <Card>Ship</Card>
389
+ <RevealGroup as="ul" stagger={80}>
390
+ <li><Card>Plan</Card></li>
391
+ <li><Card>Build</Card></li>
392
+ <li><Card>Ship</Card></li>
385
393
  </RevealGroup>
386
394
 
387
395
  <AnimatedNumber decimals={1} suffix="%" value={99.8} />
@@ -414,3 +422,10 @@ form a standalone item, or `as="section"` when it is a labeled region in a large
414
422
 
415
423
  Use `variant="default"` for the original neutral surface, `variant="accent"` for a featured metric,
416
424
  or `variant="glass"` for selective translucency.
425
+
426
+ ## Consumer composition recipes
427
+
428
+ See [consumer UI recipes](../../docs/consumer-ui-recipes.md) for static React icons, responsive
429
+ record tables, keyboard-aware native sheets, whole-unit amount fields, adaptive editors, and
430
+ asynchronous action states. Each recipe identifies the public primitives and the behavior that
431
+ remains owned by the application.
@@ -8,8 +8,10 @@ import {
8
8
  getLumenChartToneClassName,
9
9
  hasLumenChartData,
10
10
  type LumenBarChartLayout,
11
+ type LumenChartLabels,
11
12
  type LumenChartOrientation,
12
13
  type LumenChartSeries,
14
+ resolveLumenChartLabels,
13
15
  scaleLumenChartValue
14
16
  } from '@santi020k/lumen-core'
15
17
 
@@ -26,6 +28,7 @@ interface Props extends HTMLAttributes<'figure'> {
26
28
  formatValue?: (value: number) => string
27
29
  glass?: boolean | 'subtle' | 'strong'
28
30
  heading?: string
31
+ labels?: Partial<LumenChartLabels>
29
32
  layout?: LumenBarChartLayout
30
33
  orientation?: LumenChartOrientation
31
34
  presentation?: 'bare' | 'default'
@@ -41,20 +44,23 @@ const {
41
44
  class: classProp = '',
42
45
  className = '',
43
46
  description,
44
- emptyLabel = 'No chart data available.',
47
+ emptyLabel,
45
48
  formatCategory = String,
46
49
  formatValue = String,
47
50
  glass = false,
48
51
  heading,
52
+ labels,
49
53
  layout = 'grouped',
50
54
  orientation = 'vertical',
51
55
  series,
52
56
  showLegend = series.length > 1,
53
57
  showTable = true,
54
- summary = formatLumenChartSummary(series, formatValue),
58
+ summary: summaryProp,
55
59
  ...rest
56
60
  } = Astro.props
57
61
 
62
+ const resolvedLabels = resolveLumenChartLabels(labels)
63
+ const summary = summaryProp ?? formatLumenChartSummary(series, formatValue, resolvedLabels)
58
64
  const geometry = createLumenBarGeometry(series, { categoryWidth, layout, orientation })
59
65
  const hasData = hasLumenChartData(series)
60
66
  const ticks = getLumenChartTicks(geometry.domain)
@@ -75,7 +81,7 @@ const labelFor = (item: LumenChartSeries, category: number | string) => (
75
81
  const formattedValueFor = (item: LumenChartSeries, category: number | string) => {
76
82
  const value = valueFor(item, category)
77
83
 
78
- return value === null || !Number.isFinite(value) ? 'Not available' : formatValue(value)
84
+ return value === null || !Number.isFinite(value) ? resolvedLabels.notAvailable : formatValue(value)
79
85
  }
80
86
 
81
87
  const categoryLabelFor = (category: number | string) => {
@@ -100,7 +106,7 @@ const margin = orientation === 'horizontal' ?
100
106
  >
101
107
  {
102
108
  showLegend && hasData && (
103
- <ul class="ui-chart__legend" aria-label="Chart legend">
109
+ <ul class="ui-chart__legend" aria-label={resolvedLabels.chartLegend}>
104
110
  {series.map((item, index) => (
105
111
  <li class={getLumenChartToneClassName(item.tone, index)}>
106
112
  <span aria-hidden="true" />
@@ -110,7 +116,7 @@ const margin = orientation === 'horizontal' ?
110
116
  </ul>
111
117
  )
112
118
  }
113
- {!hasData && <p class="ui-chart__empty" role="status">{emptyLabel}</p>}
119
+ {!hasData && <p class="ui-chart__empty" role="status">{emptyLabel ?? resolvedLabels.empty}</p>}
114
120
  <div class="ui-chart__plot" hidden={!hasData}>
115
121
  <svg
116
122
  aria-hidden="true"
@@ -186,12 +192,12 @@ const margin = orientation === 'horizontal' ?
186
192
  {
187
193
  showTable && hasData && (
188
194
  <details class="ui-chart__data">
189
- <summary>View chart data</summary>
195
+ <summary>{resolvedLabels.viewData}</summary>
190
196
  <div>
191
197
  <table>
192
198
  <thead>
193
199
  <tr>
194
- <th scope="col">Category</th>
200
+ <th scope="col">{resolvedLabels.category}</th>
195
201
  {series.map(item => <th scope="col">{item.label}</th>)}
196
202
  </tr>
197
203
  </thead>
@@ -11,7 +11,9 @@ import {
11
11
  getLumenChartToneClassName,
12
12
  hasLumenChartData,
13
13
  type LumenChartDatum,
14
+ type LumenChartLabels,
14
15
  type LumenComboSeries,
16
+ resolveLumenChartLabels,
15
17
  resolveLumenChartTone } from '@santi020k/lumen-core'
16
18
 
17
19
  import Chart from './Chart.astro'
@@ -23,13 +25,16 @@ interface Props extends HTMLAttributes<'figure'> {
23
25
  description?: string
24
26
  formatValue?: (value: number) => string
25
27
  heading?: string
28
+ labels?: Partial<LumenChartLabels>
26
29
  series: readonly LumenComboSeries[]
27
30
  showLegend?: boolean
28
31
  showTable?: boolean
29
32
  summary?: string
30
33
  }
31
34
 
32
- const { caption, class: classProp = '', className = '', description, formatValue = String, heading, series, showLegend = true, showTable = true, summary = formatLumenChartSummary(series, formatValue), ...rest } = Astro.props
35
+ const { caption, class: classProp = '', className = '', description, formatValue = String, heading, labels, series, showLegend = true, showTable = true, summary: summaryProp, ...rest } = Astro.props
36
+ const resolvedLabels = resolveLumenChartLabels(labels)
37
+ const summary = summaryProp ?? formatLumenChartSummary(series, formatValue, resolvedLabels)
33
38
  const width = 640
34
39
  const height = 320
35
40
  const padding = 44
@@ -82,8 +87,8 @@ const hasData = hasLumenChartData(series)
82
87
  ---
83
88
 
84
89
  <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>}
90
+ {showLegend && hasData && <ul class="ui-chart__legend" aria-label={resolvedLabels.chartLegend}>{series.map((item, index) => <li class={getLumenChartToneClassName(item.tone, index)}><span aria-hidden="true" />{item.label}</li>)}</ul>}
91
+ {!hasData && <p class="ui-chart__empty" role="status">{resolvedLabels.empty}</p>}
87
92
  <div class="ui-chart__plot" hidden={!hasData}><svg aria-hidden="true" viewBox={`0 0 ${width} ${height}`}><g class="
88
93
  ui-bar-chart__marks
89
94
  "
@@ -107,11 +112,11 @@ d={geometry.path}
107
112
  )
108
113
  })}</svg></div>
109
114
  {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 => (
115
+ <details class="ui-chart__data"><summary>{resolvedLabels.viewData}</summary><div><table><thead><tr><th scope="col">{resolvedLabels.category}</th>{series.map(item => <th scope="col">{item.label}</th>)}</tr></thead><tbody>{categories.map(category => (
111
116
  <tr><th scope="row">{String(category)}</th>{series.map(item => {
112
117
  const datum = item.data.find(point => point.x === category)
113
118
 
114
- return <td>{datum?.label ?? (datum?.y === undefined || datum.y === null || !Number.isFinite(datum.y) ? 'Not available' : formatValue(datum.y))}</td>
119
+ return <td>{datum?.label ?? (datum?.y === undefined || datum.y === null || !Number.isFinite(datum.y) ? resolvedLabels.notAvailable : formatValue(datum.y))}</td>
115
120
  })}</tr>
116
121
  ))}</tbody></table></div></details>
117
122
  )}
@@ -10,9 +10,11 @@ interface Props extends HTMLAttributes<'button'> {
10
10
  errorLabel?: string
11
11
  label?: string
12
12
  resetAfter?: number
13
+ size?: 'default' | 'sm' | 'lg' | 'icon'
13
14
  target?: string
14
15
  toast?: boolean
15
16
  value?: string
17
+ variant?: 'default' | 'destructive' | 'ghost' | 'link' | 'outline' | 'secondary'
16
18
  }
17
19
 
18
20
  const {
@@ -22,16 +24,22 @@ const {
22
24
  errorLabel = 'Could not copy to clipboard',
23
25
  label = 'Copy to clipboard',
24
26
  resetAfter = 2000,
27
+ size = 'default',
25
28
  target,
26
29
  toast = false,
27
30
  type = 'button',
28
31
  value,
32
+ variant = 'outline',
29
33
  ...rest
30
34
  } = Astro.props
31
35
 
32
36
  const { classList, passthrough } = resolveLumenAstroProps(rest, [
33
37
  'ui-button',
34
- 'ui-button--outline',
38
+ `ui-button--${variant}`,
39
+ size === 'default' && 'ui-button--default-size',
40
+ size === 'sm' && 'ui-button--sm',
41
+ size === 'lg' && 'ui-button--lg',
42
+ size === 'icon' && 'ui-button--icon',
35
43
  'ui-copy-button'
36
44
  ], classProp, className)
37
45
  ---
@@ -46,10 +54,19 @@ const { classList, passthrough } = resolveLumenAstroProps(rest, [
46
54
  data-copy-target={target}
47
55
  data-copy-toast={toast ? 'true' : undefined}
48
56
  data-copy-value={value}
57
+ data-size={size}
58
+ data-slot="copy-button"
49
59
  data-state="idle"
50
60
  data-ui-copy-button
61
+ data-variant={variant}
51
62
  type={type}
52
63
  {...passthrough}
53
64
  >
54
- <slot>Copy</slot>
65
+ <span data-slot="copy-idle"><slot>Copy</slot></span>
66
+ <span aria-hidden="true" data-slot="copy-copied" hidden>
67
+ <slot name="copied">{copiedLabel}</slot>
68
+ </span>
69
+ <span aria-hidden="true" data-slot="copy-error" hidden>
70
+ <slot name="error">{errorLabel}</slot>
71
+ </span>
55
72
  </button>
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  import type { HTMLAttributes } from 'astro/types'
3
3
 
4
- import { createLumenHeatmapGeometry, type LumenHeatmapDatum } from '@santi020k/lumen-core'
4
+ import { createLumenHeatmapGeometry, type LumenChartLabels, type LumenHeatmapDatum, resolveLumenChartLabels } from '@santi020k/lumen-core'
5
5
 
6
6
  import Chart from './Chart.astro'
7
7
 
@@ -13,20 +13,23 @@ interface Props extends HTMLAttributes<'figure'> {
13
13
  description?: string
14
14
  formatValue?: (value: number) => string
15
15
  heading?: string
16
+ labels?: Partial<LumenChartLabels>
16
17
  showTable?: boolean
17
18
  summary?: string
18
19
  }
19
20
 
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 { caption, class: classProp = '', className = '', data, description, formatValue = String, heading, labels, showTable = true, summary: summaryProp, ...rest } = Astro.props
21
22
  const geometry = createLumenHeatmapGeometry(data)
22
23
  const hasData = geometry.cells.some(cell => cell.value !== null && Number.isFinite(cell.value))
24
+ const resolvedLabels = resolveLumenChartLabels(labels)
25
+ const summary = summaryProp ?? resolvedLabels.formatHeatmapSummary(geometry.cells.filter(cell => cell.value !== null && Number.isFinite(cell.value)).length)
23
26
  ---
24
27
 
25
28
  <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>}
29
+ {!hasData && <p class="ui-chart__empty" role="status">{resolvedLabels.empty}</p>}
27
30
  <div class="ui-chart__plot" hidden={!hasData}><svg aria-hidden="true" viewBox={`0 0 ${geometry.width} ${geometry.height}`}><g class="
28
31
  ui-heatmap__cells
29
32
  "
30
33
  >{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>}
34
+ {showTable && <details class="ui-chart__data"><summary>{resolvedLabels.viewData}</summary><div><table><thead><tr><th scope="col">{resolvedLabels.column}</th><th scope="col">{resolvedLabels.row}</th><th scope="col">{resolvedLabels.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) ? resolvedLabels.notAvailable : formatValue(cell.value))}</td></tr>)}</tbody></table></div></details>}
32
35
  </Chart>
@@ -10,7 +10,9 @@ import {
10
10
  getLumenChartTicks,
11
11
  getLumenChartToneClassName,
12
12
  hasLumenChartData,
13
+ type LumenChartLabels,
13
14
  type LumenChartSeries,
15
+ resolveLumenChartLabels,
14
16
  resolveLumenChartTone,
15
17
  scaleLumenChartValue
16
18
  } from '@santi020k/lumen-core'
@@ -28,6 +30,7 @@ interface Props extends HTMLAttributes<'figure'> {
28
30
  formatValue?: (value: number) => string
29
31
  glass?: boolean | 'subtle' | 'strong'
30
32
  heading?: string
33
+ labels?: Partial<LumenChartLabels>
31
34
  markers?: 'all' | 'auto' | 'none' | boolean | number
32
35
  presentation?: 'bare' | 'default'
33
36
  referenceValue?: number
@@ -43,20 +46,23 @@ const {
43
46
  class: classProp = '',
44
47
  className = '',
45
48
  description,
46
- emptyLabel = 'No chart data available.',
49
+ emptyLabel,
47
50
  formatCategory = String,
48
51
  formatValue = String,
49
52
  glass = false,
50
53
  heading,
54
+ labels,
51
55
  markers = 'auto',
52
56
  referenceValue,
53
57
  series,
54
58
  showLegend = series.length > 1,
55
59
  showTable = true,
56
- summary = formatLumenChartSummary(series, formatValue),
60
+ summary: summaryProp,
57
61
  ...rest
58
62
  } = Astro.props
59
63
 
64
+ const resolvedLabels = resolveLumenChartLabels(labels)
65
+ const summary = summaryProp ?? formatLumenChartSummary(series, formatValue, resolvedLabels)
60
66
  const width = 640
61
67
  const height = 320
62
68
  const padding = 44
@@ -107,7 +113,7 @@ const referenceY =
107
113
  >
108
114
  {
109
115
  showLegend && hasData && (
110
- <ul class="ui-chart__legend" aria-label="Chart legend">
116
+ <ul class="ui-chart__legend" aria-label={resolvedLabels.chartLegend}>
111
117
  {series.map((item, index) => (
112
118
  <li class={getLumenChartToneClassName(item.tone, index)}>
113
119
  <span aria-hidden="true" />
@@ -117,7 +123,7 @@ const referenceY =
117
123
  </ul>
118
124
  )
119
125
  }
120
- {!hasData && <p class="ui-chart__empty" role="status">{emptyLabel}</p>}
126
+ {!hasData && <p class="ui-chart__empty" role="status">{emptyLabel ?? resolvedLabels.empty}</p>}
121
127
  <div class="ui-chart__plot" hidden={!hasData}>
122
128
  <svg
123
129
  aria-hidden="true"
@@ -212,12 +218,12 @@ const referenceY =
212
218
  {
213
219
  showTable && hasData && (
214
220
  <details class="ui-chart__data">
215
- <summary>View chart data</summary>
221
+ <summary>{resolvedLabels.viewData}</summary>
216
222
  <div>
217
223
  <table>
218
224
  <thead>
219
225
  <tr>
220
- <th scope="col">Category</th>
226
+ <th scope="col">{resolvedLabels.category}</th>
221
227
  {series.map(item => <th scope="col">{item.label}</th>)}
222
228
  </tr>
223
229
  </thead>
@@ -232,7 +238,7 @@ const referenceY =
232
238
  <td>
233
239
  {datum?.label ??
234
240
  (datum?.y === undefined || datum.y === null || !Number.isFinite(datum.y) ?
235
- 'Not available' :
241
+ resolvedLabels.notAvailable :
236
242
  formatValue(datum.y))}
237
243
  </td>
238
244
  )
@@ -7,9 +7,10 @@ import {
7
7
  getLumenChartToneClassName,
8
8
  getLumenPieChartVariantClassName,
9
9
  hasLumenPieData,
10
+ type LumenChartLabels,
10
11
  type LumenChartSeries,
11
- type LumenPieChartVariant
12
- } from '@santi020k/lumen-core'
12
+ type LumenPieChartVariant,
13
+ resolveLumenChartLabels } from '@santi020k/lumen-core'
13
14
 
14
15
  import Chart from './Chart.astro'
15
16
 
@@ -22,6 +23,7 @@ interface Props extends HTMLAttributes<'figure'> {
22
23
  description?: string
23
24
  glass?: boolean | 'subtle' | 'strong'
24
25
  heading?: string
26
+ labels?: Partial<LumenChartLabels>
25
27
  series: LumenChartSeries
26
28
  showLegend?: boolean
27
29
  showTable?: boolean
@@ -39,6 +41,7 @@ const {
39
41
  description,
40
42
  glass = false,
41
43
  heading,
44
+ labels,
42
45
  series,
43
46
  showLegend = true,
44
47
  showTable = true,
@@ -49,6 +52,7 @@ const {
49
52
  } = Astro.props
50
53
 
51
54
  const geometry = createLumenPieGeometry(series.data, { variant })
55
+ const resolvedLabels = resolveLumenChartLabels(labels)
52
56
  const hasData = hasLumenPieData(series.data)
53
57
 
54
58
  const renderedSeries = {
@@ -56,7 +60,7 @@ const renderedSeries = {
56
60
  data: series.data.filter(datum => datum.y !== null && Number.isFinite(datum.y) && datum.y > 0)
57
61
  }
58
62
 
59
- const summary = summaryProp ?? formatLumenChartSummary([renderedSeries], valueFormatter)
63
+ const summary = summaryProp ?? formatLumenChartSummary([renderedSeries], valueFormatter, resolvedLabels)
60
64
 
61
65
  const percentageFor = (percentage: number) => new Intl.NumberFormat(undefined, {
62
66
  maximumFractionDigits: percentage < 0.01 ? 1 : 0,
@@ -75,7 +79,7 @@ const percentageFor = (percentage: number) => new Intl.NumberFormat(undefined, {
75
79
  >
76
80
  {
77
81
  showLegend && hasData && (
78
- <ul class="ui-chart__legend" aria-label="Chart legend">
82
+ <ul class="ui-chart__legend" aria-label={resolvedLabels.chartLegend}>
79
83
  {geometry.slices.map(slice => (
80
84
  <li class={getLumenChartToneClassName(slice.tone)}>
81
85
  <span aria-hidden="true" />
@@ -85,7 +89,7 @@ const percentageFor = (percentage: number) => new Intl.NumberFormat(undefined, {
85
89
  </ul>
86
90
  )
87
91
  }
88
- {!hasData && <p class="ui-chart__empty" role="status">No chart data available.</p>}
92
+ {!hasData && <p class="ui-chart__empty" role="status">{resolvedLabels.empty}</p>}
89
93
  <div class="ui-chart__plot ui-pie-chart__plot" hidden={!hasData}>
90
94
  <svg
91
95
  aria-hidden="true"
@@ -125,13 +129,13 @@ const percentageFor = (percentage: number) => new Intl.NumberFormat(undefined, {
125
129
  {
126
130
  showTable && hasData && (
127
131
  <details class="ui-chart__data">
128
- <summary>View chart data</summary>
132
+ <summary>{resolvedLabels.viewData}</summary>
129
133
  <div>
130
134
  <table>
131
135
  <thead>
132
136
  <tr>
133
- <th scope="col">Category</th>
134
- <th scope="col">Value</th>
137
+ <th scope="col">{resolvedLabels.category}</th>
138
+ <th scope="col">{resolvedLabels.value}</th>
135
139
  <th scope="col">Share</th>
136
140
  </tr>
137
141
  </thead>
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  import type { HTMLAttributes } from 'astro/types'
3
3
 
4
- import { createLumenRangeGeometry, getLumenChartTicks, getLumenChartToneClassName, type LumenChartTone, type LumenRangeDatum, scaleLumenChartValue } from '@santi020k/lumen-core'
4
+ import { createLumenRangeGeometry, getLumenChartTicks, getLumenChartToneClassName, type LumenChartLabels, type LumenChartTone, type LumenRangeDatum, resolveLumenChartLabels, scaleLumenChartValue } from '@santi020k/lumen-core'
5
5
 
6
6
  import Chart from './Chart.astro'
7
7
 
@@ -13,28 +13,30 @@ interface Props extends HTMLAttributes<'figure'> {
13
13
  description?: string
14
14
  formatValue?: (value: number) => string
15
15
  heading?: string
16
+ labels?: Partial<LumenChartLabels>
16
17
  showTable?: boolean
17
18
  summary?: string
18
19
  tone?: LumenChartTone
19
20
  }
20
21
 
21
- const { caption, class: classProp = '', className = '', data, description, formatValue = String, heading, showTable = true, summary: summaryProp, tone = 'series-1', ...rest } = Astro.props
22
+ const { caption, class: classProp = '', className = '', data, description, formatValue = String, heading, labels, showTable = true, summary: summaryProp, tone = 'series-1', ...rest } = Astro.props
22
23
  const width = 640
23
24
  const height = 320
24
25
  const padding = 44
25
26
  const geometry = createLumenRangeGeometry(data, { height, padding, width })
27
+ const resolvedLabels = resolveLumenChartLabels(labels)
26
28
 
27
29
  const summary = summaryProp ?? (
28
30
  geometry.points.length === 0 ?
29
- 'No chart data available.' :
30
- `${geometry.points.length} available ranges.`
31
+ resolvedLabels.formatRangeSummary(0) :
32
+ resolvedLabels.formatRangeSummary(geometry.points.length)
31
33
  )
32
34
 
33
35
  const ticks = getLumenChartTicks(geometry.domain)
34
36
  ---
35
37
 
36
38
  <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>}
39
+ {geometry.points.length === 0 && <p class="ui-chart__empty" role="status">{resolvedLabels.empty}</p>}
38
40
  <div class="ui-chart__plot" hidden={geometry.points.length === 0}><svg aria-hidden="true" viewBox={`0 0 ${width} ${height}`}><g class="
39
41
  ui-chart__grid
40
42
  "
@@ -54,5 +56,5 @@ y1={point.highCoordinate}
54
56
  y2={point.lowCoordinate}
55
57
  ><title>{`${point.xLabel ?? point.x}: ${point.label ?? `${formatValue(point.low ?? 0)}–${formatValue(point.high ?? 0)}`}`}</title></line>
56
58
  ))}</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>}
59
+ {showTable && <details class="ui-chart__data"><summary>{resolvedLabels.viewData}</summary><div><table><thead><tr><th scope="col">{resolvedLabels.category}</th><th scope="col">{resolvedLabels.low}</th><th scope="col">{resolvedLabels.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) ? resolvedLabels.notAvailable : formatValue(item.low)}</td><td>{item.high === null || !Number.isFinite(item.high) ? resolvedLabels.notAvailable : formatValue(item.high)}</td></tr>)}</tbody></table></div></details>}
58
60
  </Chart>
@@ -5,6 +5,7 @@ import { resolveLumenAstroProps } from '@santi020k/lumen-core'
5
5
 
6
6
  interface Props extends HTMLAttributes<'div'> {
7
7
  animation?: 'fade' | 'slide-up' | 'scale'
8
+ as?: 'div' | 'ol' | 'section' | 'ul'
8
9
  class?: string | null
9
10
  className?: string
10
11
  delay?: number
@@ -16,6 +17,7 @@ interface Props extends HTMLAttributes<'div'> {
16
17
 
17
18
  const {
18
19
  animation = 'slide-up',
20
+ as: Tag = 'div',
19
21
  class: classProp = '',
20
22
  className = '',
21
23
  delay = 0,
@@ -44,7 +46,7 @@ const { classList, passthrough } = resolveLumenAstroProps(rest, [
44
46
  ], classProp, className)
45
47
  ---
46
48
 
47
- <div
49
+ <Tag
48
50
  class:list={classList}
49
51
  data-ui-reveal-group
50
52
  data-ui-reveal-once={String(once)}
@@ -53,4 +55,4 @@ const { classList, passthrough } = resolveLumenAstroProps(rest, [
53
55
  {...passthrough}
54
56
  >
55
57
  <slot />
56
- </div>
58
+ </Tag>
@@ -6,8 +6,10 @@ import {
6
6
  formatLumenChartSummary,
7
7
  getLumenChartTicks,
8
8
  getLumenChartToneClassName,
9
+ type LumenChartLabels,
9
10
  type LumenChartScaleType,
10
11
  type LumenChartSeries,
12
+ resolveLumenChartLabels,
11
13
  scaleLumenChartValue } from '@santi020k/lumen-core'
12
14
 
13
15
  import Chart from './Chart.astro'
@@ -20,6 +22,7 @@ interface Props extends HTMLAttributes<'figure'> {
20
22
  emptyLabel?: string
21
23
  formatValue?: (value: number) => string
22
24
  heading?: string
25
+ labels?: Partial<LumenChartLabels>
23
26
  series: readonly LumenChartSeries[]
24
27
  showLegend?: boolean
25
28
  showTable?: boolean
@@ -32,9 +35,10 @@ const {
32
35
  class: classProp = '',
33
36
  className = '',
34
37
  description,
35
- emptyLabel = 'No chart data available.',
38
+ emptyLabel,
36
39
  formatValue = String,
37
40
  heading,
41
+ labels,
38
42
  series,
39
43
  showLegend = series.length > 1,
40
44
  showTable = true,
@@ -44,6 +48,7 @@ const {
44
48
  } = Astro.props
45
49
 
46
50
  const geometry = createLumenScatterGeometry(series, { xScale })
51
+ const resolvedLabels = resolveLumenChartLabels(labels)
47
52
 
48
53
  const projectedSeries = series.map(item => ({
49
54
  ...item,
@@ -51,14 +56,14 @@ const projectedSeries = series.map(item => ({
51
56
  })).filter(item => item.data.length > 0)
52
57
 
53
58
  const hasData = geometry.points.length > 0
54
- const summary = summaryProp ?? formatLumenChartSummary(projectedSeries, formatValue)
59
+ const summary = summaryProp ?? formatLumenChartSummary(projectedSeries, formatValue, resolvedLabels)
55
60
  const xTicks = getLumenChartTicks(geometry.xDomain)
56
61
  const yTicks = getLumenChartTicks(geometry.domain)
57
62
  ---
58
63
 
59
64
  <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>}
65
+ {showLegend && hasData && <ul class="ui-chart__legend" aria-label={resolvedLabels.chartLegend}>{series.map((item, index) => <li class={getLumenChartToneClassName(item.tone, index)}><span aria-hidden="true" />{item.label}</li>)}</ul>}
66
+ {!hasData && <p class="ui-chart__empty" role="status">{emptyLabel ?? resolvedLabels.empty}</p>}
62
67
  <div class="ui-chart__plot" hidden={!hasData}>
63
68
  <svg aria-hidden="true" viewBox={`0 0 ${geometry.width} ${geometry.height}`}>
64
69
  <g class="ui-chart__grid">{yTicks.map(tick => {
@@ -74,5 +79,5 @@ const yTicks = getLumenChartTicks(geometry.domain)
74
79
  <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
80
  </svg>
76
81
  </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>}
82
+ {showTable && hasData && <details class="ui-chart__data"><summary>{resolvedLabels.viewData}</summary><div><table><thead><tr><th scope="col">{resolvedLabels.x}</th><th scope="col">{resolvedLabels.series}</th><th scope="col">{resolvedLabels.value}</th><th scope="col">{resolvedLabels.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) ? resolvedLabels.notAvailable : formatValue(point.size)}</td></tr>)}</tbody></table></div></details>}
78
83
  </Chart>
@@ -5,6 +5,7 @@ import { resolveLumenAstroProps } from '@santi020k/lumen-core'
5
5
 
6
6
  interface Props extends HTMLAttributes<'div'> {
7
7
  animation?: 'fade' | 'slide-up' | 'scale'
8
+ as?: 'article' | 'div' | 'section'
8
9
  class?: string | null
9
10
  className?: string
10
11
  delay?: number
@@ -15,6 +16,7 @@ interface Props extends HTMLAttributes<'div'> {
15
16
 
16
17
  const {
17
18
  animation = 'fade',
19
+ as: Tag = 'div',
18
20
  class: classProp = '',
19
21
  className = '',
20
22
  delay = 0,
@@ -40,7 +42,7 @@ const { classList, passthrough } = resolveLumenAstroProps(rest, [
40
42
  ], classProp, className)
41
43
  ---
42
44
 
43
- <div
45
+ <Tag
44
46
  class:list={classList}
45
47
  data-ui-reveal-once={String(once)}
46
48
  data-ui-reveal-threshold={safeThreshold}
@@ -49,4 +51,4 @@ const { classList, passthrough } = resolveLumenAstroProps(rest, [
49
51
  {...passthrough}
50
52
  >
51
53
  <slot />
52
- </div>
54
+ </Tag>
@@ -21,6 +21,7 @@ const { classList, passthrough } = resolveLumenAstroProps(rest, [
21
21
 
22
22
  <a
23
23
  class:list={classList}
24
+ data-slot="skip-link"
24
25
  {...passthrough}
25
26
  >
26
27
  <slot />
@@ -7,17 +7,20 @@ interface Props extends HTMLAttributes<'div'> {
7
7
  class?: string | null
8
8
  className?: string
9
9
  glass?: boolean | 'subtle' | 'strong'
10
+ layout?: 'records' | 'scroll'
10
11
  }
11
12
 
12
13
  const {
13
14
  class: classProp = '',
14
15
  className = '',
15
16
  glass = false,
17
+ layout = 'scroll',
16
18
  ...rest
17
19
  } = Astro.props
18
20
 
19
21
  const { classList, passthrough } = resolveLumenAstroProps(rest, [
20
22
  'ui-table-wrap',
23
+ layout === 'records' && 'ui-table-wrap--records',
21
24
  glass && 'ui-table-wrap--glass',
22
25
  glass === 'subtle' && 'ui-glass-subtle',
23
26
  glass === 'strong' && 'ui-glass-strong'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@santi020k/lumen-astro",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -42,15 +42,15 @@
42
42
  "lumen": "./bin/lumen.mjs"
43
43
  },
44
44
  "dependencies": {
45
- "uqr": "^0.1.3",
46
- "@santi020k/lumen-core": "^2.0.0",
47
- "@santi020k/lumen": "^2.0.0"
45
+ "@santi020k/lumen": "^3.0.0",
46
+ "@santi020k/lumen-core": "^3.0.0",
47
+ "uqr": "^0.1.3"
48
48
  },
49
49
  "devDependencies": {
50
- "@astrojs/ts-plugin": "^1.10.11",
51
- "astro": "^7.2.6",
52
- "typescript": "^6.0.3",
53
- "vitest": "^4.1.11"
50
+ "@astrojs/ts-plugin": "^1.10.12",
51
+ "astro": "^7.3.5",
52
+ "typescript": "~6.0.3",
53
+ "vitest": "^5.0.1"
54
54
  },
55
55
  "peerDependencies": {
56
56
  "astro": ">=5"
@@ -1456,6 +1456,20 @@ const initCopyButtons = (scope: ParentNode): void => {
1456
1456
 
1457
1457
  let resetTimer = 0
1458
1458
 
1459
+ const setCopyButtonState = (state: 'copied' | 'error' | 'idle'): void => {
1460
+ button.dataset.state = state
1461
+
1462
+ for (const part of button.querySelectorAll<HTMLElement>('[data-slot^="copy-"]')) {
1463
+ const isActive = part.dataset.slot === `copy-${state}`
1464
+
1465
+ part.hidden = !isActive
1466
+
1467
+ part.setAttribute('aria-hidden', String(!isActive))
1468
+ }
1469
+ }
1470
+
1471
+ setCopyButtonState('idle')
1472
+
1459
1473
  button.addEventListener('click', async () => {
1460
1474
  const label = button.dataset.copyLabel ?? 'Copy to clipboard'
1461
1475
 
@@ -1474,7 +1488,7 @@ const initCopyButtons = (scope: ParentNode): void => {
1474
1488
 
1475
1489
  await navigator.clipboard.writeText(value)
1476
1490
 
1477
- button.dataset.state = 'copied'
1491
+ setCopyButtonState('copied')
1478
1492
 
1479
1493
  button.setAttribute('aria-label', copiedLabel)
1480
1494
 
@@ -1491,7 +1505,7 @@ const initCopyButtons = (scope: ParentNode): void => {
1491
1505
  }))
1492
1506
  }
1493
1507
  } catch (error) {
1494
- button.dataset.state = 'error'
1508
+ setCopyButtonState('error')
1495
1509
 
1496
1510
  button.setAttribute('aria-label', errorLabel)
1497
1511
 
@@ -1518,7 +1532,7 @@ const initCopyButtons = (scope: ParentNode): void => {
1518
1532
  2000
1519
1533
 
1520
1534
  resetTimer = window.setTimeout(() => {
1521
- button.dataset.state = 'idle'
1535
+ setCopyButtonState('idle')
1522
1536
 
1523
1537
  button.setAttribute('aria-label', label)
1524
1538
 
@@ -698,7 +698,7 @@
698
698
  .ui-badge--warning {
699
699
  background: hsl(var(--warning) / 0.12);
700
700
  border-color: hsl(var(--warning) / 0.36);
701
- color: hsl(var(--warning));
701
+ color: hsl(var(--ink));
702
702
  }
703
703
 
704
704
  .ui-input,
@@ -893,7 +893,7 @@
893
893
  .ui-field > p,
894
894
  .ui-field > small {
895
895
  margin: 0;
896
- color: hsl(var(--ink-muted));
896
+ color: hsl(var(--ink-soft));
897
897
  font-size: 0.8125rem;
898
898
  line-height: 1.5;
899
899
  }
@@ -1190,6 +1190,61 @@
1190
1190
  font-size: 0.875rem;
1191
1191
  }
1192
1192
 
1193
+ .ui-table__label {
1194
+ display: none;
1195
+ }
1196
+
1197
+ @media (max-width: 48rem) {
1198
+ .ui-table-wrap--records table,
1199
+ .ui-table-wrap--records tbody {
1200
+ display: block;
1201
+ min-width: 0;
1202
+ }
1203
+
1204
+ .ui-table-wrap--records thead {
1205
+ position: absolute;
1206
+ width: 1px;
1207
+ height: 1px;
1208
+ padding: 0;
1209
+ overflow: hidden;
1210
+ clip-path: inset(50%);
1211
+ white-space: nowrap;
1212
+ }
1213
+
1214
+ .ui-table-wrap--records tbody tr {
1215
+ display: grid;
1216
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 9rem), 1fr));
1217
+ gap: 1rem;
1218
+ padding: 1rem;
1219
+ }
1220
+
1221
+ .ui-table-wrap--records tbody tr + tr {
1222
+ border-top: 1px solid hsl(var(--line));
1223
+ }
1224
+
1225
+ .ui-table-wrap--records tbody td {
1226
+ display: block;
1227
+ min-width: 0;
1228
+ padding: 0;
1229
+ border: 0;
1230
+ overflow-wrap: anywhere;
1231
+ white-space: normal;
1232
+ text-align: start;
1233
+ }
1234
+
1235
+ .ui-table-wrap--records .ui-table__label {
1236
+ display: block;
1237
+ margin-bottom: 0.25rem;
1238
+ color: hsl(var(--ink-soft));
1239
+ font-size: 0.75rem;
1240
+ font-weight: 600;
1241
+ }
1242
+
1243
+ .ui-table-wrap--records .ui-table__cell--wide {
1244
+ grid-column: 1 / -1;
1245
+ }
1246
+ }
1247
+
1193
1248
  .ui-table-wrap th,
1194
1249
  .ui-data-table th {
1195
1250
  border-bottom: 1px solid hsl(var(--line));