@santi020k/lumen-astro 1.6.0 → 1.7.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
@@ -9,6 +9,13 @@ configuration is required to render the components.
9
9
  pnpm add @santi020k/lumen-astro
10
10
  ```
11
11
 
12
+ The adapter also exposes Lumen's discovery and diagnostics CLI:
13
+
14
+ ```bash
15
+ pnpm exec lumen show Tabs
16
+ pnpm exec lumen doctor
17
+ ```
18
+
12
19
  Import the CSS once in your root layout and mount `UIPrimitives` once in that same app shell if you
13
20
  use interactive components such as dialogs, menus, popovers, tabs, carousels, toggles, command
14
21
  lists, or toasts. Do not add `UIPrimitives` beside each component instance; one root include
@@ -18,6 +25,11 @@ enhances all matching Lumen markup on the page.
18
25
  @import "@santi020k/lumen-astro/styles.css";
19
26
  ```
20
27
 
28
+ Applications using the essential forms, feedback, tabs, dialogs, calendar, and data-table surface
29
+ can instead load `@santi020k/lumen-astro/styles/critical.css`. The generated entry is about 50 KiB
30
+ raw versus 170 KiB for the complete catalog; switch to `styles.css` when using components outside
31
+ that documented critical set.
32
+
21
33
  The stylesheet defaults `--ui-font` to `"Montserrat", "Avenir Next", "Segoe UI", sans-serif`.
22
34
  It declares the family stack but does not bundle or load font files. Load Montserrat once through
23
35
  your preferred delivery path, or override `--ui-font` in application CSS.
@@ -32,7 +44,7 @@ With Tailwind, keep the Lumen import in the same shared CSS entry:
32
44
 
33
45
  ```astro
34
46
  ---
35
- import { UIPrimitives } from '@santi020k/lumen-astro'
47
+ import UIPrimitives from '@santi020k/lumen-astro/runtime'
36
48
  ---
37
49
 
38
50
  <UIPrimitives />
@@ -52,20 +64,35 @@ import { Button, Card, Input } from '@santi020k/lumen-astro'
52
64
  ```
53
65
 
54
66
  `Input` and `NativeSelect` preserve the native numeric `size` attribute. Use `visualSize="sm"` or
55
- `visualSize="lg"` for presentation. The pre-1.0 `size="sm|lg"` visual alias remains available for
56
- incremental migrations, but new code should keep native behavior and visual styling separate.
67
+ `visualSize="lg"` for presentation so native behavior and visual styling remain separate.
57
68
 
58
69
  ```astro
59
70
  <Input size={32} visualSize="sm" />
60
71
  <NativeSelect size={8} visualSize="lg" />
61
72
  ```
62
73
 
74
+ `PhoneInput` generates localized country names, supplementary flags, and calling codes when custom
75
+ `countries` are not provided. Mount `UIPrimitives` once to enable as-you-type formatting,
76
+ metadata-backed validation, automatic country detection for pasted international numbers, and the
77
+ `ui:phone-change` event with a `LumenPhoneNumber` detail.
78
+
79
+ The phone metadata and normalization controller are selector-loaded only when a rendered page
80
+ contains `PhoneInput`; pages without the component do not evaluate or download that controller.
81
+
82
+ ```astro
83
+ <PhoneInput countryValue="CO" locale="en-US" name="hospitalPhone" />
84
+ ```
85
+
63
86
  ## Compound interactive components
64
87
 
65
88
  Popover, dropdown menu, tabs, and tooltip parts expose the DOM and ARIA contracts consumed by the
66
89
  shared progressive-enhancement runtime. Prefer these parts over recreating roles and `data-ui-*`
67
90
  attributes by hand.
68
91
 
92
+ Tabs keep the selected trigger visible when a narrow tab list scrolls horizontally and emit a
93
+ typed `ui:tabs-change` event. Import `LumenTabsChangeDetail` or `LumenTabsChangeEvent` from this
94
+ package when application behavior follows the selected value.
95
+
69
96
  ```astro
70
97
  ---
71
98
  import {
@@ -171,6 +198,22 @@ const errors = result?.error && isInputError(result.error)
171
198
 
172
199
  See the [Astro Actions form guide](https://lumen.santi020k.com/docs/forms/astro-actions).
173
200
 
201
+ ## Error states
202
+
203
+ Use `ErrorState` when a page or region cannot show its primary content. Pass recovery controls
204
+ through the `actions` slot; the application still owns retry and logging behavior. Static states are
205
+ not live regions by default, so set `announce="polite"` only when a newly rendered failure needs an
206
+ announcement.
207
+
208
+ ```astro
209
+ <ErrorState title="Could not load projects" description="Check your connection and try again.">
210
+ <Button slot="actions">Try again</Button>
211
+ </ErrorState>
212
+ ```
213
+
214
+ See the repository [Astro error-handling guide](../../docs/error-handling.md#astro) for the complete
215
+ example, retry ownership, announcement behavior, and verification guidance.
216
+
174
217
  Use `Icon` for Lucide icons by name across framework adapters.
175
218
 
176
219
  ```astro
