@signal9/era-ui 2.14.0 → 2.16.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.
@@ -16,6 +16,14 @@
16
16
  "sections": [],
17
17
  "file": "surfaces.md"
18
18
  },
19
+ {
20
+ "slug": "measurements",
21
+ "title": "Measurements",
22
+ "summary": "Every component measured as it renders — gaps, radii, and the laws they must obey.",
23
+ "tokenEstimate": 25,
24
+ "sections": [],
25
+ "file": "measurements.md"
26
+ },
19
27
  {
20
28
  "slug": "text",
21
29
  "title": "Text",
@@ -166,6 +174,17 @@
166
174
  ],
167
175
  "file": "code-block.md"
168
176
  },
177
+ {
178
+ "slug": "copy-button",
179
+ "title": "Copy Button",
180
+ "summary": "An icon button that copies text and cross-fades to a result glyph.",
181
+ "tokenEstimate": 257,
182
+ "sections": [
183
+ "Import",
184
+ "Props"
185
+ ],
186
+ "file": "copy-button.md"
187
+ },
169
188
  {
170
189
  "slug": "kv",
171
190
  "title": "KV",
@@ -0,0 +1,3 @@
1
+ # Measurements
2
+
3
+ Every component measured as it renders — gaps, radii, and the laws they must obey.
@@ -2,9 +2,7 @@
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLAttributes } from 'svelte/elements';
4
4
  import { cn } from '../../utils/index.js';
5
- import { Button } from '../button';
6
- import Copy from '@lucide/svelte/icons/copy';
7
- import Check from '@lucide/svelte/icons/check';
5
+ import { CopyButton } from '../copy-button/index.js';
8
6
 
9
7
  let {
10
8
  ref = $bindable(null),
@@ -20,16 +18,9 @@
20
18
  children?: Snippet;
21
19
  } = $props();
22
20
 
23
- let copied = $state(false);
24
-
25
- function copy() {
26
- const text = code ?? ref?.querySelector('code')?.textContent ?? '';
27
- navigator.clipboard.writeText(text);
28
- copied = true;
29
- setTimeout(() => {
30
- copied = false;
31
- }, 1500);
32
- }
21
+ // Resolved at click time: with a `children` snippet the text only exists in
22
+ // the DOM, so it can't be read during render.
23
+ const copyText = () => code ?? ref?.querySelector('code')?.textContent ?? '';
33
24
  </script>
34
25
 
35
26
  <div
