@streamscloud/kit 0.49.0 → 0.51.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.
@@ -3,13 +3,13 @@ import { EmptyState } from '../empty-state';
3
3
  import { Icon } from '../icon';
4
4
  import { TableCell, TableHideableCell } from './table-cells';
5
5
  import { TableLocalization } from './table-localization';
6
- import { createTableView, cssWidth } from './table-view.svelte';
6
+ import { columnWidthStyle, createTableView } from './table-view.svelte';
7
7
  import IconReorderDotsHorizontal from '@fluentui/svg-icons/icons/re_order_dots_horizontal_20_regular.svg?raw';
8
8
  import { flip } from 'svelte/animate';
9
9
  import { dndzone } from 'svelte-dnd-action';
10
- let { model, showPlaceholder = false, showAlternativeView = false, on, alternativeView, placeholderRow, customNoItemsPlaceholder } = $props();
10
+ let { model, showAlternativeView = false, noItemsPlaceholder, on, alternativeView, customNoItemsPlaceholder } = $props();
11
11
  const localization = new TableLocalization();
12
- const view = createTableView(() => model, () => on);
12
+ const view = createTableView(() => model, () => on, () => noItemsPlaceholder);
13
13
  const flipDurationMs = 200;
14
14
  let tableRef = $state.raw(null);
15
15
  let dndItems = $state.raw(null);
@@ -51,18 +51,20 @@ const transformDraggedElement = (draggedRow) => {
51
51
  };
52
52
  </script>
53
53
 
