feat: lint codebase and run precommit hook
This commit is contained in:
@@ -31,6 +31,7 @@ Stories are organized into two main categories:
|
||||
3. Optionally create a fields file: `fields.ts` (for controls configuration)
|
||||
|
||||
**Example:**
|
||||
|
||||
```
|
||||
src/molecules/hover-card/
|
||||
├── hover-card.stories.svelte
|
||||
@@ -44,16 +45,16 @@ Every story file follows this structure:
|
||||
```svelte
|
||||
<script module>
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
|
||||
|
||||
// Component imports
|
||||
import * as ComponentName from '@cio/ui/base/component-name';
|
||||
|
||||
|
||||
// Other imports (icons, utilities, etc.)
|
||||
import IconName from '@lucide/svelte/icons/icon-name';
|
||||
|
||||
|
||||
// Fields import (if using controls)
|
||||
import { FIELDS } from './fields';
|
||||
|
||||
|
||||
// Story definition
|
||||
const { Story } = defineMeta({
|
||||
title: 'Category/ComponentName',
|
||||
@@ -66,7 +67,7 @@ Every story file follows this structure:
|
||||
},
|
||||
tags: ['autodocs']
|
||||
});
|
||||
|
||||
|
||||
// State variables (if needed)
|
||||
let someState = $state('');
|
||||
</script>
|
||||
@@ -83,25 +84,23 @@ Every story file follows this structure:
|
||||
### Component Imports
|
||||
|
||||
- **Namespace imports** for multi-part components:
|
||||
|
||||
```svelte
|
||||
import * as HoverCard from '@cio/ui/base/hover-card';
|
||||
import * as Card from '@cio/ui/base/card';
|
||||
import * as Field from '@cio/ui/base/field';
|
||||
import * as HoverCard from '@cio/ui/base/hover-card'; import * as Card from '@cio/ui/base/card'; import * as Field
|
||||
from '@cio/ui/base/field';
|
||||
```
|
||||
|
||||
- **Named imports** for single components:
|
||||
```svelte
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import { Input } from '@cio/ui/base/input';
|
||||
import { Avatar } from '@cio/ui/base/avatar';
|
||||
import {Button} from '@cio/ui/base/button'; import {Input} from '@cio/ui/base/input'; import {Avatar} from '@cio/ui/base/avatar';
|
||||
```
|
||||
|
||||
### Icon Imports
|
||||
|
||||
Use Lucide Svelte icons:
|
||||
|
||||
```svelte
|
||||
import CalendarDaysIcon from '@lucide/svelte/icons/calendar-days';
|
||||
import UsersIcon from '@lucide/svelte/icons/users';
|
||||
import CalendarDaysIcon from '@lucide/svelte/icons/calendar-days'; import UsersIcon from '@lucide/svelte/icons/users';
|
||||
import BookOpenIcon from '@lucide/svelte/icons/book-open';
|
||||
```
|
||||
|
||||
@@ -115,20 +114,20 @@ import BookOpenIcon from '@lucide/svelte/icons/book-open';
|
||||
### Component Reference
|
||||
|
||||
- For multi-part components, reference the `Root` component:
|
||||
|
||||
```svelte
|
||||
component: HoverCard.Root
|
||||
component: Card.Root
|
||||
component: HoverCard.Root component: Card.Root
|
||||
```
|
||||
|
||||
- For single components, reference directly:
|
||||
```svelte
|
||||
component: Button
|
||||
component: Input
|
||||
component: Button component: Input
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
Standard parameters configuration:
|
||||
|
||||
```svelte
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
@@ -159,6 +158,7 @@ Always include `tags: ['autodocs']` for automatic documentation generation.
|
||||
### Template with Args (for controls)
|
||||
|
||||
For atoms that need to pass args to controls:
|
||||
|
||||
```svelte
|
||||
<Story name="Default">
|
||||
{#snippet template(args)}
|
||||
@@ -170,6 +170,7 @@ For atoms that need to pass args to controls:
|
||||
### Multiple Stories
|
||||
|
||||
Create multiple stories to showcase different use cases:
|
||||
|
||||
```svelte
|
||||
<Story name="Default">
|
||||
{#snippet template()}
|
||||
@@ -199,6 +200,7 @@ export const FIELDS = ['class'] as string[];
|
||||
```
|
||||
|
||||
Common fields:
|
||||
|
||||
- `'class'` - CSS classes
|
||||
- `'variant'` - Component variants
|
||||
- `'size'` - Component sizes
|
||||
@@ -210,6 +212,7 @@ Common fields:
|
||||
### Atoms
|
||||
|
||||
Basic, indivisible components:
|
||||
|
||||
- Button, Input, Badge, Avatar, Label, Checkbox, Switch, etc.
|
||||
- Single-purpose components
|
||||
- Usually don't compose other components
|
||||
@@ -217,6 +220,7 @@ Basic, indivisible components:
|
||||
### Molecules
|
||||
|
||||
Composite components:
|
||||
|
||||
- Card, Field, ButtonGroup, HoverCard, Empty, etc.
|
||||
- Made of multiple atoms or other components
|
||||
- More complex functionality
|
||||
@@ -279,6 +283,7 @@ Since ClassroomIO is an LMS platform, all story content should be **LMS-related*
|
||||
### Content Patterns
|
||||
|
||||
**Instructor Profiles:**
|
||||
|
||||
```svelte
|
||||
<h4 class="text-sm font-semibold">Sarah Johnson</h4>
|
||||
<p class="text-sm">Senior Full-Stack Developer & Instructor</p>
|
||||
@@ -286,6 +291,7 @@ Since ClassroomIO is an LMS platform, all story content should be **LMS-related*
|
||||
```
|
||||
|
||||
**Course Information:**
|
||||
|
||||
```svelte
|
||||
<h4 class="text-sm font-semibold">Advanced React Patterns</h4>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
@@ -294,6 +300,7 @@ Since ClassroomIO is an LMS platform, all story content should be **LMS-related*
|
||||
```
|
||||
|
||||
**Student Information:**
|
||||
|
||||
```svelte
|
||||
<h4 class="text-sm font-semibold">Alex Martinez</h4>
|
||||
<p class="text-muted-foreground text-xs">5 courses enrolled</p>
|
||||
@@ -455,4 +462,3 @@ When creating a new story, ensure:
|
||||
- [Storybook Svelte CSF Addon](https://storybook.js.org/addons/@storybook/addon-svelte-csf)
|
||||
- [Bits UI Documentation](https://bits-ui.com/docs/components)
|
||||
- [Lucide Icons](https://lucide.dev/icons/)
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"dev": "storybook dev -p 6006",
|
||||
"build": "storybook build",
|
||||
"preview": "storybook preview",
|
||||
"format": "prettier --write .",
|
||||
"build-storybook": "storybook build",
|
||||
"upload:storybook": "tsx scripts/upload-storybook.ts",
|
||||
"publish:storybook": "pnpm build-storybook && pnpm upload:storybook"
|
||||
|
||||
@@ -1,9 +1 @@
|
||||
export const FIELDS = [
|
||||
'type',
|
||||
'value',
|
||||
'onValueChange',
|
||||
'disabled',
|
||||
'name',
|
||||
'loop',
|
||||
'orientation'
|
||||
] as string[];
|
||||
export const FIELDS = ['type', 'value', 'onValueChange', 'disabled', 'name', 'loop', 'orientation'] as string[];
|
||||
|
||||
@@ -1,8 +1 @@
|
||||
export const FIELDS = [
|
||||
'checked',
|
||||
'onCheckedChange',
|
||||
'disabled',
|
||||
'name',
|
||||
'value',
|
||||
'required'
|
||||
] as string[];
|
||||
export const FIELDS = ['checked', 'onCheckedChange', 'disabled', 'name', 'value', 'required'] as string[];
|
||||
|
||||
@@ -1,6 +1 @@
|
||||
export const FIELDS = [
|
||||
'open',
|
||||
'onOpenChange',
|
||||
'onOpenChangeComplete',
|
||||
'disabled'
|
||||
] as string[];
|
||||
export const FIELDS = ['open', 'onOpenChange', 'onOpenChangeComplete', 'disabled'] as string[];
|
||||
|
||||
@@ -1,8 +1 @@
|
||||
export const FIELDS = [
|
||||
'value',
|
||||
'onValueChange',
|
||||
'label',
|
||||
'filter',
|
||||
'columns',
|
||||
'disablePointerSelection'
|
||||
] as string[];
|
||||
export const FIELDS = ['value', 'onValueChange', 'label', 'filter', 'columns', 'disablePointerSelection'] as string[];
|
||||
|
||||
@@ -68,22 +68,19 @@
|
||||
{
|
||||
name: 'v0-1.5-sm',
|
||||
description: 'Everyday tasks and UI generation.',
|
||||
image:
|
||||
'https://images.unsplash.com/photo-1650804068570-7fb2e3dbf888?q=80&w=640&auto=format&fit=crop',
|
||||
image: 'https://images.unsplash.com/photo-1650804068570-7fb2e3dbf888?q=80&w=640&auto=format&fit=crop',
|
||||
credit: 'Valeria Reverdo on Unsplash'
|
||||
},
|
||||
{
|
||||
name: 'v0-1.5-lg',
|
||||
description: 'Advanced thinking or reasoning.',
|
||||
image:
|
||||
'https://images.unsplash.com/photo-1610280777472-54133d004c8c?q=80&w=640&auto=format&fit=crop',
|
||||
image: 'https://images.unsplash.com/photo-1610280777472-54133d004c8c?q=80&w=640&auto=format&fit=crop',
|
||||
credit: 'Michael Oeser on Unsplash'
|
||||
},
|
||||
{
|
||||
name: 'v0-2.0-mini',
|
||||
description: 'Open Source model for everyone.',
|
||||
image:
|
||||
'https://images.unsplash.com/photo-1602146057681-08560aee8cde?q=80&w=640&auto=format&fit=crop',
|
||||
image: 'https://images.unsplash.com/photo-1602146057681-08560aee8cde?q=80&w=640&auto=format&fit=crop',
|
||||
credit: 'Cherry Laithang on Unsplash'
|
||||
}
|
||||
];
|
||||
@@ -413,4 +410,3 @@
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
|
||||
@@ -1,7 +1 @@
|
||||
export const FIELDS = [
|
||||
'onValueChange',
|
||||
'value',
|
||||
'dir',
|
||||
'onOpenChange',
|
||||
'disabled'
|
||||
] as string[];
|
||||
export const FIELDS = ['onValueChange', 'value', 'dir', 'onOpenChange', 'disabled'] as string[];
|
||||
|
||||
@@ -1,7 +1 @@
|
||||
export const FIELDS = [
|
||||
'count',
|
||||
'page',
|
||||
'onPageChange',
|
||||
'perPage',
|
||||
'orientation'
|
||||
] as string[];
|
||||
export const FIELDS = ['count', 'page', 'onPageChange', 'perPage', 'orientation'] as string[];
|
||||
|
||||
@@ -1,9 +1 @@
|
||||
export const FIELDS = [
|
||||
'value',
|
||||
'onValueChange',
|
||||
'disabled',
|
||||
'required',
|
||||
'name',
|
||||
'loop',
|
||||
'orientation'
|
||||
] as string[];
|
||||
export const FIELDS = ['value', 'onValueChange', 'disabled', 'required', 'name', 'loop', 'orientation'] as string[];
|
||||
|
||||
@@ -1,8 +1 @@
|
||||
export const FIELDS = [
|
||||
'checked',
|
||||
'onCheckedChange',
|
||||
'disabled',
|
||||
'name',
|
||||
'required',
|
||||
'value'
|
||||
] as string[];
|
||||
export const FIELDS = ['checked', 'onCheckedChange', 'disabled', 'name', 'required', 'value'] as string[];
|
||||
|
||||
@@ -1,7 +1 @@
|
||||
export const FIELDS = [
|
||||
'value',
|
||||
'onValueChange',
|
||||
'disabled',
|
||||
'activationMode',
|
||||
'orientation'
|
||||
] as string[];
|
||||
export const FIELDS = ['value', 'onValueChange', 'disabled', 'activationMode', 'orientation'] as string[];
|
||||
|
||||
@@ -1,6 +1 @@
|
||||
export const FIELDS = [
|
||||
'disableHoverableContent',
|
||||
'delayDuration',
|
||||
'ignoreNonKeyboardFocus',
|
||||
'disabled'
|
||||
] as string[];
|
||||
export const FIELDS = ['disableHoverableContent', 'delayDuration', 'ignoreNonKeyboardFocus', 'disabled'] as string[];
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export const FIELDS = ['class'] as string[];
|
||||
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export const FIELDS = ['disabled', 'class', 'type', 'tooltip', 'tooltipSide'] as string[];
|
||||
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export const FIELDS = ['class'] as string[];
|
||||
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export const FIELDS = ['class'] as string[];
|
||||
|
||||
|
||||
@@ -94,4 +94,3 @@
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import Root from "./button-group.svelte";
|
||||
import Text from "./button-group-text.svelte";
|
||||
import Separator from "./button-group-separator.svelte";
|
||||
import Root from './button-group.svelte';
|
||||
import Text from './button-group-text.svelte';
|
||||
import Separator from './button-group-separator.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
Text,
|
||||
Separator,
|
||||
//
|
||||
Root as ButtonGroup,
|
||||
Text as ButtonGroupText,
|
||||
Separator as ButtonGroupSeparator,
|
||||
Root,
|
||||
Text,
|
||||
Separator,
|
||||
//
|
||||
Root as ButtonGroup,
|
||||
Text as ButtonGroupText,
|
||||
Separator as ButtonGroupSeparator
|
||||
};
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import Root from "./card.svelte";
|
||||
import Content from "./card-content.svelte";
|
||||
import Description from "./card-description.svelte";
|
||||
import Footer from "./card-footer.svelte";
|
||||
import Header from "./card-header.svelte";
|
||||
import Title from "./card-title.svelte";
|
||||
import Action from "./card-action.svelte";
|
||||
import Root from './card.svelte';
|
||||
import Content from './card-content.svelte';
|
||||
import Description from './card-description.svelte';
|
||||
import Footer from './card-footer.svelte';
|
||||
import Header from './card-header.svelte';
|
||||
import Title from './card-title.svelte';
|
||||
import Action from './card-action.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Description,
|
||||
Footer,
|
||||
Header,
|
||||
Title,
|
||||
Action,
|
||||
//
|
||||
Root as Card,
|
||||
Content as CardContent,
|
||||
Description as CardDescription,
|
||||
Footer as CardFooter,
|
||||
Header as CardHeader,
|
||||
Title as CardTitle,
|
||||
Action as CardAction,
|
||||
Root,
|
||||
Content,
|
||||
Description,
|
||||
Footer,
|
||||
Header,
|
||||
Title,
|
||||
Action,
|
||||
//
|
||||
Root as Card,
|
||||
Content as CardContent,
|
||||
Description as CardDescription,
|
||||
Footer as CardFooter,
|
||||
Header as CardHeader,
|
||||
Title as CardTitle,
|
||||
Action as CardAction
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
import { Drawer as DrawerPrimitive } from 'vaul-svelte';
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: DrawerPrimitive.CloseProps = $props();
|
||||
let { ref = $bindable(null), ...restProps }: DrawerPrimitive.CloseProps = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Close bind:ref data-slot="drawer-close" {...restProps} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
import { Drawer as DrawerPrimitive } from 'vaul-svelte';
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: DrawerPrimitive.TriggerProps = $props();
|
||||
let { ref = $bindable(null), ...restProps }: DrawerPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Trigger bind:ref data-slot="drawer-trigger" {...restProps} />
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
import { Drawer as DrawerPrimitive } from 'vaul-svelte';
|
||||
|
||||
let {
|
||||
shouldScaleBackground = true,
|
||||
open = $bindable(false),
|
||||
activeSnapPoint = $bindable(null),
|
||||
...restProps
|
||||
}: DrawerPrimitive.RootProps = $props();
|
||||
let {
|
||||
shouldScaleBackground = true,
|
||||
open = $bindable(false),
|
||||
activeSnapPoint = $bindable(null),
|
||||
...restProps
|
||||
}: DrawerPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<DrawerPrimitive.Root {shouldScaleBackground} bind:open bind:activeSnapPoint {...restProps} />
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
||||
import { Drawer as DrawerPrimitive } from 'vaul-svelte';
|
||||
|
||||
import Root from "./drawer.svelte";
|
||||
import Content from "./drawer-content.svelte";
|
||||
import Description from "./drawer-description.svelte";
|
||||
import Overlay from "./drawer-overlay.svelte";
|
||||
import Footer from "./drawer-footer.svelte";
|
||||
import Header from "./drawer-header.svelte";
|
||||
import Title from "./drawer-title.svelte";
|
||||
import NestedRoot from "./drawer-nested.svelte";
|
||||
import Close from "./drawer-close.svelte";
|
||||
import Trigger from "./drawer-trigger.svelte";
|
||||
import Root from './drawer.svelte';
|
||||
import Content from './drawer-content.svelte';
|
||||
import Description from './drawer-description.svelte';
|
||||
import Overlay from './drawer-overlay.svelte';
|
||||
import Footer from './drawer-footer.svelte';
|
||||
import Header from './drawer-header.svelte';
|
||||
import Title from './drawer-title.svelte';
|
||||
import NestedRoot from './drawer-nested.svelte';
|
||||
import Close from './drawer-close.svelte';
|
||||
import Trigger from './drawer-trigger.svelte';
|
||||
|
||||
const Portal: typeof DrawerPrimitive.Portal = DrawerPrimitive.Portal;
|
||||
|
||||
export {
|
||||
Root,
|
||||
NestedRoot,
|
||||
Content,
|
||||
Description,
|
||||
Overlay,
|
||||
Footer,
|
||||
Header,
|
||||
Title,
|
||||
Trigger,
|
||||
Portal,
|
||||
Close,
|
||||
Root,
|
||||
NestedRoot,
|
||||
Content,
|
||||
Description,
|
||||
Overlay,
|
||||
Footer,
|
||||
Header,
|
||||
Title,
|
||||
Trigger,
|
||||
Portal,
|
||||
Close,
|
||||
|
||||
//
|
||||
Root as Drawer,
|
||||
NestedRoot as DrawerNestedRoot,
|
||||
Content as DrawerContent,
|
||||
Description as DrawerDescription,
|
||||
Overlay as DrawerOverlay,
|
||||
Footer as DrawerFooter,
|
||||
Header as DrawerHeader,
|
||||
Title as DrawerTitle,
|
||||
Trigger as DrawerTrigger,
|
||||
Portal as DrawerPortal,
|
||||
Close as DrawerClose,
|
||||
//
|
||||
Root as Drawer,
|
||||
NestedRoot as DrawerNestedRoot,
|
||||
Content as DrawerContent,
|
||||
Description as DrawerDescription,
|
||||
Overlay as DrawerOverlay,
|
||||
Footer as DrawerFooter,
|
||||
Header as DrawerHeader,
|
||||
Title as DrawerTitle,
|
||||
Trigger as DrawerTrigger,
|
||||
Portal as DrawerPortal,
|
||||
Close as DrawerClose
|
||||
};
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import Root from "./empty.svelte";
|
||||
import Header from "./empty-header.svelte";
|
||||
import Media from "./empty-media.svelte";
|
||||
import Title from "./empty-title.svelte";
|
||||
import Description from "./empty-description.svelte";
|
||||
import Content from "./empty-content.svelte";
|
||||
import Root from './empty.svelte';
|
||||
import Header from './empty-header.svelte';
|
||||
import Media from './empty-media.svelte';
|
||||
import Title from './empty-title.svelte';
|
||||
import Description from './empty-description.svelte';
|
||||
import Content from './empty-content.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
Header,
|
||||
Media,
|
||||
Title,
|
||||
Description,
|
||||
Content,
|
||||
//
|
||||
Root as Empty,
|
||||
Header as EmptyHeader,
|
||||
Media as EmptyMedia,
|
||||
Title as EmptyTitle,
|
||||
Description as EmptyDescription,
|
||||
Content as EmptyContent,
|
||||
Root,
|
||||
Header,
|
||||
Media,
|
||||
Title,
|
||||
Description,
|
||||
Content,
|
||||
//
|
||||
Root as Empty,
|
||||
Header as EmptyHeader,
|
||||
Media as EmptyMedia,
|
||||
Title as EmptyTitle,
|
||||
Description as EmptyDescription,
|
||||
Content as EmptyContent
|
||||
};
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
import { LinkPreview as HoverCardPrimitive } from "bits-ui";
|
||||
import Content from "./hover-card-content.svelte";
|
||||
import Trigger from "./hover-card-trigger.svelte";
|
||||
import { LinkPreview as HoverCardPrimitive } from 'bits-ui';
|
||||
import Content from './hover-card-content.svelte';
|
||||
import Trigger from './hover-card-trigger.svelte';
|
||||
|
||||
const Root = HoverCardPrimitive.Root;
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Trigger,
|
||||
Root as HoverCard,
|
||||
Content as HoverCardContent,
|
||||
Trigger as HoverCardTrigger,
|
||||
};
|
||||
export { Root, Content, Trigger, Root as HoverCard, Content as HoverCardContent, Trigger as HoverCardTrigger };
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import Root from "./input-group.svelte";
|
||||
import Addon from "./input-group-addon.svelte";
|
||||
import Button from "./input-group-button.svelte";
|
||||
import Input from "./input-group-input.svelte";
|
||||
import Text from "./input-group-text.svelte";
|
||||
import Textarea from "./input-group-textarea.svelte";
|
||||
import Root from './input-group.svelte';
|
||||
import Addon from './input-group-addon.svelte';
|
||||
import Button from './input-group-button.svelte';
|
||||
import Input from './input-group-input.svelte';
|
||||
import Text from './input-group-text.svelte';
|
||||
import Textarea from './input-group-textarea.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
Addon,
|
||||
Button,
|
||||
Input,
|
||||
Text,
|
||||
Textarea,
|
||||
//
|
||||
Root as InputGroup,
|
||||
Addon as InputGroupAddon,
|
||||
Button as InputGroupButton,
|
||||
Input as InputGroupInput,
|
||||
Text as InputGroupText,
|
||||
Textarea as InputGroupTextarea,
|
||||
Root,
|
||||
Addon,
|
||||
Button,
|
||||
Input,
|
||||
Text,
|
||||
Textarea,
|
||||
//
|
||||
Root as InputGroup,
|
||||
Addon as InputGroupAddon,
|
||||
Button as InputGroupButton,
|
||||
Input as InputGroupInput,
|
||||
Text as InputGroupText,
|
||||
Textarea as InputGroupTextarea
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Root from "./input.svelte";
|
||||
import Root from './input.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Input,
|
||||
Root,
|
||||
//
|
||||
Root as Input
|
||||
};
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
import Root from "./item.svelte";
|
||||
import Group from "./item-group.svelte";
|
||||
import Separator from "./item-separator.svelte";
|
||||
import Header from "./item-header.svelte";
|
||||
import Footer from "./item-footer.svelte";
|
||||
import Content from "./item-content.svelte";
|
||||
import Title from "./item-title.svelte";
|
||||
import Description from "./item-description.svelte";
|
||||
import Actions from "./item-actions.svelte";
|
||||
import Media from "./item-media.svelte";
|
||||
import Root from './item.svelte';
|
||||
import Group from './item-group.svelte';
|
||||
import Separator from './item-separator.svelte';
|
||||
import Header from './item-header.svelte';
|
||||
import Footer from './item-footer.svelte';
|
||||
import Content from './item-content.svelte';
|
||||
import Title from './item-title.svelte';
|
||||
import Description from './item-description.svelte';
|
||||
import Actions from './item-actions.svelte';
|
||||
import Media from './item-media.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
Group,
|
||||
Separator,
|
||||
Header,
|
||||
Footer,
|
||||
Content,
|
||||
Title,
|
||||
Description,
|
||||
Actions,
|
||||
Media,
|
||||
//
|
||||
Root as Item,
|
||||
Group as ItemGroup,
|
||||
Separator as ItemSeparator,
|
||||
Header as ItemHeader,
|
||||
Footer as ItemFooter,
|
||||
Content as ItemContent,
|
||||
Title as ItemTitle,
|
||||
Description as ItemDescription,
|
||||
Actions as ItemActions,
|
||||
Media as ItemMedia,
|
||||
Root,
|
||||
Group,
|
||||
Separator,
|
||||
Header,
|
||||
Footer,
|
||||
Content,
|
||||
Title,
|
||||
Description,
|
||||
Actions,
|
||||
Media,
|
||||
//
|
||||
Root as Item,
|
||||
Group as ItemGroup,
|
||||
Separator as ItemSeparator,
|
||||
Header as ItemHeader,
|
||||
Footer as ItemFooter,
|
||||
Content as ItemContent,
|
||||
Title as ItemTitle,
|
||||
Description as ItemDescription,
|
||||
Actions as ItemActions,
|
||||
Media as ItemMedia
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import Root from "./kbd.svelte";
|
||||
import Group from "./kbd-group.svelte";
|
||||
import Root from './kbd.svelte';
|
||||
import Group from './kbd-group.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
Group,
|
||||
//
|
||||
Root as Kbd,
|
||||
Group as KbdGroup,
|
||||
Root,
|
||||
Group,
|
||||
//
|
||||
Root as Kbd,
|
||||
Group as KbdGroup
|
||||
};
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import Root from "./navigation-menu.svelte";
|
||||
import Content from "./navigation-menu-content.svelte";
|
||||
import Indicator from "./navigation-menu-indicator.svelte";
|
||||
import Item from "./navigation-menu-item.svelte";
|
||||
import Link from "./navigation-menu-link.svelte";
|
||||
import List from "./navigation-menu-list.svelte";
|
||||
import Trigger from "./navigation-menu-trigger.svelte";
|
||||
import Viewport from "./navigation-menu-viewport.svelte";
|
||||
import Root from './navigation-menu.svelte';
|
||||
import Content from './navigation-menu-content.svelte';
|
||||
import Indicator from './navigation-menu-indicator.svelte';
|
||||
import Item from './navigation-menu-item.svelte';
|
||||
import Link from './navigation-menu-link.svelte';
|
||||
import List from './navigation-menu-list.svelte';
|
||||
import Trigger from './navigation-menu-trigger.svelte';
|
||||
import Viewport from './navigation-menu-viewport.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Indicator,
|
||||
Item,
|
||||
Link,
|
||||
List,
|
||||
Trigger,
|
||||
Viewport,
|
||||
//
|
||||
Root as NavigationMenuRoot,
|
||||
Content as NavigationMenuContent,
|
||||
Indicator as NavigationMenuIndicator,
|
||||
Item as NavigationMenuItem,
|
||||
Link as NavigationMenuLink,
|
||||
List as NavigationMenuList,
|
||||
Trigger as NavigationMenuTrigger,
|
||||
Viewport as NavigationMenuViewport,
|
||||
Root,
|
||||
Content,
|
||||
Indicator,
|
||||
Item,
|
||||
Link,
|
||||
List,
|
||||
Trigger,
|
||||
Viewport,
|
||||
//
|
||||
Root as NavigationMenuRoot,
|
||||
Content as NavigationMenuContent,
|
||||
Indicator as NavigationMenuIndicator,
|
||||
Item as NavigationMenuItem,
|
||||
Link as NavigationMenuLink,
|
||||
List as NavigationMenuList,
|
||||
Trigger as NavigationMenuTrigger,
|
||||
Viewport as NavigationMenuViewport
|
||||
};
|
||||
|
||||
@@ -10,12 +10,6 @@
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="page-action"
|
||||
class={cn('ui:flex ui:items-center ui:gap-2', className)}
|
||||
{...restProps}
|
||||
>
|
||||
<div bind:this={ref} data-slot="page-action" class={cn('ui:flex ui:items-center ui:gap-2', className)} {...restProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -18,4 +18,3 @@
|
||||
>
|
||||
{@render children?.()}
|
||||
</p>
|
||||
|
||||
|
||||
@@ -5,153 +5,150 @@ import * as zxcvbnCommonPackage from '@zxcvbn-ts/language-common';
|
||||
import * as zxcvbnEnPackage from '@zxcvbn-ts/language-en';
|
||||
|
||||
const passwordOptions = {
|
||||
translations: zxcvbnEnPackage.translations,
|
||||
graphs: zxcvbnCommonPackage.adjacencyGraphs,
|
||||
dictionary: {
|
||||
...zxcvbnCommonPackage.dictionary,
|
||||
...zxcvbnEnPackage.dictionary
|
||||
}
|
||||
translations: zxcvbnEnPackage.translations,
|
||||
graphs: zxcvbnCommonPackage.adjacencyGraphs,
|
||||
dictionary: {
|
||||
...zxcvbnCommonPackage.dictionary,
|
||||
...zxcvbnEnPackage.dictionary
|
||||
}
|
||||
};
|
||||
|
||||
zxcvbnOptions.setOptions(passwordOptions);
|
||||
|
||||
type PasswordRootStateProps = WritableBoxedValues<{
|
||||
hidden: boolean;
|
||||
hidden: boolean;
|
||||
}> &
|
||||
ReadableBoxedValues<{
|
||||
minScore: number;
|
||||
}>;
|
||||
ReadableBoxedValues<{
|
||||
minScore: number;
|
||||
}>;
|
||||
|
||||
type PasswordState = {
|
||||
value: string;
|
||||
copyMounted: boolean;
|
||||
toggleMounted: boolean;
|
||||
strengthMounted: boolean;
|
||||
tainted: boolean;
|
||||
value: string;
|
||||
copyMounted: boolean;
|
||||
toggleMounted: boolean;
|
||||
strengthMounted: boolean;
|
||||
tainted: boolean;
|
||||
};
|
||||
|
||||
const defaultPasswordState: PasswordState = {
|
||||
value: '',
|
||||
copyMounted: false,
|
||||
toggleMounted: false,
|
||||
strengthMounted: false,
|
||||
tainted: false
|
||||
value: '',
|
||||
copyMounted: false,
|
||||
toggleMounted: false,
|
||||
strengthMounted: false,
|
||||
tainted: false
|
||||
};
|
||||
|
||||
class PasswordRootState {
|
||||
passwordState = $state(defaultPasswordState);
|
||||
passwordState = $state(defaultPasswordState);
|
||||
|
||||
constructor(readonly opts: PasswordRootStateProps) {}
|
||||
constructor(readonly opts: PasswordRootStateProps) {}
|
||||
|
||||
// only re-run when the password changes
|
||||
strength = $derived.by(() => zxcvbn(this.passwordState.value));
|
||||
// only re-run when the password changes
|
||||
strength = $derived.by(() => zxcvbn(this.passwordState.value));
|
||||
}
|
||||
|
||||
type PasswordInputStateProps = WritableBoxedValues<{
|
||||
value: string;
|
||||
value: string;
|
||||
}> &
|
||||
ReadableBoxedValues<{
|
||||
ref: HTMLInputElement | null;
|
||||
}>;
|
||||
ReadableBoxedValues<{
|
||||
ref: HTMLInputElement | null;
|
||||
}>;
|
||||
|
||||
class PasswordInputState {
|
||||
constructor(
|
||||
readonly root: PasswordRootState,
|
||||
readonly opts: PasswordInputStateProps
|
||||
) {
|
||||
watch(
|
||||
() => this.opts.value.current,
|
||||
() => {
|
||||
if (this.root.passwordState.value !== this.opts.value.current) {
|
||||
this.root.passwordState.tainted = true;
|
||||
this.root.passwordState.value = this.opts.value.current;
|
||||
}
|
||||
}
|
||||
);
|
||||
constructor(
|
||||
readonly root: PasswordRootState,
|
||||
readonly opts: PasswordInputStateProps
|
||||
) {
|
||||
watch(
|
||||
() => this.opts.value.current,
|
||||
() => {
|
||||
if (this.root.passwordState.value !== this.opts.value.current) {
|
||||
this.root.passwordState.tainted = true;
|
||||
this.root.passwordState.value = this.opts.value.current;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
if (!this.root.passwordState.strengthMounted) return;
|
||||
$effect(() => {
|
||||
if (!this.root.passwordState.strengthMounted) return;
|
||||
|
||||
// if the password is empty, we let the `required` attribute handle the validation
|
||||
if (
|
||||
this.root.passwordState.value !== '' &&
|
||||
this.root.strength.score < this.root.opts.minScore.current
|
||||
) {
|
||||
this.opts.ref.current?.setCustomValidity('Password is too weak');
|
||||
} else {
|
||||
this.opts.ref.current?.setCustomValidity('');
|
||||
}
|
||||
});
|
||||
}
|
||||
// if the password is empty, we let the `required` attribute handle the validation
|
||||
if (this.root.passwordState.value !== '' && this.root.strength.score < this.root.opts.minScore.current) {
|
||||
this.opts.ref.current?.setCustomValidity('Password is too weak');
|
||||
} else {
|
||||
this.opts.ref.current?.setCustomValidity('');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
props = $derived.by(() => ({
|
||||
'aria-invalid':
|
||||
this.root.strength.score < this.root.opts.minScore.current &&
|
||||
this.root.passwordState.tainted &&
|
||||
this.root.passwordState.strengthMounted
|
||||
}));
|
||||
props = $derived.by(() => ({
|
||||
'aria-invalid':
|
||||
this.root.strength.score < this.root.opts.minScore.current &&
|
||||
this.root.passwordState.tainted &&
|
||||
this.root.passwordState.strengthMounted
|
||||
}));
|
||||
}
|
||||
|
||||
class PasswordToggleVisibilityState {
|
||||
constructor(readonly root: PasswordRootState) {
|
||||
this.root.passwordState.toggleMounted = true;
|
||||
constructor(readonly root: PasswordRootState) {
|
||||
this.root.passwordState.toggleMounted = true;
|
||||
|
||||
// this way we go back to the correct padding when toggle is unmounted
|
||||
$effect(() => {
|
||||
return () => {
|
||||
this.root.passwordState.toggleMounted = false;
|
||||
};
|
||||
});
|
||||
}
|
||||
// this way we go back to the correct padding when toggle is unmounted
|
||||
$effect(() => {
|
||||
return () => {
|
||||
this.root.passwordState.toggleMounted = false;
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class PasswordCopyState {
|
||||
constructor(readonly root: PasswordRootState) {
|
||||
this.root.passwordState.copyMounted = true;
|
||||
constructor(readonly root: PasswordRootState) {
|
||||
this.root.passwordState.copyMounted = true;
|
||||
|
||||
// this way we go back to the correct padding when copy is unmounted
|
||||
$effect(() => {
|
||||
return () => {
|
||||
this.root.passwordState.copyMounted = false;
|
||||
};
|
||||
});
|
||||
}
|
||||
// this way we go back to the correct padding when copy is unmounted
|
||||
$effect(() => {
|
||||
return () => {
|
||||
this.root.passwordState.copyMounted = false;
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class PasswordStrengthState {
|
||||
constructor(readonly root: PasswordRootState) {
|
||||
this.root.passwordState.strengthMounted = true;
|
||||
constructor(readonly root: PasswordRootState) {
|
||||
this.root.passwordState.strengthMounted = true;
|
||||
|
||||
$effect(() => {
|
||||
return () => {
|
||||
this.root.passwordState.strengthMounted = false;
|
||||
};
|
||||
});
|
||||
}
|
||||
$effect(() => {
|
||||
return () => {
|
||||
this.root.passwordState.strengthMounted = false;
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
get strength() {
|
||||
return this.root.strength;
|
||||
}
|
||||
get strength() {
|
||||
return this.root.strength;
|
||||
}
|
||||
}
|
||||
|
||||
const ctx = new Context<PasswordRootState>('password-root-state');
|
||||
|
||||
export function usePassword(props: PasswordRootStateProps) {
|
||||
return ctx.set(new PasswordRootState(props));
|
||||
return ctx.set(new PasswordRootState(props));
|
||||
}
|
||||
|
||||
export function usePasswordInput(props: PasswordInputStateProps) {
|
||||
return new PasswordInputState(ctx.get(), props);
|
||||
return new PasswordInputState(ctx.get(), props);
|
||||
}
|
||||
|
||||
export function usePasswordToggleVisibility() {
|
||||
return new PasswordToggleVisibilityState(ctx.get());
|
||||
return new PasswordToggleVisibilityState(ctx.get());
|
||||
}
|
||||
|
||||
export function usePasswordCopy() {
|
||||
return new PasswordCopyState(ctx.get());
|
||||
return new PasswordCopyState(ctx.get());
|
||||
}
|
||||
|
||||
export function usePasswordStrength() {
|
||||
return new PasswordStrengthState(ctx.get());
|
||||
return new PasswordStrengthState(ctx.get());
|
||||
}
|
||||
|
||||
@@ -1,47 +1,40 @@
|
||||
import type {
|
||||
WithChildren,
|
||||
WithoutChildren,
|
||||
Meter as MeterPrimitive,
|
||||
Toggle as TogglePrimitive
|
||||
} from 'bits-ui';
|
||||
import type { WithChildren, WithoutChildren, Meter as MeterPrimitive, Toggle as TogglePrimitive } from 'bits-ui';
|
||||
import type { HTMLAttributes, HTMLInputAttributes } from 'svelte/elements';
|
||||
import type { CopyButtonProps } from '../copy-button/types';
|
||||
import type { ZxcvbnResult } from '@zxcvbn-ts/core';
|
||||
|
||||
export type PasswordRootPropsWithoutHTML = WithChildren<{
|
||||
ref?: HTMLDivElement | null;
|
||||
hidden?: boolean;
|
||||
/** The minimum acceptable score for a password. (0-4)
|
||||
*
|
||||
* @default 3
|
||||
*/
|
||||
minScore?: 0 | 1 | 2 | 3 | 4;
|
||||
ref?: HTMLDivElement | null;
|
||||
hidden?: boolean;
|
||||
/** The minimum acceptable score for a password. (0-4)
|
||||
*
|
||||
* @default 3
|
||||
*/
|
||||
minScore?: 0 | 1 | 2 | 3 | 4;
|
||||
}>;
|
||||
|
||||
export type PasswordRootProps = WithoutChildren<HTMLAttributes<HTMLDivElement>> &
|
||||
PasswordRootPropsWithoutHTML;
|
||||
export type PasswordRootProps = WithoutChildren<HTMLAttributes<HTMLDivElement>> & PasswordRootPropsWithoutHTML;
|
||||
|
||||
export type PasswordInputPropsWithoutHTML = WithChildren<{
|
||||
ref?: HTMLInputElement | null;
|
||||
value?: string;
|
||||
ref?: HTMLInputElement | null;
|
||||
value?: string;
|
||||
}>;
|
||||
|
||||
export type PasswordInputProps = Omit<
|
||||
WithoutChildren<HTMLInputAttributes>,
|
||||
'type' | 'files' | 'aria-invalid' | 'value'
|
||||
WithoutChildren<HTMLInputAttributes>,
|
||||
'type' | 'files' | 'aria-invalid' | 'value'
|
||||
> &
|
||||
PasswordInputPropsWithoutHTML;
|
||||
PasswordInputPropsWithoutHTML;
|
||||
|
||||
export type PasswordToggleVisibilityProps = Omit<
|
||||
TogglePrimitive.RootProps,
|
||||
'children' | 'pressed' | 'aria-label' | 'tabindex'
|
||||
TogglePrimitive.RootProps,
|
||||
'children' | 'pressed' | 'aria-label' | 'tabindex'
|
||||
>;
|
||||
|
||||
export type PasswordCopyButtonProps = Omit<CopyButtonProps, 'children' | 'text'>;
|
||||
|
||||
export type PasswordStrengthPropsWithoutHTML = {
|
||||
strength?: ZxcvbnResult;
|
||||
strength?: ZxcvbnResult;
|
||||
};
|
||||
|
||||
export type PasswordStrengthProps = PasswordStrengthPropsWithoutHTML &
|
||||
WithoutChildren<MeterPrimitive.RootProps>;
|
||||
export type PasswordStrengthProps = PasswordStrengthPropsWithoutHTML & WithoutChildren<MeterPrimitive.RootProps>;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Root from "./separator.svelte";
|
||||
import Root from './separator.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Separator,
|
||||
Root,
|
||||
//
|
||||
Root as Separator
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Root from "./textarea.svelte";
|
||||
import Root from './textarea.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Textarea,
|
||||
Root,
|
||||
//
|
||||
Root as Textarea
|
||||
};
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import Root from './toggle.svelte';
|
||||
export {
|
||||
toggleVariants,
|
||||
type ToggleSize,
|
||||
type ToggleVariant,
|
||||
type ToggleVariants
|
||||
} from './toggle.svelte';
|
||||
export { toggleVariants, type ToggleSize, type ToggleVariant, type ToggleVariants } from './toggle.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Toggle
|
||||
Root,
|
||||
//
|
||||
Root as Toggle
|
||||
};
|
||||
|
||||
@@ -2,4 +2,3 @@ import DomainInput from './domain-input.svelte';
|
||||
|
||||
export { DomainInput };
|
||||
export { DomainInput as default };
|
||||
|
||||
|
||||
@@ -8,12 +8,7 @@
|
||||
tooltipSide?: 'top' | 'bottom' | 'left' | 'right';
|
||||
}
|
||||
|
||||
let {
|
||||
tooltip,
|
||||
tooltipSide = 'top',
|
||||
children,
|
||||
...buttonProps
|
||||
}: Props = $props();
|
||||
let { tooltip, tooltipSide = 'top', children, ...buttonProps }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if tooltip}
|
||||
@@ -34,4 +29,3 @@
|
||||
{@render children?.()}
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -2,4 +2,3 @@ import IconButton from './icon-button.svelte';
|
||||
|
||||
export { IconButton };
|
||||
export { IconButton as default };
|
||||
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
import type { CropArea } from 'svelte-easy-crop';
|
||||
|
||||
export const getFileFromUrl = async (url: string, fileName = 'cropped.png'): Promise<File> => {
|
||||
// Fetch the file data from the URL
|
||||
const response = await fetch(url);
|
||||
// Fetch the file data from the URL
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch resource: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch resource: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
// Convert the response into a Blob
|
||||
const blob = await response.blob();
|
||||
// Convert the response into a Blob
|
||||
const blob = await response.blob();
|
||||
|
||||
// Create and return a File. You can set a custom type if needed.
|
||||
return new File([blob], fileName, { type: blob.type });
|
||||
// Create and return a File. You can set a custom type if needed.
|
||||
return new File([blob], fileName, { type: blob.type });
|
||||
};
|
||||
|
||||
const createImage = (url: string): Promise<HTMLImageElement> => {
|
||||
return new Promise<HTMLImageElement>((resolve, reject) => {
|
||||
const image = new Image();
|
||||
image.addEventListener('load', () => resolve(image));
|
||||
image.addEventListener('error', (error) => reject(error));
|
||||
image.setAttribute('crossOrigin', 'anonymous'); // needed to avoid cross-origin issues on CodeSandbox
|
||||
image.src = url;
|
||||
});
|
||||
return new Promise<HTMLImageElement>((resolve, reject) => {
|
||||
const image = new Image();
|
||||
image.addEventListener('load', () => resolve(image));
|
||||
image.addEventListener('error', (error) => reject(error));
|
||||
image.setAttribute('crossOrigin', 'anonymous'); // needed to avoid cross-origin issues on CodeSandbox
|
||||
image.src = url;
|
||||
});
|
||||
};
|
||||
|
||||
const getRadianAngle = (degreeValue: number) => {
|
||||
return (degreeValue * Math.PI) / 180;
|
||||
return (degreeValue * Math.PI) / 180;
|
||||
};
|
||||
|
||||
/** Gets the cropped image from the src using the cropped area
|
||||
@@ -36,50 +36,46 @@ const getRadianAngle = (degreeValue: number) => {
|
||||
* @param rotation
|
||||
* @returns
|
||||
*/
|
||||
export const getCroppedImg = async (
|
||||
imageSrc: string,
|
||||
pixelCrop: CropArea,
|
||||
rotation = 0
|
||||
): Promise<string> => {
|
||||
const image = await createImage(imageSrc);
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
export const getCroppedImg = async (imageSrc: string, pixelCrop: CropArea, rotation = 0): Promise<string> => {
|
||||
const image = await createImage(imageSrc);
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
if (!ctx) {
|
||||
throw new Error('Error getting 2d rendering context');
|
||||
}
|
||||
if (!ctx) {
|
||||
throw new Error('Error getting 2d rendering context');
|
||||
}
|
||||
|
||||
const maxSize = Math.max(image.width, image.height);
|
||||
const safeArea = 2 * ((maxSize / 2) * Math.sqrt(2));
|
||||
const maxSize = Math.max(image.width, image.height);
|
||||
const safeArea = 2 * ((maxSize / 2) * Math.sqrt(2));
|
||||
|
||||
// set each dimensions to double largest dimension to allow for a safe area for the
|
||||
// image to rotate in without being clipped by canvas context
|
||||
canvas.width = safeArea;
|
||||
canvas.height = safeArea;
|
||||
// set each dimensions to double largest dimension to allow for a safe area for the
|
||||
// image to rotate in without being clipped by canvas context
|
||||
canvas.width = safeArea;
|
||||
canvas.height = safeArea;
|
||||
|
||||
// translate canvas context to a central location on image to allow rotating around the center.
|
||||
ctx.translate(safeArea / 2, safeArea / 2);
|
||||
ctx.rotate(getRadianAngle(rotation));
|
||||
ctx.translate(-safeArea / 2, -safeArea / 2);
|
||||
// translate canvas context to a central location on image to allow rotating around the center.
|
||||
ctx.translate(safeArea / 2, safeArea / 2);
|
||||
ctx.rotate(getRadianAngle(rotation));
|
||||
ctx.translate(-safeArea / 2, -safeArea / 2);
|
||||
|
||||
// draw rotated image and store data.
|
||||
ctx.drawImage(image, safeArea / 2 - image.width * 0.5, safeArea / 2 - image.height * 0.5);
|
||||
const data = ctx.getImageData(0, 0, safeArea, safeArea);
|
||||
// draw rotated image and store data.
|
||||
ctx.drawImage(image, safeArea / 2 - image.width * 0.5, safeArea / 2 - image.height * 0.5);
|
||||
const data = ctx.getImageData(0, 0, safeArea, safeArea);
|
||||
|
||||
// set canvas width to final desired crop size - this will clear existing context
|
||||
canvas.width = pixelCrop.width;
|
||||
canvas.height = pixelCrop.height;
|
||||
// set canvas width to final desired crop size - this will clear existing context
|
||||
canvas.width = pixelCrop.width;
|
||||
canvas.height = pixelCrop.height;
|
||||
|
||||
// paste generated rotate image with correct offsets for x,y crop values.
|
||||
ctx.putImageData(
|
||||
data,
|
||||
Math.round(0 - safeArea / 2 + image.width * 0.5 - pixelCrop.x),
|
||||
Math.round(0 - safeArea / 2 + image.height * 0.5 - pixelCrop.y)
|
||||
);
|
||||
// paste generated rotate image with correct offsets for x,y crop values.
|
||||
ctx.putImageData(
|
||||
data,
|
||||
Math.round(0 - safeArea / 2 + image.width * 0.5 - pixelCrop.x),
|
||||
Math.round(0 - safeArea / 2 + image.height * 0.5 - pixelCrop.y)
|
||||
);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
canvas.toBlob((file) => {
|
||||
resolve(URL.createObjectURL(file!));
|
||||
}, 'image/png');
|
||||
});
|
||||
return new Promise((resolve) => {
|
||||
canvas.toBlob((file) => {
|
||||
resolve(URL.createObjectURL(file!));
|
||||
}, 'image/png');
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2,4 +2,3 @@ import InputField from './input-field.svelte';
|
||||
|
||||
export { InputField };
|
||||
export { InputField as default };
|
||||
|
||||
|
||||
@@ -2,4 +2,3 @@ import Password from './password.svelte';
|
||||
|
||||
export { Password };
|
||||
export { Password as default };
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
bind:value
|
||||
{disabled}
|
||||
aria-invalid={ariaInvalid}
|
||||
autocomplete={autocomplete}
|
||||
{autocomplete}
|
||||
{...restProps}
|
||||
/>
|
||||
<InputGroup.Addon align="inline-end">
|
||||
@@ -77,4 +77,3 @@
|
||||
</Tooltip.Provider>
|
||||
</InputGroup.Addon>
|
||||
</InputGroup.Root>
|
||||
|
||||
|
||||
@@ -2,4 +2,3 @@ import Search from './search.svelte';
|
||||
|
||||
export { Search };
|
||||
export { Search as default };
|
||||
|
||||
|
||||
@@ -4,13 +4,13 @@ import List from './underline-tabs-list.svelte';
|
||||
import Trigger from './underline-tabs-trigger.svelte';
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
List,
|
||||
Trigger,
|
||||
//
|
||||
Root as UnderlineTabs,
|
||||
Content as UnderlineTabsContent,
|
||||
List as UnderlineTabsList,
|
||||
Trigger as UnderlineTabsTrigger
|
||||
Root,
|
||||
Content,
|
||||
List,
|
||||
Trigger,
|
||||
//
|
||||
Root as UnderlineTabs,
|
||||
Content as UnderlineTabsContent,
|
||||
List as UnderlineTabsList,
|
||||
Trigger as UnderlineTabsTrigger
|
||||
};
|
||||
|
||||
@@ -4,85 +4,85 @@ import type { Tabs as TabsPrimitive } from 'bits-ui';
|
||||
import { crossfade } from 'svelte/transition';
|
||||
|
||||
export const [send, receive] = crossfade({
|
||||
duration: (d) => Math.sqrt(d * 200)
|
||||
duration: (d) => Math.sqrt(d * 200)
|
||||
});
|
||||
|
||||
type UnderlineTabsRootProps = WritableBoxedValues<{
|
||||
value: string;
|
||||
value: string;
|
||||
}> &
|
||||
ReadableBoxedValues<{
|
||||
id: string;
|
||||
}>;
|
||||
ReadableBoxedValues<{
|
||||
id: string;
|
||||
}>;
|
||||
|
||||
class UnderlineTabsRootState {
|
||||
hoveredTab = $state<string | null>(null);
|
||||
isHovered = $state(false);
|
||||
constructor(readonly opts: UnderlineTabsRootProps) {
|
||||
this.hoveredTab = this.opts.value.current;
|
||||
}
|
||||
hoveredTab = $state<string | null>(null);
|
||||
isHovered = $state(false);
|
||||
constructor(readonly opts: UnderlineTabsRootProps) {
|
||||
this.hoveredTab = this.opts.value.current;
|
||||
}
|
||||
}
|
||||
|
||||
type UnderlineTabsTriggerProps = ReadableBoxedValues<{
|
||||
value: string;
|
||||
onmouseenter: TabsPrimitive.TriggerProps['onmouseenter'];
|
||||
onmouseleave: TabsPrimitive.TriggerProps['onmouseleave'];
|
||||
onfocus: TabsPrimitive.TriggerProps['onfocus'];
|
||||
onblur: TabsPrimitive.TriggerProps['onblur'];
|
||||
value: string;
|
||||
onmouseenter: TabsPrimitive.TriggerProps['onmouseenter'];
|
||||
onmouseleave: TabsPrimitive.TriggerProps['onmouseleave'];
|
||||
onfocus: TabsPrimitive.TriggerProps['onfocus'];
|
||||
onblur: TabsPrimitive.TriggerProps['onblur'];
|
||||
}>;
|
||||
|
||||
class UnderlineTabsTriggerState {
|
||||
constructor(
|
||||
readonly opts: UnderlineTabsTriggerProps,
|
||||
readonly rootState: UnderlineTabsRootState
|
||||
) {}
|
||||
constructor(
|
||||
readonly opts: UnderlineTabsTriggerProps,
|
||||
readonly rootState: UnderlineTabsRootState
|
||||
) {}
|
||||
|
||||
handleFocus() {
|
||||
this.rootState.isHovered = true;
|
||||
this.rootState.hoveredTab = this.opts.value.current;
|
||||
}
|
||||
handleFocus() {
|
||||
this.rootState.isHovered = true;
|
||||
this.rootState.hoveredTab = this.opts.value.current;
|
||||
}
|
||||
|
||||
handleBlur() {
|
||||
if (this.rootState.hoveredTab === this.opts.value.current) {
|
||||
this.rootState.isHovered = false;
|
||||
}
|
||||
}
|
||||
handleBlur() {
|
||||
if (this.rootState.hoveredTab === this.opts.value.current) {
|
||||
this.rootState.isHovered = false;
|
||||
}
|
||||
}
|
||||
|
||||
onmouseenter(e: Parameters<NonNullable<TabsPrimitive.TriggerProps['onmouseenter']>>[0]) {
|
||||
this.handleFocus();
|
||||
this.opts.onmouseenter.current?.(e);
|
||||
}
|
||||
onmouseenter(e: Parameters<NonNullable<TabsPrimitive.TriggerProps['onmouseenter']>>[0]) {
|
||||
this.handleFocus();
|
||||
this.opts.onmouseenter.current?.(e);
|
||||
}
|
||||
|
||||
onmouseleave(e: Parameters<NonNullable<TabsPrimitive.TriggerProps['onmouseleave']>>[0]) {
|
||||
this.handleBlur();
|
||||
this.opts.onmouseleave.current?.(e);
|
||||
}
|
||||
onmouseleave(e: Parameters<NonNullable<TabsPrimitive.TriggerProps['onmouseleave']>>[0]) {
|
||||
this.handleBlur();
|
||||
this.opts.onmouseleave.current?.(e);
|
||||
}
|
||||
|
||||
onfocus(e: Parameters<NonNullable<TabsPrimitive.TriggerProps['onfocus']>>[0]) {
|
||||
this.handleFocus();
|
||||
this.opts.onfocus.current?.(e);
|
||||
}
|
||||
onfocus(e: Parameters<NonNullable<TabsPrimitive.TriggerProps['onfocus']>>[0]) {
|
||||
this.handleFocus();
|
||||
this.opts.onfocus.current?.(e);
|
||||
}
|
||||
|
||||
onblur(e: Parameters<NonNullable<TabsPrimitive.TriggerProps['onblur']>>[0]) {
|
||||
this.handleBlur();
|
||||
onblur(e: Parameters<NonNullable<TabsPrimitive.TriggerProps['onblur']>>[0]) {
|
||||
this.handleBlur();
|
||||
|
||||
this.opts.onblur.current?.(e);
|
||||
}
|
||||
this.opts.onblur.current?.(e);
|
||||
}
|
||||
|
||||
props = $derived.by(() => ({
|
||||
value: this.opts.value.current,
|
||||
onmouseenter: this.onmouseenter.bind(this),
|
||||
onmouseleave: this.onmouseleave.bind(this),
|
||||
onfocus: this.onfocus.bind(this),
|
||||
onblur: this.onblur.bind(this)
|
||||
}));
|
||||
props = $derived.by(() => ({
|
||||
value: this.opts.value.current,
|
||||
onmouseenter: this.onmouseenter.bind(this),
|
||||
onmouseleave: this.onmouseleave.bind(this),
|
||||
onfocus: this.onfocus.bind(this),
|
||||
onblur: this.onblur.bind(this)
|
||||
}));
|
||||
}
|
||||
|
||||
const ctx = new Context<UnderlineTabsRootState>('underline-tabs-root-ctx');
|
||||
|
||||
export function useUnderlineTabs(opts: UnderlineTabsRootProps) {
|
||||
return ctx.set(new UnderlineTabsRootState(opts));
|
||||
return ctx.set(new UnderlineTabsRootState(opts));
|
||||
}
|
||||
|
||||
export function useUnderlineTabsTrigger(opts: UnderlineTabsTriggerProps) {
|
||||
return new UnderlineTabsTriggerState(opts, ctx.get());
|
||||
return new UnderlineTabsTriggerState(opts, ctx.get());
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
type Options = {
|
||||
/** The time before the copied status is reset. */
|
||||
delay: number;
|
||||
/** The time before the copied status is reset. */
|
||||
delay: number;
|
||||
};
|
||||
|
||||
/** Use this hook to copy text to the clipboard and show a copied state.
|
||||
@@ -26,58 +26,58 @@ type Options = {
|
||||
*
|
||||
*/
|
||||
export class UseClipboard {
|
||||
#copiedStatus = $state<'success' | 'failure'>();
|
||||
private delay: number;
|
||||
private timeout: ReturnType<typeof setTimeout> | undefined = undefined;
|
||||
#copiedStatus = $state<'success' | 'failure'>();
|
||||
private delay: number;
|
||||
private timeout: ReturnType<typeof setTimeout> | undefined = undefined;
|
||||
|
||||
constructor({ delay = 500 }: Partial<Options> = {}) {
|
||||
this.delay = delay;
|
||||
}
|
||||
constructor({ delay = 500 }: Partial<Options> = {}) {
|
||||
this.delay = delay;
|
||||
}
|
||||
|
||||
/** Copies the given text to the users clipboard.
|
||||
*
|
||||
* ## Usage
|
||||
* ```ts
|
||||
* clipboard.copy('Hello, World!');
|
||||
* ```
|
||||
*
|
||||
* @param text
|
||||
* @returns
|
||||
*/
|
||||
async copy(text: string) {
|
||||
if (this.timeout) {
|
||||
this.#copiedStatus = undefined;
|
||||
clearTimeout(this.timeout);
|
||||
}
|
||||
/** Copies the given text to the users clipboard.
|
||||
*
|
||||
* ## Usage
|
||||
* ```ts
|
||||
* clipboard.copy('Hello, World!');
|
||||
* ```
|
||||
*
|
||||
* @param text
|
||||
* @returns
|
||||
*/
|
||||
async copy(text: string) {
|
||||
if (this.timeout) {
|
||||
this.#copiedStatus = undefined;
|
||||
clearTimeout(this.timeout);
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
|
||||
this.#copiedStatus = 'success';
|
||||
this.#copiedStatus = 'success';
|
||||
|
||||
this.timeout = setTimeout(() => {
|
||||
this.#copiedStatus = undefined;
|
||||
}, this.delay);
|
||||
} catch {
|
||||
// an error can occur when not in the browser or if the user hasn't given clipboard access
|
||||
this.#copiedStatus = 'failure';
|
||||
this.timeout = setTimeout(() => {
|
||||
this.#copiedStatus = undefined;
|
||||
}, this.delay);
|
||||
} catch {
|
||||
// an error can occur when not in the browser or if the user hasn't given clipboard access
|
||||
this.#copiedStatus = 'failure';
|
||||
|
||||
this.timeout = setTimeout(() => {
|
||||
this.#copiedStatus = undefined;
|
||||
}, this.delay);
|
||||
}
|
||||
this.timeout = setTimeout(() => {
|
||||
this.#copiedStatus = undefined;
|
||||
}, this.delay);
|
||||
}
|
||||
|
||||
return this.#copiedStatus;
|
||||
}
|
||||
return this.#copiedStatus;
|
||||
}
|
||||
|
||||
/** true when the user has just copied to the clipboard. */
|
||||
get copied() {
|
||||
return this.#copiedStatus === 'success';
|
||||
}
|
||||
/** true when the user has just copied to the clipboard. */
|
||||
get copied() {
|
||||
return this.#copiedStatus === 'success';
|
||||
}
|
||||
|
||||
/** Indicates whether a copy has occurred
|
||||
* and gives a status of either `success` or `failure`. */
|
||||
get status() {
|
||||
return this.#copiedStatus;
|
||||
}
|
||||
/** Indicates whether a copy has occurred
|
||||
* and gives a status of either `success` or `failure`. */
|
||||
get status() {
|
||||
return this.#copiedStatus;
|
||||
}
|
||||
}
|
||||
|
||||
+3807
-1
File diff suppressed because one or more lines are too long
@@ -44,7 +44,8 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch"
|
||||
"dev": "tsc --watch",
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^4.1.8"
|
||||
|
||||
@@ -2,13 +2,7 @@
|
||||
* Server-side file type validation utilities for preventing malicious uploads
|
||||
* Specifically designed to prevent SVG XSS attacks and other security issues
|
||||
*/
|
||||
export const ALLOWED_IMAGE_TYPES = [
|
||||
'image/jpeg',
|
||||
'image/jpg',
|
||||
'image/png',
|
||||
'image/gif',
|
||||
'image/webp'
|
||||
] as const;
|
||||
export const ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'] as const;
|
||||
|
||||
export const ALLOWED_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp'] as const;
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from './fileValidation';
|
||||
export * from './fileValidation';
|
||||
|
||||
@@ -40,11 +40,10 @@ Schemas are used **without custom error messages** - just validation rules:
|
||||
import { ZCourseClone } from '@cio/utils/validation';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
|
||||
export const cloneRouter = new Hono()
|
||||
.post('/', zValidator('json', ZCourseClone), async (c) => {
|
||||
const data = c.req.valid('json'); // Fully typed!
|
||||
// ... handle validated data
|
||||
});
|
||||
export const cloneRouter = new Hono().post('/', zValidator('json', ZCourseClone), async (c) => {
|
||||
const data = c.req.valid('json'); // Fully typed!
|
||||
// ... handle validated data
|
||||
});
|
||||
```
|
||||
|
||||
### Frontend Usage (Dashboard)
|
||||
@@ -60,13 +59,13 @@ let errors: Record<string, string> = {};
|
||||
|
||||
function handleSubmit() {
|
||||
const result = ZEmailData.safeParse(formData);
|
||||
|
||||
|
||||
if (!result.success) {
|
||||
// Automatically maps to translations using convention
|
||||
errors = mapZodErrorsToTranslations(result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Use validated data
|
||||
doSomething(result.data);
|
||||
}
|
||||
@@ -83,9 +82,9 @@ Schemas are clean with **no custom error messages**:
|
||||
```typescript
|
||||
// ✅ Clean schema (in @cio/utils)
|
||||
export const ZEmailData = z.object({
|
||||
to: z.string().email(), // No message
|
||||
subject: z.string().min(1), // No message
|
||||
content: z.string().min(10) // No message
|
||||
to: z.string().email(), // No message
|
||||
subject: z.string().min(1), // No message
|
||||
content: z.string().min(10) // No message
|
||||
});
|
||||
```
|
||||
|
||||
@@ -97,12 +96,12 @@ validations.{fieldName}.{errorCode}
|
||||
|
||||
### Example Translation Keys
|
||||
|
||||
| Field | Validation | Error Code | Translation Key |
|
||||
|-------|-----------|------------|-----------------|
|
||||
| `to` | `.email()` | `invalid_string` | `validations.to.invalid_email` |
|
||||
| `subject` | `.min(1)` | `too_small` | `validations.subject.too_small` |
|
||||
| `username` | `.min(3)` | `too_small` | `validations.username.too_small` |
|
||||
| `username` | `.max(20)` | `too_big` | `validations.username.too_big` |
|
||||
| Field | Validation | Error Code | Translation Key |
|
||||
| ---------- | ---------- | ---------------- | -------------------------------- |
|
||||
| `to` | `.email()` | `invalid_string` | `validations.to.invalid_email` |
|
||||
| `subject` | `.min(1)` | `too_small` | `validations.subject.too_small` |
|
||||
| `username` | `.min(3)` | `too_small` | `validations.username.too_small` |
|
||||
| `username` | `.max(20)` | `too_big` | `validations.username.too_big` |
|
||||
|
||||
### Translation File Example
|
||||
|
||||
@@ -139,14 +138,14 @@ validations.{fieldName}.{errorCode}
|
||||
|
||||
### Common Error Codes
|
||||
|
||||
| Zod Error | When It Occurs |
|
||||
|-----------|----------------|
|
||||
| `too_small` | String too short, number too small, array too few items |
|
||||
| `too_big` | String too long, number too large, array too many items |
|
||||
| `invalid_email` | Invalid email format (from `.email()`) |
|
||||
| `invalid_url` | Invalid URL format (from `.url()`) |
|
||||
| `invalid_type` | Wrong type (e.g., undefined when string expected) |
|
||||
| `invalid_enum_value` | Value not in allowed options |
|
||||
| Zod Error | When It Occurs |
|
||||
| -------------------- | ------------------------------------------------------- |
|
||||
| `too_small` | String too short, number too small, array too few items |
|
||||
| `too_big` | String too long, number too large, array too many items |
|
||||
| `invalid_email` | Invalid email format (from `.email()`) |
|
||||
| `invalid_url` | Invalid URL format (from `.url()`) |
|
||||
| `invalid_type` | Wrong type (e.g., undefined when string expected) |
|
||||
| `invalid_enum_value` | Value not in allowed options |
|
||||
|
||||
## Naming Convention
|
||||
|
||||
@@ -165,14 +164,14 @@ import * as z from 'zod';
|
||||
/**
|
||||
* Lesson Create Validation Schema
|
||||
* Used for creating new lessons
|
||||
*
|
||||
*
|
||||
* Translation keys:
|
||||
* - validations.title.too_small
|
||||
* - validations.duration.too_small
|
||||
*/
|
||||
export const ZLessonCreate = z.object({
|
||||
title: z.string().min(1), // Clean - no message
|
||||
duration: z.number().min(1) // Clean - no message
|
||||
title: z.string().min(1), // Clean - no message
|
||||
duration: z.number().min(1) // Clean - no message
|
||||
});
|
||||
export type TLessonCreate = z.infer<typeof ZLessonCreate>;
|
||||
|
||||
@@ -186,7 +185,7 @@ export * from './lesson';
|
||||
// packages/utils/src/validation/index.ts
|
||||
export * from './course';
|
||||
export * from './mail';
|
||||
export * from './lesson'; // Add this
|
||||
export * from './lesson'; // Add this
|
||||
```
|
||||
|
||||
### 3. Use in API
|
||||
@@ -196,11 +195,10 @@ export * from './lesson'; // Add this
|
||||
import { ZLessonCreate } from '@cio/utils/validation/lesson';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
|
||||
export const lessonRouter = new Hono()
|
||||
.post('/', zValidator('json', ZLessonCreate), async (c) => {
|
||||
const data = c.req.valid('json');
|
||||
// ... create lesson
|
||||
});
|
||||
export const lessonRouter = new Hono().post('/', zValidator('json', ZLessonCreate), async (c) => {
|
||||
const data = c.req.valid('json');
|
||||
// ... create lesson
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Add Translations in Frontend
|
||||
@@ -232,13 +230,13 @@ let errors = {};
|
||||
|
||||
function handleSubmit() {
|
||||
const result = ZLessonCreate.safeParse(formData);
|
||||
|
||||
|
||||
if (!result.success) {
|
||||
errors = mapZodErrorsToTranslations(result.error);
|
||||
// errors.title = "Title is required" (translated)
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
createLesson(result.data);
|
||||
}
|
||||
```
|
||||
@@ -258,8 +256,8 @@ export const ZUserProfile = z.object({
|
||||
|
||||
// ❌ Bad - Hardcoded messages
|
||||
export const ZUserProfile = z.object({
|
||||
email: z.string().email('Invalid email'), // Don't do this
|
||||
username: z.string().min(3, 'Too short') // Don't do this
|
||||
email: z.string().email('Invalid email'), // Don't do this
|
||||
username: z.string().min(3, 'Too short') // Don't do this
|
||||
});
|
||||
|
||||
// ❌ Bad - Translated messages in schema
|
||||
@@ -273,7 +271,9 @@ export const ZUserProfile = z.object({
|
||||
### 2. Always Export Types
|
||||
|
||||
```typescript
|
||||
export const ZCourseClone = z.object({ /* ... */ });
|
||||
export const ZCourseClone = z.object({
|
||||
/* ... */
|
||||
});
|
||||
export type TCourseClone = z.infer<typeof ZCourseClone>;
|
||||
```
|
||||
|
||||
@@ -285,7 +285,7 @@ Help frontend developers know which translation keys to add:
|
||||
/**
|
||||
* User Profile Validation Schema
|
||||
* Used for updating user profile information
|
||||
*
|
||||
*
|
||||
* Translation keys (convention-based):
|
||||
* - validations.email.invalid_email
|
||||
* - validations.username.too_small
|
||||
@@ -328,9 +328,15 @@ Keep related schemas together in the same entity folder:
|
||||
|
||||
```typescript
|
||||
// validation/user/user.ts
|
||||
export const ZUserCreate = z.object({ /* ... */ });
|
||||
export const ZUserUpdate = z.object({ /* ... */ });
|
||||
export const ZUserProfile = z.object({ /* ... */ });
|
||||
export const ZUserCreate = z.object({
|
||||
/* ... */
|
||||
});
|
||||
export const ZUserUpdate = z.object({
|
||||
/* ... */
|
||||
});
|
||||
export const ZUserProfile = z.object({
|
||||
/* ... */
|
||||
});
|
||||
```
|
||||
|
||||
### 6. Don't Duplicate Schemas
|
||||
@@ -341,7 +347,7 @@ export const ZUserProfile = z.object({ /* ... */ });
|
||||
// ❌ Bad - Reimplementing in frontend
|
||||
// apps/dashboard/src/schemas/user.ts
|
||||
export const userSchema = z.object({
|
||||
email: z.string().email(t.get('validations.email.invalid')),
|
||||
email: z.string().email(t.get('validations.email.invalid'))
|
||||
// ... duplicating from @cio/utils
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as z from 'zod';
|
||||
|
||||
export const ForgotPasswordSchema = z.object({
|
||||
email: z.email(),
|
||||
email: z.email()
|
||||
});
|
||||
|
||||
export type TForgotPassword = z.infer<typeof ForgotPasswordSchema>;
|
||||
export type TForgotPassword = z.infer<typeof ForgotPasswordSchema>;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as z from 'zod';
|
||||
|
||||
export const ResetPasswordSchema = z.object({
|
||||
password: z.string().min(8),
|
||||
confirmPassword: z.string().min(8),
|
||||
token: z.string(),
|
||||
password: z.string().min(8),
|
||||
confirmPassword: z.string().min(8),
|
||||
token: z.string()
|
||||
});
|
||||
|
||||
export type TResetPassword = z.infer<typeof ResetPasswordSchema>;
|
||||
|
||||
Reference in New Issue
Block a user