@marianmeres/stuic 3.167.0 → 3.169.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 (115) hide show
  1. package/AGENTS.md +6 -6
  2. package/dist/README.md +1 -1
  3. package/dist/attachments/index.d.ts +1 -0
  4. package/dist/attachments/index.js +1 -0
  5. package/dist/attachments/long-press.d.ts +43 -0
  6. package/dist/attachments/long-press.js +107 -0
  7. package/dist/components/Breadcrumbs/Breadcrumbs.svelte +198 -0
  8. package/dist/components/Breadcrumbs/Breadcrumbs.svelte.d.ts +60 -0
  9. package/dist/components/Breadcrumbs/README.md +174 -0
  10. package/dist/components/Breadcrumbs/i18n-sk.d.ts +17 -0
  11. package/dist/components/Breadcrumbs/i18n-sk.js +19 -0
  12. package/dist/components/Breadcrumbs/i18n.d.ts +31 -0
  13. package/dist/components/Breadcrumbs/i18n.js +40 -0
  14. package/dist/components/Breadcrumbs/index.css +84 -0
  15. package/dist/components/Breadcrumbs/index.d.ts +4 -0
  16. package/dist/components/Breadcrumbs/index.js +4 -0
  17. package/dist/components/Breadcrumbs/json-ld.d.ts +58 -0
  18. package/dist/components/Breadcrumbs/json-ld.js +56 -0
  19. package/dist/components/Collapsible/Collapsible.svelte +84 -15
  20. package/dist/components/Collapsible/Collapsible.svelte.d.ts +29 -2
  21. package/dist/components/Collapsible/README.md +82 -23
  22. package/dist/components/Collapsible/index.css +38 -0
  23. package/dist/components/Collapsible/index.d.ts +1 -1
  24. package/dist/components/ContextMenu/ContextMenu.svelte +302 -0
  25. package/dist/components/ContextMenu/ContextMenu.svelte.d.ts +94 -0
  26. package/dist/components/ContextMenu/README.md +159 -0
  27. package/dist/components/ContextMenu/i18n-sk.d.ts +17 -0
  28. package/dist/components/ContextMenu/i18n-sk.js +18 -0
  29. package/dist/components/ContextMenu/i18n.d.ts +30 -0
  30. package/dist/components/ContextMenu/i18n.js +38 -0
  31. package/dist/components/ContextMenu/index.css +26 -0
  32. package/dist/components/ContextMenu/index.d.ts +3 -0
  33. package/dist/components/ContextMenu/index.js +3 -0
  34. package/dist/components/CopyButton/CopyButton.svelte +278 -0
  35. package/dist/components/CopyButton/CopyButton.svelte.d.ts +76 -0
  36. package/dist/components/CopyButton/README.md +241 -0
  37. package/dist/components/CopyButton/i18n-sk.d.ts +17 -0
  38. package/dist/components/CopyButton/i18n-sk.js +20 -0
  39. package/dist/components/CopyButton/i18n.d.ts +35 -0
  40. package/dist/components/CopyButton/i18n.js +43 -0
  41. package/dist/components/CopyButton/index.css +67 -0
  42. package/dist/components/CopyButton/index.d.ts +3 -0
  43. package/dist/components/CopyButton/index.js +3 -0
  44. package/dist/components/DataTable/DataTable.svelte +41 -34
  45. package/dist/components/DataTable/README.md +45 -0
  46. package/dist/components/DataTable/i18n-sk.js +1 -0
  47. package/dist/components/DataTable/i18n.d.ts +1 -0
  48. package/dist/components/DataTable/i18n.js +3 -0
  49. package/dist/components/DataTable/index.css +41 -3
  50. package/dist/components/EmptyState/EmptyState.svelte +121 -0
  51. package/dist/components/EmptyState/EmptyState.svelte.d.ts +40 -0
  52. package/dist/components/EmptyState/README.md +188 -0
  53. package/dist/components/EmptyState/index.css +154 -0
  54. package/dist/components/EmptyState/index.d.ts +1 -0
  55. package/dist/components/EmptyState/index.js +1 -0
  56. package/dist/components/Input/FieldOptions.svelte +92 -2
  57. package/dist/components/Input/FieldOptions.svelte.d.ts +13 -0
  58. package/dist/components/Input/README.md +66 -31
  59. package/dist/components/Input/_internal/FieldLikeChips.svelte +286 -0
  60. package/dist/components/Input/_internal/FieldLikeChips.svelte.d.ts +65 -0
  61. package/dist/components/Input/index.css +58 -0
  62. package/dist/components/Pagination/Pagination.svelte +250 -0
  63. package/dist/components/Pagination/Pagination.svelte.d.ts +69 -0
  64. package/dist/components/Pagination/README.md +152 -0
  65. package/dist/components/Pagination/i18n-sk.d.ts +17 -0
  66. package/dist/components/Pagination/i18n-sk.js +24 -0
  67. package/dist/components/Pagination/i18n.d.ts +41 -0
  68. package/dist/components/Pagination/i18n.js +49 -0
  69. package/dist/components/Pagination/index.css +50 -0
  70. package/dist/components/Pagination/index.d.ts +4 -0
  71. package/dist/components/Pagination/index.js +4 -0
  72. package/dist/components/Pagination/pagination-range.d.ts +21 -0
  73. package/dist/components/Pagination/pagination-range.js +41 -0
  74. package/dist/components/Pill/Pill.svelte +8 -2
  75. package/dist/components/Pill/Pill.svelte.d.ts +2 -0
  76. package/dist/components/Pill/README.md +35 -22
  77. package/dist/components/Pill/index.css +57 -18
  78. package/dist/components/Stat/README.md +157 -0
  79. package/dist/components/Stat/Stat.svelte +226 -0
  80. package/dist/components/Stat/Stat.svelte.d.ts +54 -0
  81. package/dist/components/Stat/index.css +217 -0
  82. package/dist/components/Stat/index.d.ts +1 -0
  83. package/dist/components/Stat/index.js +1 -0
  84. package/dist/components/Stepper/README.md +181 -0
  85. package/dist/components/Stepper/Stepper.svelte +240 -0
  86. package/dist/components/Stepper/Stepper.svelte.d.ts +86 -0
  87. package/dist/components/Stepper/i18n-sk.d.ts +17 -0
  88. package/dist/components/Stepper/i18n-sk.js +21 -0
  89. package/dist/components/Stepper/i18n.d.ts +35 -0
  90. package/dist/components/Stepper/i18n.js +43 -0
  91. package/dist/components/Stepper/index.css +302 -0
  92. package/dist/components/Stepper/index.d.ts +3 -0
  93. package/dist/components/Stepper/index.js +3 -0
  94. package/dist/components/TrendChart/README.md +123 -0
  95. package/dist/components/TrendChart/TrendChart.svelte +114 -0
  96. package/dist/components/TrendChart/TrendChart.svelte.d.ts +36 -0
  97. package/dist/components/TrendChart/index.css +51 -0
  98. package/dist/components/TrendChart/index.d.ts +1 -0
  99. package/dist/components/TrendChart/index.js +1 -0
  100. package/dist/icons/index.d.ts +4 -0
  101. package/dist/icons/index.js +4 -0
  102. package/dist/index.css +7 -0
  103. package/dist/index.d.ts +7 -0
  104. package/dist/index.js +12 -0
  105. package/dist/utils/copy-to-clipboard.d.ts +43 -0
  106. package/dist/utils/copy-to-clipboard.js +102 -0
  107. package/dist/utils/index.d.ts +1 -0
  108. package/dist/utils/index.js +1 -0
  109. package/docs/architecture.md +1 -1
  110. package/docs/domains/attachments.md +41 -4
  111. package/docs/domains/components.md +280 -13
  112. package/docs/domains/utils.md +8 -6
  113. package/docs/maybe-todo.md +132 -0
  114. package/docs/upgrading.md +6 -0
  115. package/package.json +11 -1
