@x33025/sveltely 0.0.23 → 0.0.25

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.
@@ -0,0 +1,151 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { tick } from 'svelte';
4
+ import { Plus } from '@lucide/svelte';
5
+
6
+ type Props = {
7
+ placeholder?: string;
8
+ tags: string[];
9
+ selection?: string[];
10
+ action?: Snippet;
11
+ };
12
+
13
+ let {
14
+ placeholder = 'Add a tag...',
15
+ tags = $bindable<string[]>(),
16
+ selection = $bindable<string[] | undefined>(),
17
+ action
18
+ }: Props = $props();
19
+
20
+ let inputValue = $state('');
21
+ let showInput = $state(false);
22
+ let inputEl = $state<HTMLInputElement | null>(null);
23
+ const selectionEnabled = $derived(selection !== undefined);
24
+
25
+ const addTag = (rawValue: string) => {
26
+ const nextTag = rawValue.trim();
27
+ if (!nextTag || tags.includes(nextTag)) return;
28
+ tags = [...tags, nextTag];
29
+ };
30
+
31
+ const toggleSelected = (tag: string) => {
32
+ if (!selection) return;
33
+
34
+ if (selection.includes(tag)) {
35
+ selection = selection.filter((value) => value !== tag);
36
+ return;
37
+ }
38
+
39
+ selection = [...selection, tag];
40
+ };
41
+
42
+ const onKeydown = (event: KeyboardEvent) => {
43
+ if ((event.key === 'Enter' || event.key === ',') && inputValue.trim()) {
44
+ event.preventDefault();
45
+ addTag(inputValue);
46
+ inputValue = '';
47
+ return;
48
+ }
49
+
50
+ if (event.key === 'Escape') {
51
+ inputValue = '';
52
+ showInput = false;
53
+ return;
54
+ }
55
+
56
+ if (event.key === 'Backspace' && !inputValue && tags.length > 0) {
57
+ tags = tags.slice(0, -1);
58
+ }
59
+ };
60
+
61
+ const openInput = async () => {
62
+ showInput = true;
63
+ await tick();
64
+ inputEl?.focus();
65
+ };
66
+
67
+ const onBlur = () => {
68
+ if (!inputValue.trim()) {
69
+ showInput = false;
70
+ }
71
+ };
72
+
73
+ $effect(() => {
74
+ if (!selection) return;
75
+ const nextSelected = selection.filter((tag) => tags.includes(tag));
76
+ if (nextSelected.length !== selection.length) {
77
+ selection = nextSelected;
78
+ }
79
+ });
80
+ </script>
81
+
82
+ <div class="w-full max-w-lg">
83
+ <div class="tag-row flex flex-wrap items-center">
84
+ {#each tags as tag (tag)}
85
+ {#if selectionEnabled}
86
+ <button
87
+ type="button"
88
+ class="tag-surface inline-flex items-center gap-2"
89
+ class:tag-selected={selection?.includes(tag)}
90
+ onclick={() => toggleSelected(tag)}
91
+ aria-pressed={selection?.includes(tag)}
92
+ >
93
+ {tag}
94
+ </button>
95
+ {:else}
96
+ <span class="tag-surface inline-flex items-center gap-2">{tag}</span>
97
+ {/if}
98
+ {/each}
99
+
100
+ {#if showInput}
101
+ <input
102
+ bind:this={inputEl}
103
+ bind:value={inputValue}
104
+ class="tag-surface tag-input-field min-w-36 outline-none"
105
+ {placeholder}
106
+ onkeydown={onKeydown}
107
+ onblur={onBlur}
108
+ />
109
+ {:else}
110
+ <button
111
+ type="button"
112
+ class="tag-surface chip-input-action inline-flex items-center justify-center font-semibold"
113
+ aria-label="Add tag"
114
+ onclick={openInput}
115
+ >
116
+ <Plus style="width: var(--chip-input-font-size); height: var(--chip-input-font-size);" />
117
+ </button>
118
+ {/if}
119
+
120
+ {#if action}
121
+ {@render action()}
122
+ {/if}
123
+ </div>
124
+ </div>
125
+
126
+ <style>
127
+ .tag-row {
128
+ gap: var(--chip-input-gap);
129
+ }
130
+
131
+ .tag-surface {
132
+ background: var(--chip-input-background);
133
+ color: var(--chip-input-text);
134
+ font-size: var(--chip-input-font-size);
135
+ border-radius: var(--chip-input-border-radius);
136
+ padding: var(--chip-input-padding);
137
+ border: 1px solid var(--chip-input-border-color);
138
+ }
139
+
140
+ .tag-selected {
141
+ border-color: var(--chip-input-highlight);
142
+ }
143
+
144
+ .tag-surface:hover {
145
+ background: var(--chip-input-hover);
146
+ }
147
+
148
+ .tag-input-field:hover {
149
+ background: var(--chip-input-background);
150
+ }
151
+ </style>
@@ -0,0 +1,10 @@
1
+ import type { Snippet } from 'svelte';
2
+ type Props = {
3
+ placeholder?: string;
4
+ tags: string[];
5
+ selection?: string[];
6
+ action?: Snippet;
7
+ };
8
+ declare const ChipInput: import("svelte").Component<Props, {}, "tags" | "selection">;
9
+ type ChipInput = ReturnType<typeof ChipInput>;
10
+ export default ChipInput;
@@ -22,8 +22,70 @@
22
22
 
23
23
  let isOpen = $state(false);
24
24
  let triggerEl = $state<HTMLElement | null>(null);
25
+ let menuEl = $state<HTMLElement | null>(null);
25
26
  let menuCoords = $state({ top: 0, left: 0 });
26
27
  let menuTransform = $state('none');
28
+ let resolvedAnchor = $state<'top' | 'bottom' | 'leading' | 'trailing'>('bottom');
29
+
30
+ type Point = { x: number; y: number };
31
+
32
+ const isInsideRect = (rect: DOMRect, x: number, y: number) =>
33
+ x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
34
+
35
+ const pointInPolygon = (point: Point, polygon: Point[]) => {
36
+ let inside = false;
37
+ for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
38
+ const xi = polygon[i].x;
39
+ const yi = polygon[i].y;
40
+ const xj = polygon[j].x;
41
+ const yj = polygon[j].y;
42
+ const intersects =
43
+ yi > point.y !== yj > point.y &&
44
+ point.x < ((xj - xi) * (point.y - yi)) / (yj - yi + Number.EPSILON) + xi;
45
+ if (intersects) inside = !inside;
46
+ }
47
+ return inside;
48
+ };
49
+
50
+ const getSafePolygon = (
51
+ triggerRect: DOMRect,
52
+ menuRect: DOMRect,
53
+ currentAnchor: 'top' | 'bottom' | 'leading' | 'trailing'
54
+ ): Point[] => {
55
+ if (currentAnchor === 'bottom') {
56
+ return [
57
+ { x: triggerRect.left, y: triggerRect.top },
58
+ { x: triggerRect.right, y: triggerRect.top },
59
+ { x: menuRect.right, y: menuRect.top },
60
+ { x: menuRect.left, y: menuRect.top }
61
+ ];
62
+ }
63
+
64
+ if (currentAnchor === 'top') {
65
+ return [
66
+ { x: triggerRect.left, y: triggerRect.bottom },
67
+ { x: triggerRect.right, y: triggerRect.bottom },
68
+ { x: menuRect.right, y: menuRect.bottom },
69
+ { x: menuRect.left, y: menuRect.bottom }
70
+ ];
71
+ }
72
+
73
+ if (currentAnchor === 'leading') {
74
+ return [
75
+ { x: triggerRect.right, y: triggerRect.top },
76
+ { x: triggerRect.right, y: triggerRect.bottom },
77
+ { x: menuRect.right, y: menuRect.bottom },
78
+ { x: menuRect.right, y: menuRect.top }
79
+ ];
80
+ }
81
+
82
+ return [
83
+ { x: triggerRect.left, y: triggerRect.top },
84
+ { x: triggerRect.left, y: triggerRect.bottom },
85
+ { x: menuRect.left, y: menuRect.bottom },
86
+ { x: menuRect.left, y: menuRect.top }
87
+ ];
88
+ };
27
89
 
28
90
  function open() {
29
91
  if (triggerEl) {
@@ -32,29 +94,29 @@
32
94
  const spaceBelow = window.innerHeight - rect.bottom;
33
95
  const spaceLeft = rect.left;
34
96
  const spaceRight = window.innerWidth - rect.right;
35
- let resolvedAnchor = anchor;
97
+ let nextAnchor = anchor;
36
98
 
37
99
  if (anchor === 'bottom' && spaceBelow < spaceAbove) {
38
- resolvedAnchor = 'top';
100
+ nextAnchor = 'top';
39
101
  } else if (anchor === 'top' && spaceAbove < spaceBelow) {
40
- resolvedAnchor = 'bottom';
102
+ nextAnchor = 'bottom';
41
103
  } else if (anchor === 'leading' && spaceLeft < spaceRight) {
42
- resolvedAnchor = 'trailing';
104
+ nextAnchor = 'trailing';
43
105
  } else if (anchor === 'trailing' && spaceRight < spaceLeft) {
44
- resolvedAnchor = 'leading';
106
+ nextAnchor = 'leading';
45
107
  }
108
+ resolvedAnchor = nextAnchor;
46
109
 
47
- if (resolvedAnchor === 'top' || resolvedAnchor === 'bottom') {
110
+ if (nextAnchor === 'top' || nextAnchor === 'bottom') {
48
111
  const left = align === 'left' ? rect.left + window.scrollX : rect.right + window.scrollX;
49
112
  const top =
50
- resolvedAnchor === 'bottom' ? rect.bottom + window.scrollY : rect.top + window.scrollY;
113
+ nextAnchor === 'bottom' ? rect.bottom + window.scrollY : rect.top + window.scrollY;
51
114
  menuCoords = { top, left };
52
- if (resolvedAnchor === 'top' && align === 'right')
53
- menuTransform = 'translate(-100%, -100%)';
54
- else if (resolvedAnchor === 'top') menuTransform = 'translateY(-100%)';
115
+ if (nextAnchor === 'top' && align === 'right') menuTransform = 'translate(-100%, -100%)';
116
+ else if (nextAnchor === 'top') menuTransform = 'translateY(-100%)';
55
117
  else if (align === 'right') menuTransform = 'translateX(-100%)';
56
118
  else menuTransform = 'none';
57
- } else if (resolvedAnchor === 'leading') {
119
+ } else if (nextAnchor === 'leading') {
58
120
  menuCoords = { top: rect.top + window.scrollY, left: rect.left + window.scrollX };
59
121
  menuTransform = 'translateX(-100%)';
60
122
  } else {
@@ -84,12 +146,39 @@
84
146
  close();
85
147
  }
86
148
 
149
+ function handleEscape(event: KeyboardEvent) {
150
+ if (!isOpen) return;
151
+ if (event.key === 'Escape') close();
152
+ }
153
+
154
+ function handlePointerMove(event: MouseEvent) {
155
+ if (!isOpen || !triggerEl || !menuEl) return;
156
+
157
+ const pointer = { x: event.clientX, y: event.clientY };
158
+ const triggerRect = triggerEl.getBoundingClientRect();
159
+ const menuRect = menuEl.getBoundingClientRect();
160
+ const nextSafePolygon = getSafePolygon(triggerRect, menuRect, resolvedAnchor);
161
+
162
+ if (isInsideRect(triggerRect, pointer.x, pointer.y)) return;
163
+ if (isInsideRect(menuRect, pointer.x, pointer.y)) return;
164
+
165
+ if (pointInPolygon(pointer, nextSafePolygon)) return;
166
+
167
+ close();
168
+ }
169
+
87
170
  function handleSelect() {
88
171
  if (closeOnSelect) close();
89
172
  }
90
173
  </script>
91
174
 
92
- <svelte:window onclick={handleOutsideClick} onscroll={close} onresize={close} />
175
+ <svelte:window
176
+ onclick={handleOutsideClick}
177
+ onscroll={close}
178
+ onresize={close}
179
+ onkeydown={handleEscape}
180
+ onmousemove={handlePointerMove}
181
+ />
93
182
 
94
183
  <div class="dropdown-container relative inline-block text-left {className}">
95
184
  <div bind:this={triggerEl}>
@@ -112,6 +201,7 @@
112
201
  role="menu"
113
202
  aria-orientation="vertical"
114
203
  tabindex="-1"
204
+ bind:this={menuEl}
115
205
  >
116
206
  <div
117
207
  class="overflow-auto"
package/dist/index.d.ts CHANGED
@@ -10,4 +10,4 @@ export { default as Spinner } from './components/Spinner.svelte';
10
10
  export { default as TextShimmer } from './components/TextShimmer.svelte';
11
11
  export { default as Tooltip } from './components/Tooltip.svelte';
12
12
  export { default as Dropdown } from './components/Dropdown';
13
- export { default as TabView } from './components/TabView';
13
+ export { default as ChipInput } from './components/ChipInput.svelte';
package/dist/index.js CHANGED
@@ -10,4 +10,4 @@ export { default as Spinner } from './components/Spinner.svelte';
10
10
  export { default as TextShimmer } from './components/TextShimmer.svelte';
11
11
  export { default as Tooltip } from './components/Tooltip.svelte';
12
12
  export { default as Dropdown } from './components/Dropdown';
13
- export { default as TabView } from './components/TabView';
13
+ export { default as ChipInput } from './components/ChipInput.svelte';
@@ -41,6 +41,12 @@
41
41
  height: 1px;
42
42
  width: 100%;
43
43
  }
44
+
45
+ .chip-input-action {
46
+ width: calc(var(--chip-input-font-size) + 16px);
47
+ height: calc(var(--chip-input-font-size) + 16px);
48
+ padding: 0;
49
+ }
44
50
  }
45
51
 
46
52
  @layer theme {
@@ -84,5 +90,15 @@
84
90
  0px,
85
91
  calc(var(--segmented-picker-border-radius) - var(--segmented-picker-outer-padding))
86
92
  );
93
+
94
+ --chip-input-gap: var(--spacing);
95
+ --chip-input-padding: 4px 8px;
96
+ --chip-input-background: var(--color-zinc-100);
97
+ --chip-input-hover: var(--color-zinc-200);
98
+ --chip-input-text: var(--color-black);
99
+ --chip-input-font-size: 12px;
100
+ --chip-input-border-radius: 9999px;
101
+ --chip-input-border-color: transparent;
102
+ --chip-input-highlight: var(--color-zinc-300);
87
103
  }
88
104
  }
package/dist/style.css CHANGED
@@ -7,9 +7,10 @@
7
7
  "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
8
8
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
9
9
  "Courier New", monospace;
10
+ --color-red-50: oklch(97.1% 0.013 17.38);
11
+ --color-red-700: oklch(50.5% 0.213 27.518);
10
12
  --color-gray-100: oklch(96.7% 0.003 264.542);
11
13
  --color-gray-200: oklch(92.8% 0.006 264.531);
12
- --color-gray-500: oklch(55.1% 0.027 264.364);
13
14
  --color-gray-700: oklch(37.3% 0.034 259.733);
14
15
  --color-gray-900: oklch(21% 0.034 264.665);
15
16
  --color-zinc-50: oklch(98.5% 0 0);
@@ -23,8 +24,7 @@
23
24
  --color-black: #000;
24
25
  --color-white: #fff;
25
26
  --spacing: 0.25rem;
26
- --text-xs: 0.75rem;
27
- --text-xs--line-height: calc(1 / 0.75);
27
+ --container-lg: 32rem;
28
28
  --text-sm: 0.875rem;
29
29
  --text-sm--line-height: calc(1.25 / 0.875);
30
30
  --font-weight-medium: 500;
@@ -224,8 +224,8 @@
224
224
  .z-50 {
225
225
  z-index: 50;
226
226
  }
227
- .ml-auto {
228
- margin-left: auto;
227
+ .mt-2 {
228
+ margin-top: calc(var(--spacing) * 2);
229
229
  }
230
230
  .block {
231
231
  display: block;
@@ -239,6 +239,9 @@
239
239
  .inline-block {
240
240
  display: inline-block;
241
241
  }
242
+ .inline-flex {
243
+ display: inline-flex;
244
+ }
242
245
  .size-4 {
243
246
  width: calc(var(--spacing) * 4);
244
247
  height: calc(var(--spacing) * 4);
@@ -268,6 +271,12 @@
268
271
  .w-full {
269
272
  width: 100%;
270
273
  }
274
+ .max-w-lg {
275
+ max-width: var(--container-lg);
276
+ }
277
+ .min-w-36 {
278
+ min-width: calc(var(--spacing) * 36);
279
+ }
271
280
  .flex-1 {
272
281
  flex: 1;
273
282
  }
@@ -283,9 +292,15 @@
283
292
  .flex-col {
284
293
  flex-direction: column;
285
294
  }
295
+ .flex-wrap {
296
+ flex-wrap: wrap;
297
+ }
286
298
  .items-center {
287
299
  align-items: center;
288
300
  }
301
+ .justify-center {
302
+ justify-content: center;
303
+ }
289
304
  .gap-2 {
290
305
  gap: calc(var(--spacing) * 2);
291
306
  }
@@ -308,6 +323,9 @@
308
323
  .rounded {
309
324
  border-radius: 0.25rem;
310
325
  }
326
+ .rounded-\[var\(--chip-input-border-radius\)\] {
327
+ border-radius: var(--chip-input-border-radius);
328
+ }
311
329
  .rounded-full {
312
330
  border-radius: calc(infinity * 1px);
313
331
  }
@@ -331,6 +349,9 @@
331
349
  border-left-style: var(--tw-border-style);
332
350
  border-left-width: 1px;
333
351
  }
352
+ .border-\[var\(--chip-input-border-color\)\] {
353
+ border-color: var(--chip-input-border-color);
354
+ }
334
355
  .border-gray-200 {
335
356
  border-color: var(--color-gray-200);
336
357
  }
@@ -343,8 +364,8 @@
343
364
  .border-zinc-200 {
344
365
  border-color: var(--color-zinc-200);
345
366
  }
346
- .bg-gray-100 {
347
- background-color: var(--color-gray-100);
367
+ .bg-\[var\(--chip-input-background\)\] {
368
+ background-color: var(--chip-input-background);
348
369
  }
349
370
  .bg-white {
350
371
  background-color: var(--color-white);
@@ -383,10 +404,6 @@
383
404
  font-size: var(--text-sm);
384
405
  line-height: var(--tw-leading, var(--text-sm--line-height));
385
406
  }
386
- .text-xs {
387
- font-size: var(--text-xs);
388
- line-height: var(--tw-leading, var(--text-xs--line-height));
389
- }
390
407
  .font-medium {
391
408
  --tw-font-weight: var(--font-weight-medium);
392
409
  font-weight: var(--font-weight-medium);
@@ -399,14 +416,14 @@
399
416
  --tw-tracking: var(--tracking-wide);
400
417
  letter-spacing: var(--tracking-wide);
401
418
  }
402
- .text-gray-500 {
403
- color: var(--color-gray-500);
419
+ .text-\[var\(--chip-input-text\)\] {
420
+ color: var(--chip-input-text);
404
421
  }
405
422
  .text-gray-700 {
406
423
  color: var(--color-gray-700);
407
424
  }
408
- .text-gray-900 {
409
- color: var(--color-gray-900);
425
+ .text-red-700 {
426
+ color: var(--color-red-700);
410
427
  }
411
428
  .text-zinc-500 {
412
429
  color: var(--color-zinc-500);
@@ -484,6 +501,17 @@
484
501
  --tw-ease: var(--ease-in-out);
485
502
  transition-timing-function: var(--ease-in-out);
486
503
  }
504
+ .outline-none {
505
+ --tw-outline-style: none;
506
+ outline-style: none;
507
+ }
508
+ .hover\:bg-\[var\(--chip-input-hover\)\] {
509
+ &:hover {
510
+ @media (hover: hover) {
511
+ background-color: var(--chip-input-hover);
512
+ }
513
+ }
514
+ }
487
515
  .hover\:bg-gray-100 {
488
516
  &:hover {
489
517
  @media (hover: hover) {
@@ -491,6 +519,13 @@
491
519
  }
492
520
  }
493
521
  }
522
+ .hover\:bg-red-50 {
523
+ &:hover {
524
+ @media (hover: hover) {
525
+ background-color: var(--color-red-50);
526
+ }
527
+ }
528
+ }
494
529
  .hover\:bg-zinc-100 {
495
530
  &:hover {
496
531
  @media (hover: hover) {
@@ -504,6 +539,16 @@
504
539
  outline-style: none;
505
540
  }
506
541
  }
542
+ .disabled\:cursor-not-allowed {
543
+ &:disabled {
544
+ cursor: not-allowed;
545
+ }
546
+ }
547
+ .disabled\:opacity-40 {
548
+ &:disabled {
549
+ opacity: 40%;
550
+ }
551
+ }
507
552
  }
508
553
  @layer base {
509
554
  html, body {
@@ -551,6 +596,11 @@
551
596
  height: 1px;
552
597
  width: 100%;
553
598
  }
599
+ .chip-input-action {
600
+ width: calc(var(--chip-input-font-size) + 16px);
601
+ height: calc(var(--chip-input-font-size) + 16px);
602
+ padding: 0;
603
+ }
554
604
  }
555
605
  @layer theme {
556
606
  :root {
@@ -589,6 +639,15 @@
589
639
  0px,
590
640
  calc(var(--segmented-picker-border-radius) - var(--segmented-picker-outer-padding))
591
641
  );
642
+ --chip-input-gap: var(--spacing);
643
+ --chip-input-padding: 4px 8px;
644
+ --chip-input-background: var(--color-zinc-100);
645
+ --chip-input-hover: var(--color-zinc-200);
646
+ --chip-input-text: var(--color-black);
647
+ --chip-input-font-size: 12px;
648
+ --chip-input-border-radius: 9999px;
649
+ --chip-input-border-color: transparent;
650
+ --chip-input-highlight: var(--color-zinc-300);
592
651
  }
593
652
  }
594
653
  @property --tw-rotate-x {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x33025/sveltely",
3
- "version": "0.0.23",
3
+ "version": "0.0.25",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",
@@ -1,52 +0,0 @@
1
- <script lang="ts">
2
- import { getContext } from 'svelte';
3
- import type { Snippet } from 'svelte';
4
-
5
- interface Props {
6
- value: string;
7
- disabled?: boolean;
8
- class?: string;
9
- activeClass?: string;
10
- children?: Snippet;
11
- }
12
-
13
- let {
14
- value,
15
- disabled = false,
16
- class: className = '',
17
- activeClass = '',
18
- children
19
- }: Props = $props();
20
-
21
- const context = getContext<{
22
- active: string;
23
- setActive: (value: string) => void;
24
- registerTab?: (value: string) => void;
25
- }>('tabView');
26
-
27
- const isActive = $derived(context.active === value);
28
-
29
- $effect(() => {
30
- context.registerTab?.(value);
31
- });
32
-
33
- function handleClick() {
34
- if (disabled) return;
35
- context.setActive(value);
36
- }
37
- </script>
38
-
39
- <button
40
- id={'tab-' + value}
41
- type="button"
42
- role="tab"
43
- aria-selected={isActive}
44
- aria-controls={'panel-' + value}
45
- {disabled}
46
- class="{className} {isActive ? activeClass : ''}"
47
- onclick={handleClick}
48
- >
49
- {#if children}
50
- {@render children()}
51
- {/if}
52
- </button>
@@ -1,11 +0,0 @@
1
- import type { Snippet } from 'svelte';
2
- interface Props {
3
- value: string;
4
- disabled?: boolean;
5
- class?: string;
6
- activeClass?: string;
7
- children?: Snippet;
8
- }
9
- declare const Tab: import("svelte").Component<Props, {}, "">;
10
- type Tab = ReturnType<typeof Tab>;
11
- export default Tab;
@@ -1,32 +0,0 @@
1
- <script lang="ts">
2
- import { getContext } from 'svelte';
3
- import type { Snippet } from 'svelte';
4
-
5
- interface Props {
6
- value: string;
7
- class?: string;
8
- children?: Snippet;
9
- }
10
-
11
- let { value, class: className = '', children }: Props = $props();
12
-
13
- const context = getContext<{
14
- active: string;
15
- setActive: (value: string) => void;
16
- }>('tabView');
17
-
18
- const isActive = $derived(context.active === value);
19
- </script>
20
-
21
- {#if isActive}
22
- <div
23
- id={"panel-" + value}
24
- role="tabpanel"
25
- aria-labelledby={"tab-" + value}
26
- class={className}
27
- >
28
- {#if children}
29
- {@render children()}
30
- {/if}
31
- </div>
32
- {/if}
@@ -1,9 +0,0 @@
1
- import type { Snippet } from 'svelte';
2
- interface Props {
3
- value: string;
4
- class?: string;
5
- children?: Snippet;
6
- }
7
- declare const TabPanel: import("svelte").Component<Props, {}, "">;
8
- type TabPanel = ReturnType<typeof TabPanel>;
9
- export default TabPanel;
@@ -1,38 +0,0 @@
1
- <script lang="ts">
2
- import { setContext } from 'svelte';
3
- import type { Snippet } from 'svelte';
4
-
5
- interface Props {
6
- active: string;
7
- onChange?: (value: string) => void;
8
- tabs?: Snippet;
9
- panels?: Snippet;
10
- }
11
-
12
- let { active = $bindable(), onChange, tabs, panels }: Props = $props();
13
-
14
- function setActive(value: string) {
15
- if (active === value) return;
16
- active = value;
17
- onChange?.(value);
18
- }
19
-
20
- setContext('tabView', {
21
- get active() {
22
- return active;
23
- },
24
- setActive
25
- });
26
- </script>
27
-
28
- {#if tabs}
29
- <div role="tablist">
30
- {@render tabs()}
31
- </div>
32
- {/if}
33
-
34
- {#if panels}
35
- <div>
36
- {@render panels()}
37
- </div>
38
- {/if}
@@ -1,10 +0,0 @@
1
- import type { Snippet } from 'svelte';
2
- interface Props {
3
- active: string;
4
- onChange?: (value: string) => void;
5
- tabs?: Snippet;
6
- panels?: Snippet;
7
- }
8
- declare const TabView: import("svelte").Component<Props, {}, "active">;
9
- type TabView = ReturnType<typeof TabView>;
10
- export default TabView;
@@ -1,8 +0,0 @@
1
- import TabViewRoot from './TabView.svelte';
2
- import Tab from './Tab.svelte';
3
- import TabPanel from './TabPanel.svelte';
4
- declare const TabView: typeof TabViewRoot & {
5
- Tab: typeof Tab;
6
- Panel: typeof TabPanel;
7
- };
8
- export default TabView;
@@ -1,7 +0,0 @@
1
- import TabViewRoot from './TabView.svelte';
2
- import Tab from './Tab.svelte';
3
- import TabPanel from './TabPanel.svelte';
4
- const TabView = TabViewRoot;
5
- TabView.Tab = Tab;
6
- TabView.Panel = TabPanel;
7
- export default TabView;