From 15473865b44ae05750afdc11d105b091d8ea0f8a Mon Sep 17 00:00:00 2001 From: Ronan Berder Date: Fri, 31 Oct 2025 08:56:44 +0800 Subject: [PATCH] Cleaning up button group --- docs/src/_includes/partials/sidebar.njk | 1 + docs/src/assets/styles.css | 319 ++++++++++++++---------- docs/src/components/button-group.njk | 245 ++++++++++++++++++ docs/src/components/command.njk | 10 +- src/css/basecoat.css | 79 ++++-- 5 files changed, 498 insertions(+), 156 deletions(-) create mode 100644 docs/src/components/button-group.njk diff --git a/docs/src/_includes/partials/sidebar.njk b/docs/src/_includes/partials/sidebar.njk index 14b11c8..28733cc 100644 --- a/docs/src/_includes/partials/sidebar.njk +++ b/docs/src/_includes/partials/sidebar.njk @@ -86,6 +86,7 @@ { label: "Badge", url: "/components/badge", attrs: link_attrs }, { label: "Breadcrumb", url: "/components/breadcrumb", attrs: link_attrs }, { label: "Button", url: "/components/button", attrs: link_attrs }, + { label: "Button Group", url: "/components/button-group", attrs: link_attrs }, { label: "Card", url: "/components/card", attrs: link_attrs }, { label: "Checkbox", url: "/components/checkbox", attrs: link_attrs }, { label: "Kbd", url: "/components/kbd", attrs: link_attrs }, diff --git a/docs/src/assets/styles.css b/docs/src/assets/styles.css index 98a413d..757ca65 100644 --- a/docs/src/assets/styles.css +++ b/docs/src/assets/styles.css @@ -505,6 +505,9 @@ .w-\[271px\] { width: 271px; } + .w-fit { + width: fit-content; + } .w-full { width: 100%; } @@ -622,6 +625,9 @@ .items-start { align-items: flex-start; } + .items-stretch { + align-items: stretch; + } .justify-between { justify-content: space-between; } @@ -817,6 +823,9 @@ .\!p-3\.5 { padding: calc(var(--spacing) * 3.5) !important; } + .p-0 { + padding: calc(var(--spacing) * 0); + } .p-1 { padding: calc(var(--spacing) * 1); } @@ -1843,6 +1852,149 @@ } } } +@layer components { + .badge, .badge-primary, .badge-secondary, .badge-destructive, .badge-outline { + display: inline-flex; + width: fit-content; + flex-shrink: 0; + align-items: center; + justify-content: center; + gap: calc(var(--spacing) * 1); + overflow: hidden; + border-radius: var(--radius-md); + border-style: var(--tw-border-style); + border-width: 1px; + padding-inline: calc(var(--spacing) * 2); + padding-block: calc(var(--spacing) * 0.5); + font-size: var(--text-xs); + line-height: var(--tw-leading, var(--text-xs--line-height)); + --tw-font-weight: var(--font-weight-medium); + font-weight: var(--font-weight-medium); + white-space: nowrap; + transition-property: color,box-shadow; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + &:focus-visible { + border-color: var(--color-ring); + } + &:focus-visible { + --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + &:focus-visible { + --tw-ring-color: var(--color-ring); + @supports (color: color-mix(in lab, red, red)) { + --tw-ring-color: color-mix(in oklab, var(--color-ring) 50%, transparent); + } + } + &[aria-invalid="true"] { + border-color: var(--color-destructive); + } + &[aria-invalid="true"] { + --tw-ring-color: var(--color-destructive); + @supports (color: color-mix(in lab, red, red)) { + --tw-ring-color: color-mix(in oklab, var(--color-destructive) 20%, transparent); + } + } + &:is(.dark *) { + &[aria-invalid="true"] { + --tw-ring-color: var(--color-destructive); + @supports (color: color-mix(in lab, red, red)) { + --tw-ring-color: color-mix(in oklab, var(--color-destructive) 40%, transparent); + } + } + } + &>svg { + pointer-events: none; + } + &>svg { + width: calc(var(--spacing) * 3); + height: calc(var(--spacing) * 3); + } + } + .badge, .badge-primary { + border-color: transparent; + background-color: var(--color-primary); + color: var(--color-primary-foreground); + a& { + &:hover { + @media (hover: hover) { + background-color: var(--color-primary); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-primary) 90%, transparent); + } + } + } + } + } + .badge-secondary { + border-color: transparent; + background-color: var(--color-secondary); + color: var(--color-secondary-foreground); + a& { + &:hover { + @media (hover: hover) { + background-color: var(--color-secondary); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-secondary) 90%, transparent); + } + } + } + } + } + .badge-destructive { + border-color: transparent; + background-color: var(--color-destructive); + color: var(--color-white); + &:focus-visible { + --tw-ring-color: var(--color-destructive); + @supports (color: color-mix(in lab, red, red)) { + --tw-ring-color: color-mix(in oklab, var(--color-destructive) 20%, transparent); + } + } + &:is(.dark *) { + background-color: var(--color-destructive); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-destructive) 60%, transparent); + } + } + &:is(.dark *) { + &:focus-visible { + --tw-ring-color: var(--color-destructive); + @supports (color: color-mix(in lab, red, red)) { + --tw-ring-color: color-mix(in oklab, var(--color-destructive) 40%, transparent); + } + } + } + a& { + &:hover { + @media (hover: hover) { + background-color: var(--color-destructive); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-destructive) 90%, transparent); + } + } + } + } + } + .badge-outline { + color: var(--color-foreground); + a& { + &:hover { + @media (hover: hover) { + background-color: var(--color-accent); + } + } + } + a& { + &:hover { + @media (hover: hover) { + color: var(--color-accent-foreground); + } + } + } + } +} @layer components { .btn, .btn-primary, .btn-secondary, .btn-outline, .btn-ghost, .btn-link, .btn-destructive, .btn-sm, .btn-sm-primary, .btn-sm-secondary, .btn-sm-outline, .btn-sm-ghost, .btn-sm-link, .btn-sm-destructive, .btn-lg, .btn-lg-primary, .btn-lg-secondary, .btn-lg-outline, .btn-lg-ghost, .btn-lg-link, .btn-lg-destructive, .btn-icon, .btn-icon-primary, .btn-icon-secondary, .btn-icon-outline, .btn-icon-ghost, .btn-icon-link, .btn-icon-destructive, .btn-sm-icon, .btn-sm-icon-primary, .btn-sm-icon-secondary, .btn-sm-icon-outline, .btn-sm-icon-ghost, .btn-sm-icon-link, .btn-sm-icon-destructive, .btn-lg-icon, .btn-lg-icon-primary, .btn-lg-icon-secondary, .btn-lg-icon-outline, .btn-lg-icon-ghost, .btn-lg-icon-link, .btn-lg-icon-destructive { display: inline-flex; @@ -2065,146 +2217,49 @@ } } } -@layer components { - .badge, .badge-primary, .badge-secondary, .badge-destructive, .badge-outline { - display: inline-flex; - width: fit-content; +.button-group { + isolation: isolate; + display: inline-flex; + > *:hover, > *:focus-visible, > :is(.dropdown-menu, .popover) > button:hover, > :is(.dropdown-menu, .popover) > button:focus-visible { + position: relative; + z-index: 10; + } + > hr[role='separator'] { + margin: calc(var(--spacing) * 0); + height: auto; + width: calc(var(--spacing) * 0); flex-shrink: 0; - align-items: center; - justify-content: center; - gap: calc(var(--spacing) * 1); - overflow: hidden; - border-radius: var(--radius-md); + align-self: stretch; border-style: var(--tw-border-style); border-width: 1px; - padding-inline: calc(var(--spacing) * 2); - padding-block: calc(var(--spacing) * 0.5); - font-size: var(--text-xs); - line-height: var(--tw-leading, var(--text-xs--line-height)); - --tw-font-weight: var(--font-weight-medium); - font-weight: var(--font-weight-medium); - white-space: nowrap; - transition-property: color,box-shadow; - transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); - transition-duration: var(--tw-duration, var(--default-transition-duration)); - &:focus-visible { - border-color: var(--color-ring); + border-color: var(--color-input); + } + &:not([data-orientation='vertical']) { + > *:not(:first-child), > :is(.dropdown-menu, .popover):not(:first-child) > button { + margin-left: -1px; + border-top-left-radius: 0; + border-bottom-left-radius: 0; } - &:focus-visible { - --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - &:focus-visible { - --tw-ring-color: var(--color-ring); - @supports (color: color-mix(in lab, red, red)) { - --tw-ring-color: color-mix(in oklab, var(--color-ring) 50%, transparent); - } - } - &[aria-invalid="true"] { - border-color: var(--color-destructive); - } - &[aria-invalid="true"] { - --tw-ring-color: var(--color-destructive); - @supports (color: color-mix(in lab, red, red)) { - --tw-ring-color: color-mix(in oklab, var(--color-destructive) 20%, transparent); - } - } - &:is(.dark *) { - &[aria-invalid="true"] { - --tw-ring-color: var(--color-destructive); - @supports (color: color-mix(in lab, red, red)) { - --tw-ring-color: color-mix(in oklab, var(--color-destructive) 40%, transparent); - } - } - } - &>svg { - pointer-events: none; - } - &>svg { - width: calc(var(--spacing) * 3); - height: calc(var(--spacing) * 3); + > *:not(:last-child), > :is(.dropdown-menu, .popover):not(:last-child) > button { + border-top-right-radius: 0; + border-bottom-right-radius: 0; } } - .badge, .badge-primary { - border-color: transparent; - background-color: var(--color-primary); - color: var(--color-primary-foreground); - a& { - &:hover { - @media (hover: hover) { - background-color: var(--color-primary); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-primary) 90%, transparent); - } - } - } + &[data-orientation='vertical'] { + flex-direction: column; + > hr[role='separator'] { + height: 1px; + width: auto; } - } - .badge-secondary { - border-color: transparent; - background-color: var(--color-secondary); - color: var(--color-secondary-foreground); - a& { - &:hover { - @media (hover: hover) { - background-color: var(--color-secondary); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-secondary) 90%, transparent); - } - } - } + > *:not(:first-child) { + margin-top: -1px; + margin-left: calc(var(--spacing) * 0); + border-top-left-radius: 0; + border-top-right-radius: 0; } - } - .badge-destructive { - border-color: transparent; - background-color: var(--color-destructive); - color: var(--color-white); - &:focus-visible { - --tw-ring-color: var(--color-destructive); - @supports (color: color-mix(in lab, red, red)) { - --tw-ring-color: color-mix(in oklab, var(--color-destructive) 20%, transparent); - } - } - &:is(.dark *) { - background-color: var(--color-destructive); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-destructive) 60%, transparent); - } - } - &:is(.dark *) { - &:focus-visible { - --tw-ring-color: var(--color-destructive); - @supports (color: color-mix(in lab, red, red)) { - --tw-ring-color: color-mix(in oklab, var(--color-destructive) 40%, transparent); - } - } - } - a& { - &:hover { - @media (hover: hover) { - background-color: var(--color-destructive); - @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in oklab, var(--color-destructive) 90%, transparent); - } - } - } - } - } - .badge-outline { - color: var(--color-foreground); - a& { - &:hover { - @media (hover: hover) { - background-color: var(--color-accent); - } - } - } - a& { - &:hover { - @media (hover: hover) { - color: var(--color-accent-foreground); - } - } + > *:not(:last-child) { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; } } } diff --git a/docs/src/components/button-group.njk b/docs/src/components/button-group.njk new file mode 100644 index 0000000..401629b --- /dev/null +++ b/docs/src/components/button-group.njk @@ -0,0 +1,245 @@ +--- +layout: layouts/page.njk +title: Button Group +description: A container that groups related buttons together with consistent styling. +toc: + - label: Usage + id: usage + - label: Examples + id: examples + children: + - label: Orientation + id: example-orientation + - label: Nested + id: example-nested + - label: Separator + id: example-separator + - label: Dropdown Menu + id: example-dropdown-menu + - label: Popover + id: example-popover +--- + +{% from "dropdown-menu.njk" import dropdown_menu %} +{% from "popover.njk" import popover %} +{% from "macros/code_preview.njk" import code_preview %} +{% from "macros/code_block.njk" import code_block %} + +{% set code_default %} +
+ + +
+ + +
+ +
+ + {% set trigger %} + {% lucide "ellipsis" %} + {% endset %} + {% call dropdown_menu( + trigger=trigger, + trigger_attrs={"class": "btn-icon-outline"}, + popover_attrs={"data-align": "end"} + ) %} +
+ {% lucide "mail-check" %} + Mark as Read +
+
+ {% lucide "archive" %} + Archive +
+
+
+ {% lucide "clock" %} + Snooze +
+
+ {% lucide "calendar" %} + Add to Calendar +
+
+ {% lucide "list-filter-plus" %} + Add to List +
+
+ + {% endcall %} +
+{% endset %} + +{{ code_preview("button-group-default", code_default | prettyHtml) }} + +

