Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions configurator/src/components/DomainPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import QuickKnobs from './QuickKnobs.svelte';
import StylePresetRow from './StylePresetRow.svelte';
import ColorAssignments from './ColorAssignments.svelte';
import ShadeRamp from './ShadeRamp.svelte';
import DomainPreview from './DomainPreview.svelte';
import Icon from './Icon.svelte';
import { DOMAIN_PREVIEWS } from '../lib/domainPreviews.js';
Expand Down Expand Up @@ -113,6 +114,7 @@
let showStatus = $state(false);
// Semantic-roles preview leads the Colors panel — open by default.
let showColorRoles = $state(true);
let showShadeRamp = $state(false);

// Other token domains: an inline live preview that LEADS the panel (the
// generalisation of the Colors "Semantic roles" section). Present only for
Expand Down Expand Up @@ -333,6 +335,16 @@
{/each}
</div>
</details>

<!-- Shade ramp (superlight → superdark for each brand color) -->
<details class="cfg-card panel__card" bind:open={showShadeRamp}>
<summary class="panel__card-head panel__expand-summary">
<span class="panel__expand-chev" aria-hidden="true">›</span>
<span class="panel__card-title">Shade ramp</span>
<span class="panel__expand-count">7-step scale per brand color</span>
</summary>
<ShadeRamp />
</details>
{:else if basicGroups.length}
{#each basicGroups as group (group.title)}
<section class="cfg-card panel__card">
Expand Down
52 changes: 51 additions & 1 deletion configurator/src/components/DomainPreview.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,16 @@
{#if group.section}<p class="dp__section">{group.section}</p>{/if}

<!-- ── Type specimen ──────────────────────────────────────────── -->
{#if spec.kind === 'type' && group.section === 'Font stacks'}
{#if spec.kind === 'type' && group.section === 'Specimen'}
<div class="dp__specimen-box">
<p class="dp__specimen-heading">The quick brown fox jumps over the lazy dog.</p>
<p class="dp__specimen-body">Whereas disregard and contempt for human rights have resulted in barbarous acts which have outraged the conscience of mankind, and the advent of a world in which human beings shall enjoy freedom of speech and belief.</p>
<p class="dp__specimen-meta">
<span class="dp__specimen-tag">Heading: <code>--sf-font-heading</code></span>
<span class="dp__specimen-tag">Body: <code>--sf-font-body</code></span>
</p>
</div>
{:else if spec.kind === 'type' && group.section === 'Font stacks'}
<div class="dp__fonts">
{#each group.items as it (it.token)}
<div class="dp__font-row" style="font-family: var({it.token})">
Expand Down Expand Up @@ -290,6 +299,47 @@
.dp__font-label { font-size: 10px; color: var(--cfg-text-faint); text-transform: uppercase; letter-spacing: 0.06em; }
.dp__font-sample { font-size: 15px; }

/* Font pairing specimen */
.dp__specimen-box {
display: flex;
flex-direction: column;
gap: 10px;
padding: 14px;
border: 1px solid var(--sf-color-border, var(--cfg-border));
border-radius: var(--sf-radius-m, 8px);
background: var(--sf-color-bg, var(--cfg-bg));
}
.dp__specimen-heading {
margin: 0;
font-family: var(--sf-font-heading);
font-size: var(--sf-h2-size, 1.75rem);
font-weight: var(--sf-h2-font-weight, 700);
line-height: var(--sf-h2-line-height, 1.2);
color: var(--sf-color-heading, var(--sf-color-text, var(--cfg-text)));
}
.dp__specimen-body {
margin: 0;
font-family: var(--sf-font-body);
font-size: var(--sf-text-m, 1rem);
line-height: var(--sf-leading-normal, 1.6);
color: var(--sf-color-text--secondary, var(--cfg-text-muted));
}
.dp__specimen-meta {
margin: 4px 0 0;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.dp__specimen-tag {
font-size: 10px;
color: var(--cfg-text-faint);
}
.dp__specimen-tag code {
font-family: var(--cfg-mono);
font-size: 9.5px;
color: var(--cfg-accent, var(--cfg-text-faint));
}

/* Spacing + container bars */
.dp__bars { display: flex; flex-direction: column; gap: 6px; }
.dp__bar-row { display: flex; align-items: center; gap: 10px; }
Expand Down
199 changes: 199 additions & 0 deletions configurator/src/components/ShadeRamp.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
<script>
/**
* Brand-color shade ramp.
*
* Resolves and displays the full 7-step shade ramp for each brand color
* (superlight → superdark) using the framework's derived consumption tokens.
* Updates reactively whenever overrides or the preview theme changes.
*
* Brand families only: primary, secondary, tertiary, action, neutral, base.
* Status families (success/warning/error/info/danger) intentionally omit the
* shade ramp — they expose only the subtle/muted/strong triplet.
*/
import { overrides, ui } from '../lib/store.svelte.js';
import { measureBackground } from '../lib/probeHost.js';

const BRAND_KEYS = [
{ key: 'primary', label: 'Primary' },
{ key: 'secondary', label: 'Secondary' },
{ key: 'tertiary', label: 'Tertiary' },
{ key: 'action', label: 'Action' },
{ key: 'neutral', label: 'Neutral' },
{ key: 'base', label: 'Base' },
];

const SHADE_STEPS = [
{ suffix: '-superlight', label: 'superlight' },
{ suffix: '-xlight', label: 'xlight' },
{ suffix: '-lighter', label: 'lighter' },
{ suffix: '', label: 'base' },
{ suffix: '-darker', label: 'darker' },
{ suffix: '-xdark', label: 'xdark' },
{ suffix: '-superdark', label: 'superdark' },
];

const ALL_SHADE_TOKENS = BRAND_KEYS.flatMap(({ key }) =>
SHADE_STEPS.map(({ suffix }) => `--sf-color-${key}${suffix}`)
);

/** @type {Record<string, string>} token → resolved rgb() string */
let resolved = $state({});

$effect(() => {
for (const k in overrides) void overrides[k];
void ui.previewTheme;
queueMicrotask(() => {
const next = {};
for (const token of ALL_SHADE_TOKENS) {
const rgb = measureBackground(`var(${token})`);
if (rgb && rgb !== 'rgba(0, 0, 0, 0)') next[token] = rgb;
}
resolved = next;
});
});

/** Perceived lightness (0–255) — decides whether to use dark or light text on a swatch. */
function perceived(rgb) {
if (!rgb) return 128;
const m = /rgba?\((\d+),\s*(\d+),\s*(\d+)/.exec(rgb);
if (!m) return 128;
return 0.299 * +m[1] + 0.587 * +m[2] + 0.114 * +m[3];
}
</script>

<div class="sr">
<p class="sr__intro">
7-step ramp for each brand color, resolved against the <strong>{ui.previewTheme}</strong> theme.
</p>

{#each BRAND_KEYS as { key, label } (key)}
<div class="sr__row">
<span class="sr__name">{label}</span>
<div class="sr__swatches">
{#each SHADE_STEPS as { suffix, label: stepLabel } (`${key}${suffix}`)}
{@const token = `--sf-color-${key}${suffix}`}
{@const bg = resolved[token]}
{@const isLight = bg ? perceived(bg) > 128 : true}
<div class="sr__swatch-wrap">
<div
class="sr__swatch"
class:sr__swatch--empty={!bg}
style:background-color={bg ?? 'transparent'}
title="{token}: {bg ?? 'resolving…'}"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
>
{#if !bg}
<span class="sr__dots" aria-hidden="true">···</span>
{:else if suffix === ''}
<!-- Mark the base step -->
<span class="sr__base-dot" class:sr__base-dot--dark={!isLight} aria-hidden="true">●</span>
{/if}
</div>
<span class="sr__step-label">{stepLabel}</span>
</div>
{/each}
</div>
</div>
{/each}

<p class="sr__note">
Theme: <strong>{ui.previewTheme}</strong> · toggle in the preview pane to compare modes.
</p>
</div>

<style>
.sr {
display: flex;
flex-direction: column;
gap: 12px;
padding: 14px 16px;
}

.sr__intro {
margin: 0;
font-size: 11.5px;
color: var(--cfg-text-muted);
}

.sr__row {
display: flex;
flex-direction: column;
gap: 4px;
}

.sr__name {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.07em;
color: var(--cfg-text-faint);
}

.sr__swatches {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 3px;
}

.sr__swatch-wrap {
display: flex;
flex-direction: column;
align-items: center;
gap: 3px;
}

.sr__swatch {
width: 100%;
height: 36px;
border-radius: var(--cfg-radius-s, 4px);
border: 1px solid rgba(0, 0, 0, 0.07);
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s ease;
background-image:
conic-gradient(#444 25%, #2a2a2a 0 50%, #444 0 75%, #2a2a2a 0);
background-size: 8px 8px;
}
.sr__swatch:not(.sr__swatch--empty) {
background-image: none;
}

.sr__dots {
font-size: 10px;
color: var(--cfg-text-faint);
letter-spacing: 1px;
}

.sr__base-dot {
font-size: 8px;
color: rgba(0, 0, 0, 0.35);
}
.sr__base-dot--dark {
color: rgba(255, 255, 255, 0.5);
}

.sr__step-label {
font-size: 8.5px;
color: var(--cfg-text-faint);
text-align: center;
line-height: 1.2;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
}

.sr__note {
margin: 4px 0 0;
font-size: 11px;
color: var(--cfg-text-faint);
border-top: 1px solid var(--cfg-border);
padding-top: 10px;
}

@media (max-width: 600px) {
.sr { padding: 10px 12px; }
.sr__swatch { height: 28px; }
.sr__step-label { font-size: 7.5px; }
}
</style>
Loading
Loading