@kolkrabbi/kol-framework 0.4.0 → 0.4.2

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
@@ -630,6 +630,29 @@ code, pre {
630
630
  padding-top: 32px;
631
631
  }
632
632
 
633
+ /* Filename chip — same scale as the lang chip; no text-transform (casing
634
+ * law: filenames render as authored). When both chips show, the lang chip
635
+ * yields the top-left slot and sits inboard of the copy button. */
636
+ .kol-codeblock-filename {
637
+ position: absolute;
638
+ top: 8px;
639
+ left: 12px;
640
+ font-family: var(--kol-font-family-mono, monospace);
641
+ font-size: 10px;
642
+ font-weight: 600;
643
+ letter-spacing: 0.08em;
644
+ color: var(--kol-fg-40);
645
+ }
646
+
647
+ .kol-codeblock-filename ~ .kol-codeblock-lang {
648
+ left: auto;
649
+ right: 56px;
650
+ }
651
+
652
+ .kol-codeblock-filename + pre {
653
+ padding-top: 32px;
654
+ }
655
+
633
656
  /* Positioning only — the chip look itself is the CopyButton atom
634
657
  * (.kol-copy-btn in kol-theme). */
635
658
  .kol-codeblock-copy {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolkrabbi/kol-framework",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
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",
@@ -27,8 +27,8 @@
27
27
  "app-shell"
28
28
  ],
