@tomehq/theme 0.2.5 → 0.2.7

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/dist/entry.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  entry_default
3
- } from "./chunk-7MUTU5D4.js";
3
+ } from "./chunk-X4VQYPKO.js";
4
4
  export {
5
5
  entry_default as default
6
6
  };
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  Shell,
4
4
  THEME_PRESETS,
5
5
  entry_default
6
- } from "./chunk-7MUTU5D4.js";
6
+ } from "./chunk-X4VQYPKO.js";
7
7
  export {
8
8
  AiChat,
9
9
  entry_default as App,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomehq/theme",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Tome default theme and React app shell",
5
5
  "type": "module",
6
6
  "main": "./src/index.tsx",
@@ -9,8 +9,8 @@
9
9
  "./entry": "./src/entry.tsx"
10
10
  },
11
11
  "dependencies": {
12
- "@tomehq/components": "0.2.5",
13
- "@tomehq/core": "0.2.5"
12
+ "@tomehq/components": "0.2.6",
13
+ "@tomehq/core": "0.2.6"
14
14
  },
15
15
  "peerDependencies": {
16
16
  "react": "^18.0.0 || ^19.0.0",
package/src/Shell.tsx CHANGED
@@ -331,7 +331,8 @@ export function Shell({
331
331
  return window.matchMedia?.("(prefers-color-scheme: dark)").matches ?? true;
332
332
  });
333
333
 
334
- const [sbOpen, setSb] = useState(true);
334
+ const [mobile, setMobile] = useState(() => typeof window !== "undefined" && window.innerWidth < 768);
335
+ const [sbOpen, setSb] = useState(() => typeof window !== "undefined" && window.innerWidth >= 768);
335
336
  const [searchOpen, setSearch] = useState(false);
336
337
  const [versionDropdownOpen, setVersionDropdown] = useState(false);
337
338
  const [localeDropdownOpen, setLocaleDropdown] = useState(false);
@@ -340,7 +341,7 @@ export function Shell({
340
341
  const isOldVersion = versioning && currentVersion && currentVersion !== versioning.current;
341
342
  const [expanded, setExpanded] = useState<string[]>(navigation.map(n => n.section));
342
343
  const contentRef = useRef<HTMLDivElement>(null);
343
- const [wide, setWide] = useState(true);
344
+ const [wide, setWide] = useState(() => typeof window !== "undefined" && window.innerWidth > 1100);
344
345
 
345
346
  const preset = (config.theme?.preset || "amber") as PresetName;
346
347
  const baseTokens = THEME_PRESETS[preset]?.[isDark ? "dark" : "light"] || THEME_PRESETS.amber.dark;
@@ -377,11 +378,24 @@ export function Shell({
377
378
  }, [isDark]);
378
379
 
379
380
  useEffect(() => {
380
- const c = () => setWide(window.innerWidth > 1100);
381
- c(); window.addEventListener("resize", c);
382
- return () => window.removeEventListener("resize", c);
381
+ const handleResize = () => {
382
+ const w = window.innerWidth;
383
+ setWide(w > 1100);
384
+ setMobile(w < 768);
385
+ };
386
+ handleResize();
387
+ window.addEventListener("resize", handleResize);
388
+ return () => window.removeEventListener("resize", handleResize);
383
389
  }, []);
384
390
 
391
+ // Lock body scroll when mobile sidebar is open
392
+ useEffect(() => {
393
+ if (mobile && sbOpen) {
394
+ document.body.style.overflow = "hidden";
395
+ return () => { document.body.style.overflow = ""; };
396
+ }
397
+ }, [mobile, sbOpen]);
398
+
385
399
  useEffect(() => { contentRef.current?.scrollTo(0, 0); }, [currentPageId]);
386
400
 
387
401
  // ── TOC: Config-based depth filtering + frontmatter opt-out ──
@@ -500,16 +514,25 @@ export function Shell({
500
514
  allPages={allPages}
501
515
  onNavigate={(id) => { onNavigate(id); setSearch(false); }}
502
516
  onClose={() => setSearch(false)}
517
+ mobile={mobile}
503
518
  />
504
519
  ) : null}
505
520
 
506
521
  <div style={{ display: "flex", height: "100vh" }}>
522
+ {/* Mobile sidebar backdrop */}
523
+ {mobile && sbOpen && (
524
+ <div onClick={() => setSb(false)} style={{
525
+ position: "fixed", inset: 0, zIndex: 200,
526
+ background: "rgba(0,0,0,0.4)", backdropFilter: "blur(2px)",
527
+ }} />
528
+ )}
507
529
  {/* Sidebar */}
508
530
  <aside style={{
509
531
  width: sbOpen ? 270 : 0, minWidth: sbOpen ? 270 : 0,
510
532
  background: "var(--sbBg)", borderRight: "1px solid var(--bd)",
511
533
  display: "flex", flexDirection: "column",
512
534
  transition: "width .2s, min-width .2s", overflow: "hidden",
535
+ ...(mobile ? { position: "fixed" as const, top: 0, left: 0, bottom: 0, zIndex: 201 } : {}),
513
536
  }}>
514
537
  <a href="/" style={{ padding: "18px 20px", display: "flex", alignItems: "baseline", gap: 6, borderBottom: "1px solid var(--bd)", textDecoration: "none", color: "inherit" }}>
515
538
  <span style={{ fontFamily: "var(--font-heading)", fontSize: 22, fontWeight: 700, fontStyle: "italic" }}>
@@ -519,7 +542,7 @@ export function Shell({
519
542
  </a>
520
543
 
521
544
  <div style={{ padding: "12px 14px" }}>
522
- <button onClick={() => setSearch(true)} style={{
545
+ <button onClick={() => { setSearch(true); if (mobile) setSb(false); }} style={{
523
546
  display: "flex", alignItems: "center", gap: 8, width: "100%",
524
547
  background: "var(--cdBg)", border: "1px solid var(--bd)", borderRadius: 2,
525
548
  padding: "8px 12px", cursor: "pointer", color: "var(--txM)", fontSize: 12.5,
@@ -545,7 +568,7 @@ export function Shell({
545
568
  {sec.pages.map(p => {
546
569
  const active = currentPageId === p.id;
547
570
  return (
548
- <button key={p.id} onClick={() => onNavigate(p.id)} style={{
571
+ <button key={p.id} onClick={() => { onNavigate(p.id); if (mobile) setSb(false); }} style={{
549
572
  display: "flex", alignItems: "center", gap: 10, width: "100%",
550
573
  textAlign: "left", background: "none",
551
574
  border: "none", borderRadius: 0,
@@ -567,7 +590,7 @@ export function Shell({
567
590
  <div style={{ padding: "12px 16px", borderTop: "1px solid var(--bd)", display: "flex", alignItems: "center", justifyContent: "space-between" }}>
568
591
  {/* TOM-12: Only show toggle when mode is "auto" */}
569
592
  {themeMode === "auto" ? (
570
- <button onClick={() => setDark(d => !d)} style={{ background: "none", border: "none", color: "var(--txM)", cursor: "pointer", display: "flex" }}>
593
+ <button aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"} onClick={() => setDark(d => !d)} style={{ background: "none", border: "none", color: "var(--txM)", cursor: "pointer", display: "flex" }}>
571
594
  {isDark ? <SunIcon /> : <MoonIcon />}
572
595
  </button>
573
596
  ) : <div />}
@@ -580,24 +603,30 @@ export function Shell({
580
603
  <div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
581
604
  {/* Header */}
582
605
  <header style={{
583
- display: "flex", alignItems: "center", gap: 12, padding: "10px 24px",
606
+ display: "flex", alignItems: "center", gap: mobile ? 8 : 12, padding: mobile ? "8px 12px" : "10px 24px",
584
607
  borderBottom: "1px solid var(--bd)", background: "var(--hdBg)", backdropFilter: "blur(12px)",
585
608
  }}>
586
- <button onClick={() => setSb(!sbOpen)} style={{ background: "none", border: "none", color: "var(--txM)", cursor: "pointer", display: "flex" }}>
609
+ <button aria-label={sbOpen ? "Close sidebar" : "Open sidebar"} onClick={() => setSb(!sbOpen)} style={{ background: "none", border: "none", color: "var(--txM)", cursor: "pointer", display: "flex" }}>
587
610
  {sbOpen ? <XIcon /> : <MenuIcon />}
588
611
  </button>
589
- <div style={{ display: "flex", alignItems: "center", gap: 8, fontFamily: "var(--font-code)", fontSize: 11, color: "var(--txM)", letterSpacing: ".03em", flex: 1 }}>
590
- {navigation.map(s => {
591
- const f = s.pages.find(p => p.id === currentPageId);
592
- if (!f) return null;
593
- return <span key={s.section} style={{ display: "flex", alignItems: "center", gap: 8 }}>
594
- <span>{s.section}</span><ChevRight /><span style={{ color: "var(--ac)" }}>{f.title}</span>
595
- </span>;
596
- })}
597
- </div>
612
+ {mobile ? (
613
+ <span style={{ fontSize: 13, color: "var(--ac)", fontFamily: "var(--font-code)", flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
614
+ {navigation.flatMap(s => s.pages).find(p => p.id === currentPageId)?.title || ""}
615
+ </span>
616
+ ) : (
617
+ <div style={{ display: "flex", alignItems: "center", gap: 8, fontFamily: "var(--font-code)", fontSize: 11, color: "var(--txM)", letterSpacing: ".03em", flex: 1 }}>
618
+ {navigation.map(s => {
619
+ const f = s.pages.find(p => p.id === currentPageId);
620
+ if (!f) return null;
621
+ return <span key={s.section} style={{ display: "flex", alignItems: "center", gap: 8 }}>
622
+ <span>{s.section}</span><ChevRight /><span style={{ color: "var(--ac)" }}>{f.title}</span>
623
+ </span>;
624
+ })}
625
+ </div>
626
+ )}
598
627
 
599
628
  {/* Top Nav Links */}
600
- {config.topNav && config.topNav.length > 0 && (
629
+ {config.topNav && config.topNav.length > 0 && !mobile && (
601
630
  <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
602
631
  {config.topNav.map((link) => {
603
632
  const isExternal = link.href.startsWith("http") || !link.href.startsWith("#");
@@ -771,8 +800,8 @@ export function Shell({
771
800
 
772
801
  {/* Content + TOC */}
773
802
  <div ref={contentRef} style={{ flex: 1, overflow: "auto", display: "flex" }}>
774
- <main style={{ flex: 1, maxWidth: 760, padding: "40px 48px 80px", margin: "0 auto" }}>
775
- <h1 style={{ fontFamily: "var(--font-heading)", fontSize: 38, fontWeight: 400, fontStyle: "italic", lineHeight: 1.15, marginBottom: 8 }}>
803
+ <main style={{ flex: 1, maxWidth: mobile ? "100%" : 760, padding: mobile ? "24px 16px 60px" : "40px 48px 80px", margin: "0 auto", minWidth: 0 }}>
804
+ <h1 style={{ fontFamily: "var(--font-heading)", fontSize: mobile ? 26 : 38, fontWeight: 400, fontStyle: "italic", lineHeight: 1.15, marginBottom: 8 }}>
776
805
  {pageTitle}
777
806
  </h1>
778
807
  {pageDescription && <p style={{ fontSize: 16, color: "var(--tx2)", lineHeight: 1.6, marginBottom: 32 }}>{pageDescription}</p>}
@@ -794,7 +823,7 @@ export function Shell({
794
823
 
795
824
  {/* TOM-48: Edit this page link + TOM-54: Last updated */}
796
825
  {(editUrl || lastUpdated) && (
797
- <div style={{ marginTop: 40, display: "flex", alignItems: "center", justifyContent: "space-between", gap: 16 }}>
826
+ <div style={{ marginTop: 40, display: "flex", flexDirection: mobile ? "column" : "row", alignItems: mobile ? "flex-start" : "center", justifyContent: "space-between", gap: mobile ? 8 : 16 }}>
798
827
  {editUrl && (
799
828
  <div data-testid="edit-page-link">
800
829
  <a
@@ -822,7 +851,7 @@ export function Shell({
822
851
  )}
823
852
 
824
853
  {/* Prev / Next */}
825
- <div style={{ display: "flex", justifyContent: "space-between", marginTop: (editUrl || lastUpdated) ? 16 : 48, paddingTop: 24, borderTop: "1px solid var(--bd)", gap: 16 }}>
854
+ <div style={{ display: "flex", flexDirection: mobile ? "column" : "row", justifyContent: "space-between", marginTop: (editUrl || lastUpdated) ? 16 : 48, paddingTop: 24, borderTop: "1px solid var(--bd)", gap: mobile ? 12 : 16 }}>
826
855
  {prev ? (
827
856
  <button onClick={() => onNavigate(prev.id)} style={{
828
857
  display: "flex", alignItems: "center", gap: 8, background: "none",
@@ -898,10 +927,11 @@ interface SearchResult {
898
927
  }
899
928
 
900
929
  // ── SEARCH MODAL (TOM-15) ────────────────────────────────
901
- function SearchModal({ allPages, onNavigate, onClose }: {
930
+ function SearchModal({ allPages, onNavigate, onClose, mobile }: {
902
931
  allPages: Array<{ id: string; title: string; description?: string }>;
903
932
  onNavigate: (id: string) => void;
904
933
  onClose: () => void;
934
+ mobile?: boolean;
905
935
  }) {
906
936
  const [q, setQ] = useState("");
907
937
  const [results, setResults] = useState<SearchResult[]>([]);
@@ -993,12 +1023,15 @@ function SearchModal({ allPages, onNavigate, onClose }: {
993
1023
  return (
994
1024
  <div onClick={onClose} style={{
995
1025
  position: "fixed", inset: 0, zIndex: 1000, background: "rgba(0,0,0,0.55)",
996
- backdropFilter: "blur(6px)", display: "flex", alignItems: "flex-start",
997
- justifyContent: "center", paddingTop: "12vh",
1026
+ backdropFilter: "blur(6px)", display: "flex",
1027
+ alignItems: mobile ? "stretch" : "flex-start",
1028
+ justifyContent: "center", paddingTop: mobile ? 0 : "12vh",
998
1029
  }}>
999
1030
  <div onClick={e => e.stopPropagation()} style={{
1000
- background: "var(--sf)", border: "1px solid var(--bd)", borderRadius: 2,
1001
- width: "100%", maxWidth: 520, boxShadow: "0 24px 80px rgba(0,0,0,0.4)", overflow: "hidden",
1031
+ background: "var(--sf)", border: mobile ? "none" : "1px solid var(--bd)", borderRadius: mobile ? 0 : 2,
1032
+ width: "100%", maxWidth: mobile ? "100%" : 520, boxShadow: mobile ? "none" : "0 24px 80px rgba(0,0,0,0.4)",
1033
+ overflow: "hidden", display: "flex", flexDirection: "column" as const,
1034
+ ...(mobile ? { height: "100%" } : {}),
1002
1035
  }}>
1003
1036
  <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "14px 18px", borderBottom: "1px solid var(--bd)" }}>
1004
1037
  <SearchIcon />
@@ -1009,7 +1042,7 @@ function SearchModal({ allPages, onNavigate, onClose }: {
1009
1042
  />
1010
1043
  <kbd style={{ fontFamily: "var(--font-code)", fontSize: 10, color: "var(--txM)", background: "var(--cdBg)", padding: "2px 6px", borderRadius: 2, border: "1px solid var(--bd)" }}>ESC</kbd>
1011
1044
  </div>
1012
- {results.length > 0 && <div style={{ padding: 6, maxHeight: 360, overflow: "auto" }}>
1045
+ {results.length > 0 && <div style={{ padding: 6, maxHeight: mobile ? "none" : 360, overflow: "auto", flex: mobile ? 1 : undefined }}>
1013
1046
  {results.map((r, i) => (
1014
1047
  <button key={r.id + i} onClick={() => onNavigate(r.id)} style={{
1015
1048
  display: "block", width: "100%", textAlign: "left",
package/src/entry.tsx CHANGED
@@ -59,6 +59,15 @@ const contentStyles = `
59
59
  .tome-content img { max-width: 100%; border-radius: 2px; }
60
60
  .tome-content hr { border: none; border-top: 1px solid var(--bd); margin: 2em 0; }
61
61
 
62
+ /* Mobile responsive content */
63
+ @media (max-width: 767px) {
64
+ .tome-content h2 { font-size: 1.2em; margin-top: 1.5em; }
65
+ .tome-content h3 { font-size: 1.05em; }
66
+ .tome-content pre code { font-size: 12px; padding: 0.8em 1em; }
67
+ .tome-content table { display: block; overflow-x: auto; -webkit-overflow-scrolling: touch; }
68
+ .tome-content blockquote { margin: 0.8em 0; }
69
+ }
70
+
62
71
  /* Selection style */
63
72
  ::selection { background: var(--acD); color: var(--ac); }
64
73
 
@@ -88,6 +97,14 @@ const contentStyles = `
88
97
  html.dark .shiki span[style*="--shiki-dark:#6A737D"] {
89
98
  --shiki-dark: #a0aab5 !important;
90
99
  }
100
+
101
+ /* Light mode: darken low-contrast github-light tokens for WCAG AA on --cdBg backgrounds */
102
+ html:not(.dark) .shiki span[style*="color:#6A737D"] { color: #57606a !important; }
103
+ html:not(.dark) .shiki span[style*="color:#E36209"] { color: #b35405 !important; }
104
+ html:not(.dark) .shiki span[style*="color:#6F42C1"] { color: #5a32a3 !important; }
105
+ html:not(.dark) .shiki span[style*="color:#22863A"] { color: #1a6e2e !important; }
106
+ html:not(.dark) .shiki span[style*="color:#D73A49"] { color: #b62324 !important; }
107
+ html:not(.dark) .shiki span[style*="color:#005CC5"] { color: #0349b4 !important; }
91
108
  `;
92
109
 
93
110
  // ── PAGE TYPES ────────────────────────────────────────────