@lavalogic/scoria 0.37.8 → 0.37.10

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,7 +1,7 @@
1
1
  <svelte:options runes />
2
2
 
3
3
  <script lang="ts">
4
- import { cssLength, passthrough } from '../Helpers/Helpers.svelte.js';
4
+ import { cssLength } from '../Helpers/Helpers.svelte.js';
5
5
  import { InputVariant } from '../Types/Internal/InputVariant.js';
6
6
  import { Size } from '../Types/Internal/Size.js';
7
7
  import Icon from './Icon.svelte';
@@ -28,7 +28,10 @@
28
28
 
29
29
  onblur,
30
30
  onfocus,
31
- onkeydown = passthrough,
31
+ // No default `onkeydown` - see TextInput.svelte for the rationale.
32
+ // The legacy default (`passthrough`) blurred the input on every
33
+ // alphanumeric keystroke and made the field unusable.
34
+ onkeydown,
32
35
  oninput,
33
36
 
34
37
  inputRef = $bindable(),
@@ -1,7 +1,7 @@
1
1
  <svelte:options runes />
2
2
 
3
3
  <script lang="ts">
4
- import { cssLength, passthrough } from '../Helpers/Helpers.svelte.js';
4
+ import { cssLength } from '../Helpers/Helpers.svelte.js';
5
5
  import { ColourSet } from '../scss/colours.js';
6
6
  import { Size } from '../Types/Internal/Size.js';
7
7
  import Action from './Action.svelte';
@@ -19,7 +19,10 @@
19
19
  autocomplete = 'current-password',
20
20
  onblur,
21
21
  onfocus,
22
- onkeydown = passthrough,
22
+ // No default `onkeydown` - see TextInput.svelte for the rationale.
23
+ // The legacy default (`passthrough`) blurred the input on every
24
+ // alphanumeric keystroke and made the field unusable.
25
+ onkeydown,
23
26
  }: PasswordInputProps = $props();
24
27
 
25
28
  function toggleVisibility(): void {
@@ -67,14 +67,41 @@ function makeSpec(kind, def, internals = {}) {
67
67
  });
68
68
  return Object.freeze(spec);
69
69
  }
