This repository has no description
1<script module lang="ts">
2 import { tv, type VariantProps } from "$lib/tv";
3 import type { ButtonVariants } from "./Button.svelte";
4
5 export const buttonGroup = tv({
6 base: "inline-flex items-center",
7 variants: {
8 // joined groups are the segmented control from Figma, built the way the appview's
9 // .btn-group is: the group alone draws the outer border and the recessed surface,
10 // its segments drop their own top/bottom border entirely (border-y-0) and stretch
11 // to fill the inner box. that matters because a segment's y-border is the same
12 // token as the group's, so keeping both stacks two 1px lines into one 2px-looking
13 // edge above and below the selected segment. horizontally the segments keep their
14 // border and it's pulled onto the neighbour's (-ml-px) so each seam collapses to a
15 // single divider, and the outer ones onto the group's own edge.
16 //
17 // the recessed surface has to stay darker than the selected segment's
18 // bg-background-default, and no single token does that in both modes — subtle is
19 // below default in light (#f9fafb vs #fff) but above it in dark (#374151 vs
20 // #1f2937) — so dark falls back to canvas (#111827), i.e. the appview's gray-900.
21 spaced: {
22 true: "gap-2",
23 false: [
24 "items-stretch gap-0 rounded border border-border-default bg-background-subtle dark:bg-background-canvas",
25 "*:relative [&>*]:border-y-0",
26 "[&>*:first-child]:-ml-px [&>*:last-child]:-mr-px",
27 "[&>*:not(:first-child)]:-ml-px [&>*:not(:first-child)]:[--btn-radius-l:0px]",
28 "[&>*:not(:last-child)]:[--btn-radius-r:0px]",
29 "[&>*:focus-visible]:z-10 [&>*:hover]:z-10"
30 ]
31 }
32 },
33 defaultVariants: {
34 spaced: false
35 }
36 });
37
38 export type ButtonGroupVariants = VariantProps<typeof buttonGroup>;
39
40 /**
41 * Recipe for using ButtonGroup as a tab/segment switcher (e.g. Open/Closed issue filters,
42 * Write/Preview editor tabs). Per Figma, the selected segment reads as a raised, lighter
43 * surface (`default` + its inset shadow) while unselected segments are flat `ghost`
44 * buttons — never the plain `default` variant, which reads as the *hovered* state of an
45 * unselected segment. Figma also gives an unselected segment the same foreground as a
46 * selected one, where `ghost` on its own is muted, so the recipe overrides the colour
47 * here rather than changing `ghost` everywhere it's used.
48 */
49 export function segmentProps(selected: boolean): {
50 variant: ButtonVariants["variant"];
51 insetShadow: boolean;
52 class?: string;
53 } {
54 return selected
55 ? { variant: "default", insetShadow: true }
56 : { variant: "ghost", insetShadow: false, class: "text-foreground-default" };
57 }
58</script>
59
60<script lang="ts">
61 import type { Snippet } from "svelte";
62
63 interface Props {
64 spaced?: ButtonGroupVariants["spaced"];
65 class?: string;
66 children: Snippet;
67 }
68
69 let { spaced = false, class: className, children }: Props = $props();
70
71 const classes = $derived(buttonGroup({ spaced, class: className }));
72</script>
73
74<div class={classes}>
75 {@render children()}
76</div>