@ai-matrx/canvas 0.5.3 → 0.6.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/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.1
4
+
5
+ Live measurement of 0.6.0 (a 756px pane, five tabs, Quick Chat's header actions present):
6
+
7
+ - **The strip takes the header's free width** (`flex: 1`). Sized to its own intrinsic width, the browser undercut every text tab by 2–3px and cut each name; now tabs lay out in real room ("+" sits at the strip's end).
8
+ - **No ellipsis.** System fonts overflow their intrinsic width by a fraction of a pixel, which put "…" on names that fit ("Quick Ch…"). Titles clip; a name really cut (`data-truncated` on the tab, measured: more than 1px) fades at its end.
9
+ - **Room for names:** only the active tab reserves its (always shown) close's room — a background tab's close overlays on hover with the name fading under it; icon gap 4px; an empty leading slot takes no room and no gap (the actions keep the end with `margin-left: auto`).
10
+
11
+ ### Consumer action
12
+
13
+ - None.
14
+
15
+ ## 0.6.0
16
+
17
+ Independent live sweep (admin, clone):
18
+
19
+ - **Tabs read fully.** A 751px pane showed "Quic", "Messa" while 363px of header sat empty: `.mxc-tab`'s `container: inline-size` made every tab's intrinsic width ignore its text, so the strip sized to nothing. Tabs are now as wide as their names (max 12rem), shrink evenly only when the header is out of room (floor 4.5rem), then the strip scrolls with a visible cue — the clipped edge fades and "Scroll tabs left/right" chevrons page it. The active tab stays in view. Every tab reserves its close's room, so a name is never under the button (ellipsis when squeezed). The narrow-tab icon drop (container query) is gone.
20
+ - **Back keeps the body.** History entries carry the body's `instance`; Back returns the SAME live body, and the content Back leaves is kept in `CanvasItem.kept` (newest last) — navigating to it again revives its body ("+" → Quick Chat → type → Back → Quick Chat finds the draft). The newest `CANVAS_KEEP_ALIVE_DEPTH` (5) back steps and the kept steps stay mounted-hidden in the body layer; closing the tab disposes them. Bodies are ordered by birth, so Back never reorders the DOM.
21
+ - **Phones present one pane.** At `CANVAS_PHONE_MAX_WIDTH` (767px) a restored multi-pane layout shows ONE pane (the focused one) whose strip carries every pane's tabs; choosing another pane's tab presents that pane. The layout tree is untouched.
22
+ - **`CanvasToggle` is a pressed toggle** (`pressed` → `aria-pressed` + the tap-target pressed look) while the canvas is open.
23
+ - **Double-clicking a launcher item** no longer lands in the tab it opens: the rest of that multi-click is swallowed; launcher items are `user-select: none`.
24
+
25
+ ### Consumer action
26
+
27
+ - Code that builds `CanvasHistoryEntry` objects itself may pass `instance`; code that reads `CanvasItem` can ignore the new `kept`.
28
+ - CSS that targeted `.mxc-tabs` as the header's direct child: the strip is now `.mxc-tabstrip > .mxc-tabs`.
29
+
3
30
  ## 0.5.3
4
31
 
5
32
  - A kind's `HeaderAction` renders in its own gapless row (`.mxc-kind-actions`): tap-target buttons space themselves, and the pane's 2px action gap tripped the tap-target misuse guard over the tab strip (seen live with Quick Chat).
package/README.md CHANGED
@@ -62,6 +62,12 @@ canvas.back(item.id);
62
62
 
63
63
  Navigating to something already open elsewhere focuses it there (a launcher tab closes; any other tab keeps what it shows). History is plain JSON and persists. A lone tab shown as a title keeps its own close.
64
64
 
65
+ **Back never throws a body away.** The newest `CANVAS_KEEP_ALIVE_DEPTH` (5) steps Back can reach, and the 5 most recent contents Back stepped away from (`item.kept`), keep their bodies mounted and hidden. Back returns the SAME live body; choosing a content Back left (Quick Chat again from the launcher) revives its body — a typed draft is still there. Older steps still work but mount fresh. Closing the tab disposes every kept body.
66
+
67
+ **The strip.** A tab is as wide as its name (up to 12rem); tabs shrink evenly only when the header runs out of room (floor 4.5rem), then the strip scrolls — the clipped edge fades and chevrons page it. The active tab stays in view.
68
+
69
+ **Phones** (`CANVAS_PHONE_MAX_WIDTH`, 767px): the column presents ONE pane — the focused one — whose strip carries every pane's tabs; choosing a tab from another pane presents that pane. The layout tree is untouched, so a wider screen gets its split back.
70
+
65
71
  ## A page's own launcher entries
