@lybx-leyw/dsh-pages 0.1.0 → 0.1.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/DESIGN-ZH.md CHANGED
@@ -37,6 +37,12 @@
37
37
 
38
38
  内置 DSH 工作台(`home`)与页面管理(`pages`)和其他页面使用同一注册表,不另建导航状态机。配置中的 home 与 manager 身份必须不同。
39
39
 
40
+ 页面壳只拥有**导航与页面生命周期,绝不规定页面版式**。它给当前页一块裸的整幅区域,区域内的一切都归页面自己:标题、工具条、返回链接、可读栏宽、空态、图标。曾经的默认图标、「返回工作台」按钮与居中的内容栏,等于让页面壳替所有页面决定版式 —— 复杂页面因此被强行塞进壳的形状里。
41
+
42
+ 图标随注册一起由页面声明:`icon` 组件,或没有图标库时用 `glyph` 字符串。页面壳只渲染页面声明的东西,不做任何替代 —— 漏声明会暴露出来,而不是被相似的默认图形掩盖。
43
+
44
+ 键盘处理遵循同一条边界:页面壳只在按 Escape 时关闭自己的导轨浮层,其余焦点收束、Tab 顺序与 Escape 都留给页面。带编辑器、对话框或「离开本页」手势的页面自己拥有这三件事。
45
+
40
46
  三个显式分组为 `top`、`overflow`(回收组)、`bottom`。回收是用户编排动作,不是空间不足时自动挤出页面。`overflow` 非空时显示 **…**,回收页仍可从中访问。管理页自身也能回收,并从该入口重新打开。
41
47
 
42
48
  固定是独立状态:回收页面保持 `region: 'overflow'`,以 `pinned` 与 `pinOrder` 控制顶部组 **…** 后追加的快捷入口,同时仍列在回收组。取消固定只移除快捷入口,不会把页面晋升为 `top`。普通排序与固定排序不能混为一谈;`promotePage` 是编排动作,不是固定状态的权威。
package/DESIGN.md CHANGED
@@ -37,6 +37,12 @@ Settings remain in the native settings panel. Settings contributions are child s
37
37
 
38
38
  The built-ins are DSH workbench (`home`) and Page manager (`pages`), registered into the same page registry as other pages. They are not a separate navigation state machine. The configured home and manager identities must differ.
39
39
 
40
+ The shell owns **navigation and the active-page lifecycle, never page layout**. It allocates a bare full-bleed region for the active page; everything inside that region belongs to the page: headings, toolbars, back links, readable-width columns, empty states, icons. A shell that supplied a default icon, a "back to workbench" button, or a centred content column was deciding page-level UI for every page at once, which is exactly what forces complex pages into a shell-shaped box.
41
+
42
+ A page contributes its icon as part of its registration — an `icon` component, or a `glyph` string for packages without an icon library. The shell renders what the page declared and substitutes nothing, so a missing icon stays visible instead of being masked by a look-alike default.
43
+
44
+ Keyboard handling follows the same boundary: the shell closes its own rail popover on Escape and otherwise leaves focus containment, tab order, and Escape to the page. A page with an editor, a dialog, or a "leave this page" gesture owns all three.
45
+
40
46
  The three explicit groups are `top`, `overflow` (recycle), and `bottom`. Recycling is a user arrangement choice, not automatic eviction caused by limited rail space. Nonempty `overflow` displays **…**, so recycled pages remain reachable. The manager itself can be recycled and reopened there.
41
47
 
42
48
  Pinning is independent state: a recycled page retains `region: 'overflow'`, with `pinned` and `pinOrder` controlling its extra shortcut after **…** in the top group. It remains listed in the recycle group. Unpinning removes that shortcut without promoting the page into `top`. Normal ordering and pin ordering must not be conflated; `promotePage` is an arrangement action, not the pin-state authority.
