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
10 changes: 5 additions & 5 deletions .claude/skills/layered-ui-rails/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ Populate layout regions with `content_for` (always above the render call):
<%# Inject into <head> (e.g. per-tenant theming) %>
<% content_for :l_ui_head do %>
<style nonce="<%= content_security_policy_nonce %>">
:root { --accent: 220 80% 55%; }
:root { --accent: oklch(0.58 0.19 255); }
</style>
<% end %>

Expand Down Expand Up @@ -150,17 +150,17 @@ All controllers use the `l-ui--` namespace and are auto-registered via importmap

## Theming

Override CSS custom properties after the engine import. Values are space-separated HSL channels (e.g. `220 80% 55%`, or `0 0% 100%` for white). Keywords and hex/rgb won't work - tokens are wrapped in `hsl()` when consumed. A converter such as https://colorpicker.dev/ can help translate hex/rgb values.
Override CSS custom properties after the engine import. Values are full CSS colors - `oklch()` is recommended for perceptually uniform mixing and consistent contrast, but `#hex`, `rgb()`, and keywords also work. A converter such as https://oklch.com/ can help translate from hex/rgb.

```css
@import "./layered_ui";

:root {
--accent: 220 80% 55%;
--accent-foreground: 0 0% 100%;
--accent: oklch(0.58 0.19 255);
--accent-foreground: oklch(1 0 0);
}
.dark {
--accent: 220 80% 65%;
--accent: oklch(0.72 0.14 255);
}
```

Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/layered-ui-rails/references/CSS.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ WCAG 2.2 AA table pattern:

## Theming tokens

All color values are space-separated HSL channels (e.g. `220 80% 55%`, or `0 0% 100%` for white). Override after importing the engine CSS. Keywords and hex/rgb won't work - tokens are wrapped in `hsl()` when consumed. A converter such as https://colorpicker.dev/ can help translate hex/rgb values.
All color values are full CSS colors - `oklch()` is recommended for perceptually uniform mixing and consistent contrast (e.g. `oklch(0.58 0.19 255)`), but `#hex`, `rgb()`, and keywords also work. Override after importing the engine CSS. A converter such as https://oklch.com/ can help translate from hex/rgb.

Tier 1 - Accent (quick branding):
```
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Guidance for AI agents working in this repository.

- **Entry:** `require "layered-ui-rails"` → `lib/layered/ui.rb` → `lib/layered/ui/engine.rb`
- **Engine:** importmap, assets, Pagy helpers when present; helpers: `AuthenticationHelper`, `NavigationHelper`, `PagyHelper`
- **CSS** `app/assets/tailwind/layered/ui/styles.css`: HSL tokens, `.dark` on `<html>`, `@theme` utilities (`bg-background`, etc.), BEM components (`.l-ui-button--primary`, etc.). Layout: 63px header, 240px sidebar, 320px panel. WCAG 2.2 AA.
- **CSS** `app/assets/tailwind/layered/ui/styles.css`: OKLCH tokens, `.dark` on `<html>`, `@theme` utilities (`bg-background`, etc.), BEM components (`.l-ui-button--primary`, etc.). Layout: 63px header, 240px sidebar, 320px panel. WCAG 2.2 AA.
- **CSS `@apply`:** Multi-line with grouping (layout → spacing → typography → colors → effects). Single utilities may stay on one line.
- **Generators:** `bin/rails generate layered:ui:install` (copy CSS, import CSS, import JS)
- **JS** `app/javascript/layered_ui/`: Stimulus controllers registered as `l-ui--theme`, `l-ui--navigation`, `l-ui--panel`, `l-ui--modal`, `l-ui--tabs`
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

All notable changes to this project will be documented in this file. This project follows [Semantic Versioning](https://semver.org/).

## [Unreleased]

### Changed

- **Breaking:** Theme tokens migrated from HSL channels (`220 80% 55%`) to full `oklch()` values for perceptually uniform mixing. `@theme` now references the vars directly instead of wrapping them in `hsl()`. Existing overrides files need to be updated: replace `hue sat% lightness%` with the equivalent `oklch(L C H)` value (or any valid CSS color - `#hex`, `rgb()`, and keywords all work now).

## [0.7.0] - 2026-04-23

### Added
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,14 +104,14 @@ All colors are CSS custom properties on `:root` using a two-tier system:
@import "./layered_ui";

:root {
--accent: 220 80% 55%;
--accent-foreground: 0 0% 100%;
--accent: oklch(0.58 0.19 255);
--accent-foreground: oklch(1 0 0);
}

