@santi020k/lumen-elements 3.0.1 → 4.0.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 (66) hide show
  1. package/README.md +497 -5
  2. package/dist/bullet-chart-html.d.ts +5 -0
  3. package/dist/bullet-chart-html.d.ts.map +1 -0
  4. package/dist/bullet-chart-html.js +56 -0
  5. package/dist/chart-activation.d.ts +15 -0
  6. package/dist/chart-activation.d.ts.map +1 -0
  7. package/dist/chart-activation.js +93 -0
  8. package/dist/chart-html.d.ts +22 -0
  9. package/dist/chart-html.d.ts.map +1 -0
  10. package/dist/chart-html.js +1 -0
  11. package/dist/comparison-chart-html.d.ts +4 -0
  12. package/dist/comparison-chart-html.d.ts.map +1 -0
  13. package/dist/comparison-chart-html.js +36 -0
  14. package/dist/components/ai-surfaces.d.ts +115 -0
  15. package/dist/components/ai-surfaces.d.ts.map +1 -0
  16. package/dist/components/ai-surfaces.js +270 -0
  17. package/dist/components/button.d.ts +51 -2
  18. package/dist/components/button.d.ts.map +1 -1
  19. package/dist/components/button.js +81 -2
  20. package/dist/components/card.d.ts +6 -0
  21. package/dist/components/card.d.ts.map +1 -1
  22. package/dist/components/card.js +6 -1
  23. package/dist/components/combobox.d.ts +1 -2
  24. package/dist/components/combobox.d.ts.map +1 -1
  25. package/dist/components/combobox.js +27 -121
  26. package/dist/components/dashboard.d.ts +41 -0
  27. package/dist/components/dashboard.d.ts.map +1 -0
  28. package/dist/components/dashboard.js +88 -0
  29. package/dist/components/device-frame.d.ts +31 -0
  30. package/dist/components/device-frame.d.ts.map +1 -0
  31. package/dist/components/device-frame.js +95 -0
  32. package/dist/components/foundations.d.ts +30 -2
  33. package/dist/components/foundations.d.ts.map +1 -1
  34. package/dist/components/foundations.js +27 -2
  35. package/dist/components/image-comparison.d.ts +33 -0
  36. package/dist/components/image-comparison.d.ts.map +1 -0
  37. package/dist/components/image-comparison.js +144 -0
  38. package/dist/components/media-selection.d.ts +41 -0
  39. package/dist/components/media-selection.d.ts.map +1 -0
  40. package/dist/components/media-selection.js +114 -0
  41. package/dist/components/media-viewport.d.ts +30 -0
  42. package/dist/components/media-viewport.d.ts.map +1 -0
  43. package/dist/components/media-viewport.js +108 -0
  44. package/dist/components/virtual-list.d.ts +38 -0
  45. package/dist/components/virtual-list.d.ts.map +1 -0
  46. package/dist/components/virtual-list.js +30 -0
  47. package/dist/components/visual-interactions.d.ts +65 -0
  48. package/dist/components/visual-interactions.d.ts.map +1 -0
  49. package/dist/components/visual-interactions.js +91 -0
  50. package/dist/components/world-map.d.ts +46 -0
  51. package/dist/components/world-map.d.ts.map +1 -0
  52. package/dist/components/world-map.js +312 -0
  53. package/dist/consumer-behaviors.d.ts +36 -0
  54. package/dist/consumer-behaviors.d.ts.map +1 -0
  55. package/dist/consumer-behaviors.js +1 -0
  56. package/dist/define.d.ts +229 -6
  57. package/dist/define.d.ts.map +1 -1
  58. package/dist/define.js +1 -1
  59. package/dist/element-base.js +1 -1
  60. package/dist/expanded-chart-html.d.ts +9 -0
  61. package/dist/expanded-chart-html.d.ts.map +1 -0
  62. package/dist/expanded-chart-html.js +61 -0
  63. package/dist/index.d.ts +2 -1
  64. package/dist/index.d.ts.map +1 -1
  65. package/package.json +34 -6
  66. package/styles/critical.css +634 -61