package/lib/client.js CHANGED
@@ -320,6 +320,17 @@ var styles = `
320
320
  [data-pages-manager] .pm-id{display:block;margin-top:2px;font:10px ui-monospace,monospace;color:var(--dsw-alias-label-caption,#8a9099)}
321
321
  [data-pages-manager] .pm-actions{display:flex;flex:0 1 auto;flex-wrap:wrap;align-items:center;gap:6px;max-width:100%}
322
322
  [data-pages-manager] button,[data-pages-manager] select{min-height:32px;max-width:100%;border:1px solid var(--dsw-alias-settings-card-stroke,#0000001f);border-radius:5px;background:var(--dsw-alias-bg-base,#f4f5f7);color:var(--dsw-alias-label-secondary,#4b5058);font:inherit;font-size:12px;padding:4px 8px;cursor:pointer}
323
+ /* A native <select> draws its arrow inside the content box, so Latin-size
324
+ padding put the arrow on top of CJK labels (\u6C49\u5B57 is about twice as wide as
325
+ the Latin glyphs the default padding assumed). Reserve the arrow's own space
326
+ explicitly and draw the arrow ourselves instead of relying on the UA's. */
327
+ [data-pages-manager] select{
328
+ appearance:none;-webkit-appearance:none;-moz-appearance:none;
329
+ padding:4px 26px 4px 9px;
330
+ background-image:linear-gradient(45deg,transparent 50%,currentColor 50%),linear-gradient(135deg,currentColor 50%,transparent 50%);
331
+ background-position:calc(100% - 13px) 52%,calc(100% - 8px) 52%;
332
+ background-size:5px 5px,5px 5px;background-repeat:no-repeat;
333
+ }
323
334
  [data-pages-manager] button{min-width:32px;white-space:normal;overflow-wrap:anywhere}
324
335
  [data-pages-manager] button:hover:not(:disabled){background:var(--dsw-alias-button-elevated-fill,#0000000f)}
325
336
  [data-pages-manager] :is(button,select):focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#2f6fed);outline-offset:2px}
@@ -577,16 +588,6 @@ var secondary = "var(--dsw-alias-label-secondary, #aaa)";
577
588
  var border = "1px solid var(--dsw-alias-border-l2, #555)";
578
589
  var surface = "var(--dsw-alias-bg-layer-2, #181818)";
579
590
  var foreground = "var(--dsw-alias-label-primary, #ddd)";
580
- var button = {
581
- font: "inherit",
582
- fontSize: 13,
583
- color: foreground,
584
- background: surface,
585
- border,
586
- borderRadius: 6,
587
- padding: "9px 14px",
588
- cursor: "pointer"
589
- };
590
591
  var layoutCss = (width) => `
591
592
  #root:has([data-dsh-pages-shell]) { box-sizing: border-box; padding-left: ${width}px; }
592
593
  [data-dsh-pages-rail] button:hover { background: var(--dsw-alias-interactive-bg-hover, #333); }
@@ -605,17 +606,10 @@ function RailIcon({ width, children }) {
605
606
  transformOrigin: "center"
606
607
  }, children });
607
608
  }
608
- function PageIcon({ entry, home }) {
609
+ function PageIcon({ entry }) {
609
610
  if (entry.icon) return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(entry.icon, {});
610
- if (entry.id === home) {
611
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { width: "23", height: "23", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.65", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "m3 10 9-7 9 7M6 9v12h12V9M10 21v-7h4v7" }) });
612
- }
613
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("svg", { width: "23", height: "23", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.65", "aria-hidden": "true", children: [
614
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { x: "3", y: "4", width: "7", height: "7", rx: "1.6" }),
615
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { x: "14", y: "4", width: "7", height: "7", rx: "1.6" }),
616
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { x: "3", y: "13", width: "7", height: "7", rx: "1.6" }),
617
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { x: "14", y: "13", width: "7", height: "7", rx: "1.6" })
618
- ] });
611
+ if (typeof entry.glyph === "string" && entry.glyph.length > 0) return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { "aria-hidden": "true", children: entry.glyph });
612
+ return null;
619
613
  }
