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
105 changes: 101 additions & 4 deletions apps/docs/src/components/docs/Demo.astro
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,51 @@ interface Props {
showCode?: boolean;
/** Language for syntax highlighting (default: html) */
lang?: 'html' | 'css' | 'js';
/**
* Force a specific theme for this demo ("dark" | "light" | "split")
* - "dark": Demo preview uses dark theme context
* - "light": Demo preview uses light theme context
* - "split": Shows both light and dark side-by-side
* - undefined: Follows system/page theme
*/
theme?: 'dark' | 'light' | 'split';
}

const { code, title, showCode = true, lang = 'html' } = Astro.props;
const { code, title, showCode = true, lang = 'html', theme } = Astro.props;

// Clean up the code for display (trim whitespace)
const cleanCode = code.trim();

// Determine container classes
const previewClass = theme === 'split' ? 'demo-preview demo-preview--split' : 'demo-preview';
---

<div class="demo-container">
{title && <p class="demo-title">{title}</p>}
<div class="demo-preview">
<Fragment set:html={cleanCode} />
</div>
{theme === 'split' ? (
<div class={previewClass}>
<div class="demo-preview-pane" s-theme="light">
<div class="demo-pane-header">Light</div>
<div class="demo-pane-content">
<Fragment set:html={cleanCode} />
</div>
</div>
<div class="demo-preview-pane" s-theme="dark">
<div class="demo-pane-header">Dark</div>
<div class="demo-pane-content">
<Fragment set:html={cleanCode} />
</div>
</div>
</div>
) : theme ? (
<div class={previewClass} s-theme={theme}>
<Fragment set:html={cleanCode} />
</div>
) : (
<div class={previewClass}>
<Fragment set:html={cleanCode} />
</div>
)}
{showCode && (
<div class="demo-code">
<Code code={cleanCode} lang={lang} theme="css-variables" />
Expand Down Expand Up @@ -52,6 +84,71 @@ const cleanCode = code.trim();
background: var(--s-surface-default);
}

/* Theme island support - forced dark/light contexts */
.demo-preview[s-theme='dark'] {
background: oklch(12% 0.025 220);
color: oklch(95% 0.01 220);
}

.demo-preview[s-theme='light'] {
background: oklch(98% 0.005 220);
color: oklch(8% 0.02 220);
}

/* Split view - side-by-side light and dark */
.demo-preview--split {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0;
gap: 1px;
background: var(--s-border-default);
}

.demo-preview-pane {
display: flex;
flex-direction: column;
}

.demo-preview-pane[s-theme='light'] {
background: oklch(98% 0.005 220);
color: oklch(8% 0.02 220);
}

.demo-preview-pane[s-theme='dark'] {
background: oklch(12% 0.025 220);
color: oklch(95% 0.01 220);
}

.demo-pane-header {
padding: 0.375rem 0.75rem;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
}

/* Ensure accessible contrast for labels */
.demo-preview-pane[s-theme='light'] .demo-pane-header {
color: oklch(45% 0.02 220);
border-bottom: 1px solid oklch(85% 0.01 220);
}

.demo-preview-pane[s-theme='dark'] .demo-pane-header {
color: oklch(75% 0.02 220);
border-bottom: 1px solid oklch(25% 0.02 220);
}

.demo-pane-content {
padding: var(--s-space-6);
}

/* Responsive: stack on mobile */
@media (max-width: 640px) {
.demo-preview--split {
grid-template-columns: 1fr;
}
}

.demo-code {
border-top: 1px solid var(--s-border-default);
background: var(--s-surface-sunken);
Expand Down
68 changes: 66 additions & 2 deletions apps/docs/src/content/docs/utilities/functions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -251,12 +251,76 @@ Respects reduced motion preferences:

### s-dark-aware

Adapts background to color scheme:
A sophisticated dark-awareness system that goes beyond simple color swaps. It provides:

1. **Container-Relative Theming** - Uses CSS style queries to detect parent theme context, not just system preference
2. **OKLCH Perceptual Contrast** - Auto-computes readable text color based on background lightness
3. **Visual Adaptations** - Subtle border glow prevents bleeding, font weight adjusts for halation

<Demo
title="Dark-Aware in Both Themes"
theme="split"
code={`<div s-dark-aware style="padding: 1rem;">
<h4 style="margin-bottom: 0.5rem;">Self-Healing Card</h4>
<p>Text contrast, border glow, and font weight all adapt automatically.</p>
</div>`}
/>

#### Theme Containers

Create "dark islands" on light pages (or vice versa) with `s-theme`:

```html
<!-- Explicit dark container on a light page -->
<aside s-theme="dark">
<div s-dark-aware>Inherits dark context from parent</div>
</aside>

<!-- Follows system preference -->
<div s-dark-aware>Falls back to OS preference</div>
```

<Demo
title="Theme Islands"
code={`<div style="display: flex; gap: 1rem;">
<div s-theme="light" style="flex: 1; padding: 1rem; border: 1px solid oklch(0.8 0 0);">
<div s-dark-aware style="padding: 1rem;">
<strong>Light Island</strong>
<p style="margin-top: 0.25rem;">Forced light context</p>
</div>
</div>
<div s-theme="dark" style="flex: 1; padding: 1rem; border: 1px solid oklch(0.3 0 0);">
<div s-dark-aware style="padding: 1rem;">
<strong>Dark Island</strong>
<p style="margin-top: 0.25rem;">Forced dark context</p>
</div>
</div>
</div>`}
/>

#### Variants

```html
<div s-dark-aware>Background changes in dark mode</div>
<!-- Default: background, text, glow, weight adjustment -->
<div s-dark-aware>Full adaptation</div>

<!-- Subtle: text and weight only, no background -->
<div s-dark-aware="subtle">For inline elements</div>

<!-- Strong: more pronounced glow for hero sections -->
<div s-dark-aware="strong">Maximum visual separation</div>
```

#### Technical Details

| Feature | Light Mode | Dark Mode |
|---------|------------|-----------|
| Text Color | OKLCH L=0.15 | OKLCH L=0.95 |
| Border Glow | None | 0.5px white @ 8% |
| Font Weight | Base | Base - 50 |

The font weight reduction counters **halation** - the optical illusion where light text on dark backgrounds appears "fatter" due to light bleed in human vision.

## Creating Custom Functions

Define your own reusable CSS functions:
Expand Down
152 changes: 143 additions & 9 deletions packages/core/src/utils/functions.css
Original file line number Diff line number Diff line change
Expand Up @@ -208,18 +208,152 @@
}

/**
* Dark mode aware styling using if()
* Dark Mode Aware Styling
*
* A sophisticated dark-awareness system that goes beyond simple color swaps:
*
* 1. CONTAINER-RELATIVE THEMING
* Uses CSS style queries to detect parent theme context, not just system preference.
* A dark sidebar on a light-mode site will correctly theme its children.
*
* 2. PERCEPTUAL OKLCH CONTRAST
* Uses OKLCH lightness math to ensure text readability on any background.
* No manual light/dark text color definitions needed.
*
* 3. VISUAL ADAPTATION BEYOND COLOR
* - Border glow: Subtle inner highlight prevents dark elements from "bleeding"
* - Font weight: Reduces weight ~50 units to counter halation (light-on-dark bloat)
*
* Usage:
* <div s-theme="dark"> <!-- Explicit dark container -->
* <div s-dark-aware>Auto-adapts!</div> <!-- Inherits dark context -->
* </div>
*
* <div s-dark-aware> <!-- Falls back to system preference -->
* Content here
* </div>
*/

/* =============================================================================
THEME CONTAINER SETUP
Containers that establish a theme context for descendants
============================================================================= */

/**
* Theme containers - establish dark/light context for style queries
* These create a "theme boundary" that s-dark-aware elements can detect
*/
[s-theme] {
container-name: theme;
}

[s-theme="dark"] {
--_theme-is-dark: 1;
color-scheme: dark;
background-color: var(--s-surface-sunken, oklch(12% 0.02 220));
color: var(--s-text-primary, oklch(95% 0.01 220));
}

[s-theme="light"] {
--_theme-is-dark: 0;
color-scheme: light;
background-color: var(--s-surface-raised, oklch(98% 0.005 220));
color: var(--s-text-primary, oklch(15% 0.02 220));
}

/* =============================================================================
DARK-AWARE COMPONENT
Self-healing elements that adapt to their theme context
============================================================================= */

[s-dark-aware] {
/* Fallback for non-supporting browsers */
--_bg: var(--s-surface-raised);
background-color: var(--_bg);
/**
* LAYER 1: System preference baseline (universal fallback)
* Works in all browsers, detects OS-level dark mode
*/
--_da-is-dark: 0;
--_da-bg: var(--s-surface-raised);

@media (prefers-color-scheme: dark) {
--_da-is-dark: 1;
--_da-bg: var(--s-surface-sunken);
}

--_bg: if(
media(prefers-color-scheme: dark): var(--s-surface-sunken);
else: var(--s-surface-raised);
);
background-color: var(--_bg);
/**
* LAYER 2: Container-relative theming via style queries
* If inside an [s-theme] container, inherit its theme regardless of system preference
* This enables "dark islands" on light sites and vice versa
*/
@supports (container-type: inline-size) {
@container theme style(--_theme-is-dark: 1) {
--_da-is-dark: 1;
--_da-bg: var(--s-surface-sunken);
}

@container theme style(--_theme-is-dark: 0) {
--_da-is-dark: 0;
--_da-bg: var(--s-surface-raised);
}
}

/* Apply computed background */
background-color: var(--_da-bg);

/**
* LAYER 3: OKLCH Perceptual Contrast Text
* Automatically compute readable text color based on background lightness
* Uses L=0.55 threshold for WCAG 2.1 AA 4.5:1 contrast
*/
color: var(--s-text-primary); /* Fallback */

@supports (color: oklch(from red l c h)) {
color: oklch(
from var(--_da-bg)
/* If background L < 0.55: use light text (L=0.95), else dark text (L=0.15) */
clamp(0.15, calc((0.55 - l) * 1000 + 0.15), 0.95)
0 0
);
}

/**
* LAYER 4: Border Glow (Dark Mode Anti-Bleed)
* In dark mode, elements can "bleed" into dark backgrounds.
* A subtle inner highlight (0.5px at 8% white) creates visual separation.
* Only applies when --_da-is-dark is 1.
*/
--_da-glow-opacity: calc(var(--_da-is-dark) * 0.08);
box-shadow: inset 0 0 0 0.5px oklch(1 0 0 / var(--_da-glow-opacity));

/**
* LAYER 5: Font Weight Compensation (Halation Correction)
* Light text on dark backgrounds appears "fatter" due to light bleed (halation).
* We reduce font-weight by 50 units in dark mode to maintain visual consistency.
* Uses @property for type-safe font-weight interpolation.
*/
--_da-weight-base: var(--s-font-weight-normal, 400);
--_da-weight-adjustment: calc(var(--_da-is-dark) * 50);
font-weight: calc(var(--_da-weight-base) - var(--_da-weight-adjustment));
}

/**
* Variant: s-dark-aware="subtle"
* Less aggressive adaptations for inline/text elements
*/
[s-dark-aware="subtle"] {
/* No background change, just text and weight */
background-color: transparent;
box-shadow: none;
}

/**
* Variant: s-dark-aware="strong"
* More pronounced dark mode effects for hero sections
*/
[s-dark-aware="strong"] {
--_da-glow-opacity: calc(var(--_da-is-dark) * 0.12);
box-shadow:
inset 0 0 0 0.5px oklch(1 0 0 / var(--_da-glow-opacity)),
inset 0 1px 0 0 oklch(1 0 0 / calc(var(--_da-is-dark) * 0.05));
}

/* =============================================================================
Expand Down