@notis_ai/cli 0.2.0-beta.157.1 → 0.2.0-beta.159.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/agent-hooks/notis-agent-hook.mjs +8296 -7912
- package/dist/base-skills/notis-apps/SKILL.md +34 -513
- package/dist/base-skills/notis-apps/references/architecture.md +164 -0
- package/dist/base-skills/notis-apps/references/design.md +165 -0
- package/dist/base-skills/notis-apps/references/release.md +99 -0
- package/dist/base-skills/notis-apps/references/sdk.md +61 -0
- package/dist/base-skills/notis-apps/references/troubleshooting.md +23 -0
- package/dist/base-skills/notis-cli/SKILL.md +19 -200
- package/dist/base-skills/notis-cli/references/app-delivery.md +18 -0
- package/dist/base-skills/notis-cli/references/native-databases.md +20 -0
- package/dist/base-skills/notis-cli/references/tool-examples.md +56 -0
- package/dist/base-skills/notis-cli/references/troubleshooting.md +39 -0
- package/dist/base-skills/notis-query/SKILL.md +13 -651
- package/dist/base-skills/notis-query/references/database-discovery.md +59 -0
- package/dist/base-skills/notis-query/references/documents.md +50 -0
- package/dist/base-skills/notis-query/references/query.md +543 -0
- package/dist/skill-sync/index.js +24 -7
- package/dist/skill-sync/index.js.map +4 -4
- package/dist/skill-sync-worker.mjs +2989 -0
- package/package.json +1 -1
- package/src/cli.js +4 -0
- package/src/command-specs/diagnostics.js +37 -0
- package/src/command-specs/skills.js +23 -5
- package/src/runtime/profiles.js +5 -2
- package/src/runtime/skill-sync/cloud-client.ts +2 -1
- package/src/runtime/skill-sync/index.ts +24 -6
- package/src/runtime/skill-sync/types.ts +2 -0
- package/src/runtime/skill-sync-service.js +109 -0
- package/src/skill-sync-worker-entry.js +2 -0
- package/src/skill-sync-worker.js +50 -0
- package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +36 -7
- package/template/packages/sdk/src/components/MultiSelectCheckbox.tsx +3 -1
- package/template/packages/sdk/src/hooks/useCollectionInteractions.ts +138 -28
- package/template/packages/sdk/src/hooks/useDocuments.ts +4 -1
- package/template/packages/sdk/src/hooks/useLongPressSelection.ts +79 -0
- package/template/packages/sdk/src/hooks/useMultiSelect.ts +2 -8
- package/template/packages/sdk/src/index.ts +3 -0
- package/template/packages/sdk/src/interactions/actions.ts +14 -1
- package/template/packages/sdk/src/interactions/shortcuts.tsx +79 -19
- package/template/packages/sdk/src/interactions/visibility.ts +13 -0
- package/template/packages/sdk/src/interactions.ts +3 -0
- package/template/packages/sdk/src/queryCache.ts +10 -2
|
@@ -17,12 +17,17 @@ import {
|
|
|
17
17
|
} from '../interactions/actions';
|
|
18
18
|
import {
|
|
19
19
|
activateShortcutCollection,
|
|
20
|
+
releaseShortcutCollection,
|
|
20
21
|
createShortcutCollectionOwner,
|
|
21
22
|
useShortcuts,
|
|
22
23
|
type ShortcutDefinition,
|
|
23
24
|
type ShortcutScope,
|
|
24
25
|
} from '../interactions/shortcuts';
|
|
25
26
|
|
|
27
|
+
import type { MultiSelectActionBarProps } from '../components/MultiSelectActionBar';
|
|
28
|
+
import { isInteractionElementVisible } from '../interactions/visibility';
|
|
29
|
+
import { useLongPressSelection } from './useLongPressSelection';
|
|
30
|
+
|
|
26
31
|
export const COLLECTION_ITEM_ATTRIBUTE = 'data-notis-collection-item-id';
|
|
27
32
|
const LEGACY_ROW_ATTRIBUTE = 'data-notis-row-id';
|
|
28
33
|
const INTERACTIVE_SELECTOR = 'button, a, input, textarea, select, [role="button"], [contenteditable]:not([contenteditable="false"])';
|
|
@@ -93,6 +98,8 @@ export interface UseCollectionInteractionsOptions<T> {
|
|
|
93
98
|
enabled?: boolean;
|
|
94
99
|
bindKeyboardShortcuts?: boolean;
|
|
95
100
|
enableDragSelect?: boolean;
|
|
101
|
+
/** Touch-only long press; optional per view. */
|
|
102
|
+
enableLongPressSelection?: boolean;
|
|
96
103
|
/** Plain item clicks activate the item and clear checkbox selection by default. */
|
|
97
104
|
clearSelectionOnPlainClick?: boolean;
|
|
98
105
|
dragThreshold?: number;
|
|
@@ -114,6 +121,7 @@ export interface CollectionInteractionController<T> {
|
|
|
114
121
|
lastClickedId: string | null;
|
|
115
122
|
dragRect: SelectionMarqueeRect | null;
|
|
116
123
|
actions: ResolvedCollectionAction[];
|
|
124
|
+
getActionBarProps: () => Pick<MultiSelectActionBarProps, 'selectedCount' | 'actions' | 'collectionOwnerId' | 'isAvailable' | 'shortcutsEnabled' | 'onClearSelection'>;
|
|
117
125
|
|
|
118
126
|
isSelected: (id: string) => boolean;
|
|
119
127
|
getSelectedItems: () => T[];
|
|
@@ -128,7 +136,7 @@ export interface CollectionInteractionController<T> {
|
|
|
128
136
|
onCheckboxClick: (id: string) => (event: ReactMouseEvent) => void;
|
|
129
137
|
onRowMouseDown: (id: string) => (event: ReactMouseEvent) => void;
|
|
130
138
|
getRowProps: (id: string) => Record<string, string>;
|
|
131
|
-
getItemProps: (id: string) => {
|
|
139
|
+
getItemProps: (id: string) => Partial<ReturnType<ReturnType<typeof useLongPressSelection>>> & {
|
|
132
140
|
[COLLECTION_ITEM_ATTRIBUTE]: string;
|
|
133
141
|
[LEGACY_ROW_ATTRIBUTE]: string;
|
|
134
142
|
tabIndex: number;
|
|
@@ -213,6 +221,7 @@ export function useCollectionInteractions<T>(
|
|
|
213
221
|
enabled = true,
|
|
214
222
|
bindKeyboardShortcuts = true,
|
|
215
223
|
enableDragSelect = selectionMode === 'multiple',
|
|
224
|
+
enableLongPressSelection = false,
|
|
216
225
|
clearSelectionOnPlainClick = true,
|
|
217
226
|
dragThreshold = 5,
|
|
218
227
|
shortcuts: shortcutOverrides,
|
|
@@ -240,6 +249,8 @@ export function useCollectionInteractions<T>(
|
|
|
240
249
|
const selectedIdsRef = useRef(selectedIds);
|
|
241
250
|
const activeIdRef = useRef(activeId);
|
|
242
251
|
const anchorIdRef = useRef(anchorId);
|
|
252
|
+
// Keep manually selected rows outside the current keyboard range on reversal.
|
|
253
|
+
const keyboardRangeBaseRef = useRef<Set<string> | null>(null);
|
|
243
254
|
const onSelectionChangeRef = useRef(onSelectionChange);
|
|
244
255
|
const onActiveIdChangeRef = useRef(onActiveIdChange);
|
|
245
256
|
const onAnchorIdChangeRef = useRef(onAnchorIdChange);
|
|
@@ -247,6 +258,11 @@ export function useCollectionInteractions<T>(
|
|
|
247
258
|
const onActivateRef = useRef(onActivate);
|
|
248
259
|
const resolveNextIdRef = useRef(resolveNextId);
|
|
249
260
|
const containerRef = useRef<HTMLElement | null>(null);
|
|
261
|
+
const [containerElement, setContainerElement] = useState<HTMLElement | null>(null);
|
|
262
|
+
const setContainerRef = useCallback((node: HTMLElement | null) => {
|
|
263
|
+
containerRef.current = node;
|
|
264
|
+
setContainerElement(node);
|
|
265
|
+
}, []);
|
|
250
266
|
const shortcutCollectionOwnerRef = useRef<string | null>(null);
|
|
251
267
|
if (!shortcutCollectionOwnerRef.current) {
|
|
252
268
|
shortcutCollectionOwnerRef.current = createShortcutCollectionOwner();
|
|
@@ -254,6 +270,9 @@ export function useCollectionInteractions<T>(
|
|
|
254
270
|
|
|
255
271
|
itemsRef.current = items;
|
|
256
272
|
getIdRef.current = getId;
|
|
273
|
+
if (!setsEqual(selectedIdsRef.current, selectedIds) || anchorIdRef.current !== anchorId) {
|
|
274
|
+
keyboardRangeBaseRef.current = null;
|
|
275
|
+
}
|
|
257
276
|
selectedIdsRef.current = selectedIds;
|
|
258
277
|
activeIdRef.current = activeId;
|
|
259
278
|
anchorIdRef.current = anchorId;
|
|
@@ -316,6 +335,7 @@ export function useCollectionInteractions<T>(
|
|
|
316
335
|
const activate = useCallback((id: string) => {
|
|
317
336
|
const item = itemsRef.current.find((candidate) => getIdRef.current(candidate) === id);
|
|
318
337
|
if (!item || !selectableIdsRef.current.includes(id)) return;
|
|
338
|
+
keyboardRangeBaseRef.current = null;
|
|
319
339
|
setActive(id, 'activate');
|
|
320
340
|
setAnchor(id, 'activate');
|
|
321
341
|
onActivateRef.current?.(item, id);
|
|
@@ -323,6 +343,7 @@ export function useCollectionInteractions<T>(
|
|
|
323
343
|
|
|
324
344
|
const toggle = useCallback((id: string) => {
|
|
325
345
|
if (selectionMode === 'none' || !selectableIdsRef.current.includes(id)) return;
|
|
346
|
+
keyboardRangeBaseRef.current = null;
|
|
326
347
|
const next = new Set(selectedIdsRef.current);
|
|
327
348
|
if (next.has(id)) next.delete(id);
|
|
328
349
|
else {
|
|
@@ -334,6 +355,7 @@ export function useCollectionInteractions<T>(
|
|
|
334
355
|
}, [commitSelection, selectionMode, setAnchor]);
|
|
335
356
|
|
|
336
357
|
const select = useCallback((ids: string[]) => {
|
|
358
|
+
keyboardRangeBaseRef.current = null;
|
|
337
359
|
const next = new Set(ids);
|
|
338
360
|
const last = ids[ids.length - 1] ?? null;
|
|
339
361
|
setAnchor(last, 'toggle');
|
|
@@ -341,12 +363,14 @@ export function useCollectionInteractions<T>(
|
|
|
341
363
|
}, [commitSelection, setAnchor]);
|
|
342
364
|
|
|
343
365
|
const clear = useCallback(() => {
|
|
366
|
+
keyboardRangeBaseRef.current = null;
|
|
344
367
|
setAnchor(null, 'clear');
|
|
345
368
|
commitSelection(new Set(), 'clear');
|
|
346
369
|
}, [commitSelection, setAnchor]);
|
|
347
370
|
|
|
348
371
|
const selectAll = useCallback(() => {
|
|
349
372
|
if (selectionMode === 'none') return;
|
|
373
|
+
keyboardRangeBaseRef.current = null;
|
|
350
374
|
const ids = selectionMode === 'single' ? selectableIdsRef.current.slice(0, 1) : selectableIdsRef.current;
|
|
351
375
|
const last = ids[ids.length - 1] ?? null;
|
|
352
376
|
setAnchor(last, 'select-all');
|
|
@@ -361,12 +385,16 @@ export function useCollectionInteractions<T>(
|
|
|
361
385
|
commitSelection(new Set([toId]), reason);
|
|
362
386
|
return;
|
|
363
387
|
}
|
|
364
|
-
commitSelection(new Set(
|
|
388
|
+
commitSelection(new Set([
|
|
389
|
+
...(keyboardRangeBaseRef.current ?? []),
|
|
390
|
+
...ids.slice(Math.min(from, to), Math.max(from, to) + 1),
|
|
391
|
+
]), reason);
|
|
365
392
|
}, [commitSelection]);
|
|
366
393
|
|
|
367
394
|
const selectRange = useCallback((fromId: string, toId: string) => {
|
|
368
395
|
const ids = selectableIdsRef.current;
|
|
369
396
|
if (selectionMode === 'none' || !ids.includes(toId)) return;
|
|
397
|
+
keyboardRangeBaseRef.current = null;
|
|
370
398
|
const resolvedFromId = ids.includes(fromId)
|
|
371
399
|
? fromId
|
|
372
400
|
: activeIdRef.current && ids.includes(activeIdRef.current)
|
|
@@ -390,6 +418,9 @@ export function useCollectionInteractions<T>(
|
|
|
390
418
|
|
|
391
419
|
useEffect(() => {
|
|
392
420
|
const allowed = new Set(selectableIds);
|
|
421
|
+
if (keyboardRangeBaseRef.current) {
|
|
422
|
+
keyboardRangeBaseRef.current = new Set(Array.from(keyboardRangeBaseRef.current).filter((id) => allowed.has(id)));
|
|
423
|
+
}
|
|
393
424
|
const pruned = new Set(Array.from(selectedIdsRef.current).filter((id) => allowed.has(id)));
|
|
394
425
|
if (!setsEqual(pruned, selectedIdsRef.current)) commitSelection(pruned, 'items-changed');
|
|
395
426
|
if (activeIdRef.current && !allowed.has(activeIdRef.current)) setActive(null, 'items-changed');
|
|
@@ -398,11 +429,39 @@ export function useCollectionInteractions<T>(
|
|
|
398
429
|
}
|
|
399
430
|
}, [commitSelection, selectableIds, setActive, setAnchor]);
|
|
400
431
|
|
|
432
|
+
const focusItem = useCallback((id: string) => {
|
|
433
|
+
const nodes = containerRef.current?.querySelectorAll<HTMLElement>(`[${COLLECTION_ITEM_ATTRIBUTE}]`);
|
|
434
|
+
const node = nodes ? Array.from(nodes).find(
|
|
435
|
+
(candidate) => candidate.getAttribute(COLLECTION_ITEM_ATTRIBUTE) === id && isInteractionElementVisible(candidate),
|
|
436
|
+
) : null;
|
|
437
|
+
node?.focus({ preventScroll: true });
|
|
438
|
+
node?.scrollIntoView?.({ block: 'nearest', inline: 'nearest' });
|
|
439
|
+
}, []);
|
|
440
|
+
|
|
441
|
+
const focusPointerItem = useCallback((id: string) => {
|
|
442
|
+
setActive(id, 'activate');
|
|
443
|
+
focusItem(id);
|
|
444
|
+
}, [focusItem, setActive]);
|
|
445
|
+
|
|
446
|
+
const selectPointerRange = useCallback((id: string) => {
|
|
447
|
+
const rangeAnchor = anchorIdRef.current ?? activeIdRef.current ?? id;
|
|
448
|
+
const ids = selectableIdsRef.current;
|
|
449
|
+
const from = ids.indexOf(rangeAnchor);
|
|
450
|
+
const to = ids.indexOf(id);
|
|
451
|
+
const range = new Set(ids.slice(Math.min(from, to), Math.max(from, to) + 1));
|
|
452
|
+
const base = keyboardRangeBaseRef.current ?? new Set(Array.from(selectedIdsRef.current).filter((selected) => !range.has(selected)));
|
|
453
|
+
keyboardRangeBaseRef.current = base;
|
|
454
|
+
setAnchor(rangeAnchor, 'range');
|
|
455
|
+
replaceRange(rangeAnchor, id, 'range');
|
|
456
|
+
focusPointerItem(id);
|
|
457
|
+
}, [focusPointerItem, replaceRange, setAnchor]);
|
|
458
|
+
|
|
401
459
|
const moveActive = useCallback((direction: CollectionNavigationDirection, extend = false) => {
|
|
402
460
|
const ids = selectableIdsRef.current;
|
|
403
461
|
if (ids.length === 0) return;
|
|
404
462
|
const current = activeIdRef.current;
|
|
405
|
-
|
|
463
|
+
// Range gestures follow the displayed order, independent of asymmetric grid geometry.
|
|
464
|
+
let next = extend ? null : resolveNextIdRef.current?.({
|
|
406
465
|
direction,
|
|
407
466
|
currentId: current,
|
|
408
467
|
items: itemsRef.current,
|
|
@@ -419,23 +478,21 @@ export function useCollectionInteractions<T>(
|
|
|
419
478
|
if (!next || next === current) return;
|
|
420
479
|
const stableAnchor = anchorIdRef.current ?? current ?? next;
|
|
421
480
|
if (extend) {
|
|
481
|
+
keyboardRangeBaseRef.current ??= new Set(
|
|
482
|
+
Array.from(selectedIdsRef.current).filter((id) => id !== stableAnchor),
|
|
483
|
+
);
|
|
422
484
|
if (!anchorIdRef.current) {
|
|
423
485
|
setAnchor(stableAnchor, 'range');
|
|
424
486
|
}
|
|
425
487
|
replaceRange(stableAnchor, next, 'range');
|
|
426
488
|
} else {
|
|
489
|
+
keyboardRangeBaseRef.current = new Set();
|
|
427
490
|
setAnchor(next, 'navigate');
|
|
428
491
|
}
|
|
429
492
|
setActive(next, extend ? 'range' : 'navigate');
|
|
430
493
|
const nextId = next;
|
|
431
|
-
requestAnimationFrame(() =>
|
|
432
|
-
|
|
433
|
-
const node = nodes ? Array.from(nodes).find(
|
|
434
|
-
(candidate) => candidate.getAttribute(COLLECTION_ITEM_ATTRIBUTE) === nextId,
|
|
435
|
-
) : null;
|
|
436
|
-
node?.focus({ preventScroll: true });
|
|
437
|
-
});
|
|
438
|
-
}, [replaceRange, setActive, setAnchor]);
|
|
494
|
+
requestAnimationFrame(() => focusItem(nextId));
|
|
495
|
+
}, [focusItem, replaceRange, setActive, setAnchor]);
|
|
439
496
|
|
|
440
497
|
const keyboard = useMemo<CollectionKeyboardShortcuts>(
|
|
441
498
|
() => ({ ...DEFAULT_SHORTCUTS, ...(shortcutOverrides || {}) }),
|
|
@@ -478,11 +535,29 @@ export function useCollectionInteractions<T>(
|
|
|
478
535
|
});
|
|
479
536
|
return definitions;
|
|
480
537
|
}, [activate, clear, keyboard, moveActive, selectAll, selectionMode, toggle]);
|
|
538
|
+
const isAvailable = useCallback(() => isInteractionElementVisible(containerRef.current), []);
|
|
539
|
+
useEffect(() => {
|
|
540
|
+
const owner = shortcutCollectionOwnerRef.current!;
|
|
541
|
+
const container = containerElement;
|
|
542
|
+
const Observer = container?.ownerDocument.defaultView?.MutationObserver;
|
|
543
|
+
const observer = Observer ? new Observer(() => {
|
|
544
|
+
if (!isAvailable()) releaseShortcutCollection(owner);
|
|
545
|
+
}) : null;
|
|
546
|
+
let element: Element | null = container;
|
|
547
|
+
while (element) {
|
|
548
|
+
observer?.observe(element, { attributes: true, attributeFilter: ['hidden', 'inert', 'aria-hidden', 'style', 'class'] });
|
|
549
|
+
const root: Node = element.getRootNode();
|
|
550
|
+
element = element.parentElement ?? ('host' in root ? (root as ShadowRoot).host : null);
|
|
551
|
+
}
|
|
552
|
+
return () => { observer?.disconnect(); releaseShortcutCollection(owner); };
|
|
553
|
+
}, [containerElement, isAvailable]);
|
|
554
|
+
|
|
481
555
|
useShortcuts(keyDefinitions, {
|
|
482
556
|
enabled: enabled && bindKeyboardShortcuts,
|
|
483
557
|
scope: shortcutScope,
|
|
484
558
|
priority: shortcutPriority,
|
|
485
559
|
collectionOwnerId: shortcutCollectionOwnerRef.current,
|
|
560
|
+
isAvailable,
|
|
486
561
|
});
|
|
487
562
|
|
|
488
563
|
const activateCollectionShortcuts = useCallback(() => {
|
|
@@ -604,23 +679,34 @@ export function useCollectionInteractions<T>(
|
|
|
604
679
|
event.preventDefault();
|
|
605
680
|
event.stopPropagation();
|
|
606
681
|
toggle(id);
|
|
682
|
+
focusPointerItem(id);
|
|
607
683
|
armSuppressNextClick();
|
|
608
|
-
}, [armSuppressNextClick, enabled, toggle]);
|
|
684
|
+
}, [armSuppressNextClick, enabled, focusPointerItem, toggle]);
|
|
609
685
|
|
|
610
686
|
const onCheckboxClick = useCallback((id: string) => (event: ReactMouseEvent) => {
|
|
611
687
|
event.stopPropagation();
|
|
688
|
+
if (!enabled) return;
|
|
612
689
|
if (event.shiftKey && selectionMode === 'multiple') {
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
690
|
+
selectPointerRange(id);
|
|
691
|
+
} else {
|
|
692
|
+
toggle(id);
|
|
693
|
+
focusPointerItem(id);
|
|
694
|
+
}
|
|
695
|
+
}, [enabled, focusPointerItem, selectPointerRange, selectionMode, toggle]);
|
|
617
696
|
|
|
618
697
|
const getRowProps = useCallback((id: string) => ({
|
|
619
698
|
[COLLECTION_ITEM_ATTRIBUTE]: id,
|
|
620
699
|
[LEGACY_ROW_ATTRIBUTE]: id,
|
|
621
700
|
}), []);
|
|
622
701
|
|
|
702
|
+
const longPress = useLongPressSelection((id) => {
|
|
703
|
+
if (!enabled || selectionMode === 'none') return;
|
|
704
|
+
activateCollectionShortcuts();
|
|
705
|
+
toggle(id);
|
|
706
|
+
focusPointerItem(id);
|
|
707
|
+
});
|
|
623
708
|
const getItemProps = useCallback((id: string) => ({
|
|
709
|
+
...(enableLongPressSelection ? longPress(id) : {}),
|
|
624
710
|
[COLLECTION_ITEM_ATTRIBUTE]: id,
|
|
625
711
|
[LEGACY_ROW_ATTRIBUTE]: id,
|
|
626
712
|
tabIndex: activeIdRef.current === id || (!activeIdRef.current && selectableIdsRef.current[0] === id) ? 0 : -1,
|
|
@@ -633,15 +719,14 @@ export function useCollectionInteractions<T>(
|
|
|
633
719
|
},
|
|
634
720
|
onMouseDown: onRowMouseDown(id),
|
|
635
721
|
onClick: (event: ReactMouseEvent) => {
|
|
636
|
-
if (event.defaultPrevented) return;
|
|
722
|
+
if (!enabled || event.defaultPrevented) return;
|
|
637
723
|
if (!selectableIdsRef.current.includes(id)) return;
|
|
638
724
|
const target = event.target as HTMLElement | null;
|
|
639
725
|
const interactiveTarget = target?.closest(INTERACTIVE_SELECTOR);
|
|
640
726
|
if (interactiveTarget && interactiveTarget !== event.currentTarget) return;
|
|
641
727
|
if (event.shiftKey && selectionMode === 'multiple') {
|
|
642
728
|
event.preventDefault();
|
|
643
|
-
|
|
644
|
-
selectRange(rangeAnchor, id);
|
|
729
|
+
selectPointerRange(id);
|
|
645
730
|
return;
|
|
646
731
|
}
|
|
647
732
|
if (event.metaKey || event.ctrlKey) return;
|
|
@@ -649,19 +734,19 @@ export function useCollectionInteractions<T>(
|
|
|
649
734
|
activate(id);
|
|
650
735
|
},
|
|
651
736
|
onKeyDown: (event: ReactKeyboardEvent) => {
|
|
652
|
-
if (event.key !== ' ' || event.metaKey || event.ctrlKey || event.altKey) return;
|
|
737
|
+
if (!enabled || event.key !== ' ' || event.metaKey || event.ctrlKey || event.altKey) return;
|
|
653
738
|
if (event.target !== event.currentTarget) return;
|
|
654
739
|
event.preventDefault();
|
|
655
740
|
if (selectionMode === 'none') activate(id);
|
|
656
741
|
else toggle(id);
|
|
657
742
|
},
|
|
658
|
-
}), [activate, activateCollectionShortcuts, clear, clearSelectionOnPlainClick, onRowMouseDown,
|
|
743
|
+
}), [activate, activateCollectionShortcuts, clear, clearSelectionOnPlainClick, enabled, enableLongPressSelection, longPress, onRowMouseDown, selectPointerRange, selectionMode, setActive, toggle]);
|
|
659
744
|
|
|
660
745
|
const getCheckboxProps = useCallback((id: string) => ({
|
|
661
746
|
isSelected: selectedIdsRef.current.has(id),
|
|
662
|
-
disabled: !selectableIdsRef.current.includes(id),
|
|
747
|
+
disabled: !enabled || !selectableIdsRef.current.includes(id),
|
|
663
748
|
onClick: onCheckboxClick(id),
|
|
664
|
-
}), [onCheckboxClick]);
|
|
749
|
+
}), [enabled, onCheckboxClick]);
|
|
665
750
|
|
|
666
751
|
const getSelectedItems = useCallback(() => getItemsForIds(selectedIdsRef.current), [getItemsForIds]);
|
|
667
752
|
const isSelected = useCallback((id: string) => selectedIds.has(id), [selectedIds]);
|
|
@@ -681,14 +766,38 @@ export function useCollectionInteractions<T>(
|
|
|
681
766
|
icon: action.icon,
|
|
682
767
|
shortcut,
|
|
683
768
|
destructive: action.destructive ?? action.intent === 'delete',
|
|
684
|
-
disabled,
|
|
769
|
+
disabled: !enabled || disabled,
|
|
685
770
|
pending: action.pending,
|
|
686
|
-
onRun: () => action.onRun(actionContext),
|
|
771
|
+
onRun: () => { if (!enabled || disabled || action.pending) return; return action.onRun(actionContext); },
|
|
687
772
|
};
|
|
688
|
-
}), [actionContext, actionDefinitions]);
|
|
773
|
+
}), [actionContext, actionDefinitions, enabled]);
|
|
774
|
+
|
|
775
|
+
// Optimistic filtering can remove the last selected row before a request settles.
|
|
776
|
+
// Keep pending keys on the visible controller, independently of toolbar mounting.
|
|
777
|
+
useShortcuts(resolvedActions.flatMap((action): ShortcutDefinition[] => (
|
|
778
|
+
action.shortcut && (action.pending || action.disabled)
|
|
779
|
+
? [{ id: `collection.pending.${action.id}`, keys: action.shortcut, allowRepeat: true, onTrigger: () => undefined }]
|
|
780
|
+
: []
|
|
781
|
+
)), {
|
|
782
|
+
enabled: bindKeyboardShortcuts && resolvedActions.some((action) => action.pending),
|
|
783
|
+
scope: shortcutScope,
|
|
784
|
+
priority: shortcutPriority + 25,
|
|
785
|
+
collectionOwnerId: shortcutCollectionOwnerRef.current!,
|
|
786
|
+
isAvailable,
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
const getActionBarProps = useCallback(() => ({
|
|
790
|
+
selectedCount: selectedIds.size,
|
|
791
|
+
actions: resolvedActions,
|
|
792
|
+
collectionOwnerId: shortcutCollectionOwnerRef.current!,
|
|
793
|
+
isAvailable,
|
|
794
|
+
// Pending work may disable collection gestures but must retain its action keys.
|
|
795
|
+
shortcutsEnabled: bindKeyboardShortcuts && (enabled || resolvedActions.some((action) => action.pending)),
|
|
796
|
+
onClearSelection: enabled ? clear : undefined,
|
|
797
|
+
}), [selectedIds.size, resolvedActions, isAvailable, enabled, bindKeyboardShortcuts, clear]);
|
|
689
798
|
|
|
690
799
|
const getContainerProps = useCallback(() => ({
|
|
691
|
-
ref:
|
|
800
|
+
ref: setContainerRef,
|
|
692
801
|
onMouseDown: (event: ReactMouseEvent) => {
|
|
693
802
|
activateCollectionShortcuts();
|
|
694
803
|
handleContainerMouseDown(event);
|
|
@@ -696,7 +805,7 @@ export function useCollectionInteractions<T>(
|
|
|
696
805
|
onFocusCapture: activateCollectionShortcuts,
|
|
697
806
|
onClickCapture: handleContainerClickCapture,
|
|
698
807
|
style: { userSelect: 'none' as const },
|
|
699
|
-
}), [activateCollectionShortcuts, handleContainerClickCapture, handleContainerMouseDown]);
|
|
808
|
+
}), [activateCollectionShortcuts, handleContainerClickCapture, handleContainerMouseDown, setContainerRef]);
|
|
700
809
|
|
|
701
810
|
return {
|
|
702
811
|
selectedIds,
|
|
@@ -707,6 +816,7 @@ export function useCollectionInteractions<T>(
|
|
|
707
816
|
lastClickedId: anchorId,
|
|
708
817
|
dragRect,
|
|
709
818
|
actions: resolvedActions,
|
|
819
|
+
getActionBarProps,
|
|
710
820
|
isSelected,
|
|
711
821
|
getSelectedItems,
|
|
712
822
|
setActive,
|
|
@@ -19,6 +19,8 @@ export interface UseDocumentsOptions {
|
|
|
19
19
|
offset?: number;
|
|
20
20
|
/** Fetch every page, starting at offset, instead of returning only one page. */
|
|
21
21
|
fetchAll?: boolean;
|
|
22
|
+
/** App-view lists can omit bodies until opening/searching a document. Default: true. */
|
|
23
|
+
includeContent?: boolean;
|
|
22
24
|
enabled?: boolean;
|
|
23
25
|
}
|
|
24
26
|
|
|
@@ -47,7 +49,7 @@ export function useDocuments(
|
|
|
47
49
|
): UseDocumentsResult {
|
|
48
50
|
const runtime = useNotisRuntime();
|
|
49
51
|
const query = useQuery<DocumentRecord[]>(
|
|
50
|
-
['documents', databaseSlug, options.filter ?? null, options.pageSize ?? null, options.offset ?? 0, Boolean(options.fetchAll)],
|
|
52
|
+
['documents', databaseSlug, options.filter ?? null, options.pageSize ?? null, options.offset ?? 0, Boolean(options.fetchAll), options.includeContent !== false],
|
|
51
53
|
async () => {
|
|
52
54
|
if (!runtime) throw new Error('Notis runtime not available');
|
|
53
55
|
const allDocuments: unknown[] = [];
|
|
@@ -58,6 +60,7 @@ export function useDocuments(
|
|
|
58
60
|
query: {
|
|
59
61
|
...(options.filter ?? {}),
|
|
60
62
|
...(options.pageSize !== undefined ? { page_size: options.pageSize } : {}),
|
|
63
|
+
...(options.includeContent === false ? { include_content: false } : {}),
|
|
61
64
|
},
|
|
62
65
|
...(offset > 0 ? { offset } : {}),
|
|
63
66
|
}, { dedupe: true, readOnly: true });
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
4
|
+
import type {
|
|
5
|
+
MouseEvent as ReactMouseEvent,
|
|
6
|
+
PointerEvent as ReactPointerEvent,
|
|
7
|
+
} from 'react';
|
|
8
|
+
|
|
9
|
+
const LONG_PRESS_MS = 500;
|
|
10
|
+
const MOVE_TOLERANCE_PX = 10;
|
|
11
|
+
const INTERACTIVE_SELECTOR = [
|
|
12
|
+
'button',
|
|
13
|
+
'a[href]',
|
|
14
|
+
'input',
|
|
15
|
+
'textarea',
|
|
16
|
+
'select',
|
|
17
|
+
'summary',
|
|
18
|
+
'[contenteditable]:not([contenteditable="false"])',
|
|
19
|
+
'[role="button"]',
|
|
20
|
+
'[role="link"]',
|
|
21
|
+
'[role="menuitem"]',
|
|
22
|
+
'[role="option"]',
|
|
23
|
+
'[role="switch"]',
|
|
24
|
+
'[role="tab"]',
|
|
25
|
+
].join(', ');
|
|
26
|
+
|
|
27
|
+
/** Adds a touch-only long-press entry point without rendering idle checkboxes. */
|
|
28
|
+
export function useLongPressSelection(onSelect: (id: string) => void) {
|
|
29
|
+
const onSelectRef = useRef(onSelect);
|
|
30
|
+
onSelectRef.current = onSelect;
|
|
31
|
+
const timerRef = useRef<number | null>(null);
|
|
32
|
+
const startRef = useRef<{ x: number; y: number } | null>(null);
|
|
33
|
+
const suppressClickRef = useRef(false);
|
|
34
|
+
|
|
35
|
+
const cancel = useCallback(() => {
|
|
36
|
+
if (timerRef.current !== null) window.clearTimeout(timerRef.current);
|
|
37
|
+
timerRef.current = null;
|
|
38
|
+
startRef.current = null;
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
useEffect(() => cancel, [cancel]);
|
|
42
|
+
|
|
43
|
+
return useCallback((id: string) => ({
|
|
44
|
+
onPointerDown: (event: ReactPointerEvent<HTMLElement>) => {
|
|
45
|
+
if (event.pointerType !== 'touch' || event.button !== 0) return;
|
|
46
|
+
const target = event.target;
|
|
47
|
+
if (
|
|
48
|
+
target instanceof HTMLElement
|
|
49
|
+
&& target !== event.currentTarget
|
|
50
|
+
&& target.closest(INTERACTIVE_SELECTOR)
|
|
51
|
+
) return;
|
|
52
|
+
cancel();
|
|
53
|
+
startRef.current = { x: event.clientX, y: event.clientY };
|
|
54
|
+
timerRef.current = window.setTimeout(() => {
|
|
55
|
+
timerRef.current = null;
|
|
56
|
+
startRef.current = null;
|
|
57
|
+
suppressClickRef.current = true;
|
|
58
|
+
onSelectRef.current(id);
|
|
59
|
+
}, LONG_PRESS_MS);
|
|
60
|
+
},
|
|
61
|
+
onPointerMove: (event: ReactPointerEvent<HTMLElement>) => {
|
|
62
|
+
const start = startRef.current;
|
|
63
|
+
if (!start) return;
|
|
64
|
+
if (
|
|
65
|
+
Math.abs(event.clientX - start.x) > MOVE_TOLERANCE_PX
|
|
66
|
+
|| Math.abs(event.clientY - start.y) > MOVE_TOLERANCE_PX
|
|
67
|
+
) cancel();
|
|
68
|
+
},
|
|
69
|
+
onPointerUp: cancel,
|
|
70
|
+
onPointerCancel: cancel,
|
|
71
|
+
onClickCapture: (event: ReactMouseEvent<HTMLElement>) => {
|
|
72
|
+
if (!suppressClickRef.current) return false;
|
|
73
|
+
suppressClickRef.current = false;
|
|
74
|
+
event.preventDefault();
|
|
75
|
+
event.stopPropagation();
|
|
76
|
+
return true;
|
|
77
|
+
},
|
|
78
|
+
}), [cancel]);
|
|
79
|
+
}
|
|
@@ -43,10 +43,7 @@ export interface MultiSelectController<T> {
|
|
|
43
43
|
onCheckboxClick: (id: string) => (event: ReactMouseEvent) => void;
|
|
44
44
|
onRowMouseDown: (id: string) => (event: ReactMouseEvent) => void;
|
|
45
45
|
getRowProps: (id: string) => { [ROW_ATTR]: string };
|
|
46
|
-
getItemProps:
|
|
47
|
-
[ROW_ATTR]: string;
|
|
48
|
-
onMouseDown: (event: ReactMouseEvent) => void;
|
|
49
|
-
};
|
|
46
|
+
getItemProps: CollectionInteractionController<T>['getItemProps'];
|
|
50
47
|
getCheckboxProps: (id: string) => {
|
|
51
48
|
isSelected: boolean;
|
|
52
49
|
onClick: (event: ReactMouseEvent) => void;
|
|
@@ -72,10 +69,7 @@ export function useMultiSelect<T>(options: UseMultiSelectOptions<T>): MultiSelec
|
|
|
72
69
|
});
|
|
73
70
|
|
|
74
71
|
const getRowProps = useCallback((id: string) => ({ [ROW_ATTR]: id }), []);
|
|
75
|
-
const getItemProps =
|
|
76
|
-
[ROW_ATTR]: id,
|
|
77
|
-
onMouseDown: controller.onRowMouseDown(id),
|
|
78
|
-
}), [controller.onRowMouseDown]);
|
|
72
|
+
const getItemProps = controller.getItemProps;
|
|
79
73
|
|
|
80
74
|
return {
|
|
81
75
|
selectedIds: controller.selectedIds,
|
|
@@ -151,3 +151,6 @@ export type {
|
|
|
151
151
|
export { useToolQuery } from './hooks/useToolQuery';
|
|
152
152
|
|
|
153
153
|
export { Skeleton, ViewSkeleton } from './components/Skeleton';
|
|
154
|
+
|
|
155
|
+
export { useLongPressSelection } from './hooks/useLongPressSelection';
|
|
156
|
+
export { isInteractionElementVisible } from './interactions/visibility';
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
|
|
3
|
-
export type CollectionActionIntent =
|
|
3
|
+
export type CollectionActionIntent =
|
|
4
|
+
| 'archive' | 'star' | 'delete'
|
|
5
|
+
| 'enable' | 'disable' | 'pause' | 'resume'
|
|
6
|
+
| 'move' | 'add-to-folder' | 'complete' | 'start-progress'
|
|
7
|
+
| 'custom';
|
|
4
8
|
|
|
5
9
|
export interface CollectionActionContext<T> {
|
|
6
10
|
selectedIds: string[];
|
|
@@ -13,6 +17,7 @@ export interface CollectionAction<T> {
|
|
|
13
17
|
intent?: CollectionActionIntent;
|
|
14
18
|
label?: string;
|
|
15
19
|
icon?: ReactNode;
|
|
20
|
+
/** Omit to use the intent default, supply a key to override, or false to remove it. */
|
|
16
21
|
shortcut?: string | false;
|
|
17
22
|
destructive?: boolean;
|
|
18
23
|
disabled?: boolean | ((context: CollectionActionContext<T>) => boolean);
|
|
@@ -35,6 +40,14 @@ const ACTION_DEFAULTS: Record<Exclude<CollectionActionIntent, 'custom'>, { label
|
|
|
35
40
|
archive: { label: 'Archive', shortcut: 'E' },
|
|
36
41
|
star: { label: 'Star', shortcut: 'S' },
|
|
37
42
|
delete: { label: 'Delete', shortcut: '#' },
|
|
43
|
+
enable: { label: 'Enable', shortcut: 'E' },
|
|
44
|
+
disable: { label: 'Disable', shortcut: 'D' },
|
|
45
|
+
pause: { label: 'Pause', shortcut: 'P' },
|
|
46
|
+
resume: { label: 'Resume', shortcut: 'R' },
|
|
47
|
+
move: { label: 'Move', shortcut: 'M' },
|
|
48
|
+
'add-to-folder': { label: 'Add to folder', shortcut: 'F' },
|
|
49
|
+
complete: { label: 'Mark complete', shortcut: 'C' },
|
|
50
|
+
'start-progress': { label: 'Start progress', shortcut: 'P' },
|
|
38
51
|
};
|
|
39
52
|
|
|
40
53
|
export function collectionActionDefaults(intent: CollectionActionIntent | undefined): {
|