@run0/jiki-ui 0.1.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/dist/AIChatPanel.d.ts +26 -0
- package/dist/BrowserWindow.d.ts +22 -0
- package/dist/CodeEditor.d.ts +8 -0
- package/dist/FileExplorer.d.ts +9 -0
- package/dist/MobileTabBar.d.ts +28 -0
- package/dist/PanelToggle.d.ts +9 -0
- package/dist/Terminal.d.ts +11 -0
- package/dist/chat-theme.d.ts +11 -0
- package/dist/index.cjs +1742 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.mjs +1720 -0
- package/dist/index.mjs.map +1 -0
- package/dist/language-labels.d.ts +1 -0
- package/dist/shiki-languages.d.ts +1 -0
- package/dist/theme.d.ts +19 -0
- package/dist/types.d.ts +12 -0
- package/dist/use-shiki-highlighter.d.ts +7 -0
- package/dist/useMediaQuery.d.ts +1 -0
- package/package.json +52 -0
- package/src/AIChatPanel.tsx +577 -0
- package/src/BrowserWindow.tsx +370 -0
- package/src/CodeEditor.tsx +185 -0
- package/src/FileExplorer.tsx +157 -0
- package/src/MobileTabBar.tsx +133 -0
- package/src/PanelToggle.tsx +101 -0
- package/src/Terminal.tsx +178 -0
- package/src/chat-theme.ts +81 -0
- package/src/index.ts +33 -0
- package/src/language-labels.ts +26 -0
- package/src/shiki-languages.ts +27 -0
- package/src/theme.ts +144 -0
- package/src/types.ts +21 -0
- package/src/use-shiki-highlighter.ts +113 -0
- package/src/useMediaQuery.ts +17 -0
package/src/theme.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { AccentColor, TerminalLine } from "./types";
|
|
2
|
+
|
|
3
|
+
export interface EditorTheme {
|
|
4
|
+
saveButtonActive: string;
|
|
5
|
+
caret: string;
|
|
6
|
+
selection: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface TerminalTheme {
|
|
10
|
+
commandStyle: string;
|
|
11
|
+
promptColor: string;
|
|
12
|
+
spinnerBorder: string;
|
|
13
|
+
inputCaret: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const EDITOR_THEMES: Record<AccentColor, EditorTheme> = {
|
|
17
|
+
emerald: {
|
|
18
|
+
saveButtonActive:
|
|
19
|
+
"bg-emerald-500/20 text-emerald-400 hover:bg-emerald-500/30 border border-emerald-500/30",
|
|
20
|
+
caret: "caret-emerald-400",
|
|
21
|
+
selection: "selection:bg-emerald-500/20",
|
|
22
|
+
},
|
|
23
|
+
violet: {
|
|
24
|
+
saveButtonActive:
|
|
25
|
+
"bg-violet-500/20 text-violet-400 hover:bg-violet-500/30 border border-violet-500/30",
|
|
26
|
+
caret: "caret-violet-400",
|
|
27
|
+
selection: "selection:bg-violet-500/20",
|
|
28
|
+
},
|
|
29
|
+
orange: {
|
|
30
|
+
saveButtonActive:
|
|
31
|
+
"bg-orange-500/20 text-orange-400 hover:bg-orange-500/30 border border-orange-500/30",
|
|
32
|
+
caret: "caret-orange-400",
|
|
33
|
+
selection: "selection:bg-orange-500/20",
|
|
34
|
+
},
|
|
35
|
+
blue: {
|
|
36
|
+
saveButtonActive:
|
|
37
|
+
"bg-blue-500/20 text-blue-400 hover:bg-blue-500/30 border border-blue-500/30",
|
|
38
|
+
caret: "caret-blue-400",
|
|
39
|
+
selection: "selection:bg-blue-500/20",
|
|
40
|
+
},
|
|
41
|
+
pink: {
|
|
42
|
+
saveButtonActive:
|
|
43
|
+
"bg-pink-500/20 text-pink-400 hover:bg-pink-500/30 border border-pink-500/30",
|
|
44
|
+
caret: "caret-pink-400",
|
|
45
|
+
selection: "selection:bg-pink-500/20",
|
|
46
|
+
},
|
|
47
|
+
green: {
|
|
48
|
+
saveButtonActive:
|
|
49
|
+
"bg-green-500/20 text-green-400 hover:bg-green-500/30 border border-green-500/30",
|
|
50
|
+
caret: "caret-green-400",
|
|
51
|
+
selection: "selection:bg-green-500/20",
|
|
52
|
+
},
|
|
53
|
+
amber: {
|
|
54
|
+
saveButtonActive:
|
|
55
|
+
"bg-amber-500/20 text-amber-400 hover:bg-amber-500/30 border border-amber-500/30",
|
|
56
|
+
caret: "caret-amber-400",
|
|
57
|
+
selection: "selection:bg-amber-500/20",
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const TERMINAL_THEMES: Record<AccentColor, TerminalTheme> = {
|
|
62
|
+
emerald: {
|
|
63
|
+
commandStyle: "text-emerald-400 font-semibold",
|
|
64
|
+
promptColor: "text-emerald-500",
|
|
65
|
+
spinnerBorder: "border-t-emerald-400",
|
|
66
|
+
inputCaret: "caret-emerald-400",
|
|
67
|
+
},
|
|
68
|
+
violet: {
|
|
69
|
+
commandStyle: "text-violet-400 font-semibold",
|
|
70
|
+
promptColor: "text-violet-500",
|
|
71
|
+
spinnerBorder: "border-t-violet-400",
|
|
72
|
+
inputCaret: "caret-violet-400",
|
|
73
|
+
},
|
|
74
|
+
orange: {
|
|
75
|
+
commandStyle: "text-orange-400 font-semibold",
|
|
76
|
+
promptColor: "text-orange-500",
|
|
77
|
+
spinnerBorder: "border-t-orange-400",
|
|
78
|
+
inputCaret: "caret-orange-400",
|
|
79
|
+
},
|
|
80
|
+
blue: {
|
|
81
|
+
commandStyle: "text-blue-400 font-semibold",
|
|
82
|
+
promptColor: "text-blue-500",
|
|
83
|
+
spinnerBorder: "border-t-blue-400",
|
|
84
|
+
inputCaret: "caret-blue-400",
|
|
85
|
+
},
|
|
86
|
+
pink: {
|
|
87
|
+
commandStyle: "text-pink-400 font-semibold",
|
|
88
|
+
promptColor: "text-pink-500",
|
|
89
|
+
spinnerBorder: "border-t-pink-400",
|
|
90
|
+
inputCaret: "caret-pink-400",
|
|
91
|
+
},
|
|
92
|
+
green: {
|
|
93
|
+
commandStyle: "text-green-400 font-semibold",
|
|
94
|
+
promptColor: "text-green-500",
|
|
95
|
+
spinnerBorder: "border-t-green-400",
|
|
96
|
+
inputCaret: "caret-green-400",
|
|
97
|
+
},
|
|
98
|
+
amber: {
|
|
99
|
+
commandStyle: "text-amber-400 font-semibold",
|
|
100
|
+
promptColor: "text-amber-500",
|
|
101
|
+
spinnerBorder: "border-t-amber-400",
|
|
102
|
+
inputCaret: "caret-amber-400",
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export interface FileExplorerTheme {
|
|
107
|
+
selected: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const FILE_EXPLORER_THEMES: Record<AccentColor, FileExplorerTheme> = {
|
|
111
|
+
emerald: { selected: "bg-emerald-500/15 text-emerald-300" },
|
|
112
|
+
violet: { selected: "bg-violet-500/15 text-violet-300" },
|
|
113
|
+
orange: { selected: "bg-orange-500/15 text-orange-300" },
|
|
114
|
+
blue: { selected: "bg-blue-500/15 text-blue-300" },
|
|
115
|
+
pink: { selected: "bg-pink-500/15 text-pink-300" },
|
|
116
|
+
green: { selected: "bg-green-500/15 text-green-300" },
|
|
117
|
+
amber: { selected: "bg-amber-500/15 text-amber-300" },
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export function getFileExplorerTheme(color: AccentColor): FileExplorerTheme {
|
|
121
|
+
return FILE_EXPLORER_THEMES[color];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function getEditorTheme(color: AccentColor): EditorTheme {
|
|
125
|
+
return EDITOR_THEMES[color];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function getTerminalTheme(color: AccentColor): TerminalTheme {
|
|
129
|
+
return TERMINAL_THEMES[color];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function getLineStyles(
|
|
133
|
+
color: AccentColor,
|
|
134
|
+
overrides?: Partial<Record<TerminalLine["type"], string>>,
|
|
135
|
+
): Record<TerminalLine["type"], string> {
|
|
136
|
+
return {
|
|
137
|
+
command: TERMINAL_THEMES[color].commandStyle,
|
|
138
|
+
stdout: "text-zinc-300",
|
|
139
|
+
stderr: "text-red-400",
|
|
140
|
+
info: "text-blue-400 italic",
|
|
141
|
+
success: "text-emerald-400",
|
|
142
|
+
...overrides,
|
|
143
|
+
};
|
|
144
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface TerminalLine {
|
|
2
|
+
id: number;
|
|
3
|
+
type: "command" | "stdout" | "stderr" | "info" | "success";
|
|
4
|
+
text: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type AccentColor =
|
|
8
|
+
| "emerald"
|
|
9
|
+
| "violet"
|
|
10
|
+
| "orange"
|
|
11
|
+
| "blue"
|
|
12
|
+
| "pink"
|
|
13
|
+
| "green"
|
|
14
|
+
| "amber";
|
|
15
|
+
|
|
16
|
+
export interface FileEntry {
|
|
17
|
+
name: string;
|
|
18
|
+
path: string;
|
|
19
|
+
isDir: boolean;
|
|
20
|
+
children?: FileEntry[];
|
|
21
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import type { Highlighter } from "shiki";
|
|
3
|
+
import { getShikiLang } from "./shiki-languages";
|
|
4
|
+
|
|
5
|
+
// Module-level singleton — shared across all CodeEditor instances
|
|
6
|
+
let highlighterInstance: Highlighter | null = null;
|
|
7
|
+
let highlighterPromise: Promise<Highlighter> | null = null;
|
|
8
|
+
|
|
9
|
+
async function getOrCreateHighlighter(): Promise<Highlighter> {
|
|
10
|
+
if (highlighterInstance) return highlighterInstance;
|
|
11
|
+
if (highlighterPromise) return highlighterPromise;
|
|
12
|
+
|
|
13
|
+
highlighterPromise = import("shiki")
|
|
14
|
+
.then(({ createHighlighter }) =>
|
|
15
|
+
createHighlighter({ themes: ["github-dark"], langs: [] }),
|
|
16
|
+
)
|
|
17
|
+
.then(h => {
|
|
18
|
+
highlighterInstance = h;
|
|
19
|
+
return h;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
return highlighterPromise;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function escapeHtml(text: string): string {
|
|
26
|
+
return text
|
|
27
|
+
.replace(/&/g, "&")
|
|
28
|
+
.replace(/</g, "<")
|
|
29
|
+
.replace(/>/g, ">");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function buildFallbackHtml(code: string): string {
|
|
33
|
+
return `<pre style="margin:0;background:transparent"><code style="color:#e4e4e7">${escapeHtml(code)}</code></pre>`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function doHighlight(
|
|
37
|
+
code: string,
|
|
38
|
+
filename: string | null,
|
|
39
|
+
el: HTMLDivElement | null,
|
|
40
|
+
): Promise<void> {
|
|
41
|
+
if (!el || !highlighterInstance) return;
|
|
42
|
+
|
|
43
|
+
const lang = getShikiLang(filename);
|
|
44
|
+
|
|
45
|
+
if (
|
|
46
|
+
lang !== "plaintext" &&
|
|
47
|
+
!highlighterInstance.getLoadedLanguages().includes(lang)
|
|
48
|
+
) {
|
|
49
|
+
try {
|
|
50
|
+
await highlighterInstance.loadLanguage(
|
|
51
|
+
lang as Parameters<typeof highlighterInstance.loadLanguage>[0],
|
|
52
|
+
);
|
|
53
|
+
} catch {
|
|
54
|
+
el.innerHTML = buildFallbackHtml(code);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
el.innerHTML = highlighterInstance.codeToHtml(code, {
|
|
61
|
+
lang,
|
|
62
|
+
theme: "github-dark",
|
|
63
|
+
});
|
|
64
|
+
} catch {
|
|
65
|
+
el.innerHTML = buildFallbackHtml(code);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Manages Shiki syntax highlighting via direct DOM updates — no React state,
|
|
71
|
+
* no re-renders. Updates the target element's innerHTML directly.
|
|
72
|
+
*/
|
|
73
|
+
export function useShikiHighlighter(
|
|
74
|
+
code: string,
|
|
75
|
+
filename: string | null,
|
|
76
|
+
targetRef: React.RefObject<HTMLDivElement | null>,
|
|
77
|
+
): void {
|
|
78
|
+
const readyRef = useRef(false);
|
|
79
|
+
const debounceRef = useRef<ReturnType<typeof setTimeout> | undefined>(
|
|
80
|
+
undefined,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Initialize highlighter singleton on first mount
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
let cancelled = false;
|
|
86
|
+
getOrCreateHighlighter().then(() => {
|
|
87
|
+
if (cancelled) return;
|
|
88
|
+
readyRef.current = true;
|
|
89
|
+
doHighlight(code, filename, targetRef.current);
|
|
90
|
+
});
|
|
91
|
+
return () => {
|
|
92
|
+
cancelled = true;
|
|
93
|
+
};
|
|
94
|
+
}, []);
|
|
95
|
+
|
|
96
|
+
// Debounced highlight on code/filename change — direct DOM mutation
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (!readyRef.current) {
|
|
99
|
+
// Show fallback while Shiki loads
|
|
100
|
+
if (targetRef.current) {
|
|
101
|
+
targetRef.current.innerHTML = buildFallbackHtml(code);
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
clearTimeout(debounceRef.current);
|
|
107
|
+
debounceRef.current = setTimeout(() => {
|
|
108
|
+
doHighlight(code, filename, targetRef.current);
|
|
109
|
+
}, 30);
|
|
110
|
+
|
|
111
|
+
return () => clearTimeout(debounceRef.current);
|
|
112
|
+
}, [code, filename]);
|
|
113
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
export function useMediaQuery(query: string): boolean {
|
|
4
|
+
const [matches, setMatches] = useState(() => {
|
|
5
|
+
if (typeof window === "undefined") return false;
|
|
6
|
+
return window.matchMedia(query).matches;
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
const mql = window.matchMedia(query);
|
|
11
|
+
const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
|
|
12
|
+
mql.addEventListener("change", handler);
|
|
13
|
+
return () => mql.removeEventListener("change", handler);
|
|
14
|
+
}, [query]);
|
|
15
|
+
|
|
16
|
+
return matches;
|
|
17
|
+
}
|