@santi020k/lumen-astro 0.2.0 → 0.4.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 +83 -0
- package/components/Anchor.astro +14 -2
- package/components/BarChart.astro +185 -0
- package/components/Chart.astro +23 -0
- package/components/CodeTabs.astro +92 -0
- package/components/Dialog.astro +4 -0
- package/components/Icon.astro +1 -1
- package/components/LineChart.astro +227 -0
- package/components/Particles.astro +46 -0
- package/components/PieChart.astro +138 -0
- package/components/Pill.astro +6 -3
- package/components/ScrollProgress.astro +38 -0
- package/components/Sparkline.astro +72 -0
- package/components/Stat.astro +9 -4
- package/components/Watermark.astro +1 -1
- package/index.d.ts +6 -0
- package/index.ts +6 -0
- package/package.json +3 -3
- package/runtime/UIPrimitives.astro +43 -0
package/README.md
CHANGED
|
@@ -18,6 +18,10 @@ enhances all matching Lumen markup on the page.
|
|
|
18
18
|
@import "@santi020k/lumen-astro/styles.css";
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
The stylesheet defaults `--ui-font` to `"Montserrat", "Avenir Next", "Segoe UI", sans-serif`.
|
|
22
|
+
It declares the family stack but does not bundle or load font files. Load Montserrat once through
|
|
23
|
+
your preferred delivery path, or override `--ui-font` in application CSS.
|
|
24
|
+
|
|
21
25
|
With Tailwind, keep the Lumen import in the same shared CSS entry:
|
|
22
26
|
|
|
23
27
|
```css
|
|
@@ -61,10 +65,75 @@ import { Button, Icon } from '@santi020k/lumen-astro'
|
|
|
61
65
|
<Icon name="search" label="Search" />
|
|
62
66
|
```
|
|
63
67
|
|
|
68
|
+
## Long-form navigation
|
|
69
|
+
|
|
70
|
+
`Anchor` accepts an optional heading `depth` so article tables of contents can preserve their
|
|
71
|
+
hierarchy. `ScrollProgress` derives reading progress from the document and pins the indicator to
|
|
72
|
+
the top or bottom viewport edge. Both use the single root `UIPrimitives` runtime.
|
|
73
|
+
|
|
74
|
+
```astro
|
|
75
|
+
---
|
|
76
|
+
import { Anchor, ScrollProgress } from '@santi020k/lumen-astro'
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
<ScrollProgress aria-label="Article reading progress" />
|
|
80
|
+
<Anchor items={[
|
|
81
|
+
{ depth: 2, href: '#install', label: 'Install' },
|
|
82
|
+
{ depth: 3, href: '#astro', label: 'Astro' }
|
|
83
|
+
]} />
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Data visualization
|
|
87
|
+
|
|
88
|
+
Use `Sparkline` beside a metric, `BarChart` for categorical comparison, `LineChart` for ordered
|
|
89
|
+
trends, and `PieChart` for a small part-to-whole breakdown. The chart components accept
|
|
90
|
+
already-aggregated series, use Lumen data-color tokens, and include a revealable semantic data
|
|
91
|
+
table by default.
|
|
92
|
+
|
|
93
|
+
```astro
|
|
94
|
+
---
|
|
95
|
+
import { LineChart } from '@santi020k/lumen-astro'
|
|
96
|
+
|
|
97
|
+
const series = [{
|
|
98
|
+
id: 'views',
|
|
99
|
+
label: 'Views',
|
|
100
|
+
data: [{ x: 'Mon', y: 42 }, { x: 'Tue', y: 68 }]
|
|
101
|
+
}]
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
<LineChart aria-label="Views by day" heading="Website traffic" {series} />
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`PieChart` accepts one `LumenChartSeries`; its data points become slices. It defaults to
|
|
108
|
+
`variant="donut"` and also supports `variant="pie"`.
|
|
109
|
+
|
|
64
110
|
Lucide intentionally excludes brand marks. For GitHub and other project-specific icons, put the
|
|
65
111
|
SVG in the default slot instead of passing an unsupported `name`; Astro warns about unknown named
|
|
66
112
|
icons during development.
|
|
67
113
|
|
|
114
|
+
## Code tabs
|
|
115
|
+
|
|
116
|
+
Use `CodeTabs` for related install commands, language variants, or configuration examples. Each
|
|
117
|
+
item supplies a stable value, visible label, code string, and optional language. A shared
|
|
118
|
+
`storageKey` keeps matching instances synchronized and remembers the reader's selection.
|
|
119
|
+
|
|
120
|
+
```astro
|
|
121
|
+
---
|
|
122
|
+
import { CodeTabs } from '@santi020k/lumen-astro'
|
|
123
|
+
|
|
124
|
+
const installCommands = [
|
|
125
|
+
{ value: 'pnpm', label: 'pnpm', code: 'pnpm add @santi020k/lumen-astro', language: 'bash' },
|
|
126
|
+
{ value: 'npm', label: 'npm', code: 'npm install @santi020k/lumen-astro', language: 'bash' }
|
|
127
|
+
]
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
<CodeTabs
|
|
131
|
+
ariaLabel="Package manager"
|
|
132
|
+
items={installCommands}
|
|
133
|
+
storageKey="preferred-package-manager"
|
|
134
|
+
/>
|
|
135
|
+
```
|
|
136
|
+
|
|
68
137
|
## Optimized images
|
|
69
138
|
|
|
70
139
|
Lumen's Astro `Image` delegates to `astro:assets`, so imported image metadata, responsive layouts,
|
|
@@ -145,3 +214,17 @@ variants. The older `variant="glass"` form remains supported for cards. Overlay
|
|
|
145
214
|
<DatePicker glass="subtle" />
|
|
146
215
|
<Select glass="strong" options={['Astro', 'React']} />
|
|
147
216
|
```
|
|
217
|
+
|
|
218
|
+
## Semantic stat roots
|
|
219
|
+
|
|
220
|
+
`Stat` renders a `div` by default. Use `as="article"` when the metric and its supporting content
|
|
221
|
+
form a standalone item, or `as="section"` when it is a labeled region in a larger view.
|
|
222
|
+
|
|
223
|
+
```astro
|
|
224
|
+
<Stat as="article" label="Active workspaces" value="1,234" variant="accent">
|
|
225
|
+
<p>Across all production organizations.</p>
|
|
226
|
+
</Stat>
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Use `variant="default"` for the original neutral surface, `variant="accent"` for a featured metric,
|
|
230
|
+
or `variant="glass"` for selective translucency.
|
package/components/Anchor.astro
CHANGED
|
@@ -3,7 +3,13 @@ import { resolveLumenAstroProps } from '@santi020k/lumen-core'
|
|
|
3
3
|
|
|
4
4
|
import type { HTMLAttributes } from 'astro/types'
|
|
5
5
|
|
|
6
|
-
type
|
|
6
|
+
type AnchorDepth = 1 | 2 | 3 | 4 | 5 | 6
|
|
7
|
+
|
|
8
|
+
type AnchorLink = {
|
|
9
|
+
depth?: AnchorDepth
|
|
10
|
+
href: string
|
|
11
|
+
label: string
|
|
12
|
+
}
|
|
7
13
|
|
|
8
14
|
export interface Props extends HTMLAttributes<'nav'> {
|
|
9
15
|
class?: string | null
|
|
@@ -34,7 +40,13 @@ const { classList, passthrough } = resolveLumenAstroProps(rest, [
|
|
|
34
40
|
<ol>
|
|
35
41
|
{items.map((item, index) => (
|
|
36
42
|
<li>
|
|
37
|
-
<a
|
|
43
|
+
<a
|
|
44
|
+
data-active={index === 0 ? 'true' : 'false'}
|
|
45
|
+
data-depth={item.depth}
|
|
46
|
+
href={item.href}
|
|
47
|
+
>
|
|
48
|
+
{item.label}
|
|
49
|
+
</a>
|
|
38
50
|
</li>
|
|
39
51
|
))}
|
|
40
52
|
</ol>
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
---
|
|
2
|
+
import {
|
|
3
|
+
createLumenBarGeometry,
|
|
4
|
+
getLumenChartTicks,
|
|
5
|
+
hasLumenChartData,
|
|
6
|
+
type LumenBarChartLayout,
|
|
7
|
+
type LumenChartOrientation,
|
|
8
|
+
type LumenChartSeries,
|
|
9
|
+
resolveLumenChartTone,
|
|
10
|
+
scaleLumenChartValue
|
|
11
|
+
} from '@santi020k/lumen-core'
|
|
12
|
+
|
|
13
|
+
import type { HTMLAttributes } from 'astro/types'
|
|
14
|
+
|
|
15
|
+
import Chart from './Chart.astro'
|
|
16
|
+
|
|
17
|
+
interface Props extends HTMLAttributes<'figure'> {
|
|
18
|
+
caption?: string
|
|
19
|
+
class?: string | null
|
|
20
|
+
className?: string
|
|
21
|
+
description?: string
|
|
22
|
+
glass?: boolean | 'subtle' | 'strong'
|
|
23
|
+
heading?: string
|
|
24
|
+
layout?: LumenBarChartLayout
|
|
25
|
+
orientation?: LumenChartOrientation
|
|
26
|
+
series: readonly LumenChartSeries[]
|
|
27
|
+
showLegend?: boolean
|
|
28
|
+
showTable?: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const {
|
|
32
|
+
caption,
|
|
33
|
+
class: classProp = '',
|
|
34
|
+
className = '',
|
|
35
|
+
description,
|
|
36
|
+
glass = false,
|
|
37
|
+
heading,
|
|
38
|
+
layout = 'grouped',
|
|
39
|
+
orientation = 'vertical',
|
|
40
|
+
series,
|
|
41
|
+
showLegend = series.length > 1,
|
|
42
|
+
showTable = true,
|
|
43
|
+
...rest
|
|
44
|
+
} = Astro.props
|
|
45
|
+
const geometry = createLumenBarGeometry(series, { layout, orientation })
|
|
46
|
+
const hasData = hasLumenChartData(series)
|
|
47
|
+
const ticks = getLumenChartTicks(geometry.domain)
|
|
48
|
+
const categories = geometry.categories.map(category => category.label)
|
|
49
|
+
const valueFor = (item: LumenChartSeries, category: number | string) =>
|
|
50
|
+
item.data.find(datum => datum.x === category)?.y ?? null
|
|
51
|
+
const labelFor = (item: LumenChartSeries, category: number | string) =>
|
|
52
|
+
item.data.find(datum => datum.x === category)?.label
|
|
53
|
+
const margin = orientation === 'horizontal'
|
|
54
|
+
? { bottom: 24, left: 112, right: 20, top: 16 }
|
|
55
|
+
: { bottom: 52, left: 52, right: 16, top: 16 }
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
<Chart
|
|
59
|
+
class:list={['ui-bar-chart', classProp, className]}
|
|
60
|
+
{caption}
|
|
61
|
+
{description}
|
|
62
|
+
{glass}
|
|
63
|
+
{heading}
|
|
64
|
+
{...rest}
|
|
65
|
+
>
|
|
66
|
+
{
|
|
67
|
+
showLegend && hasData && (
|
|
68
|
+
<ul class="ui-chart__legend" aria-label="Chart legend">
|
|
69
|
+
{series.map((item, index) => (
|
|
70
|
+
<li class={`ui-chart-tone--${resolveLumenChartTone(item.tone, index)}`}>
|
|
71
|
+
<span aria-hidden="true" />
|
|
72
|
+
{item.label}
|
|
73
|
+
</li>
|
|
74
|
+
))}
|
|
75
|
+
</ul>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
{!hasData && <p class="ui-chart__empty" role="status">No chart data available.</p>}
|
|
79
|
+
<div class="ui-chart__plot" hidden={!hasData}>
|
|
80
|
+
<svg
|
|
81
|
+
aria-hidden="true"
|
|
82
|
+
preserveAspectRatio="xMidYMid meet"
|
|
83
|
+
viewBox={`0 0 ${geometry.width} ${geometry.height}`}
|
|
84
|
+
>
|
|
85
|
+
<g class="ui-chart__grid">
|
|
86
|
+
{
|
|
87
|
+
ticks.map((tick) => {
|
|
88
|
+
const coordinate =
|
|
89
|
+
orientation === 'horizontal'
|
|
90
|
+
? scaleLumenChartValue(
|
|
91
|
+
tick,
|
|
92
|
+
geometry.domain,
|
|
93
|
+
margin.left,
|
|
94
|
+
geometry.width - margin.right
|
|
95
|
+
)
|
|
96
|
+
: scaleLumenChartValue(
|
|
97
|
+
tick,
|
|
98
|
+
geometry.domain,
|
|
99
|
+
geometry.height - margin.bottom,
|
|
100
|
+
margin.top
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return orientation === 'horizontal' ? (
|
|
104
|
+
<line
|
|
105
|
+
x1={coordinate}
|
|
106
|
+
x2={coordinate}
|
|
107
|
+
y1={margin.top}
|
|
108
|
+
y2={geometry.height - margin.bottom}
|
|
109
|
+
/>
|
|
110
|
+
) : (
|
|
111
|
+
<line
|
|
112
|
+
x1={margin.left}
|
|
113
|
+
x2={geometry.width - margin.right}
|
|
114
|
+
y1={coordinate}
|
|
115
|
+
y2={coordinate}
|
|
116
|
+
/>
|
|
117
|
+
)
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
</g>
|
|
121
|
+
<g class="ui-chart__axis-labels">
|
|
122
|
+
{
|
|
123
|
+
geometry.categories.map((category) => (
|
|
124
|
+
<text
|
|
125
|
+
dominant-baseline={orientation === 'horizontal' ? 'middle' : undefined}
|
|
126
|
+
text-anchor={orientation === 'horizontal' ? 'end' : 'middle'}
|
|
127
|
+
x={category.x}
|
|
128
|
+
y={category.y}
|
|
129
|
+
>
|
|
130
|
+
{String(category.label)}
|
|
131
|
+
</text>
|
|
132
|
+
))
|
|
133
|
+
}
|
|
134
|
+
</g>
|
|
135
|
+
<g class="ui-bar-chart__marks">
|
|
136
|
+
{
|
|
137
|
+
geometry.marks.map((mark) => (
|
|
138
|
+
<rect
|
|
139
|
+
class={`ui-chart-tone--${mark.tone}`}
|
|
140
|
+
height={mark.height}
|
|
141
|
+
rx="4"
|
|
142
|
+
width={mark.width}
|
|
143
|
+
x={mark.x}
|
|
144
|
+
y={mark.y}
|
|
145
|
+
>
|
|
146
|
+
<title>
|
|
147
|
+
{`${String(mark.category)} · ${mark.seriesLabel}: ${mark.value}`}
|
|
148
|
+
</title>
|
|
149
|
+
</rect>
|
|
150
|
+
))
|
|
151
|
+
}
|
|
152
|
+
</g>
|
|
153
|
+
</svg>
|
|
154
|
+
</div>
|
|
155
|
+
{
|
|
156
|
+
showTable && hasData && (
|
|
157
|
+
<details class="ui-chart__data">
|
|
158
|
+
<summary>View chart data</summary>
|
|
159
|
+
<div>
|
|
160
|
+
<table>
|
|
161
|
+
<thead>
|
|
162
|
+
<tr>
|
|
163
|
+
<th scope="col">Category</th>
|
|
164
|
+
{series.map(item => <th scope="col">{item.label}</th>)}
|
|
165
|
+
</tr>
|
|
166
|
+
</thead>
|
|
167
|
+
<tbody>
|
|
168
|
+
{categories.map(category => (
|
|
169
|
+
<tr>
|
|
170
|
+
<th scope="row">{String(category)}</th>
|
|
171
|
+
{series.map(item => (
|
|
172
|
+
<td>
|
|
173
|
+
{labelFor(item, category) ??
|
|
174
|
+
String(valueFor(item, category) ?? 'Not available')}
|
|
175
|
+
</td>
|
|
176
|
+
))}
|
|
177
|
+
</tr>
|
|
178
|
+
))}
|
|
179
|
+
</tbody>
|
|
180
|
+
</table>
|
|
181
|
+
</div>
|
|
182
|
+
</details>
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
</Chart>
|
package/components/Chart.astro
CHANGED
|
@@ -4,15 +4,23 @@ import { resolveLumenAstroProps } from '@santi020k/lumen-core'
|
|
|
4
4
|
import type { HTMLAttributes } from 'astro/types'
|
|
5
5
|
|
|
6
6
|
interface Props extends HTMLAttributes<'figure'> {
|
|
7
|
+
caption?: string
|
|
7
8
|
class?: string | null
|
|
8
9
|
className?: string
|
|
10
|
+
description?: string
|
|
9
11
|
glass?: boolean | 'subtle' | 'strong'
|
|
12
|
+
heading?: string
|
|
13
|
+
value?: string
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
const {
|
|
17
|
+
caption,
|
|
13
18
|
class: classProp = '',
|
|
14
19
|
className = '',
|
|
20
|
+
description,
|
|
15
21
|
glass = false,
|
|
22
|
+
heading,
|
|
23
|
+
value,
|
|
16
24
|
...rest
|
|
17
25
|
} = Astro.props
|
|
18
26
|
|
|
@@ -28,5 +36,20 @@ const { classList, passthrough } = resolveLumenAstroProps(rest, [
|
|
|
28
36
|
class:list={classList}
|
|
29
37
|
{...passthrough}
|
|
30
38
|
>
|
|
39
|
+
{
|
|
40
|
+
(heading || description || value || Astro.slots.has('header')) && (
|
|
41
|
+
<header>
|
|
42
|
+
<slot name="header">
|
|
43
|
+
<div class="ui-chart__heading">
|
|
44
|
+
{heading && <h3>{heading}</h3>}
|
|
45
|
+
{description && <p>{description}</p>}
|
|
46
|
+
</div>
|
|
47
|
+
{value && <strong data-ui-chart-value>{value}</strong>}
|
|
48
|
+
</slot>
|
|
49
|
+
<slot name="actions" />
|
|
50
|
+
</header>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
31
53
|
<slot />
|
|
54
|
+
{caption && <figcaption>{caption}</figcaption>}
|
|
32
55
|
</figure>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { resolveLumenAstroProps } from '@santi020k/lumen-core'
|
|
3
|
+
|
|
4
|
+
import type { HTMLAttributes } from 'astro/types'
|
|
5
|
+
|
|
6
|
+
import Code from './Code.astro'
|
|
7
|
+
import Tabs from './Tabs.astro'
|
|
8
|
+
|
|
9
|
+
type CodeTheme = 'auto' | 'lumen' | 'santi020k'
|
|
10
|
+
|
|
11
|
+
export interface CodeTabItem {
|
|
12
|
+
code: string
|
|
13
|
+
label: string
|
|
14
|
+
language?: string
|
|
15
|
+
value: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface Props extends HTMLAttributes<'div'> {
|
|
19
|
+
ariaLabel?: string
|
|
20
|
+
class?: string | null
|
|
21
|
+
className?: string
|
|
22
|
+
copy?: boolean
|
|
23
|
+
initialValue?: string
|
|
24
|
+
items: readonly CodeTabItem[]
|
|
25
|
+
storageKey?: string
|
|
26
|
+
theme?: CodeTheme
|
|
27
|
+
wrap?: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const {
|
|
31
|
+
ariaLabel = 'Code examples',
|
|
32
|
+
class: classProp = '',
|
|
33
|
+
className = '',
|
|
34
|
+
copy = true,
|
|
35
|
+
initialValue,
|
|
36
|
+
items,
|
|
37
|
+
storageKey,
|
|
38
|
+
theme = 'auto',
|
|
39
|
+
wrap = true,
|
|
40
|
+
...rest
|
|
41
|
+
} = Astro.props
|
|
42
|
+
|
|
43
|
+
const selectedValue = initialValue ?? items[0]?.value
|
|
44
|
+
const tabsStateProps = {
|
|
45
|
+
...(selectedValue === undefined ? {} : { initialValue: selectedValue }),
|
|
46
|
+
...(storageKey === undefined ? {} : { storageKey })
|
|
47
|
+
}
|
|
48
|
+
const { classList, passthrough } = resolveLumenAstroProps(
|
|
49
|
+
rest,
|
|
50
|
+
['ui-code-tabs'],
|
|
51
|
+
classProp ?? '',
|
|
52
|
+
className
|
|
53
|
+
)
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
<Tabs
|
|
57
|
+
class:list={classList}
|
|
58
|
+
{...tabsStateProps}
|
|
59
|
+
{...passthrough}
|
|
60
|
+
>
|
|
61
|
+
<div class="ui-code-tabs__list" role="tablist" aria-label={ariaLabel}>
|
|
62
|
+
{items.map(item => (
|
|
63
|
+
<button
|
|
64
|
+
aria-selected={item.value === selectedValue ? 'true' : 'false'}
|
|
65
|
+
class="ui-code-tabs__tab"
|
|
66
|
+
data-value={item.value}
|
|
67
|
+
role="tab"
|
|
68
|
+
type="button"
|
|
69
|
+
>
|
|
70
|
+
{item.label}
|
|
71
|
+
</button>
|
|
72
|
+
))}
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
{items.map(item => (
|
|
76
|
+
<div
|
|
77
|
+
class="ui-code-tabs__panel"
|
|
78
|
+
data-value={item.value}
|
|
79
|
+
hidden={item.value !== selectedValue}
|
|
80
|
+
role="tabpanel"
|
|
81
|
+
>
|
|
82
|
+
<Code
|
|
83
|
+
code={item.code}
|
|
84
|
+
copy={copy}
|
|
85
|
+
{...(item.language === undefined ? {} : { language: item.language })}
|
|
86
|
+
theme={theme}
|
|
87
|
+
variant="block"
|
|
88
|
+
wrap={wrap}
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
))}
|
|
92
|
+
</Tabs>
|
package/components/Dialog.astro
CHANGED
|
@@ -7,6 +7,7 @@ interface Props extends HTMLAttributes<'dialog'> {
|
|
|
7
7
|
class?: string | null
|
|
8
8
|
className?: string
|
|
9
9
|
glass?: boolean | 'subtle' | 'strong'
|
|
10
|
+
layout?: 'centered' | 'fullscreen'
|
|
10
11
|
/**
|
|
11
12
|
* @deprecated Use `glass` instead. `surface="glass"` maps to `glass={true}`.
|
|
12
13
|
*/
|
|
@@ -17,6 +18,7 @@ const {
|
|
|
17
18
|
class: classProp = '',
|
|
18
19
|
className = '',
|
|
19
20
|
glass = false,
|
|
21
|
+
layout = 'centered',
|
|
20
22
|
surface = 'default',
|
|
21
23
|
...rest
|
|
22
24
|
} = Astro.props
|
|
@@ -24,6 +26,7 @@ const {
|
|
|
24
26
|
const resolvedGlass = resolveLumenGlass(glass, surface)
|
|
25
27
|
const { classList, passthrough } = resolveLumenAstroProps(rest, [
|
|
26
28
|
'ui-dialog',
|
|
29
|
+
layout === 'fullscreen' && 'ui-dialog--fullscreen',
|
|
27
30
|
resolvedGlass && 'ui-dialog--glass',
|
|
28
31
|
glass === 'subtle' && 'ui-glass-subtle',
|
|
29
32
|
glass === 'strong' && 'ui-glass-strong'
|
|
@@ -32,6 +35,7 @@ const { classList, passthrough } = resolveLumenAstroProps(rest, [
|
|
|
32
35
|
|
|
33
36
|
<dialog
|
|
34
37
|
data-surface={resolvedGlass ? 'glass' : 'default'}
|
|
38
|
+
data-layout={layout}
|
|
35
39
|
data-ui-dialog
|
|
36
40
|
class:list={classList}
|
|
37
41
|
{...passthrough}
|
package/components/Icon.astro
CHANGED
|
@@ -32,7 +32,7 @@ const isDecorative = decorative ?? !label
|
|
|
32
32
|
const iconSvg = name ? renderLumenIconSvg(name) : ''
|
|
33
33
|
|
|
34
34
|
if (name && !iconSvg && import.meta.env.DEV) {
|
|
35
|
-
console.warn(`[Lumen Icon] Unknown
|
|
35
|
+
console.warn(`[Lumen Icon] Unknown icon "${name}". Register an icon pack or use the default slot for project-specific SVGs.`)
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
const { classList, passthrough } = resolveLumenAstroProps(rest, [
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
---
|
|
2
|
+
import {
|
|
3
|
+
alignLumenChartSeries,
|
|
4
|
+
createLumenLineGeometry,
|
|
5
|
+
getLumenChartCategories,
|
|
6
|
+
getLumenChartDomain,
|
|
7
|
+
getLumenChartTicks,
|
|
8
|
+
hasLumenChartData,
|
|
9
|
+
type LumenChartSeries,
|
|
10
|
+
resolveLumenChartTone,
|
|
11
|
+
scaleLumenChartValue
|
|
12
|
+
} from '@santi020k/lumen-core'
|
|
13
|
+
|
|
14
|
+
import type { HTMLAttributes } from 'astro/types'
|
|
15
|
+
|
|
16
|
+
import Chart from './Chart.astro'
|
|
17
|
+
|
|
18
|
+
interface Props extends HTMLAttributes<'figure'> {
|
|
19
|
+
area?: boolean
|
|
20
|
+
caption?: string
|
|
21
|
+
class?: string | null
|
|
22
|
+
className?: string
|
|
23
|
+
description?: string
|
|
24
|
+
glass?: boolean | 'subtle' | 'strong'
|
|
25
|
+
heading?: string
|
|
26
|
+
markers?: boolean
|
|
27
|
+
referenceValue?: number
|
|
28
|
+
series: readonly LumenChartSeries[]
|
|
29
|
+
showLegend?: boolean
|
|
30
|
+
showTable?: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const {
|
|
34
|
+
area = false,
|
|
35
|
+
caption,
|
|
36
|
+
class: classProp = '',
|
|
37
|
+
className = '',
|
|
38
|
+
description,
|
|
39
|
+
glass = false,
|
|
40
|
+
heading,
|
|
41
|
+
markers = true,
|
|
42
|
+
referenceValue,
|
|
43
|
+
series,
|
|
44
|
+
showLegend = series.length > 1,
|
|
45
|
+
showTable = true,
|
|
46
|
+
...rest
|
|
47
|
+
} = Astro.props
|
|
48
|
+
const width = 640
|
|
49
|
+
const height = 320
|
|
50
|
+
const padding = 44
|
|
51
|
+
const categories = getLumenChartCategories(series)
|
|
52
|
+
const hasData = hasLumenChartData(series)
|
|
53
|
+
const alignedSeries = series.map(item => alignLumenChartSeries(item, categories))
|
|
54
|
+
const domain = getLumenChartDomain([
|
|
55
|
+
...alignedSeries.flatMap(item => item.data.map(datum => datum.y)),
|
|
56
|
+
referenceValue ?? null
|
|
57
|
+
], false)
|
|
58
|
+
const geometries = alignedSeries.map(item =>
|
|
59
|
+
createLumenLineGeometry(item.data, {
|
|
60
|
+
domain,
|
|
61
|
+
height,
|
|
62
|
+
includeZero: false,
|
|
63
|
+
padding,
|
|
64
|
+
width
|
|
65
|
+
})
|
|
66
|
+
)
|
|
67
|
+
const ticks = getLumenChartTicks(domain)
|
|
68
|
+
const labelStep = Math.max(1, Math.ceil(categories.length / 8))
|
|
69
|
+
const valueFor = (item: LumenChartSeries, category: number | string) =>
|
|
70
|
+
item.data.find(datum => datum.x === category)
|
|
71
|
+
const referenceY =
|
|
72
|
+
referenceValue === undefined
|
|
73
|
+
? undefined
|
|
74
|
+
: scaleLumenChartValue(referenceValue, domain, height - padding, padding)
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
<Chart
|
|
78
|
+
class:list={['ui-line-chart', classProp, className]}
|
|
79
|
+
{caption}
|
|
80
|
+
{description}
|
|
81
|
+
{glass}
|
|
82
|
+
{heading}
|
|
83
|
+
{...rest}
|
|
84
|
+
>
|
|
85
|
+
{
|
|
86
|
+
showLegend && hasData && (
|
|
87
|
+
<ul class="ui-chart__legend" aria-label="Chart legend">
|
|
88
|
+
{series.map((item, index) => (
|
|
89
|
+
<li class={`ui-chart-tone--${resolveLumenChartTone(item.tone, index)}`}>
|
|
90
|
+
<span aria-hidden="true" />
|
|
91
|
+
{item.label}
|
|
92
|
+
</li>
|
|
93
|
+
))}
|
|
94
|
+
</ul>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
{!hasData && <p class="ui-chart__empty" role="status">No chart data available.</p>}
|
|
98
|
+
<div class="ui-chart__plot" hidden={!hasData}>
|
|
99
|
+
<svg
|
|
100
|
+
aria-hidden="true"
|
|
101
|
+
preserveAspectRatio="xMidYMid meet"
|
|
102
|
+
viewBox={`0 0 ${width} ${height}`}
|
|
103
|
+
>
|
|
104
|
+
<g class="ui-chart__grid">
|
|
105
|
+
{
|
|
106
|
+
ticks.map((tick) => {
|
|
107
|
+
const y = scaleLumenChartValue(
|
|
108
|
+
tick,
|
|
109
|
+
domain,
|
|
110
|
+
height - padding,
|
|
111
|
+
padding
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<>
|
|
116
|
+
<line x1={padding} x2={width - padding} y1={y} y2={y} />
|
|
117
|
+
<text x={padding - 8} y={y}>
|
|
118
|
+
{Number(tick.toPrecision(4))}
|
|
119
|
+
</text>
|
|
120
|
+
</>
|
|
121
|
+
)
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
</g>
|
|
125
|
+
<g class="ui-chart__axis-labels">
|
|
126
|
+
{
|
|
127
|
+
categories.map((category, index) => {
|
|
128
|
+
if (
|
|
129
|
+
index % labelStep !== 0 &&
|
|
130
|
+
index !== categories.length - 1
|
|
131
|
+
)
|
|
132
|
+
return null
|
|
133
|
+
|
|
134
|
+
const denominator = Math.max(1, categories.length - 1)
|
|
135
|
+
const x =
|
|
136
|
+
padding + (index / denominator) * (width - padding * 2)
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<text text-anchor="middle" x={x} y={height - 14}>
|
|
140
|
+
{String(category)}
|
|
141
|
+
</text>
|
|
142
|
+
)
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
</g>
|
|
146
|
+
{
|
|
147
|
+
referenceY !== undefined && (
|
|
148
|
+
<line
|
|
149
|
+
class="ui-chart__reference"
|
|
150
|
+
x1={padding}
|
|
151
|
+
x2={width - padding}
|
|
152
|
+
y1={referenceY}
|
|
153
|
+
y2={referenceY}
|
|
154
|
+
/>
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
{
|
|
158
|
+
geometries.map((geometry, index) => {
|
|
159
|
+
const item = series[index]
|
|
160
|
+
|
|
161
|
+
if (!item) return null
|
|
162
|
+
|
|
163
|
+
const tone = resolveLumenChartTone(item.tone, index)
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<g class={`ui-line-chart__series ui-chart-tone--${tone}`}>
|
|
167
|
+
{area &&
|
|
168
|
+
geometry.areaPaths.map(path => (
|
|
169
|
+
<path class="ui-line-chart__area" d={path} />
|
|
170
|
+
))}
|
|
171
|
+
<path class="ui-line-chart__line" d={geometry.path} />
|
|
172
|
+
{markers &&
|
|
173
|
+
geometry.points.map(point => (
|
|
174
|
+
<circle
|
|
175
|
+
class="ui-line-chart__point"
|
|
176
|
+
cx={point.xCoordinate}
|
|
177
|
+
cy={point.yCoordinate}
|
|
178
|
+
r="3"
|
|
179
|
+
>
|
|
180
|
+
<title>
|
|
181
|
+
{`${point.label ?? String(point.x)} · ${item.label}: ${point.y}`}
|
|
182
|
+
</title>
|
|
183
|
+
</circle>
|
|
184
|
+
))}
|
|
185
|
+
</g>
|
|
186
|
+
)
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
</svg>
|
|
190
|
+
</div>
|
|
191
|
+
{
|
|
192
|
+
showTable && hasData && (
|
|
193
|
+
<details class="ui-chart__data">
|
|
194
|
+
<summary>View chart data</summary>
|
|
195
|
+
<div>
|
|
196
|
+
<table>
|
|
197
|
+
<thead>
|
|
198
|
+
<tr>
|
|
199
|
+
<th scope="col">Category</th>
|
|
200
|
+
{series.map(item => <th scope="col">{item.label}</th>)}
|
|
201
|
+
</tr>
|
|
202
|
+
</thead>
|
|
203
|
+
<tbody>
|
|
204
|
+
{categories.map(category => (
|
|
205
|
+
<tr>
|
|
206
|
+
<th scope="row">{String(category)}</th>
|
|
207
|
+
{series.map(item => {
|
|
208
|
+
const datum = valueFor(item, category)
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<td>
|
|
212
|
+
{datum?.label ??
|
|
213
|
+
(datum?.y === null || datum === undefined
|
|
214
|
+
? 'Not available'
|
|
215
|
+
: String(datum.y))}
|
|
216
|
+
</td>
|
|
217
|
+
)
|
|
218
|
+
})}
|
|
219
|
+
</tr>
|
|
220
|
+
))}
|
|
221
|
+
</tbody>
|
|
222
|
+
</table>
|
|
223
|
+
</div>
|
|
224
|
+
</details>
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
</Chart>
|
|
@@ -29,3 +29,49 @@ const { classList, passthrough } = resolveLumenAstroProps(rest, [
|
|
|
29
29
|
>
|
|
30
30
|
<slot />
|
|
31
31
|
</div>
|
|
32
|
+
|
|
33
|
+
<script>
|
|
34
|
+
type ParticleDensity = 'low' | 'medium' | 'high'
|
|
35
|
+
|
|
36
|
+
const densityConfig: Record<ParticleDensity, number> = {
|
|
37
|
+
low: 15,
|
|
38
|
+
medium: 25,
|
|
39
|
+
high: 40
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const particleTones = [
|
|
43
|
+
'var(--brand)',
|
|
44
|
+
'var(--accent)',
|
|
45
|
+
'var(--glow, var(--brand))'
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
const initParticles = (scope: ParentNode = document) => {
|
|
49
|
+
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
|
|
50
|
+
|
|
51
|
+
for (const container of scope.querySelectorAll<HTMLElement>('[data-ui-particles]')) {
|
|
52
|
+
if (container.hasAttribute('data-ui-particles-initialized')) continue
|
|
53
|
+
|
|
54
|
+
container.setAttribute('data-ui-particles-initialized', '')
|
|
55
|
+
|
|
56
|
+
const density = (container.dataset.uiParticles || 'medium') as ParticleDensity
|
|
57
|
+
const count = densityConfig[density]
|
|
58
|
+
|
|
59
|
+
for (let index = 0; index < count; index++) {
|
|
60
|
+
const particle = document.createElement('span')
|
|
61
|
+
const size = Math.random() * 12 + 8
|
|
62
|
+
|
|
63
|
+
particle.className = 'ui-particles__particle'
|
|
64
|
+
particle.style.setProperty('--ui-particle-delay', `${Math.random() * 8}s`)
|
|
65
|
+
particle.style.setProperty('--ui-particle-duration', `${20 + Math.random() * 15}s`)
|
|
66
|
+
particle.style.setProperty('--ui-particle-left', `${Math.random() * 100}%`)
|
|
67
|
+
particle.style.setProperty('--ui-particle-size', `${size}px`)
|
|
68
|
+
particle.style.setProperty('--ui-particle-tone', particleTones[Math.floor(Math.random() * particleTones.length)])
|
|
69
|
+
particle.style.setProperty('--ui-particle-top', `${Math.random() * 100}%`)
|
|
70
|
+
container.appendChild(particle)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
initParticles()
|
|
76
|
+
document.addEventListener('astro:after-swap', () => initParticles())
|
|
77
|
+
</script>
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
---
|
|
2
|
+
import {
|
|
3
|
+
createLumenPieGeometry,
|
|
4
|
+
hasLumenPieData,
|
|
5
|
+
type LumenChartSeries,
|
|
6
|
+
type LumenPieChartVariant
|
|
7
|
+
} from '@santi020k/lumen-core'
|
|
8
|
+
|
|
9
|
+
import type { HTMLAttributes } from 'astro/types'
|
|
10
|
+
|
|
11
|
+
import Chart from './Chart.astro'
|
|
12
|
+
|
|
13
|
+
interface Props extends HTMLAttributes<'figure'> {
|
|
14
|
+
caption?: string
|
|
15
|
+
centerLabel?: string
|
|
16
|
+
centerValue?: string
|
|
17
|
+
class?: string | null
|
|
18
|
+
className?: string
|
|
19
|
+
description?: string
|
|
20
|
+
glass?: boolean | 'subtle' | 'strong'
|
|
21
|
+
heading?: string
|
|
22
|
+
series: LumenChartSeries
|
|
23
|
+
showLegend?: boolean
|
|
24
|
+
showTable?: boolean
|
|
25
|
+
valueFormatter?: (value: number) => string
|
|
26
|
+
variant?: LumenPieChartVariant
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const {
|
|
30
|
+
caption,
|
|
31
|
+
centerLabel,
|
|
32
|
+
centerValue,
|
|
33
|
+
class: classProp = '',
|
|
34
|
+
className = '',
|
|
35
|
+
description,
|
|
36
|
+
glass = false,
|
|
37
|
+
heading,
|
|
38
|
+
series,
|
|
39
|
+
showLegend = true,
|
|
40
|
+
showTable = true,
|
|
41
|
+
valueFormatter = String,
|
|
42
|
+
variant = 'donut',
|
|
43
|
+
...rest
|
|
44
|
+
} = Astro.props
|
|
45
|
+
const geometry = createLumenPieGeometry(series.data, { variant })
|
|
46
|
+
const hasData = hasLumenPieData(series.data)
|
|
47
|
+
const percentageFor = (percentage: number) =>
|
|
48
|
+
new Intl.NumberFormat(undefined, {
|
|
49
|
+
maximumFractionDigits: percentage < 0.01 ? 1 : 0,
|
|
50
|
+
style: 'percent'
|
|
51
|
+
}).format(percentage)
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
<Chart
|
|
55
|
+
class:list={['ui-pie-chart', `ui-pie-chart--${variant}`, classProp, className]}
|
|
56
|
+
{caption}
|
|
57
|
+
{description}
|
|
58
|
+
{glass}
|
|
59
|
+
{heading}
|
|
60
|
+
{...rest}
|
|
61
|
+
>
|
|
62
|
+
{
|
|
63
|
+
showLegend && hasData && (
|
|
64
|
+
<ul class="ui-chart__legend" aria-label="Chart legend">
|
|
65
|
+
{geometry.slices.map(slice => (
|
|
66
|
+
<li class={`ui-chart-tone--${slice.tone}`}>
|
|
67
|
+
<span aria-hidden="true" />
|
|
68
|
+
{slice.label}
|
|
69
|
+
</li>
|
|
70
|
+
))}
|
|
71
|
+
</ul>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
{!hasData && <p class="ui-chart__empty" role="status">No chart data available.</p>}
|
|
75
|
+
<div class="ui-chart__plot ui-pie-chart__plot" hidden={!hasData}>
|
|
76
|
+
<svg
|
|
77
|
+
aria-hidden="true"
|
|
78
|
+
preserveAspectRatio="xMidYMid meet"
|
|
79
|
+
viewBox={`0 0 ${geometry.size} ${geometry.size}`}
|
|
80
|
+
>
|
|
81
|
+
<g class="ui-pie-chart__slices">
|
|
82
|
+
{
|
|
83
|
+
geometry.slices.map(slice => (
|
|
84
|
+
<path
|
|
85
|
+
class={`ui-chart-tone--${slice.tone}`}
|
|
86
|
+
d={slice.path}
|
|
87
|
+
fill-rule="evenodd"
|
|
88
|
+
>
|
|
89
|
+
<title>
|
|
90
|
+
{`${slice.label}: ${valueFormatter(slice.value)} (${percentageFor(slice.percentage)})`}
|
|
91
|
+
</title>
|
|
92
|
+
</path>
|
|
93
|
+
))
|
|
94
|
+
}
|
|
95
|
+
</g>
|
|
96
|
+
</svg>
|
|
97
|
+
{
|
|
98
|
+
variant === 'donut' && (centerLabel != null || centerValue != null) && (
|
|
99
|
+
<>
|
|
100
|
+
<div class="ui-pie-chart__center" aria-hidden="true">
|
|
101
|
+
{centerValue != null && <strong>{centerValue}</strong>}
|
|
102
|
+
{centerLabel != null && <span>{centerLabel}</span>}
|
|
103
|
+
</div>
|
|
104
|
+
<div class="ui-sr-only">
|
|
105
|
+
{centerValue} {centerLabel}
|
|
106
|
+
</div>
|
|
107
|
+
</>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
</div>
|
|
111
|
+
{
|
|
112
|
+
showTable && hasData && (
|
|
113
|
+
<details class="ui-chart__data">
|
|
114
|
+
<summary>View chart data</summary>
|
|
115
|
+
<div>
|
|
116
|
+
<table>
|
|
117
|
+
<thead>
|
|
118
|
+
<tr>
|
|
119
|
+
<th scope="col">Category</th>
|
|
120
|
+
<th scope="col">Value</th>
|
|
121
|
+
<th scope="col">Share</th>
|
|
122
|
+
</tr>
|
|
123
|
+
</thead>
|
|
124
|
+
<tbody>
|
|
125
|
+
{geometry.slices.map(slice => (
|
|
126
|
+
<tr>
|
|
127
|
+
<th scope="row">{slice.label}</th>
|
|
128
|
+
<td>{valueFormatter(slice.value)}</td>
|
|
129
|
+
<td>{percentageFor(slice.percentage)}</td>
|
|
130
|
+
</tr>
|
|
131
|
+
))}
|
|
132
|
+
</tbody>
|
|
133
|
+
</table>
|
|
134
|
+
</div>
|
|
135
|
+
</details>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
</Chart>
|
package/components/Pill.astro
CHANGED
|
@@ -8,6 +8,7 @@ interface Props extends HTMLAttributes<'span'> {
|
|
|
8
8
|
className?: string
|
|
9
9
|
count?: number | string
|
|
10
10
|
href?: string
|
|
11
|
+
variant?: 'brand' | 'neutral' | 'outline'
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
const {
|
|
@@ -15,20 +16,22 @@ const {
|
|
|
15
16
|
className = '',
|
|
16
17
|
count,
|
|
17
18
|
href,
|
|
19
|
+
variant = 'neutral',
|
|
18
20
|
...rest
|
|
19
21
|
} = Astro.props
|
|
20
22
|
|
|
21
23
|
const { classList, passthrough } = resolveLumenAstroProps(rest, [
|
|
22
|
-
'ui-pill'
|
|
24
|
+
'ui-pill',
|
|
25
|
+
variant !== 'neutral' && `ui-pill--${variant}`
|
|
23
26
|
], classProp, className)
|
|
24
27
|
---
|
|
25
28
|
|
|
26
29
|
{href
|
|
27
|
-
? <a class:list={classList} href={href} {...passthrough}>
|
|
30
|
+
? <a class:list={classList} data-variant={variant} href={href} {...passthrough}>
|
|
28
31
|
<slot />
|
|
29
32
|
{count != null && <span class="ui-pill__count">{count}</span>}
|
|
30
33
|
</a>
|
|
31
|
-
: <span class:list={classList} {...passthrough}>
|
|
34
|
+
: <span class:list={classList} data-variant={variant} {...passthrough}>
|
|
32
35
|
<slot />
|
|
33
36
|
{count != null && <span class="ui-pill__count">{count}</span>}
|
|
34
37
|
</span>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { resolveLumenAstroProps } from '@santi020k/lumen-core'
|
|
3
|
+
|
|
4
|
+
import type { HTMLAttributes } from 'astro/types'
|
|
5
|
+
|
|
6
|
+
interface Props extends HTMLAttributes<'div'> {
|
|
7
|
+
class?: string | null
|
|
8
|
+
className?: string
|
|
9
|
+
position?: 'bottom' | 'top'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const {
|
|
13
|
+
'aria-label': ariaLabel = 'Reading progress',
|
|
14
|
+
class: classProp = '',
|
|
15
|
+
className = '',
|
|
16
|
+
position = 'top',
|
|
17
|
+
...rest
|
|
18
|
+
} = Astro.props
|
|
19
|
+
|
|
20
|
+
const { classList, passthrough } = resolveLumenAstroProps(rest, [
|
|
21
|
+
'ui-scroll-progress',
|
|
22
|
+
position === 'bottom' && 'ui-scroll-progress--bottom'
|
|
23
|
+
], classProp, className)
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
<div
|
|
27
|
+
aria-label={ariaLabel}
|
|
28
|
+
aria-valuemax="100"
|
|
29
|
+
aria-valuemin="0"
|
|
30
|
+
aria-valuenow="0"
|
|
31
|
+
class:list={classList}
|
|
32
|
+
data-position={position}
|
|
33
|
+
data-ui-scroll-progress
|
|
34
|
+
role="progressbar"
|
|
35
|
+
{...passthrough}
|
|
36
|
+
>
|
|
37
|
+
<span class="ui-scroll-progress__bar"></span>
|
|
38
|
+
</div>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
import {
|
|
3
|
+
composeClassName,
|
|
4
|
+
createLumenLineGeometry,
|
|
5
|
+
type LumenChartTone,
|
|
6
|
+
resolveLumenChartTone
|
|
7
|
+
} from '@santi020k/lumen-core'
|
|
8
|
+
|
|
9
|
+
import type { HTMLAttributes } from 'astro/types'
|
|
10
|
+
|
|
11
|
+
interface Props extends HTMLAttributes<'span'> {
|
|
12
|
+
area?: boolean
|
|
13
|
+
class?: string | null
|
|
14
|
+
className?: string
|
|
15
|
+
label: string
|
|
16
|
+
showEndpoint?: boolean
|
|
17
|
+
tone?: LumenChartTone
|
|
18
|
+
values: readonly number[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const {
|
|
22
|
+
area = false,
|
|
23
|
+
class: classProp = '',
|
|
24
|
+
className = '',
|
|
25
|
+
label,
|
|
26
|
+
showEndpoint = true,
|
|
27
|
+
tone,
|
|
28
|
+
values,
|
|
29
|
+
...rest
|
|
30
|
+
} = Astro.props
|
|
31
|
+
const data = values.map((value, index) => ({ x: index, y: value }))
|
|
32
|
+
const geometry = createLumenLineGeometry(data, {
|
|
33
|
+
height: 40,
|
|
34
|
+
padding: 3,
|
|
35
|
+
width: 120
|
|
36
|
+
})
|
|
37
|
+
const resolvedTone = resolveLumenChartTone(tone)
|
|
38
|
+
const endpoint = geometry.points[geometry.points.length - 1]
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
<span
|
|
42
|
+
aria-label={label}
|
|
43
|
+
class={composeClassName(
|
|
44
|
+
'ui-sparkline',
|
|
45
|
+
`ui-chart-tone--${resolvedTone}`,
|
|
46
|
+
classProp,
|
|
47
|
+
className
|
|
48
|
+
)}
|
|
49
|
+
role="img"
|
|
50
|
+
{...rest}
|
|
51
|
+
>
|
|
52
|
+
<svg aria-hidden="true" preserveAspectRatio="none" viewBox="0 0 120 40">
|
|
53
|
+
{
|
|
54
|
+
area &&
|
|
55
|
+
geometry.areaPaths.map((path) => (
|
|
56
|
+
<path class="ui-sparkline__area" d={path} />
|
|
57
|
+
))
|
|
58
|
+
}
|
|
59
|
+
<path class="ui-sparkline__line" d={geometry.path} />
|
|
60
|
+
{
|
|
61
|
+
showEndpoint && endpoint && (
|
|
62
|
+
<circle
|
|
63
|
+
class="ui-sparkline__endpoint"
|
|
64
|
+
cx={endpoint.xCoordinate}
|
|
65
|
+
cy={endpoint.yCoordinate}
|
|
66
|
+
r="2.5"
|
|
67
|
+
/>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
</svg>
|
|
71
|
+
<span class="ui-sr-only">{label}</span>
|
|
72
|
+
</span>
|
package/components/Stat.astro
CHANGED
|
@@ -4,31 +4,36 @@ import { resolveLumenAstroProps } from '@santi020k/lumen-core'
|
|
|
4
4
|
import type { HTMLAttributes } from 'astro/types'
|
|
5
5
|
|
|
6
6
|
interface Props extends HTMLAttributes<'div'> {
|
|
7
|
+
as?: 'article' | 'div' | 'section'
|
|
7
8
|
class?: string | null
|
|
8
9
|
className?: string
|
|
9
10
|
label?: string
|
|
10
11
|
value?: string
|
|
12
|
+
variant?: 'accent' | 'default' | 'glass'
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
const {
|
|
16
|
+
as: Tag = 'div',
|
|
14
17
|
class: classProp = '',
|
|
15
18
|
className = '',
|
|
16
19
|
label,
|
|
17
20
|
value,
|
|
21
|
+
variant = 'default',
|
|
18
22
|
...rest
|
|
19
23
|
} = Astro.props
|
|
20
24
|
|
|
21
25
|
const { classList, passthrough } = resolveLumenAstroProps(rest, [
|
|
22
|
-
'ui-stat'
|
|
26
|
+
'ui-stat',
|
|
27
|
+
variant !== 'default' && `ui-stat--${variant}`
|
|
23
28
|
], classProp, className)
|
|
24
29
|
---
|
|
25
30
|
|
|
26
|
-
<
|
|
31
|
+
<Tag
|
|
27
32
|
class:list={classList}
|
|
33
|
+
data-variant={variant}
|
|
28
34
|
{...passthrough}
|
|
29
35
|
>
|
|
30
36
|
{label && <div class="ui-stat-label">{label}</div>}
|
|
31
37
|
{value && <div class="ui-stat-value">{value}</div>}
|
|
32
38
|
<slot />
|
|
33
|
-
</
|
|
34
|
-
|
|
39
|
+
</Tag>
|
|
@@ -39,7 +39,7 @@ const watermarkSvg = [
|
|
|
39
39
|
`<svg xmlns="http://www.w3.org/2000/svg" width="${tileSize}" height="${tileSize}" viewBox="0 0 ${tileSize} ${tileSize}">`,
|
|
40
40
|
`<text x="${tileSize / 2}" y="${tileSize / 2}" dominant-baseline="middle" text-anchor="middle"`,
|
|
41
41
|
` transform="rotate(${rotate} ${tileSize / 2} ${tileSize / 2})"`,
|
|
42
|
-
' fill="black" font-family="
|
|
42
|
+
' fill="black" font-family="Montserrat, Avenir Next, Segoe UI, sans-serif" font-size="16" font-weight="500" letter-spacing="1.6">',
|
|
43
43
|
`${escapedWatermarkText}</text></svg>`
|
|
44
44
|
].join('')
|
|
45
45
|
const watermarkImage = `url("data:image/svg+xml,${encodeURIComponent(watermarkSvg)}")`
|
package/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export { default as Autocomplete } from './components/Autocomplete.astro'
|
|
|
13
13
|
export { default as Avatar } from './components/Avatar.astro'
|
|
14
14
|
export { default as BackToTop } from './components/BackToTop.astro'
|
|
15
15
|
export { default as Badge } from './components/Badge.astro'
|
|
16
|
+
export { default as BarChart } from './components/BarChart.astro'
|
|
16
17
|
export { default as Breadcrumb } from './components/Breadcrumb.astro'
|
|
17
18
|
export { default as Bubble } from './components/Bubble.astro'
|
|
18
19
|
export { default as Button } from './components/Button.astro'
|
|
@@ -26,6 +27,7 @@ export { default as Cascader } from './components/Cascader.astro'
|
|
|
26
27
|
export { default as Chart } from './components/Chart.astro'
|
|
27
28
|
export { default as Checkbox } from './components/Checkbox.astro'
|
|
28
29
|
export { default as Code } from './components/Code.astro'
|
|
30
|
+
export { default as CodeTabs } from './components/CodeTabs.astro'
|
|
29
31
|
export { default as Collapsible } from './components/Collapsible.astro'
|
|
30
32
|
export { default as ColorPicker } from './components/ColorPicker.astro'
|
|
31
33
|
export { default as Combobox } from './components/Combobox.astro'
|
|
@@ -57,6 +59,7 @@ export { default as Item } from './components/Item.astro'
|
|
|
57
59
|
export { default as Kbd } from './components/Kbd.astro'
|
|
58
60
|
export { default as Label } from './components/Label.astro'
|
|
59
61
|
export { default as LanguageToggle } from './components/LanguageToggle.astro'
|
|
62
|
+
export { default as LineChart } from './components/LineChart.astro'
|
|
60
63
|
export { default as Link } from './components/Link.astro'
|
|
61
64
|
export { default as Marker } from './components/Marker.astro'
|
|
62
65
|
export { default as Mentions } from './components/Mentions.astro'
|
|
@@ -71,6 +74,7 @@ export { default as NumberField } from './components/NumberField.astro'
|
|
|
71
74
|
export { default as Pagination } from './components/Pagination.astro'
|
|
72
75
|
export { default as Particles } from './components/Particles.astro'
|
|
73
76
|
export { default as PhoneInput } from './components/PhoneInput.astro'
|
|
77
|
+
export { default as PieChart } from './components/PieChart.astro'
|
|
74
78
|
export { default as Pill } from './components/Pill.astro'
|
|
75
79
|
export { default as Popconfirm } from './components/Popconfirm.astro'
|
|
76
80
|
export { default as Popover } from './components/Popover.astro'
|
|
@@ -84,6 +88,7 @@ export { default as RevealGroup } from './components/RevealGroup.astro'
|
|
|
84
88
|
export { default as RichTextEditor } from './components/RichTextEditor.astro'
|
|
85
89
|
export { default as Schedule } from './components/Schedule.astro'
|
|
86
90
|
export { default as ScrollArea } from './components/ScrollArea.astro'
|
|
91
|
+
export { default as ScrollProgress } from './components/ScrollProgress.astro'
|
|
87
92
|
export { default as ScrollReveal } from './components/ScrollReveal.astro'
|
|
88
93
|
export { default as SearchField } from './components/SearchField.astro'
|
|
89
94
|
export { default as Segmented } from './components/Segmented.astro'
|
|
@@ -95,6 +100,7 @@ export { default as Skeleton } from './components/Skeleton.astro'
|
|
|
95
100
|
export { default as SkipLink } from './components/SkipLink.astro'
|
|
96
101
|
export { default as Slider } from './components/Slider.astro'
|
|
97
102
|
export { default as Sonner } from './components/Sonner.astro'
|
|
103
|
+
export { default as Sparkline } from './components/Sparkline.astro'
|
|
98
104
|
export { default as SpeedDial } from './components/SpeedDial.astro'
|
|
99
105
|
export { default as Spinner } from './components/Spinner.astro'
|
|
100
106
|
export { default as Stat } from './components/Stat.astro'
|
package/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ export { default as Autocomplete } from './components/Autocomplete.astro'
|
|
|
13
13
|
export { default as Avatar } from './components/Avatar.astro'
|
|
14
14
|
export { default as BackToTop } from './components/BackToTop.astro'
|
|
15
15
|
export { default as Badge } from './components/Badge.astro'
|
|
16
|
+
export { default as BarChart } from './components/BarChart.astro'
|
|
16
17
|
export { default as Breadcrumb } from './components/Breadcrumb.astro'
|
|
17
18
|
export { default as Bubble } from './components/Bubble.astro'
|
|
18
19
|
export { default as Button } from './components/Button.astro'
|
|
@@ -26,6 +27,7 @@ export { default as Cascader } from './components/Cascader.astro'
|
|
|
26
27
|
export { default as Chart } from './components/Chart.astro'
|
|
27
28
|
export { default as Checkbox } from './components/Checkbox.astro'
|
|
28
29
|
export { default as Code } from './components/Code.astro'
|
|
30
|
+
export { default as CodeTabs } from './components/CodeTabs.astro'
|
|
29
31
|
export { default as Collapsible } from './components/Collapsible.astro'
|
|
30
32
|
export { default as ColorPicker } from './components/ColorPicker.astro'
|
|
31
33
|
export { default as Combobox } from './components/Combobox.astro'
|
|
@@ -57,6 +59,7 @@ export { default as Item } from './components/Item.astro'
|
|
|
57
59
|
export { default as Kbd } from './components/Kbd.astro'
|
|
58
60
|
export { default as Label } from './components/Label.astro'
|
|
59
61
|
export { default as LanguageToggle } from './components/LanguageToggle.astro'
|
|
62
|
+
export { default as LineChart } from './components/LineChart.astro'
|
|
60
63
|
export { default as Link } from './components/Link.astro'
|
|
61
64
|
export { default as Marker } from './components/Marker.astro'
|
|
62
65
|
export { default as Mentions } from './components/Mentions.astro'
|
|
@@ -71,6 +74,7 @@ export { default as NumberField } from './components/NumberField.astro'
|
|
|
71
74
|
export { default as Pagination } from './components/Pagination.astro'
|
|
72
75
|
export { default as Particles } from './components/Particles.astro'
|
|
73
76
|
export { default as PhoneInput } from './components/PhoneInput.astro'
|
|
77
|
+
export { default as PieChart } from './components/PieChart.astro'
|
|
74
78
|
export { default as Pill } from './components/Pill.astro'
|
|
75
79
|
export { default as Popconfirm } from './components/Popconfirm.astro'
|
|
76
80
|
export { default as Popover } from './components/Popover.astro'
|
|
@@ -84,6 +88,7 @@ export { default as RevealGroup } from './components/RevealGroup.astro'
|
|
|
84
88
|
export { default as RichTextEditor } from './components/RichTextEditor.astro'
|
|
85
89
|
export { default as Schedule } from './components/Schedule.astro'
|
|
86
90
|
export { default as ScrollArea } from './components/ScrollArea.astro'
|
|
91
|
+
export { default as ScrollProgress } from './components/ScrollProgress.astro'
|
|
87
92
|
export { default as ScrollReveal } from './components/ScrollReveal.astro'
|
|
88
93
|
export { default as SearchField } from './components/SearchField.astro'
|
|
89
94
|
export { default as Segmented } from './components/Segmented.astro'
|
|
@@ -95,6 +100,7 @@ export { default as Skeleton } from './components/Skeleton.astro'
|
|
|
95
100
|
export { default as SkipLink } from './components/SkipLink.astro'
|
|
96
101
|
export { default as Slider } from './components/Slider.astro'
|
|
97
102
|
export { default as Sonner } from './components/Sonner.astro'
|
|
103
|
+
export { default as Sparkline } from './components/Sparkline.astro'
|
|
98
104
|
export { default as SpeedDial } from './components/SpeedDial.astro'
|
|
99
105
|
export { default as Spinner } from './components/Spinner.astro'
|
|
100
106
|
export { default as Stat } from './components/Stat.astro'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@santi020k/lumen-astro",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"uqr": "^0.1.3",
|
|
36
|
-
"@santi020k/lumen": "^0.
|
|
37
|
-
"@santi020k/lumen-core": "^0.
|
|
36
|
+
"@santi020k/lumen": "^0.4.0",
|
|
37
|
+
"@santi020k/lumen-core": "^0.4.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@astrojs/ts-plugin": "^1.10.10",
|
|
@@ -4112,6 +4112,47 @@
|
|
|
4112
4112
|
}
|
|
4113
4113
|
}
|
|
4114
4114
|
|
|
4115
|
+
let scrollProgressFrame = 0
|
|
4116
|
+
|
|
4117
|
+
const updateScrollProgress = (): void => {
|
|
4118
|
+
scrollProgressFrame = 0
|
|
4119
|
+
|
|
4120
|
+
const scrollingElement = document.scrollingElement ?? document.documentElement
|
|
4121
|
+
const maximum = scrollingElement.scrollHeight - scrollingElement.clientHeight
|
|
4122
|
+
|
|
4123
|
+
const percentage = maximum > 0
|
|
4124
|
+
? Math.min(100, Math.max(0, (scrollingElement.scrollTop / maximum) * 100))
|
|
4125
|
+
: 0
|
|
4126
|
+
|
|
4127
|
+
for (const root of document.querySelectorAll<HTMLElement>('[data-ui-scroll-progress]')) {
|
|
4128
|
+
const bar = root.querySelector<HTMLElement>('.ui-scroll-progress__bar')
|
|
4129
|
+
|
|
4130
|
+
root.setAttribute('aria-valuenow', `${Math.round(percentage)}`)
|
|
4131
|
+
|
|
4132
|
+
if (bar) bar.style.transform = `scaleX(${percentage / 100})`
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
|
|
4136
|
+
const requestScrollProgressUpdate = (): void => {
|
|
4137
|
+
if (scrollProgressFrame) return
|
|
4138
|
+
|
|
4139
|
+
scrollProgressFrame = requestAnimationFrame(updateScrollProgress)
|
|
4140
|
+
}
|
|
4141
|
+
|
|
4142
|
+
const initScrollProgress = (scope: ParentNode): void => {
|
|
4143
|
+
if (!scope.querySelector('[data-ui-scroll-progress]')) return
|
|
4144
|
+
|
|
4145
|
+
requestScrollProgressUpdate()
|
|
4146
|
+
|
|
4147
|
+
if (document.documentElement.dataset.uiScrollProgressBound === 'true') return
|
|
4148
|
+
|
|
4149
|
+
document.documentElement.dataset.uiScrollProgressBound = 'true'
|
|
4150
|
+
|
|
4151
|
+
window.addEventListener('resize', requestScrollProgressUpdate, { passive: true })
|
|
4152
|
+
|
|
4153
|
+
window.addEventListener('scroll', requestScrollProgressUpdate, { passive: true })
|
|
4154
|
+
}
|
|
4155
|
+
|
|
4115
4156
|
const initTours = (scope: ParentNode): void => {
|
|
4116
4157
|
for (const root of scope.querySelectorAll<HTMLElement>('[data-ui-tour]')) {
|
|
4117
4158
|
if (root.dataset.uiBound === 'true') continue
|
|
@@ -4654,6 +4695,8 @@
|
|
|
4654
4695
|
|
|
4655
4696
|
initAnchors(scope)
|
|
4656
4697
|
|
|
4698
|
+
initScrollProgress(scope)
|
|
4699
|
+
|
|
4657
4700
|
initTours(scope)
|
|
4658
4701
|
|
|
4659
4702
|
initTransfers(scope)
|