rastack 0.0.25 → 0.0.27
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/CHANGELOG.md +4 -0
- package/components/auto-form/AutoForm.tsx +254 -0
- package/components/auto-form/fields.ts +108 -0
- package/components/auto-form/use-auto-form.ts +89 -0
- package/components/data-table/DataTable.tsx +267 -0
- package/components/data-table/columns.ts +117 -0
- package/components/data-table/use-data-table.ts +141 -0
- package/components/index.ts +77 -0
- package/components/primitives/controls.tsx +291 -0
- package/components/primitives/field.tsx +117 -0
- package/components/primitives/index.ts +11 -0
- package/components/primitives/misc.tsx +173 -0
- package/components/primitives/table.tsx +147 -0
- package/components/style.ts +195 -0
- package/components/types.ts +98 -0
- package/components/util.ts +14 -0
- package/dist/rastack-wasm-build.d.ts +1 -1
- package/dist/rastack-wasm-build.js +3 -3
- package/hooks/form/form.ts +10 -4
- package/hooks/form/interfaces.ts +40 -1
- package/hooks/form/structure.ts +83 -26
- package/hooks/query/delete.ts +3 -1
- package/package.json +1 -1
- package/src/rastack-wasm-build.ts +3 -3
- package/test/components.spec.ts +386 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
tokens,
|
|
4
|
+
mergeStyles,
|
|
5
|
+
controlBaseStyle,
|
|
6
|
+
controlSizing,
|
|
7
|
+
focusRingStyle,
|
|
8
|
+
invalidControlStyle,
|
|
9
|
+
buttonVariantStyle,
|
|
10
|
+
type ButtonVariant,
|
|
11
|
+
type ControlSize,
|
|
12
|
+
} from "../style";
|
|
13
|
+
import { Spinner } from "./misc";
|
|
14
|
+
|
|
15
|
+
/** Track focus so a control can apply the shared token focus ring on the web. */
|
|
16
|
+
function useFocusRing<
|
|
17
|
+
E extends { onFocus?: unknown; onBlur?: unknown },
|
|
18
|
+
>(handlers: {
|
|
19
|
+
onFocus?: React.FocusEventHandler<never>;
|
|
20
|
+
onBlur?: React.FocusEventHandler<never>;
|
|
21
|
+
}): {
|
|
22
|
+
focused: boolean;
|
|
23
|
+
onFocus: React.FocusEventHandler<never>;
|
|
24
|
+
onBlur: React.FocusEventHandler<never>;
|
|
25
|
+
} {
|
|
26
|
+
const [focused, setFocused] = useState(false);
|
|
27
|
+
return {
|
|
28
|
+
focused,
|
|
29
|
+
onFocus: (e) => {
|
|
30
|
+
setFocused(true);
|
|
31
|
+
handlers.onFocus?.(e);
|
|
32
|
+
},
|
|
33
|
+
onBlur: (e) => {
|
|
34
|
+
setFocused(false);
|
|
35
|
+
handlers.onBlur?.(e);
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// --- Button -------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
43
|
+
variant?: ButtonVariant;
|
|
44
|
+
size?: ControlSize;
|
|
45
|
+
/** Show a spinner and block interaction. */
|
|
46
|
+
loading?: boolean;
|
|
47
|
+
/** Stretch to the container width. */
|
|
48
|
+
block?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A token-styled button with five variants and three sizes. */
|
|
52
|
+
export function Button({
|
|
53
|
+
variant = "primary",
|
|
54
|
+
size = "md",
|
|
55
|
+
loading = false,
|
|
56
|
+
block = false,
|
|
57
|
+
disabled = false,
|
|
58
|
+
style,
|
|
59
|
+
children,
|
|
60
|
+
...rest
|
|
61
|
+
}: ButtonProps): React.JSX.Element {
|
|
62
|
+
const s = controlSizing[size];
|
|
63
|
+
const isDisabled = disabled || loading;
|
|
64
|
+
const base: React.CSSProperties = {
|
|
65
|
+
display: block ? "flex" : "inline-flex",
|
|
66
|
+
width: block ? "100%" : undefined,
|
|
67
|
+
alignItems: "center",
|
|
68
|
+
justifyContent: "center",
|
|
69
|
+
gap: tokens.space.sm,
|
|
70
|
+
padding: `${s.paddingY} ${s.paddingX}`,
|
|
71
|
+
minHeight: s.height,
|
|
72
|
+
fontFamily: tokens.font.sans,
|
|
73
|
+
fontSize: s.fontSize,
|
|
74
|
+
fontWeight: tokens.font.weightMedium as unknown as number,
|
|
75
|
+
lineHeight: 1,
|
|
76
|
+
borderRadius: tokens.radius.md,
|
|
77
|
+
cursor: isDisabled ? "not-allowed" : "pointer",
|
|
78
|
+
opacity: isDisabled ? 0.6 : 1,
|
|
79
|
+
transition: `background ${tokens.duration.fast} ease, opacity ${tokens.duration.fast} ease`,
|
|
80
|
+
...buttonVariantStyle[variant],
|
|
81
|
+
};
|
|
82
|
+
return (
|
|
83
|
+
<button
|
|
84
|
+
type="button"
|
|
85
|
+
style={mergeStyles(base, style)}
|
|
86
|
+
disabled={isDisabled}
|
|
87
|
+
{...rest}
|
|
88
|
+
>
|
|
89
|
+
{loading ? (
|
|
90
|
+
<Spinner
|
|
91
|
+
size={Math.round(parseFloat(s.fontSize) * 14)}
|
|
92
|
+
color="currentColor"
|
|
93
|
+
/>
|
|
94
|
+
) : null}
|
|
95
|
+
{children}
|
|
96
|
+
</button>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// --- Input --------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
export interface InputProps extends Omit<
|
|
103
|
+
React.InputHTMLAttributes<HTMLInputElement>,
|
|
104
|
+
"size"
|
|
105
|
+
> {
|
|
106
|
+
size?: ControlSize;
|
|
107
|
+
/** Paint the invalid (danger) border. */
|
|
108
|
+
invalid?: boolean;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** A token-styled text input (also `type="number"`, `email`, `date`, …). */
|
|
112
|
+
export function Input({
|
|
113
|
+
size = "md",
|
|
114
|
+
invalid = false,
|
|
115
|
+
style,
|
|
116
|
+
onFocus,
|
|
117
|
+
onBlur,
|
|
118
|
+
...rest
|
|
119
|
+
}: InputProps): React.JSX.Element {
|
|
120
|
+
const ring = useFocusRing({ onFocus, onBlur });
|
|
121
|
+
const base: React.CSSProperties = {
|
|
122
|
+
...controlBaseStyle(size),
|
|
123
|
+
...(invalid ? invalidControlStyle : null),
|
|
124
|
+
...(ring.focused ? focusRingStyle : null),
|
|
125
|
+
};
|
|
126
|
+
return (
|
|
127
|
+
<input
|
|
128
|
+
style={mergeStyles(base, style)}
|
|
129
|
+
onFocus={ring.onFocus}
|
|
130
|
+
onBlur={ring.onBlur}
|
|
131
|
+
{...rest}
|
|
132
|
+
/>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// --- Textarea -----------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|
139
|
+
size?: ControlSize;
|
|
140
|
+
invalid?: boolean;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** A token-styled multi-line text input. */
|
|
144
|
+
export function Textarea({
|
|
145
|
+
size = "md",
|
|
146
|
+
invalid = false,
|
|
147
|
+
style,
|
|
148
|
+
onFocus,
|
|
149
|
+
onBlur,
|
|
150
|
+
rows = 3,
|
|
151
|
+
...rest
|
|
152
|
+
}: TextareaProps): React.JSX.Element {
|
|
153
|
+
const ring = useFocusRing({ onFocus, onBlur });
|
|
154
|
+
const base: React.CSSProperties = {
|
|
155
|
+
...controlBaseStyle(size),
|
|
156
|
+
...(invalid ? invalidControlStyle : null),
|
|
157
|
+
...(ring.focused ? focusRingStyle : null),
|
|
158
|
+
resize: "vertical",
|
|
159
|
+
minHeight: "auto",
|
|
160
|
+
height: "auto",
|
|
161
|
+
};
|
|
162
|
+
return (
|
|
163
|
+
<textarea
|
|
164
|
+
rows={rows}
|
|
165
|
+
style={mergeStyles(base, style)}
|
|
166
|
+
onFocus={ring.onFocus}
|
|
167
|
+
onBlur={ring.onBlur}
|
|
168
|
+
{...rest}
|
|
169
|
+
/>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// --- Select -------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
export interface SelectOption {
|
|
176
|
+
label: string;
|
|
177
|
+
value: string | number;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export interface SelectProps extends Omit<
|
|
181
|
+
React.SelectHTMLAttributes<HTMLSelectElement>,
|
|
182
|
+
"size"
|
|
183
|
+
> {
|
|
184
|
+
size?: ControlSize;
|
|
185
|
+
invalid?: boolean;
|
|
186
|
+
/** Options to render. Omit and pass `<option>` children for full control. */
|
|
187
|
+
options?: SelectOption[];
|
|
188
|
+
/** A leading, empty, disabled option — the "choose…" affordance. */
|
|
189
|
+
placeholder?: string;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** A token-styled native `<select>`. */
|
|
193
|
+
export function Select({
|
|
194
|
+
size = "md",
|
|
195
|
+
invalid = false,
|
|
196
|
+
options,
|
|
197
|
+
placeholder,
|
|
198
|
+
style,
|
|
199
|
+
onFocus,
|
|
200
|
+
onBlur,
|
|
201
|
+
children,
|
|
202
|
+
...rest
|
|
203
|
+
}: SelectProps): React.JSX.Element {
|
|
204
|
+
const ring = useFocusRing({ onFocus, onBlur });
|
|
205
|
+
const base: React.CSSProperties = {
|
|
206
|
+
...controlBaseStyle(size),
|
|
207
|
+
...(invalid ? invalidControlStyle : null),
|
|
208
|
+
...(ring.focused ? focusRingStyle : null),
|
|
209
|
+
appearance: "none",
|
|
210
|
+
cursor: "pointer",
|
|
211
|
+
// room for the chevron
|
|
212
|
+
paddingRight: tokens.space.xl,
|
|
213
|
+
backgroundImage:
|
|
214
|
+
"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M2 4l4 4 4-4' fill='none' stroke='%23888' stroke-width='1.5'/%3E%3C/svg%3E\")",
|
|
215
|
+
backgroundRepeat: "no-repeat",
|
|
216
|
+
backgroundPosition: `right ${tokens.space.sm} center`,
|
|
217
|
+
};
|
|
218
|
+
return (
|
|
219
|
+
<select
|
|
220
|
+
style={mergeStyles(base, style)}
|
|
221
|
+
onFocus={ring.onFocus}
|
|
222
|
+
onBlur={ring.onBlur}
|
|
223
|
+
{...rest}
|
|
224
|
+
>
|
|
225
|
+
{placeholder !== undefined ? (
|
|
226
|
+
<option value="" disabled>
|
|
227
|
+
{placeholder}
|
|
228
|
+
</option>
|
|
229
|
+
) : null}
|
|
230
|
+
{options
|
|
231
|
+
? options.map((o) => (
|
|
232
|
+
<option key={String(o.value)} value={o.value}>
|
|
233
|
+
{o.label}
|
|
234
|
+
</option>
|
|
235
|
+
))
|
|
236
|
+
: children}
|
|
237
|
+
</select>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// --- Checkbox -----------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
export interface CheckboxProps extends Omit<
|
|
244
|
+
React.InputHTMLAttributes<HTMLInputElement>,
|
|
245
|
+
"type"
|
|
246
|
+
> {
|
|
247
|
+
/** An inline label rendered to the right of the box. */
|
|
248
|
+
label?: React.ReactNode;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** A token-styled checkbox (accent colour follows the theme). */
|
|
252
|
+
export function Checkbox({
|
|
253
|
+
label,
|
|
254
|
+
style,
|
|
255
|
+
disabled,
|
|
256
|
+
...rest
|
|
257
|
+
}: CheckboxProps): React.JSX.Element {
|
|
258
|
+
const box = (
|
|
259
|
+
<input
|
|
260
|
+
type="checkbox"
|
|
261
|
+
disabled={disabled}
|
|
262
|
+
style={mergeStyles(
|
|
263
|
+
{
|
|
264
|
+
width: "1rem",
|
|
265
|
+
height: "1rem",
|
|
266
|
+
accentColor: tokens.color.accent,
|
|
267
|
+
cursor: disabled ? "not-allowed" : "pointer",
|
|
268
|
+
},
|
|
269
|
+
style,
|
|
270
|
+
)}
|
|
271
|
+
{...rest}
|
|
272
|
+
/>
|
|
273
|
+
);
|
|
274
|
+
if (label === undefined) return box;
|
|
275
|
+
return (
|
|
276
|
+
<label
|
|
277
|
+
style={{
|
|
278
|
+
display: "inline-flex",
|
|
279
|
+
alignItems: "center",
|
|
280
|
+
gap: tokens.space.sm,
|
|
281
|
+
fontFamily: tokens.font.sans,
|
|
282
|
+
fontSize: "0.875rem",
|
|
283
|
+
color: tokens.color.text,
|
|
284
|
+
cursor: disabled ? "not-allowed" : "pointer",
|
|
285
|
+
}}
|
|
286
|
+
>
|
|
287
|
+
{box}
|
|
288
|
+
{label}
|
|
289
|
+
</label>
|
|
290
|
+
);
|
|
291
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { tokens, mergeStyles } from "../style";
|
|
3
|
+
|
|
4
|
+
export interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {
|
|
5
|
+
/** Append a danger-coloured `*`. */
|
|
6
|
+
required?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** A token-styled form label. */
|
|
10
|
+
export function Label({
|
|
11
|
+
required = false,
|
|
12
|
+
style,
|
|
13
|
+
children,
|
|
14
|
+
...rest
|
|
15
|
+
}: LabelProps): React.JSX.Element {
|
|
16
|
+
const base: React.CSSProperties = {
|
|
17
|
+
display: "inline-block",
|
|
18
|
+
fontFamily: tokens.font.sans,
|
|
19
|
+
fontSize: "0.8125rem",
|
|
20
|
+
fontWeight: tokens.font.weightMedium as unknown as number,
|
|
21
|
+
color: tokens.color.text,
|
|
22
|
+
};
|
|
23
|
+
return (
|
|
24
|
+
<label style={mergeStyles(base, style)} {...rest}>
|
|
25
|
+
{children}
|
|
26
|
+
{required ? (
|
|
27
|
+
<span style={{ color: tokens.color.danger, marginLeft: 2 }}>*</span>
|
|
28
|
+
) : null}
|
|
29
|
+
</label>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Muted helper text under a control. */
|
|
34
|
+
export function FieldHint({
|
|
35
|
+
style,
|
|
36
|
+
children,
|
|
37
|
+
...rest
|
|
38
|
+
}: React.HTMLAttributes<HTMLDivElement>): React.JSX.Element {
|
|
39
|
+
const base: React.CSSProperties = {
|
|
40
|
+
fontFamily: tokens.font.sans,
|
|
41
|
+
fontSize: "0.75rem",
|
|
42
|
+
color: tokens.color.textMuted,
|
|
43
|
+
};
|
|
44
|
+
return (
|
|
45
|
+
<div style={mergeStyles(base, style)} {...rest}>
|
|
46
|
+
{children}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A danger-coloured validation message. */
|
|
52
|
+
export function FieldError({
|
|
53
|
+
style,
|
|
54
|
+
children,
|
|
55
|
+
...rest
|
|
56
|
+
}: React.HTMLAttributes<HTMLDivElement>): React.JSX.Element {
|
|
57
|
+
const base: React.CSSProperties = {
|
|
58
|
+
fontFamily: tokens.font.sans,
|
|
59
|
+
fontSize: "0.75rem",
|
|
60
|
+
color: tokens.color.danger,
|
|
61
|
+
};
|
|
62
|
+
return (
|
|
63
|
+
<div role="alert" style={mergeStyles(base, style)} {...rest}>
|
|
64
|
+
{children}
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface FieldProps {
|
|
70
|
+
/** The label text; omit for an unlabelled field. */
|
|
71
|
+
label?: React.ReactNode;
|
|
72
|
+
/** Associates the label with the control. */
|
|
73
|
+
htmlFor?: string;
|
|
74
|
+
required?: boolean;
|
|
75
|
+
/** When truthy, shown as a `<FieldError>` (and hides the hint). */
|
|
76
|
+
error?: string;
|
|
77
|
+
/** Helper text shown when there is no error. */
|
|
78
|
+
hint?: React.ReactNode;
|
|
79
|
+
/** The control itself. */
|
|
80
|
+
children: React.ReactNode;
|
|
81
|
+
style?: React.CSSProperties;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The label + control + message stack used by `<AutoForm>`, also usable on its
|
|
86
|
+
* own to lay out a hand-built control.
|
|
87
|
+
*/
|
|
88
|
+
export function Field({
|
|
89
|
+
label,
|
|
90
|
+
htmlFor,
|
|
91
|
+
required,
|
|
92
|
+
error,
|
|
93
|
+
hint,
|
|
94
|
+
children,
|
|
95
|
+
style,
|
|
96
|
+
}: FieldProps): React.JSX.Element {
|
|
97
|
+
const base: React.CSSProperties = {
|
|
98
|
+
display: "flex",
|
|
99
|
+
flexDirection: "column",
|
|
100
|
+
gap: tokens.space.xs,
|
|
101
|
+
};
|
|
102
|
+
return (
|
|
103
|
+
<div style={mergeStyles(base, style)}>
|
|
104
|
+
{label != null ? (
|
|
105
|
+
<Label htmlFor={htmlFor} required={required}>
|
|
106
|
+
{label}
|
|
107
|
+
</Label>
|
|
108
|
+
) : null}
|
|
109
|
+
{children}
|
|
110
|
+
{error ? (
|
|
111
|
+
<FieldError>{error}</FieldError>
|
|
112
|
+
) : hint ? (
|
|
113
|
+
<FieldHint>{hint}</FieldHint>
|
|
114
|
+
) : null}
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The token-styled building blocks — the "shadcn layer". Every one paints
|
|
3
|
+
* exclusively from design tokens (see `../style.ts`) and forwards `style` /
|
|
4
|
+
* `className` / native props, so you can compose or restyle freely. The
|
|
5
|
+
* data-connected components (`<DataTable>`, `<AutoForm>`) are built on top of
|
|
6
|
+
* these.
|
|
7
|
+
*/
|
|
8
|
+
export * from "./controls";
|
|
9
|
+
export * from "./field";
|
|
10
|
+
export * from "./table";
|
|
11
|
+
export * from "./misc";
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import React, { useEffect } from "react";
|
|
2
|
+
import { tokens, mergeStyles } from "../style";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* One-time injection of the keyframes the kit needs (only the spinner today).
|
|
6
|
+
* Guarded so it runs once per document and no-ops under SSR / React Native.
|
|
7
|
+
*/
|
|
8
|
+
let keyframesInjected = false;
|
|
9
|
+
function useKitKeyframes(): void {
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (keyframesInjected || typeof document === "undefined") return;
|
|
12
|
+
keyframesInjected = true;
|
|
13
|
+
const el = document.createElement("style");
|
|
14
|
+
el.setAttribute("data-rastack-ui", "");
|
|
15
|
+
el.textContent = "@keyframes rastack-spin{to{transform:rotate(360deg)}}";
|
|
16
|
+
document.head.appendChild(el);
|
|
17
|
+
}, []);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SpinnerProps {
|
|
21
|
+
/** Diameter in px. Default 16. */
|
|
22
|
+
size?: number;
|
|
23
|
+
/** Ring colour. Defaults to the accent token. */
|
|
24
|
+
color?: string;
|
|
25
|
+
style?: React.CSSProperties;
|
|
26
|
+
"aria-label"?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** An indeterminate loading ring, painted from tokens. */
|
|
30
|
+
export function Spinner({
|
|
31
|
+
size = 16,
|
|
32
|
+
color,
|
|
33
|
+
style,
|
|
34
|
+
...rest
|
|
35
|
+
}: SpinnerProps): React.JSX.Element {
|
|
36
|
+
useKitKeyframes();
|
|
37
|
+
const base: React.CSSProperties = {
|
|
38
|
+
display: "inline-block",
|
|
39
|
+
width: size,
|
|
40
|
+
height: size,
|
|
41
|
+
border: `2px solid ${tokens.color.border}`,
|
|
42
|
+
borderTopColor: color ?? tokens.color.accent,
|
|
43
|
+
borderRadius: "50%",
|
|
44
|
+
animation: "rastack-spin 0.6s linear infinite",
|
|
45
|
+
flex: "none",
|
|
46
|
+
};
|
|
47
|
+
return (
|
|
48
|
+
<span
|
|
49
|
+
role="status"
|
|
50
|
+
aria-label={rest["aria-label"] ?? "Loading"}
|
|
51
|
+
style={mergeStyles(base, style)}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type BadgeTone = "neutral" | "accent" | "success" | "danger" | "warning";
|
|
57
|
+
|
|
58
|
+
const badgeTone: Record<BadgeTone, React.CSSProperties> = {
|
|
59
|
+
neutral: {
|
|
60
|
+
background: tokens.color.surfaceAlt,
|
|
61
|
+
color: tokens.color.textMuted,
|
|
62
|
+
},
|
|
63
|
+
accent: { background: tokens.color.accent, color: tokens.color.onAccent },
|
|
64
|
+
success: { background: tokens.color.success, color: "#ffffff" },
|
|
65
|
+
danger: { background: tokens.color.danger, color: "#ffffff" },
|
|
66
|
+
warning: { background: tokens.color.warning, color: "#1a1205" },
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
|
|
70
|
+
tone?: BadgeTone;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** A small status pill. */
|
|
74
|
+
export function Badge({
|
|
75
|
+
tone = "neutral",
|
|
76
|
+
style,
|
|
77
|
+
children,
|
|
78
|
+
...rest
|
|
79
|
+
}: BadgeProps): React.JSX.Element {
|
|
80
|
+
const base: React.CSSProperties = {
|
|
81
|
+
display: "inline-flex",
|
|
82
|
+
alignItems: "center",
|
|
83
|
+
gap: tokens.space.xs,
|
|
84
|
+
padding: `2px ${tokens.space.sm}`,
|
|
85
|
+
fontFamily: tokens.font.sans,
|
|
86
|
+
fontSize: "0.75rem",
|
|
87
|
+
fontWeight: tokens.font.weightMedium as unknown as number,
|
|
88
|
+
lineHeight: 1.4,
|
|
89
|
+
borderRadius: tokens.radius.pill,
|
|
90
|
+
whiteSpace: "nowrap",
|
|
91
|
+
...badgeTone[tone],
|
|
92
|
+
};
|
|
93
|
+
return (
|
|
94
|
+
<span style={mergeStyles(base, style)} {...rest}>
|
|
95
|
+
{children}
|
|
96
|
+
</span>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
101
|
+
/** Drop the default padding (e.g. to hold a flush `<DataTable>`). */
|
|
102
|
+
flush?: boolean;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** A surface container: token background, border, radius and shadow. */
|
|
106
|
+
export function Card({
|
|
107
|
+
flush,
|
|
108
|
+
style,
|
|
109
|
+
children,
|
|
110
|
+
...rest
|
|
111
|
+
}: CardProps): React.JSX.Element {
|
|
112
|
+
const base: React.CSSProperties = {
|
|
113
|
+
background: tokens.color.surface,
|
|
114
|
+
border: `1px solid ${tokens.color.border}`,
|
|
115
|
+
borderRadius: tokens.radius.lg,
|
|
116
|
+
boxShadow: tokens.shadow.sm,
|
|
117
|
+
padding: flush ? 0 : tokens.space.lg,
|
|
118
|
+
color: tokens.color.text,
|
|
119
|
+
overflow: "hidden",
|
|
120
|
+
};
|
|
121
|
+
return (
|
|
122
|
+
<div style={mergeStyles(base, style)} {...rest}>
|
|
123
|
+
{children}
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
type SpaceKey = keyof typeof tokens.space;
|
|
129
|
+
|
|
130
|
+
function resolveGap(
|
|
131
|
+
gap: SpaceKey | number | string | undefined,
|
|
132
|
+
): string | number | undefined {
|
|
133
|
+
if (gap == null) return undefined;
|
|
134
|
+
if (typeof gap === "number") return gap;
|
|
135
|
+
return (tokens.space as Record<string, string>)[gap] ?? gap;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface StackProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
139
|
+
direction?: "row" | "column";
|
|
140
|
+
/** A `space` token key (`"md"`), a number (px) or any CSS length. */
|
|
141
|
+
gap?: SpaceKey | number | string;
|
|
142
|
+
align?: React.CSSProperties["alignItems"];
|
|
143
|
+
justify?: React.CSSProperties["justifyContent"];
|
|
144
|
+
wrap?: boolean;
|
|
145
|
+
inline?: boolean;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** A flexbox layout primitive with token-aware `gap`. */
|
|
149
|
+
export function Stack({
|
|
150
|
+
direction = "column",
|
|
151
|
+
gap = "md",
|
|
152
|
+
align,
|
|
153
|
+
justify,
|
|
154
|
+
wrap,
|
|
155
|
+
inline,
|
|
156
|
+
style,
|
|
157
|
+
children,
|
|
158
|
+
...rest
|
|
159
|
+
}: StackProps): React.JSX.Element {
|
|
160
|
+
const base: React.CSSProperties = {
|
|
161
|
+
display: inline ? "inline-flex" : "flex",
|
|
162
|
+
flexDirection: direction,
|
|
163
|
+
gap: resolveGap(gap),
|
|
164
|
+
alignItems: align,
|
|
165
|
+
justifyContent: justify,
|
|
166
|
+
flexWrap: wrap ? "wrap" : undefined,
|
|
167
|
+
};
|
|
168
|
+
return (
|
|
169
|
+
<div style={mergeStyles(base, style)} {...rest}>
|
|
170
|
+
{children}
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
}
|