feat: add pinned allocator lesson view
This commit is contained in:
+29
-1
@@ -27,9 +27,15 @@ dlatego nie należy wystawiać go bezpośrednio poza zaufany host.
|
|||||||
Hierarchia nawigacji jest stała:
|
Hierarchia nawigacji jest stała:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
task → block → phase → step → snapshot
|
card → task → block → phase → step → snapshot
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Ruch kursora przez `move` lub `PUT current` nie uruchamia symulatora.
|
||||||
|
Jawna aktywacja odbywa się przez `Enter` w WWW albo `F2` w WWW/Neovimie,
|
||||||
|
które wywołują `POST /api/navigation/sync`. Dla poziomu BLOCK backend wybiera
|
||||||
|
pierwszy STEP tego bloku. CARD i TASK są odrzucane, ponieważ nie wskazują
|
||||||
|
konkretnego checkpointu diagramu.
|
||||||
|
|
||||||
| Metoda i endpoint | Znaczenie |
|
| Metoda i endpoint | Znaczenie |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `GET /api/keyboard` | Skróty i obsługiwane poziomy nawigacji. |
|
| `GET /api/keyboard` | Skróty i obsługiwane poziomy nawigacji. |
|
||||||
@@ -37,6 +43,7 @@ task → block → phase → step → snapshot
|
|||||||
| `GET /api/navigation/current` | Bieżąca pozycja oraz status checkpointu. |
|
| `GET /api/navigation/current` | Bieżąca pozycja oraz status checkpointu. |
|
||||||
| `PUT /api/navigation/current` | Ustawia dokładną pozycję i poziom fokusu. |
|
| `PUT /api/navigation/current` | Ustawia dokładną pozycję i poziom fokusu. |
|
||||||
| `POST /api/navigation/move` | Przesuwa kursor o `delta` na wybranym poziomie. |
|
| `POST /api/navigation/move` | Przesuwa kursor o `delta` na wybranym poziomie. |
|
||||||
|
| `POST /api/navigation/sync` | Odtwarza checkpoint przypisany do bieżącej, zapisanej pozycji. |
|
||||||
|
|
||||||
Przykład ustawienia kroku — identyfikatory są danymi konkretnej karty:
|
Przykład ustawienia kroku — identyfikatory są danymi konkretnej karty:
|
||||||
|
|
||||||
@@ -81,6 +88,13 @@ Wywołanie orkiestratora przypina zarówno kontener, jak i rewizję karty:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Backend przechowuje bieżącą nawigację i stan viewera w SQLite. Domyślna baza
|
||||||
|
to `.stem/card-state.sqlite3`; ścieżkę można zmienić przez
|
||||||
|
`CARD_STATE_DATABASE`. Ta sama aplikacja wystawia HTTP również przez lokalny
|
||||||
|
socket `.stem/card-api.sock` (`CARD_API_SOCKET`). Repozytorium jest montowane w
|
||||||
|
kontenerze jako `/workspace`, dlatego Neovim wywołuje API przez
|
||||||
|
`/workspace/.stem/card-api.sock` bez otwierania dodatkowego portu sieciowego.
|
||||||
|
|
||||||
Replay kończy się powodzeniem wyłącznie ze statusem `ready`. Zmiana kontenera,
|
Replay kończy się powodzeniem wyłącznie ze statusem `ready`. Zmiana kontenera,
|
||||||
socketu MCP albo źródłowego JSON-a podczas operacji powoduje odrzucenie
|
socketu MCP albo źródłowego JSON-a podczas operacji powoduje odrzucenie
|
||||||
wyniku; pozycja strony i `SYNC ON` nie są wtedy publikowane.
|
wyniku; pozycja strony i `SYNC ON` nie są wtedy publikowane.
|
||||||
@@ -93,6 +107,8 @@ wyniku; pozycja strony i `SYNC ON` nie są wtedy publikowane.
|
|||||||
| `GET /api/nvim/state` | Bieżący bufor, okno, kursor i tryb Neovima. |
|
| `GET /api/nvim/state` | Bieżący bufor, okno, kursor i tryb Neovima. |
|
||||||
| `PUT /api/nvim/cursor` | Ustawia `row`, `column` i opcjonalnie `window`. |
|
| `PUT /api/nvim/cursor` | Ustawia `row`, `column` i opcjonalnie `window`. |
|
||||||
| `POST /api/nvim/input` | Przekazuje do 256 znaków przez `nvim_input`. |
|
| `POST /api/nvim/input` | Przekazuje do 256 znaków przez `nvim_input`. |
|
||||||
|
| `POST /api/nvim/reset` | Resetuje cel i odświeża debugger (`F1`). |
|
||||||
|
| `POST /api/nvim/redraw` | Dopasowuje host pane i grid UI, a następnie wykonuje pełny redraw (`Ctrl+\``). |
|
||||||
| `WS /api/nvim-ui` | Strumień zewnętrznego UI Neovima do panelu WWW. |
|
| `WS /api/nvim-ui` | Strumień zewnętrznego UI Neovima do panelu WWW. |
|
||||||
|
|
||||||
Pole `viewer.scroll` zapisuje położenie dokumentu (`x`, `y`), numer strony
|
Pole `viewer.scroll` zapisuje położenie dokumentu (`x`, `y`), numer strony
|
||||||
@@ -104,6 +120,10 @@ Wyłączenie `viewer.nvim.sync` automatycznie wyłącza również
|
|||||||
`viewer.nvim.control`. Samo ponowne włączenie synchronizacji nie uzbraja
|
`viewer.nvim.control`. Samo ponowne włączenie synchronizacji nie uzbraja
|
||||||
sterowania klawiaturą bez jawnego `control: true`.
|
sterowania klawiaturą bez jawnego `control: true`.
|
||||||
|
|
||||||
|
`viewer.allocator.visible` przechowuje stan przypiętego modelu alokatora.
|
||||||
|
Model jest przełączany przez `Alt+6` i pokazuje fazę/krok, `allocp`, zajętość
|
||||||
|
areny oraz wartości związane z bieżącym checkpointem.
|
||||||
|
|
||||||
### Podział odpowiedzialności widoków
|
### Podział odpowiedzialności widoków
|
||||||
|
|
||||||
Interaktywny diagram UML pokazuje przepływ i służy do wyboru kroku. Nie
|
Interaktywny diagram UML pokazuje przepływ i służy do wyboru kroku. Nie
|
||||||
@@ -119,6 +139,14 @@ diagramem.
|
|||||||
| `GET /api/progress` | Katalog, statusy i podsumowanie wykonania. |
|
| `GET /api/progress` | Katalog, statusy i podsumowanie wykonania. |
|
||||||
| `PUT /api/progress/:itemId` | Ustawia `pending` albo `approved`, notatkę i aktora. |
|
| `PUT /api/progress/:itemId` | Ustawia `pending` albo `approved`, notatkę i aktora. |
|
||||||
| `GET /api/report/teams.md` | Generuje bieżący raport Markdown do Teams. |
|
| `GET /api/report/teams.md` | Generuje bieżący raport Markdown do Teams. |
|
||||||
|
| `GET/POST /api/events` | Odczytuje lub dopisuje chronologiczne zdarzenia z timestampem backendu. |
|
||||||
|
| `POST /api/evidence` | Zapisuje PNG/JPEG i przypina go do zdarzenia. |
|
||||||
|
| `GET /api/evidence/:name` | Udostępnia zapisany dowód. |
|
||||||
|
| `GET /api/report/lesson.html` | Renderuje chronologiczny raport HTML z dowodami. |
|
||||||
|
|
||||||
|
Zdarzenia są dopisywane do SQLite i obejmują nawigację, aktywacje
|
||||||
|
checkpointów, reset/redraw Neovima, zmiany viewera oraz postęp. Dzięki temu
|
||||||
|
raport pokazuje rzeczywistą kolejność pracy, a nie wyłącznie końcowy stan.
|
||||||
|
|
||||||
## Planowane strategie i adnotacje
|
## Planowane strategie i adnotacje
|
||||||
|
|
||||||
|
|||||||
+300
-18
@@ -3,7 +3,7 @@ import { createRoot } from "react-dom/client";
|
|||||||
import "./react.css";
|
import "./react.css";
|
||||||
|
|
||||||
type Status = "pending" | "approved";
|
type Status = "pending" | "approved";
|
||||||
type NavigationLevel = "task" | "block" | "phase" | "step" | "snapshot";
|
type NavigationLevel = "card" | "task" | "block" | "phase" | "step" | "snapshot";
|
||||||
type NavigationPosition = {
|
type NavigationPosition = {
|
||||||
task_index: number;
|
task_index: number;
|
||||||
block_index: number;
|
block_index: number;
|
||||||
@@ -36,6 +36,7 @@ type ApiViewerState = {
|
|||||||
sheet_scroll_top?: number;
|
sheet_scroll_top?: number;
|
||||||
};
|
};
|
||||||
viewport: { width: number; height: number };
|
viewport: { width: number; height: number };
|
||||||
|
allocator?: { visible: boolean };
|
||||||
nvim: {
|
nvim: {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
sync: boolean;
|
sync: boolean;
|
||||||
@@ -465,6 +466,8 @@ function Topbar({
|
|||||||
setNvimExpanded,
|
setNvimExpanded,
|
||||||
nvimFit,
|
nvimFit,
|
||||||
setNvimFit,
|
setNvimFit,
|
||||||
|
allocatorVisible,
|
||||||
|
setAllocatorVisible,
|
||||||
nvimSummary,
|
nvimSummary,
|
||||||
}: {
|
}: {
|
||||||
toc: CardModel["toc"];
|
toc: CardModel["toc"];
|
||||||
@@ -480,6 +483,8 @@ function Topbar({
|
|||||||
setNvimExpanded: (value: boolean) => void;
|
setNvimExpanded: (value: boolean) => void;
|
||||||
nvimFit: boolean;
|
nvimFit: boolean;
|
||||||
setNvimFit: (value: boolean) => void;
|
setNvimFit: (value: boolean) => void;
|
||||||
|
allocatorVisible: boolean;
|
||||||
|
setAllocatorVisible: (value: boolean) => void;
|
||||||
nvimSummary: NvimPanelSummary;
|
nvimSummary: NvimPanelSummary;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
@@ -549,6 +554,16 @@ function Topbar({
|
|||||||
>
|
>
|
||||||
<kbd className="nvim-toolbar-key">A5</kbd><span className="nvim-toolbar-label">FIT</span>
|
<kbd className="nvim-toolbar-key">A5</kbd><span className="nvim-toolbar-label">FIT</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`nvim-height-toggle nvim-toolbar-button${allocatorVisible ? " is-active" : ""}`}
|
||||||
|
aria-label={`Alt+6: model alokatora ${allocatorVisible ? "przypięty" : "ukryty"}`}
|
||||||
|
title={allocatorVisible ? "Alt+6 · Ukryj model alokatora" : "Alt+6 · Przypnij model alokatora"}
|
||||||
|
aria-pressed={allocatorVisible}
|
||||||
|
onClick={() => setAllocatorVisible(!allocatorVisible)}
|
||||||
|
>
|
||||||
|
<kbd className="nvim-toolbar-key">A6</kbd><span className="nvim-toolbar-label">MODEL</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<span className="nvim-toolbar-boundary" aria-hidden="true">|</span>
|
<span className="nvim-toolbar-boundary" aria-hidden="true">|</span>
|
||||||
{nvimVisible && (
|
{nvimVisible && (
|
||||||
@@ -561,7 +576,7 @@ function Topbar({
|
|||||||
{nvimSummary.detail && <small>{nvimSummary.detail}</small>}
|
{nvimSummary.detail && <small>{nvimSummary.detail}</small>}
|
||||||
</span>
|
</span>
|
||||||
<span className="nvim-shortcut-strip" aria-label="Skróty panelu Neovima">
|
<span className="nvim-shortcut-strip" aria-label="Skróty panelu Neovima">
|
||||||
<kbd>Alt+W</kbd> okna · <kbd>F12</kbd> skróty
|
<kbd>F1</kbd> reset · <kbd>F2</kbd> sync · <kbd>Alt+W</kbd> okna · <kbd>F12</kbd> skróty
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -608,6 +623,138 @@ type ActiveNvimStep = {
|
|||||||
focusLevel?: NavigationLevel;
|
focusLevel?: NavigationLevel;
|
||||||
activate?: boolean;
|
activate?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function allocatorStepFromModel(
|
||||||
|
model: CardModel,
|
||||||
|
navigation?: ApiNavigation | null,
|
||||||
|
): ActiveNvimStep | null {
|
||||||
|
for (const section of model.sections) {
|
||||||
|
for (const asset of section.assets) {
|
||||||
|
const interactive = asset.interactive;
|
||||||
|
if (interactive?.kind !== "uml-sequence" || !interactive.phases?.length) continue;
|
||||||
|
if (navigation?.task.id && interactive.task?.id !== navigation.task.id) continue;
|
||||||
|
if (navigation?.block.id && interactive.block?.id !== navigation.block.id) continue;
|
||||||
|
for (const phase of interactive.phases) {
|
||||||
|
if (navigation?.phase.id && phase.id !== navigation.phase.id) continue;
|
||||||
|
const step = navigation?.step.id
|
||||||
|
? phase.steps.find(item => item.id === navigation.step.id)
|
||||||
|
: phase.steps[0];
|
||||||
|
if (!step) continue;
|
||||||
|
return {
|
||||||
|
task: interactive.task ?? null,
|
||||||
|
block: interactive.block ?? null,
|
||||||
|
phase: { id: phase.id, label: phase.label },
|
||||||
|
step,
|
||||||
|
focusLevel: navigation?.focus_level ?? "step",
|
||||||
|
activate: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AllocatorPanel({ visible, model }: { visible: boolean; model: CardModel }) {
|
||||||
|
const [active, setActive] = useState<ActiveNvimStep | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const selected = (event: Event) => {
|
||||||
|
setActive((event as CustomEvent<ActiveNvimStep>).detail ?? null);
|
||||||
|
};
|
||||||
|
window.addEventListener("stem:nvim-step", selected);
|
||||||
|
setActive(allocatorStepFromModel(model));
|
||||||
|
void fetch("/api/navigation/current", { cache: "no-store" })
|
||||||
|
.then(response => response.ok ? response.json() : Promise.reject())
|
||||||
|
.then(payload => {
|
||||||
|
const resolved = allocatorStepFromModel(model, payload.current ?? null);
|
||||||
|
if (resolved) setActive(resolved);
|
||||||
|
})
|
||||||
|
.catch(() => undefined);
|
||||||
|
return () => window.removeEventListener("stem:nvim-step", selected);
|
||||||
|
}, [model]);
|
||||||
|
|
||||||
|
if (!visible) return null;
|
||||||
|
const focusLevel = active?.focusLevel ?? "card";
|
||||||
|
const arena = active?.step.snapshot.memory.arena;
|
||||||
|
const hasUmlCursor = Boolean(active && focusLevel !== "card" && focusLevel !== "task");
|
||||||
|
const size = Math.max(1, Number(arena?.size_bytes ?? 64));
|
||||||
|
const offset = arena?.offset == null
|
||||||
|
? null
|
||||||
|
: Math.max(0, Math.min(size, Number(arena.offset)));
|
||||||
|
const byteValues = arena?.bytes.trim().split(/\s+/).filter(Boolean) ?? [];
|
||||||
|
const cellCount = Math.min(size, 64);
|
||||||
|
const facts = active
|
||||||
|
? [
|
||||||
|
...active.step.snapshot.memory.items.map(item => ({
|
||||||
|
name: item.name,
|
||||||
|
value: item.value,
|
||||||
|
})),
|
||||||
|
...active.step.snapshot.frame.fields.map(field => ({
|
||||||
|
name: field.name,
|
||||||
|
value: field.value,
|
||||||
|
})),
|
||||||
|
].filter((fact, index, all) =>
|
||||||
|
all.findIndex(candidate => candidate.name === fact.name) === index,
|
||||||
|
).slice(0, 8)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="allocator-panel" aria-label="Przypięty model alokatora">
|
||||||
|
<header>
|
||||||
|
<span>ALLOCATOR · PINNED</span>
|
||||||
|
<strong>
|
||||||
|
{active
|
||||||
|
? `${active.phase.label} · ${String(active.step.number).padStart(2, "0")} ${active.step.label}`
|
||||||
|
: "Oczekiwanie na diagram UML"}
|
||||||
|
</strong>
|
||||||
|
<small>
|
||||||
|
{hasUmlCursor
|
||||||
|
? active!.step.snapshot.description
|
||||||
|
: "Wybierz co najmniej BLOCK. F2 wybierze jego pierwszy krok i wykona synchronizację."}
|
||||||
|
</small>
|
||||||
|
</header>
|
||||||
|
<div className="allocator-panel-body">
|
||||||
|
<dl className="allocator-metrics">
|
||||||
|
<div><dt>FOCUS</dt><dd>{focusLevel.toUpperCase()}</dd></div>
|
||||||
|
<div><dt>BASE</dt><dd>{arena?.base ?? "—"}</dd></div>
|
||||||
|
<div><dt>ALLOCP</dt><dd>{arena?.cursor ?? "—"}</dd></div>
|
||||||
|
<div><dt>USED / FREE</dt><dd>{offset == null ? "—" : `${offset} / ${size - offset} B`}</dd></div>
|
||||||
|
</dl>
|
||||||
|
<div className={`allocator-arena-view${hasUmlCursor ? "" : " is-inactive"}`}>
|
||||||
|
<div className="allocator-arena-labels">
|
||||||
|
<span>allocbuf[0]</span>
|
||||||
|
<strong>{offset == null ? "allocp jeszcze nieustawiony" : `offset ${offset}`}</strong>
|
||||||
|
<span>allocbuf[{size - 1}]</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="allocator-pinned-arena"
|
||||||
|
style={{ "--allocator-size": cellCount } as React.CSSProperties}
|
||||||
|
>
|
||||||
|
{Array.from({ length: cellCount }, (_, index) => (
|
||||||
|
<i
|
||||||
|
key={index}
|
||||||
|
className={offset != null && index < offset ? "is-allocated" : ""}
|
||||||
|
title={`allocbuf[${index}] = ${byteValues[index] ?? "??"}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{offset != null && (
|
||||||
|
<b
|
||||||
|
className="allocator-cursor"
|
||||||
|
style={{ left: `${offset / size * 100}%` }}
|
||||||
|
title={`allocp = ${arena?.cursor}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="allocator-facts" aria-label="Wartości bieżącego kroku">
|
||||||
|
{facts.length ? facts.map(fact => (
|
||||||
|
<div key={fact.name}><span>{fact.name}</span><code>{fact.value}</code></div>
|
||||||
|
)) : <p>Brak snapshotu.</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
type NvimConnectionStatus = {
|
type NvimConnectionStatus = {
|
||||||
state: "hidden" | "connecting" | "connected" | "offline" | "unselected";
|
state: "hidden" | "connecting" | "connected" | "offline" | "unselected";
|
||||||
message?: string;
|
message?: string;
|
||||||
@@ -1253,6 +1400,25 @@ function NeovimPanel({
|
|||||||
});
|
});
|
||||||
saveNvimGrid(snapshotRef, gridRef.current);
|
saveNvimGrid(snapshotRef, gridRef.current);
|
||||||
redraw();
|
redraw();
|
||||||
|
await new Promise<void>(resolve => window.requestAnimationFrame(() => resolve()));
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (canvas && activeStepRef.current?.step.snapshot_ref === snapshotRef) {
|
||||||
|
try {
|
||||||
|
const dataUrl = canvas.toDataURL("image/png");
|
||||||
|
void fetch("/api/evidence", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
actor: document.documentElement.dataset.cardClientId ?? "browser",
|
||||||
|
snapshot_ref: snapshotRef,
|
||||||
|
caption: `${String(detail.step.number).padStart(2, "0")} ${detail.step.label}`,
|
||||||
|
data_url: dataUrl,
|
||||||
|
}),
|
||||||
|
}).catch(() => undefined);
|
||||||
|
} catch {
|
||||||
|
// Evidence is auxiliary; a failed screenshot must not invalidate replay.
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
if (controller.signal.aborted || checkpointGenerationRef.current !== generation) return;
|
if (controller.signal.aborted || checkpointGenerationRef.current !== generation) return;
|
||||||
@@ -2168,7 +2334,7 @@ function SnapshotInteractiveUmlSequence({
|
|||||||
);
|
);
|
||||||
const [focusLevel, setFocusLevel] = useState<NavigationLevel>(() => {
|
const [focusLevel, setFocusLevel] = useState<NavigationLevel>(() => {
|
||||||
const saved = localStorage.getItem(focusStorageKey);
|
const saved = localStorage.getItem(focusStorageKey);
|
||||||
return saved === "task" || saved === "block" || saved === "phase"
|
return saved === "card" || saved === "task" || saved === "block" || saved === "phase"
|
||||||
|| saved === "step" || saved === "snapshot"
|
|| saved === "step" || saved === "snapshot"
|
||||||
? saved
|
? saved
|
||||||
: "step";
|
: "step";
|
||||||
@@ -2304,7 +2470,7 @@ function SnapshotInteractiveUmlSequence({
|
|||||||
);
|
);
|
||||||
hit.dataset.umlStep = String(index);
|
hit.dataset.umlStep = String(index);
|
||||||
hit.classList.add("uml-step-hit");
|
hit.classList.add("uml-step-hit");
|
||||||
hit.classList.toggle("uml-step-hit-active", index === stepIndex);
|
hit.classList.toggle("uml-step-hit-active", focusLevel !== "card" && index === stepIndex);
|
||||||
hit.classList.toggle("uml-step-hit-completed", completed);
|
hit.classList.toggle("uml-step-hit-completed", completed);
|
||||||
svg.appendChild(hit);
|
svg.appendChild(hit);
|
||||||
});
|
});
|
||||||
@@ -2390,9 +2556,14 @@ function SnapshotInteractiveUmlSequence({
|
|||||||
focusLevel: level,
|
focusLevel: level,
|
||||||
activate: activate && document.documentElement.dataset.nvimSyncMode === "on",
|
activate: activate && document.documentElement.dataset.nvimSyncMode === "on",
|
||||||
};
|
};
|
||||||
window.dispatchEvent(new CustomEvent<ActiveNvimStep>("stem:nvim-step", { detail }));
|
const dispatch = () => {
|
||||||
if (!persist) return;
|
window.dispatchEvent(new CustomEvent<ActiveNvimStep>("stem:nvim-step", { detail }));
|
||||||
void fetch("/api/navigation/current", {
|
};
|
||||||
|
if (!persist) {
|
||||||
|
dispatch();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const save = fetch("/api/navigation/current", {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: { "content-type": "application/json" },
|
headers: { "content-type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -2409,11 +2580,19 @@ function SnapshotInteractiveUmlSequence({
|
|||||||
sync_requested: Boolean(detail.activate),
|
sync_requested: Boolean(detail.activate),
|
||||||
actor: document.documentElement.dataset.cardClientId ?? "browser",
|
actor: document.documentElement.dataset.cardClientId ?? "browser",
|
||||||
}),
|
}),
|
||||||
}).catch(() => undefined);
|
});
|
||||||
|
if (detail.activate) {
|
||||||
|
// Activation must be audited against the newly selected step. In a static
|
||||||
|
// export the request fails, but the local interactive model still works.
|
||||||
|
void save.then(dispatch, dispatch);
|
||||||
|
} else {
|
||||||
|
dispatch();
|
||||||
|
void save.catch(() => undefined);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const selectIndex = (
|
const selectIndex = (
|
||||||
next: number,
|
next: number,
|
||||||
activate = true,
|
activate = false,
|
||||||
level: NavigationLevel = focusLevel,
|
level: NavigationLevel = focusLevel,
|
||||||
persist = true,
|
persist = true,
|
||||||
) => {
|
) => {
|
||||||
@@ -2455,7 +2634,7 @@ function SnapshotInteractiveUmlSequence({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
const next = Number(hits[0]?.dataset.umlStep);
|
const next = Number(hits[0]?.dataset.umlStep);
|
||||||
if (Number.isInteger(next)) selectIndex(next, true, "step");
|
if (Number.isInteger(next)) selectIndex(next, false, "step");
|
||||||
};
|
};
|
||||||
const handleSvgKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
const handleSvgKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
if (event.key !== "Enter" && event.key !== " ") return;
|
if (event.key !== "Enter" && event.key !== " ") return;
|
||||||
@@ -2463,11 +2642,11 @@ function SnapshotInteractiveUmlSequence({
|
|||||||
const next = Number(target.dataset?.umlStep);
|
const next = Number(target.dataset?.umlStep);
|
||||||
if (!Number.isInteger(next)) return;
|
if (!Number.isInteger(next)) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
selectIndex(next, true, "step");
|
selectIndex(next, event.key === "Enter", "step");
|
||||||
};
|
};
|
||||||
const selectPhase = (phase: UmlSequencePhase) => {
|
const selectPhase = (phase: UmlSequencePhase) => {
|
||||||
const next = entries.findIndex((entry) => entry.phase.id === phase.id);
|
const next = entries.findIndex((entry) => entry.phase.id === phase.id);
|
||||||
if (next >= 0) selectIndex(next, true, "phase");
|
if (next >= 0) selectIndex(next, false, "phase");
|
||||||
};
|
};
|
||||||
|
|
||||||
const active = entries[stepIndex];
|
const active = entries[stepIndex];
|
||||||
@@ -2494,7 +2673,9 @@ function SnapshotInteractiveUmlSequence({
|
|||||||
entry.phase.id === command.phase.id && entry.step.id === command.step.id,
|
entry.phase.id === command.phase.id && entry.step.id === command.step.id,
|
||||||
);
|
);
|
||||||
if (next < 0) return;
|
if (next < 0) return;
|
||||||
selectIndex(next, command.sync_requested, command.focus_level, false);
|
// Remote navigation only moves the teaching cursor. The producer that
|
||||||
|
// requested a replay owns it; reflecting state must never replay twice.
|
||||||
|
selectIndex(next, false, command.focus_level, false);
|
||||||
};
|
};
|
||||||
window.addEventListener("stem:navigation-command", applyCommand);
|
window.addEventListener("stem:navigation-command", applyCommand);
|
||||||
return () => window.removeEventListener("stem:navigation-command", applyCommand);
|
return () => window.removeEventListener("stem:navigation-command", applyCommand);
|
||||||
@@ -2536,7 +2717,7 @@ function SnapshotInteractiveUmlSequence({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hierarchy: NavigationLevel[] = ["task", "block", "phase", "step", "snapshot"];
|
const hierarchy: NavigationLevel[] = ["card", "task", "block", "phase", "step", "snapshot"];
|
||||||
if ((event.key === "ArrowLeft" || event.key === "ArrowRight" || event.key === "Escape")
|
if ((event.key === "ArrowLeft" || event.key === "ArrowRight" || event.key === "Escape")
|
||||||
&& !event.altKey && !event.ctrlKey && !event.shiftKey) {
|
&& !event.altKey && !event.ctrlKey && !event.shiftKey) {
|
||||||
const direction = event.key === "ArrowRight" ? 1 : -1;
|
const direction = event.key === "ArrowRight" ? 1 : -1;
|
||||||
@@ -2552,6 +2733,7 @@ function SnapshotInteractiveUmlSequence({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event.key !== "ArrowUp" && event.key !== "ArrowDown") return;
|
if (event.key !== "ArrowUp" && event.key !== "ArrowDown") return;
|
||||||
|
if (focusLevel === "card" && !event.altKey && !event.ctrlKey && !event.shiftKey) return;
|
||||||
const direction = event.key === "ArrowUp" ? -1 : 1;
|
const direction = event.key === "ArrowUp" ? -1 : 1;
|
||||||
let level: NavigationLevel = focusLevel;
|
let level: NavigationLevel = focusLevel;
|
||||||
if (event.altKey && event.shiftKey && !event.ctrlKey) level = "snapshot";
|
if (event.altKey && event.shiftKey && !event.ctrlKey) level = "snapshot";
|
||||||
@@ -2568,7 +2750,7 @@ function SnapshotInteractiveUmlSequence({
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
level,
|
level,
|
||||||
delta: direction,
|
delta: direction,
|
||||||
sync_requested: document.documentElement.dataset.nvimSyncMode === "on",
|
sync_requested: false,
|
||||||
actor: document.documentElement.dataset.cardClientId ?? "browser",
|
actor: document.documentElement.dataset.cardClientId ?? "browser",
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
@@ -2628,7 +2810,7 @@ function SnapshotInteractiveUmlSequence({
|
|||||||
<button
|
<button
|
||||||
key={entry.step.id}
|
key={entry.step.id}
|
||||||
aria-label={`Krok ${entry.step.number}: ${entry.step.label}`}
|
aria-label={`Krok ${entry.step.number}: ${entry.step.label}`}
|
||||||
aria-pressed={index === stepIndex}
|
aria-pressed={focusLevel !== "card" && index === stepIndex}
|
||||||
data-completed={isDone ? "true" : "false"}
|
data-completed={isDone ? "true" : "false"}
|
||||||
onClick={() => selectIndex(index)}
|
onClick={() => selectIndex(index)}
|
||||||
>
|
>
|
||||||
@@ -2857,6 +3039,10 @@ function ProgressDock({
|
|||||||
<a href="/api/report/teams.md" target="_blank" rel="noopener">
|
<a href="/api/report/teams.md" target="_blank" rel="noopener">
|
||||||
Otwórz raport do Teams
|
Otwórz raport do Teams
|
||||||
</a>
|
</a>
|
||||||
|
<br />
|
||||||
|
<a href="/api/report/lesson.html" target="_blank" rel="noopener">
|
||||||
|
Otwórz ślad lekcji ze zrzutami
|
||||||
|
</a>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2873,12 +3059,16 @@ function ShortcutHelp({ open, onClose }: { open: boolean; onClose: () => void })
|
|||||||
["→", "zejdź poziom niżej"],
|
["→", "zejdź poziom niżej"],
|
||||||
["← / Esc", "wróć poziom wyżej"],
|
["← / Esc", "wróć poziom wyżej"],
|
||||||
["Enter", "wybierz element; przy SYNC ON odtwórz stan"],
|
["Enter", "wybierz element; przy SYNC ON odtwórz stan"],
|
||||||
|
["F1", "reset aktualnego celu Neovima/kontenera"],
|
||||||
|
["F2", "synchronizuj cel z kursorem drzewa UML"],
|
||||||
|
["Ctrl + `", "przelicz siatkę i odśwież cały widok Neovima"],
|
||||||
["Ctrl + Enter", "zatwierdź lub cofnij zatwierdzenie kroku"],
|
["Ctrl + Enter", "zatwierdź lub cofnij zatwierdzenie kroku"],
|
||||||
["Alt + 1", "pokaż lub ukryj panel Neovima"],
|
["Alt + 1", "pokaż lub ukryj panel Neovima"],
|
||||||
["Alt + 2", "przełącz SYNC OFF / SYNC ON"],
|
["Alt + 2", "przełącz SYNC OFF / SYNC ON"],
|
||||||
["Alt + 3", "uzbrój lub rozbrój sterowanie Neovimem"],
|
["Alt + 3", "uzbrój lub rozbrój sterowanie Neovimem"],
|
||||||
["Alt + 4", "wysoki panel 90% / zapisana wysokość normalna"],
|
["Alt + 4", "wysoki panel 90% / zapisana wysokość normalna"],
|
||||||
["Alt + 5", "dopasuj cały ekran Neovima; wyłącz ręczny suwak"],
|
["Alt + 5", "dopasuj cały ekran Neovima; wyłącz ręczny suwak"],
|
||||||
|
["Alt + 6", "przypnij lub ukryj model alokatora"],
|
||||||
["Alt + 3, potem najedź", "przekaż klawiaturę i mysz do aktywnej sesji"],
|
["Alt + 3, potem najedź", "przekaż klawiaturę i mysz do aktywnej sesji"],
|
||||||
["Alt + W", "prefiks poleceń okien Neovima (<C-W>)"],
|
["Alt + W", "prefiks poleceń okien Neovima (<C-W>)"],
|
||||||
["Alt + strzałka", "przejdź do sąsiedniego okna Neovima"],
|
["Alt + strzałka", "przejdź do sąsiedniego okna Neovima"],
|
||||||
@@ -2896,7 +3086,7 @@ function ShortcutHelp({ open, onClose }: { open: boolean; onClose: () => void })
|
|||||||
<header>
|
<header>
|
||||||
<div>
|
<div>
|
||||||
<small>F12 · NAWIGACJA DYDAKTYCZNA</small>
|
<small>F12 · NAWIGACJA DYDAKTYCZNA</small>
|
||||||
<h2 id="shortcut-help-title">TASK → BLOCK → PHASE → STEP → SNAPSHOT</h2>
|
<h2 id="shortcut-help-title">CARD → TASK → BLOCK → PHASE → STEP → SNAPSHOT</h2>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={onClose} aria-label="Zamknij skorowidz">×</button>
|
<button onClick={onClose} aria-label="Zamknij skorowidz">×</button>
|
||||||
</header>
|
</header>
|
||||||
@@ -2930,6 +3120,7 @@ function App({ model }: { model: CardModel }) {
|
|||||||
const pendingRemoteNavigationReportRef = useRef(false);
|
const pendingRemoteNavigationReportRef = useRef(false);
|
||||||
const remoteScrollFrameRef = useRef(0);
|
const remoteScrollFrameRef = useRef(0);
|
||||||
const remoteScrollTokenRef = useRef(0);
|
const remoteScrollTokenRef = useRef(0);
|
||||||
|
const shortcutStatusTimerRef = useRef(0);
|
||||||
const [scale, setScaleState] = useState(2.18);
|
const [scale, setScaleState] = useState(2.18);
|
||||||
const [nvimVisible, setNvimVisibleState] = useState(
|
const [nvimVisible, setNvimVisibleState] = useState(
|
||||||
() => localStorage.getItem("stem-card-nvim-visible") !== "false",
|
() => localStorage.getItem("stem-card-nvim-visible") !== "false",
|
||||||
@@ -2943,6 +3134,9 @@ function App({ model }: { model: CardModel }) {
|
|||||||
);
|
);
|
||||||
const [nvimExpanded, setNvimExpandedState] = useState(false);
|
const [nvimExpanded, setNvimExpandedState] = useState(false);
|
||||||
const [nvimFit, setNvimFitState] = useState(false);
|
const [nvimFit, setNvimFitState] = useState(false);
|
||||||
|
const [allocatorVisible, setAllocatorVisibleState] = useState(
|
||||||
|
() => localStorage.getItem("stem-card-allocator-visible") !== "false",
|
||||||
|
);
|
||||||
const [nvimSummary, setNvimSummary] = useState<NvimPanelSummary>({
|
const [nvimSummary, setNvimSummary] = useState<NvimPanelSummary>({
|
||||||
state: "hidden",
|
state: "hidden",
|
||||||
syncMode: false,
|
syncMode: false,
|
||||||
@@ -2953,6 +3147,60 @@ function App({ model }: { model: CardModel }) {
|
|||||||
const [progress, setProgress] = useState<Progress | null>(null);
|
const [progress, setProgress] = useState<Progress | null>(null);
|
||||||
const [progressError, setProgressError] = useState("");
|
const [progressError, setProgressError] = useState("");
|
||||||
const [shortcutsOpen, setShortcutsOpen] = useState(false);
|
const [shortcutsOpen, setShortcutsOpen] = useState(false);
|
||||||
|
const [shortcutStatus, setShortcutStatus] = useState("");
|
||||||
|
const showShortcutStatus = useCallback((message: string) => {
|
||||||
|
window.clearTimeout(shortcutStatusTimerRef.current);
|
||||||
|
setShortcutStatus(message);
|
||||||
|
shortcutStatusTimerRef.current = window.setTimeout(() => setShortcutStatus(""), 4200);
|
||||||
|
}, []);
|
||||||
|
const runWebFunctionKey = useCallback(async (key: "F1" | "F2") => {
|
||||||
|
showShortcutStatus(`${key} · wykonywanie…`);
|
||||||
|
try {
|
||||||
|
const endpoint = key === "F1" ? "/api/nvim/reset" : "/api/navigation/sync";
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify({ actor: clientIdRef.current }),
|
||||||
|
});
|
||||||
|
const payload = await response.json().catch(() => ({}));
|
||||||
|
if (!response.ok) throw new Error(payload.message ?? payload.error ?? `HTTP ${response.status}`);
|
||||||
|
if (key === "F2" && payload.current) {
|
||||||
|
navigationRevisionRef.current = Math.max(
|
||||||
|
navigationRevisionRef.current,
|
||||||
|
Number(payload.current.revision ?? 0),
|
||||||
|
);
|
||||||
|
window.dispatchEvent(new CustomEvent<ApiNavigation>("stem:navigation-command", {
|
||||||
|
detail: payload.current,
|
||||||
|
}));
|
||||||
|
showShortcutStatus(
|
||||||
|
`F2 · ${String(payload.current.step.number).padStart(2, "0")} ${payload.current.step.label} · READY`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showShortcutStatus("F1 · reset przyjęty");
|
||||||
|
}
|
||||||
|
window.setTimeout(() => window.dispatchEvent(new Event("stem:nvim-refresh")), 120);
|
||||||
|
} catch (error) {
|
||||||
|
showShortcutStatus(`${key} · ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
}, [showShortcutStatus]);
|
||||||
|
const runWebRedraw = useCallback(async () => {
|
||||||
|
showShortcutStatus("Ctrl+` · przeliczanie widoku…");
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/nvim/redraw", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify({ actor: clientIdRef.current }),
|
||||||
|
});
|
||||||
|
const payload = await response.json().catch(() => ({}));
|
||||||
|
if (!response.ok) throw new Error(payload.message ?? payload.error ?? `HTTP ${response.status}`);
|
||||||
|
const grid = payload.external_ui?.grid;
|
||||||
|
const dimensions = grid ? ` · ${grid.width}×${grid.height}` : "";
|
||||||
|
showShortcutStatus(`Ctrl+\` · widok odświeżony${dimensions}`);
|
||||||
|
window.dispatchEvent(new Event("stem:nvim-refresh"));
|
||||||
|
} catch (error) {
|
||||||
|
showShortcutStatus(`Ctrl+\` · ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
}, [showShortcutStatus]);
|
||||||
const publishViewer = useCallback(async (viewer: Record<string, unknown>) => {
|
const publishViewer = useCallback(async (viewer: Record<string, unknown>) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/viewer/state", {
|
const response = await fetch("/api/viewer/state", {
|
||||||
@@ -3030,6 +3278,11 @@ function App({ model }: { model: CardModel }) {
|
|||||||
setNvimFitState(value);
|
setNvimFitState(value);
|
||||||
void publishViewer({ nvim: { visible: value || nvimVisible, fit: value, expanded: false } });
|
void publishViewer({ nvim: { visible: value || nvimVisible, fit: value, expanded: false } });
|
||||||
};
|
};
|
||||||
|
const setAllocatorVisible = (value: boolean) => {
|
||||||
|
setAllocatorVisibleState(value);
|
||||||
|
localStorage.setItem("stem-card-allocator-visible", String(value));
|
||||||
|
void publishViewer({ allocator: { visible: value } });
|
||||||
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let second = 0;
|
let second = 0;
|
||||||
const first = window.requestAnimationFrame(() => {
|
const first = window.requestAnimationFrame(() => {
|
||||||
@@ -3069,6 +3322,7 @@ function App({ model }: { model: CardModel }) {
|
|||||||
expanded: nvimExpanded,
|
expanded: nvimExpanded,
|
||||||
fit: nvimFit,
|
fit: nvimFit,
|
||||||
},
|
},
|
||||||
|
allocator: { visible: allocatorVisible },
|
||||||
};
|
};
|
||||||
const applyViewer = (viewer: ApiViewerState) => {
|
const applyViewer = (viewer: ApiViewerState) => {
|
||||||
const fit = viewer.nvim.visible && viewer.nvim.fit;
|
const fit = viewer.nvim.visible && viewer.nvim.fit;
|
||||||
@@ -3079,12 +3333,15 @@ function App({ model }: { model: CardModel }) {
|
|||||||
setNvimControlModeState(viewer.nvim.sync && viewer.nvim.control);
|
setNvimControlModeState(viewer.nvim.sync && viewer.nvim.control);
|
||||||
setNvimExpandedState(expanded);
|
setNvimExpandedState(expanded);
|
||||||
setNvimFitState(fit);
|
setNvimFitState(fit);
|
||||||
|
const allocator = viewer.allocator?.visible ?? true;
|
||||||
|
setAllocatorVisibleState(allocator);
|
||||||
localStorage.setItem("stem-card-nvim-visible", String(viewer.nvim.visible));
|
localStorage.setItem("stem-card-nvim-visible", String(viewer.nvim.visible));
|
||||||
localStorage.setItem("stem-card-nvim-sync-mode", String(viewer.nvim.sync));
|
localStorage.setItem("stem-card-nvim-sync-mode", String(viewer.nvim.sync));
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
"stem-card-nvim-control-mode",
|
"stem-card-nvim-control-mode",
|
||||||
String(viewer.nvim.sync && viewer.nvim.control),
|
String(viewer.nvim.sync && viewer.nvim.control),
|
||||||
);
|
);
|
||||||
|
localStorage.setItem("stem-card-allocator-visible", String(allocator));
|
||||||
const token = ++remoteScrollTokenRef.current;
|
const token = ++remoteScrollTokenRef.current;
|
||||||
applyingRemoteScrollRef.current = true;
|
applyingRemoteScrollRef.current = true;
|
||||||
window.cancelAnimationFrame(remoteScrollFrameRef.current);
|
window.cancelAnimationFrame(remoteScrollFrameRef.current);
|
||||||
@@ -3267,6 +3524,26 @@ function App({ model }: { model: CardModel }) {
|
|||||||
setNvimFit(!nvimFit);
|
setNvimFit(!nvimFit);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (browserCommand === "Digit6") {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
setAllocatorVisible(!allocatorVisible);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((event.key === "F1" || event.key === "F2")
|
||||||
|
&& !event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
void runWebFunctionKey(event.key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.ctrlKey && !event.altKey && !event.metaKey && !event.shiftKey
|
||||||
|
&& (event.code === "Backquote" || event.key === "`")) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
void runWebRedraw();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (event.key === "F12") {
|
if (event.key === "F12") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopImmediatePropagation();
|
event.stopImmediatePropagation();
|
||||||
@@ -3281,7 +3558,8 @@ function App({ model }: { model: CardModel }) {
|
|||||||
};
|
};
|
||||||
window.addEventListener("keydown", navigate, true);
|
window.addEventListener("keydown", navigate, true);
|
||||||
return () => window.removeEventListener("keydown", navigate, true);
|
return () => window.removeEventListener("keydown", navigate, true);
|
||||||
}, [nvimControlMode, nvimExpanded, nvimFit, nvimSyncMode, nvimVisible, shortcutsOpen]);
|
}, [allocatorVisible, nvimControlMode, nvimExpanded, nvimFit, nvimSyncMode, nvimVisible, runWebFunctionKey, runWebRedraw, shortcutsOpen]);
|
||||||
|
useEffect(() => () => window.clearTimeout(shortcutStatusTimerRef.current), []);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const wheel = (event: WheelEvent) => {
|
const wheel = (event: WheelEvent) => {
|
||||||
if (!event.altKey || event.ctrlKey) return;
|
if (!event.altKey || event.ctrlKey) return;
|
||||||
@@ -3346,8 +3624,11 @@ function App({ model }: { model: CardModel }) {
|
|||||||
setNvimExpanded={setNvimExpanded}
|
setNvimExpanded={setNvimExpanded}
|
||||||
nvimFit={nvimFit}
|
nvimFit={nvimFit}
|
||||||
setNvimFit={setNvimFit}
|
setNvimFit={setNvimFit}
|
||||||
|
allocatorVisible={allocatorVisible}
|
||||||
|
setAllocatorVisible={setAllocatorVisible}
|
||||||
nvimSummary={nvimSummary}
|
nvimSummary={nvimSummary}
|
||||||
/>
|
/>
|
||||||
|
<AllocatorPanel visible={allocatorVisible} model={model} />
|
||||||
<NeovimPanel
|
<NeovimPanel
|
||||||
visible={nvimVisible}
|
visible={nvimVisible}
|
||||||
syncMode={nvimSyncMode}
|
syncMode={nvimSyncMode}
|
||||||
@@ -3357,6 +3638,7 @@ function App({ model }: { model: CardModel }) {
|
|||||||
scale={scale}
|
scale={scale}
|
||||||
onSummaryChange={setNvimSummary}
|
onSummaryChange={setNvimSummary}
|
||||||
/>
|
/>
|
||||||
|
{shortcutStatus && <output className="viewer-action-toast">{shortcutStatus}</output>}
|
||||||
</div>
|
</div>
|
||||||
<main className="paper">
|
<main className="paper">
|
||||||
<FrontPage front={model.front} />
|
<FrontPage front={model.front} />
|
||||||
|
|||||||
@@ -24,6 +24,24 @@
|
|||||||
z-index: 80;
|
z-index: 80;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
.viewer-action-toast {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 12;
|
||||||
|
right: 9px;
|
||||||
|
bottom: 9px;
|
||||||
|
max-width: min(720px, calc(100vw - 18px));
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #42b8e7;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: #102c38ee;
|
||||||
|
color: #dff7ff;
|
||||||
|
padding: 5px 9px;
|
||||||
|
box-shadow: 0 4px 14px #0007;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font: 750 10px/1.25 ui-monospace, monospace;
|
||||||
|
}
|
||||||
.viewer-chrome .topbar {
|
.viewer-chrome .topbar {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: auto;
|
top: auto;
|
||||||
@@ -41,6 +59,9 @@
|
|||||||
.viewer-chrome.is-nvim-fit .topbar {
|
.viewer-chrome.is-nvim-fit .topbar {
|
||||||
flex: none;
|
flex: none;
|
||||||
}
|
}
|
||||||
|
.viewer-chrome.is-nvim-fit .allocator-panel {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
.viewer-chrome.is-nvim-fit .nvim-panel {
|
.viewer-chrome.is-nvim-fit .nvim-panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -159,6 +180,159 @@
|
|||||||
background: #eaf6fb;
|
background: #eaf6fb;
|
||||||
color: #174d64;
|
color: #174d64;
|
||||||
}
|
}
|
||||||
|
.allocator-panel {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-top: 1px solid #40515a;
|
||||||
|
border-bottom: 1px solid #40515a;
|
||||||
|
background: #f7f9f5;
|
||||||
|
color: #182127;
|
||||||
|
box-shadow: 0 4px 12px #0002;
|
||||||
|
font: 11px/1.25 system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
.allocator-panel > header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 125px minmax(260px, auto) 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: baseline;
|
||||||
|
min-height: 22px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-bottom: 1px solid #b7c1c4;
|
||||||
|
background: #e8eef0;
|
||||||
|
}
|
||||||
|
.allocator-panel > header > span {
|
||||||
|
color: #166987;
|
||||||
|
font: 800 9px/1.2 ui-monospace, monospace;
|
||||||
|
letter-spacing: .06em;
|
||||||
|
}
|
||||||
|
.allocator-panel > header > strong {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font: 750 11px/1.2 ui-monospace, monospace;
|
||||||
|
}
|
||||||
|
.allocator-panel > header > small {
|
||||||
|
overflow: hidden;
|
||||||
|
color: #536168;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.allocator-panel-body {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 230px minmax(420px, 1fr) minmax(310px, .8fr);
|
||||||
|
gap: 9px;
|
||||||
|
align-items: stretch;
|
||||||
|
padding: 5px 8px 7px;
|
||||||
|
}
|
||||||
|
.allocator-metrics {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 3px 8px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.allocator-metrics > div {
|
||||||
|
min-width: 0;
|
||||||
|
border-left: 2px solid #8ea2ab;
|
||||||
|
padding-left: 5px;
|
||||||
|
}
|
||||||
|
.allocator-metrics dt {
|
||||||
|
color: #627077;
|
||||||
|
font: 750 7px/1.2 ui-monospace, monospace;
|
||||||
|
letter-spacing: .05em;
|
||||||
|
}
|
||||||
|
.allocator-metrics dd {
|
||||||
|
margin: 1px 0 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font: 700 10px/1.2 ui-monospace, monospace;
|
||||||
|
}
|
||||||
|
.allocator-arena-view {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.allocator-arena-view.is-inactive {
|
||||||
|
opacity: .42;
|
||||||
|
}
|
||||||
|
.allocator-arena-labels {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
color: #536168;
|
||||||
|
font: 700 8px/1.2 ui-monospace, monospace;
|
||||||
|
}
|
||||||
|
.allocator-arena-labels strong {
|
||||||
|
color: #075f80;
|
||||||
|
}
|
||||||
|
.allocator-pinned-arena {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(var(--allocator-size), minmax(2px, 1fr));
|
||||||
|
height: 32px;
|
||||||
|
border: 1px solid #28343a;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.allocator-pinned-arena i {
|
||||||
|
min-width: 0;
|
||||||
|
border-right: 1px solid #e1e5e6;
|
||||||
|
background: #fbfcfc;
|
||||||
|
}
|
||||||
|
.allocator-pinned-arena i:nth-child(8n) {
|
||||||
|
border-right-color: #87969d;
|
||||||
|
}
|
||||||
|
.allocator-pinned-arena i.is-allocated {
|
||||||
|
border-right-color: #b2d8e6;
|
||||||
|
background: #43a8ce;
|
||||||
|
}
|
||||||
|
.allocator-cursor {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
top: -4px;
|
||||||
|
bottom: -4px;
|
||||||
|
width: 2px;
|
||||||
|
background: #d02d25;
|
||||||
|
transform: translateX(-1px);
|
||||||
|
box-shadow: 0 0 0 1px #fff9;
|
||||||
|
}
|
||||||
|
.allocator-cursor::before {
|
||||||
|
content: "allocp";
|
||||||
|
position: absolute;
|
||||||
|
top: 1px;
|
||||||
|
left: 4px;
|
||||||
|
color: #8d1d17;
|
||||||
|
font: 800 7px/1 ui-monospace, monospace;
|
||||||
|
}
|
||||||
|
.allocator-facts {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 2px 7px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.allocator-facts > div {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 5px;
|
||||||
|
border-bottom: 1px dotted #b4bec2;
|
||||||
|
}
|
||||||
|
.allocator-facts span {
|
||||||
|
overflow: hidden;
|
||||||
|
color: #46565e;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.allocator-facts code {
|
||||||
|
overflow: hidden;
|
||||||
|
color: #142e39;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font: 700 9px/1.3 ui-monospace, monospace;
|
||||||
|
}
|
||||||
|
.allocator-facts p {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
margin: 0;
|
||||||
|
color: #647278;
|
||||||
|
}
|
||||||
.nvim-panel {
|
.nvim-panel {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -1460,6 +1634,19 @@ text.uml-step-element-active {
|
|||||||
.nvim-work-indicator {
|
.nvim-work-indicator {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
.allocator-panel > header {
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
}
|
||||||
|
.allocator-panel > header > small {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.allocator-panel-body {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.allocator-metrics,
|
||||||
|
.allocator-facts {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
.memory-lanes {
|
.memory-lanes {
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user