.dark {
--accent: 220 80% 65%;
--accent-foreground: 0 0% 9%;
--button-primary-text: 0 0% 100%; /* white icons/text on colored buttons */
--accent: oklch(0.72 0.14 255);
--accent-foreground: oklch(0.2044 0 0);
--button-primary-text: oklch(1 0 0); /* white icons/text on colored buttons */
}
```

Expand All @@ -120,7 +120,7 @@ For dynamic theming (e.g. per-tenant branding), use `content_for :l_ui_head` to
```erb
<% content_for :l_ui_head do %>
<style>
:root { --accent: <%= @tenant.accent_hsl %>; --accent-foreground: 0 0% 100%; }
:root { --accent: <%= @tenant.accent_color %>; --accent-foreground: oklch(1 0 0); }
</style>
<% end %>
```
Expand All @@ -132,7 +132,7 @@ For dynamic theming (e.g. per-tenant branding), use `content_for :l_ui_head` to
> ```erb
> <% content_for :l_ui_head do %>
> <style nonce="<%= content_security_policy_nonce %>">
> :root { --accent: <%= @tenant.accent_hsl %>; --accent-foreground: 0 0% 100%; }
> :root { --accent: <%= @tenant.accent_color %>; --accent-foreground: oklch(1 0 0); }
> </style>
> <% end %>
> ```
Expand Down
132 changes: 66 additions & 66 deletions app/assets/tailwind/layered/ui/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
@variant dark (&:where(.dark, .dark *));

/*
* Color tokens (HSL channels, e.g. "220 80% 55%")
* Color tokens (full oklch() values, e.g. "oklch(0.7 0.15 240)")
*
* Tier 1 - Accent: --accent, --accent-foreground
* Tier 2 - Full palette: background, foreground, border, surface, etc.
Expand All @@ -48,59 +48,59 @@
@layer base {
:root {
/* Tier 1 - Accent */
--accent: 0 0% 9%;
--accent-foreground: 0 0% 100%;
--accent: oklch(0.2044 0 0);
--accent-foreground: oklch(1 0 0);
/* Tier 2 - Full palette */
--background: 0 0% 100%;
--foreground: 0 0% 13%;
--foreground-muted: 0 0% 29%;
--border: 0 0% 91%;
--border-control: 0 0% 55%;
--ring: 0 0% 13%;
--surface: 0 0% 96%;
--surface-highlighted: 0 0% 91%;
--background: oklch(1 0 0);
--foreground: oklch(0.2484 0 0);
--foreground-muted: oklch(0.4089 0 0);
--border: oklch(0.9312 0 0);
--border-control: oklch(0.6409 0 0);
--ring: oklch(0.2484 0 0);
--surface: oklch(0.9696 0 0);
--surface-highlighted: oklch(0.9312 0 0);
--button-primary-bg: var(--accent);
--button-primary-text: var(--accent-foreground);
--button-primary-icon: var(--button-primary-text);
--danger: 0 72% 38%;
--danger-light: 0 100% 97%;
--danger-text: 0 72% 35%;
--success-bg: 142 76% 65%;
--success-text: 142 76% 13%;
--switch-track-checked: 142 70% 38%;
--warning-bg: 48 96% 65%;
--warning-text: 48 96% 15%;
--error-bg: 0 84% 75%;
--error-text: 0 93% 12%;
--danger: oklch(0.47 0.1742 27.23);
--danger-light: oklch(0.9663 0.0166 17.44);
--danger-text: oklch(0.443 0.1634 27.14);
--success-bg: oklch(0.8395 0.1698 152.91);
--success-text: oklch(0.3088 0.0763 150.76);
--switch-track-checked: oklch(0.6334 0.1664 149.75);
--warning-bg: oklch(0.8908 0.1551 94.86);
--warning-text: oklch(0.3625 0.0732 93.12);
--error-bg: oklch(0.748 0.1306 20.64);
--error-text: oklch(0.2248 0.0874 28.11);
--header-height: 63px;
}

.dark {
/* Tier 1 - Accent */
--accent: 0 0% 100%;
--accent-foreground: 0 0% 9%;
--accent: oklch(1 0 0);
--accent-foreground: oklch(0.2044 0 0);
/* Tier 2 - Full palette */
--background: 0 0% 0%;
--foreground: 0 0% 89%;
--foreground-muted: 0 0% 71%;
--border: 0 0% 16%;
--border-control: 0 0% 40%;
--ring: 0 0% 89%;
--surface: 0 0% 8%;
--surface-highlighted: 0 0% 16%;
--background: oklch(0 0 0);
--foreground: oklch(0.9157 0 0);
--foreground-muted: oklch(0.7733 0 0);
--border: oklch(0.2801 0 0);
--border-control: oklch(0.5103 0 0);
--ring: oklch(0.9157 0 0);
--surface: oklch(0.193 0 0);
--surface-highlighted: oklch(0.2801 0 0);
--button-primary-bg: var(--accent);
--button-primary-text: var(--accent-foreground);
--button-primary-icon: var(--button-primary-text);
--danger: 0 85% 60%;
--danger-light: 0 93% 15%;
--danger-text: 0 85% 64%;
--success-bg: 142 76% 15%;
--success-text: 142 76% 80%;
--switch-track-checked: 142 70% 45%;
--warning-bg: 48 96% 15%;
--warning-text: 48 96% 80%;
--error-bg: 0 93% 12%;
--error-text: 0 84% 75%;
--danger: oklch(0.6362 0.2102 25.49);
--danger-light: oklch(0.2596 0.1021 28.32);
--danger-text: oklch(0.6607 0.1921 24.02);
--success-bg: oklch(0.3385 0.0852 150.45);
--success-text: oklch(0.8999 0.1022 155.94);
--switch-track-checked: oklch(0.7176 0.1902 149.6);
--warning-bg: oklch(0.3625 0.0732 93.12);
--warning-text: oklch(0.9336 0.1 95.79);
--error-bg: oklch(0.2248 0.0874 28.11);
--error-text: oklch(0.748 0.1306 20.64);
}

/* Typography */
Expand Down Expand Up @@ -148,7 +148,7 @@
.l-ui-body,
.l-ui-body * {
scrollbar-width: thin;
scrollbar-color: hsl(var(--foreground-muted) / 0.3) transparent;
scrollbar-color: color-mix(in oklch, var(--foreground-muted) 30%, transparent) transparent;
}
}

Expand All @@ -157,29 +157,29 @@
@theme {
--font-manrope: 'Manrope', ui-sans-serif, system-ui, sans-serif;
--font-inter: 'Inter', ui-sans-serif, system-ui, sans-serif;
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
--color-foreground-muted: hsl(var(--foreground-muted));
--color-border: hsl(var(--border));
--color-border-control: hsl(var(--border-control));
--color-surface: hsl(var(--surface));
--color-surface-highlighted: hsl(var(--surface-highlighted));
--color-button-primary-bg: hsl(var(--button-primary-bg));
--color-button-primary-text: hsl(var(--button-primary-text));
--color-button-primary-icon: hsl(var(--button-primary-icon));
--color-danger: hsl(var(--danger));
--color-danger-light: hsl(var(--danger-light));
--color-danger-text: hsl(var(--danger-text));
--color-success-bg: hsl(var(--success-bg));
--color-success-text: hsl(var(--success-text));
--color-switch-track-checked: hsl(var(--switch-track-checked));
--color-warning-bg: hsl(var(--warning-bg));
--color-warning-text: hsl(var(--warning-text));
--color-error-bg: hsl(var(--error-bg));
--color-error-text: hsl(var(--error-text));
--color-ring: hsl(var(--ring));
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-foreground-muted: var(--foreground-muted);
--color-border: var(--border);
--color-border-control: var(--border-control);
--color-surface: var(--surface);
--color-surface-highlighted: var(--surface-highlighted);
--color-button-primary-bg: var(--button-primary-bg);
--color-button-primary-text: var(--button-primary-text);
--color-button-primary-icon: var(--button-primary-icon);
--color-danger: var(--danger);
--color-danger-light: var(--danger-light);
--color-danger-text: var(--danger-text);
--color-success-bg: var(--success-bg);
--color-success-text: var(--success-text);
--color-switch-track-checked: var(--switch-track-checked);
--color-warning-bg: var(--warning-bg);
--color-warning-text: var(--warning-text);
--color-error-bg: var(--error-bg);
--color-error-text: var(--error-text);
--color-ring: var(--ring);
}

/* Accessibility */
Expand Down Expand Up @@ -1092,7 +1092,7 @@ pre.l-ui-surface {
w-4 h-4
pointer-events-none;
content: '';
background-color: hsl(var(--foreground));
background-color: var(--foreground);
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='black'%3E%3Cpath d='M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z'/%3E%3C/svg%3E");
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='black'%3E%3Cpath d='M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z'/%3E%3C/svg%3E");
-webkit-mask-size: contain;
Expand Down
85 changes: 43 additions & 42 deletions lib/generators/layered/ui/create_overrides_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@ def overrides_content
* This file is NOT overwritten by the install generator, so your
* changes are preserved when you upgrade layered-ui-rails.
*
* Values are space-separated HSL channels (e.g. `220 80% 55%`,
* or `0 0% 100%` for white). Keywords and hex/rgb won't work -
* tokens are wrapped in `hsl()` when consumed. A converter such
* as https://colorpicker.dev/ can help translate hex/rgb values.
* Values are full `oklch()` colors (e.g. `oklch(0.7 0.15 240)`).
* Any valid CSS color works too - `#hex`, `rgb(...)`, keywords -
* but `oklch()` is recommended for perceptually uniform mixing
* and consistent contrast. A converter such as
* https://oklch.com/ can help translate from hex/rgb.
*/

