Skip to content

Commit 4c5237c

Browse files
nicktrnclaude
andauthored
feat(webapp): themes refinement, new black & white themes, 2 accessibility toggles (#4547)
## What this does Rounds out the theme work behind the existing `hasThemeSwitcher` flag. **Two new themes.** Black and White sit alongside Dark and Light. They inherit their neighbour's whole token set and only pin their surfaces flat, so sections are separated by grid lines rather than layered fills. **`System` is now configurable at both ends.** You choose which theme the OS light setting lands on (Light or White) and which the dark setting lands on (Dark or Black). **Two accessibility toggles.** - *Stronger colors* — swaps tinted status chips for solid fills, drops decorative icon accents to monochrome, and darkens chart series that didn't clear 3:1 on a white plot. - *Underline links* — underlines body-text links, so an underline always means the preference is on rather than being a hover style. **Contrast slider.** Stores a 0–100 position within the active theme's own range rather than a shared scale, so 35% stays 35% when you switch themes. Each theme maps it in CSS, which keeps `system` working before hydration. **Appearance in the account popover.** A submenu listing the themes with a check against the current one, plus a link through to the full set on your profile. Picking one applies immediately rather than waiting for the write to round-trip. **Profile page.** Each row now saves on its own — no submit button. Name and email show their value inline with an edit button; the email row is read-only when an identity provider owns the address. **A `/storybook/colors` audit page.** Renders every colour-carrying pattern in the app once per theme plus once under Stronger colors, and measures contrast ratios off the live DOM rather than a hard-coded table, so it can't go stale. --- ## Demo https://github.com/user-attachments/assets/d56cd4d8-719f-4ec5-a990-e04cdb98def1 --- ## Compatibility The stored preference shape is unchanged (`version: "1"`), and the four new fields are all optional. The retired `classic` theme falls back to Dark, whose palette at contrast 0 is what Classic shipped. One deliberate change worth knowing: the default contrast moves from 50 to 0, so existing users who never touched the slider will see slightly less contrast than before. That's what makes 0 mean "the base palette". --- ## Testing Switched between every theme from both the account popover and the profile page, in the expanded and collapsed rail, checking `data-theme` follows and survives a reload. Dragged the contrast slider in each theme and confirmed the percentage label tracks the handle and resnaps if a save fails. Checked both accessibility toggles across the `/storybook/colors` page, which is also where the contrast ratios were read from. Confirmed the Appearance entry stays hidden for a non-admin while the flag is off. <!-- conductor-workspace-link --> --- [Open workspace in Conductor](https://app.conductor.build/workspace/fee50611-7623-4422-bada-ed1cba317ed1) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent dd3a1c0 commit 4c5237c

154 files changed

Lines changed: 7801 additions & 2122 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: feature
4+
---
5+
6+
The dashboard has two new themes, Black and White, plus appearance options for stronger colors and underlined links.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/** Solid circle. Paired with {@link CircleOutlineIcon} by the Black and White
2+
* theme options — the filled disc reads as the opposite of the active theme. */
3+
export function CircleFilledIcon({ className }: { className?: string }) {
4+
return (
5+
<svg
6+
className={className}
7+
width="24"
8+
height="24"
9+
viewBox="0 0 24 24"
10+
fill="none"
11+
xmlns="http://www.w3.org/2000/svg"
12+
>
13+
<circle cx="12" cy="12" r="9" fill="currentColor" />
14+
</svg>
15+
);
16+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/** Hollow circle. Paired with {@link CircleFilledIcon} by the Black and White
2+
* theme options, which show the active theme's background through the ring. */
3+
export function CircleOutlineIcon({ className }: { className?: string }) {
4+
return (
5+
<svg
6+
className={className}
7+
width="24"
8+
height="24"
9+
viewBox="0 0 24 24"
10+
fill="none"
11+
xmlns="http://www.w3.org/2000/svg"
12+
>
13+
<circle cx="12" cy="12" r="8" stroke="currentColor" strokeWidth="2" />
14+
</svg>
15+
);
16+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/** Pencil over a couple of text lines — editing a value in place. */
2+
export function EditPencilIcon({ className }: { className?: string }) {
3+
return (
4+
<svg
5+
className={className}
6+
width="24"
7+
height="24"
8+
viewBox="0 0 24 24"
9+
fill="none"
10+
xmlns="http://www.w3.org/2000/svg"
11+
>
12+
<path
13+
d="M18.7573 3.6275L20.3732 5.24335C21.1542 6.0244 21.1542 7.29073 20.3732 8.07178L9.72032 18.7246C9.57777 18.8671 9.3957 18.9631 9.19759 19.0002L4.03377 19.9669L5.00052 14.8031C5.03765 14.6051 5.1336 14.4229 5.27604 14.2804L15.9289 3.6275C16.71 2.84645 17.9763 2.84645 18.7573 3.6275Z"
14+
stroke="currentColor"
15+
strokeWidth="2"
16+
/>
17+
<line x1="17.6464" y1="10.3536" x2="13.6464" y2="6.35355" stroke="currentColor" />
18+
<path d="M13 21L21 21" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
19+
<path d="M18 17L21 17" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
20+
</svg>
21+
);
22+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/** Monitor on a stand — the System theme, which follows the OS appearance. */
2+
export function MonitorIcon({ className }: { className?: string }) {
3+
return (
4+
<svg
5+
className={className}
6+
width="24"
7+
height="24"
8+
viewBox="0 0 24 24"
9+
fill="none"
10+
xmlns="http://www.w3.org/2000/svg"
11+
>
12+
<path
13+
d="M21 13H3M11 17H13L14 21H10L11 17ZM5 17H19C20.1046 17 21 16.1046 21 15V6C21 4.89543 20.1046 4 19 4H5C3.89543 4 3 4.89543 3 6V15C3 16.1046 3.89543 17 5 17Z"
14+
stroke="currentColor"
15+
strokeWidth="2"
16+
strokeLinecap="square"
17+
strokeLinejoin="round"
18+
/>
19+
</svg>
20+
);
21+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/** Crescent moon — the dark theme. */
2+
export function MoonIcon({ className }: { className?: string }) {
3+
return (
4+
<svg
5+
className={className}
6+
width="24"
7+
height="24"
8+
viewBox="0 0 24 24"
9+
fill="none"
10+
xmlns="http://www.w3.org/2000/svg"
11+
>
12+
<path
13+
d="M20.9638 12.7674C19.8361 13.5447 18.4693 13.9998 16.9961 13.9998C13.1301 13.9998 9.99609 10.8657 9.99609 6.99975C9.99609 5.52667 10.4511 4.15987 11.2283 3.03223C6.61911 3.42277 3 7.28768 3 11.9979C3 16.9674 7.0286 20.996 11.9981 20.996C16.7084 20.996 20.5734 17.3767 20.9638 12.7674Z"
14+
stroke="currentColor"
15+
strokeWidth="2"
16+
strokeLinecap="round"
17+
strokeLinejoin="round"
18+
/>
19+
</svg>
20+
);
21+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
export function SunIcon({ className }: { className?: string }) {
2+
return (
3+
<svg
4+
className={className}
5+
width="24"
6+
height="24"
7+
viewBox="0 0 24 24"
8+
fill="none"
9+
xmlns="http://www.w3.org/2000/svg"
10+
>
11+
<path
12+
fillRule="evenodd"
13+
clipRule="evenodd"
14+
d="M15.5355 8.46447C17.4882 10.4171 17.4882 13.5829 15.5355 15.5355C13.5829 17.4882 10.4171 17.4882 8.46447 15.5355C6.51184 13.5829 6.51184 10.4171 8.46447 8.46447C10.4171 6.51184 13.5829 6.51184 15.5355 8.46447Z"
15+
stroke="currentColor"
16+
strokeWidth="2"
17+
strokeLinecap="round"
18+
strokeLinejoin="round"
19+
/>
20+
<path
21+
d="M12 3V1M12 23V21M21 12H23M1 12H3M5.63603 5.63604L4.22182 4.22183M19.7782 19.7782L18.364 18.364M18.364 5.63606L19.7782 4.22184M4.22183 19.7782L5.63605 18.364"
22+
stroke="currentColor"
23+
strokeWidth="2"
24+
strokeLinecap="round"
25+
strokeLinejoin="round"
26+
/>
27+
</svg>
28+
);
29+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/** Toggle switch, knob to the left. */
2+
export function ToggleSwitchIcon({ className }: { className?: string }) {
3+
return (
4+
<svg
5+
className={className}
6+
width="24"
7+
height="24"
8+
viewBox="0 0 24 24"
9+
fill="none"
10+
xmlns="http://www.w3.org/2000/svg"
11+
>
12+
<path
13+
d="M15.5 5H8.5C4.63401 5 1.5 8.13401 1.5 12C1.5 15.866 4.63401 19 8.5 19H15.5C19.366 19 22.5 15.866 22.5 12C22.5 8.13401 19.366 5 15.5 5Z"
14+
stroke="currentColor"
15+
strokeWidth="2"
16+
/>
17+
<path
18+
d="M8.5 15C10.1569 15 11.5 13.6569 11.5 12C11.5 10.3431 10.1569 9 8.5 9C6.84315 9 5.5 10.3431 5.5 12C5.5 13.6569 6.84315 15 8.5 15Z"
19+
fill="currentColor"
20+
/>
21+
</svg>
22+
);
23+
}
-5.54 KB
Binary file not shown.

apps/webapp/app/components/billing/BillingLimitConfigSection.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { Paragraph } from "~/components/primitives/Paragraph";
2020
import { RadioGroup, RadioGroupItem } from "~/components/primitives/RadioButton";
2121
import type { BillingLimitResult } from "~/services/billingLimit.schemas";
2222
import { formatCurrency } from "~/utils/numberFormatter";
23+
import { TextLink } from "~/components/primitives/TextLink";
2324

2425
export const billingLimitFormSchema = z.discriminatedUnion("mode", [
2526
z.object({
@@ -339,10 +340,7 @@ function LimitReachedCalloutContent({
339340
When this limit is reached, queued runs will be held for {gracePeriodLabel}, then new triggers
340341
will be rejected until you increase or remove the limit. Limits are enforced with a short
341342
delay, so spend may briefly exceed the limit before grace begins. See our{" "}
342-
<a href="https://trigger.dev/terms" className="underline">
343-
terms
344-
</a>{" "}
345-
for refund policy details.
343+
<TextLink href="https://trigger.dev/terms">terms</TextLink> for refund policy details.
346344
{cancelInProgressRuns ? (
347345
<> In-progress runs will be cancelled when the limit is hit.</>
348346
) : null}

0 commit comments

Comments
 (0)