mirror of
https://github.com/Lifeforge-app/lifeforge.git
synced 2026-06-27 14:26:06 +00:00
fix: various ui fixes
This commit is contained in:
@@ -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))
|
||||
|
||||
|
||||
@@ -724,11 +724,35 @@ For each field in `.setupFields({...})`, render the corresponding new field comp
|
||||
| `amount: { ... }` with type `currency` | `<CurrencyField name="amount" .../>` | Same as key |
|
||||
| `amount: { ... }` with type `number` | `<NumberField name="amount" .../>` | 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 |
|
||||
|
||||
@@ -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
|
||||
<span
|
||||
style={{
|
||||
backgroundColor: category.color + '20',
|
||||
color: category.color
|
||||
}}
|
||||
>
|
||||
{category.name}
|
||||
</span>
|
||||
```
|
||||
|
||||
> [!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
|
||||
<Flex
|
||||
@@ -279,8 +316,19 @@ interface BoxOwnProps<T extends ElementType = 'div'> {
|
||||
minHeight?: ResponsiveProp<string>; maxHeight?: ResponsiveProp<string>
|
||||
inset?: ResponsiveProp<string>; top?: ResponsiveProp<string>; bottom?: ResponsiveProp<string>
|
||||
left?: ResponsiveProp<string>; right?: ResponsiveProp<string>; zIndex?: ResponsiveProp<string>
|
||||
|
||||
// Arbitrary CSS Props (responsive strings)
|
||||
aspectRatio?: ResponsiveProp<string>
|
||||
flex?: ResponsiveProp<string>; flexBasis?: ResponsiveProp<string>
|
||||
flexGrow?: ResponsiveProp<string>; flexShrink?: ResponsiveProp<string>
|
||||
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.
|
||||
|
||||
@@ -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<T, TKey>) {
|
||||
return (
|
||||
<Flex
|
||||
@@ -34,6 +41,7 @@ export function ViewModeSelector<
|
||||
height="4em"
|
||||
p={size === 'small' ? 'xs' : 'sm'}
|
||||
r="lg"
|
||||
{...rest}
|
||||
>
|
||||
{options.map(({ value, icon, text }) => (
|
||||
<Transition key={value}>
|
||||
|
||||
@@ -58,13 +58,7 @@ export function EmptyStateScreen({
|
||||
>
|
||||
{icon !== undefined &&
|
||||
(typeof icon === 'string' ? (
|
||||
<Icon
|
||||
icon={icon}
|
||||
style={{
|
||||
width: smaller ? '4.5rem' : '8rem',
|
||||
height: smaller ? '4.5rem' : '8rem'
|
||||
}}
|
||||
/>
|
||||
<Icon icon={icon} size={smaller ? '4rem' : '8rem'} />
|
||||
) : (
|
||||
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({
|
||||
<Text
|
||||
align="center"
|
||||
as="p"
|
||||
size={smaller ? 'base' : 'lg'}
|
||||
style={{
|
||||
marginTop: '-0.5rem',
|
||||
paddingLeft: '1.5rem',
|
||||
paddingRight: '1.5rem'
|
||||
}}
|
||||
px="lg"
|
||||
size={smaller ? 'sm' : 'lg'}
|
||||
whiteSpace="pre-wrap"
|
||||
>
|
||||
{message.description}
|
||||
@@ -116,12 +106,8 @@ export function EmptyStateScreen({
|
||||
<Text
|
||||
align="center"
|
||||
as="p"
|
||||
size={smaller ? 'base' : 'lg'}
|
||||
style={{
|
||||
marginTop: '-0.5rem',
|
||||
paddingLeft: '1.5rem',
|
||||
paddingRight: '1.5rem'
|
||||
}}
|
||||
px="lg"
|
||||
size={smaller ? 'sm' : 'lg'}
|
||||
whiteSpace="pre-wrap"
|
||||
>
|
||||
{message.namespace === false
|
||||
|
||||
@@ -28,6 +28,7 @@ export function ErrorScreen({ message, showRetryButton }: ErrorScreenProps) {
|
||||
{showRetryButton && (
|
||||
<Button
|
||||
icon="tabler:refresh"
|
||||
namespace={false}
|
||||
variant="secondary"
|
||||
onClick={() => window.location.reload()}
|
||||
>
|
||||
|
||||
@@ -162,7 +162,6 @@ export function ModuleHeader({
|
||||
position="relative"
|
||||
r="md"
|
||||
style={{ zIndex: 50 }}
|
||||
width="24em"
|
||||
>
|
||||
<Menu as="div">
|
||||
<Transition>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Flex } from '@/components/primitives'
|
||||
import { Flex, type FlexProps } from '@/components/primitives'
|
||||
|
||||
import { NavButton } from './components/NavButton'
|
||||
import { PageNumbers } from './components/PageNumbers'
|
||||
|
||||
interface PaginationProps {
|
||||
interface PaginationProps extends FlexProps<'div'> {
|
||||
/** Current active page */
|
||||
page: number
|
||||
/** Callback function when the page is changed */
|
||||
onPageChange: (page: number | ((prevPage: number) => number)) => void
|
||||
onPageChange: (page: number) => void
|
||||
/** Total number of pages */
|
||||
totalPages: number
|
||||
/** Additional class names for the pagination container */
|
||||
@@ -18,30 +18,18 @@ export function Pagination({
|
||||
page,
|
||||
onPageChange,
|
||||
totalPages,
|
||||
className = ''
|
||||
...rest
|
||||
}: PaginationProps): React.ReactElement {
|
||||
const previousPage = () => {
|
||||
onPageChange(prevPage => {
|
||||
if (prevPage > 1) {
|
||||
return prevPage - 1
|
||||
}
|
||||
|
||||
return prevPage
|
||||
})
|
||||
onPageChange(Math.max(1, page - 1))
|
||||
}
|
||||
|
||||
const nextPage = () => {
|
||||
onPageChange(prevPage => {
|
||||
if (prevPage < totalPages) {
|
||||
return prevPage + 1
|
||||
}
|
||||
|
||||
return prevPage
|
||||
})
|
||||
onPageChange(Math.min(totalPages, page + 1))
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex align="center" className={className} gap="sm" justify="between">
|
||||
<Flex align="center" gap="sm" justify="between" {...rest}>
|
||||
<NavButton
|
||||
direction="previous"
|
||||
hidden={page === 1}
|
||||
|
||||
Reference in New Issue
Block a user