@lavalogic/scoria 0.37.19 → 0.37.21

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.
@@ -724,10 +724,11 @@
724
724
  div.input-cell {
725
725
  background-color: #f4f7f9;
726
726
  height: var(--height);
727
- overflow: visible;
727
+ overflow-x: clip;
728
+ overflow-y: visible;
728
729
  padding-left: 0;
729
730
  padding-right: 0;
730
- border-right: solid 1px #cbd4da !important;
731
+ border-right: none;
731
732
  box-sizing: border-box;
732
733
  }
733
734
  div.input-cell div.stick-right {
@@ -7,7 +7,6 @@
7
7
  import Action from '../../../Action.svelte';
8
8
  import Checkbox from '../../../Checkbox.svelte';
9
9
  import ThreeStateRadio from '../../../ThreeStateRadio.svelte';
10
- import { ColumnType } from '../../Types/Columns/ColumnType.js';
11
10
  import { TableContext } from '../../Types/Context/TableContext.svelte.js';
12
11
  import type { Primitive } from '../../Types/Context/TableInitOptions.js';
13
12
  import { Size } from '../../../../Types/Internal/Size.js';
@@ -18,7 +17,6 @@
18
17
 
19
18
  const { columnDef }: ColumnListRowProps<T> = $props();
20
19
 
21
- const isExpandColumn = $derived(columnDef?.columnType === ColumnType.Expand);
22
20
 
23
21
  /**
24
22
  * Position of this column inside the sorted column list, used to
@@ -412,13 +410,9 @@
412
410
  Expand columns can never be pinned, so the group goes
413
411
  fully disabled for them.
414
412
  -->
415
- <div
416
- class="pin-selector"
417
- class:disabled={isExpandColumn}
418
- >
413
+ <div class="pin-selector">
419
414
  <ThreeStateRadio
420
415
  value={pinSide}
421
- disabled={isExpandColumn}
422
416
  noLabel="Pin to left"
423
417
  neutralLabel="Unpinned"
424
418
  yesLabel="Pin to right"
@@ -115,11 +115,24 @@
115
115
  tableContext.endFocus(focusDetails.coordinates);
116
116
  }}
117
117
  >
118
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
118
119
  <div
119
120
  class="slot-wrapper"
120
121
  class:multifocus={focusDetails.isMultiFocused}
121
122
  class:multifocus-start={focusDetails.isMultiFocusStart}
122
123
  bind:this={cellRef}
124
+ onmousedown={(e) => {
125
+ // Stop the browser's default mousedown -> focus side effect
126
+ // on the inner Action button. The default focus call
127
+ // implicitly calls `scrollIntoView` against the button (which
128
+ // sits inside a sticky right-pinned cell whose layout position
129
+ // is at the far right end of the row), causing the table to
130
+ // jump horizontally on every click of the ellipsis menu. We
131
+ // still want focus / activation behaviour, so the click handler
132
+ // on the Action proper handles the popup toggle and the outer
133
+ // `.focusable` cell handler manages the focus state.
134
+ e.preventDefault();
135
+ }}
123
136
  >
124
137
  <!-- TODO {isMultiFocusStart} -->
125
138
  <Action
@@ -83,11 +83,17 @@
83
83
  >
84
84
  <div class="bubble-parent">
85
85
  {#await values then values}
86
- <!-- The label string itself is not guaranteed unique so the key combines
87
- index and value; both pieces are required because duplicate labels
88
- would otherwise collapse and reordering with identical labels
89
- would force unrelated bubbles to swap. -->
90
- {#each values as value, index (`${index}:${value}`)}
86
+ <!-- `maxBubbles` (default 3) caps how many pills render inline;
87
+ anything beyond gets a single `"+N more"` overflow chip with
88
+ the hidden labels as a hover tooltip. Key combines index and
89
+ value because duplicate labels would otherwise collapse and
90
+ reordering with identical labels would force unrelated
91
+ bubbles to swap. -->
92
+ {@const maxBubbles = (cell.column.columnDef as BubbleDef<T>).maxBubbles ?? 3}
93
+ {@const visibleBubbles = values.slice(0, maxBubbles)}
94
+ {@const overflowCount = Math.max(0, values.length - maxBubbles)}
95
+ {@const overflowTitle = values.slice(maxBubbles).join(', ')}
96
+ {#each visibleBubbles as value, index (`${index}:${value}`)}
91
97
  {@const colourPromise = (cell.column.columnDef as BubbleDef<T>).getColour?.(
92
98
  cell.row.original,
93
99
  cell.row.originalIndex,
@@ -105,6 +111,12 @@
105
111
  {/await}
106
112
  {/if}
107
113
  {/each}
114
+ {#if overflowCount > 0}
115
+ <span
116
+ class="bubble-overflow"
117
+ title={overflowTitle}
118
+ >+{overflowCount} more</span>
119
+ {/if}
108
120
  {/await}
109
121
  </div>
110
122
  </div>
@@ -186,7 +198,7 @@
186
198
 
187
199
  div.bubble-parent {
188
200
  display: flex;
189
- flex-flow: row wrap;
201
+ flex-flow: row nowrap;
190
202
  max-width: 100%;
191
203
  height: 100%;
192
204
  overflow-x: clip;
@@ -194,4 +206,16 @@ div.bubble-parent {
194
206
  align-items: center;
195
207
  gap: 0.75rem;
196
208
  --line-height: 1;
209
+ }
210
+
211
+ .bubble-overflow {
212
+ flex-shrink: 0;
213
+ padding: 0.125rem 0.5rem;
214
+ border-radius: 9999px;
215
+ background-color: #e9ecf1;
216
+ color: #647d8e;
217
+ font-size: 0.75rem;
218
+ font-weight: 500;
219
+ white-space: nowrap;
220
+ cursor: default;
197
221
  }</style>
@@ -370,6 +370,7 @@ export function createColumnFactory() {
370
370
  id,
371
371
  ...commonProps(opts, 'bubble'),
372
372
  accessorFn: opts.value,
373
+ maxBubbles: opts.maxBubbles,
373
374
  getColour: opts.colour,
374
375
  // Default to a string-typed filter so bubble columns appear in
375
376
  // the quick-filter row and advanced filter panel by default
@@ -360,7 +360,11 @@
360
360
  handler(e);
361
361
  }}
362
362
  ondblclick={() => {
363
- tableContext.autoResize(header.column.columnDef).catch(devCatch);
363
+ // Reset the column back to its factory-default width
364
+ // (clears the user-resized flag too, so the grid
365
+ // track returns to its original `minmax(W, 1fr)` /
366
+ // fixed-`${W}px` branch as configured).
367
+ header.column.columnDef.resetWidth();
364
368
  }}
365
369
  class="resizer right"
366
370
  class:isResizing={header.column.getIsResizing()}
@@ -349,12 +349,17 @@
349
349
  --input-width: 4rem;
350
350
  --input-border: none;
351
351
  }
352
- .spread_row .block.nopad {
352
+ .spread_row .records-per-page {
353
+ --input-width: 4.5rem;
354
+ padding: 0 0.5rem;
355
+ gap: 0.375rem;
356
+ }
357
+ .spread_row .records-per-page.nopad {
353
358
  padding: 0;
354
359
  --button-padding-horizontal: 1rem;
355
360
  --button-padding-vertical: 0.56rem;
356
361
  }
357
- .spread_row .block.nopad.page-arrow {
362
+ .spread_row .records-per-page.nopad.page-arrow {
358
363
  --button-padding-horizontal: 0.6rem;
359
364
  --button-padding-vertical: 0.42rem;
360
365
  }
@@ -162,6 +162,17 @@ export declare abstract class ColumnDef<T extends object, in FilterFnType = neve
162
162
  * track, false = `minmax(width, 1fr)` flex track. */
163
163
  get widthIsFixed(): boolean;
164
164
  set widthIsFixed(v: boolean);
165
+ private readonly _originalWidth;
166
+ private readonly _originalMaxWidth;
167
+ private readonly _originalWidthIsFixed;
168
+ /**
169
+ * Reset this column's width-related state back to the values the
170
+ * factory created it with. Clears `userResized` so the grid track
171
+ * goes back to its flexible / fixed branch as originally configured.
172
+ * Used by the header resize handle's double-click to give consumers
173
+ * a single-action "restore default size" affordance.
174
+ */
175
+ resetWidth(): void;
165
176
  private _isResizing;
166
177
  /** Whether the resize handle is currently active. */
167
178
  get isResizing(): boolean;
@@ -36,9 +36,12 @@ export class ColumnDef {
36
36
  this.accessorFn = $derived(props.accessorFn);
37
37
  this._minSize = $state(props.minSize ?? 48);
38
38
  this._width = $state(props.width ?? props.minSize ?? 48);
39
+ this._originalWidth = props.width ?? props.minSize ?? 48;
39
40
  this._maxWidth = $state(props.maxWidth);
41
+ this._originalMaxWidth = props.maxWidth;
40
42
  this._wrapValue = $state(props.wrapValue ?? false);
41
43
  this._widthIsFixed = $state(props.widthIsFixed ?? false);
44
+ this._originalWidthIsFixed = props.widthIsFixed ?? false;
42
45
  this.isEditable = $derived(props.isEditable ?? (() => Promise.resolve(false)));
43
46
  this.resizable = $derived(props.resizable ?? true);
44
47
  this.header = $derived(props.header);
@@ -119,6 +122,26 @@ export class ColumnDef {
119
122
  set widthIsFixed(v) {
120
123
  this._widthIsFixed = v;
121
124
  }
125
+ // Snapshots of width / maxWidth / widthIsFixed taken at construction
126
+ // time. The user can revert any in-place mutation (drag-resize,
127
+ // programmatic write) back to these values via `resetWidth()`. They
128
+ // are plain fields (not `$state`) because they never change.
129
+ _originalWidth;
130
+ _originalMaxWidth;
131
+ _originalWidthIsFixed;
132
+ /**
133
+ * Reset this column's width-related state back to the values the
134
+ * factory created it with. Clears `userResized` so the grid track
135
+ * goes back to its flexible / fixed branch as originally configured.
136
+ * Used by the header resize handle's double-click to give consumers
137
+ * a single-action "restore default size" affordance.
138
+ */
139
+ resetWidth() {
140
+ this._width = this._originalWidth;
141
+ this._maxWidth = this._originalMaxWidth;
142
+ this._widthIsFixed = this._originalWidthIsFixed;
143
+ this._userResized = false;
144
+ }
122
145
  _isResizing = $state(false);
123
146
  /** Whether the resize handle is currently active. */
124
147
  get isResizing() {
@@ -14,6 +14,10 @@ import { AbstractDisplayDef, type AbstractDisplayDefProps } from './AbstractDisp
14
14
  export interface BubbleDefProps<T extends object, in FilterValueType, out FilterOptionsType> extends AbstractDisplayDefProps<T, FilterValueType, FilterOptionsType> {
15
15
  /** Per-bubble colour mapper. Invoked per bubble value per render. */
16
16
  getColour?: (item: T, index: number, value: string) => BubbleColour | Promise<BubbleColour | null | undefined>;
17
+ /** Maximum number of pills shown per cell before the overflow chip
18
+ * kicks in. Pills beyond this cap are hidden and a single
19
+ * `"+N more"` chip is rendered in their place. Defaults to 3. */
20
+ maxBubbles?: number;
17
21
  }
18
22
  /**
19
23
  * `BubbleDef<T, FilterValueType, FilterOptionsType>` renders the cell
@@ -29,4 +33,6 @@ export declare class BubbleDef<T extends object, in FilterValueType = StringFilt
29
33
  set filterFn(v: FilterFn<T, FilterValueType> | undefined);
30
34
  /** Per-bubble colour mapper. */
31
35
  readonly getColour: ((item: T, index: number, value: string) => BubbleColour | Promise<BubbleColour | null | undefined>) | undefined;
36
+ /** Cap on rendered pills before the overflow chip. */
37
+ readonly maxBubbles: number;
32
38
  }
@@ -15,6 +15,7 @@ export class BubbleDef extends AbstractDisplayDef {
15
15
  // props.getColour re-derives the wrapper and discards the stale cache.
16
16
  this.getColour = $derived(memoiseRowValueAccessor(props.getColour));
17
17
  this._filterFn = $state(props.filterFn);
18
+ this.maxBubbles = $derived(props.maxBubbles ?? 3);
18
19
  }
19
20
  _filterFn;
20
21
  /** Filter predicate. Reassignable. */
@@ -26,4 +27,6 @@ export class BubbleDef extends AbstractDisplayDef {
26
27
  }
27
28
  /** Per-bubble colour mapper. */
28
29
  getColour;
30
+ /** Cap on rendered pills before the overflow chip. */
31
+ maxBubbles;
29
32
  }
@@ -208,6 +208,12 @@ export interface BubbleColumnOptions<TRow extends object, TValue = unknown> exte
208
208
  * supported (scoria internally wraps it in `Promise.resolve`).
209
209
  */
210
210
  getOptions?: () => ReadonlyArray<SelectOption<TValue>> | Promise<ReadonlyArray<SelectOption<TValue>>>;
211
+ /**
212
+ * Maximum number of pills rendered before they collapse into an
213
+ * overflow chip (`"+N more"`). Defaults to 3. Set a larger number
214
+ * to show more pills inline; set `Infinity` to disable the cap.
215
+ */
216
+ maxBubbles?: number;
211
217
  }
212
218
  /** Options for `col.progress({ header, value, ... })`. Renders a horizontal
213
219
  * progress bar from a numeric ratio. */
@@ -107,6 +107,44 @@ export function createTable(options) {
107
107
  if (options.initialState) {
108
108
  applyInitialState(ctx, options.initialState);
109
109
  }
110
+ // ── 9. Auto-pin structural columns ──────────────────────────────
111
+ // `col.actions` (the ellipsis menu) and `col.rowSelection` (the
112
+ // select-row checkbox) are conventionally pinned to the right and
113
+ // left respectively across every table in the app, so apply that
114
+ // pin automatically here. The caller can override either pin via
115
+ // `initialState.columnPinning` (which runs first above), or via
116
+ // the live `setColumnPinning` updater on the table context.
117
+ const autoPinLeft = new Map();
118
+ const autoPinRight = new Map();
119
+ for (const spec of specs) {
120
+ const d = specsById.get(spec.id);
121
+ if (!d) {
122
+ continue;
123
+ }
124
+ const def = unwrapColumnSpec(d).def;
125
+ if (spec.kind === 'actions') {
126
+ autoPinRight.set(spec.id, def.width);
127
+ }
128
+ else if (spec.kind === 'rowSelection') {
129
+ autoPinLeft.set(spec.id, def.width);
130
+ }
131
+ }
132
+ if (autoPinLeft.size > 0 || autoPinRight.size > 0) {
133
+ const current = ctx.columnPinning;
134
+ const nextLeft = new Map(current.left ?? []);
135
+ const nextRight = new Map(current.right ?? []);
136
+ for (const [id, width] of autoPinLeft) {
137
+ if (!nextLeft.has(id) && !nextRight.has(id)) {
138
+ nextLeft.set(id, width);
139
+ }
140
+ }
141
+ for (const [id, width] of autoPinRight) {
142
+ if (!nextLeft.has(id) && !nextRight.has(id)) {
143
+ nextRight.set(id, width);
144
+ }
145
+ }
146
+ ctx.columnPinning = { left: nextLeft, right: nextRight };
147
+ }
110
148
  // ── 9. Build the structured Table instance ─────────────────────
111
149
  const table = Object.freeze({
112
150
  get name() {
@@ -1866,19 +1866,22 @@
1866
1866
  div.input-cell {
1867
1867
  background-color: colours.$ui-blue-1;
1868
1868
  height: var(--height);
1869
- overflow: visible;
1869
+ // `overflow-x: clip` so a narrow filter cell's internal widget
1870
+ // (select chevron, filter-mode funnel, etc.) cannot paint past
1871
+ // the column boundary. The column-separator border now lives on
1872
+ // the outer grid item (`.quick-filter-row > .cell` in
1873
+ // `TableBody.svelte`), so clipping here keeps both the widget
1874
+ // AND the cell's own background inside the track and the border
1875
+ // stays visible. `overflow-y: visible` preserves the inline
1876
+ // dropdown spillage for select / date popups.
1877
+ overflow-x: clip;
1878
+ overflow-y: visible;
1870
1879
  padding-left: 0;
1871
1880
  padding-right: 0;
1872
- // Reassert the column-separator border-right here so it survives
1873
- // even when a narrow select-filter cell's internal widget (the
1874
- // SingleSelect / MultiSelect dropdown trigger) paints flush to
1875
- // the right edge. Without this rule, two adjacent select filters
1876
- // visually merge because the inner widget's background covers
1877
- // the cell's table-cell-basic border. `!important` is here to
1878
- // override the more-specific .focusable rule from
1879
- // table-cell-basic that occasionally re-paints the right edge
1880
- // transparent during focus.
1881
- border-right: solid 1px colours.$ui-blue-4 !important;
1881
+ // Inner widget no longer paints a border-right - the outer cell
1882
+ // wrapper owns the column divider so adjacent filters cannot
1883
+ // visually merge regardless of what the widget paints inside.
1884
+ border-right: none;
1882
1885
  box-sizing: border-box;
1883
1886
 
1884
1887
  div.stick-right {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lavalogic/scoria",
3
3
  "description": "Svelte components used for the FloWMS Web Frontend",
4
- "version": "0.37.19",
4
+ "version": "0.37.21",
5
5
  "publishConfig": {
6
6
  "access": "restricted"
7
7
  },