stack/apps/dashboard/DESIGN-GUIDE.md
BilalG1 57ff5d3ce9
Some checks failed
all-good: Did all the other checks pass? / all-good (push) Has been cancelled
Ensure Prisma migrations are in sync with the schema / check_prisma_migrations (22.x) (push) Has been cancelled
DB migration compat / Check if migrations changed (push) Has been cancelled
Docker Server Build and Push / Docker Build and Push Server (push) Has been cancelled
Docker Server Build and Run / docker (push) Has been cancelled
Runs E2E API Tests (Local Emulator) / E2E Tests (Local Emulator, Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (mock, 22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (prod, 22.x) (push) Has been cancelled
Runs E2E API Tests with custom port prefix / build (22.x) (push) Has been cancelled
Runs E2E Fallback Tests / E2E Fallback Tests (Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Lint & build / lint_and_build (24) (push) Has been cancelled
TOC Generator / TOC Generator (push) Has been cancelled
DB migration compat / Back-compat — Current branch migrations with ${{ needs.check-migrations-changed.outputs.base_branch }} branch code (push) Has been cancelled
DB migration compat / Forward-compat — Current branch code with ${{ needs.check-migrations-changed.outputs.base_branch }} branch migrations (push) Has been cancelled
DB migration compat / No migration changes (skipped) (push) Has been cancelled
feat(hexclave): PR 2 — visible rebrand (Hexclave brand goes public) (#1481)
## Summary

**Stacked on [#1475](https://github.com/hexclave/stack-auth/pull/1475)**
(`cl/hexclave-pr1`, the invisible compatibility layer). Diff vs that
base = the actual PR 2 code.

This is **PR 2 of the Stack Auth → Hexclave rebrand: the visible flip**.
Old wire identifiers (cookies, request/response headers, Bearer prefix,
JWT issuers, MCP tool name) keep working indefinitely via PR 1's
dual-accept. This PR flips every user-visible surface — package names
taught in docs, SDK class names in code examples, dashboard setup
snippets, page titles, error messages, email content, CLI binary,
default base URLs, GitHub repo slug, contributor guidance — to the
Hexclave brand.

See [`RENAME-TO-HEXCLAVE.md`](./RENAME-TO-HEXCLAVE.md) → *"PR 2: Rebrand
to Hexclave (visible)"* for the full per-work-area spec.

## What's implemented (per the plan's PR 2 scope)

- **SDK base URLs** flipped: `defaultBaseUrl` and
`defaultAnalyticsBaseUrl` in
[common.ts](packages/template/src/lib/stack-app/apps/implementations/common.ts:127)
→ `https://api.hexclave.com` / `https://r.hexclave.com`. PR 1's
[`getHardcodedFallbackUrls`](packages/stack-shared/src/utils/urls.tsx:199)
table now keys on the Hexclave domain.

- **Domain inventory sweep** (16 subdomains from the plan): every
`api/app/docs/discord/demo/mcp/skill/feedback/test/preview/r/api2/api.staging/idp-jwk-audience/built-with.stack-auth.com`
reference in production code, docs-mintlify, examples, READMEs, and
contributor guidance flipped to `*.hexclave.com`. Carve-outs: PR 1's
intentional JWT issuer dual-accept table in
[tokens.tsx](apps/backend/src/lib/tokens.tsx), the legacy `./docs/`
folder, the `unified-docs-widget` allowlist (deliberately accepts both
during DNS transition), and `url-targets.ts` hosted-component default
(baked into existing customer deploys).

- **`@deprecated` JSDoc** on every `Stack*` public export
([packages/template/src/lib/stack-app/index.ts](packages/template/src/lib/stack-app/index.ts)
+ [packages/template/src/index.ts](packages/template/src/index.ts)) —
`StackClientApp`, `StackServerApp`, `StackAdminApp` + every
constructor/options/JSON type, `StackHandler`, `StackProvider`,
`StackTheme`, `useStackApp`, `defineStackConfig`, `StackConfig`.
Hexclave\* aliases are now canonical.

- **Runtime `console.warn`**
([packages/template/src/internal/deprecation-warning.ts](packages/template/src/internal/deprecation-warning.ts))
— once-per-process when the SDK is loaded from a `@stackframe/*`
artifact. Detection uses the existing
`STACK_COMPILE_TIME_CLIENT_PACKAGE_VERSION_SENTINEL` (rewritten at build
time to e.g. `js @stackframe/stack@2.8.92` or `js
@hexclave/next@1.0.0`); `@hexclave/*` mirror artifacts short-circuit the
warning.

- **Tier 3 data migration**: new idempotent SQL migration
[`20260523000000_rename_internal_project_to_hexclave`](apps/backend/prisma/migrations/20260523000000_rename_internal_project_to_hexclave/migration.sql)
— updates the internal Project `displayName` 'Stack Dashboard' →
'Hexclave Dashboard' and `description` only if both still hold the
pre-rebrand defaults. Operator-renamed projects untouched, missing row
no-ops, re-runs are no-ops. [`seed.ts`](apps/backend/prisma/seed.ts:87)
default flipped. `getSharedEmailConfig("Stack Auth")` → `("Hexclave")`.

- **Tier 4 brand strings** (mechanical sweep, ~340 files):
- Page + OpenAPI titles (Hexclave API / Dashboard / REST API / Webhooks
API / Documentation). OpenAPI `info.description` documents
`X-Hexclave-*` headers as canonical with compat note on `X-Stack-*`.
- `HexclaveAssertionError` message text
([errors.tsx:71](packages/stack-shared/src/utils/errors.tsx:71)) — "an
error in Stack." → "an error in Hexclave."
- Known-error message templates
([known-errors.tsx](packages/stack-shared/src/known-errors.tsx)) flipped
to lead with `x-hexclave-*` + the new `docs.hexclave.com` URL; legacy
`x-stack-*` mentioned as compat aliases. **25 e2e test files updated in
lockstep**.
- Email content: failed-emails-digest body, sendTestEmail recipient (now
`sent-with-hexclave.com`), test-email-recipient default.
  - `CHANGELOG.md` title → "Hexclave Changelog".
- `AGENTS.md` env var convention: new vars prefix `HEXCLAVE_` /
`NEXT_PUBLIC_HEXCLAVE_` for Category A/B; legacy `STACK_*` explicitly
noted as accepted via PR 1's dual-read.

- **CLI / init wizard**:
- Every dashboard setup snippet, init-stack template, and docs-mintlify
page teaches `npx @hexclave/cli@latest init` (was
`@stackframe/stack-cli`).
[setup-page.tsx](apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/setup-page.tsx)
+
[link-existing-onboarding](apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client-parts/link-existing-onboarding.tsx).
- [init-stack](packages/init-stack/src/index.ts:634)
`STACK_*_INSTALL_PACKAGE_NAME_OVERRIDE` defaults flipped to
`@hexclave/*`.
- Generated `stack/client.ts` / `stack/server.ts` import from
`@hexclave/next` and reference `HexclaveClientApp` /
`HexclaveServerApp`.
- Internal `StackAuthKeys` dashboard component renamed to
`HexclaveKeys`.

- **docs-mintlify rewrite** (legacy `./docs/` intentionally untouched
per scoping decision):
- **78 MDX files swept**.
`@stackframe/{react,stack,js,tanstack-start,...}` →
`@hexclave/{react,stack,js,...}` in install snippets and code blocks;
`Stack*` SDK class names → `Hexclave*` in all code examples; 'Stack
Auth' brand phrase → 'Hexclave'.
- `openapi/{server,admin,client,webhooks}.json` titles → 'Hexclave REST
API' / 'Hexclave Webhooks API'.

- **Generators flipped before regeneration**:
-
[`packages/stack-shared/src/helpers/init-prompt.ts`](packages/stack-shared/src/helpers/init-prompt.ts),
[`/ai/prompts.ts`](packages/stack-shared/src/ai/prompts.ts),
[`apps/backend/src/lib/ai/prompts.ts`](apps/backend/src/lib/ai/prompts.ts),
[`apps/backend/src/lib/ai/tools/create-email-{template,draft}.ts`](apps/backend/src/lib/ai/tools/create-email-template.ts),
[`apps/skills/src/app/route.ts`](apps/skills/src/app/route.ts) (taught
MCP tool → `ask_hexclave` with compat note; CLI binary teach →
`hexclave`),
[`docs-mintlify/snippets/home-prompt-island.jsx`](docs-mintlify/snippets/home-prompt-island.jsx),
[`packages/template/README.md`](packages/template/README.md) +
integrations/convex/component/README.md.
  - `generate-sdks` propagated changes to `packages/{react,stack,js}`.

- **OpenAPI dual-documentation**:
[`apps/backend/src/app/api/latest/route.ts`](apps/backend/src/app/api/latest/route.ts)
now lists `X-Hexclave-*` headers as primary documented schemas with
`X-Stack-*` duplicates marked `.optional()` (both accepted at runtime by
PR 1's normalize-at-proxy shim).

- **`@stackframe/emails` virtual module**: dual-aliased to
`@hexclave/emails` at the bundler boundary
([email-rendering.tsx:89](apps/backend/src/lib/email-rendering.tsx:89)).
Stored email templates continue to import from either name; new
AI-generated templates and the system prompt teach `@hexclave/emails`.

- **Tier 2 mirror-publish wiring** (new this PR, lays the groundwork for
`@hexclave/*` first publish):
-
[`scripts/rewrite-packages-to-hexclave.ts`](scripts/rewrite-packages-to-hexclave.ts)
— rewrites 9 publishable `@stackframe/*` → `@hexclave/*` `package.json`
files (reads `HEXCLAVE_VERSION` env or `--version=` flag), pins
cross-deps to the shared `@hexclave` version, registers `hexclave` bin
alongside `stack` for `@hexclave/cli`.
-
[`.github/workflows/npm-publish.yaml`](.github/workflows/npm-publish.yaml)
appended with rewrite-then-republish step. `pnpm publish` skips
already-on-npm versions so reruns are safe.

- **Sender email domain**: `noreply@stackframe.co` →
`noreply@sent-with-hexclave.com` (the dedicated transactional-sender
domain split per the plan, to isolate bulk deliverability from
`hexclave.com` reputation); `security@` / `team@stack-auth.com` inbound
mailboxes → `@hexclave.com`.

- **Self-host docs**: docker network / container names in the bash
examples flipped from `stack-auth` to `hexclave` (`hexclave-postgres`,
`hexclave-clickhouse`, `hexclave.env`). The docker image tag
`stackauth/server:latest` stays per the plan's locked decision.

- **GitHub repo slug**: `hexclave/stack-auth` → `hexclave/hexclave` in
every `package.json` `repository` field, README link, CHANGELOG
raw-asset URL.

## Carve-outs (deliberately untouched)

-
**[`apps/backend/src/lib/tokens.tsx`](apps/backend/src/lib/tokens.tsx)**
JWT issuer dual-accept table — PR 1 intentional infrastructure, kept
indefinitely.
- **Legacy `./docs/` folder** — per scoping decision (only
`docs-mintlify/` rewritten).
- **`unified-docs-widget` hostname allowlist** — accepts both
`.hexclave.com` (canonical) and `.stack-auth.com` (transition window)
for DNS rollout.
- **`url-targets.ts`** hosted-domain default
`.built-with-stack-auth.com` — wire identifier baked into existing
customer deploys; indefinite read-fallback.
- **Binary visual assets** (logos, favicons, OG images, README
screenshots) — out of scope for this PR. Need design work; tracked
separately.

## Verification

- **`pnpm typecheck`** on
`packages/{template,stack-shared,react,stack,js}` + `apps/dashboard`:
**all green**. The remaining backend / e-commerce-demo typecheck errors
are pre-existing (Prisma codegen output +
`./generated/api-versions.json` not present in fresh worktrees without
`pnpm run codegen-prisma` + a live DB) and unrelated to this diff.
- **`pnpm lint`** on the same 6 packages: all green.
- **Final grep** for residual `Stack Auth` / `stack-auth.com` /
`@stackframe/stack-cli@latest` references: zero outside the intentional
carve-outs above.
- **25 e2e test files updated in lockstep** with the known-error message
changes (asserted strings flipped to match the new x-hexclave-* +
compat-note messages).

## Deploy blockers (ops sequencing before this rebrand goes live)

This PR is code-complete, but the rebrand's visible surfaces (SDK
default URLs, dashboard links, npm READMEs, REST error messages, runtime
deprecation warning) all point at `*.hexclave.com` / `@hexclave/*`
resources that don't exist yet. None of these are fixable from a PR —
they're ops/registrar/npm work that has to be sequenced before merging
this to a release tag.

Suggested ordering, hardest blockers first:

### Tier 1 — required before customer-facing deploy (everything below
this line *will visibly break customers on day 1* if skipped)

1. **DNS + TLS for `api.hexclave.com` + `api1./api2.hexclave.com`** →
must point at the same backend that serves `api.stack-auth.com` (or a
backend that mirrors PR 1's dual-accept). The SDK's new `defaultBaseUrl`
is `https://api.hexclave.com`; every customer that relied on the old
default and upgrades to a post-PR2 SDK build sends API requests here.
Until this resolves, every default-config customer's API call NXDOMAINs.
2. **DNS for `app.hexclave.com`** → the dashboard. Referenced in the
SDK's default-error messages ("Please create a project on the Hexclave
dashboard at https://app.hexclave.com"), the init-stack flow's
`wizard-congrats` redirect, and the OAuth dashboard handoff.
3. **DNS for `docs.hexclave.com`** + Mintlify deploy → the SDK runtime
deprecation warning (`https://docs.hexclave.com/migration`), every
README, every "Learn more" link in the dashboard, and every REST API
error body (`/api/overview#authentication`) points here. The MDX is in
this PR; the docs build target needs DNS.
4. **DNS for `mcp.hexclave.com`** → the MCP server endpoint that every
taught agent integration (`claude mcp add ...`, `cursor`, `codex`,
`vscode`) registers. Until this resolves, every `npx
@hexclave/cli@latest init` MCP-registration step fails.
5. **Reserve the `@hexclave` npm scope + set repo variable
`HEXCLAVE_VERSION`** → the mirror-publish step in
`.github/workflows/npm-publish.yaml` is gated on this variable. Without
it, the entire taught onboarding command `npx @hexclave/cli@latest init`
404s from the npm registry, *and* every README that says "install
`@hexclave/next`" leads to install failure. Pick the initial version
intentionally (`1.0.0` or aligned to `@stackframe/stack`); don't accept
a silent default.

### Tier 2 — required before announcing the rebrand publicly (lookalike
or low-traffic surfaces, but visibly broken)

6. **DNS for `r.hexclave.com`** → the analytics beacon
`defaultAnalyticsBaseUrl`. Silent failure if missing (analytics drops),
but should land alongside Tier 1.
7. **Register `sent-with-hexclave.com` + full email auth (SPF / DKIM /
DMARC)** → the new default sender domain for shared-sender transactional
emails. Without it the dashboard "send test email" path emits bounces,
and shared-sender flows (`getSharedEmailConfig("Hexclave")`) deliver to
spam at best.
8. **MX + SPF / DMARC for `hexclave.com`** → `team@hexclave.com` and
`security@hexclave.com` mailboxes. The security disclosure mailbox is
referenced in [`.github/SECURITY.md`](.github/SECURITY.md);
`team@hexclave.com` is the actual recipient of internal feedback emails
sent at runtime by
[`apps/backend/src/lib/internal-feedback-emails.tsx`](apps/backend/src/lib/internal-feedback-emails.tsx).
Today, every runtime feedback email bounces.
9. **DNS for `skill.hexclave.com`** → the canonical AI-agent skill fetch
URL (the agent bootstrap pivot). Without it, the entire "agent downloads
`SKILL.md` from a known URL" flow taught in
[`packages/stack-shared/src/helpers/init-prompt.ts`](packages/stack-shared/src/helpers/init-prompt.ts)
fails.
10. **Create `github.com/hexclave/hexclave` as a public repo** (even as
a redirect to `hexclave/stack-auth`) **OR** rewrite every `package.json`
`"repository"` field + dashboard footer "view on GitHub" link to point
at `hexclave/stack-auth` (which already exists). Currently every npm
package page's "Repository" link is dead, and the dashboard's GitHub
button + dev-tool repo link are dead.

### Tier 3 — broken but low-visibility / low-traffic

11. **DNS for `discord.hexclave.com`** → Discord invite redirect, used
in every README's chip and the dashboard footer.
12. **DNS for `demo.hexclave.com`** → " Demo" badge in every npm
package README. Broken-image badge on the package page.
13. **DNS + TLS for `built-with-hexclave.com`** → optional
hosted-handler domain (the default reverted to
`.built-with-stack-auth.com` in this PR's carve-outs, so this only
matters for projects that manually flip).

## Other follow-ups (not deploy-blocking)

- **E2E snapshot regen across the full suite** for the dual-emitted
`x-hexclave-*` response headers (PR 1 follow-up; `vitest -u` in CI
absorbs).
- **Binary visual assets** — logos, favicons, OG images, README
screenshots; need design pass.
- **Backend OpenAPI fumadocs regen** in CI flow — the JSON files in
`docs-mintlify/openapi/` are committed but regen runs in CI. Verify the
workflow that does this still works against the post-PR2 source.
- **Backend typecheck infra debt** — needs `codegen-prisma` +
`codegen-route-info` to clear; pre-existing, unaffected by this PR.

## Test plan

- [ ] CI runs full e2e suite (with `vitest -u` to absorb residual
snapshot deltas, then committed back).
- [ ] Spot-check: new `@hexclave/cli init` (once published) generates
`hexclave.config.ts` and works against a fresh project.
- [ ] Spot-check: existing customer with `@stackframe/stack` import sees
the once-per-process `console.warn` recommending `@hexclave/next` on SDK
init.
- [ ] Manual: dashboard setup page renders the `npx @hexclave/cli@latest
init` snippet and the `x-hexclave-publishable-client-key` API header in
the curl example.
- [ ] Manual: a fresh `pnpm run prisma migrate` against a clean DB sets
the internal project displayName to 'Hexclave Dashboard'.

---------

Co-authored-by: Konstantin Wohlwend <n2d4xc@gmail.com>
2026-05-26 19:18:20 -07:00

32 KiB

Hexclave Dashboard Design Guide

This guide defines the source of truth for dashboard UI design and implementation. It is intentionally written for both humans and AI agents.

If this guide conflicts with older examples in the codebase, follow this guide.


1) Core Principle (Non-Negotiable)

Always prefer components from apps/dashboard/src/components/design-components.

  • Do not build new ad-hoc visual primitives (for example custom GlassCard, custom ChartCard, custom badge pills, custom pill toggles, custom list rows) if a design-components component exists.
  • If the desired UI can be achieved by tweaking/customizing/extending a design-components component, do that instead of creating a page-local alternative.
  • In all cases, default to design-components first; only use a non-design-components approach when there is absolutely no viable way to achieve the result with design-components.
  • Use @/components/ui/* primitives only when no design-components equivalent exists, or when the design-components component intentionally wraps the primitive.
  • Match existing design-components behavior:
    • hover-exit transitions (transition-* duration-150 hover:transition-none)
    • glassmorphic surfaces where appropriate
    • semantic variants for alerts/badges
    • async-safe click handlers via design-components primitives

2) Fast Decision Tree

Use this when implementing a new dashboard UI quickly:

  1. Need a section container/card?
    • Use DesignCard.
    • For chart-heavy analytics surfaces (especially Recharts tooltips/overflow), use DesignAnalyticsCard.
  2. Need user-facing status/info/warning/error message?
    • Use DesignAlert.
  3. Need small semantic label (sent, failed, queued, active)?
    • Use DesignBadge.
  4. Need user action button, especially async?
    • Use DesignButton.
  5. Need text input/selectors in design-components surfaces?
    • Use DesignInput and DesignSelectorDropdown.
  6. Need a segmented/pill switcher?
    • Use DesignPillToggle.
  7. Need category tabs with count badges?
    • Use DesignCategoryTabs.
  8. Need row/list item with action buttons/menu?
    • Use DesignListItemRow (or DesignUserList for user rows).
  9. Need settings/property grid editor?
    • Use DesignEditableGrid.
  10. Need interactive / sortable / searchable data table?
  • Use DataGrid + useDataSource + createDefaultDataGridState from @stackframe/dashboard-ui-components.
  1. Need dropdown action/selector/toggle menu?
  • Use DesignMenu.
  1. Need a focus-trapping modal/dialog (confirmation, rich modal, tester, form)?
  • Use DesignDialog.

3) Allowed Base UI Usage

@/components/ui/* can still be used for primitives that do not currently have a design-components equivalent:

  • confirmation-pattern helpers like ActionDialog / FormDialog (these wrap an onClick/form submit lifecycle and remain useful when you only need a dressed-up confirm/submit prompt)
  • side-sheets/popovers (Sheet, Popover, etc.)
  • complex layout containers where design-components does not provide one
  • highly specialized editor internals

For any general-purpose modal surface (rich detail dialogs, tester surfaces, data dialogs, settings popovers presented as modals), use DesignDialog instead of wiring Dialog + DialogContent + DialogHeader etc. by hand. DesignDialog is the canonical glassmorphic dialog surface for the dashboard — see §4.14 below.

When using a primitive directly:

  • keep visual style compatible with design-components surfaces
  • do not duplicate a design-components component API locally
  • consider creating/extending a design-components component instead of repeating local patterns

3.1) Best Practices (Always Apply)

  • Build with design-components primitives first, then add minimal page-level styling.
  • Keep components composable: pass data/config via props instead of hardcoding display logic.
  • Favor semantic APIs (variant, color, gradient) over raw class-heavy style forks.
  • Use accessible defaults:
    • clear labels for icon-only controls
    • keyboard focus visibility (focus-visible:*)
    • semantic roles where applicable
  • Keep behavior deterministic:
    • one visual language per screen
    • one status-color mapping across all routes
    • one interaction pattern per control type

3.2) Color System (Light + Dark Theme)

Use semantic tokens and design-components variants first. Avoid ad-hoc hardcoded colors unless there is a documented semantic reason.

Theme token usage priority

  1. Use component variants (DesignAlert, DesignBadge, DesignCard gradient, tab/toggle gradients).
  2. Use semantic Tailwind tokens (bg-background, text-foreground, text-muted-foreground, border-border).
  3. Use opacity layers for subtle surfaces (for example bg-foreground/[0.03]).

Surface and text rules

  • Primary surfaces: bg-background + subtle ring/border.
  • Secondary/muted surfaces: low-opacity foreground overlays.
  • Primary text: text-foreground.
  • Secondary text: text-muted-foreground.
  • Never use pure black/white hardcoded utility values for app UI text/surfaces.

Semantic state colors

  • Success: green/emerald (DesignAlert variant="success", DesignBadge color="green")
  • Error: red (DesignAlert variant="error", DesignBadge color="red")
  • Warning: orange/amber (DesignAlert variant="warning", DesignBadge color="orange")
  • Info: blue/cyan (DesignAlert variant="info", DesignBadge color="blue" or "cyan")

Light vs dark guidance

  • Ensure every custom color choice has dark-mode readability.
  • In dark mode, reduce high-contrast fills and rely on low-opacity tints + rings.
  • Keep contrast high for text and medium for non-critical chrome.

3.3) Typography System

Keep typography concise and consistent. Prefer existing design-components/header patterns.

  • Page title: text-xl sm:text-2xl font-semibold tracking-tight
  • Section heading: text-xs font-semibold uppercase tracking-wider
  • Body/default control text: text-sm
  • Secondary metadata: text-xs text-muted-foreground
  • Micro labels/badges: text-[10px] to text-[11px]

Typography rules

  • Use uppercase tracking only for section labels and compact metadata headings.
  • Avoid introducing new arbitrary font sizes when an existing size serves the purpose.
  • Keep line-length short in cards and alerts for scanability.
  • For numeric/stat values, use tabular numerals where needed.

3.4) Spacing and Layout Guidelines

Use a compact, repeatable spacing rhythm.

Spacing rhythm

  • gap-1 (4px): tight icon/text coupling
  • gap-2 (8px): compact control spacing
  • gap-3 (12px): standard row spacing
  • gap-4 (16px): section-internal spacing
  • gap-5 (20px): larger section grouping

Padding rhythm

  • p-2 / px-3 py-2: compact controls
  • p-3: standard compact blocks
  • p-4 to p-5: card content/major sections

Layout rules

  • Use rounded-2xl for major containers/cards.
  • Use rounded-xl for controls (inputs, toggles, small cards).
  • Preserve visual hierarchy:
    • page spacing > section spacing > control spacing
  • Avoid mixing unrelated spacing scales inside a single component.

3.5) Animation and Micro-Interactions

Motion should feel immediate, subtle, and informative.

Core motion rules

  • No hover-enter delay transitions.
  • Use hover-exit transitions: transition-* duration-150 hover:transition-none.
  • Keep interaction transitions short and subtle.
  • Do not use large animated movement in dense admin surfaces.

Duration guidance

  • 50ms-100ms: very small icon feedback
  • 150ms: standard hover/focus/press recovery
  • 200ms-300ms: layout/state transitions (panel collapse, sheet-like reveals)
  • >300ms: only ambient/non-critical effects

Micro-interaction patterns

  • Hover:
    • text brightens slightly
    • ring/shadow intensifies subtly
  • Press:
    • instant feedback (no delayed press animation)
  • Focus:
    • visible focus-visible ring on all interactive elements
  • Loading:
    • use design-components built-in loading states (DesignButton, tabs/toggles with async)
    • never freeze the UI without feedback

Motion accessibility

  • Respect prefers-reduced-motion for non-essential effects.
  • Keep micro-interactions understandable without relying on animation alone.

3.6) Best-Practice Checklist (Visual + UX)

  • Uses design-components primitives before custom wrappers.
  • Uses semantic variants/colors instead of custom status styles.
  • Works in both light and dark themes with readable contrast.
  • Uses approved typography scale and hierarchy.
  • Uses consistent spacing rhythm (gap-2/3/4, p-3/4/5).
  • Uses snappy hover-exit transitions and clear focus rings.
  • Provides clear loading/disabled/empty/error states.

4) Component-by-Component Contract

This section is prescriptive: use these components with these props for these scenarios.

4.1 DesignCard

File: apps/dashboard/src/components/design-components/card.tsx

Use for:

  • page sections
  • grouped controls
  • analytics panels
  • list containers
  • glassmorphic blocks used in email/project pages

Props you should use most:

  • title, icon, optional subtitle for section headers
  • gradient: "blue" | "cyan" | "purple" | "green" | "orange" | "default"
  • glassmorphic (optional explicit override)
  • contentClassName for content spacing overrides

Important behavior:

  • If title is provided, icon is required by type.
  • Layout is auto-derived:
    • title + subtitle -> full header
    • title only -> compact header
    • no title -> body-only card
  • useGlassmorphicDefault() makes nested components default to glassmorphic behavior.

Default recommendation:

  • for dashboard sections, use glassmorphic style (either explicit or via nesting context)
  • use gradient="default" unless there is semantic reason for colored tint

4.1.1 DesignAnalyticsCard (and chart helpers)

File: apps/dashboard/src/components/design-components/analytics-card.tsx

Use for:

  • chart-heavy analytics shells on overview and metrics surfaces
  • cards where chart tooltips need to escape clipping/stacking issues
  • previously duplicated glass analytics wrappers (ChartCard, GlassCard clones)

Exports:

  • DesignAnalyticsCard
  • DesignAnalyticsCardHeader
  • DesignChartLegend
  • useInfiniteListWindow
  • DesignInfiniteScrollList

DesignAnalyticsCard props:

  • gradient: "blue" | "cyan" | "purple" | "green" | "orange" | "slate"
  • className

Rules:

  • prefer DesignAnalyticsCard over local chart wrappers for overview/analytics cards
  • keep chart implementation local (Recharts config, data transforms), but keep shell/legend/list plumbing shared
  • use DesignChartLegend instead of hand-rolled dot/label legend rows when layout matches
  • use useInfiniteListWindow for incremental scrolling lists in analytics/list tabs

4.2 DesignAlert

File: apps/dashboard/src/components/design-components/alert.tsx

Use for:

  • save success/failure
  • warning states (for example SMTP/provider configuration warnings)
  • informational notices

Props:

  • variant: "default" | "success" | "error" | "warning" | "info"
  • title
  • description
  • glassmorphic when rendered on glass surfaces

Rules:

  • use semantic variant instead of custom alert class combinations
  • keep title short and actionable
  • put longer explanation in description

4.3 DesignBadge

File: apps/dashboard/src/components/design-components/badge.tsx

Use for:

  • status chips (sent, failed, queued, draft, active)
  • small semantic labels in headers and lists

Props:

  • label
  • color: "blue" | "cyan" | "purple" | "green" | "orange" | "red"
  • icon (optional)
  • size: "sm" | "md"
  • contentMode: "both" | "text" | "icon"

Rules:

  • choose color by meaning, not preference
  • use contentMode="icon" only when icon is provided
  • for icon-only badges, accessibility is already handled via aria-label

4.4 DesignButton

File: apps/dashboard/src/components/design-components/button.tsx

Use for:

  • all primary/secondary actions in dashboard surfaces
  • async submit/save/delete actions

Props:

  • variant: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" | "plain"
  • size: "default" | "sm" | "lg" | "icon"
  • onClick (can be async)
  • loading (optional controlled mode)
  • loadingStyle: "spinner" | "disabled"
  • asChild if composition with links/triggers is needed

Rules:

  • prefer DesignButton over base Button for async behavior and consistent loading semantics
  • do not hand-roll loading spinners for standard button actions

4.5 DesignInput

File: apps/dashboard/src/components/design-components/input.tsx

Use for:

  • text fields inside design-components surfaces
  • compact filter fields and inline settings inputs

Props:

  • size: "sm" | "md" | "lg"
  • prefixItem for fixed prefix UI
  • leadingIcon for icon-leading input
  • regular input props (placeholder, value, onChange, disabled, etc.)

Rules:

  • use prefixItem for prefixed values (domains/paths/currency symbols)
  • use leadingIcon for search or query fields

4.6 DesignSelectorDropdown

File: apps/dashboard/src/components/design-components/select.tsx

Use for:

  • standard single-select dropdowns in dashboard settings and filters

Props:

  • value
  • onValueChange
  • options: { value, label, disabled? }[]
  • placeholder
  • size: "sm" | "md" | "lg"
  • disabled

Rules:

  • prefer this instead of raw Select in feature pages unless custom behavior is required

4.7 DesignPillToggle

File: apps/dashboard/src/components/design-components/pill-toggle.tsx

Use for:

  • segmented controls
  • viewport switches
  • compact mode switches

Props:

  • options: { id, label, icon? }[]
  • selected
  • onSelect
  • size: "sm" | "md" | "lg"
  • gradient
  • showLabels (set false for icon-only controls)
  • glassmorphic

Rules:

  • do not create custom inline pill toggle components if this fits
  • use showLabels={false} only with clear icons and tooltip-friendly labels

4.8 DesignCategoryTabs

File: apps/dashboard/src/components/design-components/tabs.tsx

Use for:

  • top-level category switching with optional count badges

Props:

  • categories: { id, label, count?, badgeCount? }[]
  • selectedCategory
  • onSelect
  • showBadge
  • size: "sm" | "md"
  • gradient
  • glassmorphic

Rules:

  • use for category-level navigation, not micro toggles
  • if there are no category counts and control is small, DesignPillToggle may be better

4.9 DesignMenu

File: apps/dashboard/src/components/design-components/menu.tsx

Use for:

  • standard row/card action menus
  • selector menu (radio group)
  • toggles menu (checkbox items)

Variants and required props:

  • variant="actions" with items
  • variant="selector" with options, value, onValueChange
  • variant="toggles" with options, onToggleChange

Common shared props:

  • trigger: "button" | "icon"
  • triggerLabel
  • triggerIcon
  • align
  • label
  • withIcons

Rules:

  • prefer this over local DropdownMenu wrappers for common action menus

4.10 DesignListItemRow and DesignUserList

File: apps/dashboard/src/components/design-components/list.tsx

Use for:

  • structured list rows with optional per-row actions
  • user activity/user list rows

DesignListItemRow props:

  • title, optional subtitle
  • icon
  • size: "sm" | "lg"
  • buttons (direct actions or menu actions)
  • onClick

DesignUserList props:

  • users: { name, email, time, color? }[]
  • onUserClick
  • showAvatar
  • gradient: "blue-purple" | "cyan-blue" | "none"

Rules:

  • use size="sm" for dense lists
  • use size="lg" for card-like list entries
  • replace custom row/card list items with this unless layout is truly unique

4.11 DesignEditableGrid

File: apps/dashboard/src/components/design-components/editable-grid.tsx

Use for:

  • key/value settings editors
  • mixed-type setting controls
  • deferred save/discard patterns

Props:

  • items (typed union: text, boolean, dropdown, custom-dropdown, custom-button, custom)
  • columns: 1 | 2
  • deferredSave, hasChanges, onSave, onDiscard
  • externalModifiedKeys

Rules:

  • prefer this for config forms that are row-based and editable inline
  • use deferred save mode when many fields should be committed together

4.12 DataGrid + useDataSource + createDefaultDataGridState

Package: @stackframe/dashboard-ui-components

Use for:

  • interactive, sortable, searchable data tables
  • any table with more than ~20 rows or that needs pagination, column visibility, quick search, or CSV export

Canonical pattern:

import { DataGrid, useDataSource, createDefaultDataGridState, type DataGridColumnDef } from "@stackframe/dashboard-ui-components";

const columns: DataGridColumnDef<MyRow>[] = [
  { id: "name", header: "Name", accessor: "name", width: 200, type: "string" },
  { id: "status", header: "Status", accessor: "status", width: 120, type: "singleSelect",
    valueOptions: [{ value: "active", label: "Active" }, { value: "inactive", label: "Inactive" }],
    renderCell: ({ value }) => <DesignBadge label={String(value)} color={value === "active" ? "green" : "red"} size="sm" /> },
];

const [gridState, setGridState] = useState(() => createDefaultDataGridState(columns));
const gridData = useDataSource({
  data: myRows,
  columns,
  getRowId: (row) => row.id,
  sorting: gridState.sorting,
  quickSearch: gridState.quickSearch,
  pagination: gridState.pagination,
  paginationMode: "client",
});

<DataGrid
  columns={columns}
  rows={gridData.rows}
  getRowId={(row) => row.id}
  totalRowCount={gridData.totalRowCount}
  isLoading={gridData.isLoading}
  state={gridState}
  onChange={setGridState}
  toolbar={false}       // set to false to hide; omit for default toolbar
  onRowClick={(row) => handleClick(row)}
  maxHeight={400}
/>

Key props:

  • columns (DataGridColumnDef[]): column definitions with id, header, accessor, type, optional renderCell, optional cellOverflow
  • rows (TRow[]): always gridData.rows from useDataSource, NEVER your raw array
  • getRowId ((row) => RowId): unique row identifier (RowId is string)
  • state / onChange: fully controlled grid state (sorting, pagination, search, visibility)
  • totalRowCount: total rows for pagination display
  • toolbar: false to hide, omit for default, or render function for custom
  • onRowClick: optional row click handler
  • maxHeight: max pixel height before scrolling
  • rowHeight: number (default 44) for fixed height, or "auto" for dynamic row measurement
  • estimatedRowHeight: estimated row height for the virtualizer when rowHeight="auto" (default 44)

Cell overflow:

  • cellOverflow: "truncate" (default): single-line with text-overflow ellipsis
  • cellOverflow: "wrap": content wraps naturally; rows grow when rowHeight="auto"
  • Use cellOverflow: "wrap" for badge lists, permission chips, multi-line text
  • Use default truncate for UUIDs, emails, dates, single-line text
const columns: DataGridColumnDef<MyRow>[] = [
  { id: "userId", header: "User ID", width: 130 },                            // truncates (default)
  { id: "auth", header: "Auth methods", width: 150, cellOverflow: "wrap",     // badges wrap, row grows
    renderCell: ({ row }) => (
      <div className="flex flex-wrap gap-1">
        {row.authTypes.map((t) => <Badge key={t}>{t}</Badge>)}
      </div>
    ),
  },
];

<DataGrid columns={columns} rowHeight="auto" estimatedRowHeight={48} ... />

Rules:

  • always initialize state with createDefaultDataGridState(columns) — never build the state object by hand
  • always use useDataSource to process data — the grid does not sort/filter/paginate on its own
  • columns must be stable across renders (define outside component or wrap in useMemo)
  • renderCell must be a pure function — no React hooks inside it
  • read the full JSDoc on the DataGrid component for iron rules and advanced usage

4.13 CursorBlastEffect

File: apps/dashboard/src/components/design-components/cursor-blast-effect.tsx

Use for:

  • optional high-feedback interactions (playground/internal prototyping)

Props:

  • blastLifetimeMs
  • maxActiveBlasts
  • rageClickThreshold
  • rageClickWindowMs
  • rageClickRadiusPx
  • containerRef

Rules:

  • keep as optional enhancement, not required UX
  • avoid distracting overuse in production-critical flows

4.14 DesignDialog

File: packages/dashboard-ui-components/src/components/dialog.tsx (re-exported through @/components/design-components)

Use for:

  • any focus-trapping modal in the dashboard (confirmations, rich detail dialogs, tester surfaces, settings forms presented as modals)
  • replacing hand-wired Dialog + DialogContent + DialogHeader combinations from @stackframe/stack-ui

Props you should use most:

  • trigger: element wrapped in a DialogTrigger. Skip when controlling externally via open/onOpenChange/defaultOpen.
  • size: "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl" | "5xl" | "6xl" | "7xl" | "full" (defaults to "lg").
  • variant: "glassmorphic" (default) or "plain". glassmorphic applies the dashboard's blurred surface + dimmed overlay.
  • icon: a Phosphor icon component (or null to skip the chip).
  • title / description: standard header text. title is automatically wired into DialogTitle for a11y.
  • headerContent: rich content rendered below the icon/title block — use this for embedded summary cards, metric tiles, or status pills inside the header.
  • customHeader: complete override of the header region. You become responsible for rendering an accessible DialogTitle.
  • footer: footer node rendered in a styled bottom bar. Wrap close buttons in DesignDialogClose asChild (re-exported by the same module).
  • noBodyPadding: disable the default px-6 py-4 padding for full-bleed body content.
  • hideTopCloseButton: hide the top-right "X" rendered by DialogContent for fully custom close affordances.
  • bodyClassName / headerClassName / footerClassName / overlayClassName / className: fine-grained class overrides for each region.

Re-exports (import these from the same module — do not mix with @stackframe/stack-ui for the same dialog):

  • DesignDialogClose (alias of DialogClose)
  • DesignDialogTrigger (alias of DialogTrigger)
  • DesignDialogTitle / DesignDialogDescription
  • DesignDialogRoot (alias of Dialog) for the rare cases that need raw Dialog.Root semantics

Rules:

  • Do not write Dialog + DialogContent + DialogHeader + DialogBody + DialogFooter directly when building a full-page modal — use DesignDialog.
  • When refactoring an existing dialog, preserve any headerContent summary cards, sparklines, or stat tiles by passing them via the headerContent prop rather than crafting a customHeader from scratch.
  • Footer slot already provides border + background + responsive flex; don't re-add wrapping divs with the same styling.
  • Use the smallest size that fits your content. md/lg for confirmations, 2xl/3xl for detail dialogs with summary cards, 5xl+ for tester/forms.

Common shapes (for AI agents):

// Confirmation
<DesignDialog
  trigger={<DesignButton size="sm">Open</DesignButton>}
  icon={InfoIcon}
  title="Heads up"
  description="You're about to do something."
  footer={
    <DesignDialogClose asChild>
      <DesignButton variant="secondary" size="sm">Close</DesignButton>
    </DesignDialogClose>
  }
>
  <p className="text-sm">Body content.</p>
</DesignDialog>

// Rich modal with summary card
<DesignDialog
  open={open}
  onOpenChange={setOpen}
  size="2xl"
  icon={PulseIcon}
  title="Rule trigger history"
  description="3 total triggers"
  headerContent={<SummaryCard />}
  footer={
    <DesignDialogClose asChild>
      <DesignButton variant="secondary" size="sm">Close</DesignButton>
    </DesignDialogClose>
  }
>
  {/* recent triggers list */}
</DesignDialog>

// Wide tester / form
<DesignDialog
  trigger={<DesignButton size="sm">Open tester</DesignButton>}
  size="5xl"
  icon={FlaskIcon}
  title="Test sign-up rules"
  description="Simulate a sign-up request."
  footer={
    <>
      <DesignDialogClose asChild>
        <DesignButton variant="secondary" size="sm">Cancel</DesignButton>
      </DesignDialogClose>
      <DesignButton size="sm">Run test</DesignButton>
    </>
  }
>
  <TesterForm />
</DesignDialog>

5) Route-Specific Guidance (Project + Email Surfaces)

Reference surfaces:

  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-drafts
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-outbox
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-templates
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-themes

Current pattern in these pages often uses custom card/header/pill components. New and refactored code should standardize to design-components primitives as follows.

5.0 /projects/[projectId]/(overview) analytics surfaces

Use:

  • chart/list shells: DesignAnalyticsCard
  • compact chart headers: DesignAnalyticsCardHeader
  • stacked chart legends: DesignChartLegend
  • incremental list rendering: useInfiniteListWindow (or DesignInfiniteScrollList where it fits)

Avoid:

  • page-local ChartCard wrappers
  • duplicated IntersectionObserver list window logic per card

5.1 /projects/[projectId]/emails

Use:

  • section containers: DesignCard (title, icon, optional subtitle, gradient)
  • alerts: DesignAlert (variant by state)
  • status chips: DesignBadge (green for sent, red for failed)
  • actions: DesignButton
  • table: DataGrid + useDataSource + createDefaultDataGridState

Avoid:

  • custom GlassCard
  • custom status-badge component
  • raw Alert unless special composition is required

5.2 /projects/[projectId]/email-drafts (list)

Use:

  • list container: DesignCard
  • row items: DesignListItemRow (size="lg" for card rows or size="sm" for dense list)
  • row menus: DesignMenu with variant="actions"
  • empty state action: DesignButton

Avoid:

  • custom DraftCard for standard list row behavior

5.3 /projects/[projectId]/email-drafts/[draftId] (editor)

Use:

  • status/sync alerts: DesignAlert
  • scope/status chips: DesignBadge
  • editor side controls: DesignButton, DesignSelectorDropdown, DesignInput as needed

Keep:

  • specialized editor layout systems if no design-components equivalent exists

5.4 /projects/[projectId]/email-outbox

Use:

  • section cards: DesignCard (preferred for visual consistency with other email screens)
  • filters: DesignSelectorDropdown, DesignInput
  • status badges: DesignBadge
  • action buttons/menus: DesignButton, DesignMenu
  • data grid/list table: DataGrid + useDataSource + createDefaultDataGridState

Avoid:

  • mixed badge systems (Badge in some places, custom badges elsewhere)

5.5 /projects/[projectId]/email-templates

Use:

  • template item containers: DesignCard (gradient per semantic section)
  • alerts/warnings: DesignAlert
  • actions: DesignButton
  • template row action menu: DesignMenu

Avoid:

  • inline repeated glass class blocks for each template card

5.6 /projects/[projectId]/email-templates/[templateId]

Use:

  • save/error notices: DesignAlert
  • top actions: DesignButton
  • state tags: DesignBadge where needed

5.7 /projects/[projectId]/email-themes

Use:

  • section containers: DesignCard
  • viewport/device selector: DesignPillToggle
  • status messages: DesignAlert
  • theme state badges: DesignBadge
  • actions: DesignButton, DesignMenu

Avoid:

  • custom ViewportSelector if DesignPillToggle supports the same behavior

5.8 /projects/[projectId]/email-themes/[themeId]

Use:

  • state feedback: DesignAlert
  • actions: DesignButton
  • optional segmented controls: DesignPillToggle

6) Semantic Mapping Rules

