@x33025/sveltely 0.0.57 → 0.0.58

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.
@@ -15,7 +15,7 @@
15
15
  { label: 'Month', value: 'month' }
16
16
  ];
17
17
 
18
- let value = $state('week');
18
+ let value = $state('day');
19
19
  </script>
20
20
 
21
21
  <div class="vstack center gap-2">
@@ -0,0 +1,20 @@
1
+ <script module lang="ts">
2
+ export const demo = {
3
+ name: 'TokenSearchInput',
4
+ description: 'Search field that turns comma-separated entries into removable tokens.',
5
+ isProminent: false,
6
+ columnSpan: 2
7
+ };
8
+ </script>
9
+
10
+ <script lang="ts">
11
+ import TokenSearchInput from './TokenSearchInput.svelte';
12
+
13
+ let tokens = $state(['svelte', 'ui']);
14
+ let value = $state('');
15
+ </script>
16
+
17
+ <div class="vstack w-full max-w-md gap-2">
18
+ <TokenSearchInput bind:tokens bind:value placeholder="Search keywords" class="w-full" />
19
+ <p class="text-xs text-zinc-500">Tokens: {tokens.join(', ') || 'empty'}</p>
20
+ </div>
@@ -0,0 +1,10 @@
1
+ export declare const demo: {
2
+ name: string;
3
+ description: string;
4
+ isProminent: boolean;
5
+ columnSpan: number;
6
+ };
7
+ import TokenSearchInput from './TokenSearchInput.svelte';
8
+ declare const TokenSearchInput: import("svelte").Component<Record<string, never>, {}, "">;
9
+ type TokenSearchInput = ReturnType<typeof TokenSearchInput>;
10
+ export default TokenSearchInput;
@@ -0,0 +1,124 @@
1
+ <script lang="ts">
2
+ import { SearchIcon, X } from '@lucide/svelte';
3
+ import type { HTMLInputAttributes } from 'svelte/elements';
4
+
5
+ type Props = {
6
+ tokens?: string[];
7
+ value?: string;
8
+ confirmText?: string | null;
9
+ inputClass?: string;
10
+ tokenClass?: string;
11
+ placeholder?: string;
12
+ tokenPlaceholder?: string;
13
+ normalizeToken?: (value: string) => string;
14
+ showIcon?: boolean;
15
+ } & Omit<HTMLInputAttributes, 'children' | 'type' | 'value' | 'placeholder'>;
16
+
17
+ const defaultNormalizeToken = (value: string) => value.trim().replace(/\s+/g, ' ');
18
+
19
+ let {
20
+ tokens = $bindable<string[]>([]),
21
+ value = $bindable(''),
22
+ confirmText = null,
23
+ inputClass = '',
24
+ tokenClass = '',
25
+ placeholder = 'Search',
26
+ tokenPlaceholder = 'Add search token',
27
+ normalizeToken = defaultNormalizeToken,
28
+ showIcon = false,
29
+ class: className = '',
30
+ disabled = false,
31
+ ...props
32
+ }: Props = $props();
33
+
34
+ const addToken = (rawValue: string) => {
35
+ const token = normalizeToken(rawValue);
36
+ if (!token || tokens.includes(token)) return;
37
+ tokens = [...tokens, token];
38
+ };
39
+
40
+ const commitValue = () => {
41
+ addToken(value);
42
+ value = '';
43
+ };
44
+
45
+ const removeToken = (index: number) => {
46
+ if (index < 0 || index >= tokens.length) return;
47
+ tokens = tokens.filter((_, currentIndex) => currentIndex !== index);
48
+ };
49
+
50
+ const onKeydown = (event: KeyboardEvent) => {
51
+ if (event.key === 'Enter' || event.key === ',') {
52
+ event.preventDefault();
53
+ commitValue();
54
+ return;
55
+ }
56
+
57
+ if (event.key === 'Backspace' && value.length === 0 && tokens.length > 0) {
58
+ event.preventDefault();
59
+ tokens = tokens.slice(0, -1);
60
+ }
61
+ };
62
+
63
+ const onPaste = (event: ClipboardEvent) => {
64
+ const pasted = event.clipboardData?.getData('text') ?? '';
65
+ if (!pasted.includes(',')) return;
66
+
67
+ event.preventDefault();
68
+
69
+ const nextTokens = [...tokens];
70
+ const seen = new Set(nextTokens);
71
+ for (const part of pasted.split(',')) {
72
+ const token = normalizeToken(part);
73
+ if (!token || seen.has(token)) continue;
74
+ nextTokens.push(token);
75
+ seen.add(token);
76
+ }
77
+
78
+ tokens = nextTokens;
79
+ value = '';
80
+ };
81
+ </script>
82
+
83
+ <div
84
+ class="token-search-input hstack w-full flex-nowrap items-center gap-1 overflow-x-auto overflow-y-hidden whitespace-nowrap {className}"
85
+ class:token-search-input-has-tokens={tokens.length > 0}
86
+ data-disabled={disabled ? 'true' : 'false'}
87
+ >
88
+ {#if showIcon}
89
+ <span class="token-search-input-icon" aria-hidden="true">
90
+ <SearchIcon class="size-4" />
91
+ </span>
92
+ {/if}
93
+
94
+ {#each tokens as token, index (`${token}-${index}`)}
95
+ <button
96
+ type="button"
97
+ class="token-search-input-token {tokenClass}"
98
+ onclick={() => removeToken(index)}
99
+ aria-label={`Remove search token ${token}`}
100
+ {disabled}
101
+ >
102
+ <span>{token}</span>
103
+ <X class="size-3" aria-hidden="true" />
104
+ </button>
105
+ {/each}
106
+
107
+ <input
108
+ type="search"
109
+ bind:value
110
+ {disabled}
111
+ placeholder={tokens.length === 0 ? placeholder : tokenPlaceholder}
112
+ class="token-search-input-field {inputClass}"
113
+ onkeydown={onKeydown}
114
+ onpaste={onPaste}
115
+ onblur={commitValue}
116
+ {...props}
117
+ />
118
+
119
+ {#if confirmText}
120
+ <span class="token-search-input-confirm">
121
+ {confirmText}
122
+ </span>
123
+ {/if}
124
+ </div>
@@ -0,0 +1,15 @@
1
+ import type { HTMLInputAttributes } from 'svelte/elements';
2
+ type Props = {
3
+ tokens?: string[];
4
+ value?: string;
5
+ confirmText?: string | null;
6
+ inputClass?: string;
7
+ tokenClass?: string;
8
+ placeholder?: string;
9
+ tokenPlaceholder?: string;
10
+ normalizeToken?: (value: string) => string;
11
+ showIcon?: boolean;
12
+ } & Omit<HTMLInputAttributes, 'children' | 'type' | 'value' | 'placeholder'>;
13
+ declare const TokenSearchInput: import("svelte").Component<Props, {}, "value" | "tokens">;
14
+ type TokenSearchInput = ReturnType<typeof TokenSearchInput>;
15
+ export default TokenSearchInput;
package/dist/index.d.ts CHANGED
@@ -14,3 +14,4 @@ export { default as Popover } from './components/Popover';
14
14
  export { default as ChipInput } from './components/ChipInput.svelte';
15
15
  export { default as AsyncButton } from './components/AsyncButton.svelte';
16
16
  export { default as SearchInput } from './components/SearchInput.svelte';
17
+ export { default as TokenSearchInput } from './components/TokenSearchInput.svelte';
package/dist/index.js CHANGED
@@ -14,3 +14,4 @@ export { default as Popover } from './components/Popover';
14
14
  export { default as ChipInput } from './components/ChipInput.svelte';
15
15
  export { default as AsyncButton } from './components/AsyncButton.svelte';
16
16
  export { default as SearchInput } from './components/SearchInput.svelte';
17
+ export { default as TokenSearchInput } from './components/TokenSearchInput.svelte';
@@ -75,7 +75,7 @@
75
75
  }
76
76
 
77
77
  .pagination-button {
78
- @apply rounded bg-zinc-100 p-2;
78
+ @apply rounded-full bg-zinc-100 p-2;
79
79
  }
80
80
 
81
81
  .pagination-button:disabled {
@@ -83,7 +83,7 @@
83
83
  }
84
84
 
85
85
  .pagination-input {
86
- @apply rounded bg-zinc-100 px-3 py-1 outline-none;
86
+ @apply rounded-full bg-zinc-100 px-3 py-1 outline-none;
87
87
  }
88
88
 
89
89
  .pagination {
@@ -257,7 +257,7 @@
257
257
  }
258
258
 
259
259
  .segmented-picker {
260
- @apply gap-1 rounded-md bg-zinc-100 p-1;
260
+ @apply gap-1 rounded-full bg-zinc-100 p-1;
261
261
  }
262
262
 
263
263
  .segmented-picker-disabled {
@@ -265,7 +265,7 @@
265
265
  }
266
266
 
267
267
  .segmented-picker-button {
268
- @apply rounded-sm px-2.5 py-1;
268
+ @apply rounded-full px-2.5 py-1;
269
269
  }
270
270
 
271
271
  .segmented-picker-button-active {
@@ -282,7 +282,7 @@
282
282
  }
283
283
 
284
284
  .search-input {
285
- @apply inline-flex items-center gap-2 rounded-md border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-900 shadow-xs transition-colors;
285
+ @apply inline-flex items-center gap-2 rounded-full border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-900 shadow-xs transition-colors;
286
286
  }
287
287
 
288
288
  .search-input:focus-within {
@@ -308,6 +308,51 @@
308
308
  .search-input-confirm {
309
309
  @apply shrink-0 text-xs font-medium tracking-[0.18em] text-zinc-500 uppercase;
310
310
  }
311
+
312
+ .token-search-input {
313
+ @apply min-w-0 rounded-full border border-zinc-200 text-sm text-zinc-900 transition-colors outline-none;
314
+ @apply px-3 py-[7px];
315
+ }
316
+
317
+ .token-search-input:focus-within {
318
+ @apply border-zinc-400;
319
+ }
320
+
321
+ .token-search-input-has-tokens {
322
+ @apply p-1;
323
+ }
324
+
325
+ .token-search-input[data-disabled='true'] {
326
+ @apply cursor-not-allowed opacity-60;
327
+ }
328
+
329
+ .token-search-input-icon {
330
+ @apply shrink-0 text-zinc-500;
331
+ }
332
+
333
+ .token-search-input-field {
334
+ @apply field-sizing-content min-w-0 flex-1 bg-transparent outline-none;
335
+ }
336
+
337
+ .token-search-input-field::-webkit-search-cancel-button {
338
+ appearance: none;
339
+ }
340
+
341
+ .token-search-input-token {
342
+ @apply inline-flex shrink-0 items-center gap-1 rounded-full border border-zinc-200 bg-zinc-100 px-2 py-0.5 text-zinc-700 transition-colors;
343
+ }
344
+
345
+ .token-search-input-token:hover:not(:disabled) {
346
+ @apply border-zinc-300 bg-zinc-200;
347
+ }
348
+
349
+ .token-search-input-token:disabled {
350
+ @apply cursor-not-allowed opacity-60;
351
+ }
352
+
353
+ .token-search-input-confirm {
354
+ @apply shrink-0 text-xs font-medium tracking-[0.18em] text-zinc-500 uppercase;
355
+ }
311
356
  }
312
357
 
313
358
  @layer theme {
package/dist/style.css CHANGED
@@ -26,6 +26,7 @@
26
26
  --color-white: #fff;
27
27
  --spacing: 0.25rem;
28
28
  --container-sm: 24rem;
29
+ --container-md: 28rem;
29
30
  --container-3xl: 48rem;
30
31
  --text-xs: 0.75rem;
31
32
  --text-xs--line-height: calc(1 / 0.75);
@@ -310,6 +311,9 @@
310
311
  .max-w-3xl {
311
312
  max-width: var(--container-3xl);
312
313
  }
314
+ .max-w-md {
315
+ max-width: var(--container-md);
316
+ }
313
317
  .max-w-sm {
314
318
  max-width: var(--container-sm);
315
319
  }
@@ -339,6 +343,9 @@
339
343
  .flex-col {
340
344
  flex-direction: column;
341
345
  }
346
+ .flex-nowrap {
347
+ flex-wrap: nowrap;
348
+ }
342
349
  .flex-wrap {
343
350
  flex-wrap: wrap;
344
351
  }
@@ -379,6 +386,12 @@
379
386
  .overflow-hidden {
380
387
  overflow: hidden;
381
388
  }
389
+ .overflow-x-auto {
390
+ overflow-x: auto;
391
+ }
392
+ .overflow-y-hidden {
393
+ overflow-y: hidden;
394
+ }
382
395
  .rounded {
383
396
  border-radius: 0.25rem;
384
397
  }
@@ -481,6 +494,9 @@
481
494
  --tw-tracking: var(--tracking-wide);
482
495
  letter-spacing: var(--tracking-wide);
483
496
  }
497
+ .whitespace-nowrap {
498
+ white-space: nowrap;
499
+ }
484
500
  .text-gray-700 {
485
501
  color: var(--color-gray-700);
486
502
  }
@@ -697,7 +713,7 @@
697
713
  padding: calc(var(--spacing) * 0);
698
714
  }
699
715
  .pagination-button {
700
- border-radius: 0.25rem;
716
+ border-radius: calc(infinity * 1px);
701
717
  background-color: var(--color-zinc-100);
702
718
  padding: calc(var(--spacing) * 2);
703
719
  }
@@ -706,7 +722,7 @@
706
722
  opacity: 50%;
707
723
  }
