Cleaning up button group

This commit is contained in:
Ronan Berder
2025-10-31 08:56:44 +08:00
parent 6148716f68
commit 15473865b4
5 changed files with 498 additions and 156 deletions
+1
View File
@@ -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 },
+187 -132
View File
@@ -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;
}
}
}
+245
View File
@@ -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 %}
<div class="flex w-fit items-stretch gap-2">
<button class="btn-icon-outline" aria-label="Go Back">{% lucide "arrow-left" %}</button>
<div role="group" class="button-group">
<button class="btn-outline">Archive</button>
<button class="btn-outline">Report</button>
</div>
<div role="group" class="button-group">
<button class="btn-outline">Snooze</button>
{% set trigger %}
{% lucide "ellipsis" %}
{% endset %}
{% call dropdown_menu(
trigger=trigger,
trigger_attrs={"class": "btn-icon-outline"},
popover_attrs={"data-align": "end"}
) %}
<div role="menuitem">
{% lucide "mail-check" %}
Mark as Read
</div>
<div role="menuitem">
{% lucide "archive" %}
Archive
</div>
<hr role="separator">
<div role="menuitem">
{% lucide "clock" %}
Snooze
</div>
<div role="menuitem">
{% lucide "calendar" %}
Add to Calendar
</div>
<div role="menuitem">
{% lucide "list-filter-plus" %}
Add to List
</div>
<hr role="separator">
<div role="menuitem" class="text-destructive hover:bg-destructive/10 dark:hover:bg-destructive/20 focus:bg-destructive/10 dark:focus:bg-destructive/20 focus:text-destructive [&_svg]:!text-destructive">
{% lucide "trash-2" %}
Trash
</div>
{% endcall %}
</div>
{% endset %}
{{ code_preview("button-group-default", code_default | prettyHtml) }}
<h2 id="usage"><a href="#usage">Usage</a></h2>
{{ code_block(code_default | prettyHtml, "html") }}
<h2 id="examples"><a href="#examples">Examples</a></h2>
<h3 id="example-orientation"><a href="#example-orientation">Orientation</a></h3>
<section class="prose">
<p>Use <code>data-orientation="vertical"</code> to stack buttons vertically.</p>
</section>
{% set code_orientation %}
<div role="group" class="button-group" data-orientation="vertical">
<button class="btn-icon-outline" aria-label="Zoom in">
{% lucide "plus" %}
</button>
<button class="btn-icon-outline" aria-label="Zoom out">
{% lucide "minus" %}
</button>
</div>
{% endset %}
{{ code_preview("button-group-orientation", code_orientation | prettyHtml) }}
<h3 id="example-size"><a href="#example-size">Size</a></h3>
{% set code_size %}
<div class="flex flex-col items-start gap-8">
<div role="group" class="button-group">
<button class="btn-sm-outline">Small</button>
<button class="btn-sm-outline">Button</button>
<button class="btn-sm-outline">Group</button>
<button class="btn-sm-icon-outline">{% lucide "plus" %}</button>
</div>
<div role="group" class="button-group">
<button class="btn-outline">Default</button>
<button class="btn-outline">Button</button>
<button class="btn-outline">Group</button>
<button class="btn-icon-outline">{% lucide "plus" %}</button>
</div>
<div role="group" class="button-group">
<button class="btn-lg-outline">Small</button>
<button class="btn-lg-outline">Button</button>
<button class="btn-lg-outline">Group</button>
<button class="btn-lg-icon-outline">{% lucide "plus" %}</button>
</div>
</div>
{% endset %}
{{ code_preview("button-group-size", code_size | prettyHtml) }}
<h3 id="example-nested"><a href="#example-nested">Nested</a></h3>
{% set code_nested %}
<div role="group" aria-label="Pagination controls" class="flex w-fit items-stretch gap-2">
<div role="group" class="button-group">
<button class="btn-sm-outline">1</button>
<button class="btn-sm-outline">2</button>
<button class="btn-sm-outline">3</button>
<button class="btn-sm-outline">4</button>
<button class="btn-sm-outline">5</button>
</div>
<div role="group" class="button-group">
<button class="btn-sm-icon-outline" aria-label="Previous page">
{% lucide "chevron-left" %}
</button>
<button class="btn-sm-icon-outline" aria-label="Next page">
{% lucide "chevron-right" %}
</button>
</div>
</div>
{% endset %}
{{ code_preview("button-group-nested", code_nested | prettyHtml) }}
<h3 id="example-separator"><a href="#example-separator">Separator</a></h3>
<section class="prose">
<p>Use <code>&lt;hr role="separator"&gt;</code> to visually divide buttons within a group.</p>
</section>
{% set code_separator %}
<div role="group" class="button-group">
<button class="btn-secondary">Copy</button>
<hr role="separator">
<button class="btn-secondary">Paste</button>
</div>
{% endset %}
{{ code_preview("button-group-separator", code_separator | prettyHtml) }}
<h3 id="example-dropdown-menu"><a href="#example-dropdown-menu">Dropdown Menu</a></h3>
{% set code_dropdown_menu %}
<div role="group" class="button-group">
<button class="btn-outline">Follow</button>
{% set trigger %}
{% lucide "chevron-down" %}
{% endset %}
{% call dropdown_menu(
trigger=trigger,
trigger_attrs={"class": "btn-icon-outline"},
popover_attrs={"data-align": "end"}
) %}
<div role="menuitem">
{% lucide "volume-off" %}
Mute Conversation
</div>
<div role="menuitem">
{% lucide "check" %}
Mark as Read
</div>
<div role="menuitem">
{% lucide "triangle-alert" %}
Report Conversation
</div>
<div role="menuitem">
{% lucide "user-round-x" %}
Block User
</div>
<div role="menuitem">
{% lucide "share" %}
Share Conversation
</div>
<div role="menuitem">
{% lucide "copy" %}
Copy Conversation
</div>
<hr role="separator">
<div role="menuitem" class="text-destructive hover:bg-destructive/10 dark:hover:bg-destructive/20 focus:bg-destructive/10 dark:focus:bg-destructive/20 focus:text-destructive [&_svg]:!text-destructive">
{% lucide "trash-2" %}
Delete Conversation
</div>
{% endcall %}
</div>
{% endset %}
{{ code_preview("button-group-dropdown-menu", code_dropdown_menu | prettyHtml) }}
<h3 id="example-nested"><a href="#example-nested">Popover</a></h3>
{% set code_popover %}
<div role="group" class="button-group">
<button class="btn-outline">
{% lucide "bot" %}
Copilot
</button>
{% call popover(
trigger=trigger,
trigger_attrs={"class": "btn-icon-outline"},
popover_attrs={"class": "w-72 p-0", "data-align": "end"}
) %}
<header class="px-4 py-3 text-sm font-medium border-b">Agent Tasks</header>
<section class="p-4 text-sm space-y-2">
<textarea class="textarea w-full mb-4" placeholder="Describe your taks in natural language"></textarea>
<p class="font-medium">Start a new task with Copilot</p>
<p class="text-muted-foreground">
Describe your task in natural language. Copilot will work in the
background and open a pull request for your review.
</p>
</section>
{% endcall %}
</div>
{% endset %}
{{ code_preview("button-group-popover", code_popover | prettyHtml) }}
+5 -5
View File
@@ -19,11 +19,11 @@ toc:
{% macro command_items(suggestions_id="suggestions", settings_id="settings") %}
<div role="group" aria-labelledby="{{ suggestions_id }}">
<span role="heading" id="{{ suggestions_id }}">Suggestions</span>
<div role="option" data-value="calendar">
<div role="option" data-value="calendar" onclick="this.closest('dialog').close()">
{% lucide "calendar" %}
<span>Calendar</span>
</div>
<div role="option" data-value="search-emoji">
<div role="option" data-value="search-emoji" onclick="this.closest('dialog').close()">
{% lucide "smile" %}
<span>Search Emoji</span>
</div>
@@ -35,17 +35,17 @@ toc:
<hr role="separator" />
<div role="group" aria-labelledby="{{ settings_id }}">
<span role="heading" id="{{ settings_id }}">Settings</span>
<div role="option" data-value="profile">
<div role="option" data-value="profile" onclick="this.closest('dialog').close()">
{% lucide "user" %}
<span>Profile</span>
<kbd class="ml-auto text-muted-foreground bg-transparent tracking-widest">⌘P</kbd>
</div>
<div role="option" data-value="billing">
<div role="option" data-value="billing" onclick="this.closest('dialog').close()">
{% lucide "credit-card" %}
<span>Billing</span>
<kbd class="ml-auto text-muted-foreground bg-transparent tracking-widest">⌘B</kbd>
</div>
<div role="option" data-value="settings">
<div role="option" data-value="settings" onclick="this.closest('dialog').close()">
{% lucide "settings" %}
<span>Settings</span>
<kbd class="ml-auto text-muted-foreground bg-transparent tracking-widest">⌘S</kbd>
+60 -19
View File
@@ -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;
}
}
}