29
29
  "dependencies": {
30
- "@kolkrabbi/kol-component": "0.9.0",
31
- "@kolkrabbi/kol-icons": "0.5.0"
30
+ "@kolkrabbi/kol-component": "0.10.1",
31
+ "@kolkrabbi/kol-icons": "0.6.1"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "react": "^18.3.0 || ^19.0.0",
@@ -78,7 +78,7 @@ export default function ShellHeader({
78
78
  onClick={onMenuClick}
79
79
  aria-label="Open navigation menu"
80
80
  >
81
- <Icon name="menu" size={18} />
81
+ <Icon name="hamburger" size={18} />
82
82
  </button>
83
83
  )}
84
84
  </div>
@@ -123,7 +123,7 @@ export default function ShellHeader({
123
123
  onClick={onNavToggle}
124
124
  aria-label="Toggle navigation sidebar"
125
125
  >
126
- <Icon name="dock-left" size={18} />
126
+ <Icon name="panel-left" size={18} />
127
127
  </button>
128
128
  )}
129
129
  {onTocToggle && (
package/src/SideNav.jsx CHANGED
@@ -73,7 +73,7 @@ function GroupNode({ group, basePath, activeSectionId, indent, onNavigate, isAct
73
73
  return (
74
74
  <li>
75
75
  <div
76
- className={`kol-sidenav-group kol-helper-10 uppercase ${isAncestor ? 'text-emphasis' : 'text-subtle'}`}
76
+ className={`kol-sidenav-group kol-helper-10 ${isAncestor ? 'text-emphasis' : 'text-subtle'}`}
77
77
  style={{ paddingLeft: indent }}
78
78
  >
79
79
  {group.label}
@@ -6,13 +6,13 @@ export default function SubPageHero({ backTo, backLabel, label, title, lede }) {
6
6
  {backTo && (
7
7
  <Link
8
8
  to={backTo}
9
- className="kol-back-link kol-helper-12 uppercase tracking-widest text-body hover:text-emphasis no-underline"
9
+ className="kol-back-link kol-helper-12 tracking-widest text-body hover:text-emphasis no-underline"
10
10
  >
11
11
  {backLabel}
12
12
  </Link>
13
13
  )}
14
14
  {label && (
15
- <p className="kol-helper-12 uppercase tracking-widest text-meta m-0 mb-4">
15
+ <p className="kol-helper-12 tracking-widest text-meta m-0 mb-4">
16
16
  {label}
17
17
  </p>
18
18
  )}
@@ -1,27 +1,5 @@
1
- import { useEffect, useState } from 'react'
2
1
  import { Icon } from '@kolkrabbi/kol-icons'
3
-
4
- const STORAGE_KEY = 'kol-theme'
5
-
6
- /* An explicit choice is an app-set data-theme attribute or a saved user
7
- * toggle. Absent both, the DS is light-first and follows the system
8
- * (prefers-color-scheme) — matching the theme CSS, which only darkens via
9
- * explicit [data-theme="dark"] or the media query. */
10
- function getExplicitTheme() {
11
- const attr = document.documentElement.dataset.theme
12
- if (attr === 'light' || attr === 'dark') return attr
13
- try {
14
- const saved = localStorage.getItem(STORAGE_KEY)
15
- if (saved === 'light' || saved === 'dark') return saved
16
- } catch { /* storage blocked */ }
17
- return null
18
- }
19
-
20
- function getInitialTheme() {
21
- if (typeof document === 'undefined') return 'light'
22
- return getExplicitTheme()
23
- ?? (window.matchMedia?.('(prefers-color-scheme: dark)')?.matches ? 'dark' : 'light')
24
- }
2
+ import { useTheme } from './theme.js'
25
3
 
26
4
  /**
27
5
  * Theme toggle — horizontal icon-swap button.
@@ -37,50 +15,18 @@ function getInitialTheme() {
37
15
  * bg, no hover bg). For sidenav rows where the row should read
38
16
  * as plain text + icon, not a button.
39
17
  *
40
- * Self-contained but never imposing: initial state is the app-set
41
- * `data-theme` attribute, else the user's saved toggle (`kol-theme`), else
42
- * the system preference. `data-theme` + localStorage are written ONLY on a
43
- * user toggle — mounting the component leaves the DOM untouched, so an
44
- * undecided page keeps following prefers-color-scheme via the theme CSS.
45
- * The icon-swap animation slides the `mode-toggle-01` / `mode-toggle-02`
46
- * pair (v1 set) horizontally — two different halves of the split circle,
47
- * so the slide is a visible flip.
18
+ * Theme state lives in useTheme (./theme.js) — the exported machinery
19
+ * (initial-state resolution, saved-choice re-stamp, system-follow, cross-
20
+ * instance sync). This component is only the icon-swap UI on top of it.
21
+ * The icon-swap animation slides a doubled `mode-toggle-01` (v1 set)
22
+ * horizontally — the split-circle glyph makes the slide itself read as
23
+ * the flip (same-icon slide, per design).
48
24
  */
49
25
  export default function ThemeToggle({ variant = 'icon', className = '' }) {
50
- const [theme, setTheme] = useState(getInitialTheme)
51
-
52
- useEffect(() => {
53
- /* Re-stamp a previously saved user choice so persistence works in apps
54
- * with no theme boot script — but only when the app itself hasn't set
55
- * data-theme (an app-set attribute always wins over storage). */
56
- const attr = document.documentElement.dataset.theme
57
- let saved = null
58
- try { saved = localStorage.getItem(STORAGE_KEY) } catch { /* storage blocked */ }
59
- if (attr !== 'light' && attr !== 'dark') {
60
- if (saved === 'light' || saved === 'dark') {
61
- document.documentElement.dataset.theme = saved
62
- return
63
- }
64
- /* No explicit choice anywhere — CSS follows the system on its own;
65
- * this listener only keeps the toggle's icon in sync with it. */
66
- const mq = window.matchMedia?.('(prefers-color-scheme: dark)')
67
- if (!mq?.addEventListener) return
68
- const onChange = (e) => {
69
- try { if (localStorage.getItem(STORAGE_KEY)) return } catch { /* storage blocked */ }
70
- setTheme(e.matches ? 'dark' : 'light')
71
- }
72
- mq.addEventListener('change', onChange)
73
- return () => mq.removeEventListener('change', onChange)
74
- }
75
- }, [])
26
+ const { isDark, toggle } = useTheme()
76
27
 
77
- const isDark = theme === 'dark'
78
28
  const next = isDark ? 'light' : 'dark'
79
- const handleToggle = () => {
80
- setTheme(next)
81
- document.documentElement.dataset.theme = next
82
- try { localStorage.setItem(STORAGE_KEY, next) } catch { /* storage blocked */ }
83
- }
29
+ const handleToggle = toggle
84
30
 
85
31
  const iconSwap = (size) => (
86
32
  <span
@@ -93,7 +39,7 @@ export default function ThemeToggle({ variant = 'icon', className = '' }) {
93
39
  style={{ width: size * 2, transform: isDark ? 'translateX(0)' : `translateX(-${size}px)` }}
94
40
  >
95
41
  <Icon name="mode-toggle-01" size={size} />
96
- <Icon name="mode-toggle-02" size={size} />
42
+ <Icon name="mode-toggle-01" size={size} />
97
43
  </span>
98
44
  </span>
99
45
  )
package/src/index.js CHANGED
@@ -12,6 +12,7 @@ export { ShellTocContext, ShellTocCollapsedContext } from './AppShell.jsx'
12
12
  export { default as SideNav } from './SideNav.jsx'
13
13
  export { default as ShellHeader } from './ShellHeader.jsx'
14
14
  export { default as ThemeToggle } from './ThemeToggle.jsx'
15
+ export { useTheme, applyTheme, getInitialTheme, THEME_STORAGE_KEY } from './theme.js'
15
16
  export { default as Layout } from './Layout.jsx'
16
17
  export { default as PageSection } from './PageSection.jsx'
17
18
  export { default as PortalFooter } from './PortalFooter.jsx'
package/src/theme.js ADDED
@@ -0,0 +1,91 @@
1
+ import { useEffect, useState } from 'react'
2
+
3
+ /* Theme-state machinery — the logic behind ThemeToggle, exported so app code
4
+ * (navbars, heroes, anything theme-aware) can read/set the theme without
5
+ * mounting the toggle UI. Policy (unchanged from ThemeToggle): the DS is
6
+ * light-first; an explicit choice is an app-set data-theme attribute or a
7
+ * saved user toggle, and absent both the page follows prefers-color-scheme
8
+ * via the theme CSS. `data-theme` + localStorage are written ONLY through
9
+ * applyTheme (a user action) — mounting a reader leaves the DOM untouched. */
10
+
11
+ export const THEME_STORAGE_KEY = 'kol-theme'
12
+
13
+ function getExplicitTheme() {
14
+ const attr = document.documentElement.dataset.theme
15
+ if (attr === 'light' || attr === 'dark') return attr
16
+ try {
17
+ const saved = localStorage.getItem(THEME_STORAGE_KEY)
18
+ if (saved === 'light' || saved === 'dark') return saved
19
+ } catch { /* storage blocked */ }
20
+ return null
21
+ }
22
+
23
+ export function getInitialTheme() {
24
+ if (typeof document === 'undefined') return 'light'
25
+ return getExplicitTheme()
26
+ ?? (window.matchMedia?.('(prefers-color-scheme: dark)')?.matches ? 'dark' : 'light')
27
+ }
28
+
29
+ /** Stamp + persist an explicit theme choice. */
30
+ export function applyTheme(theme) {
31
+ document.documentElement.dataset.theme = theme
32
+ try { localStorage.setItem(THEME_STORAGE_KEY, theme) } catch { /* storage blocked */ }
33
+ }
34
+
35
+ /**
36
+ * useTheme — live theme state, synced across every consumer.
37
+ *
38
+ * All instances observe the html `data-theme` attribute, so any writer
39
+ * (this hook's setTheme, ThemeToggle, an app boot script) updates every
40
+ * reader. When the page has no explicit choice, instances follow the
41
+ * system preference. Returns { theme, isDark, setTheme, toggle }.
42
+ */
43
+ export function useTheme() {
44
+ const [theme, setThemeState] = useState(getInitialTheme)
45
+
46
+ useEffect(() => {
47
+ /* Re-stamp a previously saved user choice so persistence works in apps
48
+ * with no theme boot script — but only when the app itself hasn't set
49
+ * data-theme (an app-set attribute always wins over storage). Idempotent
50
+ * across instances. */
51
+ const attr = document.documentElement.dataset.theme
52
+ let saved = null
53
+ try { saved = localStorage.getItem(THEME_STORAGE_KEY) } catch { /* storage blocked */ }
54
+ if (attr !== 'light' && attr !== 'dark' && (saved === 'light' || saved === 'dark')) {
55
+ document.documentElement.dataset.theme = saved
56
+ }
57
+
58
+ /* Sync with any data-theme writer. */
59
+ const observer = new MutationObserver(() => {
60
+ const current = document.documentElement.dataset.theme
61
+ if (current === 'light' || current === 'dark') setThemeState(current)
62
+ })
63
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] })
64
+
65
+ /* No explicit choice anywhere — follow the system, like the theme CSS. */
66
+ const mq = window.matchMedia?.('(prefers-color-scheme: dark)')
67
+ const onChange = (e) => {
68
+ try { if (localStorage.getItem(THEME_STORAGE_KEY)) return } catch { /* storage blocked */ }
69
+ if (document.documentElement.dataset.theme) return
70
+ setThemeState(e.matches ? 'dark' : 'light')
71
+ }
72
+ mq?.addEventListener?.('change', onChange)
73
+
74
+ return () => {
75
+ observer.disconnect()
76
+ mq?.removeEventListener?.('change', onChange)
77
+ }
78
+ }, [])
79
+
80
+ const setTheme = (t) => {
81
+ setThemeState(t)
82
+ applyTheme(t)
83
+ }
84
+
85
+ return {
86
+ theme,
87
+ isDark: theme === 'dark',
88
+ setTheme,
89
+ toggle: () => setTheme(theme === 'dark' ? 'light' : 'dark'),
90
+ }
91
+ }