@db-vincent/components 0.0.0-stage → 0.1.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/package.json +28 -4
- package/src/astro/Accordion.astro +37 -0
- package/src/astro/Alert.astro +73 -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 +116 -0
- package/src/astro/CodeBlock.astro +17 -0
- package/src/astro/EmptyState.astro +45 -0
- package/src/astro/Example.astro +37 -0
- package/src/astro/Grid.astro +50 -0
- package/src/astro/GridItem.astro +23 -0
- package/src/astro/Icon.astro +52 -0
- package/src/astro/Modal.astro +117 -0
- package/src/astro/Navbar.astro +756 -0
- package/src/astro/PageHeader.astro +34 -0
- package/src/astro/Pagination.astro +162 -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 +403 -0
- package/src/astro/Sidebar.astro +215 -0
- package/src/astro/Skeleton.astro +39 -0
- package/src/astro/Spinner.astro +19 -0
- package/src/astro/Table.astro +254 -0
- package/src/astro/TextField.astro +287 -0
- package/src/astro/ThemeToggle.astro +66 -0
- package/src/astro/Toast.astro +181 -0
- package/src/astro/TopBar.astro +29 -0
- package/src/data/icons.ts +104 -0
- package/src/scripts/toast.ts +531 -0
- package/src/styles/accordion.css +77 -0
- package/src/styles/alert.css +106 -0
- package/src/styles/badge.css +46 -0
- package/src/styles/base.css +203 -0
- package/src/styles/breadcrumbs.css +52 -0
- package/src/styles/button.css +163 -0
- package/src/styles/card.css +108 -0
- package/src/styles/chip.css +104 -0
- package/src/styles/choice.css +137 -0
- package/src/styles/code.css +42 -0
- package/src/styles/empty.css +57 -0
- package/src/styles/example.css +55 -0
- package/src/styles/grid.css +62 -0
- package/src/styles/input.css +198 -0
- package/src/styles/modal.css +143 -0
- package/src/styles/motion.css +96 -0
- package/src/styles/nav.css +230 -0
- package/src/styles/navbar.css +656 -0
- package/src/styles/page-header.css +59 -0
- package/src/styles/pagination.css +77 -0
- package/src/styles/section.css +36 -0
- package/src/styles/select.css +235 -0
- package/src/styles/skeleton.css +64 -0
- package/src/styles/spinner.css +42 -0
- package/src/styles/table.css +175 -0
- package/src/styles/theme-toggle.css +29 -0
- package/src/styles/toast.css +207 -0
- package/src/styles/topbar.css +30 -0
- package/README.md +0 -3
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Icon from "./Icon.astro";
|
|
3
|
+
import "../styles/input.css";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Text field. Label above, hint and count below, error replaces the hint.
|
|
7
|
+
* Hint and error are wired through aria-describedby; invalid sets
|
|
8
|
+
* aria-invalid. `validate` adds native constraint validation on blur, with
|
|
9
|
+
* live recovery while typing. Password fields get a reveal toggle, and
|
|
10
|
+
* counters update as the value changes.
|
|
11
|
+
*/
|
|
12
|
+
interface Props {
|
|
13
|
+
label?: string;
|
|
14
|
+
id?: string;
|
|
15
|
+
name?: string;
|
|
16
|
+
type?: string;
|
|
17
|
+
value?: string;
|
|
18
|
+
placeholder?: string;
|
|
19
|
+
hint?: string;
|
|
20
|
+
error?: string;
|
|
21
|
+
valid?: boolean;
|
|
22
|
+
validate?: boolean;
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
required?: boolean;
|
|
25
|
+
optional?: boolean;
|
|
26
|
+
multiline?: boolean;
|
|
27
|
+
rows?: number;
|
|
28
|
+
maxlength?: number;
|
|
29
|
+
minlength?: number;
|
|
30
|
+
pattern?: string;
|
|
31
|
+
count?: boolean;
|
|
32
|
+
size?: "sm" | "md" | "lg";
|
|
33
|
+
autocomplete?: string;
|
|
34
|
+
inputmode?:
|
|
35
|
+
"text" | "numeric" | "decimal" | "email" | "tel" | "search" | "url";
|
|
36
|
+
class?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const {
|
|
40
|
+
label,
|
|
41
|
+
id,
|
|
42
|
+
name,
|
|
43
|
+
type = "text",
|
|
44
|
+
value,
|
|
45
|
+
placeholder,
|
|
46
|
+
hint,
|
|
47
|
+
error,
|
|
48
|
+
valid = false,
|
|
49
|
+
validate = false,
|
|
50
|
+
disabled = false,
|
|
51
|
+
required = false,
|
|
52
|
+
optional = false,
|
|
53
|
+
multiline = false,
|
|
54
|
+
rows = 3,
|
|
55
|
+
maxlength,
|
|
56
|
+
minlength,
|
|
57
|
+
pattern,
|
|
58
|
+
count = false,
|
|
59
|
+
size = "md",
|
|
60
|
+
autocomplete,
|
|
61
|
+
inputmode,
|
|
62
|
+
class: className,
|
|
63
|
+
} = Astro.props;
|
|
64
|
+
|
|
65
|
+
const fieldId = id ?? `field-${Math.random().toString(36).slice(2, 9)}`;
|
|
66
|
+
const hintId = `${fieldId}-hint`;
|
|
67
|
+
const errorId = `${fieldId}-error`;
|
|
68
|
+
const describedby =
|
|
69
|
+
[hint && (!error || validate) && hintId, (error || validate) && errorId]
|
|
70
|
+
.filter(Boolean)
|
|
71
|
+
.join(" ") || undefined;
|
|
72
|
+
const current = value ?? "";
|
|
73
|
+
const isPassword = !multiline && type === "password";
|
|
74
|
+
const inputClass = [
|
|
75
|
+
"ds-input",
|
|
76
|
+
size !== "md" && `ds-input--${size}`,
|
|
77
|
+
error && "is-invalid",
|
|
78
|
+
valid && !error && "is-valid",
|
|
79
|
+
valid && !error && "ds-input--with-state",
|
|
80
|
+
]
|
|
81
|
+
.filter(Boolean)
|
|
82
|
+
.join(" ");
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
<div
|
|
86
|
+
class:list={["ds-field", isPassword && "ds-field--reveal", className]}
|
|
87
|
+
data-ds-field
|
|
88
|
+
data-message={error}
|
|
89
|
+
>
|
|
90
|
+
{label && (
|
|
91
|
+
<label class="ds-field__label" for={fieldId}>
|
|
92
|
+
{label}
|
|
93
|
+
{optional && <span class="ds-field__optional"> (optional)</span>}
|
|
94
|
+
{required && (
|
|
95
|
+
<span class="ds-field__required" aria-hidden="true">
|
|
96
|
+
{" "}
|
|
97
|
+
*
|
|
98
|
+
</span>
|
|
99
|
+
)}
|
|
100
|
+
</label>
|
|
101
|
+
)}
|
|
102
|
+
|
|
103
|
+
<div class="ds-field__control">
|
|
104
|
+
{multiline ? (
|
|
105
|
+
<textarea
|
|
106
|
+
class={inputClass}
|
|
107
|
+
id={fieldId}
|
|
108
|
+
name={name}
|
|
109
|
+
rows={rows}
|
|
110
|
+
placeholder={placeholder}
|
|
111
|
+
disabled={disabled}
|
|
112
|
+
required={required}
|
|
113
|
+
maxlength={maxlength}
|
|
114
|
+
minlength={minlength}
|
|
115
|
+
aria-invalid={error ? "true" : undefined}
|
|
116
|
+
aria-describedby={describedby}
|
|
117
|
+
autocomplete={autocomplete}
|
|
118
|
+
>{current}</textarea>
|
|
119
|
+
) : (
|
|
120
|
+
<input
|
|
121
|
+
class={inputClass}
|
|
122
|
+
id={fieldId}
|
|
123
|
+
name={name}
|
|
124
|
+
type={type}
|
|
125
|
+
value={value}
|
|
126
|
+
placeholder={placeholder}
|
|
127
|
+
disabled={disabled}
|
|
128
|
+
required={required}
|
|
129
|
+
maxlength={maxlength}
|
|
130
|
+
minlength={minlength}
|
|
131
|
+
pattern={pattern}
|
|
132
|
+
inputmode={inputmode}
|
|
133
|
+
autocomplete={autocomplete}
|
|
134
|
+
aria-invalid={error ? "true" : undefined}
|
|
135
|
+
aria-describedby={describedby}
|
|
136
|
+
/>
|
|
137
|
+
)}
|
|
138
|
+
{valid && !error && (
|
|
139
|
+
<>
|
|
140
|
+
<span class="ds-field__state" aria-hidden="true">
|
|
141
|
+
<Icon name="check" size={14} />
|
|
142
|
+
</span>
|
|
143
|
+
<span class="ds-sr-only">Valid.</span>
|
|
144
|
+
</>
|
|
145
|
+
)}
|
|
146
|
+
{isPassword && (
|
|
147
|
+
<button
|
|
148
|
+
class="ds-field__reveal"
|
|
149
|
+
type="button"
|
|
150
|
+
data-ds-reveal
|
|
151
|
+
aria-label="Show password"
|
|
152
|
+
aria-pressed="false"
|
|
153
|
+
aria-controls={fieldId}
|
|
154
|
+
disabled={disabled}
|
|
155
|
+
>
|
|
156
|
+
<span data-ds-reveal-show aria-hidden="true">
|
|
157
|
+
<Icon name="eye" size={16} />
|
|
158
|
+
</span>
|
|
159
|
+
<span data-ds-reveal-hide aria-hidden="true" hidden>
|
|
160
|
+
<Icon name="eye-off" size={16} />
|
|
161
|
+
</span>
|
|
162
|
+
</button>
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
{((error && !validate) || (hint && (!error || validate))) && (
|
|
167
|
+
<div
|
|
168
|
+
class:list={[error && !validate ? "ds-field__error" : "ds-field__hint"]}
|
|
169
|
+
id={error && !validate ? errorId : hintId}
|
|
170
|
+
>
|
|
171
|
+
{error && !validate && <Icon name="alert-circle" size={14} />}
|
|
172
|
+
{error && !validate ? error : hint}
|
|
173
|
+
</div>
|
|
174
|
+
)}
|
|
175
|
+
|
|
176
|
+
{count && maxlength && (
|
|
177
|
+
<>
|
|
178
|
+
<span class="ds-field__count" data-ds-count>
|
|
179
|
+
{current.length} / {maxlength}
|
|
180
|
+
</span>
|
|
181
|
+
<span class="ds-sr-only" aria-live="polite" data-ds-count-live></span>
|
|
182
|
+
</>
|
|
183
|
+
)}
|
|
184
|
+
|
|
185
|
+
{validate && (
|
|
186
|
+
<div class="ds-field__error" id={errorId} data-ds-error hidden={!error}>
|
|
187
|
+
<Icon name="alert-circle" size={14} />
|
|
188
|
+
<span data-ds-error-text>{error || "Check this field."}</span>
|
|
189
|
+
</div>
|
|
190
|
+
)}
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<script>
|
|
194
|
+
const fields = document.querySelectorAll<HTMLElement>("[data-ds-field]");
|
|
195
|
+
|
|
196
|
+
fields.forEach((field) => {
|
|
197
|
+
const input = field.querySelector<HTMLInputElement | HTMLTextAreaElement>(
|
|
198
|
+
".ds-input",
|
|
199
|
+
);
|
|
200
|
+
const count = field.querySelector<HTMLElement>("[data-ds-count]");
|
|
201
|
+
const countLive = field.querySelector<HTMLElement>("[data-ds-count-live]");
|
|
202
|
+
const error = field.querySelector<HTMLElement>("[data-ds-error]");
|
|
203
|
+
const errorText = field.querySelector<HTMLElement>("[data-ds-error-text]");
|
|
204
|
+
const hint = field.querySelector<HTMLElement>(".ds-field__hint");
|
|
205
|
+
if (!input) return;
|
|
206
|
+
|
|
207
|
+
if (count && input.maxLength > 0) {
|
|
208
|
+
// The visible counter updates silently; screen readers only hear it
|
|
209
|
+
// near the limit, not on every keystroke.
|
|
210
|
+
const updateCount = () => {
|
|
211
|
+
count.textContent = `${input.value.length} / ${input.maxLength}`;
|
|
212
|
+
const left = input.maxLength - input.value.length;
|
|
213
|
+
if (countLive) {
|
|
214
|
+
countLive.textContent =
|
|
215
|
+
left <= 5
|
|
216
|
+
? `${left} ${left === 1 ? "character" : "characters"} left`
|
|
217
|
+
: "";
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
input.addEventListener("input", updateCount);
|
|
221
|
+
updateCount();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (!error || !errorText) return;
|
|
225
|
+
|
|
226
|
+
const message = () => {
|
|
227
|
+
if (input.validity.valueMissing) {
|
|
228
|
+
return input.dataset.requiredMessage || "This field is required.";
|
|
229
|
+
}
|
|
230
|
+
if (input.validity.tooShort) {
|
|
231
|
+
return `Use at least ${input.minLength} characters.`;
|
|
232
|
+
}
|
|
233
|
+
if (input.validity.patternMismatch) {
|
|
234
|
+
return field.dataset.message || "Use the requested format.";
|
|
235
|
+
}
|
|
236
|
+
if (input.validity.typeMismatch) {
|
|
237
|
+
return "Enter a valid value.";
|
|
238
|
+
}
|
|
239
|
+
if (input.validity.tooLong) {
|
|
240
|
+
return `Use at most ${input.maxLength} characters.`;
|
|
241
|
+
}
|
|
242
|
+
return "";
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const apply = (invalid: boolean, text: string) => {
|
|
246
|
+
input.classList.toggle("is-invalid", invalid);
|
|
247
|
+
if (invalid) {
|
|
248
|
+
input.setAttribute("aria-invalid", "true");
|
|
249
|
+
} else {
|
|
250
|
+
input.removeAttribute("aria-invalid");
|
|
251
|
+
}
|
|
252
|
+
error.hidden = !invalid;
|
|
253
|
+
errorText.textContent = text;
|
|
254
|
+
if (hint) hint.hidden = invalid;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const validate = () => {
|
|
258
|
+
const text = message();
|
|
259
|
+
apply(!!text, text);
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
input.addEventListener("blur", validate);
|
|
263
|
+
input.addEventListener("input", () => {
|
|
264
|
+
if (input.getAttribute("aria-invalid") === "true") validate();
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
document
|
|
269
|
+
.querySelectorAll<HTMLElement>("[data-ds-reveal]")
|
|
270
|
+
.forEach((button) => {
|
|
271
|
+
const field = button.closest("[data-ds-field]");
|
|
272
|
+
const input = field?.querySelector<HTMLInputElement>(".ds-input");
|
|
273
|
+
if (!input) return;
|
|
274
|
+
const show = button.querySelector<HTMLElement>("[data-ds-reveal-show]");
|
|
275
|
+
const hide = button.querySelector<HTMLElement>("[data-ds-reveal-hide]");
|
|
276
|
+
|
|
277
|
+
button.addEventListener("click", () => {
|
|
278
|
+
const revealed = input.type === "text";
|
|
279
|
+
input.type = revealed ? "password" : "text";
|
|
280
|
+
// Stable name, state in aria-pressed: "Show password, pressed".
|
|
281
|
+
button.setAttribute("aria-pressed", revealed ? "false" : "true");
|
|
282
|
+
if (show) show.hidden = !revealed;
|
|
283
|
+
if (hide) hide.hidden = revealed;
|
|
284
|
+
input.focus({ preventScroll: true });
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
</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 `@ds/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,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>
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export const ICONS: Record<string, string> = {
|
|
2
|
+
"app-window":
|
|
3
|
+
'<rect x="2" y="4" width="20" height="16" rx="2"/><path d="M2 9h20"/><path d="M6 6.5h.01M9 6.5h.01"/>',
|
|
4
|
+
card: '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 15h18"/>',
|
|
5
|
+
"check-square":
|
|
6
|
+
'<rect x="3" y="3" width="18" height="18" rx="2"/><path d="m8 12 3 3 5-6"/>',
|
|
7
|
+
"chevrons-up-down": '<path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/>',
|
|
8
|
+
"circle-dot":
|
|
9
|
+
'<circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="3"/>',
|
|
10
|
+
loader:
|
|
11
|
+
'<path d="M12 2v4M12 18v4M4.9 4.9l2.8 2.8M16.3 16.3l2.8 2.8M2 12h4M18 12h4M4.9 19.1l2.8-2.8M16.3 7.7l2.8-2.8"/>',
|
|
12
|
+
"message-square":
|
|
13
|
+
'<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2Z"/>',
|
|
14
|
+
"mouse-pointer": '<path d="m4 4 7 17 2.5-7.5L21 11 4 4Z"/>',
|
|
15
|
+
"panel-top":
|
|
16
|
+
'<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/>',
|
|
17
|
+
"rectangle-horizontal": '<rect x="3" y="8" width="18" height="8" rx="2"/>',
|
|
18
|
+
rows: '<rect x="3" y="3" width="18" height="7" rx="1.5"/><rect x="3" y="14" width="18" height="7" rx="1.5"/>',
|
|
19
|
+
"square-dashed":
|
|
20
|
+
'<path d="M5 3a2 2 0 0 0-2 2M19 3a2 2 0 0 1 2 2M21 19a2 2 0 0 1-2 2M5 21a2 2 0 0 1-2-2M9 3h1M9 21h1M14 3h1M14 21h1M3 9v1M21 9v1M3 14v1M21 14v1"/>',
|
|
21
|
+
table:
|
|
22
|
+
'<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M3 15h18M9 9v12"/>',
|
|
23
|
+
tag: '<path d="M12.6 2.6A2 2 0 0 0 11.2 2H4a2 2 0 0 0-2 2v7.2a2 2 0 0 0 .6 1.4l8.7 8.7a2.4 2.4 0 0 0 3.4 0l6.6-6.6a2.4 2.4 0 0 0 0-3.4Z"/><circle cx="7.5" cy="7.5" r="1"/>',
|
|
24
|
+
"text-cursor":
|
|
25
|
+
'<path d="M17 22h-1a4 4 0 0 1-4-4V6a4 4 0 0 1 4-4h1"/><path d="M7 22h1a4 4 0 0 0 4-4v-1"/><path d="M7 2h1a4 4 0 0 1 4 4v1"/>',
|
|
26
|
+
"alert-circle":
|
|
27
|
+
'<circle cx="12" cy="12" r="9"/><path d="M12 8v4"/><path d="M12 16h.01"/>',
|
|
28
|
+
"alert-triangle":
|
|
29
|
+
'<path d="M10.3 3.6 2.4 17a2 2 0 0 0 1.7 3h15.8a2 2 0 0 0 1.7-3L13.7 3.6a2 2 0 0 0-3.4 0Z"/><path d="M12 9v4"/><path d="M12 17h.01"/>',
|
|
30
|
+
"arrow-left": '<path d="m12 19-7-7 7-7"/><path d="M19 12H5"/>',
|
|
31
|
+
"arrow-right": '<path d="M5 12h14"/><path d="m12 5 7 7-7 7"/>',
|
|
32
|
+
"arrow-up-right": '<path d="M7 17 17 7"/><path d="M7 7h10v10"/>',
|
|
33
|
+
activity: '<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>',
|
|
34
|
+
bell: '<path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/>',
|
|
35
|
+
book: '<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2Z"/>',
|
|
36
|
+
box: '<path d="m21 8-9 5-9-5"/><path d="M3 8v8l9 5 9-5V8"/><path d="m12 3 9 5-9 5-9-5 9-5Z"/>',
|
|
37
|
+
check: '<path d="M20 6 9 17l-5-5"/>',
|
|
38
|
+
"check-circle":
|
|
39
|
+
'<circle cx="12" cy="12" r="9"/><path d="m8.5 12 2.5 2.5 4.5-5"/>',
|
|
40
|
+
"chevron-down": '<path d="m6 9 6 6 6-6"/>',
|
|
41
|
+
"chevron-left": '<path d="m15 18-6-6 6-6"/>',
|
|
42
|
+
"chevron-right": '<path d="m9 18 6-6-6-6"/>',
|
|
43
|
+
clock: '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
|
|
44
|
+
command:
|
|
45
|
+
'<path d="M9 6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6Z"/>',
|
|
46
|
+
cpu: '<rect x="6" y="6" width="12" height="12" rx="2"/><path d="M9 2v4M15 2v4M9 18v4M15 18v4M2 9h4M2 15h4M18 9h4M18 15h4"/>',
|
|
47
|
+
database:
|
|
48
|
+
'<ellipse cx="12" cy="5" rx="8" ry="3"/><path d="M4 5v14c0 1.7 3.6 3 8 3s8-1.3 8-3V5"/><path d="M4 12c0 1.7 3.6 3 8 3s8-1.3 8-3"/>',
|
|
49
|
+
download: '<path d="M12 3v12"/><path d="m7 10 5 5 5-5"/><path d="M5 21h14"/>',
|
|
50
|
+
external:
|
|
51
|
+
'<path d="M15 3h6v6"/><path d="m10 14 11-11"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>',
|
|
52
|
+
eye: '<path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/>',
|
|
53
|
+
"eye-off":
|
|
54
|
+
'<path d="m3 3 18 18"/><path d="M10.6 10.6a3 3 0 0 0 4.2 4.2"/><path d="M9.9 5.2A10.5 10.5 0 0 1 12 5c6.5 0 10 7 10 7a17 17 0 0 1-2.2 3.2"/><path d="M6.6 6.6A17 17 0 0 0 2 12s3.5 7 10 7a10.5 10.5 0 0 0 4.1-.8"/>',
|
|
55
|
+
"file-text":
|
|
56
|
+
'<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z"/><path d="M14 2v6h6"/><path d="M8 13h8M8 17h6"/>',
|
|
57
|
+
filter: '<path d="M3 5h18l-7 8v6l-4 2v-8Z"/>',
|
|
58
|
+
"git-branch":
|
|
59
|
+
'<path d="M6 3v12"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/>',
|
|
60
|
+
grid: '<rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/>',
|
|
61
|
+
"hard-drive":
|
|
62
|
+
'<path d="M22 12H2"/><path d="M5.5 5h13L22 12v5a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-5Z"/><path d="M6 16h.01M10 16h.01"/>',
|
|
63
|
+
home: '<path d="m3 10 9-7 9 7v10a1 1 0 0 1-1 1h-5v-7H9v7H4a1 1 0 0 1-1-1Z"/>',
|
|
64
|
+
inbox:
|
|
65
|
+
'<path d="M22 12h-6l-2 3h-4l-2-3H2"/><path d="M5.5 5h13L22 12v5a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-5Z"/>',
|
|
66
|
+
info: '<circle cx="12" cy="12" r="9"/><path d="M12 11v5"/><path d="M12 8h.01"/>',
|
|
67
|
+
layers:
|
|
68
|
+
'<path d="m12 2 9 5-9 5-9-5 9-5Z"/><path d="m3 12 9 5 9-5"/><path d="m3 17 9 5 9-5"/>',
|
|
69
|
+
login:
|
|
70
|
+
'<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><path d="m10 17 5-5-5-5"/><path d="M15 12H3"/>',
|
|
71
|
+
"log-out":
|
|
72
|
+
'<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><path d="m16 17 5-5-5-5"/><path d="M21 12H9"/>',
|
|
73
|
+
menu: '<path d="M4 6h16M4 12h16M4 18h16"/>',
|
|
74
|
+
minus: '<path d="M5 12h14"/>',
|
|
75
|
+
"more-horizontal":
|
|
76
|
+
'<circle cx="5" cy="12" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/>',
|
|
77
|
+
moon: '<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z"/>',
|
|
78
|
+
network:
|
|
79
|
+
'<rect x="9" y="2" width="6" height="6" rx="1"/><rect x="2" y="16" width="6" height="6" rx="1"/><rect x="16" y="16" width="6" height="6" rx="1"/><path d="M12 8v4M5 16v-4h14v4"/>',
|
|
80
|
+
"panel-left":
|
|
81
|
+
'<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v18"/>',
|
|
82
|
+
pause: '<path d="M9 5v14M15 5v14"/>',
|
|
83
|
+
play: '<path d="m7 4 13 8-13 8Z"/>',
|
|
84
|
+
plus: '<path d="M12 5v14M5 12h14"/>',
|
|
85
|
+
refresh:
|
|
86
|
+
'<path d="M20 11a8 8 0 0 0-14.9-4L3 10"/><path d="M3 4v6h6"/><path d="M4 13a8 8 0 0 0 14.9 4L21 14"/><path d="M21 20v-6h-6"/>',
|
|
87
|
+
search: '<circle cx="11" cy="11" r="7"/><path d="m20 20-4-4"/>',
|
|
88
|
+
server:
|
|
89
|
+
'<rect x="3" y="3" width="18" height="7" rx="2"/><rect x="3" y="14" width="18" height="7" rx="2"/><path d="M7 6.5h.01M7 17.5h.01"/>',
|
|
90
|
+
settings:
|
|
91
|
+
'<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.9l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.9-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.9.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.9 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.9l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.9.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.9-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.9V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1Z"/>',
|
|
92
|
+
shield: '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10Z"/>',
|
|
93
|
+
sliders:
|
|
94
|
+
'<path d="M4 21v-7M4 10V3M12 21v-9M12 8V3M20 21v-5M20 12V3"/><path d="M1 14h6M9 8h6M17 16h6"/>',
|
|
95
|
+
sun: '<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/>',
|
|
96
|
+
terminal: '<path d="m4 17 6-6-6-6"/><path d="M12 19h8"/>',
|
|
97
|
+
trash:
|
|
98
|
+
'<path d="M3 6h18"/><path d="M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M10 11v6M14 11v6"/>',
|
|
99
|
+
upload: '<path d="M12 21V9"/><path d="m7 14 5-5 5 5"/><path d="M5 3h14"/>',
|
|
100
|
+
users:
|
|
101
|
+
'<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.9"/><path d="M16 3.1a4 4 0 0 1 0 7.8"/>',
|
|
102
|
+
x: '<path d="M18 6 6 18M6 6l12 12"/>',
|
|
103
|
+
zap: '<path d="M13 2 3 14h9l-1 8 10-12h-9l1-8Z"/>',
|
|
104
|
+
};
|