@carbonbits/sixr 1.3.0 → 1.4.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 (42) hide show
  1. package/README.md +77 -18
  2. package/dist/chunk-NZHHNVXZ.js +118 -0
  3. package/dist/chunk-NZHHNVXZ.js.map +1 -0
  4. package/dist/email-input.cjs +390 -0
  5. package/dist/email-input.cjs.map +1 -0
  6. package/dist/email-input.d.cts +14 -0
  7. package/dist/email-input.d.ts +14 -0
  8. package/dist/email-input.js +367 -0
  9. package/dist/email-input.js.map +1 -0
  10. package/dist/index-DwjxC4l4.d.cts +62 -0
  11. package/dist/index-DwjxC4l4.d.ts +62 -0
  12. package/dist/index.cjs +112 -0
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.cts +48 -9
  15. package/dist/index.d.ts +48 -9
  16. package/dist/index.js +1 -0
  17. package/dist/password-input.cjs +406 -0
  18. package/dist/password-input.cjs.map +1 -0
  19. package/dist/password-input.d.cts +20 -0
  20. package/dist/password-input.d.ts +20 -0
  21. package/dist/password-input.js +385 -0
  22. package/dist/password-input.js.map +1 -0
  23. package/dist/phone-input.cjs +386 -0
  24. package/dist/phone-input.cjs.map +1 -0
  25. package/dist/phone-input.d.cts +12 -0
  26. package/dist/phone-input.d.ts +12 -0
  27. package/dist/phone-input.js +363 -0
  28. package/dist/phone-input.js.map +1 -0
  29. package/dist/styles.css +1 -1
  30. package/dist/text-input.cjs +228 -59
  31. package/dist/text-input.cjs.map +1 -1
  32. package/dist/text-input.d.cts +10 -58
  33. package/dist/text-input.d.ts +10 -58
  34. package/dist/text-input.js +226 -60
  35. package/dist/text-input.js.map +1 -1
  36. package/dist/typography.cjs +120 -0
  37. package/dist/typography.cjs.map +1 -0
  38. package/dist/typography.d.cts +46 -0
  39. package/dist/typography.d.ts +46 -0
  40. package/dist/typography.js +3 -0
  41. package/dist/typography.js.map +1 -0
  42. package/package.json +45 -1
