@parallelworks/ui 0.93.0 → 0.94.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -228,6 +228,14 @@ A page declares its header in one place, and the shell shows it:
228
228
  such as a log's follow and refresh: its buttons are outlined, and `primary`
229
229
  fills the one, if any, the panel leads to.
230
230
  **`toolbar`** is a row under the bar, such as tabs or a time range.
231
+ - A list's controls may sit in **`actions`**: a `ListToolbar` there, with its
232
+ filter, display options and search, leaves the list's count under its rows.
233
+ Where its search field, opened, wouldn't fit beside the trail, as on a phone,
234
+ the open search takes the whole bar until it closes; the rest of the bar
235
+ stays as it was underneath, hidden and `inert`, and focus returns to the
236
+ search button. While a search term narrows the list with the field closed,
237
+ the button shows a dot and its name says so (`searchActive`). Nothing to set
238
+ up: the bar measures it.
231
239
  - **`hidden`** takes the bar away while the page is mounted, for a page that
232
240
  fills the window.
233
241
 
@@ -1318,3 +1326,20 @@ link; put page content inside it without nesting a second `<main>`.
1318
1326
  Frame styles use the `pw-ui` cascade layer. Put broad host element defaults
1319
1327
  (such as anchor colors and scrollbar styling) in a lower-priority layer declared
1320
1328
  before the package styles, so they do not replace navigation states.
1329
+
1330
+ Applications that supply their own navigation items can load only the layout:
1331
+
1332
+ ```tsx
1333
+ import { AppFrame, AppFrameToggle } from '@parallelworks/ui/app-frame'
1334
+ import '@parallelworks/ui/app-frame/layout.css'
1335
+ ```
1336
+
1337
+ This includes the sidebar, scrolling regions, resize handle, mobile drawer,
1338
+ toggle and close buttons, focus indicators, and skip link. Style custom navigation
1339
+ items in the host, including their selected, hover, rail, and mobile states.
1340
+
1341
+ When using `AppFrameNavItem`, also import
1342
+ `@parallelworks/ui/app-frame/navigation.css`. The existing
1343
+ `@parallelworks/ui/app-frame.css` import includes both stylesheets and continues
1344
+ to work without changes. Use either the combined import or the two separate
1345
+ imports; there is no need to load both forms.
@@ -60,66 +60,6 @@
60
60
  flex-shrink: 0;
61
61
  padding: 8px 12px;
62
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
63
  .pw-frame-toggle {
124
64
  display: inline-flex;
125
65
  align-items: center;
@@ -134,6 +74,9 @@
134
74
  cursor: pointer;
135
75
  flex-shrink: 0;
136
76
  }
