@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.
- package/AGENTS.md +6 -6
- package/dist/README.md +1 -1
- package/dist/attachments/index.d.ts +1 -0
- package/dist/attachments/index.js +1 -0
- package/dist/attachments/long-press.d.ts +43 -0
- package/dist/attachments/long-press.js +107 -0
- package/dist/components/Breadcrumbs/Breadcrumbs.svelte +198 -0
- package/dist/components/Breadcrumbs/Breadcrumbs.svelte.d.ts +60 -0
- package/dist/components/Breadcrumbs/README.md +174 -0
- package/dist/components/Breadcrumbs/i18n-sk.d.ts +17 -0
- package/dist/components/Breadcrumbs/i18n-sk.js +19 -0
- package/dist/components/Breadcrumbs/i18n.d.ts +31 -0
- package/dist/components/Breadcrumbs/i18n.js +40 -0
- package/dist/components/Breadcrumbs/index.css +84 -0
- package/dist/components/Breadcrumbs/index.d.ts +4 -0
- package/dist/components/Breadcrumbs/index.js +4 -0
- package/dist/components/Breadcrumbs/json-ld.d.ts +58 -0
- package/dist/components/Breadcrumbs/json-ld.js +56 -0
- package/dist/components/Collapsible/Collapsible.svelte +84 -15
- package/dist/components/Collapsible/Collapsible.svelte.d.ts +29 -2
- package/dist/components/Collapsible/README.md +82 -23
- package/dist/components/Collapsible/index.css +38 -0
- package/dist/components/Collapsible/index.d.ts +1 -1
- package/dist/components/ContextMenu/ContextMenu.svelte +302 -0
- package/dist/components/ContextMenu/ContextMenu.svelte.d.ts +94 -0
- package/dist/components/ContextMenu/README.md +159 -0
- package/dist/components/ContextMenu/i18n-sk.d.ts +17 -0
- package/dist/components/ContextMenu/i18n-sk.js +18 -0
- package/dist/components/ContextMenu/i18n.d.ts +30 -0
- package/dist/components/ContextMenu/i18n.js +38 -0
- package/dist/components/ContextMenu/index.css +26 -0
- package/dist/components/ContextMenu/index.d.ts +3 -0
- package/dist/components/ContextMenu/index.js +3 -0
- package/dist/components/CopyButton/CopyButton.svelte +278 -0
- package/dist/components/CopyButton/CopyButton.svelte.d.ts +76 -0
- package/dist/components/CopyButton/README.md +241 -0
- package/dist/components/CopyButton/i18n-sk.d.ts +17 -0
- package/dist/components/CopyButton/i18n-sk.js +20 -0
- package/dist/components/CopyButton/i18n.d.ts +35 -0
- package/dist/components/CopyButton/i18n.js +43 -0
- package/dist/components/CopyButton/index.css +67 -0
- package/dist/components/CopyButton/index.d.ts +3 -0
- package/dist/components/CopyButton/index.js +3 -0
- package/dist/components/DataTable/DataTable.svelte +41 -34
- package/dist/components/DataTable/README.md +45 -0
- package/dist/components/DataTable/i18n-sk.js +1 -0
- package/dist/components/DataTable/i18n.d.ts +1 -0
- package/dist/components/DataTable/i18n.js +3 -0
- package/dist/components/DataTable/index.css +41 -3
- package/dist/components/EmptyState/EmptyState.svelte +121 -0
- package/dist/components/EmptyState/EmptyState.svelte.d.ts +40 -0
- package/dist/components/EmptyState/README.md +188 -0
- package/dist/components/EmptyState/index.css +154 -0
- package/dist/components/EmptyState/index.d.ts +1 -0
- package/dist/components/EmptyState/index.js +1 -0
- package/dist/components/Input/FieldOptions.svelte +92 -2
- package/dist/components/Input/FieldOptions.svelte.d.ts +13 -0
- package/dist/components/Input/README.md +66 -31
- package/dist/components/Input/_internal/FieldLikeChips.svelte +286 -0
- package/dist/components/Input/_internal/FieldLikeChips.svelte.d.ts +65 -0
- package/dist/components/Input/index.css +58 -0
- package/dist/components/Pagination/Pagination.svelte +250 -0
- package/dist/components/Pagination/Pagination.svelte.d.ts +69 -0
- package/dist/components/Pagination/README.md +152 -0
- package/dist/components/Pagination/i18n-sk.d.ts +17 -0
- package/dist/components/Pagination/i18n-sk.js +24 -0
- package/dist/components/Pagination/i18n.d.ts +41 -0
- package/dist/components/Pagination/i18n.js +49 -0
- package/dist/components/Pagination/index.css +50 -0
- package/dist/components/Pagination/index.d.ts +4 -0
- package/dist/components/Pagination/index.js +4 -0
- package/dist/components/Pagination/pagination-range.d.ts +21 -0
- package/dist/components/Pagination/pagination-range.js +41 -0
- package/dist/components/Pill/Pill.svelte +8 -2
- package/dist/components/Pill/Pill.svelte.d.ts +2 -0
- package/dist/components/Pill/README.md +35 -22
- package/dist/components/Pill/index.css +57 -18
- package/dist/components/Stat/README.md +157 -0
- package/dist/components/Stat/Stat.svelte +226 -0
- package/dist/components/Stat/Stat.svelte.d.ts +54 -0
- package/dist/components/Stat/index.css +217 -0
- package/dist/components/Stat/index.d.ts +1 -0
- package/dist/components/Stat/index.js +1 -0
- package/dist/components/Stepper/README.md +181 -0
- package/dist/components/Stepper/Stepper.svelte +240 -0
- package/dist/components/Stepper/Stepper.svelte.d.ts +86 -0
- package/dist/components/Stepper/i18n-sk.d.ts +17 -0
- package/dist/components/Stepper/i18n-sk.js +21 -0
- package/dist/components/Stepper/i18n.d.ts +35 -0
- package/dist/components/Stepper/i18n.js +43 -0
- package/dist/components/Stepper/index.css +302 -0
- package/dist/components/Stepper/index.d.ts +3 -0
- package/dist/components/Stepper/index.js +3 -0
- package/dist/components/TrendChart/README.md +123 -0
- package/dist/components/TrendChart/TrendChart.svelte +114 -0
- package/dist/components/TrendChart/TrendChart.svelte.d.ts +36 -0
- package/dist/components/TrendChart/index.css +51 -0
- package/dist/components/TrendChart/index.d.ts +1 -0
- package/dist/components/TrendChart/index.js +1 -0
- package/dist/icons/index.d.ts +4 -0
- package/dist/icons/index.js +4 -0
- package/dist/index.css +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +12 -0
- package/dist/utils/copy-to-clipboard.d.ts +43 -0
- package/dist/utils/copy-to-clipboard.js +102 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/docs/architecture.md +1 -1
- package/docs/domains/attachments.md +41 -4
- package/docs/domains/components.md +280 -13
- package/docs/domains/utils.md +8 -6
- package/docs/maybe-todo.md +132 -0
- package/docs/upgrading.md +6 -0
- 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,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
|
+
};
|