@f-ewald/components 1.11.0 → 1.13.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 +7 -0
- package/custom-elements.json +1651 -115
- package/dist/auto-scroll.d.ts +37 -0
- package/dist/auto-scroll.d.ts.map +1 -0
- package/dist/auto-scroll.js +100 -0
- package/dist/auto-scroll.js.map +1 -0
- package/dist/form-field.d.ts +39 -0
- package/dist/form-field.d.ts.map +1 -0
- package/dist/form-field.js +145 -0
- package/dist/form-field.js.map +1 -0
- package/dist/icons.d.ts +1 -0
- package/dist/icons.d.ts.map +1 -1
- package/dist/icons.js +1 -0
- package/dist/icons.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/load-more.d.ts +33 -0
- package/dist/load-more.d.ts.map +1 -0
- package/dist/load-more.js +82 -0
- package/dist/load-more.js.map +1 -0
- package/dist/percent-bar-chart.d.ts +32 -3
- package/dist/percent-bar-chart.d.ts.map +1 -1
- package/dist/percent-bar-chart.js +101 -36
- package/dist/percent-bar-chart.js.map +1 -1
- package/dist/scroll-to-bottom.d.ts +41 -0
- package/dist/scroll-to-bottom.d.ts.map +1 -0
- package/dist/scroll-to-bottom.js +189 -0
- package/dist/scroll-to-bottom.js.map +1 -0
- package/dist/scroll-to-top.d.ts +41 -0
- package/dist/scroll-to-top.d.ts.map +1 -0
- package/dist/scroll-to-top.js +189 -0
- package/dist/scroll-to-top.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-checkbox.d.ts +49 -0
- package/dist/ui-checkbox.d.ts.map +1 -0
- package/dist/ui-checkbox.js +228 -0
- package/dist/ui-checkbox.js.map +1 -0
- package/dist/utils/scroll.d.ts +9 -0
- package/dist/utils/scroll.d.ts.map +1 -0
- package/dist/utils/scroll.js +30 -0
- package/dist/utils/scroll.js.map +1 -0
- package/docs/auto-scroll.md +52 -0
- package/docs/form-field.md +63 -0
- package/docs/layouts/form-page.md +8 -4
- package/docs/load-more.md +43 -0
- package/docs/percent-bar-chart.md +19 -5
- package/docs/scroll-to-bottom.md +68 -0
- package/docs/scroll-to-top.md +59 -0
- package/docs/tree-view.md +69 -0
- package/docs/ui-checkbox.md +58 -0
- package/llms.txt +223 -6
- package/package.json +1 -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"]}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { LitElement, type PropertyValues } from "lit";
|
|
2
|
+
/**
|
|
3
|
+
* A form-associated boolean checkbox, usable standalone or inside a native
|
|
4
|
+
* `<form>`. Submits `name=on` when checked (matching native
|
|
5
|
+
* `<input type="checkbox">` semantics) and participates fully in form
|
|
6
|
+
* `reset()`, ancestor `<fieldset disabled>`, and `required` validity.
|
|
7
|
+
*
|
|
8
|
+
* Renders a native `<input type="checkbox">` wrapped in a `<label>`, styled
|
|
9
|
+
* via `:has()` on the wrapping label (matching `radio-pills`/`radio-cards`)
|
|
10
|
+
* rather than styling the native input directly; the checkbox itself renders
|
|
11
|
+
* at `1rem`, matching the existing radio-input convention.
|
|
12
|
+
*
|
|
13
|
+
* @element ui-checkbox
|
|
14
|
+
* @fires change - The checkbox was toggled by the user, in either direction;
|
|
15
|
+
* detail: `{ checked }`. Programmatic `checked` assignments do not fire it.
|
|
16
|
+
*/
|
|
17
|
+
export declare class UiCheckbox extends LitElement {
|
|
18
|
+
#private;
|
|
19
|
+
static formAssociated: boolean;
|
|
20
|
+
static styles: import("lit").CSSResult[];
|
|
21
|
+
/** Whether the box is checked. */
|
|
22
|
+
checked: boolean;
|
|
23
|
+
/** Visual "partial selection" state; cleared on the next user interaction. */
|
|
24
|
+
indeterminate: boolean;
|
|
25
|
+
/** Disables interaction; merged with an ancestor `<fieldset disabled>`. */
|
|
26
|
+
disabled: boolean;
|
|
27
|
+
/** Marks the control invalid via `ElementInternals` while unchecked. */
|
|
28
|
+
required: boolean;
|
|
29
|
+
/** Form field name; submitted as `name=on` only while checked. */
|
|
30
|
+
name: string;
|
|
31
|
+
/** Visible label text rendered next to the box. */
|
|
32
|
+
label: string;
|
|
33
|
+
private _formDisabled;
|
|
34
|
+
protected willUpdate(_changed: PropertyValues): void;
|
|
35
|
+
protected updated(changed: PropertyValues): void;
|
|
36
|
+
/** Restores the checked state captured at first render, per the form contract. */
|
|
37
|
+
formResetCallback(): void;
|
|
38
|
+
/** Mirrors an ancestor fieldset's disabled state. */
|
|
39
|
+
formDisabledCallback(disabled: boolean): void;
|
|
40
|
+
/** Restores the checked state from saved form state (bfcache/autofill). */
|
|
41
|
+
formStateRestoreCallback(state: string | File | FormData | null): void;
|
|
42
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
43
|
+
}
|
|
44
|
+
declare global {
|
|
45
|
+
interface HTMLElementTagNameMap {
|
|
46
|
+
"ui-checkbox": UiCheckbox;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=ui-checkbox.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,228 @@
|
|
|
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 } from "lit";
|
|
8
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
9
|
+
import { ref } from "lit/directives/ref.js";
|
|
10
|
+
import { tokens } from "./tokens.js";
|
|
11
|
+
/**
|
|
12
|
+
* A form-associated boolean checkbox, usable standalone or inside a native
|
|
13
|
+
* `<form>`. Submits `name=on` when checked (matching native
|
|
14
|
+
* `<input type="checkbox">` semantics) and participates fully in form
|
|
15
|
+
* `reset()`, ancestor `<fieldset disabled>`, and `required` validity.
|
|
16
|
+
*
|
|
17
|
+
* Renders a native `<input type="checkbox">` wrapped in a `<label>`, styled
|
|
18
|
+
* via `:has()` on the wrapping label (matching `radio-pills`/`radio-cards`)
|
|
19
|
+
* rather than styling the native input directly; the checkbox itself renders
|
|
20
|
+
* at `1rem`, matching the existing radio-input convention.
|
|
21
|
+
*
|
|
22
|
+
* @element ui-checkbox
|
|
23
|
+
* @fires change - The checkbox was toggled by the user, in either direction;
|
|
24
|
+
* detail: `{ checked }`. Programmatic `checked` assignments do not fire it.
|
|
25
|
+
*/
|
|
26
|
+
let UiCheckbox = class UiCheckbox extends LitElement {
|
|
27
|
+
constructor() {
|
|
28
|
+
super(...arguments);
|
|
29
|
+
/** Whether the box is checked. */
|
|
30
|
+
this.checked = false;
|
|
31
|
+
/** Visual "partial selection" state; cleared on the next user interaction. */
|
|
32
|
+
this.indeterminate = false;
|
|
33
|
+
/** Disables interaction; merged with an ancestor `<fieldset disabled>`. */
|
|
34
|
+
this.disabled = false;
|
|
35
|
+
/** Marks the control invalid via `ElementInternals` while unchecked. */
|
|
36
|
+
this.required = false;
|
|
37
|
+
/** Form field name; submitted as `name=on` only while checked. */
|
|
38
|
+
this.name = "";
|
|
39
|
+
/** Visible label text rendered next to the box. */
|
|
40
|
+
this.label = "";
|
|
41
|
+
this._formDisabled = false;
|
|
42
|
+
this.#internals = this.attachInternals();
|
|
43
|
+
this.#defaultChecked = false;
|
|
44
|
+
this.#inputEl = null;
|
|
45
|
+
this.#onInputRef = (el) => {
|
|
46
|
+
this.#inputEl = el ?? null;
|
|
47
|
+
if (this.#inputEl)
|
|
48
|
+
this.#inputEl.indeterminate = this.indeterminate;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
static { this.formAssociated = true; }
|
|
52
|
+
static { this.styles = [
|
|
53
|
+
tokens,
|
|
54
|
+
css `
|
|
55
|
+
:host {
|
|
56
|
+
display: inline-block;
|
|
57
|
+
}
|
|
58
|
+
.checkbox {
|
|
59
|
+
display: inline-flex;
|
|
60
|
+
align-items: center;
|
|
61
|
+
gap: 0.5rem;
|
|
62
|
+
min-height: 2rem;
|
|
63
|
+
cursor: pointer;
|
|
64
|
+
font-family: var(--ui-font, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");
|
|
65
|
+
font-size: var(--ui-font-size-sm, 0.75rem);
|
|
66
|
+
line-height: var(--ui-line-height-tight, 1.25);
|
|
67
|
+
color: var(--ui-text, #0f172a);
|
|
68
|
+
}
|
|
69
|
+
.checkbox input {
|
|
70
|
+
width: 1rem;
|
|
71
|
+
height: 1rem;
|
|
72
|
+
margin: 0;
|
|
73
|
+
accent-color: var(--ui-primary, #4f46e5);
|
|
74
|
+
cursor: pointer;
|
|
75
|
+
}
|
|
76
|
+
.checkbox:has(input:focus-visible) {
|
|
77
|
+
outline: none;
|
|
78
|
+
}
|
|
79
|
+
.checkbox:has(input:focus-visible) input {
|
|
80
|
+
outline: none;
|
|
81
|
+
box-shadow: var(--ui-focus-ring, 0 0 0 3px rgb(79 70 229 / 0.35));
|
|
82
|
+
border-radius: var(--ui-radius-sm, 0.25rem);
|
|
83
|
+
}
|
|
84
|
+
.checkbox:has(input:disabled) {
|
|
85
|
+
cursor: not-allowed;
|
|
86
|
+
opacity: 0.6;
|
|
87
|
+
}
|
|
88
|
+
.checkbox:has(input:disabled) input {
|
|
89
|
+
cursor: not-allowed;
|
|
90
|
+
}
|
|
91
|
+
.required-mark {
|
|
92
|
+
color: var(--ui-danger, #dc2626);
|
|
93
|
+
}
|
|
94
|
+
.sr-only {
|
|
95
|
+
position: absolute;
|
|
96
|
+
width: 1px;
|
|
97
|
+
height: 1px;
|
|
98
|
+
margin: -1px;
|
|
99
|
+
padding: 0;
|
|
100
|
+
overflow: hidden;
|
|
101
|
+
clip: rect(0 0 0 0);
|
|
102
|
+
clip-path: inset(50%);
|
|
103
|
+
white-space: nowrap;
|
|
104
|
+
border: 0;
|
|
105
|
+
}
|
|
106
|
+
@media (forced-colors: active) {
|
|
107
|
+
.checkbox:has(input:focus-visible) input {
|
|
108
|
+
outline: 2px solid CanvasText;
|
|
109
|
+
outline-offset: 2px;
|
|
110
|
+
box-shadow: none;
|
|
111
|
+
}
|
|
112
|
+
.checkbox:has(input:disabled) {
|
|
113
|
+
color: GrayText;
|
|
114
|
+
opacity: 1;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
`,
|
|
118
|
+
]; }
|
|
119
|
+
#internals;
|
|
120
|
+
#defaultChecked;
|
|
121
|
+
#inputEl;
|
|
122
|
+
/** Whether the host or an ancestor fieldset currently disables the control. */
|
|
123
|
+
get #isDisabled() {
|
|
124
|
+
return this.disabled || this._formDisabled;
|
|
125
|
+
}
|
|
126
|
+
willUpdate(_changed) {
|
|
127
|
+
if (!this.hasUpdated)
|
|
128
|
+
this.#defaultChecked = this.checked;
|
|
129
|
+
}
|
|
130
|
+
updated(changed) {
|
|
131
|
+
if (changed.has("checked") || changed.has("name"))
|
|
132
|
+
this.#syncFormValue();
|
|
133
|
+
if (changed.has("checked") || changed.has("required") || changed.has("disabled") || changed.has("_formDisabled")) {
|
|
134
|
+
this.#syncValidity();
|
|
135
|
+
}
|
|
136
|
+
if (changed.has("indeterminate") && this.#inputEl) {
|
|
137
|
+
this.#inputEl.indeterminate = this.indeterminate;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/** Restores the checked state captured at first render, per the form contract. */
|
|
141
|
+
formResetCallback() {
|
|
142
|
+
this.checked = this.#defaultChecked;
|
|
143
|
+
this.indeterminate = false;
|
|
144
|
+
}
|
|
145
|
+
/** Mirrors an ancestor fieldset's disabled state. */
|
|
146
|
+
formDisabledCallback(disabled) {
|
|
147
|
+
this._formDisabled = disabled;
|
|
148
|
+
}
|
|
149
|
+
/** Restores the checked state from saved form state (bfcache/autofill). */
|
|
150
|
+
formStateRestoreCallback(state) {
|
|
151
|
+
if (typeof state !== "string")
|
|
152
|
+
return;
|
|
153
|
+
this.checked = state === "on";
|
|
154
|
+
}
|
|
155
|
+
#syncFormValue() {
|
|
156
|
+
if (!this.name || !this.checked) {
|
|
157
|
+
this.#internals.setFormValue(null);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
this.#internals.setFormValue("on");
|
|
161
|
+
}
|
|
162
|
+
#syncValidity() {
|
|
163
|
+
if (this.#isDisabled) {
|
|
164
|
+
this.#internals.setValidity({});
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (this.required && !this.checked) {
|
|
168
|
+
this.#internals.setValidity({ valueMissing: true }, "Please check this box to continue.", this.#inputEl ?? undefined);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
this.#internals.setValidity({});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
#onChange(e) {
|
|
175
|
+
const input = e.target;
|
|
176
|
+
this.checked = input.checked;
|
|
177
|
+
this.indeterminate = false;
|
|
178
|
+
this.dispatchEvent(new CustomEvent("change", { detail: { checked: this.checked }, bubbles: true, composed: true }));
|
|
179
|
+
}
|
|
180
|
+
#onInputRef;
|
|
181
|
+
render() {
|
|
182
|
+
return html `
|
|
183
|
+
<label class="checkbox">
|
|
184
|
+
<input
|
|
185
|
+
${ref(this.#onInputRef)}
|
|
186
|
+
type="checkbox"
|
|
187
|
+
.checked=${this.checked}
|
|
188
|
+
?disabled=${this.#isDisabled}
|
|
189
|
+
?required=${this.required}
|
|
190
|
+
@change=${(e) => this.#onChange(e)}
|
|
191
|
+
/>
|
|
192
|
+
<span
|
|
193
|
+
>${this.label}${this.required
|
|
194
|
+
? html `<span class="required-mark" aria-hidden="true"> *</span><span class="sr-only">
|
|
195
|
+
(required)</span
|
|
196
|
+
>`
|
|
197
|
+
: ""}</span
|
|
198
|
+
>
|
|
199
|
+
</label>
|
|
200
|
+
`;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
__decorate([
|
|
204
|
+
property({ type: Boolean })
|
|
205
|
+
], UiCheckbox.prototype, "checked", void 0);
|
|
206
|
+
__decorate([
|
|
207
|
+
property({ type: Boolean })
|
|
208
|
+
], UiCheckbox.prototype, "indeterminate", void 0);
|
|
209
|
+
__decorate([
|
|
210
|
+
property({ type: Boolean })
|
|
211
|
+
], UiCheckbox.prototype, "disabled", void 0);
|
|
212
|
+
__decorate([
|
|
213
|
+
property({ type: Boolean })
|
|
214
|
+
], UiCheckbox.prototype, "required", void 0);
|
|
215
|
+
__decorate([
|
|
216
|
+
property()
|
|
217
|
+
], UiCheckbox.prototype, "name", void 0);
|
|
218
|
+
__decorate([
|
|
219
|
+
property()
|
|
220
|
+
], UiCheckbox.prototype, "label", void 0);
|
|
221
|
+
__decorate([
|
|
222
|
+
state()
|
|
223
|
+
], UiCheckbox.prototype, "_formDisabled", void 0);
|
|
224
|
+
UiCheckbox = __decorate([
|
|
225
|
+
customElement("ui-checkbox")
|
|
226
|
+
], UiCheckbox);
|
|
227
|
+
export { UiCheckbox };
|
|
228
|
+
//# sourceMappingURL=ui-checkbox.js.map
|
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** A scrollable region: either `window`/document scrolling, or an element with its own scrollbox. */
|
|
2
|
+
export type ScrollableTarget = HTMLElement | Window;
|
|
3
|
+
/** Pixels between the current scroll position and the top edge. */
|
|
4
|
+
export declare function distanceFromTop(target: ScrollableTarget): number;
|
|
5
|
+
/** Pixels between the current scroll position and the bottom edge. */
|
|
6
|
+
export declare function distanceFromBottom(target: ScrollableTarget): number;
|
|
7
|
+
/** Scrolls `target` to its top or bottom edge, honoring `prefers-reduced-motion`. */
|
|
8
|
+
export declare function scrollToEdge(target: ScrollableTarget, edge: "top" | "bottom"): void;
|
|
9
|
+
//# sourceMappingURL=scroll.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scroll.d.ts","sourceRoot":"","sources":["../../src/utils/scroll.ts"],"names":[],"mappings":"AAAA,qGAAqG;AACrG,MAAM,MAAM,gBAAgB,GAAG,WAAW,GAAG,MAAM,CAAC;AAUpD,mEAAmE;AACnE,wBAAgB,eAAe,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,CAEhE;AAED,sEAAsE;AACtE,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,CAGnE;AAED,qFAAqF;AACrF,wBAAgB,YAAY,CAAC,MAAM,EAAE,gBAAgB,EAAE,IAAI,EAAE,KAAK,GAAG,QAAQ,GAAG,IAAI,CAUnF"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
function metrics(target) {
|
|
2
|
+
if (target instanceof Window) {
|
|
3
|
+
const doc = document.documentElement;
|
|
4
|
+
return { scrollTop: window.scrollY, scrollHeight: doc.scrollHeight, clientHeight: window.innerHeight };
|
|
5
|
+
}
|
|
6
|
+
return { scrollTop: target.scrollTop, scrollHeight: target.scrollHeight, clientHeight: target.clientHeight };
|
|
7
|
+
}
|
|
8
|
+
/** Pixels between the current scroll position and the top edge. */
|
|
9
|
+
export function distanceFromTop(target) {
|
|
10
|
+
return metrics(target).scrollTop;
|
|
11
|
+
}
|
|
12
|
+
/** Pixels between the current scroll position and the bottom edge. */
|
|
13
|
+
export function distanceFromBottom(target) {
|
|
14
|
+
const { scrollTop, scrollHeight, clientHeight } = metrics(target);
|
|
15
|
+
return scrollHeight - scrollTop - clientHeight;
|
|
16
|
+
}
|
|
17
|
+
/** Scrolls `target` to its top or bottom edge, honoring `prefers-reduced-motion`. */
|
|
18
|
+
export function scrollToEdge(target, edge) {
|
|
19
|
+
const behavior = window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
20
|
+
? "instant"
|
|
21
|
+
: "smooth";
|
|
22
|
+
const top = edge === "top" ? 0 : metrics(target).scrollHeight;
|
|
23
|
+
if (target instanceof Window) {
|
|
24
|
+
window.scrollTo({ top, behavior });
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
target.scrollTo({ top, behavior });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=scroll.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scroll.js","sourceRoot":"","sources":["../../src/utils/scroll.ts"],"names":[],"mappings":"AAGA,SAAS,OAAO,CAAC,MAAwB;IACvC,IAAI,MAAM,YAAY,MAAM,EAAE,CAAC;QAC7B,MAAM,GAAG,GAAG,QAAQ,CAAC,eAAe,CAAC;QACrC,OAAO,EAAE,SAAS,EAAE,MAAM,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,CAAC,YAAY,EAAE,YAAY,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC;IACzG,CAAC;IACD,OAAO,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,YAAY,EAAE,MAAM,CAAC,YAAY,EAAE,YAAY,EAAE,MAAM,CAAC,YAAY,EAAE,CAAC;AAC/G,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,eAAe,CAAC,MAAwB;IACtD,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC;AACnC,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,kBAAkB,CAAC,MAAwB;IACzD,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAClE,OAAO,YAAY,GAAG,SAAS,GAAG,YAAY,CAAC;AACjD,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,YAAY,CAAC,MAAwB,EAAE,IAAsB;IAC3E,MAAM,QAAQ,GAAmB,MAAM,CAAC,UAAU,CAAC,kCAAkC,CAAC,CAAC,OAAO;QAC5F,CAAC,CAAC,SAAS;QACX,CAAC,CAAC,QAAQ,CAAC;IACb,MAAM,GAAG,GAAG,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC;IAC9D,IAAI,MAAM,YAAY,MAAM,EAAE,CAAC;QAC7B,MAAM,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC;IACrC,CAAC;SAAM,CAAC;QACN,MAAM,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC;IACrC,CAAC;AACH,CAAC","sourcesContent":["/** A scrollable region: either `window`/document scrolling, or an element with its own scrollbox. */\nexport type ScrollableTarget = HTMLElement | Window;\n\nfunction metrics(target: ScrollableTarget): { scrollTop: number; scrollHeight: number; clientHeight: number } {\n if (target instanceof Window) {\n const doc = document.documentElement;\n return { scrollTop: window.scrollY, scrollHeight: doc.scrollHeight, clientHeight: window.innerHeight };\n }\n return { scrollTop: target.scrollTop, scrollHeight: target.scrollHeight, clientHeight: target.clientHeight };\n}\n\n/** Pixels between the current scroll position and the top edge. */\nexport function distanceFromTop(target: ScrollableTarget): number {\n return metrics(target).scrollTop;\n}\n\n/** Pixels between the current scroll position and the bottom edge. */\nexport function distanceFromBottom(target: ScrollableTarget): number {\n const { scrollTop, scrollHeight, clientHeight } = metrics(target);\n return scrollHeight - scrollTop - clientHeight;\n}\n\n/** Scrolls `target` to its top or bottom edge, honoring `prefers-reduced-motion`. */\nexport function scrollToEdge(target: ScrollableTarget, edge: \"top\" | \"bottom\"): void {\n const behavior: ScrollBehavior = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n ? \"instant\"\n : \"smooth\";\n const top = edge === \"top\" ? 0 : metrics(target).scrollHeight;\n if (target instanceof Window) {\n window.scrollTo({ top, behavior });\n } else {\n target.scrollTo({ top, behavior });\n }\n}\n"]}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# `<auto-scroll>`
|
|
2
|
+
|
|
3
|
+
Wraps arbitrary slotted content (e.g. `timeline-container`) and keeps it
|
|
4
|
+
scrolled to the bottom as new children are appended — but only while the
|
|
5
|
+
user is already scrolled near the bottom ("stick to bottom", a chat/log-
|
|
6
|
+
viewer convention). If the user has scrolled up to read earlier content,
|
|
7
|
+
new content does not yank the scroll position back down.
|
|
8
|
+
|
|
9
|
+
New content is detected via a `MutationObserver`, so no cooperation from
|
|
10
|
+
whatever is slotted in is required. The host itself is the scrollable
|
|
11
|
+
region (`overflow-y: auto`) and needs a consumer-supplied bounded height to
|
|
12
|
+
have anything to scroll, e.g. `auto-scroll { height: 24rem; }`.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
import "@f-ewald/components/auto-scroll.js";
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```html
|
|
23
|
+
<auto-scroll style="height: 24rem">
|
|
24
|
+
<timeline-container>
|
|
25
|
+
<timeline-entry datetime="2026-07-23T09:00:00Z">
|
|
26
|
+
<span slot="headline">Deployment started</span>
|
|
27
|
+
Release v1.4.0 is rolling out.
|
|
28
|
+
</timeline-entry>
|
|
29
|
+
</timeline-container>
|
|
30
|
+
</auto-scroll>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Attributes / properties
|
|
34
|
+
|
|
35
|
+
| Property | Attribute | Type | Default | Description |
|
|
36
|
+
| --- | --- | --- | --- | --- |
|
|
37
|
+
| `threshold` | `threshold` | `number` | `24` | Pixels of tolerance from the bottom edge counted as "still at the bottom". |
|
|
38
|
+
| `pinned` | _(JS property only)_ | `boolean` | `—` | Whether the view is currently stuck to the bottom. Read-only; see `scrollToBottom()`. _(read-only)_ |
|
|
39
|
+
|
|
40
|
+
## Events
|
|
41
|
+
|
|
42
|
+
| Event | Description |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| `pinned-change` | The stick-to-bottom state toggled; detail: `{ pinned }`. |
|
|
45
|
+
|
|
46
|
+
## Slots
|
|
47
|
+
|
|
48
|
+
_None._
|
|
49
|
+
|
|
50
|
+
## CSS custom properties
|
|
51
|
+
|
|
52
|
+
_None._
|