@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.
Files changed (103) hide show
  1. package/README.md +403 -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/LollipopChart.astro +8 -0
  46. package/components/MediaFilmstrip.astro +19 -0
  47. package/components/MediaThumbnail.astro +29 -0
  48. package/components/MediaViewport.astro +38 -0
  49. package/components/Mentions.astro +2 -1
  50. package/components/MessageScroller.astro +6 -0
  51. package/components/MotionGroup.astro +19 -0
  52. package/components/NativeSelect.astro +2 -2
  53. package/components/Pagination.astro +2 -1
  54. package/components/PasswordField.astro +1 -1
  55. package/components/PhoneInput.astro +60 -9
  56. package/components/PhoneNumber.astro +19 -0
  57. package/components/PieChart.astro +26 -3
  58. package/components/PromptComposer.astro +36 -0
  59. package/components/RangeChart.astro +38 -8
  60. package/components/ScatterChart.astro +62 -14
  61. package/components/Segmented.astro +5 -5
  62. package/components/Select.astro +8 -8
  63. package/components/SourceCitation.astro +18 -0
  64. package/components/Sparkline.astro +9 -10
  65. package/components/Stack.astro +2 -2
  66. package/components/StreamMessage.astro +26 -0
  67. package/components/Tabs.astro +3 -0
  68. package/components/ToolActivity.astro +17 -0
  69. package/components/TreeSelect.astro +1 -0
  70. package/components/VirtualList.astro +5 -2
  71. package/components/VisualEffect.astro +21 -0
  72. package/components/WaterfallChart.astro +27 -0
  73. package/components/WorldMap.astro +213 -0
  74. package/forms.ts +17 -4
  75. package/index.ts +37 -0
  76. package/internal/ChartInspection.astro +27 -0
  77. package/internal/ComparisonChart.astro +69 -0
  78. package/internal/IntervalChart.astro +66 -0
  79. package/package.json +6 -4
  80. package/runtime/UIPrimitives.astro +670 -647
  81. package/runtime/chart-actions.ts +19 -0
  82. package/runtime/controllers/amount-field.ts +12 -0
  83. package/runtime/controllers/attachments.ts +13 -0
  84. package/runtime/controllers/chart-activation.ts +33 -0
  85. package/runtime/controllers/charts.ts +33 -0
  86. package/runtime/controllers/combobox.ts +39 -0
  87. package/runtime/controllers/comparison-reset.ts +20 -0
  88. package/runtime/controllers/data-table.ts +142 -0
  89. package/runtime/controllers/dialogs.ts +32 -32
  90. package/runtime/controllers/document-navigation.ts +54 -22
  91. package/runtime/controllers/file-upload.ts +89 -0
  92. package/runtime/controllers/image-comparison.ts +53 -0
  93. package/runtime/controllers/media-selection.ts +20 -0
  94. package/runtime/controllers/media-viewport.ts +54 -0
  95. package/runtime/controllers/media-workspace.ts +8 -0
  96. package/runtime/controllers/message-scroller.ts +13 -0
  97. package/runtime/controllers/motion.ts +15 -16
  98. package/runtime/controllers/optional-media.ts +29 -0
  99. package/runtime/controllers/phone-input.ts +128 -39
  100. package/runtime/controllers/visual-interactions.ts +75 -0
  101. package/runtime/controllers/world-map.ts +154 -0
  102. package/styles/critical.css +634 -61
  103. package/styles/lumen.css +101 -0