77
+ .pw-frame-toggle:hover {
78
+ background: var(--theme-sidebar-hover, var(--theme-hover));
79
+ }
137
80
  .pw-frame-toggle svg {
138
81
  width: 18px;
139
82
  height: 18px;
@@ -249,7 +192,6 @@
249
192
  .pw-frame-drawer .pw-frame-brand {
250
193
  padding-top: 0;
251
194
  }
252
- .pw-frame-drawer .pw-frame-nav-item,
253
195
  .pw-app-frame[data-mobile] .pw-frame-toggle {
254
196
  min-height: 44px;
255
197
  }
@@ -273,7 +215,75 @@
273
215
  @media (prefers-reduced-motion: reduce) {
274
216
  .pw-frame-sidebar,
275
217
  .pw-frame-backdrop,
276
- .pw-frame-drawer,
218
+ .pw-frame-drawer {
219
+ transition: none;
220
+ }
221
+ }
222
+ }
223
+ @layer pw-ui {
224
+ .pw-frame-nav-item {
225
+ display: flex;
226
+ align-items: center;
227
+ gap: 8px;
228
+ min-height: 36px;
229
+ min-width: 0;
230
+ width: 100%;
231
+ border: 0;
232
+ border-radius: 8px;
233
+ padding: 8px;
234
+ background: transparent;
235
+ color: inherit;
236
+ font: inherit;
237
+ font-size: 14px;
238
+ font-weight: 500;
239
+ line-height: 20px;
240
+ cursor: pointer;
241
+ text-decoration: none;
242
+ text-align: start;
243
+ transition: background-color 150ms ease-out;
244
+ }
245
+ .pw-frame-nav-item:hover {
246
+ background: var(--theme-sidebar-hover, var(--theme-hover));
247
+ }
248
+ .pw-frame-nav-item[aria-current] {
249
+ background: var(--theme-sidebar-active-bg, var(--theme-accent-subtle));
250
+ color: var(--theme-sidebar-active-text, var(--theme-accent));
251
+ }
252
+ .pw-frame-nav-item:is(:disabled, [aria-disabled]) {
253
+ opacity: 0.5;
254
+ cursor: not-allowed;
255
+ }
256
+ .pw-frame-nav-icon {
257
+ display: flex;
258
+ flex: 0 0 18px;
259
+ align-items: center;
260
+ justify-content: center;
261
+ }
262
+ .pw-frame-nav-icon svg {
263
+ width: 18px;
264
+ height: 18px;
265
+ }
266
+ .pw-frame-nav-label {
267
+ flex: 1;
268
+ min-width: 0;
269
+ overflow: hidden;
270
+ text-overflow: ellipsis;
271
+ white-space: nowrap;
272
+ }
273
+ .pw-frame-nav-badge {
274
+ flex-shrink: 0;
275
+ font-size: 12px;
276
+ font-variant-numeric: tabular-nums;
277
+ }
278
+ [data-sidebar-mode="rail"] .pw-frame-nav-item {
279
+ padding-inline: 0;
280
+ justify-content: center;
281
+ height: 40px;
282
+ }
283
+ .pw-frame-drawer .pw-frame-nav-item {
284
+ min-height: 44px;
285
+ }
286
+ @media (prefers-reduced-motion: reduce) {
277
287
  .pw-frame-nav-item {
278
288
  transition: none;
279
289
  }
@@ -0,0 +1,222 @@
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-toggle {
64
+ display: inline-flex;
65
+ align-items: center;
66
+ justify-content: center;
67
+ width: 36px;
68
+ height: 36px;
69
+ border: 0;
70
+ border-radius: 8px;
71
+ padding: 0;
72
+ color: inherit;
73
+ background: transparent;
74
+ cursor: pointer;
75
+ flex-shrink: 0;
76
+ }
77
+ .pw-frame-toggle:hover {
78
+ background: var(--theme-sidebar-hover, var(--theme-hover));
79
+ }
80
+ .pw-frame-toggle svg {
81
+ width: 18px;
82
+ height: 18px;
83
+ }
84
+ .pw-frame-sidebar :focus-visible,
85
+ .pw-frame-drawer :focus-visible {
86
+ outline: 2px solid currentColor;
87
+ outline-offset: 2px;
88
+ }
89
+ .pw-frame-resize {
90
+ position: absolute;
91
+ z-index: 1;
92
+ inset-block: 0;
93
+ inset-inline-end: -4px;
94
+ width: 8px;
95
+ cursor: col-resize;
96
+ touch-action: none;
97
+ }
98
+ .pw-frame-resize::after {
99
+ content: "";
100
+ position: absolute;
101
+ inset-block: 8px;
102
+ inset-inline-start: 3px;
103
+ width: 2px;
104
+ border-radius: 2px;
105
+ }
106
+ .pw-frame-resize:hover::after,
107
+ .pw-frame-resize:focus-visible::after {
108
+ background: var(--theme-sidebar-text, var(--theme-accent));
109
+ opacity: 0.5;
110
+ }
111
+ .pw-frame-surface-wrap {
112
+ flex: 1;
113
+ display: flex;
114
+ flex-direction: column;
115
+ min-width: 0;
116
+ min-height: 0;
117
+ padding: 8px;
118
+ padding-inline-start: 0;
119
+ }
120
+ .pw-frame-surface {
121
+ flex: 1;
122
+ display: flex;
123
+ flex-direction: column;
124
+ min-width: 0;
125
+ min-height: 0;
126
+ overflow: hidden;
127
+ isolation: isolate;
128
+ border: 1px solid var(--theme-border);
129
+ border-radius: 12px;
130
+ background: var(--theme-app-bg);
131
+ background-clip: padding-box;
132
+ }
133
+ .pw-frame-header,
134
+ .pw-frame-banner {
135
+ flex-shrink: 0;
136
+ }
137
+ .pw-frame-main {
138
+ flex: 1;
139
+ min-width: 0;
140
+ min-height: 0;
141
+ overflow: auto;
142
+ }
143
+ .pw-app-frame[data-full-bleed] .pw-frame-surface-wrap {
144
+ padding: 0;
145
+ }
146
+ .pw-app-frame[data-full-bleed] .pw-frame-surface {
147
+ border: 0;
148
+ border-radius: 0;
149
+ }
150
+ .pw-app-frame[data-mobile] .pw-frame-surface-wrap {
151
+ padding: 8px;
152
+ }
153
+ .pw-app-frame[data-mobile][data-full-bleed] .pw-frame-surface-wrap {
154
+ padding: 0;
155
+ }
156
+ .pw-frame-backdrop {
157
+ position: absolute;
158
+ inset: 0;
159
+ z-index: var(--layer-drawer, 1100);
160
+ background: var(--theme-backdrop, rgb(0 0 0 / 0.35));
161
+ transition: opacity 200ms cubic-bezier(0.25, 0.1, 0.25, 1);
162
+ }
163
+ .pw-frame-backdrop[data-starting-style],
164
+ .pw-frame-backdrop[data-ending-style] {
165
+ opacity: 0;
166
+ }
167
+ .pw-frame-drawer {
168
+ position: absolute;
169
+ inset-block: 8px;
170
+ inset-inline-start: 8px;
171
+ max-width: calc(100% - 48px);
172
+ z-index: var(--layer-drawer, 1100);
173
+ border-radius: 12px;
174
+ box-shadow: 0 12px 32px rgb(0 0 0 / 0.2);
175
+ outline: none;
176
+ transform: translateX(0);
177
+ transition: transform 200ms cubic-bezier(0.25, 0.1, 0.25, 1);
178
+ }
179
+ .pw-frame-drawer[data-starting-style],
180
+ .pw-frame-drawer[data-ending-style] {
181
+ transform: translateX(calc(-100% - 16px));
182
+ }
183
+ .pw-frame-drawer:dir(rtl)[data-starting-style],
184
+ .pw-frame-drawer:dir(rtl)[data-ending-style] {
185
+ transform: translateX(calc(100% + 16px));
186
+ }
187
+ .pw-frame-mobile-close {
188
+ display: flex;
189
+ justify-content: flex-end;
190
+ padding: 4px 4px 0;
191
+ }
192
+ .pw-frame-drawer .pw-frame-brand {
193
+ padding-top: 0;
194
+ }
195
+ .pw-app-frame[data-mobile] .pw-frame-toggle {
196
+ min-height: 44px;
197
+ }
198
+ .pw-app-frame[data-mobile] .pw-frame-toggle {
199
+ width: 44px;
200
+ height: 44px;
201
+ }
202
+ .pw-frame-skip {
203
+ position: fixed;
204
+ inset-block-start: -100px;
205
+ inset-inline-start: 8px;
206
+ padding: 8px 16px;
207
+ border-radius: 6px;
208
+ background: var(--theme-panel-bg);
209
+ color: var(--theme-app);
210
+ z-index: var(--layer-popover, 1400);
211
+ }
212
+ .pw-frame-skip:focus {
213
+ inset-block-start: 8px;
214
+ }
215
+ @media (prefers-reduced-motion: reduce) {
216
+ .pw-frame-sidebar,
217
+ .pw-frame-backdrop,
218
+ .pw-frame-drawer {
219
+ transition: none;
220
+ }
221
+ }
222
+ }
@@ -0,0 +1,69 @@
1
+ @layer pw-ui {
2
+ .pw-frame-nav-item {
3
+ display: flex;
4
+ align-items: center;
5
+ gap: 8px;
6
+ min-height: 36px;
7
+ min-width: 0;
8
+ width: 100%;
9
+ border: 0;
10
+ border-radius: 8px;
11
+ padding: 8px;
12
+ background: transparent;
13
+ color: inherit;
14
+ font: inherit;
15
+ font-size: 14px;
16
+ font-weight: 500;
17
+ line-height: 20px;
18
+ cursor: pointer;
19
+ text-decoration: none;
20
+ text-align: start;
21
+ transition: background-color 150ms ease-out;
22
+ }
23
+ .pw-frame-nav-item:hover {
24
+ background: var(--theme-sidebar-hover, var(--theme-hover));
25
+ }
26
+ .pw-frame-nav-item[aria-current] {
27
+ background: var(--theme-sidebar-active-bg, var(--theme-accent-subtle));
28
+ color: var(--theme-sidebar-active-text, var(--theme-accent));
29
+ }
30
+ .pw-frame-nav-item:is(:disabled, [aria-disabled]) {
31
+ opacity: 0.5;
32
+ cursor: not-allowed;
33
+ }
34
+ .pw-frame-nav-icon {
35
+ display: flex;
36
+ flex: 0 0 18px;
37
+ align-items: center;
38
+ justify-content: center;
39
+ }
40
+ .pw-frame-nav-icon svg {
41
+ width: 18px;
42
+ height: 18px;
43
+ }
44
+ .pw-frame-nav-label {
45
+ flex: 1;
46
+ min-width: 0;
47
+ overflow: hidden;
48
+ text-overflow: ellipsis;
49
+ white-space: nowrap;
50
+ }
51
+ .pw-frame-nav-badge {
52
+ flex-shrink: 0;
53
+ font-size: 12px;
54
+ font-variant-numeric: tabular-nums;
55
+ }
56
+ [data-sidebar-mode="rail"] .pw-frame-nav-item {
57
+ padding-inline: 0;
58
+ justify-content: center;
59
+ height: 40px;
60
+ }
61
+ .pw-frame-drawer .pw-frame-nav-item {
62
+ min-height: 44px;
63
+ }
64
+ @media (prefers-reduced-motion: reduce) {
65
+ .pw-frame-nav-item {
66
+ transition: none;
67
+ }
68
+ }
69
+ }
@@ -151,47 +151,62 @@ function HeaderLayout({ breadcrumbs, heading, titleActions, actions, toolbar, fi
151
151
  const [row, setRow] = useState(null);
152
152
  const [lead, setLead] = useState(null);
153
153
  const [end, setEnd] = useState(null);
154
+ const [cover, setCover] = useState(null);
154
155
  useActionsFit(fit, row, lead, end);
156
+ useLayoutEffect(() => {
157
+ fit.setCoverNode(cover);
158
+ return () => fit.setCoverNode(null);
159
+ }, [fit, cover]);
155
160
  useSyncExternalStore(fit.subscribe, fit.getVersion, fit.getVersion);
156
161
  const moved = fit.menu();
157
162
  const hasTrail = breadcrumbs.length > 0;
163
+ const covered = fit.isCovered();
158
164
  return /* @__PURE__ */ jsxs("div", {
159
165
  className: cx("@container w-full shrink-0", !inPlace && "bg-(--theme-app-bg)", className),
160
166
  children: [/* @__PURE__ */ jsxs("div", {
161
167
  ref: setRow,
162
- className: cx("flex items-center justify-between gap-4 border-b border-(--theme-border) px-4", compact ? "h-8" : "h-12"),
163
- children: [/* @__PURE__ */ jsxs("div", {
164
- ref: setLead,
165
- className: "flex min-w-0 flex-1 items-center gap-2",
166
- children: [
167
- navToggle?.(),
168
- hasTrail && /* @__PURE__ */ jsx(Breadcrumbs, {
169
- crumbs: breadcrumbs,
170
- heading
171
- }),
172
- present(titleActions) && /* @__PURE__ */ jsx("div", {
173
- className: "flex shrink-0 items-center gap-0.5",
174
- children: titleActions
175
- }),
176
- hasTrail && breadcrumbPin?.({ crumbs: breadcrumbs })
177
- ]
178
- }), present(actions) && /* @__PURE__ */ jsxs("div", {
179
- ref: setEnd,
180
- className: "flex shrink-0 items-center gap-1",
181
- children: [/* @__PURE__ */ jsx(HeaderActionsFitContext, {
182
- value: fit,
183
- children: actions
184
- }), moved.length > 0 && /* @__PURE__ */ jsx(ActionMenu, {
185
- items: moved,
186
- label: t.moreActions,
187
- triggerClassName: buttonClasses({
188
- variant: "ghost",
189
- size: "xs",
190
- iconOnly: true
191
- }),
192
- children: /* @__PURE__ */ jsx(MoreIcon, {})
193
- })]
194
- })]
168
+ className: cx("relative flex items-center justify-between gap-4 border-b border-(--theme-border) px-4", compact ? "h-8" : "h-12"),
169
+ children: [
170
+ /* @__PURE__ */ jsxs("div", {
171
+ ref: setLead,
172
+ inert: covered,
173
+ className: cx("flex min-w-0 flex-1 items-center gap-2", covered && "invisible"),
174
+ children: [
175
+ navToggle?.(),
176
+ hasTrail && /* @__PURE__ */ jsx(Breadcrumbs, {
177
+ crumbs: breadcrumbs,
178
+ heading
179
+ }),
180
+ present(titleActions) && /* @__PURE__ */ jsx("div", {
181
+ className: "flex shrink-0 items-center gap-0.5",
182
+ children: titleActions
183
+ }),
184
+ hasTrail && breadcrumbPin?.({ crumbs: breadcrumbs })
185
+ ]
186
+ }),
187
+ present(actions) && /* @__PURE__ */ jsxs("div", {
188
+ ref: setEnd,
189
+ inert: covered,
190
+ className: cx("flex shrink-0 items-center gap-1", covered && "invisible"),
191
+ children: [/* @__PURE__ */ jsx(HeaderActionsFitContext, {
192
+ value: fit,
193
+ children: actions
194
+ }), moved.length > 0 && /* @__PURE__ */ jsx(ActionMenu, {
195
+ items: moved,
196
+ label: t.moreActions,
197
+ triggerClassName: buttonClasses({
198
+ variant: "ghost",
199
+ size: "xs",
200
+ iconOnly: true
201
+ }),
202
+ children: /* @__PURE__ */ jsx(MoreIcon, {})
203
+ })]
204
+ }),
205
+ /* @__PURE__ */ jsx("div", {
206
+ ref: setCover,
207
+ className: "pointer-events-none absolute inset-0 flex items-center px-4"
208
+ })
209
+ ]
195
210
  }), present(toolbar) && /* @__PURE__ */ jsx("div", {
196
211
  className: "px-4",
197
212
  children: toolbar
@@ -243,10 +258,13 @@ function useActionsFit(fit, row, lead, end) {
243
258
  }
244
259
  leadMin += Math.max(0, leadItems - 1) * LEAD_GAP;
245
260
  const room = rowWidth - Number.parseFloat(style.paddingLeft || "0") - Number.parseFloat(style.paddingRight || "0") - leadMin - ROW_GAP;
261
+ const shown = end.getBoundingClientRect().width;
262
+ const moved = items.filter((id) => fit.isOverflowed(id));
246
263
  fit.setOverflow(overflowingActions(items.map((id) => ({
247
264
  id,
248
265
  width: width(id)
249
- })), end.getBoundingClientRect().width, items.filter((id) => fit.isOverflowed(id)), room));
266
+ })), shown, moved, room));
267
+ fit.setSearchCovers(searchCovers(end, items, moved, width, shown, room));
250
268
  };
251
269
  measure();
252
270
  const resize = new ResizeObserver(measure);
@@ -270,5 +288,23 @@ function useActionsFit(fit, row, lead, end) {
270
288
  widths
271
289
  ]);
272
290
  }
