Tabs.svelte 2.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
  1. <script lang="ts">
  2. import type { Snippet } from 'svelte';
  3. import { cn } from '$lib/utils/cn.js';
  4. import { setContext } from 'svelte';
  5. interface Tab {
  6. id: string;
  7. label: string;
  8. icon?: Snippet;
  9. disabled?: boolean;
  10. }
  11. interface Props {
  12. tabs: Tab[];
  13. active?: string;
  14. class?: string;
  15. panelClass?: string;
  16. children?: Snippet<[string]>;
  17. onchange?: (id: string) => void;
  18. }
  19. let {
  20. tabs,
  21. active = $bindable(tabs[0]?.id ?? ''),
  22. class: className = '',
  23. panelClass = '',
  24. children,
  25. onchange,
  26. }: Props = $props();
  27. function select(id: string) {
  28. active = id;
  29. onchange?.(id);
  30. }
  31. setContext('tabs', { get active() { return active; } });
  32. </script>
  33. <div class={cn('flex flex-col gap-0', className)}>
  34. <!-- Tab list -->
  35. <div
  36. role="tablist"
  37. class="flex items-center gap-0.5 border-b border-[var(--border)] overflow-x-auto"
  38. >
  39. {#each tabs as tab}
  40. <button
  41. role="tab"
  42. aria-selected={active === tab.id}
  43. aria-controls="tabpanel-{tab.id}"
  44. disabled={tab.disabled}
  45. onclick={() => select(tab.id)}
  46. class={cn(
  47. 'relative flex items-center gap-1.5 px-3.5 py-2.5 text-sm font-medium',
  48. 'whitespace-nowrap transition-colors duration-[150ms]',
  49. 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)] focus-visible:ring-inset',
  50. 'cursor-pointer disabled:cursor-not-allowed disabled:opacity-40',
  51. active === tab.id
  52. ? 'text-[var(--foreground)]'
  53. : 'text-[var(--muted-foreground)] hover:text-[var(--foreground)]'
  54. )}
  55. >
  56. {#if tab.icon}{@render tab.icon()}{/if}
  57. {tab.label}
  58. <!-- Active indicator -->
  59. {#if active === tab.id}
  60. <span
  61. class="absolute bottom-0 left-0 right-0 h-0.5 bg-[var(--primary)] rounded-t-[2px]"
  62. ></span>
  63. {/if}
  64. </button>
  65. {/each}
  66. </div>
  67. <!-- Panel -->
  68. <div
  69. id="tabpanel-{active}"
  70. role="tabpanel"
  71. class={cn('mt-4', panelClass)}
  72. >
  73. {#if children}{@render children(active)}{/if}
  74. </div>
  75. </div>