@apliteni/apliteni-ui 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/motion.js ADDED
@@ -0,0 +1,68 @@
1
+ // apliteni-ui — motion hook.
2
+ //
3
+ // The one small, optional piece of JS behind the CSS motion library: a
4
+ // scroll-reveal activator plus a replay helper. Everything else is pure CSS
5
+ // (src/styles/motion.css). Framework-agnostic, no dependencies, guarded so it
6
+ // no-ops cleanly under `node --test` / SSR (no window, no IntersectionObserver).
7
+ //
8
+ // import { initReveal } from 'apliteni-ui/motion'
9
+ // initReveal(); // wire every [data-reveal] on the page
10
+ // initReveal(myContainer); // scope to a subtree
11
+ //
12
+ // Stagger a group by giving siblings --reveal-i: 0, 1, 2, … (see motion.css).
13
+
14
+ /** True only when the user asked the OS to reduce motion. Safe off-DOM. */
15
+ export function prefersReducedMotion() {
16
+ return (
17
+ typeof matchMedia === 'function' &&
18
+ matchMedia('(prefers-reduced-motion: reduce)').matches
19
+ );
20
+ }
21
+
22
+ /** Stagger delay for the Nth item given a per-step delay. Pure + testable. */
23
+ export function staggerDelay(index, stepMs = 120) {
24
+ return Math.max(0, index) * stepMs;
25
+ }
26
+
27
+ /**
28
+ * Reveal every [data-reveal] under `root` as it scrolls into view.
29
+ * - Marks <html class="js-reveal"> so the CSS hides them only when JS is live.
30
+ * - Reduced-motion or no IntersectionObserver → reveal everything at once.
31
+ * Returns the observer (or undefined when there's nothing to observe).
32
+ */
33
+ export function initReveal(root) {
34
+ if (typeof document === 'undefined') return undefined;
35
+ const scope = root || document;
36
+ const els = scope.querySelectorAll('[data-reveal]');
37
+ if (!els.length) return undefined;
38
+
39
+ document.documentElement.classList.add('js-reveal');
40
+ const show = (el) => el.classList.add('is-revealed');
41
+
42
+ if (prefersReducedMotion() || typeof IntersectionObserver === 'undefined') {
43
+ els.forEach(show);
44
+ return undefined;
45
+ }
46
+
47
+ const io = new IntersectionObserver(
48
+ (entries, obs) => {
49
+ for (const entry of entries) {
50
+ if (entry.isIntersecting) {
51
+ show(entry.target);
52
+ obs.unobserve(entry.target);
53
+ }
54
+ }
55
+ },
56
+ { rootMargin: '0px 0px -10% 0px', threshold: 0.05 },
57
+ );
58
+ els.forEach((el) => io.observe(el));
59
+ return io;
60
+ }
61
+
62
+ /** Restart the CSS animation on an element (for a "replay" control). */
63
+ export function replay(el) {
64
+ if (!el || !el.style) return;
65
+ el.style.animation = 'none';
66
+ void el.offsetWidth; // force reflow so the next frame re-triggers
67
+ el.style.animation = '';
68
+ }
@@ -0,0 +1,93 @@
1
+ /* ============================================================================
2
+ * aurora — ambient warm backdrop: layered blurred glow "blobs" + optional
3
+ * paper grain. The kit's one signature page background, so every app gets one
4
+ * warm identity for free.
5
+ *
6
+ * Markup comes from aurora() in components/index.js, but you can hand-roll it:
7
+ * <div class="ui-aurora ui-aurora--animated" aria-hidden="true">
8
+ * <span class="ui-aurora__blob ui-aurora__blob--accent" style="--au-x:12%;--au-y:-4%"></span>
9
+ * ...
10
+ * <span class="ui-aurora__grain"></span>
11
+ * </div>
12
+ *
13
+ * Colours default to the accent glow tokens, so the whole field re-themes with
14
+ * the sub-theme (phoenix / ocean / emerald) and holds up in dark AND light with
15
+ * no per-accent rules. Override --au-1/2/3 on .ui-aurora to retune.
16
+ * ========================================================================== */
17
+
18
+ .ui-aurora {
19
+ /* Tone slots — mixed to a visible alpha off the accent families so the field
20
+ actually reads as an aurora (the --glow-* tokens are tuned faint for the
21
+ small .ui-bg-* utilities). --purple-light + --grad-to re-point per accent,
22
+ so the whole field re-themes; --cyan stays a fixed teal. Override to retune. */
23
+ --au-1: color-mix(in srgb, var(--purple-light) 52%, transparent);
24
+ --au-2: color-mix(in srgb, var(--cyan) 40%, transparent);
25
+ --au-3: color-mix(in srgb, var(--grad-to) 44%, transparent);
26
+
27
+ position: absolute;
28
+ inset: 0;
29
+ z-index: 0;
30
+ overflow: hidden;
31
+ pointer-events: none;
32
+ }
33
+ /* Light theme: the accent families are darker solids, so dial the mix right
34
+ down — a whisper of tint, not a wash. */
35
+ :root[data-theme="light"] .ui-aurora {
36
+ --au-1: color-mix(in srgb, var(--purple-light) 18%, transparent);
37
+ --au-2: color-mix(in srgb, var(--cyan) 15%, transparent);
38
+ --au-3: color-mix(in srgb, var(--grad-to) 17%, transparent);
39
+ }
40
+
41
+ /* Full-bleed: pin to the viewport behind the whole page. Give real content a
42
+ normal (non-negative) stacking context and it sits on top automatically. */
43
+ .ui-aurora--fixed {
44
+ position: fixed;
45
+ z-index: -1;
46
+ }
47
+
48
+ /* A blurred radial blob. Position via --au-x / --au-y (its centre), scale via
49
+ --au-size. Centring uses `translate` so the drift animation owns `transform`. */
50
+ .ui-aurora__blob {
51
+ position: absolute;
52
+ top: var(--au-y, 50%);
53
+ left: var(--au-x, 50%);
54
+ width: var(--au-size, 60%);
55
+ aspect-ratio: 1;
56
+ translate: -50% -50%;
57
+ border-radius: 50%;
58
+ background: radial-gradient(circle at center, var(--au-tone), transparent 68%);
59
+ filter: blur(70px);
60
+ will-change: transform;
61
+ }
62
+ .ui-aurora__blob--accent { --au-tone: var(--au-1); }
63
+ .ui-aurora__blob--teal { --au-tone: var(--au-2); }
64
+ .ui-aurora__blob--warm { --au-tone: var(--au-3); }
65
+
66
+ /* Slow ambient drift. Staggered per blob via --au-delay so they never line up. */
67
+ .ui-aurora--animated .ui-aurora__blob {
68
+ animation: ui-aurora-drift 26s ease-in-out infinite alternate;
69
+ animation-delay: var(--au-delay, 0s);
70
+ }
71
+ @keyframes ui-aurora-drift {
72
+ from { transform: translate(-2%, -1%) scale(1); }
73
+ to { transform: translate(3%, 2%) scale(1.12); }
74
+ }
75
+
76
+ /* Paper grain — a low-opacity fractal-noise veil. `overlay` in dark keeps it
77
+ from muddying the blacks; `multiply` in light reads as fine tooth on paper. */
78
+ .ui-aurora__grain {
79
+ position: absolute;
80
+ inset: 0;
81
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
82
+ background-size: 160px 160px;
83
+ opacity: 0.5;
84
+ mix-blend-mode: overlay;
85
+ }
86
+ :root[data-theme="light"] .ui-aurora__grain {
87
+ opacity: 0.4;
88
+ mix-blend-mode: multiply;
89
+ }
90
+
91
+ @media (prefers-reduced-motion: reduce) {
92
+ .ui-aurora__blob { animation: none; }
93
+ }
@@ -89,8 +89,12 @@
89
89
  pointer-events: none;
