Skip to content

Commit 5c05ec1

Browse files
committed
new feature
1 parent 1dbe2ec commit 5c05ec1

7 files changed

Lines changed: 406 additions & 44 deletions

File tree

index.html

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,18 @@
66
<title>syskit</title>
77
<script>
88
(function () {
9-
var t = localStorage.getItem('syskit-theme') || 'dark';
9+
// Restore path after GitHub Pages 404 redirect
10+
var redirect = sessionStorage.getItem('syskit-redirect');
11+
if (redirect) {
12+
sessionStorage.removeItem('syskit-redirect');
13+
history.replaceState(null, null, redirect);
14+
}
15+
16+
// Apply theme before paint to avoid flash
17+
var saved = localStorage.getItem('syskit-theme') || 'system';
18+
var t = saved === 'system'
19+
? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
20+
: saved;
1021
document.documentElement.setAttribute('data-theme', t);
1122
})();
1223
</script>

public/404.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<meta charset="UTF-8" />
5+
<script>
6+
// Store the intended path and redirect to the app root.
7+
// The app reads sessionStorage on load and restores the route.
8+
sessionStorage.setItem("syskit-redirect", location.pathname);
9+
</script>
10+
<meta http-equiv="refresh" content="0;URL='./'">
11+
</head>
12+
</html>

src/App.jsx

Lines changed: 107 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import IPInfoMap from "./components/IPInfoMap.jsx";
1010
import NsLookup from "./components/NsLookup.jsx";
1111
import EpochConverter from "./components/EpochConverter.jsx";
1212
import DnsPropagation from "./components/DnsPropagation.jsx";
13+
import UrlEncoder from "./components/UrlEncoder.jsx";
14+
import RegexTester from "./components/RegexTester.jsx";
1315
import Disclaimer from "./components/Disclaimer.jsx";
1416

