pmx-canvas 0.7.0 → 0.7.1
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/.github/extensions/pmx-canvas/extension.mjs +72 -11
- package/CHANGELOG.md +16 -0
- package/Readme.md +10 -8
- package/dist/canvas/global.css +73 -10
- package/dist/canvas/index.js +58 -58
- package/dist/types/cli/daemon.d.ts +5 -5
- package/dist/types/cli/shared.d.ts +3 -1
- package/docs/cli.md +5 -0
- package/package.json +1 -1
- package/skills/pmx-canvas/SKILL.md +17 -9
- package/skills/pmx-canvas/evals/evals.json +130 -0
- package/skills/pmx-canvas/references/installing-pmx-canvas.md +4 -4
- package/skills/pmx-canvas/references/tours-and-recording.md +69 -0
- package/skills/pmx-canvas-testing/SKILL.md +7 -2
- package/src/cli/commands/apps.ts +1 -1
- package/src/cli/commands/export.ts +9 -2
- package/src/cli/daemon.ts +37 -7
- package/src/cli/shared.ts +11 -2
- package/src/client/App.tsx +0 -1
- package/src/client/canvas/AttentionToast.tsx +5 -3
- package/src/client/canvas/CanvasNode.tsx +0 -1
- package/src/client/canvas/ContextMenu.tsx +0 -1
- package/src/client/canvas/ExportDialog.tsx +3 -1
- package/src/client/canvas/Presentation.tsx +39 -9
- package/src/client/canvas/SelectionBar.tsx +0 -2
- package/src/client/nodes/AttachmentNode.tsx +27 -30
- package/src/client/state/attention-bridge.ts +3 -1
- package/src/client/state/sse-bridge.ts +3 -1
- package/src/client/theme/global.css +73 -10
- package/src/server/operations/ops/imports.ts +19 -3
|
@@ -23,6 +23,10 @@ let managedWorkspaceRoot = null;
|
|
|
23
23
|
let managedPort = null;
|
|
24
24
|
const managedLogs = [];
|
|
25
25
|
const panelServers = new Map();
|
|
26
|
+
// Canvas actions are invoked separately from `open`, and their context can
|
|
27
|
+
// omit the canvas input. Keep the server selected for each visible instance so
|
|
28
|
+
// an action cannot fall back to the process/default port behind the panel.
|
|
29
|
+
const panelTargets = new Map();
|
|
26
30
|
|
|
27
31
|
function normalizeBaseUrl(value) {
|
|
28
32
|
if (typeof value !== "string" || value.trim() === "") return null;
|
|
@@ -227,7 +231,7 @@ async function resolvePmxServer(ctxOrInput, options = {}) {
|
|
|
227
231
|
const allowWorkspaceMismatch = input?.allowWorkspaceMismatch === true;
|
|
228
232
|
for (const candidate of candidateBaseUrls(input)) {
|
|
229
233
|
const probe = await probeServer(candidate.baseUrl, workspaceRoot, {
|
|
230
|
-
allowWorkspaceMismatch
|
|
234
|
+
allowWorkspaceMismatch,
|
|
231
235
|
});
|
|
232
236
|
if (probe.ok) return probe;
|
|
233
237
|
}
|
|
@@ -245,6 +249,29 @@ async function resolvePmxServer(ctxOrInput, options = {}) {
|
|
|
245
249
|
return await startManagedServer(workspaceRoot, input);
|
|
246
250
|
}
|
|
247
251
|
|
|
252
|
+
async function resolvePanelTarget(ctx, options = {}) {
|
|
253
|
+
const selected = panelTargets.get(ctx?.instanceId);
|
|
254
|
+
if (!selected) return await resolvePmxServer(ctx, options);
|
|
255
|
+
|
|
256
|
+
const probe = await probeServer(selected.baseUrl, selected.workspaceRoot, {
|
|
257
|
+
timeoutMs: options.timeoutMs ?? 2_000,
|
|
258
|
+
});
|
|
259
|
+
if (probe.ok) return probe;
|
|
260
|
+
return {
|
|
261
|
+
...probe,
|
|
262
|
+
baseUrl: null,
|
|
263
|
+
error: `The displayed PMX Canvas target is no longer valid: ${probe.error ?? "health check failed"}. Action withheld.`,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function rememberPanelTarget(ctx, pmx) {
|
|
268
|
+
if (!ctx?.instanceId || !pmx?.ok || !pmx.baseUrl) return;
|
|
269
|
+
panelTargets.set(ctx.instanceId, {
|
|
270
|
+
baseUrl: pmx.baseUrl,
|
|
271
|
+
workspaceRoot: pmx.health.workspace,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
248
275
|
function escapeHtml(value) {
|
|
249
276
|
return String(value)
|
|
250
277
|
.replaceAll("&", "&")
|
|
@@ -350,6 +377,19 @@ async function readRequestJson(req) {
|
|
|
350
377
|
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
351
378
|
}
|
|
352
379
|
|
|
380
|
+
async function verifiedEntryTarget(entry) {
|
|
381
|
+
if (!entry.pmx?.baseUrl) return { ok: false, baseUrl: null, error: "PMX Canvas server is unavailable." };
|
|
382
|
+
const probe = await probeServer(entry.pmx.baseUrl, entry.pmx.health.workspace, {
|
|
383
|
+
timeoutMs: 2_000,
|
|
384
|
+
});
|
|
385
|
+
if (probe.ok) return probe;
|
|
386
|
+
return {
|
|
387
|
+
...probe,
|
|
388
|
+
baseUrl: null,
|
|
389
|
+
error: `The displayed PMX Canvas target is no longer valid: ${probe.error ?? "health check failed"}. Action withheld.`,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
353
393
|
function jsonResponse(res, statusCode, body) {
|
|
354
394
|
res.writeHead(statusCode, { "Content-Type": "application/json; charset=utf-8" });
|
|
355
395
|
res.end(JSON.stringify(body));
|
|
@@ -377,13 +417,23 @@ async function startPanelServer(instanceId, ctx, pmx) {
|
|
|
377
417
|
return;
|
|
378
418
|
}
|
|
379
419
|
if (req.method === "GET" && url.pathname === "/context") {
|
|
380
|
-
const
|
|
420
|
+
const target = await verifiedEntryTarget(entry);
|
|
421
|
+
if (!target.ok || !target.baseUrl) {
|
|
422
|
+
jsonResponse(res, 409, target);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const context = await getAxContext(target.baseUrl, entry.workspaceRoot, entry.input);
|
|
381
426
|
jsonResponse(res, 200, context);
|
|
382
427
|
return;
|
|
383
428
|
}
|
|
384
429
|
if (req.method === "POST" && url.pathname === "/focus") {
|
|
385
430
|
const body = await readRequestJson(req);
|
|
386
|
-
const
|
|
431
|
+
const target = await verifiedEntryTarget(entry);
|
|
432
|
+
if (!target.ok || !target.baseUrl) {
|
|
433
|
+
jsonResponse(res, 409, target);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const result = await setAxFocus(target.baseUrl, entry.workspaceRoot, entry.input, body.nodeIds);
|
|
387
437
|
jsonResponse(res, 200, result);
|
|
388
438
|
return;
|
|
389
439
|
}
|
|
@@ -396,8 +446,9 @@ async function startPanelServer(instanceId, ctx, pmx) {
|
|
|
396
446
|
await copilotSession?.send({ prompt: body.prompt });
|
|
397
447
|
// Explicit user instruction from the panel → mirror onto the AX
|
|
398
448
|
// timeline as a steering message (fire-and-forget).
|
|
399
|
-
|
|
400
|
-
|
|
449
|
+
const target = await verifiedEntryTarget(entry);
|
|
450
|
+
if (target.ok && target.baseUrl) {
|
|
451
|
+
void fetchJson(target.baseUrl, "/api/canvas/ax/steer", {
|
|
401
452
|
method: "POST",
|
|
402
453
|
headers: { "Content-Type": "application/json" },
|
|
403
454
|
body: JSON.stringify({ message: body.prompt, source: "copilot" }),
|
|
@@ -492,7 +543,7 @@ async function recordInjectedContext(baseUrl, context, injected) {
|
|
|
492
543
|
}
|
|
493
544
|
|
|
494
545
|
async function getAxStatus(ctx) {
|
|
495
|
-
const resolved = await
|
|
546
|
+
const resolved = await resolvePanelTarget(ctx, { autoStart: false });
|
|
496
547
|
if (!resolved.ok || !resolved.baseUrl) return { ok: false, server: resolved };
|
|
497
548
|
const state = await fetchJson(resolved.baseUrl, "/api/canvas/ax", { timeoutMs: 2_000 });
|
|
498
549
|
return { ok: true, server: resolved, ax: state };
|
|
@@ -517,7 +568,7 @@ async function setAxFocus(baseUrl, workspaceRoot, input = {}, nodeIds = []) {
|
|
|
517
568
|
// Resolves the server first so the action returns an actionable error when the
|
|
518
569
|
// canvas is unavailable, then maps the neutral primitive over plain HTTP.
|
|
519
570
|
async function postAxRecord(ctx, path, payload) {
|
|
520
|
-
const resolved = await
|
|
571
|
+
const resolved = await resolvePanelTarget(ctx, { autoStart: false });
|
|
521
572
|
if (!resolved.ok || !resolved.baseUrl) {
|
|
522
573
|
return { ok: false, error: resolved.error ?? "PMX Canvas server is unavailable." };
|
|
523
574
|
}
|
|
@@ -625,7 +676,7 @@ function reportSteeringDeliveryError(error) {
|
|
|
625
676
|
}
|
|
626
677
|
|
|
627
678
|
async function getAxTimeline(ctx, limit) {
|
|
628
|
-
const resolved = await
|
|
679
|
+
const resolved = await resolvePanelTarget(ctx, { autoStart: false });
|
|
629
680
|
if (!resolved.ok || !resolved.baseUrl) return { ok: false, error: resolved.error };
|
|
630
681
|
const query = typeof limit === "number" && limit > 0 ? `?limit=${limit}` : "";
|
|
631
682
|
return await fetchJson(resolved.baseUrl, `/api/canvas/ax/timeline${query}`, { timeoutMs: 2_000 });
|
|
@@ -674,7 +725,7 @@ const pmxCanvas = createCanvas({
|
|
|
674
725
|
name: "get_ax_context",
|
|
675
726
|
description: "Return the current PMX Canvas AX pinned and focused context.",
|
|
676
727
|
handler: async (ctx) => {
|
|
677
|
-
const resolved = await
|
|
728
|
+
const resolved = await resolvePanelTarget(ctx, { autoStart: false });
|
|
678
729
|
if (!resolved.ok || !resolved.baseUrl) return { ok: false, error: resolved.error };
|
|
679
730
|
return await getAxContext(resolved.baseUrl, workspaceRootFrom(ctx), ctx.input ?? {});
|
|
680
731
|
},
|
|
@@ -693,7 +744,13 @@ const pmxCanvas = createCanvas({
|
|
|
693
744
|
required: ["nodeIds"],
|
|
694
745
|
additionalProperties: false,
|
|
695
746
|
},
|
|
696
|
-
handler: async (ctx) =>
|
|
747
|
+
handler: async (ctx) => {
|
|
748
|
+
const resolved = await resolvePanelTarget(ctx, { autoStart: false });
|
|
749
|
+
if (!resolved.ok || !resolved.baseUrl) {
|
|
750
|
+
throw new CanvasError("pmx_unavailable", resolved.error ?? "PMX Canvas server is unavailable.");
|
|
751
|
+
}
|
|
752
|
+
return await setAxFocus(resolved.baseUrl, workspaceRootFrom(ctx), ctx.input ?? {}, ctx.input?.nodeIds);
|
|
753
|
+
},
|
|
697
754
|
},
|
|
698
755
|
{
|
|
699
756
|
name: "send_instruction",
|
|
@@ -835,7 +892,7 @@ const pmxCanvas = createCanvas({
|
|
|
835
892
|
additionalProperties: false,
|
|
836
893
|
},
|
|
837
894
|
handler: async (ctx) => {
|
|
838
|
-
const resolved = await
|
|
895
|
+
const resolved = await resolvePanelTarget(ctx, { autoStart: false });
|
|
839
896
|
if (!resolved.ok || !resolved.baseUrl) {
|
|
840
897
|
return { ok: false, error: resolved.error ?? "PMX Canvas server is unavailable." };
|
|
841
898
|
}
|
|
@@ -863,6 +920,7 @@ const pmxCanvas = createCanvas({
|
|
|
863
920
|
},
|
|
864
921
|
],
|
|
865
922
|
open: async (ctx) => {
|
|
923
|
+
panelTargets.delete(ctx.instanceId);
|
|
866
924
|
let pmx;
|
|
867
925
|
try {
|
|
868
926
|
pmx = await resolvePmxServer(ctx);
|
|
@@ -877,6 +935,7 @@ const pmxCanvas = createCanvas({
|
|
|
877
935
|
}
|
|
878
936
|
|
|
879
937
|
if (pmx.ok && pmx.baseUrl) {
|
|
938
|
+
rememberPanelTarget(ctx, pmx);
|
|
880
939
|
const fallbackPanel = panelServers.get(ctx.instanceId);
|
|
881
940
|
if (fallbackPanel) {
|
|
882
941
|
panelServers.delete(ctx.instanceId);
|
|
@@ -898,6 +957,7 @@ const pmxCanvas = createCanvas({
|
|
|
898
957
|
panel.entry.workspaceRoot = workspaceRootFrom(ctx);
|
|
899
958
|
panel.entry.input = ctx.input ?? {};
|
|
900
959
|
}
|
|
960
|
+
rememberPanelTarget(ctx, pmx);
|
|
901
961
|
return {
|
|
902
962
|
title: "PMX Canvas",
|
|
903
963
|
status: pmx.ok ? "Connected" : "Needs server",
|
|
@@ -905,6 +965,7 @@ const pmxCanvas = createCanvas({
|
|
|
905
965
|
};
|
|
906
966
|
},
|
|
907
967
|
onClose: async (ctx) => {
|
|
968
|
+
panelTargets.delete(ctx.instanceId);
|
|
908
969
|
const panel = panelServers.get(ctx.instanceId);
|
|
909
970
|
if (!panel) return;
|
|
910
971
|
panelServers.delete(ctx.instanceId);
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ All notable changes to `pmx-canvas` are documented here. This project follows
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.7.1] - 2026-09-30
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Imported cards avoid existing content and multi-card imports form a group named after the source file.
|
|
13
|
+
- Asking an agent to import now includes processing consent in one click; review actions have clearer buttons and a separate sticky footer.
|
|
14
|
+
- Presentations support right-drag panning and a visible exit button; export Open and Download actions look like buttons.
|
|
15
|
+
- Board navigation no longer reports removed cards or carries old session receipts onto Home.
|
|
16
|
+
- Refused edits show their reason without a tooltip, keep the reason in Updates, and leave refused deletions visible.
|
|
17
|
+
- Copilot panel actions use the displayed server and refuse writes if its workspace changes.
|
|
18
|
+
- Inline batch JSON works with either spaced or equals-style flags.
|
|
19
|
+
- CLI exports support an independent `--include-derived-text` opt-in and reject unknown flags.
|
|
20
|
+
- Stopping from a different installation preserves live daemon PID files and reports the mismatch.
|
|
21
|
+
- Daemon stop verifies the process on Windows and refuses to stop a live process whose installation cannot be verified.
|
|
22
|
+
- Bundled skills include a retrievable tour reference, conditional browser-host checks, and eval scenarios for the new board and import workflows.
|
|
23
|
+
|
|
8
24
|
## [0.7.0] - 2026-09-29
|
|
9
25
|
|
|
10
26
|
### Highlights
|
package/Readme.md
CHANGED
|
@@ -83,12 +83,12 @@ the canvas is the union of its [built-in node types](docs/node-types.md) and
|
|
|
83
83
|
file reads, web fetch, anything on its toolbelt.
|
|
84
84
|
|
|
85
85
|
Dropping a PDF or Office document attaches the original (up to 20 MiB). It does
|
|
86
|
-
not bundle a converter or automatically launch an agent.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
support are not guaranteed.
|
|
86
|
+
not bundle a converter or automatically launch an agent. Clicking **Ask agent to
|
|
87
|
+
import** also authorizes processing by the connected host's tools/model provider;
|
|
88
|
+
there is no separate consent confirmation. The agent returns Markdown sections
|
|
89
|
+
with references and warnings for review. Multi-card imports are placed together
|
|
90
|
+
in a free area, in a group named after the source file. Remote hosts may require
|
|
91
|
+
manual transfer; OCR, charts, and host format support are not guaranteed.
|
|
92
92
|
|
|
93
93
|
JSON-render panels support bound form state, nested repeats, watchers, and Card
|
|
94
94
|
header/footer slots. Form edits remain viewer-local until explicitly submitted;
|
|
@@ -246,7 +246,9 @@ surfaces (the core never imports a host SDK).
|
|
|
246
246
|
session override), wakes Copilot when the board sends a steer, injects
|
|
247
247
|
pinned/focused AX context on prompt submission, and exposes actions for focus,
|
|
248
248
|
work items, approval gates, review annotations, the AX timeline, and
|
|
249
|
-
host-capability reporting.
|
|
249
|
+
host-capability reporting. Actions stay bound to the server shown by their
|
|
250
|
+
panel and are withheld if that server no longer matches the panel workspace.
|
|
251
|
+
Install it into any repo with
|
|
250
252
|
`pmx-canvas copilot install-extension` (`--dry-run` to preview).
|
|
251
253
|
- **Codex app** — native through the Codex in-app Browser (opened to
|
|
252
254
|
`/workbench`) plus the PMX MCP server: agents read `canvas://ax-context` /
|
|
@@ -401,7 +403,7 @@ services:
|
|
|
401
403
|
```bash
|
|
402
404
|
# .agents/setup — install the CLI (pin the exact version: a fresh orb running
|
|
403
405
|
# @latest can silently pick up a newer release than the one you validated)
|
|
404
|
-
npm install -g pmx-canvas@0.7.
|
|
406
|
+
npm install -g pmx-canvas@0.7.1
|
|
405
407
|
```
|
|
406
408
|
|
|
407
409
|
The server binds the portal-assigned `$PORT` automatically (gated on the
|
package/dist/canvas/global.css
CHANGED
|
@@ -4784,7 +4784,7 @@ html.is-node-resizing .ext-app-preview-catcher {
|
|
|
4784
4784
|
position: absolute;
|
|
4785
4785
|
top: 18px;
|
|
4786
4786
|
right: 18px;
|
|
4787
|
-
max-width: min(
|
|
4787
|
+
max-width: min(360px, calc(100% - 36px));
|
|
4788
4788
|
display: inline-flex;
|
|
4789
4789
|
align-items: center;
|
|
4790
4790
|
gap: 8px;
|
|
@@ -4792,7 +4792,7 @@ html.is-node-resizing .ext-app-preview-catcher {
|
|
|
4792
4792
|
background: color-mix(in srgb, var(--c-panel-glass) 96%, transparent);
|
|
4793
4793
|
backdrop-filter: blur(14px);
|
|
4794
4794
|
border: 1px solid color-mix(in srgb, var(--c-line) 76%, var(--c-warn) 24%);
|
|
4795
|
-
border-radius:
|
|
4795
|
+
border-radius: 12px;
|
|
4796
4796
|
box-shadow: 0 8px 22px var(--c-shadow);
|
|
4797
4797
|
color: var(--c-text);
|
|
4798
4798
|
font: inherit;
|
|
@@ -4831,15 +4831,27 @@ html.is-node-resizing .ext-app-preview-catcher {
|
|
|
4831
4831
|
box-shadow: 0 0 0 2px color-mix(in srgb, currentColor 28%, transparent);
|
|
4832
4832
|
}
|
|
4833
4833
|
|
|
4834
|
+
.attention-toast-copy {
|
|
4835
|
+
display: grid;
|
|
4836
|
+
gap: 4px;
|
|
4837
|
+
min-width: 0;
|
|
4838
|
+
text-align: left;
|
|
4839
|
+
overflow-wrap: anywhere;
|
|
4840
|
+
}
|
|
4841
|
+
|
|
4842
|
+
.attention-toast-detail {
|
|
4843
|
+
color: var(--c-text-soft);
|
|
4844
|
+
font-size: 12px;
|
|
4845
|
+
line-height: 1.45;
|
|
4846
|
+
max-height: 9em;
|
|
4847
|
+
overflow: auto;
|
|
4848
|
+
}
|
|
4849
|
+
|
|
4834
4850
|
.attention-toast-title {
|
|
4835
4851
|
font-size: 12px;
|
|
4836
4852
|
font-weight: 600;
|
|
4837
4853
|
line-height: 1.2;
|
|
4838
4854
|
letter-spacing: -0.005em;
|
|
4839
|
-
white-space: nowrap;
|
|
4840
|
-
overflow: hidden;
|
|
4841
|
-
text-overflow: ellipsis;
|
|
4842
|
-
max-width: 200px;
|
|
4843
4855
|
}
|
|
4844
4856
|
|
|
4845
4857
|
.attention-history {
|
|
@@ -6841,12 +6853,23 @@ html.is-node-dragging .intent-line-layer {
|
|
|
6841
6853
|
padding: 4px 12px;
|
|
6842
6854
|
cursor: pointer;
|
|
6843
6855
|
}
|
|
6844
|
-
.is-presenting > :not(.app-main):not(.presentation-error),
|
|
6856
|
+
.is-presenting > :not(.app-main):not(.presentation-error):not(.presentation-navigation),
|
|
6845
6857
|
.is-presenting .app-main > :not(.canvas-region),
|
|
6846
6858
|
.is-presenting .canvas-region > :not(.canvas-viewport) {
|
|
6847
6859
|
display: none !important;
|
|
6848
6860
|
}
|
|
6849
6861
|
.is-presenting .canvas-viewport { pointer-events: none; }
|
|
6862
|
+
.presentation-navigation {
|
|
6863
|
+
position: fixed;
|
|
6864
|
+
inset: 0;
|
|
6865
|
+
z-index: 99;
|
|
6866
|
+
}
|
|
6867
|
+
.presentation-exit {
|
|
6868
|
+
position: absolute;
|
|
6869
|
+
top: 16px;
|
|
6870
|
+
right: 16px;
|
|
6871
|
+
box-shadow: 0 2px 12px var(--c-shadow);
|
|
6872
|
+
}
|
|
6850
6873
|
.presentation-error {
|
|
6851
6874
|
position: fixed;
|
|
6852
6875
|
bottom: 16px;
|
|
@@ -7104,10 +7127,17 @@ html.is-node-dragging .intent-line-layer {
|
|
|
7104
7127
|
}
|
|
7105
7128
|
.export-dialog-links {
|
|
7106
7129
|
display: flex;
|
|
7130
|
+
flex-wrap: wrap;
|
|
7107
7131
|
gap: 12px;
|
|
7132
|
+
margin-top: 12px;
|
|
7108
7133
|
}
|
|
7109
7134
|
.export-dialog-links a {
|
|
7110
|
-
|
|
7135
|
+
display: inline-flex;
|
|
7136
|
+
align-items: center;
|
|
7137
|
+
justify-content: center;
|
|
7138
|
+
box-sizing: border-box;
|
|
7139
|
+
text-decoration: none;
|
|
7140
|
+
word-break: normal;
|
|
7111
7141
|
}
|
|
7112
7142
|
|
|
7113
7143
|
/* ── Home categories ── */
|
|
@@ -7267,9 +7297,42 @@ html.is-node-dragging .intent-line-layer {
|
|
|
7267
7297
|
.attachment-node p { margin: 0; color: var(--c-text-soft); line-height: 1.5; }
|
|
7268
7298
|
.attachment-node a, .md-import-source a { color: var(--c-accent); }
|
|
7269
7299
|
.attachment-status { font-weight: 600; }
|
|
7270
|
-
.attachment-
|
|
7271
|
-
.
|
|
7300
|
+
.attachment-button,
|
|
7301
|
+
.presentation-exit,
|
|
7302
|
+
.export-dialog-links a {
|
|
7303
|
+
min-height: 38px;
|
|
7304
|
+
padding: 8px 14px;
|
|
7305
|
+
border: 1px solid var(--c-text-soft);
|
|
7306
|
+
border-radius: 8px;
|
|
7307
|
+
background: var(--c-panel);
|
|
7308
|
+
color: var(--c-text);
|
|
7309
|
+
font: inherit;
|
|
7310
|
+
font-weight: 600;
|
|
7311
|
+
cursor: pointer;
|
|
7312
|
+
}
|
|
7313
|
+
.attachment-button.is-primary,
|
|
7314
|
+
.export-dialog-links a.is-primary { background: var(--c-accent); border-color: var(--c-accent); color: var(--c-bg); }
|
|
7315
|
+
.attachment-button:hover:not(:disabled),
|
|
7316
|
+
.presentation-exit:hover,
|
|
7317
|
+
.export-dialog-links a:hover { filter: brightness(1.12); }
|
|
7318
|
+
.attachment-button:focus-visible,
|
|
7319
|
+
.presentation-exit:focus-visible,
|
|
7320
|
+
.export-dialog-links a:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 3px; }
|
|
7321
|
+
.attachment-button:disabled { opacity: 0.5; cursor: default; }
|
|
7272
7322
|
.attachment-review { width: 100%; border-top: 1px solid var(--c-line); padding-top: 12px; }
|
|
7323
|
+
.attachment-review-actions {
|
|
7324
|
+
position: sticky;
|
|
7325
|
+
bottom: -16px;
|
|
7326
|
+
display: flex;
|
|
7327
|
+
flex-wrap: wrap;
|
|
7328
|
+
justify-content: flex-end;
|
|
7329
|
+
gap: 12px;
|
|
7330
|
+
margin-top: 24px;
|
|
7331
|
+
padding: 16px 0;
|
|
7332
|
+
border-top: 1px solid var(--c-line);
|
|
7333
|
+
background: var(--c-panel);
|
|
7334
|
+
z-index: 1;
|
|
7335
|
+
}
|
|
7273
7336
|
.attachment-review pre { white-space: pre-wrap; overflow-wrap: anywhere; font: 12px/1.6 var(--mono); padding: 12px; background: var(--c-bg); border-radius: 8px; }
|
|
7274
7337
|
.attachment-review h4 { margin-bottom: 6px; }
|
|
7275
7338
|
.md-import-source { padding: 6px 12px; font-size: 11px; overflow-wrap: anywhere; color: var(--c-text-soft); border-top: 1px solid var(--c-line); }
|