Skip to content
Merged
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
3 changes: 1 addition & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ Transition tokens live in `core/tokens.css`:

`@property` color interpolation is demonstrated by `.sf-color-pulse` which animates `--sf-color-primary-light` lightness via `sf-color-pulse` keyframes — proving that registered custom properties interpolate smoothly in oklch.

**slashed.accessibility** — `:focus-visible`, `.sr-only`, `.skip-link`, reduced-motion resets, plus the a11y patterns `.sf-focus-parent` (v0.3.0) and `.sf-clickable-parent` (added in v0.3.0). High in the stack to override motion without relying solely on `!important`. Selective `!important` used only where override is a genuine accessibility barrier (focus ring, reduced motion, sr-only). `.sr-only` uses `overflow: clip` (modern consensus — avoids creating a new scroll container unlike the legacy `overflow: hidden`). `.visually-hidden` is shipped as a synonym of `.sr-only` for teams that prefer the WHATWG naming convention.
**slashed.accessibility** — `:focus-visible`, `.sr-only`, `.skip-link`, reduced-motion resets, plus the a11y patterns `.sf-focus-parent` (v0.3.0) and `.sf-clickable-parent` (added in v0.3.0). High in the stack to override motion without relying solely on `!important`. Selective `!important` used only where override is a genuine accessibility barrier (focus ring, reduced motion, sr-only). `.sr-only` uses `overflow: clip` (modern consensus — avoids creating a new scroll container unlike the legacy `overflow: hidden`).

**slashed.print** — `@media print` only. Contains `@page` rule consuming `--sf-print-*` tokens. Authored colour is preserved by default; consumers opt into ink-on-paper via `.print-no-color` or force colour via `.print-color-exact`. `!important` is reserved for selectors whose semantics require defeating consumer CSS: the hide-list (`nav, aside, button, input, select, textarea, dialog, [popover], .no-print`), `details > summary`, and the two opt-in colour classes.

Expand Down Expand Up @@ -282,7 +282,6 @@ without reducing collision risk in practice.
|---|---|---|
| `.sr-only` | accessibility | Industry-standard screen-reader name |
| `.sr-only-focusable` | accessibility | Companion to `.sr-only` |
| `.visually-hidden` | accessibility | WHATWG synonym of `.sr-only` |
| `.skip-link` | accessibility | Common a11y pattern name |
| `.no-motion` | accessibility | Reads as a behaviour toggle |
| `.no-print` | print | Reads as a behaviour toggle |
Expand Down
7 changes: 6 additions & 1 deletion docs/demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@
margin-top: 0;
}

#main > .sf-section {
padding-inline: var(--sf-space-l);
border-top: 1px solid var(--sf-color-border);
}