@@ -0,0 +1,26 @@
1
+ /* =============================================================================
2
+ CONTEXT MENU COMPONENT
3
+ The menu panel itself IS a DropdownMenu — theme it via the
4
+ --stuic-dropdown-menu-* tokens. This file only styles the target area.
5
+ ============================================================================= */
6
+
7
+ @layer components {
8
+ .stuic-context-menu {
9
+ -webkit-tap-highlight-color: transparent;
10
+ }
11
+
12
+ /* Long-press enabled: keep the platform from hijacking the press.
13
+ -webkit-touch-callout blocks iOS's link/image callout on long-press;
14
+ user-select (coarse pointers only) blocks the text-selection long-press —
15
+ desktop text selection inside the area stays intact. */
16
+ .stuic-context-menu[data-longpress] {
17
+ -webkit-touch-callout: none;
18
+ }
19
+
20
+ @media (pointer: coarse) {
21
+ .stuic-context-menu[data-longpress] {
22
+ user-select: none;
23
+ -webkit-user-select: none;
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,3 @@
1
+ export { default as ContextMenu, type Props as ContextMenuProps, type ContextMenuItem, type ContextMenuActionItem, type ContextMenuPosition, type ContextMenuSearchConfig, CONTEXT_MENU_BASE_CLASSES, CONTEXT_MENU_ANCHOR_CLASSES, } from "./ContextMenu.svelte";
2
+ export { createContextMenuT, CONTEXT_MENU_MESSAGES_EN, type ContextMenuMessageKey, type ContextMenuMessages, } from "./i18n.js";
3
+ export { CONTEXT_MENU_MESSAGES_SK } from "./i18n-sk.js";
@@ -0,0 +1,3 @@
1
+ export { default as ContextMenu, CONTEXT_MENU_BASE_CLASSES, CONTEXT_MENU_ANCHOR_CLASSES, } from "./ContextMenu.svelte";
2
+ export { createContextMenuT, CONTEXT_MENU_MESSAGES_EN, } from "./i18n.js";
3
+ export { CONTEXT_MENU_MESSAGES_SK } from "./i18n-sk.js";
@@ -0,0 +1,278 @@
1
+ <script lang="ts" module>
2
+ import type { Snippet } from "svelte";
3
+ import type { IntentColorKey } from "../../utils/design-tokens.js";
4
+ import type { THC } from "../Thc/Thc.svelte";
5
+ import type { TranslateFn } from "../../types.js";
6
+ import type { Props as ButtonProps } from "../Button/Button.svelte";
7
+ import type { TooltipConfig } from "../../actions/tooltip/tooltip.svelte.js";
8
+
9
+ /** Feedback phase the button is in. Exposed as `data-state` and to `children`. */
10
+ export type CopyButtonState = "idle" | "copied" | "error";
11
+
12
+ /** What to copy: a string, or a getter (sync or async) resolved on click. */
13
+ export type CopyButtonText = string | (() => string | Promise<string>);
14
+
15
+ export interface Props extends Omit<
16
+ ButtonProps,
17
+ | "children"
18
+ | "onclick"
19
+ | "href"
20
+ | "target"
21
+ | "checked"
22
+ | "roleSwitch"
23
+ | "iconSwap"
24
+ | "x"
25
+ | "nav"
26
+ | "spinner"
27
+ | "spinnerOnly"
28
+ | "tooltip"
29
+ > {
30
+ /**
31
+ * The text to copy. A getter is resolved on click (sync or async) — use it for
32
+ * values that are expensive to build or only known at click time. Keep async
33
+ * getters fast: browsers gate clipboard writes on the user gesture.
34
+ */
35
+ text: CopyButtonText;
36
+ /**
37
+ * Visible label next to the icon. Without it the button is icon-only and gets
38
+ * an accessible name from `t("copy")`. Pass `true` for the localized default
39
+ * ("Copy").
40
+ */
41
+ label?: THC | true;
42
+ /** Label while in the "copied" state (default: `t("copied")`). Only with `label`. */
43
+ labelCopied?: THC;
44
+ /** Label while in the "error" state (default: `t("copy_failed")`). Only with `label`. */
45
+ labelError?: THC;
46
+ /** Idle icon as an svg/html string (default: copy icon). `false` renders no icons. */
47
+ icon?: string | false;
48
+ /** "copied" icon (default: check). `false` hides it; omitted follows `icon === false`. */
49
+ iconCopied?: string | false;
50
+ /** "error" icon (default: ×). `false` hides it; omitted follows `icon === false`. */
51
+ iconError?: string | false;
52
+ /**
53
+ * How long the copied/error feedback stays (ms, default 2000). `0` keeps it until
54
+ * the next click.
55
+ */
56
+ feedbackDuration?: number;
57
+ /** Button intent while "copied" (default `"success"`). `false` keeps `intent`. */
58
+ intentCopied?: IntentColorKey | false;
59
+ /** Button intent while in "error" (default `"destructive"`). `false` keeps `intent`. */
60
+ intentError?: IntentColorKey | false;
61
+ /** Fired after a successful copy, with the text that was copied. */
62
+ onCopied?: (text: string) => void;
63
+ /** Fired when the copy failed (clipboard unavailable, permission denied, getter threw). */
64
+ onError?: (error: unknown) => void;
65
+ /**
66
+ * Plain click hook, called before copying. Call `e.preventDefault()` to skip the
67
+ * copy for this click.
68
+ */
69
+ onclick?: (e: MouseEvent) => void;
70
+ /**
71
+ * Tooltip (the stuic `tooltip` action), state-aware: it reads "Copy" while idle
72
+ * and flips to "Copied" / "Copy failed" with the feedback — live, while it is
73
+ * showing. `true` uses those localized texts; a string replaces the idle text
74
+ * only; a `TooltipConfig` gives full control (position, class, …) and only
75
+ * falls back to the state text when it returns no `content`; `false` disables.
76
+ * Default: on for icon-only buttons, off when a `label` or `children` already
77
+ * carry the feedback.
78
+ */
79
+ tooltip?: boolean | string | TooltipConfig;
80
+ /** i18n translate function (see `createCopyButtonT`) */
81
+ t?: TranslateFn;
82
+ /** Override the whole button content; receives the current feedback state. */
83
+ children?: Snippet<[{ state: CopyButtonState; copied: boolean }]>;
84
+ /** Class for the icon wrapper */
85
+ classIcon?: string;
86
+ /** Class for the label wrapper */
87
+ classLabel?: string;
88
+ }
89
+ </script>
90
+
91
+ <script lang="ts">
92
+ import { twMerge } from "../../utils/tw-merge.js";
93
+ import { copyToClipboard } from "../../utils/copy-to-clipboard.js";
94
+ import { iconCopy, iconCheck, iconX } from "../../icons/index.js";
95
+ import Button from "../Button/Button.svelte";
96
+ import Thc from "../Thc/Thc.svelte";
97
+ import { t_default } from "./i18n.js";
98
+
99
+ let {
100
+ text,
101
+ label,
102
+ labelCopied,
103
+ labelError,
104
+ icon,
105
+ iconCopied,
106
+ iconError,
107
+ feedbackDuration = 2000,
108
+ intentCopied = "success",
109
+ intentError = "destructive",
110
+ onCopied,
111
+ onError,
112
+ onclick,
113
+ tooltip: tooltipProp,
114
+ t = t_default,
115
+ children,
116
+ intent,
117
+ variant = "ghost",
118
+ size = "sm",
119
+ disabled,
120
+ unstyled = false,
121
+ class: classProp,
122
+ classIcon: classIconProp,
123
+ classLabel: classLabelProp,
124
+ el = $bindable(),
125
+ ...rest
126
+ }: Props = $props();
127
+
128
+ let phase = $state<CopyButtonState>("idle");
129
+ // Live-region text. Cleared on reset so the next feedback is a fresh announcement.
130
+ let announcement = $state("");
131
+
132
+ let timer: ReturnType<typeof setTimeout> | undefined;
133
+ // Click sequence — a click whose async work finishes after a newer click has
134
+ // started must not overwrite that newer click's feedback.
135
+ let seq = 0;
136
+
137
+ function clearTimer() {
138
+ if (timer) clearTimeout(timer);
139
+ timer = undefined;
140
+ }
141
+
142
+ function setFeedback(next: Exclude<CopyButtonState, "idle">) {
143
+ clearTimer();
144
+ phase = next;
145
+ announcement = next === "copied" ? t("copied") : t("copy_failed");
146
+ if (feedbackDuration > 0) {
147
+ timer = setTimeout(() => {
148
+ timer = undefined;
149
+ phase = "idle";
150
+ announcement = "";
151
+ }, feedbackDuration);
152
+ }
153
+ }
154
+
155
+ async function handleClick(e: MouseEvent) {
156
+ onclick?.(e);
157
+ if (e.defaultPrevented) return;
158
+
159
+ const id = ++seq;
160
+ try {
161
+ // A plain string (or sync getter) is written synchronously within the click —
162
+ // Safari wants the clipboard write close to the user gesture.
163
+ const maybe = typeof text === "function" ? text() : text;
164
+ const value =
165
+ typeof (maybe as Promise<string>)?.then === "function"
166
+ ? await (maybe as Promise<string>)
167
+ : (maybe as string);
168
+ await copyToClipboard(value);
169
+ if (id !== seq) return;
170
+ setFeedback("copied");
171
+ onCopied?.(value);
172
+ } catch (err) {
173
+ if (id !== seq) return;
174
+ setFeedback("error");
175
+ onError?.(err);
176
+ }
177
+ }
178
+
179
+ // pending feedback reset must not fire into an unmounted component
180
+ $effect(() => clearTimer);
181
+
182
+ let copied = $derived(phase === "copied");
183
+
184
+ let _intent = $derived.by(() => {
185
+ if (phase === "copied") return intentCopied === false ? intent : intentCopied;
186
+ if (phase === "error") return intentError === false ? intent : intentError;
187
+ return intent;
188
+ });
189
+
190
+ let _label: THC | undefined = $derived.by(() => {
191
+ if (!label) return undefined;
192
+ if (phase === "copied") return labelCopied ?? t("copied");
193
+ if (phase === "error") return labelError ?? t("copy_failed");
194
+ return label === true ? t("copy") : label;
195
+ });
196
+
197
+ let _icon: string | undefined = $derived.by(() => {
198
+ const base = icon === false ? undefined : (icon ?? iconCopy());
199
+ if (phase === "copied") {
200
+ if (iconCopied === false) return undefined;
201
+ return iconCopied ?? (icon === false ? undefined : iconCheck());
202
+ }
203
+ if (phase === "error") {
204
+ if (iconError === false) return undefined;
205
+ return iconError ?? (icon === false ? undefined : iconX());
206
+ }
207
+ return base;
208
+ });
209
+
210
+ let _iconOnly = $derived(!children && !label);
211
+
212
+ // The localized text for the current phase — the icon-only accessible name and
213
+ // the default tooltip content.
214
+ let _stateText = $derived(
215
+ phase === "copied" ? t("copied") : phase === "error" ? t("copy_failed") : t("copy")
216
+ );
217
+
218
+ // One stable config closure (the tooltip action keeps the function it was mounted
219
+ // with and re-reads it reactively) that branches on the prop inside.
220
+ const _tooltipConfig: TooltipConfig = () => {
221
+ if (tooltipProp === false || (tooltipProp === undefined && !_iconOnly)) {
222
+ return { enabled: false };
223
+ }
224
+ if (typeof tooltipProp === "function") {
225
+ const cfg = tooltipProp();
226
+ return { ...cfg, content: cfg.content ?? _stateText };
227
+ }
228
+ return {
229
+ enabled: true,
230
+ content:
231
+ phase === "idle" && typeof tooltipProp === "string" ? tooltipProp : _stateText,
232
+ };
233
+ };
234
+
235
+ let _class = $derived(unstyled ? classProp : twMerge("stuic-copy-button", classProp));
236
+ let _classIcon = $derived(
237
+ unstyled ? classIconProp : twMerge("stuic-copy-button-icon", classIconProp)
238
+ );
239
+ let _classLabel = $derived(
240
+ unstyled ? classLabelProp : twMerge("stuic-copy-button-label", classLabelProp)
241
+ );
242
+ </script>
243
+
244
+ {#snippet content()}
245
+ {#if children}
246
+ {@render children({ state: phase, copied })}
247
+ {:else}
248
+ {#if _icon}
249
+ <span class={_classIcon} aria-hidden="true">{@html _icon}</span>
250
+ {/if}
251
+ {#if _label !== undefined}
252
+ <span class={_classLabel}><Thc thc={_label} /></span>
253
+ {/if}
254
+ {/if}
255
+ {/snippet}
256
+
257
+ <Button
258
+ bind:el
259
+ type="button"
260
+ class={_class}
261
+ intent={_intent}
262
+ {variant}
263
+ {size}
264
+ iconButton={_iconOnly}
265
+ {disabled}
266
+ {unstyled}
267
+ aria-label={_iconOnly ? _stateText : undefined}
268
+ tooltip={_tooltipConfig}
269
+ data-state={phase}
270
+ onclick={handleClick}
271
+ children={content}
272
+ {...rest}
273
+ />
274
+ <!-- Announces the outcome to assistive tech. A sibling (not inside the button) so the
275
+ announcement never leaks into the button's accessible name. -->
276
+ <span class="sr-only" role="status" aria-live="polite" aria-atomic="true">
277
+ {announcement}
278
+ </span>
@@ -0,0 +1,76 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { IntentColorKey } from "../../utils/design-tokens.js";
3
+ import type { THC } from "../Thc/Thc.svelte";
4
+ import type { TranslateFn } from "../../types.js";
5
+ import type { Props as ButtonProps } from "../Button/Button.svelte";
6
+ import type { TooltipConfig } from "../../actions/tooltip/tooltip.svelte.js";
7
+ /** Feedback phase the button is in. Exposed as `data-state` and to `children`. */
8
+ export type CopyButtonState = "idle" | "copied" | "error";
9
+ /** What to copy: a string, or a getter (sync or async) resolved on click. */
10
+ export type CopyButtonText = string | (() => string | Promise<string>);
11
+ export interface Props extends Omit<ButtonProps, "children" | "onclick" | "href" | "target" | "checked" | "roleSwitch" | "iconSwap" | "x" | "nav" | "spinner" | "spinnerOnly" | "tooltip"> {
12
+ /**
13
+ * The text to copy. A getter is resolved on click (sync or async) — use it for
14
+ * values that are expensive to build or only known at click time. Keep async
15
+ * getters fast: browsers gate clipboard writes on the user gesture.
16
+ */
17
+ text: CopyButtonText;
18
+ /**
19
+ * Visible label next to the icon. Without it the button is icon-only and gets
20
+ * an accessible name from `t("copy")`. Pass `true` for the localized default
21
+ * ("Copy").
22
+ */
23
+ label?: THC | true;
24
+ /** Label while in the "copied" state (default: `t("copied")`). Only with `label`. */
25
+ labelCopied?: THC;
26
+ /** Label while in the "error" state (default: `t("copy_failed")`). Only with `label`. */
27
+ labelError?: THC;
28
+ /** Idle icon as an svg/html string (default: copy icon). `false` renders no icons. */
29
+ icon?: string | false;
30
+ /** "copied" icon (default: check). `false` hides it; omitted follows `icon === false`. */
31
+ iconCopied?: string | false;
32
+ /** "error" icon (default: ×). `false` hides it; omitted follows `icon === false`. */
33
+ iconError?: string | false;
34
+ /**
35
+ * How long the copied/error feedback stays (ms, default 2000). `0` keeps it until
36
+ * the next click.
37
+ */
38
+ feedbackDuration?: number;
39
+ /** Button intent while "copied" (default `"success"`). `false` keeps `intent`. */
40
+ intentCopied?: IntentColorKey | false;
41
+ /** Button intent while in "error" (default `"destructive"`). `false` keeps `intent`. */
42
+ intentError?: IntentColorKey | false;
43
+ /** Fired after a successful copy, with the text that was copied. */
44
+ onCopied?: (text: string) => void;
45
+ /** Fired when the copy failed (clipboard unavailable, permission denied, getter threw). */
46
+ onError?: (error: unknown) => void;
47
+ /**
48
+ * Plain click hook, called before copying. Call `e.preventDefault()` to skip the
49
+ * copy for this click.
50
+ */
51
+ onclick?: (e: MouseEvent) => void;
52
+ /**
53
+ * Tooltip (the stuic `tooltip` action), state-aware: it reads "Copy" while idle
54
+ * and flips to "Copied" / "Copy failed" with the feedback — live, while it is
55
+ * showing. `true` uses those localized texts; a string replaces the idle text
56
+ * only; a `TooltipConfig` gives full control (position, class, …) and only
57
+ * falls back to the state text when it returns no `content`; `false` disables.
58
+ * Default: on for icon-only buttons, off when a `label` or `children` already
59
+ * carry the feedback.
60
+ */
61
+ tooltip?: boolean | string | TooltipConfig;
62
+ /** i18n translate function (see `createCopyButtonT`) */
63
+ t?: TranslateFn;
64
+ /** Override the whole button content; receives the current feedback state. */
65
+ children?: Snippet<[{
66
+ state: CopyButtonState;
67
+ copied: boolean;
68
+ }]>;
69
+ /** Class for the icon wrapper */
70
+ classIcon?: string;
71
+ /** Class for the label wrapper */
72
+ classLabel?: string;
73
+ }
74
+ declare const CopyButton: import("svelte").Component<Props, {}, "el">;
75
+ type CopyButton = ReturnType<typeof CopyButton>;
76
+ export default CopyButton;
@@ -0,0 +1,241 @@
1
+ # CopyButton
2
+
3
+ Click-to-copy button with built-in feedback. It **is a `Button`** (same `intent`,
4
+ `variant`, `size` and theming) that writes `text` to the clipboard on click, flips to a
5
+ "copied" state (check icon, success intent, localized label/name, screen-reader
6
+ announcement) for a moment, and reports the outcome through `onCopied` / `onError`.
7
+
8
+ The clipboard write itself is a standalone utility — `copyToClipboard()` — so the same
9
+ logic can be used programmatically, without the button.
10
+
11
+ ## Props
12
+
13
+ Everything `Button` accepts (except the toggle/link/spinner extras) plus:
14
+
15
+ | Prop | Type | Default | Description |
16
+ | ------------------ | ------------------------------------------- | ------------------ | ----------------------------------------------------------------------------------------------- |
17
+ | `text` | `string \| () => string \| Promise<string>` | required | What to copy. A getter is resolved on click (sync or async) |
18
+ | `label` | `THC \| true` | - | Visible label. Omit for an icon-only button (accessible name from `t("copy")`); `true` = "Copy" |
19
+ | `labelCopied` | `THC` | `t("copied")` | Label while copied (only with `label`) |
20
+ | `labelError` | `THC` | `t("copy_failed")` | Label while in error (only with `label`) |
21
+ | `icon` | `string \| false` | copy icon | Idle icon (svg/html string). `false` = no icons at all |
22
+ | `iconCopied` | `string \| false` | check icon | Copied icon |
23
+ | `iconError` | `string \| false` | × icon | Error icon |
24
+ | `feedbackDuration` | `number` | `2000` | How long (ms) the copied/error feedback stays. `0` = until the next click |
25
+ | `intentCopied` | `IntentColorKey \| false` | `"success"` | Button intent while copied. `false` keeps `intent` |
26
+ | `intentError` | `IntentColorKey \| false` | `"destructive"` | Button intent while in error. `false` keeps `intent` |
27
+ | `onCopied` | `(text: string) => void` | - | Fired after a successful copy with the copied text |
28
+ | `onError` | `(error: unknown) => void` | - | Fired when the copy failed (clipboard unavailable / denied, getter threw) |
29
+ | `onclick` | `(e: MouseEvent) => void` | - | Runs before copying; `e.preventDefault()` skips the copy |
30
+ | `tooltip` | `boolean \| string \| TooltipConfig` | icon-only: `true` | State-aware tooltip ("Copy" → "Copied"); see below |
31
+ | `t` | `TranslateFn` | English | i18n (see below) |
32
+ | `variant` | `ButtonVariant` | `"ghost"` | Button variant (CopyButton's default differs from Button's `"solid"`) |
33
+ | `size` | `ButtonSize` | `"sm"` | Button size (CopyButton's default differs from Button's `"md"`) |
34
+ | `unstyled` | `boolean` | `false` | Skip all default styling |
35
+ | `class` | `string` | - | Additional CSS classes (merged via twMerge) |
36
+ | `classIcon` | `string` | - | Class for the icon wrapper |
37
+ | `classLabel` | `string` | - | Class for the label wrapper |
38
+ | `el` | `HTMLElement` | - | Button element reference (bindable) |
39
+
40
+ ## Snippet Props
41
+
42
+ | Snippet | Description |
43
+ | ---------- | ---------------------------------------------------------------------------------------------- |
44
+ | `children` | Override the whole button content. Receives `{ state: "idle" \| "copied" \| "error", copied }` |
45
+
46
+ ## Usage
47
+
48
+ ### Icon-only (default)
49
+
50
+ ```svelte
51
+ <script lang="ts">
52
+ import { CopyButton } from "@marianmeres/stuic";
53
+ const apiKey = "sk-live-…";
54
+ </script>
55
+
56
+ <code>{apiKey}</code>
57
+ <CopyButton text={apiKey} />
58
+ ```
59
+
60
+ ### With a label
61
+
62
+ ```svelte
63
+ <!-- "Copy" → "Copied" (localized defaults) -->
64
+ <CopyButton text={url} label />
65
+
66
+ <!-- custom labels, regular button look -->
67
+ <CopyButton
68
+ text={url}
69
+ label="Copy link"
70
+ labelCopied="Link copied!"
71
+ variant="outline"
72
+ size="md"
73
+ />
74
+ ```
75
+
76
+ ### Tooltip
77
+
78
+ Icon-only buttons get a tooltip out of the box (the stuic `tooltip` action): it reads
79
+ "Copy" on hover and flips to "Copied" / "Copy failed" live, while it is showing. With a
80
+ `label` or `children` the visible text already carries the feedback, so the tooltip is
81
+ off unless asked for.
82
+
83
+ ```svelte
84
+ <!-- default for icon-only: "Copy" → "Copied" -->
85
+ <CopyButton text={apiKey} />
86
+
87
+ <!-- custom idle text; the feedback texts stay localized -->
88
+ <CopyButton text={apiKey} tooltip="Copy API key" />
89
+
90
+ <!-- labeled button, tooltip forced on -->
91
+ <CopyButton text={url} label="Copy link" tooltip />
92
+
93
+ <!-- full control: any TooltipConfig; without `content` it still gets the state text -->
94
+ <CopyButton text={apiKey} tooltip={() => ({ position: "bottom", class: "font-mono" })} />
95
+
96
+ <!-- off -->
97
+ <CopyButton text={apiKey} tooltip={false} />
98
+ ```
99
+
100
+ Tooltips follow the global `setTooltipsEnabled()` switch (auto-off on touch devices) and
101
+ need CSS anchor positioning in the browser, like every stuic tooltip.
102
+
103
+ ### Notify when copied
104
+
105
+ The component announces the result to assistive tech by itself (a visually hidden live
106
+ region). For a visible app-level notification, hook `onCopied` / `onError` into your
107
+ notifications stack:
108
+
109
+ ```svelte
110
+ <script lang="ts">
111
+ import { CopyButton, Notifications, NotificationsStack } from "@marianmeres/stuic";
112
+ const notifications = new NotificationsStack();
113
+ </script>
114
+
115
+ <CopyButton
116
+ text={shareUrl}
117
+ label="Copy link"
118
+ onCopied={() => notifications.success("Link copied to clipboard")}
119
+ onError={() => notifications.error("Could not copy — please copy the link manually")}
120
+ />
121
+
122
+ <Notifications {notifications} />
123
+ ```
124
+
125
+ ### Lazy / async text
126
+
127
+ Pass a getter when the value is expensive to build or only known at click time. Keep
128
+ it fast — browsers tie clipboard writes to the user gesture, and Safari is strict
129
+ about it.
130
+
131
+ ```svelte
132
+ <CopyButton text={() => JSON.stringify(exportData(), null, 2)} label="Copy JSON" />
133
+
134
+ <CopyButton
135
+ text={async () => (await fetch("/api/share-link")).text()}
136
+ label="Copy share link"
137
+ />
138
+ ```
139
+
140
+ ### Feedback tuning
141
+
142
+ ```svelte
143
+ <!-- keep the base intent while copied, and stay "copied" until the next click -->
144
+ <CopyButton
145
+ text={x}
146
+ intent="primary"
147
+ variant="solid"
148
+ intentCopied={false}
149
+ feedbackDuration={0}
150
+ label
151
+ />
152
+
153
+ <!-- no icons, text only -->
154
+ <CopyButton text={x} icon={false} label="Copy" labelCopied="✓ Copied" />
155
+ ```
156
+
157
+ ### Custom content
158
+
159
+ ```svelte
160
+ <CopyButton text={x} variant="soft">
161
+ {#snippet children({ state, copied })}
162
+ {copied ? "🎉 Got it" : state === "error" ? "😬 Nope" : "Grab it"}
163
+ {/snippet}
164
+ </CopyButton>
165
+ ```
166
+
167
+ ### Programmatic copy
168
+
169
+ `copyToClipboard(text)` is the same write the button performs: the async Clipboard
170
+ API with a `document.execCommand("copy")` fallback (insecure contexts, older
171
+ browsers, a rejected async write). It resolves once the text is on the clipboard and
172
+ rejects only when every path failed. Call it from a user gesture.
173
+
174
+ ```ts
175
+ import { copyToClipboard, isCopyToClipboardSupported } from "@marianmeres/stuic";
176
+
177
+ async function share() {
178
+ try {
179
+ await copyToClipboard(location.href);
180
+ notifications.success("Copied");
181
+ } catch (e) {
182
+ notifications.error("Copy failed");
183
+ }
184
+ }
185
+
186
+ // decide up-front whether to offer copying at all (always false during SSR)
187
+ if (isCopyToClipboardSupported()) {
188
+ /* … */
189
+ }
190
+ ```
191
+
192
+ ### i18n
193
+
194
+ English is built in; Slovak is bundled and opt-in. Any partial catalog works — missing
195
+ keys fall back to English.
196
+
197
+ ```svelte
198
+ <script lang="ts">
199
+ import {
200
+ CopyButton,
201
+ createCopyButtonT,
202
+ COPY_BUTTON_MESSAGES_SK,
203
+ } from "@marianmeres/stuic";
204
+ const t = createCopyButtonT(COPY_BUTTON_MESSAGES_SK);
205
+ </script>
206
+
207
+ <CopyButton text={x} {t} />
208
+ ```
209
+
210
+ | Key | English | Used for |
211
+ | ------------- | ------------- | --------------------------------------------------- |
212
+ | `copy` | `Copy` | Idle accessible name (icon-only) and `label={true}` |
213
+ | `copied` | `Copied` | Copied name/label and the live announcement |
214
+ | `copy_failed` | `Copy failed` | Error name/label and the live announcement |
215
+
216
+ ## Accessibility
217
+
218
+ - Icon-only mode gets an `aria-label` that tracks the state (`Copy` → `Copied` /
219
+ `Copy failed`) plus a tooltip with the same text. With a `label`, the visible text is
220
+ the name.
221
+ - A visually hidden `role="status"` live region next to the button announces the
222
+ outcome, so screen-reader users hear "Copied" without the icon.
223
+ - The feedback intent swap is a color cue only — the icon and the text change too.
224
+ - The icon wrapper is `aria-hidden`.
225
+
226
+ ## CSS Variables
227
+
228
+ The button surface (colors, padding, radius, focus ring) themes via the
229
+ `--stuic-button-*` tokens. CopyButton adds:
230
+
231
+ | Variable | Default | Description |
232
+ | --------------------------------------- | -------- | ---------------------------------------------------------------- |
233
+ | `--stuic-copy-button-icon-size` | `1.25em` | Icon size (any `svg` inside the icon wrapper) |
234
+ | `--stuic-copy-button-icon-pop-duration` | `200ms` | Length of the icon "pop" when the state changes |
235
+ | `--stuic-copy-button-icon-pop-scale` | `0.6` | Scale the pop starts from (`prefers-reduced-motion` disables it) |
236
+
237
+ ## Data Attributes
238
+
239
+ - `data-state` - `"idle" | "copied" | "error"` (kept even when `unstyled`)
240
+ - plus everything `Button` sets (`data-variant`, `data-size`, `data-intent`,
241
+ `data-icon-button` in icon-only mode, …)
@@ -0,0 +1,17 @@
1
+ import type { CopyButtonMessages } from "./i18n.js";
2
+ /**
3
+ * Slovak message catalog for `CopyButton`. Opt-in — English stays the built-in
4
+ * default, and this module is only pulled into a bundle when it is actually imported
5
+ * (the component itself never references it).
6
+ *
7
+ * @example
8
+ * ```svelte
9
+ * <script>
10
+ * import { CopyButton, createCopyButtonT, COPY_BUTTON_MESSAGES_SK } from "@marianmeres/stuic";
11
+ * const t = createCopyButtonT(COPY_BUTTON_MESSAGES_SK);
12
+ * </script>
13
+ *
14
+ * <CopyButton text={url} {t} />
15
+ * ```
16
+ */
17
+ export declare const COPY_BUTTON_MESSAGES_SK: CopyButtonMessages;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Slovak message catalog for `CopyButton`. Opt-in — English stays the built-in
3
+ * default, and this module is only pulled into a bundle when it is actually imported
4
+ * (the component itself never references it).
5
+ *
6
+ * @example
7
+ * ```svelte
8
+ * <script>
9
+ * import { CopyButton, createCopyButtonT, COPY_BUTTON_MESSAGES_SK } from "@marianmeres/stuic";
10
+ * const t = createCopyButtonT(COPY_BUTTON_MESSAGES_SK);
11
+ * </script>
12
+ *
13
+ * <CopyButton text={url} {t} />
14
+ * ```
15
+ */
16
+ export const COPY_BUTTON_MESSAGES_SK = {
17
+ copy: "Kopírovať",
18
+ copied: "Skopírované",
19
+ copy_failed: "Kopírovanie zlyhalo",
20
+ };