diff --git a/apps/api/src/core/functions/routes/utils/response.ts b/apps/api/src/core/functions/routes/utils/response.ts index 11b5c3d6a..f24a9982e 100644 --- a/apps/api/src/core/functions/routes/utils/response.ts +++ b/apps/api/src/core/functions/routes/utils/response.ts @@ -18,14 +18,6 @@ export function clientError({ }) { const logger = createLogger({ name: moduleName || 'unknown-module' }) - fs.readdirSync('medium').forEach(file => { - if (fs.statSync('medium/' + file).isFile()) { - fs.unlinkSync('medium/' + file) - } else { - fs.rmdirSync('medium/' + file, { recursive: true }) - } - }) - try { logger.error( chalk.red(typeof message === 'string' ? message : JSON.stringify(message)) @@ -43,14 +35,6 @@ export function clientError({ export function serverError(res: Response, err?: string, moduleName?: string) { const logger = createLogger({ name: moduleName || 'unknown-module' }) - fs.readdirSync('medium').forEach(file => { - if (fs.statSync('medium/' + file).isFile()) { - fs.unlinkSync('medium/' + file) - } else { - fs.rmdirSync('medium/' + file, { recursive: true }) - } - }) - try { logger.error(chalk.red(err)) diff --git a/instructions/form-system-migration.md b/instructions/form-system-migration.md index 18a845380..fd4129ed3 100644 --- a/instructions/form-system-migration.md +++ b/instructions/form-system-migration.md @@ -724,11 +724,35 @@ For each field in `.setupFields({...})`, render the corresponding new field comp | `amount: { ... }` with type `currency` | `` | Same as key | | `amount: { ... }` with type `number` | `` | Same as key | -**Old field config property → new field prop mapping:** +**Localization Convention for Field Labels:** -| Old `.setupFields` property | New prop | Remarks | -| --------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------- | -| `label` | `label` | Same — still passed through i18n translation | +Field `label` props are **auto-translated** through the module's i18n namespace (provided by `FormModal`'s `namespace` prop). The locale keys follow this convention: + +```json +{ + "inputs": { + "modifyCollection": { + "name": "Collection Name" + }, + "modifyType": { + "name": "Category Name", + "icon": "Category Icon" + }, + "modifyEntry": { + "name": "Music Name", + "author": "Author", + "type": "Score Type", + "collection": "Collection" + } + } +} +``` + +The label prop references the key **after** `inputs.` — e.g. `label="modifyEntry.name"` resolves to `inputs.modifyEntry.name` in the locale file. There is no `inputs.` prefix needed in the label prop — the `FieldWrapper` component auto-prepends the `inputs.` namespace segment. + +> 💡 **Why this convention?** The `inputs.` grouping in the locale file keeps all form field labels organized under one section, while the label prop stays short and focused on the modal-specific key. Each modal gets its own sub-object under `inputs` (e.g. `modifyCollection`, `modifyEntry`, `modifyType`), avoiding flat naming collisions. + +**Old field config property → new field prop mapping:** | `icon` | `icon` | Same | | `required` | `required` | Same | | `placeholder` | `placeholder` | Same | diff --git a/packages/ui/DESIGN.md b/packages/ui/DESIGN.md index ad738ea9f..4a59b3b82 100644 --- a/packages/ui/DESIGN.md +++ b/packages/ui/DESIGN.md @@ -89,6 +89,25 @@ Typography values scale with `--custom-font-scale`: - `semibold`: `'600'` - `bold`: `'700'` +### D. Category/Tag Chips + +For dynamic category labels or tag chips that need background colors based on data (not theme tokens), use **inline `style` with `backgroundColor`** set directly from the data source. Do NOT create `.css.ts` files for one-off dynamic colors — inline styles are the correct pattern here because the color value is computed at runtime from external data. + +✅ **Correct (dynamic colors from data):** +```tsx + + {category.name} + +``` + +> [!TIP] +> **Dynamic category colors are the exception to Rule 2.** If the background/color pair comes from database or API data (e.g. `bg-green-500/20 text-green-500`), use inline `style` with hex color + opacity. Do not create token mappings or CSS files for dynamic data-driven colors. + --- ## 3. Style Resolution & The Styling Engine @@ -111,6 +130,7 @@ Colors in LifeForge are resolved through an **arbitrary CSS variable architectur 1. **Base Palette:** `transparent`, `dangerous` (`#ef4444`), `muted` (mid-gray), `primary` (user-accented), `inherit`, `custom-50` to `custom-900` (accent shades), and `bg-50` to `bg-950` (system background shades). 2. **Tailwind Palette Names:** Colors like `red-500`, `blue-600`, `emerald-400` map directly to tailwind shades. 3. **Color with Opacity:** Zero-runtime opacity can be added using the `colorWithOpacity(token, opacityValue)` utility. +4. **No `white` or `black` color tokens.** Use `bg-50` for white and `bg-950` for black. The `green-*`, `red-*`, `yellow-*`, and other Tailwind palette names are available for semantic colors (e.g. `red-500` for destructive states, `green-600` for success). #### Theme & State Specific Variants @@ -190,7 +210,24 @@ const semiTransparentPrimary = colorWithOpacity('primary', '30%') _Supported Opacity Levels:_ `'5%'`, `'10%'`, `'20%'`, `'30%'`, `'40%'`, `'50%'`, `'60%'`, `'70%'`, `'80%'`, `'90%'`. -`colorWithOpacity` can be used **directly in the `bg` prop** — no inline `style` needed: +`colorWithOpacity` can be used **directly in the `bg` prop** — no inline `style` needed. It also works in vanilla-extract `.css.ts` files at build time via `.toString()`: + +```typescript +// In a .css.ts file (build-time evaluation) +import { colorWithOpacity } from '@lifeforge/ui' + +const bg200Opacity30 = colorWithOpacity('bg-200', '30%').toString() + +export const stripe = style({ + selectors: { + '&:nth-child(odd)': { + backgroundColor: bg200Opacity30 + } + } +}) +``` + +> **Important:** `colorWithOpacity` with `'80%'` opacity is the correct replacement for Tailwind's `/80` opacity syntax (e.g. `bg-500/80`). Do not use inline `style` for opacity. ```tsx { minHeight?: ResponsiveProp; maxHeight?: ResponsiveProp inset?: ResponsiveProp; top?: ResponsiveProp; bottom?: ResponsiveProp left?: ResponsiveProp; right?: ResponsiveProp; zIndex?: ResponsiveProp + + // Arbitrary CSS Props (responsive strings) + aspectRatio?: ResponsiveProp + flex?: ResponsiveProp; flexBasis?: ResponsiveProp + flexGrow?: ResponsiveProp; flexShrink?: ResponsiveProp + overflow?: ResponsiveProp<'visible' | 'hidden' | 'scroll' | 'auto'> + overflowX?: ResponsiveProp<'visible' | 'hidden' | 'scroll' | 'auto'> + overflowY?: ResponsiveProp<'visible' | 'hidden' | 'scroll' | 'auto'> ``` +> [!TIP] +> **`aspectRatio`, `overflow`, `flex`, `flexGrow`, `flexShrink`, `flexBasis` are available as props on all primitives** — no inline `style` needed. Always check if a CSS property exists in the `ArbitraryProps` type before resorting to inline styles. + > [!WARNING] > **`top`, `right`, `bottom`, `left`, and `inset` accept raw CSS strings** (e.g. `"0.5rem"`, `"8px"`, `"50%"`), not `SpaceToken` values like `sm`, `md`, `lg`. > Spacing tokens only work with padding (`p`, `px`, `py`, `pt`, `pr`, `pb`, `pl`) and margin (`m`, `mx`, `my`, `mt`, `mr`, `mb`, `ml`) props. @@ -1502,6 +1550,9 @@ Before submitting a pull request, verify that you have adhered to all core desig - [ ] **Correct loaders:** Form/button loading states use the pre-animated `svg-spinners:ring-resize` icon and **never** use custom `animate-spin` utilities. - [ ] **Type safety:** Typescript `any` is never used. All prop overrides and custom handlers are explicitly typed. - [ ] **Datetime manipulation:** Standard JavaScript `Date` is never used. `day.js` is imported for any date calculations. +- [ ] **Check ArbitraryProps first:** Before reaching for inline `style` or `.css.ts`, check if the CSS property is available via `ArbitraryProps` (e.g. `aspectRatio`, `overflow`, `flex`, `flexGrow`, `flexShrink`, `flexBasis`). +- [ ] **Component deconstruction:** If a component's JSX grows beyond a single conceptual block, extract sub-sections into their own component files under a `components/` subdirectory. +- [ ] **Hooks co-location:** Hooks that are only consumed by a child component should live inside that child component, not in the parent. - [ ] **List spacing:** For vertical lists (`Stack`), `mb="lg"` is the standard bottom margin — no need to define responsive sizes like `mb={{ base: '6rem', md: 'lg' }}`. The spacing token `lg` is consistent across breakpoints and is sufficient for all list containers. - [ ] **Component organization:** Components are strictly separated into individual files under their respective `components/` folders instead of being grouped together. - [ ] **Conventional functions:** All React components use standard function declarations (`export function Component()`) and avoid arrow functions. diff --git a/packages/ui/src/components/data-display/ViewModeSelector/index.tsx b/packages/ui/src/components/data-display/ViewModeSelector/index.tsx index 359fd928a..9bdf7473b 100644 --- a/packages/ui/src/components/data-display/ViewModeSelector/index.tsx +++ b/packages/ui/src/components/data-display/ViewModeSelector/index.tsx @@ -1,9 +1,15 @@ -import { Flex, Icon, Text, Transition } from '@/components/primitives' +import { + Flex, + type FlexProps, + Icon, + Text, + Transition +} from '@/components/primitives' interface ViewModeSelectorProps< T extends ReadonlyArray<{ value: string; icon?: string; text?: string }>, TKey = T[number]['value'] -> { +> extends FlexProps<'div'> { /** The current selected mode */ currentMode: TKey size?: 'small' | 'default' @@ -23,7 +29,8 @@ export function ViewModeSelector< currentMode, size = 'default', onModeChange, - options + options, + ...rest }: ViewModeSelectorProps) { return ( {options.map(({ value, icon, text }) => ( diff --git a/packages/ui/src/components/feedback/EmptyStateScreen/index.tsx b/packages/ui/src/components/feedback/EmptyStateScreen/index.tsx index 88e93e187..286736346 100644 --- a/packages/ui/src/components/feedback/EmptyStateScreen/index.tsx +++ b/packages/ui/src/components/feedback/EmptyStateScreen/index.tsx @@ -58,13 +58,7 @@ export function EmptyStateScreen({ > {icon !== undefined && (typeof icon === 'string' ? ( - + ) : ( icon ))} @@ -72,7 +66,7 @@ export function EmptyStateScreen({ align="center" as="h2" color={{ base: 'bg-800', dark: 'bg-100' }} - size={smaller ? '2xl' : '3xl'} + size={smaller ? 'xl' : '3xl'} style={{ paddingLeft: '1.5rem', paddingRight: '1.5rem' }} weight="semibold" > @@ -97,12 +91,8 @@ export function EmptyStateScreen({ {message.description} @@ -116,12 +106,8 @@ export function EmptyStateScreen({ {message.namespace === false diff --git a/packages/ui/src/components/feedback/ErrorScreen/index.tsx b/packages/ui/src/components/feedback/ErrorScreen/index.tsx index b00a717dd..541f06f2e 100644 --- a/packages/ui/src/components/feedback/ErrorScreen/index.tsx +++ b/packages/ui/src/components/feedback/ErrorScreen/index.tsx @@ -28,6 +28,7 @@ export function ErrorScreen({ message, showRetryButton }: ErrorScreenProps) { {showRetryButton && (