Files

541 lines
20 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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" 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 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>
<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 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">
<UsersList hoverable />
</div>
<div class="col-md-6">
<UsersListHeaders />
</div>
<div class="col-md-6">
<UsersList2 />
</div>
</div>
</DefaultLayout>