@kolkrabbi/kol-framework 0.4.1 → 0.5.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 +23 -0
- package/package.json +3 -3
- package/src/ShellHeader.jsx +2 -2
- package/src/SideNav.jsx +1 -1
- package/src/SubPageHero.jsx +2 -2
- package/src/ThemeToggle.jsx +10 -63
- package/src/index.js +1 -0
- package/src/theme.js +91 -0
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.
|
|
3
|
+
"version": "0.5.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",
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"app-shell"
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@kolkrabbi/kol-component": "0.
|
|
31
|
-
"@kolkrabbi/kol-icons": "0.
|
|
30
|
+
"@kolkrabbi/kol-component": "0.11.0",
|
|
31
|
+
"@kolkrabbi/kol-icons": "0.7.0"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"react": "^18.3.0 || ^19.0.0",
|
package/src/ShellHeader.jsx
CHANGED
|
@@ -78,7 +78,7 @@ export default function ShellHeader({
|
|
|
78
78
|
onClick={onMenuClick}
|
|
79
79
|
aria-label="Open navigation menu"
|
|
80
80
|
>
|
|
81
|
-
<Icon name="
|
|
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="
|
|
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
|
|
76
|
+
className={`kol-sidenav-group kol-helper-10 ${isAncestor ? 'text-emphasis' : 'text-subtle'}`}
|
|
77
77
|
style={{ paddingLeft: indent }}
|
|
78
78
|
>
|
|
79
79
|
{group.label}
|
package/src/SubPageHero.jsx
CHANGED
|
@@ -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
|
|
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
|
|
15
|
+
<p className="kol-helper-12 tracking-widest text-meta m-0 mb-4">
|
|
16
16
|
{label}
|
|
17
17
|
</p>
|
|
18
18
|
)}
|
package/src/ThemeToggle.jsx
CHANGED
|
@@ -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
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* user toggle — mounting the component leaves the DOM untouched, so an
|
|
44
|
-
* undecided page keeps following prefers-color-scheme via the theme CSS.
|
|
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.
|
|
45
21
|
* The icon-swap animation slides a doubled `mode-toggle-01` (v1 set)
|
|
46
22
|
* horizontally — the split-circle glyph makes the slide itself read as
|
|
47
23
|
* the flip (same-icon slide, per design).
|
|
48
24
|
*/
|
|
49
25
|
export default function ThemeToggle({ variant = 'icon', className = '' }) {
|
|
50
|
-
const
|
|
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
|
|
@@ -123,16 +69,17 @@ export default function ThemeToggle({ variant = 'icon', className = '' }) {
|
|
|
123
69
|
)
|
|
124
70
|
}
|
|
125
71
|
|
|
126
|
-
// variant === 'icon' (default)
|
|
72
|
+
// variant === 'icon' (default) — 36×36 button / 20px glyph, the DS reference
|
|
73
|
+
// (user-verified 2026-07-15; was 32/18, which consumers shimmed via CSS)
|
|
127
74
|
return (
|
|
128
75
|
<button
|
|
129
76
|
type="button"
|
|
130
77
|
onClick={handleToggle}
|
|
131
78
|
aria-label={`Switch to ${next} mode`}
|
|
132
79
|
title={`Switch to ${next} mode`}
|
|
133
|
-
className={`inline-flex items-center justify-center w-
|
|
80
|
+
className={`inline-flex items-center justify-center w-9 h-9 p-0 bg-transparent border-0 cursor-pointer text-emphasis hover:opacity-80 transition-opacity duration-300 ${className}`.trim()}
|
|
134
81
|
>
|
|
135
|
-
{iconSwap(
|
|
82
|
+
{iconSwap(20)}
|
|
136
83
|
</button>
|
|
137
84
|
)
|
|
138
85
|
}
|
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
|
+
}
|