style: apply prettier formatting across frontend components and pages
Some checks failed
Build and Push Backend Image / build (push) Successful in 1m4s
Build and Push Frontend Image / build (push) Has been cancelled

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 10:49:35 +01:00
parent 9c5dba5c90
commit f2871b98db
35 changed files with 706 additions and 589 deletions

View File

@@ -320,6 +320,5 @@
{/if} {/if}
</div> </div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,50 +1,49 @@
<script lang="ts" module> <script lang="ts" module>
import { type VariantProps, tv } from "tailwind-variants"; import { type VariantProps, tv } from "tailwind-variants";
export const badgeVariants = tv({ export const badgeVariants = tv({
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3", base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3",
variants: { variants: {
variant: { variant: {
default: default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent",
"bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent", secondary:
secondary: "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent",
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent", destructive:
destructive: "bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white",
"bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white", outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", },
}, },
}, defaultVariants: {
defaultVariants: { variant: "default",
variant: "default", },
}, });
});
export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"]; export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
</script> </script>
<script lang="ts"> <script lang="ts">
import type { HTMLAnchorAttributes } from "svelte/elements"; import type { HTMLAnchorAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js"; import { cn, type WithElementRef } from "$lib/utils.js";
let { let {
ref = $bindable(null), ref = $bindable(null),
href, href,
class: className, class: className,
variant = "default", variant = "default",
children, children,
...restProps ...restProps
}: WithElementRef<HTMLAnchorAttributes> & { }: WithElementRef<HTMLAnchorAttributes> & {
variant?: BadgeVariant; variant?: BadgeVariant;
} = $props(); } = $props();
</script> </script>
<svelte:element <svelte:element
this={href ? "a" : "span"} this={href ? "a" : "span"}
bind:this={ref} bind:this={ref}
data-slot="badge" data-slot="badge"
{href} {href}
class={cn(badgeVariants({ variant }), className)} class={cn(badgeVariants({ variant }), className)}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
</svelte:element> </svelte:element>

View File

@@ -1,76 +1,76 @@
<script lang="ts"> <script lang="ts">
import type { ComponentProps } from "svelte"; import type { ComponentProps } from "svelte";
import type Calendar from "./calendar.svelte"; import type Calendar from "./calendar.svelte";
import CalendarMonthSelect from "./calendar-month-select.svelte"; import CalendarMonthSelect from "./calendar-month-select.svelte";
import CalendarYearSelect from "./calendar-year-select.svelte"; import CalendarYearSelect from "./calendar-year-select.svelte";
import { DateFormatter, getLocalTimeZone, type DateValue } from "@internationalized/date"; import { DateFormatter, getLocalTimeZone, type DateValue } from "@internationalized/date";
let { let {
captionLayout, captionLayout,
months, months,
monthFormat, monthFormat,
years, years,
yearFormat, yearFormat,
month, month,
locale, locale,
placeholder = $bindable(), placeholder = $bindable(),
monthIndex = 0, monthIndex = 0,
}: { }: {
captionLayout: ComponentProps<typeof Calendar>["captionLayout"]; captionLayout: ComponentProps<typeof Calendar>["captionLayout"];
months: ComponentProps<typeof CalendarMonthSelect>["months"]; months: ComponentProps<typeof CalendarMonthSelect>["months"];
monthFormat: ComponentProps<typeof CalendarMonthSelect>["monthFormat"]; monthFormat: ComponentProps<typeof CalendarMonthSelect>["monthFormat"];
years: ComponentProps<typeof CalendarYearSelect>["years"]; years: ComponentProps<typeof CalendarYearSelect>["years"];
yearFormat: ComponentProps<typeof CalendarYearSelect>["yearFormat"]; yearFormat: ComponentProps<typeof CalendarYearSelect>["yearFormat"];
month: DateValue; month: DateValue;
placeholder: DateValue | undefined; placeholder: DateValue | undefined;
locale: string; locale: string;
monthIndex: number; monthIndex: number;
} = $props(); } = $props();
function formatYear(date: DateValue) { function formatYear(date: DateValue) {
const dateObj = date.toDate(getLocalTimeZone()); const dateObj = date.toDate(getLocalTimeZone());
if (typeof yearFormat === "function") return yearFormat(dateObj.getFullYear()); if (typeof yearFormat === "function") return yearFormat(dateObj.getFullYear());
return new DateFormatter(locale, { year: yearFormat }).format(dateObj); return new DateFormatter(locale, { year: yearFormat }).format(dateObj);
} }
function formatMonth(date: DateValue) { function formatMonth(date: DateValue) {
const dateObj = date.toDate(getLocalTimeZone()); const dateObj = date.toDate(getLocalTimeZone());
if (typeof monthFormat === "function") return monthFormat(dateObj.getMonth() + 1); if (typeof monthFormat === "function") return monthFormat(dateObj.getMonth() + 1);
return new DateFormatter(locale, { month: monthFormat }).format(dateObj); return new DateFormatter(locale, { month: monthFormat }).format(dateObj);
} }
</script> </script>
{#snippet MonthSelect()} {#snippet MonthSelect()}
<CalendarMonthSelect <CalendarMonthSelect
{months} {months}
{monthFormat} {monthFormat}
value={month.month} value={month.month}
onchange={(e) => { onchange={(e) => {
if (!placeholder) return; if (!placeholder) return;
const v = Number.parseInt(e.currentTarget.value); const v = Number.parseInt(e.currentTarget.value);
const newPlaceholder = placeholder.set({ month: v }); const newPlaceholder = placeholder.set({ month: v });
placeholder = newPlaceholder.subtract({ months: monthIndex }); placeholder = newPlaceholder.subtract({ months: monthIndex });
}} }}
/> />
{/snippet} {/snippet}
{#snippet YearSelect()} {#snippet YearSelect()}
<CalendarYearSelect {years} {yearFormat} value={month.year} /> <CalendarYearSelect {years} {yearFormat} value={month.year} />
{/snippet} {/snippet}
{#if captionLayout === "dropdown"} {#if captionLayout === "dropdown"}
{@render MonthSelect()} {@render MonthSelect()}
{@render YearSelect()} {@render YearSelect()}
{:else if captionLayout === "dropdown-months"} {:else if captionLayout === "dropdown-months"}
{@render MonthSelect()} {@render MonthSelect()}
{#if placeholder} {#if placeholder}
{formatYear(placeholder)} {formatYear(placeholder)}
{/if} {/if}
{:else if captionLayout === "dropdown-years"} {:else if captionLayout === "dropdown-years"}
{#if placeholder} {#if placeholder}
{formatMonth(placeholder)} {formatMonth(placeholder)}
{/if} {/if}
{@render YearSelect()} {@render YearSelect()}
{:else} {:else}
{formatMonth(month)} {formatYear(month)} {formatMonth(month)} {formatYear(month)}
{/if} {/if}

View File

@@ -1,19 +1,19 @@
<script lang="ts"> <script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui"; import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils.js";
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
...restProps ...restProps
}: CalendarPrimitive.CellProps = $props(); }: CalendarPrimitive.CellProps = $props();
</script> </script>
<CalendarPrimitive.Cell <CalendarPrimitive.Cell
bind:ref bind:ref
class={cn( class={cn(
"relative size-(--cell-size) p-0 text-center text-sm focus-within:z-20 [&:first-child[data-selected]_[data-bits-day]]:rounded-s-md [&:last-child[data-selected]_[data-bits-day]]:rounded-e-md", "relative size-(--cell-size) p-0 text-center text-sm focus-within:z-20 [&:first-child[data-selected]_[data-bits-day]]:rounded-s-md [&:last-child[data-selected]_[data-bits-day]]:rounded-e-md",
className className,
)} )}
{...restProps} {...restProps}
/> />

View File

@@ -1,35 +1,35 @@
<script lang="ts"> <script lang="ts">
import { buttonVariants } from "$lib/components/ui/button/index.js"; import { buttonVariants } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils.js";
import { Calendar as CalendarPrimitive } from "bits-ui"; import { Calendar as CalendarPrimitive } from "bits-ui";
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
...restProps ...restProps
}: CalendarPrimitive.DayProps = $props(); }: CalendarPrimitive.DayProps = $props();
</script> </script>
<CalendarPrimitive.Day <CalendarPrimitive.Day
bind:ref bind:ref
class={cn( class={cn(
buttonVariants({ variant: "ghost" }), buttonVariants({ variant: "ghost" }),
"flex size-(--cell-size) flex-col items-center justify-center gap-1 p-0 leading-none font-normal whitespace-nowrap select-none", "flex size-(--cell-size) flex-col items-center justify-center gap-1 p-0 leading-none font-normal whitespace-nowrap select-none",
"[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground [&[data-today][data-disabled]]:text-muted-foreground", "[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground [&[data-today][data-disabled]]:text-muted-foreground",
"data-[selected]:bg-primary dark:data-[selected]:hover:bg-accent/50 data-[selected]:text-primary-foreground", "data-[selected]:bg-primary dark:data-[selected]:hover:bg-accent/50 data-[selected]:text-primary-foreground",
// Outside months // Outside months
"[&[data-outside-month]:not([data-selected])]:text-muted-foreground [&[data-outside-month]:not([data-selected])]:hover:text-accent-foreground", "[&[data-outside-month]:not([data-selected])]:text-muted-foreground [&[data-outside-month]:not([data-selected])]:hover:text-accent-foreground",
// Disabled // Disabled
"data-[disabled]:text-muted-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", "data-[disabled]:text-muted-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
// Unavailable // Unavailable
"data-[unavailable]:text-muted-foreground data-[unavailable]:line-through", "data-[unavailable]:text-muted-foreground data-[unavailable]:line-through",
// hover // hover
"dark:hover:text-accent-foreground", "dark:hover:text-accent-foreground",
// focus // focus
"focus:border-ring focus:ring-ring/50 focus:relative", "focus:border-ring focus:ring-ring/50 focus:relative",
// inner spans // inner spans
"[&>span]:text-xs [&>span]:opacity-70", "[&>span]:text-xs [&>span]:opacity-70",
className className,
)} )}
{...restProps} {...restProps}
/> />

View File

@@ -1,12 +1,12 @@
<script lang="ts"> <script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui"; import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils.js";
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
...restProps ...restProps
}: CalendarPrimitive.GridBodyProps = $props(); }: CalendarPrimitive.GridBodyProps = $props();
</script> </script>
<CalendarPrimitive.GridBody bind:ref class={cn(className)} {...restProps} /> <CalendarPrimitive.GridBody bind:ref class={cn(className)} {...restProps} />

View File

@@ -1,12 +1,12 @@
<script lang="ts"> <script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui"; import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils.js";
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
...restProps ...restProps
}: CalendarPrimitive.GridHeadProps = $props(); }: CalendarPrimitive.GridHeadProps = $props();
</script> </script>
<CalendarPrimitive.GridHead bind:ref class={cn(className)} {...restProps} /> <CalendarPrimitive.GridHead bind:ref class={cn(className)} {...restProps} />

