1
0
mirror of https://github.com/pocket-id/pocket-id.git synced 2026-02-12 20:10:14 +00:00

feat: add support for dark mode oidc client icons (#1039)

Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
Kyle Mendell
2025-10-24 02:57:12 -05:00
committed by GitHub
parent eb3963d0fc
commit 028d1c858e
19 changed files with 381 additions and 119 deletions

View File

@@ -41,8 +41,8 @@
<div
class={{
'group relative flex items-center rounded': true,
'bg-[#F1F1F5]': forceColorScheme === 'light',
'bg-[#27272A]': forceColorScheme === 'dark',
'bg-[#F5F5F5]': forceColorScheme === 'light',
'bg-[#262626]': forceColorScheme === 'dark',
'bg-muted': !forceColorScheme
}}
>

View File

@@ -47,7 +47,12 @@
const dataPromise = oidcService.updateClient(client.id, updatedClient);
const imagePromise =
updatedClient.logo !== undefined
? oidcService.updateClientLogo(client, updatedClient.logo)
? oidcService.updateClientLogo(client, updatedClient.logo, true)
: Promise.resolve();
const darkImagePromise =
updatedClient.darkLogo !== undefined
? oidcService.updateClientLogo(client, updatedClient.darkLogo, false)
: Promise.resolve();
client.isPublic = updatedClient.isPublic;
@@ -56,8 +61,15 @@
? m.enabled()
: m.disabled();
await Promise.all([dataPromise, imagePromise])
await Promise.all([dataPromise, imagePromise, darkImagePromise])
.then(() => {
// Update the hasLogo and hasDarkLogo flags after successful upload
if (updatedClient.logo !== undefined) {
client.hasLogo = updatedClient.logo !== null || !!updatedClient.logoUrl;
}
if (updatedClient.darkLogo !== undefined) {
client.hasDarkLogo = updatedClient.darkLogo !== null || !!updatedClient.darkLogoUrl;
}
toast.success(m.oidc_client_updated_successfully());
})
.catch((e) => {

View File

@@ -2,6 +2,7 @@
import FormInput from '$lib/components/form/form-input.svelte';
import SwitchWithLabel from '$lib/components/form/switch-with-label.svelte';
import { Button } from '$lib/components/ui/button';
import * as Tabs from '$lib/components/ui/tabs';
import { m } from '$lib/paraglide/messages';
import type {
OidcClient,
@@ -13,7 +14,7 @@
import { createForm } from '$lib/utils/form-util';
import { cn } from '$lib/utils/style';
import { callbackUrlSchema, emptyToUndefined, optionalUrl } from '$lib/utils/zod-util';
import { LucideChevronDown } from '@lucide/svelte';
import { LucideChevronDown, LucideMoon, LucideSun } from '@lucide/svelte';
import { slide } from 'svelte/transition';
import { z } from 'zod/v4';
import FederatedIdentitiesInput from './federated-identities-input.svelte';
@@ -32,9 +33,13 @@
let isLoading = $state(false);
let showAdvancedOptions = $state(false);
let logo = $state<File | null | undefined>();
let darkLogo = $state<File | null | undefined>();
let logoDataURL: string | null = $state(
existingClient?.hasLogo ? cachedOidcClientLogo.getUrl(existingClient!.id) : null
);
let darkLogoDataURL: string | null = $state(
existingClient?.hasDarkLogo ? cachedOidcClientLogo.getUrl(existingClient!.id, false) : null
);
const client = {
id: '',
@@ -48,7 +53,8 @@
credentials: {
federatedIdentities: existingClient?.credentials?.federatedIdentities || []
},
logoUrl: ''
logoUrl: '',
darkLogoUrl: ''
};
const formSchema = z.object({
@@ -70,6 +76,7 @@
requiresReauthentication: z.boolean(),
launchURL: optionalUrl,
logoUrl: optionalUrl,
darkLogoUrl: optionalUrl,
credentials: z.object({
federatedIdentities: z.array(
z.object({
@@ -92,37 +99,63 @@
const success = await callback({
...data,
logo: $inputs.logoUrl?.value ? null : logo,
logoUrl: $inputs.logoUrl?.value
logo: $inputs.logoUrl?.value ? undefined : logo,
logoUrl: $inputs.logoUrl?.value,
darkLogo: $inputs.darkLogoUrl?.value ? undefined : darkLogo,
darkLogoUrl: $inputs.darkLogoUrl?.value
});
const hasLogo = logo != null || !!$inputs.logoUrl?.value;
if (success && existingClient && hasLogo) {
logoDataURL = cachedOidcClientLogo.getUrl(existingClient.id);
const hasDarkLogo = darkLogo != null || !!$inputs.darkLogoUrl?.value;
if (success && existingClient) {
if (hasLogo) {
logoDataURL = cachedOidcClientLogo.getUrl(existingClient.id);
}
if (hasDarkLogo) {
darkLogoDataURL = cachedOidcClientLogo.getUrl(existingClient.id, false);
}
}
if (success && !existingClient) form.reset();
isLoading = false;
}
function onLogoChange(input: File | string | null) {
function onLogoChange(input: File | string | null, light: boolean = true) {
if (input == null) return;
const logoUrlInput = light ? $inputs.logoUrl : $inputs.darkLogoUrl;
if (typeof input === 'string') {
logo = null;
logoDataURL = input || null;
$inputs.logoUrl!.value = input;
if (light) {
logo = null;
logoDataURL = input || null;
} else {
darkLogo = null;
darkLogoDataURL = input || null;
}
logoUrlInput!.value = input;
} else {
logo = input;
$inputs.logoUrl && ($inputs.logoUrl.value = '');
logoDataURL = URL.createObjectURL(input);
if (light) {
logo = input;
logoDataURL = URL.createObjectURL(input);
} else {
darkLogo = input;
darkLogoDataURL = URL.createObjectURL(input);
}
logoUrlInput && (logoUrlInput.value = '');
}
}
function resetLogo() {
logo = null;
logoDataURL = null;
$inputs.logoUrl && ($inputs.logoUrl.value = '');
function resetLogo(light: boolean = true) {
if (light) {
logo = null;
logoDataURL = null;
$inputs.logoUrl && ($inputs.logoUrl.value = '');
} else {
darkLogo = null;
darkLogoDataURL = null;
$inputs.darkLogoUrl && ($inputs.darkLogoUrl.value = '');
}
}
function getFederatedIdentityErrors(errors: z.ZodError<any> | undefined) {
@@ -182,13 +215,49 @@
bind:checked={$inputs.requiresReauthentication.value}
/>
</div>
<div class="mt-7">
<OidcClientImageInput
{logoDataURL}
{resetLogo}
clientName={$inputs.name.value}
{onLogoChange}
/>
<div class="mt-7 w-full md:w-1/2">
<Tabs.Root value="light-logo">
<Tabs.Content value="light-logo">
<OidcClientImageInput
{logoDataURL}
resetLogo={() => resetLogo(true)}
clientName={$inputs.name.value}
light={true}
onLogoChange={(input) => onLogoChange(input, true)}
>
{#snippet tabTriggers()}
<Tabs.List class="grid h-9 w-full grid-cols-2">
<Tabs.Trigger value="light-logo" class="px-3">
<LucideSun class="size-4" />
</Tabs.Trigger>
<Tabs.Trigger value="dark-logo" class="px-3">
<LucideMoon class="size-4" />
</Tabs.Trigger>
</Tabs.List>
{/snippet}
</OidcClientImageInput>
</Tabs.Content>
<Tabs.Content value="dark-logo">
<OidcClientImageInput
light={false}
logoDataURL={darkLogoDataURL}
resetLogo={() => resetLogo(false)}
clientName={$inputs.name.value}
onLogoChange={(input) => onLogoChange(input, false)}
>
{#snippet tabTriggers()}
<Tabs.List class="grid h-9 w-full grid-cols-2">
<Tabs.Trigger value="light-logo" class="px-3">
<LucideSun class="size-4" />
</Tabs.Trigger>
<Tabs.Trigger value="dark-logo" class="px-3">
<LucideMoon class="size-4" />
</Tabs.Trigger>
</Tabs.List>
{/snippet}
</OidcClientImageInput>
</Tabs.Content>
</Tabs.Root>
</div>
{#if showAdvancedOptions}

View File

@@ -5,40 +5,53 @@
import { Label } from '$lib/components/ui/label';
import { m } from '$lib/paraglide/messages';
import { LucideX } from '@lucide/svelte';
import type { Snippet } from 'svelte';
let {
logoDataURL,
clientName,
resetLogo,
onLogoChange
onLogoChange,
light,
tabTriggers
}: {
logoDataURL: string | null;
clientName: string;
resetLogo: () => void;
onLogoChange: (file: File | string | null) => void;
tabTriggers?: Snippet;
light: boolean;
} = $props();
let id = `oidc-client-logo-${light ? 'light' : 'dark'}`;
</script>
<Label for="logo">{m.logo()}</Label>
<div class="flex items-end gap-4">
<Label for={id}>{m.logo()}</Label>
<div class="flex h-24 items-end gap-4">
<div class="flex flex-col gap-2">
{#if tabTriggers}
{@render tabTriggers()}
{/if}
<div class="flex flex-wrap items-center gap-2">
<UrlFileInput {id} label={m.upload_logo()} accept="image/*" onchange={onLogoChange} />
</div>
</div>
{#if logoDataURL}
<div class="flex items-start gap-4">
<div class="relative shrink-0">
<ImageBox class="size-24" src={logoDataURL} alt={m.name_logo({ name: clientName })} />
<ImageBox
class="size-24 {light ? 'bg-[#F5F5F5]' : 'bg-[#262626]'}"
src={logoDataURL}
alt={m.name_logo({ name: clientName })}
/>
<Button
variant="destructive"
size="icon"
onclick={resetLogo}
class="absolute -top-2 -right-2 size-6 rounded-full shadow-md"
class="absolute -top-2 -right-2 size-6 rounded-full shadow-md "
>
<LucideX class="size-3" />
</Button>
</div>
</div>
{/if}
<div class="flex flex-col gap-3">
<div class="flex flex-wrap items-center gap-2">
<UrlFileInput label={m.upload_logo()} accept="image/*" onchange={onLogoChange} />
</div>
</div>
</div>

View File

@@ -13,6 +13,7 @@
import { cachedOidcClientLogo } from '$lib/utils/cached-image-util';
import { axiosErrorToast } from '$lib/utils/error-util';
import { LucidePencil, LucideTrash } from '@lucide/svelte';
import { mode } from 'mode-watcher';
import { toast } from 'svelte-sonner';
const oidcService = new OIDCService();
@@ -22,6 +23,8 @@
return tableRef?.refresh();
}
const isLightMode = $derived(mode.current === 'light');
const booleanFilterValues = [
{ label: m.enabled(), value: true },
{ label: m.disabled(), value: false }
@@ -103,7 +106,7 @@
{#if item.hasLogo}
<ImageBox
class="size-12 rounded-lg"
src={cachedOidcClientLogo.getUrl(item.id)}
src={cachedOidcClientLogo.getUrl(item.id, isLightMode)}
alt={m.name_logo({ name: item.name })}
/>
{:else}

View File

@@ -40,7 +40,7 @@
<ImageBox
class="size-14"
src={client.hasLogo
? cachedOidcClientLogo.getUrl(client.id)
? cachedOidcClientLogo.getUrl(client.id, isLightMode)
: cachedApplicationLogo.getUrl(isLightMode)}
alt={m.name_logo({ name: client.name })}
/>