@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,34 @@
1
+ ---
2
+ import "../styles/page-header.css";
3
+
4
+ /**
5
+ * Page header. One h1, an optional lede, metadata and actions. There is no
6
+ * eyebrow: the breadcrumb in the navbar already says where the page sits.
7
+ * `display` is for the single landing page per site, not for every route.
8
+ */
9
+ interface Props {
10
+ title?: string;
11
+ lede?: string;
12
+ display?: boolean;
13
+ class?: string;
14
+ }
15
+
16
+ const { title, lede, display = false, class: className } = Astro.props;
17
+ ---
18
+
19
+ <header
20
+ class:list={[
21
+ "ds-page-header",
22
+ display && "ds-page-header--display",
23
+ className,
24
+ ]}
25
+ >
26
+ <h1 class="ds-page-header__title">{title ?? <slot name="title" />}</h1>
27
+ <p class="ds-page-header__lede">{lede ?? <slot name="lede" />}</p>
28
+ <div class="ds-page-header__meta">
29
+ <slot name="meta" />
30
+ </div>
31
+ <div class="ds-page-header__actions">
32
+ <slot name="actions" />
33
+ </div>
34
+ </header>
@@ -0,0 +1,212 @@
1
+ ---
2
+ import Icon from "./Icon.astro";
3
+ import "../styles/pagination.css";
4
+
5
+ interface Props {
6
+ current: number;
7
+ total: number;
8
+ href?: string;
9
+ label?: string;
10
+ interactive?: boolean;
11
+ class?: string;
12
+ }
13
+
14
+ const {
15
+ current,
16
+ total,
17
+ href = "?page={page}",
18
+ label = "Pagination",
19
+ interactive = false,
20
+ class: className,
21
+ } = Astro.props;
22
+
23
+ const urlFor = (page: number) => href.replace("{page}", String(page));
24
+
25
+ type PageToken = number | "ellipsis-start" | "ellipsis-end";
26
+ const tokens: PageToken[] = [];
27
+ if (total <= 7) {
28
+ for (let page = 1; page <= total; page++) tokens.push(page);
29
+ } else {
30
+ tokens.push(1);
31
+ if (current > 4) tokens.push("ellipsis-start");
32
+ const start = Math.max(2, current - 1);
33
+ const end = Math.min(total - 1, current + 1);
34
+ for (let page = start; page <= end; page++) tokens.push(page);
35
+ if (current < total - 3) tokens.push("ellipsis-end");
36
+ tokens.push(total);
37
+ }
38
+
39
+ const Tag = interactive ? "button" : "a";
40
+ const pageProps = (page: number) =>
41
+ interactive
42
+ ? { type: "button" as const, "data-page": page }
43
+ : { href: urlFor(page), "data-page": page };
44
+ ---
45
+
46
+ <nav
47
+ class:list={[
48
+ "ds-pagination",
49
+ interactive && "ds-pagination--interactive",
50
+ className,
51
+ ]}
52
+ aria-label={label}
53
+ data-pagination-interactive={interactive ? "true" : undefined}
54
+ data-total={total}
55
+ data-current={current}
56
+ >
57
+ <ul class="ds-pagination__list">
58
+ <li>
59
+ <Tag
60
+ class="ds-pagination__link ds-pagination__link--direction"
61
+ {...pageProps(current - 1)}
62
+ aria-disabled={current <= 1 ? "true" : undefined}
63
+ disabled={interactive && current <= 1 ? true : undefined}
64
+ data-direction="prev"
65
+ rel={interactive ? undefined : "prev"}
66
+ >
67
+ <Icon name="chevron-left" size={14} />
68
+ <span class="ds-pagination__label">Previous</span>
69
+ </Tag>
70
+ </li>
71
+ {tokens.map((token) =>
72
+ typeof token === "number" ? (
73
+ <li data-token>
74
+ <Tag
75
+ class="ds-pagination__link"
76
+ {...pageProps(token)}
77
+ aria-current={token === current ? "page" : undefined}
78
+ aria-label={`Page ${token}`}
79
+ >
80
+ {token}
81
+ </Tag>
82
+ </li>
83
+ ) : (
84
+ <li data-token>
85
+ <span class="ds-pagination__ellipsis" aria-hidden="true">
86
+ …
87
+ </span>
88
+ </li>
89
+ ),
90
+ )}
91
+ <li>
92
+ <Tag
93
+ class="ds-pagination__link ds-pagination__link--direction"
94
+ {...pageProps(current + 1)}
95
+ aria-disabled={current >= total ? "true" : undefined}
96
+ disabled={interactive && current >= total ? true : undefined}
97
+ data-direction="next"
98
+ rel={interactive ? undefined : "next"}
99
+ >
100
+ <span class="ds-pagination__label">Next</span>
101
+ <Icon name="chevron-right" size={14} />
102
+ </Tag>
103
+ </li>
104
+ </ul>
105
+ </nav>
106
+
107
+ <script>
108
+ type PageToken = number | "ellipsis-start" | "ellipsis-end";
109
+
110
+ // Same windowing as the server render in the frontmatter.
111
+ const buildTokens = (current: number, total: number): PageToken[] => {
112
+ const tokens: PageToken[] = [];
113
+ if (total <= 7) {
114
+ for (let page = 1; page <= total; page++) tokens.push(page);
115
+ return tokens;
116
+ }
117
+ tokens.push(1);
118
+ if (current > 4) tokens.push("ellipsis-start");
119
+ const start = Math.max(2, current - 1);
120
+ const end = Math.min(total - 1, current + 1);
121
+ for (let page = start; page <= end; page++) tokens.push(page);
122
+ if (current < total - 3) tokens.push("ellipsis-end");
123
+ tokens.push(total);
124
+ return tokens;
125
+ };
126
+
127
+ const renderToken = (token: PageToken, current: number): HTMLLIElement => {
128
+ const item = document.createElement("li");
129
+ item.dataset.token = "";
130
+ if (typeof token !== "number") {
131
+ const ellipsis = document.createElement("span");
132
+ ellipsis.className = "ds-pagination__ellipsis";
133
+ ellipsis.setAttribute("aria-hidden", "true");
134
+ ellipsis.textContent = "\u2026";
135
+ item.append(ellipsis);
136
+ return item;
137
+ }
138
+ const button = document.createElement("button");
139
+ button.type = "button";
140
+ button.className = "ds-pagination__link";
141
+ button.dataset.page = String(token);
142
+ button.setAttribute("aria-label", `Page ${token}`);
143
+ if (token === current) button.setAttribute("aria-current", "page");
144
+ button.textContent = String(token);
145
+ item.append(button);
146
+ return item;
147
+ };
148
+
149
+ const render = (nav: HTMLElement, page: number) => {
150
+ const total = Number(nav.dataset.total);
151
+ nav.dataset.current = String(page);
152
+ const previous = nav.querySelector<HTMLButtonElement>(
153
+ '[data-direction="prev"]',
154
+ );
155
+ const next = nav.querySelector<HTMLButtonElement>(
156
+ '[data-direction="next"]',
157
+ );
158
+ const nextItem = next?.closest("li");
159
+ if (!previous || !next || !nextItem) return;
160
+
161
+ const hadFocus = nav.contains(document.activeElement);
162
+ nav.querySelectorAll("li[data-token]").forEach((item) => item.remove());
163
+ buildTokens(page, total).forEach((token) =>
164
+ nextItem.before(renderToken(token, page)),
165
+ );
166
+
167
+ previous.dataset.page = String(Math.max(1, page - 1));
168
+ previous.disabled = page <= 1;
169
+ previous.setAttribute("aria-disabled", page <= 1 ? "true" : "false");
170
+ next.dataset.page = String(Math.min(total, page + 1));
171
+ next.disabled = page >= total;
172
+ next.setAttribute("aria-disabled", page >= total ? "true" : "false");
173
+
174
+ // Page buttons are re-created and a direction button can become
175
+ // disabled; hand focus to the current page when the control is gone.
176
+ const focused = document.activeElement;
177
+ const stillUsable =
178
+ focused instanceof HTMLButtonElement &&
179
+ nav.contains(focused) &&
180
+ !focused.disabled;
181
+ if (hadFocus && !stillUsable) {
182
+ nav.querySelector<HTMLElement>('[aria-current="page"]')?.focus();
183
+ }
184
+ };
185
+
186
+ const bind = () => {
187
+ document
188
+ .querySelectorAll<HTMLElement>("[data-pagination-interactive]")
189
+ .forEach((nav) => {
190
+ if (nav.dataset.bound) return;
191
+ nav.dataset.bound = "true";
192
+ nav.addEventListener("click", (event) => {
193
+ const control = (event.target as HTMLElement).closest<HTMLElement>(
194
+ "[data-page]",
195
+ );
196
+ if (!control || control.hasAttribute("disabled")) return;
197
+ event.preventDefault();
198
+ const page = Number(control.dataset.page);
199
+ render(nav, page);
200
+ nav.dispatchEvent(
201
+ new CustomEvent("page-change", {
202
+ bubbles: true,
203
+ detail: { page },
204
+ }),
205
+ );
206
+ });
207
+ });
208
+ };
209
+
210
+ bind();
211
+ document.addEventListener("astro:page-load", bind);
212
+ </script>
@@ -0,0 +1,51 @@
1
+ ---
2
+ import Button from "./Button.astro";
3
+ import "../styles/popover.css";
4
+
5
+ /**
6
+ * Popover on the native `popover` attribute. The trigger's `popovertarget`
7
+ * supplies toggling, light dismiss, Escape and focus return, so no script is
8
+ * needed. Placement uses CSS anchor positioning and flips near the viewport
9
+ * edge. For a list of commands use Menu; for a hint use Tooltip.
10
+ */
11
+ interface Props {
12
+ /** Accessible name for the surface. */
13
+ label: string;
14
+ triggerLabel?: string;
15
+ triggerVariant?: "primary" | "secondary" | "ghost" | "danger";
16
+ placement?: "top" | "bottom" | "start" | "end";
17
+ id?: string;
18
+ class?: string;
19
+ }
20
+
21
+ const {
22
+ label,
23
+ triggerLabel = "Open",
24
+ triggerVariant = "secondary",
25
+ placement = "bottom",
26
+ id,
27
+ class: className,
28
+ } = Astro.props;
29
+
30
+ const surfaceId = id ?? `popover-${Math.random().toString(36).slice(2, 9)}`;
31
+ ---
32
+
33
+ <span class="ds-anchor">
34
+ <Button
35
+ variant={triggerVariant}
36
+ type="button"
37
+ popovertarget={surfaceId}
38
+ aria-haspopup="dialog"
39
+ >
40
+ {triggerLabel}
41
+ </Button>
42
+ <div
43
+ class:list={["ds-popover", `ds-popover--${placement}`, className]}
44
+ id={surfaceId}
45
+ popover="auto"
46
+ role="dialog"
47
+ aria-label={label}
48
+ >
49
+ <slot />
50
+ </div>
51
+ </span>
@@ -0,0 +1,71 @@
1
+ ---
2
+ import "../styles/progress.css";
3
+
4
+ /**
5
+ * Progress bar. Determinate when `value` is set, indeterminate otherwise.
6
+ * Uses role="progressbar" rather than <progress> so the bar can animate with
7
+ * a transform and fall back to a static bar under reduced motion.
8
+ */
9
+ interface Props {
10
+ label: string;
11
+ /** Current value, 0 to `max`. Omit for an indeterminate wait. */
12
+ value?: number;
13
+ max?: number;
14
+ /** Overrides the visible and announced value, e.g. "3 of 5 files". */
15
+ valueText?: string;
16
+ tone?: "accent" | "success" | "warning" | "danger";
17
+ size?: "sm" | "md";
18
+ id?: string;
19
+ class?: string;
20
+ }
21
+
22
+ const {
23
+ label,
24
+ value,
25
+ max = 100,
26
+ valueText,
27
+ tone = "accent",
28
+ size = "md",
29
+ id,
30
+ class: className,
31
+ } = Astro.props;
32
+
33
+ const labelId = `${id ?? `progress-${Math.random().toString(36).slice(2, 9)}`}-label`;
34
+ const determinate = value !== undefined;
35
+ const clamped = determinate ? Math.min(Math.max(value, 0), max) : 0;
36
+ const fraction = max > 0 ? clamped / max : 0;
37
+ const text =
38
+ valueText ?? (determinate ? `${Math.round(fraction * 100)}%` : undefined);
39
+ ---
40
+
41
+ <div
42
+ class:list={[
43
+ "ds-progress",
44
+ tone !== "accent" && `ds-progress--${tone}`,
45
+ size !== "md" && `ds-progress--${size}`,
46
+ !determinate && "ds-progress--indeterminate",
47
+ className,
48
+ ]}
49
+ id={id}
50
+ >
51
+ <div class="ds-progress__head">
52
+ <span class="ds-progress__label" id={labelId}>
53
+ {label}
54
+ </span>
55
+ {text && <span class="ds-progress__value">{text}</span>}
56
+ </div>
57
+ <div
58
+ class="ds-progress__track"
59
+ role="progressbar"
60
+ aria-labelledby={labelId}
61
+ aria-valuemin="0"
62
+ aria-valuemax={max}
63
+ aria-valuenow={determinate ? clamped : undefined}
64
+ aria-valuetext={text}
65
+ >
66
+ <div
67
+ class="ds-progress__bar"
68
+ style={determinate ? `--ds-progress-value: ${fraction}` : undefined}
69
+ ></div>
70
+ </div>
71
+ </div>
@@ -0,0 +1,58 @@
1
+ ---
2
+ import "../styles/choice.css";
3
+
4
+ /** Radio. Wrap a set in RadioGroup for a native fieldset and legend. */
5
+ interface Props {
6
+ label: string;
7
+ description?: string;
8
+ id?: string;
9
+ name: string;
10
+ value: string;
11
+ checked?: boolean;
12
+ disabled?: boolean;
13
+ required?: boolean;
14
+ class?: string;
15
+ }
16
+
17
+ const {
18
+ label,
19
+ description,
20
+ id,
21
+ name,
22
+ value,
23
+ checked = false,
24
+ disabled = false,
25
+ required = false,
26
+ class: className,
27
+ } = Astro.props;
28
+
29
+ const inputId = id ?? `radio-${Math.random().toString(36).slice(2, 9)}`;
30
+ const descriptionId = description ? `${inputId}-description` : undefined;
31
+ // The name comes from the label text only; the description is announced
32
+ // once, as a description, instead of twice (name + description).
33
+ const labelId = `${inputId}-label`;
34
+ ---
35
+
36
+ <label class:list={["ds-choice", "ds-choice--radio", className]}>
37
+ <input
38
+ class="ds-choice__input"
39
+ id={inputId}
40
+ type="radio"
41
+ name={name}
42
+ value={value}
43
+ checked={checked}
44
+ disabled={disabled}
45
+ required={required}
46
+ aria-labelledby={description ? labelId : undefined}
47
+ aria-describedby={descriptionId}
48
+ />
49
+ <span class="ds-choice__box" aria-hidden="true"></span>
50
+ <span class="ds-choice__label">
51
+ <span id={labelId}>{label}</span>
52
+ {description && (
53
+ <span class="ds-choice__description" id={descriptionId}>
54
+ {description}
55
+ </span>
56
+ )}
57
+ </span>
58
+ </label>
@@ -0,0 +1,25 @@
1
+ ---
2
+ import "../styles/choice.css";
3
+
4
+ /** Native fieldset/legend grouping for radios or checkboxes. */
5
+ interface Props {
6
+ legend: string;
7
+ hint?: string;
8
+ class?: string;
9
+ }
10
+
11
+ const { legend, hint, class: className } = Astro.props;
12
+ const hintId = hint
13
+ ? `group-${Math.random().toString(36).slice(2, 9)}-hint`
14
+ : undefined;
15
+ ---
16
+
17
+ <fieldset class:list={["ds-choice-group", className]} aria-describedby={hintId}>
18
+ <legend class="ds-choice-group__legend">{legend}</legend>
19
+ {hint && (
20
+ <p class="ds-choice-group__hint" id={hintId}>
21
+ {hint}
22
+ </p>
23
+ )}
24
+ <slot />
25
+ </fieldset>
@@ -0,0 +1,25 @@
1
+ ---
2
+ import "../styles/section.css";
3
+
4
+ /** Numbered documentation section: heading, mono index and optional intro. */
5
+ interface Props {
6
+ title: string;
7
+ id: string;
8
+ index?: string;
9
+ intro?: string;
10
+ class?: string;
11
+ }
12
+
13
+ const { title, id, index, intro, class: className } = Astro.props;
14
+ ---
15
+
16
+ <section class:list={["ds-section", className]} aria-labelledby={id}>
17
+ <div class="ds-section__head">
18
+ <h2 class="ds-section__title" id={id}>
19
+ {title}
20
+ </h2>
21
+ {index && <span class="ds-section__index">{index}</span>}
22
+ </div>
23
+ {intro && <p class="ds-section__intro">{intro}</p>}
24
+ <slot />
25
+ </section>