@marianmeres/stuic 2.1.2 → 2.1.4

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.
@@ -27,7 +27,6 @@
27
27
  }
28
28
  });
29
29
 
30
- let value = $state();
31
30
  let isPending = $state(false);
32
31
  </script>
33
32
 
@@ -49,13 +48,13 @@
49
48
  noClickOutsideClose
50
49
  type={acp?.current?.type}
51
50
  class={twMerge(
52
- "max-w-xl justify-end max-h-[62vh] sm:max-h-[200px] border p-4 rounded-lg",
51
+ "max-w-xl justify-end max-h-[62vh] h-auto border p-4 rounded-lg",
53
52
  // different max-h based on not/existing content
54
- isTHCNotEmpty(acp?.current?.content) ? "sm:max-h-[200px]" : "sm:max-h-[150px]",
55
- acp?.current?.type === PROMPT && "sm:max-h-[250px]",
53
+ // isTHCNotEmpty(acp?.current?.content) ? "sm:max-h-[200px]" : "sm:max-h-[150px]",
54
+ // acp?.current?.type === PROMPT && "sm:max-h-[250px]",
56
55
  classProp
57
56
  )}
58
57
  >
59
- <Current bind:value bind:isPending {acp} />
58
+ <Current bind:isPending {acp} />
60
59
  </ModalDialog>
61
60
  {/if}
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { tick } from "svelte";
2
3
  import { twMerge } from "../../utils/tw-merge.js";
3
4
  import Button from "../Button/Button.svelte";
4
5
  import FieldInput from "../Input/FieldInput.svelte";
@@ -16,7 +17,6 @@
16
17
 
17
18
  interface Props {
18
19
  acp?: AlertConfirmPromptStack;
19
- value?: any;
20
20
  isPending?: boolean;
21
21
  forceAsHtml?: boolean;
22
22
  class?: string;
@@ -35,7 +35,6 @@
35
35
 
36
36
  let {
37
37
  acp,
38
- value = $bindable(),
39
38
  isPending = $bindable(false),
40
39
  forceAsHtml = true,
41
40
  class: classProp,
@@ -62,22 +61,35 @@
62
61
  return out;
63
62
  });
64
63
 
65
- const createOnClick = (worker: CallableFunction) => async (e: Event) => {
66
- e.preventDefault();
67
- isPending = true;
68
- await Promise.resolve(worker(current.type === PROMPT ? value : true));
69
- isPending = false;
70
- value = null;
71
- };
64
+ let inputEl = $state<any>();
65
+ let okButtonEl = $state<any>();
66
+
67
+ const createOnClick =
68
+ (type: "cancel" | "ok" | "custom", worker: CallableFunction) =>
69
+ async (e: Event | null) => {
70
+ e?.preventDefault();
71
+
72
+ // trigger validate
73
+ if (inputEl && type === "ok" && current.type === PROMPT) {
74
+ inputEl.dispatchEvent(new Event("input", { bubbles: true }));
75
+ inputEl.dispatchEvent(new Event("change", { bubbles: true }));
76
+ if (!inputEl.checkValidity()) return;
77
+ }
78
+
79
+ //
80
+ isPending = true;
81
+ await Promise.resolve(worker(current.type === PROMPT ? current.value : true));
82
+ isPending = false;
83
+ };
72
84
 
73
85
  const debug = (c?: string, flag = 0) =>
74
86
  c && flag ? `outline outline-dashed ${c}` : "";
75
87
 
76
88
  // Default classes
77
89
 
78
- const _class = "p-1 sm:p-2 h-full flex flex-col overflow-hidden";
90
+ const _class = "p-1 sm:p-2 h-full flex flex-col min-h-[150px]"; // overflow-hidden
79
91
 
80
- const _classWrap = `flex-1 sm:flex sm:items-start overflow-hidden`;
92
+ const _classWrap = `flex-1 sm:flex sm:items-start `; // overflow-hidden
81
93
 
