@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.
Files changed (86) hide show
  1. package/package.json +28 -4
  2. package/src/astro/Accordion.astro +37 -0
  3. package/src/astro/Alert.astro +73 -0
  4. package/src/astro/Avatar.astro +88 -0
  5. package/src/astro/AvatarGroup.astro +63 -0
  6. package/src/astro/Badge.astro +23 -0
  7. package/src/astro/Breadcrumbs.astro +45 -0
  8. package/src/astro/Button.astro +125 -0
  9. package/src/astro/Card.astro +63 -0
  10. package/src/astro/Checkbox.astro +61 -0
  11. package/src/astro/Chip.astro +135 -0
  12. package/src/astro/CodeBlock.astro +17 -0
  13. package/src/astro/Drawer.astro +30 -0
  14. package/src/astro/EmptyState.astro +45 -0
  15. package/src/astro/Example.astro +37 -0
  16. package/src/astro/Grid.astro +56 -0
  17. package/src/astro/GridItem.astro +25 -0
  18. package/src/astro/Icon.astro +52 -0
  19. package/src/astro/Menu.astro +151 -0
  20. package/src/astro/MenuItem.astro +57 -0
  21. package/src/astro/Modal.astro +124 -0
  22. package/src/astro/Navbar.astro +756 -0
  23. package/src/astro/PageHeader.astro +34 -0
  24. package/src/astro/Pagination.astro +212 -0
  25. package/src/astro/Popover.astro +51 -0
  26. package/src/astro/Progress.astro +71 -0
  27. package/src/astro/Radio.astro +58 -0
  28. package/src/astro/RadioGroup.astro +25 -0
  29. package/src/astro/Section.astro +25 -0
  30. package/src/astro/Select.astro +420 -0
  31. package/src/astro/Sidebar.astro +215 -0
  32. package/src/astro/Skeleton.astro +41 -0
  33. package/src/astro/Spinner.astro +20 -0
  34. package/src/astro/Stepper.astro +82 -0
  35. package/src/astro/Switch.astro +78 -0
  36. package/src/astro/Table.astro +268 -0
  37. package/src/astro/Tabs.astro +83 -0
  38. package/src/astro/TextField.astro +305 -0
  39. package/src/astro/ThemeToggle.astro +66 -0
  40. package/src/astro/Toast.astro +181 -0
  41. package/src/astro/Tooltip.astro +54 -0
  42. package/src/astro/TopBar.astro +29 -0
  43. package/src/data/icons.ts +120 -0
  44. package/src/scripts/avatar.ts +33 -0
  45. package/src/scripts/menu.ts +218 -0
  46. package/src/scripts/tabs.ts +127 -0
  47. package/src/scripts/toast.ts +565 -0
  48. package/src/scripts/tooltip.ts +127 -0
  49. package/src/styles/accordion.css +79 -0
  50. package/src/styles/alert.css +108 -0
  51. package/src/styles/avatar.css +131 -0
  52. package/src/styles/badge.css +48 -0
  53. package/src/styles/base.css +205 -0
  54. package/src/styles/breadcrumbs.css +54 -0
  55. package/src/styles/button.css +174 -0
  56. package/src/styles/card.css +112 -0
  57. package/src/styles/chip.css +106 -0
  58. package/src/styles/choice.css +143 -0
  59. package/src/styles/code.css +44 -0
  60. package/src/styles/empty.css +59 -0
  61. package/src/styles/example.css +57 -0
  62. package/src/styles/grid.css +64 -0
  63. package/src/styles/input.css +208 -0
  64. package/src/styles/menu.css +151 -0
  65. package/src/styles/modal.css +285 -0
  66. package/src/styles/motion.css +114 -0
  67. package/src/styles/nav.css +232 -0
  68. package/src/styles/navbar.css +658 -0
  69. package/src/styles/page-header.css +62 -0
  70. package/src/styles/pagination.css +81 -0
  71. package/src/styles/popover.css +134 -0
  72. package/src/styles/progress.css +95 -0
  73. package/src/styles/section.css +38 -0
  74. package/src/styles/select.css +246 -0
  75. package/src/styles/skeleton.css +66 -0
  76. package/src/styles/spinner.css +44 -0
  77. package/src/styles/sr-only.css +14 -0
  78. package/src/styles/stepper.css +169 -0
  79. package/src/styles/switch.css +143 -0
  80. package/src/styles/table.css +177 -0
  81. package/src/styles/tabs.css +120 -0
  82. package/src/styles/theme-toggle.css +31 -0
  83. package/src/styles/toast.css +225 -0
  84. package/src/styles/tooltip.css +63 -0
  85. package/src/styles/topbar.css +32 -0
  86. package/README.md +0 -3
