@spfx-fast/sharepoint-ui 0.0.0-stage → 0.4.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/AGENTS.md +211 -0
- package/LICENSE +21 -0
- package/README.md +897 -2
- package/components.json +5810 -0
- package/dist/appearance.d.ts +6 -0
- package/dist/appearance.js +217 -0
- package/dist/elements/accordion.d.ts +9 -0
- package/dist/elements/accordion.js +338 -0
- package/dist/elements/alert.d.ts +2 -0
- package/dist/elements/alert.js +62 -0
- package/dist/elements/avatar-group-primitives.d.ts +3 -0
- package/dist/elements/avatar-group-primitives.js +189 -0
- package/dist/elements/avatar-group.d.ts +2 -0
- package/dist/elements/avatar-group.js +110 -0
- package/dist/elements/avatar.d.ts +4 -0
- package/dist/elements/avatar.js +251 -0
- package/dist/elements/badge.d.ts +8 -0
- package/dist/elements/badge.js +182 -0
- package/dist/elements/breadcrumb.d.ts +4 -0
- package/dist/elements/breadcrumb.js +233 -0
- package/dist/elements/button-group.d.ts +1 -0
- package/dist/elements/button-group.js +20 -0
- package/dist/elements/button-variants.d.ts +6 -0
- package/dist/elements/button-variants.js +339 -0
- package/dist/elements/button.d.ts +5 -0
- package/dist/elements/button.js +233 -0
- package/dist/elements/card-primitives.d.ts +3 -0
- package/dist/elements/card-primitives.js +83 -0
- package/dist/elements/card.d.ts +5 -0
- package/dist/elements/card.js +213 -0
- package/dist/elements/carousel.d.ts +92 -0
- package/dist/elements/carousel.js +1112 -0
- package/dist/elements/color-picker.d.ts +24 -0
- package/dist/elements/color-picker.js +1004 -0
- package/dist/elements/combobox.d.ts +1 -0
- package/dist/elements/combobox.js +1012 -0
- package/dist/elements/data-grid-model.d.ts +117 -0
- package/dist/elements/data-grid-model.js +294 -0
- package/dist/elements/data-grid.d.ts +29 -0
- package/dist/elements/data-grid.js +1567 -0
- package/dist/elements/date-picker.d.ts +1 -0
- package/dist/elements/date-picker.js +486 -0
- package/dist/elements/dialog.d.ts +23 -0
- package/dist/elements/dialog.js +409 -0
- package/dist/elements/divider.d.ts +2 -0
- package/dist/elements/divider.js +66 -0
- package/dist/elements/drawer.d.ts +3 -0
- package/dist/elements/drawer.js +308 -0
- package/dist/elements/field-composition.d.ts +26 -0
- package/dist/elements/field-composition.js +133 -0
- package/dist/elements/field.d.ts +1 -0
- package/dist/elements/field.js +67 -0
- package/dist/elements/file-upload.d.ts +6 -0
- package/dist/elements/file-upload.js +211 -0
- package/dist/elements/floating-position.d.ts +26 -0
- package/dist/elements/floating-position.js +39 -0
- package/dist/elements/image.d.ts +28 -0
- package/dist/elements/image.js +38 -0
- package/dist/elements/info.d.ts +9 -0
- package/dist/elements/info.js +102 -0
- package/dist/elements/input.d.ts +5 -0
- package/dist/elements/input.js +255 -0
- package/dist/elements/label.d.ts +15 -0
- package/dist/elements/label.js +56 -0
- package/dist/elements/link.d.ts +1 -0
- package/dist/elements/link.js +92 -0
- package/dist/elements/list.d.ts +19 -0
- package/dist/elements/list.js +297 -0
- package/dist/elements/menu.d.ts +33 -0
- package/dist/elements/menu.js +1103 -0
- package/dist/elements/message-bar.d.ts +10 -0
- package/dist/elements/message-bar.js +299 -0
- package/dist/elements/nav.d.ts +32 -0
- package/dist/elements/nav.js +791 -0
- package/dist/elements/overflow.d.ts +9 -0
- package/dist/elements/overflow.js +218 -0
- package/dist/elements/persona.d.ts +3 -0
- package/dist/elements/persona.js +124 -0
- package/dist/elements/popover.d.ts +77 -0
- package/dist/elements/popover.js +840 -0
- package/dist/elements/progress-bar.d.ts +4 -0
- package/dist/elements/progress-bar.js +82 -0
- package/dist/elements/radio-group.d.ts +1 -0
- package/dist/elements/radio-group.js +303 -0
- package/dist/elements/rating-display.d.ts +2 -0
- package/dist/elements/rating-display.js +125 -0
- package/dist/elements/rating.d.ts +2 -0
- package/dist/elements/rating.js +206 -0
- package/dist/elements/rich-text-editor.d.ts +18 -0
- package/dist/elements/rich-text-editor.js +508 -0
- package/dist/elements/search.d.ts +3 -0
- package/dist/elements/search.js +207 -0
- package/dist/elements/select.d.ts +6 -0
- package/dist/elements/select.js +291 -0
- package/dist/elements/shared.d.ts +19 -0
- package/dist/elements/shared.js +396 -0
- package/dist/elements/skeleton.d.ts +6 -0
- package/dist/elements/skeleton.js +91 -0
- package/dist/elements/slider.d.ts +1 -0
- package/dist/elements/slider.js +248 -0
- package/dist/elements/spin-button.d.ts +7 -0
- package/dist/elements/spin-button.js +499 -0
- package/dist/elements/spinner.d.ts +4 -0
- package/dist/elements/spinner.js +124 -0
- package/dist/elements/swatch-picker.d.ts +5 -0
- package/dist/elements/swatch-picker.js +363 -0
- package/dist/elements/table.d.ts +21 -0
- package/dist/elements/table.js +261 -0
- package/dist/elements/tabs.d.ts +1 -0
- package/dist/elements/tabs.js +276 -0
- package/dist/elements/tag-picker.d.ts +29 -0
- package/dist/elements/tag-picker.js +697 -0
- package/dist/elements/tags.d.ts +21 -0
- package/dist/elements/tags.js +421 -0
- package/dist/elements/teaching-popover.d.ts +32 -0
- package/dist/elements/teaching-popover.js +412 -0
- package/dist/elements/text.d.ts +5 -0
- package/dist/elements/text.js +69 -0
- package/dist/elements/textarea.d.ts +1 -0
- package/dist/elements/textarea.js +182 -0
- package/dist/elements/toast.d.ts +25 -0
- package/dist/elements/toast.js +337 -0
- package/dist/elements/toaster.d.ts +6 -0
- package/dist/elements/toaster.js +356 -0
- package/dist/elements/toggle.d.ts +2 -0
- package/dist/elements/toggle.js +246 -0
- package/dist/elements/toolbar.d.ts +12 -0
- package/dist/elements/toolbar.js +381 -0
- package/dist/elements/tooltip.d.ts +76 -0
- package/dist/elements/tooltip.js +510 -0
- package/dist/elements/tree-model.d.ts +19 -0
- package/dist/elements/tree-model.js +34 -0
- package/dist/elements/tree.d.ts +5 -0
- package/dist/elements/tree.js +273 -0
- package/dist/elements/view-card.d.ts +2 -0
- package/dist/elements/view-card.js +78 -0
- package/dist/elements/virtual-list.d.ts +41 -0
- package/dist/elements/virtual-list.js +369 -0
- package/dist/elements.d.ts +72 -0
- package/dist/elements.js +207 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.js +66 -0
- package/dist/react-theme-styles.d.ts +2 -0
- package/dist/react-theme-styles.js +75 -0
- package/dist/react.d.ts +4540 -0
- package/dist/react.js +6799 -0
- package/dist/theme.d.ts +169 -0
- package/dist/theme.js +167 -0
- package/dist/virtualizer-hooks.d.ts +80 -0
- package/dist/virtualizer-hooks.js +253 -0
- package/package.json +87 -4
- package/scripts/generate-token-index.mjs +43 -0
- package/scripts/sync-component-catalog-signatures.mjs +55 -0
- package/scripts/validate-component-catalog.mjs +44 -0
- package/skills/sharepoint-ui/SKILL.md +12 -0
- package/skills/sharepoint-ui/references/choosing-components.md +20 -0
- package/skills/sharepoint-ui/references/sharepoint-patterns.md +17 -0
- package/tokens.json +3777 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { base, syncHostAccessibleName } from "./shared.js";
|
|
2
|
+
/** File selection surface. Uploading and SharePoint persistence remain the consumer's responsibility. */
|
|
3
|
+
export function defineFileUpload() {
|
|
4
|
+
if (customElements.get("spui-file-upload"))
|
|
5
|
+
return;
|
|
6
|
+
class SpuiFileUpload extends HTMLElement {
|
|
7
|
+
static formAssociated = true;
|
|
8
|
+
static observedAttributes = ["accept", "multiple", "disabled", "required", "name", "max-files", "max-file-size", "label", "aria-label", "aria-labelledby", "aria-description", "aria-describedby", "aria-invalid", "browse-label", "remove-label", "drop-label", "selected-label", "invalid-message", "required-message"];
|
|
9
|
+
internals;
|
|
10
|
+
input;
|
|
11
|
+
list;
|
|
12
|
+
status;
|
|
13
|
+
selected = [];
|
|
14
|
+
formDisabled = false;
|
|
15
|
+
get form() { return this.internals?.form ?? null; }
|
|
16
|
+
get files() { return this.selected; }
|
|
17
|
+
constructor() { super(); try {
|
|
18
|
+
this.internals = this.attachInternals();
|
|
19
|
+
}
|
|
20
|
+
catch { /* Native picker continues to work where form-associated elements are unavailable. */ } }
|
|
21
|
+
connectedCallback() { if (!this.shadowRoot)
|
|
22
|
+
this.render(); this.sync(); }
|
|
23
|
+
attributeChangedCallback() { if (this.isConnected && this.input)
|
|
24
|
+
this.sync(); }
|
|
25
|
+
formDisabledCallback(disabled) { this.formDisabled = disabled; if (this.input)
|
|
26
|
+
this.sync(); }
|
|
27
|
+
formResetCallback() { if (this.status)
|
|
28
|
+
this.status.textContent = ""; this.setFiles([], false); }
|
|
29
|
+
formStateRestoreCallback(state) { if (state instanceof File)
|
|
30
|
+
this.setFiles([state], false);
|
|
31
|
+
else if (state instanceof FormData)
|
|
32
|
+
this.setFiles(state.getAll(this.getAttribute("name") || "").filter((value) => value instanceof File), false); }
|
|
33
|
+
render() {
|
|
34
|
+
const shadow = this.attachShadow({ mode: "open" });
|
|
35
|
+
const style = document.createElement("style");
|
|
36
|
+
style.textContent = `${base}:host{display:block;min-width:0;color:var(--spui-ui-text,var(--spui-semantic-body-text,#242424));font-family:var(--spui-font-family,inherit)}.label{display:block;margin-block-end:6px;font-size:14px;font-weight:600}.drop{box-sizing:border-box;display:grid;place-items:center;gap:6px;min-height:104px;padding:16px;border:1px dashed var(--spui-ui-stroke,var(--spui-semantic-input-border,#8a8886));border-radius:var(--spui-control-radius,6px);background:var(--spui-ui-surface,var(--spui-semantic-input-background,#fff));text-align:center;transition:border-color var(--spui-motion-duration-fast,120ms) var(--spui-motion-easing-standard,cubic-bezier(.2,0,0,1)),background-color var(--spui-motion-duration-fast,120ms) var(--spui-motion-easing-standard,cubic-bezier(.2,0,0,1))}.drop[data-dragging=true]{border-color:var(--spui-focus-ring,var(--spui-ui-accent,#0078d4));background:var(--spui-ui-surface-hover,var(--spui-ui-canvas,#f5f5f5))}.hint{color:var(--spui-ui-muted,var(--spui-semantic-body-subtext,#616161));font-size:12px}.browse{min-height:32px;padding:4px 12px;border:1px solid var(--spui-control-border,var(--spui-ui-stroke,#8a8886));border-radius:var(--spui-control-radius,4px);background:var(--spui-ui-surface,#fff);color:inherit;font:inherit;cursor:pointer}.browse:hover:not(:disabled){background:var(--spui-ui-surface-hover,var(--spui-ui-canvas,#f5f5f5))}.browse:focus-visible,.remove:focus-visible{outline:2px solid var(--spui-focus-ring,var(--spui-ui-accent,#0078d4));outline-offset:2px}.browse:disabled,.remove:disabled{opacity:.55;cursor:not-allowed}.input{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.files{display:grid;gap:4px;margin:8px 0 0;padding:0;list-style:none}.files:empty{display:none}.file{display:flex;align-items:center;gap:8px;min-width:0;padding:6px 8px;border:1px solid var(--spui-ui-stroke,#d1d1d1);border-radius:var(--spui-control-radius,4px)}.filename{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.size{flex:none;color:var(--spui-ui-muted,#616161);font-size:12px;font-variant-numeric:tabular-nums}.remove{min-width:32px;min-height:32px;border:0;border-radius:var(--spui-control-radius,4px);background:transparent;color:inherit;font:inherit;cursor:pointer}.status{margin-block-start:6px;color:var(--spui-semantic-error-text,var(--spui-ui-danger,#b10e1c));font-size:12px}.status:empty{display:none}:host([data-spui-appearance=glass]) .drop{backdrop-filter:var(--spui-card-backdrop,blur(12px) saturate(1.2))}@media(prefers-reduced-motion:reduce){.drop{transition:none}}@media(forced-colors:active){.drop,.file{border-color:CanvasText;background:Canvas;color:CanvasText}.drop[data-dragging=true]{outline:2px solid Highlight}.browse{border-color:ButtonText;background:ButtonFace;color:ButtonText}}`;
|
|
37
|
+
const label = document.createElement("label");
|
|
38
|
+
label.className = "label";
|
|
39
|
+
label.part.add("label");
|
|
40
|
+
const drop = document.createElement("div");
|
|
41
|
+
drop.className = "drop";
|
|
42
|
+
drop.part.add("dropzone");
|
|
43
|
+
drop.tabIndex = -1;
|
|
44
|
+
const hint = document.createElement("span");
|
|
45
|
+
hint.className = "hint";
|
|
46
|
+
const browse = document.createElement("button");
|
|
47
|
+
browse.type = "button";
|
|
48
|
+
browse.className = "browse";
|
|
49
|
+
browse.part.add("browse-button");
|
|
50
|
+
this.input = document.createElement("input");
|
|
51
|
+
this.input.type = "file";
|
|
52
|
+
this.input.className = "input";
|
|
53
|
+
this.input.part.add("input");
|
|
54
|
+
browse.addEventListener("click", () => this.input.click());
|
|
55
|
+
this.input.addEventListener("change", () => { this.addFiles(Array.from(this.input.files ?? [])); this.input.value = ""; });
|
|
56
|
+
drop.addEventListener("dragover", event => { event.preventDefault(); if (!this.isDisabled())
|
|
57
|
+
drop.dataset.dragging = "true"; });
|
|
58
|
+
drop.addEventListener("dragleave", event => { if (!drop.contains(event.relatedTarget))
|
|
59
|
+
delete drop.dataset.dragging; });
|
|
60
|
+
drop.addEventListener("drop", event => { event.preventDefault(); delete drop.dataset.dragging; if (!this.isDisabled())
|
|
61
|
+
this.addFiles(Array.from(event.dataTransfer?.files ?? [])); });
|
|
62
|
+
drop.addEventListener("keydown", event => { if (event.key === "Enter" || event.key === " ") {
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
this.input.click();
|
|
65
|
+
} });
|
|
66
|
+
this.input.addEventListener("focus", () => browse.focus());
|
|
67
|
+
this.list = document.createElement("ul");
|
|
68
|
+
this.list.className = "files";
|
|
69
|
+
this.list.part.add("file-list");
|
|
70
|
+
this.list.setAttribute("aria-label", "Selected files");
|
|
71
|
+
this.status = document.createElement("div");
|
|
72
|
+
this.status.className = "status";
|
|
73
|
+
this.status.part.add("status");
|
|
74
|
+
this.status.setAttribute("role", "status");
|
|
75
|
+
this.status.setAttribute("aria-live", "polite");
|
|
76
|
+
drop.append(hint, browse, this.input);
|
|
77
|
+
shadow.append(style, label, drop, this.list, this.status);
|
|
78
|
+
}
|
|
79
|
+
sync() {
|
|
80
|
+
const disabled = this.isDisabled();
|
|
81
|
+
this.input.accept = this.getAttribute("accept") || "";
|
|
82
|
+
this.input.multiple = this.hasAttribute("multiple");
|
|
83
|
+
this.input.disabled = disabled;
|
|
84
|
+
this.input.required = this.hasAttribute("required") && this.selected.length === 0;
|
|
85
|
+
const label = this.getAttribute("aria-label") || this.getAttribute("label") || "Add files";
|
|
86
|
+
const browse = this.getAttribute("browse-label") || "Browse files";
|
|
87
|
+
if (!this.hasAttribute("role"))
|
|
88
|
+
this.setAttribute("role", "group");
|
|
89
|
+
const labelElement = this.shadowRoot?.querySelector(".label");
|
|
90
|
+
if (labelElement) {
|
|
91
|
+
labelElement.textContent = this.getAttribute("label") || "";
|
|
92
|
+
labelElement.hidden = !this.hasAttribute("label");
|
|
93
|
+
}
|
|
94
|
+
this.input.setAttribute("aria-label", label);
|
|
95
|
+
syncHostAccessibleName(this, this.input);
|
|
96
|
+
const invalid = this.getAttribute("aria-invalid");
|
|
97
|
+
if (invalid === null)
|
|
98
|
+
this.input.removeAttribute("aria-invalid");
|
|
99
|
+
else
|
|
100
|
+
this.input.setAttribute("aria-invalid", invalid);
|
|
101
|
+
const hint = this.shadowRoot?.querySelector(".hint");
|
|
102
|
+
if (hint)
|
|
103
|
+
hint.textContent = this.getAttribute("drop-label") || "Drag files here or browse to select";
|
|
104
|
+
const button = this.shadowRoot?.querySelector(".browse");
|
|
105
|
+
if (button) {
|
|
106
|
+
button.textContent = browse;
|
|
107
|
+
button.disabled = disabled;
|
|
108
|
+
button.setAttribute("aria-label", browse);
|
|
109
|
+
}
|
|
110
|
+
this.list?.setAttribute("aria-label", `${label}: ${this.getAttribute("selected-label") || "selected files"}`);
|
|
111
|
+
this.renderFiles();
|
|
112
|
+
this.syncForm();
|
|
113
|
+
}
|
|
114
|
+
addFiles(files) {
|
|
115
|
+
if (!files.length || this.isDisabled())
|
|
116
|
+
return;
|
|
117
|
+
const max = Number(this.getAttribute("max-files"));
|
|
118
|
+
const limit = Number(this.getAttribute("max-file-size"));
|
|
119
|
+
const next = this.hasAttribute("multiple") ? [...this.selected] : [];
|
|
120
|
+
const errors = [];
|
|
121
|
+
for (const file of files) {
|
|
122
|
+
if (!this.accepts(file)) {
|
|
123
|
+
errors.push({ code: "file-type-not-accepted", file });
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (Number.isFinite(limit) && limit > 0 && file.size > limit) {
|
|
127
|
+
errors.push({ code: "file-too-large", file });
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (Number.isFinite(max) && max > 0 && next.length >= max) {
|
|
131
|
+
errors.push({ code: "too-many-files", file });
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (!next.some(existing => existing.name === file.name && existing.size === file.size && existing.lastModified === file.lastModified))
|
|
135
|
+
next.push(file);
|
|
136
|
+
}
|
|
137
|
+
this.status.textContent = errors.length ? (this.getAttribute("invalid-message") || this.getAttribute(errors[0].code === "file-too-large" ? "file-too-large-message" : errors[0].code === "too-many-files" ? "max-files-message" : "file-type-message") || this.describeError(errors[0])) : "";
|
|
138
|
+
this.setFiles(next, true);
|
|
139
|
+
if (errors.length)
|
|
140
|
+
this.dispatchEvent(new CustomEvent("spui-file-error", { detail: { errors }, bubbles: true, composed: true }));
|
|
141
|
+
}
|
|
142
|
+
describeError(error) {
|
|
143
|
+
if (error.code === "file-too-large")
|
|
144
|
+
return `${error.file?.name || "File"} exceeds the allowed size.`;
|
|
145
|
+
if (error.code === "too-many-files")
|
|
146
|
+
return "The maximum number of files has been reached.";
|
|
147
|
+
return "The selected file type is not accepted.";
|
|
148
|
+
}
|
|
149
|
+
accepts(file) {
|
|
150
|
+
const accepted = (this.getAttribute("accept") || "").split(",").map(value => value.trim().toLowerCase()).filter(Boolean);
|
|
151
|
+
if (!accepted.length)
|
|
152
|
+
return true;
|
|
153
|
+
const name = file.name.toLowerCase();
|
|
154
|
+
const type = file.type.toLowerCase();
|
|
155
|
+
return accepted.some(value => value.startsWith(".") ? name.endsWith(value) : value.endsWith("/*") ? type.startsWith(value.slice(0, -1)) : type === value);
|
|
156
|
+
}
|
|
157
|
+
setFiles(files, emit) {
|
|
158
|
+
this.selected = files;
|
|
159
|
+
this.renderFiles();
|
|
160
|
+
this.syncForm();
|
|
161
|
+
if (emit)
|
|
162
|
+
this.dispatchEvent(new CustomEvent("spui-file-change", { detail: { files: [...files] }, bubbles: true, composed: true }));
|
|
163
|
+
}
|
|
164
|
+
renderFiles() {
|
|
165
|
+
if (!this.list)
|
|
166
|
+
return;
|
|
167
|
+
this.list.replaceChildren(...this.selected.map((file, index) => {
|
|
168
|
+
const item = document.createElement("li");
|
|
169
|
+
item.className = "file";
|
|
170
|
+
item.part.add("file");
|
|
171
|
+
const name = document.createElement("span");
|
|
172
|
+
name.className = "filename";
|
|
173
|
+
name.textContent = file.name;
|
|
174
|
+
const size = document.createElement("span");
|
|
175
|
+
size.className = "size";
|
|
176
|
+
size.textContent = this.formatSize(file.size);
|
|
177
|
+
const remove = document.createElement("button");
|
|
178
|
+
remove.type = "button";
|
|
179
|
+
remove.className = "remove";
|
|
180
|
+
remove.textContent = "×";
|
|
181
|
+
remove.disabled = this.isDisabled();
|
|
182
|
+
remove.setAttribute("aria-label", `${this.getAttribute("remove-label") || "Remove"} ${file.name}`);
|
|
183
|
+
remove.addEventListener("click", () => {
|
|
184
|
+
this.status.textContent = "";
|
|
185
|
+
const next = this.selected.filter((_, i) => i !== index);
|
|
186
|
+
this.setFiles(next, true);
|
|
187
|
+
const nextRemove = this.list.querySelectorAll(".remove")[Math.min(index, next.length - 1)];
|
|
188
|
+
(nextRemove ?? this.shadowRoot?.querySelector(".browse"))?.focus();
|
|
189
|
+
});
|
|
190
|
+
item.append(name, size, remove);
|
|
191
|
+
return item;
|
|
192
|
+
}));
|
|
193
|
+
}
|
|
194
|
+
formatSize(size) { return size < 1024 ? `${size} B` : size < 1024 * 1024 ? `${(size / 1024).toFixed(1)} KB` : `${(size / (1024 * 1024)).toFixed(1)} MB`; }
|
|
195
|
+
syncForm() {
|
|
196
|
+
if (!this.internals)
|
|
197
|
+
return;
|
|
198
|
+
const name = this.getAttribute("name") || "";
|
|
199
|
+
const data = new FormData();
|
|
200
|
+
if (name)
|
|
201
|
+
this.selected.forEach(file => data.append(name, file, file.name));
|
|
202
|
+
this.internals.setFormValue(this.selected.length && name ? data : null, JSON.stringify(this.selected.map(file => ({ name: file.name, size: file.size, lastModified: file.lastModified }))));
|
|
203
|
+
if (this.hasAttribute("required") && !this.selected.length)
|
|
204
|
+
this.internals.setValidity({ valueMissing: true }, this.getAttribute("required-message") || "Select at least one file.", this.shadowRoot?.querySelector(".browse") ?? undefined);
|
|
205
|
+
else
|
|
206
|
+
this.internals.setValidity({});
|
|
207
|
+
}
|
|
208
|
+
isDisabled() { return this.formDisabled || this.hasAttribute("disabled"); }
|
|
209
|
+
}
|
|
210
|
+
customElements.define("spui-file-upload", SpuiFileUpload);
|
|
211
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type FloatingPlacement = "top" | "bottom" | "start" | "end" | "left" | "right";
|
|
2
|
+
export type FloatingPosition = {
|
|
3
|
+
top: number;
|
|
4
|
+
left: number;
|
|
5
|
+
placement: Exclude<FloatingPlacement, "start" | "end">;
|
|
6
|
+
align: FloatingAlign;
|
|
7
|
+
};
|
|
8
|
+
export type FloatingAlign = "start" | "center" | "end" | "top" | "bottom";
|
|
9
|
+
export type FloatingModifiers = {
|
|
10
|
+
crossAxisOffset?: number;
|
|
11
|
+
coverTarget?: boolean;
|
|
12
|
+
overflowBoundaryPadding?: {
|
|
13
|
+
top?: number;
|
|
14
|
+
right?: number;
|
|
15
|
+
bottom?: number;
|
|
16
|
+
left?: number;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
export type FloatingAnchorRect = Pick<DOMRectReadOnly, "left" | "top" | "right" | "bottom" | "width" | "height">;
|
|
20
|
+
export declare function getFloatingPosition(anchor: FloatingAnchorRect, surface: Pick<DOMRectReadOnly, "width" | "height">, placement: FloatingPlacement, direction: "ltr" | "rtl", gap?: number, viewport?: {
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
}, align?: FloatingAlign, fallbackPlacements?: Array<{
|
|
24
|
+
placement: FloatingPlacement;
|
|
25
|
+
align?: FloatingAlign;
|
|
26
|
+
}>, pinned?: boolean, flipBoundary?: Pick<DOMRectReadOnly, "left" | "top" | "right" | "bottom">, overflowBoundary?: Pick<DOMRectReadOnly, "left" | "top" | "right" | "bottom">, modifiers?: FloatingModifiers): FloatingPosition;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export function getFloatingPosition(anchor, surface, placement, direction, gap = 8, viewport = { width: innerWidth, height: innerHeight }, align = "center", fallbackPlacements = [], pinned = false, flipBoundary, overflowBoundary, modifiers = {}) {
|
|
2
|
+
const viewportBoundary = { left: 0, top: 0, right: viewport.width, bottom: viewport.height };
|
|
3
|
+
const flipBounds = flipBoundary ?? viewportBoundary;
|
|
4
|
+
const overflowBounds = overflowBoundary ?? viewportBoundary;
|
|
5
|
+
const overflowPadding = modifiers.overflowBoundaryPadding ?? {};
|
|
6
|
+
const side = placement === "start" ? direction === "rtl" ? "right" : "left" : placement === "end" ? direction === "rtl" ? "left" : "right" : placement;
|
|
7
|
+
const opposite = { top: "bottom", bottom: "top", left: "right", right: "left" };
|
|
8
|
+
const coordinates = (candidate, candidateAlign = align) => {
|
|
9
|
+
const candidateInlineAlign = candidateAlign === "start" || candidateAlign === "top" ? "start" : candidateAlign === "end" || candidateAlign === "bottom" ? "end" : "center";
|
|
10
|
+
const candidateBlockAlign = candidateAlign === "start" || candidateAlign === "top" ? "start" : candidateAlign === "end" || candidateAlign === "bottom" ? "end" : "center";
|
|
11
|
+
const inlineStartLeft = direction === "rtl" ? anchor.right - surface.width : anchor.left;
|
|
12
|
+
const inlineEndLeft = direction === "rtl" ? anchor.left : anchor.right - surface.width;
|
|
13
|
+
return candidate === "top" || candidate === "bottom"
|
|
14
|
+
? { top: candidate === "top" ? (modifiers.coverTarget ? anchor.bottom : anchor.top) - surface.height - gap : (modifiers.coverTarget ? anchor.top : anchor.bottom) + gap, left: (candidateInlineAlign === "start" ? inlineStartLeft : candidateInlineAlign === "end" ? inlineEndLeft : anchor.left + (anchor.width - surface.width) / 2) + (modifiers.crossAxisOffset ?? 0) }
|
|
15
|
+
: { top: (candidateBlockAlign === "start" ? anchor.top : candidateBlockAlign === "end" ? anchor.bottom - surface.height : anchor.top + (anchor.height - surface.height) / 2) + (modifiers.crossAxisOffset ?? 0), left: candidate === "left" ? (modifiers.coverTarget ? anchor.right : anchor.left) - surface.width - gap : (modifiers.coverTarget ? anchor.left : anchor.right) + gap };
|
|
16
|
+
};
|
|
17
|
+
const fits = (_candidate, point) => point.top >= flipBounds.top + gap && point.left >= flipBounds.left + gap && point.top + surface.height <= flipBounds.bottom - gap && point.left + surface.width <= flipBounds.right - gap;
|
|
18
|
+
const candidates = [
|
|
19
|
+
{ placement: side, align },
|
|
20
|
+
...(pinned ? [] : fallbackPlacements),
|
|
21
|
+
...(!pinned ? [{ placement: opposite[side], align }] : []),
|
|
22
|
+
].map(candidate => ({ placement: candidate.placement === "start" ? direction === "rtl" ? "right" : "left" : candidate.placement === "end" ? direction === "rtl" ? "left" : "right" : candidate.placement, align: candidate.align ?? align }));
|
|
23
|
+
const unique = candidates.filter((candidate, index) => candidates.findIndex(other => other.placement === candidate.placement && other.align === candidate.align) === index);
|
|
24
|
+
let selected = unique.map(candidate => ({ ...candidate, point: coordinates(candidate.placement, candidate.align) })).find(candidate => fits(candidate.placement, candidate.point));
|
|
25
|
+
if (!selected) {
|
|
26
|
+
selected = unique.map(candidate => ({ ...candidate, point: coordinates(candidate.placement, candidate.align) })).sort((a, b) => {
|
|
27
|
+
const overflow = (candidate) => Math.max(0, flipBounds.left + gap - candidate.point.left) + Math.max(0, candidate.point.left + surface.width - (flipBounds.right - gap)) + Math.max(0, flipBounds.top + gap - candidate.point.top) + Math.max(0, candidate.point.top + surface.height - (flipBounds.bottom - gap));
|
|
28
|
+
return overflow(a) - overflow(b);
|
|
29
|
+
})[0];
|
|
30
|
+
}
|
|
31
|
+
const actual = selected.placement;
|
|
32
|
+
const point = selected.point;
|
|
33
|
+
return {
|
|
34
|
+
placement: actual,
|
|
35
|
+
align: selected.align,
|
|
36
|
+
left: Math.max(overflowBounds.left + gap + (overflowPadding.left ?? 0), Math.min(point.left, overflowBounds.right - surface.width - gap - (overflowPadding.right ?? 0))),
|
|
37
|
+
top: Math.max(overflowBounds.top + gap + (overflowPadding.top ?? 0), Math.min(point.top, overflowBounds.bottom - surface.height - gap - (overflowPadding.bottom ?? 0))),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type ImageFit = "none" | "center" | "contain" | "cover" | "default";
|
|
2
|
+
export type ImageShape = "square" | "rounded" | "circular";
|
|
3
|
+
export type ImageOptions = {
|
|
4
|
+
src: string;
|
|
5
|
+
alt: string;
|
|
6
|
+
srcSet?: string;
|
|
7
|
+
sizes?: string;
|
|
8
|
+
width?: number;
|
|
9
|
+
height?: number;
|
|
10
|
+
title?: string;
|
|
11
|
+
className?: string;
|
|
12
|
+
loading?: "eager" | "lazy";
|
|
13
|
+
decoding?: "async" | "sync" | "auto";
|
|
14
|
+
fetchPriority?: "high" | "low" | "auto";
|
|
15
|
+
fit?: ImageFit;
|
|
16
|
+
shape?: ImageShape;
|
|
17
|
+
block?: boolean;
|
|
18
|
+
bordered?: boolean;
|
|
19
|
+
shadow?: boolean;
|
|
20
|
+
};
|
|
21
|
+
type ImageStyleOptions = Pick<ImageOptions, "fit" | "shape" | "block" | "bordered" | "shadow"> & {
|
|
22
|
+
width?: number | string;
|
|
23
|
+
height?: number | string;
|
|
24
|
+
};
|
|
25
|
+
export declare function getImageStyle({ fit, shape, block, bordered, shadow, width, height }: ImageStyleOptions): Record<string, string>;
|
|
26
|
+
/** Create a native image with browser-native responsive loading and meaningful alt text. */
|
|
27
|
+
export declare function createImage(options: ImageOptions): HTMLImageElement;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function getImageStyle({ fit = "default", shape = "square", block, bordered, shadow, width, height }) {
|
|
2
|
+
const fitFill = fit !== "default" && width == null && height == null;
|
|
3
|
+
return {
|
|
4
|
+
display: block ? "block" : "inline-block",
|
|
5
|
+
maxWidth: "100%",
|
|
6
|
+
...(block ? { width: "100%" } : {}),
|
|
7
|
+
...(fitFill ? { width: "100%", height: "100%" } : {}),
|
|
8
|
+
...(fit === "default" ? {} : { objectFit: fit === "center" ? "none" : fit }),
|
|
9
|
+
...(fit === "default" ? {} : { objectPosition: fit === "none" ? "left top" : "center" }),
|
|
10
|
+
borderRadius: shape === "circular" ? "50%" : shape === "rounded" ? "var(--spui-image-radius,var(--spui-control-radius,4px))" : "0",
|
|
11
|
+
...(bordered ? { border: "1px solid var(--spui-image-border,var(--spui-ui-stroke,var(--spui-semantic-body-divider,#d1d1d1)))" } : {}),
|
|
12
|
+
...(shadow ? { boxShadow: "var(--spui-image-shadow,var(--spui-card-shadow,0 1px 3px #0002))" } : {}),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/** Create a native image with browser-native responsive loading and meaningful alt text. */
|
|
16
|
+
export function createImage(options) {
|
|
17
|
+
const image = document.createElement("img");
|
|
18
|
+
image.alt = options.alt;
|
|
19
|
+
if (options.srcSet)
|
|
20
|
+
image.srcset = options.srcSet;
|
|
21
|
+
if (options.sizes)
|
|
22
|
+
image.sizes = options.sizes;
|
|
23
|
+
if (options.width !== undefined)
|
|
24
|
+
image.width = options.width;
|
|
25
|
+
if (options.height !== undefined)
|
|
26
|
+
image.height = options.height;
|
|
27
|
+
if (options.title)
|
|
28
|
+
image.title = options.title;
|
|
29
|
+
if (options.className)
|
|
30
|
+
image.className = options.className;
|
|
31
|
+
image.loading = options.loading ?? "lazy";
|
|
32
|
+
image.decoding = options.decoding ?? "async";
|
|
33
|
+
image.fetchPriority = options.fetchPriority ?? "auto";
|
|
34
|
+
Object.assign(image.style, getImageStyle(options));
|
|
35
|
+
// Configure the candidate set and intrinsic layout before starting any image request.
|
|
36
|
+
image.src = options.src;
|
|
37
|
+
return image;
|
|
38
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type LabelOptions } from "./label.js";
|
|
2
|
+
export type InfoPlacement = "top" | "bottom" | "start" | "end";
|
|
3
|
+
export type InfoLabelOptions = LabelOptions & {
|
|
4
|
+
infoButtonLabel?: string;
|
|
5
|
+
placement?: InfoPlacement;
|
|
6
|
+
};
|
|
7
|
+
export declare function defineInfoButton(): void;
|
|
8
|
+
/** Build a native form label with an adjacent, separately focusable information button. */
|
|
9
|
+
export declare function createInfoLabel(labelContent: string | Node, info: string | Node, options?: InfoLabelOptions): HTMLSpanElement;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { definePopover } from "./popover.js";
|
|
2
|
+
import { createLabel } from "./label.js";
|
|
3
|
+
export function defineInfoButton() {
|
|
4
|
+
if (customElements.get("spui-info-button"))
|
|
5
|
+
return;
|
|
6
|
+
class SpuiInfoButton extends HTMLElement {
|
|
7
|
+
static observedAttributes = ["label", "info", "placement", "open", "disabled", "size", "inline", "popover-placement", "popover-label", "popover-arrow", "popover-trap-focus"];
|
|
8
|
+
popoverElement;
|
|
9
|
+
trigger;
|
|
10
|
+
contentSlot;
|
|
11
|
+
get open() { return this.popoverElement?.open ?? this.hasAttribute("open"); }
|
|
12
|
+
set open(value) { if (this.popoverElement)
|
|
13
|
+
this.popoverElement.open = Boolean(value);
|
|
14
|
+
else
|
|
15
|
+
this.toggleAttribute("open", Boolean(value)); }
|
|
16
|
+
get inline() { return this.getAttribute("inline") !== "false"; }
|
|
17
|
+
set inline(value) { if (value)
|
|
18
|
+
this.removeAttribute("inline");
|
|
19
|
+
else
|
|
20
|
+
this.setAttribute("inline", "false"); }
|
|
21
|
+
connectedCallback() {
|
|
22
|
+
definePopover();
|
|
23
|
+
if (!this.shadowRoot)
|
|
24
|
+
this.render();
|
|
25
|
+
this.sync();
|
|
26
|
+
}
|
|
27
|
+
attributeChangedCallback() { if (this.isConnected)
|
|
28
|
+
this.sync(); }
|
|
29
|
+
render() {
|
|
30
|
+
const shadow = this.attachShadow({ mode: "open" });
|
|
31
|
+
const style = document.createElement("style");
|
|
32
|
+
style.textContent = `:host{display:inline-flex;vertical-align:middle;line-height:1}:host([hidden]){display:none}button{display:inline-grid;place-items:center;box-sizing:border-box;width:var(--spui-info-button-size,20px);height:var(--spui-info-button-size,20px);padding:2px;border:1px solid transparent;border-radius:50%;background:transparent;color:var(--spui-info-button-color,var(--spui-ui-muted,var(--spui-semantic-body-text,#484644)));cursor:pointer;font:inherit}button:hover:not(:disabled){background:var(--spui-info-button-hover,var(--spui-ui-surface-hover,#f3f2f1));color:var(--spui-ui-accent,var(--spui-semantic-link,#0f6cbd))}button:focus-visible{outline:2px solid var(--spui-focus-ring,var(--spui-ui-accent,#005a9e));outline-offset:2px}button:disabled{opacity:.55;cursor:not-allowed}svg{width:calc(var(--spui-info-button-size,20px) * .8);height:calc(var(--spui-info-button-size,20px) * .8);fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round;pointer-events:none}:host([size=small]){--spui-info-button-size:16px}:host([size=large]){--spui-info-button-size:24px}:host([data-spui-appearance=glass]) button{backdrop-filter:blur(8px)}@media(forced-colors:active){button{border-color:ButtonText;color:ButtonText}button:focus-visible{outline-color:Highlight}}`;
|
|
33
|
+
const popover = document.createElement("spui-popover");
|
|
34
|
+
this.popoverElement = popover;
|
|
35
|
+
const trigger = document.createElement("button");
|
|
36
|
+
this.trigger = trigger;
|
|
37
|
+
trigger.type = "button";
|
|
38
|
+
trigger.slot = "trigger";
|
|
39
|
+
trigger.part.add("control");
|
|
40
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
41
|
+
svg.setAttribute("viewBox", "0 0 20 20");
|
|
42
|
+
svg.setAttribute("aria-hidden", "true");
|
|
43
|
+
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
44
|
+
path.setAttribute("d", "M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm0-9v5m0-8h.01");
|
|
45
|
+
svg.append(path);
|
|
46
|
+
trigger.append(svg);
|
|
47
|
+
const content = document.createElement("div");
|
|
48
|
+
content.slot = "content";
|
|
49
|
+
content.part.add("content");
|
|
50
|
+
const slot = document.createElement("slot");
|
|
51
|
+
this.contentSlot = slot;
|
|
52
|
+
slot.addEventListener("slotchange", () => this.syncContent());
|
|
53
|
+
content.append(slot);
|
|
54
|
+
popover.append(trigger, content);
|
|
55
|
+
popover.addEventListener("spui-open-change", event => {
|
|
56
|
+
if (event.detail.open)
|
|
57
|
+
requestAnimationFrame(() => {
|
|
58
|
+
const surface = popover.shadowRoot?.querySelector(".surface");
|
|
59
|
+
surface?.setAttribute("tabindex", "-1");
|
|
60
|
+
surface?.focus();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
shadow.append(style, popover);
|
|
64
|
+
}
|
|
65
|
+
sync() {
|
|
66
|
+
if (!this.popoverElement || !this.trigger)
|
|
67
|
+
return;
|
|
68
|
+
const label = this.getAttribute("label") || "More information";
|
|
69
|
+
this.trigger.setAttribute("aria-label", label);
|
|
70
|
+
this.trigger.disabled = this.hasAttribute("disabled");
|
|
71
|
+
this.popoverElement.setAttribute("placement", this.getAttribute("popover-placement") || this.getAttribute("placement") || "top");
|
|
72
|
+
this.popoverElement.setAttribute("label", this.getAttribute("popover-label") || label);
|
|
73
|
+
this.popoverElement.inline = this.inline;
|
|
74
|
+
this.popoverElement.toggleAttribute("arrow", this.hasAttribute("popover-arrow"));
|
|
75
|
+
this.popoverElement.toggleAttribute("trap-focus", this.hasAttribute("popover-trap-focus"));
|
|
76
|
+
if (this.popoverElement.open !== this.hasAttribute("open"))
|
|
77
|
+
this.popoverElement.open = this.hasAttribute("open");
|
|
78
|
+
this.syncContent();
|
|
79
|
+
}
|
|
80
|
+
syncContent() {
|
|
81
|
+
if (!this.contentSlot)
|
|
82
|
+
return;
|
|
83
|
+
this.contentSlot.textContent = !this.childNodes.length && this.hasAttribute("info") ? this.getAttribute("info") ?? "" : "";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
customElements.define("spui-info-button", SpuiInfoButton);
|
|
87
|
+
}
|
|
88
|
+
/** Build a native form label with an adjacent, separately focusable information button. */
|
|
89
|
+
export function createInfoLabel(labelContent, info, options = {}) {
|
|
90
|
+
defineInfoButton();
|
|
91
|
+
const wrapper = document.createElement("span");
|
|
92
|
+
Object.assign(wrapper.style, { display: "inline-flex", alignItems: "baseline", gap: "0.25em" });
|
|
93
|
+
const label = createLabel(labelContent, options);
|
|
94
|
+
wrapper.append(label);
|
|
95
|
+
const button = document.createElement("spui-info-button");
|
|
96
|
+
button.setAttribute("label", options.infoButtonLabel ?? "More information");
|
|
97
|
+
if (options.placement)
|
|
98
|
+
button.setAttribute("placement", options.placement);
|
|
99
|
+
button.append(typeof info === "string" ? document.createTextNode(info) : info);
|
|
100
|
+
wrapper.append(button);
|
|
101
|
+
return wrapper;
|
|
102
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type InputSize = "small" | "medium" | "large";
|
|
2
|
+
export type InputAppearance = "outline" | "underline" | "filled-darker" | "filled-lighter" | "filled-darker-shadow" | "filled-lighter-shadow";
|
|
3
|
+
export type InputType = "button" | "checkbox" | "color" | "date" | "datetime-local" | "email" | "file" | "hidden" | "image" | "month" | "number" | "password" | "radio" | "range" | "reset" | "search" | "submit" | "tel" | "text" | "time" | "url" | "week" | (string & {});
|
|
4
|
+
/** Bare, form-associated input; pair it with Label or Field for an accessible name. */
|
|
5
|
+
export declare function defineInput(): void;
|