291
+ var SEARCH_FIELD_WIDTH = 288;
292
+ /**
293
+ * Whether a search in the actions, opened in place, would leave the trail too little room even
294
+ * with every action in the "…" menu, so it takes the whole bar instead. It works from the
295
+ * actions' width as they are now, whether the search shows as its button or its field, so the
296
+ * answer is the same either way.
297
+ */
298
+ function searchCovers(end, items, moved, width, shown, room) {
299
+ const control = end.querySelector("[data-header-search]")?.firstElementChild;
300
+ if (!control) return false;
301
+ let least = shown - control.getBoundingClientRect().width + SEARCH_FIELD_WIDTH;
302
+ const still = items.filter((id) => !moved.includes(id));
303
+ if (still.length > 0) {
304
+ least -= still.reduce((sum, id) => sum + width(id) + 4, 0);
305
+ if (moved.length === 0) least += 28;
306
+ }
307
+ return least > room + .5;
308
+ }
273
309
  //#endregion
274
310
  export { PageHeader, PageHeaderBar, PageHeaderProvider };
@@ -653,6 +653,8 @@ export interface UIStrings {
653
653
  /** Where the page sits, for `Pagination` too narrow for page numbers: it shows "5 / 12". */
654
654
  pageOfTotal: (page: number, total: number) => string;
655
655
  removeSearchTerm: (term: string) => string;
656
+ /** The search button's name while a query it holds narrows the list, its field closed. */
657
+ searchActive: (label: string) => string;
656
658
  };