620
614
  function PageShell(props) {
621
615
  const snapshot = props.usePages((value) => value);
@@ -639,7 +633,6 @@ function PageShell(props) {
639
633
  const home = props.homePage;
640
634
  const open = snapshot.activePage !== home;
641
635
  const manager = snapshot.activePage === props.managerPage;
642
- const close = (0, import_react2.useRef)(null);
643
636
  const shell = (0, import_react2.useRef)(null);
644
637
  const moreButton = (0, import_react2.useRef)(null);
645
638
  const contentId = (0, import_react2.useId)();
@@ -649,15 +642,6 @@ function PageShell(props) {
649
642
  const parked = snapshot.pages.filter((entry) => entry.region === "overflow");
650
643
  const menuTop = menu === "more" && moreButton.current !== null ? Math.max(12, Math.min(moreButton.current.getBoundingClientRect().top, globalThis.innerHeight - 260)) : 12;
651
644
  const dismiss = () => props.selectPage(home);
652
- (0, import_react2.useEffect)(() => {
653
- if (!open) return;
654
- close.current?.focus();
655
- const containFocus = (event) => {
656
- if (!shell.current?.contains(event.target)) close.current?.focus();
657
- };
658
- document.addEventListener("focusin", containFocus);
659
- return () => document.removeEventListener("focusin", containFocus);
660
- }, [open, snapshot.activePage]);
661
645
  (0, import_react2.useEffect)(() => {
662
646
  if (menu === null) return void 0;
663
647
  const onPointer = (event) => {
@@ -677,15 +661,9 @@ function PageShell(props) {
677
661
  const keyDown = (event) => {
678
662
  if (!open) return;
679
663
  event.stopPropagation();
680
- if (event.key === "Escape") {
664
+ if (event.key === "Escape" && menu !== null) {
681
665
  event.preventDefault();
682
- dismiss();
683
- } else if (event.key === "Tab") {
684
- const controls = Array.from(shell.current.querySelectorAll('button:not(:disabled), input:not(:disabled), select:not(:disabled), a[href], [tabindex="0"]')).filter((node) => node.getClientRects().length > 0);
685
- const index = controls.indexOf(document.activeElement);
686
- const next = event.shiftKey ? index <= 0 ? controls.length - 1 : index - 1 : (index + 1) % controls.length;
687
- event.preventDefault();
688
- (controls[next] || shell.current).focus();
666
+ setMenu(null);
689
667
  }
690
668
  };
691
669
  const navItem = (entry) => {
@@ -706,7 +684,7 @@ function PageShell(props) {
706
684
  background: selected ? "var(--dsw-alias-interactive-bg-hover, #333)" : "transparent",
707
685
  boxShadow: selected ? "inset 2px 0 0 currentColor" : "none"
708
686
  },
709
- children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(RailIcon, { width: railWidth, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PageIcon, { entry, home }) })
687
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(RailIcon, { width: railWidth, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PageIcon, { entry }) })
710
688
  },
711
689
  entry.id
712
690
  );
@@ -820,7 +798,7 @@ function PageShell(props) {
820
798
  background: selected ? "var(--dsw-alias-interactive-bg-hover, #333)" : "transparent"
821
799
  },
822
800
  children: [
823
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { display: "grid", placeItems: "center", width: 20, height: 20 }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PageIcon, { entry, home }) }),
801
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { display: "grid", placeItems: "center", width: 20, height: 20 }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PageIcon, { entry }) }),
824
802
  label
825
803
  ]
826
804
  }
@@ -852,43 +830,45 @@ function PageShell(props) {
852
830
  )
853
831
  ] }, entry.id);
854
832
  }) }),
