@exegia/corpora-ui 0.20.0 → 0.22.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/dist-lib/components/blocks/auth/auth-flow-atom.d.ts +77 -0
- package/dist-lib/components/blocks/auth/auth-session-atom.d.ts +28 -0
- package/dist-lib/components/blocks/auth/auth-state-type.d.ts +71 -0
- package/dist-lib/components/blocks/auth/auth-state.d.ts +9 -0
- package/dist-lib/components/blocks/auth/use-auth-state.d.ts +52 -0
- package/dist-lib/components/blocks/layout.d.ts +4 -1
- package/dist-lib/components/blocks/nav/sidebar/__tests__/ai-sidebar-atom.test.d.ts +1 -0
- package/dist-lib/components/blocks/nav/sidebar/ai-sidebar-atom.d.ts +217 -0
- package/dist-lib/components/blocks/nav/sidebar/index.d.ts +3 -1
- package/dist-lib/components/blocks/nav/sidebar/sidebar-context.d.ts +3 -0
- package/dist-lib/components/blocks/nav/sidebar/sidebar-row.d.ts +3 -1
- package/dist-lib/components/blocks/nav/sidebar/type.d.ts +109 -29
- package/dist-lib/components/blocks/nav/sidebar/use-ai-sidebar-state.d.ts +29 -0
- package/dist-lib/components/blocks/nav/sidebar/use-ai-sidebar.d.ts +6 -1
- package/dist-lib/components/blocks/nav/sidebar/utils.d.ts +1 -1
- package/dist-lib/components/blocks/profile/index.d.ts +7 -0
- package/dist-lib/components/blocks/profile/profile-card-atom.d.ts +68 -0
- package/dist-lib/components/blocks/profile/profile-card-block.d.ts +28 -39
- package/dist-lib/components/blocks/profile/type.d.ts +80 -0
- package/dist-lib/components/blocks/profile/use-profile-card-state.d.ts +24 -0
- package/dist-lib/components/blocks/profile/use-profile-card.d.ts +34 -0
- package/dist-lib/components/blocks/shell/__tests__/shell-fit-atom.test.d.ts +1 -0
- package/dist-lib/components/blocks/shell/__tests__/shell-metrics.test.d.ts +1 -0
- package/dist-lib/components/blocks/shell/animated-panel-provider.d.ts +1 -1
- package/dist-lib/components/blocks/shell/animated-panel.d.ts +1 -1
- package/dist-lib/components/blocks/shell/index.d.ts +4 -12
- package/dist-lib/components/blocks/shell/shell-fit-atom.d.ts +68 -0
- package/dist-lib/components/blocks/shell/shell-metrics.d.ts +54 -0
- package/dist-lib/components/blocks/shell/type.d.ts +115 -3
- package/dist-lib/components/blocks/shell/use-shell-fit-state.d.ts +25 -0
- package/dist-lib/components/blocks/shell/use-shell-fit.d.ts +14 -0
- package/dist-lib/components/blocks/shell/use-shell-panels.d.ts +12 -1
- package/dist-lib/components/blocks/shell/utils.d.ts +32 -0
- package/dist-lib/components/composed/__tests__/logo.test.d.ts +1 -0
- package/dist-lib/components/composed/logo.d.ts +34 -0
- package/dist-lib/components/composed/tree/__tests__/tree-atom.test.d.ts +1 -0
- package/dist-lib/components/composed/tree/constants.d.ts +4 -4
- package/dist-lib/components/composed/tree/index.d.ts +3 -1
- package/dist-lib/components/composed/tree/tree-atom.d.ts +160 -0
- package/dist-lib/components/composed/tree/tree-node.d.ts +6 -1
- package/dist-lib/components/composed/tree/type.d.ts +116 -18
- package/dist-lib/components/composed/tree/use-tree-dnd.d.ts +18 -7
- package/dist-lib/components/composed/tree/use-tree-state.d.ts +25 -0
- package/dist-lib/components/composed/tree/use-tree.d.ts +4 -0
- package/dist-lib/components/composed/user-avatar.d.ts +5 -28
- package/dist-lib/components/user-avatar/__tests__/user-avatar.test.d.ts +1 -0
- package/dist-lib/components/user-avatar/component.d.ts +1 -8
- package/dist-lib/components/user-avatar/index.d.ts +5 -0
- package/dist-lib/components/user-avatar/presence-badge.d.ts +16 -0
- package/dist-lib/components/user-avatar/type.d.ts +53 -0
- package/dist-lib/components/user-avatar/use-user-avatar-state.d.ts +26 -0
- package/dist-lib/components/user-avatar/use-user-avatar.d.ts +37 -0
- package/dist-lib/components/user-avatar/user-avatar-atom.d.ts +64 -0
- package/dist-lib/components/user-avatar/utils.d.ts +22 -0
- package/dist-lib/index.d.ts +6 -3
- package/dist-lib/index.js +3732 -2670
- package/dist-lib/index.js.map +1 -1
- package/dist-lib/state/exegia-provider.d.ts +51 -0
- package/dist-lib/state/index.d.ts +4 -0
- package/dist-lib/state/store.d.ts +17 -0
- package/package.json +15 -12
- package/src/components/blocks/auth/__tests__/auth-state-atom.test.tsx +247 -0
- package/src/components/blocks/auth/auth-flow-atom.ts +238 -0
- package/src/components/blocks/auth/auth-session-atom.ts +82 -0
- package/src/components/blocks/auth/auth-state-type.ts +97 -0
- package/src/components/blocks/auth/auth-state.ts +52 -0
- package/src/components/blocks/auth/use-auth-state.ts +127 -0
- package/src/components/blocks/layout.ts +30 -2
- package/src/components/blocks/nav/sidebar/__tests__/ai-sidebar-atom.test.tsx +325 -0
- package/src/components/blocks/nav/sidebar/ai-sidebar-atom.ts +698 -0
- package/src/components/blocks/nav/sidebar/ai-sidebar.tsx +48 -42
- package/src/components/blocks/nav/sidebar/index.ts +53 -1
- package/src/components/blocks/nav/sidebar/sidebar-context.ts +15 -0
- package/src/components/blocks/nav/sidebar/sidebar-row.tsx +119 -50
- package/src/components/blocks/nav/sidebar/type.ts +125 -30
- package/src/components/blocks/nav/sidebar/use-ai-sidebar-state.ts +123 -0
- package/src/components/blocks/nav/sidebar/use-ai-sidebar.ts +301 -248
- package/src/components/blocks/nav/sidebar/utils.ts +1 -1
- package/src/components/blocks/profile/__tests__/profile-card-block.test.tsx +194 -0
- package/src/components/blocks/profile/index.ts +28 -0
- package/src/components/blocks/profile/profile-card-atom.ts +247 -0
- package/src/components/blocks/profile/profile-card-block.tsx +153 -74
- package/src/components/blocks/profile/type.ts +95 -0
- package/src/components/blocks/profile/use-profile-card-state.ts +67 -0
- package/src/components/blocks/profile/use-profile-card.ts +126 -0
- package/src/components/blocks/shell/__tests__/shell-fit-atom.test.tsx +360 -0
- package/src/components/blocks/shell/__tests__/shell-layout.test.tsx +192 -3
- package/src/components/blocks/shell/__tests__/shell-metrics.test.ts +108 -0
- package/src/components/blocks/shell/animated-panel-inset.tsx +5 -1
- package/src/components/blocks/shell/animated-panel-provider.tsx +77 -11
- package/src/components/blocks/shell/animated-panel-trigger.tsx +4 -0
- package/src/components/blocks/shell/animated-panel.tsx +126 -29
- package/src/components/blocks/shell/index.ts +18 -13
- package/src/components/blocks/shell/shell-fit-atom.ts +243 -0
- package/src/components/blocks/shell/shell-layout.tsx +55 -53
- package/src/components/blocks/shell/shell-metrics.ts +79 -0
- package/src/components/blocks/shell/type.ts +130 -3
- package/src/components/blocks/shell/use-shell-fit-state.ts +49 -0
- package/src/components/blocks/shell/use-shell-fit.ts +135 -0
- package/src/components/blocks/shell/use-shell-panels.ts +57 -4
- package/src/components/blocks/shell/utils.ts +44 -4
- package/src/components/composed/__tests__/logo.test.tsx +60 -0
- package/src/components/composed/logo.tsx +159 -0
- package/src/components/composed/tree/CLAUDE.md +132 -0
- package/src/components/composed/tree/__tests__/tree-atom.test.tsx +217 -0
- package/src/components/composed/tree/__tests__/tree.test.tsx +162 -37
- package/src/components/composed/tree/__tests__/use-tree.test.tsx +2 -1
- package/src/components/composed/tree/constants.ts +4 -4
- package/src/components/composed/tree/index.ts +38 -0
- package/src/components/composed/tree/tree-atom.ts +590 -0
- package/src/components/composed/tree/tree-node.tsx +117 -78
- package/src/components/composed/tree/tree.tsx +24 -16
- package/src/components/composed/tree/type.ts +125 -15
- package/src/components/composed/tree/use-tree-dnd.ts +82 -51
- package/src/components/composed/tree/use-tree-state.ts +105 -0
- package/src/components/composed/tree/use-tree.ts +185 -184
- package/src/components/composed/user-avatar.tsx +11 -99
- package/src/components/user-avatar/__tests__/user-avatar.test.tsx +286 -0
- package/src/components/user-avatar/component.tsx +151 -22
- package/src/components/user-avatar/fallback.tsx +3 -1
- package/src/components/user-avatar/index.ts +19 -0
- package/src/components/user-avatar/presence-badge.tsx +56 -0
- package/src/components/user-avatar/type.ts +58 -0
- package/src/components/user-avatar/use-user-avatar-state.ts +60 -0
- package/src/components/user-avatar/use-user-avatar.ts +180 -0
- package/src/components/user-avatar/user-avatar-atom.ts +218 -0
- package/src/components/user-avatar/utils.ts +94 -0
- package/src/index.ts +35 -3
- package/src/state/exegia-provider.tsx +79 -0
- package/src/state/index.ts +4 -0
- package/src/state/store.ts +19 -0
- package/dist-lib/components/blocks/nav/sidebar-block.d.ts +0 -14
- package/dist-lib/components/blocks/nav/sidebar-nav-row.d.ts +0 -3
- package/dist-lib/components/blocks/nav/types.d.ts +0 -60
- package/dist-lib/components/blocks/nav/utils.d.ts +0 -1
- package/dist-lib/components/blocks/shell/animated-sidebar-content.d.ts +0 -2
- package/dist-lib/components/blocks/shell/animated-sidebar-footer.d.ts +0 -2
- package/dist-lib/components/blocks/shell/animated-sidebar-group-content.d.ts +0 -2
- package/dist-lib/components/blocks/shell/animated-sidebar-group-label.d.ts +0 -2
- package/dist-lib/components/blocks/shell/animated-sidebar-group.d.ts +0 -2
- package/dist-lib/components/blocks/shell/animated-sidebar-header.d.ts +0 -2
- package/dist-lib/components/blocks/shell/animated-sidebar-menu-button.d.ts +0 -3
- package/dist-lib/components/blocks/shell/animated-sidebar-menu-item.d.ts +0 -2
- package/dist-lib/components/blocks/shell/animated-sidebar-menu-sub-button.d.ts +0 -3
- package/dist-lib/components/blocks/shell/animated-sidebar-menu-sub-item.d.ts +0 -2
- package/dist-lib/components/blocks/shell/animated-sidebar-menu-sub.d.ts +0 -2
- package/dist-lib/components/blocks/shell/animated-sidebar-menu.d.ts +0 -2
- package/src/components/blocks/nav/__tests__/sidebar-block.test.tsx +0 -125
- package/src/components/blocks/nav/sidebar-block.tsx +0 -142
- package/src/components/blocks/nav/sidebar-nav-row.tsx +0 -78
- package/src/components/blocks/nav/types.ts +0 -65
- package/src/components/blocks/nav/utils.ts +0 -4
- package/src/components/blocks/shell/animated-sidebar-content.tsx +0 -19
- package/src/components/blocks/shell/animated-sidebar-footer.tsx +0 -19
- package/src/components/blocks/shell/animated-sidebar-group-content.tsx +0 -16
- package/src/components/blocks/shell/animated-sidebar-group-label.tsx +0 -29
- package/src/components/blocks/shell/animated-sidebar-group.tsx +0 -16
- package/src/components/blocks/shell/animated-sidebar-header.tsx +0 -16
- package/src/components/blocks/shell/animated-sidebar-menu-button.tsx +0 -151
- package/src/components/blocks/shell/animated-sidebar-menu-item.tsx +0 -20
- package/src/components/blocks/shell/animated-sidebar-menu-sub-button.tsx +0 -85
- package/src/components/blocks/shell/animated-sidebar-menu-sub-item.tsx +0 -19
- package/src/components/blocks/shell/animated-sidebar-menu-sub.tsx +0 -44
- package/src/components/blocks/shell/animated-sidebar-menu.tsx +0 -26
- /package/dist-lib/components/blocks/{nav/__tests__/sidebar-block.test.d.ts → auth/__tests__/auth-state-atom.test.d.ts} +0 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { act, render, screen } from "@testing-library/react"
|
|
3
|
+
import userEvent from "@testing-library/user-event"
|
|
4
|
+
import { createStore, getDefaultStore } from "jotai"
|
|
5
|
+
|
|
6
|
+
import { ExegiaProvider } from "@/state"
|
|
7
|
+
import { ShellLayout } from "../shell-layout"
|
|
8
|
+
import {
|
|
9
|
+
measureShellFitAtom,
|
|
10
|
+
mountShellFitAtom,
|
|
11
|
+
removeShellFitInstance,
|
|
12
|
+
resetShellPanelWidthAtom,
|
|
13
|
+
resizeShellPanelAtom,
|
|
14
|
+
shellFitFitsAtom,
|
|
15
|
+
shellFitMeasuredAtom,
|
|
16
|
+
shellFitPanelBoundsAtom,
|
|
17
|
+
shellFitPanelWidthAtom,
|
|
18
|
+
shellFitRequestedWidthAtom,
|
|
19
|
+
shellFitStateAtom,
|
|
20
|
+
} from "../shell-fit-atom"
|
|
21
|
+
import type { ShellMetrics } from "../shell-metrics"
|
|
22
|
+
import { useShellFitActions, useShellFitState } from "../use-shell-fit-state"
|
|
23
|
+
import { useShellPanels } from "../use-shell-panels"
|
|
24
|
+
import type { TPanelMap } from "../type"
|
|
25
|
+
import { SHELL_WIDTHS } from "../utils"
|
|
26
|
+
|
|
27
|
+
type Store = ReturnType<typeof createStore>
|
|
28
|
+
|
|
29
|
+
/** The shell's shipping numbers on a 1024px viewport: rail 256 + body 360 +
|
|
30
|
+
* panel 320 = 936, with 32px of frame. */
|
|
31
|
+
const WIDE: ShellMetrics = {
|
|
32
|
+
rail: 256,
|
|
33
|
+
insetMin: 360,
|
|
34
|
+
panelMin: 320,
|
|
35
|
+
viewport: 1024,
|
|
36
|
+
chrome: 32,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The same shell squeezed to 800px — 936 needed, no room. */
|
|
40
|
+
const NARROW: ShellMetrics = { ...WIDE, viewport: 800 }
|
|
41
|
+
|
|
42
|
+
function measure(store: Store, id: string, metrics: ShellMetrics) {
|
|
43
|
+
store.set(measureShellFitAtom(id), metrics)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe("shell-fit atoms · unmeasured", () => {
|
|
47
|
+
test("fails open: fits, no width, no measurement", () => {
|
|
48
|
+
const store = createStore()
|
|
49
|
+
|
|
50
|
+
expect(store.get(shellFitFitsAtom("a"))).toBe(true)
|
|
51
|
+
expect(store.get(shellFitMeasuredAtom("a"))).toBeNull()
|
|
52
|
+
expect(store.get(shellFitPanelWidthAtom("a"))).toBeNull()
|
|
53
|
+
expect(store.get(shellFitPanelBoundsAtom("a"))).toEqual({ min: 0, max: 0 })
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test("a resize before any measurement is remembered unclamped", () => {
|
|
57
|
+
const store = createStore()
|
|
58
|
+
store.set(resizeShellPanelAtom("a"), 900)
|
|
59
|
+
|
|
60
|
+
// Nothing to clamp against yet, so the ask is kept as-is …
|
|
61
|
+
expect(store.get(shellFitRequestedWidthAtom("a"))).toBe(900)
|
|
62
|
+
expect(store.get(shellFitPanelWidthAtom("a"))).toBeNull()
|
|
63
|
+
|
|
64
|
+
// … and clamped on read the moment there is a room to clamp to.
|
|
65
|
+
measure(store, "a", WIDE)
|
|
66
|
+
expect(store.get(shellFitPanelWidthAtom("a"))).toBe(1024 - 32 - 256 - 360)
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
describe("shell-fit atoms · the rule", () => {
|
|
71
|
+
test("fits while every column has its floor, not once they do not", () => {
|
|
72
|
+
const store = createStore()
|
|
73
|
+
|
|
74
|
+
measure(store, "a", WIDE)
|
|
75
|
+
expect(store.get(shellFitFitsAtom("a"))).toBe(true)
|
|
76
|
+
|
|
77
|
+
measure(store, "a", NARROW)
|
|
78
|
+
expect(store.get(shellFitFitsAtom("a"))).toBe(false)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test("the panel opens at its floor and may grow into the body's slack", () => {
|
|
82
|
+
const store = createStore()
|
|
83
|
+
measure(store, "a", WIDE)
|
|
84
|
+
|
|
85
|
+
expect(store.get(shellFitPanelWidthAtom("a"))).toBe(320)
|
|
86
|
+
expect(store.get(shellFitPanelBoundsAtom("a"))).toEqual({
|
|
87
|
+
min: 320,
|
|
88
|
+
max: 1024 - 32 - 256 - 360,
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test("a resize is clamped on the way in", () => {
|
|
93
|
+
const store = createStore()
|
|
94
|
+
measure(store, "a", WIDE)
|
|
95
|
+
|
|
96
|
+
store.set(resizeShellPanelAtom("a"), 900)
|
|
97
|
+
expect(store.get(shellFitRequestedWidthAtom("a"))).toBe(376)
|
|
98
|
+
|
|
99
|
+
store.set(resizeShellPanelAtom("a"), 10)
|
|
100
|
+
expect(store.get(shellFitRequestedWidthAtom("a"))).toBe(320)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test("a narrowing viewport pulls the panel down without forgetting the ask", () => {
|
|
104
|
+
const store = createStore()
|
|
105
|
+
measure(store, "a", WIDE)
|
|
106
|
+
store.set(resizeShellPanelAtom("a"), 360)
|
|
107
|
+
expect(store.get(shellFitPanelWidthAtom("a"))).toBe(360)
|
|
108
|
+
|
|
109
|
+
// Less slack: 1000 - 32 - 256 - 360 = 352.
|
|
110
|
+
measure(store, "a", { ...WIDE, viewport: 1000 })
|
|
111
|
+
expect(store.get(shellFitPanelWidthAtom("a"))).toBe(352)
|
|
112
|
+
expect(store.get(shellFitRequestedWidthAtom("a"))).toBe(360)
|
|
113
|
+
|
|
114
|
+
// Room comes back, and so does the width they chose.
|
|
115
|
+
measure(store, "a", WIDE)
|
|
116
|
+
expect(store.get(shellFitPanelWidthAtom("a"))).toBe(360)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test("folding the rail hands its column to the panel's ceiling", () => {
|
|
120
|
+
const store = createStore()
|
|
121
|
+
measure(store, "a", WIDE)
|
|
122
|
+
expect(store.get(shellFitPanelBoundsAtom("a")).max).toBe(376)
|
|
123
|
+
|
|
124
|
+
measure(store, "a", { ...WIDE, rail: 56 })
|
|
125
|
+
expect(store.get(shellFitPanelBoundsAtom("a")).max).toBe(576)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test("a measurement that changes nothing is not a change", () => {
|
|
129
|
+
const store = createStore()
|
|
130
|
+
measure(store, "a", WIDE)
|
|
131
|
+
const before = store.get(shellFitStateAtom("a"))
|
|
132
|
+
|
|
133
|
+
measure(store, "a", { ...WIDE })
|
|
134
|
+
expect(store.get(shellFitStateAtom("a"))).toBe(before)
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
describe("shell-fit atoms · seed and reset", () => {
|
|
139
|
+
test("mount seeds the requested width once", () => {
|
|
140
|
+
const store = createStore()
|
|
141
|
+
store.set(mountShellFitAtom("a"), { panelWidth: 400 })
|
|
142
|
+
measure(store, "a", { ...WIDE, viewport: 1400 })
|
|
143
|
+
expect(store.get(shellFitPanelWidthAtom("a"))).toBe(400)
|
|
144
|
+
|
|
145
|
+
// A later mount (StrictMode, a re-render with a new default) never
|
|
146
|
+
// overwrites a width the user dragged to.
|
|
147
|
+
store.set(resizeShellPanelAtom("a"), 500)
|
|
148
|
+
store.set(mountShellFitAtom("a"), { panelWidth: 340 })
|
|
149
|
+
expect(store.get(shellFitPanelWidthAtom("a"))).toBe(500)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test("reset goes back to the seed, or the floor without one", () => {
|
|
153
|
+
const store = createStore()
|
|
154
|
+
store.set(mountShellFitAtom("a"), { panelWidth: 400 })
|
|
155
|
+
measure(store, "a", { ...WIDE, viewport: 1400 })
|
|
156
|
+
store.set(resizeShellPanelAtom("a"), 500)
|
|
157
|
+
|
|
158
|
+
store.set(resetShellPanelWidthAtom("a"))
|
|
159
|
+
expect(store.get(shellFitPanelWidthAtom("a"))).toBe(400)
|
|
160
|
+
|
|
161
|
+
measure(store, "b", WIDE)
|
|
162
|
+
store.set(resizeShellPanelAtom("b"), 360)
|
|
163
|
+
store.set(resetShellPanelWidthAtom("b"))
|
|
164
|
+
expect(store.get(shellFitPanelWidthAtom("b"))).toBe(320)
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
describe("shell-fit atoms · instances", () => {
|
|
169
|
+
test("two ids never share state", () => {
|
|
170
|
+
const store = createStore()
|
|
171
|
+
measure(store, "a", WIDE)
|
|
172
|
+
measure(store, "b", NARROW)
|
|
173
|
+
|
|
174
|
+
expect(store.get(shellFitFitsAtom("a"))).toBe(true)
|
|
175
|
+
expect(store.get(shellFitFitsAtom("b"))).toBe(false)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
test("removing an instance drops its state", () => {
|
|
179
|
+
const store = createStore()
|
|
180
|
+
measure(store, "a", WIDE)
|
|
181
|
+
store.set(resizeShellPanelAtom("a"), 360)
|
|
182
|
+
|
|
183
|
+
removeShellFitInstance("a")
|
|
184
|
+
|
|
185
|
+
expect(store.get(shellFitMeasuredAtom("a"))).toBeNull()
|
|
186
|
+
expect(store.get(shellFitRequestedWidthAtom("a"))).toBeNull()
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
test("two stores never share state", () => {
|
|
190
|
+
const one = createStore()
|
|
191
|
+
const two = createStore()
|
|
192
|
+
measure(one, "a", NARROW)
|
|
193
|
+
|
|
194
|
+
expect(one.get(shellFitFitsAtom("a"))).toBe(false)
|
|
195
|
+
expect(two.get(shellFitFitsAtom("a"))).toBe(true)
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// By id, through React
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
const PANELS: TPanelMap = {
|
|
204
|
+
left: {
|
|
205
|
+
id: "nav",
|
|
206
|
+
name: "Navigation",
|
|
207
|
+
component: <div>Nav body</div>,
|
|
208
|
+
open: true,
|
|
209
|
+
side: "left",
|
|
210
|
+
},
|
|
211
|
+
right: {
|
|
212
|
+
id: "inspector",
|
|
213
|
+
name: "Inspector",
|
|
214
|
+
component: <div>Inspector body</div>,
|
|
215
|
+
open: true,
|
|
216
|
+
side: "right",
|
|
217
|
+
},
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Answer the shell's CSS-variable probes with real px (happy-dom has no
|
|
221
|
+
* layout engine); see `shell-layout.test.tsx` for the full story. */
|
|
222
|
+
function stubShellWidths() {
|
|
223
|
+
const px = Object.fromEntries(
|
|
224
|
+
Object.entries(SHELL_WIDTHS).map(([name, value]) => [
|
|
225
|
+
name,
|
|
226
|
+
Number.parseFloat(value),
|
|
227
|
+
])
|
|
228
|
+
)
|
|
229
|
+
const original = HTMLElement.prototype.getBoundingClientRect
|
|
230
|
+
HTMLElement.prototype.getBoundingClientRect = function (this: HTMLElement) {
|
|
231
|
+
const isProbe =
|
|
232
|
+
this.style.visibility === "hidden" && this.style.position === "absolute"
|
|
233
|
+
if (!isProbe) return original.call(this)
|
|
234
|
+
const variable = /var\((--[a-z-]+)\)/.exec(this.style.width)?.[1]
|
|
235
|
+
return { width: (variable && px[variable]) || 0 } as DOMRect
|
|
236
|
+
}
|
|
237
|
+
return () => {
|
|
238
|
+
HTMLElement.prototype.getBoundingClientRect = original
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Reads and drives the shell by id — mounted as a SIBLING of the shell, so
|
|
243
|
+
* it holds no controller and sits under no shell context. */
|
|
244
|
+
function RemoteInspectorControls({ shellId }: { shellId: string }) {
|
|
245
|
+
const { fits, panelWidth, bounds } = useShellFitState(shellId)
|
|
246
|
+
const { resizePanel, resetPanelWidth } = useShellFitActions(shellId)
|
|
247
|
+
return (
|
|
248
|
+
<div>
|
|
249
|
+
<p>{`fits: ${String(fits)}`}</p>
|
|
250
|
+
<p>{`width: ${String(panelWidth)}`}</p>
|
|
251
|
+
<p>{`max: ${bounds.max}`}</p>
|
|
252
|
+
<button onClick={() => resizePanel(360)} type="button">
|
|
253
|
+
Widen
|
|
254
|
+
</button>
|
|
255
|
+
<button onClick={resetPanelWidth} type="button">
|
|
256
|
+
Reset width
|
|
257
|
+
</button>
|
|
258
|
+
</div>
|
|
259
|
+
)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function NamedShellUnprovided({ shellId }: { shellId?: string }) {
|
|
263
|
+
const panels = useShellPanels({ shellId })
|
|
264
|
+
return <ShellLayout {...panels.providerProps} panels={PANELS} variant="web" />
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** `useShellPanels` reads the same atoms the shell writes, so it has to sit
|
|
268
|
+
* under the same `ExegiaProvider` as the shell — mounted above it, it would
|
|
269
|
+
* read Jotai's default store while the shell writes to the provider's. */
|
|
270
|
+
function NamedShell({ shellId }: { shellId?: string }) {
|
|
271
|
+
const panels = useShellPanels({ shellId, defaultPanelWidth: 340 })
|
|
272
|
+
return (
|
|
273
|
+
<>
|
|
274
|
+
<RemoteInspectorControls shellId={panels.shellId} />
|
|
275
|
+
<p>{`hook width: ${String(panels.panelWidth)}`}</p>
|
|
276
|
+
<ShellLayout {...panels.providerProps} panels={PANELS} variant="web" />
|
|
277
|
+
</>
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
describe("shell-fit · by id", () => {
|
|
282
|
+
test("a named shell is readable and resizable from outside it", async () => {
|
|
283
|
+
const user = userEvent.setup()
|
|
284
|
+
const restore = stubShellWidths()
|
|
285
|
+
try {
|
|
286
|
+
// happy-dom's default 1024px viewport: 936 needed. No stylesheet, so the
|
|
287
|
+
// frame measures 0 and the ceiling is 1024 - 256 - 360 = 408.
|
|
288
|
+
render(
|
|
289
|
+
<ExegiaProvider>
|
|
290
|
+
<NamedShell shellId="app-shell" />
|
|
291
|
+
</ExegiaProvider>
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
expect(screen.getByText("fits: true")).toBeDefined()
|
|
295
|
+
expect(screen.getByText("max: 408")).toBeDefined()
|
|
296
|
+
// `defaultPanelWidth` seeded the width, and the hook above the provider
|
|
297
|
+
// reads it back out of the same atoms.
|
|
298
|
+
expect(screen.getByText("width: 340")).toBeDefined()
|
|
299
|
+
expect(screen.getByText("hook width: 340")).toBeDefined()
|
|
300
|
+
|
|
301
|
+
await user.click(screen.getByRole("button", { name: "Widen" }))
|
|
302
|
+
expect(screen.getByText("width: 360")).toBeDefined()
|
|
303
|
+
expect(screen.getByText("hook width: 360")).toBeDefined()
|
|
304
|
+
|
|
305
|
+
await user.click(screen.getByRole("button", { name: "Reset width" }))
|
|
306
|
+
expect(screen.getByText("width: 340")).toBeDefined()
|
|
307
|
+
} finally {
|
|
308
|
+
restore()
|
|
309
|
+
removeShellFitInstance("app-shell")
|
|
310
|
+
}
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
test("a generated id is dropped with the hook that made it", () => {
|
|
314
|
+
const restore = stubShellWidths()
|
|
315
|
+
function Probe() {
|
|
316
|
+
const panels = useShellPanels()
|
|
317
|
+
return (
|
|
318
|
+
<ShellLayout {...panels.providerProps} panels={PANELS} variant="web">
|
|
319
|
+
<span data-testid="shell-id">{panels.shellId}</span>
|
|
320
|
+
</ShellLayout>
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
try {
|
|
324
|
+
const { unmount } = render(<Probe />)
|
|
325
|
+
const shellId = screen.getByTestId("shell-id").textContent ?? ""
|
|
326
|
+
// No ExegiaProvider here, so the hooks fall back to Jotai's default
|
|
327
|
+
// store — the provider wrote its measurement there under the hook's id.
|
|
328
|
+
const store = getDefaultStore()
|
|
329
|
+
expect(shellId).not.toBe("")
|
|
330
|
+
expect(store.get(shellFitMeasuredAtom(shellId))).not.toBeNull()
|
|
331
|
+
|
|
332
|
+
unmount()
|
|
333
|
+
// Nothing is left under it once the hook is gone: the family hands out
|
|
334
|
+
// a fresh atom, and a fresh atom reads as unmeasured.
|
|
335
|
+
expect(store.get(shellFitMeasuredAtom(shellId))).toBeNull()
|
|
336
|
+
} finally {
|
|
337
|
+
restore()
|
|
338
|
+
}
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
test("an explicit id outlives the shell", () => {
|
|
342
|
+
const restore = stubShellWidths()
|
|
343
|
+
try {
|
|
344
|
+
const store = getDefaultStore()
|
|
345
|
+
const { unmount } = render(<NamedShellUnprovided shellId="kept" />)
|
|
346
|
+
act(() => {
|
|
347
|
+
store.set(resizeShellPanelAtom("kept"), 360)
|
|
348
|
+
})
|
|
349
|
+
expect(store.get(shellFitRequestedWidthAtom("kept"))).toBe(360)
|
|
350
|
+
|
|
351
|
+
unmount()
|
|
352
|
+
// The dragged width is still there for the next mount — a route change
|
|
353
|
+
// does not reset the inspector.
|
|
354
|
+
expect(store.get(shellFitRequestedWidthAtom("kept"))).toBe(360)
|
|
355
|
+
} finally {
|
|
356
|
+
restore()
|
|
357
|
+
removeShellFitInstance("kept")
|
|
358
|
+
}
|
|
359
|
+
})
|
|
360
|
+
})
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { describe, expect, mock, test } from "bun:test"
|
|
2
|
-
import { render, screen } from "@testing-library/react"
|
|
2
|
+
import { act, render, screen } from "@testing-library/react"
|
|
3
3
|
import userEvent from "@testing-library/user-event"
|
|
4
4
|
|
|
5
5
|
import { ShellLayout } from "../shell-layout"
|
|
6
6
|
import { useShellPanels } from "../use-shell-panels"
|
|
7
|
+
import { SHELL_WIDTHS } from "../utils"
|
|
7
8
|
import type { TPanelMap, UseShellPanelsOptions } from "../type"
|
|
8
9
|
|
|
9
10
|
const PANELS: TPanelMap = {
|
|
@@ -23,6 +24,68 @@ const PANELS: TPanelMap = {
|
|
|
23
24
|
},
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
/** The shell resolves its columns from CSS variables with hidden probe
|
|
28
|
+
* elements. happy-dom has no layout engine, so every probe measures 0 and the
|
|
29
|
+
* fit rule fails open — answer the probes with the real px behind each
|
|
30
|
+
* variable so the rule works on the numbers it ships with:
|
|
31
|
+
*
|
|
32
|
+
* rail 256 (56 folded) + body 360 + panel 320
|
|
33
|
+
*
|
|
34
|
+
* Only probes are intercepted; every other box keeps happy-dom's answer. */
|
|
35
|
+
function stubShellWidths() {
|
|
36
|
+
const px = Object.fromEntries(
|
|
37
|
+
Object.entries(SHELL_WIDTHS).map(([name, value]) => [
|
|
38
|
+
name,
|
|
39
|
+
Number.parseFloat(value),
|
|
40
|
+
])
|
|
41
|
+
)
|
|
42
|
+
const original = HTMLElement.prototype.getBoundingClientRect
|
|
43
|
+
HTMLElement.prototype.getBoundingClientRect = function (this: HTMLElement) {
|
|
44
|
+
const isProbe =
|
|
45
|
+
this.style.visibility === "hidden" && this.style.position === "absolute"
|
|
46
|
+
if (!isProbe) return original.call(this)
|
|
47
|
+
|
|
48
|
+
const variable = /var\((--[a-z-]+)\)/.exec(this.style.width)?.[1]
|
|
49
|
+
return { width: (variable && px[variable]) || 0 } as DOMRect
|
|
50
|
+
}
|
|
51
|
+
return () => {
|
|
52
|
+
HTMLElement.prototype.getBoundingClientRect = original
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface HappyDOMWindow {
|
|
57
|
+
happyDOM?: { setViewport: (viewport: { width: number }) => void }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Resize the viewport the way the shell hears it: happy-dom's own viewport
|
|
61
|
+
* plus the `resize` event, inside `act` so the remeasure lands in this tick. */
|
|
62
|
+
function resizeViewport(width: number) {
|
|
63
|
+
act(() => {
|
|
64
|
+
;(globalThis as HappyDOMWindow).happyDOM?.setViewport({ width })
|
|
65
|
+
window.dispatchEvent(new Event("resize"))
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** happy-dom's default, restored between viewport tests. */
|
|
70
|
+
const WIDE_VIEWPORT = 1024
|
|
71
|
+
|
|
72
|
+
/** Mount the shell at `width` px of viewport with real column widths in play.
|
|
73
|
+
* The returned function puts both back — inside `act`, because happy-dom
|
|
74
|
+
* fires `resize` on `setViewport` and the shell (still mounted until
|
|
75
|
+
* cleanup) remeasures on it. */
|
|
76
|
+
function shellViewport(width: number) {
|
|
77
|
+
const restoreWidths = stubShellWidths()
|
|
78
|
+
;(globalThis as HappyDOMWindow).happyDOM?.setViewport({ width })
|
|
79
|
+
return () => {
|
|
80
|
+
restoreWidths()
|
|
81
|
+
act(() => {
|
|
82
|
+
;(globalThis as HappyDOMWindow).happyDOM?.setViewport({
|
|
83
|
+
width: WIDE_VIEWPORT,
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
26
89
|
/** The desktop rail / drawer elements, queried by their landmark roles. */
|
|
27
90
|
function leftRail() {
|
|
28
91
|
return screen.getByRole("navigation", { name: "Primary navigation" })
|
|
@@ -73,7 +136,7 @@ describe("ShellLayout", () => {
|
|
|
73
136
|
expect(leftRail().getAttribute("data-state")).toBe("collapsed")
|
|
74
137
|
})
|
|
75
138
|
|
|
76
|
-
test("
|
|
139
|
+
test("the shell owns the trigger icon, ignoring a panel's trigger node", async () => {
|
|
77
140
|
const user = userEvent.setup()
|
|
78
141
|
render(
|
|
79
142
|
<ShellLayout
|
|
@@ -86,12 +149,19 @@ describe("ShellLayout", () => {
|
|
|
86
149
|
)
|
|
87
150
|
|
|
88
151
|
const rightTrigger = screen.getByRole("button", { name: "Toggle panel" })
|
|
89
|
-
expect(rightTrigger.textContent).toContain("Custom trigger")
|
|
152
|
+
expect(rightTrigger.textContent).not.toContain("Custom trigger")
|
|
90
153
|
|
|
91
154
|
await user.click(rightTrigger)
|
|
92
155
|
expect(rightDrawer().getAttribute("data-state")).toBe("expanded")
|
|
93
156
|
})
|
|
94
157
|
|
|
158
|
+
test("a side with no panel gets no header trigger", () => {
|
|
159
|
+
render(<ShellLayout panels={{ left: PANELS.left! }} variant="web" />)
|
|
160
|
+
|
|
161
|
+
expect(screen.getByRole("button", { name: "Toggle sidebar" })).toBeDefined()
|
|
162
|
+
expect(screen.queryByRole("button", { name: "Toggle panel" })).toBeNull()
|
|
163
|
+
})
|
|
164
|
+
|
|
95
165
|
test("a panel's own defaultOpen flag seeds its side's initial state", () => {
|
|
96
166
|
render(
|
|
97
167
|
<ShellLayout
|
|
@@ -122,6 +192,56 @@ describe("ShellLayout", () => {
|
|
|
122
192
|
expect(rightDrawer().getAttribute("data-state")).toBe("collapsed")
|
|
123
193
|
})
|
|
124
194
|
|
|
195
|
+
test("keeps the right panel while every column fits", () => {
|
|
196
|
+
// 256 + 360 + 320 = 936, inside happy-dom's 1024px viewport.
|
|
197
|
+
const restore = shellViewport(WIDE_VIEWPORT)
|
|
198
|
+
try {
|
|
199
|
+
render(<ShellLayout panels={PANELS} variant="web" />)
|
|
200
|
+
|
|
201
|
+
expect(rightDrawer()).toBeDefined()
|
|
202
|
+
expect(screen.getByRole("button", { name: "Toggle panel" })).toBeDefined()
|
|
203
|
+
} finally {
|
|
204
|
+
restore()
|
|
205
|
+
}
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
test("drops the right panel when the columns no longer fit", () => {
|
|
209
|
+
// 936 needed, 800 available.
|
|
210
|
+
const restore = shellViewport(800)
|
|
211
|
+
try {
|
|
212
|
+
render(<ShellLayout panels={PANELS} variant="web" />)
|
|
213
|
+
|
|
214
|
+
expect(screen.queryByRole("complementary")).toBeNull()
|
|
215
|
+
// Its trigger goes with it rather than driving nothing.
|
|
216
|
+
expect(screen.queryByRole("button", { name: "Toggle panel" })).toBeNull()
|
|
217
|
+
// The rail and the body are untouched — they are never the ones to go.
|
|
218
|
+
expect(leftRail()).toBeDefined()
|
|
219
|
+
expect(screen.getByRole("main")).toBeDefined()
|
|
220
|
+
} finally {
|
|
221
|
+
restore()
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
test("folding the left rail hands its column to the right panel", async () => {
|
|
226
|
+
const user = userEvent.setup()
|
|
227
|
+
// Expanded: 256 + 360 + 320 = 936 > 800. Folded: 56 + 360 + 320 = 736.
|
|
228
|
+
const restore = shellViewport(800)
|
|
229
|
+
try {
|
|
230
|
+
render(<ShellLayout panels={PANELS} variant="web" />)
|
|
231
|
+
|
|
232
|
+
expect(leftRail().getAttribute("data-state")).toBe("expanded")
|
|
233
|
+
expect(screen.queryByRole("complementary")).toBeNull()
|
|
234
|
+
|
|
235
|
+
await user.click(screen.getByRole("button", { name: "Toggle sidebar" }))
|
|
236
|
+
|
|
237
|
+
expect(leftRail().getAttribute("data-state")).toBe("collapsed")
|
|
238
|
+
expect(rightDrawer()).toBeDefined()
|
|
239
|
+
expect(screen.getByRole("button", { name: "Toggle panel" })).toBeDefined()
|
|
240
|
+
} finally {
|
|
241
|
+
restore()
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
|
|
125
245
|
test("⌘B toggles the left rail", async () => {
|
|
126
246
|
const user = userEvent.setup()
|
|
127
247
|
render(<ShellLayout panels={PANELS} variant="web" />)
|
|
@@ -148,6 +268,7 @@ function HookedShell({
|
|
|
148
268
|
<button onClick={() => panels.toggle("right")} type="button">
|
|
149
269
|
External toggle
|
|
150
270
|
</button>
|
|
271
|
+
{panels.isNarrow && <p>No room for the inspector</p>}
|
|
151
272
|
</ShellLayout>
|
|
152
273
|
)
|
|
153
274
|
}
|
|
@@ -180,6 +301,74 @@ describe("useShellPanels", () => {
|
|
|
180
301
|
expect(rightDrawer().getAttribute("data-state")).toBe("collapsed")
|
|
181
302
|
})
|
|
182
303
|
|
|
304
|
+
test("mirrors the shell's too-narrow verdict back into the hook", () => {
|
|
305
|
+
const restore = shellViewport(800)
|
|
306
|
+
try {
|
|
307
|
+
render(<HookedShell />)
|
|
308
|
+
|
|
309
|
+
// The shell measures, the hook hears about it, and outside UI stands
|
|
310
|
+
// down with the panel it would have driven.
|
|
311
|
+
expect(screen.getByText("No room for the inspector")).toBeDefined()
|
|
312
|
+
expect(screen.queryByRole("complementary")).toBeNull()
|
|
313
|
+
} finally {
|
|
314
|
+
restore()
|
|
315
|
+
}
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
test("leaves the hook's verdict alone while the panel fits", () => {
|
|
319
|
+
const restore = shellViewport(WIDE_VIEWPORT)
|
|
320
|
+
try {
|
|
321
|
+
render(<HookedShell />)
|
|
322
|
+
|
|
323
|
+
expect(screen.queryByText("No room for the inspector")).toBeNull()
|
|
324
|
+
expect(rightDrawer()).toBeDefined()
|
|
325
|
+
} finally {
|
|
326
|
+
restore()
|
|
327
|
+
}
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
test("refuses to open the right panel while the shell is too narrow", async () => {
|
|
331
|
+
const user = userEvent.setup()
|
|
332
|
+
const onPanelChange = mock(() => {})
|
|
333
|
+
const restore = shellViewport(800)
|
|
334
|
+
try {
|
|
335
|
+
render(<HookedShell onPanelChange={onPanelChange} />)
|
|
336
|
+
|
|
337
|
+
await user.click(screen.getByRole("button", { name: "External toggle" }))
|
|
338
|
+
|
|
339
|
+
// Nothing opened, and nothing was reported — the write never happened,
|
|
340
|
+
// so widening the viewport later cannot surface a stale panel.
|
|
341
|
+
expect(screen.queryByRole("complementary")).toBeNull()
|
|
342
|
+
expect(onPanelChange).not.toHaveBeenCalled()
|
|
343
|
+
} finally {
|
|
344
|
+
restore()
|
|
345
|
+
}
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
test("retires the right panel's state when the shell goes too narrow", async () => {
|
|
349
|
+
const user = userEvent.setup()
|
|
350
|
+
const onPanelChange = mock(() => {})
|
|
351
|
+
const restore = shellViewport(WIDE_VIEWPORT)
|
|
352
|
+
try {
|
|
353
|
+
render(<HookedShell onPanelChange={onPanelChange} />)
|
|
354
|
+
|
|
355
|
+
await user.click(screen.getByRole("button", { name: "External toggle" }))
|
|
356
|
+
expect(rightDrawer().getAttribute("data-state")).toBe("expanded")
|
|
357
|
+
|
|
358
|
+
resizeViewport(800)
|
|
359
|
+
|
|
360
|
+
expect(screen.queryByRole("complementary")).toBeNull()
|
|
361
|
+
expect(onPanelChange).toHaveBeenLastCalledWith(false, "right")
|
|
362
|
+
|
|
363
|
+
// The state went with the panel: coming back does not spring it open.
|
|
364
|
+
resizeViewport(WIDE_VIEWPORT)
|
|
365
|
+
|
|
366
|
+
expect(rightDrawer().getAttribute("data-state")).toBe("collapsed")
|
|
367
|
+
} finally {
|
|
368
|
+
restore()
|
|
369
|
+
}
|
|
370
|
+
})
|
|
371
|
+
|
|
183
372
|
test("the shell's own triggers round-trip through the hook's state", async () => {
|
|
184
373
|
const user = userEvent.setup()
|
|
185
374
|
render(<HookedShell />)
|