@cronocode/react-box 3.2.0 → 3.2.1
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/.claude/rules/react-box-rules.md +17 -0
- package/.claude/skills/react-box/SKILL.md +17 -0
- package/BOX_AI_CONTEXT.md +430 -812
- package/components/dropdown/dropdownContext.d.ts +17 -0
- package/components/dropdown/dropdownItemRenderer.d.ts +6 -0
- package/components/dropdown/dropdownItems.d.ts +15 -0
- package/components/dropdown/dropdownSearch.d.ts +8 -0
- package/components/dropdown/utils.d.ts +1 -0
- package/components/dropdown.cjs +1 -1
- package/components/dropdown.d.ts +6 -5
- package/components/dropdown.mjs +212 -160
- package/components/select.cjs +1 -0
- package/components/select.d.ts +39 -0
- package/components/select.mjs +29 -0
- package/package.json +2 -2
package/BOX_AI_CONTEXT.md
CHANGED
|
@@ -1,213 +1,74 @@
|
|
|
1
1
|
# @cronocode/react-box - AI Assistant Context
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Runtime CSS-in-JS library. `Box` component accepts ~144 CSS props and generates CSS classes at runtime. Same prop values share a single class.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
## CRITICAL RULES
|
|
7
|
+
## CRITICAL RULES
|
|
8
8
|
|
|
9
9
|
### Rule #1: NEVER Use Inline Styles
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
**If a prop doesn't exist for a CSS property**, create it using `Box.extend()` in your boxExtends.ts file. Never fall back to inline styles.
|
|
32
|
-
|
|
33
|
-
### Common Style-to-Prop Conversions
|
|
34
|
-
|
|
35
|
-
| Inline Style (WRONG) | Box Prop (CORRECT) |
|
|
36
|
-
| --------------------------------------------- | ------------------------------------- |
|
|
37
|
-
| `style={{ width: "100%" }}` | `width="fit"` |
|
|
38
|
-
| `style={{ width: "100vw" }}` | `width="fit-screen"` |
|
|
39
|
-
| `style={{ height: "100%" }}` | `height="fit"` |
|
|
40
|
-
| `style={{ height: "100vh" }}` | `height="fit-screen"` |
|
|
41
|
-
| `style={{ minHeight: "100vh" }}` | `minHeight="fit-screen"` |
|
|
42
|
-
| `style={{ maxWidth: "1200px" }}` | `maxWidth={300}` (300/4=75rem=1200px) |
|
|
43
|
-
| `style={{ alignItems: "center" }}` | `ai="center"` |
|
|
44
|
-
| `style={{ justifyContent: "space-between" }}` | `jc="between"` |
|
|
45
|
-
| `style={{ flexDirection: "column" }}` | `d="column"` |
|
|
46
|
-
| `style={{ pointerEvents: "none" }}` | `pointerEvents="none"` |
|
|
47
|
-
| `style={{ cursor: "pointer" }}` | `cursor="pointer"` |
|
|
48
|
-
| `style={{ overflow: "hidden" }}` | `overflow="hidden"` |
|
|
49
|
-
| `style={{ position: "relative" }}` | `position="relative"` |
|
|
50
|
-
| `style={{ zIndex: 10 }}` | `zIndex={10}` |
|
|
51
|
-
| `style={{ opacity: 0.5 }}` | `opacity={0.5}` |
|
|
52
|
-
|
|
53
|
-
### When a Prop Doesn't Exist
|
|
54
|
-
|
|
55
|
-
If you need a CSS property that doesn't have a Box prop, extend Box instead of using inline styles:
|
|
56
|
-
|
|
57
|
-
```tsx
|
|
58
|
-
// boxExtends.ts - Create new props for missing CSS properties
|
|
59
|
-
import Box from '@cronocode/react-box';
|
|
60
|
-
|
|
61
|
-
export const { extendedProps, extendedPropTypes } = Box.extend(
|
|
62
|
-
{}, // CSS variables (if needed)
|
|
63
|
-
{
|
|
64
|
-
// Add new props
|
|
65
|
-
aspectRatio: [
|
|
66
|
-
{
|
|
67
|
-
values: ['auto', '1/1', '16/9', '4/3', '3/2'] as const,
|
|
68
|
-
styleName: 'aspect-ratio',
|
|
69
|
-
valueFormat: (value) => value,
|
|
70
|
-
},
|
|
71
|
-
],
|
|
72
|
-
backdropBlur: [
|
|
73
|
-
{
|
|
74
|
-
values: ['none', 'sm', 'md', 'lg'] as const,
|
|
75
|
-
styleName: 'backdrop-filter',
|
|
76
|
-
valueFormat: (value) => {
|
|
77
|
-
const map = { none: 'none', sm: 'blur(4px)', md: 'blur(8px)', lg: 'blur(16px)' };
|
|
78
|
-
return map[value];
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
],
|
|
82
|
-
},
|
|
83
|
-
{}, // Extended existing props (if needed)
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
// Now use the new props
|
|
87
|
-
<Box aspectRatio="16/9" backdropBlur="md" />;
|
|
88
|
-
```
|
|
11
|
+
Always use Box props. If a prop doesn't exist, create it with `Box.extend()` (see Extension System).
|
|
12
|
+
|
|
13
|
+
| Inline Style (WRONG) | Box Prop (CORRECT) |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `style={{ width: "100%" }}` | `width="fit"` |
|
|
16
|
+
| `style={{ width: "100vw" }}` | `width="fit-screen"` |
|
|
17
|
+
| `style={{ height: "100%" }}` | `height="fit"` |
|
|
18
|
+
| `style={{ height: "100vh" }}` | `height="fit-screen"` |
|
|
19
|
+
| `style={{ minHeight: "100vh" }}` | `minHeight="fit-screen"` |
|
|
20
|
+
| `style={{ maxWidth: "1200px" }}` | `maxWidth={300}` (300/4=75rem=1200px) |
|
|
21
|
+
| `style={{ alignItems: "center" }}` | `ai="center"` |
|
|
22
|
+
| `style={{ justifyContent: "space-between" }}` | `jc="between"` |
|
|
23
|
+
| `style={{ flexDirection: "column" }}` | `d="column"` |
|
|
24
|
+
| `style={{ pointerEvents: "none" }}` | `pointerEvents="none"` |
|
|
25
|
+
| `style={{ cursor: "pointer" }}` | `cursor="pointer"` |
|
|
26
|
+
| `style={{ overflow: "hidden" }}` | `overflow="hidden"` |
|
|
27
|
+
| `style={{ position: "relative" }}` | `position="relative"` |
|
|
28
|
+
| `style={{ zIndex: 10 }}` | `zIndex={10}` |
|
|
29
|
+
| `style={{ opacity: 0.5 }}` | `opacity={0.5}` |
|
|
89
30
|
|
|
90
31
|
### Rule #2: ALWAYS Use Component Shortcuts
|
|
91
32
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
The `tag` prop should ONLY be used for rare HTML elements that don't have a component shortcut (e.g., `<Box tag="datalist">`). For all common elements, use the corresponding component.
|
|
33
|
+
NEVER use `<Box tag="...">` when a component exists. NEVER use `<Box display="flex/grid">`.
|
|
95
34
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
<Box display="grid" gridCols={3}>...</Box> // WRONG!
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
**CORRECT** - Using semantic components:
|
|
112
|
-
|
|
113
|
-
```tsx
|
|
114
|
-
// DO THIS INSTEAD
|
|
115
|
-
import { Img, Link, H1, Nav } from '@cronocode/react-box/components/semantics';
|
|
116
|
-
import Button from '@cronocode/react-box/components/button';
|
|
117
|
-
import Flex from '@cronocode/react-box/components/flex';
|
|
118
|
-
import Grid from '@cronocode/react-box/components/grid';
|
|
119
|
-
|
|
120
|
-
<Img props={{ src: logo, alt: "Logo" }} />
|
|
121
|
-
<Link props={{ href: "#" }}>Link</Link>
|
|
122
|
-
<Button onClick={...}>Click</Button>
|
|
123
|
-
<Flex gap={4}>...</Flex>
|
|
124
|
-
<Grid gridCols={3}>...</Grid>
|
|
125
|
-
<Nav>...</Nav>
|
|
126
|
-
<H1 fontSize={32}>Title</H1>
|
|
127
|
-
```
|
|
35
|
+
| Instead of... | Use... | Import from |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `<Box display="flex">` | `<Flex>` | `components/flex` |
|
|
38
|
+
| `<Box display="grid">` | `<Grid>` | `components/grid` |
|
|
39
|
+
| `<Box tag="button">` | `<Button>` | `components/button` |
|
|
40
|
+
| `<Box tag="input">` | `<Textbox>` | `components/textbox` |
|
|
41
|
+
| `<Box tag="textarea">` | `<Textarea>` | `components/textarea` |
|
|
42
|
+
| `<Box tag="a/img/label">` | `<Link>/<Img>/<Label>` | `components/semantics` |
|
|
43
|
+
| `<Box tag="h1/h2/h3/h4/h5/h6">` | `<H1>/<H2>/.../<H6>` | `components/semantics` |
|
|
44
|
+
| `<Box tag="p/span">` | `<P>/<Span>` | `components/semantics` |
|
|
45
|
+
| `<Box tag="nav/header/footer/main">` | `<Nav>/<Header>/<Footer>/<Main>` | `components/semantics` |
|
|
46
|
+
| `<Box tag="section/article/aside">` | `<Section>/<Article>/<Aside>` | `components/semantics` |
|
|
128
47
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
| Instead of... | Use... | Import from |
|
|
132
|
-
| ---------------------- | ------------ | ------------------------------------------- |
|
|
133
|
-
| `<Box display="flex">` | `<Flex>` | `@cronocode/react-box/components/flex` |
|
|
134
|
-
| `<Box display="grid">` | `<Grid>` | `@cronocode/react-box/components/grid` |
|
|
135
|
-
| `<Box tag="button">` | `<Button>` | `@cronocode/react-box/components/button` |
|
|
136
|
-
| `<Box tag="input">` | `<Textbox>` | `@cronocode/react-box/components/textbox` |
|
|
137
|
-
| `<Box tag="textarea">` | `<Textarea>` | `@cronocode/react-box/components/textarea` |
|
|
138
|
-
| `<Box tag="a">` | `<Link>` | `@cronocode/react-box/components/semantics` |
|
|
139
|
-
| `<Box tag="img">` | `<Img>` | `@cronocode/react-box/components/semantics` |
|
|
140
|
-
| `<Box tag="h1">` | `<H1>` | `@cronocode/react-box/components/semantics` |
|
|
141
|
-
| `<Box tag="h2">` | `<H2>` | `@cronocode/react-box/components/semantics` |
|
|
142
|
-
| `<Box tag="h3">` | `<H3>` | `@cronocode/react-box/components/semantics` |
|
|
143
|
-
| `<Box tag="p">` | `<P>` | `@cronocode/react-box/components/semantics` |
|
|
144
|
-
| `<Box tag="span">` | `<Span>` | `@cronocode/react-box/components/semantics` |
|
|
145
|
-
| `<Box tag="nav">` | `<Nav>` | `@cronocode/react-box/components/semantics` |
|
|
146
|
-
| `<Box tag="header">` | `<Header>` | `@cronocode/react-box/components/semantics` |
|
|
147
|
-
| `<Box tag="footer">` | `<Footer>` | `@cronocode/react-box/components/semantics` |
|
|
148
|
-
| `<Box tag="main">` | `<Main>` | `@cronocode/react-box/components/semantics` |
|
|
149
|
-
| `<Box tag="section">` | `<Section>` | `@cronocode/react-box/components/semantics` |
|
|
150
|
-
| `<Box tag="article">` | `<Article>` | `@cronocode/react-box/components/semantics` |
|
|
151
|
-
| `<Box tag="aside">` | `<Aside>` | `@cronocode/react-box/components/semantics` |
|
|
152
|
-
| `<Box tag="label">` | `<Label>` | `@cronocode/react-box/components/semantics` |
|
|
48
|
+
All imports from `@cronocode/react-box/components/...`. Semantics also export: `Mark`, `Figure`, `Figcaption`, `Details`, `Summary`, `Menu`, `Time`.
|
|
153
49
|
|
|
154
50
|
---
|
|
155
51
|
|
|
156
|
-
##
|
|
52
|
+
## Numeric Value Formatters
|
|
157
53
|
|
|
158
|
-
|
|
54
|
+
**#1 source of confusion.** Different props have different dividers:
|
|
159
55
|
|
|
160
|
-
|
|
56
|
+
| Prop Category | Divider | Example | CSS Output |
|
|
57
|
+
|---|---|---|---|
|
|
58
|
+
| Spacing (`p`, `m`, `gap`, `px`, `py`, `mx`, `my`, etc.) | 4 | `p={4}` | `padding: 1rem` (16px) |
|
|
59
|
+
| Font size (`fontSize`) | **16** | `fontSize={14}` | `font-size: 0.875rem` (14px) |
|
|
60
|
+
| Width/Height (numeric) | 4 | `width={20}` | `width: 5rem` (80px) |
|
|
61
|
+
| Border width (`b`, `bx`, `by`, `bt`, `br`, `bb`, `bl`) | none | `b={1}` | `border-width: 1px` |
|
|
62
|
+
| Border radius (`borderRadius`) | none | `borderRadius={8}` | `border-radius: 8px` |
|
|
63
|
+
| Line height (`lineHeight`) | none | `lineHeight={24}` | `line-height: 24px` |
|
|
161
64
|
|
|
162
65
|
```tsx
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
//
|
|
166
|
-
<Box p={4} bgColor="blue-500" color="white">Content</Box>
|
|
167
|
-
|
|
168
|
-
// Renders as different elements
|
|
169
|
-
<Box tag="button" p={3}>Button</Box>
|
|
170
|
-
<Box tag="a" props={{ href: '/link' }}>Link</Box>
|
|
66
|
+
// fontSize: divider 16 → value maps directly to px
|
|
67
|
+
fontSize={12} // 12px fontSize={14} // 14px fontSize={16} // 16px
|
|
68
|
+
fontSize={18} // 18px fontSize={24} // 24px fontSize={32} // 32px
|
|
171
69
|
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
<Button p={3}>Button</Button>
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
---
|
|
178
|
-
|
|
179
|
-
## Critical: Numeric Value Formatters
|
|
180
|
-
|
|
181
|
-
**This is the #1 source of confusion.** Different props have different dividers:
|
|
182
|
-
|
|
183
|
-
| Prop Category | Divider | Formula | Example | CSS Output |
|
|
184
|
-
| ------------------------------------------------------- | ------- | ------------ | ------------------ | ------------------------ |
|
|
185
|
-
| Spacing (`p`, `m`, `gap`, `px`, `py`, `mx`, `my`, etc.) | 4 | value/4 rem | `p={4}` | `padding: 1rem` (16px) |
|
|
186
|
-
| Font size (`fontSize`) | **16** | value/16 rem | `fontSize={16}` | `font-size: 1rem` (16px) |
|
|
187
|
-
| Width/Height (numeric) | 4 | value/4 rem | `width={20}` | `width: 5rem` (80px) |
|
|
188
|
-
| Border width (`b`, `bx`, `by`, `bt`, `br`, `bb`, `bl`) | none | direct px | `b={1}` | `border-width: 1px` |
|
|
189
|
-
| Border radius (`borderRadius`) | none | direct px | `borderRadius={8}` | `border-radius: 8px` |
|
|
190
|
-
|
|
191
|
-
### Common fontSize Values
|
|
192
|
-
|
|
193
|
-
```tsx
|
|
194
|
-
fontSize={12} // → 0.75rem ≈ 12px (small)
|
|
195
|
-
fontSize={14} // → 0.875rem ≈ 14px (body)
|
|
196
|
-
fontSize={16} // → 1rem = 16px (default)
|
|
197
|
-
fontSize={18} // → 1.125rem ≈ 18px (large)
|
|
198
|
-
fontSize={24} // → 1.5rem = 24px (h2)
|
|
199
|
-
fontSize={32} // → 2rem = 32px (h1)
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
### Common Spacing Values (divider = 4)
|
|
203
|
-
|
|
204
|
-
```tsx
|
|
205
|
-
p={1} // → 0.25rem = 4px
|
|
206
|
-
p={2} // → 0.5rem = 8px
|
|
207
|
-
p={3} // → 0.75rem = 12px
|
|
208
|
-
p={4} // → 1rem = 16px
|
|
209
|
-
p={6} // → 1.5rem = 24px
|
|
210
|
-
p={8} // → 2rem = 32px
|
|
70
|
+
// Spacing: divider 4 → value/4 = rem
|
|
71
|
+
p={1} // 4px p={2} // 8px p={3} // 12px p={4} // 16px p={6} // 24px p={8} // 32px
|
|
211
72
|
```
|
|
212
73
|
|
|
213
74
|
---
|
|
@@ -216,375 +77,116 @@ p={8} // → 2rem = 32px
|
|
|
216
77
|
|
|
217
78
|
### Spacing
|
|
218
79
|
|
|
219
|
-
| Prop
|
|
220
|
-
|
|
221
|
-
| `p`
|
|
222
|
-
| `
|
|
223
|
-
| `
|
|
224
|
-
| `pt`, `pr`, `pb`, `pl` | padding-top/right/bottom/left |
|
|
225
|
-
| `m` | margin |
|
|
226
|
-
| `mx`, `my` | margin horizontal/vertical |
|
|
227
|
-
| `mt`, `mr`, `mb`, `ml` | margin-top/right/bottom/left |
|
|
228
|
-
| `gap` | gap (flexbox/grid) |
|
|
80
|
+
| Prop | CSS Property |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `p` / `px` / `py` / `pt` / `pr` / `pb` / `pl` | padding (all / horizontal / vertical / individual) |
|
|
83
|
+
| `m` / `mx` / `my` / `mt` / `mr` / `mb` / `ml` | margin (all / horizontal / vertical / individual) |
|
|
84
|
+
| `gap` | gap (flexbox/grid) |
|
|
229
85
|
|
|
230
86
|
### Layout
|
|
231
87
|
|
|
232
|
-
| Prop
|
|
233
|
-
|
|
234
|
-
| `display` | display
|
|
235
|
-
| `d`
|
|
236
|
-
| `wrap`
|
|
237
|
-
| `ai`
|
|
238
|
-
| `jc`
|
|
239
|
-
| `flex`
|
|
240
|
-
| `grow` | flex-grow | number |
|
|
241
|
-
| `shrink` | flex-shrink | number |
|
|
88
|
+
| Prop | CSS Property | Values |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| `display` | display | `'flex'`, `'block'`, `'inline'`, `'grid'`, `'none'`, `'inline-flex'`, etc. |
|
|
91
|
+
| `d` | flex-direction | `'row'`, `'column'`, `'row-reverse'`, `'column-reverse'` |
|
|
92
|
+
| `wrap` | flex-wrap | `'wrap'`, `'nowrap'`, `'wrap-reverse'` |
|
|
93
|
+
| `ai` | align-items | `'center'`, `'start'`, `'end'`, `'stretch'`, `'baseline'` |
|
|
94
|
+
| `jc` | justify-content | `'center'`, `'start'`, `'end'`, `'between'`, `'around'`, `'evenly'` |
|
|
95
|
+
| `flex` / `grow` / `shrink` | flex / flex-grow / flex-shrink | number or string |
|
|
242
96
|
|
|
243
97
|
### Sizing
|
|
244
98
|
|
|
245
|
-
| Prop
|
|
246
|
-
|
|
247
|
-
| `width`
|
|
248
|
-
| `
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
// Positioning
|
|
273
|
-
top="10%" // → top: 10%
|
|
274
|
-
left="50%" // → left: 50%
|
|
275
|
-
right="0%" // → right: 0%
|
|
276
|
-
bottom="20%" // → bottom: 20%
|
|
277
|
-
|
|
278
|
-
// Fraction shortcuts (width/height only)
|
|
279
|
-
width="1/2" // → 50%
|
|
280
|
-
width="1/3" // → 33.333%
|
|
281
|
-
width="2/3" // → 66.666%
|
|
282
|
-
width="1/4" // → 25%
|
|
283
|
-
width="3/4" // → 75%
|
|
284
|
-
|
|
285
|
-
// Special values
|
|
286
|
-
width="fit" // → 100%
|
|
287
|
-
width="fit-screen" // → 100vw
|
|
288
|
-
height="fit" // → 100%
|
|
289
|
-
height="fit-screen"// → 100vh
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
### Colors (Tailwind-like palette)
|
|
293
|
-
|
|
294
|
-
| Prop | CSS Property |
|
|
295
|
-
| ------------- | ---------------- |
|
|
296
|
-
| `bgColor` | background-color |
|
|
297
|
-
| `color` | color |
|
|
298
|
-
| `borderColor` | border-color |
|
|
299
|
-
|
|
300
|
-
**Color values**: `'gray-50'` through `'gray-900'`, same for `red`, `orange`, `yellow`, `green`, `teal`, `blue`, `indigo`, `purple`, `pink`, `violet`.
|
|
301
|
-
|
|
302
|
-
Also: `'white'`, `'black'`, `'transparent'`, `'inherit'`, `'currentColor'`
|
|
303
|
-
|
|
304
|
-
### Borders
|
|
305
|
-
|
|
306
|
-
| Prop | CSS Property |
|
|
307
|
-
| ---------------------- | ---------------------------------------------------------- |
|
|
308
|
-
| `b` | border-width (all sides) |
|
|
309
|
-
| `bx` | border-left-width + border-right-width |
|
|
310
|
-
| `by` | border-top-width + border-bottom-width |
|
|
311
|
-
| `bt`, `br`, `bb`, `bl` | individual sides |
|
|
312
|
-
| `borderRadius` | border-radius |
|
|
313
|
-
| `borderStyle` | border-style (`'solid'`, `'dashed'`, `'dotted'`, `'none'`) |
|
|
314
|
-
|
|
315
|
-
### Typography
|
|
316
|
-
|
|
317
|
-
| Prop | CSS Property |
|
|
318
|
-
| ---------------- | ------------------------------------------------------------ |
|
|
319
|
-
| `fontSize` | font-size (divider: 16) |
|
|
320
|
-
| `fontWeight` | font-weight (`400`, `500`, `600`, `700`, etc.) |
|
|
321
|
-
| `lineHeight` | line-height (number = pixels, e.g. `lineHeight={24}` → 24px) |
|
|
322
|
-
| `textAlign` | text-align |
|
|
323
|
-
| `textDecoration` | text-decoration |
|
|
324
|
-
| `textTransform` | text-transform |
|
|
325
|
-
| `whiteSpace` | white-space |
|
|
326
|
-
| `overflow` | overflow |
|
|
327
|
-
| `textOverflow` | text-overflow |
|
|
328
|
-
|
|
329
|
-
### Positioning
|
|
330
|
-
|
|
331
|
-
| Prop | CSS Property |
|
|
332
|
-
| -------------------------------- | ------------------------------------------------------------ |
|
|
333
|
-
| `position` | position (`'relative'`, `'absolute'`, `'fixed'`, `'sticky'`) |
|
|
334
|
-
| `top`, `right`, `bottom`, `left` | positioning offsets |
|
|
335
|
-
| `zIndex` | z-index |
|
|
336
|
-
|
|
337
|
-
### Effects
|
|
338
|
-
|
|
339
|
-
| Prop | CSS Property |
|
|
340
|
-
| --------------- | --------------------------------------------------------------- |
|
|
341
|
-
| `shadow` | box-shadow (`'small'`, `'medium'`, `'large'`, `'xl'`, `'none'`) |
|
|
342
|
-
| `opacity` | opacity |
|
|
343
|
-
| `cursor` | cursor |
|
|
344
|
-
| `pointerEvents` | pointer-events |
|
|
345
|
-
| `transition` | transition |
|
|
346
|
-
| `transform` | transform |
|
|
99
|
+
| Prop | CSS Property | Accepts |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| `width` / `height` | width / height | number (rem/4), `'auto'`, `'fit'` (100%), `'fit-screen'` (100vw/vh), fractions (`'1/2'`, `'1/3'`, `'2/3'`, `'1/4'`, `'3/4'`), percentages (`'33%'`) |
|
|
102
|
+
| `minWidth` / `maxWidth` / `minHeight` / `maxHeight` | min/max sizing | number or string |
|
|
103
|
+
|
|
104
|
+
All sizing, spacing, and positioning props also accept percentage strings: `p="5%"`, `top="10%"`, `gap="2%"`.
|
|
105
|
+
|
|
106
|
+
### Visual
|
|
107
|
+
|
|
108
|
+
| Prop | CSS Property | Notes |
|
|
109
|
+
|---|---|---|
|
|
110
|
+
| `bgColor` / `color` / `borderColor` | background-color / color / border-color | Tailwind palette: `'gray-50'`..`'gray-900'`, same for red/orange/yellow/green/teal/blue/indigo/purple/pink/violet. Also `'white'`, `'black'`, `'transparent'`, `'currentColor'` |
|
|
111
|
+
| `b` / `bx` / `by` / `bt` / `br` / `bb` / `bl` | border-width | direct px |
|
|
112
|
+
| `borderRadius` | border-radius | direct px |
|
|
113
|
+
| `borderStyle` | border-style | `'solid'`, `'dashed'`, `'dotted'`, `'none'` |
|
|
114
|
+
| `fontSize` | font-size | divider 16 |
|
|
115
|
+
| `fontWeight` | font-weight | `400`, `500`, `600`, `700`, etc. |
|
|
116
|
+
| `lineHeight` | line-height | direct px |
|
|
117
|
+
| `textAlign` / `textDecoration` / `textTransform` / `whiteSpace` / `textOverflow` | text properties | string values |
|
|
118
|
+
| `overflow` | overflow | `'hidden'`, `'auto'`, `'scroll'`, `'visible'` |
|
|
119
|
+
| `position` | position | `'relative'`, `'absolute'`, `'fixed'`, `'sticky'` |
|
|
120
|
+
| `top` / `right` / `bottom` / `left` / `inset` | positioning offsets | number or string |
|
|
121
|
+
| `zIndex` | z-index | number |
|
|
122
|
+
| `shadow` | box-shadow | `'small'`, `'medium'`, `'large'`, `'xl'`, `'none'` |
|
|
123
|
+
| `opacity` | opacity | number |
|
|
124
|
+
| `cursor` / `pointerEvents` / `transition` / `transform` / `userSelect` | misc | string values |
|
|
347
125
|
|
|
348
126
|
---
|
|
349
127
|
|
|
350
|
-
## Pseudo-Classes
|
|
351
|
-
|
|
352
|
-
Apply styles on interaction states:
|
|
128
|
+
## Pseudo-Classes, Breakpoints & Themes
|
|
353
129
|
|
|
354
130
|
```tsx
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
focus={{ outline: 'none', ring: 2 }}
|
|
359
|
-
active={{ bgColor: 'blue-700' }}
|
|
360
|
-
disabled={{ opacity: 0.5, cursor: 'not-allowed' }}
|
|
361
|
-
/>
|
|
362
|
-
```
|
|
131
|
+
// Pseudo-classes: hover, focus, active, disabled, checked, indeterminate, required, selected,
|
|
132
|
+
// focusWithin, focusVisible, first, last, even, odd, empty
|
|
133
|
+
<Box bgColor="blue-500" hover={{ bgColor: 'blue-600' }} disabled={{ opacity: 0.5 }} />
|
|
363
134
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
---
|
|
135
|
+
// Responsive breakpoints (mobile-first): sm(640) md(768) lg(1024) xl(1280) xxl(1536)
|
|
136
|
+
<Box p={2} md={{ p: 4 }} lg={{ p: 6 }} />
|
|
367
137
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
Mobile-first breakpoints using min-width media queries:
|
|
371
|
-
|
|
372
|
-
```tsx
|
|
373
|
-
<Box
|
|
374
|
-
p={2} // Base (mobile)
|
|
375
|
-
sm={{ p: 3 }} // ≥640px
|
|
376
|
-
md={{ p: 4 }} // ≥768px
|
|
377
|
-
lg={{ p: 6 }} // ≥1024px
|
|
378
|
-
xl={{ p: 8 }} // ≥1280px
|
|
379
|
-
xxl={{ p: 10 }} // ≥1536px
|
|
380
|
-
/>
|
|
138
|
+
// Combine: breakpoints can nest pseudo-classes
|
|
139
|
+
<Box bgColor="white" hover={{ bgColor: 'gray-100' }} md={{ bgColor: 'gray-50', hover: { bgColor: 'gray-200' } }} />
|
|
381
140
|
```
|
|
382
141
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
```tsx
|
|
386
|
-
<Box
|
|
387
|
-
bgColor="white"
|
|
388
|
-
hover={{ bgColor: 'gray-100' }}
|
|
389
|
-
md={{
|
|
390
|
-
bgColor: 'gray-50',
|
|
391
|
-
hover: { bgColor: 'gray-200' },
|
|
392
|
-
}}
|
|
393
|
-
/>
|
|
394
|
-
```
|
|
395
|
-
|
|
396
|
-
---
|
|
397
|
-
|
|
398
|
-
## Theme System
|
|
399
|
-
|
|
400
|
-
### Setting Up Themes
|
|
401
|
-
|
|
402
|
-
The `Box.Theme` component provides theme management with automatic detection based on system preferences.
|
|
403
|
-
|
|
404
|
-
**Auto-detect theme** (recommended):
|
|
405
|
-
|
|
406
|
-
```tsx
|
|
407
|
-
import Box from '@cronocode/react-box';
|
|
408
|
-
|
|
409
|
-
function App() {
|
|
410
|
-
return (
|
|
411
|
-
<Box.Theme>
|
|
412
|
-
{/* Theme auto-detects from prefers-color-scheme: 'light' or 'dark' */}
|
|
413
|
-
<YourApp />
|
|
414
|
-
</Box.Theme>
|
|
415
|
-
);
|
|
416
|
-
}
|
|
417
|
-
```
|
|
418
|
-
|
|
419
|
-
**Explicit theme**:
|
|
420
|
-
|
|
421
|
-
```tsx
|
|
422
|
-
<Box.Theme theme="dark">
|
|
423
|
-
<YourApp />
|
|
424
|
-
</Box.Theme>
|
|
425
|
-
```
|
|
426
|
-
|
|
427
|
-
**Global vs Local theme**:
|
|
428
|
-
|
|
429
|
-
```tsx
|
|
430
|
-
// Global: Adds theme class to document.documentElement (affects entire page)
|
|
431
|
-
<Box.Theme theme="dark" use="global">
|
|
432
|
-
<YourApp />
|
|
433
|
-
</Box.Theme>
|
|
434
|
-
|
|
435
|
-
// Local (default): Wraps children in a Box with theme class (scoped)
|
|
436
|
-
<Box.Theme theme="dark" use="local">
|
|
437
|
-
<Section>Content</Section>
|
|
438
|
-
</Box.Theme>
|
|
439
|
-
```
|
|
440
|
-
|
|
441
|
-
### Using the Theme Hook
|
|
142
|
+
### Theme System
|
|
442
143
|
|
|
443
144
|
```tsx
|
|
444
145
|
import Box from '@cronocode/react-box';
|
|
445
146
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
// Must be used within Box.Theme provider
|
|
453
|
-
function App() {
|
|
454
|
-
return (
|
|
455
|
-
<Box.Theme>
|
|
456
|
-
<ThemeSwitcher />
|
|
457
|
-
</Box.Theme>
|
|
458
|
-
);
|
|
459
|
-
}
|
|
460
|
-
```
|
|
461
|
-
|
|
462
|
-
### Theme-Aware Styles
|
|
463
|
-
|
|
464
|
-
Apply styles based on the active theme:
|
|
465
|
-
|
|
466
|
-
```tsx
|
|
467
|
-
<Box
|
|
468
|
-
bgColor="white"
|
|
469
|
-
color="gray-900"
|
|
470
|
-
theme={{
|
|
471
|
-
dark: {
|
|
472
|
-
bgColor: 'gray-900',
|
|
473
|
-
color: 'gray-100',
|
|
474
|
-
},
|
|
475
|
-
}}
|
|
476
|
-
/>
|
|
477
|
-
```
|
|
147
|
+
// Setup: wrap app in Box.Theme (auto-detects via prefers-color-scheme)
|
|
148
|
+
<Box.Theme> {/* auto light/dark detection */}
|
|
149
|
+
<Box.Theme theme="dark"> {/* explicit theme, ignores system pref */}
|
|
150
|
+
<Box.Theme theme="dark" use="global"> {/* applies class + data-theme to <html> */}
|
|
151
|
+
<Box.Theme storageKey="app-theme"> {/* persists user choice to localStorage */}
|
|
478
152
|
|
|
479
|
-
|
|
153
|
+
// Hook: read + set theme programmatically (must be within Box.Theme)
|
|
154
|
+
const [theme, setTheme] = Box.useTheme();
|
|
155
|
+
setTheme('dark'); // set explicit theme (persists to localStorage if storageKey provided)
|
|
156
|
+
setTheme(null); // reset to system auto-detection (clears localStorage)
|
|
480
157
|
|
|
481
|
-
|
|
158
|
+
// Supports any custom theme name — not limited to 'light'/'dark'
|
|
159
|
+
<Box.Theme theme="high-contrast">
|
|
482
160
|
|
|
483
|
-
|
|
161
|
+
// Theme-aware styles — nests with pseudo-classes and breakpoints
|
|
484
162
|
<Box
|
|
485
|
-
bgColor="white"
|
|
163
|
+
bgColor="white" color="gray-900"
|
|
486
164
|
hover={{ bgColor: 'gray-100' }}
|
|
487
|
-
theme={{
|
|
488
|
-
dark: {
|
|
489
|
-
bgColor: 'gray-800',
|
|
490
|
-
hover: { bgColor: 'gray-700' },
|
|
491
|
-
},
|
|
492
|
-
}}
|
|
165
|
+
theme={{ dark: { bgColor: 'gray-900', color: 'gray-100', hover: { bgColor: 'gray-700' } } }}
|
|
493
166
|
/>
|
|
494
167
|
```
|
|
495
168
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
Combine all three systems:
|
|
499
|
-
|
|
500
|
-
```tsx
|
|
501
|
-
<Box
|
|
502
|
-
p={4}
|
|
503
|
-
bgColor="white"
|
|
504
|
-
md={{
|
|
505
|
-
p: 6,
|
|
506
|
-
bgColor: 'gray-50',
|
|
507
|
-
}}
|
|
508
|
-
theme={{
|
|
509
|
-
dark: {
|
|
510
|
-
bgColor: 'gray-900',
|
|
511
|
-
md: {
|
|
512
|
-
bgColor: 'gray-800',
|
|
513
|
-
},
|
|
514
|
-
},
|
|
515
|
-
}}
|
|
516
|
-
/>
|
|
517
|
-
```
|
|
169
|
+
**Props**: `theme?` (string — explicit theme name), `use?` (`'global'`|`'local'`, default `'local'`), `storageKey?` (string — localStorage key for persistence).
|
|
170
|
+
**DOM**: Sets `data-theme` attribute and theme class on wrapper (local) or `document.documentElement` (global). Cleaned up on unmount.
|
|
518
171
|
|
|
519
172
|
---
|
|
520
173
|
|
|
521
174
|
## Component System
|
|
522
175
|
|
|
523
|
-
### Using Built-in Components
|
|
524
|
-
|
|
525
|
-
```tsx
|
|
526
|
-
<Box component="button" variant="primary">Click me</Box>
|
|
527
|
-
<Box component="button.icon" />
|
|
528
|
-
```
|
|
529
|
-
|
|
530
|
-
### Pre-built Components
|
|
531
|
-
|
|
532
|
-
Import ready-to-use components:
|
|
533
|
-
|
|
534
176
|
```tsx
|
|
535
|
-
//
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
import Dropdown from '@cronocode/react-box/components/dropdown';
|
|
541
|
-
import Tooltip from '@cronocode/react-box/components/tooltip';
|
|
542
|
-
|
|
543
|
-
// Layout
|
|
544
|
-
import Flex from '@cronocode/react-box/components/flex'; // Box with display="flex" (supports inline prop)
|
|
545
|
-
import Grid from '@cronocode/react-box/components/grid'; // Box with display="grid" (supports inline prop)
|
|
546
|
-
|
|
547
|
-
<Button variant="primary">Submit</Button>
|
|
548
|
-
<Textbox placeholder="Enter text..." />
|
|
549
|
-
<Flex gap={4} ai="center">...</Flex>
|
|
550
|
-
<Flex inline gap={2}>Inline flex</Flex>
|
|
551
|
-
```
|
|
552
|
-
|
|
553
|
-
### Semantic HTML Components
|
|
554
|
-
|
|
555
|
-
Alias components for semantic HTML elements (from `components/semantics`):
|
|
556
|
-
|
|
557
|
-
```tsx
|
|
558
|
-
import {
|
|
559
|
-
// Typography
|
|
560
|
-
P, H1, H2, H3, H4, H5, H6, Span, Mark,
|
|
561
|
-
// Structure
|
|
562
|
-
Header, Footer, Main, Nav, Section, Article, Aside,
|
|
563
|
-
// Other
|
|
564
|
-
Label, Link, Img, Figure, Figcaption, Details, Summary, Menu, Time,
|
|
565
|
-
} from '@cronocode/react-box/components/semantics';
|
|
566
|
-
|
|
567
|
-
// These are Box aliases with the correct HTML tag
|
|
568
|
-
<H1 fontSize={32} fontWeight={700}>Title</H1>
|
|
569
|
-
<P color="gray-600">Paragraph text</P>
|
|
570
|
-
<Link props={{ href: '/about' }} color="blue-500">About</Link>
|
|
571
|
-
<Flex tag="nav" gap={4}>...</Flex>
|
|
177
|
+
// Component + variant props apply registered styles from Box.components()
|
|
178
|
+
<Box component="card" variant="bordered">
|
|
179
|
+
<Box component="card.header">Title</Box>
|
|
180
|
+
<Box component="card.body">Content</Box>
|
|
181
|
+
</Box>
|
|
572
182
|
```
|
|
573
183
|
|
|
574
|
-
###
|
|
184
|
+
### Box.components() — Define Custom Component Styles
|
|
575
185
|
|
|
576
186
|
```tsx
|
|
577
|
-
// Define custom component styles
|
|
578
187
|
Box.components({
|
|
579
188
|
card: {
|
|
580
|
-
styles: {
|
|
581
|
-
display: 'flex',
|
|
582
|
-
d: 'column',
|
|
583
|
-
p: 4,
|
|
584
|
-
bgColor: 'white',
|
|
585
|
-
borderRadius: 8,
|
|
586
|
-
shadow: 'medium',
|
|
587
|
-
},
|
|
189
|
+
styles: { display: 'flex', d: 'column', p: 4, bgColor: 'white', borderRadius: 8, shadow: 'medium' },
|
|
588
190
|
variants: {
|
|
589
191
|
bordered: { b: 1, borderColor: 'gray-200', shadow: 'none' },
|
|
590
192
|
elevated: { shadow: 'large' },
|
|
@@ -595,148 +197,78 @@ Box.components({
|
|
|
595
197
|
},
|
|
596
198
|
},
|
|
597
199
|
});
|
|
598
|
-
|
|
599
|
-
// Use it
|
|
600
|
-
<Box component="card" variant="bordered">
|
|
601
|
-
<Box component="card.header">Title</Box>
|
|
602
|
-
<Box component="card.body">Content</Box>
|
|
603
|
-
</Box>;
|
|
604
200
|
```
|
|
605
201
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
Components can inherit from other components using the `extends` property. The base component's full style tree (styles, variants, children) is used as the foundation, and the extending component's definitions are deep-merged on top:
|
|
202
|
+
### Component Inheritance with `extends`
|
|
609
203
|
|
|
610
204
|
```tsx
|
|
611
205
|
Box.components({
|
|
612
206
|
subgrid: {
|
|
613
|
-
extends: 'datagrid', //
|
|
207
|
+
extends: 'datagrid', // inherits all styles, variants, and children; deep-merges overrides
|
|
614
208
|
styles: { b: 0, borderRadius: 0, shadow: 'none' },
|
|
615
|
-
children: {
|
|
616
|
-
header: { children: { cell: { styles: { fontSize: 12 } } } },
|
|
617
|
-
},
|
|
209
|
+
children: { header: { children: { cell: { styles: { fontSize: 12 } } } } },
|
|
618
210
|
},
|
|
619
211
|
});
|
|
620
212
|
```
|
|
621
213
|
|
|
622
|
-
This is especially useful for DataGrid, where the internal style tree is complex (pinning, sticky headers, hover groups, etc.). With `extends`, you only need to declare overrides.
|
|
623
|
-
|
|
624
214
|
---
|
|
625
215
|
|
|
626
216
|
## Extension System
|
|
627
217
|
|
|
628
|
-
###
|
|
218
|
+
### Box.extend() — Add New Props, Colors, Variables
|
|
629
219
|
|
|
630
220
|
```tsx
|
|
631
221
|
import Box from '@cronocode/react-box';
|
|
632
222
|
|
|
633
223
|
export const { extendedProps, extendedPropTypes } = Box.extend(
|
|
634
|
-
//
|
|
635
|
-
{
|
|
636
|
-
|
|
637
|
-
|
|
224
|
+
{ 'brand-primary': '#ff6600', 'brand-secondary': '#0066ff' }, // CSS variables
|
|
225
|
+
{ // New props
|
|
226
|
+
aspectRatio: [{
|
|
227
|
+
values: ['auto', '1/1', '16/9', '4/3'] as const,
|
|
228
|
+
styleName: 'aspect-ratio',
|
|
229
|
+
valueFormat: (value) => value,
|
|
230
|
+
}],
|
|
638
231
|
},
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
},
|
|
651
|
-
],
|
|
652
|
-
color: [
|
|
653
|
-
{
|
|
654
|
-
values: ['brand-primary', 'brand-secondary'] as const,
|
|
655
|
-
styleName: 'color',
|
|
656
|
-
valueFormat: (value, getVariable) => getVariable(value),
|
|
657
|
-
},
|
|
658
|
-
],
|
|
232
|
+
{ // Extend existing props with new values
|
|
233
|
+
bgColor: [{
|
|
234
|
+
values: ['brand-primary', 'brand-secondary'] as const,
|
|
235
|
+
styleName: 'background-color',
|
|
236
|
+
valueFormat: (value, getVariable) => getVariable(value),
|
|
237
|
+
}],
|
|
238
|
+
color: [{
|
|
239
|
+
values: ['brand-primary', 'brand-secondary'] as const,
|
|
240
|
+
styleName: 'color',
|
|
241
|
+
valueFormat: (value, getVariable) => getVariable(value),
|
|
242
|
+
}],
|
|
659
243
|
},
|
|
660
244
|
);
|
|
661
|
-
|
|
662
|
-
// Now use your custom colors
|
|
663
|
-
<Box bgColor="brand-primary" color="white">
|
|
664
|
-
Branded
|
|
665
|
-
</Box>;
|
|
666
245
|
```
|
|
667
246
|
|
|
668
|
-
### Per-Property Values (Multi-styleName
|
|
247
|
+
### Per-Property Values (Multi-styleName)
|
|
669
248
|
|
|
670
|
-
When
|
|
249
|
+
When `styleName` is an array, `valueFormat` is called once per CSS property with `styleName` as the third argument. Use for typography presets or any design token spanning multiple CSS properties:
|
|
671
250
|
|
|
672
251
|
```tsx
|
|
673
|
-
const textStyleNames = ['display-lg', 'display-sm'] as const;
|
|
674
|
-
|
|
675
252
|
Box.extend(
|
|
676
|
-
{
|
|
677
|
-
'text-display-lg-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
'text-display-sm-letter-spacing': '-0.015em',
|
|
685
|
-
},
|
|
686
|
-
{
|
|
687
|
-
textStyle: [
|
|
688
|
-
{
|
|
689
|
-
values: textStyleNames,
|
|
690
|
-
styleName: ['font-size', 'font-weight', 'line-height', 'letter-spacing'],
|
|
691
|
-
valueFormat: (value, getVariable, styleName) => {
|
|
692
|
-
const suffixMap: Record<string, string> = {
|
|
693
|
-
'font-size': 'size',
|
|
694
|
-
'font-weight': 'weight',
|
|
695
|
-
'line-height': 'line-height',
|
|
696
|
-
'letter-spacing': 'letter-spacing',
|
|
697
|
-
};
|
|
698
|
-
return getVariable(`text-${value}-${suffixMap[styleName!]}`);
|
|
699
|
-
},
|
|
253
|
+
{ 'text-display-lg-size': '36px', 'text-display-lg-weight': '700',
|
|
254
|
+
'text-display-lg-line-height': '1.2', 'text-display-lg-letter-spacing': '-0.02em' },
|
|
255
|
+
{ textStyle: [{
|
|
256
|
+
values: ['display-lg', 'display-sm'] as const,
|
|
257
|
+
styleName: ['font-size', 'font-weight', 'line-height', 'letter-spacing'],
|
|
258
|
+
valueFormat: (value, getVariable, styleName) => {
|
|
259
|
+
const suffix = { 'font-size': 'size', 'font-weight': 'weight', 'line-height': 'line-height', 'letter-spacing': 'letter-spacing' };
|
|
260
|
+
return getVariable(`text-${value}-${suffix[styleName!]}`);
|
|
700
261
|
},
|
|
701
|
-
|
|
702
|
-
},
|
|
262
|
+
}] },
|
|
703
263
|
{},
|
|
704
264
|
);
|
|
705
|
-
|
|
706
|
-
// Usage — one prop sets four CSS properties with different values
|
|
707
|
-
<Box textStyle="display-lg" />
|
|
708
|
-
// Generates: font-size:var(--text-display-lg-size); font-weight:var(--text-display-lg-weight);
|
|
709
|
-
// line-height:var(--text-display-lg-line-height); letter-spacing:var(--text-display-lg-letter-spacing)
|
|
265
|
+
// <Box textStyle="display-lg" /> → sets all 4 CSS properties
|
|
710
266
|
```
|
|
711
267
|
|
|
712
|
-
**Note**: The `styleName` parameter is optional and only available on string-valued `BoxStyle` definitions (the ones with `getVariableValue` as the second parameter). For single `styleName` props, `valueFormat` still works exactly as before.
|
|
713
|
-
|
|
714
|
-
**When to use style grouping**: Always use this feature via `Box.extend()` when multiple CSS properties logically belong together and should always be applied as a unit. Typography presets (font-size + font-weight + line-height + letter-spacing), spacing scales, or any design token that spans multiple CSS properties should be defined as a grouped prop rather than relying on developers to set each property individually. This ensures consistency — the grouped properties can't drift apart — and reduces the number of props on each component. When building a project with react-box, define grouped props in your `Box.extend()` setup for any set of styles that always travel together.
|
|
715
|
-
|
|
716
268
|
### TypeScript Type Augmentation
|
|
717
269
|
|
|
718
|
-
**Manual approach** (simple cases):
|
|
719
|
-
|
|
720
270
|
```typescript
|
|
721
|
-
// types.d.ts
|
|
722
|
-
import '@cronocode/react-box/types';
|
|
723
|
-
|
|
724
|
-
declare module '@cronocode/react-box/types' {
|
|
725
|
-
namespace Augmented {
|
|
726
|
-
interface BoxPropTypes {
|
|
727
|
-
bgColor: 'brand-primary' | 'brand-secondary';
|
|
728
|
-
}
|
|
729
|
-
interface ComponentsTypes {
|
|
730
|
-
card: 'bordered' | 'elevated';
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
```
|
|
735
|
-
|
|
736
|
-
**Generic approach** (recommended - auto-extracts types from your definitions):
|
|
737
|
-
|
|
738
|
-
```typescript
|
|
739
|
-
// types.d.ts
|
|
271
|
+
// types.d.ts — Generic approach (recommended)
|
|
740
272
|
import { ExtractComponentsAndVariants, ExtractBoxStyles } from '@cronocode/react-box/types';
|
|
741
273
|
import { components } from './boxComponents';
|
|
742
274
|
import { extendedPropTypes, extendedProps } from './boxExtends';
|
|
@@ -748,201 +280,103 @@ declare module '@cronocode/react-box/types' {
|
|
|
748
280
|
interface ComponentsTypes extends ExtractComponentsAndVariants<typeof components> {}
|
|
749
281
|
}
|
|
750
282
|
}
|
|
283
|
+
|
|
284
|
+
// Manual approach (simple cases):
|
|
285
|
+
declare module '@cronocode/react-box/types' {
|
|
286
|
+
namespace Augmented {
|
|
287
|
+
interface BoxPropTypes { bgColor: 'brand-primary' | 'brand-secondary' }
|
|
288
|
+
interface ComponentsTypes { card: 'bordered' | 'elevated' }
|
|
289
|
+
}
|
|
290
|
+
}
|
|
751
291
|
```
|
|
752
292
|
|
|
753
293
|
---
|
|
754
294
|
|
|
755
295
|
## Common Patterns
|
|
756
296
|
|
|
757
|
-
### Flex Container
|
|
758
|
-
|
|
759
297
|
```tsx
|
|
760
298
|
import Flex from '@cronocode/react-box/components/flex';
|
|
761
|
-
|
|
762
|
-
<Flex d="column" gap={4} ai="center" jc="between">
|
|
763
|
-
{children}
|
|
764
|
-
</Flex>;
|
|
765
|
-
```
|
|
766
|
-
|
|
767
|
-
### Card
|
|
768
|
-
|
|
769
|
-
```tsx
|
|
770
|
-
<Box p={4} bgColor="white" borderRadius={8} shadow="medium">
|
|
771
|
-
{content}
|
|
772
|
-
</Box>
|
|
773
|
-
```
|
|
774
|
-
|
|
775
|
-
### Button
|
|
776
|
-
|
|
777
|
-
```tsx
|
|
299
|
+
import Grid from '@cronocode/react-box/components/grid';
|
|
778
300
|
import Button from '@cronocode/react-box/components/button';
|
|
779
|
-
|
|
780
|
-
<Button
|
|
781
|
-
px={4}
|
|
782
|
-
py={2}
|
|
783
|
-
bgColor="blue-500"
|
|
784
|
-
color="white"
|
|
785
|
-
borderRadius={6}
|
|
786
|
-
fontWeight={500}
|
|
787
|
-
hover={{ bgColor: 'blue-600' }}
|
|
788
|
-
disabled={{ opacity: 0.5, cursor: 'not-allowed' }}
|
|
789
|
-
>
|
|
790
|
-
Click me
|
|
791
|
-
</Button>;
|
|
792
|
-
```
|
|
793
|
-
|
|
794
|
-
### Input Field
|
|
795
|
-
|
|
796
|
-
```tsx
|
|
797
301
|
import Textbox from '@cronocode/react-box/components/textbox';
|
|
798
302
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
px={3}
|
|
803
|
-
py={2}
|
|
804
|
-
b={1}
|
|
805
|
-
borderColor="gray-300"
|
|
806
|
-
borderRadius={6}
|
|
807
|
-
focus={{ borderColor: 'blue-500', outline: 'none' }}
|
|
808
|
-
/>;
|
|
809
|
-
```
|
|
810
|
-
|
|
811
|
-
### Grid Layout
|
|
303
|
+
// Flex layout
|
|
304
|
+
<Flex d="column" gap={4} ai="center" jc="between">{children}</Flex>
|
|
305
|
+
<Flex inline gap={2}>Inline flex</Flex>
|
|
812
306
|
|
|
813
|
-
|
|
814
|
-
|
|
307
|
+
// Responsive stack
|
|
308
|
+
<Flex d="column" gap={2} md={{ d: 'row', gap: 4 }}>{children}</Flex>
|
|
815
309
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
<Box key={item.id}>{item.content}</Box>
|
|
819
|
-
))}
|
|
820
|
-
</Grid>;
|
|
821
|
-
```
|
|
310
|
+
// Grid
|
|
311
|
+
<Grid gridCols={3} gap={4}>{items.map(i => <Box key={i.id}>{i.content}</Box>)}</Grid>
|
|
822
312
|
|
|
823
|
-
|
|
313
|
+
// Card
|
|
314
|
+
<Box p={4} bgColor="white" borderRadius={8} shadow="medium">{content}</Box>
|
|
824
315
|
|
|
825
|
-
|
|
826
|
-
|
|
316
|
+
// Button with states
|
|
317
|
+
<Button px={4} py={2} bgColor="blue-500" color="white" borderRadius={6}
|
|
318
|
+
hover={{ bgColor: 'blue-600' }} disabled={{ opacity: 0.5, cursor: 'not-allowed' }}>Click</Button>
|
|
827
319
|
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
```
|
|
320
|
+
// Input
|
|
321
|
+
<Textbox placeholder="Enter..." width="fit" px={3} py={2} b={1} borderColor="gray-300"
|
|
322
|
+
borderRadius={6} focus={{ borderColor: 'blue-500', outline: 'none' }} />
|
|
832
323
|
|
|
833
|
-
|
|
324
|
+
// Truncated text
|
|
325
|
+
<Box overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap">Long text...</Box>
|
|
834
326
|
|
|
835
|
-
|
|
836
|
-
<Box overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap">
|
|
837
|
-
Long text that will be truncated...
|
|
838
|
-
</Box>
|
|
839
|
-
```
|
|
840
|
-
|
|
841
|
-
### Overlay/Modal Backdrop
|
|
842
|
-
|
|
843
|
-
```tsx
|
|
327
|
+
// Overlay
|
|
844
328
|
<Box position="fixed" top={0} left={0} right={0} bottom={0} bgColor="black" opacity={0.5} zIndex={50} />
|
|
845
329
|
```
|
|
846
330
|
|
|
847
|
-
**
|
|
848
|
-
|
|
849
|
-
```tsx
|
|
850
|
-
import Tooltip from '@cronocode/react-box/components/tooltip';
|
|
851
|
-
|
|
852
|
-
<Tooltip content="Tooltip text">
|
|
853
|
-
<Button>Hover me</Button>
|
|
854
|
-
</Tooltip>;
|
|
855
|
-
```
|
|
856
|
-
|
|
857
|
-
---
|
|
858
|
-
|
|
859
|
-
## Group Hover (hoverGroup)
|
|
331
|
+
**Portals**: For tooltips/dropdowns that need to escape `overflow: hidden`, use `Tooltip` component (renders via React portal into `#crono-box` container).
|
|
860
332
|
|
|
861
|
-
|
|
333
|
+
### Group Hover (hoverGroup)
|
|
862
334
|
|
|
863
335
|
```tsx
|
|
864
|
-
// Parent with a className
|
|
865
336
|
<Flex className="card-row" gap={2}>
|
|
866
|
-
{
|
|
867
|
-
<Box opacity={0} hoverGroup={{ 'card-row': { opacity: 1 } }}>
|
|
868
|
-
Actions
|
|
869
|
-
</Box>
|
|
337
|
+
<Box opacity={0} hoverGroup={{ 'card-row': { opacity: 1 } }}>Actions</Box>
|
|
870
338
|
</Flex>
|
|
871
339
|
```
|
|
872
340
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
## Server-Side Rendering (SSG/SSR)
|
|
341
|
+
### Server-Side Rendering
|
|
876
342
|
|
|
877
343
|
```tsx
|
|
878
344
|
import { getStyles, resetStyles } from '@cronocode/react-box/ssg';
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
// Inject into your HTML
|
|
884
|
-
<style id="crono-box">{cssString}</style>;
|
|
885
|
-
|
|
886
|
-
// Reset for next request (in SSR)
|
|
887
|
-
resetStyles();
|
|
345
|
+
const cssString = getStyles(); // after rendering, get CSS string
|
|
346
|
+
<style id="crono-box">{cssString}</style>
|
|
347
|
+
resetStyles(); // reset for next SSR request
|
|
888
348
|
```
|
|
889
349
|
|
|
890
350
|
---
|
|
891
351
|
|
|
892
352
|
## Key Reminders for AI Assistants
|
|
893
353
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
### Value Formatting
|
|
907
|
-
|
|
908
|
-
4. **fontSize uses divider 16**, not 4. `fontSize={14}` → 14px, NOT 3.5px
|
|
909
|
-
5. **Spacing uses divider 4**. `p={4}` → 16px (1rem)
|
|
910
|
-
6. **Border width is direct px**. `b={1}` → 1px
|
|
911
|
-
7. **lineHeight is direct px**. `lineHeight={24}` → 24px
|
|
912
|
-
|
|
913
|
-
### Syntax & Patterns
|
|
914
|
-
|
|
915
|
-
8. **Colors are Tailwind-like**: `'gray-500'`, `'blue-600'`, etc.
|
|
916
|
-
9. **Breakpoints are mobile-first**: base → sm → md → lg → xl → xxl
|
|
917
|
-
10. **Theme styles nest**: `theme={{ dark: { hover: { ... } } }}`
|
|
918
|
-
11. **HTML attributes go in `props`**: `<Link props={{ href: '/link' }}>` - NOT directly as Box props
|
|
919
|
-
12. **Percentage widths**: Use strings like `width="1/2"` for 50%
|
|
920
|
-
13. **Full size shortcuts**: `width="fit"` = 100%, `width="fit-screen"` = 100vw
|
|
921
|
-
14. **Box is memoized** with `React.memo` - props comparison is efficient
|
|
354
|
+
1. **NEVER `style={{ }}`** — use Box props. Missing prop? Use `Box.extend()`
|
|
355
|
+
2. **NEVER `<Box tag="...">` for common elements** — use `<Button>`, `<Link>`, `<H1>`, `<P>`, `<Nav>`, etc.
|
|
356
|
+
3. **NEVER `<Box display="flex/grid">`** — use `<Flex>` / `<Grid>`
|
|
357
|
+
4. **fontSize divider is 16** (not 4). `fontSize={14}` → 14px
|
|
358
|
+
5. **Spacing divider is 4**. `p={4}` → 16px (1rem)
|
|
359
|
+
6. **Border/borderRadius/lineHeight are direct px**. `b={1}` → 1px
|
|
360
|
+
7. **Colors are Tailwind-like**: `'gray-500'`, `'blue-600'`
|
|
361
|
+
8. **Breakpoints are mobile-first**: base → sm → md → lg → xl → xxl
|
|
362
|
+
9. **Theme styles nest**: `theme={{ dark: { hover: { ... } } }}`
|
|
363
|
+
10. **HTML attributes go in `props` prop**: `<Link props={{ href: '/about' }}>` not `<Link href>`
|
|
364
|
+
11. **Size shortcuts**: `width="fit"` = 100%, `width="fit-screen"` = 100vw, `width="1/2"` = 50%
|
|
365
|
+
12. **Box is memoized** with `React.memo`
|
|
922
366
|
|
|
923
367
|
---
|
|
924
368
|
|
|
925
369
|
## DataGrid Component
|
|
926
370
|
|
|
927
|
-
> **Full documentation:** See [`../docs/DATAGRID.md`](../docs/DATAGRID.md) for complete API reference, all features, component tree, style customization, and designs system.
|
|
928
|
-
|
|
929
|
-
A feature-rich data grid with sorting, filtering, grouping, row selection, column pinning, row detail, server-side pagination, virtualization, and full style customization via the component design system.
|
|
930
|
-
|
|
931
371
|
```tsx
|
|
932
372
|
import DataGrid from '@cronocode/react-box/components/dataGrid';
|
|
933
|
-
```
|
|
934
|
-
|
|
935
|
-
### Quick Reference
|
|
936
373
|
|
|
937
|
-
```tsx
|
|
938
374
|
<DataGrid
|
|
939
375
|
data={users}
|
|
940
376
|
def={{
|
|
941
377
|
rowKey: 'id',
|
|
942
378
|
title: 'Users',
|
|
943
|
-
topBar: true,
|
|
944
|
-
bottomBar: true,
|
|
945
|
-
globalFilter: true,
|
|
379
|
+
topBar: true, bottomBar: true, globalFilter: true,
|
|
946
380
|
rowSelection: { pinned: true },
|
|
947
381
|
showRowNumber: { pinned: true },
|
|
948
382
|
rowHeight: 40,
|
|
@@ -953,81 +387,265 @@ import DataGrid from '@cronocode/react-box/components/dataGrid';
|
|
|
953
387
|
{ key: 'email', header: 'Email', width: 250, filterable: true },
|
|
954
388
|
{ key: 'status', header: 'Status', filterable: { type: 'multiselect' } },
|
|
955
389
|
{ key: 'country', header: 'Country', pin: 'RIGHT' },
|
|
390
|
+
{
|
|
391
|
+
key: 'actions', header: '', pin: 'RIGHT', width: 80, sortable: false, resizable: false,
|
|
392
|
+
Cell: ({ cell }) => <Button onClick={() => edit(cell.row.data)}>Edit</Button>,
|
|
393
|
+
},
|
|
956
394
|
],
|
|
957
|
-
rowDetail: {
|
|
958
|
-
content: (user) => <UserDetails user={user} />,
|
|
959
|
-
height: 'auto',
|
|
960
|
-
expandOnRowClick: true,
|
|
961
|
-
},
|
|
962
|
-
// pagination: { totalCount: 500, pageSize: 25 }, // for server-side pagination
|
|
395
|
+
rowDetail: { content: (user) => <UserDetails user={user} />, height: 'auto', expandOnRowClick: true },
|
|
963
396
|
}}
|
|
964
|
-
component="subgrid" // optional: use a different component style tree
|
|
965
397
|
onSelectionChange={(e) => console.log(e.selectedRowKeys)}
|
|
966
|
-
// page={1} // controlled pagination
|
|
967
|
-
// onPageChange={(p, size) => {}} // page change handler
|
|
968
|
-
// onSortChange={(col, dir) => {}} // server-side sort handler
|
|
969
|
-
// onServerStateChange={(state) => fetch(state)} // unified server state callback
|
|
970
398
|
/>
|
|
971
399
|
```
|
|
972
400
|
|
|
973
|
-
###
|
|
401
|
+
### DataGridProps
|
|
974
402
|
|
|
975
403
|
| Prop | Type | Description |
|
|
976
|
-
|
|
977
|
-
| `data` | `TRow[]` | Row data
|
|
978
|
-
| `def` | `GridDefinition` | Grid
|
|
979
|
-
| `component` | `string` |
|
|
404
|
+
|---|---|---|
|
|
405
|
+
| `data` | `TRow[]` | Row data (required) |
|
|
406
|
+
| `def` | `GridDefinition` | Grid config (required) |
|
|
407
|
+
| `component` | `string` | Style tree name (default: `'datagrid'`) |
|
|
980
408
|
| `loading` | `boolean` | Loading state |
|
|
981
|
-
| `filters` | `((row
|
|
982
|
-
| `page` | `number` | Controlled
|
|
983
|
-
| `
|
|
984
|
-
| `
|
|
985
|
-
| `
|
|
986
|
-
| `
|
|
987
|
-
| `
|
|
409
|
+
| `filters` | `((row) => boolean)[]` | External predicate filters (applied before column/global filters) |
|
|
410
|
+
| `page` / `onPageChange` | `number` / `(page, size) => void` | Controlled pagination (1-indexed) |
|
|
411
|
+
| `onSortChange` | `(columnKey, direction) => void` | Sort callback (`direction`: `'ASC'`/`'DESC'`/`undefined`) |
|
|
412
|
+
| `onServerStateChange` | `(state) => void` | Unified: `{ page, pageSize, sortColumn, sortDirection, columnFilters, globalFilterValue }` |
|
|
413
|
+
| `onSelectionChange` | `(event) => void` | `event`: `{ action, selectedRowKeys, affectedRowKeys, isAllSelected }` |
|
|
414
|
+
| `expandedRowKeys` / `onExpandedRowKeysChange` | `Key[]` / `(keys) => void` | Controlled expanded rows |
|
|
415
|
+
| `globalFilterValue` / `onGlobalFilterChange` | `string` / `(value) => void` | Controlled global filter |
|
|
416
|
+
| `columnFilters` / `onColumnFiltersChange` | `ColumnFilters` / `(filters) => void` | Controlled column filters |
|
|
417
|
+
|
|
418
|
+
### GridDefinition
|
|
419
|
+
|
|
420
|
+
| Prop | Type | Default | Description |
|
|
421
|
+
|---|---|---|---|
|
|
422
|
+
| `columns` | `ColumnType[]` | required | Column definitions |
|
|
423
|
+
| `rowKey` | `keyof TRow \| (row) => Key` | auto | Unique row identifier |
|
|
424
|
+
| `rowHeight` | `number` | `48` | Row height in px |
|
|
425
|
+
| `visibleRowsCount` | `number \| 'all'` | `10` | Visible rows. `'all'` disables virtualization |
|
|
426
|
+
| `showRowNumber` | `boolean \| { pinned?, width? }` | `false` | Row number column |
|
|
427
|
+
| `rowSelection` | `boolean \| { pinned? }` | `false` | Checkbox selection column |
|
|
428
|
+
| `rowDetail` | `{ content, height?, expandOnRowClick?, pinned? }` | — | Expandable detail panel. `height`: `'auto'`/number/`(row) => number` |
|
|
429
|
+
| `pagination` | `{ totalCount, pageSize? }` | — | Server-side pagination. Bypasses client-side filtering |
|
|
430
|
+
| `topBar` / `bottomBar` | `boolean` | `false` | Show top/bottom bars |
|
|
431
|
+
| `title` / `topBarContent` | `ReactNode` | — | Top bar content |
|
|
432
|
+
| `globalFilter` | `boolean` | `false` | Enable global fuzzy search |
|
|
433
|
+
| `globalFilterKeys` | `(keyof TRow)[]` | all | Limit global filter columns |
|
|
434
|
+
| `sortable` / `resizable` | `boolean` | `true` | Enable sorting/resizing for all columns |
|
|
435
|
+
| `noDataComponent` | `ReactNode` | `'empty'` | Custom empty state |
|
|
436
|
+
|
|
437
|
+
### ColumnType
|
|
438
|
+
|
|
439
|
+
| Prop | Type | Default | Description |
|
|
440
|
+
|---|---|---|---|
|
|
441
|
+
| `key` | `Key` | required | Column identifier (maps to TRow property) |
|
|
442
|
+
| `header` | `string` | — | Header text |
|
|
443
|
+
| `width` | `number` | `200` | Base width in px |
|
|
444
|
+
| `align` | `'left' \| 'right' \| 'center'` | `'left'` | Cell alignment |
|
|
445
|
+
| `pin` | `'LEFT' \| 'RIGHT'` | — | Pin to edge (sticky on scroll) |
|
|
446
|
+
| `columns` | `ColumnType[]` | — | Nested columns (grouped header) |
|
|
447
|
+
| `Cell` | `({ cell }) => ReactNode` | — | Custom renderer. `cell`: `{ value, row, column, grid }` |
|
|
448
|
+
| `sortable` / `resizable` | `boolean` | inherits | Override grid-level setting |
|
|
449
|
+
| `flexible` | `boolean` | `true` | Participate in flex width distribution |
|
|
450
|
+
| `filterable` | `boolean \| FilterConfig` | — | `true` (text), `{ type: 'number', min?, max? }`, `{ type: 'multiselect', options? }` |
|
|
451
|
+
|
|
452
|
+
### Server-Side Pagination
|
|
453
|
+
|
|
454
|
+
```tsx
|
|
455
|
+
<DataGrid
|
|
456
|
+
data={pageData} page={page} loading={loading}
|
|
457
|
+
onServerStateChange={(state) => {
|
|
458
|
+
// state = { page, pageSize, sortColumn, sortDirection, columnFilters, globalFilterValue }
|
|
459
|
+
setPage(state.page); refetch(state);
|
|
460
|
+
}}
|
|
461
|
+
def={{ columns: [...], bottomBar: true, pagination: { totalCount, pageSize: 25 }, globalFilter: true }}
|
|
462
|
+
/>
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### Style Customization
|
|
466
|
+
|
|
467
|
+
Component tree (all customizable via `Box.components()`):
|
|
468
|
+
```
|
|
469
|
+
datagrid → content, topBar (globalFilter > stats, columnGroups > item), header > cell (contextMenu, resizer),
|
|
470
|
+
filter > row > cell, body > cell | row | groupRow | detailRow, bottomBar > pagination
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
```tsx
|
|
474
|
+
Box.components({
|
|
475
|
+
datagrid: { children: { header: { children: { cell: { styles: { textTransform: 'uppercase' } } } } } },
|
|
476
|
+
subgrid: {
|
|
477
|
+
extends: 'datagrid', // MUST use extends — inherits pinning, sticky, hover groups, filters, etc.
|
|
478
|
+
styles: { b: 0, shadow: 'none' },
|
|
479
|
+
children: { body: { children: { cell: { styles: { fontSize: 13 } } } } },
|
|
480
|
+
},
|
|
481
|
+
});
|
|
482
|
+
<DataGrid component="subgrid" data={data} def={def} /> {/* children resolve under subgrid.* */}
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
---
|
|
486
|
+
|
|
487
|
+
## Dropdown Component
|
|
488
|
+
|
|
489
|
+
```tsx
|
|
490
|
+
import Dropdown from '@cronocode/react-box/components/dropdown';
|
|
491
|
+
```
|
|
988
492
|
|
|
989
|
-
###
|
|
493
|
+
### Usage
|
|
990
494
|
|
|
991
495
|
```tsx
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
496
|
+
// Single selection (uncontrolled)
|
|
497
|
+
<Dropdown<string> defaultValue="apple" onChange={(value, values) => console.log(value)}>
|
|
498
|
+
<Dropdown.Unselect>Pick a fruit...</Dropdown.Unselect>
|
|
499
|
+
<Dropdown.Item value="apple">Apple</Dropdown.Item>
|
|
500
|
+
<Dropdown.Item value="banana">Banana</Dropdown.Item>
|
|
501
|
+
</Dropdown>
|
|
502
|
+
|
|
503
|
+
// Controlled
|
|
504
|
+
<Dropdown<string> value={fruit} onChange={(value) => setFruit(value!)}>
|
|
505
|
+
<Dropdown.Item value="apple">Apple</Dropdown.Item>
|
|
506
|
+
<Dropdown.Item value="banana">Banana</Dropdown.Item>
|
|
507
|
+
</Dropdown>
|
|
508
|
+
|
|
509
|
+
// Multiple + search + checkboxes
|
|
510
|
+
<Dropdown<string> multiple showCheckbox isSearchable searchPlaceholder="Search...">
|
|
511
|
+
<Dropdown.SelectAll>Select all</Dropdown.SelectAll>
|
|
512
|
+
<Dropdown.EmptyItem>No results</Dropdown.EmptyItem>
|
|
513
|
+
<Dropdown.Display>{(values) => values.length === 0 ? 'Pick...' : `${values.length} selected`}</Dropdown.Display>
|
|
514
|
+
<Dropdown.Item value="apple">Apple</Dropdown.Item>
|
|
515
|
+
<Dropdown.Item value="banana">Banana</Dropdown.Item>
|
|
516
|
+
</Dropdown>
|
|
517
|
+
|
|
518
|
+
// Form integration: name prop renders hidden inputs with JSON-stringified values
|
|
519
|
+
<Dropdown<string> name="fruits" multiple defaultValue={['apple']}>...</Dropdown>
|
|
995
520
|
```
|
|
996
521
|
|
|
522
|
+
### Props
|
|
523
|
+
|
|
524
|
+
| Prop | Type | Description |
|
|
525
|
+
|---|---|---|
|
|
526
|
+
| `value` / `defaultValue` | `TVal \| TVal[]` | Controlled / uncontrolled selected value(s) |
|
|
527
|
+
| `multiple` | `boolean` | Multi-select mode |
|
|
528
|
+
| `isSearchable` | `boolean` | Show search input when open |
|
|
529
|
+
| `searchPlaceholder` | `string` | Search input placeholder |
|
|
530
|
+
| `hideIcon` | `boolean` | Hide chevron icon |
|
|
531
|
+
| `showCheckbox` | `boolean` | Show checkboxes in multiple mode |
|
|
532
|
+
| `name` | `string` | Form field name (renders hidden `<input>` elements) |
|
|
533
|
+
| `onChange` | `(value: TVal \| undefined, values: TVal[]) => void` | Selection callback |
|
|
534
|
+
| `itemsProps` | `BoxStyleProps` | Style overrides for the opened items container (`dropdown.items`) |
|
|
535
|
+
| `iconProps` | `BoxStyleProps` | Style overrides for the chevron icon container (`dropdown.icon`) |
|
|
536
|
+
| `variant` | `ClassNameType` | Propagates to root **and all child sub-components** |
|
|
537
|
+
|
|
538
|
+
Also accepts all `BoxProps` (styling props) which apply to the root button element.
|
|
539
|
+
|
|
540
|
+
### Sub-Components
|
|
541
|
+
|
|
542
|
+
All sub-components accept BoxProps for per-instance style overrides.
|
|
543
|
+
|
|
544
|
+
| Sub-Component | Purpose |
|
|
545
|
+
|---|---|
|
|
546
|
+
| `Dropdown.Item<TVal>` | Selectable option. Requires `value` prop |
|
|
547
|
+
| `Dropdown.Unselect` | Clear selection option (shown when items selected) |
|
|
548
|
+
| `Dropdown.SelectAll` | Select all (shown in `multiple` when not all selected) |
|
|
549
|
+
| `Dropdown.EmptyItem` | Shown when search yields no results |
|
|
550
|
+
| `Dropdown.Display` | Custom display: static content or `(values: TVal[], isOpen: boolean) => ReactNode` |
|
|
551
|
+
|
|
997
552
|
### Style Customization
|
|
998
553
|
|
|
999
|
-
|
|
554
|
+
**Per-instance** — use `itemsProps`, `iconProps`, or BoxProps directly on sub-components:
|
|
555
|
+
|
|
556
|
+
```tsx
|
|
557
|
+
<Dropdown<string> itemsProps={{ width: 80, maxHeight: 50 }} iconProps={{ color: 'gray-400' }}>
|
|
558
|
+
<Dropdown.Item value="a" bgColor="blue-50" fontWeight={600}>Highlighted</Dropdown.Item>
|
|
559
|
+
</Dropdown>
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
**Global** — override defaults via `Box.components()` (deep-merged):
|
|
1000
563
|
|
|
1001
564
|
```tsx
|
|
1002
565
|
Box.components({
|
|
1003
|
-
|
|
1004
|
-
|
|
566
|
+
dropdown: {
|
|
567
|
+
styles: { borderRadius: 4, bgColor: 'gray-50' },
|
|
1005
568
|
children: {
|
|
1006
|
-
|
|
1007
|
-
|
|
569
|
+
items: { styles: { shadow: 'large', borderRadius: 4 } },
|
|
570
|
+
item: { styles: { borderRadius: 2, hover: { bgColor: 'blue-50' } } },
|
|
1008
571
|
},
|
|
1009
572
|
},
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
573
|
+
});
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
**Custom variants** — `variant` propagates to all children. Define matching variants on each child:
|
|
577
|
+
|
|
578
|
+
```tsx
|
|
579
|
+
Box.components({
|
|
580
|
+
dropdown: {
|
|
581
|
+
variants: { dense: { p: 1, fontSize: 12 } },
|
|
1014
582
|
children: {
|
|
1015
|
-
|
|
1016
|
-
|
|
583
|
+
items: { variants: { dense: { maxHeight: 40, gap: 0 } } },
|
|
584
|
+
item: { variants: { dense: { p: 1, lineHeight: 16 } } },
|
|
585
|
+
unselect: { variants: { dense: { p: 1 } } },
|
|
586
|
+
selectAll: { variants: { dense: { p: 1 } } },
|
|
587
|
+
emptyItem: { variants: { dense: { p: 1 } } },
|
|
1017
588
|
},
|
|
1018
589
|
},
|
|
1019
590
|
});
|
|
591
|
+
<Dropdown variant="dense">...</Dropdown> // applies to root + all children
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
### Component Style Tree
|
|
595
|
+
|
|
596
|
+
| Component Name | Description | Built-in Variants |
|
|
597
|
+
|---|---|---|
|
|
598
|
+
| `dropdown` | Root button trigger | `compact` |
|
|
599
|
+
| `dropdown.items` | Opened items container (portal) | — |
|
|
600
|
+
| `dropdown.item` | Selectable item | `compact`, `multiple` |
|
|
601
|
+
| `dropdown.unselect` | Clear selection option | `compact` |
|
|
602
|
+
| `dropdown.selectAll` | Select all option | `compact` |
|
|
603
|
+
| `dropdown.emptyItem` | No results placeholder | `compact` |
|
|
604
|
+
| `dropdown.icon` | Chevron arrow container | — |
|
|
605
|
+
|
|
606
|
+
---
|
|
607
|
+
|
|
608
|
+
## Select Component
|
|
1020
609
|
|
|
1021
|
-
|
|
1022
|
-
|
|
610
|
+
Data-driven dropdown — pass `data` + `def` instead of composing children. Wraps Dropdown internally, shares the same `dropdown.*` style tree.
|
|
611
|
+
|
|
612
|
+
```tsx
|
|
613
|
+
import Select from '@cronocode/react-box/components/select';
|
|
614
|
+
|
|
615
|
+
// Basic
|
|
616
|
+
<Select<User, number> data={users} def={{ valueKey: 'id', displayKey: 'name', placeholder: 'Pick...' }}
|
|
617
|
+
value={selected} onChange={(value) => setSelected(value!)} />
|
|
618
|
+
|
|
619
|
+
// Multiple + search + custom display
|
|
620
|
+
<Select<User, number> data={users} multiple showCheckbox isSearchable searchPlaceholder="Search..."
|
|
621
|
+
def={{
|
|
622
|
+
valueKey: 'id', displayKey: 'name', placeholder: 'Pick users...',
|
|
623
|
+
selectAllText: 'Select all', emptyText: 'No results',
|
|
624
|
+
display: (user) => `${user.name} — ${user.role}`,
|
|
625
|
+
selectedDisplay: (rows) => `${rows.length} selected`,
|
|
626
|
+
}} />
|
|
1023
627
|
```
|
|
1024
628
|
|
|
629
|
+
### SelectDef
|
|
630
|
+
|
|
631
|
+
| Prop | Type | Description |
|
|
632
|
+
|---|---|---|
|
|
633
|
+
| `valueKey` | `keyof TRow` | Required — field used as option value |
|
|
634
|
+
| `displayKey` | `keyof TRow` | Field to display (defaults to valueKey) |
|
|
635
|
+
| `display` | `(row: TRow) => ReactNode` | Custom render per item |
|
|
636
|
+
| `selectedDisplay` | `(rows: TRow[], isOpen: boolean) => ReactNode` | Custom trigger display (receives resolved row objects) |
|
|
637
|
+
| `placeholder` | `string` | Unselect/placeholder text |
|
|
638
|
+
| `selectAllText` | `string` | Select all option text (multiple mode) |
|
|
639
|
+
| `emptyText` | `string` | Empty search results text |
|
|
640
|
+
|
|
641
|
+
Also accepts: `data` (TRow[]), `value`/`defaultValue`, `multiple`, `isSearchable`, `searchPlaceholder`, `showCheckbox`, `hideIcon`, `name`, `onChange`, `itemsProps`, `iconProps`, `variant`, and all BoxProps. Same styling/variants as Dropdown.
|
|
642
|
+
|
|
1025
643
|
---
|
|
1026
644
|
|
|
1027
645
|
## Debugging Tips
|
|
1028
646
|
|
|
1029
|
-
1. **Inspect styles**:
|
|
1030
|
-
2. **
|
|
1031
|
-
3. **
|
|
647
|
+
1. **Inspect styles**: `<style id="crono-box">` in document head
|
|
648
|
+
2. **Class names**: Elements get classes like `_b`, `_2a`, etc.
|
|
649
|
+
3. **CSS variables**: In `:root` rules
|
|
1032
650
|
4. **Theme issues**: Ensure `<Box.Theme>` wraps your app
|
|
1033
651
|
5. **Portal theming**: Tooltips/dropdowns use `#crono-box` container
|