diff --git a/web/src/lib/components/repo/RefSelector.stories.svelte b/web/src/lib/components/repo/RefSelector.stories.svelte index ac18f8f3..f0a44361 100644 --- a/web/src/lib/components/repo/RefSelector.stories.svelte +++ b/web/src/lib/components/repo/RefSelector.stories.svelte @@ -11,7 +11,8 @@ repoName: "tangled", current: "main", branches: ["main", "feature/storybook"], - tags: ["v1.0.0", "v0.9.0"] + tags: ["v1.0.0", "v0.9.0"], + defaultBranch: "main" } }); diff --git a/web/src/lib/components/repo/RefSelector.svelte b/web/src/lib/components/repo/RefSelector.svelte index 42a51330..b2c0eef7 100644 --- a/web/src/lib/components/repo/RefSelector.svelte +++ b/web/src/lib/components/repo/RefSelector.svelte @@ -3,6 +3,7 @@ import { resolve } from "$app/paths"; import GitCompare from "$icon/git-compare"; import Select from "$lib/components/ui/Select.svelte"; + import type { SelectOption } from "$lib/components/ui/selectField"; interface Props { ownerHandle: string; @@ -10,14 +11,27 @@ current: string; branches: string[]; tags: string[]; + defaultBranch?: string; } - let { ownerHandle, repoName, current, branches, tags }: Props = $props(); + let { ownerHandle, repoName, current, branches, tags, defaultBranch }: Props = $props(); const base = $derived(`/${ownerHandle}/${repoName}`); - const onchange = (event: Event) => { - const value = (event.currentTarget as HTMLSelectElement).value; + // no counts in the headings: the knot caps how many refs it hands back, so the length here + // is the loaded count rather than the repo's total, and it would not track the filter either + const options = $derived([ + ...branches.map((branch) => ({ + value: branch, + hint: branch === defaultBranch ? "default" : undefined, + group: "Branches" + })), + ...(tags.length > 0 + ? tags.map((tag) => ({ value: tag, group: "Tags" })) + : [{ value: "No tags found", disabled: true, group: "Tags" }]) + ]); + + const onSelect = (value: string) => { if (value === current) return; void goto(resolve(`${base}/tree/${encodeURIComponent(value)}` as "/")); }; @@ -26,23 +40,21 @@
- + {#snippet footer()} + + {/snippet}
{#each form.stats.keys() as i (i)} - + = 0 ? optionId(activeIndex) : undefined} + aria-label={label ? `${label} filter` : "Filter options"} + /> +
+ +
+ {#each rows as { option, index, heading } (option.value)} + {#if heading} +
{heading}
+ {/if} + + {:else} +

{emptyLabel}

+ {/each} +
+ + {#if footer} +
{@render footer()}
+ {/if} +
+ + + diff --git a/web/src/lib/components/ui/Field.stories.svelte b/web/src/lib/components/ui/Field.stories.svelte index 89a06fa4..c5f70c7e 100644 --- a/web/src/lib/components/ui/Field.stories.svelte +++ b/web/src/lib/components/ui/Field.stories.svelte @@ -63,11 +63,14 @@ {#snippet template(args)} - + + import { resolve } from "$app/paths"; import { defineMeta } from "@storybook/addon-svelte-csf"; import Select from "./Select.svelte"; + import type { SelectOption } from "./selectField"; const { Story } = defineMeta({ title: "UI/Select", component: Select, tags: ["autodocs"], argTypes: { + rich: { control: "boolean" }, error: { control: "boolean" }, disabled: { control: "boolean" } }, args: { + rich: false, error: false, - disabled: false + disabled: false, + label: "Pick one", + value: "two", + options: [ + { value: "one", label: "One" }, + { value: "two", label: "Two" }, + { value: "three", label: "Three" } + ] satisfies SelectOption[] } }); + + const refs: SelectOption[] = [ + { value: "main", hint: "default", group: "Branches" }, + { value: "next", group: "Branches" }, + { value: "feature/combobox", group: "Branches" }, + { value: "feature/storybook", group: "Branches" }, + { value: "v1.0.0", group: "Tags" }, + { value: "v0.9.0", group: "Tags" } + ]; + + const many: SelectOption[] = Array.from({ length: 60 }, (_, index) => ({ + value: index === 0 ? "main" : `renovate/dependency-${index}`, + hint: index === 0 ? "default" : undefined, + group: "Branches" + })); - - - - - - + + + {#snippet template(args)} +
+ {/snippet} +
+ + + {#snippet template(args)} +
+ {/snippet} +
+ + + {#snippet template(args)} +
+ {/snippet} +
+ + + {#snippet template(args)} +
+ +
+ {/snippet} +
+ + + {#snippet template(args)} +
+ + {/snippet} +
+ + + {#snippet template(args)} +
+ {/snippet}
- - - - + + {#snippet template(args)} +
+ {/snippet}
diff --git a/web/src/lib/components/ui/Select.svelte b/web/src/lib/components/ui/Select.svelte index bc0b5158..decda772 100644 --- a/web/src/lib/components/ui/Select.svelte +++ b/web/src/lib/components/ui/Select.svelte @@ -1,85 +1,131 @@ - - -
- -
+{#snippet nativeOptions(items: SelectOption[])} + {#each items as option (option.value)} + + {/each} +{/snippet} + +{#if rich} + +{:else} +
+ +
+{/if} diff --git a/web/src/lib/components/ui/selectField.ts b/web/src/lib/components/ui/selectField.ts new file mode 100644 index 00000000..b4c64b3e --- /dev/null +++ b/web/src/lib/components/ui/selectField.ts @@ -0,0 +1,60 @@ +import { tv, type VariantProps } from "tailwind-variants"; + +/** one row of a Select, in either rendering */ +export interface SelectOption { + value: string; + /** shown instead of `value`; `value` is still matched when filtering */ + label?: string; + /** right-aligned secondary text, e.g. "default" on a repo's default branch. rich only: + * a native `