@santi020k/lumen-astro 1.1.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,88 @@ 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
+
120
+ ## Context navigation
121
+
122
+ Use `ContextNavigation` below a primary header when the available links change with a selected
123
+ platform, product, or workspace. Put the stable selector or identity in the `context` slot and one
124
+ independently named `NavigationMenu` in the default slot. Long link sets scroll horizontally.
125
+
126
+ ```astro
127
+ ---
128
+ import { ContextNavigation, NativeSelect, NavigationMenu } from '@santi020k/lumen-astro'
129
+ ---
130
+
131
+ <ContextNavigation>
132
+ <NativeSelect slot="context" aria-label="Documentation platform" visualSize="sm">
133
+ <option>Web</option>
134
+ <option>Apple</option>
135
+ <option>Android</option>
136
+ </NativeSelect>
137
+ <NavigationMenu aria-label="Web documentation" variant="unstyled">
138
+ <a href="/docs/web" aria-current="page">Overview</a>
139
+ <a href="/docs/components">Components</a>
140
+ <a href="/docs/web/playground">Playground</a>
141
+ </NavigationMenu>
142
+ </ContextNavigation>
143
+ ```
144
+
63
145
  ## Forms and Astro Actions
64
146
 
65
147
  `Form` renders a semantic form and preserves native `method`, `action`, `enctype`, autocomplete,
@@ -227,6 +309,14 @@ with `--ui-logo-delay`.
227
309
 
228
310
  The package ships the complete Astro primitive catalog plus a small progressive-enhancement runtime for dialogs, popovers, tabs, menus, command filtering, carousels, and toasts.
229
311
 
312
+ ## Kanban boards
313
+
314
+ Compose `KanbanBoard` and `KanbanColumn` with ordinary `Card` items. Give every item a stable
315
+ `data-ui-kanban-item` and add a dedicated button with `data-ui-kanban-handle`. `UIPrimitives` emits
316
+ the cancellable `ui:kanban-move-request` event for keyboard, mouse, and touch movement without
317
+ changing the DOM. Keep persistence, pending state, rollback, and status rules in the application.
318
+ Use `Empty variant="compact"` inside empty columns and `Skeleton` cards while loading.
319
+
230
320
  ## Motion
231
321
 
232
322
  Use the shared `fast`, `standard`, and `slow` duration vocabulary across motion primitives.
@@ -0,0 +1,37 @@
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
+ variant?: 'default' | 'unstyled'
10
+ }
11
+
12
+ const {
13
+ class: classProp = '',
14
+ className = '',
15
+ variant = 'default',
16
+ ...rest
17
+ } = Astro.props
18
+
19
+ const { classList, passthrough } = resolveLumenAstroProps(rest, [
20
+ 'ui-context-navigation',
21
+ variant === 'unstyled' && 'ui-context-navigation--unstyled'
22
+ ], classProp, className)
23
+ ---
24
+
25
+ <div
26
+ class:list={classList}
27
+ data-slot="context-navigation"
28
+ data-variant={variant}
29
+ {...passthrough}
30
+ >
31
+ {Astro.slots.has('context') && (
32
+ <div class="ui-context-navigation__context" data-slot="context-navigation-context">
33
+ <slot name="context" />
34
+ </div>
35
+ )}
36
+ <slot />
37
+ </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>
@@ -7,17 +7,20 @@ interface Props extends HTMLAttributes<'section'> {
7
7
  class?: string | null
8
8
  className?: string
9
9
  glass?: boolean | 'subtle' | 'strong'
10
+ variant?: 'compact' | 'default'
10
11
  }
11
12
 
12
13
  const {
13
14
  class: classProp = '',
14
15
  className = '',
15
16
  glass = false,
17
+ variant = 'default',
16
18
  ...rest
17
19
  } = Astro.props
18
20
 
