diff --git a/.github/PULL_REQUEST_TEMPLATE/design-system.md b/.github/PULL_REQUEST_TEMPLATE/design-system.md new file mode 100644 index 0000000..315636f --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/design-system.md @@ -0,0 +1,38 @@ +## Design-system checklist + +Use this checklist when your PR changes design tokens, shared components in `packages/ui`, or introduces a new component to the design system. + +### API compatibility + +- [ ] Component API follows existing conventions: `className` forwarding, `cva` variants, `...props` spread last +- [ ] No breaking API changes without a **migration note** in the PR description (see changelog below) +- [ ] Subpath export added to `packages/ui/package.json` if this is a new component + +### Theme & visual + +- [ ] Verified in **light theme** and **dark theme** +- [ ] Verified at **mobile** viewport width (360px) +- [ ] No new raw hex colours, arbitrary font sizes, or arbitrary radius values — `bun run check:tokens` passes clean + +### Accessibility + +- [ ] Interactive elements are operable by **keyboard alone** (Tab, Enter/Escape, arrow keys where applicable) +- [ ] Correct ARIA roles and states are exposed (use Base UI primitives where possible) +- [ ] Colour is never the only signal — shape, label, or position accompanies any colour-coded state + +### Testing + +- [ ] Non-trivial logic includes a unit test (`*.test.tsx` alongside the component) +- [ ] Component added to the gallery (`apps/web/src/features/gallery/components/gallery-page.tsx`) +- [ ] Visual regression snapshots updated (`bun run test:e2e -- design-system-visual --update-snapshots`) and reviewed in the diff + +### Changelog + +- [ ] Entry added to [`packages/ui/CHANGELOG.md`](../../packages/ui/CHANGELOG.md) under the correct section (`Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, or `Accessibility`) +- [ ] Breaking changes include a `### Migration note` subsection + +### CI + +- [ ] `bun run typecheck --filter=@workspace/ui` passes +- [ ] `bun run lint --filter=@workspace/ui` passes +- [ ] `bun run test:e2e -- design-system-visual` passes (or snapshots intentionally updated) \ No newline at end of file diff --git a/DESIGN.md b/DESIGN.md index fb237bd..3765c14 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -120,9 +120,11 @@ then review the diffs in `e2e/design-system-visual.spec.ts-snapshots/` in your P - give reviewers one place to see every variant of a component at once instead of hunting through feature pages for one that happens to use the state you changed - act as the fixed, deterministic target for the visual regression suite above +- provide an **RTL toggle** (top-right corner) that switches the page between left-to-right and right-to-left layout, making directional-CSS regressions visible at a glance When you add a new component to `packages/ui`, add it to the gallery in the same PR — see [`packages/ui/CONTRIBUTING.md`](./packages/ui/CONTRIBUTING.md) for the full checklist. +<<<<<<< HEAD ## Separators and dividers Borders are the cheapest way to make a layout look organised and the fastest way to make it look noisy. Linear's calm hierarchy comes from *space* and *surface* doing the grouping, with lines reserved for the few places where a relationship genuinely needs marking. [`packages/ui/src/components/separator.tsx`](./packages/ui/src/components/separator.tsx) (DS-080) exists so those few places look the same everywhere. @@ -140,6 +142,47 @@ Borders are the cheapest way to make a layout look organised and the fastest way **Labelled dividers.** `` names a break instead of just drawing one — grouped form sections, "or" between auth methods, date breaks in a feed. The rules are decorative and the label is ordinary text, so assistive technology reads the words rather than the geometry. Label contrast comes from `text-text-secondary`, which holds up in light, dark, and high-contrast themes. Existing page borders were left alone in DS-080; migrate them opportunistically when you're already touching the markup. +======= +## Right-to-left (RTL) support + +### Logical CSS properties + +The design system uses **logical CSS properties** instead of physical directional properties wherever the behaviour is semantic (i.e. describes content flow rather than a fixed visual direction): + +| Physical | Logical equivalent | +|---|---| +| `left` / `right` | `inset-inline-start` / `inset-inline-end` | +| `margin-left` / `margin-right` | `margin-inline-start` / `margin-inline-end` | +| `padding-left` / `padding-right` | `padding-inline-start` / `padding-inline-end` | +| `border-left` / `border-right` | `border-inline-start` / `border-inline-end` | +| `text-left` / `text-right` | `text-start` / `text-end` | +| `translate-x` | Use `inset-inline-start` + `translate` or percent-based | + +Tailwind v4 provides logical utility classes — `ms-*`, `me-*`, `ps-*`, `pe-*`, `border-s-*`, `border-e-*`, `text-start`, `text-end`, `inset-inline-start-*`, `inset-inline-end-*` — which map to the correct physical side at runtime based on the `dir` attribute. + +### Justified physical directions + +Some directional properties are intentionally kept physical: + +- **Numeric financial values**: columns containing prices, sizes, volumes, APY, and other numbers use `text-right` (not `text-end`) because financial convention requires right-aligned digits in all locales, regardless of script direction. +- **Chart time flow**: time-series charts (candlesticks, line charts) render left-to-right regardless of page direction, per financial-market convention. +- **Data-attributed sides**: components that accept a physical `side` prop (e.g. `sheet` with `side="left"` or `side="right"`, tooltip arrow positioning for `data-[side=left]`/`data-[side=right]`) remain physical because they refer to the screen-relative position, not the content-flow direction. The logical `inline-start` / `inline-end` variants are also supported for direction-aware placement. + +### DirectionProvider + +A `DirectionProvider` (in `apps/web/src/ui/direction-provider.tsx`) persists the chosen direction to `localStorage` under `so4-direction` and sets the `dir` attribute on ``. It wraps the app in the provider tree alongside `ThemeProvider`. + +### RTL gallery fixture + +The component gallery at `/gallery` includes an RTL/LTR toggle button (top-right corner). Use it during development and review to verify that: + +- Dialogs, menus, fields, tabs, breadcrumbs, tables, and navigation remain usable in RTL +- Leading/trailing icons and controls mirror correctly +- Focus order follows DOM order and remains logical +- Numeric financial values preserve readable direction + +The visual regression suite (`e2e/design-system-visual.spec.ts`) captures gallery screenshots in both directions across all themes and viewports. Run `bun run test:e2e -- design-system-visual` to verify, or `bun run test:e2e -- design-system-visual --update-snapshots` to update baselines after an intentional change. +>>>>>>> e242e90 (Audit layouts for RTL and logical CSS properties) ## Audit history diff --git a/apps/web/src/app/providers/index.tsx b/apps/web/src/app/providers/index.tsx index 5d9e96c..0c98e73 100644 --- a/apps/web/src/app/providers/index.tsx +++ b/apps/web/src/app/providers/index.tsx @@ -4,6 +4,7 @@ import type { ErrorInfo, ReactNode } from "react" import { useWalletStore } from "@/features/wallet/store/wallet-store" import { NETWORK } from "@/app/config/network" import { ThemeProvider } from "@/ui/theme-provider" +import { DirectionProvider } from "@/ui/direction-provider" import { ErrorPage } from "@/app/error-page" import { validateIndexerConfig } from "@/app/config/indexer" import { useIndexerInvalidation } from "@/lib/graphql/use-indexer-invalidation" @@ -149,7 +150,9 @@ export function AppProviders({ children }: { children: ReactNode }) { - {children} + + {children} + diff --git a/apps/web/src/features/earn/components/discover/discover-tab.tsx b/apps/web/src/features/earn/components/discover/discover-tab.tsx index 3794da3..dbcf3f4 100644 --- a/apps/web/src/features/earn/components/discover/discover-tab.tsx +++ b/apps/web/src/features/earn/components/discover/discover-tab.tsx @@ -171,7 +171,7 @@ export function DiscoverTab() { -
+
Sort setSort("apy")}> APY {sort === "apy" && "↓"} @@ -247,7 +247,7 @@ export function DiscoverTab() { Short token
-

+

APY based on trailing 30-day performance

diff --git a/apps/web/src/features/earn/components/distributions/distributions-table.tsx b/apps/web/src/features/earn/components/distributions/distributions-table.tsx index 29869c0..2ab88e2 100644 --- a/apps/web/src/features/earn/components/distributions/distributions-table.tsx +++ b/apps/web/src/features/earn/components/distributions/distributions-table.tsx @@ -88,13 +88,13 @@ export function DistributionsTable({ distributions, onClaim }: DistributionsTabl
- + - + @@ -124,7 +124,7 @@ export function DistributionsTable({ distributions, onClaim }: DistributionsTabl )} - - ) diff --git a/packages/ui/src/components/tabs.tsx b/packages/ui/src/components/tabs.tsx index e9fd1b1..38c0874 100644 --- a/packages/ui/src/components/tabs.tsx +++ b/packages/ui/src/components/tabs.tsx @@ -60,10 +60,10 @@ function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) { + @@ -103,7 +103,7 @@ function TooltipShortcut({
Epoch Date Amount Token StatusTxTx
+ {row.txHash ? ( {row.txHash.slice(0, 8)}… diff --git a/apps/web/src/features/earn/components/portfolio/assets-list.tsx b/apps/web/src/features/earn/components/portfolio/assets-list.tsx index e663631..22f7533 100644 --- a/apps/web/src/features/earn/components/portfolio/assets-list.tsx +++ b/apps/web/src/features/earn/components/portfolio/assets-list.tsx @@ -66,7 +66,7 @@ function LoadingRows() {
- +
diff --git a/apps/web/src/features/earn/components/portfolio/rewards-bar.tsx b/apps/web/src/features/earn/components/portfolio/rewards-bar.tsx index f6cd57f..245b422 100644 --- a/apps/web/src/features/earn/components/portfolio/rewards-bar.tsx +++ b/apps/web/src/features/earn/components/portfolio/rewards-bar.tsx @@ -149,7 +149,7 @@ export function RewardsBar() { isLoading={isLoading} /> -
+
>([40]) const [announceCount, setAnnounceCount] = useState(0) return (
-
-

Component Gallery

-

- Every packages/ui primitive, all variants. See{" "} - - DESIGN.md - {" "} - and{" "} - - packages/ui/CONTRIBUTING.md - {" "} - for how to add to this page. -

+
+
+

Component Gallery

+

+ Every packages/ui primitive, all variants. See{" "} + + DESIGN.md + {" "} + and{" "} + + packages/ui/CONTRIBUTING.md + {" "} + for how to add to this page. +

+
+
diff --git a/apps/web/src/features/pools/components/gm-pool-row.tsx b/apps/web/src/features/pools/components/gm-pool-row.tsx index 9d1e197..4f02842 100644 --- a/apps/web/src/features/pools/components/gm-pool-row.tsx +++ b/apps/web/src/features/pools/components/gm-pool-row.tsx @@ -46,7 +46,7 @@ function ValueCell({ className?: string }) { if (isLoading) { - return + return } return ( @@ -207,7 +207,7 @@ export function GmPoolRow({ market, variant, onMetricsChange }: GmPoolRowProps)
+ {pendingTx ? ( -
+

{pendingTx.mode === "deposit" ? "Deposit pending" : "Withdrawal pending"}

diff --git a/apps/web/src/features/referrals/components/distributions/distributions-tab.tsx b/apps/web/src/features/referrals/components/distributions/distributions-tab.tsx index 692e6a7..8e3e09f 100644 --- a/apps/web/src/features/referrals/components/distributions/distributions-tab.tsx +++ b/apps/web/src/features/referrals/components/distributions/distributions-tab.tsx @@ -97,13 +97,13 @@ export function DistributionsTab() {
- + - + @@ -118,7 +118,7 @@ export function DistributionsTab() { -
Epoch Date Amount Token USD valueActionAction
{formatToken(d.amount, d.token)} {d.token} {formatUsd(d.amountUsd)} +
) } diff --git a/packages/ui/src/components/dialog.tsx b/packages/ui/src/components/dialog.tsx index 820d9a1..ca15afc 100644 --- a/packages/ui/src/components/dialog.tsx +++ b/packages/ui/src/components/dialog.tsx @@ -54,7 +54,7 @@ function DialogContent({ data-slot="dialog-content" className={cn( // Positioning & sizing - "fixed top-1/2 left-1/2 z-50 w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2", + "fixed top-1/2 inset-inline-start-1/2 z-50 w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2", // Mobile: cap height so content scrolls inside rather than overflowing the viewport "max-h-[calc(100dvh-2rem)] overflow-x-hidden overflow-y-auto", // Layout, surface, typography diff --git a/packages/ui/src/components/dropdown-menu.tsx b/packages/ui/src/components/dropdown-menu.tsx index 5373ef8..da4ff01 100644 --- a/packages/ui/src/components/dropdown-menu.tsx +++ b/packages/ui/src/components/dropdown-menu.tsx @@ -79,14 +79,14 @@ function DropdownMenuCheckboxItem({ svg]:size-3.5 [&>svg]:shrink-0", + "relative flex w-full cursor-default select-none items-center gap-2 rounded-lg py-1.5 pe-2 ps-8 text-xs/relaxed outline-none data-highlighted:bg-accent data-highlighted:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 [&>svg]:size-3.5 [&>svg]:shrink-0", className )} {...props} > svg]:size-3.5 [&>svg]:shrink-0", + "relative flex w-full cursor-default select-none items-center gap-2 rounded-lg py-1.5 pe-2 ps-8 text-xs/relaxed outline-none data-highlighted:bg-accent data-highlighted:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 [&>svg]:size-3.5 [&>svg]:shrink-0", className )} {...props} >
)} {count != null && ( - + {count} {count === 1 ? 'result' : 'results'} )} diff --git a/packages/ui/src/components/table.tsx b/packages/ui/src/components/table.tsx index 6597393..d7cfbee 100644 --- a/packages/ui/src/components/table.tsx +++ b/packages/ui/src/components/table.tsx @@ -38,7 +38,7 @@ function TableHeadRow({ className, ...props }: React.ComponentProps<"tr">) { return (