[READ-ONLY] Mirror of https://github.com/flo-bit/shapecraft. flo-bit.dev/shapecraft/
0

Configure Feed

Select the types of activity you want to include in your feed.

shapecraft / webapp / src / lib / components / ParamPanel.svelte
5.6 kB 165 lines
1<script lang="ts"> 2 import { ChevronDown, ChevronRight, RotateCcw, Search } from '@lucide/svelte'; 3 import type { OptionDef } from 'shapecraft/core/schema'; 4 import type { GeneratorEntry } from '$lib/registry'; 5 import { displayValue, getPreset, resetParams, setParam, setPreset } from '$lib/editor.svelte'; 6 import SliderControl from './controls/SliderControl.svelte'; 7 import ToggleControl from './controls/ToggleControl.svelte'; 8 import SelectControl from './controls/SelectControl.svelte'; 9 import ColorControl from './controls/ColorControl.svelte'; 10 import ColorArrayControl from './controls/ColorArrayControl.svelte'; 11 12 let { entry }: { entry: GeneratorEntry } = $props(); 13 14 const presetNames = $derived(Object.keys(entry.presets)); 15 16 let query = $state(''); 17 let collapsed = $state<Record<string, boolean>>({}); 18 19 interface Group { 20 name: string; 21 params: [string, OptionDef][]; 22 } 23 24 // Groups in order of first appearance in the schema; search filters params 25 // by label/key and overrides collapsing so matches are always visible. 26 const groups = $derived.by(() => { 27 const q = query.trim().toLowerCase(); 28 const out: Group[] = []; 29 for (const [key, def] of Object.entries(entry.schema)) { 30 if (q && !(def.label ?? key).toLowerCase().includes(q) && !key.toLowerCase().includes(q)) 31 continue; 32 const name = def.group ?? 'General'; 33 let group = out.find((g) => g.name === name); 34 if (!group) { 35 group = { name, params: [] }; 36 out.push(group); 37 } 38 group.params.push([key, def]); 39 } 40 return out; 41 }); 42 43 const searching = $derived(query.trim() !== ''); 44 45 function isOpen(name: string) { 46 return searching || !collapsed[`${entry.id}:${name}`]; 47 } 48 function toggle(name: string) { 49 if (searching) return; 50 collapsed[`${entry.id}:${name}`] = !collapsed[`${entry.id}:${name}`]; 51 } 52</script> 53 54<aside class="flex w-84 shrink-0 flex-col border-l border-line bg-panel"> 55 <div class="flex items-center justify-between px-4 pt-[15px] pb-[11px]"> 56 <h3 class="text-[15px] font-bold tracking-tight">Parameters</h3> 57 <button 58 type="button" 59 class="grid size-7.5 place-items-center rounded-[9px] border border-line2 bg-panel2 text-muted hover:text-ink" 60 title="Reset to defaults" 61 onclick={() => resetParams(entry.id)} 62 > 63 <RotateCcw size={15} /> 64 </button> 65 </div> 66 <div 67 class="mx-3.5 mb-3 flex h-9 items-center gap-2 rounded-[9px] border border-line2 bg-bg px-2.5" 68 > 69 <Search size={15} class="shrink-0 text-faint" /> 70 <input 71 type="text" 72 class="w-full border-none bg-transparent p-0 text-[13px] text-ink placeholder:text-faint focus:ring-0" 73 placeholder="Search parameters…" 74 bind:value={query} 75 /> 76 </div> 77 {#if presetNames.length > 1} 78 <div class="mx-3.5 mb-3"> 79 <select 80 class="h-9.5 w-full rounded-[9px] border-line2 bg-panel2 text-[13px] font-semibold text-ink focus:border-accent focus:ring-0" 81 value={getPreset(entry.id)} 82 onchange={(e) => setPreset(entry.id, e.currentTarget.value)} 83 > 84 {#each presetNames as p (p)} 85 <option value={p}>{p}</option> 86 {/each} 87 </select> 88 </div> 89 {/if} 90 <div class="min-h-0 flex-1 overflow-y-auto px-1.5 pb-3"> 91 {#each groups as group, gi (entry.id + ':' + group.name)} 92 <div class={gi > 0 ? 'border-t border-line' : ''}> 93 <button 94 type="button" 95 class="flex w-full items-center gap-2.5 px-2.5 py-3 text-left" 96 onclick={() => toggle(group.name)} 97 > 98 <span class="text-faint"> 99 {#if isOpen(group.name)} 100 <ChevronDown size={15} /> 101 {:else} 102 <ChevronRight size={15} /> 103 {/if} 104 </span> 105 <span class="flex-1 text-[13px] font-bold">{group.name}</span> 106 <span class="mono text-[11px] text-faint">{group.params.length}</span> 107 </button> 108 {#if isOpen(group.name)} 109 <div class="flex flex-col gap-[13px] px-2.5 pt-0.5 pb-3.5"> 110 {#each group.params as [key, def] (entry.id + ':' + key)} 111 {#if def.type === 'range'} 112 <SliderControl 113 label={def.label ?? key} 114 value={displayValue(entry, key) as number | [number, number]} 115 min={def.min} 116 max={def.max} 117 step={def.step ?? 0.01} 118 onchange={(v) => setParam(entry.id, key, v)} 119 /> 120 {:else if def.type === 'integer'} 121 <SliderControl 122 label={def.label ?? key} 123 value={displayValue(entry, key) as number | [number, number]} 124 min={def.min} 125 max={def.max} 126 step={1} 127 onchange={(v) => setParam(entry.id, key, v)} 128 /> 129 {:else if def.type === 'boolean'} 130 <ToggleControl 131 label={def.label ?? key} 132 value={displayValue(entry, key) as boolean} 133 onchange={(v) => setParam(entry.id, key, v)} 134 /> 135 {:else if def.type === 'select'} 136 <SelectControl 137 label={def.label ?? key} 138 value={displayValue(entry, key) as string} 139 options={def.options} 140 onchange={(v) => setParam(entry.id, key, v)} 141 /> 142 {:else if def.type === 'color'} 143 <ColorControl 144 label={def.label ?? key} 145 value={displayValue(entry, key) as string} 146 onchange={(v) => setParam(entry.id, key, v)} 147 /> 148 {:else if def.type === 'color-array'} 149 <ColorArrayControl 150 label={def.label ?? key} 151 colors={displayValue(entry, key) as string[]} 152 min={def.min} 153 max={def.max} 154 onchange={(v) => setParam(entry.id, key, v)} 155 /> 156 {/if} 157 {/each} 158 </div> 159 {/if} 160 </div> 161 {:else} 162 <div class="px-3 py-6 text-center text-[13px] text-faint">No matching parameters</div> 163 {/each} 164 </div> 165</aside>