gbs-add-block 1.2.18 → 1.3.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/README.md +2 -2
- package/index.cjs +28 -7
- package/package.json +1 -1
- package/source/beta-components/accordion/README.md +45 -0
- package/source/beta-components/accordion/__tests__/core.test.ts +58 -0
- package/source/beta-components/accordion/core/index.ts +2 -0
- package/source/beta-components/accordion/core/open.ts +56 -0
- package/source/beta-components/accordion/core/types.ts +17 -0
- package/source/beta-components/accordion/index.ts +8 -0
- package/source/beta-components/accordion/react/Accordion.tsx +133 -0
- package/source/beta-components/accordion/react/AccordionItem.tsx +120 -0
- package/source/beta-components/accordion/react/context.ts +20 -0
- package/source/beta-components/accordion/react/props.ts +13 -0
- package/source/beta-components/accordion/styles.css +208 -0
- package/source/beta-components/alert/README.md +39 -0
- package/source/beta-components/alert/__tests__/core.test.ts +17 -0
- package/source/beta-components/alert/core/index.ts +2 -0
- package/source/beta-components/alert/core/live.ts +25 -0
- package/source/beta-components/alert/core/types.ts +7 -0
- package/source/beta-components/alert/index.ts +5 -0
- package/source/beta-components/alert/react/Alert.tsx +113 -0
- package/source/beta-components/alert/react/icons.tsx +21 -0
- package/source/beta-components/alert/react/locale.ts +5 -0
- package/source/beta-components/alert/react/props.ts +11 -0
- package/source/beta-components/alert/styles.css +155 -0
- package/source/beta-components/avatar/__tests__/core.test.ts +88 -0
- package/source/beta-components/avatar/core/identity.ts +103 -0
- package/source/beta-components/avatar/core/index.ts +2 -0
- package/source/beta-components/avatar/core/types.ts +12 -0
- package/source/beta-components/avatar/index.ts +7 -0
- package/source/beta-components/avatar/react/Avatar.tsx +118 -0
- package/source/beta-components/avatar/react/AvatarGroup.tsx +74 -0
- package/source/beta-components/avatar/react/locale.ts +11 -0
- package/source/beta-components/avatar/react/props.ts +6 -0
- package/source/beta-components/avatar/styles.css +227 -0
- package/source/beta-components/badge/README.md +37 -0
- package/source/beta-components/badge/__tests__/core.test.ts +46 -0
- package/source/beta-components/badge/core/count.ts +37 -0
- package/source/beta-components/badge/core/index.ts +2 -0
- package/source/beta-components/badge/core/types.ts +11 -0
- package/source/beta-components/badge/index.ts +7 -0
- package/source/beta-components/badge/react/Badge.tsx +89 -0
- package/source/beta-components/badge/react/Tag.tsx +82 -0
- package/source/beta-components/badge/react/locale.ts +5 -0
- package/source/beta-components/badge/react/props.ts +6 -0
- package/source/beta-components/badge/styles.css +204 -0
- package/source/beta-components/number-input/README.md +53 -0
- package/source/beta-components/number-input/__tests__/core.test.ts +151 -0
- package/source/beta-components/number-input/core/format.ts +37 -0
- package/source/beta-components/number-input/core/index.ts +4 -0
- package/source/beta-components/number-input/core/parse.ts +118 -0
- package/source/beta-components/number-input/core/step.ts +73 -0
- package/source/beta-components/number-input/core/types.ts +32 -0
- package/source/beta-components/number-input/index.ts +5 -0
- package/source/beta-components/number-input/react/NumberInput.tsx +465 -0
- package/source/beta-components/number-input/react/icons.tsx +17 -0
- package/source/beta-components/number-input/react/locale.ts +7 -0
- package/source/beta-components/number-input/react/props.ts +11 -0
- package/source/beta-components/number-input/styles.css +231 -0
- package/source/beta-components/progress/README.md +38 -0
- package/source/beta-components/progress/__tests__/core.test.ts +56 -0
- package/source/beta-components/progress/core/index.ts +2 -0
- package/source/beta-components/progress/core/progress.ts +40 -0
- package/source/beta-components/progress/core/types.ts +23 -0
- package/source/beta-components/progress/index.ts +7 -0
- package/source/beta-components/progress/react/CircularProgress.tsx +106 -0
- package/source/beta-components/progress/react/Progress.tsx +113 -0
- package/source/beta-components/progress/react/locale.ts +7 -0
- package/source/beta-components/progress/react/props.ts +6 -0
- package/source/beta-components/progress/styles.css +202 -0
- package/source/beta-components/radio-group/README.md +58 -0
- package/source/beta-components/radio-group/__tests__/core.test.ts +64 -0
- package/source/beta-components/radio-group/core/group.ts +36 -0
- package/source/beta-components/radio-group/core/index.ts +2 -0
- package/source/beta-components/radio-group/core/types.ts +20 -0
- package/source/beta-components/radio-group/index.ts +9 -0
- package/source/beta-components/radio-group/react/Radio.tsx +144 -0
- package/source/beta-components/radio-group/react/RadioGroup.tsx +179 -0
- package/source/beta-components/radio-group/react/context.ts +18 -0
- package/source/beta-components/radio-group/react/locale.ts +5 -0
- package/source/beta-components/radio-group/react/props.ts +12 -0
- package/source/beta-components/radio-group/styles.css +270 -0
- package/source/beta-components/shared/__tests__/position.test.ts +143 -170
- package/source/beta-components/shared/core/position.ts +2 -15
- package/source/beta-components/shared/react/Popover.tsx +5 -18
- package/source/beta-components/switch/README.md +46 -0
- package/source/beta-components/switch/__tests__/core.test.ts +64 -0
- package/source/beta-components/switch/core/index.ts +2 -0
- package/source/beta-components/switch/core/toggle.ts +54 -0
- package/source/beta-components/switch/core/types.ts +9 -0
- package/source/beta-components/switch/index.ts +5 -0
- package/source/beta-components/switch/react/Switch.tsx +219 -0
- package/source/beta-components/switch/react/locale.ts +5 -0
- package/source/beta-components/switch/react/props.ts +12 -0
- package/source/beta-components/switch/styles.css +229 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Accordion styles.
|
|
3
|
+
*
|
|
4
|
+
* Each section is a native <details>, so the marker has to be taken off (two
|
|
5
|
+
* ways: `list-item` display in Firefox, the WebKit pseudo-element elsewhere)
|
|
6
|
+
* before a chevron of our own goes on. Tokens read the shared --gbs-*
|
|
7
|
+
* variables when they are set on an ancestor (set them on :root to theme every
|
|
8
|
+
* component at once). Rules live in the `components` layer, so utility classes
|
|
9
|
+
* passed through `className` / `classNames` override them.
|
|
10
|
+
*/
|
|
11
|
+
@layer theme, base, components, utilities;
|
|
12
|
+
|
|
13
|
+
@layer components {
|
|
14
|
+
.ac-root {
|
|
15
|
+
--ac-fg: var(--gbs-fg, var(--dg-fg, light-dark(#18181b, #f4f4f5)));
|
|
16
|
+
--ac-muted: var(--gbs-muted, var(--dg-muted, light-dark(#71717a, #a1a1aa)));
|
|
17
|
+
--ac-bg: var(--gbs-bg, var(--dg-bg, light-dark(#ffffff, #0b0b0e)));
|
|
18
|
+
--ac-border: var(--gbs-border, var(--dg-border, light-dark(#e4e4e7, #27272a)));
|
|
19
|
+
--ac-hover: var(--gbs-hover, var(--dg-hover, light-dark(#f4f4f5, #1f1f23)));
|
|
20
|
+
--ac-focus: var(--gbs-focus, var(--dg-focus, light-dark(#2563eb, #60a5fa)));
|
|
21
|
+
--ac-radius: var(--gbs-radius, var(--dg-radius, 8px));
|
|
22
|
+
--ac-font-size: var(--gbs-font-size, var(--dg-font-size, 13px));
|
|
23
|
+
--ac-px: 14px;
|
|
24
|
+
--ac-py: 12px;
|
|
25
|
+
--ac-duration: 180ms;
|
|
26
|
+
|
|
27
|
+
color-scheme: inherit;
|
|
28
|
+
display: flex;
|
|
29
|
+
flex-direction: column;
|
|
30
|
+
gap: 8px;
|
|
31
|
+
min-width: 0;
|
|
32
|
+
font-size: var(--ac-font-size);
|
|
33
|
+
line-height: 1.5;
|
|
34
|
+
color: var(--ac-fg);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
:where(.dark, [data-theme="dark"]) .ac-root {
|
|
38
|
+
color-scheme: dark;
|
|
39
|
+
}
|
|
40
|
+
:where(.light, [data-theme="light"]) .ac-root {
|
|
41
|
+
color-scheme: light;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
:where(.ac-root) *,
|
|
45
|
+
:where(.ac-root) *::before,
|
|
46
|
+
:where(.ac-root) *::after {
|
|
47
|
+
box-sizing: border-box;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.ac-root[data-size="sm"] {
|
|
51
|
+
--ac-px: 10px;
|
|
52
|
+
--ac-py: 8px;
|
|
53
|
+
--ac-font-size: 12px;
|
|
54
|
+
}
|
|
55
|
+
.ac-root[data-size="lg"] {
|
|
56
|
+
--ac-px: 18px;
|
|
57
|
+
--ac-py: 16px;
|
|
58
|
+
--ac-font-size: 14px;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* ---------------------------------------------------------------- item */
|
|
62
|
+
|
|
63
|
+
.ac-item {
|
|
64
|
+
min-width: 0;
|
|
65
|
+
border: 1px solid var(--ac-border);
|
|
66
|
+
border-radius: var(--ac-radius);
|
|
67
|
+
background: var(--ac-bg);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* Contained: one block with dividers instead of separate cards. */
|
|
71
|
+
.ac-root[data-variant="contained"] {
|
|
72
|
+
gap: 0;
|
|
73
|
+
border: 1px solid var(--ac-border);
|
|
74
|
+
border-radius: var(--ac-radius);
|
|
75
|
+
overflow: hidden;
|
|
76
|
+
}
|
|
77
|
+
.ac-root[data-variant="contained"] .ac-item {
|
|
78
|
+
border: 0;
|
|
79
|
+
border-block-end: 1px solid var(--ac-border);
|
|
80
|
+
border-radius: 0;
|
|
81
|
+
}
|
|
82
|
+
.ac-root[data-variant="contained"] .ac-item:last-child {
|
|
83
|
+
border-block-end: 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* Plain: rules only, for a sidebar or a FAQ. */
|
|
87
|
+
.ac-root[data-variant="plain"] {
|
|
88
|
+
gap: 0;
|
|
89
|
+
}
|
|
90
|
+
.ac-root[data-variant="plain"] .ac-item {
|
|
91
|
+
border: 0;
|
|
92
|
+
border-block-end: 1px solid var(--ac-border);
|
|
93
|
+
border-radius: 0;
|
|
94
|
+
background: none;
|
|
95
|
+
}
|
|
96
|
+
.ac-root[data-variant="plain"] .ac-header {
|
|
97
|
+
padding-inline: 0;
|
|
98
|
+
}
|
|
99
|
+
.ac-root[data-variant="plain"] .ac-body {
|
|
100
|
+
padding-inline: 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* -------------------------------------------------------------- header */
|
|
104
|
+
|
|
105
|
+
.ac-header {
|
|
106
|
+
display: flex;
|
|
107
|
+
align-items: center;
|
|
108
|
+
gap: 10px;
|
|
109
|
+
padding: var(--ac-py) var(--ac-px);
|
|
110
|
+
border-radius: inherit;
|
|
111
|
+
cursor: pointer;
|
|
112
|
+
user-select: none;
|
|
113
|
+
/* Firefox draws the triangle through `display: list-item`. */
|
|
114
|
+
list-style: none;
|
|
115
|
+
}
|
|
116
|
+
.ac-header::-webkit-details-marker {
|
|
117
|
+
display: none;
|
|
118
|
+
}
|
|
119
|
+
.ac-header:hover {
|
|
120
|
+
background: var(--ac-hover);
|
|
121
|
+
}
|
|
122
|
+
.ac-header:focus-visible {
|
|
123
|
+
outline: 2px solid var(--ac-focus);
|
|
124
|
+
outline-offset: -2px;
|
|
125
|
+
}
|
|
126
|
+
.ac-item[data-disabled] .ac-header {
|
|
127
|
+
cursor: not-allowed;
|
|
128
|
+
}
|
|
129
|
+
.ac-item[data-disabled] .ac-title {
|
|
130
|
+
color: var(--ac-muted);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.ac-heading {
|
|
134
|
+
display: flex;
|
|
135
|
+
flex: 1 1 auto;
|
|
136
|
+
flex-direction: column;
|
|
137
|
+
gap: 1px;
|
|
138
|
+
min-width: 0;
|
|
139
|
+
}
|
|
140
|
+
.ac-title {
|
|
141
|
+
font-weight: 600;
|
|
142
|
+
}
|
|
143
|
+
.ac-description {
|
|
144
|
+
color: var(--ac-muted);
|
|
145
|
+
font-weight: 400;
|
|
146
|
+
}
|
|
147
|
+
.ac-meta {
|
|
148
|
+
display: inline-flex;
|
|
149
|
+
flex: none;
|
|
150
|
+
align-items: center;
|
|
151
|
+
gap: 6px;
|
|
152
|
+
color: var(--ac-muted);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.ac-icon {
|
|
156
|
+
display: inline-flex;
|
|
157
|
+
flex: none;
|
|
158
|
+
color: var(--ac-muted);
|
|
159
|
+
transition: rotate var(--ac-duration) ease;
|
|
160
|
+
}
|
|
161
|
+
.ac-icon > svg {
|
|
162
|
+
width: 16px;
|
|
163
|
+
height: 16px;
|
|
164
|
+
}
|
|
165
|
+
.ac-item[open] > .ac-header .ac-icon {
|
|
166
|
+
rotate: 180deg;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* ------------------------------------------------------------- content */
|
|
170
|
+
|
|
171
|
+
.ac-body {
|
|
172
|
+
padding: 0 var(--ac-px) var(--ac-py);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/*
|
|
176
|
+
* Opening animates where the browser can do it: `interpolate-size` lets a
|
|
177
|
+
* height of `auto` be interpolated, and `::details-content` is the box to
|
|
178
|
+
* animate. Browsers without either simply show and hide, which is the
|
|
179
|
+
* behaviour everything else falls back to anyway.
|
|
180
|
+
*/
|
|
181
|
+
@supports (interpolate-size: allow-keywords) {
|
|
182
|
+
.ac-root {
|
|
183
|
+
interpolate-size: allow-keywords;
|
|
184
|
+
}
|
|
185
|
+
.ac-item::details-content {
|
|
186
|
+
height: 0;
|
|
187
|
+
overflow: hidden;
|
|
188
|
+
transition:
|
|
189
|
+
height var(--ac-duration) ease,
|
|
190
|
+
content-visibility var(--ac-duration) allow-discrete;
|
|
191
|
+
}
|
|
192
|
+
.ac-item[open]::details-content {
|
|
193
|
+
height: auto;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
@media (prefers-reduced-motion: reduce) {
|
|
198
|
+
.ac-root {
|
|
199
|
+
--ac-duration: 0s;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
@media (forced-colors: active) {
|
|
204
|
+
.ac-item {
|
|
205
|
+
border: 1px solid CanvasText;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Alert
|
|
2
|
+
|
|
3
|
+
A message that stays in the page: a trial ending, a read-only record, the
|
|
4
|
+
summary above a form that failed validation. For React 19, styled to match the
|
|
5
|
+
rest of the library. No runtime dependencies besides React.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { Alert } from "@/components/alert";
|
|
9
|
+
import "@/components/alert/styles.css";
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
<Alert variant="warning" title="Your trial ends in 3 days">
|
|
14
|
+
After that the workspace becomes read-only. <a href="/billing">Add a card</a> to keep it.
|
|
15
|
+
</Alert>
|
|
16
|
+
|
|
17
|
+
<Alert variant="danger" title="We could not save your changes" onDismiss={() => setShown(false)} />
|
|
18
|
+
|
|
19
|
+
<Alert variant="success" size="sm">240 rows imported.</Alert>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Props:** `variant` (`info` `success` `warning` `danger` `neutral`), `size`
|
|
23
|
+
(`sm` `md`), `title`, `description` or `children`, `icon` (a node, or `false`),
|
|
24
|
+
`actions`, `onDismiss`, `classNames` (`root` `icon` `content` `title`
|
|
25
|
+
`description` `actions` `dismiss`), `localeText`, `ref`, and every `<div>`
|
|
26
|
+
attribute.
|
|
27
|
+
|
|
28
|
+
Which one to reach for:
|
|
29
|
+
|
|
30
|
+
| | Interrupts | Lives | Use for |
|
|
31
|
+
| --- | --- | --- | --- |
|
|
32
|
+
| **Dialog** | Yes, blocks | Top layer | A decision that must happen now |
|
|
33
|
+
| **Toast** | No, and leaves | A corner | Confirming what already happened |
|
|
34
|
+
| **Alert** | No, and stays | In the layout | The state a page or section is in |
|
|
35
|
+
|
|
36
|
+
`liveness(variant)` is exported from the framework-free `core`: problems get
|
|
37
|
+
`role="alert"` and interrupt; everything else gets `role="status"` and waits its
|
|
38
|
+
turn. An alert that was on the page from the start announces nothing either
|
|
39
|
+
way, because a live region only speaks when its contents change.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { liveness } from "../core/live";
|
|
3
|
+
|
|
4
|
+
describe("liveness", () => {
|
|
5
|
+
it("interrupts for problems", () => {
|
|
6
|
+
// Someone whose card was declined needs to hear it now, not after the
|
|
7
|
+
// paragraph being read finishes.
|
|
8
|
+
expect(liveness("danger")).toEqual({ role: "alert", live: "assertive" });
|
|
9
|
+
expect(liveness("warning")).toEqual({ role: "alert", live: "assertive" });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("waits its turn for everything else", () => {
|
|
13
|
+
for (const variant of ["info", "success", "neutral"] as const) {
|
|
14
|
+
expect(liveness(variant)).toEqual({ role: "status", live: "polite" });
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { AlertVariant } from "./types";
|
|
2
|
+
|
|
3
|
+
export interface Liveness {
|
|
4
|
+
/** `alert` interrupts whatever is being read; `status` waits its turn. */
|
|
5
|
+
role: "alert" | "status";
|
|
6
|
+
live: "assertive" | "polite";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* How loudly an alert announces itself.
|
|
11
|
+
*
|
|
12
|
+
* A banner that appears after the page has loaded is news, and a screen reader
|
|
13
|
+
* has to be told whether it is worth interrupting for. Problems are:
|
|
14
|
+
* `role="alert"` cuts into whatever is being read, which is right for "your
|
|
15
|
+
* card was declined" and rude for "3 rows imported".
|
|
16
|
+
*
|
|
17
|
+
* A banner that was on the page from the start announces nothing either way —
|
|
18
|
+
* a live region only speaks when its contents change — so this only decides
|
|
19
|
+
* what happens when one appears or its text is replaced.
|
|
20
|
+
*/
|
|
21
|
+
export function liveness(variant: AlertVariant): Liveness {
|
|
22
|
+
return variant === "danger" || variant === "warning"
|
|
23
|
+
? { role: "alert", live: "assertive" }
|
|
24
|
+
: { role: "status", live: "polite" };
|
|
25
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
useMemo,
|
|
5
|
+
type CSSProperties,
|
|
6
|
+
type HTMLAttributes,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
type Ref,
|
|
9
|
+
} from "react";
|
|
10
|
+
import { liveness } from "../core/live";
|
|
11
|
+
import type { AlertLocaleText, AlertSize, AlertVariant } from "../core/types";
|
|
12
|
+
import { VARIANT_ICONS, XIcon } from "./icons";
|
|
13
|
+
import { defaultAlertText } from "./locale";
|
|
14
|
+
import { cx, type AlertSlot } from "./props";
|
|
15
|
+
|
|
16
|
+
export interface AlertProps extends Omit<HTMLAttributes<HTMLDivElement>, "title" | "role"> {
|
|
17
|
+
/** Default `info`. */
|
|
18
|
+
variant?: AlertVariant;
|
|
19
|
+
/** Default `md`. `sm` drops the title to one line of text. */
|
|
20
|
+
size?: AlertSize;
|
|
21
|
+
title?: ReactNode;
|
|
22
|
+
/** The message. `children` works too, and is the better place for links. */
|
|
23
|
+
description?: ReactNode;
|
|
24
|
+
children?: ReactNode;
|
|
25
|
+
/** Replace the variant's glyph, or pass `false` for none. */
|
|
26
|
+
icon?: ReactNode | false;
|
|
27
|
+
/** Buttons under the message: "Try again", "Go to billing". */
|
|
28
|
+
actions?: ReactNode;
|
|
29
|
+
/** Show a close button. The alert does not hide itself; remove it in the handler. */
|
|
30
|
+
onDismiss?(): void;
|
|
31
|
+
className?: string;
|
|
32
|
+
classNames?: Partial<Record<AlertSlot, string>>;
|
|
33
|
+
style?: CSSProperties;
|
|
34
|
+
localeText?: Partial<AlertLocaleText>;
|
|
35
|
+
ref?: Ref<HTMLDivElement>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A message that stays in the page: a trial ending, a read-only record, the
|
|
40
|
+
* summary above a form that failed validation.
|
|
41
|
+
*
|
|
42
|
+
* It is the third of three ways to say something, and the only one that is
|
|
43
|
+
* neither interrupting nor temporary. Use a **Dialog** when the message needs a
|
|
44
|
+
* decision before anything else can happen, a **Toast** to confirm something
|
|
45
|
+
* that has already happened, and an Alert for the state a page or section is
|
|
46
|
+
* in.
|
|
47
|
+
*/
|
|
48
|
+
export function Alert(props: AlertProps) {
|
|
49
|
+
const {
|
|
50
|
+
ref,
|
|
51
|
+
variant = "info",
|
|
52
|
+
size = "md",
|
|
53
|
+
title,
|
|
54
|
+
description,
|
|
55
|
+
children,
|
|
56
|
+
icon,
|
|
57
|
+
actions,
|
|
58
|
+
onDismiss,
|
|
59
|
+
className,
|
|
60
|
+
classNames,
|
|
61
|
+
style,
|
|
62
|
+
localeText,
|
|
63
|
+
...rest
|
|
64
|
+
} = props;
|
|
65
|
+
|
|
66
|
+
const text = useMemo(() => ({ ...defaultAlertText, ...localeText }), [localeText]);
|
|
67
|
+
const { role, live } = liveness(variant);
|
|
68
|
+
const Glyph = VARIANT_ICONS[variant];
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div
|
|
72
|
+
{...rest}
|
|
73
|
+
ref={ref}
|
|
74
|
+
// Only what appears after the page has loaded is announced; a banner that
|
|
75
|
+
// was there from the start is read in its turn, like any other text.
|
|
76
|
+
role={role}
|
|
77
|
+
aria-live={live}
|
|
78
|
+
className={cx("al-root", classNames?.root, className)}
|
|
79
|
+
style={style}
|
|
80
|
+
data-variant={variant}
|
|
81
|
+
data-size={size}
|
|
82
|
+
>
|
|
83
|
+
{icon !== false && (
|
|
84
|
+
<span className={cx("al-icon", classNames?.icon)} aria-hidden="true">
|
|
85
|
+
{icon ?? <Glyph />}
|
|
86
|
+
</span>
|
|
87
|
+
)}
|
|
88
|
+
|
|
89
|
+
<div className={cx("al-content", classNames?.content)}>
|
|
90
|
+
{title && <p className={cx("al-title", classNames?.title)}>{title}</p>}
|
|
91
|
+
{(description || children) && (
|
|
92
|
+
<div className={cx("al-description", classNames?.description)}>
|
|
93
|
+
{description}
|
|
94
|
+
{children}
|
|
95
|
+
</div>
|
|
96
|
+
)}
|
|
97
|
+
{actions && <div className={cx("al-actions", classNames?.actions)}>{actions}</div>}
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
{onDismiss && (
|
|
101
|
+
<button
|
|
102
|
+
type="button"
|
|
103
|
+
className={cx("al-dismiss", classNames?.dismiss)}
|
|
104
|
+
aria-label={text.dismiss}
|
|
105
|
+
title={text.dismiss}
|
|
106
|
+
onClick={onDismiss}
|
|
107
|
+
>
|
|
108
|
+
<XIcon />
|
|
109
|
+
</button>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AlertCircleIcon,
|
|
3
|
+
InfoIcon,
|
|
4
|
+
SuccessIcon,
|
|
5
|
+
WarningIcon,
|
|
6
|
+
XIcon as SharedX,
|
|
7
|
+
type IconProps,
|
|
8
|
+
} from "../../shared/react/icons";
|
|
9
|
+
import type { AlertVariant } from "../core/types";
|
|
10
|
+
|
|
11
|
+
/** 14px, to sit in the corner without pulling the eye from the message. */
|
|
12
|
+
export const XIcon = (p: IconProps) => <SharedX width={14} height={14} {...p} />;
|
|
13
|
+
|
|
14
|
+
/** The glyph each variant carries, so colour is never the only signal. */
|
|
15
|
+
export const VARIANT_ICONS: Record<AlertVariant, (props: IconProps) => React.JSX.Element> = {
|
|
16
|
+
info: InfoIcon,
|
|
17
|
+
success: SuccessIcon,
|
|
18
|
+
warning: WarningIcon,
|
|
19
|
+
danger: AlertCircleIcon,
|
|
20
|
+
neutral: InfoIcon,
|
|
21
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Alert styles.
|
|
3
|
+
*
|
|
4
|
+
* A banner that sits in the page, tinted by variant. Tokens read the shared
|
|
5
|
+
* --gbs-* variables when they are set on an ancestor (set them on :root to
|
|
6
|
+
* theme every component at once). Rules live in the `components` layer, so
|
|
7
|
+
* utility classes passed through `className` / `classNames` override them.
|
|
8
|
+
*/
|
|
9
|
+
@layer theme, base, components, utilities;
|
|
10
|
+
|
|
11
|
+
@layer components {
|
|
12
|
+
.al-root {
|
|
13
|
+
--al-fg: var(--gbs-fg, var(--dg-fg, light-dark(#18181b, #f4f4f5)));
|
|
14
|
+
--al-muted: var(--gbs-muted, var(--dg-muted, light-dark(#71717a, #a1a1aa)));
|
|
15
|
+
--al-border: var(--gbs-border, var(--dg-border, light-dark(#e4e4e7, #27272a)));
|
|
16
|
+
--al-bg: var(--gbs-bg, var(--dg-bg, light-dark(#ffffff, #0b0b0e)));
|
|
17
|
+
--al-hover: var(--gbs-hover, var(--dg-hover, light-dark(#f4f4f5, #1f1f23)));
|
|
18
|
+
--al-focus: var(--gbs-focus, var(--dg-focus, light-dark(#2563eb, #60a5fa)));
|
|
19
|
+
--al-info: var(--gbs-info, var(--gbs-accent, var(--dg-accent, light-dark(#2563eb, #60a5fa))));
|
|
20
|
+
--al-success: var(--gbs-success, light-dark(#15803d, #4ade80));
|
|
21
|
+
--al-warning: var(--gbs-warning, light-dark(#b45309, #fbbf24));
|
|
22
|
+
--al-danger: var(--gbs-danger, var(--dg-danger, light-dark(#dc2626, #f87171)));
|
|
23
|
+
--al-radius: var(--gbs-radius, var(--dg-radius, 8px));
|
|
24
|
+
--al-font-size: var(--gbs-font-size, var(--dg-font-size, 13px));
|
|
25
|
+
--al-accent: var(--al-info);
|
|
26
|
+
--al-px: 14px;
|
|
27
|
+
--al-py: 12px;
|
|
28
|
+
|
|
29
|
+
color-scheme: inherit;
|
|
30
|
+
position: relative;
|
|
31
|
+
display: flex;
|
|
32
|
+
align-items: flex-start;
|
|
33
|
+
gap: 10px;
|
|
34
|
+
min-width: 0;
|
|
35
|
+
padding: var(--al-py) var(--al-px);
|
|
36
|
+
/* The bar carries the variant at a glance; the tint alone is too faint to
|
|
37
|
+
read as a signal, and colour is never the only one — there is a glyph. */
|
|
38
|
+
border: 1px solid var(--al-border);
|
|
39
|
+
border-inline-start: 3px solid var(--al-accent);
|
|
40
|
+
border-radius: var(--al-radius);
|
|
41
|
+
background: color-mix(in oklab, var(--al-accent) 7%, var(--al-bg));
|
|
42
|
+
font-size: var(--al-font-size);
|
|
43
|
+
line-height: 1.5;
|
|
44
|
+
color: var(--al-fg);
|
|
45
|
+
text-wrap: pretty;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
:where(.dark, [data-theme="dark"]) .al-root {
|
|
49
|
+
color-scheme: dark;
|
|
50
|
+
}
|
|
51
|
+
:where(.light, [data-theme="light"]) .al-root {
|
|
52
|
+
color-scheme: light;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
:where(.al-root) *,
|
|
56
|
+
:where(.al-root) *::before,
|
|
57
|
+
:where(.al-root) *::after {
|
|
58
|
+
box-sizing: border-box;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.al-root[data-variant="success"] {
|
|
62
|
+
--al-accent: var(--al-success);
|
|
63
|
+
}
|
|
64
|
+
.al-root[data-variant="warning"] {
|
|
65
|
+
--al-accent: var(--al-warning);
|
|
66
|
+
}
|
|
67
|
+
.al-root[data-variant="danger"] {
|
|
68
|
+
--al-accent: var(--al-danger);
|
|
69
|
+
}
|
|
70
|
+
.al-root[data-variant="neutral"] {
|
|
71
|
+
--al-accent: var(--al-muted);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.al-root[data-size="sm"] {
|
|
75
|
+
--al-px: 10px;
|
|
76
|
+
--al-py: 8px;
|
|
77
|
+
--al-font-size: 12px;
|
|
78
|
+
gap: 8px;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.al-icon {
|
|
82
|
+
display: inline-flex;
|
|
83
|
+
flex: none;
|
|
84
|
+
/* Optically centred on the first line of text rather than its box. */
|
|
85
|
+
margin-top: 1px;
|
|
86
|
+
color: var(--al-accent);
|
|
87
|
+
}
|
|
88
|
+
.al-icon > svg {
|
|
89
|
+
width: 16px;
|
|
90
|
+
height: 16px;
|
|
91
|
+
}
|
|
92
|
+
.al-root[data-size="sm"] .al-icon > svg {
|
|
93
|
+
width: 14px;
|
|
94
|
+
height: 14px;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.al-content {
|
|
98
|
+
display: flex;
|
|
99
|
+
flex: 1 1 auto;
|
|
100
|
+
flex-direction: column;
|
|
101
|
+
gap: 2px;
|
|
102
|
+
min-width: 0;
|
|
103
|
+
}
|
|
104
|
+
.al-title {
|
|
105
|
+
margin: 0;
|
|
106
|
+
font-weight: 600;
|
|
107
|
+
}
|
|
108
|
+
.al-description {
|
|
109
|
+
margin: 0;
|
|
110
|
+
/* Not muted: this is the message, not a hint about one. */
|
|
111
|
+
color: inherit;
|
|
112
|
+
}
|
|
113
|
+
.al-description a {
|
|
114
|
+
color: var(--al-accent);
|
|
115
|
+
text-underline-offset: 2px;
|
|
116
|
+
}
|
|
117
|
+
.al-actions {
|
|
118
|
+
display: flex;
|
|
119
|
+
flex-wrap: wrap;
|
|
120
|
+
align-items: center;
|
|
121
|
+
gap: 8px;
|
|
122
|
+
margin-top: 8px;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.al-dismiss {
|
|
126
|
+
display: inline-flex;
|
|
127
|
+
flex: none;
|
|
128
|
+
align-items: center;
|
|
129
|
+
justify-content: center;
|
|
130
|
+
width: 22px;
|
|
131
|
+
height: 22px;
|
|
132
|
+
margin: -2px -4px 0 0;
|
|
133
|
+
padding: 0;
|
|
134
|
+
border: 0;
|
|
135
|
+
border-radius: 4px;
|
|
136
|
+
background: none;
|
|
137
|
+
color: var(--al-muted);
|
|
138
|
+
cursor: pointer;
|
|
139
|
+
}
|
|
140
|
+
.al-dismiss:hover {
|
|
141
|
+
background: var(--al-hover);
|
|
142
|
+
color: var(--al-fg);
|
|
143
|
+
}
|
|
144
|
+
.al-dismiss:focus-visible {
|
|
145
|
+
outline: 2px solid var(--al-focus);
|
|
146
|
+
outline-offset: 1px;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@media (forced-colors: active) {
|
|
150
|
+
.al-root {
|
|
151
|
+
border: 1px solid CanvasText;
|
|
152
|
+
border-inline-start-width: 3px;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|