@santi020k/lumen-astro 1.2.0 → 1.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 CHANGED
@@ -60,6 +60,63 @@ incremental migrations, but new code should keep native behavior and visual styl
60
60
  <NativeSelect size={8} visualSize="lg" />
61
61
  ```
62
62
 
63
+ ## Compound interactive components
64
+
65
+ Popover, dropdown menu, tabs, and tooltip parts expose the DOM and ARIA contracts consumed by the
66
+ shared progressive-enhancement runtime. Prefer these parts over recreating roles and `data-ui-*`
67
+ attributes by hand.
68
+
69
+ ```astro
70
+ ---
71
+ import {
72
+ Button,
73
+ DropdownMenu,
74
+ DropdownMenuContent,
75
+ DropdownMenuItem,
76
+ DropdownMenuSeparator,
77
+ DropdownMenuTrigger,
78
+ Popover,
79
+ PopoverPanel,
80
+ PopoverTrigger,
81
+ Tabs,
82
+ TabsList,
83
+ TabsPanel,
84
+ TabsTrigger,
85
+ Tooltip,
86
+ TooltipContent
87
+ } from '@santi020k/lumen-astro'
88
+ ---
89
+
90
+ <DropdownMenu>
91
+ <DropdownMenuTrigger>Download</DropdownMenuTrigger>
92
+ <DropdownMenuContent>
93
+ <DropdownMenuItem>macOS</DropdownMenuItem>
94
+ <DropdownMenuItem>Windows</DropdownMenuItem>
95
+ <DropdownMenuSeparator />
96
+ <DropdownMenuItem disabled status="Soon">Android</DropdownMenuItem>
97
+ </DropdownMenuContent>
98
+ </DropdownMenu>
99
+
100
+ <Popover>
101
+ <PopoverTrigger>Filters</PopoverTrigger>
102
+ <PopoverPanel>Filter controls</PopoverPanel>
103
+ </Popover>
104
+
105
+ <Tabs initialValue="preview">
106
+ <TabsList aria-label="Workspace view">
107
+ <TabsTrigger value="preview">Preview</TabsTrigger>
108
+ <TabsTrigger value="code">Code</TabsTrigger>
109
+ </TabsList>
110
+ <TabsPanel value="preview">Rendered result</TabsPanel>
111
+ <TabsPanel value="code">Source code</TabsPanel>
112
+ </Tabs>
113
+
114
+ <Tooltip>
115
+ <Button aria-label="Save changes">Save</Button>
116
+ <TooltipContent>Save changes</TooltipContent>
117
+ </Tooltip>
118
+ ```
119
+
63
120
  ## Context navigation
64
121
 
65
122
  Use `ContextNavigation` below a primary header when the available links change with a selected
@@ -0,0 +1,35 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types'
3
+
4
+ import { resolveLumenAstroProps } from '@santi020k/lumen-core'
5
+
6
+ interface Props extends HTMLAttributes<'div'> {
7
+ class?: string | null
8
+ className?: string
9
+ intensity?: 'medium' | 'strong' | 'subtle'
10
+ tone?: 'accent' | 'brand' | 'neutral'
11
+ variant?: 'aurora' | 'dots' | 'grid' | 'rays'
12
+ }
13
+
14
+ const {
15
+ class: classProp = '',
16
+ className = '',
17
+ intensity = 'medium',
18
+ tone = 'brand',
19
+ variant = 'aurora',
20
+ ...rest
21
+ } = Astro.props
22
+
23
+ const { classList, passthrough } = resolveLumenAstroProps(rest, [
24
+ 'ui-backdrop',
25
+ `ui-backdrop--${variant}`,
26
+ `ui-backdrop--${tone}`,
27
+ `ui-backdrop--${intensity}`
28
+ ], classProp, className)
29
+ ---
30
+
31
+ <div class:list={classList} {...passthrough}>
32
+ <div class="ui-backdrop__content">
33
+ <slot />
34
+ </div>
35
+ </div>
@@ -0,0 +1,28 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types'
3
+
4
+ import { resolveLumenAstroProps } from '@santi020k/lumen-core'
5
+
6
+ interface Props extends HTMLAttributes<'div'> {
7
+ class?: string | null
8
+ className?: string
9
+ }
10
+
11
+ const {
12
+ class: classProp = '',
13
+ className = '',
14
+ role = 'menu',
15
+ ...rest
16
+ } = Astro.props
17
+
18
+ const { classList, passthrough } = resolveLumenAstroProps(
19
+ rest,
20
+ ['ui-menu__content'],
21
+ classProp,
22
+ className
23
+ )
24
+ ---
25
+
26
+ <div role={role} class:list={classList} {...passthrough}>
27
+ <slot />
28
+ </div>
@@ -0,0 +1,39 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types'
3
+
4
+ import { resolveLumenAstroProps } from '@santi020k/lumen-core'
5
+
6
+ interface Props extends HTMLAttributes<'button'> {
7
+ class?: string | null
8
+ className?: string
9
+ status?: string
10
+ }
11
+
12
+ const {
13
+ class: classProp = '',
14
+ className = '',
15
+ disabled = false,
16
+ status,
17
+ type = 'button',
18
+ ...rest
19
+ } = Astro.props
20
+
21
+ const { classList, passthrough } = resolveLumenAstroProps(
22
+ rest,
23
+ ['ui-menu__item'],
24
+ classProp,
25
+ className
26
+ )
27
+ ---
28
+
29
+ <button
30
+ type={type}
31
+ role="menuitem"
32
+ class:list={classList}
33
+ disabled={disabled}
34
+ aria-disabled={disabled ? 'true' : undefined}
35
+ {...passthrough}
36
+ >
37
+ <span class="ui-menu__item-label"><slot /></span>
38
+ {status && <span class="ui-menu__item-status">{status}</span>}
39
+ </button>
@@ -0,0 +1,25 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types'
3
+
4
+ import { resolveLumenAstroProps } from '@santi020k/lumen-core'
5
+
6
+ interface Props extends HTMLAttributes<'div'> {
7
+ class?: string | null
8
+ className?: string
9
+ }
10
+
11
+ const {
12
+ class: classProp = '',
13
+ className = '',
14
+ ...rest
15
+ } = Astro.props
16
+
17
+ const { classList, passthrough } = resolveLumenAstroProps(
18
+ rest,
19
+ ['ui-menu__separator'],
20
+ classProp,
21
+ className
22
+ )
23
+ ---
24
+
25
+ <div role="separator" class:list={classList} {...passthrough} />
@@ -0,0 +1,39 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types'
3
+
4
+ import { resolveLumenAstroProps } from '@santi020k/lumen-core'
5
+
6
+ interface Props extends HTMLAttributes<'button'> {
7
+ class?: string | null
8
+ className?: string
9
+ }
10
+
11
+ const {
12
+ class: classProp = '',
13
+ className = '',
14
+ type = 'button',
15
+ ...rest
16
+ } = Astro.props
17
+
18
+ const { classList, passthrough } = resolveLumenAstroProps(
19
+ rest,
20
+ [
21
+ 'ui-button',
22
+ 'ui-button--outline',
23
+ 'ui-button--default-size',
24
+ 'ui-menu__trigger'
25
+ ],
26
+ classProp,
27
+ className
28
+ )
29
+ ---
30
+
31
+ <button
32
+ type={type}
33
+ class:list={classList}
34
+ data-ui-trigger
35
+ aria-haspopup="menu"
36
+ {...passthrough}
37
+ >
38
+ <slot />
39
+ </button>
@@ -0,0 +1,43 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types'
3
+
4
+ import { resolveLumenAstroProps } from '@santi020k/lumen-core'
5
+
6
+ interface Props extends HTMLAttributes<'div'> {
7
+ class?: string | null
8
+ className?: string
9
+ label?: string
10
+ size?: 'lg' | 'md' | 'sm'
11
+ tone?: 'accent' | 'brand' | 'neutral'
12
+ variant?: 'glow' | 'grid' | 'orbit'
13
+ }
14
+
15
+ const {
16
+ class: classProp = '',
17
+ className = '',
18
+ label,
19
+ size = 'md',
20
+ tone = 'brand',
21
+ variant = 'orbit',
22
+ ...rest
23
+ } = Astro.props
24
+
25
+ const { classList, passthrough } = resolveLumenAstroProps(rest, [
26
+ 'ui-graphic',
27
+ `ui-graphic--${variant}`,
28
+ `ui-graphic--${tone}`,
29
+ `ui-graphic--${size}`
30
+ ], classProp, className)
31
+ ---
32
+
33
+ <div
34
+ aria-hidden={label ? undefined : 'true'}
35
+ aria-label={label}
36
+ class:list={classList}
37
+ role={label ? 'img' : undefined}
38
+ {...passthrough}
39
+ >
40
+ <div class="ui-graphic__content">
41
+ <slot />
42
+ </div>
43
+ </div>
@@ -0,0 +1,67 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types'
3
+
4
+ import { resolveLumenAstroProps } from '@santi020k/lumen-core'
5
+
6
+ interface Props extends HTMLAttributes<'span'> {
7
+ class?: string | null
8
+ className?: string
9
+ label?: string
10
+ size?: 'lg' | 'md' | 'sm'
11
+ tone?: 'accent' | 'auto' | 'brand' | 'neutral'
12
+ variant?: 'empty' | 'error' | 'offline' | 'success'
13
+ }
14
+
15
+ const {
16
+ class: classProp = '',
17
+ className = '',
18
+ label,
19
+ size = 'md',
20
+ tone = 'auto',
21
+ variant = 'empty',
22
+ ...rest
23
+ } = Astro.props
24
+
25
+ const { classList, passthrough } = resolveLumenAstroProps(rest, [
26
+ 'ui-illustration',
27
+ `ui-illustration--${variant}`,
28
+ `ui-illustration--${tone}`,
29
+ `ui-illustration--${size}`
30
+ ], classProp, className)
31
+ ---
32
+
33
+ <span
34
+ aria-hidden={label ? undefined : 'true'}
35
+ aria-label={label}
36
+ class:list={classList}
37
+ role={label ? 'img' : undefined}
38
+ {...passthrough}
39
+ >
40
+ <svg aria-hidden="true" fill="none" viewBox="0 0 120 120">
41
+ <circle class="ui-illustration__wash" cx="60" cy="60" r="48" />
42
+ {variant === 'empty' && (
43
+ <g>
44
+ <path d="M32 48h56l-7 35H39l-7-35Z" />
45
+ <path d="M43 48 49 36h22l6 12M47 66h26" />
46
+ </g>
47
+ )}
48
+ {variant === 'success' && (
49
+ <g>
50
+ <circle cx="60" cy="60" r="28" />
51
+ <path d="m45 60 10 10 21-23" />
52
+ </g>
53
+ )}
54
+ {variant === 'error' && (
55
+ <g>
56
+ <circle cx="60" cy="60" r="28" />
57
+ <path d="m49 49 22 22m0-22L49 71" />
58
+ </g>
59
+ )}
60
+ {variant === 'offline' && (
61
+ <g>
62
+ <path d="M37 74h44a14 14 0 0 0 1-28 23 23 0 0 0-42-4 16 16 0 0 0-3 32Z" />
63
+ <path d="m38 34 44 52" />
64
+ </g>
65
+ )}
66
+ </svg>
67
+ </span>
@@ -1,20 +1,41 @@
1
1
  ---
2
2
  import type { HTMLAttributes } from 'astro/types'
3
3
 
4
- import { resolveLumenAstroProps } from '@santi020k/lumen-core'
4
+ import {
5
+ formatLumenLanguageLabel,
6
+ getLumenLocalePair,
7
+ type LumenLocaleOption,
8
+ normalizeLumenLocales,
9
+ resolveLumenAstroProps
10
+ } from '@santi020k/lumen-core'
5
11
 
6
12
  interface Props extends HTMLAttributes<'button'> {
7
13
  class?: string | null
8
14
  className?: string
15
+ defaultValue?: string
16
+ labelTemplate?: string
17
+ locales?: readonly LumenLocaleOption[]
18
+ storageKey?: string
19
+ value?: string
9
20
  }
10
21
 
11
22
  const {
12
23
  class: classProp = '',
13
24
  className = '',
25
+ defaultValue,
26
+ labelTemplate = 'Change language from {current} to {next}',
27
+ locales: localeOptions,
28
+ storageKey,
14
29
  type = 'button',
30
+ value,
15
31
  ...rest
16
32
  } = Astro.props
17
33
 
34
+ const locales = normalizeLumenLocales(localeOptions)
35
+ const initialValue = value ?? defaultValue ?? locales[0]?.value
36
+ const { current, next } = getLumenLocalePair(locales, initialValue)
37
+ const accessibleLabel = formatLumenLanguageLabel(labelTemplate, current, next)
38
+
18
39
  const { classList, passthrough } = resolveLumenAstroProps(rest, [
19
40
  'ui-language-toggle'
20
41
  ], classProp, className)
@@ -24,7 +45,19 @@ const { classList, passthrough } = resolveLumenAstroProps(rest, [
24
45
  type={type}
25
46
  class:list={classList}
26
47
  data-ui-language-toggle
48
+ data-ui-language-controlled={value === undefined ? undefined : ''}
49
+ data-ui-language-initial-value={initialValue}
50
+ data-ui-language-label-template={labelTemplate}
51
+ data-ui-language-locales={JSON.stringify(locales)}
52
+ data-ui-language-value={current.value}
53
+ data-ui-language-next-value={next.value}
54
+ data-ui-language-storage-key={storageKey}
55
+ aria-label={accessibleLabel}
27
56
  {...passthrough}
28
57
  >
29
- <slot />
58
+ {
59
+ Astro.slots.has('default') ?
60
+ <slot /> :
61
+ <span data-ui-language-label lang={next.value}>{next.label}</span>
62
+ }
30
63
  </button>
@@ -0,0 +1,27 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types'
3
+
4
+ import { resolveLumenAstroProps } from '@santi020k/lumen-core'
5
+
6
+ interface Props extends HTMLAttributes<'div'> {
7
+ class?: string | null
8
+ className?: string
9
+ }
10
+
11
+ const {
12
+ class: classProp = '',
13
+ className = '',
14
+ ...rest
15
+ } = Astro.props
16
+
17
+ const { classList, passthrough } = resolveLumenAstroProps(
18
+ rest,
19
+ ['ui-popover__panel'],
20
+ classProp,
21
+ className
22
+ )
23
+ ---
24
+
25
+ <div class:list={classList} {...passthrough}>
26
+ <slot />
27
+ </div>
@@ -0,0 +1,34 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types'
3
+
4
+ import { resolveLumenAstroProps } from '@santi020k/lumen-core'
5
+
6
+ interface Props extends HTMLAttributes<'button'> {
7
+ class?: string | null
8
+ className?: string
9
+ }
10
+
11
+ const {
12
+ class: classProp = '',
13
+ className = '',
14
+ type = 'button',
15
+ ...rest
16
+ } = Astro.props
17
+
18
+ const { classList, passthrough } = resolveLumenAstroProps(
19
+ rest,
20
+ ['ui-button', 'ui-button--outline', 'ui-button--default-size'],
21
+ classProp,
22
+ className
23
+ )
24
+ ---
25
+
26
+ <button
27
+ type={type}
28
+ class:list={classList}
29
+ data-ui-trigger
30
+ aria-haspopup="menu"
31
+ {...passthrough}
32
+ >
33
+ <slot />
34
+ </button>
@@ -0,0 +1,34 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types'
3
+
4
+ import { resolveLumenAstroProps } from '@santi020k/lumen-core'
5
+
6
+ interface Props extends HTMLAttributes<'div'> {
7
+ class?: string | null
8
+ className?: string
9
+ orientation?: 'horizontal' | 'vertical'
10
+ }
11
+
12
+ const {
13
+ class: classProp = '',
14
+ className = '',
15
+ orientation = 'horizontal',
16
+ ...rest
17
+ } = Astro.props
18
+
19
+ const { classList, passthrough } = resolveLumenAstroProps(
20
+ rest,
21
+ [],
22
+ classProp,
23
+ className
24
+ )
25
+ ---
26
+
27
+ <div
28
+ role="tablist"
29
+ aria-orientation={orientation}
30
+ class:list={classList}
31
+ {...passthrough}
32
+ >
33
+ <slot />
34
+ </div>
@@ -0,0 +1,35 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types'
3
+
4
+ import { resolveLumenAstroProps } from '@santi020k/lumen-core'
5
+
6
+ interface Props extends HTMLAttributes<'div'> {
7
+ class?: string | null
8
+ className?: string
9
+ value: string
10
+ }
11
+
12
+ const {
13
+ class: classProp = '',
14
+ className = '',
15
+ value,
16
+ ...rest
17
+ } = Astro.props
18
+
19
+ const { classList, passthrough } = resolveLumenAstroProps(
20
+ rest,
21
+ [],
22
+ classProp,
23
+ className
24
+ )
25
+ ---
26
+
27
+ <div
28
+ role="tabpanel"
29
+ data-value={value}
30
+ tabindex="0"
31
+ class:list={classList}
32
+ {...passthrough}
33
+ >
34
+ <slot />
35
+ </div>
@@ -0,0 +1,36 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types'
3
+
4
+ import { resolveLumenAstroProps } from '@santi020k/lumen-core'
5
+
6
+ interface Props extends HTMLAttributes<'button'> {
7
+ class?: string | null
8
+ className?: string
9
+ value: string
10
+ }
11
+
12
+ const {
13
+ class: classProp = '',
14
+ className = '',
15
+ type = 'button',
16
+ value,
17
+ ...rest
18
+ } = Astro.props
19
+
20
+ const { classList, passthrough } = resolveLumenAstroProps(
21
+ rest,
22
+ [],
23
+ classProp,
24
+ className
25
+ )
26
+ ---
27
+
28
+ <button
29
+ type={type}
30
+ role="tab"
31
+ data-value={value}
32
+ class:list={classList}
33
+ {...passthrough}
34
+ >
35
+ <slot />
36
+ </button>
@@ -0,0 +1,27 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types'
3
+
4
+ import { resolveLumenAstroProps } from '@santi020k/lumen-core'
5
+
6
+ interface Props extends HTMLAttributes<'span'> {
7
+ class?: string | null
8
+ className?: string
9
+ }
10
+
11
+ const {
12
+ class: classProp = '',
13
+ className = '',
14
+ ...rest
15
+ } = Astro.props
16
+
17
+ const { classList, passthrough } = resolveLumenAstroProps(
18
+ rest,
19
+ [],
20
+ classProp,
21
+ className
22
+ )
23
+ ---
24
+
25
+ <span role="tooltip" class:list={classList} {...passthrough}>
26
+ <slot />
27
+ </span>
package/index.ts CHANGED
@@ -11,6 +11,7 @@ export { default as AspectRatio } from './components/AspectRatio.astro'
11
11
  export { default as Attachment } from './components/Attachment.astro'
12
12
  export { default as Autocomplete } from './components/Autocomplete.astro'
13
13
  export { default as Avatar } from './components/Avatar.astro'
14
+ export { default as Backdrop } from './components/Backdrop.astro'
14
15
  export { default as BackToTop } from './components/BackToTop.astro'
15
16
  export { default as Badge } from './components/Badge.astro'
16
17
  export { default as BarChart } from './components/BarChart.astro'
@@ -51,6 +52,10 @@ export { default as Dialog } from './components/Dialog.astro'
51
52
  export { default as Direction } from './components/Direction.astro'
52
53
  export { default as Drawer } from './components/Drawer.astro'
53
54
  export { default as DropdownMenu } from './components/DropdownMenu.astro'
55
+ export { default as DropdownMenuContent } from './components/DropdownMenuContent.astro'
56
+ export { default as DropdownMenuItem } from './components/DropdownMenuItem.astro'
57
+ export { default as DropdownMenuSeparator } from './components/DropdownMenuSeparator.astro'
58
+ export { default as DropdownMenuTrigger } from './components/DropdownMenuTrigger.astro'
54
59
  export { default as Empty } from './components/Empty.astro'
55
60
  export { default as ErrorSummary } from './components/ErrorSummary.astro'
56
61
  export { default as Eyebrow } from './components/Eyebrow.astro'
@@ -61,9 +66,11 @@ export { default as FloatingBadge } from './components/FloatingBadge.astro'
61
66
  export { default as Form } from './components/Form.astro'
62
67
  export { default as FormattedDate } from './components/FormattedDate.astro'
63
68
  export { default as GradientDivider } from './components/GradientDivider.astro'
69
+ export { default as Graphic } from './components/Graphic.astro'
64
70
  export { default as Grid } from './components/Grid.astro'
65
71
  export { default as HoverCard } from './components/HoverCard.astro'
66
72
  export { default as Icon } from './components/Icon.astro'
73
+ export { default as Illustration } from './components/Illustration.astro'
67
74
  export { default as Image } from './components/Image.astro'
68
75
  export { default as Input } from './components/Input.astro'
69
76
  export { default as InputGroup } from './components/InputGroup.astro'
@@ -95,6 +102,8 @@ export { default as PieChart } from './components/PieChart.astro'
95
102
  export { default as Pill } from './components/Pill.astro'
96
103
  export { default as Popconfirm } from './components/Popconfirm.astro'
97
104
  export { default as Popover } from './components/Popover.astro'
105
+ export { default as PopoverPanel } from './components/PopoverPanel.astro'
106
+ export { default as PopoverTrigger } from './components/PopoverTrigger.astro'
98
107
  export { default as Progress } from './components/Progress.astro'
99
108
  export { default as Prose } from './components/Prose.astro'
100
109
  export { default as QRCode } from './components/QRCode.astro'
@@ -131,6 +140,9 @@ export { default as Stepper } from './components/Stepper.astro'
131
140
  export { default as Switch } from './components/Switch.astro'
132
141
  export { default as Table } from './components/Table.astro'
133
142
  export { default as Tabs } from './components/Tabs.astro'
143
+ export { default as TabsList } from './components/TabsList.astro'
144
+ export { default as TabsPanel } from './components/TabsPanel.astro'
145
+ export { default as TabsTrigger } from './components/TabsTrigger.astro'
134
146
  export { default as TagGroup } from './components/TagGroup.astro'
135
147
  export { default as Textarea } from './components/Textarea.astro'
136
148
  export { default as ThemeBuilder } from './components/ThemeBuilder.astro'
@@ -143,6 +155,7 @@ export { default as Toggle } from './components/Toggle.astro'
143
155
  export { default as ToggleGroup } from './components/ToggleGroup.astro'
144
156
  export { default as Toolbar } from './components/Toolbar.astro'
145
157
  export { default as Tooltip } from './components/Tooltip.astro'
158
+ export { default as TooltipContent } from './components/TooltipContent.astro'
146
159
  export { default as Tour } from './components/Tour.astro'
147
160
  export { default as Transfer } from './components/Transfer.astro'
148
161
  export { default as Tree } from './components/Tree.astro'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@santi020k/lumen-astro",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -37,8 +37,8 @@
37
37
  ],
38
38
  "dependencies": {
39
39
  "uqr": "^0.1.3",
40
- "@santi020k/lumen": "^1.2.0",
41
- "@santi020k/lumen-core": "^1.2.0"
40
+ "@santi020k/lumen": "^1.4.0",
41
+ "@santi020k/lumen-core": "^1.4.0"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@astrojs/ts-plugin": "^1.10.10",
@@ -15,13 +15,16 @@ import {
15
15
  exportThemeCss,
16
16
  exportThemeDesignTokens,
17
17
  exportThemeFigmaVariables,
18
+ formatLumenLanguageLabel,
19
+ getLumenLocalePair,
18
20
  getLumenRichTextShortcut,
19
21
  isLumenRichTextToggleCommand,
22
+ type LumenLocaleOption,
20
23
  type LumenRichTextChangeDetail,
21
24
  type LumenRichTextCommandDetail,
22
25
  type LumenThemeTokens,
23
- lumenTokenNames
24
- } from '@santi020k/lumen-core'
26
+ lumenTokenNames,
27
+ normalizeLumenLocales } from '@santi020k/lumen-core'
25
28
 
26
29
  /* eslint-disable complexity */
27
30
  /* Stateful DOM handlers stay local so Astro consumers do not need a framework runtime. */
@@ -237,6 +240,18 @@ const initDisclosures = (scope: ParentNode): void => {
237
240
  nextItem?.focus()
238
241
  })
239
242
 
243
+ if (root.hasAttribute('data-ui-dropdown-menu')) {
244
+ panel.addEventListener('click', event => {
245
+ const item = event.target instanceof Element ?
246
+ event.target.closest<HTMLElement>('[role="menuitem"]') :
247
+ null
248
+
249
+ if (!item || item.matches(':disabled, [aria-disabled="true"]')) return
250
+
251
+ setOpen(trigger, panel, false)
252
+ })
253
+ }
254
+
240
255
  if (root.hasAttribute('data-ui-hover-card')) {
241
256
  root.addEventListener('mouseenter', () => setOpen(trigger, panel, true))
242
257
 
@@ -933,6 +948,12 @@ const initTabs = (scope: ParentNode): void => {
933
948
  const panels = [...root.querySelectorAll<HTMLElement>('[role="tabpanel"]')]
934
949
  .filter(panel => panel.closest('[data-ui-tabs]') === root)
935
950
 
951
+ const tabList = tabs[0]?.closest<HTMLElement>('[role="tablist"]')
952
+
953
+ const orientation = tabList?.getAttribute('aria-orientation') === 'vertical' ?
954
+ 'vertical' :
955
+ 'horizontal'
956
+
936
957
  if (!tabs.length || !panels.length) continue
937
958
 
938
959
  const rootId = root.id || `ui-tabs-${++uiTabsCount}`
@@ -1034,13 +1055,22 @@ const initTabs = (scope: ParentNode): void => {
1034
1055
  tab.addEventListener('click', () => activate(tab))
1035
1056
 
1036
1057
  tab.addEventListener('keydown', event => {
1037
- const keys = ['ArrowLeft', 'ArrowRight', 'Home', 'End']
1058
+ const keys = orientation === 'vertical' ?
1059
+ ['ArrowUp', 'ArrowDown', 'Home', 'End'] :
1060
+ ['ArrowLeft', 'ArrowRight', 'Home', 'End']
1038
1061
 
1039
1062
  if (!keys.includes(event.key)) return
1040
1063
 
1041
1064
  event.preventDefault()
1042
1065
 
1043
- const nextTab = tabs[getLoopedIndex(event.key, index, tabs.length, ['ArrowRight'])]
1066
+ const nextTab = tabs[
1067
+ getLoopedIndex(
1068
+ event.key,
1069
+ index,
1070
+ tabs.length,
1071
+ orientation === 'vertical' ? ['ArrowDown'] : ['ArrowRight']
1072
+ )
1073
+ ]
1044
1074
 
1045
1075
  if (nextTab) {
1046
1076
  activate(nextTab)
@@ -1282,6 +1312,109 @@ const initThemeToggles = (scope: ParentNode): void => {
1282
1312
  }
1283
1313
  }
1284
1314
 
1315
+ const isUnknownArray = (value: unknown): value is unknown[] => Array.isArray(value)
1316
+
1317
+ const parseLanguageToggleLocales = (value: string | undefined): readonly LumenLocaleOption[] => {
1318
+ if (!value) return normalizeLumenLocales(undefined)
1319
+
1320
+ try {
1321
+ const parsed: unknown = JSON.parse(value)
1322
+
1323
+ if (!isUnknownArray(parsed)) return normalizeLumenLocales(undefined)
1324
+
1325
+ const locales = parsed.flatMap(item => {
1326
+ if (!item || typeof item !== 'object' || !('label' in item) || !('value' in item)) return []
1327
+
1328
+ const { label, value: localeValue } = item
1329
+
1330
+ return typeof label === 'string' && typeof localeValue === 'string' ?
1331
+ [{ label, value: localeValue }] :
1332
+ []
1333
+ })
1334
+
1335
+ return normalizeLumenLocales(locales)
1336
+ } catch {
1337
+ return normalizeLumenLocales(undefined)
1338
+ }
1339
+ }
1340
+
1341
+ const initLanguageToggles = (scope: ParentNode): void => {
1342
+ for (const control of scope.querySelectorAll<HTMLButtonElement>('[data-ui-language-toggle]')) {
1343
+ if (control.dataset.uiBound === 'true') continue
1344
+
1345
+ control.dataset.uiBound = 'true'
1346
+
1347
+ const locales = parseLanguageToggleLocales(control.dataset.uiLanguageLocales)
1348
+ const storageKey = control.dataset.uiLanguageStorageKey
1349
+ const controlled = control.hasAttribute('data-ui-language-controlled')
1350
+
1351
+ const labelTemplate = control.dataset.uiLanguageLabelTemplate ??
1352
+ 'Change language from {current} to {next}'
1353
+
1354
+ const sync = (value: string): void => {
1355
+ const { current, next } = getLumenLocalePair(locales, value)
1356
+ const label = control.querySelector<HTMLElement>('[data-ui-language-label]')
1357
+
1358
+ control.dataset.uiLanguageValue = current.value
1359
+
1360
+ control.dataset.uiLanguageNextValue = next.value
1361
+
1362
+ control.setAttribute('aria-label', formatLumenLanguageLabel(labelTemplate, current, next))
1363
+
1364
+ if (label) {
1365
+ label.lang = next.value
1366
+
1367
+ label.textContent = next.label
1368
+ }
1369
+ }
1370
+
1371
+ let storedValue: string | null = null
1372
+
1373
+ if (!controlled && storageKey) {
1374
+ try {
1375
+ storedValue = localStorage.getItem(storageKey)
1376
+ } catch {
1377
+ // Storage can be unavailable in privacy modes.
1378
+ }
1379
+ }
1380
+
1381
+ const documentValue = document.documentElement.lang || undefined
1382
+
1383
+ const initialValue = controlled ?
1384
+ control.dataset.uiLanguageValue :
1385
+ storedValue ?? control.dataset.uiLanguageInitialValue ?? documentValue
1386
+
1387
+ const initialPair = getLumenLocalePair(locales, initialValue)
1388
+
1389
+ sync(initialPair.current.value)
1390
+
1391
+ if (!controlled) document.documentElement.lang = initialPair.current.value
1392
+
1393
+ control.addEventListener('click', () => {
1394
+ const { current, next } = getLumenLocalePair(locales, control.dataset.uiLanguageValue)
1395
+
1396
+ if (!controlled) {
1397
+ document.documentElement.lang = next.value
1398
+
1399
+ sync(next.value)
1400
+
1401
+ if (storageKey) {
1402
+ try {
1403
+ localStorage.setItem(storageKey, next.value)
1404
+ } catch {
1405
+ // Storage can be unavailable in privacy modes.
1406
+ }
1407
+ }
1408
+ }
1409
+
1410
+ control.dispatchEvent(new CustomEvent('ui:language-change', {
1411
+ bubbles: true,
1412
+ detail: { previousValue: current.value, value: next.value }
1413
+ }))
1414
+ })
1415
+ }
1416
+ }
1417
+
1285
1418
  const getVisibleItems = (items: HTMLElement[]): HTMLElement[] => items.filter(item => !item.hidden &&
1286
1419
  item.offsetParent !== null &&
1287
1420
  !item.hasAttribute('disabled') &&
@@ -5460,6 +5593,8 @@ const initUiPrimitives = (scope: ParentNode = document): void => {
5460
5593
 
5461
5594
  initThemeToggles(scope)
5462
5595
 
5596
+ initLanguageToggles(scope)
5597
+
5463
5598
  initCommand(scope)
5464
5599
 
5465
5600
  initComboboxes(scope)