@kolkrabbi/kol-theme 0.43.1 → 0.45.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.
@@ -307,7 +307,7 @@
307
307
  * navigation is location, not a toggled tool. */
308
308
  .kol-btn-nav {
309
309
  background: transparent;
310
- color: var(--kol-oq-64);
310
+ color: var(--kol-oq-80);
311
311
  border: 1px solid transparent;
312
312
  }
313
313
  @media (hover: hover) {
@@ -329,6 +329,8 @@
329
329
  * Button variant on the opaque tier: rest oq-12, hover/active one stop past
330
330
  * each, same state math as the rest of the family. */
331
331
  .kol-btn-grey {
332
+ /* Ladder (user ruling 2026-08-15): 12 rest → 04 hover (darker) → 32 press,
333
+ * then eased back down. */
332
334
  background: var(--kol-oq-12);
333
335
  color: var(--kol-surface-on-primary);
334
336
  border: 1px solid transparent;
@@ -804,95 +806,117 @@ a:hover .icon-hover,
804
806
  display: inline-flex;
805
807
  align-items: center;
806
808
  justify-content: center;
807
- /* PINNED SQUARE — the box never moves with glyph size (user law 2026-07-28,
808
- * the same rule .kol-icon-frame-{sm,md,lg} obeys at 28/32/36). This class
809
- * derived its size from `padding: 0.5rem` + whatever glyph it was handed, so
810
- * a 14px icon silently produced a 30px control off the ladder. 32px is md on
811
- * both the icon-frame squares and the button ladder (26/32/40), and it is
812
- * exactly what 0.5rem + a 16px glyph already measured — CodeBlock's button
813
- * does not move. */
814
- width: 32px;
815
- height: 32px;
816
- padding: 0;
817
- /* rungs below; the base stays md so a class used without one still lands
818
- * on the ladder rather than collapsing to its content */
809
+ padding: 0.5rem;
819
810
  background: color-mix(in srgb, var(--kol-surface-on-primary) 2%, transparent);
820
811
  border: none;
821
812
  border-radius: var(--kol-radius-sm);
822
- /* ink on the OPAQUE scale (user ruling 2026-08-15): a 72%-transparent glyph
823
- * over a photograph lets the image through the strokes and reads as mush.
824
- * oq is a baked grey — same value, nothing bleeds through. */
825
- color: var(--kol-oq-72);
813
+ color: color-mix(in srgb, var(--kol-surface-on-primary) 72%, transparent);
826
814
  cursor: pointer;
827
815
  transition: background-color var(--kol-transition-base), color var(--kol-transition-base);
828
816
  }
829
817
 
830
818
  .kol-copy-btn:hover {
831
819
  background: color-mix(in srgb, var(--kol-surface-on-primary) 8%, transparent);
832
- /* FULL ink on hover — no mix, no stop below 100% */
833
820
  color: var(--kol-surface-on-primary);
834
821
  }
835
822
 
836
- /* the click step, missing since this class was written — hover had a stop,
837
- * pressed had none, so the button gave no feedback on the way down. */
838
- .kol-copy-btn:active {
839
- background: color-mix(in srgb, var(--kol-surface-on-primary) 12%, transparent);
840
- color: var(--kol-surface-on-primary);
841
- }
823
+ /* ─────────────────────────────────────────────────────────────────────
824
+ * TWO SEPARATE CONTROLS (user ruling 2026-08-15). They shared .kol-copy-btn
825
+ * as a base and every edit to one moved the other; the split is the point.
826
+ * ───────────────────────────────────────────────────────────────────── */
842
827
 
843
- /* --grey's ladder runs DOWN, not up (user ruling 2026-08-15). oq mixes ink
844
- * INTO the surface, so a higher stop is a LIGHTER box the wrong direction for
845
- * a control that has to hold its own against a bright image. Rest is oq-12,
846
- * hover steps darker to oq-08, and the click lands on --kol-surface-primary
847
- * itself: fully opaque, no mix, the strongest the scale goes. */
848
- .kol-copy-btn--grey:hover {
849
- background: var(--kol-oq-08);
850
- color: var(--kol-surface-on-primary);
851
- }
852
- .kol-copy-btn--grey:active {
853
- background: var(--kol-surface-primary);
828
+ /* over MEDIA boxed, opaque plate, full ink. Sits on a photograph, so it
829
+ * needs its own fill to stay legible and never dims at rest. */
830
+ .kol-media-control {
831
+ display: inline-flex;
832
+ align-items: center;
833
+ justify-content: center;
834
+ width: 32px;
835
+ height: 32px;
836
+ padding: 0;
837
+ border: none;
838
+ border-radius: var(--kol-radius-sm);
839
+ background: var(--kol-oq-12);
854
840
  color: var(--kol-surface-on-primary);
855
- }
856
-
857
- /* --grey the same control, over MEDIA instead of over a surface
858
- * (user ruling 2026-08-15). The base rule and every fg-scale variant are
859
- * TRANSLUCENT, so they are content-dependent: a light wash vanishes on a light
860
- * photo, a dark scrim vanishes on a dark one. There is no stop that survives
861
- * both, because the problem is the transparency, not the value.
862
- *
863
- * `oq` is the answer and kol-opaque.css:11 already says so "use oq when you
864
- * need an OPAQUE grey (cover layer, CHIP OVER A PHOTO, any fill that must hide
865
- * what's behind it)". It mixes into the surface instead of into transparent, so
866
- * the control reads identically whatever the image does.
867
- *
868
- * The 12 / 16 / 24 ladder is not new — it is exactly .kol-btn-grey's, and
869
- * `grey` is already this system's name for an oq fill (kol-btn-grey,
870
- * kol-dd-trigger--grey, kol-icon-frame-grey). Same name, same stops. */
871
- /* the same three squares as .kol-icon-frame-{sm,md,lg}. The SOLO glyph ladder
872
- * (16/20/24) pairs 1:1 with them and ActionButton resolves it the pairing is
873
- * never re-typed at a call site. */
874
- /* bare no square, no fill, no padding; the glyph and its ink only. The host
875
- * owns the frame (CodeBlock / SearchInput / ThemeToggle sense of the word).
876
- * Hover and active still move the INK, so the control is not stateless. */
877
- .kol-copy-btn--bare {
878
- width: auto;
879
- height: auto;
841
+ cursor: pointer;
842
+ /* the house curve, not --kol-transition-base's generic material ease: it
843
+ * settles instead of stopping, which is what reads as smooth on the press. */
844
+ transition: background-color 1500ms var(--kol-ease-bounce), color 1500ms var(--kol-ease-bounce);
845
+ }
846
+ /* hover goes LIGHTER, not darker (user ruling 2026-08-15) oq mixes ink into
847
+ * the surface, so up the scale is up in brightness. */
848
+ /* the brand anchor, with the theme's own palette as the fallback — the yellow
849
+ * lives in kol-brand-color.css which the consumer supplies, so the theme must
850
+ * not hard-depend on it. NOT --kol-accent-primary, which resolves to white. */
851
+ .kol-media-control:hover {
852
+ background: var(--kol-surface-tertiary);
853
+ color: var(--kol-color-yellow-300, var(--kol-palette-yellow));
854
+ }
855
+ /* The press RAMPS. The rule that is being entered owns the curve going in, so
856
+ * this one carries a slow ease-in-out; the base rule's bounce owns coming back
857
+ * out. One bezier cannot do both the bounce covers most of its distance in
858
+ * the first quarter, which is exactly why the press read as instant. */
859
+ /* the glyph GROWS on interaction: 18 at rest, 20 on hover and press, inside the
860
+ * unchanged 32px box. The SOLO md rung (20) stays the base and rest scales DOWN
861
+ * to it 20 × 0.9 = 18 so the ladder value is still what the component asks
862
+ * for. Scale, not a size swap: the box never moves (2026-07-28 law) and an SVG
863
+ * re-render would not tween. */
864
+ .kol-media-control .kol-action-glyph {
865
+ transform: scale(0.9);
866
+ transition: transform 500ms var(--kol-ease-house);
867
+ }
868
+ .kol-media-control:hover .kol-action-glyph,
869
+ .kol-media-control--pressed .kol-action-glyph {
870
+ transform: scale(1);
871
+ }
872
+
873
+ .kol-media-control:active,
874
+ .kol-media-control--pressed {
875
+ background: var(--kol-oq-32);
876
+ transition: background-color 1500ms cubic-bezier(0.65, 0, 0.35, 1);
877
+ }
878
+
879
+ /* in TEXT — bare, no box, no fill. Rests on the SAME ink as the copy beside it
880
+ * (--kol-fg-meta = fg-48): these sit on a solid plate, not on a photograph, so
881
+ * the transparent scale is safe here and matching the text is worth more than
882
+ * the opaque rung. `--on` is a toggle's held state, not a hover. */
883
+ .kol-inline-control {
884
+ display: inline-flex;
885
+ align-items: center;
886
+ justify-content: center;
887
+ width: 16px;
888
+ height: 16px;
880
889
  padding: 0;
890
+ border: none;
881
891
  background: transparent;
892
+ color: var(--kol-oq-64);
893
+ cursor: pointer;
894
+ transition: color var(--kol-transition-base);
882
895
  }
883
- .kol-copy-btn--bare:hover,
884
- .kol-copy-btn--bare:active {
885
- background: transparent;
886
- }
887
-
888
- .kol-copy-btn-sm { width: 28px; height: 28px; }
889
- .kol-copy-btn-md { width: 32px; height: 32px; }
890
- .kol-copy-btn-lg { width: 36px; height: 36px; }
891
-
892
- .kol-copy-btn--grey {
893
- background: var(--kol-oq-12);
894
- }
895
-
896
+ /* ONE rest tone for every inline control (user ruling 2026-08-15): the star and
897
+ * the trash beside it sit at the same weight until you reach for one. They part
898
+ * on HOVER — a neutral control brightens, the accent control goes yellow. Yellow
899
+ * is the accent's alone, on hover and on its on-state; spending it on every
900
+ * hover left the on-state with nothing of its own to say. */
901
+ .kol-inline-control:hover { color: var(--kol-oq-96); }
902
+
903
+ .kol-inline-control--accent:hover,
904
+ .kol-inline-control--on,
905
+ .kol-inline-control--on:hover { color: var(--kol-color-yellow-300, var(--kol-palette-yellow)); }
906
+
907
+ /* TWO GLYPHS, not one glyph and a fill (user ruling 2026-08-15: *"just create
908
+ * another star"*, *"leave the one alone in cards"*). `star` stays stroke-only
909
+ * and `star-solid` is the filled twin; a control that has an on-state names
910
+ * both and ActionButton crossfades them.
911
+ *
912
+ * The alternative — filling the shared `star.svg` and unfilling it here — was
913
+ * tried and reverted: a glyph that ships filled renders filled in every
914
+ * consumer that never asked for a state, and the DS cannot see those.
915
+ *
916
+ * The `--on` fill below is kept for a control that ships ONE glyph and carries
917
+ * its state in the class instead; it is a no-op on a stroke-only icon. */
918
+ .kol-inline-control svg path { transition: fill var(--kol-transition-base); }
919
+ .kol-inline-control--on svg path { fill: currentColor; }
896
920
 
897
921
  /* ─────────────────────────────────────────────────────────────────────
898
922
  * Figure — atoms/Figure.jsx
@@ -818,3 +818,149 @@
818
818
  * (.kol-tag--sm, .kol-sidenav-group, .kol-control-* all set px directly). */
819
819
  padding-top: 20px;
820
820
  }
821
+
822
+ /* ─────────────────────────────────────────────────────────────────────
823
+ * Content family hover — molecules/ContentCard.jsx · ContentRow.jsx
824
+ *
825
+ * ONE rule, driven by a per-variant custom property the component sets
826
+ * (`--kol-content-hover-bg`). The alternative was a hover utility per
827
+ * variant in JSX, which Tailwind cannot generate from package source
828
+ * anyway (the SegmentedToggle lesson — component chrome belongs in
829
+ * kol-theme). A variant with no hover simply never sets the property,
830
+ * and the rule resolves to nothing.
831
+ *
832
+ * The fill is an oq-* step per 05-control-chrome.md's state model; the
833
+ * value itself is chosen in ContentCard/ContentRow's HOVER table.
834
+ * ───────────────────────────────────────────────────────────────────── */
835
+ @media (hover: hover) {
836
+ .kol-content-hover:hover {
837
+ background-color: var(--kol-content-hover-bg);
838
+ }
839
+ /* border is its OWN class, not a fallback chain off the bg one: a variant
840
+ * that steps its fill but not its frame (catalog) and one that steps its
841
+ * frame but not its fill (work) are both real, and an undefined var in a
842
+ * border-color declaration resolves to `unset` — which inherits, and quietly
843
+ * repaints a border nobody asked to move. */
844
+ .kol-content-hover-frame:hover {
845
+ border-color: var(--kol-content-hover-border);
846
+ }
847
+ }
848
+
849
+ /* ─────────────────────────────────────────────────────────────────────
850
+ * ContentRow — the responsive step
851
+ *
852
+ * The row publishes its box as custom properties and reads them here, so
853
+ * the md: step is ONE media query instead of per-variant Tailwind variants
854
+ * that Tailwind cannot generate from package source anyway. A variant that
855
+ * publishes no `-md` value falls back to its base and never steps.
856
+ * ───────────────────────────────────────────────────────────────────── */
857
+ .kol-row {
858
+ gap: var(--kol-row-gap);
859
+ padding: var(--kol-row-pad);
860
+ min-height: var(--kol-row-min-h);
861
+ background: var(--kol-row-bg);
862
+ border-color: var(--kol-row-border);
863
+ transition: background-color 300ms var(--kol-ease-house),
864
+ border-color 300ms var(--kol-ease-house);
865
+ }
866
+ .kol-row > .kol-row-thumb {
867
+ width: var(--kol-row-thumb);
868
+ }
869
+
870
+ /* 768 = the ruled `md` rung (08-breakpoints/01-values.md). A media query
871
+ * cannot read a custom property, so the number is a literal by necessity —
872
+ * naming the rung is the only thing that stops it reading as arbitrary. */
873
+ @media (min-width: 768px) {
874
+ .kol-row {
875
+ gap: var(--kol-row-gap-md, var(--kol-row-gap));
876
+ padding: var(--kol-row-pad-md, var(--kol-row-pad));
877
+ min-height: var(--kol-row-min-h-md, var(--kol-row-min-h));
878
+ }
879
+ .kol-row > .kol-row-thumb {
880
+ width: var(--kol-row-thumb-md, var(--kol-row-thumb));
881
+ }
882
+ }
883
+
884
+ /* ─────────────────────────────────────────────────────────────────────
885
+ * ContentCard — the work DRAWER
886
+ *
887
+ * A caption plate that rises over the bottom of the artwork on hover.
888
+ * The card is image-only at rest by design: a work shelf is a wall of
889
+ * images, and the caption is the reveal.
890
+ *
891
+ * Absolute, so it never adds height to the card and never reflows the
892
+ * shelf. Opacity only — a translucent wash over a photograph is exactly
893
+ * what the oq-* tier exists to avoid, but this plate is OPAQUE
894
+ * (surface-inverse) and it is the plate's PRESENCE that animates, not
895
+ * its transparency over the image.
896
+ * ───────────────────────────────────────────────────────────────────── */
897
+ .kol-card-drawer {
898
+ position: absolute;
899
+ inset-inline: 0;
900
+ bottom: 0;
901
+ z-index: 10;
902
+ opacity: 0;
903
+ transition: opacity 300ms var(--kol-ease-house);
904
+ }
905
+
906
+ @media (hover: hover) {
907
+ .group:hover > .kol-card-drawer {
908
+ opacity: 1;
909
+ }
910
+ }
911
+
912
+ /* touch has no hover — the caption must not be unreachable there */
913
+ @media (hover: none) {
914
+ .kol-card-drawer {
915
+ opacity: 1;
916
+ }
917
+ }
918
+
919
+ /* The card's text plate steps its padding at md where the variant declares
920
+ * one (work: 16 → 24, matching the shipped drawer's `p-4 md:p-6`). Same
921
+ * custom-property mechanism as .kol-row — Tailwind cannot generate an
922
+ * md: variant from package source, and these are per-variant data. */
923
+ /* the ruled `md` rung, same as .kol-row above */
924
+ @media (min-width: 768px) {
925
+ .kol-card-plate {
926
+ padding: var(--kol-plate-pad-md, var(--kol-plate-pad));
927
+ }
928
+ }
929
+
930
+ /* The card's rest colours, for the same reason as .kol-row — an inline
931
+ * background/borderColor outranks .kol-content-hover:hover, so the step
932
+ * silently never rendered. */
933
+ .kol-card {
934
+ background: var(--kol-card-bg);
935
+ border-color: var(--kol-card-border);
936
+ transition: background-color 300ms var(--kol-ease-house),
937
+ border-color 300ms var(--kol-ease-house);
938
+ }
939
+
940
+ /* ─────────────────────────────────────────────────────────────────────
941
+ * The house expand — molecules/SearchInput.jsx · organisms/ContentFilters.jsx
942
+ *
943
+ * A collapsed control that opens to its field: the search glyph widening
944
+ * into an input. Two components hand-wrote the same three declarations
945
+ * inline (`width 600ms`, `background 400ms`, `opacity 300ms`, all on the
946
+ * house curve), which is how the curve came to be hardcoded in seven
947
+ * places to begin with.
948
+ *
949
+ * Durations are STAGGERED on purpose and that is the whole character of
950
+ * the move: the box travels longest (600), its fill settles sooner (400),
951
+ * and the content inside arrives last and quickest (300) so it does not
952
+ * smear across the widening box.
953
+ *
954
+ * The width VALUE stays inline — it is per-instance data (a consumer's
955
+ * expanded width), not chrome. Only the motion lives here.
956
+ * ───────────────────────────────────────────────────────────────────── */
957
+ .kol-expand {
958
+ transition: width 600ms var(--kol-ease-house),
959
+ background 400ms var(--kol-ease-house),
960
+ border-color 400ms var(--kol-ease-house);
961
+ }
962
+
963
+ /* the content that rides inside an expanding box */
964
+ .kol-expand-content {
965
+ transition: opacity 300ms var(--kol-ease-house);
966
+ }
package/kol-theme.css CHANGED
@@ -95,8 +95,18 @@
95
95
 
96
96
  /* The house curve (2026-08-15) — the easing the cards actually use; was
97
97
  * hardcoded in 5 files before this token existed. Curve only, no duration:
98
- * it pairs with whatever duration the motion needs. */
99
- --kol-ease-house: cubic-bezier(0.16, 1, 0.3, 1);
98
+ * it pairs with whatever duration the motion needs.
99
+ *
100
+ * BALANCED, not expo (user ruling 2026-08-15). This was easeOutExpo
101
+ * `cubic-bezier(0.16, 1, 0.3, 1)`, which spends ~90% of its distance in the
102
+ * first fifth of the duration and the rest on a settle you cannot see — so
103
+ * every motion read as instant no matter what duration it was given, and a
104
+ * small hover affordance never showed its move at all. */
105
+ --kol-ease-house: cubic-bezier(0.4, 0, 0.2, 1);
106
+ /* the bounce — overshoots past its target and settles back (y > 1 is what
107
+ * makes it bounce; house only decelerates). For a press that should feel
108
+ * springy rather than merely smooth. */
109
+ --kol-ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
100
110
 
101
111
  /* Content widths (2026-07-28; panel added 2026-07-30) — ONE frame, three
102
112
  * inner caps (chess law). Every page: mx-auto max-w-shell + the one padding
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolkrabbi/kol-theme",
3
- "version": "0.43.1",
3
+ "version": "0.45.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",