feat: map shapes, image persistence, grab tool, layer rename/delete, i18n, page metadata

This commit is contained in:
AlacrisDevs
2026-02-08 23:11:09 +02:00
parent 75a2aefadb
commit f2384bceb8
125 changed files with 22605 additions and 3902 deletions

View File

@@ -1,18 +1,55 @@
<script lang="ts">
import { Button, Input, Avatar } from "$lib/components/ui";
import {
Button,
Input,
Avatar,
Select,
Textarea,
} from "$lib/components/ui";
import type { SupabaseClient } from "@supabase/supabase-js";
import * as m from "$lib/paraglide/messages";
import type { Database } from "$lib/supabase/types";
import { toasts } from "$lib/stores/toast.svelte";
import { invalidateAll } from "$app/navigation";
import {
SUPPORTED_CURRENCIES,
DATE_FORMAT_OPTIONS,
WEEK_START_OPTIONS,
CALENDAR_VIEW_OPTIONS,
TIMEZONE_OPTIONS,
EVENT_STATUS_OPTIONS,
DASHBOARD_LAYOUT_OPTIONS,
DEPT_MODULE_OPTIONS,
EVENT_COLOR_PRESETS,
formatCurrency,
} from "$lib/utils/currency";
interface OrgData {
id: string;
name: string;
slug: string;
avatar_url?: string | null;
description?: string | null;
theme_color?: string | null;
currency?: string;
date_format?: string;
timezone?: string;
week_start_day?: string;
default_calendar_view?: string;
default_event_color?: string;
default_event_status?: string;
default_dept_modules?: string[];
default_dept_layout?: string;
feature_chat?: boolean;
feature_sponsors?: boolean;
feature_contacts?: boolean;
feature_budget?: boolean;
social_links?: Record<string, string>;
}
interface Props {
supabase: SupabaseClient<Database>;
org: {
id: string;
name: string;
slug: string;
avatar_url?: string | null;
};
org: OrgData;
isOwner: boolean;
onLeave: () => void;
onDelete: () => void;
@@ -20,20 +57,116 @@
let { supabase, org, isOwner, onLeave, onDelete }: Props = $props();
// ── Organization details ──
// svelte-ignore state_referenced_locally
let orgName = $state(org.name);
// svelte-ignore state_referenced_locally
let orgSlug = $state(org.slug);
// svelte-ignore state_referenced_locally
let avatarUrl = $state(org.avatar_url ?? null);
// svelte-ignore state_referenced_locally
let orgDescription = $state(org.description ?? "");
// svelte-ignore state_referenced_locally
let themeColor = $state(org.theme_color ?? "#00a3e0");
let isSaving = $state(false);
let isUploading = $state(false);
let avatarInput = $state<HTMLInputElement | null>(null);
// ── Preferences ──
// svelte-ignore state_referenced_locally
let currency = $state(org.currency ?? "EUR");
// svelte-ignore state_referenced_locally
let dateFormat = $state(org.date_format ?? "DD/MM/YYYY");
// svelte-ignore state_referenced_locally
let timezone = $state(org.timezone ?? "Europe/Tallinn");
// svelte-ignore state_referenced_locally
let weekStartDay = $state(org.week_start_day ?? "monday");
// svelte-ignore state_referenced_locally
let defaultCalendarView = $state(org.default_calendar_view ?? "month");
let isSavingPrefs = $state(false);
// ── Event defaults ──
// svelte-ignore state_referenced_locally
let defaultEventColor = $state(org.default_event_color ?? "#7986cb");
// svelte-ignore state_referenced_locally
let defaultEventStatus = $state(org.default_event_status ?? "planning");
// svelte-ignore state_referenced_locally
let defaultDeptModules = $state<string[]>(
org.default_dept_modules ?? ["kanban", "files", "checklist"],
);
// svelte-ignore state_referenced_locally
let defaultDeptLayout = $state(org.default_dept_layout ?? "split");
let isSavingDefaults = $state(false);
// ── Social links ──
// svelte-ignore state_referenced_locally
let socialWebsite = $state(org.social_links?.website ?? "");
// svelte-ignore state_referenced_locally
let socialInstagram = $state(org.social_links?.instagram ?? "");
// svelte-ignore state_referenced_locally
let socialFacebook = $state(org.social_links?.facebook ?? "");
// svelte-ignore state_referenced_locally
let socialDiscord = $state(org.social_links?.discord ?? "");
// svelte-ignore state_referenced_locally
let socialLinkedin = $state(org.social_links?.linkedin ?? "");
// svelte-ignore state_referenced_locally
let socialX = $state(org.social_links?.x ?? "");
// svelte-ignore state_referenced_locally
let socialYoutube = $state(org.social_links?.youtube ?? "");
// svelte-ignore state_referenced_locally
let socialTiktok = $state(org.social_links?.tiktok ?? "");
// svelte-ignore state_referenced_locally
let socialFienta = $state(org.social_links?.fienta ?? "");
// svelte-ignore state_referenced_locally
let socialTwitch = $state(org.social_links?.twitch ?? "");
let isSavingSocial = $state(false);
// ── Feature toggles ──
// svelte-ignore state_referenced_locally
let featureChat = $state(org.feature_chat ?? true);
// svelte-ignore state_referenced_locally
let featureSponsors = $state(org.feature_sponsors ?? true);
// svelte-ignore state_referenced_locally
let featureContacts = $state(org.feature_contacts ?? true);
// svelte-ignore state_referenced_locally
let featureBudget = $state(org.feature_budget ?? true);
let isSavingFeatures = $state(false);
const currencyPreview = $derived(formatCurrency(1234.56, currency));
$effect(() => {
orgName = org.name;
orgSlug = org.slug;
avatarUrl = org.avatar_url ?? null;
orgDescription = org.description ?? "";
themeColor = org.theme_color ?? "#00a3e0";
currency = org.currency ?? "EUR";
dateFormat = org.date_format ?? "DD/MM/YYYY";
timezone = org.timezone ?? "Europe/Tallinn";
weekStartDay = org.week_start_day ?? "monday";
defaultCalendarView = org.default_calendar_view ?? "month";
defaultEventColor = org.default_event_color ?? "#7986cb";
defaultEventStatus = org.default_event_status ?? "planning";
defaultDeptModules = org.default_dept_modules ?? [
"kanban",
"files",
"checklist",
];
defaultDeptLayout = org.default_dept_layout ?? "split";
socialWebsite = org.social_links?.website ?? "";
socialInstagram = org.social_links?.instagram ?? "";
socialFacebook = org.social_links?.facebook ?? "";
socialDiscord = org.social_links?.discord ?? "";
socialLinkedin = org.social_links?.linkedin ?? "";
socialX = org.social_links?.x ?? "";
socialYoutube = org.social_links?.youtube ?? "";
socialTiktok = org.social_links?.tiktok ?? "";
socialFienta = org.social_links?.fienta ?? "";
socialTwitch = org.social_links?.twitch ?? "";
featureChat = org.feature_chat ?? true;
featureSponsors = org.feature_sponsors ?? true;
featureContacts = org.feature_contacts ?? true;
featureBudget = org.feature_budget ?? true;
});
async function handleAvatarUpload(e: Event) {
@@ -41,7 +174,6 @@
const file = input.files?.[0];
if (!file) return;
// Validate file
if (!file.type.startsWith("image/")) {
toasts.error("Please select an image file.");
return;
@@ -113,7 +245,12 @@
isSaving = true;
const { error } = await supabase
.from("organizations")
.update({ name: orgName, slug: orgSlug })
.update({
name: orgName,
slug: orgSlug,
description: orgDescription.trim(),
theme_color: themeColor,
})
.eq("id", org.id);
if (error) {
@@ -121,15 +258,118 @@
} else if (orgSlug !== org.slug) {
window.location.href = `/${orgSlug}/settings`;
} else {
await invalidateAll();
toasts.success("Settings saved.");
}
isSaving = false;
}
async function savePreferences() {
isSavingPrefs = true;
const { error } = await supabase
.from("organizations")
.update({
currency,
date_format: dateFormat,
timezone,
week_start_day: weekStartDay,
default_calendar_view: defaultCalendarView,
})
.eq("id", org.id);
if (error) {
toasts.error("Failed to save preferences.");
} else {
await invalidateAll();
toasts.success("Preferences saved.");
}
isSavingPrefs = false;
}
async function saveEventDefaults() {
isSavingDefaults = true;
const { error } = await supabase
.from("organizations")
.update({
default_event_color: defaultEventColor,
default_event_status: defaultEventStatus,
default_dept_modules: defaultDeptModules,
default_dept_layout: defaultDeptLayout,
})
.eq("id", org.id);
if (error) {
toasts.error("Failed to save event defaults.");
} else {
await invalidateAll();
toasts.success("Event defaults saved.");
}
isSavingDefaults = false;
}
async function saveFeatureToggles() {
isSavingFeatures = true;
const { error } = await supabase
.from("organizations")
.update({
feature_chat: featureChat,
feature_sponsors: featureSponsors,
feature_contacts: featureContacts,
feature_budget: featureBudget,
})
.eq("id", org.id);
if (error) {
toasts.error("Failed to save feature settings.");
} else {
await invalidateAll();
toasts.success("Feature settings saved.");
}
isSavingFeatures = false;
}
async function saveSocialLinks() {
isSavingSocial = true;
const links: Record<string, string> = {};
if (socialWebsite.trim()) links.website = socialWebsite.trim();
if (socialInstagram.trim()) links.instagram = socialInstagram.trim();
if (socialFacebook.trim()) links.facebook = socialFacebook.trim();
if (socialDiscord.trim()) links.discord = socialDiscord.trim();
if (socialLinkedin.trim()) links.linkedin = socialLinkedin.trim();
if (socialX.trim()) links.x = socialX.trim();
if (socialYoutube.trim()) links.youtube = socialYoutube.trim();
if (socialTiktok.trim()) links.tiktok = socialTiktok.trim();
if (socialFienta.trim()) links.fienta = socialFienta.trim();
if (socialTwitch.trim()) links.twitch = socialTwitch.trim();
const { error } = await (supabase as any)
.from("organizations")
.update({ social_links: links })
.eq("id", org.id);
if (error) {
toasts.error("Failed to save social links.");
} else {
await invalidateAll();
toasts.success("Social links saved.");
}
isSavingSocial = false;
}
function toggleModule(mod: string) {
if (defaultDeptModules.includes(mod)) {
defaultDeptModules = defaultDeptModules.filter((m) => m !== mod);
} else {
defaultDeptModules = [...defaultDeptModules, mod];
}
}
</script>
<div class="flex flex-col gap-6 max-w-2xl">
<!-- Organization Details -->
<div class="bg-dark/30 border border-light/5 rounded-xl p-5 flex flex-col gap-5">
<div
class="bg-dark/30 border border-light/5 rounded-xl p-5 flex flex-col gap-5"
>
<h2 class="font-heading text-body text-white">Organization details</h2>
<!-- Avatar Upload -->
@@ -175,6 +415,29 @@
bind:value={orgSlug}
placeholder="my-org"
/>
<Textarea
variant="compact"
label="Description"
bind:value={orgDescription}
placeholder="What does your organization do?"
rows={2}
resize="none"
/>
<!-- Theme Color (hidden for now) -->
<!-- <div class="flex flex-col gap-2">
<span class="font-body text-body-sm text-light/60">Theme color</span>
<div class="flex items-center gap-3">
<label
class="w-8 h-8 rounded-lg cursor-pointer overflow-hidden border border-light/10"
style="background-color: {themeColor}"
>
<input type="color" bind:value={themeColor} class="opacity-0 w-0 h-0" />
</label>
<span class="text-[12px] text-light/40 font-mono">{themeColor}</span>
</div>
</div> -->
<div>
<Button size="sm" onclick={saveGeneralSettings} loading={isSaving}
>Save Changes</Button
@@ -182,9 +445,401 @@
</div>
</div>
<!-- Social & Links -->
<div
class="bg-dark/30 border border-light/5 rounded-xl p-5 flex flex-col gap-5"
>
<div>
<h2 class="font-heading text-body text-white">
{m.settings_social_title()}
</h2>
<p class="text-[11px] text-light/40 mt-0.5">
{m.settings_social_desc()}
</p>
</div>
<Input
variant="compact"
type="url"
label={m.settings_social_website()}
bind:value={socialWebsite}
placeholder={m.settings_social_website_placeholder()}
icon="language"
/>
<div class="grid grid-cols-2 gap-4">
<Input
variant="compact"
type="url"
label={m.settings_social_instagram()}
bind:value={socialInstagram}
placeholder={m.settings_social_instagram_placeholder()}
/>
<Input
variant="compact"
type="url"
label={m.settings_social_facebook()}
bind:value={socialFacebook}
placeholder={m.settings_social_facebook_placeholder()}
/>
</div>
<div class="grid grid-cols-2 gap-4">
<Input
variant="compact"
type="url"
label={m.settings_social_discord()}
bind:value={socialDiscord}
placeholder={m.settings_social_discord_placeholder()}
/>
<Input
variant="compact"
type="url"
label={m.settings_social_linkedin()}
bind:value={socialLinkedin}
placeholder={m.settings_social_linkedin_placeholder()}
/>
</div>
<div class="grid grid-cols-2 gap-4">
<Input
variant="compact"
type="url"
label={m.settings_social_x()}
bind:value={socialX}
placeholder={m.settings_social_x_placeholder()}
/>
<Input
variant="compact"
type="url"
label={m.settings_social_youtube()}
bind:value={socialYoutube}
placeholder={m.settings_social_youtube_placeholder()}
/>
</div>
<div class="grid grid-cols-2 gap-4">
<Input
variant="compact"
type="url"
label={m.settings_social_tiktok()}
bind:value={socialTiktok}
placeholder={m.settings_social_tiktok_placeholder()}
/>
<Input
variant="compact"
type="url"
label={m.settings_social_fienta()}
bind:value={socialFienta}
placeholder={m.settings_social_fienta_placeholder()}
/>
</div>
<Input
variant="compact"
type="url"
label={m.settings_social_twitch()}
bind:value={socialTwitch}
placeholder={m.settings_social_twitch_placeholder()}
/>
<div>
<Button size="sm" onclick={saveSocialLinks} loading={isSavingSocial}
>{m.settings_social_save()}</Button
>
</div>
</div>
<!-- Preferences -->
<div
class="bg-dark/30 border border-light/5 rounded-xl p-5 flex flex-col gap-5"
>
<div>
<h2 class="font-heading text-body text-white">Preferences</h2>
<p class="text-[11px] text-light/40 mt-0.5">
Currency, date format, timezone, and calendar defaults.
</p>
</div>
<!-- Currency -->
<Select
variant="compact"
label="Currency"
bind:value={currency}
placeholder=""
options={SUPPORTED_CURRENCIES.map((c) => ({
value: c.code,
label: c.label,
}))}
hint={`Preview: ${currencyPreview}`}
/>
<!-- Date Format -->
<Select
variant="compact"
label="Date format"
bind:value={dateFormat}
placeholder=""
options={DATE_FORMAT_OPTIONS}
/>
<!-- Timezone -->
<Select
variant="compact"
label="Timezone"
bind:value={timezone}
placeholder=""
groups={TIMEZONE_OPTIONS.map((g) => ({
label: g.group,
options: g.zones.map((z) => ({
value: z,
label: z.replace(/_/g, " "),
})),
}))}
/>
<div class="grid grid-cols-2 gap-4">
<Select
variant="compact"
label="Week starts on"
bind:value={weekStartDay}
placeholder=""
options={WEEK_START_OPTIONS}
/>
<Select
variant="compact"
label="Default calendar view"
bind:value={defaultCalendarView}
placeholder=""
options={CALENDAR_VIEW_OPTIONS}
/>
</div>
<div>
<Button size="sm" onclick={savePreferences} loading={isSavingPrefs}
>Save Preferences</Button
>
</div>
</div>
<!-- Event Defaults -->
<div
class="bg-dark/30 border border-light/5 rounded-xl p-5 flex flex-col gap-5"
>
<div>
<h2 class="font-heading text-body text-white">Event defaults</h2>
<p class="text-[11px] text-light/40 mt-0.5">
Defaults applied when creating new events and departments.
</p>
</div>
<!-- Default Event Color -->
<div class="flex flex-col gap-2">
<span class="font-body text-body-sm text-light/60"
>Default event color</span
>
<div class="flex flex-wrap gap-2">
{#each EVENT_COLOR_PRESETS as color}
<button
type="button"
class="w-7 h-7 rounded-lg border-2 transition-all {defaultEventColor ===
color
? 'border-white scale-110'
: 'border-transparent hover:border-light/30'}"
style="background-color: {color}"
onclick={() => (defaultEventColor = color)}
></button>
{/each}
<label
class="w-7 h-7 rounded-lg border-2 border-dashed border-light/20 hover:border-light/40 transition-all cursor-pointer flex items-center justify-center overflow-hidden"
title="Custom color"
>
<input
type="color"
class="opacity-0 absolute w-0 h-0"
bind:value={defaultEventColor}
/>
<span
class="material-symbols-rounded text-light/30"
style="font-size: 14px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 14;"
>colorize</span
>
</label>
</div>
</div>
<!-- Default Event Status -->
<Select
variant="compact"
label="Default event status"
bind:value={defaultEventStatus}
placeholder=""
options={EVENT_STATUS_OPTIONS}
/>
<!-- Default Department Layout -->
<Select
variant="compact"
label="Default department layout"
bind:value={defaultDeptLayout}
placeholder=""
options={DASHBOARD_LAYOUT_OPTIONS}
/>
<!-- Default Department Modules -->
<div class="flex flex-col gap-2">
<span class="font-body text-body-sm text-light/60"
>Default department modules</span
>
<p class="text-[11px] text-light/30">
Modules auto-added when a new department is created.
</p>
<div class="grid grid-cols-2 gap-2">
{#each DEPT_MODULE_OPTIONS as mod}
<button
type="button"
class="flex items-center gap-2.5 px-3 py-2.5 rounded-xl border transition-all text-left {defaultDeptModules.includes(
mod.value,
)
? 'bg-primary/10 border-primary/30 text-white'
: 'bg-dark/30 border-light/5 text-light/40 hover:border-light/10'}"
onclick={() => toggleModule(mod.value)}
>
<span
class="material-symbols-rounded {defaultDeptModules.includes(
mod.value,
)
? 'text-primary'
: 'text-light/30'}"
style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;"
>{mod.icon}</span
>
<span class="text-body-sm font-body">{mod.label}</span>
</button>
{/each}
</div>
</div>
<div>
<Button
size="sm"
onclick={saveEventDefaults}
loading={isSavingDefaults}>Save Event Defaults</Button
>
</div>
</div>
<!-- Feature Toggles -->
<div
class="bg-dark/30 border border-light/5 rounded-xl p-5 flex flex-col gap-5"
>
<div>
<h2 class="font-heading text-body text-white">Features</h2>
<p class="text-[11px] text-light/40 mt-0.5">
Enable or disable features for this organization.
</p>
</div>
<div class="flex flex-col gap-3">
<!-- Chat toggle hidden for now -->
<!-- <label
class="flex items-center justify-between px-3 py-3 rounded-xl bg-dark/30 border border-light/5 cursor-pointer hover:border-light/10 transition-colors"
>
<div class="flex items-center gap-3">
<span class="material-symbols-rounded text-purple-400" style="font-size: 20px;">chat</span>
<div>
<p class="text-body-sm text-white">Chat</p>
<p class="text-[11px] text-light/30">Real-time messaging via Matrix</p>
</div>
</div>
<input type="checkbox" bind:checked={featureChat} class="w-4 h-4 rounded accent-primary" />
</label> -->
<label
class="flex items-center justify-between px-3 py-3 rounded-xl bg-dark/30 border border-light/5 cursor-pointer hover:border-light/10 transition-colors"
>
<div class="flex items-center gap-3">
<span
class="material-symbols-rounded text-emerald-400"
style="font-size: 20px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;"
>account_balance</span
>
<div>
<p class="text-body-sm text-white">Budget & Finances</p>
<p class="text-[11px] text-light/30">
Income/expense tracking, planned vs actual budgets
</p>
</div>
</div>
<input
type="checkbox"
bind:checked={featureBudget}
class="w-4 h-4 rounded accent-primary"
/>
</label>
<label
class="flex items-center justify-between px-3 py-3 rounded-xl bg-dark/30 border border-light/5 cursor-pointer hover:border-light/10 transition-colors"
>
<div class="flex items-center gap-3">
<span
class="material-symbols-rounded text-indigo-400"
style="font-size: 20px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;"
>handshake</span
>
<div>
<p class="text-body-sm text-white">Sponsors</p>
<p class="text-[11px] text-light/30">
Sponsor CRM with tiers, deliverables, and fund
tracking
</p>
</div>
</div>
<input
type="checkbox"
bind:checked={featureSponsors}
class="w-4 h-4 rounded accent-primary"
/>
</label>
<label
class="flex items-center justify-between px-3 py-3 rounded-xl bg-dark/30 border border-light/5 cursor-pointer hover:border-light/10 transition-colors"
>
<div class="flex items-center gap-3">
<span
class="material-symbols-rounded text-blue-400"
style="font-size: 20px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;"
>contacts</span
>
<div>
<p class="text-body-sm text-white">Contacts</p>
<p class="text-[11px] text-light/30">
Vendor and contact directory per department
</p>
</div>
</div>
<input
type="checkbox"
bind:checked={featureContacts}
class="w-4 h-4 rounded accent-primary"
/>
</label>
</div>
<div>
<Button
size="sm"
onclick={saveFeatureToggles}
loading={isSavingFeatures}>Save Features</Button
>
</div>
</div>
<!-- Danger Zone -->
{#if isOwner}
<div class="bg-dark/30 border border-error/10 rounded-xl p-5 flex flex-col gap-3">
<div
class="bg-dark/30 border border-error/10 rounded-xl p-5 flex flex-col gap-3"
>
<h4 class="font-heading text-body-sm text-error">Danger Zone</h4>
<p class="font-body text-[11px] text-light/40">
Permanently delete this organization and all its data.
@@ -199,10 +854,15 @@
<!-- Leave Organization (non-owners) -->
{#if !isOwner}
<div class="bg-dark/30 border border-light/5 rounded-xl p-5 flex flex-col gap-3">
<h4 class="font-heading text-body-sm text-white">Leave Organization</h4>
<div
class="bg-dark/30 border border-light/5 rounded-xl p-5 flex flex-col gap-3"
>
<h4 class="font-heading text-body-sm text-white">
Leave Organization
</h4>
<p class="font-body text-[11px] text-light/40">
Leave this organization. You will need to be re-invited to rejoin.
Leave this organization. You will need to be re-invited to
rejoin.
</p>
<div>
<Button variant="secondary" size="sm" onclick={onLeave}

View File

@@ -1,11 +1,5 @@
<script lang="ts">
import {
Button,
Modal,
Input,
Select,
Avatar,
} from "$lib/components/ui";
import { Button, Modal, Input, Select, Avatar } from "$lib/components/ui";
import { toasts } from "$lib/stores/toast.svelte";
import type { SupabaseClient } from "@supabase/supabase-js";
import type { Database } from "$lib/supabase/types";
@@ -177,7 +171,11 @@
})}
</h2>
</div>
<Button size="sm" icon="person_add" onclick={() => (showInviteModal = true)}>
<Button
size="sm"
icon="person_add"
onclick={() => (showInviteModal = true)}
>
{m.settings_members_invite()}
</Button>
</div>
@@ -194,7 +192,9 @@
class="flex items-center justify-between py-2 px-3 bg-light/5 rounded-lg"
>
<div>
<p class="text-body-sm text-white">{invite.email}</p>
<p class="text-body-sm text-white">
{invite.email}
</p>
<p class="text-[11px] text-light/40">
Invited as {invite.role} • Expires {new Date(
invite.expires_at,
@@ -211,7 +211,11 @@
)}
title={m.settings_members_copy_link()}
>
<span class="material-symbols-rounded" style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;">content_copy</span>
<span
class="material-symbols-rounded"
style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;"
>content_copy</span
>
</button>
<button
type="button"
@@ -219,7 +223,11 @@
onclick={() => cancelInvite(invite.id)}
title="Cancel invite"
>
<span class="material-symbols-rounded" style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;">close</span>
<span
class="material-symbols-rounded"
style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;"
>close</span
>
</button>
</div>
</div>
@@ -272,7 +280,11 @@
onclick={() => openMemberModal(member)}
title="Edit"
>
<span class="material-symbols-rounded" style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;">edit</span>
<span
class="material-symbols-rounded"
style="font-size: 18px; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 18;"
>edit</span
>
</button>
{/if}
</div>
@@ -289,31 +301,42 @@
title="Invite Member"
>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-1.5">
<label for="invite-email" class="text-body-sm text-light/60 font-body">Email address</label>
<input
id="invite-email"
type="email"
bind:value={inviteEmail}
placeholder="colleague@example.com"
class="bg-dark border border-light/10 rounded-xl px-3 py-2 text-body-sm text-white placeholder:text-light/30 focus:outline-none focus:border-primary"
/>
</div>
<div class="flex flex-col gap-1.5">
<label for="invite-role" class="text-body-sm text-light/60 font-body">Role</label>
<select
id="invite-role"
bind:value={inviteRole}
class="bg-dark border border-light/10 rounded-xl px-3 py-2 text-body-sm text-white focus:outline-none focus:border-primary"
<Input
variant="compact"
type="email"
label="Email address"
bind:value={inviteEmail}
placeholder="colleague@example.com"
/>
<Select
variant="compact"
label="Role"
bind:value={inviteRole}
placeholder=""
options={[
{ value: "viewer", label: "Viewer - Can view content" },
{
value: "commenter",
label: "Commenter - Can view and comment",
},
{
value: "editor",
label: "Editor - Can create and edit content",
},
{
value: "admin",
label: "Admin - Can manage members and settings",
},
]}
/>
<div
class="flex items-center justify-end gap-3 pt-2 border-t border-light/5"
>
<button
type="button"
class="px-4 py-2 text-body-sm text-light/60 hover:text-white transition-colors"
onclick={() => (showInviteModal = false)}>Cancel</button
>
<option value="viewer">Viewer - Can view content</option>
<option value="commenter">Commenter - Can view and comment</option>
<option value="editor">Editor - Can create and edit content</option>
<option value="admin">Admin - Can manage members and settings</option>
</select>
</div>
<div class="flex items-center justify-end gap-3 pt-2 border-t border-light/5">
<button type="button" class="px-4 py-2 text-body-sm text-light/60 hover:text-white transition-colors" onclick={() => (showInviteModal = false)}>Cancel</button>
<button
type="button"
disabled={!inviteEmail.trim() || isSendingInvite}
@@ -338,7 +361,9 @@
<div class="flex flex-col gap-4">
<div class="flex items-center gap-3 p-3 bg-dark/50 rounded-xl">
<Avatar
name={memberProfile?.full_name || memberProfile?.email || "?"}
name={memberProfile?.full_name ||
memberProfile?.email ||
"?"}
src={memberProfile?.avatar_url}
size="sm"
/>
@@ -351,29 +376,38 @@
</p>
</div>
</div>
<div class="flex flex-col gap-1.5">
<label for="member-role" class="text-body-sm text-light/60 font-body">Role</label>
<select
id="member-role"
bind:value={selectedMemberRole}
class="bg-dark border border-light/10 rounded-xl px-3 py-2 text-body-sm text-white focus:outline-none focus:border-primary"
>
<option value="viewer">Viewer</option>
<option value="commenter">Commenter</option>
<option value="editor">Editor</option>
<option value="admin">Admin</option>
</select>
</div>
<button type="button" class="text-[11px] text-error hover:underline self-start" onclick={removeMember}>
<Select
variant="compact"
label="Role"
bind:value={selectedMemberRole}
placeholder=""
options={[
{ value: "viewer", label: "Viewer" },
{ value: "commenter", label: "Commenter" },
{ value: "editor", label: "Editor" },
{ value: "admin", label: "Admin" },
]}
/>
<button
type="button"
class="text-[11px] text-error hover:underline self-start"
onclick={removeMember}
>
Remove from organization
</button>
<div class="flex items-center justify-end gap-3 pt-2 border-t border-light/5">
<button type="button" class="px-4 py-2 text-body-sm text-light/60 hover:text-white transition-colors" onclick={() => (showMemberModal = false)}>Cancel</button>
<div
class="flex items-center justify-end gap-3 pt-2 border-t border-light/5"
>
<button
type="button"
class="px-4 py-2 text-body-sm text-light/60 hover:text-white transition-colors"
onclick={() => (showMemberModal = false)}>Cancel</button
>
<button
type="button"
class="px-4 py-2 bg-primary text-background rounded-xl font-body text-body-sm hover:bg-primary-hover transition-colors"
onclick={updateMemberRole}
>Save</button>
onclick={updateMemberRole}>Save</button
>
</div>
</div>
{/if}