54
+ {#snippet noItemsContent()}
55
+ {#if customNoItemsPlaceholder}
56
+ {@render customNoItemsPlaceholder()}
57
+ {:else}
58
+ <EmptyState graphic="zero-content" title={localization.noItemsFoundTitle} description={localization.noItemsFoundBody} />
59
+ {/if}
60
+ {/snippet}
61
+
54
62
  <div
55
63
  class="table-container table-container--{model.styles?.size ?? 'md'} table-container--{model.styles?.variant ?? 'plain'}"
56
64
  class:table-container--zebra={model.styles?.zebra === true}>
57
65
  {#if alternativeView && showAlternativeView}
58
- {#if showPlaceholder || view.showNoItemsPlaceholder}
59
- <div class="alternative-view__placeholder">
60
- {#if showPlaceholder}
61
- {@render placeholderRow?.()}
62
- {:else if customNoItemsPlaceholder}
63
- {@render customNoItemsPlaceholder()}
64
- {/if}
65
- </div>
66
+ {#if view.showNoItemsPlaceholder}
67
+ <div class="alternative-view__placeholder">{@render noItemsContent()}</div>
66
68
  {:else}
67
69
  {@render alternativeView()}
68
70
  {/if}
@@ -73,7 +75,7 @@ const transformDraggedElement = (draggedRow) => {
73
75
  <th class="table__th table__drag-handle">&nbsp;</th>
74
76
  {#each model.columns as column (column)}
75
77
  <TableHideableCell column={column}>
76
- <th class="table__th" class:table__th--centered={column.centered} style={cssWidth(column)} title={column.title}>
78
+ <th class="table__th" class:table__th--centered={column.centered} style={columnWidthStyle(column)} title={column.title}>
77
79
  <DynamicComponent model={column.headerView} />
78
80
  </th>
79
81
  </TableHideableCell>
@@ -91,18 +93,10 @@ const transformDraggedElement = (draggedRow) => {
91
93
  }}
92
94
  onconsider={handleConsider}
93
95
  onfinalize={handleFinalize}>
94
- {#if showPlaceholder || view.showNoItemsPlaceholder}
96
+ {#if view.showNoItemsPlaceholder}
95
97
  <tr class="table__tr table__tr--placeholder">
96
98
  <td colspan={visibleColspan}>
97
- <div class="table__placeholder">
98
- {#if showPlaceholder}
99
- {@render placeholderRow?.()}
100
- {:else if customNoItemsPlaceholder}
101
- {@render customNoItemsPlaceholder()}
102
- {:else}
103
- <EmptyState graphic="zero-content" title={localization.noItemsFoundTitle} description={localization.noItemsFoundBody} />
104
- {/if}
105
- </div>
99
+ <div class="table__placeholder">{@render noItemsContent()}</div>
106
100
  </td>
107
101
  </tr>
108
102
  {:else}
@@ -132,7 +126,7 @@ const transformDraggedElement = (draggedRow) => {
132
126
  class:table__td--centered={column.centered}
133
127
  class:table__td--clickable={!!column.onClick}
134
128
  class:table__td--flush={flushBottom}
135
- style={cssWidth(column)}
129
+ style={columnWidthStyle(column)}
136
130
  onclick={() => column.onClick && column.onClick(item)}>
137
131
  <TableCell model={model} item={item} column={column} itemIndex={itemIndex} on={{ itemsReordered: on?.itemsReordered }} />
138
132
  </td>
@@ -5,8 +5,14 @@ declare function $$render<T extends {
5
5
  }>(): {
6
6
  props: {
7
7
  model: TableModel<T>;
8
- showPlaceholder?: boolean;
9
8
  showAlternativeView?: boolean;
9
+ /**
10
+ * When an empty list turns into the no-items placeholder. A number delays it by that many ms (anti-flicker while the first page is in flight); a boolean
11
+ * decides outright — `true` shows it as soon as the list is empty, `false` never shows it (pair it with your own loading / empty UI). Rows always win:
12
+ * the placeholder is never rendered over a non-empty list.
13
+ * @default 700
14
+ */
15
+ noItemsPlaceholder?: number | boolean;
10
16
  on?: {
11
17
  selectionChanged?: (e: T[]) => void;
12
18
  itemsSelected?: (e: T[]) => void;
@@ -14,7 +20,6 @@ declare function $$render<T extends {
14
20
  itemsReordered?: (e: T[]) => void;
15
21
  };
16
22
  alternativeView?: Snippet;
17
- placeholderRow?: Snippet;
18
23
  customNoItemsPlaceholder?: Snippet;
19
24
  };
20
25
  exports: {};
@@ -2,10 +2,10 @@
2
2
  import { EmptyState } from '../empty-state';
3
3
  import { TableCell, TableHideableCell } from './table-cells';
4
4
  import { TableLocalization } from './table-localization';
5
- import { createTableView, cssWidth } from './table-view.svelte';
6
- let { model, showPlaceholder = false, showAlternativeView = false, grouping, on, alternativeView, placeholderRow, customNoItemsPlaceholder } = $props();
5
+ import { columnWidthStyle, createTableView } from './table-view.svelte';
6
+ let { model, showAlternativeView = false, noItemsPlaceholder, grouping, on, alternativeView, customNoItemsPlaceholder } = $props();
7
7
  const localization = new TableLocalization();
8
- const view = createTableView(() => model, () => on);
8
+ const view = createTableView(() => model, () => on, () => noItemsPlaceholder);
9
9
  const displayRows = $derived.by(() => {
10
10
  const items = model.items;
11
11
  const grp = grouping;
@@ -37,18 +37,20 @@ const displayRows = $derived.by(() => {
37
37
  const visibleColspan = $derived(model.columns.filter((column) => !column.hidden).length);
38
38
  </script>
39
39
 
40
+ {#snippet noItemsContent()}
41
+ {#if customNoItemsPlaceholder}
42
+ {@render customNoItemsPlaceholder()}
43
+ {:else}
44
+ <EmptyState graphic="zero-content" title={localization.noItemsFoundTitle} description={localization.noItemsFoundBody} />
45
+ {/if}
46
+ {/snippet}
47
+
40
48
  <div
41
49
  class="table-container table-container--{model.styles?.size ?? 'md'} table-container--{model.styles?.variant ?? 'plain'}"
42
50
  class:table-container--zebra={model.styles?.zebra === true}>
43
51
  {#if alternativeView && showAlternativeView}
44
- {#if showPlaceholder || view.showNoItemsPlaceholder}
45
- <div class="alternative-view__placeholder">
46
- {#if showPlaceholder}
47
- {@render placeholderRow?.()}
48
- {:else if customNoItemsPlaceholder}
49
- {@render customNoItemsPlaceholder()}
50
- {/if}
51
- </div>
52
+ {#if view.showNoItemsPlaceholder}
53
+ <div class="alternative-view__placeholder">{@render noItemsContent()}</div>
52
54
  {:else}
53
55
  {@render alternativeView()}
54
56
  {/if}
@@ -58,7 +60,7 @@ const visibleColspan = $derived(model.columns.filter((column) => !column.hidden)
58
60
  <tr>
59
61
  {#each model.columns as column (column)}
60
62
  <TableHideableCell column={column}>
61
- <th class="table__th" class:table__th--centered={column.centered} style={cssWidth(column)} title={column.title}>
63
+ <th class="table__th" class:table__th--centered={column.centered} style={columnWidthStyle(column)} title={column.title}>
62
64
  <DynamicComponent model={column.headerView} />
63
65
  </th>
64
66
  </TableHideableCell>
@@ -66,18 +68,10 @@ const visibleColspan = $derived(model.columns.filter((column) => !column.hidden)
66
68
  </tr>
67
69
  </thead>
68
70
  <tbody>
69
- {#if showPlaceholder || view.showNoItemsPlaceholder}
71
+ {#if view.showNoItemsPlaceholder}
70
72
  <tr class="table__tr table__tr--placeholder">
71
73
  <td colspan={visibleColspan}>
72
- <div class="table__placeholder">
73
- {#if showPlaceholder}
74
- {@render placeholderRow?.()}
75
- {:else if customNoItemsPlaceholder}
76
- {@render customNoItemsPlaceholder()}
77
- {:else}
78
- <EmptyState graphic="zero-content" title={localization.noItemsFoundTitle} description={localization.noItemsFoundBody} />
79
- {/if}
80
- </div>
74
+ <div class="table__placeholder">{@render noItemsContent()}</div>
81
75
  </td>
82
76
  </tr>
83
77
  {:else}
@@ -112,7 +106,7 @@ const visibleColspan = $derived(model.columns.filter((column) => !column.hidden)
112
106
  class:table__td--centered={column.centered}
113
107
  class:table__td--clickable={!!column.onClick}
114
108
  class:table__td--flush={flushBottom}
115
- style={cssWidth(column)}
109
+ style={columnWidthStyle(column)}
116
110
  onclick={() => column.onClick && column.onClick(item)}>
117
111
  <TableCell model={model} item={item} column={column} itemIndex={itemIndex} on={{ itemsReordered: on?.itemsReordered }} />
118
112
  </td>
@@ -5,8 +5,14 @@ declare function $$render<T extends {
5
5
  }>(): {
6
6
  props: {
7
7
  model: TableModel<T>;
8
- showPlaceholder?: boolean;
9
8
  showAlternativeView?: boolean;
9
+ /**
10
+ * When an empty list turns into the no-items placeholder. A number delays it by that many ms (anti-flicker while the first page is in flight); a boolean
11
+ * decides outright — `true` shows it as soon as the list is empty, `false` never shows it (pair it with your own loading / empty UI). Rows always win:
12
+ * the placeholder is never rendered over a non-empty list.
13
+ * @default 700
14
+ */
15
+ noItemsPlaceholder?: number | boolean;
10
16
  /** Group consecutive (already-sorted) rows under section headers. `key` returns a group id per item (`null` = ungrouped); a header row renders wherever the key changes between neighbours. */
11
17
  grouping?: {
12
18
  key: (item: T) => string | null;
@@ -26,7 +32,6 @@ declare function $$render<T extends {
26
32
  itemsReordered?: (e: T[]) => void;
27
33
  };
28
34
  alternativeView?: Snippet;
29
- placeholderRow?: Snippet;
30
35
  customNoItemsPlaceholder?: Snippet;
31
36
  };
32
37
  exports: {};
@@ -24,7 +24,8 @@ export declare class ResolvedColumn<T extends WithId> {
24
24
  get id(): keyof T;
25
25
  get title(): string;
26
26
  get centered(): boolean;
27
- get fixedCssWidth(): string;
27
+ get cssWidth(): string;
28
+ get minCssWidth(): string;
28
29
  get hideable(): boolean;
29
30
  get sortLabelsOverride(): {
30
31
  asc: string;
@@ -31,8 +31,11 @@ export class ResolvedColumn {
31
31
  get centered() {
32
32
  return this.def.centered ?? false;
33
33
  }
34
- get fixedCssWidth() {
35
- return this.def.fixedCssWidth ?? '';
34
+ get cssWidth() {
35
+ return this.def.cssWidth ?? '';
36
+ }
37
+ get minCssWidth() {
38
+ return this.def.minCssWidth ?? '';
36
39
  }
37
40
  get hideable() {
38
41
  if ('hideable' in this.def) {
@@ -113,7 +113,7 @@ export class TableModel {
113
113
  }
114
114
  // Append actions column if needed
115
115
  if (this._itemActions.length) {
116
- const actionsColumn = new ResolvedColumn({ type: 'actions', centered: true, fixedCssWidth: Css.em(75) });
116
+ const actionsColumn = new ResolvedColumn({ type: 'actions', centered: true, cssWidth: Css.em(75) });
117
117
  actionsColumn.headerView = new DynamicComponentModel({
118
118
  component: TableHeader,
119
119
  props: { column: { title: 'Actions' } }
@@ -151,7 +151,7 @@ export class TableModel {
151
151
  const resolved = new ResolvedColumn({
152
152
  ...column,
153
153
  centered: true,
154
- fixedCssWidth: column.fixedCssWidth || Css.em(50)
154
+ cssWidth: column.cssWidth || Css.em(50)
155
155
  });
156
156
  resolved.headerView = new DynamicComponentModel({
157
157
  component: TableCheckboxHeader,
@@ -163,7 +163,7 @@ export class TableModel {
163
163
  const resolved = new ResolvedColumn({
164
164
  ...column,
165
165
  centered: true,
166
- fixedCssWidth: column.fixedCssWidth || Css.em(75)
166
+ cssWidth: column.cssWidth || Css.em(75)
167
167
  });
168
168
  resolved.headerView = resolveSortableHeader(resolved);
169
169
  return resolved;
@@ -6,9 +6,10 @@ type SelectionCallbacks<T> = {
6
6
  itemsSelected?: (e: T[]) => void;
7
7
  itemsDeselected?: (e: T[]) => void;
8
8
  };
9
- export declare const cssWidth: <T extends WithId>(column: ResolvedColumn<T>) => string;
10
- /** Shared presentation wiring for the table views: selection-event subscriptions + the debounced no-items placeholder. */
11
- export declare const createTableView: <T extends WithId>(model: () => TableModel<T>, on: () => SelectionCallbacks<T> | undefined) => {
9
+ /** Inline width declarations for a column's `th` / `td`: the preferred width plus, when set, the floor the browser must honour. */
10
+ export declare const columnWidthStyle: <T extends WithId>(column: ResolvedColumn<T>) => string;
11
+ /** Shared presentation wiring for the table views: selection-event subscriptions + the no-items placeholder decision (`number` = delay in ms, boolean = explicit). */
12
+ export declare const createTableView: <T extends WithId>(model: () => TableModel<T>, on: () => SelectionCallbacks<T> | undefined, noItemsPlaceholder?: () => number | boolean | undefined) => {
12
13
  readonly showNoItemsPlaceholder: boolean;
13
14
  };
14
15
  export {};
@@ -1,7 +1,12 @@
1
1
  import { untrack } from 'svelte';
2
- export const cssWidth = (column) => (column.fixedCssWidth ? `width: ${column.fixedCssWidth};` : 'width: auto;');
3
- /** Shared presentation wiring for the table views: selection-event subscriptions + the debounced no-items placeholder. */
4
- export const createTableView = (model, on) => {
2
+ /** Inline width declarations for a column's `th` / `td`: the preferred width plus, when set, the floor the browser must honour. */
3
+ export const columnWidthStyle = (column) => {
4
+ const width = column.cssWidth ? `width: ${column.cssWidth};` : 'width: auto;';
5
+ return column.minCssWidth ? `${width} min-width: ${column.minCssWidth};` : width;
6
+ };
7
+ const NO_ITEMS_PLACEHOLDER_DELAY_MS = 700;
8
+ /** Shared presentation wiring for the table views: selection-event subscriptions + the no-items placeholder decision (`number` = delay in ms, boolean = explicit). */
9
+ export const createTableView = (model, on, noItemsPlaceholder = () => undefined) => {
5
10
  let showNoItemsPlaceholder = $state(false);
6
11
  let timeout = 0;
7
12
  $effect(() => untrack(() => {
@@ -14,14 +19,20 @@ export const createTableView = (model, on) => {
14
19
  return () => subscriptions.forEach((unsubscribe) => unsubscribe());
15
20
  }));
16
21
  $effect(() => {
22
+ const decision = noItemsPlaceholder() ?? NO_ITEMS_PLACEHOLDER_DELAY_MS;
17
23
  if (model().items.length) {
18
24
  clearTimeout(timeout);
19
25
  showNoItemsPlaceholder = false;
20
26
  return;
21
27
  }
28
+ if (typeof decision === 'boolean') {
29
+ clearTimeout(timeout);
30
+ showNoItemsPlaceholder = decision;
31
+ return;
32
+ }
22
33
  timeout = window.setTimeout(() => {
23
34
  showNoItemsPlaceholder = !model().items.length;
24
- }, 700);
35
+ }, decision);
25
36
  return () => clearTimeout(timeout);
26
37
  });
27
38
  return {
@@ -11,7 +11,13 @@ export type IOneOfTableColumn<T> = ITableBadgeColumn<T> | ITableButtonColumn<T>
11
11
  export type TableColumnType = 'actions' | 'badge' | 'button' | 'by' | 'checkbox' | 'icon' | 'image' | 'move-row' | 'snippet' | 'text';
12
12
  interface ITableColumnBase {
13
13
  type: TableColumnType;
14
- fixedCssWidth?: string;
14
+ /**
15
+ * Preferred column width. Under the table's `table-layout: auto` this is a suggestion: content can push a column wider, and a column with no width of its
16
+ * own is left with whatever the others do not claim. Use `minCssWidth` for a width the browser must honour.
17
+ */
18
+ cssWidth?: string;
19
+ /** Floor the column will not shrink below, taken out of columns whose `cssWidth` is merely a suggestion. Use it on the column that must keep space. */
20
+ minCssWidth?: string;
15
21
  centered?: boolean;
16
22
  }
17
23
  export interface ITableColumn<T> extends ITableColumnBase {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamscloud/kit",
3
- "version": "0.49.0",
3
+ "version": "0.51.0",
4
4
  "author": "StreamsCloud",
5
5
  "repository": {
6
6
  "type": "git",