708
724
  .pagination-input {
709
- border-radius: 0.25rem;
725
+ border-radius: calc(infinity * 1px);
710
726
  background-color: var(--color-zinc-100);
711
727
  padding-inline: calc(var(--spacing) * 3);
712
728
  padding-block: calc(var(--spacing) * 1);
@@ -894,7 +910,7 @@
894
910
  }
895
911
  .segmented-picker {
896
912
  gap: calc(var(--spacing) * 1);
897
- border-radius: var(--radius-md);
913
+ border-radius: calc(infinity * 1px);
898
914
  background-color: var(--color-zinc-100);
899
915
  padding: calc(var(--spacing) * 1);
900
916
  }
@@ -904,7 +920,7 @@
904
920
  filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
905
921
  }
906
922
  .segmented-picker-button {
907
- border-radius: var(--radius-sm);
923
+ border-radius: calc(infinity * 1px);
908
924
  padding-inline: calc(var(--spacing) * 2.5);
909
925
  padding-block: calc(var(--spacing) * 1);
910
926
  }
@@ -928,7 +944,7 @@
928
944
  display: inline-flex;
929
945
  align-items: center;
930
946
  gap: calc(var(--spacing) * 2);
931
- border-radius: var(--radius-md);
947
+ border-radius: calc(infinity * 1px);
932
948
  border-style: var(--tw-border-style);
