@kolkrabbi/kol-theme 0.13.6 → 0.18.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.
@@ -166,23 +166,17 @@
166
166
  max-width: 100%;
167
167
  }
168
168
 
169
+ /* POSITION ONLY. The control is a DS Button (quiet, icon-only) as of
170
+ * 2026-08-01 — it brings its own box, border, colour set and states, so this
171
+ * class stopped painting one. It used to hand-draw a bordered square around a
172
+ * literal `×` CHARACTER, which is why the button had no hover, no focus ring
173
+ * and no glyph. The inset is --kol-spacing-3, the rung that already carried
174
+ * this value. */
169
175
  .kol-overlay-close {
170
176
  position: absolute;
171
- top: 12px;
172
- right: 12px;
177
+ top: var(--kol-spacing-3);
178
+ right: var(--kol-spacing-3);
173
179
  z-index: 10;
174
- display: inline-flex;
175
- align-items: center;
176
- justify-content: center;
177
- width: 32px;
178
- height: 32px;
179
- border-radius: var(--kol-radius-sm);
180
- border: 1px solid var(--kol-oq-16);
181
- background: var(--kol-surface-primary);
182
- color: var(--kol-surface-on-primary);
183
- font-size: 16px;
184
- line-height: 1;
185
- cursor: pointer;
186
180
  }
187
181
 
188
182
  /* ═══════════════════════════════════════════════════════════════
@@ -894,6 +888,13 @@ a:hover .icon-hover,
894
888
  padding: 0;
895
889
  }
896
890
 
891
+ /* Radius (2026-07-30 ruling): either the system's 4px or a full round, nothing
892
+ * between. `sm` is the default and carries no class — it's on .kol-icon-frame
893
+ * above. `full` is a deliberate exception to the hard 4px repo invariant,
894
+ * because a round frame is its own chrome idiom (edge-straddling controls,
895
+ * avatars, status dots) rather than a radius tweak. */
896
+ .kol-icon-frame-radius-full { border-radius: 9999px; }
897
+
897
898
  /* pinned squares — the box never moves with glyph size (user law 2026-07-28) */
898
899
  .kol-icon-frame-sm { width: 28px; height: 28px; }
899
900
  .kol-icon-frame-md { width: 32px; height: 32px; }
@@ -73,7 +73,6 @@
73
73
  padding: 4px 16px;
74
74
  border-radius: var(--kol-radius-full);
75
75
  background-color: var(--kol-surface-on-primary);
76
- font-family: var(--kol-font-family-mono);
77
76
  color: var(--kol-surface-primary);
78
77
  border: none;
79
78
  }
@@ -84,7 +83,6 @@
84
83
  padding: 4px 16px;
85
84
  border-radius: var(--kol-radius-full);
86
85
  background-color: color-mix(in srgb, var(--kol-surface-on-primary) 16%, transparent);
87
- font-family: var(--kol-font-family-mono);
88
86
  color: var(--kol-surface-on-primary);
89
87
  border: 1px solid transparent;
90
88
  }
@@ -95,238 +93,102 @@
95
93
  padding: 4px 16px;
96
94
  border-radius: var(--kol-radius-full);
97
95
  background-color: var(--kol-surface-primary);
98
- font-family: var(--kol-font-family-mono);
99
96
  color: var(--kol-surface-on-primary);
100
97
  border: 1px solid var(--kol-border-default);
101
98
  }
102
99
 
103
- .pill-sm {
104
- font-size: 10px;
105
- font-weight: 470;
106
- padding: 2px 10px;
107
- }
108
-
109
- .pill-md {
110
- font-size: 12px;
111
- font-weight: 470;
112
- padding: 4px 16px;
113
- }
114
-
115
- .pill-lg {
116
- font-size: 14px;
117
- font-weight: 470;
118
- padding: 6px 20px;
119
- }
100
+ /* PADDING ONLY — type is kol-helper-*, applied in Pill.jsx. Same ruling as
101
+ * .kol-tag--* above (2026-08-01). */
102
+ .pill-sm { padding: 2px 10px; }
103
+ .pill-md { padding: 4px 16px; }
104
+ .pill-lg { padding: 6px 20px; }
120
105
 
121
106
 
122
107
  /* ─────────────────────────────────────────────────────────────────────
123
- * Tag — molecules/Tag.jsx
124
- * .tag-control / -sm / -md / -lg
125
- * .control-unified-inverse (inverse variant ad-hoc legacy naming)
108
+ * Tag — atoms/Tag.jsx. REBUILT ON PILL'S MODEL (user ruling 2026-08-01).
109
+ *
110
+ * Was: four class schemes for one component (.tag-control, .tag, .tag-naked,
111
+ * .tag-control-inverse), four declared variants, and exactly ONE :hover rule
112
+ * between them. Passing `color` swapped the base class off .tag-control and
113
+ * silently cost the chip its interaction state.
114
+ *
115
+ * Now: ONE scheme. `.kol-tag` is the box; `--primary|--secondary|--inverse` is
116
+ * the look, matching Pill and Button's vocabulary; `--sm|--md|--lg` is the
117
+ * scale. EVERY variant carries hover and active — a variant without a state is
118
+ * the defect this rewrite exists to remove, and `pnpm validate:chrome` fails
119
+ * any that lacks one.
126
120
  * ───────────────────────────────────────────────────────────────────── */
