luxen-ui 0.25.0 → 0.26.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 (249) hide show
  1. package/README.md +1 -0
  2. package/bin/cli.mjs +16 -1
  3. package/cdn/custom-elements.json +402 -276
  4. package/cdn/elements/alert-dialog/alert-dialog.d.ts +3 -2
  5. package/cdn/elements/alert-dialog/alert-dialog.d.ts.map +1 -1
  6. package/cdn/elements/alert-dialog/alert-dialog.js.map +1 -1
  7. package/cdn/elements/avatar/avatar.js +1 -1
  8. package/cdn/elements/avatar/avatar.js.map +1 -1
  9. package/cdn/elements/carousel/carousel.js +1 -1
  10. package/cdn/elements/carousel/carousel.js.map +1 -1
  11. package/cdn/elements/combobox/combobox.d.ts.map +1 -1
  12. package/cdn/elements/combobox/combobox.js +2 -1
  13. package/cdn/elements/combobox/combobox.js.map +1 -1
  14. package/cdn/elements/dialog/dialog.d.ts +2 -1
  15. package/cdn/elements/dialog/dialog.d.ts.map +1 -1
  16. package/cdn/elements/dialog/dialog.js.map +1 -1
  17. package/cdn/elements/dialog/dialog.styles.js +1 -1
  18. package/cdn/elements/dialog/dialog.styles.js.map +1 -1
  19. package/cdn/elements/drawer/drawer.d.ts +7 -7
  20. package/cdn/elements/drawer/drawer.js +1 -1
  21. package/cdn/elements/drawer/drawer.js.map +1 -1
  22. package/cdn/elements/dropdown-item/dropdown-item.js +1 -1
  23. package/cdn/elements/dropdown-item/dropdown-item.js.map +1 -1
  24. package/cdn/elements/input-otp/input-otp.d.ts +2 -2
  25. package/cdn/elements/input-otp/input-otp.js.map +1 -1
  26. package/cdn/elements/prose-editor/prose-editor.js +1 -1
  27. package/cdn/elements/prose-editor/prose-editor.js.map +1 -1
  28. package/cdn/elements/rating/rating.js +1 -1
  29. package/cdn/elements/rating/rating.js.map +1 -1
  30. package/cdn/elements/select/select.d.ts.map +1 -1
  31. package/cdn/elements/select/select.js +2 -1
  32. package/cdn/elements/select/select.js.map +1 -1
  33. package/cdn/elements/slider/slider.js +1 -1
  34. package/cdn/elements/slider/slider.js.map +1 -1
  35. package/cdn/elements/stories-viewer/stories-viewer.js +1 -1
  36. package/cdn/elements/stories-viewer/stories-viewer.js.map +1 -1
  37. package/cdn/elements/table/table.meta.d.ts +55 -0
  38. package/cdn/elements/table/table.meta.d.ts.map +1 -0
  39. package/cdn/elements/table/table.meta.js +0 -0
  40. package/cdn/elements/tag/tag.js +1 -1
  41. package/cdn/elements/tag/tag.js.map +1 -1
  42. package/cdn/elements/tree/tree.js +1 -1
  43. package/cdn/elements/tree/tree.js.map +1 -1
  44. package/cdn/elements/tree-item/tree-item.js +1 -1
  45. package/cdn/elements/tree-item/tree-item.js.map +1 -1
  46. package/cdn/shared/controllers/listbox-nav.d.ts +7 -0
  47. package/cdn/shared/controllers/listbox-nav.d.ts.map +1 -1
  48. package/cdn/shared/controllers/listbox-nav.js +1 -1
  49. package/cdn/shared/controllers/listbox-nav.js.map +1 -1
  50. package/cdn/shared/styles/button-core.styles.js +1 -1
  51. package/cdn/shared/styles/button-core.styles.js.map +1 -1
  52. package/cdn/shared/styles/checkbox-appearance.styles.js +1 -1
  53. package/cdn/shared/styles/checkbox-appearance.styles.js.map +1 -1
  54. package/cdn/shared/styles/picker-core.styles.js +1 -1
  55. package/cdn/shared/styles/picker-core.styles.js.map +1 -1
  56. package/cdn/standalone.css +284 -67
  57. package/cdn/standalone.js +38 -28
  58. package/cdn/standalone.js.map +1 -1
  59. package/cdn/styles/base.css +11 -0
  60. package/cdn/styles/elements/breadcrumb.css +2 -2
  61. package/cdn/styles/elements/button.css +2 -2
  62. package/cdn/styles/elements/checkbox.css +2 -2
  63. package/cdn/styles/elements/close-button/circle.css +2 -2
  64. package/cdn/styles/elements/close-button/ring.css +2 -2
  65. package/cdn/styles/elements/close-button/square.css +2 -2
  66. package/cdn/styles/elements/disclosure.css +1 -1
  67. package/cdn/styles/elements/input-otp.css +19 -3
  68. package/cdn/styles/elements/input-stepper/default.css +1 -1
  69. package/cdn/styles/elements/input-stepper/pill.css +1 -1
  70. package/cdn/styles/elements/input-stepper/rounded.css +1 -1
  71. package/cdn/styles/elements/input.css +11 -11
  72. package/cdn/styles/elements/prose-editor.css +6 -6
  73. package/cdn/styles/elements/radio-group.css +2 -2
  74. package/cdn/styles/elements/radio.css +2 -2
  75. package/cdn/styles/elements/segmented-control.css +1 -1
  76. package/cdn/styles/elements/select.css +5 -5
  77. package/cdn/styles/elements/story.css +2 -2
  78. package/cdn/styles/elements/switch.css +2 -2
  79. package/cdn/styles/elements/table.css +186 -0
  80. package/cdn/styles/elements/tabs/enclosed.css +3 -3
  81. package/cdn/styles/elements/tabs/line.css +3 -3
  82. package/cdn/styles/elements/textarea.css +5 -5
  83. package/cdn/styles/preset.css +19 -6
  84. package/cdn/styles/tokens/aliases.css +5 -3
  85. package/cdn/styles/tokens/forms.css +3 -3
  86. package/cdn/styles/tokens.css +8 -6
  87. package/dist/css/base.css +11 -0
  88. package/dist/css/elements/breadcrumb.css +2 -2
  89. package/dist/css/elements/button.css +2 -2
  90. package/dist/css/elements/checkbox.css +2 -2
  91. package/dist/css/elements/close-button/circle.css +2 -2
  92. package/dist/css/elements/close-button/ring.css +2 -2
  93. package/dist/css/elements/close-button/square.css +2 -2
  94. package/dist/css/elements/disclosure.css +1 -1
  95. package/dist/css/elements/input-otp.css +19 -3
  96. package/dist/css/elements/input-stepper/default.css +1 -1
  97. package/dist/css/elements/input-stepper/pill.css +1 -1
  98. package/dist/css/elements/input-stepper/rounded.css +1 -1
  99. package/dist/css/elements/input.css +11 -11
  100. package/dist/css/elements/prose-editor.css +6 -6
  101. package/dist/css/elements/radio-group.css +2 -2
  102. package/dist/css/elements/radio.css +2 -2
  103. package/dist/css/elements/segmented-control.css +1 -1
  104. package/dist/css/elements/select.css +5 -5
  105. package/dist/css/elements/story.css +2 -2
  106. package/dist/css/elements/switch.css +2 -2
  107. package/dist/css/elements/table.css +186 -0
  108. package/dist/css/elements/tabs/enclosed.css +3 -3
  109. package/dist/css/elements/tabs/line.css +3 -3
  110. package/dist/css/elements/textarea.css +5 -5
  111. package/dist/css/preset.css +19 -6
  112. package/dist/css/tokens/aliases.css +5 -3
  113. package/dist/css/tokens/forms.css +3 -3
  114. package/dist/css/tokens.css +8 -6
  115. package/dist/custom-elements.json +402 -276
  116. package/dist/elements/alert-dialog/alert-dialog.d.ts +3 -2
  117. package/dist/elements/alert-dialog/alert-dialog.d.ts.map +1 -1
  118. package/dist/elements/alert-dialog/alert-dialog.js +3 -2
  119. package/dist/elements/avatar/avatar.css +2 -2
  120. package/dist/elements/carousel/carousel.css +18 -0
  121. package/dist/elements/combobox/combobox.css +9 -0
  122. package/dist/elements/combobox/combobox.d.ts.map +1 -1
  123. package/dist/elements/combobox/combobox.js +1 -0
  124. package/dist/elements/dialog/dialog.css +12 -0
  125. package/dist/elements/dialog/dialog.d.ts +2 -1
  126. package/dist/elements/dialog/dialog.d.ts.map +1 -1
  127. package/dist/elements/dialog/dialog.js +2 -1
  128. package/dist/elements/drawer/drawer.css +11 -1
  129. package/dist/elements/drawer/drawer.d.ts +7 -7
  130. package/dist/elements/drawer/drawer.js +7 -7
  131. package/dist/elements/dropdown-item/dropdown-item.css +1 -1
  132. package/dist/elements/input-otp/input-otp.d.ts +2 -2
  133. package/dist/elements/input-otp/input-otp.js +2 -2
  134. package/dist/elements/prose-editor/prose-editor.css +5 -3
  135. package/dist/elements/rating/rating.css +9 -10
  136. package/dist/elements/select/select.css +10 -5
  137. package/dist/elements/select/select.d.ts.map +1 -1
  138. package/dist/elements/select/select.js +1 -0
  139. package/dist/elements/slider/slider.css +5 -1
  140. package/dist/elements/stories-viewer/stories-viewer.css +10 -2
  141. package/dist/elements/table/table.meta.d.ts +55 -0
  142. package/dist/elements/table/table.meta.d.ts.map +1 -0
  143. package/dist/elements/table/table.meta.js +65 -0
  144. package/dist/elements/tag/tag.css +3 -3
  145. package/dist/elements/tree/tree.css +3 -3
  146. package/dist/elements/tree-item/tree-item.css +5 -3
  147. package/dist/metadata/alert-dialog.json +90 -85
  148. package/dist/metadata/alert.json +22 -22
  149. package/dist/metadata/avatar.json +26 -26
  150. package/dist/metadata/badge.json +13 -13
  151. package/dist/metadata/breadcrumb.json +9 -9
  152. package/dist/metadata/button.json +40 -40
  153. package/dist/metadata/carousel.json +133 -133
  154. package/dist/metadata/checkbox.json +13 -13
  155. package/dist/metadata/close-button.json +15 -15
  156. package/dist/metadata/color-scheme-icon.json +13 -13
  157. package/dist/metadata/combobox.json +77 -94
  158. package/dist/metadata/dialog.json +49 -44
  159. package/dist/metadata/disclosure.json +17 -17
  160. package/dist/metadata/divider.json +12 -12
  161. package/dist/metadata/drawer.json +61 -61
  162. package/dist/metadata/dropdown-item.json +50 -50
  163. package/dist/metadata/dropdown.json +32 -32
  164. package/dist/metadata/form-field.json +15 -15
  165. package/dist/metadata/icon.json +8 -8
  166. package/dist/metadata/index.json +1761 -1765
  167. package/dist/metadata/input-group.json +5 -5
  168. package/dist/metadata/input-otp.json +17 -17
  169. package/dist/metadata/input-stepper.json +24 -24
  170. package/dist/metadata/input.json +31 -31
  171. package/dist/metadata/popover.json +49 -49
  172. package/dist/metadata/progress.json +15 -15
  173. package/dist/metadata/prose-editor.json +110 -116
  174. package/dist/metadata/radio.json +15 -15
  175. package/dist/metadata/rating.json +25 -47
  176. package/dist/metadata/segmented-control.json +24 -46
  177. package/dist/metadata/select.json +81 -98
  178. package/dist/metadata/skeleton.json +12 -12
  179. package/dist/metadata/slider.json +60 -78
  180. package/dist/metadata/spinner.json +5 -5
  181. package/dist/metadata/sticky-bar.json +13 -13
  182. package/dist/metadata/stories-viewer.json +105 -105
  183. package/dist/metadata/stories.json +17 -17
  184. package/dist/metadata/story.json +41 -41
  185. package/dist/metadata/switch.json +9 -9
  186. package/dist/metadata/table.json +88 -0
  187. package/dist/metadata/tabs.json +26 -26
  188. package/dist/metadata/tag.json +50 -50
  189. package/dist/metadata/textarea.json +28 -28
  190. package/dist/metadata/toast.json +16 -16
  191. package/dist/metadata/tooltip.json +49 -49
  192. package/dist/metadata/tree-item.json +73 -73
  193. package/dist/metadata/tree.json +39 -39
  194. package/dist/shared/controllers/listbox-nav.d.ts +7 -0
  195. package/dist/shared/controllers/listbox-nav.d.ts.map +1 -1
  196. package/dist/shared/controllers/listbox-nav.js +12 -0
  197. package/dist/shared/styles/button-core.css +2 -2
  198. package/dist/shared/styles/checkbox-appearance.css +2 -2
  199. package/dist/shared/styles/picker-core.css +13 -10
  200. package/dist/templates/elements/alert-dialog.md +19 -18
  201. package/dist/templates/elements/alert.md +4 -4
  202. package/dist/templates/elements/avatar.md +9 -9
  203. package/dist/templates/elements/badge.md +4 -4
  204. package/dist/templates/elements/breadcrumb.md +4 -4
  205. package/dist/templates/elements/button.md +8 -8
  206. package/dist/templates/elements/carousel.md +32 -32
  207. package/dist/templates/elements/checkbox.md +4 -4
  208. package/dist/templates/elements/close-button.md +2 -2
  209. package/dist/templates/elements/color-scheme-icon.md +13 -13
  210. package/dist/templates/elements/combobox.md +19 -19
  211. package/dist/templates/elements/dialog.md +11 -10
  212. package/dist/templates/elements/disclosure.md +3 -3
  213. package/dist/templates/elements/divider.md +2 -2
  214. package/dist/templates/elements/drawer.md +15 -15
  215. package/dist/templates/elements/dropdown.md +15 -15
  216. package/dist/templates/elements/form-field.md +3 -3
  217. package/dist/templates/elements/icon.md +1 -1
  218. package/dist/templates/elements/input-otp.md +5 -5
  219. package/dist/templates/elements/input-stepper.md +4 -4
  220. package/dist/templates/elements/input.md +15 -8
  221. package/dist/templates/elements/popover.md +8 -8
  222. package/dist/templates/elements/progress.md +3 -3
  223. package/dist/templates/elements/prose-editor.md +38 -39
  224. package/dist/templates/elements/radio.md +6 -6
  225. package/dist/templates/elements/rating.md +5 -13
  226. package/dist/templates/elements/segmented-control.md +4 -4
  227. package/dist/templates/elements/select.md +23 -23
  228. package/dist/templates/elements/skeleton.md +2 -2
  229. package/dist/templates/elements/slider.md +13 -13
  230. package/dist/templates/elements/spinner.md +1 -1
  231. package/dist/templates/elements/sticky-bar.md +2 -2
  232. package/dist/templates/elements/stories-viewer.md +29 -29
  233. package/dist/templates/elements/stories.md +4 -4
  234. package/dist/templates/elements/story.md +11 -11
  235. package/dist/templates/elements/switch.md +5 -5
  236. package/dist/templates/elements/table.md +210 -0
  237. package/dist/templates/elements/tabs.md +4 -4
  238. package/dist/templates/elements/tag.md +14 -14
  239. package/dist/templates/elements/textarea.md +8 -8
  240. package/dist/templates/elements/toast.md +4 -4
  241. package/dist/templates/elements/tooltip.md +9 -9
  242. package/dist/templates/elements/tree-item.md +18 -18
  243. package/dist/templates/elements/tree.md +13 -13
  244. package/dist/templates/foundations/focus.md +303 -0
  245. package/elements.json +9 -0
  246. package/package.json +1 -1
  247. package/postcss-plugin-prefix.js +2 -2
  248. package/templates/SKILL.md.tpl +1 -0
  249. package/templates/choosing-elements.md.tpl +1 -0
