@f-ewald/components 1.15.0 → 1.17.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 (42) hide show
  1. package/README.md +1 -0
  2. package/custom-elements.json +720 -70
  3. package/dist/icons.d.ts +1 -0
  4. package/dist/icons.d.ts.map +1 -1
  5. package/dist/icons.js +1 -0
  6. package/dist/icons.js.map +1 -1
  7. package/dist/index.d.ts +3 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +3 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/mapbox-map.d.ts +59 -0
  12. package/dist/mapbox-map.d.ts.map +1 -0
  13. package/dist/mapbox-map.js +144 -0
  14. package/dist/mapbox-map.js.map +1 -0
  15. package/dist/photo-gallery.d.ts +4 -0
  16. package/dist/photo-gallery.d.ts.map +1 -1
  17. package/dist/photo-gallery.js +14 -1
  18. package/dist/photo-gallery.js.map +1 -1
  19. package/dist/range-slider.d.ts +48 -0
  20. package/dist/range-slider.d.ts.map +1 -0
  21. package/dist/range-slider.js +240 -0
  22. package/dist/range-slider.js.map +1 -0
  23. package/dist/toast-notification.d.ts +27 -9
  24. package/dist/toast-notification.d.ts.map +1 -1
  25. package/dist/toast-notification.js +68 -17
  26. package/dist/toast-notification.js.map +1 -1
  27. package/dist/tree-view.d.ts +9 -0
  28. package/dist/tree-view.d.ts.map +1 -1
  29. package/dist/tree-view.js +66 -4
  30. package/dist/tree-view.js.map +1 -1
  31. package/dist/ui-checkbox.d.ts +11 -3
  32. package/dist/ui-checkbox.d.ts.map +1 -1
  33. package/dist/ui-checkbox.js +31 -2
  34. package/dist/ui-checkbox.js.map +1 -1
  35. package/docs/mapbox-map.md +62 -0
  36. package/docs/photo-gallery.md +5 -0
  37. package/docs/range-slider.md +59 -0
  38. package/docs/toast-notification.md +15 -6
  39. package/docs/tree-view.md +7 -1
  40. package/docs/ui-checkbox.md +9 -1
  41. package/llms.txt +97 -13
  42. package/package.json +3 -1
@@ -6,18 +6,23 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
6
6
  };
7
7
  import { LitElement, css, html, nothing } from "lit";
8
8
  import { customElement, state } from "lit/decorators.js";
9
- import { iconX } from "./icons.js";
9
+ import { iconCheckCircle, iconExclamationCircle, iconExclamationTriangle, iconInfo, iconX, } from "./icons.js";
10
10
  import { tokens } from "./tokens.js";
11
11
  const DEFAULT_DURATION_MS = 5000;
12
12
  /**
13
13
  * Fixed-position stack of dismissible notifications, anchored top-right
14
- * (top-full-width on mobile). Not wired to any app state yet — callers add
15
- * toasts imperatively via the `show()` method on a live element reference,
14
+ * (top-full-width on mobile). Every toast shares one fixed width so entries
15
+ * never appear narrower or wider than one another. Not wired to any app state
16
+ * yet — callers add toasts imperatively via the `show()` method on a live
17
+ * element reference,
16
18
  * e.g. `document.querySelector('toast-notification')?.show('Offline', { variant: 'error' })`,
17
19
  * or via the `notifySuccess`/`notifyError`/`notifyInfo` module-level helpers
18
- * exported from this file. Each toast auto-dismisses after `duration` ms and
19
- * can also be dismissed via its button. Appears/disappears instantly — no
20
- * slide/fade transitions.
20
+ * exported from this file. The first argument is the required bold headline; an
21
+ * optional `description` renders a smaller, non-bold second line. Each variant
22
+ * leads with a matching status icon (success → check, error → exclamation
23
+ * circle, info → information circle, warning → exclamation triangle). Each toast
24
+ * auto-dismisses after `duration` ms and can also be dismissed via its ✕
25
+ * button. Appears/disappears instantly — no slide/fade transitions.
21
26
  *
22
27
  * @element toast-notification
23
28
  */
@@ -39,7 +44,7 @@ let ToastNotification = class ToastNotification extends LitElement {
39
44
  display: flex;
40
45
  flex-direction: column;
41
46
  gap: 0.5rem;
42
- max-width: 22.5rem;
47
+ width: 22.5rem;
43
48
  pointer-events: none;
44
49
  }
