Current section
Files
Jump to
Current section
Files
priv/static/lantern_ui.css
/*
* LanternUI design tokens + default theme.
*
* Components read every value from `--lantern-*` custom properties, each chained
* to a host (Fluxon) token then a hard fallback, e.g.:
*
* color: var(--lantern-fg, var(--foreground, #09090b));
* background: var(--lantern-surface, var(--background-base, #fff));
* border-color: var(--lantern-border, var(--border, #e4e4e7));
* accent: var(--lantern-accent, var(--color-primary-600, oklch(0.637 0.192 38)));
*
* COLOR MODEL (shadcn-style):
* - `primary` is MONOCHROME (near-black in light, white in dark) — the color of
* buttons, checked controls, and default badges. Restrained, premium.
* - `accent` is the flicker coral/ember orange — focus rings, selection,
* highlights, links, live-status. The brand shows up sparingly, not on every
* button.
*
* WHEN TO IMPORT THIS FILE
* - Fluxon (or any host that defines the fallback tokens): DON'T import — the
* host's tokens flow in through the fallbacks and LanternUI matches it.
* - Standalone / embedded tools (lantern, livecode, s3) / the demo: import this
* for a complete light+dark theme with zero host CSS required.
* - Recolor by overriding any `--lantern-*` variable on `:root` or a subtree.
*
* THEMING SURFACES
* - Light is the `:root` default; `.dark` (or system dark, unless `.light`) flips colors.
* - Density: the default is shadcn-compact (32px controls / 13px text). Set
* `data-lantern-density="comfortable"` on an ancestor for a roomier scale.
*/
:root {
/* ── Color · foreground ── */
--lantern-fg: #09090b;
--lantern-fg-muted: #71717a;
--lantern-fg-subtle: #a1a1aa;
/* ── Color · surfaces ── */
--lantern-surface: #ffffff;
--lantern-surface-raised: #ffffff;
--lantern-surface-sunken: #f4f4f5;
--lantern-surface-hover: #f4f4f5;
/* ── Color · lines ── */
--lantern-border: #e4e4e7;
--lantern-border-strong: #d4d4d8;
/* ── Color · primary (monochrome — buttons, checked controls, default badges) ── */
--lantern-primary: #18181b;
--lantern-primary-fg: #fafafa;
/* ── Color · secondary (subtle filled controls) ── */
--lantern-secondary: #f4f4f5;
--lantern-secondary-fg: #18181b;
/* ── Color · accent + status ── */
/* Flicker brand accent — coral/ember orange (OKLCH warm hue ~38), shared
across the lantern ecosystem. Used sparingly: focus rings, selection,
links, highlights, live-status. In a flicker/Fluxon host the
component-level fallback to --color-primary-600 already yields this. */
--lantern-accent: oklch(0.637 0.192 38);
--lantern-accent-fg: #ffffff;
--lantern-accent-soft: color-mix(in oklab, var(--lantern-accent) 12%, transparent);
--lantern-danger: #dc2626;
--lantern-danger-fg: #ffffff;
--lantern-success: #16a34a;
--lantern-warning: #d97706;
/* ── Focus ring (coral, shadcn-style 3px soft ring via color-mix) ── */
--lantern-ring: var(--lantern-accent);
--lantern-ring-soft: color-mix(in oklab, var(--lantern-ring) 25%, transparent);
/* ── Radius ── */
--lantern-radius-sm: 0.375rem;
--lantern-radius-md: 0.5rem;
--lantern-radius-lg: 0.625rem;
/* ── Shadows (shadcn shadow-xs / shadow-sm) ── */
--lantern-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--lantern-shadow-md: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
/* ── Typography ── */
--lantern-font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--lantern-font-mono: ui-monospace, "SF Mono", "Cascadia Code", "JetBrains Mono", Menlo, monospace;
/* ── Motion ── */
--lantern-duration: 150ms;
--lantern-ease: cubic-bezier(0.4, 0, 0.2, 1);
/* ── Density (default = shadcn-compact: 32px controls, 13px text) ── */
--lantern-control-h: 2rem;
--lantern-control-px: 0.75rem;
--lantern-text: 0.8125rem;
--lantern-text-sm: 0.75rem;
--lantern-gap: 0.4375rem;
--lantern-radius: var(--lantern-radius-md);
}
/* Roomier scale for content-first hosts (marketing pages, forms-heavy apps). */
[data-lantern-density="comfortable"] {
--lantern-control-h: 2.25rem;
--lantern-control-px: 1rem;
--lantern-text: 0.875rem;
--lantern-text-sm: 0.8125rem;
--lantern-gap: 0.5rem;
}
/* Dark theme — explicit opt-in via `.dark`. */
.dark {
--lantern-fg: #fafafa;
--lantern-fg-muted: #a1a1aa;
--lantern-fg-subtle: #71717a;
--lantern-surface: #09090b;
--lantern-surface-raised: #0c0c0e;
--lantern-surface-sunken: #18181b;
--lantern-surface-hover: #26262a;
--lantern-border: #26262a;
--lantern-border-strong: #3f3f46;
--lantern-primary: #fafafa;
--lantern-primary-fg: #18181b;
--lantern-secondary: #27272a;
--lantern-secondary-fg: #fafafa;
--lantern-accent: oklch(0.685 0.182 39);
--lantern-accent-fg: #ffffff;
--lantern-danger: #ef4444;
--lantern-success: #22c55e;
--lantern-warning: #f59e0b;
--lantern-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.4);
--lantern-shadow-md: 0 1px 3px 0 rgb(0 0 0 / 0.5), 0 1px 2px -1px rgb(0 0 0 / 0.5);
}
/* System dark — applies unless the host explicitly opts into `.light`. */
@media (prefers-color-scheme: dark) {
:root:not(.light) {
--lantern-fg: #fafafa;
--lantern-fg-muted: #a1a1aa;
--lantern-fg-subtle: #71717a;
--lantern-surface: #09090b;
--lantern-surface-raised: #0c0c0e;
--lantern-surface-sunken: #18181b;
--lantern-surface-hover: #26262a;
--lantern-border: #26262a;
--lantern-border-strong: #3f3f46;
--lantern-primary: #fafafa;
--lantern-primary-fg: #18181b;
--lantern-secondary: #27272a;
--lantern-secondary-fg: #fafafa;
--lantern-accent: oklch(0.685 0.182 39);
--lantern-accent-fg: #ffffff;
--lantern-danger: #ef4444;
--lantern-success: #22c55e;
--lantern-warning: #f59e0b;
--lantern-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.4);
--lantern-shadow-md: 0 1px 3px 0 rgb(0 0 0 / 0.5), 0 1px 2px -1px rgb(0 0 0 / 0.5);
}
}
@media (prefers-reduced-motion: reduce) {
:root {
--lantern-duration: 0ms;
}
}
/* ═══ Components ══════════════════════════════════════════════════════════
*
* Namespaced (`lui-`) so LanternUI coexists with any host CSS (incl. Fluxon)
* with zero build step. Every value reads the tokens above; the consumer's
* `class=` attribute merges on top via LanternUI.Class.
*/
/* ── Icon ── */
.lui-icon {
width: 1.05em;
height: 1.05em;
flex-shrink: 0;
}
/* ── Button ──
* Colors set --lui-c; variants derive bg/border/fg from it, so all
* color × variant combinations work without a compiled class matrix. */
.lui-btn {
--lui-c: var(--lantern-primary);
--lui-c-fg: var(--lantern-primary-fg);
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
height: var(--lantern-control-h);
padding: 0 var(--lantern-control-px);
font-family: var(--lantern-font);
font-size: var(--lantern-text);
font-weight: 500;
letter-spacing: -0.006em;
white-space: nowrap;
border-radius: var(--lantern-radius);
border: 1px solid transparent;
cursor: pointer;
outline: none;
transition:
background-color var(--lantern-duration) var(--lantern-ease),
border-color var(--lantern-duration) var(--lantern-ease),
box-shadow var(--lantern-duration) var(--lantern-ease),
opacity var(--lantern-duration) var(--lantern-ease);
}
.lui-btn[data-color="danger"] { --lui-c: var(--lantern-danger); --lui-c-fg: var(--lantern-danger-fg); }
.lui-btn[data-color="warning"] { --lui-c: var(--lantern-warning); --lui-c-fg: #ffffff; }
.lui-btn[data-color="success"] { --lui-c: var(--lantern-success); --lui-c-fg: #ffffff; }
.lui-btn[data-color="info"] { --lui-c: var(--lantern-accent); --lui-c-fg: var(--lantern-accent-fg); }
.lui-btn[data-variant="solid"] {
background: var(--lui-c);
color: var(--lui-c-fg);
box-shadow: var(--lantern-shadow);
}
.lui-btn[data-variant="solid"]:hover { opacity: 0.9; }
.lui-btn[data-variant="soft"] {
background: color-mix(in oklab, var(--lui-c) 10%, transparent);
color: var(--lui-c);
}
.lui-btn[data-variant="soft"]:hover {
background: color-mix(in oklab, var(--lui-c) 16%, transparent);
}
.lui-btn[data-variant="surface"] {
background: var(--lantern-surface-sunken);
color: var(--lantern-fg);
}
.lui-btn[data-variant="surface"]:hover { background: var(--lantern-surface-hover); }
.lui-btn[data-variant="outline"],
.lui-btn[data-variant="dashed"] {
background: var(--lantern-surface-raised);
color: var(--lantern-fg);
border-color: var(--lantern-border);
box-shadow: var(--lantern-shadow);
}
.lui-btn[data-variant="dashed"] { border-style: dashed; }
.lui-btn[data-variant="outline"]:hover,
.lui-btn[data-variant="dashed"]:hover { background: var(--lantern-surface-sunken); }
.lui-btn[data-variant="ghost"] {
background: transparent;
color: var(--lantern-fg);
}
.lui-btn[data-variant="ghost"]:hover { background: var(--lantern-surface-sunken); }
.lui-btn:focus-visible,
.lui-input-wrap:focus-within,
.lui-cal-day:focus-visible,
.lui-cal-nav:focus-visible {
border-color: var(--lantern-ring);
box-shadow: 0 0 0 3px var(--lantern-ring-soft);
}
.lui-btn[disabled] {
opacity: 0.5;
pointer-events: none;
}
.lui-btn[data-size="xs"] { height: 1.5rem; padding: 0 0.5rem; font-size: var(--lantern-text-sm); }
.lui-btn[data-size="sm"] { height: 1.75rem; padding: 0 0.625rem; font-size: var(--lantern-text-sm); }
.lui-btn[data-size="lg"] { height: 2.25rem; padding: 0 1rem; }
.lui-btn[data-size="xl"] { height: 2.5rem; padding: 0 1.25rem; font-size: 0.875rem; }
.lui-btn[data-size^="icon"] { padding: 0; }
.lui-btn[data-size="icon-xs"] { width: 1.5rem; height: 1.5rem; }
.lui-btn[data-size="icon-sm"] { width: 1.75rem; height: 1.75rem; }
.lui-btn[data-size="icon-md"],
.lui-btn[data-size="icon"] { width: var(--lantern-control-h); height: var(--lantern-control-h); }
.lui-btn[data-size="icon-lg"] { width: 2.25rem; height: 2.25rem; }
.lui-btn[data-size="icon-xl"] { width: 2.5rem; height: 2.5rem; }
.lui-btn-group {
display: inline-flex;
}
.lui-btn-group .lui-btn:not(:first-child) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
margin-left: -1px;
}
.lui-btn-group .lui-btn:not(:last-child) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
/* ── Form field ── */
.lui-field {
display: flex;
flex-direction: column;
gap: 0.3125rem;
font-family: var(--lantern-font);
}
.lui-label {
font-size: var(--lantern-text);
font-weight: 550;
color: var(--lantern-fg);
display: inline-flex;
align-items: baseline;
gap: 0.375rem;
}
.lui-sublabel {
font-size: var(--lantern-text-sm);
font-weight: 400;
color: var(--lantern-fg-muted);
}
.lui-description,
.lui-help {
font-size: var(--lantern-text-sm);
color: var(--lantern-fg-muted);
margin: 0;
}
.lui-error {
display: flex;
align-items: center;
gap: 0.3125rem;
font-size: var(--lantern-text-sm);
color: var(--lantern-danger);
margin: 0;
}
.lui-error-icon { width: 0.875rem; height: 0.875rem; }
.lui-input-row {
display: flex;
align-items: stretch;
gap: 0.4375rem;
}
.lui-input-wrap {
flex: 1;
display: flex;
align-items: center;
gap: 0.4375rem;
min-width: 0;
height: var(--lantern-control-h);
padding: 0 0.625rem;
background: var(--lantern-surface);
border: 1px solid var(--lantern-border);
border-radius: var(--lantern-radius);
box-shadow: var(--lantern-shadow);
transition:
border-color var(--lantern-duration) var(--lantern-ease),
box-shadow var(--lantern-duration) var(--lantern-ease);
}
.lui-input-wrap[data-invalid] {
border-color: var(--lantern-danger);
}
.lui-input-wrap[data-invalid]:focus-within {
border-color: var(--lantern-danger);
box-shadow: 0 0 0 3px color-mix(in oklab, var(--lantern-danger) 20%, transparent);
}
.lui-input-wrap[data-disabled] {
opacity: 0.5;
pointer-events: none;
}
.lui-input {
flex: 1;
min-width: 0;
border: none;
background: none;
outline: none;
padding: 0;
font-family: inherit;
font-size: var(--lantern-text);
color: var(--lantern-fg);
}
.lui-input::placeholder { color: var(--lantern-fg-subtle); }
.lui-inner-affix {
display: inline-flex;
align-items: center;
color: var(--lantern-fg-muted);
font-size: var(--lantern-text-sm);
}
.lui-outer-affix { display: inline-flex; align-items: center; }
.lui-field[data-size="xs"] .lui-input-wrap { height: 1.5rem; }
.lui-field[data-size="sm"] .lui-input-wrap { height: 1.75rem; }
.lui-field[data-size="lg"] .lui-input-wrap { height: 2.25rem; }
.lui-field[data-size="xl"] .lui-input-wrap { height: 2.5rem; }
/* ── Calendar ── */
.lui-cal {
display: inline-block;
padding: 0.75rem;
background: var(--lantern-surface-raised);
border: 1px solid var(--lantern-border);
border-radius: var(--lantern-radius-lg);
box-shadow: var(--lantern-shadow-md);
font-family: var(--lantern-font);
user-select: none;
}
.lui-cal-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.lui-cal-title {
font-size: var(--lantern-text);
font-weight: 600;
color: var(--lantern-fg);
letter-spacing: -0.01em;
}
.lui-cal-nav {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.75rem;
height: 1.75rem;
border: 1px solid var(--lantern-border);
border-radius: var(--lantern-radius-sm);
background: var(--lantern-surface-raised);
color: var(--lantern-fg-muted);
cursor: pointer;
outline: none;
transition: background-color var(--lantern-duration) var(--lantern-ease);
}
.lui-cal-nav:hover { background: var(--lantern-surface-sunken); color: var(--lantern-fg); }
.lui-cal-nav .lui-icon { width: 0.875rem; height: 0.875rem; }
.lui-cal-grid { display: flex; flex-direction: column; gap: 2px; }
.lui-cal-row { display: grid; grid-template-columns: repeat(7, 2rem); gap: 2px; }
.lui-cal-wd {
text-align: center;
font-size: var(--lantern-text-sm);
color: var(--lantern-fg-subtle);
font-weight: 500;
padding: 0.25rem 0;
}
.lui-cal-day {
height: 2rem;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: var(--lantern-text);
font-variant-numeric: tabular-nums;
color: var(--lantern-fg);
background: transparent;
border: 1px solid transparent;
border-radius: var(--lantern-radius-sm);
cursor: pointer;
outline: none;
transition: background-color var(--lantern-duration) var(--lantern-ease);
}
.lui-cal-day:hover { background: var(--lantern-surface-sunken); }
.lui-cal-day[data-outside] { color: var(--lantern-fg-subtle); }
.lui-cal-day[data-today] { box-shadow: inset 0 0 0 1.5px var(--lantern-accent); }
.lui-cal-day[aria-selected="true"] {
background: var(--lantern-primary);
color: var(--lantern-primary-fg);
font-weight: 600;
}
.lui-cal-day[disabled] {
opacity: 0.35;
pointer-events: none;
}
/* ── Segmented date/time field ── */
.lui-dtf {
display: inline-flex;
align-items: center;
gap: 0.25rem;
height: var(--lantern-control-h);
padding: 0 0.625rem;
background: var(--lantern-surface);
border: 1px solid var(--lantern-border);
border-radius: var(--lantern-radius);
box-shadow: var(--lantern-shadow);
font-family: var(--lantern-font);
font-size: var(--lantern-text);
font-variant-numeric: tabular-nums;
color: var(--lantern-fg);
transition:
border-color var(--lantern-duration) var(--lantern-ease),
box-shadow var(--lantern-duration) var(--lantern-ease);
}
.lui-dtf:focus-within {
border-color: var(--lantern-ring);
box-shadow: 0 0 0 3px var(--lantern-ring-soft);
}
.lui-dtf[data-disabled] {
opacity: 0.5;
pointer-events: none;
}
.lui-dtf-segs {
display: inline-flex;
align-items: baseline;
flex: 1;
min-width: 0;
}
.lui-dtf-seg {
padding: 0.0625rem 0.1875rem;
border-radius: 0.25rem;
outline: none;
cursor: text;
}
.lui-dtf-seg:hover { background: var(--lantern-surface-sunken); }
.lui-dtf-seg:focus {
background: var(--lantern-primary);
color: var(--lantern-primary-fg);
}
.lui-dtf-seg:not([data-set]) { color: var(--lantern-fg-subtle); }
.lui-dtf-seg:not([data-set]):focus { color: var(--lantern-primary-fg); }
.lui-dtf-sep {
color: var(--lantern-fg-subtle);
padding: 0 0.0625rem;
}
.lui-dtf-clear {
border: none;
background: none;
color: var(--lantern-fg-subtle);
font-size: var(--lantern-text);
line-height: 1;
padding: 0.125rem 0.25rem;
border-radius: 0.25rem;
cursor: pointer;
}
.lui-dtf-clear:hover { color: var(--lantern-danger); background: var(--lantern-surface-sunken); }
.lui-dtf-suffix {
display: inline-flex;
align-items: center;
color: var(--lantern-fg-muted);
}
/* ── Picker (field + calendar popover composition) ── */
.lui-picker {
position: relative;
display: inline-flex;
flex: 1;
min-width: 0;
}
.lui-picker .lui-dtf { flex: 1; }
.lui-picker-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
background: none;
color: var(--lantern-fg-muted);
padding: 0.125rem;
border-radius: 0.25rem;
cursor: pointer;
outline: none;
}
.lui-picker-toggle:hover { color: var(--lantern-fg); background: var(--lantern-surface-sunken); }
.lui-picker-toggle:focus-visible {
box-shadow: 0 0 0 3px var(--lantern-ring-soft);
}
.lui-picker-toggle .lui-icon { width: 1rem; height: 1rem; }
.lui-picker-panel {
z-index: 50;
/* position:fixed + top/left set by the runtime's position() */
}
.lui-picker-panel .lui-picker-cal { display: block; }
.lui-picker-foot {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.5rem 0.75rem;
margin-top: -0.25rem;
background: var(--lantern-surface-raised);
border: 1px solid var(--lantern-border);
border-top: none;
border-radius: 0 0 var(--lantern-radius-lg) var(--lantern-radius-lg);
box-shadow: var(--lantern-shadow-md);
}
.lui-picker-foot-space { flex: 1; }
.lui-picker-panel .lui-cal { border-radius: var(--lantern-radius-lg) var(--lantern-radius-lg) 0 0; box-shadow: none; }