19
21
  const { classList, passthrough } = resolveLumenAstroProps(rest, [
20
22
  'ui-empty',
23
+ variant === 'compact' && 'ui-empty--compact',
21
24
  glass && 'ui-empty--glass',
22
25
  glass === 'subtle' && 'ui-glass-subtle',
23
26
  glass === 'strong' && 'ui-glass-strong'
@@ -0,0 +1,32 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types'
3
+
4
+ import { resolveLumenAstroProps } from '@santi020k/lumen-core'
5
+
6
+ interface Props extends HTMLAttributes<'section'> {
7
+ class?: string | null
8
+ className?: string
9
+ }
10
+
11
+ const {
12
+ class: classProp = '',
13
+ className = '',
14
+ tabindex = '0',
15
+ ...rest
16
+ } = Astro.props
17
+
18
+ const { classList, passthrough } = resolveLumenAstroProps(rest, [
19
+ 'ui-kanban-board',
20
+ 'ui-kanban'
21
+ ], classProp, className)
22
+ ---
23
+
24
+ <section
25
+ aria-orientation="horizontal"
26
+ class:list={classList}
27
+ data-ui-kanban
28
+ tabindex={tabindex}
29
+ {...passthrough}
30
+ >
31
+ <slot />
32
+ </section>
@@ -0,0 +1,30 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types'
3
+
4
+ import { resolveLumenAstroProps } from '@santi020k/lumen-core'
5
+
6
+ interface Props extends HTMLAttributes<'section'> {
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(rest, [
20
+ 'ui-kanban__column'
21
+ ], classProp, className)
22
+ ---
23
+
24
+ <section
25
+ class:list={classList}
26
+ data-ui-kanban-column={value}
27
+ {...passthrough}
28
+ >
29
+ <slot />
30
+ </section>
@@ -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
@@ -40,6 +40,7 @@ export { default as Combobox } from './components/Combobox.astro'
40
40
  export { default as Command } from './components/Command.astro'
41
41
  export { default as Container } from './components/Container.astro'
42
42
  export { default as ContextMenu } from './components/ContextMenu.astro'
43
+ export { default as ContextNavigation } from './components/ContextNavigation.astro'
43
44
  export { default as CopyButton } from './components/CopyButton.astro'
44
45
  export { default as CoverImage } from './components/CoverImage.astro'
45
46
  export { default as DataTable } from './components/DataTable.astro'
@@ -50,6 +51,10 @@ export { default as Dialog } from './components/Dialog.astro'
50
51
  export { default as Direction } from './components/Direction.astro'
51
52
  export { default as Drawer } from './components/Drawer.astro'
52
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'
53
58
  export { default as Empty } from './components/Empty.astro'
54
59
  export { default as ErrorSummary } from './components/ErrorSummary.astro'
55
60
  export { default as Eyebrow } from './components/Eyebrow.astro'
@@ -68,6 +73,8 @@ export { default as Input } from './components/Input.astro'
68
73
  export { default as InputGroup } from './components/InputGroup.astro'
69
74
  export { default as InputOTP } from './components/InputOTP.astro'
70
75
  export { default as Item } from './components/Item.astro'
76
+ export { default as KanbanBoard } from './components/KanbanBoard.astro'
77
+ export { default as KanbanColumn } from './components/KanbanColumn.astro'
71
78
  export { default as Kbd } from './components/Kbd.astro'
72
79
  export { default as Label } from './components/Label.astro'
73
80
  export { default as LanguageToggle } from './components/LanguageToggle.astro'
@@ -92,6 +99,8 @@ export { default as PieChart } from './components/PieChart.astro'
92
99
  export { default as Pill } from './components/Pill.astro'
93
100
  export { default as Popconfirm } from './components/Popconfirm.astro'
94
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'
95
104
  export { default as Progress } from './components/Progress.astro'
96
105
  export { default as Prose } from './components/Prose.astro'
97
106
  export { default as QRCode } from './components/QRCode.astro'
@@ -128,6 +137,9 @@ export { default as Stepper } from './components/Stepper.astro'
128
137
  export { default as Switch } from './components/Switch.astro'
129
138
  export { default as Table } from './components/Table.astro'
130
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'
131
143
  export { default as TagGroup } from './components/TagGroup.astro'
132
144
  export { default as Textarea } from './components/Textarea.astro'
133
145
  export { default as ThemeBuilder } from './components/ThemeBuilder.astro'
@@ -140,6 +152,7 @@ export { default as Toggle } from './components/Toggle.astro'
140
152
  export { default as ToggleGroup } from './components/ToggleGroup.astro'
141
153
  export { default as Toolbar } from './components/Toolbar.astro'
142
154
  export { default as Tooltip } from './components/Tooltip.astro'
155
+ export { default as TooltipContent } from './components/TooltipContent.astro'
143
156
  export { default as Tour } from './components/Tour.astro'
144
157
  export { default as Transfer } from './components/Transfer.astro'
145
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.1.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.1.0",
41
- "@santi020k/lumen-core": "^1.1.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",
@@ -57,10 +57,19 @@
57
57
  "url": "https://github.com/santi020k/lumen.git",
58
58
  "directory": "packages/astro"
59
59
  },
60
- "homepage": "https://lumen.santi020k.com",
60
+ "homepage": "https://lumen.santi020k.com/docs/frameworks/astro",
61
61
  "bugs": {
62
62
  "url": "https://github.com/santi020k/lumen/issues"
63
63
  },
64
+ "keywords": [
65
+ "astro",
66
+ "astro-components",
67
+ "accessible-ui",
68
+ "component-library",
69
+ "design-system",
70
+ "lumen",
71
+ "ui"
72
+ ],
64
73
  "author": {
65
74
  "name": "Santiago Molina",
66
75
  "url": "https://santi020k.com"
@@ -10,17 +10,21 @@
10
10
 
11
11
  <script>
12
12
  import {
13
+ createLumenKanbanMoveDetail,
13
14
  createThemeFromHue,
14
15
  exportThemeCss,
15
16
  exportThemeDesignTokens,
16
17
  exportThemeFigmaVariables,
18
+ formatLumenLanguageLabel,
19
+ getLumenLocalePair,
17
20
  getLumenRichTextShortcut,
18
21
  isLumenRichTextToggleCommand,
22
+ type LumenLocaleOption,
19
23
  type LumenRichTextChangeDetail,
20
24
  type LumenRichTextCommandDetail,
21
25
  type LumenThemeTokens,
22
- lumenTokenNames
23
- } from '@santi020k/lumen-core'
26
+ lumenTokenNames,
27
+ normalizeLumenLocales } from '@santi020k/lumen-core'
24
28
 
25
29
  /* eslint-disable complexity */
26
30
  /* Stateful DOM handlers stay local so Astro consumers do not need a framework runtime. */
@@ -236,6 +240,18 @@ const initDisclosures = (scope: ParentNode): void => {
236
240
  nextItem?.focus()
237
241
  })
238
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
+
239
255
  if (root.hasAttribute('data-ui-hover-card')) {
240
256
  root.addEventListener('mouseenter', () => setOpen(trigger, panel, true))
241
257
 
@@ -932,6 +948,12 @@ const initTabs = (scope: ParentNode): void => {
932
948
  const panels = [...root.querySelectorAll<HTMLElement>('[role="tabpanel"]')]
933
949
  .filter(panel => panel.closest('[data-ui-tabs]') === root)
934
950
 
951
+ const tabList = tabs[0]?.closest<HTMLElement>('[role="tablist"]')
952
+
953
+ const orientation = tabList?.getAttribute('aria-orientation') === 'vertical' ?
954
+ 'vertical' :
955
+ 'horizontal'
956
+
935
957
  if (!tabs.length || !panels.length) continue
936
958
 
937
959
  const rootId = root.id || `ui-tabs-${++uiTabsCount}`
@@ -1033,13 +1055,22 @@ const initTabs = (scope: ParentNode): void => {
1033
1055
  tab.addEventListener('click', () => activate(tab))
1034
1056
 
1035
1057
  tab.addEventListener('keydown', event => {
1036
- const keys = ['ArrowLeft', 'ArrowRight', 'Home', 'End']
1058
+ const keys = orientation === 'vertical' ?
1059
+ ['ArrowUp', 'ArrowDown', 'Home', 'End'] :
1060
+ ['ArrowLeft', 'ArrowRight', 'Home', 'End']
1037
1061
 
1038
1062
  if (!keys.includes(event.key)) return
1039
1063
 
1040
1064
  event.preventDefault()
1041
1065
 
1042
- 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
+ ]
1043
1074
 
1044
1075
  if (nextTab) {
1045
1076
  activate(nextTab)
@@ -1281,6 +1312,109 @@ const initThemeToggles = (scope: ParentNode): void => {
1281
1312
  }
1282
1313
  }
1283
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
+
1284
1418
  const getVisibleItems = (items: HTMLElement[]): HTMLElement[] => items.filter(item => !item.hidden &&
1285
1419
  item.offsetParent !== null &&
1286
1420
  !item.hasAttribute('disabled') &&
@@ -3237,6 +3371,239 @@ const initSchedules = (scope: ParentNode): void => {
3237
3371
  }
3238
3372
  }
3239
3373
 
3374
+ const kanbanColumnSelector = '[data-ui-kanban-column]'
3375
+
3376
+ const initKanbanBoards = (scope: ParentNode): void => {
3377
+ for (const root of scope.querySelectorAll<HTMLElement>('[data-ui-kanban]')) {
3378
+ if (root.dataset.uiKanbanBound === 'true') continue
3379
+
3380
+ root.dataset.uiKanbanBound = 'true'
3381
+
3382
+ for (const handle of root.querySelectorAll<HTMLElement>('[data-ui-kanban-handle]'))
3383
+ handle.draggable = true
3384
+
3385
+ let pointer: {
3386
+ active: boolean
3387
+ handle: HTMLElement
3388
+ item: HTMLElement
3389
+ pointerId: number
3390
+ startX: number
3391
+ startY: number
3392
+ } | undefined
3393
+
3394
+ const itemFor = (target: EventTarget | null): HTMLElement | null => target instanceof Element ?
3395
+ target.closest<HTMLElement>('[data-ui-kanban-item]') :
3396
+ null
3397
+
3398
+ const columnFor = (target: EventTarget | null): HTMLElement | null => target instanceof Element ?
3399
+ target.closest<HTMLElement>(kanbanColumnSelector) :
3400
+ null
3401
+
3402
+ const clearState = (): void => {
3403
+ pointer = undefined
3404
+
3405
+ for (const item of root.querySelectorAll<HTMLElement>('[data-ui-kanban-item]'))
3406
+ if (item.dataset.state === 'dragging') delete item.dataset.state
3407
+
3408
+ for (const column of root.querySelectorAll<HTMLElement>(kanbanColumnSelector))
3409
+ if (column.dataset.state === 'drop-target') delete column.dataset.state
3410
+ }
3411
+
3412
+ const announce = (column: HTMLElement): void => {
3413
+ let live = root.querySelector<HTMLElement>('[data-ui-kanban-live]')
3414
+
3415
+ if (!live) {
3416
+ live = document.createElement('span')
3417
+
3418
+ live.className = 'ui-visually-hidden'
3419
+
3420
+ live.dataset.uiKanbanLive = ''
3421
+
3422
+ live.setAttribute('aria-live', 'polite')
3423
+
3424
+ live.setAttribute('role', 'status')
3425
+
3426
+ root.append(live)
3427
+ }
3428
+
3429
+ const label = column.getAttribute('aria-label') ||
3430
+ column.querySelector('h2, h3, [data-ui-kanban-column-label]')?.textContent?.trim() ||
3431
+ column.dataset.uiKanbanColumn || ''
3432
+
3433
+ live.textContent = `Move requested to ${label}.`
3434
+ }
3435
+
3436
+ const requestMove = (
3437
+ item: HTMLElement,
3438
+ targetColumn: HTMLElement,
3439
+ input: 'keyboard' | 'pointer'
3440
+ ): void => {
3441
+ const fromColumn = item.closest<HTMLElement>(kanbanColumnSelector)
3442
+ ?.dataset.uiKanbanColumn ?? ''
3443
+
3444
+ const detail = createLumenKanbanMoveDetail({
3445
+ fromColumn,
3446
+ input,
3447
+ itemId: item.dataset.uiKanbanItem ?? item.id,
3448
+ toColumn: targetColumn.dataset.uiKanbanColumn ?? ''
3449
+ })
3450
+
3451
+ if (!detail) return
3452
+
3453
+ const accepted = root.dispatchEvent(new CustomEvent('ui:kanban-move-request', {
3454
+ bubbles: true,
3455
+ cancelable: true,
3456
+ detail
3457
+ }))
3458
+
3459
+ if (accepted) announce(targetColumn)
3460
+ }
3461
+
3462
+ root.addEventListener('dragstart', event => {
3463
+ const handle = event.target instanceof Element ?
3464
+ event.target.closest<HTMLElement>('[data-ui-kanban-handle]') :
3465
+ null
3466
+
3467
+ const item = itemFor(handle)
3468
+
3469
+ if (!handle || !item || item.getAttribute('aria-busy') === 'true') {
3470
+ event.preventDefault()
3471
+
3472
+ return
3473
+ }
3474
+
3475
+ event.dataTransfer?.setData('text/plain', item.dataset.uiKanbanItem ?? item.id)
3476
+
3477
+ if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move'
3478
+
3479
+ item.dataset.state = 'dragging'
3480
+ })
3481
+
3482
+ root.addEventListener('dragend', clearState)
3483
+
3484
+ root.addEventListener('dragover', event => {
3485
+ const column = columnFor(event.target)
3486
+
3487
+ if (!column) return
3488
+
3489
+ event.preventDefault()
3490
+
3491
+ column.dataset.state = 'drop-target'
3492
+
3493
+ if (event.dataTransfer) event.dataTransfer.dropEffect = 'move'
3494
+ })
3495
+
3496
+ root.addEventListener('dragleave', event => {
3497
+ const column = columnFor(event.target)
3498
+
3499
+ if (
3500
+ !column ||
3501
+ (event.relatedTarget instanceof Node && column.contains(event.relatedTarget))
3502
+ ) return
3503
+
3504
+ delete column.dataset.state
3505
+ })
3506
+
3507
+ root.addEventListener('drop', event => {
3508
+ const column = columnFor(event.target)
3509
+ const itemId = event.dataTransfer?.getData('text/plain') ?? ''
3510
+
3511
+ const item = [...root.querySelectorAll<HTMLElement>('[data-ui-kanban-item]')]
3512
+ .find(candidate => (candidate.dataset.uiKanbanItem ?? candidate.id) === itemId)
3513
+
3514
+ if (!column || !item) return
3515
+
3516
+ event.preventDefault()
3517
+
3518
+ requestMove(item, column, 'pointer')
3519
+
3520
+ clearState()
3521
+ })
3522
+
3523
+ root.addEventListener('keydown', event => {
3524
+ if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return
3525
+
3526
+ const handle = event.target instanceof Element ?
3527
+ event.target.closest<HTMLElement>('[data-ui-kanban-handle]') :
3528
+ null
3529
+
3530
+ const item = itemFor(handle)
3531
+ const currentColumn = columnFor(item)
3532
+
3533
+ if (!handle || !item || !currentColumn || item.getAttribute('aria-busy') === 'true') return
3534
+
3535
+ const columns = [...root.querySelectorAll<HTMLElement>(kanbanColumnSelector)]
3536
+ const currentIndex = columns.indexOf(currentColumn)
3537
+ const direction = event.key === 'ArrowRight' ? 1 : -1
3538
+ const rtl = getComputedStyle(root).direction === 'rtl'
3539
+ const target = columns[currentIndex + (rtl ? -direction : direction)]
3540
+
3541
+ if (!target) return
3542
+
3543
+ event.preventDefault()
3544
+
3545
+ requestMove(item, target, 'keyboard')
3546
+ })
3547
+
3548
+ root.addEventListener('pointerdown', event => {
3549
+ if (event.pointerType === 'mouse') return
3550
+
3551
+ const handle = event.target instanceof Element ?
3552
+ event.target.closest<HTMLElement>('[data-ui-kanban-handle]') :
3553
+ null
3554
+
3555
+ const item = itemFor(handle)
3556
+
3557
+ if (!handle || !item || item.getAttribute('aria-busy') === 'true') return
3558
+
3559
+ pointer = {
3560
+ active: false,
3561
+ handle,
3562
+ item,
3563
+ pointerId: event.pointerId,
3564
+ startX: event.clientX,
3565
+ startY: event.clientY
3566
+ }
3567
+ })
3568
+
3569
+ root.addEventListener('pointermove', event => {
3570
+ if (!pointer || pointer.pointerId !== event.pointerId) return
3571
+
3572
+ if (!pointer.active && Math.hypot(
3573
+ event.clientX - pointer.startX,
3574
+ event.clientY - pointer.startY
3575
+ ) < 8) return
3576
+
3577
+ pointer.active = true
3578
+
3579
+ pointer.handle.setPointerCapture(event.pointerId)
3580
+
3581
+ pointer.item.dataset.state = 'dragging'
3582
+
3583
+ const target = document.elementFromPoint(event.clientX, event.clientY)
3584
+ ?.closest<HTMLElement>(kanbanColumnSelector)
3585
+
3586
+ for (const column of root.querySelectorAll<HTMLElement>(kanbanColumnSelector)) {
3587
+ if (column === target) column.dataset.state = 'drop-target'
3588
+ else delete column.dataset.state
3589
+ }
3590
+ })
3591
+
3592
+ root.addEventListener('pointerup', event => {
3593
+ if (!pointer || pointer.pointerId !== event.pointerId) return
3594
+
3595
+ const target = document.elementFromPoint(event.clientX, event.clientY)
3596
+ ?.closest<HTMLElement>(kanbanColumnSelector)
3597
+
3598
+ if (pointer.active && target) requestMove(pointer.item, target, 'pointer')
3599
+
3600
+ clearState()
3601
+ })
3602
+
3603
+ root.addEventListener('pointercancel', clearState)
3604
+ }
3605
+ }
3606
+
3240
3607
  const initTreeCollections = (scope: ParentNode): void => {
3241
3608
  for (const root of scope.querySelectorAll<HTMLElement>('[role="tree"], [role="treegrid"]')) {
3242
3609
  if (root.dataset.uiBound === 'true') continue
@@ -5226,6 +5593,8 @@ const initUiPrimitives = (scope: ParentNode = document): void => {
5226
5593
 
5227
5594
  initThemeToggles(scope)
5228
5595
 
5596
+ initLanguageToggles(scope)
5597
+
5229
5598
  initCommand(scope)
5230
5599
 
5231
5600
  initComboboxes(scope)
@@ -5252,6 +5621,8 @@ const initUiPrimitives = (scope: ParentNode = document): void => {
5252
5621
 
5253
5622
  initSchedules(scope)
5254
5623
 
5624
+ initKanbanBoards(scope)
5625
+
5255
5626
  initTreeCollections(scope)
5256
5627
 
5257
5628
  initVirtualLists(scope)