docs: import TRM design handoff + defer adoption to phase 3.8
A design handoff bundle generated by Claude Design (claude.ai/design) on 2026-05-02. Defines the Bloomberg/F1-pit-wall aesthetic for TRM: - ink-on-paper base + race-flag red accent (#E8412B) - square-edged everything, sharp printed offset shadows - mono numerics (JetBrains Mono) for any changing value - Goldplay (real licensed font, three weights in bundle fonts/) - four surfaces designed: dashboard / leaderboard / mobile / marketing (SPA scope is the first two) The bundle is committed in-tree at TRM_Design_System-handoff/ so 3.8 has the full source material when it picks the work up. Includes: - Top-level + project READMEs (the design spec) - chats/chat1.md (intent + iteration history) - colors_and_type.css (token set, drop-in for Tailwind 4 @theme) - fonts/ (Goldplay regular/semibold/bold) - ui_kits/ (HTML prototypes per surface) - preview/ (per-token visual reference cards) Updated phase-3-dogfood-readiness/README.md task 3.8 row to point at the bundle and document the recommended approach (retheme shadcn via CSS variable overrides + Tailwind 4 @theme, not replace). Why deferred: foundational tokens are non-blocking for Phase 1 (login + placeholder home) and Phase 2 (live map without chrome). Applying them now would either delay dogfood-blocking work or land partial styling that gets reworked when 3.8 lands the full pass.
This commit is contained in:
@@ -30,7 +30,7 @@ Phase 2 produces a working live map. Phase 3 makes that map _fielded_ — usable
|
||||
| 3.5 | Empty / loading state polish | "Connecting…", "Waiting for first positions…", "No devices in this event yet" |
|
||||
| 3.6 | Vitest + React Testing Library setup + targeted tests | Auth store, coalescer, position-store reducer. Not exhaustive |
|
||||
| 3.7 | Production logging discipline | Console-error captures + optional remote-shipper for stage |
|
||||
| 3.8 | Visual brand pass | Apply the actual TRM brand if one exists by then. Logo + colour palette |
|
||||
| 3.8 | Visual brand pass — adopt TRM design system | Apply the design handoff bundle. Bundle path + scope + recommended approach below. |
|
||||
|
||||
## Acceptance for the phase as a whole
|
||||
|
||||
@@ -40,3 +40,25 @@ Phase 2 produces a working live map. Phase 3 makes that map _fielded_ — usable
|
||||
- [ ] Clicking a device opens the detail panel; the panel shows the device's current vehicle/crew (joined from Directus) and recent positions (from the store).
|
||||
- [ ] Vitest runs in CI; existing tests are green; coverage is documented but not gated on a percentage threshold.
|
||||
- [ ] The dogfood-day operator sign-off: a marshal can use the SPA on race day without us standing over them. (This is the real acceptance gate; the rest is observable proxy.)
|
||||
|
||||
## Task 3.8 — TRM design system adoption (deferred from earlier)
|
||||
|
||||
A design handoff bundle was generated by Claude Design (claude.ai/design) on 2026-05-02 and lives at `TRM_Design_System-handoff/trm-design-system/` in this repo. It defines a **Bloomberg / F1-pit-wall** aesthetic — high data density, ink-on-paper base with one race-flag red accent (`#E8412B`), mono numerics for any changing value, square-edged everything, sharp printed offset shadows (no blur), Goldplay (real licensed font with three weights in the bundle's `fonts/`) + JetBrains Mono + Inter. Four surfaces designed: web dashboard, live leaderboard, mobile app, marketing site — only the first two are in this SPA's scope.
|
||||
|
||||
The bundle's READMEs (top-level + `trm-design-system/README.md` + `project/README.md`) and `chats/chat1.md` are the source of truth. `colors_and_type.css` ships the full token set as CSS custom properties.
|
||||
|
||||
**Recommended approach when Phase 3 picks this up — retheme shadcn, don't replace.** Tailwind 4's `@theme` directive maps cleanly to the design's tokens; shadcn's CSS-variable-driven theming lets us keep all the existing primitives (Button / Input / Form / Card / Alert) and only swap the cosmetics. Concrete steps:
|
||||
|
||||
1. Translate `colors_and_type.css` into a Tailwind `@theme` block — the paper / ink / night / race-accent / viz palettes all become Tailwind utilities.
|
||||
2. Override shadcn's `--background`, `--foreground`, `--primary`, `--destructive`, `--ring`, `--radius` to point at the TRM tokens.
|
||||
3. Add Goldplay as a webfont via the bundle's woff2 / ttf files (drop into `public/fonts/`); register JetBrains Mono via Google Fonts.
|
||||
4. Set the global radius to `0` (square-edged) and replace shadcn's soft shadows with the printed offset utility (`box-shadow: 2px 2px 0 0 var(--ink)`).
|
||||
5. Reskin shadcn primitives where defaults conflict — mainly: button radius, card border (full-ink 1 px not hairline), focus ring colour (race-flag red, 2 px outline + offset).
|
||||
6. Adopt night mode for the Phase 2 live-map page only (the design says night is the default for trackside / kiosk views, not a toggle); paper/ink everywhere else.
|
||||
7. Replace the home-page placeholder + login page with the design's chrome (56 px top bar, 240/64 px collapsible sidebar — the dashboard layout).
|
||||
|
||||
**Out of scope for 3.8:** mobile app and marketing site. Those are separate products in the design system; the SPA only adopts dashboard + leaderboard surfaces.
|
||||
|
||||
**Open question for when 3.8 starts:** the design notes the brand may have a custom icon set (cube / pixel-grid / map glyphs from the logo) — Lucide is the documented substitute. Stick with Lucide unless the custom set surfaces by then.
|
||||
|
||||
**Why deferred:** the design's foundational tokens are non-blocking for Phase 1 (login + placeholder home) and Phase 2 (live map without chrome). Adopting them now would either delay the dogfood-blocking work or land partial styling that gets reworked in 3.8 anyway. The bundle is committed alongside this plan so it's findable when Phase 3 runs.
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# CODING AGENTS: READ THIS FIRST
|
||||
|
||||
This is a **handoff bundle** from Claude Design (claude.ai/design).
|
||||
|
||||
A user mocked up designs in HTML/CSS/JS using an AI design tool, then exported this bundle so a coding agent can implement the designs for real.
|
||||
|
||||
## What you should do — IMPORTANT
|
||||
|
||||
**Read the chat transcripts first.** There are 1 chat transcript(s) in `trm-design-system/chats/`. The transcripts show the full back-and-forth between the user and the design assistant — they tell you **what the user actually wants** and **where they landed** after iterating. Don't skip them. The final HTML files are the output, but the chat is where the intent lives.
|
||||
|
||||
**Find the primary design file under `trm-design-system/project/` and read it top to bottom.** The chat transcripts will tell you which file the user was last iterating on. Then **follow its imports**: open every file it pulls in (shared components, CSS, scripts) so you understand how the pieces fit together before you start implementing.
|
||||
|
||||
**If anything is ambiguous, ask the user to confirm before you start implementing.** It's much cheaper to clarify scope up front than to build the wrong thing.
|
||||
|
||||
## About the design files
|
||||
|
||||
The design medium is **HTML/CSS/JS** — these are prototypes, not production code. Your job is to **recreate them pixel-perfectly** in whatever technology makes sense for the target codebase (React, Vue, native, whatever fits). Match the visual output; don't copy the prototype's internal structure unless it happens to fit.
|
||||
|
||||
**Don't render these files in a browser or take screenshots unless the user asks you to.** Everything you need — dimensions, colors, layout rules — is spelled out in the source. Read the HTML and CSS directly; a screenshot won't tell you anything they don't.
|
||||
|
||||
## Bundle contents
|
||||
|
||||
- `trm-design-system/README.md` — this file
|
||||
- `trm-design-system/chats/` — conversation transcripts (read these!)
|
||||
- `trm-design-system/project/` — the `TRM Design System` project files (HTML prototypes, assets, components)
|
||||
@@ -0,0 +1,393 @@
|
||||
# Event Timing Dashboard
|
||||
|
||||
_Started 2026-04-30 19:47 UTC_
|
||||
|
||||
---
|
||||
|
||||
## User
|
||||
|
||||
|
||||
|
||||
## Assistant
|
||||
|
||||
I'll start by exploring what's been provided for TRM - Tracking & Race Management.
|
||||
|
||||
_[tool: list_files]_
|
||||
|
||||
The project is empty — no codebase, Figma links, or design assets have been provided. Before I build a design system from scratch (which the guidance specifically warns against), I need some materials to work from.
|
||||
|
||||
Could you provide any of the following for TRM - Tracking & Race Management?
|
||||
|
||||
1. **Codebase** — via the Import menu (local repo, GitHub link, or another project)
|
||||
2. **Figma file** — a link to the design system, components, or product screens
|
||||
3. **Screenshots** — of the existing product UI, marketing site, or any branded materials
|
||||
4. **Brand assets** — logos, color palette, fonts, brand guidelines doc, or a slide template
|
||||
5. **Product context** — even a brief description of what TRM does (race timing? GPS tracking? event management? motorsport? running? sailing?), who uses it, and what surfaces exist (mobile app, web dashboard, race-day kiosk, marketing site, etc.)
|
||||
|
||||
A few quick questions to point me in the right direction even before assets arrive:
|
||||
|
||||
_[tool: questions_v2]_
|
||||
|
||||
## User
|
||||
|
||||
Questions answered:
|
||||
- domain: Multi-sport / generic event timing
|
||||
- surfaces: Web dashboard (organizers / officials), Marketing website, Live tracking / leaderboard view, Mobile app (athletes / spectators)
|
||||
- assets: Nothing — please mock from scratch
|
||||
- vibe: Technical / data-dense (think Bloomberg, F1 pit wall)
|
||||
- logo: uploads/logo-1777578841639.png
|
||||
|
||||
## Assistant
|
||||
|
||||
I'll build a TRM design system from scratch with a technical/data-dense aesthetic. Let me start by examining the logo and laying out the plan.
|
||||
|
||||
_[tool: view_image]_
|
||||
|
||||
_[tool: image_metadata]_
|
||||
|
||||
Good — bold geometric wordmark with a 3D cube (T), a pixel-grid (R), and a folded map (M) as inset glyphs. That's a great starting visual vocabulary: blocky, technical, geometric, ink-on-white. I'll build the system around it.
|
||||
|
||||
_[tool: update_todos]_
|
||||
|
||||
_[tool: set_project_title]_
|
||||
|
||||
_[tool: copy_files]_
|
||||
|
||||
Now I'll define the system. Visual direction: **Bloomberg/F1-pit-wall** technical density on a clean white-and-ink base. Mono numerics for all data, sharp small-radius corners, fine 1px borders, a single high-energy accent color (race-flag amber/red), and semantic data-viz colors that read clearly on dense leaderboards.
|
||||
|
||||
Let me set up fonts and core tokens.
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
Now the README with brand context, content fundamentals, and visual foundations.
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
Now let's build the preview cards. I'll create a shared frame helper, then crank through them in batches.
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
## Type cards
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: snip]_
|
||||
|
||||
## Color cards
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: snip]_
|
||||
|
||||
## Spacing, radii, shadows
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: snip]_
|
||||
|
||||
## Component cards
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: snip]_
|
||||
|
||||
## Brand cards
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: snip]_
|
||||
|
||||
Now register all preview cards.
|
||||
|
||||
_[tool: register_assets]_
|
||||
|
||||
_[tool: snip]_
|
||||
|
||||
## UI Kits — let me build a shared kit-frame helper, then each kit.
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: snip]_
|
||||
|
||||
## UI Kit 1: Web Dashboard
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: snip]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: show_html]_
|
||||
|
||||
_[tool: show_html]_
|
||||
|
||||
_[tool: snip]_
|
||||
|
||||
_[tool: get_webview_logs]_
|
||||
|
||||
_[tool: save_screenshot]_
|
||||
|
||||
_[tool: view_image]_
|
||||
|
||||
_[tool: snip]_
|
||||
|
||||
Dashboard looks great. Let me move to the leaderboard kit.
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: snip]_
|
||||
|
||||
_[tool: show_html]_
|
||||
|
||||
_[tool: get_webview_logs]_
|
||||
|
||||
_[tool: save_screenshot]_
|
||||
|
||||
_[tool: view_image]_
|
||||
|
||||
_[tool: snip]_
|
||||
|
||||
Now the mobile UI kit.
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: copy_starter_component]_
|
||||
|
||||
_[tool: snip]_
|
||||
|
||||
_[tool: grep]_
|
||||
|
||||
_[tool: grep]_
|
||||
|
||||
_[tool: snip]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: snip]_
|
||||
|
||||
_[tool: show_html]_
|
||||
|
||||
_[tool: snip]_
|
||||
|
||||
_[tool: get_webview_logs]_
|
||||
|
||||
_[tool: save_screenshot]_
|
||||
|
||||
_[tool: snip]_
|
||||
|
||||
_[tool: view_image]_
|
||||
|
||||
_[tool: snip]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: snip]_
|
||||
|
||||
_[tool: show_html]_
|
||||
|
||||
_[tool: snip]_
|
||||
|
||||
_[tool: get_webview_logs]_
|
||||
|
||||
_[tool: save_screenshot]_
|
||||
|
||||
_[tool: snip]_
|
||||
|
||||
_[tool: view_image]_
|
||||
|
||||
_[tool: snip]_
|
||||
|
||||
_[tool: register_assets]_
|
||||
|
||||
_[tool: write_file]_
|
||||
|
||||
_[tool: done]_
|
||||
|
||||
_[tool: snip]_
|
||||
|
||||
_[tool: fork_verifier_agent]_
|
||||
|
||||
The TRM Design System is built. Open the **Design System** tab to see all 27 cards (Type, Colors, Spacing, Components, Brand) plus the four UI kits.
|
||||
|
||||
### Caveats — please help me iterate
|
||||
|
||||
- **Source material was a logo only**, so the brand voice, content tone, sport vocabulary, vibe, and pricing copy are all *plausible-but-invented*. Treat as a starting hypothesis.
|
||||
- **Font substitutions flagged:** Space Grotesk (display/UI), JetBrains Mono (data), Inter (prose) — all best-fit Google Fonts. If TRM has licensed faces, please share `.woff2`.
|
||||
- **Iconography substituted:** Lucide via CDN. The logo's three inset glyphs (cube · pixel-grid · folded map) hint at a custom set — if it exists, swap in.
|
||||
- **No real product imagery.** I avoided fabricating photos. Hero/social slots are clean ink layouts only.
|
||||
- **Race-specific iconography** (checkered flag, timing chip, podium, sailing buoy, transition area) is currently rendered with Lucide approximations — a small bespoke SVG set would lift everything.
|
||||
- **Sport coverage is generic.** I leaned on running for examples. If TRM is heavier in cycling, sailing, or motorsport, sample data and wave terminology should shift.
|
||||
|
||||
### **What I need from you to make this perfect**
|
||||
|
||||
1. **A handful of real product screenshots** (any surface — dashboard, kiosk, mobile) so I can replace the invented layouts with the real visual vocabulary.
|
||||
2. **Confirm or correct** the brand accent (`#E8412B` race-flag red) and the square-edged, hairline-bordered, mono-numeric direction.
|
||||
3. **Sport mix** — which 3 sports does TRM most need to look great in?
|
||||
4. Any existing **Figma file, codebase, or marketing site URL** — even a pasted hero screenshot — would replace the biggest assumptions in one move.
|
||||
|
||||
Pick any one of those four and I'll iterate.
|
||||
|
||||
## User
|
||||
|
||||
|
||||
|
||||
## Assistant
|
||||
|
||||
_[tool: snip]_
|
||||
|
||||
_[tool: list_files]_
|
||||
|
||||
_[tool: read_file]_
|
||||
|
||||
Goldplay is a humanist sans — best fit as the **display/UI** family (replacing Space Grotesk). Only Semibold weight provided, so I'll register it at 600 and let other weights synthesize.
|
||||
|
||||
_[tool: str_replace_edit]_
|
||||
|
||||
_[tool: snip]_
|
||||
|
||||
_[tool: grep]_
|
||||
|
||||
_[tool: str_replace_edit]_
|
||||
|
||||
_[tool: snip]_
|
||||
|
||||
_[tool: grep]_
|
||||
|
||||
_[tool: str_replace_edit]_
|
||||
|
||||
_[tool: str_replace_edit]_
|
||||
|
||||
_[tool: snip]_
|
||||
|
||||
Now update the README font reference and re-register the typography cards so the system picks up the new versions.
|
||||
|
||||
_[tool: grep]_
|
||||
|
||||
_[tool: snip]_
|
||||
|
||||
_[tool: str_replace_edit]_
|
||||
|
||||
_[tool: str_replace_edit]_
|
||||
|
||||
_[tool: grep]_
|
||||
|
||||
_[tool: snip]_
|
||||
|
||||
_[tool: str_replace_edit]_
|
||||
|
||||
_[tool: register_assets]_
|
||||
|
||||
_[tool: snip]_
|
||||
|
||||
_[tool: done]_
|
||||
|
||||
Goldplay is wired in — installed at `fonts/goldplay-semibold.woff2`, mapped to `--font-display` / `--font-ui`, and now driving every heading, button, label, and overline across the previews and all four UI kits. README and the type preview cards are updated to reflect the new family.
|
||||
|
||||
**Caveats:**
|
||||
- Only the Semibold cut was provided. 400 and 700 weights synthesize from the same file, which is fine on screen but a real Regular and Bold cut would tighten things up — share them if you have them.
|
||||
- Mono (JetBrains Mono) and body (Inter) are still Google Font substitutes; let me know if TRM uses licensed faces there too.
|
||||
|
||||
## User
|
||||
|
||||
What about these regular and bold goldplay?
|
||||
|
||||
## Assistant
|
||||
|
||||
_[tool: snip]_
|
||||
|
||||
_[tool: copy_files]_
|
||||
|
||||
_[tool: read_file]_
|
||||
|
||||
_[tool: str_replace_edit]_
|
||||
|
||||
_[tool: snip]_
|
||||
|
||||
_[tool: str_replace_edit]_
|
||||
|
||||
_[tool: done]_
|
||||
|
||||
_[tool: snip]_
|
||||
|
||||
Goldplay Regular and Bold are now wired in as real font files — no more synthesis. All three weights (400 / 600 / 700) load from their own files, and the README's font note has been updated to reflect the full installed set.
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
# TRM — Tracking & Race Management
|
||||
|
||||
**Design System v1.0**
|
||||
|
||||
TRM (Tracking & Race Management) is a multi-sport, generic event-timing platform — race directors, timing officials, athletes, and spectators all live in one product family. Think of the way an F1 pit-wall display, a marathon results board, a triathlon transition mat, and a sailing nav screen all need to coexist visually: high-density numerical data, instant readability under any light, and absolute trust in the numbers on screen.
|
||||
|
||||
The system covers four surfaces:
|
||||
|
||||
| Surface | Audience | Density |
|
||||
|---|---|---|
|
||||
| **Web dashboard** | Race directors, timing officials, race control | Very high |
|
||||
| **Live tracking / leaderboard** | Spectators, broadcast, kiosk | Very high, glanceable |
|
||||
| **Mobile app** | Athletes, spectators following a friend | Medium |
|
||||
| **Marketing website** | Race organizers evaluating the product | Editorial |
|
||||
|
||||
---
|
||||
|
||||
## Sources & provenance
|
||||
|
||||
This system was built **from scratch** — no existing codebase, Figma file, or brand guide was provided. The only input was a **logo PNG** (`uploads/logo-1777578841639.png` → `assets/logo-trm.png`): a bold geometric wordmark with three inset glyphs — a 3D **cube** in the T, a **pixel grid** in the R, and a **folded map** in the M. That triad — *object · data · place* — is the conceptual core of the brand and runs through the rest of the system.
|
||||
|
||||
If you receive updated source material (real product screenshots, Figma, fonts, a brand book), treat this document as a starting hypothesis to be replaced — not as canon.
|
||||
|
||||
---
|
||||
|
||||
## CONTENT FUNDAMENTALS
|
||||
|
||||
TRM copy reads like a **race-control radio call**: short, factual, present tense, no hype. The product is responsible for numbers people care about (a podium finish, a qualifying time), so the voice is calm and exact. Drama lives in the data, not in the prose.
|
||||
|
||||
### Voice attributes
|
||||
|
||||
- **Precise over expressive.** "Lap 14 / 22" not "Almost there!"
|
||||
- **Active, present tense.** "Smith passes Jones" not "Smith has just managed to pass Jones."
|
||||
- **Short.** Most UI strings are ≤ 4 words. Sentences in marketing rarely exceed 16.
|
||||
- **No exclamation marks** in product UI. Marketing may use one per page, max.
|
||||
- **Second person ("you") in marketing.** First person plural ("we") only when explicitly speaking as the company. Product UI prefers no pronoun at all ("Add bib" not "Add a bib to your event").
|
||||
- **Numerals beat words.** "3 laps left," not "three laps remaining."
|
||||
- **Time is sacred.** Always render times in mono with tabular figures: `01:23:45.678`. Leading zeros on minutes/seconds. Milliseconds when relevant.
|
||||
|
||||
### Casing
|
||||
|
||||
| Context | Style |
|
||||
|---|---|
|
||||
| Buttons, links, menu items | **Sentence case** ("Start race", "Export results") |
|
||||
| Labels above fields | **UPPERCASE** with letter-spacing (overline class) |
|
||||
| Section headings | **Sentence case** |
|
||||
| Table column headers | **UPPERCASE** mono, e.g. `POS BIB NAME GAP` |
|
||||
| Status flags | **UPPERCASE** ("LIVE", "DNF", "DSQ", "DNS") |
|
||||
| Brand wordmark | **TRM** — always uppercase, never "Trm" |
|
||||
|
||||
### Vocabulary — preferred terms
|
||||
|
||||
| Use | Not |
|
||||
|---|---|
|
||||
| Event | Race (use "race" only for a single heat within an event) |
|
||||
| Participant | User, racer, runner (sport-specific only when the event is single-sport) |
|
||||
| Bib | Number, ID |
|
||||
| Wave | Group, batch |
|
||||
| Split | Checkpoint *(use "checkpoint" only when split is geographic)* |
|
||||
| Leaderboard | Ranking, scoreboard |
|
||||
| DNF / DSQ / DNS | Did not finish / Disqualified / Did not start *(spell out only on first use)* |
|
||||
| Live | Real-time *(reserve "real-time" for technical copy)* |
|
||||
|
||||
### Emoji & symbols
|
||||
|
||||
- **No decorative emoji** anywhere in product UI. None.
|
||||
- Marketing may use **🏁** or **▲ ▼** as standalone graphic elements but never inline in sentences.
|
||||
- Race-flag glyphs are part of the design system as **CSS-drawn flags or SVG**, not emoji.
|
||||
- Mathematical / typographic symbols *are* used freely: `+`, `−`, `±`, `→`, `·`, `°`, `Δ`.
|
||||
|
||||
### Sample copy
|
||||
|
||||
> **Marketing hero:**
|
||||
> *Run any event. Time any sport. TRM is the race-day operating system used by 4,000+ event organizers — from 5K fun runs to 24-hour endurance races.*
|
||||
|
||||
> **Empty state in dashboard:**
|
||||
> *No events yet. Create your first event to start timing.*
|
||||
|
||||
> **Toast on save:**
|
||||
> *Saved · 14:02:11* *(timestamped, mono)*
|
||||
|
||||
> **Race-control alert:**
|
||||
> *Bib 247 — missed split 3 — flagging for review*
|
||||
|
||||
> **Push notification, athlete app:**
|
||||
> *Maya passed split 2 — 00:42:18 — pace 4:13/km*
|
||||
|
||||
---
|
||||
|
||||
## VISUAL FOUNDATIONS
|
||||
|
||||
The system is **ink on paper, with one race-flag accent**. Square-edged. Hairline-bordered. Mono-numeric. The visual mood sits between a Bloomberg terminal and a printed race program.
|
||||
|
||||
### Colors
|
||||
|
||||
- **Paper / Ink palette** drives 90% of screens. Off-white `#FAFAF7` ground, deep ink `#0E0E0C` strokes and type. No true black, no true white — both are exhausting on data screens.
|
||||
- **Night mode** (`#0B0D10`) is the default for live tracking, kiosk, and trackside displays. It is not a "dark theme toggle" — it's a separate context.
|
||||
- **Race-flag red `#E8412B`** is the only brand accent. Reserved for: primary CTAs, "live" pulse, danger states, the leading position in a leaderboard. Used sparingly. If everything is red, nothing is.
|
||||
- **Amber `#F2A516`** = caution / yellow flag / warnings.
|
||||
- **Green `#2E8C4A`** = go / improvement / personal best / passed checkpoint.
|
||||
- **Blue `#2563C8`** = informational, link, "tracking" pulse.
|
||||
- **Categorical viz palette** is 8 hues, ordered by frequency of use; never assign meaning to a viz hue beyond chart legend.
|
||||
|
||||
### Typography
|
||||
|
||||
- **Display & UI:** *Goldplay* (Semibold supplied; 400 / 700 synthesized). The brand face — humanist sans with confident curves and a slightly editorial register. Used for headings, buttons, navigation, labels.
|
||||
- **Mono / data:** *JetBrains Mono* (400 / 500 / 600). Tabular figures are non-negotiable for any time, gap, distance, or ranking. Every number that could change is mono.
|
||||
- **Body / long-form prose:** *Inter* (400 / 600). Used only in marketing long-form and documentation. The dashboard does not have body prose.
|
||||
- **Letter-spacing:** display is tight (`-0.02em`). Overlines / table headers are open (`+0.10em`) and uppercase.
|
||||
|
||||
> ✅ **Goldplay (display/UI)** — brand face installed with three weights: Regular `400` (`fonts/goldplay-regular.ttf`), Semibold `600` (`fonts/goldplay-semibold.woff2`), Bold `700` (`fonts/goldplay-bold.ttf`). No synthesis. **Mono / body** are still Google Font substitutes — share licensed faces if available.e *Berkeley Mono* or *GT America Mono*), please provide the `.woff2` / `.ttf` files and we'll swap them in.
|
||||
|
||||
### Spacing & grid
|
||||
|
||||
- **4-px base** unit. Standard scale: 4 · 8 · 12 · 16 · 20 · 24 · 32 · 40 · 48 · 64 · 80 · 96.
|
||||
- **12-column grid** at 1280 px container, 24 px gutters, 16 px column gap.
|
||||
- **Table density** is the system's defining choice. Default row height is **36 px**, "dense" is **28 px**, and dense is the dashboard default — race directors look at hundreds of rows.
|
||||
|
||||
### Corners & borders
|
||||
|
||||
- **Square by default.** `--r-0: 0`. Cards, inputs, buttons, modals: all square.
|
||||
- **2 px radius** for chips and small inputs only.
|
||||
- **4–10 px radius** reserved for marketing cards and the mobile app surface.
|
||||
- **Hairline 1 px borders** — `var(--hairline)` for division within a card; full ink `1px solid var(--ink)` for the outer edge of any "first-class" container (modal, primary card, button).
|
||||
- **No rounded pills**, except for status badges where pill is functionally meaningful (`LIVE`, `DNF`).
|
||||
|
||||
### Shadows & elevation
|
||||
|
||||
TRM uses a **printed offset shadow** as its primary elevation model — a sharp 2 px or 4 px ink-colored offset, no blur. It says "the chip is sitting on the page," not "the chip is glowing." Soft blurry shadows are allowed only on mobile sheets and marketing site.
|
||||
|
||||
| Token | Use |
|
||||
|---|---|
|
||||
| `--shadow-2` (2 px ink offset) | Buttons at rest, primary cards |
|
||||
| `--shadow-3` (4 px ink offset) | Buttons / cards on hover |
|
||||
| `--shadow-soft` | Mobile bottom sheets |
|
||||
| `--shadow-pop` | Marketing testimonial / floating dropdowns |
|
||||
|
||||
### Backgrounds & imagery
|
||||
|
||||
- **No gradients** in product UI. Marketing may use a **flat ink-to-night vertical gradient** behind a hero — never multi-color.
|
||||
- **Imagery vibe:** high-contrast B&W or duotone (ink + flag) photography of race-day moments — start lines, timing mats, marshals, finish tape. Crops are **tight and asymmetric**. Color photos are allowed in marketing but should be **slightly desaturated and warm-graded** so they coexist with the ink palette.
|
||||
- **Patterns:** a subtle **dot-grid** (`2 px dot, 24 px spacing`, ink at 8% opacity) and a **diagonal hatch** (used sparingly to mark "in progress" or "estimated" data). These are CSS-drawn, never image files.
|
||||
- **No hand-drawn illustrations.** No abstract blobs. No gradients-as-backgrounds. The brand is photographic + typographic.
|
||||
|
||||
### Animation
|
||||
|
||||
- **Functional, never decorative.** Transitions are 120–320 ms, almost always `--ease-snap`.
|
||||
- Hover changes use `120 ms`. Modal / sheet entrances use `180 ms`. Chart redraws use `320 ms`.
|
||||
- **The "live" pulse** is the one signature motion: a 2 s loop where a red dot scales from 1.0 → 1.15 and a fading ring expands from 0 → 16 px. Used at most once per screen, next to the word LIVE.
|
||||
- **No bouncy / spring easing.** No fly-ins from off-screen. No micro-interactions on every button.
|
||||
|
||||
### States — hover, press, focus, disabled
|
||||
|
||||
| State | Treatment |
|
||||
|---|---|
|
||||
| **Hover** (button) | Shadow grows from `--shadow-2` → `--shadow-3`, button translates `1px ↑ 1px ←` |
|
||||
| **Hover** (link) | Color shifts `--ink` → `--flag` |
|
||||
| **Hover** (table row) | Background shifts to `--paper-2`, no elevation change |
|
||||
| **Press** | Shadow collapses to `0 0 0 0`, button translates back to `0,0` |
|
||||
| **Focus** | 2 px outline in `--flag`, 2 px offset, *never* removed. Keyboard users matter on race day. |
|
||||
| **Disabled** | Opacity 40%, cursor `not-allowed`, no shadow |
|
||||
|
||||
### Transparency & blur
|
||||
|
||||
- **Backdrop blur** used only on mobile bottom sheets and the kiosk overlay (`backdrop-filter: blur(12px)` over a `rgba(11,13,16,0.7)` scrim).
|
||||
- Tints (`--flag-tint`, `--amber-tint`, `--green-tint`, `--blue-tint`) are flat 10–15% mixes, never alpha layers, so they composite predictably over `--paper-2`.
|
||||
|
||||
### Layout rules — fixed elements
|
||||
|
||||
- **Top bar** is `56 px`, full-width, ink border on the bottom, sticky.
|
||||
- **Side nav** is `240 px` collapsed to `64 px`, ink border on the right, sticky.
|
||||
- **Live tracking pages** use a **3-column** layout: leaderboard (40%) · map (40%) · selected-athlete detail (20%).
|
||||
- **Tables fill remaining space**, scroll independently of the page.
|
||||
- **Time/clock** is always pinned top-right of any "race in progress" view, mono, jumbo size.
|
||||
|
||||
### Iconography
|
||||
|
||||
See the **ICONOGRAPHY** section below.
|
||||
|
||||
---
|
||||
|
||||
## ICONOGRAPHY
|
||||
|
||||
TRM uses **Lucide** as its icon system — a clean, square-cornered, 2 px stroke library that pairs with Goldplay's humanist sans and the square-edged component language. Lucide is loaded via CDN (`unpkg.com/lucide@latest`) in HTML examples and via the `lucide-react` package in production code.
|
||||
|
||||
> ⚠ **Substitution flag:** Lucide is a substitute. The TRM logo's three inset glyphs (cube, pixel-grid, map) suggest the brand may have a custom-drawn icon set in the same blocky style. If that set exists, swap it in — Lucide is the closest CDN-available match (clean strokes, square caps, geometric).
|
||||
|
||||
### Icon rules
|
||||
|
||||
- **Stroke weight:** 2 px (Lucide default). Never 1.5 / 1 — strokes get lost on dark mode.
|
||||
- **Sizes:** 14 / 16 / 20 / 24 / 32 px. Never in between.
|
||||
- **Color:** inherits `currentColor`. Default `--ink-3`; hover state inherits whatever the parent's hover color is.
|
||||
- **Alignment:** icons are **box-aligned**, not optically aligned. The tabular feel beats the typographic feel here.
|
||||
- **Race-specific iconography** that Lucide doesn't cover (checkered flag, timing chip, transition area, podium, finish-line tape, sailing buoy, lap split) is rendered as **inline SVG** in the same 2 px stroke style. A small set lives in `assets/icons/` as we encounter the need.
|
||||
|
||||
### Emoji
|
||||
|
||||
**Not used** in product UI. Only race-flag glyphs (🏁) are tolerated in marketing, and even then only as standalone graphic elements at large size — never inline in sentences.
|
||||
|
||||
### Unicode glyphs
|
||||
|
||||
Used freely for compact mathematical / directional indicators:
|
||||
|
||||
- `↑ ↓ → ←` for trend arrows in leaderboards
|
||||
- `▲ ▼` for sort direction
|
||||
- `±` for tolerance ("split window ±5s")
|
||||
- `·` as a compact bullet between meta items ("Wave 3 · 4.2 km · 47 starters")
|
||||
- `°` for temperature ("18°C, dry")
|
||||
- `Δ` for delta ("Δ +1.4s")
|
||||
|
||||
### Logo
|
||||
|
||||
- `assets/logo-trm.png` — primary wordmark, ink on transparent, 286×158.
|
||||
- Minimum width: 80 px.
|
||||
- Clear space: half the cap-height of the wordmark, all sides.
|
||||
- **Never** stretch, recolor (except inversion to paper-on-ink), or place on a busy photograph without a 1 px ink-bordered paper plate behind it.
|
||||
|
||||
---
|
||||
|
||||
## INDEX — what's in this folder
|
||||
|
||||
| Path | Purpose |
|
||||
|---|---|
|
||||
| `README.md` | This file. Read first. |
|
||||
| `SKILL.md` | Agent-Skill manifest for Claude Code / portable use. |
|
||||
| `colors_and_type.css` | All design tokens as CSS custom properties + base type styles. **Import this first** in any HTML mock. |
|
||||
| `assets/` | Logos, icons, brand imagery. |
|
||||
| `assets/logo-trm.png` | Primary wordmark. |
|
||||
| `preview/` | Per-token / per-component preview cards rendered into the Design System tab. Useful as visual reference. |
|
||||
| `ui_kits/dashboard/` | Web dashboard for organizers — sidebar, tables, race control. |
|
||||
| `ui_kits/leaderboard/` | Live tracking / spectator leaderboard view. |
|
||||
| `ui_kits/mobile/` | Athlete & spectator mobile app inside an iOS frame. |
|
||||
| `ui_kits/marketing/` | Marketing site — homepage hero, features, pricing strip. |
|
||||
|
||||
Each `ui_kits/<name>/` folder contains its own `README.md` describing the screens, an `index.html` you can open directly, and JSX components for the major UI pieces.
|
||||
|
||||
---
|
||||
|
||||
## Quick start for designers
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="colors_and_type.css">
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
```
|
||||
|
||||
Then build with the tokens. The preview cards in `preview/` are good copy-paste starting points for any new screen.
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: trm-design
|
||||
description: Use this skill to generate well-branded interfaces and assets for TRM (Tracking & Race Management), either for production or throwaway prototypes/mocks/etc. Contains essential design guidelines, colors, type, fonts, assets, and UI kit components for prototyping.
|
||||
user-invocable: true
|
||||
---
|
||||
|
||||
Read the README.md file within this skill, and explore the other available files.
|
||||
|
||||
If creating visual artifacts (slides, mocks, throwaway prototypes, etc), copy assets out and create static HTML files for the user to view. If working on production code, you can copy assets and read the rules here to become an expert in designing with this brand.
|
||||
|
||||
If the user invokes this skill without any other guidance, ask them what they want to build or design, ask some questions, and act as an expert designer who outputs HTML artifacts _or_ production code, depending on the need.
|
||||
|
||||
## Quick orientation
|
||||
|
||||
- `README.md` — brand context, content tone, visual foundations, iconography
|
||||
- `colors_and_type.css` — all design tokens; **import first** in any HTML
|
||||
- `assets/logo-trm.png` — primary wordmark
|
||||
- `preview/*.html` — token & component reference cards
|
||||
- `ui_kits/dashboard/` — organizer web dashboard
|
||||
- `ui_kits/leaderboard/` — spectator/kiosk live tracking (night mode)
|
||||
- `ui_kits/mobile/` — athlete & spectator iOS app
|
||||
- `ui_kits/marketing/` — marketing homepage
|
||||
|
||||
## Defining traits
|
||||
|
||||
- **Ink on paper, one race-flag accent** (`#E8412B`)
|
||||
- **Square corners**, hairline borders, printed-offset shadows (`2px 2px 0 0 ink`)
|
||||
- **Mono numerics** (JetBrains Mono, tabular figures) for every time/gap/distance
|
||||
- **Display:** Space Grotesk, tight tracking, sentence case
|
||||
- **Tables are dense** — 28px rows in dashboard
|
||||
- **Night mode** for trackside/kiosk surfaces
|
||||
- **No gradients, no decorative emoji, no rounded pills** (except status badges)
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,257 @@
|
||||
/* ============================================================
|
||||
TRM — Tracking & Race Management
|
||||
Foundational tokens: colors, type, spacing, radii, shadows
|
||||
============================================================ */
|
||||
|
||||
/* ---- Webfonts ----
|
||||
Display + UI: "Goldplay" (brand face — Regular / Semibold / Bold)
|
||||
Mono / data: "JetBrains Mono"
|
||||
Body: "Inter"
|
||||
*/
|
||||
@font-face {
|
||||
font-family: "Goldplay";
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
src: url("fonts/goldplay-regular.ttf") format("truetype");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Goldplay";
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
src: url("fonts/goldplay-semibold.woff2") format("woff2"),
|
||||
url("fonts/goldplay-semibold.woff") format("woff");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Goldplay";
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
src: url("fonts/goldplay-bold.ttf") format("truetype");
|
||||
}
|
||||
@import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Inter:wght@400;500;600;700&display=swap");
|
||||
|
||||
:root {
|
||||
/* ---------- COLOR — INK & PAPER ---------- */
|
||||
--paper: #FAFAF7; /* off-white page */
|
||||
--paper-2: #F2F2EE; /* card sunk a level */
|
||||
--paper-3: #E8E8E2; /* deeper sunk / table stripe */
|
||||
--ink: #0E0E0C; /* primary text & strokes */
|
||||
--ink-2: #2A2A26; /* strong secondary */
|
||||
--ink-3: #5A5A53; /* body secondary */
|
||||
--ink-4: #8A8A82; /* meta / placeholder */
|
||||
--ink-5: #B8B8B0; /* hairlines on light */
|
||||
--ink-6: #DCDCD4; /* dividers */
|
||||
--line: #0E0E0C; /* default border = ink */
|
||||
--hairline: rgba(14,14,12,0.12);
|
||||
|
||||
/* ---------- COLOR — DARK MODE / TIMING SCREEN ---------- */
|
||||
--night: #0B0D10; /* trackside dark bg */
|
||||
--night-2: #14171C;
|
||||
--night-3: #1E222A;
|
||||
--night-line: #2A2F38;
|
||||
--night-fg: #F2F2EE;
|
||||
--night-fg-2: #B8B8B0;
|
||||
--night-fg-3: #8A8A82;
|
||||
|
||||
/* ---------- COLOR — RACE ACCENTS ---------- */
|
||||
--flag: #E8412B; /* primary accent — race-flag red */
|
||||
--flag-2: #C8331F;
|
||||
--flag-tint: #FBE3DE;
|
||||
--amber: #F2A516; /* yellow-flag / caution */
|
||||
--amber-2: #C88500;
|
||||
--amber-tint: #FBEFD0;
|
||||
--green: #2E8C4A; /* go / personal best / pass */
|
||||
--green-2: #1F6A36;
|
||||
--green-tint: #DDEFE2;
|
||||
--blue: #2563C8; /* link / info / "live" pulse */
|
||||
--blue-2: #1A4FA8;
|
||||
--blue-tint: #DCE6F8;
|
||||
--violet: #6B46C1; /* secondary categorical */
|
||||
--magenta: #C9296F;
|
||||
--teal: #188C8A;
|
||||
|
||||
/* Semantic shortcuts (status, alerts) */
|
||||
--ok: var(--green);
|
||||
--warn: var(--amber);
|
||||
--danger: var(--flag);
|
||||
--info: var(--blue);
|
||||
|
||||
/* ---------- COLOR — DATA VIZ (categorical, 8) ---------- */
|
||||
--viz-1: #E8412B; /* flag red */
|
||||
--viz-2: #2563C8; /* blue */
|
||||
--viz-3: #2E8C4A; /* green */
|
||||
--viz-4: #F2A516; /* amber */
|
||||
--viz-5: #6B46C1; /* violet */
|
||||
--viz-6: #188C8A; /* teal */
|
||||
--viz-7: #C9296F; /* magenta */
|
||||
--viz-8: #5A5A53; /* graphite */
|
||||
|
||||
/* ---------- TYPE — FAMILIES ---------- */
|
||||
--font-display: "Goldplay", "Helvetica Neue", Arial, sans-serif;
|
||||
--font-ui: "Goldplay", "Helvetica Neue", Arial, sans-serif;
|
||||
--font-body: "Inter", "Helvetica Neue", Arial, sans-serif;
|
||||
--font-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
||||
|
||||
/* ---------- TYPE — SCALE ----------
|
||||
Modular but not strict. Optimized for data density.
|
||||
*/
|
||||
--fs-overline: 11px; /* uppercase labels */
|
||||
--fs-micro: 12px; /* table meta */
|
||||
--fs-small: 13px; /* body small */
|
||||
--fs-body: 15px; /* body */
|
||||
--fs-lead: 17px; /* lead paragraph */
|
||||
--fs-h6: 14px;
|
||||
--fs-h5: 16px;
|
||||
--fs-h4: 20px;
|
||||
--fs-h3: 26px;
|
||||
--fs-h2: 34px;
|
||||
--fs-h1: 48px;
|
||||
--fs-display: 72px; /* hero / kiosk timing */
|
||||
--fs-jumbo: 120px; /* leaderboard primary clock */
|
||||
|
||||
--lh-tight: 1.05;
|
||||
--lh-snug: 1.2;
|
||||
--lh-normal: 1.35;
|
||||
--lh-loose: 1.55;
|
||||
|
||||
--ls-tight: -0.02em;
|
||||
--ls-normal: 0;
|
||||
--ls-wide: 0.04em;
|
||||
--ls-caps: 0.10em; /* overline / small caps */
|
||||
|
||||
/* ---------- SPACING (4px base) ---------- */
|
||||
--s-0: 0;
|
||||
--s-1: 2px;
|
||||
--s-2: 4px;
|
||||
--s-3: 8px;
|
||||
--s-4: 12px;
|
||||
--s-5: 16px;
|
||||
--s-6: 20px;
|
||||
--s-7: 24px;
|
||||
--s-8: 32px;
|
||||
--s-9: 40px;
|
||||
--s-10: 48px;
|
||||
--s-11: 64px;
|
||||
--s-12: 80px;
|
||||
--s-13: 96px;
|
||||
|
||||
/* ---------- RADIUS ---------- */
|
||||
--r-0: 0; /* default — TRM is square-edged */
|
||||
--r-1: 2px;
|
||||
--r-2: 4px; /* chips / inputs */
|
||||
--r-3: 6px; /* small cards */
|
||||
--r-4: 10px; /* large cards */
|
||||
--r-pill: 999px; /* pills only when truly needed */
|
||||
|
||||
/* ---------- BORDERS ---------- */
|
||||
--bw-1: 1px;
|
||||
--bw-2: 2px;
|
||||
--bw-3: 3px;
|
||||
--b-default: 1px solid var(--ink);
|
||||
--b-hair: 1px solid var(--hairline);
|
||||
--b-night: 1px solid var(--night-line);
|
||||
|
||||
/* ---------- ELEVATION ---------- */
|
||||
/* TRM uses sharp 1-2px offset shadows ("printed" feel), not soft blooms. */
|
||||
--shadow-0: none;
|
||||
--shadow-1: 0 1px 0 0 var(--ink); /* underline */
|
||||
--shadow-2: 2px 2px 0 0 var(--ink); /* button rest */
|
||||
--shadow-3: 4px 4px 0 0 var(--ink); /* card hover */
|
||||
--shadow-soft: 0 1px 2px rgba(14,14,12,0.06), 0 4px 12px rgba(14,14,12,0.04);
|
||||
--shadow-pop: 0 8px 24px rgba(14,14,12,0.12);
|
||||
|
||||
/* ---------- MOTION ---------- */
|
||||
--ease-snap: cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
--ease-linear: linear;
|
||||
--t-quick: 120ms;
|
||||
--t-base: 180ms;
|
||||
--t-slow: 320ms;
|
||||
|
||||
/* ---------- LAYOUT ---------- */
|
||||
--container: 1280px;
|
||||
--gutter: 24px;
|
||||
--col-gap: 16px;
|
||||
--grid-cols: 12;
|
||||
|
||||
/* ---------- COMPONENT TOKENS ---------- */
|
||||
--control-h-sm: 28px;
|
||||
--control-h-md: 36px;
|
||||
--control-h-lg: 44px;
|
||||
--table-row-h: 36px;
|
||||
--table-row-h-dense: 28px;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
SEMANTIC TYPOGRAPHY — apply directly or via classes
|
||||
============================================================ */
|
||||
html, body {
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
font-family: var(--font-body);
|
||||
font-size: var(--fs-body);
|
||||
line-height: var(--lh-normal);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
h1, .h1, h2, .h2, h3, .h3, h4, .h4, h5, .h5, h6, .h6 {
|
||||
font-family: var(--font-display);
|
||||
color: var(--ink);
|
||||
letter-spacing: var(--ls-tight);
|
||||
line-height: var(--lh-tight);
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
h1, .h1 { font-size: var(--fs-h1); font-weight: 700; }
|
||||
h2, .h2 { font-size: var(--fs-h2); font-weight: 700; }
|
||||
h3, .h3 { font-size: var(--fs-h3); }
|
||||
h4, .h4 { font-size: var(--fs-h4); }
|
||||
h5, .h5 { font-size: var(--fs-h5); line-height: var(--lh-snug); }
|
||||
h6, .h6 { font-size: var(--fs-h6); line-height: var(--lh-snug); }
|
||||
|
||||
.display { font-family: var(--font-display); font-size: var(--fs-display); font-weight: 700; line-height: var(--lh-tight); letter-spacing: var(--ls-tight); }
|
||||
.jumbo { font-family: var(--font-mono); font-size: var(--fs-jumbo); font-weight: 600; line-height: 1; letter-spacing: -0.04em; font-variant-numeric: tabular-nums; }
|
||||
|
||||
p, .body { font-size: var(--fs-body); line-height: var(--lh-loose); color: var(--ink-2); }
|
||||
.lead { font-size: var(--fs-lead); line-height: var(--lh-loose); color: var(--ink-2); }
|
||||
.small { font-size: var(--fs-small); }
|
||||
.micro { font-size: var(--fs-micro); color: var(--ink-3); }
|
||||
|
||||
.overline {
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--fs-overline);
|
||||
font-weight: 600;
|
||||
letter-spacing: var(--ls-caps);
|
||||
text-transform: uppercase;
|
||||
color: var(--ink-3);
|
||||
}
|
||||
|
||||
.mono, code, kbd, samp, pre {
|
||||
font-family: var(--font-mono);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.num { font-family: var(--font-mono); font-variant-numeric: tabular-nums; letter-spacing: -0.01em; }
|
||||
|
||||
a {
|
||||
color: var(--ink);
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 1px;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
a:hover { color: var(--flag); }
|
||||
|
||||
hr { border: 0; border-top: 1px solid var(--ink); margin: var(--s-7) 0; }
|
||||
|
||||
/* Foreground/background tokens (semantic aliases) */
|
||||
:root {
|
||||
--fg-1: var(--ink);
|
||||
--fg-2: var(--ink-2);
|
||||
--fg-3: var(--ink-3);
|
||||
--fg-4: var(--ink-4);
|
||||
--bg-1: var(--paper);
|
||||
--bg-2: var(--paper-2);
|
||||
--bg-3: var(--paper-3);
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,17 @@
|
||||
/* Shared base for preview cards. Each card = 700x<H>, no titles
|
||||
(the asset name renders outside). Just show the thing. */
|
||||
*,*::before,*::after { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; }
|
||||
body {
|
||||
width: 700px;
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
font-family: var(--font-body);
|
||||
padding: 20px;
|
||||
}
|
||||
.card-pad { padding: 20px; }
|
||||
.row { display: flex; gap: 12px; align-items: center; }
|
||||
.col { display: flex; flex-direction: column; gap: 8px; }
|
||||
.fill { flex: 1; }
|
||||
.dot { width: 8px; height: 8px; border-radius: 999px; display: inline-block; }
|
||||
.hairline { border-top: 1px solid var(--hairline); }
|
||||
@@ -0,0 +1,35 @@
|
||||
<!doctype html><html><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../colors_and_type.css">
|
||||
<link rel="stylesheet" href="_card.css">
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<style>
|
||||
body { padding: 24px; }
|
||||
.grid { display: grid; grid-template-columns: repeat(8, 1fr); gap: 14px; }
|
||||
.ic { display:flex; flex-direction:column; align-items:center; gap:6px; padding: 10px; border: 1px solid var(--hairline); }
|
||||
.ic [data-lucide] { width: 22px; height: 22px; stroke-width: 2; color: var(--ink); }
|
||||
.ic .nm { font-family: var(--font-mono); font-size: 9px; color: var(--ink-3); letter-spacing: 0.04em; text-transform: uppercase;}
|
||||
.lbl { font-family: var(--font-mono); font-size: 10px; color: var(--ink-3); margin-top: 14px; }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="overline" style="margin-bottom:14px;">Iconography · Lucide 2px stroke</div>
|
||||
<div class="grid">
|
||||
<div class="ic"><i data-lucide="flag"></i><div class="nm">flag</div></div>
|
||||
<div class="ic"><i data-lucide="timer"></i><div class="nm">timer</div></div>
|
||||
<div class="ic"><i data-lucide="map-pin"></i><div class="nm">pin</div></div>
|
||||
<div class="ic"><i data-lucide="trophy"></i><div class="nm">trophy</div></div>
|
||||
<div class="ic"><i data-lucide="users"></i><div class="nm">users</div></div>
|
||||
<div class="ic"><i data-lucide="bar-chart-3"></i><div class="nm">chart</div></div>
|
||||
<div class="ic"><i data-lucide="settings"></i><div class="nm">settings</div></div>
|
||||
<div class="ic"><i data-lucide="bell"></i><div class="nm">bell</div></div>
|
||||
<div class="ic"><i data-lucide="play"></i><div class="nm">play</div></div>
|
||||
<div class="ic"><i data-lucide="pause"></i><div class="nm">pause</div></div>
|
||||
<div class="ic"><i data-lucide="circle-stop"></i><div class="nm">stop</div></div>
|
||||
<div class="ic"><i data-lucide="radio"></i><div class="nm">radio</div></div>
|
||||
<div class="ic"><i data-lucide="route"></i><div class="nm">route</div></div>
|
||||
<div class="ic"><i data-lucide="watch"></i><div class="nm">watch</div></div>
|
||||
<div class="ic"><i data-lucide="wifi"></i><div class="nm">wifi</div></div>
|
||||
<div class="ic"><i data-lucide="download"></i><div class="nm">export</div></div>
|
||||
</div>
|
||||
<div class="lbl">Sizes 14 / 16 / 20 / 24 / 32 · color inherits currentColor</div>
|
||||
<script>lucide.createIcons();</script>
|
||||
</body></html>
|
||||
@@ -0,0 +1,25 @@
|
||||
<!doctype html><html><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../colors_and_type.css">
|
||||
<link rel="stylesheet" href="_card.css">
|
||||
<style>
|
||||
body { padding: 24px; }
|
||||
.grid { display: grid; grid-template-columns: 1.2fr 1fr; gap: 18px; align-items: stretch; }
|
||||
.panel { border: 1px solid var(--ink); padding: 22px; display:flex; align-items:center; justify-content:center; }
|
||||
.panel.dark { background: var(--ink); }
|
||||
.panel img { max-width: 100%; height: auto; }
|
||||
.panel.dark img { filter: invert(1) brightness(1.1); }
|
||||
.meta { font-family: var(--font-mono); font-size: 10px; color: var(--ink-3); margin-top: 8px;}
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="overline" style="margin-bottom:14px;">Logo · primary wordmark</div>
|
||||
<div class="grid">
|
||||
<div>
|
||||
<div class="panel"><img src="../assets/logo-trm.png" alt="TRM" style="width:260px;" /></div>
|
||||
<div class="meta">Ink on paper · default</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="panel dark"><img src="../assets/logo-trm.png" alt="TRM" style="width:200px;" /></div>
|
||||
<div class="meta">Paper on ink · night surfaces</div>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
@@ -0,0 +1,41 @@
|
||||
<!doctype html><html><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../colors_and_type.css">
|
||||
<link rel="stylesheet" href="_card.css">
|
||||
<style>
|
||||
body { padding: 0; }
|
||||
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0; height: 240px; }
|
||||
.panel { padding: 22px; position: relative; overflow: hidden; }
|
||||
.a {
|
||||
background-color: var(--paper);
|
||||
background-image: radial-gradient(var(--ink) 1px, transparent 1px);
|
||||
background-size: 16px 16px;
|
||||
background-position: 0 0;
|
||||
border: 1px solid var(--ink);
|
||||
}
|
||||
.b {
|
||||
border: 1px solid var(--ink);
|
||||
border-left: 0;
|
||||
background:
|
||||
repeating-linear-gradient(
|
||||
45deg,
|
||||
var(--paper-2),
|
||||
var(--paper-2) 6px,
|
||||
var(--paper) 6px,
|
||||
var(--paper) 12px
|
||||
);
|
||||
}
|
||||
.lbl { font-family: var(--font-mono); font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink); position: absolute; bottom: 12px; left: 14px; background: var(--paper); border: 1px solid var(--ink); padding: 3px 8px; }
|
||||
.tag { position: absolute; top: 14px; right: 14px; font-family: var(--font-mono); font-size: 11px; color: var(--ink-3); }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="grid">
|
||||
<div class="panel a">
|
||||
<div class="tag">8% ink · 16px</div>
|
||||
<div class="lbl">Dot grid</div>
|
||||
</div>
|
||||
<div class="panel b">
|
||||
<div class="tag">45° · 12px</div>
|
||||
<div class="lbl">Diagonal hatch · "estimated"</div>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
@@ -0,0 +1,23 @@
|
||||
<!doctype html><html><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../colors_and_type.css">
|
||||
<link rel="stylesheet" href="_card.css">
|
||||
<style>
|
||||
body { padding: 24px; background: var(--night); color: var(--night-fg); }
|
||||
.row { display: flex; align-items: center; gap: 18px; }
|
||||
.live { display: inline-flex; align-items: center; gap: 10px; font-family: var(--font-display); font-size: 14px; font-weight: 700; letter-spacing: 0.10em; color: var(--paper); }
|
||||
.dot-wrap { position: relative; width: 14px; height: 14px; }
|
||||
.dot { position: absolute; inset: 3px; background: var(--flag); border-radius: 999px; animation: scale 2s infinite ease-in-out; }
|
||||
.ring { position: absolute; inset: 0; border-radius: 999px; border: 1px solid var(--flag); animation: ring 2s infinite ease-out; opacity: 0; }
|
||||
@keyframes scale { 0%,100% { transform: scale(1);} 50% { transform: scale(1.15);} }
|
||||
@keyframes ring { 0% { transform: scale(1); opacity: 0.7;} 100% { transform: scale(2.6); opacity: 0; } }
|
||||
.clk { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 56px; font-weight: 600; letter-spacing: -0.03em; color: var(--paper); }
|
||||
.meta { font-family: var(--font-mono); font-size: 11px; color: var(--night-fg-3); margin-top: 14px; text-transform: uppercase; letter-spacing: 0.08em;}
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="overline" style="color:var(--night-fg-3); margin-bottom:14px;">Live pulse · signature motion</div>
|
||||
<div class="row">
|
||||
<div class="live"><span class="dot-wrap"><span class="ring"></span><span class="dot"></span></span>LIVE</div>
|
||||
<div class="clk">01:23:45.6</div>
|
||||
</div>
|
||||
<div class="meta">2s loop · scale 1→1.15 · ring 0→2.6× · used at most once per screen</div>
|
||||
</body></html>
|
||||
@@ -0,0 +1,34 @@
|
||||
<!doctype html><html><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../colors_and_type.css">
|
||||
<link rel="stylesheet" href="_card.css">
|
||||
<style>
|
||||
body { padding: 20px; }
|
||||
.grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; }
|
||||
.swatch { border: 1px solid var(--ink); }
|
||||
.chip { height: 80px; display: flex; align-items: flex-end; padding: 8px 10px; }
|
||||
.chip .name { font-family: var(--font-display); font-weight: 600; font-size: 13px; }
|
||||
.meta { padding: 8px 10px; border-top: 1px solid var(--ink); font-family: var(--font-mono); font-size: 11px; line-height: 1.4; }
|
||||
.meta .label { color: var(--ink-3); }
|
||||
.meta .hex { color: var(--ink); }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="overline" style="margin-bottom:12px;">Brand · accents</div>
|
||||
<div class="grid">
|
||||
<div class="swatch">
|
||||
<div class="chip" style="background:var(--flag); color:#fff;"><span class="name">Flag</span></div>
|
||||
<div class="meta"><div class="hex">#E8412B</div><div class="label">Primary CTA · Live · #1 pos</div></div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="chip" style="background:var(--amber); color:var(--ink);"><span class="name">Amber</span></div>
|
||||
<div class="meta"><div class="hex">#F2A516</div><div class="label">Caution · Yellow flag · Warn</div></div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="chip" style="background:var(--green); color:#fff;"><span class="name">Green</span></div>
|
||||
<div class="meta"><div class="hex">#2E8C4A</div><div class="label">Go · PB · Pass</div></div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="chip" style="background:var(--blue); color:#fff;"><span class="name">Blue</span></div>
|
||||
<div class="meta"><div class="hex">#2563C8</div><div class="label">Info · Link · Tracking</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
@@ -0,0 +1,36 @@
|
||||
<!doctype html><html><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../colors_and_type.css">
|
||||
<link rel="stylesheet" href="_card.css">
|
||||
<style>
|
||||
body { padding: 20px; }
|
||||
.row { display: grid; grid-template-columns: repeat(8, 1fr); border: 1px solid var(--ink); }
|
||||
.step { height: 64px; display: flex; flex-direction: column; justify-content: flex-end; padding: 6px 8px; border-right: 1px solid var(--hairline); }
|
||||
.step:last-child { border-right: 0; }
|
||||
.step .name { font-family: var(--font-mono); font-size: 10px; opacity: 0.85; }
|
||||
.step .hex { font-family: var(--font-mono); font-size: 10px; opacity: 0.7; }
|
||||
.label { font-family: var(--font-mono); font-size: 11px; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.06em; margin: 14px 0 6px; }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="overline" style="margin-bottom:8px;">Paper / Ink scale — light surface</div>
|
||||
<div class="row">
|
||||
<div class="step" style="background:#FFFFFF;color:var(--ink);"><div class="name">white</div><div class="hex">#FFFFFF</div></div>
|
||||
<div class="step" style="background:var(--paper);color:var(--ink);"><div class="name">paper</div><div class="hex">#FAFAF7</div></div>
|
||||
<div class="step" style="background:var(--paper-2);color:var(--ink);"><div class="name">paper-2</div><div class="hex">#F2F2EE</div></div>
|
||||
<div class="step" style="background:var(--paper-3);color:var(--ink);"><div class="name">paper-3</div><div class="hex">#E8E8E2</div></div>
|
||||
<div class="step" style="background:var(--ink-6);color:var(--ink);"><div class="name">ink-6</div><div class="hex">#DCDCD4</div></div>
|
||||
<div class="step" style="background:var(--ink-5);color:var(--ink);"><div class="name">ink-5</div><div class="hex">#B8B8B0</div></div>
|
||||
<div class="step" style="background:var(--ink-4);color:#fff;"><div class="name">ink-4</div><div class="hex">#8A8A82</div></div>
|
||||
<div class="step" style="background:var(--ink-3);color:#fff;"><div class="name">ink-3</div><div class="hex">#5A5A53</div></div>
|
||||
</div>
|
||||
<div class="label">Ink scale — type & strokes</div>
|
||||
<div class="row">
|
||||
<div class="step" style="background:var(--ink-3);color:#fff;"><div class="name">ink-3</div><div class="hex">#5A5A53</div></div>
|
||||
<div class="step" style="background:var(--ink-2);color:#fff;"><div class="name">ink-2</div><div class="hex">#2A2A26</div></div>
|
||||
<div class="step" style="background:var(--ink);color:#fff;"><div class="name">ink</div><div class="hex">#0E0E0C</div></div>
|
||||
<div class="step" style="background:var(--night-3);color:#fff;"><div class="name">night-3</div><div class="hex">#1E222A</div></div>
|
||||
<div class="step" style="background:var(--night-2);color:#fff;"><div class="name">night-2</div><div class="hex">#14171C</div></div>
|
||||
<div class="step" style="background:var(--night);color:#fff;"><div class="name">night</div><div class="hex">#0B0D10</div></div>
|
||||
<div class="step" style="background:#000;color:#fff;"><div class="name">black</div><div class="hex">#000000</div></div>
|
||||
<div class="step" style="background:transparent;color:var(--ink);border-left: 1px solid var(--hairline);"><div class="name">—</div><div class="hex"></div></div>
|
||||
</div>
|
||||
</body></html>
|
||||
@@ -0,0 +1,34 @@
|
||||
<!doctype html><html><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../colors_and_type.css">
|
||||
<link rel="stylesheet" href="_card.css">
|
||||
<style>
|
||||
body { padding: 0; background: var(--night); color: var(--night-fg); }
|
||||
.wrap { padding: 22px; }
|
||||
.row { display: grid; grid-template-columns: repeat(6, 1fr); border: 1px solid var(--night-line); }
|
||||
.step { height: 70px; display: flex; flex-direction: column; justify-content: flex-end; padding: 6px 8px; border-right: 1px solid var(--night-line); font-family: var(--font-mono); font-size: 10px; }
|
||||
.step:last-child { border-right: 0; }
|
||||
.pill { display:inline-flex; align-items:center; gap:6px; padding:3px 8px; border:1px solid currentColor; font-family: var(--font-mono); font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; border-radius: 999px; margin-right:8px;}
|
||||
.pill .dot { width:6px; height:6px; border-radius:999px; background: currentColor; }
|
||||
.live-dot { display:inline-block; width:8px; height:8px; border-radius:999px; background: var(--flag); margin-right:6px; }
|
||||
.lbl { font-family: var(--font-mono); font-size: 11px; color: var(--night-fg-3); text-transform: uppercase; letter-spacing: 0.06em; margin: 14px 0 6px; }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div class="overline" style="margin-bottom:8px; color: var(--night-fg-2);">Night mode · trackside / kiosk</div>
|
||||
<div class="row">
|
||||
<div class="step" style="background:var(--night); color:var(--night-fg);"><div>night</div><div style="opacity:.7">#0B0D10</div></div>
|
||||
<div class="step" style="background:var(--night-2); color:var(--night-fg);"><div>night-2</div><div style="opacity:.7">#14171C</div></div>
|
||||
<div class="step" style="background:var(--night-3); color:var(--night-fg);"><div>night-3</div><div style="opacity:.7">#1E222A</div></div>
|
||||
<div class="step" style="background:var(--night-line); color:var(--night-fg);"><div>line</div><div style="opacity:.7">#2A2F38</div></div>
|
||||
<div class="step" style="background:var(--night-fg-3); color:var(--night);"><div>fg-3</div><div style="opacity:.7">#8A8A82</div></div>
|
||||
<div class="step" style="background:var(--night-fg); color:var(--night);"><div>fg</div><div style="opacity:.7">#F2F2EE</div></div>
|
||||
</div>
|
||||
<div class="lbl">Status pills on night</div>
|
||||
<div>
|
||||
<span class="pill" style="color:var(--flag);"><span class="live-dot"></span>LIVE</span>
|
||||
<span class="pill" style="color:var(--green);"><span class="dot"></span>FINISHED</span>
|
||||
<span class="pill" style="color:var(--amber);"><span class="dot"></span>FLAG</span>
|
||||
<span class="pill" style="color:var(--night-fg-3);"><span class="dot"></span>DNS</span>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
@@ -0,0 +1,34 @@
|
||||
<!doctype html><html><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../colors_and_type.css">
|
||||
<link rel="stylesheet" href="_card.css">
|
||||
<style>
|
||||
body { padding: 20px; }
|
||||
.grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; }
|
||||
.item { border: 1px solid var(--ink); }
|
||||
.top { padding: 12px; background: var(--paper-2); border-bottom: 1px solid var(--ink); display:flex; align-items:center; gap:8px; }
|
||||
.pill { display:inline-flex; align-items:center; gap:6px; padding:3px 8px; border:1px solid currentColor; font-family: var(--font-mono); font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; border-radius: 999px; }
|
||||
.pill .dot { width:6px; height:6px; border-radius:999px; background: currentColor; }
|
||||
.body { padding: 12px; font-size: 13px; color: var(--ink-3); line-height: 1.4; }
|
||||
.body strong { color: var(--ink); font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.04em; }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="overline" style="margin-bottom:12px;">Semantic · status</div>
|
||||
<div class="grid">
|
||||
<div class="item">
|
||||
<div class="top"><span class="pill" style="color:var(--green); background: var(--green-tint);"><span class="dot"></span>OK</span></div>
|
||||
<div class="body"><strong>green / ok</strong><br/>Started · finished · pass · personal best</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="top"><span class="pill" style="color:var(--amber-2); background: var(--amber-tint);"><span class="dot"></span>WARN</span></div>
|
||||
<div class="body"><strong>amber / warn</strong><br/>Yellow flag · slow split · review</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="top"><span class="pill" style="color:var(--flag-2); background: var(--flag-tint);"><span class="dot"></span>DANGER</span></div>
|
||||
<div class="body"><strong>flag / danger</strong><br/>DSQ · DNF · red flag · error</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<div class="top"><span class="pill" style="color:var(--blue-2); background: var(--blue-tint);"><span class="dot"></span>INFO</span></div>
|
||||
<div class="body"><strong>blue / info</strong><br/>Live · tracking · link</div>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
@@ -0,0 +1,35 @@
|
||||
<!doctype html><html><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../colors_and_type.css">
|
||||
<link rel="stylesheet" href="_card.css">
|
||||
<style>
|
||||
body { padding: 20px; }
|
||||
.swatches { display: grid; grid-template-columns: repeat(8, 1fr); border: 1px solid var(--ink); }
|
||||
.sw { aspect-ratio: 1/1.1; display:flex; flex-direction:column; justify-content:flex-end; padding: 6px 8px; color:#fff; font-family: var(--font-mono); font-size: 10px; }
|
||||
.sw .n { opacity:0.85; }
|
||||
.sw .h { opacity:0.7; }
|
||||
.chart { margin-top: 14px; height: 90px; border: 1px solid var(--ink); padding: 10px; display:flex; align-items:flex-end; gap: 4px; background: var(--paper-2);}
|
||||
.bar { flex: 1; }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="overline" style="margin-bottom:8px;">Categorical · data viz (8)</div>
|
||||
<div class="swatches">
|
||||
<div class="sw" style="background:var(--viz-1);"><div class="n">viz-1</div><div class="h">E8412B</div></div>
|
||||
<div class="sw" style="background:var(--viz-2);"><div class="n">viz-2</div><div class="h">2563C8</div></div>
|
||||
<div class="sw" style="background:var(--viz-3);"><div class="n">viz-3</div><div class="h">2E8C4A</div></div>
|
||||
<div class="sw" style="background:var(--viz-4); color:var(--ink);"><div class="n">viz-4</div><div class="h">F2A516</div></div>
|
||||
<div class="sw" style="background:var(--viz-5);"><div class="n">viz-5</div><div class="h">6B46C1</div></div>
|
||||
<div class="sw" style="background:var(--viz-6);"><div class="n">viz-6</div><div class="h">188C8A</div></div>
|
||||
<div class="sw" style="background:var(--viz-7);"><div class="n">viz-7</div><div class="h">C9296F</div></div>
|
||||
<div class="sw" style="background:var(--viz-8);"><div class="n">viz-8</div><div class="h">5A5A53</div></div>
|
||||
</div>
|
||||
<div class="chart">
|
||||
<div class="bar" style="background:var(--viz-1); height:60%"></div>
|
||||
<div class="bar" style="background:var(--viz-2); height:80%"></div>
|
||||
<div class="bar" style="background:var(--viz-3); height:42%"></div>
|
||||
<div class="bar" style="background:var(--viz-4); height:90%"></div>
|
||||
<div class="bar" style="background:var(--viz-5); height:35%"></div>
|
||||
<div class="bar" style="background:var(--viz-6); height:65%"></div>
|
||||
<div class="bar" style="background:var(--viz-7); height:48%"></div>
|
||||
<div class="bar" style="background:var(--viz-8); height:25%"></div>
|
||||
</div>
|
||||
</body></html>
|
||||
@@ -0,0 +1,40 @@
|
||||
<!doctype html><html><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../colors_and_type.css">
|
||||
<link rel="stylesheet" href="_card.css">
|
||||
<style>
|
||||
body { padding: 24px; }
|
||||
.row { display: flex; flex-wrap: wrap; gap: 14px; }
|
||||
.alert { display: grid; grid-template-columns: 8px 1fr auto; gap: 12px; align-items: center; padding: 10px 14px; border: 1px solid var(--ink); background: var(--paper-2); width: 100%; }
|
||||
.alert .bar { width: 4px; align-self: stretch; }
|
||||
.alert .msg { font-family: var(--font-display); font-size: 13px; }
|
||||
.alert .meta { font-family: var(--font-mono); font-size: 11px; color: var(--ink-3); }
|
||||
.alert.warn .bar { background: var(--amber); }
|
||||
.alert.danger .bar { background: var(--flag); }
|
||||
.alert.ok .bar { background: var(--green); }
|
||||
.alert.info .bar { background: var(--blue); }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="overline" style="margin-bottom:14px;">Inline alerts · race control</div>
|
||||
<div class="row">
|
||||
<div class="alert danger">
|
||||
<div class="bar"></div>
|
||||
<div><span class="msg">Bib 247 — missed split 3</span> <span class="meta">flagging for review</span></div>
|
||||
<div class="meta">14:02:11</div>
|
||||
</div>
|
||||
<div class="alert warn">
|
||||
<div class="bar"></div>
|
||||
<div><span class="msg">Yellow flag — sector 2</span> <span class="meta">debris cleared in 90s</span></div>
|
||||
<div class="meta">14:00:42</div>
|
||||
</div>
|
||||
<div class="alert ok">
|
||||
<div class="bar"></div>
|
||||
<div><span class="msg">Wave 3 cleared start mat</span> <span class="meta">412 / 412 starters</span></div>
|
||||
<div class="meta">13:45:00</div>
|
||||
</div>
|
||||
<div class="alert info">
|
||||
<div class="bar"></div>
|
||||
<div><span class="msg">Tracking active</span> <span class="meta">14 of 14 mats online</span></div>
|
||||
<div class="meta">13:30:00</div>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
@@ -0,0 +1,33 @@
|
||||
<!doctype html><html><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../colors_and_type.css">
|
||||
<link rel="stylesheet" href="_card.css">
|
||||
<style>
|
||||
body { padding: 24px; }
|
||||
.row { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
|
||||
.pill { display:inline-flex; align-items:center; gap:6px; padding: 3px 10px; font-family: var(--font-mono); font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; border-radius: 999px; border: 1px solid currentColor;}
|
||||
.pill .dot { width:6px; height:6px; border-radius:999px; background: currentColor; }
|
||||
.live .dot { background: var(--flag); animation: pulse 2s infinite ease-out; }
|
||||
@keyframes pulse { 0% { box-shadow: 0 0 0 0 var(--flag); } 100% { box-shadow: 0 0 0 8px rgba(232,65,43,0); } }
|
||||
.chip { display:inline-flex; align-items:center; gap:6px; padding: 3px 8px; font-family: var(--font-mono); font-size: 11px; border: 1px solid var(--ink); background: var(--paper-2); border-radius: 2px;}
|
||||
.lbl { font-family: var(--font-mono); font-size: 10px; color: var(--ink-3); margin: 16px 0 8px; }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="overline" style="margin-bottom:14px;">Status badges</div>
|
||||
<div class="row">
|
||||
<span class="pill live" style="color:var(--flag);"><span class="dot"></span>LIVE</span>
|
||||
<span class="pill" style="color:var(--green);"><span class="dot"></span>FINISHED</span>
|
||||
<span class="pill" style="color:var(--amber-2);"><span class="dot"></span>FLAG</span>
|
||||
<span class="pill" style="color:var(--blue-2);"><span class="dot"></span>STARTED</span>
|
||||
<span class="pill" style="color:var(--ink-3);"><span class="dot"></span>DNS</span>
|
||||
<span class="pill" style="color:var(--ink);"><span class="dot"></span>DNF</span>
|
||||
<span class="pill" style="color:var(--flag-2);"><span class="dot"></span>DSQ</span>
|
||||
</div>
|
||||
<div class="lbl">Filter chips</div>
|
||||
<div class="row">
|
||||
<span class="chip">Wave 1</span>
|
||||
<span class="chip">Wave 2</span>
|
||||
<span class="chip" style="background:var(--ink); color:var(--paper); border-color:var(--ink);">Wave 3 ✕</span>
|
||||
<span class="chip">M40–49</span>
|
||||
<span class="chip">Elite</span>
|
||||
</div>
|
||||
</body></html>
|
||||
@@ -0,0 +1,39 @@
|
||||
<!doctype html><html><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../colors_and_type.css">
|
||||
<link rel="stylesheet" href="_card.css">
|
||||
<style>
|
||||
body { padding: 24px; }
|
||||
.row { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; }
|
||||
.btn {
|
||||
font-family: var(--font-display); font-weight: 600; font-size: 14px;
|
||||
border: 1px solid var(--ink); background: var(--paper); color: var(--ink);
|
||||
padding: 0 16px; height: 36px; cursor: pointer; display: inline-flex;
|
||||
align-items: center; gap: 8px; box-shadow: 2px 2px 0 0 var(--ink);
|
||||
transition: transform 120ms var(--ease-snap), box-shadow 120ms var(--ease-snap);
|
||||
border-radius: 0;
|
||||
}
|
||||
.btn.primary { background: var(--ink); color: var(--paper); }
|
||||
.btn.flag { background: var(--flag); color: #fff; border-color: var(--ink); }
|
||||
.btn.ghost { box-shadow: none; }
|
||||
.btn.sm { height: 28px; padding: 0 12px; font-size: 12px; }
|
||||
.btn.lg { height: 44px; padding: 0 22px; font-size: 15px; }
|
||||
.btn.disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
.lbl { font-family: var(--font-mono); font-size: 10px; color: var(--ink-3); width: 100%; margin-top: 6px; }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="overline" style="margin-bottom:14px;">Buttons</div>
|
||||
<div class="row">
|
||||
<button class="btn primary">Start race</button>
|
||||
<button class="btn flag">⏺ Go live</button>
|
||||
<button class="btn">Export results</button>
|
||||
<button class="btn ghost">Cancel</button>
|
||||
<button class="btn disabled" disabled>Disabled</button>
|
||||
</div>
|
||||
<div class="lbl">Default · primary (ink) · flag (live) · ghost · disabled</div>
|
||||
<div class="row" style="margin-top:18px;">
|
||||
<button class="btn sm">Small</button>
|
||||
<button class="btn">Default</button>
|
||||
<button class="btn lg primary">Large primary</button>
|
||||
</div>
|
||||
<div class="lbl">Sizes — sm 28 · md 36 · lg 44</div>
|
||||
</body></html>
|
||||
@@ -0,0 +1,42 @@
|
||||
<!doctype html><html><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../colors_and_type.css">
|
||||
<link rel="stylesheet" href="_card.css">
|
||||
<style>
|
||||
body { padding: 24px; }
|
||||
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
||||
.field { display: flex; flex-direction: column; gap: 6px; }
|
||||
.field label { font-family: var(--font-display); font-size: 11px; font-weight: 600; letter-spacing: 0.10em; text-transform: uppercase; color: var(--ink-3);}
|
||||
.input {
|
||||
border: 1px solid var(--ink); background: var(--paper); height: 36px;
|
||||
padding: 0 12px; font-family: var(--font-mono); font-size: 14px; color: var(--ink);
|
||||
border-radius: 2px;
|
||||
}
|
||||
.input:focus { outline: 2px solid var(--flag); outline-offset: 2px; }
|
||||
.input.error { border-color: var(--flag); }
|
||||
.hint { font-family: var(--font-mono); font-size: 11px; color: var(--ink-4); }
|
||||
.err { color: var(--flag); }
|
||||
.select { appearance: none; background-image: linear-gradient(45deg,transparent 50%,var(--ink) 50%),linear-gradient(135deg,var(--ink) 50%,transparent 50%); background-position: calc(100% - 14px) 14px, calc(100% - 9px) 14px; background-size: 5px 5px; background-repeat: no-repeat; padding-right: 30px;}
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="overline" style="margin-bottom:14px;">Inputs</div>
|
||||
<div class="grid">
|
||||
<div class="field">
|
||||
<label>Bib number</label>
|
||||
<input class="input" value="247" />
|
||||
<div class="hint">Tabular figures — leading zeros optional</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Wave</label>
|
||||
<select class="input select"><option>Wave 1 · Elite</option></select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Start time</label>
|
||||
<input class="input" value="09:15:00.000" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Bib number</label>
|
||||
<input class="input error" value="—" />
|
||||
<div class="hint err">Bib already in use by Wave 2</div>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
@@ -0,0 +1,34 @@
|
||||
<!doctype html><html><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../colors_and_type.css">
|
||||
<link rel="stylesheet" href="_card.css">
|
||||
<style>
|
||||
body { padding: 24px; }
|
||||
.grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 14px; }
|
||||
.card { border: 1px solid var(--ink); padding: 14px; background: var(--paper); }
|
||||
.lbl { font-family: var(--font-display); font-size: 11px; font-weight: 600; letter-spacing: 0.10em; text-transform: uppercase; color: var(--ink-3); }
|
||||
.num { font-family: var(--font-mono); font-variant-numeric: tabular-nums; color: var(--ink); font-weight: 600; letter-spacing: -0.02em; line-height: 1; }
|
||||
.delta { font-family: var(--font-mono); font-size: 11px; }
|
||||
.up { color: var(--green); }
|
||||
.dn { color: var(--flag); }
|
||||
.meta { font-family: var(--font-mono); font-size: 11px; color: var(--ink-4); }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="overline" style="margin-bottom:14px;">Stat / KPI cards</div>
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<div class="lbl">Active bibs</div>
|
||||
<div class="num" style="font-size: 40px; margin-top:8px;">1,247</div>
|
||||
<div class="delta up" style="margin-top:6px;">↑ 12 in last 5 min</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="lbl">Lead time</div>
|
||||
<div class="num" style="font-size: 40px; margin-top:8px;">42:18.4</div>
|
||||
<div class="delta meta" style="margin-top:6px;">Bib 247 · split 3</div>
|
||||
</div>
|
||||
<div class="card" style="background: var(--ink); color: var(--paper); border-color: var(--ink);">
|
||||
<div class="lbl" style="color: var(--paper-3);">Leader gap</div>
|
||||
<div class="num" style="font-size: 40px; margin-top:8px; color: var(--paper);">+0:04.2</div>
|
||||
<div class="delta dn" style="margin-top:6px;">↓ closing 0.3s/km</div>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
@@ -0,0 +1,32 @@
|
||||
<!doctype html><html><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../colors_and_type.css">
|
||||
<link rel="stylesheet" href="_card.css">
|
||||
<style>
|
||||
body { padding: 0; }
|
||||
.wrap { padding: 18px; }
|
||||
table { width: 100%; border-collapse: collapse; border: 1px solid var(--ink); }
|
||||
th, td { text-align: left; padding: 0 10px; height: 28px; font-family: var(--font-mono); font-size: 12px; border-bottom: 1px solid var(--hairline); }
|
||||
th { background: var(--paper-2); font-weight: 600; font-size: 10px; letter-spacing: 0.10em; text-transform: uppercase; color: var(--ink-3); border-bottom: 1px solid var(--ink); }
|
||||
tr:nth-child(even) td { background: rgba(14,14,12,0.025); }
|
||||
td.num { text-align: right; font-variant-numeric: tabular-nums; }
|
||||
.pos { font-weight: 700; color: var(--ink); }
|
||||
.pos.gold { color: var(--flag); }
|
||||
.gain { color: var(--green); }
|
||||
.loss { color: var(--flag); }
|
||||
.name { font-family: var(--font-display); font-weight: 500; font-size: 13px; }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div class="overline" style="margin-bottom:10px;">Leaderboard table · dense 28px</div>
|
||||
<table>
|
||||
<thead><tr><th style="width:40px;">POS</th><th style="width:50px;">BIB</th><th>NAME</th><th>WAVE</th><th class="num">SPLIT 3</th><th class="num">GAP</th><th class="num">PACE</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="pos gold">1</td><td>247</td><td class="name">Maya Chen</td><td>Elite</td><td class="num">00:42:18.4</td><td class="num">—</td><td class="num">04:13</td></tr>
|
||||
<tr><td class="pos">2</td><td>188</td><td class="name">Ravi Park</td><td>Elite</td><td class="num">00:42:22.6</td><td class="num loss">+0:04.2</td><td class="num">04:14</td></tr>
|
||||
<tr><td class="pos">3</td><td>044</td><td class="name">Noemi Vega</td><td>Elite</td><td class="num">00:42:30.1</td><td class="num loss">+0:11.7</td><td class="num">04:15</td></tr>
|
||||
<tr><td class="pos">4</td><td>302</td><td class="name">Tomás Riera</td><td>Elite</td><td class="num">00:42:55.0</td><td class="num loss">+0:36.6</td><td class="num">04:18</td></tr>
|
||||
<tr><td class="pos">5</td><td>119</td><td class="name">Anika Joshi</td><td>Elite</td><td class="num">00:43:01.2</td><td class="num loss">+0:42.8</td><td class="num">04:19</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body></html>
|
||||
@@ -0,0 +1,43 @@
|
||||
<!doctype html><html><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../colors_and_type.css">
|
||||
<link rel="stylesheet" href="_card.css">
|
||||
<style>
|
||||
body { padding: 0; }
|
||||
.frame { border: 1px solid var(--ink); margin: 20px; height: 220px; display: grid; grid-template-rows: 36px 1fr; }
|
||||
.top { border-bottom: 1px solid var(--ink); display:flex; align-items:center; padding: 0 14px; gap: 10px; background: var(--paper-2); }
|
||||
.logo { font-family: var(--font-display); font-weight:700; letter-spacing: -0.02em; }
|
||||
.nav { display: flex; gap: 14px; margin-left: 18px; font-family: var(--font-display); font-size:13px; font-weight:500; color: var(--ink-3); }
|
||||
.clk { margin-left: auto; font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 13px; }
|
||||
.cols { display: grid; grid-template-columns: 64px 1fr; height:100%;}
|
||||
.side { border-right: 1px solid var(--ink); background: var(--paper-2); }
|
||||
.side-item { height: 36px; border-bottom: 1px solid var(--hairline); display:flex; align-items:center; justify-content:center; font-family: var(--font-mono); font-size: 11px; color: var(--ink-3); }
|
||||
.body { padding: 14px; }
|
||||
.grid { display: grid; grid-template-columns: 4fr 4fr 2fr; gap: 12px; height: 100%; }
|
||||
.col { border: 1px solid var(--hairline); padding: 8px; font-family: var(--font-mono); font-size: 10px; color: var(--ink-3); display:flex; flex-direction:column; gap:4px;}
|
||||
.col strong { color: var(--ink); font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase;}
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<div class="top">
|
||||
<div class="logo">TRM</div>
|
||||
<div class="nav">Events · Live · Results · Settings</div>
|
||||
<div class="clk">14:02:11</div>
|
||||
</div>
|
||||
<div class="cols">
|
||||
<div class="side">
|
||||
<div class="side-item">⌘</div>
|
||||
<div class="side-item">▦</div>
|
||||
<div class="side-item">▶</div>
|
||||
<div class="side-item">⊙</div>
|
||||
<div class="side-item">≡</div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="grid">
|
||||
<div class="col"><strong>Leaderboard 40%</strong>POS BIB NAME GAP<br/>1 247 M.Chen —<br/>2 188 R.Park +0:04.2<br/>3 044 N.Vega +0:11.7</div>
|
||||
<div class="col"><strong>Map 40%</strong>Track view · checkpoints · live dots</div>
|
||||
<div class="col"><strong>Detail 20%</strong>Selected athlete</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
@@ -0,0 +1,22 @@
|
||||
<!doctype html><html><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../colors_and_type.css">
|
||||
<link rel="stylesheet" href="_card.css">
|
||||
<style>
|
||||
body { padding: 20px; }
|
||||
.row { display: grid; grid-template-columns: repeat(6, 1fr); gap: 14px; }
|
||||
.item { display: flex; flex-direction: column; align-items: center; gap: 8px; }
|
||||
.box { width: 80px; height: 80px; background: var(--paper-2); border: 1px solid var(--ink); }
|
||||
.lbl { font-family: var(--font-mono); font-size: 10px; color: var(--ink-3); }
|
||||
.val { font-family: var(--font-mono); font-size: 11px; color: var(--ink); font-weight: 600; }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="overline" style="margin-bottom:14px;">Corner radius · square by default</div>
|
||||
<div class="row">
|
||||
<div class="item"><div class="box" style="border-radius:0;"></div><div class="val">0</div><div class="lbl">r-0 default</div></div>
|
||||
<div class="item"><div class="box" style="border-radius:2px;"></div><div class="val">2</div><div class="lbl">r-1</div></div>
|
||||
<div class="item"><div class="box" style="border-radius:4px;"></div><div class="val">4</div><div class="lbl">r-2 chip</div></div>
|
||||
<div class="item"><div class="box" style="border-radius:6px;"></div><div class="val">6</div><div class="lbl">r-3 sm card</div></div>
|
||||
<div class="item"><div class="box" style="border-radius:10px;"></div><div class="val">10</div><div class="lbl">r-4 lg card</div></div>
|
||||
<div class="item"><div class="box" style="border-radius:999px;"></div><div class="val">∞</div><div class="lbl">r-pill status</div></div>
|
||||
</div>
|
||||
</body></html>
|
||||
@@ -0,0 +1,27 @@
|
||||
<!doctype html><html><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../colors_and_type.css">
|
||||
<link rel="stylesheet" href="_card.css">
|
||||
<style>
|
||||
body { padding: 20px; }
|
||||
.row { display: flex; align-items: flex-end; gap: 12px; }
|
||||
.item { display: flex; flex-direction: column; align-items: center; gap: 6px; }
|
||||
.bar { background: var(--ink); }
|
||||
.lbl { font-family: var(--font-mono); font-size: 10px; color: var(--ink-3); }
|
||||
.val { font-family: var(--font-mono); font-size: 11px; color: var(--ink); font-weight: 600; }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="overline" style="margin-bottom:14px;">Spacing scale · 4-px base</div>
|
||||
<div class="row">
|
||||
<div class="item"><div class="bar" style="width:4px;height:4px;"></div><div class="val">4</div><div class="lbl">s-2</div></div>
|
||||
<div class="item"><div class="bar" style="width:8px;height:8px;"></div><div class="val">8</div><div class="lbl">s-3</div></div>
|
||||
<div class="item"><div class="bar" style="width:12px;height:12px;"></div><div class="val">12</div><div class="lbl">s-4</div></div>
|
||||
<div class="item"><div class="bar" style="width:16px;height:16px;"></div><div class="val">16</div><div class="lbl">s-5</div></div>
|
||||
<div class="item"><div class="bar" style="width:20px;height:20px;"></div><div class="val">20</div><div class="lbl">s-6</div></div>
|
||||
<div class="item"><div class="bar" style="width:24px;height:24px;"></div><div class="val">24</div><div class="lbl">s-7</div></div>
|
||||
<div class="item"><div class="bar" style="width:32px;height:32px;"></div><div class="val">32</div><div class="lbl">s-8</div></div>
|
||||
<div class="item"><div class="bar" style="width:40px;height:40px;"></div><div class="val">40</div><div class="lbl">s-9</div></div>
|
||||
<div class="item"><div class="bar" style="width:48px;height:48px;"></div><div class="val">48</div><div class="lbl">s-10</div></div>
|
||||
<div class="item"><div class="bar" style="width:64px;height:64px;"></div><div class="val">64</div><div class="lbl">s-11</div></div>
|
||||
<div class="item"><div class="bar" style="width:80px;height:80px;"></div><div class="val">80</div><div class="lbl">s-12</div></div>
|
||||
</div>
|
||||
</body></html>
|
||||
@@ -0,0 +1,20 @@
|
||||
<!doctype html><html><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../colors_and_type.css">
|
||||
<link rel="stylesheet" href="_card.css">
|
||||
<style>
|
||||
body { padding: 24px; }
|
||||
.row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 24px; }
|
||||
.item { display: flex; flex-direction: column; align-items: flex-start; gap: 10px; }
|
||||
.box { width: 110px; height: 70px; background: var(--paper); border: 1px solid var(--ink); display:flex; align-items:center; justify-content:center; font-family: var(--font-display); font-weight:600; font-size:14px;}
|
||||
.lbl { font-family: var(--font-mono); font-size: 10px; color: var(--ink-3); }
|
||||
.val { font-family: var(--font-mono); font-size: 11px; color: var(--ink); font-weight: 600; }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="overline" style="margin-bottom:18px;">Elevation · printed offset shadow</div>
|
||||
<div class="row">
|
||||
<div class="item"><div class="box">flat</div><div class="val">shadow-0</div><div class="lbl">surface · no shadow</div></div>
|
||||
<div class="item"><div class="box" style="box-shadow: 2px 2px 0 0 var(--ink);">2px</div><div class="val">shadow-2</div><div class="lbl">button rest</div></div>
|
||||
<div class="item"><div class="box" style="box-shadow: 4px 4px 0 0 var(--ink);">4px</div><div class="val">shadow-3</div><div class="lbl">button hover · card</div></div>
|
||||
<div class="item"><div class="box" style="box-shadow: 0 8px 24px rgba(14,14,12,0.12);">soft</div><div class="val">shadow-pop</div><div class="lbl">menu · marketing</div></div>
|
||||
</div>
|
||||
</body></html>
|
||||
@@ -0,0 +1,34 @@
|
||||
<!doctype html><html><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../colors_and_type.css">
|
||||
<link rel="stylesheet" href="_card.css">
|
||||
<style>
|
||||
body { padding: 24px; }
|
||||
.stack > * + * { margin-top: 14px; }
|
||||
.meta { color: var(--ink-4); font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.04em; text-transform: uppercase; }
|
||||
.line { display: grid; grid-template-columns: 1fr auto; align-items: baseline; gap: 16px; border-bottom: 1px solid var(--hairline); padding-bottom: 10px;}
|
||||
.line:last-child { border-bottom: 0; padding-bottom: 0; }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="stack">
|
||||
<div class="line">
|
||||
<div class="lead">Race directors trust TRM to time everything from a 60-runner park 5K to a 24-hour endurance event with 4,000 starters across six waves.</div>
|
||||
<div class="meta">Lead · 17/24 · Inter 400</div>
|
||||
</div>
|
||||
<div class="line">
|
||||
<div class="body">Body. Used in marketing copy and long-form documentation. The dashboard itself rarely contains body prose — it's all tables and tokens.</div>
|
||||
<div class="meta">Body · 15/22 · Inter 400</div>
|
||||
</div>
|
||||
<div class="line">
|
||||
<div class="small">Small. Field hints, table footnotes, secondary detail beneath a headline number.</div>
|
||||
<div class="meta">Small · 13/18 · Inter 400</div>
|
||||
</div>
|
||||
<div class="line">
|
||||
<div class="micro">Micro. Reserved for table meta and timestamp pills. Use sparingly — must remain legible at glance.</div>
|
||||
<div class="meta">Micro · 12/16 · Inter 400</div>
|
||||
</div>
|
||||
<div class="line">
|
||||
<div class="overline">Wave 3 · 4.2 km · 47 starters</div>
|
||||
<div class="meta">Overline · 11/14 · Goldplay 600 · +0.10em</div>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
@@ -0,0 +1,34 @@
|
||||
<!doctype html><html><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../colors_and_type.css">
|
||||
<link rel="stylesheet" href="_card.css">
|
||||
<style>
|
||||
body { padding: 24px; }
|
||||
.stack > * + * { margin-top: 14px; }
|
||||
.meta { color: var(--ink-4); font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.04em; text-transform: uppercase; }
|
||||
.line { display: grid; grid-template-columns: 1fr auto; align-items: baseline; gap: 16px; border-bottom: 1px solid var(--hairline); padding-bottom: 10px;}
|
||||
.line:last-child { border-bottom: 0; padding-bottom: 0; }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="stack">
|
||||
<div class="line">
|
||||
<div class="display" style="font-size:56px;">Lap 14 / 22</div>
|
||||
<div class="meta">Display · 56/56 · Goldplay 700</div>
|
||||
</div>
|
||||
<div class="line">
|
||||
<div class="h1">Run any event.</div>
|
||||
<div class="meta">H1 · 48/52 · Goldplay 700</div>
|
||||
</div>
|
||||
<div class="line">
|
||||
<div class="h2">Time any sport.</div>
|
||||
<div class="meta">H2 · 34/38 · Goldplay 700</div>
|
||||
</div>
|
||||
<div class="line">
|
||||
<div class="h3">Wave 3 starts at 09:15</div>
|
||||
<div class="meta">H3 · 26/30 · Goldplay 600</div>
|
||||
</div>
|
||||
<div class="line">
|
||||
<div class="h4">Live race control</div>
|
||||
<div class="meta">H4 · 20/24 · Goldplay 600</div>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
@@ -0,0 +1,41 @@
|
||||
<!doctype html><html><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../colors_and_type.css">
|
||||
<link rel="stylesheet" href="_card.css">
|
||||
<style>
|
||||
body { padding: 24px; }
|
||||
.meta { color: var(--ink-4); font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.04em; text-transform: uppercase; }
|
||||
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px 24px; }
|
||||
.stack > * + * { margin-top: 12px; }
|
||||
.clk { font-family: var(--font-mono); font-variant-numeric: tabular-nums; color: var(--ink); letter-spacing: -0.03em; line-height: 1; font-weight: 600; }
|
||||
.row-meta { display: flex; justify-content: space-between; align-items: baseline; }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="stack">
|
||||
<div class="row-meta">
|
||||
<div class="clk" style="font-size:80px;">01:23:45.6</div>
|
||||
<div class="meta">Jumbo · JetBrains Mono 600 · 80px</div>
|
||||
</div>
|
||||
<div class="row-meta">
|
||||
<div class="clk" style="font-size:40px;">+00:04.218</div>
|
||||
<div class="meta">Mono lg · 40px · gap value</div>
|
||||
</div>
|
||||
<div class="grid" style="margin-top: 6px;">
|
||||
<div>
|
||||
<div class="num" style="font-size:22px;">04:13<span style="color:var(--ink-4)">/km</span></div>
|
||||
<div class="meta" style="margin-top:4px;">Mono md · 22px · pace</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="num" style="font-size:22px;">247</div>
|
||||
<div class="meta" style="margin-top:4px;">Mono md · bib</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="num" style="font-size:14px;">POS BIB NAME GAP</div>
|
||||
<div class="meta" style="margin-top:4px;">Table header · 14px · uppercase</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="num" style="font-size:14px;"> 1 247 M. CHEN —</div>
|
||||
<div class="meta" style="margin-top:4px;">Table row · 14px · tabular figs</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
@@ -0,0 +1,25 @@
|
||||
<!doctype html><html><head><meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../colors_and_type.css">
|
||||
<link rel="stylesheet" href="_card.css">
|
||||
<style>
|
||||
body { padding: 24px; }
|
||||
.scale { display: grid; grid-template-columns: 60px 80px 80px 1fr; gap: 8px 16px; align-items: baseline; }
|
||||
.scale > div:nth-child(4n+1) { font-family: var(--font-mono); font-size: 11px; color: var(--ink-3); letter-spacing: 0.06em; text-transform: uppercase; }
|
||||
.scale > div:nth-child(4n+2),
|
||||
.scale > div:nth-child(4n+3) { font-family: var(--font-mono); font-size: 11px; color: var(--ink-4); }
|
||||
.scale > div:nth-child(4n+4) { font-family: var(--font-display); color: var(--ink); }
|
||||
.row-h { display:contents; }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="overline" style="margin-bottom:14px;">Type scale</div>
|
||||
<div class="scale">
|
||||
<div>display</div><div>72px</div><div>1.05</div><div style="font-size:48px;font-weight:700;letter-spacing:-0.02em;">Aa Race</div>
|
||||
<div>h1</div><div>48px</div><div>1.05</div><div style="font-size:36px;font-weight:700;letter-spacing:-0.02em;">Aa Race</div>
|
||||
<div>h2</div><div>34px</div><div>1.20</div><div style="font-size:28px;font-weight:700;letter-spacing:-0.02em;">Aa Race</div>
|
||||
<div>h3</div><div>26px</div><div>1.20</div><div style="font-size:22px;font-weight:600;">Aa Race</div>
|
||||
<div>h4</div><div>20px</div><div>1.35</div><div style="font-size:18px;font-weight:600;">Aa Race</div>
|
||||
<div>body</div><div>15px</div><div>1.55</div><div style="font-family:var(--font-body);font-size:15px;">The race begins at noon.</div>
|
||||
<div>small</div><div>13px</div><div>1.55</div><div style="font-family:var(--font-body);font-size:13px;">The race begins at noon.</div>
|
||||
<div>over</div><div>11px</div><div>+0.10em</div><div class="overline" style="font-size:11px;">Wave 3 · 47 starters</div>
|
||||
</div>
|
||||
</body></html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
@@ -0,0 +1,23 @@
|
||||
function AlertFeed({ alerts }) {
|
||||
return (
|
||||
<div className="card" style={{ height: '100%' }}>
|
||||
<div className="card-head">
|
||||
<h3>Race control feed</h3>
|
||||
<span className="pill live" style={{ marginLeft: 'auto' }}><span className="dot"></span>LIVE</span>
|
||||
</div>
|
||||
<div style={{ maxHeight: 360, overflowY: 'auto' }}>
|
||||
{alerts.map((a, i) => (
|
||||
<div key={i} style={{ display: 'grid', gridTemplateColumns: '4px 1fr auto', gap: 12, alignItems: 'flex-start', padding: '10px 14px', borderBottom: '1px solid var(--hairline)' }}>
|
||||
<div style={{ width: 4, alignSelf: 'stretch', background: a.color }}></div>
|
||||
<div>
|
||||
<div style={{ fontFamily: 'var(--font-display)', fontSize: 13, fontWeight: 500 }}>{a.msg}</div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--ink-3)', marginTop: 2 }}>{a.detail}</div>
|
||||
</div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--ink-4)' }}>{a.time}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
window.AlertFeed = AlertFeed;
|
||||
@@ -0,0 +1,42 @@
|
||||
function EventsTable({ events, onSelect }) {
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-head">
|
||||
<h3>Events</h3>
|
||||
<div style={{ marginLeft: 'auto', display: 'flex', gap: 8 }}>
|
||||
<button className="btn ghost"><i data-lucide="filter"></i>Filter</button>
|
||||
<button className="btn primary"><i data-lucide="plus"></i>New event</button>
|
||||
</div>
|
||||
</div>
|
||||
<table className="lb">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: 30 }}></th>
|
||||
<th>EVENT</th>
|
||||
<th>DATE</th>
|
||||
<th>SPORT</th>
|
||||
<th className="num">STARTERS</th>
|
||||
<th className="num">WAVES</th>
|
||||
<th>STATUS</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{events.map(e => (
|
||||
<tr key={e.id} onClick={() => onSelect && onSelect(e)} style={{ cursor: 'pointer' }}>
|
||||
<td><i data-lucide={e.icon} style={{ width: 14, height: 14, color: 'var(--ink-3)' }}></i></td>
|
||||
<td className="name">{e.name}</td>
|
||||
<td>{e.date}</td>
|
||||
<td>{e.sport}</td>
|
||||
<td className="num">{e.starters.toLocaleString()}</td>
|
||||
<td className="num">{e.waves}</td>
|
||||
<td><span className={'pill ' + e.statusClass}><span className="dot"></span>{e.status}</span></td>
|
||||
<td><i data-lucide="chevron-right" style={{ width: 14, height: 14, color: 'var(--ink-4)' }}></i></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
window.EventsTable = EventsTable;
|
||||
@@ -0,0 +1,16 @@
|
||||
function KpiStrip({ items }) {
|
||||
return (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: `repeat(${items.length}, 1fr)`, gap: 0, border: '1px solid var(--ink)', background: 'var(--paper)' }}>
|
||||
{items.map((it, i) => (
|
||||
<div key={i} className="kpi" style={{ borderRight: i < items.length - 1 ? '1px solid var(--hairline)' : 'none' }}>
|
||||
<div className="lbl">{it.label}</div>
|
||||
<div className="val">{it.value}</div>
|
||||
<div className={'delta ' + (it.dir || '')} style={{ color: it.dir === 'up' ? 'var(--green)' : it.dir === 'dn' ? 'var(--flag)' : 'var(--ink-4)' }}>
|
||||
{it.delta}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
window.KpiStrip = KpiStrip;
|
||||
@@ -0,0 +1,17 @@
|
||||
# Dashboard UI Kit — TRM
|
||||
|
||||
Race director / timing official dashboard. Primary surface for organizing events, managing waves and bibs, and running race control on race day.
|
||||
|
||||
## Screens covered
|
||||
- **Events overview** — list of upcoming/live/past events with KPI strip
|
||||
- **Race control** — live event view with leaderboard, mat status, and alert feed
|
||||
|
||||
## Components
|
||||
- `TopBar.jsx` — sticky header with brand, search, clock, user
|
||||
- `SideNav.jsx` — primary navigation
|
||||
- `EventsTable.jsx` — events list with status pills
|
||||
- `KpiStrip.jsx` — 4-up KPI cards
|
||||
- `RaceControl.jsx` — leaderboard + alert feed + mat status
|
||||
- `AlertFeed.jsx` — race-control alerts
|
||||
|
||||
Open `index.html` to interact (click between screens).
|
||||
@@ -0,0 +1,58 @@
|
||||
function RaceControl({ leaders, alerts, mats }) {
|
||||
return (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1.4fr 1fr', gap: 16 }}>
|
||||
<div className="card">
|
||||
<div className="card-head">
|
||||
<h3>Live leaderboard · Coastline 10K</h3>
|
||||
<span className="pill live" style={{ marginLeft: 'auto' }}><span className="dot"></span>LIVE</span>
|
||||
<span style={{ fontFamily: 'var(--font-mono)', fontVariantNumeric: 'tabular-nums', fontSize: 14, fontWeight: 600 }}>00:42:18.4</span>
|
||||
</div>
|
||||
<table className="lb">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: 36 }}>POS</th>
|
||||
<th style={{ width: 50 }}>BIB</th>
|
||||
<th>NAME</th>
|
||||
<th>WAVE</th>
|
||||
<th className="num">SPLIT 3</th>
|
||||
<th className="num">GAP</th>
|
||||
<th className="num">PACE</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{leaders.map(l => (
|
||||
<tr key={l.pos}>
|
||||
<td style={{ fontWeight: 700, color: l.pos === 1 ? 'var(--flag)' : 'var(--ink)' }}>{l.pos}</td>
|
||||
<td>{l.bib}</td>
|
||||
<td className="name">{l.name}</td>
|
||||
<td>{l.wave}</td>
|
||||
<td className="num">{l.split}</td>
|
||||
<td className={'num ' + (l.gap === '—' ? '' : 'loss')}>{l.gap}</td>
|
||||
<td className="num">{l.pace}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<div className="card">
|
||||
<div className="card-head"><h3>Timing mats</h3></div>
|
||||
<div style={{ padding: 14, display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 8 }}>
|
||||
{mats.map(m => (
|
||||
<div key={m.id} style={{ border: '1px solid var(--hairline)', padding: '8px 10px', display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<span className="dot" style={{ width: 8, height: 8, borderRadius: 999, background: m.online ? 'var(--green)' : 'var(--flag)', display: 'inline-block' }}></span>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontFamily: 'var(--font-display)', fontSize: 12, fontWeight: 600 }}>{m.name}</div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--ink-4)' }}>{m.id} · {m.km} km</div>
|
||||
</div>
|
||||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--ink-3)' }}>{m.reads}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<AlertFeed alerts={alerts} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
window.RaceControl = RaceControl;
|
||||
@@ -0,0 +1,39 @@
|
||||
function SideNav({ active, onNav }) {
|
||||
const items = [
|
||||
{ id: 'events', icon: 'calendar', label: 'Events', badge: '12' },
|
||||
{ id: 'live', icon: 'radio', label: 'Race control', badge: '1' },
|
||||
{ id: 'results', icon: 'trophy', label: 'Results' },
|
||||
{ id: 'participants', icon: 'users', label: 'Participants' },
|
||||
{ id: 'mats', icon: 'route', label: 'Timing mats' },
|
||||
{ id: 'reports', icon: 'bar-chart-3', label: 'Reports' },
|
||||
];
|
||||
const settings = [
|
||||
{ id: 'team', icon: 'user-cog', label: 'Team' },
|
||||
{ id: 'billing', icon: 'credit-card', label: 'Billing' },
|
||||
{ id: 'settings', icon: 'settings', label: 'Settings' },
|
||||
];
|
||||
return (
|
||||
<aside className="kit-side">
|
||||
<div className="kit-side-section">Operations</div>
|
||||
{items.map(it => (
|
||||
<div key={it.id} className={'kit-nav-item' + (active === it.id ? ' active' : '')} onClick={() => onNav(it.id)}>
|
||||
<i data-lucide={it.icon}></i>
|
||||
<span>{it.label}</span>
|
||||
{it.badge && <span className="badge">{it.badge}</span>}
|
||||
</div>
|
||||
))}
|
||||
<div className="kit-side-section">Account</div>
|
||||
{settings.map(it => (
|
||||
<div key={it.id} className={'kit-nav-item' + (active === it.id ? ' active' : '')} onClick={() => onNav(it.id)}>
|
||||
<i data-lucide={it.icon}></i>
|
||||
<span>{it.label}</span>
|
||||
</div>
|
||||
))}
|
||||
<div style={{ flex: 1 }}></div>
|
||||
<div style={{ padding: 14, borderTop: '1px solid var(--hairline)', fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--ink-4)', letterSpacing: '0.06em', textTransform: 'uppercase' }}>
|
||||
v3.18.0 · build 2741
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
window.SideNav = SideNav;
|
||||
@@ -0,0 +1,22 @@
|
||||
// TopBar — sticky header for the dashboard.
|
||||
function TopBar({ clock }) {
|
||||
return (
|
||||
<header className="kit-top">
|
||||
<div className="kit-brand">
|
||||
<img src="../../assets/logo-trm.png" alt="TRM" />
|
||||
<span className="b-meta">Dashboard</span>
|
||||
</div>
|
||||
<div className="kit-search">
|
||||
<i data-lucide="search" style={{ width: 14, height: 14, color: 'var(--ink-3)' }}></i>
|
||||
<input placeholder="Search events, bibs, athletes…" />
|
||||
</div>
|
||||
<div className="kit-top-actions">
|
||||
<span className="pill live"><span className="dot"></span>LIVE · 1 EVENT</span>
|
||||
<span className="kit-clk">{clock}</span>
|
||||
<i data-lucide="bell" style={{ width: 18, height: 18 }}></i>
|
||||
<div style={{ width: 28, height: 28, border: '1px solid var(--ink)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 600 }}>JR</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
window.TopBar = TopBar;
|
||||
@@ -0,0 +1,128 @@
|
||||
<!doctype html>
|
||||
<html><head><meta charset="utf-8">
|
||||
<title>TRM Dashboard · UI Kit</title>
|
||||
<link rel="stylesheet" href="../../colors_and_type.css">
|
||||
<link rel="stylesheet" href="../kit.css">
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel" src="TopBar.jsx"></script>
|
||||
<script type="text/babel" src="SideNav.jsx"></script>
|
||||
<script type="text/babel" src="KpiStrip.jsx"></script>
|
||||
<script type="text/babel" src="EventsTable.jsx"></script>
|
||||
<script type="text/babel" src="AlertFeed.jsx"></script>
|
||||
<script type="text/babel" src="RaceControl.jsx"></script>
|
||||
<script type="text/babel">
|
||||
const { useState, useEffect } = React;
|
||||
|
||||
const EVENTS = [
|
||||
{ id: 1, name: 'Coastline 10K · Spring 2026', date: 'Apr 27, 09:00', sport: 'Run', starters: 1247, waves: 4, status: 'LIVE', statusClass: 'live', icon: 'radio' },
|
||||
{ id: 2, name: 'Granite Crit Series · Round 3', date: 'May 03, 18:30', sport: 'Cycling', starters: 184, waves: 6, status: 'SCHEDULED', statusClass: 'info', icon: 'calendar' },
|
||||
{ id: 3, name: 'Twin Peaks Triathlon', date: 'May 11, 06:30', sport: 'Triathlon', starters: 612, waves: 8, status: 'SCHEDULED', statusClass: 'info', icon: 'calendar' },
|
||||
{ id: 4, name: 'Harbor Sprint Regatta', date: 'May 18, 11:00', sport: 'Sailing', starters: 88, waves: 3, status: 'DRAFT', statusClass: 'neutral', icon: 'edit' },
|
||||
{ id: 5, name: 'Northshore Marathon', date: 'Apr 06, 07:30', sport: 'Run', starters: 3402, waves: 5, status: 'FINISHED', statusClass: 'ok', icon: 'check' },
|
||||
{ id: 6, name: 'Iron Sands Ultra 100', date: 'Mar 22, 05:00', sport: 'Ultra', starters: 287, waves: 1, status: 'FINISHED', statusClass: 'ok', icon: 'check' },
|
||||
];
|
||||
|
||||
const LEADERS = [
|
||||
{ pos: 1, bib: 247, name: 'Maya Chen', wave: 'Elite', split: '00:42:18.4', gap: '—', pace: '04:13' },
|
||||
{ pos: 2, bib: 188, name: 'Ravi Park', wave: 'Elite', split: '00:42:22.6', gap: '+0:04.2', pace: '04:14' },
|
||||
{ pos: 3, bib: 44, name: 'Noemi Vega', wave: 'Elite', split: '00:42:30.1', gap: '+0:11.7', pace: '04:15' },
|
||||
{ pos: 4, bib: 302, name: 'Tomás Riera', wave: 'Elite', split: '00:42:55.0', gap: '+0:36.6', pace: '04:18' },
|
||||
{ pos: 5, bib: 119, name: 'Anika Joshi', wave: 'Elite', split: '00:43:01.2', gap: '+0:42.8', pace: '04:19' },
|
||||
{ pos: 6, bib: 401, name: 'Felix Okafor', wave: 'Open', split: '00:43:08.7', gap: '+0:50.3', pace: '04:19' },
|
||||
{ pos: 7, bib: 77, name: 'Sara Lindqvist',wave:'Open', split: '00:43:14.0', gap: '+0:55.6', pace: '04:20' },
|
||||
{ pos: 8, bib: 215, name: 'David Müller', wave: 'Open', split: '00:43:22.4', gap: '+1:04.0', pace: '04:21' },
|
||||
];
|
||||
|
||||
const ALERTS = [
|
||||
{ msg: 'Bib 247 missed split 3', detail: 'Flagging for manual review', time: '14:02:11', color: 'var(--flag)' },
|
||||
{ msg: 'Yellow flag · sector 2', detail: 'Debris cleared in 90s', time: '14:00:42', color: 'var(--amber)' },
|
||||
{ msg: 'Wave 4 cleared start mat', detail: '412 / 412 starters', time: '13:45:00', color: 'var(--green)' },
|
||||
{ msg: 'Tracking active', detail: '14 of 14 mats online', time: '13:30:00', color: 'var(--blue)' },
|
||||
{ msg: 'Bib 088 retired', detail: 'Marked DNF · medical', time: '13:24:18', color: 'var(--ink-3)' },
|
||||
];
|
||||
|
||||
const MATS = [
|
||||
{ id: 'M-01', name: 'Start', km: 0, online: true, reads: '1,247' },
|
||||
{ id: 'M-02', name: 'Split 1', km: 2.5, online: true, reads: '1,201' },
|
||||
{ id: 'M-03', name: 'Split 2', km: 5.0, online: true, reads: '1,184' },
|
||||
{ id: 'M-04', name: 'Split 3', km: 7.5, online: true, reads: '892' },
|
||||
{ id: 'M-05', name: 'Finish', km: 10, online: true, reads: '0' },
|
||||
{ id: 'M-06', name: 'Backup', km: 10, online: false, reads: '—' },
|
||||
];
|
||||
|
||||
function App() {
|
||||
const [active, setActive] = useState('events');
|
||||
const [selectedEvent, setSelectedEvent] = useState(null);
|
||||
const [clock, setClock] = useState('14:02:11');
|
||||
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => {
|
||||
const d = new Date();
|
||||
setClock(d.toTimeString().slice(0,8));
|
||||
}, 1000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
useEffect(() => { lucide.createIcons(); });
|
||||
|
||||
const showRaceControl = active === 'live' || (selectedEvent && selectedEvent.statusClass === 'live');
|
||||
|
||||
return (
|
||||
<div className="kit-app">
|
||||
<TopBar clock={clock} />
|
||||
<div className="kit-main">
|
||||
<SideNav active={showRaceControl ? 'live' : active} onNav={(id) => { setActive(id); setSelectedEvent(null); }} />
|
||||
<main className="kit-content">
|
||||
{showRaceControl ? (
|
||||
<>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 14, marginBottom: 18 }}>
|
||||
<button className="btn ghost" onClick={() => { setActive('events'); setSelectedEvent(null); }} style={{ padding: 0, height: 'auto', boxShadow: 'none', border: 0, color: 'var(--ink-3)', fontSize: 12 }}>← All events</button>
|
||||
<h1 style={{ fontSize: 28, margin: 0 }}>Coastline 10K · Spring 2026</h1>
|
||||
<span className="pill live"><span className="dot"></span>LIVE</span>
|
||||
<div style={{ marginLeft: 'auto', display: 'flex', gap: 8 }}>
|
||||
<button className="btn"><i data-lucide="download"></i>Export</button>
|
||||
<button className="btn flag"><i data-lucide="flag"></i>Issue red flag</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<KpiStrip items={[
|
||||
{ label: 'Active bibs', value: '1,247', delta: '↑ 12 in last 5 min', dir: 'up' },
|
||||
{ label: 'Lead time', value: '42:18.4', delta: 'Bib 247 · split 3' },
|
||||
{ label: 'Avg pace', value: '4:23/km', delta: '↓ 0:06 vs target', dir: 'dn' },
|
||||
{ label: 'Mats online', value: '14 / 14', delta: '100% uptime', dir: 'up' },
|
||||
]} />
|
||||
</div>
|
||||
<RaceControl leaders={LEADERS} alerts={ALERTS} mats={MATS} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 14, marginBottom: 18 }}>
|
||||
<h1 style={{ fontSize: 32, margin: 0 }}>Events</h1>
|
||||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--ink-4)' }}>12 total · 1 live · 3 scheduled</span>
|
||||
</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<KpiStrip items={[
|
||||
{ label: 'Events this season', value: '12', delta: '↑ 3 vs last season', dir: 'up' },
|
||||
{ label: 'Total starters', value: '5,820', delta: '↑ 18% YoY', dir: 'up' },
|
||||
{ label: 'Avg finish rate', value: '94.2%', delta: '↑ 1.4 pts', dir: 'up' },
|
||||
{ label: 'Outstanding reviews',value: '7', delta: '3 high priority' },
|
||||
]} />
|
||||
</div>
|
||||
<EventsTable events={EVENTS} onSelect={setSelectedEvent} />
|
||||
</>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
|
||||
</script>
|
||||
</body></html>
|
||||
@@ -0,0 +1,67 @@
|
||||
/* Shared chrome for all UI kits — top bar / sidebar / table tokens */
|
||||
:root { --top-h: 56px; --side-w: 240px; }
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; min-height: 100vh; background: var(--paper); color: var(--ink); font-family: var(--font-body); }
|
||||
|
||||
.kit-app { display: grid; grid-template-rows: var(--top-h) 1fr; min-height: 100vh; }
|
||||
.kit-top {
|
||||
display: grid; grid-template-columns: var(--side-w) 1fr auto; align-items: center;
|
||||
border-bottom: 1px solid var(--ink); background: var(--paper);
|
||||
position: sticky; top: 0; z-index: 5;
|
||||
}
|
||||
.kit-brand { display: flex; align-items: center; gap: 10px; padding: 0 18px; height: 100%; border-right: 1px solid var(--ink); }
|
||||
.kit-brand img { height: 22px; width: auto; }
|
||||
.kit-brand .b-meta { font-family: var(--font-mono); font-size: 10px; color: var(--ink-3); letter-spacing: 0.08em; text-transform: uppercase; }
|
||||
.kit-search { display: flex; align-items: center; gap: 10px; padding: 0 18px; height: 100%; }
|
||||
.kit-search input { border: 1px solid var(--ink); height: 32px; width: 320px; padding: 0 10px; font-family: var(--font-mono); font-size: 13px; background: var(--paper-2); border-radius: 2px; }
|
||||
.kit-top-actions { display: flex; align-items: center; gap: 14px; padding: 0 18px; height: 100%; border-left: 1px solid var(--hairline); }
|
||||
.kit-clk { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 14px; font-weight: 600; }
|
||||
|
||||
.kit-main { display: grid; grid-template-columns: var(--side-w) 1fr; min-height: 0; }
|
||||
.kit-side { border-right: 1px solid var(--ink); background: var(--paper); display: flex; flex-direction: column; }
|
||||
.kit-side-section { padding: 16px 14px 6px; font-family: var(--font-display); font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.10em; color: var(--ink-4); }
|
||||
.kit-nav-item { display: flex; align-items: center; gap: 10px; padding: 0 14px; height: 36px; font-family: var(--font-display); font-size: 13px; color: var(--ink-2); cursor: pointer; border-left: 2px solid transparent; }
|
||||
.kit-nav-item:hover { background: var(--paper-2); }
|
||||
.kit-nav-item.active { background: var(--paper-2); border-left-color: var(--flag); color: var(--ink); font-weight: 600; }
|
||||
.kit-nav-item [data-lucide] { width: 16px; height: 16px; stroke-width: 2; }
|
||||
.kit-nav-item .badge { margin-left: auto; font-family: var(--font-mono); font-size: 10px; color: var(--ink-3); }
|
||||
|
||||
.kit-content { padding: 22px 28px; min-width: 0; overflow: hidden; }
|
||||
|
||||
.btn { font-family: var(--font-display); font-weight: 600; font-size: 13px; border: 1px solid var(--ink); background: var(--paper); color: var(--ink); padding: 0 14px; height: 32px; cursor: pointer; display: inline-flex; align-items: center; gap: 8px; box-shadow: 2px 2px 0 0 var(--ink); border-radius: 0; transition: all 120ms var(--ease-snap); }
|
||||
.btn:hover { box-shadow: 3px 3px 0 0 var(--ink); transform: translate(-1px,-1px); }
|
||||
.btn:active { box-shadow: 0 0 0 0 var(--ink); transform: translate(2px,2px); }
|
||||
.btn.primary { background: var(--ink); color: var(--paper); }
|
||||
.btn.flag { background: var(--flag); color: #fff; }
|
||||
.btn.ghost { box-shadow: none; }
|
||||
.btn [data-lucide] { width: 14px; height: 14px; }
|
||||
|
||||
.pill { display:inline-flex; align-items:center; gap:6px; padding: 3px 10px; font-family: var(--font-mono); font-size: 10px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; border-radius: 999px; border: 1px solid currentColor; }
|
||||
.pill .dot { width:6px; height:6px; border-radius:999px; background: currentColor; }
|
||||
.pill.live { color: var(--flag); }
|
||||
.pill.live .dot { animation: pulse 2s infinite ease-out; }
|
||||
.pill.ok { color: var(--green); }
|
||||
.pill.warn { color: var(--amber-2); }
|
||||
.pill.danger { color: var(--flag-2); }
|
||||
.pill.info { color: var(--blue-2); }
|
||||
.pill.neutral { color: var(--ink-3); }
|
||||
@keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(232,65,43,0.6); } 100% { box-shadow: 0 0 0 8px rgba(232,65,43,0); } }
|
||||
|
||||
table.lb { width: 100%; border-collapse: collapse; border: 1px solid var(--ink); }
|
||||
table.lb th, table.lb td { text-align: left; padding: 0 10px; height: 28px; font-family: var(--font-mono); font-size: 12px; border-bottom: 1px solid var(--hairline); }
|
||||
table.lb th { background: var(--paper-2); font-weight: 600; font-size: 10px; letter-spacing: 0.10em; text-transform: uppercase; color: var(--ink-3); border-bottom: 1px solid var(--ink); }
|
||||
table.lb tr:hover td { background: var(--paper-2); }
|
||||
table.lb td.num { text-align: right; font-variant-numeric: tabular-nums; }
|
||||
table.lb td.name { font-family: var(--font-display); font-weight: 500; font-size: 13px; }
|
||||
.gain { color: var(--green); }
|
||||
.loss { color: var(--flag); }
|
||||
|
||||
.card { border: 1px solid var(--ink); background: var(--paper); }
|
||||
.card-head { display: flex; align-items: center; gap: 10px; padding: 10px 14px; border-bottom: 1px solid var(--ink); background: var(--paper-2); }
|
||||
.card-head h3 { font-family: var(--font-display); font-size: 13px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; margin: 0; }
|
||||
.card-body { padding: 14px; }
|
||||
.kpi { padding: 14px; }
|
||||
.kpi .lbl { font-family: var(--font-display); font-size: 10px; font-weight: 600; letter-spacing: 0.10em; text-transform: uppercase; color: var(--ink-3); }
|
||||
.kpi .val { font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 32px; font-weight: 600; letter-spacing: -0.02em; line-height: 1; margin-top: 8px; }
|
||||
.kpi .delta { font-family: var(--font-mono); font-size: 11px; margin-top: 6px; }
|
||||
@@ -0,0 +1,22 @@
|
||||
function HeroClock({ time, eventName, sport, distance, weather }) {
|
||||
return (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr auto', alignItems: 'center', padding: '24px 32px', borderBottom: '1px solid var(--night-line)' }}>
|
||||
<div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
|
||||
<img src="../../assets/logo-trm.png" alt="TRM" style={{ height: 26, filter: 'invert(1) brightness(1.1)' }} />
|
||||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--night-fg-3)', letterSpacing: '0.10em', textTransform: 'uppercase' }}>Live tracking</span>
|
||||
<span className="pill live"><span className="dot"></span>LIVE</span>
|
||||
</div>
|
||||
<div style={{ fontFamily: 'var(--font-display)', fontSize: 32, fontWeight: 700, marginTop: 14, letterSpacing: '-0.02em' }}>{eventName}</div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 13, color: 'var(--night-fg-2)', marginTop: 8, letterSpacing: '0.04em' }}>
|
||||
{sport} · {distance} · {weather}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--night-fg-3)', letterSpacing: '0.10em', textTransform: 'uppercase' }}>Race clock</div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontVariantNumeric: 'tabular-nums', fontSize: 96, fontWeight: 600, letterSpacing: '-0.04em', lineHeight: 1, marginTop: 6 }}>{time}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
window.HeroClock = HeroClock;
|
||||
@@ -0,0 +1,33 @@
|
||||
function LiveTable({ rows }) {
|
||||
return (
|
||||
<table className="lb night">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: 50 }}>POS</th>
|
||||
<th style={{ width: 60 }}>BIB</th>
|
||||
<th>NAME</th>
|
||||
<th>WAVE</th>
|
||||
<th className="num">SPLIT 3</th>
|
||||
<th className="num">GAP TO LEAD</th>
|
||||
<th className="num">PACE</th>
|
||||
<th className="num">LAST</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map(r => (
|
||||
<tr key={r.pos}>
|
||||
<td style={{ fontWeight: 700, color: r.pos === 1 ? 'var(--flag)' : 'var(--night-fg)', fontSize: 14 }}>{r.pos}</td>
|
||||
<td>{r.bib}</td>
|
||||
<td className="name" style={{ color: 'var(--night-fg)' }}>{r.name}</td>
|
||||
<td style={{ color: 'var(--night-fg-2)' }}>{r.wave}</td>
|
||||
<td className="num">{r.split}</td>
|
||||
<td className={'num ' + (r.gap === '—' ? '' : 'loss')}>{r.gap}</td>
|
||||
<td className="num">{r.pace}</td>
|
||||
<td className="num" style={{ color: 'var(--night-fg-3)' }}>{r.last}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
window.LiveTable = LiveTable;
|
||||
@@ -0,0 +1,24 @@
|
||||
function MiniMap({ checkpoints }) {
|
||||
return (
|
||||
<div className="card" style={{ background: 'var(--night-2)', borderColor: 'var(--night-line)', color: 'var(--night-fg)' }}>
|
||||
<div className="card-head" style={{ background: 'var(--night-3)', borderColor: 'var(--night-line)' }}>
|
||||
<h3 style={{ color: 'var(--night-fg)' }}>Course schematic</h3>
|
||||
<span style={{ marginLeft: 'auto', fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--night-fg-3)' }}>10.0 km · 6 mats</span>
|
||||
</div>
|
||||
<div style={{ padding: 24, position: 'relative' }}>
|
||||
<svg viewBox="0 0 600 160" style={{ width: '100%', height: 'auto' }}>
|
||||
<path d="M 30 120 Q 120 30, 220 80 T 420 70 T 570 110" fill="none" stroke="var(--night-line)" strokeWidth="2" strokeDasharray="4 4" />
|
||||
<path d="M 30 120 Q 120 30, 220 80 T 420 70" fill="none" stroke="var(--flag)" strokeWidth="3" />
|
||||
{checkpoints.map((cp, i) => (
|
||||
<g key={i}>
|
||||
<circle cx={cp.x} cy={cp.y} r="6" fill={cp.passed ? 'var(--flag)' : 'var(--night-2)'} stroke={cp.passed ? 'var(--flag)' : 'var(--night-fg-3)'} strokeWidth="2" />
|
||||
<text x={cp.x} y={cp.y - 14} textAnchor="middle" fontFamily="JetBrains Mono" fontSize="10" fill="var(--night-fg-2)">{cp.label}</text>
|
||||
<text x={cp.x} y={cp.y + 22} textAnchor="middle" fontFamily="JetBrains Mono" fontSize="9" fill="var(--night-fg-3)">{cp.km} km</text>
|
||||
</g>
|
||||
))}
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
window.MiniMap = MiniMap;
|
||||
@@ -0,0 +1,12 @@
|
||||
# Leaderboard UI Kit — TRM
|
||||
|
||||
Live spectator/broadcast leaderboard view. Optimized for night-mode kiosk and trackside displays. High-density, high-contrast, glanceable.
|
||||
|
||||
## Screens
|
||||
- **Live leaderboard** with hero clock, race meta, full table, mini-map, last-passings ticker.
|
||||
|
||||
## Components
|
||||
- `HeroClock.jsx` — jumbo race clock + meta strip
|
||||
- `LiveTable.jsx` — full-width leaderboard
|
||||
- `MiniMap.jsx` — schematic course with checkpoint dots
|
||||
- `Ticker.jsx` — last passings strip across the bottom
|
||||
@@ -0,0 +1,20 @@
|
||||
function Ticker({ items }) {
|
||||
return (
|
||||
<div style={{ borderTop: '1px solid var(--night-line)', background: 'var(--night-2)', padding: '10px 0', overflow: 'hidden', whiteSpace: 'nowrap' }}>
|
||||
<div style={{ display: 'inline-flex', gap: 32, animation: 'tickerScroll 40s linear infinite', paddingLeft: '100%' }}>
|
||||
{[...items, ...items].map((it, i) => (
|
||||
<span key={i} style={{ fontFamily: 'var(--font-mono)', fontSize: 13, color: 'var(--night-fg-2)', display: 'inline-flex', gap: 8, alignItems: 'center' }}>
|
||||
<span style={{ color: 'var(--flag)' }}>▸</span>
|
||||
<span style={{ color: 'var(--night-fg-3)' }}>{it.cp}</span>
|
||||
<span style={{ color: 'var(--night-fg)', fontWeight: 600 }}>BIB {it.bib}</span>
|
||||
<span style={{ color: 'var(--night-fg)' }}>{it.name}</span>
|
||||
<span style={{ color: 'var(--night-fg-3)' }}>{it.time}</span>
|
||||
<span style={{ color: it.delta.startsWith('-') ? 'var(--green)' : 'var(--flag)' }}>{it.delta}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<style>{`@keyframes tickerScroll { from { transform: translateX(0); } to { transform: translateX(-50%); } }`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
window.Ticker = Ticker;
|
||||
@@ -0,0 +1,96 @@
|
||||
<!doctype html>
|
||||
<html><head><meta charset="utf-8">
|
||||
<title>TRM Live Leaderboard</title>
|
||||
<link rel="stylesheet" href="../../colors_and_type.css">
|
||||
<link rel="stylesheet" href="../kit.css">
|
||||
<style>
|
||||
body { background: var(--night); color: var(--night-fg); }
|
||||
.pill.live { color: var(--flag); border-color: var(--flag); }
|
||||
table.lb.night { border-color: var(--night-line); }
|
||||
table.lb.night th { background: var(--night-3); color: var(--night-fg-3); border-bottom-color: var(--night-line); }
|
||||
table.lb.night td { border-bottom-color: var(--night-line); color: var(--night-fg); font-size: 14px; height: 36px; }
|
||||
table.lb.night tr:hover td { background: var(--night-3); }
|
||||
.loss { color: #ff8a78 !important; }
|
||||
.card { background: var(--night-2); border-color: var(--night-line); color: var(--night-fg); }
|
||||
.card-head { background: var(--night-3); border-bottom-color: var(--night-line); }
|
||||
.card-head h3 { color: var(--night-fg); }
|
||||
</style>
|
||||
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel" src="HeroClock.jsx"></script>
|
||||
<script type="text/babel" src="LiveTable.jsx"></script>
|
||||
<script type="text/babel" src="MiniMap.jsx"></script>
|
||||
<script type="text/babel" src="Ticker.jsx"></script>
|
||||
<script type="text/babel">
|
||||
const { useState, useEffect } = React;
|
||||
|
||||
const ROWS = [
|
||||
{ pos: 1, bib: 247, name: 'Maya Chen', wave: 'Elite', split: '00:42:18.4', gap: '—', pace: '04:13', last: '0:18 ago' },
|
||||
{ pos: 2, bib: 188, name: 'Ravi Park', wave: 'Elite', split: '00:42:22.6', gap: '+0:04.2', pace: '04:14', last: '0:22 ago' },
|
||||
{ pos: 3, bib: 44, name: 'Noemi Vega', wave: 'Elite', split: '00:42:30.1', gap: '+0:11.7', pace: '04:15', last: '0:30 ago' },
|
||||
{ pos: 4, bib: 302, name: 'Tomás Riera', wave: 'Elite', split: '00:42:55.0', gap: '+0:36.6', pace: '04:18', last: '0:55 ago' },
|
||||
{ pos: 5, bib: 119, name: 'Anika Joshi', wave: 'Elite', split: '00:43:01.2', gap: '+0:42.8', pace: '04:19', last: '1:01 ago' },
|
||||
{ pos: 6, bib: 401, name: 'Felix Okafor', wave: 'Open', split: '00:43:08.7', gap: '+0:50.3', pace: '04:19', last: '1:08 ago' },
|
||||
{ pos: 7, bib: 77, name: 'Sara Lindqvist',wave: 'Open', split: '00:43:14.0', gap: '+0:55.6', pace: '04:20', last: '1:14 ago' },
|
||||
{ pos: 8, bib: 215, name: 'David Müller', wave: 'Open', split: '00:43:22.4', gap: '+1:04.0', pace: '04:21', last: '1:22 ago' },
|
||||
{ pos: 9, bib: 503, name: 'Inés Báez', wave: 'Open', split: '00:43:30.6', gap: '+1:12.2', pace: '04:21', last: '1:30 ago' },
|
||||
{ pos:10, bib: 162, name: 'Kenji Sato', wave: 'Open', split: '00:43:38.0', gap: '+1:19.6', pace: '04:22', last: '1:38 ago' },
|
||||
];
|
||||
|
||||
const CHECKPOINTS = [
|
||||
{ label: 'START', km: 0, x: 30, y: 120, passed: true },
|
||||
{ label: 'CP1', km: 2.5, x: 150, y: 60, passed: true },
|
||||
{ label: 'CP2', km: 5.0, x: 270, y: 80, passed: true },
|
||||
{ label: 'CP3', km: 7.5, x: 380, y: 70, passed: true },
|
||||
{ label: 'CP4', km: 9.0, x: 480, y: 90, passed: false },
|
||||
{ label: 'FINISH', km: 10, x: 570, y: 110, passed: false },
|
||||
];
|
||||
|
||||
const TICKER = [
|
||||
{ cp: 'CP3', bib: 247, name: 'M. Chen', time: '00:42:18.4', delta: '-0:01.6' },
|
||||
{ cp: 'CP3', bib: 188, name: 'R. Park', time: '00:42:22.6', delta: '+0:00.4' },
|
||||
{ cp: 'CP3', bib: 44, name: 'N. Vega', time: '00:42:30.1', delta: '+0:01.9' },
|
||||
{ cp: 'CP3', bib: 302, name: 'T. Riera', time: '00:42:55.0', delta: '+0:04.4' },
|
||||
{ cp: 'CP3', bib: 119, name: 'A. Joshi', time: '00:43:01.2', delta: '-0:02.1' },
|
||||
{ cp: 'CP3', bib: 401, name: 'F. Okafor', time: '00:43:08.7', delta: '+0:00.6' },
|
||||
];
|
||||
|
||||
function App() {
|
||||
const [t, setT] = useState({ h: 1, m: 23, s: 45, ms: 6 });
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => {
|
||||
setT(p => {
|
||||
let ms = p.ms + 1;
|
||||
let s = p.s, m = p.m, h = p.h;
|
||||
if (ms > 9) { ms = 0; s++; }
|
||||
if (s > 59) { s = 0; m++; }
|
||||
if (m > 59) { m = 0; h++; }
|
||||
return { h, m, s, ms };
|
||||
});
|
||||
}, 100);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
const pad = n => String(n).padStart(2, '0');
|
||||
const time = `${pad(t.h)}:${pad(t.m)}:${pad(t.s)}.${t.ms}`;
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
|
||||
<HeroClock time={time} eventName="Coastline 10K · Spring 2026" sport="Run · 10K" distance="Coastal route, flat" weather="18°C · light SW · dry" />
|
||||
<div style={{ flex: 1, padding: '24px 32px', display: 'grid', gridTemplateColumns: '2fr 1fr', gap: 24, minHeight: 0 }}>
|
||||
<div style={{ minHeight: 0, overflow: 'hidden' }}>
|
||||
<LiveTable rows={ROWS} />
|
||||
</div>
|
||||
<MiniMap checkpoints={CHECKPOINTS} />
|
||||
</div>
|
||||
<Ticker items={TICKER} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
|
||||
</script>
|
||||
</body></html>
|
||||
@@ -0,0 +1,28 @@
|
||||
function FeatureGrid() {
|
||||
const features = [
|
||||
{ title: 'Chip-time accuracy', body: 'Sub-100ms read precision across UHF and BLE timing mats. One mat, one truth.', stat: '<100ms' },
|
||||
{ title: 'Any sport', body: 'Run, ride, swim, paddle, sail, ski, drive. Configure laps, splits, transitions per discipline.', stat: '14 sports' },
|
||||
{ title: 'Live everywhere', body: 'Embed leaderboards anywhere. Push splits to athletes and spectators in real time.', stat: 'Real-time' },
|
||||
{ title: 'Race control', body: 'Yellow flag, red flag, manual overrides, DSQ workflows. Race directors stay in command.', stat: 'In-app' },
|
||||
{ title: 'Results in minutes', body: 'Auto-generated category results, exportable to USATF / UCI / FFC formats.', stat: '<2 min' },
|
||||
{ title: 'Athlete companion', body: 'Free mobile app for participants and supporters. Branded for your event, no extra cost.', stat: 'iOS · Android' },
|
||||
];
|
||||
return (
|
||||
<section style={{ borderBottom: '1px solid var(--ink)' }}>
|
||||
<div style={{ padding: '64px 32px 32px' }}>
|
||||
<div className="overline">What's in TRM</div>
|
||||
<h2 style={{ fontSize: 44, margin: '12px 0 0', maxWidth: 720, letterSpacing: '-0.02em', lineHeight: 1.05 }}>The whole stack — from start mat to certificate.</h2>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', borderTop: '1px solid var(--ink)' }}>
|
||||
{features.map((f, i) => (
|
||||
<div key={i} style={{ padding: 28, borderRight: i % 3 < 2 ? '1px solid var(--ink)' : 'none', borderBottom: i < 3 ? '1px solid var(--ink)' : 'none' }}>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 32, fontWeight: 700, letterSpacing: '-0.02em', color: 'var(--flag)' }}>{f.stat}</div>
|
||||
<h3 style={{ fontSize: 20, margin: '16px 0 8px', fontFamily: 'var(--font-display)', fontWeight: 600 }}>{f.title}</h3>
|
||||
<p style={{ fontFamily: 'var(--font-body)', fontSize: 14, lineHeight: 1.55, color: 'var(--ink-2)', margin: 0 }}>{f.body}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
window.FeatureGrid = FeatureGrid;
|
||||
@@ -0,0 +1,14 @@
|
||||
function LogoWall() {
|
||||
const orgs = ['NORTHSHORE MARATHON', 'IRON SANDS ULTRA', 'GRANITE CRIT', 'TWIN PEAKS TRI', 'HARBOR REGATTA', 'SUMMIT VERTICAL', 'MIDNIGHT 24', 'COASTLINE 10K'];
|
||||
return (
|
||||
<section style={{ borderBottom: '1px solid var(--ink)', padding: '40px 32px' }}>
|
||||
<div className="overline" style={{ textAlign: 'center', marginBottom: 24 }}>Trusted by event organizers in 38 countries</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 0, border: '1px solid var(--hairline)' }}>
|
||||
{orgs.map((o, i) => (
|
||||
<div key={i} style={{ padding: '22px 16px', textAlign: 'center', borderRight: i % 4 < 3 ? '1px solid var(--hairline)' : 'none', borderBottom: i < 4 ? '1px solid var(--hairline)' : 'none', fontFamily: 'var(--font-display)', fontSize: 13, fontWeight: 700, letterSpacing: '0.10em', color: 'var(--ink-3)' }}>{o}</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
window.LogoWall = LogoWall;
|
||||
@@ -0,0 +1,33 @@
|
||||
function MktFooter() {
|
||||
const cols = [
|
||||
{ h: 'Product', l: ['Features', 'Sports', 'Hardware', 'Pricing', 'Changelog'] },
|
||||
{ h: 'Solutions', l: ['Running', 'Cycling', 'Triathlon', 'Sailing', 'Federation'] },
|
||||
{ h: 'Resources', l: ['Docs', 'API', 'Support', 'Status', 'Help center'] },
|
||||
{ h: 'Company', l: ['About', 'Customers', 'Careers', 'Press', 'Contact'] },
|
||||
];
|
||||
return (
|
||||
<footer style={{ background: 'var(--ink)', color: 'var(--paper)', padding: '56px 32px 24px' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1.4fr repeat(4, 1fr)', gap: 32 }}>
|
||||
<div>
|
||||
<img src="../../assets/logo-trm.png" alt="TRM" style={{ height: 30, filter: 'invert(1) brightness(1.1)' }} />
|
||||
<p style={{ fontFamily: 'var(--font-body)', fontSize: 13, lineHeight: 1.55, marginTop: 14, color: 'var(--paper-3)', maxWidth: 280 }}>
|
||||
The race-day operating system. Built by timing officials, for timing officials.
|
||||
</p>
|
||||
</div>
|
||||
{cols.map(c => (
|
||||
<div key={c.h}>
|
||||
<div style={{ fontFamily: 'var(--font-display)', fontSize: 11, fontWeight: 600, letterSpacing: '0.10em', textTransform: 'uppercase', color: 'var(--ink-5)' }}>{c.h}</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginTop: 12 }}>
|
||||
{c.l.map(x => <a key={x} href="#" style={{ fontFamily: 'var(--font-display)', fontSize: 13, color: 'var(--paper)', textDecoration: 'none' }}>{x}</a>)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: 48, paddingTop: 18, borderTop: '1px solid rgba(255,255,255,0.1)', fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--ink-5)', letterSpacing: '0.06em', textTransform: 'uppercase' }}>
|
||||
<div>© 2026 TRM Tracking & Race Management</div>
|
||||
<div>v3.18.0 · 99.99% uptime · 14:02:11 UTC</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
window.MktFooter = MktFooter;
|
||||
@@ -0,0 +1,70 @@
|
||||
function MktHero() {
|
||||
return (
|
||||
<section style={{ display: 'grid', gridTemplateColumns: '1.1fr 1fr', gap: 0, borderBottom: '1px solid var(--ink)' }}>
|
||||
<div style={{ padding: '80px 56px 80px 32px', borderRight: '1px solid var(--ink)' }}>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center', marginBottom: 20 }}>
|
||||
<span className="pill live"><span className="dot"></span>LIVE NOW · 47 EVENTS</span>
|
||||
</div>
|
||||
<h1 style={{ fontSize: 76, lineHeight: 0.98, letterSpacing: '-0.03em', fontWeight: 700, margin: 0 }}>
|
||||
Run any event.<br/>
|
||||
<span style={{ color: 'var(--flag)' }}>Time</span> any sport.
|
||||
</h1>
|
||||
<p style={{ fontFamily: 'var(--font-body)', fontSize: 18, lineHeight: 1.55, marginTop: 24, maxWidth: 520, color: 'var(--ink-2)' }}>
|
||||
TRM is the race-day operating system used by 4,000+ event organizers — from 5K fun runs to 24-hour endurance races. One platform for timing, tracking, and results.
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: 12, marginTop: 32 }}>
|
||||
<button className="btn primary" style={{ height: 44, padding: '0 22px', fontSize: 15 }}>Start free trial</button>
|
||||
<button className="btn" style={{ height: 44, padding: '0 22px', fontSize: 15 }}>Book a demo</button>
|
||||
</div>
|
||||
<div style={{ marginTop: 40, display: 'flex', gap: 32 }}>
|
||||
<div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 28, fontWeight: 700, letterSpacing: '-0.02em' }}>4,000+</div>
|
||||
<div style={{ fontFamily: 'var(--font-display)', fontSize: 11, fontWeight: 600, letterSpacing: '0.10em', textTransform: 'uppercase', color: 'var(--ink-3)', marginTop: 4 }}>Organizers</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 28, fontWeight: 700, letterSpacing: '-0.02em' }}>2.1M</div>
|
||||
<div style={{ fontFamily: 'var(--font-display)', fontSize: 11, fontWeight: 600, letterSpacing: '0.10em', textTransform: 'uppercase', color: 'var(--ink-3)', marginTop: 4 }}>Athletes timed</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 28, fontWeight: 700, letterSpacing: '-0.02em' }}>14</div>
|
||||
<div style={{ fontFamily: 'var(--font-display)', fontSize: 11, fontWeight: 600, letterSpacing: '0.10em', textTransform: 'uppercase', color: 'var(--ink-3)', marginTop: 4 }}>Sports supported</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ background: 'var(--night)', color: 'var(--night-fg)', padding: 32, display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<span className="pill live" style={{ color: 'var(--flag)' }}><span className="dot"></span>LIVE</span>
|
||||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--night-fg-3)', letterSpacing: '0.10em', textTransform: 'uppercase' }}>Coastline 10K · Spring 2026</span>
|
||||
</div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontVariantNumeric: 'tabular-nums', fontSize: 56, fontWeight: 600, letterSpacing: '-0.04em', lineHeight: 1, marginTop: 16 }}>01:23:45.6</div>
|
||||
<div style={{ marginTop: 24, border: '1px solid var(--night-line)' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr style={{ background: 'var(--night-3)' }}>
|
||||
<th style={{ textAlign: 'left', padding: '8px 12px', fontFamily: 'var(--font-mono)', fontSize: 10, fontWeight: 600, letterSpacing: '0.10em', textTransform: 'uppercase', color: 'var(--night-fg-3)', borderBottom: '1px solid var(--night-line)' }}>POS</th>
|
||||
<th style={{ textAlign: 'left', padding: '8px 12px', fontFamily: 'var(--font-mono)', fontSize: 10, fontWeight: 600, letterSpacing: '0.10em', textTransform: 'uppercase', color: 'var(--night-fg-3)', borderBottom: '1px solid var(--night-line)' }}>NAME</th>
|
||||
<th style={{ textAlign: 'right', padding: '8px 12px', fontFamily: 'var(--font-mono)', fontSize: 10, fontWeight: 600, letterSpacing: '0.10em', textTransform: 'uppercase', color: 'var(--night-fg-3)', borderBottom: '1px solid var(--night-line)' }}>GAP</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[
|
||||
{ p: 1, n: 'Maya Chen', g: '—' },
|
||||
{ p: 2, n: 'Ravi Park', g: '+0:04.2' },
|
||||
{ p: 3, n: 'Noemi Vega', g: '+0:11.7' },
|
||||
{ p: 4, n: 'Tomás Riera', g: '+0:36.6' },
|
||||
].map((r, i) => (
|
||||
<tr key={i}>
|
||||
<td style={{ padding: '8px 12px', fontFamily: 'var(--font-mono)', fontSize: 13, fontWeight: 700, color: r.p === 1 ? 'var(--flag)' : 'var(--night-fg)', borderBottom: '1px solid var(--night-line)' }}>{r.p}</td>
|
||||
<td style={{ padding: '8px 12px', fontFamily: 'var(--font-display)', fontSize: 14, fontWeight: 500, borderBottom: '1px solid var(--night-line)' }}>{r.n}</td>
|
||||
<td style={{ padding: '8px 12px', fontFamily: 'var(--font-mono)', fontSize: 13, textAlign: 'right', color: r.g === '—' ? 'var(--night-fg)' : '#ff8a78', borderBottom: '1px solid var(--night-line)' }}>{r.g}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--night-fg-3)', letterSpacing: '0.06em', textTransform: 'uppercase', marginTop: 14 }}>Embed any leaderboard on your event site</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
window.MktHero = MktHero;
|
||||
@@ -0,0 +1,20 @@
|
||||
function MktNav() {
|
||||
const items = ['Product', 'Sports', 'Pricing', 'Customers', 'Docs'];
|
||||
return (
|
||||
<nav style={{ display: 'grid', gridTemplateColumns: '1fr auto 1fr', alignItems: 'center', padding: '18px 32px', borderBottom: '1px solid var(--ink)', position: 'sticky', top: 0, background: 'var(--paper)', zIndex: 5 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<img src="../../assets/logo-trm.png" alt="TRM" style={{ height: 24 }} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 28, justifyContent: 'center' }}>
|
||||
{items.map(i => (
|
||||
<a key={i} href="#" style={{ fontFamily: 'var(--font-display)', fontSize: 14, fontWeight: 500, textDecoration: 'none', color: 'var(--ink)' }}>{i}</a>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 10, justifyContent: 'flex-end', alignItems: 'center' }}>
|
||||
<a href="#" style={{ fontFamily: 'var(--font-display)', fontSize: 14, fontWeight: 500, textDecoration: 'none', color: 'var(--ink)' }}>Sign in</a>
|
||||
<button className="btn primary">Start free trial</button>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
window.MktNav = MktNav;
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
function PricingStrip() {
|
||||
const tiers = [
|
||||
{ name: 'Club', price: '$0', per: 'per event', desc: 'Up to 200 starters · self-service timing.', cta: 'Start free', primary: false },
|
||||
{ name: 'Organizer', price: '$2', per: 'per starter', desc: 'Unlimited events · race control · live leaderboards.', cta: 'Start trial', primary: true },
|
||||
{ name: 'Federation', price: 'Custom', per: 'annual', desc: 'White-label · API · SLA · dedicated support.', cta: 'Contact sales', primary: false },
|
||||
];
|
||||
return (
|
||||
<section style={{ borderBottom: '1px solid var(--ink)' }}>
|
||||
<div style={{ padding: '64px 32px 32px' }}>
|
||||
<div className="overline">Pricing</div>
|
||||
<h2 style={{ fontSize: 44, margin: '12px 0 0', letterSpacing: '-0.02em', lineHeight: 1.05 }}>Pay per starter. No setup fees.</h2>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', borderTop: '1px solid var(--ink)' }}>
|
||||
{tiers.map((t, i) => (
|
||||
<div key={i} style={{ padding: 32, borderRight: i < 2 ? '1px solid var(--ink)' : 'none', background: t.primary ? 'var(--ink)' : 'var(--paper)', color: t.primary ? 'var(--paper)' : 'var(--ink)' }}>
|
||||
<div style={{ fontFamily: 'var(--font-display)', fontSize: 11, fontWeight: 600, letterSpacing: '0.10em', textTransform: 'uppercase', color: t.primary ? 'var(--paper-3)' : 'var(--ink-3)' }}>{t.name}</div>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 10, marginTop: 16 }}>
|
||||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 56, fontWeight: 700, letterSpacing: '-0.03em' }}>{t.price}</span>
|
||||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 13, color: t.primary ? 'var(--paper-3)' : 'var(--ink-3)' }}>{t.per}</span>
|
||||
</div>
|
||||
<p style={{ fontFamily: 'var(--font-body)', fontSize: 14, lineHeight: 1.55, marginTop: 16, color: t.primary ? 'var(--paper-3)' : 'var(--ink-2)' }}>{t.desc}</p>
|
||||
<button className="btn" style={{ marginTop: 20, height: 40, background: t.primary ? 'var(--flag)' : 'var(--paper)', color: t.primary ? '#fff' : 'var(--ink)', borderColor: t.primary ? 'var(--paper)' : 'var(--ink)', boxShadow: t.primary ? '2px 2px 0 0 var(--paper)' : '2px 2px 0 0 var(--ink)' }}>{t.cta}</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
window.PricingStrip = PricingStrip;
|
||||
@@ -0,0 +1,19 @@
|
||||
# Marketing Site UI Kit — TRM
|
||||
|
||||
Single-page marketing site for race organizers evaluating the platform.
|
||||
|
||||
## Sections
|
||||
- Top nav with brand
|
||||
- Hero with split layout — bold headline + live data peek
|
||||
- Logo wall (event organizers using TRM)
|
||||
- Feature grid
|
||||
- Pricing strip
|
||||
- Footer
|
||||
|
||||
## Components
|
||||
- `MktNav.jsx`
|
||||
- `MktHero.jsx`
|
||||
- `LogoWall.jsx`
|
||||
- `FeatureGrid.jsx`
|
||||
- `PricingStrip.jsx`
|
||||
- `MktFooter.jsx`
|
||||
@@ -0,0 +1,38 @@
|
||||
<!doctype html>
|
||||
<html><head><meta charset="utf-8">
|
||||
<title>TRM Marketing</title>
|
||||
<link rel="stylesheet" href="../../colors_and_type.css">
|
||||
<link rel="stylesheet" href="../kit.css">
|
||||
<style>
|
||||
body { background: var(--paper); }
|
||||
a { text-decoration: none; }
|
||||
a:hover { color: var(--flag); }
|
||||
</style>
|
||||
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel" src="MktNav.jsx"></script>
|
||||
<script type="text/babel" src="MktHero.jsx"></script>
|
||||
<script type="text/babel" src="LogoWall.jsx"></script>
|
||||
<script type="text/babel" src="FeatureGrid.jsx"></script>
|
||||
<script type="text/babel" src="PricingStrip.jsx"></script>
|
||||
<script type="text/babel" src="MktFooter.jsx"></script>
|
||||
<script type="text/babel">
|
||||
function App() {
|
||||
return (
|
||||
<div>
|
||||
<MktNav />
|
||||
<MktHero />
|
||||
<LogoWall />
|
||||
<FeatureGrid />
|
||||
<PricingStrip />
|
||||
<MktFooter />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
|
||||
</script>
|
||||
</body></html>
|
||||
@@ -0,0 +1,26 @@
|
||||
function BibCard({ bib, name, event, projected, status }) {
|
||||
return (
|
||||
<div style={{ border: '1px solid var(--ink)', padding: 18, background: 'var(--paper)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span className="pill live" style={{ fontSize: 9 }}><span className="dot"></span>LIVE</span>
|
||||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--ink-3)', letterSpacing: '0.08em', textTransform: 'uppercase' }}>{event}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 12, marginTop: 14 }}>
|
||||
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--ink-4)', letterSpacing: '0.10em', textTransform: 'uppercase' }}>BIB</span>
|
||||
<span style={{ fontFamily: 'var(--font-mono)', fontVariantNumeric: 'tabular-nums', fontSize: 56, fontWeight: 700, lineHeight: 1, letterSpacing: '-0.03em' }}>{bib}</span>
|
||||
</div>
|
||||
<div style={{ fontFamily: 'var(--font-display)', fontSize: 20, fontWeight: 700, marginTop: 6 }}>{name}</div>
|
||||
<div style={{ marginTop: 14, paddingTop: 12, borderTop: '1px solid var(--hairline)', display: 'flex', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<div style={{ fontFamily: 'var(--font-display)', fontSize: 10, fontWeight: 600, letterSpacing: '0.10em', textTransform: 'uppercase', color: 'var(--ink-3)' }}>Projected finish</div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 22, fontWeight: 600, marginTop: 4 }}>{projected}</div>
|
||||
</div>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<div style={{ fontFamily: 'var(--font-display)', fontSize: 10, fontWeight: 600, letterSpacing: '0.10em', textTransform: 'uppercase', color: 'var(--ink-3)' }}>Status</div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 14, color: 'var(--green)', marginTop: 6, fontWeight: 600 }}>{status}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
window.BibCard = BibCard;
|
||||
@@ -0,0 +1,21 @@
|
||||
function BottomNav({ active, onNav }) {
|
||||
const items = [
|
||||
{ id: 'race', label: 'My race', icon: 'M12 2v4 M12 18v4 M2 12h4 M18 12h4' },
|
||||
{ id: 'follow',label: 'Following', icon: 'M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2 M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8 M23 21v-2a4 4 0 0 0-3-3.87 M16 3.13a4 4 0 0 1 0 7.75' },
|
||||
{ id: 'find', label: 'Find', icon: 'M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16 M21 21l-4.35-4.35' },
|
||||
{ id: 'me', label: 'Profile', icon: 'M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2 M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8' },
|
||||
];
|
||||
return (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', borderTop: '1px solid var(--ink)', background: 'var(--paper)' }}>
|
||||
{items.map(it => (
|
||||
<div key={it.id} onClick={() => onNav(it.id)} style={{ padding: '10px 0 6px', textAlign: 'center', cursor: 'pointer', borderTop: active === it.id ? '2px solid var(--flag)' : '2px solid transparent', marginTop: -1 }}>
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke={active === it.id ? 'var(--flag)' : 'var(--ink-3)'} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ display: 'block', margin: '0 auto' }}>
|
||||
{it.icon.split(' M').map((d, i) => <path key={i} d={(i === 0 ? '' : 'M') + d} />)}
|
||||
</svg>
|
||||
<div style={{ fontFamily: 'var(--font-display)', fontSize: 10, fontWeight: 600, letterSpacing: '0.06em', textTransform: 'uppercase', marginTop: 4, color: active === it.id ? 'var(--flag)' : 'var(--ink-3)' }}>{it.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
window.BottomNav = BottomNav;
|
||||
@@ -0,0 +1,24 @@
|
||||
function FollowList({ people, onSelect }) {
|
||||
return (
|
||||
<div style={{ border: '1px solid var(--ink)', background: 'var(--paper)' }}>
|
||||
<div style={{ padding: '10px 14px', borderBottom: '1px solid var(--ink)', background: 'var(--paper-2)', display: 'flex', alignItems: 'center' }}>
|
||||
<span style={{ fontFamily: 'var(--font-display)', fontSize: 11, fontWeight: 600, letterSpacing: '0.10em', textTransform: 'uppercase' }}>Following</span>
|
||||
<span style={{ marginLeft: 'auto', fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--ink-3)' }}>{people.length}</span>
|
||||
</div>
|
||||
{people.map((p, i) => (
|
||||
<div key={i} onClick={() => onSelect && onSelect(p)} style={{ display: 'grid', gridTemplateColumns: '40px 1fr auto', gap: 12, padding: '12px 14px', borderBottom: i < people.length - 1 ? '1px solid var(--hairline)' : 'none', alignItems: 'center', cursor: 'pointer' }}>
|
||||
<div style={{ width: 36, height: 36, border: '1px solid var(--ink)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 700 }}>{p.bib}</div>
|
||||
<div>
|
||||
<div style={{ fontFamily: 'var(--font-display)', fontSize: 14, fontWeight: 600 }}>{p.name}</div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--ink-3)', marginTop: 2 }}>{p.event} · {p.lastSplit}</div>
|
||||
</div>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 13, fontWeight: 600 }}>{p.time}</div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: p.delta && p.delta.startsWith('-') ? 'var(--green)' : 'var(--flag)', marginTop: 2 }}>{p.delta}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
window.FollowList = FollowList;
|
||||
@@ -0,0 +1,13 @@
|
||||
# Mobile UI Kit — TRM
|
||||
|
||||
Athlete & spectator mobile companion. Inside an iOS frame for context.
|
||||
|
||||
## Screens
|
||||
- **My race** — athlete view: bib, current split, projected finish, course progress
|
||||
- **Following** — spectator view: list of tracked athletes with live status
|
||||
|
||||
## Components
|
||||
- `BibCard.jsx` — hero bib + projected finish
|
||||
- `SplitList.jsx` — vertical timeline of splits
|
||||
- `FollowList.jsx` — list of followed athletes
|
||||
- `BottomNav.jsx` — tab bar
|
||||
@@ -0,0 +1,22 @@
|
||||
function SplitList({ splits }) {
|
||||
return (
|
||||
<div style={{ border: '1px solid var(--ink)', background: 'var(--paper)' }}>
|
||||
<div style={{ padding: '10px 14px', borderBottom: '1px solid var(--ink)', background: 'var(--paper-2)', display: 'flex', alignItems: 'center' }}>
|
||||
<span style={{ fontFamily: 'var(--font-display)', fontSize: 11, fontWeight: 600, letterSpacing: '0.10em', textTransform: 'uppercase' }}>Splits</span>
|
||||
<span style={{ marginLeft: 'auto', fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--ink-3)' }}>4 of 6</span>
|
||||
</div>
|
||||
{splits.map((s, i) => (
|
||||
<div key={i} style={{ display: 'grid', gridTemplateColumns: '24px 1fr auto auto', gap: 12, padding: '10px 14px', borderBottom: i < splits.length - 1 ? '1px solid var(--hairline)' : 'none', alignItems: 'center' }}>
|
||||
<span style={{ width: 10, height: 10, borderRadius: 999, background: s.passed ? 'var(--flag)' : 'var(--paper)', border: '2px solid ' + (s.passed ? 'var(--flag)' : 'var(--ink-5)'), justifySelf: 'center' }}></span>
|
||||
<div>
|
||||
<div style={{ fontFamily: 'var(--font-display)', fontSize: 13, fontWeight: 600 }}>{s.name}</div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--ink-4)', marginTop: 2 }}>{s.km} km</div>
|
||||
</div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 13, fontWeight: 600, color: s.passed ? 'var(--ink)' : 'var(--ink-4)' }}>{s.time}</div>
|
||||
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: s.passed ? (s.delta.startsWith('-') ? 'var(--green)' : 'var(--flag)') : 'var(--ink-4)' }}>{s.delta}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
window.SplitList = SplitList;
|
||||
@@ -0,0 +1,84 @@
|
||||
<!doctype html>
|
||||
<html><head><meta charset="utf-8">
|
||||
<title>TRM Mobile · UI Kit</title>
|
||||
<link rel="stylesheet" href="../../colors_and_type.css">
|
||||
<link rel="stylesheet" href="../kit.css">
|
||||
<style>
|
||||
body { background: var(--paper-3); display: flex; align-items: center; justify-content: center; padding: 32px; min-height: 100vh; }
|
||||
.stage { display: flex; gap: 32px; align-items: flex-start; }
|
||||
</style>
|
||||
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel" src="ios-frame.jsx"></script>
|
||||
<script type="text/babel" src="BibCard.jsx"></script>
|
||||
<script type="text/babel" src="SplitList.jsx"></script>
|
||||
<script type="text/babel" src="FollowList.jsx"></script>
|
||||
<script type="text/babel" src="BottomNav.jsx"></script>
|
||||
<script type="text/babel">
|
||||
const { useState } = React;
|
||||
|
||||
const SPLITS = [
|
||||
{ name: 'Start', km: 0, time: '09:00:00.0', delta: '—', passed: true },
|
||||
{ name: 'Split 1', km: 2.5, time: '00:10:38.4', delta: '-0:01.2', passed: true },
|
||||
{ name: 'Split 2', km: 5.0, time: '00:21:14.7', delta: '-0:02.0', passed: true },
|
||||
{ name: 'Split 3', km: 7.5, time: '00:31:55.1', delta: '+0:00.4', passed: true },
|
||||
{ name: 'Split 4', km: 9.0, time: '—', delta: '—', passed: false },
|
||||
{ name: 'Finish', km: 10, time: '—', delta: '—', passed: false },
|
||||
];
|
||||
|
||||
const PEOPLE = [
|
||||
{ bib: 247, name: 'Maya Chen', event: 'Coastline 10K', lastSplit: 'Split 3', time: '31:55.1', delta: '+0:00.4' },
|
||||
{ bib: 188, name: 'Ravi Park', event: 'Coastline 10K', lastSplit: 'Split 3', time: '32:01.6', delta: '-0:01.8' },
|
||||
{ bib: 44, name: 'Noemi Vega', event: 'Coastline 10K', lastSplit: 'Split 3', time: '32:08.4', delta: '+0:02.1' },
|
||||
{ bib: 612, name: 'Diego Acosta', event: 'Twin Peaks Tri', lastSplit: 'Bike', time: '1:12:04', delta: '-0:18.0' },
|
||||
];
|
||||
|
||||
function MyRace() {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 14, padding: 14 }}>
|
||||
<BibCard bib="247" name="Maya Chen" event="Coastline 10K · Spring 2026" projected="00:42:12" status="ON PACE · PB" />
|
||||
<SplitList splits={SPLITS} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Following({ onSelect }) {
|
||||
return (
|
||||
<div style={{ padding: 14 }}>
|
||||
<FollowList people={PEOPLE} onSelect={onSelect} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [tab, setTab] = useState('race');
|
||||
return (
|
||||
<div className="stage">
|
||||
<IOSDevice title="My race" width={390} height={780}>
|
||||
<div style={{ background: 'var(--paper-2)', minHeight: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ flex: 1, overflow: 'auto' }}>
|
||||
{tab === 'race' && <MyRace />}
|
||||
{tab === 'follow' && <Following onSelect={() => setTab('race')} />}
|
||||
{tab === 'find' && <div style={{ padding: 24, textAlign: 'center', color: 'var(--ink-4)', fontFamily: 'var(--font-mono)', fontSize: 12 }}>Search any event by name or bib.</div>}
|
||||
{tab === 'me' && <div style={{ padding: 24, textAlign: 'center', color: 'var(--ink-4)', fontFamily: 'var(--font-mono)', fontSize: 12 }}>Profile · history · personal bests</div>}
|
||||
</div>
|
||||
<BottomNav active={tab} onNav={setTab} />
|
||||
</div>
|
||||
</IOSDevice>
|
||||
<IOSDevice title="Following" width={390} height={780}>
|
||||
<div style={{ background: 'var(--paper-2)', minHeight: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ flex: 1, overflow: 'auto' }}><Following /></div>
|
||||
<BottomNav active="follow" onNav={() => {}} />
|
||||
</div>
|
||||
</IOSDevice>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
|
||||
</script>
|
||||
</body></html>
|
||||
@@ -0,0 +1,338 @@
|
||||
|
||||
// iOS.jsx — Simplified iOS 26 (Liquid Glass) device frame
|
||||
// Based on the iOS 26 UI Kit + Figma status bar spec. No assets, no deps.
|
||||
// Exports: IOSDevice, IOSStatusBar, IOSNavBar, IOSGlassPill, IOSList, IOSListRow, IOSKeyboard
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Status bar
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
function IOSStatusBar({ dark = false, time = '9:41' }) {
|
||||
const c = dark ? '#fff' : '#000';
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', gap: 154, alignItems: 'center', justifyContent: 'center',
|
||||
padding: '21px 24px 19px', boxSizing: 'border-box',
|
||||
position: 'relative', zIndex: 20, width: '100%',
|
||||
}}>
|
||||
<div style={{ flex: 1, height: 22, display: 'flex', alignItems: 'center', justifyContent: 'center', paddingTop: 1.5 }}>
|
||||
<span style={{
|
||||
fontFamily: '-apple-system, "SF Pro", system-ui', fontWeight: 590,
|
||||
fontSize: 17, lineHeight: '22px', color: c,
|
||||
}}>{time}</span>
|
||||
</div>
|
||||
<div style={{ flex: 1, height: 22, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 7, paddingTop: 1, paddingRight: 1 }}>
|
||||
<svg width="19" height="12" viewBox="0 0 19 12">
|
||||
<rect x="0" y="7.5" width="3.2" height="4.5" rx="0.7" fill={c}/>
|
||||
<rect x="4.8" y="5" width="3.2" height="7" rx="0.7" fill={c}/>
|
||||
<rect x="9.6" y="2.5" width="3.2" height="9.5" rx="0.7" fill={c}/>
|
||||
<rect x="14.4" y="0" width="3.2" height="12" rx="0.7" fill={c}/>
|
||||
</svg>
|
||||
<svg width="17" height="12" viewBox="0 0 17 12">
|
||||
<path d="M8.5 3.2C10.8 3.2 12.9 4.1 14.4 5.6L15.5 4.5C13.7 2.7 11.2 1.5 8.5 1.5C5.8 1.5 3.3 2.7 1.5 4.5L2.6 5.6C4.1 4.1 6.2 3.2 8.5 3.2Z" fill={c}/>
|
||||
<path d="M8.5 6.8C9.9 6.8 11.1 7.3 12 8.2L13.1 7.1C11.8 5.9 10.2 5.1 8.5 5.1C6.8 5.1 5.2 5.9 3.9 7.1L5 8.2C5.9 7.3 7.1 6.8 8.5 6.8Z" fill={c}/>
|
||||
<circle cx="8.5" cy="10.5" r="1.5" fill={c}/>
|
||||
</svg>
|
||||
<svg width="27" height="13" viewBox="0 0 27 13">
|
||||
<rect x="0.5" y="0.5" width="23" height="12" rx="3.5" stroke={c} strokeOpacity="0.35" fill="none"/>
|
||||
<rect x="2" y="2" width="20" height="9" rx="2" fill={c}/>
|
||||
<path d="M25 4.5V8.5C25.8 8.2 26.5 7.2 26.5 6.5C26.5 5.8 25.8 4.8 25 4.5Z" fill={c} fillOpacity="0.4"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Liquid glass pill — blur + tint + shine
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
function IOSGlassPill({ children, dark = false, style = {} }) {
|
||||
return (
|
||||
<div style={{
|
||||
height: 44, minWidth: 44, borderRadius: 9999,
|
||||
position: 'relative', overflow: 'hidden',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
boxShadow: dark
|
||||
? '0 2px 6px rgba(0,0,0,0.35), 0 6px 16px rgba(0,0,0,0.2)'
|
||||
: '0 1px 3px rgba(0,0,0,0.07), 0 3px 10px rgba(0,0,0,0.06)',
|
||||
...style,
|
||||
}}>
|
||||
{/* blur + tint */}
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0, borderRadius: 9999,
|
||||
backdropFilter: 'blur(12px) saturate(180%)',
|
||||
WebkitBackdropFilter: 'blur(12px) saturate(180%)',
|
||||
background: dark ? 'rgba(120,120,128,0.28)' : 'rgba(255,255,255,0.5)',
|
||||
}} />
|
||||
{/* shine */}
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0, borderRadius: 9999,
|
||||
boxShadow: dark
|
||||
? 'inset 1.5px 1.5px 1px rgba(255,255,255,0.15), inset -1px -1px 1px rgba(255,255,255,0.08)'
|
||||
: 'inset 1.5px 1.5px 1px rgba(255,255,255,0.7), inset -1px -1px 1px rgba(255,255,255,0.4)',
|
||||
border: dark ? '0.5px solid rgba(255,255,255,0.15)' : '0.5px solid rgba(0,0,0,0.06)',
|
||||
}} />
|
||||
<div style={{ position: 'relative', zIndex: 1, display: 'flex', alignItems: 'center', padding: '0 4px' }}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Navigation bar — glass pills + large title
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
function IOSNavBar({ title = 'Title', dark = false, trailingIcon = true }) {
|
||||
const muted = dark ? 'rgba(255,255,255,0.6)' : '#404040';
|
||||
const text = dark ? '#fff' : '#000';
|
||||
const pillIcon = (content) => (
|
||||
<IOSGlassPill dark={dark}>
|
||||
<div style={{ width: 36, height: 36, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
{content}
|
||||
</div>
|
||||
</IOSGlassPill>
|
||||
);
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', flexDirection: 'column', gap: 10,
|
||||
paddingTop: 62, paddingBottom: 10, position: 'relative', zIndex: 5,
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
padding: '0 16px',
|
||||
}}>
|
||||
{/* back chevron */}
|
||||
{pillIcon(
|
||||
<svg width="12" height="20" viewBox="0 0 12 20" fill="none" style={{ marginLeft: -1 }}>
|
||||
<path d="M10 2L2 10l8 8" stroke={muted} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
)}
|
||||
{/* trailing ellipsis */}
|
||||
{trailingIcon && pillIcon(
|
||||
<svg width="22" height="6" viewBox="0 0 22 6">
|
||||
<circle cx="3" cy="3" r="2.5" fill={muted}/>
|
||||
<circle cx="11" cy="3" r="2.5" fill={muted}/>
|
||||
<circle cx="19" cy="3" r="2.5" fill={muted}/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
{/* large title */}
|
||||
<div style={{
|
||||
padding: '0 16px',
|
||||
fontFamily: '-apple-system, system-ui',
|
||||
fontSize: 34, fontWeight: 700, lineHeight: '41px',
|
||||
color: text, letterSpacing: 0.4,
|
||||
}}>{title}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Grouped list (inset card, r:26) + row (52px)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
function IOSListRow({ title, detail, icon, chevron = true, isLast = false, dark = false }) {
|
||||
const text = dark ? '#fff' : '#000';
|
||||
const sec = dark ? 'rgba(235,235,245,0.6)' : 'rgba(60,60,67,0.6)';
|
||||
const ter = dark ? 'rgba(235,235,245,0.3)' : 'rgba(60,60,67,0.3)';
|
||||
const sep = dark ? 'rgba(84,84,88,0.65)' : 'rgba(60,60,67,0.12)';
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', minHeight: 52,
|
||||
padding: '0 16px', position: 'relative',
|
||||
fontFamily: '-apple-system, system-ui', fontSize: 17,
|
||||
letterSpacing: -0.43,
|
||||
}}>
|
||||
{icon && (
|
||||
<div style={{
|
||||
width: 30, height: 30, borderRadius: 7, background: icon,
|
||||
marginRight: 12, flexShrink: 0,
|
||||
}} />
|
||||
)}
|
||||
<div style={{ flex: 1, color: text }}>{title}</div>
|
||||
{detail && <span style={{ color: sec, marginRight: 6 }}>{detail}</span>}
|
||||
{chevron && (
|
||||
<svg width="8" height="14" viewBox="0 0 8 14" style={{ flexShrink: 0 }}>
|
||||
<path d="M1 1l6 6-6 6" stroke={ter} strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
)}
|
||||
{!isLast && (
|
||||
<div style={{
|
||||
position: 'absolute', bottom: 0, right: 0,
|
||||
left: icon ? 58 : 16, height: 0.5, background: sep,
|
||||
}} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function IOSList({ header, children, dark = false }) {
|
||||
const hc = dark ? 'rgba(235,235,245,0.6)' : 'rgba(60,60,67,0.6)';
|
||||
const bg = dark ? '#1C1C1E' : '#fff';
|
||||
return (
|
||||
<div>
|
||||
{header && (
|
||||
<div style={{
|
||||
fontFamily: '-apple-system, system-ui', fontSize: 13,
|
||||
color: hc, textTransform: 'uppercase',
|
||||
padding: '8px 36px 6px', letterSpacing: -0.08,
|
||||
}}>{header}</div>
|
||||
)}
|
||||
<div style={{
|
||||
background: bg, borderRadius: 26,
|
||||
margin: '0 16px', overflow: 'hidden',
|
||||
}}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Device frame
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
function IOSDevice({
|
||||
children, width = 402, height = 874, dark = false,
|
||||
title, keyboard = false,
|
||||
}) {
|
||||
return (
|
||||
<div style={{
|
||||
width, height, borderRadius: 48, overflow: 'hidden',
|
||||
position: 'relative', background: dark ? '#000' : '#F2F2F7',
|
||||
boxShadow: '0 40px 80px rgba(0,0,0,0.18), 0 0 0 1px rgba(0,0,0,0.12)',
|
||||
fontFamily: '-apple-system, system-ui, sans-serif',
|
||||
WebkitFontSmoothing: 'antialiased',
|
||||
}}>
|
||||
{/* dynamic island */}
|
||||
<div style={{
|
||||
position: 'absolute', top: 11, left: '50%', transform: 'translateX(-50%)',
|
||||
width: 126, height: 37, borderRadius: 24, background: '#000', zIndex: 50,
|
||||
}} />
|
||||
{/* status bar (absolute) */}
|
||||
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, zIndex: 10 }}>
|
||||
<IOSStatusBar dark={dark} />
|
||||
</div>
|
||||
{/* nav + content */}
|
||||
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
{title !== undefined && <IOSNavBar title={title} dark={dark} />}
|
||||
<div style={{ flex: 1, overflow: 'auto' }}>{children}</div>
|
||||
{keyboard && <IOSKeyboard dark={dark} />}
|
||||
</div>
|
||||
{/* home indicator — always on top */}
|
||||
<div style={{
|
||||
position: 'absolute', bottom: 0, left: 0, right: 0, zIndex: 60,
|
||||
height: 34, display: 'flex', justifyContent: 'center', alignItems: 'flex-end',
|
||||
paddingBottom: 8, pointerEvents: 'none',
|
||||
}}>
|
||||
<div style={{
|
||||
width: 139, height: 5, borderRadius: 100,
|
||||
background: dark ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.25)',
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Keyboard — iOS 26 liquid glass
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
function IOSKeyboard({ dark = false }) {
|
||||
const glyph = dark ? 'rgba(255,255,255,0.7)' : '#595959';
|
||||
const sugg = dark ? 'rgba(255,255,255,0.6)' : '#333';
|
||||
const keyBg = dark ? 'rgba(255,255,255,0.22)' : 'rgba(255,255,255,0.85)';
|
||||
|
||||
// special-key icons
|
||||
const icons = {
|
||||
shift: <svg width="19" height="17" viewBox="0 0 19 17"><path d="M9.5 1L1 9.5h4.5V16h8V9.5H18L9.5 1z" fill={glyph}/></svg>,
|
||||
del: <svg width="23" height="17" viewBox="0 0 23 17"><path d="M7 1h13a2 2 0 012 2v11a2 2 0 01-2 2H7l-6-7.5L7 1z" fill="none" stroke={glyph} strokeWidth="1.6" strokeLinejoin="round"/><path d="M10 5l7 7M17 5l-7 7" stroke={glyph} strokeWidth="1.6" strokeLinecap="round"/></svg>,
|
||||
ret: <svg width="20" height="14" viewBox="0 0 20 14"><path d="M18 1v6H4m0 0l4-4M4 7l4 4" fill="none" stroke="#fff" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/></svg>,
|
||||
};
|
||||
|
||||
const key = (content, { w, flex, ret, fs = 25, k } = {}) => (
|
||||
<div key={k} style={{
|
||||
height: 42, borderRadius: 8.5,
|
||||
flex: flex ? 1 : undefined, width: w, minWidth: 0,
|
||||
background: ret ? '#08f' : keyBg,
|
||||
boxShadow: '0 1px 0 rgba(0,0,0,0.075)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontFamily: '-apple-system, "SF Compact", system-ui',
|
||||
fontSize: fs, fontWeight: 458, color: ret ? '#fff' : glyph,
|
||||
}}>{content}</div>
|
||||
);
|
||||
|
||||
const row = (keys, pad = 0) => (
|
||||
<div style={{ display: 'flex', gap: 6.5, justifyContent: 'center', padding: `0 ${pad}px` }}>
|
||||
{keys.map(l => key(l, { flex: true, k: l }))}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'relative', zIndex: 15, borderRadius: 27, overflow: 'hidden',
|
||||
padding: '11px 0 2px',
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center',
|
||||
boxShadow: dark
|
||||
? '0 -2px 20px rgba(0,0,0,0.09)'
|
||||
: '0 -1px 6px rgba(0,0,0,0.018), 0 -3px 20px rgba(0,0,0,0.012)',
|
||||
}}>
|
||||
{/* liquid glass bg — same recipe as nav pills */}
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0, borderRadius: 27,
|
||||
backdropFilter: 'blur(12px) saturate(180%)',
|
||||
WebkitBackdropFilter: 'blur(12px) saturate(180%)',
|
||||
background: dark ? 'rgba(120,120,128,0.14)' : 'rgba(255,255,255,0.25)',
|
||||
}} />
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0, borderRadius: 27,
|
||||
boxShadow: dark
|
||||
? 'inset 1.5px 1.5px 1px rgba(255,255,255,0.15)'
|
||||
: 'inset 1.5px 1.5px 1px rgba(255,255,255,0.7), inset -1px -1px 1px rgba(255,255,255,0.4)',
|
||||
border: dark ? '0.5px solid rgba(255,255,255,0.15)' : '0.5px solid rgba(0,0,0,0.06)',
|
||||
pointerEvents: 'none',
|
||||
}} />
|
||||
|
||||
{/* autocorrect bar */}
|
||||
<div style={{
|
||||
display: 'flex', gap: 20, alignItems: 'center',
|
||||
padding: '8px 22px 13px', width: '100%', boxSizing: 'border-box',
|
||||
position: 'relative',
|
||||
}}>
|
||||
{['"The"', 'the', 'to'].map((w, i) => (
|
||||
<React.Fragment key={i}>
|
||||
{i > 0 && <div style={{ width: 1, height: 25, background: '#ccc', opacity: 0.3 }} />}
|
||||
<div style={{
|
||||
flex: 1, textAlign: 'center',
|
||||
fontFamily: '-apple-system, system-ui', fontSize: 17,
|
||||
color: sugg, letterSpacing: -0.43, lineHeight: '22px',
|
||||
}}>{w}</div>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* key layout */}
|
||||
<div style={{
|
||||
display: 'flex', flexDirection: 'column', gap: 13,
|
||||
padding: '0 6.5px', width: '100%', boxSizing: 'border-box',
|
||||
position: 'relative',
|
||||
}}>
|
||||
{row(['q','w','e','r','t','y','u','i','o','p'])}
|
||||
{row(['a','s','d','f','g','h','j','k','l'], 20)}
|
||||
<div style={{ display: 'flex', gap: 14.25, alignItems: 'center' }}>
|
||||
{key(icons.shift, { w: 45, k: 'shift' })}
|
||||
<div style={{ display: 'flex', gap: 6.5, flex: 1 }}>
|
||||
{['z','x','c','v','b','n','m'].map(l => key(l, { flex: true, k: l }))}
|
||||
</div>
|
||||
{key(icons.del, { w: 45, k: 'del' })}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
|
||||
{key('ABC', { w: 92.25, fs: 18, k: 'abc' })}
|
||||
{key('', { flex: true, k: 'space' })}
|
||||
{key(icons.ret, { w: 92.25, ret: true, k: 'ret' })}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* bottom spacer (emoji+mic area, icons omitted) */}
|
||||
<div style={{ height: 56, width: '100%', position: 'relative' }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(window, {
|
||||
IOSDevice, IOSStatusBar, IOSNavBar, IOSGlassPill, IOSList, IOSListRow, IOSKeyboard,
|
||||
});
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Reference in New Issue
Block a user