@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.
Files changed (42) hide show
  1. package/dist/agent-hooks/notis-agent-hook.mjs +8296 -7912
  2. package/dist/base-skills/notis-apps/SKILL.md +34 -513
  3. package/dist/base-skills/notis-apps/references/architecture.md +164 -0
  4. package/dist/base-skills/notis-apps/references/design.md +165 -0
  5. package/dist/base-skills/notis-apps/references/release.md +99 -0
  6. package/dist/base-skills/notis-apps/references/sdk.md +61 -0
  7. package/dist/base-skills/notis-apps/references/troubleshooting.md +23 -0
  8. package/dist/base-skills/notis-cli/SKILL.md +19 -200
  9. package/dist/base-skills/notis-cli/references/app-delivery.md +18 -0
  10. package/dist/base-skills/notis-cli/references/native-databases.md +20 -0
  11. package/dist/base-skills/notis-cli/references/tool-examples.md +56 -0
  12. package/dist/base-skills/notis-cli/references/troubleshooting.md +39 -0
  13. package/dist/base-skills/notis-query/SKILL.md +13 -651
  14. package/dist/base-skills/notis-query/references/database-discovery.md +59 -0
  15. package/dist/base-skills/notis-query/references/documents.md +50 -0
  16. package/dist/base-skills/notis-query/references/query.md +543 -0
  17. package/dist/skill-sync/index.js +24 -7
  18. package/dist/skill-sync/index.js.map +4 -4
  19. package/dist/skill-sync-worker.mjs +2989 -0
  20. package/package.json +1 -1
  21. package/src/cli.js +4 -0
  22. package/src/command-specs/diagnostics.js +37 -0
  23. package/src/command-specs/skills.js +23 -5
  24. package/src/runtime/profiles.js +5 -2
  25. package/src/runtime/skill-sync/cloud-client.ts +2 -1
  26. package/src/runtime/skill-sync/index.ts +24 -6
  27. package/src/runtime/skill-sync/types.ts +2 -0
  28. package/src/runtime/skill-sync-service.js +109 -0
  29. package/src/skill-sync-worker-entry.js +2 -0
  30. package/src/skill-sync-worker.js +50 -0
  31. package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +36 -7
  32. package/template/packages/sdk/src/components/MultiSelectCheckbox.tsx +3 -1
  33. package/template/packages/sdk/src/hooks/useCollectionInteractions.ts +138 -28
  34. package/template/packages/sdk/src/hooks/useDocuments.ts +4 -1
  35. package/template/packages/sdk/src/hooks/useLongPressSelection.ts +79 -0
  36. package/template/packages/sdk/src/hooks/useMultiSelect.ts +2 -8
  37. package/template/packages/sdk/src/index.ts +3 -0
  38. package/template/packages/sdk/src/interactions/actions.ts +14 -1
  39. package/template/packages/sdk/src/interactions/shortcuts.tsx +79 -19
  40. package/template/packages/sdk/src/interactions/visibility.ts +13 -0
  41. package/template/packages/sdk/src/interactions.ts +3 -0
  42. 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(ids.slice(Math.min(from, to), Math.max(from, to) + 1)), reason);
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
- let next = resolveNextIdRef.current?.({
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
- const nodes = containerRef.current?.querySelectorAll<HTMLElement>(`[${COLLECTION_ITEM_ATTRIBUTE}]`);
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
- const rangeAnchor = anchorIdRef.current ?? activeIdRef.current ?? id;
614
- selectRange(rangeAnchor, id);
615
- } else toggle(id);
616
- }, [selectRange, selectionMode, toggle]);
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
- const rangeAnchor = anchorIdRef.current ?? activeIdRef.current ?? id;
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, selectRange, selectionMode, setActive, toggle]);
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: (node: HTMLElement | null) => { containerRef.current = node; },
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: (id: string) => {
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 = useCallback((id: string) => ({
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 = 'archive' | 'star' | 'delete' | 'custom';
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): {