@kolkrabbi/kol-theme 0.43.0 → 0.44.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,81 +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);
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;
889
+ padding: 0;
890
+ border: none;
891
+ background: transparent;
892
+ color: var(--kol-oq-64);
893
+ cursor: pointer;
894
+ transition: color var(--kol-transition-base);
855
895
  }
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.
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.
862
911
  *
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.
912
+ * The alternative — filling the shared `star.svg` and unfilling it herewas
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.
867
915
  *
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
- .kol-copy-btn-sm { width: 28px; height: 28px; }
875
- .kol-copy-btn-md { width: 32px; height: 32px; }
876
- .kol-copy-btn-lg { width: 36px; height: 36px; }
877
-
878
- .kol-copy-btn--grey {
879
- background: var(--kol-oq-12);
880
- }
881
-
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; }
882
920
 
883
921
  /* ─────────────────────────────────────────────────────────────────────
884
922
  * Figure — atoms/Figure.jsx
@@ -1035,6 +1073,16 @@ a.kol-icon-frame {
1035
1073
  color: var(--kol-surface-on-primary);
1036
1074
  border: 1px solid transparent;
1037
1075
  }
1076
+ /* plate — the ONE exception in 05-control-chrome.md § Icon box: an affordance
1077
+ * sitting on a PHOTO. It needs an opaque plate and a backdrop blur to stay
1078
+ * legible over arbitrary pixels, and no Button variant covers it. Values are
1079
+ * MediaCard's, which is the instance the doc names. */
1080
+ .kol-icon-frame-plate {
1081
+ background: var(--kol-fg-absolute-12, rgba(0, 0, 0, 0.4));
1082
+ backdrop-filter: blur(4px);
1083
+ color: var(--kol-surface-on-primary);
1084
+ }
1085
+
1038
1086
  .kol-icon-frame-grey {
1039
1087
  background: var(--kol-oq-12);
1040
1088
  color: var(--kol-surface-on-primary);
@@ -813,6 +813,126 @@
813
813
  border-top: 1px solid var(--kol-fg-08);
814
814
  /* 20px below the rule; the 20px above falls out of the rail body's own
815
815
  * gap-5, so a symmetric space–divider–space lands without this file
816
- * knowing anything about the rail. */
817
- padding-top: var(--kol-spacing-5);
816
+ * knowing anything about the rail.
817
+ * px, not --kol-spacing-5 — that token is rem, and spacing here is px
818
+ * (.kol-tag--sm, .kol-sidenav-group, .kol-control-* all set px directly). */
819
+ padding-top: 20px;
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);
818
938
  }
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.0",
3
+ "version": "0.44.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",