0. Design Principles

The memax design philosophy. Read this first — every token, component, and pattern decision flows from these 8 rules.

LLM Agent Reference — structured rules for code generation

If you are an AI agent generating memax UI code, these are the hard rules. Each rule maps to a section. Violating any rule produces non-memax output.

MEMAX DESIGN SYSTEM — HARD RULES FOR CODE GENERATION
═══════════════════════════════════════════════════════

COLORS (§13)
  text:       text-fg-1 (titles, body) | text-fg-2 (secondary) | text-fg-3 (meta) | text-fg-4 (decorative)
  surfaces:   bg-surface-1 (subtle) | bg-surface-2 (hover) | bg-surface-3 (active)
  accent:     var(--signature) — ONLY for AI indicators (✦) AND Intelligence tab controls, never decorative
  NEVER:      text-gray-*, bg-gray-*, hardcoded colors, text-foreground/XX

CONTROL COLOR SEMANTICS (§19)
  rule:       purple = Intelligence tab ONLY. Everywhere else uses NEUTRAL_INK.
  NEUTRAL_INK:       var(--fg-1) — active toggles, radio rings, radio dots
  NEUTRAL_INK_INVERSE: var(--background) — toggle thumb when filled
  off states:        NEUTRAL_TRACK_OFF / NEUTRAL_BORDER_OFF / NEUTRAL_THUMB_OFF
  import from:       @memaxlabs/ui/tokens/controls
  toggle:     iOS-solid. Track fills with active color, thumb inverts.
  radio:      border-2 ring + NEUTRAL_INK dot. NEVER fill the outer ring.
  pills vs radio rows: pills = self-explanatory labels (roles, tiers). radio rows = needs descriptions (Plain/Signature/Time).
  See §19 "Control color semantics" card for the canonical rule block.

TYPOGRAPHY (§12)
  H1:         text-[21px] font-bold                    (one per view, memory detail title)
  H2:         text-[16px] font-bold                    (section headers)
  H3:         text-[14px] font-semibold                (card titles)
  body:       text-[14px] text-fg-1                    (default)
  secondary:  text-[13px] text-fg-2 or text-fg-3       (descriptions, meta)
  caption:    text-[12px] text-fg-3                    (timestamps, kind)
  micro:      text-[10px] text-fg-4 uppercase tracking-wider font-semibold (group headers)
  weights:    400-700 only. NEVER font-light (300) or font-black (900)
  prose:      leading-[1.65] for AI answers and readable paragraphs

SURFACES (§14)
  card grid:  <Surface variant="default">     (bar-border + bar-shadow)
  detail:     <Surface variant="subtle">      (border only)
  mobile:     <Surface variant="borderless">  (full bleed)
  minimal:    <Surface variant="clean">       (bg only)
  note:       Surface's 'rounded' prop ("xl"|"2xl"|"lg") is a component API
              that resolves to the unified tokens — "xl"/"2xl" resolve to
              rounded-surface (20px); "lg" is legacy — don't use it. Surface
              defaults to rounded-surface when the prop is omitted.
  radius:     TWO tiers only — rounded-surface (20px) | rounded-chrome (14px)
              surface: cards, dialogs, popovers, bar, form sub-cards, containers
              chrome:  buttons, inputs, chips, role tags, code pills, any interactive
                       element ≤40px tall (even role tags become pill-ish at that size)
              source:  packages/ui/src/surface-radius.css (root-scoped, theme-safe)
              NEVER use rounded-lg/md/xl/2xl/3xl on product UI — those are stale.
              Exceptions:
                rounded-full  — true circles (avatars, status dots) OR status pills
                                ≤24px tall where the half-height clamps to the same
                                curve as rounded-chrome anyway
                rounded-sm    — 2px focus outlines on inline text links only

LAYOUT (§20)
  page:       max-w-4xl mx-auto px-5 sm:px-8 pb-36 md:pb-32
  top:        paddingTop: CONTENT_TOP (80px desktop)
  entrance:   animate-content-ready (on every page load)
  bar:        fixed bottom center, z-bar, max-w-[640px]
  modal:      z-modal (backdrop + content — DOM order stacks)

Z-INDEX SCALE — semantic tokens only (§20)
  NEVER use hardcoded z-50, z-[60], etc. Pick from this scale:
  z-page         (0)   regular page content
  z-bar-notif    (30)  bar notification (below bar)
  z-bar          (40)  command bar, mobile dock, top-right chrome row
  z-topic-tree   (50)  Topic Explorer floating panel + mobile fullscreen
  z-modal        (60)  Settings panel, Settings dialog, memory modal,
                       admin drawer, batch-toolbar backdrop
  z-popover      (70)  ALL popovers/dropdowns/menus/InfoPopover (above
                       modal so popovers triggered inside a dialog work)
  z-takeover     (80)  full-screen immersive: lightbox, hub-create,
                       mobile compose, surface-transition-overlay
  z-toast        (90)  top-most ephemeral feedback, impersonation-bar

  Invariant: popover > modal > topic-tree > bar > bar-notif > page.
  Invariant: takeover > popover (full-screen covers dropdowns).
  Tokens are defined as CSS vars (--z-*) in :root + @utility blocks
  in globals.css. Do NOT invent new tiers unless the token scale
  genuinely can't express the layer — add a tier with a rationale.

GLASS + BACKDROP-BLUR — unified surface material (§14)
  ALL translucent floating surfaces use ONE of three sibling classes,
  each paired with backdrop-blur-sm (8px) so the standard
  backdrop-filter property emits (Lightning CSS strips the standard
  form from raw custom-CSS declarations in some cases).
  .glass-bar       command bar                           → paired in layout.tsx
  .glass-panel     Topic Explorer tree panel             → paired in topic-tree-panel.tsx
  .glass-dropdown  ALL popovers (via PopoverContent)     → paired in packages/ui/src/components/popover.tsx
  Recipe: 65% fill + saturate(180%) contrast(1.05) + rim inset + ambient shadow.
  NEVER hand-roll a fourth glass variant. If a new surface needs glass,
  reuse one of the three. The popover primitive OWNS the material for
  every dropdown — consumers pass NO variant/override.
  Popovers: PopoverContent is always glass — no variant prop.
  Menu rows: <MenuItem> from @memaxlabs/ui — never hand-roll a button
  with px/py/hover/radius classes for menu items.

MOTION (§18)
  easing:     var(--ease-spring) — cubic-bezier(0.16, 1, 0.3, 1)
  NEVER:      linear, ease-in-out, ease
  fast:       0.15s (hover, modal, dropdown)
  normal:     0.2s (content transitions)
  AI breathe: state-slow-breathe (2.5s loop)
  entrance:   animate-content-ready (0.15s opacity + translateY)

MOBILE MOTION — HARD RULES (§38e3 mobile lifecycle)
  ░░░ THE SINGLE-BLUR INVARIANT ░░░
  At most ONE backdrop-filter: blur() element composited at a time on mobile.
  Stacking (scrim + glass edge + bar) is the #1 frame-drop cause on mobile GPUs.
  Never blur over a solid background — if bg-card / bg-background is opaque,
  backdrop-filter is INVISIBLE but still costs a full compositing pass per
  frame. Check --card / --background opacity BEFORE adding blur.

  CROSS-TAB DOCK SWITCH (mobile)
  Instant. No fade, no surface-transition overlay, no color-interpolation
  on tab buttons. router.push() only. Desktop keeps subtle fade-in; mobile
  snaps. NEVER call requestSurfaceTransitionForNavigation on mobile tabs.

  MEMORY DETAIL NAVIGATION
  Entry:  opacity fade via template's animate-fade-in (150ms). No slide-in.
  Exit:   router.back() INSTANT. No setTimeout delay, no slide-out-right.
  NEVER:  animate-slide-in-right / animate-slide-out-right on mobile routes.

  TOPIC DRILL (kitchen 29n spec)
  Duration:  FAST (150ms) on mobile, NORMAL (200ms) on desktop.
  Translate: ±16px on x-axis.
  Easing:    var(--ease-spring).
  Parallel:  AnimatePresence WITHOUT mode="wait". Serial mode doubles
             perceived duration (exit 200ms + enter 200ms = 400ms).

  MOBILE COMPOSE SHEET (kitchen 38e3)
  Surface:   ONE solid var(--background) sheet. NEVER translucent + blur.
             The compose state TAKES OVER, it doesn't LAYER on top.
  Motion:    opacity fade only (FAST 150ms). NO y-slide, NO delayed content
             fade, NO framer layout animations.
  Gesture:   drag-to-dismiss is user-initiated — keep it. Rubber-band
             resist 0.55, dismiss >96px OR velocity >720px/s, settle 0.25s.

  BAR ON MOBILE
  - No 100ms setTimeout stagger before barVisible flips.
  - No CSS transition on outer positioning div (top / transform constants).
  - No framer opacity+y fade-in on mount/tab-switch.
  - Position: calc(100dvh - 96px - var(--safe-bottom, 0px)) above dock.
  - Rest dock has zero backdrop-filter (see single-blur invariant).

  CHROME ROW (logo + hub chip + avatar)
  - No transition-all on mobile — tab switches must not animate bg/border.
  - Always transparent on mobile; banner-mode tint transition is desktop-only.
  - BrandLogo: h-5 w-24 (mobile) / h-6 w-30 (desktop).

  MEMORY ROW COMPACT (mobile recent surface)
  - showSummary = false (no description preview).
  - useStackedRecent = false (single-line layout).
  - showCopy = false (saves horizontal space).
  - leadingIdentity = "none" (title leads the scan).
  - trailingActor = author avatar | agent icon | none.
  - showTrailingContentMeta = true for pdf/image/link (NOT for docs/notes).
  - Flag: isMobileCompactRecent in memory-row-presentation.ts.

  POPOVERS / DROPDOWNS
  - No backdrop-filter over solid bg-card. Remove it — the card is opaque.
  - data-open:duration-100 (not default 150) for snap-open feel.
  - For mobile SettingsPanel-style: plain conditional render, no framer
    AnimatePresence. Framer overhead eats the budget at sub-150ms durations.

  FRAMER MOTION BUDGET
  - Never nest motion.div with opacity fades 3 levels deep for one appearance.
  - duration < 0.15s → prefer CSS transition or plain render (framer tax).
  - AnimatePresence mode="wait" → NEVER on routes; use default parallel.

CONTROLS (§19)
  buttons:    <Button variant="default|outline|secondary|ghost|destructive">
  sizes:      xs (h-6) | sm (h-7) | default (h-8) | lg (h-9)
  icon:       icon-xs (24px) | icon-sm (28px) | icon (32px) | icon-lg (36px)
  send btn:   h-8 w-8 rounded-lg. Push=foreground fill, Recall=signature fill
  toggle:     w-10 h-6 rounded-full. See CONTROL COLOR SEMANTICS above for on/off colors.

ACCESSIBILITY (§27)
  fg-1:       12.2:1 — safe everywhere
  fg-2:       5.2:1  — safe everywhere
  fg-3:       2.5:1  — meta/labels only, FAILS AA normal text in light mode
  fg-4:       1.5:1  — decorative only, NEVER readable text
  focus:      focus-visible:ring-3 focus-visible:ring-ring/50
  touch:      44px primary, 32px secondary, 24px minimum
8 principles
01
Opacity, not colors

All text and surface colors derive from --foreground via opacity. Never hardcode color values in components.

Why: One variable change updates the entire palette. Dark mode, themes, and accessibility come free.

text-fg-2 (65% opacity) instead of text-gray-500
02
Container morphing

The same container adapts to different states. Never overlay when the surface can transform.

Why: Overlays break spatial continuity. Users lose context when content jumps between surfaces.

Memory card morphs into detail view — no modal popup, no new page.
03
Signature = intelligence

Dream Violet (--signature) appears only where memax AI is actively working. Never decorative.

Why: If signature color is everywhere, it means nothing. Scarcity creates meaning.

✦ star breathes during AI streaming, static when complete. Never on borders or backgrounds.
04
No sidebar, no top bar

Floating bottom bar, centered content. Tree navigation is peek-on-demand, not always visible.

Why: Maximum content density. The bar is the only persistent UI element.

Bar: fixed bottom center (z-50). Content: scrolls freely with max-w-4xl.
05
Spring easing always

Use var(--ease-spring) for all transitions. Never linear, never ease-in-out.

Why: Spring easing (fast start, gentle settle) creates the memax feel — responsive, alive, never sluggish.

cubic-bezier(0.16, 1, 0.3, 1) for all UI motion.
06
Shape before content

Skeletons mirror the final layout exactly. Loading states show shape, not spinners.

Why: The brain anchors to layout. When content loads into the same shape, it feels instant.

Shimmer skeleton with dot+title+body matches loaded MemoryRow.
07
Spacing creates hierarchy

Use spacing and opacity to separate sections. Never add visible dividers or accent borders.

Why: Dividers add visual noise. Whitespace is a stronger (and quieter) grouping signal.

divide-border/20 (barely visible) or gap spacing, never border-l-4 colored accent.
08
Mobile-first, keyboard-ready

Default styles are mobile. Add sm:/md:/lg: for larger screens. Never block keyboard handlers behind isMobile.

Why: Touch targets and keyboard shortcuts coexist. isMobile is for visual hints only, never for disabling functionality.

Mobile: pill chips (44px touch). Desktop: text toggle. Both work with keyboard.
09
Single blur layer on mobile

At most ONE backdrop-filter element composited at a time on mobile. Never stack scrim + edge + bar. Never blur a solid bg.

Why: Mobile GPUs choke on stacked backdrop-filter. Invisible blur (over opaque bg-card) still costs a full compositing pass — pure waste.

Mobile compose = solid var(--background) sheet, zero backdrop-filter. Popover over bg-card = no blur.
10
Mobile navigation is instant

Cross-tab dock swap, memory detail back, and in-app route changes run with ZERO fade or slide on mobile. Entrance animations are opacity-only when present.

Why: Any motion on cross-tab navigation reads as lag. Apple Notes, Linear, Instagram all snap-switch tabs. Motion is reserved for the compose/drill flows that need it (kitchen 29n topic drill only).

mobile-dock onClick → router.push(). No requestSurfaceTransitionForNavigation. No color transition on tab button. router.back() has no setTimeout.
The memax feel
Quiet

No visual noise. Opacity hierarchy instead of color variety. Whitespace instead of dividers. The content breathes.

Alive

Spring easing on everything. ✦ breathes when AI works. Containers morph instead of swapping. Nothing feels static or dead.

Confident

One accent color, used sparingly. No gradients, no decorative elements. The product gets out of the way. Content is the hero.

Anti-patterns — never do this
BadGoodWhy
text-gray-500text-fg-3Gray doesn't adapt to dark mode or themes
ease-in-outvar(--ease-spring)Feels sluggish and generic
border-l-4 border-blue-500Surface variant + shadow liftColored accent lines are not memax
bg-gray-100bg-surface-1Surface tokens adapt to any theme
shadow-mdvar(--bar-shadow)Bar shadow is a composed, branded shadow
rounded-md (6px) / rounded-lg (8px) on cards or buttonsrounded-surface (20px) on cards/dialogs; rounded-chrome (14px) on buttons/inputs/chipsTwo-tier system from surface-radius.css: --app-radius-surface (20px) for large containers, --app-radius-chrome (14px) for interactive chrome ≤40px. Everything else is stale.
Modal overlay for editingInline transform / morphContainer morphing principle
Spinner for loadingSkeleton shimmerShape before content principle
text-foreground/40text-fg-3Use semantic tokens, not raw opacity
font-light (300)font-normal (400) minimumProduct uses 400-700 weight range only
backdrop-filter: blur() over bg-cardno blur — bg-card is solid, blur is invisibleFull-viewport GPU compositing pass per frame for zero visual benefit
Stacked blur layers on mobile (scrim + edge + bar)One blur surface OR solid sheetMobile GPUs drop frames on multi-backdrop-filter compositing
AnimatePresence mode="wait" on routesDefault parallel AnimatePresenceSerial mode doubles perceived duration (exit + enter)
animate-slide-in-right / out-right on mobile routesCSS fade-in + instant router.back()Slide wrappers add 250-350ms lag to every mobile route swap
setTimeout(() => router.back(), 250)router.back() instantUsers perceive the delay as lag, not animation polish
Multiple motion.div nested for one fadeSingle motion.div OR plain renderFramer overhead eats the budget at sub-150ms durations
requestSurfaceTransitionForNavigation on mobilerouter.push() onlyOverlay + content scale/translate is desktop polish; mobile snaps
Translucent scrim over page for mobile composeSolid var(--background) sheet (container takeover)Kitchen 38e3 — compose TAKES OVER, doesn't LAYER. Apple Notes pattern
Quick reference — where to find what

Foundations

Colors → Section 13

Typography → Section 12

Surfaces → Section 14

Spacing → Section 20

Motion → Section 18

Branding → Section 11

Components

Buttons/Badges → Section 19

Indicators → Section 17

State Machine → Section 16

Loading → Section 08

Empty States → Section 09

File map — source of truth
globals.css— all CSS custom properties (tokens)
ui/button.tsx— Button (CVA variants + sizes)
ui/badge.tsx— Badge (CVA variants)
ui/surface.tsx— Surface (5 container variants)
ui/pill.tsx— Pill (select / remove / add / static)
ui/skeleton.tsx— Skeleton (loading placeholder)
ui/memax-loader.tsx— MemaxLoader (signature dots)
lib/motion.ts— JS timing constants
lib/kind.ts— Kind dot colors

11. Branding

Logo wordmark, icon at multiple sizes, and loader variants.

Wordmark (icon + text)

Light

Dark

Top-bar wordmark — size, opacity, alignment (mobile + desktop)

The wordmark is the stable brand anchor. Next to the right-side hub capsule it was reading like a watermark — too small, too muted, hard to notice. The fix is size + opacity, not color. The ink stays neutral var(--foreground) so signature (violet) can keep its job as accent for hub avatars / dreams / active states without the wordmark stealing that vocabulary.

Before → after (rendered against the real top-bar right capsule)

小宝记忆空间

✗ current — h-5 (20px mobile) / h-6 (24px desktop), opacity 0.45, neutral foreground. Reads as a faded watermark next to a 48px-tall capsule. Weight ratio ≈ 1:2.

小宝记忆空间

✓ proposed — h-6 (24px, same mobile + desktop), opacity 0.9, var(--foreground). Bumping opacity 0.45 → 0.9 does the heavy lifting (2× readable weight); the mobile h-5 → h-6 nudge is a small follow-up so mobile matches desktop. Container h-12 so the vertical center lines up with the capsule.

Why neutral, not signature

Signature (violet today) is memax's accent language: hub avatar fill, dreams state, active tab indicator, recall star. Painting the wordmark with signature too would flatten that hierarchy — everything in the frame would be “the violet thing” and the accent stops carrying meaning.

Linear, Notion, Vercel, Apple all keep their wordmarks neutral for exactly this reason. The brand anchor stays stable; accents move around it.

var(--foreground) is already theme-aware at the only layer that matters: lightness. Near-black in light mode, near-white in dark, with a subtle palette-specific gray tint (see the grays table in _kitchen-context.tsx — each palette adds ~0.005 chroma so the neutral drifts warm / cool / earth / dream without ever becoming chromatic). That is the theme-awareness the wordmark needs — no hue swap required.

Wordmark vs hub ambience — two different layers

The top-bar memax wordmark is global product brand. The hub header behind it (aurora / signature fill / time-of-day drift) is local hub ambience. They are two different layers and neither should recolor the other.

Personal hubs can drift through time-based aurora, team hubs can stay on signature, dreams mode can pulse purple — none of that touches the wordmark. The logo stays stable while the header mood changes behind it.

Rule: if product branding changes, the wordmark can change. If hub ambience changes, the wordmark stays put.

Size — why h-6 (24px), not h-7 or h-5

h-5(20px, original mobile) was a genuine “too small” problem — at opacity 0.45 it read as a watermark, not a brand.

h-7 (28px) is too wide on mobile. SVG wordmark aspect ratio is 4.65:1, so h-7 → w-33 (132px). On a 375px viewport that's 35% of screen width competing with the capsule at the right edge — the top bar feels overstuffed.

h-6(24px, w-30 = 120px) is the sweet spot. On mobile it's 32% of a 375px screen instead of 35%, still leaves breathing room for the right capsule. On desktop it matches the current production value, so we ship one size for both — no md: breakpoint split. The wordmark is the stable brand anchor; making it different between breakpoints is inconsistency for no reason.

The real “feels present” fix isn't size anyway — it's opacity 0.45 → 0.9. That alone doubles the perceived weight; the size nudge is just making mobile match desktop.

Alignment — both containers at h-12, same top

Today the wordmark container is h-11 (44px) and the right capsule derives a ~46px height from its flex children. Close but not identical — and near-misses at 1–2px read as misalignment. Fix: give both containers flex h-12 items-center so they lock to the same vertical center at the same top value.

Implementation spec — use the @memaxlabs/ui component

// packages/web/src/app/(app)/layout.tsx
import { MemaxWordmark } from "@memaxlabs/ui";

function BrandLogo({ isMobile }: { isMobile: boolean }) {
  return (
    <div
      className="fixed z-50 flex h-12 items-center"   // was h-11
      style={{
        left: isMobile ? 16 : 32,
        top: isMobile
          ? "max(20px, calc(8px + var(--safe-top, 0px)))"
          : "32px",
      }}
    >
      <a
        href="/home"
        className="cursor-pointer text-foreground opacity-90 transition-opacity hover:opacity-100"
      >
        <MemaxWordmark height={24} />
      </a>
    </div>
  );
}

// Right-capsule container on line ~143 — lock height to match:
<div
  className="fixed right-4 md:right-8 z-50 flex h-12 items-center gap-2 ..."
  //                                             ^^^^^ add h-12
  ...
>

Switches from the mask-based /images/memax-wordmark.svg URL to the MemaxWordmark React component exported from @memaxlabs/ui. The component uses fill="currentColor", so text-foreground opacity-90 on the parent controls both ink and strength. No static asset to maintain, no CSS mask plumbing, and the kitchen demo above renders the exact same primitive production will use.

Four edits total: container h-11 → h-12, wordmark rendering swap to <MemaxWordmark />, text-foreground opacity-90 (replaces the hardcoded 0.45 foreground mask), and h-12 added to the right capsule so both sides share one vertical anchor.

Wordmark over aura — legibility rule (industry best practice)

Hub headers drift through aurora gradients (time-of-day, signature, dreams). When an aura sits behind the wordmark, the fixed var(--foreground) ink can fall into the gradient and become hard to read. This card answers the question: should the wordmark auto-adapt to the aura behind it?

The failure — dark ink on a darker aura

Light-theme foreground (near-black) at 0.9 on a saturated deep violet aura — legibility drops off a cliff. Dark-theme foreground (near-white) on a light peach aura has the same problem in reverse. We need a mechanism that doesn't require the wordmark to care what's behind it.

Four approaches the industry uses

✓ A. Scrim backdrop (Apple, Notion, Vercel, macOS Control Center)

The nav / logo sits on its own translucent glass backdrop (backdrop-blur-md + bg-background/52), which absorbs whatever is behind it. The wordmark itself never changes color — legibility comes from the scrim, not the ink. Brand identity is preserved (memax is always foreground-neutral), and the scrim is visually consistent regardless of aura mood. Industry default.

⚠️ B. mix-blend-mode: difference / luminosity (editorial, video heroes)

One CSS line auto-inverts the wordmark against whatever is behind it. Works well over grayscale or muted backgrounds. Against vivid gradients it goes psychedelic — the wordmark turns green/orange/cyan depending on which gradient stop is under each letter. Also kills brand intent: you lose “memax is this color”, you get “memax is whatever difference-blend says”. Wrong for a product brand that ships a distinctive signature feature elsewhere.

⚠️ C. IntersectionObserver + section-aware class swap (Linear docs, Stripe)

Each section declares its theme="light" or theme="dark", a scroll observer watches which section intersects the nav, and the wordmark container swaps a class. Full control, brand-preserving. But: stateful, JS-coupled, easy to break on dynamic content (e.g. dream aurora that drifts during a single session view). Overkill for a top-bar wordmark that lives above one kind of container.

❌ D. Auto-sample background color at runtime

Canvas-sample the pixels behind the wordmark, compute luminance, flip ink. Never do this. Expensive, flaky on animated backgrounds, impossible to SSR, impossible to snapshot test. Listed only so we name it and reject it.

Recommended — extend showBannerChrome to the wordmark side

memax already has the scrim mechanism for the right capsule: showBannerChrome is the boolean state that switches the right container between glass chrome and transparent. We apply the same treatment to the wordmark container — one source of truth, zero new state, both sides light up together when the banner is visible.

小宝记忆空间

showBannerChrome = false — plain background, both sides transparent. Wordmark reads directly against var(--background).

小宝记忆空间

showBannerChrome = true — both sides wear the same glass scrim (bg-background/52 + backdrop-blur-md + ring-1 ring-black/5). Wordmark ink stays neutral; legibility comes from the scrim. Brand anchor never wiggles.

Why scrim beats auto-adaptive color

Legibility without identity loss. The wordmark never changes color, so the brand stays recognizable. Users learn “memax looks like this” once and it holds in every surface.

Stateless. No canvas sampling, no scroll listeners, no viewport math. CSS alone. SSR-friendly, snapshot-testable, cacheable.

Symmetry. The right-side hub capsule already flips on showBannerChrome. Making the wordmark do the same thing ties the two sides into one visual beat instead of two independent rules.

Future-proof for dreams mode. If dreams ever ships an aurora that saturates more deeply, the scrim absorbs it automatically — the darker the aura, the more the scrim carries. No per-aura color tuning.

Implementation — wordmark container mirrors the right capsule

// packages/web/src/app/(app)/layout.tsx

function BrandLogo({
  isMobile,
  showBannerChrome,              // NEW — accept the same flag the right capsule uses
}: {
  isMobile: boolean;
  showBannerChrome: boolean;
}) {
  return (
    <div
      className={`fixed z-50 flex h-12 items-center rounded-2xl px-3 py-1.5 transition-all ${
        showBannerChrome
          ? "border border-white/10 bg-background/52 backdrop-blur-md ring-1 ring-black/5"
          : "border border-transparent bg-transparent backdrop-blur-0 ring-0"
      }`}
      style={{
        left: isMobile ? 16 : 32,
        top: isMobile
          ? "max(20px, calc(8px + var(--safe-top, 0px)))"
          : "32px",
      }}
    >
      <a
        href="/home"
        className="cursor-pointer text-foreground opacity-90 transition-opacity hover:opacity-100"
      >
        <MemaxWordmark height={24} />
      </a>
    </div>
  );
}

// Layout already computes showBannerChrome for the right capsule — pass the
// same value into <BrandLogo /> so both sides light up together.

Single state, two consumers. The wordmark container now wears the identical glass chrome as the right capsule, gated on the same showBannerChrome flag. When the banner is absent both sides are invisible chrome; when it is present both sides wear a matching scrim. The wordmark ink (text-foreground opacity-90) never changes.

Icon only

48px

32px (favicon)

20px (nav)

48px dark

MemaxLoader

inline

compact

Loading...

with label

3 dots in signature color, sequential pulse. Pure CSS — no requestAnimationFrame.

13. Color Tokens

Semantic color system built on oklch opacity architecture. All derived from --foreground, adapts to any theme or dark mode automatically.

Architecture — how colors are computed
--fg-1: oklch(from var(--foreground) l c h / var(--op-primary));
--surface-1: oklch(from var(--foreground) l c h / var(--op-bg-subtle));

All semantic colors are computed from --foreground with opacity multipliers. Change one variable, entire palette adapts. No hardcoded color values in components.

Foreground scale — text colors
Aa
Primarytext-fg-1

Titles, body, input text

/90
Aa
Secondarytext-fg-2

Descriptions, placeholders, secondary labels

/65
Aa
Tertiarytext-fg-3

Timestamps, meta, muted labels

/40
Aa
Decorativetext-fg-4

Hints, annotations, divider text

/20
Surface scale — background fills
Subtlebg-surface-1

Slight tint, chip background, hover hint

/3
Hoverbg-surface-2

Hover state, selected row, active tab

/5
Activebg-surface-3

Active/pressed state, strong selection

/8

Stacking demo

surface-1 (subtle tint)
surface-2 (hover)
surface-3 (active)
Theme primitives — light / dark values
TokenLightDarkUsage
--background
#fafafa
oklch(0.155 0.01 260)Page background
--foreground
#1c1c1e
oklch(0.985 0 0)Primary text base
--card
#ffffff
oklch(0.2 0.008 260)Card, bar, modal surfaces
--border
#e5e5e5
oklch(0.275 0 0)Card borders, separators
--muted
#f2f2f2
oklch(0.269 0 0)Summary callouts, subtle bg
--destructive
#ff3b30
oklch(0.704 0.191 22)Error, delete states
--primary
#000000
oklch(0.922 0 0)Primary button, CTA fill
--secondary
#e8e8e8
oklch(0.269 0 0)Secondary button, modal header
Live swatches (adapts to current theme)

Background

--background

Card

--card

Foreground

--foreground

Border

--border

Muted

--muted

Destructive

--destructive

Primary

--primary

Secondary

--secondary
Signature color — Dream Violet
Dream Violetoklch(0.62 0.16 290)

The memax intelligence marker. Appears only where memax is actively working — never decorative, never structural.

Allowed usage

✦ star indicator (AI intelligence marker)
Loading pulse dots (MemaxLoader)
Active nav dot in tree panel
Toggle switch on-state fill
Drag-drop border hint
Recall mode send button fill

Never use for

Decorative borders or backgrounds

Regular button fills or highlights

Text color for non-AI content

Branding outside the app

--signature-muted

oklch(signature / 0.12) — subtle tint for backgrounds

Bar-specific tokens
--bar-borderBar border color (12% opacity)
--bar-shadowBar shadow (layered: outline + medium + large)
--bar-bgBar background (falls back to --card)

12. Typography

Complete type system. Every text element maps to a CSS variable + Tailwind class. 7-step scale, 3-level heading hierarchy, 4 weights.

Why native sans (and one mono webfont)

Sans is the OS default — SF Pro on macOS/iOS, Segoe UI on Windows, Roboto on Android. No brand typeface, no Geist, no Inter, no Space Grotesk. Geist Mono is the only webfont we ship, for code/kbd/tokens.

Hierarchy is size × weight × tracking, never family swap. A heading is the same typeface as a memory row — just bigger, bolder, tighter. Content stays louder than chrome because the chrome literally disappears into the OS.

font-heading and font-display both alias to font-sans. Semantic intent in markup, native family at runtime. The stack is declared once in globals.css :root — never hardcode font-family.

Scar tissue: 2026-04-14 shipped next/font/google Geist, which injected a literal Geist into every font stack via its src: local() fallback. Designers with Geist locally installed saw their local Geist instead of SF Pro — thin, geometric, mismatched against every native element. Non-devs saw a completely different product. Never import Google web sans fonts into product surfaces again.

Font families
Sans — native systemfont-sans

Remember what your agents learn. Recall it anywhere.

SF Pro on macOS/iOS, Segoe UI on Windows, Roboto on Android, system-ui elsewhere. Every piece of UI text. Zero webfont load.

Heading — native (alias)font-heading

The bar is a search input that happens to save.

normal tracking

The bar is a search input that happens to save.

display tracking

Semantic alias of font-sans. Use it on section headers to signal intent. Same native family, tighter tracking via text-display-* classes.

Mono — Geist Monofont-mono

const memory = await memax.recall(query);

Code snippets, keyboard glyphs, tokens, CLI output. The only webfont we ship — cross-OS mono drift (Menlo vs Consolas vs DejaVu) is too wide to leave to the system.

CJK fallback sanity

小宝记忆空间 remember what your agents learn

小宝记忆空间 remember what your agents learn

Latin glyphs should stay in Geist. CJK falls back to system PingFang/YaHei/Noto without changing hierarchy.

Heading hierarchy (3 levels)
H1

Settings

text-[21px] font-bold21px · 700 (bold)Memory detail title, page header (one per view)
H2

Deployment Strategy

text-[16px] font-bold16px · 700 (bold)Section header, note detail title
H3

React Server Components

text-[14px] font-semibold14px · 600 (semibold)Card title, list item title, sub-section

Rule: one H1 per view. H2 for sections within a page. H3 for cards/list items. Never skip levels.

Type scale — CSS variables
TokenSizeTailwindUsage
--text-title18pxtext-[18px]Modal title, page header
--text-input15→16pxtext-[15px] sm:text-[16px]Bar input, placeholders
--text-body14pxtext-[14px]Card title, body, AI answer
--text-secondary13pxtext-[13px]Snippet, citation, button label
--text-caption12pxtext-[12px]Kind, timestamp, meta
--text-micro11pxtext-[11px]Group header, keyboard hint
--text-nano10pxtext-[10px]Citation badge, section label, DemoCard label
Type scale — live rendering
Memory title here
What did we agree on for v2?
Your deployment strategy combines blue-green with canary releases.
Copy to clipboard
core · 2h ago · 3 sources
RECENT MEMORIES
TYPE SCALE
Weight scale
400Regular

The quick brown fox jumps over the lazy dog

500Medium

The quick brown fox jumps over the lazy dog

600Semibold

The quick brown fox jumps over the lazy dog

700Bold

The quick brown fox jumps over the lazy dog

Rule: never use font-light (300) or font-black (900). Product uses 400–700 range only.

Text color hierarchy
fg-1Primary text — titles, body, input value
fg-2Secondary text — descriptions, placeholders
fg-3Tertiary text — timestamps, meta, labels
fg-4Decorative — hints, divider text, annotations

All computed from --foreground via oklch opacity. Adapts automatically to dark mode. Never use raw foreground/XX — always use text-fg-N classes.

Line height
1.0leading-none
Icons, single-line labels, badges
1.4leading-tight
Card titles, compact text
1.5leading-snug
Bar input (22px line-height on 15px)
1.65leading-[1.65]
AI answers, prose body, readable paragraphs
Composition — memory card
React Server ComponentsH3

Server components render on the server and send HTML. Client components hydrate on the client.

core·2h ago

Title: text-[14px] font-semibold text-fg-1 (H3)

Body: text-[13px] text-fg-2 leading-[1.65] line-clamp-2

Meta: text-[12px] text-fg-3 / text-fg-4

14. Surfaces

5-level elevation hierarchy + 5 Surface variants. Every container in the product uses one of these combinations.

Elevation hierarchy (5 levels)
0
0 — Page

Page canvas, scroll body. No border, no shadow.

Layout root, brain view scroll area
1
1 — Content

Content container. Subtle border (--border), no shadow.

Memory detail body (desktop), settings sections
2
2 — Elevated

Primary cards. Bar-border (12% opacity) + layered shadow.

Memory grid cards, topic cards, bar, floating panels
3
3 — Floating

Above-page overlays. Glass blur + frosted edge.

Dropdowns, popovers, command palette
4
4 — Modal

Full-screen overlay with backdrop dimming.

MemoryModal, settings dialog, confirmations

Rule: never skip elevation levels. Page → Content → Elevated → Floating → Modal.

Surface component — 5 variants (live)
DefaultFull elevation — bar-border + bar-shadow
<Surface variant="default">Memory grid, topic cards, bar
SubtleLight border, no shadow — recedes behind content
<Surface variant="subtle">Memory detail body (desktop), inline containers
FlatSame as subtle (alias) — semantic distinction for different context
<Surface variant="flat">Inline sections, nested containers
BorderlessNo border, no shadow, no rounding — full bleed
<Surface variant="borderless">Mobile detail page, fullscreen views
CleanCard background only, no border — minimal container
<Surface variant="clean">Minimal containers, collapsed sections
Bar surface
What did we agree on for v2?
h-14 · rounded-2xl · bg-card · bar-border · bar-shadow
Floating panel (dropdown / popover)

Item one

Description here

Item two (hover)

Same border/shadow as bar. Separator: border-t border-border/30. Hover: bg-surface-1.

Border radius scale
Surface20pxrounded-surface
Chrome14pxrounded-chrome
Pill9999pxrounded-full

Rule: two tiers only — rounded-surface (20px) wraps interactive rounded-chrome (14px). Source: packages/ui/src/surface-radius.css. Pick by element size — ≤40px is chrome, >40px is surface.

Glass edges (functional only)

Glass communicates layer hierarchy: “this floats above that.” Never decorative.

Frosted edge (scroll top/bottom)

backdrop-blur(20px) saturate(1.4) · mask-image dissolve

Dissolve gradient (infinite canvas)

linear-gradient(to top, var(--card), transparent) · 120px
Decision tree — which Surface?
1.
Is it a card in a grid? → default (border + shadow)
2.
Is it a detail/section body? → subtle (border only)
3.
Is it mobile or fullscreen? → borderless (no border, no rounding)
4.
Is it a minimal wrapper? → clean (bg only)
5.
Is it floating above content? → use bar-border + bar-shadow directly (not Surface)

20. Layout

Page widths, 8px spacing grid, responsive breakpoints, z-index hierarchy, and safe area insets. Every layout decision references this.

Page widths
Bar
max-w-[640px]

Global input bar. Centered, responsive width shrinks on larger screens.

w-[calc(100%-32px)] sm:w-[calc(100%-96px)] md:w-[calc(100%-128px)]
Content (brain view)
max-w-4xl (896px)

Memory grid, topic detail. Outer padding + inner max-width.

px-4 sm:px-8
Memory detail
max-w-4xl (896px)

Same max-w as list — container morphs, no new surface.

px-5 sm:px-8 md:px-10
Settings modal
880px

Centered dialog. Sidebar nav (desktop), tabs (mobile).

sidebar 220px + content px-8
Share target
max-w-sm (384px)

Single card, centered. PWA share sheet capture.