Usage

+ +{{ code_block(code_default | prettyHtml, "html") }} + +

Examples

+ +

Orientation

+ +
+

Use data-orientation="vertical" to stack buttons vertically.

+
+ +{% set code_orientation %} +
+ + +
+{% endset %} + +{{ code_preview("button-group-orientation", code_orientation | prettyHtml) }} + +

Size

+ +{% set code_size %} +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+{% endset %} + +{{ code_preview("button-group-size", code_size | prettyHtml) }} + +

Nested

+ +{% set code_nested %} +
+
+ + + + + +
+
+ + +
+
+{% endset %} + +{{ code_preview("button-group-nested", code_nested | prettyHtml) }} + +

Separator

+ +
+

Use <hr role="separator"> to visually divide buttons within a group.

+
+ +{% set code_separator %} +
+ +
+ +
+{% endset %} + +{{ code_preview("button-group-separator", code_separator | prettyHtml) }} + +

Dropdown Menu

+ +{% set code_dropdown_menu %} +
+ + {% set trigger %} + {% lucide "chevron-down" %} + {% endset %} + {% call dropdown_menu( + trigger=trigger, + trigger_attrs={"class": "btn-icon-outline"}, + popover_attrs={"data-align": "end"} + ) %} +
+ {% lucide "volume-off" %} + Mute Conversation +
+
+ {% lucide "check" %} + Mark as Read +
+
+ {% lucide "triangle-alert" %} + Report Conversation +
+
+ {% lucide "user-round-x" %} + Block User +
+
+ {% lucide "share" %} + Share Conversation +
+
+ {% lucide "copy" %} + Copy Conversation +
+
+ + {% endcall %} +
+{% endset %} + +{{ code_preview("button-group-dropdown-menu", code_dropdown_menu | prettyHtml) }} + +