@@ -216,7 +259,9 @@ non-code content `Code` semantics.
216
259
  ## Data visualization
217
260
 
218
261
  Use `Sparkline` beside a metric, `BarChart` for categorical comparison, `LineChart` for ordered
219
- trends, and `PieChart` for a small part-to-whole breakdown. The chart components accept
262
+ trends, `PieChart` for a small part-to-whole breakdown, `ScatterChart` for numeric relationships,
263
+ `Heatmap` for a matrix, `RangeChart` for intervals, and `ComboChart` for a shared-domain mix of bars,
264
+ lines, and areas. The chart components accept
220
265
  already-aggregated series, use Lumen data-color tokens, and include a revealable semantic data
221
266
  table by default.
222
267
 
@@ -237,6 +282,10 @@ const series = [{
237
282
  `PieChart` accepts one `LumenChartSeries`; its data points become slices. It defaults to
238
283
  `variant="donut"` and also supports `variant="pie"`.
239
284
 
285
+ Every chart includes a factual screen-reader summary by default and accepts `summary` for more
286
+ useful domain context. See [data visualization](../../docs/data-visualization.md) for selection,
287
+ missing-data, live-data, and accessibility guidance.
288
+
240
289
  Lucide intentionally excludes brand marks. For GitHub and other project-specific icons, put the
241
290
  SVG in the default slot instead of passing an unsupported `name`; Astro warns about unknown named
242
291
  icons during development.
package/bin/lumen.mjs ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '@santi020k/lumen/cli'
@@ -3,6 +3,7 @@ import type { HTMLAttributes } from 'astro/types'
3
3
 
4
4
  import {
5
5
  createLumenBarGeometry,
6
+ formatLumenChartSummary,
6
7
  getLumenChartTicks,
7
8
  getLumenChartToneClassName,
8
9
  hasLumenChartData,
@@ -31,6 +32,7 @@ interface Props extends HTMLAttributes<'figure'> {
31
32
  series: readonly LumenChartSeries[]
32
33
  showLegend?: boolean
33
34
  showTable?: boolean
35
+ summary?: string
34
36
  }
35
37
 
36
38
  const {
@@ -49,6 +51,7 @@ const {
49
51
  series,
50
52
  showLegend = series.length > 1,
51
53
  showTable = true,
54
+ summary = formatLumenChartSummary(series, formatValue),
52
55
  ...rest
53
56
  } = Astro.props
54
57
 
@@ -72,7 +75,7 @@ const labelFor = (item: LumenChartSeries, category: number | string) => (
72
75
  const formattedValueFor = (item: LumenChartSeries, category: number | string) => {
73
76
  const value = valueFor(item, category)
74
77
 
75
- return value === null ? 'Not available' : formatValue(value)
78
+ return value === null || !Number.isFinite(value) ? 'Not available' : formatValue(value)
76
79
  }
77
80
 
78
81
  const categoryLabelFor = (category: number | string) => {
@@ -92,6 +95,7 @@ const margin = orientation === 'horizontal' ?
92
95
  {description}
93
96
  {glass}
94
97
  {heading}
98
+ {summary}
95
99
  {...rest}
96
100
  >
97
101
  {
@@ -11,6 +11,7 @@ interface Props extends HTMLAttributes<'figure'> {
11
11
  glass?: boolean | 'subtle' | 'strong'
12
12
  heading?: string
13
13
  presentation?: 'bare' | 'default'
14
+ summary?: string
14
15
  value?: string
15
16
  }
16
17
 
@@ -22,6 +23,7 @@ const {
22
23
  glass = false,
23
24
  heading,
24
25
  presentation = 'default',
26
+ summary,
25
27
  value,
26
28
  ...rest
27
29
  } = Astro.props
@@ -53,6 +55,7 @@ const { classList, passthrough } = resolveLumenAstroProps(rest, [
53
55
  </header>
54
56
  )
55
57
  }
58
+ {summary && <p class="ui-sr-only" data-ui-chart-summary>{summary}</p>}
56
59
  <slot />
57
60
  {caption && <figcaption>{caption}</figcaption>}
58
61
  </figure>
@@ -0,0 +1,118 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types'
3
+
4
+ import {
5
+ alignLumenChartSeries,
6
+ createLumenBarGeometry,
7
+ createLumenLineGeometry,
8
+ formatLumenChartSummary,
9
+ getLumenChartCategories,
10
+ getLumenChartDomain,
11
+ getLumenChartToneClassName,
12
+ hasLumenChartData,
13
+ type LumenChartDatum,
14
+ type LumenComboSeries,
15
+ resolveLumenChartTone } from '@santi020k/lumen-core'
16
+
17
+ import Chart from './Chart.astro'
18
+
19
+ interface Props extends HTMLAttributes<'figure'> {
20
+ caption?: string
21
+ class?: string | null
22
+ className?: string
23
+ description?: string
24
+ formatValue?: (value: number) => string
25
+ heading?: string
26
+ series: readonly LumenComboSeries[]
27
+ showLegend?: boolean
28
+ showTable?: boolean
29
+ summary?: string
30
+ }
31
+
32
+ const { caption, class: classProp = '', className = '', description, formatValue = String, heading, series, showLegend = true, showTable = true, summary = formatLumenChartSummary(series, formatValue), ...rest } = Astro.props
33
+ const width = 640
34
+ const height = 320
35
+ const padding = 44
36
+ const categories = getLumenChartCategories(series)
37
+
38
+ const alignedSeries = series.map(item => ({
39
+ ...item,
40
+ data: alignLumenChartSeries(item, categories).data
41
+ }))
42
+
43
+ const domain = getLumenChartDomain(alignedSeries.flatMap(item => item.data.map(datum => datum.y)))
44
+ const barSeries = alignedSeries.filter(item => item.mark === 'bar')
45
+ const lineSeries = alignedSeries.filter(item => item.mark !== 'bar')
46
+ const bars = createLumenBarGeometry(barSeries, { domain, height, width })
47
+
48
+ const categoryPositions = new Map(
49
+ bars.categories.map(category => [`${typeof category.category}:${String(category.category)}`, category.x])
50
+ )
51
+
52
+ const drawableWidth = width - padding * 2
53
+
54
+ const alignComboLineDatum = (datum: LumenChartDatum): LumenChartDatum => {
55
+ if (barSeries.length === 0) return datum
56
+
57
+ return {
58
+ ...datum,
59
+ x: ((categoryPositions.get(`${typeof datum.x}:${String(datum.x)}`) ?? padding) - padding) / drawableWidth
60
+ }
61
+ }
62
+
63
+ const lineGeometryOptions = {
64
+ domain,
65
+ height,
66
+ padding,
67
+ width,
68
+ ...(barSeries.length === 0 ?
69
+ {} :
70
+ {
71
+ xDomain: { max: 1, min: 0 },
72
+ xScale: 'linear' as const
73
+ })
74
+ }
75
+
76
+ const lines = lineSeries.map(item => createLumenLineGeometry(
77
+ item.data.map(alignComboLineDatum),
78
+ lineGeometryOptions
79
+ ))
80
+
81
+ const hasData = hasLumenChartData(series)
82
+ ---
83
+
84
+ <Chart class:list={['ui-combo-chart', classProp, className]} {caption} {description} {heading} {summary} {...rest}>
85
+ {showLegend && hasData && <ul class="ui-chart__legend" aria-label="Chart legend">{series.map((item, index) => <li class={getLumenChartToneClassName(item.tone, index)}><span aria-hidden="true" />{item.label}</li>)}</ul>}
86
+ {!hasData && <p class="ui-chart__empty" role="status">No chart data available.</p>}
87
+ <div class="ui-chart__plot" hidden={!hasData}><svg aria-hidden="true" viewBox={`0 0 ${width} ${height}`}><g class="
88
+ ui-bar-chart__marks
89
+ "
90
+ >{bars.marks.map(mark => <rect class={getLumenChartToneClassName(mark.tone)} height={mark.height} rx="4" width={mark.width} x={mark.x} y={mark.y}><title>{`${mark.seriesLabel}: ${formatValue(mark.value)}`}</title></rect>)}</g>{lines.map((geometry, index) => {
91
+ const item = lineSeries[index]
92
+
93
+ if (!item) return null
94
+
95
+ const tone = resolveLumenChartTone(item.tone, index + barSeries.length)
96
+
97
+ return (
98
+ <g class:list={['ui-line-chart__series', getLumenChartToneClassName(tone)]}>{item.mark === 'area' && geometry.areaPaths.map(path => (
99
+ <path
100
+ class="ui-line-chart__area"
101
+ d={path}
102
+ />
103
+ )) }<path
104
+ class="ui-line-chart__line"
105
+ d={geometry.path}
106
+ /></g>
107
+ )
108
+ })}</svg></div>
109
+ {showTable && hasData && (
110
+ <details class="ui-chart__data"><summary>View chart data</summary><div><table><thead><tr><th scope="col">Category</th>{series.map(item => <th scope="col">{item.label}</th>)}</tr></thead><tbody>{categories.map(category => (
111
+ <tr><th scope="row">{String(category)}</th>{series.map(item => {
112
+ const datum = item.data.find(point => point.x === category)
113
+
114
+ return <td>{datum?.label ?? (datum?.y === undefined || datum.y === null || !Number.isFinite(datum.y) ? 'Not available' : formatValue(datum.y))}</td>
115
+ })}</tr>
116
+ ))}</tbody></table></div></details>
117
+ )}
118
+ </Chart>
@@ -0,0 +1,118 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types'
3
+
4
+ import {
5
+ type LumenErrorStateAnnouncement,
6
+ type LumenErrorStateKind,
7
+ type LumenErrorStateLayout,
8
+ resolveLumenAstroProps
9
+ } from '@santi020k/lumen-core'
10
+
11
+ import Illustration from './Illustration.astro'
12
+
13
+ type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6
14
+
15
+ const headingTags = {
16
+ 1: 'h1',
17
+ 2: 'h2',
18
+ 3: 'h3',
19
+ 4: 'h4',
20
+ 5: 'h5',
21
+ 6: 'h6'
22
+ } as const
23
+
24
+ const resolveAnnouncementRole = (
25
+ announce: LumenErrorStateAnnouncement,
26
+ role: Props['role']
27
+ ): Props['role'] => {
28
+ if (role) return role
29
+
30
+ if (announce === 'assertive') return 'alert'
31
+
32
+ if (announce === 'polite') return 'status'
33
+
34
+ return undefined
35
+ }
36
+
37
+ interface Props extends HTMLAttributes<'section'> {
38
+ announce?: LumenErrorStateAnnouncement
39
+ class?: string | null
40
+ className?: string
41
+ description?: string
42
+ headingLevel?: HeadingLevel
43
+ kind?: LumenErrorStateKind
44
+ layout?: LumenErrorStateLayout
45
+ reference?: string
46
+ referenceLabel?: string
47
+ showGraphic?: boolean
48
+ title: string
49
+ }
50
+
51
+ const {
52
+ 'aria-live': ariaLive,
53
+ announce = 'off',
54
+ class: classProp = '',
55
+ className = '',
56
+ description,
57
+ headingLevel = 2,
58
+ id,
59
+ kind = 'error',
60
+ layout = 'default',
61
+ reference,
62
+ referenceLabel = 'Reference',
63
+ role,
64
+ showGraphic = true,
65
+ title,
66
+ ...rest
67
+ } = Astro.props
68
+
69
+ const Heading = headingTags[headingLevel]
70
+ const titleId = id ? `${id}-title` : undefined
71
+ const resolvedLive = ariaLive ?? (announce === 'off' ? undefined : announce)
72
+ const resolvedRole = resolveAnnouncementRole(announce, role)
73
+
74
+ const { classList, passthrough } = resolveLumenAstroProps(rest, [
75
+ 'ui-error-state',
76
+ `ui-error-state--${kind}`,
77
+ `ui-error-state--${layout}`
78
+ ], classProp, className)
79
+ ---
80
+
81
+ <section
82
+ aria-label={titleId ? undefined : title}
83
+ aria-labelledby={titleId}
84
+ aria-live={resolvedLive}
85
+ class:list={classList}
86
+ data-kind={kind}
87
+ data-layout={layout}
88
+ data-ui-error-state
89
+ id={id}
90
+ role={resolvedRole}
91
+ {...passthrough}
92
+ >
93
+ {showGraphic && (
94
+ <div class="ui-error-state__graphic" data-slot="error-state-graphic" aria-hidden="true">
95
+ {Astro.slots.has('graphic') ?
96
+ (<slot name="graphic" />) :
97
+ (
98
+ <Illustration
99
+ size={layout === 'compact' ? 'sm' : 'md'}
100
+ variant={kind}
101
+ />
102
+ )}
103
+ </div>
104
+ )}
105
+ <div class="ui-error-state__content" data-slot="error-state-content">
106
+ <Heading class="ui-error-state__title" data-slot="error-state-title" id={titleId}>{title}</Heading>
107
+ {description && <p class="ui-error-state__description" data-slot="error-state-description">{description}</p>}
108
+ {Astro.slots.has('default') && <div class="ui-error-state__details" data-slot="error-state-details"><slot /></div>}
109
+ {reference && (
110
+ <p class="ui-error-state__reference" data-slot="error-state-reference">
111
+ <span>{referenceLabel}:</span> <code>{reference}</code>
112
+ </p>
113
+ )}
114
+ </div>
115
+ {Astro.slots.has('actions') && (
116
+ <div class="ui-error-state__actions" data-slot="error-state-actions"><slot name="actions" /></div>
117
+ )}
118
+ </section>
@@ -0,0 +1,32 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types'
3
+
4
+ import { createLumenHeatmapGeometry, type LumenHeatmapDatum } from '@santi020k/lumen-core'
5
+
6
+ import Chart from './Chart.astro'
7
+
8
+ interface Props extends HTMLAttributes<'figure'> {
9
+ caption?: string
10
+ class?: string | null
11
+ className?: string
12
+ data: readonly LumenHeatmapDatum[]
13
+ description?: string
14
+ formatValue?: (value: number) => string
15
+ heading?: string
16
+ showTable?: boolean
17
+ summary?: string
18
+ }
19
+
20
+ const { caption, class: classProp = '', className = '', data, description, formatValue = String, heading, showTable = true, summary = `${data.filter(item => item.value !== null && Number.isFinite(item.value)).length} available heatmap cells.`, ...rest } = Astro.props
21
+ const geometry = createLumenHeatmapGeometry(data)
22
+ const hasData = geometry.cells.some(cell => cell.value !== null && Number.isFinite(cell.value))
23
+ ---
24
+
25
+ <Chart class:list={['ui-heatmap', classProp, className]} {caption} {description} {heading} {summary} {...rest}>
26
+ {!hasData && <p class="ui-chart__empty" role="status">No chart data available.</p>}
27
+ <div class="ui-chart__plot" hidden={!hasData}><svg aria-hidden="true" viewBox={`0 0 ${geometry.width} ${geometry.height}`}><g class="
28
+ ui-heatmap__cells
29
+ "
30
+ >{geometry.cells.filter(cell => cell.value !== null && Number.isFinite(cell.value)).map(cell => <rect height={Math.max(0, cell.height - 2)} opacity={Math.max(0.12, cell.ratio)} width={Math.max(0, cell.width - 2)} x={cell.xCoordinate + 1} y={cell.yCoordinate + 1}><title>{`${cell.xLabel ?? cell.x} · ${cell.yLabel ?? cell.y}: ${cell.label ?? formatValue(cell.value ?? 0)}`}</title></rect>)}</g></svg></div>
31
+ {showTable && <details class="ui-chart__data"><summary>View chart data</summary><div><table><thead><tr><th scope="col">Column</th><th scope="col">Row</th><th scope="col">Value</th></tr></thead><tbody>{data.map(cell => <tr><th scope="row">{cell.xLabel ?? cell.x}</th><td>{cell.yLabel ?? cell.y}</td><td>{cell.label ?? (cell.value === null || !Number.isFinite(cell.value) ? 'Not available' : formatValue(cell.value))}</td></tr>)}</tbody></table></div></details>}
32
+ </Chart>
@@ -1,7 +1,10 @@
1
1
  ---
2
2
  import type { HTMLAttributes } from 'astro/types'
3
3
 
4
- import { resolveLumenAstroProps } from '@santi020k/lumen-core'
4
+ import {
5
+ renderLumenIllustrationSvg,
6
+ resolveLumenAstroProps
7
+ } from '@santi020k/lumen-core'
5
8
 
6
9
  interface Props extends HTMLAttributes<'span'> {
7
10
  class?: string | null
@@ -28,6 +31,8 @@ const { classList, passthrough } = resolveLumenAstroProps(rest, [
28
31
  `ui-illustration--${tone}`,
29
32
  `ui-illustration--${size}`
30
33
  ], classProp, className)
34
+
35
+ const artwork = renderLumenIllustrationSvg(variant)
31
36
  ---
32
37
 
33
38
  <span
@@ -36,32 +41,5 @@ const { classList, passthrough } = resolveLumenAstroProps(rest, [
36
41
  class:list={classList}
37
42
  role={label ? 'img' : undefined}
38
43
  {...passthrough}
39
- >
40
- <svg aria-hidden="true" fill="none" viewBox="0 0 120 120">
41
- <circle class="ui-illustration__wash" cx="60" cy="60" r="48" />
42
- {variant === 'empty' && (
43
- <g>
44
- <path d="M32 48h56l-7 35H39l-7-35Z" />
45
- <path d="M43 48 49 36h22l6 12M47 66h26" />
46
- </g>
47
- )}
48
- {variant === 'success' && (
49
- <g>
50
- <circle cx="60" cy="60" r="28" />
51
- <path d="m45 60 10 10 21-23" />
52
- </g>
53
- )}
54
- {variant === 'error' && (
55
- <g>
56
- <circle cx="60" cy="60" r="28" />
57
- <path d="m49 49 22 22m0-22L49 71" />
58
- </g>
59
- )}
60
- {variant === 'offline' && (
61
- <g>
62
- <path d="M37 74h44a14 14 0 0 0 1-28 23 23 0 0 0-42-4 16 16 0 0 0-3 32Z" />
63
- <path d="m38 34 44 52" />
64
- </g>
65
- )}
66
- </svg>
67
- </span>
44
+ set:html={artwork}
45
+ />
@@ -10,25 +10,32 @@ import { resolveLumenAstroProps } from '@santi020k/lumen-core'
10
10
  type Props = (LocalImageProps | RemoteImageProps) & {
11
11
  class?: string | null
12
12
  className?: string
13
+ fit?: 'contain' | 'cover'
13
14
  invertOnDark?: boolean
15
+ radius?: 'full' | 'lg' | 'md' | 'none' | 'sm'
14
16
  }
15
17
 
16
18
  const {
17
19
  class: classProp = '',
18
20
  className = '',
21
+ fit = 'cover',
19
22
  invertOnDark = false,
20
23
  loading = 'lazy',
24
+ radius = 'lg',
21
25
  ...rest
22
26
  } = Astro.props
23
27
 
24
28
  const { classList, passthrough } = resolveLumenAstroProps(rest, [
25
29
  'ui-image',
26
- invertOnDark && 'ui-image--invert-dark'
30
+ invertOnDark && 'ui-image--invert-dark',
31
+ `ui-image--fit-${fit}`,
32
+ `ui-image--radius-${radius}`
27
33
  ], classProp, className)
28
34
  ---
29
35
 
30
36
  <AstroImage
31
37
  loading={loading}
32
38
  class:list={classList}
39
+ fit={fit}
33
40
  {...passthrough}
34
41
  />
@@ -4,6 +4,7 @@ import type { HTMLAttributes } from 'astro/types'
4
4
  import {
5
5
  alignLumenChartSeries,
6
6
  createLumenLineGeometry,
7
+ formatLumenChartSummary,
7
8
  getLumenChartCategories,
8
9
  getLumenChartDomain,
9
10
  getLumenChartTicks,
@@ -33,6 +34,7 @@ interface Props extends HTMLAttributes<'figure'> {
33
34
  series: readonly LumenChartSeries[]
34
35
  showLegend?: boolean
35
36
  showTable?: boolean
37
+ summary?: string
36
38
  }
37
39
 
38
40
  const {
@@ -51,6 +53,7 @@ const {
51
53
  series,
52
54
  showLegend = series.length > 1,
53
55
  showTable = true,
56
+ summary = formatLumenChartSummary(series, formatValue),
54
57
  ...rest
55
58
  } = Astro.props
56
59
 
@@ -99,6 +102,7 @@ const referenceY =
99
102
  {description}
100
103
  {glass}
101
104
  {heading}
105
+ {summary}
102
106
  {...rest}
103
107
  >
104
108
  {
@@ -227,7 +231,7 @@ const referenceY =
227
231
  return (
228
232
  <td>
229
233
  {datum?.label ??
230
- (datum?.y === null || datum === undefined ?
234
+ (datum?.y === undefined || datum.y === null || !Number.isFinite(datum.y) ?
231
235
  'Not available' :
232
236
  formatValue(datum.y))}
233
237
  </td>