p-5
Width comparison (visual)
Share (384px)43%
Bar (640px)71%
Settings (880px)98%
Content (896px)100%
Spacing grid (4px base, 8px primary)
4gap-1
8gap-2
12gap-3
16gap-4
24gap-6
32gap-8
48gap-12
64gap-16
4pxgap-1Micro gaps (icon-to-icon, badge internal)
8pxgap-2Inline gaps (icon + text, dots)
12pxgap-3Tight element spacing (badge group)
16pxgap-4Card padding, grid gap, section gaps
24pxgap-6Section spacing within a page
32pxgap-8Section margin, page top padding
48pxgap-12Large section breaks, page bottom
64pxgap-16Page-level spacing (rarely used)
Responsive breakpoints
Mobile(default)
0px639px

Single column, pill chips, bottom bar, borderless surfaces

Tabletsm:
640px767px

2-column grid, text mode toggle, larger type

Desktopmd:
768px1023px

3+ column grid, sidebar peek, full bar width

Widelg:
1024px

Pinned sidebar, max content width, settings modal

Rule: mobile-first. Default styles = mobile. Add sm: / md: / lg: for larger screens. Never hide critical functionality behind a breakpoint.

Z-index hierarchy
z-toast
Impersonation bar, future toasts
z-takeover
Lightbox, hub-create, mobile compose, surface transition
z-popover
ALL popovers/dropdowns (above modal so popovers inside dialogs work)
z-modal
Settings panel/dialog, memory modal, admin drawer
z-topic-tree
Topic Explorer floating panel + mobile fullscreen
z-bar
Command bar, mobile dock, top-right chrome row
z-bar-notif
Bar notification + memory sticky header
z-page
Regular page content

Rule: semantic tokens only. NEVER use hardcoded z-50, z-[65], etc. Pick from this list. Invariants: popover > modal (popovers must work inside a Settings dialog) and takeover > popover (full-screen surfaces cover stray popovers). Tokens defined as CSS vars --z-* on :root plus @utility blocks in globals.css.

Safe area insets (CSS env())
--safe-topStatus bar inset (iOS notch)
--safe-bottomHome indicator (iPhone), bar clearance
--safe-leftLandscape notch (left)
--safe-rightLandscape notch (right)
padding-bottom: calc(var(--safe-bottom) + 16px);

Bar uses safe-bottom for iPhone home indicator clearance. Always add safe area to fixed bottom elements.

Composition — page layout
max-w-4xl content
bar (z-bar, fixed bottom)

z-page: content

z-bar: bar

Content scrolls behind bar. Bar is fixed bottom center. No sidebar, no top bar. Bottom padding: safe-bottom + bar height + 16px clearance.

18. Motion Tokens

Named timing constants + easing curves. Every animation references a token — never raw durations. Spring easing is the default.

Duration tokens
INSTANT

0.1s

UI state changes — intent label swap, right slot toggle, hover feedback

opacity: 0 → 1 on hover
FAST

0.15s

Modal open/close, expand slot, view switch, dropdown appear

scale: 0.96 → 1 on mount
NORMAL

0.2s

Content dissolve, list reorder, layout shift, page transition

translateY: 6px → 0 on enter
LOADING

0.5s

Breathing dots, loading pulses, skeleton shimmer

opacity: 0.3 → 1 loop
SIGNATURE

3.5s

Ghost breathing, slow atmospheric loops

breathing ✦ states, idle animations

CSS: var(--duration-*) · JS: import { INSTANT, FAST, NORMAL, LOADING, SIGNATURE }from "@/lib/motion"

Duration comparison — interactive
0.1s
0.15s
0.2s
0.5s
3.5s
Easing curves
Spring
cubic-bezier(0.16, 1, 0.3, 1)

Default for all UI transitions. Fast start, gentle settle. Apple-like feel.

Use for: Everything unless specified otherwise

Bounce
cubic-bezier(0.34, 1.56, 0.64, 1)

Slight overshoot then settle. Playful, attention-drawing.

Use for: Toast enter, success indicator, notification badge

Rule: never use linear or ease-in-out. Spring easing is the memax feel. Set it once in Framer Motion transition or CSS transition-timing-function.

Easing comparison — interactive
Spring (default)
Bounce
Linear (never use)
ease-in-out (never use)

Red = forbidden easings. Notice how linear feels robotic and ease-in-out feels sluggish compared to spring.

Named animations (globals.css keyframes)
animate-content-ready

Skeleton → content transition. Opacity 0→1 + translateY 6px→0.

0.15sAll loading → loaded state changes globally
state-slow-breathe

Gentle opacity pulse 0.3→1. For AI processing indicators.

2.5s infinite✦ star during AI streaming, "organizing..." text
state-fast-pulse

Quick opacity pulse. For active processing feedback.

1s infiniteKind dot during processing, inline loading
animate-fade-up

Entrance: opacity 0→1 + translateY 8px→0.

0.3sSection entrance, staggered list items
MemaxLoader

Sequential pulse dots in signature color. Brand loading.

per-dot staggerFull-page cold start, route transition
Live — breathing indicators
slow-breathe
fast-pulse
static

slow-breathe: AI streaming/processing. fast-pulse: active card processing. static: complete/idle.

Status messages (RecallingText)
memax listening
memax organizing
memax dreaming
memax connecting dots
memax thinking

Crossfade cycle during AI loading. Source: RecallingText component. verb breathes in signature color.

Motion rules
1.

Never use raw durations — always reference a token (CSS var or JS constant)

2.

Spring easing everywhere — never ease-in-out, never linear

3.

Signature color only breathes — never flashes, never blinks

4.

No animation > bad animation — if unsure, use a simple opacity fade

5.

prefers-reduced-motion — respect system setting, disable non-essential motion

27. Accessibility

WCAG contrast ratios (calculated from real tokens), focus management, reduced motion, touch targets, color blindness. Hard rules, not guidelines.

Contrast ratios — WCAG 2.1 (calculated)
TokenOpLight/bgLight/cardDark/bgAAAA LgUsage rule
fg-1
/9012.2:112.7:112.9:1PASSPASSUse for all body text, titles, headings, input values. No restrictions.
fg-2
/655.2:15.3:17.3:1PASSPASSSafe for descriptions, placeholders, secondary labels. Passes AA at all sizes.
fg-3
/402.5:12.5:13.7:1FAILFAILDECORATIVE ONLY in light mode. Timestamps, meta, kind labels. Never for readable body text. Dark mode passes AA large (3.7:1).
fg-4
/201.5:11.5:11.9:1FAILFAILNON-TEXT ONLY. Hints, annotations, divider text, decorative labels. Never for any content a user needs to read.

WCAG 2.1 AA requirements:

Normal text (<18px): minimum 4.5:1 contrast ratio

Large text (≥18px bold or ≥24px): minimum 3.0:1 contrast ratio

Non-text (icons, borders): minimum 3.0:1 contrast ratio

Signature color (Dream Violet) — contrast
Light bg:4.8:1AA large PASS
Dark bg:3.3:1AA large PASS

Signature passes AA large text (3:1) in both modes. Safe for ✦ indicators (typically 12-20px) and button fills (white text on signature bg). Not safe for small body text in light mode.

Token → text size rules (LLM reference)
fg-1

Any size, any weight. No restrictions. (12.2:1+)

fg-2

Any size, any weight. Safe everywhere. (5.2:1+)

fg-3

Minimum 12px. Use for timestamps, meta, labels — never for content users must read. (2.5:1 light, 3.7:1 dark)

fg-4

Decorative only. Annotations, DemoCard labels, divider dots. Never for readable text. (1.5:1+)

Known trade-off: fg-3 fails WCAG AA for normal text in light mode. This is intentional — fg-3 is for supplementary metadata (timestamps, kind labels) where the information is also conveyed by position, grouping, or icon. If the text must be readable standalone, use fg-2.

Focus ring specification
Button (all variants)
focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:border-ring3px ring at 50% opacity of --ring. Border also changes to --ring.
Button (destructive)
focus-visible:border-destructive/40 focus-visible:ring-destructive/20Red-tinted ring for destructive actions.
Input fields
focus:border-ring focus:ring-2 focus:ring-ring/502px ring (smaller than button). Border transitions to --ring.
Toggle switch
Browser default (no custom ring)Relies on native focus indicator. TODO: add visible ring.
Links / text buttons
focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:roundedSubtle 2px ring with rounded corners.

Focus rules:

1. Always use focus-visible, never focus — keyboard users only, not mouse clicks

2. Ring color is --ring at 50% opacity — visible but not aggressive

3. Tab order follows visual layout. No custom tabIndex unless absolutely necessary

4. All interactive elements must be reachable via Tab key

Live — focus ring demo (press Tab)

Tab through to see focus rings. Ring is always 3px (buttons) or 2px (inputs) with --ring at 50%.

Reduced motion — prefers-reduced-motion

When prefers-reduced-motion: reduce is set, these animations are disabled:

animate-content-ready → instant opacity (no transform)

animate-fade-up → removed

state-fast-pulse → static (no pulse)

state-slow-breathe → static (no breathe)

Framer Motion respects useReducedMotion() automatically. CSS-only animations use the media query in globals.css (line 1016).

What stays:

Color transitions (opacity, background) — these are non-motion

MemaxLoader dots — reduced to static display (no sequential pulse)

Placeholder text crossfade — kept but instant (no blur transition)

Touch targets — minimum sizes
44
44×44px

Primary actions: send button, mode pills, nav items, toggle switches

36
36×36px

Secondary actions: icon buttons (icon-lg), close buttons

32
32×32px

Tertiary: icon buttons (default), copy buttons, tag remove

24
24×24px

Minimum: icon-xs buttons. Only for non-critical, supplementary actions

WCAG 2.5.8: minimum 24×24px target. Apple HIG: 44×44pt for primary actions. Memax follows Apple for primary, WCAG for minimum.

Touch target patterns (pick one per element)

A. min-h-11 — element IS 44px. Use for list items, picker rows, toolbar buttons.

B. relative + after:absolute after:-inset-1.5 after:content-[''] — element visually compact, invisible pseudo extends touch area. Use for floating pills, compact toggles, icon buttons where visual 44px is too large.

C. p-3 (12px all sides) — enough padding that content + padding ≥ 44px. Simple, no pseudo needed.

Rule: choose the pattern that keeps visual density while hitting 44px tappable. Never mix — pick one per component.

Color blindness — rules
1.

Kind dots use 13 distinct hues — not distinguishable by color alone

2.

Every dot is accompanied by a text label (kind name) — color is supplementary

3.

Red (destructive) is always paired with text or icon (Trash2), never color-only

4.

Signature (violet) vs error (red) — sufficiently different hue for deuteranopia

5.

Never convey meaning through color alone — always pair with text, icon, or shape

Keyboard navigation map
TabMove focus to next interactive element
Shift+TabMove focus to previous element
EnterActivate button, submit form, open memory
EscapeClose modal, dismiss notification, clear bar
↑ / ↓Navigate memory rows, dropdown items
/Focus the bar (brain view)

15. Palette Explorer

Interactive theme preview. Switch presets to compare text hierarchy, surfaces, and accents live.

Purple-blue inspired by dreams. Distinctive, creative, memax's signature feature.

Text hierarchy
Primary/90

Memory card title

Secondary/52

Last recalled 3 hours ago from CLI

Tertiary/40

core / api-design / 2024-03-15

Muted/25

Content hash: 8f3a...

Readability test on card surface

Secondary text should be clearly readable without straining. If you squint to read this, the opacity is too low.

Tertiary text recedes but stays legible — timestamps, kinds, metadata.

Muted text: placeholders, disabled labels, hashes.

LevelSafe NeutralWarm ApricotCool SlateEarth CopperDream Violet
Primary0.90.90.90.920.9
Secondary0.50.50.520.550.52
Tertiary0.380.40.40.420.4
Muted0.250.250.250.280.25
Signature color comparison
Safe Neutraloklch(0.50 0 0)
Warm Apricotoklch(0.72 0.14 60)
Cool Slateoklch(0.65 0.10 240)
Earth Copperoklch(0.62 0.14 45)
Dream Violetoklch(0.62 0.16 290)
Gray ramp comparison

Safe Neutral (pure achromatic)

bg
card
muted
border
ring
input
muted-fg
secondary
lt-border
lt-bg

Dream Violet tint

bg
card
muted
border
ring
input
muted-fg
secondary
lt-border
lt-bg
Full card preview
API Design Patterns

REST endpoints should use plural nouns. Pagination via cursor, not offset.

core / api-design3h ago
search your memory...
Architecture Decision RecordsPRO

We chose River over SQS because Postgres-backed queues simplify our infra. Trade-off: no cross-region fan-out, but we don't need it at current scale.

decisions / infrastructurerecalled 12 timessha:4f2c...
Functional accent palette
EmberCTA / primary actions
oklch(0.65 0.18 35)
HoneycombHighlights / hover / search match
oklch(0.82 0.16 85)
SageSuccess / saved / connected
oklch(0.72 0.10 155)
DuskLinks / info accents
oklch(0.55 0.08 260)
PlumPremium / Pro indicators
oklch(0.52 0.14 310)
ClaySecondary / tertiary UI
oklch(0.58 0.06 50)
SignalError / destructive / critical
oklch(0.62 0.22 25)
Accents in context
PRO● Saved● Error
Regular text with a Dusk link color and Honeycomb highlight

2026 trend: warm-tinted grays (Linear, Notion) + bold single accent (Superhuman 0.18, Vercel 0.21). Kind dots unchanged — data layer, not UI chrome.

19. Controls

Complete interactive control gallery. All variants rendered from production components (ui/button.tsx, ui/badge.tsx, ui/pill.tsx).

Button — variants

<Button variant="default|outline|secondary|ghost|destructive|link">

Source: ui/button.tsx — CVA variants, @base-ui/react primitive

Button — sizes
xsh-6
smh-7
defaulth-8
lgh-9
Button — with icons

Use data-icon="inline-start|inline-end" for padding adjustment

Button — icon-only sizes
icon-xs24px
icon-sm28px
icon32px
icon-lg36px
Badge — variants
DefaultSecondaryOutlineDestructiveGhost
coreprocess3 sourcesErrorPro

<Badge variant="default|secondary|outline|destructive|ghost">
Source: ui/badge.tsx — h-5, rounded-4xl, text-xs

Pill — canonical chip (select / remove / add / static)

One pill to rule them all. Any chip-shaped element in the product — hub switcher, topic selector, memory tag, row metadata label — uses this primitive. Four variants cover every case; two sizes cover every density. If you find yourself writing rounded-full border bg-... inline, you're reinventing this. Stop and use <Pill> or pillClass() instead.

Variants (md size)

static labelremove — inline ×

select: chevron trailing, click opens popover. remove: × inside, for applied tags. add: dashed border empty state. static: read-only label.

Sizes — pick by the weight of surrounding text

h-9, text-[14px]
h-7, text-[12px]
sm — metadatah-5, text-[11px]

lg: comfortable weight — use when the pill is a primary element sitting next to 14px list or body content (e.g. the top-bar hub switcher whose dropdown items are 14px too). Matching weights keeps the trigger from feeling small next to its own menu. md: default chip — topic selectors, inline content chips, filter chips, role pickers. sm: ultra-dense metadata in memory rows where space is scarce.

Weight match — trigger must feel as substantial as its menu

小宝记忆空间330
✗ md trigger + 14px list = trigger feels small
小宝记忆空间330
✓ lg trigger + 14px list = matching weight

Rule of thumb: if the pill's own menu uses text-[14px], use size="lg"on the trigger so it doesn't feel 小气 next to its list.

In the wild — these are the four canonical uses

hub switcher (top bar) — size="lg"
topic selector (memory detail)
reactdeployment
memory tags
personalmemory row metadata (sm)

Composing with PopoverTrigger / Link / custom wrappers

import { Pill, pillClass, Popover, PopoverTrigger } from "@memaxlabs/ui";

// Option 1 — <Pill> component (preferred for plain cases)
<Pill variant="select" icon={<Icon />} onClick={open}>Hub name</Pill>

// Option 2 — pillClass() helper (when another element owns the interaction,
// e.g. Base UI PopoverTrigger needs to BE the button itself)
<PopoverTrigger className={pillClass({ variant: "select" })}>
  <Icon />
  <span>Hub name</span>
  <ChevronDown className="size-3.5 shrink-0 text-fg-4" />
</PopoverTrigger>

Migration map — legacy call sites to replace

web/components/features/hub-identity-chip.tsx
standalone variant → pillClass({ variant: "select" }) on the PopoverTrigger. Keep HubBadge as the icon slot, keep HubRoleTag, drop the hand-rolled border border-border/70 bg-card... and inherit the canonical rounded-lg arc.
web/components/features/topic-pills.tsx (TopicLocation)
filled state → pillClass({ variant: "select" }). Empty state → pillClass({ variant: "add" }). Keep the external × button as-is (separate 44px clear target). Canonical radius is rounded-lg— the crisp 8px arc that matches section 2a's TopicPill reference.
web/components/features/memory-row.tsx (hub label)
<Pill size="sm" variant="static">. Drop the inline text-[10px] bg-surface-1 px-1.5 py-0.5 rounded.

Source: ui/pill.tsx — rounded-lg, bg-surface-1, border-border/60, text-[14px] (lg) / text-[12px] (md) / text-[11px] (sm). Variants: select | remove | add | static. 8px arc matches section 2a's TopicPill reference — crisp, not overly round.

Pill — removable tag
reactdeploymentremovablewith clear ×

<Pill variant="remove" onRemove={fn}>label</Pill> — Source: ui/pill.tsx. Use the same `Pill` primitive for removable tags instead of a separate tag component. This keeps chip semantics and tokens aligned across the product.

Send button — bar-specific control
Push
Recall
Loading
Disabled

h-8 w-8 rounded-lg. Push: foreground fill. Recall: signature fill. Source: bar-right-portal.tsx

Toggle switch — two variants (neutral default, intelligence opt-in)

Neutral (Account / Teams / Appearance / everywhere else)

Intelligence (Intelligence tab only — AI behavior)

Neutral: NEUTRAL_INK (var(--fg-1)) track when on, NEUTRAL_TRACK_OFF when off. Intelligence: var(--signature) track when on. Both: same geometry — w-10 h-6 rounded-full track, w-4.5 h-4.5 thumb.

Toggle — async (optimistic + pending)

Optimistic: switch moves immediately. Pending: border pulses until server confirms (600ms simulated).

Radio row — option with sublabel (border-2)

border-2 outer ring. Ring color = NEUTRAL_INK when selected, NEUTRAL_BORDER_OFF otherwise. Inner dot = NEUTRAL_INK. NO background fill on the ring — radios are border+dot, not filled pills. See real impl in SelectionOption (settings-dialog.tsx) and hub-permissions-section.tsx.

Pill picker — self-explanatory short labels (Role Picker pattern)

Invite as

flex gap-1.5 flex-wrap. Selected: bg-foreground text-background font-medium. Unselected: bg-surface-1 text-fg-2 hover:bg-surface-2. Use when option labels are self-explanatory (role names, plan tiers, filter chips). Canonical impl: hub-invites-section.tsx:204-217. NOT for options that need descriptions — use RadioRow above for those.

Input field

h-8, rounded-lg, border-border, focus: border-ring + ring-2 ring-ring/50

Pill chips — mode toggle
remember·recall
rememberrecall

Desktop: text "remember · recall" (dot separator). Mobile: pill chips with 44px min touch target. Source: layout.tsx mode toggle

Control color semantics — when to use which color (canonical)

One rule rules them all: purple is reserved for Intelligence, everything else uses neutral ink. This card is the single source of truth — if you're building a new control and wondering “what color does active use?”, the answer is here.

1.

Purple (var(--signature)) = Intelligence tab ONLY. Dreams, merge, archive, organize — AI behavior toggles. Using signature purple signals “this is the AI”. Nowhere else: not in Account, Teams, Appearance, Security, Dev. If a new setting is purple and it isn't AI behavior, it's wrong.

2.

NEUTRAL_INK = everywhere else. var(--fg-1) = foreground at 0.9 opacity = body-text ink weight. One active color unifies every non-Intelligence control: toggle track on, toggle border on, radio ring on, radio dot. Visible, authoritative, not pure black.

3.

Toggles are iOS-solid. When on, the track is filled with the active color (ink or signature), border matches fill, thumb uses NEUTRAL_INK_INVERSE (var(--background)) for contrast. When off, transparent track, subtle border, muted thumb. Geometry: w-10 h-6 rounded-full track, w-4.5 h-4.5 thumb.

4.

Radios are border-2 + dot, never filled. Outer ring uses border-2 (2px), not border — 1px at 0.18 opacity is invisible. Ring color = NEUTRAL_INK when selected, NEUTRAL_BORDER_OFF when not. Inner dot = NEUTRAL_INK. No background fill on the ring — that makes the radio look like a filled pill, which is wrong.

5.

Radio rows vs pills — the decision tree. Radio rows: each option needs a description because the label alone isn't self-explanatory (“Plain / Signature / Time”, “Delete policy: none / own / any”). Show all descriptions at once — no tooltips. Pills: the labels are self-explanatory (Contributor / Viewer / Admin, Free / Pro / Team). No descriptions needed; density matters. Cargo-culting pills onto options that need descriptions is a common mistake — don't.

6.

Token source. Import from @memaxlabs/ui/tokens/controls: NEUTRAL_INK, NEUTRAL_INK_INVERSE, NEUTRAL_TRACK_OFF, NEUTRAL_BORDER_OFF, NEUTRAL_THUMB_OFF. For the Intelligence variant, use var(--signature) directly.

7.

Canonical production references. Toggle: settings-dialog.tsx ToggleRow + teams/shared.tsx ToggleRow. Radio: settings-dialog.tsx SelectionOption + hub-permissions-section.tsx. Pills: hub-invites-section.tsx:204-217. All four match — they are the reference.

Composition — settings row
Hub name

Visible to all team members

Danger zone

This action cannot be undone

Pattern: label+description left, action button right. Separator: border-t border-border/20.

InfoPopover — inline help (hover + click + tap + keyboard)
Branded — brand voice education
Your Topics

Pass brandMark as a slot when the explanation references brand vocabulary (dream, remember, recall, forget) — the ✦ visual ties copy to brand symbol in one teaching moment.

Unbranded — utility help (no brand voice)
API keys

Skip brandMark for utility help (hub management, team settings, API docs).

Hover (desktop): 150 ms open delay, 100 ms close delay

Click / Enter / Space: instant open, click outside to close

Tap (touch): instant open, tap outside to close

Esc: close (auto-handled by @base-ui)

Position: side=bottom, align=start, sideOffset=8, max-w=320px

When to reach for this

✓ 1–3 sentence explanations of what a section does and how it works — section-header inline help.

✓ Brand voice teaching moments (use brandMark prop).

When NOT to use it

✗ Short hover-only labels (keyboard shortcuts, button names) — use Tooltip.

✗ Rich content with lists, buttons, links, long-form help — compose with Popover directly.

✗ Error messages or confirmations — use Dialog or the bar toast.

First consumer: packages/web/src/components/features/topic-card.tsx — "Your Topics" header.

17. Visual Vocabulary

Two symbols only: ● dot (content) and ✦ star (memax intelligence). Behavior modifiers signal state.

● Dot — content indicator
Static — complete/resting
Fast pulse 0.8s — processing
Slow breathe 2.5s — idle
Fade out — deleting
Flash — error occurred
✦ Star — memax intelligence
Static — complete/at rest
Fast pulse 0.8s — actively working
Slow breathe 2.5s — waiting

Star uses signature color: oklch(0.62 0.16 290)

Size scale — optical balance

Star font-size ≈ 1.5× dot diameter for equal visual weight (star glyphs have more whitespace than filled circles).

xs
4 / 8pxdense lists
sm
6 / 10pxcards (default)
md
8 / 12pxsection headers
lg
12 / 20pxpage loading

16. State Machine

Content states + activity indicators. No ambiguous states.

16a. Content state transitions
  LOADING ──→ LOADED ──→ UPDATING ──→ LOADED
    │            │                        │
    │            ├──→ DELETING ──→ (removed)
    │            │
    ├──→ EMPTY   ├──→ ERROR
    │            │         │
    └──→ ERROR   └─────────┘ (retry → LOADING)

  Separate track:
  LOADED ──→ PROCESSING ──→ LOADED (auto-poll)
Loading
Empty
Loaded
Updating
Deleting
Error
Processing
16b. Activity Status Dots

Reusable liveness indicator for agents, services, connections. Derived from timestamps (last_used, last_seen) — no polling, no websocket. Green pulses to draw attention; others are static.

sm
active
idle
inactive
error
md
active
idle
inactive
error
lg
active
idle
inactive
error

Usage

>_
Claude Code2m ago3 keys
Embedding servicequeued
MCP connectiontimeout
16c. Multi-query page — per-query error isolation

A page with N queries has N independent error zones. Collapsing them into a single page-level error throws away intact data from successful queries and makes retry ambiguous (“retry what?”). Every query renders into its own card; each card owns its own error UI via <DataSectionCard>.

❌ Wrong — AND-gate collapse

if (topicsError || memoriesError) {
  return <ContentError retry={refetchAll} />
}
// → good topics data is thrown away
// → user sees red page instead of
//   3 loaded sections + 1 failed card

Current production: topic-grid.tsx:123-150, topic-detail.tsx (tree error silently swallowed).

✅ Right — per-card error

Recent
✓ 3 memories
Topics
✓ 6 topics
Inbox
Couldn't load unorganized items.

Rules

· Each query owns its visual region. No global useQueries reduce-to-error.

· Retry handlers target the specific query, never refetch the page.

· Partial success is the norm during hub switch / backend rollout — design for it.

· Error UI uses <ContentError> primitive — red pulsing dot + neutral retry link, no SIGNATURE color (design law 4).

16d. Mutation pending vs query refetching — don't double-feedback

A mutation's isPending is not the same signal as a query's isFetching. Showing both a successMessage toast and an inline visual confirmation is redundant — pick one. Production example: useDreamTriggershows a success toast AND the inbox morphs to a breathing “organizing…” state (use-dreams.ts:37, section 28).

Mutation state  →  Query state  →  Visual
───────────────────────────────────────────────
isPending       →  —              →  button dim + spinner
                                      (per-row, not global)

(settled)       →  isFetching     →  NOTHING (silent refetch,
                                      see section 35c)

isSuccess       →  data updates   →  ONE of:
                                      a) row animates in
                                         via state-memory-arrive
                                         (see section 35e)
                                      b) toast
                                         (irreversible / batch only)

isError         →  data unchanged →  surface-specific error
                                      + retry that names action

Rule of thumb: if the surface visibly updates (row vanishes, count decrements, status chip changes, new row animates in), skip the toast. Toasts are for invisible or irreversible actions (forget, undo, batch copy). Mutation inventory in section 28 should flag every mutation that has both.

Activity status rules

active— last_used <24h. Green + animate-ping pulse.

idle — last_used 1-7d. Amber, static.

inactive— >7d or never. Muted fg-4, static.

error — connection failure or expired key. Destructive red, static.

Production: lib/agents.ts (StatusDot in agent-configs-section.tsx). Extend for service health, MCP status, sync state.

8. Loading States

Shape before content. Signature color only when memax is actively working. For composed card-level state machines see section 35 (Data Section Card).

08-new. Breathing Orb — page loader
10px full-screen
6px compact
dark (glow shines)

Single signature dot that breathes — scale 0.85→1.0, opacity 0.4→1.0, soft glow halo (2.4s ease-in-out). Replaces the three-dot pulse.

08-new-b. With label — auth / invite pages
Signing you in...
08-new-c. Arrival — breathe → flare → content

Orb breathes → data arrives → dot flares outward (scale 2.8×, glow burst, 0.5s spring) → fades → content enters. Strong sense of "arriving."

Full page — cold start (current)

Loading your memory...

MemaxLoader (signature-colored sequential pulse dots) is the brand. Centered on background, not card. Text at /40. Used on brain view during initial load.

Full page — route transition

Brief transition. Compact loader, no text — user already knows where they're going.

AI streaming — breathing ✦ + summary callout
Your deployment strategy combines blue-green deployment with canary releases [1]. For risk mitigation [2], you use gradual rollouts...
Complete answer with static star — streaming done.

AI answer uses summary callout pattern (bg-surface-1 rounded-xl). Entry: animate-content-ready. ✦ breathes (state-slow-breathe) while streaming, static when complete. Text at /75.

Content area — skeleton grid

Skeleton mirrors loaded card: dot+title, body, meta. Shimmer uses foreground opacity, not gray — adapts to dark mode.

Recall loading — send button spinner + AI cycling text

Recall loading: send button shows spinner (no notification banner). AI synthesis loading: ✦ breathing + RecallingText variant="ai" crossfade below results ("memax is thinking" → "reading your memories" → "connecting the dots"...).

memax is thinking
Transition — skeleton → content (animate-content-ready)

0.15s ease-out, opacity 0→1 + translateY 6px→0. Applied globally to all loading→loaded transitions. Click to replay.

08h. Background refetch — silent by design

When a mounted data card refetches (stale-while-revalidate, post-mutation invalidation, focus refetch), memax shows no visual signal at all. The rows the user sees are already accurate, and a pulsing indicator would only distract. This is a deliberate rule. For the full composed state machine and the cache pivot / memory arrival cases see section 35.

Recent
Past 12h
claude-code captured14m
Fly.io blue-green with health checks
cursor captured1h
OAuth refresh token rotation strategy

A mounted card that just got invalidated looks exactly like this. No pulse, no star, no spinner, no dim. Data on screen is already accurate — background re-sync should not interrupt the user. For the case where data IS about to change (hub / filter pivot), see section 35d. For the case where a NEW row arrives via SSE, see section 35e — the row animates in, the card chrome stays still.

Inline — card processing indicator
Docker multi-stage builds

Use multi-stage builds to keep production images small. Separate build dependencies from runtime...

process·organizing...

Card appears immediately with content. ✦ replaces the neutral indicator during processing. "organizing..." breathes in signature color.

9. Empty States

Multiple tiers: first-time, filtered (centered + inside-card), transient, recall zero, and recall error. Every empty state is surface-specific — no generic fallback copy. For composed card-level empty handling see section 35 (Data Section Card).

First-time

No memories yet

Type anything and press Enter to remember

Remember something →
Filtered (centered, full-page)

No memories in code

Transient

No reviews pending

Recall zero-results

No matching memories found.

Try different words or rephrase your question.

i18n: recall.noResults, recall.noResultsHint. Shown inside bar expand slot when recall returns 0 results. No icon — text only.

9e. Filtered empty — inside a mounted card (NOT a centered page)

Tier 2 above is the centered “No memories in code” variant — that's correct for a full-page filter (like the brain view's kind filter). But most filters live insidea SectionCard (Recent, Inbox, Topic detail) and the empty state must render inside the card's body — the header and filter chip stay visible so the user can clear the narrow filter without losing context.

Recent
Past 12h · Claude

No memories from Claude in the past 12 hours

Try widening the window or switching actor

i18n: copy MUST echo the active filter values (“in the past 12 hours”, “from Claude”) — not a generic “No results.” Clear-filter action uses the exact filter label so the user knows what they're undoing. Chinese translations must not repeat the card title (anti-pattern: card labeled 最近 showing “最近 12h 没有结果” — “最近” × 2).

9f. Anti-pattern: `return null` when the list is empty

The single most common empty-state bug in memax: a component does if (!data.length) return nulland the entire surface disappears. The result: layout jumps, users can't see where the section was, and they can't tell whether content was deleted or is just loading. Every such callsite should render a minimum viable shell instead.

❌ Wrong — return null

(component returns null —
section vanishes from the page)

✅ Right — mounted shell with empty body

Related memories

Nothing related yet

memax will link similar memories as they arrive.

Same shape as any <DataSectionCard> empty body. Header stays so users know which section it is.

Historical audit note: keep this list current. Mounted detail sections like topic-siblings.tsx and related-memories.tsxshould use section 35's <DataSectionCard>. Headless providers such as dream-notification.tsx and processing-notification.tsx are not card targets.

9g. Bar recall error — NOT “no results”

The single biggest production risk in the audit: bar recall currently has zero error UI (expand-search-results.tsx:269-292) — a network failure falls through to “No matching memories found.” Users think their memories are gone. Recall is memax's core value prop. Fix: use <ContentError compact> inside the bar expand slot — red pulsing dot + specific message + named retry link. No RotateCw icon, no SIGNATURE color — both are reserved for memax AI working, not user retry (design law 4).

memax couldn't reach the memory server — your memories are safe.

i18n (NEW): recall.errorTitle, recall.retry. Must differ from zero-results copy. Retry label is specific (“Retry recall,” not “Try again”). Error state must never fall through to the zero-results branch.

9h. BrainView badge — desktop /home three-state morph

Desktop /home is intentionally zen: just the bar and a single floating badge under it. Not a section card, not a feed, not a summary — one line of status. Three states morph in place with a crossfade when data arrives. This is the only surface in the app that doesn't use DataSectionCard, because it isn't a section.

loading (first fetch)

loading…
remember or ask…

has memories (returning user)

type anything to remember · ? to ask
remember or ask…

empty (first-time user)

Type anything to remember it.

memax will organize it into topics as you build.

remember or ask…

Position: top: 46vh + 44px, centered horizontally. Badge sits above the bar at roughly the golden-ratio point of the viewport.

Transitions: AnimatePresencekeyed on state string. Crossfade with ~220ms duration, spring easing. No morph between states — they're distinct enough to deserve their own frames.

Why not use DataSectionCard here: desktop /home has no content list, no header, no trailing slot, no filter. It's a bar-centric surface. Forcing it through a section-card primitive would invent chrome that the surface doesn't want.

Mobile /home renders RecentSection instead of this badge — mobile has no /topics tab, so the feed lives on /home. That path goes through DataSectionCard like every other section.

9i. return-null audit — all known violations + migration status

Every file in the web package that ships return null when its data list is empty. Each row should migrate to <DataSectionCard> from section 35 with an appropriate empty body. The anti-pattern is documented in 9f above; this table tracks the cleanup.

FileSurfaceEmpty copy suggestionPriorityStatus
topic-pills.tsx:27Memory detail → topic pillsNot assigned to any topic yet.mediumpending
topic-siblings.tsx:34Topic detail → related topicsNo sibling topics yet.lowpending
related-memories.tsx:24Memory detail → relatedmemax will link similar memories as they arrive.mediumpending
agent-configs-section.tsx:72Settings → agentsNo agent configs synced yet. Run memax setup to link your agents.highpending
hub-management-view.tsx:75Settings → hub managementYou're the only member so far. Invite teammates to collaborate.highpending
dream-notification.tsx:18Bar notification (dream)(being deleted — dream flow rewrite)n/adelete
processing-notification.tsx:42Bar notification (processing)(migrate to bar notification morph — section 10b)mediumpending

Migration pattern: replace the top-level if (!data.length) return null with a DataSectionCard wrapper whose phase prop transitions through loading → empty → loaded based on the same data source. The section header always renders; the empty body uses the suggested copy above.

Copy rule:never generic. Each empty copy should tell the user something specific about this surface — what would appear here, what they can do next, or what they're waiting on. “No data” or “Nothing yet” is not enough.

i18n: empty.firstTime.title, empty.firstTime.subtitle, empty.firstTime.cta, empty.filtered.title, empty.filtered.cta, empty.transient, recall.noResults, recall.noResultsHint, recall.errorTitle, recall.retry, brainView.empty.hint, brainView.empty.tagline, brainView.empty.setupHint.

35. Data Section Card

Universal 5-phase state machine for any list-inside-a-card surface. Never unmounts mid-fetch. Composes existing primitives (Skeleton, ContentError) — does not reinvent them. Refetch is silent; content changes animate.

35a. The 5 phases + 1 modifier

Every data-bound card in memax has exactly these 5 phases. Header chrome stays identical across all of them — only the body changes. isPlaceholderData is a modifier on loaded, not a separate phase. There is no isFetching prop — background refetch is silent (see 35c).

loading (first load)

Recent

loaded

Recent
claude-code captured14m
Fly.io blue-green with health checks

Zero-downtime deploys using blue-green on Fly.io machines, automatic rollback on health check fail.

cursor captured1h
OAuth refresh token rotation strategy

Rotate refresh tokens on every use with a 30-day expiry window and revoke on suspicious activity.

pgvector HNSW index tuning3h

m=16, ef_construction=64 for 100k+ vectors. Recall ~96% at query ef=40.

loaded + isPlaceholderData (body dims)

Recent
claude-code captured14m
Fly.io blue-green with health checks

Zero-downtime deploys using blue-green on Fly.io machines, automatic rollback on health check fail.

cursor captured1h
OAuth refresh token rotation strategy

Rotate refresh tokens on every use with a 30-day expiry window and revoke on suspicious activity.

pgvector HNSW index tuning3h

m=16, ef_construction=64 for 100k+ vectors. Recall ~96% at query ef=40.

filtered-empty

Recent

No memories in the past 12 hours

Try widening the window or switching actor

empty (true — no filter)

Inbox

Inbox is clear

New captures land here before the next dream cycle

error

Recent

Couldn't reach your recent memories

Connection timed out. Your memories are safe.

35a-i. Progressive Disclosure — explicit, reversible

Recent-style cards do not auto-grow forever. Start with 5 rows, let users reveal 5 more at a time, and always allow the section to collapse back. This keeps the card bounded, preserves page hierarchy, and matches the product rule that data sections stay controllable instead of turning into infinite feeds. The footer only appears when there is more to reveal or when the section is already expanded.

35b. Interactive phase cycler

Click a phase to switch the card below. Notice the header stays identical across every phase — only the body morphs. No layout shift.

Recent
claude-code captured14m
Fly.io blue-green with health checks

Zero-downtime deploys using blue-green on Fly.io machines, automatic rollback on health check fail.

cursor captured1h
OAuth refresh token rotation strategy

Rotate refresh tokens on every use with a 30-day expiry window and revoke on suspicious activity.

pgvector HNSW index tuning3h

m=16, ef_construction=64 for 100k+ vectors. Recall ~96% at query ef=40.

35c. Background refetch — silent by design

When React Query re-fetches a card in the background (stale-while-revalidate, post-mutation invalidation, focus refetch), memax shows no visual signal. The rows on screen are already accurate — a pulsing indicator would only interrupt the user. This is a deliberate rule, not an omission.

