Skip to content

Commit 53fc822

Browse files
authored
Merge pull request #128 from hypercerts-org/feat/positioning-redesign
feat/positioning-redesign -> staging
2 parents e09f777 + ec8c6de commit 53fc822

626 files changed

Lines changed: 98723 additions & 5069 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.local.example

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,52 @@ COOKIE_SECRET=dev-secret-change-in-production
2424
UPSTASH_REDIS_REST_URL=
2525
UPSTASH_REDIS_REST_TOKEN=
2626

27+
# Magic Indexer GraphQL endpoint and DID.
28+
#
29+
# INDEXER_URL is consumed by the server-side /api/indexer and /api/notifications
30+
# proxies. If unset, the code falls back to NEXT_PUBLIC_INDEXER_URL, and then to
31+
# a hardcoded fallback (magic-indexer-prod.up.railway.app). For local dev
32+
# against the dev indexer, set this to magic-indexer-dev.up.railway.app.
33+
#
34+
# INDEXER_DID is required for the notifications JWT `aud` claim. Without it,
35+
# /api/notifications returns 503 and logs a module-load warning.
36+
INDEXER_URL=https://magic-indexer-prod.up.railway.app/graphql
37+
INDEXER_DID=
38+
39+
# Deprecated alias for INDEXER_URL; still read for backwards-compat. Prefer
40+
# INDEXER_URL above and leave this unset on new deploys.
41+
# NEXT_PUBLIC_INDEXER_URL=
42+
43+
# Optional indexer fast-path for /api/resolve-did (default false). When
44+
# "true", resolve-did reads identity (handle + the Bluesky profile block)
45+
# from the indexer's actorProfile(did) query instead of fanning out to
46+
# resolveHandle + app.bsky.actor.getProfile for DIDs the indexer has
47+
# backfilled. Falls back to the legacy path per-field on any indexer miss,
48+
# so it's safe to leave off until the actorProfile query is deployed on the
49+
# targeted indexer. Leave unset (or "false") to keep today's behaviour.
50+
# RESOLVE_DID_USE_INDEXER=false
51+
52+
# Optional rate-limit bypass key for the app's server-side indexer traffic.
53+
# Sent as `X-RateLimit-Bypass` by the /api/indexer, /api/notifications, and
54+
# resolve-did upstream fetches so the app's own proxied requests aren't
55+
# throttled by the indexer's per-IP /graphql rate limiter. Must match the
56+
# indexer's GRAPHQL_RATE_LIMIT_BYPASS_KEY. The header is sent only when this
57+
# is non-empty; only needed once the indexer enables its GraphQL rate limiter.
58+
# INDEXER_RATELIMIT_BYPASS_KEY=
59+
60+
# Optional: Group service URL and DID (CGS). Defaults to the production CGS.
61+
# NEXT_PUBLIC_GROUP_SERVICE_URL=https://groups.certified.app
62+
# NEXT_PUBLIC_GROUP_SERVICE_DID=did:web:groups.certified.app
63+
64+
# Optional: Stadia Maps API key for map tiles. When unset, the map falls back
65+
# to Carto tiles (also free for basic use).
66+
#
67+
# Note: This is a NEXT_PUBLIC_ var, so the key is inlined into the client
68+
# bundle — it is NOT secret. Stadia's intended enforcement is per-domain
69+
# Referer allowlist configured on the Stadia dashboard. Set the allowlist
70+
# there to your production domain(s) before relying on the key in prod.
71+
# NEXT_PUBLIC_STADIA_API_KEY=
72+
2773
# Optional: Set to enable confidential client (private_key_jwt) authentication
2874
# in production. Ignored in loopback dev mode (the spec mandates
2975
# token_endpoint_auth_method: none for loopback clients).

.github/workflows/ci.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- staging
7+
- main
8+
9+
jobs:
10+
verify:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- name: Checkout
14+
uses: actions/checkout@v4
15+
16+
- name: Set up Node.js
17+
uses: actions/setup-node@v4
18+
with:
19+
node-version: '20.19.0'
20+
cache: npm
21+
22+
- name: Install dependencies
23+
run: npm ci
24+
25+
- name: Lint
26+
run: npm run lint
27+
28+
- name: Type check
29+
run: npx tsc --noEmit
30+
31+
- name: Type check (tests)
32+
run: npm run typecheck:test
33+
34+
- name: Test
35+
run: npm test

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,4 @@ yarn-error.log*
4040
# typescript
4141
*.tsbuildinfo
4242
next-env.d.ts
43-
core
43+
/core

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
20.19.0