82
94
  const _classIconBox = `size-12 sm:size-10
83
95
  mt-1 mb-4 sm:my-0 sm:mr-5
@@ -87,7 +99,7 @@
87
99
  bg-neutral-950/10 text-neutral-950/80
88
100
  dark:bg-neutral-50/20 dark:text-neutral-50/80`;
89
101
 
90
- const _classContentBox = `mt-3 sm:mt-0 flex-1 overflow-hidden h-full flex flex-col`;
102
+ const _classContentBox = `mt-3 sm:mt-0 flex-1 h-full flex flex-col`; // overflow-hidden
91
103
 
92
104
  const _classTitle = `text-center sm:text-left text-base font-semibold leading-6`;
93
105
 
@@ -128,7 +140,10 @@
128
140
  <Thc thc={current.title} {forceAsHtml} />
129
141
  </h1>
130
142
 
131
- <div class="scrollable overflow-y-auto flex-1" style="scrollbar-width: thin;">
143
+ <div
144
+ class="scrollable overflow-y-auto flex-1 max-h-[30vh]"
145
+ style="scrollbar-width: thin;"
146
+ >
132
147
  {#if isTHCNotEmpty(current.content)}
133
148
  <div class={twMerge("content", _classContent, classContent)}>
134
149
  <Thc thc={current.content} {forceAsHtml} />
@@ -136,10 +151,11 @@
136
151
  {/if}
137
152
 
138
153
  {#if current.type === PROMPT}
139
- <div class={twMerge("input-box", "mt-1 p-2", classInputBox)}>
154
+ <div class={twMerge("input-box", "mt-3 p-1", classInputBox)}>
140
155
  {#if current?.promptFieldProps?.options?.length}
141
156
  <FieldSelect
142
- bind:value
157
+ bind:value={current.value}
158
+ bind:input={inputEl}
143
159
  class={twMerge("input", "m-0", classInput)}
144
160
  options={current.promptFieldProps.options}
145
161
  renderSize="sm"
@@ -148,11 +164,17 @@
148
164
  />
149
165
  {:else}
150
166
  <FieldInput
151
- bind:value
152
- class={twMerge("input", classInput)}
167
+ bind:value={current.value}
168
+ bind:input={inputEl}
169
+ class={twMerge("input", "m-0", classInput)}
153
170
  renderSize="sm"
154
171
  disabled={isPending}
155
172
  {...current?.promptFieldProps || {}}
173
+ onkeydown={(e) => {
174
+ if (e.key === "Enter" && inputEl.checkValidity()) {
175
+ okButtonEl?.focus()?.click(); // hm...
176
+ }
177
+ }}
156
178
  />
157
179
  {/if}
158
180
  </div>
@@ -166,7 +188,7 @@
166
188
  <Button
167
189
  class={twMerge("cancel", _classButton, classButton)}
168
190
  disabled={isPending}
169
- onclick={createOnClick(current.onCancel)}
191
+ onclick={createOnClick("cancel", current.onCancel)}
170
192
  >
171
193
  <Thc thc={current.labelCancel} {forceAsHtml} />
172
194
  </Button>
@@ -177,7 +199,7 @@
177
199
  <Button
178
200
  class={twMerge("custom", _classButton, classButton)}
179
201
  disabled={isPending}
180
- onclick={createOnClick(current.onCustom)}
202
+ onclick={createOnClick("custom", current.onCustom)}
181
203
  >
182
204
  <Thc thc={current.labelCustom} {forceAsHtml} />
183
205
  </Button>
@@ -188,7 +210,8 @@
188
210
  class={twMerge("ok", _classButton, classButton)}
189
211
  variant="primary"
190
212
  disabled={isPending}
191
- onclick={createOnClick(current.onOk)}
213
+ onclick={createOnClick("ok", current.onOk)}
214
+ bind:el={okButtonEl}
192
215
  >
193
216
  <Thc thc={current.labelOk} {forceAsHtml} />
194
217
  </Button>
@@ -1,7 +1,6 @@
1
1
  import { type AlertConfirmPromptStack } from "./alert-confirm-prompt-stack.svelte.js";
2
2
  interface Props {
3
3
  acp?: AlertConfirmPromptStack;
4
- value?: any;
5
4
  isPending?: boolean;
6
5
  forceAsHtml?: boolean;
7
6
  class?: string;
@@ -17,6 +16,6 @@ interface Props {
17
16
  classButton?: string;
18
17
  classSpinnerBox?: string;
19
18
  }
20
- declare const Current: import("svelte").Component<Props, {}, "value" | "isPending">;
19
+ declare const Current: import("svelte").Component<Props, {}, "isPending">;
21
20
  type Current = ReturnType<typeof Current>;
22
21
  export default Current;
@@ -9,10 +9,13 @@
9
9
  import { tick, type Snippet } from "svelte";
10
10
  import { tooltip } from "../../actions/index.js";
11
11
  import { type ValidateOptions } from "../../actions/validate.svelte.js";
12
+ import type { TranslateFn } from "../../types.js";
12
13
  import { getId } from "../../utils/get-id.js";
14
+ import { isPlainObject } from "../../utils/is-plain-object.js";
13
15
  import { maybeJsonParse } from "../../utils/maybe-json-parse.js";
14
16
  import { waitForNextRepaint } from "../../utils/paint.js";
15
17
  import { qsa } from "../../utils/qsa.js";
18
+ import { replaceMap } from "../../utils/replace-map.js";
16
19
  import { strHash } from "../../utils/str-hash.js";
17
20
  import { twMerge } from "../../utils/tw-merge.js";
18
21
  import Button from "../Button/Button.svelte";
@@ -23,9 +26,6 @@
23
26
  import X from "../X/X.svelte";
24
27
  import InputWrap from "./_internal/InputWrap.svelte";
25
28
  import FieldLikeButton from "./FieldLikeButton.svelte";
26
- import { replaceMap } from "../../utils/replace-map.js";
27
- import { isPlainObject } from "../../utils/is-plain-object.js";
28
- import type { TranslateFn } from "../../types.js";
29
29
 
30
30
  export interface Option {
31
31
  label: string;
@@ -76,6 +76,7 @@
76
76
  type SnippetWithId = Snippet<[{ id: string }]>;
77
77
 
78
78
  interface Props extends Record<string, any> {
79
+ trigger?: Snippet<[{ value: string; modal: Modal }]>;
79
80
  input?: HTMLInputElement;
80
81
  value: string;
81
82
  label?: SnippetWithId | THC;
@@ -135,9 +136,12 @@
135
136
  searchPlaceholder?: string;
136
137
  name: string;
137
138
  itemIdPropName?: string;
139
+ // for custom stuff...
140
+ onChange?: (value: string) => void;
138
141
  }
139
142
 
140
143
  let {
144
+ trigger,
141
145
  input = $bindable(),
142
146
  value = $bindable(), //
143
147
  label = "",
@@ -191,6 +195,7 @@
191
195
  searchPlaceholder,
192
196
  name,
193
197
  itemIdPropName = "id",
198
+ onChange,
194
199
  ...rest
195
200
  }: Props = $props();
196
201
 
@@ -243,7 +248,6 @@
243
248
  allowNextPrevCycle: false,
244
249
  sortFn,
245
250
  idPropName: itemIdPropName,
246
- searchable: { getContent: (item) => _renderOptionLabel(item) },
247
251
  });
248
252
 
249
253
  // second, the selected ones
@@ -284,34 +288,29 @@
284
288
  // add_new dance...
285
289
  let addNewBtn: HTMLButtonElement | undefined = $state();
286
290
  let isAddNewBtnActive = $state(false);
291
+ let touch = $state(new Date());
287
292
 
288
293
  // set value on open
289
- watch(
290
- () => modal.visibility().visible,
291
- (isVisible, wasVisible) => {
292
- // modal was just opened
293
- if (!wasVisible && isVisible) {
294
- _selectedColl.clear().addMany(maybeJsonParse(value));
295
- // IMPORTANT: focus first selected so it scrolls into view on open
296
- if (_selectedColl.size) {
297
- waitForNextRepaint().then(() => {
298
- _optionsColl.setActive(_selectedColl.items[0]);
299
- });
300
- }
301
- }
302
- }
303
- );
304
-
305
- // scroll the active option into view
306
- $effect(() => {
307
- if (modal.visibility().visible && options.active?.[itemIdPropName]) {
308
- activeEl = qsa(`#${btn_id(options.active[itemIdPropName])}`, optionsBox)[0] as any;
309
- activeEl?.scrollIntoView({ behavior: "smooth", block: "center" });
310
- activeEl?.focus();
311
- } else {
312
- activeEl = undefined;
313
- }
314
- });
294
+ // watch(
295
+ // () => modal.visibility().visible,
296
+ // (isVisible, wasVisible) => {
297
+ // // modal was just opened
298
+ // if (isVisible) {
299
+ // _selectedColl.clear().addMany(maybeJsonParse(value));
300
+ // console.log(_selectedColl.dump());
301
+ // // IMPORTANT: focus first selected so it scrolls into view on open
302
+ // if (_selectedColl.size) {
303
+ // console.log(1111);
304
+ // waitForNextRepaint().then(() => {
305
+ // _optionsColl.setActive(_selectedColl.items[0]);
306
+ // waitForNextRepaint().then(() => {
307
+ // scrollIntoViewTrigger = new Date();
308
+ // });
309
+ // });
310
+ // }
311
+ // }
312
+ // }
313
+ // );
315
314
 
316
315
  // suggest options as a typeahead feature
317
316
  const debounced = new Debounced(() => innerValue, 150);
@@ -324,10 +323,13 @@
324
323
  .then((res) => {
325
324
  const { found, coll } = res;
326
325
 
327
- // always update the existing with recent server data
328
- _selectedColl.patchMany(found);
329
326
  // continue normally, with (server) provided options...
330
327
  _optionsColl.clear().addMany(found);
328
+ // always update the existing with recent server data
329
+ _selectedColl.patchMany(found);
330
+
331
+ // update signal...
332
+ touch = new Date();
331
333
  })
332
334
  .catch((e) => {
333
335
  console.error(e);
@@ -337,6 +339,29 @@
337
339
  }
338
340
  );
339
341
 
342
+ $effect(() => {
343
+ if (modal.visibility().visible && touch) {
344
+ _selectedColl.clear().addMany(maybeJsonParse(value));
345
+ // IMPORTANT: focus first selected so it scrolls into view on open
346
+ if (_selectedColl.size) {
347
+ waitForNextRepaint().then(() => {
348
+ _optionsColl.setActive(_selectedColl.items[0]);
349
+ });
350
+ }
351
+ }
352
+ });
353
+
354
+ // scroll the active option into view
355
+ $effect(() => {
356
+ if (options.active?.[itemIdPropName]) {
357
+ activeEl = qsa(`#${btn_id(options.active[itemIdPropName])}`, optionsBox)[0] as any;
358
+ activeEl?.scrollIntoView({ behavior: "smooth", block: "center" });
359
+ activeEl?.focus();
360
+ } else {
361
+ activeEl = undefined;
362
+ }
363
+ });
364
+
340
365
  // internal DRY
341
366
  function btn_id(id: string | number, prefix = "btn-") {
342
367
  return prefix + strHash(`${id}`.repeat(3));
@@ -344,7 +369,7 @@
344
369
 
345
370
  // "inner" submit
346
371
  function try_submit(force = false) {
347
- clog("try_submit", innerValue);
372
+ // clog("try_submit", innerValue);
348
373
  if (innerValue) {
349
374
  let found = have_option_label_like(_optionsColl.items, innerValue);
350
375
  if (!found && !allowUnknown) {
@@ -396,6 +421,7 @@
396
421
  _optionsColl.clear();
397
422
  modal.close();
398
423
  _dispatch_change_to_owner();
424
+ onChange?.(value);
399
425
  }
400
426
 
401
427
  // clears, closes, submits nothing
@@ -497,33 +523,36 @@
497
523
 
498
524
  <!-- must wrap both -->
499
525
  <div>
500
- <FieldLikeButton
501
- bind:value
502
- bind:input={parentHiddenInputEl}
503
- {name}
504
- class={classProp}
505
- {label}
506
- {description}
507
- {labelLeft}
508
- {labelAfter}
509
- {below}
510
- {labelLeftWidth}
511
- {labelLeftBreakpoint}
512
- {classLabel}
513
- {classLabelBox}
514
- {classInputBox}
515
- {classInputBoxWrap}
516
- {classDescBox}
517
- {classBelowBox}
518
- {style}
519
- validate={wrappedValidate}
520
- {required}
521
- {disabled}
522
- renderValue={(v) => {
523
- if (typeof renderValue === "function") return renderValue(v);
524
- // console.log(123123, "renderValue", v);
525
- // prettier-ignore
526
- try {
526
+ {#if trigger}
527
+ {@render trigger({ value, modal })}
528
+ {:else}
529
+ <FieldLikeButton
530
+ bind:value
531
+ bind:input={parentHiddenInputEl}
532
+ {name}
533
+ class={classProp}
534
+ {label}
535
+ {description}
536
+ {labelLeft}
537
+ {labelAfter}
538
+ {below}
539
+ {labelLeftWidth}
540
+ {labelLeftBreakpoint}
541
+ {classLabel}
542
+ {classLabelBox}
543
+ {classInputBox}
544
+ {classInputBoxWrap}
545
+ {classDescBox}
546
+ {classBelowBox}
547
+ {style}
548
+ validate={wrappedValidate}
549
+ {required}
550
+ {disabled}
551
+ renderValue={(v) => {
552
+ if (typeof renderValue === "function") return renderValue(v);
553
+ // console.log(123123, "renderValue", v);
554
+ // prettier-ignore
555
+ try {
527
556
  // defensive
528
557
  if (!v) v = "[]";
529
558
 
@@ -541,9 +570,10 @@
541
570
  clog.warn(e);
542
571
  return `${e}`; // either invalid json or not array...
543
572
  }
544
- }}
545
- onclick={modal?.open}
546
- />
573
+ }}
574
+ onclick={modal?.open}
575
+ />
576
+ {/if}
547
577
 
548
578
  <Modal
549
579
  bind:this={modal}
@@ -1,9 +1,10 @@
1
1
  import { ItemCollection, type Item } from "@marianmeres/item-collection";
2
2
  import { type Snippet } from "svelte";
3
3
  import { type ValidateOptions } from "../../actions/validate.svelte.js";
4
+ import type { TranslateFn } from "../../types.js";
5
+ import Modal from "../Modal/Modal.svelte";
4
6
  import { NotificationsStack } from "../Notifications/index.js";
5
7
  import type { THC } from "../Thc/Thc.svelte";
6
- import type { TranslateFn } from "../../types.js";
7
8
  export interface Option {
8
9
  label: string;
9
10
  value: any;
@@ -12,6 +13,10 @@ type SnippetWithId = Snippet<[{
12
13
  id: string;
13
14
  }]>;
14
15
  interface Props extends Record<string, any> {
16
+ trigger?: Snippet<[{
17
+ value: string;
18
+ modal: Modal;
19
+ }]>;
15
20
  input?: HTMLInputElement;
16
21
  value: string;
17
22
  label?: SnippetWithId | THC;
@@ -59,6 +64,7 @@ interface Props extends Record<string, any> {
59
64
  searchPlaceholder?: string;
60
65
  name: string;
61
66
  itemIdPropName?: string;
67
+ onChange?: (value: string) => void;
62
68
  }
63
69
  declare const FieldOptions: import("svelte").Component<Props, {}, "value" | "input">;
64
70
  type FieldOptions = ReturnType<typeof FieldOptions>;
@@ -10,7 +10,8 @@
10
10
  text-base placeholder:text-base
11
11
  bg-transparent
12
12
  tracking-tight
13
- focus:outline-0 focus-visible:ring-0
13
+ focus:outline-none focus:ring-0
14
+ focus-visible:outline-none focus-visible:ring-0
14
15
  placeholder:tracking-tight
15
16
  placeholder:text-neutral-950/35 dark:placeholder:text-neutral-50/35
16
17
  text-neutral-950 dark:text-neutral-50;
@@ -120,7 +120,7 @@
120
120
  )}
121
121
  onkeydown={async (e) => {
122
122
  if (e.key === "Escape" && visible) {
123
- clog("on Escape keydown, preventing default and stopping propagation");
123
+ // clog("on Escape keydown, preventing default and stopping propagation");
124
124
 
125
125
  // do not allow built-in close on escape
126
126
  e.preventDefault();
@@ -37,7 +37,7 @@
37
37
  name,
38
38
  class: classProp,
39
39
  dotClass,
40
- checked = $bindable(false),
40
+ checked = $bindable(),
41
41
  required,
42
42
  disabled,
43
43
  tabindex = 0,
@@ -26,7 +26,7 @@
26
26
  size = "md",
27
27
  class: classProp,
28
28
  dotClass,
29
- checked = $bindable(false),
29
+ checked = $bindable(),
30
30
  disabled,
31
31
  tabindex = 0,
32
32
  label,
@@ -10,6 +10,7 @@ export * from "./is-mac.js";
10
10
  export * from "./is-nullish.js";
11
11
  export * from "./maybe-json-parse.js";
12
12
  export * from "./maybe-json-stringify.js";
13
+ export * from "./nl2br.js";
13
14
  export * from "./omit-pick.js";
14
15
  export * from "./paint.js";
15
16
  export * from "./persistent-state.svelte.js";
@@ -10,6 +10,7 @@ export * from "./is-mac.js";
10
10
  export * from "./is-nullish.js";
11
11
  export * from "./maybe-json-parse.js";
12
12
  export * from "./maybe-json-stringify.js";
13
+ export * from "./nl2br.js";
13
14
  export * from "./omit-pick.js";
14
15
  export * from "./paint.js";
15
16
  export * from "./persistent-state.svelte.js";
@@ -0,0 +1 @@
1
+ export declare function nl2br(str: string): string;
@@ -0,0 +1,3 @@
1
+ export function nl2br(str) {
2
+ return `${str ?? ""}`.replace(/(?:\r\n|\r|\n)/g, "<br />");
3
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/stuic",
3
- "version": "2.1.2",
3
+ "version": "2.1.4",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",
@@ -13,7 +13,9 @@
13
13
  "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
14
14
  "format": "prettier --write .",
15
15
  "lint": "prettier --check .",
16
- "test": "vitest --dir src/"
16
+ "test": "vitest --dir src/",
17
+ "svelte-check": "svelte-check",
18
+ "svelte-package": "svelte-package"
17
19
  },
18
20
  "files": [
19
21
  "dist",
@@ -57,7 +59,6 @@
57
59
  "vite": "^6.3.6",
58
60
  "vitest": "^3.2.4"
59
61
  },
60
- "packageManager": "pnpm@10.4.1+sha512.c753b6c3ad7afa13af388fa6d808035a008e30ea9993f58c6663e2bc5ff21679aa834db094987129aa4d488b86df57f7b634981b2f827cdcacc698cc0cfb88af",
61
62
  "dependencies": {
62
63
  "@marianmeres/clog": "^2.3.3",
63
64
  "@marianmeres/item-collection": "^1.2.19",