@@ -82,4 +105,12 @@ export function App() { ); } +export function App() { + return ( + + + + ); +} + export default App; diff --git a/apps/prs/react/src/dark-mode-overrides.css b/apps/prs/react/src/dark-mode-overrides.css new file mode 100644 index 0000000000..646f48dcfd --- /dev/null +++ b/apps/prs/react/src/dark-mode-overrides.css @@ -0,0 +1,17 @@ +/* + * DARK MODE + V2 TOKEN OVERRIDES + * + * Load order matters: + * 1. @abgov/style (imported in app.tsx) loads V1 tokens + * 2. design-tokens-v2 overrides with V2 values + * 3. surface-tokens.css defines the surface elevation system + * 4. dark-theme.css overrides for dark mode + * + * To disable: comment out the import of this file in app.tsx + * + * To toggle dark mode, use the moon/sun icon at the bottom of the side menu. + */ + +@import "@abgov/design-tokens-v2/dist/tokens.css"; +@import "./surface-tokens.css"; +@import "./dark-theme.css"; diff --git a/apps/prs/react/src/dark-theme.css b/apps/prs/react/src/dark-theme.css new file mode 100644 index 0000000000..ab08a8cf7c --- /dev/null +++ b/apps/prs/react/src/dark-theme.css @@ -0,0 +1,515 @@ +/* ============================================================================ + DARK MODE - Attribute-based with system preference fallback + ============================================================================ + Strategy: Override global primitives and let the var() cascade handle + component tokens. Only add targeted component overrides where the + cascade doesn't reach (hardcoded hex values in tokens.css). + + Activated by [data-theme="dark"] on . JS handles detection: + - Default: follows system preference (prefers-color-scheme) + - User override: stored in localStorage, set via theme toggle + ============================================================================ */ + +:root[data-theme="dark"] { + color-scheme: dark; + + /* ======================================================================== + GREYSCALE - Flip the scale + ======================================================================== */ + --goa-color-greyscale-white: #1e1e1e; + --goa-color-greyscale-50: #262626; + --goa-color-greyscale-100: #2a2a2a; + --goa-color-greyscale-150: #333333; + --goa-color-greyscale-200: #3d3d3d; + --goa-color-greyscale-300: #4d4d4d; + --goa-color-greyscale-400: #6a6a6a; + --goa-color-greyscale-500: #858585; + --goa-color-greyscale-600: #999999; + --goa-color-greyscale-700: #b0b0b0; + --goa-color-greyscale-800: #b8b8b8; + --goa-color-greyscale-black: #d4d4d4; + + /* ======================================================================== + INTERACTIVE - Lighter blues for dark backgrounds + ======================================================================== */ + --goa-color-interactive-default: #6b9fc9; + --goa-color-interactive-hover: #85b5d8; + --goa-color-interactive-focus: #6b9fc9; + --goa-color-interactive-secondary: #1e2d3d; + --goa-color-interactive-secondary-hover: #243648; + --goa-color-interactive-visited: #9b8ec9; + --goa-color-interactive-error: #c97b7b; + --goa-color-interactive-error-hover: #b06060; + --goa-color-interactive-error-disabled: #6a4040; + --goa-color-interactive-disabled: #4d4d4d; + + /* ======================================================================== + BRAND + ======================================================================== */ + --goa-color-brand-default: #6bc9b8; + --goa-color-brand-dark: #90ddd0; + --goa-color-brand-light: #1a3030; + + /* ======================================================================== + INFO + ======================================================================== */ + --goa-color-info-default: #6b9fc9; + --goa-color-info-dark: #5080a0; + --goa-color-info-light: #1e2d3d; + --goa-color-info-background: #1e2830; + --goa-color-info-border: #2a4050; + --goa-color-info-text: #8ab4d4; + --goa-color-info-text-dark: #a0c8e0; + --goa-color-info-text-inverse: #1a2530; + + /* ======================================================================== + SUCCESS + ======================================================================== */ + --goa-color-success-default: #7eb36a; + --goa-color-success-dark: #5a8a48; + --goa-color-success-light: #1e2e1a; + --goa-color-success-background: #1e3220; + --goa-color-success-border: #2a4020; + --goa-color-success-text: #90c880; + --goa-color-success-text-dark: #a8d8a0; + --goa-color-success-text-inverse: #1a2818; + + /* ======================================================================== + WARNING + ======================================================================== */ + --goa-color-warning-default: #c9a227; + --goa-color-warning-dark: #a08020; + --goa-color-warning-light: #2e2818; + --goa-color-warning-background: #302a16; + --goa-color-warning-border: #403518; + --goa-color-warning-text: #d4b040; + --goa-color-warning-text-dark: #3a2a00; /* darkened from light-mode #4d3700 for better contrast on golden bg */ + + /* ======================================================================== + IMPORTANT + ======================================================================== */ + --goa-color-important-default: #c9a227; + --goa-color-important-dark: #a08020; + --goa-color-important-light: #2e2818; + --goa-color-important-background: #2e2818; + --goa-color-important-border: #403518; + --goa-color-important-text: #d4b040; + --goa-color-important-text-dark: #3a2a00; /* darkened from light-mode #4d3700 for better contrast on golden bg */ + + /* ======================================================================== + EMERGENCY + ======================================================================== */ + --goa-color-emergency-default: #c97b7b; + --goa-color-emergency-dark: #c08080; + --goa-color-emergency-light: #3d2525; /* brighter for chip error bg */ + --goa-color-emergency-background: #321e1e; + --goa-color-emergency-border: #402828; + --goa-color-emergency-text: #d49090; + --goa-color-emergency-text-dark: #e0a8a8; + --goa-color-emergency-text-inverse: #281a1a; + + /* ======================================================================== + CRITICAL + ======================================================================== */ + --goa-color-critical-default: #d4d4d4; + + /* ======================================================================== + SHADOWS - Subtle light glow instead of dark shadows + ======================================================================== */ + --goa-shadow-100: 0px 1px 0px 0px rgba(255, 255, 255, 0.05); + --goa-shadow-150: 0px 1px 0px 0px rgba(255, 255, 255, 0.08); + --goa-shadow-200: 0px 3px 1px -1px rgba(255, 255, 255, 0.05); + --goa-shadow-300: 0px 4px 6px -2px rgba(0, 0, 0, 0.4); + --goa-shadow-400: 0px 8px 16px -4px rgba(0, 0, 0, 0.5); + --goa-shadow-500: 0px 12px 20px -8px rgba(0, 0, 0, 0.5); + --goa-shadow-600: 0px 20px 20px -8px rgba(0, 0, 0, 0.5); + --goa-shadow-modal: + 0px 0px 4px 0px rgba(0, 0, 0, 0.3), 0px 8px 40px 0px rgba(0, 0, 0, 0.5); + --goa-shadow-raised-light: + 0px 12px 16px -4px rgba(0, 0, 0, 0.3), 0px 4px 6px -2px rgba(0, 0, 0, 0.15); + --goa-shadow-raised-heavy: + 0px 0px 1px 0px rgba(0, 0, 0, 0.4), 0px 16px 32px -20px rgba(0, 0, 0, 0.5); + --goa-shadow-shallow-below: 0px 1px 8px 0px rgba(0, 0, 0, 0.2); + --goa-shadow-shallow-above: 0px -2px 16px 0px rgba(0, 0, 0, 0.3); + + /* ======================================================================== + OPACITY + ======================================================================== */ + --goa-opacity-background-modal: 70%; + + /* ======================================================================== + DRAWER + ======================================================================== */ + --goa-drawer-overlay-color: rgba(0, 0, 0, 0.6); + --goa-drawer-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.3), 0 8px 40px 0 rgba(0, 0, 0, 0.4); + + /* ======================================================================== + CIRCULAR PROGRESS + ======================================================================== */ + --goa-circular-progress-color-background: rgba(30, 30, 30, 0.9); + + /* ======================================================================== + HARDCODED TOKEN FIXES + These component tokens have hardcoded hex values in tokens.css, + so the greyscale cascade doesn't reach them. + ======================================================================== */ + + /* Input - hardcoded values that don't cascade */ + --goa-input-color-background-error-hover: #3a1818; + --goa-input-color-border-readonly: #3d3d3d; + + /* Radio - hardcoded values */ + --goa-radio-label-color-disabled: #6a6a6a; + --goa-radio-color-bg-error-hover: #3a1818; + + /* Temporary notification */ + --goa-temporary-notification-color-bg-basic: var(--goa-color-surface-heading); + + /* Link */ + --goa-link-color-light-visited: #9b8ec9; + + /* Microsite header - alpha/beta badge text needs dark color on amber/blue bg */ + --goa-microsite-header-alpha-badge-color-text: #4d3700; + --goa-microsite-header-beta-badge-color-text: #00527c; + + /* Important subtle badge - warning-text-dark is tuned dark for high-emphasis + (golden bg), but that leaves dark-on-dark on the subtle variant's dark + amber bg. Point the subtle content at the light amber text token. */ + --goa-badge-important-subtle-color-content: var(--goa-color-important-text); + + /* Badge - default subtle needs visible separation on dark surfaces */ + --goa-badge-default-subtle-color-bg: var(--goa-color-surface-heading); + --goa-badge-default-subtle-border: inset 0 0 0 1px #4a4a4a; + + /* Callout - content bg: subtle tint, just enough to separate from page */ + /* Medium emphasis (default - no prefix) */ + --goa-callout-info-content-bg-color: #1e2830; + --goa-callout-success-content-bg-color: #1e2820; + --goa-callout-important-content-bg-color: #28251e; + --goa-callout-emergency-content-bg-color: #2a1c1c; + /* High emphasis - content */ + --goa-callout-h-info-content-bg-color: #1e2830; + --goa-callout-h-success-content-bg-color: #1e2820; + --goa-callout-h-important-content-bg-color: #28251e; + --goa-callout-h-emergency-content-bg-color: #352020; + /* High emphasis - heading bg (midway between muted and bright status colors) */ + --goa-callout-h-info-heading-bg-color: #386590; + --goa-callout-h-success-heading-bg-color: #386030; + --goa-callout-h-important-heading-bg-color: #5e4e20; + --goa-callout-h-emergency-heading-bg-color: #6e4242; + /* Low emphasis */ + --goa-callout-l-info-content-bg-color: #1e2830; + --goa-callout-l-success-content-bg-color: #1e2820; + --goa-callout-l-important-content-bg-color: #28251e; + --goa-callout-l-emergency-content-bg-color: #352020; + + /* Table - elevate above content card so rows are distinguishable */ + --goa-table-color-bg-data: var(--goa-color-surface-table-data); + --goa-table-color-bg-heading: var(--goa-color-surface-heading); + --goa-table-color-bg-heading-hover: var(--goa-color-surface-heading-hover); + + /* Container cards */ + --goa-container-non-interactive-bg-color: var(--goa-color-surface-widget); + --goa-container-non-interactive-heading-bg-color: var(--goa-color-surface-container-heading); + + /* Primary button hover - default #6b9fc9, darken noticeably */ + --goa-button-primary-hover-color-bg: #4a7a9a; + + /* Secondary button hover bg - interactive.secondary-hover (#243648) is too subtle */ + --goa-button-secondary-hover-color-bg: #293d50; + + /* Secondary button text - interactive.hover (#5588aa) is too dark for text on dark bg */ + --goa-button-secondary-color-text: #85b5d8; + --goa-button-secondary-hover-color-text: #85b5d8; + --goa-button-secondary-focus-color-text: #85b5d8; + + /* Destructive button hover - should go darker like primary, not lighter */ + --goa-button-primary-destructive-hover-color-bg: #a06060; + + /* Radio disabled - border #b1b1b1 is too bright on dark bg, reads as active */ + --goa-radio-border-disabled: 1px solid #555555; + + /* Notification banner close button hover - slightly darker than the banner bg */ + --goa-notification-banner-important-high-close-bg-hover: #a88520; + --goa-notification-banner-important-low-close-bg-hover: #a88520; + --goa-notification-banner-emergency-high-close-bg-hover: #b06060; + --goa-notification-banner-emergency-low-close-bg-hover: #b06060; + + /* Checkbox label - uses input-color-text-secondary (greyscale-800 = #b8b8b8), too muted vs radio which inherits text-default (#d4d4d4) */ + --goa-checkbox-color-label: var(--goa-color-text-default); + + /* File upload hover/focus - keep blue tint consistent with default state */ + --goa-file-upload-color-bg-hover: #252d35; + --goa-file-upload-color-bg-focus: #252d35; + + /* Disabled button text - slightly more visible on dark backgrounds */ + --goa-button-secondary-disabled-color-text: #888888; + --goa-button-tertiary-disabled-color-text: #888888; + + /* Tertiary button - subtle surface so it reads as a button on dark backgrounds */ + --goa-button-tertiary-color-bg: var(--goa-color-surface-100); + --goa-button-tertiary-hover-color-bg: var(--goa-color-surface-250); + --goa-button-tertiary-hover-border: 1px solid #666666; + + /* Interactive containers - NOTE: body bg token exists but component doesn't use it. Needs shadow DOM patch. */ + --goa-container-interactive-bg-color: var(--goa-color-surface-widget); + --goa-container-interactive-heading-bg-color: var(--goa-color-surface-container-heading); + + /* Accordion */ + --goa-accordion-color-bg-heading: var(--goa-color-surface-heading); + --goa-accordion-color-bg-heading-hover: var(--goa-color-surface-heading-hover); + --goa-accordion-color-bg-content: var(--goa-color-surface-table-data); + + /* Input fields - subtle surface above page background */ + --goa-input-color-background-default: var(--goa-color-surface-input); + --goa-dropdown-color-bg: var(--goa-color-surface-input); + + /* Radio - match input/checkbox background so circles are visible */ + --goa-radio-color-bg: var(--goa-color-surface-input); + + /* Native dropdown chevron - hardcoded #333 in shadow DOM, needs light stroke */ + --goa-dropdown-native-chevron-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill='none' stroke='%23999999' stroke-linecap='round' stroke-linejoin='round' stroke-width='48' d='M112 184l144 144 144-144' /%3E%3C/svg%3E"); + + /* Popover - elevate above page so date picker / dropdowns read as floating */ + --goa-popover-color-bg: var(--goa-color-surface-popover); + + /* App header - subtle surface above page */ + --goa-app-header-color-bg: var(--goa-color-surface-app-header); + + /* App header V2 logos - dark variants (#545860 -> #C2C6CE for visibility on dark backgrounds). + Light defaults live in AppHeader.svelte as var() fallbacks. */ + --goa-app-header-logo-mobile: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3QgeD0iMC4wMjgzMjAzIiB3aWR0aD0iMzEuNjk1NCIgaGVpZ2h0PSIzMS42ODc2IiByeD0iNCIgZmlsbD0iIzAwQjZFRCIvPgo8ZyBjbGlwLXBhdGg9InVybCgjY2xpcDBfNTg2NTNfMjAzODgwKSI+CjxtYXNrIGlkPSJtYXNrMF81ODY1M18yMDM4ODAiIHN0eWxlPSJtYXNrLXR5cGU6YWxwaGEiIG1hc2tVbml0cz0idXNlclNwYWNlT25Vc2UiIHg9Ii0xMSIgeT0iLTIiIHdpZHRoPSI0NyIgaGVpZ2h0PSIzOSI+CjxwYXRoIGQ9Ik0yMi4wMTYxIDMxLjEwMTVDMTkuNTQ1MyAzMC4xOTY5IDE3LjEzMzggMjkuMTQgMTQuNzk1OCAyNy45MzY5QzE2LjkxODMgMjcuMTYzMSAxOC45ODc5IDI2LjI1MzIgMjAuOTkxNSAyNS4yMTMxQzIxLjE5NTkgMjcuMTk3NiAyMS41Mzc3IDI5LjE2NTggMjIuMDE0NiAzMS4xMDM2TTM1LjI4NCA2LjcxMTMyQzM0LjI1MDEgNi41ODE1OCAzNC43ODczIDcuMDU4OTYgMzQuNDk5MiA4LjQxMTU0QzMzLjI1MzcgMTQuMjQzMyAyOC40NDkzIDE4LjQ0NjYgMjMuNjI2MiAyMS4yNjY0QzIzLjEyMDggMTQuNTg4MSAyMy4zMjczIDcuMTczNjQgMjQuNTkzNyAyLjYyOTkyQzI1LjY2MjMgLTEuMjA1NjIgMjYuOTMzMSAtMC41MDE3MjkgMjUuMzU2OSAtMS4zMDc0QzIzLjY5NjIgLTIuMTU1MzYgMjEuOTE2NCAtMS4wMzUwMiAyMC40NzQ2IDEuODIyMUMxOS4wMzI3IDQuNjc5MjIgMTIuMzkyOSAyMC4xODkxIDEuNzc5MTMgMzAuNjYyMUMtMy42NDk3OCAzNi4wMjMgLTguNTYwMjggMzMuMjYxOSAtOS41NDM2OCAzMi40Mzc2Qy0xMC4zNDM3IDMxLjc2NjcgLTEwLjYzOSAzMi44MDI0IC05LjY0NjIxIDMzLjg2MzNDLTUuMjU1NTcgMzguNTYzMyAxLjE1ODkxIDM1Ljg2NjcgMy40OTQ2NyAzMy41NDkzQzkuOTQ5NTggMjcuMTQ0MSAxNy40NTQzIDEzLjM1NTkgMjAuNDkwNCA3LjUwNTUyQzIwLjEzNjkgMTIuNjAxIDIwLjIxODMgMTcuNzE3MSAyMC43MzM4IDIyLjc5ODlDMTguMjg4NSAyMy45OTAxIDE1LjczODIgMjQuOTU1OSAxMy4xMTQ5IDI1LjY4NEMxMS42MTAyIDI2LjA3NTQgMTAuNjc5NiAyNi42ODM5IDEwLjY1MjEgMjcuMzc1NkMxMC42MjI1IDI4LjEzMzMgMTEuNjMyNiAyOC43NzI2IDEzLjA5MzMgMjkuNDYwOEMxNS42OTI2IDMwLjY4NjUgMjMuMzA4NSAzNC4yNTgyIDI1LjE4NTEgMzUuMzM4NEMyNi43OTE2IDM2LjI2MzggMjcuNTc1NyAzNS41NDIgMjguMDUxNSAzNC41NDI4QzI4LjY3MzIgMzMuMjQwNCAyNi45Njg1IDMyLjQ4NzggMjUuMzE2NSAzMS45OTc1QzI0LjU4NzQgMjkuMjQ4NSAyNC4wOTIxIDI2LjQ0MzcgMjMuODM1NiAyMy42MTI1QzI3LjcwNzEgMjEuMjQ3MSAzMS41MTg3IDE4LjA5MzIgMzMuNzE1OCAxNC4xNjAyQzM0LjQyNTIgMTIuNzc3OSAzNC45NTI1IDExLjMxMDggMzUuMjg0OCA5Ljc5NDk0QzM1LjUxNDggOC44NjE0NSAzNS41Nzc2IDcuODk1MDcgMzUuNDcwMyA2LjkzOTk3QzM1LjQ3MDMgNi45Mzk5NyAzNS40NDE0IDYuNzMxMzkgMzUuMjg0OCA2LjcxMTMyIiBmaWxsPSIjQzJDNkNFIi8+CjwvbWFzaz4KPGcgbWFzaz0idXJsKCNtYXNrMF81ODY1M18yMDM4ODApIj4KPHJlY3QgeD0iMC4wMjgzMjAzIiB5PSItMC4wMDE5NTMxMiIgd2lkdGg9IjMxLjY5NTQiIGhlaWdodD0iMzEuNjk1NCIgcng9IjMuMDQ3NjIiIGZpbGw9IndoaXRlIi8+CjwvZz4KPC9nPgo8ZGVmcz4KPGNsaXBQYXRoIGlkPSJjbGlwMF81ODY1M18yMDM4ODAiPgo8cmVjdCB5PSIwLjAwNTg1OTM4IiB3aWR0aD0iMzIiIGhlaWdodD0iMzEuOTkyMiIgcng9IjQiIGZpbGw9IndoaXRlIi8+CjwvY2xpcFBhdGg+CjwvZGVmcz4KPC9zdmc+Cg=="); + --goa-app-header-logo-desktop: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTE4IiBoZWlnaHQ9IjMyIiB2aWV3Qm94PSIwIDAgMTE4IDMyIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8ZyBjbGlwLXBhdGg9InVybCgjY2xpcDBfNTg0NDdfNDI3MDkpIj4KPHBhdGggZD0iTTExNy45NDYgMTZIMTA2Ljk3NFYyNi45NzE3SDExNy45NDZWMTZaIiBmaWxsPSIjMDBCNkVEIi8+CjxwYXRoIGQ9Ik00OS43OTY2IDI1LjMwMzZDNDguNzAxMSAyNS40MDg4IDQ3LjU1NzIgMjUuNTE0OCA0Ni4zNTQ5IDI1LjU2NjlDNDYuNzkyMiAyMi4yNDggNDguNTk0MyAxNy42NjczIDUwLjgzMzcgMTguMzk4N0M1Mi4xNDQ3IDE4LjgyMDIgNTEuNDM0NCAyMi43MTk4IDQ5Ljc5NTcgMjUuMzAxOEw0OS43OTY2IDI1LjMwMzZaTTQ3LjAwOTUgMjcuNTY3NEM0Ni42ODM4IDI3LjYyMDEgNDYuMzUxOSAyNy42MjAxIDQ2LjAyNjIgMjcuNTY3NEM0Ni4xMTEgMjcuNTA4NCA0Ni4xODAxIDI3LjQyOTYgNDYuMjI3OCAyNy4zMzc5QzQ2LjI3NTUgMjcuMjQ2MSA0Ni4zMDAzIDI3LjE0NDIgNDYuMzAwMSAyNy4wNDA4VjI2LjYxOTNDNDYuOTAwOCAyNi42MTkzIDQ3LjgyOTMgMjYuNTE0MiA0OC45NzY4IDI2LjQwODFDNDguNDcyMSAyNy4wMDczIDQ3Ljc3NzUgMjcuNDE2MyA0Ny4wMDk1IDI3LjU2NzRaTTYwLjY2NTkgMTkuNjY2QzYyLjA4NjQgMTguMzk4NyA2Mi43OTY3IDE4LjUwNjYgNjIuOTU3MyAxOC44MjNDNjMuMzM5OCAxOS41NjA4IDYxLjcwMTEgMjIuMTQyOCA1OC40Nzg1IDIzLjU2NTVDNTguODEwNCAyMi4wNzk3IDU5LjU3IDIwLjcyNCA2MC42NjMyIDE5LjY2Nkg2MC42NjU5Wk0xMTMuMzc0IDIwLjkzMDVDMTEzLjIxIDE4LjM0ODUgMTEwLjg2MiAxNy45MjcgMTEwLjUzNCAxOC42NjQ4QzExMC40MjUgMTguOTI4MSAxMTEuNDA4IDE4LjgyMyAxMTEuNDA4IDIwLjY2NzFDMTExLjQwOCAyMy42NzA3IDEwOC4yNCAyNy42MjIzIDEwNC4xOTYgMjcuNjIyM0MxMDIuNzkzIDI3LjcwNzEgMTAxLjQxMSAyNy4yNDQgMTAwLjM0MSAyNi4zMzAxQzk5LjI3MTkgMjUuNDE2MyA5OC41OTc1IDI0LjEyMjkgOTguNDYwMSAyMi43MjE2Qzk4LjI5NTcgMjEuNjE1MyA5OC41Njk2IDIwLjA4NjYgOTYuNjAzMiAyMC4yOTc4Qzk1LjIzMzggMjAuNDU1OSA5NC4wMzYgMjIuOTg0OSA5Mi4yODc4IDI1LjE0MzZDOTAuODEyNSAyNi45ODc4IDkwLjE1NyAyNi44Mjk2IDkwLjQ4NDcgMjUuMjQ4OEM5MC45MjIgMjMuMjQ2NCA5Mi42MTU1IDE4LjYwOTkgOTQuNTgxOSAxOC4yOTM2Qzk1LjUxMDQgMTguMTM1NCA5NS44MzgxIDE5LjY2NTEgOTYuMjI1MiAxOC43MTUxQzk2LjM2NDIgMTguMzgwNyA5Ni40MTg5IDE4LjAxNzEgOTYuMzg0NSAxNy42NTY1Qzk2LjM1MDEgMTcuMjk1OSA5Ni4yMjc2IDE2Ljk0OTQgOTYuMDI3OSAxNi42NDc0Qzk1LjgyODIgMTYuMzQ1MyA5NS41NTc0IDE2LjA5NzIgOTUuMjM5MyAxNS45MjQ3Qzk0LjkyMTIgMTUuNzUyMyA5NC41NjU3IDE1LjY2MDkgOTQuMjA0IDE1LjY1ODZDOTIuNzgzNSAxNS42NTg2IDkxLjA5IDE3LjEzNDMgODkuNjcwNCAxOC43NjcyQzg4LjQ2OSAyMC4yNDI5IDgyLjI5NTggMjguOTg5MiA3OS42NzM5IDI3LjA5MjlDNzguNDcyNSAyNi4xOTY5IDc4LjU3ODQgMjIuNjEyOCA3OS4zNDYyIDE4LjM5ODdDODAuNDU2IDE3LjkxNzYgODEuNjQ5OCAxNy42NjA2IDgyLjg1OTEgMTcuNjQyNkM4NC4wNjg0IDE3LjYyNDUgODUuMjY5MyAxNy44NDU4IDg2LjM5MyAxOC4yOTM2Qzg3LjEwMzMgMTguNjA5OSA4Ny4yMTQ2IDE4LjU1NjkgODYuODg1MSAxNy44MTkxQzg2LjQ0NzggMTYuNzEyOCA4My45OTAyIDE0Ljk3MzcgODAuMTEyMSAxNS43NjM3QzgwLjAwMjUgMTUuNzYzNyA3OS45NDc4IDE1LjgxNjcgNzkuODM4MiAxNS44MTY3QzgwLjE2NiAxNC40NDUzIDgwLjQ5MzcgMTMuMDI0NCA4MC45MzM3IDExLjY1MzlDODEuMzE2MiAxMC4zODk0IDgyLjM1NDIgOC4yMjg4NyA3OS41NjQzIDcuODYwNEM3OC42OTA3IDcuNzAyMjMgNzkuMDczMiA4LjEyMzcyIDc4Ljc0MjcgOS4yODMwN0M3OC4xOTUgMTEuMzkwNiA3Ny41NDEzIDE0LjEyODkgNzcuMDQ5MiAxNi45MjNDNzQuNTE3NiAxOC4zNjY4IDcyLjQ1MTYgMjAuNTA2MSA3MS4wOTUxIDIzLjA4ODJDNzEuNDEwNSAyMS44OTMyIDcxLjY0NzggMjAuNjc4OSA3MS44MDU0IDE5LjQ1MjlDNzEuODM2IDE5LjE0OTkgNzEuNzUyIDE4Ljg0NjQgNzEuNTY5OSAxOC42MDI0QzcxLjM4NzkgMTguMzU4NSA3MS4xMjEgMTguMTkxOCA3MC44MjIyIDE4LjEzNTRDNzAuMjIxNSAxNy45NzcyIDY5LjQ1MjggMTguMjQwNiA2OC43NDYyIDE5LjI0MTdDNjcuMDUyNyAyMS41NjA0IDY0LjkyMjkgMjUuMTk1NyA2MS42NDU1IDI2LjcyMzVDNTkuMjk2NSAyNy44Mjk5IDU4LjI1ODUgMjYuNzIzNSA1OC4yMDQ2IDI0Ljk4NjRDNTguNjE1NyAyNC44NzgxIDU5LjAxNzQgMjQuNzM3MSA1OS40MDYxIDI0LjU2NDlDNjMuNjY2NyAyMi43NzM3IDY1LjA4NzIgMjAuMDMzNSA2NC4wNDkyIDE4LjM0NzVDNjMuMDExMiAxNi43NjY3IDYwLjExNjMgMTcuMjQxMiA1Ny43Njc0IDE5LjYxMkM1Ni41Mjk5IDIwLjk4MTcgNTUuNzgwNiAyMi43MjM0IDU1LjYzNjYgMjQuNTY0OUM1NC42NTM0IDI0Ljc3NjEgNTMuNTYwNiAyNC45MzA2IDUyLjMwNDQgMjUuMDkxNUM1NC4yNzA5IDIxLjk4MjggNTQuMTA3NCAxNy43NjcgNTEuMjA4OSAxNy4wM0M0Ny44MjIgMTYuMTg3IDQ2LjEyODUgMTkuMzQ4NyA0NS40MTkxIDIxLjk4MjhDNDUuNjkzIDE5LjAzMjQgNDYuMTI5NCAxNi4wODEgNDYuNjc1MyAxMy4xODM1QzQ2Ljk0OTIgMTEuOTE5IDQ3LjgyMiA5Ljc1ODUxIDQ1LjAzMjEgOS4zOTAwNEM0NC4xNTg0IDkuMjMxODcgNDQuMjY3IDkuNjUzMzcgNDQuMzIxOCAxMC44MTI3QzQ0LjQzMTQgMTIuMzkzNiA0Mi42MjgzIDIxLjgyNTYgNDMuNTU2OCAyNS45MzU0QzQyLjM1NTQgMjYuMzAxMSA0MS44NjMzIDI3LjE5OTkgNDMuMzkyNCAyOC4wOTU5QzQ0LjMxNzQgMjguNDgxMiA0NS4zMTU5IDI4LjY1NzQgNDYuMzE2NyAyOC42MTE5QzQ3LjMxNzUgMjguNTY2MyA0OC4yOTU5IDI4LjMwMDMgNDkuMTgyMiAyNy44MzI2QzUwLjAwNjIgMjcuNDI5OCA1MC43NDcyIDI2Ljg3NTYgNTEuMzY2OCAyNi4xOTg3QzUyLjc4NzQgMjYuMDQwNiA1NC4yNjE3IDI1Ljc3NzIgNTUuNjI3NSAyNS41NjZDNTUuODQ1NiAyNy40MTAyIDU3LjEwMjcgMjguNzc5OCA1OS45NDI5IDI4LjUxNjVDNjMuOTg1MyAyOC4xNTA4IDY3LjU5MDQgMjMuMzUyNSA2OC45NTYyIDIxLjAzNDdDNjguNjgyMyAyMy41MTE2IDY3LjA0NDUgMjguOTM4IDY5Ljg4NDYgMjguNjc0N0M3MC45ODAxIDI4LjU2OTUgNzAuNTQwMSAyOC40MTE0IDcwLjU5NDkgMjcuNDYzMkM3MC44Njg3IDI0LjE5NjQgNzMuNjU0MSAyMS40MDQxIDc2LjQzNzYgMTkuNzE3MkM3NS45NDU1IDIzLjcyMTkgNzYuMTA5OCAyNy4zMDYgNzguMDIxNSAyOC40MTE0QzgxLjUxNzEgMzAuNTE4OCA4Ni4zNzkzIDI0Ljk4NjQgODkuMTA5OSAyMS42MTQ0Qzg3Ljc0MDUgMjQuNjE3OSA4Ni45NzkxIDI4LjQxMTQgODkuMDAwMyAyOC45OTFDOTEuNDA0MSAyOS42NzU4IDkzLjMxNTcgMjUuNzc3MiA5NS41NTUxIDIyLjgyNThDOTUuODI5IDI0LjkzMzMgOTcuMzAzNCAyOC42MjE2IDEwMy4yMDMgMjguNjIxNkMxMDkuNTM5IDI4LjU2ODYgMTEzLjUyNyAyNC44ODAzIDExMy4zNjMgMjAuOTI4NkwxMTMuMzc0IDIwLjkzMDVaTTI3LjgyMTYgMjcuNTExN0MyNS42OTI2IDI2Ljc2NTYgMjMuNjEyNyAyNS44ODU1IDIxLjU5NDUgMjQuODc2NkMyMy40MjM0IDI0LjIzMTIgMjUuMjExMSAyMy40NzQ1IDI2Ljk0NzkgMjIuNjExQzI3LjEzNzUgMjQuMjYxMSAyNy40MjkzIDI1Ljg5NzggMjcuODIxNiAyNy41MTE3Wk00My45NTEyIDMwLjQ2NzZDNDMuODk2NCAzMC4zMDk1IDQzLjQ1OTEgMzAuNTIwNyA0My4wNzc1IDMwLjQ2NzZDNDEuOTI5OSAzMC4zMDk1IDQwLjQwMDggMjguNzgxNyAzOS45NjM1IDI2LjMwNDhDMzkuMTQxOSAyMS44MjQ3IDM5LjYzNTggMTcuNDAwMyA0MS4wMDE1IDEwLjgxMzZDNDEuMjc1NCA5LjU0OTE0IDQyLjE0OSA3LjM4ODYyIDM5LjM1ODIgNi45NjcxMkMzOC40ODQ2IDYuODYxOTggMzguOTIwOSA3LjI4MzQ3IDM4LjcwMjcgOC4zODk3OUMzNy42MDcyIDEzLjI5MDUgMzMuNDU4OSAxNi44MjA2IDI5LjMwNzggMTkuMTkxNUMyOC44NzA2IDEzLjYwNTkgMjkuMDM0IDcuMzM1NTkgMzAuMTI5NSAzLjU0MjExQzMxLjA2MDcgMC4zMjgzMTQgMzIuMTUyNSAwLjkwNzk4NyAzMC43ODY4IDAuMjIzMTY4QzI5LjQyMSAtMC40NjE2NSAyNy44MzcxIDAuNDMzNDYgMjYuNTgwOSAyLjg1NTQ3QzI1LjMyNDggNS4yNzc0OCAxOS41OTM0IDE4LjI5NjMgMTAuNDEzIDI3LjA5NTdDNS43MTY5NiAzMS41NzQgMS40NTQ1MiAyOS4yNTYyIDAuNjM0NzEgMjguNTcxNEMtMC4wNzU1NDUzIDI3Ljk5MTcgLTAuMjkzNzM0IDI4Ljg4NzcgMC41MjUxNTkgMjkuNzgyOEM0LjI5NDYyIDMzLjc4NzUgOS44NjYyIDMxLjUyIDExLjg4NjUgMjkuNTcxNkMxNy40NTUzIDI0LjE5NzMgMjMuOTU4MSAxMi42MDQ4IDI2LjU4NDYgNy43MDQ5N0MyNi4yNzQ5IDExLjk4ODYgMjYuMzQ3OSAxNi4yOTE0IDI2LjgwMjggMjAuNTYyQzI0LjY5MTYgMjEuNTU3NCAyMi40OTg1IDIyLjM2ODQgMjAuMjQ4IDIyLjk4NThDMTguOTM3IDIzLjMwMjIgMTguMTE3MiAyMy44Mjg4IDE4LjExNzIgMjQuNDA4NUMxOC4xMTcyIDI0Ljk4ODIgMTguOTkwOSAyNS41Njc5IDIwLjI0OCAyNi4xNDU3QzIyLjQ4NzQgMjcuMTk5OSAyOS4wOTcgMzAuMTUwNCAzMC42ODA5IDMxLjA5ODVDMzIuMDUwMyAzMS44ODg1IDMyLjc1NjkgMzEuMjU2NyAzMy4xMzk0IDMwLjQxMzdDMzMuNjg3MSAyOS4zMDc0IDMyLjIxMDkgMjguNjc2NSAzMC43OTA0IDI4LjMwNjJDMzAuMTY0NyAyNS45OTE5IDI5LjcyNjMgMjMuNjMwOCAyOS40Nzk1IDIxLjI0NTlDMzIuODExNiAxOS4yNDM2IDM2LjE0MzggMTYuNjA5NCAzOC4wMDA3IDEzLjI5MTRDMzcuNTA5NiAxNi4wMzQzIDM2LjMwNzIgMjUuNDYzNiAzOS4zNzAxIDI5LjQxNTNDMzkuNzkzNiAyOS45NjA3IDQwLjM0MDEgMzAuMzk3OCA0MC45NjQ5IDMwLjY5MDdDNDEuNTg5NiAzMC45ODM2IDQyLjI3NDggMzEuMTI0IDQyLjk2NDMgMzEuMTAwM0M0My43MjkzIDMxLjA0NzMgNDQuMDU5OCAzMC41NzM3IDQzLjk0NzUgMzAuNDY3NiIgZmlsbD0iI0MyQzZDRSIvPgo8L2c+CjxkZWZzPgo8Y2xpcFBhdGggaWQ9ImNsaXAwXzU4NDQ3XzQyNzA5Ij4KPHJlY3Qgd2lkdGg9IjExNy45NDYiIGhlaWdodD0iMzIiIGZpbGw9IndoaXRlIi8+CjwvY2xpcFBhdGg+CjwvZGVmcz4KPC9zdmc+Cg=="); + + /* Footer - subdue links and text so they don't compete with main content */ + --goa-footer-color-links: #909090; + --goa-footer-color-links-hover: #aaaaaa; + --goa-footer-color-links-secondary: #808080; + --goa-footer-color-links-secondary-hover: #999999; + + /* Accordion hover border - greyscale-black (#d4d4d4) is too bright */ + --goa-accordion-border-hover: 1px solid #555555; + --goa-accordion-divider-hover: 1px solid #555555; + + /* Accordion shadow */ + --goa-accordion-shadow: 0px 0px 0px 0px transparent; + + /* App header dropdown shadow */ + --goa-app-header-nav-menu-dropdown-shadow: drop-shadow(0px 12px 16px rgba(0, 0, 0, 0.3)) + drop-shadow(0px 4px 6px rgba(0, 0, 0, 0.15)); + + /* Work side menu account popover */ + --goa-work-side-menu-account-bg: var(--goa-color-surface-widget); + --goa-work-side-menu-account-shadow: 0px 12px 20px -8px rgba(0, 0, 0, 0.5); + + /* Form page custom properties */ + --task-group-bg: var(--goa-color-surface-section); + --app-card-bg: var(--goa-color-surface-app-header); + --task-item-border: #3d3d3d; + --task-item-hover-bg: #303030; + --goa-form-summary-bg: var(--goa-color-surface-section); + --case-detail-header-bg: var(--goa-color-surface-card); + + /* Highlighted stat card icons */ + --dashboard-highlight-icon-bg: #3d1c1c; + + /* Stat card tints - barely perceptible color shifts from the #2e2e2e base */ + --dashboard-tint-emergency-bg: #322c2c; + --dashboard-tint-emergency-hover-bg: #383030; + --dashboard-tint-success-bg: #2c322c; + --dashboard-tint-success-hover-bg: #303830; + --dashboard-tint-info-bg: #2c2c32; + --dashboard-tint-info-hover-bg: #303038; + + /* ========================================================================== + SURFACE ELEVATION SYSTEM + ========================================================================== + In light mode, shadows create depth. In dark mode, lighter surfaces do. + These overrides establish a proper elevation hierarchy: + + Level 0 - Page/app background (darkest): #1a1a1a + Level 1 - Content card, sidebar: #232323 + Level 2 - Widgets, stat cards: #2a2a2a + Level 3 - Items within widgets: #303030 + ========================================================================== */ + + /* Level 0: Page background - darkest layer */ + & .app-layout { + background-color: var(--goa-color-surface-page); + } + + /* Level 1: Main content card - elevated above page */ + & .desktop-card-container, + & .mobile-content-container { + background-color: var(--goa-color-surface-card); + } + + /* Level 1: Push drawer clip - same elevation as content card */ + & .push-drawer-clip { + background: var(--goa-color-surface-card); + } + + /* Level 2: Widgets and stat cards - elevated above content card */ + & .dashboard-widget, + & + .dashboard-stat-card:not(.dashboard-stat-card--emergency):not( + .dashboard-stat-card--success + ):not(.dashboard-stat-card--info) { + background: var(--goa-color-surface-widget); + } + + /* Level 2: Widget headers - match body (darker header creates "hole" effect) */ + & .dashboard-widget__header { + background: var(--goa-color-surface-widget); + } + + /* Level 3: Items within widgets - elevated above widget */ + & .dashboard-queue-card, + & .dashboard-recent-item { + background: var(--goa-color-surface-item); + } + + /* Level 3: Hover states */ + & .dashboard-queue-card:hover, + & .dashboard-recent-item:hover { + background: var(--goa-color-surface-item-hover); + } + + & .dashboard-stat-card--clickable:hover { + background: var(--goa-color-surface-item); + } + + /* Stat card tint borders and icon backgrounds */ + & .dashboard-stat-card--emergency { + border-color: #4a3535; + } + + & .dashboard-stat-card--emergency .dashboard-stat-card__icon { + background: #4a3030; + } + + & .dashboard-stat-card--success { + border-color: #354a35; + } + + & .dashboard-stat-card--success .dashboard-stat-card__icon { + background: #304a30; + } + + & .dashboard-stat-card--info { + border-color: #35354a; + } + + & .dashboard-stat-card--info .dashboard-stat-card__icon { + background: #30304a; + } + + /* Expandable list items - elevated surface matching container cards */ + & .expandable-list__item { + background: var(--goa-color-surface-widget); + } + + & .expandable-list__header { + background-color: var(--goa-color-surface-widget); + border-radius: var(--goa-border-radius-xl); + } + + & .expandable-list__item--expanded .expandable-list__header { + border-radius: var(--goa-border-radius-xl) var(--goa-border-radius-xl) 0 0; + } + + & .expandable-list__header:hover { + background-color: var(--goa-color-surface-widget-hover); + } + + & .expandable-list__content { + background-color: var(--goa-color-surface-content-body); + border-radius: 0 0 var(--goa-border-radius-xl) var(--goa-border-radius-xl); + } + + /* Timeline items - elevated surface */ + & .timeline-page__item, + & .timeline__item { + background: var(--goa-color-surface-widget); + } + + /* Comment card - subtle surface so it reads as a card, not just a border */ + & .page__comments_single { + background: var(--goa-color-surface-input); + } + + /* Page header/footer - match content card level */ + & .page-header, + & .page-footer, + & .desktop-card-container .page-header { + background-color: var(--goa-color-surface-card); + } + + /* Error page icon wrapper */ + & .error-page-icon-wrapper { + background-color: var(--goa-color-surface-content-body); + } + + /* Stat card icons - subtle elevation above card surface */ + & .dashboard-stat-card__icon { + background: var(--goa-color-surface-heading); + } + + /* ========================================================================== + SCROLL SHADOW OVERRIDES + ========================================================================== + Light mode uses rgba(0,0,0,0.08) which is invisible on dark backgrounds. + Use stronger black gradients for dark mode. + ========================================================================== */ + + /* Horizontal scroll shadows (data table) */ + & .scroll-container-shadow--left { + background: linear-gradient(to right, rgba(0, 0, 0, 0.3), transparent); + } + + & .scroll-container-shadow--right { + background: linear-gradient(to left, rgba(0, 0, 0, 0.3), transparent); + } + + /* Page header/footer scroll shadows */ + & .page-header::after { + background: linear-gradient(to bottom, rgba(0, 0, 0, 0.3), transparent); + } + + & .page-footer::before { + background: linear-gradient(to top, rgba(0, 0, 0, 0.3), transparent); + } + + /* Notification header/footer shadows */ + & .notification-header::after { + background: linear-gradient(to bottom, rgba(0, 0, 0, 0.2), transparent); + } + + & .notification-footer::before { + background: linear-gradient(to top, rgba(0, 0, 0, 0.2), transparent); + } + + /* Body background - @abgov/style hardcodes background:#fff on body */ + & body { + background: var(--goa-color-greyscale-white); + } + + /* Input leading icons - tone down from full white. Trailing icon buttons keep default for interactivity. */ + & goa-input { + --fill-color: #aaaaaa; + } + + /* Icon button hover bg - greyscale-100 (#2a2a2a) is too close to input bg (#262626) */ + --goa-icon-button-default-hover-color-bg: #3a3a3a; + --goa-icon-button-dark-hover-color-bg: #3a3a3a; + + /* Footer text (spans for copyright/description) - match subdued link color */ + & goa-app-footer { + color: #909090; + } + + /* Popover shadow */ + & goa-work-side-menu { + --goa-popover-box-shadow: 0 4px 6px rgba(0, 0, 0, 0.4); + } +} diff --git a/apps/prs/react/src/routes/everything.tsx b/apps/prs/react/src/routes/everything.tsx index 838fce95a9..5017cb244f 100644 --- a/apps/prs/react/src/routes/everything.tsx +++ b/apps/prs/react/src/routes/everything.tsx @@ -1133,6 +1133,17 @@ export function EverythingRoute(): JSX.Element { ))} + + + + + + + diff --git a/apps/prs/react/src/surface-tokens.css b/apps/prs/react/src/surface-tokens.css new file mode 100644 index 0000000000..3acc289329 --- /dev/null +++ b/apps/prs/react/src/surface-tokens.css @@ -0,0 +1,75 @@ +/* ============================================================================ + SURFACE TOKENS - Two-layer elevation system + ============================================================================ + Layer 1: Scale (numbered, the palette) + Layer 2: Semantic (named, reference the scale) + + In light mode, most surfaces resolve to white or near-white because + shadows handle depth. In dark mode, lighter surfaces = higher elevation. + ============================================================================ */ + +/* Light mode defaults */ +:root { + /* Scale tokens */ + --goa-color-surface-0: var(--goa-color-greyscale-50); /* #f8f8f8 */ + --goa-color-surface-50: var(--goa-color-greyscale-white); /* #ffffff */ + --goa-color-surface-100: var(--goa-color-greyscale-white); /* #ffffff */ + --goa-color-surface-150: var(--goa-color-greyscale-50); /* #f8f8f8 */ + --goa-color-surface-200: var(--goa-color-greyscale-50); /* #f8f8f8 */ + --goa-color-surface-250: var(--goa-color-greyscale-100); /* #f2f0f0 */ + --goa-color-surface-300: var(--goa-color-greyscale-white); /* #ffffff */ + --goa-color-surface-350: var(--goa-color-greyscale-100); /* #f2f0f0 */ + --goa-color-surface-400: var(--goa-color-greyscale-50); /* #f8f8f8 */ + + /* Semantic tokens - primary surfaces */ + --goa-color-surface-page: var(--goa-color-surface-0); + --goa-color-surface-default: var(--goa-color-surface-50); + --goa-color-surface-card: var(--goa-color-surface-100); + --goa-color-surface-section: var(--goa-color-surface-150); + --goa-color-surface-input: var(--goa-color-surface-200); + --goa-color-surface-content-body: var(--goa-color-surface-250); + --goa-color-surface-widget: var(--goa-color-surface-300); + --goa-color-surface-heading: var(--goa-color-surface-350); + --goa-color-surface-item: var(--goa-color-surface-400); + + /* Semantic tokens - additional surfaces */ + --goa-color-surface-table-data: var(--goa-color-greyscale-white); + --goa-color-surface-container-heading: var(--goa-color-greyscale-100); + --goa-color-surface-popover: var(--goa-color-greyscale-white); + --goa-color-surface-app-header: var(--goa-color-greyscale-white); + + /* Hover tokens */ + --goa-color-surface-card-hover: var(--goa-color-greyscale-50); + --goa-color-surface-widget-hover: var(--goa-color-greyscale-50); + --goa-color-surface-heading-hover: var(--goa-color-greyscale-150); + --goa-color-surface-item-hover: var(--goa-color-greyscale-100); + + /* Static white - never flips, for white pigment on colored backgrounds */ + --goa-color-static-white: #ffffff; +} + +/* Dark mode overrides */ +:root[data-theme="dark"] { + /* Scale */ + --goa-color-surface-0: #1a1a1a; + --goa-color-surface-50: #1e1e1e; + --goa-color-surface-100: #232323; + --goa-color-surface-150: #252525; + --goa-color-surface-200: #262626; + --goa-color-surface-250: #292929; + --goa-color-surface-300: #2e2e2e; + --goa-color-surface-350: #333333; + --goa-color-surface-400: #383838; + + /* Additional surfaces - independent values tuned for dark mode */ + --goa-color-surface-table-data: #2c2c2c; + --goa-color-surface-container-heading: #353535; + --goa-color-surface-popover: #242424; + --goa-color-surface-app-header: #222222; + + /* Hover tokens */ + --goa-color-surface-card-hover: #2e2e2e; + --goa-color-surface-widget-hover: #3a3a3a; + --goa-color-surface-heading-hover: #3f3f3f; + --goa-color-surface-item-hover: #444444; +} diff --git a/libs/angular-components/src/index.ts b/libs/angular-components/src/index.ts index 55aebfe4b6..10e50c69ad 100644 --- a/libs/angular-components/src/index.ts +++ b/libs/angular-components/src/index.ts @@ -1,3 +1,4 @@ export * from "./lib/angular-components.module"; export * from "./lib/components"; +export * from "./lib/theme/theme.service"; export * from "@abgov/ui-components-common"; diff --git a/libs/angular-components/src/lib/theme/theme.service.ts b/libs/angular-components/src/lib/theme/theme.service.ts new file mode 100644 index 0000000000..a1e79a87bb --- /dev/null +++ b/libs/angular-components/src/lib/theme/theme.service.ts @@ -0,0 +1,53 @@ +import { Injectable, effect, signal, type Signal } from "@angular/core"; + +export type GoabThemeMode = "light" | "dark"; + +const STORAGE_KEY = "goab-theme"; +const ATTRIBUTE = "data-theme"; + +@Injectable({ providedIn: "root" }) +export class GoabThemeService { + private readonly _mode = signal(this.readInitialMode()); + + /** Read-only signal for consumers. Use in templates: theme.mode() === 'dark'. */ + readonly mode: Signal = this._mode.asReadonly(); + + constructor() { + effect(() => { + const current = this._mode(); + if (typeof document === "undefined") return; + document.documentElement.setAttribute(ATTRIBUTE, current); + try { + window.localStorage.setItem(STORAGE_KEY, current); + } catch { + // localStorage blocked: fail silently. + } + }); + } + + setMode(next: GoabThemeMode): void { + this._mode.set(next); + } + + toggle(): void { + this._mode.update((prev) => (prev === "light" ? "dark" : "light")); + } + + private readInitialMode(): GoabThemeMode { + if (typeof window === "undefined") return "light"; + + try { + const stored = window.localStorage.getItem(STORAGE_KEY); + if (stored === "light" || stored === "dark") return stored; + } catch { + // fall through + } + + const fromAttribute = document.documentElement.getAttribute(ATTRIBUTE); + if (fromAttribute === "light" || fromAttribute === "dark") return fromAttribute; + + if (window.matchMedia("(prefers-color-scheme: dark)").matches) return "dark"; + + return "light"; + } +} diff --git a/libs/react-components/src/index.ts b/libs/react-components/src/index.ts index f51ac6b0b9..9131e5d6a3 100644 --- a/libs/react-components/src/index.ts +++ b/libs/react-components/src/index.ts @@ -80,3 +80,4 @@ export * from "./lib/work-side-menu-group/work-side-menu-group"; export * from "./lib/work-side-menu-item/work-side-menu-item"; export * from "./lib/work-side-notification-item/work-side-notification-item"; export * from "./lib/work-side-notification-panel/work-side-notification-panel"; +export * from "./lib/theme/theme-context"; diff --git a/libs/react-components/src/lib/theme/theme-context.tsx b/libs/react-components/src/lib/theme/theme-context.tsx new file mode 100644 index 0000000000..ceaa1094a9 --- /dev/null +++ b/libs/react-components/src/lib/theme/theme-context.tsx @@ -0,0 +1,88 @@ +import { + createContext, + useCallback, + useContext, + useEffect, + useState, + type ReactNode, +} from "react"; + +export type GoabThemeMode = "light" | "dark"; + +interface GoabThemeContextValue { + mode: GoabThemeMode; + setMode: (mode: GoabThemeMode) => void; + toggle: () => void; +} + +const STORAGE_KEY = "goab-theme"; +const ATTRIBUTE = "data-theme"; + +const GoabThemeContext = createContext(null); + +function readInitialMode(defaultMode: GoabThemeMode): GoabThemeMode { + if (typeof window === "undefined") return defaultMode; + + // Priority: localStorage > existing data-theme > system preference > defaultMode. + // Reading the attribute allows a blocking