diff --git a/apps/website/src/App.tsx b/apps/website/src/App.tsx index a1dcf97..5f39b2c 100644 --- a/apps/website/src/App.tsx +++ b/apps/website/src/App.tsx @@ -15,9 +15,9 @@ import { ThemePreferences, } from './components'; import { Button } from './components/button'; +import { PartnerDashboardMock } from './components/hero/partner-dashboard-mock'; import { GetStarted } from './components/interactive/get-started'; import { ThemeColorMeta } from './components/theme-color-meta'; -import { ThemePreview } from './components/theme-preview'; import { themeColorToPlaygroundPathHex } from './utils/playground-url-hex'; const externalLinkIcon = () => ( @@ -26,6 +26,20 @@ const externalLinkIcon = () => ( const PLAYGROUND_ORIGIN = 'https://playground.hextimator.com'; +function HeroWithDashboard() { + const { color } = useHextimatorTheme(); + return ( + <> + +
+
+ +
+
+ + ); +} + function PlaygroundSection() { const { color } = useHextimatorTheme(); const playgroundHref = useMemo( @@ -65,14 +79,11 @@ function App() {
- +
-
- -
( return (
- +
e.preventDefault()} onFocusOutside={(e) => e.preventDefault()} onInteractOutside={(e) => { diff --git a/apps/website/src/components/hero/hero.tsx b/apps/website/src/components/hero/hero.tsx index d923636..4bc10e5 100644 --- a/apps/website/src/components/hero/hero.tsx +++ b/apps/website/src/components/hero/hero.tsx @@ -20,8 +20,8 @@ function tryApplyColor(value: string, setColor: (c: string) => void) { } export function Hero() { - const { color: currentColor, setColor } = useHextimatorTheme(); - const [initialColor] = useState(currentColor); + const { color, setColor } = useHextimatorTheme(); + const [initialColor] = useState(color); const [input, setInput] = useState(''); const [pickerOpen, setPickerOpen] = useState(false); @@ -57,8 +57,6 @@ export function Hero() { }; const handleFocus = () => { - // Only open picker on focus if cycler is already stopped - // (prevents Safari's spurious focus events during value updates) if (isActive) return; setPickerOpen(true); }; @@ -87,65 +85,76 @@ export function Hero() { restart(input); }; + const hintFadeStyle = { + opacity: showHint ? 0.6 : 0, + transition: 'opacity 300ms ease-in-out', + } satisfies React.CSSProperties; + return ( -
-
- - pick any hex color - - -
-

Hextimator: one color in, branded theme out

-
-
- One - - - - in. +
+
+
+

+ Hextimator: one color in, branded theme out +

+
+
+
+ + pick any hex color + + +
+ One +
+ + + +
+ in. +
+
+ Whole + theme + out. +
+
+

+ Swap the brand color, and every shade, scale, and contrast ratio + regenerates itself. +

+
+ + +
-
- Whole - theme - out. -
-
-

- Swap the brand color, and every shade, scale, and contrast ratio - regenerates itself. -

-
- -
); diff --git a/apps/website/src/components/hero/partner-dashboard-mock.tsx b/apps/website/src/components/hero/partner-dashboard-mock.tsx new file mode 100644 index 0000000..7ede431 --- /dev/null +++ b/apps/website/src/components/hero/partner-dashboard-mock.tsx @@ -0,0 +1,178 @@ +import type { CSSProperties } from 'react'; +import { useEffect, useMemo, useState } from 'react'; +import { cn } from '../../utils/cn'; + +const BAR_DEFS = [ + { id: 'b1', tone: 'bg-accent' }, + { id: 'b2', tone: 'bg-accent-strong' }, + { id: 'b3', tone: 'bg-accent-weak' }, + { id: 'b4', tone: 'bg-accent' }, + { id: 'b5', tone: 'bg-accent-strong' }, + { id: 'b6', tone: 'bg-accent-weak' }, + { id: 'b7', tone: 'bg-accent-strong' }, +] as const; + +function chartHeightPct() { + return `${Math.round(28 + Math.random() * 72)}%`; +} + +function barHeightPx(h: string) { + return Number.parseFloat(h); +} + +function barsWithRandomHeights(_accentKey: string) { + const rows = BAR_DEFS.map((bar) => ({ + ...bar, + h: chartHeightPct(), + })); + + let lowest = Infinity; + let lowestIdx = 0; + for (let i = 0; i < rows.length; i++) { + const v = barHeightPx(rows[i].h); + if (v < lowest) { + lowest = v; + lowestIdx = i; + } + } + + return rows.map((row, i) => ({ + ...row, + tone: i === lowestIdx ? 'bg-negative' : BAR_DEFS[i].tone, + })); +} + +function mobileMainChartIndices(hs: readonly { h: string }[]) { + let lo = 0; + for (let i = 1; i < hs.length; i++) { + if (barHeightPx(hs[i].h) < barHeightPx(hs[lo].h)) lo = i; + } + const picked = new Set([lo]); + for (let i = 0; i < hs.length && picked.size < 3; i++) { + picked.add(i); + } + return [...picked].sort((a, b) => a - b); +} + +type PartnerDashboardMockProps = { + accentColor: string; +}; + +export function PartnerDashboardMock({ + accentColor, +}: PartnerDashboardMockProps) { + const [cardRotationDeg] = useState(() => { + const sign = Math.random() < 0.5 ? -1 : 1; + const value = Math.round((Math.random() * 0.4 + 0.4) * 10) / 10; + return sign * value; + }); + + const [chartBars, setChartBars] = useState(() => barsWithRandomHeights('')); + + const mobileBarIndices = useMemo( + () => mobileMainChartIndices(chartBars), + [chartBars], + ); + + useEffect(() => { + setChartBars(barsWithRandomHeights(accentColor)); + }, [accentColor]); + + return ( +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+ +
+
+ {(['left', 'mid', 'right'] as const).map((slotKey, i) => { + const barIndex = mobileBarIndices[i]; + const bar = chartBars[barIndex]; + return ( +
+ ); + })} +
+
+ {chartBars.map((bar) => ( +
+ ))} +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+ ); +} diff --git a/apps/website/src/components/theme-preview/index.ts b/apps/website/src/components/theme-preview/index.ts index b57a2a5..4f34df8 100644 --- a/apps/website/src/components/theme-preview/index.ts +++ b/apps/website/src/components/theme-preview/index.ts @@ -1 +1 @@ -export { ThemePreview } from './theme-preview'; +export { getThemePreviewEntries, ThemePreview } from './theme-preview'; diff --git a/apps/website/src/components/theme-preview/theme-preview.tsx b/apps/website/src/components/theme-preview/theme-preview.tsx index e2f708c..4389fcd 100644 --- a/apps/website/src/components/theme-preview/theme-preview.tsx +++ b/apps/website/src/components/theme-preview/theme-preview.tsx @@ -4,6 +4,7 @@ import type { ThemePreviewProps } from './theme-preview.types'; const FOREGROUND_SUFFIX = '-foreground'; const SEMANTIC_ROLES = new Set(['positive', 'negative', 'caution']); +const ROLE_ORDER = ['accent', 'surface', 'positive', 'negative', 'caution']; function getRole(token: string) { return token.split('-')[0]; @@ -18,8 +19,28 @@ function getForegroundToken(token: string) { return `${getRole(token)}${FOREGROUND_SUFFIX}`; } +export function getThemePreviewEntries( + tokens: Record, +): [string, string][] { + return Object.entries(tokens) + .filter(([key]) => { + if (key.endsWith(FOREGROUND_SUFFIX)) return false; + if (key === 'brand-exact') return false; + const role = getRole(key); + const variant = getVariant(key); + if (SEMANTIC_ROLES.has(role) && variant !== null) return false; + return true; + }) + .sort(([a], [b]) => { + const ra = ROLE_ORDER.indexOf(getRole(a)); + const rb = ROLE_ORDER.indexOf(getRole(b)); + return (ra === -1 ? 99 : ra) - (rb === -1 ? 99 : rb); + }); +} + export function ThemePreview({ defaultActive = null, + className, ...props }: ThemePreviewProps) { const { palette, mode } = useHextimatorTheme(); @@ -44,29 +65,13 @@ export function ThemePreview({ }, [clicked, defaultActive]); const tokens = palette[mode] as Record; - - const ROLE_ORDER = ['accent', 'surface', 'positive', 'negative', 'caution']; - - const entries = Object.entries(tokens) - .filter(([key]) => { - if (key.endsWith(FOREGROUND_SUFFIX)) return false; - if (key === 'brand-exact') return false; - const role = getRole(key); - const variant = getVariant(key); - if (SEMANTIC_ROLES.has(role) && variant !== null) return false; - return true; - }) - .sort(([a], [b]) => { - const ra = ROLE_ORDER.indexOf(getRole(a)); - const rb = ROLE_ORDER.indexOf(getRole(b)); - return (ra === -1 ? 99 : ra) - (rb === -1 ? 99 : rb); - }); + const entries = getThemePreviewEntries(tokens); return (
{entries.map(([token, color]) => { const isActive = active === token; @@ -76,7 +81,7 @@ export function ThemePreview({