/* ----------------------------------------------------------------
Expand All @@ -46,13 +47,13 @@ def overrides_content
* ---------------------------------------------------------------- */

:root {
/* --accent: 0 0% 9%; */
/* --accent-foreground: 0 0% 100%; */
/* --accent: oklch(0.2044 0 0); */
/* --accent-foreground: oklch(1 0 0); */
}

.dark {
/* --accent: 0 0% 100%; */
/* --accent-foreground: 0 0% 9%; */
/* --accent: oklch(1 0 0); */
/* --accent-foreground: oklch(0.2044 0 0); */
}

/* ----------------------------------------------------------------
Expand All @@ -63,49 +64,49 @@ def overrides_content

/*
:root {
--background: 0 0% 100%;
--foreground: 0 0% 13%;
--foreground-muted: 0 0% 29%;
--border: 0 0% 91%;
--border-control: 0 0% 55%;
--ring: 0 0% 13%;
--surface: 0 0% 96%;
--surface-highlighted: 0 0% 91%;
--background: oklch(1 0 0);
--foreground: oklch(0.2484 0 0);
--foreground-muted: oklch(0.4089 0 0);
--border: oklch(0.9312 0 0);
--border-control: oklch(0.6409 0 0);
--ring: oklch(0.2484 0 0);
--surface: oklch(0.9696 0 0);
--surface-highlighted: oklch(0.9312 0 0);
--button-primary-bg: var(--accent);
--button-primary-text: var(--accent-foreground);
--button-primary-icon: var(--button-primary-text);
--danger: 0 72% 38%;
--danger-light: 0 100% 97%;
--danger-text: 0 72% 35%;
--success-bg: 142 76% 65%;
--success-text: 142 76% 13%;
--warning-bg: 48 96% 65%;
--warning-text: 48 96% 15%;
--error-bg: 0 84% 75%;
--error-text: 0 93% 12%;
--danger: oklch(0.47 0.1742 27.23);
--danger-light: oklch(0.9663 0.0166 17.44);
--danger-text: oklch(0.443 0.1634 27.14);
--success-bg: oklch(0.8395 0.1698 152.91);
--success-text: oklch(0.3088 0.0763 150.76);
--warning-bg: oklch(0.8908 0.1551 94.86);
--warning-text: oklch(0.3625 0.0732 93.12);
--error-bg: oklch(0.748 0.1306 20.64);
--error-text: oklch(0.2248 0.0874 28.11);
}

.dark {
--background: 0 0% 0%;
--foreground: 0 0% 89%;
--foreground-muted: 0 0% 71%;
--border: 0 0% 16%;
--border-control: 0 0% 40%;
--ring: 0 0% 89%;
--surface: 0 0% 8%;
--surface-highlighted: 0 0% 16%;
--background: oklch(0 0 0);
--foreground: oklch(0.9157 0 0);
--foreground-muted: oklch(0.7733 0 0);
--border: oklch(0.2801 0 0);
--border-control: oklch(0.5103 0 0);
--ring: oklch(0.9157 0 0);
--surface: oklch(0.193 0 0);
--surface-highlighted: oklch(0.2801 0 0);
--button-primary-bg: var(--accent);
--button-primary-text: var(--accent-foreground);
--button-primary-icon: var(--button-primary-text);
--danger: 0 85% 60%;
--danger-light: 0 93% 15%;
--danger-text: 0 85% 64%;
--success-bg: 142 76% 15%;
--success-text: 142 76% 80%;
--warning-bg: 48 96% 15%;
--warning-text: 48 96% 80%;
--error-bg: 0 93% 12%;
--error-text: 0 84% 75%;
--danger: oklch(0.6362 0.2102 25.49);
--danger-light: oklch(0.2596 0.1021 28.32);
--danger-text: oklch(0.6607 0.1921 24.02);
--success-bg: oklch(0.3385 0.0852 150.45);
--success-text: oklch(0.8999 0.1022 155.94);
--warning-bg: oklch(0.3625 0.0732 93.12);
--warning-text: oklch(0.9336 0.1 95.79);
--error-bg: oklch(0.2248 0.0874 28.11);
--error-text: oklch(0.748 0.1306 20.64);
}
*/

Expand Down
Loading
Loading