View File

@@ -1,12 +1,12 @@
<script lang="ts"> <script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui"; import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils.js";
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
...restProps ...restProps
}: CalendarPrimitive.GridRowProps = $props(); }: CalendarPrimitive.GridRowProps = $props();
</script> </script>
<CalendarPrimitive.GridRow bind:ref class={cn("flex", className)} {...restProps} /> <CalendarPrimitive.GridRow bind:ref class={cn("flex", className)} {...restProps} />

View File

@@ -1,16 +1,16 @@
<script lang="ts"> <script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui"; import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils.js";
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
...restProps ...restProps
}: CalendarPrimitive.GridProps = $props(); }: CalendarPrimitive.GridProps = $props();
</script> </script>
<CalendarPrimitive.Grid <CalendarPrimitive.Grid
bind:ref bind:ref
class={cn("mt-4 flex w-full border-collapse flex-col gap-1", className)} class={cn("mt-4 flex w-full border-collapse flex-col gap-1", className)}
{...restProps} {...restProps}
/> />

View File

@@ -1,19 +1,19 @@
<script lang="ts"> <script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui"; import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils.js";
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
...restProps ...restProps
}: CalendarPrimitive.HeadCellProps = $props(); }: CalendarPrimitive.HeadCellProps = $props();
</script> </script>
<CalendarPrimitive.HeadCell <CalendarPrimitive.HeadCell
bind:ref bind:ref
class={cn( class={cn(
"text-muted-foreground w-(--cell-size) rounded-md text-[0.8rem] font-normal", "text-muted-foreground w-(--cell-size) rounded-md text-[0.8rem] font-normal",
className className,
)} )}
{...restProps} {...restProps}
/> />

View File

@@ -1,19 +1,19 @@
<script lang="ts"> <script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui"; import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils.js";
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
...restProps ...restProps
}: CalendarPrimitive.HeaderProps = $props(); }: CalendarPrimitive.HeaderProps = $props();
</script> </script>
<CalendarPrimitive.Header <CalendarPrimitive.Header
bind:ref bind:ref
class={cn( class={cn(
"flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium", "flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium",
className className,
)} )}
{...restProps} {...restProps}
/> />

View File

@@ -1,16 +1,16 @@
<script lang="ts"> <script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui"; import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils.js";
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
...restProps ...restProps
}: CalendarPrimitive.HeadingProps = $props(); }: CalendarPrimitive.HeadingProps = $props();
</script> </script>
<CalendarPrimitive.Heading <CalendarPrimitive.Heading
bind:ref bind:ref
class={cn("px-(--cell-size) text-sm font-medium", className)} class={cn("px-(--cell-size) text-sm font-medium", className)}
{...restProps} {...restProps}
/> />

View File

