luxen-ui 0.18.3 → 0.19.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.
Files changed (70) hide show
  1. package/README.md +1 -0
  2. package/cdn/color-scheme.d.ts +76 -0
  3. package/cdn/color-scheme.d.ts.map +1 -0
  4. package/cdn/color-scheme.js +2 -0
  5. package/cdn/color-scheme.js.map +1 -0
  6. package/cdn/config.d.ts +23 -0
  7. package/cdn/config.d.ts.map +1 -1
  8. package/cdn/custom-elements.json +136 -1
  9. package/cdn/elements/color-scheme-icon/color-scheme-icon.d.ts +55 -0
  10. package/cdn/elements/color-scheme-icon/color-scheme-icon.d.ts.map +1 -0
  11. package/cdn/elements/color-scheme-icon/color-scheme-icon.js +44 -0
  12. package/cdn/elements/color-scheme-icon/color-scheme-icon.js.map +1 -0
  13. package/cdn/elements/color-scheme-icon/index.d.ts +2 -0
  14. package/cdn/elements/color-scheme-icon/index.d.ts.map +1 -0
  15. package/cdn/elements/color-scheme-icon/index.js +2 -0
  16. package/cdn/elements/color-scheme-icon/index.js.map +1 -0
  17. package/cdn/elements/dropdown-item/dropdown-item.d.ts +9 -1
  18. package/cdn/elements/dropdown-item/dropdown-item.d.ts.map +1 -1
  19. package/cdn/elements/dropdown-item/dropdown-item.js +32 -23
  20. package/cdn/elements/dropdown-item/dropdown-item.js.map +1 -1
  21. package/cdn/elements/segmented-control/segmented-control.d.ts +2 -0
  22. package/cdn/elements/segmented-control/segmented-control.d.ts.map +1 -1
  23. package/cdn/elements/segmented-control/segmented-control.js +1 -1
  24. package/cdn/elements/segmented-control/segmented-control.js.map +1 -1
  25. package/cdn/elements/tabs/tabs.d.ts +7 -2
  26. package/cdn/elements/tabs/tabs.d.ts.map +1 -1
  27. package/cdn/elements/tabs/tabs.js.map +1 -1
  28. package/cdn/index.d.ts +2 -0
  29. package/cdn/index.d.ts.map +1 -1
  30. package/cdn/index.js +1 -1
  31. package/cdn/registry.d.ts +1 -1
  32. package/cdn/registry.d.ts.map +1 -1
  33. package/cdn/registry.js.map +1 -1
  34. package/cdn/standalone.js.map +1 -1
  35. package/dist/color-scheme.d.ts +76 -0
  36. package/dist/color-scheme.d.ts.map +1 -0
  37. package/dist/color-scheme.js +203 -0
  38. package/dist/config.d.ts +23 -0
  39. package/dist/config.d.ts.map +1 -1
  40. package/dist/custom-elements.json +136 -1
  41. package/dist/elements/color-scheme-icon/color-scheme-icon.css +98 -0
  42. package/dist/elements/color-scheme-icon/color-scheme-icon.d.ts +55 -0
  43. package/dist/elements/color-scheme-icon/color-scheme-icon.d.ts.map +1 -0
  44. package/dist/elements/color-scheme-icon/color-scheme-icon.js +138 -0
  45. package/dist/elements/color-scheme-icon/index.d.ts +2 -0
  46. package/dist/elements/color-scheme-icon/index.d.ts.map +1 -0
  47. package/dist/elements/color-scheme-icon/index.js +4 -0
  48. package/dist/elements/dropdown-item/dropdown-item.d.ts +9 -1
  49. package/dist/elements/dropdown-item/dropdown-item.d.ts.map +1 -1
  50. package/dist/elements/dropdown-item/dropdown-item.js +49 -26
  51. package/dist/elements/segmented-control/segmented-control.d.ts +2 -0
  52. package/dist/elements/segmented-control/segmented-control.d.ts.map +1 -1
  53. package/dist/elements/segmented-control/segmented-control.js +11 -1
  54. package/dist/elements/tabs/tabs.d.ts +7 -2
  55. package/dist/elements/tabs/tabs.d.ts.map +1 -1
  56. package/dist/elements/tabs/tabs.js +7 -2
  57. package/dist/index.d.ts +2 -0
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +1 -0
  60. package/dist/metadata/color-scheme-icon.json +66 -0
  61. package/dist/metadata/dropdown-item.json +9 -1
  62. package/dist/metadata/index.json +76 -2
  63. package/dist/registry.d.ts +1 -1
  64. package/dist/registry.d.ts.map +1 -1
  65. package/dist/templates/elements/color-scheme-icon.md +279 -0
  66. package/dist/templates/elements/dropdown.md +54 -1
  67. package/dist/templates/elements/tabs.md +4 -1
  68. package/elements.json +8 -0
  69. package/package.json +5 -1
  70. package/vite-plugin.ts +71 -8
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.18.3",
2
+ "version": "0.19.0",
3
3
  "elements": [
4
4
  {
5
5
  "name": "alert",
@@ -2313,6 +2313,14 @@
2313
2313
  "reflects": true,
2314
2314
  "description": "Whether this item's submenu is open. Managed by the parent `l-dropdown`."
2315
2315
  },
2316
+ {
2317
+ "name": "checkPlacement",
2318
+ "attribute": "check-placement",
2319
+ "type": "'start' | 'end'",
2320
+ "default": "'start'",
2321
+ "reflects": true,
2322
+ "description": "Which side of a `type=\"checkbox\"` item carries the check mark. `start` (the\ndefault) puts it in the leading column, where it takes the place of any\n`prefix`. `end` moves it to the trailing edge, freeing that column so the\nitem can show its own icon alongside the state."
2323
+ },
2316
2324
  {
2317
2325
  "name": "hasSubmenu",
2318
2326
  "attribute": null,
@@ -2364,7 +2372,7 @@
2364
2372
  },
2365
2373
  {
2366
2374
  "name": "prefix",
2367
- "description": "Leading content (e.g. icon)."
2375
+ "description": "Leading content (e.g. icon). A `type=\"checkbox\"` item ignores it unless `check-placement=\"end\"` frees the leading column."
2368
2376
  },
2369
2377
  {
2370
2378
  "name": "suffix",
@@ -5672,6 +5680,72 @@
5672
5680
  }
5673
5681
  ]
