CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / integration (push) Successful in 17s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python tests (push) Successful in 48s
CI & Build / Build & push image (push) Successful in 37s
#2533. theme.css claimed "removing this block is a rename sweep across the
components, tracked separately" — written in 67a529a, never filed, which made
the comment itself an instance of the survey's presence-without-reference
pattern. This is that sweep.
73 alias declarations deleted; 69 files rewritten; every --color-*-style name
now references its --fs-* token directly. Mechanical by construction: the map
IS the alias block, applied longest-name-first with a boundary guard so
--color-text never matched inside --color-text-muted. Zero survivors outside
theme.css, verified by grep rather than assumed.
One deliberate survivor: --color-shadow stays DECLARED, because it was never
an alias — it is a literal value the design system has no token for. Marked
in place as a recorded gap: promote it to an --fs-* token when a second app
needs it, don't copy the line.
Nothing is lost mode-wise: the aliases' resolve-at-use-time trick (which
absorbed 48 dark-mode overrides) lives one layer down in the --fs-* tokens'
own derivations, which is why the sweep is a pure rename. Both CSS checkers
green.
Why now rather than never: check_snippets_against_design_system reports every
--color-* reference as "unknown — renders as NOTHING", and nine recipe
snippets recorded from components.css carried the deprecated names, making
them prior art pointing the wrong way. With the sweep in, the checker's
report over re-recorded snippets should be EMPTY — the acceptance test that
proves the checker was right all along (#2517's correction).
Refs #2533
398 lines
11 KiB
Vue
398 lines
11 KiB
Vue
<script setup lang="ts">
|
|
import { onMounted, onUnmounted, ref, watch } from "vue";
|
|
import { useRouter } from "vue-router";
|
|
import AppHeader from "@/components/AppHeader.vue";
|
|
import ToastNotification from "@/components/ToastNotification.vue";
|
|
import { useTheme } from "@/composables/useTheme";
|
|
import { useShortcuts } from "@/composables/useShortcuts";
|
|
import { useAuthStore } from "@/stores/auth";
|
|
import { useSettingsStore } from "@/stores/settings";
|
|
import { apiGet, apiPut } from "@/api/client";
|
|
|
|
useTheme();
|
|
|
|
const router = useRouter();
|
|
const appVersion = ref("dev");
|
|
const authStore = useAuthStore();
|
|
const settingsStore = useSettingsStore();
|
|
const { showShortcuts, toggleShortcuts, closeShortcuts } = useShortcuts();
|
|
|
|
function startAppServices() {
|
|
settingsStore.fetchSettings();
|
|
// Sync browser timezone to the server on every login/page load.
|
|
apiPut("/api/settings", { user_timezone: Intl.DateTimeFormat().resolvedOptions().timeZone }).catch(() => {});
|
|
}
|
|
|
|
function stopAppServices() {
|
|
// no-op for now; kept as a hook for future per-session teardown
|
|
}
|
|
|
|
function isInputActive(): boolean {
|
|
const el = document.activeElement;
|
|
if (!el) return false;
|
|
const tag = (el as HTMLElement).tagName;
|
|
return tag === "INPUT" || tag === "TEXTAREA" || (el as HTMLElement).isContentEditable;
|
|
}
|
|
|
|
// Sequence shortcut support (e.g. g+n, g+t)
|
|
let pendingPrefix: string | null = null;
|
|
let prefixTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
function clearPrefix() {
|
|
pendingPrefix = null;
|
|
if (prefixTimeout) clearTimeout(prefixTimeout);
|
|
prefixTimeout = null;
|
|
}
|
|
|
|
function onGlobalKeydown(e: KeyboardEvent) {
|
|
if (!authStore.isAuthenticated) return;
|
|
|
|
// ? — toggle shortcuts overlay (only when not typing)
|
|
if (e.key === "?" && !isInputActive() && !e.ctrlKey && !e.metaKey) {
|
|
toggleShortcuts();
|
|
return;
|
|
}
|
|
|
|
// Escape — progressive: close overlay → unfocus field → go home
|
|
if (e.key === "Escape") {
|
|
clearPrefix();
|
|
if (showShortcuts.value) {
|
|
closeShortcuts();
|
|
return;
|
|
}
|
|
if (isInputActive()) {
|
|
(document.activeElement as HTMLElement).blur();
|
|
return;
|
|
}
|
|
router.push("/");
|
|
return;
|
|
}
|
|
|
|
// All remaining shortcuts: ignore when typing or using modifier keys
|
|
if (isInputActive() || e.ctrlKey || e.metaKey || e.altKey) return;
|
|
|
|
// Complete a g+X sequence
|
|
if (pendingPrefix === "g") {
|
|
clearPrefix();
|
|
switch (e.key) {
|
|
case "h": router.push("/"); break;
|
|
case "n": router.push("/notes"); break;
|
|
case "t": router.push("/"); break;
|
|
case "p": router.push("/projects"); break;
|
|
case "r": router.push("/rules"); break;
|
|
case "g": router.push("/graph"); break;
|
|
case "x": router.push("/trash"); break;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Single-key shortcuts
|
|
switch (e.key) {
|
|
case "g":
|
|
pendingPrefix = "g";
|
|
prefixTimeout = setTimeout(clearPrefix, 1500);
|
|
break;
|
|
case "n":
|
|
router.push("/notes/new");
|
|
break;
|
|
case "t":
|
|
router.push("/tasks/new");
|
|
break;
|
|
case "e": {
|
|
const name = router.currentRoute.value.name;
|
|
if (name === "note-view") {
|
|
router.push(router.currentRoute.value.path + "/edit");
|
|
}
|
|
break;
|
|
}
|
|
case "/":
|
|
e.preventDefault();
|
|
document.dispatchEvent(new CustomEvent("shortcut:focus-search"));
|
|
break;
|
|
}
|
|
}
|
|
|
|
onMounted(async () => {
|
|
document.addEventListener("keydown", onGlobalKeydown);
|
|
await authStore.checkAuth();
|
|
if (authStore.isAuthenticated) {
|
|
startAppServices();
|
|
}
|
|
try {
|
|
const data = await apiGet<{ version: string }>("/api/version");
|
|
appVersion.value = data.version;
|
|
} catch {
|
|
// silent — version display is non-critical
|
|
}
|
|
});
|
|
|
|
watch(
|
|
() => authStore.isAuthenticated,
|
|
(authenticated) => {
|
|
if (authenticated) {
|
|
startAppServices();
|
|
} else {
|
|
stopAppServices();
|
|
}
|
|
}
|
|
);
|
|
|
|
onUnmounted(() => {
|
|
document.removeEventListener("keydown", onGlobalKeydown);
|
|
stopAppServices();
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<template v-if="authStore.isAuthenticated">
|
|
<a href="#main-content" class="skip-link">Skip to main content</a>
|
|
<div class="app-shell">
|
|
<AppHeader />
|
|
<div id="main-content" class="app-content">
|
|
<router-view />
|
|
</div>
|
|
<footer class="app-footer">v{{ appVersion }}</footer>
|
|
</div>
|
|
|
|
<!-- Keyboard shortcuts overlay -->
|
|
<Transition name="shortcuts-fade">
|
|
<div v-if="showShortcuts" class="shortcuts-overlay" @click.self="closeShortcuts">
|
|
<div class="shortcuts-panel">
|
|
<div class="shortcuts-header">
|
|
<h3>Keyboard Shortcuts</h3>
|
|
<button class="shortcuts-close" aria-label="Close keyboard shortcuts" @click="closeShortcuts">×</button>
|
|
</div>
|
|
<div class="shortcuts-body">
|
|
<div class="shortcuts-section">
|
|
<div class="shortcuts-section-title">Navigation</div>
|
|
<div class="shortcut-row">
|
|
<kbd class="shortcut-key">g</kbd>
|
|
<span class="shortcut-key-sep">+</span>
|
|
<kbd class="shortcut-key">h</kbd>
|
|
<span class="shortcut-desc">Home</span>
|
|
</div>
|
|
<div class="shortcut-row">
|
|
<kbd class="shortcut-key">g</kbd>
|
|
<span class="shortcut-key-sep">+</span>
|
|
<kbd class="shortcut-key">n</kbd>
|
|
<span class="shortcut-desc">Notes</span>
|
|
</div>
|
|
<div class="shortcut-row">
|
|
<kbd class="shortcut-key">g</kbd>
|
|
<span class="shortcut-key-sep">+</span>
|
|
<kbd class="shortcut-key">t</kbd>
|
|
<span class="shortcut-desc">Knowledge (tasks)</span>
|
|
</div>
|
|
<div class="shortcut-row">
|
|
<kbd class="shortcut-key">g</kbd>
|
|
<span class="shortcut-key-sep">+</span>
|
|
<kbd class="shortcut-key">p</kbd>
|
|
<span class="shortcut-desc">Projects</span>
|
|
</div>
|
|
<div class="shortcut-row">
|
|
<kbd class="shortcut-key">g</kbd>
|
|
<span class="shortcut-key-sep">+</span>
|
|
<kbd class="shortcut-key">x</kbd>
|
|
<span class="shortcut-desc">Trash</span>
|
|
</div>
|
|
<div class="shortcut-row">
|
|
<kbd class="shortcut-key">Esc</kbd>
|
|
<span class="shortcut-desc">Unfocus field → go home</span>
|
|
</div>
|
|
<div class="shortcut-row">
|
|
<kbd class="shortcut-key">?</kbd>
|
|
<span class="shortcut-desc">Toggle this panel</span>
|
|
</div>
|
|
</div>
|
|
<div class="shortcuts-section">
|
|
<div class="shortcuts-section-title">Create</div>
|
|
<div class="shortcut-row">
|
|
<kbd class="shortcut-key">n</kbd>
|
|
<span class="shortcut-desc">New note</span>
|
|
</div>
|
|
<div class="shortcut-row">
|
|
<kbd class="shortcut-key">t</kbd>
|
|
<span class="shortcut-desc">New task</span>
|
|
</div>
|
|
</div>
|
|
<div class="shortcuts-section">
|
|
<div class="shortcuts-section-title">Lists</div>
|
|
<div class="shortcut-row">
|
|
<kbd class="shortcut-key">/</kbd>
|
|
<span class="shortcut-desc">Focus search bar</span>
|
|
</div>
|
|
<div class="shortcut-row">
|
|
<kbd class="shortcut-key">j</kbd>
|
|
<span class="shortcut-key-sep">/</span>
|
|
<kbd class="shortcut-key">k</kbd>
|
|
<span class="shortcut-desc">Move down / up</span>
|
|
</div>
|
|
<div class="shortcut-row">
|
|
<kbd class="shortcut-key">Enter</kbd>
|
|
<span class="shortcut-desc">Open selected item</span>
|
|
</div>
|
|
<div class="shortcut-row">
|
|
<kbd class="shortcut-key">e</kbd>
|
|
<span class="shortcut-desc">Edit current item (viewer)</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</template>
|
|
<template v-else>
|
|
<router-view />
|
|
</template>
|
|
<ToastNotification />
|
|
</template>
|
|
|
|
<style>
|
|
.skip-link {
|
|
position: absolute;
|
|
top: -100%;
|
|
left: 0.5rem;
|
|
z-index: 9999;
|
|
padding: 0.4rem 0.75rem;
|
|
background: var(--fs-accent);
|
|
color: var(--fs-text-on-action);
|
|
border-radius: 0 0 4px 4px;
|
|
font-size: 0.875rem;
|
|
text-decoration: none;
|
|
transition: top 0.1s;
|
|
}
|
|
.skip-link:focus {
|
|
top: 0;
|
|
}
|
|
|
|
.app-shell {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100vh;
|
|
height: 100dvh;
|
|
overflow: hidden;
|
|
}
|
|
.app-shell > .app-header {
|
|
flex-shrink: 0;
|
|
}
|
|
.app-content {
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow-y: auto;
|
|
}
|
|
.app-content:has(.knowledge-root) {
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Version footer */
|
|
.app-footer {
|
|
flex-shrink: 0;
|
|
text-align: center;
|
|
padding: 0.2rem 0;
|
|
font-size: 0.68rem;
|
|
color: var(--fs-text-tertiary);
|
|
opacity: 0.45;
|
|
user-select: none;
|
|
letter-spacing: 0.03em;
|
|
}
|
|
|
|
/* Shortcuts overlay */
|
|
.shortcuts-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: var(--fs-overlay);
|
|
z-index: 9000;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.shortcuts-panel {
|
|
background: var(--fs-surface-raised);
|
|
border: 1px solid var(--fs-border-color);
|
|
border-radius: var(--fs-radius-lg);
|
|
box-shadow: 0 8px 32px var(--color-shadow);
|
|
width: min(420px, 92vw);
|
|
overflow: hidden;
|
|
}
|
|
.shortcuts-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 0.85rem 1rem 0.75rem;
|
|
border-bottom: 1px solid var(--fs-border-color);
|
|
}
|
|
.shortcuts-header h3 {
|
|
margin: 0;
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
color: var(--fs-text-primary);
|
|
}
|
|
.shortcuts-close {
|
|
background: none;
|
|
border: none;
|
|
font-size: 1.4rem;
|
|
line-height: 1;
|
|
color: var(--fs-text-tertiary);
|
|
cursor: pointer;
|
|
padding: 0 0.25rem;
|
|
}
|
|
.shortcuts-close:hover {
|
|
color: var(--fs-text-primary);
|
|
}
|
|
.shortcuts-body {
|
|
padding: 0.75rem 1rem 1rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
}
|
|
.shortcuts-section-title {
|
|
font-size: 0.72rem;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
color: var(--fs-text-tertiary);
|
|
margin-bottom: 0.4rem;
|
|
}
|
|
.shortcut-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.35rem;
|
|
padding: 0.2rem 0;
|
|
}
|
|
.shortcut-key {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-width: 1.8rem;
|
|
padding: 0.15rem 0.4rem;
|
|
background: var(--fs-surface-raised);
|
|
border: 1px solid var(--fs-border-color);
|
|
border-bottom-width: 2px;
|
|
border-radius: 4px;
|
|
font-size: 0.78rem;
|
|
font-family: ui-monospace, monospace;
|
|
color: var(--fs-text-primary);
|
|
white-space: nowrap;
|
|
user-select: none;
|
|
}
|
|
.shortcut-key-sep {
|
|
font-size: 0.78rem;
|
|
color: var(--fs-text-tertiary);
|
|
}
|
|
.shortcut-desc {
|
|
font-size: 0.875rem;
|
|
color: var(--fs-text-primary);
|
|
margin-left: 0.25rem;
|
|
}
|
|
|
|
/* Transition */
|
|
.shortcuts-fade-enter-active,
|
|
.shortcuts-fade-leave-active {
|
|
transition: opacity 0.15s ease;
|
|
}
|
|
.shortcuts-fade-enter-from,
|
|
.shortcuts-fade-leave-to {
|
|
opacity: 0;
|
|
}
|
|
</style>
|