Skip to content

Commit e7f5d68

Browse files
committed
docs: add comprehensive v2.0 headless migration plan and API design
- Created V2_HEADLESS_MIGRATION_PLAN.md with full analysis: - Found 2,534 lines of CSS across 12 components to remove - Detailed migration strategy with 6-week timeline - Risk analysis and mitigation strategies - Created V2_API_DESIGN_PROPOSAL.md: - Recommends ClassNames object pattern - Provides detailed examples for each component - Includes styling cookbook for Tailwind, CSS Modules, etc. - Testing strategy and implementation checklist This planning phase ensures we have a clear path forward for making all components truly headless in v2.0. Related to #97 (v2.0 headless components)
1 parent 4012ad9 commit e7f5d68

File tree

2 files changed

+399
-9
lines changed

2 files changed

+399
-9
lines changed

V2_API_DESIGN_PROPOSAL.md

Lines changed: 390 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,390 @@
1+
# V2.0 Headless Components API Design Proposal
2+
3+
## Executive Summary
4+
5+
We need to choose an API pattern for v2.0 headless components. After analysis, I recommend **Option A: ClassNames Object** with optional render prop overrides for advanced use cases.
6+
7+
## Detailed API Options
8+
9+
### Option A: ClassNames Object (RECOMMENDED) ✅
10+
11+
```tsx
12+
interface HeadlessComponentProps {
13+
// Behavioral props
14+
value?: string;
15+
onChange?: (value: string) => void;
16+
17+
// Styling props
18+
className?: string; // Container class
19+
classNames?: {
20+
container?: string;
21+
trigger?: string;
22+
panel?: string;
23+
item?: string;
24+
// ... specific to each component
25+
};
26+
27+
// Optional render prop overrides
28+
renderTrigger?: (props: TriggerRenderProps) => ReactNode;
29+
}
30+
```
31+
32+
#### Implementation Example: DateFilter
33+
34+
```tsx
35+
export const DateFilter = ({ className, classNames = {}, value, onChange, ...props }) => {
36+
return (
37+
<div className={cn(className, classNames.container)} data-testid="date-filter">
38+
<FilterModeToggle className={classNames.modeToggle} active={mode === "relative"} onClick={toggleMode} />
39+
40+
{mode === "relative" ? <input className={classNames.relativeInput} value={relativeValue} onChange={handleRelativeChange} placeholder="e.g., -7d, today, next month" /> : <DateInputs className={classNames.dateInputs} startDate={startDate} endDate={endDate} onChange={handleDateChange} />}
41+
42+
<div className={classNames.actions}>
43+
<button className={classNames.applyButton} onClick={handleApply}>
44+
Apply
45+
</button>
46+
<button className={classNames.resetButton} onClick={handleReset}>
47+
Reset
48+
</button>
49+
</div>
50+
</div>
51+
);
52+
};
53+
```
54+
55+
#### Usage Examples
56+
57+
**Basic (user provides all styles):**
58+
59+
```tsx
60+
<DateFilter
61+
className="my-date-filter"
62+
classNames={{
63+
container: "flex flex-col gap-4 p-4 bg-white rounded-lg shadow",
64+
modeToggle: "flex gap-2",
65+
relativeInput: "px-3 py-2 border rounded focus:ring-2",
66+
actions: "flex gap-2 mt-4",
67+
applyButton: "px-4 py-2 bg-blue-500 text-white rounded",
68+
resetButton: "px-4 py-2 bg-gray-200 rounded",
69+
}}
70+
/>
71+
```
72+
73+
**With Style System:**
74+
75+
```tsx
76+
// styles/components.ts
77+
export const dateFilterStyles = {
78+
container: "date-filter-container",
79+
modeToggle: "date-filter-toggle",
80+
relativeInput: "date-filter-input",
81+
actions: "date-filter-actions",
82+
applyButton: "btn btn-primary",
83+
resetButton: "btn btn-secondary",
84+
};
85+
86+
// Component usage
87+
<DateFilter classNames={dateFilterStyles} />;
88+
```
89+
90+
**With CSS Modules:**
91+
92+
```tsx
93+
import styles from "./DateFilter.module.css";
94+
95+
<DateFilter classNames={styles} />;
96+
```
97+
98+
### Why ClassNames is the Best Choice
99+
100+
#### 1. **Simplicity**
101+
102+
- Easy to understand and implement
103+
- Familiar pattern (React Select, MUI, etc.)
104+
- No complex render prop logic
105+
106+
#### 2. **Flexibility**
107+
108+
- Works with any CSS solution
109+
- Allows partial styling
110+
- Can be extended with render props when needed
111+
112+
#### 3. **Type Safety**
113+
114+
```tsx
115+
interface DateFilterClassNames {
116+
container?: string;
117+
modeToggle?: string;
118+
relativeInput?: string;
119+
dateInputs?: string;
120+
actions?: string;
121+
applyButton?: string;
122+
resetButton?: string;
123+
}
124+
125+
// Full IntelliSense support
126+
```
127+
128+
#### 4. **Performance**
129+
130+
- No extra re-renders from render props
131+
- Simple string props
132+
- Tree-shakeable styles
133+
134+
#### 5. **Migration Path**
135+
136+
```tsx
137+
// v1.x (current)
138+
<DateFilter /> // Styles included
139+
140+
// v1.9 (deprecation)
141+
<DateFilter unstyled classNames={styles} /> // Opt-in
142+
143+
// v2.0 (headless)
144+
<DateFilter classNames={styles} /> // Required
145+
```
146+
147+
## Component-Specific APIs
148+
149+
### DateFilter
150+
151+
```tsx
152+
interface DateFilterClassNames {
153+
container?: string;
154+
modeToggle?: string;
155+
modeButton?: string;
156+
modeButtonActive?: string;
157+
relativeSection?: string;
158+
relativeInput?: string;
159+
relativeHint?: string;
160+
absoluteSection?: string;
161+
dateInputs?: string;
162+
dateInput?: string;
163+
dateLabel?: string;
164+
actions?: string;
165+
applyButton?: string;
166+
resetButton?: string;
167+
errorMessage?: string;
168+
}
169+
```
170+
171+
### QuickFilterDropdown
172+
173+
```tsx
174+
interface QuickFilterDropdownClassNames {
175+
container?: string;
176+
trigger?: string;
177+
triggerActive?: string;
178+
triggerIcon?: string;
179+
dropdown?: string;
180+
dropdownOpen?: string;
181+
searchSection?: string;
182+
searchInput?: string;
183+
optionsList?: string;
184+
optionGroup?: string;
185+
optionGroupLabel?: string;
186+
option?: string;
187+
optionActive?: string;
188+
optionSelected?: string;
189+
optionDisabled?: string;
190+
optionIcon?: string;
191+
optionContent?: string;
192+
optionLabel?: string;
193+
optionDescription?: string;
194+
divider?: string;
195+
emptyState?: string;
196+
loadingState?: string;
197+
}
198+
```
199+
200+
### ActiveFilters
201+
202+
```tsx
203+
interface ActiveFiltersClassNames {
204+
container?: string;
205+
filterList?: string;
206+
filterItem?: string;
207+
filterLabel?: string;
208+
filterValue?: string;
209+
filterRemove?: string;
210+
clearAll?: string;
211+
emptyState?: string;
212+
}
213+
```
214+
215+
## Styling Cookbook
216+
217+
### 1. Tailwind CSS Template
218+
219+
```tsx
220+
const tailwindStyles = {
221+
// Modern card style
222+
container: "bg-white dark:bg-gray-800 rounded-lg shadow-sm p-4",
223+
224+
// Interactive elements
225+
trigger: "px-4 py-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-md transition-colors",
226+
227+
// Form inputs
228+
input: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-2 focus:ring-blue-500 dark:bg-gray-700",
229+
230+
// Buttons
231+
primaryButton: "px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-md transition-colors",
232+
secondaryButton: "px-4 py-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-md transition-colors",
233+
};
234+
```
235+
236+
### 2. CSS Modules Template
237+
238+
```css
239+
/* DateFilter.module.css */
240+
.container {
241+
background: var(--surface);
242+
border-radius: var(--radius);
243+
padding: var(--spacing-4);
244+
}
245+
246+
.input {
247+
width: 100%;
248+
padding: var(--spacing-2) var(--spacing-3);
249+
border: 1px solid var(--border);
250+
border-radius: var(--radius-sm);
251+
}
252+
253+
.input:focus {
254+
outline: none;
255+
border-color: var(--primary);
256+
box-shadow: 0 0 0 3px var(--primary-alpha);
257+
}
258+
```
259+
260+
### 3. Styled Components Template
261+
262+
```tsx
263+
const StyledDateFilter = {
264+
container: styled.div`
265+
background: ${(props) => props.theme.surface};
266+
border-radius: ${(props) => props.theme.radius};
267+
padding: ${(props) => props.theme.spacing(4)};
268+
`,
269+
270+
input: styled.input`
271+
width: 100%;
272+
padding: ${(props) => props.theme.spacing(2, 3)};
273+
border: 1px solid ${(props) => props.theme.border};
274+
275+
&:focus {
276+
outline: none;
277+
border-color: ${(props) => props.theme.primary};
278+
}
279+
`,
280+
};
281+
```
282+
283+
## Advanced Patterns
284+
285+
### Conditional Styling
286+
287+
```tsx
288+
<DateFilter
289+
classNames={{
290+
applyButton: cn("px-4 py-2 rounded transition-colors", hasChanges ? "bg-blue-500 hover:bg-blue-600 text-white" : "bg-gray-300 cursor-not-allowed text-gray-500"),
291+
}}
292+
/>
293+
```
294+
295+
### Variant Systems
296+
297+
```tsx
298+
const createDateFilterStyles = (variant: "default" | "compact" | "inline") => ({
299+
container: cn("date-filter", {
300+
"p-4 rounded-lg shadow": variant === "default",
301+
"p-2": variant === "compact",
302+
"inline-flex items-center gap-2": variant === "inline",
303+
}),
304+
// ... other styles based on variant
305+
});
306+
307+
<DateFilter classNames={createDateFilterStyles("compact")} />;
308+
```
309+
310+
### Composition
311+
312+
```tsx
313+
// Compose multiple style sources
314+
const dateFilterStyles = {
315+
...baseStyles.dateFilter,
316+
...themeStyles.dateFilter,
317+
...customStyles,
318+
};
319+
```
320+
321+
## Testing Strategy
322+
323+
### 1. Visual Regression Tests
324+
325+
```tsx
326+
// Create stories with different style systems
327+
export const TailwindStyled = () => <DateFilter classNames={tailwindStyles} />;
328+
329+
export const CSSModulesStyled = () => <DateFilter classNames={cssModuleStyles} />;
330+
331+
export const Unstyled = () => <DateFilter classNames={{}} />;
332+
```
333+
334+
### 2. Accessibility Tests
335+
336+
```tsx
337+
test("maintains accessibility without styles", () => {
338+
render(<DateFilter classNames={{}} />);
339+
340+
// Ensure ARIA attributes work
341+
expect(screen.getByRole("button", { name: "Apply" })).toBeInTheDocument();
342+
expect(screen.getByLabelText("Start date")).toBeInTheDocument();
343+
});
344+
```
345+
346+
### 3. Style Application Tests
347+
348+
```tsx
349+
test("applies custom classNames", () => {
350+
const classNames = {
351+
container: "custom-container",
352+
applyButton: "custom-button",
353+
};
354+
355+
render(<DateFilter classNames={classNames} />);
356+
357+
expect(screen.getByTestId("date-filter")).toHaveClass("custom-container");
358+
expect(screen.getByText("Apply")).toHaveClass("custom-button");
359+
});
360+
```
361+
362+
## Implementation Checklist
363+
364+
- [ ] Remove all CSS imports from components
365+
- [ ] Delete all .module.css files
366+
- [ ] Add classNames prop to all components
367+
- [ ] Add className prop for root element
368+
- [ ] Ensure all elements can be styled
369+
- [ ] Add data-testid attributes
370+
- [ ] Forward refs where appropriate
371+
- [ ] Update TypeScript interfaces
372+
- [ ] Create migration guide
373+
- [ ] Build style templates
374+
- [ ] Update documentation
375+
- [ ] Add deprecation warnings (v1.9)
376+
- [ ] Create codemods
377+
378+
## Decision
379+
380+
**Recommendation: Proceed with ClassNames Object API**
381+
382+
This provides the best balance of:
383+
384+
- Simplicity for users
385+
- Flexibility for styling
386+
- Ease of implementation
387+
- Clear migration path
388+
- Industry-standard patterns
389+
390+
The classNames approach is proven by popular libraries like React Select, Headless UI, and others. It's intuitive, performant, and provides excellent developer experience.

0 commit comments

Comments
 (0)