90
90
  }
91
91
 
92
- /* Busy: keep the label, run a gradient-bar loader along the base, and the
93
- button is disabled (not clickable). Pass busy + the .ui-btn__bars markup. */
92
+ /* Busy: keep the label, run an indeterminate accent shimmer along the base, and
93
+ the button is disabled (not clickable). Pass busy + the .ui-btn__bars markup.
94
+ The track is inset into a rounded pill so it sits INSIDE the button — not
95
+ flush against the clipped corners — and tints from the live --accent so it
96
+ reads as an intentional progress element in every theme (and mutes uniformly
97
+ with the button in the disabled state). */
94
98
  .ui-btn[aria-busy="true"] {
95
99
  pointer-events: none;
96
100
  position: relative;
@@ -98,21 +102,46 @@
98
102
  }
99
103
  .ui-btn__bars {
100
104
  position: absolute;
101
- left: 0; right: 0; bottom: 0;
105
+ left: 10px; right: 10px; bottom: 5px;
102
106
  height: 3px;
103
- background: color-mix(in srgb, currentColor 20%, transparent);
107
+ border-radius: 999px;
108
+ background: color-mix(in srgb, var(--accent) 22%, transparent);
104
109
  overflow: hidden;
110
+ /* Clean entrance: grow + fade in on state change, once. */
111
+ transform-origin: center;
112
+ animation: ui-bars-in 180ms var(--ease) both;
105
113
  }