package/README.md CHANGED
@@ -70,43 +70,99 @@ type CartProduct = {
70
70
  Pass an `id` whenever a cart can hold two lines with the same name. Prices are
71
71
  formatted with `Intl.NumberFormat`, and the total is summed from `price`.
72
72
 
73
- ### `TextInput`
73
+ ### `Typography`
74
+
75
+ ```tsx
76
+ import { Typography } from "@carbonbits/sixr/typography";
77
+ ```
78
+
79
+ The type scale. Every piece of text the library renders goes through it, and it is
80
+ the component to reach for when yours does too. Server-safe.
81
+
82
+ | Prop | Type | Default | Description |
83
+ | -------------- | ------------------------------------------------------------------------- | ------------------ | -------------------------------------------------------- |
84
+ | `variant` | `"display" \| "heading" \| "subheading" \| "body" \| "label" \| "hint" \| "code"` | `"body"` | Size, default element and default weight. |
85
+ | `tone` | `"default" \| "muted" \| "subtle" \| "accent" \| "on-accent" \| "success" \| "warning" \| "info" \| "danger"` | `"default"` | Colour role from the design tokens. |
86
+ | `weight` | `"regular" \| "medium" \| "semibold" \| "bold"` | per variant | Overrides the variant's weight. |
87
+ | `align` | `"start" \| "center" \| "end"` | — | Text alignment. |
88
+ | `truncate` | `boolean` | `false` | One line, clipped with an ellipsis. |
89
+ | `icon` | `ReactNode` | — | An icon beside the text, sized to it, `aria-hidden`. |
90
+ | `iconPosition` | `"leading" \| "trailing"` | `"leading"` | Which side the icon sits on. |
91
+ | `as` | `"p" \| "span" \| "div" \| "h1"…"h6" \| "label" \| "code" \| …` | per variant | The element to render. |
92
+ | `asChild` | `boolean` | `false` | Merge the styles onto the child element instead. |
93
+
94
+ Default elements: `display` → `h1`, `heading` → `h2`, `subheading` → `h3`, `body` and
95
+ `hint` → `p`, `label` → `span`, `code` → `code`. Any other HTML attribute passes through.
96
+
97
+ ```tsx
98
+ <Typography variant="heading">Welcome back</Typography>
99
+ <Typography tone="muted">Sign in to your Carbonbits workspace.</Typography>
100
+ <Typography variant="hint" tone="success" icon={<CheckIcon />}>
101
+ Verification code accepted
102
+ </Typography>
103
+
104
+ // Style your own element — a Next.js Link, a Radix Label — with `asChild`.
105
+ <Typography variant="label" tone="accent" asChild>
106
+ <Link href="/reset">Forgot your password?</Link>
107
+ </Typography>
108
+ ```
109
+
110
+ ### Inputs: `TextInput`, `EmailInput`, `PasswordInput`, `PhoneInput`
74
111
 
75
112
  ```tsx
76
113
  import { TextInput } from "@carbonbits/sixr/text-input";
114
+ import { EmailInput } from "@carbonbits/sixr/email-input";
115
+ import { PasswordInput } from "@carbonbits/sixr/password-input";
116
+ import { PhoneInput } from "@carbonbits/sixr/phone-input";
77
117
  ```
78
118
 
79
- A labelled text field with optional schema validation. Interactive, so it ships its
80
- own `"use client"` directive and is reachable only at this subpath — it is not on the
81
- root barrel, which stays server-safe.
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:
121
+
122
+ | Component | Value | Fixes |
123
+ | --------------- | ---------------- | ---------------------------------------------------------------------- |
124
+ | `TextInput` | Free text | `type="text"`. Adds `mono` for the monospace face (verification codes). |
125
+ | `EmailInput` | An email address | `type="email"`, `inputMode="email"`, `autoComplete="email"`, no autocapitalise or spellcheck. |
126
+ | `PasswordInput` | A secret | `type="password"`, `autoComplete="current-password"`, and a built-in Show/Hide toggle. |
127
+ | `PhoneInput` | A phone number | `type="tel"`, `inputMode="tel"`, `autoComplete="tel"`. |
128
+
129
+ Interactive, so each ships its own `"use client"` directive and is reachable only at
130
+ its subpath — none is on the root barrel, which stays server-safe.
131
+
132
+ They share these props:
82
133
 
83
134
  | Prop | Type | Default | Description |
84
135
  | -------------------- | ------------------------------------ | -------- | ----------------------------------------------------- |
85
136
  | `label` | `string` | — | Field label, associated with the input. |
86
137
  | `hint` | `ReactNode` | — | Guidance below the field. Replaced by an error. |
87
138
  | `error` | `string` | — | Caller-supplied error. Outranks the schema. |
88
- | `schema` | `TextInputSchema` | — | Any zod schema. Only `safeParse` is called. |
139
+ | `schema` | `FieldSchema` | — | Any zod schema. Only `safeParse` is called. |
89
140
  | `validateOn` | `"blur" \| "change" \| "never"` | `"blur"` | When the schema runs. |
90
- | `onValidationChange` | `(state: TextInputValidation) => void` | — | Fires on each validation pass. |
141
+ | `onValidationChange` | `(state: FieldValidation) => void` | — | Fires on each validation pass. |
91
142
  | `onChange` | `(value: string) => void` | — | Receives the value, not the event. |
92
143
  | `value` | `string` | — | Makes the field controlled. |
93
144
  | `defaultValue` | `string` | `""` | Initial value when uncontrolled. |
94
145
  | `optional` | `boolean` | `false` | Shows an "Optional" marker on the label. |
95
146
  | `action` | `ReactNode` | — | Trailing label slot, e.g. a "Forgot?" link. |
96
- | `trailing` | `ReactNode` | — | Trailing in-field slot, e.g. a password peek. |
97
- | `mono` | `boolean` | `false` | Renders the value in the monospace face. |
98
- | `type` | `"text" \| "email" \| "password" \| "tel" \| "url" \| "search"` | `"text"` | Input type. |
147
+ | `trailing` | `ReactNode` | — | Trailing in-field slot. Not on `PasswordInput`, which owns it. |
99
148
 
100
- Any other `<input>` attribute — `placeholder`, `maxLength`, `disabled`, `inputMode` —
101
- is passed straight through.
149
+ And their own:
150
+
151
+ | Component | Prop | Type | Default | Description |
152
+ | --------------- | -------------- | -------------------------------- | -------------------------------- | ------------------------------------ |
153
+ | `TextInput` | `mono` | `boolean` | `false` | Renders the value in the monospace face. |
154
+ | `PasswordInput` | `revealable` | `boolean` | `true` | Shows the Show/Hide toggle. |
155
+ | `PasswordInput` | `revealLabels` | `{ show: string; hide: string }` | `{ show: "Show", hide: "Hide" }` | Toggle text, for localisation. |
156
+
157
+ Any other `<input>` attribute — `placeholder`, `maxLength`, `disabled`, `autoComplete` —
158
+ is passed straight through, and overrides the component's default where there is one.
102
159
 
103
160
  ```tsx
104
- import { TextInput } from "@carbonbits/sixr/text-input";
161
+ import { EmailInput } from "@carbonbits/sixr/email-input";
105
162
  import { emailSchema } from "@carbonbits/sixr/validation";
106
163
 
107
- <TextInput
164
+ <EmailInput
108
165
  label="Work email"
109
- type="email"
110
166
  placeholder="you@company.com"
111
167
  schema={emailSchema}
112
168
  onChange={setEmail}
@@ -119,7 +175,9 @@ is shown, the input gets `aria-invalid`, and the message is announced via `role=
119
175
  and wired up with `aria-describedby`.
120
176
 
121
177
  `schema` is structurally typed, so any zod schema satisfies it without zod appearing
122
- in this package's types — you only need zod installed if you use it.
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".
123
181
 
124
182
  ### `Form` / `FormError` / `SubmitButton`
125
183
 
@@ -140,8 +198,9 @@ is reachable only at this subpath.
140
198
  | `SubmitButton`| `loadingLabel` | `ReactNode` | Shown next to the spinner in place of `children` while submitting. |
141
199
 
142
200
  ```tsx
201
+ import { EmailInput } from "@carbonbits/sixr/email-input";
143
202
  import { Form, FormError, SubmitButton } from "@carbonbits/sixr/form";
144
- import { TextInput } from "@carbonbits/sixr/text-input";
203
+ import { PasswordInput } from "@carbonbits/sixr/password-input";
145
204
 
146
205
  function LoginForm() {
147
206
  const [email, setEmail] = useState("");
@@ -156,8 +215,8 @@ function LoginForm() {
156
215
 
157
216
  return (
158
217
  <Form onSubmit={handleSubmit}>
159
- <TextInput label="Work email" type="email" onChange={setEmail} />
160
- <TextInput label="Password" type="password" onChange={setPassword} />
218
+ <EmailInput label="Work email" onChange={setEmail} />
219
+ <PasswordInput label="Password" onChange={setPassword} />
161
220
  <FormError>{error}</FormError>
162
221
  <SubmitButton loadingLabel="Signing in…">Sign in</SubmitButton>
163
222
  </Form>
@@ -0,0 +1,118 @@
1
+ import { Slot } from '@radix-ui/react-slot';
2
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
+
4
+ // src/components/typography/index.tsx
5
+ var DEFAULT_ELEMENT = {
6
+ display: "h1",
7
+ heading: "h2",
8
+ subheading: "h3",
9
+ body: "p",
10
+ label: "span",
11
+ hint: "p",
12
+ code: "code"
13
+ };
14
+ var DEFAULT_WEIGHT = {
15
+ display: "semibold",
16
+ heading: "semibold",
17
+ subheading: "medium",
18
+ body: "regular",
19
+ label: "semibold",
20
+ hint: "regular",
21
+ code: "regular"
22
+ };
23
+ var VARIANT_CLASSES = {
24
+ display: "text-display leading-tight tracking-tight",
25
+ heading: "text-heading leading-tight tracking-tight",
26
+ subheading: "text-subheading leading-snug",
27
+ body: "text-field leading-normal",
28
+ label: "text-label leading-normal",
29
+ hint: "text-hint leading-normal",
30
+ code: "font-code text-field leading-normal"
31
+ };
32
+ var TONE_CLASSES = {
33
+ default: "text-fg",
34
+ muted: "text-fg-muted",
35
+ subtle: "text-fg-subtle",
36
+ accent: "text-accent",
37
+ "on-accent": "text-on-accent",
38
+ success: "text-success",
39
+ warning: "text-warning",
40
+ info: "text-info",
41
+ danger: "text-danger"
42
+ };
43
+ var WEIGHT_CLASSES = {
44
+ regular: "font-normal",
45
+ medium: "font-medium",
46
+ semibold: "font-semibold",
47
+ bold: "font-bold"
48
+ };
49
+ var ALIGN_CLASSES = {
50
+ start: "text-start",
51
+ center: "text-center",
52
+ end: "text-end"
53
+ };
54
+ var ICON_CLASSES = "inline-flex shrink-0 items-center [&>svg]:h-[1em] [&>svg]:w-[1em]";
55
+ function typographyClassName({
56
+ variant,
57
+ tone,
58
+ weight,
59
+ align,
60
+ truncate,
61
+ hasIcon,
62
+ className
63
+ }) {
64
+ return [
65
+ VARIANT_CLASSES[variant],
66
+ TONE_CLASSES[tone],
67
+ WEIGHT_CLASSES[weight],
68
+ align ? ALIGN_CLASSES[align] : "",
69
+ hasIcon ? "inline-flex items-center gap-1.5" : "",
70
+ // With an icon the root is a flex container, and text-overflow only
71
+ // clips block text, so the text span carries the truncation instead.
72
+ truncate && !hasIcon ? "truncate" : "",
73
+ className ?? ""
74
+ ].filter(Boolean).join(" ");
75
+ }
76
+ function Typography({
77
+ variant = "body",
78
+ tone = "default",
79
+ weight,
80
+ align,
81
+ truncate = false,
82
+ icon,
83
+ iconPosition = "leading",
84
+ as,
85
+ asChild = false,
86
+ className,
87
+ children,
88
+ ...rest
89
+ }) {
90
+ const hasIcon = icon !== void 0 && icon !== null;
91
+ const Component = asChild ? Slot : as ?? DEFAULT_ELEMENT[variant];
92
+ const iconNode = hasIcon ? /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: ICON_CLASSES, children: icon }) : null;
93
+ const content = hasIcon ? /* @__PURE__ */ jsxs(Fragment, { children: [
94
+ iconPosition === "leading" ? iconNode : null,
95
+ /* @__PURE__ */ jsx("span", { className: truncate ? "min-w-0 truncate" : void 0, children }),
96
+ iconPosition === "trailing" ? iconNode : null
97
+ ] }) : children;
98
+ return /* @__PURE__ */ jsx(
99
+ Component,
100
+ {
101
+ ...rest,
102
+ className: typographyClassName({
103
+ variant,
104
+ tone,
105
+ weight: weight ?? DEFAULT_WEIGHT[variant],
106
+ align,
107
+ truncate,
108
+ hasIcon,
109
+ className
110
+ }),
111
+ children: content
112
+ }
113
+ );
114
+ }
115
+
116
+ export { Typography };
117
+ //# sourceMappingURL=chunk-NZHHNVXZ.js.map
118
+ //# sourceMappingURL=chunk-NZHHNVXZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/typography/index.tsx"],"names":[],"mappings":"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","file":"chunk-NZHHNVXZ.js","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"]}
@@ -0,0 +1,390 @@
1
+ "use client";
2
+ "use strict";
3
+ "use client";
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
21
+
22
+ // src/components/email-input/index.tsx
23
+ var email_input_exports = {};
24
+ __export(email_input_exports, {
25
+ EmailInput: () => EmailInput
26
+ });
27
+ module.exports = __toCommonJS(email_input_exports);
28
+
29
+ // src/components/field/index.tsx
30
+ var import_react_label = require("@radix-ui/react-label");
31
+ var import_react2 = require("react");
32
+
33
+ // src/components/field/use-field-behaviour.ts
34
+ var import_react = require("react");
35
+ var FALLBACK_MESSAGE = "Invalid value";
36
+ function runSchema(schema, value) {
37
+ if (!schema) {
38
+ return { valid: true };
39
+ }
40
+ const result = schema.safeParse(value);
41
+ if (result.success) {
42
+ return { valid: true };
43
+ }
44
+ return {
45
+ valid: false,
46
+ error: result.error.issues[0]?.message ?? FALLBACK_MESSAGE
47
+ };
48
+ }
49
+ function useFieldBehaviour({
50
+ value,
51
+ defaultValue,
52
+ error,
53
+ onChange,
54
+ onBlur,
55
+ schema,
56
+ validateOn = "blur",
57
+ onValidationChange
58
+ }) {
59
+ const [uncontrolledValue, setUncontrolledValue] = (0, import_react.useState)(
60
+ defaultValue ?? ""
61
+ );
62
+ const [schemaError, setSchemaError] = (0, import_react.useState)(void 0);
63
+ const isControlled = value !== void 0;
64
+ const validate = (0, import_react.useCallback)(
65
+ (next) => {
66
+ const state = runSchema(schema, next);
67
+ setSchemaError(state.error);
68
+ onValidationChange?.(state);
69
+ },
70
+ [schema, onValidationChange]
71
+ );
72
+ const handleChange = (0, import_react.useCallback)(
73
+ (event) => {
74
+ const next = event.target.value;
75
+ if (!isControlled) {
76
+ setUncontrolledValue(next);
77
+ }
78
+ onChange?.(next);
79
+ if (validateOn === "change" || validateOn === "blur" && schemaError) {
80
+ validate(next);
81
+ }
82
+ },
83
+ [isControlled, onChange, validateOn, schemaError, validate]
84
+ );
85
+ const handleBlur = (0, import_react.useCallback)(
86
+ (event) => {
87
+ if (validateOn === "blur") {
88
+ validate(event.target.value);
89
+ }
90
+ onBlur?.(event);
91
+ },
92
+ [validateOn, validate, onBlur]
93
+ );
94
+ return {
95
+ currentValue: isControlled ? value : uncontrolledValue,
96
+ // A caller-supplied error outranks the schema's verdict.
97
+ shownError: error ?? schemaError,
98
+ handleChange,
99
+ handleBlur
100
+ };
101
+ }
102
+
103
+ // src/components/typography/index.tsx
104
+ var import_react_slot = require("@radix-ui/react-slot");
105
+ var import_jsx_runtime = require("react/jsx-runtime");
106
+ var DEFAULT_ELEMENT = {
107
+ display: "h1",
108
+ heading: "h2",
109
+ subheading: "h3",
110
+ body: "p",
111
+ label: "span",
112
+ hint: "p",
113
+ code: "code"
114
+ };
115
+ var DEFAULT_WEIGHT = {
116
+ display: "semibold",
117
+ heading: "semibold",
118
+ subheading: "medium",
119
+ body: "regular",
120
+ label: "semibold",
121
+ hint: "regular",
122
+ code: "regular"
123
+ };
124
+ var VARIANT_CLASSES = {
125
+ display: "text-display leading-tight tracking-tight",
126
+ heading: "text-heading leading-tight tracking-tight",
127
+ subheading: "text-subheading leading-snug",
128
+ body: "text-field leading-normal",
129
+ label: "text-label leading-normal",
130
+ hint: "text-hint leading-normal",
131
+ code: "font-code text-field leading-normal"
132
+ };
133
+ var TONE_CLASSES = {
134
+ default: "text-fg",
135
+ muted: "text-fg-muted",
136
+ subtle: "text-fg-subtle",
137
+ accent: "text-accent",
138
+ "on-accent": "text-on-accent",
139
+ success: "text-success",
140
+ warning: "text-warning",
141
+ info: "text-info",
142
+ danger: "text-danger"
143
+ };
144
+ var WEIGHT_CLASSES = {
145
+ regular: "font-normal",
146
+ medium: "font-medium",
147
+ semibold: "font-semibold",
148
+ bold: "font-bold"
149
+ };
150
+ var ALIGN_CLASSES = {
151
+ start: "text-start",
152
+ center: "text-center",
153
+ end: "text-end"
154
+ };
155
+ var ICON_CLASSES = "inline-flex shrink-0 items-center [&>svg]:h-[1em] [&>svg]:w-[1em]";
156
+ function typographyClassName({
157
+ variant,
158
+ tone,
159
+ weight,
160
+ align,
161
+ truncate,
162
+ hasIcon,
163
+ className
164
+ }) {
165
+ return [
166
+ VARIANT_CLASSES[variant],
167
+ TONE_CLASSES[tone],
168
+ WEIGHT_CLASSES[weight],
169
+ align ? ALIGN_CLASSES[align] : "",
170
+ hasIcon ? "inline-flex items-center gap-1.5" : "",
171
+ // With an icon the root is a flex container, and text-overflow only
172
+ // clips block text, so the text span carries the truncation instead.
173
+ truncate && !hasIcon ? "truncate" : "",
174
+ className ?? ""
175
+ ].filter(Boolean).join(" ");
176
+ }
177
+ function Typography({
178
+ variant = "body",
179
+ tone = "default",
180
+ weight,
181
+ align,
182
+ truncate = false,
183
+ icon,
184
+ iconPosition = "leading",
185
+ as,
186
+ asChild = false,
187
+ className,
188
+ children,
189
+ ...rest
190
+ }) {
191
+ const hasIcon = icon !== void 0 && icon !== null;
192
+ const Component = asChild ? import_react_slot.Slot : as ?? DEFAULT_ELEMENT[variant];
193
+ const iconNode = hasIcon ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", className: ICON_CLASSES, children: icon }) : null;
194
+ const content = hasIcon ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
195
+ iconPosition === "leading" ? iconNode : null,
196
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: truncate ? "min-w-0 truncate" : void 0, children }),
197
+ iconPosition === "trailing" ? iconNode : null
198
+ ] }) : children;
199
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
200
+ Component,
201
+ {
202
+ ...rest,
203
+ className: typographyClassName({
204
+ variant,
205
+ tone,
206
+ weight: weight ?? DEFAULT_WEIGHT[variant],
207
+ align,
208
+ truncate,
209
+ hasIcon,
210
+ className
211
+ }),
212
+ children: content
213
+ }
214
+ );
215
+ }
216
+
217
+ // src/components/field/index.tsx
218
+ var import_jsx_runtime2 = require("react/jsx-runtime");
219
+ var FIELD_CLASSES = "h-10 w-full rounded-field border bg-surface px-3 font-[inherit] text-field text-fg outline-none placeholder:text-fg-subtle disabled:cursor-not-allowed disabled:opacity-45";
220
+ var VALID_CLASSES = "border-line focus:border-accent focus:ring-3 focus:ring-accent-soft";
221
+ var INVALID_CLASSES = "border-danger focus:border-danger focus:ring-3 focus:ring-danger-soft";
222
+ function fieldClassName(invalid, hasTrailing, className) {
223
+ return [
224
+ FIELD_CLASSES,
225
+ invalid ? INVALID_CLASSES : VALID_CLASSES,
226
+ hasTrailing ? "pr-16" : "",
227
+ className ?? ""
228
+ ].filter(Boolean).join(" ");
229
+ }
230
+ function FieldLabel({
231
+ htmlFor,
232
+ label,
233
+ optional,
234
+ action
235
+ }) {
236
+ if (!label) {
237
+ return null;
238
+ }
239
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
240
+ Typography,
241
+ {
242
+ variant: "label",
243
+ tone: "muted",
244
+ className: "mb-1.5 flex items-center",
245
+ asChild: true,
246
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_react_label.Label, { htmlFor, children: [
247
+ label,
248
+ optional ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
249
+ Typography,
250
+ {
251
+ as: "span",
252
+ variant: "hint",
253
+ tone: "subtle",
254
+ weight: "semibold",
255
+ className: "ml-auto",
256
+ children: "Optional"
257
+ }
258
+ ) : null,
259
+ action ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "ml-auto", children: action }) : null
260
+ ] })
261
+ }
262
+ );
263
+ }
264
+ function FieldMessage({
265
+ id,
266
+ error,
267
+ hint
268
+ }) {
269
+ if (error) {
270
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
271
+ Typography,
272
+ {
273
+ as: "p",
274
+ variant: "hint",
275
+ tone: "danger",
276
+ id,
277
+ role: "alert",
278
+ className: "mt-1.5",
279
+ children: error
280
+ }
281
+ );
282
+ }
283
+ if (hint) {
284
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
285
+ Typography,
286
+ {
287
+ as: "p",
288
+ variant: "hint",
289
+ tone: "subtle",
290
+ id,
291
+ className: "mt-1.5",
292
+ children: hint
293
+ }
294
+ );
295
+ }
296
+ return null;
297
+ }
298
+ function Field({
299
+ label,
300
+ hint,
301
+ error,
302
+ optional = false,
303
+ action,
304
+ trailing,
305
+ type,
306
+ value,
307
+ defaultValue,
308
+ onChange,
309
+ schema,
310
+ validateOn = "blur",
311
+ onValidationChange,
312
+ id,
313
+ className,
314
+ onBlur,
315
+ ...rest
316
+ }) {
317
+ const generatedId = (0, import_react2.useId)();
318
+ const inputId = id ?? generatedId;
319
+ const describedById = `${inputId}-description`;
320
+ const { currentValue, shownError, handleChange, handleBlur } = useFieldBehaviour({
321
+ value,
322
+ defaultValue,
323
+ error,
324
+ onChange,
325
+ onBlur,
326
+ schema,
327
+ validateOn,
328
+ onValidationChange
329
+ });
330
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex flex-col", children: [
331
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
332
+ FieldLabel,
333
+ {
334
+ htmlFor: inputId,
335
+ label,
336
+ optional,
337
+ action
338
+ }
339
+ ),
340
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: trailing ? "relative" : void 0, children: [
341
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
342
+ "input",
343
+ {
344
+ ...rest,
345
+ id: inputId,
346
+ type,
347
+ value: currentValue,
348
+ "aria-invalid": shownError ? true : void 0,
349
+ "aria-describedby": shownError || hint ? describedById : void 0,
350
+ onChange: handleChange,
351
+ onBlur: handleBlur,
352
+ className: fieldClassName(
353
+ Boolean(shownError),
354
+ Boolean(trailing),
355
+ className
356
+ )
357
+ }
358
+ ),
359
+ trailing ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "absolute top-1/2 right-[9px] flex -translate-y-1/2 items-center text-hint font-semibold text-fg-subtle", children: trailing }) : null
360
+ ] }),
361
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(FieldMessage, { id: describedById, error: shownError, hint })
362
+ ] });
363
+ }
364
+
365
+ // src/components/email-input/index.tsx
366
+ var import_jsx_runtime3 = require("react/jsx-runtime");
367
+ function EmailInput({
368
+ autoComplete = "email",
369
+ inputMode = "email",
370
+ autoCapitalize = "none",
371
+ spellCheck = false,
372
+ ...rest
373
+ }) {
374
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
375
+ Field,
376
+ {
377
+ ...rest,
378
+ type: "email",
379
+ autoComplete,
380
+ inputMode,
381
+ autoCapitalize,
382
+ spellCheck
383
+ }
384
+ );
385
+ }
386
+ // Annotate the CommonJS export names for ESM import in node:
387
+ 0 && (module.exports = {
388
+ EmailInput
389
+ });
390
+ //# sourceMappingURL=email-input.cjs.map