From f62b49b5a5983e29d1e6765f2733f3743c687ace Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=20Grgi=C4=87?= Date: Thu, 14 May 2026 19:07:30 +0200 Subject: [PATCH 1/4] feat: add dashboard skeleton to the landing page --- apps/website/src/App.tsx | 6 +- .../hero/color-input/color-input.tsx | 4 +- .../hero/color-picker/color-picker.tsx | 4 +- apps/website/src/components/hero/hero.tsx | 140 ++++++++------- .../hero/partner-dashboard-mock.tsx | 161 ++++++++++++++++++ .../src/components/theme-preview/index.ts | 2 +- .../theme-preview/theme-preview.tsx | 49 +++--- apps/website/src/index.css | 1 + apps/website/src/main.tsx | 1 + .../src/generate/generateSemanticColors.ts | 2 +- packages/hextimator/src/generate/utils.ts | 23 +-- 11 files changed, 289 insertions(+), 104 deletions(-) create mode 100644 apps/website/src/components/hero/partner-dashboard-mock.tsx diff --git a/apps/website/src/App.tsx b/apps/website/src/App.tsx index a1dcf978..01a60a1b 100644 --- a/apps/website/src/App.tsx +++ b/apps/website/src/App.tsx @@ -17,7 +17,6 @@ import { import { Button } from './components/button'; 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 = () => ( @@ -67,12 +66,9 @@ function App() {
-
- -
( return (
diff --git a/apps/website/src/components/hero/color-picker/color-picker.tsx b/apps/website/src/components/hero/color-picker/color-picker.tsx index 14c9ae05..3a05e4de 100644 --- a/apps/website/src/components/hero/color-picker/color-picker.tsx +++ b/apps/website/src/components/hero/color-picker/color-picker.tsx @@ -135,7 +135,9 @@ export function ColorPicker({ e.preventDefault()} onFocusOutside={(e) => e.preventDefault()} diff --git a/apps/website/src/components/hero/hero.tsx b/apps/website/src/components/hero/hero.tsx index d923636a..0710da57 100644 --- a/apps/website/src/components/hero/hero.tsx +++ b/apps/website/src/components/hero/hero.tsx @@ -6,6 +6,7 @@ import { Button } from '../button'; import { registerColorCyclerStop } from './color-cycler-signal'; import { ColorInput } from './color-input'; import { ColorPicker } from './color-picker'; +import { PartnerDashboardMock } from './partner-dashboard-mock'; import { useColorCycler } from './use-color-cycler'; function tryApplyColor(value: string, setColor: (c: string) => void) { @@ -57,8 +58,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,66 +86,91 @@ 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 +
+
+ + pick any hex color + + +
+ + + +
+ 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 00000000..cb3f57e4 --- /dev/null +++ b/apps/website/src/components/hero/partner-dashboard-mock.tsx @@ -0,0 +1,161 @@ +import type { CSSProperties } from 'react'; +import { useEffect, 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' }, + { id: 'b8', 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, + })); +} + +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('')); + + useEffect(() => { + setChartBars(barsWithRandomHeights(accentColor)); + }, [accentColor]); + + return ( +
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+ + New + +
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ {chartBars.map((bar) => ( +
+ ))} +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+ ); +} diff --git a/apps/website/src/components/theme-preview/index.ts b/apps/website/src/components/theme-preview/index.ts index b57a2a5c..4f34df87 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 e2f708c4..4389fcd6 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({ )} - + diff --git a/apps/website/src/components/hero/hero.tsx b/apps/website/src/components/hero/hero.tsx index 0710da57..732d5577 100644 --- a/apps/website/src/components/hero/hero.tsx +++ b/apps/website/src/components/hero/hero.tsx @@ -93,13 +93,13 @@ export function Hero() { return (
-
-
+
+

Hextimator: one color in, branded theme out

-
-
+
+
One
@@ -141,17 +141,17 @@ export function Hero() {
in.
-
+
Whole theme out.
-

+

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

-
+
@@ -167,7 +167,7 @@ export function Hero() {
-
+
diff --git a/apps/website/src/components/hero/partner-dashboard-mock.tsx b/apps/website/src/components/hero/partner-dashboard-mock.tsx index 28f64aa6..30984f18 100644 --- a/apps/website/src/components/hero/partner-dashboard-mock.tsx +++ b/apps/website/src/components/hero/partner-dashboard-mock.tsx @@ -69,8 +69,8 @@ export function PartnerDashboardMock({ const [chartBars, setChartBars] = useState(() => barsWithRandomHeights('')); - const mobileChartIndexSet = useMemo( - () => new Set(mobileMainChartIndices(chartBars)), + const mobileBarIndices = useMemo( + () => mobileMainChartIndices(chartBars), [chartBars], ); @@ -87,29 +87,29 @@ export function PartnerDashboardMock({ } as CSSProperties } className={cn( - 'mx-auto flex w-full min-w-0 max-w-md flex-col select-none overflow-hidden rounded-2xl border border-surface-weak bg-surface-strong shadow-lg shadow-surface-weak/30 max-lg:rotate-0 lg:mx-0 lg:max-w-none lg:origin-top-right lg:scale-[1.04] lg:rounded-xl lg:rotate-(--card-rotation)', + 'mx-auto flex w-full min-w-0 max-w-md flex-col select-none overflow-hidden rounded-2xl border border-surface-weak bg-surface-strong shadow-lg shadow-surface-weak max-lg:rotate-0 lg:mx-0 lg:max-w-[min(100%,56rem)] lg:origin-top-left lg:scale-[1.04] lg:rounded-xl lg:rotate-(--card-rotation)', )} > -
-
+
+
-
+
-
+
-
+
-
-
+
+
@@ -119,29 +119,45 @@ export function PartnerDashboardMock({
-
- {chartBars.map((bar, i) => ( -
- ))} +
+
+ {(['left', 'mid', 'right'] as const).map((slotKey, i) => { + const barIndex = mobileBarIndices[i]; + const bar = chartBars[barIndex]; + return ( +
+ ); + })} +
+
+ {chartBars.map((bar) => ( +
+ ))} +
-
+
-
-
-
+
+
+
@@ -150,10 +166,10 @@ export function PartnerDashboardMock({
From a17212a7fbade5ae0b7ccb07ed28529a461add98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=20Grgi=C4=87?= Date: Thu, 14 May 2026 21:12:12 +0200 Subject: [PATCH 4/4] polish --- apps/website/src/App.tsx | 17 ++++++++- .../hero/color-picker/color-picker.tsx | 33 +++-------------- apps/website/src/components/hero/hero.tsx | 37 ++++++------------- .../hero/partner-dashboard-mock.tsx | 2 +- 4 files changed, 34 insertions(+), 55 deletions(-) diff --git a/apps/website/src/App.tsx b/apps/website/src/App.tsx index 01a60a1b..5f39b2c1 100644 --- a/apps/website/src/App.tsx +++ b/apps/website/src/App.tsx @@ -15,6 +15,7 @@ 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 { themeColorToPlaygroundPathHex } from './utils/playground-url-hex'; @@ -25,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( @@ -64,7 +79,7 @@ function App() {
- +
(null); const [isDragging, setIsDragging] = useState(false); - const [isLgUp, setIsLgUp] = useState(() => { - if (typeof window === 'undefined') return true; - return window.matchMedia(`(min-width: ${LG_PX}px)`).matches; - }); - - useEffect(() => { - const mq = window.matchMedia(`(min-width: ${LG_PX}px)`); - const sync = () => setIsLgUp(mq.matches); - sync(); - mq.addEventListener('change', sync); - return () => mq.removeEventListener('change', sync); - }, []); - const canvasCallbackRef = useCallback((canvas: HTMLCanvasElement | null) => { canvasRef.current = canvas; if (!canvas) return; @@ -157,11 +136,11 @@ export function ColorPicker({ e.preventDefault()} onFocusOutside={(e) => e.preventDefault()} onInteractOutside={(e) => { @@ -203,7 +182,7 @@ export function ColorPicker({ Continue with random colors )} - + diff --git a/apps/website/src/components/hero/hero.tsx b/apps/website/src/components/hero/hero.tsx index 732d5577..4bc10e59 100644 --- a/apps/website/src/components/hero/hero.tsx +++ b/apps/website/src/components/hero/hero.tsx @@ -6,7 +6,6 @@ import { Button } from '../button'; import { registerColorCyclerStop } from './color-cycler-signal'; import { ColorInput } from './color-input'; import { ColorPicker } from './color-picker'; -import { PartnerDashboardMock } from './partner-dashboard-mock'; import { useColorCycler } from './use-color-cycler'; function tryApplyColor(value: string, setColor: (c: string) => void) { @@ -21,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); @@ -92,16 +91,16 @@ export function Hero() { } satisfies React.CSSProperties; return ( -
-
-
+
+
+

Hextimator: one color in, branded theme out

-
-
+
+
@@ -112,16 +111,6 @@ export function Hero() {
One
-
- - pick any hex color - - -
in.
-
+
Whole theme out.
-

+

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

-
+
@@ -166,10 +155,6 @@ export function Hero() {
- -
- -
); diff --git a/apps/website/src/components/hero/partner-dashboard-mock.tsx b/apps/website/src/components/hero/partner-dashboard-mock.tsx index 30984f18..7ede4313 100644 --- a/apps/website/src/components/hero/partner-dashboard-mock.tsx +++ b/apps/website/src/components/hero/partner-dashboard-mock.tsx @@ -87,7 +87,7 @@ export function PartnerDashboardMock({ } as CSSProperties } className={cn( - 'mx-auto flex w-full min-w-0 max-w-md flex-col select-none overflow-hidden rounded-2xl border border-surface-weak bg-surface-strong shadow-lg shadow-surface-weak max-lg:rotate-0 lg:mx-0 lg:max-w-[min(100%,56rem)] lg:origin-top-left lg:scale-[1.04] lg:rounded-xl lg:rotate-(--card-rotation)', + 'mx-auto flex w-full min-w-0 max-w-md flex-col select-none overflow-hidden rounded-2xl border border-surface-weak bg-surface-strong shadow-lg shadow-surface-weak max-lg:rotate-0 lg:mx-auto lg:max-w-xl lg:origin-top lg:scale-[1.04] lg:rounded-xl lg:rotate-(--card-rotation)', )} >