5674
5682
  },
5683
+ {
5684
+ "name": "color-scheme-icon",
5685
+ "displayName": "Color scheme icon",
5686
+ "type": "shadow",
5687
+ "isCustomElement": true,
5688
+ "tag": "l-color-scheme-icon",
5689
+ "nativeTag": null,
5690
+ "selector": "l-color-scheme-icon",
5691
+ "subItemOf": null,
5692
+ "summary": "A sun that morphs into a moon. Purely presentational: it shows a\ncolor scheme, it does not choose or store one.\n\nThe two glyphs are a single shape — the disc grows, a mask slides in to carve\nthe crescent, and the rays retract in a ripple — so the change reads as one\nobject turning rather than two icons swapping. That is why this is authored\nSVG rather than an `<l-icon>`: an Iconify icon is opaque, and you cannot grow\none into another or animate a mask inside it.\n\nUse it wherever a scheme needs a glyph and the surrounding element is already\nthe control — a `menuitemcheckbox` row, a button, a legend. With no `scheme`\nit follows the page's scheme from `luxen-ui/color-scheme`, so it needs no\nwiring; set `scheme` to pin it.",
5693
+ "status": "stable",
5694
+ "appearances": [],
5695
+ "import": {
5696
+ "css": null,
5697
+ "js": "luxen-ui/color-scheme-icon"
5698
+ },
5699
+ "properties": [
5700
+ {
5701
+ "name": "scheme",
5702
+ "attribute": "scheme",
5703
+ "type": "ColorScheme | undefined",
5704
+ "default": null,
5705
+ "reflects": true,
5706
+ "description": "Which glyph to show. Leave it unset and the icon follows the page's scheme\n(`colorScheme.current` — a stored override, else the OS preference), so it\nstays in step with every other control without wiring. Set it to pin the\nglyph, for a legend or a preview that must not move."
5707
+ },
5708
+ {
5709
+ "name": "label",
5710
+ "attribute": "label",
5711
+ "type": "string | undefined",
5712
+ "default": null,
5713
+ "reflects": false,
5714
+ "description": "Accessible label. When set, the glyph becomes meaningful (`role=\"img\"`);\nwhen absent it is decorative and hidden from assistive tech — the right\ndefault inside a row or button that already carries the name."
5715
+ }
5716
+ ],
5717
+ "attributes": [],
5718
+ "events": [],
5719
+ "methods": [],
5720
+ "slots": [],
5721
+ "cssClasses": [],
5722
+ "cssParts": [
5723
+ {
5724
+ "name": "base",
5725
+ "description": "The root `<svg>`, for overriding the morph's timing or tilt."
5726
+ }
5727
+ ],
5728
+ "cssProperties": [
5729
+ {
5730
+ "name": "--size",
5731
+ "default": "1em",
5732
+ "description": "Width and height of the glyph."
5733
+ },
5734
+ {
5735
+ "name": "--color",
5736
+ "default": "currentColor",
5737
+ "description": "Glyph color."
5738
+ }
5739
+ ],
5740
+ "commands": [],
5741
+ "examples": [
5742
+ {
5743
+ "title": "In a menu row, where the row is the control",
5744
+ "language": "html",
5745
+ "code": "```html\n<l-dropdown-item type=\"checkbox\" checked>\n<l-color-scheme-icon slot=\"prefix\" scheme=\"dark\"></l-color-scheme-icon>\nDark theme\n</l-dropdown-item>\n```"
5746
+ }
5747
+ ]
5748
+ },
5675
5749
  {
5676
5750
  "name": "toast",
5677
5751
  "displayName": "Toast",
@@ -1,5 +1,5 @@
1
1
  /** Every component base name the library ships. */
2
- export type ElementBaseName = 'alert' | 'alert-dialog' | 'avatar' | 'badge' | 'button-group' | 'carousel' | 'carousel-item' | 'combobox' | 'dialog' | 'divider' | 'drawer' | 'dropdown' | 'dropdown-item' | 'dropdown-label' | 'form-field' | 'icon' | 'input-group' | 'input-otp' | 'input-stepper' | 'popover' | 'prose-editor' | 'skeleton' | 'slider' | 'spinner' | 'rating' | 'segmented-control' | 'select' | 'sticky-bar' | 'stories' | 'story' | 'stories-viewer' | 'tabs' | 'tag' | 'toast' | 'toast-item' | 'tooltip' | 'tree' | 'tree-item';
2
+ export type ElementBaseName = 'alert' | 'alert-dialog' | 'avatar' | 'badge' | 'button-group' | 'carousel' | 'carousel-item' | 'combobox' | 'dialog' | 'divider' | 'drawer' | 'dropdown' | 'dropdown-item' | 'dropdown-label' | 'form-field' | 'icon' | 'input-group' | 'input-otp' | 'input-stepper' | 'popover' | 'prose-editor' | 'skeleton' | 'slider' | 'spinner' | 'rating' | 'segmented-control' | 'select' | 'sticky-bar' | 'stories' | 'story' | 'stories-viewer' | 'tabs' | 'tag' | 'color-scheme-icon' | 'toast' | 'toast-item' | 'tooltip' | 'tree' | 'tree-item';
3
3
  /** Set the global prefix. Must be called before defining elements. */
4
4
  export declare function setPrefix(prefix: {
5
5
  element: string;
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/html/registry.ts"],"names":[],"mappings":"AAAA,mDAAmD;AACnD,MAAM,MAAM,eAAe,GACvB,OAAO,GACP,cAAc,GACd,QAAQ,GACR,OAAO,GACP,cAAc,GACd,UAAU,GACV,eAAe,GACf,UAAU,GACV,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,UAAU,GACV,eAAe,GACf,gBAAgB,GAChB,YAAY,GACZ,MAAM,GACN,aAAa,GACb,WAAW,GACX,eAAe,GACf,SAAS,GACT,cAAc,GACd,UAAU,GACV,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,mBAAmB,GACnB,QAAQ,GACR,YAAY,GACZ,SAAS,GACT,OAAO,GACP,gBAAgB,GAChB,MAAM,GACN,KAAK,GACL,OAAO,GACP,YAAY,GACZ,SAAS,GACT,MAAM,GACN,WAAW,CAAC;AAMhB,sEAAsE;AACtE,wBAAgB,SAAS,CAAC,MAAM,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAGxE;AAED,gCAAgC;AAChC,wBAAgB,SAAS,IAAI;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAE5D;AAED,gEAAgE;AAChE,wBAAgB,OAAO,CAAC,QAAQ,EAAE,eAAe,GAAG,MAAM,CAEzD;AAED,sEAAsE;AACtE,wBAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAExC;AAID,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED,kDAAkD;AAClD,wBAAgB,YAAY,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO,CAE/D;AAED,qEAAqE;AACrE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,CAE9D"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/html/registry.ts"],"names":[],"mappings":"AAAA,mDAAmD;AACnD,MAAM,MAAM,eAAe,GACvB,OAAO,GACP,cAAc,GACd,QAAQ,GACR,OAAO,GACP,cAAc,GACd,UAAU,GACV,eAAe,GACf,UAAU,GACV,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,UAAU,GACV,eAAe,GACf,gBAAgB,GAChB,YAAY,GACZ,MAAM,GACN,aAAa,GACb,WAAW,GACX,eAAe,GACf,SAAS,GACT,cAAc,GACd,UAAU,GACV,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,mBAAmB,GACnB,QAAQ,GACR,YAAY,GACZ,SAAS,GACT,OAAO,GACP,gBAAgB,GAChB,MAAM,GACN,KAAK,GACL,mBAAmB,GACnB,OAAO,GACP,YAAY,GACZ,SAAS,GACT,MAAM,GACN,WAAW,CAAC;AAMhB,sEAAsE;AACtE,wBAAgB,SAAS,CAAC,MAAM,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAGxE;AAED,gCAAgC;AAChC,wBAAgB,SAAS,IAAI;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAE5D;AAED,gEAAgE;AAChE,wBAAgB,OAAO,CAAC,QAAQ,EAAE,eAAe,GAAG,MAAM,CAEzD;AAED,sEAAsE;AACtE,wBAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAExC;AAID,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED,kDAAkD;AAClD,wBAAgB,YAAY,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO,CAE/D;AAED,qEAAqE;AACrE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,CAE9D"}
@@ -0,0 +1,279 @@
1
+ ---
2
+ outline: deep
3
+ ---
4
+
5
+ # Color scheme icon
6
+
7
+ A sun that morphs into a moon.
8
+
9
+ **`<l-color-scheme-icon>`** — Custom Element · Shadow DOM
10
+
11
+ ```html
12
+ <l-color-scheme-icon scheme="dark"></l-color-scheme-icon>
13
+ ```
14
+
15
+ The two glyphs are one shape: the disc grows, a mask slides in to carve the crescent, and the rays retract in a ripple. Nothing is added or removed, so the change reads as a single object turning rather than two icons swapping.
16
+
17
+ Its home is an icon-only button in a header — the button owns the role, the name and the pressed state; the glyph shows which scheme is in effect.
18
+
19
+ ```html
20
+ <header
21
+ class="flex w-full items-center justify-between gap-4 rounded-xl border px-4 py-3 border-[var(--l-color-border)] bg-[var(--l-color-surface)]"
22
+ >
23
+ <div class="flex items-center gap-6">
24
+ <span class="text-sm font-semibold text-[var(--l-color-text-primary)]">Acme</span>
25
+
26
+ <nav class="flex gap-4 text-sm text-[var(--l-color-text-secondary)]">
27
+ <a href="#">Dashboard</a>
28
+ <a href="#">Reports</a>
29
+ <a href="#">Settings</a>
30
+ </nav>
31
+ </div>
32
+
33
+ <button
34
+ class="l-button"
35
+ data-icon-only
36
+ aria-pressed="false"
37
+ aria-label="Dark theme"
38
+ >
39
+ <l-color-scheme-icon></l-color-scheme-icon>
40
+ </button>
41
+ </header>
42
+ ```
43
+
44
+ It is **presentational only** — it shows a scheme, it never chooses or stores one. With no `scheme` it follows [`colorScheme.current`](/overview/color-scheme), so it stays in step with every other control on the page without wiring; set `scheme` to pin it.
45
+
46
+ The example above uses inline handlers to stay readable. In an application, the click and the pressed state are what you wire — the glyph needs nothing:
47
+
48
+ ```js
49
+ import { colorScheme } from 'luxen-ui/color-scheme';
50
+
51
+ const button = document.querySelector('[aria-label="Dark theme"]');
52
+
53
+ button.addEventListener('click', () => colorScheme.toggle());
54
+
55
+ // Called once with the current scheme, then on every change — no initial pass.
56
+ colorScheme.subscribe((scheme) => {
57
+ button.setAttribute('aria-pressed', String(scheme === 'dark'));
58
+ });
59
+ ```
60
+
61
+ ### Which scheme does the glyph show?
62
+
63
+ The one **in effect**, not the one a click would move to: dark mode shows the moon. That is what makes it correct inside a `role="switch"` thumb or beside an `aria-pressed` button, where the state is already announced and the glyph should agree with it.
64
+
65
+ To advertise the destination instead — a sun meaning "switch to light" — pass the opposite of your current scheme. Pick one convention and hold it across the app; mixing them is how a toggle stops being readable.
66
+
67
+ ## Options
68
+
69
+ ### Scheme
70
+
71
+ Leave `scheme` unset and the icon follows the page's [color scheme](/overview/color-scheme) — a stored override, else the OS preference — updating itself when it changes anywhere, including in another tab.
72
+
73
+ Set `scheme` and the store is out of the picture: the glyph shows what you tell it and nothing else moves it. That is the hook for an app that already owns its light/dark state — a framework store, a class on `<html>` written by the server, a user setting fetched from an API — and also what pins a glyph in a legend or a preview.
74
+
75
+ ```js
76
+ // Your state, your rules. The icon just draws it.
77
+ theme.subscribe((value) => (icon.scheme = value));
78
+ ```
79
+
80
+ ```html
81
+ <div class="flex items-center gap-8">
82
+ <div class="flex flex-col items-center gap-2">
83
+ <l-color-scheme-icon
84
+ scheme="light"
85
+ class="[--size:2rem]"
86
+ ></l-color-scheme-icon>
87
+ <code class="text-xs text-[var(--l-color-text-secondary)]">scheme="light"</code>
88
+ </div>
89
+
90
+ <div class="flex flex-col items-center gap-2">
91
+ <l-color-scheme-icon
92
+ scheme="dark"
93
+ class="[--size:2rem]"
94
+ ></l-color-scheme-icon>
95
+ <code class="text-xs text-[var(--l-color-text-secondary)]">scheme="dark"</code>
96
+ </div>
97
+
98
+ <div class="flex flex-col items-center gap-2">
99
+ <l-color-scheme-icon class="[--size:2rem]"></l-color-scheme-icon>
100
+ <code class="text-xs text-[var(--l-color-text-secondary)]">follows the page</code>
101
+ </div>
102
+ </div>
103
+ ```
104
+
105
+ ### Size & color
106
+
107
+ `--size` sets width and height (default `1em`, so it scales with the surrounding text). `--color` defaults to `currentColor`.
108
+
109
+ ```html
110
+ <div class="flex items-center gap-8">
111
+ <l-color-scheme-icon scheme="dark"></l-color-scheme-icon>
112
+
113
+ <l-color-scheme-icon
114
+ scheme="dark"
115
+ class="[--size:2rem]"
116
+ ></l-color-scheme-icon>
117
+
118
+ <l-color-scheme-icon
119
+ scheme="dark"
120
+ class="[--size:3rem] [--color:var(--l-color-text-info)]"
121
+ ></l-color-scheme-icon>
122
+ </div>
123
+ ```
124
+
125
+ ### Accessible name
126
+
127
+ The icon is decorative by default and hidden from assistive tech, which is what you want inside a row or button that already carries the name. Set `label` to make it meaningful — it then exposes `role="img"` with that name.
128
+
129
+ Both icons below look identical; the difference exists only in the accessibility tree.
130
+
131
+ ```html
132
+ <div class="flex items-center gap-8">
133
+ <button
134
+ class="l-button"
135
+ data-icon-only
136
+ aria-label="Dark theme"
137
+ >
138
+ <l-color-scheme-icon></l-color-scheme-icon>
139
+ </button>
140
+
141
+ <l-color-scheme-icon
142
+ label="Currently in dark theme"
143
+ scheme="dark"
144
+ class="[--size:2rem]"
145
+ ></l-color-scheme-icon>
146
+ </div>
147
+ ```
148
+
149
+ ## Examples
150
+
151
+ ### In a menu row
152
+
153
+ Put it in a `type="checkbox"` [dropdown item](/elements/dropdown#theme-row) and the row is the control: whole row clickable, `menuitemcheckbox` role, menu stays open. The icon stays decorative.
154
+
155
+ Add `check-placement="end"` so the check moves to the trailing edge and leaves the leading column free — the glyph then lines up with every other row's icon.
156
+
157
+ ```html
158
+ <header
159
+ class="flex w-full items-center justify-between gap-4 rounded-xl border px-4 py-3 border-[var(--l-color-border)] bg-[var(--l-color-surface)]"
160
+ >
161
+ <div class="flex items-center gap-6">
162
+ <span class="text-sm font-semibold text-[var(--l-color-text-primary)]">Acme</span>
163
+
164
+ <nav class="flex gap-4 text-sm text-[var(--l-color-text-secondary)]">
165
+ <a href="#">Dashboard</a>
166
+ <a href="#">Reports</a>
167
+ </nav>
168
+ </div>
169
+
170
+ <l-dropdown placement="bottom-end">
171
+ <l-avatar
172
+ slot="trigger"
173
+ interactive
174
+ name="Jane Cooper"
175
+ aria-label="Account"
176
+ class="[--appearance:circle] [--color:var(--l-color-blue-200)]"
177
+ ></l-avatar>
178
+
179
+ <div
180
+ slot="header"
181
+ class="flex flex-col px-2 py-1.5"
182
+ >
183
+ <span class="text-sm font-medium text-[var(--l-color-text-primary)]">Jane Cooper</span>
184
+ <span class="text-xs text-[var(--l-color-text-secondary)]">jane.cooper@acme.com</span>
185
+ </div>
186
+
187
+ <l-divider></l-divider>
188
+
189
+ <l-dropdown-item
190
+ type="checkbox"
191
+ check-placement="end"
192
+ value="theme"
193
+ >
194
+ <l-color-scheme-icon slot="prefix"></l-color-scheme-icon>
195
+ Dark theme
196
+ </l-dropdown-item>
197
+
198
+ <l-dropdown-item
199
+ type="checkbox"
200
+ check-placement="end"
201
+ value="compact"
202
+ checked
203
+ >
204
+ <iconify-icon
205
+ slot="prefix"
206
+ icon="lucide:rows-3"
207
+ ></iconify-icon>
208
+ Compact rows
209
+ </l-dropdown-item>
210
+
211
+ <l-divider></l-divider>
212
+
213
+ <l-dropdown-item value="signout">
214
+ <iconify-icon
215
+ slot="prefix"
216
+ icon="lucide:log-out"
217
+ ></iconify-icon>
218
+ Sign out
219
+ </l-dropdown-item>
220
+ </l-dropdown>
221
+ </header>
222
+ ```
223
+
224
+ Here the row carries the state, so `checked` is what you keep in step:
225
+
226
+ ```js
227
+ import { colorScheme } from 'luxen-ui/color-scheme';
228
+
229
+ const item = document.querySelector('l-dropdown-item[value="theme"]');
230
+
231
+ document.querySelector('l-dropdown').addEventListener('select', (event) => {
232
+ if (event.item === item) colorScheme.set(item.checked ? 'dark' : 'light');
233
+ });
234
+
235
+ colorScheme.subscribe((scheme) => (item.checked = scheme === 'dark'));
236
+ ```
237
+
238
+ Do not put a [switch](/elements/switch) in a menu row instead: a `role="menu"` may only own `menuitem`, `menuitemcheckbox` and `menuitemradio` children, and a nested control gives the row two competing click targets.
239
+
240
+ ## Accessibility
241
+
242
+ ### Criteria
243
+
244
+ - **Decorative by default** — With no `label` the icon is `aria-hidden` — the surrounding row or button carries the accessible name, so nothing is announced twice
245
+ - **Meaningful when named** — Setting `label` exposes `role=img` with that name, for the rare case where the glyph stands alone
246
+ - **Reduced motion** — The morph collapses to 0ms under `prefers-reduced-motion`; the glyph still changes, it just does not animate
247
+
248
+ ### Rules
249
+
250
+ - Leave the icon decorative when it sits inside a control that already has a name — a menu row, a button
251
+ - Never rely on the glyph alone to convey the state; pair it with a label or an `aria-checked` row
252
+ - Leave `scheme` unset unless the glyph must be pinned — it then follows the page and cannot disagree with it
253
+
254
+ ## API reference
255
+
256
+ ### Importing
257
+
258
+ ```js
259
+ import 'luxen-ui/color-scheme-icon';
260
+ ```
261
+
262
+ ### Properties
263
+
264
+ - **scheme**: `ColorScheme | undefined` — Which glyph to show. Leave it unset and the icon follows the page's scheme
265
+ (`colorScheme.current` — a stored override, else the OS preference), so it
266
+ stays in step with every other control without wiring. Set it to pin the
267
+ glyph, for a legend or a preview that must not move.
268
+ - **label**: `string | undefined` — Accessible label. When set, the glyph becomes meaningful (`role="img"`);
269
+ when absent it is decorative and hidden from assistive tech — the right
270
+ default inside a row or button that already carries the name.
271
+
272
+ ### CSS custom properties
273
+
274
+ - `--size` (default: `1em`) — Width and height of the glyph.
275
+ - `--color` (default: `currentColor`) — Glyph color.
276
+
277
+ ### CSS parts
278
+
279
+ - `base` — The root `<svg>`, for overriding the morph's timing or tilt.
@@ -394,6 +394,55 @@ Use the `header` slot for a profile row, the `prefix` slot on each `<l-dropdown-
394
394
  </l-dropdown>
395
395
  ```
396
396
 
397
+ ### Theme row {#theme-row}
398
+
399
+ A display setting belongs in the menu as a `type="checkbox"` item, not as a switch nested inside one. The whole row is then the control — one click target, `role="menuitemcheckbox"`, and the menu stays open so the change is visible immediately. A nested switch would give the row two competing targets, and `role="menu"` may only own `menuitem`, `menuitemcheckbox` and `menuitemradio` children.
400
+
401
+ A checkbox item puts its check in the leading column, which leaves no room for an icon. Set `check-placement="end"` to move the check to the trailing edge and free that column, so the row keeps its icon **and** shows whether it is on — and its icon lines up with every other row in the menu.
402
+
403
+ [`<l-color-scheme-icon>`](/elements/color-scheme-icon) in the `prefix` slot then morphs from sun to moon as the row toggles. It stays decorative: the row carries the name, the role and the state.
404
+
405
+ ```html
406
+ <l-dropdown>
407
+ <l-avatar
408
+ slot="trigger"
409
+ interactive
410
+ name="Jane Cooper"
411
+ aria-label="Account"
412
+ style="--appearance: circle; --color: var(--color-purple-200)"
413
+ ></l-avatar>
414
+
415
+ <l-dropdown-item value="whats-new">
416
+ <iconify-icon
417
+ slot="prefix"
418
+ icon="lucide:megaphone"
419
+ ></iconify-icon>
420
+ What's new
421
+ </l-dropdown-item>
422
+
423
+ <l-dropdown-item
424
+ type="checkbox"
425
+ check-placement="end"
426
+ value="theme"
427
+ >
428
+ <l-color-scheme-icon slot="prefix"></l-color-scheme-icon>
429
+ Dark theme
430
+ </l-dropdown-item>
431
+
432
+ <l-divider></l-divider>
433
+
434
+ <l-dropdown-item value="signout">
435
+ <iconify-icon
436
+ slot="prefix"
437
+ icon="lucide:log-out"
438
+ ></iconify-icon>
439
+ Sign out
440
+ </l-dropdown-item>
441
+ </l-dropdown>
442
+ ```
443
+
444
+ The row reflects and changes the page's [color scheme](/overview/color-scheme); the same glyph works in a header button, outside any menu.
445
+
397
446
  ## Accessibility
398
447
 
399
448
  ### Criteria
@@ -487,12 +536,16 @@ while open.
487
536
  - **variant**: `'destructive' | undefined` (default: `undefined`) — Visual variant. `destructive` marks a delete-style action in red.
488
537
  - **checked**: `boolean` (default: `false`) — Whether the checkbox item is checked.
489
538
  - **submenu-open**: `boolean` (default: `false`) — Whether this item's submenu is open. Managed by the parent `l-dropdown`.
539
+ - **check-placement**: `'start' | 'end'` (default: `'start'`) — Which side of a `type="checkbox"` item carries the check mark. `start` (the
540
+ default) puts it in the leading column, where it takes the place of any
541
+ `prefix`. `end` moves it to the trailing edge, freeing that column so the
542
+ item can show its own icon alongside the state.
490
543
  - **hasSubmenu**: `boolean` — Whether this item has nested `slot="submenu"` items.
491
544
 
492
545
  ### `dropdown-item` Slots
493
546
 
494
547
  - **(default)** — Label text.
495
- - **prefix** — Leading content (e.g. icon).
548
+ - **prefix** — Leading content (e.g. icon). A `type="checkbox"` item ignores it unless `check-placement="end"` frees the leading column.
496
549
  - **suffix** — Trailing content.
497
550
  - **submenu** — Nested `l-dropdown-item` elements rendered in a submenu panel anchored to this item. Drop an `<hr>` between them for a separator.
498
551
 
@@ -134,7 +134,9 @@ Set `value="1"` to activate a specific tab on load (0-based index).
134
134
 
135
135
  ### Disabled tab
136
136
 
137
- Add `disabled` to a tab button to take it out of play. It is skipped by arrow keys, `Home` and `End`, it cannot be selected by a click or by `value`, and it never shows the hover pill. Use `aria-disabled="true"` instead if the tab should stay focusable so keyboard users can still discover it.
137
+ Add `disabled` to a tab button to take it out of play. It is skipped by arrow keys, `Home` and `End`, it cannot be selected by a click or by `value`, and it never shows the hover pill. `aria-disabled="true"` behaves identically — reach for it only when the native attribute is out of reach (a framework binding, a non-`button` element).
138
+
139
+ `l-tabs` itself takes no `disabled` attribute. To freeze tab switching — while a page loads, say — put `inert` on the tablist `div`: the tabs leave the tab sequence and stop answering clicks, and the active panel stays reachable.
138
140
 
139
141
  ```html
140
142
  <l-tabs variant="line">
@@ -178,6 +180,7 @@ Add `disabled` to a tab button to take it out of play. It is skipped by arrow ke
178
180
  - The first child of `l-tabs` must be a `div` containing `button` elements for the tab triggers
179
181
  - Each remaining child `div` maps to a tab panel in order
180
182
  - Do not add `role` or `aria-*` attributes manually — the custom element sets them automatically
183
+ - `l-tabs` has no host-level `disabled` attribute — put `inert` on the tablist `div` to freeze tab switching
181
184
 
182
185
  ### Keyboard interactions
183
186
 
package/elements.json CHANGED
@@ -373,6 +373,14 @@
373
373
  "inMockups": true,
374
374
  "selector": ".l-textarea"
375
375
  },
376
+ {
377
+ "name": "color-scheme-icon",
378
+ "displayName": "Color scheme icon",
379
+ "kind": "element",
380
+ "type": "shadow",
381
+ "inSkill": true,
382
+ "inMockups": false
383
+ },
376
384
  {
377
385
  "name": "toast",
378
386
  "displayName": "Toast",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "luxen-ui",
3
- "version": "0.18.3",
3
+ "version": "0.19.0",
4
4
  "description": "Modern web components and CSS-first UI library built with Lit. Framework-agnostic, customizable prefix, design tokens.",
5
5
  "keywords": [
6
6
  "custom-elements",
@@ -117,6 +117,10 @@
117
117
  "types": "./dist/shared/localize.d.ts",
118
118
  "import": "./dist/shared/localize.js"
119
119
  },
120
+ "./color-scheme": {
121
+ "types": "./dist/color-scheme.d.ts",
122
+ "import": "./dist/color-scheme.js"
123
+ },
120
124
  "./translations/*": {
121
125
  "types": "./dist/translations/*.d.ts",
122
126
  "import": "./dist/translations/*.js"
package/vite-plugin.ts CHANGED
@@ -83,11 +83,30 @@ export interface LuxenOptions {
83
83
  * commit it and edit freely.
84
84
  */
85
85
  emitTypes?: string | EmitTypesOptions;
86
+ /**
87
+ * Build-time defaults for the `luxen-ui/color-scheme` store, so a project
88
+ * never has to call `colorScheme.configure()` from its entry point.
89
+ */
90
+ colorScheme?: ColorSchemeOptions;
91
+ }
92
+
93
+ /** Build-time defaults for `luxen-ui/color-scheme`. */
94
+ export interface ColorSchemeOptions {
95
+ /**
96
+ * `localStorage` key holding the light/dark override. Default
97
+ * `'luxen-color-scheme'`; `''` disables persistence. The before-first-paint
98
+ * snippet in your `<head>` must read the same key — it runs before any module
99
+ * and cannot be rewritten by the build.
100
+ */
101
+ storageKey?: string;
102
+ /** Have the store write `color-scheme` on `<html>` itself. Default `false`. */
103
+ apply?: 'root' | false;
86
104
  }
87
105
 
88
106
  export default function luxen(options: LuxenOptions = {}): Plugin {
89
107
  let elementPrefix = 'l';
90
108
  let cssPrefix = 'l';
109
+ let colorSchemeCfg: ColorSchemeOptions = {};
91
110
 
92
111
  return {
93
112
  name: 'luxen',
@@ -99,6 +118,7 @@ export default function luxen(options: LuxenOptions = {}): Plugin {
99
118
  const fileCfg = await loadLuxenConfig();
100
119
  elementPrefix = options.elementPrefix ?? fileCfg?.elementPrefix ?? 'l';
101
120
  cssPrefix = options.cssPrefix ?? fileCfg?.cssPrefix ?? 'l';
121
+ colorSchemeCfg = { ...fileCfg?.colorScheme, ...options.colorScheme };
102
122
  const emitTypes = options.emitTypes ?? fileCfg?.emitTypes;
103
123
 
104
124
  if (emitTypes) {
@@ -127,20 +147,63 @@ export default function luxen(options: LuxenOptions = {}): Plugin {
127
147
  * switching), but ordinary consumers don't need to call it.
128
148
  */
129
149
  transform(code, id) {
130
- if (elementPrefix === 'l' && cssPrefix === 'l') return null;
131
- if (!isLuxenRegistry(id)) return null;
132
- let out = code;
133
- if (elementPrefix !== 'l') {
134
- out = out.replace(/_elementPrefix = 'l';/g, `_elementPrefix = '${elementPrefix}';`);
150
+ if (isLuxenRegistry(id)) {
151
+ if (elementPrefix === 'l' && cssPrefix === 'l') return null;
152
+ let out = code;
153
+ if (elementPrefix !== 'l') {
154
+ out = out.replace(/_elementPrefix = 'l';/g, `_elementPrefix = '${elementPrefix}';`);
155
+ }
156
+ if (cssPrefix !== 'l') {
157
+ out = out.replace(/_cssPrefix = 'l';/g, `_cssPrefix = '${cssPrefix}';`);
158
+ }
159
+ return out === code ? null : { code: out, map: null };
135
160
  }
136
- if (cssPrefix !== 'l') {
137
- out = out.replace(/_cssPrefix = 'l';/g, `_cssPrefix = '${cssPrefix}';`);
161
+
162
+ // Same trick for the color-scheme store: rewrite the two literal
163
+ // initialisers in `color-scheme.js` so a project's `luxen.config.mjs`
164
+ // defaults are baked in. `configure()` remains the runtime path.
165
+ if (isLuxenColorScheme(id)) {
166
+ const { storageKey, apply } = colorSchemeCfg;
167
+ if (storageKey === undefined && apply === undefined) return null;
168
+ let out = code;
169
+ if (storageKey !== undefined) {
170
+ out = bakeInitialiser(out, '_storageKey', "'luxen-color-scheme'", storageKey);
171
+ }
172
+ if (apply !== undefined) out = bakeInitialiser(out, '_apply', 'false', apply);
173
+ return out === code ? null : { code: out, map: null };
138
174
  }
139
- return out === code ? null : { code: out, map: null };
175
+
176
+ return null;
140
177
  },
141
178
  };
142
179
  }
143
180
 
181
+ /**
182
+ * Rewrite `let <name>[: Type] = <literal>;` to hold `value`.
183
+ *
184
+ * The optional type annotation is what makes this a helper rather than a plain
185
+ * `String.replace`: the module resolves to the compiled `let _apply = false;` in
186
+ * `dist`, but to `let _apply: ColorSchemeApply = false;` when a project points at
187
+ * the TypeScript source — which `isLuxenColorScheme` accepts. A pattern that only
188
+ * matched the compiled shape would drop half a project's configuration there,
189
+ * silently and with no build error.
190
+ */
191
+ function bakeInitialiser(code: string, name: string, literal: string, value: unknown): string {
192
+ const pattern = new RegExp(
193
+ `${name}(\\s*:\\s*[\\w.<>|'" ]+?)? = ${literal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')};`,
194
+ 'g',
195
+ );
196
+ return code.replace(
197
+ pattern,
198
+ (_match, annotation) => `${name}${annotation ?? ''} = ${JSON.stringify(value)};`,
199
+ );
200
+ }
201
+
202
+ function isLuxenColorScheme(id: string): boolean {
203
+ const path = id.replace(/\\/g, '/').split('?')[0] ?? '';
204
+ return /\/luxen-ui\/(?:dist|src\/html)\/color-scheme\.(?:js|ts)$/.test(path);
205
+ }
206
+
144
207
  function isLuxenRegistry(id: string): boolean {
145
208
  // Normalise for Windows + strip Vite query params (`?import`, `?used`, …)
146
209
  const path = id.replace(/\\/g, '/').split('?')[0] ?? '';