Files
Doormilexpress_console/CLAUDE.md

319 lines
26 KiB
Markdown

# CLAUDE.md — Doormile Express Console (xpressconsole)
> Project-level rules and conventions for Claude Code when working in this repo.
> **Read this in full before editing.** When in doubt about a pattern, copy from `src/pages/nearle/deliveries/deliveries.js` — it is the canonical reference for both the design system and the data layer.
---
## 1. What this is
A React 18 operator console for the Doormile Express dispatch platform. Operators use it to manage orders, run the AI dispatch optimiser, watch a live map of riders, edit tenants/pricing/invoices, and pull BI reports. Production users are warehouse staff, not end customers.
For the per-page API map and architectural flow chart, see the project skill **`nearlexpress-docs`** (`.claude/skills/nearlexpress-docs/SKILL.md`). Do not duplicate that content here.
---
## 2. Stack (pinned — do not upgrade without a deliberate audit)
- **React 19.2** + **react-app-rewired 2.2** (CRA, not Next.js — no `pages/` file-system routing; routes live in `src/routes/MainRoutes.js`).
- **Astryx 0.1.9** (`@astryxdesign/core`) is the UI library — see §6. **MUI 5.12** (`@mui/material`) and Ant Design (`antd 5.11`) are **legacy**, present only on the pages not yet converted, and are being removed. Never write new UI against them.
- **Icons**: `@ant-design/icons` (page actions: `EditOutlined`, `EyeOutlined`, `CloseOutlined`, etc.) **and** `react-icons` (page identity: `Md*`, `Tb*`, `Fi*`, `Gi*`, `Lu*`). Both coexist; pick by what's already imported nearby.
- **Data layer**: `@tanstack/react-query 5.17` (`useQuery`, `useInfiniteQuery`, `useMutation`). All server fetches go through this. Do **not** introduce `fetch`, `swr`, or `useEffect`-driven fetches for new code.
- **HTTP**: `axios 1.3`. Most pages import `axios` directly (raw). `src/utils/axios.js` exists with a 401 → `/login` interceptor but is **not** widely adopted — match the surrounding file rather than introducing it.
- **State**: `@reduxjs/toolkit 1.9` for cross-page state (FCM token, login user, menu, snackbar, toast). Page-local UI state stays in `useState`.
- **Routing**: `react-router-dom 6.10`, lazy-loaded via `components/Loadable`.
- **Forms**: `formik 2.2` + `yup 1.1` where present; new simple forms can use plain `useState`.
- **Dates**: `dayjs 1.11` with `utc` plugin (already extended at the top of `deliveries.js`). Use `dayjs(...).utc()` for backend timestamps and bare `dayjs(...)` for local-time bucketing — see the batch-bucketing comment in `deliveries.js` for the rationale.
- **Maps**: `leaflet` + `react-leaflet` only. Geocoding/address search via free OSM Nominatim (`src/components/nearle_components/AddressAutocomplete.js`) and routing via OSRM — no API key required. Google Maps (`@react-google-maps/api`, `react-geocode`, `react-google-autocomplete`) has been fully removed; do not reintroduce it.
- **Notifications**: `firebase 10.14` (FCM) — see `src/firebase_notification/`. Toasts via `notistack 3`.
- **Drag-and-drop**: `react-dnd` (used on the Dispatch Preview page).
---
## 3. Dev workflow
```bash
# Install
npm install # or `yarn`
# Run locally (uses .env)
npm start
# Run with a specific env file
npm run start:dev # env.development
npm run start:staging # env.staging
# Build
npm run build
npm run build:dev
npm run build:staging
# Lint (the only checked gate — there are no tests of consequence)
npm run lint
```
- **Env files at repo root**: `env.staging` is committed; `.env.development` / `.env.production` are typically gitignored. Pull from a teammate when missing.
- **Required env vars**: `REACT_APP_DOORMILE_URL` — the only API base (`https://api.doormile.com/api/v1`), used by every `/admin/*` call via `utils/doormileAxios.js`. The old jupiter-backed `REACT_APP_URL` / `REACT_APP_URL2` / `REACT_APP_URL3` have been fully retired — do not reintroduce them. No Maps API key is needed — maps and address search run on free Leaflet/OSM services. The optimiser URLs (`routes.workolik.com`, `routemate.workolik.com`) and the final-delivery-commit URL (`jupiter.nearle.app`) remain hardcoded — they're a separate solver service with no equivalent in the new API, deliberately left untouched by the backend migration. See the `nearlexpress-docs` skill and `express-console-api.md`.
- **Dev server runs on `http://localhost:3000`**. The user usually has it running already — assume it is up when reporting "reload to see it".
---
## 4. Hard constraints (do NOT)
1. **Do not introduce Next.js / SSR patterns.** No `getServerSideProps`, no `app/` directory, no `next/*` imports. This is CRA.
2. **Do not rewrite shared design tokens.** Every new page that needs the polished UI must reuse the `DT` token block (see §6). Do not invent a parallel palette.
3. **Do not change `package.json` dependency versions** unless explicitly asked. The build is sensitive to webpack/svgr/react-scripts versions (see `resolutions` in `package.json`).
4. **Do not bypass the dispatch reconcile step.** After any manual edit on `/doormile/dispatch/preview` (rider swap, step reorder), the page **must** call `POST /optimization/reconcile-steps` before `POST /deliveries/createdeliveries`. Skipping this corrupts route sequences.
5. **Do not commit `.env*` files** beyond `env.staging` (which is the agreed-shared staging baseline).
6. **Do not introduce TypeScript files** (`.ts` / `.tsx`) into this repo. It is JavaScript; mixing creates lint and tooling friction.
7. **Do not use absolute `http://localhost` URLs** in code. Always read from `process.env.REACT_APP_DOORMILE_URL` (or use `doormileAxios`, which already defaults to it).
8. **Do not log `userid`, `authname`, FCM tokens, or PII** to `console.log` in production paths. The codebase has many leftover `console.log` calls — when editing nearby, remove them rather than add more.
9. **Do not add or remove items from the sidebar without updating `src/menu-items/nearle.js`** — the menu drives both display and i18n keys.
10. **Do not use destructive git** (`reset --hard`, `push --force`, branch deletion) without explicit user instruction.
---
## 5. Architecture map
```
src/
├── App.js # Auth gate (localStorage('authname') → /login), mounts FCM listener, ThemeCustomization, Locales, Notistack, Snackbar
├── index.js # Provider wiring: Redux store, TanStack QueryClient, Router
├── config.js # Theme constants (DRAWER_WIDTH=260, fontFamily, mode, presetColor, ThemeMode, MenuOrientation, ThemeDirection)
├── routes/
│ ├── index.js # Combines MainRoutes + LoginRoutes
│ ├── MainRoutes.js # All /doormile/* routes, lazy-loaded via Loadable(lazy(...))
│ └── LoginRoutes.js
├── layout/
│ ├── MainLayout/ # Sidebar + header frame (used for all logged-in pages)
│ └── CommonLayout/ # Bare frame (login, maintenance pages)
├── menu-items/
│ └── nearle.js # Sidebar definition — id, title (FormattedMessage), url, icon. Must add new pages here.
├── store/
│ ├── index.js # configureStore + useDispatch/useSelector exports
│ └── reducers/ # fcmSlice, loginUserSlice, menu, snackbar, toastSlice, auth, actions
├── themes/ # MUI theme, palette, typography, shadows, overrides
├── pages/
│ ├── api/api.js # CENTRAL API layer — every query/mutation function lives here
│ └── nearle/ # All operator pages, grouped by feature folder
└── components/
├── Loadable.js # React.Suspense wrapper for lazy routes
├── Loader.js # Full-page backdrop spinner
├── nearle_components/
│ ├── DebounceSearchBar.js # 500ms-debounced search input with ⌘/Ctrl+K focus shortcut
│ ├── LocationAutocomplete.js # Zone picker (has `pill` variant — use it for new pages)
│ ├── LoaderWithImage.js
│ ├── GlobalToast.js
│ ├── SearchBar.js
│ ├── TableLoader.js
│ └── TitleCard.js # LEGACY — do not use for new pages (replaced by the gradient Paper header in §6)
└── third-party/
└── OpenToast.js # Wrapper around notistack — use this for toast emissions
```
- **Absolute imports work from `src/`** thanks to `jsconfig.json` (`"baseUrl": "src"`). Prefer `import x from 'pages/api/api'` over deep relative paths. Look at the surrounding file's import style and match it.
---
## 6. Design system (Astryx + the `DT` tokens)
The UI is **Astryx** (`@astryxdesign/core@0.1.9`). MUI is legacy and on its way out — see §6.6. Discover components with the CLI, don't guess:
```bash
npx -y @astryxdesign/cli component <Name>
```
(The root README's `yarn dlx` form does not work here — `yarn` is not on PATH.)
### 6.1 Where the design system lives
| Module | What it owns |
|---|---|
| `themes/astryx.js` | **The theme.** `doormileTheme` — brand accent, card/input radii, component overrides. Mounted by `layout/MainLayout` and `login.js` via `<Theme theme={doormileTheme} mode="light">`. |
| `themes/dt/status.js` | **The status registry.** `STATUS_META`, `STATUS_ALIASES`, `getStatusMeta()` — every lifecycle state's label, hex, Astryx Badge variant, StatusDot variant, and icon. |
| `themes/dt/tokens.js` | `DT` (radii, shadows, surfaces, `DT.brand`), `STATUS` raw hexes, alpha helpers `a`/`tint`/`soft`/`ring`/`edge`. Its `*Sx` exports are **legacy MUI only**. |
| `themes/dt/primitives.js` | `TableScroll` (sticky-header + brand scrollbar wrapper), `AccentAvatar`, `softPopupStyle`. |
| `components/nearle_components/` | `PageHeader`, `StatCard`, `StatusBadge`, `StatusTabs`, `DebounceSearchBar`, `TableLoader`, … |
**Never redeclare these locally.** Seventeen pages once carried private `DT` copies and drifted (`orders.js` sat on `radiusCard: 16` and the old heavy shadows while everything else had moved to `14`); five pages carried private `STATUS_META` copies that disagreed on labels for the same order. Change the shared module so every page moves together.
### 6.2 Brand colour
**The brand is black (`#000000`)** — `DT.brand`, pinned into the theme as `--color-accent`. The palette is deliberately quiet: chrome is neutral, and colour carries *meaning only* (status). Do not introduce a second brand accent.
> Astryx's accent generator derives a ramp from the accent's hue and lightness. Black has neither, so feeding it `#000000` made the generator invent a hue and render every primary button magenta. That's why `themes/astryx.js` pins the accent tokens explicitly instead of using `color: { accent }`. Don't "simplify" it back.
Anything genuinely brand-toned should read `var(--color-accent)` / `DT.brand`, never a literal `#000000`.
### 6.3 Status palette (semantic — the only place colour is allowed to shout)
Never render a status by picking a colour by hand. Resolve it:
```jsx
import StatusBadge from 'components/nearle_components/StatusBadge';
<StatusBadge status={row.orderstatus} /> // handles casing, backend enums, unknowns
```
`getStatusMeta()` accepts a canonical key (`pending`), a raw backend enum (`miler_assigned`, `converted_to_consignment`), or any casing, and falls back to a neutral badge showing the raw string so an unmapped state is visible rather than blank.
| Meaning | Hex | Astryx `Badge` | `StatusDot` |
|---|---|---|---|
| Pending / waiting | `#f59e0b` | `yellow` | `warning` |
| Accepted / assigned | `#6366f1` | `blue` | `accent` |
| Arrived | `#06b6d4` | `cyan` | `accent` |
| Picked up | `#8b5cf6` | `purple` | `accent` |
| Active / in-transit | `#14b8a6` | `teal` | `success` |
| Delivered / success | `#10b981` | `green` | `success` |
| Skipped | `#f97316` | `orange` | `warning` |
| Cancelled / error | `#ef4444` | `error` | `error` |
| Neutral / unknown | `#94a3b8` | `neutral` | `neutral` |
Badges use the **tinted** non-semantic variants on purpose. Astryx reserves the solid `success`/`warning`/`error` variants for states that demand attention, and a table where every row shouts is a table where nothing does — `cancelled` is the one lifecycle state that keeps a loud variant.
The hexes in `STATUS` (tokens.js) are the raw layer, for surfaces taking an arbitrary accent (`StatCard color=`, `AccentAvatar`, chart series). Use `status.js` to *render*; use `STATUS` only when you need a bare colour.
### 6.4 Page anatomy
Follow this order. **`pages/nearle/hubs/hubs.js` is the reference implementation** — copy its structure.
1. **`<PageHeader>`** — title, `live` subtitle with pulsing `StatusDot`, and an `action` slot (primary `Button`, zone picker). Plain and un-boxed; the old gradient header Paper is gone.
2. **KPI row** — `<Grid columns={{ minWidth: 220 }} gap={3}>` of `<StatCard>`. `color` is a status hex, or `DT.brand` for the primary tile. No top-stripes, no rainbow.
3. **`<StatusTabs>`** — the status filter strip. Supply the tab *order* and where each count comes from; labels and icons resolve from the registry.
4. **Filter + search bar** — a `<Card padding={3}>` holding an `HStack justify="between"` with the row count on the left and `<DebounceSearchBar>` on the right.
5. **Table** — `<Card padding={0}>` → `<TableScroll minWidth={…}>` → `<Table density="balanced" dividers="rows" hasHover>`. `TableScroll` supplies the sticky header and scrollbar that MUI's `TableContainer` used to.
6. **Status cells** — `<StatusBadge>`. Never a hand-rolled tinted `Box`.
7. **Row actions** — `<IconButton size="sm" variant="ghost">`, `variant="destructive"` for delete. Always pass `label` **and** `tooltip`.
8. **Empty state** — `<EmptyState title description icon>` inside a full-width `TableCell colSpan={n}`. Never antd `<Empty/>`.
9. **Loading** — `<TableLoader>` for rows, `<Loader>` for a blocking full-page backdrop. These are not interchangeable.
### 6.5 Astryx rules and known gotchas
- **No `<div>` for layout.** `HStack` / `VStack` / `Grid` / `Center` / `Stack` do spacing and alignment. No raw `<span>` wrappers either.
- **No raw values.** Colours and spacing come from tokens (`var(--color-*)`, `var(--spacing-*)`, `var(--radius-*)`). An inline `style` is the escape hatch *only* for a caller-supplied status hex, which the token system genuinely can't express (see `StatCard`).
- **No `sx` prop** — it doesn't exist. Restyle through `themes/astryx.js` so every call site moves at once.
- **`Grid` takes `columns={{ minWidth: N }}`**, not `{ base, sm }` breakpoints.
- **`Table` children-mode cells have no `align` prop** — use `style`.
- **There is no `Tabs` and no `Stepper`** in 0.1.9. Use `TabList`/`Tab` (with `endContent` for counts, `TabMenu` for overflow) and `SegmentedControl`/`ProgressBar` for wizard steps.
- **`Spinner` maxes out at 18px** — large loaders are CSS rings.
- **`Selector` doesn't forward `ref`**; **`Typeahead` has no freeSolo**, which is why `AddressAutocomplete` is a `TextInput` plus a hand-rolled list.
- **Astryx has 153 components** — search before hand-rolling: `npx -y @astryxdesign/cli search "<thing>"`.
### 6.6 Migration status
Converted: the app shell, `login.js`, the maintenance pages, the entire shared-component layer, and the pages `hubs`, `vehicles`, `reports/{profitability, RidersRoutes, mapWithRoute}`.
Still MUI: 25 operator pages and 9 `src/themes/*` files. Four pages still import antd `<Empty/>`.
Rules while both stacks coexist:
- A page is converted **whole**, never half. Mixed-stack pages are how the palette drifts.
- `ThemeCustomization` must stay mounted in `App.js` until the final page converts; the MUI theme files and the `@mui/*` / `@emotion/*` / `antd` dependencies are removed only after that.
- `OrdersTableSkeleton` renders rows *into* a page's table, so it can only convert alongside its last consumer. Converted pages use `TableLoader` instead.
- Don't add a call site for anything marked ⛔ LEGACY in `tokens.js`.
---
## 7. Data layer rules
- **Every server call belongs in `src/pages/api/api.js`** as an exported async function. Page files should never construct URLs inline for `GET`s. Mutations are sometimes kept inline (e.g. tenant pricing update) — that's tolerated but discouraged.
- **Use TanStack Query for all reads.** Query keys must include every filter that affects the response so the cache invalidates correctly. Example:
```js
useQuery({
queryKey: ['fetchCountData', appId, userid, startdate, enddate, tenantid, locationid, riderid, tabstatus],
queryFn: () => fetchCountAPI(appId, userid, ...)
});
```
- **Use `useInfiniteQuery` for paginated rows.** `getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined`. Auto-drain pages with an `IntersectionObserver` on a sentinel `<div ref={loadMoreRef} />` placed at the bottom of the `<TableContainer>`. The canonical pattern lives in `deliveries.js` (search for `useInfiniteQuery({` and the adjacent `IntersectionObserver` setup).
- **Mutations go through `useMutation`** with `onSuccess` / `onError`. After a successful mutation, call `.refetch()` on every related query — the codebase does not yet use `queryClient.invalidateQueries`. Match the surrounding file.
- **Errors → `OpenToast(message, 'error', 2000)`** from `components/third-party/OpenToast`. Toasts always anchor top-right. Duration is 2000ms for normal, 3000ms for "must be acknowledged".
- **Skeleton loading**: use `OrdersTableSkeleton` for tables (props: `rowsPerPage` default 5, `col` default 1 — `col` is the count of variable middle columns; total rendered columns = `col + 4` since checkbox, serial, notes, status, and actions are always present). Use `Loader` for full-page modal backdrop and `LoaderWithImage` for inline "loading deliveries…" states.
---
## 8. State & auth
- **Auth state lives in `localStorage`**. The keys to know: `authname` (gate in `App.js` and `utils/session.js`'s `AUTH_PRESENCE_KEY` — unrelated to which backend authenticated the session, kept as the presence flag across the migration), `doormileToken` (the actual JWT sent as `Authorization: Bearer` on every `/admin/*` call — see `utils/doormileAxios.js`), `doormileUser`, `userid`, `roleid`, `tenantid`, `userfcmtoken`, `applocations` (cached zone list, now hub-derived). When adding auth-touching code, read these directly — there is no `useAuth()` hook.
- **The 401 redirect for the Doormile API** comes from `utils/doormileAxios.js` (auto-attaches the bearer token; on 401 does a full `localStorage.clear()` + hard navigate to `/login`, matching `utils/session.js`'s `performSessionLogout` contract). `src/utils/axios.js` is a separate, older, unrelated mock-service client — don't confuse the two.
- **Redux slices** live in `src/store/reducers/`. Use them only for cross-page state (FCM token, login user, sidebar menu open, global snackbar). Do **not** put per-page form state in Redux.
---
## 9. FCM / notifications
- Initialised once in `App.js` via `generateToken()` + `initFirebaseNotificationListener()` from `firebase_notification/notification.js`.
- After **any** mutation that affects a rider (assign, cancel, change rider), call `POST /utils/notifyuser` with the target rider's `userfcmtoken`. Don't forget to also call `notifyRider` mutation on success.
- Service worker file: `public/firebase-messaging-sw.js` — do not edit unless the user explicitly asks. Subtle bugs here cause silent delivery failures.
---
## 10. Routing & adding a new page
1. Create the page file under `src/pages/nearle/<feature>/<name>.js`.
2. Add the lazy import at the top of `src/routes/MainRoutes.js`:
```js
const MyPage = Loadable(lazy(() => import('pages/nearle/<feature>/<name>')));
```
3. Register the route inside the `nearle` children array.
4. Add a sidebar entry in `src/menu-items/nearle.js` (id, `<FormattedMessage id="..." />` title, url, icon).
5. Add the i18n key in the relevant `src/utils/locales/*.json` file (or wherever the project keeps them; most existing items use the English string as the id).
---
## 11. Common gotchas
- **`utc` plugin pollution**: `deliveries.js` extends dayjs with `utc` at module load. If you import dayjs elsewhere and call `.utc()` you'll get UTC behaviour even if you didn't ask. Match what the surrounding page does — `deliveries.js` deliberately bucket-parses in local time, not UTC, to stay in sync with the dispatch page.
- **One API base**: `REACT_APP_DOORMILE_URL` (`api.doormile.com/api/v1`), via `utils/doormileAxios.js`. See `src/pages/api/CLAUDE.md` for the full migration-era rule sheet, including what the new backend has no equivalent for.
- **The `applocationid` "zone" concept no longer exists as a filterable resource** on the new API — it was a jupiter-only concept. `LocationAutocomplete`/`fetchAppLocations` now derive a picker list from `GET /admin/hubs` instead of a real zones endpoint; most list endpoints on the new backend don't accept an `applocationid` filter at all. Don't assume it's wired through end-to-end on a page you haven't checked.
- **`role` gating** uses `localStorage.getItem('roleid')`. Some buttons are conditionally rendered based on it. Do not hide UI based on string equality alone — check existing patterns.
- **Skeleton vs Loader vs LoaderWithImage** — these are different. Skeleton = per-row placeholder, Loader = full-screen backdrop blocking interaction, LoaderWithImage = inline branded spinner. Don't swap them.
- **No Maps API key exists in this project anymore.** Address search/geocoding goes through `AddressAutocomplete.js` (Nominatim) and routing through OSRM — both free, no key. Do not add `process.env.REACT_APP_GOOGLE_MAPS_API_KEY` or any Google Maps script/dependency back in.
- **`Tenants.js` has known dangling references** (`setClientstatus`, `setState`, `setSuburb`, `setTenanatPricing`, `<Collapse in={open}>`) inherited from legacy code. They are tolerated. Do **not** "fix" them as a drive-by — they are out of scope and removing them risks breaking the row collapse contents.
---
## 12. Communication style for changes
- **For UI changes**, the user runs the dev server at `http://localhost:3000`. After editing a page, end with one short sentence telling them which route to reload (e.g. "Reload http://localhost:3000/nearle/pricing to see it").
- **Don't commit unless asked.** The project has `package-lock.json` + `yarn.lock` both present — match the user's last commit's lockfile choice before suggesting `npm install` vs `yarn install`.
- **Verify after edits** with a quick JSX parse check (the user has `acorn` available in `node_modules`) — do not assume the build will pass just because Edit succeeded.
- **Mark `// removed` / `// unused` comments as code smells** — delete dead code instead of commenting it out, unless the user explicitly says "keep it commented for now".
---
## 13. Cross-references
- **For the per-page API map and the architectural flow chart** (which endpoint each page calls, what the optimisation pipeline does, FCM flow), invoke the project skill **`nearlexpress-docs`** (`.claude/skills/nearlexpress-docs/SKILL.md`) rather than restating the content here.
- **For shared design patterns** between pages, the sources of truth are `src/themes/astryx.js` (the theme), `src/themes/dt/status.js` (lifecycle rendering), `src/themes/dt/tokens.js` (radii, shadows, raw hexes), and `src/themes/dt/primitives.js` (`TableScroll`, `AccentAvatar`). Import from them; don't redesign and don't re-declare locally. `src/pages/nearle/hubs/hubs.js` is the reference for how they *compose* into a page (header → KPIs → tabs → filter → table).
<!-- ASTRYX:START -->
Astryx v0.1.9 · 153 components
CLI: run every command as `yarn dlx @astryxdesign/cli <cmd>` (shown below as `astryx ...`).
SETUP (once, in your app entry e.g. main.tsx) — without these, components render unstyled:
import "@astryxdesign/core/reset.css";
import "@astryxdesign/core/astryx.css";
WORKFLOW — discover, don't guess. Before writing UI:
1. `astryx build "<idea>"` — START HERE: returns a kit (closest [page] + [block]s + [component]s). No args = full playbook.
2. `astryx template <name> [--skeleton]` — scaffold the [page]/[block]s it named, or study their layout. Templates are reference code.
3. `astryx component <Name>` — props + examples for every component you use.
RULES:
- No <div> — components do all layout/spacing. Full page → AppShell; sidebar nav → SideNav.
- Frame first: pick the shell (AppShell / Layout+LayoutPanel) and budget regions in px BEFORE writing content (`astryx docs layout`).
- Dense data = rows (Table, List/Item) edge-to-edge — never Card-wrapped list items. Card = dashboard widgets, galleries, settings groups only.
- Status → StatusDot/Token; Badge only for counts and enumerated states, never decoration.
- Custom styling: component props first; else style/className with tokens — var(--color-*|--spacing-*|--radius-*). No raw hex/px. (No StyleX/Tailwind compiler here — don't use xstyle/utility classes.)
- Tokens for every value (`astryx docs tokens`). Brand/accent via `astryx theme` — never override --color-* in :root.
- SELF-CHECK before you finish: re-read the file and replace any raw <div>/<span> layout, imported .css/@apply, or hardcoded value (#hex, 16px) with the component or a token (var(--color-*|--spacing-*|…)). If unsure a component/prop exists, run `astryx component <Name>` / `astryx search "<thing>"`; don't hand-roll CSS.
MORE CLI:
search "<query>" find any component / hook / doc / template / block
component --list 153 components by category
template --list page + block recipes
docs <topic> color, elevation, icons, illustrations, internationalization, layout, migration, motion, principles, shape, spacing, styling, theme, tokens, typography
swizzle <Name> eject component source for deep customization
upgrade --apply run after any @astryxdesign/core bump
<!-- ASTRYX:END -->