@marianmeres/stuic 2.3.2 → 2.5.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.
Files changed (99) hide show
  1. package/README.md +0 -1
  2. package/dist/README.md +17 -11
  3. package/dist/actions/autogrow.svelte.js +4 -4
  4. package/dist/actions/autoscroll.d.ts +2 -2
  5. package/dist/actions/autoscroll.js +17 -8
  6. package/dist/actions/file-dropzone.svelte.d.ts +1 -1
  7. package/dist/actions/file-dropzone.svelte.js +1 -1
  8. package/dist/actions/focus-trap.js +33 -24
  9. package/dist/actions/highlight-dragover.svelte.js +6 -5
  10. package/dist/actions/index.d.ts +1 -0
  11. package/dist/actions/index.js +1 -0
  12. package/dist/actions/on-submit-validity-check.svelte.js +2 -2
  13. package/dist/actions/popover/PopoverContent.svelte +15 -0
  14. package/dist/actions/popover/PopoverContent.svelte.d.ts +7 -0
  15. package/dist/actions/popover/index.css +78 -0
  16. package/dist/actions/popover/index.d.ts +1 -0
  17. package/dist/actions/popover/index.js +1 -0
  18. package/dist/actions/popover/popover.svelte.d.ts +112 -0
  19. package/dist/actions/popover/popover.svelte.js +449 -0
  20. package/dist/actions/resizable-width.svelte.d.ts +1 -1
  21. package/dist/actions/resizable-width.svelte.js +7 -5
  22. package/dist/actions/tooltip/index.css +2 -7
  23. package/dist/actions/tooltip/tooltip.svelte.js +5 -4
  24. package/dist/actions/trim.svelte.d.ts +1 -1
  25. package/dist/actions/trim.svelte.js +2 -2
  26. package/dist/actions/validate.svelte.d.ts +4 -4
  27. package/dist/actions/validate.svelte.js +9 -9
  28. package/dist/components/AlertConfirmPrompt/alert-confirm-prompt-stack.svelte.d.ts +7 -6
  29. package/dist/components/AlertConfirmPrompt/alert-confirm-prompt-stack.svelte.js +1 -2
  30. package/dist/components/AlertConfirmPrompt/index.d.ts +1 -1
  31. package/dist/components/AlertConfirmPrompt/index.js +1 -1
  32. package/dist/components/AnimatedElipsis/index.d.ts +1 -1
  33. package/dist/components/AnimatedElipsis/index.js +1 -1
  34. package/dist/components/Button/index.d.ts +1 -1
  35. package/dist/components/Button/index.js +1 -1
  36. package/dist/components/ButtonGroupRadio/ButtonGroupRadio.svelte +4 -1
  37. package/dist/components/ButtonGroupRadio/index.d.ts +1 -1
  38. package/dist/components/ButtonGroupRadio/index.js +1 -1
  39. package/dist/components/ColorScheme/index.d.ts +2 -2
  40. package/dist/components/ColorScheme/index.js +2 -2
  41. package/dist/components/CommandMenu/CommandMenu.svelte +1 -1
  42. package/dist/components/CommandMenu/index.d.ts +1 -1
  43. package/dist/components/CommandMenu/index.js +1 -1
  44. package/dist/components/DismissibleMessage/index.d.ts +1 -1
  45. package/dist/components/DismissibleMessage/index.js +1 -1
  46. package/dist/components/HoverExpandableWidth/index.d.ts +1 -1
  47. package/dist/components/HoverExpandableWidth/index.js +1 -1
  48. package/dist/components/Input/FieldAssets.svelte +7 -3
  49. package/dist/components/Input/FieldLikeButton.svelte +1 -1
  50. package/dist/components/Input/FieldOptions.svelte +1 -1
  51. package/dist/components/Input/index.d.ts +7 -7
  52. package/dist/components/Input/index.js +7 -7
  53. package/dist/components/KbdShortcut/index.d.ts +1 -1
  54. package/dist/components/KbdShortcut/index.js +1 -1
  55. package/dist/components/ModalDialog/index.d.ts +1 -1
  56. package/dist/components/ModalDialog/index.js +1 -1
  57. package/dist/components/Notifications/index.d.ts +1 -1
  58. package/dist/components/Notifications/index.js +1 -1
  59. package/dist/components/Notifications/notifications-stack.svelte.d.ts +5 -5
  60. package/dist/components/Notifications/notifications-stack.svelte.js +8 -7
  61. package/dist/components/SlidingPanels/index.d.ts +1 -1
  62. package/dist/components/SlidingPanels/index.js +1 -1
  63. package/dist/components/Spinner/index.d.ts +1 -1
  64. package/dist/components/Spinner/index.js +1 -1
  65. package/dist/components/Switch/Switch.svelte +5 -2
  66. package/dist/components/Switch/index.d.ts +1 -1
  67. package/dist/components/Switch/index.js +1 -1
  68. package/dist/components/TypeaheadInput/index.d.ts +1 -1
  69. package/dist/components/TypeaheadInput/index.js +1 -1
  70. package/dist/utils/body-scroll-locker.js +4 -3
  71. package/dist/utils/breakpoint.svelte.js +0 -2
  72. package/dist/utils/colors.js +3 -3
  73. package/dist/utils/debounce.d.ts +1 -1
  74. package/dist/utils/debounce.js +1 -2
  75. package/dist/utils/escape-regex.js +1 -1
  76. package/dist/utils/event-emitter.d.ts +2 -3
  77. package/dist/utils/event-emitter.js +1 -2
  78. package/dist/utils/event-modifiers.d.ts +4 -4
  79. package/dist/utils/event-modifiers.js +4 -6
  80. package/dist/utils/get-file-type-label.js +1 -1
  81. package/dist/utils/is-image.js +2 -2
  82. package/dist/utils/is-nullish.d.ts +1 -1
  83. package/dist/utils/is-plain-object.d.ts +1 -1
  84. package/dist/utils/is-plain-object.js +4 -1
  85. package/dist/utils/maybe-json-parse.d.ts +1 -1
  86. package/dist/utils/maybe-json-parse.js +1 -1
  87. package/dist/utils/maybe-json-stringify.d.ts +1 -1
  88. package/dist/utils/move-array-item.d.ts +1 -1
  89. package/dist/utils/preload-img.js +2 -1
  90. package/dist/utils/sleep.d.ts +1 -1
  91. package/dist/utils/storage-abstraction.d.ts +13 -13
  92. package/dist/utils/storage-abstraction.js +2 -0
  93. package/dist/utils/svg-circle.js +2 -1
  94. package/dist/utils/switch.svelte.d.ts +1 -1
  95. package/dist/utils/switch.svelte.js +1 -1
  96. package/dist/utils/throttle.d.ts +1 -1
  97. package/dist/utils/throttle.js +7 -8
  98. package/dist/utils/to-integer.d.ts +1 -1
  99. package/package.json +6 -2
