@netlify/spark-ui 0.0.8 → 0.0.10

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 (69) hide show
  1. package/README.md +7 -2
  2. package/package.json +11 -2
  3. package/packages/components/Icon.tsx +149 -0
  4. package/packages/components/KeyboardKey/KeyboardKey.css +17 -0
  5. package/packages/components/KeyboardKey/KeyboardKey.tsx +19 -0
  6. package/packages/components/Modal/Modal.tsx +18 -0
  7. package/packages/components/index.ts +9 -0
  8. package/packages/components/search/SearchModal.tsx +72 -0
  9. package/packages/components/search/components/SearchFooter.tsx +11 -0
  10. package/packages/components/search/components/SearchFooterTip.tsx +11 -0
  11. package/packages/components/search/components/SearchHeading.tsx +11 -0
  12. package/packages/components/search/components/SearchInput.tsx +45 -0
  13. package/packages/components/search/components/SearchLink.tsx +33 -0
  14. package/packages/components/search/components/SearchToggle/SearchToggle.css +13 -0
  15. package/packages/components/search/components/SearchToggle/SearchToggle.tsx +18 -0
  16. package/packages/components/search/components/SearchWindow.tsx +66 -0
  17. package/packages/components/search/components/index.ts +7 -0
  18. package/packages/components/search/hooks/useKeyboardNav.ts +50 -0
  19. package/packages/components/search/hooks/useSearch.ts +68 -0
  20. package/packages/components/search/hooks/useSearchHotkeys.ts +29 -0
  21. package/packages/components/search/hooks/useSearchParams.ts +23 -0
  22. package/packages/components/search/hooks/useSearchToggles.ts +30 -0
  23. package/packages/components/search/types/search.types.tsx +29 -0
  24. package/packages/components/search/utils/index.ts +11 -0
  25. package/packages/styles/base.css +1 -0
  26. package/packages/styles/search.css +277 -0
  27. package/packages/styles/tokens.css +10 -0
  28. package/packages/styles/utils.css +9 -0
  29. package/packages/utils/useClickOutside.ts +20 -0
  30. package/packages/utils/useTimeout.ts +33 -0
  31. package/packages/assets/logos/encapsulated/darkmode/logo-netlify-encapsulated-fullcolor-darkmode.png +0 -0
  32. package/packages/assets/logos/encapsulated/darkmode/logo-netlify-encapsulated-monochrome-darkmode.png +0 -0
  33. package/packages/assets/logos/encapsulated/darkmode/logo-netlify-encapsulated-monochrome-darkmodealt.png +0 -0
  34. package/packages/assets/logos/encapsulated/lightmode/logo-netlify-encapsulated-fullcolor-lightmode.png +0 -0
  35. package/packages/assets/logos/encapsulated/lightmode/logo-netlify-encapsulated-monochrome-lightmode.png +0 -0
  36. package/packages/assets/logos/full/large/darkmode/logo-netlify-large-fullcolor-darkmode.png +0 -0
  37. package/packages/assets/logos/full/large/darkmode/logo-netlify-large-monochrome-darkmode.png +0 -0
  38. package/packages/assets/logos/full/large/lightmode/logo-netlify-large-fullcolor-lightmode.png +0 -0
  39. package/packages/assets/logos/full/large/lightmode/logo-netlify-large-monochrome-lightmode.png +0 -0
  40. package/packages/assets/logos/full/small/darkmode/logo-netlify-small-fullcolor-darkmode.png +0 -0
  41. package/packages/assets/logos/full/small/darkmode/logo-netlify-small-monochrome-darkmode.png +0 -0
  42. package/packages/assets/logos/full/small/lightmode/logo-netlify-small-fullcolor-lightmode.png +0 -0
  43. package/packages/assets/logos/full/small/lightmode/logo-netlify-small-monochrome-lightmode.png +0 -0
  44. package/packages/assets/logos/monogram/darkmode/logo-netlify-monogram-fullcolor-darkmode.png +0 -0
  45. package/packages/assets/logos/monogram/darkmode/logo-netlify-monogram-monochrome-darkmode.png +0 -0
  46. package/packages/assets/logos/monogram/darkmode/logo-netlify-monogram-monochrome-darkmodealt.png +0 -0
  47. package/packages/assets/logos/monogram/lightmode/logo-netlify-monogram-fullcolor-lightmode.png +0 -0
  48. package/packages/assets/logos/monogram/lightmode/logo-netlify-monogram-fullcolor-lightmode.svg +0 -16
  49. package/packages/assets/logos/monogram/lightmode/logo-netlify-monogram-monochrome-lightmode.png +0 -0
  50. package/packages/assets/logos/monogram/lightmode/logo-netlify-monogram-monochrome-lightmode.svg +0 -16
  51. package/packages/assets/logos/netlify-logo-encapsulated.zip +0 -0
  52. package/packages/assets/logos/netlify-logo-full.zip +0 -0
  53. package/packages/assets/logos/netlify-logo-monogram.zip +0 -0
  54. /package/packages/assets/logos/{encapsulated/darkmode/logo-netlify-encapsulated-fullcolor-darkmode.svg → logo-netlify-encapsulated-fullcolor-darkmode.svg} +0 -0
  55. /package/packages/assets/logos/{encapsulated/lightmode/logo-netlify-encapsulated-fullcolor-lightmode.svg → logo-netlify-encapsulated-fullcolor-lightmode.svg} +0 -0
  56. /package/packages/assets/logos/{encapsulated/darkmode/logo-netlify-encapsulated-monochrome-darkmode.svg → logo-netlify-encapsulated-monochrome-darkmode.svg} +0 -0
  57. /package/packages/assets/logos/{encapsulated/darkmode/logo-netlify-encapsulated-monochrome-darkmodealt.svg → logo-netlify-encapsulated-monochrome-darkmodealt.svg} +0 -0
  58. /package/packages/assets/logos/{encapsulated/lightmode/logo-netlify-encapsulated-monochrome-lightmode.svg → logo-netlify-encapsulated-monochrome-lightmode.svg} +0 -0
  59. /package/packages/assets/logos/{full/large/darkmode/logo-netlify-large-fullcolor-darkmode.svg → logo-netlify-large-fullcolor-darkmode.svg} +0 -0
  60. /package/packages/assets/logos/{full/large/lightmode/logo-netlify-large-fullcolor-lightmode.svg → logo-netlify-large-fullcolor-lightmode.svg} +0 -0
  61. /package/packages/assets/logos/{full/large/darkmode/logo-netlify-large-monochrome-darkmode.svg → logo-netlify-large-monochrome-darkmode.svg} +0 -0
  62. /package/packages/assets/logos/{full/large/lightmode/logo-netlify-large-monochrome-lightmode.svg → logo-netlify-large-monochrome-lightmode.svg} +0 -0
  63. /package/packages/assets/logos/{monogram/darkmode/logo-netlify-monogram-fullcolor-darkmode.svg → logo-netlify-monogram-fullcolor-darkmode.svg} +0 -0
  64. /package/packages/assets/logos/{monogram/darkmode/logo-netlify-monogram-monochrome-darkmode.svg → logo-netlify-monogram-monochrome-darkmode.svg} +0 -0
  65. /package/packages/assets/logos/{monogram/darkmode/logo-netlify-monogram-monochrome-darkmodealt.svg → logo-netlify-monogram-monochrome-darkmodealt.svg} +0 -0
  66. /package/packages/assets/logos/{full/small/darkmode/logo-netlify-small-fullcolor-darkmode.svg → logo-netlify-small-fullcolor-darkmode.svg} +0 -0
  67. /package/packages/assets/logos/{full/small/lightmode/logo-netlify-small-fullcolor-lightmode.svg → logo-netlify-small-fullcolor-lightmode.svg} +0 -0
  68. /package/packages/assets/logos/{full/small/darkmode/logo-netlify-small-monochrome-darkmode.svg → logo-netlify-small-monochrome-darkmode.svg} +0 -0
  69. /package/packages/assets/logos/{full/small/lightmode/logo-netlify-small-monochrome-lightmode.svg → logo-netlify-small-monochrome-lightmode.svg} +0 -0