@@ -0,0 +1,82 @@
1
+ ---
2
+ import Icon from "./Icon.astro";
3
+ import "../styles/stepper.css";
4
+ import "../styles/sr-only.css";
5
+
6
+ /**
7
+ * Progress through an ordered process. Presentational: an <ol> where the
8
+ * current step carries aria-current="step". State is shown by marker shape
9
+ * (number, check, cross) and read out as visually hidden text, never by
10
+ * colour alone.
11
+ */
12
+ interface Step {
13
+ label: string;
14
+ description?: string;
15
+ state?: "complete" | "current" | "upcoming" | "error";
16
+ }
17
+
18
+ interface Props {
19
+ steps: Step[];
20
+ orientation?: "horizontal" | "vertical";
21
+ /** Accessible name for the list, e.g. "Setup progress". */
22
+ label?: string;
23
+ class?: string;
24
+ }
25
+
26
+ const {
27
+ steps,
28
+ orientation = "horizontal",
29
+ label,
30
+ class: className,
31
+ } = Astro.props;
32
+
33
+ const STATE_TEXT = {
34
+ complete: "Completed",
35
+ error: "Error",
36
+ upcoming: "Not started",
37
+ current: "",
38
+ };
39
+ ---
40
+
41
+ <ol
42
+ class:list={[
43
+ "ds-stepper",
44
+ orientation === "vertical" && "ds-stepper--vertical",
45
+ className,
46
+ ]}
47
+ aria-label={label}
48
+ >
49
+ {steps.map((step, index) => {
50
+ const state = step.state ?? "upcoming";
51
+ return (
52
+ <li
53
+ class="ds-stepper__item"
54
+ data-state={state}
55
+ aria-current={state === "current" ? "step" : undefined}
56
+ >
57
+ <span class="ds-stepper__marker" aria-hidden="true">
58
+ {state === "complete" ? (
59
+ <Icon name="check" size={16} />
60
+ ) : state === "error" ? (
61
+ <Icon name="x" size={16} />
62
+ ) : (
63
+ index + 1
64
+ )}
65
+ </span>
66
+ <span class="ds-stepper__body">
67
+ <span class="ds-stepper__label">
68
+ {step.label}
69
+ {STATE_TEXT[state] && (
70
+ <span class="ds-sr-only">
71
+ , {STATE_TEXT[state].toLowerCase()}
72
+ </span>
73
+ )}
74
+ </span>
75
+ {step.description && (
76
+ <span class="ds-stepper__description">{step.description}</span>
77
+ )}
78
+ </span>
79
+ </li>
80
+ );
81
+ })}
82
+ </ol>
@@ -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>
@@ -0,0 +1,268 @@
1
+ ---
2
+ import Icon from "./Icon.astro";
3
+ import "../styles/sr-only.css";
4
+ import "../styles/input.css";
5
+ import "../styles/table.css";
6
+
7
+ type Cell =
8
+ | string
9
+ | number
10
+ | {
11
+ text: string | number;
12
+ mono?: boolean;
13
+ kbd?: boolean;
14
+ swatch?: string;
15
+ };
16
+
17
+ interface Column {
18
+ key: string;
19
+ label: string;
20
+ align?: "start" | "end";
21
+ numeric?: boolean;
22
+ }
23
+
24
+ interface Props {
25
+ columns: Column[];
26
+ rows: Record<string, Cell>[];
27
+ caption: string;
28
+ hideCaption?: boolean;
29
+ empty?: string;
30
+ sortable?: boolean;
31
+ filterable?: boolean;
32
+ filterLabel?: string;
33
+ filterPlaceholder?: string;
34
+ class?: string;
35
+ }
36
+
37
+ const {
38
+ columns,
39
+ rows,
40
+ caption,
41
+ hideCaption = false,
42
+ empty = "No records yet.",
43
+ sortable = false,
44
+ filterable = false,
45
+ filterLabel = "Filter rows",
46
+ filterPlaceholder = "Filter…",
47
+ class: className,
48
+ } = Astro.props;
49
+
50
+ const filterId = `table-filter-${Math.random().toString(36).slice(2, 9)}`;
51
+ ---
52
+
53
+ <div class:list={["ds-table-wrap", className]} data-ds-table>
54
+ {filterable && (
55
+ <div class="ds-table__toolbar">
56
+ <label class="ds-sr-only" for={filterId}>
57
+ {filterLabel}
58
+ </label>
59
+ <input
60
+ class="ds-input ds-input--sm ds-table__filter"
61
+ id={filterId}
62
+ type="search"
63
+ placeholder={filterPlaceholder}
64
+ data-table-filter
65
+ />
66
+ <span class="ds-table__count" aria-live="polite" data-table-count>
67
+ {rows.length} {rows.length === 1 ? "row" : "rows"}
68
+ </span>
69
+ </div>
70
+ )}
71
+ <table class:list={["ds-table", columns.length >= 4 && "ds-table--wide"]}>
72
+ <caption class:list={["ds-table__caption", hideCaption && "ds-sr-only"]}>
73
+ {caption}
74
+ </caption>
75
+ <thead>
76
+ <tr>
77
+ {columns.map((column, index) => (
78
+ <th
79
+ scope="col"
80
+ class:list={[
81
+ column.numeric && "is-numeric",
82
+ column.align === "end" && "is-end",
83
+ ]}
84
+ aria-sort={sortable ? "none" : undefined}
85
+ >
86
+ {sortable ? (
87
+ <button
88
+ class="ds-table__sort-button"
89
+ type="button"
90
+ data-sort-index={index}
91
+ >
92
+ {column.label}
93
+ <span class="ds-table__sort" aria-hidden="true">
94
+ <Icon
95
+ name="chevrons-up-down"
96
+ size={14}
97
+ class="ds-table__sort-idle"
98
+ />
99
+ <Icon
100
+ name="chevron-down"
101
+ size={14}
102
+ class="ds-table__sort-dir"
103
+ />
104
+ </span>
105
+ </button>
106
+ ) : (
107
+ column.label
108
+ )}
109
+ </th>
110
+ ))}
111
+ </tr>
112
+ </thead>
113
+ <tbody>
114
+ {rows.length === 0 ? (
115
+ <tr>
116
+ <td class="ds-table__empty" colspan={columns.length}>
117
+ {empty}
118
+ </td>
119
+ </tr>
120
+ ) : (
121
+ rows.map((row) => (
122
+ <tr data-row>
123
+ {columns.map((column) => {
124
+ const cell = row[column.key];
125
+ return (
126
+ <td
127
+ class:list={[
128
+ column.numeric && "is-numeric",
129
+ column.align === "end" && "is-end",
130
+ ]}
131
+ >
132
+ {typeof cell === "object" ? (
133
+ <>
134
+ {cell.swatch && (
135
+ <span
136
+ class="ds-table__swatch"
137
+ style={`background:${/^(#[0-9a-f]{3,8}|var\(--[a-z0-9-]+\)|[a-z]+)$/i.test(cell.swatch) ? cell.swatch : "transparent"}`}
138
+ aria-hidden="true"
139
+ ></span>
140
+ )}
141
+ {cell.kbd ? (
142
+ <kbd>{cell.text}</kbd>
143
+ ) : (
144
+ <span class:list={[cell.mono && "ds-table__mono"]}>
145
+ {cell.text}
146
+ </span>
147
+ )}
148
+ </>
149
+ ) : (
150
+ cell
151
+ )}
152
+ </td>
153
+ );
154
+ })}
155
+ </tr>
156
+ ))
157
+ )}
158
+ {filterable && rows.length > 0 && (
159
+ <tr data-table-empty hidden>
160
+ <td class="ds-table__empty" colspan={columns.length}>
161
+ No matching rows.
162
+ </td>
163
+ </tr>
164
+ )}
165
+ </tbody>
166
+ </table>
167
+ </div>
168
+
169
+ <script>
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;
186
+
187
+ const rows = Array.from(
188
+ tbody.querySelectorAll<HTMLTableRowElement>("tr[data-row]"),
189
+ );
190
+ let sortIndex = -1;
191
+ let direction = 1;
192
+
193
+ const textFor = (row: HTMLTableRowElement, index: number) =>
194
+ (row.cells[index]?.textContent || "").trim();
195
+
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
+ };
201
+
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
+ };
208
+
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
+ }
228
+
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
+ });
259
+ });
260
+
261
+ filter?.addEventListener("input", apply);
262
+ apply();
263
+ });
264
+ };
265
+
266
+ bind();
267
+ document.addEventListener("astro:page-load", bind);
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>