Use consistent semantic color/variant mapping across all pages:

  • Success/completed/sent -> DesignAlert variant="success" and DesignBadge color="green"
  • Error/failed -> DesignAlert variant="error" and DesignBadge color="red"
  • Warning/attention -> DesignAlert variant="warning" and DesignBadge color="orange"
  • Info/neutral updates -> DesignAlert variant="info" and DesignBadge color="blue" or "cyan"

Gradient mapping for cards/tabs/toggles:

  • Blue: primary navigation/state
  • Cyan: analytics/activity
  • Purple: templates/themes or creative tools
  • Green: success/completion
  • Orange: warnings/caution
  • Default: neutral/system sections

7) Interaction and Motion Rules

These rules must be preserved in custom styling and overrides:

  • no hover-enter delays
  • use hover-exit transitions: transition-* duration-150 hover:transition-none
  • keep controls snappy and readable
  • avoid heavy animation in dense admin workflows

For async actions:

  • prefer design-components primitives that already handle async/loading
  • do not swallow async errors; use existing alert-aware async utilities through design-components primitives

8) AI-Readable Implementation Checklist

Use this checklist before opening a dashboard UI PR:

  • Replaced ad-hoc cards with DesignCard where possible.
  • Replaced ad-hoc alerts with DesignAlert.
  • Replaced ad-hoc badges/status pills with DesignBadge.
  • Replaced ad-hoc segmented controls with DesignPillToggle or DesignCategoryTabs.
  • Replaced ad-hoc row/list cards with DesignListItemRow or DesignUserList.
  • Used DesignButton for async actions.
  • Used DesignSelectorDropdown/DesignInput for standard field controls.
  • Used DataGrid + useDataSource + createDefaultDataGridState for interactive tables.
  • Did not introduce duplicate local wrappers for components already in design-components.
  • Kept hover/motion behavior aligned with this guide.

