@db-vincent/components 0.0.0-stage → 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 +28 -4
- package/src/astro/Accordion.astro +37 -0
- package/src/astro/Alert.astro +73 -0
- package/src/astro/Avatar.astro +88 -0
- package/src/astro/AvatarGroup.astro +63 -0
- package/src/astro/Badge.astro +23 -0
- package/src/astro/Breadcrumbs.astro +45 -0
- package/src/astro/Button.astro +125 -0
- package/src/astro/Card.astro +63 -0
- package/src/astro/Checkbox.astro +61 -0
- package/src/astro/Chip.astro +135 -0
- package/src/astro/CodeBlock.astro +17 -0
- package/src/astro/Drawer.astro +30 -0
- package/src/astro/EmptyState.astro +45 -0
- package/src/astro/Example.astro +37 -0
- package/src/astro/Grid.astro +56 -0
- package/src/astro/GridItem.astro +25 -0
- package/src/astro/Icon.astro +52 -0
- package/src/astro/Menu.astro +151 -0
- package/src/astro/MenuItem.astro +57 -0
- package/src/astro/Modal.astro +124 -0
- package/src/astro/Navbar.astro +756 -0
- package/src/astro/PageHeader.astro +34 -0
- package/src/astro/Pagination.astro +212 -0
- package/src/astro/Popover.astro +51 -0
- package/src/astro/Progress.astro +71 -0
- package/src/astro/Radio.astro +58 -0
- package/src/astro/RadioGroup.astro +25 -0
- package/src/astro/Section.astro +25 -0
- package/src/astro/Select.astro +420 -0
- package/src/astro/Sidebar.astro +215 -0
- package/src/astro/Skeleton.astro +41 -0
- package/src/astro/Spinner.astro +20 -0
- package/src/astro/Stepper.astro +82 -0
- package/src/astro/Switch.astro +78 -0
- package/src/astro/Table.astro +268 -0
- package/src/astro/Tabs.astro +83 -0
- package/src/astro/TextField.astro +305 -0
- package/src/astro/ThemeToggle.astro +66 -0
- package/src/astro/Toast.astro +181 -0
- package/src/astro/Tooltip.astro +54 -0
- package/src/astro/TopBar.astro +29 -0
- package/src/data/icons.ts +120 -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 +565 -0
- package/src/scripts/tooltip.ts +127 -0
- package/src/styles/accordion.css +79 -0
- package/src/styles/alert.css +108 -0
- package/src/styles/avatar.css +131 -0
- package/src/styles/badge.css +48 -0
- package/src/styles/base.css +205 -0
- package/src/styles/breadcrumbs.css +54 -0
- package/src/styles/button.css +174 -0
- package/src/styles/card.css +112 -0
- package/src/styles/chip.css +106 -0
- package/src/styles/choice.css +143 -0
- package/src/styles/code.css +44 -0
- package/src/styles/empty.css +59 -0
- package/src/styles/example.css +57 -0
- package/src/styles/grid.css +64 -0
- package/src/styles/input.css +208 -0
- package/src/styles/menu.css +151 -0
- package/src/styles/modal.css +285 -0
- package/src/styles/motion.css +114 -0
- package/src/styles/nav.css +232 -0
- package/src/styles/navbar.css +658 -0
- package/src/styles/page-header.css +62 -0
- package/src/styles/pagination.css +81 -0
- package/src/styles/popover.css +134 -0
- package/src/styles/progress.css +95 -0
- package/src/styles/section.css +38 -0
- package/src/styles/select.css +246 -0
- package/src/styles/skeleton.css +66 -0
- package/src/styles/spinner.css +44 -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 +177 -0
- package/src/styles/tabs.css +120 -0
- package/src/styles/theme-toggle.css +31 -0
- package/src/styles/toast.css +225 -0
- package/src/styles/tooltip.css +63 -0
- package/src/styles/topbar.css +32 -0
- package/README.md +0 -3
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Icon from "./Icon.astro";
|
|
3
|
+
import "../styles/sr-only.css";
|
|
4
|
+
import "../styles/input.css";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Text field. Label above, hint and count below, error replaces the hint.
|
|
8
|
+
* Hint and error are wired through aria-describedby; invalid sets
|
|
9
|
+
* aria-invalid. `validate` adds native constraint validation on blur, with
|
|
10
|
+
* live recovery while typing. Password fields get a reveal toggle, and
|
|
11
|
+
* counters update as the value changes.
|
|
12
|
+
*/
|
|
13
|
+
interface Props {
|
|
14
|
+
label?: string;
|
|
15
|
+
id?: string;
|
|
16
|
+
name?: string;
|
|
17
|
+
type?: string;
|
|
18
|
+
value?: string;
|
|
19
|
+
placeholder?: string;
|
|
20
|
+
hint?: string;
|
|
21
|
+
error?: string;
|
|
22
|
+
valid?: boolean;
|
|
23
|
+
validate?: boolean;
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
required?: boolean;
|
|
26
|
+
optional?: boolean;
|
|
27
|
+
multiline?: boolean;
|
|
28
|
+
rows?: number;
|
|
29
|
+
/** Multiline only. Which way the user can drag the handle. */
|
|
30
|
+
resize?: "none" | "vertical" | "both";
|
|
31
|
+
maxlength?: number;
|
|
32
|
+
minlength?: number;
|
|
33
|
+
pattern?: string;
|
|
34
|
+
count?: boolean;
|
|
35
|
+
size?: "sm" | "md" | "lg";
|
|
36
|
+
autocomplete?: string;
|
|
37
|
+
inputmode?:
|
|
38
|
+
"text" | "numeric" | "decimal" | "email" | "tel" | "search" | "url";
|
|
39
|
+
class?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const {
|
|
43
|
+
label,
|
|
44
|
+
id,
|
|
45
|
+
name,
|
|
46
|
+
type = "text",
|
|
47
|
+
value,
|
|
48
|
+
placeholder,
|
|
49
|
+
hint,
|
|
50
|
+
error,
|
|
51
|
+
valid = false,
|
|
52
|
+
validate = false,
|
|
53
|
+
disabled = false,
|
|
54
|
+
required = false,
|
|
55
|
+
optional = false,
|
|
56
|
+
multiline = false,
|
|
57
|
+
rows = 3,
|
|
58
|
+
resize = "vertical",
|
|
59
|
+
maxlength,
|
|
60
|
+
minlength,
|
|
61
|
+
pattern,
|
|
62
|
+
count = false,
|
|
63
|
+
size = "md",
|
|
64
|
+
autocomplete,
|
|
65
|
+
inputmode,
|
|
66
|
+
class: className,
|
|
67
|
+
} = Astro.props;
|
|
68
|
+
|
|
69
|
+
const fieldId = id ?? `field-${Math.random().toString(36).slice(2, 9)}`;
|
|
70
|
+
const hintId = `${fieldId}-hint`;
|
|
71
|
+
const errorId = `${fieldId}-error`;
|
|
72
|
+
const describedby =
|
|
73
|
+
[hint && (!error || validate) && hintId, (error || validate) && errorId]
|
|
74
|
+
.filter(Boolean)
|
|
75
|
+
.join(" ") || undefined;
|
|
76
|
+
const current = value ?? "";
|
|
77
|
+
const isPassword = !multiline && type === "password";
|
|
78
|
+
const inputClass = [
|
|
79
|
+
"ds-input",
|
|
80
|
+
size !== "md" && `ds-input--${size}`,
|
|
81
|
+
multiline && resize !== "vertical" && `ds-input--resize-${resize}`,
|
|
82
|
+
error && "is-invalid",
|
|
83
|
+
valid && !error && "is-valid",
|
|
84
|
+
valid && !error && "ds-input--with-state",
|
|
85
|
+
]
|
|
86
|
+
.filter(Boolean)
|
|
87
|
+
.join(" ");
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
<div
|
|
91
|
+
class:list={["ds-field", isPassword && "ds-field--reveal", className]}
|
|
92
|
+
data-ds-field
|
|
93
|
+
data-message={error}
|
|
94
|
+
>
|
|
95
|
+
{label && (
|
|
96
|
+
<label class="ds-field__label" for={fieldId}>
|
|
97
|
+
{label}
|
|
98
|
+
{optional && <span class="ds-field__optional"> (optional)</span>}
|
|
99
|
+
{required && (
|
|
100
|
+
<span class="ds-field__required" aria-hidden="true">
|
|
101
|
+
{" "}
|
|
102
|
+
*
|
|
103
|
+
</span>
|
|
104
|
+
)}
|
|
105
|
+
</label>
|
|
106
|
+
)}
|
|
107
|
+
|
|
108
|
+
<div class="ds-field__control">
|
|
109
|
+
{multiline ? (
|
|
110
|
+
<textarea
|
|
111
|
+
class={inputClass}
|
|
112
|
+
id={fieldId}
|
|
113
|
+
name={name}
|
|
114
|
+
rows={rows}
|
|
115
|
+
placeholder={placeholder}
|
|
116
|
+
disabled={disabled}
|
|
117
|
+
required={required}
|
|
118
|
+
maxlength={maxlength}
|
|
119
|
+
minlength={minlength}
|
|
120
|
+
aria-invalid={error ? "true" : undefined}
|
|
121
|
+
aria-describedby={describedby}
|
|
122
|
+
autocomplete={autocomplete}
|
|
123
|
+
>{current}</textarea>
|
|
124
|
+
) : (
|
|
125
|
+
<input
|
|
126
|
+
class={inputClass}
|
|
127
|
+
id={fieldId}
|
|
128
|
+
name={name}
|
|
129
|
+
type={type}
|
|
130
|
+
value={value}
|
|
131
|
+
placeholder={placeholder}
|
|
132
|
+
disabled={disabled}
|
|
133
|
+
required={required}
|
|
134
|
+
maxlength={maxlength}
|
|
135
|
+
minlength={minlength}
|
|
136
|
+
pattern={pattern}
|
|
137
|
+
inputmode={inputmode}
|
|
138
|
+
autocomplete={autocomplete}
|
|
139
|
+
aria-invalid={error ? "true" : undefined}
|
|
140
|
+
aria-describedby={describedby}
|
|
141
|
+
/>
|
|
142
|
+
)}
|
|
143
|
+
{valid && !error && (
|
|
144
|
+
<>
|
|
145
|
+
<span class="ds-field__state" aria-hidden="true">
|
|
146
|
+
<Icon name="check" size={14} />
|
|
147
|
+
</span>
|
|
148
|
+
<span class="ds-sr-only">Valid.</span>
|
|
149
|
+
</>
|
|
150
|
+
)}
|
|
151
|
+
{isPassword && (
|
|
152
|
+
<button
|
|
153
|
+
class="ds-field__reveal"
|
|
154
|
+
type="button"
|
|
155
|
+
data-ds-reveal
|
|
156
|
+
aria-label="Show password"
|
|
157
|
+
aria-pressed="false"
|
|
158
|
+
aria-controls={fieldId}
|
|
159
|
+
disabled={disabled}
|
|
160
|
+
>
|
|
161
|
+
<span data-ds-reveal-show aria-hidden="true">
|
|
162
|
+
<Icon name="eye" size={16} />
|
|
163
|
+
</span>
|
|
164
|
+
<span data-ds-reveal-hide aria-hidden="true" hidden>
|
|
165
|
+
<Icon name="eye-off" size={16} />
|
|
166
|
+
</span>
|
|
167
|
+
</button>
|
|
168
|
+
)}
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
{((error && !validate) || (hint && (!error || validate))) && (
|
|
172
|
+
<div
|
|
173
|
+
class:list={[error && !validate ? "ds-field__error" : "ds-field__hint"]}
|
|
174
|
+
id={error && !validate ? errorId : hintId}
|
|
175
|
+
>
|
|
176
|
+
{error && !validate && <Icon name="alert-circle" size={14} />}
|
|
177
|
+
{error && !validate ? error : hint}
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
180
|
+
|
|
181
|
+
{count && maxlength && (
|
|
182
|
+
<>
|
|
183
|
+
<span class="ds-field__count" data-ds-count>
|
|
184
|
+
{current.length} / {maxlength}
|
|
185
|
+
</span>
|
|
186
|
+
<span class="ds-sr-only" aria-live="polite" data-ds-count-live></span>
|
|
187
|
+
</>
|
|
188
|
+
)}
|
|
189
|
+
|
|
190
|
+
{validate && (
|
|
191
|
+
<div class="ds-field__error" id={errorId} data-ds-error hidden={!error}>
|
|
192
|
+
<Icon name="alert-circle" size={14} />
|
|
193
|
+
<span data-ds-error-text>{error || "Check this field."}</span>
|
|
194
|
+
</div>
|
|
195
|
+
)}
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<script>
|
|
199
|
+
const bind = () => {
|
|
200
|
+
const fields = document.querySelectorAll<HTMLElement>("[data-ds-field]");
|
|
201
|
+
|
|
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;
|
|
218
|
+
|
|
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
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!error || !errorText) return;
|
|
237
|
+
|
|
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
|
+
};
|
|
256
|
+
|
|
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
|
+
};
|
|
268
|
+
|
|
269
|
+
const validate = () => {
|
|
270
|
+
const text = message();
|
|
271
|
+
apply(!!text, text);
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
input.addEventListener("blur", validate);
|
|
275
|
+
input.addEventListener("input", () => {
|
|
276
|
+
if (input.getAttribute("aria-invalid") === "true") validate();
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
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]");
|
|
290
|
+
|
|
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
|
+
});
|
|
300
|
+
});
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
bind();
|
|
304
|
+
document.addEventListener("astro:page-load", bind);
|
|
305
|
+
</script>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Icon from "./Icon.astro";
|
|
3
|
+
import Button from "./Button.astro";
|
|
4
|
+
import "../styles/theme-toggle.css";
|
|
5
|
+
|
|
6
|
+
/** Light/dark switch. Persists the choice and follows the OS until then. */
|
|
7
|
+
interface Props {
|
|
8
|
+
class?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { class: className } = Astro.props;
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<Button
|
|
15
|
+
variant="ghost"
|
|
16
|
+
iconOnly
|
|
17
|
+
ariaLabel="Switch color theme"
|
|
18
|
+
class:list={["ds-theme-toggle", className]}
|
|
19
|
+
data-theme-toggle
|
|
20
|
+
>
|
|
21
|
+
<span class="ds-theme-toggle__sun" aria-hidden="true">
|
|
22
|
+
<Icon name="sun" size={16} />
|
|
23
|
+
</span>
|
|
24
|
+
<span class="ds-theme-toggle__moon" aria-hidden="true">
|
|
25
|
+
<Icon name="moon" size={16} />
|
|
26
|
+
</span>
|
|
27
|
+
</Button>
|
|
28
|
+
|
|
29
|
+
<script>
|
|
30
|
+
const root = document.documentElement;
|
|
31
|
+
const query = window.matchMedia("(prefers-color-scheme: dark)");
|
|
32
|
+
const current = () =>
|
|
33
|
+
root.dataset.theme ?? (query.matches ? "dark" : "light");
|
|
34
|
+
|
|
35
|
+
const sync = () => {
|
|
36
|
+
const theme = current();
|
|
37
|
+
document
|
|
38
|
+
.querySelectorAll<HTMLElement>("[data-theme-toggle]")
|
|
39
|
+
.forEach((button) => {
|
|
40
|
+
button.setAttribute(
|
|
41
|
+
"aria-label",
|
|
42
|
+
`Switch to ${theme === "dark" ? "light" : "dark"} theme`,
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const apply = (theme: "light" | "dark") => {
|
|
48
|
+
root.dataset.theme = theme;
|
|
49
|
+
try {
|
|
50
|
+
localStorage.setItem("minerva-theme", theme);
|
|
51
|
+
} catch {}
|
|
52
|
+
sync();
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
document
|
|
56
|
+
.querySelectorAll<HTMLElement>("[data-theme-toggle]")
|
|
57
|
+
.forEach((button) => {
|
|
58
|
+
if (button.dataset.bound) return;
|
|
59
|
+
button.dataset.bound = "true";
|
|
60
|
+
button.addEventListener("click", () =>
|
|
61
|
+
apply(current() === "dark" ? "light" : "dark"),
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
query.addEventListener("change", sync);
|
|
65
|
+
sync();
|
|
66
|
+
</script>
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Icon from "./Icon.astro";
|
|
3
|
+
import Button from "./Button.astro";
|
|
4
|
+
import "../styles/toast.css";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Toast. A transient message on a raised neutral popover surface, shaped after
|
|
8
|
+
* shadcn/ui: neutral card, type icon, inline action and close. `danger` (alias
|
|
9
|
+
* `error`) and `warning` are assertive, the rest are polite.
|
|
10
|
+
*
|
|
11
|
+
* With `triggerLabel` the component renders a real button and a template; the
|
|
12
|
+
* button mounts the toast into one shared bottom-end region. For imperative
|
|
13
|
+
* use, import `toast` from `@db-vincent/components/scripts/toast` and call
|
|
14
|
+
* `toast.add`, `toast.close` or `toast.promise`.
|
|
15
|
+
*/
|
|
16
|
+
interface Props {
|
|
17
|
+
tone?: "info" | "success" | "warning" | "danger" | "error" | "loading";
|
|
18
|
+
type?: "info" | "success" | "warning" | "danger" | "error" | "loading";
|
|
19
|
+
title: string;
|
|
20
|
+
message?: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
dismissible?: boolean;
|
|
23
|
+
icon?: boolean;
|
|
24
|
+
triggerLabel?: string;
|
|
25
|
+
triggerVariant?: "primary" | "secondary" | "ghost" | "danger";
|
|
26
|
+
/** Auto-dismiss delay for the mounted toast, in milliseconds. 0 keeps it. */
|
|
27
|
+
duration?: number;
|
|
28
|
+
id?: string;
|
|
29
|
+
class?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const {
|
|
33
|
+
tone: toneProp,
|
|
34
|
+
type: typeProp,
|
|
35
|
+
title,
|
|
36
|
+
message,
|
|
37
|
+
description,
|
|
38
|
+
dismissible = true,
|
|
39
|
+
icon = true,
|
|
40
|
+
triggerLabel,
|
|
41
|
+
triggerVariant = "secondary",
|
|
42
|
+
duration = 5000,
|
|
43
|
+
id,
|
|
44
|
+
class: className,
|
|
45
|
+
} = Astro.props;
|
|
46
|
+
|
|
47
|
+
const requested = typeProp ?? toneProp ?? "info";
|
|
48
|
+
const tone = requested === "error" ? "danger" : requested;
|
|
49
|
+
const role = tone === "danger" || tone === "warning" ? "alert" : "status";
|
|
50
|
+
const live = role === "alert" ? "assertive" : "polite";
|
|
51
|
+
const iconName = {
|
|
52
|
+
info: "info",
|
|
53
|
+
success: "check-circle",
|
|
54
|
+
warning: "alert-triangle",
|
|
55
|
+
danger: "alert-circle",
|
|
56
|
+
loading: "loader",
|
|
57
|
+
}[tone];
|
|
58
|
+
const body = description ?? message;
|
|
59
|
+
const toastId = id ?? `toast-${Math.random().toString(36).slice(2, 9)}`;
|
|
60
|
+
const hasActions = Astro.slots.has("actions");
|
|
61
|
+
const classes = ["ds-toast", `ds-toast--${tone}`, className]
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.join(" ");
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
{triggerLabel ? (
|
|
67
|
+
<>
|
|
68
|
+
<Button variant={triggerVariant} type="button" data-toast-trigger={toastId}>
|
|
69
|
+
{triggerLabel}
|
|
70
|
+
</Button>
|
|
71
|
+
<template id={`${toastId}-template`}>
|
|
72
|
+
<div
|
|
73
|
+
class={classes}
|
|
74
|
+
id={toastId}
|
|
75
|
+
role={role}
|
|
76
|
+
aria-live={live}
|
|
77
|
+
aria-atomic="true"
|
|
78
|
+
data-toast
|
|
79
|
+
data-toast-type={tone}
|
|
80
|
+
data-toast-duration={duration}
|
|
81
|
+
>
|
|
82
|
+
{icon && (
|
|
83
|
+
<span class="ds-toast__icon" aria-hidden="true">
|
|
84
|
+
<Icon name={iconName} size={16} />
|
|
85
|
+
</span>
|
|
86
|
+
)}
|
|
87
|
+
<div class="ds-toast__content">
|
|
88
|
+
<p class="ds-toast__title">{title}</p>
|
|
89
|
+
{body && <p class="ds-toast__body">{body}</p>}
|
|
90
|
+
<slot />
|
|
91
|
+
</div>
|
|
92
|
+
{hasActions && (
|
|
93
|
+
<div class="ds-toast__actions">
|
|
94
|
+
<slot name="actions" />
|
|
95
|
+
</div>
|
|
96
|
+
)}
|
|
97
|
+
{dismissible && (
|
|
98
|
+
<button
|
|
99
|
+
class="ds-toast__close"
|
|
100
|
+
type="button"
|
|
101
|
+
aria-label="Close toast"
|
|
102
|
+
data-toast-close
|
|
103
|
+
>
|
|
104
|
+
<Icon name="x" size={16} />
|
|
105
|
+
</button>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
</template>
|
|
109
|
+
</>
|
|
110
|
+
) : (
|
|
111
|
+
<div
|
|
112
|
+
class={classes}
|
|
113
|
+
id={toastId}
|
|
114
|
+
role={role}
|
|
115
|
+
aria-live={live}
|
|
116
|
+
aria-atomic="true"
|
|
117
|
+
data-toast
|
|
118
|
+
data-toast-type={tone}
|
|
119
|
+
>
|
|
120
|
+
{icon && (
|
|
121
|
+
<span class="ds-toast__icon" aria-hidden="true">
|
|
122
|
+
<Icon name={iconName} size={16} />
|
|
123
|
+
</span>
|
|
124
|
+
)}
|
|
125
|
+
<div class="ds-toast__content">
|
|
126
|
+
<p class="ds-toast__title">{title}</p>
|
|
127
|
+
{body && <p class="ds-toast__body">{body}</p>}
|
|
128
|
+
<slot />
|
|
129
|
+
</div>
|
|
130
|
+
{hasActions && (
|
|
131
|
+
<div class="ds-toast__actions">
|
|
132
|
+
<slot name="actions" />
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
135
|
+
{dismissible && (
|
|
136
|
+
<button
|
|
137
|
+
class="ds-toast__close"
|
|
138
|
+
type="button"
|
|
139
|
+
aria-label="Close toast"
|
|
140
|
+
data-toast-close
|
|
141
|
+
>
|
|
142
|
+
<Icon name="x" size={16} />
|
|
143
|
+
</button>
|
|
144
|
+
)}
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
147
|
+
|
|
148
|
+
<script>
|
|
149
|
+
import { dismiss, mount } from "../scripts/toast";
|
|
150
|
+
|
|
151
|
+
document.addEventListener("click", (event) => {
|
|
152
|
+
const target = event.target;
|
|
153
|
+
if (!(target instanceof Element)) return;
|
|
154
|
+
|
|
155
|
+
const trigger = target.closest<HTMLElement>("[data-toast-trigger]");
|
|
156
|
+
if (trigger) {
|
|
157
|
+
const id = trigger.dataset.toastTrigger;
|
|
158
|
+
const template = id
|
|
159
|
+
? (document.getElementById(
|
|
160
|
+
`${id}-template`,
|
|
161
|
+
) as HTMLTemplateElement | null)
|
|
162
|
+
: null;
|
|
163
|
+
const toast = template?.content.firstElementChild?.cloneNode(true);
|
|
164
|
+
if (toast instanceof HTMLElement) {
|
|
165
|
+
// Inside the shared region the region itself is the live region.
|
|
166
|
+
if (toast.getAttribute("role") === "status") {
|
|
167
|
+
toast.removeAttribute("role");
|
|
168
|
+
toast.removeAttribute("aria-live");
|
|
169
|
+
}
|
|
170
|
+
mount(toast, trigger);
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Inline previews rendered directly on a page are not mounted in the
|
|
176
|
+
// shared region, so they keep a local close handler.
|
|
177
|
+
const close = target.closest("[data-toast-close]");
|
|
178
|
+
const toast = close?.closest<HTMLElement>("[data-toast]");
|
|
179
|
+
if (toast && !toast.closest("[data-toast-region]")) dismiss(toast);
|
|
180
|
+
});
|
|
181
|
+
</script>
|
|
@@ -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>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
import "../styles/topbar.css";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Slim, non-sticky utility strip for the top of a page.
|
|
6
|
+
*
|
|
7
|
+
* The start slot holds the page context (usually `<Breadcrumbs>`), the
|
|
8
|
+
* actions slot holds small utilities (theme toggle, command shortcut).
|
|
9
|
+
* It scrolls away with the page; persistent navigation belongs to
|
|
10
|
+
* `<Navbar>` or the sidebar.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
class?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { class: className } = Astro.props;
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
<div class:list={["ds-topbar", className]}>
|
|
21
|
+
<div class="ds-topbar__inner">
|
|
22
|
+
<div class="ds-topbar__start">
|
|
23
|
+
<slot name="start" />
|
|
24
|
+
</div>
|
|
25
|
+
<div class="ds-topbar__actions">
|
|
26
|
+
<slot name="actions" />
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|