Popover

+ +{% set code_popover %} +
+ + {% call popover( + trigger=trigger, + trigger_attrs={"class": "btn-icon-outline"}, + popover_attrs={"class": "w-72 p-0", "data-align": "end"} + ) %} +
Agent Tasks
+
+ +

Start a new task with Copilot

+

+ Describe your task in natural language. Copilot will work in the + background and open a pull request for your review. +

+
+ {% endcall %} +
+{% endset %} + +{{ code_preview("button-group-popover", code_popover | prettyHtml) }} \ No newline at end of file diff --git a/docs/src/components/command.njk b/docs/src/components/command.njk index 58797d4..ca2be6d 100644 --- a/docs/src/components/command.njk +++ b/docs/src/components/command.njk @@ -19,11 +19,11 @@ toc: {% macro command_items(suggestions_id="suggestions", settings_id="settings") %}
Suggestions -
+
{% lucide "calendar" %} Calendar
-
+
{% lucide "smile" %} Search Emoji
@@ -35,17 +35,17 @@ toc:
Settings -
+
{% lucide "user" %} Profile ⌘P
-
+
{% lucide "credit-card" %} Billing ⌘B
-
+
{% lucide "settings" %} Settings ⌘S diff --git a/src/css/basecoat.css b/src/css/basecoat.css index dfdce7a..a83b4f4 100644 --- a/src/css/basecoat.css +++ b/src/css/basecoat.css @@ -181,6 +181,30 @@ } } +/* Badge */ +@layer components { + .badge, + .badge-primary, + .badge-secondary, + .badge-destructive, + .badge-outline { + @apply inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden; + } + .badge, + .badge-primary { + @apply border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90; + } + .badge-secondary { + @apply border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90; + } + .badge-destructive { + @apply border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60; + } + .badge-outline { + @apply text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground; + } +} + /* Button */ @layer components { .btn, @@ -359,27 +383,44 @@ } } -/* Badge */ -@layer components { - .badge, - .badge-primary, - .badge-secondary, - .badge-destructive, - .badge-outline { - @apply inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden; +/* Button Group */ +.button-group { + @apply inline-flex isolate; + + > *:hover, + > *:focus-visible, + > :is(.dropdown-menu, .popover) > button:hover, + > :is(.dropdown-menu, .popover) > button:focus-visible { + @apply relative z-10; } - .badge, - .badge-primary { - @apply border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90; + + > hr[role='separator'] { + @apply w-0 h-auto self-stretch border border-input shrink-0 m-0; } - .badge-secondary { - @apply border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90; - } - .badge-destructive { - @apply border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60; - } - .badge-outline { - @apply text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground; + + &:not([data-orientation='vertical']) { + > *:not(:first-child), + > :is(.dropdown-menu, .popover):not(:first-child) > button { + @apply -ml-px rounded-l-none; + } + > *:not(:last-child), + > :is(.dropdown-menu, .popover):not(:last-child) > button { + @apply rounded-r-none; + } + } + &[data-orientation='vertical'] { + @apply flex-col; + + > hr[role='separator'] { + @apply w-auto h-px; + } + + > *:not(:first-child) { + @apply -mt-px ml-0 rounded-t-none; + } + > *:not(:last-child) { + @apply rounded-b-none; + } } }