mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-10-03 00:09:03 +02:00
feat: redesign OIDC client details page
This commit is contained in:
@@ -33,6 +33,7 @@
|
||||
labelFor,
|
||||
readonly = false,
|
||||
inputClass,
|
||||
class: className,
|
||||
...restProps
|
||||
}: HTMLAttributes<HTMLDivElement> &
|
||||
(WithChildren | WithoutChildren) & {
|
||||
@@ -52,7 +53,7 @@
|
||||
|
||||
<Field.Field
|
||||
data-disabled={disabled}
|
||||
class={cn('flex flex-col justify-between', restProps.class)}
|
||||
class={cn('flex flex-col justify-between', className)}
|
||||
{...restProps}
|
||||
>
|
||||
<div>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { cn } from '$lib/utils/style';
|
||||
import { LucideChevronDown } from '@lucide/svelte';
|
||||
import { Badge } from '../ui/badge';
|
||||
import { Button } from '../ui/button';
|
||||
@@ -8,7 +10,9 @@
|
||||
items,
|
||||
selectedItems = $bindable(),
|
||||
onSelect,
|
||||
autoClose = false
|
||||
autoClose = false,
|
||||
placeholder = m.select_an_option(),
|
||||
class: className
|
||||
}: {
|
||||
items: {
|
||||
value: string;
|
||||
@@ -17,8 +21,12 @@
|
||||
selectedItems: string[];
|
||||
onSelect?: (value: string) => void;
|
||||
autoClose?: boolean;
|
||||
placeholder?: string;
|
||||
class?: string;
|
||||
} = $props();
|
||||
|
||||
const selected = $derived(items.filter((item) => selectedItems.includes(item.value)));
|
||||
|
||||
function handleItemSelect(value: string) {
|
||||
if (selectedItems.includes(value)) {
|
||||
selectedItems = selectedItems.filter((item) => item !== value);
|
||||
@@ -32,13 +40,19 @@
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Button {...props} variant="outline">
|
||||
{#each items.filter((item) => selectedItems.includes(item.value)) as item (item.value)}
|
||||
<Badge variant="secondary">
|
||||
{item.label}
|
||||
</Badge>
|
||||
{/each}
|
||||
<LucideChevronDown class="text-muted-foreground ml-2 size-4" />
|
||||
<Button {...props} variant="outline" class={cn('w-full px-3 font-normal', className)}>
|
||||
<!-- Button centers its content in an inner span, so this one spreads the badges and the chevron apart -->
|
||||
<span class="flex w-full min-w-0 items-center justify-between gap-2">
|
||||
<!-- The badges stay on one line and are clipped so the trigger keeps the height of the other inputs -->
|
||||
<span class="flex min-w-0 gap-1 overflow-hidden">
|
||||
{#each selected as item (item.value)}
|
||||
<Badge variant="secondary">{item.label}</Badge>
|
||||
{:else}
|
||||
<span class="text-muted-foreground">{placeholder}</span>
|
||||
{/each}
|
||||
</span>
|
||||
<LucideChevronDown class="size-4 shrink-0 opacity-50" />
|
||||
</span>
|
||||
</Button>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
<script lang="ts">
|
||||
import FileInput from '$lib/components/form/file-input.svelte';
|
||||
import FormattedMessage from '$lib/components/formatted-message.svelte';
|
||||
import { Button, buttonVariants } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import * as Popover from '$lib/components/ui/popover';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { cn } from '$lib/utils/style';
|
||||
import { LucideChevronDown } from '@lucide/svelte';
|
||||
|
||||
let {
|
||||
label,
|
||||
accept,
|
||||
onchange,
|
||||
id = 'file-input'
|
||||
}: {
|
||||
label: string;
|
||||
accept?: string;
|
||||
onchange: (file: File | string | null) => void;
|
||||
id?: string;
|
||||
} = $props();
|
||||
|
||||
let url = $state('');
|
||||
let hasError = $state(false);
|
||||
|
||||
async function handleFileChange(e: Event) {
|
||||
const file = (e.target as HTMLInputElement).files?.[0] || null;
|
||||
url = '';
|
||||
hasError = false;
|
||||
onchange(file);
|
||||
}
|
||||
|
||||
async function handleUrlChange(e: Event) {
|
||||
const url = (e.target as HTMLInputElement).value.trim();
|
||||
if (!url) return;
|
||||
|
||||
try {
|
||||
new URL(url);
|
||||
hasError = false;
|
||||
} catch {
|
||||
hasError = true;
|
||||
return;
|
||||
}
|
||||
|
||||
onchange(url);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex">
|
||||
<FileInput
|
||||
{id}
|
||||
variant="secondary"
|
||||
{accept}
|
||||
onchange={handleFileChange}
|
||||
onclick={(e: any) => (e.target.value = '')}
|
||||
>
|
||||
<Button variant="secondary" class="rounded-r-none">
|
||||
{label}
|
||||
</Button>
|
||||
</FileInput>
|
||||
<Popover.Root>
|
||||
<Popover.Trigger
|
||||
class={cn(buttonVariants({ variant: 'secondary' }), 'rounded-l-none border-l')}
|
||||
>
|
||||
<LucideChevronDown class="size-4" /></Popover.Trigger
|
||||
>
|
||||
<Popover.Content class="w-80">
|
||||
<Label for="{id}-url" class="text-xs">URL</Label>
|
||||
<Input
|
||||
id="{id}-url"
|
||||
placeholder=""
|
||||
value={url}
|
||||
oninput={(e) => (url = e.currentTarget.value)}
|
||||
onfocusout={handleUrlChange}
|
||||
aria-invalid={hasError}
|
||||
type="url"
|
||||
/>
|
||||
{#if hasError}
|
||||
<p class="text-destructive mt-1 text-start text-xs">{m.invalid_url()}</p>
|
||||
{/if}
|
||||
|
||||
<p class="text-muted-foreground mt-2 text-xs">
|
||||
<FormattedMessage message={m.logo_from_url_description} />
|
||||
</p>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</div>
|
||||
@@ -2,20 +2,23 @@
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { LucideMinus, LucidePlus } from '@lucide/svelte';
|
||||
import { LucidePlus, LucideX } from '@lucide/svelte';
|
||||
|
||||
let {
|
||||
urls = $bindable(),
|
||||
error = null,
|
||||
testIdPrefix = 'url',
|
||||
disabled = false,
|
||||
keepAtLeastOne = false
|
||||
keepAtLeastOne = false,
|
||||
addLabel
|
||||
}: {
|
||||
urls: string[];
|
||||
error?: string | null;
|
||||
testIdPrefix?: string;
|
||||
disabled?: boolean;
|
||||
keepAtLeastOne?: boolean;
|
||||
// Names what the button adds, instead of the generic "Add" and "Add another"
|
||||
addLabel?: string;
|
||||
} = $props();
|
||||
|
||||
function removeUrl(index: number) {
|
||||
@@ -33,6 +36,7 @@
|
||||
{#each urls as url, i (i)}
|
||||
<div class="flex gap-x-2">
|
||||
<Input
|
||||
class="font-mono text-[13px] md:text-[13px]"
|
||||
aria-invalid={!!error}
|
||||
data-testid={`${testIdPrefix}-${i + 1}`}
|
||||
type="text"
|
||||
@@ -42,25 +46,26 @@
|
||||
{disabled}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="text-muted-foreground size-9"
|
||||
aria-label={m.remove_url({ identifier: url || i + 1 })}
|
||||
onclick={() => removeUrl(i)}
|
||||
{disabled}
|
||||
>
|
||||
<LucideMinus class="size-4" />
|
||||
<LucideX class="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<Button
|
||||
class="mt-2"
|
||||
variant="secondary"
|
||||
class="text-muted-foreground mt-1 -ml-2.5"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onclick={() => (urls = [...urls, ''])}
|
||||
{disabled}
|
||||
>
|
||||
<LucidePlus class="mr-1 size-4" />
|
||||
{urls.length === 0 ? m.add() : m.add_another()}
|
||||
{addLabel ?? (urls.length === 0 ? m.add() : m.add_another())}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -55,7 +55,10 @@
|
||||
onDismiss?.();
|
||||
if (dismissibleId) {
|
||||
const dismissedAlerts = JSON.parse(localStorage?.getItem('dismissed-alerts') || '[]');
|
||||
localStorage?.setItem('dismissed-alerts', JSON.stringify([...dismissedAlerts, dismissibleId]));
|
||||
localStorage?.setItem(
|
||||
'dismissed-alerts',
|
||||
JSON.stringify([...dismissedAlerts, dismissibleId])
|
||||
);
|
||||
isVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { page } from '$app/state';
|
||||
import { cn } from '$lib/utils/style.js';
|
||||
import { Tabs as TabsPrimitive } from 'bits-ui';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
@@ -15,7 +14,8 @@
|
||||
useHash?: boolean;
|
||||
} = $props();
|
||||
|
||||
onMount(() => {
|
||||
// Follows the hash on load and on later hash changes, so that links like `#credentials` elsewhere on the page can switch the tab
|
||||
$effect(() => {
|
||||
if (useHash && page.url.hash) {
|
||||
value = page.url.hash.substring(1);
|
||||
}
|
||||
|
||||
@@ -150,9 +150,10 @@ export function createForm<T extends z.ZodType<any, any>>(schema: T, initialValu
|
||||
}
|
||||
|
||||
// Handle unions
|
||||
// A union that accepts an empty string, like `z.url().or(z.literal(''))`, can be left blank as well
|
||||
if (fieldSchema instanceof z.ZodUnion) {
|
||||
return !fieldSchema.def.options.some((o: any) => {
|
||||
return o.def.type == 'optional';
|
||||
return o.def.type == 'optional' || (o instanceof z.ZodLiteral && o.def.values.includes(''));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user