@@ -1,48 +1,48 @@
<script lang="ts"> <script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui"; import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js"; import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down"; import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
value, value,
onchange, onchange,
...restProps ...restProps
}: WithoutChildrenOrChild<CalendarPrimitive.MonthSelectProps> = $props(); }: WithoutChildrenOrChild<CalendarPrimitive.MonthSelectProps> = $props();
</script> </script>
<span <span
class={cn( class={cn(
"has-focus:border-ring border-input has-focus:ring-ring/50 relative flex rounded-md border shadow-xs has-focus:ring-[3px]", "has-focus:border-ring border-input has-focus:ring-ring/50 relative flex rounded-md border shadow-xs has-focus:ring-[3px]",
className className,
)} )}
> >
<CalendarPrimitive.MonthSelect <CalendarPrimitive.MonthSelect
bind:ref bind:ref
class="dark:bg-popover dark:text-popover-foreground absolute inset-0 opacity-0" class="dark:bg-popover dark:text-popover-foreground absolute inset-0 opacity-0"
{...restProps} {...restProps}
> >
{#snippet child({ props, monthItems, selectedMonthItem })} {#snippet child({ props, monthItems, selectedMonthItem })}
<select {...props} {value} {onchange}> <select {...props} {value} {onchange}>
{#each monthItems as monthItem (monthItem.value)} {#each monthItems as monthItem (monthItem.value)}
<option <option
value={monthItem.value} value={monthItem.value}
selected={value !== undefined selected={value !== undefined
? monthItem.value === value ? monthItem.value === value
: monthItem.value === selectedMonthItem.value} : monthItem.value === selectedMonthItem.value}
> >
{monthItem.label} {monthItem.label}
</option> </option>
{/each} {/each}
</select> </select>
<span <span
class="[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md ps-2 pe-1 text-sm font-medium select-none [&>svg]:size-3.5" class="[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md ps-2 pe-1 text-sm font-medium select-none [&>svg]:size-3.5"
aria-hidden="true" aria-hidden="true"
> >
{monthItems.find((item) => item.value === value)?.label || selectedMonthItem.label} {monthItems.find((item) => item.value === value)?.label || selectedMonthItem.label}
<ChevronDownIcon class="size-4" /> <ChevronDownIcon class="size-4" />
</span> </span>
{/snippet} {/snippet}
</CalendarPrimitive.MonthSelect> </CalendarPrimitive.MonthSelect>
</span> </span>

View File

@@ -1,15 +1,15 @@
<script lang="ts"> <script lang="ts">
import { type WithElementRef, cn } from "$lib/utils.js"; import { type WithElementRef, cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements"; import type { HTMLAttributes } from "svelte/elements";
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
children, children,
...restProps ...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props(); }: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
</script> </script>
<div {...restProps} bind:this={ref} class={cn("flex flex-col", className)}> <div {...restProps} bind:this={ref} class={cn("flex flex-col", className)}>
{@render children?.()} {@render children?.()}
</div> </div>

View File

@@ -1,19 +1,19 @@
<script lang="ts"> <script lang="ts">
import type { HTMLAttributes } from "svelte/elements"; import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js"; import { cn, type WithElementRef } from "$lib/utils.js";
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
children, children,
...restProps ...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props(); }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script> </script>
<div <div
bind:this={ref} bind:this={ref}
class={cn("relative flex flex-col gap-4 md:flex-row", className)} class={cn("relative flex flex-col gap-4 md:flex-row", className)}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
</div> </div>

View File

@@ -1,19 +1,19 @@
<script lang="ts"> <script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js"; import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements"; import type { HTMLAttributes } from "svelte/elements";
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
children, children,
...restProps ...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props(); }: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
</script> </script>
<nav <nav
{...restProps} {...restProps}
bind:this={ref} bind:this={ref}
class={cn("absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1", className)} class={cn("absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1", className)}
> >
{@render children?.()} {@render children?.()}
</nav> </nav>

View File

