@carbonbits/sixr 1.4.0 → 1.5.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 +139 -60
- package/dist/button.cjs +223 -0
- package/dist/button.cjs.map +1 -0
- package/dist/button.d.cts +34 -0
- package/dist/button.d.ts +34 -0
- package/dist/button.js +4 -0
- package/dist/button.js.map +1 -0
- package/dist/chunk-HDTW4FRT.js +111 -0
- package/dist/chunk-HDTW4FRT.js.map +1 -0
- package/dist/email-input.cjs +213 -170
- package/dist/email-input.cjs.map +1 -1
- package/dist/email-input.d.cts +4 -6
- package/dist/email-input.d.ts +4 -6
- package/dist/email-input.js +219 -170
- package/dist/email-input.js.map +1 -1
- package/dist/field.cjs +206 -0
- package/dist/field.cjs.map +1 -0
- package/dist/field.d.cts +80 -0
- package/dist/field.d.ts +80 -0
- package/dist/field.js +186 -0
- package/dist/field.js.map +1 -0
- package/dist/form.cjs +283 -42
- package/dist/form.cjs.map +1 -1
- package/dist/form.d.cts +79 -10
- package/dist/form.d.ts +79 -10
- package/dist/form.js +285 -45
- package/dist/form.js.map +1 -1
- package/dist/index-CdYkd6zz.d.cts +22 -0
- package/dist/index-CdYkd6zz.d.ts +22 -0
- package/dist/index.cjs +138 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +116 -55
- package/dist/index.d.ts +116 -55
- package/dist/index.js +1 -0
- package/dist/password-input.cjs +217 -174
- package/dist/password-input.cjs.map +1 -1
- package/dist/password-input.d.cts +2 -3
- package/dist/password-input.d.ts +2 -3
- package/dist/password-input.js +222 -173
- package/dist/password-input.js.map +1 -1
- package/dist/phone-input.cjs +213 -170
- package/dist/phone-input.cjs.map +1 -1
- package/dist/phone-input.d.cts +5 -5
- package/dist/phone-input.d.ts +5 -5
- package/dist/phone-input.js +219 -170
- package/dist/phone-input.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/text-input.cjs +213 -170
- package/dist/text-input.cjs.map +1 -1
- package/dist/text-input.d.cts +2 -3
- package/dist/text-input.d.ts +2 -3
- package/dist/text-input.js +219 -170
- package/dist/text-input.js.map +1 -1
- package/package.json +26 -2
- package/dist/index-DwjxC4l4.d.cts +0 -62
- package/dist/index-DwjxC4l4.d.ts +0 -62
package/README.md
CHANGED
|
@@ -13,8 +13,12 @@ cleanly into a server component.
|
|
|
13
13
|
pnpm add @carbonbits/sixr
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
`react` and `react-dom` are peer dependencies (`^19.0.0`). `
|
|
17
|
-
peer dependency, needed
|
|
16
|
+
`react` and `react-dom` are peer dependencies (`^19.0.0`). `react-hook-form` (`^7.89`)
|
|
17
|
+
is a peer dependency too, needed by `Form` and the inputs — a peer rather than a
|
|
18
|
+
bundled dependency so your app and sixr share one instance and one form context.
|
|
19
|
+
`zod` (`^4`) is an optional peer dependency, needed only for
|
|
20
|
+
`@carbonbits/sixr/validation`; `Form` accepts any Standard Schema, of which every zod
|
|
21
|
+
4 schema is one.
|
|
18
22
|
|
|
19
23
|
## Usage
|
|
20
24
|
|
|
@@ -107,6 +111,39 @@ Default elements: `display` → `h1`, `heading` → `h2`, `subheading` → `h3`,
|
|
|
107
111
|
</Typography>
|
|
108
112
|
```
|
|
109
113
|
|
|
114
|
+
### `Button`
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
import { Button } from "@carbonbits/sixr/button";
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The design's action control, as the auth screens use it: accent fill, the field type
|
|
121
|
+
scale, a 4px radius and the shared 45% disabled opacity. Server-safe. `SubmitButton`
|
|
122
|
+
in `Form` is this component with `type="submit"` and the form's submitting state.
|
|
123
|
+
|
|
124
|
+
| Prop | Type | Default | Description |
|
|
125
|
+
| -------------- | ------------------------------------- | ----------- | ------------------------------------------------------------------ |
|
|
126
|
+
| `variant` | `"primary" \| "secondary" \| "ghost"` | `"primary"` | Accent fill, outlined, or bare. |
|
|
127
|
+
| `size` | `"md" \| "lg"` | `"md"` | 40px, or the 44px the design uses on touch layouts. |
|
|
128
|
+
| `fullWidth` | `boolean` | `false` | Stretch to the container. |
|
|
129
|
+
| `loading` | `boolean` | `false` | Spinner in place of the label, and disabled. Rendered button only. |
|
|
130
|
+
| `loadingLabel` | `ReactNode` | — | Shown beside the spinner while `loading`. |
|
|
131
|
+
| `asChild` | `boolean` | `false` | Merge the styles onto the child element (a `next/link`) instead. |
|
|
132
|
+
| `type` | `"button" \| "submit" \| "reset"` | `"button"` | Native button type. |
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
<Button type="submit" fullWidth loading={saving} loadingLabel="Saving…">
|
|
136
|
+
Save changes
|
|
137
|
+
</Button>
|
|
138
|
+
|
|
139
|
+
<Button variant="secondary" onClick={cancel}>Cancel</Button>
|
|
140
|
+
|
|
141
|
+
// A navigation that reads as a button, without losing the link's semantics.
|
|
142
|
+
<Button asChild fullWidth>
|
|
143
|
+
<Link href="/id/login">Sign in again</Link>
|
|
144
|
+
</Button>
|
|
145
|
+
```
|
|
146
|
+
|
|
110
147
|
### Inputs: `TextInput`, `EmailInput`, `PasswordInput`, `PhoneInput`
|
|
111
148
|
|
|
112
149
|
```tsx
|
|
@@ -116,8 +153,10 @@ import { PasswordInput } from "@carbonbits/sixr/password-input";
|
|
|
116
153
|
import { PhoneInput } from "@carbonbits/sixr/phone-input";
|
|
117
154
|
```
|
|
118
155
|
|
|
119
|
-
Labelled single-line fields
|
|
120
|
-
|
|
156
|
+
Labelled single-line fields. Each one is for a single kind of value and fixes the
|
|
157
|
+
input type, keyboard and autofill hints that value needs. Inside a `Form` they register
|
|
158
|
+
under their `name` and show whatever the form's schema reports for it; outside one
|
|
159
|
+
they are plain labelled inputs:
|
|
121
160
|
|
|
122
161
|
| Component | Value | Fixes |
|
|
123
162
|
| --------------- | ---------------- | ---------------------------------------------------------------------- |
|
|
@@ -131,20 +170,15 @@ its subpath — none is on the root barrel, which stays server-safe.
|
|
|
131
170
|
|
|
132
171
|
They share these props:
|
|
133
172
|
|
|
134
|
-
| Prop
|
|
135
|
-
|
|
|
136
|
-
| `
|
|
137
|
-
| `
|
|
138
|
-
| `
|
|
139
|
-
| `
|
|
140
|
-
| `
|
|
141
|
-
| `
|
|
142
|
-
| `
|
|
143
|
-
| `value` | `string` | — | Makes the field controlled. |
|
|
144
|
-
| `defaultValue` | `string` | `""` | Initial value when uncontrolled. |
|
|
145
|
-
| `optional` | `boolean` | `false` | Shows an "Optional" marker on the label. |
|
|
146
|
-
| `action` | `ReactNode` | — | Trailing label slot, e.g. a "Forgot?" link. |
|
|
147
|
-
| `trailing` | `ReactNode` | — | Trailing in-field slot. Not on `PasswordInput`, which owns it. |
|
|
173
|
+
| Prop | Type | Default | Description |
|
|
174
|
+
| ---------- | ----------- | ------- | -------------------------------------------------------------------- |
|
|
175
|
+
| `name` | `string` | — | The form field this edits: the key in the `Form`'s values. Required. |
|
|
176
|
+
| `label` | `string` | — | Field label, associated with the input. |
|
|
177
|
+
| `hint` | `ReactNode` | — | Guidance below the field. Replaced by an error. |
|
|
178
|
+
| `error` | `string` | — | Caller-supplied error. Outranks what the form's schema reports. |
|
|
179
|
+
| `optional` | `boolean` | `false` | Shows an "Optional" marker on the label. |
|
|
180
|
+
| `action` | `ReactNode` | — | Trailing label slot, e.g. a "Forgot?" link. |
|
|
181
|
+
| `trailing` | `ReactNode` | — | Trailing in-field slot. Not on `PasswordInput`, which owns it. |
|
|
148
182
|
|
|
149
183
|
And their own:
|
|
150
184
|
|
|
@@ -154,30 +188,21 @@ And their own:
|
|
|
154
188
|
| `PasswordInput` | `revealable` | `boolean` | `true` | Shows the Show/Hide toggle. |
|
|
155
189
|
| `PasswordInput` | `revealLabels` | `{ show: string; hide: string }` | `{ show: "Show", hide: "Hide" }` | Toggle text, for localisation. |
|
|
156
190
|
|
|
157
|
-
Any other `<input>` attribute — `placeholder`, `maxLength`, `disabled`, `autoComplete
|
|
158
|
-
is passed straight through, and overrides the component's default where
|
|
191
|
+
Any other `<input>` attribute — `placeholder`, `maxLength`, `disabled`, `autoComplete`,
|
|
192
|
+
`onChange` — is passed straight through, and overrides the component's default where
|
|
193
|
+
there is one. Validation is not a per-input concern: it is the enclosing `Form`'s
|
|
194
|
+
schema, keyed by `name`.
|
|
159
195
|
|
|
160
196
|
```tsx
|
|
161
197
|
import { EmailInput } from "@carbonbits/sixr/email-input";
|
|
162
|
-
import { emailSchema } from "@carbonbits/sixr/validation";
|
|
163
|
-
|
|
164
|
-
<EmailInput
|
|
165
|
-
label="Work email"
|
|
166
|
-
placeholder="you@company.com"
|
|
167
|
-
schema={emailSchema}
|
|
168
|
-
onChange={setEmail}
|
|
169
|
-
/>;
|
|
170
|
-
```
|
|
171
198
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
is shown, the input gets `aria-invalid`, and the message is announced via `role="alert"`
|
|
175
|
-
and wired up with `aria-describedby`.
|
|
199
|
+
<EmailInput name="email" label="Work email" placeholder="you@company.com" />;
|
|
200
|
+
```
|
|
176
201
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
202
|
+
The first schema issue for the field is shown, the input gets `aria-invalid`, and the
|
|
203
|
+
message is announced via `role="alert"` and wired up with `aria-describedby`. The
|
|
204
|
+
inputs are `Field` compositions (below) with the design's classes; a control that
|
|
205
|
+
needs a different look composes `Field` itself.
|
|
181
206
|
|
|
182
207
|
### `Form` / `FormError` / `SubmitButton`
|
|
183
208
|
|
|
@@ -185,39 +210,45 @@ default to a schema for the same reason; pair them with the ready-made ones unde
|
|
|
185
210
|
import { Form, FormError, SubmitButton } from "@carbonbits/sixr/form";
|
|
186
211
|
```
|
|
187
212
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
is
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
|
196
|
-
|
|
|
197
|
-
| `
|
|
198
|
-
| `
|
|
213
|
+
A `<form>` on react-hook-form. One schema validates the whole form — on a field's
|
|
214
|
+
first blur, then on every change until it is fixed, and again on submit — and
|
|
215
|
+
`onSubmit` only ever runs with values the schema accepted, typed as its output. While
|
|
216
|
+
`onSubmit` is pending the fields are disabled and `SubmitButton` shows a spinner.
|
|
217
|
+
Interactive, so it ships its own `"use client"` directive and is reachable only at
|
|
218
|
+
this subpath.
|
|
219
|
+
|
|
220
|
+
| Component | Prop | Type | Default | Description |
|
|
221
|
+
| -------------- | --------------- | --------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------ |
|
|
222
|
+
| `Form` | `schema` | Standard Schema (any zod 4 schema) | — | Validates the form's values. Without one, `onSubmit` receives the raw values. |
|
|
223
|
+
| `Form` | `defaultValues` | `DefaultValues<Input>` | — | Initial values, keyed by field `name`. |
|
|
224
|
+
| `Form` | `onSubmit` | `(values: Output, form: FormInstance) => void \| Promise<void>` | — | Runs with parsed values. Throwing surfaces the message through `FormError`. |
|
|
225
|
+
| `Form` | `mode` | `"onTouched" \| "onBlur" \| "onChange" \| "onSubmit" \| "all"` | `"onTouched"` | When a field first validates. |
|
|
226
|
+
| `Form` | `form` | `FormInstance` | — | Your own instance from `useFormInstance`, when the screen needs `setError`, `reset`, `watch`. |
|
|
227
|
+
| `Form` | `children` | `ReactNode` | — | Fields, `FormError`, `SubmitButton` — whatever the design calls for, in order. |
|
|
228
|
+
| `FormError` | `children` | `ReactNode` | — | Shown in place of the form's root error. Without it, the root error (from a thrown `onSubmit` or `form.setError("root", …)`). Renders nothing when there is none. |
|
|
229
|
+
| `SubmitButton` | `loadingLabel` | `ReactNode` | — | Shown next to the spinner in place of `children` while submitting. |
|
|
230
|
+
| `SubmitButton` | `variant`, `size`, `fullWidth` | as `Button` | `fullWidth` | Everything else `Button` takes, bar `type`, `loading` and `asChild`. |
|
|
199
231
|
|
|
200
232
|
```tsx
|
|
233
|
+
import { z } from "zod";
|
|
201
234
|
import { EmailInput } from "@carbonbits/sixr/email-input";
|
|
202
235
|
import { Form, FormError, SubmitButton } from "@carbonbits/sixr/form";
|
|
203
236
|
import { PasswordInput } from "@carbonbits/sixr/password-input";
|
|
237
|
+
import { emailSchema, passwordSchema } from "@carbonbits/sixr/validation";
|
|
204
238
|
|
|
205
|
-
|
|
206
|
-
const [email, setEmail] = useState("");
|
|
207
|
-
const [password, setPassword] = useState("");
|
|
208
|
-
const [error, setError] = useState<string>();
|
|
239
|
+
const loginSchema = z.object({ email: emailSchema, password: passwordSchema });
|
|
209
240
|
|
|
210
|
-
|
|
211
|
-
|
|
241
|
+
function LoginForm() {
|
|
242
|
+
const handleSubmit = async ({ email, password }: z.output<typeof loginSchema>) => {
|
|
212
243
|
const ok = await signIn(email, password);
|
|
213
|
-
if (!ok)
|
|
244
|
+
if (!ok) throw new Error("That email and password don't match.");
|
|
214
245
|
};
|
|
215
246
|
|
|
216
247
|
return (
|
|
217
|
-
<Form onSubmit={handleSubmit}>
|
|
218
|
-
<EmailInput label="Work email"
|
|
219
|
-
<PasswordInput label="Password"
|
|
220
|
-
<FormError
|
|
248
|
+
<Form schema={loginSchema} defaultValues={{ email: "", password: "" }} onSubmit={handleSubmit}>
|
|
249
|
+
<EmailInput name="email" label="Work email" />
|
|
250
|
+
<PasswordInput name="password" label="Password" />
|
|
251
|
+
<FormError />
|
|
221
252
|
<SubmitButton loadingLabel="Signing in…">Sign in</SubmitButton>
|
|
222
253
|
</Form>
|
|
223
254
|
);
|
|
@@ -230,6 +261,53 @@ control inside `Form`) picks that up for free. Fields, OAuth rows, dividers —
|
|
|
230
261
|
the design puts between the top of the form and the submit button — are just children;
|
|
231
262
|
`Form` doesn't assume a shape beyond that.
|
|
232
263
|
|
|
264
|
+
`useFormInstance({ schema, defaultValues, mode })` returns the react-hook-form instance
|
|
265
|
+
`Form` would otherwise create. Pass it as `form` when the screen needs the instance
|
|
266
|
+
too — to `setError("root", …)` after a request, `reset()` after success, or `watch()`
|
|
267
|
+
a field.
|
|
268
|
+
|
|
269
|
+
### `Field`
|
|
270
|
+
|
|
271
|
+
```tsx
|
|
272
|
+
import {
|
|
273
|
+
FieldControl,
|
|
274
|
+
FieldDescription,
|
|
275
|
+
FieldLabel,
|
|
276
|
+
FieldMessage,
|
|
277
|
+
FieldRoot,
|
|
278
|
+
useFieldContext,
|
|
279
|
+
} from "@carbonbits/sixr/field";
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
The headless structure behind every input: the id shared by label and control, the
|
|
283
|
+
description and error the control is described by, validity, and — inside a `Form` —
|
|
284
|
+
the react-hook-form bindings. No classes of its own; each part is a plain element or
|
|
285
|
+
a Radix `Slot`, so the look is entirely the composer's. This is how the shipped inputs
|
|
286
|
+
are built, and how to build a control the library doesn't ship (a select, a code
|
|
287
|
+
entry, a toggle) so it validates and announces errors the same way.
|
|
288
|
+
|
|
289
|
+
| Part | Renders | Description |
|
|
290
|
+
| ------------------- | ------------------------------ | ---------------------------------------------------------------------------------------- |
|
|
291
|
+
| `FieldRoot` | `div` (or child via `asChild`) | Takes `name`, optional `error` (outranks the schema), `required`, `disabled`. Sets `data-invalid` / `data-disabled` for styling. |
|
|
292
|
+
| `FieldLabel` | Radix `Label` | Associated with the control. |
|
|
293
|
+
| `FieldControl` | Radix `Slot` | Wires id, `name`, `aria-*` and the form bindings onto its child element; the child's own handlers still run. |
|
|
294
|
+
| `FieldDescription` | `p` (or child via `asChild`) | Guidance. Hidden while there is an error. |
|
|
295
|
+
| `FieldMessage` | `p` (or child via `asChild`) | The error, as `role="alert"`. `children` is a fallback message. Nothing while valid. |
|
|
296
|
+
|
|
297
|
+
```tsx
|
|
298
|
+
<FieldRoot name="handle" className="flex flex-col gap-1 border-b-2 border-line data-[invalid]:border-danger">
|
|
299
|
+
<FieldLabel className="text-hint uppercase">Handle</FieldLabel>
|
|
300
|
+
<FieldControl>
|
|
301
|
+
<input className="bg-transparent outline-none" placeholder="@jordan" />
|
|
302
|
+
</FieldControl>
|
|
303
|
+
<FieldDescription className="text-hint text-fg-subtle">Letters, numbers, underscores.</FieldDescription>
|
|
304
|
+
<FieldMessage className="text-hint text-danger" />
|
|
305
|
+
</FieldRoot>
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
`useFieldContext()` exposes the same wiring (`id`, `name`, `error`, `invalid`,
|
|
309
|
+
`bindings`, …) to anything else rendered inside `FieldRoot`.
|
|
310
|
+
|
|
233
311
|
## Validation schemas
|
|
234
312
|
|
|
235
313
|
```tsx
|
|
@@ -239,7 +317,8 @@ import { emailSchema, passwordSchema } from "@carbonbits/sixr/validation";
|
|
|
239
317
|
Ready-made zod schemas matching the Carbonbits auth rules: `emailSchema`,
|
|
240
318
|
`passwordSchema`, `verificationCodeSchema`, `backupCodeSchema`, `fullNameSchema`,
|
|
241
319
|
`jobTitleSchema` and `phoneSchema`. They are ordinary zod schemas — extend them with
|
|
242
|
-
`.refine`, or replace the message with `.min(8, "…")
|
|
320
|
+
`.refine`, or replace the message with `.min(8, "…")` — and compose into a `Form`'s
|
|
321
|
+
schema with `z.object({ email: emailSchema, … })`, keyed by field `name`.
|
|
243
322
|
|
|
244
323
|
This subpath needs `zod` (`^4`), which is an optional peer dependency. Nothing else in
|
|
245
324
|
the package requires it.
|
package/dist/button.cjs
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var reactSlot = require('@radix-ui/react-slot');
|
|
4
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
|
|
6
|
+
// src/components/button/index.tsx
|
|
7
|
+
var DEFAULT_ELEMENT = {
|
|
8
|
+
display: "h1",
|
|
9
|
+
heading: "h2",
|
|
10
|
+
subheading: "h3",
|
|
11
|
+
body: "p",
|
|
12
|
+
label: "span",
|
|
13
|
+
hint: "p",
|
|
14
|
+
code: "code"
|
|
15
|
+
};
|
|
16
|
+
var DEFAULT_WEIGHT = {
|
|
17
|
+
display: "semibold",
|
|
18
|
+
heading: "semibold",
|
|
19
|
+
subheading: "medium",
|
|
20
|
+
body: "regular",
|
|
21
|
+
label: "semibold",
|
|
22
|
+
hint: "regular",
|
|
23
|
+
code: "regular"
|
|
24
|
+
};
|
|
25
|
+
var VARIANT_CLASSES = {
|
|
26
|
+
display: "text-display leading-tight tracking-tight",
|
|
27
|
+
heading: "text-heading leading-tight tracking-tight",
|
|
28
|
+
subheading: "text-subheading leading-snug",
|
|
29
|
+
body: "text-field leading-normal",
|
|
30
|
+
label: "text-label leading-normal",
|
|
31
|
+
hint: "text-hint leading-normal",
|
|
32
|
+
code: "font-code text-field leading-normal"
|
|
33
|
+
};
|
|
34
|
+
var TONE_CLASSES = {
|
|
35
|
+
default: "text-fg",
|
|
36
|
+
muted: "text-fg-muted",
|
|
37
|
+
subtle: "text-fg-subtle",
|
|
38
|
+
accent: "text-accent",
|
|
39
|
+
"on-accent": "text-on-accent",
|
|
40
|
+
success: "text-success",
|
|
41
|
+
warning: "text-warning",
|
|
42
|
+
info: "text-info",
|
|
43
|
+
danger: "text-danger"
|
|
44
|
+
};
|
|
45
|
+
var WEIGHT_CLASSES = {
|
|
46
|
+
regular: "font-normal",
|
|
47
|
+
medium: "font-medium",
|
|
48
|
+
semibold: "font-semibold",
|
|
49
|
+
bold: "font-bold"
|
|
50
|
+
};
|
|
51
|
+
var ALIGN_CLASSES = {
|
|
52
|
+
start: "text-start",
|
|
53
|
+
center: "text-center",
|
|
54
|
+
end: "text-end"
|
|
55
|
+
};
|
|
56
|
+
var ICON_CLASSES = "inline-flex shrink-0 items-center [&>svg]:h-[1em] [&>svg]:w-[1em]";
|
|
57
|
+
function typographyClassName({
|
|
58
|
+
variant,
|
|
59
|
+
tone,
|
|
60
|
+
weight,
|
|
61
|
+
align,
|
|
62
|
+
truncate,
|
|
63
|
+
hasIcon,
|
|
64
|
+
className
|
|
65
|
+
}) {
|
|
66
|
+
return [
|
|
67
|
+
VARIANT_CLASSES[variant],
|
|
68
|
+
TONE_CLASSES[tone],
|
|
69
|
+
WEIGHT_CLASSES[weight],
|
|
70
|
+
align ? ALIGN_CLASSES[align] : "",
|
|
71
|
+
hasIcon ? "inline-flex items-center gap-1.5" : "",
|
|
72
|
+
// With an icon the root is a flex container, and text-overflow only
|
|
73
|
+
// clips block text, so the text span carries the truncation instead.
|
|
74
|
+
truncate && !hasIcon ? "truncate" : "",
|
|
75
|
+
className ?? ""
|
|
76
|
+
].filter(Boolean).join(" ");
|
|
77
|
+
}
|
|
78
|
+
function Typography({
|
|
79
|
+
variant = "body",
|
|
80
|
+
tone = "default",
|
|
81
|
+
weight,
|
|
82
|
+
align,
|
|
83
|
+
truncate = false,
|
|
84
|
+
icon,
|
|
85
|
+
iconPosition = "leading",
|
|
86
|
+
as,
|
|
87
|
+
asChild = false,
|
|
88
|
+
className,
|
|
89
|
+
children,
|
|
90
|
+
...rest
|
|
91
|
+
}) {
|
|
92
|
+
const hasIcon = icon !== void 0 && icon !== null;
|
|
93
|
+
const Component = asChild ? reactSlot.Slot : as ?? DEFAULT_ELEMENT[variant];
|
|
94
|
+
const iconNode = hasIcon ? /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", className: ICON_CLASSES, children: icon }) : null;
|
|
95
|
+
const content = hasIcon ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
96
|
+
iconPosition === "leading" ? iconNode : null,
|
|
97
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: truncate ? "min-w-0 truncate" : void 0, children }),
|
|
98
|
+
iconPosition === "trailing" ? iconNode : null
|
|
99
|
+
] }) : children;
|
|
100
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
101
|
+
Component,
|
|
102
|
+
{
|
|
103
|
+
...rest,
|
|
104
|
+
className: typographyClassName({
|
|
105
|
+
variant,
|
|
106
|
+
tone,
|
|
107
|
+
weight: weight ?? DEFAULT_WEIGHT[variant],
|
|
108
|
+
align,
|
|
109
|
+
truncate,
|
|
110
|
+
hasIcon,
|
|
111
|
+
className
|
|
112
|
+
}),
|
|
113
|
+
children: content
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
var BASE_CLASSES = "inline-flex shrink-0 items-center justify-center gap-2 rounded-field border font-[inherit] text-field font-semibold outline-none transition-colors focus-visible:ring-3 focus-visible:ring-accent-soft disabled:cursor-not-allowed disabled:opacity-45";
|
|
118
|
+
var VARIANT_CLASSES2 = {
|
|
119
|
+
primary: "border-transparent bg-accent text-on-accent enabled:hover:bg-accent-2",
|
|
120
|
+
secondary: "border-line bg-surface text-fg enabled:hover:bg-surface-2",
|
|
121
|
+
ghost: "border-transparent bg-transparent text-fg enabled:hover:bg-surface-2"
|
|
122
|
+
};
|
|
123
|
+
var SIZE_CLASSES = {
|
|
124
|
+
md: "h-10 px-4",
|
|
125
|
+
lg: "h-11 px-5"
|
|
126
|
+
};
|
|
127
|
+
var LABEL_TONE = {
|
|
128
|
+
primary: "on-accent",
|
|
129
|
+
secondary: "default",
|
|
130
|
+
ghost: "default"
|
|
131
|
+
};
|
|
132
|
+
function buttonClassName({
|
|
133
|
+
variant,
|
|
134
|
+
size,
|
|
135
|
+
fullWidth,
|
|
136
|
+
className
|
|
137
|
+
}) {
|
|
138
|
+
return [
|
|
139
|
+
BASE_CLASSES,
|
|
140
|
+
VARIANT_CLASSES2[variant],
|
|
141
|
+
SIZE_CLASSES[size],
|
|
142
|
+
fullWidth ? "w-full" : "",
|
|
143
|
+
className ?? ""
|
|
144
|
+
].filter(Boolean).join(" ");
|
|
145
|
+
}
|
|
146
|
+
function Spinner() {
|
|
147
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
148
|
+
"svg",
|
|
149
|
+
{
|
|
150
|
+
className: "h-4 w-4 animate-spin",
|
|
151
|
+
viewBox: "0 0 24 24",
|
|
152
|
+
fill: "none",
|
|
153
|
+
"aria-hidden": "true",
|
|
154
|
+
"data-testid": "button-spinner",
|
|
155
|
+
children: [
|
|
156
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
157
|
+
"circle",
|
|
158
|
+
{
|
|
159
|
+
className: "opacity-25",
|
|
160
|
+
cx: "12",
|
|
161
|
+
cy: "12",
|
|
162
|
+
r: "10",
|
|
163
|
+
stroke: "currentColor",
|
|
164
|
+
strokeWidth: "4"
|
|
165
|
+
}
|
|
166
|
+
),
|
|
167
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
168
|
+
"path",
|
|
169
|
+
{
|
|
170
|
+
className: "opacity-75",
|
|
171
|
+
fill: "currentColor",
|
|
172
|
+
d: "M4 12a8 8 0 0 1 8-8V0C5.373 0 0 5.373 0 12H4Z"
|
|
173
|
+
}
|
|
174
|
+
)
|
|
175
|
+
]
|
|
176
|
+
}
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
function Button({
|
|
180
|
+
variant = "primary",
|
|
181
|
+
size = "md",
|
|
182
|
+
fullWidth = false,
|
|
183
|
+
loading = false,
|
|
184
|
+
loadingLabel,
|
|
185
|
+
asChild = false,
|
|
186
|
+
type = "button",
|
|
187
|
+
disabled,
|
|
188
|
+
className,
|
|
189
|
+
children,
|
|
190
|
+
...rest
|
|
191
|
+
}) {
|
|
192
|
+
const classes = buttonClassName({ variant, size, fullWidth, className });
|
|
193
|
+
if (asChild) {
|
|
194
|
+
return /* @__PURE__ */ jsxRuntime.jsx(reactSlot.Slot, { ...rest, className: classes, children });
|
|
195
|
+
}
|
|
196
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
197
|
+
"button",
|
|
198
|
+
{
|
|
199
|
+
...rest,
|
|
200
|
+
type,
|
|
201
|
+
disabled: disabled || loading,
|
|
202
|
+
"aria-busy": loading || void 0,
|
|
203
|
+
className: classes,
|
|
204
|
+
children: loading ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
205
|
+
/* @__PURE__ */ jsxRuntime.jsx(Spinner, {}),
|
|
206
|
+
loadingLabel
|
|
207
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
208
|
+
Typography,
|
|
209
|
+
{
|
|
210
|
+
as: "span",
|
|
211
|
+
variant: "body",
|
|
212
|
+
weight: "semibold",
|
|
213
|
+
tone: LABEL_TONE[variant],
|
|
214
|
+
children
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
exports.Button = Button;
|
|
222
|
+
//# sourceMappingURL=button.cjs.map
|
|
223
|
+
//# sourceMappingURL=button.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/typography/index.tsx","../src/components/button/index.tsx"],"names":["Slot","jsx","jsxs","Fragment","VARIANT_CLASSES"],"mappings":"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","file":"button.cjs","sourcesContent":["import { Slot } from \"@radix-ui/react-slot\";\nimport type { ElementType, HTMLAttributes, ReactNode, Ref } from \"react\";\n\n/**\n * The library's type scale. Everything that renders text — field labels, hints,\n * form errors, headings — goes through here, so a size or colour lives in one\n * place and the tokens in `src/styles/index.css` stay the single source of\n * truth for what \"hint\" or \"muted\" means.\n *\n * Server-safe: no state, no handlers, no directive. It composes Radix `Slot`\n * for `asChild`, which is how a consumer puts these styles on their own\n * element (a `next/link`, a Radix `Label`) without an extra wrapper.\n */\n\nexport type TypographyVariant =\n | \"display\"\n | \"heading\"\n | \"subheading\"\n | \"body\"\n | \"label\"\n | \"hint\"\n | \"code\";\n\nexport type TypographyTone =\n | \"default\"\n | \"muted\"\n | \"subtle\"\n | \"accent\"\n | \"on-accent\"\n | \"success\"\n | \"warning\"\n | \"info\"\n | \"danger\";\n\nexport type TypographyWeight = \"regular\" | \"medium\" | \"semibold\" | \"bold\";\n\nexport type TypographyElement =\n | \"p\"\n | \"span\"\n | \"div\"\n | \"h1\"\n | \"h2\"\n | \"h3\"\n | \"h4\"\n | \"h5\"\n | \"h6\"\n | \"label\"\n | \"code\"\n | \"strong\"\n | \"em\"\n | \"small\";\n\nexport type TypographyProps = Omit<HTMLAttributes<HTMLElement>, \"color\"> & {\n /** Size and default element. `body` unless told otherwise. */\n variant?: TypographyVariant;\n /** Colour role, from the design tokens. */\n tone?: TypographyTone;\n /** Overrides the variant's default weight. */\n weight?: TypographyWeight;\n align?: \"start\" | \"center\" | \"end\";\n /** Clip to one line with an ellipsis. */\n truncate?: boolean;\n /**\n * An icon rendered beside the text, hidden from assistive tech. Sized to the\n * text with `1em`, so the same SVG fits every variant.\n */\n icon?: ReactNode;\n iconPosition?: \"leading\" | \"trailing\";\n /** The element to render. Defaults per variant (`h2` for a heading, and so on). */\n as?: TypographyElement;\n /**\n * Radix-style composition: render the child element itself, with these\n * styles merged onto it, instead of a wrapper. Takes precedence over `as`.\n */\n asChild?: boolean;\n ref?: Ref<HTMLElement>;\n children?: ReactNode;\n};\n\nconst DEFAULT_ELEMENT: Record<TypographyVariant, TypographyElement> = {\n display: \"h1\",\n heading: \"h2\",\n subheading: \"h3\",\n body: \"p\",\n label: \"span\",\n hint: \"p\",\n code: \"code\",\n};\n\nconst DEFAULT_WEIGHT: Record<TypographyVariant, TypographyWeight> = {\n display: \"semibold\",\n heading: \"semibold\",\n subheading: \"medium\",\n body: \"regular\",\n label: \"semibold\",\n hint: \"regular\",\n code: \"regular\",\n};\n\nconst VARIANT_CLASSES: Record<TypographyVariant, string> = {\n display: \"text-display leading-tight tracking-tight\",\n heading: \"text-heading leading-tight tracking-tight\",\n subheading: \"text-subheading leading-snug\",\n body: \"text-field leading-normal\",\n label: \"text-label leading-normal\",\n hint: \"text-hint leading-normal\",\n code: \"font-code text-field leading-normal\",\n};\n\nconst TONE_CLASSES: Record<TypographyTone, string> = {\n default: \"text-fg\",\n muted: \"text-fg-muted\",\n subtle: \"text-fg-subtle\",\n accent: \"text-accent\",\n \"on-accent\": \"text-on-accent\",\n success: \"text-success\",\n warning: \"text-warning\",\n info: \"text-info\",\n danger: \"text-danger\",\n};\n\nconst WEIGHT_CLASSES: Record<TypographyWeight, string> = {\n regular: \"font-normal\",\n medium: \"font-medium\",\n semibold: \"font-semibold\",\n bold: \"font-bold\",\n};\n\nconst ALIGN_CLASSES = {\n start: \"text-start\",\n center: \"text-center\",\n end: \"text-end\",\n} as const;\n\nconst ICON_CLASSES =\n \"inline-flex shrink-0 items-center [&>svg]:h-[1em] [&>svg]:w-[1em]\";\n\nfunction typographyClassName({\n variant,\n tone,\n weight,\n align,\n truncate,\n hasIcon,\n className,\n}: {\n variant: TypographyVariant;\n tone: TypographyTone;\n weight: TypographyWeight;\n align: \"start\" | \"center\" | \"end\" | undefined;\n truncate: boolean;\n hasIcon: boolean;\n className: string | undefined;\n}): string {\n return [\n VARIANT_CLASSES[variant],\n TONE_CLASSES[tone],\n WEIGHT_CLASSES[weight],\n align ? ALIGN_CLASSES[align] : \"\",\n hasIcon ? \"inline-flex items-center gap-1.5\" : \"\",\n // With an icon the root is a flex container, and text-overflow only\n // clips block text, so the text span carries the truncation instead.\n truncate && !hasIcon ? \"truncate\" : \"\",\n className ?? \"\",\n ]\n .filter(Boolean)\n .join(\" \");\n}\n\nexport function Typography({\n variant = \"body\",\n tone = \"default\",\n weight,\n align,\n truncate = false,\n icon,\n iconPosition = \"leading\",\n as,\n asChild = false,\n className,\n children,\n ...rest\n}: TypographyProps) {\n const hasIcon = icon !== undefined && icon !== null;\n // Widened to `ElementType`: the union of intrinsic tags would otherwise\n // demand a `ref` typed to one specific element.\n const Component: ElementType = asChild\n ? Slot\n : (as ?? DEFAULT_ELEMENT[variant]);\n\n const iconNode = hasIcon ? (\n <span aria-hidden=\"true\" className={ICON_CLASSES}>\n {icon}\n </span>\n ) : null;\n\n const content = hasIcon ? (\n <>\n {iconPosition === \"leading\" ? iconNode : null}\n <span className={truncate ? \"min-w-0 truncate\" : undefined}>\n {children}\n </span>\n {iconPosition === \"trailing\" ? iconNode : null}\n </>\n ) : (\n children\n );\n\n return (\n <Component\n {...rest}\n className={typographyClassName({\n variant,\n tone,\n weight: weight ?? DEFAULT_WEIGHT[variant],\n align,\n truncate,\n hasIcon,\n className,\n })}\n >\n {content}\n </Component>\n );\n}\n","import { Slot } from \"@radix-ui/react-slot\";\nimport type { ButtonHTMLAttributes, ReactNode, Ref } from \"react\";\nimport { Typography, type TypographyTone } from \"@/components/typography\";\n\n/**\n * The design's primary action, as the auth screens use it: a 40px (or 44px)\n * accent-filled control with the field type scale, a 4px radius and the\n * shared 45% disabled opacity. `secondary` and `ghost` cover the quieter\n * actions that sit beside it.\n *\n * Server-safe: no state, no handlers of its own. `asChild` composes onto a\n * consumer's element — a `next/link` for a navigation styled as a button —\n * through Radix `Slot`, so the link keeps its own semantics. `loading` swaps\n * the label for a spinner and disables the control; it applies to the\n * rendered `<button>` only, since a child element owns its own content.\n */\n\nexport type ButtonVariant = \"primary\" | \"secondary\" | \"ghost\";\nexport type ButtonSize = \"md\" | \"lg\";\n\nexport type ButtonProps = Omit<\n ButtonHTMLAttributes<HTMLButtonElement>,\n \"children\"\n> & {\n variant?: ButtonVariant;\n size?: ButtonSize;\n /** Stretch to the container, as the auth forms' submit does. */\n fullWidth?: boolean;\n /** Shows a spinner in place of the label and disables the control. */\n loading?: boolean;\n /** Shown next to the spinner while `loading`; nothing but the spinner otherwise. */\n loadingLabel?: ReactNode;\n /** Merge the styles onto the child element instead of rendering a button. */\n asChild?: boolean;\n ref?: Ref<HTMLButtonElement>;\n children: ReactNode;\n};\n\nconst BASE_CLASSES =\n \"inline-flex shrink-0 items-center justify-center gap-2 rounded-field border font-[inherit] text-field font-semibold outline-none transition-colors focus-visible:ring-3 focus-visible:ring-accent-soft disabled:cursor-not-allowed disabled:opacity-45\";\n\nconst VARIANT_CLASSES: Record<ButtonVariant, string> = {\n primary:\n \"border-transparent bg-accent text-on-accent enabled:hover:bg-accent-2\",\n secondary: \"border-line bg-surface text-fg enabled:hover:bg-surface-2\",\n ghost: \"border-transparent bg-transparent text-fg enabled:hover:bg-surface-2\",\n};\n\nconst SIZE_CLASSES: Record<ButtonSize, string> = {\n md: \"h-10 px-4\",\n lg: \"h-11 px-5\",\n};\n\nconst LABEL_TONE: Record<ButtonVariant, TypographyTone> = {\n primary: \"on-accent\",\n secondary: \"default\",\n ghost: \"default\",\n};\n\nfunction buttonClassName({\n variant,\n size,\n fullWidth,\n className,\n}: {\n variant: ButtonVariant;\n size: ButtonSize;\n fullWidth: boolean;\n className: string | undefined;\n}): string {\n return [\n BASE_CLASSES,\n VARIANT_CLASSES[variant],\n SIZE_CLASSES[size],\n fullWidth ? \"w-full\" : \"\",\n className ?? \"\",\n ]\n .filter(Boolean)\n .join(\" \");\n}\n\nfunction Spinner() {\n return (\n <svg\n className=\"h-4 w-4 animate-spin\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n aria-hidden=\"true\"\n data-testid=\"button-spinner\"\n >\n <circle\n className=\"opacity-25\"\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n strokeWidth=\"4\"\n />\n <path\n className=\"opacity-75\"\n fill=\"currentColor\"\n d=\"M4 12a8 8 0 0 1 8-8V0C5.373 0 0 5.373 0 12H4Z\"\n />\n </svg>\n );\n}\n\nexport function Button({\n variant = \"primary\",\n size = \"md\",\n fullWidth = false,\n loading = false,\n loadingLabel,\n asChild = false,\n type = \"button\",\n disabled,\n className,\n children,\n ...rest\n}: ButtonProps) {\n const classes = buttonClassName({ variant, size, fullWidth, className });\n\n if (asChild) {\n return (\n <Slot {...rest} className={classes}>\n {children}\n </Slot>\n );\n }\n\n return (\n <button\n {...rest}\n type={type}\n disabled={disabled || loading}\n aria-busy={loading || undefined}\n className={classes}\n >\n {loading ? (\n <>\n <Spinner />\n {loadingLabel}\n </>\n ) : (\n <Typography\n as=\"span\"\n variant=\"body\"\n weight=\"semibold\"\n tone={LABEL_TONE[variant]}\n >\n {children}\n </Typography>\n )}\n </button>\n );\n}\n"]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ButtonHTMLAttributes, ReactNode, Ref } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The design's primary action, as the auth screens use it: a 40px (or 44px)
|
|
6
|
+
* accent-filled control with the field type scale, a 4px radius and the
|
|
7
|
+
* shared 45% disabled opacity. `secondary` and `ghost` cover the quieter
|
|
8
|
+
* actions that sit beside it.
|
|
9
|
+
*
|
|
10
|
+
* Server-safe: no state, no handlers of its own. `asChild` composes onto a
|
|
11
|
+
* consumer's element — a `next/link` for a navigation styled as a button —
|
|
12
|
+
* through Radix `Slot`, so the link keeps its own semantics. `loading` swaps
|
|
13
|
+
* the label for a spinner and disables the control; it applies to the
|
|
14
|
+
* rendered `<button>` only, since a child element owns its own content.
|
|
15
|
+
*/
|
|
16
|
+
type ButtonVariant = "primary" | "secondary" | "ghost";
|
|
17
|
+
type ButtonSize = "md" | "lg";
|
|
18
|
+
type ButtonProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children"> & {
|
|
19
|
+
variant?: ButtonVariant;
|
|
20
|
+
size?: ButtonSize;
|
|
21
|
+
/** Stretch to the container, as the auth forms' submit does. */
|
|
22
|
+
fullWidth?: boolean;
|
|
23
|
+
/** Shows a spinner in place of the label and disables the control. */
|
|
24
|
+
loading?: boolean;
|
|
25
|
+
/** Shown next to the spinner while `loading`; nothing but the spinner otherwise. */
|
|
26
|
+
loadingLabel?: ReactNode;
|
|
27
|
+
/** Merge the styles onto the child element instead of rendering a button. */
|
|
28
|
+
asChild?: boolean;
|
|
29
|
+
ref?: Ref<HTMLButtonElement>;
|
|
30
|
+
children: ReactNode;
|
|
31
|
+
};
|
|
32
|
+
declare function Button({ variant, size, fullWidth, loading, loadingLabel, asChild, type, disabled, className, children, ...rest }: ButtonProps): react.JSX.Element;
|
|
33
|
+
|
|
34
|
+
export { Button, type ButtonProps, type ButtonSize, type ButtonVariant };
|
package/dist/button.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ButtonHTMLAttributes, ReactNode, Ref } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The design's primary action, as the auth screens use it: a 40px (or 44px)
|
|
6
|
+
* accent-filled control with the field type scale, a 4px radius and the
|
|
7
|
+
* shared 45% disabled opacity. `secondary` and `ghost` cover the quieter
|
|
8
|
+
* actions that sit beside it.
|
|
9
|
+
*
|
|
10
|
+
* Server-safe: no state, no handlers of its own. `asChild` composes onto a
|
|
11
|
+
* consumer's element — a `next/link` for a navigation styled as a button —
|
|
12
|
+
* through Radix `Slot`, so the link keeps its own semantics. `loading` swaps
|
|
13
|
+
* the label for a spinner and disables the control; it applies to the
|
|
14
|
+
* rendered `<button>` only, since a child element owns its own content.
|
|
15
|
+
*/
|
|
16
|
+
type ButtonVariant = "primary" | "secondary" | "ghost";
|
|
17
|
+
type ButtonSize = "md" | "lg";
|
|
18
|
+
type ButtonProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children"> & {
|
|
19
|
+
variant?: ButtonVariant;
|
|
20
|
+
size?: ButtonSize;
|
|
21
|
+
/** Stretch to the container, as the auth forms' submit does. */
|
|
22
|
+
fullWidth?: boolean;
|
|
23
|
+
/** Shows a spinner in place of the label and disables the control. */
|
|
24
|
+
loading?: boolean;
|
|
25
|
+
/** Shown next to the spinner while `loading`; nothing but the spinner otherwise. */
|
|
26
|
+
loadingLabel?: ReactNode;
|
|
27
|
+
/** Merge the styles onto the child element instead of rendering a button. */
|
|
28
|
+
asChild?: boolean;
|
|
29
|
+
ref?: Ref<HTMLButtonElement>;
|
|
30
|
+
children: ReactNode;
|
|
31
|
+
};
|
|
32
|
+
declare function Button({ variant, size, fullWidth, loading, loadingLabel, asChild, type, disabled, className, children, ...rest }: ButtonProps): react.JSX.Element;
|
|
33
|
+
|
|
34
|
+
export { Button, type ButtonProps, type ButtonSize, type ButtonVariant };
|
package/dist/button.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"button.js"}
|