# This is NOT the Next.js you know This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. Astryx v0.2.0 · 154 components CLI: run every command as `npx astryx ` (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 ""` — START HERE: returns a kit (closest [page] + [block]s + [component]s). No args = full playbook. 2. `astryx template [--skeleton]` — scaffold the [page]/[block]s it named, or study their layout. Templates are reference code. 3. `astryx component ` — props + examples for every component you use. RULES: - No
— 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 Tailwind utilities backed by tokens (bg-surface, text-primary, rounded-lg) via tailwind-theme.css. No raw hex/px. - 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 style={{…}}, raw
/ layout, imported .css/@apply, or hardcoded/arbitrary value (e.g. bg-[#fff], p-[13px]) with the component or a token-backed utility. If unsure a component/prop exists, run `astryx component ` / `astryx search ""`; don't hand-roll CSS. MORE CLI: search "" find any component / hook / doc / template / block component --list 154 components by category template --list page + block recipes docs color, elevation, icons, illustrations, internationalization, layout, migration, motion, principles, shape, spacing, styling, theme, tokens, typography swizzle eject component source for deep customization upgrade --apply run after any @astryxdesign/core bump # Loyaly Merchant Dashboard — project rules ## Toolchain - **Node 22 is required** (`.nvmrc` → 22.23.1). The astryx CLI hard-fails on Node 20. `nvm use` before running any `npx astryx` command or `npm run theme:build`. - Dev server runs on **port 3100** (`lytsup-site` owns 3000). ## Theme - All design values live in `src/theme/loyalyTheme.ts`. Edit that, then run `npm run theme:build`, then **commit the generated** `src/theme/loyaly.{css,js,d.ts,variants.d.ts}`. - `theme:build` is deliberately NOT wired to `prebuild` — it would break `npm run build` on Node 20. - Import the theme from **`@/theme`**, never `@/theme/loyaly` directly. `@/theme` re-attaches the Lucide icon registry, which `astryx theme build` cannot serialise (icons are React components). Skipping it silently degrades every `` to Astryx's default glyph set. - `globals.css` imports `../theme/loyaly.css`, NOT `@astryxdesign/theme-neutral/theme.css`. Astryx theme CSS is `@scope`-gated on the theme *name*, so neutral's rules match nothing under `[data-astryx-theme="loyaly"]`. ## The monochrome rule Gray is the accent. The **only** colour permitted is semantic: success, warning, error. - Astryx's categorical hues (blue/cyan/teal/purple/pink/orange) are aliased to gray in `aliasHueToGray()`. green/red/yellow deliberately keep hue — Banner's semantic statuses resolve *through* those hue tokens. - Some neutral-theme component styles hardcode colour instead of reading a token (`badge variant:info`, `progressbar variant:accent`). Those need an explicit `components` override; `aliasHueToGray()` cannot reach them. - `/tokens` (dev-only route) is the audit page. After any theme change, load it and run the chromatic sweep in the browser console — it should report **0** non-semantic colour nodes. ## The two brand accents (exception to the above) Store intelligence carries exactly **two** non-semantic hues, and nothing else may add a third. | | token | light | dark | used for | |---|---|---|---|---| | warm | `--color-brand-warm` | `#F4C430` | `#F4C430` | brand, rewards, the activities a merchant runs | | cool | `--color-brand-cool` | `#7C3AED` | `#A78BFA` | analytics, journeys, AI insight | - Defined in `src/app/globals.css` in a Tailwind `@theme` block, **not** in `loyalyTheme.ts`. They are not Astryx tokens: no component variant resolves through them, and putting them in the theme would let Badge/Banner pick them up as part of the semantic language. - Three slots per hue. `-ink` is the readable one for text and icons (light mode darkens warm to `#8A6300`; raw `#F4C430` on white is ~1.7:1). `-soft` is an icon-chip tint only — never a card background. The base is for non-text marks: chart fills, dots, bars. - `src/shared/utils/accent.ts` is the only place a hue becomes a class name. Feature files use `ACCENT[accent].ink` / `.soft`, never `text-brand-warm-ink` by hand. - Charts stay monochrome by default. `CHART.brand.{warm,cool}` is opt-in per series and is used on exactly two charts (dashboard Footfall = cool, Revenue = warm). Never make it a ramp default — `Sparkline` reads `seriesAt(0)`, so that would turn every KPI card's trend line. - An activity's accent travels on its API payload (`ActivityMetric.accent`), not from grid position, so an activity is the same colour on every screen. ## Attribution is estimated — say so Everything downstream of `ActivityImpact.customers` (repeat visits, purchases, revenue) is **modelled**, not measured. No purchase is joined to a specific spin, selfie or challenge. - The field is `attributedRevenueInr`, never `revenueInr`, and every payload carries `attribution: 'estimated' | 'observed'`. - Copy says *attributed*. Never "generated", "earned" or "drove". A merchant who reads "Selfie generated ₹3.4L" and spends against it is the failure this rule prevents. - `` sits in the header of every panel that shows an attributed figure, and renders **nothing** when the basis is `'observed'` — so a real attribution backend retires the disclosure with no copy edit. Read the basis off the payload, never hardcode it. - The single sentence lives in `ATTRIBUTION_NOTE` (types/intelligence.ts) so three panels cannot drift. ## Dashboard supporting analytics are collapsed by default The six preserved supporting panels (conversion pair, peak hours, reward usage, period rollup, store comparison) sit inside a `Collapsible`, closed by default, state in `loyaly.dashboard.supporting-analytics`. Expanded they add ~1,700px desktop / ~3,100px mobile and push "What needs your attention" — the only actionable section — to 81% of the scroll. They are evidence, not the finding. Do not re-expand by default; do not delete them either. ## Known upstream issues (Astryx 0.2.0) - **`useEntryAnimation` breaks SSR hydration.** Its source assumes `'use client'` modules never run on the server; in the App Router they do. Any `FieldStatus` (i.e. `TextInput`/`TextArea` `status={...}`) present at *initial paint* renders without the slide-down class on the server and with it on the client → an unpatchable class mismatch. Setting status on blur/submit (the normal path) is unaffected. Don't server-render a field that already has a status. - Component keys in `defineTheme({components})` are inconsistently cased: `text-input` but `progressbar`. The CLI warns on unknown keys, but that warning is **not** reliable in either direction: - A *misspelled* key (`textinput`) is dropped silently — the CSS is never emitted. - A key the CLI doesn't know but Astryx *does* use (`table-cell`, `table-header-cell`) warns yet still emits correctly. So never trust the warning alone. After any `components` change, grep the generated `src/theme/loyaly.css` for the rule, then confirm the computed style in the browser. Valid targets are whatever `themeProps('...')` is called with in `dist//*.js`. - `StyleOverrides` supports structural pseudo-classes, not just interaction ones — `':first-child'` emits correctly (used for the table first-column lead-in). ## Deployment — never ship `.next/` wholesale This filled the production disk and took the server down once. A working tree's `.next/` reaches **2+ GB**, but almost none of it is runtime state: | path | size (typical) | ships? | |---|---|---| | `.next/dev` | 1.8 GB | **no** — Turbopack dev-server cache from `npm run dev` | | `.next/cache` | 120–160 MB | **no** — incremental build cache, build host only | | `.next/server`, `types`, `trace` | ~24 MB | **no** — inputs to the standalone trace, not read at runtime | | `.next/standalone` | 54 MB | **yes** — server.js + traced node_modules | | `.next/static` | 3 MB | **yes** — hashed client assets (nginx serves at `/_next/static/`) | | `public` | 344 KB | **yes** | Those last three are the entire payload: **~55 MB unpacked, 18 MB gzipped.** - Non-Docker deploy: `npm run bundle` → `dist/loyaly-mer-login.tar.gz`. Never `scp -r .next`. - Docker: `.dockerignore` excludes `node_modules` and `.next`. **Do not delete it** — without it `COPY . .` pushes the host's 2 GB `.next` and 639 MB `node_modules` into the build context, including darwin-arm64 sharp binaries that are wrong for Alpine. - `npm run clean` drops `.next`, `dist` and the tsbuildinfo when the tree gets heavy. **`CI_BUILD=1` in container builds.** Next 16.3 enables `turbopackFileSystemCacheForBuild` by default; it writes 117 MB to `.next/cache` that only pays off when that directory is restored between builds. Docker/Dokploy starts from a clean layer every time, so it is written and never read. The Dockerfile sets `CI_BUILD=1`, which flips the flag off in `next.config.ts`: **~20% less build CPU** (32s vs 40–43s measured) and `.next` drops 202 MB → 86 MB. Leave it unset locally — repeat `npm run build` there does reuse the cache. It changes no output: the deployable payload is 58 MB either way. ## Conventions - Follow the Astryx rules above: no raw `
` for layout, no hardcoded hex/px, component props first then token-backed Tailwind utilities. - `src/lib/icons.ts` is the single icon map. Only the 26 semantic names resolve via ``; everything else is ``. Never import a lucide icon directly into a feature file. ## Settings spacing contract Every Settings screen renders inside `src/features/settings/SettingsPage.tsx`. Do not add page padding in an individual settings page — change the container instead. | | value | why | |---|---|---| | Horizontal | 32px (`paddingInline={8}`) | clears the sub-nav and the Loyaly AI rail | | Top | 32px (`paddingBlock={8}`) | title never touches the header | | Bottom | 48px (`className="pb-12"`) | last card is never flush to the fold | | Header → body | 32px (`gap={8}`) | title block reads as its own layer | | Between body blocks | 24px (`gap={6}`) | cards, tables, toolbars | `SpacingStep` stops at 10 (40px) and Stack has no `paddingBlockEnd`, which is why the 48px bottom goes through the Tailwind bridge — `pb-12` still resolves to `--spacing-12`, so no raw pixel value enters the codebase. It sits in the `utilities` layer, which the cascade puts after `astryx-base`, so it wins over `paddingBlock` without `!important`. Card padding (24px) and table cell padding (12px, 20px first-column lead-in) are set once in `loyalyTheme.ts` so every module agrees — not per page.