@blankjs/react 0.1.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/LICENSE +21 -0
- package/README.md +190 -0
- package/dist/index.d.mts +738 -0
- package/dist/index.mjs +1732 -0
- package/dist/index.mjs.map +1 -0
- package/dist/style.css +1161 -0
- package/package.json +68 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sezmol
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# blankjs
|
|
2
|
+
|
|
3
|
+
Form-first React components built on native form elements.
|
|
4
|
+
|
|
5
|
+
blankjs is a component library that treats the browser as an ally instead of an obstacle. Where other libraries rebuild form controls from `<div>`s and ARIA, blankjs styles the real elements and lets the platform do the work.
|
|
6
|
+
|
|
7
|
+
## Packages
|
|
8
|
+
|
|
9
|
+
| Package | Contents |
|
|
10
|
+
|---------|----------|
|
|
11
|
+
| `@blankjs/react` | Styled, accessible components |
|
|
12
|
+
| `@blankjs/core` | Headless hooks: `useControllableState`, `useFieldControlProps`, `useCollection` |
|
|
13
|
+
| `@blankjs/tokens` | Design tokens as CSS variables |
|
|
14
|
+
|
|
15
|
+
## Philosophy
|
|
16
|
+
|
|
17
|
+
### Native first
|
|
18
|
+
|
|
19
|
+
Most libraries render a `<button role="checkbox">`, manage its state in JavaScript, and mirror the value into a hidden input for forms. blankjs renders a real `<input type="checkbox">` and strips its default look with `appearance: none`.
|
|
20
|
+
|
|
21
|
+
The browser then handles, with zero JavaScript on our side:
|
|
22
|
+
|
|
23
|
+
- **FormData serialization**: the value is in `new FormData(form)` because the input is real
|
|
24
|
+
- **Form reset**: `<button type="reset">` restores every control
|
|
25
|
+
- **Label activation**: clicking a `<label>` toggles the control
|
|
26
|
+
- **Radio semantics**: arrow-key navigation and roving tabindex come from the platform
|
|
27
|
+
- **Tab order**: one tab stop per radio group, as users expect
|
|
28
|
+
|
|
29
|
+
Checkbox, Switch, and RadioGroup are styled native inputs. Switch is a checkbox with `role="switch"`. No state mirroring, no synthetic focus management, no drift between what you see and what the form submits.
|
|
30
|
+
|
|
31
|
+
### Form first, without a form library
|
|
32
|
+
|
|
33
|
+
Every component participates in a plain `<form>`:
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
<form onSubmit={handleSubmit}>
|
|
37
|
+
<Field.Root>
|
|
38
|
+
<Field.Label>Country</Field.Label>
|
|
39
|
+
<Select.Root name="country" defaultValue="AU">
|
|
40
|
+
<Select.Trigger>
|
|
41
|
+
<Select.Value placeholder="Select" />
|
|
42
|
+
</Select.Trigger>
|
|
43
|
+
<Select.Clear />
|
|
44
|
+
<Select.Content>
|
|
45
|
+
<Select.Item value="AU">Australia</Select.Item>
|
|
46
|
+
<Select.Item value="AT">Austria</Select.Item>
|
|
47
|
+
</Select.Content>
|
|
48
|
+
</Select.Root>
|
|
49
|
+
</Field.Root>
|
|
50
|
+
|
|
51
|
+
<MultiSelect.Root name="visited" defaultValue={["AU"]}>
|
|
52
|
+
{/* ... */}
|
|
53
|
+
</MultiSelect.Root>
|
|
54
|
+
|
|
55
|
+
<button type="submit">Submit</button>
|
|
56
|
+
<button type="reset">Reset</button>
|
|
57
|
+
</form>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const data = new FormData(form);
|
|
62
|
+
data.get("country"); // "AU"
|
|
63
|
+
data.getAll("visited"); // ["AU"]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
How it works:
|
|
67
|
+
|
|
68
|
+
- Composite widgets (Select, Combobox, MultiSelect) render hidden inputs bound to `name`
|
|
69
|
+
- MultiSelect submits one entry per selected value, read with `formData.getAll(name)`
|
|
70
|
+
- Every component listens for the form's `reset` event and restores its `defaultValue`, including controlled inputs the browser resets without firing a change event
|
|
71
|
+
- Disabled controls drop out of FormData, matching native behavior
|
|
72
|
+
|
|
73
|
+
No form library required. React Hook Form and friends still work if you want them, but they stop being a prerequisite.
|
|
74
|
+
|
|
75
|
+
### A predictable event contract
|
|
76
|
+
|
|
77
|
+
Every component follows one rule: your handler runs first, then the library acts, unless you veto.
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
<Select.Item
|
|
81
|
+
value="delete-account"
|
|
82
|
+
onClick={(e) => {
|
|
83
|
+
if (!confirmed) e.preventDefault(); // selection blocked
|
|
84
|
+
}}
|
|
85
|
+
/>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
This holds for every `onClick`, `onChange`, `onKeyDown`, and `onBlur` across the library. No `onSelect` vs `onClick` ambiguity, no undocumented ordering.
|
|
89
|
+
|
|
90
|
+
### Composition over configuration
|
|
91
|
+
|
|
92
|
+
Compound components with real DOM parts you can style and rearrange:
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
<Combobox.Root name="city" inputValue={query} onInputValueChange={setQuery}>
|
|
96
|
+
<Combobox.Input />
|
|
97
|
+
<Combobox.Clear />
|
|
98
|
+
<Combobox.Content>
|
|
99
|
+
{cities.map((c) => (
|
|
100
|
+
<Combobox.Item key={c} value={c}>{c}</Combobox.Item>
|
|
101
|
+
))}
|
|
102
|
+
</Combobox.Content>
|
|
103
|
+
</Combobox.Root>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Filtering stays in your hands: you own the list, the library owns keyboard navigation, ARIA wiring, and commit semantics.
|
|
107
|
+
|
|
108
|
+
## Components
|
|
109
|
+
|
|
110
|
+
| Component | Notes |
|
|
111
|
+
|-----------|-------|
|
|
112
|
+
| `Button` | Variants, native `type` handling |
|
|
113
|
+
| `TextInput` | Native input styled with tokens |
|
|
114
|
+
| `PasswordField` | Visibility toggle |
|
|
115
|
+
| `Textarea` | Auto-grows via CSS `field-sizing: content` |
|
|
116
|
+
| `Checkbox` | Native input, `indeterminate` support |
|
|
117
|
+
| `Switch` | Native checkbox with `role="switch"` |
|
|
118
|
+
| `RadioGroup` | Native radios, platform keyboard navigation |
|
|
119
|
+
| `Select` | Single value, typeahead, hidden input, `Select.Clear` |
|
|
120
|
+
| `MultiSelect` | Multiple values, stays open while picking, `formData.getAll` |
|
|
121
|
+
| `Combobox` | Controlled filtering, draft revert on Escape and blur |
|
|
122
|
+
| `Field` | Label, description, and error wiring through ARIA |
|
|
123
|
+
|
|
124
|
+
### Field
|
|
125
|
+
|
|
126
|
+
`Field.Root` connects a control to its label, description, and error text:
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
<Field.Root invalid={!!error}>
|
|
130
|
+
<Field.Label>Email</Field.Label>
|
|
131
|
+
<TextInput name="email" />
|
|
132
|
+
<Field.Description>Work email preferred</Field.Description>
|
|
133
|
+
<Field.Error>{error}</Field.Error>
|
|
134
|
+
</Field.Root>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The control receives `id`, `aria-describedby`, `aria-invalid`, and `disabled` from context. Group controls such as RadioGroup are labelled through `aria-labelledby`, since a `<div role="radiogroup">` is not a labelable element.
|
|
138
|
+
|
|
139
|
+
## Theming
|
|
140
|
+
|
|
141
|
+
Tokens are CSS variables. Switch themes by setting one attribute, with no JavaScript involved:
|
|
142
|
+
|
|
143
|
+
```html
|
|
144
|
+
<html data-bk-theme="dark">
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Override any token to rebrand:
|
|
148
|
+
|
|
149
|
+
```css
|
|
150
|
+
:root {
|
|
151
|
+
--bk-color-accent: oklch(0.65 0.2 250);
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Usage
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
import { Select, Field, Checkbox } from "@blankjs/react";
|
|
159
|
+
import "@blankjs/react/styles.css";
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The packages are not published to npm yet. To explore locally:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
pnpm install
|
|
166
|
+
pnpm --filter playground dev
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The playground demonstrates every component plus a full form submitting through `FormData`.
|
|
170
|
+
|
|
171
|
+
## Development
|
|
172
|
+
|
|
173
|
+
- pnpm workspaces + Turborepo
|
|
174
|
+
- React 19, TypeScript strict
|
|
175
|
+
- Vitest + Testing Library, 230+ tests across core and react
|
|
176
|
+
- tsdown for package builds
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
pnpm test
|
|
180
|
+
pnpm typecheck
|
|
181
|
+
pnpm lint
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Status
|
|
185
|
+
|
|
186
|
+
Pre-release. The API may change before 1.0.
|
|
187
|
+
|
|
188
|
+
## License
|
|
189
|
+
|
|
190
|
+
MIT
|