svelte-incant 0.1.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 (157) hide show
  1. package/README.md +75 -0
  2. package/dist/attachment.svelte.d.ts +8 -0
  3. package/dist/attachment.svelte.js +93 -0
  4. package/dist/combobox-example.svelte +155 -0
  5. package/dist/combobox-example.svelte.d.ts +3 -0
  6. package/dist/components/CodeBlock.svelte +213 -0
  7. package/dist/components/CodeBlock.svelte.d.ts +12 -0
  8. package/dist/components/header.svelte +155 -0
  9. package/dist/components/header.svelte.d.ts +3 -0
  10. package/dist/components/kbds.svelte +31 -0
  11. package/dist/components/kbds.svelte.d.ts +7 -0
  12. package/dist/components/ui/badge/badge.svelte +49 -0
  13. package/dist/components/ui/badge/badge.svelte.d.ts +32 -0
  14. package/dist/components/ui/badge/index.d.ts +2 -0
  15. package/dist/components/ui/badge/index.js +2 -0
  16. package/dist/components/ui/button/button.svelte +82 -0
  17. package/dist/components/ui/button/button.svelte.d.ts +64 -0
  18. package/dist/components/ui/button/index.d.ts +2 -0
  19. package/dist/components/ui/button/index.js +4 -0
  20. package/dist/components/ui/card/card-action.svelte +20 -0
  21. package/dist/components/ui/card/card-action.svelte.d.ts +5 -0
  22. package/dist/components/ui/card/card-content.svelte +15 -0
  23. package/dist/components/ui/card/card-content.svelte.d.ts +5 -0
  24. package/dist/components/ui/card/card-description.svelte +20 -0
  25. package/dist/components/ui/card/card-description.svelte.d.ts +5 -0
  26. package/dist/components/ui/card/card-footer.svelte +20 -0
  27. package/dist/components/ui/card/card-footer.svelte.d.ts +5 -0
  28. package/dist/components/ui/card/card-header.svelte +23 -0
  29. package/dist/components/ui/card/card-header.svelte.d.ts +5 -0
  30. package/dist/components/ui/card/card-title.svelte +20 -0
  31. package/dist/components/ui/card/card-title.svelte.d.ts +5 -0
  32. package/dist/components/ui/card/card.svelte +23 -0
  33. package/dist/components/ui/card/card.svelte.d.ts +5 -0
  34. package/dist/components/ui/card/index.d.ts +8 -0
  35. package/dist/components/ui/card/index.js +10 -0
  36. package/dist/components/ui/command/command-dialog.svelte +40 -0
  37. package/dist/components/ui/command/command-dialog.svelte.d.ts +12 -0
  38. package/dist/components/ui/command/command-empty.svelte +17 -0
  39. package/dist/components/ui/command/command-empty.svelte.d.ts +4 -0
  40. package/dist/components/ui/command/command-group.svelte +30 -0
  41. package/dist/components/ui/command/command-group.svelte.d.ts +7 -0
  42. package/dist/components/ui/command/command-input.svelte +26 -0
  43. package/dist/components/ui/command/command-input.svelte.d.ts +4 -0
  44. package/dist/components/ui/command/command-item.svelte +20 -0
  45. package/dist/components/ui/command/command-item.svelte.d.ts +4 -0
  46. package/dist/components/ui/command/command-link-item.svelte +20 -0
  47. package/dist/components/ui/command/command-link-item.svelte.d.ts +4 -0
  48. package/dist/components/ui/command/command-list.svelte +17 -0
  49. package/dist/components/ui/command/command-list.svelte.d.ts +4 -0
  50. package/dist/components/ui/command/command-loading.svelte +7 -0
  51. package/dist/components/ui/command/command-loading.svelte.d.ts +4 -0
  52. package/dist/components/ui/command/command-separator.svelte +17 -0
  53. package/dist/components/ui/command/command-separator.svelte.d.ts +4 -0
  54. package/dist/components/ui/command/command-shortcut.svelte +20 -0
  55. package/dist/components/ui/command/command-shortcut.svelte.d.ts +5 -0
  56. package/dist/components/ui/command/command.svelte +28 -0
  57. package/dist/components/ui/command/command.svelte.d.ts +8 -0
  58. package/dist/components/ui/command/index.d.ts +12 -0
  59. package/dist/components/ui/command/index.js +14 -0
  60. package/dist/components/ui/dialog/dialog-close.svelte +7 -0
  61. package/dist/components/ui/dialog/dialog-close.svelte.d.ts +4 -0
  62. package/dist/components/ui/dialog/dialog-content.svelte +45 -0
  63. package/dist/components/ui/dialog/dialog-content.svelte.d.ts +13 -0
  64. package/dist/components/ui/dialog/dialog-description.svelte +17 -0
  65. package/dist/components/ui/dialog/dialog-description.svelte.d.ts +4 -0
  66. package/dist/components/ui/dialog/dialog-footer.svelte +20 -0
  67. package/dist/components/ui/dialog/dialog-footer.svelte.d.ts +5 -0
  68. package/dist/components/ui/dialog/dialog-header.svelte +20 -0
  69. package/dist/components/ui/dialog/dialog-header.svelte.d.ts +5 -0
  70. package/dist/components/ui/dialog/dialog-overlay.svelte +20 -0
  71. package/dist/components/ui/dialog/dialog-overlay.svelte.d.ts +4 -0
  72. package/dist/components/ui/dialog/dialog-portal.svelte +7 -0
  73. package/dist/components/ui/dialog/dialog-portal.svelte.d.ts +3 -0
  74. package/dist/components/ui/dialog/dialog-title.svelte +17 -0
  75. package/dist/components/ui/dialog/dialog-title.svelte.d.ts +4 -0
  76. package/dist/components/ui/dialog/dialog-trigger.svelte +7 -0
  77. package/dist/components/ui/dialog/dialog-trigger.svelte.d.ts +4 -0
  78. package/dist/components/ui/dialog/dialog.svelte +7 -0
  79. package/dist/components/ui/dialog/dialog.svelte.d.ts +3 -0
  80. package/dist/components/ui/dialog/index.d.ts +11 -0
  81. package/dist/components/ui/dialog/index.js +13 -0
  82. package/dist/components/ui/input/index.d.ts +2 -0
  83. package/dist/components/ui/input/index.js +4 -0
  84. package/dist/components/ui/input/input.svelte +52 -0
  85. package/dist/components/ui/input/input.svelte.d.ts +13 -0
  86. package/dist/components/ui/kbd/index.d.ts +3 -0
  87. package/dist/components/ui/kbd/index.js +5 -0
  88. package/dist/components/ui/kbd/kbd-group.svelte +20 -0
  89. package/dist/components/ui/kbd/kbd-group.svelte.d.ts +5 -0
  90. package/dist/components/ui/kbd/kbd.svelte +25 -0
  91. package/dist/components/ui/kbd/kbd.svelte.d.ts +5 -0
  92. package/dist/components/ui/popover/index.d.ts +6 -0
  93. package/dist/components/ui/popover/index.js +8 -0
  94. package/dist/components/ui/popover/popover-close.svelte +7 -0
  95. package/dist/components/ui/popover/popover-close.svelte.d.ts +4 -0
  96. package/dist/components/ui/popover/popover-content.svelte +31 -0
  97. package/dist/components/ui/popover/popover-content.svelte.d.ts +10 -0
  98. package/dist/components/ui/popover/popover-portal.svelte +7 -0
  99. package/dist/components/ui/popover/popover-portal.svelte.d.ts +3 -0
  100. package/dist/components/ui/popover/popover-trigger.svelte +17 -0
  101. package/dist/components/ui/popover/popover-trigger.svelte.d.ts +4 -0
  102. package/dist/components/ui/popover/popover.svelte +7 -0
  103. package/dist/components/ui/popover/popover.svelte.d.ts +3 -0
  104. package/dist/components/ui/table/index.d.ts +9 -0
  105. package/dist/components/ui/table/index.js +11 -0
  106. package/dist/components/ui/table/table-body.svelte +20 -0
  107. package/dist/components/ui/table/table-body.svelte.d.ts +5 -0
  108. package/dist/components/ui/table/table-caption.svelte +20 -0
  109. package/dist/components/ui/table/table-caption.svelte.d.ts +5 -0
  110. package/dist/components/ui/table/table-cell.svelte +23 -0
  111. package/dist/components/ui/table/table-cell.svelte.d.ts +5 -0
  112. package/dist/components/ui/table/table-footer.svelte +20 -0
  113. package/dist/components/ui/table/table-footer.svelte.d.ts +5 -0
  114. package/dist/components/ui/table/table-head.svelte +23 -0
  115. package/dist/components/ui/table/table-head.svelte.d.ts +5 -0
  116. package/dist/components/ui/table/table-header.svelte +20 -0
  117. package/dist/components/ui/table/table-header.svelte.d.ts +5 -0
  118. package/dist/components/ui/table/table-row.svelte +23 -0
  119. package/dist/components/ui/table/table-row.svelte.d.ts +5 -0
  120. package/dist/components/ui/table/table.svelte +22 -0
  121. package/dist/components/ui/table/table.svelte.d.ts +5 -0
  122. package/dist/components/ui/tabs/index.d.ts +5 -0
  123. package/dist/components/ui/tabs/index.js +7 -0
  124. package/dist/components/ui/tabs/tabs-content.svelte +17 -0
  125. package/dist/components/ui/tabs/tabs-content.svelte.d.ts +4 -0
  126. package/dist/components/ui/tabs/tabs-list.svelte +20 -0
  127. package/dist/components/ui/tabs/tabs-list.svelte.d.ts +4 -0
  128. package/dist/components/ui/tabs/tabs-trigger.svelte +20 -0
  129. package/dist/components/ui/tabs/tabs-trigger.svelte.d.ts +4 -0
  130. package/dist/components/ui/tabs/tabs.svelte +19 -0
  131. package/dist/components/ui/tabs/tabs.svelte.d.ts +4 -0
  132. package/dist/components/ui/tooltip/index.d.ts +6 -0
  133. package/dist/components/ui/tooltip/index.js +8 -0
  134. package/dist/components/ui/tooltip/tooltip-content.svelte +52 -0
  135. package/dist/components/ui/tooltip/tooltip-content.svelte.d.ts +11 -0
  136. package/dist/components/ui/tooltip/tooltip-portal.svelte +7 -0
  137. package/dist/components/ui/tooltip/tooltip-portal.svelte.d.ts +4 -0
  138. package/dist/components/ui/tooltip/tooltip-provider.svelte +7 -0
  139. package/dist/components/ui/tooltip/tooltip-provider.svelte.d.ts +4 -0
  140. package/dist/components/ui/tooltip/tooltip-trigger.svelte +7 -0
  141. package/dist/components/ui/tooltip/tooltip-trigger.svelte.d.ts +4 -0
  142. package/dist/components/ui/tooltip/tooltip.svelte +7 -0
  143. package/dist/components/ui/tooltip/tooltip.svelte.d.ts +4 -0
  144. package/dist/focus.svelte +56 -0
  145. package/dist/focus.svelte.d.ts +13 -0
  146. package/dist/index.d.ts +5 -0
  147. package/dist/index.js +7 -0
  148. package/dist/overlay-component.svelte +19 -0
  149. package/dist/overlay-component.svelte.d.ts +6 -0
  150. package/dist/palette.svelte +132 -0
  151. package/dist/palette.svelte.d.ts +7 -0
  152. package/dist/palette.svelte.js +177 -0
  153. package/dist/shortcut.svelte +26 -0
  154. package/dist/shortcut.svelte.d.ts +8 -0
  155. package/dist/utils.d.ts +13 -0
  156. package/dist/utils.js +32 -0
  157. package/package.json +87 -0