657
659
  /** The chip row of a list's active filters, its Filter menu and its pickers. Field and option
658
660
  * labels come from the host's fields, already translated. */
@@ -634,7 +634,8 @@ var DEFAULTS = {
634
634
  pagination: "Pagination",
635
635
  page: (page) => `Page ${page}`,
636
636
  pageOfTotal: (page, total) => `Page ${page} of ${total}`,
637
- removeSearchTerm: (term) => `Remove filter ${term}`
637
+ removeSearchTerm: (term) => `Remove filter ${term}`,
638
+ searchActive: (label) => `${label}, on`
638
639
  },
639
640
  filters: {
640
641
  addFilter: "Filter",
@@ -9,6 +9,9 @@ import type { MenuEntry } from './MenuEntries.js';
9
9
  export declare class HeaderActionsFit {
10
10
  private entries;
11
11
  private overflow;
12
+ private covers;
13
+ private cover;
14
+ private coverers;
12
15
  private version;
13
16
  private listeners;
14
17
  subscribe: (listener: () => void) => (() => void);
@@ -20,11 +23,23 @@ export declare class HeaderActionsFit {
20
23
  /** The moved actions' rows, in the bar's order. */
21
24
  menu(): MenuEntry[];
22
25
  setOverflow(ids: readonly string[]): void;
26
+ /** Whether an open search would take the whole bar: its field, opened in place, wouldn't fit
27
+ * beside the trail even with every action in the "…" menu. The bar measures it. */
28
+ searchCovers(): boolean;
29
+ setSearchCovers(covers: boolean): void;
30
+ /** Where a search that takes the bar renders its field: a layer over the bar's row. */
31
+ coverNode(): HTMLElement | null;
32
+ setCoverNode(node: HTMLElement | null): void;
33
+ /** Whether a search has the bar now, so the bar hides and disables what it covers. */
34
+ isCovered(): boolean;
35
+ setCovering(id: string, covering: boolean): void;
23
36
  private changed;
24
37
  }
25
38
  /** Set around the header's `actions`, so the actions there can move into its
26
39
  * menu; title actions sit beside the title and never do. */
27
40
  export declare const HeaderActionsFitContext: import("react").Context<HeaderActionsFit | null>;
41
+ export declare const ACTION_GAP = 4;
42
+ export declare const MENU_BUTTON_WIDTH = 24;
28
43
  /**
29
44
  * The trailing actions to move into the "…" menu so the rest fit in `room`
30
45
  * pixels, last first, or none when everything fits. `shown` is the actions'
@@ -10,6 +10,9 @@ import { createContext } from "react";
10
10
  var HeaderActionsFit = class {
11
11
  entries = /* @__PURE__ */ new Map();
12
12
  overflow = [];
13
+ covers = false;
14
+ cover = null;
15
+ coverers = /* @__PURE__ */ new Set();
13
16
  version = 0;
14
17
  listeners = /* @__PURE__ */ new Set();
15
18
  subscribe = (listener) => {
@@ -46,6 +49,37 @@ var HeaderActionsFit = class {
46
49
  this.overflow = ids;
47
50
  this.changed();
48
51
  }
52
+ /** Whether an open search would take the whole bar: its field, opened in place, wouldn't fit
53
+ * beside the trail even with every action in the "…" menu. The bar measures it. */
54
+ searchCovers() {
55
+ return this.covers;
56
+ }
57
+ setSearchCovers(covers) {
58
+ if (covers !== this.covers) {
59
+ this.covers = covers;
60
+ this.changed();
61
+ }
62
+ }
63
+ /** Where a search that takes the bar renders its field: a layer over the bar's row. */
64
+ coverNode() {
65
+ return this.cover;
66
+ }
67
+ setCoverNode(node) {
68
+ if (node !== this.cover) {
69
+ this.cover = node;
70
+ this.changed();
71
+ }
72
+ }
73
+ /** Whether a search has the bar now, so the bar hides and disables what it covers. */
74
+ isCovered() {
75
+ return this.coverers.size > 0;
76
+ }
77
+ setCovering(id, covering) {
78
+ if (covering === this.coverers.has(id)) return;
79
+ if (covering) this.coverers.add(id);
80
+ else this.coverers.delete(id);
81
+ this.changed();
82
+ }
49
83
  changed() {
50
84
  this.version++;
51
85
  for (const listener of this.listeners) listener();
@@ -54,7 +88,6 @@ var HeaderActionsFit = class {
54
88
  /** Set around the header's `actions`, so the actions there can move into its
55
89
  * menu; title actions sit beside the title and never do. */
56
90
  var HeaderActionsFitContext = createContext(null);
57
- var ACTION_GAP = 4;
58
91
  /**
59
92
  * The trailing actions to move into the "…" menu so the rest fit in `room`
60
93
  * pixels, last first, or none when everything fits. `shown` is the actions'
@@ -65,14 +98,14 @@ function overflowingActions(items, shown, moved, room) {
65
98
  const width = new Map(items.map((item) => [item.id, item.width]));
66
99
  let total = shown;
67
100
  if (moved.length > 0) {
68
- total += moved.reduce((sum, id) => sum + (width.get(id) ?? 0) + ACTION_GAP, 0);
101
+ total += moved.reduce((sum, id) => sum + (width.get(id) ?? 0) + 4, 0);
69
102
  total -= 28;
70
103
  }
71
104
  const next = [];
72
105
  for (let i = items.length - 1; i >= 0 && total > room + .5; i--) {
73
106
  const item = items[i];
74
107
  if (next.length === 0) total += 28;
75
- total -= item.width + ACTION_GAP;
108
+ total -= item.width + 4;
76
109
  next.unshift(item.id);
77
110
  }
78
111
  return next;
@@ -5,7 +5,7 @@ import { useNavigation } from "../components/Provider.js";
5
5
  import { AppLink, isPlainClick } from "../components/AppLink.js";
6
6
  import { isMenuDivider, isMenuGroup, isMenuSubmenu, useRunAction, warnIfInert } from "../components/MenuEntries.js";
7
7
  import { CellPlacementContext, STACKED_ITEM, Table, hasContent } from "../components/Table.js";
8
- import { ListCountText, ListFooterCountContext } from "./listSearch.js";
8
+ import { ListCountStatus, ListFooterCountContext } from "./listSearch.js";
9
9
  import { ListFilterBar } from "./ListFilterBar.js";
10
10
  import { ListActionsHeader, ListError, ListSkeleton, StretchRowLinkContext } from "./ListTable.js";
11
11
  import { ListToolbar, toolbarCount } from "./ListToolbar.js";
@@ -69,7 +69,8 @@ function ListPage({ bleed = false, toolbar, filterBar, view, loading = false, er
69
69
  nounPlural: count.nounPlural
70
70
  } : fromToolbar;
71
71
  const showsRows = !loading && !failed && !isEmpty && !noneMatch && (!counted || counted.matches > 0);
72
- const footerCount = showsRows ? counted : void 0;
72
+ const countable = !!count || fromToolbar !== void 0;
73
+ const spokenCount = loading || failed ? void 0 : counted;
73
74
  const shareLine = counted !== void 0 && !!toolbar && isValidElement(givenBar) && givenBar.type === ListFilterBar;
74
75
  const bar = shareLine ? cloneElement(givenBar, { end: toolbar }) : givenBar;
75
76
  let body = children;
@@ -102,18 +103,21 @@ function ListPage({ bleed = false, toolbar, filterBar, view, loading = false, er
102
103
  children: [/* @__PURE__ */ jsx(ListRowActionsProvider, {
103
104
  view,
104
105
  children: body
105
- }), (footerCount || showsRows && !!pager) && /* @__PURE__ */ jsx(ListFooter, {
106
- count: footerCount,
107
- pager
106
+ }), (countable || showsRows && !!pager) && /* @__PURE__ */ jsx(ListFooter, {
107
+ count: spokenCount,
108
+ countable,
109
+ pager: showsRows ? pager : null,
110
+ shown: showsRows && (counted !== void 0 || !!pager)
108
111
  })]
109
112
  })]
110
113
  });
