Files
loyaly_cutomerweb/AGENTS.md
2026-09-25 16:31:10 +05:30

189 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- BEGIN:nextjs-agent-rules -->
# 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.
<!-- END:nextjs-agent-rules -->
<!-- ASTRYX:START -->
Astryx v0.2.0 · 154 components
CLI: run every command as `npx astryx <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 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 <div>/<span> 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 <Name>` / `astryx search "<thing>"`; don't hand-roll CSS.
MORE CLI:
search "<query>" find any component / hook / doc / template / block
component --list 154 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 -->
<!-- LOYALY:START -->
# 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 `<Icon>` 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.
- `<AttributionNote basis={…} />` 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/<Component>/*.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 `<div>` 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
`<Icon icon="search"/>`; everything else is `<Icon icon={ICONS.stores}/>`. 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.
<!-- LOYALY:END -->