127
121
 
128
- .tag-control {
122
+ .kol-tag {
129
123
  display: inline-flex;
130
124
  align-items: center;
131
- padding: 4px 16px;
125
+ gap: 6px;
132
126
  border-radius: var(--kol-radius-full);
133
- background-color: var(--kol-surface-primary);
134
- font-size: 10px;
135
- font-weight: 400;
136
- font-family: var(--kol-font-family-mono);
137
- color: var(--kol-surface-on-primary);
138
- border: 1px solid var(--kol-border-default);
139
- transition: background-color var(--kol-transition-base),
140
- color var(--kol-transition-base);
127
+ border: 1px solid transparent;
128
+ transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
141
129
  }
142
130
 
143
- /* ─────────────────────────────────────────────────────────────────────
144
- * Dropdownmolecules/Dropdown.jsx (2026-07-08 chrome law).
145
- * The trigger IS button chrome: JSX emits kol-btn kol-btn-{primary|
146
- * outline} kol-btn-{size}, so fills/hover/active/focus come from the
147
- * button rules. Classes below only add trigger layout + the open/panel
148
- * fusion: primary = one piece (same fill, no border, no gap, hairline
149
- * divider inside); outline = bordered pair, trigger's bottom edge is
150
- * the divider.
151
- * ───────────────────────────────────────────────────────────────────── */
152
-
153
- .kol-dd-trigger {
154
- width: 100%;
155
- justify-content: space-between;
156
- }
131
+ /* PADDING ONLY. Type is the kol-helper-* ramp, applied at the call site
132
+ * (Tag.jsx)these rules used to hardcode font-size + font-weight and set no
133
+ * line-height, so a 10px chip inherited the body's ~1.5 and stood 22px tall
134
+ * beside 14px rows. Two owners of one concept is the drift this repo keeps
135
+ * paying for; the class owns the type, the rule owns the box. */
136
+ .kol-tag--sm { padding: 2px 10px; }
137
+ .kol-tag--md { padding: 4px 16px; }
138
+ .kol-tag--lg { padding: 6px 20px; }
157
139
 
158
- /* Dropdowns have NO hover and NO clicked state (2026-07-15 ruling) — the
159
- * trigger borrows Button chrome for geometry/fill but its only states are
160
- * rest and open. Scoped here so Button's own rules stay untouched. */
161
- .kol-dd-trigger.kol-btn-primary:not(.kol-btn-animate):hover,
162
- .kol-dd-trigger.kol-btn-primary:not(.kol-btn-animate):not(:disabled):active {
163
- background: var(--kol-surface-secondary);
164
- color: var(--kol-surface-on-primary);
165
- }
166
- .kol-dd-trigger.kol-btn-outline:not(.kol-btn-animate):hover,
167
- .kol-dd-trigger.kol-btn-outline:not(.kol-btn-animate):not(:disabled):active {
168
- background: transparent;
169
- border-color: var(--kol-oq-16);
140
+ /* primary the filled chip. Mirrors .pill-subtle's weight. */
141
+ .kol-tag--primary {
142
+ background-color: color-mix(in srgb, var(--kol-surface-on-primary) 16%, transparent);
170
143
  color: var(--kol-surface-on-primary);
171
144
  }
172
-
173
- .kol-dd-trigger--open {
174
- border-radius: var(--kol-radius-sm) var(--kol-radius-sm) 0 0;
145
+ .kol-tag--primary:hover {
146
+ background-color: color-mix(in srgb, var(--kol-surface-on-primary) 24%, transparent);
175
147
  }
176
-
177
- .kol-dd-panel {
178
- color: var(--kol-surface-on-primary);
179
- border-radius: 0 0 var(--kol-radius-sm) var(--kol-radius-sm);
180
- padding-bottom: 4px;
148
+ .kol-tag--primary.is-active {
149
+ background-color: color-mix(in srgb, var(--kol-surface-on-primary) 32%, transparent);
150
+ border-color: currentColor;
181
151
  }
182
152
 
183
- .kol-dd-panel--primary { background-color: var(--kol-surface-secondary); }
184
-
185
- .kol-dd-panel--outline {
153
+ /* secondary — the outlined chip. Mirrors .pill-outline. */
154
+ .kol-tag--secondary {
186
155
  background-color: var(--kol-surface-primary);
187
- border: 1px solid var(--kol-oq-16);
188
- border-top: none;
189
- }
190
-
191
- /* grey — the opaque grey chrome (oq-12, fill law:
192
- * interactive surfaces never translucent). Dropdown-only chrome; rest = hover
193
- * = active per the no-states dropdown ruling (no kol-btn-* class emitted, so
194
- * no button state rules ever apply). */
195
- .kol-dd-trigger--grey {
196
- background-color: var(--kol-oq-12);
197
156
  color: var(--kol-surface-on-primary);
198
- border: 1px solid transparent;
199
- }
200
-
201
- .kol-dd-panel--grey { background-color: var(--kol-oq-12); }
202
-
203
- .kol-dd-div {
204
- height: 1px;
205
- background-color: var(--kol-border-default);
206
- margin: 0 12px;
207
- }
208
-
209
- /* The option list — chrome, not consumer utilities (brief-2.0 defect 2: a
210
- * consumer without @source got an uncapped, unscrollable panel). */
211
- .kol-dd-list {
212
- display: flex;
213
- flex-direction: column;
214
- align-items: stretch;
215
- max-height: 300px;
216
- overflow-y: auto;
157
+ border-color: var(--kol-border-default);
217
158
  }
218
-
219
- /* The trigger chevron — pushed to the trailing edge, flips on open state.
220
- * Replaces the ml-auto utility + inline rotate style in Dropdown.jsx. */
221
- .kol-dd-caret {
222
- margin-left: auto;
223
- transition: transform 300ms;
159
+ .kol-tag--secondary:hover {
160
+ background-color: color-mix(in srgb, var(--kol-surface-on-primary) 08%, transparent);
224
161
  }
225
-
226
- .kol-dd-trigger[data-state="open"] .kol-dd-caret {
227
- transform: rotate(180deg);
162
+ .kol-tag--secondary.is-active {
163
+ background-color: color-mix(in srgb, var(--kol-surface-on-primary) 16%, transparent);
164
+ border-color: currentColor;
228
165
  }
229
166
 
230
- .tag-control:hover {
231
- background-color: var(--kol-oq-12);
232
- color: var(--kol-surface-on-primary);
233
- border-color: transparent;
167
+ /* inverse — the flipped chip. Mirrors .pill-inverse. */
168
+ .kol-tag--inverse {
169
+ background-color: var(--kol-surface-on-primary);
170
+ color: var(--kol-surface-primary);
234
171
  }
235
-
236
- .tag-control.is-active {
237
- background-color: var(--kol-oq-12);
238
- color: var(--kol-surface-on-primary);
239
- border-color: var(--kol-oq-12);
172
+ .kol-tag--inverse:hover {
173
+ background-color: color-mix(in srgb, var(--kol-surface-on-primary) 88%, transparent);
240
174
  }
241
-
242
- .tag-sm { font-size: 10px; font-weight: 470; padding: 4px 10px; }
243
- .tag-md { font-size: 12px; font-weight: 470; padding: 5px 12px; }
244
- .tag-lg { font-size: 14px; font-weight: 470; padding: 6px 14px; }
245
-
246
- /* ─────────────────────────────────────────────────────────────────────
247
- * Tag color family — molecules/Tag.jsx `color` prop (wave-4 vault parity,
248
- * 2026-07-30). Tag emits `tag tag--{color}` (chip) / `tag-naked
249
- * tag--{color}` (text-only); the classes shipped in the JSX but the RULES
250
- * never migrated out of the monorepo workshop — the same dangling trap as
251
- * the palette tokens themselves (kol-color.css, 2026-07-16) — so every
252
- * colored tag rendered dead. Re-authored from the monorepo colors spec
253
- * (02-design-system/01-colors.md § Tag Color Classes): light mode =
254
- * palette-*-light fill + dark ink; dark mode = saturated palette-* fill +
255
- * light ink (yellow keeps dark ink); `dark` = the on-primary/surface flip.
256
- * Inks are LITERALS (the surface poles): on a fixed palette fill a
257
- * theme-flipping token is wrong by definition.
258
- * ───────────────────────────────────────────────────────────────────── */
259
-
260
- .tag {
261
- display: inline-flex;
262
- align-items: center;
263
- gap: 6px;
264
- border-radius: var(--kol-radius-full);
265
- font-family: var(--kol-font-family-mono);
266
- border: 1px solid transparent;
267
- transition: background-color var(--kol-transition-base),
268
- color var(--kol-transition-base);
175
+ .kol-tag--inverse.is-active {
176
+ border-color: var(--kol-surface-primary);
269
177
  }
270
178
 
271
- /* naked — text-only: no fill, no chip padding (the reader-sidebar voice).
272
- * Sits AFTER .tag-{size} so its padding reset wins at equal specificity. */
273
- .tag-naked {
179
+ .kol-tag-dismiss {
274
180
  display: inline-flex;
275
181
  align-items: center;
276
- gap: 6px;
277
- padding: 0;
278
- background: transparent;
279
- border: none;
280
- font-family: var(--kol-font-family-mono);
182
+ cursor: pointer;
183
+ opacity: 0.64;
184
+ transition: opacity 0.15s ease;
281
185
  }
186
+ .kol-tag-dismiss:hover { opacity: 1; }
282
187
 
283
- /* Light mode (default): light fill + dark ink */
284
- .tag--blue { background-color: var(--kol-palette-blue-light); color: #121215; }
285
- .tag--teal { background-color: var(--kol-palette-teal-light); color: #121215; }
286
- .tag--green { background-color: var(--kol-palette-green-light); color: #121215; }
287
- .tag--yellow { background-color: var(--kol-palette-yellow-light); color: #121215; }
288
- .tag--red { background-color: var(--kol-palette-red-light); color: #121215; }
289
- .tag--orange { background-color: var(--kol-palette-orange-light); color: #121215; }
290
- .tag--purple { background-color: var(--kol-palette-purple-light); color: #121215; }
291
- .tag--dark { background-color: var(--kol-surface-on-primary); color: var(--kol-surface-primary); }
292
-
293
- /* naked + color = saturated text, both modes (no fill to carry the pair) */
294
- .tag-naked.tag--blue { background: transparent; color: var(--kol-palette-blue); }
295
- .tag-naked.tag--teal { background: transparent; color: var(--kol-palette-teal); }
296
- .tag-naked.tag--green { background: transparent; color: var(--kol-palette-green); }
297
- .tag-naked.tag--yellow { background: transparent; color: var(--kol-palette-yellow); }
298
- .tag-naked.tag--red { background: transparent; color: var(--kol-palette-red); }
299
- .tag-naked.tag--orange { background: transparent; color: var(--kol-palette-orange); }
300
- .tag-naked.tag--purple { background: transparent; color: var(--kol-palette-purple); }
301
- .tag-naked.tag--dark { background: transparent; color: var(--kol-surface-on-primary); }
302
-
303
- /* active (Tag.jsx emits .tag--active when active + colored) */
304
- .tag--active { border-color: currentColor; }
305
-
306
- /* Dark mode: saturated fill + light ink (yellow keeps dark ink). Scoped to
307
- * .tag.tag--{color} so the chip flips while .tag-naked (equal specificity,
308
- * earlier in the sheet) keeps its saturated TEXT — a bare .tag--{color}
309
- * override here would out-order the naked rules and give text tags a fill.
310
- * MIRROR of the system-follow block below — edit both. */
311
- :is([data-theme="dark"], .dark) .tag.tag--blue { background-color: var(--kol-palette-blue); color: #fafafa; }
312
- :is([data-theme="dark"], .dark) .tag.tag--teal { background-color: var(--kol-palette-teal); color: #fafafa; }
313
- :is([data-theme="dark"], .dark) .tag.tag--green { background-color: var(--kol-palette-green); color: #fafafa; }
314
- :is([data-theme="dark"], .dark) .tag.tag--yellow { background-color: var(--kol-palette-yellow); color: #121215; }
315
- :is([data-theme="dark"], .dark) .tag.tag--red { background-color: var(--kol-palette-red); color: #fafafa; }
316
- :is([data-theme="dark"], .dark) .tag.tag--orange { background-color: var(--kol-palette-orange); color: #fafafa; }
317
- :is([data-theme="dark"], .dark) .tag.tag--purple { background-color: var(--kol-palette-purple); color: #fafafa; }
318
-
319
- /* System-follow dark — only when no explicit theme is stamped (explicit >
320
- * system > light, USER law). MIRROR of the block above — edit both. */
321
- @media (prefers-color-scheme: dark) {
322
- :root:not([data-theme]) .tag.tag--blue { background-color: var(--kol-palette-blue); color: #fafafa; }
323
- :root:not([data-theme]) .tag.tag--teal { background-color: var(--kol-palette-teal); color: #fafafa; }
324
- :root:not([data-theme]) .tag.tag--green { background-color: var(--kol-palette-green); color: #fafafa; }
325
- :root:not([data-theme]) .tag.tag--yellow { background-color: var(--kol-palette-yellow); color: #121215; }
326
- :root:not([data-theme]) .tag.tag--red { background-color: var(--kol-palette-red); color: #fafafa; }
327
- :root:not([data-theme]) .tag.tag--orange { background-color: var(--kol-palette-orange); color: #fafafa; }
328
- :root:not([data-theme]) .tag.tag--purple { background-color: var(--kol-palette-purple); color: #fafafa; }
329
- }
188
+ /* The per-colour tag palette was DELETED 2026-08-01 with the `color` prop.
189
+ * A chip's look is its variant now (kol-tag--primary|secondary|inverse), the
190
+ * same vocabulary Pill and Button use. Tag colour BY TAXONOMY returns later
191
+ * as its own decision, layered ON the variants rather than replacing them. */
330
192
 
331
193
  .control-unified-inverse {
332
194
  display: inline-flex;
@@ -608,3 +470,47 @@
608
470
  background: color-mix(in srgb, var(--kol-surface-on-primary) 8%, transparent);
609
471
  color: var(--kol-surface-on-primary);
610
472
  }
473
+
474
+ /* ─────────────────────────────────────────────────────────────────────
475
+ * OVERLAY CHROME — the ONE floating surface (user ruling 2026-08-01).
476
+ *
477
+ * There was none. `.kol-modal` is an unstyled hook, and ShellSearchOverlay
478
+ * hand-wrote its own look at the call site: `rounded-[var(--kol-radius-2xl)]`,
479
+ * `shadow-[0_20px_60px_rgba(0,0,0,0.4)]`, `bg-black/60`, `backdrop-blur-[1px]`
480
+ * — four arbitrary values, none of them referenced anywhere else. The 20px
481
+ * radius in particular appears NOWHERE else in this repo's chrome (which uses
482
+ * `sm` and `full`, counted), which is exactly why it read as foreign.
483
+ *
484
+ * TagModeOverlay meanwhile rendered as a page-like <article> with no overlay
485
+ * chrome at all — one product, two floating surfaces, nothing in common.
486
+ *
487
+ * Both wear these two classes now. `validate:chrome` C3 fails any arbitrary
488
+ * rounded-[…] / shadow-[…] / bg-black/NN / backdrop-blur-[…] in package chrome.
489
+ * ───────────────────────────────────────────────────────────────────── */
490
+
491
+ /* LOOK ONLY — the call site owns the box. Three scrims exist and two are
492
+ * `fixed inset-0` while one is `absolute inset-0`; baking position in would
493
+ * have forced a wrong one on two of them. The class owns what was drifting:
494
+ * the tint (60 / 50 / 50 across three files) and the blur. */
495
+ .kol-overlay-scrim {
496
+ background-color: color-mix(in srgb, #000 60%, transparent);
497
+ backdrop-filter: blur(1px);
498
+ }
499
+
500
+ .kol-overlay-panel {
501
+ position: relative;
502
+ width: 100%;
503
+ overflow: hidden;
504
+ background-color: var(--kol-surface-primary);
505
+ /* NO BORDER, NO SHADOW (user 2026-08-01). Both were tried and both were
506
+ * wrong: the shadow was another product's floating-card idiom, and the
507
+ * hairline left an empty palette reading as a bordered empty box. The SCRIM
508
+ * is the separation — a 60% tint plus blur is already a stronger edge than
509
+ * an 8%-opacity line. */
510
+ /* `sm` — the radius this repo's chrome actually uses. */
511
+ border-radius: var(--kol-radius-sm);
512
+ /* NO SHADOW (user ruling 2026-08-01). The border and the scrim already
513
+ * separate the panel; a drop-shadow on top is the floating-card idiom from
514
+ * another product. --kol-shadow-overlay was added for this an hour earlier
515
+ * and is deleted with it: it solved the wrong problem. */
516
+ }
@@ -175,13 +175,22 @@
175
175
  display: inline-flex;
176
176
  align-items: center;
177
177
  padding: 2px 4px;
178
- margin: 0 4px;
178
+ /* No outer margin: spacing is the CONTAINER's job. The margin here
179
+ * double-spaced against every flex row's own gap, which is the ragged
180
+ * rhythm the metadata rows showed. */
179
181
  border-radius: 2px;
180
182
  font-family: var(--kol-font-family-mono);
181
183
  font-size: 0.75rem;
182
184
  font-weight: 400;
183
185
  line-height: 1;
184
186
  white-space: nowrap;
187
+ /* Carries its OWN fill as of 2026-08-01. The look lived only in the
188
+ * `.kol-table code` descendant rule below, so a token chip rendered right
189
+ * inside a table and completely unstyled anywhere else — which is why the
190
+ * component pages hand-rolled a Tailwind lookalike. One concept, two
191
+ * spellings: exactly the drift `.text-fg-*` vs `--kol-fg-*` taught. */
192
+ color: var(--kol-fg-80);
193
+ background-color: var(--kol-fg-08);
185
194
  }
186
195
 
187
196
  .kol-table-wrapper.kol-table--simple { border: none; border-radius: 0; }
@@ -425,3 +434,125 @@
425
434
  object-fit: cover;
426
435
  object-position: center;
427
436
  }
437
+
438
+ /* ═══════════════════════════════════════════════════════════════
439
+ * MediaLibrary — organisms/MediaLibrary.jsx
440
+ * The picker card, the tile grid, the tree rows and the path bar.
441
+ * The card width and tile floor were inline literals in the four consumer
442
+ * forks (kol-ds-fxr MediaPicker.jsx:214 and :274) — they are tokens here
443
+ * because geometry is referenced, never re-guessed per consumer.
444
+ * NOT here: the scrim, the centring, Esc/backdrop dismiss and the corner
445
+ * close — FullscreenOverlay (.kol-overlay) already owns all of it.
446
+ * NO elevation. A drop shadow under the modal was smuggled in 2026-08-01
447
+ * and struck out the same day (user ruling) — the scrim is the separation.
448
+ * ═══════════════════════════════════════════════════════════════ */
449
+
450
+ .kol-media-picker {
451
+ /* The card's resting width. Was 45rem and the ContentFilters header could
452
+ * not fit in it: "10 of 10" wrapped to three lines, the view toggle clipped,
453
+ * and the close button collided with it. The header is the constraint, not
454
+ * the grid. It still shrinks inside .kol-overlay's padding. */
455
+ --kol-media-picker-w: 64rem;
456
+ /* the grid's tile floor — auto-fill packs as many as clear this */
457
+ --kol-media-tile-min: 10rem;
458
+
459
+ display: flex;
460
+ flex-direction: column;
461
+ width: var(--kol-media-picker-w);
462
+ max-width: 100%;
463
+ max-height: 85vh;
464
+ /* min-height:0 + the body owning the scroll — the card itself scrolling was
465
+ * what dragged the sticky header out of view. */
466
+ min-height: 0;
467
+ overflow: hidden;
468
+ /* Extra block-start: FullscreenOverlay's close button is absolutely placed
469
+ * on the SHEET corner, so at even padding it landed on top of the view
470
+ * toggle. The card reserves the lane instead of the close moving. */
471
+ padding: var(--kol-spacing-12) var(--kol-spacing-5) var(--kol-spacing-5);
472
+ border: 1px solid var(--kol-oq-08);
473
+ border-radius: var(--kol-radius-sm);
474
+ background: var(--kol-surface-primary);
475
+ }
476
+
477
+ /* The browser is the same body with no card around it — it fills whatever
478
+ * box the consumer gives it, so a page can host it full-bleed. */
479
+ .kol-media-browser {
480
+ display: flex;
481
+ flex-direction: column;
482
+ width: 100%;
483
+ height: 100%;
484
+ --kol-media-tile-min: 10rem;
485
+ }
486
+
487
+ .kol-media-grid {
488
+ display: grid;
489
+ gap: var(--kol-spacing-4);
490
+ grid-template-columns: repeat(auto-fill, minmax(var(--kol-media-tile-min), 1fr));
491
+ list-style: none;
492
+ margin: 0;
493
+ padding: 0;
494
+ }
495
+
496
+ .kol-media-list {
497
+ display: flex;
498
+ flex-direction: column;
499
+ list-style: none;
500
+ margin: 0;
501
+ padding: 0;
502
+ }
503
+
504
+ /* One folder row in the tree. Finder's list model: the chevron discloses in
505
+ * place, so the parent never leaves the screen and there is no breadcrumb
506
+ * stacked above a divider. Same row idiom as the rails — a drill-down and a
507
+ * nav row are the same gesture. */
508
+ .kol-media-folder {
509
+ display: flex;
510
+ align-items: center;
511
+ gap: var(--kol-spacing-2);
512
+ padding-block: var(--kol-pad-rail-row-y);
513
+ border-radius: var(--kol-radius-sm);
514
+ cursor: pointer;
515
+ transition: background-color var(--kol-transition-fast);
516
+ }
517
+
518
+ .kol-media-folder:hover {
519
+ background: var(--kol-oq-04);
520
+ }
521
+
522
+ /* The path bar rides the FOOT, where Finder puts it. */
523
+ .kol-media-pathbar {
524
+ display: flex;
525
+ align-items: center;
526
+ gap: var(--kol-spacing-1);
527
+ margin-block-start: auto;
528
+ padding-block-start: var(--kol-spacing-3);
529
+ border-top: 1px solid var(--kol-oq-08);
530
+ }
531
+
532
+ /* The BODY scrolls, never the card — a scrolling card dragged the header out
533
+ * of view, which is what made the picker feel broken. */
534
+ .kol-media-scroll {
535
+ flex: 1;
536
+ min-height: 0;
537
+ overflow-y: auto;
538
+ }
539
+
540
+ /* Sortable header — a real button inside the <th> so it is keyboard-reachable.
541
+ * The glyph is always in the layout box (opacity, not display) so the header
542
+ * does not reflow on first sort. */
543
+ .kol-table-sort {
544
+ display: inline-flex;
545
+ align-items: center;
546
+ gap: var(--kol-spacing-1);
547
+ font: inherit;
548
+ color: inherit;
549
+ background: none;
550
+ border: none;
551
+ padding: 0;
552
+ cursor: pointer;
553
+ }
554
+
555
+ .kol-table-sort:hover { color: var(--kol-surface-on-primary); }
556
+ .kol-table-sort svg { opacity: 0.25; transition: opacity var(--kol-transition-fast); }
557
+ .kol-table-sort:hover svg { opacity: 0.6; }
558
+ [aria-sort] .kol-table-sort svg { opacity: 1; }
@@ -93,33 +93,64 @@
93
93
  display: none;
94
94
  }
95
95
 
96
- /* Sidebar toggle buttons (collapsible section headers) layout only.
97
- * Type/color carried in JSX (kol-helper-10 text-meta). */
98
- /* Vertical rhythm matches .shell-nav-group-header (6px rows) ledger-2.0
99
- * #2.6 ruling (final, user-reviewed 2026-07-15): the rail keeps its quieter
100
- * 10px label scale (deliberate hierarchy vs the nav's 14px), row rhythm is
101
- * shared. */
102
- .shell-sidebar-toggle {
96
+ /* THE EYEBROW ROW the ONE owner of a rail section header's box (user ruling
97
+ * 2026-08-01). Left and right rails render the same row and composed it
98
+ * differently, so they drifted: the left stacked `.shell-sidebar-toggle` +
99
+ * `.shell-sidebar-label` on the wrapper AND `.shell-sidebar-label` again on
100
+ * the inner label two margin-owners on one row plus an inline height on
101
+ * the chevron; the right borrowed `.shell-nav-group-header` (the nav row, a
102
+ * different scale) with one `.shell-sidebar-label`. Same eyebrow, two boxes,
103
+ * no version difference between them: the failure ARCHITECTURE §5 records for
104
+ * cascade layers, pointed at spacing.
105
+ *
106
+ * The rule: an eyebrow row wears ONE box class and NOTHING else beside
107
+ * kol-doc-eyebrow. Two names, by role, sharing one definition so they cannot
108
+ * drift from each other either:
109
+ *
110
+ * .shell-sidebar-toggle the eyebrow that collapses its section
111
+ * .shell-sidebar-label the eyebrow that is just a label
112
+ *
113
+ * Both own padding AND margin. Do not stack them, do not set y-spacing inline,
114
+ * do not repeat a spacing class on a child. Type/colour come from
115
+ * kol-doc-eyebrow. `pnpm validate:rails` R3 enforces it.
116
+ *
117
+ * Vertical rhythm matches .shell-nav-group-header via --kol-pad-rail-row-y —
118
+ * ledger-2.0 #2.6 ruling (final, user-reviewed 2026-07-15): the rail keeps its
119
+ * quieter label scale (deliberate hierarchy vs the nav's), row rhythm is
120
+ * shared. It is shared by a token now, not by copies of a number. */
121
+ .shell-sidebar-toggle,
122
+ .shell-sidebar-label {
103
123
  display: flex;
104
124
  align-items: center;
105
- gap: 6px;
106
- cursor: pointer;
125
+ justify-content: space-between;
126
+ gap: var(--kol-pad-rail-row-y);
107
127
  width: 100%;
128
+ text-align: left;
108
129
  border: none;
109
130
  background: transparent;
110
- padding: 6px 0;
131
+ padding: var(--kol-pad-rail-row-y) 0;
132
+ margin-bottom: var(--kol-spacing-2);
111
133
  transition: color 0.15s ease;
112
134
  }
113
135
 
114
- .shell-sidebar-toggle:hover {
115
- color: color-mix(in srgb, var(--kol-surface-on-primary) 72%, transparent);
136
+ /* Interactivity is a property of the element, not a second class at the call
137
+ * site a static eyebrow and a clickable one must not have to remember a
138
+ * different class pair to get the same box. */
139
+ .shell-sidebar-toggle,
140
+ .shell-sidebar-label:is(button, a, [role='button']) {
141
+ cursor: pointer;
116
142
  }
117
143
 
118
- /* Sidebar section labels — margin only; type/color in JSX. */
119
- .shell-sidebar-label {
120
- margin-bottom: 8px;
144
+ .shell-sidebar-toggle:hover,
145
+ .shell-sidebar-label:is(button, a, [role='button']):hover {
146
+ color: color-mix(in srgb, var(--kol-surface-on-primary) 72%, transparent);
121
147
  }
122
148
 
149
+ /* NB — the old `.shell-sidebar-label { margin-bottom }` rule was DELETED here
150
+ * (2026-08-01). It was the second box owner: it re-declared the eyebrow's
151
+ * bottom margin below the shared rule above, so an element wearing both
152
+ * classes got its spacing decided by source order. The shared rule owns it. */
153
+
123
154
  /* Sidebar links — layout only; type/color (+ active) in JSX. */
124
155
  .shell-sidebar-link {
125
156
  padding: 4px 0;
@@ -176,9 +207,29 @@
176
207
  margin-bottom: 12px;
177
208
  }
178
209
 
179
- /* Navigation group header layout only; type/color in JSX. */
210
+ /* THE CHAPTER ROW (the rail's names, user 2026-08-01):
211
+ *
212
+ * CATEGORY the eyebrow .shell-sidebar-toggle / -label
213
+ * CHAPTER `Foundations (5)` .shell-nav-group-header ← this
214
+ * PAGE `Tokens` .shell-nav-item
215
+ * SECTION the right rail's rows (same PAGE row idiom)
216
+ *
217
+ * CHAPTER and PAGE were indistinguishable: both `kol-mono-14`, and the only
218
+ * difference was `.text-body` — which is a COLOUR utility, not type. A parent
219
+ * and its children reading identically is not a hierarchy.
220
+ *
221
+ * The weight is the difference, and it stays INSIDE the one ramp: 500 here
222
+ * against kol-mono-14's own 400 on the page row. Not a second type class —
223
+ * R1 exists to stop a second RAMP in the rails, and a weight within one ramp
224
+ * is not one.
225
+ *
226
+ * Layout: same y-rhythm as the eyebrow (--kol-pad-rail-row-y), no bottom
227
+ * margin because .shell-nav-items owns the gap under it. A CATEGORY must never
228
+ * borrow this class — that is how the right rail drifted; use
229
+ * .shell-sidebar-toggle. */
180
230
  .shell-nav-group-header {
181
- padding: 6px 0 6px 0;
231
+ font-weight: 500;
232
+ padding: var(--kol-pad-rail-row-y) 0;
182
233
  cursor: pointer;
183
234
  display: flex;
184
235
  justify-content: space-between;
@@ -190,24 +241,66 @@
190
241
  color: color-mix(in srgb, var(--kol-surface-on-primary) 90%, transparent);
191
242
  }
192
243
 
244
+ /* THE ROW CONTAINER — owns the gap so the two rails cannot disagree. They
245
+ * did: `space-y-0` on one side, `space-y-4` on the other, for the same list. */
193
246
  .shell-nav-items {
194
247
  display: flex;
195
248
  flex-direction: column;
196
- gap: 4px;
249
+ gap: 0;
197
250
  margin-top: 4px;
198
251
  }
199
252
 
200
253
  /* Navigation items — layout only; type/color (+ active) in JSX. */
254
+ /* THE PAGE ROW — L3, the class owns the whole look (user ruling 2026-08-01).
255
+ * PAGE in the rail's vocabulary (`Tokens`); the right rail's rows are SECTIONS
256
+ * and wear this same idiom deliberately. Weight stays the ramp's own 400 — the
257
+ * CHAPTER above it carries 500, and that difference IS the hierarchy.
258
+ *
259
+ * `.shell-nav-item` was the shared name and nothing else: every call site
260
+ * hand-wrote its own utility stack around it, so ONE row idiom shipped as NINE
261
+ * strings across five files — `block kol-mono-14 text-body transition-colors
262
+ * focus-visible:ring-focus hover:text-emphasis` in one rail, bare
263
+ * `kol-mono-14` in the other, `flex items-center gap-2 …` for actions. R1/R2
264
+ * locked the TYPE class and nothing locked the stack: the eyebrow-box failure,
265
+ * one rung down.
266
+ *
267
+ * Layout, colour, hover, transition and focus live HERE now. Markup comes from
268
+ * `RailRow`; `validate:rails` R4 fails a hand-written `shell-nav-item`. */
201
269
  .shell-nav-item {
270
+ display: flex;
271
+ align-items: center;
272
+ gap: 8px;
273
+ width: 100%;
202
274
  padding: 4px 12px 4px 20px;
275
+ border: none;
276
+ background: transparent;
277
+ text-align: left;
203
278
  border-radius: 0px;
204
279
  text-decoration: none;
205
- transition: all 0.15s ease;
206
- display: block;
280
+ color: var(--kol-fg-64);
281
+ cursor: pointer;
282
+ transition: color 0.15s ease, background-color 0.15s ease;
207
283
  }
208
284
 
209
285
  .shell-nav-item:hover {
210
- color: color-mix(in srgb, var(--kol-surface-on-primary) 100%, transparent);
286
+ color: var(--kol-surface-on-primary);
287
+ }
288
+
289
+ .shell-nav-item:focus-visible {
290
+ outline: 1px solid var(--kol-fg-32);
291
+ outline-offset: -1px;
292
+ }
293
+
294
+ /* The active row — one definition, not `text-emphasis` typed at each site. */
295
+ .shell-nav-item.is-active {
296
+ color: var(--kol-surface-on-primary);
297
+ }
298
+
299
+ /* The trailing slot: counts, doc numbers, anything pushed right. */
300
+ .shell-nav-item-trailing {
301
+ margin-left: auto;
302
+ flex-shrink: 0;
303
+ color: var(--kol-fg-48);
211
304
  }
212
305
 
213
306
  .shell-nav-item-doc {
package/kol-theme.css CHANGED
@@ -72,6 +72,15 @@
72
72
  --kol-spacing-gap-md: var(--kol-spacing-4);
73
73
  --kol-spacing-gap-lg: var(--kol-spacing-6);
74
74
 
75
+ /* RAIL ROW rhythm (2026-08-01) — the y-padding every sidebar row shares:
76
+ * the eyebrow (.shell-sidebar-toggle) and the nav group header
77
+ * (.shell-nav-group-header). Deliberately OFF the spacing scale — a tighter
78
+ * chrome rhythm than --kol-spacing-1, and both rules already carried the
79
+ * same literal in two places. Named here so the left and right rails cannot
80
+ * disagree about it again; the row's bottom separation is --kol-spacing-2
81
+ * and needs no alias of its own. */
82
+ --kol-pad-rail-row-y: 0.375rem;
83
+
75
84
  /* Content widths (2026-07-28; panel added 2026-07-30) — ONE frame, three
76
85
  * inner caps (chess law). Every page: mx-auto max-w-shell + the one padding
77
86
  * rhythm, content LEFT-ANCHORED inside. Width is a content decision, never a
@@ -81,7 +90,16 @@
81
90
  * text. Nothing else — a hardcoded max-w-[Npx] at a call site means this
82
91
  * scale failed; file it, don't improvise it.
83
92
  * kol-framework's --kol-container-max is the responsive RESOLUTION of the
84
- * shell (100% → 1400 → 1600 → shell), not a rival family. */
93
+ * shell (100% → 1400 → 1600 → shell), not a rival family.
94
+ *
95
+ * canvas (2026-07-31, user ruling) — the rung between shell and panel: the
96
+ * BODY of a page inside the shell's main column, i.e. item fields (swatch
97
+ * grids, the icon wall, card galleries) that panel would squeeze and the
98
+ * frame does not bind. Authored in rem because it is a max-width, not a
99
+ * device measure: 87.5rem = 1400 at the default root size. Flat at every
100
+ * breakpoint — a max-width only bites once there is room for it, and it
101
+ * nests: a table inside it still stops at panel, a paragraph at measure. */
102
+ --kol-content-canvas: 87.5rem;
85
103
  --kol-content-shell: 1800px;
86
104
  --kol-content-panel: 960px;
87
105
  --kol-content-column: 768px;
@@ -89,6 +107,11 @@
89
107
 
90
108
  /* Border radius (4px increments) */
91
109
  --kol-radius-none: 0;
110
+ /* xs (2026-08-01) — the hairline rung. Claimed out loud: 2px had SIX
111
+ hand-written `rounded-[2px]` call sites (swatches, colour rows, the
112
+ glass panel) and no token, so the scale's 4px floor was simply being
113
+ ignored rather than followed. A value used six times is a rung. */
114
+ --kol-radius-xs: 2px;
92
115
  --kol-radius-sm: 4px;
93
116
  --kol-radius-md: 8px;
94
117
  --kol-radius-lg: 12px;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolkrabbi/kol-theme",
3
- "version": "0.13.6",
3
+ "version": "0.18.0",
4
4
  "description": "KOL (Kolkrabbi) design-system tokens + base CSS — brand-neutral. The canonical token/cascade layer every other KOL package and consumer builds on.",
5
5
  "license": "MIT",
6
6
  "type": "module",