@@ -0,0 +1,68 @@
1
+ import { useState } from 'preact/hooks';
2
+ import type { SearchState } from '../types/search.types';
3
+ import { useTimeout } from '../../../utils/useTimeout';
4
+ import { getSiteIcon } from '../utils';
5
+
6
+ /**
7
+ * Hook that encapsulates client-side search logic:
8
+ *
9
+ * - Initializes and manages search state (`searchTerm`, `results`, `isLoading`)
10
+ * - Uses a debounced `useTimeout` to delay firing search requests until user input stabilizes
11
+ * - Triggers `submitSearch` when input is empty or 3+ characters, querying `window.pagefind`
12
+ * - Maps and formats raw search results into a consistent shape with title, excerpt, url, and icon
13
+ * - Exposes `setSearchTerm` for live input handling and `submitSearch` for manual triggering
14
+ */
15
+
16
+ export function useSearch() {
17
+ const [state, setSearchState] = useState<SearchState>({
18
+ searchTerm: '',
19
+ results: [],
20
+ isLoading: false,
21
+ });
22
+
23
+ const { reset, run } = useTimeout((query: string) => {
24
+ if (query.length > 2 || query.length === 0) {
25
+ submitSearch(query);
26
+ }
27
+ }, 300);
28
+
29
+ const submitSearch = async (query: string) => {
30
+ if (!window?.pagefind) return;
31
+
32
+ setSearchState(prev => ({ ...prev, isLoading: true }));
33
+
34
+ const search = await window.pagefind.search(query);
35
+ const results = await Promise.all(
36
+ search.results.map(async (result: any) => {
37
+ const data = await result.data();
38
+ return {
39
+ icon: getSiteIcon(data.meta?.content_type),
40
+ content_type: data.meta?.content_type,
41
+ title: data.meta?.title,
42
+ url: data.url,
43
+ excerpt: data.excerpt,
44
+ };
45
+ })
46
+ );
47
+
48
+ setSearchState(prev => ({
49
+ ...prev,
50
+ results,
51
+ isLoading: false,
52
+ }));
53
+ };
54
+
55
+ const handleSearchChange = (query: string) => {
56
+ setSearchState(prev => ({ ...prev, searchTerm: query }));
57
+ reset();
58
+ run(query);
59
+ };
60
+
61
+ return {
62
+ state,
63
+ actions: {
64
+ setSearchTerm: handleSearchChange,
65
+ submitSearch,
66
+ },
67
+ };
68
+ }
@@ -0,0 +1,29 @@
1
+ import { useEffect } from 'preact/hooks';
2
+
3
+ /**
4
+ * Custom hook that enables a keyboard shortcut (`⌘ + K` or `Ctrl + K`)
5
+ * to toggle a search UI open or closed.
6
+ *
7
+ * - When the user presses `Meta/Ctrl + K`, it prevents the default browser behavior,
8
+ * stops the event from bubbling up, and either opens or closes the search UI
9
+ * depending on the current `isOpen` state.
10
+ *
11
+ * @param isOpen - Whether the search interface is currently open.
12
+ * @param onOpen - Callback to open the search interface.
13
+ * @param onClose - Callback to close the search interface.
14
+ */
15
+
16
+ export function useSearchHotkeys(isOpen: boolean, onOpen: () => void, onClose: () => void) {
17
+ useEffect(() => {
18
+ const handleKeydown = (e: KeyboardEvent) => {
19
+ if (e.metaKey && e.key === 'k') {
20
+ e.preventDefault();
21
+ e.stopImmediatePropagation();
22
+ isOpen ? onClose() : onOpen();
23
+ }
24
+ };
25
+
26
+ document.addEventListener('keydown', handleKeydown);
27
+ return () => document.removeEventListener('keydown', handleKeydown);
28
+ }, [isOpen, onOpen, onClose]);
29
+ }
@@ -0,0 +1,23 @@
1
+ import { useEffect } from 'preact/hooks';
2
+
3
+ /**
4
+ * Custom hook that checks the current URL for a `search` query parameter
5
+ * and, if found, updates the search term and opens the search interface.
6
+ *
7
+ * Useful for enabling deep linking to a search view with a pre-filled term,
8
+ * such as when sharing URLs like `?search=netlify`.
9
+ *
10
+ * @param setSearchTerm - Function to update the search input state.
11
+ * @param onOpen - Function to trigger opening of the search interface (e.g., modal or drawer).
12
+ */
13
+
14
+ export function useSearchParams(setSearchTerm: (term: string) => void, onOpen: () => void) {
15
+ useEffect(() => {
16
+ if (typeof location === 'undefined') return;
17
+ const query = location?.search && new URLSearchParams(location.search).get('search');
18
+ if (query) {
19
+ setSearchTerm(query);
20
+ onOpen();
21
+ }
22
+ }, [setSearchTerm, onOpen]);
23
+ }
@@ -0,0 +1,30 @@
1
+ import { useEffect } from 'preact/hooks';
2
+
3
+ /**
4
+ * Custom hook that attaches click event listeners to all DOM elements
5
+ * with the `[data-ntl-search-toggle]` attribute. When clicked, these elements
6
+ * will trigger the provided `onOpen` callback.
7
+ *
8
+ * This is useful for enabling search toggles via declarative HTML attributes
9
+ * in situations where the search modal or drawer needs to be controlled from
10
+ * multiple points in the DOM.
11
+ *
12
+ * @param onOpen - A function to be called when a toggle element is clicked.
13
+ */
14
+
15
+ export function useSearchToggles(onOpen: () => void) {
16
+ useEffect(() => {
17
+ const toggles = document.querySelectorAll('[data-ntl-search-toggle]');
18
+
19
+ if (!toggles.length) return;
20
+
21
+ toggles.forEach(t => {
22
+ t.removeAttribute('disabled');
23
+ t.addEventListener('click', onOpen);
24
+ });
25
+
26
+ return () => {
27
+ toggles.forEach(t => t.removeEventListener('click', onOpen));
28
+ };
29
+ }, [onOpen]);
30
+ }
@@ -0,0 +1,29 @@
1
+ import type { ComponentChildren, FunctionComponent } from 'preact';
2
+ import type { IconName } from '../../Icon';
3
+
4
+ export interface ResultProps {
5
+ icon: IconName;
6
+ title: string;
7
+ content_type?: string;
8
+ url: string;
9
+ excerpt?: string;
10
+ }
11
+
12
+ export interface SearchWindowProps {
13
+ searchLoading: boolean;
14
+ searchTerm?: string;
15
+ results?: ResultProps[];
16
+ children: ComponentChildren;
17
+ }
18
+
19
+ export interface SearchResultsProps {
20
+ searchLoading: boolean;
21
+ searchTerm?: string;
22
+ results?: ResultProps[];
23
+ }
24
+
25
+ export interface SearchState {
26
+ searchTerm: string;
27
+ results: ResultProps[];
28
+ isLoading: boolean;
29
+ }
@@ -0,0 +1,11 @@
1
+ import { IconName } from '../../Icon';
2
+
3
+ const SITE_ICONS: Record<string, IconName> = {
4
+ blog: 'book',
5
+ changelog: 'rss',
6
+ doc: 'pageWithLines',
7
+ };
8
+
9
+ export function getSiteIcon(contentType?: string): IconName {
10
+ return contentType ? SITE_ICONS[contentType] || 'pageWithLines' : 'pageWithLines';
11
+ }
@@ -1,4 +1,5 @@
1
1
  @import 'tokens.css';
