mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-09-17 20:31:47 +02:00
feat: shadcn quant selects with dynamic trigger width in download command
Replaces the native quant selects with the ui/select component using a new xs trigger size: h-6, mono text, w-fit so the trigger width follows the selected quant instead of reserving space for the widest option. The primary-tinted look and the dashed default-preview state carry over via trigger classes; SELECT_CLASS is no longer needed. Assisted-by: Claude Sonnet
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
import { HuggingFaceService } from '$lib/services';
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
/** Org whose avatar is shown (may differ from the repo's org for base models). */
|
||||
org: string;
|
||||
/** Repo's own org, shown as a small corner badge when provided. */
|
||||
@@ -22,6 +23,7 @@
|
||||
|
||||
let {
|
||||
baseImageClass = '',
|
||||
class: className = '',
|
||||
org,
|
||||
quantImageClass = 'h-full w-full',
|
||||
quantOrg,
|
||||
@@ -57,7 +59,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<span class="relative mt-0.5 inline-flex shrink-0">
|
||||
<span class="relative mt-0.5 inline-flex shrink-0 {className}">
|
||||
{#if avatarError}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
onclick={() => onSelect?.(model.id)}
|
||||
type="button"
|
||||
>
|
||||
<ModelsDiscoverAvatar org={avatarOrg} quantOrg={showBaseModelAvatar ? org : undefined} />
|
||||
<ModelsDiscoverAvatar class="mt-1" org={avatarOrg} quantOrg={showBaseModelAvatar ? org : undefined} />
|
||||
|
||||
<span class="min-w-0 flex-1">
|
||||
<ModelId
|
||||
|
||||
+59
-26
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { type QuantOption, SELECT_CLASS } from './download-options.utils';
|
||||
import { type QuantOption } from './download-options.utils';
|
||||
import { Check, Copy } from '@lucide/svelte';
|
||||
import * as Select from '$lib/components/ui/select';
|
||||
import { type ModelSidecar } from '$lib/constants';
|
||||
import { copyToClipboard } from '$lib/utils';
|
||||
|
||||
@@ -43,6 +44,14 @@
|
||||
|
||||
let copied = $state(false);
|
||||
|
||||
// Trigger labels: the select trigger has no automatic value rendering.
|
||||
let baseSelectedLabel = $derived(
|
||||
baseOptions.find((option) => option.path === basePick)?.label ?? basePick
|
||||
);
|
||||
let draftSelectedLabel = $derived(
|
||||
draftOptions.find((option) => option.path === draftPick)?.label ?? draftPick
|
||||
);
|
||||
|
||||
async function copy() {
|
||||
await copyToClipboard(command);
|
||||
copied = true;
|
||||
@@ -67,19 +76,33 @@
|
||||
|
||||
<!-- Base quant: always part of the command, the 8-bit file by default. -->
|
||||
{#if baseOptions.length}
|
||||
<select
|
||||
aria-label="Base model quantization"
|
||||
class="{SELECT_CLASS} {mainSelected ? '' : 'border-dashed'} -ml-2"
|
||||
onchange={(e) => onBasePick(e.currentTarget.value)}
|
||||
title={mainSelected ? undefined : 'Default quant - pick a file above or another quant here'}
|
||||
value={basePick}
|
||||
>
|
||||
{#each baseOptions as option (option.path)}
|
||||
<option disabled={option.disabled} value={option.path}>
|
||||
{option.label}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
<Select.Root onValueChange={(v) => v && onBasePick(v)} type="single" value={basePick}>
|
||||
<Select.Trigger
|
||||
aria-label="Base model quantization"
|
||||
class="-ml-2 border-primary/15 bg-primary/[0.07] font-mono text-foreground hover:bg-primary/15 focus-visible:border-primary/40 focus-visible:ring-0 {mainSelected
|
||||
? ''
|
||||
: 'border-dashed'}"
|
||||
size="xs"
|
||||
title={mainSelected
|
||||
? undefined
|
||||
: 'Default quant - pick a file above or another quant here'}
|
||||
>
|
||||
{baseSelectedLabel}
|
||||
</Select.Trigger>
|
||||
|
||||
<Select.Content class="font-mono text-xs">
|
||||
{#each baseOptions as option (option.path)}
|
||||
<Select.Item
|
||||
class="text-xs"
|
||||
disabled={option.disabled}
|
||||
label={option.label}
|
||||
value={option.path}
|
||||
>
|
||||
{option.label}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
{/if}
|
||||
|
||||
<!-- Draft segment: appears once a draft is picked, quant inline too. -->
|
||||
@@ -88,18 +111,28 @@
|
||||
|
||||
<span class="shrink-0">{modelId}{draftQuant ? ':' : ''}</span>
|
||||
|
||||
<select
|
||||
aria-label="Draft model quantization"
|
||||
class="{SELECT_CLASS} -ml-2"
|
||||
onchange={(e) => onDraftPick(e.currentTarget.value)}
|
||||
value={draftPick}
|
||||
>
|
||||
{#each draftOptions as option (option.path)}
|
||||
<option disabled={option.disabled} value={option.path}>
|
||||
{option.label}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
<Select.Root onValueChange={(v) => v && onDraftPick(v)} type="single" value={draftPick}>
|
||||
<Select.Trigger
|
||||
aria-label="Draft model quantization"
|
||||
class="-ml-2 border-primary/15 bg-primary/[0.07] font-mono text-foreground hover:bg-primary/15 focus-visible:border-primary/40 focus-visible:ring-0"
|
||||
size="xs"
|
||||
>
|
||||
{draftSelectedLabel}
|
||||
</Select.Trigger>
|
||||
|
||||
<Select.Content class="font-mono text-xs">
|
||||
{#each draftOptions as option (option.path)}
|
||||
<Select.Item
|
||||
class="text-xs"
|
||||
disabled={option.disabled}
|
||||
label={option.label}
|
||||
value={option.path}
|
||||
>
|
||||
{option.label}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
|
||||
<span>--spec-type</span>
|
||||
|
||||
|
||||
@@ -43,9 +43,3 @@ export function labelFor(path: string): string {
|
||||
|
||||
return basename.replace(/\.gguf$/i, '');
|
||||
}
|
||||
|
||||
// min-w keeps the value clear of the native chevron: Safari sizes a select
|
||||
// to its widest option, so an exactly-as-wide value would otherwise let the
|
||||
// chevron overlap the text (draft selects are all same-width quants).
|
||||
export const SELECT_CLASS =
|
||||
'h-6 min-w-22 max-w-36 shrink-0 cursor-pointer rounded border border-primary/15 bg-primary/[0.07] py-0 pr-3 pl-2 font-mono text-xs text-foreground outline-none transition-colors hover:bg-primary/15 focus-visible:border-primary/40 focus-visible:ring-0';
|
||||
|
||||
@@ -11,18 +11,24 @@
|
||||
variant = 'default',
|
||||
...restProps
|
||||
}: WithoutChild<SelectPrimitive.TriggerProps> & {
|
||||
size?: 'sm' | 'default';
|
||||
size?: 'xs' | 'sm' | 'default';
|
||||
variant?: 'default' | 'plain';
|
||||
} = $props();
|
||||
|
||||
// Super small trigger: fits its selected value, for dense inline use.
|
||||
const xsClasses =
|
||||
"flex h-6 w-fit items-center justify-between gap-1 rounded-md border border-input bg-transparent px-2 py-0 text-xs whitespace-nowrap outline-none select-none transition-colors focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[placeholder]:text-muted-foreground dark:bg-input/30 dark:hover:bg-input/50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='text-'])]:text-muted-foreground";
|
||||
|
||||
const baseClasses = $derived(
|
||||
variant === 'plain'
|
||||
? "group inline-flex w-full items-center justify-end gap-2 whitespace-nowrap px-0 py-0 text-sm font-medium text-muted-foreground transition-colors focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3 [&_svg:not([class*='text-'])]:text-muted-foreground"
|
||||
: "flex w-fit items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none select-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground"
|
||||
: size === 'xs'
|
||||
? xsClasses
|
||||
: "flex w-fit items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none select-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground"
|
||||
);
|
||||
|
||||
const chevronClasses = $derived(
|
||||
variant === 'plain'
|
||||
variant === 'plain' || size === 'xs'
|
||||
? 'size-3 opacity-60 transition-transform group-data-[state=open]:-rotate-180'
|
||||
: 'size-4 opacity-50'
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user