@cosxai/ui 1.0.0-alpha.13 → 1.0.0-alpha.15

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/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## 1.0.0-alpha.15
2
+
3
+ - ActionBar `foldable={false}`: always shown in full — no fold button, and a stored fold is ignored (a page whose bar is its whole toolbar, like a document viewer).
4
+
5
+ ## 1.0.0-alpha.14
6
+
7
+ - ActionBar: an action can carry `divider` (a rule before it), `iconOnly` (just the icon, label in the tooltip) and `wrap` + `wrapKey` (wrap the button in a menu or popover trigger so it opens something anchored to itself).
8
+
1
9
  ## 1.0.0-alpha.13 (2026-10-04)
2
10
 
3
11
  - BottomSheet: the scrolling list reaches the sheet's sides (its padding
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosxai/ui",
3
- "version": "1.0.0-alpha.13",
3
+ "version": "1.0.0-alpha.15",
4
4
  "description": "COSX Design System 3.0 \u2014 tokens, Tailwind v4 theme and React components",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -11,6 +11,7 @@ import {
11
11
  useRef,
12
12
  useState,
13
13
  type KeyboardEvent as ReactKeyboardEvent,
14
+ type ReactElement,
14
15
  type ReactNode,
15
16
  } from 'react';
16
17
 
@@ -35,6 +36,14 @@ export type ActionBarAction = {
35
36
  disabledReason?: string | undefined;
36
37
  /** Actions of a kind: more than two sharing a group fold into one menu. */
37
38
  group?: string | undefined;
39
+ /** A thin rule before it, setting it apart from the actions on its left. */
40
+ divider?: boolean | undefined;
41
+ /** Always just the icon (a star, ⋯); the label names it for screen readers and the tooltip. */
42
+ iconOnly?: boolean | undefined;
43
+ /** Wrap the button, e.g. in a menu's or popover's trigger (asChild), so it opens something anchored to itself. */
44
+ wrap?: ((button: ReactElement) => ReactElement) | undefined;
45
+ /** Changes when what `wrap` shows changes (the bar re-renders only when an action's fields change). */
46
+ wrapKey?: string | undefined;
38
47
  };
39
48
 
