feature: show recipe edit form sheet
- move from edit directly in table to side-sheet instead Signed-off-by: pakintada@gmail.com <Pakin>
This commit is contained in:
parent
3f5eb8d07d
commit
e6d1ac9a99
36 changed files with 1307 additions and 71 deletions
|
|
@ -22,6 +22,7 @@ import { DragHandle } from './recipelist-table.svelte';
|
||||||
import { createRawSnippet } from 'svelte';
|
import { createRawSnippet } from 'svelte';
|
||||||
import RecipelistMatSelect from './recipelist-mat-select.svelte';
|
import RecipelistMatSelect from './recipelist-mat-select.svelte';
|
||||||
import { recipeDataEvent } from '$lib/core/stores/recipeStore';
|
import { recipeDataEvent } from '$lib/core/stores/recipeStore';
|
||||||
|
import RecipelistValueEditor from './recipelist-value-editor.svelte';
|
||||||
|
|
||||||
export type RecipelistMaterial = {
|
export type RecipelistMaterial = {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -122,5 +123,13 @@ export const columns: ColumnDef<RecipelistMaterial>[] = [
|
||||||
...row.original.values
|
...row.original.values
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'actions',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return renderComponent(RecipelistValueEditor, {
|
||||||
|
row_id: row.original.id
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
|
||||||
491
src/lib/components/recipe-details/recipelist-value-editor.svelte
Normal file
491
src/lib/components/recipe-details/recipelist-value-editor.svelte
Normal file
|
|
@ -0,0 +1,491 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { CupSodaIcon, PencilIcon, ChevronRightIcon, StarsIcon } from '@lucide/svelte/icons';
|
||||||
|
import Button from '$lib/components/ui/button/button.svelte';
|
||||||
|
import * as Select from '$lib/components/ui/select/index';
|
||||||
|
import * as Sheet from '$lib/components/ui/sheet/index';
|
||||||
|
import * as Field from '$lib/components/ui/field/index';
|
||||||
|
import * as Collapsible from '$lib/components/ui/collapsible/index';
|
||||||
|
import Input from '../ui/input/input.svelte';
|
||||||
|
import Textarea from '../ui/textarea/textarea.svelte';
|
||||||
|
|
||||||
|
import {
|
||||||
|
latestRecipeToppingData,
|
||||||
|
recipeDataEvent,
|
||||||
|
toppingGroupFromServerQuery,
|
||||||
|
toppingListFromServerQuery
|
||||||
|
} from '$lib/core/stores/recipeStore';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { addNotification } from '$lib/core/stores/noti';
|
||||||
|
import { get } from 'svelte/store';
|
||||||
|
import { ValueEvent } from './value_event';
|
||||||
|
import ScrollArea from '../ui/scroll-area/scroll-area.svelte';
|
||||||
|
|
||||||
|
let { row_id }: { row_id: number } = $props();
|
||||||
|
|
||||||
|
let current_editing_data: any = $state();
|
||||||
|
let changed_data: any = $state();
|
||||||
|
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
let toggledOpenPowder = $state(false);
|
||||||
|
let toggledOpenSyrup = $state(false);
|
||||||
|
let toggledOpenWater = $state(false);
|
||||||
|
let toggledOpenFeed = $state(false);
|
||||||
|
|
||||||
|
// --------------------------------------------------
|
||||||
|
|
||||||
|
let categories: any[] = $state([]);
|
||||||
|
let topping_lists: any[] = $state([]);
|
||||||
|
|
||||||
|
let selected_category_id = $state('');
|
||||||
|
let selected_topping_list_id = $state('');
|
||||||
|
|
||||||
|
const selected_category = $derived(categories.find((c) => c.groupID === selected_category_id));
|
||||||
|
const available_topping_lists = $derived(
|
||||||
|
selected_category
|
||||||
|
? topping_lists.filter((tpl) =>
|
||||||
|
selected_category.idInGroup
|
||||||
|
.split(',')
|
||||||
|
.map((x: string) => parseInt(x))
|
||||||
|
.includes(tpl.id)
|
||||||
|
)
|
||||||
|
: []
|
||||||
|
);
|
||||||
|
|
||||||
|
const selected_topping_list = $derived(
|
||||||
|
topping_lists.find((tpl) => tpl.id === selected_topping_list_id)
|
||||||
|
);
|
||||||
|
// let group_categories: any = $state({});
|
||||||
|
|
||||||
|
// let selected_topping_group = $state('');
|
||||||
|
// let selected_topping_list = $state('');
|
||||||
|
// let selectable_topping_list = $state<any[]>();
|
||||||
|
|
||||||
|
let value_event_state = $state<ValueEvent>(ValueEvent.NONE);
|
||||||
|
|
||||||
|
function getToppingSlotIndex(mat_id: number) {
|
||||||
|
return mat_id - 8110 - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEvents(event: { event_type: string; payload: any; index: number | undefined }) {
|
||||||
|
// console.log('triggered event', event.event_type, JSON.stringify(event.payload));
|
||||||
|
if (event.event_type === 'edit_mat_field_prep') {
|
||||||
|
// update value, do re-render
|
||||||
|
if (event.payload) {
|
||||||
|
current_editing_data = event.payload;
|
||||||
|
console.log(`GET requested data: ${JSON.stringify(current_editing_data)}`);
|
||||||
|
|
||||||
|
// default topping
|
||||||
|
if (
|
||||||
|
current_editing_data['mat_type'] === 'topping' &&
|
||||||
|
current_editing_data['current_topping_group'] &&
|
||||||
|
current_editing_data['toppings']
|
||||||
|
) {
|
||||||
|
let tg_default_data = current_editing_data['current_topping_group'];
|
||||||
|
let default_gid = tg_default_data['groupID'];
|
||||||
|
let default_tid = tg_default_data['idDefault'];
|
||||||
|
|
||||||
|
// get topping slot data
|
||||||
|
let idx = getToppingSlotIndex(current_editing_data['mat_id']);
|
||||||
|
let default_from_recipe = current_editing_data['toppings'][idx]['ListGroupID'][0];
|
||||||
|
|
||||||
|
selected_category_id = default_gid;
|
||||||
|
selected_topping_list_id = default_from_recipe ?? default_tid ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// default first open if has value
|
||||||
|
toggledOpenPowder =
|
||||||
|
current_editing_data.powder.gram > 0 || current_editing_data.powder.time > 0;
|
||||||
|
toggledOpenSyrup =
|
||||||
|
current_editing_data.syrup.gram > 0 || current_editing_data.syrup.time > 0;
|
||||||
|
toggledOpenWater =
|
||||||
|
current_editing_data.water.yield > 0 || current_editing_data.water.cold > 0;
|
||||||
|
toggledOpenFeed =
|
||||||
|
current_editing_data.feed.pattern > 0 || current_editing_data.feed.parameter > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestDataFromDisplay() {
|
||||||
|
console.log('sending request edit', row_id);
|
||||||
|
recipeDataEvent.set({
|
||||||
|
event_type: 'edit_mat_field',
|
||||||
|
payload: row_id,
|
||||||
|
index: row_id
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (current_editing_data === undefined) {
|
||||||
|
addNotification('ERR:Unable to edit');
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFieldValueChange(field_name: string[], new_value: any) {
|
||||||
|
console.log('change on', JSON.stringify(field_name), new_value);
|
||||||
|
|
||||||
|
// validate if field value changes
|
||||||
|
let curr_val;
|
||||||
|
for (let field of field_name) {
|
||||||
|
if (!curr_val) {
|
||||||
|
curr_val = current_editing_data[field];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
curr_val = curr_val[field];
|
||||||
|
} catch (e) {
|
||||||
|
console.error('try get field error: ', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let has_changed = curr_val !== new_value;
|
||||||
|
|
||||||
|
if (has_changed) {
|
||||||
|
if (value_event_state === ValueEvent.NONE) {
|
||||||
|
value_event_state = ValueEvent.EDITED;
|
||||||
|
|
||||||
|
// save change
|
||||||
|
let single_key = '';
|
||||||
|
for (let field of field_name) {
|
||||||
|
single_key += field + '_';
|
||||||
|
}
|
||||||
|
single_key = single_key.slice(0, single_key.length - 1);
|
||||||
|
|
||||||
|
console.log('save to key', single_key);
|
||||||
|
changed_data[single_key] = new_value;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// revert value in key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveEditingValue() {
|
||||||
|
console.log('saving value ...');
|
||||||
|
if (value_event_state === ValueEvent.EDITED) {
|
||||||
|
recipeDataEvent.set({
|
||||||
|
event_type: 'save_mat_field',
|
||||||
|
payload: current_editing_data,
|
||||||
|
index: row_id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleToppingGroupChange(v: any) {
|
||||||
|
console.log('change topping group', JSON.stringify(v));
|
||||||
|
selected_category_id = v.groupID;
|
||||||
|
// get default
|
||||||
|
selected_topping_list_id = v.idDefault ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleToppingListChange(v: any) {
|
||||||
|
console.log('Topping list chose: ', JSON.stringify(v));
|
||||||
|
selected_topping_list_id = v.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// recipelist-value-editor.svelte?t=1773541064272:57 GET requested data: {"mat_id":1002,"mat_type":"bean","mat_name":"medium-roasts (1002)","params":{"esp-v2-press-value":"24"},"toppings":[{"ListGroupID":[1,0,0,0],"defaultIDSelect":1,"groupID":"1","isUse":true},{"ListGroupID":[6,0,0,0],"defaultIDSelect":31,"groupID":"6","isUse":true},{"ListGroupID":[7,0,0,0],"defaultIDSelect":33,"groupID":"7","isUse":true},{"ListGroupID":[0,0,0,0],"defaultIDSelect":0,"groupID":"0","isUse":false},{"ListGroupID":[0,0,0,0],"defaultIDSelect":0,"groupID":"0","isUse":false},{"ListGroupID":[0,0,0,0],"defaultIDSelect":0,"groupID":"0","isUse":false},{"ListGroupID":[530,0,0,0],"defaultIDSelect":532,"groupID":"530","isUse":true},{"ListGroupID":[500,0,0,0],"defaultIDSelect":502,"groupID":"500","isUse":true}],"current_topping_list":[],"has_mix_ord":false,"feed":{"pattern":0,"parameter":0},"water":{"cold":0,"yield":30},"powder":{"gram":7,"time":9},"syrup":{"gram":0,"time":0},"stir_time":120}
|
||||||
|
// recipelist-value-editor.svelte?t=1773541064272:79 sending request edit 0
|
||||||
|
// recipelist-value.svelte:224 request edit mat
|
||||||
|
// recipelist-value-editor.svelte?t=1773541064272:57 GET requested data: {"mat_id":9501,"mat_type":"cup","mat_name":"CUP paper (9501)","params":{},"toppings":[{"ListGroupID":[1,0,0,0],"defaultIDSelect":1,"groupID":"1","isUse":true},{"ListGroupID":[6,0,0,0],"defaultIDSelect":31,"groupID":"6","isUse":true},{"ListGroupID":[7,0,0,0],"defaultIDSelect":33,"groupID":"7","isUse":true},{"ListGroupID":[0,0,0,0],"defaultIDSelect":0,"groupID":"0","isUse":false},{"ListGroupID":[0,0,0,0],"defaultIDSelect":0,"groupID":"0","isUse":false},{"ListGroupID":[0,0,0,0],"defaultIDSelect":0,"groupID":"0","isUse":false},{"ListGroupID":[530,0,0,0],"defaultIDSelect":532,"groupID":"530","isUse":true},{"ListGroupID":[500,0,0,0],"defaultIDSelect":502,"groupID":"500","isUse":true}],"current_topping_list":[],"has_mix_ord":false,"feed":{"pattern":0,"parameter":0},"water":{"cold":0,"yield":0},"powder":{"gram":0,"time":0},"syrup":{"gram":0,"time":0},"stir_time":0}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
categories = get(toppingGroupFromServerQuery);
|
||||||
|
topping_lists = get(toppingListFromServerQuery);
|
||||||
|
|
||||||
|
return recipeDataEvent.subscribe((event) => {
|
||||||
|
if (event !== null && event.index !== undefined && event.index === row_id) {
|
||||||
|
handleEvents(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Sheet.Root>
|
||||||
|
<Sheet.Trigger>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="icon"
|
||||||
|
class="relative size-8 p-0"
|
||||||
|
onclick={() => requestDataFromDisplay()}
|
||||||
|
>
|
||||||
|
<PencilIcon />
|
||||||
|
</Button>
|
||||||
|
</Sheet.Trigger>
|
||||||
|
<Sheet.Content side="right">
|
||||||
|
<Sheet.Header>
|
||||||
|
<Sheet.Title>Value Editor</Sheet.Title>
|
||||||
|
<Sheet.Description>
|
||||||
|
Make changes to current order of material. Click save when you're done.
|
||||||
|
</Sheet.Description>
|
||||||
|
</Sheet.Header>
|
||||||
|
|
||||||
|
<!-- form -->
|
||||||
|
<div class="max-w-md min-w-sm p-4">
|
||||||
|
<form>
|
||||||
|
<Field.Group>
|
||||||
|
<Field.Set>
|
||||||
|
<Field.Group>
|
||||||
|
<Field.Field>
|
||||||
|
<Field.Label for="material_name">Current Material</Field.Label>
|
||||||
|
<Input
|
||||||
|
id="material_name"
|
||||||
|
placeholder="Material Name"
|
||||||
|
value={`${current_editing_data.mat_name}`}
|
||||||
|
disabled={true}
|
||||||
|
/>
|
||||||
|
<Field.Description>
|
||||||
|
<div class="my-2 flex gap-2">
|
||||||
|
Material Type: {current_editing_data.mat_type.toString().toUpperCase()}
|
||||||
|
{#if current_editing_data.mat_type === 'cup'}
|
||||||
|
<CupSodaIcon />
|
||||||
|
{:else if current_editing_data.mat_type === 'topping'}
|
||||||
|
<StarsIcon />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Field.Description>
|
||||||
|
</Field.Field>
|
||||||
|
</Field.Group>
|
||||||
|
</Field.Set>
|
||||||
|
|
||||||
|
<ScrollArea class="h-[60vh] w-full" type="always">
|
||||||
|
<!-- topping layout -->
|
||||||
|
<div
|
||||||
|
class={current_editing_data.mat_type === 'topping'
|
||||||
|
? 'my-4 grid grid-cols-2 gap-4'
|
||||||
|
: 'hidden'}
|
||||||
|
>
|
||||||
|
<Field.Field>
|
||||||
|
<!-- topping group -->
|
||||||
|
<Field.Label for="topping_group">Group</Field.Label>
|
||||||
|
<Select.Root
|
||||||
|
type="single"
|
||||||
|
value={selected_category_id}
|
||||||
|
onValueChange={(v: any) => handleToppingGroupChange(v)}
|
||||||
|
>
|
||||||
|
<Select.Trigger id="topping_group">
|
||||||
|
<span>
|
||||||
|
{selected_category?.otherName ?? selected_category?.name ?? 'Select Category'}
|
||||||
|
</span>
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Content>
|
||||||
|
{#each categories as tg}
|
||||||
|
<Select.Item value={tg}>{tg.otherName ?? tg.name}</Select.Item>
|
||||||
|
{/each}
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
|
</Field.Field>
|
||||||
|
<Field.Field>
|
||||||
|
<!-- topping list -->
|
||||||
|
<Field.Label for="topping_list">Select</Field.Label>
|
||||||
|
<Select.Root
|
||||||
|
type="single"
|
||||||
|
disabled={!selected_category_id}
|
||||||
|
value={selected_topping_list_id}
|
||||||
|
onValueChange={(v: any) => handleToppingListChange(v)}
|
||||||
|
>
|
||||||
|
<Select.Trigger id="topping_list">
|
||||||
|
<span>
|
||||||
|
{selected_topping_list?.otherName ??
|
||||||
|
selected_topping_list?.name ??
|
||||||
|
'Select Topping'}
|
||||||
|
</span>
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Content>
|
||||||
|
{#each available_topping_lists as tl}
|
||||||
|
<Select.Item value={tl}>{tl.otherName ?? tl.name}</Select.Item>
|
||||||
|
{/each}
|
||||||
|
</Select.Content>
|
||||||
|
</Select.Root>
|
||||||
|
</Field.Field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- main fields -->
|
||||||
|
<Collapsible.Root class="group/collapsible" open={toggledOpenPowder}>
|
||||||
|
<Collapsible.Trigger>
|
||||||
|
<div
|
||||||
|
class="flex flex-row items-center justify-between gap-4 rounded-xl p-2 text-sm text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
||||||
|
>
|
||||||
|
Powder
|
||||||
|
<ChevronRightIcon
|
||||||
|
class="ms-auto transition-transform group-data-[state=open]/collapsible:rotate-90"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
<Collapsible.Content>
|
||||||
|
<div class="grid grid-cols-2 gap-2 rounded-3xl border p-4">
|
||||||
|
<!-- powder -->
|
||||||
|
<Field.Field>
|
||||||
|
<Field.Label for="powder_gram">Powder (g)</Field.Label>
|
||||||
|
<Input
|
||||||
|
id="powder_gram"
|
||||||
|
placeholder="Powder material in gram"
|
||||||
|
value={`${current_editing_data.powder.gram}`}
|
||||||
|
onchange={(v) =>
|
||||||
|
onFieldValueChange(['powder', 'gram'], v.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
</Field.Field>
|
||||||
|
<Field.Field>
|
||||||
|
<Field.Label for="powder_time">Powder (sec)</Field.Label>
|
||||||
|
<Input
|
||||||
|
id="powder_time"
|
||||||
|
placeholder="Powder material released by seconds"
|
||||||
|
value={`${current_editing_data.powder.time}`}
|
||||||
|
onchange={(v) =>
|
||||||
|
onFieldValueChange(['powder', 'time'], v.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
</Field.Field>
|
||||||
|
</div>
|
||||||
|
</Collapsible.Content>
|
||||||
|
</Collapsible.Root>
|
||||||
|
<Field.Separator />
|
||||||
|
|
||||||
|
<Collapsible.Root class="group/collapsible" open={toggledOpenSyrup}>
|
||||||
|
<Collapsible.Trigger>
|
||||||
|
<div
|
||||||
|
class="flex flex-row items-center justify-between gap-4 rounded-xl p-2 text-sm text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
||||||
|
>
|
||||||
|
Syrup
|
||||||
|
<ChevronRightIcon
|
||||||
|
class="ms-auto transition-transform group-data-[state=open]/collapsible:rotate-90"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
<Collapsible.Content>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<!-- syrup -->
|
||||||
|
<Field.Field>
|
||||||
|
<Field.Label for="syrup_gram">Syrup (g)</Field.Label>
|
||||||
|
<Input
|
||||||
|
id="syrup_gram"
|
||||||
|
placeholder="Syrup material in gram"
|
||||||
|
value={`${current_editing_data.syrup.gram}`}
|
||||||
|
onchange={(v) => onFieldValueChange(['syrup', 'gram'], v.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
</Field.Field>
|
||||||
|
<Field.Field>
|
||||||
|
<Field.Label for="syrup_time">Syrup (sec)</Field.Label>
|
||||||
|
<Input
|
||||||
|
id="syrup_time"
|
||||||
|
placeholder="Syrup material released by seconds"
|
||||||
|
value={`${current_editing_data.syrup.time}`}
|
||||||
|
onchange={(v) => onFieldValueChange(['syrup', 'time'], v.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
</Field.Field>
|
||||||
|
</div>
|
||||||
|
</Collapsible.Content>
|
||||||
|
</Collapsible.Root>
|
||||||
|
<Field.Separator />
|
||||||
|
|
||||||
|
<Collapsible.Root class="group/collapsible" open={toggledOpenWater}>
|
||||||
|
<Collapsible.Trigger>
|
||||||
|
<div
|
||||||
|
class="flex flex-row items-center justify-between gap-4 rounded-xl p-2 text-sm text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
||||||
|
>
|
||||||
|
Water
|
||||||
|
<ChevronRightIcon
|
||||||
|
class="ms-auto transition-transform group-data-[state=open]/collapsible:rotate-90"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
<Collapsible.Content>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<!-- water -->
|
||||||
|
<Field.Field>
|
||||||
|
<Field.Label for="water_hot">Water (Hot)</Field.Label>
|
||||||
|
<Input
|
||||||
|
id="water_hot"
|
||||||
|
value={`${current_editing_data.water.yield}`}
|
||||||
|
onchange={(v) =>
|
||||||
|
onFieldValueChange(['water', 'yield'], v.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
</Field.Field>
|
||||||
|
<Field.Field>
|
||||||
|
<Field.Label for="water_cold">Water (Cold)</Field.Label>
|
||||||
|
<Input
|
||||||
|
id="water_cold"
|
||||||
|
value={`${current_editing_data.water.cold}`}
|
||||||
|
onchange={(v) => onFieldValueChange(['water', 'cold'], v.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
</Field.Field>
|
||||||
|
</div>
|
||||||
|
</Collapsible.Content>
|
||||||
|
</Collapsible.Root>
|
||||||
|
<Field.Separator />
|
||||||
|
|
||||||
|
<Collapsible.Root class="group/collapsible" open={toggledOpenFeed}>
|
||||||
|
<Collapsible.Trigger>
|
||||||
|
<div
|
||||||
|
class="flex flex-row items-center justify-between gap-4 rounded-xl p-2 text-sm text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
||||||
|
>
|
||||||
|
Feed
|
||||||
|
<ChevronRightIcon
|
||||||
|
class="ms-auto transition-transform group-data-[state=open]/collapsible:rotate-90"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
<Collapsible.Content>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<!-- feed -->
|
||||||
|
<Field.Field>
|
||||||
|
<Field.Label for="feed_pattern">Feed Pattern</Field.Label>
|
||||||
|
<Input
|
||||||
|
id="feed_pattern"
|
||||||
|
value={`${current_editing_data.feed.pattern}`}
|
||||||
|
onchange={(v) =>
|
||||||
|
onFieldValueChange(['feed', 'pattern'], v.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
</Field.Field>
|
||||||
|
<Field.Field>
|
||||||
|
<Field.Label for="feed_param">Feed Level</Field.Label>
|
||||||
|
<Input
|
||||||
|
id="feed_param"
|
||||||
|
value={`${current_editing_data.feed.parameter}`}
|
||||||
|
onchange={(v) =>
|
||||||
|
onFieldValueChange(['feed', 'parameter'], v.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
</Field.Field>
|
||||||
|
</div>
|
||||||
|
</Collapsible.Content>
|
||||||
|
</Collapsible.Root>
|
||||||
|
<Field.Separator />
|
||||||
|
|
||||||
|
<Field.Field class="my-4">
|
||||||
|
<Field.Label for="stir_time">Stirring time</Field.Label>
|
||||||
|
<Input
|
||||||
|
id="stir_time"
|
||||||
|
value={`${current_editing_data.stir_time}`}
|
||||||
|
onchange={(v) => onFieldValueChange(['stir_time'], v.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<Field.Description>
|
||||||
|
<div class="my-2 flex gap-2"></div>
|
||||||
|
</Field.Description>
|
||||||
|
</Field.Field>
|
||||||
|
|
||||||
|
<Field.Separator />
|
||||||
|
<Field.Set>
|
||||||
|
<Field.Group>
|
||||||
|
<Field.Field class="my-2">
|
||||||
|
<Field.Label for="string_params">Parameters</Field.Label>
|
||||||
|
<Textarea
|
||||||
|
id="string_params"
|
||||||
|
placeholder="Additional parameters i.e. press value, etc."
|
||||||
|
class="resize-none"
|
||||||
|
value={JSON.stringify(current_editing_data.params)}
|
||||||
|
/>
|
||||||
|
<Field.Description>
|
||||||
|
Special keys/values used for controlling specific behavior
|
||||||
|
</Field.Description>
|
||||||
|
</Field.Field>
|
||||||
|
</Field.Group>
|
||||||
|
</Field.Set>
|
||||||
|
</ScrollArea>
|
||||||
|
<!-- final -->
|
||||||
|
<Field.Field orientation="horizontal">
|
||||||
|
<Button type="button" onclick={() => saveEditingValue()}>Save</Button>
|
||||||
|
<Button variant="outline" type="button">Cancel</Button>
|
||||||
|
</Field.Field>
|
||||||
|
</Field.Group>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Sheet.Content>
|
||||||
|
</Sheet.Root>
|
||||||
|
|
@ -46,21 +46,6 @@
|
||||||
let currentStringParams: any = $state({});
|
let currentStringParams: any = $state({});
|
||||||
let currentMaterialInt: number = $state(0);
|
let currentMaterialInt: number = $state(0);
|
||||||
|
|
||||||
// All ui states
|
|
||||||
let showBlenderParam = $state(false);
|
|
||||||
let showMixParam = $state(false);
|
|
||||||
let showSyrupParam = $state(false);
|
|
||||||
let showWaterParam = $state(false);
|
|
||||||
let showPowderParam = $state(false);
|
|
||||||
let showIceParam = $state(false);
|
|
||||||
let showAdjustGrinder = $state(false);
|
|
||||||
let showEspressoParam = $state(false);
|
|
||||||
let showToppingIdx = $state(false);
|
|
||||||
let showEquipmentLayout = $state(false);
|
|
||||||
let showToppingParam = $state(false);
|
|
||||||
let showSubtractPreWater = $state(false);
|
|
||||||
let showCleanOptionParam = $state(false);
|
|
||||||
|
|
||||||
// toppings
|
// toppings
|
||||||
let currentToppings: any = $state([]);
|
let currentToppings: any = $state([]);
|
||||||
// current topping of this row
|
// current topping of this row
|
||||||
|
|
@ -169,6 +154,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCurrentSelectedToppingList() {
|
function getCurrentSelectedToppingList() {
|
||||||
|
// FIXME: show unknown on preview
|
||||||
|
|
||||||
let current_selected = selectableToppingInGroup.find(
|
let current_selected = selectableToppingInGroup.find(
|
||||||
(x) => x.id === currentToppings[getToppingSlot()]['ListGroupID'][0]
|
(x) => x.id === currentToppings[getToppingSlot()]['ListGroupID'][0]
|
||||||
);
|
);
|
||||||
|
|
@ -216,10 +203,32 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleEvents(event: { event_type: string; payload: any; index: number | undefined }) {
|
function handleEvents(event: { event_type: string; payload: any; index: number | undefined }) {
|
||||||
console.log('triggered event', event.event_type, JSON.stringify(event.payload));
|
// console.log('triggered event', event.event_type, JSON.stringify(event.payload));
|
||||||
if (event.event_type === 'mat_change') {
|
if (event.event_type === 'mat_change') {
|
||||||
// update value, do re-render
|
// update value, do re-render
|
||||||
initialize();
|
initialize();
|
||||||
|
} else if (event.event_type === 'edit_mat_field') {
|
||||||
|
console.log('request edit mat');
|
||||||
|
// pack all shown data
|
||||||
|
recipeDataEvent.set({
|
||||||
|
event_type: 'edit_mat_field_prep',
|
||||||
|
payload: {
|
||||||
|
mat_id: currentMaterialInt,
|
||||||
|
mat_type: currentMaterialType,
|
||||||
|
mat_name: mat_id,
|
||||||
|
params: currentStringParams,
|
||||||
|
toppings: currentToppings,
|
||||||
|
current_topping_group: currentToppingInRow,
|
||||||
|
current_topping_list: selectableToppingInGroup,
|
||||||
|
has_mix_ord: hasMixOrder,
|
||||||
|
feed,
|
||||||
|
water,
|
||||||
|
powder,
|
||||||
|
syrup,
|
||||||
|
stir_time
|
||||||
|
},
|
||||||
|
index: row_uid
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -230,7 +239,7 @@
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
initialize();
|
initialize();
|
||||||
unsubRecipeDataEvent = recipeDataEvent.subscribe((event) => {
|
unsubRecipeDataEvent = recipeDataEvent.subscribe((event) => {
|
||||||
if (event && event.index && event.index === row_uid) {
|
if (event !== null && event.index !== undefined && event.index === row_uid) {
|
||||||
// has some event
|
// has some event
|
||||||
handleEvents(event);
|
handleEvents(event);
|
||||||
}
|
}
|
||||||
|
|
@ -244,27 +253,29 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if currentMaterialType === 'topping'}
|
<div>
|
||||||
<!-- do topping layout -->
|
{#if currentMaterialType === 'topping'}
|
||||||
<div>
|
<!-- do topping layout -->
|
||||||
<!-- get name of topping -->
|
<div>
|
||||||
<div class="mx-auto my-4 flex flex-row gap-8">
|
<!-- get name of topping -->
|
||||||
<!-- popup selector for topping group -->
|
<div class="mx-auto my-4 flex flex-row gap-8">
|
||||||
<Button variant="default">{getCurrentSelectedToppingGroup()}</Button>
|
<p class="text-muted-foreground">
|
||||||
<!-- popup selector for topping list -->
|
<b>
|
||||||
<Button variant="default">{getCurrentSelectedToppingList()}</Button>
|
{getCurrentSelectedToppingGroup()}
|
||||||
|
</b>, {getCurrentSelectedToppingList()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{:else}
|
||||||
{:else}
|
<div>
|
||||||
<div>
|
<div class="flex w-full flex-row gap-4">
|
||||||
<div class="flex w-full flex-row gap-4">
|
<!-- string param -->
|
||||||
<!-- string param -->
|
{#if !hasMixOrder}
|
||||||
{#if !hasMixOrder}
|
<!-- check if param is esp-v2-press-value -->
|
||||||
<!-- check if param is esp-v2-press-value -->
|
|
||||||
|
|
||||||
{#if currentStringParams['esp-v2-press-value']}
|
{#if currentStringParams['esp-v2-press-value']}
|
||||||
<div class="space-y-1">
|
<div class="my-4">
|
||||||
<Label class="font-bold" for={`esp_v2_press_${row_uid}`}>Press</Label>
|
<!-- <Label class="font-bold" for={`esp_v2_press_${row_uid}`}>Press</Label>
|
||||||
<div class="flex flex-row items-center space-x-2 text-center">
|
<div class="flex flex-row items-center space-x-2 text-center">
|
||||||
<Input
|
<Input
|
||||||
class="w-16"
|
class="w-16"
|
||||||
|
|
@ -275,13 +286,18 @@
|
||||||
triggerEditChange(`${v.currentTarget.id}=${v.currentTarget.value}`)}
|
triggerEditChange(`${v.currentTarget.id}=${v.currentTarget.value}`)}
|
||||||
/>
|
/>
|
||||||
<p>mA</p>
|
<p>mA</p>
|
||||||
</div>
|
</div> -->
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if water.yield > 0}
|
<p class="text-muted-foreground">
|
||||||
<div class="space-y-1">
|
<b> Press </b>
|
||||||
<Label class="font-bold" for={`water_yield_volume_${row_uid}`}>Hot</Label>
|
{currentStringParams['esp-v2-press-value']} mA
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if water.yield > 0}
|
||||||
|
<div class="my-4">
|
||||||
|
<!-- <Label class="font-bold" for={`water_yield_volume_${row_uid}`}>Hot</Label>
|
||||||
<div class="flex flex-row items-center space-x-2 text-center">
|
<div class="flex flex-row items-center space-x-2 text-center">
|
||||||
<Input
|
<Input
|
||||||
class="w-16"
|
class="w-16"
|
||||||
|
|
@ -292,13 +308,18 @@
|
||||||
triggerEditChange(`${v.currentTarget.id}=${v.currentTarget.value}`)}
|
triggerEditChange(`${v.currentTarget.id}=${v.currentTarget.value}`)}
|
||||||
/>
|
/>
|
||||||
<p>ml</p>
|
<p>ml</p>
|
||||||
</div>
|
</div> -->
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if water.cold > 0}
|
<p class="text-muted-foreground">
|
||||||
<div class="space-y-1">
|
<b> Hot </b>
|
||||||
<Label class="font-bold" for={`water_cold_volume_${row_uid}`}>Cold</Label>
|
{water.yield} ml
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if water.cold > 0}
|
||||||
|
<div class="my-4">
|
||||||
|
<!-- <Label class="font-bold" for={`water_cold_volume_${row_uid}`}>Cold</Label>
|
||||||
<div class="flex flex-row items-center space-x-2 text-center">
|
<div class="flex flex-row items-center space-x-2 text-center">
|
||||||
<Input
|
<Input
|
||||||
class="w-16"
|
class="w-16"
|
||||||
|
|
@ -309,13 +330,17 @@
|
||||||
triggerEditChange(`${v.currentTarget.id}=${v.currentTarget.value}`)}
|
triggerEditChange(`${v.currentTarget.id}=${v.currentTarget.value}`)}
|
||||||
/>
|
/>
|
||||||
<p>ml</p>
|
<p>ml</p>
|
||||||
|
</div> -->
|
||||||
|
<p class="text-muted-foreground">
|
||||||
|
<b> Hot </b>
|
||||||
|
{water.cold} ml
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if currentMaterialType !== 'cup' && currentMaterialType !== 'topping' && stir_time > 0}
|
{#if currentMaterialType !== 'cup' && currentMaterialType !== 'topping' && stir_time > 0}
|
||||||
<div class="space-y-1">
|
<div class="my-4">
|
||||||
<Label class="font-bold" for={`stir_time_${row_uid}`}>{getStirTimeName()}</Label>
|
<!-- <Label class="font-bold" for={`stir_time_${row_uid}`}>{getStirTimeName()}</Label>
|
||||||
<div class="flex flex-row items-center space-x-2 text-center">
|
<div class="flex flex-row items-center space-x-2 text-center">
|
||||||
<Input
|
<Input
|
||||||
class="w-16"
|
class="w-16"
|
||||||
|
|
@ -326,15 +351,19 @@
|
||||||
triggerEditChange(`${v.currentTarget.id}=${v.currentTarget.value}`)}
|
triggerEditChange(`${v.currentTarget.id}=${v.currentTarget.value}`)}
|
||||||
/>
|
/>
|
||||||
<p>sec</p>
|
<p>sec</p>
|
||||||
|
</div> -->
|
||||||
|
<p class="text-muted-foreground">
|
||||||
|
<b>{getStirTimeName()}</b>
|
||||||
|
{stir_time / 10} sec(s)
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- display powder/syrup -->
|
<!-- display powder/syrup -->
|
||||||
{#if currentMaterialType === 'syrup' || currentMaterialType === 'powder' || currentMaterialType === 'bean'}
|
{#if currentMaterialType === 'syrup' || currentMaterialType === 'powder' || currentMaterialType === 'bean'}
|
||||||
<div class="space-y-1">
|
<div class="my-4">
|
||||||
<Label class="font-bold" for={`powder_syrup_volume_${row_uid}`}>Volume</Label>
|
<!-- <Label class="font-bold" for={`powder_syrup_volume_${row_uid}`}>Volume</Label>
|
||||||
<div class="flex flex-row items-center space-x-2 text-center">
|
<div class="flex flex-row items-center space-x-2 text-center">
|
||||||
<Input
|
<Input
|
||||||
class="w-16"
|
class="w-16"
|
||||||
|
|
@ -344,17 +373,23 @@
|
||||||
onchangecapture={(v) =>
|
onchangecapture={(v) =>
|
||||||
triggerEditChange(`${v.currentTarget.id}=${v.currentTarget.value}`)}
|
triggerEditChange(`${v.currentTarget.id}=${v.currentTarget.value}`)}
|
||||||
/>
|
/>
|
||||||
<p>gram</p>
|
<p>gram</p> -->
|
||||||
|
<p class="text-muted-foreground">
|
||||||
|
<b> Volume </b>
|
||||||
|
{getPowderSyrupValue()} gram
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<!-- feed pattern -->
|
||||||
|
{#if feed.parameter > 0 || feed.pattern > 0}
|
||||||
|
<div class="mx-auto my-4 flex items-center gap-8 text-center">
|
||||||
|
<p class="text-muted-foreground">Style {feed.pattern}</p>
|
||||||
|
<p class="text-muted-foreground">Level {feed.parameter}</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<!-- feed pattern -->
|
{/if}
|
||||||
{#if feed.parameter > 0 || feed.pattern > 0}
|
|
||||||
<div class="mx-auto my-4 flex items-center gap-8 text-center">
|
<!-- show sheet -->
|
||||||
<Button variant="outline">Style {feed.pattern}</Button>
|
</div>
|
||||||
<Button variant="outline">Level {feed.parameter}</Button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
|
||||||
6
src/lib/components/recipe-details/value_event.ts
Normal file
6
src/lib/components/recipe-details/value_event.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
enum ValueEvent {
|
||||||
|
NONE,
|
||||||
|
EDITED
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ValueEvent };
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Collapsible as CollapsiblePrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { ref = $bindable(null), ...restProps }: CollapsiblePrimitive.ContentProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CollapsiblePrimitive.Content bind:ref data-slot="collapsible-content" {...restProps} />
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Collapsible as CollapsiblePrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { ref = $bindable(null), ...restProps }: CollapsiblePrimitive.TriggerProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CollapsiblePrimitive.Trigger bind:ref data-slot="collapsible-trigger" {...restProps} />
|
||||||
11
src/lib/components/ui/collapsible/collapsible.svelte
Normal file
11
src/lib/components/ui/collapsible/collapsible.svelte
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Collapsible as CollapsiblePrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
open = $bindable(false),
|
||||||
|
...restProps
|
||||||
|
}: CollapsiblePrimitive.RootProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CollapsiblePrimitive.Root bind:ref bind:open data-slot="collapsible" {...restProps} />
|
||||||
13
src/lib/components/ui/collapsible/index.ts
Normal file
13
src/lib/components/ui/collapsible/index.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import Root from "./collapsible.svelte";
|
||||||
|
import Trigger from "./collapsible-trigger.svelte";
|
||||||
|
import Content from "./collapsible-content.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Content,
|
||||||
|
Trigger,
|
||||||
|
//
|
||||||
|
Root as Collapsible,
|
||||||
|
Content as CollapsibleContent,
|
||||||
|
Trigger as CollapsibleTrigger,
|
||||||
|
};
|
||||||
20
src/lib/components/ui/field/field-content.svelte
Normal file
20
src/lib/components/ui/field/field-content.svelte
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<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<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="field-content"
|
||||||
|
class={cn("group/field-content flex flex-1 flex-col gap-1.5 leading-snug", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
25
src/lib/components/ui/field/field-description.svelte
Normal file
25
src/lib/components/ui/field/field-description.svelte
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
<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<HTMLParagraphElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="field-description"
|
||||||
|
class={cn(
|
||||||
|
"text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance",
|
||||||
|
"last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5",
|
||||||
|
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</p>
|
||||||
58
src/lib/components/ui/field/field-error.svelte
Normal file
58
src/lib/components/ui/field/field-error.svelte
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import type { Snippet } from "svelte";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
errors,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||||
|
children?: Snippet;
|
||||||
|
errors?: { message?: string }[];
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const hasContent = $derived.by(() => {
|
||||||
|
// has slotted error
|
||||||
|
if (children) return true;
|
||||||
|
|
||||||
|
// no errors
|
||||||
|
if (!errors || errors.length === 0) return false;
|
||||||
|
|
||||||
|
// has an error but no message
|
||||||
|
if (errors.length === 1 && !errors[0]?.message) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const isMultipleErrors = $derived(errors && errors.length > 1);
|
||||||
|
const singleErrorMessage = $derived(errors && errors.length === 1 && errors[0]?.message);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if hasContent}
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
role="alert"
|
||||||
|
data-slot="field-error"
|
||||||
|
class={cn("text-destructive text-sm font-normal", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{#if children}
|
||||||
|
{@render children()}
|
||||||
|
{:else if singleErrorMessage}
|
||||||
|
{singleErrorMessage}
|
||||||
|
{:else if isMultipleErrors}
|
||||||
|
<ul class="ms-4 flex list-disc flex-col gap-1">
|
||||||
|
{#each errors ?? [] as error, index (index)}
|
||||||
|
{#if error?.message}
|
||||||
|
<li>{error.message}</li>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
23
src/lib/components/ui/field/field-group.svelte
Normal file
23
src/lib/components/ui/field/field-group.svelte
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<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<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="field-group"
|
||||||
|
class={cn(
|
||||||
|
"group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
26
src/lib/components/ui/field/field-label.svelte
Normal file
26
src/lib/components/ui/field/field-label.svelte
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Label } from "$lib/components/ui/label/index.js";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
import type { ComponentProps } from "svelte";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: ComponentProps<typeof Label> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Label
|
||||||
|
bind:ref
|
||||||
|
data-slot="field-label"
|
||||||
|
class={cn(
|
||||||
|
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
|
||||||
|
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4",
|
||||||
|
"has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</Label>
|
||||||
29
src/lib/components/ui/field/field-legend.svelte
Normal file
29
src/lib/components/ui/field/field-legend.svelte
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
variant = "legend",
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLLegendElement>> & {
|
||||||
|
variant?: "legend" | "label";
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<legend
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="field-legend"
|
||||||
|
data-variant={variant}
|
||||||
|
class={cn(
|
||||||
|
"mb-3 font-medium",
|
||||||
|
"data-[variant=legend]:text-base",
|
||||||
|
"data-[variant=label]:text-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</legend>
|
||||||
38
src/lib/components/ui/field/field-separator.svelte
Normal file
38
src/lib/components/ui/field/field-separator.svelte
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Separator } from "$lib/components/ui/separator/index.js";
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import type { Snippet } from "svelte";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||||
|
children?: Snippet;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const hasContent = $derived(!!children);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="field-separator"
|
||||||
|
data-content={hasContent}
|
||||||
|
class={cn(
|
||||||
|
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
<Separator class="absolute inset-0 top-1/2" />
|
||||||
|
{#if children}
|
||||||
|
<span
|
||||||
|
class="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
|
||||||
|
data-slot="field-separator-content"
|
||||||
|
>
|
||||||
|
{@render children()}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
24
src/lib/components/ui/field/field-set.svelte
Normal file
24
src/lib/components/ui/field/field-set.svelte
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLFieldsetAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLFieldsetAttributes> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<fieldset
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="field-set"
|
||||||
|
class={cn(
|
||||||
|
"flex flex-col gap-6",
|
||||||
|
"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</fieldset>
|
||||||
23
src/lib/components/ui/field/field-title.svelte
Normal file
23
src/lib/components/ui/field/field-title.svelte
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<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<HTMLDivElement>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="field-title"
|
||||||
|
class={cn(
|
||||||
|
"flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
53
src/lib/components/ui/field/field.svelte
Normal file
53
src/lib/components/ui/field/field.svelte
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
<script lang="ts" module>
|
||||||
|
import { tv, type VariantProps } from "tailwind-variants";
|
||||||
|
|
||||||
|
export const fieldVariants = tv({
|
||||||
|
base: "group/field data-[invalid=true]:text-destructive flex w-full gap-3",
|
||||||
|
variants: {
|
||||||
|
orientation: {
|
||||||
|
vertical: "flex-col [&>*]:w-full [&>.sr-only]:w-auto",
|
||||||
|
horizontal: [
|
||||||
|
"flex-row items-center",
|
||||||
|
"[&>[data-slot=field-label]]:flex-auto",
|
||||||
|
"has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
||||||
|
],
|
||||||
|
responsive: [
|
||||||
|
"flex-col @md/field-group:flex-row @md/field-group:items-center [&>*]:w-full @md/field-group:[&>*]:w-auto [&>.sr-only]:w-auto",
|
||||||
|
"@md/field-group:[&>[data-slot=field-label]]:flex-auto",
|
||||||
|
"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
orientation: "vertical",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type FieldOrientation = VariantProps<typeof fieldVariants>["orientation"];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
orientation = "vertical",
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||||
|
orientation?: FieldOrientation;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
role="group"
|
||||||
|
data-slot="field"
|
||||||
|
data-orientation={orientation}
|
||||||
|
class={cn(fieldVariants({ orientation }), className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
33
src/lib/components/ui/field/index.ts
Normal file
33
src/lib/components/ui/field/index.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import Field from "./field.svelte";
|
||||||
|
import Set from "./field-set.svelte";
|
||||||
|
import Legend from "./field-legend.svelte";
|
||||||
|
import Group from "./field-group.svelte";
|
||||||
|
import Content from "./field-content.svelte";
|
||||||
|
import Label from "./field-label.svelte";
|
||||||
|
import Title from "./field-title.svelte";
|
||||||
|
import Description from "./field-description.svelte";
|
||||||
|
import Separator from "./field-separator.svelte";
|
||||||
|
import Error from "./field-error.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Field,
|
||||||
|
Set,
|
||||||
|
Legend,
|
||||||
|
Group,
|
||||||
|
Content,
|
||||||
|
Label,
|
||||||
|
Title,
|
||||||
|
Description,
|
||||||
|
Separator,
|
||||||
|
Error,
|
||||||
|
//
|
||||||
|
Set as FieldSet,
|
||||||
|
Legend as FieldLegend,
|
||||||
|
Group as FieldGroup,
|
||||||
|
Content as FieldContent,
|
||||||
|
Label as FieldLabel,
|
||||||
|
Title as FieldTitle,
|
||||||
|
Description as FieldDescription,
|
||||||
|
Separator as FieldSeparator,
|
||||||
|
Error as FieldError,
|
||||||
|
};
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="label"
|
data-slot="label"
|
||||||
class={cn(
|
class={cn(
|
||||||
"flex select-none items-center gap-2 text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50 group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50",
|
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
|
|
|
||||||
37
src/lib/components/ui/select/index.ts
Normal file
37
src/lib/components/ui/select/index.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import Root from "./select.svelte";
|
||||||
|
import Group from "./select-group.svelte";
|
||||||
|
import Label from "./select-label.svelte";
|
||||||
|
import Item from "./select-item.svelte";
|
||||||
|
import Content from "./select-content.svelte";
|
||||||
|
import Trigger from "./select-trigger.svelte";
|
||||||
|
import Separator from "./select-separator.svelte";
|
||||||
|
import ScrollDownButton from "./select-scroll-down-button.svelte";
|
||||||
|
import ScrollUpButton from "./select-scroll-up-button.svelte";
|
||||||
|
import GroupHeading from "./select-group-heading.svelte";
|
||||||
|
import Portal from "./select-portal.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
Group,
|
||||||
|
Label,
|
||||||
|
Item,
|
||||||
|
Content,
|
||||||
|
Trigger,
|
||||||
|
Separator,
|
||||||
|
ScrollDownButton,
|
||||||
|
ScrollUpButton,
|
||||||
|
GroupHeading,
|
||||||
|
Portal,
|
||||||
|
//
|
||||||
|
Root as Select,
|
||||||
|
Group as SelectGroup,
|
||||||
|
Label as SelectLabel,
|
||||||
|
Item as SelectItem,
|
||||||
|
Content as SelectContent,
|
||||||
|
Trigger as SelectTrigger,
|
||||||
|
Separator as SelectSeparator,
|
||||||
|
ScrollDownButton as SelectScrollDownButton,
|
||||||
|
ScrollUpButton as SelectScrollUpButton,
|
||||||
|
GroupHeading as SelectGroupHeading,
|
||||||
|
Portal as SelectPortal,
|
||||||
|
};
|
||||||
45
src/lib/components/ui/select/select-content.svelte
Normal file
45
src/lib/components/ui/select/select-content.svelte
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
import SelectPortal from "./select-portal.svelte";
|
||||||
|
import SelectScrollUpButton from "./select-scroll-up-button.svelte";
|
||||||
|
import SelectScrollDownButton from "./select-scroll-down-button.svelte";
|
||||||
|
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||||
|
import type { ComponentProps } from "svelte";
|
||||||
|
import type { WithoutChildrenOrChild } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
sideOffset = 4,
|
||||||
|
portalProps,
|
||||||
|
children,
|
||||||
|
preventScroll = true,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChild<SelectPrimitive.ContentProps> & {
|
||||||
|
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof SelectPortal>>;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPortal {...portalProps}>
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
bind:ref
|
||||||
|
{sideOffset}
|
||||||
|
{preventScroll}
|
||||||
|
data-slot="select-content"
|
||||||
|
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 relative z-50 max-h-(--bits-select-content-available-height) min-w-[8rem] origin-(--bits-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
<SelectScrollUpButton />
|
||||||
|
<SelectPrimitive.Viewport
|
||||||
|
class={cn(
|
||||||
|
"h-(--bits-select-anchor-height) w-full min-w-(--bits-select-anchor-width) scroll-my-1 p-1"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</SelectPrimitive.Viewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPortal>
|
||||||
21
src/lib/components/ui/select/select-group-heading.svelte
Normal file
21
src/lib/components/ui/select/select-group-heading.svelte
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
import type { ComponentProps } from "svelte";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: ComponentProps<typeof SelectPrimitive.GroupHeading> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPrimitive.GroupHeading
|
||||||
|
bind:ref
|
||||||
|
data-slot="select-group-heading"
|
||||||
|
class={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</SelectPrimitive.GroupHeading>
|
||||||
7
src/lib/components/ui/select/select-group.svelte
Normal file
7
src/lib/components/ui/select/select-group.svelte
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { ref = $bindable(null), ...restProps }: SelectPrimitive.GroupProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPrimitive.Group bind:ref data-slot="select-group" {...restProps} />
|
||||||
38
src/lib/components/ui/select/select-item.svelte
Normal file
38
src/lib/components/ui/select/select-item.svelte
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import CheckIcon from "@lucide/svelte/icons/check";
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
children: childrenProp,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChild<SelectPrimitive.ItemProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPrimitive.Item
|
||||||
|
bind:ref
|
||||||
|
{value}
|
||||||
|
data-slot="select-item"
|
||||||
|
class={cn(
|
||||||
|
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 ps-2 pe-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{#snippet children({ selected, highlighted })}
|
||||||
|
<span class="absolute end-2 flex size-3.5 items-center justify-center">
|
||||||
|
{#if selected}
|
||||||
|
<CheckIcon class="size-4" />
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
{#if childrenProp}
|
||||||
|
{@render childrenProp({ selected, highlighted })}
|
||||||
|
{:else}
|
||||||
|
{label || value}
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
</SelectPrimitive.Item>
|
||||||
20
src/lib/components/ui/select/select-label.svelte
Normal file
20
src/lib/components/ui/select/select-label.svelte
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<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<HTMLDivElement>> & {} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot="select-label"
|
||||||
|
class={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
7
src/lib/components/ui/select/select-portal.svelte
Normal file
7
src/lib/components/ui/select/select-portal.svelte
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let { ...restProps }: SelectPrimitive.PortalProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPrimitive.Portal {...restProps} />
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildrenOrChild<SelectPrimitive.ScrollDownButtonProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPrimitive.ScrollDownButton
|
||||||
|
bind:ref
|
||||||
|
data-slot="select-scroll-down-button"
|
||||||
|
class={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
<ChevronDownIcon class="size-4" />
|
||||||
|
</SelectPrimitive.ScrollDownButton>
|
||||||
20
src/lib/components/ui/select/select-scroll-up-button.svelte
Normal file
20
src/lib/components/ui/select/select-scroll-up-button.svelte
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import ChevronUpIcon from "@lucide/svelte/icons/chevron-up";
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildrenOrChild<SelectPrimitive.ScrollUpButtonProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPrimitive.ScrollUpButton
|
||||||
|
bind:ref
|
||||||
|
data-slot="select-scroll-up-button"
|
||||||
|
class={cn("flex cursor-default items-center justify-center py-1", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
<ChevronUpIcon class="size-4" />
|
||||||
|
</SelectPrimitive.ScrollUpButton>
|
||||||
18
src/lib/components/ui/select/select-separator.svelte
Normal file
18
src/lib/components/ui/select/select-separator.svelte
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { Separator as SeparatorPrimitive } from "bits-ui";
|
||||||
|
import { Separator } from "$lib/components/ui/separator/index.js";
|
||||||
|
import { cn } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: SeparatorPrimitive.RootProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Separator
|
||||||
|
bind:ref
|
||||||
|
data-slot="select-separator"
|
||||||
|
class={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
29
src/lib/components/ui/select/select-trigger.svelte
Normal file
29
src/lib/components/ui/select/select-trigger.svelte
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
|
||||||
|
import { cn, type WithoutChild } from "$lib/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
children,
|
||||||
|
size = "default",
|
||||||
|
...restProps
|
||||||
|
}: WithoutChild<SelectPrimitive.TriggerProps> & {
|
||||||
|
size?: "sm" | "default";
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPrimitive.Trigger
|
||||||
|
bind:ref
|
||||||
|
data-slot="select-trigger"
|
||||||
|
data-size={size}
|
||||||
|
class={cn(
|
||||||
|
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none select-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
<ChevronDownIcon class="size-4 opacity-50" />
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
11
src/lib/components/ui/select/select.svelte
Normal file
11
src/lib/components/ui/select/select.svelte
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Select as SelectPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
let {
|
||||||
|
open = $bindable(false),
|
||||||
|
value = $bindable(),
|
||||||
|
...restProps
|
||||||
|
}: SelectPrimitive.RootProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectPrimitive.Root bind:open bind:value={value as never} {...restProps} />
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot={dataSlot}
|
data-slot={dataSlot}
|
||||||
class={cn(
|
class={cn(
|
||||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px",
|
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:min-h-full data-[orientation=vertical]:w-px",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
|
|
|
||||||
7
src/lib/components/ui/textarea/index.ts
Normal file
7
src/lib/components/ui/textarea/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import Root from "./textarea.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Root,
|
||||||
|
//
|
||||||
|
Root as Textarea,
|
||||||
|
};
|
||||||
23
src/lib/components/ui/textarea/textarea.svelte
Normal file
23
src/lib/components/ui/textarea/textarea.svelte
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { cn, type WithElementRef, type WithoutChildren } from "$lib/utils.js";
|
||||||
|
import type { HTMLTextareaAttributes } from "svelte/elements";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
value = $bindable(),
|
||||||
|
class: className,
|
||||||
|
"data-slot": dataSlot = "textarea",
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildren<WithElementRef<HTMLTextareaAttributes>> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
bind:this={ref}
|
||||||
|
data-slot={dataSlot}
|
||||||
|
class={cn(
|
||||||
|
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
bind:value
|
||||||
|
{...restProps}
|
||||||
|
></textarea>
|
||||||
|
|
@ -4,6 +4,7 @@ import { get, writable } from 'svelte/store';
|
||||||
import { handleIncomingMessages } from '../handlers/messageHandler';
|
import { handleIncomingMessages } from '../handlers/messageHandler';
|
||||||
import { queue as msgQueue } from '../handlers/ws_messageSender';
|
import { queue as msgQueue } from '../handlers/ws_messageSender';
|
||||||
import { auth } from '../client/firebase';
|
import { auth } from '../client/firebase';
|
||||||
|
import { addNotification } from './noti';
|
||||||
|
|
||||||
let socket: WebSocket | null = null;
|
let socket: WebSocket | null = null;
|
||||||
|
|
||||||
|
|
@ -16,6 +17,7 @@ export function connectToWebsocket() {
|
||||||
|
|
||||||
socket.addEventListener('open', () => {
|
socket.addEventListener('open', () => {
|
||||||
socketStore.set(socket);
|
socketStore.set(socket);
|
||||||
|
addNotification('INFO:Connected!');
|
||||||
|
|
||||||
// recover messages on connect, flushing
|
// recover messages on connect, flushing
|
||||||
while (get(msgQueue).length) {
|
while (get(msgQueue).length) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue