Initial commit of the L'Ami Fiduciaire SaaS platform built on Laravel 12, Vue 3, Inertia.js 2, and Tailwind CSS 4. Story 0.1 (rename folders to declarations in database) is implemented and code-reviewed: migration, rollback, and 6 Pest tests all passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
83 lines
2.3 KiB
Vue
83 lines
2.3 KiB
Vue
<script setup lang="ts">
|
|
import type { HTMLAttributes, Ref } from "vue"
|
|
import { defaultDocument, useEventListener, useMediaQuery, useVModel } from "@vueuse/core"
|
|
import { TooltipProvider } from "reka-ui"
|
|
import { computed, ref } from "vue"
|
|
import { cn } from "@/lib/utils"
|
|
import { provideSidebarContext, SIDEBAR_COOKIE_MAX_AGE, SIDEBAR_COOKIE_NAME, SIDEBAR_KEYBOARD_SHORTCUT, SIDEBAR_WIDTH, SIDEBAR_WIDTH_ICON } from "./utils"
|
|
|
|
const props = withDefaults(defineProps<{
|
|
defaultOpen?: boolean
|
|
open?: boolean
|
|
class?: HTMLAttributes["class"]
|
|
}>(), {
|
|
defaultOpen: !defaultDocument?.cookie.includes(`${SIDEBAR_COOKIE_NAME}=false`),
|
|
open: undefined,
|
|
})
|
|
|
|
const emits = defineEmits<{
|
|
"update:open": [open: boolean]
|
|
}>()
|
|
|
|
const isMobile = useMediaQuery("(max-width: 768px)")
|
|
const openMobile = ref(false)
|
|
|
|
const open = useVModel(props, "open", emits, {
|
|
defaultValue: props.defaultOpen ?? false,
|
|
passive: (props.open === undefined) as false,
|
|
}) as Ref<boolean>
|
|
|
|
function setOpen(value: boolean) {
|
|
open.value = value // emits('update:open', value)
|
|
|
|
// This sets the cookie to keep the sidebar state.
|
|
document.cookie = `${SIDEBAR_COOKIE_NAME}=${open.value}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
|
|
}
|
|
|
|
function setOpenMobile(value: boolean) {
|
|
openMobile.value = value
|
|
}
|
|
|
|
// Helper to toggle the sidebar.
|
|
function toggleSidebar() {
|
|
return isMobile.value ? setOpenMobile(!openMobile.value) : setOpen(!open.value)
|
|
}
|
|
|
|
useEventListener("keydown", (event: KeyboardEvent) => {
|
|
if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) {
|
|
event.preventDefault()
|
|
toggleSidebar()
|
|
}
|
|
})
|
|
|
|
// We add a state so that we can do data-state="expanded" or "collapsed".
|
|
// This makes it easier to style the sidebar with Tailwind classes.
|
|
const state = computed(() => open.value ? "expanded" : "collapsed")
|
|
|
|
provideSidebarContext({
|
|
state,
|
|
open,
|
|
setOpen,
|
|
isMobile,
|
|
openMobile,
|
|
setOpenMobile,
|
|
toggleSidebar,
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<TooltipProvider :delay-duration="0">
|
|
<div
|
|
data-slot="sidebar-wrapper"
|
|
:style="{
|
|
'--sidebar-width': SIDEBAR_WIDTH,
|
|
'--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
|
|
}"
|
|
:class="cn('group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full', props.class)"
|
|
v-bind="$attrs"
|
|
>
|
|
<slot />
|
|
</div>
|
|
</TooltipProvider>
|
|
</template>
|