tera-system-ui 0.2.2 → 0.2.4
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.
- package/dist/components/bottom-sheet/BottomSheet.recipe.d.ts +20 -0
- package/dist/components/bottom-sheet/BottomSheet.svelte +113 -4
- package/dist/components/bottom-sheet/bottomSheetStore.svelte.d.ts +28 -1
- package/dist/components/bottom-sheet/bottomSheetStore.svelte.js +60 -39
- package/dist/components/combobox/Combobox.svelte +6 -0
- package/dist/components/date-range-picker/DateRangePicker.svelte +14 -1
- package/dist/components/dialog/Dialog.svelte +15 -2
- package/dist/components/drawer/Drawer.svelte +23 -1
- package/dist/components/dropdown-menu/components/DropdownMenu.svelte +7 -0
- package/dist/components/popover/Popover.svelte +8 -1
- package/dist/components/select/Select.svelte +14 -2
- package/dist/components/toast/ToastContainer.svelte +8 -1
- package/dist/components/tooltip/Tooltip.recipe.js +6 -1
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.js +3 -0
- package/dist/hooks/overlayLayer.svelte.d.ts +43 -0
- package/dist/hooks/overlayLayer.svelte.js +74 -0
- package/dist/hooks/overlayStack.svelte.d.ts +59 -0
- package/dist/hooks/overlayStack.svelte.js +107 -0
- package/dist/llms/bottom-sheet.md +2 -0
- package/dist/tokens/index.d.ts +35 -0
- package/dist/tokens/index.js +34 -0
- package/package.json +6 -1
- package/registry/bottom-sheet.json +50 -50
- package/registry/combobox.json +2 -2
- package/registry/date-range-picker.json +2 -2
- package/registry/dialog.json +1 -1
- package/registry/drawer.json +2 -2
- package/registry/dropdown-menu.json +2 -2
- package/registry/popover.json +2 -2
- package/registry/select.json +2 -2
- package/registry/toast.json +1 -1
- package/registry/tooltip.json +1 -1
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"path": "src/lib/components/date-range-picker/DateRangePicker.svelte",
|
|
16
16
|
"target": "DateRangePicker.svelte",
|
|
17
17
|
"type": "svelte",
|
|
18
|
-
"content": "<script lang=\"ts\">\n import type {DateRange} from 'bits-ui';\n import {DateRangeField, Popover as PopoverPrimitive, RangeCalendar as CalendarPrimitive} from 'bits-ui';\n import {CalendarDate} from '@internationalized/date';\n import IconCalendar from \"@tabler/icons-svelte/icons/calendar\";\n import IconChevronLeft from \"@tabler/icons-svelte/icons/chevron-left\";\n import IconChevronRight from \"@tabler/icons-svelte/icons/chevron-right\";\n import {IconX} from '$lib/internal/icons';\n import {cn} from '$lib/utils/utils.js';\n import {untrack} from 'svelte';\n import {innerWidth} from 'svelte/reactivity/window';\n import {Dialog} from '$lib/components/dialog/index.js';\n import {dateRangePickerRecipe, type DateRangePickerProps} from './DateRangePicker.recipe.js';\n\n let {\n dateRange = $bindable([]),\n onchange,\n enableTime = false,\n placeholder = 'Select date range',\n disabled = false,\n class: className,\n minValue,\n maxValue,\n isDateDisabled,\n numberOfMonths: numberOfMonthsProp,\n weekStartsOn,\n calendarIcon,\n clearIcon,\n prevIcon,\n nextIcon,\n closeIcon,\n }: DateRangePickerProps = $props();\n\n const styles = dateRangePickerRecipe();\n\n const isDesktop = $derived((innerWidth.current ?? 768) >= 768);\n\n let open = $state(false);\n\n // ── Date-field value: drives DateRangeField segments ──────────────────\n function toCalendarDate(date: Date): CalendarDate {\n return new CalendarDate(date.getFullYear(), date.getMonth() + 1, date.getDate());\n }\n\n function fromCalendarDate(date: CalendarDate, timeStr: string): Date {\n const [h, m] = timeStr.split(':').map(Number);\n return new Date(date.year, date.month - 1, date.day, h, m);\n }\n\n const [initD0, initD1] = dateRange;\n let fieldValue = $state<DateRange>({\n start: initD0 ? toCalendarDate(initD0) : undefined,\n end: initD1 ? toCalendarDate(initD1) : undefined,\n });\n\n // ── Pending: temporary calendar state while picker is open ────────────\n let pendingRange = $state<DateRange>({start: undefined, end: undefined});\n let startTime = $state('00:00');\n let endTime = $state('23:59');\n // Derived: highlight whichever quick-select preset matches the current\n // pendingRange (works for manual picks, quick-select clicks, and pre-set values).\n const activeQuickSelect = $derived.by((): string | null => {\n if (!pendingRange.start || !pendingRange.end) return null;\n const ps = pendingRange.start as CalendarDate;\n const pe = pendingRange.end as CalendarDate;\n for (const {key} of quickRanges) {\n const {start, end} = getQuickRange(key);\n if (ps.compare(toCalendarDate(start)) === 0 &&\n pe.compare(toCalendarDate(end)) === 0) {\n return key;\n }\n }\n return null;\n });\n\n const hasRange = $derived(dateRange.length >= 2);\n const canApply = $derived(!!pendingRange.start && !!pendingRange.end);\n\n // ── Field direct-edit handler ─────────────────────────────────────────\n function handleFieldChange(range: DateRange | undefined) {\n if (!range) return;\n fieldValue = range;\n if (range.start && range.end) {\n const result: Date[] = [\n fromCalendarDate(range.start as CalendarDate, enableTime ? startTime : '00:00'),\n fromCalendarDate(range.end as CalendarDate, enableTime ? endTime : '23:59'),\n ];\n dateRange = result;\n onchange?.(result);\n } else if (!range.start && !range.end && dateRange.length > 0) {\n dateRange = [];\n onchange?.([]);\n }\n }\n\n // ── Picker open: copy fieldValue → pendingRange ───────────────────────\n function initPendingState() {\n pendingRange = {start: fieldValue.start, end: fieldValue.end};\n const [d0, d1] = dateRange;\n if (enableTime && d0 && d1) {\n const sh = d0.getHours(), sm = d0.getMinutes();\n const eh = d1.getHours(), em = d1.getMinutes();\n startTime = `${String(sh).padStart(2, '0')}:${String(sm).padStart(2, '0')}`;\n endTime = `${String(eh).padStart(2, '0')}:${String(em).padStart(2, '0')}`;\n } else {\n startTime = '00:00';\n endTime = '23:59';\n }\n }\n\n $effect(() => {\n if (open) untrack(initPendingState);\n });\n\n // ── Apply / Reset ─────────────────────────────────────────────────────\n function applyRange() {\n if (!pendingRange.start || !pendingRange.end) return;\n const start = pendingRange.start as CalendarDate;\n const end = pendingRange.end as CalendarDate;\n fieldValue = {start, end};\n const result: Date[] = [\n fromCalendarDate(start, enableTime ? startTime : '00:00'),\n fromCalendarDate(end, enableTime ? endTime : '23:59'),\n ];\n dateRange = result;\n onchange?.(result);\n open = false;\n }\n\n function resetRange() {\n fieldValue = {start: undefined, end: undefined};\n dateRange = [];\n onchange?.([]);\n open = false;\n }\n\n // ── Quick select ──────────────────────────────────────────────────────\n function applyQuickSelect(key: string) {\n const {start, end} = getQuickRange(key);\n pendingRange = {start: toCalendarDate(start), end: toCalendarDate(end)};\n }\n\n function getQuickRange(key: string): { start: Date; end: Date } {\n const now = new Date();\n const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());\n switch (key) {\n case 'today':\n return {start: new Date(today), end: new Date(today)};\n case 'yesterday': {\n const y = new Date(today);\n y.setDate(today.getDate() - 1);\n return {start: y, end: new Date(y)};\n }\n case 'thisWeek': {\n const dow = today.getDay();\n const mon = new Date(today);\n mon.setDate(today.getDate() - (dow === 0 ? 6 : dow - 1));\n return {start: mon, end: new Date(today)};\n }\n case 'last7': {\n const s = new Date(today);\n s.setDate(today.getDate() - 6);\n return {start: s, end: new Date(today)};\n }\n case 'last30': {\n const s = new Date(today);\n s.setDate(today.getDate() - 29);\n return {start: s, end: new Date(today)};\n }\n case 'thisMonth':\n return {\n start: new Date(today.getFullYear(), today.getMonth(), 1),\n end: new Date(today.getFullYear(), today.getMonth() + 1, 0)\n };\n case 'lastMonth':\n return {\n start: new Date(today.getFullYear(), today.getMonth() - 1, 1),\n end: new Date(today.getFullYear(), today.getMonth(), 0)\n };\n case 'thisQuarter': {\n const q = Math.floor(today.getMonth() / 3);\n return {\n start: new Date(today.getFullYear(), q * 3, 1),\n end: new Date(today.getFullYear(), q * 3 + 3, 0)\n };\n }\n case 'last3Months':\n return {\n start: new Date(today.getFullYear(), today.getMonth() - 3, today.getDate()),\n end: new Date(today)\n };\n default:\n return {start: new Date(today), end: new Date(today)};\n }\n }\n\n const quickRanges = [\n {key: 'today', label: 'Today'},\n {key: 'yesterday', label: 'Yesterday'},\n {key: 'thisWeek', label: 'This week'},\n {key: 'last7', label: 'Last 7 days'},\n {key: 'last30', label: 'Last 30 days'},\n {key: 'thisMonth', label: 'This month'},\n {key: 'lastMonth', label: 'Last month'},\n {key: 'thisQuarter', label: 'This quarter'},\n {key: 'last3Months', label: 'Last 3 months'},\n ];\n\n // ── Styles (recipe-driven) ────────────────────────────────────────────\n const triggerStyles = $derived(styles.trigger({disabled}));\n const iconBtnStyles = styles.iconBtn();\n const segmentStyles = styles.segment();\n const dayStyles = styles.day();\n const popoverContentStyles = styles.panel();\n</script>\n\n<!-- ─── Shared snippets ──────────────────────────────────────────────────── -->\n\n{#snippet dateField()}\n <DateRangeField.Root\n bind:value={fieldValue}\n onValueChange={handleFieldChange}\n {disabled}\n class={styles.field()}\n >\n <DateRangeField.Input type=\"start\" class=\"flex items-center gap-0\">\n {#snippet children({segments})}\n {#each segments as {part, value}, si (si)}\n {#if part === 'literal'}\n <span class=\"text-text-tertiary text-sm select-none\">{value}</span>\n {:else}\n <DateRangeField.Segment {part} class={segmentStyles}>{value}</DateRangeField.Segment>\n {/if}\n {/each}\n {/snippet}\n </DateRangeField.Input>\n\n <span class=\"text-text-tertiary text-sm mx-1.5 select-none shrink-0\">–</span>\n\n <DateRangeField.Input type=\"end\" class=\"flex items-center gap-0\">\n {#snippet children({segments})}\n {#each segments as {part, value}, si (si)}\n {#if part === 'literal'}\n <span class=\"text-text-tertiary text-sm select-none\">{value}</span>\n {:else}\n <DateRangeField.Segment {part} class={segmentStyles}>{value}</DateRangeField.Segment>\n {/if}\n {/each}\n {/snippet}\n </DateRangeField.Input>\n </DateRangeField.Root>\n{/snippet}\n\n{#snippet calendarGrid(numberOfMonths: number)}\n <CalendarPrimitive.Root\n bind:value={pendingRange}\n {numberOfMonths}\n weekdayFormat=\"short\"\n fixedWeeks\n {minValue}\n {maxValue}\n isDateDisabled={isDateDisabled ? (d) => isDateDisabled(d as CalendarDate) : undefined}\n weekStartsOn={weekStartsOn}\n >\n {#snippet children({months, weekdays})}\n <div class={cn('flex justify-center', numberOfMonths > 1 && 'gap-6')}>\n {#each months as month, i (month.value.toString())}\n <div class=\"flex flex-col gap-2\">\n <!-- Month heading -->\n <div class=\"flex items-center justify-between gap-2\">\n {#if i === 0}\n <CalendarPrimitive.PrevButton class={styles.navBtn()}>\n {#if prevIcon}{@render prevIcon()}{:else}\n <IconChevronLeft class=\"size-4\"/>\n {/if}\n </CalendarPrimitive.PrevButton>\n {:else}\n <div class=\"size-7 shrink-0\"></div>\n {/if}\n\n <span class={styles.monthLabel()}>\n {new Date(month.value.year, month.value.month - 1)\n .toLocaleDateString('en-US', {month: 'long', year: 'numeric'})}\n </span>\n\n {#if i === months.length - 1}\n <CalendarPrimitive.NextButton class={styles.navBtn()}>\n {#if nextIcon}{@render nextIcon()}{:else}\n <IconChevronRight class=\"size-4\"/>\n {/if}\n </CalendarPrimitive.NextButton>\n {:else}\n <div class=\"size-7 shrink-0\"></div>\n {/if}\n </div>\n\n <!-- Grid -->\n <CalendarPrimitive.Grid class=\"w-full border-collapse\">\n <CalendarPrimitive.GridHead>\n <CalendarPrimitive.GridRow class=\"flex\">\n {#each weekdays as day (day)}\n <CalendarPrimitive.HeadCell class={styles.weekday()}>\n {day}\n </CalendarPrimitive.HeadCell>\n {/each}\n </CalendarPrimitive.GridRow>\n </CalendarPrimitive.GridHead>\n <CalendarPrimitive.GridBody>\n {#each month.weeks as weekDates, wi (wi)}\n <CalendarPrimitive.GridRow class=\"flex mt-0.5\">\n {#each weekDates as date (date.toString())}\n <CalendarPrimitive.Cell\n {date}\n month={month.value}\n class={styles.cell()}\n >\n <CalendarPrimitive.Day class={dayStyles}/>\n </CalendarPrimitive.Cell>\n {/each}\n </CalendarPrimitive.GridRow>\n {/each}\n </CalendarPrimitive.GridBody>\n </CalendarPrimitive.Grid>\n </div>\n {/each}\n </div>\n {/snippet}\n </CalendarPrimitive.Root>\n{/snippet}\n\n{#snippet timePickers()}\n <div class={styles.timeBar()}>\n <span class={styles.timeLabelStart()}>Start</span>\n <input\n type=\"time\"\n bind:value={startTime}\n class={styles.timeInput()}\n />\n <span class={styles.timeArrow()}>→</span>\n <span class={styles.timeLabelEnd()}>End</span>\n <input\n type=\"time\"\n bind:value={endTime}\n class={styles.timeInput()}\n />\n </div>\n{/snippet}\n\n{#snippet pickerFooter()}\n <div class={styles.footer()}>\n <button\n class={styles.footerReset()}\n onclick={resetRange}\n >\n Reset\n </button>\n <button\n disabled={!canApply}\n class={styles.footerApply()}\n onclick={applyRange}\n >\n Apply\n </button>\n </div>\n{/snippet}\n\n<!-- ─── Root ─────────────────────────────────────────────────────────────── -->\n\n<div class={cn('inline-flex w-full', className)}>\n {#if isDesktop}\n <!-- ── DESKTOP: Popover anchored to the whole trigger container ── -->\n <PopoverPrimitive.Root bind:open>\n <PopoverPrimitive.Trigger class={triggerStyles}>\n {#snippet child({props: triggerProps})}\n <!--\n Intercept clicks: only toggle the popover when clicking\n outside the date-field area (calendar icon, empty space).\n -->\n <div\n {...triggerProps}\n onclick={(e) => {\n if (!(e.target as HTMLElement).closest('.drp-field')) {\n (triggerProps.onclick as EventListener | undefined)?.(e);\n }\n }}\n class={triggerStyles}\n >\n {@render dateField()}\n\n <div class=\"flex items-center gap-1 shrink-0 ml-1\">\n {#if calendarIcon}{@render calendarIcon()}{:else}\n <IconCalendar class=\"size-4 text-text-tertiary pointer-events-none\"/>\n {/if}\n {#if hasRange}\n <button\n type=\"button\"\n class={iconBtnStyles}\n aria-label=\"Clear date range\"\n onclick={(e) => { e.stopPropagation(); resetRange(); }}\n >\n {#if clearIcon}{@render clearIcon()}{:else}\n <IconX class=\"size-3\"/>\n {/if}\n </button>\n {/if}\n </div>\n </div>\n {/snippet}\n </PopoverPrimitive.Trigger>\n\n <PopoverPrimitive.Portal>\n <PopoverPrimitive.Content sideOffset={4} align=\"start\" class={popoverContentStyles}>\n <div class=\"flex\">\n <!-- Quick-select sidebar -->\n <div class={styles.presetSidebar()}>\n <p class={styles.presetHeading()}>\n Quick Select\n </p>\n {#each quickRanges as range (range.key)}\n <button\n class={styles.presetItem({presetActive: activeQuickSelect === range.key})}\n onclick={() => applyQuickSelect(range.key)}\n >\n {range.label}\n </button>\n {/each}\n </div>\n\n <!-- Calendar + time + footer -->\n <div class=\"flex flex-col min-w-0\">\n <div class=\"p-4\">\n {@render calendarGrid(numberOfMonthsProp ?? 2)}\n </div>\n {#if enableTime}{@render timePickers()}{/if}\n {@render pickerFooter()}\n </div>\n </div>\n </PopoverPrimitive.Content>\n </PopoverPrimitive.Portal>\n </PopoverPrimitive.Root>\n\n {:else}\n <!-- ── MOBILE: plain trigger → Dialog (bottom-sheet style) ─────── -->\n <div class={triggerStyles}>\n {@render dateField()}\n\n <div class=\"flex items-center gap-1 shrink-0 ml-1\">\n <button\n class={iconBtnStyles}\n aria-label=\"Open date picker\"\n onclick={() => (open = true)}\n >\n {#if calendarIcon}{@render calendarIcon()}{:else}\n <IconCalendar class=\"size-4\"/>\n {/if}\n </button>\n {#if hasRange}\n <button class={iconBtnStyles} aria-label=\"Clear date range\" onclick={resetRange}>\n {#if clearIcon}{@render clearIcon()}{:else}\n <IconX class=\"size-3\"/>\n {/if}\n </button>\n {/if}\n </div>\n </div>\n\n <Dialog bind:open size=\"sm\" closeButton={false} padding=\"none\">\n <div class=\"flex flex-col\">\n <!-- Dialog header -->\n <div class={styles.dialogHeader()}>\n <span class={styles.dialogTitle()}>Select Date Range</span>\n <button class={iconBtnStyles} aria-label=\"Close\" onclick={() => (open = false)}>\n {#if closeIcon}{@render closeIcon()}{:else}\n <IconX class=\"size-4\"/>\n {/if}\n </button>\n </div>\n\n <!-- Horizontal quick-select pills -->\n <div class={styles.pillBar()}\n style=\"scrollbar-width:none;-ms-overflow-style:none;\">\n {#each quickRanges as range (range.key)}\n <button\n class={styles.presetPill({pillActive: activeQuickSelect === range.key})}\n onclick={() => applyQuickSelect(range.key)}\n >\n {range.label}\n </button>\n {/each}\n </div>\n\n <!-- Single-month calendar -->\n <div class=\"p-4 overflow-auto\">\n {@render calendarGrid(1)}\n </div>\n\n {#if enableTime}{@render timePickers()}{/if}\n {@render pickerFooter()}\n </div>\n </Dialog>\n {/if}\n</div>\n"
|
|
18
|
+
"content": "<script lang=\"ts\">\n import type {DateRange} from 'bits-ui';\n import {DateRangeField, Popover as PopoverPrimitive, RangeCalendar as CalendarPrimitive} from 'bits-ui';\n import {CalendarDate} from '@internationalized/date';\n import IconCalendar from \"@tabler/icons-svelte/icons/calendar\";\n import IconChevronLeft from \"@tabler/icons-svelte/icons/chevron-left\";\n import IconChevronRight from \"@tabler/icons-svelte/icons/chevron-right\";\n import {IconX} from '$lib/internal/icons';\n import {cn} from '$lib/utils/utils.js';\n import {untrack} from 'svelte';\n import {innerWidth} from 'svelte/reactivity/window';\n import {Dialog} from '$lib/components/dialog/index.js';\n import {overlayLayer, withZIndex} from '$lib/hooks';\n import {dateRangePickerRecipe, type DateRangePickerProps} from './DateRangePicker.recipe.js';\n\n let {\n dateRange = $bindable([]),\n onchange,\n enableTime = false,\n placeholder = 'Select date range',\n disabled = false,\n class: className,\n minValue,\n maxValue,\n isDateDisabled,\n numberOfMonths: numberOfMonthsProp,\n weekStartsOn,\n calendarIcon,\n clearIcon,\n prevIcon,\n nextIcon,\n closeIcon,\n }: DateRangePickerProps = $props();\n\n const styles = dateRangePickerRecipe();\n\n const isDesktop = $derived((innerWidth.current ?? 768) >= 768);\n\n let open = $state(false);\n\n // ── Date-field value: drives DateRangeField segments ──────────────────\n function toCalendarDate(date: Date): CalendarDate {\n return new CalendarDate(date.getFullYear(), date.getMonth() + 1, date.getDate());\n }\n\n function fromCalendarDate(date: CalendarDate, timeStr: string): Date {\n const [h, m] = timeStr.split(':').map(Number);\n return new Date(date.year, date.month - 1, date.day, h, m);\n }\n\n const [initD0, initD1] = dateRange;\n let fieldValue = $state<DateRange>({\n start: initD0 ? toCalendarDate(initD0) : undefined,\n end: initD1 ? toCalendarDate(initD1) : undefined,\n });\n\n // ── Pending: temporary calendar state while picker is open ────────────\n let pendingRange = $state<DateRange>({start: undefined, end: undefined});\n let startTime = $state('00:00');\n let endTime = $state('23:59');\n // Derived: highlight whichever quick-select preset matches the current\n // pendingRange (works for manual picks, quick-select clicks, and pre-set values).\n const activeQuickSelect = $derived.by((): string | null => {\n if (!pendingRange.start || !pendingRange.end) return null;\n const ps = pendingRange.start as CalendarDate;\n const pe = pendingRange.end as CalendarDate;\n for (const {key} of quickRanges) {\n const {start, end} = getQuickRange(key);\n if (ps.compare(toCalendarDate(start)) === 0 &&\n pe.compare(toCalendarDate(end)) === 0) {\n return key;\n }\n }\n return null;\n });\n\n const hasRange = $derived(dateRange.length >= 2);\n const canApply = $derived(!!pendingRange.start && !!pendingRange.end);\n\n // ── Field direct-edit handler ─────────────────────────────────────────\n function handleFieldChange(range: DateRange | undefined) {\n if (!range) return;\n fieldValue = range;\n if (range.start && range.end) {\n const result: Date[] = [\n fromCalendarDate(range.start as CalendarDate, enableTime ? startTime : '00:00'),\n fromCalendarDate(range.end as CalendarDate, enableTime ? endTime : '23:59'),\n ];\n dateRange = result;\n onchange?.(result);\n } else if (!range.start && !range.end && dateRange.length > 0) {\n dateRange = [];\n onchange?.([]);\n }\n }\n\n // ── Picker open: copy fieldValue → pendingRange ───────────────────────\n function initPendingState() {\n pendingRange = {start: fieldValue.start, end: fieldValue.end};\n const [d0, d1] = dateRange;\n if (enableTime && d0 && d1) {\n const sh = d0.getHours(), sm = d0.getMinutes();\n const eh = d1.getHours(), em = d1.getMinutes();\n startTime = `${String(sh).padStart(2, '0')}:${String(sm).padStart(2, '0')}`;\n endTime = `${String(eh).padStart(2, '0')}:${String(em).padStart(2, '0')}`;\n } else {\n startTime = '00:00';\n endTime = '23:59';\n }\n }\n\n $effect(() => {\n if (open) untrack(initPendingState);\n });\n\n // ── Apply / Reset ─────────────────────────────────────────────────────\n function applyRange() {\n if (!pendingRange.start || !pendingRange.end) return;\n const start = pendingRange.start as CalendarDate;\n const end = pendingRange.end as CalendarDate;\n fieldValue = {start, end};\n const result: Date[] = [\n fromCalendarDate(start, enableTime ? startTime : '00:00'),\n fromCalendarDate(end, enableTime ? endTime : '23:59'),\n ];\n dateRange = result;\n onchange?.(result);\n open = false;\n }\n\n function resetRange() {\n fieldValue = {start: undefined, end: undefined};\n dateRange = [];\n onchange?.([]);\n open = false;\n }\n\n // ── Quick select ──────────────────────────────────────────────────────\n function applyQuickSelect(key: string) {\n const {start, end} = getQuickRange(key);\n pendingRange = {start: toCalendarDate(start), end: toCalendarDate(end)};\n }\n\n function getQuickRange(key: string): { start: Date; end: Date } {\n const now = new Date();\n const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());\n switch (key) {\n case 'today':\n return {start: new Date(today), end: new Date(today)};\n case 'yesterday': {\n const y = new Date(today);\n y.setDate(today.getDate() - 1);\n return {start: y, end: new Date(y)};\n }\n case 'thisWeek': {\n const dow = today.getDay();\n const mon = new Date(today);\n mon.setDate(today.getDate() - (dow === 0 ? 6 : dow - 1));\n return {start: mon, end: new Date(today)};\n }\n case 'last7': {\n const s = new Date(today);\n s.setDate(today.getDate() - 6);\n return {start: s, end: new Date(today)};\n }\n case 'last30': {\n const s = new Date(today);\n s.setDate(today.getDate() - 29);\n return {start: s, end: new Date(today)};\n }\n case 'thisMonth':\n return {\n start: new Date(today.getFullYear(), today.getMonth(), 1),\n end: new Date(today.getFullYear(), today.getMonth() + 1, 0)\n };\n case 'lastMonth':\n return {\n start: new Date(today.getFullYear(), today.getMonth() - 1, 1),\n end: new Date(today.getFullYear(), today.getMonth(), 0)\n };\n case 'thisQuarter': {\n const q = Math.floor(today.getMonth() / 3);\n return {\n start: new Date(today.getFullYear(), q * 3, 1),\n end: new Date(today.getFullYear(), q * 3 + 3, 0)\n };\n }\n case 'last3Months':\n return {\n start: new Date(today.getFullYear(), today.getMonth() - 3, today.getDate()),\n end: new Date(today)\n };\n default:\n return {start: new Date(today), end: new Date(today)};\n }\n }\n\n const quickRanges = [\n {key: 'today', label: 'Today'},\n {key: 'yesterday', label: 'Yesterday'},\n {key: 'thisWeek', label: 'This week'},\n {key: 'last7', label: 'Last 7 days'},\n {key: 'last30', label: 'Last 30 days'},\n {key: 'thisMonth', label: 'This month'},\n {key: 'lastMonth', label: 'Last month'},\n {key: 'thisQuarter', label: 'This quarter'},\n {key: 'last3Months', label: 'Last 3 months'},\n ];\n\n // ── Styles (recipe-driven) ────────────────────────────────────────────\n const triggerStyles = $derived(styles.trigger({disabled}));\n const iconBtnStyles = styles.iconBtn();\n const segmentStyles = styles.segment();\n const dayStyles = styles.day();\n const popoverContentStyles = styles.panel();\n\n // Shared stacking authority — gated on the DESKTOP branch only. On mobile this component\n // renders a real <Dialog>, which registers itself; a second entry here would own no\n // element to receive z (the `panel` slot only exists on the desktop popover) while still\n // consuming a stack slot and shifting every later overlay's depth for nothing.\n const layer = overlayLayer({isOpen: () => open && isDesktop, kind: 'popover'});\n const panelZIndex = $derived(layer.zIndex === undefined ? undefined : layer.zIndex + 1);\n</script>\n\n<!-- ─── Shared snippets ──────────────────────────────────────────────────── -->\n\n{#snippet dateField()}\n <DateRangeField.Root\n bind:value={fieldValue}\n onValueChange={handleFieldChange}\n {disabled}\n class={styles.field()}\n >\n <DateRangeField.Input type=\"start\" class=\"flex items-center gap-0\">\n {#snippet children({segments})}\n {#each segments as {part, value}, si (si)}\n {#if part === 'literal'}\n <span class=\"text-text-tertiary text-sm select-none\">{value}</span>\n {:else}\n <DateRangeField.Segment {part} class={segmentStyles}>{value}</DateRangeField.Segment>\n {/if}\n {/each}\n {/snippet}\n </DateRangeField.Input>\n\n <span class=\"text-text-tertiary text-sm mx-1.5 select-none shrink-0\">–</span>\n\n <DateRangeField.Input type=\"end\" class=\"flex items-center gap-0\">\n {#snippet children({segments})}\n {#each segments as {part, value}, si (si)}\n {#if part === 'literal'}\n <span class=\"text-text-tertiary text-sm select-none\">{value}</span>\n {:else}\n <DateRangeField.Segment {part} class={segmentStyles}>{value}</DateRangeField.Segment>\n {/if}\n {/each}\n {/snippet}\n </DateRangeField.Input>\n </DateRangeField.Root>\n{/snippet}\n\n{#snippet calendarGrid(numberOfMonths: number)}\n <CalendarPrimitive.Root\n bind:value={pendingRange}\n {numberOfMonths}\n weekdayFormat=\"short\"\n fixedWeeks\n {minValue}\n {maxValue}\n isDateDisabled={isDateDisabled ? (d) => isDateDisabled(d as CalendarDate) : undefined}\n weekStartsOn={weekStartsOn}\n >\n {#snippet children({months, weekdays})}\n <div class={cn('flex justify-center', numberOfMonths > 1 && 'gap-6')}>\n {#each months as month, i (month.value.toString())}\n <div class=\"flex flex-col gap-2\">\n <!-- Month heading -->\n <div class=\"flex items-center justify-between gap-2\">\n {#if i === 0}\n <CalendarPrimitive.PrevButton class={styles.navBtn()}>\n {#if prevIcon}{@render prevIcon()}{:else}\n <IconChevronLeft class=\"size-4\"/>\n {/if}\n </CalendarPrimitive.PrevButton>\n {:else}\n <div class=\"size-7 shrink-0\"></div>\n {/if}\n\n <span class={styles.monthLabel()}>\n {new Date(month.value.year, month.value.month - 1)\n .toLocaleDateString('en-US', {month: 'long', year: 'numeric'})}\n </span>\n\n {#if i === months.length - 1}\n <CalendarPrimitive.NextButton class={styles.navBtn()}>\n {#if nextIcon}{@render nextIcon()}{:else}\n <IconChevronRight class=\"size-4\"/>\n {/if}\n </CalendarPrimitive.NextButton>\n {:else}\n <div class=\"size-7 shrink-0\"></div>\n {/if}\n </div>\n\n <!-- Grid -->\n <CalendarPrimitive.Grid class=\"w-full border-collapse\">\n <CalendarPrimitive.GridHead>\n <CalendarPrimitive.GridRow class=\"flex\">\n {#each weekdays as day (day)}\n <CalendarPrimitive.HeadCell class={styles.weekday()}>\n {day}\n </CalendarPrimitive.HeadCell>\n {/each}\n </CalendarPrimitive.GridRow>\n </CalendarPrimitive.GridHead>\n <CalendarPrimitive.GridBody>\n {#each month.weeks as weekDates, wi (wi)}\n <CalendarPrimitive.GridRow class=\"flex mt-0.5\">\n {#each weekDates as date (date.toString())}\n <CalendarPrimitive.Cell\n {date}\n month={month.value}\n class={styles.cell()}\n >\n <CalendarPrimitive.Day class={dayStyles}/>\n </CalendarPrimitive.Cell>\n {/each}\n </CalendarPrimitive.GridRow>\n {/each}\n </CalendarPrimitive.GridBody>\n </CalendarPrimitive.Grid>\n </div>\n {/each}\n </div>\n {/snippet}\n </CalendarPrimitive.Root>\n{/snippet}\n\n{#snippet timePickers()}\n <div class={styles.timeBar()}>\n <span class={styles.timeLabelStart()}>Start</span>\n <input\n type=\"time\"\n bind:value={startTime}\n class={styles.timeInput()}\n />\n <span class={styles.timeArrow()}>→</span>\n <span class={styles.timeLabelEnd()}>End</span>\n <input\n type=\"time\"\n bind:value={endTime}\n class={styles.timeInput()}\n />\n </div>\n{/snippet}\n\n{#snippet pickerFooter()}\n <div class={styles.footer()}>\n <button\n class={styles.footerReset()}\n onclick={resetRange}\n >\n Reset\n </button>\n <button\n disabled={!canApply}\n class={styles.footerApply()}\n onclick={applyRange}\n >\n Apply\n </button>\n </div>\n{/snippet}\n\n<!-- ─── Root ─────────────────────────────────────────────────────────────── -->\n\n<div class={cn('inline-flex w-full', className)}>\n {#if isDesktop}\n <!-- ── DESKTOP: Popover anchored to the whole trigger container ── -->\n <PopoverPrimitive.Root bind:open>\n <PopoverPrimitive.Trigger class={triggerStyles}>\n {#snippet child({props: triggerProps})}\n <!--\n Intercept clicks: only toggle the popover when clicking\n outside the date-field area (calendar icon, empty space).\n -->\n <div\n {...triggerProps}\n onclick={(e) => {\n if (!(e.target as HTMLElement).closest('.drp-field')) {\n (triggerProps.onclick as EventListener | undefined)?.(e);\n }\n }}\n class={triggerStyles}\n >\n {@render dateField()}\n\n <div class=\"flex items-center gap-1 shrink-0 ml-1\">\n {#if calendarIcon}{@render calendarIcon()}{:else}\n <IconCalendar class=\"size-4 text-text-tertiary pointer-events-none\"/>\n {/if}\n {#if hasRange}\n <button\n type=\"button\"\n class={iconBtnStyles}\n aria-label=\"Clear date range\"\n onclick={(e) => { e.stopPropagation(); resetRange(); }}\n >\n {#if clearIcon}{@render clearIcon()}{:else}\n <IconX class=\"size-3\"/>\n {/if}\n </button>\n {/if}\n </div>\n </div>\n {/snippet}\n </PopoverPrimitive.Trigger>\n\n <PopoverPrimitive.Portal>\n <PopoverPrimitive.Content\n sideOffset={4}\n align=\"start\"\n class={popoverContentStyles}\n style={withZIndex(undefined, panelZIndex)}\n escapeKeydownBehavior={layer.isTopmost ? 'close' : 'ignore'}\n >\n <div class=\"flex\">\n <!-- Quick-select sidebar -->\n <div class={styles.presetSidebar()}>\n <p class={styles.presetHeading()}>\n Quick Select\n </p>\n {#each quickRanges as range (range.key)}\n <button\n class={styles.presetItem({presetActive: activeQuickSelect === range.key})}\n onclick={() => applyQuickSelect(range.key)}\n >\n {range.label}\n </button>\n {/each}\n </div>\n\n <!-- Calendar + time + footer -->\n <div class=\"flex flex-col min-w-0\">\n <div class=\"p-4\">\n {@render calendarGrid(numberOfMonthsProp ?? 2)}\n </div>\n {#if enableTime}{@render timePickers()}{/if}\n {@render pickerFooter()}\n </div>\n </div>\n </PopoverPrimitive.Content>\n </PopoverPrimitive.Portal>\n </PopoverPrimitive.Root>\n\n {:else}\n <!-- ── MOBILE: plain trigger → Dialog (bottom-sheet style) ─────── -->\n <div class={triggerStyles}>\n {@render dateField()}\n\n <div class=\"flex items-center gap-1 shrink-0 ml-1\">\n <button\n class={iconBtnStyles}\n aria-label=\"Open date picker\"\n onclick={() => (open = true)}\n >\n {#if calendarIcon}{@render calendarIcon()}{:else}\n <IconCalendar class=\"size-4\"/>\n {/if}\n </button>\n {#if hasRange}\n <button class={iconBtnStyles} aria-label=\"Clear date range\" onclick={resetRange}>\n {#if clearIcon}{@render clearIcon()}{:else}\n <IconX class=\"size-3\"/>\n {/if}\n </button>\n {/if}\n </div>\n </div>\n\n <Dialog bind:open size=\"sm\" closeButton={false} padding=\"none\">\n <div class=\"flex flex-col\">\n <!-- Dialog header -->\n <div class={styles.dialogHeader()}>\n <span class={styles.dialogTitle()}>Select Date Range</span>\n <button class={iconBtnStyles} aria-label=\"Close\" onclick={() => (open = false)}>\n {#if closeIcon}{@render closeIcon()}{:else}\n <IconX class=\"size-4\"/>\n {/if}\n </button>\n </div>\n\n <!-- Horizontal quick-select pills -->\n <div class={styles.pillBar()}\n style=\"scrollbar-width:none;-ms-overflow-style:none;\">\n {#each quickRanges as range (range.key)}\n <button\n class={styles.presetPill({pillActive: activeQuickSelect === range.key})}\n onclick={() => applyQuickSelect(range.key)}\n >\n {range.label}\n </button>\n {/each}\n </div>\n\n <!-- Single-month calendar -->\n <div class=\"p-4 overflow-auto\">\n {@render calendarGrid(1)}\n </div>\n\n {#if enableTime}{@render timePickers()}{/if}\n {@render pickerFooter()}\n </div>\n </Dialog>\n {/if}\n</div>\n"
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
"path": "src/lib/components/date-range-picker/index.ts",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"sharedModules": {
|
|
40
40
|
"utils": true,
|
|
41
41
|
"icons": true,
|
|
42
|
-
"hooks":
|
|
42
|
+
"hooks": true
|
|
43
43
|
},
|
|
44
44
|
"exports": [
|
|
45
45
|
"DateRangePicker",
|
package/registry/dialog.json
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"path": "src/lib/components/dialog/Dialog.svelte",
|
|
22
22
|
"target": "Dialog.svelte",
|
|
23
23
|
"type": "svelte",
|
|
24
|
-
"content": "<script lang=\"ts\">\n import {Dialog as DialogPrimitive} from 'bits-ui';\n import {type DialogProps, dialogRecipe} from './Dialog.recipe';\n import {IconX} from '$lib/internal/icons';\n import {cn} from '$lib/utils/index.js';\n import {closeOnBackNavigation} from '$lib/hooks';\n\n let {\n children,\n open = $bindable(false),\n ref = $bindable(null),\n closeOnClickOutside = true,\n closeOnNavigateBack = true,\n closeButton = true,\n size = 'sm',\n header,\n footer,\n class: className,\n position = 'center',\n padding,\n staticRender = false,\n preventScroll,\n onOpenChangeComplete,\n triggerRef,\n focusTriggerAfterClose = true,\n containerClass,\n headerClass,\n bodyClass,\n footerClass,\n closeIcon,\n ...props\n }: DialogProps = $props();\n\n const isFull = $derived(size === 'full');\n\n const r = $derived(\n dialogRecipe({\n size,\n position,\n full: isFull,\n padding: padding === 'none' ? 'none' : 'normal',\n })\n );\n\n function handleCloseAutoFocus(e: Event) {\n if (triggerRef && focusTriggerAfterClose) {\n e.preventDefault();\n triggerRef.focus();\n }\n }\n\n closeOnBackNavigation({\n enabled: () => closeOnNavigateBack,\n isOpen: () => open,\n close: () => { open = false; },\n });\n</script>\n\n<DialogPrimitive.Root bind:open onOpenChangeComplete={onOpenChangeComplete}>\n <DialogPrimitive.Portal>\n <!-- Backdrop -->\n <DialogPrimitive.Overlay class={r.overlay()} />\n\n <!--\n We use the `child` snippet (bits-ui \"asChild\" pattern) so that bits-ui gates its\n internal <ScrollLock> on `open.current`. Without this, the `children` branch renders\n <ScrollLock> unconditionally — with forceMount the lock never releases, leaving\n `document.body { pointer-events:none; overflow:hidden }` active even when closed.\n -->\n <DialogPrimitive.Content\n bind:ref\n forceMount={staticRender || undefined}\n interactOutsideBehavior={closeOnClickOutside ? 'close' : 'ignore'}\n preventScroll={preventScroll}\n onCloseAutoFocus={handleCloseAutoFocus}\n {...props}\n >\n {#snippet child({ props: contentProps })}\n <div\n {...contentProps}\n inert={staticRender && !open ? true : undefined}\n class={cn(\n contentProps.class,\n r.content(),\n staticRender && 'data-[state=closed]:hidden',\n className\n )}\n >\n {#if closeButton}\n <DialogPrimitive.Close\n class={r.close()}\n aria-label=\"Close\"\n >\n {#if closeIcon}\n {@render closeIcon()}\n {:else}\n <IconX class=\"size-4\"/>\n {/if}\n </DialogPrimitive.Close>\n {/if}\n\n <div class={r.container({ class: containerClass })}>\n {#if header}\n <header class={r.header({ class: headerClass })}>\n {@render header()}\n </header>\n {/if}\n\n <main class={r.body({ class: bodyClass })}>\n {@render children?.()}\n </main>\n\n {#if footer}\n <footer class={r.footer({ class: footerClass })}>\n {@render footer()}\n </footer>\n {/if}\n </div>\n </div>\n {/snippet}\n </DialogPrimitive.Content>\n </DialogPrimitive.Portal>\n</DialogPrimitive.Root>\n"
|
|
24
|
+
"content": "<script lang=\"ts\">\n import {Dialog as DialogPrimitive} from 'bits-ui';\n import {type DialogProps, dialogRecipe} from './Dialog.recipe';\n import {IconX} from '$lib/internal/icons';\n import {cn} from '$lib/utils/index.js';\n import {closeOnBackNavigation, overlayLayer, withZIndex} from '$lib/hooks';\n\n let {\n children,\n open = $bindable(false),\n ref = $bindable(null),\n closeOnClickOutside = true,\n closeOnNavigateBack = true,\n closeButton = true,\n size = 'sm',\n header,\n footer,\n class: className,\n position = 'center',\n padding,\n staticRender = false,\n preventScroll,\n onOpenChangeComplete,\n triggerRef,\n focusTriggerAfterClose = true,\n containerClass,\n headerClass,\n bodyClass,\n footerClass,\n closeIcon,\n ...props\n }: DialogProps = $props();\n\n // Registers with the shared stacking authority so a dialog opened from INSIDE another\n // layered surface (the canonical case: a BottomSheet at z 1000/1001) lands above it\n // instead of losing the race with its own hardcoded z-50. Undefined until first open, so\n // SSR / never-opened dialogs keep the recipe's class exactly as before.\n const layer = overlayLayer({isOpen: () => open, kind: 'dialog'});\n const contentZIndex = $derived(layer.zIndex === undefined ? undefined : layer.zIndex + 1);\n\n const isFull = $derived(size === 'full');\n\n const r = $derived(\n dialogRecipe({\n size,\n position,\n full: isFull,\n padding: padding === 'none' ? 'none' : 'normal',\n })\n );\n\n function handleCloseAutoFocus(e: Event) {\n if (triggerRef && focusTriggerAfterClose) {\n e.preventDefault();\n triggerRef.focus();\n }\n }\n\n closeOnBackNavigation({\n enabled: () => closeOnNavigateBack,\n isOpen: () => open,\n close: () => { open = false; },\n });\n</script>\n\n<DialogPrimitive.Root bind:open onOpenChangeComplete={onOpenChangeComplete}>\n <DialogPrimitive.Portal>\n <!-- Backdrop -->\n <DialogPrimitive.Overlay class={r.overlay()} style={withZIndex(undefined, layer.zIndex)} />\n\n <!--\n We use the `child` snippet (bits-ui \"asChild\" pattern) so that bits-ui gates its\n internal <ScrollLock> on `open.current`. Without this, the `children` branch renders\n <ScrollLock> unconditionally — with forceMount the lock never releases, leaving\n `document.body { pointer-events:none; overflow:hidden }` active even when closed.\n -->\n <DialogPrimitive.Content\n bind:ref\n forceMount={staticRender || undefined}\n interactOutsideBehavior={closeOnClickOutside ? 'close' : 'ignore'}\n preventScroll={preventScroll}\n escapeKeydownBehavior={layer.isTopmost ? 'close' : 'ignore'}\n onCloseAutoFocus={handleCloseAutoFocus}\n {...props}\n >\n {#snippet child({ props: contentProps })}\n <!--\n `style:z-index` (a directive, not an attribute) so it merges with — rather\n than clobbers — whatever bits-ui and the app put in `style`, and wins over\n the recipe's z-50 class. To override it from an app, use `z-[…]!`.\n -->\n <div\n {...contentProps}\n style:z-index={contentZIndex}\n inert={staticRender && !open ? true : undefined}\n class={cn(\n contentProps.class,\n r.content(),\n staticRender && 'data-[state=closed]:hidden',\n className\n )}\n >\n {#if closeButton}\n <DialogPrimitive.Close\n class={r.close()}\n aria-label=\"Close\"\n >\n {#if closeIcon}\n {@render closeIcon()}\n {:else}\n <IconX class=\"size-4\"/>\n {/if}\n </DialogPrimitive.Close>\n {/if}\n\n <div class={r.container({ class: containerClass })}>\n {#if header}\n <header class={r.header({ class: headerClass })}>\n {@render header()}\n </header>\n {/if}\n\n <main class={r.body({ class: bodyClass })}>\n {@render children?.()}\n </main>\n\n {#if footer}\n <footer class={r.footer({ class: footerClass })}>\n {@render footer()}\n </footer>\n {/if}\n </div>\n </div>\n {/snippet}\n </DialogPrimitive.Content>\n </DialogPrimitive.Portal>\n</DialogPrimitive.Root>\n"
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
"path": "src/lib/components/dialog/index.ts",
|
package/registry/drawer.json
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"path": "src/lib/components/drawer/Drawer.svelte",
|
|
16
16
|
"target": "Drawer.svelte",
|
|
17
17
|
"type": "svelte",
|
|
18
|
-
"content": "<script lang=\"ts\">\n import {type DrawerProps, drawerRecipe} from './Drawer.recipe';\n\n let {\n open = $bindable(false),\n side = 'right',\n size = 'md',\n closeOnClickOutside = true,\n header,\n footer,\n children,\n closeIcon,\n onclose,\n }: DrawerProps = $props();\n\n const r = $derived(drawerRecipe({side, size}));\n\n function close() {\n open = false;\n onclose?.();\n }\n</script>\n\n{#if open}\n <!-- Backdrop -->\n <div\n role=\"presentation\"\n class={r.overlay()}\n
|
|
18
|
+
"content": "<script lang=\"ts\">\n import {type DrawerProps, drawerRecipe} from './Drawer.recipe';\n import {overlayLayer} from '$lib/hooks';\n\n let {\n open = $bindable(false),\n side = 'right',\n size = 'md',\n closeOnClickOutside = true,\n header,\n footer,\n children,\n closeIcon,\n onclose,\n }: DrawerProps = $props();\n\n // Shared stacking authority. The drawer renders IN PLACE (no portal), but it still takes\n // a slot so overlays opened from inside it stack above, and so Escape can resolve a\n // single topmost across every overlay type.\n const layer = overlayLayer({isOpen: () => open, kind: 'drawer'});\n const contentZIndex = $derived(layer.zIndex === undefined ? undefined : layer.zIndex + 1);\n\n const r = $derived(drawerRecipe({side, size}));\n\n function close() {\n open = false;\n onclose?.();\n }\n\n // Escape was documented on `closeOnClickOutside` but never actually worked: the handler\n // lived on a <div role=\"presentation\"> with no tabindex, which can't receive keydown.\n // It has to work now — a registered surface that swallows its slot without honouring\n // Escape would turn Escape off for everything beneath it (bits-ui overlays below defer\n // to whoever is topmost). Window CAPTURE + topmost-gated, mirroring BottomSheet: capture\n // runs before bits-ui's document listener, so `isTopmost` is read as of the keypress\n // rather than after another overlay has already closed and left this one on top.\n function handleKeydown(e: KeyboardEvent) {\n if (e.key === 'Escape' && open && closeOnClickOutside && layer.isTopmost) {\n e.preventDefault();\n close();\n }\n }\n</script>\n\n<svelte:window onkeydowncapture={handleKeydown}/>\n\n{#if open}\n <!-- Backdrop -->\n <div\n role=\"presentation\"\n class={r.overlay()}\n style:z-index={layer.zIndex}\n onclick={() => closeOnClickOutside && close()}\n ></div>\n\n <!-- Drawer panel -->\n <div\n class={r.content()}\n style:z-index={contentZIndex}\n role=\"dialog\"\n aria-modal=\"true\"\n >\n {#if header}\n <header class={r.header()}>\n {@render header()}\n <button\n type=\"button\"\n onclick={close}\n class={r.headerClose()}\n aria-label=\"Close drawer\"\n >\n {#if closeIcon}\n {@render closeIcon()}\n {:else}\n <svg class=\"size-4\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\">\n <path d=\"M4 4l8 8M12 4l-8 8\"/>\n </svg>\n {/if}\n </button>\n </header>\n {/if}\n\n <main class={r.body()}>\n {@render children?.()}\n </main>\n\n {#if footer}\n <footer class={r.footer()}>\n {@render footer()}\n </footer>\n {/if}\n </div>\n{/if}\n"
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
"path": "src/lib/components/drawer/index.ts",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"sharedModules": {
|
|
35
35
|
"utils": false,
|
|
36
36
|
"icons": false,
|
|
37
|
-
"hooks":
|
|
37
|
+
"hooks": true
|
|
38
38
|
},
|
|
39
39
|
"exports": [
|
|
40
40
|
"Drawer",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"path": "src/lib/components/dropdown-menu/components/DropdownMenu.svelte",
|
|
10
10
|
"target": "components/DropdownMenu.svelte",
|
|
11
11
|
"type": "svelte",
|
|
12
|
-
"content": "<script lang=\"ts\">\n import {DropdownMenu as DropdownMenuPrimitive} from 'bits-ui';\n import {dropdownMenuRecipe, type DropdownMenuProps} from '../DropdownMenu.recipe';\n\n let {\n children,\n trigger,\n class: className,\n open = $bindable(false),\n side = 'bottom',\n sideOffset = 4,\n align = 'start',\n loop,\n preventScroll,\n ...props\n }: DropdownMenuProps = $props();\n\n const r = dropdownMenuRecipe();\n</script>\n\n<DropdownMenuPrimitive.Root bind:open>\n {#if trigger}\n <DropdownMenuPrimitive.Trigger>\n {#snippet child({ props: triggerProps })}\n <span {...triggerProps} class=\"inline-flex\">\n {@render trigger()}\n </span>\n {/snippet}\n </DropdownMenuPrimitive.Trigger>\n {/if}\n\n <DropdownMenuPrimitive.Portal>\n <DropdownMenuPrimitive.Content\n {side}\n {sideOffset}\n {align}\n {loop}\n {preventScroll}\n {...props}\n class={r.content({ class: className })}\n >\n {@render children?.()}\n </DropdownMenuPrimitive.Content>\n </DropdownMenuPrimitive.Portal>\n</DropdownMenuPrimitive.Root>\n"
|
|
12
|
+
"content": "<script lang=\"ts\">\n import {DropdownMenu as DropdownMenuPrimitive} from 'bits-ui';\n import {dropdownMenuRecipe, type DropdownMenuProps} from '../DropdownMenu.recipe';\n import {overlayLayer, withZIndex} from '$lib/hooks';\n\n let {\n children,\n trigger,\n class: className,\n open = $bindable(false),\n side = 'bottom',\n sideOffset = 4,\n align = 'start',\n loop,\n preventScroll,\n ...props\n }: DropdownMenuProps = $props();\n\n // Shared stacking authority — a menu opened from inside a Dialog or BottomSheet must\n // land above it, which its hardcoded z-50 could not do.\n const layer = overlayLayer({isOpen: () => open, kind: 'menu'});\n const contentZIndex = $derived(layer.zIndex === undefined ? undefined : layer.zIndex + 1);\n\n const r = dropdownMenuRecipe();\n</script>\n\n<DropdownMenuPrimitive.Root bind:open>\n {#if trigger}\n <DropdownMenuPrimitive.Trigger>\n {#snippet child({ props: triggerProps })}\n <span {...triggerProps} class=\"inline-flex\">\n {@render trigger()}\n </span>\n {/snippet}\n </DropdownMenuPrimitive.Trigger>\n {/if}\n\n <DropdownMenuPrimitive.Portal>\n <DropdownMenuPrimitive.Content\n {side}\n {sideOffset}\n {align}\n {loop}\n {preventScroll}\n escapeKeydownBehavior={layer.isTopmost ? 'close' : 'ignore'}\n {...props}\n class={r.content({ class: className })}\n style={withZIndex(props.style, contentZIndex)}\n >\n {@render children?.()}\n </DropdownMenuPrimitive.Content>\n </DropdownMenuPrimitive.Portal>\n</DropdownMenuPrimitive.Root>\n"
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
15
|
"path": "src/lib/components/dropdown-menu/components/DropdownMenuGroup.svelte",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"sharedModules": {
|
|
60
60
|
"utils": false,
|
|
61
61
|
"icons": false,
|
|
62
|
-
"hooks":
|
|
62
|
+
"hooks": true
|
|
63
63
|
},
|
|
64
64
|
"exports": [
|
|
65
65
|
"DropdownMenu",
|
package/registry/popover.json
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"path": "src/lib/components/popover/Popover.svelte",
|
|
22
22
|
"target": "Popover.svelte",
|
|
23
23
|
"type": "svelte",
|
|
24
|
-
"content": "<script lang=\"ts\">\n import {Popover as PopoverPrimitive} from 'bits-ui';\n import {type PopoverProps, popoverRecipe} from './Popover.recipe';\n\n let {\n children,\n trigger,\n class: className,\n open = $bindable(false),\n side = 'bottom',\n sideOffset = 4,\n align = 'start',\n alignOffset,\n closeOnClickOutside = true,\n closeOnEscape = true,\n trapFocus,\n preventScroll,\n onInteractOutside,\n ref = $bindable(null),\n ...props\n }: PopoverProps = $props();\n\n const r = $derived(popoverRecipe());\n</script>\n\n<PopoverPrimitive.Root bind:open>\n {#if trigger}\n <PopoverPrimitive.Trigger>\n {#snippet child({ props: triggerProps })}\n <span {...triggerProps} class={r.trigger()}>\n {@render trigger()}\n </span>\n {/snippet}\n </PopoverPrimitive.Trigger>\n {/if}\n\n <PopoverPrimitive.Portal>\n <PopoverPrimitive.Content\n bind:ref\n {side}\n {sideOffset}\n {align}\n {alignOffset}\n {trapFocus}\n {preventScroll}\n interactOutsideBehavior={closeOnClickOutside ? 'close' : 'ignore'}\n escapeKeydownBehavior={closeOnEscape ? 'close' : 'ignore'}\n onInteractOutside={() => onInteractOutside?.()}\n class={r.content({ class: className })}\n {...props}\n >\n {@render children?.()}\n </PopoverPrimitive.Content>\n </PopoverPrimitive.Portal>\n</PopoverPrimitive.Root>\n"
|
|
24
|
+
"content": "<script lang=\"ts\">\n import {Popover as PopoverPrimitive} from 'bits-ui';\n import {type PopoverProps, popoverRecipe} from './Popover.recipe';\n import {overlayLayer, withZIndex} from '$lib/hooks';\n\n let {\n children,\n trigger,\n class: className,\n open = $bindable(false),\n side = 'bottom',\n sideOffset = 4,\n align = 'start',\n alignOffset,\n closeOnClickOutside = true,\n closeOnEscape = true,\n trapFocus,\n preventScroll,\n onInteractOutside,\n ref = $bindable(null),\n ...props\n }: PopoverProps = $props();\n\n // Shared stacking authority — a popover opened from inside a BottomSheet must land above\n // it rather than behind, which its hardcoded z-50 could never do. Content sits at z+1,\n // the slot's content rung (backdrop-owning overlays put their backdrop on z).\n const layer = overlayLayer({isOpen: () => open, kind: 'popover'});\n const contentZIndex = $derived(layer.zIndex === undefined ? undefined : layer.zIndex + 1);\n\n const r = $derived(popoverRecipe());\n</script>\n\n<PopoverPrimitive.Root bind:open>\n {#if trigger}\n <PopoverPrimitive.Trigger>\n {#snippet child({ props: triggerProps })}\n <span {...triggerProps} class={r.trigger()}>\n {@render trigger()}\n </span>\n {/snippet}\n </PopoverPrimitive.Trigger>\n {/if}\n\n <PopoverPrimitive.Portal>\n <PopoverPrimitive.Content\n bind:ref\n {side}\n {sideOffset}\n {align}\n {alignOffset}\n {trapFocus}\n {preventScroll}\n interactOutsideBehavior={closeOnClickOutside ? 'close' : 'ignore'}\n escapeKeydownBehavior={closeOnEscape && layer.isTopmost ? 'close' : 'ignore'}\n onInteractOutside={() => onInteractOutside?.()}\n class={r.content({ class: className })}\n {...props}\n style={withZIndex(props.style, contentZIndex)}\n >\n {@render children?.()}\n </PopoverPrimitive.Content>\n </PopoverPrimitive.Portal>\n</PopoverPrimitive.Root>\n"
|
|
25
25
|
}
|
|
26
26
|
],
|
|
27
27
|
"dependencies": [
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"sharedModules": {
|
|
36
36
|
"utils": false,
|
|
37
37
|
"icons": false,
|
|
38
|
-
"hooks":
|
|
38
|
+
"hooks": true
|
|
39
39
|
},
|
|
40
40
|
"exports": [
|
|
41
41
|
"Popover",
|
package/registry/select.json
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"path": "src/lib/components/select/Select.svelte",
|
|
28
28
|
"target": "Select.svelte",
|
|
29
29
|
"type": "svelte",
|
|
30
|
-
"content": "<script lang=\"ts\">\n import {Select as SelectPrimitive} from 'bits-ui';\n import {type SelectProps, selectRecipe} from './Select.recipe';\n import {IconCheck, IconChevronDown} from '$lib/internal/icons';\n import IconChevronsUp from '@tabler/icons-svelte/icons/chevrons-up';\n import IconChevronsDown from '@tabler/icons-svelte/icons/chevrons-down';\n\n let {\n type = 'single',\n options = [],\n groups,\n value = $bindable(),\n placeholder = 'Select…',\n variant,\n size,\n disabled,\n status,\n loop,\n allowDeselect,\n required,\n name,\n class: className,\n checkIcon,\n onchange,\n }: SelectProps = $props();\n\n // All options flat — used for label lookup in multiple mode\n const allOptions = $derived(\n groups ? groups.flatMap(g => g.options) : options\n );\n\n // Single mode: selected label\n const selectedLabel = $derived.by(() => {\n const v = value;\n if (typeof v !== 'string' || !v) return '';\n return allOptions.find(o => o.value === v)?.label ?? v;\n });\n\n // Multiple mode: selected chips\n const selectedChips = $derived(\n Array.isArray(value)\n ? (value as string[]).map(v => ({ value: v, label: allOptions.find(o => o.value === v)?.label ?? v }))\n : []\n );\n\n const r = $derived(selectRecipe({ variant, size, disabled, status }));\n\n const minHeight = \"21rem\"\n</script>\n\n{#snippet itemList()}\n <SelectPrimitive.Viewport class={r.viewport()}>\n {#if groups && groups.length > 0}\n {#each groups as group (group.label)}\n <SelectPrimitive.Group>\n <SelectPrimitive.GroupHeading\n class={r.group()}\n >\n {group.label}\n </SelectPrimitive.GroupHeading>\n {#each group.options as option (option.value)}\n {@render renderItem(option)}\n {/each}\n </SelectPrimitive.Group>\n {/each}\n {:else}\n {#each options as option (option.value)}\n {@render renderItem(option)}\n {/each}\n {/if}\n </SelectPrimitive.Viewport>\n{/snippet}\n\n{#if type === 'multiple'}\n <SelectPrimitive.Root\n type=\"multiple\"\n bind:value={value as string[] | undefined}\n onValueChange={(v) => onchange?.(v)}\n {disabled}\n {loop}\n {required}\n {name}\n >\n <!--\n Multiple-mode trigger: chip tags displayed in the button, individual × spans\n are non-interactive (no onclick) to avoid button-in-button nesting.\n Managing selection is done exclusively through the dropdown items.\n -->\n <SelectPrimitive.Trigger class={r.trigger({ class: `h-auto min-h-[var(--tera-control-height-md)] flex-wrap py-1.5 ${className ?? ''}` })}>\n {#if selectedChips.length > 0}\n {#each selectedChips as chip (chip.value)}\n <!-- Plain span tag — no interactive elements inside <button> -->\n <span class=\"inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs bg-interactive-subtle text-interactive-text border border-interactive-border\">\n {chip.label}\n <!-- Use span with role for the remove hint; actual removal happens via re-clicking in dropdown -->\n<!-- <span class=\"flex items-center opacity-60\" aria-hidden=\"true\"><IconX class=\"size-3\" /></span>-->\n </span>\n {/each}\n {:else}\n <span class=\"text-text-tertiary flex-1\">{placeholder}</span>\n {/if}\n <IconChevronDown class=\"size-4 shrink-0 text-text-tertiary ml-auto transition-transform ease-ui duration-[var(--tera-duration-fast)] data-[state=open]:rotate-180\" />\n </SelectPrimitive.Trigger>\n\n <SelectPrimitive.Portal>\n <SelectPrimitive.Content\n sideOffset={4}\n class={r.content()}\n style
|
|
30
|
+
"content": "<script lang=\"ts\">\n import {Select as SelectPrimitive} from 'bits-ui';\n import {type SelectProps, selectRecipe} from './Select.recipe';\n import {IconCheck, IconChevronDown} from '$lib/internal/icons';\n import IconChevronsUp from '@tabler/icons-svelte/icons/chevrons-up';\n import IconChevronsDown from '@tabler/icons-svelte/icons/chevrons-down';\n import {overlayLayer, withZIndex} from '$lib/hooks';\n\n let {\n type = 'single',\n options = [],\n groups,\n value = $bindable(),\n placeholder = 'Select…',\n variant,\n size,\n disabled,\n status,\n loop,\n allowDeselect,\n required,\n name,\n class: className,\n checkIcon,\n onchange,\n }: SelectProps = $props();\n\n // All options flat — used for label lookup in multiple mode\n const allOptions = $derived(\n groups ? groups.flatMap(g => g.options) : options\n );\n\n // Single mode: selected label\n const selectedLabel = $derived.by(() => {\n const v = value;\n if (typeof v !== 'string' || !v) return '';\n return allOptions.find(o => o.value === v)?.label ?? v;\n });\n\n // Multiple mode: selected chips\n const selectedChips = $derived(\n Array.isArray(value)\n ? (value as string[]).map(v => ({ value: v, label: allOptions.find(o => o.value === v)?.label ?? v }))\n : []\n );\n\n const r = $derived(selectRecipe({ variant, size, disabled, status }));\n\n // Internal only — Select exposes no `open` prop, but the shared stacking authority needs\n // to know when the listbox is up so a Select inside a Dialog/BottomSheet lands above it\n // instead of behind. `bind:open` on SelectPrimitive.Root is purely observational here.\n let open = $state(false);\n\n const layer = overlayLayer({isOpen: () => open, kind: 'menu'});\n const contentZIndex = $derived(layer.zIndex === undefined ? undefined : layer.zIndex + 1);\n\n const minHeight = \"21rem\"\n\n // The content already carries a `style` (max-height); z-index must be CONCATENATED onto\n // it, not added as a second `style` attribute.\n const contentStyle = $derived(withZIndex(\n `max-height: min(${minHeight}, var(--bits-select-content-available-height))`,\n contentZIndex\n ));\n</script>\n\n{#snippet itemList()}\n <SelectPrimitive.Viewport class={r.viewport()}>\n {#if groups && groups.length > 0}\n {#each groups as group (group.label)}\n <SelectPrimitive.Group>\n <SelectPrimitive.GroupHeading\n class={r.group()}\n >\n {group.label}\n </SelectPrimitive.GroupHeading>\n {#each group.options as option (option.value)}\n {@render renderItem(option)}\n {/each}\n </SelectPrimitive.Group>\n {/each}\n {:else}\n {#each options as option (option.value)}\n {@render renderItem(option)}\n {/each}\n {/if}\n </SelectPrimitive.Viewport>\n{/snippet}\n\n{#if type === 'multiple'}\n <SelectPrimitive.Root\n type=\"multiple\"\n bind:value={value as string[] | undefined}\n bind:open\n onValueChange={(v) => onchange?.(v)}\n {disabled}\n {loop}\n {required}\n {name}\n >\n <!--\n Multiple-mode trigger: chip tags displayed in the button, individual × spans\n are non-interactive (no onclick) to avoid button-in-button nesting.\n Managing selection is done exclusively through the dropdown items.\n -->\n <SelectPrimitive.Trigger class={r.trigger({ class: `h-auto min-h-[var(--tera-control-height-md)] flex-wrap py-1.5 ${className ?? ''}` })}>\n {#if selectedChips.length > 0}\n {#each selectedChips as chip (chip.value)}\n <!-- Plain span tag — no interactive elements inside <button> -->\n <span class=\"inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs bg-interactive-subtle text-interactive-text border border-interactive-border\">\n {chip.label}\n <!-- Use span with role for the remove hint; actual removal happens via re-clicking in dropdown -->\n<!-- <span class=\"flex items-center opacity-60\" aria-hidden=\"true\"><IconX class=\"size-3\" /></span>-->\n </span>\n {/each}\n {:else}\n <span class=\"text-text-tertiary flex-1\">{placeholder}</span>\n {/if}\n <IconChevronDown class=\"size-4 shrink-0 text-text-tertiary ml-auto transition-transform ease-ui duration-[var(--tera-duration-fast)] data-[state=open]:rotate-180\" />\n </SelectPrimitive.Trigger>\n\n <SelectPrimitive.Portal>\n <SelectPrimitive.Content\n sideOffset={4}\n class={r.content()}\n style={contentStyle}\n >\n <SelectPrimitive.ScrollUpButton class=\"flex w-full items-center justify-center pt-1 text-text-tertiary cursor-default hover:text-text-primary transition-colors\">\n <IconChevronsUp class=\"size-3.5\" />\n </SelectPrimitive.ScrollUpButton>\n {@render itemList()}\n <SelectPrimitive.ScrollDownButton class=\"flex w-full items-center justify-center pb-1 text-text-tertiary cursor-default hover:text-text-primary transition-colors\">\n <IconChevronsDown class=\"size-3.5\" />\n </SelectPrimitive.ScrollDownButton>\n </SelectPrimitive.Content>\n </SelectPrimitive.Portal>\n </SelectPrimitive.Root>\n\n{:else}\n <SelectPrimitive.Root\n type=\"single\"\n bind:value={value as string | undefined}\n bind:open\n onValueChange={(v) => onchange?.(v)}\n {disabled}\n {loop}\n {allowDeselect}\n {required}\n {name}\n >\n <SelectPrimitive.Trigger class={r.trigger({ class: className })}>\n <span class={!value ? 'text-text-tertiary' : 'text-text-primary'}>\n {value ? selectedLabel : placeholder}\n </span>\n <IconChevronDown\n class=\"size-4 shrink-0 text-text-tertiary transition-transform ease-ui duration-[var(--tera-duration-fast)] data-[state=open]:rotate-180\"\n />\n </SelectPrimitive.Trigger>\n\n <SelectPrimitive.Portal>\n <SelectPrimitive.Content\n sideOffset={6}\n class={r.content()}\n style={contentStyle}\n >\n <SelectPrimitive.ScrollUpButton class=\"flex w-full items-center justify-center pt-1 text-text-tertiary cursor-default hover:text-text-primary transition-colors\">\n <IconChevronsUp class=\"size-3.5\" />\n </SelectPrimitive.ScrollUpButton>\n {@render itemList()}\n <SelectPrimitive.ScrollDownButton class=\"flex w-full items-center justify-center pb-1 text-text-tertiary cursor-default hover:text-text-primary transition-colors\">\n <IconChevronsDown class=\"size-3.5\" />\n </SelectPrimitive.ScrollDownButton>\n </SelectPrimitive.Content>\n </SelectPrimitive.Portal>\n </SelectPrimitive.Root>\n{/if}\n\n{#snippet renderItem(option: { value: string; label: string; disabled?: boolean })}\n <SelectPrimitive.Item\n value={option.value}\n label={option.label}\n disabled={option.disabled}\n class={r.item()}\n >\n {#snippet children({selected})}\n <span class=\"flex-1\">{option.label}</span>\n {#if selected}\n {#if checkIcon}\n {@render checkIcon()}\n {:else}\n <IconCheck class=\"size-4 text-interactive shrink-0\"/>\n {/if}\n {/if}\n {/snippet}\n </SelectPrimitive.Item>\n{/snippet}\n"
|
|
31
31
|
}
|
|
32
32
|
],
|
|
33
33
|
"dependencies": [
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"sharedModules": {
|
|
43
43
|
"utils": false,
|
|
44
44
|
"icons": true,
|
|
45
|
-
"hooks":
|
|
45
|
+
"hooks": true
|
|
46
46
|
},
|
|
47
47
|
"exports": [
|
|
48
48
|
"Select",
|
package/registry/toast.json
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"path": "src/lib/components/toast/ToastContainer.svelte",
|
|
22
22
|
"target": "ToastContainer.svelte",
|
|
23
23
|
"type": "svelte",
|
|
24
|
-
"content": "<script lang=\"ts\">\n import {toast, type ToastVariant} from './toast.svelte.js';\n\n // Status carried by a leading icon-chip (matches the redesigned Alert), not a\n // side-stripe bar. Semantic -default/-text tokens so tones stay dark-safe.\n const chipColor: Record<ToastVariant, string> = {\n success: 'bg-success-default/15 text-success-text',\n error: 'bg-error-default/15 text-error-text',\n info: 'bg-information-default/15 text-information-text',\n warning: 'bg-warning-default/15 text-warning-text',\n };\n\n const iconSymbol: Record<ToastVariant, string> = {\n success: '✓', error: '✕', info: 'ℹ', warning: '⚠',\n };\n</script>\n\n<div\n aria-live=\"polite\"\n aria-label=\"Notifications\"\n class=\"fixed bottom-4 right-4 z-[
|
|
24
|
+
"content": "<script lang=\"ts\">\n import {toast, type ToastVariant} from './toast.svelte.js';\n\n // Status carried by a leading icon-chip (matches the redesigned Alert), not a\n // side-stripe bar. Semantic -default/-text tokens so tones stay dark-safe.\n const chipColor: Record<ToastVariant, string> = {\n success: 'bg-success-default/15 text-success-text',\n error: 'bg-error-default/15 text-error-text',\n info: 'bg-information-default/15 text-information-text',\n warning: 'bg-warning-default/15 text-warning-text',\n };\n\n const iconSymbol: Record<ToastVariant, string> = {\n success: '✓', error: '✕', info: 'ℹ', warning: '⚠',\n };\n</script>\n\n<!--\n z-[8000] = the PINNED toast layer (`layer.toast` in tera-system-ui/tokens — keep in sync;\n Tailwind can only emit a class it reads as literal text). Toasts don't register with the\n overlay stack: a notification must sit above modals regardless of open order, so ordering\n it by open time would be wrong. It was z-[60], which put it BEHIND an open BottomSheet\n (z 1000/1001) — a toast fired over a sheet was simply invisible. It now floats above.\n-->\n<div\n aria-live=\"polite\"\n aria-label=\"Notifications\"\n class=\"fixed bottom-4 right-4 z-[8000] flex flex-col gap-2 max-sm:right-1/2 max-sm:translate-x-1/2\"\n>\n {#each toast.items as item (item.id)}\n <div\n class=\"flex items-start gap-3 min-w-[280px] max-w-sm p-3 bg-surface-raised rounded-[var(--tera-toast-radius,var(--border-radius-container))] shadow-lg border border-border-default animate-in slide-in-from-bottom-4 fade-in duration-[var(--tera-duration-slow)] ease-spring\"\n >\n <span class=\"shrink-0 grid place-items-center size-5 rounded-full text-[11px] font-bold leading-none mt-px {chipColor[item.variant]}\" aria-hidden=\"true\">{iconSymbol[item.variant]}</span>\n <div class=\"flex-1 min-w-0\">\n <p class=\"text-sm font-semibold text-text-primary\">{item.message}</p>\n {#if item.description}\n <p class=\"text-sm text-text-secondary mt-0.5\">{item.description}</p>\n {/if}\n {#if item.action}\n <button\n class=\"mt-1.5 text-xs font-semibold text-interactive hover:text-interactive-hover hover:underline transition-colors duration-[var(--tera-duration-fast)] ease-ui\"\n onclick={item.action.onclick}\n >{item.action.label}</button>\n {/if}\n </div>\n <button\n type=\"button\"\n class=\"shrink-0 -m-1.5 p-2.5 rounded text-text-tertiary hover:text-text-primary hover:bg-surface-hover focus-visible:shadow-focus transition-colors duration-[var(--tera-duration-fast)] ease-ui\"\n onclick={() => toast.remove(item.id)}\n aria-label=\"Close\"\n >\n <svg class=\"size-3.5\" viewBox=\"0 0 14 14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\">\n <path d=\"M2 2l10 10M12 2l-10 10\"/>\n </svg>\n </button>\n </div>\n {/each}\n</div>\n"
|
|
25
25
|
}
|
|
26
26
|
],
|
|
27
27
|
"dependencies": [],
|
package/registry/tooltip.json
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"path": "src/lib/components/tooltip/Tooltip.recipe.ts",
|
|
16
16
|
"target": "Tooltip.recipe.ts",
|
|
17
17
|
"type": "recipe",
|
|
18
|
-
"content": "import {tv, type VariantProps} from \"tailwind-variants\";\nimport type {Snippet} from \"svelte\";\n\n/**\n * Tooltip recipe — the canonical styled layer for the base design language.\n *\n * Multi-part component (built on bits-ui Tooltip): the recipe is slotted by role\n * — trigger (the inline wrapper around the trigger element), content (the\n * floating panel) and arrow (the pointer). An app restyles any part\n * independently by extending with `tv({ extend: tooltipRecipe, ... })` in typed\n * code; base layout, animation and color states are inherited.\n *\n * Restyle-internals (the lighter rung): the per-component CSS var below\n * (--tera-tooltip-radius) lets a theme/app retune corner radius with no code\n * change; it defaults to the global axis token via an inline fallback. See\n * docs/ARCHITECTURE.md for worked examples (kept out of this JSDoc so the llms\n * doc generator doesn't grep its tokens).\n */\nexport const tooltipRecipe = tv({\n slots: {\n trigger: 'inline-flex',\n content: `z-
|
|
18
|
+
"content": "import {tv, type VariantProps} from \"tailwind-variants\";\nimport type {Snippet} from \"svelte\";\n\n/**\n * Tooltip recipe — the canonical styled layer for the base design language.\n *\n * Multi-part component (built on bits-ui Tooltip): the recipe is slotted by role\n * — trigger (the inline wrapper around the trigger element), content (the\n * floating panel) and arrow (the pointer). An app restyles any part\n * independently by extending with `tv({ extend: tooltipRecipe, ... })` in typed\n * code; base layout, animation and color states are inherited.\n *\n * Restyle-internals (the lighter rung): the per-component CSS var below\n * (--tera-tooltip-radius) lets a theme/app retune corner radius with no code\n * change; it defaults to the global axis token via an inline fallback. See\n * docs/ARCHITECTURE.md for worked examples (kept out of this JSDoc so the llms\n * doc generator doesn't grep its tokens).\n */\nexport const tooltipRecipe = tv({\n slots: {\n trigger: 'inline-flex',\n // z-[9000] = the PINNED tooltip layer (`layer.tooltip` in tera-system-ui/tokens —\n // keep in sync; Tailwind can only emit a class it reads as literal text). Tooltips\n // don't register with the overlay stack: they're transient, pointer-events:none\n // chrome that belongs above any modal regardless of open order, so ordering them by\n // open time would be meaningless. 9000 stays clear of third-party peaks (~10000).\n content: `z-[9000] max-w-[220px] px-2.5 py-1.5 text-xs leading-snug font-mono font-medium tracking-tight rounded-[var(--tera-tooltip-radius,var(--tera-radius-sm))]\n bg-neutral-token-13 text-neutral-token-1 shadow-lg\n animate-in fade-in zoom-in-95 duration-[var(--tera-duration-fast)] ease-ui\n data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=closed]:zoom-out-95`,\n arrow: 'text-neutral-token-13',\n },\n});\n\nexport type TooltipRecipeProps = VariantProps<typeof tooltipRecipe>;\n\n/**\n * @component Tooltip - Floating label shown on hover or keyboard focus.\n * Built on BitsUI Tooltip primitive with portal rendering.\n */\nexport interface TooltipProps {\n /** Tooltip content — a plain string or a Svelte snippet for rich markup. */\n content: string | Snippet;\n /** Which side of the trigger the tooltip appears on. */\n side?: 'top' | 'bottom' | 'left' | 'right';\n /** Alignment of the tooltip along the chosen side. */\n align?: 'start' | 'center' | 'end';\n /** Distance in pixels between the trigger element and the tooltip panel. */\n sideOffset?: number;\n /** Delay in milliseconds before the tooltip appears after hover/focus. */\n delay?: number;\n /** Prevents the tooltip from ever opening. */\n disabled?: boolean;\n /** The element that triggers the tooltip on hover/focus. */\n children?: Snippet;\n /** Additional CSS classes for the trigger wrapper span. */\n class?: string;\n /** Replace the default pointer arrow with custom markup (structural seam). */\n arrow?: Snippet;\n}\n"
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
"path": "src/lib/components/tooltip/Tooltip.svelte",
|