@streamscloud/kit 0.68.1 → 0.70.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.
@@ -1,11 +1,21 @@
1
1
  <script lang="ts" generics="T">import { DragAndDrop } from './drag-and-drop.svelte';
2
+ import { onDestroy } from 'svelte';
2
3
  const { dragEventModel, children } = $props();
4
+ let startedEvent = null;
3
5
  const onDragStart = () => {
4
- DragAndDrop.event = dragEventModel;
6
+ startedEvent = dragEventModel;
7
+ DragAndDrop.event = startedEvent;
5
8
  };
6
9
  const onDragEnd = () => {
10
+ startedEvent = null;
7
11
  DragAndDrop.event = null;
8
12
  };
13
+ // A source detached mid-drag never receives `dragend`, which would leave the store set forever.
14
+ onDestroy(() => {
15
+ if (startedEvent && DragAndDrop.event === startedEvent) {
16
+ DragAndDrop.event = null;
17
+ }
18
+ });
9
19
  </script>
10
20
 
11
21
  <div class="drag-source" draggable="true" ondragstart={onDragStart} ondragend={onDragEnd} role="none">
@@ -16,7 +26,7 @@ const onDragEnd = () => {
16
26
  @component
17
27
  Wraps arbitrary content and makes it the source of a kit drag-and-drop session. On `dragstart`
18
28
  the supplied `dragEventModel` is written to the `DragAndDrop` singleton store; on `dragend`
19
- it's cleared. Pair with one or more `DropZone`s elsewhere in the tree — zones whose
29
+ it's cleared (also when the source unmounts mid-drag). Pair with one or more `DropZone`s elsewhere in the tree — zones whose
20
30
  `acceptedEvents` includes the model's `type` will activate while the drag is in progress.
21
31
 
22
32
  The wrapper takes 100% × 100% of its slot with `cursor: grab` so it overlays the host's
@@ -1,5 +1,5 @@
1
1
  import type { DragEventModel } from './types';
2
- import type { Snippet } from 'svelte';
2
+ import { type Snippet } from 'svelte';
3
3
  declare function $$render<T>(): {
4
4
  props: {
5
5
  /** Payload to write into the shared `DragAndDrop` store on `dragstart`. */
@@ -28,7 +28,7 @@ interface $$IsomorphicComponent {
28
28
  /**
29
29
  * Wraps arbitrary content and makes it the source of a kit drag-and-drop session. On `dragstart`
30
30
  * the supplied `dragEventModel` is written to the `DragAndDrop` singleton store; on `dragend`
31
- * it's cleared. Pair with one or more `DropZone`s elsewhere in the tree — zones whose
31
+ * it's cleared (also when the source unmounts mid-drag). Pair with one or more `DropZone`s elsewhere in the tree — zones whose
32
32
  * `acceptedEvents` includes the model's `type` will activate while the drag is in progress.
33
33
  *
34
34
  * The wrapper takes 100% × 100% of its slot with `cursor: grab` so it overlays the host's
@@ -3,11 +3,11 @@ import { PageLayoutLocalization } from './page-layout-localization';
3
3
  import IconDismiss from '@fluentui/svg-icons/icons/dismiss_20_regular.svg?raw';
4
4
  import IconWindow from '@fluentui/svg-icons/icons/window_20_regular.svg?raw';
5
5
  import IconWindowMultiple from '@fluentui/svg-icons/icons/window_multiple_20_regular.svg?raw';
6
- const { children, title, actions, navigationToggle, editorControls, scrollable = false } = $props();
6
+ const { children, title, actions, navigationToggle, editorControls, scrollable = false, inert = false } = $props();
7
7
  const localization = new PageLayoutLocalization();
8
8
  </script>
9
9
 
10
- <div class="page-layout" class:page-layout--scrollable={scrollable}>
10
+ <div class="page-layout" class:page-layout--scrollable={scrollable} inert={inert}>
11
11
  <div class="page-layout__header">
12
12
  <div class="page-layout__title">
13
13
  {#if typeof title === 'function'}
@@ -9,6 +9,8 @@ type Props = {
9
9
  /** Editor chrome — the fixed panels-toggle + close pair. Set it to render both buttons; omit it entirely for a non-editor page. */
10
10
  editorControls?: EditorControls;
11
11
  scrollable?: boolean;
12
+ /** Sets `inert` on the whole layout — header and body stop taking focus, clicks and assistive-tech interaction. */
13
+ inert?: boolean;
12
14
  };
13
15
  /**
14
16
  * PageLayout — top-level window chrome. Renders a fixed-height header (title + actions + an optional
@@ -19,7 +19,7 @@ const handleConsider = (e) => {
19
19
  dndItems = e.detail.items;
20
20
  };
21
21
  const handleFinalize = (e) => {
22
- model.reorderItems(e.detail.items);
22
+ model.items = e.detail.items;
23
23
  on?.itemsReordered?.(e.detail.items);
24
24
  dndItems = null;
25
25
  };
@@ -24,7 +24,7 @@ const changeItemPosition = (target, positionChange) => {
24
24
  const newItems = [...model.items];
25
25
  newItems.splice(index, 1);
26
26
  newItems.splice(newIndex, 0, target);
27
- model.reorderItems(newItems);
27
+ model.items = newItems;
28
28
  on?.itemsReordered?.(newItems);
29
29
  };
30
30
  </script>
@@ -18,7 +18,6 @@ export declare class TableModel<T extends WithId> {
18
18
  private _itemGroupActions;
19
19
  private _styles;
20
20
  private _selection;
21
- private _preserveSelection;
22
21
  private _dataProvider;
23
22
  private _highlightedItemIds;
24
23
  constructor(init: TableModelInit<T>);
@@ -29,14 +28,14 @@ export declare class TableModel<T extends WithId> {
29
28
  get styles(): Readonly<TableStyles>;
30
29
  get selection(): Repository<T>;
31
30
  get highlightedItemIds(): string[];
31
+ /** Keeps the selection a subset of `value`: selected ids pick up their new objects, ids no longer present are deselected. Reset it with `selection.clear()`. */
32
32
  set items(value: T[]);
33
33
  set itemActions(value: TableItemAction<T>[]);
34
34
  set columns(value: IAnyTableColumn<T>[]);
35
35
  set highlightedItemIds(value: string[]);
36
- /** Reassign items as a pure reorder of the same set — preserves the current selection (unlike the `items` setter). */
37
- reorderItems: (value: T[]) => void;
38
36
  updateStyles: (styles: Partial<TableStyles>) => void;
39
37
  setResolvedColumns: (columns: ResolvedColumn<T>[]) => void;
38
+ private reconcileSelection;
40
39
  private reconcileColumns;
41
40
  private mapColumn;
42
41
  }
@@ -12,13 +12,11 @@ export class TableModel {
12
12
  _itemGroupActions = $state.raw([]);
13
13
  _styles = $state({ stickyHeader: true });
14
14
  _selection;
15
- _preserveSelection = false;
16
15
  _dataProvider;
17
16
  _highlightedItemIds = $state.raw([]);
18
17
  constructor(init) {
19
18
  this._init = init;
20
19
  this._selection = init.selectionSettings?.repository ?? createRepository();
21
- this._preserveSelection = init.selectionSettings?.preserveSelection || false;
22
20
  this._dataProvider = createRepository();
23
21
  this._itemActions = init.itemActions || [];
24
22
  this._itemGroupActions = init.itemGroupActions || [];
@@ -65,12 +63,11 @@ export class TableModel {
65
63
  get highlightedItemIds() {
66
64
  return this._highlightedItemIds;
67
65
  }
66
+ /** Keeps the selection a subset of `value`: selected ids pick up their new objects, ids no longer present are deselected. Reset it with `selection.clear()`. */
68
67
  set items(value) {
69
68
  this._items = value;
70
69
  this._dataProvider.set(...value);
71
- if (!this._preserveSelection) {
72
- this._selection.clear();
73
- }
70
+ untrack(() => this.reconcileSelection(value));
74
71
  }
75
72
  set itemActions(value) {
76
73
  this._itemActions = value;
@@ -81,17 +78,27 @@ export class TableModel {
81
78
  set highlightedItemIds(value) {
82
79
  this._highlightedItemIds = value;
83
80
  }
84
- /** Reassign items as a pure reorder of the same set — preserves the current selection (unlike the `items` setter). */
85
- reorderItems = (value) => {
86
- this._items = value;
87
- this._dataProvider.set(...value);
88
- };
89
81
  updateStyles = (styles) => {
90
82
  Object.assign(this._styles, styles);
91
83
  };
92
84
  setResolvedColumns = (columns) => {
93
85
  this._columns = columns;
94
86
  };
87
+ reconcileSelection = (items) => {
88
+ const selected = this._selection.items;
89
+ if (!selected.length) {
90
+ return;
91
+ }
92
+ const itemsById = new Map(items.map((item) => [item.id, item]));
93
+ const gone = selected.filter((item) => !itemsById.has(item.id));
94
+ if (gone.length) {
95
+ this._selection.remove(...gone);
96
+ }
97
+ const kept = selected.filter((item) => itemsById.has(item.id));
98
+ if (kept.some((item) => itemsById.get(item.id) !== item)) {
99
+ this._selection.set(...kept.map((item) => itemsById.get(item.id) ?? item));
100
+ }
101
+ };
95
102
  reconcileColumns = (newDefs) => {
96
103
  // Save state from current columns
97
104
  const stateMap = new Map();
@@ -135,7 +135,6 @@ export type PageQuery<TOrder> = {
135
135
  perPage: number;
136
136
  };
137
137
  export type SelectionSettings<T extends WithId> = {
138
- preserveSelection?: boolean;
139
138
  repository?: Repository<T>;
140
139
  };
141
140
  export type TableColumnSortDirection = 'none' | 'asc' | 'desc';
@@ -12,7 +12,7 @@ import {} from './tree-node.svelte';
12
12
  import IconCheckmark from '@fluentui/svg-icons/icons/checkmark_20_regular.svg?raw';
13
13
  import IconChevronRight from '@fluentui/svg-icons/icons/chevron_right_20_regular.svg?raw';
14
14
  import IconMoreVertical from '@fluentui/svg-icons/icons/more_vertical_20_regular.svg?raw';
15
- import { untrack } from 'svelte';
15
+ import { onDestroy, untrack } from 'svelte';
16
16
  import { slide } from 'svelte/transition';
17
17
  let { node, keyboard, checked, posInSet, setSize, hideExpandIcon = false, level = 0 } = $props();
18
18
  const localization = new TreeLocalization();
@@ -22,6 +22,7 @@ let renameDraft = $state('');
22
22
  let hoveredNode = null;
23
23
  let leafElement = $state();
24
24
  let dbClickTimeout = undefined;
25
+ let startedDrag = null;
25
26
  const children = $derived(node.children);
26
27
  const visibleActions = $derived(node.actions.filter((a) => !a.visible || a.visible(node)));
27
28
  const actionsShown = $derived(!node.renaming && visibleActions.length > 0);
@@ -63,15 +64,23 @@ const handleDragStart = (e) => {
63
64
  return;
64
65
  }
65
66
  e.stopPropagation();
66
- DragAndDrop.event = node.drag;
67
+ startedDrag = node.drag;
68
+ DragAndDrop.event = startedDrag;
67
69
  if (e.dataTransfer) {
68
70
  e.dataTransfer.effectAllowed = 'move';
69
71
  e.dataTransfer.setData('application/x-sc-tree-node', node.id);
70
72
  }
71
73
  };
72
74
  const handleDragEnd = () => {
75
+ startedDrag = null;
73
76
  DragAndDrop.event = null;
74
77
  };
78
+ // A row detached mid-drag never receives `dragend`, which would leave the store set forever.
79
+ onDestroy(() => {
80
+ if (startedDrag && DragAndDrop.event === startedDrag) {
81
+ DragAndDrop.event = null;
82
+ }
83
+ });
75
84
  const handleDragEnter = () => {
76
85
  if (acceptsCurrentDrag && leafElement) {
77
86
  TreeDropTarget.claim(node, leafElement);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamscloud/kit",
3
- "version": "0.68.1",
3
+ "version": "0.70.0",
4
4
  "author": "StreamsCloud",
5
5
  "repository": {
6
6
  "type": "git",