@carbonbits/sixr 1.1.0 → 1.2.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 +102 -90
- package/dist/index.d.cts +63 -1
- package/dist/index.d.ts +63 -1
- package/dist/styles.css +1 -1
- package/dist/text-input.cjs +216 -0
- package/dist/text-input.cjs.map +1 -0
- package/dist/text-input.d.cts +65 -0
- package/dist/text-input.d.ts +65 -0
- package/dist/text-input.js +196 -0
- package/dist/text-input.js.map +1 -0
- package/dist/validation.cjs +43 -0
- package/dist/validation.cjs.map +1 -0
- package/dist/validation.d.cts +28 -0
- package/dist/validation.d.ts +28 -0
- package/dist/validation.js +35 -0
- package/dist/validation.js.map +1 -0
- package/package.json +31 -4
package/README.md
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
E-commerce geared React component library for [carbonbits](https://carbonbits.co.ke/sixr).
|
|
4
4
|
|
|
5
|
-
Ships ESM + CJS builds with type declarations, and a prebuilt stylesheet.
|
|
6
|
-
are React Server Component safe
|
|
7
|
-
|
|
5
|
+
Ships ESM + CJS builds with type declarations, and a prebuilt stylesheet. Static
|
|
6
|
+
components are React Server Component safe and need no `"use client"` boundary in the
|
|
7
|
+
Next.js App Router; interactive ones carry their own directive, so either kind imports
|
|
8
|
+
cleanly into a server component.
|
|
8
9
|
|
|
9
10
|
## Install
|
|
10
11
|
|
|
@@ -12,10 +13,13 @@ are React Server Component safe, so they work in the Next.js App Router without
|
|
|
12
13
|
pnpm add @carbonbits/sixr
|
|
13
14
|
```
|
|
14
15
|
|
|
15
|
-
`react` and `react-dom` are peer dependencies (`^19.0.0`).
|
|
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
18
|
|
|
17
19
|
## Usage
|
|
18
20
|
|
|
21
|
+
Load the stylesheet once, at the root of your app:
|
|
22
|
+
|
|
19
23
|
```tsx
|
|
20
24
|
// app/layout.tsx
|
|
21
25
|
import "@carbonbits/sixr/styles.css";
|
|
@@ -40,113 +44,121 @@ export default function Page() {
|
|
|
40
44
|
The root barrel (`@carbonbits/sixr`) re-exports everything and works too, but the
|
|
41
45
|
subpath is the one to reach for.
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
library itself uses, so it is safe to load alongside a consumer's own Tailwind build.
|
|
45
|
-
|
|
46
|
-
## Contributing
|
|
47
|
+
## Components
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
picks up the right version).
|
|
49
|
+
### `Cart`
|
|
50
50
|
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
pnpm storybook # component workbench on :6006
|
|
54
|
-
pnpm test # vitest
|
|
55
|
-
pnpm run lint # biome: lint + format check
|
|
56
|
-
pnpm run lint:fix # biome: apply safe fixes
|
|
57
|
-
pnpm run typecheck
|
|
58
|
-
pnpm run build # dist/ — JS, .d.ts and styles.css
|
|
59
|
-
pnpm run build-storybook
|
|
51
|
+
```tsx
|
|
52
|
+
import { Cart } from "@carbonbits/sixr/cart";
|
|
60
53
|
```
|
|
61
54
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
`
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
55
|
+
| Prop | Type | Default | Description |
|
|
56
|
+
| ---------- | --------------- | --------- | --------------------------------------- |
|
|
57
|
+
| `products` | `CartProduct[]` | `[]` | Lines to render. Empty shows a placeholder. |
|
|
58
|
+
| `currency` | `string` | `"KES"` | ISO 4217 code used to format prices. |
|
|
59
|
+
| `locale` | `string` | `"en-KE"` | BCP 47 locale used to format prices. |
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
type CartProduct = {
|
|
63
|
+
/** Stable identity for the line, used as the React key. Falls back to `name`. */
|
|
64
|
+
id?: string;
|
|
65
|
+
name: string;
|
|
66
|
+
price: number;
|
|
67
|
+
};
|
|
68
|
+
```
|
|
73
69
|
|
|
74
|
-
|
|
70
|
+
Pass an `id` whenever a cart can hold two lines with the same name. Prices are
|
|
71
|
+
formatted with `Intl.NumberFormat`, and the total is summed from `price`.
|
|
75
72
|
|
|
76
|
-
|
|
77
|
-
2. a matching subpath in the package.json `exports` map
|
|
78
|
-
3. a re-export from `src/index.ts`, for the barrel
|
|
73
|
+
### `TextInput`
|
|
79
74
|
|
|
80
|
-
|
|
75
|
+
```tsx
|
|
76
|
+
import { TextInput } from "@carbonbits/sixr/text-input";
|
|
77
|
+
```
|
|
81
78
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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.
|
|
82
|
+
|
|
83
|
+
| Prop | Type | Default | Description |
|
|
84
|
+
| -------------------- | ------------------------------------ | -------- | ----------------------------------------------------- |
|
|
85
|
+
| `label` | `string` | — | Field label, associated with the input. |
|
|
86
|
+
| `hint` | `ReactNode` | — | Guidance below the field. Replaced by an error. |
|
|
87
|
+
| `error` | `string` | — | Caller-supplied error. Outranks the schema. |
|
|
88
|
+
| `schema` | `TextInputSchema` | — | Any zod schema. Only `safeParse` is called. |
|
|
89
|
+
| `validateOn` | `"blur" \| "change" \| "never"` | `"blur"` | When the schema runs. |
|
|
90
|
+
| `onValidationChange` | `(state: TextInputValidation) => void` | — | Fires on each validation pass. |
|
|
91
|
+
| `onChange` | `(value: string) => void` | — | Receives the value, not the event. |
|
|
92
|
+
| `value` | `string` | — | Makes the field controlled. |
|
|
93
|
+
| `defaultValue` | `string` | `""` | Initial value when uncontrolled. |
|
|
94
|
+
| `optional` | `boolean` | `false` | Shows an "Optional" marker on the label. |
|
|
95
|
+
| `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. |
|
|
99
|
+
|
|
100
|
+
Any other `<input>` attribute — `placeholder`, `maxLength`, `disabled`, `inputMode` —
|
|
101
|
+
is passed straight through.
|
|
85
102
|
|
|
86
|
-
|
|
103
|
+
```tsx
|
|
104
|
+
import { TextInput } from "@carbonbits/sixr/text-input";
|
|
105
|
+
import { emailSchema } from "@carbonbits/sixr/validation";
|
|
106
|
+
|
|
107
|
+
<TextInput
|
|
108
|
+
label="Work email"
|
|
109
|
+
type="email"
|
|
110
|
+
placeholder="you@company.com"
|
|
111
|
+
schema={emailSchema}
|
|
112
|
+
onChange={setEmail}
|
|
113
|
+
/>;
|
|
114
|
+
```
|
|
87
115
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
116
|
+
Validation runs on blur by default, then on every keystroke once the field has gone
|
|
117
|
+
invalid, so a corrected value clears the message immediately. The first schema issue
|
|
118
|
+
is shown, the input gets `aria-invalid`, and the message is announced via `role="alert"`
|
|
119
|
+
and wired up with `aria-describedby`.
|
|
91
120
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
| `fix:` | patch |
|
|
95
|
-
| `feat:` | minor |
|
|
96
|
-
| `feat!:` / `BREAKING CHANGE:` | major |
|
|
97
|
-
| `chore:`, `ci:`, `docs:`, `test:`| none |
|
|
121
|
+
`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.
|
|
98
123
|
|
|
99
|
-
|
|
100
|
-
the bump back to `master`, tags it, and opens a GitHub release.
|
|
124
|
+
## Validation schemas
|
|
101
125
|
|
|
102
|
-
|
|
126
|
+
```tsx
|
|
127
|
+
import { emailSchema, passwordSchema } from "@carbonbits/sixr/validation";
|
|
128
|
+
```
|
|
103
129
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
130
|
+
Ready-made zod schemas matching the Carbonbits auth rules: `emailSchema`,
|
|
131
|
+
`passwordSchema`, `verificationCodeSchema`, `backupCodeSchema`, `fullNameSchema`,
|
|
132
|
+
`jobTitleSchema` and `phoneSchema`. They are ordinary zod schemas — extend them with
|
|
133
|
+
`.refine`, or replace the message with `.min(8, "…")`.
|
|
107
134
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
npm stage view <stage-id> # inspect before approving
|
|
111
|
-
npm stage approve <stage-id> # prompts for 2FA, goes live
|
|
112
|
-
npm stage reject <stage-id> # discard instead
|
|
113
|
-
```
|
|
135
|
+
This subpath needs `zod` (`^4`), which is an optional peer dependency. Nothing else in
|
|
136
|
+
the package requires it.
|
|
114
137
|
|
|
115
|
-
|
|
116
|
-
even though the GitHub release and tag already exist. Staging needs npm >= 11.15.0
|
|
117
|
-
and Node >= 22.14, which CI pins.
|
|
138
|
+
## Styling
|
|
118
139
|
|
|
119
|
-
|
|
140
|
+
`dist/styles.css` is compiled by Tailwind 4 and contains only the utilities the
|
|
141
|
+
library itself uses, so it is safe to load alongside a consumer's own Tailwind build.
|
|
120
142
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
| `GH_APP_ID` | the release GitHub App |
|
|
125
|
-
| `GH_APP_PRIVATE_KEY` | that App's private key, **base64-encoded** |
|
|
126
|
-
| `CHROMATIC_PROJECT_TOKEN` | Chromatic publish |
|
|
143
|
+
Colours and control metrics come from CSS custom properties, all namespaced `--sixr-*`,
|
|
144
|
+
so nothing here can clobber your own Tailwind theme. Override any of them to retheme
|
|
145
|
+
the library without rebuilding it:
|
|
127
146
|
|
|
128
|
-
|
|
129
|
-
|
|
147
|
+
```css
|
|
148
|
+
:root {
|
|
149
|
+
--sixr-accent: #0f766e;
|
|
150
|
+
--sixr-line: #d4d4d8;
|
|
151
|
+
}
|
|
152
|
+
```
|
|
130
153
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
installation token from a GitHub App instead, and **that App must be listed in the
|
|
134
|
-
ruleset's bypass list** or the push still fails. The App needs `contents: write` and
|
|
135
|
-
`pull_requests: write` on this repo, nothing more.
|
|
154
|
+
Dark values follow `prefers-color-scheme` by default. To drive the theme yourself, set
|
|
155
|
+
`data-theme="dark"` or `data-theme="light"` on the root element.
|
|
136
156
|
|
|
137
|
-
|
|
138
|
-
is not interchangeable with `GH_TOKEN`. semantic-release builds its push URL as
|
|
139
|
-
`https://<GH_TOKEN>@github.com/...`, but as
|
|
140
|
-
`https://x-access-token:<GITHUB_TOKEN>@github.com/...` when it detects Actions.
|
|
141
|
-
An App installation token is only accepted in the second form — via `GH_TOKEN` it
|
|
142
|
-
fails on `git ls-remote` with `Invalid username or token`.
|
|
157
|
+
## Versioning
|
|
143
158
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
shadows any working token and fails with `EINVALIDGHTOKEN`. If that secret still
|
|
147
|
-
exists in repo settings, delete it.
|
|
159
|
+
Released under [semantic versioning](https://semver.org/): breaking changes only land
|
|
160
|
+
in major versions, so `^` ranges are safe to pin against.
|
|
148
161
|
|
|
149
|
-
##
|
|
162
|
+
## License
|
|
150
163
|
|
|
151
|
-
|
|
152
|
-
[Chromatic](https://www.chromatic.com/) for visual regression review.
|
|
164
|
+
MIT
|
package/dist/index.d.cts
CHANGED
|
@@ -1,2 +1,64 @@
|
|
|
1
1
|
export { Cart, CartProduct, CartProps } from './cart.cjs';
|
|
2
|
-
import 'react';
|
|
2
|
+
import { InputHTMLAttributes, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Structural shape of a zod schema — every zod schema satisfies it, and any
|
|
6
|
+
* version of zod does. Declaring it this way rather than importing `ZodType`
|
|
7
|
+
* keeps zod out of this module's public types, so consumers who only want an
|
|
8
|
+
* input do not have to install zod to typecheck it.
|
|
9
|
+
*/
|
|
10
|
+
type TextInputSchema = {
|
|
11
|
+
safeParse: (value: unknown) => {
|
|
12
|
+
success: true;
|
|
13
|
+
} | {
|
|
14
|
+
success: false;
|
|
15
|
+
error: {
|
|
16
|
+
issues: readonly {
|
|
17
|
+
message: string;
|
|
18
|
+
}[];
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
/** Outcome of a single validation pass, handed to `onValidationChange`. */
|
|
23
|
+
type TextInputValidation = {
|
|
24
|
+
valid: boolean;
|
|
25
|
+
/** First issue reported by the schema, or `undefined` when valid. */
|
|
26
|
+
error?: string;
|
|
27
|
+
};
|
|
28
|
+
type TextInputProps = Omit<InputHTMLAttributes<HTMLInputElement>, "onChange" | "value" | "defaultValue" | "type"> & {
|
|
29
|
+
label?: string;
|
|
30
|
+
/** Guidance under the field. Replaced by the error message while invalid. */
|
|
31
|
+
hint?: ReactNode;
|
|
32
|
+
/**
|
|
33
|
+
* Error supplied by the caller — a server-side or cross-field failure. Takes
|
|
34
|
+
* precedence over the schema, so a form can hold a field invalid regardless
|
|
35
|
+
* of what `schema` makes of the value on its own.
|
|
36
|
+
*/
|
|
37
|
+
error?: string;
|
|
38
|
+
/** Renders the design's muted "Optional" marker at the end of the label. */
|
|
39
|
+
optional?: boolean;
|
|
40
|
+
/** Trailing label slot, for affordances like the design's "Forgot?" link. */
|
|
41
|
+
action?: ReactNode;
|
|
42
|
+
/** Trailing in-field slot, for affordances like the design's password peek. */
|
|
43
|
+
trailing?: ReactNode;
|
|
44
|
+
/** Renders the value in the design's monospace face. */
|
|
45
|
+
mono?: boolean;
|
|
46
|
+
type?: "text" | "email" | "password" | "tel" | "url" | "search";
|
|
47
|
+
value?: string;
|
|
48
|
+
defaultValue?: string;
|
|
49
|
+
onChange?: (value: string) => void;
|
|
50
|
+
/**
|
|
51
|
+
* Any zod schema that accepts a string. Only `safeParse` is used, so a
|
|
52
|
+
* refined, transformed or preprocessed schema works as well as a plain one.
|
|
53
|
+
*/
|
|
54
|
+
schema?: TextInputSchema;
|
|
55
|
+
/**
|
|
56
|
+
* When the schema runs. `blur` — the default — avoids scolding someone
|
|
57
|
+
* mid-keystroke; once a field has gone invalid it revalidates on every
|
|
58
|
+
* keystroke so the message clears as soon as it is fixed.
|
|
59
|
+
*/
|
|
60
|
+
validateOn?: "blur" | "change" | "never";
|
|
61
|
+
onValidationChange?: (state: TextInputValidation) => void;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type { TextInputProps, TextInputSchema, TextInputValidation };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,64 @@
|
|
|
1
1
|
export { Cart, CartProduct, CartProps } from './cart.js';
|
|
2
|
-
import 'react';
|
|
2
|
+
import { InputHTMLAttributes, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Structural shape of a zod schema — every zod schema satisfies it, and any
|
|
6
|
+
* version of zod does. Declaring it this way rather than importing `ZodType`
|
|
7
|
+
* keeps zod out of this module's public types, so consumers who only want an
|
|
8
|
+
* input do not have to install zod to typecheck it.
|
|
9
|
+
*/
|
|
10
|
+
type TextInputSchema = {
|
|
11
|
+
safeParse: (value: unknown) => {
|
|
12
|
+
success: true;
|
|
13
|
+
} | {
|
|
14
|
+
success: false;
|
|
15
|
+
error: {
|
|
16
|
+
issues: readonly {
|
|
17
|
+
message: string;
|
|
18
|
+
}[];
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
/** Outcome of a single validation pass, handed to `onValidationChange`. */
|
|
23
|
+
type TextInputValidation = {
|
|
24
|
+
valid: boolean;
|
|
25
|
+
/** First issue reported by the schema, or `undefined` when valid. */
|
|
26
|
+
error?: string;
|
|
27
|
+
};
|
|
28
|
+
type TextInputProps = Omit<InputHTMLAttributes<HTMLInputElement>, "onChange" | "value" | "defaultValue" | "type"> & {
|
|
29
|
+
label?: string;
|
|
30
|
+
/** Guidance under the field. Replaced by the error message while invalid. */
|
|
31
|
+
hint?: ReactNode;
|
|
32
|
+
/**
|
|
33
|
+
* Error supplied by the caller — a server-side or cross-field failure. Takes
|
|
34
|
+
* precedence over the schema, so a form can hold a field invalid regardless
|
|
35
|
+
* of what `schema` makes of the value on its own.
|
|
36
|
+
*/
|
|
37
|
+
error?: string;
|
|
38
|
+
/** Renders the design's muted "Optional" marker at the end of the label. */
|
|
39
|
+
optional?: boolean;
|
|
40
|
+
/** Trailing label slot, for affordances like the design's "Forgot?" link. */
|
|
41
|
+
action?: ReactNode;
|
|
42
|
+
/** Trailing in-field slot, for affordances like the design's password peek. */
|
|
43
|
+
trailing?: ReactNode;
|
|
44
|
+
/** Renders the value in the design's monospace face. */
|
|
45
|
+
mono?: boolean;
|
|
46
|
+
type?: "text" | "email" | "password" | "tel" | "url" | "search";
|
|
47
|
+
value?: string;
|
|
48
|
+
defaultValue?: string;
|
|
49
|
+
onChange?: (value: string) => void;
|
|
50
|
+
/**
|
|
51
|
+
* Any zod schema that accepts a string. Only `safeParse` is used, so a
|
|
52
|
+
* refined, transformed or preprocessed schema works as well as a plain one.
|
|
53
|
+
*/
|
|
54
|
+
schema?: TextInputSchema;
|
|
55
|
+
/**
|
|
56
|
+
* When the schema runs. `blur` — the default — avoids scolding someone
|
|
57
|
+
* mid-keystroke; once a field has gone invalid it revalidates on every
|
|
58
|
+
* keystroke so the message clears as soon as it is fixed.
|
|
59
|
+
*/
|
|
60
|
+
validateOn?: "blur" | "change" | "never";
|
|
61
|
+
onValidationChange?: (state: TextInputValidation) => void;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type { TextInputProps, TextInputSchema, TextInputValidation };
|
package/dist/styles.css
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
|
|
2
|
-
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-white:#fff;--spacing:.25rem;--container-md:28rem;--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--font-weight-medium:500;--font-weight-semibold:600;--radius-lg:.5rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.contents{display:contents}.flex{display:flex}.w-full{width:100%}.max-w-md{max-width:var(--container-md)}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.flex-col{flex-direction:column}.items-baseline{align-items:baseline}.justify-between{justify-content:space-between}.gap-4{gap:calc(var(--spacing) * 4)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-gray-100>:not(:last-child)){border-color:var(--color-gray-100)}.rounded-lg{border-radius:var(--radius-lg)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-gray-200{border-color:var(--color-gray-200)}.bg-white{background-color:var(--color-white)}.p-4{padding:calc(var(--spacing) * 4)}.py-2{padding-block:calc(var(--spacing) * 2)}.pt-2{padding-top:calc(var(--spacing) * 2)}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}
|
|
2
|
+
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-white:#fff;--spacing:.25rem;--container-md:28rem;--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--font-weight-medium:500;--font-weight-semibold:600;--radius-lg:.5rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.visible{visibility:visible}.absolute{position:absolute}.relative{position:relative}.top-1\/2{top:50%}.right-\[9px\]{right:9px}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.ml-auto{margin-left:auto}.contents{display:contents}.flex{display:flex}.inline{display:inline}.table{display:table}.h-10{height:calc(var(--spacing) * 10)}.w-full{width:100%}.max-w-md{max-width:var(--container-md)}.-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.flex-col{flex-direction:column}.items-baseline{align-items:baseline}.items-center{align-items:center}.justify-between{justify-content:space-between}.gap-4{gap:calc(var(--spacing) * 4)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-gray-100>:not(:last-child)){border-color:var(--color-gray-100)}.rounded-field{border-radius:4px}.rounded-lg{border-radius:var(--radius-lg)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-danger{border-color:var(--sixr-danger)}.border-gray-200{border-color:var(--color-gray-200)}.border-line{border-color:var(--sixr-line)}.bg-surface{background-color:var(--sixr-surface)}.bg-white{background-color:var(--color-white)}.p-4{padding:calc(var(--spacing) * 4)}.px-3{padding-inline:calc(var(--spacing) * 3)}.py-2{padding-block:calc(var(--spacing) * 2)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pr-16{padding-right:calc(var(--spacing) * 16)}.font-\[inherit\]{font-family:inherit}.font-code{font-family:var(--sixr-font-code)}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-field{font-size:13px}.text-hint{font-size:11px}.text-label{font-size:11.5px}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.text-danger{color:var(--sixr-danger)}.text-fg{color:var(--sixr-fg)}.text-fg-muted{color:var(--sixr-fg-muted)}.text-fg-subtle{color:var(--sixr-fg-subtle)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.outline-none{--tw-outline-style:none;outline-style:none}.placeholder\:text-fg-subtle::placeholder{color:var(--sixr-fg-subtle)}.focus\:border-accent:focus{border-color:var(--sixr-accent)}.focus\:border-danger:focus{border-color:var(--sixr-danger)}.focus\:ring-3:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-accent-soft:focus{--tw-ring-color:var(--sixr-accent-soft)}.focus\:ring-danger-soft:focus{--tw-ring-color:var(--sixr-danger-soft)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-45:disabled{opacity:.45}}:root{--sixr-bg:#f4f4f5;--sixr-surface:#fff;--sixr-surface-2:#f4f4f6;--sixr-surface-3:#eaeaec;--sixr-line:#e2e2e5;--sixr-line-strong:#cdcdd2;--sixr-fg:#0b0b0d;--sixr-fg-muted:#55555e;--sixr-fg-subtle:#8e8e99;--sixr-accent:#0b0b0d;--sixr-accent-2:#000;--sixr-accent-soft:#ededef;--sixr-on-accent:#fff;--sixr-success:#16a34a;--sixr-success-soft:#e9f5ec;--sixr-warning:#b45309;--sixr-info:#2563eb;--sixr-danger:#dc2626;--sixr-danger-soft:#fdecec;--sixr-font-code:"JetBrains Mono", ui-monospace, monospace}@media (prefers-color-scheme:dark){:root:not([data-theme=light]){--sixr-bg:#000;--sixr-surface:#0b0b0d;--sixr-surface-2:#121215;--sixr-surface-3:#1b1b1f;--sixr-line:#232328;--sixr-line-strong:#3a3a42;--sixr-fg:#fff;--sixr-fg-muted:#a3a3ad;--sixr-fg-subtle:#70707b;--sixr-accent:#fff;--sixr-accent-2:#e4e4e8;--sixr-accent-soft:#17171b;--sixr-on-accent:#000;--sixr-success:#34d399;--sixr-success-soft:#12301e;--sixr-warning:#fbbf24;--sixr-info:#60a5fa;--sixr-danger:#f87171;--sixr-danger-soft:#2a1416}}:root[data-theme=dark]{--sixr-bg:#000;--sixr-surface:#0b0b0d;--sixr-surface-2:#121215;--sixr-surface-3:#1b1b1f;--sixr-line:#232328;--sixr-line-strong:#3a3a42;--sixr-fg:#fff;--sixr-fg-muted:#a3a3ad;--sixr-fg-subtle:#70707b;--sixr-accent:#fff;--sixr-accent-2:#e4e4e8;--sixr-accent-soft:#17171b;--sixr-on-accent:#000;--sixr-success:#34d399;--sixr-success-soft:#12301e;--sixr-warning:#fbbf24;--sixr-info:#60a5fa;--sixr-danger:#f87171;--sixr-danger-soft:#2a1416}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}
|
|
@@ -0,0 +1,216 @@
|
|
|
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/text-input/index.tsx
|
|
23
|
+
var text_input_exports = {};
|
|
24
|
+
__export(text_input_exports, {
|
|
25
|
+
TextInput: () => TextInput
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(text_input_exports);
|
|
28
|
+
var import_react = require("react");
|
|
29
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
30
|
+
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";
|
|
31
|
+
var VALID_CLASSES = "border-line focus:border-accent focus:ring-3 focus:ring-accent-soft";
|
|
32
|
+
var INVALID_CLASSES = "border-danger focus:border-danger focus:ring-3 focus:ring-danger-soft";
|
|
33
|
+
var FALLBACK_MESSAGE = "Invalid value";
|
|
34
|
+
function runSchema(schema, value) {
|
|
35
|
+
if (!schema) {
|
|
36
|
+
return { valid: true };
|
|
37
|
+
}
|
|
38
|
+
const result = schema.safeParse(value);
|
|
39
|
+
if (result.success) {
|
|
40
|
+
return { valid: true };
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
valid: false,
|
|
44
|
+
error: result.error.issues[0]?.message ?? FALLBACK_MESSAGE
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function fieldClassName(invalid, mono, hasTrailing, className) {
|
|
48
|
+
return [
|
|
49
|
+
FIELD_CLASSES,
|
|
50
|
+
invalid ? INVALID_CLASSES : VALID_CLASSES,
|
|
51
|
+
mono ? "font-code" : "",
|
|
52
|
+
hasTrailing ? "pr-16" : "",
|
|
53
|
+
className ?? ""
|
|
54
|
+
].filter(Boolean).join(" ");
|
|
55
|
+
}
|
|
56
|
+
function FieldLabel({
|
|
57
|
+
htmlFor,
|
|
58
|
+
label,
|
|
59
|
+
optional,
|
|
60
|
+
action
|
|
61
|
+
}) {
|
|
62
|
+
if (!label) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
66
|
+
"label",
|
|
67
|
+
{
|
|
68
|
+
htmlFor,
|
|
69
|
+
className: "mb-1.5 flex items-center text-label font-semibold text-fg-muted",
|
|
70
|
+
children: [
|
|
71
|
+
label,
|
|
72
|
+
optional ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ml-auto text-hint font-semibold text-fg-subtle", children: "Optional" }) : null,
|
|
73
|
+
action ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ml-auto", children: action }) : null
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
function FieldMessage({
|
|
79
|
+
id,
|
|
80
|
+
error,
|
|
81
|
+
hint
|
|
82
|
+
}) {
|
|
83
|
+
if (error) {
|
|
84
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { id, role: "alert", className: "mt-1.5 text-hint text-danger", children: error });
|
|
85
|
+
}
|
|
86
|
+
if (hint) {
|
|
87
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { id, className: "mt-1.5 text-hint text-fg-subtle", children: hint });
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
function useFieldBehaviour({
|
|
92
|
+
value,
|
|
93
|
+
defaultValue,
|
|
94
|
+
error,
|
|
95
|
+
onChange,
|
|
96
|
+
onBlur,
|
|
97
|
+
schema,
|
|
98
|
+
validateOn = "blur",
|
|
99
|
+
onValidationChange
|
|
100
|
+
}) {
|
|
101
|
+
const [uncontrolledValue, setUncontrolledValue] = (0, import_react.useState)(
|
|
102
|
+
defaultValue ?? ""
|
|
103
|
+
);
|
|
104
|
+
const [schemaError, setSchemaError] = (0, import_react.useState)(void 0);
|
|
105
|
+
const isControlled = value !== void 0;
|
|
106
|
+
const validate = (0, import_react.useCallback)(
|
|
107
|
+
(next) => {
|
|
108
|
+
const state = runSchema(schema, next);
|
|
109
|
+
setSchemaError(state.error);
|
|
110
|
+
onValidationChange?.(state);
|
|
111
|
+
},
|
|
112
|
+
[schema, onValidationChange]
|
|
113
|
+
);
|
|
114
|
+
const handleChange = (0, import_react.useCallback)(
|
|
115
|
+
(event) => {
|
|
116
|
+
const next = event.target.value;
|
|
117
|
+
if (!isControlled) {
|
|
118
|
+
setUncontrolledValue(next);
|
|
119
|
+
}
|
|
120
|
+
onChange?.(next);
|
|
121
|
+
if (validateOn === "change" || validateOn === "blur" && schemaError) {
|
|
122
|
+
validate(next);
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
[isControlled, onChange, validateOn, schemaError, validate]
|
|
126
|
+
);
|
|
127
|
+
const handleBlur = (0, import_react.useCallback)(
|
|
128
|
+
(event) => {
|
|
129
|
+
if (validateOn === "blur") {
|
|
130
|
+
validate(event.target.value);
|
|
131
|
+
}
|
|
132
|
+
onBlur?.(event);
|
|
133
|
+
},
|
|
134
|
+
[validateOn, validate, onBlur]
|
|
135
|
+
);
|
|
136
|
+
return {
|
|
137
|
+
currentValue: isControlled ? value : uncontrolledValue,
|
|
138
|
+
// A caller-supplied error outranks the schema's verdict.
|
|
139
|
+
shownError: error ?? schemaError,
|
|
140
|
+
handleChange,
|
|
141
|
+
handleBlur
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function TextInput({
|
|
145
|
+
label,
|
|
146
|
+
hint,
|
|
147
|
+
error,
|
|
148
|
+
optional = false,
|
|
149
|
+
action,
|
|
150
|
+
trailing,
|
|
151
|
+
mono = false,
|
|
152
|
+
type = "text",
|
|
153
|
+
value,
|
|
154
|
+
defaultValue,
|
|
155
|
+
onChange,
|
|
156
|
+
schema,
|
|
157
|
+
validateOn = "blur",
|
|
158
|
+
onValidationChange,
|
|
159
|
+
id,
|
|
160
|
+
className,
|
|
161
|
+
onBlur,
|
|
162
|
+
...rest
|
|
163
|
+
}) {
|
|
164
|
+
const generatedId = (0, import_react.useId)();
|
|
165
|
+
const inputId = id ?? generatedId;
|
|
166
|
+
const describedById = `${inputId}-description`;
|
|
167
|
+
const { currentValue, shownError, handleChange, handleBlur } = useFieldBehaviour({
|
|
168
|
+
value,
|
|
169
|
+
defaultValue,
|
|
170
|
+
error,
|
|
171
|
+
onChange,
|
|
172
|
+
onBlur,
|
|
173
|
+
schema,
|
|
174
|
+
validateOn,
|
|
175
|
+
onValidationChange
|
|
176
|
+
});
|
|
177
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-col", children: [
|
|
178
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
179
|
+
FieldLabel,
|
|
180
|
+
{
|
|
181
|
+
htmlFor: inputId,
|
|
182
|
+
label,
|
|
183
|
+
optional,
|
|
184
|
+
action
|
|
185
|
+
}
|
|
186
|
+
),
|
|
187
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: trailing ? "relative" : void 0, children: [
|
|
188
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
189
|
+
"input",
|
|
190
|
+
{
|
|
191
|
+
...rest,
|
|
192
|
+
id: inputId,
|
|
193
|
+
type,
|
|
194
|
+
value: currentValue,
|
|
195
|
+
"aria-invalid": shownError ? true : void 0,
|
|
196
|
+
"aria-describedby": shownError || hint ? describedById : void 0,
|
|
197
|
+
onChange: handleChange,
|
|
198
|
+
onBlur: handleBlur,
|
|
199
|
+
className: fieldClassName(
|
|
200
|
+
Boolean(shownError),
|
|
201
|
+
mono,
|
|
202
|
+
Boolean(trailing),
|
|
203
|
+
className
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
),
|
|
207
|
+
trailing ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "absolute top-1/2 right-[9px] -translate-y-1/2 text-hint font-semibold text-fg-subtle", children: trailing }) : null
|
|
208
|
+
] }),
|
|
209
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(FieldMessage, { id: describedById, error: shownError, hint })
|
|
210
|
+
] });
|
|
211
|
+
}
|
|
212
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
213
|
+
0 && (module.exports = {
|
|
214
|
+
TextInput
|
|
215
|
+
});
|
|
216
|
+
//# sourceMappingURL=text-input.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/text-input/index.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n type ChangeEvent,\n type FocusEvent,\n type InputHTMLAttributes,\n type ReactNode,\n useCallback,\n useId,\n useState,\n} from \"react\";\n\n/**\n * Structural shape of a zod schema — every zod schema satisfies it, and any\n * version of zod does. Declaring it this way rather than importing `ZodType`\n * keeps zod out of this module's public types, so consumers who only want an\n * input do not have to install zod to typecheck it.\n */\nexport type TextInputSchema = {\n safeParse: (\n value: unknown,\n ) =>\n | { success: true }\n | { success: false; error: { issues: readonly { message: string }[] } };\n};\n\n/** Outcome of a single validation pass, handed to `onValidationChange`. */\nexport type TextInputValidation = {\n valid: boolean;\n /** First issue reported by the schema, or `undefined` when valid. */\n error?: string;\n};\n\nexport type TextInputProps = Omit<\n InputHTMLAttributes<HTMLInputElement>,\n \"onChange\" | \"value\" | \"defaultValue\" | \"type\"\n> & {\n label?: string;\n /** Guidance under the field. Replaced by the error message while invalid. */\n hint?: ReactNode;\n /**\n * Error supplied by the caller — a server-side or cross-field failure. Takes\n * precedence over the schema, so a form can hold a field invalid regardless\n * of what `schema` makes of the value on its own.\n */\n error?: string;\n /** Renders the design's muted \"Optional\" marker at the end of the label. */\n optional?: boolean;\n /** Trailing label slot, for affordances like the design's \"Forgot?\" link. */\n action?: ReactNode;\n /** Trailing in-field slot, for affordances like the design's password peek. */\n trailing?: ReactNode;\n /** Renders the value in the design's monospace face. */\n mono?: boolean;\n type?: \"text\" | \"email\" | \"password\" | \"tel\" | \"url\" | \"search\";\n value?: string;\n defaultValue?: string;\n onChange?: (value: string) => void;\n /**\n * Any zod schema that accepts a string. Only `safeParse` is used, so a\n * refined, transformed or preprocessed schema works as well as a plain one.\n */\n schema?: TextInputSchema;\n /**\n * When the schema runs. `blur` — the default — avoids scolding someone\n * mid-keystroke; once a field has gone invalid it revalidates on every\n * keystroke so the message clears as soon as it is fixed.\n */\n validateOn?: \"blur\" | \"change\" | \"never\";\n onValidationChange?: (state: TextInputValidation) => void;\n};\n\nconst FIELD_CLASSES =\n \"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\";\nconst VALID_CLASSES =\n \"border-line focus:border-accent focus:ring-3 focus:ring-accent-soft\";\nconst INVALID_CLASSES =\n \"border-danger focus:border-danger focus:ring-3 focus:ring-danger-soft\";\nconst FALLBACK_MESSAGE = \"Invalid value\";\n\nfunction runSchema(\n schema: TextInputSchema | undefined,\n value: string,\n): TextInputValidation {\n if (!schema) {\n return { valid: true };\n }\n\n const result = schema.safeParse(value);\n if (result.success) {\n return { valid: true };\n }\n\n return {\n valid: false,\n error: result.error.issues[0]?.message ?? FALLBACK_MESSAGE,\n };\n}\n\nfunction fieldClassName(\n invalid: boolean,\n mono: boolean,\n hasTrailing: boolean,\n className: string | undefined,\n): string {\n return [\n FIELD_CLASSES,\n invalid ? INVALID_CLASSES : VALID_CLASSES,\n mono ? \"font-code\" : \"\",\n hasTrailing ? \"pr-16\" : \"\",\n className ?? \"\",\n ]\n .filter(Boolean)\n .join(\" \");\n}\n\nfunction FieldLabel({\n htmlFor,\n label,\n optional,\n action,\n}: {\n htmlFor: string;\n label: string | undefined;\n optional: boolean;\n action: ReactNode;\n}) {\n if (!label) {\n return null;\n }\n\n return (\n <label\n htmlFor={htmlFor}\n className=\"mb-1.5 flex items-center text-label font-semibold text-fg-muted\"\n >\n {label}\n {optional ? (\n <span className=\"ml-auto text-hint font-semibold text-fg-subtle\">\n Optional\n </span>\n ) : null}\n {action ? <span className=\"ml-auto\">{action}</span> : null}\n </label>\n );\n}\n\nfunction FieldMessage({\n id,\n error,\n hint,\n}: {\n id: string;\n error: string | undefined;\n hint: ReactNode;\n}) {\n if (error) {\n return (\n <p id={id} role=\"alert\" className=\"mt-1.5 text-hint text-danger\">\n {error}\n </p>\n );\n }\n\n if (hint) {\n return (\n <p id={id} className=\"mt-1.5 text-hint text-fg-subtle\">\n {hint}\n </p>\n );\n }\n\n return null;\n}\n\ntype FieldBehaviour = Pick<\n TextInputProps,\n | \"value\"\n | \"defaultValue\"\n | \"error\"\n | \"onChange\"\n | \"onBlur\"\n | \"schema\"\n | \"validateOn\"\n | \"onValidationChange\"\n>;\n\n/**\n * Owns the value (controlled or not), the schema verdict, and when to re-run\n * it. Split out so the component itself stays markup.\n */\nfunction useFieldBehaviour({\n value,\n defaultValue,\n error,\n onChange,\n onBlur,\n schema,\n validateOn = \"blur\",\n onValidationChange,\n}: FieldBehaviour) {\n const [uncontrolledValue, setUncontrolledValue] = useState(\n defaultValue ?? \"\",\n );\n const [schemaError, setSchemaError] = useState<string | undefined>(undefined);\n\n const isControlled = value !== undefined;\n\n const validate = useCallback(\n (next: string) => {\n const state = runSchema(schema, next);\n setSchemaError(state.error);\n onValidationChange?.(state);\n },\n [schema, onValidationChange],\n );\n\n const handleChange = useCallback(\n (event: ChangeEvent<HTMLInputElement>) => {\n const next = event.target.value;\n if (!isControlled) {\n setUncontrolledValue(next);\n }\n onChange?.(next);\n\n // Revalidate on every keystroke once the field is already invalid, so a\n // corrected value clears the message without waiting for another blur.\n if (validateOn === \"change\" || (validateOn === \"blur\" && schemaError)) {\n validate(next);\n }\n },\n [isControlled, onChange, validateOn, schemaError, validate],\n );\n\n const handleBlur = useCallback(\n (event: FocusEvent<HTMLInputElement>) => {\n if (validateOn === \"blur\") {\n validate(event.target.value);\n }\n onBlur?.(event);\n },\n [validateOn, validate, onBlur],\n );\n\n return {\n currentValue: isControlled ? value : uncontrolledValue,\n // A caller-supplied error outranks the schema's verdict.\n shownError: error ?? schemaError,\n handleChange,\n handleBlur,\n };\n}\n\nexport function TextInput({\n label,\n hint,\n error,\n optional = false,\n action,\n trailing,\n mono = false,\n type = \"text\",\n value,\n defaultValue,\n onChange,\n schema,\n validateOn = \"blur\",\n onValidationChange,\n id,\n className,\n onBlur,\n ...rest\n}: TextInputProps) {\n const generatedId = useId();\n const inputId = id ?? generatedId;\n const describedById = `${inputId}-description`;\n\n const { currentValue, shownError, handleChange, handleBlur } =\n useFieldBehaviour({\n value,\n defaultValue,\n error,\n onChange,\n onBlur,\n schema,\n validateOn,\n onValidationChange,\n });\n\n return (\n <div className=\"flex flex-col\">\n <FieldLabel\n htmlFor={inputId}\n label={label}\n optional={optional}\n action={action}\n />\n\n <div className={trailing ? \"relative\" : undefined}>\n <input\n {...rest}\n id={inputId}\n type={type}\n value={currentValue}\n aria-invalid={shownError ? true : undefined}\n aria-describedby={shownError || hint ? describedById : undefined}\n onChange={handleChange}\n onBlur={handleBlur}\n className={fieldClassName(\n Boolean(shownError),\n mono,\n Boolean(trailing),\n className,\n )}\n />\n {trailing ? (\n <span className=\"absolute top-1/2 right-[9px] -translate-y-1/2 text-hint font-semibold text-fg-subtle\">\n {trailing}\n </span>\n ) : null}\n </div>\n\n <FieldMessage id={describedById} error={shownError} hint={hint} />\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { InputHTMLAttributes, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Structural shape of a zod schema — every zod schema satisfies it, and any
|
|
6
|
+
* version of zod does. Declaring it this way rather than importing `ZodType`
|
|
7
|
+
* keeps zod out of this module's public types, so consumers who only want an
|
|
8
|
+
* input do not have to install zod to typecheck it.
|
|
9
|
+
*/
|
|
10
|
+
type TextInputSchema = {
|
|
11
|
+
safeParse: (value: unknown) => {
|
|
12
|
+
success: true;
|
|
13
|
+
} | {
|
|
14
|
+
success: false;
|
|
15
|
+
error: {
|
|
16
|
+
issues: readonly {
|
|
17
|
+
message: string;
|
|
18
|
+
}[];
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
/** Outcome of a single validation pass, handed to `onValidationChange`. */
|
|
23
|
+
type TextInputValidation = {
|
|
24
|
+
valid: boolean;
|
|
25
|
+
/** First issue reported by the schema, or `undefined` when valid. */
|
|
26
|
+
error?: string;
|
|
27
|
+
};
|
|
28
|
+
type TextInputProps = Omit<InputHTMLAttributes<HTMLInputElement>, "onChange" | "value" | "defaultValue" | "type"> & {
|
|
29
|
+
label?: string;
|
|
30
|
+
/** Guidance under the field. Replaced by the error message while invalid. */
|
|
31
|
+
hint?: ReactNode;
|
|
32
|
+
/**
|
|
33
|
+
* Error supplied by the caller — a server-side or cross-field failure. Takes
|
|
34
|
+
* precedence over the schema, so a form can hold a field invalid regardless
|
|
35
|
+
* of what `schema` makes of the value on its own.
|
|
36
|
+
*/
|
|
37
|
+
error?: string;
|
|
38
|
+
/** Renders the design's muted "Optional" marker at the end of the label. */
|
|
39
|
+
optional?: boolean;
|
|
40
|
+
/** Trailing label slot, for affordances like the design's "Forgot?" link. */
|
|
41
|
+
action?: ReactNode;
|
|
42
|
+
/** Trailing in-field slot, for affordances like the design's password peek. */
|
|
43
|
+
trailing?: ReactNode;
|
|
44
|
+
/** Renders the value in the design's monospace face. */
|
|
45
|
+
mono?: boolean;
|
|
46
|
+
type?: "text" | "email" | "password" | "tel" | "url" | "search";
|
|
47
|
+
value?: string;
|
|
48
|
+
defaultValue?: string;
|
|
49
|
+
onChange?: (value: string) => void;
|
|
50
|
+
/**
|
|
51
|
+
* Any zod schema that accepts a string. Only `safeParse` is used, so a
|
|
52
|
+
* refined, transformed or preprocessed schema works as well as a plain one.
|
|
53
|
+
*/
|
|
54
|
+
schema?: TextInputSchema;
|
|
55
|
+
/**
|
|
56
|
+
* When the schema runs. `blur` — the default — avoids scolding someone
|
|
57
|
+
* mid-keystroke; once a field has gone invalid it revalidates on every
|
|
58
|
+
* keystroke so the message clears as soon as it is fixed.
|
|
59
|
+
*/
|
|
60
|
+
validateOn?: "blur" | "change" | "never";
|
|
61
|
+
onValidationChange?: (state: TextInputValidation) => void;
|
|
62
|
+
};
|
|
63
|
+
declare function TextInput({ label, hint, error, optional, action, trailing, mono, type, value, defaultValue, onChange, schema, validateOn, onValidationChange, id, className, onBlur, ...rest }: TextInputProps): react.JSX.Element;
|
|
64
|
+
|
|
65
|
+
export { TextInput, type TextInputProps, type TextInputSchema, type TextInputValidation };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { InputHTMLAttributes, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Structural shape of a zod schema — every zod schema satisfies it, and any
|
|
6
|
+
* version of zod does. Declaring it this way rather than importing `ZodType`
|
|
7
|
+
* keeps zod out of this module's public types, so consumers who only want an
|
|
8
|
+
* input do not have to install zod to typecheck it.
|
|
9
|
+
*/
|
|
10
|
+
type TextInputSchema = {
|
|
11
|
+
safeParse: (value: unknown) => {
|
|
12
|
+
success: true;
|
|
13
|
+
} | {
|
|
14
|
+
success: false;
|
|
15
|
+
error: {
|
|
16
|
+
issues: readonly {
|
|
17
|
+
message: string;
|
|
18
|
+
}[];
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
/** Outcome of a single validation pass, handed to `onValidationChange`. */
|
|
23
|
+
type TextInputValidation = {
|
|
24
|
+
valid: boolean;
|
|
25
|
+
/** First issue reported by the schema, or `undefined` when valid. */
|
|
26
|
+
error?: string;
|
|
27
|
+
};
|
|
28
|
+
type TextInputProps = Omit<InputHTMLAttributes<HTMLInputElement>, "onChange" | "value" | "defaultValue" | "type"> & {
|
|
29
|
+
label?: string;
|
|
30
|
+
/** Guidance under the field. Replaced by the error message while invalid. */
|
|
31
|
+
hint?: ReactNode;
|
|
32
|
+
/**
|
|
33
|
+
* Error supplied by the caller — a server-side or cross-field failure. Takes
|
|
34
|
+
* precedence over the schema, so a form can hold a field invalid regardless
|
|
35
|
+
* of what `schema` makes of the value on its own.
|
|
36
|
+
*/
|
|
37
|
+
error?: string;
|
|
38
|
+
/** Renders the design's muted "Optional" marker at the end of the label. */
|
|
39
|
+
optional?: boolean;
|
|
40
|
+
/** Trailing label slot, for affordances like the design's "Forgot?" link. */
|
|
41
|
+
action?: ReactNode;
|
|
42
|
+
/** Trailing in-field slot, for affordances like the design's password peek. */
|
|
43
|
+
trailing?: ReactNode;
|
|
44
|
+
/** Renders the value in the design's monospace face. */
|
|
45
|
+
mono?: boolean;
|
|
46
|
+
type?: "text" | "email" | "password" | "tel" | "url" | "search";
|
|
47
|
+
value?: string;
|
|
48
|
+
defaultValue?: string;
|
|
49
|
+
onChange?: (value: string) => void;
|
|
50
|
+
/**
|
|
51
|
+
* Any zod schema that accepts a string. Only `safeParse` is used, so a
|
|
52
|
+
* refined, transformed or preprocessed schema works as well as a plain one.
|
|
53
|
+
*/
|
|
54
|
+
schema?: TextInputSchema;
|
|
55
|
+
/**
|
|
56
|
+
* When the schema runs. `blur` — the default — avoids scolding someone
|
|
57
|
+
* mid-keystroke; once a field has gone invalid it revalidates on every
|
|
58
|
+
* keystroke so the message clears as soon as it is fixed.
|
|
59
|
+
*/
|
|
60
|
+
validateOn?: "blur" | "change" | "never";
|
|
61
|
+
onValidationChange?: (state: TextInputValidation) => void;
|
|
62
|
+
};
|
|
63
|
+
declare function TextInput({ label, hint, error, optional, action, trailing, mono, type, value, defaultValue, onChange, schema, validateOn, onValidationChange, id, className, onBlur, ...rest }: TextInputProps): react.JSX.Element;
|
|
64
|
+
|
|
65
|
+
export { TextInput, type TextInputProps, type TextInputSchema, type TextInputValidation };
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
// src/components/text-input/index.tsx
|
|
5
|
+
import {
|
|
6
|
+
useCallback,
|
|
7
|
+
useId,
|
|
8
|
+
useState
|
|
9
|
+
} from "react";
|
|
10
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
+
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";
|
|
12
|
+
var VALID_CLASSES = "border-line focus:border-accent focus:ring-3 focus:ring-accent-soft";
|
|
13
|
+
var INVALID_CLASSES = "border-danger focus:border-danger focus:ring-3 focus:ring-danger-soft";
|
|
14
|
+
var FALLBACK_MESSAGE = "Invalid value";
|
|
15
|
+
function runSchema(schema, value) {
|
|
16
|
+
if (!schema) {
|
|
17
|
+
return { valid: true };
|
|
18
|
+
}
|
|
19
|
+
const result = schema.safeParse(value);
|
|
20
|
+
if (result.success) {
|
|
21
|
+
return { valid: true };
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
valid: false,
|
|
25
|
+
error: result.error.issues[0]?.message ?? FALLBACK_MESSAGE
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function fieldClassName(invalid, mono, hasTrailing, className) {
|
|
29
|
+
return [
|
|
30
|
+
FIELD_CLASSES,
|
|
31
|
+
invalid ? INVALID_CLASSES : VALID_CLASSES,
|
|
32
|
+
mono ? "font-code" : "",
|
|
33
|
+
hasTrailing ? "pr-16" : "",
|
|
34
|
+
className ?? ""
|
|
35
|
+
].filter(Boolean).join(" ");
|
|
36
|
+
}
|
|
37
|
+
function FieldLabel({
|
|
38
|
+
htmlFor,
|
|
39
|
+
label,
|
|
40
|
+
optional,
|
|
41
|
+
action
|
|
42
|
+
}) {
|
|
43
|
+
if (!label) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return /* @__PURE__ */ jsxs(
|
|
47
|
+
"label",
|
|
48
|
+
{
|
|
49
|
+
htmlFor,
|
|
50
|
+
className: "mb-1.5 flex items-center text-label font-semibold text-fg-muted",
|
|
51
|
+
children: [
|
|
52
|
+
label,
|
|
53
|
+
optional ? /* @__PURE__ */ jsx("span", { className: "ml-auto text-hint font-semibold text-fg-subtle", children: "Optional" }) : null,
|
|
54
|
+
action ? /* @__PURE__ */ jsx("span", { className: "ml-auto", children: action }) : null
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
function FieldMessage({
|
|
60
|
+
id,
|
|
61
|
+
error,
|
|
62
|
+
hint
|
|
63
|
+
}) {
|
|
64
|
+
if (error) {
|
|
65
|
+
return /* @__PURE__ */ jsx("p", { id, role: "alert", className: "mt-1.5 text-hint text-danger", children: error });
|
|
66
|
+
}
|
|
67
|
+
if (hint) {
|
|
68
|
+
return /* @__PURE__ */ jsx("p", { id, className: "mt-1.5 text-hint text-fg-subtle", children: hint });
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
function useFieldBehaviour({
|
|
73
|
+
value,
|
|
74
|
+
defaultValue,
|
|
75
|
+
error,
|
|
76
|
+
onChange,
|
|
77
|
+
onBlur,
|
|
78
|
+
schema,
|
|
79
|
+
validateOn = "blur",
|
|
80
|
+
onValidationChange
|
|
81
|
+
}) {
|
|
82
|
+
const [uncontrolledValue, setUncontrolledValue] = useState(
|
|
83
|
+
defaultValue ?? ""
|
|
84
|
+
);
|
|
85
|
+
const [schemaError, setSchemaError] = useState(void 0);
|
|
86
|
+
const isControlled = value !== void 0;
|
|
87
|
+
const validate = useCallback(
|
|
88
|
+
(next) => {
|
|
89
|
+
const state = runSchema(schema, next);
|
|
90
|
+
setSchemaError(state.error);
|
|
91
|
+
onValidationChange?.(state);
|
|
92
|
+
},
|
|
93
|
+
[schema, onValidationChange]
|
|
94
|
+
);
|
|
95
|
+
const handleChange = useCallback(
|
|
96
|
+
(event) => {
|
|
97
|
+
const next = event.target.value;
|
|
98
|
+
if (!isControlled) {
|
|
99
|
+
setUncontrolledValue(next);
|
|
100
|
+
}
|
|
101
|
+
onChange?.(next);
|
|
102
|
+
if (validateOn === "change" || validateOn === "blur" && schemaError) {
|
|
103
|
+
validate(next);
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
[isControlled, onChange, validateOn, schemaError, validate]
|
|
107
|
+
);
|
|
108
|
+
const handleBlur = useCallback(
|
|
109
|
+
(event) => {
|
|
110
|
+
if (validateOn === "blur") {
|
|
111
|
+
validate(event.target.value);
|
|
112
|
+
}
|
|
113
|
+
onBlur?.(event);
|
|
114
|
+
},
|
|
115
|
+
[validateOn, validate, onBlur]
|
|
116
|
+
);
|
|
117
|
+
return {
|
|
118
|
+
currentValue: isControlled ? value : uncontrolledValue,
|
|
119
|
+
// A caller-supplied error outranks the schema's verdict.
|
|
120
|
+
shownError: error ?? schemaError,
|
|
121
|
+
handleChange,
|
|
122
|
+
handleBlur
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function TextInput({
|
|
126
|
+
label,
|
|
127
|
+
hint,
|
|
128
|
+
error,
|
|
129
|
+
optional = false,
|
|
130
|
+
action,
|
|
131
|
+
trailing,
|
|
132
|
+
mono = false,
|
|
133
|
+
type = "text",
|
|
134
|
+
value,
|
|
135
|
+
defaultValue,
|
|
136
|
+
onChange,
|
|
137
|
+
schema,
|
|
138
|
+
validateOn = "blur",
|
|
139
|
+
onValidationChange,
|
|
140
|
+
id,
|
|
141
|
+
className,
|
|
142
|
+
onBlur,
|
|
143
|
+
...rest
|
|
144
|
+
}) {
|
|
145
|
+
const generatedId = useId();
|
|
146
|
+
const inputId = id ?? generatedId;
|
|
147
|
+
const describedById = `${inputId}-description`;
|
|
148
|
+
const { currentValue, shownError, handleChange, handleBlur } = useFieldBehaviour({
|
|
149
|
+
value,
|
|
150
|
+
defaultValue,
|
|
151
|
+
error,
|
|
152
|
+
onChange,
|
|
153
|
+
onBlur,
|
|
154
|
+
schema,
|
|
155
|
+
validateOn,
|
|
156
|
+
onValidationChange
|
|
157
|
+
});
|
|
158
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
|
|
159
|
+
/* @__PURE__ */ jsx(
|
|
160
|
+
FieldLabel,
|
|
161
|
+
{
|
|
162
|
+
htmlFor: inputId,
|
|
163
|
+
label,
|
|
164
|
+
optional,
|
|
165
|
+
action
|
|
166
|
+
}
|
|
167
|
+
),
|
|
168
|
+
/* @__PURE__ */ jsxs("div", { className: trailing ? "relative" : void 0, children: [
|
|
169
|
+
/* @__PURE__ */ jsx(
|
|
170
|
+
"input",
|
|
171
|
+
{
|
|
172
|
+
...rest,
|
|
173
|
+
id: inputId,
|
|
174
|
+
type,
|
|
175
|
+
value: currentValue,
|
|
176
|
+
"aria-invalid": shownError ? true : void 0,
|
|
177
|
+
"aria-describedby": shownError || hint ? describedById : void 0,
|
|
178
|
+
onChange: handleChange,
|
|
179
|
+
onBlur: handleBlur,
|
|
180
|
+
className: fieldClassName(
|
|
181
|
+
Boolean(shownError),
|
|
182
|
+
mono,
|
|
183
|
+
Boolean(trailing),
|
|
184
|
+
className
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
),
|
|
188
|
+
trailing ? /* @__PURE__ */ jsx("span", { className: "absolute top-1/2 right-[9px] -translate-y-1/2 text-hint font-semibold text-fg-subtle", children: trailing }) : null
|
|
189
|
+
] }),
|
|
190
|
+
/* @__PURE__ */ jsx(FieldMessage, { id: describedById, error: shownError, hint })
|
|
191
|
+
] });
|
|
192
|
+
}
|
|
193
|
+
export {
|
|
194
|
+
TextInput
|
|
195
|
+
};
|
|
196
|
+
//# sourceMappingURL=text-input.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/text-input/index.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n type ChangeEvent,\n type FocusEvent,\n type InputHTMLAttributes,\n type ReactNode,\n useCallback,\n useId,\n useState,\n} from \"react\";\n\n/**\n * Structural shape of a zod schema — every zod schema satisfies it, and any\n * version of zod does. Declaring it this way rather than importing `ZodType`\n * keeps zod out of this module's public types, so consumers who only want an\n * input do not have to install zod to typecheck it.\n */\nexport type TextInputSchema = {\n safeParse: (\n value: unknown,\n ) =>\n | { success: true }\n | { success: false; error: { issues: readonly { message: string }[] } };\n};\n\n/** Outcome of a single validation pass, handed to `onValidationChange`. */\nexport type TextInputValidation = {\n valid: boolean;\n /** First issue reported by the schema, or `undefined` when valid. */\n error?: string;\n};\n\nexport type TextInputProps = Omit<\n InputHTMLAttributes<HTMLInputElement>,\n \"onChange\" | \"value\" | \"defaultValue\" | \"type\"\n> & {\n label?: string;\n /** Guidance under the field. Replaced by the error message while invalid. */\n hint?: ReactNode;\n /**\n * Error supplied by the caller — a server-side or cross-field failure. Takes\n * precedence over the schema, so a form can hold a field invalid regardless\n * of what `schema` makes of the value on its own.\n */\n error?: string;\n /** Renders the design's muted \"Optional\" marker at the end of the label. */\n optional?: boolean;\n /** Trailing label slot, for affordances like the design's \"Forgot?\" link. */\n action?: ReactNode;\n /** Trailing in-field slot, for affordances like the design's password peek. */\n trailing?: ReactNode;\n /** Renders the value in the design's monospace face. */\n mono?: boolean;\n type?: \"text\" | \"email\" | \"password\" | \"tel\" | \"url\" | \"search\";\n value?: string;\n defaultValue?: string;\n onChange?: (value: string) => void;\n /**\n * Any zod schema that accepts a string. Only `safeParse` is used, so a\n * refined, transformed or preprocessed schema works as well as a plain one.\n */\n schema?: TextInputSchema;\n /**\n * When the schema runs. `blur` — the default — avoids scolding someone\n * mid-keystroke; once a field has gone invalid it revalidates on every\n * keystroke so the message clears as soon as it is fixed.\n */\n validateOn?: \"blur\" | \"change\" | \"never\";\n onValidationChange?: (state: TextInputValidation) => void;\n};\n\nconst FIELD_CLASSES =\n \"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\";\nconst VALID_CLASSES =\n \"border-line focus:border-accent focus:ring-3 focus:ring-accent-soft\";\nconst INVALID_CLASSES =\n \"border-danger focus:border-danger focus:ring-3 focus:ring-danger-soft\";\nconst FALLBACK_MESSAGE = \"Invalid value\";\n\nfunction runSchema(\n schema: TextInputSchema | undefined,\n value: string,\n): TextInputValidation {\n if (!schema) {\n return { valid: true };\n }\n\n const result = schema.safeParse(value);\n if (result.success) {\n return { valid: true };\n }\n\n return {\n valid: false,\n error: result.error.issues[0]?.message ?? FALLBACK_MESSAGE,\n };\n}\n\nfunction fieldClassName(\n invalid: boolean,\n mono: boolean,\n hasTrailing: boolean,\n className: string | undefined,\n): string {\n return [\n FIELD_CLASSES,\n invalid ? INVALID_CLASSES : VALID_CLASSES,\n mono ? \"font-code\" : \"\",\n hasTrailing ? \"pr-16\" : \"\",\n className ?? \"\",\n ]\n .filter(Boolean)\n .join(\" \");\n}\n\nfunction FieldLabel({\n htmlFor,\n label,\n optional,\n action,\n}: {\n htmlFor: string;\n label: string | undefined;\n optional: boolean;\n action: ReactNode;\n}) {\n if (!label) {\n return null;\n }\n\n return (\n <label\n htmlFor={htmlFor}\n className=\"mb-1.5 flex items-center text-label font-semibold text-fg-muted\"\n >\n {label}\n {optional ? (\n <span className=\"ml-auto text-hint font-semibold text-fg-subtle\">\n Optional\n </span>\n ) : null}\n {action ? <span className=\"ml-auto\">{action}</span> : null}\n </label>\n );\n}\n\nfunction FieldMessage({\n id,\n error,\n hint,\n}: {\n id: string;\n error: string | undefined;\n hint: ReactNode;\n}) {\n if (error) {\n return (\n <p id={id} role=\"alert\" className=\"mt-1.5 text-hint text-danger\">\n {error}\n </p>\n );\n }\n\n if (hint) {\n return (\n <p id={id} className=\"mt-1.5 text-hint text-fg-subtle\">\n {hint}\n </p>\n );\n }\n\n return null;\n}\n\ntype FieldBehaviour = Pick<\n TextInputProps,\n | \"value\"\n | \"defaultValue\"\n | \"error\"\n | \"onChange\"\n | \"onBlur\"\n | \"schema\"\n | \"validateOn\"\n | \"onValidationChange\"\n>;\n\n/**\n * Owns the value (controlled or not), the schema verdict, and when to re-run\n * it. Split out so the component itself stays markup.\n */\nfunction useFieldBehaviour({\n value,\n defaultValue,\n error,\n onChange,\n onBlur,\n schema,\n validateOn = \"blur\",\n onValidationChange,\n}: FieldBehaviour) {\n const [uncontrolledValue, setUncontrolledValue] = useState(\n defaultValue ?? \"\",\n );\n const [schemaError, setSchemaError] = useState<string | undefined>(undefined);\n\n const isControlled = value !== undefined;\n\n const validate = useCallback(\n (next: string) => {\n const state = runSchema(schema, next);\n setSchemaError(state.error);\n onValidationChange?.(state);\n },\n [schema, onValidationChange],\n );\n\n const handleChange = useCallback(\n (event: ChangeEvent<HTMLInputElement>) => {\n const next = event.target.value;\n if (!isControlled) {\n setUncontrolledValue(next);\n }\n onChange?.(next);\n\n // Revalidate on every keystroke once the field is already invalid, so a\n // corrected value clears the message without waiting for another blur.\n if (validateOn === \"change\" || (validateOn === \"blur\" && schemaError)) {\n validate(next);\n }\n },\n [isControlled, onChange, validateOn, schemaError, validate],\n );\n\n const handleBlur = useCallback(\n (event: FocusEvent<HTMLInputElement>) => {\n if (validateOn === \"blur\") {\n validate(event.target.value);\n }\n onBlur?.(event);\n },\n [validateOn, validate, onBlur],\n );\n\n return {\n currentValue: isControlled ? value : uncontrolledValue,\n // A caller-supplied error outranks the schema's verdict.\n shownError: error ?? schemaError,\n handleChange,\n handleBlur,\n };\n}\n\nexport function TextInput({\n label,\n hint,\n error,\n optional = false,\n action,\n trailing,\n mono = false,\n type = \"text\",\n value,\n defaultValue,\n onChange,\n schema,\n validateOn = \"blur\",\n onValidationChange,\n id,\n className,\n onBlur,\n ...rest\n}: TextInputProps) {\n const generatedId = useId();\n const inputId = id ?? generatedId;\n const describedById = `${inputId}-description`;\n\n const { currentValue, shownError, handleChange, handleBlur } =\n useFieldBehaviour({\n value,\n defaultValue,\n error,\n onChange,\n onBlur,\n schema,\n validateOn,\n onValidationChange,\n });\n\n return (\n <div className=\"flex flex-col\">\n <FieldLabel\n htmlFor={inputId}\n label={label}\n optional={optional}\n action={action}\n />\n\n <div className={trailing ? \"relative\" : undefined}>\n <input\n {...rest}\n id={inputId}\n type={type}\n value={currentValue}\n aria-invalid={shownError ? true : undefined}\n aria-describedby={shownError || hint ? describedById : undefined}\n onChange={handleChange}\n onBlur={handleBlur}\n className={fieldClassName(\n Boolean(shownError),\n mono,\n Boolean(trailing),\n className,\n )}\n />\n {trailing ? (\n <span className=\"absolute top-1/2 right-[9px] -translate-y-1/2 text-hint font-semibold text-fg-subtle\">\n {trailing}\n </span>\n ) : null}\n </div>\n\n <FieldMessage id={describedById} error={shownError} hint={hint} />\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var zod = require('zod');
|
|
4
|
+
|
|
5
|
+
// src/validation/index.ts
|
|
6
|
+
var REQUIRED = 1;
|
|
7
|
+
var MIN_PASSWORD_LENGTH = 8;
|
|
8
|
+
var VERIFICATION_CODE_LENGTH = 8;
|
|
9
|
+
var MIN_BACKUP_CODE_LENGTH = 6;
|
|
10
|
+
var MIN_PHONE_LENGTH = 7;
|
|
11
|
+
var EMAIL_PATTERN = /.+@.+\..+/;
|
|
12
|
+
var UPPER_CASE = /[A-Z]/;
|
|
13
|
+
var LOWER_CASE = /[a-z]/;
|
|
14
|
+
var DIGIT = /[0-9]/;
|
|
15
|
+
var SYMBOL = /[^A-Za-z0-9]/;
|
|
16
|
+
var SEPARATORS = /-/g;
|
|
17
|
+
var PHONE_PATTERN = new RegExp(`^[+]?[\\d\\s().-]{${MIN_PHONE_LENGTH},}$`);
|
|
18
|
+
var emailSchema = zod.z.string().trim().min(REQUIRED, "Enter your work email").regex(EMAIL_PATTERN, "Enter a valid email address");
|
|
19
|
+
var passwordSchema = zod.z.string().min(MIN_PASSWORD_LENGTH, "Use at least 8 characters").refine(
|
|
20
|
+
(value) => UPPER_CASE.test(value) && LOWER_CASE.test(value),
|
|
21
|
+
"Use both upper and lower case letters"
|
|
22
|
+
).refine(
|
|
23
|
+
(value) => DIGIT.test(value) || SYMBOL.test(value),
|
|
24
|
+
"Add a number or symbol"
|
|
25
|
+
);
|
|
26
|
+
var verificationCodeSchema = zod.z.string().transform((value) => value.replace(SEPARATORS, "").toUpperCase()).refine(
|
|
27
|
+
(value) => value.length === VERIFICATION_CODE_LENGTH,
|
|
28
|
+
"Enter the 8-character code"
|
|
29
|
+
);
|
|
30
|
+
var backupCodeSchema = zod.z.string().trim().min(MIN_BACKUP_CODE_LENGTH, "Enter a backup code");
|
|
31
|
+
var fullNameSchema = zod.z.string().trim().min(REQUIRED, "Enter your full name");
|
|
32
|
+
var jobTitleSchema = zod.z.string().trim().min(REQUIRED, "Enter your job title");
|
|
33
|
+
var phoneSchema = zod.z.string().trim().regex(PHONE_PATTERN, "Enter a valid mobile number");
|
|
34
|
+
|
|
35
|
+
exports.backupCodeSchema = backupCodeSchema;
|
|
36
|
+
exports.emailSchema = emailSchema;
|
|
37
|
+
exports.fullNameSchema = fullNameSchema;
|
|
38
|
+
exports.jobTitleSchema = jobTitleSchema;
|
|
39
|
+
exports.passwordSchema = passwordSchema;
|
|
40
|
+
exports.phoneSchema = phoneSchema;
|
|
41
|
+
exports.verificationCodeSchema = verificationCodeSchema;
|
|
42
|
+
//# sourceMappingURL=validation.cjs.map
|
|
43
|
+
//# sourceMappingURL=validation.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/validation/index.ts"],"names":["z"],"mappings":";;;;;AAYA,IAAM,QAAA,GAAW,CAAA;AACjB,IAAM,mBAAA,GAAsB,CAAA;AAC5B,IAAM,wBAAA,GAA2B,CAAA;AACjC,IAAM,sBAAA,GAAyB,CAAA;AAC/B,IAAM,gBAAA,GAAmB,CAAA;AAEzB,IAAM,aAAA,GAAgB,WAAA;AACtB,IAAM,UAAA,GAAa,OAAA;AACnB,IAAM,UAAA,GAAa,OAAA;AACnB,IAAM,KAAA,GAAQ,OAAA;AACd,IAAM,MAAA,GAAS,cAAA;AACf,IAAM,UAAA,GAAa,IAAA;AACnB,IAAM,aAAA,GAAgB,IAAI,MAAA,CAAO,CAAA,kBAAA,EAAqB,gBAAgB,CAAA,GAAA,CAAK,CAAA;AAOpE,IAAM,WAAA,GAAcA,KAAA,CACxB,MAAA,EAAO,CACP,IAAA,EAAK,CACL,GAAA,CAAI,QAAA,EAAU,uBAAuB,CAAA,CACrC,KAAA,CAAM,aAAA,EAAe,6BAA6B;AAG9C,IAAM,iBAAiBA,KAAA,CAC3B,MAAA,GACA,GAAA,CAAI,mBAAA,EAAqB,2BAA2B,CAAA,CACpD,MAAA;AAAA,EACC,CAAC,UAAU,UAAA,CAAW,IAAA,CAAK,KAAK,CAAA,IAAK,UAAA,CAAW,KAAK,KAAK,CAAA;AAAA,EAC1D;AACF,CAAA,CACC,MAAA;AAAA,EACC,CAAC,UAAU,KAAA,CAAM,IAAA,CAAK,KAAK,CAAA,IAAK,MAAA,CAAO,KAAK,KAAK,CAAA;AAAA,EACjD;AACF;AAOK,IAAM,sBAAA,GAAyBA,KAAA,CACnC,MAAA,EAAO,CACP,UAAU,CAAC,KAAA,KAAU,KAAA,CAAM,OAAA,CAAQ,UAAA,EAAY,EAAE,CAAA,CAAE,WAAA,EAAa,CAAA,CAChE,MAAA;AAAA,EACC,CAAC,KAAA,KAAU,KAAA,CAAM,MAAA,KAAW,wBAAA;AAAA,EAC5B;AACF;AAGK,IAAM,gBAAA,GAAmBA,MAC7B,MAAA,EAAO,CACP,MAAK,CACL,GAAA,CAAI,wBAAwB,qBAAqB;AAG7C,IAAM,cAAA,GAAiBA,MAC3B,MAAA,EAAO,CACP,MAAK,CACL,GAAA,CAAI,UAAU,sBAAsB;AAEhC,IAAM,cAAA,GAAiBA,MAC3B,MAAA,EAAO,CACP,MAAK,CACL,GAAA,CAAI,UAAU,sBAAsB;AAMhC,IAAM,WAAA,GAAcA,MACxB,MAAA,EAAO,CACP,MAAK,CACL,KAAA,CAAM,eAAe,6BAA6B","file":"validation.cjs","sourcesContent":["import { z } from \"zod\";\n\n/**\n * Field rules taken from the Carbonbits V1 auth design, which validates inline\n * as you type. The predicates here are the ones that screen actually applies —\n * kept in one place so a form built from `TextInput` agrees with the design\n * rather than re-deriving the rules per screen.\n *\n * These are ordinary zod schemas: extend them with `.refine`, swap a message\n * with `.min(8, \"…\")`, or ignore them and pass your own.\n */\n\nconst REQUIRED = 1;\nconst MIN_PASSWORD_LENGTH = 8;\nconst VERIFICATION_CODE_LENGTH = 8;\nconst MIN_BACKUP_CODE_LENGTH = 6;\nconst MIN_PHONE_LENGTH = 7;\n\nconst EMAIL_PATTERN = /.+@.+\\..+/;\nconst UPPER_CASE = /[A-Z]/;\nconst LOWER_CASE = /[a-z]/;\nconst DIGIT = /[0-9]/;\nconst SYMBOL = /[^A-Za-z0-9]/;\nconst SEPARATORS = /-/g;\nconst PHONE_PATTERN = new RegExp(`^[+]?[\\\\d\\\\s().-]{${MIN_PHONE_LENGTH},}$`);\n\n/**\n * The design checks `/.+@.+\\..+/` rather than anything stricter, which accepts\n * the addresses people actually type and leaves real verification to the\n * confirmation email. Kept deliberately, in place of `z.email()`.\n */\nexport const emailSchema = z\n .string()\n .trim()\n .min(REQUIRED, \"Enter your work email\")\n .regex(EMAIL_PATTERN, \"Enter a valid email address\");\n\n/** At least 8 characters, upper and lower case, and a number or symbol. */\nexport const passwordSchema = z\n .string()\n .min(MIN_PASSWORD_LENGTH, \"Use at least 8 characters\")\n .refine(\n (value) => UPPER_CASE.test(value) && LOWER_CASE.test(value),\n \"Use both upper and lower case letters\",\n )\n .refine(\n (value) => DIGIT.test(value) || SYMBOL.test(value),\n \"Add a number or symbol\",\n );\n\n/**\n * Eight characters, displayed as `XXXX-XXXX`. The separator is presentational,\n * so it is stripped before the length is counted and the parsed value comes\n * back normalised and upper-cased.\n */\nexport const verificationCodeSchema = z\n .string()\n .transform((value) => value.replace(SEPARATORS, \"\").toUpperCase())\n .refine(\n (value) => value.length === VERIFICATION_CODE_LENGTH,\n \"Enter the 8-character code\",\n );\n\n/** Recovery codes; the design enables the button at 6 characters. */\nexport const backupCodeSchema = z\n .string()\n .trim()\n .min(MIN_BACKUP_CODE_LENGTH, \"Enter a backup code\");\n\n/** Free-text profile fields — the design only requires them to be non-empty. */\nexport const fullNameSchema = z\n .string()\n .trim()\n .min(REQUIRED, \"Enter your full name\");\n\nexport const jobTitleSchema = z\n .string()\n .trim()\n .min(REQUIRED, \"Enter your job title\");\n\n/**\n * Optional in the design, and used for recovery and alerts only, so this stays\n * permissive: digits, spaces and the usual punctuation, in any arrangement.\n */\nexport const phoneSchema = z\n .string()\n .trim()\n .regex(PHONE_PATTERN, \"Enter a valid mobile number\");\n"]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The design checks `/.+@.+\..+/` rather than anything stricter, which accepts
|
|
5
|
+
* the addresses people actually type and leaves real verification to the
|
|
6
|
+
* confirmation email. Kept deliberately, in place of `z.email()`.
|
|
7
|
+
*/
|
|
8
|
+
declare const emailSchema: z.ZodString;
|
|
9
|
+
/** At least 8 characters, upper and lower case, and a number or symbol. */
|
|
10
|
+
declare const passwordSchema: z.ZodString;
|
|
11
|
+
/**
|
|
12
|
+
* Eight characters, displayed as `XXXX-XXXX`. The separator is presentational,
|
|
13
|
+
* so it is stripped before the length is counted and the parsed value comes
|
|
14
|
+
* back normalised and upper-cased.
|
|
15
|
+
*/
|
|
16
|
+
declare const verificationCodeSchema: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
17
|
+
/** Recovery codes; the design enables the button at 6 characters. */
|
|
18
|
+
declare const backupCodeSchema: z.ZodString;
|
|
19
|
+
/** Free-text profile fields — the design only requires them to be non-empty. */
|
|
20
|
+
declare const fullNameSchema: z.ZodString;
|
|
21
|
+
declare const jobTitleSchema: z.ZodString;
|
|
22
|
+
/**
|
|
23
|
+
* Optional in the design, and used for recovery and alerts only, so this stays
|
|
24
|
+
* permissive: digits, spaces and the usual punctuation, in any arrangement.
|
|
25
|
+
*/
|
|
26
|
+
declare const phoneSchema: z.ZodString;
|
|
27
|
+
|
|
28
|
+
export { backupCodeSchema, emailSchema, fullNameSchema, jobTitleSchema, passwordSchema, phoneSchema, verificationCodeSchema };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The design checks `/.+@.+\..+/` rather than anything stricter, which accepts
|
|
5
|
+
* the addresses people actually type and leaves real verification to the
|
|
6
|
+
* confirmation email. Kept deliberately, in place of `z.email()`.
|
|
7
|
+
*/
|
|
8
|
+
declare const emailSchema: z.ZodString;
|
|
9
|
+
/** At least 8 characters, upper and lower case, and a number or symbol. */
|
|
10
|
+
declare const passwordSchema: z.ZodString;
|
|
11
|
+
/**
|
|
12
|
+
* Eight characters, displayed as `XXXX-XXXX`. The separator is presentational,
|
|
13
|
+
* so it is stripped before the length is counted and the parsed value comes
|
|
14
|
+
* back normalised and upper-cased.
|
|
15
|
+
*/
|
|
16
|
+
declare const verificationCodeSchema: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
17
|
+
/** Recovery codes; the design enables the button at 6 characters. */
|
|
18
|
+
declare const backupCodeSchema: z.ZodString;
|
|
19
|
+
/** Free-text profile fields — the design only requires them to be non-empty. */
|
|
20
|
+
declare const fullNameSchema: z.ZodString;
|
|
21
|
+
declare const jobTitleSchema: z.ZodString;
|
|
22
|
+
/**
|
|
23
|
+
* Optional in the design, and used for recovery and alerts only, so this stays
|
|
24
|
+
* permissive: digits, spaces and the usual punctuation, in any arrangement.
|
|
25
|
+
*/
|
|
26
|
+
declare const phoneSchema: z.ZodString;
|
|
27
|
+
|
|
28
|
+
export { backupCodeSchema, emailSchema, fullNameSchema, jobTitleSchema, passwordSchema, phoneSchema, verificationCodeSchema };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
// src/validation/index.ts
|
|
4
|
+
var REQUIRED = 1;
|
|
5
|
+
var MIN_PASSWORD_LENGTH = 8;
|
|
6
|
+
var VERIFICATION_CODE_LENGTH = 8;
|
|
7
|
+
var MIN_BACKUP_CODE_LENGTH = 6;
|
|
8
|
+
var MIN_PHONE_LENGTH = 7;
|
|
9
|
+
var EMAIL_PATTERN = /.+@.+\..+/;
|
|
10
|
+
var UPPER_CASE = /[A-Z]/;
|
|
11
|
+
var LOWER_CASE = /[a-z]/;
|
|
12
|
+
var DIGIT = /[0-9]/;
|
|
13
|
+
var SYMBOL = /[^A-Za-z0-9]/;
|
|
14
|
+
var SEPARATORS = /-/g;
|
|
15
|
+
var PHONE_PATTERN = new RegExp(`^[+]?[\\d\\s().-]{${MIN_PHONE_LENGTH},}$`);
|
|
16
|
+
var emailSchema = z.string().trim().min(REQUIRED, "Enter your work email").regex(EMAIL_PATTERN, "Enter a valid email address");
|
|
17
|
+
var passwordSchema = z.string().min(MIN_PASSWORD_LENGTH, "Use at least 8 characters").refine(
|
|
18
|
+
(value) => UPPER_CASE.test(value) && LOWER_CASE.test(value),
|
|
19
|
+
"Use both upper and lower case letters"
|
|
20
|
+
).refine(
|
|
21
|
+
(value) => DIGIT.test(value) || SYMBOL.test(value),
|
|
22
|
+
"Add a number or symbol"
|
|
23
|
+
);
|
|
24
|
+
var verificationCodeSchema = z.string().transform((value) => value.replace(SEPARATORS, "").toUpperCase()).refine(
|
|
25
|
+
(value) => value.length === VERIFICATION_CODE_LENGTH,
|
|
26
|
+
"Enter the 8-character code"
|
|
27
|
+
);
|
|
28
|
+
var backupCodeSchema = z.string().trim().min(MIN_BACKUP_CODE_LENGTH, "Enter a backup code");
|
|
29
|
+
var fullNameSchema = z.string().trim().min(REQUIRED, "Enter your full name");
|
|
30
|
+
var jobTitleSchema = z.string().trim().min(REQUIRED, "Enter your job title");
|
|
31
|
+
var phoneSchema = z.string().trim().regex(PHONE_PATTERN, "Enter a valid mobile number");
|
|
32
|
+
|
|
33
|
+
export { backupCodeSchema, emailSchema, fullNameSchema, jobTitleSchema, passwordSchema, phoneSchema, verificationCodeSchema };
|
|
34
|
+
//# sourceMappingURL=validation.js.map
|
|
35
|
+
//# sourceMappingURL=validation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/validation/index.ts"],"names":[],"mappings":";;;AAYA,IAAM,QAAA,GAAW,CAAA;AACjB,IAAM,mBAAA,GAAsB,CAAA;AAC5B,IAAM,wBAAA,GAA2B,CAAA;AACjC,IAAM,sBAAA,GAAyB,CAAA;AAC/B,IAAM,gBAAA,GAAmB,CAAA;AAEzB,IAAM,aAAA,GAAgB,WAAA;AACtB,IAAM,UAAA,GAAa,OAAA;AACnB,IAAM,UAAA,GAAa,OAAA;AACnB,IAAM,KAAA,GAAQ,OAAA;AACd,IAAM,MAAA,GAAS,cAAA;AACf,IAAM,UAAA,GAAa,IAAA;AACnB,IAAM,aAAA,GAAgB,IAAI,MAAA,CAAO,CAAA,kBAAA,EAAqB,gBAAgB,CAAA,GAAA,CAAK,CAAA;AAOpE,IAAM,WAAA,GAAc,CAAA,CACxB,MAAA,EAAO,CACP,IAAA,EAAK,CACL,GAAA,CAAI,QAAA,EAAU,uBAAuB,CAAA,CACrC,KAAA,CAAM,aAAA,EAAe,6BAA6B;AAG9C,IAAM,iBAAiB,CAAA,CAC3B,MAAA,GACA,GAAA,CAAI,mBAAA,EAAqB,2BAA2B,CAAA,CACpD,MAAA;AAAA,EACC,CAAC,UAAU,UAAA,CAAW,IAAA,CAAK,KAAK,CAAA,IAAK,UAAA,CAAW,KAAK,KAAK,CAAA;AAAA,EAC1D;AACF,CAAA,CACC,MAAA;AAAA,EACC,CAAC,UAAU,KAAA,CAAM,IAAA,CAAK,KAAK,CAAA,IAAK,MAAA,CAAO,KAAK,KAAK,CAAA;AAAA,EACjD;AACF;AAOK,IAAM,sBAAA,GAAyB,CAAA,CACnC,MAAA,EAAO,CACP,UAAU,CAAC,KAAA,KAAU,KAAA,CAAM,OAAA,CAAQ,UAAA,EAAY,EAAE,CAAA,CAAE,WAAA,EAAa,CAAA,CAChE,MAAA;AAAA,EACC,CAAC,KAAA,KAAU,KAAA,CAAM,MAAA,KAAW,wBAAA;AAAA,EAC5B;AACF;AAGK,IAAM,gBAAA,GAAmB,EAC7B,MAAA,EAAO,CACP,MAAK,CACL,GAAA,CAAI,wBAAwB,qBAAqB;AAG7C,IAAM,cAAA,GAAiB,EAC3B,MAAA,EAAO,CACP,MAAK,CACL,GAAA,CAAI,UAAU,sBAAsB;AAEhC,IAAM,cAAA,GAAiB,EAC3B,MAAA,EAAO,CACP,MAAK,CACL,GAAA,CAAI,UAAU,sBAAsB;AAMhC,IAAM,WAAA,GAAc,EACxB,MAAA,EAAO,CACP,MAAK,CACL,KAAA,CAAM,eAAe,6BAA6B","file":"validation.js","sourcesContent":["import { z } from \"zod\";\n\n/**\n * Field rules taken from the Carbonbits V1 auth design, which validates inline\n * as you type. The predicates here are the ones that screen actually applies —\n * kept in one place so a form built from `TextInput` agrees with the design\n * rather than re-deriving the rules per screen.\n *\n * These are ordinary zod schemas: extend them with `.refine`, swap a message\n * with `.min(8, \"…\")`, or ignore them and pass your own.\n */\n\nconst REQUIRED = 1;\nconst MIN_PASSWORD_LENGTH = 8;\nconst VERIFICATION_CODE_LENGTH = 8;\nconst MIN_BACKUP_CODE_LENGTH = 6;\nconst MIN_PHONE_LENGTH = 7;\n\nconst EMAIL_PATTERN = /.+@.+\\..+/;\nconst UPPER_CASE = /[A-Z]/;\nconst LOWER_CASE = /[a-z]/;\nconst DIGIT = /[0-9]/;\nconst SYMBOL = /[^A-Za-z0-9]/;\nconst SEPARATORS = /-/g;\nconst PHONE_PATTERN = new RegExp(`^[+]?[\\\\d\\\\s().-]{${MIN_PHONE_LENGTH},}$`);\n\n/**\n * The design checks `/.+@.+\\..+/` rather than anything stricter, which accepts\n * the addresses people actually type and leaves real verification to the\n * confirmation email. Kept deliberately, in place of `z.email()`.\n */\nexport const emailSchema = z\n .string()\n .trim()\n .min(REQUIRED, \"Enter your work email\")\n .regex(EMAIL_PATTERN, \"Enter a valid email address\");\n\n/** At least 8 characters, upper and lower case, and a number or symbol. */\nexport const passwordSchema = z\n .string()\n .min(MIN_PASSWORD_LENGTH, \"Use at least 8 characters\")\n .refine(\n (value) => UPPER_CASE.test(value) && LOWER_CASE.test(value),\n \"Use both upper and lower case letters\",\n )\n .refine(\n (value) => DIGIT.test(value) || SYMBOL.test(value),\n \"Add a number or symbol\",\n );\n\n/**\n * Eight characters, displayed as `XXXX-XXXX`. The separator is presentational,\n * so it is stripped before the length is counted and the parsed value comes\n * back normalised and upper-cased.\n */\nexport const verificationCodeSchema = z\n .string()\n .transform((value) => value.replace(SEPARATORS, \"\").toUpperCase())\n .refine(\n (value) => value.length === VERIFICATION_CODE_LENGTH,\n \"Enter the 8-character code\",\n );\n\n/** Recovery codes; the design enables the button at 6 characters. */\nexport const backupCodeSchema = z\n .string()\n .trim()\n .min(MIN_BACKUP_CODE_LENGTH, \"Enter a backup code\");\n\n/** Free-text profile fields — the design only requires them to be non-empty. */\nexport const fullNameSchema = z\n .string()\n .trim()\n .min(REQUIRED, \"Enter your full name\");\n\nexport const jobTitleSchema = z\n .string()\n .trim()\n .min(REQUIRED, \"Enter your job title\");\n\n/**\n * Optional in the design, and used for recovery and alerts only, so this stays\n * permissive: digits, spaces and the usual punctuation, in any arrangement.\n */\nexport const phoneSchema = z\n .string()\n .trim()\n .regex(PHONE_PATTERN, \"Enter a valid mobile number\");\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carbonbits/sixr",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "e-commerce geared component library",
|
|
5
5
|
"homepage": "https://carbonbits.co.ke/sixr",
|
|
6
6
|
"repository": {
|
|
@@ -42,6 +42,26 @@
|
|
|
42
42
|
"default": "./dist/cart.cjs"
|
|
43
43
|
}
|
|
44
44
|
},
|
|
45
|
+
"./text-input": {
|
|
46
|
+
"import": {
|
|
47
|
+
"types": "./dist/text-input.d.ts",
|
|
48
|
+
"default": "./dist/text-input.js"
|
|
49
|
+
},
|
|
50
|
+
"require": {
|
|
51
|
+
"types": "./dist/text-input.d.cts",
|
|
52
|
+
"default": "./dist/text-input.cjs"
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"./validation": {
|
|
56
|
+
"import": {
|
|
57
|
+
"types": "./dist/validation.d.ts",
|
|
58
|
+
"default": "./dist/validation.js"
|
|
59
|
+
},
|
|
60
|
+
"require": {
|
|
61
|
+
"types": "./dist/validation.d.cts",
|
|
62
|
+
"default": "./dist/validation.cjs"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
45
65
|
"./styles.css": "./dist/styles.css",
|
|
46
66
|
"./package.json": "./package.json"
|
|
47
67
|
},
|
|
@@ -54,7 +74,7 @@
|
|
|
54
74
|
"packageManager": "pnpm@12.4.2",
|
|
55
75
|
"scripts": {
|
|
56
76
|
"build": "pnpm run build:js && pnpm run build:css",
|
|
57
|
-
"build:js": "tsup",
|
|
77
|
+
"build:js": "rm -rf dist && tsup",
|
|
58
78
|
"build:css": "tailwindcss -i ./src/styles/index.css -o ./dist/styles.css --minify",
|
|
59
79
|
"typecheck": "tsc --noEmit",
|
|
60
80
|
"lint": "biome check --error-on-warnings",
|
|
@@ -68,7 +88,13 @@
|
|
|
68
88
|
},
|
|
69
89
|
"peerDependencies": {
|
|
70
90
|
"react": "^19.0.0",
|
|
71
|
-
"react-dom": "^19.0.0"
|
|
91
|
+
"react-dom": "^19.0.0",
|
|
92
|
+
"zod": "^4.0.0"
|
|
93
|
+
},
|
|
94
|
+
"peerDependenciesMeta": {
|
|
95
|
+
"zod": {
|
|
96
|
+
"optional": true
|
|
97
|
+
}
|
|
72
98
|
},
|
|
73
99
|
"devDependencies": {
|
|
74
100
|
"@biomejs/biome": "^2.5.14",
|
|
@@ -101,6 +127,7 @@
|
|
|
101
127
|
"tsup": "^8.5.1",
|
|
102
128
|
"typescript": "^6.0.3",
|
|
103
129
|
"vite": "^8.3.0",
|
|
104
|
-
"vitest": "^5.0.1"
|
|
130
|
+
"vitest": "^5.0.1",
|
|
131
|
+
"zod": "^4.0.0"
|
|
105
132
|
}
|
|
106
133
|
}
|