mirror of
https://github.com/tabler/tabler.git
synced 2026-10-12 05:06:55 +04:00
Add item slots, sizes, selectable and nested items to list group (#3272)
This commit is contained in:
1 parent
5aad08b7c1
commit
69c1114f7a
10 files changed
+898
-112
No files matched your search
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@tabler/core": minor
|
||||
"@tabler/preview": minor
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Added item slots, `.list-group-sm` and `.list-group-lg` sizes, selectable and nested items to the list group.
|
||||
@@ -1968,7 +1968,7 @@ $list-group-border-color: var(--border-color) !default;
|
||||
$list-group-border-width: var(--border-width) !default;
|
||||
$list-group-border-radius: var(--border-radius) !default;
|
||||
|
||||
$list-group-item-padding-y: $card-cap-padding-y !default;
|
||||
$list-group-item-padding-y: 1rem !default;
|
||||
$list-group-item-padding-x: $card-cap-padding-x !default;
|
||||
|
||||
$list-group-hover-bg: var(--hover-bg) !default;
|
||||
@@ -1984,6 +1984,12 @@ $list-group-action-hover-color: var(--emphasis-color) !default;
|
||||
$list-group-action-active-color: var(--body-color) !default;
|
||||
$list-group-action-active-bg: var(--active-bg) !default;
|
||||
$list-group-header-padding-y: 0.5rem !default;
|
||||
$list-group-item-gap: 1rem !default;
|
||||
$list-group-nested-indent: 2.25rem !default;
|
||||
$list-group-item-padding-y-sm: 0.5rem !default;
|
||||
$list-group-item-gap-sm: 0.75rem !default;
|
||||
$list-group-item-padding-y-lg: 1.5rem !default;
|
||||
$list-group-item-gap-lg: 1.25rem !default;
|
||||
|
||||
// Color variants: a tinted row, matched to the `bg-{color}-lt` utilities
|
||||
$list-group-variant-border-alpha: 0.24 !default;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
.list-group {
|
||||
--list-group-header-padding-y: #{$list-group-header-padding-y};
|
||||
--list-group-item-gap: #{$list-group-item-gap};
|
||||
--list-group-nested-indent: #{$list-group-nested-indent};
|
||||
|
||||
margin-inline-start: 0;
|
||||
margin-inline-end: 0;
|
||||
@@ -35,10 +37,98 @@
|
||||
}
|
||||
}
|
||||
|
||||
.list-group-item.active {
|
||||
// A checked `.list-group-selectable-check` marks its item like `.active`,
|
||||
// the same way `.table-selectable` marks a table row.
|
||||
.list-group-item.active,
|
||||
.list-group-selectable .list-group-item:has(.list-group-selectable-check:checked) {
|
||||
background-color: var(--active-bg);
|
||||
border-inline-start-color: $component-active-bg;
|
||||
border-inline-start-width: $border-width-wide;
|
||||
// An inset shadow instead of a wider border, so the content does not shift
|
||||
box-shadow: inset $border-width-wide 0 0 $component-active-bg;
|
||||
}
|
||||
|
||||
// Density
|
||||
.list-group-sm {
|
||||
--list-group-item-padding-y: #{$list-group-item-padding-y-sm};
|
||||
--list-group-item-gap: #{$list-group-item-gap-sm};
|
||||
}
|
||||
|
||||
.list-group-lg {
|
||||
--list-group-item-padding-y: #{$list-group-item-padding-y-lg};
|
||||
--list-group-item-gap: #{$list-group-item-gap-lg};
|
||||
}
|
||||
|
||||
// Item slots: media, content (title + description), meta and actions.
|
||||
// The item becomes a flex row only when it holds a content slot, so items
|
||||
// with plain text or a grid row inside keep their layout.
|
||||
.list-group-item:has(> .list-group-item-content) {
|
||||
display: flex;
|
||||
gap: var(--list-group-item-gap);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.list-group-item-media,
|
||||
.list-group-item-meta,
|
||||
.list-group-item-actions {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.list-group-item-media {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.list-group-item-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.list-group-item-title,
|
||||
.list-group-item-description {
|
||||
display: block;
|
||||
@include text-truncate();
|
||||
}
|
||||
|
||||
.list-group-item-title {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.list-group-item-description,
|
||||
.list-group-item-meta {
|
||||
color: var(--secondary);
|
||||
}
|
||||
|
||||
// Nested items: a block of indented items under a parent item, usually a
|
||||
// `.collapse` toggled by that parent. It draws the side and bottom borders its
|
||||
// borderless items leave out.
|
||||
.list-group-nested {
|
||||
border: var(--list-group-border-width) solid var(--list-group-border-color);
|
||||
border-top-width: 0;
|
||||
|
||||
> .list-group-item {
|
||||
padding-inline-start: calc(var(--list-group-item-padding-x) + var(--list-group-nested-indent));
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
+ .list-group-item {
|
||||
border-top-width: 0;
|
||||
}
|
||||
|
||||
.list-group-flush > & {
|
||||
border-inline-width: 0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Chevron of a parent item, turned over while its nested items are open
|
||||
.list-group-item-toggle {
|
||||
@include transition(transform $transition-time);
|
||||
|
||||
[aria-expanded='true'] & {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.list-bordered {
|
||||
|
||||
@@ -13,8 +13,29 @@ classnames:
|
||||
desc: One entry
|
||||
- class: list-group-header
|
||||
desc: Section label between entries
|
||||
- class: list-group-item-media
|
||||
desc: Slot for an icon, an avatar or an input on the leading edge of an entry
|
||||
- class: list-group-item-content
|
||||
desc: Main slot of an entry; its presence turns the entry into a row of slots
|
||||
- class: list-group-item-title
|
||||
desc: First line of the content, cut with an ellipsis
|
||||
- class: list-group-item-description
|
||||
desc: Second line of the content, in the secondary color
|
||||
- class: list-group-item-meta
|
||||
desc: Slot for a time, a count or a badge after the content
|
||||
- class: list-group-item-actions
|
||||
desc: Slot for controls on the trailing edge of an entry
|
||||
- class: list-group-selectable-check
|
||||
desc: Checkbox or radio that marks its entry when checked
|
||||
- class: list-group-nested
|
||||
desc: Block of indented entries under a parent entry, usually a collapse
|
||||
- class: list-group-item-toggle
|
||||
desc: Chevron of a parent entry, turned over while its nested entries are open
|
||||
size:
|
||||
- class: list-group-sm
|
||||
desc: Compact entries
|
||||
- class: list-group-lg
|
||||
desc: Roomy entries
|
||||
style:
|
||||
- class: list-group-flush
|
||||
desc: Removes the outer border, to sit flush in a card
|
||||
@@ -29,6 +50,8 @@ classnames:
|
||||
desc: Highlights the entry under the cursor
|
||||
- class: list-group-item-action
|
||||
desc: Marks an entry as a link or a button
|
||||
- class: list-group-selectable
|
||||
desc: Gives an entry with a checked input the active look
|
||||
direction:
|
||||
- class: list-group-horizontal
|
||||
desc: Lays the entries out in a row; -{breakpoint} only above that width
|
||||
@@ -61,6 +84,113 @@ The container can be any element. Use `ul` with `li` items for a real list, or `
|
||||
</ul>
|
||||
</Example>
|
||||
|
||||
## Item layout
|
||||
|
||||
An item can hold more than one line of text. Put a `list-group-item-content` element in the item and it becomes a row of slots:
|
||||
|
||||
- `list-group-item-media` - an icon, an avatar or an input before the content,
|
||||
- `list-group-item-content` - the main part, with `list-group-item-title` and `list-group-item-description`,
|
||||
- `list-group-item-meta` - a time, a count or a badge after the content,
|
||||
- `list-group-item-actions` - buttons and other controls at the end.
|
||||
|
||||
Only the content slot is required. The title and the description are cut with an ellipsis when they do not fit.
|
||||
|
||||
<Example bg="surface-secondary" column>
|
||||
<div class="list-group">
|
||||
<div class="list-group-item">
|
||||
<div class="list-group-item-media">
|
||||
<span class="avatar" style="background-image: url(/static/avatars/000m.jpg)"></span>
|
||||
</div>
|
||||
<div class="list-group-item-content">
|
||||
<div class="list-group-item-title">Paweł Kuna</div>
|
||||
<div class="list-group-item-description">UI Designer</div>
|
||||
</div>
|
||||
<div class="list-group-item-meta">2 h</div>
|
||||
<div class="list-group-item-actions">
|
||||
<button type="button" class="btn btn-sm">Follow</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-group-item">
|
||||
<div class="list-group-item-media">
|
||||
<span class="avatar" style="background-image: url(/static/avatars/016f.jpg)"></span>
|
||||
</div>
|
||||
<div class="list-group-item-content">
|
||||
<div class="list-group-item-title">Alice Marlin</div>
|
||||
<div class="list-group-item-description">Frontend developer</div>
|
||||
</div>
|
||||
<div class="list-group-item-meta">3 d</div>
|
||||
<div class="list-group-item-actions">
|
||||
<button type="button" class="btn btn-sm">Follow</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
The item itself can be a link or a `label`. Use a `label` with a checkbox, a radio or a switch inside, so a click on the whole row changes the input.
|
||||
|
||||
<Example bg="surface-secondary" column>
|
||||
<div class="list-group">
|
||||
<label class="list-group-item">
|
||||
<span class="list-group-item-media"><Icon name="mail" color="muted" /></span>
|
||||
<span class="list-group-item-content">
|
||||
<span class="list-group-item-title">Email</span>
|
||||
<span class="list-group-item-description">A summary every morning</span>
|
||||
</span>
|
||||
<span class="list-group-item-actions">
|
||||
<span class="form-check form-switch m-0"><input type="checkbox" role="switch" class="form-check-input" checked /></span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="list-group-item">
|
||||
<span class="list-group-item-media"><Icon name="device-mobile" color="muted" /></span>
|
||||
<span class="list-group-item-content">
|
||||
<span class="list-group-item-title">Push</span>
|
||||
<span class="list-group-item-description">Mentions and direct messages</span>
|
||||
</span>
|
||||
<span class="list-group-item-actions">
|
||||
<span class="form-check form-switch m-0"><input type="checkbox" role="switch" class="form-check-input" /></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
### Longer text
|
||||
|
||||
The slots are centered and the description stays on one line. For longer text add the `align-items-start` utility to the item and `text-wrap` to the description.
|
||||
|
||||
<Example bg="surface-secondary" column>
|
||||
<div class="list-group">
|
||||
<div class="list-group-item align-items-start">
|
||||
<div class="list-group-item-media">
|
||||
<span class="avatar" style="background-image: url(/static/avatars/000m.jpg)"></span>
|
||||
</div>
|
||||
<div class="list-group-item-content">
|
||||
<div class="list-group-item-title">Paweł Kuna</div>
|
||||
<div class="list-group-item-description text-wrap">The new list layout works well on the dashboard. Can we use the same slots for the search results?</div>
|
||||
</div>
|
||||
<div class="list-group-item-meta">2 h</div>
|
||||
</div>
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
## Sizes
|
||||
|
||||
Add `list-group-sm` for compact items or `list-group-lg` for items with more room. The classes change the vertical padding of the items and the gap between the slots.
|
||||
|
||||
<Example bg="surface-secondary" column>
|
||||
<div class="list-group list-group-sm mb-3">
|
||||
<div class="list-group-item">Small item</div>
|
||||
<div class="list-group-item">Small item</div>
|
||||
</div>
|
||||
<div class="list-group mb-3">
|
||||
<div class="list-group-item">Default item</div>
|
||||
<div class="list-group-item">Default item</div>
|
||||
</div>
|
||||
<div class="list-group list-group-lg">
|
||||
<div class="list-group-item">Large item</div>
|
||||
<div class="list-group-item">Large item</div>
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
## Variants
|
||||
|
||||
### Flush
|
||||
@@ -101,6 +231,57 @@ Mark the current item with `active`. Tabler shows it with a thicker border on th
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
### Selectable items
|
||||
|
||||
Add `list-group-selectable` to the container and `list-group-selectable-check` to the checkbox or radio in each item. A checked input gives its item the same look as `active`, with no JavaScript. Use a `label` as the item, so a click on the whole row changes the input.
|
||||
|
||||
<Example bg="surface-secondary" column>
|
||||
<div class="list-group list-group-selectable">
|
||||
<label class="list-group-item">
|
||||
<span class="list-group-item-media"><input type="checkbox" class="form-check-input list-group-selectable-check m-0" checked /></span>
|
||||
<span class="list-group-item-content">
|
||||
<span class="list-group-item-title">Paweł Kuna</span>
|
||||
<span class="list-group-item-description">UI Designer</span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="list-group-item">
|
||||
<span class="list-group-item-media"><input type="checkbox" class="form-check-input list-group-selectable-check m-0" /></span>
|
||||
<span class="list-group-item-content">
|
||||
<span class="list-group-item-title">Alice Marlin</span>
|
||||
<span class="list-group-item-description">Frontend developer</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
### Nested items
|
||||
|
||||
Put the child items in a `list-group-nested` element right after their parent item. The nested items are indented and have no borders between them. Make the element a `collapse` and the parent item its toggle to open and close the group. Add `list-group-item-toggle` to the chevron, and it turns over while the group is open.
|
||||
|
||||
<Example bg="surface-secondary" column>
|
||||
<div class="list-group">
|
||||
<button type="button" class="list-group-item list-group-item-action" data-bs-toggle="collapse" data-bs-target="#list-nested-projects" aria-expanded="true" aria-controls="list-nested-projects">
|
||||
<span class="list-group-item-media"><Icon name="folder" color="muted" /></span>
|
||||
<span class="list-group-item-content">
|
||||
<span class="list-group-item-title">Projects</span>
|
||||
</span>
|
||||
<span class="list-group-item-meta"><Icon name="chevron-down" class="list-group-item-toggle" /></span>
|
||||
</button>
|
||||
<div class="list-group-nested collapse show" id="list-nested-projects">
|
||||
<a href="#" class="list-group-item list-group-item-action">Website redesign</a>
|
||||
<a href="#" class="list-group-item list-group-item-action">Mobile app</a>
|
||||
</div>
|
||||
<a href="#" class="list-group-item list-group-item-action">
|
||||
<span class="list-group-item-media"><Icon name="settings" color="muted" /></span>
|
||||
<span class="list-group-item-content">
|
||||
<span class="list-group-item-title">Settings</span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
The indent is the `--tblr-list-group-nested-indent` variable. Its default matches an icon in the media slot.
|
||||
|
||||
### Section header
|
||||
|
||||
Use `list-group-header` to split a long list into groups. The header is a small uppercase label with its own background.
|
||||
@@ -122,20 +303,16 @@ Add `list-group-hoverable` to the container to highlight the row under the point
|
||||
<Example bg="surface-secondary" column>
|
||||
<div class="list-group list-group-hoverable">
|
||||
<div class="list-group-item">
|
||||
<div class="row align-items-center">
|
||||
<div class="col text-truncate">Change deprecated html tags</div>
|
||||
<div class="col-auto">
|
||||
<a href="#" class="list-group-item-actions" aria-label="Add to favorites"><Icon name="star" color="muted" /></a>
|
||||
</div>
|
||||
<div class="list-group-item-content">
|
||||
<div class="list-group-item-title">Change deprecated html tags</div>
|
||||
</div>
|
||||
<a href="#" class="list-group-item-actions" aria-label="Add to favorites"><Icon name="star" color="muted" /></a>
|
||||
</div>
|
||||
<div class="list-group-item">
|
||||
<div class="row align-items-center">
|
||||
<div class="col text-truncate">Fix spacing in the navbar</div>
|
||||
<div class="col-auto">
|
||||
<a href="#" class="list-group-item-actions show" aria-label="Remove from favorites"><Icon name="star" color="yellow" /></a>
|
||||
</div>
|
||||
<div class="list-group-item-content">
|
||||
<div class="list-group-item-title">Fix spacing in the navbar</div>
|
||||
</div>
|
||||
<a href="#" class="list-group-item-actions show" aria-label="Remove from favorites"><Icon name="star" color="yellow" /></a>
|
||||
</div>
|
||||
</div>
|
||||
</Example>
|
||||
@@ -234,30 +411,22 @@ This pattern joins several variants: a flush list inside a card, hoverable rows,
|
||||
<div class="card-header"><h3 class="card-title">Notifications</h3></div>
|
||||
<div class="list-group list-group-flush list-group-hoverable">
|
||||
<div class="list-group-item">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto">
|
||||
<span class="status-dot status-dot-animated bg-red d-block"></span>
|
||||
</div>
|
||||
<div class="col text-truncate">
|
||||
<a href="#" class="text-body d-block">Build failed</a>
|
||||
<div class="d-block text-secondary text-truncate mt-n1">The last deploy did not finish</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a href="#" class="list-group-item-actions" aria-label="Add to favorites"><Icon name="star" color="muted" /></a>
|
||||
</div>
|
||||
<div class="list-group-item-media">
|
||||
<span class="status-dot status-dot-animated bg-red"></span>
|
||||
</div>
|
||||
<div class="list-group-item-content">
|
||||
<a href="#" class="list-group-item-title">Build failed</a>
|
||||
<div class="list-group-item-description">The last deploy did not finish</div>
|
||||
</div>
|
||||
<a href="#" class="list-group-item-actions" aria-label="Add to favorites"><Icon name="star" color="muted" /></a>
|
||||
</div>
|
||||
<div class="list-group-item">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto"><span class="status-dot d-block"></span></div>
|
||||
<div class="col text-truncate">
|
||||
<a href="#" class="text-body d-block">New comment</a>
|
||||
<div class="d-block text-secondary text-truncate mt-n1">Alice replied in the design thread</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a href="#" class="list-group-item-actions show" aria-label="Remove from favorites"><Icon name="star" color="yellow" /></a>
|
||||
</div>
|
||||
<div class="list-group-item-media"><span class="status-dot"></span></div>
|
||||
<div class="list-group-item-content">
|
||||
<a href="#" class="list-group-item-title">New comment</a>
|
||||
<div class="list-group-item-description">Alice replied in the design thread</div>
|
||||
</div>
|
||||
<a href="#" class="list-group-item-actions show" aria-label="Remove from favorites"><Icon name="star" color="yellow" /></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+516
-26
@@ -1,50 +1,540 @@
|
||||
---
|
||||
import DefaultLayout from '@shared/layouts/DefaultLayout.astro'
|
||||
import Card from '@ui/Card.astro'
|
||||
import CardBody from '@ui/CardBody.astro'
|
||||
import CardHeader from '@ui/CardHeader.astro'
|
||||
import CardTitle from '@ui/CardTitle.astro'
|
||||
import CardSubtitle from '@ui/CardSubtitle.astro'
|
||||
import CardDropdown from '@ui/CardDropdown.astro'
|
||||
import UsersList from '@shared/components/cards/UsersList.astro'
|
||||
import UsersList2 from '@shared/components/cards/UsersList2.astro'
|
||||
import UsersListHeaders from '@shared/components/cards/UsersListHeaders.astro'
|
||||
import ListGroup from '@ui/ListGroup.astro'
|
||||
import ListGroupItem from '@ui/ListGroupItem.astro'
|
||||
import Avatar from '@ui/Avatar.astro'
|
||||
import Badge from '@ui/Badge.astro'
|
||||
import Flag from '@ui/Flag.astro'
|
||||
import Icon from '@ui/Icon.astro'
|
||||
import Progress from '@ui/Progress.astro'
|
||||
import StatusDot from '@ui/StatusDot.astro'
|
||||
import DocsLink from '@ui/DocsLink.astro'
|
||||
import Empty from '@ui/Empty.astro'
|
||||
import { people } from '@shared/lib/people'
|
||||
import type { FlagCountry } from '@shared/lib/tokens'
|
||||
import transactionsData from '@data/transactions.json'
|
||||
import notificationsData from '@data/notifications.json'
|
||||
|
||||
const team = people.slice(0, 4)
|
||||
|
||||
const folders = [
|
||||
{ icon: 'inbox', title: 'Inbox', description: 'Messages waiting for a reply', count: 12 },
|
||||
{ icon: 'send', title: 'Sent', description: 'Everything you have sent', count: 148 },
|
||||
{ icon: 'archive', title: 'Archive', description: 'Closed conversations', count: 2304 },
|
||||
{ icon: 'trash', title: 'Trash', description: 'Removed after 30 days', count: 7 },
|
||||
]
|
||||
|
||||
const plans = [
|
||||
{ id: 'starter', title: 'Starter', description: 'Up to 3 projects', price: '$9' },
|
||||
{ id: 'team', title: 'Team', description: 'Unlimited projects, 10 members', price: '$29' },
|
||||
{ id: 'business', title: 'Business', description: 'Unlimited members and SSO', price: '$79' },
|
||||
]
|
||||
|
||||
const channels = [
|
||||
{ id: 'email', icon: 'mail', title: 'Email', description: 'A summary every morning', checked: true },
|
||||
{ id: 'push', icon: 'device-mobile', title: 'Push', description: 'Mentions and direct messages', checked: true },
|
||||
{ id: 'slack', icon: 'brand-slack', title: 'Slack', description: 'Posts to the #updates channel', checked: false },
|
||||
]
|
||||
|
||||
const comments = [
|
||||
{ text: 'The new list layout works well on the dashboard. Can we use the same slots for the search results?', time: '2 h' },
|
||||
{ text: 'Yes, the results only need a wrapped description and the avatar aligned to the top.', time: '1 h' },
|
||||
{ text: 'Done. I also added the nested items to the sidebar.', time: '12 min' },
|
||||
]
|
||||
|
||||
const tree = [
|
||||
{ id: 'list-nested-projects', icon: 'folder', title: 'Projects', open: true, items: ['Website redesign', 'Mobile app', 'Design system'] },
|
||||
{ id: 'list-nested-teams', icon: 'users', title: 'Teams', open: false, items: ['Design', 'Engineering'] },
|
||||
]
|
||||
|
||||
const sizes = [
|
||||
{ size: undefined, title: 'Default', subtitle: 'Without a size class the items use the default padding.' },
|
||||
{ size: 'sm', title: 'Small', subtitle: 'Add <code>list-group-sm</code> for a compact list.' },
|
||||
{ size: 'lg', title: 'Large', subtitle: 'Add <code>list-group-lg</code> for more room around each item.' },
|
||||
] as const
|
||||
|
||||
const files = [
|
||||
{ icon: 'file-type-pdf', name: 'Annual report 2025.pdf', size: '2.4 MB' },
|
||||
{ icon: 'file-spreadsheet', name: 'Budget forecast.xlsx', size: '318 kB' },
|
||||
{ icon: 'photo', name: 'Office photos.zip', size: '48 MB' },
|
||||
{ icon: 'file-text', name: 'Meeting notes.md', size: '4 kB' },
|
||||
]
|
||||
|
||||
const visits: { country: string; flag: FlagCountry; value: number }[] = [
|
||||
{ country: 'United States', flag: 'us', value: 64 },
|
||||
{ country: 'Germany', flag: 'de', value: 48 },
|
||||
{ country: 'Poland', flag: 'pl', value: 31 },
|
||||
{ country: 'Japan', flag: 'jp', value: 17 },
|
||||
]
|
||||
|
||||
// Rows of transactions.json whose logo exists in static/.
|
||||
const transactionIds = ['3', '4', '9', '11']
|
||||
const transactions = (transactionsData as { id: string; counterparty: string; counterparty_logo: string; description: string; amount: number }[])
|
||||
.filter((item) => transactionIds.includes(item.id))
|
||||
.map((item) => ({
|
||||
...item,
|
||||
amountLabel: `${item.amount < 0 ? '−' : '+'}${Math.abs(item.amount).toLocaleString('en-US', { style: 'currency', currency: 'USD' })}`,
|
||||
}))
|
||||
|
||||
const notifications = (notificationsData as { title: string; description: string; status: string }[]).slice(0, 4).map((item, index) => ({
|
||||
...item,
|
||||
unread: item.status === 'unread',
|
||||
time: `${(index + 1) * 2} h`,
|
||||
}))
|
||||
---
|
||||
|
||||
<DefaultLayout title="Lists" pageMenu="base.lists" description="Lists of users, contacts, and links — built from list-group items or a plain card body.">
|
||||
<DefaultLayout title="Lists" pageMenu="base.lists" pageLibs={['sortablejs']} description="Vertical lists built from list-group items: media, content, meta and actions in one row.">
|
||||
<DocsLink slot="page-header-actions" path="/ui/components/list-group" />
|
||||
|
||||
<div class="row row-cards">
|
||||
<div class="col-md-6">
|
||||
<div class="row row-cards">
|
||||
<div class="col-12">
|
||||
<UsersList2 />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<UsersList offset={8} checkbox checkedIds={[2, 5, 8]} title="Contacts" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<Card>
|
||||
<CardBody>
|
||||
<CardTitle>Text only</CardTitle>
|
||||
<CardSubtitle>A plain <code>list-group-item</code> holds one line of text.</CardSubtitle>
|
||||
</CardBody>
|
||||
<ListGroup flush>
|
||||
{folders.map((item) => <ListGroupItem>{item.title}</ListGroupItem>)}
|
||||
</ListGroup>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<Card>
|
||||
<CardBody>
|
||||
<CardTitle>Title and description</CardTitle>
|
||||
<CardSubtitle>Put <code>list-group-item-title</code> and <code>list-group-item-description</code> in <code>list-group-item-content</code>.</CardSubtitle>
|
||||
</CardBody>
|
||||
<ListGroup flush>
|
||||
{folders.map((item) => <ListGroupItem title={item.title} description={item.description} />)}
|
||||
</ListGroup>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<Card>
|
||||
<CardBody>
|
||||
<CardTitle>With icon</CardTitle>
|
||||
<CardSubtitle>The <code>list-group-item-media</code> slot holds an icon before the content.</CardSubtitle>
|
||||
</CardBody>
|
||||
<ListGroup flush>
|
||||
{
|
||||
folders.map((item) => (
|
||||
<ListGroupItem title={item.title} description={item.description}>
|
||||
<Icon slot="media" name={item.icon} class="text-secondary" />
|
||||
</ListGroupItem>
|
||||
))
|
||||
}
|
||||
</ListGroup>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<Card>
|
||||
<CardBody>
|
||||
<CardTitle>With avatar</CardTitle>
|
||||
<CardSubtitle>The media slot takes an avatar the same way.</CardSubtitle>
|
||||
</CardBody>
|
||||
<ListGroup flush>
|
||||
{
|
||||
team.map((person) => (
|
||||
<ListGroupItem title={person.full_name} description={person.job_title}>
|
||||
<Avatar slot="media" person={person} />
|
||||
</ListGroupItem>
|
||||
))
|
||||
}
|
||||
</ListGroup>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<Card>
|
||||
<CardBody>
|
||||
<CardTitle>With meta</CardTitle>
|
||||
<CardSubtitle>Use <code>list-group-item-meta</code> for a time, a count or a badge at the end.</CardSubtitle>
|
||||
</CardBody>
|
||||
<ListGroup flush>
|
||||
{
|
||||
folders.map((item) => (
|
||||
<ListGroupItem title={item.title} description={item.description}>
|
||||
<Fragment slot="meta">{item.count}</Fragment>
|
||||
</ListGroupItem>
|
||||
))
|
||||
}
|
||||
</ListGroup>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<Card>
|
||||
<CardBody>
|
||||
<CardTitle>All slots</CardTitle>
|
||||
<CardSubtitle>Media, content, meta and <code>list-group-item-actions</code> in one item.</CardSubtitle>
|
||||
</CardBody>
|
||||
<ListGroup flush>
|
||||
{
|
||||
team.map((person, index) => (
|
||||
<ListGroupItem title={person.full_name} description={person.job_title}>
|
||||
<Avatar slot="media" person={person} />
|
||||
<Fragment slot="meta">{index === 0 ? <Badge color="green" light text="Owner" /> : `${index * 3} d`}</Fragment>
|
||||
<CardDropdown slot="actions" />
|
||||
</ListGroupItem>
|
||||
))
|
||||
}
|
||||
</ListGroup>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<Card>
|
||||
<CardBody>
|
||||
<CardTitle>Links</CardTitle>
|
||||
<CardSubtitle>Add <code>list-group-item-action</code> to a link item. It supports <code>active</code> and <code>disabled</code>.</CardSubtitle>
|
||||
</CardBody>
|
||||
<ListGroup flush>
|
||||
{
|
||||
folders.map((item, index) => (
|
||||
<ListGroupItem as="a" href={index === 3 ? undefined : '#'} action active={index === 0} disabled={index === 3} aria-current={index === 0 ? 'true' : undefined} title={item.title} description={item.description}>
|
||||
<Icon slot="media" name={item.icon} class="text-secondary" />
|
||||
<Icon slot="meta" name="chevron-right" />
|
||||
</ListGroupItem>
|
||||
))
|
||||
}
|
||||
</ListGroup>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<Card>
|
||||
<CardBody>
|
||||
<CardTitle>Actions on hover</CardTitle>
|
||||
<CardSubtitle>In a <code>list-group-hoverable</code> list the actions show on hover. Add <code>show</code> to keep them visible.</CardSubtitle>
|
||||
</CardBody>
|
||||
<ListGroup flush hoverable>
|
||||
{
|
||||
team.map((person, index) => (
|
||||
<ListGroupItem title={person.full_name} description={person.job_title} showActions={index === 1}>
|
||||
<Avatar slot="media" person={person} />
|
||||
<button type="button" slot="actions" class="btn btn-action" aria-label={index === 1 ? 'Remove from favorites' : 'Add to favorites'}>
|
||||
<Icon name="star" class={index === 1 ? 'text-yellow' : undefined} />
|
||||
</button>
|
||||
</ListGroupItem>
|
||||
))
|
||||
}
|
||||
</ListGroup>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<Card>
|
||||
<CardBody>
|
||||
<CardTitle>Buttons</CardTitle>
|
||||
<CardSubtitle>The actions slot holds any control, for example a button.</CardSubtitle>
|
||||
</CardBody>
|
||||
<ListGroup flush>
|
||||
{
|
||||
team.map((person, index) => (
|
||||
<ListGroupItem title={person.full_name} description={person.job_title}>
|
||||
<Avatar slot="media" person={person} />
|
||||
<button type="button" slot="actions" class:list={['btn btn-sm', index === 0 && 'btn-primary']}>
|
||||
{index === 0 ? 'Follow' : 'Following'}
|
||||
</button>
|
||||
</ListGroupItem>
|
||||
))
|
||||
}
|
||||
</ListGroup>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<Card>
|
||||
<CardBody>
|
||||
<CardTitle>Checkboxes</CardTitle>
|
||||
<CardSubtitle>In a <code>list-group-selectable</code> list a checked <code>list-group-selectable-check</code> marks its item.</CardSubtitle>
|
||||
</CardBody>
|
||||
<ListGroup flush selectable>
|
||||
{
|
||||
team.map((person, index) => (
|
||||
<ListGroupItem as="label" title={person.full_name} description={person.job_title}>
|
||||
<Fragment slot="media">
|
||||
<input type="checkbox" class="form-check-input list-group-selectable-check m-0 me-3" checked={index % 2 === 0} />
|
||||
<Avatar person={person} />
|
||||
</Fragment>
|
||||
</ListGroupItem>
|
||||
))
|
||||
}
|
||||
</ListGroup>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<Card>
|
||||
<CardBody>
|
||||
<CardTitle>Radios</CardTitle>
|
||||
<CardSubtitle>Radio inputs with one <code>name</code> make a single choice list. Use a <code>label</code> as the item.</CardSubtitle>
|
||||
</CardBody>
|
||||
<ListGroup flush selectable>
|
||||
{
|
||||
plans.map((plan, index) => (
|
||||
<ListGroupItem as="label" title={plan.title} description={plan.description}>
|
||||
<input slot="media" type="radio" name="list-plan" value={plan.id} class="form-check-input list-group-selectable-check m-0" checked={index === 1} />
|
||||
<Fragment slot="meta">{plan.price}</Fragment>
|
||||
</ListGroupItem>
|
||||
))
|
||||
}
|
||||
</ListGroup>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<Card>
|
||||
<CardBody>
|
||||
<CardTitle>Switches</CardTitle>
|
||||
<CardSubtitle>Put a switch in the actions slot to build a settings list.</CardSubtitle>
|
||||
</CardBody>
|
||||
<ListGroup flush>
|
||||
{
|
||||
channels.map((channel) => (
|
||||
<ListGroupItem as="label" title={channel.title} description={channel.description}>
|
||||
<Icon slot="media" name={channel.icon} class="text-secondary" />
|
||||
<span slot="actions" class="form-check form-switch m-0">
|
||||
<input type="checkbox" role="switch" class="form-check-input" checked={channel.checked} />
|
||||
</span>
|
||||
</ListGroupItem>
|
||||
))
|
||||
}
|
||||
</ListGroup>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<Card>
|
||||
<CardBody>
|
||||
<CardTitle>Wrapped description</CardTitle>
|
||||
<CardSubtitle>Add <code>align-items-start</code> to the item and <code>text-wrap</code> to the description for longer text.</CardSubtitle>
|
||||
</CardBody>
|
||||
<ListGroup flush>
|
||||
{
|
||||
comments.map((comment, index) => (
|
||||
<ListGroupItem class="align-items-start" title={team[index]?.full_name}>
|
||||
<Avatar slot="media" person={team[index]} />
|
||||
<div class="list-group-item-description text-wrap">{comment.text}</div>
|
||||
<Fragment slot="meta">{comment.time}</Fragment>
|
||||
</ListGroupItem>
|
||||
))
|
||||
}
|
||||
</ListGroup>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<Card>
|
||||
<CardBody>
|
||||
<CardTitle>Nested items</CardTitle>
|
||||
<CardSubtitle>Put <code>list-group-nested</code> on a collapse after the parent item. <code>list-group-item-toggle</code> turns the chevron.</CardSubtitle>
|
||||
</CardBody>
|
||||
<ListGroup flush>
|
||||
{
|
||||
tree.map((group) => (
|
||||
<>
|
||||
<ListGroupItem as="button" type="button" action title={group.title} data-bs-toggle="collapse" data-bs-target={`#${group.id}`} aria-expanded={group.open ? 'true' : 'false'} aria-controls={group.id}>
|
||||
<Icon slot="media" name={group.icon} class="text-secondary" />
|
||||
<Icon slot="meta" name="chevron-down" class="list-group-item-toggle" />
|
||||
</ListGroupItem>
|
||||
<div class:list={['list-group-nested collapse', group.open && 'show']} id={group.id}>
|
||||
{group.items.map((item) => (
|
||||
<ListGroupItem as="a" href="#" action>
|
||||
{item}
|
||||
</ListGroupItem>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
))
|
||||
}
|
||||
<ListGroupItem as="a" href="#" action title="Settings">
|
||||
<Icon slot="media" name="settings" class="text-secondary" />
|
||||
</ListGroupItem>
|
||||
</ListGroup>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<Card>
|
||||
<CardBody>
|
||||
<CardTitle>Drag handle</CardTitle>
|
||||
<CardSubtitle>Add <code>data-sortable</code> to the list and put the handle in the media slot.</CardSubtitle>
|
||||
</CardBody>
|
||||
<ListGroup flush data-sortable='{"animation":150,"handle":".sortable-handle"}'>
|
||||
{
|
||||
team.map((person) => (
|
||||
<ListGroupItem title={person.full_name} description={person.job_title}>
|
||||
<Fragment slot="media">
|
||||
<span class="sortable-handle cursor-move text-secondary me-3" aria-hidden="true">
|
||||
<Icon name="grip-vertical" />
|
||||
</span>
|
||||
<Avatar person={person} />
|
||||
</Fragment>
|
||||
</ListGroupItem>
|
||||
))
|
||||
}
|
||||
</ListGroup>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{
|
||||
sizes.map((item) => (
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<Card>
|
||||
<CardHeader title="Links and buttons" />
|
||||
<ListGroup flush>
|
||||
<ListGroupItem as="a" href="#" action active aria-current="true"> The current link item </ListGroupItem>
|
||||
<ListGroupItem as="a" href="#" action>A second link item</ListGroupItem>
|
||||
<ListGroupItem as="a" href="#" action>A third link item</ListGroupItem>
|
||||
<ListGroupItem as="a" href="#" action>A fourth link item</ListGroupItem>
|
||||
<ListGroupItem as="a" action disabled>A disabled link item</ListGroupItem>
|
||||
<CardBody>
|
||||
<CardTitle>{item.title}</CardTitle>
|
||||
<CardSubtitle>
|
||||
<Fragment set:html={item.subtitle} />
|
||||
</CardSubtitle>
|
||||
</CardBody>
|
||||
<ListGroup flush size={item.size}>
|
||||
{team.map((person) => (
|
||||
<ListGroupItem title={person.full_name} description={person.job_title}>
|
||||
<Avatar slot="media" person={person} size={item.size === 'sm' ? 'sm' : undefined} />
|
||||
</ListGroupItem>
|
||||
))}
|
||||
</ListGroup>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<Card>
|
||||
<CardHeader title="Notifications" />
|
||||
<ListGroup flush>
|
||||
{
|
||||
notifications.map((item) => (
|
||||
<ListGroupItem as="a" href="#" action title={item.title} description={item.description}>
|
||||
<StatusDot slot="media" color={item.unread ? 'blue' : undefined} animated={item.unread} ariaLabel={item.unread ? 'Unread' : 'Read'} />
|
||||
<Fragment slot="meta">{item.time}</Fragment>
|
||||
</ListGroupItem>
|
||||
))
|
||||
}
|
||||
</ListGroup>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<Card>
|
||||
<CardHeader title="Transactions" />
|
||||
<ListGroup flush>
|
||||
{
|
||||
transactions.map((item) => (
|
||||
<ListGroupItem title={item.counterparty} description={item.description}>
|
||||
<Avatar slot="media" src={item.counterparty_logo} />
|
||||
<span slot="meta" class:list={['fw-medium', item.amount < 0 ? 'text-body' : 'text-green']}>
|
||||
{item.amountLabel}
|
||||
</span>
|
||||
</ListGroupItem>
|
||||
))
|
||||
}
|
||||
</ListGroup>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<Card>
|
||||
<CardHeader title="Files" />
|
||||
<ListGroup flush>
|
||||
{
|
||||
files.map((file) => (
|
||||
<ListGroupItem title={file.name} description={file.size}>
|
||||
<Avatar slot="media" icon={file.icon} />
|
||||
<button type="button" slot="actions" class="btn btn-action" aria-label={`Download ${file.name}`}>
|
||||
<Icon name="download" />
|
||||
</button>
|
||||
</ListGroupItem>
|
||||
))
|
||||
}
|
||||
</ListGroup>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<Card>
|
||||
<CardHeader title="Visits by country" />
|
||||
<ListGroup flush>
|
||||
{
|
||||
visits.map((item) => (
|
||||
<ListGroupItem title={item.country}>
|
||||
<Flag slot="media" flag={item.flag} />
|
||||
<Progress size="sm" class="mt-2" value={item.value} aria-label={`${item.value}% of visits`} />
|
||||
<Fragment slot="meta">{item.value}%</Fragment>
|
||||
</ListGroupItem>
|
||||
))
|
||||
}
|
||||
</ListGroup>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<Card>
|
||||
<CardBody>
|
||||
<CardTitle>Inside a card body</CardTitle>
|
||||
<CardSubtitle>Without <code>list-group-flush</code> the list keeps its own border and rounded corners.</CardSubtitle>
|
||||
<ListGroup>
|
||||
{
|
||||
files.slice(0, 3).map((file) => (
|
||||
<ListGroupItem title={file.name}>
|
||||
<Icon slot="media" name={file.icon} class="text-secondary" />
|
||||
<Fragment slot="meta">{file.size}</Fragment>
|
||||
</ListGroupItem>
|
||||
))
|
||||
}
|
||||
</ListGroup>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<Card>
|
||||
<CardHeader title="Loading" />
|
||||
<ListGroup flush class="placeholder-glow" aria-hidden="true">
|
||||
{
|
||||
team.map((_, index) => (
|
||||
<ListGroupItem>
|
||||
<span slot="media" class="avatar placeholder" />
|
||||
<div class:list={['placeholder', index % 2 ? 'col-5' : 'col-7']} />
|
||||
<div class:list={['placeholder placeholder-xs', index % 2 ? 'col-9' : 'col-8']} />
|
||||
</ListGroupItem>
|
||||
))
|
||||
}
|
||||
</ListGroup>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<Card>
|
||||
<CardHeader title="Empty" />
|
||||
<Empty icon="inbox-off" title="No messages" description="New messages from your team show up here." />
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<UsersList offset={8} checkbox checkedIds={[2, 5, 8]} title="Contacts" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="row row-cards">
|
||||
<div class="col-12">
|
||||
<UsersList hoverable />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<UsersListHeaders />
|
||||
</div>
|
||||
</div>
|
||||
<UsersList hoverable />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<UsersListHeaders />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<UsersList2 />
|
||||
</div>
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
@@ -18,7 +18,7 @@ interface Props {
|
||||
class?: string
|
||||
}
|
||||
|
||||
const { offset = 0, hoverable = false, checkbox, checkedIds, title = 'Last commits', class: className } = Astro.props
|
||||
const { offset = 0, hoverable = false, checkbox = false, checkedIds, title = 'Last commits', class: className } = Astro.props
|
||||
|
||||
const limit = 8
|
||||
|
||||
@@ -44,40 +44,25 @@ const rows = people.slice(offset, offset + limit).map((person, idx) => {
|
||||
<div class="card-header">
|
||||
<CardTitle>{title}</CardTitle>
|
||||
</div>
|
||||
<ListGroup flush hoverable={hoverable}>
|
||||
<ListGroup flush hoverable={hoverable} selectable={checkbox}>
|
||||
{
|
||||
rows.map((row) => (
|
||||
<ListGroupItem active={row.checked}>
|
||||
<div class="row align-items-center">
|
||||
{checkbox ? (
|
||||
<div class="col-auto">
|
||||
<input type="checkbox" class="form-check-input" checked={row.checked} />
|
||||
</div>
|
||||
) : (
|
||||
<div class="col-auto">
|
||||
<span class={`badge${row.color !== 'x' ? ` bg-${row.color}` : ''}`} />
|
||||
</div>
|
||||
)}
|
||||
<div class="col-auto">
|
||||
<a href="#" tabindex="-1" aria-hidden="true">
|
||||
<Avatar person={row.person} />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col text-truncate">
|
||||
<a href="#" class="text-reset d-block">
|
||||
{row.person.full_name}
|
||||
</a>
|
||||
<div class="d-block text-secondary text-truncate mt-n1">{row.description}</div>
|
||||
</div>
|
||||
{hoverable && (
|
||||
<div class="col-auto">
|
||||
<a href="#" class={`list-group-item-actions${row.checked ? ' show' : ''}`} aria-label={row.checked ? 'Remove from favorites' : 'Add to favorites'}>
|
||||
<Icon name="star" class={row.starColor} />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ListGroupItem active={!checkbox && row.checked} showActions={row.checked}>
|
||||
<Fragment slot="media">
|
||||
{checkbox ? <input type="checkbox" class="form-check-input list-group-selectable-check m-0" checked={row.checked} aria-label={`Select ${row.person.full_name}`} /> : <span class={`badge${row.color !== 'x' ? ` bg-${row.color}` : ''}`} />}
|
||||
<a href="#" class="ms-3" tabindex="-1" aria-hidden="true">
|
||||
<Avatar person={row.person} />
|
||||
</a>
|
||||
</Fragment>
|
||||
<a href="#" class="list-group-item-title">
|
||||
{row.person.full_name}
|
||||
</a>
|
||||
<div class="list-group-item-description">{row.description}</div>
|
||||
{hoverable && (
|
||||
<a href="#" slot="actions" aria-label={row.checked ? 'Remove from favorites' : 'Add to favorites'}>
|
||||
<Icon name="star" class={row.starColor} />
|
||||
</a>
|
||||
)}
|
||||
</ListGroupItem>
|
||||
))
|
||||
}
|
||||
|
||||
@@ -39,19 +39,13 @@ const rows = sorted.map((person, idx) => {
|
||||
<>
|
||||
{row.header && <ListGroupHeader class="sticky-top">{row.header}</ListGroupHeader>}
|
||||
<ListGroupItem>
|
||||
<div class="row">
|
||||
<div class="col-auto">
|
||||
<a href="#" tabindex="-1" aria-hidden="true">
|
||||
<Avatar person={row.person} />
|
||||
</a>
|
||||
</div>
|
||||
<div class="col text-truncate">
|
||||
<a href="#" class="text-body d-block">
|
||||
{row.person.full_name}
|
||||
</a>
|
||||
<div class="text-secondary text-truncate mt-n1">{row.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#" slot="media" tabindex="-1" aria-hidden="true">
|
||||
<Avatar person={row.person} />
|
||||
</a>
|
||||
<a href="#" class="list-group-item-title">
|
||||
{row.person.full_name}
|
||||
</a>
|
||||
<div class="list-group-item-description">{row.description}</div>
|
||||
</ListGroupItem>
|
||||
</>
|
||||
))
|
||||
|
||||
@@ -5,18 +5,24 @@ import type { HTMLTag } from 'astro/types'
|
||||
|
||||
type PolymorphicProps = { as?: HTMLTag }
|
||||
|
||||
/** core/scss/ui/_lists.scss `.list-group-{size}` density modifiers */
|
||||
type ListGroupSize = 'sm' | 'lg'
|
||||
|
||||
interface Props extends PolymorphicProps {
|
||||
size?: ListGroupSize | undefined
|
||||
flush?: boolean
|
||||
hoverable?: boolean
|
||||
/** list-group-selectable: an item with a checked .list-group-selectable-check input gets the active look */
|
||||
selectable?: boolean
|
||||
transparent?: boolean
|
||||
class?: string
|
||||
/** remaining attributes (data-sortable, style, …) are forwarded to the element */
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
const { as: Element = 'div', flush, hoverable, transparent, class: className, ...rest }: Props = Astro.props
|
||||
const { as: Element = 'div', size, flush, hoverable, selectable, transparent, class: className, ...rest }: Props = Astro.props
|
||||
---
|
||||
|
||||
<Element class:list={['list-group', flush && 'list-group-flush', hoverable && 'list-group-hoverable', transparent && 'list-group-transparent', className]} {...rest}>
|
||||
<Element class:list={['list-group', size && `list-group-${size}`, flush && 'list-group-flush', hoverable && 'list-group-hoverable', selectable && 'list-group-selectable', transparent && 'list-group-transparent', className]} {...rest}>
|
||||
<slot />
|
||||
</Element>
|
||||
@@ -1,7 +1,8 @@
|
||||
---
|
||||
import type { HTMLTag } from 'astro/types'
|
||||
|
||||
// List group item (.list-group-item).
|
||||
// List group item (.list-group-item). With a title, a description or a named
|
||||
// slot it renders the slot layout: media, content, meta and actions in a row.
|
||||
|
||||
type PolymorphicProps = { as?: HTMLTag }
|
||||
|
||||
@@ -10,14 +11,52 @@ interface Props extends PolymorphicProps {
|
||||
action?: boolean
|
||||
active?: boolean
|
||||
disabled?: boolean
|
||||
/** first line of the content slot (.list-group-item-title) */
|
||||
title?: string | undefined
|
||||
/** second line of the content slot (.list-group-item-description) */
|
||||
description?: string | undefined
|
||||
/** keeps the actions slot visible in a hoverable list (.show) */
|
||||
showActions?: boolean
|
||||
class?: string
|
||||
/** remaining attributes (href, data-*, …) are forwarded to the element */
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
const { as: Element = 'div', action, active, disabled, class: className, ...rest }: Props = Astro.props
|
||||
const { as: Element = 'div', action, active, disabled, title, description, showActions, class: className, ...rest }: Props = Astro.props
|
||||
|
||||
const hasMedia = Astro.slots.has('media')
|
||||
const hasMeta = Astro.slots.has('meta')
|
||||
const hasActions = Astro.slots.has('actions')
|
||||
const hasSlots = Boolean(title || description || hasMedia || hasMeta || hasActions)
|
||||
---
|
||||
|
||||
<Element class:list={['list-group-item', action && 'list-group-item-action', active && 'active', disabled && 'disabled', className]} aria-disabled={disabled ? 'true' : undefined} tabindex={Element === 'a' && disabled ? -1 : undefined} {...rest}>
|
||||
<slot />
|
||||
{
|
||||
hasSlots ? (
|
||||
<>
|
||||
{hasMedia && (
|
||||
<div class="list-group-item-media">
|
||||
<slot name="media" />
|
||||
</div>
|
||||
)}
|
||||
<div class="list-group-item-content">
|
||||
{title && <div class="list-group-item-title">{title}</div>}
|
||||
{description && <div class="list-group-item-description">{description}</div>}
|
||||
<slot />
|
||||
</div>
|
||||
{hasMeta && (
|
||||
<div class="list-group-item-meta">
|
||||
<slot name="meta" />
|
||||
</div>
|
||||
)}
|
||||
{hasActions && (
|
||||
<div class:list={['list-group-item-actions', showActions && 'show']}>
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<slot />
|
||||
)
|
||||
}
|
||||
</Element>
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
interface Props {
|
||||
color?: string
|
||||
color?: string | undefined
|
||||
animated?: boolean
|
||||
/** accessible name (visually-hidden text inside the dot) */
|
||||
ariaLabel?: string
|
||||
|
||||
Reference in new issue
Block a user