diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..9f9b44c --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,16 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "loyaly-staff-dev", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "dev"], + "port": 3200 + }, + { + "name": "loyaly-staff-attach", + "url": "http://localhost:3200", + "port": 3200 + } + ] +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..432c2e7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,57 @@ +# Docker build context excludes. +# +# WITHOUT this file, the `COPY . .` in the builder stage ships the host's +# .next/ (2.1 GB, almost all of it the Turbopack dev cache) and node_modules/ +# (639 MB, with darwin-arm64 sharp binaries that are wrong for Alpine) into +# the build context. That is what filled the production disk. + +# Dependencies — reinstalled from the lockfile in the deps stage +node_modules +.pnp +.pnp.* +.yarn + +# Build output — regenerated by `npm run build` in the builder stage. +# .next/dev alone is 1.8 GB of dev-server cache that must never leave the host. +.next +out +build +dist + +# VCS + local tooling +.git +.gitignore +.github +.claude +.vscode +.idea + +# Incremental compiler state (253 KB and host-specific) +*.tsbuildinfo +next-env.d.ts + +# Environment. +# +# `.env` IS copied in (see the Dockerfile's runner stage) — it holds the +# production platform host, which is not a secret, and is what the standalone +# server reads at boot. Excluding it is what shipped an image with no +# LOYALY_API_BASE and made every BFF call fail. +# +# `.env.*` stays out, and that exclusion is load-bearing: a developer's +# `.env.local` points LOYALY_API_BASE at http://127.0.0.1:8088, and @next/env +# loads `.env.local` AHEAD of `.env`. One leaked into the image and the +# deployed console calls localhost — silently, with no error to read. +.env.* +*.pem + +# Docs and infra that the build does not read +*.md +Dockerfile +.dockerignore +nginx.conf + +# Noise +.DS_Store +coverage +npm-debug.log* +yarn-error.log* diff --git a/.env b/.env new file mode 100644 index 0000000..303636b --- /dev/null +++ b/.env @@ -0,0 +1,79 @@ +# --------------------------------------------------------------------------- +# Production runtime configuration. COMMITTED ON PURPOSE — carries no secret. +# --------------------------------------------------------------------------- +# +# This file is the production environment. It is read by `next build` and, more +# importantly, by the standalone `server.js` at boot (Next calls loadEnvConfig +# on the server's working directory), so the deployed container knows the +# platform host without anyone remembering to type it into a dashboard. +# +# ── Precedence, exactly as @next/env resolves it ──────────────────────────── +# +# 1. real process.env (Dokploy / docker -e / systemd) ← always wins +# 2. .env.production.local +# 3. .env.local ← LOCAL DEV ONLY. Never enters the image. +# 4. .env.production +# 5. .env ← this file, the floor everything falls back to +# +# A value already present in process.env is never overwritten by a file, so +# setting LOYALY_API_BASE in Dokploy still overrides this — nothing here locks +# the deployment in. It only removes "unset" as a possible state. +# +# ── Working on this locally? ──────────────────────────────────────────────── +# Put your overrides in `.env.local` (gitignored, loaded ahead of this file). +# Without one, `npm run dev` will talk to the PRODUCTION platform, because that +# is what this file says. `.env.example` has the local values to copy. + +# The one shared Loyaly platform API (Behavision). Server-side only and +# deliberately NOT NEXT_PUBLIC: publishing the host would let a browser bypass +# the BFF, which is what keeps the access token out of JavaScript. +# +# NOT platform.loyaly.ai — that host serves THIS console, not the API. Pointing +# the variable there makes the BFF call its own origin, which fails in a way +# that looks like a broken login form rather than a misconfiguration. +# apiClient.ts rejects that hostname by name for exactly this reason. +# +# NOT REQUIRED in production any more. Production accepts exactly one origin, so +# an unset variable could never have meant another one, and platformApi resolves +# it to that origin on its own. It stays here so `docker run` is self-describing +# and so development has something to read. +# +# Why that change was needed: @next/env only fills a variable that is ABSENT. +# Verified against the installed copy — a real environment variable set to the +# EMPTY STRING stays empty and this file is NOT consulted. So one blank field in +# a dashboard silently defeated the value below and took production down with +# "LOYALY_API_BASE is required in production". +LOYALY_API_BASE=https://mcp.loyaly.ai + +# Browser → this app's own BFF routes, which are same-origin. Empty is correct +# and is what makes the console work on any hostname it is served from: +# requests go to /api/... on whatever origin loaded the page (localhost:3100 in +# dev, platform.loyaly.ai in production) and the server hop above reaches the +# platform. Setting this to the platform host would send the browser straight +# at the API with no session cookie and no token — do not. +# +# It is NEXT_PUBLIC, so it is inlined at BUILD time, not read at runtime. +# Changing it in Dokploy's environment panel would do nothing without a rebuild. +NEXT_PUBLIC_API_BASE= + +# AUTH_SECRET is deliberately NOT in this file. It is the ONLY variable this +# deployment requires, and the only one that cannot ship. +# +# It signs the session cookie and encrypts the platform token bundle, so a +# value committed here is a session-forging key in git — anyone who can read +# the repo could mint a cookie for any user. It was already removed from the +# Dockerfile once for that reason; do not reintroduce it here. +# +# Set it as a Dokploy environment variable in the RUNTIME panel — a value set as +# a BUILD argument is not present when the server runs, which looks exactly like +# never having set it. Alternatively mount the value and set AUTH_SECRET_FILE to +# its path (the Docker/Swarm secret convention); AUTH_SECRET wins if both exist. +# +# Production refuses to sign sessions without it. Generate with: +# +# openssl rand -hex 32 +# +# Hex, not base64: a base64 value ends in '=' and can contain '+' and '/', and +# an environment editor that splits a line on the first '=' can store that +# truncated or empty. A silently-empty AUTH_SECRET looks exactly like an unset +# one, which is a slow afternoon. Hex has nothing a parser can mangle. diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a2ab085 --- /dev/null +++ b/.env.example @@ -0,0 +1,42 @@ +# --------------------------------------------------------------------------- +# Template for `.env.local` — your LOCAL overrides. Copy it: +# +# cp .env.example .env.local +# +# Do not copy it to `.env`. `.env` is committed and already holds the +# production values; `.env.local` is loaded ahead of it and is gitignored. +# --------------------------------------------------------------------------- + +# The one shared Loyaly platform API (Behavision). Server-side only and +# deliberately NOT NEXT_PUBLIC: publishing the host would let a browser bypass +# the BFF, which is what keeps the access token out of JavaScript. +# +# local dev http://127.0.0.1:8088 ← what belongs in .env.local +# production https://mcp.loyaly.ai ← already set in the committed .env +# +# NOT platform.loyaly.ai — that host serves THIS console, not the API. Pointing +# the variable there makes the BFF call its own origin, which fails in a way +# that looks like a broken login form rather than a misconfiguration. +# +# Production no longer requires this: it accepts exactly one origin, so an unset +# value can only have meant that one, and platformApi resolves it. Any OTHER +# host set explicitly is still rejected. Locally it is worth setting, because a +# dev machine legitimately means a different address. +LOYALY_API_BASE=http://127.0.0.1:8088 + +# Signs the session cookie and encrypts the platform token bundle. +# +# The ONLY variable production requires, the only real secret, and the only one +# taken solely from the environment — it is in no committed file, by design. +# Set it as a Dokploy environment variable in the RUNTIME panel (a build +# argument is not present at runtime), or mount it and set AUTH_SECRET_FILE to +# its path. Locally, any string works; leave it blank and a development key is +# used. +# +# Generate with: openssl rand -hex 32 (hex, not base64 — a trailing '=' can be +# mangled by a dashboard env editor that splits on the first '=') +AUTH_SECRET= + +# Browser → this app's own BFF routes. Same origin, so leave it empty. Inlined +# at BUILD time (NEXT_PUBLIC), so changing it at runtime does nothing. +NEXT_PUBLIC_API_BASE= diff --git a/.gitignore b/.gitignore index 5ef6a52..9b0122f 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ # production /build +/dist # misc .DS_Store @@ -30,8 +31,23 @@ yarn-debug.log* yarn-error.log* .pnpm-debug.log* -# env files (can opt-in for committing if needed) -.env* +# env files. +# +# `.env*` used to be blanket-ignored, and that was the deploy bug: the image +# shipped with no LOYALY_API_BASE at all, so every BFF call died on "required +# in production — refusing to guess the Loyaly platform host" and the console +# looked like a broken login form. The production host is not a secret, so it +# now lives in a committed `.env` and ships with the build. +# +# What stays ignored is the per-machine and per-secret layer: +.env.local +.env.*.local +# +# What is committed: +# .env production defaults (no secret) — loaded by the running server +# .env.example the template, the record of which variables exist +# +# AUTH_SECRET belongs in neither. It is injected by Dokploy at runtime. # vercel .vercel diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..f9e7451 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22.23.1 diff --git a/AGENTS.md b/AGENTS.md index 643577d..d916a0d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,3 +7,182 @@ This version has breaking changes — APIs, conventions, and file structure may 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. + diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..82d1316 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,120 @@ +# Architecture + +How this codebase is organised, and the rules that keep it that way. Read this +before adding a feature; it should take about five minutes. + +## The layers + +Data flows in one direction, and each layer is allowed to know only the one +below it: + +``` + component / page renders state, owns no rules + ↓ + features//hooks binds a service or repository to React + ↓ + features//services domain rules: validation, interpretation, defaults + ↓ + features//repositories TRANSPORT ONLY — the single place that knows a URL + ↓ + app/api//route.ts HTTP boundary: authorises, parses, responds + ↓ + features//mock/*.mock fixtures, server-side only +``` + +**Connecting a real backend touches the repository layer and nothing else.** +Point `NEXT_PUBLIC_API_BASE` at a host, or rewrite the four lines in a +repository, and every component above it is untouched. That is the property the +whole structure exists to protect, so: + +- **Never** `import` from a `mock/` module in a component, hook or page. Server + Components read through a `*ServerRepository`; clients go over HTTP. +- **Never** call `fetch` in a component. `shared/services/httpClient` is the + only module that does. +- A repository never interprets a response, and a service never builds a URL. + +## Where things live + +``` +src/ + app/ routing only — thin files, no logic + (public)/login unauthenticated routes → PublicLayout → GuestGuard + (workspace)/… authenticated routes → ProtectedLayout → AuthGuard + api/ route handlers + proxy.ts the server-side auth gate (Next 16 renamed middleware → proxy) + features// + components/ UI for this feature only + hooks/ data access + view state + services/ domain rules, framework-free, unit-testable + repositories/ URLs and verbs + types/ the wire contract for this feature + mock/ fixtures (server-side) + utils/ · config/ · guards/ · providers/ as needed + shared/ + components/ brand · charts · patterns · primitives · scope · data · motion + hooks/ useResource · useScope · useBreakpoint · usePersistentFlag + layouts/ ProtectedLayout · PublicLayout · workspace shell + providers/ app-wide state (workspace scope) + services/ httpClient (transport) · apiRoute (handler helpers) + types/ the response envelope + mock/ seeded RNG shared by every fixture + theme/ design tokens; edit loyalyTheme.ts, run `npm run theme:build` +``` + +A feature owns everything about itself. If two features need the same thing, it +moves to `shared/` — it does not get imported across features, with one +deliberate exception: `features/dashboard/types` holds the analytics primitives +(`TimePoint`, `ActivityEvent`) that LYTs and Stores genuinely share, because +duplicating them would let two modules disagree about the same wire shape. + +## Authentication + +Three independent gates, in order of authority: + +1. **`src/proxy.ts`** — runs before any protected route renders. No valid + session cookie, no page. API paths get a 401 JSON; pages get a redirect to + `/login?next=…`. This is the one that matters. +2. **`requireApiSession()`** — every data route handler calls it. Next's own + docs warn that proxy coverage can be silently lost by a matcher change or a + route move, so the check is repeated where the data is. +3. **`AuthGuard` / `GuestGuard`** — client-side. Covers what the server never + sees: a session expiring in an open tab, a logout in another tab, a + client-side navigation. **Not a security boundary.** + +The session is a signed httpOnly cookie (`features/auth/services/sessionToken`), +so the browser cannot read or forge it. `SessionProvider` holds only what to +draw, never what to permit — it asks `GET /api/auth/session` and believes the +answer. + +Swapping the mock for a real identity provider means changing +`verifyCredentials` in `features/auth/mock/users.mock.ts` and the three URLs in +`authRepository`. Nothing else, including the login form, is aware. + +## Conventions + +- **No `any`.** The codebase has zero, and `tsc --noEmit` is expected to pass + before every commit. +- **Components stay under ~300 lines.** When one grows past that, the split is + usually behaviour-into-a-hook, not markup-into-more-markup. +- **Names say what the thing is**: `DashboardKpiCard`, `StaffAttendanceTable`. + Never `Card.tsx`, `utils.ts`, `NewFile.tsx`. +- **Styling comes from the design system**, in this order: component props → + token-backed Tailwind utilities → a theme override in `loyalyTheme.ts`. There + are no per-feature CSS files; see the note below. + +### On CSS + +The brief that produced this refactor asked for per-feature stylesheets +(`dashboard/styles/kpi.css`, and so on) so that changing one module cannot +affect another. That isolation is already total here, by construction rather +than by convention: styling lives in component props and utility classes scoped +to the element they are written on, so there is no selector in the codebase +that *can* reach another feature. Adding stylesheets would introduce global +selectors — the one mechanism that can leak — and they would have to fight +StyleX's `@layer astryx-base` for the cascade. Shared visual decisions belong in +`theme/loyalyTheme.ts`, which is the single place a change is meant to be +system-wide. + +If a future component genuinely needs CSS that props and utilities cannot +express, `astryx swizzle ` ejects the source for that one component +rather than opening a stylesheet the whole app can be edited through. diff --git a/CLAUDE.md b/CLAUDE.md index 43c994c..bd4c824 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1 +1,32 @@ @AGENTS.md + + +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 + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d82e645 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,154 @@ +# syntax=docker/dockerfile:1 + +# Stage 1: Install dependencies +FROM node:22-alpine AS deps +RUN apk add --no-cache libc6-compat +WORKDIR /app + +COPY package.json package-lock.json ./ +# devDeps are required to build (typescript, tailwind, eslint-config-next). +# This whole stage is discarded — none of it reaches the runner. +RUN npm ci --no-audit --no-fund + +# Stage 2: Build the Next.js application +FROM node:22-alpine AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +ENV NEXT_TELEMETRY_DISABLED=1 +ENV NODE_ENV=production +# Each Docker build starts from a clean layer, so Turbopack's .next/cache is +# written but never restored. Skipping it cuts ~20% of build CPU (the metric +# that matters on a 1-vCPU host) and 116 MB off this layer. +ENV CI_BUILD=1 + +RUN npm run build + +# Stage 3: Production runner with Next.js Standalone +FROM node:22-alpine AS runner +RUN apk add --no-cache libc6-compat +WORKDIR /app + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +# ── Runtime configuration ──────────────────────────────────────────────── +# +# EXACTLY ONE variable must be supplied to this container. That is the whole +# deployment contract, and it is one because everything else either ships in +# the image or can only have one legal value. +# +# AUTH_SECRET signs the session cookie and encrypts the platform token +# bundle. Generate with: openssl rand -hex 32 +# → NOT shipped, and never can be: a secret in the image is +# readable with `docker history`, and a secret in git is a +# session-forging key for anyone who can read the repo. +# → Set it in Dokploy → Environment (the RUNTIME panel — a +# BUILD argument is not present when the server runs), or +# mount it and set AUTH_SECRET_FILE to its path. +# +# AUTH_SECRET_FILE optional alternative: a path to read the secret from, the +# standard Docker/Swarm secret convention. AUTH_SECRET wins +# when both are set. Use this when a dashboard field mangles +# the value. +# +# LOYALY_API_BASE no longer required. Production accepts exactly one origin +# (https://mcp.loyaly.ai), so an unset variable could never +# have meant anything else; shared/config/platformApi now +# resolves it to that origin. Setting it to any OTHER host +# is still rejected by name. It also still ships in the .env +# copied below, which keeps `docker run` self-describing. +# +# Hex rather than base64 for the secret, on purpose. `openssl rand -base64 48` +# ends in '=' and may contain '+' and '/'. Pasted into a dashboard field or a +# KEY=VALUE editor that splits on the first '=', that value can be stored +# truncated — or not at all — and the result is indistinguishable from never +# having set it. Hex is [0-9a-f] only, so there is nothing for a parser to +# mangle. 32 bytes is 256 bits, more than the HMAC and the AES-256 key derived +# from it need. +# +# A container started without the secret does not die and does not 502. It +# boots, names the missing variable on stderr (including any environment +# variable whose NAME looks like a near-miss for AUTH_SECRET, which is the one +# cause invisible from a dashboard), and answers 503 with +# `x-loyaly-config: misconfigured` on every gated request. +# +# ── Where the secret must be set in Dokploy ────────────────────────────── +# The "Environment Variables" tab. NOT "Build Arguments" and NOT "Build +# Secrets": Dokploy's own documentation is explicit that both of those are +# build-time only and are absent from the running container, so a secret placed +# there is indistinguishable, from inside the container, from never having been +# set at all. The boot log says which of the two happened. +# +# ── Two probe endpoints, deliberately separate ─────────────────────────── +# /api/health LIVENESS — 200 whenever the process answers. Safe to probe +# unconditionally; can never remove a serving +# container from rotation. +# /api/ready READINESS — 503 while a required variable is missing. Meant +# for a DEPLOY gate, in Dokploy → Advanced → Swarm +# Settings, paired with Update Config +# `Order: start-first` + `FailureAction: rollback` +# so a misconfigured new task is rolled back while +# the previous good one keeps serving. + +# Run as a non-root user; nextjs owns nothing it does not need to write. +RUN addgroup -g 1001 -S nodejs && adduser -u 1001 -S nextjs -G nodejs + +# Copy public static assets and standalone build output. +# These three paths are the ENTIRE runtime payload (~57 MB). Never copy the +# whole .next/ directory here — .next/dev and .next/cache are build-host-only +# and account for ~1.96 GB. +COPY --from=builder --chown=nextjs:nodejs /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +# The production environment, as a file the server reads at boot. +# +# Deliberately redundant, and worth keeping. `next build` already copies .env +# (and .env.production, and nothing else — see writeStandaloneDirectory in +# next/dist/build/index.js) into .next/standalone, so the line above lands one +# at /app/.env on its own. But it only does that when .env was in the BUILD +# CONTEXT, and .dockerignore excluded it until recently — which is precisely +# how images shipped with no LOYALY_API_BASE at all. +# +# This line turns that silent outcome into a loud one: exclude .env again and +# the Docker build FAILS here with "file not found" instead of producing an +# unconfigured image that starts and then rejects every sign-in. +# +# It does not pin the deployment either way: @next/env never overwrites a +# variable already present in process.env, so anything set in Dokploy wins. +COPY --chown=nextjs:nodejs .env ./.env + +USER nextjs + +EXPOSE 3000 + +# NO HEALTHCHECK, on purpose. +# +# One was added here and removed within the hour, because it recreated the +# exact 502 it was meant to replace. Dokploy runs applications as Docker Swarm +# services, and Swarm does not merely REPORT an unhealthy task — it pulls it +# out of the service load balancer and reschedules it. So a healthcheck wired +# to /api/health, which answers 503 while a required variable is missing, meant: +# +# AUTH_SECRET unset -> /api/health 503 -> task unhealthy -> removed from the +# load balancer and restarted -> Traefik has no backend -> 502 Bad Gateway on +# every url, which is precisely the symptom this whole change exists to end. +# +# The container would have been up, serving a 503 that names the fault, and +# nobody could have reached it. "A broken deploy must not look healthy" is a +# real concern, but enforcing it in the orchestrator destroys the diagnostics — +# and an outage you cannot see the reason for is the more expensive failure. +# +# So: the container stays in rotation whenever it can serve HTTP at all, and +# the configuration state is reported where it can actually be read — 503 with +# `x-loyaly-config: misconfigured` on every gated request, /api/health for a +# direct answer, and the named variable in the boot log. +# +# If a healthcheck is ever added back, it must probe LIVENESS (is the server +# answering?) and never configuration, or this comment is being relearned. + +CMD ["node", "server.js"] diff --git a/docs/API-GAP-REPORT.md b/docs/API-GAP-REPORT.md new file mode 100644 index 0000000..678f478 --- /dev/null +++ b/docs/API-GAP-REPORT.md @@ -0,0 +1,41 @@ +# API gap report — after backend integration + +Generated 2026-09-09. Format: `Feature | Existing API | Frontend status | Missing API` + +## Wired to the platform + +| Feature | Existing API | Frontend status | Missing API | +|---|---|---|---| +| Login | `POST /api/auth/login` | Wired | — | +| Session restore | `GET /api/auth/me` | Wired — confirmed on every load | — | +| Token refresh | `POST /api/auth/refresh` | Wired — single-flight, persist-before-use | — | +| Logout | `POST /api/auth/logout` | Wired — revokes upstream, then clears | — | +| Site switcher / Store page | `GET /api/sites` | Wired | — | +| Dashboard Visitors | `GET /api/reports/footfall` | Wired — server `total` | — | +| Dashboard Purchases / Revenue / Conversion | `GET /api/reports/conversion` | Wired | — | +| Footfall chart | `GET /api/reports/footfall?bucket=day` | Wired | — | +| Revenue chart · Sales page | `GET /api/reports/conversion?bucket=day` | Wired | — | +| Recent arrivals · Activity page | `GET /api/visits` | Wired — cursor echoed, deduped on `visit_id` | — | +| Customer photos | `GET /api/faces/…` | Wired — proxied so `` works | — | +| Team | `GET /api/team` | Wired | — | +| Mobile → dashboard purchases | `POST /api/purchases` | Wired via `POST /api/visits` | `GET /api/purchases` | + +## Cannot be wired — backend required + +| Feature | Existing API | Frontend status | Missing API | +|---|---|---|---| +| LYT programme | none | Unavailable panel | `GET /api/rewards`, `/api/rewards/redemptions`, `/api/lyts/ledger` | +| Engagement activities | none | Unavailable panel | `GET /api/activities`, `/api/activities/impact` | +| Campaigns | none | Unavailable panel | `GET /api/campaigns` | +| Customer journey | partial (visit/purchase/return only) | Unavailable panel | `GET /api/reports/journey` | +| Orders / products / stock | none | Unavailable panel | `GET /api/purchases`, `/api/products` | +| Payment split / refunds | none | Unavailable panel | `GET /api/reports/payments`, `/api/reports/refunds` | +| Staff attendance & ranking | `/api/team` is console accounts | Unavailable panel | `GET /api/staff`, `/api/staff/attendance`, `/api/reports/staff-sales` | +| Business profile | none | Unavailable panel | `GET/PATCH /api/settings/profile` | +| Roles matrix · Integrations · API keys · Billing | none | Still local state | `GET /api/settings/{roles,integrations,api-keys,billing}` | +| Security → sessions | `GET/DELETE /api/auth/sessions` | **Not yet wired** — endpoint exists | — | +| Invitations / join flow | `POST /api/team/invitations`, `GET /api/auth/invitation`, `POST /api/auth/register` | **Not yet wired** — endpoints exist, service written | — | +| Loyaly AI chat | none | Still mock replies | `POST /api/ai/chat` | +| Live arrivals stream | `GET /api/visits/stream` | **Not yet wired** — polling only | — | +| Cameras / site health | `GET /api/cameras`, `/api/sites/{id}/check` | **Not yet wired** — service written | — | +| Visitor directory | `GET /api/visitors`, `/history`, `PUT /profile`, `DELETE` | **Not yet wired** — service written | — | diff --git a/docs/API-INVENTORY.md b/docs/API-INVENTORY.md new file mode 100644 index 0000000..f43ff20 --- /dev/null +++ b/docs/API-INVENTORY.md @@ -0,0 +1,279 @@ +# Loyaly Merchant OS — API inventory & backend wiring plan + +Audit date: 2026-09-09 · Branch `main` · Audited against the running app on :3100 + +Purpose: establish exactly what data the frontend consumes today, which of it is +fake, and what a real backend must expose. Compare this against your API spec and +mark each row **match / rename / missing / extra**. + +--- + +## 1. How data flows today + +``` +component → hook (useResource) → repository (owns the URL) → httpClient + │ + NEXT_PUBLIC_API_BASE ────┤ + │ + (unset) → Next route handler → fixture generator + (set) → YOUR BACKEND +``` + +**The seam already exists and works.** `src/shared/services/httpClient.ts` line 22: + +```ts +const BASE = process.env.NEXT_PUBLIC_API_BASE ?? ''; +``` + +Set that to your API host and every endpoint in §2 bypasses the local route +handlers entirely. No component changes. This is the intended cutover. + +**Response envelope** — every endpoint must return this shape (`src/shared/types/api.ts`): + +```jsonc +// success +{ "data": , "meta": { "generatedAt": "ISO-8601", "range": "30d", "storeId": "all" } } +// failure (any non-2xx, or 2xx with error present) +{ "error": { "code": "internal|not_found|bad_request|unauthorized", "message": "…" } } +``` + +`meta.generatedAt` is **required**: the UI measures every relative time +("2 hours ago", days-to-expiry, the greeting) against the server clock, never +`Date.now()`. + +**Scope query** — every analytics endpoint is filtered by the same two params: + +| param | values | +|---|---| +| `storeId` | `all` \| a store id | +| `range` | `7d` \| `30d` \| `90d` \| `mtd` \| `ytd` \| `custom` | + +**Auth** — session is an httpOnly cookie. `credentials: 'same-origin'` is sent on +every request. A cross-origin backend needs CORS + `SameSite=None; Secure`, or +keep the Next routes as a thin proxy. + +--- + +## 2. Endpoints that EXIST (24) — all fixture-backed + +Legend: **T** = TypeScript response type, defined in the file noted. + +### Auth — `src/features/auth/types/auth.ts` + +| Method | Path | Body / Query | Returns | +|---|---|---|---| +| POST | `/api/auth/login` | `{email, password, rememberMe}` (JSON **and** form-encoded) | `AuthSession` | +| POST | `/api/auth/logout` | `{}` | `{ok: boolean}` | +| GET | `/api/auth/session` | — | `AuthSession \| null` | + +`AuthSession = {user: {id, email, name, role: 'owner'|'manager'|'analyst', organisation}, expiresAt}` + +### Dashboard — `src/features/dashboard/types/dashboard.ts` + `intelligence.ts` + +| Method | Path | Extra query | Returns | +|---|---|---|---| +| GET | `/api/dashboard/kpis` | scope | `Kpi[]` | +| GET | `/api/dashboard/timeseries` | scope | `TimePoint[]` | +| GET | `/api/dashboard/peak-hours` | scope | `HourCell[]` | +| GET | `/api/dashboard/activity` | scope | `ActivityEvent[]` | +| GET | `/api/dashboard/store-comparison` | scope | `StoreComparison[]` | +| GET | `/api/dashboard/reward-usage` | scope | `RewardUsagePoint[]` | +| GET | `/api/dashboard/performance` | scope + `granularity=weekly\|monthly` | `PeriodPoint[]` | +| GET | `/api/dashboard/briefing` | scope | `DashboardBriefing` | +| GET | `/api/dashboard/activity-metrics` | scope | `ActivityMetric[]` | +| GET | `/api/dashboard/journey` | scope | `JourneyStage[]` | +| GET | `/api/dashboard/campaigns` | scope | `CampaignSummary[]` | +| GET | `/api/dashboard/insights` | scope | `Insight[]` | + +Key shapes: + +```ts +Kpi { id:'visitors'|'purchases'|'revenue'|'activeRewards', label, value, + unit:'count'|'inr'|'lyt'|'pct', deltaPct, isRiseGood, trend:{t,v}[] } +TimePoint { t:'YYYY-MM-DD', visitors, purchases, revenue, conversion } +HourCell { day:0-6 (0=Mon), hour:0-23, value } +ActivityEvent{ id, at:ISO, kind:'reward_redeemed'|'staff_checked_in'|'purchase' + |'reward_expired'|'store_opened', title, detail?, storeId } +PeriodPoint { label:'W32'|'Aug', visitors, purchases, revenue } +Insight { id, severity:'info'|'success'|'warning'|'error', title, body, + action?:{label, href} } +``` + +**Activity intelligence** (`intelligence.ts`) — the model shared by Dashboard and Lyts: + +```ts +ActivityMetric { + id: 'walk'|'visit'|'selfie'|'spin'|'scratch'|'brand'|'challenge'|'friend'|'shop'|'event' + label, description + group: 'engagement'|'growth'|'commerce' + accent: 'warm'|'cool' // fixed per activity, travels on the payload + count, deltaPct + status: 'live'|'paused'|'draft' + lytsIssued // MEASURED (ledger). 1 LYT = ₹1 + isFeatured // the 6 the dashboard summarises + impact: { + customers // MEASURED + rewardClaims? // omitted, never 0, when activity grants none + repeatVisits, purchases // ATTRIBUTED + attributedRevenueInr // ATTRIBUTED — never name this `revenue` + attribution: 'estimated'|'observed' + } +} +JourneyStage { id:'visit'|'engage'|'purchase'|'return'|'refer', label, value, conversionPct? } +CampaignSummary { id, name, activityId, accent, status:'live'|'ended'|'scheduled', + steps:{label,value}[], attributedRevenueInr, attribution } +``` + +> **Contract rule.** `attribution` is not decorative. When it is `'estimated'` the +> UI shows a disclosure ("Attribution is estimated…") beside every attributed +> figure; when your backend can join purchases to activity events, return +> `'observed'` and the disclosure disappears with no frontend change. +> Invariants the UI assumes: `purchases ≤ repeatVisits ≤ customers ≤ count`. + +### Lyts — `src/features/lyts/types/reward.ts` + +| Method | Path | Returns | +|---|---|---| +| GET | `/api/lyts/rewards` | `Reward[]` | +| GET | `/api/lyts/redemptions` | `TimePoint[]` ⚠️ | +| GET | `/api/lyts/activity` | `ActivityEvent[]` | + +`Reward { id, name, costLyt, claimed, used, expiresAt: ISO|null, status: 'active'|'paused'|'expiring'|'expired' }` + +⚠️ **Known contract smell:** redemptions reuses `TimePoint`, and the Lyts charts +read `visitors` as "Issued" and `purchases` as "Redeemed". Your backend should +return a purpose-built shape — `{t, issued, redeemed}` — and I'll update the two +chart call sites. + +### Stores — `src/features/stores/types/store.ts` + +| Method | Path | Returns | +|---|---|---| +| GET | `/api/stores` | `Store[]` | +| GET | `/api/stores/:storeId` | `Store` (404 → `not_found`) | + +`Store { id, name, status:'open'|'closed'|'maintenance', visitors, purchases, revenueInr, conversionPct, staffCount }` + +### Staff — `src/features/staff/types/staff.ts` + +| Method | Path | Returns | +|---|---|---| +| GET | `/api/staff` | `StaffMember[]` | +| GET | `/api/staff/summary` | `StaffSummary` | +| GET | `/api/staff/attendance` | `AttendancePoint[]` | + +### Settings — `src/features/settings/types/settings.ts` + +| Method | Path | Returns | +|---|---|---| +| GET | `/api/settings/profile` | `MerchantProfile` | +| PATCH | `/api/settings/profile` | `MerchantProfile` (merged) — **does not persist today** | + +--- + +## 3. Endpoints that DO NOT EXIST — must be created + +These screens are live in the UI with **no API, no repository, and no persistence**. +Mutations are `useState` only: refresh the page and every change is gone. + +### 3a. Commerce — the worst-wired module + +`/commerce` imports `commerceService.ts` **directly and synchronously** from 10 +component files. No repository, no route handler, no loading or error state. + +Needed: + +| Method | Path | Returns | +|---|---|---| +| GET | `/api/commerce/kpis` | revenue, orders, AOV, net sales, refunds, conversion (value + delta each) | +| GET | `/api/commerce/revenue-trend` | `{t, revenue, target}[]` | +| GET | `/api/commerce/orders-trend` | `{t, orders}[]` | +| GET | `/api/commerce/payment-breakdown` | `{method, amount, sharePct}[]` | +| GET | `/api/commerce/store-performance` | `{storeId, name, revenueInr}[]` | +| GET | `/api/commerce/top-products` | `{id, name, category, unitsSold, revenueInr, growthPct, stockCount, stockStatus}[]` | +| GET | `/api/commerce/alerts` | `{id, severity, title, body}[]` | +| GET | `/api/commerce/orders` | recent orders feed | + +### 3b. Settings — 7 screens, all local-state fakes + +| Screen | Needed endpoints | +|---|---| +| Team & Staff | `GET/POST/PATCH/DELETE /api/settings/team` (+ suspend, password reset, invite) | +| Stores | `GET/POST/PATCH/DELETE /api/settings/stores` | +| Roles & Permissions | `GET/PUT /api/settings/roles` (permission matrix) | +| Integrations | `GET /api/settings/integrations`, `POST/DELETE .../:id/connection` | +| API & Webhooks | `GET/POST/DELETE /api/settings/api-keys`, `GET/POST/DELETE /api/settings/webhooks`, `GET /api/settings/webhooks/logs` | +| Security | `GET /api/settings/sessions`, `DELETE /api/settings/sessions/:id`, `GET /api/settings/audit-log`, `POST /api/auth/password`, `POST/DELETE /api/auth/2fa` | +| Billing | `GET /api/settings/billing`, `GET /api/settings/invoices`, `PATCH /api/settings/payout-account` | + +### 3c. Loyaly AI + +The chat panel streams from `services/ai/mockAi.ts` — a local prompt classifier +picking canned templates. `loyalyAiRepository` is the only repository in the app +that imports a mock directly. + +Needed: `POST /api/ai/chat` (streaming — SSE or chunked), plus +`GET/POST/DELETE /api/ai/conversations` if history is to survive reload. + +### 3d. Store switcher — **critical** + +`STORE_OPTIONS` is a **hardcoded array in `src/shared/providers/WorkspaceProvider.tsx`** +(line 45), duplicated from the fixture roster. This array scopes *every request in +the app*. It must be fed from `GET /api/stores`, or a real merchant sees fixture +store names in the global switcher. + +--- + +## 4. Hardcoded / dummy data inventory + +| Location | What | Removal | +|---|---|---| +| `src/features/*/mock/*.ts` (11 files, ~1,970 lines) | all fixture generators | delete at cutover | +| `src/shared/mock/rng.ts` | seeded RNG | delete at cutover | +| `src/features/commerce/services/commerceService.ts` | KPIs, trends, products, alerts, store multipliers | replace with repository | +| `src/features/settings/components/*.tsx` × 7 | `INITIAL_STAFF`, `INITIAL_STORES`, `INITIAL_MATRIX`, `INITIAL_APPS`, `INITIAL_KEYS`, `INITIAL_WEBHOOKS`, `WEBHOOK_LOGS`, `INITIAL_SESSIONS`, `AUDIT_LOGS`, `INVOICES` | replace with hooks | +| `src/shared/providers/WorkspaceProvider.tsx` | `STORE_OPTIONS` | feed from `/api/stores` | +| `src/features/loyaly-ai/services/ai/**` (~570 lines) | prompt router + reply templates | replace with real AI endpoint | +| `src/features/auth/mock/users.mock.ts` | 5 users, **plaintext passwords** | replace `verifyCredentials()` body | +| `src/app/api/**/route.ts` (24 files) | fixture wiring + `?_state=` simulation | delete or keep as proxy — see §6 | + +**Auth note.** `verifyCredentials()` is already the single credential seam — its +body becomes an HTTP call and nothing else changes. But it currently returns +`unknown_email` vs `wrong_password` separately, which is a **user-enumeration +oracle**. Collapse both to one message at the route when you go live. +`AUTH_SECRET` must be set in production (`sessionToken.ts` throws without it). + +--- + +## 5. What I recommend NOT doing yet + +Deleting the fixtures before the real endpoints exist leaves every screen in an +error state and removes the only way to verify the wiring. The fixtures are also +the **executable spec** — `intelligence.mock.ts` encodes the funnel invariants +your backend has to honour. + +Order that keeps the app working at every step: + +1. **Now (backend-independent):** add the missing seams — commerce repository + + route, settings hooks + routes, `STORE_OPTIONS` from `/api/stores`, AI + repository seam. Fixtures stay behind them. Every module then has one file to + swap. +2. **You send the API spec MD.** I diff it against §2/§3 and report + match / rename / missing / extra per endpoint. +3. **Cutover:** point `NEXT_PUBLIC_API_BASE` at the real host, adapt any shape + mismatches in the repository layer only, delete `mock/` + `src/app/api/**`. +4. **Verify:** typecheck, build, and walk every screen with the network tab. + +--- + +## 6. Decision needed from you + +**Do the Next.js route handlers stay?** + +- **A — Direct:** frontend calls your backend. Delete `src/app/api/**`. Needs CORS + and a cross-origin-safe session cookie. +- **B — Proxy (recommended):** keep the route handlers, replace each fixture call + with a `fetch` to your backend. Session cookie stays first-party, your API host + is never exposed to the browser, and the `?_state=error|empty|loading` dev + harness keeps working. + diff --git a/docs/BEHAVISION-GAP-ANALYSIS.md b/docs/BEHAVISION-GAP-ANALYSIS.md new file mode 100644 index 0000000..5f55519 --- /dev/null +++ b/docs/BEHAVISION-GAP-ANALYSIS.md @@ -0,0 +1,280 @@ +# Behavision API ↔ Loyaly Merchant OS — gap analysis + +Audit date: 2026-09-09 · API: `https://mcp.loyaly.ai` · Frontend: this repo + +(Host corrected 2026-09-21: this line read `https://platform.loyaly.ai`, which +serves this console, not the API. See `src/shared/config/platformApi.ts:76-80`.) + +--- + +## Headline finding + +**These are two different products.** + +This frontend was built as a **loyalty & rewards console**: LYT points, reward +catalogues, engagement activities (spin / selfie / scratch / challenge / +referral), campaigns, redemption liability, commerce orders. + +Behavision is a **camera-based footfall & visitor-recognition platform**: sites, +cameras, face templates, arrivals, visitor identity, footfall and conversion +reports. + +They overlap on roughly **one third** of the surface — the part that is genuinely +about *people arriving at a shop and buying something*. The rest of the UI has no +data source in this API and never will until a loyalty backend exists. + +So "remove all hardcodes" has a consequence that needs a decision, not a +guess: **removing the fixtures makes about half the current UI go blank.** §4 +lays out the options. + +The reverse is also true and more interesting: **Behavision exposes a lot of real +product this console does not surface at all** — a visitor directory, live camera +views, site health checks, an invitation/join flow, device management, GDPR +erasure. See §5. + +--- + +## 1. What maps — build these for real + +| Screen / panel | Behavision endpoint | Notes | +|---|---|---| +| Login | `POST /api/auth/login` | different token model — §3 | +| Session restore | `GET /api/auth/me` | | +| Logout | `POST /api/auth/logout` | | +| Dashboard · Visitors KPI | `GET /api/reports/footfall` | use server `total`, never sum buckets | +| Dashboard · Purchases KPI | `GET /api/reports/conversion` | | +| Dashboard · Revenue KPI | `GET /api/reports/conversion` | | +| Dashboard · Footfall chart | `GET /api/reports/footfall?bucket=day` | | +| Dashboard · Revenue chart | `GET /api/reports/conversion?bucket=day` | | +| Dashboard · Visitors vs purchases | both reports | | +| Dashboard · Conversion chart | `GET /api/reports/conversion` | | +| Dashboard · Peak hours heatmap | `GET /api/reports/footfall?bucket=hour` | 7×24 grid from hourly buckets | +| Dashboard · Period rollup | `…?bucket=week\|month` | | +| Dashboard · Store comparison | `GET /api/reports/footfall?site=` per site | | +| Dashboard · Recent activity feed | `GET /api/visits` | **arrivals only** — see below | +| Stores list | `GET /api/sites` | + `fraction_below_gate`, cameras up | +| Store detail | `GET /api/sites/{id}/check` | five-step smoke test | +| Settings · Team | `GET /api/team`, `PATCH /api/team/{id}` | replaces `INITIAL_STAFF` | +| Settings · Security → sessions | `GET/DELETE /api/auth/sessions`, `POST …/revoke-others` | replaces `INITIAL_SESSIONS` | +| Store switcher (`STORE_OPTIONS`) | `GET /api/sites` | **critical — scopes every request** | + +**Customer journey** maps partially and honestly: + +| stage | source | +|---|---| +| Visit | footfall `total` | +| Engage | ✗ no source | +| Purchase | conversion | +| Return | footfall `returning` | +| Refer | ✗ no source | + +**Activity feed caveat.** The current feed renders five event kinds +(`reward_redeemed`, `staff_checked_in`, `purchase`, `reward_expired`, +`store_opened`). `/api/visits` supplies **arrivals only**. Four of the five kinds +have no source. The feed becomes an arrivals feed — which is arguably the better +screen, and is what the spec calls "the screen a mobile app is for". + +--- + +## 2. What does NOT map — no endpoint exists + +| Area | Frontend surface | Status | +|---|---|---| +| **LYT programme** | entire `/lyts` page: rewards, claimed/used, redemption rate, outstanding liability, expiry alerts, reward usage chart, LYTs issued | ✗ no rewards concept in the API | +| **Activity intelligence** | walk / selfie / spin / scratch / brand / challenge / friend / shop / event, impact chains, attribution, activity grouping | ✗ only *visit* has an analogue | +| **Campaign performance** | campaign funnels | ✗ | +| **Store insights** | "What needs your attention", AI briefing | ✗ | +| **Commerce** | orders, products, categories, stock, payment methods, refunds, AOV, alerts | ✗ except revenue/basket via conversion report; `POST /api/purchases` writes but there is no orders read | +| **Staff module** | attendance (present/absent/late/leave), punctuality, sales per head, rewards issued, performance score | ✗ **different concept** — `/api/team` is console *user accounts*, not shop-floor rostering | +| **Settings · Profile** | business name, GSTIN, timezone, currency, LYTs per ₹100 | ✗ no business-profile endpoint | +| **Settings · Roles matrix** | per-permission grid | ✗ role is a single enum via `PATCH /api/team/{id}` | +| **Settings · Integrations** | connected apps | ✗ | +| **Settings · API & Webhooks** | keys, endpoints, delivery logs | ✗ | +| **Settings · Billing** | plan, invoices, payout account | ✗ | +| **Settings · Security → audit log, 2FA** | | ✗ (sessions *do* exist) | +| **Loyaly AI** | chat panel | ✗ | + +--- + +## 3. Architectural changes required + +These are not cosmetic. Each one has a defined failure mode. + +### 3.1 Auth: cookie+HMAC → Bearer JWT with rotation + +Today: `proxy.ts` gates every route on an httpOnly cookie carrying an +HMAC-signed payload minted locally by `sessionToken.ts`. There is no upstream. + +Behavision: `access_token` + `refresh_token`, both rotating. + +**Recommendation — keep the Next routes as a BFF (Backend-For-Frontend).** +Tokens live server-side; the browser keeps only the existing httpOnly cookie. +This is not conservatism, it buys five specific things: + +1. `proxy.ts` and the whole SSR gate keep working unchanged. +2. Tokens never reach JS, so an XSS cannot exfiltrate a refresh token. +3. **Web `` cannot send `Authorization`.** A BFF can proxy + `/api/faces/…` and the browser just uses a normal ``. Otherwise + every avatar needs fetch + `createObjectURL` + revoke-on-unmount. +4. The single-flight refresh lock lives in one server process, not in N tabs. +5. CORS and `SameSite=None` disappear as problems. + +**Three client rules the spec calls out — all must be implemented in the BFF:** + +- `401` + `"error": "token_expired"` → refresh **once**, retry, silently. Any + other 401 is a real sign-out. +- **Serialise refresh behind one lock.** Refresh tokens are single-use; four + concurrent panels would each spend it and three would lose. This dashboard + fires **9 parallel requests on one page load** — it will hit this on the first + expiry, every time, without the lock. +- **Persist rotated tokens before using them.** A crash between refresh and + persist leaves a token the server has already invalidated. + +Also: marshal the request body before the first attempt — a retry re-sends it, +and a stream is spent after the first read. + +### 3.2 Response envelope + +Frontend expects `{data, meta:{generatedAt, range, storeId}}`. Behavision +returns bare objects/arrays. + +**`meta.generatedAt` is load-bearing** — every relative timestamp, the greeting, +and every days-to-expiry countdown measures against the server clock, never +`Date.now()`. The BFF must synthesise it (from `polled_at`, or the response +time) or ~20 `useResource` call sites all need rewriting. + +Wrap in the BFF. Cheapest correct option by a wide margin. + +### 3.3 Scope parameters + +`?storeId=all&range=30d` → `?site=&from=YYYY-MM-DD&to=YYYY-MM-DD&tz=…&bucket=…` + +- `storeId: 'all'` → omit `site` entirely. +- `RangeKey` → concrete `from`/`to`. `to` is **inclusive**. +- Send `tz`; buckets come back as **local wall time with no offset**. +- Use `site` (documented spelling). An unknown query param is *silently ignored*, + so `site_id` in the wrong place returns the whole estate instead of an error. + +### 3.4 Report arithmetic — three traps + +- **Do not sum buckets to get the total.** `total` is unique people over the + window; someone who came Monday and Thursday is 1 person, 2 bucket-visitors. + Show the server's `total`, visits underneath. +- **`new + returning` can be less than `total`** — a site sending counts without + templates records real footfall by an unidentified person. +- **Do not `new Date()` a bucket label.** They are wall-time strings with no + offset; parsing shifts every label into the viewer's zone. + `formatDayLabel()` currently does `new Date(iso)` — it pins `timeZone:'UTC'` + so it survives, but bucket labels should be passed through, not parsed. + +### 3.5 Errors + +Behavision: `{"error": "invalid_code", "message": "…"}` (flat). +Frontend: `{error: {code, message}}` (nested), with codes +`internal|not_found|bad_request|unauthorized`. + +Map in the BFF. And **show the server's `message`** — it is written for humans; +branch on `error`, never on the prose. New codes to handle: `token_expired`, +`too_many_attempts` (429), `last_owner` (409), `invalid_code` (404), +**`501` = feature off for this deployment, not an error**. + +`404` is also what another tenant's data returns — never surface it as "deleted". + +### 3.6 Cursor pagination — new concept + +`GET /api/visits` is cursor-based and lossless; polling by timestamp +**permanently skips rows** when a burst exceeds `limit`. `useResource` has no +cursor concept — the arrivals feed needs a cursor-aware hook. + +- Echo the returned `cursor` on every poll. +- An empty poll returns **your own cursor**, not `""`. +- A cursor that fails to parse → drop it, re-poll without one. +- Delivery is at-least-once → de-duplicate on `visit_id`. +- `GET /api/visits/stream` (SSE) is the low-latency path; needs an HTTP client + that can set `Authorization` (browser `EventSource` cannot). + +### 3.7 Images — new subsystem + +- **Missing photo is data, not an error.** Images are off by default across the + product; `available: false` with a `reason` is the normal case. Render + initials, never an error state. +- `auth: true` → send Bearer. `auth` absent/false → presigned, use directly. + **Do not infer from the URL shape.** Treat any relative URL as needing auth. +- **Every hand-out is written to the audit log.** Fetch once per screen, not + once per component — two components asking for one face puts two rows in + "who looked at my customers" for one glance. +- Web: object URL + **revoke on unmount**, or a screen left open all afternoon + holds hundreds of copies of one photograph. (The BFF proxy in §3.1 avoids + this entirely.) + +### 3.8 Roles + +`UserRole = 'owner' | 'manager' | 'analyst'` → **`'staff' | 'manager' | 'owner'`**. +`analyst` does not exist. Platform admin = `role === 'admin'` **and** empty +`client_id` — the two together, never the role alone. + +### 3.9 References instead of uuids + +`V-42`, `chennai`, `Office1`, `priya@tenext.in` all work in paths and filters, +and are **immutable** — safe to put in a URL or a saved report. Display names are +not. The store switcher should key on `site_slug`, and deep links should use it. + +Ambiguity resolves to nothing, not a guess. Unknown ref: **404 in a path, 400 in +a filter**. + +--- + +## 4. The decision: what happens to the unmapped half + +The API gives a clean idiom for this: **`501` — "the feature is off for this +deployment, not an error."** + +| | option | result | +|---|---|---| +| **A** | Delete the unmapped modules | Smallest, most honest app. Lose `/lyts`, `/commerce`, activity intelligence, campaigns, insights, staff attendance, most of settings. Recoverable from git when a loyalty backend ships. | +| **B** | Keep the UI, render "not available in this deployment" | Nothing hardcoded, nothing invented, screens stay for when the backend arrives. Costs a small unavailable-state component. | +| **C** | Keep fixtures behind an explicit `DEMO_MODE` flag | Sales demos keep working; real deployments show B. Highest complexity. | + +**My recommendation: B**, with A for `/commerce` specifically — commerce is the +one module with no seam at all (10 components import a service synchronously), +so there is nothing to preserve, and conversion-report revenue can move onto the +dashboard where it belongs. + +Either way **no invented number survives**, which is what you asked for. + +--- + +## 5. Real product this console is not exposing + +Worth knowing before we decide what to delete — the API supports screens that do +not exist here yet: + +- **Visitor directory** — `GET /api/visitors?q=` search by name/phone/`V-42`, + `/history`, `PUT /profile` (name, phone, notes), and **`DELETE` erasure** + (destroys face template + photo, keeps visits unlinked, irreversible; a `502` + means *nothing* was deleted and must be reported as failure, never swallowed). +- **Live camera view** — `GET /api/cameras/{id}/live`, SSE relayed from the shop PC. +- **Camera management** — `GET /api/cameras` with latest still, `PATCH /api/cameras/{id}`. +- **Site health** — `GET /api/sites/{id}/check`, five-step smoke test. +- **Invitation / join flow** — mint a code, preview it unauthenticated, redeem it + into a full session. Replaces the fake "Add Staff Member" form entirely. +- **Device management** — the user's own signed-in devices, with `current` marked. +- **Arrivals SSE stream** — the live feed. + +--- + +## 6. Proposed sequence + +1. **BFF + auth** — Behavision login/refresh/logout/me behind the existing + cookie; single-flight refresh; envelope + error adapters; scope→params + mapper. Nothing else can be wired until this exists. +2. **Kill the highest-risk hardcode** — `STORE_OPTIONS` → `GET /api/sites`. +3. **Reports** — dashboard KPIs, footfall, conversion, peak hours, rollup, + comparison. +4. **Arrivals** — cursor-aware feed replacing the mock activity timeline. +5. **Team + Sessions + Invitations** — replaces four fake settings screens with + real ones. +6. **Apply the §4 decision** to everything unmapped; delete `src/features/*/mock/**` + and `src/shared/mock/`. +7. **Verify** — typecheck, build, walk every screen against the live API. + diff --git a/docs/PLATFORM-STATUS.md b/docs/PLATFORM-STATUS.md new file mode 100644 index 0000000..8c17e47 --- /dev/null +++ b/docs/PLATFORM-STATUS.md @@ -0,0 +1,60 @@ +# Live platform status — mcp.loyaly.ai + +Probed 2026-09-09. `LOYALY_API_BASE=https://mcp.loyaly.ai` + +The host is confirmed as the Behavision platform: it answers the documented flat +`{"error": "...", "message": "..."}` contract with the documented codes +(`bad_credentials`, `unauthorized`, `not_found`, `bad_request`). + +**8 of the 16 endpoints the spec documents are not deployed yet.** +A `401 unauthorized` proves the route exists and is gated. A +`404 {"error":"not_found","message":"No such endpoint."}` means it is absent. + +## Live + +| Endpoint | Probe | Console feature | +|---|---|---| +| `POST /api/auth/login` | 401 `bad_credentials` | Sign in — **wired** | +| `POST /api/auth/refresh` | 400 `refresh_token is required` | Token rotation — **wired** | +| `POST /api/auth/logout` | 401 `unauthorized` | Sign out — **wired** | +| `GET /api/auth/me` | 401 `unauthorized` | Session confirmation — **wired** | +| `GET /api/sites` | 401 `unauthorized` | Site switcher + Store page — **wired** | +| `GET /api/visitors` | 401 `unauthorized` | Customer directory — service written, UI not built | +| `GET /api/reports/footfall` | 401 `unauthorized` | Visitors KPI + Footfall chart — **wired** | +| `GET /api/reports/conversion` | 401 `unauthorized` | Purchases/Revenue/Conversion + Sales — **wired** | + +## Not deployed + +| Endpoint | Probe | Blocks | +|---|---|---| +| `GET /api/visits` | 404 | **Recent arrivals feed, Activity page** | +| `POST /api/purchases` | 404 | **Mobile → dashboard purchase flow** | +| `GET /api/team` | 404 | Leaderboard team table | +| `GET /api/team/invitations` | 404 | Invite flow | +| `GET /api/auth/invitation` | 404 | Join preview | +| `POST /api/auth/register` | 404 | Redeeming an invitation | +| `GET /api/auth/sessions` | 404 | Device management | +| `GET /api/cameras` | 404 | Camera list / live view | + +## Consequence for the "most important requirement" + +The brief's §7 flow — + +``` +Mobile → POST /api/visits → DB → Dashboard GET /api/visits → new visit appears +Mobile → POST /api/purchases → DB → reports update +``` + +— cannot run today. **Neither `/api/visits` nor `/api/purchases` is deployed.** +The console side is built and pointed at both; they return 404 until the +platform ships them. + +What *does* work end to end once there is an account: sign in, site switching, +and every KPI and chart on the Dashboard and Sales pages, since those read the +two report endpoints that are live. + +## Still needed + +A valid account on `mcp.loyaly.ai`. There is no open registration by design, +and `POST /api/auth/register` is not deployed either — so an account has to be +created directly on the platform side. diff --git a/eslint.config.mjs b/eslint.config.mjs index 05e726d..0aa8e8b 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -11,7 +11,15 @@ const eslintConfig = defineConfig([ ".next/**", "out/**", "build/**", + // Deploy artifact from `npm run bundle` — traced vendor code, not ours. + "dist/**", "next-env.d.ts", + // Generated by `npm run theme:build` — edit src/theme/loyalyTheme.ts + // instead. The emitted .d.ts uses a triple-slash reference we do not own. + "src/theme/loyaly.css", + "src/theme/loyaly.js", + "src/theme/loyaly.d.ts", + "src/theme/loyaly.variants.d.ts", ]), ]); diff --git a/next.config.ts b/next.config.ts index e9ffa30..be4da9c 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,25 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + output: "standalone", + experimental: { + // Turbopack's build cache lives in .next/cache and only pays off when that + // directory survives between builds. A Docker/Dokploy build starts from a + // clean layer every time, so the cache is written and never read — ~117 MB + // of pure write cost per build. CI_BUILD is set in the Dockerfile only, so + // local `npm run build` keeps its warm cache. + turbopackFileSystemCacheForBuild: process.env.CI_BUILD !== "1", + }, + allowedDevOrigins: ["192.168.0.117", "192.168.0.*", "192.168.1.*", "localhost", "127.0.0.1"], + images: { + remotePatterns: [ + { + protocol: "https", + hostname: "images.unsplash.com", + }, + ], + }, }; export default nextConfig; + diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..a95ed20 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,73 @@ +# ───────────────────────────────────────────────────────────────────────────── +# NOT USED BY THE DEPLOYED IMAGE. Kept only as a reference. +# +# The image ran `node server.js & nginx -g 'daemon off;'` and exposed port 80 +# until commit 28258b5 ("fix docker port"), which dropped nginx and made the +# Next standalone server the container's only process on port 3000. Nothing +# installs nginx any more, and .dockerignore excludes this file from the build +# context, so editing it CANNOT affect production — the TLS termination and +# reverse proxy in front of the container are Dokploy's Traefik, configured in +# the Dokploy dashboard, not here. +# +# That matters when a 502 Bad Gateway shows up: this file is the obvious place +# to look and the wrong one. A 502 means Traefik had no healthy container to +# proxy to. Check the container's log for `[loyaly] configuration problem` and +# `GET /api/health` first. +# ───────────────────────────────────────────────────────────────────────────── + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + gzip on; + gzip_proxied any; + gzip_comp_level 6; + gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml; + + upstream nextjs_upstream { + server 127.0.0.1:3000; + } + + server { + listen 80; + server_name localhost; + + # Serve Next.js compiled static assets directly via Nginx + location /_next/static/ { + alias /app/.next/static/; + expires 365d; + access_log off; + add_header Cache-Control "public, max-age=31536000, immutable"; + } + + # Serve public directory assets + location /public/ { + alias /app/public/; + expires 30d; + access_log off; + } + + # Reverse proxy dynamic routes, SSR, and API routes to Next.js standalone server + location / { + proxy_pass http://nextjs_upstream; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + } +} diff --git a/package-lock.json b/package-lock.json index 798e197..731fda8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,19 @@ "name": "loyaly-staff", "version": "0.1.0", "dependencies": { + "@astryxdesign/core": "^0.2.0", + "@astryxdesign/theme-neutral": "^0.2.0", + "@stylexjs/stylex": "^0.19.0", + "framer-motion": "^12.43.0", + "lucide-react": "^1.28.0", "next": "16.3.6", "react": "19.2.8", - "react-dom": "19.2.8" + "react-dom": "19.2.8", + "recharts": "^3.10.1", + "server-only": "^0.0.1" }, "devDependencies": { + "@astryxdesign/cli": "^0.2.0", "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", @@ -36,6 +44,73 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@astryxdesign/cli": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@astryxdesign/cli/-/cli-0.2.0.tgz", + "integrity": "sha512-JTMP6Au3r3IOqZcvF648XP7akiynSYfbYM3nxHyIjhYVTCGnwPAF6mrQRTzxoTSh5NvRktYzV5Mv3tEuhvw0lg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "commander": "^12.1.0", + "jiti": "^2.7.0", + "jscodeshift": "^17.3.0", + "zod": "^4.4.3" + }, + "bin": { + "astryx": "bin/astryx.mjs", + "cli": "bin/astryx.mjs" + }, + "peerDependencies": { + "@astryxdesign/charts": "*", + "@astryxdesign/core": "*", + "@astryxdesign/lab": "*", + "@astryxdesign/theme-neutral": "*", + "gpt-tokenizer": "^3.4.0" + }, + "peerDependenciesMeta": { + "@astryxdesign/charts": { + "optional": true + }, + "@astryxdesign/core": { + "optional": true + }, + "@astryxdesign/lab": { + "optional": true + }, + "@astryxdesign/theme-neutral": { + "optional": true + } + } + }, + "node_modules/@astryxdesign/core": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@astryxdesign/core/-/core-0.2.0.tgz", + "integrity": "sha512-lj+RZ5iZ/k9Hm/WzxCd+p4d513HIp64gXhWFfsqJSshfFEQZO2X8GKbYx5MQIN7mneFr7lX/+qMv/4pMBT2QNA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "intl-messageformat": "^11.2.9" + }, + "peerDependencies": { + "@stylexjs/stylex": "^0.19.0", + "react": ">=19.0.0", + "react-dom": ">=19.0.0" + } + }, + "node_modules/@astryxdesign/theme-neutral": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@astryxdesign/theme-neutral/-/theme-neutral-0.2.0.tgz", + "integrity": "sha512-mQ+0TTKnOcQEret+djWhttYlLCwmZcJFAvBFyz7nOT5jBdnb0a+svg8/6p8Ew7RYkI+Dnk5+G3r26Q8wsnysOQ==", + "license": "MIT", + "dependencies": { + "lucide-react": "^1.18.0" + }, + "peerDependencies": { + "@astryxdesign/core": "0.2.0", + "react": ">=19" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -109,6 +184,19 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-compilation-targets": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", @@ -126,6 +214,28 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/helper-globals": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", @@ -136,6 +246,20 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-imports": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", @@ -168,6 +292,61 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", @@ -228,6 +407,233 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-syntax-flow": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.29.7.tgz", + "integrity": "sha512-ajMX6QPcyomotqwpzhkYGxcK2i/us0rs1Qo9QvUpa+Fca0FTmqrzKrctoIYLMxcOhGZldGT/BAVkRGTWBiR8gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz", + "integrity": "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-flow-strip-types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.29.7.tgz", + "integrity": "sha512-wRHeUjUjCZnMHmiO5bRgjFLcoEh7JyTdByOW11ahhwNa4V0bmeGEaIvt51yq0zQp2yWIpqfxXXPyUP6GFJZHOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-flow": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz", + "integrity": "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz", + "integrity": "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.29.7.tgz", + "integrity": "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.9.tgz", + "integrity": "sha512-FFwIwzU+7SCOuxxV4YtJql6T9981ZVTm+FHO5GhVsRqCTdy0WwrEZh3l42ARpXruJUDGOptdduHW6Zr7pNPLNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-syntax-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-flow": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.29.7.tgz", + "integrity": "sha512-KYIRV0BuaN68CDdsqFkAD7MU7yipUqQNuNElwATdxaIdpTjhvtY82QvkBJs7zV3Evxj2jFAAZ1iO8nyy0nhjqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-transform-flow-strip-types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.29.7.tgz", + "integrity": "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/register": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.29.7.tgz", + "integrity": "sha512-AMGJoWuES861riy6pcB0fphE1YXybtQnBYQMuIyPv6mKLiosfa79BKTnAOyx215c/3RJPJpdQwoHZ3earVH7AA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "find-cache-dir": "^2.0.0", + "make-dir": "^2.1.0", + "pirates": "^4.0.6", + "source-map-support": "^0.5.16" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/template": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", @@ -453,6 +859,27 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@formatjs/fast-memoize": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.7.tgz", + "integrity": "sha512-zXfhLpvA6T7+efdt9JLbBwZ00tT7NsBMDVnDu8rpHeNNv8KfRZAMo2gkG0k9lK/Nzc//3kJ9pImsfuJxk3KhUA==", + "license": "MIT" + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.18.tgz", + "integrity": "sha512-wX1efcL8d7K5QapOTRsP/OLTA/kpwe4LwqWyNxDNg5iRs8i9jJbCyxB5P8jc4KOW5oiLzz2DJr+BRY5xMV9+bA==", + "license": "MIT", + "dependencies": { + "@formatjs/icu-skeleton-parser": "2.1.11" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.11.tgz", + "integrity": "sha512-j8cUmOJzVgkHuS0QiQ6ga76UIoLOFSAMWhs7aZJztH3aAdCOAE6vpC8KVvFB4cU10ON0y2/5oOVmPJ43s2lTwA==", + "license": "MIT" + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -1317,6 +1744,32 @@ "node": ">=12.4.0" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", + "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1324,6 +1777,29 @@ "dev": true, "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@stylexjs/stylex": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@stylexjs/stylex/-/stylex-0.19.1.tgz", + "integrity": "sha512-JwNFm0MOYPqi+M4Ax5Un4ZxfZp4HEAX2sAWetZ88KALFcv1eEiHNJs75oO3V3U7RVWnzfkiM8i9eBowC3hqgUw==", + "license": "MIT", + "dependencies": { + "css-mediaquery": "^0.1.2", + "invariant": "^2.2.4", + "styleq": "0.2.1" + } + }, "node_modules/@swc/helpers": { "version": "0.5.23", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", @@ -1615,6 +2091,69 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -1650,7 +2189,7 @@ "version": "19.3.0", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -1666,6 +2205,12 @@ "@types/react": "^19.3.0" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.70.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.70.1.tgz", @@ -2518,6 +3063,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ast-types": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.3.tgz", + "integrity": "sha512-FvWoWYfSCM6kRxCSH+MGLHIKKGRL6A6AW7Zek2O32REPQRdg131428uRTKMBYAeRd3XXAaHDS60Wpri7CdKDrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -2648,6 +3206,13 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, "node_modules/call-bind": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", @@ -2751,6 +3316,30 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2771,6 +3360,23 @@ "dev": true, "license": "MIT" }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2800,13 +3406,140 @@ "node": ">= 8" } }, + "node_modules/css-mediaquery": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz", + "integrity": "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==", + "license": "BSD" + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -2886,6 +3619,12 @@ } } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3194,6 +3933,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-toolkit": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.52.0.tgz", + "integrity": "sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks", + "tests/types", + "tests/browser-compat" + ] + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -3578,6 +4329,20 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", @@ -3624,6 +4389,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3711,6 +4482,21 @@ "node": ">=8" } }, + "node_modules/find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -3749,6 +4535,29 @@ "dev": true, "license": "ISC" }, + "node_modules/flow-estree": { + "version": "0.332.0", + "resolved": "https://registry.npmjs.org/flow-estree/-/flow-estree-0.332.0.tgz", + "integrity": "sha512-nsh1Ty3l/a8xJLGWnrZ0VFfQXjIpA6Gm1LqovVyj8RR6gE9Hz/cDl951qNNUnZtJ08u+6V0SFrY66XAoct76Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/flow-parser": { + "version": "0.332.0", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.332.0.tgz", + "integrity": "sha512-Ly9ylvWknL6YGW2lq0stmDg8PfEh9qHxPoqlZLDjjH/9yd9Ik37m335a0WngsxQvDfrW8lwBPm++RmZm3e4lFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flow-estree": "0.332.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -3765,6 +4574,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/framer-motion": { + "version": "12.43.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.43.0.tgz", + "integrity": "sha512-1eaL3RvR/kAlbG7UYcpMptEyzPoENO0c6w7ZnB3/hh2vSAz/6uGAFn6fdoqTBguNstf3MsFhJHsD/0DHiclG+g==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.43.0", + "motion-utils": "^12.39.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3955,6 +4791,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gpt-tokenizer": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/gpt-tokenizer/-/gpt-tokenizer-3.4.0.tgz", + "integrity": "sha512-wxFLnhIXTDjYebd9A9pGl3e31ZpSypbpIJSOswbgop5jLte/AsZVDvjlbEuVFlsqZixVKqbcoNmRlFDf6pz/UQ==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -4083,6 +4927,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "11.1.18", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.18.tgz", + "integrity": "sha512-EQyQtLiYW029lyoczMl/Hh4Xu7cDecSc58JRYpHyL4tIAu3eqd1yJzQX04d2BZHDkzFFvm6qJEJWOtfDSWAXbQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -4125,6 +4979,34 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/intl-messageformat": { + "version": "11.2.15", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.2.15.tgz", + "integrity": "sha512-W4VNytsXyWgW5lvVchmsb9xgA5rSKqoflu3/IQ9UCtB0z4R+Bx9MoEkwFT/+30RCl/KzUoH0QGGe9lppF5SuvQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/fast-memoize": "3.1.7", + "@formatjs/icu-messageformat-parser": "3.5.18" + } + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -4411,6 +5293,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -4570,6 +5465,16 @@ "dev": true, "license": "ISC" }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/iterator.prototype": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", @@ -4602,7 +5507,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -4628,6 +5532,60 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jscodeshift": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/jscodeshift/-/jscodeshift-17.4.0.tgz", + "integrity": "sha512-i3ESKiiTsGynxzTg5BhsZViD0ai72/6SsI1efDZxG6/5KCoElsmquxtyhXK5lpEgoO7MTNpYkjFEdEL97SkBNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/plugin-transform-class-properties": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/preset-flow": "^7.24.7", + "@babel/preset-typescript": "^7.24.7", + "@babel/register": "^7.24.6", + "flow-parser": "0.*", + "graceful-fs": "^4.2.4", + "neo-async": "^2.5.0", + "picocolors": "^1.0.1", + "picomatch": "^4.0.2", + "recast": "^0.23.11", + "tmp": "^0.2.3", + "write-file-atomic": "^5.0.1" + }, + "bin": { + "jscodeshift": "bin/jscodeshift.js" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@babel/preset-env": "^7.1.6" + }, + "peerDependenciesMeta": { + "@babel/preset-env": { + "optional": true + } + } + }, + "node_modules/jscodeshift/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -4701,6 +5659,16 @@ "json-buffer": "3.0.1" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -5023,7 +5991,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -5042,6 +6009,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.48.0.tgz", + "integrity": "sha512-R0CIKY/fXiC6y9xRBADgsK+VW2p/pcTJOMhLZf1T+uG+vJUvyUR02nGOgmIIC7MPkiNK4Ox8QDnbqF8rJYWPZQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -5052,6 +6028,30 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5109,6 +6109,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/motion-dom": { + "version": "12.43.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.43.0.tgz", + "integrity": "sha512-azKON4d9S65PEoFUiQTMTgPheEmzf2QngdRc50AKfJp9Q9mmcBVw22c8eMq9k8kxOFHdL7+WZY7N/5F/lwiDag==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.39.0" + } + }, + "node_modules/motion-utils": { + "version": "12.39.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz", + "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5157,6 +6172,13 @@ "dev": true, "license": "MIT" }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, "node_modules/next": { "version": "16.3.6", "resolved": "https://registry.npmjs.org/next/-/next-16.3.6.tgz", @@ -5459,6 +6481,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5518,6 +6550,105 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -5635,9 +6766,93 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, "license": "MIT" }, + "node_modules/react-redux": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", + "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/recast": { + "version": "0.23.21", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.21.tgz", + "integrity": "sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/recharts": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.10.1.tgz", + "integrity": "sha512-QXFrvt6IVcw7eeZCoyXTwkIJAX3Dv1nyVhMicXJ47GsGDDpcN8z6o644DibE9XjpBTThtsomLKnTV6lc+cVFUA==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^11.1.8", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.2.0", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5682,6 +6897,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/reselect": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz", + "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==", + "license": "MIT" + }, "node_modules/resolve": { "version": "2.0.0-next.7", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", @@ -5832,6 +7053,12 @@ "semver": "bin/semver.js" } }, + "node_modules/server-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", + "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -5881,6 +7108,19 @@ "node": ">= 0.4" } }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/sharp": { "version": "0.35.4", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", @@ -6043,6 +7283,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -6052,6 +7315,17 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -6233,6 +7507,12 @@ } } }, + "node_modules/styleq": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/styleq/-/styleq-0.2.1.tgz", + "integrity": "sha512-L0TR0NQb+X4/ktDEKmjWyp27gla+LUYi/by5k5SjKXf6/pvZP7wbwEB5J+tqxdFVPgzbsuz+d4RTScO/QZquBw==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -6280,6 +7560,12 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -6328,6 +7614,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6619,6 +7915,37 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.7.0.tgz", + "integrity": "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6734,6 +8061,20 @@ "node": ">=0.10.0" } }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index cdd73b0..b31fe00 100644 --- a/package.json +++ b/package.json @@ -3,17 +3,30 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", + "dev": "next dev -p 3200", "build": "next build", - "start": "next start", - "lint": "eslint" + "start": "next start -p 3200", + "lint": "eslint", + "clean": "rm -rf .next dist tsconfig.tsbuildinfo", + "bundle": "bash scripts/bundle.sh", + "theme:build": "astryx theme build src/theme/loyalyTheme.ts", + "typecheck": "tsc --noEmit", + "dev:preview": "next dev" }, "dependencies": { + "@astryxdesign/core": "^0.2.0", + "@astryxdesign/theme-neutral": "^0.2.0", + "@stylexjs/stylex": "^0.19.0", + "framer-motion": "^12.43.0", + "lucide-react": "^1.28.0", "next": "16.3.6", "react": "19.2.8", - "react-dom": "19.2.8" + "react-dom": "19.2.8", + "recharts": "^3.10.1", + "server-only": "^0.0.1" }, "devDependencies": { + "@astryxdesign/cli": "^0.2.0", "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", diff --git a/public/brand/login-hero-1.jpeg b/public/brand/login-hero-1.jpeg new file mode 100644 index 0000000..b521af4 Binary files /dev/null and b/public/brand/login-hero-1.jpeg differ diff --git a/public/brand/login-hero-2.jpeg b/public/brand/login-hero-2.jpeg new file mode 100644 index 0000000..06ac541 Binary files /dev/null and b/public/brand/login-hero-2.jpeg differ diff --git a/public/brand/loyaly-logo.png b/public/brand/loyaly-logo.png new file mode 100644 index 0000000..4faf3ab Binary files /dev/null and b/public/brand/loyaly-logo.png differ diff --git a/public/brand/loyaly-mark.png b/public/brand/loyaly-mark.png new file mode 100644 index 0000000..115e8d3 Binary files /dev/null and b/public/brand/loyaly-mark.png differ diff --git a/public/file.svg b/public/file.svg deleted file mode 100644 index 004145c..0000000 --- a/public/file.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/globe.svg b/public/globe.svg deleted file mode 100644 index 567f17b..0000000 --- a/public/globe.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/icons/icon-192.png b/public/icons/icon-192.png new file mode 100644 index 0000000..844fe8c Binary files /dev/null and b/public/icons/icon-192.png differ diff --git a/public/icons/icon-512.png b/public/icons/icon-512.png new file mode 100644 index 0000000..9984f76 Binary files /dev/null and b/public/icons/icon-512.png differ diff --git a/public/icons/icon-maskable-512.png b/public/icons/icon-maskable-512.png new file mode 100644 index 0000000..1e2f218 Binary files /dev/null and b/public/icons/icon-maskable-512.png differ diff --git a/public/next.svg b/public/next.svg deleted file mode 100644 index 5174b28..0000000 --- a/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/vercel.svg b/public/vercel.svg deleted file mode 100644 index 7705396..0000000 --- a/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/white-logo.png b/public/white-logo.png new file mode 100644 index 0000000..d627037 Binary files /dev/null and b/public/white-logo.png differ diff --git a/public/window.svg b/public/window.svg deleted file mode 100644 index b2b2a44..0000000 --- a/public/window.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/scripts/bundle.sh b/scripts/bundle.sh new file mode 100755 index 0000000..e7e603b --- /dev/null +++ b/scripts/bundle.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# +# Produce the deployable artifact for a non-Docker deploy. +# +# Copying .next/ wholesale is what filled the production disk: a working tree's +# .next/ reaches 2+ GB, but only three paths are read at runtime — +# .next/standalone the server + its traced node_modules +# .next/static hashed client assets (served by nginx at /_next/static/) +# public unhashed public assets +# Everything else (.next/cache, .next/dev, .next/server, .next/types) is +# build-host state and must never be shipped. +# +# Usage: npm run bundle -> dist/loyaly-mer-login.tar.gz + +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +OUT="dist" +STAGE="$OUT/bundle" + +for p in .next/standalone .next/static public; do + if [ ! -d "$p" ]; then + echo "error: $p missing — run 'npm run build' first" >&2 + exit 1 + fi +done + +# .env is the production environment, not a secret — LOYALY_API_BASE lives in +# it and the server reads it at boot. It is tracked in git, so a missing one +# means the tree is wrong, not that this deploy opted out. +if [ ! -f .env ]; then + echo "error: .env missing — it is committed; restore it with 'git checkout .env'" >&2 + exit 1 +fi + +rm -rf "$STAGE" +mkdir -p "$STAGE" + +# standalone already contains server.js, package.json and traced node_modules, +# and expects static/ and public/ to sit beside it in the same layout. +cp -R .next/standalone/. "$STAGE/" +mkdir -p "$STAGE/.next" +cp -R .next/static "$STAGE/.next/static" +cp -R public "$STAGE/public" + +# Beside server.js, which is where Next's loadEnvConfig looks. NOT .env.local — +# that is the dev override and would point the deployed server at 127.0.0.1. +cp .env "$STAGE/.env" + +TARBALL="$OUT/loyaly-mer-login.tar.gz" +rm -f "$TARBALL" +tar -czf "$TARBALL" -C "$STAGE" . + +echo +echo "bundle: $(du -sh "$STAGE" | cut -f1) ($STAGE)" +echo "tarball: $(du -sh "$TARBALL" | cut -f1) ($TARBALL)" +echo +echo "deploy: scp $TARBALL :/srv/ && tar -xzf loyaly-mer-login.tar.gz -C /srv/app" +echo "run: AUTH_SECRET=... PORT=3000 HOSTNAME=0.0.0.0 NODE_ENV=production node server.js" +echo +echo "note: LOYALY_API_BASE ships in the bundled .env. AUTH_SECRET does not —" +echo " it signs sessions and must come from the host's environment." diff --git a/src/app/(dev)/layout.tsx b/src/app/(dev)/layout.tsx new file mode 100644 index 0000000..b68798f --- /dev/null +++ b/src/app/(dev)/layout.tsx @@ -0,0 +1,16 @@ +import {notFound} from 'next/navigation'; + +/** + * /tokens and /charts are internal design-system tools, not product surface. + * Without this gate they appear in the production route table and are publicly + * reachable. `dynamic = 'force-dynamic'` is required so the check runs per + * request rather than being folded into a static prerender. + */ +export const dynamic = 'force-dynamic'; + +export default function DevLayout({children}: {children: React.ReactNode}) { + if (process.env.NODE_ENV === 'production') { + notFound(); + } + return <>{children}; +} diff --git a/src/app/(dev)/tokens/page.tsx b/src/app/(dev)/tokens/page.tsx new file mode 100644 index 0000000..720f11f --- /dev/null +++ b/src/app/(dev)/tokens/page.tsx @@ -0,0 +1,339 @@ +'use client'; + +/** + * Dev-only design-system audit page. + * + * Its job is to make the monochrome rule falsifiable: every Astryx surface + * that can carry a colour is rendered here, including the categorical hue + * variants we deliberately aliased to gray. Anything that still shows a hue + * other than success-green / warning-amber / error-red is a token we missed + * in aliasHueToGray(), and gets added there rather than patched locally. + */ + +import {useState, useSyncExternalStore} from 'react'; +import {Card} from '@astryxdesign/core/Card'; +import {Button} from '@astryxdesign/core/Button'; +import {Badge} from '@astryxdesign/core/Badge'; +import {Banner} from '@astryxdesign/core/Banner'; +import {StatusDot} from '@astryxdesign/core/StatusDot'; +import {ProgressBar} from '@astryxdesign/core/ProgressBar'; +import {Skeleton} from '@astryxdesign/core/Skeleton'; +import {EmptyState} from '@astryxdesign/core/EmptyState'; +import {TextInput} from '@astryxdesign/core/TextInput'; +import {TextArea} from '@astryxdesign/core/TextArea'; +import {Switch} from '@astryxdesign/core/Switch'; +import {CheckboxInput} from '@astryxdesign/core/CheckboxInput'; +import {Divider} from '@astryxdesign/core/Divider'; +import {Avatar} from '@astryxdesign/core/Avatar'; +import {Icon} from '@astryxdesign/core/Icon'; +import {Heading, Text} from '@astryxdesign/core/Text'; +import {VStack, HStack} from '@astryxdesign/core/Layout'; +import {Grid} from '@astryxdesign/core/Grid'; + +const HUES = [ + 'blue', + 'cyan', + 'green', + 'orange', + 'pink', + 'purple', + 'red', + 'teal', + 'yellow', +] as const; + +const DATA_RAMP = [1, 2, 3, 4, 5] as const; + +function Section({title, hint, children}: {title: string; hint?: string; children: React.ReactNode}) { + return ( + + + {title} + {hint ? {hint} : null} + + + {children} + + + ); +} + +function Swatch({label, color}: {label: string; color: string}) { + return ( + +
+ {label} + + ); +} + +export default function TokensPage() { + const [name, setName] = useState(''); + const [query, setQuery] = useState('Free coffee'); + const [notes, setNotes] = useState(''); + const [auto, setAuto] = useState(true); + const [closed, setClosed] = useState(false); + // "Has this hydrated yet?" via useSyncExternalStore — false on the server, + // true on the client, with no setState in an effect body. + const showError = useSyncExternalStore( + () => () => {}, + () => true, + () => false, + ); + + return ( + + + Design tokens + + Monochrome audit. The only colour permitted below is semantic: + success, warning, error. Everything else must read as gray. + + + +
+ + + + + + + + +
+ +
+ + Primary text — white on black. + Secondary text — #A1A1AA. + Disabled text — #71717A. + + + + + + + + +
+ +
+ + +
+ +
+ + + + + + + +
+ +
+ + {HUES.map((hue) => ( + + ))} + +
+ +
+ + {(['default', 'transparent', 'muted', 'gray', ...HUES] as const).map((v) => ( + + {v} + + ))} + +
+ +
+ + {(['neutral', 'accent', 'success', 'warning', 'error'] as const).map((v) => ( + + + {v} + + ))} + +
+ +
+ + + + + + +
+ +
+ + + + {/* + Status is applied AFTER mount on purpose. Astryx's useEntryAnimation + assumes 'use client' modules never execute on the server, which is + false in the App Router: SSR renders without the slide-down class, + then hydration (which lands after the first rAF) renders with it, + and React reports a class mismatch it will not patch up. + Real forms set status on blur/submit, so they never hit this; only + a status present at initial paint does. See AGENTS.md. + */} + + + +