This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / web / src / lib / components / ui / ButtonGroup.svelte
3.1 kB 76 lines
1<script module lang="ts"> 2 import { tv, type VariantProps } from "tailwind-variants"; 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-sm 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>