2
+ @import 'utils.css';
2
3
  @import 'fonts.css';
3
4
  @import 'layout.css';
4
5
  @import 'buttons.css';
@@ -0,0 +1,277 @@
1
+ /* Imports */
2
+ @import url('./tokens.css');
3
+ @import url('./utils.css');
4
+
5
+ /* Root Variables */
6
+ .search-modal {
7
+ --ntl-search-padding-inline: .5em;
8
+ --ntl-search-color-text: var(--color-text-2);
9
+ --ntl-search-color-bg: var(--color-bg-1);
10
+ --ntl-search-color-border: var(--color-bg-3);
11
+ --ntl-search-border-radius: 8px;
12
+ --ntl-search-max-w: 600px;
13
+ --ntl-search-text-highlight: var(--color-text-accent);
14
+ --ntl-search-footer-key-color-text: var(--color-text-2);
15
+ }
16
+
17
+ /* Container Styles */
18
+ .search-container {
19
+ --mark-bg: var(--ntl-search-text-highlight);
20
+ display: grid;
21
+ width: min(var(--ntl-search-max-w), 100%);
22
+ min-height: 470px;
23
+ background-color: var(--ntl-search-color-bg);
24
+ border-radius: var(--ntl-search-border-radius);
25
+ overflow: hidden;
26
+ }
27
+
28
+ .search-modal {
29
+ padding: 0;
30
+ border: none;
31
+ border-radius: var(--ntl-search-border-radius);
32
+ width: min(var(--ntl-search-max-w), 100%);
33
+ margin-inline: auto;
34
+ }
35
+
36
+ /* Search Bar */
37
+ .search {
38
+ display: flex;
39
+ flex-wrap: wrap;
40
+ align-items: center;
41
+ overflow: hidden;
42
+ background: var(--ntl-search-color-bg);
43
+ border-bottom: 1px solid var(--ntl-search-color-border);
44
+ }
45
+
46
+ .search > * {
47
+ display: inline-block;
48
+ }
49
+
50
+ .search-trigger {
51
+ width: var(--ntl-search-icon-size, 1rem);
52
+ height: auto;
53
+ border: none;
54
+ background: none;
55
+ padding: 0;
56
+ cursor: pointer;
57
+ padding-inline: var(--space-2xs);
58
+ color: var(--color-text-1);
59
+ box-sizing: content-box;
60
+ display: block;
61
+ }
62
+
63
+ /* Search Input */
64
+ .search-input-wrapper {
65
+ all: unset;
66
+ display: flex;
67
+ width: 100%;
68
+ }
69
+
70
+ .search-input {
71
+ display: inline-flex;
72
+ box-sizing: border-box;
73
+ background: var(--ntl-search-color-bg);
74
+ inline-size: 100%;
75
+ padding-block: .75rem;
76
+ padding-inline: var(--ntl-search-padding-inline);
77
+ flex: 1;
78
+ border: none;
79
+ outline: none;
80
+ font-size: 1rem;
81
+ }
82
+
83
+ .search-input::placeholder {
84
+ color: var(--ntl-search-color-text);
85
+ }
86
+
87
+ /* Search Label */
88
+ .search-label {
89
+ display: flex;
90
+ padding-inline: var(--ntl-search-padding-inline);
91
+ }
92
+
93
+ .search-label > * {
94
+ display: inline-block;
95
+ margin: 0;
96
+ }
97
+
98
+ /* Icons */
99
+ .search-icon {
100
+ display: flex;
101
+ width: 1em;
102
+ height: auto;
103
+ color: lightgray;
104
+ }
105
+
106
+ .icon {
107
+ display: flex;
108
+ width: 1em;
109
+ height: auto;
110
+ height: 1em;
111
+ }
112
+
113
+ .icon [data-icon="caretRight"] {
114
+ width: 10px;
115
+ color: var(--neutral-300);
116
+ }
117
+
118
+ /* Search Key & Term */
119
+ .search-term {
120
+ font-weight: var(--font-medium);
121
+ font-size: 1.1em;
122
+ }
123
+
124
+ /* Search Results */
125
+ .search-results-heading {
126
+ padding-inline: 1rem;
127
+ margin-block: var(--space-xs);
128
+ font-size: var(--step--2);
129
+ font-weight: var(--font-regular);
130
+ color: var(--color-text-3);
131
+ }
132
+
133
+ .search-window {
134
+ height: 400px;
135
+ overflow-x: hidden;
136
+ overflow-y: auto;
137
+ overscroll-behavior: contain;
138
+ margin-block: 0;
139
+ background: var(--ntl-search-color-bg);
140
+ }
141
+
142
+ .results-list, ul[role="list"] {
143
+ list-style: none;
144
+ padding: 0;
145
+ }
146
+
147
+ .results-list-item {
148
+ border-bottom: 1px solid var(--color-bg-3);
149
+ }
150
+ .results-list-item:first-of-type {
151
+ border-top: 1px solid var(--color-bg-3);
152
+ }
153
+
154
+ /* Result Items */
155
+ .result-link {
156
+ all: unset;
157
+ display: flex;
158
+ flex-wrap: wrap;
159
+ row-gap: var(--space-3xs);
160
+ padding: var(--ntl-result-padding-block, 1em) var(--ntl-result-padding-inline, 1rem);
161
+ font-size: var(--step--1);
162
+ font-weight: var(--font-regular);
163
+ text-decoration: none;
164
+ cursor: pointer;
165
+ }
166
+
167
+ .result-link:is(:hover, :focus-within) {
168
+ background: var(--neutral-100);
169
+ }
170
+
171
+ .result-header {
172
+ display: flex;
173
+ justify-content: space-between;
174
+ width: 100%;
175
+ align-items: center;
176
+ }
177
+
178
+ .result-title {
179
+ display: flex;
180
+ align-items: center;
181
+ gap: var(--space-s);
182
+ font-weight: var(--font-semibold);
183
+ color: var(--color-text-2);
184
+ }
185
+
186
+ .result-excerpt {
187
+ color: var(--color-text-3);
188
+ line-height: 1.3;
189
+ font-size: .9em;
190
+ padding-inline-start: var(--space-l);
191
+ }
192
+
193
+ /* Footer */
194
+ .search-footer {
195
+ border-top: 1px solid var(--ntl-search-color-border);
196
+ padding: var(--space-2xs) var(--space-s);
197
+ background: var(--color-bg-2);
198
+ display: flex;
199
+ gap: var(--space-m);
200
+ }
201
+
202
+ .search-footer-tip {
203
+ --ntl-key-text-color: var(--ntl-search-footer-key-color-text);
204
+ color: var(--ntl-search-footer-key-color-text);
205
+ display: flex;
206
+ gap: var(--space-2xs);
207
+ align-items: center;
208
+ font-size: var(--step--1);
209
+ }
210
+
211
+ /* Animations */
212
+ .search-modal {
213
+ transition: display .2s allow-discrete, overlay .2s allow-discrete;
214
+ animation: closeSearch .2s forwards;
215
+ &[open] {
216
+ animation: openSearch .2s forwards;
217
+ }
218
+ }
219
+
220
+ @keyframes openSearch {
221
+ from {
222
+ opacity: 0;
223
+ transform: translateY(20px) scale(.9);
224
+ }
225
+ to {
226
+ opacity: 1;
227
+ transform: translateY(0px) scale(1);
228
+ }
229
+ }
230
+
231
+ @keyframes closeSearch {
232
+ from {
233
+ opacity: 1;
234
+ transform: translateY(0px) scale(1);
235
+ }
236
+ 50% {
237
+ opacity: .3;
238
+ }
239
+ to {
240
+ opacity: 0;
241
+ transform: translateY(20px) scale(.9);
242
+ }
243
+ }
244
+
245
+ .search-modal::backdrop {
246
+ animation: closeSearchBackdrop .2s forwards;
247
+ }
248
+
249
+ .search-modal[open]::backdrop {
250
+ animation: openSearchBackdrop .2s forwards;
251
+ }
252
+
253
+ @keyframes openSearchBackdrop {
254
+ from {
255
+ opacity: 0;
256
+ backdrop-filter: blur(0px);
257
+ }
258
+ to {
259
+ opacity: 1;
260
+ backdrop-filter: blur(2px);
261
+ }
262
+ }
263
+
264
+ @keyframes closeSearchBackdrop {
265
+ from {
266
+ opacity: 1;
267
+ backdrop-filter: blur(1.5px);
268
+ }
269
+ 50% {
270
+ opacity: .3;
271
+ }
272
+ to {
273
+ opacity: 0;
274
+ backdrop-filter: blur(0px);
275
+
276
+ }
277
+ }
@@ -29,6 +29,8 @@
29
29
  --color-success-text: var(--green-800);