855
- open && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { id: contentId, "aria-labelledby": contentId, style: {
833
+ open && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("section", { id: contentId, "aria-label": props.resolveLabel(active?.label ?? ""), style: {
834
+ // A bare full-bleed area. Everything a page shows inside it — headings,
835
+ // toolbars, a back button, its own padding — belongs to the page. The
836
+ // shell provides the region and the lifecycle, never the layout.
856
837
  position: "absolute",
857
838
  inset: `0 0 0 ${railWidth}px`,
858
- overflow: "auto",
859
839
  boxSizing: "border-box",
860
840
  pointerEvents: "auto",
841
+ overflow: "hidden",
842
+ display: "flex",
843
+ flexDirection: "column",
844
+ minWidth: 0,
845
+ minHeight: 0,
861
846
  background: surface,
862
- color: foreground,
863
- fontSize: 14,
864
- lineHeight: 1.6,
865
- padding: "24px clamp(16px, 5vw, 80px)"
866
- }, children: [
867
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("header", { style: { borderBottom: border, paddingBottom: 24, display: "flex", flexWrap: "wrap", alignItems: "center", justifyContent: "space-between", gap: 16 }, children: [
868
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("button", { type: "button", ref: close, style: button, onClick: dismiss, children: [
869
- "\u2190 ",
870
- props.t("back")
871
- ] }),
872
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { color: secondary, fontSize: 11, letterSpacing: "0.12em" }, children: props.resolveLabel(active?.label ?? "") })
873
- ] }),
874
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { maxWidth: 760, margin: "clamp(32px, 8vh, 100px) auto" }, children: manager ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
875
- PageManager,
876
- {
877
- entries: snapshot.pages,
878
- railWidth,
879
- resolveLabel: (entry) => props.resolveLabel(entry.label),
880
- t: props.t,
881
- actions: {
882
- selectPage: props.selectPage,
883
- setPageRegion: props.setPageRegion,
884
- setPagePinned: props.setPagePinned,
885
- setRailWidth: props.setRailWidth,
886
- setPageOrder: props.setPageOrder,
887
- promotePage: props.promotePage
888
- }
847
+ color: foreground
848
+ }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { "data-dsh-pages-content": "", style: {
849
+ flex: 1,
850
+ minWidth: 0,
851
+ minHeight: 0,
852
+ overflow: "auto",
853
+ display: "flex",
854
+ flexDirection: "column"
855
+ }, children: manager ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
856
+ PageManager,
857
+ {
858
+ entries: snapshot.pages,
859
+ railWidth,
860
+ resolveLabel: (entry) => props.resolveLabel(entry.label),
861
+ t: props.t,
862
+ actions: {
863
+ selectPage: props.selectPage,
864
+ setPageRegion: props.setPageRegion,
865
+ setPagePinned: props.setPagePinned,
866
+ setRailWidth: props.setRailWidth,
867
+ setPageOrder: props.setPageOrder,
868
+ promotePage: props.promotePage
889
869
  }
890
- ) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react2.default.Fragment, { children: props.renderSlot(props.pageSlot, {}, { entryKey: snapshot.activePage }) }, snapshot.activePage) })
891
- ] })
870
+ }
871
+ ) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react2.default.Fragment, { children: props.renderSlot(props.pageSlot, {}, { entryKey: snapshot.activePage }) }, snapshot.activePage) }) })
892
872
  ]
893
873
  }
894
874
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lybx-leyw/dsh-pages",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Product pages on top of DeepSeek Harness: a page slot tree that hosts the native workbench as one page",
5
5
  "private": false,
6
6
  "license": "MIT",
@@ -25,6 +25,17 @@ const styles = `
25
25
  [data-pages-manager] .pm-id{display:block;margin-top:2px;font:10px ui-monospace,monospace;color:var(--dsw-alias-label-caption,#8a9099)}
26
26
  [data-pages-manager] .pm-actions{display:flex;flex:0 1 auto;flex-wrap:wrap;align-items:center;gap:6px;max-width:100%}
27
27
  [data-pages-manager] button,[data-pages-manager] select{min-height:32px;max-width:100%;border:1px solid var(--dsw-alias-settings-card-stroke,#0000001f);border-radius:5px;background:var(--dsw-alias-bg-base,#f4f5f7);color:var(--dsw-alias-label-secondary,#4b5058);font:inherit;font-size:12px;padding:4px 8px;cursor:pointer}
28
+ /* A native <select> draws its arrow inside the content box, so Latin-size
29
+ padding put the arrow on top of CJK labels (汉字 is about twice as wide as
30
+ the Latin glyphs the default padding assumed). Reserve the arrow's own space
31
+ explicitly and draw the arrow ourselves instead of relying on the UA's. */
32
+ [data-pages-manager] select{
33
+ appearance:none;-webkit-appearance:none;-moz-appearance:none;
34
+ padding:4px 26px 4px 9px;
35
+ background-image:linear-gradient(45deg,transparent 50%,currentColor 50%),linear-gradient(135deg,currentColor 50%,transparent 50%);
36
+ background-position:calc(100% - 13px) 52%,calc(100% - 8px) 52%;
37
+ background-size:5px 5px,5px 5px;background-repeat:no-repeat;
38
+ }
28
39
  [data-pages-manager] button{min-width:32px;white-space:normal;overflow-wrap:anywhere}
29
40
  [data-pages-manager] button:hover:not(:disabled){background:var(--dsw-alias-button-elevated-fill,#0000000f)}
30
41
  [data-pages-manager] :is(button,select):focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#2f6fed);outline-offset:2px}
@@ -25,10 +25,6 @@ const secondary = 'var(--dsw-alias-label-secondary, #aaa)';
25
25
  const border = '1px solid var(--dsw-alias-border-l2, #555)';
26
26
  const surface = 'var(--dsw-alias-bg-layer-2, #181818)';
27
27
  const foreground = 'var(--dsw-alias-label-primary, #ddd)';
28
- const button = {
29
- font: 'inherit', fontSize: 13, color: foreground,
30
- background: surface, border, borderRadius: 6, padding: '9px 14px', cursor: 'pointer',
31
- };
32
28
  // Explicit shell geometry, not discovery or relocation of native slots. Removing
33
29
  // this component removes the rule and restores the original geometry.
34
30
  const layoutCss = width => `
