From 832986d67a5c27bec9da22f95ec55a7bf01c13a4 Mon Sep 17 00:00:00 2001 From: colton Date: Wed, 14 Oct 2026 12:46:35 -0700 Subject: [PATCH 01/11] web: Gable light foundation, tokens, theme switch and the hex sweep Every colour the desk draws is now an RGB channel variable (--gb-*) in the design system tokens: :root holds Industrial Dark exactly as before and :root[data-theme="light"] holds the Gable light palette. The Tailwind theme reads them (white, black, zinc, slate, gray and the status palettes, the brand aliases, plate and backdrop, glow and elevation shadows). A theme module and a sun or moon control in the desk header and on the front door let a person set light or dark for themselves, stored per signed in subject and applied before the first render. The 250 arbitrary hex colour classes in the apps became tokens, picture plates and switch knobs use plate, modal veils use backdrop, and a check keeps the sweep swept. Co-Authored-By: Claude Sonnet 5.5 Signed-off-by: colton --- web/apps/desk/index.html | 14 +- .../desk/src/components/BarcodeScanner.ts | 14 +- .../components/customers/CustomerSelect.ts | 8 +- .../components/inventory/AddProductModal.ts | 2 +- .../inventory/InventoryTransferModal.ts | 14 +- .../inventory/ProductMarginModal.ts | 4 +- .../inventory/StockAdjustmentModal.ts | 12 +- .../src/components/invoices/PaymentModal.ts | 2 +- .../src/components/invoices/SalesDialog.ts | 2 +- .../desk/src/components/layout/app-shell.ts | 3 +- .../src/components/layout/driver-layout.ts | 6 +- .../src/components/layout/portal-layout.ts | 2 +- .../desk/src/components/layout/yard-layout.ts | 6 +- .../components/logistics/AssignOrderModal.ts | 6 +- .../components/logistics/CreateRouteModal.ts | 10 +- .../src/components/logistics/DeliveryList.ts | 4 +- .../src/components/logistics/RouteList.ts | 4 +- .../desk/src/components/portal/CartSidebar.ts | 2 +- .../src/components/products/ProductSelect.ts | 8 +- .../src/components/quotes/LineItemEditor.ts | 16 +- .../components/quotes/acknowledgment-modal.ts | 2 +- .../desk/src/components/ui/app-disabled.ts | 2 +- web/apps/desk/src/components/ui/omnibar.ts | 4 +- .../desk/src/components/ui/session-expired.ts | 2 +- .../desk/src/components/ui/shortcuts-modal.ts | 2 +- .../desk/src/components/ui/toast-container.ts | 2 +- web/apps/desk/src/main.ts | 5 + web/apps/desk/src/pages/Home.ts | 2 +- .../src/pages/accounting/AccountsPayable.ts | 6 +- .../src/pages/accounts/AccountDetailPage.ts | 4 +- web/apps/desk/src/pages/admin/AppsPage.ts | 2 +- .../src/pages/admin/pricing/MarketCheck.ts | 2 +- .../src/pages/admin/pricing/MatrixGrid.ts | 2 +- .../admin/tech_admin/MarketCheckSettings.ts | 2 +- .../pages/admin/tech_admin/TechAdminPage.ts | 18 +- .../src/pages/credit-memos/CreditMemoForm.ts | 2 +- .../src/pages/credit-memos/CreditMemoList.ts | 2 +- .../desk/src/pages/driver/DeliveryDetail.ts | 10 +- web/apps/desk/src/pages/driver/RouteList.ts | 4 +- web/apps/desk/src/pages/driver/StopList.ts | 6 +- web/apps/desk/src/pages/governance/NewRFC.ts | 8 +- .../desk/src/pages/governance/RFCDetail.ts | 2 +- .../src/pages/inventory/tabs/ProductSEOTab.ts | 2 +- .../pages/inventory/tabs/ProductStockTab.ts | 2 +- .../desk/src/pages/invoices/InvoiceList.ts | 4 +- .../src/pages/logistics/FleetManagement.ts | 4 +- .../src/pages/millwork/BlueprintVerifier.ts | 22 +- .../src/pages/millwork/DoorConfigurator.ts | 30 +- .../src/pages/millwork/ProductConfigurator.ts | 46 +- web/apps/desk/src/pages/orders/OrderList.ts | 2 +- web/apps/desk/src/pages/portal/PortalCart.ts | 4 +- .../desk/src/pages/portal/PortalCheckout.ts | 8 +- .../desk/src/pages/portal/PortalDashboard.ts | 4 +- .../desk/src/pages/portal/PortalDeliveries.ts | 8 +- .../desk/src/pages/portal/PortalInvite.ts | 10 +- .../desk/src/pages/portal/PortalInvoices.ts | 4 +- .../desk/src/pages/portal/PortalOrders.ts | 4 +- .../src/pages/portal/PortalProductDetail.ts | 4 +- web/apps/desk/src/pages/portal/PortalTeam.ts | 6 +- .../pos/catalog/gable-pos-catalog.test.ts | 2 +- .../pages/pos/catalog/gable-pos-catalog.ts | 2 +- .../src/pages/projects/ProjectDashboard.ts | 10 +- .../desk/src/pages/projects/ProjectList.ts | 8 +- .../src/pages/purchasing/NewPurchaseOrder.ts | 14 +- .../pages/purchasing/PurchaseOrderDetail.ts | 12 +- .../src/pages/purchasing/PurchaseOrderList.ts | 2 +- .../purchasing/PurchasingRecommendations.ts | 22 +- web/apps/desk/src/pages/quotes/QuoteList.ts | 2 +- .../pages/reports/CustomerStatementPage.ts | 8 +- .../desk/src/pages/reports/SavedReports.ts | 2 +- web/apps/desk/src/pages/yard/CycleCount.ts | 4 +- .../desk/src/pages/yard/InventoryLookup.ts | 2 +- web/apps/desk/src/pages/yard/PickDetail.ts | 2 +- web/apps/desk/src/pages/yard/PickQueue.ts | 2 +- web/apps/desk/src/pages/yard/ReceivePO.ts | 4 +- .../desk/src/test/no-hardcoded-colour.test.ts | 47 ++ web/apps/front-door/index.html | 14 +- web/apps/front-door/src/front-door.ts | 6 +- web/apps/front-door/src/main.ts | 6 +- web/package-lock.json | 1 + web/packages/auth/src/custody.test.ts | 14 + web/packages/auth/src/custody.ts | 12 + web/packages/design-system/package.json | 1 + web/packages/design-system/src/brand-logo.ts | 8 +- web/packages/design-system/src/index.ts | 14 + .../src/light-palette.fixture.ts | 251 +++++++++ .../design-system/src/theme-toggle.test.ts | 42 ++ .../design-system/src/theme-toggle.ts | 51 ++ web/packages/design-system/src/theme.test.ts | 91 +++ web/packages/design-system/src/theme.ts | 116 ++++ web/packages/design-system/src/tokens.css | 528 +++++++++++++++++- web/packages/design-system/src/tokens.test.ts | 191 +++++++ web/packages/design-system/tailwind/theme.js | 60 +- 93 files changed, 1663 insertions(+), 282 deletions(-) create mode 100644 web/apps/desk/src/test/no-hardcoded-colour.test.ts create mode 100644 web/packages/design-system/src/light-palette.fixture.ts create mode 100644 web/packages/design-system/src/theme-toggle.test.ts create mode 100644 web/packages/design-system/src/theme-toggle.ts create mode 100644 web/packages/design-system/src/theme.test.ts create mode 100644 web/packages/design-system/src/theme.ts create mode 100644 web/packages/design-system/src/tokens.test.ts diff --git a/web/apps/desk/index.html b/web/apps/desk/index.html index eba368b2d..7505f529a 100644 --- a/web/apps/desk/index.html +++ b/web/apps/desk/index.html @@ -21,7 +21,7 @@ `, 'black'], + ] + it.each(catches)('catches %s', (_name, text, value) => { + expect(findLiterals(text, false).map(([, v]) => v)).toContain(value) + }) + + it('does not call a variable read or a token class a literal', () => { + const ok = `