114
+ .ui-btn--sm .ui-btn__bars { left: 7px; right: 7px; bottom: 4px; }
115
+ .ui-btn--lg .ui-btn__bars { left: 14px; right: 14px; bottom: 7px; }
106
116
  .ui-btn__bars i {
107
117
  position: absolute;
108
118
  height: 100%;
109
- border-radius: 3px;
110
- background: linear-gradient(90deg, var(--purple-light), var(--cyan));
119
+ border-radius: 999px;
120
+ background: linear-gradient(90deg,
121
+ color-mix(in srgb, var(--accent) 30%, transparent), var(--accent));
122
+ }
123
+ /* On the filled primary the surface IS the accent, so shimmer in the contrast
124
+ colour instead — otherwise the bars vanish into the button. */
125
+ .ui-btn--primary .ui-btn__bars {
126
+ background: color-mix(in srgb, var(--accent-contrast) 26%, transparent);
127
+ }
128
+ .ui-btn--primary .ui-btn__bars i {
129
+ background: linear-gradient(90deg,
130
+ color-mix(in srgb, var(--accent-contrast) 35%, transparent),
131
+ var(--accent-contrast));
132
+ }
133
+ .ui-btn__bars i:nth-child(1) { width: 38%; animation: ui-bars1 2s cubic-bezier(0.65, 0.05, 0.35, 1) infinite; }
134
+ .ui-btn__bars i:nth-child(2) { width: 22%; animation: ui-bars2 2s cubic-bezier(0.65, 0.05, 0.35, 1) 0.8s infinite; }
135
+ @keyframes ui-bars1 { 0% { left: -45%; } 100% { left: 110%; } }
136
+ @keyframes ui-bars2 { 0% { left: -28%; } 100% { left: 120%; } }
137
+ @keyframes ui-bars-in { from { opacity: 0; transform: scaleX(0.72); } to { opacity: 1; transform: scaleX(1); } }
138
+ /* Reduced motion: no entrance, no sweep — just a static, intentional accent
139
+ fill so the busy state still reads clearly. */
140
+ @media (prefers-reduced-motion: reduce) {
141
+ .ui-btn__bars { animation: none; opacity: 1; transform: none; }
142
+ .ui-btn__bars i { animation: none; }
143
+ .ui-btn__bars i:nth-child(1) { left: 0; width: 100%; }
144
+ .ui-btn__bars i:nth-child(2) { display: none; }
111
145
  }
112
- .ui-btn__bars i:nth-child(1) { width: 35%; animation: ui-bars1 2s cubic-bezier(0.65, 0.05, 0.35, 1) infinite; }
113
- .ui-btn__bars i:nth-child(2) { width: 20%; animation: ui-bars2 2s cubic-bezier(0.65, 0.05, 0.35, 1) 0.8s infinite; }
114
- @keyframes ui-bars1 { 0% { left: -40%; } 100% { left: 110%; } }
115
- @keyframes ui-bars2 { 0% { left: -25%; } 100% { left: 120%; } }
116
- @media (prefers-reduced-motion: reduce) { .ui-btn__bars i { animation: none; } .ui-btn__bars i:nth-child(1) { left: 0; } }
117
146
 
118
147
  @keyframes ui-spin { to { transform: rotate(360deg); } }
@@ -26,28 +26,95 @@
26
26
  .ui-callout--danger { background: var(--glow-pink); }
27
27
  .ui-callout--danger .ui-callout__icon { color: var(--pink); }
28
28
 