933
949
  border-width: 1px;
934
950
  border-color: var(--color-zinc-200);
@@ -976,6 +992,84 @@
976
992
  color: var(--color-zinc-500);
977
993
  text-transform: uppercase;
978
994
  }
995
+ .token-search-input {
996
+ min-width: calc(var(--spacing) * 0);
997
+ border-radius: calc(infinity * 1px);
998
+ border-style: var(--tw-border-style);
999
+ border-width: 1px;
1000
+ border-color: var(--color-zinc-200);
1001
+ font-size: var(--text-sm);
1002
+ line-height: var(--tw-leading, var(--text-sm--line-height));
1003
+ color: var(--color-zinc-900);
1004
+ transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
1005
+ transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
1006
+ transition-duration: var(--tw-duration, var(--default-transition-duration));
1007
+ --tw-outline-style: none;
1008
+ outline-style: none;
1009
+ padding-inline: calc(var(--spacing) * 3);
1010
+ padding-block: 7px;
1011
+ }
1012
+ .token-search-input:focus-within {
1013
+ border-color: var(--color-zinc-400);
1014
+ }
1015
+ .token-search-input-has-tokens {
1016
+ padding: calc(var(--spacing) * 1);
1017
+ }
1018
+ .token-search-input[data-disabled='true'] {
1019
+ cursor: not-allowed;
1020
+ opacity: 60%;
1021
+ }
1022
+ .token-search-input-icon {
1023
+ flex-shrink: 0;
1024
+ color: var(--color-zinc-500);
1025
+ }
1026
+ .token-search-input-field {
1027
+ field-sizing: content;
1028
+ min-width: calc(var(--spacing) * 0);
1029
+ flex: 1;
1030
+ background-color: transparent;
1031
+ --tw-outline-style: none;
1032
+ outline-style: none;
1033
+ }
1034
+ .token-search-input-field::-webkit-search-cancel-button {
1035
+ appearance: none;
1036
+ }
1037
+ .token-search-input-token {
1038
+ display: inline-flex;
1039
+ flex-shrink: 0;
1040
+ align-items: center;
1041
+ gap: calc(var(--spacing) * 1);
1042
+ border-radius: calc(infinity * 1px);
1043
+ border-style: var(--tw-border-style);
1044
+ border-width: 1px;
1045
+ border-color: var(--color-zinc-200);
1046
+ background-color: var(--color-zinc-100);
1047
+ padding-inline: calc(var(--spacing) * 2);
1048
+ padding-block: calc(var(--spacing) * 0.5);
1049
+ color: var(--color-zinc-700);
1050
+ transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
1051
+ transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
1052
+ transition-duration: var(--tw-duration, var(--default-transition-duration));
1053
+ }
1054
+ .token-search-input-token:hover:not(:disabled) {
1055
+ border-color: var(--color-zinc-300);
1056
+ background-color: var(--color-zinc-200);
1057
+ }
1058
+ .token-search-input-token:disabled {
1059
+ cursor: not-allowed;
1060
+ opacity: 60%;
1061
+ }
1062
+ .token-search-input-confirm {
1063
+ flex-shrink: 0;
1064
+ font-size: var(--text-xs);
1065
+ line-height: var(--tw-leading, var(--text-xs--line-height));
1066
+ --tw-font-weight: var(--font-weight-medium);
1067
+ font-weight: var(--font-weight-medium);
1068
+ --tw-tracking: 0.18em;
1069
+ letter-spacing: 0.18em;
1070
+ color: var(--color-zinc-500);
1071
+ text-transform: uppercase;
1072
+ }
979
1073
  }
980
1074
  @layer theme {
981
1075
  :root {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x33025/sveltely",
3
- "version": "0.0.57",
3
+ "version": "0.0.58",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",
@@ -39,6 +39,7 @@
39
39
  "./style.css": "./dist/style.css"
40
40
  },
41
41
  "peerDependencies": {
42
+ "@sveltejs/kit": "^2.50.1",
42
43
  "svelte": "^5.0.0"
43
44
  },
44
45
  "devDependencies": {