From edc9a6781a4ef9d85c377c20531fd3999a49c37e Mon Sep 17 00:00:00 2001 From: harsha balakrishnan Date: Fri, 10 Jul 2026 15:02:00 +0530 Subject: [PATCH 1/3] =?UTF-8?q?feat(metrics):=20first-run=20scoreboard,=20?= =?UTF-8?q?today=20totals,=20activation=20(PK-1=E2=80=935)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add local activation constant, job success-rate history, post-convert BatchScoreboard (this batch / today / all-time), optional destination prompt, first-run hero copy, dual-section README, and docs/TASKS.md. Token banner period includes today. Privacy copy on all stats surfaces. --- README.md | 247 +++++++++-------------- docs/TASKS.md | 24 +++ src/App.svelte | 107 +++++++++- src/components/BatchScoreboard.svelte | 235 +++++++++++++++++++++ src/components/OnboardingScreen.svelte | 7 + src/components/TokenSavingsBanner.svelte | 14 +- src/components/TokenSavingsPanel.svelte | 9 + src/index.css | 24 +++ src/lib/activation.test.ts | 44 ++++ src/lib/activation.ts | 44 ++++ src/lib/jobOutcomes.test.ts | 39 ++++ src/lib/jobOutcomes.ts | 81 ++++++++ src/lib/store.ts | 6 +- src/lib/tokenStats.test.ts | 16 +- src/lib/tokenStats.ts | 38 ++++ src/locales/en.json | 35 +++- src/locales/es.json | 35 +++- src/locales/zh.json | 35 +++- 18 files changed, 863 insertions(+), 177 deletions(-) create mode 100644 docs/TASKS.md create mode 100644 src/components/BatchScoreboard.svelte create mode 100644 src/lib/activation.test.ts create mode 100644 src/lib/activation.ts create mode 100644 src/lib/jobOutcomes.test.ts create mode 100644 src/lib/jobOutcomes.ts diff --git a/README.md b/README.md index 2a7f215..2c3c88c 100644 --- a/README.md +++ b/README.md @@ -4,202 +4,151 @@

ParseKit

-

Turn documents into AI-ready Markdown

-

- Download for Mac (Apple Silicon) + Download for Mac  ·  Install guide

