@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 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 { HTMLAttributes, ReactElement } from 'react';
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.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",