Recent
claude-code captured14m
Fly.io blue-green with health checks

Zero-downtime deploys using blue-green on Fly.io machines, automatic rollback on health check fail.

cursor captured1h
OAuth refresh token rotation strategy

Rotate refresh tokens on every use with a 30-day expiry window and revoke on suspicious activity.

pgvector HNSW index tuning3h

m=16, ef_construction=64 for 100k+ vectors. Recall ~96% at query ef=40.

React Query will re-fetch this card in the background on window focus, after mutations, on interval, etc. The card does not react visually to any of it — the rows on screen are already accurate.

Reserve motion for moments that change what the user sees. Background sync does not. For the case where data IS about to change (hub / filter pivot) see 35d. For the case where a NEW row arrives (SSE push, optimistic insert) see 35e — the row animates in, the card chrome stays still.

35d. Cache pivot — isPlaceholderData, body dims for outgoing data

This IS a moment where the user should see something change: the user just switched hubs or filters, and the rows on screen are about to be replaced. React Query's placeholderData: (prev) => prev keeps the previous data visible while the new query resolves, and the card dims its body to 0.72 to signal “outgoing.” Never drops to a skeleton. The dim is the ONLY visual cue — no pulse, no star, no spinner.

Recent
claude-code captured14m
Fly.io blue-green with health checks

Zero-downtime deploys using blue-green on Fly.io machines, automatic rollback on health check fail.

cursor captured1h
OAuth refresh token rotation strategy

Rotate refresh tokens on every use with a 30-day expiry window and revoke on suspicious activity.

pgvector HNSW index tuning3h

m=16, ef_construction=64 for 100k+ vectors. Recall ~96% at query ef=40.

Data is PLACEHOLDER — previous hub's rows held visible while the new hub's query resolves (placeholderData: (prev) => prev). Body dims to 0.72 signaling “outgoing,” rows swap in place when new data arrives. Never drops to a skeleton. The dim is the ONLY visual cue — no pulse, no star, no spinner.

35e. Memory arrival — row entrance animation (SSE / optimistic)

The other legitimate motion moment. When an agent pushes a new memory (SSE event) or the user just captured something, the new row lands with a soft upward settle and a fleeting signature trace on its left edge. Card chrome does nothing — the motion lives entirely on the new row. This is memax's “memory appearing” signature: quiet, expensive-feeling, and still legible during silent refetch.

Recent
claude-code captured14m
Fly.io blue-green with health checks

Zero-downtime deploys using blue-green on Fly.io machines, automatic rollback on health check fail.

cursor captured1h
OAuth refresh token rotation strategy

Rotate refresh tokens on every use with a 30-day expiry window and revoke on suspicious activity.

pgvector HNSW index tuning3h

m=16, ef_construction=64 for 100k+ vectors. Recall ~96% at query ef=40.

A new row lands softly with a brief left-edge signature trace and a smoky neutral lift, then settles into a normal row in under a second. The card chrome does not react — no pulsing header, no card dim, no skeleton. Only the new content moves.

This IS a legitimate motion moment: an agent just captured something, the user's surface gained content, memax signals “memax AI produced this” with a restrained signature trace instead of a tinted block (design law 4 — signature = intelligence working), while the rest of the effect stays in quiet neutral grays. The accent disappears quickly so the row becomes indistinguishable from existing rows once the eye registers the arrival.

Production: state-memory-arrive class in globals.css. Applied by the row component when the caller marks the memory as freshly inserted (SSE event handler in memax-event-bridge.tsx sets a flag on cache-inserted rows, row reads the flag, applies class, clears flag after 2.4s).

35f. Anti-patterns vs correct

Left side: patterns we currently ship in production. Right side: the DataSectionCardequivalent. Scan production for these anti-patterns — they're the biggest source of UI state bugs in the audit.

❌ return null on refetch / empty

(component returns null —
section vanishes — layout jumps)

if (!data?.length) return null;

✅ mounted shell, rows stay, refetch is silent

Recent
claude-code captured14m
Fly.io blue-green with health checks

Zero-downtime deploys using blue-green on Fly.io machines, automatic rollback on health check fail.

cursor captured1h
OAuth refresh token rotation strategy

Rotate refresh tokens on every use with a 30-day expiry window and revoke on suspicious activity.

pgvector HNSW index tuning3h

m=16, ef_construction=64 for 100k+ vectors. Recall ~96% at query ef=40.

Card stays mounted during background refetch. No pulsing ✦, no body dim. Data on screen is accurate — motion would only distract.

❌ generic fallback copy

Recent

暂无结果

Reuses t.states.empty.transient — no surface context, no brand voice, no echo of the active filter.

✅ surface-specific + filter echo

Recent

No memories in the past 12 hours

Try widening the window or switching actor

Title echoes the filter. Clear action names what will change.

❌ reinvented error visual

Something went wrong

Signature color on a retry button violates Law 4(Dream Violet = AI working, NEVER decorative). Generic “Something went wrong” also violates the no-fallback-copy rule.

✅ ContentError primitive + specific copy

Recent

Couldn't reach your recent memories

Connection timed out. Your memories are safe.

Uses <ContentError plain> — red pulsing dot, neutral retry link, specific action label.

35g. API contract
prop
type
contract
phase
'loading' | 'loaded' | 'empty' | 'filtered-empty' | 'error'
Caller derives from React Query flags. See 35h for the derivation.
isPlaceholderData
boolean
Previous query's data held while the new one runs (hub / filter pivot). Dims body to 0.72. Only visual signal for any async state on a loaded card.
icon
LucideIcon (optional)
Header icon. Clock for Recent, Inbox for Inbox, etc.
label
string
Header label. MUST differ from any wrapping h2 to avoid repetition.
trailing
ReactNode
Filter chip, count badge, copy / select actions.
skeleton
ReactNode (optional)
Custom shape for non-row surfaces. Defaults to row-shaped <Skeleton> mirror.
emptyCopy
{ title, hint? }
Required when phase = 'empty'. No filter active; surface is truly empty.
filteredEmptyCopy
{ title, hint?, clearLabel, onClear }
Required when phase = 'filtered-empty'. Title MUST echo the active filter.
errorCopy
{ title, detail?, retryLabel, onRetry }
Required when phase = 'error'. retryLabel is specific (e.g. 'Reload recent'), never 'Try again'.
children
ReactNode
Rows or custom body. Only rendered when phase = 'loaded'.

The caller is responsible for deriving phasefrom React Query flags (see 35h). The component is dumb — it just renders what it's told. Row-level states (updating / deleting / processing) live in the row component, NOT in the card phase — a card can hold rows in mixed mutation states simultaneously.

35h. Caller template — Recent section migration

Drop this shape into topic-grid.tsx RecentSection. Delete the existing if (!recentPages) return null (L548) and the hand-rolled card wrapper. All state branches flow through the primitive.

// Recent section — production wiring target
const RECENT_PREVIEW_LIMIT = 5;
const {
  data, isPending, isError, isPlaceholderData, refetch, fetchNextPage,
  hasNextPage, isFetchingNextPage,
} = useRecentMemories({
  hubId,
  window,
  actor,
  expanded: visibleCount > RECENT_PREVIEW_LIMIT,
});

const memories = flattenRecentMemories(data);
const visible = memories.slice(0, visibleCount);
const expanded = visibleCount > RECENT_PREVIEW_LIMIT;
const hasMore = visible.length < memories.length || hasNextPage;
const canLoadMore = visible.length < memories.length || hasNextPage;
const nextIncrement = Math.min(5, Math.max(memories.length - visible.length, 5));
const isFilterActive = window !== "7d" || actor !== "all";

const phase: CardPhase =
  isPending && !data          ? "loading"
  : isError && !data          ? "error"
  : memories.length === 0 && isFilterActive ? "filtered-empty"
  : memories.length === 0     ? "empty"
  :                             "loaded";

// Note: we do NOT pass isFetching. Background refetch is silent by
// design — the rows on screen are already accurate (see 35c).
return (
  <DataSectionCard
    icon={Clock}
    label={t.memoryView.freshMemory.other}
    trailing={<RecentFilterTrigger window={window} actor={actor} />}
    phase={phase}
    isPlaceholderData={phase === "loaded" && isPlaceholderData}
    emptyCopy={{
      title: t.memoryView.recentEmptyTitle,
      hint:  t.memoryView.recentEmptyHint,
    }}
    filteredEmptyCopy={{
      title: t.memoryView.recentFilteredTitle,
      hint:  t.memoryView.recentFilteredHint,
      clearLabel: t.memoryView.clearFilters,
      onClear: resetFilters,
    }}
    errorCopy={{
      title:      t.memoryView.recentErrorTitle,
      detail:     t.memoryView.recentErrorDetail,
      retryLabel: t.memoryView.recentErrorRetry,
      onRetry:    () => refetch(),
    }}
  >
    {visible.map((m, i) => (
      <MemoryRow
        key={getRecentRenderKey(m.id)}
        memory={m}
        surface="recent"
        showDivider={i > 0}
        isNew={recentlyArrived.has(m.id)}
      />
    ))}
    {(hasMore || expanded) && (
      <footer className="border-t border-border/30 px-4 py-2.5 flex items-center justify-center gap-4">
        {expanded && (
          <button onClick={() => setVisibleCount(RECENT_PREVIEW_LIMIT)}>
            {t.memoryView.collapseRecent}
          </button>
        )}
        {canLoadMore && (
          <button
            disabled={isFetchingNextPage}
            onClick={async () => {
              const target = visibleCount + 5;
              if (target > memories.length && hasNextPage && !isFetchingNextPage) {
                await fetchNextPage();
              }
              setVisibleCount((n) => n + 5);
            }}
          >
            {isFetchingNextPage
              ? t.memoryView.loadingMore
              : interpolate(t.memoryView.loadMoreRecent, { n: String(nextIncrement) })}
          </button>
        )}
      </footer>
    )}
  </DataSectionCard>
);
35i. Production migration targets

Files that should migrate to DataSectionCard:

  • packages/web/src/components/features/topic-grid.tsx — RecentSection (L369+), InboxSection (L810+)
  • packages/web/src/components/features/topic-detail.tsx — subtopic groups, ungrouped section
  • packages/web/src/components/features/agent-configs-section.tsx — agents list, API keys list
  • packages/web/src/components/features/hub-management-view.tsx — members, invites
  • packages/web/src/components/features/related-memories.tsx, topic-siblings.tsx, topic-pills.tsx — delete their return null branches
  • packages/web/src/components/features/dream-dialog.tsx — review list body

Non-migrations (intentional): the bar (different shell), full-page routes (use <MemaxLoader> at route level), and the memory detail page (single-record surface, not a list).

Also required in globals.css (memory arrival animation):

@keyframes memaxMemoryArrive {
  0%   { opacity: 0; transform: translateY(-4px);
         box-shadow: inset 1.5px 0 0 oklch(from var(--signature) l c h / 0.12),
                     0 10px 24px color-mix(in oklch, var(--foreground) 6%, transparent); }
  18%  { opacity: 1; transform: translateY(0);
         box-shadow: inset 1.5px 0 0 oklch(from var(--signature) l c h / 0.1),
                     0 6px 16px color-mix(in oklch, var(--foreground) 4%, transparent); }
  60%  { box-shadow: inset 1px 0 0 oklch(from var(--signature) l c h / 0.05),
                     0 2px 8px color-mix(in oklch, var(--foreground) 2%, transparent); }
  100% { opacity: 1; transform: translateY(0);
         box-shadow: inset 0 0 0 transparent, 0 0 0 transparent; }
}
.state-memory-arrive {
  animation: memaxMemoryArrive 820ms var(--ease-spring) both;
}

28. Async Action Pattern

Universal mutation lifecycle: Idle → Pending → Success/Error → Idle. MutationCache global handlers with meta-driven feedback. Zero per-callsite wiring.

28a · State Machine
Idle
Pending
Success
Error
→ idle
Ready
28b · Toast Styles
Success
3 memories forgotten
Error
Couldn't forget memories
Info / Pending
Remembering...
28c · Pattern Reference (LLM)
Architecture

MutationCache on QueryClient handles ALL mutation feedback globally.

meta.successMessage — static string or (data, vars) => string for dynamic messages.

meta.errorMessage — same signature. Shown for 5s with dismiss.

meta.skipGlobalToast — for mutations with custom UX (push flow with undo).

Bridge: module-level callback ref (mutation-toast.ts) wired by BarProvider on mount.

Rules for Adding New Mutations
  1. Add meta: { errorMessage: t.toast.* } to every mutation — even if optimistic rollback handles the UI.
  2. Add successMessage only for irreversible or batch actions (disconnect, batch delete).
  3. Use skipGlobalToast: true only when the mutation has its own undo-capable feedback.
  4. Callsite callbacks are for UI side-effects only (selection.exit(), navigation) — never for toasts.
  5. All messages go through i18n (t.*). Dynamic messages use interpolate().
  6. useBarToast() is ONLY for non-mutation feedback (clipboard copy, DnD drop). Never use it inside mutation callbacks — MutationCache handles that.
28d · Mutation Inventory
HookSuccessError
useBatchDeletedynamic (count)t.batch.forgetFailed
useBatchMoveToTopicdynamic (count)t.batch.moveFailed
useBatchMoveToHubdynamic (count)t.batch.moveFailed
useCreateMemorysilentcustom
useUpdateMemorysilentt.toast.updateFailed
useDeleteMemorysilentt.toast.deleteFailed
useShareMemoryt.toast.sharedt.toast.shareFailed
useDisconnectAgentt.toast.disconnectedt.toast.disconnectFailed
useUpdateAgentsilentt.toast.updateFailed
useDeleteAgentConfigsilentt.toast.deleteFailed
useRevokeApiKeysilentt.toast.revokeFailed
useCreateTopicsilentt.toast.topicFailed
useUpdateTopicsilentt.toast.topicFailed
useDeleteTopicsilentt.toast.topicFailed
useMemoryMove (authoritative user-move path)t.batch.moved / t.toast.moved / t.toast.movedTot.batch.moveFailed / targetNotFound / noWriteAccess
useResolveReviewsilentt.toast.reviewFailed
useUpdateSettingssilentt.toast.settingsFailed
useDreamTriggersilentt.toast.organizeFailed

26. Composition Recipes

Full-page assembly from tokens → components → layout. Copy the structure, swap the content. Every class is annotated with its source section.

Recipe — Brain View (memory grid)
Recent
React Server Componentscore · 2h

Server components render on the server...

Deployment strategyprocess · 5h

Blue-green with canary releases...

Auth middleware decisiondecisions · 1d

Moving to JWT with refresh rotation...

Topics
Deployment
12 memories
Architecture
8 memories
mWhat did we agree on...

max-w-4xl · px-5 sm:px-8

pb-36 md:pb-32 (bar clearance)

bar: z-bar, fixed bottom center

Container: max-w-4xl mx-auto px-5 sm:px-8 pb-36 md:pb-32

Group header: text-[10px] text-fg-3 uppercase tracking-wider font-semibold

Memory row: px-4 py-2.5 border-t border-border/30 hover:bg-surface-1

Topic card: Surface variant="default" p-3

Bar: fixed bottom center, z-bar, rounded-2xl, bar-border + bar-shadow

Recipe — Memory Detail (borderless)

React Server Components

Forget
>_Claude Codecaptured·memax·2d ago·recalled 47×
memax summary

Server components render on the server and send HTML. Client components hydrate on the client. The boundary is the "use client" directive.

Original content·342 words
reactarchitecture
core·private

borderless — no Surface card

provenance strip: agent + captured + age + recalled

summary: text-fg-1 hero, ✦ signature

raw content: collapsed disclosure, text-fg-2

dissolve into --background

Borderless — no Surface card. All on --background.

Provenance: agent icon + name + captured + project + age + recalled N×

Summary: text-fg-1 hero, ✦ in signature. Raw content: collapsed disclosure, text-fg-2.

Sections: border-t border-border/20 + mt-5 pt-5. All content pl-8.

Dissolve: linear-gradient(to top, var(--background), transparent)

Recipe — Settings Dialog
General
Integrations
Intelligence
Advanced

General

Display name

Visible to team members

Theme

System, Light, or Dark

System
Memory Dreams

Nightly consolidation

z-modal: backdrop rgba(0,0,0,0.15)

z-modal: modal 880px centered

sidebar: 220px, border-r border-border/30

rows: divide-y divide-border/20

Modal: 880px centered, z-modal, rounded-2xl, bar-border + bar-shadow

Backdrop: z-modal, rgba(0,0,0,0.15)

Sidebar: 220px, border-r border-border/30

Active tab: bg-surface-2 text-fg-1 font-medium

Rows: divide-y divide-border/20, py-3

Copy-paste markup — for LLM agents and developers

Structured code blocks for the most common patterns. Copy directly into new components.

Memory RowBrain view grid, topic detail, search results
<div className="w-full px-4 py-2.5 hover:bg-surface-1 transition-colors border-t border-border/30">
  <div className="flex items-center gap-2">
    <span className="h-1.5 w-1.5 rounded-full" style={{ background: dotColor }} />
    <span className="text-[14px] font-medium text-foreground truncate flex-1">{title}</span>
    <span className="text-[13px] text-fg-3 tabular-nums">{meta}</span>
  </div>
  <p className="text-[13px] text-fg-3 truncate mt-0.5 pl-5">{summary}</p>
</div>
AI Distillation (memory detail hero)Memory detail — summary as primary content, on --background
<div>
  <div className="flex items-center gap-1.5 mb-3">
    <span className="text-[12px]" style={{ color: "var(--signature)" }}>✦</span>
    <span className="text-[13px] text-fg-3">memax summary</span>
  </div>
  <div className="text-[16px] text-fg-1 leading-[1.65]">{summary}</div>
</div>
Settings RowSettings dialog, hub management
<div className="flex items-center justify-between py-3">
  <div>
    <span className="text-[14px] text-fg-1">{label}</span>
    <p className="text-[12px] text-fg-3">{description}</p>
  </div>
  <Button variant="outline" size="sm">{action}</Button>
</div>
Page ShellAny new route under /(app)/
<div className="pb-36 md:pb-32 animate-content-ready" style={{ paddingTop: CONTENT_TOP }}>
  <div className="mx-auto max-w-4xl px-5 sm:px-8">
    {/* Page content */}
  </div>
</div>
Group HeaderSection label above memory rows or card grids
<span className="text-[10px] text-fg-3 uppercase tracking-wider font-semibold">
  {groupLabel}
</span>
Empty StateNo results, empty topic, first-run
<div className="flex flex-col items-center justify-center text-center" style={{ minHeight: CENTERED_HEIGHT }}>
  <p className="text-[16px] text-fg-2 mb-2">{message}</p>
  <p className="text-[13px] text-fg-3">{hint}</p>
</div>
Token cheat sheet — quick lookup

Page layout

width: max-w-4xl (896px)

padding: px-5 sm:px-8

bottom: pb-36 md:pb-32

top: CONTENT_TOP (80px)

entrance: animate-content-ready

Surfaces

grid card: Surface variant="default"

detail: borderless (no Surface)

section sep: border-t border-border/20

row hover: hover:bg-surface-1

row divider: border-t border-border/30

Typography

H1: text-[21px] font-bold

H2: text-[16px] font-bold

H3: text-[14px] font-semibold

body: text-[14px] text-fg-1

meta: text-[12px] text-fg-3

Motion

easing: var(--ease-spring)

fast: 0.15s (modal, hover)

normal: 0.2s (content, layout)

enter: animate-content-ready

ai: state-slow-breathe

4. Memory Row Variants

Canonical spec for MemoryRow surfaces, attribution rules, and responsive behavior. This is the source of truth for row-level behavior across recent, topic, inbox, list, and recall.

Rules

Feed surfaces are attribution-rich. Content surfaces are attribution-light.

SurfaceIndicatorTextSummaryMetaMobile
recentavatar, agent icon, or content iconteam: `human pushed` / `human via agent`; personal: `agent captured`; plain personal recent: `You saved`yescontent type, age, copyhide the name, keep the verb
topiccontent icon for explicit actorsnoneyestrailing actor, agesame as desktop
inboxcontent icon onlynonenotrailing actor, agesame as desktop
recallavatar or agent iconsame attribution rules as recentyeshub badge, agehide the name, keep the verb
listavatar or agent iconsame attribution rules as recentyessource, count, agehide the name, keep the verb
Summary rendering — flat in rows, full in detail

One scan anchor per row. In a feed or list, the title is the only thing the eye is allowed to latch onto. If the summary renders inline **bold** or `code`, every row sprouts a second anchor that fights the title, and scannability collapses. This is why Gmail, Linear, Notion, Apple Mail, and Readwise all render list previews as flat plain text and save rich markdown for the detail view.

Before → after (same AI summary, rendered two ways)

Inbox north star: top-right desktop, route-based mobile

Inbox positioning decided: desktop places entry in top-right chrome between hub chip and avatar, opening anchored.

1h

✗ current — MarkdownSnippet renders **…** as font-semibold text-fg-2. The bold lead-in is brighter AND heavier than the body, so the row now has three hierarchy levels (title / bold-lead / body). Stack ten of these and the eye has nowhere to rest.

Inbox north star: top-right desktop, route-based mobile

Inbox positioning decided: desktop places entry in top-right chrome between hub chip and avatar, opening anchored.

1h

✓ flat — flat prop strips inline **/*/` markers. The preview is one quiet glance in text-fg-3 regular, and the title is the only anchor. Full markdown rendering is still available in the detail view where emphasis serves a real purpose.

The rule

Row preview <MarkdownSnippet text={m.summary} maxLen={120}flat />. Always flat. One scan anchor. Gmail / Linear / Notion convention.

Detail view <MarkdownSnippet text={m.summary}/> (default, not flat). User opened the memory to read it; emphasis now serves a purpose and doesn't compete with anything.

Architecture — two render paths, one data shape

Flat is not a bandaid — it is the correct layering. memax distills memories into rich structured markdown on purpose, because the detail view is where users actually read the memory and structure (bold lead-ins, lists, headings, citations) earns its keep. Stripping that at the distillation layer would make the detail view worse to serve a presentation concern that belongs in the UI.

The right architecture is one data shape, two render paths — same memory.summary field, rendered differently by surface:

Detail view → <AISummary />

Full react-markdown + remarkGfm — bold, italic, lists, headings, inline code, paragraphs, citation badges.

Custom renderers for <p>, <strong>, <a> (citation [N] → inline button).

Source: packages/web/src/components/features/ai-summary.tsx

Structure serves reading — bold lead-ins land, TL;DR patterns land, the user gets what the AI actually wrote.

List preview → <MarkdownSnippet flat />

Strips inline **/*/` and collapses to plain text in text-fg-3, 120-char truncation.

Block-level stripping (headings / lists / code fences) is always on — previews are one-liners.

Source: packages/ui/src/components/markdown-snippet.tsx

Scannability serves the feed — one anchor per row, the title. Reading happens on click.

Do NOT constrain the distillation prompt. Rich markdown in the data shape is an asset, not a bug. Any new surface that renders summaries gets to pick its own layer — flat for feeds and list rows, full <AISummary /> for detail and reading contexts. This is standard Separation of Concerns: data stays rich, presentation adapts per surface.

Industry references

Gmail — uses read/unread state to bold the entire row, never mid-sentence bold inside a single snippet.
Linear — issue list title is medium fg-1, description is regular fg-3, no inline emphasis in preview.
Notion — database row title is medium fg-1, property preview is regular, markdown in preview is stripped to plain text.
Apple Mail — subject medium, preview regular fg-3, no mid-text bold even if the source email had it.
Readwise — title semibold, excerpt regular, bold is reserved for the open-article reading view.
Raindrop / Pocket / GitHub notifications — same pattern. Title anchor, muted flat preview.

Regression tests — what's locked in

The rules on this card are pinned by packages/web/src/lib/markdown-snippet.test.ts. 25 cases, covering both modes + edge cases. If a future refactor tries to flatten detail-view emphasis or un-flatten row previews, CI fails before the change lands. Highlights:

flat mode:strips **bold** / *italic* / `code` → no <strong><em><code>
flat mode: canonical **Label:** body pattern flattens to plain prose
flat mode:search highlight still renders <mark> around matches
default mode:**bold** still renders <strong> (detail view intact)
block stripping: headings / code fences / list markers / newlines collapsed
broken input: unclosed **bold, stray *, mismatched ` all fall through fallback without throwing
truncation:ellipsis appears iff text exceeds maxLen; boundary token fixup doesn't leak markers
normalization: __bold__ (underscore form) still routes to strong in default mode

Run locally: pnpm --filter @memaxlabs/web exec vitest run src/lib/markdown-snippet.test.ts

Attribution Matrix

Team hub

`human pushed` when there is no agent

`human via agent` when a tool captured on their behalf

Example: `Ziyang via [agent icon] Claude Code`

Personal hub

`agent captured` when an agent is present

`You saved` on desktop recent when there is no agent

mobile plain recent rows collapse to compact artifact layout

no attribution text on quieter surfaces when there is no agent

Example: `Claude Code captured` / `You saved`

Recent — Feed Surface

Recent is attribution-rich. Desktop keeps the actor name and, when a tool captured on their behalf, shows the agent icon inline before the agent name. Mobile drops the name for team and agent rows. Plain personal rows collapse to the compact artifact layout instead of showing `saved` next to a file icon.

viaReranker cost analysis
5h

Cohere rerank v3 at $1/1000 queries, 50ms p95...

D
pushedAPI auth flow
2h

OAuth2 refresh token rotation with 30-day expiry...

capturedSession: refactored ingest pipeline
8h

Moved chunking from handler to worker, added retry logic...

You savedBlue-green deployment strategy
2d

Zero-downtime deploys using blue-green with Fly.io machines...

Topic — Quiet Content Surface

Topic rows answer “what is in this topic?” Explicit non-self actors move to the trailing edge, so the left side stays content-led and the title reads first.

Reranker cost analysis
Z
5h

Cohere rerank v3 at $1/1000 queries, 50ms p95...

Session: refactored ingest pipeline
8h

Moved chunking from handler to worker, added retry logic...

Security audit checklist.pdf
4d

Quarterly audit process, control mapping, evidence...

OpenTelemetry semantic conventions
6d

External reference for trace and metric naming.

Inbox + Recall

Inbox stays minimal, but still reserves the trailing slot for explicit actors.

API auth flow
D
2h
Session: refactored ingest pipeline
8h

Recall stays attribution-rich because it is cross-context and often cross-hub.

viaReranker cost analysis
backend-team5h

Cohere rerank v3 at $1/1000 queries, 50ms p95...

capturedSession: refactored ingest pipeline
8h

Moved chunking from handler to worker, added retry logic...

Identity Slot Unification Proposal — decision context

A · Strict identity axis

Team rows use avatars.

Agent rows use branded agent tiles.

Personal plain rows also use the user avatar.

Content type moves to trailing meta.

B · Identity-or-artifact hybrid

Team and agent rows stay identity-first.

Personal plain rows use neutral artifact icons.

No taxonomy color in the row-leading slot.

C · Surface-dependent

Recent behaves like a feed, but plain personal rows stay artifact-led.

Topic behaves like content inspection, so artifact can win.

This is the shipped production direction.

A. Strict identity axis

Team, agent, and personal plain all resolve to one identity lane. Content type is pushed into trailing meta.

Claude Code · captured12m
Homepage load 1.2s after image pipeline rewrite

Landing page image compression cut the first meaningful load.

You · saved37m
Thursday investor demo emphasis for Sarah

Keep the speed improvement above the architecture slide.

S
Sarah · marketing · pushed1h
Media embargo lifts Friday

Do not publish performance data before the embargo clears.

You · saved3h
Security audit checklist.pdf

Quarterly audit controls and evidence requirements.

B. Identity-or-artifact hybrid

Personal plain notes resolve to a neutral FileText outline. PDF / link / image stay neutral, not kind-colored.

Claude Code · captured12m
Homepage load 1.2s after image pipeline rewrite

Landing page image compression cut the first meaningful load.

note37m
Thursday investor demo emphasis for Sarah

Keep the speed improvement above the architecture slide.

note3h
Security audit checklist.pdf

Quarterly audit controls and evidence requirements.

note4h
OpenTelemetry conventions reference

External semantic naming reference for traces and metrics.

C. Surface-dependent

Recent uses the chosen hybridized feed rule: team and agent rows are identity-first, plain personal rows keep artifact icons with `You saved`.

Claude Code · captured12m
Homepage load 1.2s after image pipeline rewrite

Landing page image compression cut the first meaningful load.

note37m
Thursday investor demo emphasis for Sarah

Keep the speed improvement above the architecture slide.

S
Sarah · marketing · pushed1h
Media embargo lifts Friday

Do not publish performance data before the embargo clears.

note3h
Security audit checklist.pdf

Quarterly audit controls and evidence requirements.

Topic uses B: hybrid content inspection.

Homepage load 1.2s after image pipeline rewrite12m

Landing page image compression cut the first meaningful load.

Thursday investor demo emphasis for Sarah37m

Keep the speed improvement above the architecture slide.

S
Media embargo lifts Friday1h

Do not publish performance data before the embargo clears.

Security audit checklist.pdf3h

Quarterly audit controls and evidence requirements.

Size Primitives

Same primitive, four sizes. Decide at a glance before touching production.

sm

S
team
agent
personal plain

md18

S
team
agent
personal plain

md20

S
team
agent
personal plain

lg

S
team
agent
personal plain
Density Stress

Decide at feed length, not at single-row zoom. `md18` and `md20` rendered with the same 12-row dataset.

md18 — denser, less identity presence

Claude Code · captured12m
Homepage load 1.2s after image pipeline rewrite

Landing page image compression cut the first meaningful load.

note37m
Thursday investor demo emphasis for Sarah

Keep the speed improvement above the architecture slide.

S
Sarah · marketing · pushed1h
Media embargo lifts Friday

Do not publish performance data before the embargo clears.

note3h
Security audit checklist.pdf

Quarterly audit controls and evidence requirements.

note4h
OpenTelemetry conventions reference

External semantic naming reference for traces and metrics.

Memax Dream · captured5h
Dream grouped auth fixes into Auth & Security

New auth notes were clustered under the topic automatically.

note7h
Refresh token rotation note

Mention rollback guard in the implementation notes.

D
Derek · backend · pushed9h
SAML callback boundary fix

Callback validation tightened around tenant boundaries.

note11h
On-call handoff image

Incident timeline screenshot from the handoff deck.

Claude Code · captured1d
Reranker cost analysis

Cohere rerank cost profile versus frequency buckets.

note2d
Blue-green deployment strategy

Zero-downtime deploy path with machine warmup.

A
Ava · growth · pushed3d
Partner launch checklist

Coordinate messaging and final legal review timing.

md20 — more identity presence, slightly more vertical cost

Claude Code · captured12m
Homepage load 1.2s after image pipeline rewrite

Landing page image compression cut the first meaningful load.

note37m
Thursday investor demo emphasis for Sarah

Keep the speed improvement above the architecture slide.

S
Sarah · marketing · pushed1h
Media embargo lifts Friday

Do not publish performance data before the embargo clears.

note3h
Security audit checklist.pdf

Quarterly audit controls and evidence requirements.

note4h
OpenTelemetry conventions reference

External semantic naming reference for traces and metrics.

Memax Dream · captured5h
Dream grouped auth fixes into Auth & Security

New auth notes were clustered under the topic automatically.

note7h
Refresh token rotation note

Mention rollback guard in the implementation notes.

D
Derek · backend · pushed9h
SAML callback boundary fix

Callback validation tightened around tenant boundaries.

note11h
On-call handoff image

Incident timeline screenshot from the handoff deck.

Claude Code · captured1d
Reranker cost analysis

Cohere rerank cost profile versus frequency buckets.

note2d
Blue-green deployment strategy

Zero-downtime deploy path with machine warmup.

A
Ava · growth · pushed3d
Partner launch checklist

Coordinate messaging and final legal review timing.

Comparison Criteria

Five-point checklist

1. Personal plain readability

2. Topic scan speed

3. Recall speed

4. Mobile compression

5. Team + agent compound rows

Decision locked so far

Dots stay on topic cards only.

Inbox keeps `sm`.

Processing state overrides identity.

Hero stays `lg`.

1. Topics

Topics page composition. Canonical row rules now live in section 04: Memory Row Variants. This section focuses on how recent, topic, and inbox rows fit together at the page level.

1a. Row anatomy — universal 2-line structure

Baseline row anatomy for page-level topic layouts. The full attribution and responsive rules live in section 04. Use this section to validate page composition, spacing, and section hierarchy.

Blue-green deployment strategy
backend-team2d

Zero-downtime deploys using blue-green with Fly.io machines...

Line 1: [Indicator] Title [Meta: hub, age, copy]
Line 2: Summary (optional, pl-5 indent). No tags at row level.
1a-exp. Row without kind — 3 options