.demo-section h2 {
font-size: var(--sf-text-xl);
color: var(--sf-color-primary);
Expand Down Expand Up @@ -1937,7 +1942,7 @@ <h3>Focus Ring (accessibility.css)</h3>
</details>
</div>

<h3>.sr-only / .visually-hidden</h3>
<h3>.sr-only</h3>
<p style="font-size: var(--sf-text-s)">Hidden from sighted users but available to screen readers:<span class="sr-only"> [THIS TEXT IS SCREEN-READER ONLY — not visible]</span> <em>(check with DevTools or a screen reader)</em></p>

<h3>Disabled States</h3>
Expand Down
18 changes: 3 additions & 15 deletions integrations/bricks/admin-app/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,9 @@
import { ui } from './lib/stores.svelte.js';
import TabNav from './components/TabNav.svelte';
import ColorTab from './components/ColorTab.svelte';
import ContrastTab from './components/ContrastTab.svelte';
import TypographyTab from './components/TypographyTab.svelte';
import SpacingTab from './components/SpacingTab.svelte';
import RadiusTab from './components/RadiusTab.svelte';
import ShadowsTab from './components/ShadowsTab.svelte';
import MotionTab from './components/MotionTab.svelte';
import ZindexTab from './components/ZindexTab.svelte';
import MiscTab from './components/MiscTab.svelte';
import VariablesTab from './components/VariablesTab.svelte';
import ClassesTab from './components/ClassesTab.svelte';
import BundleTab from './components/BundleTab.svelte';
Expand Down Expand Up @@ -77,20 +73,12 @@
<section class="slashed-svelte-admin__body">
{#if ui.activeTab === 'colors'}
<ColorTab />
{:else if ui.activeTab === 'contrast'}
<ContrastTab />
{:else if ui.activeTab === 'typography'}
<TypographyTab />
{:else if ui.activeTab === 'spacing'}
<SpacingTab />
{:else if ui.activeTab === 'radius'}
<RadiusTab />
{:else if ui.activeTab === 'shadows'}
<ShadowsTab />
{:else if ui.activeTab === 'motion'}
<MotionTab />
{:else if ui.activeTab === 'zindex'}
<ZindexTab />
{:else if ui.activeTab === 'misc'}
<MiscTab />
{:else if ui.activeTab === 'variables'}
<VariablesTab />
{:else if ui.activeTab === 'classes'}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<script>
let { children } = $props();
let open = $state(false);
const bodyId = `adv-body-${Math.random().toString(36).slice(2, 9)}`;
</script>

<div class="advanced-section">
<button
type="button"
class="advanced-section__toggle"
aria-expanded={open}
aria-controls={bodyId}
onclick={() => (open = !open)}
>
Advanced settings
<span class="advanced-section__arrow" class:open>{open ? '▲' : '▼'}</span>
</button>
{#if open}
<div id={bodyId} class="advanced-section__body">
{@render children()}
</div>
{/if}
</div>

<style>
.advanced-section { margin-top: 24px; }
.advanced-section__toggle {
background: none;
border: none;
color: #2271b1;
font-size: 13px;
cursor: pointer;
padding: 6px 0;
display: flex;
align-items: center;
gap: 6px;
font-weight: 500;
}
.advanced-section__toggle:hover { text-decoration: underline; }
.advanced-section__body { margin-top: 12px; }
</style>
96 changes: 51 additions & 45 deletions integrations/bricks/admin-app/src/components/ColorTab.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@
*/
import { meta } from '../lib/stores.svelte.js';
import ColorRow from './ColorRow.svelte';
import AdvancedSection from './AdvancedSection.svelte';

const colors = meta.defaults?.colors ?? {};

/** Capitalize first letter for display labels (matches the legacy ucfirst()). */
const cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);

/** Brand color keys — only the 6 core brand colors for the basic section. */
const brandEntries = Object.entries(colors.brand ?? {});
</script>

<section>
Expand All @@ -26,7 +30,7 @@
</p>

<div class="rows">
{#each Object.entries(colors.brand ?? {}) as [name, oklch] (name)}
{#each brandEntries as [name, oklch] (name)}
<ColorRow
storeKey={`brand_${name}`}
label={cap(name)}
Expand All @@ -37,54 +41,56 @@
{/each}
</div>

<h2 class="dark-heading">Brand Colors — Dark Mode</h2>
<p class="hint">
Optional overrides for dark mode. Leave empty to let the framework auto-derive
dark variants from the light source colors. Set explicit values for full
control over dark-mode appearance.
</p>
<AdvancedSection>
<h2 class="status-heading">Status Colors — Light Mode</h2>
<div class="rows">
{#each Object.entries(colors.status ?? {}) as [name, oklch] (name)}
<ColorRow
storeKey={`status_${name}`}
label={cap(name)}
hexHint={colors.status_hex_hints?.[name] ?? ''}
rawHint={oklch}
cssVar={`--sf-color-${name}-light`}
/>
{/each}
</div>

<div class="rows rows--dark">
{#each Object.entries(colors.brand_dark ?? colors.brand ?? {}) as [name] (name)}
<ColorRow
storeKey={`brand_dark_${name}`}
label={cap(name)}
hexHint={colors.brand_dark_hex_hints?.[name] ?? ''}
rawHint={colors.brand_dark?.[name] ?? ''}
cssVar={`--sf-color-${name}-dark`}
/>
{/each}
</div>
<h2 class="dark-heading">Brand Colors — Dark Mode</h2>
<p class="hint">
Optional overrides for dark mode. Leave empty to let the framework auto-derive
dark variants from the light source colors. Set explicit values for full
control over dark-mode appearance.
</p>

<h2 class="status-heading">Status Colors — Light Mode</h2>
<div class="rows">
{#each Object.entries(colors.status ?? {}) as [name, oklch] (name)}
<ColorRow
storeKey={`status_${name}`}
label={cap(name)}
hexHint={colors.status_hex_hints?.[name] ?? ''}
rawHint={oklch}
cssVar={`--sf-color-${name}-light`}
/>
{/each}
</div>
<div class="rows rows--dark">
{#each Object.entries(colors.brand_dark ?? colors.brand ?? {}) as [name] (name)}
<ColorRow
storeKey={`brand_dark_${name}`}
label={cap(name)}
hexHint={colors.brand_dark_hex_hints?.[name] ?? ''}
rawHint={colors.brand_dark?.[name] ?? ''}
cssVar={`--sf-color-${name}-dark`}
/>
{/each}
</div>

<h2 class="dark-heading">Status Colors — Dark Mode</h2>
<p class="hint">
Optional overrides for dark mode. Leave empty for auto-derivation.
</p>
<h2 class="dark-heading">Status Colors — Dark Mode</h2>
<p class="hint">
Optional overrides for dark mode. Leave empty for auto-derivation.
</p>

<div class="rows rows--dark">
{#each Object.entries(colors.status_dark ?? colors.status ?? {}) as [name] (name)}
<ColorRow
storeKey={`status_dark_${name}`}
label={cap(name)}
hexHint={colors.status_dark_hex_hints?.[name] ?? ''}
rawHint={colors.status_dark?.[name] ?? ''}
cssVar={`--sf-color-${name}-dark`}
/>
{/each}
</div>
<div class="rows rows--dark">
{#each Object.entries(colors.status_dark ?? colors.status ?? {}) as [name] (name)}
<ColorRow
storeKey={`status_dark_${name}`}
label={cap(name)}
hexHint={colors.status_dark_hex_hints?.[name] ?? ''}
rawHint={colors.status_dark?.[name] ?? ''}
cssVar={`--sf-color-${name}-dark`}
/>
{/each}
</div>
</AdvancedSection>
</section>

<style>
Expand Down
71 changes: 66 additions & 5 deletions integrations/bricks/admin-app/src/components/LivePreview.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,16 @@
* render meaningfully inside this small box, and adding them would
* just inflate the preview into a second admin form.
*/
import { tokens } from '../lib/stores.svelte.js';
import { tokens, meta } from '../lib/stores.svelte.js';

/** Brand color names rendered as swatches; mirrors the legacy preview. */
const brand = ['primary', 'secondary', 'tertiary', 'action', 'neutral', 'base'];
/** Status colors rendered alongside brand, same as legacy. */
const statuses = ['success', 'warning', 'error', 'info', 'danger'];

/** Shorthand accessors for default color hints from meta. */
const defaultColors = meta.defaults?.colors ?? {};

/**
* Build inline CSS custom properties for the preview container.
*
Expand All @@ -36,22 +39,26 @@
* Custom properties cascade to all descendants, so var(--sf-color-…)
* and var(--sf-font-…) on swatches/text resolve through the
* container's inline style.
*
* Falls back to meta.defaults.colors.*_hex_hints when no override is
* stored, so swatches show real colors even before the user touches
* anything (fixes the grey-swatch bug, issue #145).
*/
const inlineStyle = $derived.by(() => {
const pairs = [];
const colors = tokens.colors ?? {};
const typography = tokens.typography ?? {};

for (const name of brand) {
const v = colors[`brand_${name}`];
const v = colors[`brand_${name}`] ?? defaultColors.brand_hex_hints?.[name];
if (v) pairs.push(`--sf-color-${name}-light:${v}`);
const vd = colors[`brand_dark_${name}`];
const vd = colors[`brand_dark_${name}`] ?? defaultColors.brand_dark_hex_hints?.[name];
if (vd) pairs.push(`--sf-color-${name}-dark:${vd}`);
}
for (const name of statuses) {
const v = colors[`status_${name}`];
const v = colors[`status_${name}`] ?? defaultColors.status_hex_hints?.[name];
if (v) pairs.push(`--sf-color-${name}-light:${v}`);
const vd = colors[`status_dark_${name}`];
const vd = colors[`status_dark_${name}`] ?? defaultColors.status_dark_hex_hints?.[name];
if (vd) pairs.push(`--sf-color-${name}-dark:${vd}`);
}
if (typography.font_body) pairs.push(`--sf-font-body:${typography.font_body}`);
Expand Down Expand Up @@ -139,6 +146,24 @@
border: none;
cursor: default;
}
.slashed-preview__dark-section {
margin-top: 14px;
padding-top: 12px;
border-top: 1px solid #e9eaeb;
}
.slashed-preview__dark-heading {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: #50575e;
margin: 0 0 8px;
}
.slashed-preview__dark-bg {
background: #1d2327;
border-radius: 6px;
padding: 10px 12px;
}
.slashed-preview__caption {
margin-top: 16px;
color: #50575e;
Expand Down Expand Up @@ -180,6 +205,15 @@
{name}
</div>
{/each}
{#each statuses as name (name)}
<div
class="slashed-preview__swatch"
style:background={`var(--sf-color-${name}-light, #ddd)`}
title={name}
>
{name}
</div>
{/each}
</div>
<div class="slashed-preview__buttons">
<span
Expand All @@ -191,6 +225,33 @@
style:background={'var(--sf-color-action-light, #0891b2)'}
>Action</span>
</div>

<div class="slashed-preview__dark-section">
<p class="slashed-preview__dark-heading">Dark mode</p>
<div class="slashed-preview__dark-bg">
<div class="slashed-preview__swatches">
{#each brand as name (name)}
<div
class="slashed-preview__swatch"
style:background={`var(--sf-color-${name}-dark, #333)`}
title={`${name} dark`}
>
{name}
</div>
{/each}
{#each statuses as name (name)}
<div
class="slashed-preview__swatch"
style:background={`var(--sf-color-${name}-dark, #333)`}
title={`${name} dark`}
>
{name}
</div>
{/each}
</div>
</div>
</div>

<div class="slashed-preview__caption">
Generated CSS:
<code>{css || '/* (defaults — no overrides set) */'}</code>
Expand Down
Loading