@@ -0,0 +1,76 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types'
3
+
4
+ import { createLumenCalendarHeatmapGeometry, type LumenCalendarHeatmapDatum, type LumenCalendarHeatmapOptions, type LumenChartLabels, resolveLumenChartLabels } from '@santi020k/lumen-core'
5
+
6
+ import Chart from './Chart.astro'
7
+
8
+ interface Props extends HTMLAttributes<'figure'>, LumenCalendarHeatmapOptions {
9
+ data: readonly LumenCalendarHeatmapDatum[]
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
+ formatDate?: (date: string) => string
18
+ weekdayLabels?: readonly string[]
19
+ labels?: Partial<LumenChartLabels>
20
+ showTable?: boolean
21
+ }
22
+
23
+ const { data, startDate, endDate, weekStartsOn = 0, domain, formatValue = String, formatDate = String,
24
+ weekdayLabels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], labels, showTable = true, summary,
25
+ class: classProp, className, ...props } = Astro.props
26
+
27
+ const text = resolveLumenChartLabels(labels)
28
+ const model = createLumenCalendarHeatmapGeometry(data, { startDate, endDate, weekStartsOn, domain })
29
+ const observed = model.cells.filter(cell => cell.value !== null).length
30
+ const format = (value: number | null) => value === null ? text.notAvailable : formatValue(value)
31
+ const width = Math.max(240, model.weekCount * 18 + 52)
32
+ const factual = model.valid ? `${formatDate(startDate)} – ${formatDate(endDate)}. ${text.value}: ${observed}/${model.cells.length}. ${text.notAvailable}: ${model.cells.length - observed}.` : text.invalidData
33
+ ---
34
+ <Chart {...props} class:list={['ui-calendar-heatmap', classProp, className]} summary={summary ?? factual}>
35
+ {model.valid ?
36
+ (
37
+ <>
38
+ <p class="ui-calendar-heatmap__range">{formatDate(startDate)} – {formatDate(endDate)}</p>
39
+ <div class="ui-calendar-heatmap__plot" role="group" tabindex="0" aria-label={text.chartData}>
40
+ <svg viewBox={`0 0 ${width} 152`} width={width} height="152" aria-hidden="true">
41
+ {Array.from({ length: 7 }, (_, day) => (
42
+ <text
43
+ x="0"
44
+ y={day * 18 + 15}
45
+ class="ui-calendar-heatmap__weekday"
46
+ >{weekdayLabels[(day + weekStartsOn) % 7] ?? ''}</text>
47
+ ))}
48
+ {model.cells.map(cell => (
49
+ <g><rect class="ui-calendar-heatmap__cell" data-missing={cell.value === null ? '' : undefined} x={cell.week * 18 + 46} y={cell.day * 18 + 2} width="15" height="15" rx="3" style={{ opacity: cell.ratio === null ? 1 : 0.15 + cell.ratio * 0.85 }} /><title>{formatDate(cell.date)}: {format(cell.value)}</title>{cell.value === null && (
50
+ <text
51
+ x={cell.week * 18 + 53.5}
52
+ y={cell.day * 18 + 14}
53
+ text-anchor="middle"
54
+ class="ui-calendar-heatmap__missing"
55
+ >×</text>
56
+ )}</g>
57
+ ))}
58
+ {model.cells.filter(cell => cell.day === 0 && cell.week % 4 === 0).map(cell => (
59
+ <text
60
+ x={cell.week * 18 + 46}
61
+ y="145"
62
+ class="ui-calendar-heatmap__weekday"
63
+ >{cell.date.slice(5)}</text>
64
+ ))}
65
+ </svg>
66
+ </div>
67
+ <div class="ui-calendar-heatmap__legend"><span>{formatValue(model.domain.min)}</span><span
68
+ class="ui-calendar-heatmap__scale"
69
+ aria-hidden="true"
70
+ /><span>{formatValue(model.domain.max)}</span><span>× {text.notAvailable}</span></div>
71
+ {!showTable && <ul class="ui-sr-only">{model.cells.map(cell => <li>{formatDate(cell.date)}: {format(cell.value)}</li>)}</ul>}
72
+ {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><th scope="col">{text.value}</th></tr></thead><tbody>{model.cells.map(cell => <tr><th scope="row">{formatDate(cell.date)}</th><td>{format(cell.value)}</td></tr>)}</tbody></table></div></details>}
73
+ </>
74
+ ) :
75
+ <p class="ui-chart__empty" role="status">{text.invalidData}</p>}
76
+ </Chart>
@@ -7,6 +7,7 @@ interface Props extends HTMLAttributes<'div'> {
7
7
  as?: 'article' | 'div' | 'section'
8
8
  class?: string | null
9
9
  className?: string
10
+ density?: 'comfortable' | 'compact' | 'spacious'
10
11
  glass?: boolean | 'subtle' | 'strong'
11
12
  variant?: 'default' | 'glass' | 'interactive' | 'muted' | 'unstyled'
12
13
  }
