Skip to content

Commit b71b41d

Browse files
authored
option render rework (#95)
* feat: extract options to components * fix: don't expand tooltip on hover * feat: nicer multiline default height * feat: error propagation * feat: bring back schema refine
1 parent 9f68a0e commit b71b41d

File tree

9 files changed

+402
-290
lines changed

9 files changed

+402
-290
lines changed

src/lib/components/tooltip-label.svelte

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import * as Tooltip from '$lib/components/ui/tooltip';
33
import { Label } from '$lib/components/ui/label';
44
import type { Snippet } from 'svelte';
5-
import type { string } from 'zod';
5+
import type { LabelRootProps } from 'bits-ui';
66
77
const {
88
tooltip,
@@ -11,8 +11,9 @@
1111
required,
1212
title,
1313
class: className,
14-
children: labelChild
15-
}: {
14+
children: labelChild,
15+
...rest
16+
}: LabelRootProps & {
1617
extra?: Record<string, ExtraValue>;
1718
tooltip?: string;
1819
type?: string;
@@ -23,7 +24,7 @@
2324
} = $props();
2425
2526
type ExtraValue =
26-
| true
27+
| boolean
2728
| string
2829
| {
2930
value: string | true;
@@ -37,7 +38,8 @@
3738
{#snippet child({ props })}
3839
<Label
3940
{...props}
40-
class="{className} relative !m-0 inline w-full cursor-help content-center truncate hover:min-w-max "
41+
{...rest}
42+
class="{className} relative !m-0 inline w-full cursor-help content-center truncate"
4143
>
4244
<span class="flex items-center overflow-hidden">
4345
<span class="truncate">

src/routes/(app)/sessions/create/+page.svelte

Lines changed: 14 additions & 253 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@
4545
import { zod4 } from 'sveltekit-superforms/adapters';
4646
import * as schemas from './schemas';
4747
48-
import type { HTMLInputTypeAttribute } from 'svelte/elements';
4948
import type { paths, components, operations } from '$generated/api';
5049
import { onMount, tick } from 'svelte';
5150
@@ -69,6 +68,7 @@
6968
import { fade } from 'svelte/transition';
7069
import CopyButton from '$lib/components/copy-button.svelte';
7170
import Pip from '$lib/components/pip.svelte';
71+
import OptionField from './OptionField.svelte';
7272
7373
type CreateSessionRequest = NonNullable<
7474
operations['createSession']['requestBody']
@@ -79,38 +79,6 @@
7979
[P in keyof Required<T>]: Pick<T, P> extends Required<Pick<T, P>> ? T[P] : T[P] | undefined;
8080
};
8181
82-
const inputTypes: {
83-
[K in PublicRegistryAgent['options'][string]['type']]: HTMLInputTypeAttribute;
84-
} = {
85-
string: 'text',
86-
number: 'number',
87-
secret: 'password',
88-
blob: 'file',
89-
'list[blob]': 'file',
90-
bool: 'number',
91-
i8: 'number',
92-
'list[i8]': 'number',
93-
f64: 'number',
94-
'list[f64]': 'number',
95-
f32: 'number',
96-
'list[f32]': 'number',
97-
i32: 'number',
98-
'list[i32]': 'number',
99-
i64: 'text',
100-
'list[i64]': 'text',
101-
i16: 'number',
102-
'list[i16]': 'number',
103-
'list[string]': 'string',
104-
u8: 'number',
105-
'list[u8]': 'number',
106-
u32: 'number',
107-
'list[u32]': 'number',
108-
u64: 'text',
109-
'list[u64]': 'text',
110-
u16: 'number',
111-
'list[u16]': 'number'
112-
};
113-
11482
let ctx = appContext.get();
11583
11684
let error: string | null = $state(null);
@@ -174,8 +142,8 @@
174142
server: ctx.server
175143
});
176144
} else {
177-
throw new Error('no data received');
178145
sendingForm = false;
146+
throw new Error('no data received');
179147
}
180148
} catch (e) {
181149
console.log(e);
@@ -223,9 +191,6 @@
223191
}
224192
} satisfies Provider;
225193
226-
let newServerAddress: string = $state('');
227-
let newServerPort: string = $state('');
228-
229194
const importFromJson = (json: string) => {
230195
try {
231196
const data: CreateSessionRequest = JSON.parse(json);
@@ -616,220 +581,6 @@
616581
}
617582
</script>
618583

619-
{#snippet optionRow(name: any, opt: any)}
620-
<li class="hover:bg-muted/50 border-b px-4 py-2">
621-
<Form.ElementField
622-
class="flex gap-2"
623-
{form}
624-
name="agents[{selectedAgent!}].options.{name}.value"
625-
>
626-
<Form.Control>
627-
{#snippet children({ props })}
628-
<TooltipLabel
629-
class="max-w-1/4 min-w-1/4 {opt.required ? 'hover:pr-[0.5em]' : ''}"
630-
title={name}
631-
tooltip={opt?.display?.description ?? 'No description provided.'}
632-
extra={{
633-
required: opt.required,
634-
type: opt.type
635-
}}
636-
>
637-
{opt?.display?.label ?? name}
638-
</TooltipLabel>
639-
640-
{#if opt.type === 'blob'}
641-
<Input
642-
{...props}
643-
class="m-0"
644-
type={inputTypes[opt.type as keyof typeof inputTypes]}
645-
bind:value={
646-
() => $formData.agents[selectedAgent!]!.options[name]?.value,
647-
(value) => {
648-
$formData.agents[selectedAgent!]!.options[name] = {
649-
type: opt.type,
650-
value
651-
} as any; // FIXME: !!
652-
}
653-
}
654-
aria-invalid={$allErrors.length > 0 &&
655-
opt.required &&
656-
$formData.agents[selectedAgent!]!.options[name]?.value === undefined}
657-
placeholder={'default' in opt ? opt.default?.toString() : undefined}
658-
/>
659-
{:else if opt.type.includes('list')}
660-
{@const list = Array.isArray($formData.agents[selectedAgent!]!.options[name]?.value)
661-
? ($formData.agents[selectedAgent!]!.options[name]!.value as any[])
662-
: []}
663-
<ol class="flex w-full flex-col gap-1 rounded-md">
664-
<li>
665-
<Button
666-
onclick={() => {
667-
const optObj = $formData.agents[selectedAgent!]!.options[name];
668-
if (optObj && Array.isArray(optObj.value)) {
669-
(optObj.value as string[]).push('');
670-
} else {
671-
$formData.agents[selectedAgent!]!.options[name] = {
672-
type: opt.type,
673-
value: ['']
674-
} as any;
675-
}
676-
$formData.agents = $formData.agents;
677-
}}
678-
class="m-0 w-full">Add value</Button
679-
>
680-
</li>
681-
{#each list, i}
682-
<li>
683-
<ButtonGroup.Root class="m-0 w-full">
684-
<Input
685-
type={opt.secret
686-
? 'password'
687-
: inputTypes[opt.type as keyof typeof inputTypes]}
688-
bind:value={
689-
() => {
690-
const optObj = $formData.agents[selectedAgent!]!.options[name];
691-
const arr = Array.isArray(optObj?.value)
692-
? (optObj.value as any[])
693-
: undefined;
694-
return arr ? arr[i] : '';
695-
},
696-
(value) => {
697-
const optObj = $formData.agents[selectedAgent!]!.options[name];
698-
if (!optObj || !Array.isArray(optObj.value)) {
699-
// initialize as array and set the i'th element
700-
$formData.agents[selectedAgent!]!.options[name] = {
701-
type: opt.type,
702-
value: []
703-
} as any;
704-
}
705-
($formData.agents[selectedAgent!]!.options[name]!.value as any[])[i] =
706-
value;
707-
// trigger reactivity
708-
$formData.agents = $formData.agents;
709-
}
710-
}
711-
/>
712-
<Button
713-
variant="outline"
714-
class="m-0"
715-
size="icon"
716-
onclick={() => {
717-
const optObj = $formData.agents[selectedAgent!]!.options[name];
718-
if (optObj && Array.isArray(optObj.value)) {
719-
(optObj.value as string[]).splice(i, 1);
720-
// trigger reactivity
721-
$formData.agents = $formData.agents;
722-
}
723-
}}
724-
>
725-
<IconXRegular />
726-
</Button>
727-
</ButtonGroup.Root>
728-
</li>
729-
{/each}
730-
</ol>
731-
{:else if opt.type === 'bool'}
732-
<ButtonGroup.Root class="m-0 justify-start">
733-
<Button
734-
class={cn(
735-
($formData.agents[selectedAgent!]!.options[name]?.value ?? opt.default) ===
736-
true && 'bg-accent text-accent-foreground'
737-
)}
738-
onclick={() => {
739-
const optObj = $formData.agents[selectedAgent!]!.options[name];
740-
if (optObj) {
741-
optObj.value = true;
742-
} else {
743-
$formData.agents[selectedAgent!]!.options[name] = {
744-
type: opt.type,
745-
value: true
746-
} as any;
747-
}
748-
$formData.agents = $formData.agents;
749-
}}>True</Button
750-
>
751-
<Button
752-
class=" {$formData.agents[selectedAgent!]!.options[name]?.value === false ||
753-
($formData.agents[selectedAgent!]!.options[name]?.value === undefined &&
754-
opt.default === false)
755-
? 'bg-accent text-accent-foreground'
756-
: ''}"
757-
onclick={() => {
758-
const optObj = $formData.agents[selectedAgent!]!.options[name];
759-
if (optObj) {
760-
optObj.value = false;
761-
} else {
762-
$formData.agents[selectedAgent!]!.options[name] = {
763-
type: opt.type,
764-
value: false
765-
} as any;
766-
}
767-
$formData.agents = $formData.agents;
768-
}}>False</Button
769-
>
770-
</ButtonGroup.Root>
771-
{:else if opt?.display?.multiline === true}
772-
<Textarea
773-
{...props}
774-
class="relative m-0 h-42 resize-none"
775-
bind:value={
776-
() => {
777-
const v = $formData.agents[selectedAgent!]!.options[name]?.value;
778-
return typeof v === 'string' || typeof v === 'number' ? String(v) : '';
779-
},
780-
(value) => {
781-
$formData.agents[selectedAgent!]!.options[name] = {
782-
type: opt.type,
783-
value
784-
} as any;
785-
}
786-
}
787-
defaultValue={opt.default}
788-
aria-invalid={(() => {
789-
const error = $errors?.agents?.[selectedAgent!]?.options?.[name];
790-
if (error && JSON.stringify(error).includes('{}')) return undefined;
791-
else if (error) return true;
792-
else return undefined;
793-
})()}
794-
placeholder={'default' in opt ? opt.default?.toString() : undefined}
795-
/>
796-
{:else}
797-
<Input
798-
{...props}
799-
type={opt.secret ? 'password' : inputTypes[opt.type as keyof typeof inputTypes]}
800-
bind:value={
801-
() => $formData.agents[selectedAgent!]!.options[name]?.value,
802-
(value) => {
803-
$formData.agents[selectedAgent!]!.options[name] = {
804-
type: opt.type,
805-
value
806-
} as any; // FIXME: !!
807-
}
808-
}
809-
class="m-0 w-full "
810-
defaultValue={opt.default}
811-
aria-invalid={(() => {
812-
const error = $errors?.agents?.[selectedAgent!]?.options?.[name];
813-
if (error && JSON.stringify(error).includes('{}')) return undefined;
814-
else if (error) return true;
815-
else return undefined;
816-
})()}
817-
placeholder={'default' in opt ? opt.default?.toString() : undefined}
818-
/>
819-
{/if}
820-
{/snippet}
821-
</Form.Control>
822-
</Form.ElementField>
823-
824-
{#if JSON.stringify($errors?.agents?.[selectedAgent!]?.options?.[name]) !== '{}' && JSON.stringify($errors?.agents?.[selectedAgent!]?.options?.[name])}
825-
<span class="text-xs">
826-
{$errors?.agents?.[selectedAgent!]?.options?.[name]?.value ??
827-
$errors?.agents?.[selectedAgent!]?.options?.[name]}
828-
</span>
829-
{/if}
830-
</li>
831-
{/snippet}
832-
833584
<header class="bg-background sticky top-0 flex h-16 shrink-0 items-center gap-2 border-b px-4">
834585
<Sidebar.Trigger class="-ml-1" />
835586
<Separator orientation="vertical" class="mr-2 h-4" />
@@ -1393,7 +1144,12 @@
13931144
<Accordion.Content class="!p-0">
13941145
<ol>
13951146
{#each entries as [name, opt] (name)}
1396-
{@render optionRow(name, opt)}
1147+
<OptionField
1148+
superform={form}
1149+
agent={selectedAgent!}
1150+
{name}
1151+
meta={opt}
1152+
/>
13971153
{/each}
13981154
</ol>
13991155
</Accordion.Content>
@@ -1402,7 +1158,12 @@
14021158
{:else}
14031159
<ol>
14041160
{#each entries as [name, opt] (name)}
1405-
{@render optionRow(name, opt)}
1161+
<OptionField
1162+
superform={form}
1163+
agent={selectedAgent!}
1164+
{name}
1165+
meta={opt}
1166+
/>
14061167
{/each}
14071168
</ol>
14081169
{/if}

0 commit comments

Comments
 (0)