@notis_ai/cli 0.2.0-beta.1793038587372.1 → 0.2.0-beta.1793098461724.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/dist/sdk/src/components/MultiSelectActionBar.tsx +16 -5
- package/dist/sdk/src/hooks/useCollectionInteractions.ts +14 -1
- package/dist/sdk/src/interactions.ts +2 -0
- package/dist/sdk/src/ui/engine/step.ts +54 -0
- package/dist/space-harness/host.js +79 -74
- package/dist/space-sdk/dist/{chunk-LJHS6OJW.js → chunk-24AUBDKN.js} +2 -2
- package/dist/space-sdk/dist/index.js +8 -3
- package/dist/space-sdk/dist/ui.js +3 -3
- package/dist/space-sdk/src/components/MultiSelectActionBar.d.ts +5 -1
- package/dist/space-sdk/src/hooks/useCollectionInteractions.d.ts +6 -0
- package/dist/space-sdk/src/tailwind-candidates.d.ts +36 -1
- package/dist/space-sdk/src/ui/engine/index.d.ts +2 -2
- package/dist/space-sdk/src/ui/engine/step.d.ts +9 -0
- package/dist/space-sdk/src/ui/list/ListReader.d.ts +7 -4
- package/dist/space-sdk/src/ui/list/useListKeys.d.ts +4 -0
- package/dist/view-renderer/dist/host.js +147 -142
- package/package.json +3 -3
|
@@ -27,6 +27,10 @@ export interface MultiSelectActionBarProps {
|
|
|
27
27
|
actions: MultiSelectAction[];
|
|
28
28
|
/** Override the "{count} selected" label units. Default: "selected" with no item word. */
|
|
29
29
|
itemLabel?: { singular: string; plural: string };
|
|
30
|
+
/** Complete count text in the caller's language, including plural agreement. */
|
|
31
|
+
selectionLabel?: string;
|
|
32
|
+
/** Accessible toolbar name in the caller's language. */
|
|
33
|
+
toolbarLabel?: string;
|
|
30
34
|
/** Optional extra class on the outer container (composed alongside the inline styles). */
|
|
31
35
|
className?: string;
|
|
32
36
|
/** Optional override for the bar's positioning style. Defaults to bottom-center floating. */
|
|
@@ -125,6 +129,8 @@ export function MultiSelectActionBar({
|
|
|
125
129
|
selectedCount,
|
|
126
130
|
actions,
|
|
127
131
|
itemLabel,
|
|
132
|
+
selectionLabel,
|
|
133
|
+
toolbarLabel,
|
|
128
134
|
className,
|
|
129
135
|
style,
|
|
130
136
|
shortcutsEnabled = true,
|
|
@@ -196,9 +202,9 @@ export function MultiSelectActionBar({
|
|
|
196
202
|
? itemLabel.singular
|
|
197
203
|
: itemLabel.plural
|
|
198
204
|
: 'selected';
|
|
199
|
-
const countLabel = itemLabel
|
|
205
|
+
const countLabel = selectionLabel ?? (itemLabel
|
|
200
206
|
? `${selectedCount} ${countWord} selected`
|
|
201
|
-
: `${selectedCount} selected
|
|
207
|
+
: `${selectedCount} selected`);
|
|
202
208
|
|
|
203
209
|
const composedStyle: CSSProperties = { ...containerBaseStyle, ...(style || {}) };
|
|
204
210
|
|
|
@@ -215,7 +221,7 @@ export function MultiSelectActionBar({
|
|
|
215
221
|
onClearSelection();
|
|
216
222
|
}}
|
|
217
223
|
role="toolbar"
|
|
218
|
-
aria-label={`Bulk actions for ${selectedCount} selected ${countWord}`}
|
|
224
|
+
aria-label={toolbarLabel ?? `Bulk actions for ${selectedCount} selected ${countWord}`}
|
|
219
225
|
className={className}
|
|
220
226
|
style={composedStyle}
|
|
221
227
|
>
|
|
@@ -225,8 +231,13 @@ export function MultiSelectActionBar({
|
|
|
225
231
|
[data-notis-bulk-actions] { flex-wrap: wrap; width: calc(100% - 2rem) !important; }
|
|
226
232
|
[data-notis-bulk-count] { flex-basis: 100%; padding: 0.375rem 0.5rem !important; font-size: 14px !important; }
|
|
227
233
|
[data-notis-bulk-divider] { display: none; }
|
|
228
|
-
|
|
229
|
-
|
|
234
|
+
/* Every action gets an equal column, icon over label, so none is cut off at the edge
|
|
235
|
+
(longer labels such as French ones wrap inside their column instead). */
|
|
236
|
+
[data-notis-bulk-action-list] { display: grid !important; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); width: 100%; overflow: visible !important; }
|
|
237
|
+
[data-notis-bulk-actions] button { flex-direction: column; justify-content: flex-start; gap: 4px !important; min-width: 0; min-height: 56px; padding: 8px 4px 6px !important; font-size: 12px !important; line-height: 1.25; white-space: normal !important; text-align: center; }
|
|
238
|
+
[data-notis-bulk-actions] kbd { display: none !important; }
|
|
239
|
+
[data-notis-bulk-action-icon][data-has-shortcut] { display: inline-flex !important; }
|
|
240
|
+
[data-notis-bulk-action-icon] svg { width: 18px; height: 18px; }
|
|
230
241
|
}
|
|
231
242
|
@media (hover: none) and (pointer: coarse) {
|
|
232
243
|
[data-notis-bulk-actions] kbd { display: none !important; }
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
|
|
27
27
|
import type { MultiSelectActionBarProps } from '../components/MultiSelectActionBar';
|
|
28
28
|
import { isInteractionElementVisible } from '../interactions/visibility';
|
|
29
|
+
import { travelBetween, type TravelDirection } from '../ui/engine/step';
|
|
29
30
|
import { useLongPressSelection } from './useLongPressSelection';
|
|
30
31
|
import { useMarqueeSelection, type SelectionMarqueeRect } from './useMarqueeSelection';
|
|
31
32
|
|
|
@@ -117,6 +118,11 @@ export interface CollectionInteractionController<T> {
|
|
|
117
118
|
lastClickedId: string | null;
|
|
118
119
|
dragRect: SelectionMarqueeRect | null;
|
|
119
120
|
actions: ResolvedCollectionAction[];
|
|
121
|
+
/**
|
|
122
|
+
* The way the active item last moved (J/K, the arrows, a click further up or down); 'next' until it moves. A page
|
|
123
|
+
* that removes the active item continues this way (iOS Photos), else the other way at the edge.
|
|
124
|
+
*/
|
|
125
|
+
travelDirection: () => TravelDirection;
|
|
120
126
|
getActionBarProps: () => Pick<MultiSelectActionBarProps, 'selectedCount' | 'actions' | 'collectionOwnerId' | 'isAvailable' | 'shortcutsEnabled' | 'onClearSelection'>;
|
|
121
127
|
|
|
122
128
|
isSelected: (id: string) => boolean;
|
|
@@ -237,6 +243,7 @@ export function useCollectionInteractions<T>(
|
|
|
237
243
|
const selectedIdsRef = useRef(selectedIds);
|
|
238
244
|
const activeIdRef = useRef(activeId);
|
|
239
245
|
const anchorIdRef = useRef(anchorId);
|
|
246
|
+
const travelRef = useRef<TravelDirection>('next');
|
|
240
247
|
// Keep manually selected rows outside the current keyboard range on reversal.
|
|
241
248
|
const keyboardRangeBaseRef = useRef<Set<string> | null>(null);
|
|
242
249
|
const onSelectionChangeRef = useRef(onSelectionChange);
|
|
@@ -307,6 +314,9 @@ export function useCollectionInteractions<T>(
|
|
|
307
314
|
const setActive = useCallback((id: string | null, reason: CollectionInteractionReason = 'activate') => {
|
|
308
315
|
const next = id && selectableIdsRef.current.includes(id) ? id : null;
|
|
309
316
|
if (activeIdRef.current === next) return;
|
|
317
|
+
// A click or focus further up or down sets the way; keys set it themselves (moveActive), a wrap included.
|
|
318
|
+
const travel = reason === 'items-changed' ? null : travelBetween(selectableIdsRef.current, activeIdRef.current, next);
|
|
319
|
+
if (travel) travelRef.current = travel;
|
|
310
320
|
activeIdRef.current = next;
|
|
311
321
|
if (controlledActiveId === undefined) setUncontrolledActiveId(next);
|
|
312
322
|
onActiveIdChangeRef.current?.(next, reason);
|
|
@@ -448,6 +458,7 @@ export function useCollectionInteractions<T>(
|
|
|
448
458
|
const ids = selectableIdsRef.current;
|
|
449
459
|
if (ids.length === 0) return;
|
|
450
460
|
const current = activeIdRef.current;
|
|
461
|
+
const forward = direction === 'next' || direction === 'down' || direction === 'right';
|
|
451
462
|
// Range gestures follow the displayed order, independent of asymmetric grid geometry.
|
|
452
463
|
let next = extend ? null : resolveNextIdRef.current?.({
|
|
453
464
|
direction,
|
|
@@ -457,7 +468,6 @@ export function useCollectionInteractions<T>(
|
|
|
457
468
|
}) ?? null;
|
|
458
469
|
if (!next) {
|
|
459
470
|
const currentIndex = current ? ids.indexOf(current) : -1;
|
|
460
|
-
const forward = direction === 'next' || direction === 'down' || direction === 'right';
|
|
461
471
|
const nextIndex = currentIndex === -1
|
|
462
472
|
? forward ? 0 : ids.length - 1
|
|
463
473
|
: Math.min(ids.length - 1, Math.max(0, currentIndex + (forward ? 1 : -1)));
|
|
@@ -478,6 +488,7 @@ export function useCollectionInteractions<T>(
|
|
|
478
488
|
setAnchor(next, 'navigate');
|
|
479
489
|
}
|
|
480
490
|
setActive(next, extend ? 'range' : 'navigate');
|
|
491
|
+
travelRef.current = forward ? 'next' : 'previous';
|
|
481
492
|
const nextId = next;
|
|
482
493
|
requestAnimationFrame(() => focusItem(nextId));
|
|
483
494
|
}, [focusItem, replaceRange, setActive, setAnchor]);
|
|
@@ -649,6 +660,7 @@ export function useCollectionInteractions<T>(
|
|
|
649
660
|
}), [enabled, onCheckboxClick]);
|
|
650
661
|
|
|
651
662
|
const getSelectedItems = useCallback(() => getItemsForIds(selectedIdsRef.current), [getItemsForIds]);
|
|
663
|
+
const travelDirection = useCallback(() => travelRef.current, []);
|
|
652
664
|
const isSelected = useCallback((id: string) => selectedIds.has(id), [selectedIds]);
|
|
653
665
|
|
|
654
666
|
const actionContext = useMemo<CollectionActionContext<T>>(() => ({
|
|
@@ -716,6 +728,7 @@ export function useCollectionInteractions<T>(
|
|
|
716
728
|
lastClickedId: anchorId,
|
|
717
729
|
dragRect,
|
|
718
730
|
actions: resolvedActions,
|
|
731
|
+
travelDirection,
|
|
719
732
|
getActionBarProps,
|
|
720
733
|
isSelected,
|
|
721
734
|
getSelectedItems,
|
|
@@ -19,6 +19,8 @@ export type {
|
|
|
19
19
|
ResolvedCollectionAction,
|
|
20
20
|
} from './interactions/actions';
|
|
21
21
|
export { useCollectionInteractions, COLLECTION_ITEM_ATTRIBUTE } from './hooks/useCollectionInteractions';
|
|
22
|
+
export { continuationOf, travelBetween } from './ui/engine/step';
|
|
23
|
+
export type { TravelDirection } from './ui/engine/step';
|
|
22
24
|
export type {
|
|
23
25
|
CollectionInteractionController,
|
|
24
26
|
CollectionInteractionReason,
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface StepOptions {
|
|
2
|
+
/** Wrap from the last item to the first (and back). Default true. */
|
|
3
|
+
wrap?: boolean;
|
|
4
|
+
/** The key is held (`event.repeat`): never wrap, stop at the edge. */
|
|
5
|
+
repeat?: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Next cursor id for J/K stepping. Wraps only on a fresh press, so holding J stops at the last row instead of
|
|
10
|
+
* spinning back to the top. No cursor: J lands on the first item, K on the last.
|
|
11
|
+
*/
|
|
12
|
+
export function stepCursor(ids: readonly string[], cursor: string | null | undefined, delta: number,
|
|
13
|
+
options: StepOptions = {}): string | null {
|
|
14
|
+
if (!ids.length) return null;
|
|
15
|
+
const { wrap = true, repeat = false } = options;
|
|
16
|
+
const index = cursor == null ? -1 : ids.indexOf(cursor);
|
|
17
|
+
if (index === -1) return delta >= 0 ? ids[0]! : ids[ids.length - 1]!;
|
|
18
|
+
let next = index + delta;
|
|
19
|
+
if (next < 0 || next >= ids.length) {
|
|
20
|
+
if (wrap && !repeat) next = ((next % ids.length) + ids.length) % ids.length;
|
|
21
|
+
else next = Math.max(0, Math.min(ids.length - 1, next));
|
|
22
|
+
}
|
|
23
|
+
return ids[next]!;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The way the cursor last moved through a list. A removal continues that way (iOS Photos). */
|
|
27
|
+
export type TravelDirection = 'next' | 'previous';
|
|
28
|
+
|
|
29
|
+
/** The way a move from `from` to `to` went in `ids`; null when either is not in the list or nothing moved. */
|
|
30
|
+
export function travelBetween(ids: readonly string[], from: string | null | undefined,
|
|
31
|
+
to: string | null | undefined): TravelDirection | null {
|
|
32
|
+
if (from == null || to == null || from === to) return null;
|
|
33
|
+
const a = ids.indexOf(from);
|
|
34
|
+
const b = ids.indexOf(to);
|
|
35
|
+
if (a === -1 || b === -1) return null;
|
|
36
|
+
return b > a ? 'next' : 'previous';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The id that takes `gone`'s place after it left `previous` (`now` holds the ids still listed): the nearest one still
|
|
41
|
+
* there in the travel direction, else the nearest the other way. Null when none is left.
|
|
42
|
+
*/
|
|
43
|
+
export function continuationOf(previous: readonly string[], now: readonly string[] | ReadonlySet<string>,
|
|
44
|
+
gone: string, direction: TravelDirection = 'next'): string | null {
|
|
45
|
+
const present = now instanceof Set ? now : new Set(now as readonly string[]);
|
|
46
|
+
const index = previous.indexOf(gone);
|
|
47
|
+
if (index === -1) return null;
|
|
48
|
+
const scan = (delta: number) => {
|
|
49
|
+
for (let i = index + delta; i >= 0 && i < previous.length; i += delta) if (present.has(previous[i]!)) return previous[i]!;
|
|
50
|
+
return null;
|
|
51
|
+
};
|
|
52
|
+
const first = direction === 'next' ? 1 : -1;
|
|
53
|
+
return scan(first) ?? scan(-first);
|
|
54
|
+
}
|