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:
pakintada@gmail.com 2026-03-16 14:29:53 +07:00
parent 3f5eb8d07d
commit e6d1ac9a99
36 changed files with 1307 additions and 71 deletions

View file

@ -22,6 +22,7 @@ import { DragHandle } from './recipelist-table.svelte';
import { createRawSnippet } from 'svelte';
import RecipelistMatSelect from './recipelist-mat-select.svelte';
import { recipeDataEvent } from '$lib/core/stores/recipeStore';
import RecipelistValueEditor from './recipelist-value-editor.svelte';
export type RecipelistMaterial = {
id: number;
@ -122,5 +123,13 @@ export const columns: ColumnDef<RecipelistMaterial>[] = [
...row.original.values
});
}
},
{
id: 'actions',
cell: ({ row }) => {
return renderComponent(RecipelistValueEditor, {
row_id: row.original.id
});
}
}
];

View 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>

View file

@ -46,21 +46,6 @@
let currentStringParams: any = $state({});
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
let currentToppings: any = $state([]);
// current topping of this row
@ -169,6 +154,8 @@
}
function getCurrentSelectedToppingList() {
// FIXME: show unknown on preview
let current_selected = selectableToppingInGroup.find(
(x) => x.id === currentToppings[getToppingSlot()]['ListGroupID'][0]
);
@ -216,10 +203,32 @@
}
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') {
// update value, do re-render
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(() => {
initialize();
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
handleEvents(event);
}
@ -244,27 +253,29 @@
});
</script>
{#if currentMaterialType === 'topping'}
<!-- do topping layout -->
<div>
<!-- get name of topping -->
<div class="mx-auto my-4 flex flex-row gap-8">
<!-- popup selector for topping group -->
<Button variant="default">{getCurrentSelectedToppingGroup()}</Button>
<!-- popup selector for topping list -->
<Button variant="default">{getCurrentSelectedToppingList()}</Button>
<div>
{#if currentMaterialType === 'topping'}
<!-- do topping layout -->
<div>
<!-- get name of topping -->
<div class="mx-auto my-4 flex flex-row gap-8">
<p class="text-muted-foreground">
<b>
{getCurrentSelectedToppingGroup()}
</b>, {getCurrentSelectedToppingList()}
</p>
</div>
</div>
</div>
{:else}
<div>
<div class="flex w-full flex-row gap-4">
<!-- string param -->
{#if !hasMixOrder}
<!-- check if param is esp-v2-press-value -->
{:else}
<div>
<div class="flex w-full flex-row gap-4">
<!-- string param -->
{#if !hasMixOrder}
<!-- check if param is esp-v2-press-value -->
{#if currentStringParams['esp-v2-press-value']}
<div class="space-y-1">
<Label class="font-bold" for={`esp_v2_press_${row_uid}`}>Press</Label>
{#if currentStringParams['esp-v2-press-value']}
<div class="my-4">
<!-- <Label class="font-bold" for={`esp_v2_press_${row_uid}`}>Press</Label>
<div class="flex flex-row items-center space-x-2 text-center">
<Input
class="w-16"
@ -275,13 +286,18 @@
triggerEditChange(`${v.currentTarget.id}=${v.currentTarget.value}`)}
/>
<p>mA</p>
</div>
</div>
{/if}
</div> -->
{#if water.yield > 0}
<div class="space-y-1">
<Label class="font-bold" for={`water_yield_volume_${row_uid}`}>Hot</Label>
<p class="text-muted-foreground">
<b> Press </b>
{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">
<Input
class="w-16"
@ -292,13 +308,18 @@
triggerEditChange(`${v.currentTarget.id}=${v.currentTarget.value}`)}
/>
<p>ml</p>
</div>
</div>
{/if}
</div> -->
{#if water.cold > 0}
<div class="space-y-1">
<Label class="font-bold" for={`water_cold_volume_${row_uid}`}>Cold</Label>
<p class="text-muted-foreground">
<b> Hot </b>
{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">
<Input
class="w-16"
@ -309,13 +330,17 @@
triggerEditChange(`${v.currentTarget.id}=${v.currentTarget.value}`)}
/>
<p>ml</p>
</div> -->
<p class="text-muted-foreground">
<b> Hot </b>
{water.cold} ml
</p>
</div>
</div>
{/if}
{/if}
{#if currentMaterialType !== 'cup' && currentMaterialType !== 'topping' && stir_time > 0}
<div class="space-y-1">
<Label class="font-bold" for={`stir_time_${row_uid}`}>{getStirTimeName()}</Label>
{#if currentMaterialType !== 'cup' && currentMaterialType !== 'topping' && stir_time > 0}
<div class="my-4">
<!-- <Label class="font-bold" for={`stir_time_${row_uid}`}>{getStirTimeName()}</Label>
<div class="flex flex-row items-center space-x-2 text-center">
<Input
class="w-16"
@ -326,15 +351,19 @@
triggerEditChange(`${v.currentTarget.id}=${v.currentTarget.value}`)}
/>
<p>sec</p>
</div> -->
<p class="text-muted-foreground">
<b>{getStirTimeName()}</b>
{stir_time / 10} sec(s)
</p>
</div>
</div>
{/if}
{/if}
{/if}
<!-- display powder/syrup -->
{#if currentMaterialType === 'syrup' || currentMaterialType === 'powder' || currentMaterialType === 'bean'}
<div class="space-y-1">
<Label class="font-bold" for={`powder_syrup_volume_${row_uid}`}>Volume</Label>
<!-- display powder/syrup -->
{#if currentMaterialType === 'syrup' || currentMaterialType === 'powder' || currentMaterialType === 'bean'}
<div class="my-4">
<!-- <Label class="font-bold" for={`powder_syrup_volume_${row_uid}`}>Volume</Label>
<div class="flex flex-row items-center space-x-2 text-center">
<Input
class="w-16"
@ -344,17 +373,23 @@
onchangecapture={(v) =>
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>
{/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>
{/if}
</div>
<!-- feed pattern -->
{#if feed.parameter > 0 || feed.pattern > 0}
<div class="mx-auto my-4 flex items-center gap-8 text-center">
<Button variant="outline">Style {feed.pattern}</Button>
<Button variant="outline">Level {feed.parameter}</Button>
</div>
{/if}
</div>
{/if}
{/if}
<!-- show sheet -->
</div>

View file

@ -0,0 +1,6 @@
enum ValueEvent {
NONE,
EDITED
}
export { ValueEvent };

View file

@ -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} />

View file

@ -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} />

View 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} />

View 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,
};

View 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>

View 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>

View 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}

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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,
};

View file

@ -13,7 +13,7 @@
bind:ref
data-slot="label"
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
)}
{...restProps}

View 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,
};

View 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>

View 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>

View 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} />

View 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>

View 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>

View file

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

View file

@ -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>

View 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>

View 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}
/>

View 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>

View 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} />

View file

@ -14,7 +14,7 @@
bind:ref
data-slot={dataSlot}
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
)}
{...restProps}

View file

@ -0,0 +1,7 @@
import Root from "./textarea.svelte";
export {
Root,
//
Root as Textarea,
};

View 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>

View file

@ -4,6 +4,7 @@ import { get, writable } from 'svelte/store';
import { handleIncomingMessages } from '../handlers/messageHandler';
import { queue as msgQueue } from '../handlers/ws_messageSender';
import { auth } from '../client/firebase';
import { addNotification } from './noti';
let socket: WebSocket | null = null;
@ -16,6 +17,7 @@ export function connectToWebsocket() {
socket.addEventListener('open', () => {
socketStore.set(socket);
addNotification('INFO:Connected!');
// recover messages on connect, flushing
while (get(msgQueue).length) {