AGENTS.md

Lines changed: 187 additions & 47 deletions
Large diffs are not rendered by default.

CHANGES.md

Lines changed: 677 additions & 0 deletions
Large diffs are not rendered by default.

CLAUDE.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# CLAUDE.md — certified-app
2+
3+
Claude Code reads this file at session start. The longer agent reference is `AGENTS.md`; the design system is `DESIGN.md`. This file holds the **hard rules** that drift most often and the **first checks** to run before any UI change.
4+
5+
## Hard rules (do not violate)
6+
7+
1. **`border-radius` is `var(--radius)` (2 px), `999px` for pills, `50%` for circles.** Never `4`, `6`, `8`, `12`, `16`, `20` px. The sign-in modal is not an exception.
8+
2. **No raw hex / rgb / rgba** outside `src/app/styles/tokens.css` and `src/app/styles/landing.css`. Everywhere else uses tokens (`--bg-canvas`, `--fg-primary`, `--border-default`, …).
9+
3. **Breakpoints are 800 / 1100 / 1300 only.** "Below desktop" → `@media (max-width: 799px)`. Don't introduce 768 / 760 / 640.
10+
4. **Shadows are `var(--shadow-sm | --shadow-md | --shadow-lg)`.** No `box-shadow: 0 X Y rgba(...)`.
11+
5. **Z-index is a `--z-*` token.** No literal numbers.
12+
6. **Headings use the canonical scale + `font-headline`.** `text-h1`/`text-h2`/`text-h3`/`text-h4`. Never `text-xl` / `text-lg` / `text-2xl` for app headings.
13+
7. **Modals use `<AppDialog>`** (or `<ConfirmDialog>` / `<DeleteRecordDialog>` which wrap it). Never hand-roll a backdrop + `useFocusTrap`.
14+
8. **Icon-only buttons are `<Button size="icon" aria-label="…">`.** The TypeScript discriminated union enforces `aria-label`.
15+
9. **Dark mode must work.** Toggle `data-theme="dark"` on `<html>` and verify. Don't pin colors so they break the flip. Landing must also flip (use `--color-navy` / `--color-off-white` — theme-aware — not `--color-primary` / `--color-white` which are invariant).
16+
10. **Reach for a primitive in `src/components/ui/` before writing a new component or BEM class.** The audit that produced this rule found 12 button vocabularies and 8 card families coexisting — don't restart that drift.
17+
18+
## First checks before merging a UI change
19+
20+
```bash
21+
# 1. No off-spec radii
22+
grep -rEn "border-radius:\s+(4|6|8|12|16|20)px" src/app/styles/
23+
24+
# 2. No non-canonical breakpoints
25+
grep -rEn "@media.*?\((max|min)-width:\s*(76[08]|64[80])px\)" src/app/styles/
26+
27+
# 3. No legal-style headings on app pages
28+
grep -rn "font-headline text-\(xl\|lg\|2xl\)" src/app/ src/components/
29+
30+
# 4. No hand-rolled modal backdrops
31+
grep -rn "signin-modal__backdrop" src/components/ | grep -v "ui/sign-in-modal.tsx\|ui/feedback-modal.tsx"
32+
33+
# 5. Type + lint must be clean
34+
npx tsc --noEmit
35+
npm run lint
36+
```
37+
38+
All five should be silent (or, for lint, no change vs. main's baseline of 60 warnings).
39+
40+
## Where to look
41+
42+
- **Design rules:** `DESIGN.md` — read §14 first ("Rules at a glance" and the consolidation pass).
43+
- **Architecture / conventions / pitfalls:** `AGENTS.md`. The "Rules at a glance" callout in §0 mirrors this file's hard rules with more detail. §11 (CSS) and §12 (Component) have the full primitive catalog.
44+
- **Design audit + visual divergence sheet:** `docs/design-audit/component-audit.md`, `docs/design-audit/visual-divergence.md`, `docs/design-audit/divergence-sheet.html` (open in a browser).
45+
- **Implementation plan / decision log:** `docs/design-consolidation/plan.md`.
46+
47+
## Workflow conventions (per the user's global rules)
48+
49+
- For non-trivial work, follow the plan → review → branch → PR flow in the user's global CLAUDE.md (`~/.claude/CLAUDE.md`).
50+
- For certified-app specifically, **substantial work commits directly to `staging`** unless explicitly told otherwise (per `feedback_certified_app_staging.md`).
51+
- Draft PR is `staging → main`. Never auto-open it.
52+
- Never merge — leave the PR Draft + CI green and ask the user.
53+
- No emojis in code, commits, or PR bodies (the `🤖 Generated with` footer is fine).

DESIGN.md

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# DESIGN.md — Certs.social
22

3+
> **AI agents: read §14 first.** It locks in the post-consolidation rules (2026-05-28) and supersedes earlier sections where they contradict. Read the "Rules at a glance" callout in `AGENTS.md` §0 in parallel — it's the executive summary of what to do and not do.
4+
35
## 1. Visual Theme & Atmosphere
46

57
Certs.social feels like a notary's ledger reimagined as a mobile app — austere, monochrome, and quietly authoritative. The near-absence of color forces attention onto the content: serif headlines anchor each card like a document title, while the surrounding chrome recedes into warm grays. It's deliberately under-decorated — no gradients, no brand accent hue, no playful illustrations. The restraint *is* the brand.
@@ -211,7 +213,14 @@ All icons from **Lucide React**. Conventions:
211213

212214
### Modals
213215

214-
**Standard modal** (sign-in, domain): Centered on desktop, full-width on mobile. `bg-elevated`, 1px `border-default`, `shadow-lg`. Entry animation: `modalFadeIn` (backdrop 200ms) + `modalSlideUp` (content 300ms, spring easing). Sign-in modal uses 20px radius — the only intentional exception to the 2px system.
216+
**Standard sign-in modal** (`.signin-modal` only): Centered on desktop, full-width on mobile. `bg-elevated`, 1px `border-default`, `shadow-lg`. Entry animation: `modalFadeIn` (backdrop 200ms) + `modalSlideUp` (content 300ms, spring easing). 20px radius + 40px hero padding. This shape is **reserved for the sign-in surface** — it's an intentional exception to the 2px system because sign-in is a once-per-session, branded surface.
217+
218+
**App modals** (every other in-app dialog — endorse-people, create-list, sync-social-graph, future): use `<dialog className="signin-modal app-modal …">`. The `.app-modal` modifier inherits the sign-in chrome (backdrop / animation / close button / focus styling) but overrides:
219+
220+
- `border-radius: var(--radius)` (2px — matches cards, dropdowns, inputs).
221+
- Padding trimmed to `16px 20px 12px` header / `0 20px 20px` body — denser than the sign-in surface, which is right for form-style and list-style modals.
222+
223+
If you're building a new modal that isn't the sign-in flow, **always** add `app-modal` alongside `signin-modal`. Forgetting it makes the dialog read as a chunky sign-in surface and breaks the 2px system everywhere else.
215224

216225
**Bottom sheet** (mobile account switcher, mobile feedback): Fixed to bottom, draggable handle, swipe-down-to-dismiss. `bg-elevated`, top border-radius. Expandable via swipe-up.
217226

@@ -531,9 +540,81 @@ Things an AI agent is likely to get wrong:
531540
1. **Start with CSS custom properties.** Never hard-code colors — use `var(--bg-canvas)`, `var(--fg-primary)`, etc. This ensures dark mode works automatically.
532541
2. **Default to `var(--radius)` (2px).** Only use 999px for pills (badges, avatars, sign-in submit) or 50% for circles.
533542
3. **Use the `<Button>` component.** Don't create new button styles in CSS — add variants to the component if needed.
534-
4. **Mobile-first CSS.** Write base styles for mobile, then use `@media (min-width: 769px)` for desktop overrides.
543+
4. **Mobile-first CSS.** Write base styles for mobile, then use `@media (min-width: 800px)` for desktop overrides. For "below desktop", use `max-width: 799px`. 768 / 760 / 640 are not canonical — see §14.1.
535544
5. **Check both themes.** Toggle `data-theme="dark"` and verify all text is readable, borders are visible, and the primary button inverts correctly.
536545
6. **Respect the type system.** Headlines → Noto Serif 700. Body → Inter 400. Labels → Inter 500–600 uppercase. Don't mix these roles.
537546
7. **No new shadows on cards.** Cards communicate elevation via background color (`--bg-elevated` vs `--bg-canvas`), not box-shadow. Shadows are reserved for floating elements (modals, tooltips, dropdowns).
538547
8. **Test skeleton states.** Every new data-fetching component needs a skeleton. Match the geometry of the loaded state — rectangles where text will be, circles where avatars will be, same spacing.
539548
9. **Icon sizing follows context.** 14px for inline actions alongside text, 20–22px for navigation chrome, 24px for bottom nav. Don't mix these.
549+
550+
---
551+
552+
## 14. Design consolidation pass (2026-05-28)
553+
554+
This section documents the changes from `feat/design-consolidation` (PR #108, merged into `feat/positioning-redesign`). It supersedes any contradictions earlier in this file.
555+
556+
The audit that drove the work: [`docs/design-audit/component-audit.md`](docs/design-audit/component-audit.md).
557+
The visual divergence sheet: [`docs/design-audit/visual-divergence.md`](docs/design-audit/visual-divergence.md).
558+
The implementation plan + decision log: [`docs/design-consolidation/plan.md`](docs/design-consolidation/plan.md).
559+
560+
### 14.0 Rules at a glance (the agent quick-reference)
561+
562+
The shortlist of rules that drift most often. Hold yourself to these on every UI change.
563+
564+
| Rule | Quick test |
565+
| --- | --- |
566+
| `border-radius` is `var(--radius)` (2 px), pills `999px`, circles `50%`. No 4/6/8/12/16/20. | `grep -rEn "border-radius:\s+(4\|6\|8\|12\|16\|20)px" src/app/styles/` returns 0 |
567+
| No raw hex / rgb outside `tokens.css` (+ `landing.css` for the brand palette). | Search for `#[0-9a-fA-F]{3,8}` in your diff |
568+
| Canonical breakpoints are 800 / 1100 / 1300; "below desktop" is `max-width: 799px`. | `grep -E "@media.*?\((max\|min)-width:\s*(76[08]\|64[80])px\)"` returns 0 |
569+
| Shadows are `var(--shadow-sm\|md\|lg)`. No ad-hoc `box-shadow: 0 X Y rgba(...)`. | Search `box-shadow:` for non-token values |
570+
| Z-index is a `--z-*` token. | Search `z-index:\s+[0-9]+` for literals |
571+
| Headings use `text-h1`..`text-h4` + `font-headline`. Not `text-xl` / `text-lg`. | Search your diff for `font-headline text-(xl\|lg\|2xl)` |
572+
| Modals use `<AppDialog>`. No hand-rolled backdrops. | Search your diff for `signin-modal__backdrop` |
573+
| Icon-only buttons are `<Button size="icon" aria-label="…">`. | TypeScript enforces the label |
574+
| Dark mode must work. | Toggle `data-theme="dark"` and verify everything reads |
575+
| Reach for a `src/components/ui/` primitive before writing a new component or CSS class. | List the directory first |
576+
577+
### 14.1 New rules
578+
579+
1. **All `border-radius` values are `var(--radius)` (2 px).** No exceptions. The previous `--radius: 2px` policy was being eroded by 116+ instances of 4 / 6 / 8 / 12 / 16 / 20 px corners; those are gone. Pills stay at `999px`, circles at `50%`. The sign-in modal is no longer a "hero exception" (was 20 px → 2 px).
580+
2. **Landing has proper dark mode.** The "landing palette kept invariant so /welcome always renders light-themed" policy is retired. The landing tokens (`--color-navy`, `--color-off-white`, `--color-light-gray`, `--color-mid-gray`, `--color-dark-gray`, `--color-surface`, `--color-surface-container-low`) flip in `[data-theme="dark"]`. `--color-primary` and `--color-white` remain invariant for systems that still depend on them (skip-nav, brand SVG).
581+
3. **Breakpoints: 800 / 1100 / 1300 only.** Previously `landing.css` used 768 (9 places) and `home/explore/workspace` used 760 (5 places). All migrated to `max-width: 799px` to match the existing "just below desktop" convention.
582+
4. **Form input padding follows the 4-px grid.** `12 × 14` and `7 × 12` arbitrary values were replaced with `12 × 16` / `8 × 12`.
583+
5. **Cert-detail / project-detail "wide" pages share the 1280 px fullbleed width** with profile / settings / workspace.
584+
585+
### 14.2 New UI components
586+
587+
| Component | File | Purpose |
588+
| --- | --- | --- |
589+
| `<Card variant="row\|elevated\|inset">` | `src/components/ui/card.tsx` | Canonical card with three shapes. Migrate `.feed-card`, `.dash-card`, `.explore-*-card`, `.app-card`, `.endorsements-v2__card` to it. |
590+
| `<Tabs>`, `<TabList>`, `<Tab>`, `<TabPanel>` | `src/components/ui/tabs.tsx` | Proper ARIA tab pattern with keyboard arrow navigation. Migrate `.profile-tabs__tab`, `.feed-tabs__tab`. |
591+
| `<Skeleton variant="line\|box\|circle\|text">` | `src/components/ui/skeleton.tsx` | Single primitive for all loading states. Migrate `ActivityCardSkeleton`, `NotificationRowSkeleton`, `.feed-card__author--skeleton`, etc. |
592+
| `<Popover>`, `<PopoverTrigger>`, `<PopoverContent>`, `<PopoverItem>` | `src/components/ui/popover.tsx` | Floating menus — click-outside, Esc, ARIA wired. Migrate `.feed-filter`, account switcher menu, workspace breadcrumb menu, `.response-menu__menu`. |
593+
594+
### 14.3 Extended component APIs
595+
596+
| Component | New API | What absorbed |
597+
| --- | --- | --- |
598+
| `<Button>` | `size="icon"` (40 × 40 square, requires `aria-label`) | `.desktop-top-bar__icon-btn` and similar icon-only buttons. Variant `accent` rejected; domain modal moved to `primary`. |
599+
| `<Input>` | `size="sm\|md\|lg"` (36 / 44 / 56 px), `variant="default\|underline\|inline-edit"` | `.signin-modal__input` (size=lg), `.delete-record-dialog__input` (variant=inline-edit). |
600+
| `<Badge>` | New variants: `tag`, `role`, `count`, `high-quality`, `standard`, `draft`, `test`. `compact` prop for the tighter 11 px chip. | `.feed-card__label*` (4 quality variants), `.org-list__item-role`. `FeedLabelPill` now composes Badge. |
601+
602+
### 14.4 Modal hygiene
603+
604+
`AddOrgModal` and `MembershipSyncModal` moved from hand-rolled backdrop/Esc/focus-trap implementations to the canonical `<AppDialog>`. `CustomDomainModal` migration is deferred (multi-step indicator needs visual review). `<ResponsiveModal>` extraction from `FeedbackModal` is deferred until a second consumer exists.
605+
606+
### 14.5 Z-index tokens
607+
608+
Added `--z-feedback: 10000` and `--z-feedback-above: 10001` to the token map. Hardcoded z-index values (`49`, `999`, `10000`, `10001`) in `layout.css` / `landing.css` / `components.css` are now token references.
609+
610+
### 14.6 What didn't make it
611+
612+
- Migration of every `.profile-tabs__tab` / `.feed-tabs__tab` to `<Tabs>` — primitive shipped, call sites stay until each is touched.
613+
- Migration of every CSS-based card to `<Card>` — same.
614+
- Migration of every CSS-based popover to `<Popover>` — same.
615+
- Migration of skeleton CSS to `<Skeleton>` — same.
616+
- `CustomDomainModal``<AppDialog>`.
617+
- `<ResponsiveModal>` extraction from `FeedbackModal`.
618+
- A stylelint rule that flags `border-radius: 6px` and raw hex outside `tokens.css`.
619+
620+
These are documented as follow-on work in `docs/design-consolidation/plan.md`.

0 commit comments

Comments
 (0)