package/README.md CHANGED
@@ -6,4 +6,3 @@ OPINIONATED, UNSTABLE AND IN PROGRESS...
6
6
 
7
7
  Ongoing effort to build reusable Svelte UI primitives as I need them, inspired by many
8
8
  other libs -- cherry-picked, combined, adjusted and tweaked to my personal preference and needs.
9
-
package/dist/README.md CHANGED
@@ -12,21 +12,22 @@ npm install @marianmeres/stuic
12
12
 
13
13
  ```svelte
14
14
  <script>
15
- import { Button, Modal } from '@marianmeres/stuic';
15
+ import { Button, Modal } from "@marianmeres/stuic";
16
16
 
17
- let modal;
17
+ let modal;
18
18
  </script>
19
19
 
20
20
  <Button onclick={() => modal.open()}>Open Modal</Button>
21
21
 
22
22
  <Modal bind:this={modal}>
23
- <p>Hello from Modal!</p>
23
+ <p>Hello from Modal!</p>
24
24
  </Modal>
25
25
  ```
26
26
 
27
27
  ## Components
28
28
 
29
29
  ### Layout & Overlays
30
+
30
31
  - **AppShell** - Application layout wrapper with header, sidebar, main content
31
32
  - **Backdrop** - Fullscreen overlay with transitions
32
33
  - **Modal** - Dialog overlay with focus trap and scroll lock
@@ -34,6 +35,7 @@ npm install @marianmeres/stuic
34
35
  - **Drawer** - Slide-in panel from screen edges