66
72
 
67
73
  ```tsx
package/dist/index.cjs CHANGED
@@ -24,11 +24,13 @@ __export(src_exports, {
24
24
  CANVAS_DEFAULT_WIDTH: () => CANVAS_DEFAULT_WIDTH,
25
25
  CANVAS_DESKTOP_LAYOUT_RULES: () => CANVAS_DESKTOP_LAYOUT_RULES,
26
26
  CANVAS_HISTORY_LIMIT: () => CANVAS_HISTORY_LIMIT,
27
+ CANVAS_KEEP_ALIVE_DEPTH: () => CANVAS_KEEP_ALIVE_DEPTH,
27
28
  CANVAS_LAUNCHER_KIND: () => CANVAS_LAUNCHER_KIND,
28
29
  CANVAS_MAIN_WINDOW: () => CANVAS_MAIN_WINDOW,
29
30
  CANVAS_MIN_SPLIT_FRACTION: () => CANVAS_MIN_SPLIT_FRACTION,
30
31
  CANVAS_MIN_WIDTH: () => CANVAS_MIN_WIDTH,
31
32
  CANVAS_NARROW_WIDTH: () => CANVAS_NARROW_WIDTH,
33
+ CANVAS_PHONE_MAX_WIDTH: () => CANVAS_PHONE_MAX_WIDTH,
32
34
  CANVAS_SHORTCUTS: () => CANVAS_SHORTCUTS,
33
35
  CANVAS_STORAGE_KEY: () => CANVAS_STORAGE_KEY,
34
36
  CANVAS_WEB_LAYOUT_RULES: () => CANVAS_WEB_LAYOUT_RULES,
@@ -95,6 +97,7 @@ module.exports = __toCommonJS(src_exports);
95
97
  var CANVAS_MAIN_WINDOW = "main";
96
98
  var CANVAS_LAUNCHER_KIND = "canvas.launcher";
97
99
  var CANVAS_HISTORY_LIMIT = 50;
100
+ var CANVAS_KEEP_ALIVE_DEPTH = 5;
98
101
  var CANVAS_MIN_WIDTH = 360;
99
102
  var CANVAS_DEFAULT_WIDTH = 640;
100
103
  var CANVAS_MIN_SPLIT_FRACTION = 0.12;
@@ -460,8 +463,10 @@ function sendHome(state, itemId, home, activate) {
460
463
  next = insertInto(next, homeFrame.focusedPaneId, itemId, activate);
461
464
  return activate ? withFrame(next, target, { focusedPaneId: homeFrame.focusedPaneId }) : next;
462
465
  }
463
- function newItem(kind, key, title, data, now, history) {
466
+ function newItem(kind, key, title, data, now, steps) {
464
467
  const id = canvasItemId(kind, key);
468
+ const history = steps?.history ?? [];
469
+ const kept = steps?.kept ?? [];
465
470
  return {
466
471
  id,
467
472
  kind,
@@ -470,12 +475,14 @@ function newItem(kind, key, title, data, now, history) {
470
475
  data,
471
476
  openedAt: now,
472
477
  updatedAt: now,
473
- instance: `${now.toString(36)}:${id}`,
474
- ...history && history.length > 0 ? { history: history.slice(-CANVAS_HISTORY_LIMIT) } : {}
478
+ // A reused instance is the SAME live body coming back (Back, or returning to a kept step).
479
+ instance: steps?.instance ?? `${now.toString(36)}:${id}`,
480
+ ...history.length > 0 ? { history: history.slice(-CANVAS_HISTORY_LIMIT) } : {},
481
+ ...kept.length > 0 ? { kept: kept.slice(-CANVAS_KEEP_ALIVE_DEPTH) } : {}
475
482
  };
476
483
  }
477
484
  function historyEntry(item) {
478
- return { kind: item.kind, key: item.key, title: item.title, data: item.data };
485
+ return { kind: item.kind, key: item.key, title: item.title, data: item.data, ...item.instance ? { instance: item.instance } : {} };
479
486
  }
480
487
  function replaceInPlace(state, current, next) {
481
488
  const pane = paneOf(state, current.id);
@@ -504,17 +511,25 @@ function sanitizeHistory(raw) {
504
511
  if (!entry || typeof entry !== "object") continue;
505
512
  const e = entry;
506
513
  if (typeof e.kind !== "string" || typeof e.key !== "string") continue;
507
- out.push({ kind: e.kind, key: e.key, title: typeof e.title === "string" ? e.title : null, data: e.data ?? null });
514
+ out.push({
515
+ kind: e.kind,
516
+ key: e.key,
517
+ title: typeof e.title === "string" ? e.title : null,
518
+ data: e.data ?? null,
519
+ ...typeof e.instance === "string" && e.instance ? { instance: e.instance } : {}
520
+ });
508
521
  }
509
522
  return out.slice(-CANVAS_HISTORY_LIMIT);
510
523
  }
511
524
  function sanitizeItem(item) {
512
525
  const history = sanitizeHistory(item.history);
513
- const { history: _history, instance, ...rest } = item;
526
+ const kept = sanitizeHistory(item.kept).slice(-CANVAS_KEEP_ALIVE_DEPTH);
527
+ const { history: _history, kept: _kept, instance, ...rest } = item;
514
528
  return {
515
529
  ...rest,
516
530
  ...typeof instance === "string" && instance ? { instance } : {},
517
- ...history.length > 0 ? { history } : {}
531
+ ...history.length > 0 ? { history } : {},
532
+ ...kept.length > 0 ? { kept } : {}
518
533
  };
519
534
  }
520
535
  function clampWidth(width) {
@@ -701,7 +716,22 @@ function canvasReducer(state = createInitialCanvasState(), action) {
701
716
  return current.kind === CANVAS_LAUNCHER_KIND ? canvasReducer(focused, canvasActions.closeItem(itemId)) : focused;
702
717
  }
703
718
  const history = [...current.history ?? [], historyEntry(current)];
704
- return replaceInPlace(state, current, newItem(to.kind, to.key, to.title ?? null, to.data ?? null, now, history));
719
+ const keptAll = current.kept ?? [];
720
+ const keptIndex = keptAll.findIndex((entry) => canvasItemId(entry.kind, entry.key) === targetId);
721
+ const revived = keptIndex >= 0 ? keptAll[keptIndex] : void 0;
722
+ const kept = keptAll.filter((_, i) => i !== keptIndex);
723
+ return replaceInPlace(
724
+ state,
725
+ current,
726
+ newItem(
727
+ to.kind,
728
+ to.key,
729
+ to.title !== void 0 ? to.title : revived?.title ?? null,
730
+ to.data !== void 0 ? to.data : revived?.data ?? null,
731
+ now,
732
+ { history, kept, instance: revived?.instance }
733
+ )
734
+ );
705
735
  }
706
736
  case `${P}back`: {
707
737
  const { itemId, now } = action.payload;
@@ -714,7 +744,12 @@ function canvasReducer(state = createInitialCanvasState(), action) {
714
744
  if (state.items[targetId]) {
715
745
  return focusExisting(canvasReducer(state, canvasActions.closeItem(itemId)), targetId);
716
746
  }
717
- return replaceInPlace(state, current, newItem(entry.kind, entry.key, entry.title, entry.data, now, history.slice(0, -1)));
747
+ const kept = [...(current.kept ?? []).filter((k) => canvasItemId(k.kind, k.key) !== itemId), historyEntry(current)];
748
+ return replaceInPlace(
749
+ state,
750
+ current,
751
+ newItem(entry.kind, entry.key, entry.title, entry.data, now, { history: history.slice(0, -1), kept, instance: entry.instance })
752
+ );
718
753
  }
719
754
  case `${P}closeItem`: {
720
755
  const { itemId } = action.payload;
@@ -1551,6 +1586,7 @@ function selectCanvasItemHistory(s, itemId) {
1551
1586
  var selectCanvasCanGoBack = (s, itemId) => (s.items[itemId]?.history?.length ?? 0) > 0;
1552
1587
 
1553
1588
  // src/core/presentation.ts
1589
+ var CANVAS_PHONE_MAX_WIDTH = 767;
1554
1590
  var CANVAS_NARROW_WIDTH = 480;
1555
1591
  var SQUARE_BAND = 1.2;
1556
1592
  function canvasOrientation(width, height) {