UNPKG

alouette

Version:

A modern, customizable design system built on top of NativeWind v5 with configurable defaults

207 lines (154 loc) 6.59 kB
--- name: alouette-styling description: > Styling conventions for writing components with alouette: express enum-like props (variant/size/shadow/state) as tailwind-variants tv() variants instead of a Record lookup, give one component a single tv() with slots instead of several tv objects, style through className instead of inline style, and write pixel sizes as arbitrary values (w-[380px]) rather than the canonical spacing-scale class. States the alouette design principles every component must satisfy. Load when writing, restyling or reviewing a component. type: core library: alouette library_version: "22.6.0" requires: - alouette-theming sources: - "christophehurpeau/alouette:packages/alouette/src/ui/containers/Modal.tsx" - "christophehurpeau/alouette:packages/alouette/src/ui/containers/Surface.tsx" - "christophehurpeau/alouette:packages/alouette/src/ui/selection/SegmentedItem.tsx" --- # alouette — Styling conventions How a component's classes are declared and applied. Which classes to use is covered by alouette-theming (tokens), alouette-layout (spacing/radius/shadow) and alouette-typography (text). ## Design principles Every component is measured against these: - Consistent UI - Unique - Professional - Accessible - Depth using shades, shadows and tint - Multi-platform: web and native (ios/android at the moment) - Explicit affordance element for interactive surfaces (Chevron icon or action text “View”, “Open”, or “Details”) - Animated ## Core patterns ### Enum-like props are `tv()` variants A prop that selects between a fixed set of classes (`variant`, `size`, `shadow`, `selected`, `disabled`) is a `tv()` variant. The keys become the prop's type via `VariantProps`, and `defaultVariants` supplies the default. Resolve a value in component code only when one variant's default depends on another prop (`Surface`: `shadow` defaults to `lowered` when `variant="lowered"`). ```tsx import { type VariantProps, tv } from "tailwind-variants"; const surfaceVariants = tv({ base: "overflow-hidden", variants: { variant: { surface: "bg-surface", lowered: "bg-lowered" }, size: { sm: "p-m rounded-sm", md: "p-xl rounded-sm" }, }, defaultVariants: { variant: "surface", size: "md" }, }); type SurfaceVariantProps = VariantProps<typeof surfaceVariants>; export interface SurfaceProps extends BoxProps, SurfaceVariantProps {} ``` Pass the incoming `className` through the call (`surfaceVariants({ size, className })`) so callers can extend it. ### One component, one `tv()` — use `slots` A component that styles several elements declares **one** `tv()` with `slots`, not one `tv()` per element. The variant props are then declared once and applied to every element from a single call, so the elements cannot drift. ```tsx const modalVariants = tv({ slots: { panel: "w-full max-h-full", inset: "bg-highlight shadow-l", footer: "…" }, variants: { size: { sm: { panel: "max-w-[360px]", inset: "rounded-sm p-xs", footer: "py-xs" }, md: { panel: "max-w-[520px]", inset: "rounded-sm p-m", footer: "py-m" }, }, }, defaultVariants: { size: "md" }, }); const styles = modalVariants({ size, withFooter: footer !== undefined }); <View className={styles.panel()}> <View className={styles.inset()}>…</View> </View>; ``` `extend` still applies for a slot that is a strict superset of another tv (`labelVariants = tv({ extend: foregroundVariants, base: "…" })`). ### `className`, not inline `style` Everything that can be a class is a class classes participate in theming, variants, `group-*`/state modifiers and the Tailwind scan. Inline `style` is only for a value computed at runtime that no static class can express (a measured dimension, `windowHeight * 0.7`, a token-derived pixel height). ### Pixel sizes as arbitrary values Write a pixel dimension as an arbitrary value `w-[380px]`, `max-w-[460px]`, `min-h-[44px]` and keep it even when the linter suggests the canonical spacing-scale class (`w-95`, `max-w-115`, `min-h-11`). The scale class hides the actual pixel value, which is the thing being reasoned about. This applies to one-off pixel dimensions only; padding, gap and radius still use the token scale (`p-m`, `gap-xs`, `rounded-sm`). ## Common Mistakes ### HIGH A `Record` lookup instead of a `tv()` variant Wrong: ```tsx const LAYER_CLASS = { surface: "bg-surface", lowered: "bg-lowered" } as const; <Box className={LAYER_CLASS[variant]} />; ``` Correct: ```tsx const boxVariants = tv({ variants: { variant: { surface: "bg-surface", lowered: "bg-lowered" } }, defaultVariants: { variant: "surface" }, }); <Box className={boxVariants({ variant })} />; ``` The lookup map duplicates the prop's union type by hand, has no default handling, no compound variants and no `className` merge. Source: packages/alouette/src/ui/containers/Surface.tsx ### HIGH Several `tv()` objects for one component Wrong: ```tsx const chipVariants = tv({ variants: { selected: , disabled: } }); const segmentVariants = tv({ variants: { selected: , disabled: } }); const labelVariants = tv({ variants: { selected: , disabled: } }); ``` Correct: ```tsx const itemVariants = tv({ slots: { chip: "…", segment: "…", label: "…" }, variants: { selected: { true: { chip: "opacity-100", label: "text-on-accent" } } }, }); const styles = itemVariants({ selected, disabled }); ``` Repeating `selected`/`disabled` across separate tv objects means every variant change has to be made in three places, and each element is called separately at render time. Source: packages/alouette/src/ui/containers/Modal.tsx ### MEDIUM Inline `style` for a static value Wrong: ```tsx <View style={{ maxWidth: 460, paddingHorizontal: 16 }} /> ``` Correct: ```tsx <View className="max-w-[460px] px-m" /> ``` An inline `style` is invisible to variants, state modifiers and theming, and on web it wins over the class it silently conflicts with. Source: packages/alouette/src/ui/containers/Modal.tsx ### MEDIUM Collapsing a pixel size to the spacing-scale class Wrong: ```tsx <View className="w-95 min-h-11" /> ``` Correct: ```tsx <View className="w-[380px] min-h-[44px]" /> ``` `w-95` / `min-h-11` are the same pixels but read as scale steps; the explicit arbitrary value keeps the measurement legible (`min-h-[44px]` is the accessibility touch-target minimum, not a padding choice). Source: packages/alouette/src/ui/actions/Button.tsx See also: alouette-theming/SKILL.md (which classes), alouette-layout/SKILL.md (spacing/radius/shadow scale).