35
36
 
36
37
  ### Forms & Inputs
38
+
37
39
  - **FieldInput** - Text input with label, validation, description
38
40
  - **FieldTextarea** - Multiline text input with autogrow support
39
41
  - **FieldSelect** - Native select dropdown
@@ -46,12 +48,14 @@ npm install @marianmeres/stuic
46
48
  - **Fieldset** - Group of form fields with legend
47
49
 
48
50
  ### Buttons & Controls
51
+
49
52
  - **Button** - Styled button with variants (primary, secondary, ghost)
50
53
  - **ButtonGroupRadio** - Radio-style button group
51
54
  - **Switch** - Toggle switch component
52
55
  - **X** - Close/dismiss button icon
53
56
 
54
57
  ### Feedback & Notifications
58
+
55
59
  - **Notifications** - Toast notification system
56
60
  - **AlertConfirmPrompt** - Modal dialogs for alerts, confirms, and prompts
57
61
  - **DismissibleMessage** - Closable message banner
@@ -59,11 +63,13 @@ npm install @marianmeres/stuic
59
63
  - **Spinner** - Loading spinner animation
60
64
 
61
65
  ### Navigation & Interaction
66
+
62
67
  - **CommandMenu** - Keyboard-driven command palette
63
68
  - **TypeaheadInput** - Autocomplete text input
64
69
  - **KbdShortcut** - Keyboard shortcut display
65
70
 
66
71
  ### Utilities
72
+
67
73
  - **ColorScheme** - Dark/light mode management
68
74
  - **Thc** - Render text, HTML, or component dynamically
69
75
  - **SlidingPanels** - Animated panel transitions
@@ -86,7 +92,7 @@ npm install @marianmeres/stuic
86
92
  ## Utilities
87
93
 
88
94
  ```ts
89
- import { debounce, throttle, twMerge, localStorageState } from '@marianmeres/stuic';
95
+ import { debounce, throttle, twMerge, localStorageState } from "@marianmeres/stuic";
90
96
  ```
91
97
 
92
98
  - **debounce/throttle** - Function rate limiting
@@ -103,10 +109,10 @@ Components use CSS custom properties for theming. Override in your CSS:
103
109
 
104
110
  ```css
105
111
  :root {
106
- --color-button-bg: theme('colors.blue.500');
107
- --color-button-text: white;
108
- --color-input-accent: theme('colors.blue.500');
109
- /* ... */
112
+ --color-button-bg: theme("colors.blue.500");
113
+ --color-button-text: white;
114
+ --color-input-accent: theme("colors.blue.500");
115
+ /* ... */
110
116
  }
111
117
  ```
112
118
 
@@ -117,10 +123,10 @@ See `src/lib/theme.css` for all available custom properties.
117
123
  All components export their Props types:
118
124
 
119
125
  ```ts
120
- import type { ButtonProps, ModalProps } from '@marianmeres/stuic';
126
+ import type { ButtonProps, ModalProps } from "@marianmeres/stuic";
121
127
 
122
128
  const buttonConfig: Partial<ButtonProps> = {
123
- variant: 'primary',
124
- size: 'lg',
129
+ variant: "primary",
130
+ size: "lg",
125
131
  };
126
132
  ```
