Monorepo for Tangled
tangled.org
1<script module lang="ts">
2 import { tv, type VariantProps } from "tailwind-variants";
3
4 export const buttonCard = tv({
5 base: [
6 "flex h-40 w-48 cursor-pointer gap-3 rounded-(--btn-radius) px-4 py-3 no-underline select-none [--btn-radius:var(--radius-sm)]",
7 "transition-colors duration-150 ease-in-out",
8 "hover:no-underline",
9 "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-border-focus",
10 "disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50"
11 ],
12 variants: {
13 variant: {
14 default: [
15 "border border-border-default bg-background-default",
16 "hover:bg-background-subtle"
17 ],
18 ghost: ["border border-transparent bg-transparent", "hover:bg-background-inset"]
19 },
20 insetShadow: {
21 true: "",
22 false: ""
23 }
24 },
25 compoundVariants: [
26 // raised variants draw inset depth via a ::before layer.
27 {
28 variant: "default",
29 insetShadow: true,
30 class: [
31 "relative z-0 before:absolute before:inset-0 before:-z-10 before:rounded-[max(calc(var(--btn-radius)-2px),0px)] before:shadow-[inset_0_-2px_0_0_var(--color-shadow-inner-subtle)] before:transition-all before:duration-150 before:content-['']",
32 "hover:before:shadow-[inset_0_-2px_0_0_var(--color-shadow-inner-strong-hover)]",
33 "active:translate-y-0.5 active:before:shadow-[inset_0_2px_2px_0_var(--color-shadow-inner-subtle)]",
34 "disabled:active:translate-y-0"
35 ]
36 }
37 ],
38 defaultVariants: {
39 variant: "default",
40 insetShadow: false
41 }
42 });
43
44 export type ButtonCardVariants = VariantProps<typeof buttonCard>;
45</script>
46
47<script lang="ts">
48 import type { ResolvedPathname } from "$app/types";
49 import type { Component, Snippet } from "svelte";
50 import type {
51 HTMLAnchorAttributes,
52 HTMLButtonAttributes,
53 SvelteHTMLElements
54 } from "svelte/elements";
55
56 interface Props extends Omit<HTMLAnchorAttributes & HTMLButtonAttributes, "class" | "children"> {
57 variant?: ButtonCardVariants["variant"];
58 insetShadow?: boolean;
59 href?: ResolvedPathname;
60 type?: "button" | "submit" | "reset";
61 disabled?: boolean;
62 title?: string;
63 description?: string;
64 icon?: Component<SvelteHTMLElements["svg"]>;
65 class?: string;
66 children?: Snippet;
67 }
68
69 let {
70 variant = "default",
71 insetShadow = false,
72 href,
73 type = "button",
74 disabled = false,
75 title,
76 description,
77 icon,
78 class: className,
79 children,
80 ...rest
81 }: Props = $props();
82
83 const classes = $derived(buttonCard({ variant, insetShadow, class: className }));
84 const Icon = $derived(icon);
85</script>
86
87{#snippet content()}
88 <div class="flex flex-col items-start gap-1.5">
89 {#if title}
90 <div class="flex flex-row items-center gap-2">
91 {#if Icon}
92 <Icon class="size-4 shrink-0" aria-hidden="true" />
93 {/if}
94 <span class="text-sm font-medium text-foreground-default">{title}</span>
95 </div>
96 {/if}
97 {#if description}
98 <span class="text-xs text-foreground-muted">{description}</span>
99 {/if}
100 </div>
101{/snippet}
102
103{#if href}
104 <a
105 {href}
106 class={classes}
107 aria-disabled={disabled ? "true" : undefined}
108 tabindex={disabled ? -1 : undefined}
109 {...rest}
110 >
111 {@render (children ?? content)()}
112 </a>
113{:else}
114 <button {type} {disabled} class={classes} {...rest}>
115 {@render (children ?? content)()}
116 </button>
117{/if}