Menu

Change language

GitHub repo

Form Layouts

blocklayout

Structured create/edit entity form ("New project"): stacked SectionCards for project details (name with live slug preview, description with char counter), configuration (visibility Select, start date, budget), and access (link vs restricted RadioGroup, notification/comment Switch rows) with a sticky-feel footer bar. Create stays disabled until the name is filled; validation hints appear under empty required fields after the first submit attempt.

Installation

$npx shadcn-svelte@latest add https://uipkge.dev/r/svelte/form-layouts.json
Manifest:https://uipkge.dev/r/svelte/form-layouts.jsonInstalls to:src/lib/components/blocks/form-layouts/

Variants

Loading interactive previews…

Files installed (1)

  • src/lib/components/blocks/form-layouts/FormLayouts.svelte6.9 kB
    <script lang="ts" module>
      export interface FormLayoutsProps {
        class?: string
      }
    </script>
    
    <script lang="ts">
      import { Globe, Link2, Lock } from '@lucide/svelte'
      import { Button } from '$lib/components/ui/button'
      import { Input } from '$lib/components/ui/input'
      import { Label } from '$lib/components/ui/label'
      import { RadioGroup, RadioGroupItem } from '$lib/components/ui/radio-group'
      import { SectionCard } from '$lib/components/ui/section-card'
      import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '$lib/components/ui/select'
      import { Switch } from '$lib/components/ui/switch'
      import { Textarea } from '$lib/components/ui/textarea'
    
      let { class: className }: FormLayoutsProps = $props()
    
      const DESCRIPTION_LIMIT = 280
    
      let name = $state('')
      let description = $state('')
      let visibility = $state('internal')
      let startDate = $state('')
      let budget = $state('')
      let access = $state<'link' | 'restricted'>('link')
      let notifications = $state(true)
      let comments = $state(true)
      let submitted = $state(false)
    
      const slug = $derived.by(() => {
        const cleaned = name
          .trim()
          .toLowerCase()
          .replace(/[^a-z0-9]+/g, '-')
          .replace(/^-+|-+$/g, '')
        return cleaned || 'your-project'
      })
    
      const showNameError = $derived(submitted && name.trim() === '')
    
      function handleSubmit() {
        submitted = true
        if (name.trim() === '') return
      }
    </script>
    
    <div data-slot="form-layouts" class={['max-w-2xl', className]}>
      <form
        class="space-y-6"
        novalidate
        onsubmit={(e) => {
          e.preventDefault()
          handleSubmit()
        }}
      >
        <SectionCard title="Project details" description="Name your project and describe what it covers.">
          <div class="space-y-4">
            <div class="space-y-2">
              <Label for="fl-name">Name</Label>
              <Input
                id="fl-name"
                bind:value={name}
                placeholder="Q3 launch site"
                autocomplete="off"
                aria-invalid={showNameError || undefined}
              />
              {#if showNameError}
                <p role="alert" class="text-destructive text-xs">Project name is required.</p>
              {/if}
            </div>
            <div class="border-border bg-muted/40 flex items-center gap-2 rounded-md border px-3 py-2">
              <Globe class="text-muted-foreground size-4 shrink-0" aria-hidden="true" />
              <p class="min-w-0 truncate text-xs">
                <span class="text-muted-foreground">acme.uipkge.app/</span>
                <span class="text-foreground font-medium">{slug}</span>
              </p>
            </div>
            <div class="space-y-2">
              <Label for="fl-description">Description</Label>
              <Textarea id="fl-description" bind:value={description} rows={3} placeholder="What is this project about?" />
              <p
                class="text-right text-xs {description.length > DESCRIPTION_LIMIT
                  ? 'text-destructive'
                  : 'text-muted-foreground'}"
              >
                {description.length}/{DESCRIPTION_LIMIT}
              </p>
            </div>
          </div>
        </SectionCard>
    
        <SectionCard title="Configuration" description="Visibility, schedule and budget for the project.">
          <div class="grid gap-4 sm:grid-cols-3">
            <div class="space-y-2">
              <Label for="fl-visibility">Visibility</Label>
              <Select bind:value={visibility}>
                <SelectTrigger id="fl-visibility" class="w-full">
                  <SelectValue />
                </SelectTrigger>
                <SelectContent>
                  <SelectItem value="public">Public</SelectItem>
                  <SelectItem value="internal">Internal</SelectItem>
                  <SelectItem value="private">Private</SelectItem>
                </SelectContent>
              </Select>
            </div>
            <div class="space-y-2">
              <Label for="fl-start-date">Start date</Label>
              <Input id="fl-start-date" bind:value={startDate} type="date" />
            </div>
            <div class="space-y-2">
              <Label for="fl-budget">Budget</Label>
              <Input id="fl-budget" bind:value={budget} type="number" min={0} step={500} class="text-left" />
            </div>
          </div>
        </SectionCard>
    
        <SectionCard title="Access" description="Decide who can open the project and what they can do.">
          <div class="space-y-5">
            <RadioGroup bind:value={access} class="gap-3">
              <div
                class="hover:bg-accent/50 has-data-[state=checked]:border-primary flex items-start gap-3 rounded-lg border p-3 transition-colors"
              >
                <RadioGroupItem id="fl-access-link" value="link" class="mt-0.5" />
                <label for="fl-access-link" class="cursor-pointer select-none">
                  <span class="flex items-center gap-1.5 text-sm font-medium">
                    <Link2 class="size-4" aria-hidden="true" />
                    Anyone with the link
                  </span>
                  <span class="text-muted-foreground mt-0.5 block text-xs"> Guests can view without signing in. </span>
                </label>
              </div>
              <div
                class="hover:bg-accent/50 has-data-[state=checked]:border-primary flex items-start gap-3 rounded-lg border p-3 transition-colors"
              >
                <RadioGroupItem id="fl-access-restricted" value="restricted" class="mt-0.5" />
                <label for="fl-access-restricted" class="cursor-pointer select-none">
                  <span class="flex items-center gap-1.5 text-sm font-medium">
                    <Lock class="size-4" aria-hidden="true" />
                    Restricted
                  </span>
                  <span class="text-muted-foreground mt-0.5 block text-xs">
                    Only invited members can open the project.
                  </span>
                </label>
              </div>
            </RadioGroup>
    
            <div class="divide-y rounded-lg border">
              <div class="flex items-center justify-between gap-4 p-4">
                <div class="min-w-0">
                  <p class="text-sm font-medium">Notifications</p>
                  <p class="text-muted-foreground mt-0.5 text-xs">Email the team on status changes and mentions.</p>
                </div>
                <Switch bind:checked={notifications} aria-label="Toggle notifications" />
              </div>
              <div class="flex items-center justify-between gap-4 p-4">
                <div class="min-w-0">
                  <p class="text-sm font-medium">Comments</p>
                  <p class="text-muted-foreground mt-0.5 text-xs">Let viewers comment on tasks and updates.</p>
                </div>
                <Switch bind:checked={comments} aria-label="Toggle comments" />
              </div>
            </div>
          </div>
        </SectionCard>
    
        <div
          class="bg-background/80 border-border sticky bottom-0 -mx-1 flex items-center justify-end gap-2 rounded-b-xl border-t px-1 py-4 backdrop-blur"
        >
          <Button type="button" variant="ghost" size="sm">Cancel</Button>
          <Button type="submit" size="sm" disabled={name.trim() === ''}>Create project</Button>
        </div>
      </form>
    </div>
    

Raw manifest:https://uipkge.dev/r/svelte/form-layouts.json