package/README.md CHANGED
@@ -1,9 +1,80 @@
1
- # @santi020k/lumen-elements
1
+ <p align="center">
2
+ <a href="https://lumen.santi020k.com">
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/santi020k/lumen/main/docs/assets/readme/package-dark.svg">
5
+ <img src="https://raw.githubusercontent.com/santi020k/lumen/main/docs/assets/readme/package-light.svg" alt="Lumen UI — Web. Native. Thoughtfully connected." width="1200" height="184">
6
+ </picture>
7
+ </a>
8
+ </p>
9
+
10
+ <h1 align="center">Lumen UI · Web Components</h1>
11
+
12
+ <p align="center">Custom elements · Browser-native composition · Shared styles</p>
13
+
14
+ <p align="center">
15
+ <a href="https://www.npmjs.com/package/@santi020k/lumen-elements"><img src="https://img.shields.io/npm/v/@santi020k/lumen-elements?style=flat-square&color=0369a0" alt="npm version"></a>
16
+ <a href="https://github.com/santi020k/lumen/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-13967e?style=flat-square" alt="MIT license"></a>
17
+ </p>
18
+
19
+ <p align="center">
20
+ <a href="https://lumen.santi020k.com/docs/frameworks/elements">Documentation</a>
21
+ ·
22
+ <a href="https://www.npmjs.com/package/@santi020k/lumen-elements">npm</a>
23
+ ·
24
+ <a href="https://github.com/santi020k/lumen/tree/main/packages/elements">Source</a>
25
+ ·
26
+ <a href="https://github.com/santi020k/lumen/issues">Issues</a>
27
+ </p>
28
+
29
+ **Package:** `@santi020k/lumen-elements`
30
+
31
+ **On this page:** [Install](#install) · [Usage](#usage) · [Appearance presets](#appearance-presets) · [Resources](#resources)
32
+
33
+ <details>
34
+ <summary>Browse all sections</summary>
35
+
36
+ - [Install](#install)
37
+ - [Appearance presets](#appearance-presets)
38
+ - [Usage](#usage)
39
+ - [Language selection](#language-selection)
40
+ - [Forms](#forms)
41
+ - [Context navigation](#context-navigation)
42
+ - [Statistics](#statistics)
43
+ - [Data visualization](#data-visualization)
44
+ - [Optimized images](#optimized-images)
45
+ - [Interactive behavior](#interactive-behavior)
46
+ - [Motion](#motion)
47
+ - [Glass surfaces](#glass-surfaces)
48
+ - [Consumer composition recipes](#consumer-composition-recipes)
49
+ - [Exact amount fields](#exact-amount-fields)
50
+ - [Phone presentation in v4](#phone-presentation-in-v4)
51
+ - [Combobox keyboard behavior](#combobox-keyboard-behavior)
52
+ - [Content flow](#content-flow)
53
+ - [Attachment composition](#attachment-composition)
54
+ - [Dashboard composition](#dashboard-composition)
55
+ - [Combobox lifecycle](#combobox-lifecycle)
56
+ - [Device demonstrations](#device-demonstrations)
57
+ - [Visual interactions and product blocks](#visual-interactions-and-product-blocks)
58
+ - [World map](#world-map)
59
+ - [Studio media workspace](#studio-media-workspace)
60
+ - [Resources](#resources)
61
+
62
+ </details>
2
63
 
3
64
  Web Components for Lumen UI.
4
65
 
5
66
  This package registers standards-based custom elements for the shared Lumen primitive catalog.
6
67
 
68
+ `ImageComparison` form resets restore the latest externally configured `value`; user range input
69
+ does not replace that reset baseline.
70
+
71
+ See the [shared web form contracts](https://github.com/santi020k/lumen/blob/main/docs/form-controls.md) for value ownership, reset,
72
+ submission, disabled state, and event behavior.
73
+
74
+ Form controls use `visualSize` (`visual-size` in Elements) with `default`, `sm` and `lg`.
75
+ Select, PhoneInput and Segmented follow Input and NativeSelect; numeric input/select `size` keeps
76
+ its native meaning. See the [v4 migration guide](https://github.com/santi020k/lumen/blob/main/docs/migrating-v3-to-v4.md#form-control-visual-sizing).
77
+
7
78
  ## Install
8
79
 
9
80
  ```bash
@@ -31,6 +102,10 @@ The stylesheet defaults `--ui-font` to `"Montserrat", "Avenir Next", "Segoe UI",
31
102
  It declares the family stack but does not bundle or load font files. Load Montserrat once through
32
103
  your preferred delivery path, or override `--ui-font` in application CSS.
33
104
 
105
+ ## Appearance presets
106
+
107
+ Wrap registered components in `data-lumen-preset="studio"`; ThemeBuilder preset buttons use `data-ui-theme-preset`. See [appearance presets](https://github.com/santi020k/lumen/blob/main/docs/appearance-presets.md).
108
+
34
109
  ## Usage
35
110
 
36
111
  Register the elements once per custom-element registry, then use `lumen-*` tags anywhere HTML is
@@ -45,8 +120,8 @@ independently from the document registry.
45
120
  </script>
46
121
 
47
122
  <lumen-card>
48
- <label for="email">Email</label>
49
- <lumen-input id="email" type="email" placeholder="you@example.com"></lumen-input>
123
+ <lumen-label id="email-label">Email</lumen-label>
124
+ <lumen-input aria-labelledby="email-label" id="email" type="email" placeholder="you@example.com"></lumen-input>
50
125
  <lumen-button>Subscribe</lumen-button>
51
126
  </lumen-card>
52
127
  ```
@@ -85,6 +160,18 @@ selection, dismissal, and focus behavior without importing the complete catalog.
85
160
  still use `defineLumenElements`; do not replace the full entrypoint when the application needs a
86
161
  behavior-backed element that does not yet have a granular module.
87
162
 
163
+ VirtualList also has an independent registration entrypoint, including windowing and cleanup:
164
+
165
+ ```ts
166
+ import { defineLumenVirtualList } from '@santi020k/lumen-elements/components/virtual-list'
167
+
168
+ defineLumenVirtualList()
169
+ ```
170
+
171
+ It registers the same constructor as the full catalog and accepts an optional registry. Pair it
172
+ with the matching stylesheet. `pnpm run measure:selective-imports` compares this entry with the
173
+ complete catalog using an equivalent single-component consumer.
174
+
88
175
  Foundation-only pages can register fifteen layout, composition, and accessibility elements as one
89
176
  small implementation-level bundle:
90
177
 
@@ -222,6 +309,19 @@ or `variant="glass"` for selective translucency.
222
309
 
223
310
  ## Data visualization
224
311
 
312
+ The web visualization milestone adds `WaterfallChart` for signed changes and explicit totals, and
313
+ `Histogram` for precomputed numeric bins (`frequency="density"` for unequal widths). Line charts
314
+ support explicit continuous axes, annotations, optional keyboard/pointer/touch inspection, and
315
+ synchronized cursors. Heatmaps show labeled axes, a color legend, and explicit missing cells.
316
+ Heatmap JSON is validated as a complete collection: a malformed row rejects the dataset instead
317
+ of displaying a partial result. Null and nonfinite numeric measurements remain missing cells.
318
+ See the [visualization contracts](https://github.com/santi020k/lumen/blob/main/docs/data-visualization.md) and
319
+ [interactive web example](https://lumen.santi020k.com/docs/web/data-visualization).
320
+
321
+ Use `<lumen-histogram bins="...">` and `<lumen-waterfall-chart data="...">` with JSON arrays.
322
+ `<lumen-line-chart interactive x-scale="time" sync-group="report">` emits
323
+ `ui:chart-cursor-change` with `{ x }`. Set `annotations` to a JSON array.
324
+
225
325
  Set serializable `series` data through the JavaScript property for application data. The JSON
226
326
  attribute form is useful for static HTML and server output.
227
327
 
@@ -276,6 +376,16 @@ document-level toast controller events.
276
376
 
277
377
  ## Interactive behavior
278
378
 
379
+ Internal behavior methods use native JavaScript private members. Public properties, events and
380
+ registration APIs retain their existing contracts.
381
+
382
+ Disclosure keyboard navigation skips hidden or inert regions, invisible controls, and native
383
+ disabled controls, including a disabled fieldset. Available controls in a fieldset's first legend
384
+ retain their native keyboard behavior; removing `inert` makes a region available again.
385
+
386
+ Mentions keeps suggestion navigation on the textarea through `aria-activedescendant`; suggestion
387
+ buttons are excluded from the Tab sequence while Enter and pointer selection still insert a mention.
388
+
279
389
  Registering the elements wires behavior-heavy primitives without a framework runtime. DataTable,
280
390
  Dialog, Popover, DropdownMenu, ContextMenu, Tabs, Select, ThemeBuilder, Toast, Tooltip, forms,
281
391
  Calendar, InputOTP, DateRangePicker, RichTextEditor, Schedule, Resizable, and VirtualList track the
@@ -283,6 +393,10 @@ Astro runtime's data event, validation, calendar grids, OTP segmentation, date r
283
393
  text command, context menu, schedule drag/drop, resizable pane sizing, theme export, ARIA,
284
394
  keyboard, Escape, dismissal, and toast controller semantics while keeping markup declarative and
285
395
  Declarative-Shadow-DOM friendly.
396
+ Calendar attribute updates preserve the focused day in the owning document after iframe adoption.
397
+ Calendar form resets restore the latest configured `value` attribute; interactive date selections
398
+ do not replace that reset baseline. Form error summaries can focus associated native controls
399
+ outside the form subtree when their `form` attribute names that form.
286
400
  FileUpload, Tour, Anchor, CopyButton, Progress, ScrollProgress, Transfer, Mentions, Cascader, and TreeSelect also run directly through
287
401
  their registered custom elements; no Astro runtime or host controller is required.
288
402
  Rich text controls may provide `data-ui-editor-value` for commands such as `formatBlock` and
@@ -291,6 +405,9 @@ formatting shortcuts, and keep toggle controls synchronized through `aria-presse
291
405
  `lumen-tabs` keeps the selected trigger visible in narrow horizontal lists and emits
292
406
  `ui:tabs-change`; import `LumenTabsChangeDetail` or `LumenTabsChangeEvent` for its typed detail.
293
407
 
408
+ Nested `lumen-tabs` and `lumen-code-tabs` keep independent selection and panel state. Keyboard
409
+ navigation skips disabled triggers and stays within the active tab group.
410
+
294
411
  `<lumen-kanban-board>` and `<lumen-kanban-column value="…">` provide the same controlled board
295
412
  contract. Mark ordinary card items with `data-ui-kanban-item` and put `data-ui-kanban-handle` on a
296
413
  dedicated button. The board emits the cancellable `ui:kanban-move-request` event for keyboard,
@@ -326,7 +443,7 @@ Use `<lumen-error-state>` when a region or page cannot show its primary content.
326
443
  heading, explanation, optional safe reference, and recovery actions in light DOM, label the region
327
444
  with `aria-labelledby`, and use the documented `data-slot="error-state-*"` hooks. Lumen presents the
328
445
  failure; application code owns exception capture, logging, and retry policy. See the repository
329
- [Web Components error-handling guide](../../docs/error-handling.md#web-components) for the complete
446
+ [Web Components error-handling guide](https://github.com/santi020k/lumen/blob/main/docs/error-handling.md#web-components) for the complete
330
447
  light-DOM structure, ARIA ownership, retry boundary, and verification guidance.
331
448
 
332
449
  ## Motion
@@ -345,6 +462,36 @@ The elements adapter exposes the same motion vocabulary as Astro and React. Use
345
462
  <lumen-animated-number decimals="1" suffix="%" value="99.8"></lumen-animated-number>
346
463
  ```
347
464
 
465
+ ### Elements labels and dialogs
466
+
467
+ `lumen-input` owns an internal native control. Name that control using `aria-labelledby` pointing
468
+ at a visible label, or `aria-label` on the host. A native label's `for` pointing only at the custom
469
+ host does not name the internal input. Use the public `lumen-label` for visible label styling.
470
+ Do not type a custom host as `HTMLInputElement` or `HTMLButtonElement`; use its exported element
471
+ class and runtime narrowing when accessing element-specific methods.
472
+
473
+ Use `lumen-dialog` as the behavior owner. Its public methods are `show(trigger?)` and `close()`,
474
+ not `showModal()` on the custom host. A native `dialog` child is its documented modal contract:
475
+
476
+ ```html
477
+ <lumen-button data-ui-dialog-trigger="profile-dialog">Edit profile</lumen-button>
478
+ <lumen-dialog id="profile-dialog">
479
+ <dialog aria-labelledby="profile-title">
480
+ <h2 id="profile-title">Profile settings</h2>
481
+ <lumen-field>
482
+ <lumen-label id="profile-name-label">Display name</lumen-label>
483
+ <lumen-input aria-labelledby="profile-name-label" value="Ada"></lumen-input>
484
+ </lumen-field>
485
+ <lumen-button data-ui-dialog-close type="button">Cancel</lumen-button>
486
+ </dialog>
487
+ </lumen-dialog>
488
+ ```
489
+
490
+ Register `Button`, `Dialog`, `Field`, `Input`, and `Label` through `defineLumenElements` before use.
491
+ The dialog behavior focuses its first focusable control, handles Escape, traps focus, and returns
492
+ focus to its trigger. Keep draft state in the form control or application; closing does not reset it.
493
+ Do not replace this behavior with a parallel native-dialog controller.
494
+
348
495
  ## Glass surfaces
349
496
 
350
497
  Load `@santi020k/lumen-elements/styles.css` once, register the elements, then use the shared glass
@@ -360,7 +507,352 @@ attributes.
360
507
 
361
508
  ## Consumer composition recipes
362
509
 
363
- See [consumer UI recipes](../../docs/consumer-ui-recipes.md) for static React icons, responsive
510
+ See [consumer UI recipes](https://github.com/santi020k/lumen/blob/main/docs/consumer-ui-recipes.md) for static React icons, responsive
364
511
  record tables, keyboard-aware native sheets, whole-unit amount fields, adaptive editors, and
365
512
  asynchronous action states. Each recipe identifies the public primitives and the behavior that
366
513
  remains owned by the application.
514
+
515
+ ## Exact amount fields
516
+
517
+ `lumen-amount-field` creates a visible localized input and a named hidden input containing the
518
+ complete ASCII decimal string. `ui:amount-change` reports `{ draft, value }`; incomplete drafts
519
+ have an undefined value. A host ID `amount` gives its visible input ID `amount-input` for labels.
520
+
521
+ ```html
522
+ <lumen-amount-field name="amount" locale="es-CO" default-value="1234.50"
523
+ aria-label="Amount COP"></lumen-amount-field>
524
+ ```
525
+
526
+ See [consumer workflows](https://github.com/santi020k/lumen/blob/main/docs/consumer-ui-recipes.md#executable-consumer-workflows) for
527
+ validation, reset and `auto-scroll` activity feeds.
528
+
529
+ ### Accessible code examples
530
+
531
+ `<lumen-code variant="block" copy>` enhances its native `pre > code` child with a
532
+ copy button. Use `copy-label`, `copied-label`, and `error-label` for clipboard
533
+ feedback, and `code-label` to name its keyboard-scrollable source region. The same
534
+ labels can be placed on an enclosing `<lumen-code-tabs>`; child labels take
535
+ precedence. Copy emits `ui:copy-success` or `ui:copy-error` and announces feedback
536
+ in a live region. Clipboard failures leave the source available for manual copy.
537
+
538
+ Unwrapped `pre` children receive `tabindex="0"` and region semantics while keeping
539
+ authored accessible names. Generated names follow `code-label` updates and the current enclosing code tabs after reconnecting. Changing to `wrap="true"` removes only enhancer-added focus attributes.
540
+ Disabling `copy` removes only generated copy controls and cancels pending feedback.
541
+ With `wrap="true"`, the component does not add an extra
542
+ tab stop. Keep native `pre` and `code` children rather than placing source text in
543
+ HTML attributes.
544
+
545
+ ### Virtual list and editor ownership
546
+
547
+ `VirtualList` displays fixed-height rows with inert spacers that retain the full scroll extent.
548
+ Rows stay mounted; use pagination when the initial DOM cost matters. Scrolling, resizing, sizing
549
+ changes and direct row changes refresh the window. Focused rows remain available. See the
550
+ [fixed-height list contract](https://github.com/santi020k/lumen/blob/main/docs/ai-usage.md#fixed-height-virtual-lists).
551
+
552
+ External rich-text engines should handle the cancelable `ui:editor-command-request` event before
553
+ execution and use `ui:editor-command` only for completion notifications. React also supports
554
+ `useRichTextEditor({ commandHandler })`. Disable native toolbar state syncing when the external
555
+ engine owns it. See the [editor guidance](https://github.com/santi020k/lumen/blob/main/docs/ai-usage.md).
556
+
557
+ ## Phone presentation in v4
558
+
559
+ Phone inputs bundle the same offline flag artwork on every platform. The selected country shows
560
+ its flag and calling code inside one continuous input border. Country names remain in the native
561
+ picker and its accessible name; flags are supplementary. Unknown flag codes fall back to text.
562
+
563
+ Astro and React `PhoneInput` accept `disabled`, `readOnly`, `required`, `errorMessage`,
564
+ `showValidationError`, and `inputProps`. Their `id` targets the number input in v4; React also
565
+ accepts `inputRef`. Web Components use `disabled`, `readonly`, `required`, `error-message`,
566
+ `show-validation-error="false"`, and `input-id`, with native input attributes on the host.
567
+ Both controls lock together and validation remains associated with the input.
568
+
569
+ The `phone-input`, `phone-country`, and `country-flag` styling parts plus `--ui-phone-height`,
570
+ `--ui-phone-padding`, and `--ui-phone-country-gap` replace consumer CSS overlays.
571
+
572
+ Astro and React also export `CountryFlag` (`regionCode`, optional `decorative`) and `PhoneNumber`
573
+ (`value: LumenPhoneNumber`, optional `link`). A telephone link is rendered only for a complete
574
+ E.164 value. Use the model returned by the phone normalizer; keep domain persistence in your app.
575
+ Artwork attribution is shipped with the core package in `PHONE_FLAG_LICENSE.txt`.
576
+
577
+ When `PhoneInput` is registered, `defineLumenElements` also registers `lumen-country-flag`
578
+ (`country`, `decorative`, optional `label`) and `lumen-phone-number` (`country`, `value`, `link`,
579
+ optional `locale`). The read-only element normalizes its string value before creating a tel link.
580
+
581
+ ## Combobox keyboard behavior
582
+
583
+ In v4, Combobox retains input focus and exposes its active option through `aria-activedescendant`.
584
+ Enter commits an active option; text editing and composition remain native. Escape dismisses one
585
+ nested control at a time. See the [shared keyboard contract](https://github.com/santi020k/lumen/blob/main/docs/ai-usage.md#combobox-keyboard-behavior-in-v4)
586
+ for dynamic options, controlled inputs and migration guidance.
587
+
588
+ ## Content flow
589
+
590
+ Stack and Grid own sibling spacing. Their gap accepts `related`, `group` (default) and `section`,
591
+ or canonical `none`, `xs`, `sm`, `md`, `lg`, `xl`, `2xl`, `3xl` sizes. Card owns the inset and gap
592
+ between its visible parts; use `density="compact"`, `"comfortable"` (default) or `"spacious"`.
593
+ Elements uses the same names as attributes. Use a nested Stack for CardContent groups and Field
594
+ for label/control/feedback. See [content flow](https://github.com/santi020k/lumen/blob/main/docs/content-flow.md) and the
595
+ [v4 migration guide](https://github.com/santi020k/lumen/blob/main/docs/migrating-to-lumen.md) for ownership and changed explicit gaps.
596
+
597
+ ### Reading and complete compositions
598
+
599
+ Prose and Typography trim outer child margins and separate headings from preceding text.
600
+ Container gutters grow from 16px to 32px with viewport width; override `--ui-container-gutter`
601
+ when a product needs fixed gutters. Card allows interactive overflow; use AspectRatio to clip media.
602
+ Install `content-flow-header`, `content-flow-settings`, `content-flow-list` or `content-flow-actions`
603
+ with `lumen add <recipe> --target astro|react|elements`. MCP returns the same complete examples.
604
+ Connect application actions and replace sample IDs before reuse. See
605
+ [content flow](https://github.com/santi020k/lumen/blob/main/docs/content-flow.md) for composition and migration guidance.
606
+
607
+ ### Large fixed-height collections
608
+
609
+ `VirtualList` supports an empty `mode="data"` root with the public Core data controller.
610
+ Only the visible window, overscan and focused neighbors mount. Stable keys retain row identity;
611
+ applications own offscreen editing state. See [data rendering](https://github.com/santi020k/lumen/blob/main/docs/virtual-list-data.md) for
612
+ setup, lifecycle, accessibility and the mounted-mode tradeoff.
613
+
614
+ Forms validate native controls associated through the `form` attribute even outside the form tree.
615
+ Timed toasts retain their remaining duration until both pointer hover and keyboard focus leave.
616
+
617
+ ### Compound dialog tasks
618
+
619
+ Register `DialogHeader`, `DialogTitle`, `DialogBody`, `DialogFooter`, and
620
+ `DialogClose` with `Dialog`. Keep the body directly inside the dialog host for
621
+ independent scrolling. Use a native heading inside the title host and a native
622
+ button inside the close host; their semantics and disabled behavior remain native.
623
+ Associate the heading id with the dialog's `aria-labelledby`.
624
+
625
+ ```html
626
+ <lumen-dialog aria-labelledby="record-title">
627
+ <lumen-dialog-header>
628
+ <lumen-dialog-title><h2 id="record-title">Edit record</h2></lumen-dialog-title>
629
+ </lumen-dialog-header>
630
+ <lumen-dialog-body><form id="record-form"><label>Name <input name="name"></label></form></lumen-dialog-body>
631
+ <lumen-dialog-footer>
632
+ <lumen-dialog-close><button type="button">Cancel</button></lumen-dialog-close>
633
+ <button type="submit" form="record-form">Save</button>
634
+ </lumen-dialog-footer>
635
+ </lumen-dialog>
636
+ ```
637
+
638
+ A cancelled click or disabled native button does not dismiss the dialog. Nested
639
+ close actions only dismiss their own dialog. `lumen-file-upload` accepts
640
+ `selected-files-label` containing `{count}` for localized multiple-file feedback.
641
+
642
+ ### Rich description rows
643
+
644
+ Register `Descriptions`, `DescriptionItem`, `DescriptionTerm`, and `DescriptionDetail`
645
+ to compose rich values. The item, term, and detail hosts expose `group`, `term`, and
646
+ `definition` roles respectively. Use a labeled group for the overall collection and
647
+ associate the detail with its term when the relationship needs an explicit label.
648
+ These custom hosts provide ARIA semantics; they are not native `dl`, `dt`, or `dd` tags.
649
+
650
+ ```html
651
+ <lumen-descriptions role="group" aria-label="Record details">
652
+ <lumen-description-item>
653
+ <lumen-description-term id="status-label">Status</lumen-description-term>
654
+ <lumen-description-detail aria-labelledby="status-label"><strong>Active</strong></lumen-description-detail>
655
+ </lumen-description-item>
656
+ </lumen-descriptions>
657
+ ```
658
+
659
+ For native definition-list markup, place a complete native `dl` with `div`, `dt`,
660
+ and `dd` children inside `lumen-descriptions` instead of nesting custom hosts inside the `dl`.
661
+
662
+ ### Stepper progress
663
+
664
+ `lumen-stepper` is a generic element: it applies the shared `ui-stepper` presentation class and
665
+ `ui-stepper--vertical` for `orientation="vertical"`, but owns no step generation or current-step
666
+ behavior. Provide the complete step markup as light-DOM children:
667
+
668
+ ```html
669
+ <lumen-stepper role="list" aria-label="Setup steps">
670
+ <div class="ui-stepper__step" data-state="complete" role="listitem">
671
+ <span class="ui-stepper__marker">1</span>
672
+ <span class="ui-stepper__content"><span class="ui-stepper__title">Account</span></span>
673
+ </div>
674
+ <div aria-current="step" class="ui-stepper__step" data-state="current" role="listitem">
675
+ <span class="ui-stepper__marker">2</span>
676
+ <span class="ui-stepper__content"><span class="ui-stepper__title">Workspace</span></span>
677
+ </div>
678
+ </lumen-stepper>
679
+ ```
680
+
681
+ Each step is a native `div` with `role="listitem"`, `class="ui-stepper__step"`, and
682
+ `data-state="complete" | "current" | "upcoming"`. Give the current step `aria-current="step"`. Each
683
+ step holds a `ui-stepper__marker` span and a `ui-stepper__content` span containing a
684
+ `ui-stepper__title` span and an optional `ui-stepper__description` span.
685
+
686
+ ## Attachment composition
687
+
688
+ Use `AttachmentList` to group native `li` children and `AttachmentPreview` for browser-owned images
689
+ with localized loading, error, and unsupported-file states. Compose independent actions rather
690
+ than nesting controls inside a linked Attachment. The application retains file validation,
691
+ authorization, persistence, and object URL cleanup. See the
692
+ [attachment composition recipe](https://github.com/santi020k/lumen/blob/main/docs/consumer-ui-recipes.md#attachment-previews-and-file-lists)
693
+ for adapter props, slots, child contracts, retry identity, and safe state events.
694
+
695
+ ### Chart datum actions
696
+
697
+ All seven data charts accept the opt-in `drilldown` attribute. Each plotted observation gains a
698
+ pointer target and a matching native button under an accessible disclosure, even with
699
+ `show-table="false"`. Listen for the bubbling, composed `ui:chart-datum-activate` event on the chart
700
+ host; its validated `LumenChartDatumActivationDetail` contains original data identities and values.
701
+ Missing measurements and invalid ranges have no actions; pie charts expose only positive slices.
702
+
703
+ ```html
704
+ <lumen-bar-chart drilldown explore-data-label="Explore chart data"
705
+ datum-action-prefix="Open details: " show-table="false"></lumen-bar-chart>
706
+ ```
707
+
708
+ Use the `explore-data-label` and `datum-action-prefix` attributes for localized text, or assign a
709
+ `datumActionFormatter(context)` property for a contextual label formatter. The exported
710
+ `LumenChartDatumActionsElement` type describes this host property contract. Updating series, data,
711
+ or labels preserves the open disclosure and restores focus to the same available action. Chart
712
+ hosts own their controller lifecycle across disconnect/reconnect; mixed Astro pages leave these
713
+ hosts to Elements. Application code owns navigation, requests, and filtering.
714
+
715
+ ## Dashboard composition
716
+
717
+ `FilterBar` groups host-owned filtering controls, active criteria, reset actions, and a polite
718
+ result announcement. `ChangeSummary` presents explicit before/after values and application-owned
719
+ changed state. Neither component owns requests, persistence, parsing, or financial policy.
720
+
721
+ ScatterChart supports independent X/Y formatting, explicit domains, logarithmic positive X values,
722
+ and labeled reference lines/regions. See [consumer UI recipes](https://github.com/santi020k/lumen/blob/main/docs/consumer-ui-recipes.md)
723
+ for dashboard tables, freshness, import review, activity inbox, and persistent Kanban patterns.
724
+
725
+ ### Actual-versus-target charts
726
+
727
+ `lumen-bullet-chart` compares a nullable actual `value` with a finite `target` and optional
728
+ labeled `ranges`. A strong actual bar, target marker, readable value labels, and expandable exact
729
+ data work together. Domains include zero and all measurements; invalid inputs fail closed.
730
+ Null values stay distinct from zero. See the [chart guide](https://github.com/santi020k/lumen/blob/main/docs/data-visualization.md#actual-values-and-targets)
731
+ for the input, localization, and domain contracts.
732
+
733
+ ### Rankings and paired comparisons
734
+
735
+ Use `LollipopChart` for zero-based rankings and `DumbbellChart` for paired measurements (native
736
+ `LumenLollipopChart` and `LumenDumbbellChart`). Supply ordered comparison data with `id`, `label`,
737
+ nullable `value`, optional nullable `reference`, and optional `tone`. Both charts preserve missing
738
+ values and expose exact data. Set `value-label` to name the current measurement and `reference-label`
739
+ to name the paired measurement; `summary` supplies an escaped accessible interpretation that remains
740
+ available when `show-table="false"`. See the [shared visualization contract](https://github.com/santi020k/lumen/blob/main/docs/data-visualization.md#rankings-and-paired-comparisons).
741
+
742
+ ### Calendar activity, ordered stages and distributions
743
+
744
+ `CalendarHeatmap`, `FunnelChart`, and `BoxPlot` share validated geometry with every Lumen adapter.
745
+ CalendarHeatmap takes date-only UTC `startDate`/`endDate`, nullable `{ date, value }` data and optional
746
+ `weekStartsOn` (0 for Sunday or 1 for Monday). It fills omitted dates as missing, keeps zero distinct,
747
+ and limits the inclusive range to 3,660 days. `weekdayLabels` always indexes Sunday through Saturday;
748
+ `dateFormatter` customizes readable dates without changing their identity.
749
+
750
+ FunnelChart takes ordered `{ id, label, value, tone? }` stages with nonnegative nullable values.
751
+ Stages retain the supplied order, including increasing values; the chart derives no conversion rates.
752
+ BoxPlot takes precomputed `{ id, label, min, q1, median, q3, max, outliers?, tone? }` statistics.
753
+ Statistics must be ordered and finite, or all five must be null for a missing row. Explicit domains
754
+ must contain all observations and outliers. `statisticLabels` localizes the six statistic names.
755
+
756
+ All three accept `valueFormatter`, chart `labels`, `summary`, and `show-table` (default true).
757
+ Exact data and missing measurements remain readable; invalid input fails closed instead of dropping
758
+ observations or clipping the domain. Prefer retaining the data table for complete visual inspection.
759
+
760
+ Register `CalendarHeatmap`, `FunnelChart`, and `BoxPlot` through `defineLumenElements`.
761
+ Use `<lumen-calendar-heatmap start-date="2026-01-01" end-date="2026-01-31">`,
762
+ `<lumen-funnel-chart>`, and `<lumen-box-plot>`. Assign typed `data`, `labels`, `valueFormatter`,
763
+ `dateFormatter`/`weekdayLabels` (calendar), and `statisticLabels` (box) properties, or supply JSON
764
+ `data` attributes. Calendar supports `week-starts-on` and JSON `weekday-labels`; domains use
765
+ `domain-min`/`domain-max`. Box statistic attributes are `min-label`, `q1-label`, `median-label`,
766
+ `q3-label`, `max-label`, and `outliers-label`. `show-table="false"` hides exact-data disclosure;
767
+ provide an application-owned summary when additional interpretation is useful.
768
+
769
+ ## Combobox lifecycle
770
+
771
+ `lumen-combobox` enhances its input and listbox when both are available. Children can arrive after
772
+ connection or be replaced by an application renderer. Disconnecting the host releases listeners
773
+ and observers; reconnecting binds the current children.
774
+
775
+ Decoded chart annotations ignore invalid supplied axes; omitted axes use the shared `y` default.
776
+
777
+ React hosts can assign `type` on scalar input elements before or after connection. The input
778
+ reflects that property to its native control without clearing the value. Register elements once
779
+ at the app boundary and handle their bubbling native input events.
780
+
781
+ `lumen-button` activates with Enter or a Space release. Canceled Enter events, canceled Space
782
+ keydown events before the host handles them, and canceled Space releases suppress activation. The
783
+ host consumes Space keydown to prevent scrolling; cancel its key release or click to suppress activation
784
+ from an ancestor after that keydown has been consumed. It blocks
785
+ disabled/loading activation, including direct clicks. Blocking flags expose `aria-disabled`;
786
+ removing them restores any prior application-supplied ARIA value. Native nested controls retain
787
+ their own keyboard path.
788
+
789
+ ## Device demonstrations
790
+
791
+ `DeviceFrame` presents slotted HTML, an image, or a titled iframe inside `macbook-pro`, `macbook-air`, `imac`,
792
+ `iphone`, `pixel`, and `ipad-pro` frames, with generic `laptop`, `desktop`, `android`, and `tablet` options.
793
+ Device-specific enclosures include a MacBook Pro display notch and full front edge, a tapered MacBook Air deck, curved iMac
794
+ stand and chin, rounded phone glass with separate rails and buttons, and an iPad Pro with even
795
+ bezels and no Home button. The generic `tablet` retains its classic enclosure.
796
+ Shells are decorative and do not emulate device hardware. `orientation` selects portrait or landscape; `tone` selects
797
+ light or dark chrome independently of the screen content. Use `color="white"`,
798
+ `color="black"`, or a 3-, 4-, 6-, or 8-digit hex color such as `color="#a9b8ac"` for
799
+ a custom hardware finish. `color` takes precedence over `tone`; invalid values fall back to the
800
+ tone. The finish stays independent of the content theme. For CSS-driven updates, set
801
+ `--ui-device-color` to a CSS color on the frame.
802
+
803
+ Iframe layouts use the preset screen viewport and scale to the available width. Override
804
+ `screenWidth` and `screenHeight` for a custom viewport (1–16384 CSS pixels). Slotted HTML shares
805
+ the host viewport; container queries can adapt it to the screen. Images preserve their proportions.
806
+ `scroll={false}` clips HTML overflow; an iframe manages its own scrolling.
807
+
808
+ Supply image alternative text and iframe titles. Consumers own iframe `sandbox`, `allow`,
809
+ loading, and referrer policies. Remote sites can refuse embedding through their response headers.
810
+ The decorative shell does not alter focus or intercept interactions. Camera and home-indicator
811
+ details occupy separate chrome outside live HTML and iframe viewports. Direct phone images extend
812
+ under the decorative camera and home indicator for a full-bleed presentation.
813
+ Screen presets are 1280 × 800 for MacBook Pro and MacBook Air, 1440 × 810 for iMac, 390 × 844 for iPhone,
814
+ 412 × 915 for Pixel, and 834 × 1194 for iPad Pro (1194 × 834 in landscape). These are demonstration
815
+ viewports, not physical display specifications.
816
+
817
+ ```html
818
+ <lumen-device-frame device="android" tone="light">
819
+ <iframe src="/demo" title="Mobile application demo" loading="lazy"></iframe>
820
+ </lumen-device-frame>
821
+ ```
822
+
823
+ Register with `defineLumenElements(['DeviceFrame'])` or `defineLumenDeviceFrame()` from
824
+ `@santi020k/lumen-elements/components/device-frame`. Use `screen-width`, `screen-height`, and
825
+ `scroll="false"` attributes. The element preserves the initial child nodes inside its screen;
826
+ append later content to `.ui-device-frame__screen`.
827
+ ## Visual interactions and product blocks
828
+
829
+ See [visual interactions](https://github.com/santi020k/lumen/blob/main/docs/visual-interactions.md) for keyed motion, semantic effects,
830
+ chart continuity, AI surfaces, optional SDK integrations, and the four installable product recipes.
831
+
832
+ ## World map
833
+
834
+ WorldMap supports highlighted countries, location markers, dotted or solid styles, country selection,
835
+ and theme customization. Use `initialView="highlighted"` (Elements: `initial-view`) to start with a
836
+ regional view. Zoom toward the cursor with Ctrl/Cmd-scroll, or fit highlighted countries using the
837
+ map controls. Import geography explicitly from
838
+ `@santi020k/lumen-core/world-map-data`; it is excluded from root exports. See the
839
+ [WorldMap usage guide](https://github.com/santi020k/lumen/blob/main/docs/world-map.md) for adapter examples, events, localization,
840
+ accessibility, and customization.
841
+
842
+ ## Studio media workspace
843
+
844
+ Compose MediaViewport, MediaThumbnail, MediaFilmstrip and ImageComparison modes with the
845
+ [Studio media workspace recipes](https://github.com/santi020k/lumen/blob/main/docs/studio-media-workspaces.md). Applications retain
846
+ media loading, selection, adjustment algorithms, processing, export and persistence.
847
+
848
+ ## Resources
849
+
850
+ | Guide | What you will find |
851
+ | --- | --- |
852
+ | [Styling contract](https://github.com/santi020k/lumen/blob/main/docs/styling-contract.md) | Stable styling parts, component variables, and overrides. |
853
+ | [Error handling](https://github.com/santi020k/lumen/blob/main/docs/error-handling.md) | Field feedback, summaries, alerts, and recovery surfaces. |
854
+ | [Contributing](https://github.com/santi020k/lumen/blob/main/CONTRIBUTING.md) | Setup, checks, and contribution workflow. |
855
+ | [Release history](https://github.com/santi020k/lumen/releases) | Published releases and version notes. |
856
+
857
+ Part of [Lumen UI](https://lumen.santi020k.com), created by [Santiago Molina](https://santi020k.com).
858
+ Licensed under [MIT](https://github.com/santi020k/lumen/blob/main/LICENSE); third-party artwork retains its own notices.
@@ -0,0 +1,5 @@
1
+ import { type LumenBulletRange, type LumenChartLabels } from '@santi020k/lumen-core';
2
+ export declare const bulletNumberAttribute: (element: HTMLElement, name: string) => number;
3
+ export declare const parseBulletRanges: (source: string | null) => readonly LumenBulletRange[];
4
+ export declare const bulletChartHtml: (element: HTMLElement, ranges: readonly LumenBulletRange[], text: Readonly<LumenChartLabels>, format: (value: number) => string) => string;
5
+ //# sourceMappingURL=bullet-chart-html.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bullet-chart-html.d.ts","sourceRoot":"","sources":["../src/bullet-chart-html.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyD,KAAK,gBAAgB,EAAE,KAAK,gBAAgB,EAAmB,MAAM,uBAAuB,CAAA;AAI5J,eAAO,MAAM,qBAAqB,GAAI,SAAS,WAAW,EAAE,MAAM,MAAM,KAAG,MAI1E,CAAA;AAgBD,eAAO,MAAM,iBAAiB,GAAI,QAAQ,MAAM,GAAG,IAAI,KAAG,SAAS,gBAAgB,EAelF,CAAA;AAED,eAAO,MAAM,eAAe,GAC1B,SAAS,WAAW,EAAE,QAAQ,SAAS,gBAAgB,EAAE,EAAE,MAAM,QAAQ,CAAC,gBAAgB,CAAC,EAC3F,QAAQ,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,KAChC,MA0BF,CAAA"}
@@ -0,0 +1,56 @@
1
+ import { createLumenBulletGeometry, getLumenChartToneClassName, lumenChartTones } from '@santi020k/lumen-core';
2
+ import { escapeChartHtml } from './chart-html.js';
3
+ export const bulletNumberAttribute = (element, name) => {
4
+ const value = element.getAttribute(name);
5
+ return value === null || value.trim() === '' ? NaN : Number(value);
6
+ };
7
+ const bulletRangeWithTone = (end, label, inputTone) => {
8
+ const tone = lumenChartTones.find(candidate => candidate === inputTone);
9
+ if (inputTone !== undefined && !tone)
10
+ return { end: NaN, label: '' };
11
+ return { end, label, ...(tone ? { tone } : {}) };
12
+ };
13
+ const bulletDomain = (element) => {
14
+ if (!element.hasAttribute('domain-min') && !element.hasAttribute('domain-max'))
15
+ return {};
16
+ return { domain: { min: bulletNumberAttribute(element, 'domain-min'), max: bulletNumberAttribute(element, 'domain-max') } };
17
+ };
18
+ export const parseBulletRanges = (source) => {
19
+ try {
20
+ const entries = JSON.parse(source ?? '[]');
21
+ if (!Array.isArray(entries))
22
+ return [{ end: NaN, label: '' }];
23
+ return entries.map((entry) => {
24
+ if (typeof entry !== 'object' || entry === null || !('end' in entry) || !('label' in entry) ||
25
+ typeof entry.end !== 'number' || typeof entry.label !== 'string')
26
+ return { end: NaN, label: '' };
27
+ return bulletRangeWithTone(entry.end, entry.label, 'tone' in entry ? entry.tone : undefined);
28
+ });
29
+ }
30
+ catch {
31
+ return [{ end: NaN, label: '' }];
32
+ }
33
+ };
34
+ export const bulletChartHtml = (element, ranges, text, format) => {
35
+ const value = element.hasAttribute('value') ? bulletNumberAttribute(element, 'value') : null;
36
+ const target = bulletNumberAttribute(element, 'target');
37
+ const model = createLumenBulletGeometry(value, target, { ranges, ...bulletDomain(element) });
38
+ if (!model.valid)
39
+ return `<p class="ui-chart__empty" role="status">${escapeChartHtml(text.invalidData)}</p>`;
40
+ const actual = value === null ? text.notAvailable : format(value);
41
+ const targetLabel = element.getAttribute('target-label') ?? 'Target';
42
+ const tone = lumenChartTones.find(candidate => candidate === element.getAttribute('tone')) ?? 'series-1';
43
+ const summary = element.getAttribute('summary') ?? `${text.value}: ${actual}. ${targetLabel}: ${format(target)}.`;
44
+ const bands = model.ranges.map((range, index) => `<span class="ui-bullet-chart__range ${getLumenChartToneClassName(range.tone ?? 'neutral')}" style="left:${range.startRatio * 100}%;width:${(range.endRatio - range.startRatio) * 100}%;opacity:${0.08 + index / Math.max(1, model.ranges.length - 1) * 0.14}"></span>`).join('');
45
+ const bar = value === null ? '' : `<span class="ui-bullet-chart__bar ${getLumenChartToneClassName(tone)}" style="left:${model.valueStartRatio * 100}%;width:${model.valueWidthRatio * 100}%"></span>`;
46
+ const ticks = model.ticks.map(tick => `<span>${escapeChartHtml(format(tick.value))}</span>`).join('');
47
+ const rangeValue = (range) => `${format(range.start)}–${format(range.end)}`;
48
+ const legend = model.ranges.length ? `<ul class="ui-bullet-chart__ranges">${model.ranges.map(range => `<li class="${getLumenChartToneClassName(range.tone ?? 'neutral')}"><span>${escapeChartHtml(range.label)}</span><span>${escapeChartHtml(rangeValue(range))}</span></li>`).join('')}</ul>` : '';
49
+ const rows = [
50
+ [text.value, actual],
51
+ [targetLabel, format(target)],
52
+ ...model.ranges.map(range => [range.label, rangeValue(range)])
53
+ ];
54
+ const table = element.getAttribute('show-table') === 'false' ? '' : `<details class="ui-chart__data"><summary>${escapeChartHtml(text.viewData)}</summary><div role="group" tabindex="0" aria-label="${escapeChartHtml(text.chartData)}"><table><thead><tr><th scope="col">${escapeChartHtml(text.category)}</th><th scope="col">${escapeChartHtml(text.value)}</th></tr></thead><tbody>${rows.map(([label, number]) => `<tr><th scope="row">${escapeChartHtml(label ?? '')}</th><td>${escapeChartHtml(number ?? '')}</td></tr>`).join('')}</tbody></table></div></details>`;
55
+ return `<p class="ui-sr-only" data-ui-chart-summary>${escapeChartHtml(summary)}</p><div class="ui-bullet-chart__values"><div><span>${escapeChartHtml(text.value)}</span><strong>${escapeChartHtml(actual)}</strong></div><p><span class="ui-bullet-chart__target-key" aria-hidden="true"></span>${escapeChartHtml(targetLabel)}<strong>${escapeChartHtml(format(target))}</strong></p></div><div class="ui-bullet-chart__plot" aria-hidden="true"><div class="ui-bullet-chart__track">${bands}${bar}<span class="ui-bullet-chart__target" style="left:${model.targetRatio * 100}%"></span></div><div class="ui-bullet-chart__ticks">${ticks}</div></div>${legend}${table}`;
56
+ };