111
114
  }
112
- /** The line under a list's last row: its count at the rows' start, its pager at their end. */
113
- function ListFooter({ count, pager }) {
115
+ /** The line under a list's last row: its count at the rows' start, its pager at their end. With no
116
+ * rows to show, only a screen reader finds it, so the count's status stays mounted. */
117
+ function ListFooter({ count, countable, pager, shown }) {
114
118
  return /* @__PURE__ */ jsxs("div", {
115
- className: "flex min-h-10 flex-wrap items-center gap-x-3 px-4 empty:hidden [&>:not([role=status])]:ml-auto [&>nav]:flex-1 [&>nav]:px-0 [&>nav>div:first-child]:justify-end",
116
- children: [count && /* @__PURE__ */ jsx(ListCountText, { ...count }), pager]
119
+ className: shown ? "flex min-h-10 flex-wrap items-center gap-x-3 px-4 empty:hidden [&>:not([role=status])]:ml-auto [&>nav]:flex-1 [&>nav]:px-0 [&>nav>div:first-child]:justify-end" : "sr-only",
120
+ children: [countable && /* @__PURE__ */ jsx(ListCountStatus, { count }), pager]
117
121
  });
118
122
  }
119
123
  var STACKED = { stacked: true };
@@ -35,7 +35,14 @@ export declare const ListFooterCountContext: import("react").Context<boolean>;
35
35
  export declare const ListToolbarInlineContext: import("react").Context<boolean>;
36
36
  /** The count's words, "7 tokens" or "2 of 7 tokens", as a status so a screen reader hears the
37
37
  * count change as a filter narrows it. */
38
- export declare function ListCountText({ total, matches, filterActive, noun, nounPlural }: ListCountProps): import("react").JSX.Element;
38
+ export declare function ListCountText(props: ListCountProps): import("react").JSX.Element;
39
+ /** A list's count as a status that can say nothing yet: kept mounted while its text is empty, so
40
+ * a screen reader hears the count when it arrives, as it would not from a status that mounts
41
+ * with its text already in. */
42
+ export declare function ListCountStatus({ count, className, }: {
43
+ count: ListCountProps | undefined;
44
+ className?: string | undefined;
45
+ }): import("react").JSX.Element;
39
46
  /** A list's count, placed by hand.
40
47
  * @deprecated Give `ListPage` a `count` instead (with its `total` and `matches`): it shows the
41
48
  * count under the last row, beside a `pager`, and hides it while the list shows its empty or
@@ -1,12 +1,15 @@
1
+ import { cx } from "../cx.js";
1
2
  import { buttonClasses } from "../components/buttonClasses.js";
2
3
  import { CloseIcon, RemoveIcon, SearchIcon } from "../icons.js";
3
4
  import { ariaKeyShortcuts } from "../components/Kbd.js";
4
5
  import { Tooltip } from "../components/Tooltip.js";
5
6
  import { useLocale, usePersistedState, useStrings } from "../components/Provider.js";
6
7
  import { isTextEntryContext } from "../components/shortcutKey.js";
8
+ import { HeaderActionsFitContext } from "../components/headerActionsFit.js";
7
9
  import { formatInteger } from "../format.js";
8
- import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
9
- import { jsx, jsxs } from "react/jsx-runtime";
10
+ import { createContext, use, useCallback, useContext, useEffect, useId, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
11
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
12
+ import { createPortal } from "react-dom";
10
13
  //#region src/list/listSearch.tsx
11
14
  var NO_TERMS = [];
12
15
  /** Pass `enabled: false` to suspend the global ⌘F/`/` hijack (e.g. when search isn't shown and browser Find should stay active). */
@@ -69,67 +72,149 @@ function useListSearch(enabled = true, storageKey) {
69
72
  function ListSearchControl({ search, placeholder, label: labelProp }) {
70
73
  const { common, list } = useStrings();
71
74
  const label = labelProp ?? common.search;
72
- if (search.searchOpen || search.terms.length > 0) return /* @__PURE__ */ jsxs("div", {
73
- className: "flex min-h-7 w-72 max-w-[55vw] flex-wrap items-center gap-1.5 rounded-md border border-(--theme-border) bg-(--theme-muted-panel-bg) px-2 py-0.5",
74
- children: [
75
- /* @__PURE__ */ jsx(SearchIcon, { className: "w-3.5 h-3.5 shrink-0 text-(--theme-muted-text-color)" }),
76
- search.terms.map((term) => /* @__PURE__ */ jsxs("span", {
77
- className: "flex max-w-40 items-center gap-1 rounded border border-(--theme-border) bg-(--theme-app-bg) px-1.5 py-px text-[12px] text-(--theme-app)",
78
- children: [/* @__PURE__ */ jsx("span", {
79
- className: "truncate",
80
- children: term
81
- }), /* @__PURE__ */ jsx("button", {
75
+ const fit = use(HeaderActionsFitContext);
76
+ useSyncExternalStore(fit?.subscribe ?? subscribeNever, fit?.getVersion ?? zero, zero);
77
+ const covers = fit?.searchCovers() ?? false;
78
+ const coverNode = fit?.coverNode() ?? null;
79
+ const covering = covers && search.searchOpen && coverNode !== null;
80
+ const field = covering || !covers && (search.searchOpen || search.terms.length > 0);
81
+ const active = search.terms.length > 0 || search.filter.trim() !== "";
82
+ const id = useId();
83
+ useLayoutEffect(() => {
84
+ fit?.setCovering(id, covering);
85
+ }, [
86
+ fit,
87
+ id,
88
+ covering
89
+ ]);
90
+ useLayoutEffect(() => fit ? () => fit.setCovering(id, false) : void 0, [fit, id]);
91
+ const button = useRef(null);
92
+ const returnFocus = useRef(false);
93
+ useLayoutEffect(() => {
94
+ if (!field && returnFocus.current) {
95
+ returnFocus.current = false;
96
+ queueMicrotask(() => button.current?.focus());
97
+ }
98
+ });
99
+ const { inputRef, searchOpen } = search;
100
+ const wasCovering = useRef(covering);
101
+ useLayoutEffect(() => {
102
+ if (wasCovering.current === covering) return;
103
+ wasCovering.current = covering;
104
+ if (!searchOpen) return;
105
+ queueMicrotask(() => {
106
+ const focused = document.activeElement;
107
+ if (!focused || focused === document.body || coverNode?.parentElement?.contains(focused)) inputRef.current?.focus();
108
+ });
109
+ });
110
+ if (field) {
111
+ const close = () => {
112
+ returnFocus.current = true;
113
+ search.clearSearch();
114
+ };
115
+ const closeOnEscape = (e) => {
116
+ if (e.key === "Escape" && search.searchOpen) {
117
+ e.preventDefault();
118
+ returnFocus.current = covering || search.terms.length === 0;
119
+ search.closeSearch();
120
+ return true;
121
+ }
122
+ return false;
123
+ };
124
+ const box = /* @__PURE__ */ jsxs("div", {
125
+ className: cx("flex flex-wrap items-center gap-1.5 rounded-md border border-(--theme-border) bg-(--theme-muted-panel-bg) px-2 py-0.5", covering ? "pointer-events-auto min-h-8 w-full" : "min-h-7 w-72 max-w-[55vw]"),
126
+ children: [
127
+ /* @__PURE__ */ jsx(SearchIcon, { className: "w-3.5 h-3.5 shrink-0 text-(--theme-muted-text-color)" }),
128
+ search.terms.map((term) => /* @__PURE__ */ jsxs("span", {
129
+ className: "flex max-w-40 items-center gap-1 rounded border border-(--theme-border) bg-(--theme-app-bg) px-1.5 py-px text-[12px] text-(--theme-app)",
130
+ children: [/* @__PURE__ */ jsx("span", {
131
+ className: "truncate",
132
+ children: term
133
+ }), /* @__PURE__ */ jsx("button", {
134
+ type: "button",
135
+ onClick: () => search.removeTerm(term),
136
+ "aria-label": list.removeSearchTerm(term),
137
+ className: "shrink-0 rounded p-0.5 text-(--theme-muted-text-color) hover:text-(--theme-app) transition-colors",
138
+ children: /* @__PURE__ */ jsx(RemoveIcon, { className: "w-2.5 h-2.5" })
139
+ })]
140
+ }, term)),
141
+ /* @__PURE__ */ jsx("input", {
142
+ ref: search.inputRef,
143
+ value: search.filter,
144
+ onChange: (e) => search.setFilter(e.target.value),
145
+ onFocus: search.openSearch,
146
+ "aria-label": label,
147
+ onKeyDown: (e) => {
148
+ if (closeOnEscape(e)) return;
149
+ if (e.key === "Enter") {
150
+ e.preventDefault();
151
+ search.addTerm(search.filter);
152
+ } else if (e.key === "Backspace" && !search.filter) {
153
+ const lastTerm = search.terms.at(-1);
154
+ if (lastTerm) search.removeTerm(lastTerm);
155
+ }
156
+ },
157
+ placeholder,
158
+ className: "min-w-24 flex-1 bg-transparent text-[13px] text-(--theme-app) placeholder:text-(--theme-placeholder) focus:outline-none"
159
+ }),
160
+ /* @__PURE__ */ jsx("button", {
82
161
  type: "button",
83
- onClick: () => search.removeTerm(term),
84
- "aria-label": list.removeSearchTerm(term),
85
- className: "shrink-0 rounded p-0.5 text-(--theme-muted-text-color) hover:text-(--theme-app) transition-colors",
86
- children: /* @__PURE__ */ jsx(RemoveIcon, { className: "w-2.5 h-2.5" })
87
- })]
88
- }, term)),
89
- /* @__PURE__ */ jsx("input", {
90
- ref: search.inputRef,
91
- value: search.filter,
92
- onChange: (e) => search.setFilter(e.target.value),
93
- onFocus: search.openSearch,
94
- "aria-label": label,
95
- onKeyDown: (e) => {
96
- if (e.key === "Escape" && search.searchOpen) {
97
- e.preventDefault();
98
- search.closeSearch();
99
- } else if (e.key === "Enter") {
100
- e.preventDefault();
101
- search.addTerm(search.filter);
102
- } else if (e.key === "Backspace" && !search.filter) {
103
- const lastTerm = search.terms.at(-1);
104
- if (lastTerm) search.removeTerm(lastTerm);
105
- }
106
- },
107
- placeholder,
108
- className: "min-w-24 flex-1 bg-transparent text-[13px] text-(--theme-app) placeholder:text-(--theme-placeholder) focus:outline-none"
109
- }),
110
- /* @__PURE__ */ jsx("button", {
111
- type: "button",
112
- onClick: search.clearSearch,
113
- "aria-label": common.clearSearch,
114
- className: "shrink-0 rounded p-0.5 text-(--theme-muted-text-color) hover:text-(--theme-app) transition-colors",
115
- children: /* @__PURE__ */ jsx(CloseIcon, { className: "w-3 h-3" })
162
+ onClick: close,
163
+ onKeyDown: closeOnEscape,
164
+ "aria-label": common.clearSearch,
165
+ className: cx("shrink-0 rounded text-(--theme-muted-text-color) hover:text-(--theme-app) transition-colors", covering ? "-me-1 inline-flex size-6 items-center justify-center" : "p-0.5"),
166
+ children: /* @__PURE__ */ jsx(CloseIcon, { className: "w-3 h-3" })
167
+ })
168
+ ]
169
+ });
170
+ if (covering && coverNode) return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
171
+ "data-header-search": "",
172
+ className: "contents",
173
+ children: /* @__PURE__ */ jsx(SearchButton, {
174
+ label,
175
+ name: active ? list.searchActive(label) : label,
176
+ active,
177
+ onOpen: search.openSearch
116
178
  })
117
- ]
179
+ }), createPortal(box, coverNode)] });
180
+ return fit ? /* @__PURE__ */ jsx("span", {
181
+ "data-header-search": "",
182
+ className: "contents",
183
+ children: box
184
+ }) : box;
185
+ }
186
+ const control = /* @__PURE__ */ jsx(SearchButton, {
187
+ ref: button,
188
+ label,
189
+ name: active ? list.searchActive(label) : label,
190
+ active,
191
+ onOpen: search.openSearch
118
192
  });
193
+ return fit ? /* @__PURE__ */ jsx("span", {
194
+ "data-header-search": "",
195
+ className: "contents",
196
+ children: control
197
+ }) : control;
198
+ }
199
+ var subscribeNever = () => () => {};
200
+ var zero = () => 0;
201
+ /** The search closed: a button, with a dot while a query it holds narrows the list. */
202
+ function SearchButton({ ref, label, name = label, active, onOpen }) {
119
203
  return /* @__PURE__ */ jsx(Tooltip, {
120
204
  content: label,
121
205
  shortcut: "/",
122
- children: /* @__PURE__ */ jsx("button", {
206
+ children: /* @__PURE__ */ jsxs("button", {
207
+ ref,
123
208
  type: "button",
124
- onClick: search.openSearch,
125
- "aria-label": label,
209
+ onClick: onOpen,
210
+ "aria-label": name,
126
211
  "aria-keyshortcuts": `/ ${ariaKeyShortcuts("Mod+F")}`,
127
212
  className: buttonClasses({
128
213
  variant: "outline",
129
214
  size: "sm",
130
215
  iconOnly: true
131
216
  }),
132
- children: /* @__PURE__ */ jsx(SearchIcon, {})
217
+ children: [/* @__PURE__ */ jsx(SearchIcon, {}), active && /* @__PURE__ */ jsx("span", { className: "absolute right-0 top-0 h-1.5 w-1.5 rounded-full bg-(--theme-link)" })]
133
218
  })
134
219
  });
135
220
  }
@@ -141,14 +226,25 @@ var ListFooterCountContext = createContext(false);
141
226
  var ListToolbarInlineContext = createContext(false);
142
227
  /** The count's words, "7 tokens" or "2 of 7 tokens", as a status so a screen reader hears the
143
228
  * count change as a filter narrows it. */
144
- function ListCountText({ total, matches, filterActive, noun, nounPlural }) {
229
+ function ListCountText(props) {
230
+ return /* @__PURE__ */ jsx(ListCountStatus, { count: props });
231
+ }
232
+ /** A list's count as a status that can say nothing yet: kept mounted while its text is empty, so
233
+ * a screen reader hears the count when it arrives, as it would not from a status that mounts
234
+ * with its text already in. */
235
+ function ListCountStatus({ count, className = "text-[13px] text-(--theme-muted-text-color)" }) {
145
236
  const { list: t } = useStrings();
146
237
  const locale = useLocale();
147
- const count = (n) => formatInteger(n, locale);
238
+ const n = (value) => formatInteger(value, locale);
239
+ let words = "";
240
+ if (count) {
241
+ const { total, matches, filterActive, noun, nounPlural } = count;
242
+ words = filterActive ? t.matchCount(n(matches), n(total), nounPlural) : t.count(n(total), total === 1 ? noun : nounPlural);
243
+ }
148
244
  return /* @__PURE__ */ jsx("span", {
149
245
  role: "status",
150
- className: "text-[13px] text-(--theme-muted-text-color)",
151
- children: filterActive ? t.matchCount(count(matches), count(total), nounPlural) : t.count(count(total), total === 1 ? noun : nounPlural)
246
+ className,
247
+ children: words
152
248
  });
153
249
  }
154
250
  /** A list's count, placed by hand.
@@ -163,4 +259,4 @@ function ListCount(props) {
163
259
  });
164
260
  }
165
261
  //#endregion
166
- export { ListCount, ListCountText, ListFooterCountContext, ListSearchControl, ListToolbarInlineContext, useListSearch };
262
+ export { ListCount, ListCountStatus, ListCountText, ListFooterCountContext, ListSearchControl, ListToolbarInlineContext, useListSearch };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parallelworks/ui",
3
- "version": "0.93.0",
3
+ "version": "0.94.1",
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": {
@@ -81,7 +81,9 @@
81
81
  "types": "./dist/app-frame/index.d.ts",
82
82
  "import": "./dist/app-frame/index.js"
83
83
  },
84
- "./app-frame.css": "./dist/app-frame/app-frame.css"
84
+ "./app-frame.css": "./dist/app-frame/app-frame.css",
85
+ "./app-frame/layout.css": "./dist/app-frame/layout.css",
86
+ "./app-frame/navigation.css": "./dist/app-frame/navigation.css"
85
87
  },
86
88
  "publishConfig": {
87
89
  "registry": "https://registry.npmjs.org"
@@ -153,7 +155,7 @@
153
155
  "scripts": {
154
156
  "build": "vite build && pnpm build:types && pnpm build:css",
155
157
  "build:types": "tsc -p tsconfig.build.json && node scripts/declaration-extensions.mjs",
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/",
158
+ "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/*.css dist/app-frame/ && cat src/app-frame/layout.css src/app-frame/navigation.css > dist/app-frame/app-frame.css",
157
159
  "clean": "rm -rf dist",
158
160
  "test": "vitest run",
159
161
  "lint:package": "publint --strict",