30
30
  --color-text-1: var(--neutral-800);
31
31
  --color-text-2: var(--neutral-600);
32
+ --color-text-3: var(--neutral-400);
33
+ --color-text-accent: var(--teal-000);
32
34
  --color-warning-bg: var(--gold-100);
33
35
  --color-warning-border: var(--gold-300);
34
36
  --color-warning-text: var(--gold-800);
@@ -157,6 +159,8 @@
157
159
  --color-success-text: var(--green-800);
158
160
  --color-text-1: var(--neutral-800);
159
161
  --color-text-2: var(--neutral-600);
162
+ --color-text-3: var(--neutral-400);
163
+ --color-text-accent: var(--teal-000);
160
164
  --color-warning-bg: var(--gold-100);
161
165
  --color-warning-border: var(--gold-300);
162
166
  --color-warning-text: var(--gold-800);
@@ -189,6 +193,8 @@
189
193
  --color-success-text: var(--green-800);
190
194
  --color-text-1: var(--neutral-800);
191
195
  --color-text-2: var(--neutral-600);
196
+ --color-text-3: var(--neutral-400);
197
+ --color-text-accent: var(--teal-000);
192
198
  --color-warning-bg: var(--gold-100);
193
199
  --color-warning-border: var(--gold-300);
194
200
  --color-warning-text: var(--gold-800);