` + expect(findLiterals(ok, false)).toEqual([]) + }) + + it('an allowlisted literal does not exempt the rest of its file', () => { + const file = 'desk/index.html' + const allowed = ALLOWED.find((a) => a.file === file) + const planted = `${allowed?.value ?? ''}

` + const left = findLiterals(planted, false).filter( + ([, v]) => !ALLOWED.some((a) => a.file === file && a.value === v), + ) + expect(left.map(([, v]) => v)).toEqual(['#123456']) + }) }) diff --git a/web/packages/design-system/src/light-palette.fixture.ts b/web/packages/design-system/src/light-palette.fixture.ts index d9e278df9..e7f9b9203 100644 --- a/web/packages/design-system/src/light-palette.fixture.ts +++ b/web/packages/design-system/src/light-palette.fixture.ts @@ -249,3 +249,22 @@ export const LIGHT_PALETTE = { } } } as const + +/** + * The fuchsia ramp is derived, not part of the palette file: the purple + * ramp's lightness and chroma at fuchsia's hue, so the status steps keep 4.6:1 + * or better on the ground and card and the tints keep ink at 12:1 or better. + */ +export const LIGHT_FUCHSIA = { + "50": "#772e82", + "100": "#7a3185", + "200": "#7d3488", + "300": "#80378c", + "400": "#863d91", + "500": "#894094", + "600": "#833a8f", + "700": "#7a3185", + "800": "#eddaef", + "900": "#f4e1f6", + "950": "#f8e8fa" +} as const diff --git a/web/packages/design-system/src/theme.test.ts b/web/packages/design-system/src/theme.test.ts index 1dfb0a30b..7441265d5 100644 --- a/web/packages/design-system/src/theme.test.ts +++ b/web/packages/design-system/src/theme.test.ts @@ -66,11 +66,29 @@ describe('theme switch', () => { expect(currentTheme()).toBe('light') }) - it('gives a person with no stored choice the last used value, and keeps it', () => { + it('shows a person with no choice of their own the last used value, without storing it as theirs', () => { setTheme('light') // the front door, nobody signed in useThemeSubject('person-c') expect(currentTheme()).toBe('light') - expect(localStorage.getItem(themeKeyFor('person-c'))).toBe('light') + expect(localStorage.getItem(themeKeyFor('person-c'))).toBeNull() + // their own choice is written only when they make one + setTheme('dark') + expect(localStorage.getItem(themeKeyFor('person-c'))).toBe('dark') + }) + + it('follows a change made in another tab, for the key that applies here', () => { + useThemeSubject('person-e') + localStorage.setItem(themeKeyFor('person-e'), 'light') + window.dispatchEvent(new StorageEvent('storage', { key: themeKeyFor('person-e'), newValue: 'light' })) + expect(currentTheme()).toBe('light') + // another person's key, or a value that is not a theme, changes nothing + window.dispatchEvent(new StorageEvent('storage', { key: themeKeyFor('person-x'), newValue: 'dark' })) + window.dispatchEvent(new StorageEvent('storage', { key: themeKeyFor('person-e'), newValue: 'purple' })) + expect(currentTheme()).toBe('light') + // signed out: the last used key applies + useThemeSubject(null) + window.dispatchEvent(new StorageEvent('storage', { key: THEME_KEY, newValue: 'dark' })) + expect(currentTheme()).toBe('dark') }) it('ignores a stored value that is not a theme', () => { diff --git a/web/packages/design-system/src/theme.ts b/web/packages/design-system/src/theme.ts index 072d16741..907300371 100644 --- a/web/packages/design-system/src/theme.ts +++ b/web/packages/design-system/src/theme.ts @@ -64,8 +64,9 @@ function apply(theme: Theme): void { /** * Pick the subject whose choice applies (null = nobody signed in) and apply - * it. A subject with no stored choice keeps the last used value, which is - * then stored for them. + * it. A subject with no stored choice of their own is shown the last used + * value, which is NOT written under their key: a person's key only ever + * holds that person's own choice (setTheme writes it). */ export function useThemeSubject(subject: string | null): Theme { activeSubject = subject !== null && subject !== '' ? subject : null @@ -74,9 +75,7 @@ export function useThemeSubject(subject: string | null): Theme { if (activeSubject === null) { theme = lastUsed ?? 'dark' } else { - const own = read(themeKeyFor(activeSubject)) - theme = own ?? lastUsed ?? 'dark' - if (own === null) write(themeKeyFor(activeSubject), theme) + theme = read(themeKeyFor(activeSubject)) ?? lastUsed ?? 'dark' } apply(theme) write(THEME_KEY, theme) @@ -101,6 +100,20 @@ export function toggleTheme(): Theme { return next } +/** + * Another tab changed the choice: follow it. Only the key that applies to + * this tab is followed (the signed in person's own, else the last used), and + * the value is applied without being written again. + */ +function onStorage(e: StorageEvent): void { + const key = activeSubject !== null ? themeKeyFor(activeSubject) : THEME_KEY + if (e.key !== key) return + const theme = parse(e.newValue) + if (theme !== null) apply(theme) +} + +if (typeof window !== 'undefined') window.addEventListener('storage', onStorage) + /** Subscribe to theme changes; returns the unsubscribe. */ export function onThemeChange(listener: (theme: Theme) => void): () => void { const handler = (e: Event) => listener((e as CustomEvent).detail) diff --git a/web/packages/design-system/src/tokens.css b/web/packages/design-system/src/tokens.css index a437b8ed1..2a0416f6d 100644 --- a/web/packages/design-system/src/tokens.css +++ b/web/packages/design-system/src/tokens.css @@ -12,8 +12,6 @@ @layer base { :root { - color-scheme: dark; - /* Industrial Dark: colour channels read by the Tailwind theme */ --gb-white: 255 255 255; --gb-black: 0 0 0; @@ -226,6 +224,17 @@ --gb-lime-800: 63 98 18; --gb-lime-900: 54 83 20; --gb-lime-950: 26 46 5; + --gb-fuchsia-50: 253 244 255; + --gb-fuchsia-100: 250 232 255; + --gb-fuchsia-200: 245 208 254; + --gb-fuchsia-300: 240 171 252; + --gb-fuchsia-400: 232 121 249; + --gb-fuchsia-500: 217 70 239; + --gb-fuchsia-600: 192 38 211; + --gb-fuchsia-700: 162 28 175; + --gb-fuchsia-800: 134 25 143; + --gb-fuchsia-900: 112 26 117; + --gb-fuchsia-950: 74 4 78; --gb-gable-green: 0 255 163; --gb-gable-green-hover: 0 229 147; --gb-deep-space: 10 11 16; @@ -242,6 +251,19 @@ --gb-sage-tint: 39 39 42; --gb-glow: 0 0 15px rgba(0, 255, 163, 0.3); --gb-pale: 224 224 224; + --gb-portal-ground: 12 14 20; + --gb-portal-panel: 17 19 32; + --gb-chip-ground: 0 0 0; + --gb-shade: 0 0 0; + --gb-photo-btn: 255 255 255; + --gb-map-note: 102 102 102; + --gb-chart-tick: 82 82 91; + --gb-hover-row: 255 255 255; + --gb-hover-row-a: 0.05; + --gb-chart-fulfilled: 129 140 248; + --gb-picture-plate: 255 255 255; + --gb-tile: 10 11 16; + --gb-tile-shadow: 0 0 #0000; --gb-voice: 255 255 255; --gb-voice-soft: 212 212 216; --gb-voice-text: 161 161 170; @@ -539,6 +561,17 @@ --gb-lime-800: 217 230 207; --gb-lime-900: 224 237 213; --gb-lime-950: 230 244 220; + --gb-fuchsia-50: 119 46 130; + --gb-fuchsia-100: 122 49 133; + --gb-fuchsia-200: 125 52 136; + --gb-fuchsia-300: 128 55 140; + --gb-fuchsia-400: 134 61 145; + --gb-fuchsia-500: 137 64 148; + --gb-fuchsia-600: 131 58 143; + --gb-fuchsia-700: 122 49 133; + --gb-fuchsia-800: 237 218 239; + --gb-fuchsia-900: 244 225 246; + --gb-fuchsia-950: 248 232 250; --gb-gable-green: 4 108 67; --gb-gable-green-hover: 4 92 56; --gb-deep-space: 240 232 224; @@ -555,6 +588,19 @@ --gb-sage-tint: 226 234 213; --gb-glow: 0 0 #0000; --gb-pale: 53 49 44; + --gb-portal-ground: 240 232 224; + --gb-portal-panel: 248 243 236; + --gb-chip-ground: 248 243 236; + --gb-shade: 240 232 224; + --gb-photo-btn: 252 250 246; + --gb-map-note: 109 104 99; + --gb-chart-tick: 109 104 99; + --gb-hover-row: 240 232 224; + --gb-hover-row-a: 0.7; + --gb-chart-fulfilled: 86 100 61; + --gb-picture-plate: 240 232 224; + --gb-tile: 251 249 244; + --gb-tile-shadow: 0 1px 2px rgb(34 31 29 / 0.14); --gb-voice: 86 100 61; --gb-voice-soft: 86 100 61; --gb-voice-text: 86 100 61; diff --git a/web/packages/design-system/src/tokens.test.ts b/web/packages/design-system/src/tokens.test.ts index 4b4d3d45c..bfda6ef0e 100644 --- a/web/packages/design-system/src/tokens.test.ts +++ b/web/packages/design-system/src/tokens.test.ts @@ -11,7 +11,7 @@ import { describe, it, expect } from 'vitest' import { readFileSync } from 'node:fs' import { resolve } from 'node:path' import colors from 'tailwindcss/colors' -import { LIGHT_PALETTE } from './light-palette.fixture' +import { LIGHT_PALETTE, LIGHT_FUCHSIA } from './light-palette.fixture' const css = readFileSync(resolve(process.cwd(), 'src/tokens.css'), 'utf8') @@ -56,12 +56,15 @@ const STEPS = ['50', '100', '200', '300', '400', '500', '600', '700', '800', '90 const NEUTRALS = ['zinc', 'slate', 'gray'] as const const STATUS = Object.keys(LIGHT_PALETTE.status) as (keyof typeof LIGHT_PALETTE.status)[] +/** The shipped palettes whose dark values are Tailwind's own. */ +const DEFAULT_PALETTES = [...NEUTRALS, ...STATUS, 'fuchsia'] as const + describe('Industrial Dark is unchanged', () => { it('holds Tailwind default values for white, black and every palette', () => { expect(dark['--gb-white']).toBe('255 255 255') expect(dark['--gb-black']).toBe('0 0 0') const dflt = colors as unknown as Record> - for (const name of [...NEUTRALS, ...STATUS]) { + for (const name of DEFAULT_PALETTES) { for (const step of STEPS) { expect(dark[`--gb-${name}-${step}`], `${name}-${step}`).toBe(channels(dflt[name][step])) } @@ -125,6 +128,112 @@ describe('Industrial Dark is unchanged', () => { expect(dark['--gb-glow-40-30']).toBe('0 0 40px rgba(0, 255, 163, 0.3)') }) + it('pins every dark value: no variable is unlisted and none differs', () => { + const dflt = colors as unknown as Record> + const expected: Record = { + '--gb-white': '255 255 255', + '--gb-black': '0 0 0', + } + for (const name of DEFAULT_PALETTES) { + for (const step of STEPS) expected[`--gb-${name}-${step}`] = channels(dflt[name][step]) + } + const hexes: Record = { + 'gable-green': '#00FFA3', + 'gable-green-hover': '#00E593', + 'gable-green-tint': '#00331F', + 'deep-space': '#0A0B10', + 'slate-steel': '#161821', + 'surface-2': '#1E2029', + 'surface-3': '#252836', + 'safety-red': '#F43F5E', + 'blueprint-blue': '#38BDF8', + plate: '#FFFFFF', + backdrop: '#000000', + sage: colors.zinc[400], + 'sage-fill': colors.zinc[600], + 'sage-tint': colors.zinc[800], + voice: '#FFFFFF', + 'voice-soft': colors.zinc[300], + 'voice-text': colors.zinc[400], + 'voice-fill': colors.zinc[500], + pale: '#E0E0E0', + well: '#12131A', + 'well-hover': '#1a1c26', + 'till-surface': '#161b22', + 'till-edge': '#30363d', + 'till-text': '#e6edf3', + 'till-muted': '#8b949e', + 'till-soft': '#c9d1d9', + 'till-field': '#0d1117', + 'till-accent': '#8957e5', + 'till-go': '#238636', + 'till-ok': '#3fb950', + 'till-bad': '#f85149', + 'till-warn': '#d29922', + 'till-rule': '#21262d', + 'portal-ground': '#0C0E14', + 'portal-panel': '#111320', + 'chip-ground': '#000000', + shade: '#000000', + 'photo-btn': '#FFFFFF', + 'map-note': '#666666', + 'chart-tick': colors.zinc[600], + 'chart-fulfilled': colors.indigo[400], + 'picture-plate': '#FFFFFF', + tile: '#0A0B10', + } + for (const [name, value] of Object.entries(hexes)) expected[`--gb-${name}`] = channels(value) + expected['--gb-hover-row'] = '255 255 255' + expected['--gb-hover-row-a'] = '0.05' + Object.assign(expected, { + '--gb-tile-shadow': '0 0 #0000', + '--gb-glow': '0 0 15px rgba(0, 255, 163, 0.3)', + '--gb-glow-soft': '0 0 15px rgba(0, 255, 163, 0.1)', + '--gb-glow-strong': '0 0 25px rgba(0, 255, 163, 0.5)', + '--gb-glow-10-30': '0 0 10px rgba(0, 255, 163, 0.3)', + '--gb-glow-20-10': '0 0 20px rgba(0, 255, 163, 0.1)', + '--gb-glow-20-30': '0 0 20px rgba(0, 255, 163, 0.3)', + '--gb-glow-30-15': '0 0 30px rgba(0, 255, 163, 0.15)', + '--gb-glow-40-30': '0 0 40px rgba(0, 255, 163, 0.3)', + '--gb-elevation-1': '0px 1px 2px rgba(0, 0, 0, 0.3), 0px 1px 3px 1px rgba(0, 0, 0, 0.15)', + '--gb-elevation-2': '0px 1px 2px rgba(0, 0, 0, 0.3), 0px 2px 6px 2px rgba(0, 0, 0, 0.15)', + '--gb-elevation-3': '0px 1px 3px rgba(0, 0, 0, 0.3), 0px 4px 8px 3px rgba(0, 0, 0, 0.15)', + // the semantic HSL variables and the radius, as the base file had them + '--background': '230 20% 5%', + '--foreground': '210 40% 98%', + '--primary': '158 100% 50%', + '--primary-foreground': '230 20% 5%', + '--primary-container': '158 100% 10%', + '--on-primary-container': '158 100% 80%', + '--secondary': '217 33% 17%', + '--secondary-foreground': '210 40% 98%', + '--secondary-container': '217 33% 25%', + '--on-secondary-container': '210 40% 98%', + '--tertiary': '199 95% 60%', + '--tertiary-foreground': '230 20% 5%', + '--destructive': '346 87% 60%', + '--destructive-foreground': '210 40% 98%', + '--card': '229 19% 11%', + '--card-foreground': '210 40% 98%', + '--popover': '229 19% 11%', + '--popover-foreground': '210 40% 98%', + '--muted': '217 33% 17%', + '--muted-foreground': '215 20% 65%', + '--accent': '158 100% 50%', + '--accent-foreground': '230 20% 5%', + '--border': '217 33% 20%', + '--input': '217 33% 20%', + '--ring': '158 100% 50%', + '--radius': '0.75rem', + }) + expect(Object.keys(dark).sort()).toEqual(Object.keys(expected).sort()) + for (const [key, value] of Object.entries(expected)) expect(dark[key], key).toBe(value) + }) + + it('draws no colour-scheme on the dark root (native controls stay as they were)', () => { + expect(css.slice(0, css.indexOf(':root[data-theme="light"] {'))).not.toMatch(/color-scheme/) + }) + it('keeps the semantic HSL variables as they were', () => { expect(dark['--background']).toBe('230 20% 5%') expect(dark['--primary']).toBe('158 100% 50%') @@ -190,6 +299,12 @@ describe('Gable light holds the palette values', () => { } }) + it('the derived fuchsia ramp', () => { + for (const step of STEPS) { + expect(light[`--gb-fuchsia-${step}`], `fuchsia-${step}`).toBe(channels(LIGHT_FUCHSIA[step])) + } + }) + it('soft ink tinted shadows and no glow', () => { expect(light['--gb-glow']).toBe('0 0 #0000') expect(light['--gb-glow-strong']).toBe('0 0 #0000') @@ -232,6 +347,10 @@ describe('Gable light contrast (WCAG 2), computed from the tokens', () => { ['ink label on sage fill', 'white', 'sage-fill', 4.5], ['green text on green tint', 'gable-green', 'gable-green-tint', 4.5], ['sage text on sage tint', 'sage', 'sage-tint', 4.5], + ['fuchsia 300 (A2A chip) on card', 'fuchsia-300', 'slate-steel', 4.5], + ['fuchsia 300 on ground', 'fuchsia-300', 'deep-space', 4.5], + ['fuchsia 500 on card', 'fuchsia-500', 'slate-steel', 4.5], + ['ink on fuchsia tint (950)', 'white', 'fuchsia-950', 4.5], ] it.each(PAIRS)('%s', (_name, fg, bg, needed) => { diff --git a/web/packages/design-system/tailwind/theme.js b/web/packages/design-system/tailwind/theme.js index 848e674ee..75ce30dc0 100644 --- a/web/packages/design-system/tailwind/theme.js +++ b/web/packages/design-system/tailwind/theme.js @@ -17,7 +17,7 @@ const STEPS = ['50', '100', '200', '300', '400', '500', '600', '700', '800', '90 const PALETTES = [ 'zinc', 'slate', 'gray', 'emerald', 'green', 'red', 'rose', 'amber', 'yellow', 'orange', 'blue', - 'sky', 'teal', 'indigo', 'violet', 'purple', 'pink', 'cyan', 'lime', + 'sky', 'teal', 'indigo', 'violet', 'purple', 'fuchsia', 'pink', 'cyan', 'lime', ]; /** rgb(var(--gb-) / ): one colour that follows the mode. */ @@ -95,6 +95,19 @@ export const gableTheme = { 'sage-tint': channel('sage-tint'), // Colours with their own dark value, so Industrial Dark draws exactly as before 'pale': channel('pale'), + 'portal-ground': channel('portal-ground'), + 'portal-panel': channel('portal-panel'), + 'chip-ground': channel('chip-ground'), + 'shade': channel('shade'), + 'photo-btn': channel('photo-btn'), + 'map-note': channel('map-note'), + 'chart-tick': channel('chart-tick'), + 'chart-fulfilled': channel('chart-fulfilled'), + // A picture's plate (white in dark, the warm ground in light) and a tile on a card + 'picture-plate': channel('picture-plate'), + tile: channel('tile'), + // A row hover: white at 5 percent in dark, a warm stone step in light + 'hover-row': 'rgb(var(--gb-hover-row) / var(--gb-hover-row-a))', // The second voice: dark draws today's neutrals, light draws sage 'voice': channel('voice'), 'voice-soft': channel('voice-soft'), @@ -135,6 +148,7 @@ export const gableTheme = { 'elevation-1': 'var(--gb-elevation-1)', 'elevation-2': 'var(--gb-elevation-2)', 'elevation-3': 'var(--gb-elevation-3)', + 'tile': 'var(--gb-tile-shadow)', 'glow': 'var(--gb-glow)', 'glow-soft': 'var(--gb-glow-soft)', 'glow-10-30': 'var(--gb-glow-10-30)',