@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.
Files changed (56) hide show
  1. package/README.md +139 -60
  2. package/dist/button.cjs +223 -0
  3. package/dist/button.cjs.map +1 -0
  4. package/dist/button.d.cts +34 -0
  5. package/dist/button.d.ts +34 -0
  6. package/dist/button.js +4 -0
  7. package/dist/button.js.map +1 -0
  8. package/dist/chunk-HDTW4FRT.js +111 -0
  9. package/dist/chunk-HDTW4FRT.js.map +1 -0
  10. package/dist/email-input.cjs +213 -170
  11. package/dist/email-input.cjs.map +1 -1
  12. package/dist/email-input.d.cts +4 -6
  13. package/dist/email-input.d.ts +4 -6
  14. package/dist/email-input.js +219 -170
  15. package/dist/email-input.js.map +1 -1
  16. package/dist/field.cjs +206 -0
  17. package/dist/field.cjs.map +1 -0
  18. package/dist/field.d.cts +80 -0
  19. package/dist/field.d.ts +80 -0
  20. package/dist/field.js +186 -0
  21. package/dist/field.js.map +1 -0
  22. package/dist/form.cjs +283 -42
  23. package/dist/form.cjs.map +1 -1
  24. package/dist/form.d.cts +79 -10
  25. package/dist/form.d.ts +79 -10
  26. package/dist/form.js +285 -45
  27. package/dist/form.js.map +1 -1
  28. package/dist/index-CdYkd6zz.d.cts +22 -0
  29. package/dist/index-CdYkd6zz.d.ts +22 -0
  30. package/dist/index.cjs +138 -34
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.d.cts +116 -55
  33. package/dist/index.d.ts +116 -55
  34. package/dist/index.js +1 -0
  35. package/dist/password-input.cjs +217 -174
  36. package/dist/password-input.cjs.map +1 -1
  37. package/dist/password-input.d.cts +2 -3
  38. package/dist/password-input.d.ts +2 -3
  39. package/dist/password-input.js +222 -173
  40. package/dist/password-input.js.map +1 -1
  41. package/dist/phone-input.cjs +213 -170
  42. package/dist/phone-input.cjs.map +1 -1
  43. package/dist/phone-input.d.cts +5 -5
  44. package/dist/phone-input.d.ts +5 -5
  45. package/dist/phone-input.js +219 -170
  46. package/dist/phone-input.js.map +1 -1
  47. package/dist/styles.css +1 -1
  48. package/dist/text-input.cjs +213 -170
  49. package/dist/text-input.cjs.map +1 -1
  50. package/dist/text-input.d.cts +2 -3
  51. package/dist/text-input.d.ts +2 -3
  52. package/dist/text-input.js +219 -170
  53. package/dist/text-input.js.map +1 -1
  54. package/package.json +26 -2
  55. package/dist/index-DwjxC4l4.d.cts +0 -62
  56. 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`). `zod` (`^4`) is an optional
17
- peer dependency, needed only for `@carbonbits/sixr/validation`.
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 with optional schema validation. Each one is for a single
120
- kind of value and fixes the input type, keyboard and autofill hints that value needs:
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 | Type | Default | Description |
135
- | -------------------- | ------------------------------------ | -------- | ----------------------------------------------------- |
136
- | `label` | `string` | — | Field label, associated with the input. |
137
- | `hint` | `ReactNode` | — | Guidance below the field. Replaced by an error. |
138
- | `error` | `string` | — | Caller-supplied error. Outranks the schema. |
139
- | `schema` | `FieldSchema` | — | Any zod schema. Only `safeParse` is called. |
140
- | `validateOn` | `"blur" \| "change" \| "never"` | `"blur"` | When the schema runs. |
141
- | `onValidationChange` | `(state: FieldValidation) => void` | — | Fires on each validation pass. |
142
- | `onChange` | `(value: string) => void` | — | Receives the value, not the event. |
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 there is one.
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
- Validation runs on blur by default, then on every keystroke once the field has gone
173
- invalid, so a corrected value clears the message immediately. The first schema issue
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
- `schema` is structurally typed, so any zod schema satisfies it without zod appearing
178
- in this package's types — you only need zod installed if you use it. The inputs never
179
- default to a schema for the same reason; pair them with the ready-made ones under
180
- "Validation schemas".
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
- The shared skeleton behind the login/signup screens: a `<form>` that disables its
189
- fields for the duration of `onSubmit`, plus a submit button that swaps to a spinner
190
- for the same duration. Interactive, so it ships its own `"use client"` directive and
191
- is reachable only at this subpath.
192
-
193
- | Component | Prop | Type | Description |
194
- | ------------- | -------------- | -------------------------- | --------------------------------------------------------- |
195
- | `Form` | `onSubmit` | `() => void \| Promise<void>` | Called after `preventDefault`. A returned promise drives the disabled/spinner state until it settles. |
196
- | `Form` | `children` | `ReactNode` | Fields, `FormError`, `SubmitButton` — whatever the design calls for, in order. |
197
- | `FormError` | `children` | `ReactNode` | Form-level error (a rejected credential, a failed request). Renders nothing when empty. |
198
- | `SubmitButton`| `loadingLabel` | `ReactNode` | Shown next to the spinner in place of `children` while submitting. |
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
- function LoginForm() {
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
- const handleSubmit = async () => {
211
- setError(undefined);
241
+ function LoginForm() {
242
+ const handleSubmit = async ({ email, password }: z.output<typeof loginSchema>) => {
212
243
  const ok = await signIn(email, password);
213
- if (!ok) setError("That email and password don't match.");
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" onChange={setEmail} />
219
- <PasswordInput label="Password" onChange={setPassword} />
220
- <FormError>{error}</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.
@@ -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 };
@@ -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,4 @@
1
+ export { Button } from './chunk-HDTW4FRT.js';
2
+ import './chunk-NZHHNVXZ.js';
3
+ //# sourceMappingURL=button.js.map
4
+ //# sourceMappingURL=button.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"button.js"}