If kind is fully removed, what anchors the left side? Comparing: title-only, content-type icon, and tags-inline. Agent/team attribution tiers are unaffected (they already don't use kind).

A. Title only — no indicator

Blue-green deployment strategy2d

Zero-downtime deploys using blue-green with Fly.io machines...

fly.iozero-downtimeinfra
Staging deploy process3d

Push to staging branch triggers auto-deploy...

B. Content-type icon — visual anchor, no label

Blue-green deployment strategy2d

Zero-downtime deploys using blue-green with Fly.io machines...

Staging deploy process3d

Push to staging branch triggers auto-deploy...

Architecture whiteboard photo1w
Fly.io multi-region docs5d

Neutral dot for text, FileText for PDF, ImageIcon for image, LinkIcon for link. All foreground/20 with no taxonomy color.

C. First tag as context label — replaces subkind

fly.io·Blue-green deployment strategy2d

Zero-downtime deploys using blue-green with Fly.io machines...

staging·Staging deploy process3d

Uses first tag as context label. More specific than kind but varies wildly. Only works if tags are consistently short.

1b. Attribution tiers — see section 04 for canonical rules

These examples are retained as visual references, but section 04 is the source of truth for the row system and attribution behavior.

Tier 1 — Agent auto-capture
captured8h
Session: refactored ingest pipeline

Moved chunking from handler to dedicated worker, added retry logic...

Tier 2 — Team human push
D
pushed2h
API auth flow

OAuth2 refresh token rotation with 30-day expiry...

Tier 2b — Team human via agent
via cursor5h
Reranker cost analysis

Cohere rerank v3 at $1/1000 queries, 50ms p95...

Tier 3 — Personal (fallback to content-type dot)
Blue-green deployment strategy
2d

Zero-downtime deploys using blue-green with Fly.io machines...

1b-hub. Hub attribution — clean is the chosen direction

The clean stacked attribution is the chosen direction for `recent`. Recall stays compact. Topic rows stay icon-only and quiet.

Compact — inline context, hub in meta

Blue-green deployment strategy
2d

Zero-downtime deploys using blue-green with Fly.io machines...

D
pushed2h
API auth flow

OAuth2 refresh token rotation with 30-day expiry...

captured8h
Session: refactored ingest pipeline

Moved chunking from handler to dedicated worker, added retry logic...

A
pushed1d
Figma token export workflow

Legacy compact comparison only. We keep this here to show why the stacked treatment is better, not as an active recommendation.

Clean — attribution above, verbs, titles aligned

Blue-green deployment strategy2d
D
Derek pushed· backend-team2h
API auth flow
claude-code captured· backend-team8h
Session: refactored ingest pipeline
A
Alex pushed· design-team1d
Figma token export workflow

Chosen direction for `recent`: titles stay aligned, team rows use human-first attribution, and personal no-agent rows remain single-line.

1c. Content type indicators — personal tier only

Topic rows are intentionally quiet: icon or avatar only, then title and summary. No name, no `via`, no `captured` copy.

Blue-green deployment strategy
2d

Zero-downtime deploys using blue-green with Fly.io machines...

Staging deploy process
3d

Push to staging branch triggers auto-deploy...

Architecture whiteboard photo
1w

System diagram from Monday standup...

Fly.io multi-region docs
5d

Official guide for multi-region Postgres with read replicas...

Text → neutral dot · PDF → FileText icon · Image → ImageIcon · Link → LinkIcon. Personal indicators stay quiet and non-semantic.

1d. Surface presets — same memory, 5 contexts

Same memory rendered on each surface. The canonical rule matrix lives in section 04; this page-level demo keeps the topic layout grounded in those rules.

Recent — full meta + copy

Blue-green deployment strategy
2d

Zero-downtime deploys using blue-green with Fly.io machines...

Inbox — minimal, just title + age

Blue-green deployment strategy
2d

Topic — summary focus, no source

Blue-green deployment strategy
2d

Zero-downtime deploys using blue-green with Fly.io machines...

List — source + count + age

Blue-green deployment strategy
5×cli2d

Zero-downtime deploys using blue-green with Fly.io machines...

Recall — summary focus, no source/count

Blue-green deployment strategy
2d

Zero-downtime deploys using blue-green with Fly.io machines...

1e. Processing state — ✦ organizing...

Universal across all surfaces. ✦ breathes with signature color. No summary, no tags, no meta — just title + status.

CleanShot 2026-04-08organizing...
Blue-green deployment strategy
2d

Zero-downtime deploys using blue-green with Fly.io machines...

1f. Topics page — Recent + Topics + Inbox

Full layout: page header → Recent section (Clock, dropdown time filter, Select) → topic cards grid → inbox (Inbox icon, count). Gap-3 consistent spacing.

Your Topics

147 memories · 6 topics

Recent
CleanShot 2026-04-08organizing...
Blue-green deployment strategy
2d

Zero-downtime deploys using blue-green with Fly.io machines...

Staging deploy process
3d

Push to staging branch triggers auto-deploy...

Inbox
3

Will be organized in the next dream cycle

Random debugging note
6h
Meeting with Alex re: pricing
1d
CLI flag idea for --export
2d
1f-team. Recent section — team hub attribution

Team-hub recent uses the same canonical `recent` row behavior from section 04: stacked attribution, human-first wording, and copy action in the row meta.

Recent
D
pushed2h
API auth flow

OAuth2 refresh token rotation with 30-day expiry...

via cursor5h
Reranker cost analysis

Cohere rerank v3 at $1/1000 queries, 50ms p95...

captured8h
Session: refactored ingest pipeline

Moved chunking from handler to dedicated worker, added retry logic...

1f-pagination. Recent pagination — progressive disclosure, not infinite scroll

Problem: current production uses an IntersectionObserver sentinel to auto-load more memories on scroll. Once expanded, Recent fills the entire page and never stops. Users feel trapped in a continuously growing list with no way back.

Principle: infinite scroll is for passive consumption feeds (TikTok, Twitter timeline). Progressive disclosure is for task-oriented lists (Linear issues, Gmail inbox, Slack messages). Recent is task-oriented — users are reviewing what got added, deciding if they need to fix anything, scanning for something specific.

Pattern:show 5 initial rows. Explicit “Show 5 more” button with remaining count. “Collapse” link appears in the section header trailing slot the moment the user expands past the initial 5 — always visible, always one tap away.

Interactive — tap “Show 5 more” to expand, “收起” to reset

Recent过去 12h
5 / 27
Auth middleware rewrite notes3m
React Server Component boundary rules12m
Deployment rollback incident — Apr 928m
Ziyang pushed reranker cost analysis41m
Tailwind 4 migration — color token changes1h

Load batch: 5 rows per tap. The constant RECENT_BATCH = 5 can be tuned later but should stay small enough that each tap is a deliberate step, not a lazy expansion.

Initial limit:5 rows. Fresh visit always starts at 5, regardless of previous session state. No localStorage persistence of displayLimit — users don't want to inherit yesterday's expand state.

When remaining ≤ batch size: CTA copy changes to “Show last N · 显示剩余 N 条” so the final tap shows the exact truth.

When all loaded: CTA disappears entirely. Collapse affordance stays in the header as long as displayLimit > 5.

Server pagination:under the hood, cursor pagination still fetches in the background when the requested limit exceeds what's loaded. User sees one coherent “Show more” action regardless.

Same pattern applies to Inbox. Inbox is also a task-oriented list section — use the identical disclosure mechanic. The constants INBOX_INITIAL / INBOX_BATCH can share values with Recent unless product signal says otherwise.

1f-pagination-why. Why infinite scroll is wrong here (industry reference)
Surface typeUser's jobPatternExamples
Passive consumption feedKeep me entertainedInfinite scroll ✓TikTok, Instagram, Twitter timeline, Reddit home
Task-oriented listFind / review / actProgressive disclosure or pagination ✓Linear, GitHub, Gmail, Notion, Slack, Things
Long-form contentRead in orderSingle page or TOCMedium, Substack, Wikipedia
Dashboard / metricsScan at a glanceFixed viewport, no scrollGrafana, Datadog, Linear home

Recent, Inbox, Topic detail memory list, and Recall results are all task-oriented lists → progressive disclosure wins for all four. The current production infinite-scroll fallback in RecentSection is the exception to fix.

1f-inbox. Inbox section — 3 states

Inbox behavior differs by plan. Free users wait for nightly dream cycle. Pro users can trigger organization immediately. During organization, ✦ breathes and rows show progress.

Free — wait for dream cycle

Inbox
3

Will be organized in the next dream cycle

Random debugging note
6h
Meeting with Alex re: pricing
1d

Pro — organize now

Inbox
3

3 memories waiting — organize now or let the dream cycle handle it

Random debugging note
6h
Meeting with Alex re: pricing
1d
CLI flag idea for --export
2d

Organizing — dream triggered

Inbox
organizing 3 memories...
Random debugging note
6h
Meeting with Alex re: pricing
1d
CLI flag idea for --export
2d
1g. Topic detail — description + subtopics + ungrouped

Inside a topic. Breadcrumb → header with AI description → subtopic sections → “Other” section for ungrouped memories. No kind tabs (kind deprecated from UI). Rows use surface="topic": summary visible, edge-to-edge (px-4, no wrapper padding).

Your TopicsDeployment

Deployment

23 memories · 3 subtopics

Blue-green deploys, rollback procedures, staging automation, and CI/CD pipeline configuration for Fly.io.

Staging
8
Blue-green deployment strategy
2d

Zero-downtime deploys using blue-green with Fly.io machines...

Staging deploy process
3d

Push to staging branch triggers auto-deploy...

Production
11
Fly.io multi-region docs
5d

Official guide for multi-region Postgres with read replicas...

Architecture whiteboard photo
1w

System diagram from Monday standup...

Other
4
Fly.io multi-region notes
3d
Deploy checklist draft
5d
1g-flat. Topic detail — leaf topic with description

Leaf topic (no subtopics). Description shown under header. Memories in a single flat card. No kind tabs.

Your TopicsGo Patterns

Go Patterns

31 memories

Error handling conventions, context propagation rules, interface design principles, and goroutine lifecycle patterns.

Error handling conventions
2d

Zero-downtime deploys using blue-green with Fly.io machines...

Context propagation rules
3d

Push to staging branch triggers auto-deploy...

Interface design principles
5d

Official guide for multi-region Postgres with read replicas...

Goroutine lifecycle patterns
1w

System diagram from Monday standup...

1h. Topic card states — 4 variants

Cards show AI description for leaf topics and subtopic names for parents. No extra taxonomy indicators. Description comes from dream engine. h-full + flex-col pins age to bottom for consistent card height.

Leaf + description

Parent + subtopics

Pinned leaf

Empty (new)

1h-next. Topic cards — memax identity, not Notion

Cards are the right container. Problem: current cards look generic. Fix: AI description always visible, ✦ for dream signal, content preview for scent. Three options below.

A. Content-dense — description + memory preview

Deployment

Blue-green deploys, rollback procedures, staging automation

Fly.io machine-based blue-green with health checks
23 memories· 3 subtopics
Go Patterns

Error handling, context propagation, interface design

Table-driven tests with testcontainers setup
31 memories
Frontend

React Server Components, Tailwind, Radix primitives

TanStack Query infinite scroll with sentinel observer
22 memories· 2 subtopics

B. Compact — icon-led, description, sub-topics as · text

Deployment23

Blue-green deploys, rollback procedures

Staging · Production · Rollback

Authentication18

OAuth2 refresh flow, token rotation

Go Patterns31

Error handling, context propagation

Database15

Connection pooling, migration strategy

Frontend22

RSC, Tailwind, Radix primitives

Components · Styling

Team Docs9

Onboarding, code review guide

memax identity signals:

• ✦ + dream-purple = AI organized this, not the user
• Memory preview = content scent (what's IN the topic)
• Description = dream engine output
• No timestamps — recent activity via ✦ preview

1i. Empty states — 3 variants

Pre-dream (waiting)

Topics appear after your first dream

memax will organize your memories by subject during the nightly dream cycle.

Inbox clean

Your memory is clean

Dreams disabled

Dreams are turned off

Enable dreams to organize your memories.

Unified MemoryRow: One component across all surfaces. Canonical behavior now lives in section 04. This page-level section demonstrates how those row rules compose inside Recent, Topics, and Inbox.

Kind deprecation: Subkind labels and color coding removed from rows and cards. Topics + tags provide better navigation. Kind tabs removed from topic detail.

Topic cards: Leaf topics show AI description (from dream engine). Parent topics show subtopic names with chevrons. No extra taxonomy indicators. h-full + flex-col pins age to bottom for consistent grid height.

Topic detail:Description under header (from dream engine). Subtopic groups + “Other” section for ungrouped memories. No kind tabs. Edge-to-edge rows (px-4, no px-1.5 wrapper).

Hub attribution: Hub name lives in the canonical row rules in section 04. Current chosen split: `recent` uses stacked attribution, `recall` stays compact, `topic` stays icon-only, and `inbox` stays minimal.

Rows: Edge-to-edge within card (px-4). No rounded-lg. Flat dividers (border-t border-border/30). hover:bg-surface-1.

Copy:Per-row copy (markdown) on Recent surface only. Group copy is explicit-intent only — enter Select mode → batch toolbar Copy (context block format). Section headers no longer host a group-level “Copy N for AI” button.

2. Memory Detail & Recall

Borderless reading surface. No card wrapping content — like Notion, Linear, Bear. Title → provenance → AI summary → collapsed raw content → meta footer → topic siblings. Whitespace and faint lines create structure.

2a. Agent-captured, with AI summary (primary case)
React Server Components Architecture
Forget
Claude Codecaptured·memax·ARCHITECTURE.md·2d ago·recalled 47×
🏗️Architecture
memax summary

React Server Components render on the server and send HTML to the client. Client Components hydrate and become interactive. The boundary is the "use client" directive.

This enables zero-JS for static content while maintaining interactivity where needed. Data fetching happens at the component level with async/await.

Server Components are a new kind of Component that renders ahead of time, before bundling, in an environment separate from your client app or SSR server...

[Full raw content here. text-fg-2 = secondary to the distillation above.]

reactarchitectureserver-components
core·private
Also in Architecture
Deployment strategy — blue-green with canary5h
Auth middleware decision — JWT with refresh rotation1d
Database migration patterns3d

Entirely on --background. No Surface card. No border. No rounded container.

Sections separated by border-t border-border/20 + mt-5 pt-5 (whitespace + faint line)

All content pl-8 (aligned past back chevron)

Summary: text-fg-1 hero. Raw content: text-fg-2 collapsed. Tags/kind: bottom.

2b. Web-captured, no summary (short memory)
Quick note about lunch meeting
Forget
you·3h ago

Met with Sarah about the Q3 roadmap. Key takeaways: focus on performance first, then features. Ship the caching layer before the new dashboard.

meetings
meetings·private

No summary → content at text-fg-1 directly. No ✦, no disclosure. No topic → no siblings. recalled 0× → hidden. Cleanest possible page.

2c. Processing state (just captured)
Docker multi-stage builds
Cursorcaptured·just now

Still remembering...

memax is organizing this memory

Processing: ✦ slow-breathe centered on --background. Provenance visible. No Surface card needed.

2c-ii. Loading skeleton (borderless)

Skeleton mirrors the borderless layout exactly: back button area → provenance line → content lines → footer tags. All on --background. Uses bg-foreground/[0.07] + animate-pulse with staggered delays. Transitions to loaded via animate-content-ready.

2c-iii. Error state

Couldn't load this memory

Check your connection and try again

Error: back button visible (escape route). Centered message + retry on --background. Red dot (destructive), text-fg-2 message, text-fg-3 hint, ghost retry button.

2c-iv. Loading → loaded transition

Skeleton → content uses animate-content-ready on the content wrapper. 0.15s opacity 0→1 + translateY 6px→0.

// Pattern:

{isLoading ? (

<DetailSkeleton />

) : (

<div className="animate-content-ready">

// loaded content

</div>

)}

MANDATORY for every loading→loaded swap. No pop-in without animation. Skeleton shapes must match loaded layout exactly.

2c-v. Sticky header (appears on scroll)
React Server Components Architecture

MemoryStickyHeader component. Fixed z-40, fades in on scroll via IntersectionObserver. mask-image dissolve at bottom. Shows recall count when ≥10 (activity signal in compact form). Extracted as reusable component.

2d. Forget flow — header morph

Default

Forget

Confirming

ForgetKeep

Forget left (destructive, requires movement), Keep right (safe, cursor already there). Navigation cancels.

2e. Mobile layout
React Server Components
Claude Codecaptured·memax·2d ago·recalled 47×
🏗️Architecture
memax summary

Server Components render on the server. Client Components hydrate on the client. The boundary is "use client".

Server Components are a new kind of Component...

reactarchitecture
core·private

Mobile: identical design, not "narrower desktop." Back above title. No pl-8 (no back button inline). Provenance wraps. Same borderless surface as desktop now.

2f. Activity signal — recall count scaling
Claude Codecaptured·2d ago·recalled 3×
Claude Codecaptured·1w ago·recalled 23×
Claude Codecaptured·1mo ago·recalled 127×

0=hidden, 1-9=fg-3, 10-49=fg-2 medium, 50+=fg-1 medium. Typographic weight only, no color.

Design rules — memory detail
1.

Borderless. No Surface card. Entire page is --background. Whitespace + faint border-border/20 lines separate sections. Same on desktop and mobile.

2.

Provenance first. After title: WHO captured, FROM WHERE, how often recalled. This is memax, not a note app.

3.

Summary = hero. AI distillation gets text-fg-1. Raw content collapses at text-fg-2.

4.

No summary → no split. Content renders directly. No ✦, no disclosure. Simplest case.

5.

Classification at bottom. Kind + tags + boundary in footer. Top = provenance. Middle = content. Bottom = organization.

6.

Not a dead end. Topic pill → /topics/[id]. Siblings show 2-3 neighbors. Tags filterable.

3. Forget Experience

Container morphs from neutral to glassmorphism destructive tint. No modal — the surface transforms in place. Three states: normal, forgetting, forgotten.

Note header states

Normal

Normal note header

Forgetting

Forgetting…

Forgotten

This memory has been forgottenUndo
Full card morph preview

Card — Normal

React Server Components render on the server and stream HTML to the client.

3 days ago

Card — Forgetting

React Server Components render on the server and stream HTML to the client.

Are you sure?

Card — Forgotten

React Server Components render on the server and stream HTML to the client.

Memory forgotten

25. Batch Operations

Indicator-slot morph + glass toolbar. Entry via “Select” in section header trailing slot. Every action is end-to-end.

25a. Entry — where batch starts on each surface

Each section header has a trailing slot (right side, after spacer). “Select” lives there as subtle text. Tapping it enters selection mode — indicators morph to rings, toolbar appears. “Done” exits. Same pattern on every surface. In production, the current row indicator stays in the same slot and morphs there; it should never hard-swap to a disconnected checkbox.

RecentPast 24h
… memory rows …
Unassigned12
… inbox rows …
🚀Deployment23
… topic rows …

Alternative entries: long-press (mobile, 500ms), shift+click (desktop range select). Both auto-enter selection mode.

25b. Indicator Morph — same container, 3 states

The h-3 w-3 indicator container morphs with spring easing. No checkbox slides in. The row's own indicator slot becomes the selection affordance, then returns to its native icon on exit.

Default
Ring
Check
25c. Full Flow — tap Select, pick rows, act

End-to-end: “Select” enters mode → indicators morph to rings → tap rows to select → checks appear → “Done” exits back to the native row indicator.

Recent
Past 24h
Blue-green deploy rollback procedure2h
OAuth2 token rotation — refresh flow4h
pgvector index tuning — probes=101d
Tailwind v4 migration notes1d
River queue retry backoff config2d
Cohere rerank threshold tuning3d
25d. Move — glass picker anchored to toolbar

“Move” opens a glass picker above the toolbar. Topics with emoji icons. Tap topic → memories move → rows animate out → toast “3 → Deployment” → selection clears.

Move to

Picker anchored to Move button, opens upward. Click outside or Esc closes. Same glass treatment as bar expand slot.

25e. Forget — toolbar morphs to confirmation

Same toolbar container. Actions collapse, destructive confirmation appears. Glassmorphism destructive treatment (kitchen 03). Forget LEFT, Keep RIGHT (cursor safety — cursor was on Forget, Keep appears where cursor is).

Before

After “Forget” tap

Forget 3 memories?
25f. Mobile — long-press entry, compact toolbar

Long-press (500ms, haptic feedback) enters selection on mobile. “Select” text still available in header. Compact toolbar: icons only, same glass treatment. Fixed above safe area.

Recent
Done
Blue-green deploy rollback procedure
OAuth2 token rotation — refresh flow
pgvector index tuning — probes=10
Tailwind v4 migration notes
25g. When batch matters — real user scenarios
Inbox triageWeekly

20 unassigned memories. Select by theme, batch move to topics. Primary batch use case.

Move to topics
Context handoffPer project

Starting new project. Select all auth memories, copy as <memax-context> for agent.

Copy context
Dream cleanupMonthly

After dreams merge duplicates. Forget stale memories the dream engine didn't catch.

Batch forget
Knowledge backupOn demand

Leaving project. Export topic memories as .md file for handoff.

Export .md
25h. State Machine — complete lifecycle

Every batch action follows the same state machine. No dead states, no silent failures.

1
Idle

Normal rows. Native row indicators. No selection UI visible.

2
Selection active

"Select" → "Done" in header. Indicators morph to rings. Click toggles.

3
Toolbar visible

≥1 selected. Glass toolbar at bottom: count + Move/Copy/Export/Forget.

4
Confirm (forget only)

Toolbar morphs in place: "Forget N memories? [Forget] [Keep]".

5
Pending

Toolbar shows spinner + count. Actions disabled. No user interaction.

6
Success

Selection exits. Bar toast: "3 memories forgotten" / "3 memories moved" / "Copied".

7
Error

Selection stays. Bar toast (error): "Failed to forget/move". User can retry.

Pending state (toolbar morphs to spinner)

3 memories...

Success toast (bar notification)

3 memories forgotten

Architecture (for LLM agents)

SelectionProvider— section-scoped (not layout-level). Each section wraps its own provider from the parent (TopicGrid, TopicDetailPage). useSelection() inside the section reads the correct context. Esc exits globally.

BatchToolbar— portals to document.body via createPortal (escapes overflow-hidden + transform ancestors). z-60 (above bar z-50). Signals GlobalBar to hide via batch-active.ts bridge. Slides up (y 24→0, opacity, FAST spring). Uses bar tokens (--bar-bg, --bar-border).

batch-active.ts— module-level signal (same pattern as mutation-toast.ts). BatchToolbar signals active/inactive on mount/unmount. GlobalBar listens: bar slides down + fades (marginTop 0→24, avoids transform conflict with translateX(-50%)). Bar/toolbar share same bottom position — container morphing with animated swap.

MemoryRow— indicator slot morphs native indicator → ring → check → native indicator (spring easing, same slot). Click: selectionActive ? toggle : navigate. Long-press 500ms on mobile enters selection.

FilterDropdown— portalled to body (escapes card overflow-hidden). Positioned via anchorRef getBoundingClientRect.

Toasts— via MutationCache meta (universal async action pattern, kitchen 28). No per-callsite toast wiring. Callsite callbacks for UI side-effects only (selection.exit()).

Server: POST /v1/memories/batch-delete (single SQL), POST /v1/memories/batch-move (topic or hub). Atomic — zero partial state.

5. Loading Text

Time-progressive loading with animated star indicators. Each mode has a unique visual identity: sonar rings (recall), pulse dots (ask), settling glow (remember).

Recall mode — sonar rings (searching memory)
Ask mode — pulse dots (thinking)
Remember mode — settling glow (placing memory)

38. Bar — North Star (alt)

Second take on the bar redesign. 24m glass · recall default · AI slot always reserved · inline Remember feedback · hub badge · mobile mirror · slash minimal. Sibling to section 37 — read 38q for deltas.

38.0 Sync note — 2026-04-14 production state

Production moved to a cleaner engineering model after the initial port: one derived interaction state in bar-context.tsx, one explicit mobile compose state (docked | mirror | fullscreen), and thinner render portals that follow that state instead of re-deriving behavior locally.

  • Plain / opens command discovery only. No chip yet.
  • A command chip appears only after explicit commit: exact /topic or /hub, tapping a command row, or pressing on a highlighted command.
  • Committed command + empty query shows the full target list. Typing filters that list. Backspace on empty query degrades the chip back to raw slash text.
  • Staged files force remember mode and suppress recall / AI surfaces on both desktop and mobile.
  • Remember feedback is owned inline by RememberRow. Global bar notifications are ambient-only, not remember lifecycle.
  • Mobile keeps one persistent bottom thumb bar. Mirror/results fade in above it. Fullscreen compose is the higher tier, not a second bottom bar or a desktop-style expand card.
38a. Principles — ten rules

This take optimizes for continuity: the user should see the same surface evolve through every phase, not watch zones appear and disappear. The bar is memax’s one command palette — glass, downward, recall-default, layout-reserved AI, inline save.

  1. 24m glass chrome. oklch(card / 0.92) · blur(24) saturate(140) · layered shadow · 1px border /0.5. The whole bar feels lifted off the page.
  2. Recall is default. Nothing on the left in zone ① — no icon, no capsule. The placeholder (dump or ask…) is the affordance. A scope pill appears only for topic / command scope. Same lead-with-nothing pattern as Spotlight, ChatGPT, Claude, Notion, Linear ⌘K.
  3. Downward cascade. [1] at top, [N] at bottom. Natural ranked-list order.
  4. AI slot always reserved. Zone ② is present from the first keystroke (dim placeholder), through thinking / streaming / complete. Free users see an upgrade stub in the same slot — layout never jumps.
  5. Remember row is inline + transient. Zone ③ is always visible and hosts inline feedback for the save itself. Idle CTA reads Dump to memax (matches the placeholder voice). Transients use the action verb: Sending… → Sent · Undo → idle, or Couldn’t send · Retry. Two voices on purpose — dump invites, send commits. Remember-specific notification banners are deprecated; passive notifications remain for ambient events only.
  6. Hub badge on Remember row. Multi-hub + team hub → {hub} badge persists through every state. Single hub / personal: no badge.
  7. Stay put, rise on engage. Bar REST position is route-aware: 42vh on /home, docked at 32px-from-bottom on /memories and overlay routes, above-dock on mobile. Real interaction lifts it to top 24vh with a 0.45s spring. Result surface caps at max-h-[60vh]+ “+ N more” in zone ④.
  8. Mobile is a mirror. Notion pattern: bar stays above keyboard (thumb zone), mirror input at top of screen (eye zone), results between. Edge-to-edge, no card. A 📷 camera icon in zone ① opens the native photo / file picker when the input is empty + no files staged. Three exits: X, swipe-down, tap scrim.
  9. Slash is minimal + route-aware. Three commands — /topic, /hub, /forget. Plain / is discovery only; partial prefixes like /h rank the closest command but do not commit the chip yet. /forget remains route-scoped: only appears in the command list when the user is on /memories/{id} and forgets thatmounted memory — never an abstract “highlighted row.” Hidden everywhere else.
  10. Real glyphs, 24m hint bar. esc — literal Unicode in <kbd>. Hint bar is 24m style: plain text + single-glyph chips, not pill chains.

Deprecated patterns — do not reintroduce

Two old intent-routing mechanisms are gone and must not come back in the production port:

  • packages/web/src/lib/detect-intent.ts — the EN/ZH regex sniffer (question words, 什么/怎么/为什么/吗/呢). Replaced by the content-shape rule: newline or files ⇒ dump, else ⇒ recall. Delete the file on port.
  • ?prefix recall trigger (kitchen 24 proposal) — never shipped to production, and won’t. The user asks in natural language; the bar infers action from content shape, not magic characters.
  • Mode capsule tap-to-toggle (kitchen 31 ModeExpandedCard). Already covered by §2 above, but worth repeating: the capsule in 38 is a read-only scope indicator, not a mode switch.
  • as a send-button CTA icon (kitchen 38 pre 2026-04-13). Conflated the “AI is working” signal with an action affordance and violated the design-system rule signature = intelligence, never decorative. The send button is now unified (color communicates mode), matching ChatGPT / Claude / Perplexity. stays sacred to zone ②.

Open question — dream notifications

Deleting BarNotificationCard also deletes today’s dream event surface (dream · dreaming · dream_complete). Three options to decide before porting:

  • Dock badge on mobile + header chip on desktop (ambient).
  • Reuse MemaxStatusStrip (already exists for section-level progress).
  • Toast top-center — deviates from “no overlay when the surface can morph” but is the simplest port.

Flag for product decision. Dream is memax’s differentiator — the surface matters.

38b. Anatomy — five stacked zones, always in this order

Glass container, five zones stacked top-to-bottom. Zones ②③ can be empty but never reorder. Zone ⑤ (hint bar) only appears when the result surface is open and on desktop.

Input row — scope · textarea · send
Synthesis slot — reserved for both tiers
Remember row — conversion CTA + inline feedback
Source list — ranked [1] at top, load more at bottom
Hint bar — contextual keyboard hints

anatomy reference — not a real state

38c. Position — stay put, rise to 24vh on engage

The bar’s resting position is unchanged from production: 42vh centered on /home, 32px-from-bottom on /memories and overlay routes, above-dock on mobile. Same fade-in / fade-out animation as today. What changes is the engaged position — once real interaction exists, the bar animates to top 24vh (center-upper) with the existing 0.45s spring. Result surface caps at max-h-[60vh] with a “+ N more” affordance at the bottom of zone ④. On esc the bar returns to its rest position.

dump or ask…

rest · top 42vh · /home (hero moment, unchanged)

auth architecture

AI answer will appear here after
Dump to memaxmemax-team

Auth Middleware

Auth

JWT + API key dual path…

Security Rules

Security

Owner isolation…

+ 12 more

engaged · top 24vh · max-h-[60vh] · + Load more

38d. Typing — all zones live, AI placeholder reserved

First keystroke opens the result surface. Zone ② holds a dim AI placeholder (same for Pro/free — layout doesn’t jump). Zone ③ is the explicit save affordance. Zone ④ live-updates as keyword + FTS results arrive. No citation numbers yet.

ask memax or / for commands

/commandsrecallescclose

engaged · empty · focused (no query)

auth architecture

AI answer will appear here after
Dump to memaxmemax-team

Auth Middleware Design

Authentication

JWT + API key dual path, agent_name from key…

Security Rules

Security·memax-team

Owner isolation at data layer, every query filters by owner_id…

MCP OAuth JWT Fix

Authentication

Added AgentName claim to JWT, MCP OAuth tokens carry identity…

recall⌘↵remember↑↓navigateescclose

typing · t = 0–200ms · keyword + FTS

38e. Compose — ChatGPT-style auto-grow, CSS Grid stacked

Zone ① morphs when the user pastes or writes multiline text. CSS Grid two templates, instant switch, only gap animates (0.2s spring). Auto-grow via useLayoutEffect + scrollHeight, bounded at 200px before a thin scrollbar appears.

Our auth uses JWT + API key dual path

collapsed · single-line · [scope input send]

Meeting notes — bar redesign review: - 24m glass adopted, search icon default - Remember row replaces notification card - Mobile = Notion mirror

expanded · multiline · [input full] / [scope . send]

how do we handle JWT refresh when the token is close to expiry and the user is offline

AI answer will appear here after
Dump to memaxmemax-team

Auth Middleware Design

Authentication

Refresh rotation, offline grace window…

expanded + result surface visible

38e2. Mobile compose — two tiers, in-place then full-screen

Compose on mobile is a two-tier experience. Short content stays in the thumb-zone bar (Tier 1, in-place growth). Long content escapes to a full-screen takeover (Tier 2, Apple Notes pattern) where the only thing on screen is the textarea, a back chevron, and a save button.

Tier 1 — bar textarea grows vertically from one line up to ~3 lines (~150px). The mirror at the top echoes the multi-line content in sync — they stay visually consistent. Source list shrinks to give the bar room but stays scrollable. Auto-grow via useLayoutEffect + scrollHeight, same JS pattern as desktop.

Tier 2 — full-screen compose takeover. Triggered automatically when the textarea would exceed 3 lines, OR manually by tapping the expand icon that appears next to the send button as soon as there’s content. In Tier 2 the mirror, source list, and remember row all hide — the bar is the screen. Back chevron (top-left) returns to the bar in Tier 1 state, preserving the content. Save button (top-right) commits and returns to the resting dock.

Inspiration: Apple Notes, Gmail compose, Notion’s mobile block editor. The user is either browsing (Tier 0 resting dock / Tier 1 recall-then-type) OR writing (Tier 2 fullscreen). The two modes feel distinct.

Send button — always . Color says what fires.

The icon is always — same as ChatGPT, Claude, Perplexity, Gemini, Arc. Universal 2026 chat-input convention, zero learning curve. The background color tells the user what tapping will fire. One rule, four cases:

  • empty → disabled, ghost outline
  • single-line, no files → signature violet fill → tap fires recall + AI
  • text contains \n (Return pressed) → dark foreground fill → tap fires dump
  • files staged → dark foreground fill → tap fires dump (files lock the bar to remember)

Why explicit \nand not visual wrap: a long single line that just wraps isn’t multi-paragraph — it’s still a recall query. Only a deliberate Return signals composition. Robust and predictable.

Why unified instead of for recall: is the memax design system’s “intelligence is working” glyph — reserved for zone ②’s AI thinking state. Using it as a CTA icon conflates two meanings and violates the “signature = intelligence, never decorative” rule. Unified lets stay sacred to the synthesis slot.

Desktop override: forces dump from any state (edge case: short single-line note worth saving). No recall-forcer — if you’re composing and want to search, delete the newlines. Edge case, acceptable friction. The Remember row in zone ③ never disappears, so users always see a second dump affordance.

dumping

Meeting notes from product review
bar redesign approved
mobile tier 2 takeover

Dump to memaxmemax-team
Bar Redesign Kitchen 24
Mobile Takeover Pattern
+ 3 more

Meeting notes from product review bar redesign approved mobile tier 2 takeover

keyboard

tier 1 · bar grows in place, mirror syncs

Dump to memax
Save

Meeting notes from product review: - Bar redesign kitchen 38 approved - Mobile tier 2 = fullscreen takeover - Apple Notes pattern, back + save in top bar - Mirror + source list hidden in tier 2 - Auto-trigger at 4+ lines, manual via expand Follow-ups: - wire production ComposeInputRow - handle discard guard on back

memax-team
9 lines · 312 chars
keyboard

tier 2 · fullscreen takeover, Apple Notes pattern

Transition rules:

  • resting dock → tap bar → Tier 1 compose (single-line, mirror echoes query)
  • Tier 1 → user keeps typing → textarea grows 2 → 3 lines, mirror grows with it
  • Tier 1 → tap OR textarea reaches 4 lines → Tier 2 fullscreen
  • Tier 2 → tap back → return to Tier 1 (content preserved)
  • Tier 2 → tap Save → Remember flow fires → return to resting dock
  • Tier 2 → tap esc (hardware keyboard if attached) → cancel (discard guard)
  • In Tier 2, drag-drop doesn’t apply (mobile). Photo attachment via the icon in the toolbar above the keyboard.

Web drag-drop is orthogonal to compose state — the dashed signature chrome (see 38f) fires from the container regardless of whether the textarea is collapsed or expanded. Dropping files onto a compose-expanded bar stages chips in the usual place above the input row and locks the bar to remember. One less thing to worry about on desktop.

38e3. Mobile lifecycle — 6 states, every transition explicit

The full mobile journey as a filmstrip. This card exists because 38n + 38e2 show the component-level details but leave the end-to-end flow implicit. If you’re porting the mobile branch and piecing states together from two cards, read THIS card first. It is the single source of truth for the mobile state machine.

Production wiring: [(app)/layout.tsx](../../packages/web/src/app/(app)/layout.tsx) already branches on useIsMobile(). The current north-star branch is cleaner than the earliest port: mobile uses one explicit compose state (docked | mirror | fullscreen) and keeps one persistent thumb-zone bar at the bottom. Scrim + mirror + results render above it in mirror mode; fullscreen is the higher tier. Portal slots (#bar-input-slot, #bar-right-slot, #bar-logo-slot) still work — they just target the shared mobile row instead of a separate desktop card.

memax · /home

dump or ask…

state 0 · resting dock

above dock, single-line, no scrim

asking

what do I know about auth architecture

Dump to memaxteam
1Agent Identity98%
2MCP OAuth JWT91%
3Security Rules74%
auth architecture
keyboard

state 2 · mirror compose

tap bar → scrim + mirror slide down

dumping

Meeting notes
bar redesign approved
mobile tier 2 takeover

Dump 3 lines to memax
1Bar Redesign87%

Meeting notes bar redesign approved mobile tier 2 takeover

keyboard

state 3 · mirror grow

multi-line write still keeps one persistent bottom bar

Dump to memax
Save

Meeting notes from product review: - Kitchen 38 approved - Mobile tier 2 fullscreen - Back preserves content Follow-ups: - wire production bar-context

memax-team
7 lines
keyboard

state 4 · tier 2 fullscreen

explicit higher writing tier → Apple Notes takeover

Memax uses a dual auth architecture: API keys carry agent_name for CLI/MCP, OAuth JWTs embed it as a claim.

Dump to memaxteam
1Agent Identity98%
2MCP OAuth JWT91%
3Security Rules74%
auth architecture
keyboard

state 5 · post-↑ result

AI streams into mirror (recall)

asking

what do I know about auth architecture

tap scrim ↑
Dump to memax
1Agent Identity98%
2MCP OAuth JWT91%
swipe down
auth architecture
keyboard

state 6 · exit paths

X · swipe-down · tap scrim → state 0

Note: state 1 (compose entry transition) is not mocked as its own frame — it’s the animation between state 0 and state 2. See the transition table below for timing and mechanics.

FromToTriggerDurationWhat animates
dockmirrortap bar / focus0.35s springscrim fade-in · mirror slide down from top · dock stays mounted as the single editable bar · keyboard opens (native)
mirrortier1newline / multi-line growthinstant template switch · gap 0.2ssame bottom bar grows in place · mirror tracks content · source list shrinks · send color flips violet → dark
tier1tier2explicit expand / compose takeover 0.3s springmirror collapse to 0 · source list hide · remember row hide · top bar slide-in · textarea expand to 60vh
tier2tier1tap back0.3s springreverse of above · content preserved (no discard)
mirrorresulttap send (↑ violet)0.2s FASTmirror content swaps from query echo → thinking → streaming AI · send becomes spinner · source list citations hydrate
tier1/2docktap send (↑ dark) OR Saveoptimistic instant · 0.35s exitfireRemember fires · scrim fade-out · mirror slide up · dock remains the same bar · Sent transient stays inline
anydockX button (layered)0.35s springpeels back one layer per tap (edit → revert → clear → exit)
anydockswipe-down on source listfollows finger · 0.25s settleiOS modal dismiss · content preserved · rubber-band if pulled past threshold
anydocktap scrim above mirror0.35s springreverse of entry · content preserved · scrim fade + mirror slide + dock fade

Production wiring (what codex actually touches)

  • useIsMobile() already exists. bar-context.tsx reads it and exposes isMobile on the context. The mobile branch of (app)/layout.tsx now also owns a single mobile compose enum state instead of competing booleans.
  • Scrim — new. Mount a fixed-position overlay div when mobile enters mirror or fullscreen compose. Style: bg: oklch(from var(--background) l c h / 0.88); backdrop-filter: blur(8px); z-index: 40. Click handler on the scrim = tap-scrim exit (State 6).
  • Mirror — new. Mount above the scrim using MOBILE_OVERLAY_CONTENT_TOP(defined in lib/layout.ts). Bind to the same valuefrom bar context. It’s a read-only <p>, NOT a second textarea. Line count echoes the real textarea below so they grow in sync during Tier 1.
  • Tier 2 takeover — new. Mount a full-screen absolute layer when isMobile && tier === 2. Contains its own top bar, textarea, and bottom toolbar. The existing bar portal content is hidden (not unmounted — state preservation).
  • Resting bar and dock — already exist. Don’t touch mobile-dock.tsx(kitchen 30). It’s orthogonal to the bar lifecycle.
  • Keyboard avoidance — the viewport already has interactiveWidget: resizes-content set in app/layout.tsx. When keyboard opens, the viewport shrinks and the bar stays anchored to the bottom automatically. No manual offset math needed.
  • Exit handler precedence — three paths (X tap, swipe-down, scrim tap) all route through the same exitCompose()function in bar-context. Don’t duplicate the logic in each handler — one source of truth.

State machine — the 7 names you need to know

  • state 0 · dock — resting, single-line, above mobile dock
  • state 1 · entry — 0.35s transition (not a standing state)
  • state 2 · mirror — active compose, single-line, mirror echoes query
  • state 3 · mirror grow — same bottom bar + mirror growing in sync, multi-line
  • state 4 · tier2 — fullscreen takeover, Apple Notes
  • state 5 · result — post-send, AI streaming (recall) OR sent transient (dump)
  • state 6 · exit — 0.35s transition back to state 0
38e4. Engineering state machine — production

Explicit reducers + derived surface ownership. This is the actual source of truth for state flow.

The bar now has two reducers plus one derived surface model: a UI shell reducer (bar-machine), a domain reducer (bar-domain), and a pure surface derivation layer (bar-surface). Layout positions from the surface model; visual components read that model instead of inventing their own booleans.

BarMachine (UI shell)

state:
  value
  scope: recall | topic | command
  phase: input | loading | recall-result
  barFocused
  barOverlayOpen
  mobileComposeState: docked | mirror | fullscreen
  activeNavigationIndex
  isBarExpanded
  detectedUrl
  memorySearch
  selectedMemoryId
  isDragging

peelBack order:
  restore-query
  reset-phase
  reset-remember
  clear-value
  close-mobile-compose
  collapse-expanded
  clear-scope
  hide-overlay
  blur

BarDomain (async + feedback)

state:
  recallQuery
  askAnswer
  aiLoading / aiStreaming
  rememberState / rememberedTitle
  stagedFiles
  barNotification / dreamNotification / statusNotification
  selectedMemory
  pushing

resets:
  resetRecall
  resetRemember
  resetDomain

BarSurface (derived presentation)

derived:
  visibilityState:
    hidden | rest | engaged

  expandSurfaceKind:
    hidden
    command
    remember
    recall-input
    recall-loading
    recall-result

rules:
  layout positions only
  child renderers read explicit surface kinds
  remember is first-class; never inferred from raw text
  recall UI never survives as a stale side effect
  command / remember / recall are mutually ordered surface owners
38f. File & URL — drag-drop, paste, eager upload

Files stage as chips above the input. Drag-over flips the chrome to a dashed signature border. URL paste auto-detects and offers capture. Eager upload (ChatGPT pattern) — files start uploading on drop. When files are staged the bar locks to remember (send = ↑).

screenshot.png

Auth flow diagram

Dump 1 file to memaxmemax-team

single image pasted

design-spec.mdmockup.pngnotes.pdf

Design review materials

Dump 3 files to memaxmemax-team

batch · 3 files · one still uploading

github.com/MemaxLabs/memax-internal/pull/42Capture page

dump or ask…

URL pasted · Capture page offered

Drop files here…

drag active · dashed signature border

38g. Searching → streaming → complete — Pro, AI top-of-surface

On : query locks, send spins, semantic recall + AI synthesis fire in parallel. Zone ② flips placeholder → thinking → streaming → complete. Citations in zone ④ — [1] at top.

auth architecture

Thinking about auth architecture
Dump to memaxmemax-team
1

Agent Identity Architecture

Authentication98%

Auth = identity. API key → agent_name, OAuth JWT → claim…

2

MCP OAuth JWT Fix

Authentication·memax-team91%

Added AgentName claim to JWT…

3

Security Rules

Security74%

Owner isolation at data layer…

searching · results hydrated, AI thinking

auth architecture

Memax uses a dual auth architecture 1: API keys carry agent_name for CLI/MCP, while OAuth JWTs embed agent_name as a claim 2.
Dump to memaxmemax-team
1

Agent Identity Architecture

Authentication98%

Auth = identity. API key → agent_name, OAuth JWT → claim…

2

MCP OAuth JWT Fix

Authentication·memax-team91%

Added AgentName claim to JWT…

3

Security Rules

Security74%

Owner isolation at data layer…

streaming · tokens appending live

auth architecture

Memax uses a dual auth architecture 1: API keys carry agent_name for CLI/MCP, while OAuth JWTs embed agent_name as a claim 2. Owner isolation 3 is enforced at the data layer.
Dump to memaxmemax-team
1

Agent Identity Architecture

Authentication98%

Auth = identity. API key → agent_name, OAuth JWT → claim…

2

MCP OAuth JWT Fix

Authentication·memax-team91%

Added AgentName claim to JWT…

3

Security Rules

Security74%

Owner isolation at data layer…

open⌘⌫forget↑↓navigateescclose

complete · ✦ static · copy toolbar · query preserved

38h. Free vs Pro — zone ② reserved for both tiers

Layout identical. Only zone ② content differs: Pro streams the answer, free shows a dim upgrade stub in the exact same slot. Zones ③④ sit at the same vertical position on both.

deployment

AI answers are a Pro feature — recall stays free.
Dump to memaxmemax-team
1

Deployment Process

Infrastructure95%

Fly.io two-process deploy…

2

CI/CD Pipeline

Infrastructure78%

GitHub Actions workflow…

free · upgrade stub in zone ②

deployment

Deploy uses Fly.io with two processes 1: API server and worker. CI/CD 2 runs via GitHub Actions on push to main.
Dump to memaxmemax-team
1

Deployment Process

Infrastructure95%

Fly.io two-process deploy…

2

CI/CD Pipeline

Infrastructure78%

GitHub Actions workflow…

pro · streamed answer in zone ②

38i. Keyboard navigation — highlight + contextual hints

move across zones ③ and ④. opens a highlighted source row, or remembers if the Remember row is highlighted. Highlight tint = bg-surface-1, no border, no ring.

auth architecture

AI answer will appear here after
Dump to memaxmemax-team

Auth Middleware Design

Authentication

JWT + API key dual path…

Security Rules

Security

Owner isolation…

remember↑↓navigateescclose

remember row highlighted · ↵ = remember

auth architecture

Memax uses a dual auth architecture 1
Dump to memaxmemax-team
1

Agent Identity Architecture

Authentication98%

Auth = identity. API key → agent_name…

2

MCP OAuth JWT Fix

Authentication91%

Added AgentName claim to JWT…

open⌘⌫forget↑↓navigateescclose

source row highlighted · ↵ = open · ⌘⌫ = forget

38j. Remember feedback — inline, replaces BarNotificationCard

Zone ③ morphs through its transient states in place. No separate notification card, no push-down of the bar, no flicker. Hub badge persists through every state so the user always knows where the memory is going. Undo window = 4s; auto-dismiss after that reverts the row to idle.

Dump to memaxmemax-team
Dump to memaxmemax-team
Sending…memax-team
Sent to memax. · “Auth Architecturememax-team
Couldn’t send. · Network error

Transition table (4s total, success path):

  1. idle or click → sending (value cleared optimistically)
  2. sending → API accepts → sent (✓ + Undo, 4s window)
  3. sent + 4s → idle (row returns to CTA for next query)
  4. sending → API fails → error (content restored, 5s auto-dismiss, retry available)
  5. Undo → row dismisses, created memory deleted, content restored
38k. Slash commands — /topic + /hub shipped, /forget deferred

Typing / as the first character opens command discovery. Plain slash stays plain slash; no chip yet. After explicit command commit, zone ④ becomes the target list for that command and empty query shows the full set before the user narrows it.

  • /topic {name} — scope recall to a topic. Always available.
  • /hub {name} — switch active workspace. Always available.
  • /forget remains deferred until the forget lifecycle is fully reworked inside the bar. It stays a route-scoped future command, not a half-wired shipped action.

Dropped: /import (drag-drop already imports), /share (no share, only Copy / Copy-for-AI), /remember ( covers it), /settings (gear icon covers it).

command

/

topicJump to a topic (scopes recall)
hubSwitch active workspace
run↑↓navigateescclose

/ · on /home · command discovery only

command

/

topicJump to a topic (scopes recall)
hubSwitch active workspace
forgetForget “Auth Architecture”
run↑↓navigateescclose

future reference · route-scoped /forget stays deferred

topic

/topic pkce

PKCESecurity › OAuth
OAuth flowsSecurity
Token storageSecurity › Sessions
drill↑↓navigateescclose

/topic pkce · fuzzy-filtered topic jump

38l. Topic scope — capsule reflects route context (absorbs 24m)

On /memories/topics/{id} the bar picks up an implicit topic scope. Scope indicator flips from a Search icon to a labeled pill. Queries bias to the active topic. Absorbs kitchen 24m — the bar IS the picker.

Engineering

pkce

AI answer will appear here after
Dump to memaxmemax-team

PKCE flow — native apps

Security › OAuth

Proof Key for Code Exchange, required for mobile OAuth…

OAuth flows reference

Security

Authorization code, client credentials, device flow…

inside a topic · bar scoped to Engineering

38m. Errors & empty — inline, never a modal

Recall errors render inline in zone ④. AI errors render inline in zone ② — sources below still show. No results = empty state pointing at the Remember row.

zimbabwe onboarding

AI answer will appear here after
Dump to memax

No memories match this query yet.

Save it above to start remembering.

no results · Remember row is the primary affordance

auth architecture

AI answer will appear here after
Dump to memaxmemax-team

Recall failed.

Network error — your query is still here.

recall failed · inline error · retry available

auth architecture

AI answer failed — sources below are still available.
Dump to memaxmemax-team
1

Agent Identity Architecture

Authentication98%

Auth = identity…

2

Security Rules

Security74%

Owner isolation at data layer…

ai failed · sources still usable · quiet fallback

38n. Mobile — Notion-style mirror, photo capture, three exits

Notion mobile pattern. Bar stays above the keyboard as the thumb zone. On focus, a mirror input slides down from the top of the screen (eye zone) — the user sees what they’re typing in large type while their thumbs stay comfortable. Result surface sits between them, scrollable. Edge-to-edge, no card chrome. One persistent bottom bar only — the top mirror is read-only context, not a second editable bar.

Photo capture — a 📷 icon lives in zone ① between the textarea and the send button. It appears whenever the input is empty AND no files are staged (matching production bar-right-portal behavior). Tapping opens the native iOS / Android picker → photo library, files, or live camera. Selected media stages as a chip and the bar locks to dump (recall hidden, send becomes ↑). Same eager-upload flow as desktop drag-drop.

Three exits:

  • X button in the right-side action cluster of zone ① — layered (peels back one state), same role as desktop esc
  • Swipe down on the source list when scrolled to top — native iOS modal dismiss gesture
  • Tap the scrim above the mirror — returns to resting dock, query preserved

asking

what do I know about auth architecture

Remember “…”memax-team
1Agent Identity98%
2MCP OAuth JWT91%
3Security Rules74%
auth architecture
keyboard

compose · mirror echoes query (top = eye zone)

Memax uses a dual auth architecture [1]: API keys carry agent_name for CLI/MCP, while OAuth JWTs embed it as a claim [2]. Owner isolation [3] at the data layer.

Remember “…”memax-team
1Agent Identity98%
2MCP OAuth JWT91%
3Security Rules74%
auth architecture
keyboard

compose · AI streams into eye zone

asking

what do I know about auth architecture

Remember “…”memax-team
1Agent Identity98%
2MCP OAuth JWT91%
3Security Rules74%
swipe down
auth architecture
keyboard

exit · X · swipe-down · tap scrim

38o. Hint bar — 24m style, <kbd> for glyphs only

24m pattern: thin row, plain text + single-glyph chips, never chains of boxy pills. Left primary, right close. Desktop only — mobile has no keyboard hints.

deploy

recall⌘↵rememberescclose

typing

deploy

open⌘⌫forget↑↓navigateescclose

row highlighted

command

/

run↑↓navigateescclose

command mode

38p. State machine — single flow, layered esc
StateZone ②Zone ③Zone ④Transition
rest→ engaged on focus/keystroke
engaged (empty)→ typing on first char
typingplaceholderRemember {query}keyword + FTS live→ searching on
composeplaceholderRemember rowkeyword + FTS livezone ① morphs to stacked layout
stagedRemember N fileslocks to remember (recall hidden)
searchingthinking (✦ breathe)Remember rowsemantic fades in over keyword→ streaming / complete / error
streaming (Pro)AI tokens + ✦ breatheRemember rowsources with [N] citations→ complete on stream end
completeAI + copy toolbarRemember rowsources + load more→ typing on new keystroke
remembering✦ Remembering…→ sent / error
sent✓ Sent · Undo (4s)→ idle (autodismiss) / undone
error✕ Couldn’t save · Retrycontent restored, user retries
commandcommand discovery / topic+hub target list→ typing on backspace past /
topic scopeplaceholderRemember (in-topic)topic-biased resultsset by route, cleared by chip X

Layered esc — peel back one layer per press:

  1. edited value ≠ recallQuery → restore value
  2. result surface open → dismiss results + clear recall
  3. textarea has text → clear it
  4. scope chip active → degrade chip back to raw slash or clear scope
  5. bar engaged on /home → return to rest
  6. otherwise → blur
38q. Deltas — what differs from section 37

Both sections are valid targets. This card is for the team reviewer — scan the deltas, pick a direction, then one of the two gets deleted during the production port.

AspectSection 37Section 38 (this)Why 38
Chromeflat bar-bg + bar-border24m glass · blur(24) · /0.92user explicitly said “I like 24m shadow/color/style”
Positiontop 16vh everywhererest 42vh on /home · docked elsewhere · engaged 24vhhero moment + middle-upper breathing room
Zones ②③ presencemutually exclusive · ② typing, ③ after ↵② always reserved · ③ always visiblecontinuity — user tracks the same surface evolving
Remember row lifecycledisappears after Enterstays visible, morphs through transient statesuser can always save, feedback is inline
Save feedbackimplied toast (not shown)inline zone ③ transients (replaces notification card)one fewer surface; container morphing
Hub badgeabsenton Remember row, all statesmulti-hub users need the target visible at save time
Mobilesingle surface, results cascade upwardNotion mirror — eye zone top, thumb zone bottomuser’s original ask; separates where you look from where you type
Slash commands7 (includes /import, /share, /remember, /settings)2 shipped (/topic, /hub) + /forget deferred/import ≡ drag-drop, /share doesn’t exist, /remember ≡ ⌘↵
Compose morph (24f)absentpresent · CSS Grid stacked layoutChatGPT auto-grow is load-bearing for long pastes
File/URL capture (24g)absentchips, drag-active, URL capturethe only non-text push path — can’t drop it
Topic scope indicatorinline #chip in input rowlabeled capsule on the leftmatches the command-scope visual (same shape, different label)
Load more / max heightunspecifiedmax-h-[60vh] + “+ N more” rowuser explicitly asked for a fixed max with pagination
Send button iconSparkle (lucide ✦) for recall, ↑ for commandunified ↑ · color changes by mode (violet ↔ dark)ChatGPT / Claude / Perplexity convention · frees ✦ back to AI-only (design system rule)
Deprecated inputsdetect-intent.ts deleteddetect-intent.ts deleted · ? prefix deleted · mode capsule tap deleted38 is explicit about all three — see callout in 38a

Things the two share: downward cascade, recall-default Enter, AI above sources, for remember, slash as a command palette trigger, literal Unicode glyphs in <kbd>, layered esc, intent detector deleted, kitchen 24m absorbed. The disagreements are all about presence vs absence: whether zones stay visible across phases, whether mobile duplicates the input, whether the save target is visible.

32. Dev Debugger

North star for the live Memax event tray used to inspect recall-heavy flows.

Floating Tray

memax debugger

Live

A floating event timeline for recall, AI, and remember flows.

Recall9
AI3
Remember2
System1
recall

Recall submitted

find the onboarding notes for Codex

AI auto-answer on · now

now
recall

Recall results ready

9 ranked memories across hubs

184ms · now

now
ai

AI answer completed

Synthesized answer with citations [1] [2] [3]

stream finished · 2s

now
remember

Remember saved

Launch checklist for the dev debugger

web push · 6s

now
Behavior Rules

Debugger is dev-only and hidden behind Dev Tools.

Recall is the primary trace: submit, fetch, results, panel open, AI answer.

Remember flows log both direct text saves and staged file uploads.

Tray stays global so the events remain visible while people interact with the bar.

6. Tree Panel

Topic tree — both navigator AND manipulation canvas. Three responsive modes (desktop pinned, desktop overlay, mobile bottom sheet). Supports memory → topic drops from every memory surface + topic → topic drag-to-reparent within the current hub. Kbd/touch/mobile accessibility via the ⋮ menu picker on each node (no dnd-kit KeyboardSensor). See manipulation rules block at the top of this file for cycle/depth/hub invariants.

22a. Desktop — pinned sidebar (content shifts right)

Full-height sticky sidebar. Logo stays fixed (same position always). Tree header aligns with CONTENT_TOP. ChevronsLeft collapses. Content push animated via motion.div width 0↔280. Hover left edge reveals overlay. Persisted.

Topics
Deployment23
Staging8
Production11
Rollback4
Authentication18
OAuth6
Tokens5
Go Patterns31
Database15
Frontend22
Team Docs9
3 unassigned memories

Content shifts right

Sidebar takes space in layout flow

22b. Desktop — hover-reveal overlay (peeking)

No toggle button. Hover left edge (12px zone, 150ms delay) reveals overlay. Starts at HEADER_TOP (not full height). » pins to layout flow. Mouse-leave dismisses after 300ms. Backdrop at 8% opacity.

Content stays full width

Hover left edge to reveal

22c. Mobile — bottom sheet (current production)

Code icon next to “Your Topics” page header (right side). Opens bottom sheet overlay with drag handle. Toggle is contextual, not in fixed header.

Your Topics

10 memories · 5 topics

Topics
Deployment23
Staging8
Production11
Rollback4
Authentication18
OAuth6
Tokens5
Go Patterns31
Database15
Frontend22
Team Docs9
3 unassigned memories
22d. Mobile — master/detail push (target, Notion-style)

Tree IS the page content. Tap a topic → detail slides in from right. Back arrow returns. No separate toggle button. No overlay. This is the target pattern for mobile topic navigation.

Your Topics

147 memories · 6 topics

Deployment23
Authentication18
Go Patterns31
Database15
Frontend22
Team Docs9

Topic detail

Memory list here

22e. Tree node states

Collapsed (has children)

Deployment23

Active (selected)

Deployment23

Leaf (no children)

Go Patterns31

Drop target (DnD hover)

Database15
22f. Empty states — pre-dream, dreams off

Waiting for first dream

Topics appear after your first dream

memax organizes your memories during the nightly dream cycle.

Dreams disabled

Dreams are turned off

Enable dreams to organize your knowledge.

Production files: topic-tree-panel.tsx (provider + pinned/peek), topic-tree-content.tsx (data + empty states + root drop row), topic-tree-node.tsx (recursive node + grip + ⋮ menu + invalid-drop ring), topic-dnd-provider.tsx (shared DndContext, memory + topic discriminator, dropTargetsActive gate), topic-dnd-hooks.tsx (useDraggableMemory, useDraggableTopic, useDroppableTopic, TopicDragContext), use-topic-move.ts (mover hook), topic-move-picker.tsx (kbd/touch a11y path). layout.tsx (FloatingTreeToggle, SidebarSlot, BrandLogo).

Desktop (Notion/Linear-style floating-toggle model): no permanent rail — main content goes full width when the tree is closed. FloatingTreeToggle renders a small ChevronsRight button to the right of the fixed BrandLogo when `!isPinned && !isDragSessionOpen`, opening the tree on click. SidebarSlot animates width 0↔280 based on `isPinned || isDragSessionOpen`; the close button lives inside PinnedTreeSidebar's top row (`marginTop: 32 + h-12 items-center`) on the SAME vertical band as the fixed BrandLogo, so open→close has no vertical jump. No redundant “Topic Explorer” header label — the content and close button already identify the sidebar. Tree is always mounted in SidebarSlot so droppables register from page load; dropTargetsActive on TopicDragContext (`isPinned || isDragSessionOpen`) gates collision detection so collapsed tree nodes don't match drops in main content area. isPinned persisted under `memax_tree_pinned` localStorage key (legacy name, semantics are now "is the tree open"). No hover-peek.

Typography (kitchen row-title rule, memory `b1f8fa7a` / §29 rules 32-34): 14px for ALL row titles — hub row and topic rows share the same size. Hierarchy via opacity + weight, never size. Hub row is the strongest entry: `text-[14px] font-medium text-fg-1` — reads as the tree root anchor. Topic rows fade as scaffolding: inactive `font-normal text-fg-2`, active `font-medium text-fg-1`. Trailing memory counts are 12px `text-fg-4` metadata. No 15px bumps, no semibold, no size-based hierarchy.

Mobile: Code icon in page header (topic-grid.tsx) opens bottom sheet via TopicTreePanelOverlayHost. No rail, no SidebarSlot — mobile uses its own drawer surface. Future target: master/detail push (22d).

7. Hub Experience

Team hub: ambient workspace switching from the top-right chrome beside the avatar, plus attribution, management, and invite. Personal by default, team by intent.

20a. Hub switcher — top-right anchor

Anchored beside the avatar in the top-right chrome. Single-hub users see no chip. Multi-hub users get a persistent current-hub chip; the avatar dropdown stays account-only. Active hub shown with checkmark. Users icon for team hubs. “Create team hub” link at bottom.

Single hub user (no hub chip)

D

Derek

52 memories

🌙 Dark
EN / 中

Multi-hub user (chip beside avatar, not inside dropdown)

D

Derek

backend-team · 31 memories

Personal
Personal52
backend-team
backend-team31
design-team
design-team12
Create team hub
🌙 Dark
EN / 中
20b. Memory attribution — hub badge on rows

Team memories show lucide Users + hub name (text-[12px] text-fg-4). Personal memories show no badge (default = no label). Only visible when user has 2+ hubs. Processing row unchanged. Flat dividers, icon containers match production.

Recent
D
Derek via Claude CodeAPI authentication flow3d

OAuth2 refresh flow with token rotation strategy...

architecture·Personal auth notesweb5d
Deploy runbook updateorganizing...
20c. Hub-aware bar behavior

Push toast: “Sent — memax will organize.” (personal) or “Sent → hub-name — memax will organize.” (team). Recall results show hub attribution in cross-hub mode. Bar clears immediately after submit (chat-style, no blocking confirmation).

Push to personal hub (toast, no hub label)

Sent — memax will organize.

Push to team hub (toast with target)

Sent → backend-team — memax will organize.

Recall results (cross-hub, shows attribution)

API authentication flowbackend-team92%
My personal auth notes87%
Auth middleware architecturebackend-team84%
20d. Hub creation (settings Account tab)

Inline form in settings dialog Account tab, below existing hub list. Name input → auto-generated slug → Create. Hub appears in pill dropdown immediately. Also accessible via dropdown “Create team hub” link.

Team hubs

Open a hub to manage members, roles, invites, permissions, and ownership.

Create team hub

Your team will share a knowledge base. Invite members after creation.

20e. Hub management (settings, expandable per hub)

Click team hub in settings → expands to show members + invite lifecycle. Rename hub (Pencil icon, inline edit). Members sorted by role with (you) marker. Owner/admin: hover-to-reveal X for member removal with destructive confirmation. Role badge becomes an inline editor for Admin/Contributor/Viewer. Invite management: list active invites with role badge, expiry, copy, regenerate, revoke. Ownership transfer is a dedicated owner-only section with pending state.

backend-teamteam

5 members · 31 memories

Your role

Owner

Members

5

Shared memories

31

Members

D
Derek(you)2w ago
S
Sarah1w ago
J
James3d ago
A
Alex3d ago
P
Priya1d ago

Ownership

Transfer pending

Sarah needs to accept ownership before the role changes.

Expires Apr 14, 2026

Member removal confirmation

Remove James?

Invite links

https://memax.app/invite/a8f3c2e9...
Contributor·Expires Apr 14, 2026
https://memax.app/invite/b7d4e1f0...
Viewer·Expires Apr 16, 2026

Revoke invite confirmation

Revoke this invite link?

Contributor permissions

Memory deletion

Contributors can delete memories they contributed

Own memories only

Manage topics

Contributors can create and organize topics

Danger zone

Permanently delete backend-team and all 31 memories. 4 members will lose access.

Personal memories are not affected.

20f. Invite accept page (/invite/[token])

Centered card, same visual language as login. Shows hub name, memory count, member count, inviter. Single CTA. Expired tokens show error state.

Join backend-team

31 shared memories

5 members

Invited by Derek

Search and contribute to
your team's shared knowledge.

Expires Apr 12, 2026

20g. Hub-scoped inbox states

Inbox is hub-scoped. Shows unassigned memories for active hub. Clean state uses DREAM_PURPLE ✦. All states match production patterns (Clock/Inbox icons, flat dividers, icon containers).

Team inbox (has items)

Inbox4

Will be organized in the next dream cycle

Will be organized in the next dream cycle

note·Staging deploy checklist
D
2h
note·Meeting notes — API review
S
5h

Inbox clean

Inbox0

Your memory is clean

20h. Hub creation states

All states for hub creation: loading (breathing ✦), error (inline message), success (auto-switch + open management). Maps to teams-section.tsx HubCreateForm.

Creating

Error

Hub name already taken

Success → management

design-team createdAuto-switches + opens management
20i. Empty hub onboarding (just created, alone)

When hub has 1 member (owner) and 0 memories, replace member list with prominent invite CTA. This is the first thing an owner sees after creating a hub.

design-team

1 member · 0 memories · just now

Invite your first teammate

Share a link to start building shared knowledge.

20j. Invite link states + role picker

Four states: not generated (CTA button), generating (breathing ✦), generated (code + copy + expiry), error (inline message). Plus role picker shown before generating — select Contributor/Viewer/Admin. Maps to teams-section.tsx InviteCreateButton.

Not generated

Generating

Generated

https://memax.app/invite/a8f3c2e9...

Expires Apr 14, 2026

Error

Could not generate invite link.

Role picker (before generate)

Invite as

20k. Invite accept page — all states

Logged in: single CTA. Not logged in: “Sign in to join” → OAuth → return to page → auto-accept. Expired/used show error. Accepted shows redirect.

Logged in

Join backend-team

5 members · Invited by Derek

Not logged in

Join backend-team

5 members · Invited by Derek

→ OAuth → returnTo in localStorage → auto-accept on return

Expired

Invite expired

Ask the hub owner for a new link.

Accepted

Joined!

Redirecting to your team's memory hub...

20l. Delete hub confirmation

Two-step: button → destructive confirmation with memory count + member impact. “Personal memories are not affected.” After delete: return to list, auto-switch to personal.

Default

Confirmation

Permanently delete backend-team and all 31 memories. 4 members will lose access.

Personal memories are not affected.

20m. Role-based management views

Owner/Admin: full management — rename hub, remove members (hover-to-reveal X), invite lifecycle (list/revoke/regenerate/role picker), delete hub. Contributor/Viewer: read-only member list + leave only. Roles affect ACTIONS not VIEWS — all roles see the same memories.

Owner / Admin

D
Derek(you)Owner
S
SarahAdmin
J
JamesContributor
Rename hub

Full control: rename, remove members, manage invites (list/revoke/regenerate), delete hub.

Contributor / Viewer

D
DerekOwner
S
SarahAdmin
J
JamesContributor

No invite button. No rename. No delete. No remove. Read-only member list + leave only.

20n. Ownership transfer

Two-step transfer only. Owner selects an existing member, the target explicitly accepts, and the previous owner is automatically demoted to Admin. No same-screen instant owner swap.

Owner view

Transfer pending to Sarah. Derek stays owner until Sarah accepts.

Sarah

Admin · Expires Apr 18, 2026

Target member view

Sarah sees one focused decision: accept ownership or decline.

Take ownership of backend-team

Derek will become Admin after you accept.

20o. Hub Scope Filter — view + push

Hub switcher = scope filter. Selection controls BOTH what you see (read) AND where you push (write). “All” is read-only aggregation — push defaults to personal. Single-hub users see no filter UI.

Hub scope selector (in dropdown)

D

Derek

All · 95 memories

Push → Personal

Scope model

AllRead: everything·Push: → personal
PersonalRead: personal only·Push: → personal
TeamRead: team only·Push: → team

Recall always crosses all hubs.

Regardless of view scope, recall searches everything accessible. Hub badges on results show origin.

20p. Main-view hub identity anchor

The “you are here” anchor for multi-hub browsing. Hub identity now lives in the persistent top-right chrome beside the avatar, not above each page h2. h2 forks on hub kind: personal → “Your Topics” (warm), team → “Topics” (neutral).

Single-hub user (no chip)

Your Topics

52 memories · 5 topics

Zero noise when hubs.length < 2. h2 is warm “Your Topics”.

Multi-hub — personal active

Your Topics

52 memories · 5 topics

Hub context lives in persistent top chrome, while h2 keeps the warm possessive “Your Topics.”

Multi-hub — team active

Topics

31 memories · 3 topics

Team h2 drops to neutral “Topics” — “Your Topics” inside a shared team hub is semantically off. Role tag stays inline on the chip (admin / owner only).

Interactive — click top-right chip to open the switcher popover

Topics

31 memories · 3 topics

Popover from the global top-right anchor

Switch hub
Personal
PersonalPersonal52
backend-team
backend-teamAdmin31
design-team
design-teamContributor12
Create team hub

Contract

· Chip renders in the top-right chrome beside the avatar, not inside page bodies. The page h2 stays responsible only for page identity.

· Chip is hidden entirely when hubs.length < 2. Single-hub users see nothing — zero chrome.

· Click opens the hub switcher popover from the global top-right anchor. The avatar dropdown no longer repeats the hub list.

· Inside the switcher list, both personal and team keep the leading icon. Only personal keeps the text tag, so users can spot the default home quickly without duplicating team identity.

· h2 forks on hub kind: personal → “Your Topics” (warm, possessive), team → “Topics” (neutral, shared). Chip carries hub identity, h2 carries page identity. No more {name}'s Topics” interpolation fork in i18n.

· Role tag shown inline only for Owner / Admin. Contributors & Viewers see just the hub name — keeps the chip quiet for readers.

· Production mapping: app/(app)/layout.tsx (top-right chrome anchor), settings-panel.tsx (remove duplicated hub list), topic-grid.tsx (keep t.topics.titlePersonal vs t.topics.titleTeam fork only).

20q. Push target indicator

Bar shows where push goes. Personal scope: no indicator (default). Team scope: hub name pill right of input. All scope: “→ Personal” default, user can tap to change. Push toast always confirms target.

Bar — team scope (push target shown)

type to remember, ask to recall...backend-team

Bar — personal scope (no indicator needed)

type to remember, ask to recall...

Bar — “All” scope (push defaults to personal)

type to remember, ask to recall...Personal

Push toasts by scope

Sent — memax will organize.(personal scope)
Sent → backend-team — memax will organize.(team scope)
Sent — memax will organize.(“All” scope → defaults to personal)

Push target rules:

• Personal scope → push to personal hub (no indicator)

• Team scope → push to that team (arrow pill indicator)

• All scope → push to personal (muted indicator, tappable to switch)

• Single-hub users → no scope UI, no push indicator

20n. Hub switch feedback

Uses existing bar notification system. Success type, 2s auto-dismiss. Avatar subtitle updates immediately. No new component.

After switching hub in dropdown, bar shows confirmation notification. Avatar subtitle updates immediately.

Switched to backend-team
remember or ask...

Success type, 2s auto-dismiss. Uses existing bar notification system. No new component needed.

7r. Profile — display name edit

In settings Account tab. Click name → inline edit → save on blur/Enter. display_name is the single source of truth (seeded from OAuth, editable here). Used in attribution rows, settings panel, avatar dropdown.

J

yejiahaoderek@gmail.com

free

Inline edit: no modal, no save button. Bottom border appears on hover, solidifies on focus. Save on blur or Enter. PATCH /v1/auth/me with display_name. Optimistic update in useAuth.

Industry pattern: Slack, Notion, Linear, Vercel, GitHub, Figma all use top-left scope selector. 2-click standard. Full context swap on switch.

Routing: switched hub sets read context only. Push writes require explicit targeting (for example X-Hub-ID) or fall back to personal. Recall reads across all accessible hubs.

New user invite flow: Click link → see hub info → “Sign in to join” → OAuth with returnTo → auto-accept on return → switch to hub → land in /home.

20p. HubHeader — full-bleed banner, three aurora modes

What it is.A page-level identity header that renders on /home, /topics, and any hub-scoped landing. Replaces the old “你的主题 · 55 条记忆” heading that floated between section cards — that was hub metadata mislabelled as a section title.

One component. HubHeaderBanner with an aurora prop. Full-bleed banner bleeds under the app top bar (logo + hub dropdown + user avatar). Avatar hangs across the banner edge, title/greeting /stats sit on --background.

Three modes. signature (default) — static memax-branded gradient, stable across team members and time of day. time — drifts with time of day, personal-hub opt-in only, team hubs ignore. none — plain typographic fallback, no banner, no aurora. See 20p-f for the Settings › Appearance switcher.

Rules honored: no left-colored-borders, no toolbar in the header, no mode change, no decorative chrome, no content in the banner top-right (that area is reserved for the app shell). Greeting is contextual — dream deltas, inbox overflow, review-needed, quiet-night. Never generic time-only.

20p-a. Three aurora modes — plain / signature / time

One component, HubHeaderBanner, with three background treatments via the aurora prop. Shipping default is signature.

  • Plain (aurora="none") — no banner layer. Pure typography on --background. Same treatment as 20p-f. Minimal. For users who find any ambient background noisy, or as a fallback if the browser doesn't support gradients/blur.
  • Signature (aurora="signature", default) — static memax-branded signature gradient. Violet + soft magenta. Stable across time, stable across team members. This is the production default.
  • Time (aurora="time") — time-of-day driven gradient. Personal hub opt-in only (Settings › Appearance › “Match time of day”). Team hubs cannot enable this — the time signal is a personal concept, not a shared one.

Plain — no aurora, pure typography

D
Personal

早上好,Derek。memax 昨晚整理了 4 个主题。

247 条记忆·12 个主题·3 待整理·1 条待确认

Signature — static memax gradient (recommended default)

D
Personal

早上好,Derek。memax 昨晚整理了 4 个主题。

247 条记忆·12 个主题·3 待整理·1 条待确认

Time — shifts with time of day (personal opt-in)

D
Personal

早上好,Derek。memax 昨晚整理了 4 个主题。

247 条记忆·12 个主题·3 待整理·1 条待确认
20p-b. Centered-notion layout — signature aurora, 200px

Direct Notion translation. Avatar centered, hanging 50/50 across the banner edge. Title/greeting/stats stack below left-aligned. Title bumps to 28px for the hero feel.

Use for hero states (onboarding, return-after-absence) where the hub identity should take a full-width moment. Daily workspace uses the inline-right variant from 20p-k.

D
Personal

欢迎回来,Derek。最近都想些什么?

312 条记忆·14 个主题
20p-c. Mobile compact — all three modes (140px)

Mobile: banner drops to 140px, avatar to 56px, title to 20px, greeting to 13px. Plain mode has no banner, just a small top padding and the same typographic content.

Plain

D
Personal

晚上好,Derek。收件箱里有 28 条等你整理。

171 条记忆·4 个主题·28 待整理

Signature

D
Personal

晚上好,Derek。收件箱里有 28 条等你整理。

171 条记忆·4 个主题·28 待整理

Time

D
Personal

晚上好,Derek。收件箱里有 28 条等你整理。

171 条记忆·4 个主题·28 待整理
20p-d. States gallery — 9 canonical states on signature

The same HubHeaderBanner across every data state: morning-dream-delta, afternoon-clean, evening-inbox-overflow, deep-night-quiet, first-time, return-after-absence, team-morning, team-review-needed. All on the signature aurora (the shipping default), so you can see how the same surface adapts content without changing its skin.

Notice: team-morning and team-review-needed render a square avatar + member cluster beside the hub name. Personal hubs render a circle avatar and skip the cluster. Stats row adapts: first-time collapses to a setup CTA, team states include member count, inbox-heavy states include an inbox number, review-needed includes a pending-review count.

Morning — dream deltas

D
Personal

早上好,Derek。memax 昨晚整理了 4 个主题。

247 条记忆·12 个主题·3 待整理·1 条待确认

Afternoon — clean

D
Personal

下午好,Derek。一切井井有条。

143 条记忆·6 个主题

Evening — inbox overflow

D
Personal

晚上好,Derek。收件箱里有 28 条等你整理。

171 条记忆·4 个主题·28 待整理

Deep night — contemplative

D
Personal

深夜了,Derek。深夜的想法最清澈。

89 条记忆·3 个主题

First-time / empty

D
Personal

欢迎使用 memax,Derek。

开始记录你的第一条吧

Return after absence

D
Personal

欢迎回来,Derek。最近都想些什么?

312 条记忆·14 个主题

Team — morning + activity

🛠
backend-team
D
Z
A
+2

早上好,Derek。团队今天多了 6 条新记忆。

147 条记忆·6 个主题·5 成员·12 待整理

Team — review needed

🛠
backend-team
D
Z
A
+2

Derek,有 3 条合并冲突等你决定。

147 条记忆·6 个主题·5 成员·4 待整理·3 条待确认
20p-e. Full-page context — app top bar + banner + Recent below

End-to-end. The real app has a top bar: memax logo top-left, hub dropdown + user avatar top-right. The banner bleeds under this top bar — the aurora is the ambient backdrop for the app chrome, not something that starts below it.

On signature/time modes, top-bar elements get frosted backdrop-blur pills so they stay legible on the gradient. On plain mode, the pills drop away — elements sit on --background normally. Compare the three modes below using the same mock state (team hub, morning).

Plain — no aurora under top bar

memax
D
🛠
backend-team
D
Z
A
+2

早上好,Derek。团队今天多了 6 条新记忆。

147 条记忆·6 个主题·5 成员·12 待整理
新鲜记忆过去 12h
选择
… memory rows …

Signature — default production treatment

memax
D
🛠
backend-team
D
Z
A
+2

早上好,Derek。团队今天多了 6 条新记忆。

147 条记忆·6 个主题·5 成员·12 待整理
新鲜记忆过去 12h
选择
… memory rows …

Time — personal opt-in (morning bucket)

memax
D
🛠
backend-team
D
Z
A
+2

早上好,Derek。团队今天多了 6 条新记忆。

147 条记忆·6 个主题·5 成员·12 待整理
新鲜记忆过去 12h
选择
… memory rows …
20p-f. Settings › Appearance — the aurora mode switcher

How users change aurora mode. The control lives in Settings › Appearance, not on the banner itself (the banner has no chrome). Three radio options + live preview. Preference is user-global (one setting per user across all hubs, not per-hub).

Team hubs auto-downgrade time signature at render time. The setting stays at timefor the user — they'll see the time drift again whenever they view a personal hub. No feature flags, no hub-level override in the data model: a simple two-line rule in the render path.

Appearance

Hub header style

How the banner at the top of each hub page renders.

Preview

D
Personal

早上好,Derek。memax 昨晚整理了 4 个主题。

247 条记忆·12 个主题·3 待整理·1 条待确认
20p-g. Greeting copy matrix — brand voice decisions

Greeting priority when multiple states apply: first-time → review-needed → dream-deltas → inbox-overflow → clean → time-only. Lower-priority states roll into the stats line instead. Variety within a state: 2–3 alternates, deterministically rotated per day.

SituationGreeting (zh)Greeting (en)
Morning + dream deltas早上好,Derek。memax 昨晚整理了 4 个主题。Good morning, Derek. Dreams reorganized 4 topics overnight.
Morning + clean早上好,Derek。一切井井有条。Good morning, Derek. Everything's in order.
Afternoon + inbox overflow下午好,Derek。收件箱里有 28 条等你整理。Good afternoon, Derek. 28 items waiting in your inbox.
Evening + review needed晚上好,Derek。有 3 条合并冲突等你决定。Good evening, Derek. 3 merge conflicts need your call.
Deep night + clean深夜了,Derek。深夜的想法最清澈。Late night, Derek. Quiet thoughts travel far.
Return after 7d absence欢迎回来,Derek。最近都想些什么?Welcome back, Derek. What have you been thinking about?
First-time / empty欢迎使用 memax,Derek。Welcome to memax, Derek.
Team + team activity早上好,Derek。团队今天多了 6 条新记忆。Good morning, Derek. Team added 6 memories today.
Team + review neededDerek,有 3 条合并冲突等你决定。Derek, 3 merge conflicts need your call.

Rules:warm but not cheesy · acknowledge state, don't hype · comma after name in English · never prefix with Hi / Hello · never include hub name in the greeting (it's already above) · bilingual parity (not translations — each language gets its own copy pass).

i18n namespace (new): hub.greeting.* with ~15 keys covering the situation matrix plus 2–3 alternates each.

20p-h. Banner design rules
1.

One component, three modes. HubHeaderBanner owns all three aurora treatments via the aurora prop: "none" (plain typography), "signature" (static memax gradient, default), and "time" (opt-in time-of-day drift). No separate components, no feature-flag branches in callers.

2.

Signature is the default. Stable, memax-branded, consistent across time and team members. Time-drift is a personal-hub-only opt-in (Settings › Appearance › “Match time of day”). Team hubs cannot enable time-drift — time-of-day is a personal concept and makes no sense for a shared workspace.

3.

Plain is the escape hatch. aurora="none" renders pure typography on --background — identical to the 20p-f borderless treatment. No banner layer, no halo on the avatar, no negative margin. For users who prefer minimal ambient, or as a fallback when gradients/blur don 't render (e.g. reduced-motion, old browsers).

4.

Banner bleeds under the app top bar. The app shell's top bar (memax logo left + hub dropdown and user avatar right) is absolutely positioned on top of the banner. The aurora is the backdrop for the top bar elements — they are NOT in a separate bar above it. On signature/time modes, top-bar elements wear frosted backdrop-blur pills to stay legible. On plain mode, the pills drop.

5.

No content in the banner's top-right. That area is reserved for the app shell (hub dropdown + user avatar). The banner itself renders no time icon, no Change button, no Pin button. Aurora is ambient — the user doesn 't customize it and doesn't need controls.

6.

Shorter than Notion. 180px desktop default (200px for hero states like onboarding), 140px mobile. Notion's 280px assumes a rarely-visited project page. memax /hub is a daily workspace — every pixel above the fold is a budget.

7.

Fade, don't cut. Bottom 25% of the banner fades to --background via a gradient overlay (not mask-image — the mask interferes with the avatar halo). No hard horizontal line between banner and content.

8.

Avatar crosses the edge. Inline-right: 25% above / 75% below (title lands on clean background). Centered-notion: 50/50 (dramatic hang). Negative margin-top on the avatar wrapper; everything else in natural flow. Plain mode: no negative margin, no halo.

9.

Title never touches the banner. In both layouts the title sits entirely below the banner bottom edge, on --background. Aurora is ambient context, not a backdrop for typography — placing the title on the aurora costs legibility and reduces the semantic weight of the name.

10.

HubHeaderAurora (the card) is deprecated. The rounded glass card variant remains in the kitchen for comparison only. In production: delete it, use HubHeaderBanner with the default aurora="signature" and layout="inline-right".

20q. Hub switch — always land in memory view

The decision. Topics are hub-scoped, memory detail is hub-scoped, and even a plain Recent row is still content from the old hub. Leaving the user on whatever route happened to be open creates loopholes: some switches re-scope in place, others navigate, and memory detail can point at the wrong hub context.

The fix.Switching hub always navigates to the new hub's /memories view. You were looking at memories before; now you're looking at the new hub's memories. One destination, one rule, no route-specific exceptions.

20q-a. Transition frames — switch hub → fade → new hub's Memories
Tap frames:
Your TopicsAuth & Security
Auth & Security

JWT tokens, OAuth flows, API key management…

23 memories · 3 subtopics

API key rotation policy3h
OAuth refresh token audit5h
SAML callback boundary fix7h
JWT validation middleware1w
D
Personal

Frame 1 is the fade transition (actual production is an opacity crossfade over ~220ms, not a topic remap and not a loading spinner). Frame 2 lands the user on the new hub's memories view so hub switches always have one destination and one mental model.

20q-b. Dropdown hint — tells you what you'll see, not what you'll lose

Subtle inline hint in the hub switcher dropdown when the user is currently inside a topic. Tells them what they'll see, not what they'll lose.

Without hint (default state)

Switch hub

Inside a topic — shows what you'll see

Switch hub

23. Agent Management

Unified agent identity, API keys, config sync, activity stats. Settings > Agents tab northstar.

23a. Northstar — Agent Overview

Each connected agent gets a unified card. Identity icon with accent color, status dot (active/idle/inactive), stats summary. Expand for API keys, config files, rename, revoke.

claude-codevia API key

API Keys

mxk_a1b2····All hubs2m ago
mxk_c3d4····memax-team1h ago

Synced Configs· 30 extracted

CLAUDE.mdv1
CLAUDE.md5v3
MEMORYindex8v1
Design Patternsfeedback3v1
Container Morphingfeedback2v1
CLAUDE.md12v2
147 memories pushedLast active 2m ago
23b. Rename — inline edit

User can customize the display name shown in memory attribution and across the app. The agent slug (claude-code) is immutable — set during memax setup. Priority: user custom → AGENT_NAMES map → raw slug.

Display mode

Claude Code

claude-code

Editing mode

claude-code

Enter to save

Display name is user-customizable. Slug (claude-code) is immutable, auto-set from agent type during memax setup. Display name flows to memory attribution.

23c. Revoke & Disconnect

Three levels of removal: revoke single key, revoke all keys for an agent, or full disconnect (keys + configs). Memories are always preserved — they belong to the user, not the agent.

Normal state

mxk_a1b2····All hubs2m ago

Revoke confirmation

Revoke mxk_a1b2? Agent will lose access.

Disconnect agent (revoke all keys + delete configs)

Disconnect Claude Code? 2 keys and 6 configs will be removed. Memories are preserved.

Two-stage confirmation pattern. Revoking a single key is granular. “Disconnect agent” revokes all keys + deletes all configs for that agent. Memories pushed by the agent are always preserved (they belong to the user, not the agent).

23d. Activity Status

Status derived from last_usedon API keys. No MCP polling needed — every API call updates the timestamp.

Just active2m agoObserved in the last 5 minutes
Active recently3d agoObserved recently, not necessarily live
ConnectedConnected, but no recent observed activity
23d2. Keys without agent identity

Orphan keys stay visible and explain why pushes look human until the user reissues them as an agent key.

Keys without agent identity

These authenticate as you. Memories pushed with them appear as your own.

mxk_abcd····hatch-agent-v3
23d3. Credential class pill
claude-codevia API key
claude-codevia OAuth
claude-codevia session
23d4. Unnamed agent remediation

Unknown-slug OAuth grants stay usable, but the card becomes clearly remediable instead of pretending it has a stable identity.

Unnamed agentActive recently
unknownvia OAuth
This connection was created without a stable agent identity. Disconnect this credential and have the agent reconnect through MCP OAuth — its registration will be validated.
23e. Agent Identity System

Each agent type gets a unique icon + accent color. Consistent across attribution, settings, and management surfaces. Icon is mapped from agent_name slug, not user-configurable (keeps visual consistency).

Claude Code

claude-code

Claude

claude-ai

Cursor

cursor

Codex

codex

Gemini CLI

gemini

GitHub Copilot

copilot

Windsurf

windsurf

OpenClaw

openclaw

OpenCode

opencode

Other

generic

Memax Agent

memax

23f. Empty State — no agents connected

Loading

No agents

Connect your first agent

Claude Code, Cursor, Windsurf, Copilot, Codex, Gemini

npm i -g memax-cli && memax setup

Data model

Agent identity is derived from api_keys.agent_name — set during memax setup. Each agent type can have multiple keys (per-hub, per-device). Activity status from api_keys.last_used.

Display name priority: user custom → AGENT_NAMES map → raw slug. Stored as agent_display_name on api_keys (future migration). Unified constant shared across memory-row, settings, and attribution.

New endpoint needed: GET /v1/agents/summary — aggregates api_keys + configs + memory counts per agent_name.

Production files: agent-configs-section.tsx, settings-dialog.tsx, memory-row.tsx. API: GET /v1/auth/api-keys, GET /v1/configs.

10. Dream Experience

Dreams show up across memax — while they are happening, after they finish, and anywhere they leave something worth your attention.

10a. dream states

dreams haven't run yet

They run nightly to organize your memory

dreaming…

Scanning 247 memories

Last night · 247 memories
3 notes merged
1 contradiction found
2 notes archived

Your memory is clean

dream failed

Processing timed out. Your memories are safe.

dreams are turned off

10b. dreams around memax — the morning-after flow

dreams never live on a dedicated page. You open memax the morning after an overnight dream and the signal is already where it matters: one ephemeral line near the bar, one row-accent on the items that moved, one N new pill on the topic cards that grew.

No aggregate “what changed” card, no review queue, no dream panel. Three cooperating surfaces, each doing one job.

1. Bar-adjacent notification — ephemeral, single line

memax is dreaming

scanning 247 memories

remember or ask…

while running

dreams updated 4 topics · view

Auth · Deployment · Frontend · Docs

remember or ask…

completed overnight

dreams are clean — nothing to review

last night · 247 memories

remember or ask…

clean run

Auto-dismisses ~6s after complete or on first interaction. Tap completed state → scrolls to Recent with a 12h filter applied. That's the entire “review entry point.”

2. Recent — global review feed with row accents on moved items

Recent12h
Copy 5 · Select
API key rotation policy updateAuth & Security3h
OAuth refresh token auditAuth & Security5h
SAML callback boundary fixAuth & Security7h
Staging rollback script v2Deployment9h
RSC caching notesFrontend11h
Fly.io health check tuningDeployment2d
Reranker cost analysisAuth & Security3d

Each dream-touched row gets a 2px signature-muted left accent and an inline topic destination. The accent is the onlydream signal — no header, no banner, no separate “what changed” card. Select any wrong ones → existing global batch toolbar → move.

3. Topic grid — N new pills on cards that grew

🛡Auth & Security3 new

JWT tokens, OAuth, API keys

23 memories

🚀Deployment2 new

Blue-green, rollback, staging

47 memories

📐Frontend1 new

RSC, Tailwind, Radix

22 memories

💾Database

Connection pooling, migrations

15 memories

📦Go Patterns

Error handling, context

31 memories

📝Team Docs

Onboarding, code review

9 memories

The N newpill is the spatial announcement — it says “this topic is where to look.” Inside the topic, delta becomes row-accent only (section 29d). The count never re-appears.

10c. Conflicts live in inbox, not a dedicated queue

When the dream engine finds a contradiction it can't resolve, it lands in the inbox as a conflict row. Tap → expands inline into a simple decision surface (two notes, four buttons). No separate review queue, no card stack, no ceremony.

The inbox already exists. The dream engine just writes into it. Each conflict is its own inbox row — resolve one, it disappears, next conflict arrives as the next row.

Inbox3
Organize
New captured thought about RSC caching4h
Meeting notes from Monday standup1d

Tap the ✦ conflict row to expand inline. Two notes, four buttons — no card stack, no ceremony.

i18n keys: dreams.noRuns, dreams.running, dreams.completed,

dreams.clean, dreams.failed, dreams.disabled, dreams.merge,

dreams.archive, dreams.contradiction, dreams.review.*

dreams should feel gentle but clear. Let them glow while they are happening, settle when they are done, and only ask for attention when you really need to decide something.

29. Topic Redesign

Topics are the spatial browse surface. Three-tier topic cards, recall→topic bridge, 5-level drill navigation with inline subtopic groups, ⌘P picker, and orphan-memory handling at every depth. Dream activity lives in §36 Inbox — topic cards and topic detail are silent (no ✦ badges, no row accents, no delta headers). When the dream engine flags something that needs a decision, it surfaces as a review in Inbox, never here.

29a. Topics main view — scale-aware 3 modes

One component. The Topics page renders a single scale-aware view that picks its own density based on topicCount. The user never switches manually unless they want to override.

Recursive at every drilled level. The same A/B/C mode logic applies inside a topic too — if a topic has 200 direct subtopics, that level renders in Mode B (dense list), not Mode A (rich grid). Whatever scope you're in (hub root or 5 levels deep), the renderer asks the same question: how many children at this level?

  • Mode A — Personal (≤20 topics) — 3-col rich cards with description + 2-3 subtopic chips
  • Mode B — Team (20-80) — 4-col dense cards, name + last-touched only, search bar at top + Pinned section
  • Mode C — Enterprise (80+) — virtualized list, single row per topic, search-first + Pinned + Recently visited at top

Thresholds 20 / 80 are starting values — tune from real usage. Each demo below is the same component fed a different topic count.

29a-mode-a. Mode A — Personal grid (≤20 topics)

3-col rich cards. Each card carries icon + name + description (2-line clamp) + 2-3 subtopic chips + last-touched timestamp. Description and chips give “what's in there” before you click. No memory count. No subtopic count. Just ambient temporal context.

Your Topics · 8recent activity ▾
Engineering2h

Backend, frontend, infra — Go services, RSC patterns, Fly.io, GitHub Actions.

Auth & Security5h

JWT tokens, OAuth flows, API key rotation, boundary enforcement, secret scanning.

Frontend8h

React Server Components, Tailwind, Radix primitives, design tokens.

Database1d

PostgreSQL, pgvector, migrations, connection pooling, replication strategies.

Deployment1d

Blue-green, rollback procedures, staging automation, Fly.io machine health.

Ingest Pipeline2d

Content ingestion, chunking, embedding, deduplication.

Team Docs3d

Onboarding, code review, release process, incident runbooks.

Go Patterns5d

Error handling, context propagation, interface design, testing patterns.

29a-mode-b. Mode B — Team dense grid (20-80 topics)

4-col compact cards. Description and chips drop. Each card = icon + name + last-touched. Top of view: search input + sort dropdown + Pinned section. Default sort: by last-touched.

47 topics
recent ▾

Pinned

Engineering2h

All Topics

Auth & Security5h
Frontend8h
Database1d
Deployment1d
Ingest Pipeline2d
Team Docs3d
Go Patterns5d
CI/CD2d
Testing3d
Compliance4d
Redis5d
Logging6d
Monitoring7d
Vendor8d
Legal9d
29a-mode-c. Mode C — Enterprise list (80+ topics)

Single-row virtualized list. No card chrome. Each row = icon + name + last-touched (right-aligned). Header row shows count + ⌘K hint + sort. No local search input — the global command bar handles topic filtering (rule 25). Pinned cards + Recently visited cards sit above the virtualized list, which mounts only visible rows so 1000+ topics scroll smoothly.

247 topics
recent ▾

Pinned

Engineering2h

Recently visited

Auth & Security5h
Frontend8h
Database1d

All Topics · 247

Engineering2h
Auth & Security5h
Frontend8h
Database1d
Deployment1d
Ingest Pipeline2d
Team Docs3d
Go Patterns5d
Cache layers3d
Queue system4d
Observability5d
Vendor decisions6d
Post-mortems7d
API contracts8d
+ 233 more (virtualized)
29a-toggle. View toggle — user override (grid / dense / list)

Top-right of the topics view: a 3-icon toggle that lets the user override the auto-mode. Choice persists in localStorage per hub. Default = auto. Useful for power users who want dense list even on a small personal hub.

Your Topics · 8recent activity ▾
Engineering2h

Backend, frontend, infra — Go services, RSC patterns, Fly.io, GitHub Actions.

Auth & Security5h

JWT tokens, OAuth flows, API key rotation, boundary enforcement, secret scanning.

Frontend8h

React Server Components, Tailwind, Radix primitives, design tokens.

Database1d

PostgreSQL, pgvector, migrations, connection pooling, replication strategies.

Deployment1d

Blue-green, rollback procedures, staging automation, Fly.io machine health.

Ingest Pipeline2d

Content ingestion, chunking, embedding, deduplication.

Team Docs3d

Onboarding, code review, release process, incident runbooks.

Go Patterns5d

Error handling, context propagation, interface design, testing patterns.

29-pin. Pin flow — corner icon + shared-layout morph

Every topic card / row carries a Pin icon in its top-right corner. Empty state is text-fg-4 outlined, revealed only on hover / focus (desktop) or always visible (mobile). Filled state is text-fg-1with a 45° rotation — the icon looks “pushed in” when active.

Clicking the icon toggles pinned state with a shared-layout morph: the card animates from the All section up to the Pinned section (or back down on unpin) using framer-motion layoutId. Duration NORMAL (0.2s), easing EASE spring from @memaxlabs/ui/tokens/motion. The Pinned section height animates with AnimatePresence — when the last pin is removed it collapses to 0 cleanly (no empty-section flash).

Pinned

Engineering2h

All Topics

Auth & Security5h
Frontend8h
Database1d
Deployment1d
Ingest Pipeline2d
Team Docs3d
Go Patterns5d

Hover a card → the pin icon appears in the top-right. Click it → the card animates up to the Pinned section (or back down on unpin). Shared layoutId + the NORMAL spring transition from @memaxlabs/ui/tokens/motion.

Industry references: Linear's favorites, Notion's star, Apple Reminders. All three use a persistent icon affordance + a shared-layout animation rather than a dropdown action. We match that pattern because it keeps the surface quiet — no menu to open, no confirmation to dismiss.

29-context-menu. Long-press / right-click — topic actions

Pin is fast but limited. For rename + forget, the topic card exposes a context menu. Desktop: right-click anywhere on the card. Mobile: long-press (hold 400ms). Menu appears as a glass panel anchored to the click point. Three items: Pin / Unpin (toggles, matches corner icon state), Rename, Forget (destructive, red, opens confirm dialog in prod).

Keyboard: focus a card row and press Space or Enter to drill in; P to toggle pin; Menu key / Shift+F10 to open the context menu at the focused row. All three map to the same handlers behind the scenes.

Long-press (hold 400ms) any row to open the context menu.

Engineering2h
Auth & Security5h
Frontend8h
Database1d
Deployment1d

Prod wiring: wrap the menu in Radix DropdownMenu with modal=falseso it doesn't trap focus (the user is still in the topic list). Right-click uses onContextMenu; long-press uses a 400ms timer cleared on touch-move / touch-end. Mobile haptic: fire navigator.vibrate(10) when the menu opens.

29-scale-nested. Mode B/C recurses at every level

The scale-aware view is recursive (topic system rule 2). When a topic has >20 direct subtopics, its detail page switches from inline-groups to the same dense-grid / list view that the top-level topics page uses. Same component, different root.

Example: Engineering → Backendhas 80 direct subtopics. Instead of rendering 80 inline collapsible groups in one scroll, the Backend detail page renders them as a Mode-B dense grid with search. The user picks one, drills in, and that subtopic's detail page again decides its own mode based on directChildCount. Scale applies locally; never globally.

EngineeringBackend

Go services, database, queues, cache. Owned by platform team.

Search 80 subtopics in Backend…
recent ▾
Database1d
Queue2d
Cache3d
API gateway4d
Observability5d
Monitoring6d
Logging7d
Tracing8d
Rate limiting9d
Auth middleware10d
Feature flags11d
Billing12d
Webhooks13d
Email service14d
Push notifications15d
File storage16d
Show 20 more · 80 total
Gap vs prod: the current DrillNavigationMock always inlines nested children regardless of count. Wiring this recursion means the drill rebase reads directChildCount at the new root and picks Mode A/B/C for that level.
29-loadmore-subtopics. L2 interactive — Show more subtopics

The L2 pagination boundary(topic system rule 18): subtopics within a level. Initial render shows the first batch; “Show 20 more” appends the next group in place with a staggered fade-in (framer-motion AnimatePresence, NORMAL (0.2s) EASE, per-item delay 20ms). No layout jump — the container grows downward and existing rows don't move.

Independent cursor from L1 (topics) and L3 (memories-within- subtopic). Over 80 subtopics at a level, the section auto-switches to the Mode-B dense grid with search — see §29-scale-nested for the recursed-Mode-B treatment. Reduced-motion: instant, no stagger, no height animation.

Backend
30 subtopics
Auth service
API gateway
Database
Queue system
Cache layers
5 of 30
29-orphans-mega. 500+ orphan memories — interactive

Orphan memories (direct children of a topic, not inside any subtopic) render unlabeled at the top of the level (rule 4). At scale — e.g. 527 captures that never got assigned — the orphan section becomes its own paginated block with its own Show 20 more boundary, independent of the subtopic list below it.

Escape hatch for orphans is different from subtopics. Orphans have no drill target — they belong to the topic itself, not a subtopic. So instead of “Open full view →”, the orphan section uses search-first: an inline filter input that matches against all 527 titles locally. Type in the field below — the list filters in place; clear it to restore pagination. Rule 23.

Platform Ops

All the things that don't fit anywhere else yet.

Direct memories

527 unassigned
Webhook retry strategy notes1h
Postgres connection timeout investigation2h
River worker memory leak (2026-03)3h
Redis eviction policy audit4h
OpenTelemetry collector config5h
Feature flag rollout protocol6h
20 of 527

Subtopics

Incidents
Post-mortems
Runbooks
29-loading. Skeleton rows while paginating

Lazy-load-by-drill (rule 19) means every boundary has a loading state. The kitchen shows the skeletons so Codex doesn't have to guess shapes.

  • L1 — top-level topics page first paint: 6 card skeletons that match Mode A card shape (icon circle + 2 lines + chip row). Mode B/C use row skeletons of the same row height.
  • L2 — subtopics-within-a-level Show-more click: 4 new subtopic-header skeletons append to the list while the fetch is in flight. No layout jump.
  • L3 — memories-within-subtopic Show-more click: 8 memory-row skeletons append at the end of the list. Last-touched and topicLabel chips fade in once real data arrives.
  • Drill rebase — in-place morph: during the 29n cross-fade, the outgoing content fades to skeletons of the incoming shape at 0.5 opacity, then the real content replaces them. Prevents empty-container flash.

L1 · topic card skeletons

L2 · subtopic header skeletons

L3 · memory row skeletons

29-hierarchy. Topic vs memory — typography + icon + no-summary

The prod screenshot that triggered this pass had three real problems compounding: memory rows showed inconsistent summary lines; memory titles used font-medium which flattened the hierarchy against topic rows; every memory row carried a FileText icon that visually competed with the topic Folder icon. Users couldn't tell what was anchor vs content at a glance.

Three coordinated rules fix it — no added chrome, just removing the prod divergences and applying memax DNA (content-led, chrome recedes). See rules 32–34 below.

  • Rule 32 — Typography: topic rows fade when closed (text-[14px] text-fg-2) and commit when open (text-[14px] font-medium text-fg-1). Memory rows stay text-[14px] text-fg-1 regular always. Same 14px size for both — content is king, chrome (structure) recedes. Industry reference: Linear / Notion / GitHub all use same-size list items with weight-via-state hierarchy.
  • Rule 33 — Content icon (doc vs non-doc): the split is doc vs non-doc, not text vs everything. Doc-like (no badge): text, pdf, markdown, code — all reading content, scan-identical. Non-doc (trailing badge at text-fg-4): image, link. These behave differently from reading content. Topic row always has its TopicIcon anchor; memory row usually has nothing on the left. Icon presence is the strongest anchor-vs-content signal in the list and pairs with rule 32 to carry hierarchy without extra chrome.
  • Rule 34 — No summary on list rows: memory rows in topic view are single-line (title + optional trailing badge + age). Summary lives in memory detail. Topic description lives in the topic focus header + expanded subtopic header only. Predictable rhythm; no "some rows have description, some don't" jaggedness.

Why no vertical rail: memax DNA is "no dividers, content-led". Indentation (16px per level, rule 5 caps inline at 2 levels desktop / 1 mobile) + rule 32 open/close contrast + rule 33 icon presence together carry the hierarchy. A rail would duplicate signals already in place. Notion uses rails because Notion pages nest arbitrarily — memax doesn't (rule 5 cap). If hierarchy still feels unclear after all three rules land in prod, revisit as a follow-up; don't add chrome preemptively.

Every §29h demo renders these rules — see 29h-large below for OAuth memories with mixed content types. The orphan row is a PDF ("Security review Q1 decisions") — correctly renders nothing because PDFs are doc-like. Inside the OAuth subtopic, two non-doc rows render trailing badges: the RFC 7636 link icon and the OAuth consent flow image icon. Codex just needs to drop three prod divergences: memory-row.tsx:548 font-medium on memory title, memory-row.tsx:340 FileText fallback in renderLeadingIdentity, and memory-row-presentation.ts:66-71 showSummary including "topic" in its surface whitelist.

Your TopicsAuth & Security
Auth & Security

JWT tokens, OAuth flows, API key management, boundary enforcement.

Security review Q1 decisions1h
Threat model draft — new endpoints4h
PKCE for native apps3h
RFC 7636 — official PKCE spec4h
Refresh token rotation bug5h
OAuth consent flow screenshot6h
State parameter audit7h
OIDC nonce validation1d
Authorization code flow notes3d
Key rotation policy v34h
Scoped token design6h
Auth0 API key rotation guide1d
mTLS client cert rotation2d
HSM-backed signing keys1w
29c. Recall → topic bridge (grouped results)

Recall results grouped by topic. Topics with >1 match get a clickable header row. ✦ prefix on dream-discovered topics.

Auth & Security(3 matches)
1OAuth Token Refresh92%
2JWT Validation Strategy87%
3API Key Rotation81%
Deployment(1 matches)
4Staging Deploy Guide78%

Topic rows: icon + name + match count + → chevron. ✦ prefix for dream-discovered. Individual results indented (px-6). Uses topic_id + topic_name from RecalledMemory.

LLM RULES — Recall → Topic Bridge

• Production file: expand-search-results.tsx. Groups POST-Enter semantic results by topic_id.

• Only groups when 2+ distinct topics. Single topic = flat list (no noise).

• Topic icon resolved client-side from useTopics() cache. No server changes.

• NO ✦ on topic headers in recall. Topics are inherently dream-created — marking them adds noise. ✦ is only for the topic grid (first-visit discovery signal).

• Pre-Enter (keyword/FTS) results stay flat — no topic data available.

• Indented rows: px-6 (vs px-4). Topic name suppressed in row metadata when grouped.

• Touch targets: header min-h-11, rows min-h-11 via content + padding.

29e. Mobile — single column for all 3 modes

Mobile collapses every grid to 1 column. Mode A keeps the rich card (icon + name + description + chips + last-touched). Mode B keeps the dense card (icon + name + last-touched). Mode C keeps the list row. Same content density, just stacked. The search-first patterns in B and C still apply at the top.

Engineering2h

Backend, frontend, infra — Go services, RSC patterns, Fly.io, GitHub Actions.

Auth & Security5h

JWT tokens, OAuth flows, API key rotation, boundary enforcement, secret scanning.

Frontend8h

React Server Components, Tailwind, Radix primitives, design tokens.

29g. Topic fetch failures — contextual inline retry

Topics page / inbox counts

Couldn't load your topics right now.

Topic grouping and inbox counts are temporarily unavailable.

Topic detail shell

Couldn't open this topic right now.

The topic header or structure didn't come through. Try again.

Topic memory list / recent section / tree panel

Couldn't load memories for this topic.

The topic is here, but its memory list didn't load this time.

Reuses the existing memory-detail error treatment: no border, no tinted card, just centered copy + ghost retry on background. Never silent empty fallback. Production files: topic-grid.tsx, topic-detail.tsx, topic-tree-content.tsx.

29h. Topic navigation — one page, one scroll, inline groups

Core model.Inside a topic, you see one memory list container. Subtopics render as inline collapsible groups — not as separate pages. Drill-down-as-page forces users to lose context on every tap; inline groups let them skim the whole structure in one pass and collapse what they don't care about.

Size class drives default collapse state, not page shape. Small and medium topics open all subtopics by default (still scannable). Large and huge topics start collapsed — the user expands what they need. There is no auto-expand-on-delta and no delta counts on headers. Topic navigation is silent about dreams (see rule 1 of the topic system rules).

One container (no separate “since your last visit” card), no ✦ N new pills, no row accents, indent grows with depth (capped at 3 levels). Orphan memories render unlabeled at the top of every level.

29h-small. 8 memories, no subtopics — flat list

Trivial case. No grouping chrome. No headers. Just the memory list rendered top-to-bottom. Last-touched timestamps on the right of each row (rule 17); no delta accents.

Your TopicsTeam Docs
Team Docs

Onboarding, code review, release process.

Onboarding checklist2d
Code review expectations3d
Release process — beta4d
Incident response runbook1w
Architecture decision log template2w
Git workflow note3w
Pairing session guide1mo
Shipping checklist1mo
29h-medium. 40 memories, 3 subtopics — open by default

Medium topic. All three subtopics render open on arrival because the total is still scannable without collapse. The first two memories render unlabeled at the top — those belong directly to the topic, not to any subtopic. No dream-delta treatment — rows are neutral.

Your TopicsFrontend
Frontend

React Server Components, Tailwind, Radix primitives.

Tailwind 4 migration notes3h
Radix vs base-ui primitive comparison1d
Server action error handling4h
RSC cache tag invalidation6h
Streaming suspense boundaries9h
Use client vs use server — decision tree2d
Form actions without useFormState3d
Passing server data to client components4d
Spring easing tokens — memax set1w
Framer Motion layout shared elements2w
Entrance animation patterns3w
GPU-friendly transform-only rules1mo
TanStack Query cache hydration1w
Zustand slice patterns2w
29h-large. 150 memories, 6 subtopics — closed by default

Large topic. All subtopics start collapsed; the user expands what they need. Subtopic headers carry just icon · name · description — no counts, no delta pills, no ✦ badges. The surface is pure spatial browse (rule 1 + rule 9).

Your TopicsAuth & Security
Auth & Security

JWT tokens, OAuth flows, API key management, boundary enforcement.

Security review Q1 decisions1h
Threat model draft — new endpoints4h
PKCE for native apps3h
RFC 7636 — official PKCE spec4h
Refresh token rotation bug5h
OAuth consent flow screenshot6h
State parameter audit7h
OIDC nonce validation1d
Authorization code flow notes3d
Key rotation policy v34h
Scoped token design6h
Auth0 API key rotation guide1d
mTLS client cert rotation2d
HSM-backed signing keys1w
29h-huge. 612 memories, 12 subtopics — virtualized

Huge topic. All 12 subtopics render collapsed — visually compact, one scroll of headers tells the whole structure. The + N more row at the bottom represents the virtualized rows — in production, only visible rows render; this demo simulates the cap. Mode C of the recursive scale-aware view (rule 2) would auto-select here if the count exceeds 80.

Your TopicsBackend Archive
Backend Archive

Historical engineering notes — 3 years of decisions, migrations, and post-mortems.

Redis memory pressure alerts5h
LRU vs LFU decision8h
+ 600 more (virtualized — only visible rows render)
29h-deep. Nested subtopics — 2 levels deep, indent model

Subtopics can themselves contain subtopics. Indent grows with depth (16px per level, capped at 3). No delta rollup, no ✦ counts — nested groups are just nested groups. Desktop inline cap is 2 (topic + one nested), anything deeper becomes a drill-in chip. See §29j-* for the drill flow.

Your TopicsIngest Pipeline
Ingest Pipeline

Content ingestion, chunking, embedding, deduplication.

Pipeline architecture diagram2d
Markdown chunker edge case3h
Semantic paragraph boundaries5h
Headings as chunk boundaries7h
Code block preservation2d
Table chunking strategy1w
Layout-preserving extraction1w
Figure caption linkage2w
Voyage AI rate limits1w
Batch size tuning2w
29h-search. `/` filter — auto-expands matching subtopics

Type in the search box (top-right of the topic header). Only memories with titles matching the query render. Matching memories inside collapsed subtopics force the parent subtopic open so results aren't hidden. Clear the query → prior collapse state restores. In production this is triggered by the / keyboard shortcut.

Your TopicsAuth & Security
Auth & Security

JWT tokens, OAuth flows, API key management, boundary enforcement.

Security review Q1 decisions1h
Threat model draft — new endpoints4h
PKCE for native apps3h
RFC 7636 — official PKCE spec4h
Refresh token rotation bug5h
OAuth consent flow screenshot6h
State parameter audit7h
OIDC nonce validation1d
Authorization code flow notes3d
Key rotation policy v34h
Scoped token design6h
Auth0 API key rotation guide1d
mTLS client cert rotation2d
HSM-backed signing keys1w
29h-rules. Navigation rules summary
1.

One page per topic. Subtopics are inline collapsible groups, not separate pages. No drill-down-as-route. Breadcrumb stays at Your Topics > Topic Name regardless of which groups are expanded.

2.

Direct memories render first, unlabeled. Memories that belong to the topic but not to any subtopic render at the top of the list with no group header. Section ends when the first subtopic header appears.

3.

Default collapse by size class. Small (≤20) / medium (20–100): all subtopics open. Large (100–500) / huge (500+): all closed. The user expands what they need. No auto-expand-on-delta — topic navigation is silent about dreams (see topic system rule 1).

4.

No delta, no counts, no accents. Subtopic headers carry icon · name · description only. No newCount, no ✦ pills, no row accents on “new” memories, no rollups. If something needs the user's call, it surfaces in §36 Inbox. Topic navigation stays pure spatial browse.

5.

Indent capped at 3 levels. Deeper nesting flattens — deeper children render at the same indent as level 3 with their parent name prefixed in the title if disambiguation is needed. Prevents runaway indentation in pathological trees.

6.

Expand state persists per topic per user. LocalStorage keyed by memax-topic-expanded-{hubId}-{topicId}. Manual toggles are the only source of truth. No auto-expand on any signal.

7.

Search overrides expand state. Typing in / search force-expands any subtopic containing a match. Clearing the search restores the prior expand state.

8.

Virtualization for huge topics. Topics over 500 memories use virtualized row rendering (only visible rows mount). Subtopic headers always render — they're cheap and they're navigation. Memory rows inside expanded subtopics virtualize.

29-expand. Expand vs drill — two tap targets on one row

An inline subtopic row carries two tap targets. The chevron on the left expands / collapses the row in place; the row body (icon · name · description) drills into that subtopic, rebasing the container via the §29n morph. Same pattern as Notion toggles, Linear nested projects, Finder list view.

  • Chevron click onToggle(sub.id), stays on current page. Chevron rotates 0° → 90° FAST (0.15s) EASE. Content region animates { height: 0, opacity: 0 } { height: 'auto', opacity: 1 } via framer-motion AnimatePresence over NORMAL (0.2s). overflow: hiddenclips during animation so child padding doesn't leak.
  • Body click onDrill(sub.id), rebases the container (breadcrumb gains a segment, content region slides +16px + cross-fades via §29n). Container stays put; only the content inside morphs.
  • Keyboard Tab goes chevron → body → next row. Space / Enter on chevron toggles; Enter on body drills. Both buttons carry aria-label (“Expand X” / “Open X”) and the chevron also sets aria-expanded.
  • Reduced motion useReducedMotion()from framer-motion returns true → both the chevron rotation and the height animation are skipped. Opacity crossfades stay (they don't trigger vestibular issues). Matches Apple HIG guidance for Reduce Motion.
Go error handling patterns (2026-Q1)2h
Structured logging with slog5h
Context propagation across goroutines1d

Industry references. Notion: same dual-target pattern — chevron toggles, title navigates. Linear nested projects: identical. Apple Finder list view: click disclosure triangle to expand, tap folder name to navigate. Radix Accordion primitive: chevron is the trigger, content is the panel; we're borrowing the animation shape but keeping our own data-layer so the row body can act as a second affordance.

29j. Team hub deep navigation — intro

Team hubs are Memax's focus. They accumulate far more memory, far more subtopics, and deeper trees than personal hubs. A 50-person engineering team's Engineering topic legitimately grows to depth 3-5 — Engineering › Backend › Database › Postgres › Migrations › Zero-downtime patterns.

The inline-2 reading limit still holds (human working memory doesn 't scale with team size), but drill-in is now a primary flow, not an edge case. This section demonstrates the full e2e team navigation: sticky breadcrumb, drill-in chips beyond the cap, in-topic search, mobile flow, hub overview mode, and row-level author attribution. Dream reviews (merge / stale / low-confidence) surface in §36 Inbox only — never in the topic view.

Mock: TEAM_HUB_ENGINEERING — Acme Engineering hub, 2,147 memories, 48 subtopics across 5 depth levels, with team attribution and 7-day activity rollups.
29j-a. Step 1 — Root view (desktop, cap=1)

Starting point. Breadcrumb is sticky at top (scroll to confirm). Depth 0 (Backend, Frontend, …) + depth 1 (Database, Queue, Cache under Backend) render inline. Depth 2+ becomes drill-in rows: Postgres, MySQL, Redis show as clickable chips with affordance. Fuzzy search across all depths lives in the global bar (§24m); no separate picker hotkey.

Your Topics
Engineering

All engineering knowledge — backend, frontend, infra, data, security.

Engineering principles (2026 refresh)
D
3d
Weekly eng sync notes
A
5d
Service decomposition plan
A
2h
Gateway timeout audit
C
6h
Schema convention v4
A
4h
Index naming policy
B
9h
River job priority tuning
B
6h
Stampede defense — singleflight
A
9h
29j-b. Step 2 — Drill into Backend

User clicks Backend → depth 2 drill chip, or clicks a drill chip directly. Breadcrumb gains a segment. List rebases in place— no page transition. Backend's depth 0 children (Database, Queue system, Cache layers) become the new inline roots. Recent activity strip under the header shows +31 in 7 days by 4 contributors. Click any earlier breadcrumb segment to walk back.

Your Topics
Backend

Go services, database, queues, cache. Owned by platform team.

Last 7 days · +31·
A
B
C
D
Service decomposition plan
A
2h
Gateway timeout audit
C
6h
Schema convention v4
A
4h
Index naming policy
B
9h
Connection pool sizing guide
A
5h
pgvector index tuning
C
12h
InnoDB buffer pool sizing
B
1mo
Eviction policy trade-offs
C
8h
River job priority tuning
B
6h
Stampede defense — singleflight
A
9h
29j-c. Step 3 — Drill into Database

Three segments deep. Postgres / MySQL / Redis are now the new depth 0, all inline. Their children (Migrations, Replication, …) stay inline as depth 1. Everything deeper is drill-in chips. Activity strip shows tighter subtree context (+18, 3 people).

Your Topics
Database

Postgres primary, MySQL legacy, Redis cache — schemas, migrations, replication.

Last 7 days · +18·
A
B
C
Schema convention v4
A
4h
Index naming policy
B
9h
Connection pool sizing guide
A
5h
pgvector index tuning
C
12h
NOT NULL backfill checklist
A
3h
Shadow table pattern — full guide
B
7h
Replication lag alerting
C
1d
InnoDB buffer pool sizing
B
1mo
Eviction policy trade-offs
C
8h
29j-d. Step 4 — Drill into Postgres

Four segments deep. This is still fluid — each rebase is a cheap list re-render, not a route push. Migrations, Replication, Query optimization are new depth 0. Zero-downtime / Rollback / Version-control sit at depth 1, inline. Anything deeper is a drill chip again.

Your Topics
Postgres

Our primary OLTP store. Migrations, replication, query tuning, HA.

Last 7 days · +14·
A
B
C
Connection pool sizing guide
A
5h
pgvector index tuning
C
12h
NOT NULL backfill checklist
A
3h
Shadow table pattern — full guide
B
7h
Expand/contract schema migration
A
4h
Dual-write invariant checks
B
8h
Backfill window sizing
A
2d
Rollback decision tree
D
6h
Replication lag alerting
C
1d
29j-e. Step 5 — Drill into Migrations (deepest leaf path)

Five segments deep — this is the maximum the Topic model allows. Zero-downtime patterns, Rollback strategies, Version control are the new depth 0. Notice the list is still readable — the breadcrumb carries the full path, the header carries the narrow subject, and the list shows only what matters at this level. No IDE-style 5-level indent tower.

Your Topics
Migrations

Zero-downtime patterns, rollback, version control, backfills.

Last 7 days · +8·
A
B
NOT NULL backfill checklist
A
3h
Shadow table pattern — full guide
B
7h
Expand/contract schema migration
A
4h
Dual-write invariant checks
B
8h
Backfill window sizing
A
2d
Rollback decision tree
D
6h
29j-h. Mobile — cap=0, drill at depth 1

Mobile has 32px less horizontal space per indent level, so the cap tightens to 0 (only top level inlines). Breadcrumb collapses to ← Backend with the full path compressed to the right. Every depth-1 subtopic becomes a drill chip. Activity strip still shows under the header.

Your Topics
Backend

Go services, database, queues, cache. Owned by platform team.

Last 7 days · +31·
A
B
C
D
Service decomposition plan
A
2h
Gateway timeout audit
C
6h
Schema convention v4
A
4h
Index naming policy
B
9h
River job priority tuning
B
6h
Stampede defense — singleflight
A
9h
29j-i. Mobile — after one drill step

User tapped Database → now at Backend › Database. Breadcrumb compact shows ← Database for fast single-tap back. Depth 1 children (Postgres, MySQL, Redis) again become drill chips.

Engineering › Backend
Database

Postgres primary, MySQL legacy, Redis cache — schemas, migrations, replication.

Last 7 days · +18·
A
B
C
Schema convention v4
A
4h
Index naming policy
B
9h
Connection pool sizing guide
A
5h
pgvector index tuning
C
12h
InnoDB buffer pool sizing
B
1mo
Eviction policy trade-offs
C
8h
29j-rules. Team hub navigation rules
1.

Inline cap stays at 2 (desktop) / 1 (mobile). Team hub depth grows, reading capacity does not. Subtopics beyond the cap render as drill-in chips — single-row, no toggle chevron, right-side affordance.

2.

Drill-in is in-place rebase, not route push. Clicking a drill chip re-parents the list without a page transition. The container, header, and list animate as one content replace (container morph principle). Back/forward is via the breadcrumb; browser history pushes as a secondary effect so deep-links still work.

3.

Breadcrumb is first-class. Sticky at top on desktop, ← parent compact on mobile. Every segment is a click target. ⌘← keyboard shortcut navigates up one level. Typing Esc resets to topic root.

4.

The global bar is the depth escape hatch. 100+ subtopic team hubs need fuzzy search across all depths. That's the command bar's job (§24), not a separate picker modal. When the bar opens from inside a topic view it should bias toward in-topic matches first, then widen to global. See §24m “Topic-scoped recall — conceptual northstar”. Mobile uses the prod MobileTreeSheet ( topic-tree-panel.tsx) for hierarchical jump + the bar for search; no extra picker sheet.

5.

/ search crosses all depth. In-topic search always queries every descendant, not just the currently-visible level. Matching memories force-expand their containing subtopic. Clearing the query restores prior drill and expand state.

6.

Row-level author attribution. Every memory row in a team hub shows an 18px avatar of the pusher to the right of the title. Hover = full name. Personal hubs keep that slot empty for You, but reserve it for agent actors so the row still stays content-led.

7.

Recent activity strip under every header. Team-hub-only. Last 7 days · +18 · @A @B @C below the subtopic header. Anchors “where's the team working right now” intuition.

8.

Hub overview is a side mode. Separate view for onboarding / orientation. Flat-2 grid of depth 0 cards with depth 1 children as preview chips. Does not replace drill-in — drill is for working, overview is for orienting.

9.

Dream merge suggestions live in §36 Inbox, not here. The dream engine may detect redundant sibling subtopics and propose a merge, but that surfaces as a Review in the Inbox (see §36 “Low-confidence / structural proposal” row pattern), never as a banner or card inside the topic detail view. Topic navigation stays silent.

10.

Drill state persists per user per topic. LocalStorage key: memax-drill-{hubId}-{topicId}. A team member's drill state is their own — other members seeing the same topic start at their own last-visited subtopic, not yours.

29k. Icons at every depth — 28 / 16 / 14 px

Every Topic row in the data model carries an icon field (Lucide icon name). Render size varies by context:

  • 28px — focus header (the topic / subtopic currently being viewed)
  • 16px — inline subtopic header row
  • 14px — drill-in chip (subtopic beyond inline cap)
  • Memory rows do not get the topic icon — they already carry their own content-type icon via MemoryRow.
  • Unknown icon name → fallback to FileText. Handled by TopicIcon helper.

See NAV_MOCK_LARGE(Auth & Security with Shield) — every inline subtopic header, drill chip, and focus title picks up its own icon. Unknown values fall through to FileText silently.

Your TopicsAuth & Security
Auth & Security

JWT tokens, OAuth flows, API key management, boundary enforcement.

Security review Q1 decisions1h
Threat model draft — new endpoints4h
PKCE for native apps3h
RFC 7636 — official PKCE spec4h
Refresh token rotation bug5h
OAuth consent flow screenshot6h
State parameter audit7h
OIDC nonce validation1d
Authorization code flow notes3d
Key rotation policy v34h
Scoped token design6h
Auth0 API key rotation guide1d
mTLS client cert rotation2d
HSM-backed signing keys1w
29l. Pagination inside a subtopic — 'Show N more'

A subtopic can hold hundreds of memories. Rendering all of them inline breaks scroll and DOM weight. The canonical pattern:

  • Initial load 20 memories (DEFAULT_SUBTOPIC_PAGE_SIZE)
  • “Show N more” at the bottom of the list loads the next 20
  • Once expanded, show “Collapse” to return to the first 20
  • Search results render all matches (pagination disabled)
  • Production endpoint (gap): GET /v1/topics/:id/memories?subtopic_id=X&cursor=Y&limit=20
RSC patterns (50 memories)50
Server action error handling1d
RSC cache tag invalidation1d
Streaming suspense boundaries2d
Use client vs use server — decision tree2d
Form actions without useFormState3d
Passing server data to client components3d
Parallel route caching edges4d
Revalidate on-demand with tag API4d
Partial prerender notes5d
Server components and third-party libs5d
Server action error handling6d
RSC cache tag invalidation6d
Streaming suspense boundaries7d
Use client vs use server — decision tree7d
Form actions without useFormState8d
Passing server data to client components8d
Parallel route caching edges9d
Revalidate on-demand with tag API9d
Partial prerender notes10d
Server components and third-party libs10d
29n. Drill animation — container morph, not route push

Drilling into a subtopic (or popping back) animates the content inside the container, not the container itself. Forward = new content slides in from +16px with cross-fade. Backward = from −16px. Breadcrumb segments animate with layout; focus header cross-fades.

  • Duration: NORMAL (0.2s) from @memaxlabs/ui/tokens/motion
  • Easing: EASE — cubic-bezier spring [0.16, 1, 0.3, 1]
  • Transform-only: translateX + opacity. No layout properties animate.
  • Reduced-motion fallback: wrap motion values in useReducedMotion() → skip translate, keep instant opacity swap.
  • Same rules on desktop and mobile. Mobile does NOT use iOS-style route stack — it's still a container morph.
Level 1 of 5
Your TopicsEngineering

Engineering

All engineering knowledge — backend, frontend, infra, data, security.

29o. Memory row → detail — prod reference (NOT a proposal)

This card documents what production already does. Desktop and mobile use different metaphors because opening a memory is a new surface, not a container morph (29n covers the morph case — same topic, different subtopic).

  • Desktop — centered glass modal on top of the topic view. Topic stays visible behind at reduced opacity so the user keeps their place. Close = Esc / click outside / ✕. This already works; no kitchen animation spec needed.
  • Mobile — push-in / push-out. The incoming surface pushes from the right edge, the outgoing topic view is pushed off to the left as a single coordinated motion (not a slide-over on top). Back = iOS edge swipe or back button, which runs the same motion in reverse. This is the transition Memax mobile already uses for topic→memory, hub→hub, and settings drill-down.
  • Why document instead of design: the animation is already shipped and tuned. Earlier drafts of this card proposed a “slide-from-right” modal which conflicted with the existing push-in/out metaphor. Codex should not reimplement it — wire the existing prod transition into the new topic navigation surface.

Production references: the mobile page transition lives in the app shell (see packages/web/src/app/ layout + its page wrapper), and the desktop memory modal uses Radix's dialog primitives with our glass tokens.

Design rules — topic system
1.

Topics are silent about dreams. No “✦ N new” badges on cards, no row accents in detail, no delta headers, no “since your last visit” banners. Dream activity and decisions live in §36 Inbox. The topic surface is pure spatial browse.

2.

Scale-aware main view, recursive at every level. One component renders the topics list. Three modes by directChildCount: A (≤20, rich grid), B (20-80, dense grid + search + Pinned), C (80+, virtualized list + search-first + Pinned/Recent). Same logic recurses inside any topic — a subtopic with 200 children renders Mode B at THAT level. Thresholds 20/80 are starting values; tune with usage. See §29a.

3.

Detail is a focused layer, not a page. Breadcrumb → icon + name + full description → meta → memory list in one scrolling container. When the user drills into a subtopic, the header rebases to show THAT subtopic's own icon + description (see §29j focus rebase).

4.

Orphan memories render at every depth. A topic can have direct memories not assigned to any subtopic. Same for subtopics — they can have direct memories AND children. Orphans render unlabeled at the top of each level, above the first subtopic group. The subtopic header row is the visual boundary — no “General” divider.

5.

Inline cap at 2, drill past. Desktop shows 2 depth levels inline (top + one nested); anything deeper becomes a drill-in chip. Mobile caps at 1. Max depth is 5 (Topic model constraint) — all 5 levels are reachable through drill, not inline expansion.

6.

Recall → topic bridge. Recall results group by topic_id when there are 2+ distinct topics. Clickable header row with match count. ✦ prefix only in the topic grid for first-visit discovery — NOT on the recall bridge row (topics are inherently dream-created, the marker is noise).

7.

Borderless detail surface. Topic detail content sits on --background without a framing card. Matches memory detail treatment.

8.

Mobile is structural. Single column grid. Large = full card. Medium = compact. Small = horizontal scroll pills. Tree trigger as discoverable row at top. Drill cap=1.

9.

No counts on topic cards / subtopic headers / drill chips / picker results. No “47 memories”, no “3 subtopics”, no “5 subs · 142” on chips. The only number a topic surface carries is the last-touched timestamp— ambient temporal context, not metadata repetition. Sort, picker filtering, and pagination still use counts at the data layer; the UI just doesn't render them.

10.

The global bar is the depth escape hatch. 100+ subtopic team hubs need fuzzy search across all depths. That belongs to the command bar's recall mode (§24m), not a separate picker modal. Inside a topic view, the bar biases toward topic-local matches first, then widens to global. No ⌘P subtopic picker modal; no mobile picker bottom sheet.

11.

Icons at every depth. Topic.Icon (Lucide icon name) renders at 28px in the focus header, 16px in inline subtopic headers, 14px in drill-in chips. Unknown names fall back to FileText. Memory rows do not carry the topic icon — they have their own content-type icon. See §29k.

12.

Memory lists inside subtopics paginate. Initial load 20 (DEFAULT_SUBTOPIC_PAGE_SIZE), “Show N more” loads next 20, “Collapse ” returns to 20. Search results render all matches. Production: GET /v1/topics/:id/memories?subtopic_id=X&cursor=Y&limit=20. See §29l.

13.

No stats noise in focus headers. Do NOT render “{memoryCount} memories · {subtopicCount} direct subtopics · {levels}levels deep”. The list below IS the signal. The only header meta allowed is the team-hub recent activity strip (see §29j rule 7), because that's collaboration context, not metadata repetition.

14.

Mobile hierarchy = prod MobileTreeSheet. Mobile has no ⌘P. The existing MobileTreeSheet in topic-tree-panel.tsx handles drill-down tree + any-level jump. Topic-aware fuzzy search lives in the command bar (§24m). The compact ← parent breadcrumb stays for single-step back. No separate picker or path sheet.

15.

Drill animation is container morph. Content inside the container slides ±16px + cross-fades. Duration NORMAL (0.2s), easing EASE spring. Transform-only — no layout properties. Same rules on desktop and mobile. NOT an iOS-style route stack. Breadcrumb segments animate with layout. Reduced-motion: skip translate, keep instant opacity. See §29n.

16.

Memory row → detail uses the existing prod transitions. Desktop: Radix dialog in a centered glass container; topic view stays behind at reduced opacity. Mobile: push-in / push-out (incoming surface pushes from the right, topic view shifts left as one coordinated motion). Both are already shipped in prod — kitchen 29 does NOT spec a new animation, it just documents which existing transition to wire. See §29o.

17.

Last-touched timestamp on every topic card / row. Neutral fg-3 small text, NOT a badge. Encompasses both human push and dream organization — no source attribution. The default sort key. Source-agnostic by design: users don't care who touched it, they care it's recent.

18.

Three pagination boundaries. L1: top-level topics — cursor pagination on /topics page; Mode C virtualizes. L2: subtopics within a level— inline render first 20, “Show more subtopics” progressive disclosure; auto-switch to search-first when count > 80. L3: memories within a subtopic — DEFAULT_SUBTOPIC_PAGE_SIZE = 20, “Show N more” loads next 20, “Collapse” returns. Each boundary is independent and uses its own cursor.

19.

Lazy load by drill — never recurse. Loading a topic detail fetches that topic's direct subtopics + first 20 orphan memories. Drilling into a subtopic fetches THAT subtopic's direct children + 20 orphans. Never preload nested grandchildren. A 5-level tree with 100 things at each level = 5 fetches as the user drills, not 10 billion items upfront. Counts come from denormalized columns on Topic, never from counting child rows.

20.

View toggle override. Top-right of the topics view: 3-icon toggle (grid / dense / list) that lets the user override the auto-mode. Choice persists in localStorage per hub. Default = auto (mode selected by directChildCount). For power users who want dense list on a small personal hub. See §29a-toggle.

21.

Topic card chips drill directly. In Mode A (rich grid), each topSubtopics chip is its own tap target. Card body tap → drill to the topic root. Chip tap → drill straight to that subtopic, skipping the topic root view. Both use the SAME §29n container morph. Chip click must stopPropagationso the parent card click doesn't also fire. See §29a-mode-a.

22.

Scale is recursive, locally. Drilling into a subtopic doesn't just swap content — the new root re-computes its own Mode A/B/C from its own directChildCount. A topic with 12 children renders Mode A at the root AND Mode B inside a subtopic that has 80 grandchildren. See §29-scale-nested.

23.

Orphans paginate independently. Orphan memories (rule 4) get their own L3 boundary with its own Show-more cursor. When orphan count > 80, the orphan section switches to dense-list-with-search — a local Mode C independent of the subtopics list below. See §29-orphans-mega.

24.

Every boundary has a skeleton. L1 first paint renders topic card skeletons matching Mode A/B/C row shapes. L2 Show-more appends 4 subtopic-header skeletons in place. L3 Show-more appends 8 memory-row skeletons. The §29n drill morph cross-fades outgoing content into skeletons of the incoming shape at 0.5 opacity, then swaps in real data — prevents empty-container flash. See §29-loading.

25.

Search lives in the command bar, not in the topics view. Mode B and Mode C do NOT render a local search input. Filtering the topic list is the §24 command bar's job — when the user is on /topics and hits ⌘K, the bar's recall mode is scoped to topic matches across the current hub. Mode B/C header rows show a quiet “⌘K to search” hint (desktop) beside the sort dropdown; mobile relies on the already-visible bar. One surface per job, no duplicate search affordances. Two search boxes on one screen force the user to guess which one filters which scope — don't do it.

26.

Pin = corner icon + shared-layout morph. Every topic card / row carries a Pin icon top-right. Empty: text-fg-4 outlined, revealed on hover/focus (desktop), always visible (mobile). Filled: text-fg-1, rotated 45°. Toggle click → stopPropagation + framer-motion layoutId morph from the All section to the Pinned section (or back), NORMAL (0.2s) EASE spring. Pinned section collapses via AnimatePresence when empty. Per-hub localStorage persistence. Matches Linear/Notion/Apple Reminders pattern. See §29-pin.

27.

Long-press / right-click → topic context menu. Three items: Pin/Unpin (toggles, matches corner icon state), Rename, Forget (destructive, oklch(0.55 0.2 25), opens confirm dialog in prod). Desktop trigger: onContextMenu. Mobile trigger: touchstart + 400ms timer, cleared on touchmove / touchend. Mobile fires navigator.vibrate(10) on menu open. Keyboard: focus row + P for pin, Shift+F10 / Menu key for context menu. Prod wraps in Radix DropdownMenu with modal=false so focus stays on the topic list. See §29-context-menu.

28.

Subtopic row has two tap targets: chevron and body. Chevron click → onToggle(id), expand / collapse in place, stays on current page. Row body click (icon + name + description) → onDrill(id), rebases the container to that subtopic via the §29n morph. Two separate <button>s in one flex row with items-start so the chevron stays top-aligned when the body grows (otherwise items-stretch + items-center shifts the chevron down on expand). Both buttons carry aria-label; chevron sets aria-expanded. Tab order: chevron → body → next row. Matches Notion toggles, Linear nested projects, Finder list view, Radix Accordion. See §29-expand.

29.

Expand / collapse animates chevron + content height. Chevron rotates rotate(0deg → 90deg) over FAST (0.15s) EASE. Content region is wrapped in framer-motion <AnimatePresence> + <motion.div> with { height: 0, opacity: 0 } { height: 'auto', opacity: 1 }, duration NORMAL (0.2s), easing EASE. overflow: hidden on the wrapper clips child padding during animation. Nested subtopics compose their own AnimatePresence inside — no special handling needed. useReducedMotion()from framer-motion gates both the chevron rotation and the height animation: when true, both are instant — opacity crossfade stays (doesn't trigger vestibular issues, matches Apple HIG Reduce Motion). See §29-expand.

30.

Inline expand has an escape hatch at INLINE_EXPAND_THRESHOLD = 100. Expanding a subtopic inline is for quick peek, not sustained reading. While its memory count is ≤ 100, SubtopicMemoryListrenders “Show 20 more” + “Open full view →” side-by-side at the bottom of the list (L3 pagination). Past 100, “Show more” disappears and “Open full view → (N memories)” is the only footer action — nudging the user to drill via §29n instead of paginating endlessly in place. The escape hatch wires to the same onDrill(subtopicId) handler as the row body button (rule 28), so chevron-expand + body- drill + escape-hatch-drill all compose cleanly. Orphans don't get this hatch — they have no drill target; instead they switch to search-first local filter when count > 80 (rule 23). See §29l + §29-expand.

31.

LEGACY — delete topic-tree-panel.tsx during migration. The desktop sidebar tree (packages/web/src/components/features/topic-tree-panel.tsx) and its MobileTreeSheet duplicate what the new kitchen model already gives us: hierarchy rendering → chevron expand (rule 28); any-level jump → command bar §24m; breadcrumb anchor → topic detail header. Codex should delete both files after the migration, but not before §24m is wired (the bar must be topic-aware with activeTopicId / scope="hub" scopes — otherwise users lose any-level search with no replacement). Ordering constraint: 1) wire §24m 2) delete topic-tree-panel.tsx + MobileTreeSheet 3) update all call sites (hub page, topic page, mobile dock). Until then, keep the legacy tree running in parallel — it still works, it's just redundant with the new model.

32.

Topic fades as scaffolding, memory stays content. Topic / subtopic header rows render as text-[14px] with state-driven contrast: text-fg-2 regular when collapsed (fades as scaffolding — "this is a signpost, keep scanning"), font-medium text-fg-1 when open ("you've committed to this branch"). Memory rows stay text-[14px] text-fg-1 regular always — content is king, chrome (structure) is background. Memax design DNA: the reading target should always be prominent; the scaffolding should recede until you ask it to commit. Same font size for both (14px) — no size delta, industry reference is Linear / Notion / GitHub where list items share the same size and differentiate via weight + state. Prod memory-row.tsx:548 has font-mediumon memory titles — that's the divergence to fix (drop the font-medium, match the kitchen). Differentiation between topic-open and memory comes from the open-state icon + weight combination, plus rule 33 (icon presence). See §29-hierarchy.

33.

Content icon: doc-like types render nothing, non-doc types render a trailing badge. The split is doc vs non-doc, not text vs everything else. Doc-like (no badge): text, pdf, markdown, code — all reading content, behaviorally the same when scanning. Non-doc (trailing h-3 w-3 text-fg-4 badge via MemoryContentTypeBadge): image, link. These are behaviorally different — scanning for "that screenshot" or "that URL I saved" is a different task than scanning for "that decision I wrote down". The rarity of non-doc rows in a typical topic is exactly what makes the badge valuable — it marks "this is not reading content" without shouting. Topic row always has its leading TopicIcon; memory row usually has nothing on the left. The presence/absence of leading icon is the strongest anchor-vs-content signal in the list — pairs with rule 32 to carry hierarchy without extra chrome. For team hubs, author avatar / agent icon still occupies the leading slot (attribution > content type in team context). Matches GitHub issues, Notion pages, Linear tasks — all of which drop "default type" icons. Prod divergences to fix: renderLeadingIdentity() fallback at memory-row.tsx:340 should return null for doc-like types, and the trailing isRichContentType check at memory-row-presentation.ts:144 should drop pdf from its whitelist (keep only image + link). See §29-hierarchy.

34.

Memory rows never show summary in list view. Descriptions are a topic-header affordance (focus header shows full description; expanded subtopic header shows description via descriptionStrategy). Memory rows in the topic surface are single-line: title + optional trailing content badge + age. No summary, no second line, no truncated preview. Summary content lives in the memory detail view where the user has opted into reading it. Prod showSummary currently includes "topic" in its surface whitelist at memory-row-presentation.ts:66-71 — that's the divergence to fix. Recent feed keeps the summary (different rhythm, different surface).

42. Topic Icons

Proposal — how auto-generated topic identity would look end-to-end. Today every topic renders as Folder; this preview shows the full system live so you can decide whether to greenlight.

Before → After (glance test)
Today — Folder monoculture
Caching Strategy
Recipe ideas
Investor calls
Spanish vocab
Q2 roadmap
Auth refactor
Weekend trips

Scan cost: O(n) — every label must be read

Proposed — icon + accent tile
Caching Strategy
Recipe ideas
Investor calls
Spanish vocab
Q2 roadmap
Auth refactor
Weekend trips

Scan cost: O(1) — color lookup before label is read

Accent palette — 12 tones
rose
amber
lime
teal
sky
indigo
coral
honey
moss
mint
plum
slate

Signature violet (290°) is reserved for Memax chrome — never assigned to topics. Tile = 14% chroma mixed into background; stroke = full accent at the icon.

Same identity, three contexts
List row — 28px
Auth refactor
Postgres deep dive
Design language
Grid card — 40px
Q2 roadmap
14 memories
Recipe ideas
9 memories
Detail header — 56px
Dream engine notes
23 memories · synthesised last night
Density test — 24 topics at a glance
Caching Strategy
Postgres tuning
Git workflows
Auth refactor
Bugs to triage
Recipe ideas
Coffee notes
Morning routines
Q2 roadmap
Design language
Spanish vocab
Kids' activities
Investor calls
Expenses
Climbing goals
Running log
Weekend trips
Photography
Dream engine
Music discovery
Research notes
Reading list
Meetings
Inbox zero

You can find the blue ones (code), green ones (health/money), amber ones (food/triage) without reading a single label. That's the whole point.

Curated library — 72 icons across 6 categories
Work / Business
briefcase
building
presentation
handshake
target
trending-up
calendar
clock
mail
phone
users
user-plus
Code / Engineering
code
terminal
git-branch
database
server
cloud
cpu
bug
wrench
package
box
workflow
Knowledge / Learning
book
book-open
graduation-cap
lightbulb
brain
flask
microscope
compass
map
globe
library
pencil
Life / Personal
heart
home
coffee
utensils
dumbbell
plane
music
camera
gift
smile
sun
moon
Creative / Media
palette
pen-tool
image
film
mic
headphones
sparkles
wand
layers
feather
scissors
layout
Money / Ops / Security
dollar
credit-card
line-chart
pie-chart
receipt
bar-chart
shield
lock
key
alert
flag
bookmark

LLM receives category list + icon names in its prompt, routes by semantic match. Unknown returns fall back to Folder or emoji.

Emoji fallback — when no curated icon fits
🍜
Ramen spots in Tokyo{ icon: "🍜", accent: "coral" }
🐕
Dog training{ icon: "🐕", accent: "honey" }
🚀
Mars colonization{ icon: "🚀", accent: "indigo" }
🔮
Tarot readings{ icon: "🔮", accent: "plum" }

Frontend detection: icon.length ≤ 4 && !ICON_MAP[icon] → render as emoji span inside the same tile chrome. Keeps niche topics playful without exploding the curated set.

LLM routing — topic name → { icon, accent }
Topic namePreviewLLM output
Caching Strategy
{ icon: "cpu", accent: "sky" }
Recipe ideas
{ icon: "utensils", accent: "amber" }
Investor calls
{ icon: "handshake", accent: "moss" }
Auth refactor
{ icon: "shield", accent: "indigo" }
Ramen spots in Tokyo
🍜
{ icon: "🍜", accent: "coral" }
Dream engine notes
{ icon: "sparkles", accent: "plum" }
End state — Topic Explore with full identity
Topics
Caching Strategy
18 memories · 2h ago
Recipe ideas
9 memories · yesterday
Q2 roadmap
14 memories · 3d ago
Dream engine notes
23 memories · last night
Investor calls
6 memories · 1w ago
Caching Strategy
Postgres tuning
Git workflows
Auth refactor
Bugs to triage
Recipe ideas
Coffee notes
Morning routines
Q2 roadmap
Design language
Spanish vocab
Kids' activities
Investor calls
Expenses
Climbing goals
Running log

Left: dense list variant with 32px tiles. Right: topic cloud showing how accents alone carry category identity even without reading labels — blue cluster = code, amber cluster = food / triage, moss = money / ops.

43. Shell v2 — three-pane desktop, drawer + push-stack mobile

Plan 19+ frontend architecture. Left rail (Brain / Memories / Agents / Inbox) + contextual secondary panel (280px, matches existing TopicTreePanel) + main view. Glass surfaces, scarce signature accent, MemaxLogo for brand. Onboarding plan 18 nests as pinned region in Memories Overview.

43-shell. Desktop shell — click tabs, expand/collapse rail, click + to open Compose modal stub

Three zones: 56–196px LeftRail (glass) + 280px contextual SecondaryPanel (glass-tinted; hidden when tab has none — Brain) + flex main view. Signature is scarce: only the hub avatar (◐), the active tab, the Save CTA, and system_notice accent. Brand mark uses <MemaxLogo /> in neutral foreground per kitchen 11 canonical rule.

P

Personal hub

47 memories · 12 topics · 3 dreams this week

Hi, Jiahao. Drop something below — or connect an agent and let it dump for you.

Topic folder cards

plan 20 preview

On the topic detail page (`/h/<slug>/topics/<id>`), folder cards sit at top with subtopics; memory cards below. Folder card is topic icon + title + description. UI iterates; data shape (memoryCount, subtopicCount, preview, accentColor) stays so we can layer in moves without re-plumbing data.

Fresh memory

7 days · all actors

"We are our memory, we are that chimerical museum of shifting shapes, that pile of broken mirrors." — Jorge Luis Borges

memax·seed
Welcome

Foundation: PR #3370 (p1, metering crate) → PR #3374 (p2, credit-watcher daemon). Gating PRs stacked on foundation: PR #3126, PR #3127, PR #3128.

claude-code·2h ago
Tutorial

Floating glass panel with 12px insets, 296px width, GLASS_ENTER animation. Containers are chrome; brand color goes on affordances inside.

ZZiyang·yesterday
EngineeringTutorial

Type a question, hit ↵. memax pulls from everything you've dumped — across hubs, agents, file types — ranked by semantic match plus recency.

memax·seed
Tutorial

Every night memax connects the dots — surfaces contradictions, merges duplicates, organizes loose memories into topics. You wake up to a tidier brain.

memax·seed
Welcome

npx memax-cli setup --agent claude-code. One command syncs your settings + memories. Memory flows both ways.

memax·seed
Tutorial

6 seed memories. Admin-configurable in production (plan 23).

43-cards. Memory card preset — neutral halo, file-type icons, Borges card carries the only signature accent

New surface="card" preset on the existing MemoryRow component (plan 20). Halo on hover is foreground-tinted, NOT signature — depth without color cast (Manus library #e7ebc0ff). Borges (isQuote: true) is the only card that wears signature. The first cell is the + Compose entry with ⌘⇧↵.

"We are our memory, we are that chimerical museum of shifting shapes, that pile of broken mirrors." — Jorge Luis Borges

memax·seed
Welcome

Foundation: PR #3370 (p1, metering crate) → PR #3374 (p2, credit-watcher daemon). Gating PRs stacked on foundation: PR #3126, PR #3127, PR #3128.

claude-code·2h ago
Tutorial

Floating glass panel with 12px insets, 296px width, GLASS_ENTER animation. Containers are chrome; brand color goes on affordances inside.

ZZiyang·yesterday
EngineeringTutorial

Type a question, hit ↵. memax pulls from everything you've dumped — across hubs, agents, file types — ranked by semantic match plus recency.

memax·seed
Tutorial

Every night memax connects the dots — surfaces contradictions, merges duplicates, organizes loose memories into topics. You wake up to a tidier brain.

memax·seed
Welcome

npx memax-cli setup --agent claude-code. One command syncs your settings + memories. Memory flows both ways.

memax·seed
Tutorial
43-mobile. Mobile shell — main → drawer → push-topic → push-memory

No bottom dock. Hamburger summons the rail (slides in from edge, ~75% width, dim backdrop closes). Tapping a topic / memory pushes a full-screen route; or swipe-back pops. Floating ⌘K scan stays bottom-right thumb-zone. Refero: Notion mobile (apps/64), Linear mobile (apps/204).

P
Personal hub

Hi, Jiahao.

Fresh memory7 days
43-routing. URL scheme — Option B (Notion-style namespace)

Hub slug is unique only within (hub_type, owner_id) in packages/server/internal/model/hub.go, so URLs need the owner namespace to disambiguate team hubs across owners.

/h/<owner-slug>/<hub-slug>/memories            ← Memories Overview (default)
/h/<owner-slug>/<hub-slug>/topics/<topic-id>   ← Topic detail
/h/<owner-slug>/<hub-slug>/memories/<memory-id> ← Memory detail v2 (route, not modal)
/brain                                          ← BrainView (existing) → agentic v2 later
/brain/sessions/<session-id>                    ← Future agentic conversation thread
/agents                                         ← ConnectedAgent grid
/agents/<id>                                    ← Agent detail / config
/inbox                                          ← InboxControl (existing renderer, new route)

Adds users.slug column (unique, indexed). Migration shipped in plan 19 alongside the route shell.

Tokens reused. Glass surfaces (--glass-start/end/border), surface-1/2/3 (calibrated foreground opacities), fg-2/3/4 (text opacities), --signature-muted for tinted accents. Brand mark via <MemaxLogo /> from @memaxlabs/ui. Curve scale: rounded-3xl outer shells, rounded-[20px] cards, rounded-lg rows.

Migration. Cookie-flag hybrid via memax_shell=v2. Route by route, flag flips when stable. Old layout removed once all routes migrated.

Plans this informs. 19 (shell foundation), 20 (memory card preset + Compose), 21 (memory detail v2 + recall-time bug), 22 (mobile push-stack), 23 (seed memories admin), 24 (agents + inbox routes). Each ships in 2–3 codex rounds.

40. Borderless Redesign (proposal)

Two phases. Phase 1 = visual frame only (borderless shells, unified headers, loading consistency) — ships first, no backend work. Phase 2 = lifecycle signals (age-based halo, dream-delta on breadcrumb) — pending product-semantics + data-flow read before design locks. Row internals are unchanged in both phases.

1. Recent feed · desktop

Phase 1 · ships first

Outer card disappears; header grammar and row internals stay identical.

Before· carded
新鲜记忆
过去 7 天5
J
Jiahao pushed5m
Retrieval eval runs on 59 queries per commit

Server-side eval harness stays green on the recall threshold…

C
Claude Code captured2h
Hub email invites plan v4 approved

Memax email invites + admin notifications, auth deferred to Ziyang…

Z
Ziyang pushed1d
Fly.io cache warmer needs GCS mount

Worker cache eviction on redeploy caused 800ms cold starts…

After· borderless
新鲜记忆
过去 7 天5
J
Jiahao pushed5m
Retrieval eval runs on 59 queries per commit

Server-side eval harness stays green on the recall threshold…

C
Claude Code captured2h
Hub email invites plan v4 approved

Memax email invites + admin notifications, auth deferred to Ziyang…

Z
Ziyang pushed1d
Fly.io cache warmer needs GCS mount

Worker cache eviction on redeploy caused 800ms cold starts…

2. Mobile topic list · mode C

Phase 1 · ships first

~32px horizontal reclaimed. Pin moves to leading position for pinned rows only; unpinned look as today.

Before· carded · 375px
主题
⊞ ⊟ ≡
Retrieval eval3d
Hub architecture1d
Eval infra and tooling5d
Pricing and packaging2w
Dream engine6h
After· borderless · 375px
主题
⊞ ⊟ ≡
Retrieval eval3d
Hub architecture1d
Eval infra and tooling5d
Pricing and packaging2w
Dream engine6h

5. Loading skeletons

Phase 1 · ships first

Skeleton geometry mirrors final row geometry within ±2px. No card-shaped skeleton on borderless surfaces. Single animate-content-ready fade; no per-row pop-in.

Before· card-shaped stripes
新鲜记忆
After· row-shaped · matches loaded
新鲜记忆

3. Lifecycle signals on a memory row (preview)

Phase 2 · preview

Halo = pure freshness (age-based, < 5min breathes, < 24h static, else off). Dream-touched signal rides on the existing topic breadcrumb chip: signature tint + leading ✦ replaces a separate chip. Processing star is the existing memax state.

新鲜记忆
J
Normal row — seen, settled, no delta
Retrieval eval3d
Z
Fresh · age < 5 min (halo breathes)
Hub architecture2m
R
Fresh · age < 24h (halo static, pure time decay)
Pricing4h
Processing · memax is still remembering (existing)
Remembering…
C
Dream-touched · signal lives on the topic breadcrumb
Eval infra6h
D
Fresh + dream-touched — halo on indicator, tint on breadcrumb
Retrieval eval1h
J
(Killed) old rowAccent left-border — never again

4. Topic cards with dream delta (preview)

Phase 2 · preview

Chip aggregates adds + reorganizations since viewer's last topic visit. Clears when viewer opens the topic. Server-owned; requires topic_visits infrastructure (see Phase 2 brief).

Before· no delta signal
Retrieval eval

Server-side recall harness, 59 queries, graded metrics.

3d
Hub architecture

Team hubs, access model, push routing.

1d
After· delta chip when active
Retrieval eval

Server-side recall harness, 59 queries, graded metrics.

+3 · 2 reorganized3d
Hub architecture

Team hubs, access model, push routing.

1d

What stays unchanged across both phases: row padding, title / summary typography, three-tier attribution, multi-select, desktop drag grip, long-press on mobile, processing star, search highlight, hub pills, batch toolbar, memory detail page, desktop topic grid modes A/B.

Phase 1 → Phase 2: Phase 1 is purely presentational; it leaves memory-row-presentation.ts as the integration point so Phase 2 can resolve halo + breadcrumb tone without touching row internals. No lifecycle state leaks into Phase 1.

41. Lifecycle + Dream Delta — E2E Flow

What a user actually sees as memax captures, organizes, and shows their memories over time. Maps to shipped behavior in commit 11f83406. Two distinct signal families — notifications (attention/review events) and lifecycle (passive browse-context changes) — never overlap.

Halo — client-derived from created_at: <5min breathing → <24h static → off.
Breadcrumb tint — from pending_dream_action, clears on topic visit.
Delta chip — from delta_since_visit, clears on topic open.
Dream history — detail only, durable, never clears.
Act 1

A memory arrives — the halo lifecycle

User pushes or an agent captures a memory. A soft signature halo blooms behind the indicator so the row stands out while it's still fresh. Pure time decay from created_at — no server state, no mutations.

t = 30 seconds
新鲜记忆
J
Retrieval eval runs on 59 queries
30s
Halo state: breathing. Pulses at 2.4s via state-slow-breathe.
t = 4 hours
新鲜记忆
J
Retrieval eval runs on 59 queries
4h
Halo state: static. Still glanceable as new, no longer drawing attention.
t = 2 days
新鲜记忆
J
Retrieval eval runs on 59 queries
2d
Halo state: off. Reads as any other row. No lingering shimmer after 24h.
Act 2

Dreams run overnight — delta appears

Between visits, the dream engine organizes memories. When the user returns: topic card gains an aggregate chip; moved memory's breadcrumb tints signature color. Both resolved server-side, scoped to last topic visit.

Before — yesterday
Retrieval eval

Server-side recall harness, 59 queries, graded metrics.

3d
新鲜记忆
J
Eval harness output format
Eval infra5d
After — dream run overnight
Retrieval eval

Server-side recall harness, 59 queries, graded metrics.

+3 · 2 reorganized3d
新鲜记忆
J
Eval harness output format
Retrieval eval5d
Breadcrumb tint appears because memory.lifecycle.pending_dream_action is non-null. Topic card chip counts are non-overlapping: +3 inbound from unassigned, 2 inter-topic moves.
Act 3

User taps the tinted chip — reason

Curious about what changed, user taps the signature-tinted breadcrumb. Popover anchored to the chip explains what dream did, why. Popover reads from the memory row's already-loaded data — no extra API call.

Tap indicator
J
Eval harness output format
Retrieval eval5d
Moved by dream
Eval infraRetrieval eval
semantic fit with eval harness cluster
Payload source
memory.lifecycle.pending_dream_action
= {
  run_id: "3f4a…",
  action_type: "organize",
  at: "2026-04-16T23:14:02Z",
  from_topic: { id, name: "Eval infra" },
  to_topic: { id, name: "Retrieval eval" },
  reason: "Closer semantic fit…"
}
No new API call to open the popover. Content already in the row's payload from the list read. Instant. If from_topic is out-of-scope the server returns from_topic: null and the popover renders verb + reason only.
Act 4

User visits the topic — everything clears

User taps the topic name. POST /v1/topics/:id/visit fires after 300ms dwell (never prefetch). Client invalidates caches; next fetch returns pending_dream_action null and delta_since_visit null. Both signals gone.

Before visit
Retrieval eval

Server-side recall harness, 59 queries, graded metrics.

+3 · 2 reorganized3d
J
Eval harness output format
Retrieval eval5d
User taps topic → markVisit(topicId) after 300ms dwell.
After visit — next fetch
Retrieval eval

Server-side recall harness, 59 queries, graded metrics.

3d
J
Eval harness output format
Retrieval eval5d
Both signals gone. Server query joins against topic_visits.last_visited_at; visit timestamp is now past the action's created_at, so the scoped lookups return null + empty summary. No client mutation needed beyond cache invalidation.
Act 5

Memory detail — durable dream history

Even after scan-surface signals clear, user can learn what dream did. Detail page's provenance strip carries dream_history (last 10 actions, unscoped by visit). Reads like quiet context — not a callout.

Memory detail page
Eval harness output format
Jiahao·memax·captured 5d ago·recalled 14×
Reorganized by dream
Eval infraRetrieval eval·6h ago·semantic fit with eval harness
UnassignedEval infra·5d ago
Pills, not prose. Section heading carries the single ✦. Each entry renders as a visual from → to transition via the shared TopicChip primitive — same component as the row breadcrumb, so topic identity stays consistent. Organize-from-unassigned uses a CircleOff pseudo-chip.
Clear semantics
pending_dream_action
  • Scoped to viewer's last visit of the memory's current topic.
  • Shows: row breadcrumb tint + tap popover.
  • Clears: on POST /v1/topics/:id/visit.
dream_history
  • Unscoped by visit. Last 10 actions, reverse-chronological.
  • Shows: low-emphasis provenance lines on memory detail.
  • Clears: never. Durable audit for "when did this move?"
delta_since_visit
  • Per-topic aggregate since viewer's last visit.
  • Shows: topic card trailing chip ✦ +N · M reorganized.
  • Clears: on topic open (same visit write).
Act 6

Notifications ≠ lifecycle — the split, visually

Contradictions, dream-run completion, topic-review suggestions, hub invites — all fire as notifications (attention events). Lifecycle only surfaces the four passive actions that don't need user review.

Notifications (review surface)
Inbox
ContradictionThese memories got tangled — which one wins?
DreamDream run completed · 3 organized, 2 reorganized
Topic mergeSuggest merging Pricing into Business model?
User action expected. Resolve, dismiss, or ignore. Inbox seen_at tracks per-user. Lifecycle doesn't touch this surface.
Lifecycle (passive browse)
Fresh memories
J
Hub email invites plan v4 approved
Hub architecture6h
Z
Fly.io cache warmer needs GCS mount
Infrastructure1d
No user action expected. Tinted chips quietly mark what dream moved; user either notices or doesn't. Opening the topic clears the signal. No inbox entry, no badge, no ping.
Rule: Anything that requires a human decision is a notification. Anything that's "memax quietly did this while you were away" is lifecycle. contradiction, dream_run_completed, topic-merge reviews stay notification-only. Lifecycle only renders organize / merge / archive / restructure — the server query enforces it.
What you get free
  • Halo on every fresh memory (no API — pure age math).
  • Breadcrumb tint on any memory dream moved, until you visit the topic.
  • Topic card delta chip showing aggregate activity since last visit.
  • Durable dream history on memory detail — always answers "when did this move?"
What doesn't happen
  • No per-memory acknowledgement table — no client mutation on scroll.
  • No ping/toast/inbox entry for passive dream organization.
  • No halo beyond 24h — no indefinite shimmer.
  • No cross-tenant topic name leak — out-of-scope topics render verb-only.
  • No contradiction verb in lifecycle UI — contradictions are a notification.

36. Inbox

Current shipped notification surfaces in prod. This section mirrors today’s bar and inbox states so you can inspect the real kind coverage without the older north-star mock.

36-now. What ships today

The current system separates three things cleanly: live dream running in the bar, durable notification receipts, and durable decision rows in inbox.

The visuals below are intentionally the shipped ones: current bar card, current inbox row shell, current per-kind bodies, current scaffold/fallback treatment.

36-bar. Current bar notification states

dreaming

Dreaming... · 12 memories in progress

dream_complete

Dream organized 7 memories · last night · 18 scanned

update

Hub switched · memax-test

info

Copied invite link

success

Memory saved

error

Push failed · Try again
36-inbox-decision. Current decision kinds

Decision rows

These are the actionable inbox kinds shipping today.

Inbox
4

Both memories discuss PKCE but give conflicting guidance. Keep one, keep both, or dismiss.

These topics overlap heavily and should likely be consolidated into a single topic.

PKCE
4 memories
OAuth Native
3 memories
OAuth
12 memories

The child topic fits better under Postgres than its current parent.

Connection pool
6 memories
Postgres
14 memories

Annie invited you to memax-test (role: contributor)

36-inbox-receipts. Current receipt kinds

Receipt rows

These render with dismiss-only affordances in the inbox. dream_run_completed also appears as a bar push first.

Inbox
4

Lina joined memax-test (role: viewer)

The API will restart during the maintenance window tonight.

View status

Chris sent you a Memax invite for Growth.

Open invite link

This is the durable inbox receipt after the bar push.

36-inbox-scaffold. Current scaffolded kinds

Scaffold / fallback rows

These kinds are typed and rendered, but still shipped as scaffold/fallback states rather than fully produced polished flows.

Inbox
2

Stale review kind exists in the current system, but only as a scaffolded/fallback row.

Shipped today as a scaffolded stale-review row only.

Low-confidence review kind exists in the current system, but only as a scaffolded/fallback row.

Shipped today as a scaffolded low-confidence row only.

33. Memory Metadata

Topic location, classification framing, tags, and move flows. North star for memory detail page metadata.

33a. Topic Location — Assigned vs Unassigned

Assigned — shows topic name, click to move

Unassigned — dashed border, invite to assign

33b. Destination Picker — Hub → Topic N-level drill-down

Starts from hubs. Click a hub → see its topics. Topics with children drill deeper. Topic rows are the confirm target; hub rows are browse only in the single-memory move flow. Same slide pattern as 33c. Try: Your Topics → Architecture → Backend.

33c. Full Metadata Layout — Assigned Memory

Classification framed as AI output: "✦ memax classified this as". Plain-language sentence only. Boundary (private) hidden — only shown when shared.

Claude Codecaptured·memax·2d ago·recalled 47x
[ content area ]
memax classified this as
durable reference
reactarchitectureserver-components
33d. Full Metadata Layout — Unassigned Memory

Topic location always visible (dashed CTA). Same "memax classified" framing. No "private" noise.

you·1d ago·recalled 3x
[ content area ]
memax classified this as
recent activity
unclear-contentneeds-clarification
33e. Unified Drill-Down Tree — Browse Mode (Mobile)

Mobile tree panel as bottom sheet. Same drill-down as move picker. Tap a parent → drill into children. Shows memory counts. Try: Your Topics → Architecture → Backend.

Topics
33f. Unified Drill-Down Tree — Select Mode (Move)

Same component, mode="select". Hub rows drill into that hub's topics. Topic rows confirm the destination (checkmark). Same bottom sheet container. Identical navigation, different action on tap.

Move to topic
33g. Architecture

1:1 Topic Relationship: Each memory belongs to exactly one topic. memories.batchMove is the authoritative user-move contract — atomic DELETE + INSERT on memory_topics, not confidence-gated. All user surfaces (picker, batch toolbar, detail route, drag-and-drop, CLI) route through it via useMemoryMove on React Query useMutation.

AssignMemoryToTopic: confidence-gated auto-assignment used only by ingest + dreams workers. Replaying at equal confidence is a no-op by design so earlier user intent sticks.

Unified DrillDownTree:One component, two modes. mode="browse": tap navigates to topic page (tree panel). mode="select": tap picks destination (move picker). Container varies: bottom sheet (mobile), popover (desktop move), pinned sidebar (desktop browse uses expand/collapse instead). Drill-down everywhere except pinned desktop sidebar.

"memax classified" framing: Keep the label, but the body is a plain-language sentence rather than a taxonomy control. Tags remain editable because they are directly useful to the user.

Boundary (private/shared): Hidden when private (the default ~95% case). Only shown as a badge when shared. Moved to provenance strip context where it semantically belongs.

34. Landing + Onboarding

Landing page refresh (headlines, demo, CTAs) and post-login onboarding flow design.

Landing Page

Headline Options — pick one
Option A — Portable memory

Your memory. Every AI.

Stop re-explaining yourself every time you switch tools.

Option B — Agent-first

One memory. Every agent.

Your context follows you — across Claude, Cursor, ChatGPT, and every AI you use.

Option C — Provocative

Your AI forgets you. memax doesn't.

Shared memory for humans and AI agents.

34-demo. Landing demo — Claude → Ziyang handoff (the killer story)

Based on a real moment (memax memory aa92bcea): Claude discovered a memax API gap while helping Jiahao, self-flagged as unverified, and pushed a structured issue directly to the engineering hub for Ziyang — who verified and shipped the fix 43 minutes later. No Jira. No Slack DM.

Why this is the killer demo: (1) it's real; (2) it shows AI as a responsible teammate, not just a retrieval tool; (3) source_agent: claude-aiis the trust mechanism — memax makes AI contributions traceable and auditable, not a black box; (4) cross-agent + cross-person + zero-friction handoff is a product-driven story, not generic “AI good”.

1
Conversation
claude.ai
What's new in my memax team hub?
I tried recall but hit a gap — the memax_recall API lacks hub_id+ author filters, so I can't scope this properly. Let me log it.
2
AI self-flags + pushes
source_agent: claude-ai
engineering hub
needs-human-verificationp1

API gap: memax_recall missing hub_id + author filters

Can't scope recall to a single hub or author. Suggested fix: add filters to /v1/recall query params.

3
Teammate ships
Ziyangnext morning · Cursor
Anything flagged in the eng hub I should pick up today?

Claude pushed an API gap yesterday — recall is missing hub_id + author filters. Tagged p1, needs verification. Open in memax →

Verified · PR #52 merged, 43m later

Your AI found a bug. Your teammate got the fix request. No one opened Jira.

34-features. Reinforcement strip — 4 quiet value blocks

Below the handoff demo, above the CTAs. Four distinct value props the demo alone doesn't communicate explicitly: dump-from-anywhere (surface coverage), ask-from-anywhere (the headline made literal), self-organizing (Dreams, memax-unique), team memory (the handoff payoff stated plainly).

Quiet treatment — no card chrome, no borders, no shadows, just a tiny icon (h-4 w-4 text-fg-3) + 14px medium title + 13px fg-3 description. 2×2 on mobile, 4-col on desktop. Reinforcement strip, not a feature wall. The Claude→Ziyang demo above does the storytelling; this row covers the audiences (non-tech, team buyers) the demo doesn't serve as well. We deliberately do NOT add a competing tagline below the headline — the current sublineFull( “Stop re-explaining yourself every time you switch tools.”) is sharper because it has a single emotional hook (the pain). Two sublines fight each other; pick the pain hook over the feature list.

Dump from anywhere

Every surface is an entry point. CLI, web, every AI you use.

Ask from anywhere

Your context follows you. Any agent, instant recall.

Self-organizing

You dump. Memax organizes.

Team memory

Your teammate’s AI knows what yours knows.

CTA Options
Current
npx memax-cli@latest login && npx memax-cli@latest setup --all
Proposed
npx memax-cli@latest login && npx memax-cli@latest setup --all

Post-Login Onboarding

Inline cards replace the empty dashboard. Steps animate away as completed — container morphing, not overlays.

Screen 1 — Dump, Ask, Discover (shown once)

Dump

Throw anything in — text, files, or let your agents capture it automatically. No organizing needed.

Ask

Ask from anywhere — CLI, web, or your AI agent. Get one clear answer, not a list of results.

Discover

Come back tomorrow. memax organizes your knowledge into topics and surfaces what changed.

Interfaces: CLI · MCP · Web
Screen 2 — How do you work? (interactive)

How do you work? (click to select)

Track: CLI-first (3 steps)
1
Set up your agents
$ npx memax-cli@latest login && npx memax-cli@latest setup --all
Configured: Claude Code, Cursor, Copilot, Windsurf

One command configures all your agents.

2
Push your first memory
$ memax push "I prefer TypeScript, use pnpm, deploy to Vercel"
Remembered.

Push a preference, a fact, a decision your agents should know.

3
Now recall it
$ memax recall "what's my deploy target?"
You deploy to Vercel, using pnpm and TypeScript.

This works from any agent, any device.

Track: Agent-first (3 steps)
1
Your agent is ready

Claude Code

Connected via MCP

2
Tell your agent something

Remember that I prefer dark mode and use vim bindings

Claude Code

Got it — saved to your memax. All your agents will know this now.

3
Now ask your agent

What are my editor preferences?

Claude Code

You prefer dark mode and use vim bindings.

This works from Cursor, Copilot, or any connected agent.

Track: Web-first (3 steps)
1
You're already here
Type anything and press Enter to remember...Enter

The bar is your entry point. Just type.

2
Push your first memory
I prefer TypeScript, use pnpm, deploy to VercelEnter

Press Enter. Remembered.

3
Switch to recall
PressTabto switch modes
what's my deploy target?Enter
memax

You deploy to Vercel, using pnpm and TypeScript.

Team Track (after personal, optional)
1
Name your team's shared brain
engineering
2
Invite a teammate
ziyang@team.com

Or share a join link — one click to join.

3
Push something your team should know
$ memax push --hub engineering "Our staging env is staging.memax.dev, deploy via fly deploy"
Remembered in engineering hub.
4
The magic

Ziyang's Cursor asked

“Where do we deploy staging?”

Staging is at staging.memax.dev. Deploy via fly deploy.— from Jiahao, engineering hub

Your teammate's agent just recalled your knowledge. No Slack DM, no wiki search.

End-to-end walkthrough

Click through the full inline journey so you can see the state machine in motion. Welcome → preference → track → team prompt → completed. Each phase morphs in via AnimatePresence with the memax NORMAL + EASE spring.

34-e2e. Interactive walkthrough — full new-user flow

The full first-run journey as a single connected demo. Stepper at the top tracks the 5 phases; each phase morphs in via framer-motion. Click “Get started” → pick a preference → see the matching track → optional team prompt → completion. Reset button replays from the top.

Reuses the existing track demos (CLITrackDemo, AgentTrackDemo, WebTrackDemo) inside the track phase, so this card is the connective tissue not a duplicate of those flows. Stepped modals (§34-modal-agents + §34-modal-hub) are demoed separately — in prod the “Create a team hub” button on the team prompt phase launches §34-modal-hub; here the demo advances directly to completed for clarity. Codex should wire the actual modal handoff at production implementation time. State machine columns: onboarding_current_screen tracks the active phase, onboarding_completed_at gets stamped at the completed phase, and onboarding_dismissed_at allows resume from any cursor (see §34-state).

1
Welcome
2
How you work
3
Your track
4
Team
5
Done

Welcome to memax

Your memory layer across every AI. Dump anything in, ask from anywhere, come back tomorrow to find it organized.

Stepped modals — 2 only

Memax design DNA: container morph over overlays, inline wins by default. Modals are reserved for two flows that are settings-heavy + one-off + high-stakes + sequential: connecting agents, and creating/joining a team hub. Everything else is inline.

34-modal-agents. Modal A — Connect your agents (4 steps, interactive)

Invoked from the inline Agent-track prompt OR from Settings → Integrations. Four steps: Detect installed agents → Run one command with live terminal progress → Verify MCP handshake per agent → Done.

Why modal: 5–6 agents to configure in sequence, user needs focus on terminal progress, one-time flow, high information density. Reuses the settings-dialogpattern already in memax (same chrome, Esc to close, same glass treatment). Click “Copy & run” below to see the progress animation.

1 / 4

Connect your agents

memax works across the AI tools you already use.

Detected on this machine:

Claude Code

found

Cursor

found

GitHub Copilot

found

Windsurf

not installed

ChatGPT Desktop

found

Gemini CLI

not installed

34-modal-hub. Modal B — Create or join a team hub (5 steps, interactive)

Invoked from the inline team prompt, the hub switcher → “Create hub”, OR from an invite link landing. Five steps: Intent (create vs join) → Details (name + description + visibility; or paste invite link) → Invite teammates (skippable) → First push to prime the hub (skippable) → Done.

Why modal: creating an org boundary is high-stakes (name is public, members have memory access), multi-field form needs focus, the first-push priming step needs user attention without dashboard distraction. Steps 3 and 4 are explicitly skippable — invites and first push can happen anytime.

1 / 5

Team hub

Shared memory between people and agents. One place your team remembers from.

Fast path — no tutorial

The best onboarding for users who live in agents isn't a tutorial. It's the product working for them before they visit the web app. This is a visual reference for Codex — not a card the user ever sees.

34-fast-path. Silent ambient onboarding (CLI → aha moment)

For developers who run npx memax-cli setup and close the terminal, the aha moment happens ambiently the next time they open an AI agent and the hook injects context. No guided tour required.

The first web app visit is voluntary and curious, not instructed. The empty dashboard isn't empty — Dreams have already organized imported memories into topics. Web users still get the inline onboarding cards; CLI users skip them entirely (first-run detection checks if the user already has memories on login and hides the welcome card).

npx memax-cli login

0:00

GitHub OAuth. 10 seconds.

memax setup

0:10

Auto-detects installed agents. Configures MCP + hooks. Imports existing config files: MEMORY.md, .cursorrules, CLAUDE.md, AGENTS.md.

✓ Imported 23 memories from 4 agents

0:45

User closes the terminal and goes back to work.

Dreams organize imported memories into topics

— silent period —

User is doing something else. The product is working for them.

User opens Claude Code on a new task

Next morning

Memax hook injects context. Claude says: “based on your preferences, you use pnpm + TypeScript + Vercel.”

“wait, how does it know that?”

+5 seconds

The aha moment. No tutorial. No guided tour. The product just remembered. User opens the memax web app — curious, not instructed.

State machine — first-run detection + recovery

34-state. Onboarding state machine (Codex implementation guide)

Backend tracks two timestamps on the user row: onboarding_completed_at and onboarding_dismissed_at, plus a resume cursor onboarding_current_screen. Frontend renders the appropriate surface based on which state the user is in.

Key recovery rule: dismissing is never permanent. The dashboard always renders real state, but a small “Finish setup” pill appears in the top bar whenever dismissed_at IS NOT NULL AND completed_at IS NULL. Clicking it resumes at the saved cursor. Completed users see “Onboarding tour” in Settings to replay.

not_started

onboarding_completed_at IS NULL AND dismissed_at IS NULL

Welcome card (screen 1) inline on empty dashboard

User clicks [Get started] → in_progress

in_progress

current_screen IN (preference, track, team)

Current inline card morphs in place (§29n container morph)

Track complete OR user dismisses → completed / dismissed

modal_agents

Agent track triggered agents modal

ModalShell with 4-step connect flow

Close / Open memax → back to previous inline state

modal_hub

Team prompt OR hub switcher → create hub

ModalShell with 5-step create/join flow

Close / Open hub → back to previous inline state

completed

onboarding_completed_at IS NOT NULL

Dashboard renders real state. No onboarding cards.

Re-entry via Settings → Onboarding tour

dismissed

dismissed_at IS NOT NULL AND completed_at IS NULL

Dashboard real state + small persistent 'Finish setup' pill in top bar

Click pill → resume at saved current_screen

34-rules. Landing + Onboarding design rules
1.

CLI, MCP, and web are equal-class surfaces. Landing copy never frames memax as “developer-first. ” The web app is a legitimate primary surface for non-technical users, not a viewer for the CLI. Lead with the universal value prop (“Your memory. Every AI.”) and let CLI / Agent / Web appear as equal choices in the onboarding preference picker. See memax memory 18d58b0e.

2.

Landing hero is the validated positioning. Headline: “Your memory. Every AI.” Subline: “Stop re-explaining yourself every time you switch tools.” CTA: “Give your AI a memory” + the one-line install command. Single-screen, static, Linear/Vercel restraint. No purple, no gradients, no glow.

3.

Landing demo is the Claude → Ziyang handoff story. 3-panel horizontal strip (stacked on mobile): (1) Jiahao asks Claude about team hub activity, Claude discovers an API gap; (2) Claude self-flags as source_agent: claude-ai + needs-human-verificationand pushes a structured report to the hub; (3) Ziyang's Cursor surfaces it next morning, ships the fix 43 min later. Tagline: “Your AI found a bug. Your teammate got the fix request. No one opened Jira.” Shows AI as a responsible teammate + source_agent as the trust mechanism. See §34-demo.

4.

Two-speed onboarding: fast path for CLI, inline for web. CLI users who complete npx memax-cli setup skip the welcome/preference/track cards entirely — first-run detection hides them if the user already has imported memories on first login. Their aha moment is ambient (the next agent session shows context injected by the hook). Web-first users get the inline cards: welcome → preference picker → track steps → optional team prompt. See §34-fast-path + memax memory 00e29042.

5.

Inline cards by default, modals for settings-heavy flows only. Memax design DNA: container morph over overlays. Inline wins by default because it lets users experiment alongside the guidance without feeling interrupted. Modals are reserved for flows that are settings-heavy + one-off + high-stakes + sequential. By those criteria, exactly two modalsqualify: “Connect your agents” (§34-modal-agents) and “Create or join a team hub” (§34-modal-hub). Everything else — preference picker, track steps, dreams explanation, feature tooltips, “connect one more agent” nudges — stays inline.

6.

Modals are invoked from multiple entry points. The agents modal is reachable from the Agent track inline card AND from Settings → Integrations AND from a top-bar “Connect more agents” notification. The hub modal is reachable from the team prompt inline card AND from the hub switcher “Create hub” option AND from an invite link landing page. Both modals are full-flows each time — they don't depend on onboarding state.

7.

Onboarding is replayable, not one-shot. After completion, users can replay the inline sequence from Settings → Onboarding tour. Dismissing mid-flow is not permanent: a small “Finish setup” pill appears in the top bar until the user either resumes or explicitly marks setup complete. State machine: see §34-state.

8.

Modal chrome matches settings-dialog pattern. Both modals use ModalShell: glass panel (oklch 0.96 opacity + 24px blur + 140% saturate + border), max-height 640px, overflow-y: auto on body, sticky header with step dots + step count, sticky footer with Back/Skip/Continue. Step transitions use framer-motion slide + fade (NORMAL + EASE), reduced-motion fallback to opacity-only. Esc closes; click-outside closes. Matches the existing prod settings-dialog.tsx treatment.

i18n: All strings need translation keys before production implementation. Kitchen has // i18n: TODO markers next to literal strings — Codex must replace them.

Landing: packages/web/src/components/landing/landing-full.tsx

Onboarding inline cards: new component in brain-view.tsx empty state (container morph, not overlay).

Modals: new OnboardingAgentsModal and OnboardingHubModal built on the existing settings-dialogpattern. Reuse don't recreate the modal chrome.

State machine: add onboarding_completed_at, onboarding_dismissed_at, onboarding_current_screen columns on the user table.

Super-notif approach — plan 18 RFC

Alternative to the preference-based track flow above. Onboarding is two notifications (founder note + checklist), rendered on /memoriesvia a pinned region. Reuses plan 17's notifications framework — no new table, no onboarding-specific SDK surface. See docs/plans/18-onboarding-journey.md.

34-sn-founder. Founder note — kind=system_notice, source_kind=onboarding_welcome

Pinned on /memories via payload.pin_context=memories_hero. One-shot per user — not versioned on restart ( notifications_source_unique intentionally blocks replay). Dismissal is a single POST /v1/notifications/{id}/dismiss.

Hi, I'm Jiahao.

Your AI forgets you every session. You re-explain the same context, re-paste the same link, start from scratch every time you switch tools. It's exhausting.

memax is a shared brain that makes it stop. Dump anything — your agents pull what they need, your team sees what you saw, and a dream engine connects the dots while you sleep.

We ship every Friday. Things will break; tell us when they do.

— Jiahao

Come say hi in our community — coming soon

34-sn-checklist. Checklist — kind=checklist, all lifecycle states

Each state at rest. Required items (connect_agent, first_memory, first_ask, five_memories) gate auto-resolve. Weakly-required items (first_hub_invite, first_dream) are discovery hooks — auto-resolve fires at 4/7 required.

State A — first visit · 0/7

Your first week

0 of 7 done · 0/4 required

Read the welcome note

90 seconds. Worth it.

Connect your first agent

Claude Code, Cursor, Codex — one command, memory flows both ways.

Dump your first memory

Type anything in the bar, hit ⌘↵. A wiki, a link, a half-thought.

Ask memax a question

Press ↵ instead of ⌘↵ — memax answers from everything you've dumped.

Dump 5 memories

memax needs a critical mass to start seeing patterns. Unlocks dreams.

0/5

Join or start a team hub

Shared brain with people you work with. Jump into an existing hub or start your own.

Let memax dream

Unlocks after you dump 5 memories.

State B — mid-flow · 3/7 (welcome + connect + memory done, 3/5 progress)

Your first week

3 of 7 done · 2/4 required

Read the welcome note

Connected your first agent

First memory saved

Ask memax a question

Press ↵ instead of ⌘↵ — memax answers from everything you've dumped.

Dump 5 memories

memax needs a critical mass to start seeing patterns. Unlocks dreams.

3/5

Join or start a team hub

Shared brain with people you work with. Jump into an existing hub or start your own.

Let memax dream

Unlocks after you dump 5 memories.

State C — collapsed strip · 3/7

State D — auto-resolve celebration (all required done)

Your first week

All set — memax dreams nightly now.

Read the welcome note

90 seconds. Worth it.

Connected your first agent

First memory saved

Asked your first question

5 memories — dreams unlocked

Join or start a team hub

Shared brain with people you work with. Jump into an existing hub or start your own.

Let memax dream

Dreams run nightly to connect the dots. Trigger one now to see it live.

You're all set up

memax dreams nightly now. See you tomorrow.

State E — all 7 done (pre-unmount)

Your first week

All set — memax dreams nightly now.

Read the welcome note

Connected your first agent

First memory saved

Asked your first question

5 memories — dreams unlocked

You're on a team hub

First dream complete

34-sn-memories. /memories composition — first visit, stacked pinned region

Single mount of <PinnedNotifications context="memories_hero" /> between <HubHeader /> and <RecentSection /> in topic-grid.tsx:219. Renders zero, one, or both notifications depending on state. Page rhythm is identical to today once the region is empty.

/memories

Your brain

Personal hub · 0 memories · 0 topics

Hi, I'm Jiahao.

Your AI forgets you every session. You re-explain the same context, re-paste the same link, start from scratch every time you switch tools. It's exhausting.

memax is a shared brain that makes it stop. Dump anything — your agents pull what they need, your team sees what you saw, and a dream engine connects the dots while you sleep.

We ship every Friday. Things will break; tell us when they do.

— Jiahao

Come say hi in our community — coming soon

Your first week

0 of 7 done · 0/4 required

Read the welcome note

90 seconds. Worth it.

Connect your first agent

Claude Code, Cursor, Codex — one command, memory flows both ways.

Dump your first memory

Type anything in the bar, hit ⌘↵. A wiki, a link, a half-thought.

Ask memax a question

Press ↵ instead of ⌘↵ — memax answers from everything you've dumped.

Dump 5 memories

memax needs a critical mass to start seeing patterns. Unlocks dreams.

0/5

Join or start a team hub

Shared brain with people you work with. Jump into an existing hub or start your own.

Let memax dream

Unlocks after you dump 5 memories.

Fresh Memory
No memories yet. Dump something in the bar to get started.

Topics

Topics will appear after your first dream run.

34-sn-inbox. Inbox row — compact + expand-in-place

Same component, different variant prop. Compact row sits in the Needs action bucket (checklist is a decision kind). Click the chevron to expand inside the existing InboxRow shell — no modal. Dismiss here or on /memories hits the same row; SSE propagates.

Compact row (Needs action bucket)

Your first week

3/7 · keep going to unlock dreams

Sits among other inbox rows. Click chevron → expand in place.

Expand-in-place (same InboxRow shell)

Your first week

3/7 · keep going to unlock dreams

34-sn-e2e. Interactive lifecycle — click through the 9 scenes

Full journey from signup to auto-resolve. Each scene maps to a notification state transition. Transitions use memax NORMAL + EASE motion tokens. Celebration pulse = scene 8; the page returns to its normal rhythm at scene 9 after unmount.

signup → emit note + checklist

/memories

Your brain

Personal hub · 0 memories · 0 topics

Hi, I'm Jiahao.

Your AI forgets you every session. You re-explain the same context, re-paste the same link, start from scratch every time you switch tools. It's exhausting.

memax is a shared brain that makes it stop. Dump anything — your agents pull what they need, your team sees what you saw, and a dream engine connects the dots while you sleep.

We ship every Friday. Things will break; tell us when they do.

— Jiahao

Come say hi in our community — coming soon

Your first week

0 of 7 done · 0/4 required

Read the welcome note

90 seconds. Worth it.

Connect your first agent

Claude Code, Cursor, Codex — one command, memory flows both ways.

Dump your first memory

Type anything in the bar, hit ⌘↵. A wiki, a link, a half-thought.

Ask memax a question

Press ↵ instead of ⌘↵ — memax answers from everything you've dumped.

Dump 5 memories

memax needs a critical mass to start seeing patterns. Unlocks dreams.

0/5

Join or start a team hub

Shared brain with people you work with. Jump into an existing hub or start your own.

Let memax dream

Unlocks after you dump 5 memories.

34-sn-mobile. Mobile layout — 360px viewport

Items stack vertically; progress bar spans full width; CTA chips wrap below the description on narrow rows. Same card chrome as desktop — the container-morphing principle holds across breakpoints.

Your brain

Personal hub · 0 memories · 0 topics

Hi, I'm Jiahao.

Your AI forgets you every session. You re-explain the same context, re-paste the same link, start from scratch every time you switch tools. It's exhausting.

memax is a shared brain that makes it stop. Dump anything — your agents pull what they need, your team sees what you saw, and a dream engine connects the dots while you sleep.

We ship every Friday. Things will break; tell us when they do.

— Jiahao

Come say hi in our community — coming soon

Your first week

2 of 7 done · 1/4 required

Read the welcome note

Connect your first agent

Claude Code, Cursor, Codex — one command, memory flows both ways.

First memory saved

Ask memax a question

Press ↵ instead of ⌘↵ — memax answers from everything you've dumped.

Dump 5 memories

memax needs a critical mass to start seeing patterns. Unlocks dreams.

2/5

Join or start a team hub

Shared brain with people you work with. Jump into an existing hub or start your own.

Let memax dream

Unlocks after you dump 5 memories.

Copy lives in i18n. All strings under onboarding.welcome.* and onboarding.checklist.items.*. See RFC §5.5 for the full key list.

Motion. All transitions use NORMAL + EASE; the celebration pulse uses a soft overshoot ([0.34, 1.56, 0.64, 1]) over 500ms.

Design. No dividers: the hairline between the card header and item list is a 5% foreground tone shift, not a <hr />. Glass card surface, signature-accented dots, shadow-premium.

30. Mobile Dock Navigation

3-tab dock: Brain (✦ dump/ask + recent feed), Topics (📚 topics + inbox), Discover (✧ dreams + insights). Bar coexists above dock on Brain tab. Phase 1: Brain + Topics. Phase 2: add Discover.

30a. Three tabs — Brain / Topics / Discover

Each tab has ONE job. Brain = interact (dump + ask). Topics = browse (topics + inbox). Discover = insight (dreams + reports). No overlap.

↑ remember✦ recall
Recent
API rate limit decision2m
Deploy rollback procedure1h
OAuth token refresh bug3h
Team standup notes5h
Search or remember...

Brain ✦ — dump/ask + recent feed (default tab)

Your Topics

14 memories · 4 topics

Topics
Auth(23)
Go(31)
Deploy(47)
Frontend(22)
Inbox3
New design token spec1h
Pricing feedback4h
CI/CD pipeline fix1d
Topics

Topics 📚 — topics + inbox (bar hidden)

Dream Reports

Weekly insights, contradiction reviews, and knowledge consolidation reports. memax works while you sleep.

Coming in Phase 2
Discover

Discover ✧ — dreams + insights (Phase 2)

30b. Brain tab — recall full-screen takeover (Perplexity pattern)

User taps bar → switches to recall → results sheet rises to 90vh. AI synthesis streams below results. The bar stays anchored at bottom as input. This is NOT the expand slot — it's a full-screen sheet.

Recent
API rate limit decision2m
Deploy rollback procedure1h
OAuth token refresh bug3h
Search or remember...

Idle — recent feed + bar

Auth & Security(3)
OAuth token refresh92%
JWT validation strategy87%
API key rotation81%
Deployment(1)
Staging deploy guide78%

Your API rate limit is set to 100 req/min for free tier and 1000 req/min for Pro. The decision was made in the April security review...

What's our API rate limit?

Recall — results fill 90vh above bar

30c. Dock component — spec

Brain active

Topics active

Topics

Discover active

Discover

Brain tab: bar (glass pill) + naked nav dots below (no container).

Other tabs: dock (glass pill) with active label center, icon-only sides.

NEVER two glass pills stacked. Bar and dock alternate.

Nav dots: fg-4 icons, active = foreground/signature. gap-6, py-1.5.

Dock sides: icon-only, fg-3, w-11 min-h-11 (44px touch).

Dock center: icon + label, 14px font-medium, foreground.

Glass pill: mx-3, rounded-2xl, --bar-bg, --bar-border, backdrop-blur.

30d. Morphing behavior rules
1.

Brain active: Center morphs into input field. Typing, recall results, compose all happen through this element. Mode pills float above. Expand slot grows upward.

2.

Tab switch: Center crossfades from input → label (or label → input). Side icons swap positions. Spring transition, FAST (0.15s).

3.

Memory detail: Entire dock hidden (slide down + fade). Full-screen reading. Back gesture restores dock.

4.

Recall (Brain): Results expand upward from the dock (same expand slot pattern). Side tabs remain tappable — user can switch to Topics mid-recall.

5.

Batch toolbar: Morphs into center area (replacing input or label). Side tabs stay. Same container.

30e. Implementation phases

Phase 1 (now): 2 tabs — Brain + Topics

  • Add MobileDock component (fixed bottom-0, 2 tabs)
  • Brain tab: current /home with recent feed + bar
  • Topics tab: current /memories grid
  • Bar bottom offset: isMobile ? dock.height + safe-area : current
  • Hide dock on /memories/[id] detail pages

Phase 2 (dream UI ships): add Discover tab

  • Dream report production UI
  • Contradiction review inbox
  • Weekly insights surface
  • Badge on Discover tab (“3 new insights”)

Phase 3: polish

  • Tab switch micro-animation (active indicator spring)
  • Haptic feedback on tab tap
  • Brain tab recall full-screen sheet (Perplexity pattern)
  • Fullscreen compose (3+ lines expand)
Design rules — mobile dock
1.

Each tab has ONE job. Brain = interact. Topics = browse. Discover = insight. No overlap, no multi-purpose tabs.

2.

Brain is home. Default tab on app open. Shows recent feed when idle — user always sees content, never an empty screen.

3.

Recall = full screen. 90vh sheet on mobile. Not cramped in expand slot. Perplexity pattern: results fill the viewport.

4.

Dock is navigation, bar is action. Dock switches contexts. Bar captures input. They coexist but serve different purposes.

5.

Settings is NOT a tab. Low-frequency action stays in avatar dropdown. Dock is for daily workflows only.

6.

Desktop unchanged. Dock is mobile-only. Desktop keeps current bar-centric Spotlight model.

21. Explorations

Proposed / Not Yet Implemented

Proposed / Not Yet Implemented

Signature color proposals

Option A

#e8956a warm peach

Option B

#d4845e terra

Option C

#c9916e sand

Current (emerald)

#10b981 emerald

Navigation dots
home
memories
dreams
settings
Ghost card breathing

22. Coverage Gaps

State coverage matrix — what's implemented, partial, or missing across every surface.

Status matrix
LocationLoadingEmptyErrorProcessing
Bar recall
Bar remember
Bar AI synthesis
Memory grid
Note detail
Settings⚠️
Brain view
Topics page
Topic detail
Topic tree panel
Recent section

✅ implemented · ⚠️ partial · ❌ missing · — N/A

Current focus: loading and async-state consistency, not baseline error coverage

memax design system · Foundations → Components → Patterns · docs/design/memax-design-system.md