@parallelworks/ui 0.92.0 → 0.93.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 +74 -1
- package/dist/app-frame/app-frame.css +281 -0
- package/dist/app-frame/index.d.ts +73 -0
- package/dist/app-frame/index.js +369 -0
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@ so an application ships only what it imports:
|
|
|
8
8
|
| `.` | theme contract, primitives (buttons, inputs, tables, tooltips, modals, dropdowns, settings layouts, page header), `UIProvider` |
|
|
9
9
|
| `./theme` | theme seeds and presets, `generateTheme`, `applyTheme`, `fitSeed` |
|
|
10
10
|
| `./icons` | the icon barrel |
|
|
11
|
+
| `./app-frame` | inset application frame, resizable navigation, icon rail, and mobile drawer |
|
|
11
12
|
| `./list` | list pages, tables, row menus and list controls |
|
|
12
13
|
| `./form` | `DynamicForm` and its field registry |
|
|
13
14
|
| `./graph` | job dependency graph and tree view, and the visual workflow editor: the graph, the run form as an inputs editor, and their dialogs |
|
|
@@ -16,7 +17,7 @@ so an application ships only what it imports:
|
|
|
16
17
|
| `./file-explorer` | object storage explorer over a host-supplied provider |
|
|
17
18
|
| `./access` | `AccessDrawer`: who has access to a resource, and granting roles to people and groups |
|
|
18
19
|
| `./ai`, `./ai/openai`, `./ai/mock` | chat interface and adapters |
|
|
19
|
-
| `./theme.css`, `./base.css`, `./fonts.css`, `./styles.css`, `./logviewer.css`, `./ai.css` | stylesheets |
|
|
20
|
+
| `./theme.css`, `./base.css`, `./fonts.css`, `./styles.css`, `./logviewer.css`, `./ai.css`, `./app-frame.css` | stylesheets |
|
|
20
21
|
|
|
21
22
|
Hosts bind strings, navigation, data hooks and the workflow engine through
|
|
22
23
|
`UIProvider`, which also shows the package's toasts:
|
|
@@ -1245,3 +1246,75 @@ colors and sizing can obscure labels or make controls hard to use. Keep trusted
|
|
|
1245
1246
|
confirmation summaries, permissions, and security notices outside the authored
|
|
1246
1247
|
layout. Full stylesheets or mutually untrusted embedded applications require a
|
|
1247
1248
|
separate isolation design.
|
|
1249
|
+
|
|
1250
|
+
## Application frame
|
|
1251
|
+
|
|
1252
|
+
`AppFrame` provides a sidebar and an inset content surface. The sidebar collapses
|
|
1253
|
+
to a 64px icon rail, resizes with a pointer or keyboard, and becomes a modal
|
|
1254
|
+
navigation drawer below 768px. Its header, scrolling navigation, and footer stay
|
|
1255
|
+
separate from the page's scrolling content. Long navigation labels truncate within
|
|
1256
|
+
the current sidebar width; badges remain visible. Navigation scrollbar tracks stay
|
|
1257
|
+
hidden while wheel and keyboard scrolling keep destinations reachable in short
|
|
1258
|
+
containers.
|
|
1259
|
+
|
|
1260
|
+
```tsx
|
|
1261
|
+
import { AppFrame, AppFrameNavItem, AppFrameToggle } from '@parallelworks/ui/app-frame'
|
|
1262
|
+
import '@parallelworks/ui/app-frame.css'
|
|
1263
|
+
|
|
1264
|
+
<AppFrame
|
|
1265
|
+
navigationKey={pathname}
|
|
1266
|
+
brand={({ expanded }) => expanded ? <Wordmark /> : <Mark />}
|
|
1267
|
+
navigation={
|
|
1268
|
+
<nav aria-label="Main navigation">
|
|
1269
|
+
<AppFrameNavItem label="Projects" icon={<ProjectsIcon />} href="/projects" selected />
|
|
1270
|
+
</nav>
|
|
1271
|
+
}
|
|
1272
|
+
footer={({ isMobile }) => !isMobile && <AppFrameToggle />}
|
|
1273
|
+
header={<PageTools />}
|
|
1274
|
+
>
|
|
1275
|
+
<ProjectList />
|
|
1276
|
+
</AppFrame>
|
|
1277
|
+
```
|
|
1278
|
+
|
|
1279
|
+
`PageTools` can use `useAppFrame().isMobile` to show an `AppFrameToggle` on small
|
|
1280
|
+
screens. The drawer supplies its own close button, traps focus, closes on Escape
|
|
1281
|
+
or an outside click, and returns focus to its opener. Navigation items close it
|
|
1282
|
+
on an ordinary click; modified links keep the current view in place. A changed
|
|
1283
|
+
`navigationKey` also dismisses it.
|
|
1284
|
+
Reopening mobile navigation restores its scroll position within the current
|
|
1285
|
+
section. Closing it unmounts its contents, including any nested popups.
|
|
1286
|
+
Use `navigationResetKey` for a section identifier when navigation scroll should
|
|
1287
|
+
survive route changes within that section. It defaults to `navigationKey`.
|
|
1288
|
+
`sidebarProps` passes host attributes, styling, and events to the desktop sidebar
|
|
1289
|
+
and mobile dialog, for example a navigation context menu. The frame retains its
|
|
1290
|
+
own layout, dialog semantics, and measured width.
|
|
1291
|
+
|
|
1292
|
+
The host owns routing, persistence, identity, permissions, and page tools. Links
|
|
1293
|
+
use `UIProvider`'s link slot. Pass `expanded`/`onExpandedChange` and
|
|
1294
|
+
`width`/`onWidthChange` to persist preferences, or use the uncontrolled defaults.
|
|
1295
|
+
Pointer drags preview their width locally and call `onWidthChange` once when the
|
|
1296
|
+
gesture ends, so persistence does not run on every pointer movement.
|
|
1297
|
+
`defaultWidth` is 240, bounded by `minWidth` (200) and `maxWidth` (400); compact
|
|
1298
|
+
applications can choose smaller bounds. The resize handle supports arrows, Home,
|
|
1299
|
+
End, drag-to-collapse, and double-click to restore `defaultWidth`. Logical layout
|
|
1300
|
+
and resizing support right-to-left direction. Reduced motion disables resizing
|
|
1301
|
+
and drawer transitions. `resizable={false}` removes the handle.
|
|
1302
|
+
|
|
1303
|
+
`brand`, `navigation`, and `footer` accept nodes or functions receiving the frame
|
|
1304
|
+
state. `header` sits above the main content; `banner` spans both columns.
|
|
1305
|
+
`fullBleed` removes the 8px inset and 12px rounded border. The frame fills the
|
|
1306
|
+
viewport by default; pass `style={{ height: '100%' }}` for a bounded container.
|
|
1307
|
+
The mobile drawer stays inside the frame, below its banner, and uses the chosen
|
|
1308
|
+
sidebar width, capped to leave 48px for dismissal. It slides from the navigation
|
|
1309
|
+
edge while the backdrop fades; both finish in 200ms and respect reduced motion.
|
|
1310
|
+
|
|
1311
|
+
The five `--theme-sidebar-*` tokens produced by `generateTheme` style navigation:
|
|
1312
|
+
`bg`, `text`, `hover`, `active-bg`, and `active-text`. Without them the frame uses
|
|
1313
|
+
the ordinary surface and accent tokens. Content uses `--theme-app-bg` and
|
|
1314
|
+
`--theme-border`. Supply translated `labels` for navigation, skip, open, close,
|
|
1315
|
+
expand, collapse, and resize actions. The frame owns the main landmark and skip
|
|
1316
|
+
link; put page content inside it without nesting a second `<main>`.
|
|
1317
|
+
|
|
1318
|
+
Frame styles use the `pw-ui` cascade layer. Put broad host element defaults
|
|
1319
|
+
(such as anchor colors and scrollbar styling) in a lower-priority layer declared
|
|
1320
|
+
before the package styles, so they do not replace navigation states.
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
@layer pw-ui {
|
|
2
|
+
/* The frame uses the same sidebar tokens as the theme generator. */
|
|
3
|
+
.pw-app-frame {
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
height: 100dvh;
|
|
7
|
+
min-height: 0;
|
|
8
|
+
isolation: isolate;
|
|
9
|
+
background: var(--theme-sidebar-bg, var(--theme-muted-panel-bg));
|
|
10
|
+
color: var(--theme-app);
|
|
11
|
+
}
|
|
12
|
+
.pw-frame-body {
|
|
13
|
+
display: flex;
|
|
14
|
+
flex: 1;
|
|
15
|
+
min-height: 0;
|
|
16
|
+
position: relative;
|
|
17
|
+
overflow: hidden;
|
|
18
|
+
}
|
|
19
|
+
.pw-frame-sidebar,
|
|
20
|
+
.pw-frame-drawer {
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-direction: column;
|
|
23
|
+
flex-shrink: 0;
|
|
24
|
+
min-height: 0;
|
|
25
|
+
background: var(--theme-sidebar-bg, var(--theme-muted-panel-bg));
|
|
26
|
+
color: var(--theme-sidebar-text, var(--theme-app));
|
|
27
|
+
}
|
|
28
|
+
.pw-frame-sidebar {
|
|
29
|
+
position: relative;
|
|
30
|
+
transition: width 200ms cubic-bezier(0.25, 0.1, 0.25, 1);
|
|
31
|
+
}
|
|
32
|
+
.pw-frame-sidebar[data-resizing] {
|
|
33
|
+
transition: none;
|
|
34
|
+
user-select: none;
|
|
35
|
+
}
|
|
36
|
+
.pw-frame-brand {
|
|
37
|
+
padding: 12px 12px 4px;
|
|
38
|
+
flex-shrink: 0;
|
|
39
|
+
min-width: 0;
|
|
40
|
+
overflow: hidden;
|
|
41
|
+
}
|
|
42
|
+
.pw-frame-links {
|
|
43
|
+
flex: 1;
|
|
44
|
+
overflow-x: hidden;
|
|
45
|
+
overflow-y: auto;
|
|
46
|
+
padding: 12px;
|
|
47
|
+
min-width: 0;
|
|
48
|
+
min-height: 0;
|
|
49
|
+
scrollbar-width: none;
|
|
50
|
+
}
|
|
51
|
+
.pw-frame-links::-webkit-scrollbar {
|
|
52
|
+
display: none;
|
|
53
|
+
}
|
|
54
|
+
.pw-frame-links nav {
|
|
55
|
+
display: grid;
|
|
56
|
+
grid-template-columns: minmax(0, 1fr);
|
|
57
|
+
gap: 4px;
|
|
58
|
+
}
|
|
59
|
+
.pw-frame-footer {
|
|
60
|
+
flex-shrink: 0;
|
|
61
|
+
padding: 8px 12px;
|
|
62
|
+
}
|
|
63
|
+
.pw-frame-nav-item {
|
|
64
|
+
display: flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
gap: 8px;
|
|
67
|
+
min-height: 36px;
|
|
68
|
+
min-width: 0;
|
|
69
|
+
width: 100%;
|
|
70
|
+
border: 0;
|
|
71
|
+
border-radius: 8px;
|
|
72
|
+
padding: 8px;
|
|
73
|
+
background: transparent;
|
|
74
|
+
color: inherit;
|
|
75
|
+
font: inherit;
|
|
76
|
+
font-size: 14px;
|
|
77
|
+
font-weight: 500;
|
|
78
|
+
line-height: 20px;
|
|
79
|
+
cursor: pointer;
|
|
80
|
+
text-decoration: none;
|
|
81
|
+
text-align: start;
|
|
82
|
+
transition: background-color 150ms ease-out;
|
|
83
|
+
}
|
|
84
|
+
.pw-frame-nav-item:hover,
|
|
85
|
+
.pw-frame-toggle:hover {
|
|
86
|
+
background: var(--theme-sidebar-hover, var(--theme-hover));
|
|
87
|
+
}
|
|
88
|
+
.pw-frame-nav-item[aria-current] {
|
|
89
|
+
background: var(--theme-sidebar-active-bg, var(--theme-accent-subtle));
|
|
90
|
+
color: var(--theme-sidebar-active-text, var(--theme-accent));
|
|
91
|
+
}
|
|
92
|
+
.pw-frame-nav-item:is(:disabled, [aria-disabled]) {
|
|
93
|
+
opacity: 0.5;
|
|
94
|
+
cursor: not-allowed;
|
|
95
|
+
}
|
|
96
|
+
.pw-frame-nav-icon {
|
|
97
|
+
display: flex;
|
|
98
|
+
flex: 0 0 18px;
|
|
99
|
+
align-items: center;
|
|
100
|
+
justify-content: center;
|
|
101
|
+
}
|
|
102
|
+
.pw-frame-nav-icon svg {
|
|
103
|
+
width: 18px;
|
|
104
|
+
height: 18px;
|
|
105
|
+
}
|
|
106
|
+
.pw-frame-nav-label {
|
|
107
|
+
flex: 1;
|
|
108
|
+
min-width: 0;
|
|
109
|
+
overflow: hidden;
|
|
110
|
+
text-overflow: ellipsis;
|
|
111
|
+
white-space: nowrap;
|
|
112
|
+
}
|
|
113
|
+
.pw-frame-nav-badge {
|
|
114
|
+
flex-shrink: 0;
|
|
115
|
+
font-size: 12px;
|
|
116
|
+
font-variant-numeric: tabular-nums;
|
|
117
|
+
}
|
|
118
|
+
[data-sidebar-mode="rail"] .pw-frame-nav-item {
|
|
119
|
+
padding-inline: 0;
|
|
120
|
+
justify-content: center;
|
|
121
|
+
height: 40px;
|
|
122
|
+
}
|
|
123
|
+
.pw-frame-toggle {
|
|
124
|
+
display: inline-flex;
|
|
125
|
+
align-items: center;
|
|
126
|
+
justify-content: center;
|
|
127
|
+
width: 36px;
|
|
128
|
+
height: 36px;
|
|
129
|
+
border: 0;
|
|
130
|
+
border-radius: 8px;
|
|
131
|
+
padding: 0;
|
|
132
|
+
color: inherit;
|
|
133
|
+
background: transparent;
|
|
134
|
+
cursor: pointer;
|
|
135
|
+
flex-shrink: 0;
|
|
136
|
+
}
|
|
137
|
+
.pw-frame-toggle svg {
|
|
138
|
+
width: 18px;
|
|
139
|
+
height: 18px;
|
|
140
|
+
}
|
|
141
|
+
.pw-frame-sidebar :focus-visible,
|
|
142
|
+
.pw-frame-drawer :focus-visible {
|
|
143
|
+
outline: 2px solid currentColor;
|
|
144
|
+
outline-offset: 2px;
|
|
145
|
+
}
|
|
146
|
+
.pw-frame-resize {
|
|
147
|
+
position: absolute;
|
|
148
|
+
z-index: 1;
|
|
149
|
+
inset-block: 0;
|
|
150
|
+
inset-inline-end: -4px;
|
|
151
|
+
width: 8px;
|
|
152
|
+
cursor: col-resize;
|
|
153
|
+
touch-action: none;
|
|
154
|
+
}
|
|
155
|
+
.pw-frame-resize::after {
|
|
156
|
+
content: "";
|
|
157
|
+
position: absolute;
|
|
158
|
+
inset-block: 8px;
|
|
159
|
+
inset-inline-start: 3px;
|
|
160
|
+
width: 2px;
|
|
161
|
+
border-radius: 2px;
|
|
162
|
+
}
|
|
163
|
+
.pw-frame-resize:hover::after,
|
|
164
|
+
.pw-frame-resize:focus-visible::after {
|
|
165
|
+
background: var(--theme-sidebar-text, var(--theme-accent));
|
|
166
|
+
opacity: 0.5;
|
|
167
|
+
}
|
|
168
|
+
.pw-frame-surface-wrap {
|
|
169
|
+
flex: 1;
|
|
170
|
+
display: flex;
|
|
171
|
+
flex-direction: column;
|
|
172
|
+
min-width: 0;
|
|
173
|
+
min-height: 0;
|
|
174
|
+
padding: 8px;
|
|
175
|
+
padding-inline-start: 0;
|
|
176
|
+
}
|
|
177
|
+
.pw-frame-surface {
|
|
178
|
+
flex: 1;
|
|
179
|
+
display: flex;
|
|
180
|
+
flex-direction: column;
|
|
181
|
+
min-width: 0;
|
|
182
|
+
min-height: 0;
|
|
183
|
+
overflow: hidden;
|
|
184
|
+
isolation: isolate;
|
|
185
|
+
border: 1px solid var(--theme-border);
|
|
186
|
+
border-radius: 12px;
|
|
187
|
+
background: var(--theme-app-bg);
|
|
188
|
+
background-clip: padding-box;
|
|
189
|
+
}
|
|
190
|
+
.pw-frame-header,
|
|
191
|
+
.pw-frame-banner {
|
|
192
|
+
flex-shrink: 0;
|
|
193
|
+
}
|
|
194
|
+
.pw-frame-main {
|
|
195
|
+
flex: 1;
|
|
196
|
+
min-width: 0;
|
|
197
|
+
min-height: 0;
|
|
198
|
+
overflow: auto;
|
|
199
|
+
}
|
|
200
|
+
.pw-app-frame[data-full-bleed] .pw-frame-surface-wrap {
|
|
201
|
+
padding: 0;
|
|
202
|
+
}
|
|
203
|
+
.pw-app-frame[data-full-bleed] .pw-frame-surface {
|
|
204
|
+
border: 0;
|
|
205
|
+
border-radius: 0;
|
|
206
|
+
}
|
|
207
|
+
.pw-app-frame[data-mobile] .pw-frame-surface-wrap {
|
|
208
|
+
padding: 8px;
|
|
209
|
+
}
|
|
210
|
+
.pw-app-frame[data-mobile][data-full-bleed] .pw-frame-surface-wrap {
|
|
211
|
+
padding: 0;
|
|
212
|
+
}
|
|
213
|
+
.pw-frame-backdrop {
|
|
214
|
+
position: absolute;
|
|
215
|
+
inset: 0;
|
|
216
|
+
z-index: var(--layer-drawer, 1100);
|
|
217
|
+
background: var(--theme-backdrop, rgb(0 0 0 / 0.35));
|
|
218
|
+
transition: opacity 200ms cubic-bezier(0.25, 0.1, 0.25, 1);
|
|
219
|
+
}
|
|
220
|
+
.pw-frame-backdrop[data-starting-style],
|
|
221
|
+
.pw-frame-backdrop[data-ending-style] {
|
|
222
|
+
opacity: 0;
|
|
223
|
+
}
|
|
224
|
+
.pw-frame-drawer {
|
|
225
|
+
position: absolute;
|
|
226
|
+
inset-block: 8px;
|
|
227
|
+
inset-inline-start: 8px;
|
|
228
|
+
max-width: calc(100% - 48px);
|
|
229
|
+
z-index: var(--layer-drawer, 1100);
|
|
230
|
+
border-radius: 12px;
|
|
231
|
+
box-shadow: 0 12px 32px rgb(0 0 0 / 0.2);
|
|
232
|
+
outline: none;
|
|
233
|
+
transform: translateX(0);
|
|
234
|
+
transition: transform 200ms cubic-bezier(0.25, 0.1, 0.25, 1);
|
|
235
|
+
}
|
|
236
|
+
.pw-frame-drawer[data-starting-style],
|
|
237
|
+
.pw-frame-drawer[data-ending-style] {
|
|
238
|
+
transform: translateX(calc(-100% - 16px));
|
|
239
|
+
}
|
|
240
|
+
.pw-frame-drawer:dir(rtl)[data-starting-style],
|
|
241
|
+
.pw-frame-drawer:dir(rtl)[data-ending-style] {
|
|
242
|
+
transform: translateX(calc(100% + 16px));
|
|
243
|
+
}
|
|
244
|
+
.pw-frame-mobile-close {
|
|
245
|
+
display: flex;
|
|
246
|
+
justify-content: flex-end;
|
|
247
|
+
padding: 4px 4px 0;
|
|
248
|
+
}
|
|
249
|
+
.pw-frame-drawer .pw-frame-brand {
|
|
250
|
+
padding-top: 0;
|
|
251
|
+
}
|
|
252
|
+
.pw-frame-drawer .pw-frame-nav-item,
|
|
253
|
+
.pw-app-frame[data-mobile] .pw-frame-toggle {
|
|
254
|
+
min-height: 44px;
|
|
255
|
+
}
|
|
256
|
+
.pw-app-frame[data-mobile] .pw-frame-toggle {
|
|
257
|
+
width: 44px;
|
|
258
|
+
height: 44px;
|
|
259
|
+
}
|
|
260
|
+
.pw-frame-skip {
|
|
261
|
+
position: fixed;
|
|
262
|
+
inset-block-start: -100px;
|
|
263
|
+
inset-inline-start: 8px;
|
|
264
|
+
padding: 8px 16px;
|
|
265
|
+
border-radius: 6px;
|
|
266
|
+
background: var(--theme-panel-bg);
|
|
267
|
+
color: var(--theme-app);
|
|
268
|
+
z-index: var(--layer-popover, 1400);
|
|
269
|
+
}
|
|
270
|
+
.pw-frame-skip:focus {
|
|
271
|
+
inset-block-start: 8px;
|
|
272
|
+
}
|
|
273
|
+
@media (prefers-reduced-motion: reduce) {
|
|
274
|
+
.pw-frame-sidebar,
|
|
275
|
+
.pw-frame-backdrop,
|
|
276
|
+
.pw-frame-drawer,
|
|
277
|
+
.pw-frame-nav-item {
|
|
278
|
+
transition: none;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { type HTMLAttributes, type MouseEvent, type ReactNode } from 'react';
|
|
2
|
+
import { type LinkTarget } from '../components/AppLink.js';
|
|
3
|
+
export interface AppFrameLabels {
|
|
4
|
+
navigation: string;
|
|
5
|
+
skipToContent: string;
|
|
6
|
+
openNavigation: string;
|
|
7
|
+
closeNavigation: string;
|
|
8
|
+
expandSidebar: string;
|
|
9
|
+
collapseSidebar: string;
|
|
10
|
+
resizeSidebar: string;
|
|
11
|
+
}
|
|
12
|
+
export interface AppFrameState {
|
|
13
|
+
/** Labels stay visible in the mobile drawer even when the desktop rail is collapsed. */
|
|
14
|
+
expanded: boolean;
|
|
15
|
+
isMobile: boolean;
|
|
16
|
+
mobileOpen: boolean;
|
|
17
|
+
toggle: () => void;
|
|
18
|
+
closeNavigation: () => void;
|
|
19
|
+
}
|
|
20
|
+
export declare function useAppFrame(): AppFrameState;
|
|
21
|
+
type Slot = ReactNode | ((state: AppFrameState) => ReactNode);
|
|
22
|
+
export interface AppFrameProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
|
|
23
|
+
children: ReactNode;
|
|
24
|
+
/** Identity at the top of navigation. Render a compact mark when expanded is false. */
|
|
25
|
+
brand?: Slot;
|
|
26
|
+
navigation: Slot;
|
|
27
|
+
footer?: Slot;
|
|
28
|
+
/** Page tools above the independently scrolling main content. */
|
|
29
|
+
header?: ReactNode;
|
|
30
|
+
/** Optional notice above both navigation and content. */
|
|
31
|
+
banner?: ReactNode;
|
|
32
|
+
expanded?: boolean;
|
|
33
|
+
defaultExpanded?: boolean;
|
|
34
|
+
onExpandedChange?: (expanded: boolean) => void;
|
|
35
|
+
width?: number;
|
|
36
|
+
defaultWidth?: number;
|
|
37
|
+
/** Commits once when a drag ends, and on each keyboard or reset action. */
|
|
38
|
+
onWidthChange?: (width: number) => void;
|
|
39
|
+
minWidth?: number;
|
|
40
|
+
maxWidth?: number;
|
|
41
|
+
railWidth?: number;
|
|
42
|
+
resizable?: boolean;
|
|
43
|
+
/** Viewport width below which navigation becomes a modal drawer. */
|
|
44
|
+
mobileBreakpoint?: number;
|
|
45
|
+
/** A changed route key closes mobile navigation. */
|
|
46
|
+
navigationKey?: string;
|
|
47
|
+
/** Reset navigation scroll only when this section changes; defaults to navigationKey. */
|
|
48
|
+
navigationResetKey?: string;
|
|
49
|
+
/** Attributes shared by desktop navigation and the mobile dialog. */
|
|
50
|
+
sidebarProps?: HTMLAttributes<HTMLElement>;
|
|
51
|
+
/** Removes the inset and border around content. */
|
|
52
|
+
fullBleed?: boolean;
|
|
53
|
+
labels?: Partial<AppFrameLabels>;
|
|
54
|
+
mainId?: string;
|
|
55
|
+
mainClassName?: string;
|
|
56
|
+
}
|
|
57
|
+
/** An inset application surface beside a resizable icon rail or navigation column.
|
|
58
|
+
* Router, identity, persistence and permissions belong to the host. Import app-frame.css. */
|
|
59
|
+
export declare function AppFrame({ children, brand, navigation, footer, header, banner, expanded: controlledExpanded, defaultExpanded, onExpandedChange, width: controlledWidth, defaultWidth, onWidthChange, minWidth, maxWidth, railWidth, resizable, mobileBreakpoint, navigationKey, navigationResetKey, sidebarProps, fullBleed, labels: labelOverrides, mainId: suppliedMainId, mainClassName, className, style, ...props }: AppFrameProps): import("react").JSX.Element;
|
|
60
|
+
/** Put in the page header on mobile, and the sidebar footer on desktop. */
|
|
61
|
+
export declare function AppFrameToggle({ className, ...props }: Omit<HTMLAttributes<HTMLButtonElement>, 'children'>): import("react").JSX.Element;
|
|
62
|
+
export interface AppFrameNavItemProps extends LinkTarget {
|
|
63
|
+
label: string;
|
|
64
|
+
icon: ReactNode;
|
|
65
|
+
selected?: boolean;
|
|
66
|
+
badge?: ReactNode;
|
|
67
|
+
disabled?: boolean;
|
|
68
|
+
onClick?: (event: MouseEvent<HTMLElement>) => void;
|
|
69
|
+
className?: string;
|
|
70
|
+
}
|
|
71
|
+
/** Router-aware navigation that keeps icon-only rail items named and discoverable. */
|
|
72
|
+
export declare function AppFrameNavItem({ label, icon, selected, badge, disabled, onClick, className, ...target }: AppFrameNavItemProps): import("react").JSX.Element;
|
|
73
|
+
export {};
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { cx } from "../cx.js";
|
|
2
|
+
import { CloseIcon, SidebarCloseIcon, SidebarOpenIcon } from "../icons.js";
|
|
3
|
+
import { Tooltip } from "../components/Tooltip.js";
|
|
4
|
+
import { AppLink, isPlainClick } from "../components/AppLink.js";
|
|
5
|
+
import { createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
6
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
import { Dialog } from "@base-ui/react/dialog";
|
|
8
|
+
//#region src/app-frame/index.tsx
|
|
9
|
+
var defaultLabels = {
|
|
10
|
+
navigation: "Main navigation",
|
|
11
|
+
skipToContent: "Skip to content",
|
|
12
|
+
openNavigation: "Open navigation",
|
|
13
|
+
closeNavigation: "Close navigation",
|
|
14
|
+
expandSidebar: "Expand sidebar",
|
|
15
|
+
collapseSidebar: "Collapse sidebar",
|
|
16
|
+
resizeSidebar: "Resize sidebar"
|
|
17
|
+
};
|
|
18
|
+
var Context = createContext(null);
|
|
19
|
+
function useAppFrame() {
|
|
20
|
+
const context = useContext(Context);
|
|
21
|
+
if (!context) throw new Error("useAppFrame requires AppFrame");
|
|
22
|
+
return context;
|
|
23
|
+
}
|
|
24
|
+
function useFrameContext() {
|
|
25
|
+
const context = useContext(Context);
|
|
26
|
+
if (!context) throw new Error("App frame controls require AppFrame");
|
|
27
|
+
return context;
|
|
28
|
+
}
|
|
29
|
+
function useMobile(breakpoint) {
|
|
30
|
+
const media = useMemo(() => typeof window === "undefined" ? null : window.matchMedia(`(max-width: ${breakpoint - 1}px)`), [breakpoint]);
|
|
31
|
+
return useSyncExternalStore(useMemo(() => (changed) => {
|
|
32
|
+
media?.addEventListener("change", changed);
|
|
33
|
+
return () => media?.removeEventListener("change", changed);
|
|
34
|
+
}, [media]), () => media?.matches ?? false, () => false);
|
|
35
|
+
}
|
|
36
|
+
function slot(value, state) {
|
|
37
|
+
return typeof value === "function" ? value(state) : value;
|
|
38
|
+
}
|
|
39
|
+
/** An inset application surface beside a resizable icon rail or navigation column.
|
|
40
|
+
* Router, identity, persistence and permissions belong to the host. Import app-frame.css. */
|
|
41
|
+
function AppFrame({ children, brand, navigation, footer, header, banner, expanded: controlledExpanded, defaultExpanded = true, onExpandedChange, width: controlledWidth, defaultWidth = 240, onWidthChange, minWidth = 200, maxWidth = 400, railWidth = 64, resizable = true, mobileBreakpoint = 768, navigationKey, navigationResetKey = navigationKey, sidebarProps, fullBleed = false, labels: labelOverrides, mainId: suppliedMainId, mainClassName, className, style, ...props }) {
|
|
42
|
+
const id = useId();
|
|
43
|
+
const mainId = suppliedMainId ?? `${id}-main`;
|
|
44
|
+
const navigationId = `${id}-navigation`;
|
|
45
|
+
const [ownExpanded, setOwnExpanded] = useState(defaultExpanded);
|
|
46
|
+
const [ownWidth, setOwnWidth] = useState(defaultWidth);
|
|
47
|
+
const [dragWidth, setDragWidth] = useState(null);
|
|
48
|
+
const [mobileOpen, setMobileOpen] = useState(false);
|
|
49
|
+
const [resizing, setResizing] = useState(false);
|
|
50
|
+
const desktopExpanded = controlledExpanded ?? ownExpanded;
|
|
51
|
+
const minimum = Math.max(railWidth, minWidth);
|
|
52
|
+
const maximum = Math.max(minimum, maxWidth);
|
|
53
|
+
const clamp = (n) => Math.min(maximum, Math.max(minimum, Number.isFinite(n) ? n : defaultWidth));
|
|
54
|
+
const width = clamp(dragWidth ?? controlledWidth ?? ownWidth);
|
|
55
|
+
const isMobile = useMobile(mobileBreakpoint);
|
|
56
|
+
const labels = {
|
|
57
|
+
...defaultLabels,
|
|
58
|
+
...labelOverrides
|
|
59
|
+
};
|
|
60
|
+
const navRef = useRef(null);
|
|
61
|
+
const scrollRef = useRef(null);
|
|
62
|
+
const savedScrollTop = useRef(0);
|
|
63
|
+
const restoreScroll = useCallback((node) => {
|
|
64
|
+
scrollRef.current = node;
|
|
65
|
+
if (node) node.scrollTop = savedScrollTop.current;
|
|
66
|
+
}, []);
|
|
67
|
+
const portalRef = useRef(null);
|
|
68
|
+
const drag = useRef(null);
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (!isMobile && resizable) return;
|
|
71
|
+
drag.current = null;
|
|
72
|
+
setDragWidth(null);
|
|
73
|
+
setResizing(false);
|
|
74
|
+
}, [isMobile, resizable]);
|
|
75
|
+
const setExpanded = (next) => {
|
|
76
|
+
setOwnExpanded(next);
|
|
77
|
+
onExpandedChange?.(next);
|
|
78
|
+
};
|
|
79
|
+
const setWidth = (next) => {
|
|
80
|
+
const value = clamp(next);
|
|
81
|
+
setOwnWidth(value);
|
|
82
|
+
onWidthChange?.(value);
|
|
83
|
+
};
|
|
84
|
+
const closeNavigation = () => setMobileOpen(false);
|
|
85
|
+
const state = {
|
|
86
|
+
expanded: isMobile || desktopExpanded,
|
|
87
|
+
isMobile,
|
|
88
|
+
mobileOpen,
|
|
89
|
+
toggle: () => isMobile ? setMobileOpen(!mobileOpen) : setExpanded(!desktopExpanded),
|
|
90
|
+
closeNavigation,
|
|
91
|
+
labels,
|
|
92
|
+
navigationId
|
|
93
|
+
};
|
|
94
|
+
const previousLocation = useRef({
|
|
95
|
+
navigationKey,
|
|
96
|
+
navigationResetKey,
|
|
97
|
+
isMobile
|
|
98
|
+
});
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (previousLocation.current.navigationKey === navigationKey && previousLocation.current.navigationResetKey === navigationResetKey && previousLocation.current.isMobile === isMobile) return;
|
|
101
|
+
if (previousLocation.current.navigationKey !== navigationKey || previousLocation.current.isMobile !== isMobile) setMobileOpen(false);
|
|
102
|
+
if (previousLocation.current.navigationResetKey !== navigationResetKey || previousLocation.current.isMobile !== isMobile) {
|
|
103
|
+
savedScrollTop.current = 0;
|
|
104
|
+
scrollRef.current?.scrollTo({ top: 0 });
|
|
105
|
+
}
|
|
106
|
+
previousLocation.current = {
|
|
107
|
+
navigationKey,
|
|
108
|
+
navigationResetKey,
|
|
109
|
+
isMobile
|
|
110
|
+
};
|
|
111
|
+
}, [
|
|
112
|
+
navigationKey,
|
|
113
|
+
navigationResetKey,
|
|
114
|
+
isMobile
|
|
115
|
+
]);
|
|
116
|
+
const navContent = /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
117
|
+
brand && /* @__PURE__ */ jsx("div", {
|
|
118
|
+
className: "pw-frame-brand",
|
|
119
|
+
children: slot(brand, state)
|
|
120
|
+
}),
|
|
121
|
+
/* @__PURE__ */ jsx("div", {
|
|
122
|
+
className: "pw-frame-links",
|
|
123
|
+
ref: restoreScroll,
|
|
124
|
+
onScroll: (event) => {
|
|
125
|
+
savedScrollTop.current = event.currentTarget.scrollTop;
|
|
126
|
+
},
|
|
127
|
+
children: slot(navigation, state)
|
|
128
|
+
}),
|
|
129
|
+
footer && /* @__PURE__ */ jsx("div", {
|
|
130
|
+
className: "pw-frame-footer",
|
|
131
|
+
children: slot(footer, state)
|
|
132
|
+
})
|
|
133
|
+
] });
|
|
134
|
+
const resize = (clientX, pointerId) => {
|
|
135
|
+
const current = drag.current;
|
|
136
|
+
if (!current || current.pointerId !== pointerId) return;
|
|
137
|
+
const next = (clientX - current.origin) * current.direction;
|
|
138
|
+
if (desktopExpanded && next < minimum - 80) setExpanded(false);
|
|
139
|
+
else if (!desktopExpanded && next >= minimum - 50) {
|
|
140
|
+
setExpanded(true);
|
|
141
|
+
current.width = clamp(next);
|
|
142
|
+
setDragWidth(current.width);
|
|
143
|
+
} else if (desktopExpanded) {
|
|
144
|
+
current.width = clamp(next);
|
|
145
|
+
setDragWidth(current.width);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
const endResize = (event) => {
|
|
149
|
+
const ended = drag.current;
|
|
150
|
+
if (!ended || ended.pointerId !== event.pointerId) return;
|
|
151
|
+
drag.current = null;
|
|
152
|
+
setResizing(false);
|
|
153
|
+
setDragWidth(null);
|
|
154
|
+
if (desktopExpanded && ended.width !== ended.startWidth) setWidth(ended.width);
|
|
155
|
+
};
|
|
156
|
+
return /* @__PURE__ */ jsx(Context, {
|
|
157
|
+
value: state,
|
|
158
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
159
|
+
...props,
|
|
160
|
+
className: cx("pw-app-frame", className),
|
|
161
|
+
"data-mobile": isMobile || void 0,
|
|
162
|
+
"data-full-bleed": fullBleed || void 0,
|
|
163
|
+
style,
|
|
164
|
+
children: [
|
|
165
|
+
/* @__PURE__ */ jsx("a", {
|
|
166
|
+
className: "pw-frame-skip",
|
|
167
|
+
href: `#${mainId}`,
|
|
168
|
+
onClick: (event) => {
|
|
169
|
+
event.preventDefault();
|
|
170
|
+
document.getElementById(mainId)?.focus();
|
|
171
|
+
},
|
|
172
|
+
children: labels.skipToContent
|
|
173
|
+
}),
|
|
174
|
+
banner && /* @__PURE__ */ jsx("div", {
|
|
175
|
+
className: "pw-frame-banner",
|
|
176
|
+
children: banner
|
|
177
|
+
}),
|
|
178
|
+
/* @__PURE__ */ jsxs("div", {
|
|
179
|
+
className: "pw-frame-body",
|
|
180
|
+
children: [
|
|
181
|
+
!isMobile && /* @__PURE__ */ jsxs("aside", {
|
|
182
|
+
...sidebarProps,
|
|
183
|
+
ref: navRef,
|
|
184
|
+
id: navigationId,
|
|
185
|
+
className: cx("pw-frame-sidebar", sidebarProps?.className),
|
|
186
|
+
"data-sidebar-mode": desktopExpanded ? "expanded" : "rail",
|
|
187
|
+
"data-resizing": resizing || void 0,
|
|
188
|
+
style: {
|
|
189
|
+
...sidebarProps?.style,
|
|
190
|
+
width: desktopExpanded ? width : railWidth
|
|
191
|
+
},
|
|
192
|
+
children: [navContent, resizable && /* @__PURE__ */ jsx("div", {
|
|
193
|
+
role: "separator",
|
|
194
|
+
tabIndex: 0,
|
|
195
|
+
"aria-orientation": "vertical",
|
|
196
|
+
"aria-label": labels.resizeSidebar,
|
|
197
|
+
"aria-controls": navigationId,
|
|
198
|
+
"aria-valuemin": railWidth,
|
|
199
|
+
"aria-valuemax": maximum,
|
|
200
|
+
"aria-valuenow": Math.round(desktopExpanded ? width : railWidth),
|
|
201
|
+
className: "pw-frame-resize",
|
|
202
|
+
onPointerDown: (event) => {
|
|
203
|
+
if (event.button !== 0 || event.isPrimary === false || drag.current) return;
|
|
204
|
+
event.preventDefault();
|
|
205
|
+
const rect = navRef.current?.getBoundingClientRect();
|
|
206
|
+
if (!rect) return;
|
|
207
|
+
const rtl = getComputedStyle(event.currentTarget).direction === "rtl";
|
|
208
|
+
drag.current = {
|
|
209
|
+
origin: rtl ? rect.right : rect.left,
|
|
210
|
+
direction: rtl ? -1 : 1,
|
|
211
|
+
pointerId: event.pointerId,
|
|
212
|
+
startWidth: width,
|
|
213
|
+
width
|
|
214
|
+
};
|
|
215
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
216
|
+
setResizing(true);
|
|
217
|
+
},
|
|
218
|
+
onPointerMove: (event) => resize(event.clientX, event.pointerId),
|
|
219
|
+
onPointerUp: endResize,
|
|
220
|
+
onPointerCancel: endResize,
|
|
221
|
+
onLostPointerCapture: endResize,
|
|
222
|
+
onDoubleClick: () => {
|
|
223
|
+
setExpanded(true);
|
|
224
|
+
setWidth(defaultWidth);
|
|
225
|
+
},
|
|
226
|
+
onKeyDown: (event) => {
|
|
227
|
+
const rtl = getComputedStyle(event.currentTarget).direction === "rtl";
|
|
228
|
+
const smaller = rtl ? "ArrowRight" : "ArrowLeft";
|
|
229
|
+
if (![
|
|
230
|
+
smaller,
|
|
231
|
+
rtl ? "ArrowLeft" : "ArrowRight",
|
|
232
|
+
"Home",
|
|
233
|
+
"End"
|
|
234
|
+
].includes(event.key)) return;
|
|
235
|
+
event.preventDefault();
|
|
236
|
+
if (event.key === "Home") {
|
|
237
|
+
setExpanded(true);
|
|
238
|
+
setWidth(minimum);
|
|
239
|
+
} else if (event.key === "End") {
|
|
240
|
+
setExpanded(true);
|
|
241
|
+
setWidth(maximum);
|
|
242
|
+
} else if (event.key === smaller) {
|
|
243
|
+
if (width <= minimum) setExpanded(false);
|
|
244
|
+
else if (desktopExpanded) setWidth(width - 16);
|
|
245
|
+
} else if (!desktopExpanded) setExpanded(true);
|
|
246
|
+
else setWidth(width + 16);
|
|
247
|
+
}
|
|
248
|
+
})]
|
|
249
|
+
}),
|
|
250
|
+
/* @__PURE__ */ jsx("div", {
|
|
251
|
+
className: "pw-frame-surface-wrap",
|
|
252
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
253
|
+
className: "pw-frame-surface",
|
|
254
|
+
children: [header && /* @__PURE__ */ jsx("div", {
|
|
255
|
+
className: "pw-frame-header",
|
|
256
|
+
children: header
|
|
257
|
+
}), /* @__PURE__ */ jsx("main", {
|
|
258
|
+
id: mainId,
|
|
259
|
+
tabIndex: -1,
|
|
260
|
+
className: cx("pw-frame-main", mainClassName),
|
|
261
|
+
children
|
|
262
|
+
})]
|
|
263
|
+
})
|
|
264
|
+
}),
|
|
265
|
+
/* @__PURE__ */ jsx("div", { ref: portalRef })
|
|
266
|
+
]
|
|
267
|
+
}),
|
|
268
|
+
isMobile && /* @__PURE__ */ jsx(Dialog.Root, {
|
|
269
|
+
open: mobileOpen,
|
|
270
|
+
onOpenChange: setMobileOpen,
|
|
271
|
+
children: /* @__PURE__ */ jsxs(Dialog.Portal, {
|
|
272
|
+
container: portalRef,
|
|
273
|
+
children: [/* @__PURE__ */ jsx(Dialog.Backdrop, { className: "pw-frame-backdrop" }), /* @__PURE__ */ jsxs(Dialog.Popup, {
|
|
274
|
+
...sidebarProps,
|
|
275
|
+
id: navigationId,
|
|
276
|
+
className: cx("pw-frame-drawer", sidebarProps?.className),
|
|
277
|
+
"aria-label": labels.navigation,
|
|
278
|
+
"aria-modal": "true",
|
|
279
|
+
"data-sidebar-mode": "drawer",
|
|
280
|
+
style: {
|
|
281
|
+
...sidebarProps?.style,
|
|
282
|
+
width
|
|
283
|
+
},
|
|
284
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
285
|
+
className: "pw-frame-mobile-close",
|
|
286
|
+
children: /* @__PURE__ */ jsx(Dialog.Close, {
|
|
287
|
+
className: "pw-frame-toggle",
|
|
288
|
+
"aria-label": labels.closeNavigation,
|
|
289
|
+
children: /* @__PURE__ */ jsx(CloseIcon, {})
|
|
290
|
+
})
|
|
291
|
+
}), navContent]
|
|
292
|
+
})]
|
|
293
|
+
})
|
|
294
|
+
})
|
|
295
|
+
]
|
|
296
|
+
})
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
/** Put in the page header on mobile, and the sidebar footer on desktop. */
|
|
300
|
+
function AppFrameToggle({ className, ...props }) {
|
|
301
|
+
const { expanded, isMobile, mobileOpen, toggle, labels, navigationId } = useFrameContext();
|
|
302
|
+
const label = isMobile ? mobileOpen ? labels.closeNavigation : labels.openNavigation : expanded ? labels.collapseSidebar : labels.expandSidebar;
|
|
303
|
+
return /* @__PURE__ */ jsx(Tooltip, {
|
|
304
|
+
content: label,
|
|
305
|
+
side: isMobile ? "bottom" : "right",
|
|
306
|
+
children: /* @__PURE__ */ jsx("button", {
|
|
307
|
+
...props,
|
|
308
|
+
type: "button",
|
|
309
|
+
className: cx("pw-frame-toggle", className),
|
|
310
|
+
"aria-label": label,
|
|
311
|
+
"aria-expanded": isMobile ? mobileOpen : expanded,
|
|
312
|
+
"aria-controls": navigationId,
|
|
313
|
+
onClick: (event) => {
|
|
314
|
+
props.onClick?.(event);
|
|
315
|
+
if (!event.defaultPrevented) toggle();
|
|
316
|
+
},
|
|
317
|
+
children: expanded && !isMobile ? /* @__PURE__ */ jsx(SidebarCloseIcon, {}) : /* @__PURE__ */ jsx(SidebarOpenIcon, {})
|
|
318
|
+
})
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
/** Router-aware navigation that keeps icon-only rail items named and discoverable. */
|
|
322
|
+
function AppFrameNavItem({ label, icon, selected, badge, disabled, onClick, className, ...target }) {
|
|
323
|
+
const { expanded, closeNavigation } = useFrameContext();
|
|
324
|
+
const props = {
|
|
325
|
+
className: cx("pw-frame-nav-item", className),
|
|
326
|
+
"aria-current": selected ? "page" : void 0,
|
|
327
|
+
onClick: (event) => {
|
|
328
|
+
if (disabled) {
|
|
329
|
+
event.preventDefault();
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
onClick?.(event);
|
|
333
|
+
if (!event.defaultPrevented && isPlainClick(event)) closeNavigation();
|
|
334
|
+
},
|
|
335
|
+
children: /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
336
|
+
/* @__PURE__ */ jsx("span", {
|
|
337
|
+
className: "pw-frame-nav-icon",
|
|
338
|
+
"aria-hidden": "true",
|
|
339
|
+
children: icon
|
|
340
|
+
}),
|
|
341
|
+
/* @__PURE__ */ jsx("span", {
|
|
342
|
+
className: expanded ? "pw-frame-nav-label" : "sr-only",
|
|
343
|
+
children: label
|
|
344
|
+
}),
|
|
345
|
+
expanded && badge != null && /* @__PURE__ */ jsx("span", {
|
|
346
|
+
className: "pw-frame-nav-badge",
|
|
347
|
+
"aria-hidden": "true",
|
|
348
|
+
children: badge
|
|
349
|
+
})
|
|
350
|
+
] })
|
|
351
|
+
};
|
|
352
|
+
const item = target.href ? /* @__PURE__ */ jsx(AppLink, {
|
|
353
|
+
...target,
|
|
354
|
+
...props,
|
|
355
|
+
disabled
|
|
356
|
+
}) : /* @__PURE__ */ jsx("button", {
|
|
357
|
+
...props,
|
|
358
|
+
type: "button",
|
|
359
|
+
disabled
|
|
360
|
+
});
|
|
361
|
+
return expanded ? item : /* @__PURE__ */ jsx(Tooltip, {
|
|
362
|
+
content: label,
|
|
363
|
+
side: "right",
|
|
364
|
+
delay: 0,
|
|
365
|
+
children: item
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
//#endregion
|
|
369
|
+
export { AppFrame, AppFrameNavItem, AppFrameToggle, useAppFrame };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parallelworks/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.93.0",
|
|
4
4
|
"description": "React UI components: a theme contract, icons, brand logos, primitives, lists, forms, a job graph, a code editor, a log viewer, a file explorer and an AI chat, each on its own subpath",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -76,7 +76,12 @@
|
|
|
76
76
|
"types": "./dist/ai/adapter/mock/index.d.ts",
|
|
77
77
|
"import": "./dist/ai/adapter/mock/index.js"
|
|
78
78
|
},
|
|
79
|
-
"./ai.css": "./dist/ai/ai.css"
|
|
79
|
+
"./ai.css": "./dist/ai/ai.css",
|
|
80
|
+
"./app-frame": {
|
|
81
|
+
"types": "./dist/app-frame/index.d.ts",
|
|
82
|
+
"import": "./dist/app-frame/index.js"
|
|
83
|
+
},
|
|
84
|
+
"./app-frame.css": "./dist/app-frame/app-frame.css"
|
|
80
85
|
},
|
|
81
86
|
"publishConfig": {
|
|
82
87
|
"registry": "https://registry.npmjs.org"
|
|
@@ -148,7 +153,7 @@
|
|
|
148
153
|
"scripts": {
|
|
149
154
|
"build": "vite build && pnpm build:types && pnpm build:css",
|
|
150
155
|
"build:types": "tsc -p tsconfig.build.json && node scripts/declaration-extensions.mjs",
|
|
151
|
-
"build:css": "mkdir -p dist/styles dist/logviewer dist/ai && cp src/styles/theme.css src/styles/base.css src/styles/fonts.css src/styles/*.woff2 dist/styles/ && tailwindcss -i ./src/styles.css -o ./dist/styles.css --minify && cp src/logviewer/logviewer.css dist/logviewer/ && cp src/ai/ai.css dist/ai/",
|
|
156
|
+
"build:css": "mkdir -p dist/styles dist/logviewer dist/ai && cp src/styles/theme.css src/styles/base.css src/styles/fonts.css src/styles/*.woff2 dist/styles/ && tailwindcss -i ./src/styles.css -o ./dist/styles.css --minify && cp src/logviewer/logviewer.css dist/logviewer/ && cp src/ai/ai.css dist/ai/ && mkdir -p dist/app-frame && cp src/app-frame/app-frame.css dist/app-frame/",
|
|
152
157
|
"clean": "rm -rf dist",
|
|
153
158
|
"test": "vitest run",
|
|
154
159
|
"lint:package": "publint --strict",
|