feat: add date/time picker and fix launcherVersion initialization error
This commit is contained in:
1 parent
54f7f81160
commit
4dc6cb1721
33 files changed
+1074
-13
No files matched your search
@@ -41,7 +41,8 @@
|
||||
"@iconify-json/lucide": "^1.2.149",
|
||||
"@iconify-json/ph": "^1.2.2",
|
||||
"@iconify-json/tabler": "^1.2.43",
|
||||
"@lucide/svelte": "^1.16.0",
|
||||
"@internationalized/date": "^3.10.0",
|
||||
"@lucide/svelte": "^0.561.0",
|
||||
"@sveltejs/adapter-static": "^3.0.10",
|
||||
"@sveltejs/kit": "^2.61.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
||||
@@ -156,7 +157,7 @@
|
||||
|
||||
"@iconify/types": ["@iconify/[email protected]", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
|
||||
|
||||
"@internationalized/date": ["@internationalized/date@3.8.2", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-/wENk7CbvLbkUvX1tu0mwq49CVkkWpkXubGel6birjRPyo6uQ4nQpnq5xZu823zRCwwn82zgHrvgF1vZyvmVgA=="],
|
||||
"@internationalized/date": ["@internationalized/date@3.12.4", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-M1dEn4c1U1HsSlaVR8upZtSqvXrTkHDfv18H01uCSJyjVLDxnBR38v/fMxecmlwXKR4i9HeZcmgQAPE6A+aGJQ=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
@@ -170,7 +171,7 @@
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@lucide/svelte": ["@lucide/svelte@1.16.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-AvvPJnaWxeiNkAljI5MsSEc84yHPLMaWQIAJOcbX7k9au/f9ITS7cxTTQiautDiOFKVOXiYdZ+d6mtl88J+Kbg=="],
|
||||
"@lucide/svelte": ["@lucide/svelte@0.561.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-vofKV2UFVrKE6I4ewKJ3dfCXSV6iP6nWVmiM83MLjsU91EeJcEg7LoWUABLp/aOTxj1HQNbJD1f3g3L0JQgH9A=="],
|
||||
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/[email protected]", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
|
||||
|
||||
|
||||
+2
-1
@@ -55,7 +55,8 @@
|
||||
"@iconify-json/lucide": "^1.2.149",
|
||||
"@iconify-json/ph": "^1.2.2",
|
||||
"@iconify-json/tabler": "^1.2.43",
|
||||
"@lucide/svelte": "^1.16.0",
|
||||
"@internationalized/date": "^3.10.0",
|
||||
"@lucide/svelte": "^0.561.0",
|
||||
"@sveltejs/adapter-static": "^3.0.10",
|
||||
"@sveltejs/kit": "^2.61.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import ThemeTransformControls from '$lib/components/theme/ThemeTransformControls.svelte';
|
||||
import Button from '$lib/components/ui/button/button.svelte';
|
||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||
import DatePicker from '$lib/components/ui/date-picker/date-picker.svelte';
|
||||
import Input from '$lib/components/ui/input/input.svelte';
|
||||
import Label from '$lib/components/ui/label/label.svelte';
|
||||
import * as Select from '$lib/components/ui/select';
|
||||
@@ -1929,17 +1930,23 @@
|
||||
</ThemeSection>
|
||||
{:else if element.type === 'countdown'}
|
||||
<ThemeSection title="Countdown" contentClass="space-y-3 pt-2.5">
|
||||
<div class="space-y-1">
|
||||
<Label for="element-countdown-target" class={label}>Target date</Label>
|
||||
<Input
|
||||
<div class="space-y-1.5">
|
||||
<Label for="element-countdown-target" class={label}>Target date & time</Label>
|
||||
<DatePicker
|
||||
id="element-countdown-target"
|
||||
class="{field} font-mono"
|
||||
bind:value={element.target}
|
||||
withTime={true}
|
||||
placeholder="Pick a target date & time..."
|
||||
class="h-8 border-theme-800 bg-theme-950 text-xs text-theme-50 hover:bg-theme-900"
|
||||
contentClass="border-theme-800 bg-theme-950 text-theme-50"
|
||||
/>
|
||||
<Input
|
||||
class="{field} font-mono text-[10px]"
|
||||
placeholder="2027-01-01T00:00:00Z"
|
||||
bind:value={element.target}
|
||||
/>
|
||||
<p class="text-[9px] leading-relaxed text-muted-foreground">
|
||||
Use an ISO date-time with a timezone, for example <code>2027-01-01T00:00:00Z</code>. Empty
|
||||
or invalid dates show zeroes.
|
||||
Pick a date and time with the calendar, or paste an ISO date-time directly.
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
<script lang="ts">
|
||||
import type { ComponentProps } from "svelte";
|
||||
import type Calendar from "./calendar.svelte";
|
||||
import CalendarMonthSelect from "./calendar-month-select.svelte";
|
||||
import CalendarYearSelect from "./calendar-year-select.svelte";
|
||||
import { DateFormatter, getLocalTimeZone, type DateValue } from "@internationalized/date";
|
||||
|
||||
let {
|
||||
captionLayout,
|
||||
months,
|
||||
monthFormat,
|
||||
years,
|
||||
yearFormat,
|
||||
month,
|
||||
locale,
|
||||
placeholder = $bindable(),
|
||||
monthIndex = 0,
|
||||
}: {
|
||||
captionLayout: ComponentProps<typeof Calendar>["captionLayout"];
|
||||
months: ComponentProps<typeof CalendarMonthSelect>["months"];
|
||||
monthFormat: ComponentProps<typeof CalendarMonthSelect>["monthFormat"];
|
||||
years: ComponentProps<typeof CalendarYearSelect>["years"];
|
||||
yearFormat: ComponentProps<typeof CalendarYearSelect>["yearFormat"];
|
||||
month: DateValue;
|
||||
placeholder: DateValue | undefined;
|
||||
locale: string;
|
||||
monthIndex: number;
|
||||
} = $props();
|
||||
|
||||
function formatYear(date: DateValue) {
|
||||
const dateObj = date.toDate(getLocalTimeZone());
|
||||
if (typeof yearFormat === "function") return yearFormat(dateObj.getFullYear());
|
||||
return new DateFormatter(locale, { year: yearFormat }).format(dateObj);
|
||||
}
|
||||
|
||||
function formatMonth(date: DateValue) {
|
||||
const dateObj = date.toDate(getLocalTimeZone());
|
||||
if (typeof monthFormat === "function") return monthFormat(dateObj.getMonth() + 1);
|
||||
return new DateFormatter(locale, { month: monthFormat }).format(dateObj);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#snippet MonthSelect()}
|
||||
<CalendarMonthSelect
|
||||
{months}
|
||||
{monthFormat}
|
||||
value={month.month}
|
||||
onchange={(e) => {
|
||||
if (!placeholder) return;
|
||||
const v = Number.parseInt(e.currentTarget.value);
|
||||
const newPlaceholder = placeholder.set({ month: v });
|
||||
placeholder = newPlaceholder.subtract({ months: monthIndex });
|
||||
}}
|
||||
/>
|
||||
{/snippet}
|
||||
|
||||
{#snippet YearSelect()}
|
||||
<CalendarYearSelect {years} {yearFormat} value={month.year} />
|
||||
{/snippet}
|
||||
|
||||
{#if captionLayout === "dropdown"}
|
||||
{@render MonthSelect()}
|
||||
{@render YearSelect()}
|
||||
{:else if captionLayout === "dropdown-months"}
|
||||
{@render MonthSelect()}
|
||||
{#if placeholder}
|
||||
{formatYear(placeholder)}
|
||||
{/if}
|
||||
{:else if captionLayout === "dropdown-years"}
|
||||
{#if placeholder}
|
||||
{formatMonth(placeholder)}
|
||||
{/if}
|
||||
{@render YearSelect()}
|
||||
{:else}
|
||||
{formatMonth(month)} {formatYear(month)}
|
||||
{/if}
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CalendarPrimitive.CellProps = $props();
|
||||
</script>
|
||||
|
||||
<CalendarPrimitive.Cell
|
||||
bind:ref
|
||||
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",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import { buttonVariants } from "$lib/components/ui/button/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CalendarPrimitive.DayProps = $props();
|
||||
</script>
|
||||
|
||||
<CalendarPrimitive.Day
|
||||
bind:ref
|
||||
class={cn(
|
||||
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",
|
||||
"[&[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",
|
||||
// Outside months
|
||||
"[&[data-outside-month]:not([data-selected])]:text-muted-foreground [&[data-outside-month]:not([data-selected])]:hover:text-accent-foreground",
|
||||
// Disabled
|
||||
"data-[disabled]:text-muted-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
// Unavailable
|
||||
"data-[unavailable]:text-muted-foreground data-[unavailable]:line-through",
|
||||
// hover
|
||||
"dark:hover:text-accent-foreground",
|
||||
// focus
|
||||
"focus:border-ring focus:ring-ring/50 focus:relative",
|
||||
// inner spans
|
||||
"[&>span]:text-xs [&>span]:opacity-70",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CalendarPrimitive.GridBodyProps = $props();
|
||||
</script>
|
||||
|
||||
<CalendarPrimitive.GridBody bind:ref class={cn(className)} {...restProps} />
|
||||
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CalendarPrimitive.GridHeadProps = $props();
|
||||
</script>
|
||||
|
||||
<CalendarPrimitive.GridHead bind:ref class={cn(className)} {...restProps} />
|
||||
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CalendarPrimitive.GridRowProps = $props();
|
||||
</script>
|
||||
|
||||
<CalendarPrimitive.GridRow bind:ref class={cn("flex", className)} {...restProps} />
|
||||
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CalendarPrimitive.GridProps = $props();
|
||||
</script>
|
||||
|
||||
<CalendarPrimitive.Grid
|
||||
bind:ref
|
||||
class={cn("mt-4 flex w-full border-collapse flex-col gap-1", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CalendarPrimitive.HeadCellProps = $props();
|
||||
</script>
|
||||
|
||||
<CalendarPrimitive.HeadCell
|
||||
bind:ref
|
||||
class={cn(
|
||||
"text-muted-foreground w-(--cell-size) rounded-md text-[0.8rem] font-normal",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CalendarPrimitive.HeaderProps = $props();
|
||||
</script>
|
||||
|
||||
<CalendarPrimitive.Header
|
||||
bind:ref
|
||||
class={cn(
|
||||
"flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CalendarPrimitive.HeadingProps = $props();
|
||||
</script>
|
||||
|
||||
<CalendarPrimitive.Heading
|
||||
bind:ref
|
||||
class={cn("px-(--cell-size) text-sm font-medium", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,48 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
value,
|
||||
onchange,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<CalendarPrimitive.MonthSelectProps> = $props();
|
||||
</script>
|
||||
|
||||
<span
|
||||
class={cn(
|
||||
"has-focus:border-ring border-input has-focus:ring-ring/50 relative flex rounded-md border shadow-xs has-focus:ring-[3px]",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<CalendarPrimitive.MonthSelect
|
||||
bind:ref
|
||||
class="dark:bg-popover dark:text-popover-foreground absolute inset-0 opacity-0"
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet child({ props, monthItems, selectedMonthItem })}
|
||||
<select {...props} {value} {onchange}>
|
||||
{#each monthItems as monthItem (monthItem.value)}
|
||||
<option
|
||||
value={monthItem.value}
|
||||
selected={value !== undefined
|
||||
? monthItem.value === value
|
||||
: monthItem.value === selectedMonthItem.value}
|
||||
>
|
||||
{monthItem.label}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
<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"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{monthItems.find((item) => item.value === value)?.label || selectedMonthItem.label}
|
||||
<ChevronDownIcon class="size-4" />
|
||||
</span>
|
||||
{/snippet}
|
||||
</CalendarPrimitive.MonthSelect>
|
||||
</span>
|
||||
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { type WithElementRef, cn } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div {...restProps} bind:this={ref} class={cn("flex flex-col", className)}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
class={cn("relative flex flex-col gap-4 md:flex-row", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
|
||||
</script>
|
||||
|
||||
<nav
|
||||
{...restProps}
|
||||
bind:this={ref}
|
||||
class={cn("absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1", className)}
|
||||
>
|
||||
{@render children?.()}
|
||||
</nav>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
|
||||
import { buttonVariants, type ButtonVariant } from "$lib/components/ui/button/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
variant = "ghost",
|
||||
...restProps
|
||||
}: CalendarPrimitive.NextButtonProps & {
|
||||
variant?: ButtonVariant;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
{#snippet Fallback()}
|
||||
<ChevronRightIcon class="size-4" />
|
||||
{/snippet}
|
||||
|
||||
<CalendarPrimitive.NextButton
|
||||
bind:ref
|
||||
class={cn(
|
||||
buttonVariants({ variant }),
|
||||
"size-(--cell-size) bg-transparent p-0 select-none disabled:opacity-50 rtl:rotate-180",
|
||||
className
|
||||
)}
|
||||
children={children || Fallback}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import ChevronLeftIcon from "@lucide/svelte/icons/chevron-left";
|
||||
import { buttonVariants, type ButtonVariant } from "$lib/components/ui/button/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
variant = "ghost",
|
||||
...restProps
|
||||
}: CalendarPrimitive.PrevButtonProps & {
|
||||
variant?: ButtonVariant;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
{#snippet Fallback()}
|
||||
<ChevronLeftIcon class="size-4" />
|
||||
{/snippet}
|
||||
|
||||
<CalendarPrimitive.PrevButton
|
||||
bind:ref
|
||||
class={cn(
|
||||
buttonVariants({ variant }),
|
||||
"size-(--cell-size) bg-transparent p-0 select-none disabled:opacity-50 rtl:rotate-180",
|
||||
className
|
||||
)}
|
||||
children={children || Fallback}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,47 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
value,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<CalendarPrimitive.YearSelectProps> = $props();
|
||||
</script>
|
||||
|
||||
<span
|
||||
class={cn(
|
||||
"has-focus:border-ring border-input has-focus:ring-ring/50 relative flex rounded-md border shadow-xs has-focus:ring-[3px]",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<CalendarPrimitive.YearSelect
|
||||
bind:ref
|
||||
class="dark:bg-popover dark:text-popover-foreground absolute inset-0 opacity-0"
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet child({ props, yearItems, selectedYearItem })}
|
||||
<select {...props} {value}>
|
||||
{#each yearItems as yearItem (yearItem.value)}
|
||||
<option
|
||||
value={yearItem.value}
|
||||
selected={value !== undefined
|
||||
? yearItem.value === value
|
||||
: yearItem.value === selectedYearItem.value}
|
||||
>
|
||||
{yearItem.label}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
<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"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{yearItems.find((item) => item.value === value)?.label || selectedYearItem.label}
|
||||
<ChevronDownIcon class="size-4" />
|
||||
</span>
|
||||
{/snippet}
|
||||
</CalendarPrimitive.YearSelect>
|
||||
</span>
|
||||
@@ -0,0 +1,115 @@
|
||||
<script lang="ts">
|
||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||
import * as Calendar from "./index.js";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
import type { ButtonVariant } from "../button/button.svelte";
|
||||
import { isEqualMonth, type DateValue } from "@internationalized/date";
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
placeholder = $bindable(),
|
||||
class: className,
|
||||
weekdayFormat = "short",
|
||||
buttonVariant = "ghost",
|
||||
captionLayout = "label",
|
||||
locale = "en-US",
|
||||
months: monthsProp,
|
||||
years,
|
||||
monthFormat: monthFormatProp,
|
||||
yearFormat = "numeric",
|
||||
day,
|
||||
disableDaysOutsideMonth = false,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<CalendarPrimitive.RootProps> & {
|
||||
buttonVariant?: ButtonVariant;
|
||||
captionLayout?: "dropdown" | "dropdown-months" | "dropdown-years" | "label";
|
||||
months?: CalendarPrimitive.MonthSelectProps["months"];
|
||||
years?: CalendarPrimitive.YearSelectProps["years"];
|
||||
monthFormat?: CalendarPrimitive.MonthSelectProps["monthFormat"];
|
||||
yearFormat?: CalendarPrimitive.YearSelectProps["yearFormat"];
|
||||
day?: Snippet<[{ day: DateValue; outsideMonth: boolean }]>;
|
||||
} = $props();
|
||||
|
||||
const monthFormat = $derived.by(() => {
|
||||
if (monthFormatProp) return monthFormatProp;
|
||||
if (captionLayout.startsWith("dropdown")) return "short";
|
||||
return "long";
|
||||
});
|
||||
</script>
|
||||
|
||||
<!--
|
||||
Discriminated Unions + Destructing (required for bindable) do not
|
||||
get along, so we shut typescript up by casting `value` to `never`.
|
||||
-->
|
||||
<CalendarPrimitive.Root
|
||||
bind:value={value as never}
|
||||
bind:ref
|
||||
bind:placeholder
|
||||
{weekdayFormat}
|
||||
{disableDaysOutsideMonth}
|
||||
class={cn(
|
||||
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
|
||||
className
|
||||
)}
|
||||
{locale}
|
||||
{monthFormat}
|
||||
{yearFormat}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children({ months, weekdays })}
|
||||
<Calendar.Months>
|
||||
<Calendar.Nav>
|
||||
<Calendar.PrevButton variant={buttonVariant} />
|
||||
<Calendar.NextButton variant={buttonVariant} />
|
||||
</Calendar.Nav>
|
||||
{#each months as month, monthIndex (month)}
|
||||
<Calendar.Month>
|
||||
<Calendar.Header>
|
||||
<Calendar.Caption
|
||||
{captionLayout}
|
||||
months={monthsProp}
|
||||
{monthFormat}
|
||||
{years}
|
||||
{yearFormat}
|
||||
month={month.value}
|
||||
bind:placeholder
|
||||
{locale}
|
||||
{monthIndex}
|
||||
/>
|
||||
</Calendar.Header>
|
||||
<Calendar.Grid>
|
||||
<Calendar.GridHead>
|
||||
<Calendar.GridRow class="select-none">
|
||||
{#each weekdays as weekday (weekday)}
|
||||
<Calendar.HeadCell>
|
||||
{weekday.slice(0, 2)}
|
||||
</Calendar.HeadCell>
|
||||
{/each}
|
||||
</Calendar.GridRow>
|
||||
</Calendar.GridHead>
|
||||
<Calendar.GridBody>
|
||||
{#each month.weeks as weekDates (weekDates)}
|
||||
<Calendar.GridRow class="mt-2 w-full">
|
||||
{#each weekDates as date (date)}
|
||||
<Calendar.Cell {date} month={month.value}>
|
||||
{#if day}
|
||||
{@render day({
|
||||
day: date,
|
||||
outsideMonth: !isEqualMonth(date, month.value),
|
||||
})}
|
||||
{:else}
|
||||
<Calendar.Day />
|
||||
{/if}
|
||||
</Calendar.Cell>
|
||||
{/each}
|
||||
</Calendar.GridRow>
|
||||
{/each}
|
||||
</Calendar.GridBody>
|
||||
</Calendar.Grid>
|
||||
</Calendar.Month>
|
||||
{/each}
|
||||
</Calendar.Months>
|
||||
{/snippet}
|
||||
</CalendarPrimitive.Root>
|
||||
@@ -0,0 +1,40 @@
|
||||
import Root from "./calendar.svelte";
|
||||
import Cell from "./calendar-cell.svelte";
|
||||
import Day from "./calendar-day.svelte";
|
||||
import Grid from "./calendar-grid.svelte";
|
||||
import Header from "./calendar-header.svelte";
|
||||
import Months from "./calendar-months.svelte";
|
||||
import GridRow from "./calendar-grid-row.svelte";
|
||||
import Heading from "./calendar-heading.svelte";
|
||||
import GridBody from "./calendar-grid-body.svelte";
|
||||
import GridHead from "./calendar-grid-head.svelte";
|
||||
import HeadCell from "./calendar-head-cell.svelte";
|
||||
import NextButton from "./calendar-next-button.svelte";
|
||||
import PrevButton from "./calendar-prev-button.svelte";
|
||||
import MonthSelect from "./calendar-month-select.svelte";
|
||||
import YearSelect from "./calendar-year-select.svelte";
|
||||
import Month from "./calendar-month.svelte";
|
||||
import Nav from "./calendar-nav.svelte";
|
||||
import Caption from "./calendar-caption.svelte";
|
||||
|
||||
export {
|
||||
Day,
|
||||
Cell,
|
||||
Grid,
|
||||
Header,
|
||||
Months,
|
||||
GridRow,
|
||||
Heading,
|
||||
GridBody,
|
||||
GridHead,
|
||||
HeadCell,
|
||||
NextButton,
|
||||
PrevButton,
|
||||
Nav,
|
||||
Month,
|
||||
YearSelect,
|
||||
MonthSelect,
|
||||
Caption,
|
||||
//
|
||||
Root as Calendar,
|
||||
};
|
||||
@@ -0,0 +1,198 @@
|
||||
<script lang="ts">
|
||||
import Calendar from '$lib/components/ui/calendar/calendar.svelte';
|
||||
import * as Popover from '$lib/components/ui/popover';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import TimePicker from './time-picker.svelte';
|
||||
import { CalendarDate, type DateValue } from '@internationalized/date';
|
||||
import CalendarIcon from '@lucide/svelte/icons/calendar';
|
||||
import X from '@lucide/svelte/icons/x';
|
||||
|
||||
let {
|
||||
value = $bindable(''),
|
||||
withTime = false,
|
||||
includeSeconds = false,
|
||||
placeholder = withTime ? 'Pick date & time...' : 'Pick a date...',
|
||||
disabled = false,
|
||||
clearable = true,
|
||||
class: className = '',
|
||||
contentClass = '',
|
||||
open = $bindable(false),
|
||||
id = undefined,
|
||||
}: {
|
||||
value?: string;
|
||||
withTime?: boolean;
|
||||
includeSeconds?: boolean;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
clearable?: boolean;
|
||||
class?: string;
|
||||
contentClass?: string;
|
||||
open?: boolean;
|
||||
id?: string;
|
||||
} = $props();
|
||||
|
||||
let calendarValue = $state<DateValue | undefined>(undefined);
|
||||
let hours = $state(0);
|
||||
let minutes = $state(0);
|
||||
let seconds = $state(0);
|
||||
let lastEmittedValue = '';
|
||||
|
||||
// Synchronize incoming value -> internal state
|
||||
$effect(() => {
|
||||
if (value === lastEmittedValue) return;
|
||||
lastEmittedValue = value ?? '';
|
||||
|
||||
if (!value || !value.trim()) {
|
||||
calendarValue = undefined;
|
||||
hours = 0;
|
||||
minutes = 0;
|
||||
seconds = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const timestamp = Date.parse(value);
|
||||
if (!Number.isNaN(timestamp)) {
|
||||
const parsed = new Date(timestamp);
|
||||
calendarValue = new CalendarDate(
|
||||
parsed.getFullYear(),
|
||||
parsed.getMonth() + 1,
|
||||
parsed.getDate()
|
||||
);
|
||||
hours = parsed.getHours();
|
||||
minutes = parsed.getMinutes();
|
||||
seconds = parsed.getSeconds();
|
||||
} else {
|
||||
calendarValue = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
function emitChange() {
|
||||
if (!calendarValue) {
|
||||
lastEmittedValue = '';
|
||||
value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
let next = '';
|
||||
if (withTime) {
|
||||
const d = new Date(
|
||||
calendarValue.year,
|
||||
calendarValue.month - 1,
|
||||
calendarValue.day,
|
||||
hours,
|
||||
minutes,
|
||||
seconds
|
||||
);
|
||||
next = d.toISOString();
|
||||
} else {
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
next = `${calendarValue.year}-${pad(calendarValue.month)}-${pad(calendarValue.day)}`;
|
||||
}
|
||||
|
||||
lastEmittedValue = next;
|
||||
value = next;
|
||||
}
|
||||
|
||||
function handleDateChange(newDate: DateValue | undefined) {
|
||||
calendarValue = newDate;
|
||||
if (newDate) {
|
||||
emitChange();
|
||||
if (!withTime) {
|
||||
open = false;
|
||||
}
|
||||
} else {
|
||||
emitChange();
|
||||
}
|
||||
}
|
||||
|
||||
function handleTimeChange() {
|
||||
if (!calendarValue) {
|
||||
const now = new Date();
|
||||
calendarValue = new CalendarDate(now.getFullYear(), now.getMonth() + 1, now.getDate());
|
||||
}
|
||||
emitChange();
|
||||
}
|
||||
|
||||
function handleClear(e: MouseEvent) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
calendarValue = undefined;
|
||||
hours = 0;
|
||||
minutes = 0;
|
||||
seconds = 0;
|
||||
emitChange();
|
||||
}
|
||||
|
||||
const formattedDisplay = $derived.by(() => {
|
||||
if (!value || !value.trim()) return '';
|
||||
const ts = Date.parse(value);
|
||||
if (Number.isNaN(ts)) return value;
|
||||
const d = new Date(ts);
|
||||
try {
|
||||
if (withTime) {
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: includeSeconds ? 'medium' : 'short',
|
||||
}).format(d);
|
||||
}
|
||||
return new Intl.DateTimeFormat(undefined, { dateStyle: 'medium' }).format(d);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Popover.Root bind:open>
|
||||
<div class="relative flex items-center">
|
||||
<Popover.Trigger
|
||||
{id}
|
||||
{disabled}
|
||||
class={cn(
|
||||
'flex h-8 w-full items-center justify-start gap-2 rounded-md border border-input bg-background px-3 text-xs font-normal text-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 ring-offset-background',
|
||||
!formattedDisplay && 'text-muted-foreground',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<CalendarIcon class="size-3.5 shrink-0 text-muted-foreground" />
|
||||
<span class="truncate">
|
||||
{formattedDisplay || placeholder}
|
||||
</span>
|
||||
</Popover.Trigger>
|
||||
|
||||
{#if clearable && formattedDisplay && !disabled}
|
||||
<button
|
||||
type="button"
|
||||
class="absolute right-2 flex size-4 items-center justify-center rounded-sm text-muted-foreground transition-colors hover:text-foreground"
|
||||
onclick={handleClear}
|
||||
title="Clear date"
|
||||
>
|
||||
<X class="size-3" />
|
||||
<span class="sr-only">Clear</span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Popover.Content
|
||||
align="start"
|
||||
class={cn('w-auto border-border bg-popover p-0 shadow-lg', contentClass)}
|
||||
>
|
||||
<Calendar
|
||||
type="single"
|
||||
value={calendarValue}
|
||||
onValueChange={(val) => handleDateChange(val as DateValue | undefined)}
|
||||
class="p-3"
|
||||
/>
|
||||
|
||||
{#if withTime}
|
||||
<div class="border-t border-border/60 p-3 pt-2">
|
||||
<TimePicker
|
||||
bind:hours
|
||||
bind:minutes
|
||||
bind:seconds
|
||||
{includeSeconds}
|
||||
onchange={handleTimeChange}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
@@ -0,0 +1,11 @@
|
||||
import DatePicker from './date-picker.svelte';
|
||||
import TimePicker from './time-picker.svelte';
|
||||
|
||||
export {
|
||||
DatePicker,
|
||||
TimePicker,
|
||||
//
|
||||
DatePicker as Root,
|
||||
};
|
||||
|
||||
export default DatePicker;
|
||||
@@ -0,0 +1,154 @@
|
||||
<script lang="ts">
|
||||
import Button from '$lib/components/ui/button/button.svelte';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import Clock from '@lucide/svelte/icons/clock';
|
||||
|
||||
let {
|
||||
hours = $bindable(0),
|
||||
minutes = $bindable(0),
|
||||
seconds = $bindable(0),
|
||||
includeSeconds = false,
|
||||
disabled = false,
|
||||
class: className = '',
|
||||
onchange,
|
||||
}: {
|
||||
hours?: number;
|
||||
minutes?: number;
|
||||
seconds?: number;
|
||||
includeSeconds?: boolean;
|
||||
disabled?: boolean;
|
||||
class?: string;
|
||||
onchange?: () => void;
|
||||
} = $props();
|
||||
|
||||
const pad = (n: number) => String(Math.max(0, Math.floor(n))).padStart(2, '0');
|
||||
|
||||
function updateHours(val: number) {
|
||||
if (disabled) return;
|
||||
const clamped = Math.max(0, Math.min(23, Number.isNaN(val) ? 0 : val));
|
||||
if (hours !== clamped) {
|
||||
hours = clamped;
|
||||
onchange?.();
|
||||
}
|
||||
}
|
||||
|
||||
function updateMinutes(val: number) {
|
||||
if (disabled) return;
|
||||
const clamped = Math.max(0, Math.min(59, Number.isNaN(val) ? 0 : val));
|
||||
if (minutes !== clamped) {
|
||||
minutes = clamped;
|
||||
onchange?.();
|
||||
}
|
||||
}
|
||||
|
||||
function updateSeconds(val: number) {
|
||||
if (disabled) return;
|
||||
const clamped = Math.max(0, Math.min(59, Number.isNaN(val) ? 0 : val));
|
||||
if (seconds !== clamped) {
|
||||
seconds = clamped;
|
||||
onchange?.();
|
||||
}
|
||||
}
|
||||
|
||||
function setNow() {
|
||||
if (disabled) return;
|
||||
const now = new Date();
|
||||
hours = now.getHours();
|
||||
minutes = now.getMinutes();
|
||||
seconds = now.getSeconds();
|
||||
onchange?.();
|
||||
}
|
||||
|
||||
function setMidnight() {
|
||||
if (disabled) return;
|
||||
hours = 0;
|
||||
minutes = 0;
|
||||
seconds = 0;
|
||||
onchange?.();
|
||||
}
|
||||
|
||||
function setNoon() {
|
||||
if (disabled) return;
|
||||
hours = 12;
|
||||
minutes = 0;
|
||||
seconds = 0;
|
||||
onchange?.();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class={cn('space-y-2', className)}>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
|
||||
<Clock class="size-3.5" />
|
||||
<span>Time (24h)</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 font-mono text-xs">
|
||||
<label class="sr-only" for="tp-hours">Hours</label>
|
||||
<input
|
||||
id="tp-hours"
|
||||
type="number"
|
||||
min="0"
|
||||
max="23"
|
||||
{disabled}
|
||||
value={pad(hours)}
|
||||
oninput={(e) => updateHours(parseInt(e.currentTarget.value, 10))}
|
||||
class="h-7 w-11 rounded border border-input bg-background text-center text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:opacity-50 [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
||||
/>
|
||||
<span class="select-none font-bold text-muted-foreground">:</span>
|
||||
<label class="sr-only" for="tp-minutes">Minutes</label>
|
||||
<input
|
||||
id="tp-minutes"
|
||||
type="number"
|
||||
min="0"
|
||||
max="59"
|
||||
{disabled}
|
||||
value={pad(minutes)}
|
||||
oninput={(e) => updateMinutes(parseInt(e.currentTarget.value, 10))}
|
||||
class="h-7 w-11 rounded border border-input bg-background text-center text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:opacity-50 [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
||||
/>
|
||||
{#if includeSeconds}
|
||||
<span class="select-none font-bold text-muted-foreground">:</span>
|
||||
<label class="sr-only" for="tp-seconds">Seconds</label>
|
||||
<input
|
||||
id="tp-seconds"
|
||||
type="number"
|
||||
min="0"
|
||||
max="59"
|
||||
{disabled}
|
||||
value={pad(seconds)}
|
||||
oninput={(e) => updateSeconds(parseInt(e.currentTarget.value, 10))}
|
||||
class="h-7 w-11 rounded border border-input bg-background text-center text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:opacity-50 [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 pt-0.5">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
{disabled}
|
||||
class="h-6 px-2 text-[10px] text-muted-foreground hover:text-foreground"
|
||||
onclick={setNow}
|
||||
>
|
||||
Now
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
{disabled}
|
||||
class="h-6 px-2 text-[10px] text-muted-foreground hover:text-foreground"
|
||||
onclick={setMidnight}
|
||||
>
|
||||
00:00
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
{disabled}
|
||||
class="h-6 px-2 text-[10px] text-muted-foreground hover:text-foreground"
|
||||
onclick={setNoon}
|
||||
>
|
||||
12:00
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,19 @@
|
||||
import Root from "./popover.svelte";
|
||||
import Close from "./popover-close.svelte";
|
||||
import Content from "./popover-content.svelte";
|
||||
import Trigger from "./popover-trigger.svelte";
|
||||
import Portal from "./popover-portal.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Trigger,
|
||||
Close,
|
||||
Portal,
|
||||
//
|
||||
Root as Popover,
|
||||
Content as PopoverContent,
|
||||
Trigger as PopoverTrigger,
|
||||
Close as PopoverClose,
|
||||
Portal as PopoverPortal,
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Popover as PopoverPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: PopoverPrimitive.CloseProps = $props();
|
||||
</script>
|
||||
|
||||
<PopoverPrimitive.Close bind:ref data-slot="popover-close" {...restProps} />
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { Popover as PopoverPrimitive } from "bits-ui";
|
||||
import PopoverPortal from "./popover-portal.svelte";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
import type { ComponentProps } from "svelte";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
sideOffset = 4,
|
||||
align = "center",
|
||||
portalProps,
|
||||
...restProps
|
||||
}: PopoverPrimitive.ContentProps & {
|
||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof PopoverPortal>>;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<PopoverPortal {...portalProps}>
|
||||
<PopoverPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="popover-content"
|
||||
{sideOffset}
|
||||
{align}
|
||||
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",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
</PopoverPortal>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Popover as PopoverPrimitive } from "bits-ui";
|
||||
|
||||
let { ...restProps }: PopoverPrimitive.PortalProps = $props();
|
||||
</script>
|
||||
|
||||
<PopoverPrimitive.Portal {...restProps} />
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { Popover as PopoverPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: PopoverPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<PopoverPrimitive.Trigger
|
||||
bind:ref
|
||||
data-slot="popover-trigger"
|
||||
class={cn("", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Popover as PopoverPrimitive } from "bits-ui";
|
||||
|
||||
let { open = $bindable(false), ...restProps }: PopoverPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<PopoverPrimitive.Root bind:open {...restProps} />
|
||||
+1
-2
@@ -1,5 +1,4 @@
|
||||
import { browser } from '$app/environment';
|
||||
import Loading from '../screens/Loading.svelte';
|
||||
import { ezppfarm } from './api/ezpp';
|
||||
import type { Theme } from './themes';
|
||||
import type { Release } from './types';
|
||||
@@ -7,7 +6,7 @@ import { setGlobalVolume } from './utils';
|
||||
import type { Component } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const currentView = writable<Component>(Loading);
|
||||
export const currentView = writable<Component | null>(null);
|
||||
|
||||
export const platform = writable<string>('');
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { currentView, launcherVersion, trackingEnabled } from '$lib/global';
|
||||
import Loading from '../screens/Loading.svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
const View = $derived($currentView);
|
||||
const View = $derived($currentView ?? Loading);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
||||
Reference in new issue
Block a user