@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 +6 -0
- package/DESIGN.md +6 -0
- package/lib/client.js +51 -71
- package/package.json +1 -1
- package/src/page-manager.jsx +11 -0
- package/src/page-shell.jsx +26 -39
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
|
|
609
|
+
function PageIcon({ entry }) {
|
|
609
610
|
if (entry.icon) return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(entry.icon, {});
|
|
610
|
-
if (entry.
|
|
611
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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
|
-
|
|
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
package/src/page-manager.jsx
CHANGED
|
@@ -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}
|
package/src/page-shell.jsx
CHANGED
|
@@ -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
|
|
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.
|
|
52
|
-
|
|
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
|
-
//
|
|
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
|
-
(
|
|
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}
|
|
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}
|
|
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-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
<
|
|
212
|
-
|
|
213
|
-
|
|
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}
|