@carbonbits/sixr 1.2.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.
- package/README.md +124 -15
- package/dist/chunk-NZHHNVXZ.js +118 -0
- package/dist/chunk-NZHHNVXZ.js.map +1 -0
- package/dist/email-input.cjs +390 -0
- package/dist/email-input.cjs.map +1 -0
- package/dist/email-input.d.cts +14 -0
- package/dist/email-input.d.ts +14 -0
- package/dist/email-input.js +367 -0
- package/dist/email-input.js.map +1 -0
- package/dist/form.cjs +124 -0
- package/dist/form.cjs.map +1 -0
- package/dist/form.d.cts +17 -0
- package/dist/form.d.ts +17 -0
- package/dist/form.js +103 -0
- package/dist/form.js.map +1 -0
- package/dist/index-DwjxC4l4.d.cts +62 -0
- package/dist/index-DwjxC4l4.d.ts +62 -0
- package/dist/index.cjs +112 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +48 -9
- package/dist/index.d.ts +48 -9
- package/dist/index.js +1 -0
- package/dist/password-input.cjs +406 -0
- package/dist/password-input.cjs.map +1 -0
- package/dist/password-input.d.cts +20 -0
- package/dist/password-input.d.ts +20 -0
- package/dist/password-input.js +385 -0
- package/dist/password-input.js.map +1 -0
- package/dist/phone-input.cjs +386 -0
- package/dist/phone-input.cjs.map +1 -0
- package/dist/phone-input.d.cts +12 -0
- package/dist/phone-input.d.ts +12 -0
- package/dist/phone-input.js +363 -0
- package/dist/phone-input.js.map +1 -0
- package/dist/styles.css +1 -1
- package/dist/text-input.cjs +228 -59
- package/dist/text-input.cjs.map +1 -1
- package/dist/text-input.d.cts +10 -58
- package/dist/text-input.d.ts +10 -58
- package/dist/text-input.js +226 -60
- package/dist/text-input.js.map +1 -1
- package/dist/typography.cjs +120 -0
- package/dist/typography.cjs.map +1 -0
- package/dist/typography.d.cts +46 -0
- package/dist/typography.d.ts +46 -0
- package/dist/typography.js +3 -0
- package/dist/typography.js.map +1 -0
- package/package.json +56 -2
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
|
-
### `
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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` | `
|
|
139
|
+
| `schema` | `FieldSchema` | — | Any zod schema. Only `safeParse` is called. |
|
|
89
140
|
| `validateOn` | `"blur" \| "change" \| "never"` | `"blur"` | When the schema runs. |
|
|
90
|
-
| `onValidationChange` | `(state:
|
|
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
|
|
97
|
-
|
|
98
|
-
|
|
147
|
+
| `trailing` | `ReactNode` | — | Trailing in-field slot. Not on `PasswordInput`, which owns it. |
|
|
148
|
+
|
|
149
|
+
And their own:
|
|
99
150
|
|
|
100
|
-
|
|
101
|
-
|
|
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 {
|
|
161
|
+
import { EmailInput } from "@carbonbits/sixr/email-input";
|
|
105
162
|
import { emailSchema } from "@carbonbits/sixr/validation";
|
|
106
163
|
|
|
107
|
-
<
|
|
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,60 @@ 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".
|
|
181
|
+
|
|
182
|
+
### `Form` / `FormError` / `SubmitButton`
|
|
183
|
+
|
|
184
|
+
```tsx
|
|
185
|
+
import { Form, FormError, SubmitButton } from "@carbonbits/sixr/form";
|
|
186
|
+
```
|
|
187
|
+
|
|
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. |
|
|
199
|
+
|
|
200
|
+
```tsx
|
|
201
|
+
import { EmailInput } from "@carbonbits/sixr/email-input";
|
|
202
|
+
import { Form, FormError, SubmitButton } from "@carbonbits/sixr/form";
|
|
203
|
+
import { PasswordInput } from "@carbonbits/sixr/password-input";
|
|
204
|
+
|
|
205
|
+
function LoginForm() {
|
|
206
|
+
const [email, setEmail] = useState("");
|
|
207
|
+
const [password, setPassword] = useState("");
|
|
208
|
+
const [error, setError] = useState<string>();
|
|
209
|
+
|
|
210
|
+
const handleSubmit = async () => {
|
|
211
|
+
setError(undefined);
|
|
212
|
+
const ok = await signIn(email, password);
|
|
213
|
+
if (!ok) setError("That email and password don't match.");
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<Form onSubmit={handleSubmit}>
|
|
218
|
+
<EmailInput label="Work email" onChange={setEmail} />
|
|
219
|
+
<PasswordInput label="Password" onChange={setPassword} />
|
|
220
|
+
<FormError>{error}</FormError>
|
|
221
|
+
<SubmitButton loadingLabel="Signing in…">Sign in</SubmitButton>
|
|
222
|
+
</Form>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
A field never needs its own `disabled={isLoading}` — `Form` groups its children in a
|
|
228
|
+
fieldset that disables for the duration of `onSubmit`, so `SubmitButton` (and any other
|
|
229
|
+
control inside `Form`) picks that up for free. Fields, OAuth rows, dividers — anything
|
|
230
|
+
the design puts between the top of the form and the submit button — are just children;
|
|
231
|
+
`Form` doesn't assume a shape beyond that.
|
|
123
232
|
|
|
124
233
|
## Validation schemas
|
|
125
234
|
|
|
@@ -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"]}
|