@@ -42,17 +33,9 @@
42
33
  >{#if children}{@render children()}{:else}{code}{/if}</code
43
34
  ></pre>
44
35
  {#if copyable}
45
- <Button
46
- icon
47
- size="sm"
36
+ <CopyButton
37
+ text={copyText}
48
38
  class="absolute top-[calc(var(--era-inset-md)+(var(--era-text)*1.625-var(--era-h-sm))/2)] right-[calc(var(--era-inset-md)+(var(--era-text)*1.625-var(--era-h-sm))/2)]"
49
- onclick={copy}
50
- >
51
- {#if copied}
52
- <Check class="size-(--era-h-xs)" />
53
- {:else}
54
- <Copy class="size-(--era-h-xs)" />
55
- {/if}
56
- </Button>
39
+ />
57
40
  {/if}
58
41
  </div>
@@ -0,0 +1,85 @@
1
+ <script lang="ts">
2
+ import type { HTMLButtonAttributes } from 'svelte/elements';
3
+ import Copy from '@lucide/svelte/icons/copy';
4
+ import Check from '@lucide/svelte/icons/check';
5
+ import X from '@lucide/svelte/icons/x';
6
+ import { Button, type ButtonVariants } from '../button/index.js';
7
+ import { cn } from '../../utils/index.js';
8
+
9
+ let {
10
+ ref = $bindable(null),
11
+ text,
12
+ onCopy,
13
+ resetDelay = 1500,
14
+ label = 'Copy',
15
+ copiedLabel = 'Copied',
16
+ size = 'sm',
17
+ class: className,
18
+ ...restProps
19
+ // Typed off the native button rather than bits-ui's Button.RootProps: the
20
+ // latter is a union whose Omit<> exceeds TS's representable-union limit
21
+ // (the same reason ButtonVariants is a standalone interface).
22
+ }: Omit<HTMLButtonAttributes, 'onclick' | 'children'> & {
23
+ ref?: HTMLElement | null;
24
+ } & ButtonVariants & {
25
+ /**
26
+ * The string written to the clipboard. Pass a function when the value
27
+ * is only knowable at click time (e.g. text read out of the DOM) — it
28
+ * is resolved on each click, not at render.
29
+ */
30
+ text: string | (() => string);
31
+ /** Fires after the write settles, with the outcome. */
32
+ onCopy?: (status: 'success' | 'failure') => void;
33
+ /** How long the result icon holds before falling back to idle. */
34
+ resetDelay?: number;
35
+ /** Accessible name in the idle and failure states. */
36
+ label?: string;
37
+ /** Accessible name while the success icon is showing. */
38
+ copiedLabel?: string;
39
+ } = $props();
40
+
41
+ let status = $state<'idle' | 'success' | 'failure'>('idle');
42
+ let timer: ReturnType<typeof setTimeout> | undefined;
43
+
44
+ // The reset timer outlives the click that armed it, so it has to be torn
45
+ // down on destroy — a handler-only clear fires on an unmounted component.
46
+ $effect(() => () => clearTimeout(timer));
47
+
48
+ async function copy() {
49
+ try {
50
+ await navigator.clipboard.writeText(typeof text === 'function' ? text() : text);
51
+ status = 'success';
52
+ } catch {
53
+ status = 'failure';
54
+ }
55
+ onCopy?.(status);
56
+ clearTimeout(timer);
57
+ timer = setTimeout(() => (status = 'idle'), resetDelay);
58
+ }
59
+
60
+ // All three icons stay mounted and stack on one another, so the swap
61
+ // cross-fades in BOTH directions — an icon that unmounts can only animate
62
+ // its enter. The entering glyph scales up from 0.25 and unblurs while the
63
+ // leaving one does the reverse; timing rides the motion axis, so the swap
64
+ // snaps at instant, is decisive at normal, and overshoots at extra.
65
+ const iconBase =
66
+ 'absolute inset-0 size-(--era-h-xs) transition-[opacity,scale,filter] duration-(--era-duration) ease-(--era-ease)';
67
+ const iconIn = 'scale-100 opacity-100 blur-none';
68
+ const iconOut = 'scale-25 opacity-0 blur-xs';
69
+ </script>
70
+
71
+ <Button
72
+ bind:ref
73
+ icon
74
+ {size}
75
+ aria-label={status === 'success' ? copiedLabel : label}
76
+ class={className}
77
+ onclick={copy}
78
+ {...restProps}
79
+ >
80
+ <span class="relative block size-(--era-h-xs) shrink-0">
81
+ <Copy class={cn(iconBase, status === 'idle' ? iconIn : iconOut)} />
82
+ <Check class={cn(iconBase, 'text-success', status === 'success' ? iconIn : iconOut)} />
83
+ <X class={cn(iconBase, 'text-destructive', status === 'failure' ? iconIn : iconOut)} />
84
+ </span>
85
+ </Button>
@@ -0,0 +1,23 @@
1
+ import type { HTMLButtonAttributes } from 'svelte/elements';
2
+ import { type ButtonVariants } from '../button/index.js';
3
+ type $$ComponentProps = Omit<HTMLButtonAttributes, 'onclick' | 'children'> & {
4
+ ref?: HTMLElement | null;
5
+ } & ButtonVariants & {
6
+ /**
7
+ * The string written to the clipboard. Pass a function when the value
8
+ * is only knowable at click time (e.g. text read out of the DOM) — it
9
+ * is resolved on each click, not at render.
10
+ */
11
+ text: string | (() => string);
12
+ /** Fires after the write settles, with the outcome. */
13
+ onCopy?: (status: 'success' | 'failure') => void;
14
+ /** How long the result icon holds before falling back to idle. */
15
+ resetDelay?: number;
16
+ /** Accessible name in the idle and failure states. */
17
+ label?: string;
18
+ /** Accessible name while the success icon is showing. */
19
+ copiedLabel?: string;
20
+ };
21
+ declare const CopyButton: import("svelte").Component<$$ComponentProps, {}, "ref">;
22
+ type CopyButton = ReturnType<typeof CopyButton>;
23
+ export default CopyButton;
@@ -0,0 +1 @@
1
+ export { default as CopyButton } from './copy-button.svelte';
@@ -0,0 +1 @@
1
+ export { default as CopyButton } from './copy-button.svelte';
@@ -6,6 +6,7 @@ export { Badge } from './badge/index.js';
6
6
  export * as Calendar from './calendar/index.js';
7
7
  export { Chip } from './chip/index.js';
8
8
  export { CodeBlock } from './code-block/index.js';
9
+ export { CopyButton } from './copy-button/index.js';
9
10
  export * as Checkbox from './checkbox/index.js';
10
11
  export * as Combobox from './combobox/index.js';
11
12
  export * as Collapsible from './collapsible/index.js';
package/dist/ui/index.js CHANGED
@@ -6,6 +6,7 @@ export { Badge } from './badge/index.js';
6
6
  export * as Calendar from './calendar/index.js';
7
7
  export { Chip } from './chip/index.js';
8
8
  export { CodeBlock } from './code-block/index.js';
9
+ export { CopyButton } from './copy-button/index.js';
9
10
  export * as Checkbox from './checkbox/index.js';
10
11
  export * as Combobox from './combobox/index.js';
11
12
  export * as Collapsible from './collapsible/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signal9/era-ui",
3
- "version": "2.14.0",
3
+ "version": "2.16.0",
4
4
  "scripts": {
5
5
  "dev": "vite dev --host",
6
6
  "build": "vite build && npm run prepack",
@@ -1,42 +0,0 @@
1
- <script lang="ts">
2
- import Copy from '@lucide/svelte/icons/copy';
3
- import Check from '@lucide/svelte/icons/check';
4
- import X from '@lucide/svelte/icons/x';
5
- import { Button } from '../ui/button/index.js';
6
- import { cn } from '../utils/index.js';
7
-
8
- let {
9
- text,
10
- onCopy,
11
- class: className
12
- }: {
13
- text: string;
14
- onCopy?: (status: 'success' | 'failure') => void;
15
- class?: string;
16
- } = $props();
17
-
18
- let status = $state<'idle' | 'success' | 'failure'>('idle');
19
- let timer: ReturnType<typeof setTimeout>;
20
-
21
- async function copy() {
22
- try {
23
- await navigator.clipboard.writeText(text);
24
- status = 'success';
25
- } catch {
26
- status = 'failure';
27
- }
28
- onCopy?.(status as 'success' | 'failure');
29
- clearTimeout(timer);
30
- timer = setTimeout(() => (status = 'idle'), 1500);
31
- }
32
- </script>
33
-
34
- <Button icon size="icon" aria-label="Copy" class={cn(className)} onclick={copy}>
35
- {#if status === 'success'}
36
- <Check class="size-(--era-h-xs) text-success" />
37
- {:else if status === 'failure'}
38
- <X class="size-(--era-h-xs) text-destructive" />
39
- {:else}
40
- <Copy class="size-(--era-h-xs)" />
41
- {/if}
42
- </Button>
@@ -1,8 +0,0 @@
1
- type $$ComponentProps = {
2
- text: string;
3
- onCopy?: (status: 'success' | 'failure') => void;
4
- class?: string;
5
- };
6
- declare const CopyButton: import("svelte").Component<$$ComponentProps, {}, "">;
7
- type CopyButton = ReturnType<typeof CopyButton>;
8
- export default CopyButton;