From 022ccc3a7bdb881cc8640ff8e244da81ef417a27 Mon Sep 17 00:00:00 2001 From: melvinchia3636 Date: Mon, 20 Apr 2026 09:11:06 +0800 Subject: [PATCH] refactor(ui): huge progress on detailwinding --- .github/agents/de-tailwind.agent.md | 661 +++++++++++++++--- .../instructions/de-tailwind.instructions.md | 609 ---------------- packages/lifeforge-ui/TAILWIND_MIGRATION.md | 14 +- .../inputs/ColorInput/ColorInput.css.ts | 10 + .../inputs/ColorInput/ColorInput.stories.tsx | 17 +- .../inputs/ColorInput/ColorInput.tsx | 106 +-- .../components/PaletteButtons.css.ts | 10 + .../components/PaletteButtons.tsx | 63 +- .../FlatUIColorsModal.css.ts | 26 + .../modals/FlatUIColorsModal/index.tsx | 89 ++- .../MorandiColorPaletteModal.css.ts | 18 + .../modals/ModandiColorPaletteModal/index.tsx | 70 +- .../TailwindCSSColorsModal.css.ts | 15 + .../components/ColorItem.css.ts | 18 + .../components/ColorItem.tsx | 58 +- .../modals/TailwindCSSColorsModal/index.tsx | 32 +- .../ColorInput/ColorPickerModal/index.css | 29 +- .../ColorInput/ColorPickerModal/index.tsx | 174 +++-- .../ComboboxInput/ComboboxInput.stories.tsx | 246 ++++++- .../inputs/ComboboxInput/ComboboxInput.tsx | 97 ++- .../components/ComboboxInputWrapper.css.ts | 9 + .../components/ComboboxInputWrapper.tsx | 47 +- .../components/ComboboxOption.css.ts | 19 + .../components/ComboboxOption.tsx | 133 ++-- .../components/ComboboxOptions.css.ts | 9 + .../components/ComboboxOptions.tsx | 41 +- .../inputs/CurrencyInput/CurrencyInput.css.ts | 55 -- .../inputs/CurrencyInput/CurrencyInput.tsx | 70 +- .../inputs/DateInput/DateInput.css.ts | 17 +- .../inputs/DateInput/DateInput.stories.tsx | 23 +- .../components/inputs/DateInput/DateInput.tsx | 113 +-- .../src/components/inputs/FAB/FAB.stories.tsx | 29 +- .../src/components/inputs/FAB/FAB.tsx | 34 +- .../components/inputs/IconInput/IconInput.tsx | 12 +- .../ListboxInput/ListboxInput.stories.tsx | 4 +- .../inputs/ListboxInput/ListboxInput.tsx | 48 +- .../components/ListboxInputWrapper.tsx | 2 +- .../NumberInput/NumberInput.stories.tsx | 4 +- .../inputs/QRCodeScanner/QRCodeScanner.css.ts | 23 + .../inputs/QRCodeScanner/QRCodeScanner.tsx | 24 +- .../inputs/SearchInput/SearchInput.css.ts | 34 + .../SearchInput/SearchInput.stories.tsx | 101 ++- .../inputs/SearchInput/SearchInput.tsx | 147 ++-- .../SliderInput/SliderInput.stories.tsx | 95 ++- .../inputs/SliderInput/SliderInput.tsx | 142 ++-- .../inputs/TextAreaInput/TextAreaInput.css.ts | 47 -- .../TextAreaInput/TextAreaInput.stories.tsx | 42 +- .../inputs/TextAreaInput/TextAreaInput.tsx | 94 ++- .../components/inputs/TextInput/TextInput.tsx | 2 +- .../TextInput/components/TextInputBox.css.ts | 22 +- .../TextInput/components/TextInputBox.tsx | 42 +- .../InputActionButton.css.ts | 7 +- .../InputActionButton/InputActionButton.tsx | 29 +- .../shared/components/InputIcon/InputIcon.tsx | 1 + .../components/InputLabel/InputLabel.css.ts | 2 +- .../components/InputLabel/InputLabel.tsx | 39 +- .../InputWrapper/InputWrapper.css.ts | 55 +- .../components/InputWrapper/InputWrapper.tsx | 21 +- .../components/Placeholder/Placeholder.css.ts | 28 + .../components/Placeholder/Placeholder.tsx | 33 + .../shared/components/Placeholder/index.ts | 1 + .../primitives/Text/Text.stories.tsx | 76 ++ .../src/components/primitives/Text/Text.tsx | 19 +- .../components/primitives/Text/text.css.ts | 22 +- 64 files changed, 2537 insertions(+), 1642 deletions(-) delete mode 100644 .github/instructions/de-tailwind.instructions.md create mode 100644 packages/lifeforge-ui/src/components/inputs/ColorInput/ColorInput.css.ts create mode 100644 packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/PaletteButtons.css.ts create mode 100644 packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/modals/FlatUIColorsModal/FlatUIColorsModal.css.ts create mode 100644 packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/modals/ModandiColorPaletteModal/MorandiColorPaletteModal.css.ts create mode 100644 packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/modals/TailwindCSSColorsModal/TailwindCSSColorsModal.css.ts create mode 100644 packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/modals/TailwindCSSColorsModal/components/ColorItem.css.ts create mode 100644 packages/lifeforge-ui/src/components/inputs/ComboboxInput/components/ComboboxInputWrapper.css.ts create mode 100644 packages/lifeforge-ui/src/components/inputs/ComboboxInput/components/ComboboxOption.css.ts create mode 100644 packages/lifeforge-ui/src/components/inputs/ComboboxInput/components/ComboboxOptions.css.ts delete mode 100644 packages/lifeforge-ui/src/components/inputs/CurrencyInput/CurrencyInput.css.ts create mode 100644 packages/lifeforge-ui/src/components/inputs/QRCodeScanner/QRCodeScanner.css.ts create mode 100644 packages/lifeforge-ui/src/components/inputs/SearchInput/SearchInput.css.ts delete mode 100644 packages/lifeforge-ui/src/components/inputs/TextAreaInput/TextAreaInput.css.ts create mode 100644 packages/lifeforge-ui/src/components/inputs/shared/components/Placeholder/Placeholder.css.ts create mode 100644 packages/lifeforge-ui/src/components/inputs/shared/components/Placeholder/Placeholder.tsx create mode 100644 packages/lifeforge-ui/src/components/inputs/shared/components/Placeholder/index.ts diff --git a/.github/agents/de-tailwind.agent.md b/.github/agents/de-tailwind.agent.md index 77a5e793e..6ab5a00e6 100644 --- a/.github/agents/de-tailwind.agent.md +++ b/.github/agents/de-tailwind.agent.md @@ -8,10 +8,10 @@ You are a specialist at migrating lifeforge-ui components from Tailwind CSS to p Before touching ANY file, you MUST: -1. Read the full de-tailwind instruction file at `.github/instructions/de-tailwind.instructions.md`. -2. Read the **complete** source file you are about to modify — understand every conditional class, dark-mode variant, hover/focus state, and context selector. -3. Read the primitive component prop types by searching for `Box`, `Flex`, `Grid`, `Text` exports in `packages/lifeforge-ui/src/components/primitives/`. -4. Consult the space token table in the instructions to map every Tailwind spacing value before writing. +1. Read the **complete** source file you are about to modify — understand every conditional class, dark-mode variant, hover/focus state, and context selector. +2. Read the **primitive component prop types** (see §1 below) — know exactly which props are available so you don't reach for `style={}` when a prop exists. +3. Consult the **space token table** (see §2 below) to map every Tailwind spacing value before writing. +4. Understand the **system architecture** (see §0.5 below) before writing any `.css.ts` file. ## Constraints @@ -22,121 +22,588 @@ Before touching ANY file, you MUST: - DO NOT add comments, docstrings, or explanations to code you did not change. - DO NOT refactor logic, rename variables, or make any change beyond the styling migration. - ONLY work on files under `packages/lifeforge-ui/src/`. -- **Prefer `asChild` over `.css.ts`**: before writing a new `.css.ts` style, check whether wrapping the target element with a primitive using `asChild` achieves the same result. Using `asChild` eliminates wrapper DOM nodes AND removes the need for a CSS export entirely. Only fall back to `.css.ts` for properties that primitives cannot express (e.g. `transition`, complex selectors, context selectors like `.bordered &`). +- **`asChild` MUST be tried first — ALWAYS, before writing any `.css.ts` export.** For every non-primitive element that needs color, spacing, or layout styling, ask: "can a primitive with `asChild` express this?" If yes, use `asChild`. Writing a `.css.ts` export when `asChild` would have worked is a mistake. Only reach for `.css.ts` when primitives are genuinely incapable of expressing the property: `transition`, `cursor`, `borderWidth/style`, complex `boxShadow`, and context selectors (`.bordered &`, `.has-bg-image &`). -## Approach +--- -1. **Audit the file** — catalogue every Tailwind class found; classify each as layout, theme-adaptive color, structural/interactive, or context-selector. +## §0.5 — System Architecture -2. **Plan primitive replacements** — map `flex`, `grid`, bare `div`/`span`/`p` wrappers to ``, ``, ``, `` with correct props using §3 of the instructions. +### Source files -3. **Prefer `asChild` over `.css.ts`** — for every element where you would write a new `.css.ts` export, first check whether the element is a 3rd-party or Headless UI component that accepts `className`. If it does, wrap it with a primitive using `asChild` instead: - ```tsx - // ✅ Preferred — no .css.ts export needed - - - +| Path | Purpose | +|------|---------| +| `src/system/colors.ts` | `colors` map (sprinkle values), `bg[n]` / `custom[n]` objects (for `style()` calls), `ColorToken` type | +| `src/system/vars.css.ts` | `vars` design tokens — `space`, `radii`, `fontSize`, `lineHeight`, `fontWeight` | +| `src/system/sprinkles.css.ts` | `commonProperties` + `themeColorProperties` + `commonSprinkles` | +| `src/system/types.ts` | `ThemeCondition`, `ThemeConditionProp`, all prop interfaces | +| `src/system/responsive.ts` | `responsiveConditions`, `normalizeResponsiveProp`, `ResponsiveProp` | +| `src/system/layout-utils.ts` | `getResponsiveLayoutStyles`, `resolveCommonSprinkleProps` | +| `src/system/index.ts` | Barrel — everything above re-exported | - // ❌ Only if primitive props cannot express the property - - ``` - Reserve `.css.ts` exports exclusively for properties primitives cannot express: `transition`, `cursor`, `border-width/style`, complex `box-shadow`, hover/focus interactive states, and context selectors (`.bordered &`, `.has-bg-image &`). +All imports from the system use the `@/system` alias. -4. **Apply the theme-color decision tree for every colored element:** - ``` - Is it a container primitive (Box/Flex/Grid/Container/Section)? - └─ YES → use `bg` prop: bg={{ base: 'bg-50', dark: 'bg-900' }} - use `shadow` prop for box-shadow - └─ NO → Is it a ? - └─ YES → use `color`/`bg` props: color={{ base: 'bg-500', dark: 'bg-50' }} - └─ NO → Can `asChild` on a primitive replace the wrapper? - └─ YES → use `asChild` - └─ NO → write a .css.ts using themeColorProperties sprinkle (Pattern B) - ``` +### `ThemeConditionProp` -5. **For components (`.tsx`)** — create or update the sibling `ComponentName.css.ts`: - - **Pattern A — `style()`**: for `transition`, `cursor`, `borderWidth`, complex shadows, and `.bordered &` / `.has-bg-image &` context selectors that sprinkles cannot express. - - **Pattern B — `themeColorProperties` sprinkle**: for theme-adaptive `backgroundColor`, `color`, or `borderColor` on non-primitive child elements. - ```ts - import { createSprinkles } from '@vanilla-extract/sprinkles' - import { themeColorProperties } from '@/system' - const sprinkles = createSprinkles(themeColorProperties) - export const title = sprinkles({ color: { base: 'bg-500', dark: 'bg-50' } }) - ``` +A union type that accepts either a plain value **or** a per-condition map: -6. **For stories (`.stories.tsx`)** — use primitives only; no `.css.ts`. +```ts +type ThemeConditionProp = T | Partial> +// ThemeCondition = 'base' | 'dark' | 'hover' | 'darkHover' | 'hasBgImage' | 'darkHasBgImage' +``` -7. **Apply edits** — replace layout `div`/`span` wrappers and `className` props with primitives; import `* as styles from './ComponentName.css'`; use `clsx()` for conditional class composition. +The six conditions map to selectors: -8. **Verify** — search the edited file for any remaining Tailwind class names and fix them. Run `get_errors` to confirm zero TypeScript errors. - -9. **Update `TAILWIND_MIGRATION.md`** — after `get_errors` passes, mark the migrated component(s) as `[x]` in `packages/lifeforge-ui/TAILWIND_MIGRATION.md` and update the progress summary table (Migrated count, Total, and % Done for the affected category row and the **Total** row). - -## Styling Categorisation Rules - -| Property type | Destination | -|---|---| -| `display`, `flex-direction`, `gap`, `padding`, `margin`, `width`, `height`, `overflow`, `position`, `inset` | Primitive prop | -| `background-color` on a container primitive | **`bg` prop** — `bg={{ base: 'bg-50', dark: 'bg-900' }}` | -| `background-color` on a non-primitive child element | **Pattern B** — `.css.ts` sprinkle | -| `color` on `` | **`color` prop** — `color={{ base: 'bg-500', dark: 'bg-50' }}` | -| `color` on a non-Text element | **Pattern B** — `.css.ts` sprinkle | -| `border-color` | **Pattern B** — `.css.ts` sprinkle | -| `border-radius` | **`rounded` prop** on container OR `borderRadius: vars.radii.*` in `.css.ts` | -| `box-shadow` (`shadow-custom`) | **`shadow` prop** on container primitive | -| `box-shadow` (complex/non-standard) | **Pattern A** — `.css.ts` `style()` | -| `transition`, `cursor`, `hover`, `focus`, `active` | **Pattern A** — `.css.ts` `style()` | -| Dark-mode variants | **`bg`/`color` prop** (preferred) OR **Pattern B** OR **Pattern A** selectors | -| Context selectors (`.bordered &`, `.has-bg-image &`) | **Pattern A** — `.css.ts` `style()` selectors | -| `font-weight`, `font-size` | `` props | -| Spacing not in token table | `style={{ ... }}` inline on the primitive | - -## `ThemeConditionProp` — condition keys - -All `bg`, `color`, and `borderColor` (sprinkle) props accept a per-condition map: - -| Key | Selector | -|-----|----------| -| `base` | (default) | +| Condition | Selector | +|-----------|----------| +| `base` | (default — no selector) | | `dark` | `.dark &` | | `hover` | `&:hover` | | `darkHover` | `.dark &:hover` | | `hasBgImage` | `.has-bg-image &` | | `darkHasBgImage` | `.dark .has-bg-image &` | +### `themeColorProperties` (from `@/system`) + +A sprinkle `defineProperties` block that applies the 6 conditions above to +`backgroundColor`, `color`, and `borderColor` — all keyed by `ColorToken` +(`'transparent' | 'bg-50'…'bg-950' | 'custom-50'…'custom-900'`). + +All container primitives compose this into their `createSprinkles(...)` call, +which is what enables the `bg` prop. Component `.css.ts` files can also compose +it when they need theme-adaptive per-element colors. + +### Color token helpers + +| Import | Use in | +|--------|--------| +| `colors` (from `@/system`) | Sprinkle property values in `defineProperties({ properties: { color: colors } })` | +| `bg[n]` (from `@/system`) | `style()` calls — e.g. `backgroundColor: bg[50]` | +| `custom[n]` (from `@/system`) | `style()` calls — e.g. `color: custom[500]` | +| `withOpacity(token, 0.1)` (from `@/system`) | opacity-modified colors — `withOpacity(bg[500], 0.2)` | + +> **NO raw hex strings or hardcoded `var(--color-*)` in `.css.ts` files.** +> Always use `bg[n]`, `custom[n]`, or `withOpacity(...)`. + +--- + +## §1 — Primitive Components + +Import from `@components/primitives`. + +### Container primitives — shared props + +All five container primitives (`Box`, `Flex`, `Grid`, `Container`, `Section`) +share these additional props beyond their own layout props: + +| Prop | Type | Description | +|------|------|-------------| +| `bg` | `ThemeConditionProp` | Theme-adaptive background color | +| `rounded` | `ResponsiveProp` | Border radius token | +| `shadow` | `boolean` | Adds `var(--custom-shadow)` box shadow | + +The `bg` prop accepts either a flat token or a per-condition map: + ```tsx - - +// flat — same in all conditions + + +// adaptive — different per theme condition + + + ``` -## Token helpers (`.css.ts` only) +### `Box` -| Need | Import from `@/system` | -|------|------------------------| -| Background by shade | `bg[50]` … `bg[950]` | -| Accent by shade | `custom[50]` … `custom[900]` | -| With opacity | `withOpacity(bg[500], 0.2)` | +General-purpose block element. Accepts all [Layout Props] + [Margin Props]. + +```tsx + + rounded="md" // border-radius token: 'none'|'sm'|'md'|'lg'|'xl'|'2xl'|'3xl'|'full' + shadow // adds var(--custom-shadow) + // Layout props (CSS strings, responsive): + width="100%" + minWidth="0" + maxWidth="48rem" + height="100%" + // Positioning: + position="relative" // 'static'|'relative'|'absolute'|'fixed'|'sticky' + inset="0" + top="0" right="0" bottom="0" left="0" + overflow="hidden" // 'visible'|'hidden'|'scroll'|'auto' + overflowX="auto" + overflowY="hidden" + // Grid-child props (CSS strings): + gridColumn="span 2 / span 2" + gridRow="span 2 / span 2" + flexBasis="0" flexGrow="1" flexShrink="0" + // Padding (SpaceToken): + p="md" px="lg" py="sm" pt="xs" pr="md" pb="sm" pl="xl" + // Margin (SpaceToken): + m="md" mx="auto" my="lg" mt="xs" mr="sm" mb="md" ml="xl" + className="extra-class" + style={{ /* arbitrary overrides */ }} +/> +``` + +### `Flex` + +Flexbox container. + +```tsx + +``` + +### `Grid` + +CSS Grid container. + +```tsx + +``` + +### `Text` + +Inline text/span. Renders as `` by default. + +```tsx + +``` + +> **`color` and `bg` on `Text` use `ThemeConditionProp`, not `ResponsiveProp`.** +> They respond to dark mode / hover conditions, not breakpoints. + +### `asChild` pattern + +All primitives support `asChild`. When set, the primitive merges its sprinkle classes and inline style variables onto the single child element instead of rendering a wrapper DOM node: + +```tsx +// ✅ Preferred — no extra DOM node, no .css.ts export needed + + + + +// ✅ Text asChild — applies color/weight to any element + + ... + +``` + +> **Important:** when using `asChild`, the child must accept and forward both `className` AND `style`. CSS variables from sprinkles are injected via the inline `style` prop — if the child does not forward `style`, theme-adaptive colors will silently fail. + +> **`asChild` is not optional.** It is the default choice for any non-primitive element that needs primitive-expressible styling. `.css.ts` is the last resort — only when `asChild` cannot physically express the required CSS property. + +### Responsive props + +All `SpaceToken` and most layout/size props accept a responsive object: + +```tsx + + +``` + +Breakpoints: `base` | `sm` (640px) | `md` (768px) | `lg` (1024px) | `xl` (1280px) | `2xl` (1536px) + +--- + +## §2 — Space Token Reference + +`--spacing` is Tailwind's default `0.25rem` per unit. + +| SpaceToken | Value | Tailwind equivalent | +|------------|-------|---------------------| +| `none` | `0` | `0` | +| `xs` | `calc(var(--spacing) * 1)` = 0.25rem | `1` | +| `sm` | `calc(var(--spacing) * 2)` = 0.5rem | `2` | +| `md` | `calc(var(--spacing) * 4)` = 1rem | `4` | +| `lg` | `calc(var(--spacing) * 6)` = 1.5rem | `6` | +| `xl` | `calc(var(--spacing) * 8)` = 2rem | `8` | +| `2xl` | `calc(var(--spacing) * 12)` = 3rem | `12` | +| `3xl` | `calc(var(--spacing) * 16)` = 4rem | `16` | + +Spacing values **not** in this table (e.g. `gap-3` = 0.75rem, arbitrary `mt-[30%]`) must use an inline `style` prop: + +```tsx + +``` + +--- + +## §3 — Common Tailwind → Primitive Mapping + +| Tailwind class | Primitive equivalent | +|---|---| +| `flex` | `` | +| `flex flex-col` | `` | +| `flex items-center justify-center` | `` | +| `flex items-center justify-between` | `` | +| `flex-center` (utility) | `` | +| `grid grid-cols-3` | `` | +| `gap-6` | `gap="lg"` | +| `p-16` | `p="3xl"` | +| `px-16` | `px="3xl"` | +| `w-full` | `width="100%"` | +| `h-full` | `height="100%"` | +| `min-w-0` | `minWidth="0"` | +| `min-w-64` | `minWidth="16rem"` | +| `size-full` | `width="100%" height="100%"` on `` or `` | +| `col-span-2` | `gridColumn="span 2 / span 2"` on a wrapping `` | +| `row-span-2` | `gridRow="span 2 / span 2"` on a wrapping `` | +| `shrink-0` | `flexShrink="0"` | +| `text-bg-500` | `` | +| `text-bg-500 dark:text-bg-50` | `` | +| `text-lg` | `` | +| `font-semibold` | `` | +| `truncate` | `` — **shorthand must come first** | +| `text-lg sm:text-xl` | `` | +| `overflow-hidden` | `overflow="hidden"` | +| `position-relative` | `position="relative"` | +| `mb-1` | `mb="xs"` | +| `p-2 sm:p-4` | `p={{ base: 'sm', sm: 'md' }}` | +| `bg-bg-50 dark:bg-bg-900` | `bg={{ base: 'bg-50', dark: 'bg-900' }}` on a container | +| `hover:bg-bg-100 dark:hover:bg-bg-800` | `bg={{ base: 'bg-50', dark: 'bg-900', hover: 'bg-100', darkHover: 'bg-800' }}` | +| `shadow-custom` | `shadow` on any container primitive | + +--- + +## §4 — Styling Categorisation + +| Property type | Destination | +|---|---| +| `display`, `flex-direction`, `gap`, `padding`, `margin`, `width`, `height`, `overflow`, `position`, `inset` | **Primitive prop** | +| `backgroundColor` of a container primitive | **`bg` prop** — `bg={{ base: 'bg-50', dark: 'bg-900' }}` | +| `backgroundColor` of a non-primitive child | **`asChild` on ``/``** (always first) — only use `.css.ts` sprinkle if `asChild` is impossible | +| `color` of a `` | **`color` prop** — `color={{ base: 'bg-500', dark: 'bg-50' }}` | +| `color` of a non-Text element | **`asChild` with ``** — `.css.ts` sprinkle only if the element cannot accept/forward `className` + `style` | +| `borderColor` | **`.css.ts` sprinkle** (Pattern B) | +| `borderRadius` | **`rounded` prop** on container OR `borderRadius: vars.radii.*` in `.css.ts` | +| `box-shadow` (`shadow-custom`) | **`shadow` prop** on container primitive | +| `box-shadow` (complex/non-standard) | **Pattern A** — `.css.ts` `style()` | +| `transition`, `cursor`, `hover`, `focus`, `active` | **Pattern A** — `.css.ts` `style()` | +| Dark-mode variants | **`bg`/`color` prop** (preferred) OR **Pattern B** OR **Pattern A** selectors | +| Context selectors (`.bordered &`, `.has-bg-image &`) | **Pattern A** — `.css.ts` `style()` selectors | +| `font-weight`, `font-size` | **``** props | +| Spacing not in token table | **`style={{ ... }}`** inline on the primitive | + +### Theme-adaptive color decision tree + +**Follow this tree strictly, top to bottom. Do not skip ahead to `.css.ts`.** + +``` +Is the element a container primitive (Box/Flex/Grid/Container/Section)? + └─ YES → use `bg` prop: + use `shadow` prop for box-shadow + └─ NO → Is it a ? + └─ YES → use `color`/`bg` props: + └─ NO → MANDATORY: try `asChild` on a primitive first. + Does the element accept and forward className + style? + └─ YES → use `asChild`. STOP. Do not write a .css.ts export. + └─ NO → only now may you write a .css.ts sprinkle (Pattern B) +``` + +> **If you find yourself writing a `.css.ts` export for color or background, stop and ask: could `asChild` have handled this? If yes, revert and use `asChild`.** + +--- + +## §5 — `.css.ts` Patterns + +### Pattern A: `style()` for structural/interactive-only styles + +Use for `boxShadow`, `transition`, `cursor`, `borderWidth`, and rules that depend on **parent context selectors** (`.bordered &`) where a sprinkle cannot be used. + +```ts +import { style } from '@vanilla-extract/css' +import { bg, custom, withOpacity, vars } from '@/system' + +export const wrapper = style({ + borderRadius: vars.radii.lg, + transition: 'all 0.2s', + backgroundColor: bg[50], + selectors: { + '.dark &': { backgroundColor: bg[900] }, + '.bordered &': { borderWidth: '2px', borderStyle: 'solid' } + } +}) +``` + +### Pattern B: `themeColorProperties` sprinkle for theme-adaptive colors on child elements + +When a **non-primitive element inside a component** needs adaptive `backgroundColor`, `color`, or `borderColor` across dark / hover / hasBgImage conditions: + +```ts +import { createSprinkles } from '@vanilla-extract/sprinkles' +import { style } from '@vanilla-extract/css' +import { themeColorProperties, vars } from '@/system' + +const sprinkles = createSprinkles(themeColorProperties) + +export const iconWrapper = style([ + { borderRadius: vars.radii.lg }, + sprinkles({ backgroundColor: { base: 'bg-100', dark: 'bg-800' } }) +]) + +export const titleText = sprinkles({ + color: { base: 'bg-500', dark: 'bg-50' } +}) + +export const optionInactive = sprinkles({ + color: { base: 'bg-500', hover: 'bg-800', darkHover: 'bg-50' } +}) +``` + +`themeColorProperties` supports: +- `backgroundColor: ColorToken | Partial>` +- `color: ColorToken | Partial>` +- `borderColor: ColorToken | Partial>` + +### Token helpers (`.css.ts` only) + +| Need | Code | +|------|------| +| Background color | `bg[50]` … `bg[950]` | +| Accent color | `custom[50]` … `custom[900]` | +| Color with opacity | `withOpacity(bg[500], 0.1)` | | Border radius | `vars.radii.sm/md/lg/xl/2xl/3xl/full` | | Space value | `vars.space.xs/sm/md/lg/xl/2xl/3xl` | -| Font size | `vars.fontSize.*` | -| Font weight | `vars.fontWeight.*` | +| Font size | `vars.fontSize.sm/base/lg/xl/.../9xl` | +| Font weight | `vars.fontWeight.normal/medium/semibold/bold` | +| Box shadow | `boxShadow: 'var(--custom-shadow)'` | -## Space Token Quick Reference +### Common Tailwind utility → `.css.ts` mapping -| SpaceToken | Tailwind | +| Tailwind utility | `.css.ts` equivalent | |---|---| -| `none` | `0` | -| `xs` | `1` (0.25rem) | -| `sm` | `2` (0.5rem) | -| `md` | `4` (1rem) | -| `lg` | `6` (1.5rem) | -| `xl` | `8` (2rem) | -| `2xl` | `12` (3rem) | -| `3xl` | `16` (4rem) | +| `component-bg` | `bg={{ base: 'bg-50', dark: 'bg-900' }}` on primitive (preferred) OR `backgroundColor: bg[50]` + `.dark &` selector | +| `component-bg-lighter` | `backgroundColor: bg[100]` + `.dark &` → `withOpacity(bg[800], 0.5)` | +| `shadow-custom` | `shadow` prop on primitive OR `boxShadow: 'var(--custom-shadow)'` | +| `border-bg-500/20` | `borderColor: withOpacity(bg[500], 0.2)` | +| `bg-bg-500/10` | `backgroundColor: withOpacity(bg[500], 0.1)` | +| `text-bg-500 dark:text-bg-50` | `` OR `sprinkles({ color: { base: 'bg-500', dark: 'bg-50' } })` in css.ts | +| `in-[.bordered]:border-2` | `selectors: { '.bordered &': { borderWidth: '2px', borderStyle: 'solid' } }` | +| `hover:bg-bg-100` | `bg={{ ..., hover: 'bg-100' }}` on primitive OR `sprinkles({ backgroundColor: { hover: 'bg-100' } })` | +| `transition-all` | `transition: 'all 0.2s'` | +| `rounded-lg` | `rounded="lg"` prop on primitive, or `borderRadius: vars.radii.lg` in `.css.ts` | +| `text-2xl sm:text-3xl` | `` | -Any Tailwind spacing value not in this table (e.g. `gap-3`, `mt-5`, `p-10`) must use an inline `style` prop with the raw `rem` value. +--- -## Preservation Checklist +## §6 — Workflow + +### Components (`.tsx` + `.css.ts`) + +1. **Audit the file** — catalogue every Tailwind class; classify as layout, theme-adaptive color, structural/interactive, or context-selector. +2. **Plan primitive replacements** — map `flex`/`grid`/`div`/`span` wrappers to ``/``/``/`` with correct props using §3. +3. **`asChild` is mandatory before `.css.ts`** — for every styled non-primitive element, you MUST evaluate `asChild` first. Only proceed to `.css.ts` if `asChild` is structurally impossible (element does not forward `className`/`style`, or the property is `transition`/`cursor`/context selector). +4. **Create `ComponentName.css.ts`** only for what genuinely cannot be expressed via primitives — one export per logical role, named by purpose. +5. **Import** with `import * as styles from './ComponentName.css'`; compose with `clsx()`. + +### Stories (`.stories.tsx`) + +- Use primitives only. No `.css.ts`. +- Replace every `
` with ``, every `
` wrapper with ``, every ``/`

