Dev/v1.2 (#4)
Build Docker Image / build-backend (push) Successful in 1m56s
Build Docker Image / build-frontend (push) Successful in 1m55s

Co-authored-by: ISMAIL MASSERAN <topaz@Mac.dlinkrouter.local>
Reviewed-on: #4
This commit was merged in pull request #4.
This commit is contained in:
2026-07-06 12:50:55 +08:00
parent e77712105c
commit d06c4701a4
190 changed files with 5365 additions and 26981 deletions
BIN
View File
Binary file not shown.
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 275 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 202 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 33 KiB

@@ -3,6 +3,7 @@ import { SheetRoot, SheetContent } from '@/components/ui/sheet'
import { Lucide } from '@/components/ui/lucide'
import { useColorSchemeStore, type ColorSchemes } from '@/stores/color-scheme'
import { useDarkModeStore } from '@/stores/dark-mode'
import { applyColorSchemeClass, applyDarkModeClass } from '@/utils/applyAppearance'
import { ref } from 'vue'
const themeSwitcherSheet = ref(false)
@@ -10,30 +11,19 @@ const setThemeSwitcherSheet = (value: boolean) => {
themeSwitcherSheet.value = value
}
const setColorSchemeClass = () => {
const el = document.querySelectorAll('html')[0]
el?.setAttribute('data-theme', useColorSchemeStore().colorSchemeValue)
if (useDarkModeStore().darkModeValue) el?.classList.add('dark')
}
const colorSchemeStore = useColorSchemeStore()
const switchColorScheme = (colorScheme: ColorSchemes) => {
useColorSchemeStore().setColorScheme(colorScheme)
setColorSchemeClass()
applyColorSchemeClass()
setThemeSwitcherSheet(false)
}
setColorSchemeClass()
const setDarkModeClass = () => {
const el = document.querySelectorAll('html')[0]
useDarkModeStore().darkModeValue ? el?.classList.add('dark') : el?.classList.remove('dark')
}
const darkModeStore = useDarkModeStore()
const switchDarkMode = (darkMode: boolean) => {
useDarkModeStore().setDarkMode(darkMode)
setDarkModeClass()
applyDarkModeClass()
setThemeSwitcherSheet(false)
}
setDarkModeClass()
const colorSchemes: Array<ColorSchemes> = ['default', '1', '2', '3', '4', '5']
@@ -7,30 +7,28 @@ import { normalizeProps, useMachine } from "@zag-js/vue";
import { computed, provide } from "vue";
import { paginationRoot } from "@mykopkb/core/styles/pagination.styles";
const {
class: className,
asChild = false,
count,
pageSize,
siblingCount,
...props
} = defineProps<Partial<Props> & { class?: string; asChild?: boolean }>();
const props = defineProps<Partial<Props> & { class?: string; asChild?: boolean }>();
const service = useMachine(pagination.machine, {
...props,
count,
pageSize,
siblingCount,
id: crypto.randomUUID(),
const paginationId = crypto.randomUUID();
const machineProps = computed(() => {
const { class: _class, asChild: _asChild, id: _id, ...rest } = props;
return {
...rest,
id: paginationId,
};
});
const service = useMachine(pagination.machine, machineProps);
const api = computed(() => pagination.connect(service, normalizeProps));
provide("paginationApi", api);
</script>
<template>
<Slot :class="cn(paginationRoot, className)" v-bind="{ ...props, ...$attrs, ...api.getRootProps() }">
<slot v-if="asChild" />
<Slot :class="cn(paginationRoot, props.class)" v-bind="{ ...$attrs, ...api.getRootProps() }">
<slot v-if="props.asChild" />
<div v-else>
<slot />
</div>
@@ -1,7 +1,7 @@
export const paginationRoot = 'flex gap-1'
export const paginationItem = [
'h-10 px-4 py-2 inline-flex items-center justify-center rounded-xl cursor-pointer hover:bg-foreground/5',
'data-[selected]:border data-[selected]:bg-background data-[selected]:border-foreground/10 data-[selected]:font-medium data-[selected]:shadow-md/5',
'h-10 min-w-10 px-3 py-2 inline-flex items-center justify-center rounded-xl cursor-pointer text-foreground hover:bg-foreground/5',
'data-[selected]:border data-[selected]:bg-primary/10 data-[selected]:border-primary/30 data-[selected]:text-primary data-[selected]:font-semibold data-[selected]:shadow-md/5',
'data-[disabled]:opacity-70',
]
export const paginationPrevTrigger = paginationItem
+35 -30
View File
@@ -94,25 +94,29 @@ const props = withDefaults(defineProps<{
items: TableItem[];
loading?: boolean;
itemsPerPage?: number;
page?: number;
itemKey?: string;
pagination?: PaginationData | null;
showPagination?: boolean;
currentSort?: SortConfig[];
exportable?: boolean;
exportFileName?: string;
/** When true, table body scrolls inside a max-height box instead of growing the page. */
scrollable?: boolean;
/** Max height of the scrollable table area (CSS value). */
maxHeight?: string;
}>(), {
items: () => [],
headers: () => [],
loading: false,
itemsPerPage: 10,
page: 1,
itemKey: 'id',
pagination: null,
showPagination: false,
currentSort: () => [],
exportable: false,
exportFileName: 'table-data'
exportFileName: 'table-data',
scrollable: true,
maxHeight: 'min(70dvh, 640px)',
});
// Define model props
@@ -122,7 +126,6 @@ const itemsPerPageModel = defineModel<number>('items-per-page');
// Emits
const emit = defineEmits<{
'update:sort-by': [value: SortConfig[]];
'update:page': [value: number];
'update:items-per-page': [value: number];
'update:column-widths': [value: Record<string, number>];
'export-error': [error: Error];
@@ -449,7 +452,6 @@ const setItemsPerPageValue = (details: { value: string[] }) => {
const handlePageChange = (details: { page: number }) => {
pageModel.value = details.page;
emit('update:page', details.page);
};
const getItemKey = (item: TableItem, index: number) => {
@@ -461,16 +463,18 @@ const currentPage = computed(
() => pageModel.value ?? props.pagination?.current_page ?? 1,
);
const isFirstPage = computed(() => currentPage.value <= 1);
const isLastPage = computed(
() => currentPage.value >= (props.pagination?.last_page ?? 1),
);
const slots = useSlots();
const showTableToolbar = computed(() => props.exportable || !!slots.toolbar);
const scrollContainerStyle = computed(() => {
if (!props.scrollable) {
return undefined;
}
return { maxHeight: props.maxHeight };
});
const recordSummary = computed(() => {
if (!props.pagination) return null;
@@ -743,12 +747,20 @@ watch(sortBy, (newSort) => {
</div>
</div>
<div class="relative px-4 pt-3" :class="loading && 'pointer-events-none opacity-50'">
<Table variant="boxed" class="custom-data-table border-separate border-spacing-y-2.5">
<div
class="relative px-4 pt-3"
:class="[
loading && 'pointer-events-none opacity-50',
scrollable && 'data-table-scroll-area overflow-auto',
]"
:style="scrollContainerStyle"
>
<Table variant="boxed" :class="cn('custom-data-table border-separate border-spacing-y-2.5', !scrollable && 'overflow-hidden')">
<TableHeader>
<TableRow class="border-0 hover:bg-transparent">
<TableHead v-for="header in sortedHeaders" :key="header.key" :class="cn(
'group/head relative bg-primary text-primary-foreground font-semibold border-y border-primary/20 first:rounded-tl-xl first:border-s last:rounded-tr-xl last:border-e',
scrollable && 'sticky top-0 z-10',
headerAlignClass(header.align),
)" :style="{ width: `${header.width}px`, minWidth: `${header.width}px` }">
<div v-if="header.sortable" class="flex items-center gap-2 cursor-pointer select-none" role="button"
@@ -851,28 +863,21 @@ watch(sortBy, (newSort) => {
</template>
</div>
<PaginationRoot class="flex items-center gap-2" :count="pagination.total" :pageSize="pagination.per_page"
:page="currentPage" :siblingCount="1" :onPageChange="handlePageChange">
<PaginationPrevTrigger asChild>
<Button size="sm" look="outline" variant="secondary" :disabled="isFirstPage">
<ArrowLeft class="size-4" />
</Button>
<PaginationRoot class="flex items-center gap-2" :count="pagination.total" :page-size="pagination.per_page"
:page="currentPage" :sibling-count="1" :on-page-change="handlePageChange">
<PaginationPrevTrigger>
<ArrowLeft class="size-4" />
</PaginationPrevTrigger>
<PaginationContext v-slot="{ pagination: paginationApi }">
<template v-for="(pageItem, index) in paginationApi?.pages" :key="index">
<PaginationItem v-if="pageItem.type === 'page'" v-bind="{ ...pageItem }" asChild>
<Button size="sm" :look="pageItem.value === currentPage ? 'filled' : 'outline'"
:variant="pageItem.value === currentPage ? 'primary' : 'secondary'">
{{ pageItem.value }}
</Button>
<PaginationItem v-if="pageItem.type === 'page'" v-bind="{ ...pageItem }">
{{ pageItem.value }}
</PaginationItem>
<PaginationEllipsis v-else :index="index" />
</template>
</PaginationContext>
<PaginationNextTrigger asChild>
<Button size="sm" look="outline" variant="secondary" :disabled="isLastPage">
<ArrowRight class="size-4" />
</Button>
<PaginationNextTrigger>
<ArrowRight class="size-4" />
</PaginationNextTrigger>
</PaginationRoot>
</div>
@@ -881,8 +886,8 @@ watch(sortBy, (newSort) => {
</template>
<style scoped>
.custom-data-table {
overflow: hidden;
.data-table-scroll-area > :deep(div) {
overflow: visible;
}
.sortable-indicator {
-11
View File
@@ -1,11 +0,0 @@
<script setup lang="ts">
import { Box } from "@/components/ui/box";
</script>
<template>
<Box class="w-full xl:w-1/2">
<div class="text-xl font-medium capitalize border-b border-foreground/15 pb-5">{{ $route.name }}</div>
<div class="mt-16 flex flex-col gap-10">
<RouterView />
</div>
</Box>
</template>
-129
View File
@@ -1,129 +0,0 @@
<script lang="ts" setup>
import {
AccordionRoot,
AccordionItem,
AccordionTrigger,
AccordionContent,
} from "@/components/ui/accordion";
import {
Preview,
SectionTitle,
SectionContent,
InstallPackage,
PreviewCode,
} from "@/components/docs";
import { Box } from "@/components/ui/box";
</script>
<template>
<div id="preview" class="-mt-20">
<Preview>
<template #preview>
<Box raised="single" class="w-full">
<AccordionRoot class="w-full" :default-value="['product-information']">
<AccordionItem value="product-information">
<AccordionTrigger>Product Information</AccordionTrigger>
<AccordionContent>
<p class="mb-2">
Our flagship product combines cutting-edge technology with
sleek design. Built with premium materials, it offers
unparalleled performance and reliability.
</p>
<p>
Key features include advanced processing capabilities, and an
intuitive user interface designed for both beginners and
experts.
</p>
</AccordionContent>
</AccordionItem>
<AccordionItem value="shipping-details">
<AccordionTrigger>Shipping Details</AccordionTrigger>
<AccordionContent>
<p class="mb-2">
We offer worldwide shipping through trusted courier partners.
Standard delivery takes 3-5 business days, while express
shipping ensures delivery within 1-2 business days.
</p>
<p>
All orders are carefully packaged and fully insured. Track
your shipment in real-time through our dedicated tracking
portal.
</p>
</AccordionContent>
</AccordionItem>
<AccordionItem value="return-policy">
<AccordionTrigger>Return Policy</AccordionTrigger>
<AccordionContent>
<p class="mb-2">
We stand behind our products with a comprehensive 30-day
return policy. If you're not completely satisfied, simply
return the item in its original condition.
</p>
<p>
Our hassle-free return process includes free return shipping
and full refunds processed within 48 hours of receiving the
returned item.
</p>
</AccordionContent>
</AccordionItem>
</AccordionRoot>
</Box>
</template>
<template #code>
<PreviewCode>
{{ `
<Box raised="single" class="w-full">
<AccordionRoot class="w-full" :default-value="['product-information']">
<AccordionItem value="product-information">
<AccordionTrigger>Product Information</AccordionTrigger>
<AccordionContent>
<p class="mb-2">
Our flagship product combines cutting-edge technology with
sleek design. Built with premium materials, it offers
unparalleled performance and reliability.
</p>
<p>
Key features include advanced processing capabilities, and an
intuitive user interface designed for both beginners and
experts.
</p>
</AccordionContent>
</AccordionItem>
<AccordionItem value="shipping-details">
<AccordionTrigger>Shipping Details</AccordionTrigger>
<AccordionContent>
<p class="mb-2">
We offer worldwide shipping through trusted courier partners.
Standard delivery takes 3-5 business days, while express
shipping ensures delivery within 1-2 business days.
</p>
<p>
All orders are carefully packaged and fully insured. Track
your shipment in real-time through our dedicated tracking
portal.
</p>
</AccordionContent>
</AccordionItem>
<AccordionItem value="return-policy">
<AccordionTrigger>Return Policy</AccordionTrigger>
<AccordionContent>
<p class="mb-2">
We stand behind our products with a comprehensive 30-day
return policy. If you're not completely satisfied, simply
return the item in its original condition.
</p>
<p>
Our hassle-free return process includes free return shipping
and full refunds processed within 48 hours of receiving the
returned item.
</p>
</AccordionContent>
</AccordionItem>
</AccordionRoot>
</Box>
` }}
</PreviewCode>
</template>
</Preview>
</div>
</template>
-639
View File
@@ -1,639 +0,0 @@
<script lang="ts" setup>
import {
AlertRoot,
AlertTitle,
AlertDescription,
AlertCloseTrigger,
} from "@/components/ui/alert";
import {
Preview,
SectionTitle,
SectionContent,
InstallPackage,
PreviewCode,
} from "@/components/docs";
import { Compass } from "@lucide/vue";
import { Box } from "@/components/ui/box";
</script>
<template>
<div id="preview" class="-mt-20">
<Preview>
<template #preview>
<AlertRoot variant="primary">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertDescription>
This is an alert with icon, title and description.
</AlertDescription>
<AlertCloseTrigger />
</AlertRoot>
</template>
<template #code>
<PreviewCode>
{{ `
<AlertRoot variant="primary">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertDescription>
This is an alert with icon, title and description.
</AlertDescription>
<AlertCloseTrigger />
</AlertRoot>
` }}
</PreviewCode>
</template>
</Preview>
</div>
<div id="installation">
<SectionTitle>Installation</SectionTitle>
<SectionContent>Install the following dependencies:</SectionContent>
<InstallPackage>add @zag-js/vue @zag-js/alert</InstallPackage>
<SectionContent>
Copy and paste the following code into your project.
</SectionContent>
<PreviewCode title="components/ui/alert/AlertCloseTrigger.vue">
{{`
<script lang="ts" setup>
import { X } from "@lucide/vue";
import { cn } from "@mykopkb/core/utils/cn";
import { Slot } from "@/components/ui/slot";
import { alertCloseTrigger } from "@mykopkb/core/styles/alert.styles";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const context = inject<{
present: boolean;
setPresent: (value: boolean) => void;
} | null>("alertPresent", null);
</script>
<template>
<Slot :class="cn([className, alertCloseTrigger])" v-bind="{ ...props, ...$attrs }"
@click="context?.setPresent(false)">
<slot v-if="asChild" />
<div v-else>
<slot v-if="$slots.default" />
<X v-else />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/alert/AlertDescription.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { Slot } from "@/components/ui/slot";
import { alertDescription } from "@mykopkb/core/styles/alert.styles";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
</script>
<template>
<Slot :class="cn([className, alertDescription])" v-bind="{ ...props, ...$attrs }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/alert/AlertRoot.vue">
{{`
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import {
alertRootVariants,
type AlertRootVariants,
} from "@mykopkb/core/styles/alert.styles";
import { Presence } from "@/components/ui/presence";
import { ref, provide } from "vue";
const {
class: className,
look,
variant,
...rest
} = defineProps<AlertRootVariants & { class?: string }>();
const present = ref(true);
const setPresent = (value: boolean) => {
present.value = value;
};
provide("alertPresent", { present, setPresent });
</script>
<template>
<Presence :class="
cn(
alertRootVariants({
look,
variant,
}),
className
)
" v-bind="rest" :present="present">
<slot />
</Presence>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/alert/AlertTitle.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { Slot } from "@/components/ui/slot";
import { alertTitle } from "@mykopkb/core/styles/alert.styles";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
</script>
<template>
<Slot :class="cn([className, alertTitle])" v-bind="{ ...props, ...$attrs }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/alert/index.ts">
{{ `
export { default as AlertRoot } from "./AlertRoot.vue";
export { default as AlertTitle } from "./AlertTitle.vue";
export { default as AlertDescription } from "./AlertDescription.vue";
export { default as AlertCloseTrigger } from "./AlertCloseTrigger.vue";
` }}
</PreviewCode>
<SectionContent>
Update the import paths to match your project setup.
</SectionContent>
</div>
<div id="usage">
<SectionTitle>Usage</SectionTitle>
<PreviewCode>
{{ `
import {
AlertRoot,
AlertTitle,
AlertDescription,
AlertCloseTrigger,
} from "@/components/ui/alert";
` }}
</PreviewCode>
<PreviewCode>
{{ `
<AlertRoot>
<Compass />
<AlertTitle>
Success! Your changes have been saved
</AlertTitle>
<AlertDescription>
This is an alert with icon, title and description.
</AlertDescription>
<AlertCloseTrigger />
</AlertRoot>
` }}
</PreviewCode>
</div>
<div id="variants">
<SectionTitle>Variants</SectionTitle>
<SectionContent>A collection of components you can use.</SectionContent>
<Preview class="flex-col!">
<template #preview>
<AlertRoot variant="primary">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertDescription>
This is an alert with icon, title and description.
</AlertDescription>
<AlertCloseTrigger />
</AlertRoot>
<AlertRoot variant="secondary">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertDescription>
This is an alert with icon, title and description.
</AlertDescription>
<AlertCloseTrigger />
</AlertRoot>
<AlertRoot variant="success">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertDescription>
This is an alert with icon, title and description.
</AlertDescription>
<AlertCloseTrigger />
</AlertRoot>
<AlertRoot variant="danger">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertDescription>
This is an alert with icon, title and description.
</AlertDescription>
<AlertCloseTrigger />
</AlertRoot>
<AlertRoot variant="pending">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertDescription>
This is an alert with icon, title and description.
</AlertDescription>
<AlertCloseTrigger />
</AlertRoot>
<AlertRoot variant="warning">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertDescription>
This is an alert with icon, title and description.
</AlertDescription>
<AlertCloseTrigger />
</AlertRoot>
</template>
<template #code>
<PreviewCode>
{{ `
<AlertRoot variant="primary">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertDescription>
This is an alert with icon, title and description.
</AlertDescription>
<AlertCloseTrigger />
</AlertRoot>
<AlertRoot variant="secondary">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertDescription>
This is an alert with icon, title and description.
</AlertDescription>
<AlertCloseTrigger />
</AlertRoot>
<AlertRoot variant="success">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertDescription>
This is an alert with icon, title and description.
</AlertDescription>
<AlertCloseTrigger />
</AlertRoot>
<AlertRoot variant="danger">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertDescription>
This is an alert with icon, title and description.
</AlertDescription>
<AlertCloseTrigger />
</AlertRoot>
<AlertRoot variant="pending">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertDescription>
This is an alert with icon, title and description.
</AlertDescription>
<AlertCloseTrigger />
</AlertRoot>
<AlertRoot variant="warning">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertDescription>
This is an alert with icon, title and description.
</AlertDescription>
<AlertCloseTrigger />
</AlertRoot>
` }}
</PreviewCode>
</template>
</Preview>
<Preview class="flex-col!">
<template #preview>
<AlertRoot look="filled" variant="primary">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertDescription>
This is an alert with icon, title and description.
</AlertDescription>
<AlertCloseTrigger />
</AlertRoot>
<AlertRoot look="filled" variant="secondary">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertDescription>
This is an alert with icon, title and description.
</AlertDescription>
<AlertCloseTrigger />
</AlertRoot>
<AlertRoot look="filled" variant="success">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertDescription>
This is an alert with icon, title and description.
</AlertDescription>
<AlertCloseTrigger />
</AlertRoot>
<AlertRoot look="filled" variant="danger">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertDescription>
This is an alert with icon, title and description.
</AlertDescription>
<AlertCloseTrigger />
</AlertRoot>
<AlertRoot look="filled" variant="pending">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertDescription>
This is an alert with icon, title and description.
</AlertDescription>
<AlertCloseTrigger />
</AlertRoot>
<AlertRoot look="filled" variant="warning">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertDescription>
This is an alert with icon, title and description.
</AlertDescription>
<AlertCloseTrigger />
</AlertRoot>
</template>
<template #code>
<PreviewCode>
{{ `
<AlertRoot look="filled" variant="primary">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertDescription>
This is an alert with icon, title and description.
</AlertDescription>
<AlertCloseTrigger />
</AlertRoot>
<AlertRoot look="filled" variant="secondary">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertDescription>
This is an alert with icon, title and description.
</AlertDescription>
<AlertCloseTrigger />
</AlertRoot>
<AlertRoot look="filled" variant="success">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertDescription>
This is an alert with icon, title and description.
</AlertDescription>
<AlertCloseTrigger />
</AlertRoot>
<AlertRoot look="filled" variant="danger">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertDescription>
This is an alert with icon, title and description.
</AlertDescription>
<AlertCloseTrigger />
</AlertRoot>
<AlertRoot look="filled" variant="pending">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertDescription>
This is an alert with icon, title and description.
</AlertDescription>
<AlertCloseTrigger />
</AlertRoot>
<AlertRoot look="filled" variant="warning">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertDescription>
This is an alert with icon, title and description.
</AlertDescription>
<AlertCloseTrigger />
</AlertRoot>
` }}
</PreviewCode>
</template>
</Preview>
<Preview class="flex-col!">
<template #preview>
<AlertRoot variant="primary">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertCloseTrigger />
</AlertRoot>
<AlertRoot variant="secondary">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertCloseTrigger />
</AlertRoot>
<AlertRoot variant="success">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertCloseTrigger />
</AlertRoot>
<AlertRoot variant="danger">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertCloseTrigger />
</AlertRoot>
<AlertRoot variant="pending">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertCloseTrigger />
</AlertRoot>
<AlertRoot variant="warning">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertCloseTrigger />
</AlertRoot>
</template>
<template #code>
<PreviewCode>
{{ `
<AlertRoot variant="primary">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertCloseTrigger />
</AlertRoot>
<AlertRoot variant="secondary">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertCloseTrigger />
</AlertRoot>
<AlertRoot variant="success">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertCloseTrigger />
</AlertRoot>
<AlertRoot variant="danger">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertCloseTrigger />
</AlertRoot>
<AlertRoot variant="pending">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertCloseTrigger />
</AlertRoot>
<AlertRoot variant="warning">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertCloseTrigger />
</AlertRoot>
` }}
</PreviewCode>
</template>
</Preview>
<Preview class="flex-col!">
<template #preview>
<AlertRoot look="filled" variant="primary">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
</AlertRoot>
<AlertRoot look="filled" variant="secondary">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
</AlertRoot>
<AlertRoot look="filled" variant="success">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
</AlertRoot>
<AlertRoot look="filled" variant="danger">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
</AlertRoot>
<AlertRoot look="filled" variant="pending">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
</AlertRoot>
<AlertRoot look="filled" variant="warning">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
</AlertRoot>
</template>
<template #code>
<PreviewCode>
{{ `
<AlertRoot look="filled" variant="primary">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
</AlertRoot>
<AlertRoot look="filled" variant="secondary">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
</AlertRoot>
<AlertRoot look="filled" variant="success">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
</AlertRoot>
<AlertRoot look="filled" variant="danger">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
</AlertRoot>
<AlertRoot look="filled" variant="pending">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
</AlertRoot>
<AlertRoot look="filled" variant="warning">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
</AlertRoot>
` }}
</PreviewCode>
</template>
</Preview>
<Preview class="flex-col!">
<template #preview>
<Box class="p-0">
<AlertRoot variant="ghost">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertDescription>
This is an alert with icon, title and description.
</AlertDescription>
<AlertCloseTrigger />
</AlertRoot>
</Box>
<Box class="p-0" raised="single">
<AlertRoot variant="ghost">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertDescription>
This is an alert with icon, title and description.
</AlertDescription>
<AlertCloseTrigger />
</AlertRoot>
</Box>
<Box class="p-0" raised="double">
<AlertRoot variant="ghost">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertDescription>
This is an alert with icon, title and description.
</AlertDescription>
<AlertCloseTrigger />
</AlertRoot>
</Box>
</template>
<template #code>
<PreviewCode>
{{ `
<Box class="p-0">
<AlertRoot variant="ghost">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertDescription>
This is an alert with icon, title and description.
</AlertDescription>
<AlertCloseTrigger />
</AlertRoot>
</Box>
<Box class="p-0" raised="single">
<AlertRoot variant="ghost">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertDescription>
This is an alert with icon, title and description.
</AlertDescription>
<AlertCloseTrigger />
</AlertRoot>
</Box>
<Box class="p-0" raised="double">
<AlertRoot variant="ghost">
<Compass />
<AlertTitle>Success! Your changes have been saved</AlertTitle>
<AlertDescription>
This is an alert with icon, title and description.
</AlertDescription>
<AlertCloseTrigger />
</AlertRoot>
</Box>
` }}
</PreviewCode>
</template>
</Preview>
</div>
</template>
-239
View File
@@ -1,239 +0,0 @@
<script lang="ts" setup>
import {
AvatarRoot,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
import {
Preview,
SectionTitle,
SectionContent,
InstallPackage,
PreviewCode,
} from "@/components/docs";
</script>
<template>
<div id="preview" class="-mt-20">
<Preview>
<template #preview>
<AvatarRoot>
<AvatarFallback>PA</AvatarFallback>
<AvatarImage src="https://i.pravatar.cc/300" alt="avatar" />
</AvatarRoot>
</template>
<template #code>
<PreviewCode>
{{ `
<AvatarRoot>
<AvatarFallback>PA</AvatarFallback>
<AvatarImage src="https://i.pravatar.cc/300" alt="avatar" />
</AvatarRoot>
` }}
</PreviewCode>
</template>
</Preview>
</div>
<div id="installation">
<SectionTitle>Installation</SectionTitle>
<SectionContent>Install the following dependencies:</SectionContent>
<InstallPackage>add @zag-js/vue @zag-js/avatar</InstallPackage>
<SectionContent>
Copy and paste the following code into your project.
</SectionContent>
<PreviewCode title="components/ui/avatar/AvatarFallback.vue">
{{ `
<script lang="ts" setup>
import type { Api } from "@zag-js/avatar";
import { cn } from "@mykopkb/core/utils/cn";
import { avatarFallback } from "@mykopkb/core/styles/avatar.styles";
import { inject } from "vue";
import { Slot } from "@/components/ui/slot";
const {
class: className,
asChild = false,
...props
} = defineProps<{
asChild?: boolean;
class?: string;
}>();
const api = inject<Api>("avatarApi");
</script>
<template>
<Slot :class="cn(avatarFallback, className)" v-bind="{ ...props, ...$attrs, ...api?.getFallbackProps() }">
<slot v-if="asChild" />
<span v-else>
<slot />
</span>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/avatar/AvatarImage.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { avatarImage } from "@mykopkb/core/styles/avatar.styles";
import { inject } from "vue";
import type { Api } from "@zag-js/avatar";
const { class: className, ...props } = defineProps<{
class?: string;
}>();
const api = inject<Api>("avatarApi");
</script>
<template>
<img :class="cn(avatarImage, className)" v-bind="{ ...props, ...$attrs, ...api?.getImageProps() }" />
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/avatar/AvatarRoot.vue">
{{`
<script lang="ts" setup>
import * as avatar from "@zag-js/avatar";
import { provide, computed } from "vue";
import { useMachine, normalizeProps } from "@zag-js/vue";
import type { Props } from "@zag-js/avatar";
import { cn } from "@mykopkb/core/utils/cn";
import {
avatarRootVariants,
type AvatarRootVariants,
} from "@mykopkb/core/styles/avatar.styles";
import { Slot } from "@/components/ui/slot";
const {
class: className,
bordered,
asChild = false,
...props
} = defineProps<
AvatarRootVariants &
Partial<Props> & {
class?: string;
asChild?: boolean;
}
>();
const service = useMachine(avatar.machine, {
...props,
id: crypto.randomUUID(),
});
const api = computed(() => avatar.connect(service, normalizeProps));
provide("avatarApi", api);
</script>
<template>
<Slot :class="
cn(
avatarRootVariants({
bordered,
className,
}),
className
)
" v-bind="{ ...props, ...$attrs, ...api.getRootProps() }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/avatar/index.ts">
{{ `
export { default as AvatarRoot } from "./AvatarRoot.vue";
export { default as AvatarFallback } from "./AvatarFallback.vue";
export { default as AvatarImage } from "./AvatarImage.vue";
` }}
</PreviewCode>
<SectionContent>
Update the import paths to match your project setup.
</SectionContent>
</div>
<div id="usage">
<SectionTitle>Usage</SectionTitle>
<PreviewCode>
{{ `
import {
AvatarRoot,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
` }}
</PreviewCode>
<PreviewCode>
{{ `
<AvatarRoot>
<AvatarFallback>PA</AvatarFallback>
<AvatarImage src="https://i.pravatar.cc/300" alt="avatar" />
</AvatarRoot>
` }}
</PreviewCode>
</div>
<div id="variants">
<SectionTitle>Variants</SectionTitle>
<SectionContent>A collection of components you can use.</SectionContent>
<Preview>
<template #preview>
<AvatarRoot :bordered="false">
<AvatarFallback>PA</AvatarFallback>
<AvatarImage src="https://i.pravatar.cc/300" alt="avatar" />
</AvatarRoot>
</template>
<template #code>
<PreviewCode>
{{ `
<AvatarRoot :bordered="false">
<AvatarFallback>PA</AvatarFallback>
<AvatarImage src="https://i.pravatar.cc/300" alt="avatar" />
</AvatarRoot>
` }}
</PreviewCode>
</template>
</Preview>
<Preview>
<template #preview>
<AvatarRoot class="rounded-full">
<AvatarFallback>PA</AvatarFallback>
<AvatarImage src="https://i.pravatar.cc/300" alt="avatar" />
</AvatarRoot>
</template>
<template #code>
<PreviewCode>
{{ `
<AvatarRoot class="rounded-full">
<AvatarFallback>PA</AvatarFallback>
<AvatarImage src="https://i.pravatar.cc/300" alt="avatar" />
</AvatarRoot>
` }}
</PreviewCode>
</template>
</Preview>
<Preview>
<template #preview>
<AvatarRoot class="rounded-full" :bordered="false">
<AvatarFallback>PA</AvatarFallback>
<AvatarImage src="https://i.pravatar.cc/300" alt="avatar" />
</AvatarRoot>
</template>
<template #code>
<PreviewCode>
{{ `
<AvatarRoot class="rounded-full" :bordered="false">
<AvatarFallback>PA</AvatarFallback>
<AvatarImage src="https://i.pravatar.cc/300" alt="avatar" />
</AvatarRoot>
` }}
</PreviewCode>
</template>
</Preview>
</div>
</template>
-351
View File
@@ -1,351 +0,0 @@
<script lang="ts" setup>
import { ChevronDown, CheckSquare } from "@lucide/vue";
import { Badge } from "@/components/ui/badge";
import {
Preview,
SectionTitle,
SectionContent,
PreviewCode,
} from "@/components/docs";
</script>
<template>
<div id="preview" class="-mt-20">
<Preview>
<template #preview>
<Badge variant="primary">12%</Badge>
</template>
<template #code>
<PreviewCode>
{{ `
<Badge variant="primary">12%</Badge>
` }}
</PreviewCode>
</template>
</Preview>
</div>
<div id="installation">
<SectionTitle>Installation</SectionTitle>
<SectionContent>
Copy and paste the following code into your project.
</SectionContent>
<PreviewCode title="components/ui/badge/Badge.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import {
TooltipRoot,
TooltipTrigger,
TooltipPositioner,
TooltipContent,
} from "@/components/ui/tooltip";
import {
badgeVariants,
type BadgeVariants,
} from "@mykopkb/core/styles/badge.styles";
const {
class: className,
look,
variant,
content,
...props
} = defineProps<
BadgeVariants & {
class?: string;
content?: string;
}
>();
</script>
<template>
<TooltipRoot :disabled="!content">
<TooltipTrigger as-child>
<span :class="cn(badgeVariants({ look, variant, className }))" v-bind="{ ...props, ...$attrs }">
<slot />
</span>
</TooltipTrigger>
<TooltipPositioner>
<TooltipContent>\{\{ content \}\}</TooltipContent>
</TooltipPositioner>
</TooltipRoot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/badge/index.ts">
{{ `
export { default as Badge } from "./Badge.vue";
` }}
</PreviewCode>
<SectionContent>
Update the import paths to match your project setup.
</SectionContent>
</div>
<div id="usage">
<SectionTitle>Usage</SectionTitle>
<PreviewCode>
{{ `
import { Badge } from "@/components/ui/badge";
` }}
</PreviewCode>
<PreviewCode>
{{ `
<Badge>12%</Badge>
` }}
</PreviewCode>
</div>
<div id="variants">
<SectionTitle>Variants</SectionTitle>
<SectionContent>A collection of components you can use.</SectionContent>
<Preview>
<template #preview>
<Badge variant="primary">12%</Badge>
<Badge variant="secondary">12%</Badge>
<Badge variant="success">12%</Badge>
<Badge variant="danger">12%</Badge>
<Badge variant="pending">12%</Badge>
<Badge variant="warning">12%</Badge>
</template>
<template #code>
<PreviewCode>
{{ `
<Badge variant="primary">12%</Badge>
<Badge variant="secondary">12%</Badge>
<Badge variant="success">12%</Badge>
<Badge variant="danger">12%</Badge>
<Badge variant="pending">12%</Badge>
<Badge variant="warning">12%</Badge>
` }}
</PreviewCode>
</template>
</Preview>
<Preview>
<template #preview>
<Badge variant="primary"> 12%
<ChevronDown />
</Badge>
<Badge variant="secondary"> 12%
<ChevronDown />
</Badge>
<Badge variant="success"> 12%
<ChevronDown />
</Badge>
<Badge variant="danger"> 12%
<ChevronDown />
</Badge>
<Badge variant="pending"> 12%
<ChevronDown />
</Badge>
<Badge variant="warning"> 12%
<ChevronDown />
</Badge>
</template>
<template #code>
<PreviewCode>
{{ `
<Badge variant="primary"> 12%
<ChevronDown />
</Badge>
<Badge variant="secondary"> 12%
<ChevronDown />
</Badge>
<Badge variant="success"> 12%
<ChevronDown />
</Badge>
<Badge variant="danger"> 12%
<ChevronDown />
</Badge>
<Badge variant="pending"> 12%
<ChevronDown />
</Badge>
<Badge variant="warning"> 12%
<ChevronDown />
</Badge>
` }}
</PreviewCode>
</template>
</Preview>
<Preview>
<template #preview>
<Badge look="outline" variant="primary"> 12% </Badge>
<Badge look="outline" variant="secondary"> 12% </Badge>
<Badge look="outline" variant="success"> 12% </Badge>
<Badge look="outline" variant="danger"> 12% </Badge>
<Badge look="outline" variant="pending"> 12% </Badge>
<Badge look="outline" variant="warning"> 12% </Badge>
</template>
<template #code>
<PreviewCode>
{{ `
<Badge look="outline" variant="primary"> 12% </Badge>
<Badge look="outline" variant="secondary"> 12% </Badge>
<Badge look="outline" variant="success"> 12% </Badge>
<Badge look="outline" variant="danger"> 12% </Badge>
<Badge look="outline" variant="pending"> 12% </Badge>
<Badge look="outline" variant="warning"> 12% </Badge>
` }}
</PreviewCode>
</template>
</Preview>
<Preview>
<template #preview>
<Badge look="outline" variant="primary"> 12%
<ChevronDown />
</Badge>
<Badge look="outline" variant="secondary"> 12%
<ChevronDown />
</Badge>
<Badge look="outline" variant="success"> 12%
<ChevronDown />
</Badge>
<Badge look="outline" variant="danger"> 12%
<ChevronDown />
</Badge>
<Badge look="outline" variant="pending"> 12%
<ChevronDown />
</Badge>
<Badge look="outline" variant="warning"> 12%
<ChevronDown />
</Badge>
</template>
<template #code>
<PreviewCode>
{{ `
<Badge look="outline" variant="primary"> 12%
<ChevronDown />
</Badge>
<Badge look="outline" variant="secondary"> 12%
<ChevronDown />
</Badge>
<Badge look="outline" variant="success"> 12%
<ChevronDown />
</Badge>
<Badge look="outline" variant="danger"> 12%
<ChevronDown />
</Badge>
<Badge look="outline" variant="pending"> 12%
<ChevronDown />
</Badge>
<Badge look="outline" variant="warning"> 12%
<ChevronDown />
</Badge>
` }}
</PreviewCode>
</template>
</Preview>
<Preview>
<template #preview>
<Badge look="filled" variant="primary"> 12% </Badge>
<Badge look="filled" variant="secondary"> 12% </Badge>
<Badge look="filled" variant="success"> 12% </Badge>
<Badge look="filled" variant="danger"> 12% </Badge>
<Badge look="filled" variant="pending"> 12% </Badge>
<Badge look="filled" variant="warning"> 12% </Badge>
</template>
<template #code>
<PreviewCode>
{{ `
<Badge look="filled" variant="primary"> 12% </Badge>
<Badge look="filled" variant="secondary"> 12% </Badge>
<Badge look="filled" variant="success"> 12% </Badge>
<Badge look="filled" variant="danger"> 12% </Badge>
<Badge look="filled" variant="pending"> 12% </Badge>
<Badge look="filled" variant="warning"> 12% </Badge>
` }}
</PreviewCode>
</template>
</Preview>
<Preview>
<template #preview>
<Badge look="filled" variant="primary"> 12%
<ChevronDown />
</Badge>
<Badge look="filled" variant="secondary"> 12%
<ChevronDown />
</Badge>
<Badge look="filled" variant="success"> 12%
<ChevronDown />
</Badge>
<Badge look="filled" variant="danger"> 12%
<ChevronDown />
</Badge>
<Badge look="filled" variant="pending"> 12%
<ChevronDown />
</Badge>
<Badge look="filled" variant="warning"> 12%
<ChevronDown />
</Badge>
</template>
<template #code>
<PreviewCode>
{{ `
<Badge look="filled" variant="primary"> 12%
<ChevronDown />
</Badge>
<Badge look="filled" variant="secondary"> 12%
<ChevronDown />
</Badge>
<Badge look="filled" variant="success"> 12%
<ChevronDown />
</Badge>
<Badge look="filled" variant="danger"> 12%
<ChevronDown />
</Badge>
<Badge look="filled" variant="pending"> 12%
<ChevronDown />
</Badge>
<Badge look="filled" variant="warning"> 12%
<ChevronDown />
</Badge>
` }}
</PreviewCode>
</template>
</Preview>
<Preview>
<template #preview>
<Badge look="outline" variant="primary" content="12% Higher than last month">
<CheckSquare /> 12%
</Badge>
<Badge look="outline" variant="secondary" content="12% Higher than last month">
<CheckSquare /> 12%
</Badge>
<Badge look="outline" variant="success" content="12% Higher than last month">
<CheckSquare /> 12%
</Badge>
<Badge look="outline" variant="danger" content="12% Higher than last month">
<CheckSquare /> 12%
</Badge>
<Badge look="outline" variant="pending" content="12% Higher than last month">
<CheckSquare /> 12%
</Badge>
<Badge look="outline" variant="warning" content="12% Higher than last month">
<CheckSquare /> 12%
</Badge>
</template>
<template #code>
<PreviewCode>
{{ `
<Badge look="outline" variant="primary" content="12% Higher than last month">
<CheckSquare /> 12%
</Badge>
<Badge look="outline" variant="secondary" content="12% Higher than last month">
<CheckSquare /> 12%
</Badge>
<Badge look="outline" variant="success" content="12% Higher than last month">
<CheckSquare /> 12%
</Badge>
<Badge look="outline" variant="danger" content="12% Higher than last month">
<CheckSquare /> 12%
</Badge>
<Badge look="outline" variant="pending" content="12% Higher than last month">
<CheckSquare /> 12%
</Badge>
<Badge look="outline" variant="warning" content="12% Higher than last month">
<CheckSquare /> 12%
</Badge>
` }}
</PreviewCode>
</template>
</Preview>
</div>
</template>
-199
View File
@@ -1,199 +0,0 @@
<script lang="ts" setup>
import { CircleGauge } from "@lucide/vue";
import { Box } from "@/components/ui/box";
import {
Preview,
SectionTitle,
SectionContent,
PreviewCode,
} from "@/components/docs";
</script>
<template>
<div id="preview" class="-mt-20">
<Preview>
<template #preview>
<Box class="w-70">
<CircleGauge class="size-7 stroke-1 fill-foreground/10" />
<div class="mt-6 text-2xl font-medium leading-8">
$724,091.47
</div>
<div class="mt-1.5 text-xs uppercase opacity-70">
Item Sales
</div>
</Box>
</template>
<template #code>
<PreviewCode>
{{ `
<Box class="w-70">
<CircleGauge class="size-7 stroke-1 fill-foreground/10" />
<div class="mt-6 text-2xl font-medium leading-8">
$724,091.47
</div>
<div class="mt-1.5 text-xs uppercase opacity-70">
Item Sales
</div>
</Box>
` }}
</PreviewCode>
</template>
</Preview>
</div>
<div id="installation">
<SectionTitle>Installation</SectionTitle>
<SectionContent>
Copy and paste the following code into your project.
</SectionContent>
<PreviewCode title="components/ui/box/box.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import {
boxVariants,
type BoxVariants,
} from "@mykopkb/core/styles/box.styles";
import { Slot } from "@/components/ui/slot";
const {
class: className,
asChild = false,
raised,
...props
} = defineProps<
BoxVariants & {
class?: string;
asChild?: boolean;
}
>();
</script>
<template>
<Slot :class="cn(boxVariants({ raised, className }), className)" v-bind="{ ...props, ...$attrs }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/box/index.ts">
{{ `
export { default as Box } from "./box.vue";
` }}
</PreviewCode>
<SectionContent>
Update the import paths to match your project setup.
</SectionContent>
</div>
<div id="usage">
<SectionTitle>Usage</SectionTitle>
<PreviewCode>
{{ `
import { Box } from "@/components/ui/box";
` }}
</PreviewCode>
<PreviewCode>
{{ `
<Box class="w-70">
<CircleGauge class="size-7 stroke-1 fill-foreground/10" />
<div class="mt-6 text-2xl font-medium leading-8">
$724,091.47
</div>
<div class="mt-1.5 text-xs uppercase opacity-70">
Item Sales
</div>
</Box>
` }}
</PreviewCode>
</div>
<div id="variants">
<SectionTitle>Variants</SectionTitle>
<SectionContent>A collection of components you can use.</SectionContent>
<Preview>
<template #preview>
<Box class="w-70">
<CircleGauge class="size-7 stroke-1 fill-foreground/10" />
<div class="mt-6 text-2xl font-medium leading-8">
$724,091.47
</div>
<div class="mt-1.5 text-xs uppercase opacity-70">
Item Sales
</div>
</Box>
</template>
<template #code>
<PreviewCode>
{{ `
<Box class="w-70">
<CircleGauge class="size-7 stroke-1 fill-foreground/10" />
<div class="mt-6 text-2xl font-medium leading-8">
$724,091.47
</div>
<div class="mt-1.5 text-xs uppercase opacity-70">
Item Sales
</div>
</Box>
` }}
</PreviewCode>
</template>
</Preview>
<Preview>
<template #preview>
<Box raised="single" class="w-70">
<CircleGauge class="size-7 stroke-1 fill-foreground/10" />
<div class="mt-6 text-2xl font-medium leading-8">
$724,091.47
</div>
<div class="mt-1.5 text-xs uppercase opacity-70">
Item Sales
</div>
</Box>
</template>
<template #code>
<PreviewCode>
{{ `
<Box raised="single" class="w-70">
<CircleGauge class="size-7 stroke-1 fill-foreground/10" />
<div class="mt-6 text-2xl font-medium leading-8">
$724,091.47
</div>
<div class="mt-1.5 text-xs uppercase opacity-70">
Item Sales
</div>
</Box>
` }}
</PreviewCode>
</template>
</Preview>
<Preview>
<template #preview>
<Box raised="double" class="w-70">
<CircleGauge class="size-7 stroke-1 fill-foreground/10" />
<div class="mt-6 text-2xl font-medium leading-8">
$724,091.47
</div>
<div class="mt-1.5 text-xs uppercase opacity-70">
Item Sales
</div>
</Box>
</template>
<template #code>
<PreviewCode>
{{ `
<Box raised="double" class="w-70">
<CircleGauge class="size-7 stroke-1 fill-foreground/10" />
<div class="mt-6 text-2xl font-medium leading-8">
$724,091.47
</div>
<div class="mt-1.5 text-xs uppercase opacity-70">
Item Sales
</div>
</Box>
` }}
</PreviewCode>
</template>
</Preview>
</div>
</template>
-181
View File
@@ -1,181 +0,0 @@
<script lang="ts" setup>
import { Breadcrumb } from "@/components/ui/breadcrumb";
import {
Preview,
SectionTitle,
SectionContent,
PreviewCode,
} from "@/components/docs";
</script>
<template>
<div id="preview" class="-mt-20">
<Preview>
<template #preview>
<Breadcrumb :items="['Dashboard', 'Users', 'Admins', 'Settings', 'Edit Profile']" />
</template>
<template #code>
<PreviewCode>
{{ `
<Breadcrumb :items="['Dashboard', 'Users', 'Admins', 'Settings', 'Edit Profile']" />
` }}
</PreviewCode>
</template>
</Preview>
</div>
<div id="installation">
<SectionTitle>Installation</SectionTitle>
<SectionContent>
Copy and paste the following code into your project.
</SectionContent>
<PreviewCode title="components/ui/breadcrumb/Breadcrumb.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { ChevronRight, Ellipsis } from "@lucide/vue";
import {
MenuRoot,
MenuTrigger,
MenuPositioner,
MenuContent,
MenuItem,
} from "@/components/ui/menu";
import { BreadcrumbItem, BreadcrumbLink, BreadcrumbList } from ".";
const { class: className, ...props } = defineProps<{
class?: string;
items: string[];
}>();
</script>
<template>
<nav aria-label="breadcrumb" data-slot="breadcrumb" v-bind="{ ...props, ...$attrs }" :class="cn(className)">
<BreadcrumbList>
<template v-if="items.length <= 3">
<template v-for="(item, key) in items">
<BreadcrumbItem>
<BreadcrumbLink>\{\{ item \}\}</BreadcrumbLink>
</BreadcrumbItem>
<ChevronRight v-if="key < items.length - 1" />
</template>
</template>
<template v-else>
<BreadcrumbItem>
<BreadcrumbLink>\{\{ items[0] \}\}</BreadcrumbLink>
</BreadcrumbItem>
<ChevronRight />
<BreadcrumbItem>
<MenuRoot>
<MenuTrigger asChild>
<BreadcrumbLink>
<Ellipsis />
</BreadcrumbLink>
</MenuTrigger>
<MenuPositioner>
<MenuContent>
<MenuItem v-for="(item, itemKey) in items.slice(1, -2)" :value="item" :key="itemKey">
\{\{ item \}\}
</MenuItem>
</MenuContent>
</MenuPositioner>
</MenuRoot>
</BreadcrumbItem>
<ChevronRight />
<template v-for="(item, index) in items.slice(-2)">
<BreadcrumbItem>
<BreadcrumbLink>\{\{ item \}\}</BreadcrumbLink>
</BreadcrumbItem>
<ChevronRight v-if="index < 1" />
</template>
</template>
</BreadcrumbList>
</nav>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/breadcrumb/BreadcrumbItem.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { breadcrumbItem } from "@mykopkb/core/styles/breadcrumb.styles";
const { class: className, ...props } = defineProps<{
class?: string;
}>();
</script>
<template>
<li :class="cn(className, breadcrumbItem)" v-bind="{ ...props, ...$attrs }">
<slot />
</li>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/breadcrumb/BreadcrumbLink.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { breadcrumbLink } from "@mykopkb/core/styles/breadcrumb.styles";
const { class: className, ...props } = defineProps<{
class?: string;
}>();
</script>
<template>
<a :class="cn(className, breadcrumbLink)" v-bind="{ ...props, ...$attrs }">
<slot />
</a>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/breadcrumb/BreadcrumbList.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { breadcrumbList } from "@mykopkb/core/styles/breadcrumb.styles";
const { class: className, ...props } = defineProps<{
class?: string;
}>();
</script>
<template>
<ol :class="cn(className, breadcrumbList)" v-bind="{ ...props, ...$attrs }">
<slot />
</ol>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/breadcrumb/index.ts">
{{ `
export { default as Breadcrumb } from "./Breadcrumb.vue";
export { default as BreadcrumbItem } from "./BreadcrumbItem.vue";
export { default as BreadcrumbLink } from "./BreadcrumbLink.vue";
export { default as BreadcrumbList } from "./BreadcrumbList.vue";
` }}
</PreviewCode>
<SectionContent>
Update the import paths to match your project setup.
</SectionContent>
</div>
<div id="usage">
<SectionTitle>Usage</SectionTitle>
<PreviewCode>
{{ `
import { Breadcrumb } from "@/components/ui/breadcrumb";
` }}
</PreviewCode>
<PreviewCode>
{{ `
<Breadcrumb :items="[
'Dashboard',
'Users',
'Admins',
'Settings',
'Edit Profile',
]" />
` }}
</PreviewCode>
</div>
</template>
File diff suppressed because it is too large Load Diff
-445
View File
@@ -1,445 +0,0 @@
<script lang="ts" setup>
import {
CarouselRoot,
CarouselControl,
CarouselPrevTrigger,
CarouselNextTrigger,
CarouselIndicatorGroup,
CarouselIndicator,
CarouselItemGroup,
CarouselItem,
} from "@/components/ui/carousel";
import {
Preview,
SectionTitle,
SectionContent,
InstallPackage,
PreviewCode,
} from "@/components/docs";
const images = Array.from(
{ length: 5 },
(_, i) => `https://picsum.photos/seed/${i + 1}/500/300`
);
</script>
<template>
<div id="preview" class="-mt-20">
<Preview>
<template #preview>
<CarouselRoot :default-page="0" :slide-count="images.length" class="size-72">
<CarouselControl>
<CarouselPrevTrigger />
<CarouselNextTrigger />
</CarouselControl>
<CarouselIndicatorGroup>
<CarouselIndicator v-for="(_, index) in images" :key="index" :index="index" />
</CarouselIndicatorGroup>
<CarouselItemGroup>
<CarouselItem v-for="(image, index) in images" :key="index" :index="index"
class="text-5xl bold flex items-center justify-center">
{{ index + 1 }}
</CarouselItem>
</CarouselItemGroup>
</CarouselRoot>
</template>
<template #code>
<PreviewCode>
{{`
const images = Array.from(
{ length: 5 },
(_, i) => \`https://picsum.photos/seed/\${i + 1}/500/300\`
);
<CarouselRoot :default-page="0" :slide-count="images.length" class="size-72">
<CarouselControl>
<CarouselPrevTrigger />
<CarouselNextTrigger />
</CarouselControl>
<CarouselIndicatorGroup>
<CarouselIndicator v-for="(_, index) in images" :key="index" :index="index" />
</CarouselIndicatorGroup>
<CarouselItemGroup>
<CarouselItem v-for="(image, index) in images" :key="index" :index="index"
class="text-5xl bold flex items-center justify-center">
\{\{ index + 1 \}\}
</CarouselItem>
</CarouselItemGroup>
</CarouselRoot>
` }}
</PreviewCode>
</template>
</Preview>
</div>
<div id="installation">
<SectionTitle>Installation</SectionTitle>
<SectionContent>Install the following dependencies:</SectionContent>
<InstallPackage>add @zag-js/vue @zag-js/carousel</InstallPackage>
<SectionContent>
Copy and paste the following code into your project.
</SectionContent>
<PreviewCode title="components/ui/carousel/CarouselRoot.vue">
{{`
<script lang="ts" setup>
import * as carousel from "@zag-js/carousel";
import { useMachine, normalizeProps } from "@zag-js/vue";
import type { Props } from "@zag-js/carousel";
import { Slot } from "@/components/ui/slot";
import { cn } from "@mykopkb/core/utils/cn";
import { carouselRoot } from "@mykopkb/core/styles/carousel.styles";
import { computed, provide } from "vue";
const {
class: className,
defaultPage,
slideCount,
spacing = "2rem",
allowMouseDrag = true,
asChild = false,
...props
} = defineProps<Partial<Props> & { asChild?: boolean; class?: string }>();
const service = useMachine(carousel.machine, {
defaultPage,
slideCount,
spacing,
allowMouseDrag,
...props,
id: crypto.randomUUID(),
});
const api = computed(() => carousel.connect(service, normalizeProps));
provide("carouselApi", api);
</script>
<template>
<Slot :class="cn(carouselRoot, className)" v-bind="{ ...props, ...$attrs, ...api.getRootProps() }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/carousel/CarouselControl.vue">
{{ `
<script lang="ts" setup>
import { Slot } from "@/components/ui/slot";
import { cn } from "@mykopkb/core/utils/cn";
import { carouselControl } from "@mykopkb/core/styles/carousel.styles";
import type { Api } from "@zag-js/carousel";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("carouselApi");
</script>
<template>
<Slot :class="cn(carouselControl, className)" v-bind="{ ...props, ...$attrs, ...api?.getControlProps() }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/carousel/CarouselPrevTrigger.vue">
{{ `
<script lang="ts" setup>
import { Slot } from "@/components/ui/slot";
import { cn } from "@mykopkb/core/utils/cn";
import { Button } from "@/components/ui/button";
import { ArrowLeft } from "@lucide/vue";
import { carouselPrevTrigger } from "@mykopkb/core/styles/carousel.styles";
import type { Api } from "@zag-js/carousel";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("carouselApi");
</script>
<template>
<Slot v-bind="{ ...props, ...$attrs, ...api?.getPrevTriggerProps() }">
<slot v-if="asChild" />
<Button variant="ghost" v-else :class="cn(carouselPrevTrigger, className)">
<slot v-if="$slots.default" />
<ArrowLeft v-else />
</Button>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/carousel/CarouselNextTrigger.vue">
{{ `
<script lang="ts" setup>
import { Slot } from "@/components/ui/slot";
import { cn } from "@mykopkb/core/utils/cn";
import { Button } from "@/components/ui/button";
import { ArrowRight } from "@lucide/vue";
import { carouselNextTrigger } from "@mykopkb/core/styles/carousel.styles";
import type { Api } from "@zag-js/carousel";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("carouselApi");
</script>
<template>
<Slot v-bind="{ ...props, ...$attrs, ...api?.getNextTriggerProps() }">
<slot v-if="asChild" />
<Button variant="ghost" v-else :class="cn(carouselNextTrigger, className)">
<slot v-if="$slots.default" />
<ArrowRight v-else />
</Button>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/carousel/CarouselIndicatorGroup.vue">
{{ `
<script lang="ts" setup>
import { Slot } from "@/components/ui/slot";
import { cn } from "@mykopkb/core/utils/cn";
import { carouselIndicatorGroup } from "@mykopkb/core/styles/carousel.styles";
import type { Api } from "@zag-js/carousel";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("carouselApi");
</script>
<template>
<Slot :class="cn(carouselIndicatorGroup, className)"
v-bind="{ ...props, ...$attrs, ...api?.getIndicatorGroupProps() }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/carousel/CarouselIndicator.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { carouselIndicator } from "@mykopkb/core/styles/carousel.styles";
import type { Api, IndicatorProps } from "@zag-js/carousel";
import { inject } from "vue";
const {
class: className,
asChild = false,
index,
...props
} = defineProps<
{
class?: string;
asChild?: boolean;
} & IndicatorProps
>();
const api = inject<Api>("carouselApi");
</script>
<template>
<button :class="cn(carouselIndicator, className)"
v-bind="{ ...props, ...$attrs, ...api?.getIndicatorProps({ index }) }" />
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/carousel/CarouselItemGroup.vue">
{{ `
<script lang="ts" setup>
import { Slot } from "@/components/ui/slot";
import { cn } from "@mykopkb/core/utils/cn";
import { carouselItemGroup } from "@mykopkb/core/styles/carousel.styles";
import type { Api } from "@zag-js/carousel";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("carouselApi");
</script>
<template>
<Slot :class="cn(carouselItemGroup, className)" v-bind="{ ...props, ...$attrs, ...api?.getItemGroupProps() }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/carousel/CarouselItem.vue">
{{ `
<script lang="ts" setup>
import { Slot } from "@/components/ui/slot";
import { cn } from "@mykopkb/core/utils/cn";
import { Box } from "@/components/ui/box";
import { carouselItem } from "@mykopkb/core/styles/carousel.styles";
import type { Api, ItemProps } from "@zag-js/carousel";
import { inject } from "vue";
const {
class: className,
asChild = false,
index,
...props
} = defineProps<
{
class?: string;
asChild?: boolean;
} & ItemProps
>();
const api = inject<Api>("carouselApi");
</script>
<template>
<Slot v-bind="{ ...props, ...$attrs, ...api?.getItemProps({ index }) }">
<slot v-if="asChild" />
<Box v-else :class="cn(carouselItem, className)">
<slot />
</Box>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/carousel/index.ts">
{{ `
export { default as CarouselRoot } from "./CarouselRoot.vue";
export { default as CarouselControl } from "./CarouselControl.vue";
export { default as CarouselPrevTrigger } from "./CarouselPrevTrigger.vue";
export { default as CarouselNextTrigger } from "./CarouselNextTrigger.vue";
export { default as CarouselIndicatorGroup } from "./CarouselIndicatorGroup.vue";
export { default as CarouselIndicator } from "./CarouselIndicator.vue";
export { default as CarouselItemGroup } from "./CarouselItemGroup.vue";
export { default as CarouselItem } from "./CarouselItem.vue";
` }}
</PreviewCode>
<SectionContent>
Update the import paths to match your project setup.
</SectionContent>
</div>
<div id="usage">
<SectionTitle>Usage</SectionTitle>
<PreviewCode>
{{ `
import {
CarouselRoot,
CarouselControl,
CarouselPrevTrigger,
CarouselNextTrigger,
CarouselIndicatorGroup,
CarouselIndicator,
CarouselItemGroup,
CarouselItem,
} from "@/components/ui/carousel";
` }}
</PreviewCode>
<PreviewCode>
{{ `
<CarouselRoot :default-page="0" :slide-count="images.length" class="size-72">
<CarouselControl>
<CarouselPrevTrigger />
<CarouselNextTrigger />
</CarouselControl>
<CarouselIndicatorGroup>
<CarouselIndicator v-for="(_, index) in images" :key="index" :index="index" />
</CarouselIndicatorGroup>
<CarouselItemGroup>
<CarouselItem v-for="(image, index) in images" :key="index" :index="index"
class="text-5xl bold flex items-center justify-center">
\{\{ index + 1 \}\}
</CarouselItem>
</CarouselItemGroup>
</CarouselRoot>
` }}
</PreviewCode>
</div>
<div id="variants">
<SectionTitle>Variants</SectionTitle>
<SectionContent>A collection of components you can use.</SectionContent>
<Preview>
<template #preview>
<CarouselRoot :default-page="0" :slide-count="images.length" class="size-72">
<CarouselControl>
<CarouselPrevTrigger />
<CarouselNextTrigger />
</CarouselControl>
<CarouselIndicatorGroup>
<CarouselIndicator v-for="(_, index) in images" :key="index" :index="index" />
</CarouselIndicatorGroup>
<CarouselItemGroup>
<CarouselItem v-for="(image, index) in images" :key="index" :index="index">
<img :src="image" :alt="`Slide $\{index\}`" />
</CarouselItem>
</CarouselItemGroup>
</CarouselRoot>
</template>
<template #code>
<PreviewCode>
{{ `
<CarouselRoot :default-page="0" :slide-count="images.length" class="size-72">
<CarouselControl>
<CarouselPrevTrigger />
<CarouselNextTrigger />
</CarouselControl>
<CarouselIndicatorGroup>
<CarouselIndicator v-for="(_, index) in images" :key="index" :index="index" />
</CarouselIndicatorGroup>
<CarouselItemGroup>
<CarouselItem v-for="(image, index) in images" :key="index" :index="index">
<img :src="image" :alt="\`Slide $\{index\}\`" />
</CarouselItem>
</CarouselItemGroup>
</CarouselRoot>
` }}
</PreviewCode>
</template>
</Preview>
</div>
</template>
-235
View File
@@ -1,235 +0,0 @@
<script lang="ts" setup>
import { Chart, getColor } from "@/components/ui/chart";
import {
Preview,
SectionTitle,
SectionContent,
InstallPackage,
PreviewCode,
} from "@/components/docs";
</script>
<template>
<div id="preview" class="-mt-20">
<Preview>
<template #preview>
<Chart class="max-w-100" :config="{
type: 'bar',
data: {
labels: [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
],
datasets: [
{
label: 'Html Template',
maxBarThickness: 12,
data: [
60, 150, 30, 200, 180, 50, 180, 120, 230, 180, 250, 270,
],
backgroundColor: () => getColor('--color-foreground', 0.3),
borderColor: () => getColor('--color-foreground'),
borderWidth: 1,
},
],
},
options: {
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
},
},
scales: {
x: {
display: false,
},
y: {
display: false,
},
},
},
}" />
</template>
<template #code>
<PreviewCode>
{{`
<Chart class="max-w-100" :config="{
type: 'bar',
data: {
labels: [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
],
datasets: [
{
label: 'Html Template',
maxBarThickness: 12,
data: [
60, 150, 30, 200, 180, 50, 180, 120, 230, 180, 250, 270,
],
backgroundColor: () => getColor('--color-foreground', 0.3),
borderColor: () => getColor('--color-foreground'),
borderWidth: 1,
},
],
},
options: {
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
},
},
scales: {
x: {
display: false,
},
y: {
display: false,
},
},
},
}" />
` }}
</PreviewCode>
</template>
</Preview>
</div>
<div id="installation">
<SectionTitle>Installation</SectionTitle>
<SectionContent>Install the following dependencies:</SectionContent>
<InstallPackage>add chart.js</InstallPackage>
<SectionContent>
Copy and paste the following code into your project.
</SectionContent>
<PreviewCode title="components/ui/chart/Chart.vue">
{{`
<script lang="ts" setup generic="TType extends ChartType">
import ChartJs from "chart.js/auto";
import { ref, onMounted } from "vue";
import { cn } from "@mykopkb/core/utils/cn";
import { chart } from "@mykopkb/core/styles/chart.styles";
import type { ChartType, ChartConfiguration } from "chart.js";
const {
class: className,
config,
getRef,
...props
} = defineProps<{
class?: string;
config: ChartConfiguration<TType>;
getRef?: (chart: ChartJs<TType>) => void;
}>();
const chartRef = ref<
| (HTMLCanvasElement & {
instance?: ChartJs<TType>;
})
| null
>(null);
onMounted(() => {
if (chartRef.value && !chartRef.value.instance) {
chartRef.value.instance = new ChartJs(chartRef.value, config);
getRef?.(chartRef.value.instance);
}
});
</script>
<template>
<canvas :class="cn(chart, className)" ref="chartRef" v-bind="props" />
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/chart/index.ts">
{{ `
export { default as Chart } from "./Chart.vue";
export { getColor } from "./utils";
` }}
</PreviewCode>
<SectionContent>
Update the import paths to match your project setup.
</SectionContent>
</div>
<div id="usage">
<SectionTitle>Usage</SectionTitle>
<PreviewCode>
{{ `
import { Chart, getColor } from "@/components/ui/chart";
` }}
</PreviewCode>
<PreviewCode>
{{`
<Chart class="max-w-100" :config="{
type: 'bar',
data: {
labels: [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
],
datasets: [
{
label: 'Html Template',
maxBarThickness: 12,
data: [
60, 150, 30, 200, 180, 50, 180, 120, 230, 180, 250, 270,
],
backgroundColor: () => getColor('--color-foreground', 0.3),
borderColor: () => getColor('--color-foreground'),
borderWidth: 1,
},
],
},
options: {
maintainAspectRatio: false,
plugins: {
legend: {
display: false,
},
},
scales: {
x: {
display: false,
},
y: {
display: false,
},
},
},
}" />
` }}
</PreviewCode>
</div>
</template>
-235
View File
@@ -1,235 +0,0 @@
<script lang="ts" setup>
import {
CheckboxRoot,
CheckboxLabel,
CheckboxControl,
} from "@/components/ui/checkbox";
import {
Preview,
SectionTitle,
SectionContent,
InstallPackage,
PreviewCode,
} from "@/components/docs";
</script>
<template>
<div id="preview" class="-mt-20">
<Preview>
<template #preview>
<CheckboxRoot>
<CheckboxControl />
<CheckboxLabel>Accept terms and conditions</CheckboxLabel>
</CheckboxRoot>
</template>
<template #code>
<PreviewCode>
{{ `
<CheckboxRoot>
<CheckboxControl />
<CheckboxLabel>Accept terms and conditions</CheckboxLabel>
</CheckboxRoot>
` }}
</PreviewCode>
</template>
</Preview>
</div>
<div id="installation">
<SectionTitle>Installation</SectionTitle>
<SectionContent>Install the following dependencies:</SectionContent>
<InstallPackage>add @zag-js/vue @zag-js/checkbox</InstallPackage>
<SectionContent>
Copy and paste the following code into your project.
</SectionContent>
<PreviewCode title="components/ui/checkbox/CheckboxRoot.vue">
{{`
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { checkboxRoot } from "@mykopkb/core/styles/checkbox.styles";
import * as checkbox from "@zag-js/checkbox";
import { useMachine, normalizeProps } from "@zag-js/vue";
import type { Props } from "@zag-js/checkbox";
import { CheckboxHiddenInput } from ".";
import { computed, provide } from "vue";
const {
class: className,
checked = undefined,
...props
} = defineProps<Partial<Props> & { class?: string }>();
const service = useMachine(
checkbox.machine,
computed(() => ({
...props,
checked,
id: crypto.randomUUID(),
}))
);
const api = computed(() => checkbox.connect(service, normalizeProps));
provide("checkboxApi", api);
</script>
<template>
<label :class="cn(checkboxRoot, className)" v-bind="{ ...props, ...$attrs, ...api.getRootProps() }">
<slot />
<CheckboxHiddenInput />
</label>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/checkbox/CheckboxLabel.vue">
{{ `
<script lang="ts" setup>
import { Slot } from "@/components/ui/slot";
import { cn } from "@mykopkb/core/utils/cn";
import type { Api } from "@zag-js/checkbox";
import { checkboxLabel } from "@mykopkb/core/styles/checkbox.styles";
import { label } from "@mykopkb/core/styles/label.styles";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
asChild?: boolean;
class?: string;
}>();
const api = inject<Api>("checkboxApi");
</script>
<template>
<Slot :class="cn([label, checkboxLabel, className])" v-bind="{ ...props, ...$attrs, ...api?.getLabelProps() }">
<slot v-if="asChild" />
<span v-else>
<slot />
</span>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/checkbox/CheckboxControl.vue">
{{ `
<script lang="ts" setup>
import { Slot } from "@/components/ui/slot";
import { CheckIcon } from "@lucide/vue";
import { cn } from "@mykopkb/core/utils/cn";
import { checkboxControl } from "@mykopkb/core/styles/checkbox.styles";
import { CheckboxIndicator } from ".";
import type { Api } from "@zag-js/checkbox";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
asChild?: boolean;
class?: string;
}>();
const api = inject<Api>("checkboxApi");
</script>
<template>
<Slot :class="cn(checkboxControl, className)" v-bind="{ ...props, ...$attrs, ...api?.getControlProps() }">
<slot v-if="asChild" />
<div v-else>
<CheckboxIndicator>
<CheckIcon />
</CheckboxIndicator>
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/checkbox/CheckboxIndicator.vue">
{{ `
<script lang="ts" setup>
import { Slot } from "@/components/ui/slot";
import type { Api } from "@zag-js/checkbox";
import { cn } from "@mykopkb/core/utils/cn";
import { checkboxIndicator } from "@mykopkb/core/styles/checkbox.styles";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
asChild?: boolean;
class?: string;
}>();
const api = inject<Api>("checkboxApi");
</script>
<template>
<Slot :class="cn(checkboxIndicator, className)" v-bind="{ ...props, ...$attrs, ...api?.getIndicatorProps() }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/checkbox/CheckboxHiddenInput.vue">
{{ `
<script lang="ts" setup>
import type { Api } from "@zag-js/checkbox";
import { cn } from "@mykopkb/core/utils/cn";
import { checkboxHiddenInput } from "@mykopkb/core/styles/checkbox.styles";
import { inject } from "vue";
const { class: className, ...props } = defineProps<{
class?: string;
}>();
const api = inject<Api>("checkboxApi");
</script>
<template>
<input :class="cn(checkboxHiddenInput, className)"
v-bind="{ ...props, ...$attrs, ...api?.getHiddenInputProps() }" />
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/checkbox/index.ts">
{{ `
export { default as CheckboxRoot } from "./CheckboxRoot.vue";
export { default as CheckboxLabel } from "./CheckboxLabel.vue";
export { default as CheckboxControl } from "./CheckboxControl.vue";
export { default as CheckboxIndicator } from "./CheckboxIndicator.vue";
export { default as CheckboxHiddenInput } from "./CheckboxHiddenInput.vue";
` }}
</PreviewCode>
<SectionContent>
Update the import paths to match your project setup.
</SectionContent>
</div>
<div id="usage">
<SectionTitle>Usage</SectionTitle>
<PreviewCode>
{{ `
import {
CheckboxRoot,
CheckboxLabel,
CheckboxControl,
} from "@/components/ui/checkbox";
` }}
</PreviewCode>
<PreviewCode>
{{ `
<CheckboxRoot>
<CheckboxControl />
<CheckboxLabel>Accept terms and conditions</CheckboxLabel>
</CheckboxRoot>
` }}
</PreviewCode>
</div>
</template>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-560
View File
@@ -1,560 +0,0 @@
<script lang="ts" setup>
import {
DialogRoot,
DialogTrigger,
DialogContent,
DialogTitle,
DialogDescription,
DialogCloseTrigger,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { SquareX, Save, ExternalLink } from "@lucide/vue";
import {
Preview,
SectionTitle,
SectionContent,
InstallPackage,
PreviewCode,
} from "@/components/docs";
import { ref } from "vue";
const dialog = ref(false);
</script>
<template>
<div id="preview" class="-mt-20">
<Preview>
<template #preview>
<DialogRoot>
<DialogTrigger>Open Dialog</DialogTrigger>
<DialogContent>
<DialogTitle>Dialog Title</DialogTitle>
<DialogDescription>
Make changes to your profile here. Click save when you're done.
</DialogDescription>
<div class="grid gap-4 mt-2">
<div class="grid gap-2.5">
<Label for="name-1">Name</Label>
<Input id="name-1" name="name" defaultValue="Pedro Duarte" />
</div>
<div class="grid gap-2.5">
<Label for="username-1">Username</Label>
<Input id="username-1" name="username" defaultValue="@peduarte" />
</div>
</div>
<div class="flex gap-2 justify-end mt-7">
<DialogCloseTrigger>
<SquareX />
Close
</DialogCloseTrigger>
<Button variant="primary">
<Save />
Submit
</Button>
</div>
<DialogCloseTrigger />
</DialogContent>
</DialogRoot>
</template>
<template #code>
<PreviewCode>
{{ `
<DialogRoot>
<DialogTrigger>Open Dialog</DialogTrigger>
<DialogContent>
<DialogTitle>Dialog Title</DialogTitle>
<DialogDescription>
Make changes to your profile here. Click save when you're done.
</DialogDescription>
<div class="grid gap-4 mt-2">
<div class="grid gap-2.5">
<Label for="name-1">Name</Label>
<Input id="name-1" name="name" defaultValue="Pedro Duarte" />
</div>
<div class="grid gap-2.5">
<Label for="username-1">Username</Label>
<Input id="username-1" name="username" defaultValue="@peduarte" />
</div>
</div>
<div class="flex gap-2 justify-end mt-7">
<DialogCloseTrigger>
<SquareX />
Close
</DialogCloseTrigger>
<Button variant="primary">
<Save />
Submit
</Button>
</div>
<DialogCloseTrigger />
</DialogContent>
</DialogRoot>
` }}
</PreviewCode>
</template>
</Preview>
</div>
<div id="installation">
<SectionTitle>Installation</SectionTitle>
<SectionContent>Install the following dependencies:</SectionContent>
<InstallPackage>add @zag-js/vue @zag-js/dialog</InstallPackage>
<SectionContent>
Copy and paste the following code into your project.
</SectionContent>
<PreviewCode title="components/ui/dialog/DialogRoot.vue">
{{`
<script lang="ts" setup>
import { provide, computed } from "vue";
import * as dialog from "@zag-js/dialog";
import type { Props } from "@zag-js/dialog";
import { useMachine, normalizeProps } from "@zag-js/vue";
const {
class: className,
asChild = false,
open = undefined,
closeOnInteractOutside = undefined,
...props
} = defineProps<
Partial<Props> & {
class?: string;
asChild?: boolean;
}
>();
const service = useMachine(dialog.machine, {
...props,
get open() {
return open;
},
closeOnInteractOutside,
id: crypto.randomUUID(),
});
const api = computed(() => dialog.connect(service, normalizeProps));
provide("dialogApi", api);
</script>
<template>
<slot />
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/dialog/DialogTrigger.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { dialogTrigger } from "@mykopkb/core/styles/dialog.styles";
import { Button } from "@/components/ui/button";
import type { Api } from "@zag-js/dialog";
import { inject } from "vue";
import { Slot } from "@/components/ui/slot";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("dialogApi");
</script>
<template>
<Slot v-bind="{ ...props, ...$attrs, ...api?.getTriggerProps() }">
<Button variant="secondary" look="outline" v-if="!asChild" :class="cn(dialogTrigger, className)">
<slot />
</Button>
<slot v-else />
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/dialog/DialogBackdrop.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { dialogBackdrop } from "@mykopkb/core/styles/dialog.styles";
import type { Api } from "@zag-js/dialog";
import { inject } from "vue";
import { Slot } from "@/components/ui/slot";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("dialogApi");
</script>
<template>
<Slot :class="cn(dialogBackdrop, className)" v-bind="{ ...props, ...$attrs, ...api?.getBackdropProps() }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/dialog/DialogPositioner.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { dialogPositioner } from "@mykopkb/core/styles/dialog.styles";
import type { Api } from "@zag-js/dialog";
import { inject } from "vue";
import { Slot } from "@/components/ui/slot";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("dialogApi");
</script>
<template>
<Slot :class="cn(dialogPositioner, className)" v-bind="{ ...props, ...$attrs, ...api?.getPositionerProps() }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/dialog/DialogContent.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { dialogContent } from "@mykopkb/core/styles/dialog.styles";
import { Box } from "@/components/ui/box";
import { DialogBackdrop, DialogPositioner } from "@/components/ui/dialog";
import type { Api } from "@zag-js/dialog";
import { inject } from "vue";
import { Slot } from "@/components/ui/slot";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("dialogApi");
</script>
<template>
<Teleport to="body">
<DialogBackdrop />
<DialogPositioner>
<Slot v-bind="{ ...props, ...$attrs, ...api?.getContentProps() }">
<slot v-if="asChild" />
<div v-else>
<Box raised="double" :class="cn(dialogContent, className)" v-bind="{ ...props }">
<div>
<slot />
</div>
</Box>
</div>
</Slot>
</DialogPositioner>
</Teleport>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/dialog/DialogTitle.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { dialogTitle } from "@mykopkb/core/styles/dialog.styles";
import type { Api } from "@zag-js/dialog";
import { inject } from "vue";
import { Slot } from "@/components/ui/slot";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("dialogApi");
</script>
<template>
<Slot :class="cn(dialogTitle, className)" v-bind="{ ...props, ...$attrs, ...api?.getTitleProps() }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/dialog/DialogDescription.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { dialogDescription } from "@mykopkb/core/styles/dialog.styles";
import type { Api } from "@zag-js/dialog";
import { inject } from "vue";
import { Slot } from "@/components/ui/slot";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("dialogApi");
</script>
<template>
<Slot :class="cn(dialogDescription, className)" v-bind="{ ...props, ...$attrs, ...api?.getDescriptionProps() }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/dialog/DialogCloseTrigger.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { dialogCloseTrigger } from "@mykopkb/core/styles/dialog.styles";
import { Button } from "@/components/ui/button";
import type { Api } from "@zag-js/dialog";
import {
buttonVariants,
type ButtonVariants,
} from "@mykopkb/core/styles/button.styles";
import { X } from "@lucide/vue";
import { inject } from "vue";
import { Slot } from "@/components/ui/slot";
const {
class: className,
look = "outline",
variant = "secondary",
size,
asChild = false,
...props
} = defineProps<
ButtonVariants & {
class?: string;
asChild?: boolean;
}
>();
const api = inject<Api>("dialogApi");
</script>
<template>
<Slot v-bind="{ ...props, ...$attrs, ...api?.getCloseTriggerProps() }">
<Button variant="ghost" v-if="!$slots.default" :class="cn(dialogCloseTrigger, className)"
v-bind="{ ...props }">
<X class="size-4" />
</Button>
<template v-else>
<slot v-if="asChild" />
<Button v-else :class="
cn(buttonVariants({ look, variant, size, className }), className)
">
<slot />
</Button>
</template>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/dialog/index.ts">
{{ `
export { default as DialogRoot } from "./DialogRoot.vue";
export { default as DialogTrigger } from "./DialogTrigger.vue";
export { default as DialogBackdrop } from "./DialogBackdrop.vue";
export { default as DialogPositioner } from "./DialogPositioner.vue";
export { default as DialogContent } from "./DialogContent.vue";
export { default as DialogTitle } from "./DialogTitle.vue";
export { default as DialogDescription } from "./DialogDescription.vue";
export { default as DialogCloseTrigger } from "./DialogCloseTrigger.vue";
` }}
</PreviewCode>
<SectionContent>
Update the import paths to match your project setup.
</SectionContent>
</div>
<div id="usage">
<SectionTitle>Usage</SectionTitle>
<PreviewCode>
{{ `
import {
DialogRoot,
DialogTrigger,
DialogContent,
DialogTitle,
DialogDescription,
DialogCloseTrigger,
} from "@/components/ui/dialog";
` }}
</PreviewCode>
<PreviewCode>
{{ `
<DialogRoot>
<DialogTrigger>Open Dialog</DialogTrigger>
<DialogContent>
<DialogTitle>Dialog Title</DialogTitle>
<DialogDescription>
Make changes to your profile here. Click save when you're done.
</DialogDescription>
<div class="grid gap-4 mt-2">
<div class="grid gap-2.5">
<Label for="name-1">Name</Label>
<Input id="name-1" name="name" defaultValue="Pedro Duarte" />
</div>
<div class="grid gap-2.5">
<Label for="username-1">Username</Label>
<Input id="username-1" name="username" defaultValue="@peduarte" />
</div>
</div>
<div class="flex gap-2 justify-end mt-7">
<DialogCloseTrigger>
<SquareX />
Close
</DialogCloseTrigger>
<Button variant="primary">
<Save />
Submit
</Button>
</div>
<DialogCloseTrigger />
</DialogContent>
</DialogRoot>
` }}
</PreviewCode>
</div>
<div id="variants">
<SectionTitle>Variants</SectionTitle>
<SectionContent>A collection of components you can use.</SectionContent>
<Preview>
<template #preview>
<DialogRoot>
<DialogTrigger>Custom Close</DialogTrigger>
<DialogContent>
<DialogTitle>Share Link</DialogTitle>
<DialogDescription>
Anyone who has this link will be able to view this.
</DialogDescription>
<div class="grid gap-4 mt-2">
<Input id="name-1" name="name" defaultValue="https://midone-ui.com/docs/installation" />
</div>
<div class="flex gap-2 mt-5">
<DialogCloseTrigger>
<ExternalLink />
Share Link
</DialogCloseTrigger>
</div>
</DialogContent>
</DialogRoot>
</template>
<template #code>
<PreviewCode>
{{ `
<DialogRoot>
<DialogTrigger>Custom Close</DialogTrigger>
<DialogContent>
<DialogTitle>Share Link</DialogTitle>
<DialogDescription>
Anyone who has this link will be able to view this.
</DialogDescription>
<div class="grid gap-4 mt-2">
<Input id="name-1" name="name" defaultValue="https://midone-ui.com/docs/installation" />
</div>
<div class="flex gap-2 mt-5">
<DialogCloseTrigger>
<ExternalLink />
Share Link
</DialogCloseTrigger>
</div>
</DialogContent>
</DialogRoot>
` }}
</PreviewCode>
</template>
</Preview>
<Preview>
<template #preview>
<Button look="outline" variant="secondary" @click.prevent="dialog = true">
Programmatic Trigger
</Button>
<DialogRoot :open="dialog" @openChange="(details) => (dialog = details.open)">
<DialogContent>
<DialogTitle>Share Link</DialogTitle>
<DialogDescription>
Anyone who has this link will be able to view this.
</DialogDescription>
<div class="grid gap-4 mt-2">
<Input id="name-1" name="name" defaultValue="https://midone-ui.com/docs/installation" />
</div>
<div class="flex gap-2 mt-5">
<DialogCloseTrigger>
<ExternalLink />
Share Link
</DialogCloseTrigger>
</div>
</DialogContent>
</DialogRoot>
</template>
<template #code>
<PreviewCode>
{{`
const dialog = ref(false);
<Button look="outline" variant="secondary" @click.prevent="dialog = true">
Programmatic Trigger
</Button>
<DialogRoot :open="dialog" @openChange="(details) => (dialog = details.open)">
<DialogContent>
<DialogTitle>Share Link</DialogTitle>
<DialogDescription>
Anyone who has this link will be able to view this.
</DialogDescription>
<div class="grid gap-4 mt-2">
<Input id="name-1" name="name" defaultValue="https://midone-ui.com/docs/installation" />
</div>
<div class="flex gap-2 mt-5">
<DialogCloseTrigger>
<ExternalLink />
Share Link
</DialogCloseTrigger>
</div>
</DialogContent>
</DialogRoot>
` }}
</PreviewCode>
</template>
</Preview>
</div>
</template>
-484
View File
@@ -1,484 +0,0 @@
<script lang="ts" setup>
import { Button } from "@/components/ui/button";
import {
CheckboxRoot,
CheckboxLabel,
CheckboxControl,
} from "@/components/ui/checkbox";
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
FieldTitle,
FieldContent,
} from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import {
SelectRoot,
SelectControl,
SelectTrigger,
SelectValueText,
SelectContent,
SelectItemGroup,
SelectItemGroupLabel,
SelectItem,
SelectItemText,
} from "@/components/ui/select";
import * as select from "@zag-js/select";
import { Textarea } from "@/components/ui/textarea";
import {
RadioGroupRoot,
RadioGroupItem,
RadioGroupItemControl,
} from "@/components/ui/radio-group";
import {
Preview,
SectionTitle,
SectionContent,
PreviewCode,
} from "@/components/docs";
</script>
<template>
<div id="preview" class="-mt-20">
<Preview>
<template #preview>
<div class="w-full max-w-md">
<form>
<FieldGroup>
<FieldSet>
<FieldLegend>Payment Method</FieldLegend>
<FieldDescription>
All transactions are secure and encrypted
</FieldDescription>
<FieldGroup>
<Field>
<FieldLabel for="card-name">Name on Card</FieldLabel>
<Input id="card-name" placeholder="Evil Rabbit" required />
</Field>
<Field>
<FieldLabel for="card-number">Card Number</FieldLabel>
<Input id="card-number" placeholder="1234 5678 9012 3456" required />
<FieldDescription>Enter your 16-digit card number</FieldDescription>
</Field>
</FieldGroup>
</FieldSet>
<FieldSeparator />
<FieldSet>
<FieldLegend>Billing Address</FieldLegend>
<FieldDescription>
The billing address associated with your payment method
</FieldDescription>
<CheckboxRoot>
<CheckboxControl />
<CheckboxLabel class="font-normal">
Same as shipping address
</CheckboxLabel>
</CheckboxRoot>
</FieldSet>
<Field orientation="horizontal">
<Button look="outline" type="submit">Submit</Button>
<Button type="button">Cancel</Button>
</Field>
</FieldGroup>
</form>
</div>
</template>
<template #code>
<PreviewCode>
{{ `
<div class="w-full max-w-md">
<form>
<FieldGroup>
<FieldSet>
<FieldLegend>Payment Method</FieldLegend>
<FieldDescription>
All transactions are secure and encrypted
</FieldDescription>
<FieldGroup>
<Field>
<FieldLabel for="card-name">Name on Card</FieldLabel>
<Input id="card-name" placeholder="Evil Rabbit" required />
</Field>
<Field>
<FieldLabel for="card-number">Card Number</FieldLabel>
<Input id="card-number" placeholder="1234 5678 9012 3456" required />
<FieldDescription>Enter your 16-digit card number</FieldDescription>
</Field>
</FieldGroup>
</FieldSet>
<FieldSeparator />
<FieldSet>
<FieldLegend>Billing Address</FieldLegend>
<FieldDescription>
The billing address associated with your payment method
</FieldDescription>
<CheckboxRoot>
<CheckboxControl />
<CheckboxLabel class="font-normal">
Same as shipping address
</CheckboxLabel>
</CheckboxRoot>
</FieldSet>
<Field orientation="horizontal">
<Button look="outline" type="submit">Submit</Button>
<Button type="button">Cancel</Button>
</Field>
</FieldGroup>
</form>
</div>
` }}
</PreviewCode>
</template>
</Preview>
</div>
<div id="installation">
<SectionTitle>Installation</SectionTitle>
<SectionContent>
Copy and paste the following code into your project.
</SectionContent>
<PreviewCode title="components/ui/field/Field.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import {
fieldVariants,
type FieldVariants,
} from "@mykopkb/core/styles/field.styles";
const {
class: className,
orientation = "vertical",
...props
} = defineProps<
FieldVariants & {
class?: string;
}
>();
</script>
<template>
<div role="group" data-part="field" :data-orientation="orientation"
:class="cn(fieldVariants({ orientation }), className)" v-bind="{ ...props }">
<slot />
</div>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/field/FieldContent.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { fieldContent } from "@mykopkb/core/styles/field.styles";
const { class: className, ...props } = defineProps<{
class?: string;
}>();
</script>
<template>
<div data-part="field-content" :class="cn(fieldContent, className)" v-bind="{ ...props }">
<slot />
</div>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/field/FieldDescription.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { fieldDescription } from "@mykopkb/core/styles/field.styles";
const { class: className, ...props } = defineProps<{
class?: string;
}>();
</script>
<template>
<p data-part="field-description" :class="cn(fieldDescription, className)" v-bind="{ ...props }">
<slot />
</p>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/field/FieldLabel.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { Label } from "@/components/ui/label";
import { fieldLabel } from "@mykopkb/core/styles/field.styles";
const { class: className, ...props } = defineProps<{
class?: string;
}>();
</script>
<template>
<Label data-part="field-label" :class="cn(fieldLabel, className)" v-bind="{ ...props }">
<slot />
</Label>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/field/FieldSeparator.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { fieldSeparator } from "@mykopkb/core/styles/field.styles";
import { Separator } from "@/components/ui/separator";
const { class: className, ...props } = defineProps<{
class?: string;
}>();
</script>
<template>
<div data-part="field-separator" :data-content="!!$slots.default" :class="cn(fieldSeparator, className)"
v-bind="{ ...props }">
<Separator class="absolute inset-0 top-1/2" />
<span v-if="$slots.default" data-part="field-separator-content">
<slot />
</span>
</div>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/field/index.ts">
{{ `
export { default as Field } from "./Field.vue";
export { default as FieldContent } from "./FieldContent.vue";
export { default as FieldDescription } from "./FieldDescription.vue";
export { default as FieldError } from "./FieldError.vue";
export { default as FieldGroup } from "./FieldGroup.vue";
export { default as FieldLabel } from "./FieldLabel.vue";
export { default as FieldLegend } from "./FieldLegend.vue";
export { default as FieldSeparator } from "./FieldSeparator.vue";
export { default as FieldSet } from "./FieldSet.vue";
export { default as FieldTitle } from "./FieldTitle.vue";
` }}
</PreviewCode>
<SectionContent>
Update the import paths to match your project setup.
</SectionContent>
</div>
<div id="usage">
<SectionTitle>Usage</SectionTitle>
<PreviewCode>
{{ `
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
FieldTitle,
} from "@/components/ui/field";
` }}
</PreviewCode>
<PreviewCode>
{{ `
<Field>
<FieldLabel for="email">Email</FieldLabel>
<Input id="email" placeholder="Enter your email" />
<FieldDescription>We'll never share your email.</FieldDescription>
</Field>
` }}
</PreviewCode>
</div>
<div id="variants">
<SectionTitle>Variants</SectionTitle>
<SectionContent>A collection of components you can use.</SectionContent>
<Preview>
<template #preview>
<form>
<FieldGroup>
<FieldSet>
<FieldLegend>Payment Method</FieldLegend>
<FieldDescription>
All transactions are secure and encrypted
</FieldDescription>
<FieldGroup>
<Field>
<FieldLabel for="card-name">Name on Card</FieldLabel>
<Input id="card-name" placeholder="Evil Rabbit" required />
</Field>
<Field>
<FieldLabel for="card-number">Card Number</FieldLabel>
<Input id="card-number" placeholder="1234 5678 9012 3456" required />
<FieldDescription>Enter your 16-digit card number</FieldDescription>
</Field>
</FieldGroup>
</FieldSet>
<FieldSeparator />
<FieldSet>
<FieldLegend>Billing Address</FieldLegend>
<FieldDescription>
The billing address associated with your payment method
</FieldDescription>
<CheckboxRoot>
<CheckboxControl />
<CheckboxLabel class="font-normal">
Same as shipping address
</CheckboxLabel>
</CheckboxRoot>
</FieldSet>
<FieldSet>
<FieldGroup>
<Field>
<FieldLabel for="comments">Comments</FieldLabel>
<Textarea id="comments" placeholder="Add any additional comments" class="resize-none" />
</Field>
</FieldGroup>
</FieldSet>
<Field orientation="horizontal">
<Button look="outline" type="submit">Submit</Button>
<Button type="button">Cancel</Button>
</Field>
</FieldGroup>
</form>
</template>
<template #code>
<PreviewCode>
{{ `
<form>
<FieldGroup>
<FieldSet>
<FieldLegend>Payment Method</FieldLegend>
<FieldDescription>
All transactions are secure and encrypted
</FieldDescription>
<FieldGroup>
<Field>
<FieldLabel for="card-name">Name on Card</FieldLabel>
<Input id="card-name" placeholder="Evil Rabbit" required />
</Field>
<Field>
<FieldLabel for="card-number">Card Number</FieldLabel>
<Input id="card-number" placeholder="1234 5678 9012 3456" required />
<FieldDescription>Enter your 16-digit card number</FieldDescription>
</Field>
</FieldGroup>
</FieldSet>
<FieldSeparator />
<FieldSet>
<FieldLegend>Billing Address</FieldLegend>
<FieldDescription>
The billing address associated with your payment method
</FieldDescription>
<CheckboxRoot>
<CheckboxControl />
<CheckboxLabel class="font-normal">
Same as shipping address
</CheckboxLabel>
</CheckboxRoot>
</FieldSet>
<FieldSet>
<FieldGroup>
<Field>
<FieldLabel for="comments">Comments</FieldLabel>
<Textarea id="comments" placeholder="Add any additional comments" class="resize-none" />
</Field>
</FieldGroup>
</FieldSet>
<Field orientation="horizontal">
<Button look="outline" type="submit">Submit</Button>
<Button type="button">Cancel</Button>
</Field>
</FieldGroup>
</form>
` }}
</PreviewCode>
</template>
</Preview>
<Preview>
<template #preview>
<FieldGroup class="w-full max-w-xs">
<FieldSet>
<FieldLegend variant="label">Compute Environment</FieldLegend>
<FieldDescription>
Select the compute environment for your cluster.
</FieldDescription>
<RadioGroupRoot defaultValue="React">
<FieldLabel>
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>Kubernetes</FieldTitle>
<FieldDescription>
Run GPU workloads on a K8s cluster.
</FieldDescription>
</FieldContent>
<RadioGroupItem value="React">
<RadioGroupItemControl />
</RadioGroupItem>
</Field>
</FieldLabel>
<FieldLabel>
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>Virtual Machine</FieldTitle>
<FieldDescription>
Access a cluster to run GPU workloads.
</FieldDescription>
</FieldContent>
<RadioGroupItem value="Solid">
<RadioGroupItemControl />
</RadioGroupItem>
</Field>
</FieldLabel>
</RadioGroupRoot>
</FieldSet>
</FieldGroup>
</template>
<template #code>
<PreviewCode>
{{ `
<FieldGroup class="w-full max-w-xs">
<FieldSet>
<FieldLegend variant="label">Compute Environment</FieldLegend>
<FieldDescription>
Select the compute environment for your cluster.
</FieldDescription>
<RadioGroupRoot defaultValue="React">
<FieldLabel>
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>Kubernetes</FieldTitle>
<FieldDescription>
Run GPU workloads on a K8s cluster.
</FieldDescription>
</FieldContent>
<RadioGroupItem value="React">
<RadioGroupItemControl />
</RadioGroupItem>
</Field>
</FieldLabel>
<FieldLabel>
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>Virtual Machine</FieldTitle>
<FieldDescription>
Access a cluster to run GPU workloads.
</FieldDescription>
</FieldContent>
<RadioGroupItem value="Solid">
<RadioGroupItemControl />
</RadioGroupItem>
</Field>
</FieldLabel>
</RadioGroupRoot>
</FieldSet>
</FieldGroup>
` }}
</PreviewCode>
</template>
</Preview>
</div>
</template>
-74
View File
@@ -1,74 +0,0 @@
<script lang="ts" setup>
import { Input } from "@/components/ui/input";
import {
Preview,
SectionTitle,
SectionContent,
PreviewCode,
} from "@/components/docs";
</script>
<template>
<div id="preview" class="-mt-20">
<Preview>
<template #preview>
<Input class="w-84" type="email" placeholder="Email" />
</template>
<template #code>
<PreviewCode>
{{ `
<Input class="w-84" type="email" placeholder="Email" />
` }}
</PreviewCode>
</template>
</Preview>
</div>
<div id="installation">
<SectionTitle>Installation</SectionTitle>
<SectionContent>
Copy and paste the following code into your project.
</SectionContent>
<PreviewCode title="components/ui/input/Input.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { input } from "@mykopkb/core/styles/input.styles";
const {
class: className,
type,
...props
} = defineProps<{
class?: string;
type?: string;
}>();
</script>
<template>
<input :type="type" :class="cn(input, className)" v-bind="{ ...props }" />
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/input/index.ts">
{{ `
export { default as Input } from "./Input.vue";
` }}
</PreviewCode>
<SectionContent>
Update the import paths to match your project setup.
</SectionContent>
</div>
<div id="usage">
<SectionTitle>Usage</SectionTitle>
<PreviewCode>
{{ `
import { Input } from "@/components/ui/input";
` }}
</PreviewCode>
<PreviewCode>
{{ `
<Input class="w-84" type="email" placeholder="Email" />
` }}
</PreviewCode>
</div>
</template>
-369
View File
@@ -1,369 +0,0 @@
<script lang="ts" setup>
import { Map } from "@/components/ui/map";
import {
Preview,
SectionTitle,
SectionContent,
InstallPackage,
PreviewCode,
} from "@/components/docs";
const markersData = {
type: "FeatureCollection" as const,
features: [
{
type: "Feature" as const,
properties: { name: "Cafe Berlin", type: "cafe" },
geometry: {
type: "Point" as const,
coordinates: [13.388, 52.517],
},
},
{
type: "Feature" as const,
properties: { name: "Restaurant Alex", type: "restaurant" },
geometry: {
type: "Point" as const,
coordinates: [13.39, 52.518],
},
},
{
type: "Feature" as const,
properties: { name: "Coffee House", type: "cafe" },
geometry: {
type: "Point" as const,
coordinates: [13.385, 52.515],
},
},
{
type: "Feature" as const,
properties: { name: "Pizza Place", type: "restaurant" },
geometry: {
type: "Point" as const,
coordinates: [13.392, 52.519],
},
},
{
type: "Feature" as const,
properties: { name: "Burger Joint", type: "restaurant" },
geometry: {
type: "Point" as const,
coordinates: [13.387, 52.516],
},
},
{
type: "Feature" as const,
properties: { name: "Starbucks", type: "cafe" },
geometry: {
type: "Point" as const,
coordinates: [13.391, 52.52],
},
},
{
type: "Feature" as const,
properties: { name: "Sushi Bar", type: "restaurant" },
geometry: {
type: "Point" as const,
coordinates: [13.395, 52.522],
},
},
{
type: "Feature" as const,
properties: { name: "Bakery", type: "cafe" },
geometry: {
type: "Point" as const,
coordinates: [13.383, 52.514],
},
},
{
type: "Feature" as const,
properties: { name: "Italian Restaurant", type: "restaurant" },
geometry: {
type: "Point" as const,
coordinates: [13.398, 52.525],
},
},
{
type: "Feature" as const,
properties: { name: "Tea House", type: "cafe" },
geometry: {
type: "Point" as const,
coordinates: [13.38, 52.512],
},
},
],
};
</script>
<template>
<div id="preview" class="-mt-20">
<Preview>
<template #preview>
<Map class="w-full h-96" :center="[13.388, 52.517]" :zoom="9.5" :markers="markersData" />
</template>
<template #code>
<PreviewCode>
{{ `
<Map class="w-full h-96" :center="[13.388, 52.517]" :zoom="9.5" :markers="markersData" />
` }}
</PreviewCode>
</template>
</Preview>
</div>
<div id="installation">
<SectionTitle>Installation</SectionTitle>
<SectionContent>Install the following dependencies:</SectionContent>
<InstallPackage>add maplibre-gl</InstallPackage>
<SectionContent>
Copy and paste the following code into your project.
</SectionContent>
<PreviewCode title="components/ui/map/Map.vue">
{{`
<${""}script setup lang="ts">
import "maplibre-gl/dist/maplibre-gl.css";
import maplibregl, { type MapOptions } from "maplibre-gl";
import { ref, onMounted } from "vue";
import { Button } from "@/components/ui/button";
import { Plus, Minus, MapPin, Compass, Expand, X } from "@lucide/vue";
import type { FeatureCollection, Point } from "geojson";
import { cn } from "@mykopkb/core/utils/cn";
import { map } from "@mykopkb/core/styles/map.styles";
const mapRef = ref();
const mapInstance = ref<maplibregl.Map>();
const isFullscreen = ref(false);
const {
class: className,
markers,
...props
} = defineProps<
Partial<MapOptions> & {
class?: string;
markers?: FeatureCollection<Point>;
}
>();
onMounted(() => {
mapInstance.value = new maplibregl.Map({
...Object.fromEntries(Object.entries(props).filter(([_, value]) => value)),
style: "https://tiles.openfreemap.org/styles/positron",
container: mapRef.value,
});
markers &&
mapInstance.value.on("load", () => {
// Add source with clustering
mapInstance.value!.addSource("markers", {
type: "geojson",
data: markers,
cluster: true,
clusterMaxZoom: 14, // Max zoom for clustering
clusterRadius: 50, // Cluster radius in pixels
});
// Layer for clusters (circles)
mapInstance.value!.addLayer({
id: "clusters",
type: "circle",
source: "markers",
filter: ["has", "point_count"],
paint: {
"circle-color": [
"step",
["get", "point_count"],
"#cccccc", // Color for < 5 points
5,
"#cccccc", // Color for 5-10 points
10,
"#cccccc", // Color for > 10 points
],
"circle-radius": [
"step",
["get", "point_count"],
20, // Radius for < 5 points
5,
30, // Radius for 5-10 points
10,
40, // Radius for > 10 points
],
},
});
// Layer for cluster count numbers
mapInstance.value!.addLayer({
id: "cluster-count",
type: "symbol",
source: "markers",
filter: ["has", "point_count"],
layout: {
"text-field": "{point_count_abbreviated}",
"text-font": ["Open Sans Bold"],
"text-size": 12,
},
});
// Layer for individual points (unclustered)
mapInstance.value!.addLayer({
id: "unclustered-point",
type: "circle",
source: "markers",
filter: ["!", ["has", "point_count"]],
paint: {
"circle-color": "#333333",
"circle-radius": 8,
"circle-stroke-width": 2,
"circle-stroke-color": "#ffffff",
},
});
// Click on cluster to zoom in
mapInstance.value!.on("click", "clusters", async (e) => {
const features = mapInstance.value!.queryRenderedFeatures(e.point, {
layers: ["clusters"],
});
const clusterId = features[0]?.properties.cluster_id;
const source = mapInstance.value!.getSource(
"markers"
) as maplibregl.GeoJSONSource;
try {
const zoom = await source.getClusterExpansionZoom(clusterId);
mapInstance.value!.easeTo({
center: (features[0]?.geometry as any).coordinates,
zoom: zoom,
});
} catch (err) {
console.error("Error getting cluster expansion zoom:", err);
}
});
// Click on individual point to show popup
mapInstance.value!.on("click", "unclustered-point", (e) => {
const coordinates = (
e.features![0]?.geometry as any
).coordinates.slice();
const { name, type } = (e.features![0]?.properties || {}) as any;
new maplibregl.Popup()
.setLngLat(coordinates)
.setHTML(\`<h3>\${name}</h3><p>\${type}</p>\`)
.addTo(mapInstance.value!);
});
// Change cursor on hover cluster/point
mapInstance.value!.on("mouseenter", "clusters", () => {
mapInstance.value!.getCanvas().style.cursor = "pointer";
});
mapInstance.value!.on("mouseleave", "clusters", () => {
mapInstance.value!.getCanvas().style.cursor = "";
});
mapInstance.value!.on("mouseenter", "unclustered-point", () => {
mapInstance.value!.getCanvas().style.cursor = "pointer";
});
mapInstance.value!.on("mouseleave", "unclustered-point", () => {
mapInstance.value!.getCanvas().style.cursor = "";
});
});
});
const zoomIn = () => {
mapInstance.value?.zoomIn();
};
const zoomOut = () => {
mapInstance.value?.zoomOut();
};
const resetNorth = () => {
mapInstance.value?.easeTo({ bearing: 0, pitch: 0 });
};
const locateMe = () => {
if (!navigator.geolocation) {
alert("Geolocation is not supported by your browser");
return;
}
navigator.geolocation.getCurrentPosition(
(position) => {
const { longitude, latitude } = position.coords;
mapInstance.value?.flyTo({
center: [longitude, latitude],
zoom: 14,
});
new maplibregl.Marker({ color: "var(--color-foreground)" })
.setLngLat([longitude, latitude])
.addTo(mapInstance.value!);
},
(error) => {
alert("Failed to get location: " + error.message);
}
);
};
const toggleFullscreen = () => {
if (!mapRef.value) return;
if (!isFullscreen.value) {
if (mapRef.value.requestFullscreen) {
mapRef.value.requestFullscreen();
}
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
}
}
isFullscreen.value = !isFullscreen.value;
};
</${""}script>
<template>
<div data-scope="map" data-part="root" ref="mapRef" :class="cn(map, className)">
<div data-scope="map" data-part="controls">
<Button data-scope="map" data-part="zoom-in" @click="zoomIn" variant="ghost" size="sm">
<Plus />
</Button>
<Button data-scope="map" data-part="zoom-out" @click="zoomOut" variant="ghost" size="sm">
<Minus />
</Button>
<Button data-scope="map" data-part="reset-north" @click="resetNorth" variant="ghost" size="sm">
<Compass />
</Button>
<Button data-scope="map" data-part="locate" @click="locateMe" variant="ghost" size="sm">
<MapPin />
</Button>
<Button data-scope="map" data-part="toggle-fullscreen" @click="toggleFullscreen" variant="ghost" size="sm">
<Expand v-if="!isFullscreen" />
<X v-else />
</Button>
</div>
</div>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/map/index.ts">
{{ `
export { default as Map } from "./Map.vue";
` }}
</PreviewCode>
<SectionContent>
Update the import paths to match your project setup.
</SectionContent>
</div>
<div id="usage">
<SectionTitle>Usage</SectionTitle>
<PreviewCode>
{{ `
import { Map } from "@/components/ui/map";
` }}
</PreviewCode>
<PreviewCode>
{{ `
<Map class="w-full h-96" :center="[13.388, 52.517]" :zoom="9.5" :markers="markersData" />
` }}
</PreviewCode>
</div>
</template>
-831
View File
@@ -1,831 +0,0 @@
<script lang="ts" setup>
import {
MenuRoot,
MenuTrigger,
MenuPositioner,
MenuContent,
MenuItem,
MenuCheckboxItem,
MenuSeparator,
MenuTriggerItem,
MenuRadioItemGroup,
MenuItemGroupLabel,
MenuRadioItem,
} from "@/components/ui/menu";
import {
Preview,
SectionTitle,
SectionContent,
InstallPackage,
PreviewCode,
} from "@/components/docs";
import { ref } from "vue";
const react = ref(false);
const solid = ref(false);
const vue = ref(false);
const svelte = ref(false);
const value = ref("react");
</script>
<template>
<div id="preview" class="-mt-20">
<Preview>
<template #preview>
<MenuRoot class="w-56">
<MenuTrigger>Open menu</MenuTrigger>
<MenuPositioner>
<MenuContent>
<MenuItem value="react">React</MenuItem>
<MenuItem value="solid">Solid</MenuItem>
<MenuItem value="vue">Vue</MenuItem>
<MenuItem value="svelte">Svelte</MenuItem>
</MenuContent>
</MenuPositioner>
</MenuRoot>
</template>
<template #code>
<PreviewCode>
{{ `
<MenuRoot class="w-56">
<MenuTrigger>Open menu</MenuTrigger>
<MenuPositioner>
<MenuContent>
<MenuItem value="react">React</MenuItem>
<MenuItem value="solid">Solid</MenuItem>
<MenuItem value="vue">Vue</MenuItem>
<MenuItem value="svelte">Svelte</MenuItem>
</MenuContent>
</MenuPositioner>
</MenuRoot>
` }}
</PreviewCode>
</template>
</Preview>
</div>
<div id="installation">
<SectionTitle>Installation</SectionTitle>
<SectionContent>Install the following dependencies:</SectionContent>
<InstallPackage>add @zag-js/vue @zag-js/menu</InstallPackage>
<SectionContent>
Copy and paste the following code into your project.
</SectionContent>
<PreviewCode title="components/ui/menu/MenuRoot.vue">
{{`
<script lang="ts" setup>
import * as menu from "@zag-js/menu";
import type { Props } from "@zag-js/menu";
import { Slot } from "@/components/ui/slot";
import { cn } from "@mykopkb/core/utils/cn";
import { normalizeProps, useMachine } from "@zag-js/vue";
import { computed, provide } from "vue";
import { menuRoot } from "@mykopkb/core/styles/menu.styles";
const {
class: className,
asChild = false,
closeOnSelect = false,
open = undefined,
...props
} = defineProps<Partial<Props> & { class?: string; asChild?: boolean }>();
const service = useMachine(menu.machine, {
...props,
open,
closeOnSelect,
id: crypto.randomUUID(),
});
const api = computed(() => menu.connect(service, normalizeProps));
provide("menuApi", api);
</script>
<template>
<Slot :class="cn(menuRoot, className)" v-bind="{ ...props, ...$attrs }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/menu/MenuTrigger.vue">
{{ `
<script lang="ts" setup>
import { type Api } from "@zag-js/menu";
import { cn } from "@mykopkb/core/utils/cn";
import { inject } from "vue";
import { Button } from "@/components/ui/button";
import { Slot } from "@/components/ui/slot";
import { menuTrigger } from "@mykopkb/core/styles/menu.styles";
import { MenuIndicator } from ".";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("menuApi");
</script>
<template>
<Slot v-bind="{ ...api?.getTriggerProps(), ...props, ...$attrs }">
<Button variant="ghost" v-if="!asChild" :class="cn(menuTrigger, className)">
<slot />
<MenuIndicator />
</Button>
<slot v-else />
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/menu/MenuIndicator.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { ChevronDown } from "@lucide/vue";
import { menuIndicator } from "@mykopkb/core/styles/menu.styles";
import { Slot } from "@/components/ui/slot";
import type { Api } from "@zag-js/menu";
import { inject } from "vue";
const { class: className, ...props } = defineProps<{
class?: string;
}>();
const api = inject<Api>("menuApi");
</script>
<template>
<Slot :class="cn(menuIndicator, className)" v-bind="{ ...api?.getIndicatorProps(), ...props, ...$attrs }">
<slot v-if="$slots.default" />
<ChevronDown v-else />
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/menu/MenuPositioner.vue">
{{ `
<script lang="ts" setup>
import { type Api } from "@zag-js/menu";
import { cn } from "@mykopkb/core/utils/cn";
import { inject } from "vue";
import { menuPositioner } from "@mykopkb/core/styles/menu.styles";
import { Slot } from "@/components/ui/slot";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("menuApi");
</script>
<template>
<Teleport to="body">
<Slot :class="cn(menuPositioner, className)" v-bind="{ ...props, ...$attrs, ...api?.getPositionerProps() }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</Teleport>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/menu/MenuContent.vue">
{{ `
<script lang="ts" setup>
import { type Api } from "@zag-js/menu";
import { cn } from "@mykopkb/core/utils/cn";
import { inject } from "vue";
import { Box } from "@/components/ui/box";
import { Slot } from "@/components/ui/slot";
import { menuContent } from "@mykopkb/core/styles/menu.styles";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("menuApi");
</script>
<template>
<Slot :class="cn(menuContent, className)" v-bind="{ ...api?.getContentProps(), ...props, ...$attrs }">
<slot v-if="asChild" />
<Box v-else raised="single" :class="cn(menuContent, className)">
<div>
<slot />
</div>
</Box>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/menu/MenuItem.vue">
{{ `
<script lang="ts" setup>
import { Slot } from "@/components/ui/slot";
import { type Api, type ItemProps } from "@zag-js/menu";
import { inject } from "vue";
import { cn } from "@mykopkb/core/utils/cn";
import { menuItem } from "@mykopkb/core/styles/menu.styles";
const {
class: className,
shortcut,
asChild = false,
...props
} = defineProps<
ItemProps & {
class?: string;
shortcut?: string;
asChild?: boolean;
}
>();
const api = inject<Api>("menuApi");
</script>
<template>
<Slot :class="cn(menuItem, className)" v-bind="{ ...api?.getItemProps(props), ...props, ...$attrs }">
<slot v-if="asChild" />
<div v-else>
<div>
<slot />
</div>
<div>\{\{ shortcut \}\}</div>
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/menu/MenuTriggerItem.vue">
{{ `
<script lang="ts" setup>
import { type Api } from "@zag-js/menu";
import { inject } from "vue";
import { cn } from "@mykopkb/core/utils/cn";
import { ChevronRight } from "@lucide/vue";
import { Slot } from "@/components/ui/slot";
import { menuItem } from "@mykopkb/core/styles/menu.styles";
const { class: className, ...props } = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("menuApi");
</script>
<template>
<Slot :class="cn(menuItem, className)" v-bind="{ ...api?.getTriggerItemProps(api), ...props, ...$attrs }">
<slot v-if="asChild" />
<div v-else>
<div>
<slot />
</div>
<ChevronRight data-part="nested-menu-chevron" />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/menu/MenuCheckboxItem.vue">
{{ `
<script lang="ts" setup>
import type { Api, OptionItemProps } from "@zag-js/menu";
import { cn } from "@mykopkb/core/utils/cn";
import { Check } from "@lucide/vue";
import { menuItem } from "@mykopkb/core/styles/menu.styles";
import { inject } from "vue";
const {
shortcut,
class: className,
type = "checkbox",
...props
} = defineProps<
Omit<OptionItemProps, "type"> & {
class?: string;
shortcut?: string;
type?: OptionItemProps["type"];
}
>();
const api = inject<Api>("menuApi");
</script>
<template>
<div :class="cn(menuItem, className)" v-bind="{
...props,
...$attrs,
...api?.getOptionItemProps({
...props,
type,
}),
}">
<div>
<span data-part="item-indicator" v-bind="{ ...api?.getItemIndicatorProps(props) }">
<Check />
</span>
<slot />
</div>
<div>\{\{ shortcut \}\}</div>
</div>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/menu/MenuRadioItemGroup.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { menuRadioItemGroup } from "@mykopkb/core/styles/menu.styles";
import { Slot } from "@/components/ui/slot";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
</script>
<template>
<Slot :class="cn(menuRadioItemGroup, className)" v-bind="{
...props,
...$attrs,
}">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/menu/MenuItemGroupLabel.vue">
{{ `
<script lang="ts" setup>
import { Slot } from "@/components/ui/slot";
import { cn } from "@mykopkb/core/utils/cn";
import { menuItemGroupLabel } from "@mykopkb/core/styles/menu.styles";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
</script>
<template>
<Slot :class="cn(menuItemGroupLabel, className)" v-bind="{ ...props, ...$attrs }">
<slot v-if="asChild" />
<label v-else>
<slot />
</label>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/menu/MenuRadioItem.vue">
{{ `
<script lang="ts" setup>
import type { Api, OptionItemProps } from "@zag-js/menu";
import { cn } from "@mykopkb/core/utils/cn";
import { Dot } from "@lucide/vue";
import { menuItem } from "@mykopkb/core/styles/menu.styles";
import { inject } from "vue";
const {
shortcut,
class: className,
asChild = false,
type = "radio",
...props
} = defineProps<
Omit<OptionItemProps, "type"> & {
class?: string;
asChild?: boolean;
shortcut?: string;
type?: OptionItemProps["type"];
}
>();
const api = inject<Api>("menuApi");
</script>
<template>
<div :class="cn(menuItem, className)" v-bind="{
...props,
...$attrs,
...api?.getOptionItemProps({
...props,
type,
}),
}">
<div>
<span data-part="item-indicator" v-bind="{ ...api?.getItemIndicatorProps(props) }">
<Dot />
</span>
<slot />
</div>
<div>\{\{ shortcut \}\}</div>
</div>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/menu/MenuSeparator.vue">
{{ `
<script lang="ts" setup>
import type { Api } from "@zag-js/menu";
import { cn } from "@mykopkb/core/utils/cn";
import { menuSeparator } from "@mykopkb/core/styles/menu.styles";
import { Slot } from "@/components/ui/slot";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("menuApi");
</script>
<template>
<Slot :class="cn(menuSeparator, className)" v-bind="{
...props,
...$attrs,
...api?.getSeparatorProps(),
}">
<slot v-if="asChild" />
<hr v-else>
<slot />
</hr>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/menu/index.ts">
{{ `
export { default as MenuRoot } from "./MenuRoot.vue";
export { default as MenuTrigger } from "./MenuTrigger.vue";
export { default as MenuIndicator } from "./MenuIndicator.vue";
export { default as MenuPositioner } from "./MenuPositioner.vue";
export { default as MenuContent } from "./MenuContent.vue";
export { default as MenuItem } from "./MenuItem.vue";
export { default as MenuTriggerItem } from "./MenuTriggerItem.vue";
export { default as MenuCheckboxItem } from "./MenuCheckboxItem.vue";
export { default as MenuRadioItemGroup } from "./MenuRadioItemGroup.vue";
export { default as MenuItemGroupLabel } from "./MenuItemGroupLabel.vue";
export { default as MenuRadioItem } from "./MenuRadioItem.vue";
export { default as MenuSeparator } from "./MenuSeparator.vue";
` }}
</PreviewCode>
<SectionContent>
Update the import paths to match your project setup.
</SectionContent>
</div>
<div id="usage">
<SectionTitle>Usage</SectionTitle>
<PreviewCode>
{{ `
import {
MenuRoot,
MenuTrigger,
MenuPositioner,
MenuContent,
MenuItem,
MenuCheckboxItem,
MenuSeparator,
MenuTriggerItem,
MenuRadioItemGroup,
MenuItemGroupLabel,
MenuRadioItem,
} from "@/components/ui/menu";
` }}
</PreviewCode>
<PreviewCode>
{{ `
<MenuRoot class="w-56">
<MenuTrigger>Open menu</MenuTrigger>
<MenuPositioner>
<MenuContent>
<MenuItem value="react">React</MenuItem>
<MenuItem value="solid">Solid</MenuItem>
<MenuItem value="vue">Vue</MenuItem>
<MenuItem value="svelte">Svelte</MenuItem>
</MenuContent>
</MenuPositioner>
</MenuRoot>
` }}
</PreviewCode>
</div>
<div id="variants">
<SectionTitle>Variants</SectionTitle>
<SectionContent>A collection of components you can use.</SectionContent>
<Preview>
<template #preview>
<MenuRoot class="w-56">
<MenuTrigger>Open menu</MenuTrigger>
<MenuPositioner>
<MenuContent>
<MenuItem value="react">React</MenuItem>
<MenuItem value="solid">Solid</MenuItem>
<MenuItem value="vue">Vue</MenuItem>
<MenuItem value="svelte">Svelte</MenuItem>
<MenuSeparator />
<MenuItem value="react">React</MenuItem>
<MenuItem value="solid">Solid</MenuItem>
<MenuItem value="vue">Vue</MenuItem>
<MenuItem value="svelte">Svelte</MenuItem>
</MenuContent>
</MenuPositioner>
</MenuRoot>
</template>
<template #code>
<PreviewCode>
{{ `
<MenuRoot class="w-56">
<MenuTrigger>Open menu</MenuTrigger>
<MenuPositioner>
<MenuContent>
<MenuItem value="react">React</MenuItem>
<MenuItem value="solid">Solid</MenuItem>
<MenuItem value="vue">Vue</MenuItem>
<MenuItem value="svelte">Svelte</MenuItem>
<MenuSeparator />
<MenuItem value="react">React</MenuItem>
<MenuItem value="solid">Solid</MenuItem>
<MenuItem value="vue">Vue</MenuItem>
<MenuItem value="svelte">Svelte</MenuItem>
</MenuContent>
</MenuPositioner>
</MenuRoot>
` }}
</PreviewCode>
</template>
</Preview>
<Preview>
<template #preview>
<MenuRoot class="w-56">
<MenuTrigger>Open menu</MenuTrigger>
<MenuPositioner>
<MenuContent>
<MenuItem shortcut="⇧⌘P" value="react">
React
</MenuItem>
<MenuItem shortcut="⌘B" value="solid">
Solid
</MenuItem>
<MenuItem shortcut="⌘S" value="vue">
Vue
</MenuItem>
<MenuItem shortcut="⌘K" value="svelte">
Svelte
</MenuItem>
<MenuSeparator />
<MenuItem value="react">React</MenuItem>
<MenuItem value="solid">Solid</MenuItem>
<MenuItem value="vue">Vue</MenuItem>
<MenuItem shortcut="⇧⌘Q" value="svelte">
Svelte
</MenuItem>
</MenuContent>
</MenuPositioner>
</MenuRoot>
</template>
<template #code>
<PreviewCode>
{{ `
<MenuRoot class="w-56">
<MenuTrigger>Open menu</MenuTrigger>
<MenuPositioner>
<MenuContent>
<MenuItem shortcut="⇧⌘P" value="react">
React
</MenuItem>
<MenuItem shortcut="⌘B" value="solid">
Solid
</MenuItem>
<MenuItem shortcut="⌘S" value="vue">
Vue
</MenuItem>
<MenuItem shortcut="⌘K" value="svelte">
Svelte
</MenuItem>
<MenuSeparator />
<MenuItem value="react">React</MenuItem>
<MenuItem value="solid">Solid</MenuItem>
<MenuItem value="vue">Vue</MenuItem>
<MenuItem shortcut="⇧⌘Q" value="svelte">
Svelte
</MenuItem>
</MenuContent>
</MenuPositioner>
</MenuRoot>
` }}
</PreviewCode>
</template>
</Preview>
<Preview>
<template #preview>
<MenuRoot class="w-56">
<MenuTrigger>Open menu</MenuTrigger>
<MenuPositioner>
<MenuContent>
<MenuItem shortcut="⇧⌘P" value="react">
React
</MenuItem>
<MenuItem shortcut="⌘B" value="solid">
Solid
</MenuItem>
<MenuItem shortcut="⌘S" value="vue">
Vue
</MenuItem>
<MenuItem shortcut="⌘K" value="svelte">
Svelte
</MenuItem>
<MenuRoot :positioning="{ placement: 'right-start', gutter: 12 }">
<MenuTriggerItem>Frameworks</MenuTriggerItem>
<MenuPositioner>
<MenuContent>
<MenuItem value="react">React</MenuItem>
<MenuItem value="solid">Solid</MenuItem>
<MenuItem value="vue">Vue</MenuItem>
<MenuItem value="svelte">Svelte</MenuItem>
</MenuContent>
</MenuPositioner>
</MenuRoot>
<MenuSeparator />
<MenuItem disabled value="react">
React
</MenuItem>
<MenuItem value="solid">Solid</MenuItem>
<MenuItem value="vue">Vue</MenuItem>
<MenuItem shortcut="⇧⌘Q" value="svelte">
Svelte
</MenuItem>
</MenuContent>
</MenuPositioner>
</MenuRoot>
</template>
<template #code>
<PreviewCode>
{{ `
<MenuRoot class="w-56">
<MenuTrigger>Open menu</MenuTrigger>
<MenuPositioner>
<MenuContent>
<MenuItem shortcut="⇧⌘P" value="react">
React
</MenuItem>
<MenuItem shortcut="⌘B" value="solid">
Solid
</MenuItem>
<MenuItem shortcut="⌘S" value="vue">
Vue
</MenuItem>
<MenuItem shortcut="⌘K" value="svelte">
Svelte
</MenuItem>
<MenuRoot :positioning="{ placement: 'right-start', gutter: 12 }">
<MenuTriggerItem>Frameworks</MenuTriggerItem>
<MenuPositioner>
<MenuContent>
<MenuItem value="react">React</MenuItem>
<MenuItem value="solid">Solid</MenuItem>
<MenuItem value="vue">Vue</MenuItem>
<MenuItem value="svelte">Svelte</MenuItem>
</MenuContent>
</MenuPositioner>
</MenuRoot>
<MenuSeparator />
<MenuItem disabled value="react">
React
</MenuItem>
<MenuItem value="solid">Solid</MenuItem>
<MenuItem value="vue">Vue</MenuItem>
<MenuItem shortcut="⇧⌘Q" value="svelte">
Svelte
</MenuItem>
</MenuContent>
</MenuPositioner>
</MenuRoot>
` }}
</PreviewCode>
</template>
</Preview>
<Preview>
<template #preview>
<MenuRoot class="w-56">
<MenuTrigger>Open menu</MenuTrigger>
<MenuPositioner>
<MenuContent>
<MenuCheckboxItem :checked="react" :onCheckedChange="(checked) => (react = checked)" value="checked">
React
</MenuCheckboxItem>
<MenuCheckboxItem :checked="solid" :onCheckedChange="(checked) => (solid = checked)" value="checked">
Solid
</MenuCheckboxItem>
<MenuCheckboxItem :checked="vue" :onCheckedChange="(checked) => (vue = checked)" value="checked">
Vue
</MenuCheckboxItem>
<MenuCheckboxItem :checked="svelte" :onCheckedChange="(checked) => (svelte = checked)" value="checked">
Svelte
</MenuCheckboxItem>
</MenuContent>
</MenuPositioner>
</MenuRoot>
</template>
<template #code>
<PreviewCode>
{{`
<MenuRoot class="w-56">
<MenuTrigger>Open menu</MenuTrigger>
<MenuPositioner>
<MenuContent>
<MenuCheckboxItem :checked="react" :onCheckedChange="(checked) => (react = checked)" value="checked">
React
</MenuCheckboxItem>
<MenuCheckboxItem :checked="solid" :onCheckedChange="(checked) => (solid = checked)" value="checked">
Solid
</MenuCheckboxItem>
<MenuCheckboxItem :checked="vue" :onCheckedChange="(checked) => (vue = checked)" value="checked">
Vue
</MenuCheckboxItem>
<MenuCheckboxItem :checked="svelte" :onCheckedChange="(checked) => (svelte = checked)" value="checked">
Svelte
</MenuCheckboxItem>
</MenuContent>
</MenuPositioner>
</MenuRoot>
` }}
</PreviewCode>
</template>
</Preview>
<Preview>
<template #preview>
<MenuRoot class="w-56">
<MenuTrigger>Open menu</MenuTrigger>
<MenuPositioner>
<MenuContent>
<MenuRadioItemGroup>
<MenuItemGroupLabel>Frameworks</MenuItemGroupLabel>
<MenuRadioItem v-for="framework in ['React', 'Solid', 'Vue', 'Svelte']" :key="framework"
:value="framework" :checked="framework == value" :onCheckedChange="(checked) => (checked ? (value = framework) : '')
">
{{ framework }}
</MenuRadioItem>
</MenuRadioItemGroup>
</MenuContent>
</MenuPositioner>
</MenuRoot>
</template>
<template #code>
<PreviewCode>
{{`
<MenuRoot class="w-56">
<MenuTrigger>Open menu</MenuTrigger>
<MenuPositioner>
<MenuContent>
<MenuRadioItemGroup>
<MenuItemGroupLabel>Frameworks</MenuItemGroupLabel>
<MenuRadioItem v-for="framework in ['React', 'Solid', 'Vue', 'Svelte']" :key="framework"
:value="framework" :checked="framework == value" :onCheckedChange="
(checked) => (checked ? (value = framework) : '')
">
\{\{ framework \}\}
</MenuRadioItem>
</MenuRadioItemGroup>
</MenuContent>
</MenuPositioner>
</MenuRoot>
` }}
</PreviewCode>
</template>
</Preview>
</div>
</template>
-298
View File
@@ -1,298 +0,0 @@
<script lang="ts" setup>
import {
PaginationContext,
PaginationRoot,
PaginationItem,
PaginationPrevTrigger,
PaginationNextTrigger,
PaginationEllipsis,
} from "@/components/ui/pagination";
import {
Preview,
SectionTitle,
SectionContent,
InstallPackage,
PreviewCode,
} from "@/components/docs";
</script>
<template>
<div id="preview" class="-mt-20">
<Preview>
<template #preview>
<PaginationRoot :count="5000" :pageSize="10" :siblingCount="2">
<PaginationPrevTrigger>Previous</PaginationPrevTrigger>
<PaginationContext v-slot="{ pagination }">
<template v-for="(page, index) in pagination?.pages" :key="index">
<PaginationItem v-if="page.type === 'page'" v-bind="{ ...page }">
{{ page.value }}
</PaginationItem>
<PaginationEllipsis v-else :index="index" />
</template>
</PaginationContext>
<PaginationNextTrigger>Next</PaginationNextTrigger>
</PaginationRoot>
</template>
<template #code>
<PreviewCode>
{{ `
<PaginationRoot :count="5000" :pageSize="10" :siblingCount="2">
<PaginationPrevTrigger>Previous</PaginationPrevTrigger>
<PaginationContext v-slot="{ pagination }">
<template v-for="(page, index) in pagination?.pages" :key="index">
<PaginationItem v-if="page.type === 'page'" v-bind="{ ...page }">
\{\{ page.value \}\}
</PaginationItem>
<PaginationEllipsis v-else :index="index" />
</template>
</PaginationContext>
<PaginationNextTrigger>Next</PaginationNextTrigger>
</PaginationRoot>
` }}
</PreviewCode>
</template>
</Preview>
</div>
<div id="installation">
<SectionTitle>Installation</SectionTitle>
<SectionContent>Install the following dependencies:</SectionContent>
<InstallPackage>add @zag-js/vue @zag-js/pagination</InstallPackage>
<SectionContent>
Copy and paste the following code into your project.
</SectionContent>
<PreviewCode title="components/ui/pagination/PaginationContext.vue">
{{ `
<script setup lang="ts">
import type { Api } from "@zag-js/pagination";
import { inject } from "vue";
const api = inject<Api>("paginationApi");
</script>
<template>
<slot :pagination="api"></slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/pagination/PaginationEllipsis.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { paginationEllipsis } from "@mykopkb/core/styles/pagination.styles";
import { Slot } from "@/components/ui/slot";
import type { Api, EllipsisProps } from "@zag-js/pagination";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<
EllipsisProps & {
class?: string;
asChild?: boolean;
}
>();
const api = inject<Api>("paginationApi");
</script>
<template>
<Slot :class="cn(paginationEllipsis, className)"
v-bind="{ ...api?.getEllipsisProps(props), ...props, ...$attrs }">
<div v-if="!$slots.default">…</div>
<template v-else>
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</template>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/pagination/PaginationItem.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { paginationItem } from "@mykopkb/core/styles/pagination.styles";
import { Slot } from "@/components/ui/slot";
import type { Api, ItemProps } from "@zag-js/pagination";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<
ItemProps & {
class?: string;
asChild?: boolean;
}
>();
const api = inject<Api>("paginationApi");
</script>
<template>
<Slot :class="cn(paginationItem, className)" v-bind="{ ...api?.getItemProps(props), ...props, ...$attrs }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/pagination/PaginationNextTrigger.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { paginationNextTrigger } from "@mykopkb/core/styles/pagination.styles";
import { Slot } from "@/components/ui/slot";
import type { Api } from "@zag-js/pagination";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("paginationApi");
</script>
<template>
<Slot :class="cn(paginationNextTrigger, className)"
v-bind="{ ...api?.getNextTriggerProps(), ...props, ...$attrs }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/pagination/PaginationPrevTrigger.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { paginationPrevTrigger } from "@mykopkb/core/styles/pagination.styles";
import { Slot } from "@/components/ui/slot";
import type { Api } from "@zag-js/pagination";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("paginationApi");
</script>
<template>
<Slot :class="cn(paginationPrevTrigger, className)"
v-bind="{ ...api?.getPrevTriggerProps(), ...props, ...$attrs }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/pagination/PaginationRoot.vue">
{{`
<script lang="ts" setup>
import * as pagination from "@zag-js/pagination";
import type { Props } from "@zag-js/pagination";
import { Slot } from "@/components/ui/slot";
import { cn } from "@mykopkb/core/utils/cn";
import { normalizeProps, useMachine } from "@zag-js/vue";
import { computed, provide } from "vue";
import { paginationRoot } from "@mykopkb/core/styles/pagination.styles";
const {
class: className,
asChild = false,
count,
pageSize,
siblingCount,
...props
} = defineProps<Partial<Props> & { class?: string; asChild?: boolean }>();
const service = useMachine(pagination.machine, {
...props,
count,
pageSize,
siblingCount,
id: crypto.randomUUID(),
});
const api = computed(() => pagination.connect(service, normalizeProps));
provide("paginationApi", api);
</script>
<template>
<Slot :class="cn(paginationRoot, className)" v-bind="{ ...props, ...$attrs, ...api.getRootProps() }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/pagination/index.ts">
{{ `
export { default as PaginationContext } from "./PaginationContext.vue";
export { default as PaginationEllipsis } from "./PaginationEllipsis.vue";
export { default as PaginationItem } from "./PaginationItem.vue";
export { default as PaginationNextTrigger } from "./PaginationNextTrigger.vue";
export { default as PaginationPrevTrigger } from "./PaginationPrevTrigger.vue";
export { default as PaginationRoot } from "./PaginationRoot.vue";
` }}
</PreviewCode>
<SectionContent>
Update the import paths to match your project setup.
</SectionContent>
</div>
<div id="usage">
<SectionTitle>Usage</SectionTitle>
<PreviewCode>
{{ `
import {
PaginationContext,
PaginationRoot,
PaginationItem,
PaginationPrevTrigger,
PaginationNextTrigger,
PaginationEllipsis,
} from "@/components/ui/pagination";
` }}
</PreviewCode>
<PreviewCode>
{{ `
<PaginationRoot :count="5000" :pageSize="10" :siblingCount="2">
<PaginationPrevTrigger>Previous</PaginationPrevTrigger>
<PaginationContext v-slot="{ pagination }">
<template v-for="(page, index) in pagination?.pages" :key="index">
<PaginationItem v-if="page.type === 'page'" v-bind="{ ...page }">
\{\{ page.value \}\}
</PaginationItem>
<PaginationEllipsis v-else :index="index" />
</template>
</PaginationContext>
<PaginationNextTrigger>Next</PaginationNextTrigger>
</PaginationRoot>
` }}
</PreviewCode>
</div>
</template>
-447
View File
@@ -1,447 +0,0 @@
<script lang="ts" setup>
import {
PopoverRoot,
PopoverTrigger,
PopoverPositioner,
PopoverContent,
PopoverTitle,
PopoverDescription,
} from "@/components/ui/popover";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import {
Preview,
SectionTitle,
SectionContent,
InstallPackage,
PreviewCode,
} from "@/components/docs";
</script>
<template>
<div id="preview" class="-mt-20">
<Preview>
<template #preview>
<PopoverRoot>
<PopoverTrigger class="w-56">Open Popover</PopoverTrigger>
<PopoverPositioner>
<PopoverContent class="w-100">
<PopoverTitle>Dimensions</PopoverTitle>
<PopoverDescription>
Set the dimensions for the layer.
</PopoverDescription>
<div class="grid gap-3 mt-4 mb-2">
<div class="grid grid-cols-3 items-center gap-4">
<Label for="width">Width</Label>
<Input id="width" defaultValue="100%" class="col-span-2" />
</div>
<div class="grid grid-cols-3 items-center gap-4">
<Label for="maxWidth">Max. width</Label>
<Input id="maxWidth" defaultValue="300px" class="col-span-2" />
</div>
<div class="grid grid-cols-3 items-center gap-4">
<Label for="height">Height</Label>
<Input id="height" defaultValue="25px" class="col-span-2" />
</div>
<div class="grid grid-cols-3 items-center gap-4">
<Label for="maxHeight">Max. height</Label>
<Input id="maxHeight" defaultValue="none" class="col-span-2" />
</div>
</div>
</PopoverContent>
</PopoverPositioner>
</PopoverRoot>
</template>
<template #code>
<PreviewCode>
{{ `
<PopoverRoot>
<PopoverTrigger class="w-56">Open Popover</PopoverTrigger>
<PopoverPositioner>
<PopoverContent class="w-100">
<PopoverTitle>Dimensions</PopoverTitle>
<PopoverDescription>
Set the dimensions for the layer.
</PopoverDescription>
<div class="grid gap-3 mt-4 mb-2">
<div class="grid grid-cols-3 items-center gap-4">
<Label for="width">Width</Label>
<Input id="width" defaultValue="100%" class="col-span-2" />
</div>
<div class="grid grid-cols-3 items-center gap-4">
<Label for="maxWidth">Max. width</Label>
<Input id="maxWidth" defaultValue="300px" class="col-span-2" />
</div>
<div class="grid grid-cols-3 items-center gap-4">
<Label for="height">Height</Label>
<Input id="height" defaultValue="25px" class="col-span-2" />
</div>
<div class="grid grid-cols-3 items-center gap-4">
<Label for="maxHeight">Max. height</Label>
<Input id="maxHeight" defaultValue="none" class="col-span-2" />
</div>
</div>
</PopoverContent>
</PopoverPositioner>
</PopoverRoot>
` }}
</PreviewCode>
</template>
</Preview>
</div>
<div id="installation">
<SectionTitle>Installation</SectionTitle>
<SectionContent>Install the following dependencies:</SectionContent>
<InstallPackage>add @zag-js/vue @zag-js/popover</InstallPackage>
<SectionContent>
Copy and paste the following code into your project.
</SectionContent>
<PreviewCode title="components/ui/popover/PopoverArrow.vue">
{{ `
<script lang="ts" setup>
import { type Api } from "@zag-js/popover";
import { cn } from "@mykopkb/core/utils/cn";
import { inject } from "vue";
import { popoverArrow } from "@mykopkb/core/styles/popover.styles";
const { class: className, ...props } = defineProps<{
class?: string;
}>();
const api = inject<Api>("popoverApi");
</script>
<template>
<div :class="cn(popoverArrow, className)" v-bind="{ ...api?.getArrowProps(), ...props, ...$attrs }">
<slot />
</div>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/popover/PopoverArrowTip.vue">
{{ `
<script lang="ts" setup>
import { type Api } from "@zag-js/popover";
import { cn } from "@mykopkb/core/utils/cn";
import { inject } from "vue";
import { popoverArrowTip } from "@mykopkb/core/styles/popover.styles";
const { class: className, ...props } = defineProps<{
class?: string;
}>();
const api = inject<Api>("popoverApi");
</script>
<template>
<div :class="cn(popoverArrowTip, className)" v-bind="{ ...api?.getArrowTipProps(), ...props, ...$attrs }">
<slot />
</div>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/popover/PopoverContent.vue">
{{ `
<script lang="ts" setup>
import { type Api } from "@zag-js/popover";
import { cn } from "@mykopkb/core/utils/cn";
import { inject } from "vue";
import { Slot } from "@/components/ui/slot";
import { popoverContent } from "@mykopkb/core/styles/popover.styles";
import { PopoverArrow, PopoverArrowTip } from ".";
import { Box } from "@/components/ui/box";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("popoverApi");
</script>
<template>
<Slot :class="cn(popoverContent, className)" v-bind="{ ...api?.getContentProps(), ...props, ...$attrs }">
<slot v-if="asChild" />
<Box v-else raised="single" :class="cn(popoverContent, className)">
<div>
<slot />
</div>
<PopoverArrow>
<PopoverArrowTip />
</PopoverArrow>
</Box>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/popover/PopoverDescription.vue">
{{ `
<script lang="ts" setup>
import { type Api } from "@zag-js/popover";
import { cn } from "@mykopkb/core/utils/cn";
import { inject } from "vue";
import { popoverDescription } from "@mykopkb/core/styles/popover.styles";
import { Slot } from "@/components/ui/slot";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("popoverApi");
</script>
<template>
<Slot :class="cn(popoverDescription, className)"
v-bind="{ ...api?.getDescriptionProps(), ...props, ...$attrs }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/popover/PopoverIndicator.vue">
{{ `
<script lang="ts" setup>
import { type Api } from "@zag-js/popover";
import { cn } from "@mykopkb/core/utils/cn";
import { inject } from "vue";
import { Slot } from "@/components/ui/slot";
import { popoverIndicator } from "@mykopkb/core/styles/popover.styles";
import { ChevronDown } from "@lucide/vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("popoverApi");
</script>
<template>
<Slot :class="cn(popoverIndicator, className)" v-bind="{ ...api?.getIndicatorProps(), ...props, ...$attrs }">
<slot v-if="$slots.default" />
<ChevronDown v-else />
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/popover/PopoverPositioner.vue">
{{ `
<script lang="ts" setup>
import { type Api } from "@zag-js/popover";
import { cn } from "@mykopkb/core/utils/cn";
import { inject } from "vue";
import { popoverPositioner } from "@mykopkb/core/styles/popover.styles";
import { Slot } from "@/components/ui/slot";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("popoverApi");
</script>
<template>
<Teleport to="body">
<Slot :class="cn(popoverPositioner, className)"
v-bind="{ ...api?.getPositionerProps(), ...props, ...$attrs }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</Teleport>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/popover/PopoverRoot.vue">
{{`
<script lang="ts" setup>
import * as popover from "@zag-js/popover";
import type { Props } from "@zag-js/popover";
import { Slot } from "@/components/ui/slot";
import { cn } from "@mykopkb/core/utils/cn";
import { normalizeProps, useMachine } from "@zag-js/vue";
import { computed, provide } from "vue";
import { popoverRoot } from "@mykopkb/core/styles/popover.styles";
const {
class: className,
asChild = false,
open = undefined,
closeOnInteractOutside = undefined,
...props
} = defineProps<Partial<Props> & { class?: string; asChild?: boolean }>();
const service = useMachine(popover.machine, {
...props,
open,
closeOnInteractOutside,
id: crypto.randomUUID(),
});
const api = computed(() => popover.connect(service, normalizeProps));
provide("popoverApi", api);
</script>
<template>
<Slot :class="cn(popoverRoot, className)" v-bind="{ ...props, ...$attrs }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/popover/PopoverTitle.vue">
{{ `
<script lang="ts" setup>
import { type Api } from "@zag-js/popover";
import { cn } from "@mykopkb/core/utils/cn";
import { inject } from "vue";
import { Slot } from "@/components/ui/slot";
import { popoverTitle } from "@mykopkb/core/styles/popover.styles";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("popoverApi");
</script>
<template>
<Slot :class="cn(popoverTitle, className)" v-bind="{ ...api?.getTitleProps(), ...props, ...$attrs }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/popover/PopoverTrigger.vue">
{{ `
<script lang="ts" setup>
import { type Api } from "@zag-js/popover";
import { cn } from "@mykopkb/core/utils/cn";
import { inject } from "vue";
import { Button } from "@/components/ui/button";
import { Slot } from "@/components/ui/slot";
import { popoverTrigger } from "@mykopkb/core/styles/popover.styles";
import { PopoverIndicator } from ".";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("popoverApi");
</script>
<template>
<Slot v-bind="{ ...api?.getTriggerProps(), ...props, ...$attrs }">
<Button variant="ghost" v-if="!asChild" :class="cn(popoverTrigger, className)">
<slot />
<PopoverIndicator />
</Button>
<slot v-else />
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/popover/index.ts">
{{ `
export { default as PopoverArrow } from "./PopoverArrow.vue";
export { default as PopoverArrowTip } from "./PopoverArrowTip.vue";
export { default as PopoverContent } from "./PopoverContent.vue";
export { default as PopoverDescription } from "./PopoverDescription.vue";
export { default as PopoverIndicator } from "./PopoverIndicator.vue";
export { default as PopoverPositioner } from "./PopoverPositioner.vue";
export { default as PopoverRoot } from "./PopoverRoot.vue";
export { default as PopoverTitle } from "./PopoverTitle.vue";
export { default as PopoverTrigger } from "./PopoverTrigger.vue";
` }}
</PreviewCode>
<SectionContent>
Update the import paths to match your project setup.
</SectionContent>
</div>
<div id="usage">
<SectionTitle>Usage</SectionTitle>
<PreviewCode>
{{ `
import {
PopoverRoot,
PopoverTrigger,
PopoverPositioner,
PopoverContent,
PopoverTitle,
PopoverDescription,
} from "@/components/ui/popover";
` }}
</PreviewCode>
<PreviewCode>
{{ `
<PopoverRoot>
<PopoverTrigger class="w-56"> Open Popover </PopoverTrigger>
<PopoverPositioner>
<PopoverContent class="w-100">
<PopoverTitle>Dimensions</PopoverTitle>
<PopoverDescription>
Set the dimensions for the layer.
</PopoverDescription>
<div class="grid gap-3 mt-4 mb-2">
<div class="grid grid-cols-3 items-center gap-4">
<Label for="width">Width</Label>
<Input id="width" defaultValue="100%" class="col-span-2" />
</div>
<div class="grid grid-cols-3 items-center gap-4">
<Label for="maxWidth">Max. width</Label>
<Input id="maxWidth" defaultValue="300px" class="col-span-2" />
</div>
<div class="grid grid-cols-3 items-center gap-4">
<Label for="height">Height</Label>
<Input id="height" defaultValue="25px" class="col-span-2" />
</div>
<div class="grid grid-cols-3 items-center gap-4">
<Label for="maxHeight">Max. height</Label>
<Input id="maxHeight" defaultValue="none" class="col-span-2" />
</div>
</div>
</PopoverContent>
</PopoverPositioner>
</PopoverRoot>
` }}
</PreviewCode>
</div>
</template>
-255
View File
@@ -1,255 +0,0 @@
<script lang="ts" setup>
import {
ProgressRoot,
ProgressLabel,
ProgressValueText,
ProgressCircle,
ProgressCircleTrack,
ProgressCircleRange,
} from "@/components/ui/progress-circular";
import {
Preview,
SectionTitle,
SectionContent,
InstallPackage,
PreviewCode,
} from "@/components/docs";
</script>
<template>
<div id="preview" class="-mt-20">
<Preview>
<template #preview>
<ProgressRoot :defaultValue="42">
<ProgressLabel>Progress Circular</ProgressLabel>
<ProgressCircle class="max-w-48">
<ProgressCircleTrack />
<ProgressCircleRange />
</ProgressCircle>
<ProgressValueText />
</ProgressRoot>
</template>
<template #code>
<PreviewCode>
{{ `
<ProgressRoot :defaultValue="42">
<ProgressLabel>Progress Circular</ProgressLabel>
<ProgressCircle class="max-w-48">
<ProgressCircleTrack />
<ProgressCircleRange />
</ProgressCircle>
<ProgressValueText />
</ProgressRoot>
` }}
</PreviewCode>
</template>
</Preview>
</div>
<div id="installation">
<SectionTitle>Installation</SectionTitle>
<SectionContent>Install the following dependencies:</SectionContent>
<InstallPackage>add @zag-js/vue @zag-js/progress</InstallPackage>
<SectionContent>
Copy and paste the following code into your project.
</SectionContent>
<PreviewCode title="components/ui/progress-circular/ProgressCircle.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { progressCircle } from "@mykopkb/core/styles/progress-circular.styles";
import type { Api } from "@zag-js/progress";
import { inject } from "vue";
const { class: className, ...props } = defineProps<{
class?: string;
}>();
const api = inject<Api>("progressApi");
</script>
<template>
<svg :class="cn(progressCircle, className)" v-bind="{ ...api?.getCircleProps(), ...props, ...$attrs }">
<slot />
</svg>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/progress-circular/ProgressCircleRange.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { progressCircleRange } from "@mykopkb/core/styles/progress-circular.styles";
import type { Api } from "@zag-js/progress";
import { inject } from "vue";
const { class: className, ...props } = defineProps<{
class?: string;
}>();
const api = inject<Api>("progressApi");
</script>
<template>
<circle :class="cn(progressCircleRange, className)"
v-bind="{ ...api?.getCircleRangeProps(), ...props, ...$attrs }" />
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/progress-circular/ProgressCircleTrack.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { progressCircleTrack } from "@mykopkb/core/styles/progress-circular.styles";
import type { Api } from "@zag-js/progress";
import { inject } from "vue";
const { class: className, ...props } = defineProps<{
class?: string;
}>();
const api = inject<Api>("progressApi");
</script>
<template>
<circle :class="cn(progressCircleTrack, className)"
v-bind="{ ...api?.getCircleTrackProps(), ...props, ...$attrs }" />
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/progress-circular/ProgressLabel.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { progressLabel } from "@mykopkb/core/styles/progress-circular.styles";
import { Label } from "@/components/ui/label";
import { Slot } from "@/components/ui/slot";
import type { Api } from "@zag-js/progress";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("progressApi");
</script>
<template>
<Slot v-bind="{ ...api?.getLabelProps(), ...props, ...$attrs }">
<Label v-if="!asChild" :class="cn(progressLabel, className)">
<slot />
</Label>
<slot v-else />
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/progress-circular/ProgressRoot.vue">
{{`
<script lang="ts" setup>
import * as progress from "@zag-js/progress";
import type { Props } from "@zag-js/progress";
import { Slot } from "@/components/ui/slot";
import { cn } from "@mykopkb/core/utils/cn";
import { normalizeProps, useMachine } from "@zag-js/vue";
import { computed, provide } from "vue";
import { progressRoot } from "@mykopkb/core/styles/progress-circular.styles";
const {
class: className,
asChild = false,
...props
} = defineProps<Partial<Props> & { class?: string; asChild?: boolean }>();
const service = useMachine(progress.machine, {
...props,
id: crypto.randomUUID(),
});
const api = computed(() => progress.connect(service, normalizeProps));
provide("progressApi", api);
</script>
<template>
<Slot :class="cn(progressRoot, className)" v-bind="{ ...props, ...$attrs, ...api.getRootProps() }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/progress-circular/ProgressValueText.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { progressValueText } from "@mykopkb/core/styles/progress-circular.styles";
import type { Api } from "@zag-js/progress";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("progressApi");
</script>
<template>
<div :class="cn(progressValueText, className)" v-bind="{ ...api?.getValueTextProps(), ...props, ...$attrs }">
\{\{ api?.valueAsString \}\}
</div>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/progress-circular/index.ts">
{{ `
export { default as ProgressCircle } from "./ProgressCircle.vue";
export { default as ProgressCircleRange } from "./ProgressCircleRange.vue";
export { default as ProgressCircleTrack } from "./ProgressCircleTrack.vue";
export { default as ProgressLabel } from "./ProgressLabel.vue";
export { default as ProgressRoot } from "./ProgressRoot.vue";
export { default as ProgressValueText } from "./ProgressValueText.vue";
` }}
</PreviewCode>
<SectionContent>
Update the import paths to match your project setup.
</SectionContent>
</div>
<div id="usage">
<SectionTitle>Usage</SectionTitle>
<PreviewCode>
{{ `
import {
ProgressRoot,
ProgressLabel,
ProgressValueText,
ProgressCircle,
ProgressCircleTrack,
ProgressCircleRange,
} from "@/components/ui/progress-circular";
` }}
</PreviewCode>
<PreviewCode>
{{ `
<ProgressRoot :defaultValue="42">
<ProgressLabel>Progress Circular</ProgressLabel>
<ProgressCircle class="max-w-48">
<ProgressCircleTrack />
<ProgressCircleRange />
</ProgressCircle>
<ProgressValueText />
</ProgressRoot>
` }}
</PreviewCode>
</div>
</template>
-227
View File
@@ -1,227 +0,0 @@
<script lang="ts" setup>
import {
ProgressRoot,
ProgressLabel,
ProgressValueText,
ProgressTrack,
ProgressRange,
} from "@/components/ui/progress-linear";
import {
Preview,
SectionTitle,
SectionContent,
InstallPackage,
PreviewCode,
} from "@/components/docs";
</script>
<template>
<div id="preview" class="-mt-20">
<Preview>
<template #preview>
<ProgressRoot :defaultValue="42">
<ProgressLabel>Progress Linear</ProgressLabel>
<ProgressTrack class="max-w-72">
<ProgressRange />
</ProgressTrack>
<ProgressValueText />
</ProgressRoot>
</template>
<template #code>
<PreviewCode>
{{ `
<ProgressRoot :defaultValue="42">
<ProgressLabel>Progress Linear</ProgressLabel>
<ProgressTrack class="max-w-72">
<ProgressRange />
</ProgressTrack>
<ProgressValueText />
</ProgressRoot>
` }}
</PreviewCode>
</template>
</Preview>
</div>
<div id="installation">
<SectionTitle>Installation</SectionTitle>
<SectionContent>Install the following dependencies:</SectionContent>
<InstallPackage>add @zag-js/vue @zag-js/progress</InstallPackage>
<SectionContent>
Copy and paste the following code into your project.
</SectionContent>
<PreviewCode title="components/ui/progress-linear/ProgressLabel.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { progressLabel } from "@mykopkb/core/styles/progress-linear.styles";
import { Label } from "@/components/ui/label";
import { Slot } from "@/components/ui/slot";
import type { Api } from "@zag-js/progress";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("progressApi");
</script>
<template>
<Slot v-bind="{ ...api?.getLabelProps(), ...props, ...$attrs }">
<Label v-if="!asChild" :class="cn(progressLabel, className)">
<slot />
</Label>
<slot v-else />
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/progress-linear/ProgressRange.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { progressRange } from "@mykopkb/core/styles/progress-linear.styles";
import type { Api } from "@zag-js/progress";
import { inject } from "vue";
const { class: className, ...props } = defineProps<{
class?: string;
}>();
const api = inject<Api>("progressApi");
</script>
<template>
<div :class="cn(progressRange, className)" v-bind="{ ...api?.getRangeProps(), ...props, ...$attrs }" />
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/progress-linear/ProgressRoot.vue">
{{`
<script lang="ts" setup>
import * as progress from "@zag-js/progress";
import type { Props } from "@zag-js/progress";
import { Slot } from "@/components/ui/slot";
import { cn } from "@mykopkb/core/utils/cn";
import { normalizeProps, useMachine } from "@zag-js/vue";
import { computed, provide } from "vue";
import { progressRoot } from "@mykopkb/core/styles/progress-linear.styles";
const {
class: className,
asChild = false,
...props
} = defineProps<Partial<Props> & { class?: string; asChild?: boolean }>();
const service = useMachine(progress.machine, {
...props,
id: crypto.randomUUID(),
});
const api = computed(() => progress.connect(service, normalizeProps));
provide("progressApi", api);
</script>
<template>
<Slot :class="cn(progressRoot, className)" v-bind="{ ...props, ...$attrs, ...api.getRootProps() }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/progress-linear/ProgressTrack.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { progressTrack } from "@mykopkb/core/styles/progress-linear.styles";
import type { Api } from "@zag-js/progress";
import { inject } from "vue";
const { class: className, ...props } = defineProps<{
class?: string;
}>();
const api = inject<Api>("progressApi");
</script>
<template>
<div :class="cn(progressTrack, className)" v-bind="{ ...api?.getTrackProps(), ...props, ...$attrs }">
<slot />
</div>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/progress-linear/ProgressValueText.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { progressValueText } from "@mykopkb/core/styles/progress-linear.styles";
import type { Api } from "@zag-js/progress";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("progressApi");
</script>
<template>
<div :class="cn(progressValueText, className)" v-bind="{ ...api?.getValueTextProps(), ...props, ...$attrs }">
\{\{ api?.valueAsString \}\}
</div>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/progress-linear/index.ts">
{{ `
export { default as ProgressLabel } from "./ProgressLabel.vue";
export { default as ProgressRange } from "./ProgressRange.vue";
export { default as ProgressRoot } from "./ProgressRoot.vue";
export { default as ProgressTrack } from "./ProgressTrack.vue";
export { default as ProgressValueText } from "./ProgressValueText.vue";
` }}
</PreviewCode>
<SectionContent>
Update the import paths to match your project setup.
</SectionContent>
</div>
<div id="usage">
<SectionTitle>Usage</SectionTitle>
<PreviewCode>
{{ `
import {
ProgressRoot,
ProgressLabel,
ProgressValueText,
ProgressTrack,
ProgressRange,
} from "@/components/ui/progress-linear";
` }}
</PreviewCode>
<PreviewCode>
{{ `
<ProgressRoot :defaultValue="42">
<ProgressLabel>Progress Linear</ProgressLabel>
<ProgressTrack class="max-w-72">
<ProgressRange />
</ProgressTrack>
<ProgressValueText />
</ProgressRoot>
` }}
</PreviewCode>
</div>
</template>
-325
View File
@@ -1,325 +0,0 @@
<script lang="ts" setup>
import {
RadioGroupRoot,
RadioGroupLabel,
RadioGroupItem,
RadioGroupItemText,
RadioGroupItemControl,
} from "@/components/ui/radio-group";
import {
Preview,
SectionTitle,
SectionContent,
InstallPackage,
PreviewCode,
} from "@/components/docs";
const frameworks = ["React", "Solid", "Vue", "Svelte"];
</script>
<template>
<div id="preview" class="-mt-20">
<Preview>
<template #preview>
<RadioGroupRoot defaultValue="React">
<RadioGroupLabel>Framework</RadioGroupLabel>
<RadioGroupItem v-for="framework in frameworks" :key="framework" :value="framework">
<RadioGroupItemControl />
<RadioGroupItemText>{{ framework }}</RadioGroupItemText>
</RadioGroupItem>
</RadioGroupRoot>
</template>
<template #code>
<PreviewCode>
{{ `
const frameworks = ["React", "Solid", "Vue", "Svelte"];
<RadioGroupRoot defaultValue="React">
<RadioGroupLabel>Framework</RadioGroupLabel>
<RadioGroupItem v-for="framework in frameworks" :key="framework" :value="framework">
<RadioGroupItemControl />
<RadioGroupItemText>\{\{ framework \}\}</RadioGroupItemText>
</RadioGroupItem>
</RadioGroupRoot>
` }}
</PreviewCode>
</template>
</Preview>
</div>
<div id="installation">
<SectionTitle>Installation</SectionTitle>
<SectionContent>Install the following dependencies:</SectionContent>
<InstallPackage>add @zag-js/vue @zag-js/radio-group</InstallPackage>
<SectionContent>
Copy and paste the following code into your project.
</SectionContent>
<PreviewCode title="components/ui/radio-group/RadioGroupRoot.vue">
{{`
<script lang="ts" setup>
import * as radioGroup from "@zag-js/radio-group";
import type { Props } from "@zag-js/radio-group";
import { Slot } from "@/components/ui/slot";
import { cn } from "@mykopkb/core/utils/cn";
import { normalizeProps, useMachine } from "@zag-js/vue";
import { computed, provide } from "vue";
import { radioGroupRoot } from "@mykopkb/core/styles/radio-group.styles";
import { Dot } from "@lucide/vue";
import { RadioGroupIndicator } from ".";
const {
class: className,
asChild = false,
...props
} = defineProps<Partial<Props> & { class?: string; asChild?: boolean }>();
const service = useMachine(radioGroup.machine, {
...props,
id: crypto.randomUUID(),
});
const api = computed(() => radioGroup.connect(service, normalizeProps));
provide("radioGroupApi", api);
</script>
<template>
<Slot :class="cn(radioGroupRoot, className)" v-bind="{ ...props, ...$attrs, ...api.getRootProps() }">
<slot v-if="asChild" />
<div v-else>
<slot />
<RadioGroupIndicator>
<Dot />
</RadioGroupIndicator>
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/radio-group/RadioGroupLabel.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { radioGroupLabel } from "@mykopkb/core/styles/radio-group.styles";
import { label } from "@mykopkb/core/styles/label.styles";
import { Slot } from "@/components/ui/slot";
import type { Api } from "@zag-js/radio-group";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("radioGroupApi");
</script>
<template>
<Slot :class="cn([label, radioGroupLabel, className])"
v-bind="{ ...api?.getLabelProps(), ...props, ...$attrs }">
<slot v-if="asChild" />
<span v-else>
<slot />
</span>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/radio-group/RadioGroupIndicator.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { radioGroupIndicator } from "@mykopkb/core/styles/radio-group.styles";
import { Slot } from "@/components/ui/slot";
import type { Api } from "@zag-js/radio-group";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("radioGroupApi");
</script>
<template>
<Slot :class="cn(radioGroupIndicator, className)" v-bind="{ ...api?.getIndicatorProps(), ...props, ...$attrs }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/radio-group/RadioGroupItem.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { radioGroupItem } from "@mykopkb/core/styles/radio-group.styles";
import { Slot } from "@/components/ui/slot";
import { RadioGroupItemHiddenInput } from ".";
import type { Api, ItemProps } from "@zag-js/radio-group";
import { provide, inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<
ItemProps & {
class?: string;
asChild?: boolean;
}
>();
const api = inject<Api>("radioGroupApi");
provide("radioGroupItem", props);
</script>
<template>
<Slot :class="cn(radioGroupItem, className)" v-bind="{ ...api?.getItemProps(props), ...props, ...$attrs }">
<slot v-if="asChild" />
<label v-else>
<slot />
<RadioGroupItemHiddenInput />
</label>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/radio-group/RadioGroupItemText.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { radioGroupItemText } from "@mykopkb/core/styles/radio-group.styles";
import { Slot } from "@/components/ui/slot";
import type { Api, ItemProps } from "@zag-js/radio-group";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("radioGroupApi");
const itemProps = inject<ItemProps>("radioGroupItem");
</script>
<template>
<Slot :class="cn(radioGroupItemText, className)"
v-bind="{ ...api?.getItemTextProps(itemProps!), ...props, ...$attrs }">
<slot v-if="asChild" />
<span v-else>
<slot />
</span>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/radio-group/RadioGroupItemControl.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { radioGroupItemControl } from "@mykopkb/core/styles/radio-group.styles";
import { Slot } from "@/components/ui/slot";
import type { Api, ItemProps } from "@zag-js/radio-group";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("radioGroupApi");
const itemProps = inject<ItemProps>("radioGroupItem");
</script>
<template>
<Slot :class="cn(radioGroupItemControl, className)"
v-bind="{ ...api?.getItemControlProps(itemProps!), ...props, ...$attrs }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/radio-group/RadioGroupItemHiddenInput.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { radioGroupItemHiddenInput } from "@mykopkb/core/styles/radio-group.styles";
import type { Api, ItemProps } from "@zag-js/radio-group";
import { inject } from "vue";
const { class: className, ...props } = defineProps<{
class?: string;
}>();
const api = inject<Api>("radioGroupApi");
const itemProps = inject<ItemProps>("radioGroupItem");
</script>
<template>
<input :class="cn(radioGroupItemHiddenInput, className)"
v-bind="{ ...api?.getItemHiddenInputProps(itemProps!), ...props, ...$attrs }" />
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/radio-group/index.ts">
{{ `
export { default as RadioGroupRoot } from "./RadioGroupRoot.vue";
export { default as RadioGroupLabel } from "./RadioGroupLabel.vue";
export { default as RadioGroupIndicator } from "./RadioGroupIndicator.vue";
export { default as RadioGroupItem } from "./RadioGroupItem.vue";
export { default as RadioGroupItemText } from "./RadioGroupItemText.vue";
export { default as RadioGroupItemControl } from "./RadioGroupItemControl.vue";
export { default as RadioGroupItemHiddenInput } from "./RadioGroupItemHiddenInput.vue";
` }}
</PreviewCode>
<SectionContent>
Update the import paths to match your project setup.
</SectionContent>
</div>
<div id="usage">
<SectionTitle>Usage</SectionTitle>
<PreviewCode>
{{ `
import {
RadioGroupRoot,
RadioGroupLabel,
RadioGroupItem,
RadioGroupItemText,
RadioGroupItemControl,
} from "@/components/ui/radio-group";
` }}
</PreviewCode>
<PreviewCode>
{{ `
<RadioGroupRoot defaultValue="React">
<RadioGroupLabel>Framework</RadioGroupLabel>
<RadioGroupItem v-for="framework in frameworks" :key="framework" :value="framework">
<RadioGroupItemControl />
<RadioGroupItemText>\{\{ framework \}\}</RadioGroupItemText>
</RadioGroupItem>
</RadioGroupRoot>
` }}
</PreviewCode>
</div>
</template>
-259
View File
@@ -1,259 +0,0 @@
<script lang="ts" setup>
import {
Preview,
SectionTitle,
SectionContent,
InstallPackage,
PreviewCode,
} from "@/components/docs";
import {
ScrollAreaRoot,
ScrollAreaViewport,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaCorner,
} from "@/components/ui/scroll-area";
</script>
<template>
<div id="preview" class="-mt-20">
<Preview>
<template #preview>
<ScrollAreaRoot class="h-72 w-70">
<ScrollAreaViewport>
<ScrollAreaContent>
<div class="text-base font-medium mb-4">Scroll Area Example</div>
<div v-for="i in 20" :key="i" class="mb-4 last:mb-0 opacity-80">
This is line number {{ i }} of the scrollable content. It helps
demonstrate how the custom scrollbar works within the Midone UI
system.
</div>
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
<ScrollAreaCorner />
</ScrollAreaRoot>
</template>
<template #code>
<PreviewCode>
{{ `
<script setup lang="ts">
import {
ScrollAreaRoot,
ScrollAreaViewport,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaCorner,
} from "@/components/ui/scroll-area";
</script>
<template>
<ScrollAreaRoot class="h-72 w-70">
<ScrollAreaViewport>
<ScrollAreaContent>
<div class="text-base font-medium mb-4">Scroll Area Example</div>
<div v-for="i in 20" :key="i" class="mb-4 last:mb-0 opacity-80">
This is line number \{\{ i \}\} of the scrollable content. It helps
demonstrate how the custom scrollbar works within the Midone UI
system.
</div>
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
<ScrollAreaCorner />
</ScrollAreaRoot>
</template>
` }}
</PreviewCode>
</template>
</Preview>
</div>
<div id="installation">
<SectionTitle>Installation</SectionTitle>
<SectionContent>Install the following dependencies:</SectionContent>
<InstallPackage>add @zag-js/vue @zag-js/scroll-area</InstallPackage>
<SectionContent>
Copy and paste the following code into your project.
</SectionContent>
<PreviewCode title="components/ui/scroll-area/ScrollAreaRoot.vue">
{{`
<script lang="ts" setup>
import * as scrollArea from "@zag-js/scroll-area";
import type { Props } from "@zag-js/scroll-area";
import { useMachine, normalizeProps } from "@zag-js/vue";
import { cn } from "@mykopkb/core/utils/cn";
import { computed, provide } from "vue";
import { scrollAreaRoot } from "@mykopkb/core/styles/scroll-area.styles";
const { class: className, ...props } = defineProps<
Partial<Props> & {
class?: string;
}
>();
const service = useMachine(scrollArea.machine, {
...props,
id: crypto.randomUUID(),
});
const api = computed(() => scrollArea.connect(service, normalizeProps));
provide("scrollAreaApi", api);
</script>
<template>
<div v-bind="{ ...api.getRootProps() }" :class="cn(scrollAreaRoot, className)">
<slot />
</div>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/scroll-area/ScrollAreaViewport.vue">
{{ `
<script lang="ts" setup>
import type { Api } from "@zag-js/scroll-area";
import { inject } from "vue";
import { cn } from "@mykopkb/core/utils/cn";
import { scrollAreaViewport } from "@mykopkb/core/styles/scroll-area.styles";
const { class: className, ...props } = defineProps<{ class?: string }>();
const api = inject<Api<any>>("scrollAreaApi");
</script>
<template>
<div v-bind="{ ...api?.getViewportProps(), ...props }" :class="cn(scrollAreaViewport, className)">
<slot />
</div>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/scroll-area/ScrollAreaContent.vue">
{{ `
<script lang="ts" setup>
import type { Api } from "@zag-js/scroll-area";
import { inject } from "vue";
import { cn } from "@mykopkb/core/utils/cn";
import { scrollAreaContent } from "@mykopkb/core/styles/scroll-area.styles";
const { class: className, ...props } = defineProps<{ class?: string }>();
const api = inject<Api<any>>("scrollAreaApi");
</script>
<template>
<div v-bind="{ ...api?.getContentProps(), ...props }" :class="cn(scrollAreaContent, className)">
<slot />
</div>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/scroll-area/ScrollAreaScrollbar.vue">
{{ `
<script lang="ts" setup>
import type { Api, ScrollbarProps } from "@zag-js/scroll-area";
import { inject } from "vue";
import { cn } from "@mykopkb/core/utils/cn";
import { scrollAreaScrollbar } from "@mykopkb/core/styles/scroll-area.styles";
const { class: className, ...props } = defineProps<
ScrollbarProps & { class?: string }
>();
const api = inject<Api<any>>("scrollAreaApi");
</script>
<template>
<div v-bind="{ ...api?.getScrollbarProps(), ...props }" :class="cn(scrollAreaScrollbar, className)">
<slot />
</div>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/scroll-area/ScrollAreaThumb.vue">
{{ `
<script lang="ts" setup>
import type { Api, ScrollbarProps } from "@zag-js/scroll-area";
import { inject } from "vue";
import { cn } from "@mykopkb/core/utils/cn";
import { scrollAreaThumb } from "@mykopkb/core/styles/scroll-area.styles";
const { class: className, ...props } = defineProps<
ScrollbarProps & { class?: string }
>();
const api = inject<Api<any>>("scrollAreaApi");
</script>
<template>
<div v-bind="{ ...api?.getThumbProps(props), ...props }" :class="cn(scrollAreaThumb, className)" />
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/scroll-area/ScrollAreaCorner.vue">
{{ `
<script lang="ts" setup>
import type { Api } from "@zag-js/scroll-area";
import { inject } from "vue";
import { cn } from "@mykopkb/core/utils/cn";
import { scrollAreaCorner } from "@mykopkb/core/styles/scroll-area.styles";
const { class: className, ...props } = defineProps<{ class?: string }>();
const api = inject<Api<any>>("scrollAreaApi");
</script>
<template>
<div v-bind="{ ...api?.getCornerProps(), ...props }" :class="cn(scrollAreaCorner, className)" />
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/scroll-area/index.ts">
{{ `
export { default as ScrollAreaRoot } from "./ScrollAreaRoot.vue";
export { default as ScrollAreaViewport } from "./ScrollAreaViewport.vue";
export { default as ScrollAreaContent } from "./ScrollAreaContent.vue";
export { default as ScrollAreaScrollbar } from "./ScrollAreaScrollbar.vue";
export { default as ScrollAreaThumb } from "./ScrollAreaThumb.vue";
export { default as ScrollAreaCorner } from "./ScrollAreaCorner.vue";
` }}
</PreviewCode>
</div>
<div id="usage">
<SectionTitle>Usage</SectionTitle>
<PreviewCode>
{{ `
import {
ScrollAreaRoot,
ScrollAreaViewport,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaCorner,
} from "@/components/ui/scroll-area";
` }}
</PreviewCode>
<PreviewCode>
{{ `
<ScrollAreaRoot class="h-72 w-70">
<ScrollAreaViewport>
<ScrollAreaContent>
<!-- Scrollable content here -->
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
<ScrollAreaCorner />
</ScrollAreaRoot>
` }}
</PreviewCode>
</div>
</template>
-886
View File
@@ -1,886 +0,0 @@
<script lang="ts" setup>
import {
SelectRoot,
SelectLabel,
SelectControl,
SelectTrigger,
SelectValueText,
SelectContent,
SelectItemGroup,
SelectItemGroupLabel,
SelectItem,
SelectItemText,
} from "@/components/ui/select";
import * as select from "@zag-js/select";
import {
Preview,
SectionTitle,
SectionContent,
InstallPackage,
PreviewCode,
} from "@/components/docs";
const comboboxData = [
{ label: "React", code: "react" },
{ label: "Solid", code: "solid" },
{ label: "Vue", code: "vue" },
{ label: "Svelte", code: "svelte" },
];
const timezoneData = [
{
label: "North America",
items: [
{ value: "est", label: "Eastern Standard Time (EST)" },
{ value: "cst", label: "Central Standard Time (CST)" },
{ value: "mst", label: "Mountain Standard Time (MST)" },
{ value: "pst", label: "Pacific Standard Time (PST)" },
{ value: "akst", label: "Alaska Standard Time (AKST)" },
{ value: "hst", label: "Hawaii Standard Time (HST)" },
],
},
{
label: "Europe & Africa",
items: [
{ value: "gmt", label: "Greenwich Mean Time (GMT)" },
{ value: "cet", label: "Central European Time (CET)" },
{ value: "eet", label: "Eastern European Time (EET)" },
{ value: "west", label: "Western European Summer Time (WEST)" },
{ value: "cat", label: "Central Africa Time (CAT)" },
{ value: "eat", label: "East Africa Time (EAT)" },
],
},
{
label: "Asia",
items: [
{ value: "msk", label: "Moscow Time (MSK)" },
{ value: "ist", label: "India Standard Time (IST)" },
{ value: "cst_china", label: "China Standard Time (CST)" },
{ value: "jst", label: "Japan Standard Time (JST)" },
{ value: "kst", label: "Korea Standard Time (KST)" },
{
value: "ist_indonesia",
label: "Indonesia Central Standard Time (WITA)",
},
],
},
{
label: "Australia & Pacific",
items: [
{ value: "awst", label: "Australian Western Standard Time (AWST)" },
{ value: "acst", label: "Australian Central Standard Time (ACST)" },
{ value: "aest", label: "Australian Eastern Standard Time (AEST)" },
{ value: "nzst", label: "New Zealand Standard Time (NZST)" },
{ value: "fjt", label: "Fiji Time (FJT)" },
],
},
{
label: "South America",
items: [
{ value: "art", label: "Argentina Time (ART)" },
{ value: "bot", label: "Bolivia Time (BOT)" },
{ value: "brt", label: "Brasilia Time (BRT)" },
{ value: "clt", label: "Chile Standard Time (CLT)" },
],
},
];
const collection = select.collection({
items: comboboxData,
itemToValue: (item) => item.label,
});
const collectionTimezone = select.collection({
items: timezoneData.flatMap((region) =>
region.items.map((item) => ({
region: region.label,
value: item.value,
label: item.label,
}))
),
});
</script>
<template>
<div id="preview" class="-mt-20">
<Preview>
<template #preview>
<SelectRoot class="w-56" :collection="collection">
<SelectLabel>Single</SelectLabel>
<SelectControl>
<SelectTrigger>
<SelectValueText placeholder="Select a Framework" />
</SelectTrigger>
</SelectControl>
<SelectContent>
<SelectItemGroup>
<SelectItemGroupLabel> Frameworks </SelectItemGroupLabel>
<SelectItem v-for="item in collection.items" :key="item.code" :item="item">
<SelectItemText>{{ item.label }}</SelectItemText>
</SelectItem>
</SelectItemGroup>
</SelectContent>
</SelectRoot>
</template>
<template #code>
<PreviewCode>
{{`
const comboboxData = [
{ label: "React", code: "react" },
{ label: "Solid", code: "solid" },
{ label: "Vue", code: "vue" },
{ label: "Svelte", code: "svelte" },
];
const timezoneData = [
{
label: "North America",
items: [
{ value: "est", label: "Eastern Standard Time (EST)" },
{ value: "cst", label: "Central Standard Time (CST)" },
{ value: "mst", label: "Mountain Standard Time (MST)" },
{ value: "pst", label: "Pacific Standard Time (PST)" },
{ value: "akst", label: "Alaska Standard Time (AKST)" },
{ value: "hst", label: "Hawaii Standard Time (HST)" },
],
},
{
label: "Europe & Africa",
items: [
{ value: "gmt", label: "Greenwich Mean Time (GMT)" },
{ value: "cet", label: "Central European Time (CET)" },
{ value: "eet", label: "Eastern European Time (EET)" },
{ value: "west", label: "Western European Summer Time (WEST)" },
{ value: "cat", label: "Central Africa Time (CAT)" },
{ value: "eat", label: "East Africa Time (EAT)" },
],
},
{
label: "Asia",
items: [
{ value: "msk", label: "Moscow Time (MSK)" },
{ value: "ist", label: "India Standard Time (IST)" },
{ value: "cst_china", label: "China Standard Time (CST)" },
{ value: "jst", label: "Japan Standard Time (JST)" },
{ value: "kst", label: "Korea Standard Time (KST)" },
{
value: "ist_indonesia",
label: "Indonesia Central Standard Time (WITA)",
},
],
},
{
label: "Australia & Pacific",
items: [
{ value: "awst", label: "Australian Western Standard Time (AWST)" },
{ value: "acst", label: "Australian Central Standard Time (ACST)" },
{ value: "aest", label: "Australian Eastern Standard Time (AEST)" },
{ value: "nzst", label: "New Zealand Standard Time (NZST)" },
{ value: "fjt", label: "Fiji Time (FJT)" },
],
},
{
label: "South America",
items: [
{ value: "art", label: "Argentina Time (ART)" },
{ value: "bot", label: "Bolivia Time (BOT)" },
{ value: "brt", label: "Brasilia Time (BRT)" },
{ value: "clt", label: "Chile Standard Time (CLT)" },
],
},
];
const collection = select.collection({
items: comboboxData,
itemToValue: (item) => item.label,
});
const collectionTimezone = select.collection({
items: timezoneData.flatMap((region) =>
region.items.map((item) => ({
region: region.label,
value: item.value,
label: item.label,
}))
),
});
<SelectRoot class="w-56" :collection="collection">
<SelectLabel>Single</SelectLabel>
<SelectControl>
<SelectTrigger>
<SelectValueText placeholder="Select a Framework" />
</SelectTrigger>
</SelectControl>
<SelectContent>
<SelectItemGroup>
<SelectItemGroupLabel> Frameworks </SelectItemGroupLabel>
<SelectItem v-for="item in collection.items" :key="item.code" :item="item">
<SelectItemText>\{\{ item.label \}\}</SelectItemText>
</SelectItem>
</SelectItemGroup>
</SelectContent>
</SelectRoot>
` }}
</PreviewCode>
</template>
</Preview>
</div>
<div id="installation">
<SectionTitle>Installation</SectionTitle>
<SectionContent>Install the following dependencies:</SectionContent>
<InstallPackage>add @zag-js/vue @zag-js/select</InstallPackage>
<SectionContent>
Copy and paste the following code into your project.
</SectionContent>
<PreviewCode title="components/ui/select/SelectClearTrigger.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { selectClearTrigger } from "@mykopkb/core/styles/select.styles";
import { Slot } from "@/components/ui/slot";
import type { Api } from "@zag-js/select";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("selectApi");
</script>
<template>
<Slot v-bind="{ ...api?.getClearTriggerProps(), ...props, ...$attrs }">
<slot v-if="asChild" />
<span v-else :class="cn(selectClearTrigger, className)">
<slot />
</span>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/select/SelectContent.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { selectContent } from "@mykopkb/core/styles/select.styles";
import { Slot } from "@/components/ui/slot";
import { Box } from "@/components/ui/box";
import { SelectPositioner } from ".";
import type { Api } from "@zag-js/select";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("selectApi");
</script>
<template>
<Teleport to="body">
<SelectPositioner>
<Slot v-bind="{ ...api?.getContentProps(), ...props, ...$attrs }">
<slot v-if="asChild" />
<Box v-else raised="single" :class="cn(selectContent, className)">
<div>
<slot />
</div>
</Box>
</Slot>
</SelectPositioner>
</Teleport>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/select/SelectControl.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { selectControl } from "@mykopkb/core/styles/select.styles";
import { Slot } from "@/components/ui/slot";
import type { Api } from "@zag-js/select";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("selectApi");
</script>
<template>
<Slot :class="cn(selectControl, className)" v-bind="{ ...api?.getControlProps(), ...props, ...$attrs }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/select/SelectHiddenSelect.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { selectHiddenSelect } from "@mykopkb/core/styles/select.styles";
import type { Api } from "@zag-js/select";
import { inject } from "vue";
const { class: className, ...props } = defineProps<{
class?: string;
}>();
const api = inject<Api>("selectApi");
</script>
<template>
<select :class="cn(selectHiddenSelect, className)"
v-bind="{ ...api?.getHiddenSelectProps(), ...props, ...$attrs }" />
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/select/SelectIndicator.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { selectIndicator } from "@mykopkb/core/styles/select.styles";
import { ChevronDownIcon } from "@lucide/vue";
import { Slot } from "@/components/ui/slot";
import type { Api } from "@zag-js/select";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("selectApi");
</script>
<template>
<Slot :class="cn(selectIndicator, className)" v-bind="{ ...api?.getIndicatorProps(), ...props, ...$attrs }">
<slot v-if="asChild" />
<div v-else>
<slot v-if="$slots.default" />
<ChevronDownIcon v-else class="size-3.5" />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/select/SelectItem.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { selectItem } from "@mykopkb/core/styles/select.styles";
import { Slot } from "@/components/ui/slot";
import { SelectItemIndicator } from ".";
import type { Api, ItemProps } from "@zag-js/select";
import { provide, inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<
ItemProps & {
class?: string;
asChild?: boolean;
}
>();
const api = inject<Api>("selectApi");
provide("selectItem", props);
</script>
<template>
<Slot :class="cn(selectItem, className)" v-bind="{ ...api?.getItemProps(props), ...props, ...$attrs }">
<slot v-if="asChild" />
<div v-else>
<slot />
<SelectItemIndicator />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/select/SelectItemGroup.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { selectItemGroup } from "@mykopkb/core/styles/select.styles";
import { Slot } from "@/components/ui/slot";
import type { Api } from "@zag-js/select";
import { provide, inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("selectApi");
const itemGroupId = { id: crypto.randomUUID() };
provide("selectItemGroup", props);
</script>
<template>
<Slot :class="cn(selectItemGroup, className)"
v-bind="{ ...api?.getItemGroupProps(itemGroupId), ...props, ...$attrs }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/select/SelectItemGroupLabel.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { selectItemGroupLabel } from "@mykopkb/core/styles/select.styles";
import { Slot } from "@/components/ui/slot";
import type { Api, ItemGroupProps } from "@zag-js/select";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("selectApi");
const itemGroupId = inject<ItemGroupProps>("selectItemGroup");
</script>
<template>
<Slot :class="cn(selectItemGroupLabel, className)" v-bind="{ ...api?.getItemGroupLabelProps({
htmlFor: itemGroupId?.id!,
}), ...props, ...$attrs }">
<slot v-if="asChild" />
<label v-else>
<slot />
</label>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/select/SelectItemIndicator.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { selectItemIndicator } from "@mykopkb/core/styles/select.styles";
import { Slot } from "@/components/ui/slot";
import type { Api, ItemProps } from "@zag-js/select";
import { Check } from "@lucide/vue";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("selectApi");
const item = inject<ItemProps>("selectItem");
</script>
<template>
<Slot :class="cn(selectItemIndicator, className)"
v-bind="{ ...api?.getItemIndicatorProps(item!), ...props, ...$attrs }">
<slot v-if="asChild" />
<div v-else>
<slot v-if="$slots.default" />
<Check v-else class="size-3.5" />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/select/SelectItemText.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { selectItemText } from "@mykopkb/core/styles/select.styles";
import { Slot } from "@/components/ui/slot";
import type { Api, ItemProps } from "@zag-js/select";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("selectApi");
const item = inject<ItemProps>("selectItem");
</script>
<template>
<Slot :class="cn(selectItemText, className)" v-bind="{ ...api?.getItemTextProps(item!), ...props, ...$attrs }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/select/SelectLabel.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { selectLabel } from "@mykopkb/core/styles/select.styles";
import { Slot } from "@/components/ui/slot";
import { Label } from "@/components/ui/label";
import type { Api } from "@zag-js/select";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("selectApi");
</script>
<template>
<Slot v-bind="{ ...api?.getLabelProps(), ...props, ...$attrs }">
<slot v-if="asChild" />
<Label v-else :class="cn(selectLabel, className)">
<slot />
</Label>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/select/SelectPositioner.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { selectPositioner } from "@mykopkb/core/styles/select.styles";
import { Slot } from "@/components/ui/slot";
import type { Api } from "@zag-js/select";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("selectApi");
</script>
<template>
<Slot :class="cn(selectPositioner, className)" v-bind="{ ...api?.getPositionerProps(), ...props, ...$attrs }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/select/SelectRoot.vue">
{{`
<script lang="ts" setup>
import * as select from "@zag-js/select";
import type { Props } from "@zag-js/select";
import { Slot } from "@/components/ui/slot";
import { cn } from "@mykopkb/core/utils/cn";
import { normalizeProps, useMachine } from "@zag-js/vue";
import { computed, provide } from "vue";
import { selectRoot } from "@mykopkb/core/styles/select.styles";
import { SelectHiddenSelect } from ".";
const {
class: className,
asChild = false,
multiple = undefined,
open = undefined,
closeOnSelect = undefined,
...props
} = defineProps<Partial<Props> & { class?: string; asChild?: boolean }>();
const service = useMachine(select.machine, {
...props,
multiple,
open,
closeOnSelect,
id: crypto.randomUUID(),
});
const api = computed(() => select.connect(service, normalizeProps));
provide("selectApi", api);
</script>
<template>
<Slot :class="cn(selectRoot, className)" :data-multiple="multiple"
v-bind="{ ...props, ...$attrs, ...api.getRootProps() }">
<slot v-if="asChild" />
<div v-else>
<slot />
<SelectHiddenSelect />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/select/SelectTrigger.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { selectTrigger } from "@mykopkb/core/styles/select.styles";
import { Slot } from "@/components/ui/slot";
import { Button } from "@/components/ui/button";
import { SelectClearTrigger, SelectIndicator } from ".";
import type { Api } from "@zag-js/select";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("selectApi");
</script>
<template>
<Slot v-bind="{ ...api?.getTriggerProps(), ...props, ...$attrs }">
<Button variant="ghost" v-if="!asChild" :class="cn(selectTrigger, className)">
<slot />
<SelectClearTrigger>Clear</SelectClearTrigger>
<SelectIndicator />
</Button>
<slot v-else />
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/select/SelectValueText.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { selectValueText } from "@mykopkb/core/styles/select.styles";
import { Slot } from "@/components/ui/slot";
import type { Api } from "@zag-js/select";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
placeholder?: string;
}>();
const api = inject<Api>("selectApi");
</script>
<template>
<Slot :class="cn(selectValueText, className)" v-bind="{ ...api?.getValueTextProps(), ...props, ...$attrs }">
<slot v-if="asChild" />
<div v-else>\{\{ api?.valueAsString || props.placeholder \}\}</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/select/index.ts">
{{ `
export { default as SelectClearTrigger } from "./SelectClearTrigger.vue";
export { default as SelectContent } from "./SelectContent.vue";
export { default as SelectControl } from "./SelectControl.vue";
export { default as SelectHiddenSelect } from "./SelectHiddenSelect.vue";
export { default as SelectIndicator } from "./SelectIndicator.vue";
export { default as SelectItem } from "./SelectItem.vue";
export { default as SelectItemGroup } from "./SelectItemGroup.vue";
export { default as SelectItemGroupLabel } from "./SelectItemGroupLabel.vue";
export { default as SelectItemIndicator } from "./SelectItemIndicator.vue";
export { default as SelectItemText } from "./SelectItemText.vue";
export { default as SelectLabel } from "./SelectLabel.vue";
export { default as SelectPositioner } from "./SelectPositioner.vue";
export { default as SelectRoot } from "./SelectRoot.vue";
export { default as SelectTrigger } from "./SelectTrigger.vue";
export { default as SelectValueText } from "./SelectValueText.vue";
` }}
</PreviewCode>
<SectionContent>
Update the import paths to match your project setup.
</SectionContent>
</div>
<div id="usage">
<SectionTitle>Usage</SectionTitle>
<PreviewCode>
{{ `
import {
SelectRoot,
SelectLabel,
SelectControl,
SelectTrigger,
SelectValueText,
SelectContent,
SelectItemGroup,
SelectItemGroupLabel,
SelectItem,
SelectItemText,
} from "@/components/ui/select";
` }}
</PreviewCode>
<PreviewCode>
{{ `
<SelectRoot class="w-56" :collection="collection">
<SelectLabel>Single</SelectLabel>
<SelectControl>
<SelectTrigger>
<SelectValueText placeholder="Select a Framework" />
</SelectTrigger>
</SelectControl>
<SelectContent>
<SelectItemGroup>
<SelectItemGroupLabel> Frameworks </SelectItemGroupLabel>
<SelectItem v-for="item in collection.items" :key="item.code" :item="item">
<SelectItemText>\{\{ item.label \}\}</SelectItemText>
</SelectItem>
</SelectItemGroup>
</SelectContent>
</SelectRoot>
` }}
</PreviewCode>
</div>
<div id="variants">
<SectionTitle>Variants</SectionTitle>
<SectionContent>A collection of components you can use.</SectionContent>
<Preview>
<template #preview>
<SelectRoot class="w-56" :collection="collection" multiple>
<SelectLabel>Multiple</SelectLabel>
<SelectControl>
<SelectTrigger>
<SelectValueText placeholder="Select a Framework" />
</SelectTrigger>
</SelectControl>
<SelectContent>
<SelectItemGroup>
<SelectItemGroupLabel> Frameworks </SelectItemGroupLabel>
<SelectItem v-for="item in collection.items" :key="item.code" :item="item">
<SelectItemText>{{ item.label }}</SelectItemText>
</SelectItem>
</SelectItemGroup>
</SelectContent>
</SelectRoot>
</template>
<template #code>
<PreviewCode>
{{ `
<SelectRoot class="w-56" :collection="collection" multiple>
<SelectLabel>Multiple</SelectLabel>
<SelectControl>
<SelectTrigger>
<SelectValueText placeholder="Select a Framework" />
</SelectTrigger>
</SelectControl>
<SelectContent>
<SelectItemGroup>
<SelectItemGroupLabel> Frameworks </SelectItemGroupLabel>
<SelectItem v-for="item in collection.items" :key="item.code" :item="item">
<SelectItemText>\{\{ item.label \}\}</SelectItemText>
</SelectItem>
</SelectItemGroup>
</SelectContent>
</SelectRoot>
` }}
</PreviewCode>
</template>
</Preview>
<Preview>
<template #preview>
<SelectRoot class="w-56" :collection="collectionTimezone" multiple>
<SelectLabel>Scrollable</SelectLabel>
<SelectControl>
<SelectTrigger>
<SelectValueText placeholder="Select a Timezone" />
</SelectTrigger>
</SelectControl>
<SelectContent>
<SelectItemGroup v-for="item in timezoneData" :key="item.label">
<SelectItemGroupLabel>{{ item.label }}</SelectItemGroupLabel>
<SelectItem v-for="timezoneItem in item.items" :key="timezoneItem.value" :item="timezoneItem.value">
<SelectItemText>{{ timezoneItem.label }}</SelectItemText>
</SelectItem>
</SelectItemGroup>
</SelectContent>
</SelectRoot>
</template>
<template #code>
<PreviewCode>
{{ `
<SelectRoot class="w-56" :collection="collectionTimezone" multiple>
<SelectLabel>Scrollable</SelectLabel>
<SelectControl>
<SelectTrigger>
<SelectValueText placeholder="Select a Timezone" />
</SelectTrigger>
</SelectControl>
<SelectContent>
<SelectItemGroup v-for="item in timezoneData" :key="item.label">
<SelectItemGroupLabel>\{\{ item.label \}\}</SelectItemGroupLabel>
<SelectItem v-for="timezoneItem in item.items" :key="timezoneItem.value" :item="timezoneItem.value">
<SelectItemText>\{\{ timezoneItem.label \}\}</SelectItemText>
</SelectItem>
</SelectItemGroup>
</SelectContent>
</SelectRoot>
` }}
</PreviewCode>
</template>
</Preview>
</div>
</template>
-508
View File
@@ -1,508 +0,0 @@
<script lang="ts" setup>
import {
SheetRoot,
SheetTrigger,
SheetContent,
SheetTitle,
SheetDescription,
SheetCloseTrigger,
} from "@/components/ui/sheet";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { SquareX, Save, ExternalLink } from "@lucide/vue";
import {
Preview,
SectionTitle,
SectionContent,
InstallPackage,
PreviewCode,
} from "@/components/docs";
</script>
<template>
<div id="preview" class="-mt-20">
<Preview>
<template #preview>
<SheetRoot>
<SheetTrigger>Open Sheet</SheetTrigger>
<SheetContent>
<SheetTitle>Sheet Title</SheetTitle>
<SheetDescription>
Make changes to your profile here. Click save when you're done.
</SheetDescription>
<div class="grid gap-4 mt-2">
<div class="grid gap-2.5">
<Label for="name-1">Name</Label>
<Input id="name-1" name="name" defaultValue="Pedro Duarte" />
</div>
<div class="grid gap-2.5">
<Label for="username-1">Username</Label>
<Input id="username-1" name="username" defaultValue="@peduarte" />
</div>
</div>
<div class="flex gap-2 justify-end mt-7">
<SheetCloseTrigger>
<SquareX />
Close
</SheetCloseTrigger>
<Button variant="primary">
<Save />
Submit
</Button>
</div>
<SheetCloseTrigger />
</SheetContent>
</SheetRoot>
</template>
<template #code>
<PreviewCode>
{{ `
<SheetRoot>
<SheetTrigger>Open Sheet</SheetTrigger>
<SheetContent>
<SheetTitle>Sheet Title</SheetTitle>
<SheetDescription>
Make changes to your profile here. Click save when you're done.
</SheetDescription>
<div class="grid gap-4 mt-2">
<div class="grid gap-2.5">
<Label for="name-1">Name</Label>
<Input id="name-1" name="name" defaultValue="Pedro Duarte" />
</div>
<div class="grid gap-2.5">
<Label for="username-1">Username</Label>
<Input id="username-1" name="username" defaultValue="@peduarte" />
</div>
</div>
<div class="flex gap-2 justify-end mt-7">
<SheetCloseTrigger>
<SquareX />
Close
</SheetCloseTrigger>
<Button variant="primary">
<Save />
Submit
</Button>
</div>
<SheetCloseTrigger />
</SheetContent>
</SheetRoot>
` }}
</PreviewCode>
</template>
</Preview>
</div>
<div id="installation">
<SectionTitle>Installation</SectionTitle>
<SectionContent>Install the following dependencies:</SectionContent>
<InstallPackage>add @zag-js/vue @zag-js/dialog</InstallPackage>
<SectionContent>
Copy and paste the following code into your project.
</SectionContent>
<PreviewCode title="components/ui/sheet/SheetRoot.vue">
{{`
<script lang="ts" setup>
import { provide, computed } from "vue";
import * as dialog from "@zag-js/dialog";
import type { Props } from "@zag-js/dialog";
import { useMachine, normalizeProps } from "@zag-js/vue";
const {
class: className,
asChild = false,
open = undefined,
closeOnInteractOutside = undefined,
...props
} = defineProps<
Partial<Props> & {
class?: string;
asChild?: boolean;
}
>();
const service = useMachine(dialog.machine, {
...props,
get open() {
return open;
},
closeOnInteractOutside,
id: crypto.randomUUID(),
});
const api = computed(() => dialog.connect(service, normalizeProps));
provide("sheetApi", api);
</script>
<template>
<slot />
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/sheet/SheetTrigger.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { sheetTrigger } from "@mykopkb/core/styles/sheet.styles";
import { Button } from "@/components/ui/button";
import type { Api } from "@zag-js/dialog";
import { inject } from "vue";
import { Slot } from "@/components/ui/slot";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("sheetApi");
</script>
<template>
<Slot v-bind="{ ...props, ...$attrs, ...api?.getTriggerProps() }">
<Button variant="secondary" look="outline" v-if="!asChild" :class="cn(sheetTrigger, className)">
<slot />
</Button>
<slot v-else />
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/sheet/SheetBackdrop.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { sheetBackdrop } from "@mykopkb/core/styles/sheet.styles";
import type { Api } from "@zag-js/dialog";
import { inject } from "vue";
import { Slot } from "@/components/ui/slot";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("sheetApi");
</script>
<template>
<Slot :class="cn(sheetBackdrop, className)" v-bind="{ ...props, ...$attrs, ...api?.getBackdropProps() }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/sheet/SheetPositioner.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { sheetPositioner } from "@mykopkb/core/styles/sheet.styles";
import type { Api } from "@zag-js/dialog";
import { inject } from "vue";
import { Slot } from "@/components/ui/slot";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("sheetApi");
</script>
<template>
<Slot :class="cn(sheetPositioner, className)" v-bind="{ ...props, ...$attrs, ...api?.getPositionerProps() }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/sheet/SheetContent.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { sheetContent } from "@mykopkb/core/styles/sheet.styles";
import { Box } from "@/components/ui/box";
import { SheetBackdrop, SheetPositioner } from "@/components/ui/sheet";
import type { Api } from "@zag-js/dialog";
import { inject } from "vue";
import { Slot } from "@/components/ui/slot";
const {
class: className,
asChild = false,
side = "right",
...props
} = defineProps<{
class?: string;
asChild?: boolean;
side?: "top" | "right" | "bottom" | "left";
}>();
const api = inject<Api>("sheetApi");
</script>
<template>
<Teleport to="body">
<SheetBackdrop />
<SheetPositioner>
<Slot v-bind="{ ...props, ...$attrs, ...api?.getContentProps() }">
<slot v-if="asChild" />
<div v-else>
<Box raised="double" :data-side="side" :class="cn(sheetContent, className)" v-bind="{ ...props }">
<div>
<slot />
</div>
</Box>
</div>
</Slot>
</SheetPositioner>
</Teleport>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/sheet/SheetTitle.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { sheetTitle } from "@mykopkb/core/styles/sheet.styles";
import type { Api } from "@zag-js/dialog";
import { inject } from "vue";
import { Slot } from "@/components/ui/slot";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("sheetApi");
</script>
<template>
<Slot :class="cn(sheetTitle, className)" v-bind="{ ...props, ...$attrs, ...api?.getTitleProps() }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/sheet/SheetDescription.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { sheetDescription } from "@mykopkb/core/styles/sheet.styles";
import type { Api } from "@zag-js/dialog";
import { inject } from "vue";
import { Slot } from "@/components/ui/slot";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("sheetApi");
</script>
<template>
<Slot :class="cn(sheetDescription, className)" v-bind="{ ...props, ...$attrs, ...api?.getDescriptionProps() }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/sheet/SheetCloseTrigger.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { sheetCloseTrigger } from "@mykopkb/core/styles/sheet.styles";
import { Button } from "@/components/ui/button";
import type { Api } from "@zag-js/dialog";
import {
buttonVariants,
type ButtonVariants,
} from "@mykopkb/core/styles/button.styles";
import { X } from "@lucide/vue";
import { inject } from "vue";
import { Slot } from "@/components/ui/slot";
const {
class: className,
look = "outline",
variant = "secondary",
size,
asChild = false,
...props
} = defineProps<
ButtonVariants & {
class?: string;
asChild?: boolean;
}
>();
const api = inject<Api>("sheetApi");
</script>
<template>
<Slot v-bind="{ ...props, ...$attrs, ...api?.getCloseTriggerProps() }">
<Button variant="ghost" v-if="!$slots.default" :class="cn(sheetCloseTrigger, className)"
v-bind="{ ...props }">
<X class="size-4" />
</Button>
<template v-else>
<slot v-if="asChild" />
<Button v-else :class="
cn(buttonVariants({ look, variant, size, className }), className)
">
<slot />
</Button>
</template>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/sheet/index.ts">
{{ `
export { default as SheetRoot } from "./SheetRoot.vue";
export { default as SheetTrigger } from "./SheetTrigger.vue";
export { default as SheetBackdrop } from "./SheetBackdrop.vue";
export { default as SheetPositioner } from "./SheetPositioner.vue";
export { default as SheetContent } from "./SheetContent.vue";
export { default as SheetTitle } from "./SheetTitle.vue";
export { default as SheetDescription } from "./SheetDescription.vue";
export { default as SheetCloseTrigger } from "./SheetCloseTrigger.vue";
` }}
</PreviewCode>
<SectionContent>
Update the import paths to match your project setup.
</SectionContent>
</div>
<div id="usage">
<SectionTitle>Usage</SectionTitle>
<PreviewCode>
{{ `
import {
SheetRoot,
SheetTrigger,
SheetContent,
SheetTitle,
SheetDescription,
SheetCloseTrigger,
} from "@/components/ui/sheet";
` }}
</PreviewCode>
<PreviewCode>
{{ `
<SheetRoot>
<SheetTrigger>Open Sheet</SheetTrigger>
<SheetContent>
<SheetTitle>Sheet Title</SheetTitle>
<SheetDescription>
Make changes to your profile here. Click save when you're done.
</SheetDescription>
<div class="grid gap-4 mt-2">
<div class="grid gap-2.5">
<Label for="name-1">Name</Label>
<Input id="name-1" name="name" defaultValue="Pedro Duarte" />
</div>
<div class="grid gap-2.5">
<Label for="username-1">Username</Label>
<Input id="username-1" name="username" defaultValue="@peduarte" />
</div>
</div>
<div class="flex gap-2 justify-end mt-7">
<SheetCloseTrigger>
<SquareX />
Close
</SheetCloseTrigger>
<Button variant="primary">
<Save />
Submit
</Button>
</div>
<SheetCloseTrigger />
</SheetContent>
</SheetRoot>
` }}
</PreviewCode>
</div>
<div id="variants">
<SectionTitle>Variants</SectionTitle>
<SectionContent>A collection of components you can use.</SectionContent>
<Preview>
<template #preview>
<SheetRoot>
<SheetTrigger>Custom Close</SheetTrigger>
<SheetContent>
<SheetTitle>Share Link</SheetTitle>
<SheetDescription>
Anyone who has this link will be able to view this.
</SheetDescription>
<div class="grid gap-4 mt-2">
<Input id="name-1" name="name" defaultValue="https://midone-ui.com/docs/installation" />
</div>
<div class="flex gap-2 mt-5">
<SheetCloseTrigger>
<ExternalLink />
Share Link
</SheetCloseTrigger>
</div>
</SheetContent>
</SheetRoot>
</template>
<template #code>
<PreviewCode>
{{ `
<SheetRoot>
<SheetTrigger>Custom Close</SheetTrigger>
<SheetContent>
<SheetTitle>Share Link</SheetTitle>
<SheetDescription>
Anyone who has this link will be able to view this.
</SheetDescription>
<div class="grid gap-4 mt-2">
<Input id="name-1" name="name" defaultValue="https://midone-ui.com/docs/installation" />
</div>
<div class="flex gap-2 mt-5">
<SheetCloseTrigger>
<ExternalLink />
Share Link
</SheetCloseTrigger>
</div>
</SheetContent>
</SheetRoot>
` }}
</PreviewCode>
</template>
</Preview>
</div>
</template>
-493
View File
@@ -1,493 +0,0 @@
<script lang="ts" setup>
import {
SliderRoot,
SliderLabel,
SliderValueText,
SliderControl,
SliderTrack,
SliderRange,
SliderThumb,
SliderHiddenInput,
SliderMarkerGroup,
SliderMarker,
} from "@/components/ui/slider";
import {
Preview,
SectionTitle,
SectionContent,
InstallPackage,
PreviewCode,
} from "@/components/docs";
</script>
<template>
<div id="preview" class="-mt-20">
<Preview>
<template #preview>
<SliderRoot class="w-72" :defaultValue="[20]">
<SliderLabel>Max Items</SliderLabel>
<SliderControl>
<SliderTrack>
<SliderRange />
</SliderTrack>
<SliderThumb :index="0">
<SliderHiddenInput />
</SliderThumb>
</SliderControl>
<div class="flex items-center text-xs gap-1 font-medium justify-center opacity-70">
<SliderValueText /> Items
</div>
</SliderRoot>
</template>
<template #code>
<PreviewCode>
{{ `
<SliderRoot class="w-72" :defaultValue="[20]">
<SliderLabel>Max Items</SliderLabel>
<SliderControl>
<SliderTrack>
<SliderRange />
</SliderTrack>
<SliderThumb :index="0">
<SliderHiddenInput />
</SliderThumb>
</SliderControl>
<div class="flex items-center text-xs gap-1 font-medium justify-center opacity-70">
<SliderValueText /> Items
</div>
</SliderRoot>
` }}
</PreviewCode>
</template>
</Preview>
</div>
<div id="installation">
<SectionTitle>Installation</SectionTitle>
<SectionContent>Install the following dependencies:</SectionContent>
<InstallPackage>add @zag-js/vue @zag-js/slider</InstallPackage>
<SectionContent>
Copy and paste the following code into your project.
</SectionContent>
<PreviewCode title="components/ui/slider/SliderRoot.vue">
{{`
<script lang="ts" setup>
import * as slider from "@zag-js/slider";
import type { Props } from "@zag-js/slider";
import { Slot } from "@/components/ui/slot";
import { cn } from "@mykopkb/core/utils/cn";
import { normalizeProps, useMachine } from "@zag-js/vue";
import { computed, provide } from "vue";
import { sliderRoot } from "@mykopkb/core/styles/slider.styles";
const {
class: className,
asChild = false,
...props
} = defineProps<Partial<Props> & { class?: string; asChild?: boolean }>();
const service = useMachine(slider.machine, {
...props,
id: crypto.randomUUID(),
});
const api = computed(() => slider.connect(service, normalizeProps));
provide("sliderApi", api);
</script>
<template>
<Slot :class="cn(sliderRoot, className)" v-bind="{ ...props, ...$attrs, ...api.getRootProps() }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/slider/SliderLabel.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { sliderLabel } from "@mykopkb/core/styles/slider.styles";
import { Slot } from "@/components/ui/slot";
import { Label } from "@/components/ui/label";
import type { Api } from "@zag-js/slider";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("sliderApi");
</script>
<template>
<Slot v-bind="{ ...api?.getLabelProps(), ...props, ...$attrs }">
<slot v-if="asChild" />
<Label v-else :class="cn(sliderLabel, className)">
<slot />
</Label>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/slider/SliderValueText.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { sliderValueText } from "@mykopkb/core/styles/slider.styles";
import { Slot } from "@/components/ui/slot";
import type { Api } from "@zag-js/slider";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("sliderApi");
</script>
<template>
<Slot :class="cn(sliderValueText, className)" v-bind="{ ...api?.getValueTextProps(), ...props, ...$attrs }">
<slot v-if="asChild" />
<output v-else>\{\{ api?.value?.[0] \}\}</output>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/slider/SliderControl.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { sliderControl } from "@mykopkb/core/styles/slider.styles";
import { Slot } from "@/components/ui/slot";
import type { Api } from "@zag-js/slider";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("sliderApi");
</script>
<template>
<Slot :class="cn(sliderControl, className)" v-bind="{ ...api?.getControlProps(), ...props, ...$attrs }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/slider/SliderTrack.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { sliderTrack } from "@mykopkb/core/styles/slider.styles";
import { Slot } from "@/components/ui/slot";
import type { Api } from "@zag-js/slider";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("sliderApi");
</script>
<template>
<Slot :class="cn(sliderTrack, className)" v-bind="{ ...api?.getTrackProps(), ...props, ...$attrs }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/slider/SliderRange.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { sliderRange } from "@mykopkb/core/styles/slider.styles";
import { Slot } from "@/components/ui/slot";
import type { Api } from "@zag-js/slider";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("sliderApi");
</script>
<template>
<Slot :class="cn(sliderRange, className)" v-bind="{ ...api?.getRangeProps(), ...props, ...$attrs }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/slider/SliderThumb.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { sliderThumb } from "@mykopkb/core/styles/slider.styles";
import { Slot } from "@/components/ui/slot";
import type { Api, ThumbProps } from "@zag-js/slider";
import { provide, inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<
ThumbProps & {
class?: string;
asChild?: boolean;
}
>();
const api = inject<Api>("sliderApi");
provide("sliderThumb", props);
</script>
<template>
<Slot :class="cn(sliderThumb, className)" v-bind="{ ...api?.getThumbProps(props), ...props, ...$attrs }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/slider/SliderHiddenInput.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { sliderHiddenInput } from "@mykopkb/core/styles/slider.styles";
import type { Api, ThumbProps } from "@zag-js/slider";
import { inject } from "vue";
const { class: className, ...props } = defineProps<{
class?: string;
}>();
const api = inject<Api>("sliderApi");
const thumbProps = inject<ThumbProps>("sliderThumb");
</script>
<template>
<input :class="cn(sliderHiddenInput, className)"
v-bind="{ ...api?.getHiddenInputProps(thumbProps!), ...props, ...$attrs }" />
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/slider/SliderMarkerGroup.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { sliderMarkerGroup } from "@mykopkb/core/styles/slider.styles";
import { Slot } from "@/components/ui/slot";
import type { Api } from "@zag-js/slider";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("sliderApi");
</script>
<template>
<Slot :class="cn(sliderMarkerGroup, className)" v-bind="{ ...api?.getMarkerGroupProps(), ...props, ...$attrs }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/slider/SliderMarker.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { sliderMarker } from "@mykopkb/core/styles/slider.styles";
import { Slot } from "@/components/ui/slot";
import type { Api, MarkerProps } from "@zag-js/slider";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<
MarkerProps & {
class?: string;
asChild?: boolean;
}
>();
const api = inject<Api>("sliderApi");
</script>
<template>
<Slot :class="cn(sliderMarker, className)" v-bind="{ ...api?.getMarkerProps(props), ...props, ...$attrs }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/slider/index.ts">
{{ `
export { default as SliderRoot } from "./SliderRoot.vue";
export { default as SliderLabel } from "./SliderLabel.vue";
export { default as SliderValueText } from "./SliderValueText.vue";
export { default as SliderControl } from "./SliderControl.vue";
export { default as SliderTrack } from "./SliderTrack.vue";
export { default as SliderRange } from "./SliderRange.vue";
export { default as SliderThumb } from "./SliderThumb.vue";
export { default as SliderHiddenInput } from "./SliderHiddenInput.vue";
export { default as SliderMarkerGroup } from "./SliderMarkerGroup.vue";
export { default as SliderMarker } from "./SliderMarker.vue";
` }}
</PreviewCode>
<SectionContent>
Update the import paths to match your project setup.
</SectionContent>
</div>
<div id="usage">
<SectionTitle>Usage</SectionTitle>
<PreviewCode>
{{ `
import {
SliderRoot,
SliderLabel,
SliderValueText,
SliderControl,
SliderTrack,
SliderRange,
SliderThumb,
SliderHiddenInput,
SliderMarkerGroup,
SliderMarker,
} from "@/components/ui/slider";
` }}
</PreviewCode>
<PreviewCode>
{{ `
<SliderRoot class="w-72" :defaultValue="[20]">
<SliderLabel>Max Items</SliderLabel>
<SliderControl>
<SliderTrack>
<SliderRange />
</SliderTrack>
<SliderThumb :index="0">
<SliderHiddenInput />
</SliderThumb>
</SliderControl>
<div class="flex items-center text-xs gap-1 font-medium justify-center opacity-70">
<SliderValueText /> Items
</div>
</SliderRoot>
` }}
</PreviewCode>
</div>
<div id="variants">
<SectionTitle>Variants</SectionTitle>
<SectionContent>A collection of components you can use.</SectionContent>
<Preview>
<template #preview>
<SliderRoot class="w-72" :value="[20, 80]">
<SliderLabel>Price Range</SliderLabel>
<SliderControl>
<SliderTrack>
<SliderRange />
</SliderTrack>
<SliderThumb :index="0">
<SliderHiddenInput />
</SliderThumb>
<SliderThumb :index="1">
<SliderHiddenInput />
</SliderThumb>
</SliderControl>
<SliderMarkerGroup>
<SliderMarker :value="0">$0</SliderMarker>
<SliderMarker :value="25">$25</SliderMarker>
<SliderMarker :value="50">$50</SliderMarker>
<SliderMarker :value="75">$75</SliderMarker>
<SliderMarker :value="100">$100</SliderMarker>
</SliderMarkerGroup>
</SliderRoot>
</template>
<template #code>
<PreviewCode>
{{ `
<SliderRoot class="w-72" :value="[20, 80]">
<SliderLabel>Price Range</SliderLabel>
<SliderControl>
<SliderTrack>
<SliderRange />
</SliderTrack>
<SliderThumb :index="0">
<SliderHiddenInput />
</SliderThumb>
<SliderThumb :index="1">
<SliderHiddenInput />
</SliderThumb>
</SliderControl>
<SliderMarkerGroup>
<SliderMarker :value="0">$0</SliderMarker>
<SliderMarker :value="25">$25</SliderMarker>
<SliderMarker :value="50">$50</SliderMarker>
<SliderMarker :value="75">$75</SliderMarker>
<SliderMarker :value="100">$100</SliderMarker>
</SliderMarkerGroup>
</SliderRoot>
` }}
</PreviewCode>
</template>
</Preview>
</div>
</template>
-196
View File
@@ -1,196 +0,0 @@
<script lang="ts" setup>
import {
Preview,
SectionTitle,
SectionContent,
PreviewCode,
} from "@/components/docs";
import { Box } from "@/components/ui/box";
import { Button } from "@/components/ui/button";
import { ChevronLeft, ChevronRight } from "@lucide/vue";
</script>
<template>
<div id="preview" class="-mt-20">
<Preview>
<template #preview>
<div class="justify-center items-center flex gap-2">
<Box as-child>
<Button variant="ghost" class="me-2 px-2">
<ChevronLeft class="size-5" />
</Button>
</Box>
<Box as-child>
<Button variant="ghost" class="px-2">
<ChevronRight class="size-5" />
</Button>
</Box>
</div>
</template>
<template #code>
<PreviewCode>
{{ `
<div class="justify-center items-center flex gap-2">
<Box as-child>
<Button variant="ghost" class="me-2 px-2">
<ChevronLeft class="size-5" />
</Button>
</Box>
<Box as-child>
<Button variant="ghost" class="px-2">
<ChevronRight class="size-5" />
</Button>
</Box>
</div>
` }}
</PreviewCode>
</template>
</Preview>
</div>
<div id="installation">
<SectionTitle>Installation</SectionTitle>
<SectionContent>
By using <code>Slot</code> at the root, you ensure that props are correctly merged whether
you're using it as a direct wrapper or an <code>asChild</code> bridge.
</SectionContent>
<SectionContent>
Copy and paste the following code into your project.
</SectionContent>
<PreviewCode title="components/ui/slot/index.ts">
{{ `import {
defineComponent,
h,
cloneVNode,
Fragment,
isVNode,
type VNode,
type PropType,
} from "vue";
import { calculateSlot, flattenItems, type AnyProps } from "./slot";
export const Slot = defineComponent({
name: "Slot",
inheritAttrs: false,
props: {
children: {
type: [Object, Array] as PropType<any>,
},
},
setup(props, { attrs, slots }) {
return () => {
const raw = props.children ?? slots.default?.();
const isValidVNode = (item: any): item is VNode => isVNode(item) && typeof item.type !== "symbol";
const items = flattenItems<VNode>(
raw as any,
(item) => isVNode(item) && item.type === Fragment,
(item) => (isVNode(item) && Array.isArray(item.children) ? (item.children as VNode[]) : [])
).filter(isValidVNode);
const result = calculateSlot<VNode>({
props: attrs as AnyProps,
items,
isValid: isValidVNode,
getProps: (item) => (item.props as AnyProps) || {},
getChildren: (item) => item.children,
});
if (result.type === "wrapper") {
return h("div", result.props, result.children as any);
}
const target = result.target;
return cloneVNode(target, result.props, false);
};
},
});
export { Slot as Root };` }}
</PreviewCode>
<PreviewCode title="components/ui/slot/slot.ts">
{{ `export type AnyProps = Record<string, any>;
export interface SlotParams<T> {
props: AnyProps;
items: T[];
isValid: (item: T) => boolean;
getProps: (item: T) => AnyProps;
getChildren: (item: T) => any;
}
export type SlotResult<T> =
| { type: "slotted"; target: T; props: AnyProps; children: any }
| { type: "wrapper"; target: "div"; props: AnyProps; children: T[] };
export function mergeProps(slotProps: AnyProps, childProps: AnyProps): AnyProps {
const result: AnyProps = { ...childProps };
for (const key in slotProps) {
const slotValue = slotProps[key];
const isHandler = /^on[A-Z]/.test(key);
if (isHandler) {
const childValue = childProps[key];
if (typeof slotValue === "function" && typeof childValue === "function") {
result[key] = (...args: any[]) => {
childValue(...args);
slotValue(...args);
};
} else if (slotValue) {
result[key] = slotValue;
}
continue;
}
if (key === "class" || key === "className") {
const slotClasses = (slotValue || "").split(/\\s+/);
const childClasses = (childProps.class || childProps.className || "").split(/\\s+/);
const combined = Array.from(new Set([...slotClasses, ...childClasses])).filter(Boolean).join(" ");
result[key] = combined;
continue;
}
if (key === "style") {
result[key] = { ...slotValue, ...childProps.style };
continue;
}
if (childProps[key] === undefined) {
result[key] = slotValue;
}
}
return result;
}
export function flattenItems<T>(items: T | T[], isFragment: (item: T) => boolean, getChildren: (item: T) =>
T
| T[]): T[] {
const result: T[] = [];
const list = Array.isArray(items) ? items : [items];
list.forEach((item) => {
if (item === null || item === undefined) return;
if (isFragment(item)) {
const children = getChildren(item);
result.push(...flattenItems(children, isFragment, getChildren));
} else {
result.push(item);
}
});
return result;
}
export function calculateSlot<T>(params: SlotParams<T>): SlotResult<T> {
const { props, items, isValid, getProps, getChildren } = params;
if (items.length === 1) {
const primary = items[0];
if (isValid(primary)) {
return {
type: "slotted",
target: primary,
props: mergeProps(props, getProps(primary)),
children: getChildren(primary),
};
}
}
return { type: "wrapper", target: "div", props: props, children: items };
}` }}
</PreviewCode>
</div>
</template>
-230
View File
@@ -1,230 +0,0 @@
<script lang="ts" setup>
import { SwitchRoot, SwitchControl, SwitchLabel } from "@/components/ui/switch";
import {
Preview,
SectionTitle,
SectionContent,
InstallPackage,
PreviewCode,
} from "@/components/docs";
</script>
<template>
<div id="preview" class="-mt-20">
<Preview>
<template #preview>
<SwitchRoot>
<SwitchControl />
<SwitchLabel>Airplane Mode</SwitchLabel>
</SwitchRoot>
</template>
<template #code>
<PreviewCode>
{{ `
<SwitchRoot>
<SwitchControl />
<SwitchLabel>Airplane Mode</SwitchLabel>
</SwitchRoot>
` }}
</PreviewCode>
</template>
</Preview>
</div>
<div id="installation">
<SectionTitle>Installation</SectionTitle>
<SectionContent>Install the following dependencies:</SectionContent>
<InstallPackage>add @zag-js/vue @zag-js/switch</InstallPackage>
<SectionContent>
Copy and paste the following code into your project.
</SectionContent>
<PreviewCode title="components/ui/switch/SwitchRoot.vue">
{{`
<script lang="ts" setup>
import * as zagSwitch from "@zag-js/switch";
import type { Props } from "@zag-js/switch";
import { cn } from "@mykopkb/core/utils/cn";
import { switchRoot } from "@mykopkb/core/styles/switch.styles";
import { SwitchHiddenInput } from ".";
import { normalizeProps, useMachine } from "@zag-js/vue";
import { computed, provide } from "vue";
import { Slot } from "@/components/ui/slot";
const {
class: className,
asChild = false,
checked = undefined,
...props
} = defineProps<Partial<Props> & { class?: string; asChild?: boolean }>();
const service = useMachine(zagSwitch.machine, {
...props,
checked,
id: crypto.randomUUID(),
});
const api = computed(() => zagSwitch.connect(service, normalizeProps));
provide("switchApi", api);
</script>
<template>
<Slot :class="cn(switchRoot, className)" v-bind="{ ...props, ...$attrs, ...api?.getRootProps() }">
<slot v-if="asChild" />
<label v-else>
<slot />
<SwitchHiddenInput />
</label>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/switch/SwitchControl.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { switchControl } from "@mykopkb/core/styles/switch.styles";
import { SwitchThumb } from ".";
import type { Api } from "@zag-js/switch";
import { inject } from "vue";
import { Slot } from "@/components/ui/slot";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("switchApi");
</script>
<template>
<Slot :class="cn(switchControl, className)" v-bind="{ ...props, ...$attrs, ...api?.getControlProps() }">
<slot v-if="asChild" />
<span v-else>
<SwitchThumb />
</span>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/switch/SwitchThumb.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { switchThumb } from "@mykopkb/core/styles/switch.styles";
import type { Api } from "@zag-js/switch";
import { inject } from "vue";
import { Slot } from "@/components/ui/slot";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("switchApi");
</script>
<template>
<Slot :class="cn(switchThumb, className)" v-bind="{ ...props, ...$attrs, ...api?.getThumbProps() }">
<slot v-if="asChild" />
<span v-else>
<slot />
</span>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/switch/SwitchLabel.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { switchLabel } from "@mykopkb/core/styles/switch.styles";
import { label } from "@mykopkb/core/styles/label.styles";
import type { Api } from "@zag-js/switch";
import { inject } from "vue";
import { Slot } from "@/components/ui/slot";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("switchApi");
</script>
<template>
<Slot :class="cn([label, switchLabel, className])" v-bind="{ ...props, ...$attrs, ...api?.getLabelProps() }">
<slot v-if="asChild" />
<span v-else>
<slot />
</span>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/switch/SwitchHiddenInput.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { switchHiddenInput } from "@mykopkb/core/styles/switch.styles";
import type { Api } from "@zag-js/switch";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("switchApi");
</script>
<template>
<input :class="cn(switchHiddenInput, className)"
v-bind="{ ...props, ...$attrs, ...api?.getHiddenInputProps() }" />
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/switch/index.ts">
{{ `
export { default as SwitchRoot } from "./SwitchRoot.vue";
export { default as SwitchControl } from "./SwitchControl.vue";
export { default as SwitchThumb } from "./SwitchThumb.vue";
export { default as SwitchLabel } from "./SwitchLabel.vue";
export { default as SwitchHiddenInput } from "./SwitchHiddenInput.vue";
` }}
</PreviewCode>
<SectionContent>
Update the import paths to match your project setup.
</SectionContent>
</div>
<div id="usage">
<SectionTitle>Usage</SectionTitle>
<PreviewCode>
{{ `
import { SwitchRoot, SwitchControl, SwitchLabel } from "@/components/ui/switch";
` }}
</PreviewCode>
<PreviewCode>
{{ `
<SwitchRoot>
<SwitchControl />
<SwitchLabel>Airplane Mode</SwitchLabel>
</SwitchRoot>
` }}
</PreviewCode>
</div>
</template>
-653
View File
@@ -1,653 +0,0 @@
<script lang="ts" setup>
import {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
} from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import {
Preview,
SectionTitle,
SectionContent,
PreviewCode,
} from "@/components/docs";
const invoices: {
invoice: string;
paymentStatus: string;
badge: "success" | "pending" | "danger";
totalAmount: string;
paymentMethod: string;
}[] = [
{
invoice: "INV001",
paymentStatus: "Paid",
badge: "success",
totalAmount: "$250.00",
paymentMethod: "Credit Card",
},
{
invoice: "INV002",
paymentStatus: "Pending",
badge: "pending",
totalAmount: "$150.00",
paymentMethod: "PayPal",
},
{
invoice: "INV003",
paymentStatus: "Unpaid",
badge: "danger",
totalAmount: "$350.00",
paymentMethod: "Bank Transfer",
},
{
invoice: "INV004",
paymentStatus: "Paid",
badge: "success",
totalAmount: "$450.00",
paymentMethod: "Credit Card",
},
{
invoice: "INV005",
paymentStatus: "Paid",
badge: "success",
totalAmount: "$550.00",
paymentMethod: "PayPal",
},
{
invoice: "INV006",
paymentStatus: "Pending",
badge: "pending",
totalAmount: "$200.00",
paymentMethod: "Bank Transfer",
},
{
invoice: "INV007",
paymentStatus: "Unpaid",
badge: "danger",
totalAmount: "$300.00",
paymentMethod: "Credit Card",
},
];
</script>
<template>
<div id="preview" class="-mt-20">
<Preview>
<template #preview>
<Table>
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead class="w-[100px]">Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead class="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="invoice in invoices" :key="invoice.invoice">
<TableCell class="font-medium">
{{ invoice.invoice }}
</TableCell>
<TableCell>
<Badge :variant="invoice.badge">
{{ invoice.paymentStatus }}
</Badge>
</TableCell>
<TableCell>{{ invoice.paymentMethod }}</TableCell>
<TableCell class="text-right">
{{ invoice.totalAmount }}
</TableCell>
</TableRow>
</TableBody>
<TableFooter>
<TableRow>
<TableCell :colspan="3">Total</TableCell>
<TableCell class="text-right">$2,500.00</TableCell>
</TableRow>
</TableFooter>
</Table>
</template>
<template #code>
<PreviewCode>
{{ `
<Table>
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead class="w-[100px]">Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead class="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="invoice in invoices" :key="invoice.invoice">
<TableCell class="font-medium">
\{\{ invoice.invoice \}\}
</TableCell>
<TableCell>
<Badge :variant="invoice.badge">
\{\{ invoice.paymentStatus \}\}
</Badge>
</TableCell>
<TableCell>\{\{ invoice.paymentMethod \}\}</TableCell>
<TableCell class="text-right">
\{\{ invoice.totalAmount \}\}
</TableCell>
</TableRow>
</TableBody>
<TableFooter>
<TableRow>
<TableCell :colspan="3">Total</TableCell>
<TableCell class="text-right">$2,500.00</TableCell>
</TableRow>
</TableFooter>
</Table>
` }}
</PreviewCode>
</template>
</Preview>
</div>
<div id="installation">
<SectionTitle>Installation</SectionTitle>
<SectionContent>
Copy and paste the following code into your project.
</SectionContent>
<PreviewCode title="components/ui/table/Table.vue">
{{`
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import {
tableVariants,
type TableVariants,
} from "@mykopkb/core/styles/table.styles";
import { provide, computed } from "vue";
import TableContainer from "./TableContainer.vue";
const { class: className, ...props } = defineProps<
TableVariants & {
class?: string;
}
>();
provide(
"tableVariant",
computed(() => ({
variant: props.variant,
raised: props.raised,
}))
);
</script>
<template>
<TableContainer>
<table :class="cn(tableVariants({ variant, raised, className }), className)" v-bind="{ ...props }">
<slot />
</table>
</TableContainer>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/table/TableContainer.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { tableContainer } from "@mykopkb/core/styles/table.styles";
const { class: className, ...props } = defineProps<{
class?: string;
}>();
</script>
<template>
<div :class="cn(tableContainer, className)" v-bind="{ ...props }">
<slot />
</div>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/table/TableHeader.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { tableHeader } from "@mykopkb/core/styles/table.styles";
const { class: className, ...props } = defineProps<{
class?: string;
}>();
</script>
<template>
<thead :class="cn(tableHeader, className)" v-bind="{ ...props }">
<slot />
</thead>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/table/TableBody.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { tableBody } from "@mykopkb/core/styles/table.styles";
const { class: className, ...props } = defineProps<{
class?: string;
}>();
</script>
<template>
<tbody :class="cn(tableBody, className)" v-bind="{ ...props }">
<slot />
</tbody>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/table/TableFooter.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { tableFooter } from "@mykopkb/core/styles/table.styles";
const { class: className, ...props } = defineProps<{
class?: string;
}>();
</script>
<template>
<tfoot :class="cn(tableFooter, className)" v-bind="{ ...props }">
<slot />
</tfoot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/table/TableHead.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { tableHead } from "@mykopkb/core/styles/table.styles";
const { class: className, ...props } = defineProps<{
class?: string;
}>();
</script>
<template>
<th :class="cn(tableHead, className)" v-bind="{ ...props }">
<slot />
</th>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/table/TableRow.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { tableRow } from "@mykopkb/core/styles/table.styles";
const { class: className, ...props } = defineProps<{
class?: string;
}>();
</script>
<template>
<tr :class="cn(tableRow, className)" v-bind="{ ...props }">
<slot />
</tr>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/table/TableCell.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { tableCellVariants } from "@mykopkb/core/styles/table.styles";
import { inject } from "vue";
const { class: className, ...props } = defineProps<{
class?: string;
}>();
const variant = inject<
| {
variant?: "default" | "boxed" | null;
raised?: "single" | "double" | null;
}
| undefined
>("tableVariant", undefined);
</script>
<template>
<td :class="cn(tableCellVariants({ ...variant, className }), className)" v-bind="{ ...props }">
<slot />
</td>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/table/TableCaption.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { tableCaption } from "@mykopkb/core/styles/table.styles";
const { class: className, ...props } = defineProps<{
class?: string;
}>();
</script>
<template>
<caption :class="cn(tableCaption, className)" v-bind="{ ...props }">
<slot />
</caption>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/table/index.ts">
{{ `
export { default as Table } from "./Table.vue";
export { default as TableContainer } from "./TableContainer.vue";
export { default as TableHeader } from "./TableHeader.vue";
export { default as TableBody } from "./TableBody.vue";
export { default as TableFooter } from "./TableFooter.vue";
export { default as TableHead } from "./TableHead.vue";
export { default as TableRow } from "./TableRow.vue";
export { default as TableCell } from "./TableCell.vue";
export { default as TableCaption } from "./TableCaption.vue";
` }}
</PreviewCode>
<SectionContent>
Update the import paths to match your project setup.
</SectionContent>
</div>
<div id="usage">
<SectionTitle>Usage</SectionTitle>
<PreviewCode>
{{ `
import {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
} from "@/components/ui/table";
` }}
</PreviewCode>
<PreviewCode>
{{ `
<Table>
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead class="w-[100px]">Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead class="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="invoice in invoices" :key="invoice.invoice">
<TableCell class="font-medium">
\{\{ invoice.invoice \}\}
</TableCell>
<TableCell>
<Badge :variant="invoice.badge">
\{\{ invoice.paymentStatus \}\}
</Badge>
</TableCell>
<TableCell>\{\{ invoice.paymentMethod \}\}</TableCell>
<TableCell class="text-right">
\{\{ invoice.totalAmount \}\}
</TableCell>
</TableRow>
</TableBody>
<TableFooter>
<TableRow>
<TableCell :colspan="3">Total</TableCell>
<TableCell class="text-right">$2,500.00</TableCell>
</TableRow>
</TableFooter>
</Table>
` }}
</PreviewCode>
</div>
<div id="variants">
<SectionTitle>Variants</SectionTitle>
<SectionContent>A collection of components you can use.</SectionContent>
<Preview>
<template #preview>
<Table variant="boxed">
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead class="w-[100px]">Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead class="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="invoice in invoices" :key="invoice.invoice">
<TableCell class="font-medium">
{{ invoice.invoice }}
</TableCell>
<TableCell>
<Badge :variant="invoice.badge">
{{ invoice.paymentStatus }}
</Badge>
</TableCell>
<TableCell>{{ invoice.paymentMethod }}</TableCell>
<TableCell class="text-right">
{{ invoice.totalAmount }}
</TableCell>
</TableRow>
</TableBody>
<TableFooter>
<TableRow>
<TableCell :colspan="3">Total</TableCell>
<TableCell class="text-right">$2,500.00</TableCell>
</TableRow>
</TableFooter>
</Table>
</template>
<template #code>
<PreviewCode>
{{ `
<Table variant="boxed">
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead class="w-[100px]">Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead class="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="invoice in invoices" :key="invoice.invoice">
<TableCell class="font-medium">
\{\{ invoice.invoice \}\}
</TableCell>
<TableCell>
<Badge :variant="invoice.badge">
\{\{ invoice.paymentStatus \}\}
</Badge>
</TableCell>
<TableCell>\{\{ invoice.paymentMethod \}\}</TableCell>
<TableCell class="text-right">
\{\{ invoice.totalAmount \}\}
</TableCell>
</TableRow>
</TableBody>
<TableFooter>
<TableRow>
<TableCell :colspan="3">Total</TableCell>
<TableCell class="text-right">$2,500.00</TableCell>
</TableRow>
</TableFooter>
</Table>
` }}
</PreviewCode>
</template>
</Preview>
<Preview>
<template #preview>
<Table variant="boxed" raised="single">
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead class="w-[100px]">Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead class="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="invoice in invoices" :key="invoice.invoice">
<TableCell class="font-medium">
{{ invoice.invoice }}
</TableCell>
<TableCell>
<Badge :variant="invoice.badge">
{{ invoice.paymentStatus }}
</Badge>
</TableCell>
<TableCell>{{ invoice.paymentMethod }}</TableCell>
<TableCell class="text-right">
{{ invoice.totalAmount }}
</TableCell>
</TableRow>
</TableBody>
<TableFooter>
<TableRow>
<TableCell :colspan="3">Total</TableCell>
<TableCell class="text-right">$2,500.00</TableCell>
</TableRow>
</TableFooter>
</Table>
</template>
<template #code>
<PreviewCode>
{{ `
<Table variant="boxed" raised="single">
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead class="w-[100px]">Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead class="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="invoice in invoices" :key="invoice.invoice">
<TableCell class="font-medium">
\{\{ invoice.invoice \}\}
</TableCell>
<TableCell>
<Badge :variant="invoice.badge">
\{\{ invoice.paymentStatus \}\}
</Badge>
</TableCell>
<TableCell>\{\{ invoice.paymentMethod \}\}</TableCell>
<TableCell class="text-right">
\{\{ invoice.totalAmount \}\}
</TableCell>
</TableRow>
</TableBody>
<TableFooter>
<TableRow>
<TableCell :colspan="3">Total</TableCell>
<TableCell class="text-right">$2,500.00</TableCell>
</TableRow>
</TableFooter>
</Table>
` }}
</PreviewCode>
</template>
</Preview>
<Preview>
<template #preview>
<Table variant="boxed" raised="double">
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead class="w-[100px]">Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead class="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="invoice in invoices" :key="invoice.invoice">
<TableCell class="font-medium">
{{ invoice.invoice }}
</TableCell>
<TableCell>
<Badge :variant="invoice.badge">
{{ invoice.paymentStatus }}
</Badge>
</TableCell>
<TableCell>{{ invoice.paymentMethod }}</TableCell>
<TableCell class="text-right">
{{ invoice.totalAmount }}
</TableCell>
</TableRow>
</TableBody>
<TableFooter>
<TableRow>
<TableCell :colspan="3">Total</TableCell>
<TableCell class="text-right">$2,500.00</TableCell>
</TableRow>
</TableFooter>
</Table>
</template>
<template #code>
<PreviewCode>
{{ `
<Table variant="boxed" raised="double">
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead class="w-[100px]">Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead class="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="invoice in invoices" :key="invoice.invoice">
<TableCell class="font-medium">
\{\{ invoice.invoice \}\}
</TableCell>
<TableCell>
<Badge :variant="invoice.badge">
\{\{ invoice.paymentStatus \}\}
</Badge>
</TableCell>
<TableCell>\{\{ invoice.paymentMethod \}\}</TableCell>
<TableCell class="text-right">
\{\{ invoice.totalAmount \}\}
</TableCell>
</TableRow>
</TableBody>
<TableFooter>
<TableRow>
<TableCell :colspan="3">Total</TableCell>
<TableCell class="text-right">$2,500.00</TableCell>
</TableRow>
</TableFooter>
</Table>
` }}
</PreviewCode>
</template>
</Preview>
</div>
</template>
-361
View File
@@ -1,361 +0,0 @@
<script lang="ts" setup>
import { Box } from "@/components/ui/box";
import {
TabsRoot,
TabsList,
TabsTrigger,
TabsContent,
} from "@/components/ui/tabs";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { SquareX, Save, ExternalLink } from "@lucide/vue";
import {
Preview,
SectionTitle,
SectionContent,
InstallPackage,
PreviewCode,
} from "@/components/docs";
</script>
<template>
<div id="preview" class="-mt-20">
<Preview>
<template #preview>
<TabsRoot defaultValue="update-profile">
<TabsList>
<TabsTrigger value="update-profile"> Update Profile </TabsTrigger>
<TabsTrigger value="share-profile"> Share Profile </TabsTrigger>
</TabsList>
<Box raised="single" class="w-90">
<TabsContent value="update-profile">
<div class="grid gap-4 mt-2">
<div class="grid gap-2.5">
<Label for="name-1">Name</Label>
<Input id="name-1" name="name" defaultValue="Pedro Duarte" />
</div>
<div class="grid gap-2.5">
<Label for="username-1">Username</Label>
<Input id="username-1" name="username" defaultValue="@peduarte" />
</div>
</div>
<div class="flex gap-2 justify-end mt-7">
<Button>
<SquareX />
Close
</Button>
<Button variant="primary">
<Save />
Submit
</Button>
</div>
</TabsContent>
<TabsContent value="share-profile">
<div class="grid gap-4 mt-2">
<Input id="name-1" name="name" defaultValue="https://midone-ui.com/docs/installation" />
</div>
<div class="flex gap-2 mt-5">
<Button>
<ExternalLink />
Share Link
</Button>
</div>
</TabsContent>
</Box>
</TabsRoot>
</template>
<template #code>
<PreviewCode>
{{ `
<TabsRoot defaultValue="update-profile">
<TabsList>
<TabsTrigger value="update-profile"> Update Profile </TabsTrigger>
<TabsTrigger value="share-profile"> Share Profile </TabsTrigger>
</TabsList>
<Box raised="single" class="w-90">
<TabsContent value="update-profile">
<div class="grid gap-4 mt-2">
<div class="grid gap-2.5">
<Label for="name-1">Name</Label>
<Input id="name-1" name="name" defaultValue="Pedro Duarte" />
</div>
<div class="grid gap-2.5">
<Label for="username-1">Username</Label>
<Input id="username-1" name="username" defaultValue="@peduarte" />
</div>
</div>
<div class="flex gap-2 justify-end mt-7">
<Button>
<SquareX />
Close
</Button>
<Button variant="primary">
<Save />
Submit
</Button>
</div>
</TabsContent>
<TabsContent value="share-profile">
<div class="grid gap-4 mt-2">
<Input id="name-1" name="name" defaultValue="https://midone-ui.com/docs/installation" />
</div>
<div class="flex gap-2 mt-5">
<Button>
<ExternalLink />
Share Link
</Button>
</div>
</TabsContent>
</Box>
</TabsRoot>
` }}
</PreviewCode>
</template>
</Preview>
</div>
<div id="installation">
<SectionTitle>Installation</SectionTitle>
<SectionContent>Install the following dependencies:</SectionContent>
<InstallPackage>add @zag-js/react @zag-js/tabs</InstallPackage>
<SectionContent>
Copy and paste the following code into your project.
</SectionContent>
<PreviewCode title="components/ui/tabs/TabsContent.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { tabsContent } from "@mykopkb/core/styles/tabs.styles";
import type { Api, ContentProps } from "@zag-js/tabs";
import { inject } from "vue";
import { Slot } from "@/components/ui/slot";
const {
class: className,
asChild = false,
...props
} = defineProps<
ContentProps & {
class?: string;
asChild?: boolean;
}
>();
const api = inject<Api>("tabsApi");
</script>
<template>
<Slot :class="cn(tabsContent, className)" v-bind="{ ...props, ...$attrs, ...api?.getContentProps(props) }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/tabs/TabsIndicator.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { tabsIndicator } from "@mykopkb/core/styles/tabs.styles";
import type { Api } from "@zag-js/tabs";
import { inject } from "vue";
import { Slot } from "@/components/ui/slot";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("tabsApi");
</script>
<template>
<Slot :class="cn(tabsIndicator, className)" v-bind="{ ...props, ...$attrs, ...api?.getIndicatorProps() }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/tabs/TabsList.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { tabsList } from "@mykopkb/core/styles/tabs.styles";
import type { Api } from "@zag-js/tabs";
import { inject } from "vue";
import { Slot } from "@/components/ui/slot";
import { TabsIndicator } from ".";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("tabsApi");
</script>
<template>
<Slot :class="cn(tabsList, className)" v-bind="{ ...props, ...$attrs, ...api?.getListProps() }">
<slot v-if="asChild" />
<div v-else>
<slot />
<TabsIndicator />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/tabs/TabsRoot.vue">
{{`
<script lang="ts" setup>
import * as tabs from "@zag-js/tabs";
import type { Props } from "@zag-js/tabs";
import { cn } from "@mykopkb/core/utils/cn";
import { tabsRoot } from "@mykopkb/core/styles/tabs.styles";
import { normalizeProps, useMachine } from "@zag-js/vue";
import { computed, provide } from "vue";
import { Slot } from "@/components/ui/slot";
const {
class: className,
asChild = false,
...props
} = defineProps<Partial<Props> & { class?: string; asChild?: boolean }>();
const service = useMachine(tabs.machine, {
...props,
id: crypto.randomUUID(),
});
const api = computed(() => tabs.connect(service, normalizeProps));
provide("tabsApi", api);
</script>
<template>
<Slot :class="cn(tabsRoot, className)" v-bind="{ ...props, ...$attrs, ...api?.getRootProps() }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/tabs/TabsTrigger.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { tabsTrigger } from "@mykopkb/core/styles/tabs.styles";
import type { Api, TriggerProps } from "@zag-js/tabs";
import { inject } from "vue";
import { Slot } from "@/components/ui/slot";
const {
class: className,
asChild = false,
...props
} = defineProps<
TriggerProps & {
class?: string;
asChild?: boolean;
}
>();
const api = inject<Api>("tabsApi");
</script>
<template>
<Slot :class="cn(tabsTrigger, className)" v-bind="{ ...props, ...$attrs, ...api?.getTriggerProps(props) }">
<slot v-if="asChild" />
<button v-else>
<slot />
</button>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/tabs/index.ts">
{{ `
export { default as TabsContent } from "./TabsContent.vue";
export { default as TabsIndicator } from "./TabsIndicator.vue";
export { default as TabsList } from "./TabsList.vue";
export { default as TabsRoot } from "./TabsRoot.vue";
export { default as TabsTrigger } from "./TabsTrigger.vue";
` }}
</PreviewCode>
<SectionContent>
Update the import paths to match your project setup.
</SectionContent>
</div>
<div id="usage">
<SectionTitle>Usage</SectionTitle>
<PreviewCode>
{{ `
import {
TabsRoot,
TabsList,
TabsTrigger,
TabsContent,
} from "@/components/ui/tabs";
` }}
</PreviewCode>
<PreviewCode>
{{ `
<TabsRoot defaultValue="update-profile">
<TabsList>
<TabsTrigger value="update-profile"> Update Profile </TabsTrigger>
<TabsTrigger value="share-profile"> Share Profile </TabsTrigger>
</TabsList>
<Box raised="single" class="w-90">
<TabsContent value="update-profile">
<div class="grid gap-4 mt-2">
<div class="grid gap-2.5">
<Label for="name-1">Name</Label>
<Input id="name-1" name="name" defaultValue="Pedro Duarte" />
</div>
<div class="grid gap-2.5">
<Label for="username-1">Username</Label>
<Input id="username-1" name="username" defaultValue="@peduarte" />
</div>
</div>
<div class="flex gap-2 justify-end mt-7">
<Button>
<SquareX />
Close
</Button>
<Button variant="primary">
<Save />
Submit
</Button>
</div>
</TabsContent>
<TabsContent value="share-profile">
<div class="grid gap-4 mt-2">
<Input id="name-1" name="name" defaultValue="https://midone-ui.com/docs/installation" />
</div>
<div class="flex gap-2 mt-5">
<Button>
<ExternalLink />
Share Link
</Button>
</div>
</TabsContent>
</Box>
</TabsRoot>
` }}
</PreviewCode>
</div>
</template>
-69
View File
@@ -1,69 +0,0 @@
<script lang="ts" setup>
import { Textarea } from "@/components/ui/textarea";
import {
Preview,
SectionTitle,
SectionContent,
PreviewCode,
} from "@/components/docs";
</script>
<template>
<div id="preview" class="-mt-20">
<Preview>
<template #preview>
<Textarea class="w-86" placeholder="Type your message here." />
</template>
<template #code>
<PreviewCode>
{{ `
<Textarea class="w-86" placeholder="Type your message here." />
` }}
</PreviewCode>
</template>
</Preview>
</div>
<div id="installation">
<SectionTitle>Installation</SectionTitle>
<SectionContent>
Copy and paste the following code into your project.
</SectionContent>
<PreviewCode title="components/ui/textarea/Textarea.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { textarea } from "@mykopkb/core/styles/textarea.styles";
const { class: className, ...props } = defineProps<{
class?: string;
}>();
</script>
<template>
<textarea :class="cn(textarea, className)" v-bind="{ ...props }" />
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/textarea/index.ts">
{{ `
export { default as Textarea } from "./Textarea.vue";
` }}
</PreviewCode>
<SectionContent>
Update the import paths to match your project setup.
</SectionContent>
</div>
<div id="usage">
<SectionTitle>Usage</SectionTitle>
<PreviewCode>
{{ `
import { Textarea } from "@/components/ui/textarea";
` }}
</PreviewCode>
<PreviewCode>
{{ `
<Textarea class="w-86" placeholder="Type your message here." />
` }}
</PreviewCode>
</div>
</template>
-371
View File
@@ -1,371 +0,0 @@
<script lang="ts" setup>
import { Button } from "@/components/ui/button";
import {
toaster,
ToasterContainer,
ToastRoot,
ToastTitle,
ToastDescription,
ToastCloseTrigger,
} from "@/components/ui/toast";
import {
Preview,
SectionTitle,
SectionContent,
InstallPackage,
PreviewCode,
} from "@/components/docs";
</script>
<template>
<div id="preview" class="-mt-20">
<Preview>
<template #preview>
<Button @click="
() =>
toaster.create({
title: 'Event has been created',
description: 'Sunday, December 03, 2023 at 9:00 AM',
type: 'info',
})
">
Show Toast
</Button>
<ToasterContainer :toaster="toaster" v-slot="{ toast }">
<ToastRoot :key="toast.id">
<ToastTitle>{{ toast.title }}</ToastTitle>
<ToastDescription>
{{ toast.description }}
</ToastDescription>
<ToastCloseTrigger />
</ToastRoot>
</ToasterContainer>
</template>
<template #code>
<PreviewCode>
{{`
<Button @click="
() =>
toaster.create({
title: 'Event has been created',
description: 'Sunday, December 03, 2023 at 9:00 AM',
type: 'info',
})
">
Show Toast
</Button>
<ToasterContainer :toaster="toaster" v-slot="{ toast }">
<ToastRoot :key="toast.id">
<ToastTitle>\{\{ toast.title \}\}</ToastTitle>
<ToastDescription>
\{\{ toast.description \}\}
</ToastDescription>
<ToastCloseTrigger />
</ToastRoot>
</ToasterContainer>
` }}
</PreviewCode>
</template>
</Preview>
</div>
<div id="installation">
<SectionTitle>Installation</SectionTitle>
<SectionContent>Install the following dependencies:</SectionContent>
<InstallPackage>add @zag-js/vue @zag-js/toast</InstallPackage>
<SectionContent>
Copy and paste the following code into your project.
</SectionContent>
<PreviewCode title="components/ui/toast/toaster.ts">
{{ `
import * as toast from "@zag-js/toast";
const toaster = toast.createStore({
placement: "bottom-end",
overlap: true,
gap: 24,
});
export default toaster;
` }}
</PreviewCode>
<PreviewCode title="components/ui/toast/ToastRoot.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { toastRoot } from "@mykopkb/core/styles/toast.styles";
import {
boxVariants,
type BoxVariants,
} from "@mykopkb/core/styles/box.styles";
import { Slot } from "@/components/ui/slot";
import type { Api } from "@zag-js/toast";
import { inject } from "vue";
const {
class: className,
asChild = false,
raised = "single",
...props
} = defineProps<
BoxVariants & {
class?: string;
asChild?: boolean;
}
>();
const api = inject<Api>("toastApi");
</script>
<template>
<Slot :class="cn([boxVariants({ raised, className }), toastRoot, className])"
v-bind="{ ...api?.getRootProps(), ...props, ...$attrs }">
<slot v-if="asChild" />
<div v-else>
<span v-bind="{ ...api?.getGhostBeforeProps() }" />
<div data-scope="toast" data-part="progressbar" />
<slot />
<span v-bind="{ ...api?.getGhostAfterProps() }" />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/toast/ToastTitle.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { toastTitle } from "@mykopkb/core/styles/toast.styles";
import { Slot } from "@/components/ui/slot";
import type { Api } from "@zag-js/toast";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("toastApi");
</script>
<template>
<Slot :class="cn(toastTitle, className)" v-bind="{ ...api?.getTitleProps(), ...props, ...$attrs }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/toast/ToastDescription.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { toastDescription } from "@mykopkb/core/styles/toast.styles";
import { Slot } from "@/components/ui/slot";
import type { Api } from "@zag-js/toast";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("toastApi");
</script>
<template>
<Slot :class="cn(toastDescription, className)" v-bind="{ ...api?.getDescriptionProps(), ...props, ...$attrs }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/toast/ToastCloseTrigger.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { toastCloseTrigger } from "@mykopkb/core/styles/toast.styles";
import {
buttonVariants,
type ButtonVariants,
} from "@mykopkb/core/styles/button.styles";
import { Slot } from "@/components/ui/slot";
import { Button } from "@/components/ui/button";
import { X } from "@lucide/vue";
import type { Api } from "@zag-js/toast";
import { inject } from "vue";
const {
class: className,
asChild = false,
look = "outline",
variant = "secondary",
size,
...props
} = defineProps<
ButtonVariants & {
class?: string;
asChild?: boolean;
}
>();
const api = inject<Api>("toastApi");
</script>
<template>
<Slot v-bind="{ ...api?.getCloseTriggerProps(), ...props, ...$attrs }">
<Button variant="ghost" v-if="!$slots.default" :class="cn(toastCloseTrigger, className)"
v-bind="{ ...props }">
<X class="size-4" />
</Button>
<template v-else>
<slot v-if="asChild" />
<Button v-else :class="
cn(buttonVariants({ look, variant, size, className }), className)
">
<slot />
</Button>
</template>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/toast/ToastItem.vue">
{{`
<script lang="ts" setup>
import * as toast from "@zag-js/toast";
import { provide, computed } from "vue";
import { useMachine, normalizeProps } from "@zag-js/vue";
const { toastGroup, serviceGroup, index } = defineProps<{
class?: string;
asChild?: boolean;
toastGroup: toast.Options;
serviceGroup: toast.GroupService;
index: number;
}>();
const composedProps = computed(() => ({
...toastGroup,
index,
parent: serviceGroup,
}));
const service = useMachine(toast.machine, composedProps);
const api = toast.connect(service, normalizeProps);
provide("toastApi", api);
</script>
<template>
<slot :toast="{
...api,
id: toastGroup.id,
}" />
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/toast/ToasterContainer.vue">
{{`
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import * as toast from "@zag-js/toast";
import { toasterContainer } from "@mykopkb/core/styles/toast.styles";
import type { Store } from "@zag-js/toast";
import { normalizeProps, useMachine } from "@zag-js/vue";
import { computed } from "vue";
import { ToastItem } from ".";
const {
class: className,
asChild = false,
toaster,
...props
} = defineProps<{ class?: string; asChild?: boolean; toaster: Store }>();
const serviceGroup = useMachine(toast.group.machine, {
id: crypto.randomUUID(),
store: toaster,
});
const apiGroup = computed(() =>
toast.group.connect(serviceGroup, normalizeProps)
);
</script>
<template>
<Teleport to="body">
<div :class="cn(toasterContainer, className)" v-bind="{ ...apiGroup?.getGroupProps(), ...props, ...$attrs }">
<ToastItem v-for="(toastGroup, index) in apiGroup.getToasts()" :key="toastGroup.id" :index="index"
:toastGroup="toastGroup" :serviceGroup="serviceGroup" v-slot="{ toast }">
<slot :toast="toast" />
</ToastItem>
</div>
</Teleport>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/toast/index.ts">
{{ `
export { default as toaster } from "./toaster";
export { default as ToastRoot } from "./ToastRoot.vue";
export { default as ToastTitle } from "./ToastTitle.vue";
export { default as ToastDescription } from "./ToastDescription.vue";
export { default as ToastCloseTrigger } from "./ToastCloseTrigger.vue";
export { default as ToastItem } from "./ToastItem.vue";
export { default as ToasterContainer } from "./ToasterContainer.vue";
` }}
</PreviewCode>
<SectionContent>
Update the import paths to match your project setup.
</SectionContent>
</div>
<div id="usage">
<SectionTitle>Usage</SectionTitle>
<PreviewCode>
{{ `
import {
toaster,
ToasterContainer,
ToastRoot,
ToastTitle,
ToastDescription,
ToastCloseTrigger,
} from "@/components/ui/toast";
` }}
</PreviewCode>
<PreviewCode>
{{`
<Button @click="
() =>
toaster.create({
title: 'Event has been created',
description: 'Sunday, December 03, 2023 at 9:00 AM',
type: 'info',
})
">
Show Toast
</Button>
<ToasterContainer :toaster="toaster" v-slot="{ toast }">
<ToastRoot :key="toast.id">
<ToastTitle>\{\{ toast.title \}\}</ToastTitle>
<ToastDescription>
\{\{ toast.description \}\}
</ToastDescription>
<ToastCloseTrigger />
</ToastRoot>
</ToasterContainer>
` }}
</PreviewCode>
</div>
</template>
-287
View File
@@ -1,287 +0,0 @@
<script lang="ts" setup>
import {
TooltipRoot,
TooltipTrigger,
TooltipPositioner,
TooltipContent,
} from "@/components/ui/tooltip";
import {
Preview,
SectionTitle,
SectionContent,
InstallPackage,
PreviewCode,
} from "@/components/docs";
</script>
<template>
<div id="preview" class="-mt-20">
<Preview>
<template #preview>
<TooltipRoot>
<TooltipTrigger>Hover Me</TooltipTrigger>
<TooltipPositioner>
<TooltipContent>I am a tooltip!</TooltipContent>
</TooltipPositioner>
</TooltipRoot>
</template>
<template #code>
<PreviewCode>
{{ `
<TooltipRoot>
<TooltipTrigger>Hover Me</TooltipTrigger>
<TooltipPositioner>
<TooltipContent>I am a tooltip!</TooltipContent>
</TooltipPositioner>
</TooltipRoot>
` }}
</PreviewCode>
</template>
</Preview>
</div>
<div id="installation">
<SectionTitle>Installation</SectionTitle>
<SectionContent>Install the following dependencies:</SectionContent>
<InstallPackage>add @zag-js/vue @zag-js/tooltip</InstallPackage>
<SectionContent>
Copy and paste the following code into your project.
</SectionContent>
<PreviewCode title="components/ui/tooltip/TooltipRoot.vue">
{{`
<script lang="ts" setup>
import * as tooltip from "@zag-js/tooltip";
import type { Props } from "@zag-js/tooltip";
import { normalizeProps, useMachine } from "@zag-js/vue";
import { computed, provide } from "vue";
const {
class: className,
asChild = false,
open = undefined,
disabled = false,
...props
} = defineProps<Partial<Props> & { class?: string; asChild?: boolean }>();
const service = useMachine(tooltip.machine, {
...props,
positioning: {
placement: "top",
offset: { mainAxis: 10 },
},
closeDelay: 0,
openDelay: 0,
open,
disabled,
id: crypto.randomUUID(),
});
const api = computed(() => tooltip.connect(service, normalizeProps));
provide("tooltipApi", api);
</script>
<template>
<slot />
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/tooltip/TooltipTrigger.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { tooltipTrigger } from "@mykopkb/core/styles/tooltip.styles";
import { Slot } from "@/components/ui/slot";
import { Button } from "@/components/ui/button";
import type { Api } from "@zag-js/tooltip";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("tooltipApi");
</script>
<template>
<Slot v-bind="{ ...api?.getTriggerProps(), ...props, ...$attrs }">
<Button variant="secondary" look="outline" v-if="!asChild" :class="cn(tooltipTrigger, className)">
<slot />
</Button>
<slot v-else />
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/tooltip/TooltipPositioner.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { tooltipPositioner } from "@mykopkb/core/styles/tooltip.styles";
import { Slot } from "@/components/ui/slot";
import type { Api } from "@zag-js/tooltip";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("tooltipApi");
</script>
<template>
<Teleport to="body">
<Slot :class="cn(tooltipPositioner, className)"
v-bind="{ ...api?.getPositionerProps(), ...props, ...$attrs }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</Teleport>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/tooltip/TooltipContent.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { tooltipContent } from "@mykopkb/core/styles/tooltip.styles";
import { Slot } from "@/components/ui/slot";
import { TooltipArrow, TooltipArrowTip } from ".";
import type { Api } from "@zag-js/tooltip";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("tooltipApi");
</script>
<template>
<Slot :class="cn(tooltipContent, className)" v-bind="{ ...api?.getContentProps(), ...props, ...$attrs }">
<slot v-if="asChild" />
<div v-else>
<slot />
<TooltipArrow>
<TooltipArrowTip />
</TooltipArrow>
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/tooltip/TooltipArrow.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { tooltipArrow } from "@mykopkb/core/styles/tooltip.styles";
import { Slot } from "@/components/ui/slot";
import type { Api } from "@zag-js/tooltip";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("tooltipApi");
</script>
<template>
<Slot :class="cn(tooltipArrow, className)" v-bind="{ ...api?.getArrowProps(), ...props, ...$attrs }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/tooltip/TooltipArrowTip.vue">
{{ `
<script lang="ts" setup>
import { cn } from "@mykopkb/core/utils/cn";
import { tooltipArrowTip } from "@mykopkb/core/styles/tooltip.styles";
import { Slot } from "@/components/ui/slot";
import type { Api } from "@zag-js/tooltip";
import { inject } from "vue";
const {
class: className,
asChild = false,
...props
} = defineProps<{
class?: string;
asChild?: boolean;
}>();
const api = inject<Api>("tooltipApi");
</script>
<template>
<Slot :class="cn(tooltipArrowTip, className)" v-bind="{ ...api?.getArrowTipProps(), ...props, ...$attrs }">
<slot v-if="asChild" />
<div v-else>
<slot />
</div>
</Slot>
</template>
` }}
</PreviewCode>
<PreviewCode title="components/ui/tooltip/index.ts">
{{ `
export { default as TooltipRoot } from "./TooltipRoot.vue";
export { default as TooltipTrigger } from "./TooltipTrigger.vue";
export { default as TooltipPositioner } from "./TooltipPositioner.vue";
export { default as TooltipContent } from "./TooltipContent.vue";
export { default as TooltipArrow } from "./TooltipArrow.vue";
export { default as TooltipArrowTip } from "./TooltipArrowTip.vue";
` }}
</PreviewCode>
<SectionContent>
Update the import paths to match your project setup.
</SectionContent>
</div>
<div id="usage">
<SectionTitle>Usage</SectionTitle>
<PreviewCode>
{{ `
import {
TooltipRoot,
TooltipTrigger,
TooltipPositioner,
TooltipContent,
} from "@/components/ui/tooltip";
` }}
</PreviewCode>
<PreviewCode>
{{ `
<TooltipRoot>
<TooltipTrigger>Hover Me</TooltipTrigger>
<TooltipPositioner>
<TooltipContent>I am a tooltip!</TooltipContent>
</TooltipPositioner>
</TooltipRoot>
` }}
</PreviewCode>
</div>
</template>
+2
View File
@@ -2,6 +2,7 @@ import "./index.css";
import { createApp } from 'vue'
import { pinia } from '@/pinia'
import { applyAppearance } from '@/utils/applyAppearance'
import App from './App.vue'
import router from './router/index.ts'
@@ -9,6 +10,7 @@ import router from './router/index.ts'
const app = createApp(App)
app.use(pinia)
applyAppearance()
app.use(router)
app.mount('#app')
+6 -464
View File
@@ -1,483 +1,25 @@
import type { Menu } from '@/core/types/menu'
import { authMenu } from '@/modules/auth'
import { userMenu } from '@/modules/user'
import { roleMenu } from '@/modules/role'
import { membershipApplicationMenu } from '@/modules/membership-application'
import { activityMenu } from '@/modules/activity'
import { dashboardMenu } from '@/modules/dashboard/menu'
import { externalSystemMenu } from '@/modules/external-system/menu'
import { activityLogMenu } from '@/modules/activity-log/menu'
export type { Menu }
const mainMenu: (string | Menu)[] = [
'Umum',
...dashboardMenu,
...externalSystemMenu,
...activityMenu,
'Teknologi Maklumat',
...roleMenu,
...activityLogMenu,
'Pentadbiran',
...membershipApplicationMenu,
...userMenu,
'GENERAL REPORTS',
{
icon: 'CircleGauge',
title: 'Dashboards',
badge: 4,
sub_menu: [
{
icon: 'PanelBottomClose',
route_name: 'dashboard-overview-1',
title: 'Overview 1',
},
],
},
{
icon: 'SquareKanban',
title: 'E-Commerce',
badge: 2,
sub_menu: [
{
icon: 'CircleGauge',
route_name: 'categories',
title: 'Categories',
},
{
icon: 'CircleGauge',
route_name: 'add-product',
title: 'Add Product',
},
{
icon: 'CircleGauge',
title: 'Products',
sub_menu: [
{
icon: 'CircleGauge',
route_name: 'product-list',
title: 'Product List',
},
{
icon: 'CircleGauge',
route_name: 'product-grid',
title: 'Product Grid',
},
],
},
{
icon: 'CircleGauge',
title: 'Transactions',
sub_menu: [
{
icon: 'CircleGauge',
route_name: 'transaction-list',
title: 'Transaction List',
},
{
icon: 'CircleGauge',
route_name: 'transaction-detail',
title: 'Transaction Detail',
},
],
},
{
icon: 'CircleGauge',
title: 'Sellers',
sub_menu: [
{
icon: 'CircleGauge',
route_name: 'seller-list',
title: 'Seller List',
},
{
icon: 'CircleGauge',
route_name: 'seller-detail',
title: 'Seller Detail',
},
],
},
{
icon: 'CircleGauge',
route_name: 'reviews',
title: 'Reviews',
},
],
},
'APPS',
{
icon: 'CircleGauge',
route_name: 'inbox',
title: 'Inbox',
},
{
icon: 'CircleGauge',
route_name: 'file-manager',
title: 'File Manager',
badge: 5,
},
{
icon: 'CircleGauge',
route_name: 'point-of-sale',
title: 'Point of Sale',
},
{
icon: 'CircleGauge',
route_name: 'chat',
title: 'Chat',
badge: 3,
},
{
icon: 'CircleGauge',
route_name: 'post',
title: 'Post',
},
// {
// icon: 'CircleGauge',
// route_name: 'calendar',
// title: 'Calendar',
// },
'PAGES',
{
icon: 'CircleGauge',
title: 'Crud',
sub_menu: [
{
icon: 'CircleGauge',
route_name: 'crud-data-list',
title: 'Data List',
},
],
},
{
icon: 'CircleGauge',
title: 'Users',
sub_menu: [
{
icon: 'CircleGauge',
route_name: 'users-layout-1',
title: 'Layout 1',
},
{
icon: 'CircleGauge',
route_name: 'users-layout-2',
title: 'Layout 2',
},
{
icon: 'CircleGauge',
route_name: 'users-layout-3',
title: 'Layout 3',
},
],
},
{
icon: 'CircleGauge',
title: 'Pages',
sub_menu: [
{
icon: 'CircleGauge',
title: 'Wizards',
sub_menu: [
{
icon: 'CircleGauge',
route_name: 'wizard-layout-1',
title: 'Layout 1',
},
{
icon: 'CircleGauge',
route_name: 'wizard-layout-2',
title: 'Layout 2',
},
{
icon: 'CircleGauge',
route_name: 'wizard-layout-3',
title: 'Layout 3',
},
],
},
{
icon: 'CircleGauge',
title: 'Blog',
sub_menu: [
{
icon: 'CircleGauge',
route_name: 'blog-layout-1',
title: 'Layout 1',
},
{
icon: 'CircleGauge',
route_name: 'blog-layout-2',
title: 'Layout 2',
},
{
icon: 'CircleGauge',
route_name: 'blog-layout-3',
title: 'Layout 3',
},
],
},
{
icon: 'CircleGauge',
title: 'Pricing',
sub_menu: [
{
icon: 'CircleGauge',
route_name: 'pricing-layout-1',
title: 'Layout 1',
},
{
icon: 'CircleGauge',
route_name: 'pricing-layout-2',
title: 'Layout 2',
},
],
},
{
icon: 'CircleGauge',
title: 'Invoice',
sub_menu: [
{
icon: 'CircleGauge',
route_name: 'invoice-layout-1',
title: 'Layout 1',
},
{
icon: 'CircleGauge',
route_name: 'invoice-layout-2',
title: 'Layout 2',
},
],
},
{
icon: 'CircleGauge',
title: 'FAQ',
sub_menu: [
{
icon: 'CircleGauge',
route_name: 'faq-layout-1',
title: 'Layout 1',
},
{
icon: 'CircleGauge',
route_name: 'faq-layout-2',
title: 'Layout 2',
},
{
icon: 'CircleGauge',
route_name: 'faq-layout-3',
title: 'Layout 3',
},
],
},
],
},
'UI COMPONENTS',
{
icon: 'CircleGauge',
title: 'Base',
sub_menu: [
{
icon: 'CircleGauge',
route_name: 'slot',
title: 'Slot',
},
{
icon: 'CircleGauge',
route_name: 'box',
title: 'Box',
},
{
icon: 'CircleGauge',
route_name: 'scroll-area',
title: 'Scroll Area',
},
],
},
{
icon: 'CircleGauge',
title: 'Navigation',
sub_menu: [
{
icon: 'CircleGauge',
route_name: 'breadcrumb',
title: 'Breadcrumb',
},
{
icon: 'CircleGauge',
route_name: 'menu',
title: 'Menu',
},
{
icon: 'CircleGauge',
route_name: 'pagination',
title: 'Pagination',
},
{
icon: 'CircleGauge',
route_name: 'tabs',
title: 'Tabs',
},
],
},
{
icon: 'CircleGauge',
title: 'Forms',
sub_menu: [
{
icon: 'CircleGauge',
route_name: 'button',
title: 'Button',
},
{
icon: 'CircleGauge',
route_name: 'checkbox',
title: 'Checkbox',
},
{
icon: 'CircleGauge',
route_name: 'combobox',
title: 'Combobox',
},
{
icon: 'CircleGauge',
route_name: 'datepicker',
title: 'Datepicker',
},
{
icon: 'CircleGauge',
route_name: 'field',
title: 'Field',
},
{
icon: 'CircleGauge',
route_name: 'input',
title: 'Input',
},
{
icon: 'CircleGauge',
route_name: 'native-select',
title: 'Native Select',
},
{
icon: 'CircleGauge',
route_name: 'radio-group',
title: 'Radio Group',
},
{
icon: 'CircleGauge',
route_name: 'select',
title: 'Select',
},
{
icon: 'CircleGauge',
route_name: 'slider',
title: 'Slider',
},
{
icon: 'CircleGauge',
route_name: 'switch',
title: 'Switch',
},
{
icon: 'CircleGauge',
route_name: 'textarea',
title: 'Textarea',
},
],
},
{
icon: 'CircleGauge',
title: 'Data Display',
sub_menu: [
{
icon: 'CircleGauge',
route_name: 'accordion',
title: 'Accordion',
},
{
icon: 'CircleGauge',
route_name: 'avatar',
title: 'Avatar',
},
{
icon: 'CircleGauge',
route_name: 'badge',
title: 'Badge',
},
{
icon: 'CircleGauge',
route_name: 'carousel',
title: 'Carousel',
},
{
icon: 'CircleGauge',
route_name: 'table',
title: 'Table',
},
],
},
{
icon: 'CircleGauge',
title: 'Feedback',
sub_menu: [
{
icon: 'CircleGauge',
route_name: 'alert',
title: 'Alert',
},
{
icon: 'CircleGauge',
route_name: 'progress-circular',
title: 'Progress Circular',
},
{
icon: 'CircleGauge',
route_name: 'progress-linear',
title: 'Progress Linear',
},
{
icon: 'CircleGauge',
route_name: 'toast',
title: 'Toast',
},
],
},
{
icon: 'CircleGauge',
title: 'Overlay',
sub_menu: [
{
icon: 'CircleGauge',
route_name: 'dialog',
title: 'Dialog',
},
{
icon: 'CircleGauge',
route_name: 'popover',
title: 'Popover',
},
{
icon: 'CircleGauge',
route_name: 'sheet',
title: 'Sheet',
},
{
icon: 'CircleGauge',
route_name: 'tooltip',
title: 'Tooltip',
},
],
},
{
icon: 'CircleGauge',
title: 'Visuals',
sub_menu: [
{
icon: 'CircleGauge',
route_name: 'chart',
title: 'Chart',
},
{
icon: 'CircleGauge',
route_name: 'map',
title: 'Map',
},
],
},
]
export default mainMenu
@@ -0,0 +1,74 @@
import { onMounted, ref, watch } from 'vue'
import debounce from 'lodash/debounce'
import { useApiPagination } from '@/composables/useApiPagination'
import { getApiErrorMessage } from '@/core/utils/getApiErrorMessage'
import { listActivityLogs } from '../services/activity-log.service'
import type { ActivityLogItem } from '../types/activity-log.types'
export function useActivityLogList() {
const activityLogs = ref<ActivityLogItem[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
const search = ref('')
const page = ref(1)
const itemsPerPage = ref(10)
const { pagination, applyPagination } = useApiPagination({ per_page: 10 })
async function fetchActivityLogs(requestPage = page.value) {
loading.value = true
error.value = null
try {
const data = await listActivityLogs({
page: requestPage,
per_page: itemsPerPage.value,
search: search.value.trim() || undefined,
})
activityLogs.value = data.data
applyPagination(data.pagination)
page.value = data.pagination.current_page
} catch (err) {
error.value = getApiErrorMessage(err, 'Gagal memuatkan log aktiviti.')
activityLogs.value = []
} finally {
loading.value = false
}
}
const debouncedSearch = debounce(() => {
fetchActivityLogs(1)
}, 400)
watch(search, () => {
debouncedSearch()
})
watch(page, (nextPage, previousPage) => {
if (nextPage !== previousPage) {
fetchActivityLogs(nextPage)
}
})
watch(itemsPerPage, (nextValue, previousValue) => {
if (nextValue !== previousValue) {
fetchActivityLogs(1)
}
})
onMounted(() => {
fetchActivityLogs(1)
})
return {
activityLogs,
loading,
error,
search,
page,
itemsPerPage,
pagination,
fetchActivityLogs,
}
}
+2
View File
@@ -0,0 +1,2 @@
export { activityLogLayoutRoutes } from './routes'
export { activityLogMenu } from './menu'
+10
View File
@@ -0,0 +1,10 @@
import type { Menu } from '@/core/types/menu'
export const activityLogMenu: Menu[] = [
{
icon: 'ScrollText',
route_name: 'list-activity-logs',
title: 'Log Aktiviti',
permission: 'lihat log aktiviti',
},
]
@@ -0,0 +1,105 @@
<script lang="ts" setup>
import dayjs from 'dayjs'
import { Search } from '@lucide/vue'
import { AlertRoot, AlertTitle, AlertDescription } from '@/components/ui/alert'
import { Input } from '@/components/ui/input'
import DataTable from '@/components/ui/usage/DataTable.vue'
import type { TableHeader } from '@/components/ui/usage/DataTable.vue'
import { useActivityLogList } from '../composables/useActivityLogList'
import { formatModelType } from '../utils/activity-log.utils'
const {
activityLogs,
loading,
error,
search,
page,
itemsPerPage,
pagination,
} = useActivityLogList()
function formatDateTime(value: string | null | undefined): string {
if (!value) return '-'
return dayjs(value).format('DD MMM YYYY, HH:mm')
}
function formatCauserName(item: { causer?: { name?: string } | null }): string {
return item.causer?.name ?? '-'
}
const headers: TableHeader[] = [
{ title: 'Bil.', key: '#', sortable: false },
{
title: 'Tarikh',
key: 'created_at',
sortable: false,
exportValue: (item) => formatDateTime(item.created_at),
},
{
title: 'Pengguna',
key: 'causer.name',
sortable: false,
exportValue: (item) => formatCauserName(item),
},
{
title: 'Emel',
key: 'causer.email',
sortable: false,
exportValue: (item) => item.causer?.email ?? '-',
},
{ title: 'Keterangan', key: 'description', sortable: false },
{
title: 'Subjek',
key: 'subject_type',
sortable: false,
exportValue: (item) => formatModelType(item.subject_type),
},
{ title: 'Peristiwa', key: 'event', sortable: false },
]
</script>
<template>
<div class="w-full space-y-6">
<div>
<h2 class="text-lg font-medium">Log Aktiviti</h2>
<p class="mt-1 text-sm opacity-70">Semak rekod aktiviti pengguna dalam sistem.</p>
</div>
<AlertRoot v-if="error" variant="danger">
<AlertTitle>Error</AlertTitle>
<AlertDescription>{{ error }}</AlertDescription>
</AlertRoot>
<DataTable :headers="headers" :items="activityLogs" :loading="loading" :pagination="pagination" show-pagination
exportable export-file-name="activity-logs" v-model:page="page" v-model:items-per-page="itemsPerPage">
<template #toolbar>
<div class="relative w-full max-w-md flex-1">
<Search class="pointer-events-none absolute top-1/2 left-3 z-10 size-4 -translate-y-1/2 text-foreground/50"
aria-hidden="true" />
<Input v-model="search" type="search" placeholder="Cari keterangan, subjek, peristiwa, pengguna..."
class="w-full pl-9" aria-label="Cari log aktiviti" />
</div>
</template>
<template #item.created_at="{ item }">
{{ formatDateTime(item.created_at) }}
</template>
<template #item.causer.name="{ item }">
{{ formatCauserName(item) }}
</template>
<template #item.causer.email="{ item }">
<span class="lowercase">{{ item.causer?.email ?? '-' }}</span>
</template>
<template #item.subject_type="{ item }">
{{ formatModelType(item.subject_type) }}
</template>
<template #item.event="{ item }">
{{ item.event ?? '-' }}
</template>
</DataTable>
</div>
</template>
+14
View File
@@ -0,0 +1,14 @@
import type { RouteRecordRaw } from 'vue-router'
export const activityLogLayoutRoutes: RouteRecordRaw[] = [
{
path: 'activity-logs',
name: 'list-activity-logs',
component: () => import('./pages/ActivityLogList.vue'),
meta: {
title: 'Log Aktiviti',
module: 'activity-log',
permission: 'lihat log aktiviti',
},
},
]
@@ -0,0 +1,17 @@
import { api } from '@/core/services/api'
import type { PaginatedApiResponse } from '@/core/types/api'
import type { ActivityLogItem, ListActivityLogsParams } from '../types/activity-log.types'
export async function listActivityLogs(
params: ListActivityLogsParams,
): Promise<PaginatedApiResponse<ActivityLogItem>> {
const { data } = await api.get<PaginatedApiResponse<ActivityLogItem>>('/v1/activitylogs', {
params,
})
if (!data.success) {
throw new Error(data.message ?? 'Failed to load activity logs')
}
return data
}
@@ -0,0 +1,26 @@
export type ActivityLogCauser = {
id: string
name: string
email: string
}
export type ActivityLogItem = {
id: number
log_name: string | null
description: string
subject_type: string | null
subject_id: string | null
event: string | null
causer_type: string | null
causer_id: string | null
properties: Record<string, unknown> | null
created_at: string
updated_at: string
causer: ActivityLogCauser | null
}
export type ListActivityLogsParams = {
page?: number
per_page?: number
search?: string
}
@@ -0,0 +1,6 @@
export function formatModelType(value: string | null | undefined): string {
if (!value) return '-'
const segments = value.split('\\')
return segments[segments.length - 1] || value
}
+2 -2
View File
@@ -6,8 +6,8 @@ import { Button } from '@/components/ui/button'
import { AlertRoot, AlertTitle, AlertDescription } from '@/components/ui/alert'
import { logout, resolvePostAuthRoute } from '@/modules/auth'
import { useAuthStore } from '@/stores/auth'
import logoUrl from '@/assets/images/logo-kopkb.svg'
import illustrationUrl from '@/assets/images/logo-kopkb.svg'
import logoUrl from '@/assets/images/logo.svg'
import illustrationUrl from '@/assets/images/logo.svg'
const router = useRouter()
const authStore = useAuthStore()
+1 -1
View File
@@ -6,7 +6,7 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { AlertRoot, AlertTitle, AlertDescription } from '@/components/ui/alert'
import { getForgotPasswordErrorMessage, requestForgotPassword } from '@/modules/auth'
import illustrationUrl from '@/assets/images/logo-kopkb.svg'
import illustrationUrl from '@/assets/images/logo.svg'
const router = useRouter()
+3 -6
View File
@@ -13,7 +13,7 @@ import {
resolvePostAuthRoute,
} from '@/modules/auth'
import { useAuthStore } from '@/stores/auth'
import illustrationUrl from '@/assets/images/logo-kopkb.svg'
import illustrationUrl from '@/assets/images/logo.svg'
const router = useRouter()
const route = useRoute()
@@ -125,11 +125,8 @@ const appVersion = import.meta.env.VITE_APP_VERSION
<CheckboxLabel>Ingat saya</CheckboxLabel>
</CheckboxRoot>
</div>
<button
type="button"
class="opacity-70 hover:opacity-100"
@click="router.push({ name: 'forgot-password' })"
>
<button type="button" class="opacity-70 hover:opacity-100"
@click="router.push({ name: 'forgot-password' })">
Lupa Password?
</button>
</div>
+2 -4
View File
@@ -1,8 +1,6 @@
<script lang="ts" setup>
import logoUrl from '@/assets/images/logo.svg'
import illustrationUrl from '@/assets/images/illustration.svg'
import { Box } from '@/components/ui/box'
import { Button } from '@/components/ui/button'
import { RouterLink } from 'vue-router'
const appName = import.meta.env.VITE_APP_NAME
@@ -12,7 +10,7 @@ const sections = [
{
id: 'pengenalan',
title: '1. Pengenalan',
placeholder: 'Selamat datang ke SUTERA 3.0. Kami amat menghargai kepercayaan yang anda berikan untuk mengendalikan maklumat peribadi anda. Dasar Privasi ini digubal untuk membantu anda memahami bagaimana kami mengumpul, menggunakan, mendedahkan, dan melindungi data peribadi anda selaras dengan Akta Perlindungan Data Peribadi 2010 (PDPA) dan piawaian keselamatan global.\n\nDasar ini terpakai kepada semua TDM, dan mana-mana pihak yang mengakses atau menggunakan perkhidmatan, laman web, dan aplikasi kami.'
placeholder: 'Selamat datang ke MyKOPKB. Kami amat menghargai kepercayaan yang anda berikan untuk mengendalikan maklumat peribadi anda. Dasar Privasi ini digubal untuk membantu anda memahami bagaimana kami mengumpul, menggunakan, mendedahkan, dan melindungi data peribadi anda selaras dengan Akta Perlindungan Data Peribadi 2010 (PDPA) dan piawaian keselamatan global.\n\nDasar ini terpakai kepada semua Koperasi Permodalan Kelantan Berhad (KOPKB), dan mana-mana pihak yang mengakses atau menggunakan perkhidmatan, laman web, dan aplikasi kami.'
},
{
id: 'data-dikumpul',
@@ -60,7 +58,7 @@ const sections = [
id: 'hubungi',
title: '10. Hubungi Kami',
placeholder:
'Jika anda ada pertanyaan, komen atau permintaan tentang dasar ini, hubungi kami di\n- KAPT MOHAMMAD EFANDY BIN JAFFARI (effandy.jaffari@army.mil.my)'
'Jika anda ada pertanyaan, komen atau permintaan tentang dasar ini, hubungi kami di\n- ISMAIL BIN MASSERAN (ismail@koppkb.com)'
}
];
+1 -1
View File
@@ -8,7 +8,7 @@ import { Input } from '@/components/ui/input'
import { AlertRoot, AlertTitle, AlertDescription } from '@/components/ui/alert'
import { PasswordInput } from '@/components/ui/password-input'
import { getRegisterErrorMessage, register } from '@/modules/auth'
import illustrationUrl from '@/assets/images/logo-kopkb.svg'
import illustrationUrl from '@/assets/images/logo.svg'
const router = useRouter()
+15 -48
View File
@@ -12,7 +12,7 @@ import {
requestForgotPassword,
resetPassword,
} from '@/modules/auth'
import illustrationUrl from '@/assets/images/logo-kopkb.svg'
import illustrationUrl from '@/assets/images/logo.svg'
const route = useRoute()
const router = useRouter()
@@ -133,61 +133,28 @@ const onOtpInput = (event: Event) => {
</AlertRoot>
<form class="mt-8 flex flex-col gap-5" @submit.prevent="handleReset">
<Input
v-model="email"
class="box block min-w-full px-5 py-6 xl:min-w-md"
type="email"
placeholder="Email"
autocomplete="email"
required
/>
<Input
:model-value="otp"
class="box block min-w-full px-5 py-6 xl:min-w-md text-center tracking-[0.5em] text-lg"
type="text"
inputmode="numeric"
pattern="[0-9]*"
maxlength="6"
placeholder="000000"
autocomplete="one-time-code"
required
@input="onOtpInput"
/>
<PasswordInput
v-model="password"
class="box block min-w-full px-5 py-6 xl:min-w-md"
placeholder="Kata laluan baharu"
autocomplete="new-password"
required
/>
<PasswordInput
v-model="passwordConfirmation"
class="box block min-w-full px-5 py-6 xl:min-w-md"
placeholder="Sahkan kata laluan baharu"
autocomplete="new-password"
required
/>
<Input v-model="email" class="box block min-w-full px-5 py-6 xl:min-w-md" type="email"
placeholder="Email" autocomplete="email" required />
<Input :model-value="otp"
class="box block min-w-full px-5 py-6 xl:min-w-md text-center tracking-[0.5em] text-lg" type="text"
inputmode="numeric" pattern="[0-9]*" maxlength="6" placeholder="000000" autocomplete="one-time-code"
required @input="onOtpInput" />
<PasswordInput v-model="password" class="box block min-w-full px-5 py-6 xl:min-w-md"
placeholder="Kata laluan baharu" autocomplete="new-password" required />
<PasswordInput v-model="passwordConfirmation" class="box block min-w-full px-5 py-6 xl:min-w-md"
placeholder="Sahkan kata laluan baharu" autocomplete="new-password" required />
<div class="mt-5 text-center xl:mt-10 xl:text-left">
<Button class="box w-full px-4 py-5" variant="primary" type="submit"
:disabled="loading || !canSubmit">
{{ loading ? 'Menyimpan...' : 'Tetapkan Semula Kata Laluan' }}
</Button>
<Button
class="box mt-4 w-full px-4 py-5"
look="outline"
type="button"
:disabled="resendLoading || !email"
@click="handleResend"
>
<Button class="box mt-4 w-full px-4 py-5" look="outline" type="button"
:disabled="resendLoading || !email" @click="handleResend">
{{ resendLoading ? 'Menghantar...' : 'Hantar Semula Kod OTP' }}
</Button>
<Button
class="box mt-4 w-full px-4 py-5"
look="outline"
type="button"
@click="router.push({ name: 'login' })"
>
<Button class="box mt-4 w-full px-4 py-5" look="outline" type="button"
@click="router.push({ name: 'login' })">
Kembali ke Log Masuk
</Button>
</div>
@@ -1,7 +1,6 @@
<script lang="ts" setup>
import logoUrl from '@/assets/images/logo.svg'
import { Box } from '@/components/ui/box'
import { Button } from '@/components/ui/button'
import { RouterLink } from 'vue-router'
const appName = import.meta.env.VITE_APP_NAME
@@ -12,14 +11,14 @@ const sections = [
id: 'pengenalan',
title: '1. Pengenalan',
placeholder: `
Terma dan Syarat ini mengawal penggunaan platform, laman web, dan perkhidmatan yang disediakan oleh Kementerian Pertahanan Malaysia (MINDEF). Dengan mengakses atau menggunakan perkhidmatan kami, anda dianggap telah membaca, memahami, dan bersetuju untuk terikat dengan terma ini. Jika anda tidak bersetuju dengan mana-mana bahagian terma ini, sila hentikan penggunaan perkhidmatan kami.
Terma dan Syarat ini mengawal penggunaan platform, laman web, dan perkhidmatan yang disediakan oleh Koperasi Permodalan Kelantan Berhad (KOPKB). Dengan mengakses atau menggunakan perkhidmatan kami, anda dianggap telah membaca, memahami, dan bersetuju untuk terikat dengan terma ini. Jika anda tidak bersetuju dengan mana-mana bahagian terma ini, sila hentikan penggunaan perkhidmatan kami.
`,
},
{
id: 'akaun',
title: '2. Akaun & Kelayakan',
placeholder: `
Anda mesti berumur sekurang-kurangnya 18 tahun dan merupakan tentera. Anda bertanggungjawab memastikan maklumat akaun sentiasa tepat dan terkini. Anda juga bertanggungjawab menjaga kerahsiaan kata laluan serta segala aktiviti yang berlaku di bawah akaun anda.
Anda mesti berumur sekurang-kurangnya 18 tahun dan merupakan ahli KOPKB. Anda bertanggungjawab memastikan maklumat akaun sentiasa tepat dan terkini. Anda juga bertanggungjawab menjaga kerahsiaan kata laluan serta segala aktiviti yang berlaku di bawah akaun anda.
`,
},
{
@@ -40,7 +39,7 @@ Sesetengah perkhidmatan mungkin tertakluk kepada bayaran yang dinyatakan semasa
id: 'kandungan',
title: '5. Kandungan & Hak Milik',
placeholder: `
Semua kandungan, reka bentuk, logo, teks, grafik, dan bahan lain yang terdapat pada platform ini adalah hak milik Kementerian Pertahanan Malaysia (MINDEF) atau pemberi lesennya. Anda diberikan lesen terhad untuk menggunakan kandungan tersebut bagi tujuan penggunaan peribadi dan bukan komersial sahaja. Sebarang penyalinan, pengubahsuaian, atau pengedaran tanpa kebenaran bertulis adalah dilarang.
Semua kandungan, reka bentuk, logo, teks, grafik, dan bahan lain yang terdapat pada platform ini adalah hak milik Koperasi Permodalan Kelantan Berhad (KOPKB) atau pemberi lesennya. Anda diberikan lesen terhad untuk menggunakan kandungan tersebut bagi tujuan penggunaan peribadi dan bukan komersial sahaja. Sebarang penyalinan, pengubahsuaian, atau pengedaran tanpa kebenaran bertulis adalah dilarang.
`,
},
{
@@ -70,8 +69,8 @@ Kami boleh mengemas kini atau meminda Terma dan Syarat ini dari semasa ke semasa
placeholder: `
Sekiranya anda mempunyai sebarang pertanyaan berkaitan Terma dan Syarat ini, sila hubungi:
Nama: KAPT MOHAMMAD EFANDY BIN JAFFARI
E-mel: effandy.jaffari@army.mil.my
Nama: Koperasi Permodalan Kelantan Berhad (KOPKB)
E-mel: admin@koppkb.com
`,
},
] as const;
+2 -2
View File
@@ -12,8 +12,8 @@ import {
verifyEmail,
} from '@/modules/auth'
import { useAuthStore } from '@/stores/auth'
import logoUrl from '@/assets/images/logo-kopkb.svg'
import illustrationUrl from '@/assets/images/logo-kopkb.svg'
import logoUrl from '@/assets/images/logo.svg'
import illustrationUrl from '@/assets/images/logo.svg'
const route = useRoute()
const router = useRouter()
+2
View File
@@ -37,12 +37,14 @@ export interface AuthUser {
image_url: string | null
member_number: number | null
member_type: string | null
public_profile_token: string | null
status: string
gender: string | null
marriage_status: string | null
join_date: string | null
birth_date: string | null
birth_place: string | null
onboarding_completed_at: string | null
roles?: Array<AuthRole & { permissions?: AuthPermission[] }>
}
+1
View File
@@ -0,0 +1 @@
export { dashboardLayoutRoutes } from './routes'
+9
View File
@@ -0,0 +1,9 @@
import type { Menu } from '@/core/types/menu'
export const dashboardMenu: Menu[] = [
{
icon: 'CircleGauge',
route_name: 'dashboard-overview',
title: 'Dashboard',
}
]
@@ -1,8 +1,10 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { onMounted, ref } from 'vue'
import { today, getLocalTimeZone } from '@internationalized/date'
import phoneIllustration from '@/assets/images/phone-illustration.svg'
import womanIllustration from '@/assets/images/woman-illustration.svg'
import { completeOnboarding } from '@/modules/profile/services/profile.service'
import { useAuthStore } from '@/stores/auth'
import { Badge } from '@/components/ui/badge'
import { Box } from '@/components/ui/box'
import {
@@ -55,16 +57,44 @@ import { OfficialStores } from '@/components/official-stores'
import { WeeklyBestSellers } from '@/components/weekly-best-sellers'
import { WeeklyTopProducts } from '@/components/weekly-top-products'
const authStore = useAuthStore()
const pc = ref(false)
const electronic = ref(false)
const smartphone = ref(false)
const photography = ref(false)
const sport = ref(false)
const onboardingDialog = ref(true)
const onboardingDialog = ref(false)
const salesReportDate = ref<any[]>([
today(getLocalTimeZone()).subtract({ days: 7 }),
today(getLocalTimeZone()),
])
onMounted(() => {
onboardingDialog.value = !authStore.user?.onboarding_completed_at
})
async function markOnboardingComplete() {
if (authStore.user?.onboarding_completed_at) {
return
}
try {
const res = await completeOnboarding()
if (res.success) {
authStore.setUserProfile(res.data)
}
} catch {
// Will retry on next visit if the request failed
}
}
function handleOnboardingOpenChange(details: { open: boolean }) {
onboardingDialog.value = details.open
if (!details.open) {
void markOnboardingComplete()
}
}
</script>
<template>
@@ -351,7 +381,7 @@ const salesReportDate = ref<any[]>([
</div>
<div class="relative ms-auto flex-none">
<Donut2 class="w-[90px] h-[90px]" />
<div class="absolute start-0 top-0 flex h-full w-full items-center justify-center font-medium">
<div class="absolute inset-s-0 top-0 flex h-full w-full items-center justify-center font-medium">
20%
</div>
</div>
@@ -378,7 +408,7 @@ const salesReportDate = ref<any[]>([
</div>
<div class="relative ms-auto flex-none">
<Donut2 class="w-[90px] h-[90px]" />
<div class="absolute start-0 top-0 flex h-full w-full items-center justify-center font-medium">
<div class="absolute inset-s-0 top-0 flex h-full w-full items-center justify-center font-medium">
45%
</div>
</div>
@@ -435,7 +465,7 @@ const salesReportDate = ref<any[]>([
</div>
</div>
<!-- BEGIN: Onboarding Dialog -->
<DialogRoot :open="onboardingDialog" @openChange="(details) => (onboardingDialog = details.open)">
<DialogRoot :open="onboardingDialog" @openChange="handleOnboardingOpenChange">
<DialogContent class="sm:max-w-xl">
<DialogCloseTrigger />
<div class="overflow-hidden">
@@ -445,16 +475,14 @@ const salesReportDate = ref<any[]>([
<CarouselItem class="border-transparent bg-none bg-transparent" :index="0">
<div class="relative mx-3 flex flex-col items-center gap-1 px-3.5 pb-20">
<div
class="w-full bg-primary/[.05] mb-7 border-primary/10 shadow-lg shadow-black/10 relative rounded-3xl border h-52 overflow-hidden before:bg-noise before:absolute before:inset-0 before:opacity-30 after:bg-accent after:absolute after:inset-0 after:opacity-30 after:blur-2xl">
class="w-full bg-primary/5 mb-7 border-primary/10 shadow-lg shadow-black/10 relative rounded-3xl border h-52 overflow-hidden before:bg-noise before:absolute before:inset-0 before:opacity-30 after:bg-accent after:absolute after:inset-0 after:opacity-30 after:blur-2xl">
<img class="absolute inset-0 mx-auto mt-10 w-2/5 scale-125" :src="phoneIllustration"
alt="Midone - Tailwind Admin Dashboard Template" />
alt="MyKOPKB" />
</div>
<div class="px-8">
<div class="text-center text-xl font-medium">Welcome to Midone Admin!</div>
<div class="text-center text-xl font-medium">Selamat Datang ke Sistem MyKOPKB</div>
<div class="mt-3 text-center text-base leading-relaxed opacity-70">
Premium admin dashboard template for all kinds <br />
of projects. With a unique and modern design, Midone offers the perfect
foundation to build professional applications with ease.
Sistem MyKOPKB adalah sistem yang membantu pengguna untuk menguruskan permohonan keahlian KOPKB.
</div>
</div>
<div class="absolute inset-x-0 bottom-0 flex place-content-between px-5">
@@ -472,9 +500,9 @@ const salesReportDate = ref<any[]>([
<CarouselItem class="border-transparent bg-none bg-transparent" :index="1">
<div class="relative mx-3 flex flex-col items-center gap-1 px-3.5 pb-20">
<div
class="w-full bg-primary/[.05] mb-7 border-primary/10 shadow-lg shadow-black/10 relative rounded-3xl border h-52 overflow-hidden before:bg-noise before:absolute before:inset-0 before:opacity-30 after:bg-accent after:absolute after:inset-0 after:opacity-30 after:blur-2xl">
class="w-full bg-primary/5 mb-7 border-primary/10 shadow-lg shadow-black/10 relative rounded-3xl border h-52 overflow-hidden before:bg-noise before:absolute before:inset-0 before:opacity-30 after:bg-accent after:absolute after:inset-0 after:opacity-30 after:blur-2xl">
<img class="absolute inset-0 mx-auto mt-10 w-2/5 scale-125" :src="womanIllustration"
alt="Midone - Tailwind Admin Dashboard Template" />
alt="MyKOPKB" />
</div>
<div class="w-full">
<div class="text-center text-xl font-medium">Example Request Information</div>
@@ -505,6 +533,11 @@ const salesReportDate = ref<any[]>([
<CarouselPrevTrigger class="text-primary flex items-center gap-3 font-medium ms-0 px-5">
<Lucide icon="MoveLeft" /> Previous
</CarouselPrevTrigger>
<a class="text-primary flex items-center gap-3 font-medium cursor-pointer me-0 px-5"
@click.prevent="onboardingDialog = false">
Selesai
<Lucide icon="MoveRight" />
</a>
</div>
</div>
</CarouselItem>
@@ -0,0 +1,258 @@
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'
import { RouterLink, useRouter } from 'vue-router'
import phoneIllustration from '@/assets/images/phone-illustration.svg'
import womanIllustration from '@/assets/images/woman-illustration.svg'
import { AlertRoot, AlertTitle, AlertDescription } from '@/components/ui/alert'
import { Badge } from '@/components/ui/badge'
import { Box } from '@/components/ui/box'
import { Button } from '@/components/ui/button'
import {
CarouselRoot,
CarouselPrevTrigger,
CarouselNextTrigger,
CarouselItemGroup,
CarouselItem,
} from '@/components/ui/carousel'
import { DialogRoot, DialogContent, DialogCloseTrigger } from '@/components/ui/dialog'
import { Lucide, type Icon } from '@/components/ui/lucide'
import { usePermissions } from '@/composables/usePermissions'
import { completeOnboarding } from '@/modules/profile/services/profile.service'
import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore()
const router = useRouter()
const { hasPermission } = usePermissions()
const onboardingDialog = ref(false)
const appName = import.meta.env.VITE_APP_NAME
const appVersion = import.meta.env.VITE_APP_VERSION
const greeting = computed(() => {
const hour = new Date().getHours()
if (hour < 12) return 'Selamat pagi'
if (hour < 18) return 'Selamat petang'
return 'Selamat malam'
})
const availableModules = computed(() => [
{
title: 'Profil',
description: 'Kemas kini maklumat peribadi, pekerjaan, bank dan waris.',
route: 'profile-overview-2',
icon: 'User' as Icon,
visible: true,
},
{
title: 'Permohonan Keahlian',
description: 'Urus dan semak permohonan keahlian KOPKB.',
route: 'list-membership-applications',
icon: 'ClipboardList' as Icon,
visible: hasPermission('lihat permohonan keahlian'),
},
{
title: 'Aktiviti',
description: 'Lihat dan urus aktiviti serta laporan berkaitan.',
route: 'list-activities',
icon: 'CalendarDays' as Icon,
visible: hasPermission('lihat aktiviti'),
},
{
title: 'Senarai Pengguna',
description: 'Pantau dan urus akaun pengguna sistem.',
route: 'list-users',
icon: 'Users' as Icon,
visible: hasPermission('lihat pengguna'),
},
].filter((module) => module.visible))
const upcomingFeatures = [
'Ringkasan statistik keahlian',
'Carta dan laporan bulanan',
'Notifikasi dan aktiviti terkini',
'Papan pemuka pentadbir',
]
onMounted(() => {
onboardingDialog.value = !authStore.user?.onboarding_completed_at
})
async function markOnboardingComplete() {
if (authStore.user?.onboarding_completed_at) {
return
}
try {
const res = await completeOnboarding()
if (res.success) {
authStore.setUserProfile(res.data)
}
} catch {
// Will retry on next visit if the request failed
}
}
async function finishOnboarding() {
onboardingDialog.value = false
await markOnboardingComplete()
await router.push({ name: 'profile-overview-2' })
}
function handleOnboardingOpenChange(details: { open: boolean }) {
onboardingDialog.value = details.open
if (!details.open) {
void markOnboardingComplete()
}
}
</script>
<template>
<div>
<AlertRoot class="mb-6" look="outline" variant="primary">
<Lucide class="mr-2 size-4 shrink-0" icon="Construction" />
<AlertTitle>Sistem Dalam Pembangunan</AlertTitle>
<AlertDescription>
{{ appName }} {{ appVersion }} masih dalam fasa pembangunan. Beberapa fungsi papan pemuka
belum tersedia. Sila gunakan modul sedia ada melalui menu sisi atau pautan di bawah.
</AlertDescription>
</AlertRoot>
<div class="grid grid-cols-12 gap-6">
<div class="col-span-12">
<Box class="relative overflow-hidden p-6 sm:p-8">
<div class="pointer-events-none absolute -right-10 -top-10 size-40 rounded-full bg-primary/10 blur-2xl" />
<div class="relative">
<Badge variant="pending" class="mb-4">Beta</Badge>
<h2 class="text-2xl font-semibold">
{{ greeting }}{{ authStore.userName ? `, ${authStore.userName}` : '' }}
</h2>
<p class="mt-2 max-w-2xl text-base leading-relaxed opacity-70">
Selamat datang ke {{ appName }}. Papan pemuka utama sedang dibangunkan buat masa ini,
anda boleh mula dengan mengemas kini profil atau mengurus permohonan keahlian.
</p>
</div>
</Box>
</div>
<div class="col-span-12 lg:col-span-7">
<div class="mb-4 flex h-10 items-center">
<h3 class="text-lg font-medium">Modul Tersedia</h3>
</div>
<div class="grid gap-4 sm:grid-cols-2">
<Box v-for="module in availableModules" :key="module.route"
class="flex h-full flex-col p-5 transition-colors hover:bg-foreground/2">
<div class="mb-4 flex size-10 items-center justify-center rounded-xl bg-primary/10 text-primary">
<Lucide :icon="module.icon" class="size-5" />
</div>
<div class="text-base font-medium">{{ module.title }}</div>
<p class="mt-2 flex-1 text-sm leading-relaxed opacity-70">
{{ module.description }}
</p>
<Button as-child variant="ghost" class="mt-5 w-full border border-foreground/15">
<RouterLink :to="{ name: module.route }">
Pergi ke {{ module.title }}
<Lucide icon="ArrowRight" class="size-4" />
</RouterLink>
</Button>
</Box>
</div>
</div>
<div class="col-span-12 lg:col-span-5">
<div class="mb-4 flex h-10 items-center">
<h3 class="text-lg font-medium">Akan Datang</h3>
</div>
<Box class="p-5">
<p class="text-sm leading-relaxed opacity-70">
Ciri-ciri berikut sedang dirancang untuk papan pemuka ini:
</p>
<ul class="mt-4 space-y-3">
<li v-for="feature in upcomingFeatures" :key="feature" class="flex items-start gap-3 text-sm">
<Lucide icon="CircleDashed" class="mt-0.5 size-4 shrink-0 text-primary/70" />
<span>{{ feature }}</span>
</li>
</ul>
</Box>
<Box class="mt-4 p-5">
<div class="flex items-start gap-3">
<Lucide icon="Lightbulb" class="mt-0.5 size-4 shrink-0 text-warning" />
<div>
<div class="text-sm font-medium">Petua</div>
<p class="mt-1 text-sm leading-relaxed opacity-70">
Pastikan profil anda lengkap dan terkini. Maklumat yang
tepat membantu proses kelulusan berjalan lebih lancar.
</p>
</div>
</div>
</Box>
</div>
</div>
<!-- BEGIN: Onboarding Dialog -->
<DialogRoot :open="onboardingDialog" @openChange="handleOnboardingOpenChange">
<DialogContent class="sm:max-w-xl">
<DialogCloseTrigger />
<div class="overflow-hidden">
<div class="-my-5 -mx-10">
<CarouselRoot :default-page="0" :slide-count="2" class="border-0">
<CarouselItemGroup>
<CarouselItem class="border-transparent bg-none bg-transparent" :index="0">
<div class="relative mx-3 flex flex-col items-center gap-1 px-3.5 pb-20">
<div
class="w-full bg-primary/5 mb-7 border-primary/10 shadow-lg shadow-black/10 relative rounded-3xl border h-52 overflow-hidden before:bg-noise before:absolute before:inset-0 before:opacity-30 after:bg-accent after:absolute after:inset-0 after:opacity-30 after:blur-2xl">
<img class="absolute inset-0 mx-auto mt-10 w-2/5 scale-125" :src="phoneIllustration"
alt="MyKOPKB" />
</div>
<div class="px-8">
<div class="text-center text-xl font-medium">Selamat Datang ke Sistem MyKOPKB</div>
<div class="mt-3 text-center text-base leading-relaxed opacity-70">
Sistem MyKOPKB adalah sistem integrasi bagi ahli Koperasi Permodalan Kelantan Berhad (KOPKB).
</div>
</div>
<div class="absolute inset-x-0 bottom-0 flex justify-end px-5">
<CarouselNextTrigger class="text-primary flex items-center gap-3 font-medium me-0 px-5">
Seterusnya
<Lucide icon="MoveRight" />
</CarouselNextTrigger>
</div>
</div>
</CarouselItem>
<CarouselItem class="border-transparent bg-none bg-transparent" :index="1">
<div class="relative mx-3 flex flex-col items-center gap-1 px-3.5 pb-20">
<div
class="w-full bg-primary/5 mb-7 border-primary/10 shadow-lg shadow-black/10 relative rounded-3xl border h-52 overflow-hidden before:bg-noise before:absolute before:inset-0 before:opacity-30 after:bg-accent after:absolute after:inset-0 after:opacity-30 after:blur-2xl">
<img class="absolute inset-0 mx-auto mt-10 w-2/5 scale-125" :src="womanIllustration"
alt="MyKOPKB" />
</div>
<div class="px-8">
<div class="text-center text-xl font-medium">Mulakan dengan Profil Anda</div>
<div class="mt-3 text-center text-base leading-relaxed opacity-70">
Lengkapkan maklumat peribadi, pekerjaan dan butiran bank.
</div>
</div>
<div class="absolute inset-x-0 bottom-0 flex place-content-between px-5">
<CarouselPrevTrigger class="text-primary flex items-center gap-3 font-medium ms-0 px-5">
<Lucide icon="MoveLeft" /> Kembali
</CarouselPrevTrigger>
<a class="inline-flex items-center gap-2 rounded-lg bg-primary px-6 py-3 font-semibold text-white shadow-md transition-all duration-200 hover:bg-primary/90 hover:shadow-lg active:scale-95 cursor-pointer"
@click.prevent="finishOnboarding">
<span>Selesai</span>
<Lucide icon="Check" class="h-5 w-5" />
</a>
</div>
</div>
</CarouselItem>
</CarouselItemGroup>
</CarouselRoot>
</div>
</div>
</DialogContent>
</DialogRoot>
<!-- END: Onboarding Dialog -->
</div>
</template>
+10
View File
@@ -0,0 +1,10 @@
import type { RouteRecordRaw } from 'vue-router'
export const dashboardLayoutRoutes: RouteRecordRaw[] = [
{
path: '/',
name: 'dashboard-overview',
component: () => import('./pages/DashboardOverview.vue'),
meta: { title: 'Dashboard Overview', module: 'dashboard' },
},
]
@@ -0,0 +1,41 @@
import { computed, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { useExternalSystemList } from './useExternalSystemList'
import {
getExternalSystemStatus,
isExternalSystemAccessible,
} from '../utils/external-system.utils'
export function useExternalSystemDetail() {
const route = useRoute()
const { getSystemById } = useExternalSystemList()
const loading = ref(false)
const error = ref<string | null>(null)
const systemId = computed(() => String(route.params.id ?? ''))
const system = computed(() => getSystemById(systemId.value) ?? null)
const status = computed(() => (system.value ? getExternalSystemStatus(system.value) : null))
const isAccessible = computed(() =>
system.value ? isExternalSystemAccessible(system.value) : false,
)
watch(
systemId,
() => {
error.value = system.value ? null : 'Sistem luaran tidak dijumpai.'
},
{ immediate: true },
)
return {
system,
loading,
error,
status,
isAccessible,
}
}
@@ -0,0 +1,48 @@
import { computed, ref } from 'vue'
import { dummyExternalSystems } from '../data/dummy-external-systems'
import {
externalSystemStatusLabel,
getExternalSystemStatus,
} from '../utils/external-system.utils'
import type { ExternalSystem } from '../types/external-system.types'
export function useExternalSystemList() {
const search = ref('')
const loading = ref(false)
const systems = computed(() => {
const query = search.value.trim().toLowerCase()
if (!query) {
return dummyExternalSystems
}
return dummyExternalSystems.filter((system) => {
const haystack = [
system.name,
system.code,
system.description,
externalSystemStatusLabel(getExternalSystemStatus(system)),
]
.join(' ')
.toLowerCase()
return haystack.includes(query)
})
})
const availableCount = computed(
() => systems.value.filter((system) => getExternalSystemStatus(system) === 'available').length,
)
function getSystemById(id: string): ExternalSystem | undefined {
return dummyExternalSystems.find((system) => system.id === id)
}
return {
systems,
search,
loading,
availableCount,
getSystemById,
}
}
@@ -0,0 +1,38 @@
import type { ExternalSystem } from '../types/external-system.types'
export const dummyExternalSystems: ExternalSystem[] = [
{
id: 'ext-001',
code: 'KOPKB-PORTAL',
name: 'Portal Ahli KOPKB',
description:
'Sistem utama keahlian Koperasi Permodalan Kelantan Berhad untuk semakan dividen, penyata dan maklumat ahli.',
url: 'https://anggota.koppkb.com',
icon: 'Users',
is_active: true,
starts_at: '2026-01-01T00:00:00+08:00',
ends_at: null,
opens_in_new_tab: true,
contact_email: 'sokongan@koppkb.com',
notes: 'Log masuk menggunakan e-mel berdaftar ahli KOPKB.',
created_at: '2026-01-15T09:00:00+08:00',
updated_at: '2026-06-01T14:30:00+08:00',
},
{
id: 'ext-002',
code: 'AGM-VOTE',
name: 'Sistem Pengundian AGM',
description:
'Platform pengundian dalam talian untuk Mesyuarat Agung Tahunan. Hanya tersedia semasa tempoh pengundian.',
url: 'https://e-vote.erahn.com.my/login',
icon: 'Vote',
is_active: true,
starts_at: '2026-05-01T08:00:00+08:00',
ends_at: null,
opens_in_new_tab: true,
contact_email: 'agm@koppkb.com',
notes: 'Sila lengkapkan profil sebelum mengundi.',
created_at: '2026-05-20T10:00:00+08:00',
updated_at: '2026-06-28T11:15:00+08:00',
},
]
+2
View File
@@ -0,0 +1,2 @@
export { externalSystemLayoutRoutes } from './routes'
export { externalSystemMenu } from './menu'
+9
View File
@@ -0,0 +1,9 @@
import type { Menu } from '@/core/types/menu'
export const externalSystemMenu: Menu[] = [
{
icon: 'ExternalLink',
route_name: 'list-external-systems',
title: 'Sistem Luaran',
},
]
@@ -0,0 +1,115 @@
<script lang="ts" setup>
import { useRouter } from 'vue-router'
import { AlertRoot, AlertTitle, AlertDescription } from '@/components/ui/alert'
import { Badge } from '@/components/ui/badge'
import { Box } from '@/components/ui/box'
import { Button } from '@/components/ui/button'
import { Lucide } from '@/components/ui/lucide'
import { useExternalSystemDetail } from '../composables/useExternalSystemDetail'
import {
externalSystemStatusLabel,
externalSystemStatusVariant,
formatExternalSystemDateTime,
openExternalSystem,
} from '../utils/external-system.utils'
const router = useRouter()
const { system, error, status, isAccessible } = useExternalSystemDetail()
function goBack() {
router.push({ name: 'list-external-systems' })
}
function handleOpen() {
if (!system.value) return
openExternalSystem(system.value)
}
</script>
<template>
<div>
<div class="mb-5 flex flex-wrap items-center gap-3">
<Button variant="ghost" look="outline" @click="goBack">
<Lucide icon="ArrowLeft" class="size-4" />
Kembali
</Button>
</div>
<AlertRoot v-if="error" class="mb-6" variant="danger">
<AlertTitle>Ralat</AlertTitle>
<AlertDescription>{{ error }}</AlertDescription>
</AlertRoot>
<template v-else-if="system && status">
<Box class="p-5 sm:p-6">
<div class="flex flex-wrap items-start justify-between gap-4">
<div class="flex min-w-0 items-start gap-4">
<div
class="flex size-12 shrink-0 items-center justify-center rounded-2xl bg-primary/10 text-primary"
>
<Lucide :icon="system.icon" class="size-6" />
</div>
<div class="min-w-0">
<div class="text-sm opacity-70">Sistem Luaran</div>
<h2 class="text-xl font-semibold">{{ system.name }}</h2>
<div class="mt-1 text-sm font-medium text-primary/80">{{ system.code }}</div>
</div>
</div>
<Badge look="outline" :variant="externalSystemStatusVariant(status)">
{{ externalSystemStatusLabel(status) }}
</Badge>
</div>
<p class="mt-5 max-w-3xl text-sm leading-relaxed opacity-80">
{{ system.description }}
</p>
<div class="mt-6 grid gap-3 text-sm sm:grid-cols-2 lg:grid-cols-4">
<div>
<span class="opacity-70">URL:</span>
<div class="mt-0.5 break-all font-medium">{{ system.url }}</div>
</div>
<div>
<span class="opacity-70">Tarikh Mula:</span>
<div class="mt-0.5 font-medium">{{ formatExternalSystemDateTime(system.starts_at) }}</div>
</div>
<div>
<span class="opacity-70">Tarikh Tamat:</span>
<div class="mt-0.5 font-medium">{{ formatExternalSystemDateTime(system.ends_at) }}</div>
</div>
<div>
<span class="opacity-70">E-mel Sokongan:</span>
<div class="mt-0.5 font-medium">{{ system.contact_email ?? '-' }}</div>
</div>
</div>
<div v-if="system.notes" class="mt-6 border-t border-foreground/10 pt-5">
<div class="text-sm font-medium opacity-70">Nota</div>
<p class="mt-2 text-sm leading-relaxed opacity-80">{{ system.notes }}</p>
</div>
<div class="mt-6 flex flex-wrap gap-3 border-t border-foreground/10 pt-5">
<Button
look="outline"
variant="primary"
:disabled="!isAccessible"
@click="handleOpen"
>
Buka Sistem
<Lucide icon="ExternalLink" class="size-4" />
</Button>
</div>
<AlertRoot v-if="!isAccessible" class="mt-4" look="outline" variant="pending">
<Lucide class="mr-2 size-4 shrink-0" icon="Clock" />
<AlertTitle>Sistem Tidak Tersedia</AlertTitle>
<AlertDescription>
Pautan ini hanya boleh dibuka semasa tempoh acara yang ditetapkan dan sistem berstatus
aktif.
</AlertDescription>
</AlertRoot>
</Box>
</template>
</div>
</template>
@@ -0,0 +1,104 @@
<script lang="ts" setup>
import { useRouter } from 'vue-router'
import { AlertRoot, AlertTitle, AlertDescription } from '@/components/ui/alert'
import { Badge } from '@/components/ui/badge'
import { Box } from '@/components/ui/box'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Lucide } from '@/components/ui/lucide'
import { useExternalSystemList } from '../composables/useExternalSystemList'
import {
externalSystemStatusLabel,
externalSystemStatusVariant,
formatExternalSystemDateTime,
getExternalSystemStatus,
openExternalSystem,
} from '../utils/external-system.utils'
import type { ExternalSystem } from '../types/external-system.types'
const router = useRouter()
const { systems, search, loading, availableCount } = useExternalSystemList()
function goToDetail(system: ExternalSystem) {
router.push({ name: 'view-external-system', params: { id: system.id } })
}
function handleOpen(system: ExternalSystem) {
openExternalSystem(system)
}
</script>
<template>
<div>
<div class="flex flex-wrap items-start justify-between gap-4">
<div>
<h2 class="text-lg font-medium">Sistem Luaran</h2>
</div>
<Badge look="outline" variant="primary">
{{ availableCount }} tersedia
</Badge>
</div>
<div class="mt-5 grid grid-cols-12 gap-x-6 gap-y-8">
<div class="col-span-12 mt-2 flex flex-wrap items-center sm:flex-nowrap">
<div class="w-full sm:w-auto">
<div class="relative w-56">
<Input v-model="search" class="w-56 pr-10" type="search" placeholder="Cari sistem..." :disabled="loading" />
<Lucide class="absolute inset-y-0 right-0 my-auto mr-3 h-4 w-4" icon="Search" />
</div>
</div>
</div>
<template v-if="systems.length">
<Box v-for="system in systems" :key="system.id"
class="col-span-12 flex h-full flex-col p-5 md:col-span-6 xl:col-span-4">
<div class="mb-4 flex items-start justify-between gap-3">
<div class="flex size-10 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary">
<Lucide :icon="system.icon" class="size-5" />
</div>
<Badge look="outline" :variant="externalSystemStatusVariant(getExternalSystemStatus(system))">
{{ externalSystemStatusLabel(getExternalSystemStatus(system)) }}
</Badge>
</div>
<div class="text-base font-medium">{{ system.name }}</div>
<div class="mt-1 text-xs font-medium uppercase tracking-wide text-primary/80">
{{ system.code }}
</div>
<p class="mt-2 flex-1 text-sm leading-relaxed opacity-70">
{{ system.description }}
</p>
<div class="mt-4 space-y-1 text-xs opacity-70">
<div>
<span class="font-medium">Mula:</span>
{{ formatExternalSystemDateTime(system.starts_at) }}
</div>
<div>
<span class="font-medium">Tamat:</span>
{{ formatExternalSystemDateTime(system.ends_at) }}
</div>
</div>
<div class="mt-5 flex flex-col gap-2 sm:flex-row">
<Button class="w-full sm:flex-1" look="outline" variant="primary"
:disabled="getExternalSystemStatus(system) !== 'available'" @click="handleOpen(system)">
Buka Sistem
<Lucide icon="ExternalLink" class="size-4" />
</Button>
<Button class="w-full sm:flex-1" variant="ghost" @click="goToDetail(system)">
Butiran
<Lucide icon="ArrowRight" class="size-4" />
</Button>
</div>
</Box>
</template>
<Box v-else class="col-span-12 p-8 text-center">
<Lucide icon="SearchX" class="mx-auto size-8 opacity-40" />
<div class="mt-3 text-base font-medium">Tiada sistem dijumpai</div>
<p class="mt-1 text-sm opacity-70">Cuba istilah carian yang berbeza.</p>
</Box>
</div>
</div>
</template>
+22
View File
@@ -0,0 +1,22 @@
import type { RouteRecordRaw } from 'vue-router'
export const externalSystemLayoutRoutes: RouteRecordRaw[] = [
{
path: 'external-systems',
name: 'list-external-systems',
component: () => import('./pages/ExternalSystemList.vue'),
meta: {
title: 'Senarai Sistem Luaran',
module: 'external-system',
},
},
{
path: 'external-systems/:id',
name: 'view-external-system',
component: () => import('./pages/ExternalSystemDetail.vue'),
meta: {
title: 'Butiran Sistem Luaran',
module: 'external-system',
},
},
]
@@ -0,0 +1,20 @@
import type { Icon } from '@/components/ui/lucide'
export type ExternalSystemStatus = 'available' | 'upcoming' | 'ended' | 'inactive'
export type ExternalSystem = {
id: string
code: string
name: string
description: string
url: string
icon: Icon
is_active: boolean
starts_at: string | null
ends_at: string | null
opens_in_new_tab: boolean
contact_email: string | null
notes: string | null
created_at: string
updated_at: string
}
@@ -0,0 +1,74 @@
import dayjs from 'dayjs'
import type { BadgeVariants } from '@/components/ui/styles/badge.styles'
import type { ExternalSystem, ExternalSystemStatus } from '../types/external-system.types'
export function getExternalSystemStatus(
system: ExternalSystem,
now = dayjs(),
): ExternalSystemStatus {
if (!system.is_active) {
return 'inactive'
}
const startsAt = system.starts_at ? dayjs(system.starts_at) : null
const endsAt = system.ends_at ? dayjs(system.ends_at) : null
if (startsAt?.isAfter(now)) {
return 'upcoming'
}
if (endsAt?.isBefore(now)) {
return 'ended'
}
return 'available'
}
export function isExternalSystemAccessible(system: ExternalSystem, now = dayjs()): boolean {
return getExternalSystemStatus(system, now) === 'available'
}
export function externalSystemStatusLabel(status: ExternalSystemStatus): string {
switch (status) {
case 'available':
return 'Tersedia'
case 'upcoming':
return 'Akan Datang'
case 'ended':
return 'Tamat'
case 'inactive':
return 'Tidak Aktif'
}
}
export function externalSystemStatusVariant(
status: ExternalSystemStatus,
): NonNullable<BadgeVariants['variant']> {
switch (status) {
case 'available':
return 'success'
case 'upcoming':
return 'pending'
case 'ended':
return 'ghost'
case 'inactive':
return 'danger'
}
}
export function formatExternalSystemDateTime(value: string | null): string {
if (!value) return '-'
return dayjs(value).format('DD MMM YYYY, HH:mm')
}
export function openExternalSystem(system: ExternalSystem) {
if (!isExternalSystemAccessible(system)) {
return
}
window.open(
system.url,
system.opens_in_new_tab ? '_blank' : '_self',
'noopener,noreferrer',
)
}
@@ -4,7 +4,7 @@ import * as select from '@zag-js/select'
import { RouterLink } from 'vue-router'
import { Box } from '@/components/ui/box'
import { Button } from '@/components/ui/button'
import { Field, FieldError, FieldLabel } from '@/components/ui/field'
import { Field, FieldDescription, FieldError, FieldLabel } from '@/components/ui/field'
import { Input } from '@/components/ui/input'
import {
SelectRoot,
@@ -27,16 +27,19 @@ import type {
MembershipApplicationHeirForm,
MembershipApplicationReferenceForm,
} from '../types/membership-application.types'
import illustrationUrl from '@/assets/images/logo-kopkb.svg'
import illustrationUrl from '@/assets/images/logo.svg'
const MAX_FILE_SIZE_MB = 10
const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024
const MIN_STOCK_MONTHLY_CONTRIBUTION = 50
const INITIAL_MANDATORY_STOCK_MONTHLY_CONTRIBUTION = 84
const MIN_FEE_MONTHLY_CONTRIBUTION = 30
const steps = [
{ id: 1, label: 'Maklumat Peribadi' },
{ id: 2, label: 'Hubungan & Alamat' },
{ id: 3, label: 'Maklumat Pekerjaan' },
{ id: 4, label: 'Maklumat Waris' },
{ id: 4, label: 'Maklumat Penama' },
{ id: 5, label: 'Dokumen & Hantar' },
] as const
@@ -73,6 +76,31 @@ const RELATIONSHIP_OPTIONS: SelectOption[] = [
{ label: 'Lain-lain', value: 'Lain-lain' },
]
// TODO: replace with API lookup
const EMPLOYERS = [
{
name: 'INFRA QUEST SDN BHD',
address: 'Lot 1045, Jalan Dato Lundang, 15200 Kota Bharu, Kelantan',
},
{
name: 'Permodalan Kelantan Berhad',
address: 'Permodalan Kelantan Berhad, Tingkat 4, Wisma Permodalan Kelantan Berhad, Jalan Maju, 15000 Kota Bharu Kelantan',
},
{
name: 'Koperasi Permodalan Kelantan Berhad',
address: 'Lot Pt 448, Tingkat 1,Jalan Kuala Krai, Batu 3, Wakaf Che Yeh, 15150 Kota Bharu, Kelantan.',
},
{
name: "An-Nisa'",
address: 'Jln Sultan Ibrahim, Bandar Kota Bharu, 15050 Kota Bharu, Kelantan.',
},
] as const
const EMPLOYER_OPTIONS: SelectOption[] = EMPLOYERS.map((employer) => ({
label: employer.name,
value: employer.name,
}))
function createSelectCollection(options: SelectOption[]) {
return select.collection({
items: options,
@@ -94,6 +122,11 @@ function apiValueToLabel(options: SelectOption[], value: string | undefined): st
const genderCollection = createSelectCollection(GENDER_OPTIONS)
const marriageStatusCollection = createSelectCollection(MARRIAGE_STATUS_OPTIONS)
const relationshipCollection = createSelectCollection(RELATIONSHIP_OPTIONS)
const employerCollection = createSelectCollection(EMPLOYER_OPTIONS)
function getEmployerAddress(name: string): string {
return EMPLOYERS.find((employer) => employer.name === name)?.address ?? ''
}
function createEmptyReference(): MembershipApplicationReferenceForm {
return {
@@ -149,6 +182,7 @@ const genderInitial = computed(() => apiValueToLabel(GENDER_OPTIONS, form.applic
const marriageStatusInitial = computed(() =>
apiValueToLabel(MARRIAGE_STATUS_OPTIONS, form.applicant.marriage_status),
)
const employerInitial = computed(() => apiValueToLabel(EMPLOYER_OPTIONS, form.applicant.employer_name))
function setGenderValue(details: { value: string[] }) {
form.applicant.gender = labelToApiValue(GENDER_OPTIONS, details.value[0])
@@ -160,6 +194,14 @@ function setMarriageStatusValue(details: { value: string[] }) {
delete fieldErrors['applicant.marriage_status']
}
function setEmployerValue(details: { value: string[] }) {
const employerName = labelToApiValue(EMPLOYER_OPTIONS, details.value[0])
form.applicant.employer_name = employerName
form.applicant.employer_address = getEmployerAddress(employerName)
delete fieldErrors['applicant.employer_name']
delete fieldErrors['applicant.employer_address']
}
function setHeirRelationshipValue(index: number, details: { value: string[] }) {
const heir = form.heirs[index]
if (!heir) return
@@ -254,7 +296,7 @@ const stepTitle = computed(() => {
case 3:
return 'Maklumat Pekerjaan & Caruman'
case 4:
return 'Maklumat Waris'
return 'Maklumat Penama'
default:
return 'Dokumen & Pengesahan'
}
@@ -269,7 +311,7 @@ const stepDescription = computed(() => {
case 3:
return 'Masukkan maklumat pekerjaan dan caruman bulanan.'
case 4:
return 'Tambah sekurang-kurangnya satu waris.'
return 'Tambah sekurang-kurangnya satu Penama.'
default:
return 'Muat naik dokumen sokongan dan semak maklumat sebelum hantar.'
}
@@ -324,21 +366,45 @@ function validateStep(step: number): boolean {
requireField(
'applicant.stock_monthly_contribution',
form.applicant.stock_monthly_contribution,
'Caruman saham bulanan',
'Potongan modal syer minima',
)
const stockContribution = Number(form.applicant.stock_monthly_contribution)
if (
form.applicant.stock_monthly_contribution &&
(Number.isNaN(stockContribution) || stockContribution < MIN_STOCK_MONTHLY_CONTRIBUTION)
) {
setError(
'applicant.stock_monthly_contribution',
`Potongan modal syer minima mestilah sekurang-kurangnya RM${MIN_STOCK_MONTHLY_CONTRIBUTION}.`,
)
valid = false
}
requireField(
'applicant.fee_monthly_contribution',
form.applicant.fee_monthly_contribution,
'Caruman yuran bulanan',
'Potongan yuran bulanan',
)
const feeContribution = Number(form.applicant.fee_monthly_contribution)
if (
form.applicant.fee_monthly_contribution &&
(Number.isNaN(feeContribution) || feeContribution < MIN_FEE_MONTHLY_CONTRIBUTION)
) {
setError(
'applicant.fee_monthly_contribution',
`Potongan yuran bulanan mestilah sekurang-kurangnya RM${MIN_FEE_MONTHLY_CONTRIBUTION}.`,
)
valid = false
}
}
if (step === 4) {
form.heirs.forEach((heir, index) => {
requireField(`heirs.${index}.name`, heir.name, `Nama waris ${index + 1}`)
requireField(`heirs.${index}.ic_number`, heir.ic_number, `No. KP waris ${index + 1}`)
requireField(`heirs.${index}.relationship`, heir.relationship, `Hubungan waris ${index + 1}`)
requireField(`heirs.${index}.phone_number`, heir.phone_number, `No. telefon waris ${index + 1}`)
requireField(`heirs.${index}.name`, heir.name, `Nama Penama ${index + 1}`)
requireField(`heirs.${index}.ic_number`, heir.ic_number, `No. KP Penama ${index + 1}`)
requireField(`heirs.${index}.relationship`, heir.relationship, `Hubungan Penama ${index + 1}`)
requireField(`heirs.${index}.phone_number`, heir.phone_number, `No. telefon Penama ${index + 1}`)
})
}
@@ -355,6 +421,11 @@ function validateStep(step: number): boolean {
setError('documents.ic_copy', 'Salinan kad pengenalan diperlukan.')
valid = false
}
if (!form.documents.employer_letter) {
setError('documents.employer_letter', 'Surat pengesahan majikan diperlukan.')
valid = false
}
}
return valid
@@ -638,8 +709,23 @@ function stepLabelClass(stepId: number) {
<template v-else-if="currentStep === 3">
<Field class="col-span-12 sm:col-span-6">
<FieldLabel for="employer_name">Nama Majikan</FieldLabel>
<Input id="employer_name" v-model="form.applicant.employer_name" type="text" />
<FieldLabel>Nama Majikan</FieldLabel>
<SelectRoot :key="`employer-${form.applicant.employer_name}`" class="w-full"
:collection="employerCollection" :default-value="employerInitial" @value-change="setEmployerValue">
<SelectControl>
<SelectTrigger :aria-invalid="!!fieldErrors['applicant.employer_name']">
<SelectValueText placeholder="Pilih majikan" />
</SelectTrigger>
</SelectControl>
<SelectContent>
<SelectItemGroup>
<SelectItemGroupLabel>Nama Majikan</SelectItemGroupLabel>
<SelectItem v-for="item in employerCollection.items" :key="item.label" :item="item">
<SelectItemText>{{ item.label }}</SelectItemText>
</SelectItem>
</SelectItemGroup>
</SelectContent>
</SelectRoot>
<FieldError v-if="fieldErrors['applicant.employer_name']">
{{ fieldErrors['applicant.employer_name'] }}
</FieldError>
@@ -653,7 +739,7 @@ function stepLabelClass(stepId: number) {
</Field>
<Field class="col-span-12">
<FieldLabel for="employer_address">Alamat Majikan</FieldLabel>
<Textarea id="employer_address" v-model="form.applicant.employer_address" rows="3" />
<Textarea id="employer_address" v-model="form.applicant.employer_address" rows="3" disabled />
<FieldError v-if="fieldErrors['applicant.employer_address']">
{{ fieldErrors['applicant.employer_address'] }}
</FieldError>
@@ -666,17 +752,27 @@ function stepLabelClass(stepId: number) {
</FieldError>
</Field>
<Field class="col-span-12 sm:col-span-4">
<FieldLabel for="stock_monthly_contribution">Caruman Saham (RM)</FieldLabel>
<FieldLabel for="stock_monthly_contribution">Potongan Modal Syer Minima</FieldLabel>
<FieldDescription>
Minima RM{{ MIN_STOCK_MONTHLY_CONTRIBUTION }} setiap bulan.
</FieldDescription>
<Input id="stock_monthly_contribution" v-model="form.applicant.stock_monthly_contribution" type="number"
min="0" step="0.01" />
:min="MIN_STOCK_MONTHLY_CONTRIBUTION" step="0.01" />
<FieldDescription class="mt-2">
Potongan RM{{ INITIAL_MANDATORY_STOCK_MONTHLY_CONTRIBUTION }} setiap bulan adalah wajib bagi 6 bulan
pertama bagi menjelaskan modal syer minimum RM500. Anda boleh memilih potongan lebih tinggi. Selepas
RM500 dijelaskan, potongan boleh dikekalkan atau dikurangkan sehingga minima
RM{{ MIN_STOCK_MONTHLY_CONTRIBUTION }} setiap bulan.
</FieldDescription>
<FieldError v-if="fieldErrors['applicant.stock_monthly_contribution']">
{{ fieldErrors['applicant.stock_monthly_contribution'] }}
</FieldError>
</Field>
<Field class="col-span-12 sm:col-span-4">
<FieldLabel for="fee_monthly_contribution">Caruman Yuran (RM)</FieldLabel>
<FieldLabel for="fee_monthly_contribution">Potongan Yuran Bulanan</FieldLabel>
<FieldDescription>Minima RM{{ MIN_FEE_MONTHLY_CONTRIBUTION }} setiap bulan.</FieldDescription>
<Input id="fee_monthly_contribution" v-model="form.applicant.fee_monthly_contribution" type="number"
min="0" step="0.01" />
:min="MIN_FEE_MONTHLY_CONTRIBUTION" step="0.01" />
<FieldError v-if="fieldErrors['applicant.fee_monthly_contribution']">
{{ fieldErrors['applicant.fee_monthly_contribution'] }}
</FieldError>
@@ -688,7 +784,7 @@ function stepLabelClass(stepId: number) {
<div v-for="(heir, index) in form.heirs" :key="index"
class="rounded-lg border border-foreground/10 p-4">
<div class="mb-4 flex items-center justify-between">
<div class="font-medium">Waris {{ index + 1 }}</div>
<div class="font-medium">Penama</div>
<Button v-if="form.heirs.length > 1" type="button" look="outline" size="sm"
@click="removeHeir(index)">
Buang
@@ -742,10 +838,11 @@ function stepLabelClass(stepId: number) {
</Field>
</div>
</div>
<Button type="button" look="outline" @click="addHeir">
<!-- Penama hanya boleh 1 orang (disable in frontend)-->
<!-- <Button type="button" look="outline" @click="addHeir">
<Lucide icon="Plus" class="mr-2 size-4" />
Tambah Waris
</Button>
Tambah Penama
</Button> -->
</div>
</template>
@@ -806,7 +903,7 @@ function stepLabelClass(stepId: number) {
</FieldError>
</Field>
<Field class="col-span-12 sm:col-span-6">
<FieldLabel for="employer_letter">Surat Pengesahan Majikan (Pilihan)</FieldLabel>
<FieldLabel for="employer_letter">Surat Pengesahan Majikan *</FieldLabel>
<Input id="employer_letter" type="file" accept=".pdf,.jpg,.jpeg,.png"
@change="handleFileChange('employer_letter', $event)" />
<FieldError v-if="fieldErrors['documents.employer_letter']">{{ fieldErrors['documents.employer_letter']
@@ -820,7 +917,7 @@ function stepLabelClass(stepId: number) {
<div><span class="opacity-70">Emel:</span> {{ form.applicant.email }}</div>
<div><span class="opacity-70">No. KP:</span> {{ form.applicant.ic_number }}</div>
<div><span class="opacity-70">Majikan:</span> {{ form.applicant.employer_name }}</div>
<div><span class="opacity-70">Bil. Waris:</span> {{ form.heirs.length }}</div>
<div><span class="opacity-70">Bil. Penama:</span> {{ form.heirs.length }}</div>
<div>
<span class="opacity-70">Pencadang:</span>
{{ form.references.proposer.name || '-' }}
@@ -2,7 +2,7 @@
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import dayjs from 'dayjs'
import { CircleAlert, CircleCheck, Download, Eye, FileText, Pencil } from '@lucide/vue'
import { CircleAlert, CircleCheck, Download, Eye, Pencil, Trash2 } from '@lucide/vue'
import {
AlertRoot,
AlertTitle,
@@ -21,9 +21,9 @@ import { usePermissions } from '@/composables/usePermissions'
import { getApiErrorMessage, getApiValidationErrors } from '@/core/utils/getApiErrorMessage'
import {
completeMembershipApplication,
deleteMembershipApplicationDocument,
downloadMembershipApplicationDocument,
fetchMembershipApplicationDocument,
generateMembershipApplicationResultLetter,
getMembershipApplication,
submitBoardReview,
submitManagementReview,
@@ -43,12 +43,15 @@ import type {
MembershipApplicationReviewDetail,
MembershipApplicationStatus,
} from '../types/membership-application.types'
import { RESULT_LETTER_DOCUMENT_TYPE } from '../types/membership-application.types'
import {
ADMIN_ATTACHMENT_DOCUMENT_TYPE,
RESULT_LETTER_DOCUMENT_TYPE,
} from '../types/membership-application.types'
const WORKFLOW_STEPS = [
{ id: 1, label: 'Dihantar' },
{ id: 2, label: 'Semakan Pentadbiran' },
{ id: 3, label: 'Semakan Lembaga' },
{ id: 3, label: 'Keputusan Ahli Lembaga Koperasi (ALK)' },
{ id: 4, label: 'Makluman Keputusan' },
{ id: 5, label: 'Selesai' },
] as const
@@ -58,6 +61,7 @@ const DOCUMENT_TYPE_LABELS: Record<string, string> = {
photo: 'Gambar Passport',
salary_slip: 'Slip Gaji',
employer_letter: 'Surat Pengesahan Majikan',
admin_attachment: 'Lampiran Pentadbir',
[RESULT_LETTER_DOCUMENT_TYPE]: 'Surat Keputusan',
}
@@ -82,19 +86,20 @@ const pendingAction = ref<
| null
>(null)
const downloadingDocumentId = ref<string | null>(null)
const deletingDocumentId = ref<string | null>(null)
const previewOpen = ref(false)
const previewLoading = ref(false)
const previewUrl = ref<string | null>(null)
const previewDocument = ref<MembershipApplicationDocumentDetail | null>(null)
const generateDialogOpen = ref(false)
const generateSubmitting = ref(false)
const boardMeetingReference = ref('')
const boardMeetingReferenceError = ref<string | null>(null)
const deleteConfirmDialogOpen = ref(false)
const pendingDeleteDocument = ref<MembershipApplicationDocumentDetail | null>(null)
function statusLabel(status: MembershipApplicationStatus): string {
const labels: Record<MembershipApplicationStatus, string> = {
SUBMITTED: 'Dihantar',
PENDING_BOARD: 'Menunggu Lembaga',
PENDING_BOARD: 'Menunggu Keputusan Mesyuarat ALK',
MANAGEMENT_REJECTED: 'Ditolak Pentadbiran',
PENDING_NOTIFICATION: 'Menunggu Makluman',
COMPLETED: 'Selesai',
@@ -142,16 +147,14 @@ const showCompleteAction = computed(
application.value?.status === 'PENDING_NOTIFICATION',
)
const resultLetterDocument = computed(() =>
application.value?.documents.find((document) => document.type === RESULT_LETTER_DOCUMENT_TYPE) ?? null,
const canEditAdminAttachments = computed(
() =>
hasPermission('kemaskini permohonan keahlian') &&
application.value?.status !== 'COMPLETED',
)
const showGenerateResultLetter = computed(
() =>
hasPermission('jana surat keputusan keahlian') &&
application.value?.status === 'COMPLETED' &&
!resultLetterDocument.value &&
(application.value.board_result === 'PASS' || application.value.board_result === 'FAIL'),
const resultLetterDocument = computed(() =>
application.value?.documents.find((document) => document.type === RESULT_LETTER_DOCUMENT_TYPE) ?? null,
)
const applicant = computed(() => application.value?.applicant ?? null)
@@ -167,8 +170,8 @@ const confirmDialogTitle = computed(() => {
if (pendingAction.value.type === 'board') {
return pendingAction.value.decision === 'PASS'
? 'Luluskan Semakan Lembaga?'
: 'Gagalkan Semakan Lembaga?'
? 'Luluskan?'
: 'Gagalkan?'
}
return 'Selesaikan Permohonan?'
@@ -179,17 +182,17 @@ const confirmDialogDescription = computed(() => {
if (pendingAction.value.type === 'management') {
return pendingAction.value.decision === 'APPROVED'
? 'Permohonan akan dihantar ke semakan lembaga.'
? 'Permohonan akan dihantar ke mesyuarat ALK.'
: 'Permohonan akan ditolak pada peringkat pentadbiran.'
}
if (pendingAction.value.type === 'board') {
return pendingAction.value.decision === 'PASS'
? 'Permohonan akan dihantar ke peringkat makluman keputusan.'
: 'Permohonan akan ditandakan gagal semakan lembaga.'
: 'Permohonan akan ditandakan gagal mesyuarat ALK.'
}
return 'E-mel keputusan akan dihantar kepada pemohon. Akaun ahli akan dicipta jika permohonan lulus.'
return 'Surat keputusan akan dijana, dilampirkan dalam e-mel keputusan, dan dihantar kepada pemohon. Akaun ahli akan dicipta jika permohonan lulus.'
})
function workflowStepButtonClass(stepId: number) {
@@ -242,6 +245,14 @@ const uploadedDocuments = computed(() =>
application.value?.documents.filter((document) => document.type !== RESULT_LETTER_DOCUMENT_TYPE) ?? [],
)
const applicantDocuments = computed(() =>
uploadedDocuments.value.filter((document) => document.type !== ADMIN_ATTACHMENT_DOCUMENT_TYPE),
)
const adminAttachments = computed(() =>
uploadedDocuments.value.filter((document) => document.type === ADMIN_ATTACHMENT_DOCUMENT_TYPE),
)
function displayValue(value: string | number | null | undefined): string {
if (value === null || value === undefined || value === '') return '-'
return String(value)
@@ -309,7 +320,7 @@ function getReference(type: 'PROPOSER' | 'SUPPORTER'): MembershipApplicationRefe
function reviewStageLabel(stage: string): string {
if (stage === 'MANAGEMENT') return 'Semakan Pentadbiran'
if (stage === 'BOARD') return 'Semakan Lembaga'
if (stage === 'BOARD') return 'Keputusan Ahli Lembaga Koperasi (ALK)'
return stage
}
@@ -358,6 +369,11 @@ function openConfirmAction(
| { type: 'board'; decision: BoardReviewDecision }
| { type: 'complete' },
) {
if (action.type === 'complete') {
boardMeetingReference.value = ''
boardMeetingReferenceError.value = null
}
pendingAction.value = action
confirmDialogOpen.value = true
}
@@ -365,6 +381,8 @@ function openConfirmAction(
function closeConfirmDialog() {
confirmDialogOpen.value = false
pendingAction.value = null
boardMeetingReference.value = ''
boardMeetingReferenceError.value = null
}
async function confirmPendingAction() {
@@ -372,8 +390,14 @@ async function confirmPendingAction() {
error.value = null
successMessage.value = null
boardMeetingReferenceError.value = null
if (pendingAction.value.type === 'complete') {
const reference = boardMeetingReference.value.trim()
if (!reference) {
boardMeetingReferenceError.value = 'Rujukan mesyuarat lembaga diperlukan.'
return
}
completeSubmitting.value = true
} else {
reviewSubmitting.value = true
@@ -395,7 +419,9 @@ async function confirmPendingAction() {
})
boardRemarks.value = ''
} else {
response = await completeMembershipApplication(applicationId.value)
response = await completeMembershipApplication(applicationId.value, {
board_meeting_reference: boardMeetingReference.value.trim(),
})
}
application.value = response.data
@@ -407,55 +433,15 @@ async function confirmPendingAction() {
if (validationErrors?.remarks?.[0]) {
error.value = validationErrors.remarks[0]
}
if (validationErrors?.board_meeting_reference?.[0]) {
boardMeetingReferenceError.value = validationErrors.board_meeting_reference[0]
}
} finally {
reviewSubmitting.value = false
completeSubmitting.value = false
}
}
async function openGenerateDialog() {
boardMeetingReference.value = ''
boardMeetingReferenceError.value = null
generateDialogOpen.value = true
}
function closeGenerateDialog() {
if (generateSubmitting.value) return
generateDialogOpen.value = false
boardMeetingReference.value = ''
boardMeetingReferenceError.value = null
}
async function confirmGenerateLetter() {
if (!application.value || generateSubmitting.value) return
const reference = boardMeetingReference.value.trim()
if (!reference) {
boardMeetingReferenceError.value = 'Rujukan mesyuarat lembaga diperlukan.'
return
}
generateSubmitting.value = true
boardMeetingReferenceError.value = null
error.value = null
try {
const response = await generateMembershipApplicationResultLetter(application.value.id, {
board_meeting_reference: reference,
})
application.value = response.data.application
successMessage.value = response.message
closeGenerateDialog()
} catch (err) {
const validationErrors = getApiValidationErrors(err)
boardMeetingReferenceError.value = validationErrors?.board_meeting_reference?.[0] ?? null
error.value = getApiErrorMessage(err, 'Gagal menjana surat keputusan.')
} finally {
generateSubmitting.value = false
}
}
async function handleDownloadDocument(document: MembershipApplicationDocumentDetail) {
if (!application.value || downloadingDocumentId.value) return
@@ -475,6 +461,43 @@ async function handleDownloadDocument(document: MembershipApplicationDocumentDet
}
}
async function handleDeleteAdminAttachment(document: MembershipApplicationDocumentDetail) {
if (!application.value || !canEditAdminAttachments.value || deletingDocumentId.value) return
pendingDeleteDocument.value = document
deleteConfirmDialogOpen.value = true
}
function closeDeleteConfirmDialog() {
deleteConfirmDialogOpen.value = false
pendingDeleteDocument.value = null
}
async function confirmDeleteAdminAttachment() {
const document = pendingDeleteDocument.value
if (!application.value || !document || deletingDocumentId.value) return
deletingDocumentId.value = document.id
error.value = null
successMessage.value = null
try {
const response = await deleteMembershipApplicationDocument(application.value.id, document.id)
application.value = response.data
if (previewDocument.value?.id === document.id) {
handlePreviewOpenChange(false)
}
successMessage.value = response.message || 'Lampiran pentadbir berjaya dipadam.'
closeDeleteConfirmDialog()
} catch (err) {
error.value = getApiErrorMessage(err, 'Gagal memadam lampiran pentadbir.')
} finally {
deletingDocumentId.value = null
}
}
async function handleViewDocument(document: MembershipApplicationDocumentDetail) {
if (!application.value) return
@@ -649,51 +672,31 @@ onUnmounted(() => {
<Box v-if="showCompleteAction" class="p-5 sm:p-6">
<div class="font-medium">Makluman Keputusan</div>
<p class="mt-1 text-sm opacity-70">
Hantar e-mel keputusan kepada pemohon
<span v-if="application.board_result === 'PASS'"> dan cipta akaun ahli</span>.
Jana surat keputusan, hantar e-mel dengan lampiran surat kepada pemohon
<span v-if="application.board_result === 'PASS'">, dan cipta akaun ahli</span>.
</p>
<div class="mt-4 flex flex-wrap gap-2">
<Button type="button" variant="primary" :disabled="reviewSubmitting || completeSubmitting"
@click="openConfirmAction({ type: 'complete' })">
Selesaikan & Hantar Makluman
Selesaikan, Jana Surat & Hantar E-mel
</Button>
</div>
</Box>
<Box v-if="showGenerateResultLetter" class="p-5 sm:p-6">
<div class="font-medium">Surat Keputusan</div>
<p class="mt-1 text-sm opacity-70">
Jana surat keputusan lembaga untuk permohonan ini. Surat hanya boleh dijana sekali.
</p>
<div class="mt-4 flex flex-wrap gap-2">
<Button type="button" variant="primary" @click="openGenerateDialog">
<FileText class="mr-2 size-4" />
Jana Surat Keputusan
</Button>
</div>
</Box>
<Box v-else-if="resultLetterDocument" class="p-5 sm:p-6">
<Box v-if="resultLetterDocument" class="p-5 sm:p-6">
<div class="font-medium">Surat Keputusan</div>
<p class="mt-1 text-sm opacity-70">
{{ resultLetterDocument.name }} · {{ formatFileSize(resultLetterDocument.file_size) }}
</p>
<div class="mt-4 flex flex-wrap gap-2">
<Button
type="button"
look="outline"
<Button type="button" look="outline"
:disabled="previewLoading && previewDocument?.id === resultLetterDocument.id"
@click="handleViewDocument(resultLetterDocument)"
>
@click="handleViewDocument(resultLetterDocument)">
<Eye class="mr-2 size-4" />
Lihat
</Button>
<Button
type="button"
look="outline"
:disabled="downloadingDocumentId === resultLetterDocument.id"
@click="handleDownloadDocument(resultLetterDocument)"
>
<Button type="button" look="outline" :disabled="downloadingDocumentId === resultLetterDocument.id"
@click="handleDownloadDocument(resultLetterDocument)">
<Download class="mr-2 size-4" />
{{ downloadingDocumentId === resultLetterDocument.id ? 'Memuat turun...' : 'Muat Turun' }}
</Button>
@@ -722,7 +725,7 @@ onUnmounted(() => {
<TabsTrigger
class="inline-flex min-w-0 flex-1 items-center justify-center whitespace-nowrap px-2 text-xs sm:px-3 sm:text-sm"
value="heirs">
Waris
Penama
</TabsTrigger>
<TabsTrigger
class="inline-flex min-w-0 flex-1 items-center justify-center whitespace-nowrap px-2 text-xs sm:px-3 sm:text-sm"
@@ -834,7 +837,7 @@ onUnmounted(() => {
<div v-else class="space-y-4">
<div v-for="(heir, index) in application.heirs" :key="heir.id"
class="rounded-lg border border-foreground/10 p-4">
<div class="mb-4 font-medium">Waris {{ index + 1 }}</div>
<div class="mb-4 font-medium">Penama</div>
<div class="grid grid-cols-12 gap-4">
<Field class="col-span-12 sm:col-span-6">
<FieldLabel>Nama</FieldLabel>
@@ -890,30 +893,62 @@ onUnmounted(() => {
<TabsContent value="documents" class="mt-6">
<div v-if="!uploadedDocuments.length" class="opacity-70">Tiada dokumen dimuat naik.</div>
<div v-else class="space-y-3">
<div v-for="document in uploadedDocuments" :key="document.id"
class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-foreground/10 p-4">
<div>
<div class="font-medium">{{ documentLabel(document.type, document.name) }}</div>
<div class="mt-1 text-sm opacity-70">
{{ document.name }} · {{ formatFileSize(document.file_size) }}
<template v-else>
<div v-if="applicantDocuments.length" class="space-y-3">
<div class="font-medium">Dokumen Pemohon</div>
<div v-for="document in applicantDocuments" :key="document.id"
class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-foreground/10 p-4">
<div>
<div class="font-medium">{{ documentLabel(document.type, document.name) }}</div>
<div class="mt-1 text-sm opacity-70">
{{ document.name }} · {{ formatFileSize(document.file_size) }}
</div>
</div>
<div class="flex flex-wrap items-center gap-2">
<Button type="button" look="outline" size="sm"
:disabled="previewLoading && previewDocument?.id === document.id"
@click="handleViewDocument(document)">
<Eye class="mr-2 size-4" />
{{ previewLoading && previewDocument?.id === document.id ? 'Memuatkan...' : 'Lihat' }}
</Button>
<Button type="button" look="outline" size="sm" :disabled="downloadingDocumentId === document.id"
@click="handleDownloadDocument(document)">
<Download class="mr-2 size-4" />
{{ downloadingDocumentId === document.id ? 'Memuat turun...' : 'Muat Turun' }}
</Button>
</div>
</div>
<div class="flex flex-wrap items-center gap-2">
<Button type="button" look="outline" size="sm"
:disabled="previewLoading && previewDocument?.id === document.id"
@click="handleViewDocument(document)">
<Eye class="mr-2 size-4" />
{{ previewLoading && previewDocument?.id === document.id ? 'Memuatkan...' : 'Lihat' }}
</Button>
<Button type="button" look="outline" size="sm" :disabled="downloadingDocumentId === document.id"
@click="handleDownloadDocument(document)">
<Download class="mr-2 size-4" />
{{ downloadingDocumentId === document.id ? 'Memuat turun...' : 'Muat Turun' }}
</Button>
</div>
<div v-if="adminAttachments.length" class="mt-8 space-y-3">
<div class="font-medium">Lampiran Pentadbir</div>
<div v-for="document in adminAttachments" :key="document.id"
class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-foreground/10 p-4">
<div>
<div class="font-medium">{{ document.name }}</div>
<div class="mt-1 text-sm opacity-70">{{ formatFileSize(document.file_size) }}</div>
</div>
<div class="flex flex-wrap items-center gap-2">
<Button type="button" look="outline" size="sm"
:disabled="previewLoading && previewDocument?.id === document.id"
@click="handleViewDocument(document)">
<Eye class="mr-2 size-4" />
{{ previewLoading && previewDocument?.id === document.id ? 'Memuatkan...' : 'Lihat' }}
</Button>
<Button type="button" look="outline" size="sm" :disabled="downloadingDocumentId === document.id"
@click="handleDownloadDocument(document)">
<Download class="mr-2 size-4" />
{{ downloadingDocumentId === document.id ? 'Memuat turun...' : 'Muat Turun' }}
</Button>
<Button v-if="canEditAdminAttachments" type="button" look="outline" size="sm" variant="danger"
:disabled="deletingDocumentId === document.id" @click="handleDeleteAdminAttachment(document)">
<Trash2 class="mr-2 size-4" />
{{ deletingDocumentId === document.id ? 'Memadam...' : 'Padam' }}
</Button>
</div>
</div>
</div>
</div>
</template>
</TabsContent>
<TabsContent v-if="application.reviews.length" value="reviews" class="mt-6">
@@ -932,11 +967,8 @@ onUnmounted(() => {
{{ formatDateTime(review.reviewed_at) }}
</div>
</div>
<Badge
:variant="reviewDecisionBadgeVariant(review.decision, review.stage)"
:look="reviewDecisionBadgeLook(review.decision, review.stage)"
class="whitespace-nowrap"
>
<Badge :variant="reviewDecisionBadgeVariant(review.decision, review.stage)"
:look="reviewDecisionBadgeLook(review.decision, review.stage)" class="whitespace-nowrap">
{{ reviewDecisionLabel(review.decision, review.stage) }}
</Badge>
</div>
@@ -959,11 +991,18 @@ onUnmounted(() => {
</template>
<DialogRoot :open="confirmDialogOpen"
@openChange="(details) => { confirmDialogOpen = details.open; if (!details.open) pendingAction = null }">
@openChange="(details) => { if (!details.open) closeConfirmDialog(); else confirmDialogOpen = details.open }">
<DialogContent>
<div class="p-5 text-center">
<div class="mt-2 text-2xl font-medium">{{ confirmDialogTitle }}</div>
<div class="mt-2 opacity-70">{{ confirmDialogDescription }}</div>
<Field v-if="pendingAction?.type === 'complete'" class="mt-5 text-left">
<FieldLabel for="detail-board-meeting-reference">Rujukan Mesyuarat Lembaga</FieldLabel>
<Input id="detail-board-meeting-reference" v-model="boardMeetingReference" type="text"
placeholder="Contoh: Mesyuarat Lembaga Bil. 3/2026" :disabled="completeSubmitting"
@input="boardMeetingReferenceError = null" />
<FieldError v-if="boardMeetingReferenceError">{{ boardMeetingReferenceError }}</FieldError>
</Field>
</div>
<div class="px-5 pb-8 text-center">
<DialogCloseTrigger class="mr-2 w-28" :disabled="reviewSubmitting || completeSubmitting">
@@ -977,38 +1016,22 @@ onUnmounted(() => {
</DialogContent>
</DialogRoot>
<DialogRoot :open="generateDialogOpen" @openChange="(details) => { if (!details.open) closeGenerateDialog() }">
<DialogRoot :open="deleteConfirmDialogOpen"
@openChange="(details) => { if (!details.open) closeDeleteConfirmDialog() }">
<DialogContent>
<div class="p-5">
<div class="text-2xl font-medium">Jana Surat Keputusan</div>
<p v-if="application" class="mt-2 text-sm opacity-70">
{{ application.application_number }} · {{ application.applicant?.name ?? '-' }}
</p>
<Field class="mt-5">
<FieldLabel for="detail-board-meeting-reference">Rujukan Mesyuarat Lembaga</FieldLabel>
<Input
id="detail-board-meeting-reference"
v-model="boardMeetingReference"
type="text"
placeholder="Contoh: Mesyuarat Lembaga Bil. 3/2026"
:disabled="generateSubmitting"
@input="boardMeetingReferenceError = null"
/>
<FieldError v-if="boardMeetingReferenceError">{{ boardMeetingReferenceError }}</FieldError>
</Field>
<div class="p-5 text-center">
<div class="mt-2 text-2xl font-medium">Padam Lampiran Pentadbir?</div>
<div class="mt-2 opacity-70">
{{ pendingDeleteDocument?.name ?? 'Lampiran pentadbir' }} akan dipadam secara kekal.
</div>
</div>
<div class="px-5 pb-8 text-center">
<DialogCloseTrigger class="mr-2 w-32" :disabled="generateSubmitting" @click="closeGenerateDialog">
<DialogCloseTrigger class="mr-2 w-28" :disabled="!!deletingDocumentId" @click="closeDeleteConfirmDialog">
Batal
</DialogCloseTrigger>
<Button
class="w-32"
type="button"
variant="primary"
:disabled="generateSubmitting"
@click="confirmGenerateLetter"
>
{{ generateSubmitting ? 'Menjana...' : 'Jana Surat' }}
<Button class="w-28" type="button" variant="danger" :disabled="!!deletingDocumentId"
@click="confirmDeleteAdminAttachment">
{{ deletingDocumentId ? 'Memadam...' : 'Padam' }}
</Button>
</div>
</DialogContent>
@@ -2,7 +2,7 @@
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import dayjs from 'dayjs'
import { CircleAlert, CircleCheck, Download, Eye, Plus } from '@lucide/vue'
import { CircleAlert, CircleCheck, Download, Eye, Plus, Trash2 } from '@lucide/vue'
import {
AlertRoot,
AlertTitle,
@@ -12,6 +12,7 @@ import {
import { Badge } from '@/components/ui/badge'
import { Box } from '@/components/ui/box'
import { Button } from '@/components/ui/button'
import { DialogRoot, DialogContent, DialogCloseTrigger } from '@/components/ui/dialog'
import { Field, FieldError, FieldLabel } from '@/components/ui/field'
import { Input } from '@/components/ui/input'
import {
@@ -31,6 +32,7 @@ import { usePermissions } from '@/composables/usePermissions'
import { getApiErrorMessage, getApiValidationErrors } from '@/core/utils/getApiErrorMessage'
import {
downloadMembershipApplicationDocument,
deleteMembershipApplicationDocument,
fetchMembershipApplicationDocument,
getMembershipApplication,
lookupMemberByIcNumber,
@@ -52,9 +54,10 @@ import type {
MembershipApplicationReviewDetail,
MembershipApplicationStatus,
} from '../types/membership-application.types'
import { ADMIN_ATTACHMENT_DOCUMENT_TYPE } from '../types/membership-application.types'
import {
APPLICANT_DOCUMENT_UPLOAD_TYPES,
DOCUMENT_TYPE_LABELS,
DOCUMENT_UPLOAD_TYPES,
GENDER_OPTIONS,
MARRIAGE_STATUS_OPTIONS,
RELATIONSHIP_OPTIONS,
@@ -73,7 +76,7 @@ const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024
const WORKFLOW_STEPS = [
{ id: 1, label: 'Dihantar' },
{ id: 2, label: 'Semakan Pentadbiran' },
{ id: 3, label: 'Semakan Lembaga' },
{ id: 3, label: 'Keputusan Ahli Lembaga Koperasi (ALK)' },
{ id: 4, label: 'Makluman Keputusan' },
{ id: 5, label: 'Selesai' },
] as const
@@ -91,11 +94,15 @@ const application = ref<MembershipApplicationDetail | null>(null)
const form = reactive<MembershipApplicationFormState>(createEmptyFormState())
const fieldErrors = reactive<Record<string, string>>({})
const downloadingDocumentId = ref<string | null>(null)
const deletingDocumentId = ref<string | null>(null)
const uploadingDocumentType = ref<DocumentUploadType | null>(null)
const uploadingAdminAttachment = ref(false)
const previewOpen = ref(false)
const previewLoading = ref(false)
const previewUrl = ref<string | null>(null)
const previewDocument = ref<MembershipApplicationDocumentDetail | null>(null)
const deleteConfirmDialogOpen = ref(false)
const pendingDeleteDocument = ref<MembershipApplicationDocumentDetail | null>(null)
const referenceLookupLoading = reactive({
proposer: false,
@@ -131,17 +138,22 @@ const sortedReviews = computed(() => {
})
})
const documentsByType = computed(() => {
function buildDocumentsByType(types: readonly DocumentUploadType[]) {
const map: Partial<Record<DocumentUploadType, MembershipApplicationDocumentDetail>> = {}
application.value?.documents.forEach((document) => {
if (DOCUMENT_UPLOAD_TYPES.includes(document.type as DocumentUploadType)) {
if (types.includes(document.type as DocumentUploadType)) {
map[document.type as DocumentUploadType] = document
}
})
return map
})
}
const applicantDocumentsByType = computed(() => buildDocumentsByType(APPLICANT_DOCUMENT_UPLOAD_TYPES))
const adminAttachments = computed(() =>
application.value?.documents.filter((document) => document.type === ADMIN_ATTACHMENT_DOCUMENT_TYPE) ?? [],
)
function getWorkflowProgress(status: MembershipApplicationStatus) {
switch (status) {
@@ -163,7 +175,7 @@ function getWorkflowProgress(status: MembershipApplicationStatus) {
function statusLabel(status: MembershipApplicationStatus): string {
const labels: Record<MembershipApplicationStatus, string> = {
SUBMITTED: 'Dihantar',
PENDING_BOARD: 'Menunggu Lembaga',
PENDING_BOARD: 'Menunggu Keputusan Mesyuarat ALK',
MANAGEMENT_REJECTED: 'Ditolak Pentadbiran',
PENDING_NOTIFICATION: 'Menunggu Makluman',
COMPLETED: 'Selesai',
@@ -247,7 +259,7 @@ const isPreviewPdf = computed(() => (previewDocument.value ? isPdfDocument(previ
function reviewStageLabel(stage: string): string {
if (stage === 'MANAGEMENT') return 'Semakan Pentadbiran'
if (stage === 'BOARD') return 'Semakan Lembaga'
if (stage === 'BOARD') return 'Keputusan Ahli Lembaga Koperasi (ALK)'
return stage
}
@@ -468,6 +480,91 @@ async function handleDocumentUpload(type: DocumentUploadType, event: Event) {
}
}
async function handleAdminAttachmentUpload(event: Event) {
if (!application.value || !canEdit.value || uploadingAdminAttachment.value) return
const input = event.target as HTMLInputElement
const files = input.files ? Array.from(input.files) : []
input.value = ''
if (!files.length) return
const oversizedFile = files.find((file) => file.size > MAX_FILE_SIZE_BYTES)
if (oversizedFile) {
fieldErrors[`documents.${ADMIN_ATTACHMENT_DOCUMENT_TYPE}`] =
`Saiz fail melebihi ${MAX_FILE_SIZE_MB}MB.`
return
}
delete fieldErrors[`documents.${ADMIN_ATTACHMENT_DOCUMENT_TYPE}`]
uploadingAdminAttachment.value = true
error.value = null
successMessage.value = null
try {
let latestResponse = null
for (const file of files) {
latestResponse = await uploadMembershipApplicationDocument(
applicationId.value,
ADMIN_ATTACHMENT_DOCUMENT_TYPE,
file,
)
application.value = latestResponse.data
}
successMessage.value =
files.length > 1
? `${files.length} lampiran pentadbir berjaya dimuat naik.`
: latestResponse?.message || 'Lampiran pentadbir berjaya dimuat naik.'
} catch (err) {
const validationErrors = getApiValidationErrors(err)
if (validationErrors) {
setFieldErrors(validationErrors)
}
error.value = getApiErrorMessage(err, 'Gagal memuat naik lampiran pentadbir.')
} finally {
uploadingAdminAttachment.value = false
}
}
async function handleDeleteAdminAttachment(document: MembershipApplicationDocumentDetail) {
if (!application.value || !canEdit.value || deletingDocumentId.value) return
pendingDeleteDocument.value = document
deleteConfirmDialogOpen.value = true
}
function closeDeleteConfirmDialog() {
deleteConfirmDialogOpen.value = false
pendingDeleteDocument.value = null
}
async function confirmDeleteAdminAttachment() {
const document = pendingDeleteDocument.value
if (!application.value || !document || deletingDocumentId.value) return
deletingDocumentId.value = document.id
error.value = null
successMessage.value = null
try {
const response = await deleteMembershipApplicationDocument(applicationId.value, document.id)
application.value = response.data
if (previewDocument.value?.id === document.id) {
handlePreviewOpenChange(false)
}
successMessage.value = response.message || 'Lampiran pentadbir berjaya dipadam.'
closeDeleteConfirmDialog()
} catch (err) {
error.value = getApiErrorMessage(err, 'Gagal memadam lampiran pentadbir.')
} finally {
deletingDocumentId.value = null
}
}
async function handleDownloadDocument(document: MembershipApplicationDocumentDetail) {
if (!application.value || downloadingDocumentId.value) return
@@ -635,7 +732,7 @@ onUnmounted(() => {
<TabsTrigger
class="inline-flex min-w-0 flex-1 items-center justify-center whitespace-nowrap px-2 text-xs sm:px-3 sm:text-sm"
value="heirs">
Waris
Penama
</TabsTrigger>
<TabsTrigger
class="inline-flex min-w-0 flex-1 items-center justify-center whitespace-nowrap px-2 text-xs sm:px-3 sm:text-sm"
@@ -770,13 +867,13 @@ onUnmounted(() => {
<FieldLabel for="current_position">Jawatan Semasa</FieldLabel>
<Input id="current_position" v-model="form.applicant.current_position" type="text" :disabled="!canEdit" />
<FieldError v-if="fieldErrors['applicant.current_position']">{{ fieldErrors['applicant.current_position']
}}</FieldError>
}}</FieldError>
</Field>
<Field class="col-span-12">
<FieldLabel for="employer_address">Alamat Majikan</FieldLabel>
<Textarea id="employer_address" v-model="form.applicant.employer_address" rows="3" :disabled="!canEdit" />
<FieldError v-if="fieldErrors['applicant.employer_address']">{{ fieldErrors['applicant.employer_address']
}}</FieldError>
}}</FieldError>
</Field>
<Field class="col-span-12 sm:col-span-4">
<FieldLabel for="start_work_date">Tarikh Mula Berkhidmat</FieldLabel>
@@ -805,7 +902,7 @@ onUnmounted(() => {
<div class="space-y-4">
<div v-for="(heir, index) in form.heirs" :key="index" class="rounded-lg border border-foreground/10 p-4">
<div class="mb-4 flex items-center justify-between">
<div class="font-medium">Waris {{ index + 1 }}</div>
<div class="font-medium">Penama</div>
<Button v-if="canEdit && form.heirs.length > 1" type="button" look="outline" size="sm"
@click="removeHeir(index)">
Buang
@@ -822,7 +919,7 @@ onUnmounted(() => {
<FieldLabel :for="`heir-ic-${index}`">No. Kad Pengenalan</FieldLabel>
<Input :id="`heir-ic-${index}`" v-model="heir.ic_number" type="text" :disabled="!canEdit" />
<FieldError v-if="fieldErrors[`heirs.${index}.ic_number`]">{{ fieldErrors[`heirs.${index}.ic_number`]
}}</FieldError>
}}</FieldError>
</Field>
<Field class="col-span-12 sm:col-span-6">
<FieldLabel>Hubungan</FieldLabel>
@@ -855,10 +952,10 @@ onUnmounted(() => {
</Field>
</div>
</div>
<Button v-if="canEdit" type="button" look="outline" @click="addHeir">
<!-- <Button v-if="canEdit" type="button" look="outline" @click="addHeir">
<Plus class="mr-2 size-4" />
Tambah Waris
</Button>
Tambah Penama
</Button> -->
</div>
</TabsContent>
@@ -900,25 +997,27 @@ onUnmounted(() => {
Muat naik fail baharu untuk menggantikan dokumen sedia ada.
</div>
<div class="space-y-4">
<div v-for="type in DOCUMENT_UPLOAD_TYPES" :key="type" class="rounded-lg border border-foreground/10 p-4">
<div v-for="type in APPLICANT_DOCUMENT_UPLOAD_TYPES" :key="type"
class="rounded-lg border border-foreground/10 p-4">
<div class="flex flex-wrap items-start justify-between gap-3">
<div>
<div class="font-medium">{{ DOCUMENT_TYPE_LABELS[type] }}</div>
<div v-if="documentsByType[type]" class="mt-1 text-sm opacity-70">
{{ documentsByType[type]?.name }} · {{ formatFileSize(documentsByType[type]?.file_size) }}
<div v-if="applicantDocumentsByType[type]" class="mt-1 text-sm opacity-70">
{{ applicantDocumentsByType[type]?.name }} ·
{{ formatFileSize(applicantDocumentsByType[type]?.file_size) }}
</div>
<div v-else class="mt-1 text-sm opacity-70">Tiada dokumen dimuat naik.</div>
</div>
<div v-if="documentsByType[type]" class="flex flex-wrap items-center gap-2">
<div v-if="applicantDocumentsByType[type]" class="flex flex-wrap items-center gap-2">
<Button type="button" look="outline" size="sm"
:disabled="previewLoading && previewDocument?.id === documentsByType[type]?.id"
@click="documentsByType[type] && handleViewDocument(documentsByType[type]!)">
:disabled="previewLoading && previewDocument?.id === applicantDocumentsByType[type]?.id"
@click="applicantDocumentsByType[type] && handleViewDocument(applicantDocumentsByType[type]!)">
<Eye class="mr-2 size-4" />
Lihat
</Button>
<Button type="button" look="outline" size="sm"
:disabled="downloadingDocumentId === documentsByType[type]?.id"
@click="documentsByType[type] && handleDownloadDocument(documentsByType[type]!)">
:disabled="downloadingDocumentId === applicantDocumentsByType[type]?.id"
@click="applicantDocumentsByType[type] && handleDownloadDocument(applicantDocumentsByType[type]!)">
<Download class="mr-2 size-4" />
Muat Turun
</Button>
@@ -926,7 +1025,7 @@ onUnmounted(() => {
</div>
<Field v-if="canEdit" class="mt-4">
<FieldLabel :for="`document-${type}`">
{{ documentsByType[type] ? 'Ganti Dokumen' : 'Muat Naik Dokumen' }}
{{ applicantDocumentsByType[type] ? 'Ganti Dokumen' : 'Muat Naik Dokumen' }}
</FieldLabel>
<Input :id="`document-${type}`" type="file" :accept="documentAccept(type)"
:disabled="uploadingDocumentType === type" @change="handleDocumentUpload(type, $event)" />
@@ -935,6 +1034,51 @@ onUnmounted(() => {
</Field>
</div>
</div>
<div class="mt-8">
<div class="mb-4 font-medium">Lampiran Pentadbir</div>
<p class="mb-4 text-sm opacity-70">
Muat naik satu atau lebih dokumen tambahan semasa semakan permohonan. Setiap muat naik akan
menambah lampiran baharu.
</p>
<div v-if="adminAttachments.length" class="mb-4 space-y-3">
<div v-for="document in adminAttachments" :key="document.id"
class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-foreground/10 p-4">
<div>
<div class="font-medium">{{ document.name }}</div>
<div class="mt-1 text-sm opacity-70">{{ formatFileSize(document.file_size) }}</div>
</div>
<div class="flex flex-wrap items-center gap-2">
<Button type="button" look="outline" size="sm"
:disabled="previewLoading && previewDocument?.id === document.id"
@click="handleViewDocument(document)">
<Eye class="mr-2 size-4" />
Lihat
</Button>
<Button type="button" look="outline" size="sm" :disabled="downloadingDocumentId === document.id"
@click="handleDownloadDocument(document)">
<Download class="mr-2 size-4" />
Muat Turun
</Button>
<Button v-if="canEdit" type="button" look="outline" size="sm" variant="danger"
:disabled="deletingDocumentId === document.id" @click="handleDeleteAdminAttachment(document)">
<Trash2 class="mr-2 size-4" />
{{ deletingDocumentId === document.id ? 'Memadam...' : 'Padam' }}
</Button>
</div>
</div>
</div>
<div v-else class="mb-4 text-sm opacity-70">Tiada lampiran pentadbir dimuat naik.</div>
<Field v-if="canEdit" class="rounded-lg border border-foreground/10 p-4">
<FieldLabel for="document-admin-attachment">Tambah Lampiran Pentadbir</FieldLabel>
<Input id="document-admin-attachment" type="file" accept=".pdf,.jpg,.jpeg,.png" multiple
:disabled="uploadingAdminAttachment" @change="handleAdminAttachmentUpload" />
<FieldError v-if="fieldErrors[`documents.${ADMIN_ATTACHMENT_DOCUMENT_TYPE}`]">
{{ fieldErrors[`documents.${ADMIN_ATTACHMENT_DOCUMENT_TYPE}`] }}
</FieldError>
<p v-if="uploadingAdminAttachment" class="mt-1 text-sm opacity-70">Memuat naik...</p>
</Field>
</div>
</TabsContent>
<TabsContent v-if="application.reviews.length" value="reviews" class="mt-6">
@@ -998,6 +1142,27 @@ onUnmounted(() => {
</div>
</template>
<DialogRoot :open="deleteConfirmDialogOpen"
@openChange="(details) => { if (!details.open) closeDeleteConfirmDialog() }">
<DialogContent>
<div class="p-5 text-center">
<div class="mt-2 text-2xl font-medium">Padam Lampiran Pentadbir?</div>
<div class="mt-2 opacity-70">
{{ pendingDeleteDocument?.name ?? 'Lampiran pentadbir' }} akan dipadam secara kekal.
</div>
</div>
<div class="px-5 pb-8 text-center">
<DialogCloseTrigger class="mr-2 w-28" :disabled="!!deletingDocumentId" @click="closeDeleteConfirmDialog">
Batal
</DialogCloseTrigger>
<Button class="w-28" type="button" variant="danger" :disabled="!!deletingDocumentId"
@click="confirmDeleteAdminAttachment">
{{ deletingDocumentId ? 'Memadam...' : 'Padam' }}
</Button>
</div>
</DialogContent>
</DialogRoot>
<Teleport to="body">
<div v-if="previewOpen" class="fixed inset-0 z-70 flex items-center justify-center p-4 sm:p-6" role="dialog"
aria-modal="true"
@@ -1,7 +1,7 @@
<script lang="ts" setup>
import { computed, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { CircleAlert, CircleCheck, Search, Eye, Pencil, FileText, Download } from '@lucide/vue'
import { CircleAlert, CircleCheck, Search, Eye, Pencil, Download } from '@lucide/vue'
import dayjs from 'dayjs'
import * as select from '@zag-js/select'
import {
@@ -36,7 +36,6 @@ import { usePermissions } from '@/composables/usePermissions'
import {
batchCompleteMembershipApplications,
downloadMembershipApplicationDocument,
generateMembershipApplicationResultLetter,
} from '../services/membership-application.service'
import {
boardResultBadgeVariant,
@@ -46,7 +45,6 @@ import {
import type {
BatchCompleteFailedItem,
BatchCompleteResponse,
GenerateResultLetterResponse,
MembershipApplicationBoardResult,
MembershipApplicationListItem,
MembershipApplicationStatus,
@@ -57,7 +55,7 @@ type SelectOption = { label: string; value: string }
const STATUS_FILTER_OPTIONS: SelectOption[] = [
{ label: 'Semua Status', value: '' },
{ label: 'Dihantar', value: 'SUBMITTED' },
{ label: 'Menunggu Lembaga', value: 'PENDING_BOARD' },
{ label: 'Menunggu Keputusan ALK', value: 'PENDING_BOARD' },
{ label: 'Ditolak Pentadbiran', value: 'MANAGEMENT_REJECTED' },
{ label: 'Menunggu Makluman', value: 'PENDING_NOTIFICATION' },
{ label: 'Selesai', value: 'COMPLETED' },
@@ -105,18 +103,13 @@ const {
} = useMembershipApplicationList()
const canBatchComplete = computed(() => hasPermission('selesaikan permohonan keahlian'))
const canGenerateResultLetter = computed(() => hasPermission('jana surat keputusan keahlian'))
const selectedIds = ref<string[]>([])
const batchSubmitting = ref(false)
const batchConfirmOpen = ref(false)
const batchSuccessMessage = ref<string | null>(null)
const letterSuccessMessage = ref<string | null>(null)
const batchFailedItems = ref<BatchCompleteFailedItem[]>([])
const generateDialogOpen = ref(false)
const generateSubmitting = ref(false)
const boardMeetingReference = ref('')
const boardMeetingReferenceError = ref<string | null>(null)
const generateTarget = ref<MembershipApplicationListItem | null>(null)
const downloadingResultLetterId = ref<string | null>(null)
const selectableApplications = computed(() =>
@@ -167,19 +160,28 @@ function toggleSelectAllOnPage(checked: boolean) {
function openBatchConfirm() {
if (!selectedIds.value.length) return
boardMeetingReference.value = ''
boardMeetingReferenceError.value = null
batchConfirmOpen.value = true
}
async function confirmBatchComplete() {
if (!selectedIds.value.length || batchSubmitting.value) return
const reference = boardMeetingReference.value.trim()
if (!reference) {
boardMeetingReferenceError.value = 'Rujukan mesyuarat lembaga diperlukan.'
return
}
batchSubmitting.value = true
batchSuccessMessage.value = null
batchFailedItems.value = []
boardMeetingReferenceError.value = null
error.value = null
try {
const response = await batchCompleteMembershipApplications(selectedIds.value)
const response = await batchCompleteMembershipApplications(selectedIds.value, reference)
if (response.success) {
batchSuccessMessage.value = response.message
@@ -196,6 +198,8 @@ async function confirmBatchComplete() {
const responseData = err.response.data as BatchCompleteResponse
batchFailedItems.value = responseData.data?.failed ?? []
error.value = responseData.message ?? getApiErrorMessage(err, 'Gagal menyelesaikan permohonan.')
const validationErrors = getApiValidationErrors(err)
boardMeetingReferenceError.value = validationErrors?.board_meeting_reference?.[0] ?? null
} else {
error.value = getApiErrorMessage(err, 'Gagal menyelesaikan permohonan.')
}
@@ -213,7 +217,7 @@ const statusFilterInitial = computed(() => apiValueToLabel(STATUS_FILTER_OPTIONS
function statusLabel(status: MembershipApplicationStatus): string {
const labels: Record<MembershipApplicationStatus, string> = {
SUBMITTED: 'Dihantar',
PENDING_BOARD: 'Menunggu Lembaga',
PENDING_BOARD: 'Menunggu Keputusan ALK',
MANAGEMENT_REJECTED: 'Ditolak Pentadbiran',
PENDING_NOTIFICATION: 'Menunggu Makluman',
COMPLETED: 'Selesai',
@@ -241,70 +245,10 @@ function goToApplicationEdit(id: string) {
router.push({ name: 'edit-membership-application', params: { id } })
}
function canGenerateLetter(item: MembershipApplicationListItem): boolean {
return (
canGenerateResultLetter.value &&
item.status === 'COMPLETED' &&
!item.has_result_letter &&
(item.board_result === 'PASS' || item.board_result === 'FAIL')
)
}
function canDownloadLetter(item: MembershipApplicationListItem): boolean {
return !!item.has_result_letter && !!item.result_letter_document
}
function openGenerateDialog(item: MembershipApplicationListItem) {
generateTarget.value = item
boardMeetingReference.value = ''
boardMeetingReferenceError.value = null
generateDialogOpen.value = true
}
function closeGenerateDialog() {
if (generateSubmitting.value) return
generateDialogOpen.value = false
generateTarget.value = null
boardMeetingReference.value = ''
boardMeetingReferenceError.value = null
}
async function confirmGenerateLetter() {
if (!generateTarget.value || generateSubmitting.value) return
const reference = boardMeetingReference.value.trim()
if (!reference) {
boardMeetingReferenceError.value = 'Rujukan mesyuarat lembaga diperlukan.'
return
}
generateSubmitting.value = true
boardMeetingReferenceError.value = null
error.value = null
letterSuccessMessage.value = null
try {
const response = await generateMembershipApplicationResultLetter(generateTarget.value.id, {
board_meeting_reference: reference,
})
letterSuccessMessage.value = response.message
closeGenerateDialog()
await fetchApplications(page.value)
} catch (err) {
if (axios.isAxiosError(err) && err.response?.data) {
const responseData = err.response.data as GenerateResultLetterResponse
const validationErrors = getApiValidationErrors(err)
boardMeetingReferenceError.value = validationErrors?.board_meeting_reference?.[0] ?? null
error.value = responseData.message ?? getApiErrorMessage(err, 'Gagal menjana surat keputusan.')
} else {
error.value = getApiErrorMessage(err, 'Gagal menjana surat keputusan.')
}
} finally {
generateSubmitting.value = false
}
}
async function handleDownloadResultLetter(item: MembershipApplicationListItem) {
const document = item.result_letter_document
if (!document) return
@@ -384,13 +328,6 @@ const headers = computed<TableHeader[]>(() => {
<p class="mt-1 text-sm opacity-70">Urus dan semak permohonan keahlian koperasi.</p>
</div>
<AlertRoot v-if="letterSuccessMessage" variant="success">
<CircleCheck />
<AlertTitle>Berjaya</AlertTitle>
<AlertDescription>{{ letterSuccessMessage }}</AlertDescription>
<AlertCloseTrigger @click="letterSuccessMessage = null" />
</AlertRoot>
<AlertRoot v-if="batchSuccessMessage" variant="success">
<CircleCheck />
<AlertTitle>Berjaya</AlertTitle>
@@ -482,11 +419,8 @@ const headers = computed<TableHeader[]>(() => {
</template>
<template #item.status="{ item }">
<Badge
:variant="statusBadgeVariant((item as MembershipApplicationListItem).status)"
:look="statusBadgeLook((item as MembershipApplicationListItem).status)"
class="whitespace-nowrap"
>
<Badge :variant="statusBadgeVariant((item as MembershipApplicationListItem).status)"
:look="statusBadgeLook((item as MembershipApplicationListItem).status)" class="whitespace-nowrap">
{{ statusLabel((item as MembershipApplicationListItem).status) }}
</Badge>
</template>
@@ -516,27 +450,10 @@ const headers = computed<TableHeader[]>(() => {
@click="goToApplicationEdit((item as MembershipApplicationListItem).id)">
<Pencil class="size-4" aria-hidden="true" />
</Button>
<Button
v-if="canGenerateLetter(item as MembershipApplicationListItem)"
type="button"
variant="ghost"
size="sm"
class="bg-amber-600 text-white"
title="Jana surat keputusan"
@click="openGenerateDialog(item as MembershipApplicationListItem)"
>
<FileText class="size-4" aria-hidden="true" />
</Button>
<Button
v-if="canDownloadLetter(item as MembershipApplicationListItem)"
type="button"
variant="ghost"
size="sm"
class="bg-purple-600 text-white"
title="Muat turun surat keputusan"
<Button v-if="canDownloadLetter(item as MembershipApplicationListItem)" type="button" variant="ghost"
size="sm" class="bg-purple-600 text-white" title="Muat turun surat keputusan"
:disabled="downloadingResultLetterId === (item as MembershipApplicationListItem).id"
@click="handleDownloadResultLetter(item as MembershipApplicationListItem)"
>
@click="handleDownloadResultLetter(item as MembershipApplicationListItem)">
<Download class="size-4" aria-hidden="true" />
</Button>
</div>
@@ -548,8 +465,16 @@ const headers = computed<TableHeader[]>(() => {
<div class="p-5 text-center">
<div class="mt-2 text-2xl font-medium">Selesaikan Permohonan Terpilih?</div>
<div class="mt-2 opacity-70">
{{ selectedIds.length }} permohonan akan diselesaikan dan e-mel keputusan dihantar.
{{ selectedIds.length }} permohonan akan diselesaikan. Surat keputusan dijana dan e-mel dengan lampiran
surat dihantar.
</div>
<Field class="mt-5 text-left">
<FieldLabel for="batch-board-meeting-reference">Rujukan Mesyuarat Lembaga</FieldLabel>
<Input id="batch-board-meeting-reference" v-model="boardMeetingReference" type="text"
placeholder="Contoh: Mesyuarat Lembaga Bil. 3/2026" :disabled="batchSubmitting"
@input="boardMeetingReferenceError = null" />
<FieldError v-if="boardMeetingReferenceError">{{ boardMeetingReferenceError }}</FieldError>
</Field>
</div>
<div class="px-5 pb-8 text-center">
<DialogCloseTrigger class="mr-2 w-32" :disabled="batchSubmitting">
@@ -562,45 +487,5 @@ const headers = computed<TableHeader[]>(() => {
</div>
</DialogContent>
</DialogRoot>
<DialogRoot :open="generateDialogOpen" @openChange="(details) => { if (!details.open) closeGenerateDialog() }">
<DialogContent>
<div class="p-5">
<div class="text-2xl font-medium">Jana Surat Keputusan</div>
<p v-if="generateTarget" class="mt-2 text-sm opacity-70">
{{ generateTarget.application_number }} · {{ generateTarget.applicant?.name ?? '-' }}
</p>
<Field class="mt-5">
<FieldLabel for="board-meeting-reference">Rujukan Mesyuarat Lembaga</FieldLabel>
<Input
id="board-meeting-reference"
v-model="boardMeetingReference"
type="text"
placeholder="Contoh: Mesyuarat Lembaga Bil. 3/2026"
:disabled="generateSubmitting"
@input="boardMeetingReferenceError = null"
/>
<FieldError v-if="boardMeetingReferenceError">{{ boardMeetingReferenceError }}</FieldError>
</Field>
<p class="mt-3 text-sm opacity-70">
Surat hanya boleh dijana sekali dan akan disimpan sebagai dokumen permohonan.
</p>
</div>
<div class="px-5 pb-8 text-center">
<DialogCloseTrigger class="mr-2 w-32" :disabled="generateSubmitting" @click="closeGenerateDialog">
Batal
</DialogCloseTrigger>
<Button
class="w-32"
type="button"
variant="primary"
:disabled="generateSubmitting"
@click="confirmGenerateLetter"
>
{{ generateSubmitting ? 'Menjana...' : 'Jana Surat' }}
</Button>
</div>
</DialogContent>
</DialogRoot>
</div>
</template>
@@ -157,6 +157,21 @@ export async function uploadMembershipApplicationDocument(
return data
}
export async function deleteMembershipApplicationDocument(
id: string,
documentId: string,
): Promise<MembershipApplicationUpdateResponse> {
const { data } = await api.delete<MembershipApplicationUpdateResponse>(
`/v1/membership-applications/${id}/documents/${documentId}`,
)
if (!data.success) {
throw new Error(data.message ?? 'Failed to delete document')
}
return data
}
// Submit management review
export async function submitManagementReview(
id: string,
@@ -194,9 +209,11 @@ export async function submitBoardReview(
// Complete membership application
export async function completeMembershipApplication(
id: string,
payload: GenerateResultLetterPayload,
): Promise<MembershipApplicationReviewResponse> {
const { data } = await api.post<MembershipApplicationReviewResponse>(
`/v1/membership-applications/${id}/complete`,
payload,
)
if (!data.success) {
@@ -208,10 +225,14 @@ export async function completeMembershipApplication(
export async function batchCompleteMembershipApplications(
applicationIds: string[],
boardMeetingReference: string,
): Promise<BatchCompleteResponse> {
const { data } = await api.post<BatchCompleteResponse>(
'/v1/membership-applications/batch-complete',
{ application_ids: applicationIds },
{
application_ids: applicationIds,
board_meeting_reference: boardMeetingReference,
},
)
return data
@@ -81,6 +81,7 @@ export type MembershipApplicationStatus =
export type MembershipApplicationBoardResult = 'PASS' | 'FAIL'
export const RESULT_LETTER_DOCUMENT_TYPE = 'result_letter' as const
export const ADMIN_ATTACHMENT_DOCUMENT_TYPE = 'admin_attachment' as const
export interface MembershipApplicationApplicantSummary {
name: string
@@ -208,7 +209,12 @@ export type MembershipApplicationUpdateResponse = MembershipApplicationApiRespon
message: string
}
export type DocumentUploadType = 'ic_copy' | 'photo' | 'salary_slip' | 'employer_letter'
export type DocumentUploadType =
| 'ic_copy'
| 'photo'
| 'salary_slip'
| 'employer_letter'
| 'admin_attachment'
export type ManagementReviewDecision = 'APPROVED' | 'REJECTED'
export type BoardReviewDecision = 'PASS' | 'FAIL'
@@ -40,10 +40,26 @@ export const DOCUMENT_TYPE_LABELS: Record<string, string> = {
photo: 'Gambar Passport',
salary_slip: 'Slip Gaji',
employer_letter: 'Surat Pengesahan Majikan',
admin_attachment: 'Lampiran Pentadbir',
}
export const DOCUMENT_UPLOAD_TYPES = ['ic_copy', 'photo', 'salary_slip', 'employer_letter'] as const
export type DocumentUploadType = (typeof DOCUMENT_UPLOAD_TYPES)[number]
export const APPLICANT_DOCUMENT_UPLOAD_TYPES = [
'ic_copy',
'photo',
'salary_slip',
'employer_letter',
] as const
export const ADMIN_DOCUMENT_UPLOAD_TYPES = ['admin_attachment'] as const
export const DOCUMENT_UPLOAD_TYPES = [
...APPLICANT_DOCUMENT_UPLOAD_TYPES,
...ADMIN_DOCUMENT_UPLOAD_TYPES,
] as const
export type ApplicantDocumentUploadType = (typeof APPLICANT_DOCUMENT_UPLOAD_TYPES)[number]
export type AdminDocumentUploadType = (typeof ADMIN_DOCUMENT_UPLOAD_TYPES)[number]
export type DocumentUploadType = ApplicantDocumentUploadType | AdminDocumentUploadType
export function createSelectCollection(options: SelectOption[]) {
return select.collection({
@@ -0,0 +1,186 @@
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import Swal from 'sweetalert2'
import { Button } from '@/components/ui/button'
import { Lucide } from '@/components/ui/lucide'
import { getApiErrorMessage } from '@/core/utils/getApiErrorMessage'
import { downloadMemberDigitalCard } from '../services/member-digital-card.service'
import { toProxiedStorageUrl } from '../utils/member-digital-card.utils'
import MemberDigitalCardFlip from './MemberDigitalCardFlip.vue'
const props = defineProps<{
memberNumber?: string | number | null
memberName?: string | null
memberType?: string | null
companyName?: string | null
profileUrl?: string | null
imageUrl?: string | null
large?: boolean
}>()
const previewMaxWidthClass = computed(() =>
props.large ? 'max-w-sm sm:max-w-md lg:max-w-lg' : 'max-w-68 sm:max-w-xs',
)
const resolvedImageUrl = computed(() => toProxiedStorageUrl(props.imageUrl))
const isFlipped = ref(false)
const expandedOpen = ref(false)
const isPortraitPhone = ref(false)
const downloading = ref(false)
let portraitQuery: MediaQueryList | null = null
function updatePortraitPhone() {
isPortraitPhone.value = portraitQuery?.matches ?? false
}
function openExpanded() {
expandedOpen.value = true
}
function closeExpanded() {
expandedOpen.value = false
}
function toggleFlip() {
isFlipped.value = !isFlipped.value
}
async function downloadCard() {
if (downloading.value) return
downloading.value = true
const side = isFlipped.value ? 'belakang' : 'depan'
try {
await downloadMemberDigitalCard(props.memberNumber, side)
await Swal.fire({
toast: true,
position: 'top-end',
icon: 'success',
title: `Kad ${side} berjaya disimpan.`,
showConfirmButton: false,
timer: 3000,
})
} catch (error) {
await Swal.fire({
icon: 'error',
title: 'Ralat',
text: getApiErrorMessage(error, 'Gagal menyimpan kad.'),
})
} finally {
downloading.value = false
}
}
watch(expandedOpen, (open) => {
document.body.style.overflow = open ? 'hidden' : ''
})
onMounted(() => {
portraitQuery = window.matchMedia('(max-width: 767px) and (orientation: portrait)')
updatePortraitPhone()
portraitQuery.addEventListener('change', updatePortraitPhone)
})
onUnmounted(() => {
document.body.style.overflow = ''
portraitQuery?.removeEventListener('change', updatePortraitPhone)
})
</script>
<template>
<div class="flex w-full flex-col items-center gap-2">
<button
type="button"
class="relative w-full cursor-pointer border-0 bg-transparent p-0 transition-transform active:scale-[0.98]"
:class="previewMaxWidthClass"
aria-label="Buka kad digital penuh"
@click="openExpanded">
<MemberDigitalCardFlip :member-number="memberNumber" :member-name="memberName"
:member-type="memberType" :company-name="companyName" :profile-url="profileUrl"
:image-url="resolvedImageUrl" :is-flipped="isFlipped" />
</button>
<p class="text-center text-[11px] text-slate-500">
Klik kad untuk paparan penuh
</p>
<div class="flex flex-wrap items-center justify-center gap-2">
<Button type="button" variant="ghost" class="border border-foreground/15 shadow-none text-xs"
:aria-pressed="isFlipped" :disabled="downloading" @click="toggleFlip">
<Lucide class="mr-2 size-4" icon="RotateCw" />
{{ isFlipped ? 'Papar depan kad' : 'Imbas kod QR' }}
</Button>
<Button type="button" variant="ghost" class="border border-foreground/15 shadow-none text-xs"
:disabled="downloading" @click="downloadCard">
<Lucide class="mr-2 size-4" :icon="downloading ? 'LoaderCircle' : 'Download'"
:class="{ 'animate-spin': downloading }" />
{{ downloading ? 'Menyimpan...' : 'Simpan kad' }}
</Button>
</div>
<Teleport to="body">
<div
v-if="expandedOpen"
class="fixed inset-0 z-70 flex flex-col items-center justify-center gap-4 bg-black/90 p-5"
role="dialog"
aria-modal="true"
aria-label="Kad digital anggota"
@click.self="closeExpanded">
<button
type="button"
class="absolute right-4 top-4 flex size-10 items-center justify-center rounded-full border border-white/20 bg-white/10 text-white"
aria-label="Tutup"
@click="closeExpanded">
<Lucide class="size-5" icon="X" />
</button>
<div v-if="isPortraitPhone" class="flex items-center justify-center" @click.stop>
<div class="w-[min(90vh,34rem)] rotate-90">
<MemberDigitalCardFlip :member-number="memberNumber" :member-name="memberName"
:member-type="memberType" :company-name="companyName" :profile-url="profileUrl"
:image-url="resolvedImageUrl" :is-flipped="isFlipped" expanded />
</div>
</div>
<div v-else class="w-[min(100vw-2rem,32rem)] lg:w-[min(100vw-2rem,40rem)]" @click.stop>
<MemberDigitalCardFlip :member-number="memberNumber" :member-name="memberName"
:member-type="memberType" :company-name="companyName" :profile-url="profileUrl"
:image-url="resolvedImageUrl" :is-flipped="isFlipped" expanded />
</div>
<div class="flex flex-wrap items-center justify-center gap-2">
<Button
type="button"
variant="ghost"
class="border border-white/20 bg-white/10 text-xs text-white shadow-none hover:bg-white/15"
:aria-pressed="isFlipped"
:disabled="downloading"
@click.stop="toggleFlip">
<Lucide class="mr-2 size-4" icon="RotateCw" />
{{ isFlipped ? 'Papar depan kad' : 'Imbas kod QR' }}
</Button>
<Button
type="button"
variant="ghost"
class="border border-white/20 bg-white/10 text-xs text-white shadow-none hover:bg-white/15"
:disabled="downloading"
@click.stop="downloadCard">
<Lucide class="mr-2 size-4" :icon="downloading ? 'LoaderCircle' : 'Download'"
:class="{ 'animate-spin': downloading }" />
{{ downloading ? 'Menyimpan...' : 'Simpan kad' }}
</Button>
</div>
<p class="text-center text-xs text-white/60">
Klik di luar kad untuk tutup
</p>
</div>
</Teleport>
</div>
</template>
@@ -0,0 +1,104 @@
<script lang="ts" setup>
import { computed, ref, watch } from 'vue'
import QRCode from 'qrcode'
import logoUrl from '@/assets/images/logo.svg'
import { displayCardValue } from '../utils/member-digital-card.utils'
const props = withDefaults(
defineProps<{
profileUrl?: string | null
memberNumber?: string | number | null
expanded?: boolean
}>(),
{ expanded: false },
)
const qrDataUrl = ref('')
const qrError = ref(false)
const qrPixelSize = computed(() => (props.expanded ? 220 : 120))
async function renderQrCode() {
if (!props.profileUrl) {
qrDataUrl.value = ''
qrError.value = false
return
}
try {
qrDataUrl.value = await QRCode.toDataURL(props.profileUrl, {
margin: 1,
width: qrPixelSize.value,
color: {
dark: '#0f172a',
light: '#ffffff',
},
})
qrError.value = false
} catch {
qrDataUrl.value = ''
qrError.value = true
}
}
watch([() => props.profileUrl, () => props.expanded], renderQrCode, { immediate: true })
</script>
<template>
<div :class="[
'relative h-full w-full overflow-hidden rounded-2xl bg-linear-to-br from-primary/90 via-primary to-primary/80 text-primary-foreground shadow-lg ring-1 ring-white/20',
expanded ? 'p-5 sm:p-6' : 'p-3 sm:p-4',
]">
<div class="pointer-events-none absolute inset-0 bg-noise opacity-30" />
<div class="pointer-events-none absolute -right-12 -top-12 rounded-full bg-white/10"
:class="expanded ? 'size-44' : 'size-32'" />
<div class="pointer-events-none absolute -bottom-16 -left-10 rounded-full bg-white/5"
:class="expanded ? 'size-48' : 'size-36'" />
<div class="relative flex h-full min-h-0 flex-col">
<div class="flex shrink-0 items-center justify-between gap-2">
<img :src="logoUrl" alt="" class="w-auto shrink-0 brightness-0 invert"
:class="expanded ? 'h-7 sm:h-9' : 'h-5 sm:h-6'" />
<div class="text-right font-semibold uppercase opacity-75"
:class="expanded ? 'text-sm tracking-[0.18em]' : 'text-[9px] tracking-[0.18em]'">
Belakang · Kod QR
</div>
</div>
<div class="flex min-h-0 flex-1 items-center" :class="expanded ? 'mt-5 gap-6' : 'mt-3 gap-3'">
<div class="shrink-0 rounded-lg bg-white shadow-sm" :class="expanded ? 'p-3' : 'p-1.5'">
<img v-if="qrDataUrl" :src="qrDataUrl" alt="Kod QR profil anggota" class="block"
:class="expanded ? 'size-32 sm:size-36' : 'size-18 sm:size-20'" />
<div v-else class="flex items-center justify-center"
:class="expanded ? 'size-32 sm:size-36' : 'size-18 sm:size-20'">
<span class="px-1 text-center leading-tight text-slate-500" :class="expanded ? 'text-sm' : 'text-[9px]'">
{{ qrError ? 'Kod QR tidak tersedia.' : 'Memuatkan...' }}
</span>
</div>
</div>
<div class="flex min-w-0 flex-1 flex-col justify-center" :class="expanded ? 'gap-5' : 'gap-3'">
<p class="leading-snug opacity-85" :class="expanded ? 'text-base sm:text-lg' : 'text-[9px] sm:text-[10px]'">
Imbas untuk sahkan profil anggota MyKOPKB.
</p>
<div>
<div class="font-medium uppercase tracking-widest opacity-60" :class="expanded ? 'text-sm' : 'text-[9px]'">
No. Anggota
</div>
<div class="mt-0.5 font-mono font-semibold tracking-widest"
:class="expanded ? 'text-3xl sm:text-4xl' : 'text-base sm:text-lg'">
{{ displayCardValue(memberNumber) }}
</div>
</div>
</div>
</div>
<div class="shrink-0 border-t border-white/15 text-center" :class="expanded ? 'mt-4 pt-3' : 'mt-2 pt-2'">
<p class="uppercase opacity-50" :class="expanded ? 'text-xs tracking-[0.2em]' : 'text-[8px] tracking-[0.2em]'">
Koperasi Permodalan Kelantan Berhad (KOPKB)
</p>
</div>
</div>
</div>
</template>
@@ -0,0 +1,34 @@
<script lang="ts" setup>
import MemberDigitalCardBack from './MemberDigitalCardBack.vue'
import MemberDigitalCardFront from './MemberDigitalCardFront.vue'
defineProps<{
memberNumber?: string | number | null
memberName?: string | null
memberType?: string | null
companyName?: string | null
profileUrl?: string | null
imageUrl?: string | null
isFlipped: boolean
expanded?: boolean
}>()
</script>
<template>
<div class="relative w-full" style="perspective: 1000px">
<div
class="relative aspect-7/4.5 w-full transition-transform duration-500 ease-in-out"
:style="{
transformStyle: 'preserve-3d',
transform: isFlipped ? 'rotateY(180deg)' : 'rotateY(0deg)',
}">
<div class="absolute inset-0" style="backface-visibility: hidden">
<MemberDigitalCardFront :member-number="memberNumber" :member-name="memberName"
:member-type="memberType" :company-name="companyName" :image-url="imageUrl" :expanded="expanded" />
</div>
<div class="absolute inset-0" :style="{ backfaceVisibility: 'hidden', transform: 'rotateY(180deg)' }">
<MemberDigitalCardBack :profile-url="profileUrl" :member-number="memberNumber" :expanded="expanded" />
</div>
</div>
</div>
</template>
@@ -0,0 +1,97 @@
<script lang="ts" setup>
import { computed } from 'vue'
import logoUrl from '@/assets/images/logo.svg'
import { displayCardValue } from '../utils/member-digital-card.utils'
const props = withDefaults(
defineProps<{
memberNumber?: string | number | null
memberName?: string | null
memberType?: string | null
companyName?: string | null
imageUrl?: string | null
expanded?: boolean
}>(),
{ expanded: false },
)
const avatarFallback = computed(() => {
const name = props.memberName?.trim()
if (!name) return '--'
return name.slice(0, 2).toUpperCase()
})
</script>
<template>
<div :class="[
'relative h-full w-full overflow-hidden rounded-2xl bg-linear-to-br from-primary via-primary/95 to-primary/75 text-primary-foreground shadow-lg ring-1 ring-white/20',
expanded ? 'p-6 sm:p-8' : 'p-4 sm:p-5',
]">
<div class="pointer-events-none absolute inset-0 bg-noise opacity-30" />
<div class="pointer-events-none absolute -right-10 -top-10 rounded-full bg-white/10"
:class="expanded ? 'size-48' : 'size-36'" />
<div class="pointer-events-none absolute -bottom-12 -left-8 rounded-full bg-white/5"
:class="expanded ? 'size-52' : 'size-40'" />
<div
class="pointer-events-none absolute top-1/2 -translate-y-1/2 overflow-hidden rounded-md border border-white/25 bg-white/10 shadow-sm"
:class="expanded ? 'right-6 size-20 sm:size-24' : 'right-4 size-14'">
<img v-if="imageUrl" :src="imageUrl" :alt="memberName ?? 'Profil anggota'" class="size-full object-cover" />
<div v-else class="flex size-full items-center justify-center bg-white/15 font-semibold uppercase tracking-wide"
:class="expanded ? 'text-base' : 'text-[11px]'">
{{ avatarFallback }}
</div>
</div>
<div class="relative flex h-full min-h-0 flex-col">
<div class="flex shrink-0 items-start justify-between gap-3">
<img :src="logoUrl" alt="" class="w-auto brightness-0 invert"
:class="expanded ? 'h-8 sm:h-10' : 'h-6 sm:h-7'" />
<div class="text-right font-semibold uppercase opacity-80"
:class="expanded ? 'text-sm tracking-[0.2em]' : 'text-[10px] tracking-[0.2em]'">
Kad Digital
</div>
</div>
<div class="flex min-h-0 flex-1 flex-col justify-center py-2" :class="expanded ? 'gap-4' : 'gap-2'">
<div>
<div class="font-medium uppercase tracking-widest opacity-70" :class="expanded ? 'text-sm' : 'text-[10px]'">
No. Anggota
</div>
<div class="mt-0.5 font-mono font-semibold"
:class="expanded ? 'text-4xl tracking-[0.15em] sm:text-5xl' : 'text-xl tracking-[0.15em] sm:text-2xl'">
{{ displayCardValue(memberNumber) }}
</div>
</div>
<div class="min-w-0" :class="expanded ? 'pr-28 sm:pr-32' : 'pr-16'">
<div class="font-medium uppercase tracking-widest opacity-70" :class="expanded ? 'text-sm' : 'text-[10px]'">
Unit
</div>
<div class="truncate font-medium" :class="expanded ? 'text-lg sm:text-xl' : 'text-xs'">
{{ displayCardValue(companyName) }}
</div>
</div>
</div>
<div class="flex shrink-0 items-end justify-between gap-3 border-t border-white/15"
:class="expanded ? 'pt-4' : 'pt-2'">
<div class="min-w-0 flex-1">
<div class="truncate font-medium" :class="expanded ? 'text-xl sm:text-2xl' : 'text-sm'">
{{ memberName || '-' }}
</div>
<div class="mt-0.5 uppercase tracking-wide opacity-60" :class="expanded ? 'text-sm' : 'text-[10px]'">
Nama
</div>
</div>
<div class="shrink-0 text-right">
<div class="font-semibold" :class="expanded ? 'text-xl sm:text-2xl' : 'text-sm'">
{{ displayCardValue(memberType) }}
</div>
<div class="mt-0.5 uppercase tracking-wide opacity-60" :class="expanded ? 'text-sm' : 'text-[10px]'">
Jenis Anggota
</div>
</div>
</div>
</div>
</div>
</template>
+1 -1
View File
@@ -1,2 +1,2 @@
export { profileLayoutRoutes } from './routes'
export { profileLayoutRoutes, profilePublicRoutes } from './routes'
export { profileMenu } from './menu'
+63 -6
View File
@@ -69,6 +69,33 @@ const EMPLOYMENT_TYPE_OPTIONS: SelectOption[] = [
{ label: 'Freelance', value: 'Freelance' },
]
// TODO: replace with API lookup
const EMPLOYERS = [
{
name: 'Infra Quest Sdn Bhd (IQSB)',
address: 'Lot 1045, Jalan Dato Lundang, 15200 Kota Bharu, Kelantan',
},
{
name: 'Permodalan Kelantan Berhad (PKB)',
address:
'Permodalan Kelantan Berhad, Tingkat 4, Wisma Permodalan Kelantan Berhad, Jalan Maju, 15000 Kota Bharu Kelantan',
},
{
name: 'Koperasi Permodalan Kelantan Berhad (KOPKB)',
address:
'Lot Pt 448, Tingkat 1,Jalan Kuala Krai, Batu 3, Wakaf Che Yeh, 15150 Kota Bharu, Kelantan.',
},
{
name: "An-Nisa'",
address: 'Jln Sultan Ibrahim, Bandar Kota Bharu, 15050 Kota Bharu, Kelantan.',
},
] as const
const COMPANY_OPTIONS: SelectOption[] = EMPLOYERS.map((employer) => ({
label: employer.name,
value: employer.name,
}))
function createSelectCollection(options: SelectOption[]) {
return select.collection({
items: options,
@@ -88,9 +115,12 @@ function apiValueToLabel(options: SelectOption[], value: string | null | undefin
}
const employmentTypeCollection = createSelectCollection(EMPLOYMENT_TYPE_OPTIONS)
const companyNameCollection = createSelectCollection(COMPANY_OPTIONS)
const employmentTypeValue = ref<string[]>([])
const employmentTypeInitial = ref<string[]>([])
const companyNameValue = ref<string[]>([])
const companyNameInitial = ref<string[]>([])
function clearEmploymentFieldError(field: EmploymentFieldKey) {
delete employmentErrors[field]
@@ -140,6 +170,10 @@ const employmentTypeLabel = computed(() =>
const isEditingEmployment = computed(() => editingEmploymentId.value !== null)
const canAddEmployment = computed(() => !loadingEmployments.value && employments.value.length === 0)
const showEmploymentForm = computed(() => isEditingEmployment.value || canAddEmployment.value)
function setEmploymentTypeValue(details: { value: string[] }) {
employmentTypeValue.value = details.value
clearEmploymentFieldError('employment_type')
@@ -147,9 +181,17 @@ function setEmploymentTypeValue(details: { value: string[] }) {
labelToApiValue(EMPLOYMENT_TYPE_OPTIONS, details.value[0]) ?? ''
}
function setCompanyNameValue(details: { value: string[] }) {
companyNameValue.value = details.value
clearEmploymentFieldError('company_name')
employmentForm.company_name = details.value[0] ?? ''
}
function syncEmploymentSelectValues() {
employmentTypeValue.value = apiValueToLabel(EMPLOYMENT_TYPE_OPTIONS, employmentForm.employment_type)
employmentTypeInitial.value = [...employmentTypeValue.value]
companyNameValue.value = apiValueToLabel(COMPANY_OPTIONS, employmentForm.company_name)
companyNameInitial.value = [...companyNameValue.value]
}
function resetEmploymentForm() {
@@ -194,7 +236,7 @@ function validateEmploymentForm(): boolean {
let valid = true
if (!employmentForm.company_name.trim()) {
if (!companyNameValue.value[0]?.trim()) {
employmentErrors.company_name = 'Nama syarikat diperlukan.'
valid = false
}
@@ -441,7 +483,8 @@ onMounted(async () => {
Tiada pekerjaan direkodkan.
</div>
<form class="space-y-6 border-t border-foreground/10 pt-6" @submit.prevent="onSaveEmployment">
<form v-if="showEmploymentForm" class="space-y-6 border-t border-foreground/10 pt-6"
@submit.prevent="onSaveEmployment">
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h4 class="text-base font-semibold text-slate-900">
@@ -470,10 +513,24 @@ onMounted(async () => {
<FieldGroup>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<Field>
<FieldLabel for="employment-company">Nama Syarikat</FieldLabel>
<Input id="employment-company" v-model="employmentForm.company_name" type="text"
placeholder="Nama syarikat" :aria-invalid="!!employmentErrors.company_name"
@input="clearEmploymentFieldError('company_name')" />
<FieldLabel>Nama Syarikat</FieldLabel>
<SelectRoot :key="`company-name-${editingEmploymentId ?? 'new'}`" class="w-full"
:collection="companyNameCollection" :default-value="companyNameInitial" :disabled="savingEmployment"
@value-change="setCompanyNameValue">
<SelectControl>
<SelectTrigger :aria-invalid="!!employmentErrors.company_name">
<SelectValueText placeholder="Pilih syarikat" />
</SelectTrigger>
</SelectControl>
<SelectContent>
<SelectItemGroup>
<SelectItemGroupLabel>Nama Syarikat</SelectItemGroupLabel>
<SelectItem v-for="item in companyNameCollection.items" :key="item.label" :item="item">
<SelectItemText>{{ item.label }}</SelectItemText>
</SelectItem>
</SelectItemGroup>
</SelectContent>
</SelectRoot>
<FieldError v-if="employmentErrors.company_name">{{ employmentErrors.company_name }}</FieldError>
</Field>
<Field>
+62 -105
View File
@@ -90,7 +90,10 @@ const relationshipCollection = createSelectCollection(RELATIONSHIP_OPTIONS)
const relationshipValue = ref<string[]>([])
const relationshipInitial = ref<string[]>([])
const MAX_HEIRS = 1
const isEditingHeir = computed(() => editingHeirId.value !== null)
const hasReachedHeirLimit = computed(() => heirs.value.length >= MAX_HEIRS)
const showHeirForm = computed(() => isEditingHeir.value || !hasReachedHeirLimit.value)
function emptyHeirForm() {
return {
@@ -204,7 +207,7 @@ async function fetchHeirs() {
await Swal.fire({
icon: 'error',
title: 'Ralat',
text: getApiErrorMessage(error, 'Gagal memuatkan pewaris.'),
text: getApiErrorMessage(error, 'Gagal memuatkan penama.'),
})
} finally {
loadingHeirs.value = false
@@ -224,6 +227,15 @@ function startEditHeir(heir: Heir) {
}
async function onSaveHeir() {
if (!isEditingHeir.value && hasReachedHeirLimit.value) {
await Swal.fire({
icon: 'info',
title: 'Had penama',
text: 'Hanya satu penama dibenarkan.',
})
return
}
if (!validateHeirForm()) {
return
}
@@ -238,7 +250,7 @@ async function onSaveHeir() {
: await createHeir(payload)
if (!res.success) {
throw new Error(res.message ?? 'Gagal menyimpan pewaris.')
throw new Error(res.message ?? 'Gagal menyimpan penama.')
}
await fetchHeirs()
@@ -248,7 +260,7 @@ async function onSaveHeir() {
toast: true,
position: 'top-end',
icon: 'success',
title: wasEditing ? 'Pewaris berjaya dikemas kini.' : 'Pewaris berjaya ditambah.',
title: wasEditing ? 'Penama berjaya dikemas kini.' : 'Penama berjaya ditambah.',
showConfirmButton: false,
timer: 3000,
})
@@ -257,7 +269,7 @@ async function onSaveHeir() {
await Swal.fire({
icon: 'error',
title: 'Ralat',
text: getApiErrorMessage(error, 'Gagal menyimpan pewaris.'),
text: getApiErrorMessage(error, 'Gagal menyimpan penama.'),
})
}
} finally {
@@ -268,7 +280,7 @@ async function onSaveHeir() {
async function onDeleteHeir(heir: Heir) {
const result = await Swal.fire({
icon: 'warning',
title: 'Padam pewaris?',
title: 'Padam penama?',
text: 'Tindakan ini tidak boleh dibatalkan.',
showCancelButton: true,
confirmButtonText: 'Padam',
@@ -283,7 +295,7 @@ async function onDeleteHeir(heir: Heir) {
const res = await deleteHeir(heir.id)
if (!res.success) {
throw new Error(res.message ?? 'Gagal memadam pewaris.')
throw new Error(res.message ?? 'Gagal memadam penama.')
}
if (editingHeirId.value === heir.id) {
@@ -296,7 +308,7 @@ async function onDeleteHeir(heir: Heir) {
toast: true,
position: 'top-end',
icon: 'success',
title: 'Pewaris berjaya dipadam.',
title: 'Penama berjaya dipadam.',
showConfirmButton: false,
timer: 3000,
})
@@ -304,7 +316,7 @@ async function onDeleteHeir(heir: Heir) {
await Swal.fire({
icon: 'error',
title: 'Ralat',
text: getApiErrorMessage(error, 'Gagal memadam pewaris.'),
text: getApiErrorMessage(error, 'Gagal memadam penama.'),
})
} finally {
deletingHeirId.value = null
@@ -323,23 +335,20 @@ onMounted(async () => {
<div class="space-y-6">
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h3 class="text-lg font-semibold text-slate-900">Pewaris</h3>
<h3 class="text-lg font-semibold text-slate-900">Penama</h3>
<p class="mt-1 text-sm text-slate-500">
Urus maklumat pewaris anda.
Urus maklumat penama anda. Hanya satu penama dibenarkan.
</p>
</div>
</div>
<div v-if="loadingHeirs" class="text-sm text-slate-500">
Memuatkan pewaris...
Memuatkan penama...
</div>
<div v-else-if="heirs.length" class="space-y-3">
<div
v-for="heir in heirs"
:key="heir.id"
class="flex flex-col gap-4 rounded-lg border border-foreground/10 p-4 sm:flex-row sm:items-start sm:justify-between"
>
<div v-for="heir in heirs" :key="heir.id"
class="flex flex-col gap-4 rounded-lg border border-foreground/10 p-4 sm:flex-row sm:items-start sm:justify-between">
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<span class="font-medium text-slate-900">{{ heir.name }}</span>
@@ -351,64 +360,47 @@ onMounted(async () => {
<p class="mt-1 text-sm text-slate-700">{{ heir.address }}</p>
</div>
<div class="flex shrink-0 gap-2">
<Button
type="button"
variant="ghost"
class="border border-foreground/15 shadow-none"
:disabled="deletingHeirId === heir.id"
@click="startEditHeir(heir)"
>
<Button type="button" variant="ghost" class="border border-foreground/15 shadow-none"
:disabled="deletingHeirId === heir.id" @click="startEditHeir(heir)">
<Lucide class="mr-2 size-4" icon="Pencil" />
Kemaskini
</Button>
<Button
type="button"
variant="ghost"
class="border border-foreground/15 shadow-none text-danger"
:disabled="deletingHeirId === heir.id"
@click="onDeleteHeir(heir)"
>
<Lucide
class="mr-2 size-4"
:icon="deletingHeirId === heir.id ? 'LoaderCircle' : 'Trash'"
:class="{ 'animate-spin': deletingHeirId === heir.id }"
/>
<Button type="button" variant="ghost" class="border border-foreground/15 shadow-none text-danger"
:disabled="deletingHeirId === heir.id" @click="onDeleteHeir(heir)">
<Lucide class="mr-2 size-4" :icon="deletingHeirId === heir.id ? 'LoaderCircle' : 'Trash'"
:class="{ 'animate-spin': deletingHeirId === heir.id }" />
Padam
</Button>
</div>
</div>
</div>
<div
v-else
class="rounded-lg border border-dashed border-foreground/15 p-6 text-center text-sm text-slate-500"
>
Tiada pewaris direkodkan.
<div v-else class="rounded-lg border border-dashed border-foreground/15 p-6 text-center text-sm text-slate-500">
Tiada penama direkodkan.
</div>
<form class="space-y-6 border-t border-foreground/10 pt-6" @submit.prevent="onSaveHeir">
<div v-if="hasReachedHeirLimit && !isEditingHeir"
class="rounded-lg border border-foreground/10 bg-foreground/5 p-4 text-sm text-slate-600">
Had penama telah dicapai. Kemaskini atau padam penama sedia ada untuk membuat perubahan.
</div>
<form v-if="showHeirForm" class="space-y-6 border-t border-foreground/10 pt-6" @submit.prevent="onSaveHeir">
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h4 class="text-base font-semibold text-slate-900">
{{ isEditingHeir ? 'Kemaskini Pewaris' : 'Tambah Pewaris' }}
{{ isEditingHeir ? 'Kemaskini Penama' : 'Tambah Penama' }}
</h4>
<p class="mt-1 text-sm text-slate-500">
{{
isEditingHeir
? 'Kemas kini maklumat pewaris yang dipilih.'
: 'Tambah pewaris baharu ke profil anda.'
? 'Kemas kini maklumat penama yang dipilih.'
: 'Tambah penama baharu ke profil anda.'
}}
</p>
</div>
<div class="flex gap-2">
<Button
v-if="isEditingHeir"
type="button"
variant="ghost"
class="border border-foreground/15 shadow-none"
:disabled="savingHeir"
@click="resetHeirForm"
>
<Button v-if="isEditingHeir" type="button" variant="ghost" class="border border-foreground/15 shadow-none"
:disabled="savingHeir" @click="resetHeirForm">
Batal
</Button>
<Button type="submit" variant="primary" :disabled="savingHeir">
@@ -421,38 +413,21 @@ onMounted(async () => {
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<Field>
<FieldLabel for="heir-name">Nama</FieldLabel>
<Input
id="heir-name"
v-model="heirForm.name"
type="text"
placeholder="Nama penuh"
:aria-invalid="!!heirErrors.name"
@input="clearHeirFieldError('name')"
/>
<Input id="heir-name" v-model="heirForm.name" type="text" placeholder="Nama penuh"
:aria-invalid="!!heirErrors.name" @input="clearHeirFieldError('name')" />
<FieldError v-if="heirErrors.name">{{ heirErrors.name }}</FieldError>
</Field>
<Field>
<FieldLabel for="heir-ic">No. Kad Pengenalan</FieldLabel>
<Input
id="heir-ic"
v-model="heirForm.ic_number"
type="text"
placeholder="No. kad pengenalan"
:aria-invalid="!!heirErrors.ic_number"
@input="clearHeirFieldError('ic_number')"
/>
<FieldLabel for="heir-ic-number">No. Kad Pengenalan</FieldLabel>
<Input id="heir-ic-number" v-model="heirForm.ic_number" type="text" placeholder="Contoh: 900101011234"
:aria-invalid="!!heirErrors.ic_number" @input="clearHeirFieldError('ic_number')" />
<FieldError v-if="heirErrors.ic_number">{{ heirErrors.ic_number }}</FieldError>
</Field>
<Field>
<FieldLabel>Hubungan</FieldLabel>
<SelectRoot
:key="`heir-relationship-${editingHeirId ?? 'new'}`"
class="w-full"
:collection="relationshipCollection"
:default-value="relationshipInitial"
:disabled="savingHeir"
@value-change="setRelationshipValue"
>
<SelectRoot :key="`heir-relationship-${editingHeirId ?? 'new'}`" class="w-full"
:collection="relationshipCollection" :default-value="relationshipInitial" :disabled="savingHeir"
@value-change="setRelationshipValue">
<SelectControl>
<SelectTrigger :aria-invalid="!!heirErrors.relationship">
<SelectValueText placeholder="Pilih hubungan" />
@@ -461,11 +436,7 @@ onMounted(async () => {
<SelectContent>
<SelectItemGroup>
<SelectItemGroupLabel>Hubungan</SelectItemGroupLabel>
<SelectItem
v-for="item in relationshipCollection.items"
:key="item.label"
:item="item"
>
<SelectItem v-for="item in relationshipCollection.items" :key="item.label" :item="item">
<SelectItemText>{{ item.label }}</SelectItemText>
</SelectItem>
</SelectItemGroup>
@@ -474,37 +445,23 @@ onMounted(async () => {
<FieldError v-if="heirErrors.relationship">{{ heirErrors.relationship }}</FieldError>
</Field>
<Field>
<FieldLabel for="heir-phone">No. Telefon</FieldLabel>
<Input
id="heir-phone"
v-model="heirForm.phone_number"
type="text"
placeholder="No. telefon"
:aria-invalid="!!heirErrors.phone_number"
@input="clearHeirFieldError('phone_number')"
/>
<FieldLabel for="heir-phone-number">No. Telefon</FieldLabel>
<Input id="heir-phone-number" v-model="heirForm.phone_number" type="text"
placeholder="Contoh: 0123456789" :aria-invalid="!!heirErrors.phone_number"
@input="clearHeirFieldError('phone_number')" />
<FieldError v-if="heirErrors.phone_number">{{ heirErrors.phone_number }}</FieldError>
</Field>
<Field class="md:col-span-2">
<FieldLabel for="heir-address">Alamat</FieldLabel>
<Textarea
id="heir-address"
v-model="heirForm.address"
placeholder="Alamat penuh"
class="resize-none"
:aria-invalid="!!heirErrors.address"
@input="clearHeirFieldError('address')"
/>
<Textarea id="heir-address" v-model="heirForm.address" rows="3" placeholder="Alamat penama"
:aria-invalid="!!heirErrors.address" @input="clearHeirFieldError('address')" />
<FieldError v-if="heirErrors.address">{{ heirErrors.address }}</FieldError>
</Field>
<Field class="md:col-span-2">
<CheckboxRoot
:checked="heirForm.is_primary"
:disabled="savingHeir"
@checked-change="({ checked }) => (heirForm.is_primary = checked === true)"
>
<CheckboxRoot :checked="heirForm.is_primary" :disabled="savingHeir"
@checked-change="({ checked }) => (heirForm.is_primary = checked === true)">
<CheckboxControl />
<CheckboxLabel>Pewaris utama</CheckboxLabel>
<CheckboxLabel>Penama utama</CheckboxLabel>
</CheckboxRoot>
</Field>
</div>
+136 -72
View File
@@ -1,28 +1,17 @@
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'
import Swal from 'sweetalert2'
import fakers from '@/utils/faker'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Box } from '@/components/ui/box'
import { TabsRoot, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
import { MenuRoot, MenuTrigger, MenuPositioner, MenuContent, MenuItem } from '@/components/ui/menu'
import { AvatarRoot, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { SwitchRoot, SwitchControl } from '@/components/ui/switch'
import { ProgressRoot, ProgressTrack, ProgressRange } from '@/components/ui/progress-linear'
import {
CarouselRoot,
CarouselPrevTrigger,
CarouselNextTrigger,
CarouselItemGroup,
CarouselItem,
} from '@/components/ui/carousel'
import { Lucide } from '@/components/ui/lucide'
import { FileIcon } from '@/components/ui/file-icon'
import { Badge } from '@/components/ui/badge'
import logoUrl from '@/assets/images/logo-kopkb.svg'
import { getApiErrorMessage } from '@/core/utils/getApiErrorMessage'
import { listEmployments } from '@/modules/profile/services/employment.service'
import { uploadProfileImage } from '@/modules/profile/services/profile.service'
import type { Employment } from '@/modules/profile/types/employment.types'
import { useAuthStore } from '@/stores/auth'
import MemberDigitalCard from '../components/MemberDigitalCard.vue'
import ProfileTab from './ProfileTab.vue'
import EmploymentTab from './EmploymentTab.vue'
import BankDetailTab from './BankDetailTab.vue'
@@ -34,12 +23,59 @@ const authStore = useAuthStore()
const uploadingImage = ref(false)
const imageInputRef = ref<HTMLInputElement | null>(null)
const imagePreviewUrl = ref<string | null>(null)
const employments = ref<Employment[]>([])
const companyName = computed(() => {
const currentEmployment = employments.value.find((employment) => employment.is_current)
return currentEmployment?.company_name ?? employments.value[0]?.company_name ?? null
})
const profileUrl = computed(() => {
const token = authStore.user?.public_profile_token
if (!token) return null
const baseUrl = (typeof window !== 'undefined'
? window.location.origin
: import.meta.env.VITE_APP_URL || ''
).replace(/\/$/, '')
return `${baseUrl}/v/${token}`
})
const displayValue = (value: string | number | null | undefined) => {
if (value === null || value === undefined || value === '') return '-'
return String(value).trim() || '-'
}
const STATUS_LABELS: Record<string, string> = {
active: 'Aktif',
pending: 'Menunggu',
inactive: 'Tidak Aktif',
}
const statusLabel = computed(() => {
const status = authStore.userStatus
if (!status) return '-'
return STATUS_LABELS[status] ?? status.charAt(0).toUpperCase() + status.slice(1)
})
const statusBadgeVariant = computed(() => {
if (authStore.isAccountActive) return 'success' as const
if (authStore.isAccountPending) return 'pending' as const
return 'secondary' as const
})
function formatDateLabel(value: string | null | undefined): string {
if (!value) return '-'
const date = new Date(value)
if (Number.isNaN(date.getTime())) return value
return new Intl.DateTimeFormat('ms-MY', {
day: 'numeric',
month: 'short',
year: 'numeric',
}).format(date)
}
const avatarSrc = computed(
() => imagePreviewUrl.value ?? authStore.userImageUrl ?? undefined,
)
@@ -98,10 +134,21 @@ async function onImageSelected(event: Event) {
}
}
async function fetchEmployments() {
try {
const res = await listEmployments()
employments.value = res.data
} catch {
employments.value = []
}
}
onMounted(async () => {
if (!authStore.user) {
await authStore.fetchSession()
}
await fetchEmployments()
})
</script>
@@ -113,9 +160,10 @@ onMounted(async () => {
<TabsRoot defaultValue="1">
<!-- BEGIN: Profile Info -->
<Box raised="single" class="mt-5 p-0">
<div class="flex flex-col border-b border-foreground/15 p-5 lg:flex-row">
<div class="flex flex-1 items-center justify-center px-5 lg:justify-start">
<div class="relative" :class="{ 'opacity-60': uploadingImage }">
<div class="flex flex-col border-b border-foreground/15 lg:flex-row">
<!-- Identity -->
<div class="flex flex-1 items-center justify-center p-5 lg:justify-start">
<div class="relative shrink-0" :class="{ 'opacity-60': uploadingImage }">
<AvatarRoot class="size-20 border-5 bg-background rounded-full sm:size-24 lg:size-32">
<AvatarFallback>{{ authStore.userName }}</AvatarFallback>
<AvatarImage v-if="avatarSrc" :src="avatarSrc" :alt="authStore.userName" />
@@ -129,87 +177,103 @@ onMounted(async () => {
<input ref="imageInputRef" type="file" accept="image/jpeg,image/png,image/jpg,image/gif" class="hidden"
@change="onImageSelected" />
</div>
<div class="ml-5">
<div class="w-24 truncate text-lg font-medium sm:w-40 sm:whitespace-normal">
<div class="ml-5 min-w-0">
<div class="truncate text-lg font-medium sm:whitespace-normal">
{{ authStore.userName || '-' }}
</div>
<div v-if="authStore.userMemberType"
class="mt-1 truncate text-sm capitalize opacity-70 sm:whitespace-normal">
{{ authStore.userMemberType }}
</div>
<div class="mt-3 flex flex-wrap items-center gap-2">
<Badge :variant="statusBadgeVariant">{{ statusLabel }}</Badge>
<Badge v-if="authStore.userMemberNumber" look="outline" variant="secondary">
No. {{ authStore.userMemberNumber }}
</Badge>
</div>
</div>
</div>
<div
class="mt-6 flex-1 border-t border-l border-r border-foreground/15 px-5 pt-5 lg:mt-0 lg:border-t-0 lg:pt-0">
<div class="text-center font-medium lg:mt-3 lg:text-left">Maklumat Hubungan</div>
<div class="mt-4 flex flex-col items-center justify-center lg:items-start">
<!-- Contact & membership -->
<div class="flex-1 border-t border-foreground/15 p-5 lg:border-t-0 lg:border-l">
<div class="text-center font-medium lg:text-left">Maklumat Hubungan</div>
<div class="mt-4 flex flex-col items-center lg:items-start">
<div class="flex items-center truncate sm:whitespace-normal">
<Lucide class="mr-2 size-4" icon="Mail" />
<Lucide class="mr-2 size-4 shrink-0" icon="Mail" />
{{ displayValue(authStore.user?.email) }}
</div>
<div class="mt-3 flex items-center truncate sm:whitespace-normal">
<Lucide class="mr-2 size-4" icon="Phone" />
<Lucide class="mr-2 size-4 shrink-0" icon="Phone" />
{{ displayValue(authStore.user?.phone_number) }}
</div>
<div class="mt-3 flex items-center truncate sm:whitespace-normal">
<Lucide class="mr-2 size-4" icon="IdCard" />
<Lucide class="mr-2 size-4 shrink-0" icon="IdCard" />
{{ displayValue(authStore.user?.ic_number) }}
</div>
</div>
</div>
<div
class="mt-6 flex flex-1 items-center justify-center border-t border-foreground/15 px-5 pt-5 lg:mt-0 lg:border-0 lg:pt-0">
<div
class="relative aspect-1.75/1 w-full max-w-68 overflow-hidden rounded-2xl bg-linear-to-br from-primary via-primary/95 to-primary/75 p-4 text-primary-foreground shadow-lg ring-1 ring-white/20 sm:max-w-xs sm:p-5"
role="img" aria-label="Kad digital anggota">
<div class="pointer-events-none absolute inset-0 bg-noise opacity-30" />
<div class="pointer-events-none absolute -right-10 -top-10 size-36 rounded-full bg-white/10" />
<div class="pointer-events-none absolute -bottom-12 -left-8 size-40 rounded-full bg-white/5" />
<div
class="pointer-events-none absolute right-4 top-1/2 size-10 -translate-y-1/2 rounded-md border border-white/20 bg-white/10" />
<div class="relative flex h-full flex-col justify-between">
<div class="flex items-start justify-between gap-3">
<img :src="logoUrl" alt="" class="h-7 w-auto brightness-0 invert sm:h-8" />
<div class="text-right text-[10px] font-semibold uppercase tracking-[0.2em] opacity-80">
Kad Digital
</div>
<div class="mt-6 grid grid-cols-2 gap-4 sm:grid-cols-3">
<div class="text-center lg:text-left">
<div class="truncate text-base font-medium">
{{ displayValue(authStore.userPosition) }}
</div>
<div>
<div class="text-[10px] font-medium uppercase tracking-widest opacity-70">No. Anggota</div>
<div class="mt-1 font-mono text-2xl font-semibold tracking-[0.15em] sm:text-3xl">
{{ displayValue(authStore.userMemberNumber) }}
</div>
<div class="text-xs opacity-70">Jawatan</div>
</div>
<div class="text-center lg:text-left">
<div class="truncate text-base font-medium">
{{ displayValue(companyName) }}
</div>
<div class="flex items-end justify-between gap-3 border-t border-white/15 pt-3">
<div class="min-w-0 flex-1">
<div class="truncate text-sm font-medium">{{ authStore.userName || '-' }}</div>
<div class="mt-0.5 text-[10px] uppercase tracking-wide opacity-60">Nama</div>
</div>
<div class="shrink-0 text-right">
<div class="text-sm font-semibold">{{ displayValue(authStore.userMemberType) }}</div>
<div class="mt-0.5 text-[10px] uppercase tracking-wide opacity-60">Jenis Anggota</div>
</div>
<div class="text-xs opacity-70">Unit</div>
</div>
<div class="col-span-2 text-center sm:col-span-1 lg:text-left">
<div class="truncate text-base font-medium">
{{ formatDateLabel(authStore.userJoinDate) }}
</div>
<div class="text-xs opacity-70">Tarikh Sertai</div>
</div>
</div>
</div>
<!-- Digital card -->
<div
class="flex shrink-0 items-center justify-center border-t border-foreground/15 p-5 lg:border-t-0 lg:border-l">
<MemberDigitalCard large :member-number="authStore.userMemberNumber" :member-name="authStore.userName"
:member-type="authStore.userMemberType" :company-name="companyName" :profile-url="profileUrl"
:image-url="avatarSrc" />
</div>
</div>
<!-- Tabs title -->
<div class="px-5 py-4">
<TabsList class="w-full mb-0 flex justify-between">
<TabsTrigger class="w-1/4 inline-flex items-center justify-center" value="1">
<Lucide class="mr-2 size-4" icon="User" /> Profil
<TabsList class="mb-0 flex w-full">
<TabsTrigger
class="inline-flex min-w-0 flex-1 items-center justify-center whitespace-nowrap px-2 sm:px-3 md:text-sm"
value="1" aria-label="Profil">
<Lucide class="size-4 shrink-0 md:mr-2" icon="User" />
<span class="hidden md:inline">Profil</span>
</TabsTrigger>
<TabsTrigger class="w-1/4 inline-flex items-center justify-center" value="5">
<Lucide class="mr-2 size-4" icon="Briefcase" /> Pekerjaan
<TabsTrigger
class="inline-flex min-w-0 flex-1 items-center justify-center whitespace-nowrap px-2 sm:px-3 md:text-sm"
value="5" aria-label="Pekerjaan">
<Lucide class="size-4 shrink-0 md:mr-2" icon="Briefcase" />
<span class="hidden md:inline">Pekerjaan</span>
</TabsTrigger>
<TabsTrigger class="w-1/4 inline-flex items-center justify-center" value="3">
<Lucide class="mr-2 size-4" icon="Banknote" /> Bank
<TabsTrigger
class="inline-flex min-w-0 flex-1 items-center justify-center whitespace-nowrap px-2 sm:px-3 md:text-sm"
value="3" aria-label="Bank">
<Lucide class="size-4 shrink-0 md:mr-2" icon="Banknote" />
<span class="hidden md:inline">Bank</span>
</TabsTrigger>
<TabsTrigger class="w-1/4 inline-flex items-center justify-center" value="6">
<Lucide class="mr-2 size-4" icon="Users" /> Pewaris
<TabsTrigger
class="inline-flex min-w-0 flex-1 items-center justify-center whitespace-nowrap px-2 sm:px-3 md:text-sm"
value="6" aria-label="Penama">
<Lucide class="size-4 shrink-0 md:mr-2" icon="Users" />
<span class="hidden md:inline">Penama</span>
</TabsTrigger>
<TabsTrigger class="w-1/4 inline-flex items-center justify-center" value="2">
<Lucide class="mr-2 size-4" icon="Lock" /> Kata Laluan
<TabsTrigger
class="inline-flex min-w-0 flex-1 items-center justify-center whitespace-nowrap px-2 sm:px-3 md:text-sm"
value="2" aria-label="Kata Laluan">
<Lucide class="size-4 shrink-0 md:mr-2" icon="Lock" />
<span class="hidden md:inline">Kata Laluan</span>
</TabsTrigger>
</TabsList>
</div>
@@ -230,7 +294,7 @@ onMounted(async () => {
<TabsContent value="2" class="mt-8">
<ChangePasswordTab embedded />
</TabsContent>
<!-- Pewaris -->
<!-- Penama -->
<TabsContent value="6" class="mt-8">
<HeirTab embedded />
</TabsContent>
@@ -0,0 +1,140 @@
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { AlertRoot, AlertTitle, AlertDescription } from '@/components/ui/alert'
import { AvatarRoot, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Badge } from '@/components/ui/badge'
import { Box } from '@/components/ui/box'
import { Lucide } from '@/components/ui/lucide'
import logoUrl from '@/assets/images/logo.svg'
import { getApiErrorMessage } from '@/core/utils/getApiErrorMessage'
import { getPublicMemberProfile } from '../services/public-member-profile.service'
import type { PublicMemberProfile } from '../types/public-member-profile.types'
const route = useRoute()
const loading = ref(true)
const error = ref<string | null>(null)
const member = ref<PublicMemberProfile | null>(null)
const token = computed(() => String(route.params.token ?? '').trim())
const avatarFallback = computed(() => {
const name = member.value?.name?.trim()
if (!name) return '--'
return name.slice(0, 2).toUpperCase()
})
const displayValue = (value: string | number | null | undefined) => {
if (value === null || value === undefined || value === '') return '-'
return String(value).trim() || '-'
}
async function fetchMemberProfile() {
loading.value = true
error.value = null
member.value = null
if (!token.value) {
error.value = 'Pautan pengesahan tidak sah.'
loading.value = false
return
}
try {
const response = await getPublicMemberProfile(token.value)
if (!response.success || !response.data) {
throw new Error(response.message ?? 'Anggota tidak dijumpai atau tidak sah.')
}
member.value = response.data
} catch (err) {
error.value = getApiErrorMessage(err, 'Gagal memuatkan profil anggota.')
} finally {
loading.value = false
}
}
onMounted(() => {
fetchMemberProfile()
})
</script>
<template>
<div class="min-h-screen bg-slate-100 px-4 py-10">
<div class="mx-auto w-full max-w-md">
<div class="mb-6 flex flex-col items-center text-center">
<img :src="logoUrl" alt="MyKOPKB" class="h-10 w-auto" />
<h1 class="mt-4 text-xl font-semibold text-slate-900">Maklumat Anggota</h1>
</div>
<AlertRoot v-if="error" variant="danger">
<AlertTitle>Pengesahan gagal</AlertTitle>
<AlertDescription>{{ error }}</AlertDescription>
</AlertRoot>
<Box v-else-if="loading" raised="single" class="p-8 text-center text-sm text-slate-500">
Memuatkan maklumat anggota...
</Box>
<Box v-else-if="member" raised="single" class="overflow-hidden p-0">
<div class="bg-linear-to-br from-primary via-primary/95 to-primary/75 p-6 text-primary-foreground">
<div class="flex items-start justify-between gap-3">
<Badge class="bg-white/15 text-white">Disahkan</Badge>
<div class="text-right text-[10px] font-semibold uppercase tracking-[0.2em] opacity-80">
MyKOPKB
</div>
</div>
<div class="mt-6 flex items-center gap-4">
<AvatarRoot class="size-16 border-4 border-white/20 bg-white/10">
<AvatarFallback>{{ avatarFallback }}</AvatarFallback>
<AvatarImage v-if="member.image_url" :src="member.image_url" :alt="member.name" />
</AvatarRoot>
<div class="min-w-0 flex-1">
<div class="truncate text-lg font-semibold">{{ member.name }}</div>
<div class="mt-1 text-sm opacity-80">{{ displayValue(member.member_type) }}</div>
</div>
</div>
</div>
<div class="space-y-4 p-6">
<div class="flex items-center gap-3 rounded-lg border border-foreground/10 p-4">
<Lucide class="size-5 text-primary" icon="IdCard" />
<div>
<div class="text-xs uppercase tracking-wide text-slate-500">No. Anggota</div>
<div class="font-mono text-base font-semibold text-slate-900">
{{ displayValue(member.member_number) }}
</div>
</div>
</div>
<div class="flex items-center gap-3 rounded-lg border border-foreground/10 p-4">
<Lucide class="size-5 text-primary" icon="Briefcase" />
<div class="min-w-0">
<div class="text-xs uppercase tracking-wide text-slate-500">Unit</div>
<div class="truncate text-base font-medium text-slate-900">
{{ displayValue(member.company_name) }}
</div>
</div>
</div>
<div class="flex items-center gap-3 rounded-lg border border-foreground/10 p-4">
<Lucide class="size-5 text-primary" icon="CircleCheck" />
<div>
<div class="text-xs uppercase tracking-wide text-slate-500">Status</div>
<div class="text-base font-medium capitalize text-slate-900">
{{ displayValue(member.status) }}
</div>
</div>
</div>
<p class="text-center text-xs text-slate-500">
Disahkan pada {{ new Date(member.verified_at).toLocaleString('ms-MY') }}
</p>
</div>
</Box>
</div>
</div>
</template>
+9
View File
@@ -1,5 +1,14 @@
import type { RouteRecordRaw } from 'vue-router'
export const profilePublicRoutes: RouteRecordRaw[] = [
{
path: '/v/:token',
name: 'public-member-profile',
component: () => import('./pages/PublicMemberProfile.vue'),
meta: { public: true, module: 'profile' },
},
]
export const profileLayoutRoutes: RouteRecordRaw[] = [
{
path: 'profile-overview-2',
@@ -0,0 +1,35 @@
import { saveAs } from 'file-saver'
import axios from 'axios'
import { api } from '@/core/services/api'
import { buildCardDownloadFileName } from '../utils/member-digital-card.utils'
export async function downloadMemberDigitalCard(
memberNumber: string | number | null | undefined,
side: 'depan' | 'belakang',
) {
try {
const { data } = await api.get<Blob>('/v1/profile/digital-card', {
params: { side },
responseType: 'blob',
})
saveAs(data, buildCardDownloadFileName(memberNumber, side))
} catch (error) {
if (axios.isAxiosError(error) && error.response?.data instanceof Blob) {
const text = await error.response.data.text()
try {
const payload = JSON.parse(text) as { message?: string }
throw new Error(payload.message ?? 'Gagal menyimpan kad.')
} catch (parseError) {
if (parseError instanceof SyntaxError) {
throw error
}
throw parseError
}
}
throw error
}
}
@@ -63,3 +63,8 @@ export async function updatePassword(payload: UpdatePasswordPayload): Promise<Up
const { data } = await api.put<UpdatePasswordResponse>('/v1/profile/password', payload)
return data
}
export async function completeOnboarding(): Promise<UpdateProfileResponse> {
const { data } = await api.post<UpdateProfileResponse>('/v1/profile/onboarding/complete')
return data
}
@@ -0,0 +1,12 @@
import { api } from '@/core/services/api'
import type { PublicMemberProfileApiResponse } from '../types/public-member-profile.types'
export async function getPublicMemberProfile(
token: string,
): Promise<PublicMemberProfileApiResponse> {
const { data } = await api.get<PublicMemberProfileApiResponse>(
`/v1/public/members/${encodeURIComponent(token)}`,
)
return data
}
@@ -0,0 +1,16 @@
export interface PublicMemberProfile {
name: string
member_number: number | null
member_type: string | null
status: string
image_url: string | null
company_name: string | null
verified_at: string
}
export interface PublicMemberProfileApiResponse {
success: boolean
message?: string
code?: 'public_profile_not_found' | 'public_profile_token_expired'
data: PublicMemberProfile | null
}
@@ -0,0 +1,27 @@
export function displayCardValue(value: string | number | null | undefined) {
if (value === null || value === undefined || value === '') return '-'
return String(value).trim() || '-'
}
export function buildCardDownloadFileName(
memberNumber: string | number | null | undefined,
side: 'depan' | 'belakang',
) {
const number = memberNumber ?? 'anggota'
return `kad-digital-${number}-${side}.png`
}
export function toProxiedStorageUrl(url: string | null | undefined): string | null | undefined {
if (!url) return url
try {
const parsed = new URL(url, window.location.origin)
if (parsed.pathname.startsWith('/storage/')) {
return `${parsed.pathname}${parsed.search}`
}
} catch {
// Keep original URL when parsing fails.
}
return url
}
+5 -12
View File
@@ -13,16 +13,12 @@ defineProps<{
<div :class="embedded ? '' : 'mt-5'">
<Box raised="single" class="p-6">
<div class="mb-6">
<h3 class="text-lg font-semibold text-slate-900">Pewaris</h3>
<p class="mt-1 text-sm text-slate-500">Senarai pewaris pengguna.</p>
<h3 class="text-lg font-semibold text-slate-900">Penama</h3>
<p class="mt-1 text-sm text-slate-500">Senarai penama pengguna.</p>
</div>
<div v-if="user.heirs?.length" class="space-y-3">
<div
v-for="heir in user.heirs"
:key="heir.id"
class="rounded-lg border border-foreground/10 p-4"
>
<div v-for="heir in user.heirs" :key="heir.id" class="rounded-lg border border-foreground/10 p-4">
<div class="flex flex-wrap items-center gap-2">
<span class="font-medium text-slate-900">{{ heir.name }}</span>
<Badge v-if="heir.is_primary" class="bg-green-500 text-white">Utama</Badge>
@@ -34,11 +30,8 @@ defineProps<{
</div>
</div>
<div
v-else
class="rounded-lg border border-dashed border-foreground/15 p-6 text-center text-sm text-slate-500"
>
Tiada pewaris direkodkan.
<div v-else class="rounded-lg border border-dashed border-foreground/15 p-6 text-center text-sm text-slate-500">
Tiada penama direkodkan.
</div>
</Box>
</div>
@@ -146,7 +146,7 @@ onMounted(() => {
<Lucide class="mr-2 size-4" icon="Banknote" /> Bank
</TabsTrigger>
<TabsTrigger class="inline-flex w-1/4 items-center justify-center" value="6">
<Lucide class="mr-2 size-4" icon="Users" /> Pewaris
<Lucide class="mr-2 size-4" icon="Users" /> Penama
</TabsTrigger>
</TabsList>
</div>

Some files were not shown because too many files have changed in this diff Show More