@kolkrabbi/kol-framework 0.21.1 → 0.23.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/kol-framework.css CHANGED
@@ -19,6 +19,15 @@ body {
19
19
  color: var(--kol-surface-on-primary);
20
20
  }
21
21
 
22
+ /* Reserve the scrollbar gutter so a full-bleed surface cannot induce a
23
+ horizontal scroll when a vertical scrollbar appears (2026-08-15, promoted
24
+ from kol-fxr's index.css — a consumer's entry stylesheet is for IMPORTS,
25
+ and this is chrome every app on this framework wants). Consumers on an
26
+ overlay-scrollbar platform pay nothing; the property is inert there. */
27
+ :root {
28
+ scrollbar-gutter: stable;
29
+ }
30
+
22
31
  /* The global user-select:none gate + allowlist was REMOVED 2026-07-29 (user
23
32
  * law, brand-app ruling): text is selectable everywhere — copyability beats
24
33
  * chrome purity. */
@@ -143,6 +152,25 @@ body {
143
152
  }
144
153
  .kol-sidenav-footer { padding-inline: 1.5rem 1rem; }
145
154
 
155
+ /* The aside's OWN box (SideNavMobilePosition, kol-website 2026-08-25) — the
156
+ * hop's law above, one element up. `sticky top-0 self-start h-dvh flex
157
+ * flex-col z-20` rode the className, and a utility outranks every rule in
158
+ * this layer — so the 767px drawer block below (`position: fixed`) never won:
159
+ * the aside stayed IN FLOW, the one-column grid handed it an 852px row, and
160
+ * every consumer page opened one viewport down on phones (probe: aside
161
+ * `sticky`, content top=852 at 393; the consumer's stopgap was one media rule
162
+ * re-stating `fixed`). Declared here, the drawer rule beats it by source
163
+ * order — which is how it was written to work. */
164
+ .kol-sidenav {
165
+ position: sticky;
166
+ top: 0;
167
+ align-self: start;
168
+ height: 100dvh;
169
+ display: flex;
170
+ flex-direction: column;
171
+ z-index: var(--kol-z-sticky);
172
+ }
173
+
146
174
  /* :root[data-sidenav="collapsed"] — the manual collapse, rebuilt DELIBERATELY
147
175
  * 2026-08-09 (lobby: SideNavGrabResize) after the 2026-07-29 removal of the
148
176
  * renderer-less stamp. SideNav's grab edge (useDragResize.js) owns the state
@@ -885,7 +913,15 @@ body {
885
913
  }
886
914
  }
887
915
 
888
- /* Tab strip — horizontally scrollable, scrollbar hidden. */
916
+ /* Tab strip — horizontally scrollable, scrollbar hidden.
917
+ * Snaps to a tab's start on a flick (WorkshopShellMobile, kol-website
918
+ * 2026-08-25): the strip measured 613px of tabs in a 345px viewport and did
919
+ * scroll, but a swipe could settle on half a label. `proximity`, not
920
+ * `mandatory` — the last tabs can never reach a start position, and mandatory
921
+ * would refuse to show them. ShellHeader.jsx scrolls the active tab into view
922
+ * on load, to the same start edge, so the two never fight. NO edge fade: the
923
+ * 2026-07-28 ruling bans scroll-edge paint (the table wrapper's cover
924
+ * gradients died for it 2026-08-09). */
889
925
  .kol-shell-header-tabs {
890
926
  display: flex;
891
927
  flex-wrap: nowrap;
@@ -894,6 +930,7 @@ body {
894
930
  padding-top: 8px;
895
931
  scrollbar-width: none;
896
932
  -ms-overflow-style: none;
933
+ scroll-snap-type: x proximity;
897
934
  }
898
935
 
899
936
  .kol-shell-header-tabs::-webkit-scrollbar {
@@ -922,6 +959,7 @@ body {
922
959
  border-bottom: 2px solid transparent;
923
960
  transition: color 0.2s ease, border-color 0.2s ease;
924
961
  text-decoration: none;
962
+ scroll-snap-align: start;
925
963
  }
926
964
 
927
965
  .kol-shell-header-tab:hover {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolkrabbi/kol-framework",
3
- "version": "0.21.1",
3
+ "version": "0.23.0",
4
4
  "description": "KOL app shell — sidenav, layout, theme toggle, footer, heroes + brand color layer. SideNav takes nav data as props so the tree stays app-local.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -28,8 +28,8 @@
28
28
  "app-shell"
29
29
  ],
30
30
  "dependencies": {
31
- "@kolkrabbi/kol-component": "^0.46.0",
32
- "@kolkrabbi/kol-icons": "^0.17.0"
31
+ "@kolkrabbi/kol-icons": "^0.18.0",
32
+ "@kolkrabbi/kol-component": "^0.68.1"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "react": "^18.3.0 || ^19.0.0",
@@ -1,3 +1,4 @@
1
+ import { useLayoutEffect, useRef } from 'react'
1
2
  import { Icon } from '@kolkrabbi/kol-icons'
2
3
  import { IconFrame, SearchInput, Tooltip, glyphSize } from '@kolkrabbi/kol-component'
3
4
  import ThemeToggle from './ThemeToggle.jsx'
@@ -89,6 +90,26 @@ export default function ShellHeader({
89
90
  }) {
90
91
  const hasTabRow = nav.length > 0 || Boolean(search) || Boolean(onNavToggle) || Boolean(onTocToggle)
91
92
 
93
+ /* The current tab must be ON SCREEN (WorkshopShellMobile, kol-website
94
+ * 2026-08-25). The strip scrolls (.kol-shell-header-tabs) — but on a phone
95
+ * the active tab measured x=347–445 in a 345px strip: "Dashboard" was the
96
+ * page you were on and the one tab you could not see. Scrolled to the
97
+ * strip's start edge (the snap position), and ONLY when it is out of view,
98
+ * so a wide viewport never moves. Keyed on the active href, not `nav` —
99
+ * consumers rebuild that array every render. Layout effect: before paint,
100
+ * so a route change never flashes the wrong tab. */
101
+ const tabsRef = useRef(null)
102
+ const activeHref = nav.find((item) => (isActive ? isActive(item.href) : false))?.href
103
+ useLayoutEffect(() => {
104
+ const strip = tabsRef.current
105
+ const tab = strip?.querySelector('[aria-current="page"]')
106
+ if (!strip || !tab) return
107
+ const s = strip.getBoundingClientRect()
108
+ const t = tab.getBoundingClientRect()
109
+ if (t.left >= s.left && t.right <= s.right) return
110
+ strip.scrollLeft += t.left - s.left
111
+ }, [activeHref])
112
+
92
113
  return (
93
114
  <header className={`kol-shell-header sticky top-0 z-50 shrink-0 bg-surface-primary ${className}`.trim()}>
94
115
  {/* Row 1: brand block + controls */}
@@ -139,7 +160,7 @@ export default function ShellHeader({
139
160
  {hasTabRow && (
140
161
  <div className="border-b border-fg-08">
141
162
  <div className="w-full" style={{ paddingInline: 'var(--kol-pad-chrome-x)' }}>
142
- <div className="kol-shell-header-tabs">
163
+ <div className="kol-shell-header-tabs" ref={tabsRef}>
143
164
  <nav className="flex flex-1 gap-6" aria-label="Sections">
144
165
  {nav.map((item) => {
145
166
  const active = isActive ? isActive(item.href) : false
package/src/SideNav.jsx CHANGED
@@ -93,7 +93,12 @@ export default function SideNav({
93
93
  return (
94
94
  <aside
95
95
  ref={asideRef}
96
- className={`kol-sidenav sticky top-0 self-start h-dvh flex flex-col z-20${background ? ' bg-surface-primary border-r border-fg-08' : ''}${collapsed ? ' is-collapsed' : ''}${drawerOpen ? ' is-drawer-open' : ''}`}
96
+ /* Box utilities deliberately absent (SideNavMobilePosition, 2026-08-25):
97
+ * the aside's position/height/stack live in .kol-sidenav
98
+ * (kol-framework.css). `sticky … h-dvh z-20` here outranked the 767px
99
+ * drawer rule's `position: fixed`, so every consumer page opened one
100
+ * viewport down on phones. Only colour utilities ride the element. */
101
+ className={`kol-sidenav${background ? ' bg-surface-primary border-r border-fg-08' : ''}${collapsed ? ' is-collapsed' : ''}${drawerOpen ? ' is-drawer-open' : ''}`}
97
102
  >
98
103
  {/* NO chip Button in either state (user build order 2026-08-09 — "MAKE
99
104
  * IT"; supersedes the elder A/B): the pill-marked grab edge below is