@@ -0,0 +1,303 @@
1
+ ---
2
+ outline: deep
3
+ ---
4
+
5
+ # Focus
6
+
7
+ The focus indicator shows where the keyboard is. Every Luxen element draws it from the same three tokens, on `:focus-visible`, as an `outline`. Press <kbd>Tab</kbd>:
8
+
9
+ ```html
10
+ <div class="flex flex-wrap items-center gap-4">
11
+ <button
12
+ type="button"
13
+ class="l-button"
14
+ data-variant="primary"
15
+ >
16
+ Save
17
+ </button>
18
+ <button
19
+ type="button"
20
+ class="l-button"
21
+ >
22
+ Cancel
23
+ </button>
24
+ <label class="flex items-center gap-2 text-sm">
25
+ <input
26
+ type="checkbox"
27
+ class="l-checkbox"
28
+ />
29
+ Remember me
30
+ </label>
31
+ <label class="flex items-center gap-2 text-sm">
32
+ <input
33
+ type="checkbox"
34
+ role="switch"
35
+ class="l-switch"
36
+ />
37
+ Notifications
38
+ </label>
39
+ <l-segmented-control
40
+ label="View"
41
+ value="list"
42
+ >
43
+ <button value="list">List</button>
44
+ <button value="board">Board</button>
45
+ </l-segmented-control>
46
+ <input
47
+ class="l-input w-40"
48
+ placeholder="Search…"
49
+ aria-label="Search"
50
+ />
51
+ </div>
52
+ ```
53
+
54
+ ## Anatomy
55
+
56
+ | Part | Token | Default |
57
+ | ------ | ---------------------------------------------- | ----------------------------------- |
58
+ | Offset | `--l-focus-ring-offset` | `2px` |
59
+ | Ring | `--l-focus-ring-width`, `--l-focus-ring-color` | `2px`, `#d9461f` |
60
+ | Halo | `--l-form-control-focus-halo-width` | `3px`, the ring color at 22% |
61
+ | Border | `--l-form-control-border-width` | `1px`, in the ring color |
62
+ | Stroke | `--l-form-control-focus-border-width` | `2px`, drawn inward over the border |
63
+
64
+ An invalid field takes `--l-form-control-border-color-invalid` for its halo, border and stroke.
65
+
66
+ ## Placements
67
+
68
+ | Placement | Drawn | Used by |
69
+ | ------------- | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
70
+ | Outside | `outline-offset: var(--l-focus-ring-offset)` | Button, link, checkbox, radio, switch, tag, avatar, breadcrumb, slider thumb, rating star, carousel buttons |
71
+ | Inside | `outline-offset: calc(var(--l-focus-ring-width) * -1)` | Menu item, tabs, segmented control, disclosure summary, input stepper, carousel dots — items set edge to edge, where an outside ring would overlap the neighbor |
72
+ | Field | Border recolored, stroke inward, halo outside (`box-shadow`) | Input, textarea, select, `l-input-group`, `l-select` and its search box, `l-combobox`, `l-prose-editor`, the active `l-input-otp` cell |
73
+ | Active option | Inside ring, once the arrow keys moved it | `l-select` and `l-combobox` options |
74
+
75
+ In a listbox, DOM focus stays on the field and `aria-activedescendant` points at the option. The browser draws no focus there, so the option draws the ring itself when the keyboard moved it. A mouse-driven highlight stays a tint, like a hover.
76
+
77
+ ## Shown when it helps
78
+
79
+ [`:focus-visible`](https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible) lets the browser decide whether focus needs an indicator. Luxen never styles `:focus` alone.
80
+
81
+ | Focus arrives by | On | Indicator |
82
+ | -------------------------- | -------------------------- | ------------------------------ |
83
+ | <kbd>Tab</kbd>, arrow keys | Any control | Shown |
84
+ | Click or tap | Button, checkbox, tab, row | Hidden |
85
+ | Click or tap | Text field | Shown — typing follows |
86
+ | Script (`element.focus()`) | Any control | Same as the element focus left |
87
+
88
+ When a script moves focus after a click and the user must see where it landed, force it: `element.focus({ focusVisible: true })`.
89
+
90
+ ## Contrast
91
+
92
+ The ring needs **3:1** against the surface it sits on. The default passes on Luxen surfaces in both schemes, not on every background you may put under it.
93
+
94
+ Tokens cascade: set the color on the container that changes the background.
95
+
96
+ ```css
97
+ .hero {
98
+ --l-focus-ring-color: white;
99
+ }
100
+ ```
101
+
102
+ ```html
103
+ <div
104
+ class="flex w-full flex-wrap items-center gap-4 rounded-lg bg-[#1e3a8a] p-6 [color-scheme:dark] [--l-focus-ring-color:white]"
105
+ >
106
+ <button
107
+ type="button"
108
+ class="l-button"
109
+ data-variant="primary"
110
+ >
111
+ Get started
112
+ </button>
113
+ <button
114
+ type="button"
115
+ class="l-button"
116
+ >
117
+ Learn more
118
+ </button>
119
+ </div>
120
+ ```
121
+
122
+ Over a photo or a video no single color is safe. Use two — a light ring edged with dark ([WCAG technique C40](https://www.w3.org/WAI/WCAG22/Techniques/css/C40)), as `l-stories-viewer` does for the buttons over its media:
123
+
124
+ ```css
125
+ .on-media :focus-visible {
126
+ outline: var(--l-focus-ring-width) solid white;
127
+ outline-offset: var(--l-focus-ring-offset);
128
+ box-shadow: 0 0 0 calc(var(--l-focus-ring-offset) + var(--l-focus-ring-width) * 2) black;
129
+ }
130
+ ```
131
+
132
+ ## Never hidden
133
+
134
+ A sticky header or bar can cover the element that just received focus. Reserve their height on the scroll container:
135
+
136
+ ```css
137
+ html {
138
+ scroll-padding-block: 4rem 5rem; /* sticky header, sticky bar */
139
+ }
140
+ ```
141
+
142
+ A container with `overflow: hidden` clips an outside ring. Give it padding of at least offset + width (`4px` by default).
143
+
144
+ ## Forced colors
145
+
146
+ In forced colors mode (Windows High Contrast), the browser drops every `box-shadow` and repaints `outline` in a system color. Luxen rings are outlines. A field's halo is a shadow, so fields add a transparent outline there for the system to repaint.
147
+
148
+ ## Variants
149
+
150
+ Deliberate departures from the three tokens, each for a reason:
151
+
152
+ | Where | Variant | Why |
153
+ | ------------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------ |
154
+ | Invalid field | Border, stroke and halo in `--l-form-control-border-color-invalid` | The error stays readable while the field is focused |
155
+ | Destructive menu item | Ring in `--l-color-text-danger` | Matches the item's own danger color |
156
+ | Tree row, prose editor toolbar button | Half the offset | Rows and buttons sit edge to edge; a full gap reaches the neighbor |
157
+ | Carousel buttons | Gap filled with `--l-color-surface` | They float over slides: the ring always sits next to a known color |
158
+ | Stories viewer buttons | Light ring edged with a dark band | They float over video, whose brightness is unknown |
159
+ | `l-input-otp`, `l-prose-editor` | `--cell-focus-color`, `--border-color-focus` | Per-element color override, defaulting to `--l-focus-ring-color` |
160
+
161
+ ## Your own controls
162
+
163
+ Plain links, `<button>` and `<summary>` already take the ring from Luxen's base CSS (zero specificity: any rule of yours wins). For anything else, reuse the tokens so custom controls match:
164
+
165
+ ```css
166
+ .card-link:focus-visible {
167
+ outline: var(--l-focus-ring-width) solid var(--l-focus-ring-color);
168
+ outline-offset: var(--l-focus-ring-offset);
169
+ }
170
+
171
+ /* Inside a clipping container: flush with the inner edge */
172
+ .menu-row:focus-visible {
173
+ outline: var(--l-focus-ring-width) solid var(--l-focus-ring-color);
174
+ outline-offset: calc(var(--l-focus-ring-width) * -1);
175
+ }
176
+
177
+ /* Ring on a wrapper whose focusable child is visually hidden */
178
+ .swatch:has(input:focus-visible) {
179
+ outline: var(--l-focus-ring-width) solid var(--l-focus-ring-color);
180
+ outline-offset: var(--l-focus-ring-offset);
181
+ }
182
+ ```
183
+
184
+ ## Theming
185
+
186
+ ### Brand color
187
+
188
+ One value per scheme with `light-dark()` — 6.9:1 on white, 8.8:1 on the dark surface. For a neutral ring that follows the scheme and forced colors on its own, use the system color `CanvasText`.
189
+
190
+ ```css
191
+ :root {
192
+ --l-focus-ring-color: light-dark(oklch(48% 0.2 265), oklch(78% 0.14 265));
193
+ }
194
+ ```
195
+
196
+ ### One thickness everywhere
197
+
198
+ Point the field stroke at the ring width and drop the halo:
199
+
200
+ ```css
201
+ :root {
202
+ --l-focus-ring-width: 3px;
203
+ --l-form-control-focus-border-width: var(--l-focus-ring-width);
204
+ --l-form-control-focus-halo-width: 0;
205
+ }
206
+ ```
207
+
208
+ ```html
209
+ <div
210
+ class="flex flex-wrap items-center gap-4 [--l-focus-ring-width:3px] [--l-form-control-focus-border-width:var(--l-focus-ring-width)] [--l-form-control-focus-halo-width:0px]"
211
+ >
212
+ <button
213
+ type="button"
214
+ class="l-button"
215
+ >
216
+ Save
217
+ </button>
218
+ <label class="flex items-center gap-2 text-sm">
219
+ <input
220
+ type="checkbox"
221
+ class="l-checkbox"
222
+ />
223
+ Remember me
224
+ </label>
225
+ <input
226
+ class="l-input w-48"
227
+ placeholder="Full name"
228
+ aria-label="Full name"
229
+ />
230
+ </div>
231
+ ```
232
+
233
+ ### Crisp field
234
+
235
+ The stroke alone, no halo. The stroke is drawn inward: the field keeps its size and its text does not move.
236
+
237
+ ```css
238
+ :root {
239
+ --l-form-control-focus-halo-width: 0;
240
+ }
241
+ ```
242
+
243
+ ```html
244
+ <div class="grid max-w-sm gap-4 [--l-form-control-focus-halo-width:0px]">
245
+ <l-form-field>
246
+ <label>Start date</label>
247
+ <input type="date" />
248
+ </l-form-field>
249
+ <l-form-field>
250
+ <label>Plan</label>
251
+ <select class="l-select">
252
+ <option>Starter</option>
253
+ <option>Pro</option>
254
+ </select>
255
+ </l-form-field>
256
+ <l-select
257
+ label="Team"
258
+ placeholder="Pick a team…"
259
+ >
260
+ <datalist>
261
+ <option value="design">Design</option>
262
+ <option value="dev">Development</option>
263
+ </datalist>
264
+ </l-select>
265
+ <l-combobox
266
+ label="Country"
267
+ placeholder="Search a country…"
268
+ >
269
+ <datalist>
270
+ <option value="fr">France</option>
271
+ <option value="de">Germany</option>
272
+ </datalist>
273
+ </l-combobox>
274
+ </div>
275
+ ```
276
+
277
+ ## Accessibility
278
+
279
+ - **Visible** — Every focusable element shows an indicator on keyboard focus
280
+ - **Contrast** — The indicator has 3:1 against adjacent colors: 4.3:1 on white, 4.1:1 on the dark surface
281
+ - **Size** — Outside rings are a solid 2px perimeter, the minimum area of Focus Appearance (AAA). Inside rings and the field stroke are drawn inward, which covers less: set `--l-focus-ring-width` and `--l-form-control-focus-border-width` to `3px` to meet it everywhere
282
+ - **Not obscured** — Rings between items set edge to edge are drawn inside, so none is covered by a neighbor; `scroll-padding` keeps focus clear of sticky content
283
+ - **Forced colors** — Indicators are outlines, repainted by the system; never a `box-shadow` alone
284
+ - **Order** — Focus follows DOM order; composite widgets (tabs, tree, menu, radio group) are one Tab stop with arrow keys inside, and focus is never trapped
285
+
286
+ ### Rules
287
+
288
+ - Never write `outline: none` without an equally visible replacement
289
+ - Style `:focus-visible`, not `:focus`
290
+ - Draw indicators with `outline`; a `box-shadow` alone disappears in forced colors
291
+ - Keep `--l-focus-ring-width` at `2px` or more
292
+ - Check `--l-focus-ring-color` at 3:1 against every background it lands on, in light and dark; scope it where a background differs
293
+ - Set `scroll-padding` on the scroll container when the page has sticky headers or bars
294
+ - Never use a positive `tabindex`
295
+
296
+ ## Tokens
297
+
298
+ - `--l-focus-ring-color` (default: `#d9461f`) — Color of the focus indicator, shown on :focus-visible: the ring around buttons, checkbox, radio, switch, tabs, tags, menu and tree items, links, and the border and halo of a focused text field. Keep at least 3:1 against every surface it lands on (WCAG 1.4.11) — the default gives 4.3:1 on white and 4.1:1 on the dark surface. Re-theme it on an ancestor to scope it, e.g. a lighter color over a photo.
299
+ - `--l-focus-ring-width` (default: `2px`) — Thickness of the :focus-visible ring drawn around interactive elements — buttons, checkbox, radio, switch, slider thumb, rating, tabs, tags, menu and tree items, toolbar and adornment buttons, close buttons, avatars, breadcrumb links. Keep it at 2px or more (WCAG 2.4.13). Rings set inside their element (items laid edge to edge — menu items, tabs, segmented control — where an outer ring would overlap a neighbor or the panel edge) grow inward with it. Text fields show focus on their border instead: see --l-form-control-focus-border-width, which a consumer can point at this token for one focus thickness everywhere.
300
+ - `--l-focus-ring-offset` (default: `2px`) — Gap between an element and its :focus-visible ring, so the ring reads apart from the element's own border or fill. Rings set inside their element (menu items, tabs, segmented control, disclosure summary — items laid edge to edge, where an outer ring would overlap a neighbor or the panel edge) ignore it and sit flush inside the edge instead.
301
+
302
+ - `--l-form-control-focus-border-width` (default: `2px`) — Width of the stroke drawn on a focused text field (input, textarea, native select, input group, l-select and its search box, l-combobox, l-prose-editor, the active l-input-otp cell), in the focus-ring color (the invalid color on an invalid field). Drawn inward from the outer edge, over the border: the control keeps its size and its text does not move. The default `2px` covers the 1px border and 1px more inside, so a focused field reads as clearly as a ring around a button. A value at or below --l-form-control-border-width adds nothing visible: the resting border alone takes the focus color.
303
+ - `--l-form-control-focus-halo-width` (default: `3px`) — Width of the soft halo around a focused text field — a ring of the focus-ring color (the invalid color on an invalid field) at 22% opacity, outside the border. Set to `0` for a crisp stroke with no halo, typically together with --l-form-control-focus-border-width. The color is mixed at the control, so a --l-focus-ring-color re-themed on an ancestor recolors the halo too.
package/elements.json CHANGED
@@ -356,6 +356,15 @@
356
356
  "inMockups": true,
357
357
  "selector": ".l-switch"
358
358
  },
359
+ {
360
+ "name": "table",
361
+ "displayName": "Table",
362
+ "kind": "native",
363
+ "type": "native",
364
+ "inSkill": true,
365
+ "inMockups": true,
366
+ "selector": ".l-table"
367
+ },
359
368
  {
360
369
  "name": "tabs",
361
370
  "displayName": "Tabs",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "luxen-ui",
3
- "version": "0.25.0",
3
+ "version": "0.26.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",
@@ -70,14 +70,14 @@ plugin.postcss = true;
70
70
  *
71
71
  * Each element ships its shadow-DOM CSS baked into the element JS, built once
72
72
  * with the default `l` prefix — that CSS never passes through this plugin, so
73
- * it keeps reading `var(--l-focus-ring)` etc. Meanwhile the consumer's imported
73
+ * it keeps reading `var(--l-focus-ring-color)` etc. Meanwhile the consumer's imported
74
74
  * preset now only defines `--{cssPrefix}-*`. Without a bridge the two
75
75
  * namespaces never meet and every shadow token resolves to nothing.
76
76
  *
77
77
  * So for every `--l-*` token declaration we just renamed, append a one-time
78
78
  * `:root` alias pointing the canonical name at the prefixed one:
79
79
  *
80
- * :root { --l-focus-ring: var(--p-focus-ring); … }
80
+ * :root { --l-focus-ring-color: var(--p-focus-ring-color); … }
81
81
  *
82
82
  * The block rides along with whichever imported file defines the tokens
83
83
  * (preset / tokens / aliases), stays exhaustive automatically (it mirrors
@@ -42,6 +42,7 @@ These rules apply across all elements; element-specific detail lives in the per-
42
42
  - **Variants and state use `data-*` attributes** (`data-variant`, `data-appearance`), not modifier classes.
43
43
  - **Show/hide overlays with the invoker pattern**: `command="--show"` / `command="--hide"` + `commandfor="<id>"` for dialog and drawer.
44
44
  - **Colors, spacing, and typography come from design tokens** — see [references/tokens.md](references/tokens.md). Use the semantic tokens and utility classes listed there rather than arbitrary values.
45
+ - **Focus states follow [references/foundations/focus.md](references/foundations/focus.md)** — read it before styling `:focus-visible` on any control, overriding a focus token, or placing controls on a custom background.
45
46
  - **Customize through the documented surface** — CSS custom properties, CSS parts, events. Before overriding an element's styles through `::part()` or working around one of its behaviours (motion, event timing), check its spec: the knob usually exists. When it does not, that is a gap in {{name}} — report it upstream rather than shipping a local workaround.
46
47
 
47
48
  ## Choosing an element
@@ -109,6 +109,7 @@ the platform genuinely can't express the behavior (a modal, a combobox, a toast)
109
109
  | A keyboard key | `.l-kbd` |
110
110
  | A separator between content | `<l-divider>` |
111
111
  | Hierarchical, expandable data | `<l-tree>` |
112
+ | Records in rows and columns | `.l-table` (on `<table>`) |
112
113
 
113
114
  ### Navigate or organize
114
115