@@ -46,12 +42,15 @@ function RailIcon({ width, children }) {
46
42
  }
47
43
 
48
44
  /** Icons for the pages this shell itself registers. */
49
- function PageIcon({ entry, home }) {
45
+ function PageIcon({ entry }) {
46
+ // The page owns its icon. The shell renders exactly what the page declared —
47
+ // an `icon` component, or a `glyph` string for dependency-free packages — and
48
+ // never substitutes an icon of its own. A shell-invented default made every
49
+ // page that forgot an icon look identical, which hid the omission instead of
50
+ // surfacing it.
50
51
  if (entry.icon) return <entry.icon/>;
51
- if (entry.id === home) {
52
- return <svg width="23" height="23" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.65" aria-hidden="true"><path d="m3 10 9-7 9 7M6 9v12h12V9M10 21v-7h4v7"/></svg>;
53
- }
54
- return <svg width="23" height="23" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.65" aria-hidden="true"><rect x="3" y="4" width="7" height="7" rx="1.6"/><rect x="14" y="4" width="7" height="7" rx="1.6"/><rect x="3" y="13" width="7" height="7" rx="1.6"/><rect x="14" y="13" width="7" height="7" rx="1.6"/></svg>;
52
+ if (typeof entry.glyph === 'string' && entry.glyph.length > 0) return <span aria-hidden="true">{entry.glyph}</span>;
53
+ return null;
55
54
  }
56
55
 
57
56
  /**
@@ -75,7 +74,6 @@ export function PageShell(props) {
75
74
  const home = props.homePage;
76
75
  const open = snapshot.activePage !== home;
77
76
  const manager = snapshot.activePage === props.managerPage;
78
- const close = useRef(null);
79
77
  const shell = useRef(null);
80
78
  const moreButton = useRef(null);
81
79
  const contentId = useId();
@@ -93,15 +91,6 @@ export function PageShell(props) {
93
91
  ? Math.max(12, Math.min(moreButton.current.getBoundingClientRect().top, globalThis.innerHeight - 260))
94
92
  : 12;
95
93
  const dismiss = () => props.selectPage(home);
96
- useEffect(() => {
97
- if (!open) return;
98
- close.current?.focus();
99
- const containFocus = event => {
100
- if (!shell.current?.contains(event.target)) close.current?.focus();
101
- };
102
- document.addEventListener('focusin', containFocus);
103
- return () => document.removeEventListener('focusin', containFocus);
104
- }, [open, snapshot.activePage]);
105
94
  // A popover left open while its group empties would strand the panel; closing on
106
95
  // outside pointer input keeps it a transient view.
107
96
  useEffect(() => {
@@ -120,18 +109,14 @@ export function PageShell(props) {
120
109
  }, [menu]);
121
110
  const keyDown = event => {
122
111
  if (!open) return;
123
- // Native document-capture shortcuts stay with the host.
112
+ // Only the rail's own popover is a shell concern. Escape inside page content,
113
+ // tab order and focus containment belong to the page: a shell that trapped
114
+ // focus here also took Escape away from editors, dialogs and their own
115
+ // "leave this page" gestures.
124
116
  event.stopPropagation();
125
- if (event.key === 'Escape') {
126
- event.preventDefault();
127
- dismiss();
128
- } else if (event.key === 'Tab') {
129
- const controls = Array.from(shell.current.querySelectorAll('button:not(:disabled), input:not(:disabled), select:not(:disabled), a[href], [tabindex="0"]'))
130
- .filter(node => node.getClientRects().length > 0);
131
- const index = controls.indexOf(document.activeElement);
132
- const next = event.shiftKey ? (index <= 0 ? controls.length - 1 : index - 1) : (index + 1) % controls.length;
117
+ if (event.key === 'Escape' && menu !== null) {
133
118
  event.preventDefault();
134
- (controls[next] || shell.current).focus();
119
+ setMenu(null);
135
120
  }
136
121
  };
137
122
  const navItem = entry => {
@@ -145,7 +130,7 @@ export function PageShell(props) {
145
130
  color: selected ? foreground : secondary,
146
131
  background: selected ? 'var(--dsw-alias-interactive-bg-hover, #333)' : 'transparent',
147
132
  boxShadow: selected ? 'inset 2px 0 0 currentColor' : 'none',
148
- }}><RailIcon width={railWidth}><PageIcon entry={entry} home={home}/></RailIcon></button>;
133
+ }}><RailIcon width={railWidth}><PageIcon entry={entry}/></RailIcon></button>;
149
134
  };
150
135
  return <div ref={shell} data-dsh-pages-shell="" data-active-page={snapshot.activePage}
151
136
  role={open ? 'dialog' : undefined} aria-modal={open ? true : undefined}
@@ -193,7 +178,7 @@ export function PageShell(props) {
193
178
  padding: '0 10px', border: 'none', borderRadius: 7, cursor: 'pointer', font: 'inherit',
194
179
  textAlign: 'left', color: selected ? foreground : secondary,
195
180
  background: selected ? 'var(--dsw-alias-interactive-bg-hover, #333)' : 'transparent',
196
- }}><span style={{ display: 'grid', placeItems: 'center', width: 20, height: 20 }}><PageIcon entry={entry} home={home}/></span>{label}</button>
181
+ }}><span style={{ display: 'grid', placeItems: 'center', width: 20, height: 20 }}><PageIcon entry={entry}/></span>{label}</button>
197
182
  <button type="button" title={props.t(entry.pinned ? 'unpin' : 'pin')} aria-label={`${props.t(entry.pinned ? 'unpin' : 'pin')} ${label}`}
198
183
  data-dsh-pages-pin="" aria-pressed={Boolean(entry.pinned)} onClick={() => props.setPagePinned(entry.id, !entry.pinned)}
199
184
  style={{
@@ -204,15 +189,17 @@ export function PageShell(props) {
204
189
  </div>;
205
190
  })}
206
191
  </div>}
207
- {open && <section id={contentId} aria-labelledby={contentId} style={{
208
- position: 'absolute', inset: `0 0 0 ${railWidth}px`, overflow: 'auto', boxSizing: 'border-box', pointerEvents: 'auto',
209
- background: surface, color: foreground, fontSize: 14, lineHeight: 1.6, padding: '24px clamp(16px, 5vw, 80px)',
192
+ {open && <section id={contentId} aria-label={props.resolveLabel(active?.label ?? '')} style={{
193
+ // A bare full-bleed area. Everything a page shows inside it — headings,
194
+ // toolbars, a back button, its own padding — belongs to the page. The
195
+ // shell provides the region and the lifecycle, never the layout.
196
+ position: 'absolute', inset: `0 0 0 ${railWidth}px`, boxSizing: 'border-box', pointerEvents: 'auto',
197
+ overflow: 'hidden', display: 'flex', flexDirection: 'column', minWidth: 0, minHeight: 0,
198
+ background: surface, color: foreground,
210
199
  }}>
211
- <header style={{ borderBottom: border, paddingBottom: 24, display: 'flex', flexWrap: 'wrap', alignItems: 'center', justifyContent: 'space-between', gap: 16 }}>
212
- <button type="button" ref={close} style={button} onClick={dismiss}>← {props.t('back')}</button>
213
- <span style={{ color: secondary, fontSize: 11, letterSpacing: '0.12em' }}>{props.resolveLabel(active?.label ?? '')}</span>
214
- </header>
215
- <div style={{ maxWidth: 760, margin: 'clamp(32px, 8vh, 100px) auto' }}>
200
+ <div data-dsh-pages-content="" style={{
201
+ flex: 1, minWidth: 0, minHeight: 0, overflow: 'auto', display: 'flex', flexDirection: 'column',
202
+ }}>
216
203
  {manager
217
204
  ? <PageManager entries={snapshot.pages} railWidth={railWidth}
218
205
  resolveLabel={entry => props.resolveLabel(entry.label)} t={props.t}