` with ``. +- Move `col-span-*` / `row-span-*` from child `className` to a wrapping ``. +- Remove unused imports; add `Box`, `Flex`, `Grid`, `Text` from `@components/primitives`. + +### Prop ordering lint rule + +JSX props must be **alphabetical**, except: +- `ref` — always **first** +- Boolean shorthand props (e.g. `truncate`, `shadow`) — always **before** other props + +--- + +## §7 — Examples + +### Story: grid wrapper with spanning children + +```tsx +// Before +

+ +
+ +// After + + + + + +``` + +### Component: themed wrapper — bg on primitive + shadow prop + +```tsx +// Before +
+ +// Widget.css.ts — only what primitives can't express +import { style } from '@vanilla-extract/css' +import { withOpacity, bg } from '@/system' + +export const wrapper = style({ + borderColor: withOpacity(bg[500], 0.2), + selectors: { + '.bordered &': { borderWidth: '2px', borderStyle: 'solid' } + } +}) + +// After (Widget.tsx) — bg and shadow on primitive props + +``` + +### Component: child element theme colors — themeColorProperties sprinkle + +```tsx +// Before +
+

+ +// Widget.css.ts +import { createSprinkles } from '@vanilla-extract/sprinkles' +import { style } from '@vanilla-extract/css' +import { themeColorProperties, vars } from '@/system' + +const sprinkles = createSprinkles(themeColorProperties) + +export const iconWrapper = style([ + { borderRadius: vars.radii.lg }, + sprinkles({ backgroundColor: { base: 'bg-100', dark: 'bg-800' } }) +]) + +export const titleText = sprinkles({ + color: { base: 'bg-500', dark: 'bg-50' } +}) + +// After (Widget.tsx) + + +``` + +### Text with theme-adaptive color via prop (no .css.ts needed) + +```tsx +// Before + + +// After — ThemeConditionProp directly on Text + +``` + +### Interactive card — bg with hover/darkHover conditions + +```tsx +// Before +
+ +// Card.css.ts — only transition/cursor in style() +import { style } from '@vanilla-extract/css' +export const interactive = style({ cursor: 'pointer', transition: 'all 0.2s' }) + +// After (Card.tsx) + +``` + +### asChild: applying primitive props to a third-party component + +```tsx +// Before + + + + +// After — no new .css.ts needed + + + + + +``` + +--- + +## §8 — Preservation Checklist Before marking a file done, verify: @@ -149,6 +616,15 @@ Before marking a file done, verify: - [ ] Zero Tailwind class names remain in the file - [ ] `get_errors` reports zero TypeScript errors +--- + +## §9 — Completion Steps + +After `get_errors` passes: + +1. Mark the migrated component(s) as `[x]` in `packages/lifeforge-ui/TAILWIND_MIGRATION.md`. +2. Update the progress summary table — Migrated count, Total, and % Done for the affected category row and the **Total** row. + ## Output Format After completing a file migration, report: @@ -156,3 +632,4 @@ After completing a file migration, report: 2. Which `.css.ts` file(s) were created or updated. 3. A brief summary of any non-obvious mapping decisions (e.g. spacing values that needed `style={{}}`). 4. Confirmation that `get_errors` passed with zero errors. + diff --git a/.github/instructions/de-tailwind.instructions.md b/.github/instructions/de-tailwind.instructions.md deleted file mode 100644 index 75f2ba0a1..000000000 --- a/.github/instructions/de-tailwind.instructions.md +++ /dev/null @@ -1,609 +0,0 @@ ---- -applyTo: "packages/lifeforge-ui/src/**" ---- - -# De-Tailwind Instructions for lifeforge-ui - -## Overview - -Replace all Tailwind utility classes in components and stories with vanilla-extract -CSS-in-JS and the `lifeforge-ui` primitive components. The goal is zero Tailwind -in component/story source files while **preserving every pixel of styling logic**. - ---- - -## Step 0 — Read Before You Write - -Before touching any file, read: - -1. The **full component source** — understand every conditional class, every - dark-mode variant, every hover/focus state. -2. The **primitive component prop types** (see §1 below) — know exactly which - props are available so you don't reach for `style={}` when a prop exists. -3. The **space token table** (see §2 below) — map Tailwind spacing to tokens - before writing a single line. -4. The **system architecture** (see §0.5 below) — understand `ThemeConditionProp` - and `themeColorProperties` before writing any `.css.ts` file. - ---- - -## §0.5 — System Architecture - -### Source files - -| Path | Purpose | -|------|---------| -| `src/system/colors.ts` | `colors` map (sprinkle values), `bg[n]` / `custom[n]` objects (for `style()` calls), `ColorToken` type | -| `src/system/vars.css.ts` | `vars` design tokens — `space`, `radii`, `fontSize`, `lineHeight`, `fontWeight` | -| `src/system/sprinkles.css.ts` | `commonProperties` + `themeColorProperties` + `commonSprinkles` | -| `src/system/types.ts` | `ThemeCondition`, `ThemeConditionProp`, all prop interfaces | -| `src/system/responsive.ts` | `responsiveConditions`, `normalizeResponsiveProp`, `ResponsiveProp` | -| `src/system/layout-utils.ts` | `getResponsiveLayoutStyles`, `resolveCommonSprinkleProps` | -| `src/system/index.ts` | Barrel — everything above re-exported | - -All imports from the system use the `@/system` alias. - -### `ThemeConditionProp` - -A union type that accepts either a plain value **or** a per-condition map: - -```ts -type ThemeConditionProp = T | Partial> -// ThemeCondition = 'base' | 'dark' | 'hover' | 'darkHover' | 'hasBgImage' | 'darkHasBgImage' -``` - -The six conditions map to selectors: - -| Condition | Selector | -|-----------|----------| -| `base` | (default — no selector) | -| `dark` | `.dark &` | -| `hover` | `&:hover` | -| `darkHover` | `.dark &:hover` | -| `hasBgImage` | `.has-bg-image &` | -| `darkHasBgImage` | `.dark .has-bg-image &` | - -### `themeColorProperties` (from `@/system`) - -A sprinkle `defineProperties` block that applies the 6 conditions above to -`backgroundColor`, `color`, and `borderColor` — all keyed by `ColorToken` -(`'transparent' | 'bg-50'…'bg-950' | 'custom-50'…'custom-900'`). - -All container primitives compose this into their `createSprinkles(...)` call, -which is what enables the `bg` prop. Component `.css.ts` files can also compose -it when they need theme-adaptive per-element colors. - -### Color token helpers - -| Import | Use in | -|--------|--------| -| `colors` (from `@/system`) | Sprinkle property values in `defineProperties({ properties: { color: colors } })` | -| `bg[n]` (from `@/system`) | `style()` calls — e.g. `backgroundColor: bg[50]` | -| `custom[n]` (from `@/system`) | `style()` calls — e.g. `color: custom[500]` | -| `withOpacity(token, 0.1)` (from `@/system`) | opacity-modified colors — `withOpacity(bg[500], 0.2)` | - -> **NO raw hex strings or hardcoded `var(--color-*)` in `.css.ts` files.** -> Always use `bg[n]`, `custom[n]`, or `withOpacity(...)`. - ---- - -## §1 — Primitive Components - -Import from `@components/primitives`. - -### Container primitives — shared props - -All five container primitives (`Box`, `Flex`, `Grid`, `Container`, `Section`) -share these additional props beyond their own layout props: - -| Prop | Type | Description | -|------|------|-------------| -| `bg` | `ThemeConditionProp` | Theme-adaptive background color | -| `rounded` | `ResponsiveProp` | Border radius token | -| `shadow` | `boolean` | Adds `var(--custom-shadow)` box shadow | - -The `bg` prop accepts either a flat token or a per-condition map: - -```tsx -// flat — same in all conditions - - -// adaptive — different per theme condition - - - -``` - -### `Box` - -General-purpose block element. Accepts all [Layout Props] + [Margin Props]. - -```tsx - - rounded="md" // border-radius token: 'none'|'sm'|'md'|'lg'|'xl'|'2xl'|'3xl'|'full' - shadow // adds var(--custom-shadow) - // Layout props (CSS strings, responsive): - width="100%" - minWidth="0" - maxWidth="48rem" - height="100%" - // Positioning: - position="relative" // 'static'|'relative'|'absolute'|'fixed'|'sticky' - inset="0" - top="0" right="0" bottom="0" left="0" - overflow="hidden" // 'visible'|'hidden'|'scroll'|'auto' - overflowX="auto" - overflowY="hidden" - // Grid-child props (CSS strings): - gridColumn="span 2 / span 2" - gridRow="span 2 / span 2" - flexBasis="0" flexGrow="1" flexShrink="0" - // Padding (SpaceToken): - p="md" px="lg" py="sm" pt="xs" pr="md" pb="sm" pl="xl" - // Margin (SpaceToken): - m="md" mx="auto" my="lg" mt="xs" mr="sm" mb="md" ml="xl" - className="extra-class" - style={{ /* arbitrary overrides */ }} -/> -``` - -### `Flex` - -Flexbox container. - -```tsx - -``` - -### `Grid` - -CSS Grid container. - -```tsx - -``` - -### `Text` - -Inline text/span. Renders as `` by default. - -```tsx - -``` - -> **`color` and `bg` on `Text` use `ThemeConditionProp`, not `ResponsiveProp`.** -> They respond to dark mode / hover conditions, not breakpoints. - -### Responsive props - -All `SpaceToken` and most layout/size props accept a responsive object: - -```tsx - - -``` - -Breakpoints: `base` | `sm` (640px) | `md` (768px) | `lg` (1024px) | `xl` (1280px) | `2xl` (1536px) - ---- - -## §2 — Space Token Reference - -`--spacing` is Tailwind's default `0.25rem` per unit. - -| SpaceToken | Value | Tailwind equivalent | -|------------|------------------------|-----------------------| -| `none` | `0` | `0` | -| `xs` | `calc(var(--spacing) * 1)` = 0.25rem | `1` | -| `sm` | `calc(var(--spacing) * 2)` = 0.5rem | `2` | -| `md` | `calc(var(--spacing) * 4)` = 1rem | `4` | -| `lg` | `calc(var(--spacing) * 6)` = 1.5rem | `6` | -| `xl` | `calc(var(--spacing) * 8)` = 2rem | `8` | -| `2xl` | `calc(var(--spacing) * 12)` = 3rem | `12` | -| `3xl` | `calc(var(--spacing) * 16)` = 4rem | `16` | - -Spacing values **not** in the token table (e.g. `gap-3` = 0.75rem, arbitrary -`mt-[30%]`) must use an inline `style` prop: - -```tsx - -``` - ---- - -## §3 — Common Tailwind → Primitive Mapping - -| Tailwind class | Primitive equivalent | -|------------------------------------|----------------------------------------------------------| -| `flex` | `` | -| `flex flex-col` | `` | -| `flex items-center justify-center` | `` | -| `flex items-center justify-between`| `` | -| `flex-center` (utility) | `` | -| `grid grid-cols-3` | `` | -| `gap-6` | `gap="lg"` | -| `p-16` | `p="3xl"` | -| `px-16` | `px="3xl"` | -| `w-full` | `width="100%"` | -| `h-full` | `height="100%"` | -| `min-w-0` | `minWidth="0"` | -| `min-w-64` | `minWidth="16rem"` | -| `size-full` | `width="100%" height="100%"` on a `` or `` | -| `col-span-2` | `gridColumn="span 2 / span 2"` on a wrapping `` | -| `row-span-2` | `gridRow="span 2 / span 2"` on a wrapping `` | -| `shrink-0` | `flexShrink="0"` | -| `text-bg-500` | `` | -| `text-bg-500 dark:text-bg-50` | `` | -| `text-lg` | `` | -| `font-semibold` | `` | -| `truncate` | `` — **shorthand must come first** | -| `text-lg sm:text-xl` | `` | -| `overflow-hidden` | `overflow="hidden"` | -| `position-relative` | `position="relative"` | -| `mb-1` | `mb="xs"` | -| `p-2 sm:p-4` | `p={{ base: 'sm', sm: 'md' }}` | -| `bg-bg-50 dark:bg-bg-900` | `bg={{ base: 'bg-50', dark: 'bg-900' }}` on a container | -| `hover:bg-bg-100 dark:hover:bg-bg-800` | `bg={{ base: 'bg-50', dark: 'bg-900', hover: 'bg-100', darkHover: 'bg-800' }}` | -| `shadow-custom` | `shadow` on any container primitive | - ---- - -## §4 — De-Tailwinding Stories (`.stories.tsx`) - -**Rule: Use primitives only. No `.css.ts` needed.** - -### Checklist - -- [ ] Replace every `
` with `` + corresponding props -- [ ] Replace every `
` with `` + corresponding props -- [ ] Replace every `
` wrapper (no layout) with `` -- [ ] Replace `

