@santi020k/lumen-astro 1.2.0 → 1.3.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,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>
@@ -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
@@ -51,6 +51,10 @@ export { default as Dialog } from './components/Dialog.astro'
51
51
  export { default as Direction } from './components/Direction.astro'
52
52
  export { default as Drawer } from './components/Drawer.astro'
53
53
  export { default as DropdownMenu } from './components/DropdownMenu.astro'
54
+ export { default as DropdownMenuContent } from './components/DropdownMenuContent.astro'
55
+ export { default as DropdownMenuItem } from './components/DropdownMenuItem.astro'
56
+ export { default as DropdownMenuSeparator } from './components/DropdownMenuSeparator.astro'
57
+ export { default as DropdownMenuTrigger } from './components/DropdownMenuTrigger.astro'
54
58
  export { default as Empty } from './components/Empty.astro'
55
59
  export { default as ErrorSummary } from './components/ErrorSummary.astro'
56
60
  export { default as Eyebrow } from './components/Eyebrow.astro'
@@ -95,6 +99,8 @@ export { default as PieChart } from './components/PieChart.astro'
95
99
  export { default as Pill } from './components/Pill.astro'
96
100
  export { default as Popconfirm } from './components/Popconfirm.astro'
97
101
  export { default as Popover } from './components/Popover.astro'
102
+ export { default as PopoverPanel } from './components/PopoverPanel.astro'
103
+ export { default as PopoverTrigger } from './components/PopoverTrigger.astro'
98
104
  export { default as Progress } from './components/Progress.astro'
99
105
  export { default as Prose } from './components/Prose.astro'
100
106
  export { default as QRCode } from './components/QRCode.astro'
@@ -131,6 +137,9 @@ export { default as Stepper } from './components/Stepper.astro'
131
137
  export { default as Switch } from './components/Switch.astro'
132
138
  export { default as Table } from './components/Table.astro'
133
139
  export { default as Tabs } from './components/Tabs.astro'
140
+ export { default as TabsList } from './components/TabsList.astro'
141
+ export { default as TabsPanel } from './components/TabsPanel.astro'
142
+ export { default as TabsTrigger } from './components/TabsTrigger.astro'
134
143
  export { default as TagGroup } from './components/TagGroup.astro'
135
144
  export { default as Textarea } from './components/Textarea.astro'
136
145
  export { default as ThemeBuilder } from './components/ThemeBuilder.astro'
@@ -143,6 +152,7 @@ export { default as Toggle } from './components/Toggle.astro'
143
152
  export { default as ToggleGroup } from './components/ToggleGroup.astro'
144
153
  export { default as Toolbar } from './components/Toolbar.astro'
145
154
  export { default as Tooltip } from './components/Tooltip.astro'
155
+ export { default as TooltipContent } from './components/TooltipContent.astro'
146
156
  export { default as Tour } from './components/Tour.astro'
147
157
  export { default as Transfer } from './components/Transfer.astro'
148
158
  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.3.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.3.0",
41
+ "@santi020k/lumen-core": "^1.3.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)