Buttons: some action pills have outlines and some don't — all should be borderless #1029

Closed
opened 2026-10-04 04:41:33 +00:00 by kayg · 8 comments
Owner

Owner report (2026-10-04, production Settings → Mail, screenshot)

"Some buttons have borders and some don't? The ones that don't have it is correct!"
Row: "Sync now" (primary, no border), "Folders and backfill" (bordered), "Manage account" (no border), "Copy link" (bordered). Owner rule: action pills have NO outline.

Cause (likely)

packages/ui/src/components/Pill.svelte default tone draws border: 1px solid var(--pill-border, var(--glass-border)); the primary, secondary and quiet tones set --pill-border: transparent. Pills rendered with the default tone (or as <button> vs <span> inside <a>) end up bordered, so the same row mixes styles.

Fix (one shared component, no per-site overrides)

  • Action pills are borderless in every tone, with the filled tint giving the shape (secondary tint by default); keep a visible focus ring (#658) and sufficient contrast in light/dark/glass (check WCAG non-text contrast ≥ 3:1 for the filled shape against its background; if a glass background makes a fill too faint, raise the fill token, not a border).
  • Audit every Pill/button use app-wide (Settings, Mail, Files, Calendar, Notes, Photos, Money, Admin, dialogs) for remaining outlines; remove per-site border overrides; segmented controls and input fields are not action pills (leave them).
  • Add a guard test that fails if an action pill renders a non-transparent border.

Verification

Screenshots before/after of Settings → Mail, Connected Accounts, a Files toolbar, a Calendar header, a dialog footer, light/dark, 390/820/1440, 3x crops of a mixed row.

## Owner report (2026-10-04, production Settings → Mail, screenshot) "Some buttons have borders and some don't? The ones that don't have it is correct!" Row: "Sync now" (primary, no border), "Folders and backfill" (bordered), "Manage account" (no border), "Copy link" (bordered). Owner rule: action pills have NO outline. ## Cause (likely) `packages/ui/src/components/Pill.svelte` default tone draws `border: 1px solid var(--pill-border, var(--glass-border))`; the `primary`, `secondary` and `quiet` tones set `--pill-border: transparent`. Pills rendered with the default tone (or as `<button>` vs `<span>` inside `<a>`) end up bordered, so the same row mixes styles. ## Fix (one shared component, no per-site overrides) - Action pills are borderless in every tone, with the filled tint giving the shape (secondary tint by default); keep a visible focus ring (#658) and sufficient contrast in light/dark/glass (check WCAG non-text contrast ≥ 3:1 for the filled shape against its background; if a glass background makes a fill too faint, raise the fill token, not a border). - Audit every Pill/button use app-wide (Settings, Mail, Files, Calendar, Notes, Photos, Money, Admin, dialogs) for remaining outlines; remove per-site border overrides; segmented controls and input fields are not action pills (leave them). - Add a guard test that fails if an action pill renders a non-transparent border. ## Verification Screenshots before/after of Settings → Mail, Connected Accounts, a Files toolbar, a Calendar header, a dialog footer, light/dark, 390/820/1440, 3x crops of a mixed row.
Author
Owner

Starting work on branch job/pillborder-1029, based on f2f8491ff5 (origin/dev). I am tracing the shared Pill styles, all Pill uses and local border overrides before implementation.

Starting work on branch job/pillborder-1029, based on f2f8491ff5c76ab28f140c964542c97362e6b119 (origin/dev). I am tracing the shared Pill styles, all Pill uses and local border overrides before implementation.
Author
Owner

Finding: packages/ui/src/components/Pill.svelte gave default actions a --control-boundary outline, while primary/secondary/quiet tones hid it. The app-wide scan also found six Manage account(s) links in Mail and Calendars built as an anchor around a static , so they did not receive shared action styling. I am moving those links onto Pill's anchor root and making the shared action border transparent; static status/value chips, segmented controls and native selects keep their own treatment.

Finding: packages/ui/src/components/Pill.svelte gave default actions a --control-boundary outline, while primary/secondary/quiet tones hid it. The app-wide scan also found six Manage account(s) links in Mail and Calendars built as an anchor around a static <Pill as=span>, so they did not receive shared action styling. I am moving those links onto Pill's anchor root and making the shared action border transparent; static status/value chips, segmented controls and native selects keep their own treatment.
Author
Owner

Completed

Fixed #1029 in commit 145579c49e0cadc8b6ce45cdee5848fb449eef3d on job/pillborder-1029.

Pill action roots now keep a transparent border in every tone and material. Default and secondary actions use the shared neutral fill; the separate two-band focus ring remains. The Settings → Mail and Calendars account links now use Pill as the root anchor. Per-site action border overrides were removed. Remaining app --pill-border values apply only to static status or value chips.

Files

  • packages/ui/src/components/Pill.svelte
  • apps/web/src/lib/components/AppToaster.svelte
  • apps/web/src/lib/components/analytics/widgets/StatRow.svelte
  • apps/web/src/lib/themes.test.ts
  • apps/web/src/lib/components/Pill.svelte.test.ts
  • apps/web/src/routes/settings/mail/MailSection.svelte
  • apps/web/src/routes/settings/calendars/CalendarsSection.svelte
  • apps/web/e2e/pillborder-1029.mjs

Verification

bun run check output:

$ node scripts/check-user-storage.mjs && node scripts/check-type-tokens.mjs && node scripts/check-motion-tokens.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json
User browser caches use userStorage; only documented device/public-link exceptions remain.
Text sizes and UI shape values use shared role tokens.
UI transitions and animation options use shared motion tokens or documented exceptions.
Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/pillborder-1029/apps/web
Getting Svelte diagnostics...

svelte-check found 0 errors and 0 warnings

Focused Vitest output (Pill.svelte.test.ts, themes.test.ts, MailSection.svelte.test.ts, CalendarsSection.svelte.test.ts):

 RUN  v5.0.1 /home/kayg/Developer/calternal-wt/pillborder-1029/apps/web

 Test Files  4 passed (4)
      Tests  84 passed (84)
   Start at  07:26:39
   Duration  15.03s (transform 61%, tests 21%, environment 9%, import 7%, setup 2%)

  Transform  |component| transforming modules took 10.46s · 68% of tracked time, re-done on every run
             persist transforms across runs with fsModuleCache: true
             learn more: https://vitest.dev/guide/improving-performance#caching-between-reruns

Production build succeeded (✓ built in 23.41s; Wrote site to "build"; ✔ done). The production guard passed on 36 captures: Mail, Connected Accounts, Files, Calendar and a provider dialog at 390, 820 and 1440 px, in light and dark; the mixed Mail row also has a 3× crop. node --check apps/web/e2e/pillborder-1029.mjs and git diff --check returned exit 0 without output.

PASS Pill border guard: 36 production screenshots saved under /home/kayg/Developer/calternal-wt/pillborder-1029/artifacts/pillborder-1029

No Rust source changed, so Rust crate gates did not apply. cargo clean completed:

     Removed 7238 files, 4.6GiB total

Screenshot evidence

Full captures, grouped by viewport (each archive includes all five surfaces in light and dark): Phone, 390 px · Tablet, 820 px · Desktop, 1440 px.

3× crops of the mixed Mail action row:

Phone light, 390 px
Phone dark, 390 px
Tablet light, 820 px
Tablet dark, 820 px
Desktop light, 1440 px
Desktop dark, 1440 px

UX gaps

Closed: mixed default/primary/link actions now share the Pill border contract; account links are actionable Pill anchors; keyboard focus rings remain visible. No functional gap was found in the checked surfaces. Visual review is left to the screenshot reviewer.

Decisions

DESIGN §34 defines borderless action Pills but does not pick the neutral fill. Default and secondary use --control-boundary with --paper text; the theme test checks text contrast and the filled shape against shared materials across the palettes and light-glass transforms. Static status and value chips keep their existing material treatment because they are not actions.

Known gaps

The production build succeeds with Rollup use client directive warnings from existing dependencies. No Rust files changed.

## Completed Fixed #1029 in commit `145579c49e0cadc8b6ce45cdee5848fb449eef3d` on `job/pillborder-1029`. `Pill` action roots now keep a transparent border in every tone and material. Default and secondary actions use the shared neutral fill; the separate two-band focus ring remains. The Settings → Mail and Calendars account links now use Pill as the root anchor. Per-site action border overrides were removed. Remaining app `--pill-border` values apply only to static status or value chips. ## Files - `packages/ui/src/components/Pill.svelte` - `apps/web/src/lib/components/AppToaster.svelte` - `apps/web/src/lib/components/analytics/widgets/StatRow.svelte` - `apps/web/src/lib/themes.test.ts` - `apps/web/src/lib/components/Pill.svelte.test.ts` - `apps/web/src/routes/settings/mail/MailSection.svelte` - `apps/web/src/routes/settings/calendars/CalendarsSection.svelte` - `apps/web/e2e/pillborder-1029.mjs` ## Verification `bun run check` output: ```text $ node scripts/check-user-storage.mjs && node scripts/check-type-tokens.mjs && node scripts/check-motion-tokens.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json User browser caches use userStorage; only documented device/public-link exceptions remain. Text sizes and UI shape values use shared role tokens. UI transitions and animation options use shared motion tokens or documented exceptions. Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/pillborder-1029/apps/web Getting Svelte diagnostics... svelte-check found 0 errors and 0 warnings ``` Focused Vitest output (`Pill.svelte.test.ts`, `themes.test.ts`, `MailSection.svelte.test.ts`, `CalendarsSection.svelte.test.ts`): ```text RUN v5.0.1 /home/kayg/Developer/calternal-wt/pillborder-1029/apps/web Test Files 4 passed (4) Tests 84 passed (84) Start at 07:26:39 Duration 15.03s (transform 61%, tests 21%, environment 9%, import 7%, setup 2%) Transform |component| transforming modules took 10.46s · 68% of tracked time, re-done on every run persist transforms across runs with fsModuleCache: true learn more: https://vitest.dev/guide/improving-performance#caching-between-reruns ``` Production build succeeded (`✓ built in 23.41s`; `Wrote site to "build"`; `✔ done`). The production guard passed on 36 captures: Mail, Connected Accounts, Files, Calendar and a provider dialog at 390, 820 and 1440 px, in light and dark; the mixed Mail row also has a 3× crop. `node --check apps/web/e2e/pillborder-1029.mjs` and `git diff --check` returned exit 0 without output. ```text PASS Pill border guard: 36 production screenshots saved under /home/kayg/Developer/calternal-wt/pillborder-1029/artifacts/pillborder-1029 ``` No Rust source changed, so Rust crate gates did not apply. `cargo clean` completed: ```text Removed 7238 files, 4.6GiB total ``` ## Screenshot evidence Full captures, grouped by viewport (each archive includes all five surfaces in light and dark): [Phone, 390 px](https://git.kayg.org/attachments/fd29355b-623e-487b-9182-437f868eae1f) · [Tablet, 820 px](https://git.kayg.org/attachments/5777874b-d9ec-4395-a8c4-103f0b11ac14) · [Desktop, 1440 px](https://git.kayg.org/attachments/64eaccb3-94a1-442e-83b8-c684c0273a2e). 3× crops of the mixed Mail action row: ![Phone light, 390 px](https://git.kayg.org/attachments/a35d3413-a3fe-41de-b853-32b3e9de81ff) ![Phone dark, 390 px](https://git.kayg.org/attachments/2d87a202-ee6f-41d0-93e9-7bd3ae1ebe9e) ![Tablet light, 820 px](https://git.kayg.org/attachments/9ea9715e-7986-4b6d-aa6c-84dec281156d) ![Tablet dark, 820 px](https://git.kayg.org/attachments/d7feeddc-7da4-488c-9c5c-1758d0521c10) ![Desktop light, 1440 px](https://git.kayg.org/attachments/bdf98596-75d9-4e0a-a0f6-2f5081fd1044) ![Desktop dark, 1440 px](https://git.kayg.org/attachments/67669c26-6bc2-475d-9edd-170a5aa8b735) ## UX gaps Closed: mixed default/primary/link actions now share the Pill border contract; account links are actionable Pill anchors; keyboard focus rings remain visible. No functional gap was found in the checked surfaces. Visual review is left to the screenshot reviewer. ## Decisions DESIGN §34 defines borderless action Pills but does not pick the neutral fill. Default and secondary use `--control-boundary` with `--paper` text; the theme test checks text contrast and the filled shape against shared materials across the palettes and light-glass transforms. Static status and value chips keep their existing material treatment because they are not actions. ## Known gaps The production build succeeds with Rollup `use client` directive warnings from existing dependencies. No Rust files changed.
Author
Owner

Starting the #1029 send-back fix on branch job/pillborder-1029, based on dev SHA f2f8491ff5. The existing borderless-pill fix is at 145579c49; I’m adjusting the secondary surface tint and will recapture the Settings → Mail screenshots.

Starting the #1029 send-back fix on branch job/pillborder-1029, based on dev SHA f2f8491ff5c76ab28f140c964542c97362e6b119. The existing borderless-pill fix is at 145579c49; I’m adjusting the secondary surface tint and will recapture the Settings → Mail screenshots.
Author
Owner

Finding from the returned crop: the shared default action style still uses --control-boundary for fill and --paper for text. In dark themes --control-boundary resolves to --muted, which is a high contrast control edge; it creates the bright lavender block, and --paper is the dark background color, so the text is dark. I’m switching neutral actions to --paper-2/--ink in light themes and --raised/--ink in dark themes, keeping primary on --accent.

Finding from the returned crop: the shared default action style still uses --control-boundary for fill and --paper for text. In dark themes --control-boundary resolves to --muted, which is a high contrast control edge; it creates the bright lavender block, and --paper is the dark background color, so the text is dark. I’m switching neutral actions to --paper-2/--ink in light themes and --raised/--ink in dark themes, keeping primary on --accent.
Author
Owner

The first focused theme run caught a real hover contrast edge: blending the dark raised fill toward --ink lowered the Frappé label contrast to 4.26:1. Dark hover now deepens the subtle fill toward --paper; light hover still darkens toward --ink. The theme guard covers normal and hover text across all palettes.

The first focused theme run caught a real hover contrast edge: blending the dark raised fill toward --ink lowered the Frappé label contrast to 4.26:1. Dark hover now deepens the subtle fill toward --paper; light hover still darkens toward --ink. The theme guard covers normal and hover text across all palettes.
Author
Owner

Completed

Updated the shared neutral Pill tone. Light themes use --paper-2 with --ink; dark themes use --raised with --ink, which gives a quiet lift over the card. Primary actions keep the accent fill. The theme guard checks 4.5:1 text contrast and allows the text-labelled control tint to stay below 3:1 against the card, as requested and documented in DESIGN §34.

Head SHA: ee70b6e4bc26cb26a840a2b841133cb5917bcc2d

Files

  • packages/ui/src/components/Pill.svelte
  • apps/web/src/lib/components/Pill.svelte.test.ts
  • apps/web/src/lib/themes.test.ts
  • apps/web/e2e/pillborder-1029.mjs
  • docs/DESIGN.md

Verification

cargo fmt --check exited 0 with no output.

bun run check output:

$ node scripts/check-user-storage.mjs && node scripts/check-type-tokens.mjs && node scripts/check-motion-tokens.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json
User browser caches use userStorage; only documented device/public-link exceptions remain.
Text sizes and UI shape values use shared role tokens.
UI transitions and animation options use shared motion tokens or documented exceptions.
Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/pillborder-1029/apps/web
Getting Svelte diagnostics...

svelte-check found 0 errors and 0 warnings

Focused Vitest output:

 RUN  v5.0.1 /home/kayg/Developer/calternal-wt/pillborder-1029/apps/web

 Test Files  2 passed (2)
      Tests  77 passed (77)
   Start at  08:05:08
   Duration  11.63s (transform 70%, tests 21%, import 5%, environment 4%, setup 1%)

  Transform  |component| transforming modules took 8.30s · 84% of tracked time, re-done on every run
             persist transforms across runs with fsModuleCache: true
             learn more: https://vitest.dev/guide/improving-performance#caching-between-reruns

Production build output:

✓ built in 33.55s
> Using @sveltejs/adapter-static
  Wrote site to "build"
  ✔ done

Production screenshot guard:

PASS Pill border guard: 36 production screenshots saved under /home/kayg/Developer/calternal-wt/pillborder-1029/artifacts/pillborder-1029

Rust source was unchanged by this job, so clippy and Rust tests were not run. The local server build was needed because the shared CURRENT binary was absent. Cleanup completed:

     Removed 7238 files, 4.6GiB total

UX gaps closed

  • Neutral Mail row actions now use the same quiet, borderless role fill in light and dark themes.
  • The guard covers text contrast, keeps the primary role on the accent, and checks the border in production screenshots.
  • Captures cover Mail, Connected Accounts, Files, Calendar and a provider dialog at 390, 820 and 1440 px in both themes. The existing runner emulates macOS.

UX gaps left / known gaps

No gap remains for this visual fix. The full web suite and Rust clippy/tests remain for the merge round under the verification policy. The production build still emits existing vendor use client directive warnings.

Decisions

DESIGN §34 did not define the exact neutral fill roles. I chose --paper-2 / --ink for light themes and --raised / --ink for dark themes. The text label identifies the action, so the subtle fill may be below 3:1 against the card; text stays at 4.5:1 against the fill. This choice is recorded in DESIGN §34.

Screenshot evidence

Full captures, grouped by viewport (each archive contains five screens in both themes and the two Mail row crops):

3× Mail action crops:

## Completed Updated the shared neutral Pill tone. Light themes use `--paper-2` with `--ink`; dark themes use `--raised` with `--ink`, which gives a quiet lift over the card. Primary actions keep the accent fill. The theme guard checks 4.5:1 text contrast and allows the text-labelled control tint to stay below 3:1 against the card, as requested and documented in DESIGN §34. Head SHA: `ee70b6e4bc26cb26a840a2b841133cb5917bcc2d` ### Files - `packages/ui/src/components/Pill.svelte` - `apps/web/src/lib/components/Pill.svelte.test.ts` - `apps/web/src/lib/themes.test.ts` - `apps/web/e2e/pillborder-1029.mjs` - `docs/DESIGN.md` ### Verification `cargo fmt --check` exited 0 with no output. `bun run check` output: ```text $ node scripts/check-user-storage.mjs && node scripts/check-type-tokens.mjs && node scripts/check-motion-tokens.mjs && svelte-kit sync && svelte-check --tsconfig ./tsconfig.json User browser caches use userStorage; only documented device/public-link exceptions remain. Text sizes and UI shape values use shared role tokens. UI transitions and animation options use shared motion tokens or documented exceptions. Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/pillborder-1029/apps/web Getting Svelte diagnostics... svelte-check found 0 errors and 0 warnings ``` Focused Vitest output: ```text RUN v5.0.1 /home/kayg/Developer/calternal-wt/pillborder-1029/apps/web Test Files 2 passed (2) Tests 77 passed (77) Start at 08:05:08 Duration 11.63s (transform 70%, tests 21%, import 5%, environment 4%, setup 1%) Transform |component| transforming modules took 8.30s · 84% of tracked time, re-done on every run persist transforms across runs with fsModuleCache: true learn more: https://vitest.dev/guide/improving-performance#caching-between-reruns ``` Production build output: ```text ✓ built in 33.55s > Using @sveltejs/adapter-static Wrote site to "build" ✔ done ``` Production screenshot guard: ```text PASS Pill border guard: 36 production screenshots saved under /home/kayg/Developer/calternal-wt/pillborder-1029/artifacts/pillborder-1029 ``` Rust source was unchanged by this job, so clippy and Rust tests were not run. The local server build was needed because the shared `CURRENT` binary was absent. Cleanup completed: ```text Removed 7238 files, 4.6GiB total ``` ### UX gaps closed - Neutral Mail row actions now use the same quiet, borderless role fill in light and dark themes. - The guard covers text contrast, keeps the primary role on the accent, and checks the border in production screenshots. - Captures cover Mail, Connected Accounts, Files, Calendar and a provider dialog at 390, 820 and 1440 px in both themes. The existing runner emulates macOS. ### UX gaps left / known gaps No gap remains for this visual fix. The full web suite and Rust clippy/tests remain for the merge round under the verification policy. The production build still emits existing vendor `use client` directive warnings. ### Decisions DESIGN §34 did not define the exact neutral fill roles. I chose `--paper-2` / `--ink` for light themes and `--raised` / `--ink` for dark themes. The text label identifies the action, so the subtle fill may be below 3:1 against the card; text stays at 4.5:1 against the fill. This choice is recorded in DESIGN §34. ### Screenshot evidence Full captures, grouped by viewport (each archive contains five screens in both themes and the two Mail row crops): - [Phone, 390 px](https://git.kayg.org/attachments/53fd7c79-dc32-430e-bfab-ad542bbb5ec2) - [Tablet, 820 px](https://git.kayg.org/attachments/26d1d02f-1504-460d-a02b-125a666dd4c8) - [Desktop, 1440 px](https://git.kayg.org/attachments/6091d15f-aad0-442a-a7f1-f80d739da20a) 3× Mail action crops: - [Phone light](https://git.kayg.org/attachments/99628fd5-7c4b-4e9f-9140-1f64e09bf694) · [Phone dark](https://git.kayg.org/attachments/1c8fd87a-6afe-42aa-8d1e-3b423cf9401b) - [Tablet light](https://git.kayg.org/attachments/7e6b6454-b48d-4a80-a640-73319a310557) · [Tablet dark](https://git.kayg.org/attachments/710d8592-678f-4b69-8731-fe0c8ed34386) - [Desktop light](https://git.kayg.org/attachments/b6c22e0e-58fe-4dc2-b8b6-9aa7de949a2f) · [Desktop dark](https://git.kayg.org/attachments/9c2a1549-db50-4b73-974d-d6917fb1c2a2)
Author
Owner

Deployed to production (2026-10-04 ~08:56 IST, small round 4 = 6074f71d1, with #1029)

Gates on the round branch: web check COMPLETED 2002 FILES 0 ERRORS 0 WARNINGS, web tests Tests 1114 passed (1114), cargo fmt --check clean, clippy clean for calternal-plugin-notes and calternal-server, tests 193 passed / 163 passed, 0 failed. Staging healthy first.

Production check (read-only aggregate): note_calendar_logs went from 7,212 rows over 505 days to 7,804 rows over 542 days (+592 rows, +37 days), matching the 37 headingless Daily notes. Root cause was repeated Area Tags in legacy Daily notes, which stopped the resumable rebuild at a unique membership key; memberships are now a set.

## Deployed to production (2026-10-04 ~08:56 IST, small round 4 = 6074f71d1, with #1029) Gates on the round branch: web check `COMPLETED 2002 FILES 0 ERRORS 0 WARNINGS`, web tests `Tests 1114 passed (1114)`, `cargo fmt --check` clean, clippy clean for calternal-plugin-notes and calternal-server, tests 193 passed / 163 passed, 0 failed. Staging healthy first. Production check (read-only aggregate): `note_calendar_logs` went from 7,212 rows over 505 days to **7,804 rows over 542 days** (+592 rows, +37 days), matching the 37 headingless Daily notes. Root cause was repeated Area Tags in legacy Daily notes, which stopped the resumable rebuild at a unique membership key; memberships are now a set.
kayg closed this issue 2026-10-04 06:58:41 +00:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#1029
No description provided.