@@ -221,6 +227,8 @@
221
227
  --color-success-text: var(--green-900);
222
228
  --color-text-1: var(--neutral-100);
223
229
  --color-text-2: var(--neutral-300);
230
+ --color-text-3: var(--neutral-500);
231
+ --color-text-accent: var(--teal-600);
224
232
  --color-warning-bg: var(--gold-200);
225
233
  --color-warning-border: var(--gold-400);
226
234
  --color-warning-text: var(--gold-900);
@@ -253,6 +261,8 @@
253
261
  --color-success-text: var(--green-900);
254
262
  --color-text-1: var(--neutral-100);
255
263
  --color-text-2: var(--neutral-300);
264
+ --color-text-3: var(--neutral-500);
265
+ --color-text-accent: var(--teal-600);
256
266
  --color-warning-bg: var(--gold-200);
257
267
  --color-warning-border: var(--gold-400);
258
268
  --color-warning-text: var(--gold-900);
@@ -0,0 +1,9 @@
1
+ .visually-hidden {
2
+ clip: rect(0 0 0 0);
3
+ clip-path: inset(50%);
4
+ height: 1px;
5
+ overflow: hidden;
6
+ position: absolute;
7
+ white-space: nowrap;
8
+ width: 1px;
9
+ }
@@ -0,0 +1,20 @@
1
+ import { useEffect, useRef } from 'preact/hooks';
2
+
3
+ export function useClickOutside<T extends HTMLElement>(callback: () => void) {
4
+ const ref = useRef<T>(null);
5
+
6
+ useEffect(() => {
7
+ function handleClickOutside(event: MouseEvent) {
8
+ if (ref.current && !ref.current.contains(event.target as Node)) {
9
+ callback();
10
+ }
11
+ }
12
+
13
+ document.addEventListener('mousedown', handleClickOutside);
14
+ return () => {
15
+ document.removeEventListener('mousedown', handleClickOutside);
16
+ };
17
+ }, [callback]);
18
+
19
+ return ref;
20
+ }
@@ -0,0 +1,33 @@
1
+ import { useEffect, useRef } from "preact/hooks";
2
+
3
+ export function useTimeout(
4
+ callback: (latestValue: string) => void,
5
+ delay: number
6
+ ) {
7
+ const timeoutRef = useRef<NodeJS.Timeout | null>(null);
8
+ const latestValueRef = useRef<string>("");
9
+
10
+ useEffect(() => {
11
+ return () => {
12
+ if (timeoutRef.current) {
13
+ clearTimeout(timeoutRef.current);
14
+ }
15
+ };
16
+ }, []);
17
+
18
+ const reset = () => {
19
+ if (timeoutRef.current) {
20
+ clearTimeout(timeoutRef.current);
21
+ }
22
+ };
23
+
24
+ const run = (latestValue: string) => {
25
+ latestValueRef.current = latestValue;
26
+ timeoutRef.current = setTimeout(
27
+ () => callback(latestValueRef.current),
28
+ delay
29
+ );
30
+ };
31
+
32
+ return { reset, run };
33
+ }
@@ -1,16 +0,0 @@
1
- <svg width="128" height="113" viewBox="0 0 128 113" fill="none" xmlns="http://www.w3.org/2000/svg">
2
- <g clip-path="url(#clip0_236_138)">
3
- <path d="M34.593 94.0509H33.3844L27.3514 88.0179V86.8094L36.5743 77.5866H42.9639L43.8158 78.4385V84.8281L34.593 94.0509Z" fill="#05BDBA"/>
4
- <path d="M27.3514 25.816V24.6074L33.3844 18.5744H34.593L43.8158 27.7972V34.1868L42.9639 35.0388H36.5743L27.3514 25.816Z" fill="#05BDBA"/>
5
- <path d="M80.4594 74.6047H71.6824L70.9493 73.8717V53.3259C70.9493 49.6705 69.5129 46.8372 65.1046 46.7382C62.836 46.6787 60.2405 46.7382 57.4668 46.8471L57.0507 47.2731V73.8618L56.3176 74.5948H47.5406L46.8075 73.8618V38.7636L47.5406 38.0305H67.2939C74.9713 38.0305 81.1925 44.2517 81.1925 51.9291V73.8717L80.4594 74.6047Z" fill="#014847"/>
6
- <path d="M35.8412 61.4491H0.73307L0 60.716V51.9192L0.73307 51.1861H35.8412L36.5743 51.9192V60.716L35.8412 61.4491Z" fill="#05BDBA"/>
7
- <path d="M127.277 61.4491H92.1687L91.4356 60.716V51.9192L92.1687 51.1861H127.277L128.01 51.9192V60.716L127.277 61.4491Z" fill="#05BDBA"/>
8
- <path d="M58.9428 27.0642V0.73307L59.6759 0H68.4727L69.2058 0.73307V27.0642L68.4727 27.7972H59.6759L58.9428 27.0642Z" fill="#05BDBA"/>
9
- <path d="M58.9428 111.902V85.5711L59.6759 84.838H68.4727L69.2058 85.5711V111.902L68.4727 112.635H59.6759L58.9428 111.902Z" fill="#05BDBA"/>
10
- </g>
11
- <defs>
12
- <clipPath id="clip0_236_138">
13
- <rect width="128" height="112.635" fill="white"/>
14
- </clipPath>
15
- </defs>
16
- </svg>
@@ -1,16 +0,0 @@
1
- <svg width="128" height="113" viewBox="0 0 128 113" fill="none" xmlns="http://www.w3.org/2000/svg">
2
- <g clip-path="url(#clip0_236_179)">
3
- <path d="M34.593 94.2052H33.3844L27.3514 88.1722V86.9637L36.5743 77.7409H42.9639L43.8158 78.5928V84.9824L34.593 94.2052Z" fill="#181A1C"/>
4
- <path d="M27.3514 25.9703V24.7617L33.3844 18.7287H34.593L43.8158 27.9515V34.3411L42.9639 35.1931H36.5743L27.3514 25.9703Z" fill="#181A1C"/>
5
- <path d="M80.4594 74.759H71.6824L70.9493 74.026V53.4802C70.9493 49.8248 69.5129 46.9915 65.1046 46.8925C62.836 46.833 60.2405 46.8925 57.4668 47.0014L57.0507 47.4274V74.0161L56.3176 74.7491H47.5406L46.8075 74.0161V38.9179L47.5406 38.1848H67.2939C74.9713 38.1848 81.1925 44.406 81.1925 52.0834V74.026L80.4594 74.759Z" fill="#181A1C"/>
6
- <path d="M35.8412 61.6034H0.73307L0 60.8703V52.0735L0.73307 51.3404H35.8412L36.5743 52.0735V60.8703L35.8412 61.6034Z" fill="#181A1C"/>
7
- <path d="M127.277 61.6034H92.1687L91.4356 60.8703V52.0735L92.1687 51.3404H127.277L128.01 52.0735V60.8703L127.277 61.6034Z" fill="#181A1C"/>
8
- <path d="M58.9428 27.2185V0.887367L59.6759 0.154297H68.4727L69.2058 0.887367V27.2185L68.4727 27.9515H59.6759L58.9428 27.2185Z" fill="#181A1C"/>
9
- <path d="M58.9428 112.056V85.7254L59.6759 84.9923H68.4727L69.2058 85.7254V112.056L68.4727 112.79H59.6759L58.9428 112.056Z" fill="#181A1C"/>
10
- </g>
11
- <defs>
12
- <clipPath id="clip0_236_179">
13
- <rect width="128" height="112.635" fill="white" transform="translate(0 0.154297)"/>
14
- </clipPath>
15
- </defs>
16
- </svg>