@@ -1,31 +1,31 @@
<script lang="ts"> <script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui"; import { Calendar as CalendarPrimitive } from "bits-ui";
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right"; import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
import { buttonVariants, type ButtonVariant } from "$lib/components/ui/button/index.js"; import { buttonVariants, type ButtonVariant } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils.js";
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
children, children,
variant = "ghost", variant = "ghost",
...restProps ...restProps
}: CalendarPrimitive.NextButtonProps & { }: CalendarPrimitive.NextButtonProps & {
variant?: ButtonVariant; variant?: ButtonVariant;
} = $props(); } = $props();
</script> </script>
{#snippet Fallback()} {#snippet Fallback()}
<ChevronRightIcon class="size-4" /> <ChevronRightIcon class="size-4" />
{/snippet} {/snippet}
<CalendarPrimitive.NextButton <CalendarPrimitive.NextButton
bind:ref bind:ref
class={cn( class={cn(
buttonVariants({ variant }), buttonVariants({ variant }),
"size-(--cell-size) bg-transparent p-0 select-none disabled:opacity-50 rtl:rotate-180", "size-(--cell-size) bg-transparent p-0 select-none disabled:opacity-50 rtl:rotate-180",
className className,
)} )}
children={children || Fallback} children={children || Fallback}
{...restProps} {...restProps}
/> />

View File

@@ -1,31 +1,31 @@
<script lang="ts"> <script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui"; import { Calendar as CalendarPrimitive } from "bits-ui";
import ChevronLeftIcon from "@lucide/svelte/icons/chevron-left"; import ChevronLeftIcon from "@lucide/svelte/icons/chevron-left";
import { buttonVariants, type ButtonVariant } from "$lib/components/ui/button/index.js"; import { buttonVariants, type ButtonVariant } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils.js";
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
children, children,
variant = "ghost", variant = "ghost",
...restProps ...restProps
}: CalendarPrimitive.PrevButtonProps & { }: CalendarPrimitive.PrevButtonProps & {
variant?: ButtonVariant; variant?: ButtonVariant;
} = $props(); } = $props();
</script> </script>
{#snippet Fallback()} {#snippet Fallback()}
<ChevronLeftIcon class="size-4" /> <ChevronLeftIcon class="size-4" />
{/snippet} {/snippet}
<CalendarPrimitive.PrevButton <CalendarPrimitive.PrevButton
bind:ref bind:ref
class={cn( class={cn(
buttonVariants({ variant }), buttonVariants({ variant }),
"size-(--cell-size) bg-transparent p-0 select-none disabled:opacity-50 rtl:rotate-180", "size-(--cell-size) bg-transparent p-0 select-none disabled:opacity-50 rtl:rotate-180",
className className,
)} )}
children={children || Fallback} children={children || Fallback}
{...restProps} {...restProps}
/> />

View File

@@ -1,47 +1,47 @@
<script lang="ts"> <script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui"; import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js"; import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down"; import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
value, value,
...restProps ...restProps
}: WithoutChildrenOrChild<CalendarPrimitive.YearSelectProps> = $props(); }: WithoutChildrenOrChild<CalendarPrimitive.YearSelectProps> = $props();
</script> </script>
<span <span
class={cn( class={cn(
"has-focus:border-ring border-input has-focus:ring-ring/50 relative flex rounded-md border shadow-xs has-focus:ring-[3px]", "has-focus:border-ring border-input has-focus:ring-ring/50 relative flex rounded-md border shadow-xs has-focus:ring-[3px]",
className className,
)} )}
> >
<CalendarPrimitive.YearSelect <CalendarPrimitive.YearSelect
bind:ref bind:ref
class="dark:bg-popover dark:text-popover-foreground absolute inset-0 opacity-0" class="dark:bg-popover dark:text-popover-foreground absolute inset-0 opacity-0"
{...restProps} {...restProps}
> >
{#snippet child({ props, yearItems, selectedYearItem })} {#snippet child({ props, yearItems, selectedYearItem })}
<select {...props} {value}> <select {...props} {value}>
{#each yearItems as yearItem (yearItem.value)} {#each yearItems as yearItem (yearItem.value)}
<option <option
value={yearItem.value} value={yearItem.value}
selected={value !== undefined selected={value !== undefined
? yearItem.value === value ? yearItem.value === value
: yearItem.value === selectedYearItem.value} : yearItem.value === selectedYearItem.value}
> >
{yearItem.label} {yearItem.label}
</option> </option>
{/each} {/each}
</select> </select>
<span <span
class="[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md ps-2 pe-1 text-sm font-medium select-none [&>svg]:size-3.5" class="[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md ps-2 pe-1 text-sm font-medium select-none [&>svg]:size-3.5"
aria-hidden="true" aria-hidden="true"
> >
{yearItems.find((item) => item.value === value)?.label || selectedYearItem.label} {yearItems.find((item) => item.value === value)?.label || selectedYearItem.label}
<ChevronDownIcon class="size-4" /> <ChevronDownIcon class="size-4" />
</span> </span>
{/snippet} {/snippet}
</CalendarPrimitive.YearSelect> </CalendarPrimitive.YearSelect>
</span> </span>

View File

@@ -1,42 +1,42 @@
<script lang="ts"> <script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui"; import { Calendar as CalendarPrimitive } from "bits-ui";
import * as Calendar from "./index.js"; import * as Calendar from "./index.js";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js"; import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
import type { ButtonVariant } from "../button/button.svelte"; import type { ButtonVariant } from "../button/button.svelte";
import { isEqualMonth, type DateValue } from "@internationalized/date"; import { isEqualMonth, type DateValue } from "@internationalized/date";
import type { Snippet } from "svelte"; import type { Snippet } from "svelte";
let { let {
ref = $bindable(null), ref = $bindable(null),
value = $bindable(), value = $bindable(),
placeholder = $bindable(), placeholder = $bindable(),
class: className, class: className,
weekdayFormat = "short", weekdayFormat = "short",
buttonVariant = "ghost", buttonVariant = "ghost",
captionLayout = "label", captionLayout = "label",
locale = "en-US", locale = "en-US",
months: monthsProp, months: monthsProp,
years, years,
monthFormat: monthFormatProp, monthFormat: monthFormatProp,
yearFormat = "numeric", yearFormat = "numeric",
day, day,
disableDaysOutsideMonth = false, disableDaysOutsideMonth = false,
...restProps ...restProps
}: WithoutChildrenOrChild<CalendarPrimitive.RootProps> & { }: WithoutChildrenOrChild<CalendarPrimitive.RootProps> & {
buttonVariant?: ButtonVariant; buttonVariant?: ButtonVariant;
captionLayout?: "dropdown" | "dropdown-months" | "dropdown-years" | "label"; captionLayout?: "dropdown" | "dropdown-months" | "dropdown-years" | "label";
months?: CalendarPrimitive.MonthSelectProps["months"]; months?: CalendarPrimitive.MonthSelectProps["months"];
years?: CalendarPrimitive.YearSelectProps["years"]; years?: CalendarPrimitive.YearSelectProps["years"];
monthFormat?: CalendarPrimitive.MonthSelectProps["monthFormat"]; monthFormat?: CalendarPrimitive.MonthSelectProps["monthFormat"];
yearFormat?: CalendarPrimitive.YearSelectProps["yearFormat"]; yearFormat?: CalendarPrimitive.YearSelectProps["yearFormat"];
day?: Snippet<[{ day: DateValue; outsideMonth: boolean }]>; day?: Snippet<[{ day: DateValue; outsideMonth: boolean }]>;
} = $props(); } = $props();
const monthFormat = $derived.by(() => { const monthFormat = $derived.by(() => {
if (monthFormatProp) return monthFormatProp; if (monthFormatProp) return monthFormatProp;
if (captionLayout.startsWith("dropdown")) return "short"; if (captionLayout.startsWith("dropdown")) return "short";
return "long"; return "long";
}); });
</script> </script>
<!-- <!--
@@ -44,72 +44,72 @@ Discriminated Unions + Destructing (required for bindable) do not
get along, so we shut typescript up by casting `value` to `never`. get along, so we shut typescript up by casting `value` to `never`.
--> -->
<CalendarPrimitive.Root <CalendarPrimitive.Root
bind:value={value as never} bind:value={value as never}
bind:ref bind:ref
bind:placeholder bind:placeholder
{weekdayFormat} {weekdayFormat}
{disableDaysOutsideMonth} {disableDaysOutsideMonth}
class={cn( class={cn(
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent", "bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
className className,
)} )}
{locale} {locale}
{monthFormat} {monthFormat}
{yearFormat} {yearFormat}
{...restProps} {...restProps}
> >
{#snippet children({ months, weekdays })} {#snippet children({ months, weekdays })}
<Calendar.Months> <Calendar.Months>
<Calendar.Nav> <Calendar.Nav>
<Calendar.PrevButton variant={buttonVariant} /> <Calendar.PrevButton variant={buttonVariant} />
<Calendar.NextButton variant={buttonVariant} /> <Calendar.NextButton variant={buttonVariant} />
</Calendar.Nav> </Calendar.Nav>
{#each months as month, monthIndex (month)} {#each months as month, monthIndex (month)}
<Calendar.Month> <Calendar.Month>
<Calendar.Header> <Calendar.Header>
<Calendar.Caption <Calendar.Caption
{captionLayout} {captionLayout}
months={monthsProp} months={monthsProp}
{monthFormat} {monthFormat}
{years} {years}
{yearFormat} {yearFormat}
month={month.value} month={month.value}
bind:placeholder bind:placeholder
{locale} {locale}
{monthIndex} {monthIndex}
/> />
</Calendar.Header> </Calendar.Header>
<Calendar.Grid> <Calendar.Grid>
<Calendar.GridHead> <Calendar.GridHead>
<Calendar.GridRow class="select-none"> <Calendar.GridRow class="select-none">
{#each weekdays as weekday (weekday)} {#each weekdays as weekday (weekday)}
<Calendar.HeadCell> <Calendar.HeadCell>
{weekday.slice(0, 2)} {weekday.slice(0, 2)}
</Calendar.HeadCell> </Calendar.HeadCell>
{/each} {/each}
</Calendar.GridRow> </Calendar.GridRow>
</Calendar.GridHead> </Calendar.GridHead>
<Calendar.GridBody> <Calendar.GridBody>
{#each month.weeks as weekDates (weekDates)} {#each month.weeks as weekDates (weekDates)}
<Calendar.GridRow class="mt-2 w-full"> <Calendar.GridRow class="mt-2 w-full">
{#each weekDates as date (date)} {#each weekDates as date (date)}
<Calendar.Cell {date} month={month.value}> <Calendar.Cell {date} month={month.value}>
{#if day} {#if day}
{@render day({ {@render day({
day: date, day: date,
outsideMonth: !isEqualMonth(date, month.value), outsideMonth: !isEqualMonth(date, month.value),
})} })}
{:else} {:else}
<Calendar.Day /> <Calendar.Day />
{/if} {/if}
</Calendar.Cell> </Calendar.Cell>
{/each} {/each}
</Calendar.GridRow> </Calendar.GridRow>
{/each} {/each}
</Calendar.GridBody> </Calendar.GridBody>
</Calendar.Grid> </Calendar.Grid>
</Calendar.Month> </Calendar.Month>
{/each} {/each}
</Calendar.Months> </Calendar.Months>
{/snippet} {/snippet}
</CalendarPrimitive.Root> </CalendarPrimitive.Root>

View File

@@ -18,23 +18,23 @@ import Nav from "./calendar-nav.svelte";
import Caption from "./calendar-caption.svelte"; import Caption from "./calendar-caption.svelte";
export { export {
Day, Day,
Cell, Cell,
Grid, Grid,
Header, Header,
Months, Months,
GridRow, GridRow,
Heading, Heading,
GridBody, GridBody,
GridHead, GridHead,
HeadCell, HeadCell,
NextButton, NextButton,
PrevButton, PrevButton,
Nav, Nav,
Month, Month,
YearSelect, YearSelect,
MonthSelect, MonthSelect,
Caption, Caption,
// //
Root as Calendar, Root as Calendar,
}; };

View File

@@ -5,15 +5,15 @@ import Trigger from "./popover-trigger.svelte";
import Portal from "./popover-portal.svelte"; import Portal from "./popover-portal.svelte";
export { export {
Root, Root,
Content, Content,
Trigger, Trigger,
Close, Close,
Portal, Portal,
// //
Root as Popover, Root as Popover,
Content as PopoverContent, Content as PopoverContent,
Trigger as PopoverTrigger, Trigger as PopoverTrigger,
Close as PopoverClose, Close as PopoverClose,
Portal as PopoverPortal, Portal as PopoverPortal,
}; };

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { Popover as PopoverPrimitive } from "bits-ui"; import { Popover as PopoverPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: PopoverPrimitive.CloseProps = $props(); let { ref = $bindable(null), ...restProps }: PopoverPrimitive.CloseProps = $props();
</script> </script>
<PopoverPrimitive.Close bind:ref data-slot="popover-close" {...restProps} /> <PopoverPrimitive.Close bind:ref data-slot="popover-close" {...restProps} />

View File

@@ -1,31 +1,31 @@
<script lang="ts"> <script lang="ts">
import { Popover as PopoverPrimitive } from "bits-ui"; import { Popover as PopoverPrimitive } from "bits-ui";
import PopoverPortal from "./popover-portal.svelte"; import PopoverPortal from "./popover-portal.svelte";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js"; import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
import type { ComponentProps } from "svelte"; import type { ComponentProps } from "svelte";
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
sideOffset = 4, sideOffset = 4,
align = "center", align = "center",
portalProps, portalProps,
...restProps ...restProps
}: PopoverPrimitive.ContentProps & { }: PopoverPrimitive.ContentProps & {
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof PopoverPortal>>; portalProps?: WithoutChildrenOrChild<ComponentProps<typeof PopoverPortal>>;
} = $props(); } = $props();
</script> </script>
<PopoverPortal {...portalProps}> <PopoverPortal {...portalProps}>
<PopoverPrimitive.Content <PopoverPrimitive.Content
bind:ref bind:ref
data-slot="popover-content" data-slot="popover-content"
{sideOffset} {sideOffset}
{align} {align}
class={cn( class={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-end-2 data-[side=right]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--bits-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden", "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-end-2 data-[side=right]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--bits-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className className,
)} )}
{...restProps} {...restProps}
/> />
</PopoverPortal> </PopoverPortal>

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { Popover as PopoverPrimitive } from "bits-ui"; import { Popover as PopoverPrimitive } from "bits-ui";
let { ...restProps }: PopoverPrimitive.PortalProps = $props(); let { ...restProps }: PopoverPrimitive.PortalProps = $props();
</script> </script>
<PopoverPrimitive.Portal {...restProps} /> <PopoverPrimitive.Portal {...restProps} />

View File

@@ -1,17 +1,17 @@
<script lang="ts"> <script lang="ts">
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils.js";
import { Popover as PopoverPrimitive } from "bits-ui"; import { Popover as PopoverPrimitive } from "bits-ui";
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
...restProps ...restProps
}: PopoverPrimitive.TriggerProps = $props(); }: PopoverPrimitive.TriggerProps = $props();
</script> </script>
<PopoverPrimitive.Trigger <PopoverPrimitive.Trigger
bind:ref bind:ref
data-slot="popover-trigger" data-slot="popover-trigger"
class={cn("", className)} class={cn("", className)}
{...restProps} {...restProps}
/> />

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { Popover as PopoverPrimitive } from "bits-ui"; import { Popover as PopoverPrimitive } from "bits-ui";
let { open = $bindable(false), ...restProps }: PopoverPrimitive.RootProps = $props(); let { open = $bindable(false), ...restProps }: PopoverPrimitive.RootProps = $props();
</script> </script>
<PopoverPrimitive.Root bind:open {...restProps} /> <PopoverPrimitive.Root bind:open {...restProps} />

View File

@@ -17,10 +17,12 @@
<div class="min-h-screen bg-background"> <div class="min-h-screen bg-background">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<!-- Mobile top nav --> <!-- Mobile top nav -->
<div class="lg:hidden flex items-center gap-2 py-3 border-b border-border/40"> <div class="lg:hidden flex items-center gap-2 py-3 border-b border-border/40">
<a href="/" class="text-xs text-muted-foreground hover:text-foreground transition-colors shrink-0 mr-2"> <a
href="/"
class="text-xs text-muted-foreground hover:text-foreground transition-colors shrink-0 mr-2"
>
{$_("admin.nav.back_mobile")} {$_("admin.nav.back_mobile")}
</a> </a>
{#each navLinks as link (link.href)} {#each navLinks as link (link.href)}

View File

@@ -5,7 +5,10 @@ export async function load({ params, fetch, cookies }) {
const token = cookies.get("session_token") || ""; const token = cookies.get("session_token") || "";
const [articles, modelsResult] = await Promise.all([ const [articles, modelsResult] = await Promise.all([
adminListArticles(fetch, token).catch(() => []), adminListArticles(fetch, token).catch(() => []),
adminListUsers({ role: "model", limit: 200 }, fetch, token).catch(() => ({ items: [], total: 0 })), adminListUsers({ role: "model", limit: 200 }, fetch, token).catch(() => ({
items: [],
total: 0,
})),
]); ]);
const article = articles.find((a) => a.id === params.id); const article = articles.find((a) => a.id === params.id);
if (!article) throw error(404, "Article not found"); if (!article) throw error(404, "Article not found");

View File

@@ -107,13 +107,13 @@
<button <button
type="button" type="button"
class={`px-3 py-1 transition-colors ${editorTab === "write" ? "bg-primary/10 text-primary" : "text-muted-foreground"}`} class={`px-3 py-1 transition-colors ${editorTab === "write" ? "bg-primary/10 text-primary" : "text-muted-foreground"}`}
onclick={() => (editorTab = "write")} onclick={() => (editorTab = "write")}>{$_("admin.common.write")}</button
>{$_("admin.common.write")}</button> >
<button <button
type="button" type="button"
class={`px-3 py-1 transition-colors ${editorTab === "preview" ? "bg-primary/10 text-primary" : "text-muted-foreground"}`} class={`px-3 py-1 transition-colors ${editorTab === "preview" ? "bg-primary/10 text-primary" : "text-muted-foreground"}`}
onclick={() => (editorTab = "preview")} onclick={() => (editorTab = "preview")}>{$_("admin.common.preview")}</button
>{$_("admin.common.preview")}</button> >
</div> </div>
</div> </div>
<div class="sm:grid sm:grid-cols-2 sm:gap-4 min-h-96"> <div class="sm:grid sm:grid-cols-2 sm:gap-4 min-h-96">
@@ -127,7 +127,9 @@
{#if preview} {#if preview}
{@html preview} {@html preview}
{:else} {:else}
<p class="text-muted-foreground italic text-sm">{$_("admin.article_form.preview_placeholder")}</p> <p class="text-muted-foreground italic text-sm">
{$_("admin.article_form.preview_placeholder")}
</p>
{/if} {/if}
</div> </div>
</div> </div>
@@ -152,7 +154,11 @@
<SelectTrigger class="w-full"> <SelectTrigger class="w-full">
{#if selectedAuthor} {#if selectedAuthor}
{#if selectedAuthor.avatar} {#if selectedAuthor.avatar}
<img src={getAssetUrl(selectedAuthor.avatar, "mini")} alt="" class="h-5 w-5 rounded-full object-cover shrink-0" /> <img
src={getAssetUrl(selectedAuthor.avatar, "mini")}
alt=""
class="h-5 w-5 rounded-full object-cover shrink-0"
/>
{/if} {/if}
{selectedAuthor.artist_name} {selectedAuthor.artist_name}
{:else} {:else}
@@ -164,7 +170,11 @@
{#each data.authors as author (author.id)} {#each data.authors as author (author.id)}
<SelectItem value={author.id}> <SelectItem value={author.id}>
{#if author.avatar} {#if author.avatar}
<img src={getAssetUrl(author.avatar, "mini")} alt="" class="h-5 w-5 rounded-full object-cover shrink-0" /> <img
src={getAssetUrl(author.avatar, "mini")}
alt=""
class="h-5 w-5 rounded-full object-cover shrink-0"
/>
{/if} {/if}
{author.artist_name} {author.artist_name}
</SelectItem> </SelectItem>
@@ -195,7 +205,11 @@
</label> </label>
<div class="flex gap-3 pt-2"> <div class="flex gap-3 pt-2">
<Button onclick={handleSubmit} disabled={saving} class="bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"> <Button
onclick={handleSubmit}
disabled={saving}
class="bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
>
{saving ? $_("admin.common.saving") : $_("admin.common.save_changes")} {saving ? $_("admin.common.saving") : $_("admin.common.save_changes")}
</Button> </Button>
<Button variant="outline" href="/admin/articles">{$_("common.cancel")}</Button> <Button variant="outline" href="/admin/articles">{$_("common.cancel")}</Button>

View File

@@ -98,13 +98,22 @@
</div> </div>
<div class="space-y-1.5"> <div class="space-y-1.5">
<Label for="slug">{$_("admin.common.slug_field")}</Label> <Label for="slug">{$_("admin.common.slug_field")}</Label>
<Input id="slug" bind:value={slug} placeholder={$_("admin.article_form.slug_placeholder")} /> <Input
id="slug"
bind:value={slug}
placeholder={$_("admin.article_form.slug_placeholder")}
/>
</div> </div>
</div> </div>
<div class="space-y-1.5"> <div class="space-y-1.5">
<Label for="excerpt">{$_("admin.article_form.excerpt")}</Label> <Label for="excerpt">{$_("admin.article_form.excerpt")}</Label>
<Textarea id="excerpt" bind:value={excerpt} placeholder={$_("admin.article_form.excerpt_placeholder")} rows={2} /> <Textarea
id="excerpt"
bind:value={excerpt}
placeholder={$_("admin.article_form.excerpt_placeholder")}
rows={2}
/>
</div> </div>
<!-- Markdown editor with live preview --> <!-- Markdown editor with live preview -->
@@ -115,13 +124,13 @@
<button <button
type="button" type="button"
class={`px-3 py-1 transition-colors ${editorTab === "write" ? "bg-primary/10 text-primary" : "text-muted-foreground"}`} class={`px-3 py-1 transition-colors ${editorTab === "write" ? "bg-primary/10 text-primary" : "text-muted-foreground"}`}
onclick={() => (editorTab = "write")} onclick={() => (editorTab = "write")}>{$_("admin.common.write")}</button
>{$_("admin.common.write")}</button> >
<button <button
type="button" type="button"
class={`px-3 py-1 transition-colors ${editorTab === "preview" ? "bg-primary/10 text-primary" : "text-muted-foreground"}`} class={`px-3 py-1 transition-colors ${editorTab === "preview" ? "bg-primary/10 text-primary" : "text-muted-foreground"}`}
onclick={() => (editorTab = "preview")} onclick={() => (editorTab = "preview")}>{$_("admin.common.preview")}</button
>{$_("admin.common.preview")}</button> >
</div> </div>
</div> </div>
<!-- Mobile: single pane toggled; Desktop: side by side --> <!-- Mobile: single pane toggled; Desktop: side by side -->
@@ -137,7 +146,9 @@
{#if preview} {#if preview}
{@html preview} {@html preview}
{:else} {:else}
<p class="text-muted-foreground italic text-sm">{$_("admin.article_form.preview_placeholder")}</p> <p class="text-muted-foreground italic text-sm">
{$_("admin.article_form.preview_placeholder")}
</p>
{/if} {/if}
</div> </div>
</div> </div>
@@ -146,13 +157,19 @@
<div class="space-y-1.5"> <div class="space-y-1.5">
<Label>{$_("admin.common.cover_image")}</Label> <Label>{$_("admin.common.cover_image")}</Label>
<FileDropZone accept="image/*" maxFileSize={10 * MEGABYTE} onUpload={handleImageUpload} /> <FileDropZone accept="image/*" maxFileSize={10 * MEGABYTE} onUpload={handleImageUpload} />
{#if imageId}<p class="text-xs text-green-600 mt-1">{$_("admin.common.image_uploaded")}</p>{/if} {#if imageId}<p class="text-xs text-green-600 mt-1">
{$_("admin.common.image_uploaded")}
</p>{/if}
</div> </div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-1.5"> <div class="space-y-1.5">
<Label for="category">{$_("admin.article_form.category")}</Label> <Label for="category">{$_("admin.article_form.category")}</Label>
<Input id="category" bind:value={category} placeholder={$_("admin.article_form.category_placeholder")} /> <Input
id="category"
bind:value={category}
placeholder={$_("admin.article_form.category_placeholder")}
/>
</div> </div>
<div class="space-y-1.5"> <div class="space-y-1.5">
<Label>{$_("admin.common.publish_date")}</Label> <Label>{$_("admin.common.publish_date")}</Label>
@@ -171,7 +188,11 @@
</label> </label>
<div class="flex gap-3 pt-2"> <div class="flex gap-3 pt-2">
<Button onclick={handleSubmit} disabled={saving} class="bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"> <Button
onclick={handleSubmit}
disabled={saving}
class="bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
>
{saving ? $_("admin.common.creating") : $_("admin.article_form.create")} {saving ? $_("admin.common.creating") : $_("admin.article_form.create")}
</Button> </Button>
<Button variant="outline" href="/admin/articles">{$_("common.cancel")}</Button> <Button variant="outline" href="/admin/articles">{$_("common.cancel")}</Button>

View File

@@ -87,7 +87,9 @@
<div class="py-3 sm:py-6 sm:pl-6"> <div class="py-3 sm:py-6 sm:pl-6">
<div class="flex items-center justify-between mb-6 px-3 sm:px-0"> <div class="flex items-center justify-between mb-6 px-3 sm:px-0">
<h1 class="text-2xl font-bold">{$_("admin.users.title")}</h1> <h1 class="text-2xl font-bold">{$_("admin.users.title")}</h1>
<span class="text-sm text-muted-foreground">{$_("admin.users.total", { values: { total: data.total } })}</span> <span class="text-sm text-muted-foreground"
>{$_("admin.users.total", { values: { total: data.total } })}</span
>
</div> </div>
<!-- Filters --> <!-- Filters -->
@@ -120,11 +122,21 @@
<table class="w-full text-sm"> <table class="w-full text-sm">
<thead class="bg-muted/30"> <thead class="bg-muted/30">
<tr> <tr>
<th class="px-4 py-3 text-left font-medium text-muted-foreground">{$_("admin.users.col_user")}</th> <th class="px-4 py-3 text-left font-medium text-muted-foreground"
<th class="px-4 py-3 text-left font-medium text-muted-foreground hidden sm:table-cell">{$_("admin.users.col_email")}</th> >{$_("admin.users.col_user")}</th
<th class="px-4 py-3 text-left font-medium text-muted-foreground">{$_("admin.users.col_role")}</th> >
<th class="px-4 py-3 text-left font-medium text-muted-foreground hidden md:table-cell">{$_("admin.users.col_joined")}</th> <th class="px-4 py-3 text-left font-medium text-muted-foreground hidden sm:table-cell"
<th class="px-4 py-3 text-right font-medium text-muted-foreground">{$_("admin.users.col_actions")}</th> >{$_("admin.users.col_email")}</th
>
<th class="px-4 py-3 text-left font-medium text-muted-foreground"
>{$_("admin.users.col_role")}</th
>
<th class="px-4 py-3 text-left font-medium text-muted-foreground hidden md:table-cell"
>{$_("admin.users.col_joined")}</th
>
<th class="px-4 py-3 text-right font-medium text-muted-foreground"
>{$_("admin.users.col_actions")}</th
>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-border/30"> <tbody class="divide-y divide-border/30">
@@ -147,12 +159,18 @@
{/if} {/if}
<div class="min-w-0"> <div class="min-w-0">
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1.5">
<span class="font-medium truncate">{user.artist_name || user.first_name || "—"}</span> <span class="font-medium truncate"
>{user.artist_name || user.first_name || "—"}</span
>
{#if user.is_admin} {#if user.is_admin}
<Badge variant="default" class="shrink-0 text-[10px] px-1.5 py-0">{$_("admin.users.admin_badge")}</Badge> <Badge variant="default" class="shrink-0 text-[10px] px-1.5 py-0"
>{$_("admin.users.admin_badge")}</Badge
>
{/if} {/if}
</div> </div>
<span class="text-xs text-muted-foreground sm:hidden truncate block">{user.email}</span> <span class="text-xs text-muted-foreground sm:hidden truncate block"
>{user.email}</span
>
</div> </div>
</div> </div>
</td> </td>
@@ -173,7 +191,9 @@
</SelectContent> </SelectContent>
</Select> </Select>
</td> </td>
<td class="px-4 py-3 text-muted-foreground hidden md:table-cell">{formatDate(user.date_created)}</td> <td class="px-4 py-3 text-muted-foreground hidden md:table-cell"
>{formatDate(user.date_created)}</td
>
<td class="px-4 py-3 text-right"> <td class="px-4 py-3 text-right">
<div class="flex items-center justify-end gap-1"> <div class="flex items-center justify-end gap-1">
<Button size="sm" variant="ghost" href="/admin/users/{user.id}"> <Button size="sm" variant="ghost" href="/admin/users/{user.id}">
@@ -195,7 +215,9 @@
{#if data.items.length === 0} {#if data.items.length === 0}
<tr> <tr>
<td colspan="5" class="px-4 py-8 text-center text-muted-foreground">{$_("admin.users.no_results")}</td> <td colspan="5" class="px-4 py-8 text-center text-muted-foreground"
>{$_("admin.users.no_results")}</td
>
</tr> </tr>
{/if} {/if}
</tbody> </tbody>
@@ -206,7 +228,13 @@
{#if data.total > data.limit} {#if data.total > data.limit}
<div class="flex items-center justify-between mt-4 px-3 sm:px-0"> <div class="flex items-center justify-between mt-4 px-3 sm:px-0">
<span class="text-sm text-muted-foreground"> <span class="text-sm text-muted-foreground">
{$_("admin.users.showing", { values: { start: data.offset + 1, end: Math.min(data.offset + data.limit, data.total), total: data.total } })} {$_("admin.users.showing", {
values: {
start: data.offset + 1,
end: Math.min(data.offset + data.limit, data.total),
total: data.total,
},
})}
</span> </span>
<div class="flex gap-2"> <div class="flex gap-2">
<Button <Button
@@ -244,7 +272,9 @@
<Dialog.Header> <Dialog.Header>
<Dialog.Title>{$_("admin.users.delete_title")}</Dialog.Title> <Dialog.Title>{$_("admin.users.delete_title")}</Dialog.Title>
<Dialog.Description> <Dialog.Description>
{$_("admin.users.delete_description", { values: { name: deleteTarget?.artist_name || deleteTarget?.email } })} {$_("admin.users.delete_description", {
values: { name: deleteTarget?.artist_name || deleteTarget?.email },
})}
</Dialog.Description> </Dialog.Description>
</Dialog.Header> </Dialog.Header>
<Dialog.Footer> <Dialog.Footer>

View File

@@ -106,7 +106,11 @@
</Button> </Button>
<div> <div>
<h1 class="text-2xl font-bold">{data.user.artist_name || data.user.email}</h1> <h1 class="text-2xl font-bold">{data.user.artist_name || data.user.email}</h1>
<p class="text-xs text-muted-foreground">{data.user.email} · {data.user.role}{data.user.is_admin ? " · " + $_("admin.users.admin_badge").toLowerCase() : ""}</p> <p class="text-xs text-muted-foreground">
{data.user.email} · {data.user.role}{data.user.is_admin
? " · " + $_("admin.users.admin_badge").toLowerCase()
: ""}
</p>
</div> </div>
</div> </div>
@@ -155,8 +159,14 @@
</div> </div>
<!-- Admin flag --> <!-- Admin flag -->
<label class="flex items-center gap-3 rounded-lg border border-border/40 px-4 py-3 cursor-pointer hover:bg-muted/20 transition-colors"> <label
<input type="checkbox" bind:checked={isAdmin} class="h-4 w-4 rounded accent-primary shrink-0" /> class="flex items-center gap-3 rounded-lg border border-border/40 px-4 py-3 cursor-pointer hover:bg-muted/20 transition-colors"
>
<input
type="checkbox"
bind:checked={isAdmin}
class="h-4 w-4 rounded accent-primary shrink-0"
/>
<div> <div>
<span class="text-sm font-medium">{$_("admin.user_edit.is_admin")}</span> <span class="text-sm font-medium">{$_("admin.user_edit.is_admin")}</span>
<p class="text-xs text-muted-foreground">{$_("admin.user_edit.is_admin_hint")}</p> <p class="text-xs text-muted-foreground">{$_("admin.user_edit.is_admin_hint")}</p>
@@ -164,7 +174,11 @@
</label> </label>
<div class="flex gap-3"> <div class="flex gap-3">
<Button onclick={handleSave} disabled={saving} class="bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"> <Button
onclick={handleSave}
disabled={saving}
class="bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
>
{saving ? $_("admin.common.saving") : $_("admin.common.save_changes")} {saving ? $_("admin.common.saving") : $_("admin.common.save_changes")}
</Button> </Button>
</div> </div>

View File

@@ -97,7 +97,11 @@
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-1.5"> <div class="space-y-1.5">
<Label for="title">{$_("admin.common.title_field")}</Label> <Label for="title">{$_("admin.common.title_field")}</Label>
<Input id="title" bind:value={title} placeholder={$_("admin.video_form.title_placeholder")} /> <Input
id="title"
bind:value={title}
placeholder={$_("admin.video_form.title_placeholder")}
/>
</div> </div>
<div class="space-y-1.5"> <div class="space-y-1.5">
<Label for="slug">{$_("admin.common.slug_field")}</Label> <Label for="slug">{$_("admin.common.slug_field")}</Label>
@@ -107,7 +111,12 @@
<div class="space-y-1.5"> <div class="space-y-1.5">
<Label for="description">{$_("admin.video_form.description")}</Label> <Label for="description">{$_("admin.video_form.description")}</Label>
<Textarea id="description" bind:value={description} placeholder={$_("admin.video_form.description_placeholder")} rows={3} /> <Textarea
id="description"
bind:value={description}
placeholder={$_("admin.video_form.description_placeholder")}
rows={3}
/>
</div> </div>
<div class="space-y-1.5"> <div class="space-y-1.5">
@@ -127,7 +136,7 @@
{#if movieId} {#if movieId}
<video <video
src={getAssetUrl(movieId)} src={getAssetUrl(movieId)}
poster={imageId ? getAssetUrl(imageId, "preview") ?? undefined : undefined} poster={imageId ? (getAssetUrl(imageId, "preview") ?? undefined) : undefined}
controls controls
class="w-full rounded-lg bg-black max-h-72 mb-2" class="w-full rounded-lg bg-black max-h-72 mb-2"
></video> ></video>
@@ -142,7 +151,11 @@
<div class="space-y-1.5"> <div class="space-y-1.5">
<Label>{$_("admin.common.publish_date")}</Label> <Label>{$_("admin.common.publish_date")}</Label>
<DatePicker bind:value={uploadDate} placeholder={$_("admin.common.publish_date")} showTime={false} /> <DatePicker
bind:value={uploadDate}
placeholder={$_("admin.common.publish_date")}
showTime={false}
/>
</div> </div>
<div class="flex gap-6"> <div class="flex gap-6">
@@ -162,7 +175,9 @@
<Select type="multiple" bind:value={selectedModelIds}> <Select type="multiple" bind:value={selectedModelIds}>
<SelectTrigger class="w-full"> <SelectTrigger class="w-full">
{#if selectedModelIds.length} {#if selectedModelIds.length}
{$_("admin.video_form.models_selected", { values: { count: selectedModelIds.length } })} {$_("admin.video_form.models_selected", {
values: { count: selectedModelIds.length },
})}
{:else} {:else}
<span class="text-muted-foreground">{$_("admin.video_form.no_models")}</span> <span class="text-muted-foreground">{$_("admin.video_form.no_models")}</span>
{/if} {/if}
@@ -171,7 +186,11 @@
{#each data.models as model (model.id)} {#each data.models as model (model.id)}
<SelectItem value={model.id}> <SelectItem value={model.id}>
{#if model.avatar} {#if model.avatar}
<img src={getAssetUrl(model.avatar, "mini")} alt="" class="h-5 w-5 rounded-full object-cover shrink-0" /> <img
src={getAssetUrl(model.avatar, "mini")}
alt=""
class="h-5 w-5 rounded-full object-cover shrink-0"
/>
{/if} {/if}
{model.artist_name} {model.artist_name}
</SelectItem> </SelectItem>
@@ -182,7 +201,11 @@
{/if} {/if}
<div class="flex gap-3 pt-2"> <div class="flex gap-3 pt-2">
<Button onclick={handleSubmit} disabled={saving} class="bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"> <Button
onclick={handleSubmit}
disabled={saving}
class="bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
>
{saving ? $_("admin.common.saving") : $_("admin.common.save_changes")} {saving ? $_("admin.common.saving") : $_("admin.common.save_changes")}
</Button> </Button>
<Button variant="outline" href="/admin/videos">{$_("common.cancel")}</Button> <Button variant="outline" href="/admin/videos">{$_("common.cancel")}</Button>

View File

@@ -137,13 +137,17 @@
<div class="space-y-1.5"> <div class="space-y-1.5">
<Label>{$_("admin.common.cover_image")}</Label> <Label>{$_("admin.common.cover_image")}</Label>
<FileDropZone accept="image/*" maxFileSize={10 * MEGABYTE} onUpload={handleImageUpload} /> <FileDropZone accept="image/*" maxFileSize={10 * MEGABYTE} onUpload={handleImageUpload} />
{#if imageId}<p class="text-xs text-green-600 mt-1">{$_("admin.common.image_uploaded")}</p>{/if} {#if imageId}<p class="text-xs text-green-600 mt-1">
{$_("admin.common.image_uploaded")}
</p>{/if}
</div> </div>
<div class="space-y-1.5"> <div class="space-y-1.5">
<Label>{$_("admin.video_form.video_file")}</Label> <Label>{$_("admin.video_form.video_file")}</Label>
<FileDropZone accept="video/*" maxFileSize={2000 * MEGABYTE} onUpload={handleVideoUpload} /> <FileDropZone accept="video/*" maxFileSize={2000 * MEGABYTE} onUpload={handleVideoUpload} />
{#if movieId}<p class="text-xs text-green-600 mt-1">{$_("admin.video_form.video_uploaded")}</p>{/if} {#if movieId}<p class="text-xs text-green-600 mt-1">
{$_("admin.video_form.video_uploaded")}
</p>{/if}
</div> </div>
<div class="space-y-1.5"> <div class="space-y-1.5">
@@ -153,7 +157,11 @@
<div class="space-y-1.5"> <div class="space-y-1.5">
<Label>{$_("admin.common.publish_date")}</Label> <Label>{$_("admin.common.publish_date")}</Label>
<DatePicker bind:value={uploadDate} placeholder={$_("admin.common.publish_date")} showTime={false} /> <DatePicker
bind:value={uploadDate}
placeholder={$_("admin.common.publish_date")}
showTime={false}
/>
</div> </div>
<div class="flex gap-6"> <div class="flex gap-6">
@@ -189,7 +197,11 @@
{/if} {/if}
<div class="flex gap-3 pt-2"> <div class="flex gap-3 pt-2">
<Button onclick={handleSubmit} disabled={saving} class="bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"> <Button
onclick={handleSubmit}
disabled={saving}
class="bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
>
{saving ? $_("admin.common.creating") : $_("admin.video_form.create")} {saving ? $_("admin.common.creating") : $_("admin.video_form.create")}
</Button> </Button>
<Button variant="outline" href="/admin/videos">{$_("common.cancel")}</Button> <Button variant="outline" href="/admin/videos">{$_("common.cancel")}</Button>