-ParseKit is a native macOS menu-bar app that converts PDFs, Office files, spreadsheets, and images into clean Markdown, plain text, or JSON — entirely on your Mac. +--- -Raw PDFs and Office files carry layout noise, repeated headers/footers, and broken line wraps that inflate token count without adding meaning, and scanned pages aren't readable as text at all without OCR. ParseKit strips that noise locally, so more of a document's actual content fits into a single ChatGPT/Claude/Gemini context window — and teams ingesting documents in bulk (RAG pipelines, internal chat tools, batch summarization) can process more files before hitting a tokens-per-minute rate limit. It's noise removal, not compression — see the honest numbers below rather than a marketing claim. +## Start here -**Private · Batch · OCR · Offline · No API keys · No subscriptions** +### What it is -## How it works +ParseKit is a small app that lives in your Mac menu bar. You give it PDFs, Word files, spreadsheets, slides, or images — it turns them into clean Markdown (or plain text / JSON) **on your computer**. -```mermaid -flowchart LR - subgraph input["① Your documents"] - PDF["PDF"] - OFF["Word · Excel · PowerPoint"] - IMG["Images · scans"] - end +### What problem it solves - subgraph app["② ParseKit on your Mac"] - MB["Menu bar app"] - ENG["Parse engine\n(LiteParse v2)"] - MB --> ENG - end +Raw documents are messy for AI tools. They include headers, footers, and layout noise that waste tokens, and scans aren’t readable as text without OCR. ParseKit cleans that up **locally**, so you paste more useful content into ChatGPT, Claude, Gemini, or your notes — and private files never get uploaded for conversion. - subgraph output["③ Output folder"] - MD[".md Markdown"] - TXT[".txt plain text"] - JSON[".json data"] - end +### Install and first use (about 2 minutes) - subgraph use["④ Use anywhere"] - AI["ChatGPT · Claude · Gemini"] - NOTES["Obsidian · Notes"] - end +1. Download the latest DMG from [Releases](https://github.com/harshabala/parsekit/releases/latest) (Apple Silicon, macOS 12+). +2. Open the DMG → drag **ParseKit** into **Applications**. +3. Open ParseKit from Applications. Look for the icon in the **menu bar** (top-right of the screen). +4. If macOS blocks the app, run once in Terminal (or use the fix command in Settings): - PDF --> MB - OFF --> MB - IMG --> MB - ENG --> MD - ENG --> TXT - ENG --> JSON - MD --> AI - MD --> NOTES - TXT --> AI - JSON --> AI -``` + ```bash + xattr -cr /Applications/ParseKit.app + ``` -**In plain terms:** +5. Click the menu bar icon → set an **output folder** if asked → **drop one PDF or Office file** → click convert. +6. When it finishes, you’ll see **tokens saved this batch**, plus **today** and **all-time** totals. Open the output folder and paste the Markdown into your AI tool or notes. -1. Click the ParseKit icon in your menu bar. -2. Drop in a folder of PDFs, Office files, or images. -3. ParseKit converts them on your Mac — nothing is uploaded. -4. Open the output folder and paste the results into your AI tool or notes app. +**Nothing is uploaded** for conversion. Counts stay on this Mac. -
-What's happening under the hood? +### When something goes wrong -```mermaid -flowchart TB - subgraph mac["Your Mac — no cloud"] - UI["Menu bar UI"] - CORE["ParseKit app\nsettings · file picking"] - SIDE["parsekit-sidecar\nRust + LiteParse v2"] - UI <--> CORE - CORE <--> SIDE - SIDE --> OUT[".md / .txt / .json files"] - end +| Situation | What to do | +|-----------|------------| +| Word/PowerPoint fails | Open **Settings → File Support** and install the listed helpers (e.g. LibreOffice). PDFs usually work without them. | +| Scan looks empty | Turn **OCR** on in Settings and pick a language. | +| Some files fail in a batch | The results panel shows success vs fail. Retry the failed ones or try another format. | +| App won’t open | Run the `xattr` command above; details in [docs/INSTALL.md](docs/INSTALL.md). | +| First launch feels unclear | Drop **one** file first — the token savings number is the “it worked” moment. | - subgraph optional["Optional helpers on your Mac"] - LO["LibreOffice\nWord & PowerPoint"] - IM["ImageMagick\nimages"] - end - - SIDE -.-> LO - SIDE -.-> IM - - NET(("Internet")) -. "update checks only" .-> CORE - NET -. "OCR language data\n(first use)" .-> SIDE -``` - -Your files are read and written only on your machine. The only network calls are optional: checking for app updates, and downloading OCR language packs the first time you need them. +### Privacy (plain words) -
+Your documents stay on your Mac. Token savings and success counts are stored only on this device. Optional: update checks and OCR language packs may use the network; conversion itself does not send your files to a server. --- -## Why it matters for AI workflows +## For technical users -LLMs read text, not PDF layout. Naive extraction — copy-paste from Preview, raw `pdftotext`, dumping Office XML — carries formatting noise that costs tokens without adding meaning, and scanned pages need OCR before there's any text to read at all. ParseKit produces structured Markdown with page markers, and runs OCR on scans, so what you paste into an AI tool is closer to pure signal. +### Architecture -**Measured, reproducible numbers:** [`docs/benchmark-results.md`](docs/benchmark-results.md) — generated by [`scripts/benchmark_tokens.py`](scripts/benchmark_tokens.py), which runs the real `parsekit` CLI against a set of fixture documents and compares token counts against a naive, no-cleanup extraction (what you'd get without ParseKit). Numbers aren't hand-edited into this README; run the script on your own documents to reproduce them. - -**Business framing:** if your team ingests documents into LLM workflows at any volume, lower tokens per document is a throughput argument, not just a cost one — it's the difference between how many files you can push through before a rate-limit window resets. - -## How people actually use it - -1. **Scanned contract into Claude/ChatGPT** — right-click the PDF → Quick Actions → Parse to Markdown with ParseKit → paste clean text into chat instead of uploading a scan the model struggles to read. -2. **Consultant batch-prepping client docs** — drop a folder of Word files, PDFs, and decks into ParseKit, run once, feed the output folder into a Claude Project or RAG index. -3. **Developer RAG pipeline** — add `parsekit convert ./inbox --batch --out ./markdown` as a preprocessing step. Offline, no third-party upload of sensitive documents. -4. **AI coding agent mid-task** — before reading a PDF spec into context, run `parsekit convert spec.pdf --out /tmp/spec.md` and read the Markdown instead, preserving context budget for the actual task. See [skills/parsekit/SKILL.md](skills/parsekit/SKILL.md). - ---- +```mermaid +flowchart TB + subgraph UI["Menu bar UI (Svelte 5 + Tauri 2)"] + POP["Popover App.svelte"] + ONB["Onboarding window"] + HUD["Progress HUD"] + end -## Finder Quick Actions + subgraph CORE["Tauri / Rust"] + IPC["Commands + events"] + STORE["settings.json + token_stats"] + TRAY["Tray + Finder actions"] + end -Right-click any supported file in Finder → **Quick Actions** → **Parse to Markdown with ParseKit**. + subgraph ENGINE["parsekit-sidecar"] + LP["LiteParse v2"] + OCR["OCR / optional converters"] + end -- If you've set an output folder in ParseKit, files parse silently and you get a notification. -- Otherwise ParseKit opens with the files loaded. -- Also available in **System Settings → Keyboard → Keyboard Shortcuts → Services** (same Automator workflows). + POP --> IPC + ONB --> IPC + HUD --> IPC + IPC --> STORE + IPC --> ENGINE + ENGINE --> OUT["Output folder\n.md / .txt / .json"] + LP --> OUT + OCR --> OUT +``` -**Replace Original (opt-in):** a second action, **Parse to Markdown with ParseKit (Replace Original)**, moves the original to Trash after a *successful* parse only — always recoverable from Trash. +### How conversion works -Install both from **Settings → General → Finder**. +1. UI collects paths + options (format, OCR, workers). +2. Sidecar streams events: `start` → `progress` → `token_savings` → `done` / `error`. +3. Frontend records **local** token stats and job outcomes; shows a **batch scoreboard** (this batch / today / all-time / success rate). +4. Files are written only under the user-chosen output directory. -## Settings +### Product metrics (local-first) -| Tab | What it controls | -| --- | --- | -| **General** | Language, appearance, launch at login, Gatekeeper help, Finder actions, updates, global hotkey (⌃⇧M), token savings counter | -| **File Support** | OCR language, OCR threads, **required converters** — LibreOffice for Word/PowerPoint, ImageMagick for images. PDF parsing works without these. | +| Metric | Definition | Storage | +|--------|------------|---------| +| **Activation** | `first_successful_convert_with_token_estimate` — first batch with ≥1 successful file | `activatedAt` in Tauri settings store | +| **Tokens saved** | Estimate: raw extract vs cleaned Markdown (~4 chars/token) | On-disk token stats (Rust `token_stats`) | +| **Today / all-time** | Calendar-day and lifetime sums of token events | Same | +| **Success rate** | Successful vs failed files across last 20 jobs | `jobOutcomesV1` in settings store (counts only) | +| **Destination (optional)** | Soft prompt after first success (Claude / ChatGPT / … / skip) | Local settings only | -If a `.docx` fails to convert, open **Settings → File Support** — the converter checklist shows what's missing. +**Privacy line on every stats surface:** *Stored only on this device. Never uploaded.* -**Token savings counter:** ParseKit tracks tokens saved locally (no telemetry) — a quiet line in the popover, full breakdown in Settings. Scanned pages get a separate **pages unlocked** stat rather than being folded into the token count. +See [`docs/TASKS.md`](docs/TASKS.md) for the PK-1…PK-5 checklist. -**First launch blocked?** Run once in Terminal: +### Dev install & tests ```bash -xattr -cr /Applications/ParseKit.app +npm ci +npm test +npm run check +npm run build +# Full app (needs sidecar build tooling): +npm run tauri:dev ``` -Or use **Settings → General → Copy fix command**, then open Privacy & Security. - ---- - -## Features - -- Local-first processing — files never leave your Mac -- Native macOS menu-bar app -- Markdown, plain text, or JSON export -- OCR for scanned documents -- Finder Quick Actions + macOS Services menu -- Global hotkey (⌃⇧M) and clipboard-to-Markdown -- `parsekit` CLI for scripts and agents -- Local token savings counter, no telemetry -- Optional floating progress HUD for background batches - -## Get ParseKit +CLI and agent skill: [AGENTS.md](AGENTS.md), [skills/parsekit/SKILL.md](skills/parsekit/SKILL.md). -**You don't need `git clone`.** End users install the DMG: +### Benchmarks & honesty -1. [Download the DMG](https://github.com/harshabala/parsekit/releases/latest/download/ParseKit_0.2.4_aarch64.dmg) (macOS 12+, Apple Silicon) -2. Open it → drag **ParseKit** to **Applications** -3. Open from Applications → look for the icon in your **menu bar** (top-right) +Measured token comparisons: [`docs/benchmark-results.md`](docs/benchmark-results.md) via [`scripts/benchmark_tokens.py`](scripts/benchmark_tokens.py). Estimates are guides, not provider invoices. -First-launch security steps: [docs/INSTALL.md](docs/INSTALL.md) - -## Privacy - -Everything runs locally — files, OCR, and conversion never touch a server. No cloud processing, no telemetry, no tracking. - -## For AI coding agents - -ParseKit ships an agent skill at [skills/parsekit/SKILL.md](skills/parsekit/SKILL.md). Point your agent at [AGENTS.md](AGENTS.md) so it knows to run `parsekit convert` before reading PDFs or Office files into context. - -```bash -parsekit convert /path/to/spec.pdf --out /tmp/spec.md -``` +--- -macOS Shortcuts / App Intents integration is planned for a later release. +## More detail -## For developers +### Finder Quick Actions -```bash -git clone https://github.com/harshabala/parsekit.git -cd parsekit -npm install -npm run build:sidecar -npm run tauri dev -``` +Right-click a supported file → **Quick Actions → Parse to Markdown with ParseKit**. Install from **Settings → General → Finder**. -Release notes: [docs/RELEASING.md](docs/RELEASING.md) +### Features -## Credits +- Local-first conversion (files never leave the Mac for parsing) +- Menu bar native app (Tauri 2) +- Markdown, plain text, or JSON +- OCR for scans +- Finder Quick Actions + Services +- Global hotkey and clipboard helpers +- `parsekit` CLI for agents and scripts +- Local token savings + job success scoreboard +- Optional floating progress HUD -Created and crafted by [Harsha Balakrishnan](https://github.com/harshabala). +### Privacy (summary) -Development help from Claude (Anthropic), Grok (xAI), and Gemini (Google) coding agents — see [docs/ACKNOWLEDGMENTS.md](docs/ACKNOWLEDGMENTS.md). +Conversion and OCR run locally. No conversion telemetry. Token and job counters are device-local. Optional network: app updates, OCR language data on first need. -Powered by [LiteParse v2](https://github.com/run-llama/liteparse) · [Tauri](https://tauri.app) · [Svelte](https://svelte.dev) +### License -Apache-2.0 — see [LICENSE](LICENSE) +See [LICENSE](LICENSE) and [NOTICE.md](NOTICE.md). diff --git a/docs/TASKS.md b/docs/TASKS.md new file mode 100644 index 0000000..a12afd2 --- /dev/null +++ b/docs/TASKS.md @@ -0,0 +1,24 @@ +# ParseKit — Flow + metrics tasks + +Branch: `feat/flow-metrics-2026-07` +Source: `~/Desktop/parsekit-task-list-plan.md` + +| ID | Task | Status | Notes | +|----|------|--------|-------| +| PK-1 | First successful convert as hero of first-run | **Done** | First-run hero on main panel; onboarding points to convert; activation constant | +| PK-2 | Today + all-time token summary scoreboard | **Done** | BatchScoreboard + banner period includes **today** | +| PK-3 | Failure path + success rate | **Done** | Job outcomes (last 20 jobs, local); partial/fail copy on scoreboard | +| PK-4 | Optional destination prompt | **Done** | Soft prompt after first success; skippable; local only | +| PK-5 | Privacy strip on stats surfaces | **Done** | Scoreboard + token panel + first-run copy | +| README | Novice + technical dual sections | **Done** | See README.md | +| Ship | PR against master | Pending | | + +## Activation metric + +- **Name:** `first_successful_convert_with_token_estimate` (`ACTIVATION_EVENT` in `src/lib/activation.ts`) +- **Rule:** ≥1 successful file in a batch (token estimate recorded for the batch, may be 0) +- **Storage:** `activatedAt` in local Tauri settings store — never uploaded + +## Privacy + +Token events and job outcome counts stay on this Mac. No file paths or names in aggregate metrics. diff --git a/src/App.svelte b/src/App.svelte index ddc3af5..55911d7 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -49,6 +49,19 @@ recordTokenSavingsFromSidecarEvent, type TokenStats, } from "./lib/tokenStats"; + import { + ACTIVATION_AT_KEY, + CONVERT_DESTINATION_KEY, + DESTINATION_PROMPT_KEY, + qualifiesForActivation, + type ConvertDestination, + } from "./lib/activation"; + import { + loadJobOutcomes, + persistJobOutcome, + successRate, + type JobOutcomesState, + } from "./lib/jobOutcomes"; import { applyTheme, DEFAULT_THEME, normalizeThemeMode } from "./lib/theme"; import DropZone from "./components/DropZone.svelte"; import OutputFolderPicker from "./components/OutputFolderPicker.svelte"; @@ -61,6 +74,7 @@ import UpdateBanner from "./components/UpdateBanner.svelte"; import TokenSavingsBanner from "./components/TokenSavingsBanner.svelte"; + import BatchScoreboard from "./components/BatchScoreboard.svelte"; import { updateState } from "./lib/updateState.svelte"; import { finderActionState } from "./lib/finderActionState.svelte"; import { pickOutputFolder } from "./lib/picker"; @@ -140,6 +154,10 @@ let showFloatingHud = $state(true); let isBackgroundBatch = $state(false); let hudActive = $state(false); + let activatedAt = $state(null); + let jobOutcomes = $state({ jobs: [] }); + let showDestinationPrompt = $state(false); + let showBatchScoreboard = $state(false); $effect(() => { if (updateState.available) { @@ -421,6 +439,8 @@ recentBatches = await getSetting("recentBatches", []); hasSuccessfulParse = await getSetting("hasSuccessfulParse", false); configCollapsed = hasSuccessfulParse; + activatedAt = await getSetting(ACTIVATION_AT_KEY, null); + jobOutcomes = await loadJobOutcomes(); // Popover opens from tray click after onboarding — not auto-shown on launch. await resolveDefaultWorkers(await getSetting("workers", 0)); launchAtLogin = await getSetting("launchAtLogin", false); @@ -446,7 +466,11 @@ "tokenStatsPeriod", DEFAULT_TOKEN_STATS_PERIOD, ); - if (tokenStatsPeriod !== "month" && tokenStatsPeriod !== "lifetime") { + if ( + tokenStatsPeriod !== "today" && + tokenStatsPeriod !== "month" && + tokenStatsPeriod !== "lifetime" + ) { tokenStatsPeriod = DEFAULT_TOKEN_STATS_PERIOD; await setSetting("tokenStatsPeriod", tokenStatsPeriod); } @@ -679,6 +703,8 @@ startParseStallWatchdog(); lastParsingId = null; batchTokenSavings = createBatchTokenSavings(); + showBatchScoreboard = false; + showDestinationPrompt = false; totalFiles = filesToParse.length; files = filesToParse.map((path) => ({ id: path, @@ -759,13 +785,38 @@ errorMsg = null; } void addToHistory(); - if (!hasSuccessfulParse) { + const parsed = files.filter((f) => f.status === "done").length; + const notifyErrors = files.filter((f) => f.status === "error").length; + if (!hasSuccessfulParse && parsed > 0) { hasSuccessfulParse = true; configCollapsed = true; void setSetting("hasSuccessfulParse", true); } - const parsed = files.filter((f) => f.status === "done").length; - const notifyErrors = files.filter((f) => f.status === "error").length; + // PK-2/3: local job outcomes + activation (no paths stored) + const tokensInBatch = batchTokenSavings.tokensSaved; + void persistJobOutcome({ + success: parsed, + failed: notifyErrors, + tokensSaved: tokensInBatch, + }).then((next) => { + jobOutcomes = next; + }); + if ( + qualifiesForActivation({ + alreadyActivated: !!activatedAt, + successCount: parsed, + tokensSavedInBatch: tokensInBatch, + }) + ) { + const at = new Date().toISOString(); + activatedAt = at; + void setSetting(ACTIVATION_AT_KEY, at); + } + // PK-4: soft destination prompt once after first success + void getSetting(DESTINATION_PROMPT_KEY, false).then((seen) => { + showDestinationPrompt = !seen && parsed > 0; + }); + showBatchScoreboard = parsed > 0 || notifyErrors > 0; void refreshTokenStats(); void syncHudIfActive(); void invoke("show_completion_notification", { @@ -853,6 +904,32 @@ await invoke("open_in_finder", { path }); } + function handleConvertMore() { + showBatchScoreboard = false; + files = []; + lastParsingId = null; + batchTokenSavings = createBatchTokenSavings(); + selectedFiles = []; + inputDir = ""; + inputFileCount = null; + errorMsg = null; + noticeMsg = null; + } + + async function handleDestination(value: ConvertDestination) { + showDestinationPrompt = false; + await setSetting(DESTINATION_PROMPT_KEY, true); + await setSetting(CONVERT_DESTINATION_KEY, value); + } + + const lastBatchSuccessCount = $derived( + files.filter((f) => f.status === "done").length, + ); + const lastBatchFailedCount = $derived( + files.filter((f) => f.status === "error").length, + ); + const jobSuccessRateLabel = $derived(successRate(jobOutcomes).label); + function handleKeydown(e: KeyboardEvent) { if ((e.metaKey || e.ctrlKey) && e.key === "r") { e.preventDefault(); @@ -1080,7 +1157,19 @@ {/if} {/if} - {#if !isParsing && files.length > 0 && files.some((f) => f.status === "done")} + {#if !isParsing && showBatchScoreboard && files.length > 0} + openFolder(outputDir)} + onConvertMore={handleConvertMore} + onDestination={handleDestination} + /> + {:else if !isParsing && files.length > 0 && files.some((f) => f.status === "done")}
{/if} + + {#if !isParsing && !hasSuccessfulParse && !showBatchScoreboard} +
+

{t("firstRun.title")}

+

{t("firstRun.body")}

+

{t("scoreboard.privacy")}

+
+ {/if} {#if !isParsing} diff --git a/src/components/BatchScoreboard.svelte b/src/components/BatchScoreboard.svelte new file mode 100644 index 0000000..a34d2c4 --- /dev/null +++ b/src/components/BatchScoreboard.svelte @@ -0,0 +1,235 @@ + + +
+

+ {#if allFailed} + {t("scoreboard.titleFailed")} + {:else if partialFail} + {t("scoreboard.titlePartial")} + {:else} + {t("scoreboard.titleSuccess")} + {/if} +

+ +
+ {formatTokenCount(batchTokens)} + {t("scoreboard.thisBatch")} + {#if batchTokens > 0} +

+ {t("scoreboard.chatApprox", { + count: formatTokenCount(approximateChatGptMessages(batchTokens)), + })} +

+ {/if} +
+ +
+
+
{t("scoreboard.today")}
+
{formatTokenCount(todayTokens)}
+
+
+
{t("scoreboard.allTime")}
+
{formatTokenCount(lifetimeTokens)}
+
+
+
{t("scoreboard.filesAllTime")}
+
{formatTokenCount(lifetimeFiles)}
+
+
+
{t("scoreboard.successRate")}
+
{successRateLabel}
+
+
+ + {#if partialFail || allFailed} +

+ {t("scoreboard.failHint", { + ok: successCount, + fail: failedCount, + })} +

+ {/if} + +

{t("scoreboard.method")}

+

{t("scoreboard.privacy")}

+ +
+ + +
+ + {#if showDestinationPrompt && successCount > 0} +
+

{t("scoreboard.destTitle")}

+
+ + + + + + +
+
+ {/if} + + {#if lifetimeTokens > 0} +

+ {t("tokenSavings.chatGptApprox", { count: formatTokenCount(chatApprox) })} + {t("tokenSavings.approximateLabel")} +

+ {/if} +
+ + diff --git a/src/components/OnboardingScreen.svelte b/src/components/OnboardingScreen.svelte index 7364a8c..1a6ecf6 100644 --- a/src/components/OnboardingScreen.svelte +++ b/src/components/OnboardingScreen.svelte @@ -179,6 +179,13 @@ +
+
+

{t("onboarding.firstConvertTitle")}

+

{t("onboarding.firstConvertBody")}

+
+
+
+ - - - - + {/each} + -
{/if} - {#if lifetimeTokens > 0} -

- {t("tokenSavings.chatGptApprox", { count: formatTokenCount(chatApprox) })} - {t("tokenSavings.approximateLabel")} -

- {/if} + +
+ {t("scoreboard.aboutEstimate")} +

{t("scoreboard.method")}

+

{t("scoreboard.privacy")}

+ {#if lifetimeTokens > 0} +

+ {t("tokenSavings.chatGptApprox", { count: formatTokenCount(chatApprox) })} + {t("tokenSavings.approximateLabel")} +

+ {/if} +
diff --git a/src/index.css b/src/index.css index 6dcf533..07b5afa 100644 --- a/src/index.css +++ b/src/index.css @@ -400,28 +400,40 @@ main { opacity: 1; } +/* First-run coach mark: calm, purpose-first, no confetti */ .first-run-hero { margin-top: 12px; - padding: 12px 14px; - border-radius: var(--radius-md, 10px); - border: 1px dashed var(--border, rgba(0, 0, 0, 0.12)); - background: var(--bg-elevated, transparent); + padding: 14px 14px 12px; + border-radius: 12px; + border: 1px solid color-mix(in srgb, var(--primary, #3a7a4a) 18%, var(--border, rgba(0, 0, 0, 0.1))); + background: color-mix(in srgb, var(--primary, #3a7a4a) 6%, var(--bg-elevated, transparent)); + box-shadow: 0 1px 0 rgba(255, 255, 255, 0.04) inset; } .first-run-hero-title { margin: 0 0 6px; - font-size: 0.9rem; + font-size: 0.92rem; font-weight: 600; + letter-spacing: -0.015em; + line-height: 1.25; } .first-run-hero-body, .first-run-hero-privacy { margin: 0; font-size: 0.75rem; line-height: 1.45; + letter-spacing: 0.005em; opacity: 0.78; } .first-run-hero-privacy { - margin-top: 8px; - opacity: 0.65; + margin-top: 10px; + opacity: 0.58; + font-size: 0.68rem; +} +@media (prefers-reduced-transparency: reduce) { + .first-run-hero { + background: var(--bg-elevated, #f6f6f6); + box-shadow: none; + } } .post-parse-actions { diff --git a/src/locales/en.json b/src/locales/en.json index 3c34c56..21a933a 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -261,24 +261,26 @@ }, "scoreboard": { "aria": "Conversion results and token savings", + "kicker": "Result", "titleSuccess": "Conversion complete", - "titlePartial": "Conversion finished with some errors", - "titleFailed": "Conversion failed", - "thisBatch": "Tokens saved this batch (estimate)", + "titlePartial": "Finished with some errors", + "titleFailed": "Couldn’t convert these files", + "thisBatch": "Tokens saved this batch", "chatApprox": "~{count} long ChatGPT messages of noise removed", "today": "Today", "allTime": "All time", "filesAllTime": "Files converted", - "successRate": "Success (last jobs)", + "successRate": "Success rate", "successRateHint": "Successful files vs failed among your last 20 jobs on this Mac", - "failHint": "{ok} succeeded · {fail} failed. Open the list above for details — retry the failed ones or try a different format.", - "method": "Estimate = tokens in a raw extract vs cleaned Markdown (roughly 4 characters per token). Different models tokenize differently — this is a guide, not a bill.", + "failHint": "{ok} succeeded · {fail} failed. Check the list above — retry or try another format.", + "aboutEstimate": "About this estimate", + "method": "Estimate compares a raw extract to cleaned Markdown (about 4 characters per token). Models differ — this is a guide, not a bill.", "privacy": "Stored only on this device. Never uploaded.", "convertMore": "Convert more", "destAria": "Where will you use this?", - "destTitle": "Where will you use this? (optional, stays on this Mac)", + "destTitle": "Where will you use this?", "destOther": "Other", - "destSkip": "Skip" + "destSkip": "Not now" }, "tokenSavings": { "bannerToday": "{count} tokens saved today", diff --git a/src/locales/es.json b/src/locales/es.json index dddaacc..7a770a1 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -259,24 +259,26 @@ }, "scoreboard": { "aria": "Resultados de conversión y ahorro de tokens", + "kicker": "Resultado", "titleSuccess": "Conversión completa", - "titlePartial": "Conversión terminada con algunos errores", - "titleFailed": "Conversión fallida", - "thisBatch": "Tokens ahorrados en este lote (estimación)", + "titlePartial": "Terminó con algunos errores", + "titleFailed": "No se pudieron convertir", + "thisBatch": "Tokens ahorrados en este lote", "chatApprox": "~{count} mensajes largos de ChatGPT de ruido eliminado", "today": "Hoy", "allTime": "Total", "filesAllTime": "Archivos convertidos", - "successRate": "Éxito (últimos trabajos)", + "successRate": "Tasa de éxito", "successRateHint": "Archivos correctos frente a fallidos en tus últimos 20 trabajos en este Mac", - "failHint": "{ok} correctos · {fail} fallidos. Revisa la lista arriba — reintenta o prueba otro formato.", - "method": "Estimación = tokens de un extracto crudo frente a Markdown limpio (~4 caracteres por token). Es una guía, no una factura.", + "failHint": "{ok} correctos · {fail} fallidos. Revisa la lista — reintenta o prueba otro formato.", + "aboutEstimate": "Sobre esta estimación", + "method": "Compara un extracto crudo con Markdown limpio (~4 caracteres por token). Es una guía, no una factura.", "privacy": "Solo en este dispositivo. Nunca se sube.", "convertMore": "Convertir más", "destAria": "¿Dónde lo usarás?", - "destTitle": "¿Dónde lo usarás? (opcional, solo en este Mac)", + "destTitle": "¿Dónde lo usarás?", "destOther": "Otro", - "destSkip": "Omitir" + "destSkip": "Ahora no" }, "tokenSavings": { "bannerToday": "{count} tokens ahorrados hoy", diff --git a/src/locales/zh.json b/src/locales/zh.json index 68b4c33..4509378 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -259,24 +259,26 @@ }, "scoreboard": { "aria": "转换结果与 token 节省", + "kicker": "结果", "titleSuccess": "转换完成", - "titlePartial": "转换完成,部分失败", - "titleFailed": "转换失败", - "thisBatch": "本批节省 token(估算)", + "titlePartial": "已完成,部分失败", + "titleFailed": "无法转换这些文件", + "thisBatch": "本批节省 token", "chatApprox": "约 {count} 条长 ChatGPT 消息的噪声被去掉", "today": "今天", "allTime": "累计", "filesAllTime": "已转换文件", - "successRate": "成功率(最近任务)", + "successRate": "成功率", "successRateHint": "本机最近 20 次任务中成功与失败的文件数", - "failHint": "{ok} 成功 · {fail} 失败。请查看上方列表,重试或更换格式。", - "method": "估算 = 原始提取文本与清理后 Markdown 的 token 差(约每 4 字符 1 token)。仅为参考,不是账单。", + "failHint": "{ok} 成功 · {fail} 失败。查看上方列表,重试或换格式。", + "aboutEstimate": "关于此估算", + "method": "比较原始提取与清理后 Markdown(约每 4 字符 1 token)。仅为参考,不是账单。", "privacy": "仅保存在本机,不会上传。", "convertMore": "继续转换", "destAria": "你将用在哪里?", - "destTitle": "你将用在哪里?(可选,仅本机)", + "destTitle": "你将用在哪里?", "destOther": "其他", - "destSkip": "跳过" + "destSkip": "暂时跳过" }, "tokenSavings": { "bannerToday": "今日已节省 {count} 个 token", From 7a8680916d7d7e96dccc60bc400cb7b9a7856dd6 Mon Sep 17 00:00:00 2001 From: harsha balakrishnan Date: Fri, 10 Jul 2026 17:22:03 +0530 Subject: [PATCH 3/3] =?UTF-8?q?chore(design):=20impeccable=20pipeline=20?= =?UTF-8?q?=E2=80=94=20DESIGN.md,=20quieter,=20polish?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document design system; remove side-tab accent and ghost-card elevation; align BatchScoreboard to tokens; persist critique snapshot and pipeline log. --- ...src-components-batchscoreboard-svelte.json | 35 ++++ DESIGN.md | 149 ++++++++++++++++++ docs/impeccable/PIPELINE-2026-07-10.md | 45 ++++++ src/components/BatchScoreboard.svelte | 64 ++++---- src/index.css | 28 ++-- 5 files changed, 274 insertions(+), 47 deletions(-) create mode 100644 .impeccable/critique/src-components-batchscoreboard-svelte.json create mode 100644 DESIGN.md create mode 100644 docs/impeccable/PIPELINE-2026-07-10.md diff --git a/.impeccable/critique/src-components-batchscoreboard-svelte.json b/.impeccable/critique/src-components-batchscoreboard-svelte.json new file mode 100644 index 0000000..c03ef76 --- /dev/null +++ b/.impeccable/critique/src-components-batchscoreboard-svelte.json @@ -0,0 +1,35 @@ +{ + "target": "src/components/BatchScoreboard.svelte", + "date": "2026-07-10", + "mode": "degraded-single-context", + "reason": "User requested sequential full pipeline; synthesis after detect+source review", + "score": 32, + "max": 40, + "p0": 0, + "p1": 2, + "p2": 3, + "strengths": [ + "Clear primary action (Open output)", + "Progressive disclosure for estimate method", + "Local privacy posture explicit" + ], + "priorityIssues": [ + {"sev": "P1", "id": "hero-metric-risk", "text": "Large number + label pattern can read as SaaS hero-metric if over-decorated; keep restrained"}, + {"sev": "P1", "id": "dest-option-count", "text": "Six destination choices is high decision load; optional should stay quiet"}, + {"sev": "P2", "id": "secondary-contrast", "text": "Watch text-secondary contrast on tinted surfaces in light mode"}, + {"sev": "P2", "id": "popover-density", "text": "Scoreboard + progress list + recent batches can stack tall in tray popover"}, + {"sev": "P2", "id": "chip-touch", "text": "Chip min-height 32 below 44px guideline for touch; acceptable for dense tray pointer UI"} + ], + "heuristics": { + "visibility_of_status": 3, + "match_system_world": 4, + "user_control": 4, + "consistency": 3, + "error_prevention": 3, + "recognition": 3, + "flexibility": 3, + "aesthetic_minimal": 3, + "error_recovery": 3, + "help": 3 + } +} diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..41a635f --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,149 @@ +--- +name: ParseKit +description: Menu-bar document converter — private, local, native macOS craft +colors: + ink: "#1B1713" + ink-secondary: "#6B5E52" + accent: "#3F3830" + accent-hover: "#322C26" + accent-fg: "#F9F7F2" + surface: "#F7F4EE" + surface-elevated: "#FFFFFF" + border: "#B8A99A" + link: "#5C4A3A" + status-success: "#2F8A4A" + status-error: "#C23B2E" + status-warning: "#C9892A" + dark-bg: "#1C1A17" + dark-ink: "#F5F2EB" + dark-accent: "#E8C99A" +typography: + display: + fontFamily: "Instrument Serif, Georgia, Times New Roman, serif" + fontSize: "1.25rem" + fontWeight: 400 + lineHeight: 1.2 + letterSpacing: "-0.015em" + body: + fontFamily: "-apple-system, BlinkMacSystemFont, SF Pro Text, Segoe UI, sans-serif" + fontSize: "0.875rem" + fontWeight: 400 + lineHeight: 1.4 + letterSpacing: "normal" + mono: + fontFamily: "JetBrains Mono, SF Mono, Menlo, monospace" + fontSize: "0.8125rem" + fontWeight: 500 + lineHeight: 1.3 + letterSpacing: "-0.01em" + label: + fontFamily: "-apple-system, BlinkMacSystemFont, SF Pro Text, Segoe UI, sans-serif" + fontSize: "0.72rem" + fontWeight: 500 + lineHeight: 1.25 + letterSpacing: "0.01em" +rounded: + sm: "6px" + md: "8px" + lg: "12px" + pill: "999px" +spacing: + xs: "4px" + sm: "8px" + md: "12px" + lg: "16px" + xl: "24px" +components: + button-primary: + backgroundColor: "{colors.accent}" + textColor: "{colors.accent-fg}" + rounded: "{rounded.md}" + padding: "8px 14px" + button-primary-hover: + backgroundColor: "{colors.accent-hover}" + textColor: "{colors.accent-fg}" + button-secondary: + backgroundColor: "transparent" + textColor: "{colors.ink}" + rounded: "{rounded.md}" + padding: "8px 12px" + chip: + backgroundColor: "transparent" + textColor: "{colors.ink}" + rounded: "{rounded.pill}" + padding: "5px 10px" + card: + backgroundColor: "{colors.surface-elevated}" + textColor: "{colors.ink}" + rounded: "{rounded.lg}" + padding: "14px" +--- + +## Overview + +ParseKit is a **product-register** macOS menu-bar tool: design serves conversion, not marketing spectacle. Visual system is **Bone White, Pressed** — warm restrained neutrals, charcoal primary, native SF Pro body, Instrument Serif only for display (app title / about). Motion is short (120–180ms), ease-out, interruptible, reduced-motion aware. Identity lives in craft density and privacy posture, not loud color. + +Canonical tokens: `src/index.css` (`:root`, `[data-theme]`, system dark). Brand narrative: `brand.md`. Product brief: `PRODUCT.md`. + +## Colors + +OKLCH is source of truth in CSS; hex above is sRGB approximation for tooling. + +| Role | Light (OKLCH) | Use | +|------|----------------|-----| +| Ink | `oklch(0.18 0.01 50)` | Body text | +| Secondary | `oklch(0.48 0.03 50)` | Hints, labels (keep ≥4.5:1 on surface) | +| Accent | `oklch(0.32 0.03 50)` | Primary buttons, focus | +| Accent FG | `oklch(0.98 0.005 85)` | Text on primary | +| Surface glass | `oklch(0.97 0.01 85)` + blur | Popover material | +| Border | `oklch(0.72 0.02 50 / 0.95)` | Hairlines | +| Success / Error / Warning | status-* tokens | Semantic only | + +Dark theme flips ink/surface and warms the accent (`oklch(0.78 0.08 75)`). Prefer **restrained** strategy: neutrals dominate; accent ≤10% of surface. + +## Typography + +- **UI / body:** system sans (`--font-sans`) — native macOS feel. +- **Display only:** Instrument Serif (`--font-serif`) for app name / about — not for buttons, data, or dense labels. +- **Data / counts:** JetBrains Mono (`--font-mono`), tabular-nums for token counts. +- Scale ratio ~1.15–1.2; display letter-spacing floor **≥ -0.04em** (hero metrics ~-0.03em). +- Prefer weight + size hierarchy over all-caps eyebrows on every section. + +## Elevation + +Mostly **tonal layering** (secondary-bg, glass) over heavy drop shadows. + +- Prefer: 1px border **or** soft shadow ≤8px blur — not both as decoration. +- Popover: glass material + restrained inset highlight. +- Focus: `--focus-ring` translucent accent ring (never remove). + +## Components + +| Component | Notes | +|-----------|--------| +| **Primary button** | `.run-parse-btn` / accent fill; `:active scale(0.97)` | +| **Secondary button** | Transparent / light border; quieter press scale | +| **Chips** | Pill outline for optional choices (destination) | +| **Scoreboard** | Post-convert result: one hero number, platter stats, progressive disclosure for method | +| **Drop zone** | Dominant affordance for first-run convert | +| **Segmented control** | Period toggle (today / month / lifetime) | +| **Banners** | Error/notice with fly enter; reduced-motion → fade | + +States required on interactive: default, hover, focus-visible, active, disabled. + +## Do's and Don'ts + +**Do** +- Keep conversion path obvious: drop → run → open output. +- Use local metrics language (estimate, privacy line). +- Animate transform/opacity only; honor `prefers-reduced-motion` and `prefers-reduced-transparency`. +- Align new UI to tokens in `src/index.css` before inventing hex. + +**Don't** +- Side-stripe accents (`border-left` >1px on cards/callouts). +- Hero-metric SaaS clichés with gradients and confetti. +- Nested cards; identical icon+title+body grids. +- Uppercase tracked kicker on every block. +- `scale(0)` entrances; bounce/elastic easing. +- Pair thick border + wide soft shadow (ghost-card). +- Put Instrument Serif on dense UI chrome. diff --git a/docs/impeccable/PIPELINE-2026-07-10.md b/docs/impeccable/PIPELINE-2026-07-10.md new file mode 100644 index 0000000..9de363a --- /dev/null +++ b/docs/impeccable/PIPELINE-2026-07-10.md @@ -0,0 +1,45 @@ +# Impeccable pipeline — ParseKit — 2026-07-10 + +Target: `feat/flow-metrics-2026-07` (BatchScoreboard + first-run + tokens) + +## Order run + +| # | Command | Result | +|---|---------|--------| +| 1 | **document** | Wrote `DESIGN.md` from tokens + brand | +| 2 | **quieter** | Removed side-tab `border-left: 3px` on `.progress-now`; dropped uppercase kicker; removed ghost-card shadow | +| 3 | **critique** | Snapshot `.impeccable/critique/src-components-batchscoreboard-svelte.json` — ⚠️ single-context (sequential pipeline). Score ~32/40. P1: hero-metric restraint, destination option load | +| 4 | **audit** | See below | +| 5 | **polish** | Token-only colors, mono for numbers, secondary via `--text-secondary`, focus-visible chips | +| 6 | **distill** | Estimate/privacy under details; dest chips quieter | +| 7 | **clarify** | Copy: titles shorter, “Not now”, about estimate | +| 8 | **harden** | Tone states success/warn/fail; fail hint uses status tokens | +| 9 | **onboard** | First-run hero already present; kept purpose-first coach mark | + +### Skipped (with reason) + +| Command | Why | +|---------|-----| +| init | PRODUCT.md exists | +| craft / shape | Feature already built | +| extract | DESIGN.md just captured; extract later if needed | +| bolder / overdrive / colorize / delight | Product register — would add noise | +| live | Dev server not running | +| adapt | Fixed tray popover width, not multi-breakpoint web | +| animate | Motion already intentional; no decorative pass | +| typeset / layout | Folded into polish | +| optimize | No perf flags from audit | + +## Audit health (code-level) + +| Dimension | Score | Key finding | +|-----------|-------|-------------| +| Accessibility | 3 | Labels/aria present; chip height 32px (tray OK) | +| Performance | 4 | transform/opacity motion only | +| Theming | 3 | Hard-coded colors removed from scoreboard; rest of app OK | +| Responsive | 3 | Popover density if stack tall | +| Anti-patterns | 3 | Side-tab fixed; residual hero-number pattern is intentional metric | +| **Total** | **16/20** | **Good** | + +## Anti-patterns verdict +Not “AI made that” after quieter pass. Remaining risk: large metric number — mitigated with mono type, no gradient, no confetti, progressive disclosure. diff --git a/src/components/BatchScoreboard.svelte b/src/components/BatchScoreboard.svelte index b26950e..ed91a7a 100644 --- a/src/components/BatchScoreboard.svelte +++ b/src/components/BatchScoreboard.svelte @@ -74,7 +74,6 @@ out:fade={fadeOut} >
-

{t("scoreboard.kicker")}

{#if allFailed} {t("scoreboard.titleFailed")} @@ -195,40 +194,32 @@ margin-top: 12px; padding: 16px 14px 12px; border-radius: 12px; - border: 1px solid var(--border, rgba(0, 0, 0, 0.08)); - background: var(--bg-elevated, var(--surface, #fff)); - /* Subtle depth — floating result, not a flat form */ - box-shadow: 0 1px 0 rgba(255, 255, 255, 0.06) inset, - 0 8px 24px rgba(0, 0, 0, 0.04); + /* Border OR soft elevation — not both (ghost-card ban) */ + border: 1px solid var(--border-color); + background: var(--glass-bg); + box-shadow: none; } .batch-scoreboard.tone-success { - border-color: color-mix(in srgb, var(--primary, #3a7a4a) 22%, var(--border, #ddd)); + border-color: color-mix(in srgb, var(--status-success) 35%, var(--border-color)); } .batch-scoreboard.tone-warn { - border-color: color-mix(in srgb, #c9892a 28%, var(--border, #ddd)); + border-color: color-mix(in srgb, var(--status-warning) 40%, var(--border-color)); } .batch-scoreboard.tone-fail { - border-color: color-mix(in srgb, var(--destructive, #c44) 30%, var(--border, #ddd)); + border-color: color-mix(in srgb, var(--status-error) 40%, var(--border-color)); } .batch-scoreboard-header { margin-bottom: 10px; } - .batch-scoreboard-kicker { - margin: 0 0 2px; - font-size: 0.65rem; - font-weight: 600; - letter-spacing: 0.06em; - text-transform: uppercase; - opacity: 0.55; - } .batch-scoreboard-title { margin: 0; font-size: 1.05rem; font-weight: 600; letter-spacing: -0.015em; line-height: 1.2; + font-family: var(--font-sans, system-ui, sans-serif); } .batch-scoreboard-hero { @@ -240,22 +231,24 @@ display: block; font-size: 2.1rem; font-weight: 650; + font-family: var(--font-mono); font-variant-numeric: tabular-nums; - letter-spacing: -0.03em; /* large display: negative tracking */ + letter-spacing: -0.03em; line-height: 1.05; + color: var(--text-primary); } .batch-scoreboard-hero-label { display: block; margin-top: 4px; font-size: 0.72rem; letter-spacing: 0.01em; - opacity: 0.62; + color: var(--text-secondary); line-height: 1.3; } .batch-scoreboard-hero-sub { margin: 6px 0 0; font-size: 0.72rem; - opacity: 0.7; + color: var(--text-secondary); line-height: 1.35; } @@ -268,12 +261,12 @@ .batch-scoreboard-stat { padding: 8px 10px; border-radius: 8px; - background: color-mix(in srgb, var(--fg-base, #111) 4%, transparent); + background: var(--secondary-bg, color-mix(in srgb, var(--text-color) 5%, transparent)); } .batch-scoreboard-stats dt { font-size: 0.65rem; letter-spacing: 0.02em; - opacity: 0.58; + color: var(--text-secondary); margin: 0; line-height: 1.2; } @@ -281,9 +274,11 @@ margin: 3px 0 0; font-size: 0.95rem; font-weight: 600; + font-family: var(--font-mono); font-variant-numeric: tabular-nums; letter-spacing: -0.01em; line-height: 1.15; + color: var(--text-primary); } .batch-scoreboard-fail-hint { @@ -292,10 +287,11 @@ border-radius: 8px; font-size: 0.72rem; line-height: 1.4; - background: color-mix(in srgb, #c9892a 12%, transparent); + color: var(--text-primary); + background: color-mix(in srgb, var(--status-warning) 14%, var(--secondary-bg)); } .tone-fail .batch-scoreboard-fail-hint { - background: color-mix(in srgb, var(--destructive, #c44) 12%, transparent); + background: color-mix(in srgb, var(--status-error) 12%, var(--secondary-bg)); } .batch-scoreboard-actions { @@ -312,7 +308,7 @@ .batch-scoreboard-dest { margin-top: 14px; padding-top: 12px; - border-top: 1px solid color-mix(in srgb, var(--fg-base, #111) 8%, transparent); + border-top: 1px solid var(--border-color); } .batch-scoreboard-dest-title { margin: 0 0 8px; @@ -328,21 +324,27 @@ } .batch-scoreboard-chip { margin: 0; - padding: 5px 10px; + padding: 6px 12px; + min-height: 32px; border-radius: 999px; - border: 1px solid color-mix(in srgb, var(--fg-base, #111) 12%, transparent); + border: 1px solid var(--border-color); background: transparent; + color: var(--text-primary); font-size: 0.72rem; font-weight: 500; letter-spacing: -0.01em; cursor: pointer; transition: - transform 100ms ease-out, - background-color 120ms ease-out, - border-color 120ms ease-out; + transform 100ms var(--easing-decelerate, ease-out), + background-color 120ms var(--easing-standard, ease), + border-color 120ms var(--easing-standard, ease); } .batch-scoreboard-chip:hover { - background: color-mix(in srgb, var(--fg-base, #111) 5%, transparent); + background: var(--secondary-bg); + } + .batch-scoreboard-chip:focus-visible { + outline: 2px solid var(--focus-ring); + outline-offset: 2px; } .batch-scoreboard-chip:active { transform: scale(0.97); /* press feedback on pointer-down path */ diff --git a/src/index.css b/src/index.css index 07b5afa..9612747 100644 --- a/src/index.css +++ b/src/index.css @@ -405,9 +405,8 @@ main { margin-top: 12px; padding: 14px 14px 12px; border-radius: 12px; - border: 1px solid color-mix(in srgb, var(--primary, #3a7a4a) 18%, var(--border, rgba(0, 0, 0, 0.1))); - background: color-mix(in srgb, var(--primary, #3a7a4a) 6%, var(--bg-elevated, transparent)); - box-shadow: 0 1px 0 rgba(255, 255, 255, 0.04) inset; + border: 1px solid var(--border-color); + background: var(--secondary-bg); } .first-run-hero-title { margin: 0 0 6px; @@ -415,25 +414,21 @@ main { font-weight: 600; letter-spacing: -0.015em; line-height: 1.25; + color: var(--text-primary); } -.first-run-hero-body, -.first-run-hero-privacy { +.first-run-hero-body { margin: 0; font-size: 0.75rem; line-height: 1.45; - letter-spacing: 0.005em; - opacity: 0.78; + color: var(--text-secondary); + max-width: 42ch; } .first-run-hero-privacy { - margin-top: 10px; - opacity: 0.58; + margin: 10px 0 0; font-size: 0.68rem; -} -@media (prefers-reduced-transparency: reduce) { - .first-run-hero { - background: var(--bg-elevated, #f6f6f6); - box-shadow: none; - } + line-height: 1.4; + color: var(--text-secondary); + opacity: 0.9; } .post-parse-actions { @@ -667,7 +662,8 @@ label { padding: 8px 10px; border-radius: 8px; background: color-mix(in srgb, var(--accent-color) 8%, var(--secondary-bg)); - border-left: 3px solid var(--accent-color); + /* Full hairline only — no side-stripe accent (impeccable ban) */ + border: 1px solid color-mix(in srgb, var(--accent-color) 22%, var(--border-color)); min-width: 0; }