@cosxai/ui 0.2.9 → 0.3.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/package.json +1 -1
- package/src/actionbar/ActionBar.tsx +81 -43
- package/src/actionbar/index.ts +1 -0
- package/src/actionbar/types.ts +14 -0
- package/src/styles/index.css +1 -0
package/package.json
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
useRef,
|
|
7
7
|
useState,
|
|
8
8
|
type CSSProperties,
|
|
9
|
+
type ReactNode,
|
|
9
10
|
} from "react";
|
|
10
11
|
import { ActionBarContext } from "./actionbar-context";
|
|
11
12
|
import { ActionBarButton } from "./ActionBarButton";
|
|
@@ -229,8 +230,29 @@ export function ActionBar({
|
|
|
229
230
|
setPos({ type: "default" });
|
|
230
231
|
}, [storageKey]);
|
|
231
232
|
|
|
232
|
-
// ----- Entries (flat vs group) -----
|
|
233
|
-
|
|
233
|
+
// ----- Entries (flat vs group), partitioned by slot -----
|
|
234
|
+
// Items default to the leading slot. Trailing items render after a
|
|
235
|
+
// flex spacer so they pin to the right edge regardless of
|
|
236
|
+
// registration order — system status indicators (sync, identity,
|
|
237
|
+
// connection) belong here, where page items registering later
|
|
238
|
+
// can't shuffle them. buildEntries runs per slot so category
|
|
239
|
+
// grouping stays slot-local (a category split across slots is an
|
|
240
|
+
// edge case we intentionally don't fold across the spacer).
|
|
241
|
+
const { leadingEntries, trailingEntries } = useMemo(() => {
|
|
242
|
+
const leading: ActionBarItem[] = [];
|
|
243
|
+
const trailing: ActionBarItem[] = [];
|
|
244
|
+
for (const it of items) {
|
|
245
|
+
(it.slot === "trailing" ? trailing : leading).push(it);
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
leadingEntries: buildEntries(leading),
|
|
249
|
+
trailingEntries: buildEntries(trailing),
|
|
250
|
+
};
|
|
251
|
+
}, [items]);
|
|
252
|
+
const entries = useMemo(
|
|
253
|
+
() => [...leadingEntries, ...trailingEntries],
|
|
254
|
+
[leadingEntries, trailingEntries],
|
|
255
|
+
);
|
|
234
256
|
|
|
235
257
|
// Cleanup stale expansion if the corresponding group disappears.
|
|
236
258
|
useEffect(() => {
|
|
@@ -377,49 +399,65 @@ export function ActionBar({
|
|
|
377
399
|
</button>
|
|
378
400
|
) : null}
|
|
379
401
|
|
|
380
|
-
{/*
|
|
381
|
-
{
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
const catDef = categories[cat];
|
|
399
|
-
const hasActiveChild = entry.groupItems!.some((it) => it.active);
|
|
400
|
-
const isOpen = expandedKey === entry.expansionKey;
|
|
401
|
-
return (
|
|
402
|
-
<ActionBarMenuGroup
|
|
403
|
-
key={entry.expansionKey}
|
|
404
|
-
label={catDef?.label ?? cat}
|
|
405
|
-
icon={catDef?.icon}
|
|
406
|
-
hasActiveChild={hasActiveChild}
|
|
407
|
-
isOpen={isOpen}
|
|
408
|
-
onToggle={() =>
|
|
409
|
-
setExpandedKey(isOpen ? null : entry.expansionKey)
|
|
410
|
-
}
|
|
411
|
-
items={entry.groupItems!}
|
|
412
|
-
onItemClicked={(it) => {
|
|
413
|
-
it.onClick();
|
|
414
|
-
if (!it.keepGroupOpenOnClick) {
|
|
415
|
-
setExpandedKey(null);
|
|
416
|
-
}
|
|
417
|
-
}}
|
|
418
|
-
/>
|
|
419
|
-
);
|
|
420
|
-
})}
|
|
402
|
+
{/* Leading entries — flat items + group heads with disclosure regions. */}
|
|
403
|
+
{leadingEntries.map(renderEntry)}
|
|
404
|
+
|
|
405
|
+
{/* Spacer pushes trailing entries to the right edge of the bar.
|
|
406
|
+
Only rendered when there's at least one trailing entry, so the
|
|
407
|
+
bar still hugs its content tightly when no trailing slot is in
|
|
408
|
+
use. */}
|
|
409
|
+
{trailingEntries.length > 0 && (
|
|
410
|
+
<span
|
|
411
|
+
aria-hidden
|
|
412
|
+
data-ck-actionbar-spacer
|
|
413
|
+
style={{ flex: "1 1 auto", minWidth: 12 }}
|
|
414
|
+
/>
|
|
415
|
+
)}
|
|
416
|
+
|
|
417
|
+
{/* Trailing entries — pin to the right edge regardless of
|
|
418
|
+
registration order. Same rendering rules as leading. */}
|
|
419
|
+
{trailingEntries.map(renderEntry)}
|
|
421
420
|
</div>
|
|
422
421
|
);
|
|
422
|
+
|
|
423
|
+
function renderEntry(entry: BuildEntry): ReactNode {
|
|
424
|
+
if (entry.kind === "flat" && entry.item) {
|
|
425
|
+
return (
|
|
426
|
+
<ActionBarButton
|
|
427
|
+
key={entry.expansionKey}
|
|
428
|
+
icon={entry.item.icon}
|
|
429
|
+
label={entry.item.label}
|
|
430
|
+
title={entry.item.title}
|
|
431
|
+
active={entry.item.active}
|
|
432
|
+
disabled={entry.item.disabled}
|
|
433
|
+
variant={entry.item.variant}
|
|
434
|
+
hint={entry.item.hint}
|
|
435
|
+
onClick={entry.item.onClick}
|
|
436
|
+
/>
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
const cat = entry.category!;
|
|
440
|
+
const catDef = categories[cat];
|
|
441
|
+
const hasActiveChild = entry.groupItems!.some((it) => it.active);
|
|
442
|
+
const isOpen = expandedKey === entry.expansionKey;
|
|
443
|
+
return (
|
|
444
|
+
<ActionBarMenuGroup
|
|
445
|
+
key={entry.expansionKey}
|
|
446
|
+
label={catDef?.label ?? cat}
|
|
447
|
+
icon={catDef?.icon}
|
|
448
|
+
hasActiveChild={hasActiveChild}
|
|
449
|
+
isOpen={isOpen}
|
|
450
|
+
onToggle={() => setExpandedKey(isOpen ? null : entry.expansionKey)}
|
|
451
|
+
items={entry.groupItems!}
|
|
452
|
+
onItemClicked={(it) => {
|
|
453
|
+
it.onClick();
|
|
454
|
+
if (!it.keepGroupOpenOnClick) {
|
|
455
|
+
setExpandedKey(null);
|
|
456
|
+
}
|
|
457
|
+
}}
|
|
458
|
+
/>
|
|
459
|
+
);
|
|
460
|
+
}
|
|
423
461
|
}
|
|
424
462
|
|
|
425
463
|
const leftEdgeButtonStyle: CSSProperties = {
|
package/src/actionbar/index.ts
CHANGED
package/src/actionbar/types.ts
CHANGED
|
@@ -7,6 +7,16 @@ import type { ReactNode } from "react";
|
|
|
7
7
|
|
|
8
8
|
export type ActionBarItemVariant = "ghost" | "primary" | "soft";
|
|
9
9
|
|
|
10
|
+
// Slot anchor — which edge of the bar the item gravitates toward.
|
|
11
|
+
// `leading` (default) keeps existing behavior: items pack from the
|
|
12
|
+
// drag grip outward in registration order. `trailing` pins the item
|
|
13
|
+
// to the right edge with a flex spacer separating it from the
|
|
14
|
+
// leading group, regardless of registration order. Use trailing for
|
|
15
|
+
// system status indicators (sync, connection, identity) that need a
|
|
16
|
+
// stable visual home and shouldn't be shuffled by page items
|
|
17
|
+
// registering after them.
|
|
18
|
+
export type ActionBarItemSlot = "leading" | "trailing";
|
|
19
|
+
|
|
10
20
|
export interface ActionBarItem {
|
|
11
21
|
// React key for the rendered button. Stable across rerenders.
|
|
12
22
|
key: string;
|
|
@@ -33,6 +43,10 @@ export interface ActionBarItem {
|
|
|
33
43
|
// (e.g. for items whose post-click state changes the label
|
|
34
44
|
// and should stay visible).
|
|
35
45
|
keepGroupOpenOnClick?: boolean;
|
|
46
|
+
// Slot anchor. Defaults to `leading`. Trailing items render after
|
|
47
|
+
// a flex spacer so they pin to the right edge regardless of
|
|
48
|
+
// registration order — system status indicators belong here.
|
|
49
|
+
slot?: ActionBarItemSlot;
|
|
36
50
|
}
|
|
37
51
|
|
|
38
52
|
// Category definition — declared at the provider level. Drives
|