@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.
- package/README.md +2 -0
- package/custom-elements.json +434 -0
- package/dist/button-group.d.ts +2 -0
- package/dist/button-group.d.ts.map +1 -1
- package/dist/button-group.js +22 -1
- package/dist/button-group.js.map +1 -1
- package/dist/icons.d.ts +4 -0
- package/dist/icons.d.ts.map +1 -1
- package/dist/icons.js +4 -0
- package/dist/icons.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/markdown-view.d.ts +26 -0
- package/dist/markdown-view.d.ts.map +1 -0
- package/dist/markdown-view.js +176 -0
- package/dist/markdown-view.js.map +1 -0
- package/dist/tree-view.d.ts +44 -0
- package/dist/tree-view.d.ts.map +1 -0
- package/dist/tree-view.js +200 -0
- package/dist/tree-view.js.map +1 -0
- package/dist/ui-button.d.ts +2 -0
- package/dist/ui-button.d.ts.map +1 -1
- package/dist/ui-button.js +11 -1
- package/dist/ui-button.js.map +1 -1
- package/docs/button-group.md +1 -0
- package/docs/markdown-view.md +73 -0
- package/docs/tree-view.md +69 -0
- package/docs/ui-button.md +2 -0
- package/llms.txt +81 -3
- package/package.json +3 -1
|
@@ -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"]}
|
package/dist/ui-button.d.ts
CHANGED
|
@@ -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. */
|
package/dist/ui-button.d.ts.map
CHANGED
|
@@ -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,
|
|
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);
|
package/dist/ui-button.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui-button.js","sourceRoot":"","sources":["../src/ui-button.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAIrC;;;;;;;;;;;;;;;;;GAiBG;AAEI,IAAM,QAAQ,GAAd,MAAM,QAAS,SAAQ,UAAU;IAAjC;;QAGL,eAAU,GAAG,IAAI,CAAC,eAAe,EAAE,CAAC;
|
|
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"]}
|
package/docs/button-group.md
CHANGED
|
@@ -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.
|
|
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": {
|