package/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # Svelte Incant
2
+
3
+ A keyboard shortcut management library for Svelte 5.
4
+
5
+ ## Installation
6
+
7
+ ```sh
8
+ bun install svelte-incant
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ Add `Palette` component to your root layout to enable the shortcut overlay:
14
+
15
+ ```svelte
16
+ <script>
17
+ import { Palette } from 'svelte-incant';
18
+ </script>
19
+
20
+ <Palette />
21
+
22
+ <!-- ... -->
23
+ ```
24
+
25
+ Register keyboard shortcuts with the `Shortcut` component:
26
+
27
+ ```svelte
28
+ <script>
29
+ import { Shortcut } from 'svelte-incant';
30
+ </script>
31
+
32
+ <Shortcut
33
+ keys={['control', 's']}
34
+ description="Save document"
35
+ action={() => console.log('Save document')}
36
+ />
37
+ ```
38
+
39
+ For focusing elements (like inputs), use the `Focus` component:
40
+
41
+ ```svelte
42
+ <script>
43
+ import { Focus } from 'svelte-incant';
44
+ </script>
45
+
46
+ <Focus keys={['control', 'e']} description="Focus search input">
47
+ <input type="text" placeholder="Search..." />
48
+ </Focus>
49
+ ```
50
+
51
+ Or attach shortcuts directly to an element using the `@attach` directive:
52
+
53
+ ```svelte
54
+ <script>
55
+ import { shortcut } from 'svelte-incant';
56
+ </script>
57
+
58
+ <input
59
+ type="text"
60
+ placeholder="Type something..."
61
+ {@attach shortcut({
62
+ keys: ['meta', 'i'],
63
+ description: 'Focus text input'
64
+ })}
65
+ />
66
+ ```
67
+
68
+ The `@attach` directive focuses the element it attaches to directly, as opposed to the `Focus` component, which wraps the children in a `div` and focuses that.
69
+
70
+ ## Features
71
+
72
+ - **Shortcut Palette**: Press `?` to open the shortcut palette and see all registered shortcuts
73
+ - **Route-specific Shortcuts**: Shortcuts only run when their component is mounted, allowing different shortcuts in different routes
74
+ - **Focus Management**: Easily manage focus states with keyboard shortcuts
75
+ - **Component-based**: Use components or directives to register shortcuts
@@ -0,0 +1,8 @@
1
+ import { type Shortcut } from './palette.svelte.js';
2
+ import type { Attachment } from 'svelte/attachments';
3
+ type ShortcutInput = Omit<Shortcut, 'action' | 'keys'> & {
4
+ keys: string | string[] | string[][];
5
+ action?: () => void;
6
+ };
7
+ export declare function shortcut(shortcut: ShortcutInput): Attachment<HTMLElement>;
8
+ export {};
@@ -0,0 +1,93 @@
1
+ import { mount, unmount } from 'svelte';
2
+ import { add_shortcut, remove_shortcut } from './palette.svelte.js';
3
+ import OverlayComponent from './overlay-component.svelte';
4
+ import { PressedKeys, watch } from 'runed';
5
+ const pressed_keys = new PressedKeys();
6
+ const voidElements = new Set([
7
+ 'area',
8
+ 'base',
9
+ 'br',
10
+ 'col',
11
+ 'embed',
12
+ 'hr',
13
+ 'img',
14
+ 'input',
15
+ 'link',
16
+ 'meta',
17
+ 'param',
18
+ 'source',
19
+ 'track',
20
+ 'wbr'
21
+ ]);
22
+ function setupAnchor(element, targetNode, isVoidElement, keys) {
23
+ const anchor = document.createElement('div');
24
+ anchor.style.pointerEvents = 'none';
25
+ if (isVoidElement) {
26
+ anchor.style.position = 'absolute';
27
+ anchor.style.left = `${element.offsetLeft}px`;
28
+ anchor.style.top = `${element.offsetTop}px`;
29
+ anchor.style.width = `${element.offsetWidth}px`;
30
+ anchor.style.height = `${element.offsetHeight}px`;
31
+ }
32
+ else {
33
+ const style = window.getComputedStyle(element);
34
+ if (style.position === 'static') {
35
+ element.style.position = 'relative';
36
+ }
37
+ anchor.style.position = 'absolute';
38
+ anchor.style.top = '0';
39
+ anchor.style.left = '0';
40
+ anchor.style.width = '100%';
41
+ anchor.style.height = '100%';
42
+ }
43
+ const instance = mount(OverlayComponent, {
44
+ target: anchor,
45
+ props: { keys }
46
+ });
47
+ targetNode.appendChild(anchor);
48
+ return { anchor, instance };
49
+ }
50
+ function setupOutline(element) {
51
+ element.style.transition = 'outline 0s, outline-offset 0s';
52
+ watch(() => pressed_keys.has('alt'), (value) => {
53
+ if (value) {
54
+ element.style.outline = '2px dotted currentColor';
55
+ element.style.outlineOffset = '2px';
56
+ }
57
+ else {
58
+ element.style.outline = '';
59
+ element.style.outlineOffset = '';
60
+ }
61
+ });
62
+ }
63
+ export function shortcut(shortcut) {
64
+ return (element) => {
65
+ const action = () => {
66
+ element.focus();
67
+ element.click();
68
+ shortcut.action?.();
69
+ };
70
+ const shortcutData = {
71
+ ...shortcut,
72
+ action
73
+ };
74
+ add_shortcut(shortcutData);
75
+ let targetNode = element;
76
+ const tagName = element.tagName.toLowerCase();
77
+ const isVoidElement = voidElements.has(tagName);
78
+ if (isVoidElement) {
79
+ targetNode = element.parentElement;
80
+ }
81
+ if (!targetNode) {
82
+ remove_shortcut(shortcut.keys);
83
+ return () => { };
84
+ }
85
+ const { anchor, instance } = setupAnchor(element, targetNode, isVoidElement, shortcut.keys);
86
+ setupOutline(element);
87
+ return () => {
88
+ unmount(instance);
89
+ anchor.remove();
90
+ remove_shortcut(shortcut.keys);
91
+ };
92
+ };
93
+ }
@@ -0,0 +1,155 @@
1
+ <script lang="ts">
2
+ import * as Card from './components/ui/card';
3
+ import { shortcut } from './attachment.svelte.js';
4
+ import { Button } from './components/ui/button/index.js';
5
+ import * as Command from './components/ui/command/index.js';
6
+ import * as Popover from './components/ui/popover/index.js';
7
+ import * as Tabs from './components/ui/tabs/index.js';
8
+ import CodeBlock from './components/CodeBlock.svelte';
9
+
10
+ import { cn } from './utils.js';
11
+ import CheckIcon from '@lucide/svelte/icons/check';
12
+ import ChevronsUpDownIcon from '@lucide/svelte/icons/chevrons-up-down';
13
+ import { tick } from 'svelte';
14
+
15
+ const frameworks = [
16
+ {
17
+ value: 'sveltekit',
18
+ label: 'SvelteKit'
19
+ },
20
+ {
21
+ value: 'next.js',
22
+ label: 'Next.js'
23
+ },
24
+ {
25
+ value: 'nuxt.js',
26
+ label: 'Nuxt.js'
27
+ },
28
+ {
29
+ value: 'remix',
30
+ label: 'Remix'
31
+ },
32
+ {
33
+ value: 'astro',
34
+ label: 'Astro'
35
+ }
36
+ ];
37
+
38
+ let open = $state(false);
39
+ let value = $state('');
40
+ let triggerRef = $state<HTMLButtonElement>(null!);
41
+
42
+ const selectedValue = $derived(frameworks.find((f) => f.value === value)?.label);
43
+
44
+ const comboboxDemoCode =
45
+ `<script>
46
+ import { shortcut } from 'svelte-incant';
47
+ import { Button } from './components/ui/button';
48
+ import * as Popover from './components/ui/popover';
49
+ <` +
50
+ `/script>
51
+
52
+ <Popover.Root bind:open>
53
+ <Popover.Trigger bind:ref={triggerRef}>
54
+ {#snippet child({ props })}
55
+ <Button
56
+ {...props}
57
+ variant="outline"
58
+ class="w-[200px] justify-between"
59
+ role="combobox"
60
+ aria-expanded={open}
61
+ {@attach shortcut({
62
+ keys: ['meta', 'k'],
63
+ description: 'Focus framework combobox'
64
+ })}
65
+ >
66
+ {selectedValue || 'Select a framework...'}
67
+ </Button>
68
+ {/snippet}
69
+ </Popover.Trigger>
70
+ <!-- Popover content -->
71
+ </Popover.Root>`;
72
+
73
+ function closeAndFocusTrigger() {
74
+ open = false;
75
+ tick().then(() => {
76
+ triggerRef.focus();
77
+ });
78
+ }
79
+ </script>
80
+
81
+ <p class="text-sm text-muted-foreground">
82
+ Press <kbd class="rounded border bg-muted px-1 py-0.5 text-xs">⌥</kbd> (alt) to see the focus
83
+ shortcut hint. Press <kbd class="rounded border bg-muted px-1 py-0.5 text-xs">⌘ K</kbd> to focus the
84
+ combobox.
85
+ </p>
86
+
87
+ <Tabs.Root value="example" class="w-full">
88
+ <Card.Root>
89
+ <Tabs.Content value="example">
90
+ <Card.Content class="grid h-80 place-items-center">
91
+ <Popover.Root bind:open>
92
+ <Popover.Trigger bind:ref={triggerRef}>
93
+ {#snippet child({ props })}
94
+ <Button
95
+ {...props}
96
+ variant="outline"
97
+ class="w-[200px] justify-between"
98
+ role="combobox"
99
+ aria-expanded={open}
100
+ {@attach shortcut({
101
+ keys: ['meta', 'k'],
102
+ description: 'Focus framework combobox'
103
+ })}
104
+ >
105
+ {selectedValue || 'Select a framework...'}
106
+ <ChevronsUpDownIcon class="opacity-50" />
107
+ </Button>
108
+ {/snippet}
109
+ </Popover.Trigger>
110
+ <Popover.Content class="w-[200px] p-0">
111
+ <Command.Root>
112
+ <Command.Input placeholder="Search framework..." />
113
+ <Command.List>
114
+ <Command.Empty>No framework found.</Command.Empty>
115
+ <Command.Group value="frameworks">
116
+ {#each frameworks as framework (framework.value)}
117
+ <Command.Item
118
+ value={framework.value}
119
+ onSelect={() => {
120
+ value = framework.value;
121
+ closeAndFocusTrigger();
122
+ }}
123
+ >
124
+ <CheckIcon class={cn(value !== framework.value && 'text-transparent')} />
125
+ {framework.label}
126
+ </Command.Item>
127
+ {/each}
128
+ </Command.Group>
129
+ </Command.List>
130
+ </Command.Root>
131
+ </Popover.Content>
132
+ </Popover.Root>
133
+ </Card.Content>
134
+ </Tabs.Content>
135
+ <Tabs.Content value="code">
136
+ <Card.Content>
137
+ <CodeBlock language="xml" code={comboboxDemoCode} />
138
+
139
+ <p class="mt-4 text-sm text-muted-foreground">
140
+ Full combobox code from <a
141
+ href="https://shadcn-svelte.com/docs/components/combobox"
142
+ target="_blank"
143
+ class="font-mono underline"
144
+ >
145
+ https://shadcn-svelte.com/docs/components/combobox
146
+ </a>
147
+ </p>
148
+ </Card.Content>
149
+ </Tabs.Content>
150
+ </Card.Root>
151
+ <Tabs.List>
152
+ <Tabs.Trigger value="example">Example</Tabs.Trigger>
153
+ <Tabs.Trigger value="code">Code</Tabs.Trigger>
154
+ </Tabs.List>
155
+ </Tabs.Root>
@@ -0,0 +1,3 @@
1
+ declare const ComboboxExample: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type ComboboxExample = ReturnType<typeof ComboboxExample>;
3
+ export default ComboboxExample;
@@ -0,0 +1,213 @@
1
+ <script lang="ts">
2
+ import hljs from 'highlight.js/lib/core';
3
+ import javascript from 'highlight.js/lib/languages/javascript';
4
+ import xml from 'highlight.js/lib/languages/xml';
5
+ import 'highlight.js/styles/github.css';
6
+ import copy from 'copy-to-clipboard';
7
+ import type { HTMLAttributes } from 'svelte/elements';
8
+
9
+ hljs.registerLanguage('javascript', javascript);
10
+ hljs.registerLanguage('xml', xml);
11
+
12
+ let codeElement: HTMLElement;
13
+
14
+ function escapeHtml(value: string): string {
15
+ return value
16
+ .replace(/&/g, '&amp;')
17
+ .replace(/</g, '&lt;')
18
+ .replace(/>/g, '&gt;')
19
+ .replace(/"/g, '&quot;')
20
+ .replace(/'/g, '&#x27;');
21
+ }
22
+
23
+ type Props = {
24
+ autodetect?: boolean;
25
+ language?: string;
26
+ ignoreIllegals?: boolean;
27
+ code?: string;
28
+ setLanguage?: (language: string) => void;
29
+ } & HTMLAttributes<HTMLDivElement>;
30
+
31
+ let {
32
+ autodetect = true,
33
+ language = '',
34
+ setLanguage = () => {},
35
+ ignoreIllegals = true,
36
+ code,
37
+ ...restProps
38
+ }: Props = $props();
39
+
40
+ let copying = $state(0);
41
+ let highlightedCode: string = $state('');
42
+
43
+ const cannotDetectLanguage = $derived(!autodetect && !hljs.getLanguage(language));
44
+
45
+ const className = $derived(
46
+ cannotDetectLanguage ? '' : `hljs ${language} ${restProps.class ?? ''}`
47
+ );
48
+
49
+ $effect(() => {
50
+ if (!code) return;
51
+ if (cannotDetectLanguage) {
52
+ highlightedCode = escapeHtml(code);
53
+ }
54
+
55
+ if (autodetect) {
56
+ const result = hljs.highlightAuto(code);
57
+ setLanguage(result.language ?? '');
58
+ highlightedCode = result.value;
59
+ } else {
60
+ const result = hljs.highlight(code, {
61
+ language,
62
+ ignoreIllegals
63
+ });
64
+ highlightedCode = result.value;
65
+ }
66
+ });
67
+
68
+ $effect(() => {
69
+ if (codeElement) {
70
+ // eslint-disable-next-line svelte/no-dom-manipulating
71
+ codeElement.innerHTML = highlightedCode;
72
+ }
73
+ });
74
+
75
+ function onCopy() {
76
+ if (!code) return;
77
+ copy(code);
78
+ copying++;
79
+ setTimeout(() => {
80
+ copying--;
81
+ }, 2000);
82
+ }
83
+ </script>
84
+
85
+ <div class="outerWrapper">
86
+ <button class="copyButton" onclick={onCopy} aria-label="Copy code">
87
+ {#if copying}
88
+ <div>
89
+ <svg
90
+ viewBox="0 0 24 24"
91
+ width="14"
92
+ height="14"
93
+ stroke="currentColor"
94
+ stroke-width="1.5"
95
+ stroke-linecap="round"
96
+ stroke-linejoin="round"
97
+ fill="none"
98
+ shape-rendering="geometricPrecision"
99
+ >
100
+ <path d="M20 6L9 17l-5-5" />
101
+ </svg>
102
+ </div>
103
+ {:else}
104
+ <div>
105
+ <svg
106
+ viewBox="0 0 24 24"
107
+ width="14"
108
+ height="14"
109
+ stroke="currentColor"
110
+ stroke-width="1.5"
111
+ stroke-linecap="round"
112
+ stroke-linejoin="round"
113
+ fill="none"
114
+ shape-rendering="geometricPrecision"
115
+ >
116
+ <path
117
+ d="M8 17.929H6c-1.105 0-2-.912-2-2.036V5.036C4 3.91 4.895 3 6 3h8c1.105 0 2 .911 2 2.036v1.866m-6 .17h8c1.105 0 2 .91 2 2.035v10.857C20 21.09 19.105 22 18 22h-8c-1.105 0-2-.911-2-2.036V9.107c0-1.124.895-2.036 2-2.036z"
118
+ />
119
+ </svg>
120
+ </div>
121
+ {/if}
122
+ </button>
123
+
124
+ <div class="wrapper">
125
+ <div class={`${className} root`}>
126
+ <code bind:this={codeElement}></code>
127
+ </div>
128
+ </div>
129
+ </div>
130
+
131
+ <style>
132
+ :root {
133
+ --gray0: #fff;
134
+ --gray1: hsl(0, 0%, 99%);
135
+ --gray2: hsl(0, 0%, 97.3%);
136
+ --gray3: hsl(0, 0%, 95.1%);
137
+ --gray4: hsl(0, 0%, 93%);
138
+ --gray5: hsl(0, 0%, 90.9%);
139
+ --gray6: hsl(0, 0%, 88.7%);
140
+ --gray7: hsl(0, 0%, 85.8%);
141
+ --gray8: hsl(0, 0%, 78%);
142
+ --gray9: hsl(0, 0%, 56.1%);
143
+ --gray10: hsl(0, 0%, 52.3%);
144
+ --gray11: hsl(0, 0%, 43.5%);
145
+ --gray12: hsl(0, 0%, 9%);
146
+ }
147
+
148
+ .root {
149
+ padding: 16px;
150
+ margin: 0;
151
+ background: var(--gray1);
152
+ border-radius: 0;
153
+ position: relative;
154
+ line-height: 17px;
155
+ white-space: pre-wrap;
156
+ background: linear-gradient(to top, var(--gray2), var(--gray1) 16px);
157
+ }
158
+
159
+ .wrapper {
160
+ overflow: hidden;
161
+ margin: 0;
162
+ position: relative;
163
+ border-radius: 6px;
164
+ margin-top: 16px;
165
+ border: 1px solid var(--gray3);
166
+ }
167
+
168
+ .copyButton {
169
+ position: absolute;
170
+ top: 12px;
171
+ right: 12px;
172
+ z-index: 1;
173
+ width: 26px;
174
+ height: 26px;
175
+ border: 1px solid var(--gray4);
176
+ border-radius: 6px;
177
+ display: flex;
178
+ align-items: center;
179
+ justify-content: center;
180
+ background: var(--gray0);
181
+ cursor: pointer;
182
+ opacity: 0;
183
+ color: var(--gray12);
184
+ transition:
185
+ background 200ms,
186
+ box-shadow 200ms,
187
+ opacity 200ms;
188
+ }
189
+
190
+ .copyButton:hover {
191
+ background: var(--gray1);
192
+ }
193
+
194
+ .copyButton:focus-visible {
195
+ box-shadow: 0 0 0 1px var(--gray4);
196
+ }
197
+
198
+ .copyButton > div {
199
+ display: flex;
200
+ }
201
+
202
+ .outerWrapper {
203
+ position: relative;
204
+ }
205
+
206
+ .outerWrapper:hover .copyButton {
207
+ opacity: 1;
208
+ }
209
+
210
+ code {
211
+ font-size: 12px;
212
+ }
213
+ </style>
@@ -0,0 +1,12 @@
1
+ import 'highlight.js/styles/github.css';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ type Props = {
4
+ autodetect?: boolean;
5
+ language?: string;
6
+ ignoreIllegals?: boolean;
7
+ code?: string;
8
+ setLanguage?: (language: string) => void;
9
+ } & HTMLAttributes<HTMLDivElement>;
10
+ declare const CodeBlock: import("svelte").Component<Props, {}, "">;
11
+ type CodeBlock = ReturnType<typeof CodeBlock>;
12
+ export default CodeBlock;