1517
const TOOLS = [
@@ -23,66 +25,131 @@ const TOOLS = [
2325
{ id: "nslookup", label: "nslookup", glyph: "DNS", Component: NsLookup, badge: "DNS" },
2426
{ id: "epoch", label: "epoch", glyph: "ts", Component: EpochConverter, badge: "time" },
2527
{ id: "dnsprop", label: "DNS prop", glyph: "⇢", Component: DnsPropagation, badge: "checker" },
28+
{ id: "urlencode", label: "URL encode", glyph: "%20", Component: UrlEncoder, badge: "encoding" },
29+
{ id: "regex", label: "regex", glyph: ".*", Component: RegexTester, badge: "pattern" },
2630
{ id: "disclaimer", label: "Disclaimer", glyph: "§", Component: Disclaimer, badge: "legal" },
2731
];
2832

29-
const BASE = import.meta.env.BASE_URL; // "/syskit/" in prod, "/" in dev
30-
3133
function getToolFromPath() {
32-
const path = window.location.pathname;
33-
const relative = path.startsWith(BASE) ? path.slice(BASE.length) : path.replace(/^\//, "");
34-
const id = relative.split("/")[0];
34+
const segments = window.location.pathname.split("/").filter(Boolean);
35+
const id = segments[segments.length - 1] ?? "";
3536
return TOOLS.find((t) => t.id === id)?.id ?? "chmod";
3637
}
3738

38-
function ThemeToggle({ theme, onToggle }) {
39-
const isDark = theme === "dark";
39+
const THEME_OPTIONS = [
40+
{
41+
value: "light", label: "Light",
42+
icon: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>,
43+
},
44+
{
45+
value: "dark", label: "Dark",
46+
icon: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>,
47+
},
48+
{
49+
value: "system", label: "System",
50+
icon: <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>,
51+
},
52+
];
53+
54+
function ThemeToggle({ theme, onChange }) {
55+
const [open, setOpen] = useState(false);
56+
const [hovered, setHovered] = useState(null);
57+
const ref = useRef(null);
58+
59+
useEffect(() => {
60+
const handler = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
61+
document.addEventListener("mousedown", handler);
62+
return () => document.removeEventListener("mousedown", handler);
63+
}, []);
64+
65+
const current = THEME_OPTIONS.find((o) => o.value === theme);
66+
4067
return (
41-
<button
42-
onClick={onToggle}
43-
title={`Switch to ${isDark ? "light" : "dark"} theme`}
44-
style={{
45-
display: "flex", alignItems: "center", gap: 7,
46-
background: "none", border: "none", cursor: "pointer",
47-
padding: "2px 0",
48-
}}
49-
>
50-
<span style={{ fontSize: 16, lineHeight: 1, color: isDark ? "var(--text-faint)" : "var(--amber)", transition: "color 0.2s" }}></span>
51-
<span style={{
52-
display: "inline-flex", alignItems: "center",
53-
width: 40, height: 22, borderRadius: 11,
54-
background: isDark ? "var(--green-dim)" : "var(--surface-3)",
55-
border: "1px solid var(--border-2)",
56-
position: "relative", transition: "background 0.2s", flexShrink: 0,
57-
}}>
58-
<span style={{
59-
position: "absolute", left: isDark ? 20 : 2,
60-
width: 16, height: 16, borderRadius: "50%",
61-
background: isDark ? "var(--green)" : "var(--text-muted)",
62-
transition: "left 0.2s, background 0.2s", flexShrink: 0,
63-
}} />
64-
</span>
65-
<span style={{ fontSize: 16, lineHeight: 1, color: isDark ? "var(--blue)" : "var(--text-faint)", transition: "color 0.2s" }}></span>
66-
</button>
68+
<div ref={ref} style={{ position: "relative" }}>
69+
<button
70+
onClick={() => setOpen((v) => !v)}
71+
style={{
72+
display: "flex", alignItems: "center", gap: 6,
73+
padding: "5px 10px", height: 30,
74+
background: open ? "var(--surface-3)" : "var(--surface-2)",
75+
border: "1px solid var(--border)", borderRadius: 8,
76+
color: "var(--text-muted)", cursor: "pointer", transition: "all 0.15s",
77+
}}
78+
>
79+
{current?.icon}
80+
<span style={{ fontFamily: "var(--font-mono)", fontSize: "var(--xs)" }}>{current?.label}</span>
81+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"
82+
style={{ opacity: 0.5, transform: open ? "rotate(180deg)" : "none", transition: "transform 0.15s" }}>
83+
<polyline points="6 9 12 15 18 9"/>
84+
</svg>
85+
</button>
86+
87+
{open && (
88+
<div style={{
89+
position: "absolute", top: "calc(100% + 4px)", right: 0, zIndex: 300,
90+
background: "var(--surface)", border: "1px solid var(--border-2)",
91+
borderRadius: 10, overflow: "hidden", minWidth: 130,
92+
boxShadow: "0 8px 24px rgba(0,0,0,0.45)",
93+
}}>
94+
{THEME_OPTIONS.map((opt, i) => (
95+
<button
96+
key={opt.value}
97+
onMouseDown={() => { onChange(opt.value); setOpen(false); }}
98+
onMouseEnter={() => setHovered(opt.value)}
99+
onMouseLeave={() => setHovered(null)}
100+
style={{
101+
width: "100%", padding: "9px 14px", border: "none",
102+
borderBottom: i < THEME_OPTIONS.length - 1 ? "1px solid var(--border)" : "none",
103+
background: theme === opt.value ? "var(--green-bg)" : hovered === opt.value ? "var(--surface-3)" : "transparent",
104+
color: theme === opt.value ? "var(--green)" : hovered === opt.value ? "var(--text)" : "var(--text-muted)",
105+
fontFamily: "var(--font-mono)", fontSize: "var(--sm)",
106+
display: "flex", alignItems: "center", gap: 9,
107+
textAlign: "left", cursor: "pointer", transition: "background 0.1s, color 0.1s",
108+
}}
109+
>
110+
{opt.icon}
111+
{opt.label}
112+
</button>
113+
))}
114+
</div>
115+
)}
116+
</div>
67117
);
68118
}
69119

70120

71121
export default function App() {
72122
const [activeTool, setActiveTool] = useState(getToolFromPath);
73-
const [theme, setTheme] = useState(() => localStorage.getItem("syskit-theme") || "dark");
123+
const [theme, setTheme] = useState(() => localStorage.getItem("syskit-theme") || "system");
124+
125+
const resolveTheme = (t) =>
126+
t === "system"
127+
? (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light")
128+
: t;
74129

75130
useEffect(() => {
76-
document.documentElement.setAttribute("data-theme", theme);
77131
localStorage.setItem("syskit-theme", theme);
132+
document.documentElement.setAttribute("data-theme", resolveTheme(theme));
133+
}, [theme]);
134+
135+
// Re-apply when system preference changes (only relevant when theme === "system")
136+
useEffect(() => {
137+
const mq = window.matchMedia("(prefers-color-scheme: dark)");
138+
const handler = () => {
139+
if (theme === "system") {
140+
document.documentElement.setAttribute("data-theme", resolveTheme("system"));
141+
}
142+
};
143+
mq.addEventListener("change", handler);
144+
return () => mq.removeEventListener("change", handler);
78145
}, [theme]);
79146

80147
useEffect(() => {
81-
const path = window.location.pathname;
82-
const relative = path.startsWith(BASE) ? path.slice(BASE.length) : path.replace(/^\//, "");
83-
const current = relative.split("/")[0];
148+
const segments = window.location.pathname.split("/").filter(Boolean);
149+
const current = segments[segments.length - 1] ?? "";
84150
if (current !== activeTool) {
85-
history.pushState({}, "", BASE + activeTool);
151+
const base = segments.slice(0, -1).join("/");
152+
history.pushState({}, "", (base ? "/" + base : "") + "/" + activeTool);
86153
}
87154
}, [activeTool]);
88155

@@ -93,7 +160,6 @@ export default function App() {
93160
}, []);
94161

95162

96-
const toggleTheme = () => setTheme((t) => (t === "dark" ? "light" : "dark"));
97163

98164
const scrollRef = useRef(null);
99165
useEffect(() => { if (scrollRef.current) scrollRef.current.scrollTop = 0; }, [activeTool]);
@@ -163,7 +229,7 @@ export default function App() {
163229
{activeMeta?.badge}
164230
</span>
165231
<div style={{ flex: 1 }} />
166-
<ThemeToggle theme={theme} onToggle={toggleTheme} />
232+
<ThemeToggle theme={theme} onChange={setTheme} />
167233
</header>
168234

169235
{/* Scrollable content */}

src/components/RAIDCalculator.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export default function RAIDCalculator() {
9191
const [drives, setDrives] = useState(4);
9292
const [unit, setUnit] = useState("TB");
9393
const [driveSize, setDriveSize] = useState(4); // default 4 TB
94-
const [overhead, setOverhead] = useState(0.93);
94+
const [overhead, setOverhead] = useState(1);
9595

9696
// When unit changes, snap to nearest preset in new unit
9797
const handleUnitChange = (newUnit) => {

0 commit comments

Comments
 (0)