Files
loyaly-merchant/AGENTS.md

12 KiB
Raw Permalink Blame History

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 <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
    — 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 <Name> / astryx search "<thing>"; 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 <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.