Initial Commit

This commit is contained in:
2026-04-04 23:22:30 -04:00
commit 246f7357a9
61 changed files with 4792 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
import { writable } from 'svelte/store';
export type User = {
id: string;
username: string;
};
export const userStore = writable<User | null>(null);
export async function fetchUser() {
try {
const res = await fetch('/api/auth/me');
if (res.ok) {
const user = await res.json();
userStore.set(user);
} else {
userStore.set(null);
}
} catch {
userStore.set(null);
}
}
+39
View File
@@ -0,0 +1,39 @@
import { writable } from 'svelte/store';
export const themeStore = writable('Catppuccin');
export const darkModeStore = writable(true);
export const connectionStatus = writable('connecting');
export const editorViewStore = writable<any>(null);
export const documentZoomStore = writable(100);
export interface AwarenessUser {
clientId: number;
name: string;
color: string;
colorLight: string;
isLocal?: boolean;
}
export const connectedUsers = writable<AwarenessUser[]>([]);
if (typeof window !== 'undefined') {
const savedTheme = localStorage.getItem('editor-theme');
const savedDark = localStorage.getItem('editor-dark-mode');
const savedZoom = localStorage.getItem('editor-document-zoom');
if (savedTheme) themeStore.set(savedTheme);
if (savedDark !== null) darkModeStore.set(savedDark === 'true');
if (savedZoom !== null) documentZoomStore.set(parseInt(savedZoom, 10));
themeStore.subscribe(value => localStorage.setItem('editor-theme', value));
darkModeStore.subscribe(value => {
localStorage.setItem('editor-dark-mode', value.toString());
if (value) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
});
documentZoomStore.subscribe(value => localStorage.setItem('editor-document-zoom', value.toString()));
}
+143
View File
@@ -0,0 +1,143 @@
import { EditorView } from '@codemirror/view';
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { tags as t } from '@lezer/highlight';
export interface ThemeColors {
background: string;
text: string;
selection: string;
cursor: string;
keyword: string;
string: string;
number: string;
comment: string;
variable: string;
function: string;
}
export interface ThemeConfig {
icon: string;
dark: ThemeColors;
light: ThemeColors;
}
export const themes: Record<string, ThemeConfig> = {
Cerberus: {
icon: "mdi:dog",
dark: {
background: "#171717", text: "#f5f5f5", selection: "#262626", cursor: "#f5f5f5",
keyword: "#e879f9", string: "#2dd4bf", number: "#fbbf24", comment: "#737373", variable: "#f5f5f5", function: "#818cf8"
},
light: {
background: "#ffffff", text: "#171717", selection: "#f5f5f5", cursor: "#171717",
keyword: "#c026d3", string: "#0d9488", number: "#d97706", comment: "#525252", variable: "#171717", function: "#4f46e5"
}
},
Catppuccin: {
icon: "mdi:cat",
dark: {
background: "#1e1e2e", text: "#cdd6f4", selection: "#313244", cursor: "#f5e0dc",
keyword: "#cba6f7", string: "#a6e3a1", number: "#fab387", comment: "#6c7086", variable: "#cdd6f4", function: "#89b4fa"
},
light: {
background: "#eff1f5", text: "#4c4f69", selection: "#e6e9ef", cursor: "#dc8a78",
keyword: "#8839ef", string: "#40a02b", number: "#fe640b", comment: "#9ca0b0", variable: "#4c4f69", function: "#1e66f5"
}
},
"Arch Linux": {
icon: "mdi:penguin",
dark: {
background: "#0d1117", text: "#c9d1d9", selection: "#21262d", cursor: "#c9d1d9",
keyword: "#bc8cff", string: "#3fb950", number: "#ffa657", comment: "#6e7681", variable: "#c9d1d9", function: "#1793d1"
},
light: {
background: "#ffffff", text: "#24292f", selection: "#f6f8fa", cursor: "#24292f",
keyword: "#8250df", string: "#1a7f37", number: "#bc4c00", comment: "#6e7781", variable: "#24292f", function: "#1793d1"
}
}
};
export function getThemeExtension(themeName: keyof typeof themes, isDark: boolean) {
const colors = themes[themeName][isDark ? 'dark' : 'light'];
const theme = EditorView.theme({
"&": {
color: colors.text,
backgroundColor: colors.background,
height: "100%",
fontSize: "14px"
},
".cm-content": {
caretColor: colors.cursor
},
".cm-cursor, .cm-dropCursor": { borderLeftColor: colors.cursor },
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": { backgroundColor: colors.selection },
".cm-panels": { backgroundColor: colors.background, color: colors.text },
".cm-panels.cm-panels-top": { borderBottom: "2px solid black" },
".cm-panels.cm-panels-bottom": { borderTop: "2px solid black" },
".cm-searchMatch": {
backgroundColor: "#72a1ff59",
outline: "1px solid #457dff"
},
".cm-searchMatch.cm-searchMatch-selected": {
backgroundColor: "#6199ff2f"
},
".cm-activeLine": { backgroundColor: colors.selection },
".cm-selectionMatch": { backgroundColor: "#aafe661a" },
"&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket": {
backgroundColor: "#bad0f847"
},
".cm-gutters": {
backgroundColor: colors.background,
color: colors.comment,
border: "none"
},
".cm-activeLineGutter": {
backgroundColor: colors.selection
},
".cm-foldPlaceholder": {
backgroundColor: "transparent",
border: "none",
color: "#ddd"
},
".cm-tooltip": {
border: "none",
backgroundColor: colors.background
},
".cm-tooltip .cm-tooltip-arrow:before": {
borderTopColor: "transparent",
borderBottomColor: "transparent"
},
".cm-tooltip .cm-tooltip-arrow:after": {
borderTopColor: colors.background,
borderBottomColor: colors.background
},
".cm-tooltip-autocomplete": {
"& > ul > li[aria-selected]": {
backgroundColor: colors.selection,
color: colors.text
}
}
}, { dark: isDark });
const highlightStyle = HighlightStyle.define([
{ tag: t.keyword, color: colors.keyword },
{ tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], color: colors.variable },
{ tag: [t.function(t.variableName), t.labelName], color: colors.function },
{ tag: [t.color, t.constant(t.name), t.standard(t.name)], color: colors.function },
{ tag: [t.definition(t.name), t.separator], color: colors.variable },
{ tag: [t.typeName, t.className, t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: colors.number },
{ tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.link, t.special(t.string)], color: colors.keyword },
{ tag: [t.meta, t.comment], color: colors.comment },
{ tag: t.strong, fontWeight: "bold" },
{ tag: t.emphasis, fontStyle: "italic" },
{ tag: t.strikethrough, textDecoration: "line-through" },
{ tag: t.link, color: colors.comment, textDecoration: "underline" },
{ tag: t.heading, fontWeight: "bold", color: colors.function },
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: colors.number },
{ tag: [t.processingInstruction, t.string, t.inserted], color: colors.string },
{ tag: t.invalid, color: "#ff0000" },
]);
return [theme, syntaxHighlighting(highlightStyle)];
}
+46
View File
@@ -0,0 +1,46 @@
export interface Diagnostic {
message: string;
severity: string;
}
export interface CompileResponse {
svgs: string[] | null;
errors: Diagnostic[] | null;
}
export async function compileTypst(text: string, document_id?: string): Promise<CompileResponse> {
const res = await fetch('/api/compile', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, document_id }),
});
return await res.json();
}
export function exportTypst(text: string, format: 'pdf' | 'png' | 'svg', title: string = 'document', document_id?: string) {
const form = document.createElement('form');
form.method = 'POST';
form.action = `/api/export/${format}`;
form.target = '_blank';
return fetch(`/api/export/${format}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, document_id }),
})
.then((res) => {
if (!res.ok) throw new Error('Export failed');
return res.blob();
})
.then((blob) => {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${title}.${format}`;
a.click();
URL.revokeObjectURL(url);
});
}
+97
View File
@@ -0,0 +1,97 @@
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
import { get } from 'svelte/store';
import { userStore } from './auth';
import { connectionStatus, connectedUsers } from './store';
import type { AwarenessUser } from './store';
export let doc: Y.Doc | null = null;
export let text: Y.Text | null = null;
export let provider: WebsocketProvider | null = null;
const userColors = [
'#30bced', '#6eeb83', '#ffbc42', '#ecd444', '#ee6352',
'#9ac2c9', '#8acb88', '#1be7ff', '#ff0054', '#9e0059'
];
export function initYjs(docId: string) {
if (typeof window === 'undefined') return;
if (provider) {
provider.disconnect();
provider = null;
}
doc = new Y.Doc();
text = doc.getText('typst');
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const host = window.location.host;
connectionStatus.set('connecting');
provider = new WebsocketProvider(
`${protocol}//${host}/yjs`,
docId,
doc
);
const user = get(userStore);
const color = userColors[Math.floor(Math.random() * userColors.length)];
provider.awareness.setLocalStateField('user', {
name: user?.username || 'Anonymous',
color: color,
colorLight: color + '33'
});
provider.on('status', (event: { status: string }) => {
connectionStatus.set(event.status);
console.log(`Yjs connection status for ${docId}:`, event.status);
});
provider.awareness.on('change', () => {
if (!provider) return;
const states = provider.awareness.getStates();
const localId = provider.awareness.clientID;
const uniqueUsers = new Map<string, AwarenessUser>();
states.forEach((state, clientId) => {
if (state.user) {
const isLocal = clientId === localId;
const userObj = {
clientId,
...state.user,
isLocal
};
if (isLocal) {
uniqueUsers.set(state.user.name, userObj);
} else if (!uniqueUsers.has(state.user.name) || !uniqueUsers.get(state.user.name)!.isLocal) {
uniqueUsers.set(state.user.name, userObj);
}
}
});
connectedUsers.set(Array.from(uniqueUsers.values()));
});
}
export function cleanupYjs() {
if (provider) {
provider.disconnect();
provider = null;
}
doc = null;
text = null;
connectionStatus.set('disconnected');
connectedUsers.set([]);
}