45
50
  .toast {
@@ -78,8 +83,29 @@ let ToastNotification = class ToastNotification extends LitElement {
78
83
  .toast.info {
79
84
  background: var(--ui-info, #0ea5e9);
80
85
  }
81
- .message {
86
+ .toast.warning {
87
+ background: var(--ui-warning, #d97706);
88
+ }
89
+ .icon {
90
+ flex: 0 0 auto;
91
+ margin-top: 0.125rem;
92
+ line-height: var(--ui-line-height-glyph, 1);
93
+ }
94
+ .content {
82
95
  flex: 1 1 auto;
96
+ min-width: 0;
97
+ display: flex;
98
+ flex-direction: column;
99
+ gap: 0.25rem;
100
+ }
101
+ .message {
102
+ font-weight: var(--ui-font-weight-semibold, 600);
103
+ word-break: break-word;
104
+ }
105
+ .description {
106
+ font-size: var(--ui-font-size-sm, 0.75rem);
107
+ font-weight: var(--ui-font-weight-regular, 400);
108
+ line-height: var(--ui-line-height-normal, 1.5);
83
109
  word-break: break-word;
84
110
  }
85
111
  .close {
@@ -124,7 +150,7 @@ let ToastNotification = class ToastNotification extends LitElement {
124
150
  top: 0.75rem;
125
151
  right: 0.75rem;
126
152
  left: 0.75rem;
127
- max-width: none;
153
+ width: auto;
128
154
  }
129
155
  }
130
156
  `,
@@ -139,7 +165,7 @@ let ToastNotification = class ToastNotification extends LitElement {
139
165
  const id = this._nextId++;
140
166
  const variant = options.variant ?? "info";
141
167
  const duration = options.duration ?? DEFAULT_DURATION_MS;
142
- this._toasts = [...this._toasts, { id, message, variant }];
168
+ this._toasts = [...this._toasts, { id, message, variant, description: options.description }];
143
169
  if (duration > 0) {
144
170
  this._timers.set(id, setTimeout(() => this.dismiss(id), duration));
145
171
  }
@@ -163,6 +189,23 @@ let ToastNotification = class ToastNotification extends LitElement {
163
189
  clearTimeout(timer);
164
190
  this._timers.clear();
165
191
  }
192
+ /**
193
+ * Returns the decorative status icon for a variant, sized as a standalone
194
+ * 18px glyph to match the dismiss button.
195
+ * @param variant The toast variant.
196
+ */
197
+ _variantIcon(variant) {
198
+ switch (variant) {
199
+ case "success":
200
+ return iconCheckCircle(18);
201
+ case "error":
202
+ return iconExclamationCircle(18);
203
+ case "warning":
204
+ return iconExclamationTriangle(18);
205
+ default:
206
+ return iconInfo(18);
207
+ }
208
+ }
166
209
  render() {
167
210
  if (this._toasts.length === 0)
168
211
  return nothing;
@@ -173,7 +216,11 @@ let ToastNotification = class ToastNotification extends LitElement {
173
216
  role=${t.variant === "error" ? "alert" : "status"}
174
217
  aria-atomic="true"
175
218
  >
176
- <span class="message">${t.message}</span>
219
+ <span class="icon" aria-hidden="true">${this._variantIcon(t.variant)}</span>
220
+ <div class="content">
221
+ <span class="message">${t.message}</span>
222
+ ${t.description ? html `<span class="description">${t.description}</span>` : nothing}
223
+ </div>
177
224
  <button class="close" aria-label="Dismiss notification" @click=${() => this.dismiss(t.id)}>
178
225
  <span aria-hidden="true">${iconX(18)}</span>
179
226
  </button>
@@ -193,15 +240,19 @@ function getToast() {
193
240
  return document.querySelector("toast-notification");
194
241
  }
195
242
  /** Shows an error toast. Use for connection/network failures, not validation errors. */
196
- export function notifyError(message) {
197
- getToast()?.show(message, { variant: "error" });
243
+ export function notifyError(message, description) {
244
+ getToast()?.show(message, { variant: "error", description });
198
245
  }
199
246
  /** Shows a success toast. */
200
- export function notifySuccess(message) {
201
- getToast()?.show(message, { variant: "success" });
247
+ export function notifySuccess(message, description) {
248
+ getToast()?.show(message, { variant: "success", description });
202
249
  }
203
250
  /** Shows an info toast. */
204
- export function notifyInfo(message) {
205
- getToast()?.show(message, { variant: "info" });
251
+ export function notifyInfo(message, description) {
252
+ getToast()?.show(message, { variant: "info", description });
253
+ }
254
+ /** Shows a warning toast. */
255
+ export function notifyWarning(message, description) {
256
+ getToast()?.show(message, { variant: "warning", description });
206
257
  }
207
258
  //# sourceMappingURL=toast-notification.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"toast-notification.js","sourceRoot":"","sources":["../src/toast-notification.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, css, html, nothing } from \"lit\";\nimport { customElement, state } from \"lit/decorators.js\";\nimport { iconX } from \"./icons.js\";\nimport { tokens } from \"./tokens.js\";\n\nexport type ToastVariant = \"error\" | \"info\" | \"success\";\n\ninterface ToastOptions {\n /** Visual style; also selects the accent color. Defaults to \"info\". */\n variant?: ToastVariant;\n /** Auto-dismiss delay in ms. Pass 0 to require a manual close. Defaults to 5000. */\n duration?: number;\n}\n\ninterface Toast {\n id: number;\n message: string;\n variant: ToastVariant;\n}\n\nconst DEFAULT_DURATION_MS = 5000;\n\n/**\n * Fixed-position stack of dismissible notifications, anchored top-right\n * (top-full-width on mobile). Not wired to any app state yet — callers add\n * toasts imperatively via the `show()` method on a live element reference,\n * e.g. `document.querySelector('toast-notification')?.show('Offline', { variant: 'error' })`,\n * or via the `notifySuccess`/`notifyError`/`notifyInfo` module-level helpers\n * exported from this file. Each toast auto-dismisses after `duration` ms and\n * can also be dismissed via its ✕ button. Appears/disappears instantly — no\n * slide/fade transitions.\n *\n * @element toast-notification\n */\n@customElement(\"toast-notification\")\nexport class ToastNotification extends LitElement {\n @state() private _toasts: Toast[] = [];\n\n private _nextId = 0;\n private _timers = new Map<number, ReturnType<typeof setTimeout>>();\n\n static override styles = [\n tokens,\n css`\n :host {\n position: fixed;\n top: 1rem;\n right: 1rem;\n z-index: 200;\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n max-width: 22.5rem;\n pointer-events: none;\n }\n .toast {\n pointer-events: auto;\n display: flex;\n align-items: flex-start;\n gap: 0.5rem;\n border-radius: var(--ui-radius, 0.5rem);\n padding: 0.75rem;\n box-shadow: var(\n --ui-shadow-lg,\n 0 20px 25px -5px rgb(0 0 0 / 0.1),\n 0 8px 10px -6px rgb(0 0 0 / 0.1)\n );\n font-family: var(\n --ui-font,\n ui-sans-serif,\n system-ui,\n sans-serif,\n \"Apple Color Emoji\",\n \"Segoe UI Emoji\",\n \"Segoe UI Symbol\",\n \"Noto Color Emoji\"\n );\n font-size: var(--ui-font-size, 0.875rem);\n line-height: var(--ui-line-height-normal, 1.5);\n color: var(--ui-on-accent, #ffffff);\n background: var(--ui-text, #0f172a);\n }\n .toast.error {\n background: var(--ui-danger, #dc2626);\n }\n .toast.success {\n background: var(--ui-success, #16a34a);\n }\n .toast.info {\n background: var(--ui-info, #0ea5e9);\n }\n .message {\n flex: 1 1 auto;\n word-break: break-word;\n }\n .close {\n flex: 0 0 auto;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 2rem;\n height: 2rem;\n background: none;\n border: none;\n padding: 0;\n margin: -0.25rem -0.25rem -0.25rem 0;\n color: inherit;\n opacity: 0.8;\n cursor: pointer;\n line-height: var(--ui-line-height-glyph, 1);\n border-radius: var(--ui-radius-sm, 0.25rem);\n }\n .close:hover {\n opacity: 1;\n background: var(--ui-hover-overlay, rgb(255 255 255 / 0.32));\n }\n .close:focus-visible {\n outline: none;\n opacity: 1;\n box-shadow: var(--ui-focus-ring, 0 0 0 3px rgb(79 70 229 / 0.35));\n }\n @media (forced-colors: active) {\n .toast {\n border: 1px solid CanvasText;\n forced-color-adjust: auto;\n }\n .close:focus-visible {\n outline: 2px solid CanvasText;\n outline-offset: 2px;\n box-shadow: none;\n }\n }\n @media (max-width: 48rem) {\n :host {\n top: 0.75rem;\n right: 0.75rem;\n left: 0.75rem;\n max-width: none;\n }\n }\n `,\n ];\n\n /**\n * Queues a toast for display.\n * @param message Text to show.\n * @param options Variant + auto-dismiss duration (see {@link ToastOptions}).\n * @returns The toast's id — pass to `dismiss()` to remove it early.\n */\n show(message: string, options: ToastOptions = {}): number {\n const id = this._nextId++;\n const variant = options.variant ?? \"info\";\n const duration = options.duration ?? DEFAULT_DURATION_MS;\n this._toasts = [...this._toasts, { id, message, variant }];\n if (duration > 0) {\n this._timers.set(\n id,\n setTimeout(() => this.dismiss(id), duration),\n );\n }\n return id;\n }\n\n /**\n * Removes a toast immediately, cancelling its pending auto-dismiss timer if any.\n * @param id The id returned by `show()`.\n */\n dismiss(id: number) {\n const timer = this._timers.get(id);\n if (timer) {\n clearTimeout(timer);\n this._timers.delete(id);\n }\n this._toasts = this._toasts.filter((t) => t.id !== id);\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback();\n for (const timer of this._timers.values()) clearTimeout(timer);\n this._timers.clear();\n }\n\n override render() {\n if (this._toasts.length === 0) return nothing;\n return html`\n ${this._toasts.map(\n (t) => html`\n <div\n class=\"toast ${t.variant}\"\n role=${t.variant === \"error\" ? \"alert\" : \"status\"}\n aria-atomic=\"true\"\n >\n <span class=\"message\">${t.message}</span>\n <button class=\"close\" aria-label=\"Dismiss notification\" @click=${() => this.dismiss(t.id)}>\n <span aria-hidden=\"true\">${iconX(18)}</span>\n </button>\n </div>\n `,\n )}\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"toast-notification\": ToastNotification;\n }\n}\n\nfunction getToast(): ToastNotification | null {\n return document.querySelector(\"toast-notification\");\n}\n\n/** Shows an error toast. Use for connection/network failures, not validation errors. */\nexport function notifyError(message: string) {\n getToast()?.show(message, { variant: \"error\" });\n}\n\n/** Shows a success toast. */\nexport function notifySuccess(message: string) {\n getToast()?.show(message, { variant: \"success\" });\n}\n\n/** Shows an info toast. */\nexport function notifyInfo(message: string) {\n getToast()?.show(message, { variant: \"info\" });\n}\n"]}
1
+ {"version":3,"file":"toast-notification.js","sourceRoot":"","sources":["../src/toast-notification.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, css, html, nothing } from \"lit\";\nimport { customElement, state } from \"lit/decorators.js\";\nimport {\n iconCheckCircle,\n iconExclamationCircle,\n iconExclamationTriangle,\n iconInfo,\n iconX,\n} from \"./icons.js\";\nimport { tokens } from \"./tokens.js\";\n\nexport type ToastVariant = \"error\" | \"info\" | \"success\" | \"warning\";\n\ninterface ToastOptions {\n /** Visual style; also selects the accent color. Defaults to \"info\". */\n variant?: ToastVariant;\n /** Auto-dismiss delay in ms. Pass 0 to require a manual close. Defaults to 5000. */\n duration?: number;\n /**\n * Optional secondary line shown beneath the headline in smaller, non-bold\n * type. Omit for a single-line toast.\n */\n description?: string;\n}\n\ninterface Toast {\n id: number;\n message: string;\n variant: ToastVariant;\n description?: string;\n}\n\nconst DEFAULT_DURATION_MS = 5000;\n\n/**\n * Fixed-position stack of dismissible notifications, anchored top-right\n * (top-full-width on mobile). Every toast shares one fixed width so entries\n * never appear narrower or wider than one another. Not wired to any app state\n * yet — callers add toasts imperatively via the `show()` method on a live\n * element reference,\n * e.g. `document.querySelector('toast-notification')?.show('Offline', { variant: 'error' })`,\n * or via the `notifySuccess`/`notifyError`/`notifyInfo` module-level helpers\n * exported from this file. The first argument is the required bold headline; an\n * optional `description` renders a smaller, non-bold second line. Each variant\n * leads with a matching status icon (success → check, error → exclamation\n * circle, info → information circle, warning → exclamation triangle). Each toast\n * auto-dismisses after `duration` ms and can also be dismissed via its ✕\n * button. Appears/disappears instantly — no slide/fade transitions.\n *\n * @element toast-notification\n */\n@customElement(\"toast-notification\")\nexport class ToastNotification extends LitElement {\n @state() private _toasts: Toast[] = [];\n\n private _nextId = 0;\n private _timers = new Map<number, ReturnType<typeof setTimeout>>();\n\n static override styles = [\n tokens,\n css`\n :host {\n position: fixed;\n top: 1rem;\n right: 1rem;\n z-index: 200;\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n width: 22.5rem;\n pointer-events: none;\n }\n .toast {\n pointer-events: auto;\n display: flex;\n align-items: flex-start;\n gap: 0.5rem;\n border-radius: var(--ui-radius, 0.5rem);\n padding: 0.75rem;\n box-shadow: var(\n --ui-shadow-lg,\n 0 20px 25px -5px rgb(0 0 0 / 0.1),\n 0 8px 10px -6px rgb(0 0 0 / 0.1)\n );\n font-family: var(\n --ui-font,\n ui-sans-serif,\n system-ui,\n sans-serif,\n \"Apple Color Emoji\",\n \"Segoe UI Emoji\",\n \"Segoe UI Symbol\",\n \"Noto Color Emoji\"\n );\n font-size: var(--ui-font-size, 0.875rem);\n line-height: var(--ui-line-height-normal, 1.5);\n color: var(--ui-on-accent, #ffffff);\n background: var(--ui-text, #0f172a);\n }\n .toast.error {\n background: var(--ui-danger, #dc2626);\n }\n .toast.success {\n background: var(--ui-success, #16a34a);\n }\n .toast.info {\n background: var(--ui-info, #0ea5e9);\n }\n .toast.warning {\n background: var(--ui-warning, #d97706);\n }\n .icon {\n flex: 0 0 auto;\n margin-top: 0.125rem;\n line-height: var(--ui-line-height-glyph, 1);\n }\n .content {\n flex: 1 1 auto;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 0.25rem;\n }\n .message {\n font-weight: var(--ui-font-weight-semibold, 600);\n word-break: break-word;\n }\n .description {\n font-size: var(--ui-font-size-sm, 0.75rem);\n font-weight: var(--ui-font-weight-regular, 400);\n line-height: var(--ui-line-height-normal, 1.5);\n word-break: break-word;\n }\n .close {\n flex: 0 0 auto;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 2rem;\n height: 2rem;\n background: none;\n border: none;\n padding: 0;\n margin: -0.25rem -0.25rem -0.25rem 0;\n color: inherit;\n opacity: 0.8;\n cursor: pointer;\n line-height: var(--ui-line-height-glyph, 1);\n border-radius: var(--ui-radius-sm, 0.25rem);\n }\n .close:hover {\n opacity: 1;\n background: var(--ui-hover-overlay, rgb(255 255 255 / 0.32));\n }\n .close:focus-visible {\n outline: none;\n opacity: 1;\n box-shadow: var(--ui-focus-ring, 0 0 0 3px rgb(79 70 229 / 0.35));\n }\n @media (forced-colors: active) {\n .toast {\n border: 1px solid CanvasText;\n forced-color-adjust: auto;\n }\n .close:focus-visible {\n outline: 2px solid CanvasText;\n outline-offset: 2px;\n box-shadow: none;\n }\n }\n @media (max-width: 48rem) {\n :host {\n top: 0.75rem;\n right: 0.75rem;\n left: 0.75rem;\n width: auto;\n }\n }\n `,\n ];\n\n /**\n * Queues a toast for display.\n * @param message Text to show.\n * @param options Variant + auto-dismiss duration (see {@link ToastOptions}).\n * @returns The toast's id — pass to `dismiss()` to remove it early.\n */\n show(message: string, options: ToastOptions = {}): number {\n const id = this._nextId++;\n const variant = options.variant ?? \"info\";\n const duration = options.duration ?? DEFAULT_DURATION_MS;\n this._toasts = [...this._toasts, { id, message, variant, description: options.description }];\n if (duration > 0) {\n this._timers.set(\n id,\n setTimeout(() => this.dismiss(id), duration),\n );\n }\n return id;\n }\n\n /**\n * Removes a toast immediately, cancelling its pending auto-dismiss timer if any.\n * @param id The id returned by `show()`.\n */\n dismiss(id: number) {\n const timer = this._timers.get(id);\n if (timer) {\n clearTimeout(timer);\n this._timers.delete(id);\n }\n this._toasts = this._toasts.filter((t) => t.id !== id);\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback();\n for (const timer of this._timers.values()) clearTimeout(timer);\n this._timers.clear();\n }\n\n /**\n * Returns the decorative status icon for a variant, sized as a standalone\n * 18px glyph to match the dismiss button.\n * @param variant The toast variant.\n */\n private _variantIcon(variant: ToastVariant) {\n switch (variant) {\n case \"success\":\n return iconCheckCircle(18);\n case \"error\":\n return iconExclamationCircle(18);\n case \"warning\":\n return iconExclamationTriangle(18);\n default:\n return iconInfo(18);\n }\n }\n\n override render() {\n if (this._toasts.length === 0) return nothing;\n return html`\n ${this._toasts.map(\n (t) => html`\n <div\n class=\"toast ${t.variant}\"\n role=${t.variant === \"error\" ? \"alert\" : \"status\"}\n aria-atomic=\"true\"\n >\n <span class=\"icon\" aria-hidden=\"true\">${this._variantIcon(t.variant)}</span>\n <div class=\"content\">\n <span class=\"message\">${t.message}</span>\n ${t.description ? html`<span class=\"description\">${t.description}</span>` : nothing}\n </div>\n <button class=\"close\" aria-label=\"Dismiss notification\" @click=${() => this.dismiss(t.id)}>\n <span aria-hidden=\"true\">${iconX(18)}</span>\n </button>\n </div>\n `,\n )}\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"toast-notification\": ToastNotification;\n }\n}\n\nfunction getToast(): ToastNotification | null {\n return document.querySelector(\"toast-notification\");\n}\n\n/** Shows an error toast. Use for connection/network failures, not validation errors. */\nexport function notifyError(message: string, description?: string) {\n getToast()?.show(message, { variant: \"error\", description });\n}\n\n/** Shows a success toast. */\nexport function notifySuccess(message: string, description?: string) {\n getToast()?.show(message, { variant: \"success\", description });\n}\n\n/** Shows an info toast. */\nexport function notifyInfo(message: string, description?: string) {\n getToast()?.show(message, { variant: \"info\", description });\n}\n\n/** Shows a warning toast. */\nexport function notifyWarning(message: string, description?: string) {\n getToast()?.show(message, { variant: \"warning\", description });\n}\n"]}
@@ -20,6 +20,10 @@ export interface TreeNode {
20
20
  * internally and untouched by later `nodes` updates, so a user's manual
21
21
  * toggles survive a data refresh.
22
22
  *
23
+ * Set the `lines` boolean to draw classic file-tree connector guides: a
24
+ * vertical line per continuing ancestor level and a branch elbow (`└`) or
25
+ * tee (`├`) linking each node to its parent. Off by default (indentation only).
26
+ *
23
27
  * @element tree-view
24
28
  * @fires node-click - A leaf row was activated; detail is `{ id, data }`.
25
29
  */
@@ -32,6 +36,11 @@ export declare class TreeView extends LitElement {
32
36
  renderNode: (node: TreeNode) => unknown;
33
37
  /** Start every folder expanded instead of the default all-collapsed. */
34
38
  defaultExpanded: boolean;
39
+ /**
40
+ * Draw classic file-tree connector guides (vertical ancestor lines plus a
41
+ * branch elbow/tee per row) instead of indentation alone. Off by default.
42
+ */
43
+ lines: boolean;
35
44
  private expanded;
36
45
  willUpdate(changed: Map<string, unknown>): void;
37
46
  render(): unknown;
@@ -1 +1 @@
1
- {"version":3,"file":"tree-view.d.ts","sourceRoot":"","sources":["../src/tree-view.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAA2C,MAAM,KAAK,CAAC;AAM1E,oFAAoF;AACpF,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,QAAQ,EAAE,CAAC;IACtB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,qBACa,QAAS,SAAQ,UAAU;;IACtC,OAAgB,MAAM,4BAsDpB;IAEF,iFAAiF;IACjD,KAAK,EAAE,QAAQ,EAAE,CAAM;IACvD,+EAA+E;IAC/C,UAAU,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,OAAO,CAAwB;IAC/F,wEAAwE;IACZ,eAAe,UAAS;IAE3E,OAAO,CAAC,QAAQ,CAAqB;IAGrC,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAcvD;IAyEQ,MAAM,YAMd;CACF;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,WAAW,EAAE,QAAQ,CAAC;KACvB;CACF"}
1
+ {"version":3,"file":"tree-view.d.ts","sourceRoot":"","sources":["../src/tree-view.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAA2C,MAAM,KAAK,CAAC;AAM1E,oFAAoF;AACpF,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,QAAQ,EAAE,CAAC;IACtB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBACa,QAAS,SAAQ,UAAU;;IACtC,OAAgB,MAAM,4BA8FpB;IAEF,iFAAiF;IACjD,KAAK,EAAE,QAAQ,EAAE,CAAM;IACvD,+EAA+E;IAC/C,UAAU,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,OAAO,CAAwB;IAC/F,wEAAwE;IACZ,eAAe,UAAS;IACpF;;;OAGG;IACyC,KAAK,UAAS;IAEjD,OAAO,CAAC,QAAQ,CAAqB;IAGrC,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAcvD;IA2FQ,MAAM,YAMd;CACF;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,WAAW,EAAE,QAAQ,CAAC;KACvB;CACF"}
package/dist/tree-view.js CHANGED
@@ -23,6 +23,10 @@ import { tokens } from "./tokens.js";
23
23
  * internally and untouched by later `nodes` updates, so a user's manual
24
24
  * toggles survive a data refresh.
25
25
  *
26
+ * Set the `lines` boolean to draw classic file-tree connector guides: a
27
+ * vertical line per continuing ancestor level and a branch elbow (`└`) or
28
+ * tee (`├`) linking each node to its parent. Off by default (indentation only).
29
+ *
26
30
  * @element tree-view
27
31
  * @fires node-click - A leaf row was activated; detail is `{ id, data }`.
28
32
  */
@@ -35,6 +39,11 @@ let TreeView = class TreeView extends LitElement {
35
39
  this.renderNode = (node) => node.label;
36
40
  /** Start every folder expanded instead of the default all-collapsed. */
37
41
  this.defaultExpanded = false;
42
+ /**
43
+ * Draw classic file-tree connector guides (vertical ancestor lines plus a
44
+ * branch elbow/tee per row) instead of indentation alone. Off by default.
45
+ */
46
+ this.lines = false;
38
47
  this.expanded = new Set();
39
48
  this.#initializedExpansion = false;
40
49
  }
@@ -84,12 +93,52 @@ let TreeView = class TreeView extends LitElement {
84
93
  min-width: 0;
85
94
  flex: 1;
86
95
  }
96
+ :host([lines]) .row {
97
+ padding-top: 0;
98
+ padding-bottom: 0;
99
+ min-height: 1.5rem;
100
+ }
101
+ .guides {
102
+ display: flex;
103
+ align-self: stretch;
104
+ flex-shrink: 0;
105
+ }
106
+ .guide {
107
+ position: relative;
108
+ flex: 0 0 auto;
109
+ width: 1rem;
110
+ }
111
+ .guide.v::before,
112
+ .guide.connector::before {
113
+ content: "";
114
+ position: absolute;
115
+ left: 50%;
116
+ top: 0;
117
+ bottom: 0;
118
+ border-left: 1px solid var(--ui-border, #e2e8f0);
119
+ }
120
+ .guide.connector.last::before {
121
+ bottom: 50%;
122
+ }
123
+ .guide.connector::after {
124
+ content: "";
125
+ position: absolute;
126
+ left: 50%;
127
+ right: 0;
128
+ top: 50%;
129
+ border-top: 1px solid var(--ui-border, #e2e8f0);
130
+ }
87
131
  @media (forced-colors: active) {
88
132
  .row:focus-visible {
89
133
  outline: 2px solid CanvasText;
90
134
  outline-offset: -2px;
91
135
  box-shadow: none;
92
136
  }
137
+ .guide.v::before,
138
+ .guide.connector::before,
139
+ .guide.connector::after {
140
+ border-color: CanvasText;
141
+ }
93
142
  }
94
143
  `,
95
144
  ]; }
@@ -154,31 +203,41 @@ let TreeView = class TreeView extends LitElement {
154
203
  }
155
204
  return false;
156
205
  }
157
- #renderNode(node, depth) {
206
+ #renderNode(node, depth, isLast, ancestorHasNext) {
158
207
  const isFolder = !!node.children;
159
208
  const isOpen = isFolder && this.expanded.has(node.id);
209
+ const guides = this.lines
210
+ ? html `<span class="guides" aria-hidden="true">
211
+ ${ancestorHasNext.map((hasNext) => html `<span class="guide ${hasNext ? "v" : ""}"></span>`)}
212
+ <span class="guide connector ${isLast ? "last" : ""}"></span>
213
+ </span>`
214
+ : nothing;
160
215
  return html `
161
216
  <div
162
217
  class="row"
163
218
  role=${isFolder ? "button" : "link"}
164
219
  aria-expanded=${isFolder ? String(isOpen) : nothing}
165
220
  tabindex="0"
166
- style="padding-left: ${depth * 1.25}rem"
221
+ style=${this.lines ? nothing : `padding-left: ${depth * 1.25}rem`}
167
222
  @click=${(e) => this.#onRowClick(node, e)}
168
223
  @keydown=${(e) => this.#onKeydown(node, e)}
169
224
  >
225
+ ${guides}
170
226
  <span class="toggle" aria-hidden="true">
171
227
  ${isFolder ? (isOpen ? iconChevronDown(14) : iconChevronRight(14)) : nothing}
172
228
  </span>
173
229
  <span class="content">${this.renderNode(node)}</span>
174
230
  </div>
175
231
  ${isFolder && isOpen
176
- ? repeat(node.children, (child) => child.id, (child) => this.#renderNode(child, depth + 1))
232
+ ? repeat(node.children, (child) => child.id, (child, i) => this.#renderNode(child, depth + 1, i === node.children.length - 1, [
233
+ ...ancestorHasNext,
234
+ !isLast,
235
+ ]))
177
236
  : nothing}
178
237
  `;
179
238
  }
180
239
  render() {
181
- return repeat(this.nodes, (node) => node.id, (node) => this.#renderNode(node, 0));
240
+ return repeat(this.nodes, (node) => node.id, (node, i) => this.#renderNode(node, 0, i === this.nodes.length - 1, []));
182
241
  }
183
242
  };
184
243
  __decorate([
@@ -190,6 +249,9 @@ __decorate([
190
249
  __decorate([
191
250
  property({ type: Boolean, attribute: "default-expanded" })
192
251
  ], TreeView.prototype, "defaultExpanded", void 0);
252
+ __decorate([
253
+ property({ type: Boolean, reflect: true })
254
+ ], TreeView.prototype, "lines", void 0);
193
255
  __decorate([
194
256
  state()
195
257
  ], TreeView.prototype, "expanded", void 0);
@@ -1 +1 @@
1
- {"version":3,"file":"tree-view.js","sourceRoot":"","sources":["../src/tree-view.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, css, html, nothing, type TemplateResult } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport { repeat } from \"lit/directives/repeat.js\";\nimport { iconChevronDown, iconChevronRight } from \"./icons.js\";\nimport { tokens } from \"./tokens.js\";\n\n/** One tree node: a folder when `children` is set (even `[]`), a leaf otherwise. */\nexport interface TreeNode {\n id: string;\n label: string;\n children?: TreeNode[];\n data?: unknown;\n}\n\n/**\n * A generic, presentational tree shell: renders `nodes` recursively, one row\n * per node, with each row's content produced by `renderNode` (default: plain\n * label). Modeled on `data-table`'s headless pattern — knows nothing about\n * what a node's `data` means beyond what `renderNode` does with it.\n *\n * A node with a `children` array (even empty) is a folder: clicking or\n * activating its row toggles expand/collapse instead of firing `node-click`.\n * A node with no `children` is a leaf: clicking or activating its row fires\n * `node-click`. Folders start collapsed; set `default-expanded` to start\n * every folder expanded instead. Expansion state is otherwise managed\n * internally and untouched by later `nodes` updates, so a user's manual\n * toggles survive a data refresh.\n *\n * @element tree-view\n * @fires node-click - A leaf row was activated; detail is `{ id, data }`.\n */\n@customElement(\"tree-view\")\nexport class TreeView extends LitElement {\n static override styles = [\n tokens,\n css`\n :host {\n display: block;\n font-family: var(\n --ui-font,\n ui-sans-serif,\n system-ui,\n sans-serif,\n \"Apple Color Emoji\",\n \"Segoe UI Emoji\",\n \"Segoe UI Symbol\",\n \"Noto Color Emoji\"\n );\n font-size: var(--ui-font-size-sm, 0.75rem);\n }\n .row {\n display: flex;\n align-items: center;\n gap: 0.25rem;\n padding: 0.25rem 0.5rem;\n border-radius: var(--ui-radius-sm, 0.25rem);\n color: var(--ui-text, #0f172a);\n cursor: pointer;\n }\n .row:hover {\n background: var(--ui-surface-muted, #f8fafc);\n }\n .row:focus-visible {\n outline: none;\n box-shadow: var(--ui-focus-ring, 0 0 0 3px rgb(79 70 229 / 0.35));\n }\n .toggle {\n display: inline-flex;\n flex-shrink: 0;\n width: 1rem;\n height: 1rem;\n align-items: center;\n justify-content: center;\n color: var(--ui-text-muted, #64748b);\n }\n .content {\n min-width: 0;\n flex: 1;\n }\n @media (forced-colors: active) {\n .row:focus-visible {\n outline: 2px solid CanvasText;\n outline-offset: -2px;\n box-shadow: none;\n }\n }\n `,\n ];\n\n /** Tree data; opaque to this component beyond what `renderNode` does with it. */\n @property({ attribute: false }) nodes: TreeNode[] = [];\n /** Produces a row's rendered content for `node`. Default: plain label text. */\n @property({ attribute: false }) renderNode: (node: TreeNode) => unknown = (node) => node.label;\n /** Start every folder expanded instead of the default all-collapsed. */\n @property({ type: Boolean, attribute: \"default-expanded\" }) defaultExpanded = false;\n\n @state() private expanded = new Set<string>();\n #initializedExpansion = false;\n\n override willUpdate(changed: Map<string, unknown>): void {\n if (!changed.has(\"nodes\") || this.#initializedExpansion) return;\n this.#initializedExpansion = true;\n if (!this.defaultExpanded) return;\n const all = new Set<string>();\n const collect = (list: TreeNode[]) => {\n for (const node of list) {\n if (!node.children) continue;\n all.add(node.id);\n collect(node.children);\n }\n };\n collect(this.nodes);\n this.expanded = all;\n }\n\n #toggle(id: string): void {\n const next = new Set(this.expanded);\n if (next.has(id)) next.delete(id);\n else next.add(id);\n this.expanded = next;\n }\n\n #activate(node: TreeNode): void {\n if (node.children) {\n this.#toggle(node.id);\n return;\n }\n this.dispatchEvent(\n new CustomEvent(\"node-click\", {\n detail: { id: node.id, data: node.data },\n bubbles: true,\n composed: true,\n }),\n );\n }\n\n #onKeydown(node: TreeNode, e: KeyboardEvent): void {\n if (e.key !== \"Enter\" && e.key !== \" \") return;\n if (e.target !== e.currentTarget) return;\n e.preventDefault();\n this.#activate(node);\n }\n\n #onRowClick(node: TreeNode, e: MouseEvent): void {\n if (this.#isNestedInteractive(e.composedPath(), e.currentTarget)) return;\n this.#activate(node);\n }\n\n #isNestedInteractive(path: EventTarget[], row: EventTarget | null): boolean {\n const selector =\n \"a, button, input, select, textarea, summary, [contenteditable], [role='button'], [role='link'], [tabindex]\";\n for (const target of path) {\n if (target === row) return false;\n if (target instanceof HTMLElement && target.matches(selector)) return true;\n }\n return false;\n }\n\n #renderNode(node: TreeNode, depth: number): TemplateResult {\n const isFolder = !!node.children;\n const isOpen = isFolder && this.expanded.has(node.id);\n return html`\n <div\n class=\"row\"\n role=${isFolder ? \"button\" : \"link\"}\n aria-expanded=${isFolder ? String(isOpen) : nothing}\n tabindex=\"0\"\n style=\"padding-left: ${depth * 1.25}rem\"\n @click=${(e: MouseEvent) => this.#onRowClick(node, e)}\n @keydown=${(e: KeyboardEvent) => this.#onKeydown(node, e)}\n >\n <span class=\"toggle\" aria-hidden=\"true\">\n ${isFolder ? (isOpen ? iconChevronDown(14) : iconChevronRight(14)) : nothing}\n </span>\n <span class=\"content\">${this.renderNode(node)}</span>\n </div>\n ${isFolder && isOpen\n ? repeat(\n node.children!,\n (child) => child.id,\n (child) => this.#renderNode(child, depth + 1),\n )\n : nothing}\n `;\n }\n\n override render() {\n return repeat(\n this.nodes,\n (node) => node.id,\n (node) => this.#renderNode(node, 0),\n );\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"tree-view\": TreeView;\n }\n}\n"]}
1
+ {"version":3,"file":"tree-view.js","sourceRoot":"","sources":["../src/tree-view.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, css, html, nothing, type TemplateResult } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport { repeat } from \"lit/directives/repeat.js\";\nimport { iconChevronDown, iconChevronRight } from \"./icons.js\";\nimport { tokens } from \"./tokens.js\";\n\n/** One tree node: a folder when `children` is set (even `[]`), a leaf otherwise. */\nexport interface TreeNode {\n id: string;\n label: string;\n children?: TreeNode[];\n data?: unknown;\n}\n\n/**\n * A generic, presentational tree shell: renders `nodes` recursively, one row\n * per node, with each row's content produced by `renderNode` (default: plain\n * label). Modeled on `data-table`'s headless pattern — knows nothing about\n * what a node's `data` means beyond what `renderNode` does with it.\n *\n * A node with a `children` array (even empty) is a folder: clicking or\n * activating its row toggles expand/collapse instead of firing `node-click`.\n * A node with no `children` is a leaf: clicking or activating its row fires\n * `node-click`. Folders start collapsed; set `default-expanded` to start\n * every folder expanded instead. Expansion state is otherwise managed\n * internally and untouched by later `nodes` updates, so a user's manual\n * toggles survive a data refresh.\n *\n * Set the `lines` boolean to draw classic file-tree connector guides: a\n * vertical line per continuing ancestor level and a branch elbow (`└`) or\n * tee (`├`) linking each node to its parent. Off by default (indentation only).\n *\n * @element tree-view\n * @fires node-click - A leaf row was activated; detail is `{ id, data }`.\n */\n@customElement(\"tree-view\")\nexport class TreeView extends LitElement {\n static override styles = [\n tokens,\n css`\n :host {\n display: block;\n font-family: var(\n --ui-font,\n ui-sans-serif,\n system-ui,\n sans-serif,\n \"Apple Color Emoji\",\n \"Segoe UI Emoji\",\n \"Segoe UI Symbol\",\n \"Noto Color Emoji\"\n );\n font-size: var(--ui-font-size-sm, 0.75rem);\n }\n .row {\n display: flex;\n align-items: center;\n gap: 0.25rem;\n padding: 0.25rem 0.5rem;\n border-radius: var(--ui-radius-sm, 0.25rem);\n color: var(--ui-text, #0f172a);\n cursor: pointer;\n }\n .row:hover {\n background: var(--ui-surface-muted, #f8fafc);\n }\n .row:focus-visible {\n outline: none;\n box-shadow: var(--ui-focus-ring, 0 0 0 3px rgb(79 70 229 / 0.35));\n }\n .toggle {\n display: inline-flex;\n flex-shrink: 0;\n width: 1rem;\n height: 1rem;\n align-items: center;\n justify-content: center;\n color: var(--ui-text-muted, #64748b);\n }\n .content {\n min-width: 0;\n flex: 1;\n }\n :host([lines]) .row {\n padding-top: 0;\n padding-bottom: 0;\n min-height: 1.5rem;\n }\n .guides {\n display: flex;\n align-self: stretch;\n flex-shrink: 0;\n }\n .guide {\n position: relative;\n flex: 0 0 auto;\n width: 1rem;\n }\n .guide.v::before,\n .guide.connector::before {\n content: \"\";\n position: absolute;\n left: 50%;\n top: 0;\n bottom: 0;\n border-left: 1px solid var(--ui-border, #e2e8f0);\n }\n .guide.connector.last::before {\n bottom: 50%;\n }\n .guide.connector::after {\n content: \"\";\n position: absolute;\n left: 50%;\n right: 0;\n top: 50%;\n border-top: 1px solid var(--ui-border, #e2e8f0);\n }\n @media (forced-colors: active) {\n .row:focus-visible {\n outline: 2px solid CanvasText;\n outline-offset: -2px;\n box-shadow: none;\n }\n .guide.v::before,\n .guide.connector::before,\n .guide.connector::after {\n border-color: CanvasText;\n }\n }\n `,\n ];\n\n /** Tree data; opaque to this component beyond what `renderNode` does with it. */\n @property({ attribute: false }) nodes: TreeNode[] = [];\n /** Produces a row's rendered content for `node`. Default: plain label text. */\n @property({ attribute: false }) renderNode: (node: TreeNode) => unknown = (node) => node.label;\n /** Start every folder expanded instead of the default all-collapsed. */\n @property({ type: Boolean, attribute: \"default-expanded\" }) defaultExpanded = false;\n /**\n * Draw classic file-tree connector guides (vertical ancestor lines plus a\n * branch elbow/tee per row) instead of indentation alone. Off by default.\n */\n @property({ type: Boolean, reflect: true }) lines = false;\n\n @state() private expanded = new Set<string>();\n #initializedExpansion = false;\n\n override willUpdate(changed: Map<string, unknown>): void {\n if (!changed.has(\"nodes\") || this.#initializedExpansion) return;\n this.#initializedExpansion = true;\n if (!this.defaultExpanded) return;\n const all = new Set<string>();\n const collect = (list: TreeNode[]) => {\n for (const node of list) {\n if (!node.children) continue;\n all.add(node.id);\n collect(node.children);\n }\n };\n collect(this.nodes);\n this.expanded = all;\n }\n\n #toggle(id: string): void {\n const next = new Set(this.expanded);\n if (next.has(id)) next.delete(id);\n else next.add(id);\n this.expanded = next;\n }\n\n #activate(node: TreeNode): void {\n if (node.children) {\n this.#toggle(node.id);\n return;\n }\n this.dispatchEvent(\n new CustomEvent(\"node-click\", {\n detail: { id: node.id, data: node.data },\n bubbles: true,\n composed: true,\n }),\n );\n }\n\n #onKeydown(node: TreeNode, e: KeyboardEvent): void {\n if (e.key !== \"Enter\" && e.key !== \" \") return;\n if (e.target !== e.currentTarget) return;\n e.preventDefault();\n this.#activate(node);\n }\n\n #onRowClick(node: TreeNode, e: MouseEvent): void {\n if (this.#isNestedInteractive(e.composedPath(), e.currentTarget)) return;\n this.#activate(node);\n }\n\n #isNestedInteractive(path: EventTarget[], row: EventTarget | null): boolean {\n const selector =\n \"a, button, input, select, textarea, summary, [contenteditable], [role='button'], [role='link'], [tabindex]\";\n for (const target of path) {\n if (target === row) return false;\n if (target instanceof HTMLElement && target.matches(selector)) return true;\n }\n return false;\n }\n\n #renderNode(\n node: TreeNode,\n depth: number,\n isLast: boolean,\n ancestorHasNext: boolean[],\n ): TemplateResult {\n const isFolder = !!node.children;\n const isOpen = isFolder && this.expanded.has(node.id);\n const guides = this.lines\n ? html`<span class=\"guides\" aria-hidden=\"true\">\n ${ancestorHasNext.map(\n (hasNext) => html`<span class=\"guide ${hasNext ? \"v\" : \"\"}\"></span>`,\n )}\n <span class=\"guide connector ${isLast ? \"last\" : \"\"}\"></span>\n </span>`\n : nothing;\n return html`\n <div\n class=\"row\"\n role=${isFolder ? \"button\" : \"link\"}\n aria-expanded=${isFolder ? String(isOpen) : nothing}\n tabindex=\"0\"\n style=${this.lines ? nothing : `padding-left: ${depth * 1.25}rem`}\n @click=${(e: MouseEvent) => this.#onRowClick(node, e)}\n @keydown=${(e: KeyboardEvent) => this.#onKeydown(node, e)}\n >\n ${guides}\n <span class=\"toggle\" aria-hidden=\"true\">\n ${isFolder ? (isOpen ? iconChevronDown(14) : iconChevronRight(14)) : nothing}\n </span>\n <span class=\"content\">${this.renderNode(node)}</span>\n </div>\n ${isFolder && isOpen\n ? repeat(\n node.children!,\n (child) => child.id,\n (child, i) =>\n this.#renderNode(child, depth + 1, i === node.children!.length - 1, [\n ...ancestorHasNext,\n !isLast,\n ]),\n )\n : nothing}\n `;\n }\n\n override render() {\n return repeat(\n this.nodes,\n (node) => node.id,\n (node, i) => this.#renderNode(node, 0, i === this.nodes.length - 1, []),\n );\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"tree-view\": TreeView;\n }\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { LitElement, type PropertyValues } from "lit";
1
+ import { LitElement, type PropertyValues, type TemplateResult } from "lit";
2
2
  /**
3
3
  * A form-associated boolean checkbox, usable standalone or inside a native
4
4
  * `<form>`. Submits `name=on` when checked (matching native
@@ -8,7 +8,11 @@ import { LitElement, type PropertyValues } from "lit";
8
8
  * Renders a native `<input type="checkbox">` wrapped in a `<label>`, styled
9
9
  * via `:has()` on the wrapping label (matching `radio-pills`/`radio-cards`)
10
10
  * rather than styling the native input directly; the checkbox itself renders
11
- * at `1rem`, matching the existing radio-input convention.
11
+ * at `1rem`, matching the existing radio-input convention. An optional
12
+ * pre-rendered `icon` (matching `form-select`'s per-option icon convention)
13
+ * renders between the box and the label, inside the same clickable `<label>`
14
+ * — for a row that pairs a checkbox with an icon/swatch and needs the whole
15
+ * row, icon included, to stay one click target.
12
16
  *
13
17
  * @element ui-checkbox
14
18
  * @fires change - The checkbox was toggled by the user, in either direction;
@@ -30,6 +34,10 @@ export declare class UiCheckbox extends LitElement {
30
34
  name: string;
31
35
  /** Visible label text rendered next to the box. */
32
36
  label: string;
37
+ /** Pre-rendered icon template displayed between the box and the label, e.g. `iconPencil(14)` from this package's icon set. */
38
+ icon: TemplateResult | null;
39
+ /** Square icon size in pixels — 14 (inline icon size) by default. */
40
+ iconSize: number;
33
41
  private _formDisabled;
34
42
  protected willUpdate(_changed: PropertyValues): void;
35
43
  protected updated(changed: PropertyValues): void;
@@ -39,7 +47,7 @@ export declare class UiCheckbox extends LitElement {
39
47
  formDisabledCallback(disabled: boolean): void;
40
48
  /** Restores the checked state from saved form state (bfcache/autofill). */
41
49
  formStateRestoreCallback(state: string | File | FormData | null): void;
42
- render(): import("lit-html").TemplateResult<1>;
50
+ render(): TemplateResult<1>;
43
51
  }
44
52
  declare global {
45
53
  interface HTMLElementTagNameMap {
@@ -1 +1 @@
1
- {"version":3,"file":"ui-checkbox.d.ts","sourceRoot":"","sources":["../src/ui-checkbox.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAKjE;;;;;;;;;;;;;;GAcG;AACH,qBACa,UAAW,SAAQ,UAAU;;IACxC,MAAM,CAAC,cAAc,UAAQ;IAE7B,OAAgB,MAAM,4BAkEpB;IAEF,kCAAkC;IACL,OAAO,UAAS;IAC7C,8EAA8E;IACjD,aAAa,UAAS;IACnD,2EAA2E;IAC9C,QAAQ,UAAS;IAC9C,wEAAwE;IAC3C,QAAQ,UAAS;IAC9C,kEAAkE;IACtD,IAAI,SAAM;IACtB,mDAAmD;IACvC,KAAK,SAAM;IAEd,OAAO,CAAC,aAAa,CAAS;IAWvC,UAAmB,UAAU,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI,CAE5D;IAED,UAAmB,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,CAQxD;IAED,kFAAkF;IAClF,iBAAiB,IAAI,IAAI,CAGxB;IAED,qDAAqD;IACrD,oBAAoB,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAE5C;IAED,2EAA2E;IAC3E,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,QAAQ,GAAG,IAAI,GAAG,IAAI,CAGrE;IAwCQ,MAAM,yCAoBd;CACF;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,aAAa,EAAE,UAAU,CAAC;KAC3B;CACF"}
1
+ {"version":3,"file":"ui-checkbox.d.ts","sourceRoot":"","sources":["../src/ui-checkbox.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAsB,KAAK,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAK/F;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBACa,UAAW,SAAQ,UAAU;;IACxC,MAAM,CAAC,cAAc,UAAQ;IAE7B,OAAgB,MAAM,4BA4EpB;IAEF,kCAAkC;IACL,OAAO,UAAS;IAC7C,8EAA8E;IACjD,aAAa,UAAS;IACnD,2EAA2E;IAC9C,QAAQ,UAAS;IAC9C,wEAAwE;IAC3C,QAAQ,UAAS;IAC9C,kEAAkE;IACtD,IAAI,SAAM;IACtB,mDAAmD;IACvC,KAAK,SAAM;IACvB,8HAA8H;IAC9F,IAAI,EAAE,cAAc,GAAG,IAAI,CAAQ;IACnE,qEAAqE;IACzC,QAAQ,SAAM;IAEjC,OAAO,CAAC,aAAa,CAAS;IAWvC,UAAmB,UAAU,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI,CAE5D;IAED,UAAmB,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,CAQxD;IAED,kFAAkF;IAClF,iBAAiB,IAAI,IAAI,CAGxB;IAED,qDAAqD;IACrD,oBAAoB,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAE5C;IAED,2EAA2E;IAC3E,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,QAAQ,GAAG,IAAI,GAAG,IAAI,CAGrE;IAwCQ,MAAM,sBAyBd;CACF;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,aAAa,EAAE,UAAU,CAAC;KAC3B;CACF"}
@@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { LitElement, css, html } from "lit";
7
+ import { LitElement, css, html, nothing } from "lit";
8
8
  import { customElement, property, state } from "lit/decorators.js";
9
9
  import { ref } from "lit/directives/ref.js";
10
10
  import { tokens } from "./tokens.js";
@@ -17,7 +17,11 @@ import { tokens } from "./tokens.js";
17
17
  * Renders a native `<input type="checkbox">` wrapped in a `<label>`, styled
18
18
  * via `:has()` on the wrapping label (matching `radio-pills`/`radio-cards`)
19
19
  * rather than styling the native input directly; the checkbox itself renders
20
- * at `1rem`, matching the existing radio-input convention.
20
+ * at `1rem`, matching the existing radio-input convention. An optional
21
+ * pre-rendered `icon` (matching `form-select`'s per-option icon convention)
22
+ * renders between the box and the label, inside the same clickable `<label>`
23
+ * — for a row that pairs a checkbox with an icon/swatch and needs the whole
24
+ * row, icon included, to stay one click target.
21
25
  *
22
26
  * @element ui-checkbox
23
27
  * @fires change - The checkbox was toggled by the user, in either direction;
@@ -38,6 +42,10 @@ let UiCheckbox = class UiCheckbox extends LitElement {
38
42
  this.name = "";
39
43
  /** Visible label text rendered next to the box. */
40
44
  this.label = "";
45
+ /** Pre-rendered icon template displayed between the box and the label, e.g. `iconPencil(14)` from this package's icon set. */
46
+ this.icon = null;
47
+ /** Square icon size in pixels — 14 (inline icon size) by default. */
48
+ this.iconSize = 14;
41
49
  this._formDisabled = false;
42
50
  this.#internals = this.attachInternals();
43
51
  this.#defaultChecked = false;
@@ -91,6 +99,16 @@ let UiCheckbox = class UiCheckbox extends LitElement {
91
99
  .required-mark {
92
100
  color: var(--ui-danger, #dc2626);
93
101
  }
102
+ .checkbox-icon {
103
+ display: inline-flex;
104
+ width: var(--checkbox-icon-size);
105
+ height: var(--checkbox-icon-size);
106
+ flex: 0 0 var(--checkbox-icon-size);
107
+ }
108
+ .checkbox-icon > svg {
109
+ width: 100%;
110
+ height: 100%;
111
+ }
94
112
  .sr-only {
95
113
  position: absolute;
96
114
  width: 1px;
@@ -189,6 +207,11 @@ let UiCheckbox = class UiCheckbox extends LitElement {
189
207
  ?required=${this.required}
190
208
  @change=${(e) => this.#onChange(e)}
191
209
  />
210
+ ${this.icon
211
+ ? html `<span class="checkbox-icon" style=${`--checkbox-icon-size: ${this.iconSize}px`} aria-hidden="true"
212
+ >${this.icon}</span
213
+ >`
214
+ : nothing}
192
215
  <span
193
216
  >${this.label}${this.required
194
217
  ? html `<span class="required-mark" aria-hidden="true"> *</span><span class="sr-only">
@@ -218,6 +241,12 @@ __decorate([
218
241
  __decorate([
219
242
  property()
220
243
  ], UiCheckbox.prototype, "label", void 0);
244
+ __decorate([
245
+ property({ attribute: false })
246
+ ], UiCheckbox.prototype, "icon", void 0);
247
+ __decorate([
248
+ property({ type: Number })
249
+ ], UiCheckbox.prototype, "iconSize", void 0);
221
250
  __decorate([
222
251
  state()
223
252
  ], UiCheckbox.prototype, "_formDisabled", void 0);
@@ -1 +1 @@
1
- {"version":3,"file":"ui-checkbox.js","sourceRoot":"","sources":["../src/ui-checkbox.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, css, html, type PropertyValues } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport { ref } from \"lit/directives/ref.js\";\nimport { tokens } from \"./tokens.js\";\n\n/**\n * A form-associated boolean checkbox, usable standalone or inside a native\n * `<form>`. Submits `name=on` when checked (matching native\n * `<input type=\"checkbox\">` semantics) and participates fully in form\n * `reset()`, ancestor `<fieldset disabled>`, and `required` validity.\n *\n * Renders a native `<input type=\"checkbox\">` wrapped in a `<label>`, styled\n * via `:has()` on the wrapping label (matching `radio-pills`/`radio-cards`)\n * rather than styling the native input directly; the checkbox itself renders\n * at `1rem`, matching the existing radio-input convention.\n *\n * @element ui-checkbox\n * @fires change - The checkbox was toggled by the user, in either direction;\n * detail: `{ checked }`. Programmatic `checked` assignments do not fire it.\n */\n@customElement(\"ui-checkbox\")\nexport class UiCheckbox extends LitElement {\n static formAssociated = true;\n\n static override styles = [\n tokens,\n css`\n :host {\n display: inline-block;\n }\n .checkbox {\n display: inline-flex;\n align-items: center;\n gap: 0.5rem;\n min-height: 2rem;\n cursor: pointer;\n font-family: var(--ui-font, ui-sans-serif, system-ui, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\");\n font-size: var(--ui-font-size-sm, 0.75rem);\n line-height: var(--ui-line-height-tight, 1.25);\n color: var(--ui-text, #0f172a);\n }\n .checkbox input {\n width: 1rem;\n height: 1rem;\n margin: 0;\n accent-color: var(--ui-primary, #4f46e5);\n cursor: pointer;\n }\n .checkbox:has(input:focus-visible) {\n outline: none;\n }\n .checkbox:has(input:focus-visible) input {\n outline: none;\n box-shadow: var(--ui-focus-ring, 0 0 0 3px rgb(79 70 229 / 0.35));\n border-radius: var(--ui-radius-sm, 0.25rem);\n }\n .checkbox:has(input:disabled) {\n cursor: not-allowed;\n opacity: 0.6;\n }\n .checkbox:has(input:disabled) input {\n cursor: not-allowed;\n }\n .required-mark {\n color: var(--ui-danger, #dc2626);\n }\n .sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n overflow: hidden;\n clip: rect(0 0 0 0);\n clip-path: inset(50%);\n white-space: nowrap;\n border: 0;\n }\n @media (forced-colors: active) {\n .checkbox:has(input:focus-visible) input {\n outline: 2px solid CanvasText;\n outline-offset: 2px;\n box-shadow: none;\n }\n .checkbox:has(input:disabled) {\n color: GrayText;\n opacity: 1;\n }\n }\n `,\n ];\n\n /** Whether the box is checked. */\n @property({ type: Boolean }) checked = false;\n /** Visual \"partial selection\" state; cleared on the next user interaction. */\n @property({ type: Boolean }) indeterminate = false;\n /** Disables interaction; merged with an ancestor `<fieldset disabled>`. */\n @property({ type: Boolean }) disabled = false;\n /** Marks the control invalid via `ElementInternals` while unchecked. */\n @property({ type: Boolean }) required = false;\n /** Form field name; submitted as `name=on` only while checked. */\n @property() name = \"\";\n /** Visible label text rendered next to the box. */\n @property() label = \"\";\n\n @state() private _formDisabled = false;\n\n #internals = this.attachInternals();\n #defaultChecked = false;\n #inputEl: HTMLInputElement | null = null;\n\n /** Whether the host or an ancestor fieldset currently disables the control. */\n get #isDisabled(): boolean {\n return this.disabled || this._formDisabled;\n }\n\n protected override willUpdate(_changed: PropertyValues): void {\n if (!this.hasUpdated) this.#defaultChecked = this.checked;\n }\n\n protected override updated(changed: PropertyValues): void {\n if (changed.has(\"checked\") || changed.has(\"name\")) this.#syncFormValue();\n if (changed.has(\"checked\") || changed.has(\"required\") || changed.has(\"disabled\") || changed.has(\"_formDisabled\")) {\n this.#syncValidity();\n }\n if (changed.has(\"indeterminate\") && this.#inputEl) {\n this.#inputEl.indeterminate = this.indeterminate;\n }\n }\n\n /** Restores the checked state captured at first render, per the form contract. */\n formResetCallback(): void {\n this.checked = this.#defaultChecked;\n this.indeterminate = false;\n }\n\n /** Mirrors an ancestor fieldset's disabled state. */\n formDisabledCallback(disabled: boolean): void {\n this._formDisabled = disabled;\n }\n\n /** Restores the checked state from saved form state (bfcache/autofill). */\n formStateRestoreCallback(state: string | File | FormData | null): void {\n if (typeof state !== \"string\") return;\n this.checked = state === \"on\";\n }\n\n #syncFormValue(): void {\n if (!this.name || !this.checked) {\n this.#internals.setFormValue(null);\n return;\n }\n this.#internals.setFormValue(\"on\");\n }\n\n #syncValidity(): void {\n if (this.#isDisabled) {\n this.#internals.setValidity({});\n return;\n }\n if (this.required && !this.checked) {\n this.#internals.setValidity(\n { valueMissing: true },\n \"Please check this box to continue.\",\n this.#inputEl ?? undefined,\n );\n } else {\n this.#internals.setValidity({});\n }\n }\n\n #onChange(e: Event): void {\n const input = e.target as HTMLInputElement;\n this.checked = input.checked;\n this.indeterminate = false;\n this.dispatchEvent(\n new CustomEvent(\"change\", { detail: { checked: this.checked }, bubbles: true, composed: true }),\n );\n }\n\n #onInputRef = (el: Element | undefined): void => {\n this.#inputEl = (el as HTMLInputElement | undefined) ?? null;\n if (this.#inputEl) this.#inputEl.indeterminate = this.indeterminate;\n };\n\n override render() {\n return html`\n <label class=\"checkbox\">\n <input\n ${ref(this.#onInputRef)}\n type=\"checkbox\"\n .checked=${this.checked}\n ?disabled=${this.#isDisabled}\n ?required=${this.required}\n @change=${(e: Event) => this.#onChange(e)}\n />\n <span\n >${this.label}${this.required\n ? html`<span class=\"required-mark\" aria-hidden=\"true\"> *</span><span class=\"sr-only\">\n (required)</span\n >`\n : \"\"}</span\n >\n </label>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ui-checkbox\": UiCheckbox;\n }\n}\n"]}
1
+ {"version":3,"file":"ui-checkbox.js","sourceRoot":"","sources":["../src/ui-checkbox.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,EAA4C,MAAM,KAAK,CAAC;AAC/F,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AACnE,OAAO,EAAE,GAAG,EAAE,MAAM,uBAAuB,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC;;;;;;;;;;;;;;;;;;GAkBG;AAEI,IAAM,UAAU,GAAhB,MAAM,UAAW,SAAQ,UAAU;IAAnC;;QAiFL,kCAAkC;QACL,YAAO,GAAG,KAAK,CAAC;QAC7C,8EAA8E;QACjD,kBAAa,GAAG,KAAK,CAAC;QACnD,2EAA2E;QAC9C,aAAQ,GAAG,KAAK,CAAC;QAC9C,wEAAwE;QAC3C,aAAQ,GAAG,KAAK,CAAC;QAC9C,kEAAkE;QACtD,SAAI,GAAG,EAAE,CAAC;QACtB,mDAAmD;QACvC,UAAK,GAAG,EAAE,CAAC;QACvB,8HAA8H;QAC9F,SAAI,GAA0B,IAAI,CAAC;QACnE,qEAAqE;QACzC,aAAQ,GAAG,EAAE,CAAC;QAEzB,kBAAa,GAAG,KAAK,CAAC;QAEvC,eAAU,GAAG,IAAI,CAAC,eAAe,EAAE,CAAC;QACpC,oBAAe,GAAG,KAAK,CAAC;QACxB,aAAQ,GAA4B,IAAI,CAAC;QAuEzC,gBAAW,GAAG,CAAC,EAAuB,EAAQ,EAAE;YAC9C,IAAI,CAAC,QAAQ,GAAI,EAAmC,IAAI,IAAI,CAAC;YAC7D,IAAI,IAAI,CAAC,QAAQ;gBAAE,IAAI,CAAC,QAAQ,CAAC,aAAa,GAAG,IAAI,CAAC,aAAa,CAAC;QACtE,CAAC,CAAC;IA4BJ,CAAC;aA3MQ,mBAAc,GAAG,IAAI,AAAP,CAAQ;aAEb,WAAM,GAAG;QACvB,MAAM;QACN,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAyEF;KACF,AA5EqB,CA4EpB;IAqBF,UAAU,CAA0B;IACpC,eAAe,CAAS;IACxB,QAAQ,CAAiC;IAEzC,+EAA+E;IAC/E,IAAI,WAAW;QACb,OAAO,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,aAAa,CAAC;IAC7C,CAAC;IAEkB,UAAU,CAAC,QAAwB;QACpD,IAAI,CAAC,IAAI,CAAC,UAAU;YAAE,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC,OAAO,CAAC;IAC5D,CAAC;IAEkB,OAAO,CAAC,OAAuB;QAChD,IAAI,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC;YAAE,IAAI,CAAC,cAAc,EAAE,CAAC;QACzE,IAAI,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,EAAE,CAAC;YACjH,IAAI,CAAC,aAAa,EAAE,CAAC;QACvB,CAAC;QACD,IAAI,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClD,IAAI,CAAC,QAAQ,CAAC,aAAa,GAAG,IAAI,CAAC,aAAa,CAAC;QACnD,CAAC;IACH,CAAC;IAED,kFAAkF;IAClF,iBAAiB;QACf,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,eAAe,CAAC;QACpC,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC;IAC7B,CAAC;IAED,qDAAqD;IACrD,oBAAoB,CAAC,QAAiB;QACpC,IAAI,CAAC,aAAa,GAAG,QAAQ,CAAC;IAChC,CAAC;IAED,2EAA2E;IAC3E,wBAAwB,CAAC,KAAsC;QAC7D,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,OAAO;QACtC,IAAI,CAAC,OAAO,GAAG,KAAK,KAAK,IAAI,CAAC;IAChC,CAAC;IAED,cAAc;QACZ,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;YAChC,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;YACnC,OAAO;QACT,CAAC;QACD,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;IACrC,CAAC;IAED,aAAa;QACX,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACrB,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;YAChC,OAAO;QACT,CAAC;QACD,IAAI,IAAI,CAAC,QAAQ,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;YACnC,IAAI,CAAC,UAAU,CAAC,WAAW,CACzB,EAAE,YAAY,EAAE,IAAI,EAAE,EACtB,oCAAoC,EACpC,IAAI,CAAC,QAAQ,IAAI,SAAS,CAC3B,CAAC;QACJ,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;QAClC,CAAC;IACH,CAAC;IAED,SAAS,CAAC,CAAQ;QAChB,MAAM,KAAK,GAAG,CAAC,CAAC,MAA0B,CAAC;QAC3C,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;QAC7B,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC;QAC3B,IAAI,CAAC,aAAa,CAChB,IAAI,WAAW,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAChG,CAAC;IACJ,CAAC;IAED,WAAW,CAGT;IAEO,MAAM;QACb,OAAO,IAAI,CAAA;;;YAGH,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC;;qBAEZ,IAAI,CAAC,OAAO;sBACX,IAAI,CAAC,WAAW;sBAChB,IAAI,CAAC,QAAQ;oBACf,CAAC,CAAQ,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;;UAEzC,IAAI,CAAC,IAAI;YACT,CAAC,CAAC,IAAI,CAAA,qCAAqC,yBAAyB,IAAI,CAAC,QAAQ,IAAI;iBAC9E,IAAI,CAAC,IAAI;cACZ;YACJ,CAAC,CAAC,OAAO;;aAEN,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,QAAQ;YAC3B,CAAC,CAAC,IAAI,CAAA;;kBAEA;YACN,CAAC,CAAC,EAAE;;;KAGX,CAAC;IACJ,CAAC;CACF,CAAA;AA1H8B;IAA5B,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;2CAAiB;AAEhB;IAA5B,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;iDAAuB;AAEtB;IAA5B,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;4CAAkB;AAEjB;IAA5B,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;4CAAkB;AAElC;IAAX,QAAQ,EAAE;wCAAW;AAEV;IAAX,QAAQ,EAAE;yCAAY;AAES;IAA/B,QAAQ,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;wCAAoC;AAEvC;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;4CAAe;AAEzB;IAAhB,KAAK,EAAE;iDAA+B;AAlG5B,UAAU;IADtB,aAAa,CAAC,aAAa,CAAC;GAChB,UAAU,CA4MtB","sourcesContent":["import { LitElement, css, html, nothing, type PropertyValues, type TemplateResult } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport { ref } from \"lit/directives/ref.js\";\nimport { tokens } from \"./tokens.js\";\n\n/**\n * A form-associated boolean checkbox, usable standalone or inside a native\n * `<form>`. Submits `name=on` when checked (matching native\n * `<input type=\"checkbox\">` semantics) and participates fully in form\n * `reset()`, ancestor `<fieldset disabled>`, and `required` validity.\n *\n * Renders a native `<input type=\"checkbox\">` wrapped in a `<label>`, styled\n * via `:has()` on the wrapping label (matching `radio-pills`/`radio-cards`)\n * rather than styling the native input directly; the checkbox itself renders\n * at `1rem`, matching the existing radio-input convention. An optional\n * pre-rendered `icon` (matching `form-select`'s per-option icon convention)\n * renders between the box and the label, inside the same clickable `<label>`\n * — for a row that pairs a checkbox with an icon/swatch and needs the whole\n * row, icon included, to stay one click target.\n *\n * @element ui-checkbox\n * @fires change - The checkbox was toggled by the user, in either direction;\n * detail: `{ checked }`. Programmatic `checked` assignments do not fire it.\n */\n@customElement(\"ui-checkbox\")\nexport class UiCheckbox extends LitElement {\n static formAssociated = true;\n\n static override styles = [\n tokens,\n css`\n :host {\n display: inline-block;\n }\n .checkbox {\n display: inline-flex;\n align-items: center;\n gap: 0.5rem;\n min-height: 2rem;\n cursor: pointer;\n font-family: var(--ui-font, ui-sans-serif, system-ui, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\");\n font-size: var(--ui-font-size-sm, 0.75rem);\n line-height: var(--ui-line-height-tight, 1.25);\n color: var(--ui-text, #0f172a);\n }\n .checkbox input {\n width: 1rem;\n height: 1rem;\n margin: 0;\n accent-color: var(--ui-primary, #4f46e5);\n cursor: pointer;\n }\n .checkbox:has(input:focus-visible) {\n outline: none;\n }\n .checkbox:has(input:focus-visible) input {\n outline: none;\n box-shadow: var(--ui-focus-ring, 0 0 0 3px rgb(79 70 229 / 0.35));\n border-radius: var(--ui-radius-sm, 0.25rem);\n }\n .checkbox:has(input:disabled) {\n cursor: not-allowed;\n opacity: 0.6;\n }\n .checkbox:has(input:disabled) input {\n cursor: not-allowed;\n }\n .required-mark {\n color: var(--ui-danger, #dc2626);\n }\n .checkbox-icon {\n display: inline-flex;\n width: var(--checkbox-icon-size);\n height: var(--checkbox-icon-size);\n flex: 0 0 var(--checkbox-icon-size);\n }\n .checkbox-icon > svg {\n width: 100%;\n height: 100%;\n }\n .sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n overflow: hidden;\n clip: rect(0 0 0 0);\n clip-path: inset(50%);\n white-space: nowrap;\n border: 0;\n }\n @media (forced-colors: active) {\n .checkbox:has(input:focus-visible) input {\n outline: 2px solid CanvasText;\n outline-offset: 2px;\n box-shadow: none;\n }\n .checkbox:has(input:disabled) {\n color: GrayText;\n opacity: 1;\n }\n }\n `,\n ];\n\n /** Whether the box is checked. */\n @property({ type: Boolean }) checked = false;\n /** Visual \"partial selection\" state; cleared on the next user interaction. */\n @property({ type: Boolean }) indeterminate = false;\n /** Disables interaction; merged with an ancestor `<fieldset disabled>`. */\n @property({ type: Boolean }) disabled = false;\n /** Marks the control invalid via `ElementInternals` while unchecked. */\n @property({ type: Boolean }) required = false;\n /** Form field name; submitted as `name=on` only while checked. */\n @property() name = \"\";\n /** Visible label text rendered next to the box. */\n @property() label = \"\";\n /** Pre-rendered icon template displayed between the box and the label, e.g. `iconPencil(14)` from this package's icon set. */\n @property({ attribute: false }) icon: TemplateResult | null = null;\n /** Square icon size in pixels — 14 (inline icon size) by default. */\n @property({ type: Number }) iconSize = 14;\n\n @state() private _formDisabled = false;\n\n #internals = this.attachInternals();\n #defaultChecked = false;\n #inputEl: HTMLInputElement | null = null;\n\n /** Whether the host or an ancestor fieldset currently disables the control. */\n get #isDisabled(): boolean {\n return this.disabled || this._formDisabled;\n }\n\n protected override willUpdate(_changed: PropertyValues): void {\n if (!this.hasUpdated) this.#defaultChecked = this.checked;\n }\n\n protected override updated(changed: PropertyValues): void {\n if (changed.has(\"checked\") || changed.has(\"name\")) this.#syncFormValue();\n if (changed.has(\"checked\") || changed.has(\"required\") || changed.has(\"disabled\") || changed.has(\"_formDisabled\")) {\n this.#syncValidity();\n }\n if (changed.has(\"indeterminate\") && this.#inputEl) {\n this.#inputEl.indeterminate = this.indeterminate;\n }\n }\n\n /** Restores the checked state captured at first render, per the form contract. */\n formResetCallback(): void {\n this.checked = this.#defaultChecked;\n this.indeterminate = false;\n }\n\n /** Mirrors an ancestor fieldset's disabled state. */\n formDisabledCallback(disabled: boolean): void {\n this._formDisabled = disabled;\n }\n\n /** Restores the checked state from saved form state (bfcache/autofill). */\n formStateRestoreCallback(state: string | File | FormData | null): void {\n if (typeof state !== \"string\") return;\n this.checked = state === \"on\";\n }\n\n #syncFormValue(): void {\n if (!this.name || !this.checked) {\n this.#internals.setFormValue(null);\n return;\n }\n this.#internals.setFormValue(\"on\");\n }\n\n #syncValidity(): void {\n if (this.#isDisabled) {\n this.#internals.setValidity({});\n return;\n }\n if (this.required && !this.checked) {\n this.#internals.setValidity(\n { valueMissing: true },\n \"Please check this box to continue.\",\n this.#inputEl ?? undefined,\n );\n } else {\n this.#internals.setValidity({});\n }\n }\n\n #onChange(e: Event): void {\n const input = e.target as HTMLInputElement;\n this.checked = input.checked;\n this.indeterminate = false;\n this.dispatchEvent(\n new CustomEvent(\"change\", { detail: { checked: this.checked }, bubbles: true, composed: true }),\n );\n }\n\n #onInputRef = (el: Element | undefined): void => {\n this.#inputEl = (el as HTMLInputElement | undefined) ?? null;\n if (this.#inputEl) this.#inputEl.indeterminate = this.indeterminate;\n };\n\n override render() {\n return html`\n <label class=\"checkbox\">\n <input\n ${ref(this.#onInputRef)}\n type=\"checkbox\"\n .checked=${this.checked}\n ?disabled=${this.#isDisabled}\n ?required=${this.required}\n @change=${(e: Event) => this.#onChange(e)}\n />\n ${this.icon\n ? html`<span class=\"checkbox-icon\" style=${`--checkbox-icon-size: ${this.iconSize}px`} aria-hidden=\"true\"\n >${this.icon}</span\n >`\n : nothing}\n <span\n >${this.label}${this.required\n ? html`<span class=\"required-mark\" aria-hidden=\"true\"> *</span><span class=\"sr-only\">\n (required)</span\n >`\n : \"\"}</span\n >\n </label>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ui-checkbox\": UiCheckbox;\n }\n}\n"]}