summaryrefslogtreecommitdiffhomepage
path: root/src/features/workspaces/ui/WorkspacesHome.svelte
blob: d97eab74b4968e19bc531888d15b59aef39a5e81 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
<script lang="ts">
  import { onMount } from "svelte";
  import type { ComputerEntry } from "@dispatch/wire";
  import type { WorkspaceStore } from "../store.svelte";
  import { isValidSlug, workspacePath } from "../logic/route";
  import WorkspaceCard from "./WorkspaceCard.svelte";

  let {
    store,
    onNavigate,
    computers,
    hasActive,
  }: {
    store: WorkspaceStore;
    onNavigate: (path: string) => void;
    computers: readonly ComputerEntry[];
    /**
     * Optional port forwarded to each {@link WorkspaceCard}: whether the
     * workspace has at least one active (generating / queued) conversation.
     * Wired by the composition root to the app store. Absent → no indicator.
     */
    hasActive?: (workspaceId: string) => boolean;
  } = $props();

  onMount(() => {
    void store.refresh();
  });

  let newSlug = $state("");
  let slugError = $state<string | null>(null);

  const slugValid = $derived(newSlug.length > 0 && isValidSlug(newSlug));

  function createWorkspace(): void {
    const slug = newSlug.trim();
    if (!isValidSlug(slug)) {
      slugError = "Lowercase letters, digits, and hyphens (1–40 chars).";
      return;
    }
    slugError = null;
    newSlug = "";
    onNavigate(workspacePath(slug));
  }
</script>

<div class="mx-auto flex h-screen w-full max-w-3xl flex-col gap-4 p-6">
  <header class="flex items-center justify-between">
    <h1 class="text-2xl font-bold">Workspaces</h1>
    <a
      href="/default"
      class="btn btn-ghost btn-sm"
      onclick={(e) => {
        e.preventDefault();
        onNavigate("/default");
      }}
    >
      Default
    </a>
  </header>

  <form
    class="flex items-end gap-2"
    onsubmit={(e) => {
      e.preventDefault();
      createWorkspace();
    }}
  >
    <div class="flex-1">
      <label for="new-ws" class="mb-1 block text-xs font-semibold uppercase opacity-60"
        >New workspace</label
      >
      <input
        id="new-ws"
        class="input input-bordered w-full"
        placeholder="my-workspace"
        bind:value={newSlug}
        autocomplete="off"
        spellcheck="false"
      />
    </div>
    <button type="submit" class="btn btn-primary btn-sm" disabled={!slugValid}>Create</button>
  </form>
  {#if slugError}
    <p class="text-xs text-error">{slugError}</p>
  {/if}

  <div class="flex-1 overflow-y-auto">
    {#if store.loading && store.list.length === 0}
      <div class="flex h-32 items-center justify-center">
        <span class="loading loading-spinner loading-sm opacity-60"></span>
      </div>
    {:else if store.list.length === 0}
      <p class="py-8 text-center text-sm opacity-60">
        No workspaces yet. Create one above or visit <code>/your-name</code> in the URL.
      </p>
    {:else}
      <ul class="flex flex-col gap-2">
        {#each store.list as ws (ws.id)}
          <WorkspaceCard
            {ws}
            {store}
            {onNavigate}
            {computers}
            {...(hasActive ? { hasActive } : {})}
          />
        {/each}
      </ul>
    {/if}
  </div>
</div>