@@ -25,10 +25,10 @@
25
25
  export function autogrow(el, fn) {
26
26
  let lastValue = el.value;
27
27
  $effect(() => {
28
- let { enabled = true, max = 250, immediate = true, value } = fn?.() || {};
28
+ const { enabled = true, max = 250, immediate = true, value } = fn?.() || {};
29
29
  if (!enabled)
30
30
  return;
31
- function set_height(_e) {
31
+ function set_height() {
32
32
  // console.log(123, el.value);
33
33
  if (enabled) {
34
34
  el.style.height = "auto"; // Reset height to auto to correctly calculate scrollHeight
@@ -38,14 +38,14 @@ export function autogrow(el, fn) {
38
38
  // eventlistener strategy (we're not passing value)
39
39
  if (value === undefined) {
40
40
  if (immediate)
41
- set_height(null);
41
+ set_height();
42
42
  el.addEventListener("input", set_height);
43
43
  el.addEventListener("blur", set_height);
44
44
  }
45
45
  // strategy with provided value
46
46
  else {
47
47
  if (value !== lastValue) {
48
- set_height(null);
48
+ set_height();
49
49
  lastValue = value;
50
50
  }
51
51
  }
@@ -12,8 +12,8 @@ interface StoreLike<T> extends StoreReadable<T> {
12
12
  * Options for the autoscroll action.
13
13
  */
14
14
  export type AutoscrollOptions = ScrollOptions & {
15
- dependencies?: StoreReadable<any>[];
16
- logger?: (...args: any[]) => void;
15
+ dependencies?: StoreReadable<unknown>[];
16
+ logger?: (...args: unknown[]) => void;
17
17
  newScrollableContentSignal?: StoreLike<boolean>;
18
18
  shouldScrollThresholdPx?: number;
19
19
  startScrollTimeout?: number;
@@ -43,16 +43,19 @@ export function autoscroll(node, options = {
43
43
  startScrollTimeout: DEFAULTS.startScrollTimeout,
44
44
  }) {
45
45
  // use "smooth" by default
46
- options.behavior ??= 'smooth';
46
+ options.behavior ??= "smooth";
47
47
  options.shouldScrollThresholdPx ??= DEFAULTS.shouldScrollThresholdPx;
48
48
  options.startScrollTimeout ??= DEFAULTS.startScrollTimeout;
49
49
  const { behavior, shouldScrollThresholdPx, dependencies, logger, newScrollableContentSignal, startScrollTimeout, } = options || {};
50
50
  let origScrollHeight = 0;
51
- const log = (...args) => typeof logger === 'function' && logger.apply(null, [...args]);
51
+ const log = (...args) => {
52
+ if (typeof logger === "function")
53
+ logger.apply(null, [...args]);
54
+ };
52
55
  const shouldScroll = () => {
53
56
  const { scrollTop, clientHeight } = node;
54
57
  const result = origScrollHeight - scrollTop - clientHeight < shouldScrollThresholdPx;
55
- log('shouldScroll?', result, { scrollTop, origScrollHeight, clientHeight });
58
+ log("shouldScroll?", result, { scrollTop, origScrollHeight, clientHeight });
56
59
  return result;
57
60
  };
58
61
  const scroll = () => {
@@ -62,17 +65,23 @@ export function autoscroll(node, options = {
62
65
  };
63
66
  // for when children change sizes
64
67
  const resizeObserver = new ResizeObserver(() => {
65
- log('observed resize...');
66
- shouldScroll() && scroll();
68
+ log("observed resize...");
69
+ if (shouldScroll())
70
+ scroll();
67
71
  });
68
72
  // for when children
69
73
  const mutationObserver = new MutationObserver(() => {
70
- log('observed mutation...');
71
- shouldScroll() ? scroll() : newScrollableContentSignal?.set(true);
74
+ log("observed mutation...");
75
+ if (shouldScroll()) {
76
+ scroll();
77
+ }
78
+ else {
79
+ newScrollableContentSignal?.set(true);
80
+ }
72
81
  origScrollHeight = node.scrollHeight;
73
82
  });
74
83
  const unsubs = dependencies?.map((dep) => dep.subscribe((v) => {
75
- log('dependency update...', v);
84
+ log("dependency update...", v);
76
85
  setTimeout(scroll, startScrollTimeout);
77
86
  })) ?? [];
78
87
  // observe size of all children
@@ -5,7 +5,7 @@ interface FileDropzoneOptions {
5
5
  enabled?: boolean;
6
6
  inputEl: HTMLInputElement;
7
7
  allowClick?: boolean;
8
- processFiles?: (files: FileList | null, wasDrop?: boolean) => any | Promise<any>;
8
+ processFiles?: (files: FileList | null, wasDrop?: boolean) => void | Promise<void>;
9
9
  }
10
10
  /**
11
11
  * A Svelte action that turns any element into a file drop zone.
@@ -40,7 +40,7 @@
40
40
  */
41
41
  export function fileDropzone(el, fn) {
42
42
  $effect(() => {
43
- let { enabled = true, allowClick = true, inputEl, processFiles, } = fn?.() || {};
43
+ const { enabled = true, allowClick = true, inputEl, processFiles, } = fn?.() || {};
44
44
  if (!enabled)
45
45
  return;
46
46
  if (!inputEl) {
@@ -32,34 +32,36 @@ const defaults = { enabled: true, autoFocusFirst: true };
32
32
  * ```
33
33
  */
34
34
  export function focusTrap(node, options = {}) {
35
- let { enabled, autoFocusFirst } = { ...defaults, ...(options || {}) };
35
+ let enabled;
36
+ const { enabled: _enabled, autoFocusFirst } = { ...defaults, ...(options || {}) };
37
+ enabled = _enabled ?? true;
36
38
  const focusableSelectors = [
37
- '[contentEditable=true]',
39
+ "[contentEditable=true]",
38
40
  //
39
- 'button:not([disabled])',
40
- 'input:not([disabled])',
41
- 'select:not([disabled])',
42
- 'textarea:not([disabled])',
41
+ "button:not([disabled])",
42
+ "input:not([disabled])",
43
+ "select:not([disabled])",
44
+ "textarea:not([disabled])",
43
45
  //
44
- 'a[href]',
45
- 'area[href]',
46
- 'details',
47
- 'iframe',
46
+ "a[href]",
47
+ "area[href]",
48
+ "details",
49
+ "iframe",
48
50
  // see more below on tabindexes
49
51
  '[tabindex]:not([tabindex^="-"])',
50
- ].join(',');
52
+ ].join(",");
51
53
  let first;
52
54
  let last;
53
55
  // When the first element is selected, shift+tab pressed, jump to the last selectable item.
54
56
  function onFirstElemKeydown(e) {
55
- if (e.shiftKey && e.code === 'Tab') {
57
+ if (e.shiftKey && e.code === "Tab") {
56
58
  e.preventDefault();
57
59
  last.focus();
58
60
  }
59
61
  }
60
62
  // When the last item selected, tab pressed, jump to the first selectable item.
61
63
  function onLastElemKeydown(e) {
62
- if (!e.shiftKey && e.code === 'Tab') {
64
+ if (!e.shiftKey && e.code === "Tab") {
63
65
  e.preventDefault();
64
66
  first.focus();
65
67
  }
@@ -68,15 +70,15 @@ export function focusTrap(node, options = {}) {
68
70
  if (enabled === false)
69
71
  return;
70
72
  let maxTabindex = 0;
71
- let focusable = [...node.querySelectorAll(focusableSelectors)]
73
+ const focusable = [...node.querySelectorAll(focusableSelectors)]
72
74
  // filter negative tabindexes (afaik there is no :not([disabled] OR [tabindex^="-"]))
73
75
  .filter((e) => {
74
76
  // reusing loop for a side job here... see sort below
75
- maxTabindex = Math.max(maxTabindex, parseInt(e.getAttribute('tabindex') || '0'));
77
+ maxTabindex = Math.max(maxTabindex, parseInt(e.getAttribute("tabindex") || "0"));
76
78
  //
77
- if (e.getAttribute('disabled') === '')
79
+ if (e.getAttribute("disabled") === "")
78
80
  return false;
79
- if ((e.getAttribute('tabindex') || '').startsWith('-'))
81
+ if ((e.getAttribute("tabindex") || "").startsWith("-"))
80
82
  return false;
81
83
  return true;
82
84
  })
@@ -84,8 +86,8 @@ export function focusTrap(node, options = {}) {
84
86
  // but must increase zero to max + 1 first, because browsers focus zeros as last...
85
87
  // https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex
86
88
  .sort((e1, e2) => {
87
- const a = parseInt(e1.getAttribute('tabindex') || '0') || maxTabindex + 1;
88
- const b = parseInt(e2.getAttribute('tabindex') || '0') || maxTabindex + 1;
89
+ const a = parseInt(e1.getAttribute("tabindex") || "0") || maxTabindex + 1;
90
+ const b = parseInt(e2.getAttribute("tabindex") || "0") || maxTabindex + 1;
89
91
  return a - b;
90
92
  });
91
93
  if (focusable.length) {
@@ -95,14 +97,16 @@ export function focusTrap(node, options = {}) {
95
97
  if (!fromObserver && autoFocusFirst)
96
98
  first.focus();
97
99
  // Listen for keydown on first & last element
98
- first.addEventListener('keydown', onFirstElemKeydown);
99
- last.addEventListener('keydown', onLastElemKeydown);
100
+ first.addEventListener("keydown", onFirstElemKeydown);
101
+ last.addEventListener("keydown", onLastElemKeydown);
100
102
  }
101
103
  };
102
104
  queryElements(false);
103
105
  function cleanup() {
104
- first && first.removeEventListener('keydown', onFirstElemKeydown);
105
- last && last.removeEventListener('keydown', onLastElemKeydown);
106
+ if (first)
107
+ first.removeEventListener("keydown", onFirstElemKeydown);
108
+ if (last)
109
+ last.removeEventListener("keydown", onLastElemKeydown);
106
110
  }
107
111
  // When children of node are changed (added or removed)
108
112
  const observer = new MutationObserver((mutations, observer) => {
@@ -117,7 +121,12 @@ export function focusTrap(node, options = {}) {
117
121
  return {
118
122
  update(options = {}) {
119
123
  enabled = !!options?.enabled;
120
- enabled ? queryElements(false) : cleanup();
124
+ if (enabled) {
125
+ queryElements(false);
126
+ }
127
+ else {
128
+ cleanup();
129
+ }
121
130
  },
122
131
  destroy() {
123
132
  cleanup();
@@ -32,15 +32,16 @@ export function highlightDragover(el, fn) {
32
32
  // // Trigger change event for any listeners
33
33
  // el.dispatchEvent(new Event("change"));
34
34
  // }
35
- function prevent(e) {
36
- e.preventDefault();
37
- // e.stopPropagation();
38
- }
35
+ // function prevent(e: DragEvent) {
36
+ // e.preventDefault();
37
+ // // e.stopPropagation();
38
+ // }
39
39
  const HIGH = ["dragenter", "dragover"];
40
40
  const UNHIGH = ["dragleave", "drop"];
41
41
  // const ALL = [...HIGH, ...UNHIGH];
42
42
  $effect(() => {
43
- let { classes = ["dragover"], enabled = true } = fn?.() || {};
43
+ const { enabled = true } = fn?.() || {};
44
+ let { classes = ["dragover"] } = fn?.() || {};
44
45
  if (!enabled)
45
46
  return;
46
47
  if (!Array.isArray(classes))
@@ -4,6 +4,7 @@ export * from "./file-dropzone.svelte.js";
4
4
  export * from "./focus-trap.js";
5
5
  export * from "./highlight-dragover.svelte.js";
6
6
  export * from "./on-submit-validity-check.svelte.js";
7
+ export * from "./popover/popover.svelte.js";
7
8
  export * from "./resizable-width.svelte.js";
8
9
  export * from "./tooltip/tooltip.svelte.js";
9
10
  export * from "./trim.svelte.js";
@@ -4,6 +4,7 @@ export * from "./file-dropzone.svelte.js";
4
4
  export * from "./focus-trap.js";
5
5
  export * from "./highlight-dragover.svelte.js";
6
6
  export * from "./on-submit-validity-check.svelte.js";
7
+ export * from "./popover/popover.svelte.js";
7
8
  export * from "./resizable-width.svelte.js";
8
9
  export * from "./tooltip/tooltip.svelte.js";
9
10
  export * from "./trim.svelte.js";
@@ -47,9 +47,9 @@ export function onSubmitValidityCheck(node) {
47
47
  e.preventDefault();
48
48
  // this will disable all other onsubmit listeners...
49
49
  e.stopImmediatePropagation();
50
- let invalid = [];
50
+ const invalid = [];
51
51
  for (let i = 0; i < node.elements?.length; i++) {
52
- let el = node.elements[i];
52
+ const el = node.elements[i];
53
53
  if (typeof el.checkValidity === "function") {
54
54
  // hm... radios are tricky, as triggering change automatically checks the last
55
55
  // input (last radio input), which is not desired
@@ -0,0 +1,15 @@
1
+ <script lang="ts" module>
2
+ import type { THC } from "../../components/Thc/Thc.svelte";
3
+
4
+ export interface Props {
5
+ thc: THC;
6
+ }
7
+ </script>
8
+
9
+ <script lang="ts">
10
+ import Thc from "../../components/Thc/Thc.svelte";
11
+
12
+ let { thc }: Props = $props();
13
+ </script>
14
+
15
+ <Thc {thc} />
@@ -0,0 +1,7 @@
1
+ import type { THC } from "../../components/Thc/Thc.svelte";
2
+ export interface Props {
3
+ thc: THC;
4
+ }
5
+ declare const PopoverContent: import("svelte").Component<Props, {}, "">;
6
+ type PopoverContent = ReturnType<typeof PopoverContent>;
7
+ export default PopoverContent;
@@ -0,0 +1,78 @@
1
+ /* prettier-ignore */
2
+ @theme inline {
3
+ /* style props will not work as with regular components, because popovers are created outside of the anchor dom tree */
4
+ --color-popover-bg: var(--color-popover-bg, var(--color-white));
5
+ --color-popover-bg-dark: var(--color-popover-bg-dark, var(--color-neutral-800));
6
+ --color-popover-text: var(--color-popover-text, var(--color-neutral-900));
7
+ --color-popover-text-dark: var(--color-popover-text-dark, var(--color-neutral-100));
8
+ --color-popover-border: var(--color-popover-border, var(--color-neutral-200));
9
+ --color-popover-border-dark: var(--color-popover-border-dark, var(--color-neutral-700));
10
+ }
11
+
12
+ /* Base popover styles */
13
+ .stuic-popover,
14
+ .stuic-popover-fallback {
15
+ display: none;
16
+ opacity: 0;
17
+ transition-property: opacity;
18
+ }
19
+
20
+ /* Backdrop for fallback mode */
21
+ .stuic-popover-backdrop {
22
+ opacity: 0;
23
+ transition-property: opacity;
24
+ &.pop-visible {
25
+ opacity: 1;
26
+ }
27
+ }
28
+
29
+ /* Define fallback positions for auto-repositioning when popover overflows viewport */
30
+ /* span-right/span-left keep popover "above/below" but shift horizontally */
31
+ @position-try --pop-top-span-right {
32
+ position-area: top span-right; /* above, aligned to anchor's left edge */
33
+ }
34
+ @position-try --pop-top-span-left {
35
+ position-area: top span-left; /* above, aligned to anchor's right edge */
36
+ }
37
+ @position-try --pop-bottom-span-right {
38
+ position-area: bottom span-right;
39
+ }
40
+ @position-try --pop-bottom-span-left {
41
+ position-area: bottom span-left;
42
+ }
43
+ @position-try --pop-left {
44
+ position-area: left;
45
+ }
46
+ @position-try --pop-right {
47
+ position-area: right;
48
+ }
49
+
50
+ /* CSS Anchor Positioning supported mode */
51
+ @supports (anchor-name: --anchor) {
52
+ .stuic-popover {
53
+ /* position-area is set via inline style based on position param */
54
+ /* fallbacks ensure popover stays within viewport */
55
+ position-try-fallbacks:
56
+ --pop-top-span-right, --pop-top-span-left, flip-block, --pop-bottom-span-right,
57
+ --pop-bottom-span-left, --pop-left, --pop-right;
58
+
59
+ &.pop-block {
60
+ display: block;
61
+ opacity: 0;
62
+ &.pop-visible {
63
+ opacity: 1;
64
+ }
65
+ }
66
+ }
67
+ }
68
+
69
+ /* Fallback mode (centered modal) */
70
+ .stuic-popover-fallback {
71
+ &.pop-block {
72
+ display: block;
73
+ opacity: 0;
74
+ &.pop-visible {
75
+ opacity: 1;
76
+ }
77
+ }
78
+ }
@@ -0,0 +1 @@
1
+ export * from "./popover.svelte.js";
@@ -0,0 +1 @@
1
+ export * from "./popover.svelte.js";
@@ -0,0 +1,112 @@
1
+ import type { THC } from "../../components/Thc/Thc.svelte";
2
+ import "./index.css";
3
+ /**
4
+ * Checks if the browser supports CSS Anchor Positioning for popovers.
5
+ *
6
+ * Tests for support of `anchor-name`, `position-area`, `position-try`,
7
+ * and `position-try-fallbacks` CSS properties.
8
+ *
9
+ * @returns `true` if CSS Anchor Positioning is fully supported
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * if (isPopoverSupported()) {
14
+ * // Use native anchor positioning
15
+ * } else {
16
+ * // Fall back to centered modal
17
+ * }
18
+ * ```
19
+ */
20
+ export declare function isPopoverSupported(): boolean;
21
+ /**
22
+ * Valid positions for popover placement relative to the anchor element.
23
+ */
24
+ export type PopoverPosition = "top" | "top-left" | "top-right" | "top-span-left" | "top-span-right" | "bottom" | "bottom-left" | "bottom-right" | "bottom-span-left" | "bottom-span-right" | "left" | "right";
25
+ /**
26
+ * Trigger mode for the popover.
27
+ */
28
+ export type PopoverTrigger = "click" | "hover";
29
+ /**
30
+ * Options for the popover action.
31
+ */
32
+ export interface PopoverOptions {
33
+ /** Whether the popover is enabled */
34
+ enabled?: boolean;
35
+ /** Content to display (THC format: string, {text}, {html}, {component, props}, {snippet}, or Snippet) */
36
+ content?: THC | null;
37
+ /** Preferred position relative to anchor */
38
+ position?: PopoverPosition;
39
+ /** Trigger mode: "click" (default) or "hover" */
40
+ trigger?: PopoverTrigger;
41
+ /** Delay before showing (ms), mainly for hover mode */
42
+ showDelay?: number;
43
+ /** Delay before hiding (ms), mainly for hover mode */
44
+ hideDelay?: number;
45
+ /** Custom class for the popover container */
46
+ class?: string;
47
+ /** Offset/margin from the anchor element (CSS value, e.g., "0.5rem", "8px") */
48
+ offset?: string;
49
+ /** Close all other open popovers when this one opens */
50
+ closeOthers?: boolean;
51
+ /** Close on click outside (for click trigger) */
52
+ closeOnClickOutside?: boolean;
53
+ /** Close on Escape key */
54
+ closeOnEscape?: boolean;
55
+ /** Show backdrop in fallback mode */
56
+ showBackdrop?: boolean;
57
+ /** Force fallback mode (centered modal) even if CSS Anchor Positioning is supported */
58
+ forceFallback?: boolean;
59
+ /** Callback when popover opens */
60
+ onShow?: () => void;
61
+ /** Callback when popover hides */
62
+ onHide?: () => void;
63
+ /** Debug mode */
64
+ debug?: boolean;
65
+ }
66
+ /**
67
+ * A Svelte action that displays a popover anchored to an element using CSS Anchor Positioning.
68
+ *
69
+ * The popover appears on click or hover (configurable) and supports multiple positions
70
+ * with automatic fallback. When CSS Anchor Positioning is not supported, falls back
71
+ * to a centered modal overlay.
72
+ *
73
+ * @param anchorEl - The element to attach the popover to
74
+ * @param fn - Function returning popover options (reactive)
75
+ *
76
+ * @example
77
+ * ```svelte
78
+ * <!-- Click trigger (default) -->
79
+ * <button use:popover={() => ({ content: "Hello World" })}>
80
+ * Open Popover
81
+ * </button>
82
+ * ```
83
+ *
84
+ * @example
85
+ * ```svelte
86
+ * <!-- Hover trigger -->
87
+ * <button use:popover={() => ({
88
+ * content: "Hover content",
89
+ * trigger: "hover",
90
+ * position: "top"
91
+ * })}>
92
+ * Hover Me
93
+ * </button>
94
+ * ```
95
+ *
96
+ * @example
97
+ * ```svelte
98
+ * <!-- With component content -->
99
+ * <button use:popover={() => ({
100
+ * content: { component: MyComponent, props: { foo: "bar" } }
101
+ * })}>
102
+ * With Component
103
+ * </button>
104
+ * ```
105
+ *
106
+ * @remarks
107
+ * - Falls back to centered modal when CSS Anchor Positioning is not supported
108
+ * - Closes on click outside (for click trigger) and Escape key by default
109
+ * - For hover trigger, popover persists when hovering over it
110
+ * - Automatically cleans up DOM elements on unmount
111
+ */
112
+ export declare function popover(anchorEl: HTMLElement, fn?: () => PopoverOptions): void;