-
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({