@forgeax/app-shell 0.3.0 → 0.4.0
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/README.md +19 -0
- package/dist/react.d.ts +9 -2
- package/dist/react.js +167 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -33,6 +33,25 @@ registry.register(createShellContribution({
|
|
|
33
33
|
}));
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
React shell and slot-diagnostic primitives are available from the explicit
|
|
37
|
+
React entry point:
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
import {
|
|
41
|
+
ShellSlot,
|
|
42
|
+
SlotDebugOverlay,
|
|
43
|
+
isSlotDebugEnabled,
|
|
44
|
+
} from '@forgeax/app-shell/react';
|
|
45
|
+
|
|
46
|
+
export function ProductShell() {
|
|
47
|
+
return (
|
|
48
|
+
<ShellSlot as="main" name="Workspace">
|
|
49
|
+
{isSlotDebugEnabled() ? <SlotDebugOverlay /> : null}
|
|
50
|
+
</ShellSlot>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
36
55
|
## Ownership boundary
|
|
37
56
|
|
|
38
57
|
| Package | Owns |
|
package/dist/react.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ReactElement, HTMLAttributes } from 'react';
|
|
2
|
+
|
|
3
|
+
/** Stable FNV-1a color bucket for a shell slot name. */
|
|
4
|
+
declare function hashSlotHue(name: string): number;
|
|
5
|
+
/** Whether a composable `debug` query contains the exact `slots` token. */
|
|
6
|
+
declare function isSlotDebugEnabled(search?: string): boolean;
|
|
7
|
+
/** Dev-time visualization derived only from live `data-fx-slot` markers. */
|
|
8
|
+
declare function SlotDebugOverlay(): ReactElement;
|
|
2
9
|
|
|
3
10
|
type ShellSlotElement = 'aside' | 'div' | 'main' | 'section';
|
|
4
11
|
interface ShellSlotProps extends HTMLAttributes<HTMLElement> {
|
|
@@ -8,4 +15,4 @@ interface ShellSlotProps extends HTMLAttributes<HTMLElement> {
|
|
|
8
15
|
/** Structural shell marker that preserves the caller's semantic element. */
|
|
9
16
|
declare function ShellSlot({ as, name, ...props }: ShellSlotProps): ReactElement;
|
|
10
17
|
|
|
11
|
-
export { ShellSlot, type ShellSlotElement, type ShellSlotProps };
|
|
18
|
+
export { ShellSlot, type ShellSlotElement, type ShellSlotProps, SlotDebugOverlay, hashSlotHue, isSlotDebugEnabled };
|
package/dist/react.js
CHANGED
|
@@ -1,5 +1,168 @@
|
|
|
1
1
|
// src/react.tsx
|
|
2
2
|
import { createElement } from "react";
|
|
3
|
+
|
|
4
|
+
// src/slot-debug.tsx
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
6
|
+
import { jsx } from "react/jsx-runtime";
|
|
7
|
+
var OVERLAY_Z = 2147483e3;
|
|
8
|
+
var LABEL_CORNERS = [
|
|
9
|
+
{ top: 2, left: 2 },
|
|
10
|
+
{ top: 2, right: 2 },
|
|
11
|
+
{ bottom: 2, left: 2 },
|
|
12
|
+
{ bottom: 2, right: 2 }
|
|
13
|
+
];
|
|
14
|
+
function hashSlotHue(name) {
|
|
15
|
+
let hash = 2166136261;
|
|
16
|
+
for (let index = 0; index < name.length; index += 1) {
|
|
17
|
+
hash ^= name.charCodeAt(index);
|
|
18
|
+
hash = Math.imul(hash, 16777619);
|
|
19
|
+
}
|
|
20
|
+
return (hash >>> 0) % 360;
|
|
21
|
+
}
|
|
22
|
+
function isSlotDebugEnabled(search = typeof window !== "undefined" ? window.location.search : "") {
|
|
23
|
+
try {
|
|
24
|
+
const debug = new URLSearchParams(search).get("debug");
|
|
25
|
+
return debug?.split(",").some((flag) => flag.trim() === "slots") ?? false;
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function measureSlots() {
|
|
31
|
+
const elements = Array.from(document.querySelectorAll("[data-fx-slot]"));
|
|
32
|
+
const boxes = [];
|
|
33
|
+
for (const element of elements) {
|
|
34
|
+
const name = element.getAttribute("data-fx-slot") ?? "";
|
|
35
|
+
if (!name) continue;
|
|
36
|
+
let rect;
|
|
37
|
+
if (element.firstChild) {
|
|
38
|
+
const range = document.createRange();
|
|
39
|
+
range.selectNodeContents(element);
|
|
40
|
+
rect = range.getBoundingClientRect();
|
|
41
|
+
} else {
|
|
42
|
+
rect = element.getBoundingClientRect();
|
|
43
|
+
}
|
|
44
|
+
if (rect.width === 0 && rect.height === 0) continue;
|
|
45
|
+
let depth = 0;
|
|
46
|
+
let parentName = null;
|
|
47
|
+
let cursor = element.parentElement;
|
|
48
|
+
while (cursor) {
|
|
49
|
+
if (cursor.hasAttribute("data-fx-slot")) {
|
|
50
|
+
parentName ??= cursor.getAttribute("data-fx-slot");
|
|
51
|
+
depth += 1;
|
|
52
|
+
}
|
|
53
|
+
cursor = cursor.parentElement;
|
|
54
|
+
}
|
|
55
|
+
boxes.push({
|
|
56
|
+
name,
|
|
57
|
+
parentName,
|
|
58
|
+
depth,
|
|
59
|
+
left: rect.left,
|
|
60
|
+
top: rect.top,
|
|
61
|
+
width: rect.width,
|
|
62
|
+
height: rect.height
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return boxes;
|
|
66
|
+
}
|
|
67
|
+
function SlotDebugOverlay() {
|
|
68
|
+
const [boxes, setBoxes] = useState([]);
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (typeof document === "undefined") return;
|
|
71
|
+
let frame = 0;
|
|
72
|
+
const schedule = () => {
|
|
73
|
+
cancelAnimationFrame(frame);
|
|
74
|
+
frame = requestAnimationFrame(() => setBoxes(measureSlots()));
|
|
75
|
+
};
|
|
76
|
+
setBoxes(measureSlots());
|
|
77
|
+
const resizeObserver = new ResizeObserver(schedule);
|
|
78
|
+
const attach = () => {
|
|
79
|
+
resizeObserver.disconnect();
|
|
80
|
+
document.querySelectorAll("[data-fx-slot]").forEach((element) => resizeObserver.observe(element));
|
|
81
|
+
};
|
|
82
|
+
attach();
|
|
83
|
+
const mutationObserver = new MutationObserver((records) => {
|
|
84
|
+
let markerSetChanged = false;
|
|
85
|
+
for (const record of records) {
|
|
86
|
+
if (record.type === "attributes" && record.attributeName === "data-fx-slot") {
|
|
87
|
+
markerSetChanged = true;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
if (record.type !== "childList") continue;
|
|
91
|
+
const nodes = [...record.addedNodes, ...record.removedNodes];
|
|
92
|
+
markerSetChanged = nodes.some((node) => node instanceof Element && (node.matches("[data-fx-slot]") || node.querySelector("[data-fx-slot]")));
|
|
93
|
+
if (markerSetChanged) break;
|
|
94
|
+
}
|
|
95
|
+
if (markerSetChanged) attach();
|
|
96
|
+
schedule();
|
|
97
|
+
});
|
|
98
|
+
mutationObserver.observe(document.body, {
|
|
99
|
+
subtree: true,
|
|
100
|
+
childList: true,
|
|
101
|
+
attributes: true,
|
|
102
|
+
attributeFilter: ["data-fx-slot"]
|
|
103
|
+
});
|
|
104
|
+
window.addEventListener("resize", schedule);
|
|
105
|
+
window.addEventListener("scroll", schedule, true);
|
|
106
|
+
return () => {
|
|
107
|
+
cancelAnimationFrame(frame);
|
|
108
|
+
resizeObserver.disconnect();
|
|
109
|
+
mutationObserver.disconnect();
|
|
110
|
+
window.removeEventListener("resize", schedule);
|
|
111
|
+
window.removeEventListener("scroll", schedule, true);
|
|
112
|
+
};
|
|
113
|
+
}, []);
|
|
114
|
+
return /* @__PURE__ */ jsx(
|
|
115
|
+
"div",
|
|
116
|
+
{
|
|
117
|
+
style: {
|
|
118
|
+
position: "fixed",
|
|
119
|
+
inset: 0,
|
|
120
|
+
pointerEvents: "none",
|
|
121
|
+
zIndex: OVERLAY_Z
|
|
122
|
+
},
|
|
123
|
+
"data-fx-slot-overlay": "",
|
|
124
|
+
children: boxes.map((box, index) => {
|
|
125
|
+
const hue = hashSlotHue(box.name);
|
|
126
|
+
const corner = LABEL_CORNERS[box.depth % LABEL_CORNERS.length];
|
|
127
|
+
const label = box.parentName ? `${box.parentName} \u2192 ${box.name}` : box.name;
|
|
128
|
+
return /* @__PURE__ */ jsx(
|
|
129
|
+
"div",
|
|
130
|
+
{
|
|
131
|
+
style: {
|
|
132
|
+
position: "fixed",
|
|
133
|
+
left: box.left,
|
|
134
|
+
top: box.top,
|
|
135
|
+
width: box.width,
|
|
136
|
+
height: box.height,
|
|
137
|
+
background: `hsla(${hue}, 65%, 55%, ${Math.min(0.35, 0.1 + box.depth * 0.06)})`,
|
|
138
|
+
outline: `${Math.max(1, 3 - box.depth)}px solid hsla(${hue}, 65%, 55%, 0.7)`,
|
|
139
|
+
pointerEvents: "none"
|
|
140
|
+
},
|
|
141
|
+
children: /* @__PURE__ */ jsx(
|
|
142
|
+
"span",
|
|
143
|
+
{
|
|
144
|
+
style: {
|
|
145
|
+
position: "absolute",
|
|
146
|
+
...corner,
|
|
147
|
+
background: `hsla(${hue}, 65%, 25%, 0.9)`,
|
|
148
|
+
color: "#fff",
|
|
149
|
+
font: "10px/14px ui-monospace, monospace",
|
|
150
|
+
padding: "1px 4px",
|
|
151
|
+
borderRadius: 2,
|
|
152
|
+
whiteSpace: "nowrap"
|
|
153
|
+
},
|
|
154
|
+
children: label
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
},
|
|
158
|
+
`${box.name}:${index}`
|
|
159
|
+
);
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/react.tsx
|
|
3
166
|
function ShellSlot({
|
|
4
167
|
as = "div",
|
|
5
168
|
name,
|
|
@@ -8,5 +171,8 @@ function ShellSlot({
|
|
|
8
171
|
return createElement(as, { ...props, "data-fx-slot": name });
|
|
9
172
|
}
|
|
10
173
|
export {
|
|
11
|
-
ShellSlot
|
|
174
|
+
ShellSlot,
|
|
175
|
+
SlotDebugOverlay,
|
|
176
|
+
hashSlotHue,
|
|
177
|
+
isSlotDebugEnabled
|
|
12
178
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgeax/app-shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Generic Dock, Panel, Window, and Slot composition primitives",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"react": "^19.0.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
+
"@happy-dom/global-registrator": "^20.10.2",
|
|
39
40
|
"@types/react": "^19.0.0",
|
|
40
41
|
"@types/react-dom": "^19.0.0",
|
|
41
42
|
"bun-types": "^1.3.14",
|