9) Quick Snippets (Canonical)

Section Card

<DesignCard
  title="Email Log"
  subtitle="View and manage email sending history"
  icon={Envelope}
  gradient="default"
>
  {/* content */}
</DesignCard>

Semantic Alert

<DesignAlert
  variant="error"
  title="Failed to send email"
  description="Please verify provider configuration and try again."
/>

Status Badge

<DesignBadge
  label="Sent"
  color="green"
  icon={CheckCircle}
  size="sm"
/>

Viewport Toggle

<DesignPillToggle
  options={[
    { id: "desktop", label: "Desktop", icon: Desktop },
    { id: "tablet", label: "Tablet", icon: DeviceTablet },
    { id: "mobile", label: "Mobile", icon: DeviceMobile },
  ]}
  selected={viewport}
  onSelect={setViewport}
  size="sm"
  gradient="default"
/>

Category Tabs

<DesignCategoryTabs
  categories={[
    { id: "all", label: "All", count: 42 },
    { id: "failed", label: "Failed", count: 3 },
  ]}
  selectedCategory={category}
  onSelect={setCategory}
  gradient="blue"
/>

10) Anti-Patterns (Do Not Introduce)

  • Creating local GlassCard/ChartCard components instead of DesignCard or DesignAnalyticsCard.
  • Creating local status pills instead of DesignBadge.
  • Creating local segmented/pill selectors instead of DesignPillToggle.
  • Using raw Alert/Button in standard dashboard surfaces where DesignAlert/DesignButton should be used.
  • Using DesignDataTable or raw DataTable instead of DataGrid + useDataSource + createDefaultDataGridState. DesignDataTable is deprecated; all new and migrated tables use DataGrid.
  • Repeating large inline class strings for common design-components patterns.

11) Migration Priority for Existing Email Surfaces

When touching existing email/project pages, migrate in this order:

  1. Cards/surfaces (DesignCard / DesignAnalyticsCard for chart-heavy shells)
  2. Alerts (DesignAlert)
  3. Badges (DesignBadge)
  4. Toggles/tabs (DesignPillToggle / DesignCategoryTabs)
  5. Rows/lists (DesignListItemRow)
  6. Buttons/menus (DesignButton / DesignMenu)
  7. Tables/forms (DataGrid, DesignInput, DesignSelectorDropdown, DesignEditableGrid)

This order yields the biggest consistency win first.


12) Maintenance Rule

Whenever a new reusable visual pattern is introduced in dashboard features:

  • add or extend a design-components component first
  • then document the component contract and preferred usage here
  • avoid introducing permanent page-local UI primitives that duplicate design-components behavior