` with `` -- [ ] Replace `` with `` -- [ ] Move `col-span-*` / `row-span-*` from the child component `className` to a - wrapping `` around it -- [ ] Spacing not in token table → `style={{ ... }}` -- [ ] Remove the `tailwindcss/colors` import if `COLORS` is only used for story - data (keep it if it feeds `iconColor` props etc.) -- [ ] Update imports: add `Box`, `Flex`, `Grid`, `Text` from `@components/primitives`, - remove any now-unused imports - -### Prop ordering lint rules - -JSX props must be **alphabetical**, except: -- `ref` — always **first** -- Boolean shorthand props (e.g. `truncate`) — always **before** other props - ---- - -## §5 — De-Tailwinding Components (`.tsx` + `.css.ts`) - -**Rule: Use primitives for layout structure; create `ComponentName.css.ts` for -all theming, color, shadow, hover, dark-mode, and state-variant styles.** - -### Decision tree — which approach for theme-adaptive colors? - -``` -Is the element a container primitive (Box/Flex/Grid/Container/Section)? - └─ YES → use the `bg` prop: - use `shadow` prop for box-shadow - └─ NO → Is it a ? - └─ YES → use `color`/`bg` props: - └─ NO → write a .css.ts using themeColorProperties sprinkle (see below) -``` - -### Workflow - -1. **Identify styling categories** in the component: - - *Layout* (display, flex/grid, gap, padding, margin, width, height, - overflow, position) → **primitive props** - - *Adaptive background/color/border* on a container primitive → **`bg` prop** or **`shadow` prop** - - *Adaptive background/color/border* on a non-primitive element → **`.css.ts` sprinkle** - - *Interactive* (hover, focus, active, transition) not coverable by primitive props → **`.css.ts`** - - *Context selectors* (`.bordered &`, `.has-bg-image &`) → **`.css.ts`** - - *Responsive font sizes / padding* that map to tokens → **responsive primitive props** - -2. **Create `ComponentName.css.ts`** next to the component file only when needed. - -3. **Export one `style()` or `sprinkles(...)` class per logical role** — name by - purpose, not by Tailwind class names. - -4. **Import and use** with `import * as styles from './ComponentName.css'` and - `className={clsx(styles.foo, condition && styles.bar)}`. - -### `.css.ts` — two patterns - -#### Pattern A: `style()` for structural/interactive-only styles - -Use for `boxShadow`, `transition`, `cursor`, `borderWidth`, and rules that -depend on **parent context selectors** (`.bordered &`) where a sprinkle cannot -be used. - -```ts -import { style } from '@vanilla-extract/css' -import { bg, custom, withOpacity } from '@/system' -import { vars } from '@/system' - -export const wrapper = style({ - // Structural / non-theme - borderRadius: vars.radii.lg, - transition: 'all 0.2s', - // Theme-adaptive via selectors (only when themeColorProperties sprinkle can't be used) - backgroundColor: bg[50], - selectors: { - '.dark &': { backgroundColor: bg[900] }, - '.bordered &': { borderWidth: '2px', borderStyle: 'solid' } - } -}) -``` - -#### Pattern B: `themeColorProperties` sprinkle for theme-adaptive colors on child elements - -When a **non-primitive element inside a component** needs adaptive `backgroundColor`, -`color`, or `borderColor` across dark / hover / hasBgImage conditions, create a -local sprinkle from `themeColorProperties`: - -```ts -import { createSprinkles } from '@vanilla-extract/sprinkles' -import { style } from '@vanilla-extract/css' -import { themeColorProperties } from '@/system' - -const sprinkles = createSprinkles(themeColorProperties) - -// Then call sprinkles() directly as a className value: -export const iconWrapper = style([ - { borderRadius: vars.radii.lg }, - sprinkles({ backgroundColor: { base: 'bg-100', dark: 'bg-800' } }) -]) - -export const titleText = sprinkles({ - color: { base: 'bg-500', dark: 'bg-50' } -}) - -export const optionInactive = sprinkles({ - color: { base: 'bg-500', hover: 'bg-800', darkHover: 'bg-50' } -}) -``` - -`themeColorProperties` supports: -- `backgroundColor: ColorToken | Partial>` -- `color: ColorToken | Partial>` -- `borderColor: ColorToken | Partial>` - -### Token helpers - -| Need | Code | -|-----------------------------|-------------------------------------------| -| Background color (style) | `bg[50]` … `bg[950]` | -| Accent color (style) | `custom[50]` … `custom[900]` | -| Color with opacity (style) | `withOpacity(bg[500], 0.1)` | -| Border radius | `vars.radii.sm/md/lg/xl/2xl/3xl/full` | -| Space value | `vars.space.xs/sm/md/lg/xl/2xl/3xl` | -| Font size | `vars.fontSize.sm/base/lg/xl/.../9xl` | -| Font weight | `vars.fontWeight.normal/medium/semibold/bold` | -| Box shadow | `boxShadow: 'var(--custom-shadow)'` | - -### Common Tailwind utility → `.css.ts` mapping - -| Tailwind utility | `.css.ts` equivalent | -|-----------------------------|--------------------------------------------------------------| -| `component-bg` | `bg={{ base: 'bg-50', dark: 'bg-900' }}` on primitive (preferred) OR `backgroundColor: bg[50]` + `.dark &` selector | -| `component-bg-lighter` | `backgroundColor: bg[100]` + `.dark &` → `withOpacity(bg[800], 0.5)` | -| `shadow-custom` | `shadow` prop on primitive OR `boxShadow: 'var(--custom-shadow)'` | -| `border-bg-500/20` | `borderColor: withOpacity(bg[500], 0.2)` | -| `bg-bg-500/10` | `backgroundColor: withOpacity(bg[500], 0.1)` | -| `text-bg-500 dark:text-bg-50` | `` OR `sprinkles({ color: { base: 'bg-500', dark: 'bg-50' } })` in css.ts | -| `in-[.bordered]:border-2` | `selectors: { '.bordered &': { borderWidth: '2px', borderStyle: 'solid' } }` | -| `hover:bg-bg-100` | `bg={{ ..., hover: 'bg-100' }}` on primitive OR `sprinkles({ backgroundColor: { hover: 'bg-100' } })` | -| `transition-all` | `transition: 'all 0.2s'` | -| `rounded-lg` | `rounded="lg"` prop on primitive, or `borderRadius: vars.radii.lg` in `.css.ts` | -| `text-2xl sm:text-3xl` | `` | - -### What stays in `.css.ts` vs moves to primitive props - -| Property | Where | -|----------|-------| -| `display`, `flexDirection`, `gap`, `padding`, `margin`, `width`, `height`, `overflow`, `position` | **Primitive prop** | -| `backgroundColor` of a container primitive | **`bg` prop** (ThemeConditionProp) | -| `backgroundColor` of a non-primitive child | **`.css.ts` sprinkle** (Pattern B) | -| `color` of a `` | **`color` prop** (ThemeConditionProp) | -| `color` of a non-Text element | **`.css.ts` sprinkle** (Pattern B) | -| `borderColor` | **`.css.ts` sprinkle** (Pattern B) | -| `borderRadius` | **`rounded` prop** on container, or `.css.ts` for non-primitive | -| `boxShadow` of a container primitive | **`shadow` prop** | -| `boxShadow` (complex / non-standard) | **`.css.ts`** `style()` | -| `transition`, `cursor`, `hover`, `focus` | **`.css.ts`** `style()` | -| Dark-mode selectors | **`.css.ts`** `style()` selectors OR `themeColorProperties` sprinkle | -| Context selectors (`.bordered &`, `.has-bg-image &`) | **`.css.ts`** `style()` selectors | -| `fontWeight`, `fontSize` | **``** | - ---- - -## §6 — Preservation Checklist - -Before calling done, verify every item from the original: - -- [ ] All conditional class logic is reproduced (variant props, boolean flags) -- [ ] All dark-mode styles are present (`.dark &` selectors in `.css.ts`) -- [ ] All responsive styles are present (`@media` blocks or responsive props) -- [ ] All hover/focus/active states are present -- [ ] All context-selector styles are present (`.bordered &`, `.has-bg-image &`) -- [ ] `iconColor` / dynamic inline styles that can't be tokenised are still - passed as `style={{ backgroundColor: ... }}` on the relevant primitive -- [ ] No Tailwind utility classes remain in the file (search for `className="` - containing any bare Tailwind class names) -- [ ] `get_errors` reports zero TypeScript errors after all edits - ---- - -## §7 — Examples - -### Story: grid wrapper with spanning children - -```tsx -// Before -

- -
- -// After - - - - - -``` - -### Component: themed wrapper div — bg on primitive + shadow prop - -```tsx -// Before -
- -// Widget.css.ts — only what primitives can't express -import { style } from '@vanilla-extract/css' -import { withOpacity, bg } from '@/system' - -export const wrapper = style({ - borderColor: withOpacity(bg[500], 0.2), - selectors: { - '.bordered &': { borderWidth: '2px', borderStyle: 'solid' } - } -}) - -// After (Widget.tsx) — bg and shadow handled by primitive props - -``` - -### Component: child element theme colors — themeColorProperties sprinkle - -```tsx -// Before -
-

- -// Widget.css.ts -import { createSprinkles } from '@vanilla-extract/sprinkles' -import { style } from '@vanilla-extract/css' -import { themeColorProperties, bg, withOpacity, vars } from '@/system' - -const sprinkles = createSprinkles(themeColorProperties) - -export const iconWrapper = style([ - { borderRadius: vars.radii.lg }, - sprinkles({ backgroundColor: { base: 'bg-100', dark: 'bg-800' } }) -]) - -export const titleText = sprinkles({ - color: { base: 'bg-500', dark: 'bg-50' } -}) - -// After (Widget.tsx) - - -``` - -### Text with theme-adaptive color via prop (no .css.ts needed) - -```tsx -// Before - - -// After — ThemeConditionProp directly on Text - -``` - -### Interactive card — bg with hover/darkHover conditions - -```tsx -// Before -
- -// Card.css.ts — only transition/cursor remain in style() -import { style } from '@vanilla-extract/css' -export const interactive = style({ cursor: 'pointer', transition: 'all 0.2s' }) - -// After (Card.tsx) - -``` diff --git a/packages/lifeforge-ui/TAILWIND_MIGRATION.md b/packages/lifeforge-ui/TAILWIND_MIGRATION.md index 975fe9eaf..9ec785182 100644 --- a/packages/lifeforge-ui/TAILWIND_MIGRATION.md +++ b/packages/lifeforge-ui/TAILWIND_MIGRATION.md @@ -62,20 +62,20 @@ Components are considered **migrated** when they contain zero Tailwind utility c - [x] `ComboboxInput` - [x] `CurrencyInput` - [x] `DateInput` -- [ ] `FAB` +- [x] `FAB` - [x] `FileInput` - [x] `IconInput` - [ ] `Listbox` - [x] `ListboxInput` - [ ] `LocationInput` - [x] `NumberInput` -- [ ] `QRCodeScanner` +- [x] `QRCodeScanner` - [x] `RRuleInput` -- [ ] `SearchInput` -- [ ] `SliderInput` +- [x] `SearchInput` +- [x] `SliderInput` - [x] `Switch` - [ ] `TagsInput` -- [ ] `TextAreaInput` +- [x] `TextAreaInput` - [x] `TextInput` - [x] `TextInput/components/TextInputBox` @@ -127,8 +127,8 @@ Components are considered **migrated** when they contain zero Tailwind utility c | Auth | 0 | 7 | 0% | | Data Display | 8 | 8 | 100% | | Feedback | 5 | 5 | 100% | -| Inputs | 13 | 22 | 59% | +| Inputs | 18 | 22 | 82% | | Layout | 5 | 5 | 100% | | Navigation | 3 | 9 | 33% | | Overlays | 5 | 9 | 56% | -| **Total** | **46** |**73** | **63%**| +| **Total** | **51** |**73** | **70%**| diff --git a/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorInput.css.ts b/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorInput.css.ts new file mode 100644 index 000000000..f983f3af0 --- /dev/null +++ b/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorInput.css.ts @@ -0,0 +1,10 @@ +import { style } from '@vanilla-extract/css' + +import { bg } from '@/system' + +export const colorDot = style({ + selectors: { + '.group:focus-within &': { borderColor: bg[400] }, + '.dark .group:focus-within &': { borderColor: bg[700] } + } +}) diff --git a/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorInput.stories.tsx b/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorInput.stories.tsx index dfbce52b3..dd701cc3e 100644 --- a/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorInput.stories.tsx +++ b/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorInput.stories.tsx @@ -21,6 +21,21 @@ export const Default: Story = { render: args => { const [color, setColor] = useState('') - return + return + } +} + +export const PlainVariant: Story = { + args: { + label: 'Cube Color', + value: '', + namespace: 'namespace', + variant: 'plain' + }, + + render: args => { + const [color, setColor] = useState('') + + return } } diff --git a/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorInput.tsx b/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorInput.tsx index b966c5db5..ca2ef8b19 100644 --- a/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorInput.tsx +++ b/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorInput.tsx @@ -1,15 +1,16 @@ -import clsx from 'clsx' import { useRef } from 'react' import { useModalStore } from '@components/overlays' -import { Flex } from '@components/primitives' +import { Bordered, Box, Flex, Text } from '@components/primitives' import InputActionButton from '../shared/components/InputActionButton' import InputIcon from '../shared/components/InputIcon' import InputLabel from '../shared/components/InputLabel' import InputWrapper from '../shared/components/InputWrapper' +import Placeholder from '../shared/components/Placeholder' import useInputLabel from '../shared/hooks/useInputLabel' import { autoFocusableRef } from '../shared/utils/autoFocusableRef' +import { colorDot } from './ColorInput.css' import ColorPickerModal from './ColorPickerModal' interface ColorInputProps { @@ -68,7 +69,7 @@ function ColorInput({ icon="tabler:palette" /> )} - + {variant === 'classic' && label && ( )} -
-
- { - onChange(e.target.value.trim().toUpperCase()) - }} - onChange={e => { - onChange(e.target.value) - }} - /> -
- { - open(ColorPickerModal, { - value, - onChange - }) - }} - /> + + + + + + + { + onChange(e.target.value.trim().toUpperCase()) + }} + onChange={e => { + onChange(e.target.value) + }} + /> + + + + + { + open(ColorPickerModal, { + value, + onChange + }) + }} + /> ) } diff --git a/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/PaletteButtons.css.ts b/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/PaletteButtons.css.ts new file mode 100644 index 000000000..78f240ac2 --- /dev/null +++ b/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/PaletteButtons.css.ts @@ -0,0 +1,10 @@ +import { style } from '@vanilla-extract/css' + +export const tailwindButton = style({ + backgroundColor: 'var(--color-teal-500) !important', + selectors: { + '&:hover': { + backgroundColor: 'var(--color-teal-600) !important' + } + } +}) diff --git a/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/PaletteButtons.tsx b/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/PaletteButtons.tsx index e67b0d8ef..f2a4d3a96 100644 --- a/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/PaletteButtons.tsx +++ b/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/PaletteButtons.tsx @@ -1,7 +1,9 @@ import { useCallback } from 'react' import { Button, useModalStore } from '../../../../..' +import { Box, Flex } from '../../../../../components/primitives' import { useColorPickerModalStore } from '../stores/useColorPickerModalStore' +import * as styles from './PaletteButtons.css' import FlatUIColorsModal from './modals/FlatUIColorsModal' import MorandiColorPaletteModal from './modals/ModandiColorPaletteModal' import TailwindCSSColorsModal from './modals/TailwindCSSColorsModal' @@ -28,35 +30,38 @@ function PaletteButtons() { ) return ( -
- - - -
+ + + + + + + + + + + ) } diff --git a/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/modals/FlatUIColorsModal/FlatUIColorsModal.css.ts b/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/modals/FlatUIColorsModal/FlatUIColorsModal.css.ts new file mode 100644 index 000000000..185dfd160 --- /dev/null +++ b/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/modals/FlatUIColorsModal/FlatUIColorsModal.css.ts @@ -0,0 +1,26 @@ +import { style } from '@vanilla-extract/css' + +import { bg, withOpacity } from '@/system' + +export const card = style({ + selectors: { + '.dark &': { + backgroundColor: withOpacity(bg[800], 0.7) + ' !important' + } + } +}) + +export const colorButton = style({ + boxShadow: 'var(--custom-shadow)', + aspectRatio: '1', + cursor: 'pointer' +}) + +export const colorButtonSelected = style({ + boxShadow: `0 0 0 2px ${bg[100]}, 0 0 0 4px ${bg[900]}, var(--custom-shadow)`, + selectors: { + '.dark &': { + boxShadow: `0 0 0 2px ${bg[900]}, 0 0 0 4px ${bg[50]}, var(--custom-shadow)` + } + } +}) diff --git a/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/modals/FlatUIColorsModal/index.tsx b/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/modals/FlatUIColorsModal/index.tsx index f8dd7a250..b81383ba8 100644 --- a/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/modals/FlatUIColorsModal/index.tsx +++ b/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/modals/FlatUIColorsModal/index.tsx @@ -4,7 +4,9 @@ import tinycolor from 'tinycolor2' import { Card } from '@components/layout' import { ModalHeader } from '@components/overlays' +import { Box, Flex, Grid, Text } from '@components/primitives' +import * as styles from './FlatUIColorsModal.css' import PALETTES from './constants/palettes.json' function FlatUIColorsModal({ @@ -18,51 +20,72 @@ function FlatUIColorsModal({ } }) { return ( -
+ -
+ {PALETTES.map(({ name, icon, colors }) => ( - -
- - {name} -
-
+ + + + + {name} + + + {colors.map((flatUiColor, index) => ( - + + ))} -
+
))} -
-
+ + ) } diff --git a/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/modals/ModandiColorPaletteModal/MorandiColorPaletteModal.css.ts b/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/modals/ModandiColorPaletteModal/MorandiColorPaletteModal.css.ts new file mode 100644 index 000000000..6bb3db13e --- /dev/null +++ b/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/modals/ModandiColorPaletteModal/MorandiColorPaletteModal.css.ts @@ -0,0 +1,18 @@ +import { style } from '@vanilla-extract/css' + +import { bg } from '@/system' + +export const colorButton = style({ + boxShadow: 'var(--custom-shadow)', + aspectRatio: '1', + cursor: 'pointer' +}) + +export const colorButtonSelected = style({ + boxShadow: `0 0 0 2px ${bg[100]}, 0 0 0 4px ${bg[900]}, var(--custom-shadow)`, + selectors: { + '.dark &': { + boxShadow: `0 0 0 2px ${bg[900]}, 0 0 0 4px ${bg[50]}, var(--custom-shadow)` + } + } +}) diff --git a/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/modals/ModandiColorPaletteModal/index.tsx b/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/modals/ModandiColorPaletteModal/index.tsx index 6209cea32..b524d442f 100644 --- a/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/modals/ModandiColorPaletteModal/index.tsx +++ b/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/modals/ModandiColorPaletteModal/index.tsx @@ -4,7 +4,9 @@ import { sortFn } from 'color-sorter' import tinycolor from 'tinycolor2' import { ModalHeader } from '@components/overlays' +import { Box, Flex, Grid } from '@components/primitives' +import * as styles from './MorandiColorPaletteModal.css' import { MORANDI_COLORS } from './constants/morandi_colors' function MorandiColorPaletteModal({ @@ -18,42 +20,56 @@ function MorandiColorPaletteModal({ onClose: () => void }) { return ( -
+ -
+ {MORANDI_COLORS.sort(sortFn).map((morandiColor, index) => ( - + + ))} -
-
+ + ) } diff --git a/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/modals/TailwindCSSColorsModal/TailwindCSSColorsModal.css.ts b/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/modals/TailwindCSSColorsModal/TailwindCSSColorsModal.css.ts new file mode 100644 index 000000000..774d1ac20 --- /dev/null +++ b/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/modals/TailwindCSSColorsModal/TailwindCSSColorsModal.css.ts @@ -0,0 +1,15 @@ +import { style } from '@vanilla-extract/css' + +import { vars } from '@/system' + +export const colorGroupLabel = style({ + marginTop: vars.space.md, + marginBottom: vars.space.md, + width: '7rem', + textAlign: 'left', + '@media': { + 'screen and (min-width: 640px)': { + marginBottom: vars.space.sm + } + } +}) diff --git a/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/modals/TailwindCSSColorsModal/components/ColorItem.css.ts b/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/modals/TailwindCSSColorsModal/components/ColorItem.css.ts new file mode 100644 index 000000000..6bb3db13e --- /dev/null +++ b/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/modals/TailwindCSSColorsModal/components/ColorItem.css.ts @@ -0,0 +1,18 @@ +import { style } from '@vanilla-extract/css' + +import { bg } from '@/system' + +export const colorButton = style({ + boxShadow: 'var(--custom-shadow)', + aspectRatio: '1', + cursor: 'pointer' +}) + +export const colorButtonSelected = style({ + boxShadow: `0 0 0 2px ${bg[100]}, 0 0 0 4px ${bg[900]}, var(--custom-shadow)`, + selectors: { + '.dark &': { + boxShadow: `0 0 0 2px ${bg[900]}, 0 0 0 4px ${bg[50]}, var(--custom-shadow)` + } + } +}) diff --git a/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/modals/TailwindCSSColorsModal/components/ColorItem.tsx b/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/modals/TailwindCSSColorsModal/components/ColorItem.tsx index da9c7e901..af01f8eee 100644 --- a/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/modals/TailwindCSSColorsModal/components/ColorItem.tsx +++ b/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/modals/TailwindCSSColorsModal/components/ColorItem.tsx @@ -4,6 +4,10 @@ import { converter, formatHex, parse } from 'culori' import { memo, useMemo } from 'react' import tinycolor from 'tinycolor2' +import { Box, Flex, Text } from '@components/primitives' + +import * as styles from './ColorItem.css' + function ColorItem({ name, value, @@ -21,29 +25,37 @@ function ColorItem({ ) return ( -
  • - -

    {name}

    - {colorHex} -
  • + + + + + + {name} + + + {colorHex} + + ) } diff --git a/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/modals/TailwindCSSColorsModal/index.tsx b/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/modals/TailwindCSSColorsModal/index.tsx index c6ce7fc52..2c6a2ee3b 100644 --- a/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/modals/TailwindCSSColorsModal/index.tsx +++ b/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/components/modals/TailwindCSSColorsModal/index.tsx @@ -1,7 +1,9 @@ import colors from 'tailwindcss/colors' import { ModalHeader } from '@components/overlays' +import { Box, Flex, Grid, Text } from '@components/primitives' +import * as styles from './TailwindCSSColorsModal.css' import ColorItem from './components/ColorItem' function TailwindCSSColorsModal({ @@ -15,13 +17,13 @@ function TailwindCSSColorsModal({ onClose: () => void }) { return ( -
    + -
    + {([...Object.keys(colors)] as Array) .filter( colorGroup => @@ -35,13 +37,21 @@ function TailwindCSSColorsModal({ ].includes(colorGroup) ) .map((colorGroup, index) => ( -
    -

    + + {colorGroup[0].toUpperCase() + colorGroup.slice(1)} -

    -
      + {Object.entries( colors[colorGroup] as Record @@ -57,11 +67,11 @@ function TailwindCSSColorsModal({ }} /> ))} -
    -
    + +
    ))} -
    -
    + + ) } diff --git a/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/index.css b/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/index.css index 0d21412c3..08687065d 100644 --- a/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/index.css +++ b/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/index.css @@ -1,16 +1,35 @@ .w-color-editable-input { - @apply px-0!; + padding-left: 0 !important; + padding-right: 0 !important; +} + +.w-color-colorful { + width: 100% !important; } .w-color-editable-input input { font-size: 1rem !important; font-weight: 500 !important; letter-spacing: 0.1rem !important; - @apply bg-bg-50! dark:bg-bg-800! shadow-custom! text-bg-800! dark:text-bg-100! mb-2 rounded-md! py-2! text-center focus:outline-hidden; + background-color: var(--color-bg-50) !important; + box-shadow: var(--custom-shadow) !important; + color: var(--color-bg-800) !important; + margin-bottom: 0.5rem !important; + border-radius: var(--radius-md) !important; + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; + text-align: center; + outline: none; +} - &.hex { - color: rgb(244 244 245) !important; - } +.dark .w-color-editable-input input { + background-color: var(--color-bg-800) !important; + color: var(--color-bg-100) !important; +} + +.w-color-editable-input.hex input { + color: var(--editable-input-color) !important; + background-color: var(--editable-input-bg) !important; } .w-color-editable-input span { diff --git a/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/index.tsx b/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/index.tsx index 26d961f12..fa73064c1 100644 --- a/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/index.tsx +++ b/packages/lifeforge-ui/src/components/inputs/ColorInput/ColorPickerModal/index.tsx @@ -4,6 +4,7 @@ import tinycolor from 'tinycolor2' import { Button } from '@components/inputs' import { ModalHeader } from '@components/overlays' +import { Box, Flex } from '@components/primitives' import PaletteButtons from './components/PaletteButtons' import { useColorPickerModalStore } from './stores/useColorPickerModalStore' @@ -50,115 +51,112 @@ function ColorPickerModal({ }, [value]) return ( -
    + - -