Wiki
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { Form } from '@inertiajs/vue3';
|
||||
import { useTemplateRef } from 'vue';
|
||||
import ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController';
|
||||
import Heading from '@/components/Heading.vue';
|
||||
import InputError from '@/components/InputError.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -17,6 +16,7 @@ import {
|
||||
} from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController';
|
||||
|
||||
const passwordInput = useTemplateRef('passwordInput');
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<script lang="ts" setup>
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import markdownItKatex from 'markdown-it-katex';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import 'katex/dist/katex.min.css';
|
||||
|
||||
const props = defineProps<{
|
||||
markdownFileName: string;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
loaded: [];
|
||||
}>();
|
||||
|
||||
const html = ref<string>('');
|
||||
const title = ref<string>('');
|
||||
|
||||
onMounted(() => {
|
||||
const fullUrl = `/helptext/${props.markdownFileName}.md`;
|
||||
|
||||
fetch(fullUrl)
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error('Failed to load file');
|
||||
return res.text();
|
||||
})
|
||||
.then((markdown) => {
|
||||
const md = new MarkdownIt({
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true,
|
||||
});
|
||||
md.use(markdownItKatex);
|
||||
md.renderer.rules.link_open = (
|
||||
tokens,
|
||||
index,
|
||||
options,
|
||||
env,
|
||||
self,
|
||||
) => {
|
||||
const token = tokens[index];
|
||||
token.attrSet('target', '_blank');
|
||||
token.attrSet('rel', 'noopener noreferrer');
|
||||
return self.renderToken(tokens, index, options);
|
||||
};
|
||||
|
||||
html.value = md.render(markdown);
|
||||
const firstHeading = html.value.match(/<h[1-6]>(.*?)<\/h[1-6]>/i);
|
||||
title.value = firstHeading?.[1].trim() || '';
|
||||
html.value = firstHeading
|
||||
? html.value.replace(firstHeading[0], '')
|
||||
: html.value;
|
||||
emit('loaded');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Could not load help file:', err);
|
||||
html.value = '';
|
||||
title.value = '';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 w-full">
|
||||
<h1 class="b-4 mb-7 text-4xl font-semibold">{{ title }}</h1>
|
||||
|
||||
<div
|
||||
v-html="html"
|
||||
class="max-w-none block prose prose-lg text-justify prose-headings:text-left dark:prose-invert prose-code:before:hidden prose-code:after:hidden"
|
||||
role="region"
|
||||
aria-label="Help text"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.prose :deep(.katex) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.prose :deep(.katex .vlist) {
|
||||
font-size: 0.7em;
|
||||
line-height: 0em;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
|
||||
/* Custom styles for the help text */
|
||||
.exercice {
|
||||
padding: 0.1px 1rem;
|
||||
background-color: var(--accent);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.exercice ul li span {
|
||||
font-style: oblique;
|
||||
}
|
||||
</style>
|
||||
@@ -40,14 +40,14 @@ const rowBgDark = computed(() => {
|
||||
<template>
|
||||
<table class="table w-full border-collapse border border-gray-300">
|
||||
<tr class="text-left" v-if="props.iterations.length > 0">
|
||||
<th class="sticky top-0 z-10 bg-background">Époch</th>
|
||||
<th class="sticky top-0 z-10 bg-background">Époque</th>
|
||||
<th class="sticky top-0 z-10 bg-background">Exemple</th>
|
||||
<th
|
||||
v-for="(weight, index) in displayedWeights"
|
||||
v-bind:key="index"
|
||||
class="sticky top-0 z-10 bg-background"
|
||||
>
|
||||
X<sub>{{ index }}</sub>
|
||||
w<sub>{{ index }}</sub>
|
||||
</th>
|
||||
<th class="sticky top-0 z-10 bg-background">Erreur</th>
|
||||
</tr>
|
||||
|
||||
@@ -6,42 +6,47 @@ const page = usePage();
|
||||
|
||||
const links = [
|
||||
{
|
||||
name: 'Perceptron Simple',
|
||||
name: 'Introduction',
|
||||
href: '/',
|
||||
data: {},
|
||||
},
|
||||
{
|
||||
name: '1. Perceptron Simple',
|
||||
href: '/perceptron',
|
||||
data: { type: 'simple' },
|
||||
},
|
||||
{
|
||||
name: 'Descente du gradient',
|
||||
name: '2. Descente du gradient',
|
||||
href: '/perceptron',
|
||||
data: { type: 'gradientdescent' },
|
||||
},
|
||||
{
|
||||
name: 'ADALINE',
|
||||
name: '3. ADALINE',
|
||||
href: '/perceptron',
|
||||
data: { type: 'adaline' },
|
||||
},
|
||||
{
|
||||
name: 'Mono-couche',
|
||||
name: '4. Monocouche',
|
||||
href: '/perceptron',
|
||||
data: { type: 'monolayer' },
|
||||
},
|
||||
{
|
||||
name: 'Multi-couche',
|
||||
name: '5. Multicouches',
|
||||
href: '/perceptron',
|
||||
data: { type: 'multilayer' },
|
||||
},
|
||||
];
|
||||
|
||||
const isActiveLink = (link: any) => {
|
||||
return page.component === 'PerceptronViewer' && page.props.type === link.data.type;
|
||||
return (page.component === 'Home' && page.props.type === link.data?.type) || (page.component === 'PerceptronViewer' && page.props.type === link.data?.type);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header
|
||||
class="my-6 w-full text-sm not-has-[nav]:hidden"
|
||||
class="my-6 w-full overflow-x-auto text-sm not-has-[nav]:hidden"
|
||||
>
|
||||
<nav class="flex items-center justify-center gap-4">
|
||||
<nav class="min-w-max flex items-center justify-center gap-4">
|
||||
<Link
|
||||
v-for="link in links"
|
||||
:key="link.name"
|
||||
|
||||
@@ -129,8 +129,8 @@ const datasets = computed<ErrorDataset[]>(() => {
|
||||
}"
|
||||
:data="{
|
||||
labels: props.iterations.reduce((labels, iteration) => {
|
||||
if (!labels.includes(`Époch ${iteration.epoch}`)) {
|
||||
labels.push(`Époch ${iteration.epoch}`);
|
||||
if (!labels.includes(`Époque ${iteration.epoch}`)) {
|
||||
labels.push(`Époque ${iteration.epoch}`);
|
||||
}
|
||||
return labels;
|
||||
}, [] as string[]),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
// import { Form } from '@inertiajs/vue3';
|
||||
|
||||
import { useForm } from '@inertiajs/vue3';
|
||||
import { ref, watch } from 'vue';
|
||||
import {
|
||||
Form,
|
||||
@@ -17,6 +16,7 @@ import type {
|
||||
Dataset,
|
||||
InitializationMethod,
|
||||
PerceptronType,
|
||||
ValidationErrors,
|
||||
} from '@/types/perceptron';
|
||||
import Button from './ui/button/Button.vue';
|
||||
import Card from './ui/card/Card.vue';
|
||||
@@ -36,6 +36,8 @@ const props = defineProps<{
|
||||
defaultLearningRate: number;
|
||||
sessionId: string;
|
||||
defaultMaxIterations: number;
|
||||
maxIterationsLimit: number;
|
||||
errors: ValidationErrors;
|
||||
}>();
|
||||
|
||||
const selectedDatasetCopy = ref(props.selectedDataset);
|
||||
@@ -45,6 +47,52 @@ const hiddenLayersNeurons = ref(props.hiddenLayersNeurons);
|
||||
const minError = ref(props.minError);
|
||||
const learningRate = ref(props.defaultLearningRate);
|
||||
const maxIterations = ref(props.defaultMaxIterations);
|
||||
const maxIterationsInput = ref<{
|
||||
inputElement: HTMLInputElement | null;
|
||||
} | null>(null);
|
||||
const form = useForm({
|
||||
type: props.type,
|
||||
dataset: '',
|
||||
weight_init_method: selectedMethod.value,
|
||||
hidden_layers: hiddenLayers.value,
|
||||
hidden_layers_neurons: hiddenLayersNeurons.value,
|
||||
min_error: minError.value,
|
||||
learning_rate: learningRate.value,
|
||||
session_id: props.sessionId,
|
||||
training_id: '',
|
||||
max_iterations: maxIterations.value,
|
||||
});
|
||||
|
||||
function handleMaxIterationsInput(event: Event) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const numericValue = Number(input.value);
|
||||
const normalizedValue = Number.isFinite(numericValue)
|
||||
? Math.min(Math.max(numericValue, 1), props.maxIterationsLimit)
|
||||
: 1;
|
||||
|
||||
console.debug('[max_iterations] native input', {
|
||||
raw: input.value,
|
||||
numericValue,
|
||||
normalizedValue,
|
||||
limit: props.maxIterationsLimit,
|
||||
});
|
||||
|
||||
input.value = String(normalizedValue);
|
||||
maxIterations.value = normalizedValue;
|
||||
}
|
||||
|
||||
watch(maxIterations, (value) => {
|
||||
const input = maxIterationsInput.value?.inputElement;
|
||||
|
||||
console.debug('[max_iterations] ref changed', {
|
||||
value,
|
||||
displayed: input?.value,
|
||||
});
|
||||
|
||||
if (input && input.value !== String(value)) {
|
||||
input.value = String(value);
|
||||
}
|
||||
});
|
||||
|
||||
watch(selectedDatasetCopy, (newvalue) => {
|
||||
const selectedDatasetCopy = props.datasets.find(
|
||||
@@ -85,46 +133,32 @@ function startTraining() {
|
||||
return;
|
||||
}
|
||||
|
||||
trainingId.value = `${props.sessionId}-${Date.now()}`; // Unique training ID based on session and timestamp
|
||||
emit('update:trainingId', trainingId.value); // Emit the training ID to the parent component
|
||||
trainingId.value = `${props.sessionId}-${Date.now()}`;
|
||||
emit('update:trainingId', trainingId.value);
|
||||
|
||||
fetch('/api/perceptron/run', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({
|
||||
type: props.type,
|
||||
dataset: selectedDatasetCopy.value,
|
||||
weight_init_method: selectedMethod.value,
|
||||
hidden_layers: hiddenLayers.value,
|
||||
hidden_layers_neurons: hiddenLayersNeurons.value,
|
||||
min_error: minError.value,
|
||||
learning_rate: learningRate.value,
|
||||
session_id: props.sessionId,
|
||||
training_id: trainingId.value,
|
||||
max_iterations: maxIterations.value,
|
||||
}),
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
if (response.status === 429) {
|
||||
alert('Trop de requêtes pour mon petit serveur. Veuillez réessayer dans une minute.');
|
||||
} else {
|
||||
alert('Erreur lors du lancement de l\'entraînement. Veuillez réessayer.');
|
||||
}
|
||||
Object.assign(form, {
|
||||
type: props.type,
|
||||
dataset: selectedDatasetCopy.value,
|
||||
weight_init_method: selectedMethod.value,
|
||||
hidden_layers: hiddenLayers.value,
|
||||
hidden_layers_neurons: hiddenLayersNeurons.value,
|
||||
min_error: minError.value,
|
||||
learning_rate: learningRate.value,
|
||||
session_id: props.sessionId,
|
||||
training_id: trainingId.value,
|
||||
max_iterations: maxIterations.value,
|
||||
});
|
||||
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
console.log('Perceptron training started:', data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error starting perceptron training:', error);
|
||||
});
|
||||
console.debug('[max_iterations] submitting', {
|
||||
displayed: maxIterationsInput.value?.inputElement?.value,
|
||||
refValue: maxIterations.value,
|
||||
formValue: form.max_iterations,
|
||||
limit: props.maxIterationsLimit,
|
||||
});
|
||||
|
||||
form.post('/perceptron/run', {
|
||||
preserveScroll: true,
|
||||
});
|
||||
}
|
||||
|
||||
const emit = defineEmits(['update:selectedDataset', 'update:trainingId']);
|
||||
@@ -161,10 +195,11 @@ watch(selectedDatasetCopy, (newValue) => {
|
||||
v-bind:key="dataset.label"
|
||||
:value="dataset.label"
|
||||
>
|
||||
{{ dataset.label }}
|
||||
{{ dataset.label.replace(/_/g, ' ') }}
|
||||
</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
</FormControl>
|
||||
<div v-if="props.errors?.selectedDatasetCopy">{{ props.errors?.selectedDatasetCopy }}</div>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
@@ -200,6 +235,7 @@ watch(selectedDatasetCopy, (newValue) => {
|
||||
<FormControl>
|
||||
<!-- TODO : MAX input -->
|
||||
<Input
|
||||
ref="maxIterationsInput"
|
||||
type="number"
|
||||
v-model="hiddenLayers"
|
||||
min="1"
|
||||
@@ -264,17 +300,21 @@ watch(selectedDatasetCopy, (newValue) => {
|
||||
<!-- MAX ITERATIONS -->
|
||||
<FormField name="max_iterations">
|
||||
<FormItem>
|
||||
<FormLabel>Nombre maximum d'itérations</FormLabel>
|
||||
<FormLabel>Nombre maximum d'époques</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
v-model="maxIterations"
|
||||
min="0"
|
||||
max="5000"
|
||||
:model-value="maxIterations"
|
||||
min="1"
|
||||
:max="props.maxIterationsLimit"
|
||||
step="1"
|
||||
class="w-min"
|
||||
@input="handleMaxIterationsInput"
|
||||
/>
|
||||
</FormControl>
|
||||
<div v-if="form.errors.max_iterations || props.errors.max_iterations">
|
||||
{{ form.errors.max_iterations || props.errors.max_iterations }}
|
||||
</div>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</Form>
|
||||
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import UserInfo from '@/components/UserInfo.vue';
|
||||
import { logout } from '@/routes';
|
||||
import { edit } from '@/routes/profile';
|
||||
import type { User } from '@/types';
|
||||
import { edit } from '@/routes/profile';
|
||||
|
||||
type Props = {
|
||||
user: User;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import type { AccordionRootEmits, AccordionRootProps } from "reka-ui"
|
||||
import {
|
||||
AccordionRoot,
|
||||
useForwardPropsEmits,
|
||||
} from "reka-ui"
|
||||
|
||||
const props = defineProps<AccordionRootProps>()
|
||||
const emits = defineEmits<AccordionRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AccordionRoot v-slot="slotProps" data-slot="accordion" v-bind="forwarded">
|
||||
<slot v-bind="slotProps" />
|
||||
</AccordionRoot>
|
||||
</template>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import type { AccordionContentProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { AccordionContent } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<AccordionContentProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AccordionContent
|
||||
data-slot="accordion-content"
|
||||
v-bind="delegatedProps"
|
||||
class="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||
>
|
||||
<div :class="cn('pt-0 pb-4', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</template>
|
||||
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import type { AccordionItemProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { AccordionItem, useForwardProps } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<AccordionItemProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AccordionItem
|
||||
v-slot="slotProps"
|
||||
data-slot="accordion-item"
|
||||
v-bind="forwardedProps"
|
||||
:class="cn('border-b last:border-b-0', props.class)"
|
||||
>
|
||||
<slot v-bind="slotProps" />
|
||||
</AccordionItem>
|
||||
</template>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
import type { AccordionTriggerProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { ChevronDown } from "@lucide/vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import {
|
||||
AccordionHeader,
|
||||
AccordionTrigger,
|
||||
} from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<AccordionTriggerProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AccordionHeader class="flex">
|
||||
<AccordionTrigger
|
||||
data-slot="accordion-trigger"
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
cn(
|
||||
'focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-3 disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
<slot name="icon">
|
||||
<ChevronDown
|
||||
class="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200"
|
||||
/>
|
||||
</slot>
|
||||
</AccordionTrigger>
|
||||
</AccordionHeader>
|
||||
</template>
|
||||
@@ -0,0 +1,4 @@
|
||||
export { default as Accordion } from "./Accordion.vue"
|
||||
export { default as AccordionContent } from "./AccordionContent.vue"
|
||||
export { default as AccordionItem } from "./AccordionItem.vue"
|
||||
export { default as AccordionTrigger } from "./AccordionTrigger.vue"
|
||||
@@ -27,6 +27,7 @@ export const buttonVariants = cva(
|
||||
"icon": "size-9",
|
||||
"icon-sm": "size-8",
|
||||
"icon-lg": "size-10",
|
||||
"icon-xl": "size-13",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DrawerRootEmits, DrawerRootProps } from "reka-ui"
|
||||
import { DrawerRoot, useForwardPropsEmits } from "reka-ui"
|
||||
|
||||
const props = defineProps<DrawerRootProps>()
|
||||
|
||||
const emits = defineEmits<DrawerRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DrawerRoot
|
||||
v-slot="slotProps"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot v-bind="slotProps" />
|
||||
</DrawerRoot>
|
||||
</template>
|
||||
@@ -0,0 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DrawerCloseProps } from "reka-ui"
|
||||
import { DrawerClose } from "reka-ui"
|
||||
|
||||
const props = defineProps<DrawerCloseProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DrawerClose
|
||||
data-slot="drawer-close"
|
||||
v-bind="props"
|
||||
>
|
||||
<slot />
|
||||
</DrawerClose>
|
||||
</template>
|
||||
@@ -0,0 +1,48 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DrawerContentEmits, DrawerContentProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import {
|
||||
DrawerContent,
|
||||
DrawerHandle,
|
||||
DrawerPortal,
|
||||
useForwardPropsEmits,
|
||||
} from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
import DrawerOverlay from "./DrawerOverlay.vue"
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const props = defineProps<DrawerContentProps & { class?: HTMLAttributes["class"] }>()
|
||||
const emits = defineEmits<DrawerContentEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DrawerPortal>
|
||||
<DrawerOverlay />
|
||||
<DrawerContent
|
||||
data-slot="drawer-content"
|
||||
v-bind="{ ...$attrs, ...forwarded }"
|
||||
:class="cn(
|
||||
'group/drawer-content bg-background fixed z-50 flex h-auto flex-col',
|
||||
'will-change-transform transform-[translate3d(var(--drawer-swipe-movement-x,0px),var(--drawer-swipe-movement-y,0px),0)]',
|
||||
'transition-transform duration-500 ease-[cubic-bezier(0.32,0.72,0,1)] data-swiping:duration-0 data-swiping:select-none',
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
||||
'data-[swipe-direction=up]:inset-x-0 data-[swipe-direction=up]:top-0 data-[swipe-direction=up]:mb-24 data-[swipe-direction=up]:max-h-[80vh] data-[swipe-direction=up]:rounded-b-lg data-[swipe-direction=up]:data-[state=open]:slide-in-from-top data-[swipe-direction=up]:data-[state=closed]:slide-out-to-top',
|
||||
'data-[swipe-direction=down]:inset-x-0 data-[swipe-direction=down]:bottom-0 data-[swipe-direction=down]:mt-24 data-[swipe-direction=down]:max-h-[80vh] data-[swipe-direction=down]:rounded-t-lg data-[swipe-direction=down]:data-[state=open]:slide-in-from-bottom data-[swipe-direction=down]:data-[state=closed]:slide-out-to-bottom',
|
||||
'data-[swipe-direction=right]:inset-y-0 data-[swipe-direction=right]:right-0 data-[swipe-direction=right]:w-3/4 data-[swipe-direction=right]:sm:max-w-sm data-[swipe-direction=right]:data-[state=open]:slide-in-from-right data-[swipe-direction=right]:data-[state=closed]:slide-out-to-right',
|
||||
'data-[swipe-direction=left]:inset-y-0 data-[swipe-direction=left]:left-0 data-[swipe-direction=left]:w-3/4 data-[swipe-direction=left]:sm:max-w-sm data-[swipe-direction=left]:data-[state=open]:slide-in-from-left data-[swipe-direction=left]:data-[state=closed]:slide-out-to-left',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<DrawerHandle class="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[swipe-direction=down]/drawer-content:block" />
|
||||
<slot />
|
||||
</DrawerContent>
|
||||
</DrawerPortal>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DrawerDescriptionProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { DrawerDescription } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<DrawerDescriptionProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DrawerDescription
|
||||
data-slot="drawer-description"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('text-muted-foreground text-sm', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</DrawerDescription>
|
||||
</template>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="drawer-footer"
|
||||
:class="cn('mt-auto flex flex-col gap-2 p-4', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="drawer-header"
|
||||
:class="cn('flex flex-col gap-1.5 p-4', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DrawerOverlayProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { DrawerOverlay } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<DrawerOverlayProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DrawerOverlay
|
||||
data-slot="drawer-overlay"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80 duration-500', props.class)"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DrawerTitleProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { DrawerTitle } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<DrawerTitleProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DrawerTitle
|
||||
data-slot="drawer-title"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('text-foreground font-semibold', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</DrawerTitle>
|
||||
</template>
|
||||
@@ -0,0 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DrawerTriggerProps } from "reka-ui"
|
||||
import { DrawerTrigger } from "reka-ui"
|
||||
|
||||
const props = defineProps<DrawerTriggerProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DrawerTrigger
|
||||
data-slot="drawer-trigger"
|
||||
v-bind="props"
|
||||
>
|
||||
<slot />
|
||||
</DrawerTrigger>
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
export { default as Drawer } from "./Drawer.vue"
|
||||
export { default as DrawerClose } from "./DrawerClose.vue"
|
||||
export { default as DrawerContent } from "./DrawerContent.vue"
|
||||
export { default as DrawerDescription } from "./DrawerDescription.vue"
|
||||
export { default as DrawerFooter } from "./DrawerFooter.vue"
|
||||
export { default as DrawerHeader } from "./DrawerHeader.vue"
|
||||
export { default as DrawerOverlay } from "./DrawerOverlay.vue"
|
||||
export { default as DrawerTitle } from "./DrawerTitle.vue"
|
||||
export { default as DrawerTrigger } from "./DrawerTrigger.vue"
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { ref } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -13,15 +13,22 @@ const emits = defineEmits<{
|
||||
(e: "update:modelValue", payload: string | number): void
|
||||
}>()
|
||||
|
||||
const modelValue = useVModel(props, "modelValue", emits, {
|
||||
passive: true,
|
||||
defaultValue: props.defaultValue,
|
||||
})
|
||||
const inputElement = ref<HTMLInputElement | null>(null)
|
||||
|
||||
defineExpose({ inputElement })
|
||||
|
||||
function handleInput(event: Event) {
|
||||
const input = event.target as HTMLInputElement
|
||||
emits("update:modelValue", input.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
v-model="modelValue"
|
||||
ref="inputElement"
|
||||
:value="modelValue ?? defaultValue ?? ''"
|
||||
v-bind="$attrs"
|
||||
@input="handleInput"
|
||||
data-slot="input"
|
||||
:class="cn(
|
||||
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<kbd
|
||||
:class="cn(
|
||||
'bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none',
|
||||
`[&_svg:not([class*='size-'])]:size-3`,
|
||||
'[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<slot />
|
||||
</kbd>
|
||||
</template>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<kbd
|
||||
data-slot="kbd-group"
|
||||
:class="cn('inline-flex items-center gap-1', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</kbd>
|
||||
</template>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as Kbd } from "./Kbd.vue"
|
||||
export { default as KbdGroup } from "./KbdGroup.vue"
|
||||
Reference in New Issue
Block a user