@db-vincent/components 0.1.0 → 0.1.1
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/package.json +3 -3
- package/src/astro/Avatar.astro +88 -0
- package/src/astro/AvatarGroup.astro +63 -0
- package/src/astro/Chip.astro +22 -3
- package/src/astro/Drawer.astro +30 -0
- package/src/astro/Grid.astro +7 -1
- package/src/astro/GridItem.astro +3 -1
- package/src/astro/Menu.astro +151 -0
- package/src/astro/MenuItem.astro +57 -0
- package/src/astro/Modal.astro +10 -3
- package/src/astro/Pagination.astro +82 -32
- package/src/astro/Popover.astro +51 -0
- package/src/astro/Progress.astro +71 -0
- package/src/astro/Select.astro +211 -194
- package/src/astro/Skeleton.astro +4 -2
- package/src/astro/Spinner.astro +1 -0
- package/src/astro/Stepper.astro +82 -0
- package/src/astro/Switch.astro +78 -0
- package/src/astro/Table.astro +90 -76
- package/src/astro/Tabs.astro +83 -0
- package/src/astro/TextField.astro +100 -82
- package/src/astro/Toast.astro +1 -1
- package/src/astro/Tooltip.astro +54 -0
- package/src/data/icons.ts +16 -0
- package/src/scripts/avatar.ts +33 -0
- package/src/scripts/menu.ts +218 -0
- package/src/scripts/tabs.ts +127 -0
- package/src/scripts/toast.ts +45 -11
- package/src/scripts/tooltip.ts +127 -0
- package/src/styles/accordion.css +2 -0
- package/src/styles/alert.css +2 -0
- package/src/styles/avatar.css +131 -0
- package/src/styles/badge.css +2 -0
- package/src/styles/base.css +2 -0
- package/src/styles/breadcrumbs.css +2 -0
- package/src/styles/button.css +13 -2
- package/src/styles/card.css +8 -4
- package/src/styles/chip.css +2 -0
- package/src/styles/choice.css +8 -2
- package/src/styles/code.css +2 -0
- package/src/styles/empty.css +2 -0
- package/src/styles/example.css +2 -0
- package/src/styles/grid.css +2 -0
- package/src/styles/input.css +11 -1
- package/src/styles/menu.css +151 -0
- package/src/styles/modal.css +169 -27
- package/src/styles/motion.css +21 -3
- package/src/styles/nav.css +2 -0
- package/src/styles/navbar.css +2 -0
- package/src/styles/page-header.css +3 -0
- package/src/styles/pagination.css +4 -0
- package/src/styles/popover.css +134 -0
- package/src/styles/progress.css +95 -0
- package/src/styles/section.css +2 -0
- package/src/styles/select.css +12 -1
- package/src/styles/skeleton.css +2 -0
- package/src/styles/spinner.css +2 -0
- package/src/styles/sr-only.css +14 -0
- package/src/styles/stepper.css +169 -0
- package/src/styles/switch.css +143 -0
- package/src/styles/table.css +2 -0
- package/src/styles/tabs.css +120 -0
- package/src/styles/theme-toggle.css +2 -0
- package/src/styles/toast.css +41 -23
- package/src/styles/tooltip.css +63 -0
- package/src/styles/topbar.css +2 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Icon from "./Icon.astro";
|
|
3
|
+
import "../styles/switch.css";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* On/off setting that applies immediately. Native checkbox with
|
|
7
|
+
* role="switch"; the label names it, hint and error describe it.
|
|
8
|
+
*/
|
|
9
|
+
interface Props {
|
|
10
|
+
label: string;
|
|
11
|
+
hint?: string;
|
|
12
|
+
error?: string;
|
|
13
|
+
id?: string;
|
|
14
|
+
name?: string;
|
|
15
|
+
value?: string;
|
|
16
|
+
checked?: boolean;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
required?: boolean;
|
|
19
|
+
size?: "sm" | "md";
|
|
20
|
+
class?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const {
|
|
24
|
+
label,
|
|
25
|
+
hint,
|
|
26
|
+
error,
|
|
27
|
+
id,
|
|
28
|
+
name,
|
|
29
|
+
value,
|
|
30
|
+
checked = false,
|
|
31
|
+
disabled = false,
|
|
32
|
+
required = false,
|
|
33
|
+
size = "md",
|
|
34
|
+
class: className,
|
|
35
|
+
} = Astro.props;
|
|
36
|
+
|
|
37
|
+
const inputId = id ?? `switch-${Math.random().toString(36).slice(2, 9)}`;
|
|
38
|
+
const labelId = `${inputId}-label`;
|
|
39
|
+
const hintId = hint ? `${inputId}-hint` : undefined;
|
|
40
|
+
const errorId = error ? `${inputId}-error` : undefined;
|
|
41
|
+
const describedby = [hintId, errorId].filter(Boolean).join(" ") || undefined;
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
<label
|
|
45
|
+
class:list={["ds-switch", size !== "md" && `ds-switch--${size}`, className]}
|
|
46
|
+
>
|
|
47
|
+
<input
|
|
48
|
+
class="ds-switch__input"
|
|
49
|
+
id={inputId}
|
|
50
|
+
type="checkbox"
|
|
51
|
+
role="switch"
|
|
52
|
+
name={name}
|
|
53
|
+
value={value}
|
|
54
|
+
checked={checked}
|
|
55
|
+
disabled={disabled}
|
|
56
|
+
required={required}
|
|
57
|
+
aria-invalid={error ? "true" : undefined}
|
|
58
|
+
aria-labelledby={describedby ? labelId : undefined}
|
|
59
|
+
aria-describedby={describedby}
|
|
60
|
+
/>
|
|
61
|
+
<span class="ds-switch__track" aria-hidden="true">
|
|
62
|
+
<span class="ds-switch__thumb"></span>
|
|
63
|
+
</span>
|
|
64
|
+
<span class="ds-switch__label">
|
|
65
|
+
<span id={labelId}>{label}</span>
|
|
66
|
+
{hint && (
|
|
67
|
+
<span class="ds-switch__hint" id={hintId}>
|
|
68
|
+
{hint}
|
|
69
|
+
</span>
|
|
70
|
+
)}
|
|
71
|
+
{error && (
|
|
72
|
+
<span class="ds-switch__error" id={errorId}>
|
|
73
|
+
<Icon name="alert-circle" size={14} />
|
|
74
|
+
{error}
|
|
75
|
+
</span>
|
|
76
|
+
)}
|
|
77
|
+
</span>
|
|
78
|
+
</label>
|
package/src/astro/Table.astro
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import Icon from "./Icon.astro";
|
|
3
|
+
import "../styles/sr-only.css";
|
|
3
4
|
import "../styles/input.css";
|
|
4
5
|
import "../styles/table.css";
|
|
5
6
|
|
|
@@ -133,7 +134,7 @@ const filterId = `table-filter-${Math.random().toString(36).slice(2, 9)}`;
|
|
|
133
134
|
{cell.swatch && (
|
|
134
135
|
<span
|
|
135
136
|
class="ds-table__swatch"
|
|
136
|
-
style={`background:${cell.swatch}`}
|
|
137
|
+
style={`background:${/^(#[0-9a-f]{3,8}|var\(--[a-z0-9-]+\)|[a-z]+)$/i.test(cell.swatch) ? cell.swatch : "transparent"}`}
|
|
137
138
|
aria-hidden="true"
|
|
138
139
|
></span>
|
|
139
140
|
)}
|
|
@@ -166,89 +167,102 @@ const filterId = `table-filter-${Math.random().toString(36).slice(2, 9)}`;
|
|
|
166
167
|
</div>
|
|
167
168
|
|
|
168
169
|
<script>
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const textFor = (row: HTMLTableRowElement, index: number) =>
|
|
186
|
-
(row.cells[index]?.textContent || "").trim();
|
|
187
|
-
|
|
188
|
-
const numberFor = (value: string) => {
|
|
189
|
-
if (!/[0-9]/.test(value)) return null;
|
|
190
|
-
const parsed = Number(value.replace(/[^0-9.-]/g, ""));
|
|
191
|
-
return Number.isFinite(parsed) ? parsed : null;
|
|
192
|
-
};
|
|
170
|
+
const bind = () => {
|
|
171
|
+
document
|
|
172
|
+
.querySelectorAll<HTMLElement>("[data-ds-table]")
|
|
173
|
+
.forEach((root) => {
|
|
174
|
+
if (root.dataset.dsBound) return;
|
|
175
|
+
root.dataset.dsBound = "true";
|
|
176
|
+
const tbody = root.querySelector<HTMLElement>("tbody");
|
|
177
|
+
const filter = root.querySelector<HTMLInputElement>(
|
|
178
|
+
"[data-table-filter]",
|
|
179
|
+
);
|
|
180
|
+
const count = root.querySelector<HTMLElement>("[data-table-count]");
|
|
181
|
+
const empty = root.querySelector<HTMLElement>("[data-table-empty]");
|
|
182
|
+
const sortButtons = Array.from(
|
|
183
|
+
root.querySelectorAll<HTMLButtonElement>("[data-sort-index]"),
|
|
184
|
+
);
|
|
185
|
+
if (!tbody) return;
|
|
193
186
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
};
|
|
187
|
+
const rows = Array.from(
|
|
188
|
+
tbody.querySelectorAll<HTMLTableRowElement>("tr[data-row]"),
|
|
189
|
+
);
|
|
190
|
+
let sortIndex = -1;
|
|
191
|
+
let direction = 1;
|
|
200
192
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
let visible = 0;
|
|
193
|
+
const textFor = (row: HTMLTableRowElement, index: number) =>
|
|
194
|
+
(row.cells[index]?.textContent || "").trim();
|
|
204
195
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
196
|
+
const numberFor = (value: string) => {
|
|
197
|
+
if (!/[0-9]/.test(value)) return null;
|
|
198
|
+
const parsed = Number(value.replace(/[^0-9.-]/g, ""));
|
|
199
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
200
|
+
};
|
|
210
201
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
(
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
202
|
+
const compare = (a: string, b: string) => {
|
|
203
|
+
const first = numberFor(a);
|
|
204
|
+
const second = numberFor(b);
|
|
205
|
+
if (first !== null && second !== null) return first - second;
|
|
206
|
+
return a.localeCompare(b);
|
|
207
|
+
};
|
|
218
208
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
209
|
+
const apply = () => {
|
|
210
|
+
const query = filter?.value.trim().toLowerCase() ?? "";
|
|
211
|
+
let visible = 0;
|
|
212
|
+
|
|
213
|
+
rows.forEach((row) => {
|
|
214
|
+
const match =
|
|
215
|
+
!query || row.textContent.toLowerCase().includes(query);
|
|
216
|
+
row.hidden = !match;
|
|
217
|
+
if (match) visible++;
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
if (sortIndex >= 0) {
|
|
221
|
+
rows.sort(
|
|
222
|
+
(a, b) =>
|
|
223
|
+
direction *
|
|
224
|
+
compare(textFor(a, sortIndex), textFor(b, sortIndex)),
|
|
225
|
+
);
|
|
226
|
+
rows.forEach((row) => tbody.append(row));
|
|
227
|
+
}
|
|
224
228
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
229
|
+
if (empty) empty.hidden = visible !== 0;
|
|
230
|
+
if (count) {
|
|
231
|
+
count.textContent = `${visible} ${visible === 1 ? "row" : "rows"}`;
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
sortButtons.forEach((button) => {
|
|
236
|
+
button.addEventListener("click", () => {
|
|
237
|
+
const header = button.closest("th");
|
|
238
|
+
const index = Number(button.dataset.sortIndex);
|
|
239
|
+
direction =
|
|
240
|
+
sortIndex === index &&
|
|
241
|
+
header?.getAttribute("aria-sort") === "ascending"
|
|
242
|
+
? -1
|
|
243
|
+
: 1;
|
|
244
|
+
sortIndex = index;
|
|
245
|
+
sortButtons.forEach((other) => {
|
|
246
|
+
const otherHeader = other.closest("th");
|
|
247
|
+
if (!otherHeader) return;
|
|
248
|
+
otherHeader.setAttribute(
|
|
249
|
+
"aria-sort",
|
|
250
|
+
other === button
|
|
251
|
+
? direction === 1
|
|
252
|
+
? "ascending"
|
|
253
|
+
: "descending"
|
|
254
|
+
: "none",
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
apply();
|
|
258
|
+
});
|
|
246
259
|
});
|
|
260
|
+
|
|
261
|
+
filter?.addEventListener("input", apply);
|
|
247
262
|
apply();
|
|
248
263
|
});
|
|
249
|
-
|
|
264
|
+
};
|
|
250
265
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
});
|
|
266
|
+
bind();
|
|
267
|
+
document.addEventListener("astro:page-load", bind);
|
|
254
268
|
</script>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
import "../styles/tabs.css";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Tabs with automatic activation. Each item names a panel slot:
|
|
6
|
+
*
|
|
7
|
+
* <Tabs label="Service" items={[{ id: "logs", label: "Logs" }]}>
|
|
8
|
+
* <p slot="logs">…</p>
|
|
9
|
+
* </Tabs>
|
|
10
|
+
*
|
|
11
|
+
* Arrow keys, Home and End move between tabs; only the selected tab is in the
|
|
12
|
+
* tab order. The indicator slides with a transform. Fires `ds-tabs-change`.
|
|
13
|
+
*/
|
|
14
|
+
interface TabItem {
|
|
15
|
+
id: string;
|
|
16
|
+
label: string;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface Props {
|
|
21
|
+
items: TabItem[];
|
|
22
|
+
/** Accessible name for the tab list. */
|
|
23
|
+
label: string;
|
|
24
|
+
/** Id of the initially selected tab. Defaults to the first enabled tab. */
|
|
25
|
+
selected?: string;
|
|
26
|
+
id?: string;
|
|
27
|
+
class?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const { items, label, selected, id, class: className } = Astro.props;
|
|
31
|
+
|
|
32
|
+
const rootId = id ?? `tabs-${Math.random().toString(36).slice(2, 9)}`;
|
|
33
|
+
const current =
|
|
34
|
+
items.find((item) => item.id === selected && !item.disabled) ??
|
|
35
|
+
items.find((item) => !item.disabled);
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
<div class:list={["ds-tabs", className]} id={rootId} data-ds-tabs>
|
|
39
|
+
<div class="ds-tabs__list" role="tablist" aria-label={label}>
|
|
40
|
+
{items.map((item) => (
|
|
41
|
+
<button
|
|
42
|
+
class="ds-tabs__tab"
|
|
43
|
+
type="button"
|
|
44
|
+
role="tab"
|
|
45
|
+
id={`${rootId}-tab-${item.id}`}
|
|
46
|
+
aria-selected={item === current ? "true" : "false"}
|
|
47
|
+
aria-controls={`${rootId}-panel-${item.id}`}
|
|
48
|
+
tabindex={item === current ? 0 : -1}
|
|
49
|
+
disabled={item.disabled}
|
|
50
|
+
>
|
|
51
|
+
{item.label}
|
|
52
|
+
</button>
|
|
53
|
+
))}
|
|
54
|
+
<span class="ds-tabs__indicator" aria-hidden="true"></span>
|
|
55
|
+
</div>
|
|
56
|
+
{items.map((item) => (
|
|
57
|
+
<div
|
|
58
|
+
class="ds-tabs__panel"
|
|
59
|
+
role="tabpanel"
|
|
60
|
+
id={`${rootId}-panel-${item.id}`}
|
|
61
|
+
aria-labelledby={`${rootId}-tab-${item.id}`}
|
|
62
|
+
tabindex="0"
|
|
63
|
+
hidden={item !== current}
|
|
64
|
+
>
|
|
65
|
+
<slot name={item.id} />
|
|
66
|
+
</div>
|
|
67
|
+
))}
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<script>
|
|
71
|
+
import { mountTabs } from "../scripts/tabs";
|
|
72
|
+
|
|
73
|
+
const bind = () => {
|
|
74
|
+
document.querySelectorAll<HTMLElement>("[data-ds-tabs]").forEach((root) => {
|
|
75
|
+
if (root.dataset.dsBound) return;
|
|
76
|
+
root.dataset.dsBound = "true";
|
|
77
|
+
mountTabs(root);
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
bind();
|
|
82
|
+
document.addEventListener("astro:page-load", bind);
|
|
83
|
+
</script>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import Icon from "./Icon.astro";
|
|
3
|
+
import "../styles/sr-only.css";
|
|
3
4
|
import "../styles/input.css";
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -25,6 +26,8 @@ interface Props {
|
|
|
25
26
|
optional?: boolean;
|
|
26
27
|
multiline?: boolean;
|
|
27
28
|
rows?: number;
|
|
29
|
+
/** Multiline only. Which way the user can drag the handle. */
|
|
30
|
+
resize?: "none" | "vertical" | "both";
|
|
28
31
|
maxlength?: number;
|
|
29
32
|
minlength?: number;
|
|
30
33
|
pattern?: string;
|
|
@@ -52,6 +55,7 @@ const {
|
|
|
52
55
|
optional = false,
|
|
53
56
|
multiline = false,
|
|
54
57
|
rows = 3,
|
|
58
|
+
resize = "vertical",
|
|
55
59
|
maxlength,
|
|
56
60
|
minlength,
|
|
57
61
|
pattern,
|
|
@@ -74,6 +78,7 @@ const isPassword = !multiline && type === "password";
|
|
|
74
78
|
const inputClass = [
|
|
75
79
|
"ds-input",
|
|
76
80
|
size !== "md" && `ds-input--${size}`,
|
|
81
|
+
multiline && resize !== "vertical" && `ds-input--resize-${resize}`,
|
|
77
82
|
error && "is-invalid",
|
|
78
83
|
valid && !error && "is-valid",
|
|
79
84
|
valid && !error && "ds-input--with-state",
|
|
@@ -191,97 +196,110 @@ const inputClass = [
|
|
|
191
196
|
</div>
|
|
192
197
|
|
|
193
198
|
<script>
|
|
194
|
-
const
|
|
199
|
+
const bind = () => {
|
|
200
|
+
const fields = document.querySelectorAll<HTMLElement>("[data-ds-field]");
|
|
195
201
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
202
|
+
fields.forEach((field) => {
|
|
203
|
+
if (field.dataset.dsBound) return;
|
|
204
|
+
field.dataset.dsBound = "true";
|
|
205
|
+
const input = field.querySelector<HTMLInputElement | HTMLTextAreaElement>(
|
|
206
|
+
".ds-input",
|
|
207
|
+
);
|
|
208
|
+
const count = field.querySelector<HTMLElement>("[data-ds-count]");
|
|
209
|
+
const countLive = field.querySelector<HTMLElement>(
|
|
210
|
+
"[data-ds-count-live]",
|
|
211
|
+
);
|
|
212
|
+
const error = field.querySelector<HTMLElement>("[data-ds-error]");
|
|
213
|
+
const errorText = field.querySelector<HTMLElement>(
|
|
214
|
+
"[data-ds-error-text]",
|
|
215
|
+
);
|
|
216
|
+
const hint = field.querySelector<HTMLElement>(".ds-field__hint");
|
|
217
|
+
if (!input) return;
|
|
206
218
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
219
|
+
if (count && input.maxLength > 0) {
|
|
220
|
+
// The visible counter updates silently; screen readers only hear it
|
|
221
|
+
// near the limit, not on every keystroke.
|
|
222
|
+
const updateCount = () => {
|
|
223
|
+
count.textContent = `${input.value.length} / ${input.maxLength}`;
|
|
224
|
+
const left = input.maxLength - input.value.length;
|
|
225
|
+
if (countLive) {
|
|
226
|
+
countLive.textContent =
|
|
227
|
+
left <= 5
|
|
228
|
+
? `${left} ${left === 1 ? "character" : "characters"} left`
|
|
229
|
+
: "";
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
input.addEventListener("input", updateCount);
|
|
233
|
+
updateCount();
|
|
234
|
+
}
|
|
223
235
|
|
|
224
|
-
|
|
236
|
+
if (!error || !errorText) return;
|
|
225
237
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
238
|
+
const message = () => {
|
|
239
|
+
if (input.validity.valueMissing) {
|
|
240
|
+
return input.dataset.requiredMessage || "This field is required.";
|
|
241
|
+
}
|
|
242
|
+
if (input.validity.tooShort) {
|
|
243
|
+
return `Use at least ${input.minLength} characters.`;
|
|
244
|
+
}
|
|
245
|
+
if (input.validity.patternMismatch) {
|
|
246
|
+
return field.dataset.message || "Use the requested format.";
|
|
247
|
+
}
|
|
248
|
+
if (input.validity.typeMismatch) {
|
|
249
|
+
return "Enter a valid value.";
|
|
250
|
+
}
|
|
251
|
+
if (input.validity.tooLong) {
|
|
252
|
+
return `Use at most ${input.maxLength} characters.`;
|
|
253
|
+
}
|
|
254
|
+
return "";
|
|
255
|
+
};
|
|
244
256
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
257
|
+
const apply = (invalid: boolean, text: string) => {
|
|
258
|
+
input.classList.toggle("is-invalid", invalid);
|
|
259
|
+
if (invalid) {
|
|
260
|
+
input.setAttribute("aria-invalid", "true");
|
|
261
|
+
} else {
|
|
262
|
+
input.removeAttribute("aria-invalid");
|
|
263
|
+
}
|
|
264
|
+
error.hidden = !invalid;
|
|
265
|
+
errorText.textContent = text;
|
|
266
|
+
if (hint) hint.hidden = invalid;
|
|
267
|
+
};
|
|
256
268
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
269
|
+
const validate = () => {
|
|
270
|
+
const text = message();
|
|
271
|
+
apply(!!text, text);
|
|
272
|
+
};
|
|
261
273
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
274
|
+
input.addEventListener("blur", validate);
|
|
275
|
+
input.addEventListener("input", () => {
|
|
276
|
+
if (input.getAttribute("aria-invalid") === "true") validate();
|
|
277
|
+
});
|
|
265
278
|
});
|
|
266
|
-
});
|
|
267
279
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
280
|
+
document
|
|
281
|
+
.querySelectorAll<HTMLElement>("[data-ds-reveal]")
|
|
282
|
+
.forEach((button) => {
|
|
283
|
+
if (button.dataset.dsBound) return;
|
|
284
|
+
button.dataset.dsBound = "true";
|
|
285
|
+
const field = button.closest("[data-ds-field]");
|
|
286
|
+
const input = field?.querySelector<HTMLInputElement>(".ds-input");
|
|
287
|
+
if (!input) return;
|
|
288
|
+
const show = button.querySelector<HTMLElement>("[data-ds-reveal-show]");
|
|
289
|
+
const hide = button.querySelector<HTMLElement>("[data-ds-reveal-hide]");
|
|
276
290
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
291
|
+
button.addEventListener("click", () => {
|
|
292
|
+
const revealed = input.type === "text";
|
|
293
|
+
input.type = revealed ? "password" : "text";
|
|
294
|
+
// Stable name, state in aria-pressed: "Show password, pressed".
|
|
295
|
+
button.setAttribute("aria-pressed", revealed ? "false" : "true");
|
|
296
|
+
if (show) show.hidden = !revealed;
|
|
297
|
+
if (hide) hide.hidden = revealed;
|
|
298
|
+
input.focus({ preventScroll: true });
|
|
299
|
+
});
|
|
285
300
|
});
|
|
286
|
-
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
bind();
|
|
304
|
+
document.addEventListener("astro:page-load", bind);
|
|
287
305
|
</script>
|
package/src/astro/Toast.astro
CHANGED
|
@@ -10,7 +10,7 @@ import "../styles/toast.css";
|
|
|
10
10
|
*
|
|
11
11
|
* With `triggerLabel` the component renders a real button and a template; the
|
|
12
12
|
* button mounts the toast into one shared bottom-end region. For imperative
|
|
13
|
-
* use, import `toast` from `@
|
|
13
|
+
* use, import `toast` from `@db-vincent/components/scripts/toast` and call
|
|
14
14
|
* `toast.add`, `toast.close` or `toast.promise`.
|
|
15
15
|
*/
|
|
16
16
|
interface Props {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
import "../styles/popover.css";
|
|
3
|
+
import "../styles/tooltip.css";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tooltip. A short, non-essential label for a control. Wrap one focusable
|
|
7
|
+
* trigger; the script adds aria-describedby to it. Never put information a
|
|
8
|
+
* user needs, or anything interactive, in a tooltip: touch and many assistive
|
|
9
|
+
* technologies cannot reach it. Use Popover for that.
|
|
10
|
+
*
|
|
11
|
+
* <Tooltip text="Copy address"><Button iconOnly icon="copy" ariaLabel="Copy" /></Tooltip>
|
|
12
|
+
*/
|
|
13
|
+
interface Props {
|
|
14
|
+
text: string;
|
|
15
|
+
placement?: "top" | "bottom" | "start" | "end";
|
|
16
|
+
/** Hover delay in milliseconds. Keyboard focus opens at once. */
|
|
17
|
+
delay?: number;
|
|
18
|
+
id?: string;
|
|
19
|
+
class?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { text, placement = "top", delay, id, class: className } = Astro.props;
|
|
23
|
+
|
|
24
|
+
const tipId = id ?? `tooltip-${Math.random().toString(36).slice(2, 9)}`;
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
<span class="ds-anchor" data-ds-tooltip data-delay={delay}>
|
|
28
|
+
<slot />
|
|
29
|
+
<span
|
|
30
|
+
class:list={["ds-tooltip", `ds-tooltip--${placement}`, className]}
|
|
31
|
+
id={tipId}
|
|
32
|
+
role="tooltip"
|
|
33
|
+
popover="manual"
|
|
34
|
+
>
|
|
35
|
+
{text}
|
|
36
|
+
</span>
|
|
37
|
+
</span>
|
|
38
|
+
|
|
39
|
+
<script>
|
|
40
|
+
import { mountTooltip } from "../scripts/tooltip";
|
|
41
|
+
|
|
42
|
+
const bind = () => {
|
|
43
|
+
document
|
|
44
|
+
.querySelectorAll<HTMLElement>("[data-ds-tooltip]")
|
|
45
|
+
.forEach((root) => {
|
|
46
|
+
if (root.dataset.dsBound) return;
|
|
47
|
+
root.dataset.dsBound = "true";
|
|
48
|
+
mountTooltip(root);
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
bind();
|
|
53
|
+
document.addEventListener("astro:page-load", bind);
|
|
54
|
+
</script>
|