@yackey-labs/yauth-ui-solidjs 0.12.2 → 0.12.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,214 @@
1
+ import { type Component, For, Show, createSignal } from "solid-js";
2
+ import {
3
+ ROLES,
4
+ createMembers,
5
+ createOrgPermissions,
6
+ createOrgRoles,
7
+ } from "../hooks/create-organizations";
8
+ import { RoleSelector } from "./role-selector";
9
+
10
+ export interface MemberListProps {
11
+ organizationId: string;
12
+ }
13
+
14
+ const formatDate = (iso: string | null | undefined): string => {
15
+ if (!iso) return "—";
16
+ const d = new Date(iso);
17
+ return Number.isNaN(d.getTime()) ? "—" : d.toLocaleDateString();
18
+ };
19
+
20
+ /**
21
+ * Tailwind badge class for a built-in role. Custom strings render as
22
+ * the default secondary badge.
23
+ */
24
+ const roleBadgeClass = (role: string): string => {
25
+ switch (role) {
26
+ case ROLES.OWNER:
27
+ return "bg-amber-500/15 text-amber-700 dark:text-amber-300";
28
+ case ROLES.ADMIN:
29
+ return "bg-blue-500/15 text-blue-700 dark:text-blue-300";
30
+ case ROLES.BILLING_ADMIN:
31
+ return "bg-purple-500/15 text-purple-700 dark:text-purple-300";
32
+ case ROLES.MEMBER:
33
+ return "bg-secondary text-secondary-foreground";
34
+ case ROLES.VIEWER:
35
+ return "bg-muted text-muted-foreground";
36
+ default:
37
+ return "bg-secondary text-secondary-foreground";
38
+ }
39
+ };
40
+
41
+ export const MemberList: Component<MemberListProps> = (props) => {
42
+ const { members, loading, error, refetch } = createMembers(
43
+ () => props.organizationId,
44
+ );
45
+ const { hasPermission } = createOrgPermissions(() => props.organizationId);
46
+ const {
47
+ submitting,
48
+ error: actionError,
49
+ changeRole,
50
+ removeMember,
51
+ } = createOrgRoles(() => props.organizationId);
52
+
53
+ const [editingUserId, setEditingUserId] = createSignal<string | null>(null);
54
+ const [editingRole, setEditingRole] = createSignal("");
55
+
56
+ const startEdit = (userId: string, role: string) => {
57
+ setEditingUserId(userId);
58
+ setEditingRole(role);
59
+ };
60
+
61
+ const cancelEdit = () => {
62
+ setEditingUserId(null);
63
+ setEditingRole("");
64
+ };
65
+
66
+ const saveRole = async (userId: string) => {
67
+ const updated = await changeRole(userId, { role: editingRole() });
68
+ if (updated) {
69
+ await refetch();
70
+ cancelEdit();
71
+ }
72
+ };
73
+
74
+ const doRemove = async (userId: string) => {
75
+ if (!confirm("Remove this member from the organization?")) return;
76
+ const ok = await removeMember(userId);
77
+ if (ok) await refetch();
78
+ };
79
+
80
+ const showActions = () =>
81
+ hasPermission("members:change_role") || hasPermission("members:remove");
82
+
83
+ return (
84
+ <div class="space-y-3">
85
+ <Show when={error() || actionError()}>
86
+ <div
87
+ class="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive"
88
+ role="alert"
89
+ aria-live="polite"
90
+ >
91
+ {error() || actionError()}
92
+ </div>
93
+ </Show>
94
+
95
+ <Show when={loading() && members().length === 0}>
96
+ <div class="text-sm text-muted-foreground">Loading members…</div>
97
+ </Show>
98
+
99
+ <Show when={members().length > 0}>
100
+ <table class="w-full text-sm" aria-label="Organization members">
101
+ <thead>
102
+ <tr class="border-b border-input text-left text-xs font-medium text-muted-foreground">
103
+ <th scope="col" class="py-2 pr-4">User</th>
104
+ <th scope="col" class="py-2 pr-4">Role</th>
105
+ <th scope="col" class="py-2 pr-4">Status</th>
106
+ <th scope="col" class="py-2 pr-4">Joined</th>
107
+ <Show when={showActions()}>
108
+ <th scope="col" class="py-2 pr-4 text-right">Actions</th>
109
+ </Show>
110
+ </tr>
111
+ </thead>
112
+ <tbody>
113
+ <For each={members()}>
114
+ {(m) => (
115
+ <tr class="border-b border-input/40">
116
+ <td class="py-2 pr-4 font-mono text-xs">{m.user_id}</td>
117
+ <td class="py-2 pr-4">
118
+ <Show
119
+ when={editingUserId() !== m.user_id}
120
+ fallback={
121
+ <RoleSelector
122
+ value={editingRole()}
123
+ disabled={submitting()}
124
+ onChange={setEditingRole}
125
+ />
126
+ }
127
+ >
128
+ <span
129
+ class={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${roleBadgeClass(m.role)}`}
130
+ >
131
+ {m.role}
132
+ </span>
133
+ </Show>
134
+ </td>
135
+ <td class="py-2 pr-4 text-xs text-muted-foreground">
136
+ {m.status}
137
+ </td>
138
+ <td class="py-2 pr-4 text-xs text-muted-foreground">
139
+ {formatDate(m.joined_at)}
140
+ </td>
141
+ <Show when={showActions()}>
142
+ <td class="py-2 pr-4 text-right">
143
+ <div class="inline-flex gap-2">
144
+ <Show
145
+ when={editingUserId() === m.user_id}
146
+ fallback={
147
+ <>
148
+ <Show
149
+ when={
150
+ hasPermission("members:change_role") &&
151
+ m.role !== ROLES.OWNER
152
+ }
153
+ >
154
+ <button
155
+ type="button"
156
+ class="rounded-md border border-input px-2 py-1 text-xs hover:bg-secondary"
157
+ onClick={() => startEdit(m.user_id, m.role)}
158
+ >
159
+ Change role
160
+ </button>
161
+ </Show>
162
+ <Show
163
+ when={
164
+ hasPermission("members:remove") &&
165
+ m.role !== ROLES.OWNER
166
+ }
167
+ >
168
+ <button
169
+ type="button"
170
+ class="rounded-md border border-destructive/40 px-2 py-1 text-xs text-destructive hover:bg-destructive/10"
171
+ disabled={submitting()}
172
+ onClick={() => doRemove(m.user_id)}
173
+ >
174
+ Remove
175
+ </button>
176
+ </Show>
177
+ </>
178
+ }
179
+ >
180
+ <button
181
+ type="button"
182
+ class="rounded-md bg-primary px-2 py-1 text-xs font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
183
+ disabled={submitting()}
184
+ onClick={() => saveRole(m.user_id)}
185
+ >
186
+ Save
187
+ </button>
188
+ <button
189
+ type="button"
190
+ class="rounded-md border border-input px-2 py-1 text-xs hover:bg-secondary"
191
+ disabled={submitting()}
192
+ onClick={cancelEdit}
193
+ >
194
+ Cancel
195
+ </button>
196
+ </Show>
197
+ </div>
198
+ </td>
199
+ </Show>
200
+ </tr>
201
+ )}
202
+ </For>
203
+ </tbody>
204
+ </table>
205
+ </Show>
206
+
207
+ <Show when={!loading() && members().length === 0}>
208
+ <div class="rounded-md border border-dashed border-input px-4 py-6 text-center text-sm text-muted-foreground">
209
+ No members yet.
210
+ </div>
211
+ </Show>
212
+ </div>
213
+ );
214
+ };
@@ -0,0 +1,45 @@
1
+ import type { OrganizationResponse } from "@yackey-labs/yauth-client";
2
+ import type { Component } from "solid-js";
3
+ import { Show } from "solid-js/web";
4
+
5
+ export interface OrganizationCardProps {
6
+ organization: OrganizationResponse;
7
+ role?: string | null;
8
+ memberCount?: number | null;
9
+ onSelect?: (org: OrganizationResponse) => void;
10
+ }
11
+
12
+ export const OrganizationCard: Component<OrganizationCardProps> = (props) => {
13
+ return (
14
+ <button
15
+ class="flex w-full items-start justify-between gap-4 rounded-md border border-input bg-card px-4 py-3 text-left text-card-foreground shadow-sm transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
16
+ type="button"
17
+ on:click={() => props.onSelect?.(props.organization)}
18
+ >
19
+ <div class="min-w-0 flex-1">
20
+ <div class="flex items-center gap-2">
21
+ <span class="truncate text-sm font-medium">
22
+ {props.organization.display_name || props.organization.name}
23
+ </span>
24
+ <Show when={props.role}>
25
+ <span class="rounded-full bg-secondary px-2 py-0.5 text-xs font-medium text-secondary-foreground">
26
+ {props.role}
27
+ </span>
28
+ </Show>
29
+ </div>
30
+ <div class="mt-1 truncate text-xs text-muted-foreground">
31
+ @{props.organization.slug}
32
+ </div>
33
+ </div>
34
+ <Show
35
+ when={
36
+ props.memberCount !== null && props.memberCount !== undefined
37
+ }
38
+ >
39
+ <div class="shrink-0 text-xs text-muted-foreground">
40
+ {props.memberCount} {props.memberCount === 1 ? "member" : "members"}
41
+ </div>
42
+ </Show>
43
+ </button>
44
+ );
45
+ };
@@ -0,0 +1,147 @@
1
+ import type { OrganizationResponse } from "@yackey-labs/yauth-client";
2
+ import { type Component, createEffect, createSignal } from "solid-js";
3
+ import { Show } from "solid-js/web";
4
+ import {
5
+ createOrganizations,
6
+ slugify,
7
+ } from "../hooks/create-organizations";
8
+
9
+ export interface OrganizationCreateProps {
10
+ onSuccess?: (org: OrganizationResponse) => void;
11
+ onError?: (error: Error) => void;
12
+ }
13
+
14
+ export const OrganizationCreate: Component<OrganizationCreateProps> = (
15
+ props,
16
+ ) => {
17
+ const { create, error } = createOrganizations();
18
+
19
+ const [name, setName] = createSignal("");
20
+ const [displayName, setDisplayName] = createSignal("");
21
+ const [slug, setSlug] = createSignal("");
22
+ const [slugTouched, setSlugTouched] = createSignal(false);
23
+ const [submitting, setSubmitting] = createSignal(false);
24
+
25
+ createEffect(() => {
26
+ const auto = slugify(name());
27
+ if (!slugTouched()) {
28
+ setSlug(auto);
29
+ }
30
+ });
31
+
32
+ const onSlugInput = (e: InputEvent & { currentTarget: HTMLInputElement }) => {
33
+ setSlugTouched(true);
34
+ setSlug(slugify(e.currentTarget.value));
35
+ };
36
+
37
+ const handleSubmit = async (e: SubmitEvent) => {
38
+ e.preventDefault();
39
+ if (!name().trim() || !slug().trim()) return;
40
+ setSubmitting(true);
41
+ const org = await create({
42
+ name: name().trim(),
43
+ slug: slug().trim(),
44
+ display_name: displayName().trim() || undefined,
45
+ });
46
+ setSubmitting(false);
47
+ if (org) {
48
+ props.onSuccess?.(org);
49
+ setName("");
50
+ setDisplayName("");
51
+ setSlug("");
52
+ setSlugTouched(false);
53
+ } else if (error()) {
54
+ props.onError?.(new Error(error() as string));
55
+ }
56
+ };
57
+
58
+ return (
59
+ <form class="space-y-6" on:submit={handleSubmit}>
60
+ <Show when={error()}>
61
+ <div
62
+ class="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive"
63
+ role="alert"
64
+ aria-live="polite"
65
+ >
66
+ {error()}
67
+ </div>
68
+ </Show>
69
+
70
+ <div class="space-y-2">
71
+ <label
72
+ class="text-sm font-medium leading-none"
73
+ for="yauth-org-create-name"
74
+ >
75
+ Organization name
76
+ </label>
77
+ <input
78
+ class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
79
+ id="yauth-org-create-name"
80
+ name="name"
81
+ type="text"
82
+ value={name()}
83
+ on:input={(e) => setName(e.currentTarget.value)}
84
+ required
85
+ maxlength={120}
86
+ autocomplete="organization"
87
+ disabled={submitting()}
88
+ />
89
+ </div>
90
+
91
+ <div class="space-y-2">
92
+ <label
93
+ class="text-sm font-medium leading-none"
94
+ for="yauth-org-create-slug"
95
+ >
96
+ Slug
97
+ </label>
98
+ <input
99
+ class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 font-mono text-base shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
100
+ id="yauth-org-create-slug"
101
+ name="slug"
102
+ type="text"
103
+ value={slug()}
104
+ on:input={onSlugInput}
105
+ required
106
+ pattern="[a-z0-9-]+"
107
+ maxlength={64}
108
+ aria-describedby="yauth-org-create-slug-hint"
109
+ disabled={submitting()}
110
+ />
111
+ <p
112
+ id="yauth-org-create-slug-hint"
113
+ class="text-xs text-muted-foreground"
114
+ >
115
+ URL-safe identifier. Lowercase letters, numbers, and hyphens only.
116
+ </p>
117
+ </div>
118
+
119
+ <div class="space-y-2">
120
+ <label
121
+ class="text-sm font-medium leading-none"
122
+ for="yauth-org-create-display-name"
123
+ >
124
+ Display name (optional)
125
+ </label>
126
+ <input
127
+ class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
128
+ id="yauth-org-create-display-name"
129
+ name="display_name"
130
+ type="text"
131
+ value={displayName()}
132
+ on:input={(e) => setDisplayName(e.currentTarget.value)}
133
+ maxlength={120}
134
+ disabled={submitting()}
135
+ />
136
+ </div>
137
+
138
+ <button
139
+ class="inline-flex h-9 w-full cursor-pointer items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
140
+ type="submit"
141
+ disabled={submitting() || !name().trim() || !slug().trim()}
142
+ >
143
+ {submitting() ? "Creating…" : "Create organization"}
144
+ </button>
145
+ </form>
146
+ );
147
+ };
@@ -0,0 +1,68 @@
1
+ import type { Component } from "solid-js";
2
+ import { Show } from "solid-js/web";
3
+ import { createOrganization } from "../hooks/create-organizations";
4
+ import { InviteForm } from "./invite-form";
5
+ import { MemberList } from "./member-list";
6
+
7
+ export interface OrganizationDetailProps {
8
+ organizationId: string;
9
+ /** When false, hide the invite form section. */
10
+ canInvite?: boolean;
11
+ }
12
+
13
+ export const OrganizationDetail: Component<OrganizationDetailProps> = (
14
+ props,
15
+ ) => {
16
+ const { organization, loading, error } = createOrganization(
17
+ () => props.organizationId,
18
+ );
19
+
20
+ return (
21
+ <section class="space-y-6">
22
+ <Show when={error()}>
23
+ <div
24
+ class="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive"
25
+ role="alert"
26
+ aria-live="polite"
27
+ >
28
+ {error()}
29
+ </div>
30
+ </Show>
31
+
32
+ <Show when={loading() && !organization()}>
33
+ <div class="text-sm text-muted-foreground">Loading organization…</div>
34
+ </Show>
35
+
36
+ <Show when={organization()}>
37
+ {(org) => (
38
+ <>
39
+ <div class="space-y-2">
40
+ <h2 class="text-lg font-semibold">
41
+ {org().display_name || org().name}
42
+ </h2>
43
+ <p class="font-mono text-xs text-muted-foreground">
44
+ @{org().slug}
45
+ </p>
46
+ </div>
47
+
48
+ <div class="space-y-3">
49
+ <h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
50
+ Members
51
+ </h3>
52
+ <MemberList organizationId={org().id} />
53
+ </div>
54
+
55
+ <Show when={props.canInvite !== false}>
56
+ <div class="space-y-3">
57
+ <h3 class="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
58
+ Invite a member
59
+ </h3>
60
+ <InviteForm organizationId={org().id} />
61
+ </div>
62
+ </Show>
63
+ </>
64
+ )}
65
+ </Show>
66
+ </section>
67
+ );
68
+ };
@@ -0,0 +1,54 @@
1
+ import type { OrganizationResponse } from "@yackey-labs/yauth-client";
2
+ import { type Component, For } from "solid-js";
3
+ import { Show } from "solid-js/web";
4
+ import { createOrganizations } from "../hooks/create-organizations";
5
+ import { OrganizationCard } from "./organization-card";
6
+
7
+ export interface OrganizationListProps {
8
+ onSelect?: (org: OrganizationResponse) => void;
9
+ }
10
+
11
+ export const OrganizationList: Component<OrganizationListProps> = (props) => {
12
+ const { organizations, loading, error } = createOrganizations();
13
+
14
+ return (
15
+ <div class="space-y-3">
16
+ <Show when={error()}>
17
+ <div
18
+ class="rounded-md bg-destructive/10 px-3 py-2 text-sm text-destructive"
19
+ role="alert"
20
+ aria-live="polite"
21
+ >
22
+ {error()}
23
+ </div>
24
+ </Show>
25
+
26
+ <Show when={loading() && organizations().length === 0}>
27
+ <div class="text-sm text-muted-foreground">
28
+ Loading organizations…
29
+ </div>
30
+ </Show>
31
+
32
+ <Show when={!loading() && organizations().length === 0}>
33
+ <div class="rounded-md border border-dashed border-input px-4 py-6 text-center text-sm text-muted-foreground">
34
+ You're not in any organizations yet.
35
+ </div>
36
+ </Show>
37
+
38
+ <Show when={organizations().length > 0}>
39
+ <ul class="space-y-2">
40
+ <For each={organizations()}>
41
+ {(org) => (
42
+ <li>
43
+ <OrganizationCard
44
+ organization={org}
45
+ onSelect={props.onSelect}
46
+ />
47
+ </li>
48
+ )}
49
+ </For>
50
+ </ul>
51
+ </Show>
52
+ </div>
53
+ );
54
+ };
@@ -0,0 +1,139 @@
1
+ import type { ActiveOrgEntry } from "@yackey-labs/yauth-client";
2
+ import { type Component, createSignal, For, onCleanup } from "solid-js";
3
+ import { Show } from "solid-js/web";
4
+ import { createActiveOrg } from "../hooks/create-active-org";
5
+
6
+ export interface OrganizationSwitcherProps {
7
+ /**
8
+ * Override the active org id (e.g. for SSR or test fixtures). When
9
+ * omitted the component reads from `createActiveOrg()` and is fully
10
+ * self-driving.
11
+ */
12
+ activeId?: string | null;
13
+ /**
14
+ * Callback fired after a successful switch — receives the membership
15
+ * entry the caller switched into. Useful for clients that need to
16
+ * adopt a freshly-issued bearer token from `switchTo`.
17
+ */
18
+ onSwitch?: (org: ActiveOrgEntry, bearerToken: string | null) => void;
19
+ }
20
+
21
+ /**
22
+ * Organization-switcher dropdown (issue #89).
23
+ *
24
+ * Self-driving by default: reads the active-org claim and full membership
25
+ * list from the server via `createActiveOrg`, and dispatches switches
26
+ * through the active-org endpoint. Cookie callers update server-side;
27
+ * bearer callers receive a freshly-issued JWT in the response that the
28
+ * `onSwitch` callback can adopt.
29
+ */
30
+ export const OrganizationSwitcher: Component<OrganizationSwitcherProps> = (props) => {
31
+ const { activeOrgId, orgs, loading, switchTo } = createActiveOrg();
32
+ const [open, setOpen] = createSignal(false);
33
+ let containerEl: HTMLDivElement | undefined;
34
+
35
+ const effectiveActiveId = () => (props.activeId === undefined ? activeOrgId() : props.activeId);
36
+ const activeOrg = () => orgs().find((o) => o.organization_id === effectiveActiveId()) ?? null;
37
+
38
+ const handleClickOutside = (e: MouseEvent) => {
39
+ if (!containerEl) return;
40
+ if (!containerEl.contains(e.target as Node)) {
41
+ setOpen(false);
42
+ document.removeEventListener("mousedown", handleClickOutside);
43
+ }
44
+ };
45
+
46
+ const toggle = () => {
47
+ const next = !open();
48
+ setOpen(next);
49
+ if (next) {
50
+ document.addEventListener("mousedown", handleClickOutside);
51
+ } else {
52
+ document.removeEventListener("mousedown", handleClickOutside);
53
+ }
54
+ };
55
+
56
+ const choose = async (org: ActiveOrgEntry) => {
57
+ setOpen(false);
58
+ document.removeEventListener("mousedown", handleClickOutside);
59
+ const bearerToken = await switchTo(org.organization_id);
60
+ props.onSwitch?.(org, bearerToken);
61
+ };
62
+
63
+ onCleanup(() => {
64
+ document.removeEventListener("mousedown", handleClickOutside);
65
+ });
66
+
67
+ return (
68
+ <div ref={containerEl} class="relative inline-block w-full">
69
+ <button
70
+ class="inline-flex h-9 w-full cursor-pointer items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
71
+ type="button"
72
+ aria-expanded={open()}
73
+ aria-haspopup="listbox"
74
+ aria-label="Switch organization"
75
+ disabled={loading() && orgs().length === 0}
76
+ on:click={toggle}
77
+ >
78
+ <span class="truncate">
79
+ {activeOrg()?.display_name ||
80
+ activeOrg()?.slug ||
81
+ (loading() ? "Loading…" : "Select organization")}
82
+ </span>
83
+ <svg
84
+ class="h-4 w-4 shrink-0 opacity-50"
85
+ viewBox="0 0 24 24"
86
+ fill="none"
87
+ stroke="currentColor"
88
+ stroke-width="2"
89
+ aria-hidden="true"
90
+ >
91
+ <polyline points="6 9 12 15 18 9" />
92
+ </svg>
93
+ </button>
94
+
95
+ <Show when={open()}>
96
+ <div
97
+ class="absolute z-50 mt-1 max-h-64 w-full overflow-auto rounded-md border border-input bg-popover text-popover-foreground shadow-md"
98
+ role="listbox"
99
+ >
100
+ <For each={orgs()}>
101
+ {(org) => (
102
+ <button
103
+ class="flex w-full cursor-pointer items-center justify-between gap-2 px-3 py-2 text-left text-sm hover:bg-accent focus-visible:bg-accent focus-visible:outline-none"
104
+ type="button"
105
+ role="option"
106
+ aria-selected={org.organization_id === effectiveActiveId()}
107
+ on:click={() => void choose(org)}
108
+ >
109
+ <div class="min-w-0 flex-1">
110
+ <div class="truncate font-medium">
111
+ {org.display_name || org.slug || org.organization_id}
112
+ </div>
113
+ <Show when={org.slug}>
114
+ <div class="truncate font-mono text-xs text-muted-foreground">@{org.slug}</div>
115
+ </Show>
116
+ </div>
117
+ <Show when={org.organization_id === effectiveActiveId()}>
118
+ <svg
119
+ class="h-4 w-4 shrink-0"
120
+ viewBox="0 0 24 24"
121
+ fill="none"
122
+ stroke="currentColor"
123
+ stroke-width="2"
124
+ aria-hidden="true"
125
+ >
126
+ <polyline points="20 6 9 17 4 12" />
127
+ </svg>
128
+ </Show>
129
+ </button>
130
+ )}
131
+ </For>
132
+ <Show when={orgs().length === 0}>
133
+ <div class="px-3 py-2 text-sm text-muted-foreground">No organizations.</div>
134
+ </Show>
135
+ </div>
136
+ </Show>
137
+ </div>
138
+ );
139
+ };