@f-ewald/components 1.12.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 +6 -0
- package/custom-elements.json +1345 -56
- 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 +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -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/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/ui-checkbox.md +58 -0
- package/llms.txt +184 -6
- package/package.json +1 -1
|
@@ -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._
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# `<form-field>`
|
|
2
|
+
|
|
3
|
+
Per-field wrapper for a form control: label, slotted control, and an
|
|
4
|
+
optional hint or error message, in one consistent unit repeated across a
|
|
5
|
+
form. Purely presentational — composes whatever control is slotted
|
|
6
|
+
(`form-select`, `multi-select`, `autocomplete-input`, `ui-checkbox`, etc.)
|
|
7
|
+
without intercepting its events or value.
|
|
8
|
+
|
|
9
|
+
The label wraps the default slot for a best-effort visual/click
|
|
10
|
+
association only: every existing value-entry control encapsulates its
|
|
11
|
+
real `<input>` inside its own shadow DOM, so there is no light-DOM `id` a
|
|
12
|
+
`for` attribute could target from outside, and this component cannot set
|
|
13
|
+
`aria-describedby`/`aria-invalid` on an arbitrary slotted control's
|
|
14
|
+
shadow-encapsulated input for the same reason. The error message uses
|
|
15
|
+
`role="alert"` as the practical accessibility mitigation instead of true
|
|
16
|
+
`aria-describedby` association.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
import "@f-ewald/components/form-field.js";
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```html
|
|
27
|
+
<form-field label="Task state" hint="Only affects your own view">
|
|
28
|
+
<form-select></form-select>
|
|
29
|
+
</form-field>
|
|
30
|
+
<form-field label="Terms" required error="You must accept to continue">
|
|
31
|
+
<ui-checkbox label="I agree to the terms"></ui-checkbox>
|
|
32
|
+
</form-field>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Attributes / properties
|
|
36
|
+
|
|
37
|
+
| Property | Attribute | Type | Default | Description |
|
|
38
|
+
| --- | --- | --- | --- | --- |
|
|
39
|
+
| `label` | `label` | `string` | `""` | Field label text. |
|
|
40
|
+
| `hint` | `hint` | `string` | `""` | Optional helper text shown below the control when there's no `error`. |
|
|
41
|
+
| `error` | `error` | `string` | `""` | Optional error text; replaces the `hint` display when non-empty. |
|
|
42
|
+
| `required` | `required` | `boolean` | `false` | Shows a required indicator next to the label. |
|
|
43
|
+
|
|
44
|
+
## Events
|
|
45
|
+
|
|
46
|
+
_None._
|
|
47
|
+
|
|
48
|
+
## Slots
|
|
49
|
+
|
|
50
|
+
_None._
|
|
51
|
+
|
|
52
|
+
## CSS custom properties
|
|
53
|
+
|
|
54
|
+
| Custom property |
|
|
55
|
+
| --- |
|
|
56
|
+
| `--ui-danger` |
|
|
57
|
+
| `--ui-font` |
|
|
58
|
+
| `--ui-font-size-sm` |
|
|
59
|
+
| `--ui-font-size-xs` |
|
|
60
|
+
| `--ui-font-weight-medium` |
|
|
61
|
+
| `--ui-line-height-tight` |
|
|
62
|
+
| `--ui-text` |
|
|
63
|
+
| `--ui-text-muted` |
|
|
@@ -24,8 +24,11 @@ column so the eye tracks one field to the next.
|
|
|
24
24
|
</page-header>
|
|
25
25
|
|
|
26
26
|
<form style="max-width: 40rem;">
|
|
27
|
-
<frame-box label="Details"
|
|
28
|
-
|
|
27
|
+
<frame-box label="Details">
|
|
28
|
+
<form-field label="Name"><autocomplete-input name="name"></autocomplete-input></form-field>
|
|
29
|
+
<form-field label="Email" hint="Work email preferred"><autocomplete-input name="email"></autocomplete-input></form-field>
|
|
30
|
+
</frame-box>
|
|
31
|
+
<frame-box label="Role"><!-- role field, also wrapped in form-field --></frame-box>
|
|
29
32
|
|
|
30
33
|
<form-actions>
|
|
31
34
|
<ui-button slot="secondary" variant="secondary">Cancel</ui-button>
|
|
@@ -43,10 +46,11 @@ column so the eye tracks one field to the next.
|
|
|
43
46
|
- Constrain the form with a `max-width` (about `40rem`); a full-width form on a
|
|
44
47
|
wide dashboard is hard to scan.
|
|
45
48
|
- Group related fields with `frame-box`; keep one field per row in a single
|
|
46
|
-
column.
|
|
49
|
+
column. Wrap each individual field in `form-field` for a consistent
|
|
50
|
+
label/hint/error layout.
|
|
47
51
|
- `ui-button type="submit"` associates with the ancestor `<form>` even from
|
|
48
52
|
inside `form-actions`.
|
|
49
53
|
|
|
50
54
|
**Live demo:** `demo/layouts/form-page.html`
|
|
51
55
|
|
|
52
|
-
**Components:** app-shell, app-sidebar, page-header, frame-box, form-actions, ui-button
|
|
56
|
+
**Components:** app-shell, app-sidebar, page-header, frame-box, form-field, form-actions, ui-button
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# `<load-more>`
|
|
2
|
+
|
|
3
|
+
Click-to-load button for either end of a list. Fully property-driven: the
|
|
4
|
+
consumer sets `loading` while a fetch is in flight and `exhausted` once
|
|
5
|
+
there's nothing left to load; this component never fetches or manages
|
|
6
|
+
state itself.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```js
|
|
11
|
+
import "@f-ewald/components/load-more.js";
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```html
|
|
17
|
+
<load-more direction="top" label="Load older"></load-more>
|
|
18
|
+
<load-more></load-more>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Attributes / properties
|
|
22
|
+
|
|
23
|
+
| Property | Attribute | Type | Default | Description |
|
|
24
|
+
| --- | --- | --- | --- | --- |
|
|
25
|
+
| `direction` | `direction` | `"top" | "bottom"` | `"bottom"` | Which end of a list this instance loads more content for. |
|
|
26
|
+
| `loading` | `loading` | `boolean` | `false` | Consumer-managed busy flag, forwarded to the internal `ui-button`'s `busy`. |
|
|
27
|
+
| `exhausted` | `exhausted` | `boolean` | `false` | Terminal "no more content" state: disables the button and swaps its label. |
|
|
28
|
+
| `label` | `label` | `string` | `"Load more"` | Button text in the normal (loadable) state. |
|
|
29
|
+
| `exhaustedLabel` | `exhausted-label` | `string` | `"No more results"` | Button text shown once `exhausted` is true. |
|
|
30
|
+
|
|
31
|
+
## Events
|
|
32
|
+
|
|
33
|
+
| Event | Description |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| `load-more` | The button was clicked while not `loading`/`exhausted`; detail: `{ direction }`. |
|
|
36
|
+
|
|
37
|
+
## Slots
|
|
38
|
+
|
|
39
|
+
_None._
|
|
40
|
+
|
|
41
|
+
## CSS custom properties
|
|
42
|
+
|
|
43
|
+
_None._
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
# `<percent-bar-chart>`
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Bar chart for labeled rows, using D3's linear scale. Horizontal (default)
|
|
4
|
+
renders stacked rows with bars growing rightward; `orientation="vertical"`
|
|
5
|
+
renders side-by-side columns growing upward instead. `mode="percent"`
|
|
6
|
+
(default) scales `value` against a fixed 0-100 domain and labels it with a
|
|
7
|
+
`%` suffix; `mode="value"` scales it against `max` (or the largest `value`
|
|
8
|
+
present) and formats it with `valueFormat`.
|
|
5
9
|
|
|
6
10
|
## Install
|
|
7
11
|
|
|
@@ -14,10 +18,16 @@ import "@f-ewald/components/percent-bar-chart.js";
|
|
|
14
18
|
```html
|
|
15
19
|
<percent-bar-chart></percent-bar-chart>
|
|
16
20
|
<script type="module">
|
|
17
|
-
document.querySelector("percent-bar-chart")
|
|
18
|
-
|
|
19
|
-
{ key: "
|
|
21
|
+
const chart = document.querySelector("percent-bar-chart");
|
|
22
|
+
chart.groups = [
|
|
23
|
+
{ key: "a", label: "White", value: 45.2, color: "#4f46e5" },
|
|
24
|
+
{ key: "b", label: "Asian", value: 28.1, color: "#0d9488" },
|
|
20
25
|
];
|
|
26
|
+
|
|
27
|
+
// Absolute values instead of percentages, as vertical columns:
|
|
28
|
+
chart.mode = "value";
|
|
29
|
+
chart.orientation = "vertical";
|
|
30
|
+
chart.valueFormat = (value) => `$${value.toLocaleString()}`;
|
|
21
31
|
</script>
|
|
22
32
|
```
|
|
23
33
|
|
|
@@ -26,6 +36,10 @@ import "@f-ewald/components/percent-bar-chart.js";
|
|
|
26
36
|
| Property | Attribute | Type | Default | Description |
|
|
27
37
|
| --- | --- | --- | --- | --- |
|
|
28
38
|
| `groups` | _(JS property only)_ | `PercentBarGroup[]` | `[]` | Rows to render, one per group. |
|
|
39
|
+
| `mode` | `mode` | `PercentBarMode` | `"percent"` | Whether `value` is a 0-100 percentage (fixed domain) or an arbitrary number (domain from data/`max`). |
|
|
40
|
+
| `orientation` | `orientation` | `PercentBarOrientation` | `"horizontal"` | Bar direction: stacked rows growing rightward, or columns growing upward. |
|
|
41
|
+
| `max` | `max` | `number | undefined` | `—` | Explicit domain max for `mode="value"`; auto-computed from `groups` when unset. Ignored in `mode="percent"`. |
|
|
42
|
+
| `valueFormat` | _(JS property only)_ | `(value: number) => string` | `—` | Formats a row's value for its label in `mode="value"`. Defaults to locale-formatted number. |
|
|
29
43
|
|
|
30
44
|
## Events
|
|
31
45
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# `<scroll-to-bottom>`
|
|
2
|
+
|
|
3
|
+
Overlay button that appears once the page (or a given `target` container)
|
|
4
|
+
has scrolled more than `threshold` pixels away from the bottom edge, and
|
|
5
|
+
scrolls back to the bottom on click.
|
|
6
|
+
|
|
7
|
+
With no `target` (default), the button is `position: fixed` to the
|
|
8
|
+
viewport. When `target` is set, the button switches to `position:
|
|
9
|
+
absolute` and expects to be placed as a descendant of a `position:
|
|
10
|
+
relative` (or otherwise positioned) ancestor that establishes the visual
|
|
11
|
+
bounds to float within — typically `target` itself, given `overflow-y:
|
|
12
|
+
auto; position: relative`, so the button stays pinned to that container's
|
|
13
|
+
own visible corner as its content scrolls, rather than floating over the
|
|
14
|
+
whole page.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
import "@f-ewald/components/scroll-to-bottom.js";
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```html
|
|
25
|
+
<scroll-to-bottom></scroll-to-bottom>
|
|
26
|
+
|
|
27
|
+
<!-- Floats inside its own scrollport instead of the whole page: -->
|
|
28
|
+
<div id="log" style="position: relative; overflow-y: auto; height: 10rem">
|
|
29
|
+
...
|
|
30
|
+
<scroll-to-bottom threshold="20"></scroll-to-bottom>
|
|
31
|
+
</div>
|
|
32
|
+
<script type="module">
|
|
33
|
+
document.querySelector('scroll-to-bottom').target = document.querySelector('#log');
|
|
34
|
+
</script>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Attributes / properties
|
|
38
|
+
|
|
39
|
+
| Property | Attribute | Type | Default | Description |
|
|
40
|
+
| --- | --- | --- | --- | --- |
|
|
41
|
+
| `target` | _(JS property only)_ | `HTMLElement | null` | `null` | Scrollable container to control; `null` (default) scrolls `window`. |
|
|
42
|
+
| `threshold` | `threshold` | `number` | `200` | Pixels scrolled away from the bottom before the button appears. |
|
|
43
|
+
| `label` | `label` | `string` | `"Scroll to bottom"` | Visible button text, and its accessible name. |
|
|
44
|
+
|
|
45
|
+
## Events
|
|
46
|
+
|
|
47
|
+
| Event | Description |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| `scroll-to-bottom-triggered` | The button was clicked, just before scrolling; detail: `{ target }`. |
|
|
50
|
+
|
|
51
|
+
## Slots
|
|
52
|
+
|
|
53
|
+
_None._
|
|
54
|
+
|
|
55
|
+
## CSS custom properties
|
|
56
|
+
|
|
57
|
+
| Custom property |
|
|
58
|
+
| --- |
|
|
59
|
+
| `--ui-border` |
|
|
60
|
+
| `--ui-focus-ring` |
|
|
61
|
+
| `--ui-font` |
|
|
62
|
+
| `--ui-font-size-sm` |
|
|
63
|
+
| `--ui-font-weight-medium` |
|
|
64
|
+
| `--ui-line-height-tight` |
|
|
65
|
+
| `--ui-shadow` |
|
|
66
|
+
| `--ui-surface` |
|
|
67
|
+
| `--ui-surface-muted` |
|
|
68
|
+
| `--ui-text` |
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# `<scroll-to-top>`
|
|
2
|
+
|
|
3
|
+
Overlay button that appears once the page (or a given `target` container)
|
|
4
|
+
has scrolled more than `threshold` pixels away from the top edge, and
|
|
5
|
+
scrolls back to the top on click.
|
|
6
|
+
|
|
7
|
+
With no `target` (default), the button is `position: fixed` to the
|
|
8
|
+
viewport. When `target` is set, the button switches to `position:
|
|
9
|
+
absolute` and expects to be placed as a descendant of a `position:
|
|
10
|
+
relative` (or otherwise positioned) ancestor that establishes the visual
|
|
11
|
+
bounds to float within — typically `target` itself, given `overflow-y:
|
|
12
|
+
auto; position: relative`, so the button stays pinned to that container's
|
|
13
|
+
own visible corner as its content scrolls, rather than floating over the
|
|
14
|
+
whole page.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
import "@f-ewald/components/scroll-to-top.js";
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```html
|
|
25
|
+
<scroll-to-top></scroll-to-top>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Attributes / properties
|
|
29
|
+
|
|
30
|
+
| Property | Attribute | Type | Default | Description |
|
|
31
|
+
| --- | --- | --- | --- | --- |
|
|
32
|
+
| `target` | _(JS property only)_ | `HTMLElement | null` | `null` | Scrollable container to control; `null` (default) scrolls `window`. |
|
|
33
|
+
| `threshold` | `threshold` | `number` | `200` | Pixels scrolled away from the top before the button appears. |
|
|
34
|
+
| `label` | `label` | `string` | `"Scroll to top"` | Visible button text, and its accessible name. |
|
|
35
|
+
|
|
36
|
+
## Events
|
|
37
|
+
|
|
38
|
+
| Event | Description |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| `scroll-to-top-triggered` | The button was clicked, just before scrolling; detail: `{ target }`. |
|
|
41
|
+
|
|
42
|
+
## Slots
|
|
43
|
+
|
|
44
|
+
_None._
|
|
45
|
+
|
|
46
|
+
## CSS custom properties
|
|
47
|
+
|
|
48
|
+
| Custom property |
|
|
49
|
+
| --- |
|
|
50
|
+
| `--ui-border` |
|
|
51
|
+
| `--ui-focus-ring` |
|
|
52
|
+
| `--ui-font` |
|
|
53
|
+
| `--ui-font-size-sm` |
|
|
54
|
+
| `--ui-font-weight-medium` |
|
|
55
|
+
| `--ui-line-height-tight` |
|
|
56
|
+
| `--ui-shadow` |
|
|
57
|
+
| `--ui-surface` |
|
|
58
|
+
| `--ui-surface-muted` |
|
|
59
|
+
| `--ui-text` |
|