40
49
  export type ActionBarSelection = {
@@ -126,7 +135,7 @@ function editable(t: EventTarget | null): boolean {
126
135
  const dialogOpen = () => Boolean(document.querySelector('[role="dialog"][data-state="open"], [role="alertdialog"][data-state="open"]'));
127
136
 
128
137
  const signature = (a: ActionBarAction[] | null | undefined) =>
129
- a ? a.map((x) => [x.id, x.label, x.shortcut, x.active, x.disabled, x.disabledReason, x.group, x.icon?.displayName].join('\u0001')).join('\u0002') : '';
138
+ a ? a.map((x) => [x.id, x.label, x.shortcut, x.active, x.disabled, x.disabledReason, x.group, x.icon?.displayName, x.divider, x.iconOnly, x.wrapKey].join('\u0001')).join('\u0002') : '';
130
139
 
131
140
  // ─── Provider ─────────────────────────────────────────────────────────
132
141
 
@@ -384,6 +393,8 @@ export type ActionBarProps = {
384
393
  labels?: ActionBarLabels | undefined;
385
394
  /** Lay it out in place (documentation, a demo) instead of floating: no fixed position, no dragging. */
386
395
  inline?: boolean | undefined;
396
+ /** false: always shown in full — no fold button, a stored fold is ignored (a page whose bar is its whole toolbar). @default true */
397
+ foldable?: boolean | undefined;
387
398
  className?: string | undefined;
388
399
  };
389
400
 
@@ -400,7 +411,7 @@ const itemCls = cn(
400
411
  * Narrowing drops shortcuts, then labels, then folds to a handle on the
401
412
  * left edge. Drag by the grip; double-click it to reset.
402
413
  */
403
- export function ActionBar({ presentation, hidden: hiddenProp, maxActions = 6, labels = {}, inline = false, className }: ActionBarProps) {
414
+ export function ActionBar({ presentation, hidden: hiddenProp, maxActions = 6, labels = {}, inline = false, foldable = true, className }: ActionBarProps) {
404
415
  const ctx = useCtx();
405
416
  const { idle, selection, mode, hidden, activity } = ctx.read();
406
417
  const { prefs, setPrefs } = ctx;
@@ -411,7 +422,7 @@ export function ActionBar({ presentation, hidden: hiddenProp, maxActions = 6, la
411
422
  const state: ActionBarState = mode ? 'mode' : selection ? 'selection' : 'idle';
412
423
  const auto = presentation ?? presentationFor(width);
413
424
  // Entering a selection or a mode unfolds it; back in idle the user's fold state returns.
414
- const folded = state === 'idle' && (auto === 'folded' || (presentation === undefined && prefs.folded));
425
+ const folded = foldable && state === 'idle' && (auto === 'folded' || (presentation === undefined && prefs.folded));
415
426
  const shown: Exclude<ActionBarPresentation, 'folded'> = auto === 'folded' ? 'icons' : auto;
416
427
 
417
428
  // Pull the bar back into view when the window shrinks.
@@ -465,8 +476,8 @@ export function ActionBar({ presentation, hidden: hiddenProp, maxActions = 6, la
465
476
  const renderAction = (a: ActionBarAction) => {
466
477
  const keys = a.shortcut ? formatShortcut(a.shortcut) : null;
467
478
  const Glyph = a.icon;
468
- const iconOnly = shown === 'icons' && Glyph;
469
- const button = (
479
+ const iconOnly = (shown === 'icons' || a.iconOnly) && Glyph;
480
+ const plain = (
470
481
  <button
471
482
  type="button"
472
483
  aria-label={iconOnly ? a.label : undefined}
@@ -479,16 +490,18 @@ export function ActionBar({ presentation, hidden: hiddenProp, maxActions = 6, la
479
490
  >
480
491
  {Glyph && <Glyph size={16} strokeWidth={1.75} aria-hidden />}
481
492
  {!iconOnly && <span>{a.label}</span>}
482
- {shown === 'full' && keys && <kbd className="font-sans text-meta font-medium opacity-55">{keys}</kbd>}
493
+ {shown === 'full' && keys && !iconOnly && <kbd className="font-sans text-meta font-medium opacity-55">{keys}</kbd>}
483
494
  </button>
484
495
  );
485
- const hint = a.disabledReason ?? (shown !== 'full' ? [a.label, keys].filter(Boolean).join(' · ') : null);
486
- return hint && (iconOnly || a.disabledReason || keys) ? (
487
- <Tooltip key={a.id} content={hint}>
488
- {button}
489
- </Tooltip>
490
- ) : (
491
- <Fragment key={a.id}>{button}</Fragment>
496
+ const button = a.wrap ? a.wrap(plain) : plain;
497
+ const rule = a.divider ? <span aria-hidden className="mx-1 h-5 w-px shrink-0 bg-white/20 ink:bg-ink/20" /> : null;
498
+ const hint = a.disabledReason ?? (shown !== 'full' || iconOnly ? [a.label, keys].filter(Boolean).join(' · ') : null);
499
+ const tipped = hint && (iconOnly || a.disabledReason || keys) ? <Tooltip content={hint}>{button}</Tooltip> : button;
500
+ return (
501
+ <Fragment key={a.id}>
502
+ {rule}
503
+ {tipped}
504
+ </Fragment>
492
505
  );
493
506
  };
494
507
 
@@ -595,7 +608,7 @@ export function ActionBar({ presentation, hidden: hiddenProp, maxActions = 6, la
595
608
  </button>
596
609
  </>
597
610
  )}
598
- {state === 'idle' && (
611
+ {state === 'idle' && foldable && (
599
612
  <Tooltip content={`${labels.fold ?? 'Fold the action bar'} · \\`}>
600
613
  <button type="button" aria-label={labels.fold ?? 'Fold the action bar'} onClick={() => setPrefs({ folded: true })} className={cn(itemCls, 'px-2 opacity-60 hover:opacity-100 max-md:hidden')}>
601
614
  <ChevronsLeft size={16} aria-hidden />