@@ -15,6 +16,7 @@ const {
15
16
  as: Tag = 'div',
16
17
  class: classProp = '',
17
18
  className = '',
19
+ density = 'comfortable',
18
20
  glass = false,
19
21
  variant = 'default',
20
22
  ...rest
@@ -24,6 +26,7 @@ const isGlass = glass || variant === 'glass'
24
26
 
25
27
  const { classList, passthrough } = resolveLumenAstroProps(rest, [
26
28
  'ui-card',
29
+ `ui-card--${density}`,
27
30
  variant === 'unstyled' && 'ui-card--unstyled',
28
31
  variant === 'muted' && 'ui-card--muted',
29
32
  variant === 'interactive' && 'ui-card--interactive',
@@ -35,6 +38,7 @@ const { classList, passthrough } = resolveLumenAstroProps(rest, [
35
38
 
36
39
  <Tag
37
40
  class:list={classList}
41
+ data-density={density}
38
42
  data-slot="card"
39
43
  data-variant={variant}
40
44
  {...passthrough}
@@ -0,0 +1,36 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types'
3
+
4
+ import { type LumenChangeSummaryItem, resolveLumenAstroProps } from '@santi020k/lumen-core'
5
+
6
+ interface Props extends HTMLAttributes<'section'> {
7
+ items?: readonly LumenChangeSummaryItem[]
8
+ label?: string
9
+ beforeLabel?: string
10
+ afterLabel?: string
11
+ changedLabel?: string
12
+ unchangedLabel?: string
13
+ summary?: string
14
+ class?: string | null
15
+ className?: string
16
+ }
17
+
18
+ const { items = [], label = 'Changes', beforeLabel = 'Before', afterLabel = 'After', changedLabel = 'Changed',
19
+ unchangedLabel = 'Unchanged', summary, class: classProp, className, ...rest } = Astro.props
20
+
21
+ const { classList, passthrough } = resolveLumenAstroProps(rest, ['ui-change-summary'], classProp, className)
22
+ ---
23
+ <section class:list={classList} aria-label={label} {...passthrough}>
24
+ <h3 class="ui-change-summary__heading">{label}</h3>
25
+ {summary && <p class="ui-change-summary__summary">{summary}</p>}
26
+ <dl class="ui-change-summary__list">
27
+ {items.map(item => (
28
+ <div class="ui-change-summary__item" data-changed={item.changed}>
29
+ <dt>{item.label}<span class="ui-change-summary__state">{item.changed ? changedLabel : unchangedLabel}</span></dt>
30
+ <dd><span class="ui-change-summary__value-label">{beforeLabel}</span>{item.before}</dd>
31
+ <dd><span class="ui-change-summary__value-label">{afterLabel}</span>{item.after}</dd>
32
+ </div>
33
+ ))}
34
+ </dl>
35
+ <slot />
36
+ </section>
@@ -0,0 +1,24 @@
1
+ ---
2
+ import type { ChartDatumAction } from '../runtime/chart-actions'
3
+
4
+ import Button from './Button.astro'
5
+
6
+ interface Props {
7
+ actions: readonly (ChartDatumAction | null)[]
8
+ label: string
9
+ }
10
+
11
+ const { actions, label } = Astro.props
12
+ const available = actions.filter(action => action !== null)
13
+ ---
14
+
15
+ {available.length > 0 && (
16
+ <details class="ui-chart__actions" data-ui-chart-actions>
17
+ <summary>{label}</summary>
18
+ <ul>
19
+ {available.map(action => (
20
+ <li><Button variant="ghost" data-ui-chart-datum={action.serialized}>{action.label}</Button></li>
21
+ ))}
22
+ </ul>
23
+ </details>
24
+ )}
@@ -0,0 +1,11 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types'
3
+
4
+ import { type LumenWorkflowDuration, resolveLumenAstroProps } from '@santi020k/lumen-core'
5
+
6
+ interface Props extends HTMLAttributes<'div'> { duration?: LumenWorkflowDuration, className?: string }
7
+
8
+ const { duration = 'standard', class: classProp = '', className = '', ...rest } = Astro.props
9
+ const { classList, passthrough } = resolveLumenAstroProps(rest, [], classProp, className)
10
+ ---
11
+ <div class:list={classList} data-ui-chart-motion data-ui-motion-duration={duration} {...passthrough}><slot /></div>
@@ -12,6 +12,10 @@ interface Props extends HTMLAttributes<'code'> {
12
12
  className?: string
13
13
  code?: string
14
14
  copy?: boolean
15
+ codeLabel?: string
16
+ copyLabel?: string
17
+ copiedLabel?: string
18
+ errorLabel?: string
15
19
  highlighted?: boolean
16
20
  label?: string
17
21
  language?: string
@@ -25,6 +29,10 @@ const {
25
29
  className = '',
26
30
  code,
27
31
  copy = false,
32
+ codeLabel = 'Code example',
33
+ copyLabel = 'Copy code to clipboard',
34
+ copiedLabel = 'Code copied to clipboard',
35
+ errorLabel = 'Could not copy code. Select and copy it manually.',
28
36
  highlighted = false,
29
37
  label,
30
38
  language,
@@ -54,6 +62,7 @@ const renderedCode = hasCode ? renderLumenCodeHtml(code, language) : ''
54
62
  (
55
63
  <figure
56
64
  class:list={blockClassList}
65
+ data-code-label={codeLabel}
57
66
  data-code-theme={theme}
58
67
  data-language={language}
59
68
  data-slot="code"
@@ -73,8 +82,14 @@ const renderedCode = hasCode ? renderLumenCodeHtml(code, language) : ''
73
82
  </span>
74
83
  {copy && (
75
84
  <button
76
- aria-label="Copy code to clipboard"
85
+ aria-label={copyLabel}
77
86
  class="ui-code__copy"
87
+ data-copy-value={highlighted ? code : undefined}
88
+ data-copy-label={copyLabel}
89
+ data-copy-copied-label={copiedLabel}
90
+ data-copy-error-label={errorLabel}
91
+ data-state="idle"
92
+ title={copyLabel}
78
93
  data-ui-code-copy
79
94
  type="button"
80
95
  >
@@ -85,8 +100,8 @@ const renderedCode = hasCode ? renderLumenCodeHtml(code, language) : ''
85
100
  </figcaption>
86
101
  )}
87
102
  {highlighted && <slot />}
88
- {!highlighted && hasCode && <pre><code set:html={renderedCode}></code></pre>}
89
- {!highlighted && !hasCode && <pre><code><slot /></code></pre>}
103
+ {!highlighted && hasCode && <pre aria-label={!wrap ? codeLabel : undefined} role={!wrap ? 'region' : undefined} tabindex={!wrap ? 0 : undefined}><code set:html={renderedCode}></code></pre>}
104
+ {!highlighted && !hasCode && <pre aria-label={!wrap ? codeLabel : undefined} role={!wrap ? 'region' : undefined} tabindex={!wrap ? 0 : undefined}><code><slot /></code></pre>}
90
105
  </figure>
91
106
  ) :
92
107
  (
@@ -20,6 +20,10 @@ interface Props extends HTMLAttributes<'div'> {
20
20
  class?: string | null
21
21
  className?: string
22
22
  copy?: boolean
23
+ codeLabel?: string
24
+ copyLabel?: string
25
+ copiedLabel?: string
26
+ errorLabel?: string
23
27
  initialValue?: string
24
28
  items: readonly CodeTabItem[]
25
29
  storageKey?: string
@@ -32,6 +36,10 @@ const {
32
36
  class: classProp = '',
33
37
  className = '',
34
38
  copy = true,
39
+ codeLabel = 'Code example',
40
+ copyLabel = 'Copy code to clipboard',
41
+ copiedLabel = 'Code copied to clipboard',
42
+ errorLabel = 'Could not copy code. Select and copy it manually.',
35
43
  initialValue,
36
44
  items,
37
45
  storageKey,
@@ -82,6 +90,10 @@ const { classList, passthrough } = resolveLumenAstroProps(
82
90
  <Code
83
91
  code={item.code}
84
92
  copy={copy}
93
+ codeLabel={codeLabel}
94
+ copyLabel={copyLabel}
95
+ copiedLabel={copiedLabel}
96
+ errorLabel={errorLabel}
85
97
  {...(item.language === undefined ? {} : { language: item.language })}
86
98
  theme={theme}
87
99
  variant="block"
@@ -1,28 +1,35 @@
1
1
  ---
2
2
  import type { HTMLAttributes } from 'astro/types'
3
3
 
4
- import {
5
- alignLumenChartSeries,
4
+ import { alignLumenChartSeries,
6
5
  createLumenBarGeometry,
6
+ createLumenChartDatumActivation,
7
7
  createLumenLineGeometry,
8
8
  formatLumenChartSummary,
9
9
  getLumenChartCategories,
10
+ getLumenChartCategoryLabel,
11
+ getLumenChartCategoryTicks,
10
12
  getLumenChartDomain,
13
+ getLumenChartTicks,
11
14
  getLumenChartToneClassName,
12
15
  hasLumenChartData,
13
16
  type LumenChartDatum,
14
17
  type LumenChartLabels,
15
18
  type LumenComboSeries,
16
19
  resolveLumenChartLabels,
17
- resolveLumenChartTone } from '@santi020k/lumen-core'
20
+ resolveLumenChartTone, scaleLumenChartValue } from '@santi020k/lumen-core'
21
+
22
+ import { createChartDatumAction } from '../runtime/chart-actions'
18
23
 
19
24
  import Chart from './Chart.astro'
25
+ import ChartDatumActions from './ChartDatumActions.astro'
20
26
 
21
27
  interface Props extends HTMLAttributes<'figure'> {
22
28
  caption?: string
23
29
  class?: string | null
24
30
  className?: string
25
31
  description?: string
32
+ drilldown?: boolean
26
33
  formatValue?: (value: number) => string
27
34
  heading?: string
28
35
  labels?: Partial<LumenChartLabels>
@@ -32,7 +39,7 @@ interface Props extends HTMLAttributes<'figure'> {
32
39
  summary?: string
33
40
  }
34
41
 
35
- const { caption, class: classProp = '', className = '', description, formatValue = String, heading, labels, series, showLegend = true, showTable = true, summary: summaryProp, ...rest } = Astro.props
42
+ const { caption, class: classProp = '', className = '', description, drilldown = false, formatValue = String, heading, labels, series, showLegend = true, showTable = true, summary: summaryProp, ...rest } = Astro.props
36
43
  const resolvedLabels = resolveLumenChartLabels(labels)
37
44
  const summary = summaryProp ?? formatLumenChartSummary(series, formatValue, resolvedLabels)
38
45
  const width = 640
@@ -73,6 +80,8 @@ const lineGeometryOptions = {
73
80
  ...(barSeries.length === 0 ?
74
81
  {} :
75
82
  {
83
+ paddingBottom: bars.margin.bottom,
84
+ paddingTop: bars.margin.top,
76
85
  xDomain: { max: 1, min: 0 },
77
86
  xScale: 'linear' as const
78
87
  })
@@ -83,16 +92,80 @@ const lines = lineSeries.map(item => createLumenLineGeometry(
83
92
  lineGeometryOptions
84
93
  ))
85
94
 
95
+ const plotTop = barSeries.length > 0 ? bars.margin.top : padding
96
+ const plotBottom = height - (barSeries.length > 0 ? bars.margin.bottom : padding)
97
+ const plotLeft = barSeries.length > 0 ? bars.margin.left : padding
98
+ const plotRight = width - (barSeries.length > 0 ? bars.margin.right : padding)
99
+ const ticks = getLumenChartTicks(domain)
100
+
101
+ const categoryTicks = getLumenChartCategoryTicks(categories.map(category => getLumenChartCategoryLabel(series, category)), {
102
+ start: plotLeft,
103
+ end: plotRight,
104
+ ...(barSeries.length > 0 ? { positions: bars.categories.map(category => category.x) } : {})
105
+ })
106
+
86
107
  const hasData = hasLumenChartData(series)
108
+
109
+ const barActions = drilldown ?
110
+ bars.marks.map(mark => {
111
+ const item = barSeries.find(candidate => candidate.id === mark.seriesId)
112
+ const datum = item?.data.find(candidate => candidate.x === mark.category)
113
+
114
+ return item && datum ?
115
+ createChartDatumAction(
116
+ createLumenChartDatumActivation(item.id, datum),
117
+ `${datum.xLabel ?? datum.x} · ${item.label}: ${datum.label ?? formatValue(mark.value)}`,
118
+ resolvedLabels
119
+ ) :
120
+ null
121
+ }) :
122
+ []
123
+
124
+ const pointActions = drilldown ?
125
+ lines.map((geometry, index) => {
126
+ const item = lineSeries[index]
127
+
128
+ return geometry.points.map(point => {
129
+ const datum = item?.data.find(candidate => alignComboLineDatum(candidate).x === point.x)
130
+
131
+ return item && datum ?
132
+ createChartDatumAction(
133
+ createLumenChartDatumActivation(item.id, datum),
134
+ `${datum.xLabel ?? datum.x} · ${item.label}: ${datum.label ?? formatValue(point.y ?? 0)}`,
135
+ resolvedLabels
136
+ ) :
137
+ null
138
+ })
139
+ }) :
140
+ []
141
+
142
+ const datumActions = [...barActions, ...pointActions.flat()]
143
+
87
144
  ---
88
145
 
89
- <Chart class:list={['ui-combo-chart', classProp, className]} {caption} {description} {heading} {summary} {...rest}>
146
+ <Chart class:list={['ui-combo-chart', classProp, className]} {caption} {description} {heading} {summary} {...rest} data-ui-chart-activation={drilldown || undefined}>
90
147
  {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
148
  {!hasData && <p class="ui-chart__empty" role="status">{resolvedLabels.empty}</p>}
92
- <div class="ui-chart__plot" hidden={!hasData}><svg aria-hidden="true" viewBox={`0 0 ${width} ${height}`}><g class="
93
- ui-bar-chart__marks
149
+ <div aria-label={resolvedLabels.chartData} class="ui-chart__plot" role="region" tabindex="0" hidden={!hasData}><svg aria-hidden="true" viewBox={`0 0 ${width} ${height}`}><g class="
150
+ ui-chart__grid
94
151
  "
95
- >{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) => {
152
+ >{ticks.map(tick => {
153
+ const y = scaleLumenChartValue(tick, domain, plotBottom, plotTop)
154
+
155
+ return <><line x1={plotLeft} x2={plotRight} y1={y} y2={y} /><text x={plotLeft - 8} y={y}>{formatValue(tick)}</text></>
156
+ })}</g><g class="ui-chart__axis-labels">{categoryTicks.map(tick => <text x={tick.position} y={height - 12} text-anchor={tick.textAnchor}>{tick.label}</text>)}</g><g class="
157
+ ui-bar-chart__marks
158
+ "
159
+ >{bars.marks.map((mark, index) => <rect data-ui-chart-datum={barActions[index]?.serialized} 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>)}{drilldown && bars.marks.map((mark, index) => (
160
+ <rect
161
+ class="ui-chart__datum-hit"
162
+ width={Math.max(12, mark.width)}
163
+ height={Math.max(12, mark.height)}
164
+ x={mark.x - Math.max(0, 12 - mark.width) / 2}
165
+ y={mark.y - Math.max(0, 12 - mark.height) / 2}
166
+ data-ui-chart-datum={barActions[index]?.serialized}
167
+ />
168
+ ))}</g>{lines.map((geometry, index) => {
96
169
  const item = lineSeries[index]
97
170
 
98
171
  if (!item) return null
@@ -108,16 +181,25 @@ d={path}
108
181
  )) }<path
109
182
  class="ui-line-chart__line"
110
183
  d={geometry.path}
111
- /></g>
184
+ />{geometry.points.map(point => <circle class="ui-line-chart__point" cx={point.xCoordinate} cy={point.yCoordinate} r="3.5" />)}{drilldown && geometry.points.map((point, pointIndex) => (
185
+ <circle
186
+ class="ui-chart__datum-hit"
187
+ cx={point.xCoordinate}
188
+ cy={point.yCoordinate}
189
+ r="10"
190
+ data-ui-chart-datum={pointActions[index]?.[pointIndex]?.serialized}
191
+ />
192
+ ))}</g>
112
193
  )
113
194
  })}</svg></div>
114
195
  {showTable && hasData && (
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 => (
116
- <tr><th scope="row">{String(category)}</th>{series.map(item => {
196
+ <details class="ui-chart__data"><summary>{resolvedLabels.viewData}</summary><div role="group" tabindex="0" aria-label={resolvedLabels.chartData}><table><thead><tr><th scope="col">{resolvedLabels.category}</th>{series.map(item => <th scope="col">{item.label}</th>)}</tr></thead><tbody>{categories.map(category => (
197
+ <tr><th scope="row">{String(category)}</th>{alignedSeries.map(item => {
117
198
  const datum = item.data.find(point => point.x === category)
118
199
 
119
200
  return <td>{datum?.label ?? (datum?.y === undefined || datum.y === null || !Number.isFinite(datum.y) ? resolvedLabels.notAvailable : formatValue(datum.y))}</td>
120
201
  })}</tr>
121
202
  ))}</tbody></table></div></details>
122
203
  )}
204
+ {drilldown && <ChartDatumActions actions={datumActions} label={resolvedLabels.exploreData} />}
123
205
  </Chart>
@@ -0,0 +1,23 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types'
3
+
4
+ import { getLumenPhoneFlagSource } from '@santi020k/lumen-core'
5
+
6
+ interface Props extends HTMLAttributes<'span'> {
7
+ regionCode: string
8
+ decorative?: boolean
9
+ }
10
+
11
+ const { regionCode, decorative = false, class: className, ...props } = Astro.props
12
+ const source = getLumenPhoneFlagSource(regionCode)
13
+ ---
14
+ <span
15
+ {...props}
16
+ aria-hidden={decorative || undefined}
17
+ aria-label={decorative ? undefined : props['aria-label'] ?? regionCode.toUpperCase()}
18
+ class:list={['ui-country-flag', className]}
19
+ data-slot="country-flag"
20
+ role={decorative ? undefined : 'img'}
21
+ >
22
+ {source ? <img alt="" height="18" src={source} width="24" /> : regionCode.toUpperCase().slice(0, 2)}
23
+ </span>
@@ -33,6 +33,8 @@ interface Props extends HTMLAttributes<'div'> {
33
33
  class?: string | null
34
34
  className?: string
35
35
  columns?: DataTableColumn[]
36
+ layout?: 'records' | 'scroll'
37
+ sortMode?: 'client' | 'manual'
36
38
  glass?: boolean | 'subtle' | 'strong'
37
39
  name?: string
38
40
  rows?: DataTableRow[]
@@ -44,6 +46,8 @@ const {
44
46
  className = '',
45
47
  columns = [],
46
48
  glass = false,
49
+ layout = 'scroll',
50
+ sortMode = 'client',
47
51
  name,
48
52
  rows = [],
49
53
  selectable = false,
@@ -80,17 +84,20 @@ const getRowValue = (row: DataTableRow, index: number): string => String(row.row
80
84
  class:list={classList}
81
85
  data-ui-datatable
82
86
  data-ui-datatable-name={name}
87
+ data-ui-datatable-sort-mode={sortMode}
88
+ data-layout={layout}
83
89
  data-ui-datatable-selectable={selectable ? 'true' : undefined}
84
90
  data-ui-glass-track={glass ? true : undefined}
85
91
  {...passthrough}
86
92
  >
87
93
  {hasData ?
88
94
  (
89
- <table>
90
- <thead>
91
- <tr>
95
+ <table class="ui-table" data-layout={layout} role="table">
96
+ <thead role="rowgroup">
97
+ <tr role="row">
92
98
  {columns.map(column => (
93
99
  <th
100
+ data-ui-datatable-sort-key={column.key}
94
101
  data-ui-datatable-sort-type={column.sort}
95
102
  data-ui-datatable-sortable={column.sortable ? 'true' : undefined}
96
103
  scope="col"
@@ -100,15 +107,16 @@ const getRowValue = (row: DataTableRow, index: number): string => String(row.row
100
107
  ))}
101
108
  </tr>
102
109
  </thead>
103
- <tbody>
110
+ <tbody role="rowgroup">
104
111
  {rows.map((row, rowIndex) => (
105
- <tr data-ui-datatable-row data-value={getRowValue(row, rowIndex)}>
112
+ <tr role="row" data-ui-datatable-row data-value={getRowValue(row, rowIndex)}>
106
113
  {columns.map(column => {
107
114
  const cell = row[column.key]
108
115
 
109
116
  return (
110
- <td data-sort-value={getSortValue(cell)}>
111
- {formatCell(cell)}
117
+ <td role="cell" data-sort-value={getSortValue(cell)}>
118
+ {layout === 'records' && <span class="ui-table__label" aria-hidden="true">{column.header ?? column.label ?? column.key}</span>}
119
+ <div class="ui-table__value">{formatCell(cell)}</div>
112
120
  </td>
113
121
  )
114
122
  })}
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  import type { HTMLAttributes } from 'astro/types'
3
3
 
4
- import { resolveLumenAstroProps } from '@santi020k/lumen-core'
4
+ import { type LumenDateLabels, parseLumenDate, resolveLumenAstroProps, resolveLumenDateLabels, resolveLumenDateLocale } from '@santi020k/lumen-core'
5
5
 
6
6
  import Calendar from './Calendar.astro'
7
7
 
@@ -9,6 +9,8 @@ interface Props extends HTMLAttributes<'input'> {
9
9
  class?: string | null
10
10
  className?: string
11
11
  glass?: boolean | 'subtle' | 'strong'
12
+ labels?: Partial<Pick<LumenDateLabels, 'previousMonth' | 'nextMonth' | 'chooseDate'>>
13
+ locale?: string
12
14
  placeholder?: string
13
15
  }
14
16
 
@@ -18,6 +20,9 @@ const {
18
20
  disabled = false,
19
21
  glass = false,
20
22
  id,
23
+ labels,
24
+ locale,
25
+ readonly = false,
21
26
  max,
22
27
  min,
23
28
  name,
@@ -39,28 +44,28 @@ const { classList, passthrough } = resolveLumenAstroProps(rest, [
39
44
  glass === 'strong' && 'ui-glass-strong'
40
45
  ], classProp, className)
41
46
 
47
+ const resolvedLocale = resolveLumenDateLocale(locale)
48
+ const resolvedLabels = { ...resolveLumenDateLabels(resolvedLocale), ...labels }
42
49
  const selectedValue = value === undefined || value === null ? undefined : String(value)
43
- const hasSelectedValue = selectedValue !== undefined && selectedValue !== ''
44
- const placeholderText = placeholder ?? 'Choose a date'
50
+ const selectedDate = parseLumenDate(selectedValue)
51
+ const hasSelectedValue = selectedDate !== null
52
+ const placeholderText = placeholder ?? resolvedLabels.chooseDate
45
53
 
46
- const selectedDate = hasSelectedValue ?
47
- new Date(`${selectedValue}T00:00:00.000Z`) :
48
- undefined
49
-
50
- const displayValue = selectedDate && !Number.isNaN(selectedDate.getTime()) ?
51
- new Intl.DateTimeFormat('en', {
52
- day: 'numeric',
53
- month: 'short',
54
- timeZone: 'UTC',
55
- year: 'numeric'
54
+ const displayValue = selectedDate ?
55
+ new Intl.DateTimeFormat(resolvedLocale, {
56
+ day: 'numeric', month: 'short', timeZone: 'UTC', year: 'numeric'
56
57
  }).format(selectedDate) :
57
- placeholderText
58
+ placeholderText
59
+
58
60
  ---
59
61
 
60
62
  <div
61
63
  class:list={classList}
62
64
  data-placeholder={hasSelectedValue ? undefined : 'true'}
63
65
  data-ui-date-picker
66
+ data-locale={locale}
67
+ data-choose-date-label={labels?.chooseDate}
68
+ data-placeholder-default={placeholder === undefined ? 'true' : undefined}
64
69
  data-ui-glass-track={glass ? true : undefined}
65
70
  >
66
71
  <input
@@ -71,7 +76,8 @@ const displayValue = selectedDate && !Number.isNaN(selectedDate.getTime()) ?
71
76
  max={max}
72
77
  min={min}
73
78
  name={name}
74
- placeholder={placeholder}
79
+ placeholder={placeholderText}
80
+ readonly={readonly}
75
81
  required={required}
76
82
  type={type}
77
83
  value={value}
@@ -83,6 +89,7 @@ const displayValue = selectedDate && !Number.isNaN(selectedDate.getTime()) ?
83
89
  aria-controls={popoverId}
84
90
  aria-expanded="false"
85
91
  aria-haspopup="dialog"
92
+ aria-disabled={readonly ? 'true' : undefined}
86
93
  aria-required={required ? 'true' : undefined}
87
94
  class="ui-input ui-date-picker ui-date-picker__trigger"
88
95
  data-ui-date-picker-trigger
@@ -97,7 +104,7 @@ const displayValue = selectedDate && !Number.isNaN(selectedDate.getTime()) ?
97
104
  </button>
98
105
 
99
106
  <div
100
- aria-label="Choose date"
107
+ aria-label={resolvedLabels.chooseDate}
101
108
  class="ui-date-picker__popover"
102
109
  data-state="closed"
103
110
  data-ui-date-picker-popover
@@ -105,7 +112,7 @@ const displayValue = selectedDate && !Number.isNaN(selectedDate.getTime()) ?
105
112
  id={popoverId}
106
113
  role="dialog"
107
114
  >
108
- <Calendar max={typeof max === 'string' ? max : undefined} min={typeof min === 'string' ? min : undefined} value={selectedValue} />
115
+ <Calendar locale={locale} labels={labels} readOnly={readonly} max={typeof max === 'string' ? max : undefined} min={typeof min === 'string' ? min : undefined} value={selectedValue} />
109
116
  </div>
110
117
  </div>
111
118
  </div>
@@ -0,0 +1,18 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types'
3
+
4
+ import { resolveLumenAstroProps } from '@santi020k/lumen-core'
5
+
6
+ interface Props extends HTMLAttributes<'dd'> {
7
+ class?: string | null
8
+ className?: string
9
+ }
10
+
11
+ const { class: classProp = '', className = '', ...rest } = Astro.props
12
+
13
+ const { classList, passthrough } = resolveLumenAstroProps(
14
+ rest, ['ui-description-detail', 'ui-descriptions__detail'], classProp, className
15
+ )
16
+ ---
17
+
18
+ <dd class:list={classList} data-slot="description-detail" {...passthrough}><slot /></dd>