Skip to content

Commit d22a568

Browse files
committed
feat(auth-ui): UI foundation for rebuilt auth flows — layouts, theming, favicons, login UX
- layouts: split.layout + blank.layout; useSystemTheme hook - misc: dynamic-favicon + client-hints, wired into root.tsx - styling: type-scale + a11y tweaks in root.css - login: identifier screen with IdP buttons + progressive email reveal - assets: favicons (light/dark), IdP logos, illustrations under public/ - config: vite.config + package.json/bun.lock supporting deps
1 parent 1caffb6 commit d22a568

73 files changed

Lines changed: 707 additions & 111 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.

app/components/auth-form/auth-form.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Button } from '@datum-cloud/datum-ui/button';
2+
import { cn } from '@datum-cloud/datum-ui/utils';
23
import type { ReactNode } from 'react';
34
import { useNavigation } from 'react-router';
45

@@ -24,7 +25,7 @@ export function SubmitButton({ children, loading, className }: SubmitButtonProps
2425
block
2526
htmlType="submit"
2627
loading={isSubmitting}
27-
className={className}>
28+
className={cn('h-9', className)}>
2829
{children}
2930
</Button>
3031
);
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* This file contains utilities for using client hints for user preference which
3+
* are needed by the server, but are only known by the browser.
4+
*/
5+
import { getHintUtils } from '@epic-web/client-hints';
6+
import {
7+
clientHint as colorSchemeHint,
8+
subscribeToSchemeChange,
9+
} from '@epic-web/client-hints/color-scheme';
10+
import { clientHint as timeZoneHint } from '@epic-web/client-hints/time-zone';
11+
import * as React from 'react';
12+
import { useRevalidator } from 'react-router';
13+
14+
const hintsUtils = getHintUtils({
15+
theme: colorSchemeHint,
16+
timeZone: timeZoneHint,
17+
// add other hints here
18+
});
19+
20+
export const { getHints } = hintsUtils;
21+
22+
/**
23+
* @returns inline script element that checks for client hints and sets cookies
24+
* if they are not set then reloads the page if any cookie was set to an
25+
* inaccurate value.
26+
*/
27+
export function ClientHintCheck({ nonce }: { nonce: string }) {
28+
const { revalidate } = useRevalidator();
29+
React.useEffect(() => subscribeToSchemeChange(() => revalidate()), [revalidate]);
30+
31+
// Ensure nonce is always a string to match server rendering
32+
const nonceValue = nonce || '';
33+
34+
return (
35+
<script
36+
nonce={nonceValue}
37+
dangerouslySetInnerHTML={{
38+
__html: hintsUtils.getClientHintCheckScript(),
39+
}}
40+
/>
41+
);
42+
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import { useSystemTheme } from '@/hooks/useSystemTheme';
2+
import * as React from 'react';
3+
4+
// Favicon configuration for responsive theme support
5+
const FAVICON_CONFIGS = [
6+
// Standard favicons - all sizes
7+
{
8+
rel: 'icon' as const,
9+
type: 'image/png',
10+
sizes: '16x16',
11+
filename: 'favicon-16x16.png',
12+
},
13+
{
14+
rel: 'icon' as const,
15+
type: 'image/png',
16+
sizes: '32x32',
17+
filename: 'favicon-32x32.png',
18+
},
19+
{
20+
rel: 'icon' as const,
21+
type: 'image/png',
22+
sizes: '96x96',
23+
filename: 'favicon-96x96.png',
24+
},
25+
{
26+
rel: 'icon' as const,
27+
type: 'image/png',
28+
sizes: '128x128',
29+
filename: 'favicon-128x128.png',
30+
},
31+
{
32+
rel: 'icon' as const,
33+
type: 'image/png',
34+
sizes: '196x196',
35+
filename: 'favicon-196x196.png',
36+
},
37+
{
38+
rel: 'icon' as const,
39+
sizes: 'any',
40+
filename: 'favicon.ico',
41+
},
42+
// Apple touch icons - all sizes
43+
{
44+
rel: 'apple-touch-icon' as const,
45+
sizes: '57x57',
46+
filename: 'apple-touch-icons/apple-touch-icon-57x57.png',
47+
},
48+
{
49+
rel: 'apple-touch-icon' as const,
50+
sizes: '60x60',
51+
filename: 'apple-touch-icons/apple-touch-icon-60x60.png',
52+
},
53+
{
54+
rel: 'apple-touch-icon' as const,
55+
sizes: '72x72',
56+
filename: 'apple-touch-icons/apple-touch-icon-72x72.png',
57+
},
58+
{
59+
rel: 'apple-touch-icon' as const,
60+
sizes: '76x76',
61+
filename: 'apple-touch-icons/apple-touch-icon-76x76.png',
62+
},
63+
{
64+
rel: 'apple-touch-icon' as const,
65+
sizes: '114x114',
66+
filename: 'apple-touch-icons/apple-touch-icon-114x114.png',
67+
},
68+
{
69+
rel: 'apple-touch-icon' as const,
70+
sizes: '120x120',
71+
filename: 'apple-touch-icons/apple-touch-icon-120x120.png',
72+
},
73+
{
74+
rel: 'apple-touch-icon' as const,
75+
sizes: '144x144',
76+
filename: 'apple-touch-icons/apple-touch-icon-144x144.png',
77+
},
78+
{
79+
rel: 'apple-touch-icon' as const,
80+
sizes: '152x152',
81+
filename: 'apple-touch-icons/apple-touch-icon-152x152.png',
82+
},
83+
{
84+
rel: 'apple-touch-icon' as const,
85+
sizes: '167x167',
86+
filename: 'apple-touch-icons/apple-touch-icon-167x167.png',
87+
},
88+
{
89+
rel: 'apple-touch-icon' as const,
90+
sizes: '180x180',
91+
filename: 'apple-touch-icons/apple-touch-icon-180x180.png',
92+
},
93+
] as const;
94+
95+
// Microsoft tile configuration (uses meta tags, not link tags)
96+
const MSTILE_CONFIGS = [
97+
{
98+
name: 'msapplication-TileImage',
99+
filename: 'mstile/mstile-144x144.png',
100+
},
101+
{
102+
name: 'msapplication-square70x70logo',
103+
filename: 'mstile/mstile-70x70.png',
104+
},
105+
{
106+
name: 'msapplication-square150x150logo',
107+
filename: 'mstile/mstile-150x150.png',
108+
},
109+
{
110+
name: 'msapplication-wide310x150logo',
111+
filename: 'mstile/mstile-310x150.png',
112+
},
113+
{
114+
name: 'msapplication-square310x310logo',
115+
filename: 'mstile/mstile-310x310.png',
116+
},
117+
] as const;
118+
119+
/**
120+
* Dynamically updates favicon based on system theme changes
121+
* Uses matchMedia to detect system theme preferences and automatically
122+
* switches between light and dark favicons accordingly.
123+
*/
124+
export const DynamicFaviconLinks = () => {
125+
const isDarkMode = useSystemTheme();
126+
127+
React.useEffect(() => {
128+
// Only run in browser environment
129+
if (typeof document === 'undefined') return;
130+
131+
// Remove existing favicon links to avoid duplicates
132+
const existingLinks = document.querySelectorAll('link[rel*="icon"]');
133+
existingLinks.forEach((link) => link.remove());
134+
135+
// Remove existing Microsoft tile meta tags
136+
const existingMetaTiles = document.querySelectorAll('meta[name*="msapplication"]');
137+
existingMetaTiles.forEach((meta) => meta.remove());
138+
139+
// Choose favicon based on system theme
140+
// Dark favicon for light theme, light favicon for dark theme
141+
const themeFolder = isDarkMode ? 'light' : 'dark';
142+
143+
// Add new favicon links based on current theme
144+
FAVICON_CONFIGS.forEach((config) => {
145+
const link = document.createElement('link');
146+
link.rel = config.rel;
147+
148+
if ('type' in config) {
149+
link.type = config.type;
150+
}
151+
if ('sizes' in config) {
152+
link.setAttribute('sizes', config.sizes);
153+
}
154+
155+
link.href = `/favicons/${themeFolder}/${config.filename}`;
156+
157+
// Add the link to the document head
158+
document.head.appendChild(link);
159+
});
160+
161+
// Add Microsoft tile meta tags
162+
MSTILE_CONFIGS.forEach((config) => {
163+
const meta = document.createElement('meta');
164+
meta.name = config.name;
165+
meta.content = `/favicons/${themeFolder}/${config.filename}`;
166+
document.head.appendChild(meta);
167+
});
168+
}, [isDarkMode]);
169+
170+
// Return static links for SSR (will be replaced by useEffect on client)
171+
return <></>;
172+
};

app/hooks/useSystemTheme.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { useEffect, useState } from 'react';
2+
3+
/**
4+
* Custom hook to detect system theme changes using matchMedia
5+
*
6+
* @returns {boolean} isDarkMode - Whether the system is currently in dark mode
7+
*
8+
* @example
9+
* ```tsx
10+
* const isDarkMode = useSystemTheme();
11+
*
12+
* return (
13+
* <div className={isDarkMode ? 'dark-theme' : 'light-theme'}>
14+
* Current theme: {isDarkMode ? 'Dark' : 'Light'}
15+
* </div>
16+
* );
17+
* ```
18+
*/
19+
export function useSystemTheme(): boolean {
20+
const [isDarkMode, setIsDarkMode] = useState(false);
21+
22+
useEffect(() => {
23+
// Check if we're in the browser environment
24+
if (typeof window === 'undefined') return;
25+
26+
// Create media query matcher for dark mode
27+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
28+
29+
// Set initial state
30+
setIsDarkMode(mediaQuery.matches);
31+
32+
// Create event handler for theme changes
33+
const handleThemeChange = (event: MediaQueryListEvent) => {
34+
setIsDarkMode(event.matches);
35+
};
36+
37+
// Add listener for theme changes
38+
mediaQuery.addEventListener('change', handleThemeChange);
39+
40+
// Cleanup listener on unmount
41+
return () => {
42+
mediaQuery.removeEventListener('change', handleThemeChange);
43+
};
44+
}, []);
45+
46+
return isDarkMode;
47+
}

app/layouts/blank.layout.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Logo } from '@datum-cloud/datum-ui/logo';
2+
import { cn } from '@datum-cloud/datum-ui/utils';
3+
4+
export default function BlankLayout({
5+
children,
6+
className,
7+
}: {
8+
children: React.ReactNode;
9+
className?: string;
10+
}) {
11+
return (
12+
<div
13+
className={cn(
14+
'bg-background relative flex min-h-screen w-full flex-col p-3 sm:p-4 md:px-[41px] md:py-8',
15+
className
16+
)}>
17+
<div className="justify-flex-start mb-6 flex items-center">
18+
<Logo.Flat aria-label="Datum" className="h-6 w-auto" tone="brand" />
19+
</div>
20+
{children}
21+
</div>
22+
);
23+
}

0 commit comments

Comments
 (0)