70
+ /** Sensible per-kind default widths so a freshly-built table without explicit
71
+ * per-column `width` overrides ends up visually differentiated (rather than
72
+ * every column collapsing to the shared `minSize` fallback of 48px and then
73
+ * taking equal `1fr` shares of any remaining track space). Numbers chosen
74
+ * to roughly match what the legacy `<table>` element produced via the
75
+ * browser auto-layout algorithm for typical FloWMS column content. */
76
+ const KIND_DEFAULT_WIDTHS = {
77
+ text: 200,
78
+ number: 100,
79
+ checkbox: 80,
80
+ date: 140,
81
+ select: 160,
82
+ query: 160,
83
+ display: 150,
84
+ bubble: 150,
85
+ progress: 140,
86
+ validity: 60,
87
+ actions: 120,
88
+ rowSelection: 40,
89
+ expand: 40,
90
+ filterOnly: 0,
91
+ };
70
92
  /** Returns the common Def props for an option object, excluding `id`.
71
93
  * Callers pass `id` directly at the construction site so its strict
72
- * literal-key narrowing survives the object spread. */
73
- function commonProps(opts) {
94
+ * literal-key narrowing survives the object spread.
95
+ *
96
+ * `kind` selects the per-kind default width from `KIND_DEFAULT_WIDTHS`. The
97
+ * caller's `minWidth` / `width` values still win - the default is only
98
+ * applied if the consumer left the field undefined. */
99
+ function commonProps(opts, kind) {
100
+ const defaultWidth = KIND_DEFAULT_WIDTHS[kind];
74
101
  return {
75
102
  header: opts.header,
76
103
  minSize: opts.minWidth,
77
- width: opts.width,
104
+ width: opts.width ?? defaultWidth,
78
105
  allowSorting: opts.allowSorting,
79
106
  allowFiltering: opts.allowFiltering,
80
107
  defaultHidden: opts.defaultHidden,
@@ -123,7 +150,7 @@ export function createColumnFactory() {
123
150
  text(opts) {
124
151
  const def = new TextInputDef({
125
152
  id: opts.id,
126
- ...commonProps(opts),
153
+ ...commonProps(opts, 'text'),
127
154
  accessorFn: opts.accessor,
128
155
  getDisplayValue: opts.getDisplayValue,
129
156
  filterValueType: FilterValueType.String,
@@ -136,7 +163,7 @@ export function createColumnFactory() {
136
163
  number(opts) {
137
164
  const def = new NumberInputDef({
138
165
  id: opts.id,
139
- ...commonProps(opts),
166
+ ...commonProps(opts, 'number'),
140
167
  accessorFn: opts.accessor,
141
168
  getDisplayValue: opts.getDisplayValue,
142
169
  min: opts.min ?? Number.MIN_SAFE_INTEGER,
@@ -151,7 +178,7 @@ export function createColumnFactory() {
151
178
  checkbox(opts) {
152
179
  const def = new CheckboxDef({
153
180
  id: opts.id,
154
- ...commonProps(opts),
181
+ ...commonProps(opts, 'checkbox'),
155
182
  accessorFn: opts.accessor,
156
183
  getDisplayValue: opts.getDisplayValue,
157
184
  filterValueType: FilterValueType.Bool,
@@ -164,7 +191,7 @@ export function createColumnFactory() {
164
191
  date(opts) {
165
192
  const def = new DateInputDef({
166
193
  id: opts.id,
167
- ...commonProps(opts),
194
+ ...commonProps(opts, 'date'),
168
195
  accessorFn: opts.accessor,
169
196
  getDisplayValue: opts.getDisplayValue,
170
197
  showTime: opts.withTime ?? false,
@@ -178,7 +205,7 @@ export function createColumnFactory() {
178
205
  select(opts) {
179
206
  const def = new SelectDef({
180
207
  id: opts.id,
181
- ...commonProps(opts),
208
+ ...commonProps(opts, 'select'),
182
209
  accessorFn: opts.accessor,
183
210
  getOptions: normaliseOptionsLoader(opts.options),
184
211
  getDisplayValue: opts.getDisplayValue ?? ((row) => String(row[opts.id] ?? '')),
@@ -199,7 +226,7 @@ export function createColumnFactory() {
199
226
  query(opts) {
200
227
  const def = new QueryDef({
201
228
  id: opts.id,
202
- ...commonProps(opts),
229
+ ...commonProps(opts, 'query'),
203
230
  accessorFn: opts.accessor,
204
231
  // Forward the row to the consumer's query callback so it
205
232
  // may scope results by row-level context (e.g. a `pod`
@@ -224,7 +251,7 @@ export function createColumnFactory() {
224
251
  : undefined;
225
252
  const def = new DisplayDef({
226
253
  id,
227
- ...commonProps(opts),
254
+ ...commonProps(opts, 'display'),
228
255
  accessorFn: opts.value,
229
256
  modal: opts.modal,
230
257
  // Display columns have no implicit filter type (unlike the
@@ -241,7 +268,7 @@ export function createColumnFactory() {
241
268
  const id = mintSyntheticId('bubble', opts.idHint);
242
269
  const def = new BubbleDef({
243
270
  id,
244
- ...commonProps(opts),
271
+ ...commonProps(opts, 'bubble'),
245
272
  accessorFn: opts.value,
246
273
  getColour: opts.colour,
247
274
  filterValueType: opts.filterValueType,
@@ -267,7 +294,7 @@ export function createColumnFactory() {
267
294
  : () => 'grey';
268
295
  const def = new ProgressBarDef({
269
296
  id,
270
- ...commonProps(opts),
297
+ ...commonProps(opts, 'progress'),
271
298
  accessorFn: tupleAccessor,
272
299
  getColour,
273
300
  filterValueType: opts.filterValueType,
@@ -281,7 +308,7 @@ export function createColumnFactory() {
281
308
  id,
282
309
  header: opts?.header ?? '',
283
310
  minSize: opts?.minWidth,
284
- width: opts?.width,
311
+ width: opts?.width ?? KIND_DEFAULT_WIDTHS.validity,
285
312
  defaultHidden: opts?.defaultHidden,
286
313
  resizable: opts?.resizable ?? false,
287
314
  debug: opts?.debug,
@@ -295,6 +322,11 @@ export function createColumnFactory() {
295
322
  const def = new RowSelectionDef({
296
323
  isSelectable: opts?.isSelectable ?? (() => true),
297
324
  });
325
+ // RowSelection has no public width option - apply the per-kind
326
+ // default so the column does not collapse to the 48px ColumnDef
327
+ // fallback and share equal `1fr` track space with wider columns.
328
+ def.width = KIND_DEFAULT_WIDTHS.rowSelection;
329
+ def.minSize = KIND_DEFAULT_WIDTHS.rowSelection;
298
330
  return makeSpec('rowSelection', def);
299
331
  },
300
332
  actions(opts) {
@@ -317,6 +349,8 @@ export function createColumnFactory() {
317
349
  actions: wrapped,
318
350
  isEnabled: isEnabledFn ? (item) => isEnabledFn(item) : undefined,
319
351
  });
352
+ // Actions has no public width option - apply the per-kind default.
353
+ def.width = KIND_DEFAULT_WIDTHS.actions;
320
354
  return makeSpec('actions', def);
321
355
  },
322
356
  expand(opts) {
@@ -359,6 +393,9 @@ export function createColumnFactory() {
359
393
  allowCopy: opts.inner?.allowCopy,
360
394
  },
361
395
  });
396
+ // Expand has no public width option - apply the per-kind default.
397
+ def.width = KIND_DEFAULT_WIDTHS.expand;
398
+ def.minSize = KIND_DEFAULT_WIDTHS.expand;
362
399
  return makeSpec('expand', def);
363
400
  },
364
401
  filterOnly(opts) {
@@ -105,8 +105,19 @@
105
105
  untrack(() => {
106
106
  if (tableContext.focusStart && ref) {
107
107
  const { row, column: index } = tableContext.focusStart;
108
+ // `columnDefs[index]` can be undefined when the focus
109
+ // coordinate references a memory slot that no longer
110
+ // exists (e.g. inner expand tables remount with a
111
+ // different column count, or a preset hydration races
112
+ // the column-layout init). Guard before destructuring
113
+ // so an out-of-range index degrades to "no focus
114
+ // restored" instead of a hard crash.
115
+ const def = tableContext.columnDefs[index];
116
+ if (!def) {
117
+ return;
118
+ }
108
119
  let element = ref.querySelector(
109
- `#${tableContext.tableName}-cell-${row}-${tableContext.columnDefs[index].id}`
120
+ `#${tableContext.tableName}-cell-${row}-${def.id}`
110
121
  ) as HTMLElement | undefined;
111
122
 
112
123
  if (element?.tabIndex == -1) {
@@ -136,8 +147,13 @@
136
147
  untrack(() => {
137
148
  if (tableContext.focusEnd && ref) {
138
149
  const { row, column: index } = tableContext.focusEnd;
150
+ // Same out-of-range guard as the focusStart effect above.
151
+ const def = tableContext.columnDefs[index];
152
+ if (!def) {
153
+ return;
154
+ }
139
155
  const cell = ref.querySelector(
140
- `#${tableContext.tableName}-cell-${row}-${tableContext.columnDefs[index].id}`
156
+ `#${tableContext.tableName}-cell-${row}-${def.id}`
141
157
  ) as HTMLElement | undefined;
142
158
 
143
159
  cell?.scrollIntoView({
@@ -1,7 +1,7 @@
1
1
  <svelte:options runes />
2
2
 
3
3
  <script lang="ts">
4
- import { cssLength, passthrough } from '../Helpers/Helpers.svelte.js';
4
+ import { cssLength } from '../Helpers/Helpers.svelte.js';
5
5
  import { InputVariant } from '../Types/Internal/InputVariant.js';
6
6
  import { Size } from '../Types/Internal/Size.js';
7
7
  import Icon from './Icon.svelte';
@@ -33,7 +33,10 @@
33
33
  spellcheck,
34
34
  onblur,
35
35
  onfocus,
36
- onkeydown = passthrough,
36
+ // No default `onkeydown` - see TextInput.svelte for the rationale.
37
+ // The legacy default (`passthrough`) blurred the textarea on every
38
+ // alphanumeric keystroke and made the field unusable.
39
+ onkeydown,
37
40
  oninput,
38
41
  }: TextAreaProps = $props();
39
42
 
@@ -1,7 +1,7 @@
1
1
  <svelte:options runes />
2
2
 
3
3
  <script lang="ts">
4
- import { cssLength, isValidEmailAddress, passthrough } from '../Helpers/Helpers.svelte.js';
4
+ import { cssLength, isValidEmailAddress } from '../Helpers/Helpers.svelte.js';
5
5
  import { Colour, ColourSet } from '../scss/colours.js';
6
6
  import { InputVariant } from '../Types/Internal/InputVariant.js';
7
7
  import { Size } from '../Types/Internal/Size.js';
@@ -34,7 +34,17 @@
34
34
  spellcheck,
35
35
  onblur,
36
36
  onfocus,
37
- onkeydown = passthrough,
37
+ // No default `onkeydown`. The legacy default was `passthrough`,
38
+ // a helper that BLURS the target on every alphanumeric key (it is
39
+ // a barcode-scanner aid intended for buttons - note the
40
+ // `as HTMLButtonElement` cast inside it). Applied to a text input
41
+ // it makes the field unusable: each keystroke blurs the input
42
+ // before the value can update, so users see nothing as they type.
43
+ // Inputs should accept keystrokes by default; consumers that want
44
+ // the barcode-blur behaviour explicitly pass
45
+ // `onkeydown={passthrough}` (preferably renamed to
46
+ // `blurOnAlphanumeric` in a future patch).
47
+ onkeydown,
38
48
  oninput,
39
49
  }: TextInputProps = $props();
40
50
 
@@ -136,11 +136,21 @@ export declare function SQLFriendlyWithLocalTime(date: InputDate | InputDateWith
136
136
  */
137
137
  export declare const localTimeFormat: Intl.DateTimeFormat;
138
138
  /**
139
- * Default `onkeydown` handler that blurs the focused element when
140
- * the user presses an alphanumeric key. Used on input wrappers
141
- * and buttons so that scanner-driven barcode characters do not
142
- * accumulate inside the focused field; instead the buffer goes
143
- * to `UntypedInputContext`.
139
+ * Barcode-scanner helper. Wire as an `onkeydown` on **buttons and
140
+ * other non-text focusable elements** that should yield focus when a
141
+ * scanner-driven keystream begins. On every alphanumeric key it
142
+ * calls `e.target.blur()` so the buffered keystrokes flow up to
143
+ * `UntypedInputContext` instead of being captured by the focused
144
+ * element.
145
+ *
146
+ * Do NOT apply to text inputs / textareas. Blurring the field on each
147
+ * keypress makes it impossible to type into - the field deselects
148
+ * before the browser commits the character.
149
+ *
150
+ * Despite the name, this is not a "pass the event through" no-op; it
151
+ * actively blurs the target. Renaming is deferred for backwards
152
+ * compatibility, but consumer-facing prop docs should describe the
153
+ * behaviour as "blur on alphanumeric".
144
154
  */
145
155
  export declare function passthrough(e: KeyboardEvent): void;
146
156
  /**
@@ -248,11 +248,21 @@ export const localTimeFormat = new Intl.DateTimeFormat('en-GB', {
248
248
  timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
249
249
  });
250
250
  /**
251
- * Default `onkeydown` handler that blurs the focused element when
252
- * the user presses an alphanumeric key. Used on input wrappers
253
- * and buttons so that scanner-driven barcode characters do not
254
- * accumulate inside the focused field; instead the buffer goes
255
- * to `UntypedInputContext`.
251
+ * Barcode-scanner helper. Wire as an `onkeydown` on **buttons and
252
+ * other non-text focusable elements** that should yield focus when a
253
+ * scanner-driven keystream begins. On every alphanumeric key it
254
+ * calls `e.target.blur()` so the buffered keystrokes flow up to
255
+ * `UntypedInputContext` instead of being captured by the focused
256
+ * element.
257
+ *
258
+ * Do NOT apply to text inputs / textareas. Blurring the field on each
259
+ * keypress makes it impossible to type into - the field deselects
260
+ * before the browser commits the character.
261
+ *
262
+ * Despite the name, this is not a "pass the event through" no-op; it
263
+ * actively blurs the target. Renaming is deferred for backwards
264
+ * compatibility, but consumer-facing prop docs should describe the
265
+ * behaviour as "blur on alphanumeric".
256
266
  */
257
267
  export function passthrough(e) {
258
268
  if (!e.key.match(/^[a-zA-Z0-9[\]]$/gm)) {
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.8",
4
+ "version": "0.37.10",
5
5
  "publishConfig": {
6
6
  "access": "restricted"
7
7
  },