@santi020k/lumen-astro 3.0.1 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +424 -7
- package/components/AmountField.astro +21 -0
- package/components/AnimatedNumber.astro +1 -1
- package/components/ApprovalCard.astro +30 -0
- package/components/AttachmentList.astro +15 -0
- package/components/AttachmentPreview.astro +51 -0
- package/components/Avatar.astro +2 -0
- package/components/BarChart.astro +95 -31
- package/components/BoxPlot.astro +77 -0
- package/components/Breadcrumb.astro +2 -1
- package/components/BulletChart.astro +64 -0
- package/components/Button.astro +1 -1
- package/components/Calendar.astro +54 -65
- package/components/CalendarHeatmap.astro +76 -0
- package/components/Card.astro +4 -0
- package/components/ChangeSummary.astro +36 -0
- package/components/ChartDatumActions.astro +24 -0
- package/components/ChartMotion.astro +11 -0
- package/components/Code.astro +18 -3
- package/components/CodeTabs.astro +12 -0
- package/components/ComboChart.astro +93 -11
- package/components/CountryFlag.astro +23 -0
- package/components/DataTable.astro +15 -7
- package/components/DatePicker.astro +24 -17
- package/components/DescriptionDetail.astro +18 -0
- package/components/DescriptionItem.astro +18 -0
- package/components/DescriptionTerm.astro +18 -0
- package/components/DeviceFrame.astro +79 -0
- package/components/DialogBody.astro +20 -0
- package/components/DialogClose.astro +21 -0
- package/components/DialogFooter.astro +20 -0
- package/components/DialogHeader.astro +20 -0
- package/components/DialogTitle.astro +21 -0
- package/components/DumbbellChart.astro +8 -0
- package/components/FileUpload.astro +3 -1
- package/components/FilterBar.astro +25 -0
- package/components/FunnelChart.astro +50 -0
- package/components/Grid.astro +2 -2
- package/components/Heatmap.astro +61 -12
- package/components/Histogram.astro +30 -0
- package/components/Icon.astro +7 -3
- package/components/ImageComparison.astro +74 -0
- package/components/Input.astro +2 -2
- package/components/LineChart.astro +122 -66
- package/components/ListBox.astro +2 -1
- package/components/LollipopChart.astro +8 -0
- package/components/MediaFilmstrip.astro +19 -0
- package/components/MediaThumbnail.astro +29 -0
- package/components/MediaViewport.astro +38 -0
- package/components/Mentions.astro +2 -1
- package/components/MessageScroller.astro +6 -0
- package/components/MotionGroup.astro +19 -0
- package/components/NativeSelect.astro +2 -2
- package/components/Pagination.astro +2 -1
- package/components/PasswordField.astro +1 -1
- package/components/PhoneInput.astro +60 -9
- package/components/PhoneNumber.astro +19 -0
- package/components/PieChart.astro +26 -3
- package/components/Progress.astro +2 -4
- package/components/PromptComposer.astro +36 -0
- package/components/RangeChart.astro +38 -8
- package/components/ScatterChart.astro +62 -14
- package/components/Segmented.astro +5 -5
- package/components/Select.astro +8 -8
- package/components/Skeleton.astro +46 -5
- package/components/SourceCitation.astro +18 -0
- package/components/Sparkline.astro +9 -10
- package/components/Spinner.astro +4 -4
- package/components/Stack.astro +2 -2
- package/components/StreamMessage.astro +26 -0
- package/components/Tabs.astro +3 -0
- package/components/ToolActivity.astro +17 -0
- package/components/TreeSelect.astro +1 -0
- package/components/VirtualList.astro +5 -2
- package/components/VisualEffect.astro +21 -0
- package/components/WaterfallChart.astro +27 -0
- package/components/WorldMap.astro +213 -0
- package/forms.ts +17 -4
- package/index.ts +37 -0
- package/internal/ChartInspection.astro +27 -0
- package/internal/ComparisonChart.astro +69 -0
- package/internal/IntervalChart.astro +66 -0
- package/package.json +6 -4
- package/runtime/UIPrimitives.astro +739 -711
- package/runtime/chart-actions.ts +19 -0
- package/runtime/controllers/amount-field.ts +12 -0
- package/runtime/controllers/attachments.ts +13 -0
- package/runtime/controllers/chart-activation.ts +33 -0
- package/runtime/controllers/charts.ts +33 -0
- package/runtime/controllers/combobox.ts +39 -0
- package/runtime/controllers/comparison-reset.ts +20 -0
- package/runtime/controllers/data-table.ts +142 -0
- package/runtime/controllers/dialogs.ts +32 -32
- package/runtime/controllers/document-navigation.ts +54 -22
- package/runtime/controllers/file-upload.ts +89 -0
- package/runtime/controllers/image-comparison.ts +53 -0
- package/runtime/controllers/list-box-proxy.ts +209 -0
- package/runtime/controllers/media-selection.ts +20 -0
- package/runtime/controllers/media-viewport.ts +54 -0
- package/runtime/controllers/media-workspace.ts +8 -0
- package/runtime/controllers/message-scroller.ts +13 -0
- package/runtime/controllers/motion.ts +15 -16
- package/runtime/controllers/optional-media.ts +29 -0
- package/runtime/controllers/phone-input.ts +128 -39
- package/runtime/controllers/progress.ts +52 -0
- package/runtime/controllers/visual-interactions.ts +75 -0
- package/runtime/controllers/world-map.ts +154 -0
- package/styles/critical.css +696 -66
- package/styles/lumen.css +101 -0
|
@@ -1,36 +1,77 @@
|
|
|
1
1
|
---
|
|
2
2
|
import type { HTMLAttributes } from 'astro/types'
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
type LumenSkeletonAnimation,
|
|
6
|
+
type LumenSkeletonShape,
|
|
7
|
+
type LumenSkeletonSize,
|
|
8
|
+
resolveLumenAstroProps,
|
|
9
|
+
resolveLumenLoadingSemantics,
|
|
10
|
+
resolveLumenSkeletonAnimation,
|
|
11
|
+
resolveLumenSkeletonShape,
|
|
12
|
+
resolveLumenSkeletonSize
|
|
13
|
+
} from '@santi020k/lumen-core'
|
|
5
14
|
|
|
6
15
|
interface Props extends HTMLAttributes<'div'> {
|
|
16
|
+
/** `shimmer` (default) keeps the existing sweep; `pulse` fades; `none` is static. */
|
|
17
|
+
animation?: LumenSkeletonAnimation
|
|
7
18
|
class?: string | null
|
|
8
19
|
className?: string
|
|
20
|
+
/** Pixels for numbers, or a CSS length such as `0.75rem` or `60%`. */
|
|
21
|
+
height?: LumenSkeletonSize
|
|
22
|
+
shape?: LumenSkeletonShape
|
|
23
|
+
width?: LumenSkeletonSize
|
|
9
24
|
}
|
|
10
25
|
|
|
11
26
|
const {
|
|
27
|
+
animation,
|
|
12
28
|
'aria-hidden': ariaHidden,
|
|
13
29
|
'aria-label': ariaLabel,
|
|
14
30
|
'aria-labelledby': ariaLabelledby,
|
|
15
31
|
class: classProp = '',
|
|
16
32
|
className = '',
|
|
33
|
+
height,
|
|
17
34
|
role,
|
|
35
|
+
shape,
|
|
36
|
+
style,
|
|
37
|
+
width,
|
|
18
38
|
...rest
|
|
19
39
|
} = Astro.props
|
|
20
40
|
|
|
21
|
-
const
|
|
41
|
+
const semantics = resolveLumenLoadingSemantics({ ariaHidden, ariaLabel, ariaLabelledby, role })
|
|
42
|
+
const resolvedShape = resolveLumenSkeletonShape(shape)
|
|
43
|
+
const resolvedAnimation = resolveLumenSkeletonAnimation(animation)
|
|
44
|
+
const sizeVariables: Record<string, string> = {}
|
|
45
|
+
const resolvedHeight = resolveLumenSkeletonSize(height)
|
|
46
|
+
const resolvedWidth = resolveLumenSkeletonSize(width)
|
|
47
|
+
|
|
48
|
+
if (resolvedHeight) sizeVariables['--ui-skeleton-height'] = resolvedHeight
|
|
49
|
+
|
|
50
|
+
if (resolvedWidth) sizeVariables['--ui-skeleton-width'] = resolvedWidth
|
|
51
|
+
|
|
52
|
+
// Consumer styles follow the size variables so explicit declarations still win.
|
|
53
|
+
const skeletonStyle = typeof style === 'object' && style !== null ?
|
|
54
|
+
{ ...sizeVariables, ...style } :
|
|
55
|
+
[
|
|
56
|
+
...Object.entries(sizeVariables).map(([name, value]) => `${name}:${value}`),
|
|
57
|
+
typeof style === 'string' ? style : ''
|
|
58
|
+
].filter(Boolean).join(';')
|
|
22
59
|
|
|
23
60
|
const { classList, passthrough } = resolveLumenAstroProps(rest, [
|
|
24
|
-
'ui-skeleton'
|
|
61
|
+
'ui-skeleton',
|
|
62
|
+
resolvedShape === 'rectangle' ? '' : `ui-skeleton--${resolvedShape}`,
|
|
63
|
+
resolvedAnimation === 'pulse' ? 'ui-skeleton--pulse' : '',
|
|
64
|
+
resolvedAnimation === 'none' ? 'ui-skeleton--static' : ''
|
|
25
65
|
], classProp, className)
|
|
26
66
|
---
|
|
27
67
|
|
|
28
68
|
<div
|
|
29
|
-
aria-hidden={ariaHidden ??
|
|
69
|
+
aria-hidden={ariaHidden ?? semantics.ariaHidden}
|
|
30
70
|
aria-label={ariaLabel}
|
|
31
71
|
aria-labelledby={ariaLabelledby}
|
|
32
72
|
class:list={classList}
|
|
33
|
-
role={role ??
|
|
73
|
+
role={role ?? semantics.role}
|
|
74
|
+
style={skeletonStyle || undefined}
|
|
34
75
|
{...passthrough}
|
|
35
76
|
>
|
|
36
77
|
<slot />
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from 'astro/types'
|
|
3
|
+
|
|
4
|
+
import { resolveLumenCitationHref } from '@santi020k/lumen-core'
|
|
5
|
+
|
|
6
|
+
import Link from './Link.astro'
|
|
7
|
+
|
|
8
|
+
interface Props extends Omit<HTMLAttributes<'a'>, 'href'> { href: string, label: string }
|
|
9
|
+
|
|
10
|
+
const { class: className, href, label, ...rest } = Astro.props
|
|
11
|
+
const safeHref = resolveLumenCitationHref(href)
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
{safeHref ?
|
|
15
|
+
<Link {...rest} class:list={['ui-source-citation', className]} href={safeHref} rel="noopener noreferrer">{label}<slot /></Link> :
|
|
16
|
+
(
|
|
17
|
+
<span class:list={['ui-source-citation', className]}>{label}<slot /></span>
|
|
18
|
+
)}
|
|
@@ -57,16 +57,15 @@ const endpoint = geometry.points[geometry.points.length - 1]
|
|
|
57
57
|
))
|
|
58
58
|
}
|
|
59
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
60
|
</svg>
|
|
61
|
+
{
|
|
62
|
+
showEndpoint && endpoint && (
|
|
63
|
+
<span
|
|
64
|
+
aria-hidden="true"
|
|
65
|
+
class="ui-sparkline__endpoint"
|
|
66
|
+
style={{ left: `${endpoint.xCoordinate / 120 * 100}%`, top: `${endpoint.yCoordinate / 40 * 100}%` }}
|
|
67
|
+
/>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
71
70
|
<span class="ui-sr-only">{label}</span>
|
|
72
71
|
</span>
|
package/components/Spinner.astro
CHANGED
|
@@ -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 { resolveLumenAstroProps, resolveLumenLoadingSemantics } from '@santi020k/lumen-core'
|
|
5
5
|
|
|
6
6
|
interface Props extends HTMLAttributes<'span'> {
|
|
7
7
|
class?: string | null
|
|
@@ -18,7 +18,7 @@ const {
|
|
|
18
18
|
...rest
|
|
19
19
|
} = Astro.props
|
|
20
20
|
|
|
21
|
-
const
|
|
21
|
+
const semantics = resolveLumenLoadingSemantics({ ariaHidden, ariaLabel, ariaLabelledby, role })
|
|
22
22
|
|
|
23
23
|
const { classList, passthrough } = resolveLumenAstroProps(rest, [
|
|
24
24
|
'ui-spinner'
|
|
@@ -26,11 +26,11 @@ const { classList, passthrough } = resolveLumenAstroProps(rest, [
|
|
|
26
26
|
---
|
|
27
27
|
|
|
28
28
|
<span
|
|
29
|
-
aria-hidden={ariaHidden ??
|
|
29
|
+
aria-hidden={ariaHidden ?? semantics.ariaHidden}
|
|
30
30
|
aria-label={ariaLabel}
|
|
31
31
|
aria-labelledby={ariaLabelledby}
|
|
32
32
|
class:list={classList}
|
|
33
|
-
role={role ??
|
|
33
|
+
role={role ?? semantics.role}
|
|
34
34
|
{...passthrough}
|
|
35
35
|
>
|
|
36
36
|
<slot />
|
package/components/Stack.astro
CHANGED
|
@@ -9,7 +9,7 @@ interface Props extends HTMLAttributes<'div'> {
|
|
|
9
9
|
class?: string | null
|
|
10
10
|
className?: string
|
|
11
11
|
direction?: 'horizontal' | 'vertical'
|
|
12
|
-
gap?: 'lg' | 'md' | 'none' | 'sm' | 'xl'
|
|
12
|
+
gap?: '2xl' | '3xl' | 'group' | 'lg' | 'md' | 'none' | 'related' | 'section' | 'sm' | 'xl' | 'xs'
|
|
13
13
|
justify?: 'between' | 'center' | 'end' | 'start'
|
|
14
14
|
wrap?: boolean
|
|
15
15
|
}
|
|
@@ -20,7 +20,7 @@ const {
|
|
|
20
20
|
class: classProp = '',
|
|
21
21
|
className = '',
|
|
22
22
|
direction = 'vertical',
|
|
23
|
-
gap = '
|
|
23
|
+
gap = 'group',
|
|
24
24
|
justify = 'start',
|
|
25
25
|
wrap = false,
|
|
26
26
|
...rest
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from 'astro/types'
|
|
3
|
+
|
|
4
|
+
import type { LumenStreamStatus } from '@santi020k/lumen-core'
|
|
5
|
+
|
|
6
|
+
import Message from './Message.astro'
|
|
7
|
+
import Prose from './Prose.astro'
|
|
8
|
+
import Stack from './Stack.astro'
|
|
9
|
+
|
|
10
|
+
interface Props extends HTMLAttributes<'article'> {
|
|
11
|
+
label?: string
|
|
12
|
+
status?: LumenStreamStatus
|
|
13
|
+
statusLabel: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { class: className, label = 'Assistant', status = 'idle', statusLabel, ...rest } = Astro.props
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
<Message {...rest} aria-label={label} class:list={['ui-stream-message', className]} data-status={status} from="assistant">
|
|
20
|
+
<Prose aria-busy={status === 'streaming'} aria-live="off" data-ui-stream-content><slot /></Prose>
|
|
21
|
+
<Stack direction="horizontal" gap="related" wrap>
|
|
22
|
+
<span aria-live="polite" role="status">{statusLabel}</span>
|
|
23
|
+
<slot name="actions" />
|
|
24
|
+
</Stack>
|
|
25
|
+
<slot name="sources" />
|
|
26
|
+
</Message>
|
package/components/Tabs.astro
CHANGED
|
@@ -7,6 +7,7 @@ interface Props extends HTMLAttributes<'div'> {
|
|
|
7
7
|
class?: string | null
|
|
8
8
|
className?: string
|
|
9
9
|
glass?: boolean | 'subtle' | 'strong'
|
|
10
|
+
indicator?: boolean
|
|
10
11
|
initialValue?: string
|
|
11
12
|
storageKey?: string
|
|
12
13
|
}
|
|
@@ -15,6 +16,7 @@ const {
|
|
|
15
16
|
class: classProp = '',
|
|
16
17
|
className = '',
|
|
17
18
|
glass = false,
|
|
19
|
+
indicator = false,
|
|
18
20
|
initialValue,
|
|
19
21
|
storageKey,
|
|
20
22
|
...rest
|
|
@@ -29,6 +31,7 @@ const { classList, passthrough } = resolveLumenAstroProps(rest, [
|
|
|
29
31
|
---
|
|
30
32
|
|
|
31
33
|
<div
|
|
34
|
+
data-ui-tab-indicator={indicator || undefined}
|
|
32
35
|
data-initial-value={initialValue}
|
|
33
36
|
data-storage-key={storageKey}
|
|
34
37
|
data-ui-tabs
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from 'astro/types'
|
|
3
|
+
|
|
4
|
+
import type { LumenToolStatus } from '@santi020k/lumen-core'
|
|
5
|
+
|
|
6
|
+
import Collapsible from './Collapsible.astro'
|
|
7
|
+
import Stack from './Stack.astro'
|
|
8
|
+
|
|
9
|
+
interface Props extends HTMLAttributes<'details'> { label: string, status?: LumenToolStatus, statusLabel: string }
|
|
10
|
+
|
|
11
|
+
const { class: className, label, status = 'queued', statusLabel, ...rest } = Astro.props
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<Collapsible {...rest} class:list={['ui-tool-activity', className]} data-status={status}>
|
|
15
|
+
<summary>{label}</summary>
|
|
16
|
+
<Stack gap="related"><span role="status">{statusLabel}</span><slot /></Stack>
|
|
17
|
+
</Collapsible>
|
|
@@ -7,14 +7,17 @@ interface Props extends HTMLAttributes<'div'> {
|
|
|
7
7
|
class?: string | null
|
|
8
8
|
className?: string
|
|
9
9
|
glass?: boolean | 'subtle' | 'strong'
|
|
10
|
+
itemSize?: number | string
|
|
11
|
+
overscan?: number | string
|
|
12
|
+
mode?: 'data' | 'mounted'
|
|
10
13
|
}
|
|
11
14
|
|
|
12
|
-
const { class: classProp = '', className = '', glass = false, ...rest } = Astro.props
|
|
15
|
+
const { class: classProp = '', className = '', glass = false, itemSize = 44, overscan = 4, mode = 'mounted', tabindex = 0, ...rest } = Astro.props
|
|
13
16
|
|
|
14
17
|
const { classList, passthrough } = resolveLumenAstroProps(rest, ['ui-virtual-list', glass && 'ui-virtual-list--glass', glass === 'subtle' && 'ui-glass-subtle', glass === 'strong' && 'ui-glass-strong'
|
|
15
18
|
], classProp, className)
|
|
16
19
|
---
|
|
17
20
|
|
|
18
|
-
<div class:list={classList} data-ui-virtual-list {...passthrough}>
|
|
21
|
+
<div class:list={classList} data-ui-virtual-list data-ui-virtual-list-mode={mode} data-ui-item-size={itemSize} data-ui-overscan={overscan} tabindex={tabindex} {...passthrough}>
|
|
19
22
|
<slot />
|
|
20
23
|
</div>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from 'astro/types'
|
|
3
|
+
|
|
4
|
+
import { type LumenVisualEffectVariant, normalizeLumenEffectIntensity, resolveLumenAstroProps } from '@santi020k/lumen-core'
|
|
5
|
+
|
|
6
|
+
interface Props extends HTMLAttributes<'div'> {
|
|
7
|
+
animated?: boolean
|
|
8
|
+
class?: string | null
|
|
9
|
+
className?: string
|
|
10
|
+
intensity?: number
|
|
11
|
+
variant?: LumenVisualEffectVariant
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { animated = false, class: classProp, className, intensity = 0.5, style, variant = 'mesh', ...rest } = Astro.props
|
|
15
|
+
const { classList, passthrough } = resolveLumenAstroProps(rest, ['ui-visual-effect'], classProp, className)
|
|
16
|
+
const effectStyle = `${typeof style === 'string' ? style : ''};--ui-effect-intensity:${normalizeLumenEffectIntensity(intensity)}`
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
<div class:list={classList} data-ui-visual-effect={variant} data-ui-effect-animated={String(animated)} style={effectStyle} {...passthrough}>
|
|
20
|
+
<slot />
|
|
21
|
+
</div>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from 'astro/types'
|
|
3
|
+
|
|
4
|
+
import { createLumenWaterfallGeometry, type LumenChartLabels, type LumenWaterfallDatum, resolveLumenChartLabels } from '@santi020k/lumen-core'
|
|
5
|
+
|
|
6
|
+
import IntervalChart from '../internal/IntervalChart.astro'
|
|
7
|
+
|
|
8
|
+
interface Props extends HTMLAttributes<'figure'> {
|
|
9
|
+
caption?: string
|
|
10
|
+
class?: string | null
|
|
11
|
+
className?: string
|
|
12
|
+
data: readonly LumenWaterfallDatum[]
|
|
13
|
+
description?: string
|
|
14
|
+
formatValue?: (value: number) => string
|
|
15
|
+
heading?: string
|
|
16
|
+
labels?: Partial<LumenChartLabels>
|
|
17
|
+
presentation?: 'bare' | 'default'
|
|
18
|
+
showTable?: boolean
|
|
19
|
+
summary?: string
|
|
20
|
+
valueLabel?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { data, class: classProp, className, formatValue = String, labels, valueLabel = resolveLumenChartLabels(labels).value, ...rest } = Astro.props
|
|
24
|
+
const model = createLumenWaterfallGeometry(data, { formatValue })
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
<IntervalChart {...rest} class:list={['ui-waterfall-chart', classProp, className]} {model} {labels} {formatValue} formatBoundary={formatValue} {valueLabel} />
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from 'astro/types'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
findLumenWorldMapCountry,
|
|
6
|
+
LUMEN_WORLD_MAP_VIEW_BOX,
|
|
7
|
+
type LumenWorldMapCountryGeometry,
|
|
8
|
+
type LumenWorldMapMarker,
|
|
9
|
+
type LumenWorldMapVariant,
|
|
10
|
+
normalizeLumenWorldMapCountries,
|
|
11
|
+
normalizeLumenWorldMapHighlightedCountries,
|
|
12
|
+
normalizeLumenWorldMapMarkers,
|
|
13
|
+
projectLumenWorldMapCoordinate,
|
|
14
|
+
resolveLumenAstroProps,
|
|
15
|
+
resolveLumenWorldMapCountryLabel
|
|
16
|
+
} from '@santi020k/lumen-core'
|
|
17
|
+
import { type LumenWorldMapZoomLabels, lumenWorldMapZoomLabels } from '@santi020k/lumen-core/world-map-zoom'
|
|
18
|
+
|
|
19
|
+
import Button from './Button.astro'
|
|
20
|
+
|
|
21
|
+
interface Props extends HTMLAttributes<'figure'> {
|
|
22
|
+
initialView?: 'world' | 'highlighted'
|
|
23
|
+
zoomable?: boolean
|
|
24
|
+
zoomLabels?: Partial<LumenWorldMapZoomLabels>
|
|
25
|
+
animated?: boolean
|
|
26
|
+
caption?: string
|
|
27
|
+
class?: string | null
|
|
28
|
+
className?: string
|
|
29
|
+
countries: readonly LumenWorldMapCountryGeometry[]
|
|
30
|
+
description?: string
|
|
31
|
+
heading?: string
|
|
32
|
+
highlightedCountries?: readonly string[]
|
|
33
|
+
interactive?: boolean
|
|
34
|
+
label: string
|
|
35
|
+
labels?: Readonly<Record<string, string>>
|
|
36
|
+
listLabel?: string
|
|
37
|
+
markers?: readonly LumenWorldMapMarker[]
|
|
38
|
+
selectedCountry?: string
|
|
39
|
+
variant?: LumenWorldMapVariant
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const {
|
|
43
|
+
animated = true,
|
|
44
|
+
caption,
|
|
45
|
+
class: classProp = '',
|
|
46
|
+
className = '',
|
|
47
|
+
countries: inputCountries,
|
|
48
|
+
description,
|
|
49
|
+
heading,
|
|
50
|
+
highlightedCountries,
|
|
51
|
+
interactive = true,
|
|
52
|
+
label,
|
|
53
|
+
labels,
|
|
54
|
+
listLabel = 'Choose a country',
|
|
55
|
+
markers,
|
|
56
|
+
selectedCountry,
|
|
57
|
+
variant = 'dotted',
|
|
58
|
+
initialView = 'world',
|
|
59
|
+
zoomable = true,
|
|
60
|
+
zoomLabels,
|
|
61
|
+
...rest
|
|
62
|
+
} = Astro.props
|
|
63
|
+
|
|
64
|
+
const countries = normalizeLumenWorldMapCountries(inputCountries)
|
|
65
|
+
const highlightedIds = normalizeLumenWorldMapHighlightedCountries(highlightedCountries, countries)
|
|
66
|
+
const highlightedSet = new Set(highlightedIds)
|
|
67
|
+
const normalizedMarkers = normalizeLumenWorldMapMarkers(markers)
|
|
68
|
+
const selected = findLumenWorldMapCountry(countries, selectedCountry)
|
|
69
|
+
const zoomText = { ...lumenWorldMapZoomLabels, ...zoomLabels }
|
|
70
|
+
const instanceId = `ui-world-map-${crypto.randomUUID()}`
|
|
71
|
+
const neutralPatternId = `${instanceId}-dot-neutral`
|
|
72
|
+
const highlightPatternId = `${instanceId}-dot-highlight`
|
|
73
|
+
const { height: viewBoxHeight, width: viewBoxWidth } = LUMEN_WORLD_MAP_VIEW_BOX
|
|
74
|
+
const { classList, passthrough } = resolveLumenAstroProps(rest, ['ui-world-map'], classProp, className)
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
<figure
|
|
78
|
+
class:list={classList}
|
|
79
|
+
data-animated={String(animated)}
|
|
80
|
+
data-interactive={String(interactive)}
|
|
81
|
+
data-ui-world-map
|
|
82
|
+
data-initial-view={initialView}
|
|
83
|
+
data-variant={variant}
|
|
84
|
+
id={instanceId}
|
|
85
|
+
{...passthrough}
|
|
86
|
+
>
|
|
87
|
+
{
|
|
88
|
+
(heading || description) && (
|
|
89
|
+
<header class="ui-world-map__heading">
|
|
90
|
+
{heading && <h3>{heading}</h3>}
|
|
91
|
+
{description && <p>{description}</p>}
|
|
92
|
+
</header>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
{zoomable && (
|
|
96
|
+
<div class="ui-world-map__zoom-controls">
|
|
97
|
+
<Button aria-label={zoomText.zoomOut} data-ui-world-map-zoom="out" disabled size="icon" variant="outline">−</Button>
|
|
98
|
+
<output aria-label={zoomText.level} aria-live="polite" data-ui-world-map-zoom-status>100%</output>
|
|
99
|
+
<Button aria-label={zoomText.zoomIn} data-ui-world-map-zoom="in" disabled size="icon" variant="outline">+</Button>
|
|
100
|
+
<Button data-ui-world-map-zoom="fit" disabled size="sm" variant="outline">{zoomText.fit}</Button>
|
|
101
|
+
<Button data-ui-world-map-zoom="reset" disabled size="sm" variant="outline">{zoomText.reset}</Button>
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
<div class="ui-world-map__frame">
|
|
105
|
+
<div
|
|
106
|
+
aria-label={`${zoomText.viewport}: ${label}`}
|
|
107
|
+
class="ui-world-map__viewport"
|
|
108
|
+
data-ui-world-map-viewport
|
|
109
|
+
role="region"
|
|
110
|
+
tabindex={zoomable ? 0 : undefined}
|
|
111
|
+
>
|
|
112
|
+
<svg
|
|
113
|
+
aria-label={label}
|
|
114
|
+
class="ui-world-map__plot"
|
|
115
|
+
role="img"
|
|
116
|
+
viewBox={`0 0 ${viewBoxWidth} ${viewBoxHeight}`}
|
|
117
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
118
|
+
>
|
|
119
|
+
{
|
|
120
|
+
variant === 'dotted' && (
|
|
121
|
+
<defs>
|
|
122
|
+
<pattern height="4" id={neutralPatternId} patternUnits="userSpaceOnUse" width="4">
|
|
123
|
+
<circle class="ui-world-map__dot ui-world-map__dot--neutral" cx="2" cy="2" r="1.1" />
|
|
124
|
+
</pattern>
|
|
125
|
+
<pattern height="4" id={highlightPatternId} patternUnits="userSpaceOnUse" width="4">
|
|
126
|
+
<circle class="ui-world-map__dot ui-world-map__dot--highlight" cx="2" cy="2" r="1.1" />
|
|
127
|
+
</pattern>
|
|
128
|
+
</defs>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
<g aria-hidden="true" class="ui-world-map__countries">
|
|
132
|
+
{countries.map(country => {
|
|
133
|
+
const isHighlighted = highlightedSet.has(country.id)
|
|
134
|
+
const isSelected = selected?.id === country.id
|
|
135
|
+
const countryLabel = resolveLumenWorldMapCountryLabel(country, labels)
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<path
|
|
139
|
+
class:list={[
|
|
140
|
+
'ui-world-map__country',
|
|
141
|
+
isHighlighted && 'ui-world-map__country--highlighted',
|
|
142
|
+
isSelected && 'ui-world-map__country--selected'
|
|
143
|
+
]}
|
|
144
|
+
d={country.path}
|
|
145
|
+
data-label={countryLabel}
|
|
146
|
+
data-label-x={country.labelX}
|
|
147
|
+
data-label-y={country.labelY}
|
|
148
|
+
data-ui-world-map-country={country.id}
|
|
149
|
+
fill={
|
|
150
|
+
variant === 'dotted' ?
|
|
151
|
+
`url(#${isHighlighted ? highlightPatternId : neutralPatternId})` :
|
|
152
|
+
undefined
|
|
153
|
+
}
|
|
154
|
+
fill-rule="evenodd"
|
|
155
|
+
>
|
|
156
|
+
<title>{countryLabel}</title>
|
|
157
|
+
</path>
|
|
158
|
+
)
|
|
159
|
+
})}
|
|
160
|
+
</g>
|
|
161
|
+
{
|
|
162
|
+
normalizedMarkers.length > 0 && (
|
|
163
|
+
<g aria-hidden="true" class="ui-world-map__markers">
|
|
164
|
+
{normalizedMarkers.map(marker => {
|
|
165
|
+
const { x, y } = projectLumenWorldMapCoordinate(marker.longitude, marker.latitude)
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<circle class="ui-world-map__marker" cx={x} cy={y} r="4">
|
|
169
|
+
<title>{marker.label}</title>
|
|
170
|
+
</circle>
|
|
171
|
+
)
|
|
172
|
+
})}
|
|
173
|
+
</g>
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
<path aria-hidden="true" class="ui-world-map__selection" d={selected?.path ?? ''} />
|
|
177
|
+
</svg>
|
|
178
|
+
</div>
|
|
179
|
+
<span class="ui-world-map__inspection" data-ui-world-map-inspection hidden />
|
|
180
|
+
</div>
|
|
181
|
+
{
|
|
182
|
+
interactive && (
|
|
183
|
+
<label class="ui-world-map__select-label">
|
|
184
|
+
<span>{listLabel}</span>
|
|
185
|
+
<select class="ui-select ui-world-map__select" data-ui-world-map-select disabled>
|
|
186
|
+
<option value="">{listLabel}</option>
|
|
187
|
+
{countries.map(country => (
|
|
188
|
+
<option selected={selected?.id === country.id} value={country.id}>
|
|
189
|
+
{resolveLumenWorldMapCountryLabel(country, labels)}
|
|
190
|
+
</option>
|
|
191
|
+
))}
|
|
192
|
+
</select>
|
|
193
|
+
</label>
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
{
|
|
197
|
+
highlightedIds.length > 0 && (
|
|
198
|
+
<ul aria-label="Highlighted countries" class="ui-world-map__highlights" data-ui-world-map-highlights>
|
|
199
|
+
{highlightedIds.map(id => {
|
|
200
|
+
const country = findLumenWorldMapCountry(countries, id)
|
|
201
|
+
|
|
202
|
+
return country && <li>{resolveLumenWorldMapCountryLabel(country, labels)}</li>
|
|
203
|
+
})}
|
|
204
|
+
</ul>
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
{normalizedMarkers.length > 0 && (
|
|
208
|
+
<ul aria-label="Map markers" class="ui-sr-only">
|
|
209
|
+
{normalizedMarkers.map(marker => <li>{marker.label}</li>)}
|
|
210
|
+
</ul>
|
|
211
|
+
)}
|
|
212
|
+
{caption && <figcaption>{caption}</figcaption>}
|
|
213
|
+
</figure>
|
package/forms.ts
CHANGED
|
@@ -10,11 +10,24 @@ export interface AstroActionErrorLike {
|
|
|
10
10
|
|
|
11
11
|
export type LumenControlIdMap = Readonly<Record<string, string>>
|
|
12
12
|
|
|
13
|
-
const
|
|
14
|
-
typeof
|
|
15
|
-
|
|
13
|
+
const isFieldMessages = (value: unknown): boolean => (
|
|
14
|
+
value === null || value === undefined || typeof value === 'string' ||
|
|
15
|
+
(Array.isArray(value) && value.every((message: unknown) => typeof message === 'string'))
|
|
16
16
|
)
|
|
17
17
|
|
|
18
|
+
const isActionFields = (value: unknown): value is NonNullable<AstroActionErrorLike['fields']> => (
|
|
19
|
+
typeof value === 'object' && value !== null && !Array.isArray(value) &&
|
|
20
|
+
Object.values(value).every(isFieldMessages)
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
const isActionErrorLike = (error: unknown): error is AstroActionErrorLike => {
|
|
24
|
+
if (typeof error !== 'object' || error === null) return false
|
|
25
|
+
|
|
26
|
+
if ('message' in error && error.message !== undefined && typeof error.message !== 'string') return false
|
|
27
|
+
|
|
28
|
+
return !('fields' in error) || error.fields === undefined || isActionFields(error.fields)
|
|
29
|
+
}
|
|
30
|
+
|
|
18
31
|
export const normalizeAstroActionErrors = (
|
|
19
32
|
error: unknown,
|
|
20
33
|
controlIds: LumenControlIdMap = {}
|
|
@@ -35,7 +48,7 @@ export const normalizeAstroActionErrors = (
|
|
|
35
48
|
return {
|
|
36
49
|
fields: normalized.fields.map(fieldError => ({
|
|
37
50
|
...fieldError,
|
|
38
|
-
...(controlIds[fieldError.name] ?
|
|
51
|
+
...(Object.hasOwn(controlIds, fieldError.name) && controlIds[fieldError.name] ?
|
|
39
52
|
{
|
|
40
53
|
controlId: controlIds[fieldError.name]
|
|
41
54
|
} :
|