29
- /* Toast — a floating, dismissible notification */
29
+ /* ----------------------------------------------------------------------------
30
+ * Toast — a floating, dismissible notification.
31
+ * A status (colour) × style (surface) matrix, entirely token-driven:
32
+ * status success · danger · warn · info · neutral → sets --toast-accent/-glow/-on
33
+ * style soft (tinted) · solid (filled) · outline (bordered)
34
+ * -------------------------------------------------------------------------- */
30
35
  .ui-toast {
36
+ position: relative;
31
37
  display: flex;
32
- align-items: center;
38
+ align-items: flex-start;
33
39
  gap: 12px;
34
- padding: 13px 16px;
40
+ padding: 13px 15px;
35
41
  border-radius: var(--radius-md);
36
- background: var(--bg-elevated);
37
- box-shadow: var(--shadow-lg);
38
42
  color: var(--text);
39
43
  font-size: var(--text-base);
40
- max-width: 380px;
44
+ width: 100%;
45
+ max-width: 400px;
46
+ overflow: hidden; /* clips the timer bar + left marker to the rounded corners */
47
+ animation: ui-toast-in 0.28s cubic-bezier(0.22, 1, 0.36, 1) both;
41
48
  }
42
- .ui-toast__icon { width: 20px; height: 20px; flex: none; display: grid; place-items: center; border-radius: 50%; }
43
- .ui-toast__icon svg { width: 13px; height: 13px; }
44
- .ui-toast--success .ui-toast__icon { background: var(--green); color: #0c0c0c; }
45
- .ui-toast--danger .ui-toast__icon { background: var(--pink); color: #fff; }
46
- .ui-toast__body { flex: 1; }
49
+
50
+ /* status the paint tokens every style below consumes (--toast-on = the ink
51
+ that reads on the accent when it becomes a fill) */
52
+ .ui-toast--success { --toast-accent: var(--green); --toast-glow: var(--glow-green); --toast-on: #0c0c0c; }
53
+ .ui-toast--danger { --toast-accent: var(--pink); --toast-glow: var(--glow-pink); --toast-on: #ffffff; }
54
+ .ui-toast--warn { --toast-accent: var(--amber); --toast-glow: color-mix(in srgb, var(--amber) 14%, transparent); --toast-on: #0c0c0c; }
55
+ .ui-toast--info { --toast-accent: var(--cyan); --toast-glow: var(--glow-cyan); --toast-on: #0c0c0c; }
56
+ .ui-toast--neutral { --toast-accent: var(--muted); --toast-glow: var(--surface-2); --toast-on: var(--strong); }
57
+
58
+ /* left status marker (soft + outline; solid is already a full fill) */
59
+ .ui-toast--soft::before,
60
+ .ui-toast--outline::before {
61
+ content: ''; position: absolute; left: 0; top: 0; bottom: 0;
62
+ width: 3px; background: var(--toast-accent);
63
+ }
64
+
65
+ .ui-toast__icon { width: 22px; height: 22px; flex: none; margin-top: 1px; display: grid; place-items: center; border-radius: 50%; background: var(--toast-accent); color: var(--toast-on); }
66
+ .ui-toast__icon svg { width: 13px; height: 13px; stroke: currentColor; fill: none; stroke-width: 2; }
67
+ .ui-toast__body { flex: 1; min-width: 0; }
47
68
  .ui-toast__title { font-weight: var(--weight-medium); color: var(--strong); }
48
- .ui-toast__close { background: none; border: 0; color: var(--muted); cursor: pointer; padding: 2px; display: grid; place-items: center; }
69
+ .ui-toast__text { color: var(--dim); }
70
+
71
+ /* trailing action ("Undo" / "Retry") */
72
+ .ui-toast__action { flex: none; align-self: center; background: none; border: 0; padding: 4px 9px; border-radius: var(--radius-xs); font: inherit; font-weight: var(--weight-medium); color: var(--toast-accent); cursor: pointer; }
73
+ .ui-toast__action:hover { background: color-mix(in srgb, var(--toast-accent) 14%, transparent); }
74
+
75
+ .ui-toast__close { flex: none; align-self: flex-start; background: none; border: 0; color: var(--muted); cursor: pointer; padding: 2px; display: grid; place-items: center; }
76
+ .ui-toast__close svg { width: 15px; height: 15px; stroke: currentColor; fill: none; stroke-width: 1.8; }
49
77
  .ui-toast__close:hover { color: var(--text); }
50
78
 
79
+ /* auto-dismiss progress bar — .is-running is added by wireToastStack() */
80
+ .ui-toast__timer { position: absolute; left: 0; bottom: 0; height: 3px; width: 100%; background: var(--toast-accent); opacity: 0.85; transform-origin: left; }
81
+ .ui-toast__timer.is-running { animation: ui-toast-timer var(--toast-dur, 5s) linear forwards; }
82
+
83
+ /* style: soft — tinted surface */
84
+ .ui-toast--soft { background: var(--toast-glow); box-shadow: var(--shadow-sm); }
85
+
86
+ /* style: solid — filled accent, ink flips to --toast-on */
87
+ .ui-toast--solid { background: var(--toast-accent); color: var(--toast-on); box-shadow: var(--shadow-lg); }
88
+ .ui-toast--solid .ui-toast__title,
89
+ .ui-toast--solid .ui-toast__text { color: var(--toast-on); }
90
+ .ui-toast--solid .ui-toast__text { opacity: 0.82; }
91
+ .ui-toast--solid .ui-toast__icon { background: color-mix(in srgb, var(--toast-on) 22%, transparent); color: var(--toast-on); }
92
+ .ui-toast--solid .ui-toast__action { color: var(--toast-on); }
93
+ .ui-toast--solid .ui-toast__action:hover { background: color-mix(in srgb, var(--toast-on) 18%, transparent); }
94
+ .ui-toast--solid .ui-toast__close { color: color-mix(in srgb, var(--toast-on) 68%, transparent); }
95
+ .ui-toast--solid .ui-toast__close:hover { color: var(--toast-on); }
96
+ .ui-toast--solid .ui-toast__timer { background: color-mix(in srgb, var(--toast-on) 55%, transparent); }
97
+
98
+ /* style: outline — elevated card with a status-tinted border */
99
+ .ui-toast--outline { background: var(--bg-elevated); box-shadow: var(--shadow-md); border: 1px solid color-mix(in srgb, var(--toast-accent) 40%, transparent); }
100
+
101
+ /* compact — single line, no body text */
102
+ .ui-toast--compact { align-items: center; padding-top: 10px; padding-bottom: 10px; }
103
+ .ui-toast--compact .ui-toast__icon { margin-top: 0; }
104
+
105
+ @keyframes ui-toast-in { from { opacity: 0; transform: translateX(18px); } to { opacity: 1; transform: none; } }
106
+ @keyframes ui-toast-out { to { opacity: 0; transform: translateX(24px); } }
107
+ @keyframes ui-toast-timer { from { transform: scaleX(1); } to { transform: scaleX(0); } }
108
+ .ui-toast.is-leaving { animation: ui-toast-out 0.2s ease forwards; }
109
+
110
+ @media (prefers-reduced-motion: reduce) {
111
+ .ui-toast, .ui-toast.is-leaving { animation: none; }
112
+ .ui-toast__timer.is-running { animation: none; }
113
+ }
114
+
115
+ /* stack container for queued toasts */
116
+ .ui-toast-stack { display: flex; flex-direction: column; gap: 12px; width: 400px; max-width: 100%; }
117
+
51
118
  /* Success panel — centered confirmation, used after form submit */
52
119
  .ui-success {
53
120
  border-radius: var(--radius-lg);
@@ -0,0 +1,166 @@
1
+ /* ============================================================================
2
+ * Drawer — edge-anchored overlay panel (Sheet / Slide-over): a panel that
3
+ * slides in from any screen edge over a scrim. Fully token-driven so it
4
+ * re-themes across accents + light/dark. Shares its scrim + focus-trap concept
5
+ * with a future Modal (see drawer.js). `prefers-reduced-motion` drops the slide
6
+ * for a plain fade.
7
+ *
8
+ * Local tokens (surface/shadow/scrim) so the panel + scrim re-theme cleanly:
9
+ * --drawer-surface panel background --drawer-shadow panel elevation
10
+ * --drawer-scrim backdrop colour (overridden for light below)
11
+ * ========================================================================== */
12
+
13
+ .ui-drawer {
14
+ --drawer-surface: var(--surface-2);
15
+ --drawer-shadow: var(--shadow-lg);
16
+ --drawer-scrim: rgba(0, 0, 0, 0.58);
17
+ --drawer-w: 420px; /* left|right panels: width */
18
+ --drawer-h: 45vh; /* top|bottom panels: height */
19
+
20
+ position: fixed;
21
+ inset: 0;
22
+ z-index: var(--z-overlay);
23
+ pointer-events: none; /* container never blocks; scrim/panel opt back in */
24
+ }
25
+ :root[data-theme="light"] .ui-drawer { --drawer-scrim: rgba(20, 22, 40, 0.38); }
26
+
27
+ /* Scrim — fades in; visibility trails the fade so the panel can animate out. */
28
+ .ui-drawer__scrim {
29
+ position: absolute;
30
+ inset: 0;
31
+ background: var(--drawer-scrim);
32
+ pointer-events: auto;
33
+ opacity: 0;
34
+ visibility: hidden;
35
+ transition: opacity var(--dur-med) var(--ease), visibility var(--dur-med) var(--ease);
36
+ }
37
+ .ui-drawer.is-open .ui-drawer__scrim { opacity: 1; visibility: visible; }
38
+
39
+ /* Panel — the sliding surface. Flex column: header / scrollable body / footer. */
40
+ .ui-drawer__panel {
41
+ position: absolute;
42
+ display: flex;
43
+ flex-direction: column;
44
+ background: var(--drawer-surface);
45
+ box-shadow: var(--drawer-shadow);
46
+ pointer-events: auto;
47
+ opacity: 1;
48
+ visibility: hidden;
49
+ transition:
50
+ transform var(--dur-med) var(--ease),
51
+ opacity var(--dur-med) var(--ease),
52
+ visibility var(--dur-med) var(--ease);
53
+ }
54
+ .ui-drawer__panel:focus-visible { outline: none; box-shadow: var(--drawer-shadow), var(--ring); }
55
+ .ui-drawer.is-open .ui-drawer__panel { visibility: visible; transform: none; }
56
+
57
+ /* -- Edge anchoring + slide-in direction ---------------------------------- */
58
+ .ui-drawer--right .ui-drawer__panel,
59
+ .ui-drawer--left .ui-drawer__panel {
60
+ top: 0; bottom: 0;
61
+ width: min(var(--drawer-w), 100vw);
62
+ max-width: 100vw;
63
+ }
64
+ .ui-drawer--right .ui-drawer__panel {
65
+ right: 0;
66
+ border-left: 1px solid var(--border);
67
+ transform: translateX(100%);
68
+ }
69
+ .ui-drawer--left .ui-drawer__panel {
70
+ left: 0;
71
+ border-right: 1px solid var(--border);
72
+ transform: translateX(-100%);
73
+ }
74
+ .ui-drawer--top .ui-drawer__panel,
75
+ .ui-drawer--bottom .ui-drawer__panel {
76
+ left: 0; right: 0;
77
+ height: min(var(--drawer-h), 100vh);
78
+ max-height: 100vh;
79
+ }
80
+ .ui-drawer--top .ui-drawer__panel {
81
+ top: 0;
82
+ border-bottom: 1px solid var(--border);
83
+ transform: translateY(-100%);
84
+ }
85
+ .ui-drawer--bottom .ui-drawer__panel {
86
+ bottom: 0;
87
+ border-top: 1px solid var(--border);
88
+ transform: translateY(100%);
89
+ }
90
+
91
+ /* -- Sizes ----------------------------------------------------------------- */
92
+ /* left|right → width; top|bottom → height. Same names, axis chosen by side. */
93
+ .ui-drawer--right.ui-drawer--sm, .ui-drawer--left.ui-drawer--sm { --drawer-w: 320px; }
94
+ .ui-drawer--right.ui-drawer--md, .ui-drawer--left.ui-drawer--md { --drawer-w: 420px; }
95
+ .ui-drawer--right.ui-drawer--lg, .ui-drawer--left.ui-drawer--lg { --drawer-w: 560px; }
96
+ .ui-drawer--top.ui-drawer--sm, .ui-drawer--bottom.ui-drawer--sm { --drawer-h: 30vh; }
97
+ .ui-drawer--top.ui-drawer--md, .ui-drawer--bottom.ui-drawer--md { --drawer-h: 45vh; }
98
+ .ui-drawer--top.ui-drawer--lg, .ui-drawer--bottom.ui-drawer--lg { --drawer-h: 72vh; }
99
+
100
+ /* -- Slots ----------------------------------------------------------------- */
101
+ .ui-drawer__header {
102
+ display: flex;
103
+ align-items: center;
104
+ justify-content: space-between;
105
+ gap: 12px;
106
+ padding: 18px 20px;
107
+ border-bottom: 1px solid var(--border);
108
+ flex: none;
109
+ }
110
+ .ui-drawer__title {
111
+ margin: 0;
112
+ font-family: var(--font-sans);
113
+ font-size: 15px;
114
+ font-weight: 600;
115
+ letter-spacing: 0.01em;
116
+ color: var(--strong);
117
+ }
118
+ .ui-drawer__close {
119
+ display: inline-flex;
120
+ align-items: center;
121
+ justify-content: center;
122
+ width: 30px; height: 30px;
123
+ flex: none;
124
+ cursor: pointer;
125
+ color: var(--muted);
126
+ background: transparent;
127
+ border: 1px solid transparent;
128
+ border-radius: var(--radius-sm);
129
+ transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease;
130
+ }
131
+ .ui-drawer__close:hover { color: var(--strong); background: var(--surface); }
132
+ .ui-drawer__close:focus-visible { outline: none; box-shadow: var(--ring); border-color: var(--accent); }
133
+ .ui-drawer__close svg { width: 18px; height: 18px; stroke: currentColor; fill: none; stroke-width: 1.8; }
134
+
135
+ .ui-drawer__body {
136
+ flex: 1 1 auto;
137
+ min-height: 0;
138
+ overflow-y: auto;
139
+ padding: 20px;
140
+ color: var(--text);
141
+ font-family: var(--font-sans);
142
+ font-size: 13px;
143
+ line-height: 1.55;
144
+ }
145
+
146
+ .ui-drawer__footer {
147
+ display: flex;
148
+ align-items: center;
149
+ justify-content: flex-end;
150
+ gap: 10px;
151
+ padding: 16px 20px;
152
+ border-top: 1px solid var(--border);
153
+ background: var(--surface-2);
154
+ flex: none;
155
+ }
156
+
157
+ /* -- Reduced motion: no slide, just a fade -------------------------------- */
158
+ @media (prefers-reduced-motion: reduce) {
159
+ .ui-drawer__panel {
160
+ transform: none !important;
161
+ opacity: 0;
162
+ transition: opacity var(--dur-fast) linear, visibility var(--dur-fast) linear;
163
+ }
164
+ .ui-drawer.is-open .ui-drawer__panel { opacity: 1; }
165
+ .ui-drawer__scrim { transition: opacity var(--dur-fast) linear, visibility var(--dur-fast) linear; }
166
+ }
@@ -0,0 +1,133 @@
1
+ /* ============================================================================
2
+ * Dropdown — the kit's popover-list primitive (trigger + panel + item rows).
3
+ * Fully token-driven so it re-themes across accents + light/dark. Shares its
4
+ * open/close JS with the topbar's version switcher and account menu, which keep
5
+ * their own bespoke classes; this file styles the standalone `dropdown()`.
6
+ * ========================================================================== */
7
+
8
+ .ui-dropdown { position: relative; display: inline-flex; }
9
+
10
+ /* Trigger — pill button: [pre] [value] [chevron] */
11
+ .ui-dropdown__trigger {
12
+ display: inline-flex;
13
+ align-items: center;
14
+ gap: 8px;
15
+ cursor: pointer;
16
+ font-family: var(--font-sans);
17
+ font-size: 12.5px;
18
+ font-weight: 500;
19
+ color: var(--text);
20
+ background: var(--surface);
21
+ border: 1px solid var(--border);
22
+ border-radius: var(--radius-pill);
23
+ padding: 7px 11px 7px 13px;
24
+ transition: border-color 0.2s ease, background 0.2s ease;
25
+ }
26
+ .ui-dropdown__trigger:hover { border-color: var(--accent); }
27
+ .ui-dropdown__trigger:focus-visible { outline: none; box-shadow: var(--ring); border-color: var(--accent); }
28
+ .ui-dropdown__pre { color: var(--muted); }
29
+ .ui-dropdown__value { color: var(--strong); font-weight: 600; letter-spacing: 0.01em; }
30
+
31
+ /* Chevron — border caret that flips when the container is .open */
32
+ .ui-dropdown__chevron {
33
+ width: 8px; height: 8px;
34
+ border-right: 1.6px solid var(--muted);
35
+ border-bottom: 1.6px solid var(--muted);
36
+ transform: rotate(45deg) translateY(-1px);
37
+ transition: transform 0.2s ease;
38
+ flex: none;
39
+ }
40
+ .ui-dropdown.open .ui-dropdown__chevron { transform: rotate(-135deg) translateY(2px); }
41
+
42
+ /* Panel — the popover surface */
43
+ .ui-dropdown__panel {
44
+ position: absolute;
45
+ top: calc(100% + 9px);
46
+ left: 0;
47
+ min-width: 240px;
48
+ background: var(--surface-2);
49
+ border: 1px solid var(--border);
50
+ border-radius: var(--radius-lg);
51
+ padding: 6px;
52
+ box-shadow: var(--shadow-lg);
53
+ opacity: 0;
54
+ visibility: hidden;
55
+ transform: translateY(-6px);
56
+ transition: opacity 0.18s ease, transform 0.18s ease, visibility 0.18s ease;
57
+ z-index: var(--z-dropdown);
58
+ }
59
+ .ui-dropdown__panel.is-end { left: auto; right: 0; }
60
+ .ui-dropdown__panel.is-scroll { max-height: 300px; overflow-y: auto; }
61
+ .ui-dropdown.open .ui-dropdown__panel { opacity: 1; visibility: visible; transform: translateY(0); }
62
+
63
+ /* Item row — [icon] [main: label + desc] [badge] [tick] */
64
+ .ui-dropdown__item {
65
+ display: flex;
66
+ gap: 11px;
67
+ align-items: center;
68
+ padding: 9px 12px;
69
+ border-radius: var(--radius-sm);
70
+ cursor: pointer;
71
+ text-decoration: none;
72
+ color: var(--text);
73
+ transition: background 0.15s ease;
74
+ }
75
+ .ui-dropdown__item:hover { background: var(--surface); }
76
+ .ui-dropdown__item:focus-visible { outline: none; background: var(--surface); box-shadow: var(--ring); }
77
+ .ui-dropdown__main { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
78
+ .ui-dropdown__label { font-family: var(--font-sans); font-size: 12.5px; color: var(--strong); font-weight: 600; letter-spacing: 0.01em; }
79
+ .ui-dropdown__desc { font-size: 11px; color: var(--muted); line-height: 1.35; }
80
+ .ui-dropdown__ic { display: inline-flex; flex: none; color: var(--muted); }
81
+ .ui-dropdown__ic svg { width: 18px; height: 18px; stroke: currentColor; fill: none; stroke-width: 1.8; }
82
+
83
+ /* Trailing selected tick (listbox variant) */
84
+ .ui-dropdown__tick { margin-left: auto; flex: none; display: none; color: var(--accent); }
85
+ .ui-dropdown__tick svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 2.2; }
86
+ .ui-dropdown__item.is-selected .ui-dropdown__tick { display: inline-flex; }
87
+ .ui-dropdown__item.is-selected .ui-dropdown__label { color: var(--accent); }
88
+
89
+ /* When a row has both a badge and a tick, keep the badge from eating the margin */
90
+ .ui-dropdown__badge + .ui-dropdown__tick { margin-left: 8px; }
91
+
92
+ /* Trailing status badge */
93
+ .ui-dropdown__badge {
94
+ font-size: 9.5px;
95
+ letter-spacing: 0.12em;
96
+ text-transform: uppercase;
97
+ font-weight: 700;
98
+ padding: 2px 7px;
99
+ border-radius: var(--radius-pill);
100
+ margin-left: auto;
101
+ flex: none;
102
+ color: var(--muted);
103
+ background: var(--surface);
104
+ }
105
+ .ui-dropdown__badge.is-live { color: var(--green); background: var(--glow-green); }
106
+ .ui-dropdown__badge.is-accent { color: var(--accent); background: color-mix(in srgb, var(--accent) 14%, transparent); }
107
+
108
+ /* Disabled + danger rows */
109
+ .ui-dropdown__item.is-disabled { opacity: 0.45; cursor: not-allowed; pointer-events: none; }
110
+ .ui-dropdown__item.is-danger .ui-dropdown__label { color: var(--muted); }
111
+ .ui-dropdown__item.is-danger:hover .ui-dropdown__label { color: var(--accent); }
112
+
113
+ /* Grouped sections */
114
+ .ui-dropdown__section + .ui-dropdown__section { border-top: 1px solid var(--border); margin-top: 5px; padding-top: 5px; }
115
+ .ui-dropdown__group {
116
+ font-size: 10px;
117
+ letter-spacing: 0.1em;
118
+ text-transform: uppercase;
119
+ font-weight: 700;
120
+ color: var(--muted);
121
+ padding: 7px 12px 4px;
122
+ }
123
+
124
+ /* Separator */
125
+ .ui-dropdown__sep { height: 1px; background: var(--border); margin: 5px 4px; }
126
+
127
+ /* Optional pinned header/footer blocks */
128
+ .ui-dropdown__panel > .ui-dropdown__head {
129
+ padding: 11px 13px;
130
+ margin: -6px -6px 5px;
131
+ border-bottom: 1px solid var(--border);
132
+ border-radius: var(--radius-lg) var(--radius-lg) 0 0;
133
+ }