@f-ewald/components 1.10.0 → 1.12.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.
@@ -0,0 +1,200 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
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
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { LitElement, css, html, nothing } from "lit";
8
+ import { customElement, property, state } from "lit/decorators.js";
9
+ import { repeat } from "lit/directives/repeat.js";
10
+ import { iconChevronDown, iconChevronRight } from "./icons.js";
11
+ import { tokens } from "./tokens.js";
12
+ /**
13
+ * A generic, presentational tree shell: renders `nodes` recursively, one row
14
+ * per node, with each row's content produced by `renderNode` (default: plain
15
+ * label). Modeled on `data-table`'s headless pattern — knows nothing about
16
+ * what a node's `data` means beyond what `renderNode` does with it.
17
+ *
18
+ * A node with a `children` array (even empty) is a folder: clicking or
19
+ * activating its row toggles expand/collapse instead of firing `node-click`.
20
+ * A node with no `children` is a leaf: clicking or activating its row fires
21
+ * `node-click`. Folders start collapsed; set `default-expanded` to start
22
+ * every folder expanded instead. Expansion state is otherwise managed
23
+ * internally and untouched by later `nodes` updates, so a user's manual
24
+ * toggles survive a data refresh.
25
+ *
26
+ * @element tree-view
27
+ * @fires node-click - A leaf row was activated; detail is `{ id, data }`.
28
+ */
29
+ let TreeView = class TreeView extends LitElement {
30
+ constructor() {
31
+ super(...arguments);
32
+ /** Tree data; opaque to this component beyond what `renderNode` does with it. */
33
+ this.nodes = [];
34
+ /** Produces a row's rendered content for `node`. Default: plain label text. */
35
+ this.renderNode = (node) => node.label;
36
+ /** Start every folder expanded instead of the default all-collapsed. */
37
+ this.defaultExpanded = false;
38
+ this.expanded = new Set();
39
+ this.#initializedExpansion = false;
40
+ }
41
+ static { this.styles = [
42
+ tokens,
43
+ css `
44
+ :host {
45
+ display: block;
46
+ font-family: var(
47
+ --ui-font,
48
+ ui-sans-serif,
49
+ system-ui,
50
+ sans-serif,
51
+ "Apple Color Emoji",
52
+ "Segoe UI Emoji",
53
+ "Segoe UI Symbol",
54
+ "Noto Color Emoji"
55
+ );
56
+ font-size: var(--ui-font-size-sm, 0.75rem);
57
+ }
58
+ .row {
59
+ display: flex;
60
+ align-items: center;
61
+ gap: 0.25rem;
62
+ padding: 0.25rem 0.5rem;
63
+ border-radius: var(--ui-radius-sm, 0.25rem);
64
+ color: var(--ui-text, #0f172a);
65
+ cursor: pointer;
66
+ }
67
+ .row:hover {
68
+ background: var(--ui-surface-muted, #f8fafc);
69
+ }
70
+ .row:focus-visible {
71
+ outline: none;
72
+ box-shadow: var(--ui-focus-ring, 0 0 0 3px rgb(79 70 229 / 0.35));
73
+ }
74
+ .toggle {
75
+ display: inline-flex;
76
+ flex-shrink: 0;
77
+ width: 1rem;
78
+ height: 1rem;
79
+ align-items: center;
80
+ justify-content: center;
81
+ color: var(--ui-text-muted, #64748b);
82
+ }
83
+ .content {
84
+ min-width: 0;
85
+ flex: 1;
86
+ }
87
+ @media (forced-colors: active) {
88
+ .row:focus-visible {
89
+ outline: 2px solid CanvasText;
90
+ outline-offset: -2px;
91
+ box-shadow: none;
92
+ }
93
+ }
94
+ `,
95
+ ]; }
96
+ #initializedExpansion;
97
+ willUpdate(changed) {
98
+ if (!changed.has("nodes") || this.#initializedExpansion)
99
+ return;
100
+ this.#initializedExpansion = true;
101
+ if (!this.defaultExpanded)
102
+ return;
103
+ const all = new Set();
104
+ const collect = (list) => {
105
+ for (const node of list) {
106
+ if (!node.children)
107
+ continue;
108
+ all.add(node.id);
109
+ collect(node.children);
110
+ }
111
+ };
112
+ collect(this.nodes);
113
+ this.expanded = all;
114
+ }
115
+ #toggle(id) {
116
+ const next = new Set(this.expanded);
117
+ if (next.has(id))
118
+ next.delete(id);
119
+ else
120
+ next.add(id);
121
+ this.expanded = next;
122
+ }
123
+ #activate(node) {
124
+ if (node.children) {
125
+ this.#toggle(node.id);
126
+ return;
127
+ }
128
+ this.dispatchEvent(new CustomEvent("node-click", {
129
+ detail: { id: node.id, data: node.data },
130
+ bubbles: true,
131
+ composed: true,
132
+ }));
133
+ }
134
+ #onKeydown(node, e) {
135
+ if (e.key !== "Enter" && e.key !== " ")
136
+ return;
137
+ if (e.target !== e.currentTarget)
138
+ return;
139
+ e.preventDefault();
140
+ this.#activate(node);
141
+ }
142
+ #onRowClick(node, e) {
143
+ if (this.#isNestedInteractive(e.composedPath(), e.currentTarget))
144
+ return;
145
+ this.#activate(node);
146
+ }
147
+ #isNestedInteractive(path, row) {
148
+ const selector = "a, button, input, select, textarea, summary, [contenteditable], [role='button'], [role='link'], [tabindex]";
149
+ for (const target of path) {
150
+ if (target === row)
151
+ return false;
152
+ if (target instanceof HTMLElement && target.matches(selector))
153
+ return true;
154
+ }
155
+ return false;
156
+ }
157
+ #renderNode(node, depth) {
158
+ const isFolder = !!node.children;
159
+ const isOpen = isFolder && this.expanded.has(node.id);
160
+ return html `
161
+ <div
162
+ class="row"
163
+ role=${isFolder ? "button" : "link"}
164
+ aria-expanded=${isFolder ? String(isOpen) : nothing}
165
+ tabindex="0"
166
+ style="padding-left: ${depth * 1.25}rem"
167
+ @click=${(e) => this.#onRowClick(node, e)}
168
+ @keydown=${(e) => this.#onKeydown(node, e)}
169
+ >
170
+ <span class="toggle" aria-hidden="true">
171
+ ${isFolder ? (isOpen ? iconChevronDown(14) : iconChevronRight(14)) : nothing}
172
+ </span>
173
+ <span class="content">${this.renderNode(node)}</span>
174
+ </div>
175
+ ${isFolder && isOpen
176
+ ? repeat(node.children, (child) => child.id, (child) => this.#renderNode(child, depth + 1))
177
+ : nothing}
178
+ `;
179
+ }
180
+ render() {
181
+ return repeat(this.nodes, (node) => node.id, (node) => this.#renderNode(node, 0));
182
+ }
183
+ };
184
+ __decorate([
185
+ property({ attribute: false })
186
+ ], TreeView.prototype, "nodes", void 0);
187
+ __decorate([
188
+ property({ attribute: false })
189
+ ], TreeView.prototype, "renderNode", void 0);
190
+ __decorate([
191
+ property({ type: Boolean, attribute: "default-expanded" })
192
+ ], TreeView.prototype, "defaultExpanded", void 0);
193
+ __decorate([
194
+ state()
195
+ ], TreeView.prototype, "expanded", void 0);
196
+ TreeView = __decorate([
197
+ customElement("tree-view")
198
+ ], TreeView);
199
+ export { TreeView };
200
+ //# sourceMappingURL=tree-view.js.map
@@ -0,0 +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"]}
@@ -24,6 +24,8 @@ export declare class UiButton extends LitElement {
24
24
  static styles: import("lit").CSSResult[];
25
25
  /** Visual weight. */
26
26
  variant: ButtonVariant;
27
+ /** Size — `sm` reduces height/padding/font-size one step below the default. */
28
+ size: "sm" | "md";
27
29
  /** Renders an `<a href="...">` instead of a `<button>` when set. */
28
30
  href: string | null;
29
31
  /** Native button `type`. Ignored when `href` is set. */
@@ -1 +1 @@
1
- {"version":3,"file":"ui-button.d.ts","sourceRoot":"","sources":["../src/ui-button.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,MAAM,KAAK,CAAC;AAK5C,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAC;AAE/D;;;;;;;;;;;;;;;;;GAiBG;AACH,qBACa,QAAS,SAAQ,UAAU;;IACtC,MAAM,CAAC,cAAc,UAAQ;IAG7B,OAAgB,MAAM,4BA8FpB;IAEF,qBAAqB;IACT,OAAO,EAAE,aAAa,CAAa;IAC/C,oEAAoE;IACxD,IAAI,EAAE,MAAM,GAAG,IAAI,CAAQ;IACvC,wDAAwD;IAC5C,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAY;IAC3D,wCAAwC;IACX,QAAQ,UAAS;IAC9C,0EAA0E;IAC7C,IAAI,UAAS;IAE1C,qHAAqH;IACrH,OAAO,CAAC,QAAQ;IAKhB,4EAA4E;IAC5E,OAAO,CAAC,YAAY;IAKX,MAAM,yCA+Bd;CACF;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,WAAW,EAAE,QAAQ,CAAC;KACvB;CACF"}
1
+ {"version":3,"file":"ui-button.d.ts","sourceRoot":"","sources":["../src/ui-button.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,MAAM,KAAK,CAAC;AAK5C,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAC;AAE/D;;;;;;;;;;;;;;;;;GAiBG;AACH,qBACa,QAAS,SAAQ,UAAU;;IACtC,MAAM,CAAC,cAAc,UAAQ;IAG7B,OAAgB,MAAM,4BAmGpB;IAEF,qBAAqB;IACT,OAAO,EAAE,aAAa,CAAa;IAC/C,+EAA+E;IACnE,IAAI,EAAE,IAAI,GAAG,IAAI,CAAQ;IACrC,oEAAoE;IACxD,IAAI,EAAE,MAAM,GAAG,IAAI,CAAQ;IACvC,wDAAwD;IAC5C,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAY;IAC3D,wCAAwC;IACX,QAAQ,UAAS;IAC9C,0EAA0E;IAC7C,IAAI,UAAS;IAE1C,qHAAqH;IACrH,OAAO,CAAC,QAAQ;IAKhB,4EAA4E;IAC5E,OAAO,CAAC,YAAY;IAKX,MAAM,yCA+Bd;CACF;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,WAAW,EAAE,QAAQ,CAAC;KACvB;CACF"}
package/dist/ui-button.js CHANGED
@@ -32,6 +32,8 @@ let UiButton = class UiButton extends LitElement {
32
32
  this.#internals = this.attachInternals();
33
33
  /** Visual weight. */
34
34
  this.variant = "primary";
35
+ /** Size — `sm` reduces height/padding/font-size one step below the default. */
36
+ this.size = "md";
35
37
  /** Renders an `<a href="...">` instead of a `<button>` when set. */
36
38
  this.href = null;
37
39
  /** Native button `type`. Ignored when `href` is set. */
@@ -106,6 +108,11 @@ let UiButton = class UiButton extends LitElement {
106
108
  outline: none;
107
109
  box-shadow: var(--ui-focus-ring, 0 0 0 3px rgb(79 70 229 / 0.35));
108
110
  }
111
+ .btn.sm {
112
+ height: 1.5rem;
113
+ padding: 0.25rem 0.5rem;
114
+ font-size: var(--ui-font-size-xs, 0.6875rem);
115
+ }
109
116
  .spin {
110
117
  display: inline-flex;
111
118
  animation: spin 0.8s linear infinite;
@@ -152,7 +159,7 @@ let UiButton = class UiButton extends LitElement {
152
159
  e.preventDefault();
153
160
  }
154
161
  render() {
155
- const classes = `btn ${this.variant}`;
162
+ const classes = `btn ${this.variant} ${this.size}`;
156
163
  const isDisabled = this.disabled || this.busy;
157
164
  if (this.href) {
158
165
  return html `
@@ -187,6 +194,9 @@ let UiButton = class UiButton extends LitElement {
187
194
  __decorate([
188
195
  property()
189
196
  ], UiButton.prototype, "variant", void 0);
197
+ __decorate([
198
+ property()
199
+ ], UiButton.prototype, "size", void 0);
190
200
  __decorate([
191
201
  property()
192
202
  ], UiButton.prototype, "href", void 0);
@@ -1 +1 @@
1
- {"version":3,"file":"ui-button.js","sourceRoot":"","sources":["../src/ui-button.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, css, html } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport { iconArrowPath } from \"./icons.js\";\nimport { tokens } from \"./tokens.js\";\n\nexport type ButtonVariant = \"primary\" | \"secondary\" | \"danger\";\n\n/**\n * Button (or link styled as one) with an optional leading icon, in three\n * visual weights. Set `href` to render an `<a>` instead of a `<button>` —\n * same styling either way — for cross-page navigation that should look like\n * an action button; a disabled/busy link stays a real `<a>` with\n * `aria-disabled` + `pointer-events: none` rather than losing its href.\n * Put the icon in the `icon` slot and the label in the default slot.\n *\n * Form-associated (`type=\"submit\"`/`\"reset\"`): the actual `<button>` lives in\n * this element's shadow root, which native HTML form association does not\n * cross into from an ancestor light-DOM `<form>`. `type=\"submit\"`/`\"reset\"`\n * is instead wired through `ElementInternals.form` — the same mechanism\n * `address-autocomplete` uses to associate with an ancestor form.\n *\n * @element ui-button\n * @slot icon - Optional leading icon (e.g. an inline SVG).\n * @slot - Button label.\n */\n@customElement(\"ui-button\")\nexport class UiButton extends LitElement {\n static formAssociated = true;\n\n #internals = this.attachInternals();\n static override styles = [\n tokens,\n css`\n :host {\n display: inline-flex;\n }\n .btn {\n display: inline-flex;\n align-items: center;\n gap: 0.25rem;\n height: 2rem;\n border-radius: var(--ui-radius-sm, 0.25rem);\n padding: 0.5rem 1rem;\n font-size: var(--ui-font-size-sm, 0.75rem);\n font-weight: var(--ui-font-weight-medium, 500);\n line-height: var(--ui-line-height-tight, 1.25);\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 cursor: pointer;\n border: 1px solid transparent;\n text-decoration: none;\n box-sizing: border-box;\n }\n .btn.primary {\n background: var(--ui-primary, #4f46e5);\n color: var(--ui-on-accent, #ffffff);\n }\n .btn.primary:hover:not(:disabled) {\n background: var(--ui-primary-hover, #4338ca);\n }\n .btn.secondary {\n background: none;\n border-color: var(--ui-border, #e2e8f0);\n color: var(--ui-text, #0f172a);\n }\n .btn.secondary:hover:not(:disabled) {\n border-color: var(--ui-text-muted, #64748b);\n }\n .btn.danger {\n background: var(--ui-danger, #dc2626);\n color: var(--ui-on-accent, #ffffff);\n }\n .btn.danger:hover:not(:disabled) {\n background: var(--ui-danger-hover, #b91c1c);\n }\n .btn:disabled,\n .btn[aria-disabled=\"true\"] {\n opacity: 0.6;\n cursor: default;\n pointer-events: none;\n }\n .btn:focus-visible {\n outline: none;\n box-shadow: var(--ui-focus-ring, 0 0 0 3px rgb(79 70 229 / 0.35));\n }\n .spin {\n display: inline-flex;\n animation: spin 0.8s linear infinite;\n }\n .spin[hidden] {\n display: none;\n }\n @keyframes spin {\n to {\n transform: rotate(360deg);\n }\n }\n @media (prefers-reduced-motion: reduce) {\n .spin {\n animation: none;\n }\n }\n @media (forced-colors: active) {\n .btn:focus-visible {\n outline: 2px solid CanvasText;\n outline-offset: 2px;\n box-shadow: none;\n }\n .btn:disabled,\n .btn[aria-disabled=\"true\"] {\n color: GrayText;\n border-color: GrayText;\n opacity: 1;\n }\n }\n `,\n ];\n\n /** Visual weight. */\n @property() variant: ButtonVariant = \"primary\";\n /** Renders an `<a href=\"...\">` instead of a `<button>` when set. */\n @property() href: string | null = null;\n /** Native button `type`. Ignored when `href` is set. */\n @property() type: \"button\" | \"submit\" | \"reset\" = \"button\";\n /** Disables the control and dims it. */\n @property({ type: Boolean }) disabled = false;\n /** Shows a spinner in place of the icon slot and disables the control. */\n @property({ type: Boolean }) busy = false;\n\n /** Drives submit/reset on the ancestor form via ElementInternals, since a shadow-DOM button can't do it natively. */\n private _onClick() {\n if (this.type === \"submit\") this.#internals.form?.requestSubmit();\n else if (this.type === \"reset\") this.#internals.form?.reset();\n }\n\n /** Suppresses navigation while a link-styled button is disabled or busy. */\n private _onLinkClick(e: MouseEvent) {\n if (!this.disabled && !this.busy) return;\n e.preventDefault();\n }\n\n override render() {\n const classes = `btn ${this.variant}`;\n const isDisabled = this.disabled || this.busy;\n if (this.href) {\n return html`\n <a\n class=${classes}\n href=${this.href}\n aria-disabled=${isDisabled ? \"true\" : \"false\"}\n aria-busy=${this.busy ? \"true\" : \"false\"}\n @click=${this._onLinkClick}\n >\n <span class=\"spin\" aria-hidden=\"true\" ?hidden=${!this.busy}>${iconArrowPath(14)}</span>\n <slot name=\"icon\" ?hidden=${this.busy}></slot>\n <slot></slot>\n </a>\n `;\n }\n return html`\n <button\n class=${classes}\n type=\"button\"\n ?disabled=${isDisabled}\n aria-busy=${this.busy ? \"true\" : \"false\"}\n @click=${this._onClick}\n >\n <span class=\"spin\" aria-hidden=\"true\" ?hidden=${!this.busy}>${iconArrowPath(14)}</span>\n <slot name=\"icon\" ?hidden=${this.busy}></slot>\n <slot></slot>\n </button>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ui-button\": UiButton;\n }\n}\n"]}
1
+ {"version":3,"file":"ui-button.js","sourceRoot":"","sources":["../src/ui-button.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, css, html } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport { iconArrowPath } from \"./icons.js\";\nimport { tokens } from \"./tokens.js\";\n\nexport type ButtonVariant = \"primary\" | \"secondary\" | \"danger\";\n\n/**\n * Button (or link styled as one) with an optional leading icon, in three\n * visual weights. Set `href` to render an `<a>` instead of a `<button>` —\n * same styling either way — for cross-page navigation that should look like\n * an action button; a disabled/busy link stays a real `<a>` with\n * `aria-disabled` + `pointer-events: none` rather than losing its href.\n * Put the icon in the `icon` slot and the label in the default slot.\n *\n * Form-associated (`type=\"submit\"`/`\"reset\"`): the actual `<button>` lives in\n * this element's shadow root, which native HTML form association does not\n * cross into from an ancestor light-DOM `<form>`. `type=\"submit\"`/`\"reset\"`\n * is instead wired through `ElementInternals.form` — the same mechanism\n * `address-autocomplete` uses to associate with an ancestor form.\n *\n * @element ui-button\n * @slot icon - Optional leading icon (e.g. an inline SVG).\n * @slot - Button label.\n */\n@customElement(\"ui-button\")\nexport class UiButton extends LitElement {\n static formAssociated = true;\n\n #internals = this.attachInternals();\n static override styles = [\n tokens,\n css`\n :host {\n display: inline-flex;\n }\n .btn {\n display: inline-flex;\n align-items: center;\n gap: 0.25rem;\n height: 2rem;\n border-radius: var(--ui-radius-sm, 0.25rem);\n padding: 0.5rem 1rem;\n font-size: var(--ui-font-size-sm, 0.75rem);\n font-weight: var(--ui-font-weight-medium, 500);\n line-height: var(--ui-line-height-tight, 1.25);\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 cursor: pointer;\n border: 1px solid transparent;\n text-decoration: none;\n box-sizing: border-box;\n }\n .btn.primary {\n background: var(--ui-primary, #4f46e5);\n color: var(--ui-on-accent, #ffffff);\n }\n .btn.primary:hover:not(:disabled) {\n background: var(--ui-primary-hover, #4338ca);\n }\n .btn.secondary {\n background: none;\n border-color: var(--ui-border, #e2e8f0);\n color: var(--ui-text, #0f172a);\n }\n .btn.secondary:hover:not(:disabled) {\n border-color: var(--ui-text-muted, #64748b);\n }\n .btn.danger {\n background: var(--ui-danger, #dc2626);\n color: var(--ui-on-accent, #ffffff);\n }\n .btn.danger:hover:not(:disabled) {\n background: var(--ui-danger-hover, #b91c1c);\n }\n .btn:disabled,\n .btn[aria-disabled=\"true\"] {\n opacity: 0.6;\n cursor: default;\n pointer-events: none;\n }\n .btn:focus-visible {\n outline: none;\n box-shadow: var(--ui-focus-ring, 0 0 0 3px rgb(79 70 229 / 0.35));\n }\n .btn.sm {\n height: 1.5rem;\n padding: 0.25rem 0.5rem;\n font-size: var(--ui-font-size-xs, 0.6875rem);\n }\n .spin {\n display: inline-flex;\n animation: spin 0.8s linear infinite;\n }\n .spin[hidden] {\n display: none;\n }\n @keyframes spin {\n to {\n transform: rotate(360deg);\n }\n }\n @media (prefers-reduced-motion: reduce) {\n .spin {\n animation: none;\n }\n }\n @media (forced-colors: active) {\n .btn:focus-visible {\n outline: 2px solid CanvasText;\n outline-offset: 2px;\n box-shadow: none;\n }\n .btn:disabled,\n .btn[aria-disabled=\"true\"] {\n color: GrayText;\n border-color: GrayText;\n opacity: 1;\n }\n }\n `,\n ];\n\n /** Visual weight. */\n @property() variant: ButtonVariant = \"primary\";\n /** Size — `sm` reduces height/padding/font-size one step below the default. */\n @property() size: \"sm\" | \"md\" = \"md\";\n /** Renders an `<a href=\"...\">` instead of a `<button>` when set. */\n @property() href: string | null = null;\n /** Native button `type`. Ignored when `href` is set. */\n @property() type: \"button\" | \"submit\" | \"reset\" = \"button\";\n /** Disables the control and dims it. */\n @property({ type: Boolean }) disabled = false;\n /** Shows a spinner in place of the icon slot and disables the control. */\n @property({ type: Boolean }) busy = false;\n\n /** Drives submit/reset on the ancestor form via ElementInternals, since a shadow-DOM button can't do it natively. */\n private _onClick() {\n if (this.type === \"submit\") this.#internals.form?.requestSubmit();\n else if (this.type === \"reset\") this.#internals.form?.reset();\n }\n\n /** Suppresses navigation while a link-styled button is disabled or busy. */\n private _onLinkClick(e: MouseEvent) {\n if (!this.disabled && !this.busy) return;\n e.preventDefault();\n }\n\n override render() {\n const classes = `btn ${this.variant} ${this.size}`;\n const isDisabled = this.disabled || this.busy;\n if (this.href) {\n return html`\n <a\n class=${classes}\n href=${this.href}\n aria-disabled=${isDisabled ? \"true\" : \"false\"}\n aria-busy=${this.busy ? \"true\" : \"false\"}\n @click=${this._onLinkClick}\n >\n <span class=\"spin\" aria-hidden=\"true\" ?hidden=${!this.busy}>${iconArrowPath(14)}</span>\n <slot name=\"icon\" ?hidden=${this.busy}></slot>\n <slot></slot>\n </a>\n `;\n }\n return html`\n <button\n class=${classes}\n type=\"button\"\n ?disabled=${isDisabled}\n aria-busy=${this.busy ? \"true\" : \"false\"}\n @click=${this._onClick}\n >\n <span class=\"spin\" aria-hidden=\"true\" ?hidden=${!this.busy}>${iconArrowPath(14)}</span>\n <slot name=\"icon\" ?hidden=${this.busy}></slot>\n <slot></slot>\n </button>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ui-button\": UiButton;\n }\n}\n"]}
@@ -36,6 +36,7 @@ import "@f-ewald/components/button-group.js";
36
36
  | `options` | _(JS property only)_ | `ButtonGroupOption[]` | `[]` | Options to render, one segment each. |
37
37
  | `value` | `value` | `string` | `""` | Currently selected value. |
38
38
  | `disabled` | `disabled` | `boolean` | `false` | Disables every native radio in the group. |
39
+ | `iconOnly` | `icon-only` | `boolean` | `false` | Hides labels visually (icons only) while keeping them as the accessible name. |
39
40
 
40
41
  ## Events
41
42
 
@@ -0,0 +1,73 @@
1
+ # `<markdown-view>`
2
+
3
+ Renders a markdown string as sanitized, styled HTML — headings, lists,
4
+ code, tables, blockquotes, and links all get token-driven styling, with
5
+ wide content (code blocks, tables) scrolling in its own container instead
6
+ of widening the page.
7
+
8
+ The markdown source is treated as untrusted: it is always parsed with
9
+ `marked` and sanitized with `DOMPurify` before being injected, never
10
+ rendered as-is.
11
+
12
+ ## Install
13
+
14
+ ```js
15
+ import "@f-ewald/components/markdown-view.js";
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ```html
21
+ <markdown-view></markdown-view>
22
+ <script type="module">
23
+ const el = document.querySelector("markdown-view");
24
+ el.markdown = `## Release notes
25
+
26
+ - Added **markdown-view**
27
+ - Fixed a table alignment bug
28
+
29
+ \`\`\`ts
30
+ const x = 1;
31
+ \`\`\`
32
+
33
+ | Component | Status |
34
+ | --- | --- |
35
+ | markdown-view | New |
36
+
37
+ See the [changelog](#markdown-view) for details.`;
38
+ </script>
39
+ ```
40
+
41
+ ## Attributes / properties
42
+
43
+ | Property | Attribute | Type | Default | Description |
44
+ | --- | --- | --- | --- | --- |
45
+ | `markdown` | `markdown` | `string` | `""` | Raw markdown source to render. |
46
+
47
+ ## Events
48
+
49
+ _None._
50
+
51
+ ## Slots
52
+
53
+ _None._
54
+
55
+ ## CSS custom properties
56
+
57
+ | Custom property |
58
+ | --- |
59
+ | `--ui-border` |
60
+ | `--ui-font` |
61
+ | `--ui-font-mono` |
62
+ | `--ui-font-size` |
63
+ | `--ui-font-size-lg` |
64
+ | `--ui-font-size-sm` |
65
+ | `--ui-font-weight-semibold` |
66
+ | `--ui-line-height-normal` |
67
+ | `--ui-line-height-tight` |
68
+ | `--ui-primary` |
69
+ | `--ui-primary-hover` |
70
+ | `--ui-radius-sm` |
71
+ | `--ui-surface-muted` |
72
+ | `--ui-text` |
73
+ | `--ui-text-muted` |
@@ -0,0 +1,69 @@
1
+ # `<tree-view>`
2
+
3
+ A generic, presentational tree shell: renders `nodes` recursively, one row
4
+ per node, with each row's content produced by `renderNode` (default: plain
5
+ label). Modeled on `data-table`'s headless pattern — knows nothing about
6
+ what a node's `data` means beyond what `renderNode` does with it.
7
+
8
+ A node with a `children` array (even empty) is a folder: clicking or
9
+ activating its row toggles expand/collapse instead of firing `node-click`.
10
+ A node with no `children` is a leaf: clicking or activating its row fires
11
+ `node-click`. Folders start collapsed; set `default-expanded` to start
12
+ every folder expanded instead. Expansion state is otherwise managed
13
+ internally and untouched by later `nodes` updates, so a user's manual
14
+ toggles survive a data refresh.
15
+
16
+ ## Install
17
+
18
+ ```js
19
+ import "@f-ewald/components/tree-view.js";
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ```html
25
+ <tree-view></tree-view>
26
+ <script type="module">
27
+ const tree = document.querySelector("tree-view");
28
+ tree.nodes = [
29
+ {
30
+ id: "docs",
31
+ label: "docs",
32
+ children: [{ id: "fil_1", label: "notes.txt", data: { id: "fil_1" } }],
33
+ },
34
+ { id: "fil_2", label: "readme.md", data: { id: "fil_2" } },
35
+ ];
36
+ tree.renderNode = (node) => node.label;
37
+ tree.addEventListener("node-click", (e) => console.log(e.detail));
38
+ </script>
39
+ ```
40
+
41
+ ## Attributes / properties
42
+
43
+ | Property | Attribute | Type | Default | Description |
44
+ | --- | --- | --- | --- | --- |
45
+ | `nodes` | _(JS property only)_ | `TreeNode[]` | `[]` | Tree data; opaque to this component beyond what `renderNode` does with it. |
46
+ | `renderNode` | _(JS property only)_ | `(node: TreeNode) => unknown` | `—` | Produces a row's rendered content for `node`. Default: plain label text. |
47
+ | `defaultExpanded` | `default-expanded` | `boolean` | `false` | Start every folder expanded instead of the default all-collapsed. |
48
+
49
+ ## Events
50
+
51
+ | Event | Description |
52
+ | --- | --- |
53
+ | `node-click` | A leaf row was activated; detail is `{ id, data }`. |
54
+
55
+ ## Slots
56
+
57
+ _None._
58
+
59
+ ## CSS custom properties
60
+
61
+ | Custom property |
62
+ | --- |
63
+ | `--ui-focus-ring` |
64
+ | `--ui-font` |
65
+ | `--ui-font-size-sm` |
66
+ | `--ui-radius-sm` |
67
+ | `--ui-surface-muted` |
68
+ | `--ui-text` |
69
+ | `--ui-text-muted` |
package/docs/ui-button.md CHANGED
@@ -35,6 +35,7 @@ import "@f-ewald/components/ui-button.js";
35
35
  | Property | Attribute | Type | Default | Description |
36
36
  | --- | --- | --- | --- | --- |
37
37
  | `variant` | `variant` | `ButtonVariant` | `"primary"` | Visual weight. |
38
+ | `size` | `size` | `"sm" | "md"` | `"md"` | Size — `sm` reduces height/padding/font-size one step below the default. |
38
39
  | `href` | `href` | `string | null` | `null` | Renders an `<a href="...">` instead of a `<button>` when set. |
39
40
  | `type` | `type` | `"button" | "submit" | "reset"` | `"button"` | Native button `type`. Ignored when `href` is set. |
40
41
  | `disabled` | `disabled` | `boolean` | `false` | Disables the control and dims it. |
@@ -61,6 +62,7 @@ _None._
61
62
  | `--ui-focus-ring` |
62
63
  | `--ui-font` |
63
64
  | `--ui-font-size-sm` |
65
+ | `--ui-font-size-xs` |
64
66
  | `--ui-font-weight-medium` |
65
67
  | `--ui-line-height-tight` |
66
68
  | `--ui-on-accent` |
package/llms.txt CHANGED
@@ -224,7 +224,7 @@ relying on form submission.
224
224
 
225
225
  Import: `import "@f-ewald/components/button-group.js";`
226
226
 
227
- Properties: `options` (JS property only) : ButtonGroupOption[], default []; `value` (attribute `value`) : string, default ""; `disabled` (attribute `disabled`) : boolean, default false
227
+ Properties: `options` (JS property only) : ButtonGroupOption[], default []; `value` (attribute `value`) : string, default ""; `disabled` (attribute `disabled`) : boolean, default false; `iconOnly` (attribute `icon-only`) : boolean, default false
228
228
  Events: `change`
229
229
  CSS custom properties: `--ui-border`, `--ui-focus-ring`, `--ui-font`, `--ui-font-size-sm`, `--ui-font-weight-medium`, `--ui-line-height-tight`, `--ui-on-accent`, `--ui-primary`, `--ui-radius-sm`, `--ui-surface-muted`, `--ui-text`
230
230
 
@@ -952,6 +952,45 @@ Example:
952
952
  <map-pin color="#22c55e" size="26" highlighted>🏠</map-pin>
953
953
  ```
954
954
 
955
+ ## <markdown-view>
956
+
957
+ Renders a markdown string as sanitized, styled HTML — headings, lists,
958
+ code, tables, blockquotes, and links all get token-driven styling, with
959
+ wide content (code blocks, tables) scrolling in its own container instead
960
+ of widening the page.
961
+
962
+ The markdown source is treated as untrusted: it is always parsed with
963
+ `marked` and sanitized with `DOMPurify` before being injected, never
964
+ rendered as-is.
965
+
966
+ Import: `import "@f-ewald/components/markdown-view.js";`
967
+
968
+ Properties: `markdown` (attribute `markdown`) : string, default ""
969
+ Events: none
970
+ CSS custom properties: `--ui-border`, `--ui-font`, `--ui-font-mono`, `--ui-font-size`, `--ui-font-size-lg`, `--ui-font-size-sm`, `--ui-font-weight-semibold`, `--ui-line-height-normal`, `--ui-line-height-tight`, `--ui-primary`, `--ui-primary-hover`, `--ui-radius-sm`, `--ui-surface-muted`, `--ui-text`, `--ui-text-muted`
971
+
972
+ Example:
973
+ ```html
974
+ <markdown-view></markdown-view>
975
+ <script type="module">
976
+ const el = document.querySelector("markdown-view");
977
+ el.markdown = `## Release notes
978
+
979
+ - Added **markdown-view**
980
+ - Fixed a table alignment bug
981
+
982
+ \`\`\`ts
983
+ const x = 1;
984
+ \`\`\`
985
+
986
+ | Component | Status |
987
+ | --- | --- |
988
+ | markdown-view | New |
989
+
990
+ See the [changelog](#markdown-view) for details.`;
991
+ </script>
992
+ ```
993
+
955
994
  ## <multi-select>
956
995
 
957
996
  A form-associated multi-select: a trigger showing a compact summary of the
@@ -1495,6 +1534,45 @@ Example:
1495
1534
  </script>
1496
1535
  ```
1497
1536
 
1537
+ ## <tree-view>
1538
+
1539
+ A generic, presentational tree shell: renders `nodes` recursively, one row
1540
+ per node, with each row's content produced by `renderNode` (default: plain
1541
+ label). Modeled on `data-table`'s headless pattern — knows nothing about
1542
+ what a node's `data` means beyond what `renderNode` does with it.
1543
+
1544
+ A node with a `children` array (even empty) is a folder: clicking or
1545
+ activating its row toggles expand/collapse instead of firing `node-click`.
1546
+ A node with no `children` is a leaf: clicking or activating its row fires
1547
+ `node-click`. Folders start collapsed; set `default-expanded` to start
1548
+ every folder expanded instead. Expansion state is otherwise managed
1549
+ internally and untouched by later `nodes` updates, so a user's manual
1550
+ toggles survive a data refresh.
1551
+
1552
+ Import: `import "@f-ewald/components/tree-view.js";`
1553
+
1554
+ Properties: `nodes` (JS property only) : TreeNode[], default []; `renderNode` (JS property only) : (node: TreeNode) => unknown, default —; `defaultExpanded` (attribute `default-expanded`) : boolean, default false
1555
+ Events: `node-click`
1556
+ CSS custom properties: `--ui-focus-ring`, `--ui-font`, `--ui-font-size-sm`, `--ui-radius-sm`, `--ui-surface-muted`, `--ui-text`, `--ui-text-muted`
1557
+
1558
+ Example:
1559
+ ```html
1560
+ <tree-view></tree-view>
1561
+ <script type="module">
1562
+ const tree = document.querySelector("tree-view");
1563
+ tree.nodes = [
1564
+ {
1565
+ id: "docs",
1566
+ label: "docs",
1567
+ children: [{ id: "fil_1", label: "notes.txt", data: { id: "fil_1" } }],
1568
+ },
1569
+ { id: "fil_2", label: "readme.md", data: { id: "fil_2" } },
1570
+ ];
1571
+ tree.renderNode = (node) => node.label;
1572
+ tree.addEventListener("node-click", (e) => console.log(e.detail));
1573
+ </script>
1574
+ ```
1575
+
1498
1576
  ## <ui-button>
1499
1577
 
1500
1578
  Button (or link styled as one) with an optional leading icon, in three
@@ -1512,9 +1590,9 @@ is instead wired through `ElementInternals.form` — the same mechanism
1512
1590
 
1513
1591
  Import: `import "@f-ewald/components/ui-button.js";`
1514
1592
 
1515
- Properties: `variant` (attribute `variant`) : ButtonVariant, default "primary"; `href` (attribute `href`) : string | null, default null; `type` (attribute `type`) : "button" | "submit" | "reset", default "button"; `disabled` (attribute `disabled`) : boolean, default false; `busy` (attribute `busy`) : boolean, default false
1593
+ Properties: `variant` (attribute `variant`) : ButtonVariant, default "primary"; `size` (attribute `size`) : "sm" | "md", default "md"; `href` (attribute `href`) : string | null, default null; `type` (attribute `type`) : "button" | "submit" | "reset", default "button"; `disabled` (attribute `disabled`) : boolean, default false; `busy` (attribute `busy`) : boolean, default false
1516
1594
  Events: none
1517
- CSS custom properties: `--ui-border`, `--ui-danger`, `--ui-danger-hover`, `--ui-focus-ring`, `--ui-font`, `--ui-font-size-sm`, `--ui-font-weight-medium`, `--ui-line-height-tight`, `--ui-on-accent`, `--ui-primary`, `--ui-primary-hover`, `--ui-radius-sm`, `--ui-text`, `--ui-text-muted`
1595
+ CSS custom properties: `--ui-border`, `--ui-danger`, `--ui-danger-hover`, `--ui-focus-ring`, `--ui-font`, `--ui-font-size-sm`, `--ui-font-size-xs`, `--ui-font-weight-medium`, `--ui-line-height-tight`, `--ui-on-accent`, `--ui-primary`, `--ui-primary-hover`, `--ui-radius-sm`, `--ui-text`, `--ui-text-muted`
1518
1596
 
1519
1597
  Example:
1520
1598
  ```html
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@f-ewald/components",
3
3
  "private": false,
4
- "version": "1.10.0",
4
+ "version": "1.12.0",
5
5
  "description": "A collection of universally usable web components for various tasks.",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
@@ -63,7 +63,9 @@
63
63
  "d3-array": "^3.2.4",
64
64
  "d3-scale": "^4.0.2",
65
65
  "d3-shape": "^3.2.0",
66
+ "dompurify": "^3.4.12",
66
67
  "lit": "^3.3.3",
68
+ "marked": "^18.0.7",
67
69
  "zod": "^4.4.3"
68
70
  },
69
71
  "publishConfig": {