@octabits-io/nuxt-ui-kit 0.2.0

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,76 @@
1
+ <script setup lang="ts">
2
+ // Shipped as source: the consumer's Vite compiles this SFC. All imports are
3
+ // explicit — no reliance on the consumer's auto-import configuration.
4
+ // i18n key contract: period.travel.tooltip / period.travel.nights (plural) /
5
+ // period.booking.tooltip / period.booking.days (plural).
6
+ import { computed } from 'vue'
7
+ import { useI18n } from 'vue-i18n'
8
+ import { addDays, differenceInDays } from 'date-fns'
9
+ import UTooltip from '@nuxt/ui/components/Tooltip.vue'
10
+ import UIcon from '@nuxt/ui/components/Icon.vue'
11
+ import { calculateDays, createDateFormatter, type Period } from '@octabits-io/nuxt-ui-kit/dates'
12
+
13
+ const props = withDefaults(
14
+ defineProps<{
15
+ /** ISO date range. `end` is the last booked day (not the checkout date). */
16
+ period: Period
17
+ /**
18
+ * `travel` → check-in → check-out, counted in nights (guest stay).
19
+ * `booking` → check-in → last booked day, counted in days (occupancy).
20
+ */
21
+ kind: 'travel' | 'booking'
22
+ /** Render the `· N nights/days` suffix. */
23
+ showCount?: boolean
24
+ size?: 'xs' | 'sm' | 'md'
25
+ }>(),
26
+ { showCount: true, size: 'sm' },
27
+ )
28
+
29
+ const { t, locale } = useI18n()
30
+ const { formatDate, formatCheckoutDate } = createDateFormatter({ getLocale: () => locale.value })
31
+
32
+ const isTravel = computed(() => props.kind === 'travel')
33
+
34
+ const icon = computed(() => (isTravel.value ? 'i-lucide-luggage' : 'i-lucide-calendar-check'))
35
+ const tooltip = computed(() => t(isTravel.value ? 'period.travel.tooltip' : 'period.booking.tooltip'))
36
+
37
+ const startText = computed(() => formatDate(props.period.start))
38
+ const endText = computed(() =>
39
+ isTravel.value
40
+ ? formatCheckoutDate(props.period.end)
41
+ : formatDate(props.period.end),
42
+ )
43
+
44
+ const count = computed(() =>
45
+ isTravel.value
46
+ ? differenceInDays(
47
+ addDays(new Date(props.period.end), 1),
48
+ new Date(props.period.start),
49
+ )
50
+ : calculateDays(props.period),
51
+ )
52
+ const countText = computed(() =>
53
+ isTravel.value
54
+ ? t('period.travel.nights', count.value)
55
+ : t('period.booking.days', count.value),
56
+ )
57
+
58
+ const iconSize = computed(() =>
59
+ props.size === 'xs' ? 'size-3.5' : props.size === 'md' ? 'size-5' : 'size-4',
60
+ )
61
+ const textSize = computed(() =>
62
+ props.size === 'xs' ? 'text-xs' : props.size === 'md' ? 'text-base' : 'text-sm',
63
+ )
64
+ </script>
65
+
66
+ <template>
67
+ <div class="flex flex-wrap items-center gap-x-1.5 gap-y-0.5" :class="textSize">
68
+ <span class="inline-flex items-center gap-1.5 whitespace-nowrap">
69
+ <UTooltip :text="tooltip">
70
+ <UIcon :name="icon" class="shrink-0 text-muted" :class="iconSize" :aria-label="tooltip" />
71
+ </UTooltip>
72
+ {{ startText }} – {{ endText }}
73
+ </span>
74
+ <span v-if="showCount" class="whitespace-nowrap text-muted">· {{ countText }}</span>
75
+ </div>
76
+ </template>
@@ -0,0 +1,85 @@
1
+ <script setup lang="ts">
2
+ // Shipped as source: the consumer's Vite compiles this SFC. All imports are
3
+ // explicit — no reliance on the consumer's auto-import configuration.
4
+ import { ref, computed, watch } from 'vue'
5
+ import { useRoute } from 'vue-router'
6
+ import USlideover from '@nuxt/ui/components/Slideover.vue'
7
+ import UButton from '@nuxt/ui/components/Button.vue'
8
+ import UIcon from '@nuxt/ui/components/Icon.vue'
9
+
10
+ const props = withDefaults(defineProps<{
11
+ title: string
12
+ toggleLabel?: string
13
+ width?: string
14
+ loading?: boolean
15
+ headerless?: boolean
16
+ /**
17
+ * Query param that carries the current selection (sidebar-view convention,
18
+ * e.g. `?s=owner:42`) — the mobile slideover auto-closes when it changes.
19
+ */
20
+ selectionQueryKey?: string
21
+ }>(), {
22
+ width: 'w-[240px]',
23
+ selectionQueryKey: 's',
24
+ })
25
+
26
+ const open = ref(false)
27
+ const route = useRoute()
28
+
29
+ const buttonLabel = computed(() => props.toggleLabel ?? props.title)
30
+
31
+ watch(
32
+ () => [route.path, route.query[props.selectionQueryKey]],
33
+ () => { open.value = false },
34
+ )
35
+ </script>
36
+
37
+ <template>
38
+ <div class="flex h-full w-full flex-col overflow-hidden">
39
+ <!-- Page header zone: rendered once (unlike #sidebar), full width above both columns -->
40
+ <div v-if="$slots.header" class="shrink-0">
41
+ <slot name="header" />
42
+ </div>
43
+
44
+ <div class="flex min-h-0 w-full flex-1 overflow-hidden">
45
+ <USlideover v-model:open="open" side="left" :title="title">
46
+ <template #body>
47
+ <div class="flex h-full flex-col">
48
+ <div v-if="loading" class="flex flex-1 items-center justify-center">
49
+ <UIcon name="i-lucide-loader-2" class="size-6 animate-spin text-primary" />
50
+ </div>
51
+ <slot v-else name="sidebar" />
52
+ </div>
53
+ </template>
54
+ </USlideover>
55
+
56
+ <aside
57
+ :class="['hidden shrink-0 flex-col overflow-hidden border-r border-default lg:flex', width]"
58
+ >
59
+ <div v-if="!headerless" class="border-b border-default px-4 py-3">
60
+ <h2 class="font-display text-lg font-semibold tracking-tight">{{ title }}</h2>
61
+ </div>
62
+ <div v-if="loading" class="flex flex-1 items-center justify-center">
63
+ <UIcon name="i-lucide-loader-2" class="size-6 animate-spin text-primary" />
64
+ </div>
65
+ <slot v-else name="sidebar" />
66
+ </aside>
67
+
68
+ <div class="flex min-w-0 flex-1 flex-col overflow-hidden">
69
+ <div class="flex shrink-0 items-center gap-2 border-b border-default px-3 py-2 lg:hidden">
70
+ <UButton
71
+ :label="buttonLabel"
72
+ icon="i-lucide-panel-left-open"
73
+ color="neutral"
74
+ variant="outline"
75
+ size="sm"
76
+ @click="open = true"
77
+ />
78
+ </div>
79
+ <div class="min-w-0 flex-1 overflow-y-auto">
80
+ <slot />
81
+ </div>
82
+ </div>
83
+ </div>
84
+ </div>
85
+ </template>