Fix Prettier formatting regression and Astro type-check errors

`Render Modal as real markup` (#2742) reformatted ~140 files back to
tabs/semicolons, breaking the Prettier config added by the Astro
quality gates (#2749). Re-run prettier --write to restore it, and fix
the type errors it had been masking: HTMLElement casts and `this`
typing in ChangePasswordModalContent/ConfirmDeleteModalContent
scripts, plain-JS syntax in the inline-injected progress.astro script,
narrowed prop unions in Button/Check/Spinner, nullable icon svg casts
in Icons/Rating, and a duplicate firstLetters/invalid `placeholder`
attribute in Select.astro. Also fix shared/vitest.config.mts's stale
`astro/lib/**/*.test.ts` include glob left over from the shared source
restructure, which made `pnpm test` report zero test files.
This commit is contained in:
codecalm
2026-08-03 01:26:19 +02:00
parent 5246f0b6fd
commit f27c70ad38
166 changed files with 9678 additions and 10612 deletions
+21 -21
View File
@@ -1,30 +1,30 @@
---
import Button from '@ui/Button.astro';
import Avatar from '@ui/Avatar.astro';
import CardTitle from '@ui/CardTitle.astro';
import people from '@data/people.json';
import Button from '@ui/Button.astro'
import Avatar from '@ui/Avatar.astro'
import CardTitle from '@ui/CardTitle.astro'
import people from '@data/people.json'
const person = people[0];
const person = people[0]
---
<form class="card card-md" action="./" method="get" autocomplete="off" novalidate>
<div class="card-body text-center">
<div class="mb-4">
<CardTitle as="h2">Account Locked</CardTitle>
<p class="text-secondary">Please enter your password to unlock your account</p>
</div>
<div class="card-body text-center">
<div class="mb-4">
<CardTitle as="h2">Account Locked</CardTitle>
<p class="text-secondary">Please enter your password to unlock your account</p>
</div>
<div class="mb-4">
<Avatar person={person} size="xl" class="mb-3" />
<h3>{person.full_name}</h3>
</div>
<div class="mb-4">
<Avatar person={person} size="xl" class="mb-3" />
<h3>{person.full_name}</h3>
</div>
<div class="mb-4">
<input type="password" class="form-control" placeholder="Password&hellip;" />
</div>
<div class="mb-4">
<input type="password" class="form-control" placeholder="Password" />
</div>
<div>
<Button color="primary" block icon="lock-open" text="Unlock" />
</div>
</div>
<div>
<Button color="primary" block icon="lock-open" text="Unlock" />
</div>
</div>
</form>
@@ -1,15 +1,12 @@
---
import CardStamp from '@ui/CardStamp.astro';
import CardTitle from '@ui/CardTitle.astro';
import CardStamp from '@ui/CardStamp.astro'
import CardTitle from '@ui/CardTitle.astro'
---
<div class="card bg-primary text-primary-fg">
<CardStamp icon="star" color="white" textColor="primary" />
<div class="card-body">
<CardTitle>Card with background and icon</CardTitle>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Architecto at consectetur culpa ducimus eum fuga fugiat,
ipsa iusto, modi nostrum recusandae reiciendis saepe.
</p>
</div>
<CardStamp icon="star" color="white" textColor="primary" />
<div class="card-body">
<CardTitle>Card with background and icon</CardTitle>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Architecto at consectetur culpa ducimus eum fuga fugiat, ipsa iusto, modi nostrum recusandae reiciendis saepe.</p>
</div>
</div>
+19 -22
View File
@@ -1,32 +1,29 @@
---
import photos from '@data/photos.json';
import photos from '@data/photos.json'
interface Props {
title?: string;
right?: boolean;
imgId?: number;
title?: string
right?: boolean
imgId?: number
}
const { title, right, imgId = 1 } = Astro.props;
const { title, right, imgId = 1 } = Astro.props
const photo = (photos as { file: string; title: string }[])[imgId];
const imgClass = `w-100 h-100 object-cover ${right ? 'card-img-end' : 'card-img-start'}`;
const photo = (photos as { file: string; title: string }[])[imgId]
const imgClass = `w-100 h-100 object-cover ${right ? 'card-img-end' : 'card-img-start'}`
---
<div class="card">
<div class="row row-0">
<div class={`col-3${right ? ' order-md-last' : ''}`}>
<!-- Photo -->
<img src={`./static/photos/${photo.file}`} class={imgClass} alt={photo.title} />
</div>
<div class="col">
<div class="card-body">
{title && <h3 class="card-title" set:html={title} />}
<p class="text-secondary">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aperiam deleniti fugit incidunt, iste, itaque minima
neque pariatur perferendis sed suscipit velit vitae voluptatem.
</p>
</div>
</div>
</div>
<div class="row row-0">
<div class={`col-3${right ? ' order-md-last' : ''}`}>
<!-- Photo -->
<img src={`./static/photos/${photo.file}`} class={imgClass} alt={photo.title} />
</div>
<div class="col">
<div class="card-body">
{title && <h3 class="card-title" set:html={title} />}
<p class="text-secondary">Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aperiam deleniti fugit incidunt, iste, itaque minima neque pariatur perferendis sed suscipit velit vitae voluptatem.</p>
</div>
</div>
</div>
</div>
+6 -9
View File
@@ -1,14 +1,11 @@
---
import CardTitle from '@ui/CardTitle.astro';
import CardTitle from '@ui/CardTitle.astro'
---
<div class="card">
<div class="ribbon bg-red">NEW</div>
<div class="card-body">
<CardTitle>Card with text ribbon</CardTitle>
<p class="text-secondary">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Architecto at consectetur culpa ducimus eum fuga fugiat,
ipsa iusto, modi nostrum recusandae reiciendis saepe.
</p>
</div>
<div class="ribbon bg-red">NEW</div>
<div class="card-body">
<CardTitle>Card with text ribbon</CardTitle>
<p class="text-secondary">Lorem ipsum dolor sit amet, consectetur adipisicing elit. Architecto at consectetur culpa ducimus eum fuga fugiat, ipsa iusto, modi nostrum recusandae reiciendis saepe.</p>
</div>
</div>
+7 -10
View File
@@ -1,15 +1,12 @@
---
import Icon from '@ui/Icon.astro';
import CardTitle from '@ui/CardTitle.astro';
import Icon from '@ui/Icon.astro'
import CardTitle from '@ui/CardTitle.astro'
---
<div class="card">
<div class="ribbon ribbon-top bg-yellow"><Icon name="star" /></div>
<div class="card-body">
<CardTitle>Card with top ribbon</CardTitle>
<p class="text-secondary">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Architecto at consectetur culpa ducimus eum fuga fugiat,
ipsa iusto, modi nostrum recusandae reiciendis saepe.
</p>
</div>
<div class="ribbon ribbon-top bg-yellow"><Icon name="star" /></div>
<div class="card-body">
<CardTitle>Card with top ribbon</CardTitle>
<p class="text-secondary">Lorem ipsum dolor sit amet, consectetur adipisicing elit. Architecto at consectetur culpa ducimus eum fuga fugiat, ipsa iusto, modi nostrum recusandae reiciendis saepe.</p>
</div>
</div>
+27 -32
View File
@@ -1,33 +1,28 @@
---
// Tabs/content blocks are HTML strings (different order when `bottom`).
interface Props {
/** tabs-count — present but unused */
count?: number;
id?: string;
bottom?: boolean;
borderless?: boolean;
/** tabs-count — present but unused */
count?: number
id?: string
bottom?: boolean
borderless?: boolean
}
const { id = 'top', bottom, borderless } = Astro.props;
const { id = 'top', bottom, borderless } = Astro.props
const tabs = ['Activity', 'Profile', 'Settings'];
const tabs = ['Activity', 'Profile', 'Settings']
const tabsHtml = `
<!-- Cards navigation -->
<ul class="nav nav-tabs${bottom ? ' nav-tabs-bottom' : ''}">
${tabs
.map(
(tab, i) =>
` <li class="nav-item"><a href="#tab-${id}-${i + 1}" class="nav-link${i === 0 ? ' active' : ''}" data-bs-toggle="tab">${tab}</a></li>`,
)
.join('\n')}
</ul>`;
${tabs.map((tab, i) => ` <li class="nav-item"><a href="#tab-${id}-${i + 1}" class="nav-link${i === 0 ? ' active' : ''}" data-bs-toggle="tab">${tab}</a></li>`).join('\n')}
</ul>`
const tabsContentHtml = `
<div class="tab-content">
${tabs
.map(
(tab, i) => ` <!-- Content of card #${tab} -->
.map(
(tab, i) => ` <!-- Content of card #${tab} -->
<div id="tab-${id}-${i + 1}" class="card tab-pane${i === 0 ? ' active show' : ''}">
<div class="card-body">
<div class="card-title">${tab}</div>
@@ -36,24 +31,24 @@ ${tabs
</p>
</div>
</div>`,
)
.join('\n')}
</div>`;
)
.join('\n')}
</div>`
---
<!-- Cards with tabs component -->
<div class={`card-tabs${borderless ? ' border-0' : ''}`}>
{
bottom ? (
<Fragment>
<Fragment set:html={tabsContentHtml} />
<Fragment set:html={tabsHtml} />
</Fragment>
) : (
<Fragment>
<Fragment set:html={tabsHtml} />
<Fragment set:html={tabsContentHtml} />
</Fragment>
)
}
{
bottom ? (
<Fragment>
<Fragment set:html={tabsContentHtml} />
<Fragment set:html={tabsHtml} />
</Fragment>
) : (
<Fragment>
<Fragment set:html={tabsHtml} />
<Fragment set:html={tabsContentHtml} />
</Fragment>
)
}
</div>
+66 -89
View File
@@ -1,103 +1,80 @@
---
// Photos filtered to horizontal=true, then sliced by offset/limit.
import photos from '@data/photos.json';
import CardTitle from '@ui/CardTitle.astro';
import photos from '@data/photos.json'
import CardTitle from '@ui/CardTitle.astro'
interface Props {
id?: string;
title?: string;
indicators?: boolean;
indicatorsThumb?: boolean;
indicatorsThumbRatio?: boolean;
indicatorsDot?: boolean;
indicatorsVertical?: boolean;
controls?: boolean;
captions?: boolean;
offset?: number;
limit?: number;
fade?: boolean;
id?: string
title?: string
indicators?: boolean
indicatorsThumb?: boolean
indicatorsThumbRatio?: boolean
indicatorsDot?: boolean
indicatorsVertical?: boolean
controls?: boolean
captions?: boolean
offset?: number
limit?: number
fade?: boolean
}
const {
id: idProp,
title = 'Carousel',
indicators,
indicatorsThumb,
indicatorsThumbRatio,
indicatorsDot,
indicatorsVertical,
controls,
captions,
offset = 0,
limit = 5,
fade,
} = Astro.props;
const { id: idProp, title = 'Carousel', indicators, indicatorsThumb, indicatorsThumbRatio, indicatorsDot, indicatorsVertical, controls, captions, offset = 0, limit = 5, fade } = Astro.props
const carouselId = idProp ?? 'carousel';
const filteredPhotos = (photos as { file: string; horizontal?: boolean }[]).filter((p) => p.horizontal);
const slides = filteredPhotos.slice(offset, offset + limit);
const carouselId = idProp ?? 'carousel'
const filteredPhotos = (photos as { file: string; horizontal?: boolean }[]).filter((p) => p.horizontal)
const slides = filteredPhotos.slice(offset, offset + limit)
---
<div class="card">
<div class="card-header">
<CardTitle>{title}</CardTitle>
</div>
<div class="card-body">
<div id={`carousel-${carouselId}`} class={`carousel slide${fade ? ' carousel-fade' : ''}`} data-bs-ride="carousel">
{
indicators && (
<div
class={`carousel-indicators${indicatorsVertical ? ' carousel-indicators-vertical' : ''}${
indicatorsDot ? ' carousel-indicators-dot' : indicatorsThumb ? ' carousel-indicators-thumb' : ''
}`}
>
{slides.map((photo, i) => (
<button
type="button"
data-bs-target={`#carousel-${carouselId}`}
data-bs-slide-to={`${i}`}
class={`${indicatorsThumbRatio ? ' ratio ratio-4x3' : ''}${i === 0 ? ' active' : ''}`}
style={indicatorsThumb ? `background-image: url(./static/photos/${photo.file})` : undefined}
/>
))}
</div>
)
}
<div class="card-header">
<CardTitle>{title}</CardTitle>
</div>
<div class="card-body">
<div id={`carousel-${carouselId}`} class={`carousel slide${fade ? ' carousel-fade' : ''}`} data-bs-ride="carousel">
{
indicators && (
<div class={`carousel-indicators${indicatorsVertical ? ' carousel-indicators-vertical' : ''}${indicatorsDot ? ' carousel-indicators-dot' : indicatorsThumb ? ' carousel-indicators-thumb' : ''}`}>
{slides.map((photo, i) => (
<button type="button" data-bs-target={`#carousel-${carouselId}`} data-bs-slide-to={`${i}`} class={`${indicatorsThumbRatio ? ' ratio ratio-4x3' : ''}${i === 0 ? ' active' : ''}`} style={indicatorsThumb ? `background-image: url(./static/photos/${photo.file})` : undefined} />
))}
</div>
)
}
<div class="carousel-inner">
{
slides.map((photo, i) => (
<div class={`carousel-item${i === 0 ? ' active' : ''}`}>
<img class="d-block w-100" alt="" src={`./static/photos/${photo.file}`} />
<div class="carousel-inner">
{
slides.map((photo, i) => (
<div class={`carousel-item${i === 0 ? ' active' : ''}`}>
<img class="d-block w-100" alt="" src={`./static/photos/${photo.file}`} />
{captions && (
<Fragment>
<div class="carousel-caption-background d-none d-md-block" />
<div class="carousel-caption d-none d-md-block">
<h3>Slide label</h3>
<p>Nulla vitae elit libero, a pharetra augue mollis interdum.</p>
</div>
</Fragment>
)}
</div>
))
}
</div>
{captions && (
<Fragment>
<div class="carousel-caption-background d-none d-md-block" />
<div class="carousel-caption d-none d-md-block">
<h3>Slide label</h3>
<p>Nulla vitae elit libero, a pharetra augue mollis interdum.</p>
</div>
</Fragment>
)}
</div>
))
}
</div>
{
controls && (
<Fragment>
<a class="carousel-control-prev" href={`#carousel-${carouselId}`} role="button" data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true" />
<span class="visually-hidden">Previous</span>
</a>
<a class="carousel-control-next" href={`#carousel-${carouselId}`} role="button" data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true" />
<span class="visually-hidden">Next</span>
</a>
</Fragment>
)
}
</div>
</div>
{
controls && (
<Fragment>
<a class="carousel-control-prev" href={`#carousel-${carouselId}`} role="button" data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true" />
<span class="visually-hidden">Previous</span>
</a>
<a class="carousel-control-next" href={`#carousel-${carouselId}`} role="button" data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true" />
<span class="visually-hidden">Next</span>
</a>
</Fragment>
)
}
</div>
</div>
</div>
+6 -6
View File
@@ -1,6 +1,6 @@
---
// Raw CSS text inside .card-code (no highlight markup).
import CardTitle from '@ui/CardTitle.astro';
import CardTitle from '@ui/CardTitle.astro'
const code = `
.card-footer {
@@ -10,12 +10,12 @@ const code = `
border-radius: 0 0 1 2;
}
}
`;
`
---
<div class="card">
<div class="card-header">
<CardTitle>Card with code</CardTitle>
</div>
<div class="card-code">{code}</div>
<div class="card-header">
<CardTitle>Card with code</CardTitle>
</div>
<div class="card-code">{code}</div>
</div>
+41 -49
View File
@@ -1,58 +1,50 @@
---
import Button from '@ui/Button.astro';
import FormGroup from '@ui/FormGroup.astro';
import Button from '@ui/Button.astro'
import FormGroup from '@ui/FormGroup.astro'
const months = Array.from({ length: 12 }, (_, i) => i + 1);
const years = Array.from({ length: 11 }, (_, i) => 2020 + i);
const months = Array.from({ length: 12 }, (_, i) => i + 1)
const years = Array.from({ length: 11 }, (_, i) => 2020 + i)
---
<div class="card">
<div class="card-body">
<div class="mb-3">
<div class="form-label">Card number</div>
<input
type="text"
name="input-mask"
class="form-control"
data-mask="0000 0000 0000 0000"
data-mask-visible="true"
placeholder="0000 0000 0000 0000"
autocomplete="off"
/>
</div>
<div class="card-body">
<div class="mb-3">
<div class="form-label">Card number</div>
<input type="text" name="input-mask" class="form-control" data-mask="0000 0000 0000 0000" data-mask-visible="true" placeholder="0000 0000 0000 0000" autocomplete="off" />
</div>
<div class="mb-3">
<div class="form-label">Card name</div>
<input type="text" class="form-control" />
</div>
<div class="mb-3">
<div class="form-label">Card name</div>
<input type="text" class="form-control" />
</div>
<div class="row">
<div class="col-8">
<FormGroup label="Expiration date">
<div class="row g-2">
<div class="col">
<select class="form-select">
{months.map((month) => <option value={month}>{month}</option>)}
</select>
</div>
<div class="col">
<select class="form-select">
{years.map((year) => <option value={year}>{year}</option>)}
</select>
</div>
</div>
</FormGroup>
</div>
<div class="col">
<div class="mb-3">
<div class="form-label">CVV</div>
<input type="number" class="form-control" />
</div>
</div>
</div>
<div class="row">
<div class="col-8">
<FormGroup label="Expiration date">
<div class="row g-2">
<div class="col">
<select class="form-select">
{months.map((month) => <option value={month}>{month}</option>)}
</select>
</div>
<div class="col">
<select class="form-select">
{years.map((year) => <option value={year}>{year}</option>)}
</select>
</div>
</div>
</FormGroup>
</div>
<div class="col">
<div class="mb-3">
<div class="form-label">CVV</div>
<input type="number" class="form-control" />
</div>
</div>
</div>
<div class="mt-2">
<Button text="Pay now" color="primary" block />
</div>
</div>
<div class="mt-2">
<Button text="Pay now" color="primary" block />
</div>
</div>
</div>
+40 -47
View File
@@ -1,59 +1,52 @@
---
import Subheader from '@ui/Subheader.astro';
import Icon from '@ui/Icon.astro';
import Trending from '@ui/Trending.astro';
import Subheader from '@ui/Subheader.astro'
import Icon from '@ui/Icon.astro'
import Trending from '@ui/Trending.astro'
interface Props {
color?: string;
icon?: string;
/** lt — light avatar background (bg-{color}-lt) instead of bg-{color} text-white */
lt?: boolean;
title?: string;
description?: string;
/** change-value — passed to Trending (keeps its +/- sign) */
changeValue?: string;
/** change-value-unit — trending unit; defaults to '%' in Trending when unset */
changeValueUnit?: string;
class?: string;
color?: string
icon?: string
/** lt — light avatar background (bg-{color}-lt) instead of bg-{color} text-white */
lt?: boolean
title?: string
description?: string
/** change-value — passed to Trending (keeps its +/- sign) */
changeValue?: string
/** change-value-unit — trending unit; defaults to '%' in Trending when unset */
changeValueUnit?: string
class?: string
}
const {
color,
icon,
lt,
title = '1700',
description = 'Users',
changeValue,
changeValueUnit,
class: className,
} = Astro.props;
const { color, icon, lt, title = '1700', description = 'Users', changeValue, changeValueUnit, class: className } = Astro.props
const avatarClass = [color ? `bg-${color}${lt ? '-lt' : ' text-white'}` : '', 'avatar', 'avatar-square'];
const avatarClass = [color ? `bg-${color}${lt ? '-lt' : ' text-white'}` : '', 'avatar', 'avatar-square']
---
<div class:list={['card card-sm', className]}>
<div class="card-body p-3">
<div class="row align-items-center">
{
icon && (
<div class="col-auto">
<span class:list={avatarClass}><Icon name={icon} /></span>
</div>
)
}
<div class="card-body p-3">
<div class="row align-items-center">
{
icon && (
<div class="col-auto">
<span class:list={avatarClass}>
<Icon name={icon} />
</span>
</div>
)
}
<div class="col">
<Subheader>
{title}
</Subheader>
<div class="h3 m-0 p-0">
{description}
</div>
</div>
<div class="col">
<Subheader>
{title}
</Subheader>
<div class="h3 m-0 p-0">
{description}
</div>
</div>
<div class="col-auto">
<Trending value={changeValue as unknown as number} unit={changeValueUnit} />
</div>
</div>
</div>
<div class="col-auto">
<Trending value={changeValue as unknown as number} unit={changeValueUnit} />
</div>
</div>
</div>
</div>
+13 -13
View File
@@ -1,18 +1,18 @@
---
import AvatarList from '@ui/AvatarList.astro';
import Button from '@ui/Button.astro';
import CardTitle from '@ui/CardTitle.astro';
import AvatarList from '@ui/AvatarList.astro'
import Button from '@ui/Button.astro'
import CardTitle from '@ui/CardTitle.astro'
---
<div class="card card-lg card-dashed card-transparent">
<div class="card-body text-center">
<div class="mb-3">
<AvatarList stacked={true} />
</div>
<CardTitle>No Team Members</CardTitle>
<p class="text-secondary">Invite your team to<br />collaborate on this project.</p>
<div class="mt-4">
<Button text="Invite Members" color="primary" icon="plus" />
</div>
</div>
<div class="card-body text-center">
<div class="mb-3">
<AvatarList stacked={true} />
</div>
<CardTitle>No Team Members</CardTitle>
<p class="text-secondary">Invite your team to<br />collaborate on this project.</p>
<div class="mt-4">
<Button text="Invite Members" color="primary" icon="plus" />
</div>
</div>
</div>
@@ -1,5 +1,4 @@
---
import FormFooter from '@ui/FormFooter.astro'
import Button from '@ui/Button.astro'
import FormGroup from '@ui/FormGroup.astro'
+39 -39
View File
@@ -1,57 +1,57 @@
---
// `photo` and `index` are passed explicitly from the parent loop.
import Avatar from '@ui/Avatar.astro';
import Icon from '@ui/Icon.astro';
import { randomNumber, timeagoLabel } from '@shared/lib/pseudo-random';
import Avatar from '@ui/Avatar.astro'
import Icon from '@ui/Icon.astro'
import { randomNumber, timeagoLabel } from '@shared/lib/pseudo-random'
interface Photo {
file: string;
[key: string]: unknown;
file: string
[key: string]: unknown
}
interface Person {
full_name?: string;
photo?: string;
[key: string]: unknown;
full_name?: string
photo?: string
[key: string]: unknown
}
interface Props {
photo: Photo;
person: Person;
/** 1-based loop index — seeds the deterministic pseudo-random values */
index: number;
hideLikes?: boolean;
photo: Photo
person: Person
/** 1-based loop index — seeds the deterministic pseudo-random values */
index: number
hideLikes?: boolean
}
const { photo, person, index, hideLikes } = Astro.props;
const { photo, person, index, hideLikes } = Astro.props
// Heart filled when (index > 2 && index < 9) || index === 10.
const heartClass = (index > 2 && index < 9) || index === 10 ? 'icon-filled text-red' : undefined;
const heartClass = (index > 2 && index < 9) || index === 10 ? 'icon-filled text-red' : undefined
---
<div class="card card-sm">
<a href="#" class="d-block"><img src={`./static/photos/${photo.file}`} class="card-img-top" /></a>
<div class="card-body">
<div class="d-flex align-items-center">
<Avatar person={person} class="me-3 rounded" />
<div>
<div>{person.full_name}</div>
<div class="text-secondary">{timeagoLabel(index, 10)}</div>
</div>
{
!hideLikes && (
<div class="ms-auto">
<a href="#" class="text-secondary">
<Icon name="eye" />
{randomNumber(index, 300, 600)}
</a>
<a href="#" class="ms-3 text-secondary">
<Icon name="heart" class={heartClass} />
{randomNumber(index, 20, 100)}
</a>
</div>
)
}
</div>
</div>
<a href="#" class="d-block"><img src={`./static/photos/${photo.file}`} class="card-img-top" /></a>
<div class="card-body">
<div class="d-flex align-items-center">
<Avatar person={person} class="me-3 rounded" />
<div>
<div>{person.full_name}</div>
<div class="text-secondary">{timeagoLabel(index, 10)}</div>
</div>
{
!hideLikes && (
<div class="ms-auto">
<a href="#" class="text-secondary">
<Icon name="eye" />
{randomNumber(index, 300, 600)}
</a>
<a href="#" class="ms-3 text-secondary">
<Icon name="heart" class={heartClass} />
{randomNumber(index, 20, 100)}
</a>
</div>
)
}
</div>
</div>
</div>
+28 -39
View File
@@ -1,52 +1,41 @@
---
import Icon from '@ui/Icon.astro';
import icons from '@data/icons.json';
import Icon from '@ui/Icon.astro'
import icons from '@data/icons.json'
interface Props {
title?: string;
type?: 'outline' | 'filled';
title?: string
type?: 'outline' | 'filled'
}
const { title, type = 'outline' } = Astro.props;
const { title, type = 'outline' } = Astro.props
// Dev preview limits icons to 20 (see BaseLayout).
// The limit applies before the svg[type] filter — the "filled" card shows only filled icons among the first 20.
const limit = 20;
const limit = 20
const entries = Object.entries(icons as Record<string, { svg?: Record<string, string> }>).slice(
0,
limit,
);
const entries = Object.entries(icons as Record<string, { svg?: Record<string, string | null> }>).slice(0, limit)
---
<div class="card">
<div class="card-header">
<div class="card-title">{title}</div>
</div>
<div class="card-body p-0">
<div class="demo-icons-list-wrap">
<div class="demo-icons-list">
{
entries.map(
([iconName, icon]) =>
icon.svg?.[type] && (
<a
href={`https://tabler.io/icons/icon/${iconName}`}
target="_blank"
rel="noopener"
class="demo-icons-list-item"
title={iconName}
data-bs-toggle="tooltip"
data-bs-placement="top"
>
<Icon name={iconName} type={type} />
</a>
),
)
}
{/* 21 empty divs (flexbox filler) */}
{Array.from({ length: 21 }).map(() => <div />)}
</div>
</div>
</div>
<div class="card-header">
<div class="card-title">{title}</div>
</div>
<div class="card-body p-0">
<div class="demo-icons-list-wrap">
<div class="demo-icons-list">
{
entries.map(
([iconName, icon]) =>
icon.svg?.[type] && (
<a href={`https://tabler.io/icons/icon/${iconName}`} target="_blank" rel="noopener" class="demo-icons-list-item" title={iconName} data-bs-toggle="tooltip" data-bs-placement="top">
<Icon name={iconName} type={type} />
</a>
),
)
}
{/* 21 empty divs (flexbox filler) */}
{Array.from({ length: 21 }).map(() => <div />)}
</div>
</div>
</div>
</div>
@@ -1,4 +1,5 @@
---
---
<div class="card card-lg">
+35 -39
View File
@@ -1,54 +1,50 @@
---
// Feature names are a fixed list; the `features` string is split per-character into a 1/0 availability mask.
import Icon from '@ui/Icon.astro';
import Icon from '@ui/Icon.astro'
interface Props {
price?: string;
users?: string | number;
category?: string;
features?: string;
/** featured-color — ribbon + button color (e.g. "green") */
featuredColor?: string;
price?: string
users?: string | number
category?: string
features?: string
/** featured-color — ribbon + button color (e.g. "green") */
featuredColor?: string
}
const { price = '79', users = 10, category = 'Enterprise', features = '1000', featuredColor } = Astro.props;
const { price = '79', users = 10, category = 'Enterprise', features = '1000', featuredColor } = Astro.props
const featureNames = ['Sharing Tools', 'Design Tools', 'Private Messages', 'Twitter API'];
const availableFeatures = features.split('');
const featureNames = ['Sharing Tools', 'Design Tools', 'Private Messages', 'Twitter API']
const availableFeatures = features.split('')
---
<div class="card card-md">
{
featuredColor && (
<div class={`ribbon ribbon-top ribbon-bookmark bg-${featuredColor}`}>
<Icon name="star" />
</div>
)
}
{
featuredColor && (
<div class={`ribbon ribbon-top ribbon-bookmark bg-${featuredColor}`}>
<Icon name="star" />
</div>
)
}
<div class="card-body text-center">
<div class="text-uppercase text-secondary fw-medium">{category}</div>
<div class="card-body text-center">
<div class="text-uppercase text-secondary fw-medium">{category}</div>
<div class="display-5 fw-bold my-3">${price}</div>
<div class="display-5 fw-bold my-3">${price}</div>
<ul class="list-unstyled lh-lg">
<li><strong>{users}</strong> Users</li>
{
featureNames.map((feature, i) => (
<li>
{availableFeatures[i] === '1' ? (
<Icon name="check" class="me-1 text-success" />
) : (
<Icon name="x" class="me-1 text-danger" />
)}
{feature}
</li>
))
}
</ul>
<ul class="list-unstyled lh-lg">
<li><strong>{users}</strong> Users</li>
{
featureNames.map((feature, i) => (
<li>
{availableFeatures[i] === '1' ? <Icon name="check" class="me-1 text-success" /> : <Icon name="x" class="me-1 text-danger" />}
{feature}
</li>
))
}
</ul>
<div class="text-center mt-4">
<a href="#" class={`btn${featuredColor ? ` btn-${featuredColor}` : ''} w-100`}>Choose plan</a>
</div>
</div>
<div class="text-center mt-4">
<a href="#" class={`btn${featuredColor ? ` btn-${featuredColor}` : ''} w-100`}>Choose plan</a>
</div>
</div>
</div>
+24 -24
View File
@@ -1,38 +1,38 @@
---
// TODO: fallback to people[1] when person omitted — unused on card-gradients, so `person` is required here.
import Icon from '@ui/Icon.astro';
import Avatar from '@ui/Avatar.astro';
import ButtonList from '@ui/ButtonList.astro';
import Icon from '@ui/Icon.astro'
import Avatar from '@ui/Avatar.astro'
import ButtonList from '@ui/ButtonList.astro'
interface Person {
full_name?: string;
job_title?: string;
[key: string]: unknown;
full_name?: string
job_title?: string
[key: string]: unknown
}
interface Props {
person: Person;
color?: string;
person: Person
color?: string
}
const { person, color = 'yellow' } = Astro.props;
const { person, color = 'yellow' } = Astro.props
---
<div class={`card card-gradient card-gradient-${color}`}>
<div class="card-body text-center py-6">
<div class="position-absolute top-0 end-0 p-1">
<div class="btn btn-action"><Icon name="star" color="yellow" type="filled" /></div>
</div>
<div>
<Avatar size="2xl" person={person} shape="rounded-circle" />
</div>
<div class="h1 mt-4 mb-1">{person.full_name}</div>
<div class="text-secondary">{person.job_title}</div>
<div class="card-body text-center py-6">
<div class="position-absolute top-0 end-0 p-1">
<div class="btn btn-action"><Icon name="star" color="yellow" type="filled" /></div>
</div>
<div>
<Avatar size="2xl" person={person} shape="rounded-circle" />
</div>
<div class="h1 mt-4 mb-1">{person.full_name}</div>
<div class="text-secondary">{person.job_title}</div>
<ButtonList class="justify-content-center mt-3">
<a href="#" class="btn btn-icon btn-pill" title="Message" data-bs-toggle="tooltip" data-bs-placement="top"><Icon name="message" /></a>
<a href="#" class="btn btn-icon btn-pill" title="Phone" data-bs-toggle="tooltip" data-bs-placement="top"><Icon name="phone" /></a>
<a href="#" class="btn btn-icon btn-pill" title="Email" data-bs-toggle="tooltip" data-bs-placement="top"><Icon name="mail" /></a>
</ButtonList>
</div>
<ButtonList class="justify-content-center mt-3">
<a href="#" class="btn btn-icon btn-pill" title="Message" data-bs-toggle="tooltip" data-bs-placement="top"><Icon name="message" /></a>
<a href="#" class="btn btn-icon btn-pill" title="Phone" data-bs-toggle="tooltip" data-bs-placement="top"><Icon name="phone" /></a>
<a href="#" class="btn btn-icon btn-pill" title="Email" data-bs-toggle="tooltip" data-bs-placement="top"><Icon name="mail" /></a>
</ButtonList>
</div>
</div>
+36 -37
View File
@@ -1,49 +1,48 @@
---
// NOTE: widgets page passes `value=` but only `percentage` (default 20) drives the bar — `value` is accepted but ignored.
import Progress from '@ui/Progress.astro';
import AvatarList from '@ui/AvatarList.astro';
import Icon from '@ui/Icon.astro';
import CardTitle from '@ui/CardTitle.astro';
import Progress from '@ui/Progress.astro'
import AvatarList from '@ui/AvatarList.astro'
import Icon from '@ui/Icon.astro'
import CardTitle from '@ui/CardTitle.astro'
interface Props {
title?: string;
badge?: string;
offset?: number;
limit?: number;
percentage?: number | string;
percentageColor?: string;
due?: string;
/** unused — see note above */
value?: number | string;
title?: string
badge?: string
offset?: number
limit?: number
percentage?: number | string
percentageColor?: string
due?: string
/** unused — see note above */
value?: number | string
}
const {
title = 'Task Title',
badge,
offset = 40,
limit = 7,
percentage = 20,
percentageColor = 'green',
due = '2 days',
} = Astro.props;
const { title = 'Task Title', badge, offset = 40, limit = 7, percentage = 20, percentageColor = 'green', due = '2 days' } = Astro.props
---
<div class="card">
<Progress value={percentage} class="card-progress" color={percentageColor} />
<div class="card-body">
<CardTitle>
<a href="#">{title}</a>
{badge && <Fragment> <span class="badge ms-2">{badge}</span></Fragment>}
</CardTitle>
<Progress value={percentage} class="card-progress" color={percentageColor} />
<div class="card-body">
<CardTitle>
<a href="#">{title}</a>
{
badge && (
<Fragment>
{' '}
<span class="badge ms-2">{badge}</span>
</Fragment>
)
}
</CardTitle>
<AvatarList offset={offset} limit={limit} stacked class="mb-3" />
<AvatarList offset={offset} limit={limit} stacked class="mb-3" />
<div class="card-meta d-flex justify-content-between">
<div class="d-flex align-items-center">
<Icon name="check" class="me-2" />
<span>5/10</span>
</div>
<span>Due {due}</span>
</div>
</div>
<div class="card-meta d-flex justify-content-between">
<div class="d-flex align-items-center">
<Icon name="check" class="me-2" />
<span>5/10</span>
</div>
<span>Due {due}</span>
</div>
</div>
</div>
+37 -39
View File
@@ -1,53 +1,51 @@
---
// NOTE: `days-ago` param accepted but unused ("Updated 2 hours ago" is hardcoded).
import Progress from '@ui/Progress.astro';
import CardDropdown from '@ui/CardDropdown.astro';
import projects from '@data/projects.json';
import Progress from '@ui/Progress.astro'
import CardDropdown from '@ui/CardDropdown.astro'
import projects from '@data/projects.json'
interface Project {
title?: string;
image?: string;
[key: string]: unknown;
title?: string
image?: string
[key: string]: unknown
}
interface Props {
projectId?: number;
progress?: number;
daysAgo?: number;
projectId?: number
progress?: number
daysAgo?: number
}
const { projectId = 0, progress = 25 } = Astro.props;
const project = (projects as Project[])[projectId];
const { projectId = 0, progress = 25 } = Astro.props
const project = (projects as Project[])[projectId]
---
<div class="card">
<div class="card-body">
<div class="row align-items-center">
<div class="col-3">
<img src={project.image} alt={project.title} class="rounded" />
</div>
<div class="col">
<h3 class="card-title mb-1">
<a href="#" class="text-reset">{project.title}</a>
</h3>
<div class="text-secondary">
Updated 2 hours ago
</div>
<div class="card-body">
<div class="row align-items-center">
<div class="col-3">
<img src={project.image} alt={project.title} class="rounded" />
</div>
<div class="col">
<h3 class="card-title mb-1">
<a href="#" class="text-reset">{project.title}</a>
</h3>
<div class="text-secondary">Updated 2 hours ago</div>
<div class="mt-3">
<div class="row g-2 align-items-center">
<div class="col-auto">
{progress}%
</div>
<div class="col">
<Progress value={progress} size="sm" />
</div>
</div>
</div>
</div>
<div class="col-auto">
<CardDropdown />
</div>
</div>
</div>
<div class="mt-3">
<div class="row g-2 align-items-center">
<div class="col-auto">
{progress}%
</div>
<div class="col">
<Progress value={progress} size="sm" />
</div>
</div>
</div>
</div>
<div class="col-auto">
<CardDropdown />
</div>
</div>
</div>
</div>
+32 -32
View File
@@ -1,39 +1,39 @@
---
// Icon class is a literal template string (color token is not interpolated).
import CardActions from '@ui/CardActions.astro';
import Icon from '@ui/Icon.astro';
import CardTitle from '@ui/CardTitle.astro';
import crmDashboard from '@data/crm-dashboard.json';
import CardActions from '@ui/CardActions.astro'
import Icon from '@ui/Icon.astro'
import CardTitle from '@ui/CardTitle.astro'
import crmDashboard from '@data/crm-dashboard.json'
const activity = crmDashboard.recent_activity;
const activity = crmDashboard.recent_activity
---
<div class="card" style="height: 28rem">
<div class="card-header">
<CardTitle>{activity.title}</CardTitle>
<CardActions>
<a href="#" class="small fw-medium text-primary">{activity.view_all_text}</a>
</CardActions>
</div>
<div class="card-body card-body-scrollable card-body-scrollable-shadow">
<div class="divide-y">
{
activity.items.map((item) => (
<div class="py-3">
<div class="row align-items-center">
<div class="col-auto">
<span class={`avatar bg-${item.icon_color}-lt`}>
<Icon name={item.icon} class={'icon text-{{ item.icon_color }}'} />
</span>
</div>
<div class="col">
<div class="fw-medium">{item.text}</div>
<div class="small text-secondary">{item.time}</div>
</div>
</div>
</div>
))
}
</div>
</div>
<div class="card-header">
<CardTitle>{activity.title}</CardTitle>
<CardActions>
<a href="#" class="small fw-medium text-primary">{activity.view_all_text}</a>
</CardActions>
</div>
<div class="card-body card-body-scrollable card-body-scrollable-shadow">
<div class="divide-y">
{
activity.items.map((item) => (
<div class="py-3">
<div class="row align-items-center">
<div class="col-auto">
<span class={`avatar bg-${item.icon_color}-lt`}>
<Icon name={item.icon} class={'icon text-{{ item.icon_color }}'} />
</span>
</div>
<div class="col">
<div class="fw-medium">{item.text}</div>
<div class="small text-secondary">{item.time}</div>
</div>
</div>
</div>
))
}
</div>
</div>
</div>
-1
View File
@@ -1,5 +1,4 @@
---
import FormFooter from '@ui/FormFooter.astro'
import InputGroup from '@ui/InputGroup.astro'
import FormGroup from '@ui/FormGroup.astro'
+32 -33
View File
@@ -1,39 +1,38 @@
---
import { formatNumber } from '@shared/lib/string-format';
import CardTitle from '@ui/CardTitle.astro';
import { formatNumber } from '@shared/lib/string-format'
import CardTitle from '@ui/CardTitle.astro'
const services = 'Instagram:3550,Twitter:1798,Facebook:1245,TikTok:986,Pinterest:854,VK:650,Pinterest:420'
.split(',')
.map((service) => {
const [name, visitors] = service.split(':');
return { name, visitors: Number(visitors) };
});
const services = 'Instagram:3550,Twitter:1798,Facebook:1245,TikTok:986,Pinterest:854,VK:650,Pinterest:420'.split(',').map((service) => {
const [name, visitors] = service.split(':')
return { name, visitors: Number(visitors) }
})
---
<div class="card">
<div class="card-header">
<CardTitle>Social Media Traffic</CardTitle>
</div>
<table class="table card-table table-vcenter">
<thead>
<tr>
<th>Network</th>
<th colspan="2">Visitors</th>
</tr>
</thead>
<tbody>
{services.map((service) => (
<tr>
<td>{service.name}</td>
<td>{formatNumber(service.visitors)}</td>
<td class="w-50">
<div class="progress progress-xs">
<div class="progress-bar bg-primary" style={`width: ${(service.visitors / 5000.0) * 100}%`}></div>
</div>
</td>
</tr>
))}
</tbody>
</table>
<div class="card-header">
<CardTitle>Social Media Traffic</CardTitle>
</div>
<table class="table card-table table-vcenter">
<thead>
<tr>
<th>Network</th>
<th colspan="2">Visitors</th>
</tr>
</thead>
<tbody>
{
services.map((service) => (
<tr>
<td>{service.name}</td>
<td>{formatNumber(service.visitors)}</td>
<td class="w-50">
<div class="progress progress-xs">
<div class="progress-bar bg-primary" style={`width: ${(service.visitors / 5000.0) * 100}%`} />
</div>
</td>
</tr>
))
}
</tbody>
</table>
</div>
+94 -83
View File
@@ -1,96 +1,107 @@
---
import Icon from '@ui/Icon.astro';
import DropdownMenu from '@ui/DropdownMenu.astro';
import Icon from '@ui/Icon.astro'
import DropdownMenu from '@ui/DropdownMenu.astro'
interface Props {
id: string;
icons?: boolean;
/** hide-text param; not used by current call sites */
hideText?: boolean;
reverse?: boolean;
justified?: boolean;
activity?: boolean;
disabled?: boolean;
dropdown?: boolean;
settings?: boolean;
animation?: boolean;
id: string
icons?: boolean
/** hide-text param; not used by current call sites */
hideText?: boolean
reverse?: boolean
justified?: boolean
activity?: boolean
disabled?: boolean
dropdown?: boolean
settings?: boolean
animation?: boolean
}
const {
id,
icons,
hideText,
reverse = false,
justified,
activity,
disabled,
dropdown,
settings,
animation,
} = Astro.props;
const { id, icons, hideText, reverse = false, justified, activity, disabled, dropdown, settings, animation } = Astro.props
const iconClass = hideText ? undefined : 'me-2';
const navClass = `nav nav-tabs card-header-tabs${reverse ? ' flex-row-reverse' : ''}${justified ? ' nav-fill' : ''}`;
const paneClass = `tab-pane${animation ? ' fade' : ''}`;
const iconClass = hideText ? undefined : 'me-2'
const navClass = `nav nav-tabs card-header-tabs${reverse ? ' flex-row-reverse' : ''}${justified ? ' nav-fill' : ''}`
const paneClass = `tab-pane${animation ? ' fade' : ''}`
---
<div class="card">
<div class="card-header">
<ul class={navClass} data-bs-toggle="tabs">
<li class="nav-item">
<a href={`#tabs-home-${id}`} class="nav-link active" data-bs-toggle="tab">{icons && <Icon name="home" class={iconClass} />}{!hideText && 'Home'}</a>
</li>
<li class="nav-item">
<a href={`#tabs-profile-${id}`} class="nav-link" data-bs-toggle="tab">{icons && <Icon name="user" class={iconClass} />}{!hideText && 'Profile'}</a>
</li>
<div class="card-header">
<ul class={navClass} data-bs-toggle="tabs">
<li class="nav-item">
<a href={`#tabs-home-${id}`} class="nav-link active" data-bs-toggle="tab">{icons && <Icon name="home" class={iconClass} />}{!hideText && 'Home'}</a>
</li>
<li class="nav-item">
<a href={`#tabs-profile-${id}`} class="nav-link" data-bs-toggle="tab">{icons && <Icon name="user" class={iconClass} />}{!hideText && 'Profile'}</a>
</li>
{activity && (
<li class="nav-item">
<a href={`#tabs-activity-${id}`} class="nav-link" data-bs-toggle="tab">{icons && <Icon name="activity" class={iconClass} />}{!hideText && 'Activity'}</a>
</li>
)}
{
activity && (
<li class="nav-item">
<a href={`#tabs-activity-${id}`} class="nav-link" data-bs-toggle="tab">
{icons && <Icon name="activity" class={iconClass} />}
{!hideText && 'Activity'}
</a>
</li>
)
}
{disabled && (
<li class="nav-item">
<a href={`#tabs-activity-${id}`} class="nav-link disabled" data-bs-toggle="tab">{icons && <Icon name="x" class={iconClass} />}{!hideText && 'Disabled'}</a>
</li>
)}
{
disabled && (
<li class="nav-item">
<a href={`#tabs-activity-${id}`} class="nav-link disabled" data-bs-toggle="tab">
{icons && <Icon name="x" class={iconClass} />}
{!hideText && 'Disabled'}
</a>
</li>
)
}
{dropdown && (
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Dropdown</a>
<DropdownMenu />
</li>
)}
{settings && (
<li class={`nav-item ${reverse ? 'me-auto' : 'ms-auto'}`}>
<a href={`#tabs-settings-${id}`} class="nav-link" title="Settings" data-bs-toggle="tab"><Icon name="settings" /></a>
</li>
)}
</ul>
</div>
<div class="card-body">
<div class="tab-content">
<div class={`${paneClass} active show`} id={`tabs-home-${id}`}>
<h4>Home tab</h4>
<div>Cursus turpis vestibulum, dui in pharetra vulputate id sed non turpis ultricies fringilla at sed facilisis lacus pellentesque purus nibh</div>
</div>
<div class={paneClass} id={`tabs-profile-${id}`}>
<h4>Profile tab</h4>
<div>Fringilla egestas nunc quis tellus diam rhoncus ultricies tristique enim at diam, sem nunc amet, pellentesque id egestas velit sed</div>
</div>
{settings && (
<div class={paneClass} id={`tabs-settings-${id}`}>
<h4>Settings tab</h4>
<div>Donec ac vitae diam amet vel leo egestas consequat rhoncus in luctus amet, facilisi sit mauris accumsan nibh habitant senectus</div>
</div>
)}
{activity && (
<div class={paneClass} id={`tabs-activity-${id}`}>
<h4>Activity tab</h4>
<div>Donec ac vitae diam amet vel leo egestas consequat rhoncus in luctus amet, facilisi sit mauris accumsan nibh habitant senectus</div>
</div>
)}
</div>
</div>
{
dropdown && (
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
Dropdown
</a>
<DropdownMenu />
</li>
)
}
{
settings && (
<li class={`nav-item ${reverse ? 'me-auto' : 'ms-auto'}`}>
<a href={`#tabs-settings-${id}`} class="nav-link" title="Settings" data-bs-toggle="tab">
<Icon name="settings" />
</a>
</li>
)
}
</ul>
</div>
<div class="card-body">
<div class="tab-content">
<div class={`${paneClass} active show`} id={`tabs-home-${id}`}>
<h4>Home tab</h4>
<div>Cursus turpis vestibulum, dui in pharetra vulputate id sed non turpis ultricies fringilla at sed facilisis lacus pellentesque purus nibh</div>
</div>
<div class={paneClass} id={`tabs-profile-${id}`}>
<h4>Profile tab</h4>
<div>Fringilla egestas nunc quis tellus diam rhoncus ultricies tristique enim at diam, sem nunc amet, pellentesque id egestas velit sed</div>
</div>
{
settings && (
<div class={paneClass} id={`tabs-settings-${id}`}>
<h4>Settings tab</h4>
<div>Donec ac vitae diam amet vel leo egestas consequat rhoncus in luctus amet, facilisi sit mauris accumsan nibh habitant senectus</div>
</div>
)
}
{
activity && (
<div class={paneClass} id={`tabs-activity-${id}`}>
<h4>Activity tab</h4>
<div>Donec ac vitae diam amet vel leo egestas consequat rhoncus in luctus amet, facilisi sit mauris accumsan nibh habitant senectus</div>
</div>
)
}
</div>
</div>
</div>
+21 -24
View File
@@ -2,41 +2,38 @@
// person = people[person-id]; the cover photo is photos[person-id].file.
// Base card-cover already carries `card-cover-blurred`; `blurred` adds it a 2nd time
// Base card-cover already carries `card-cover-blurred`; `blurred` adds it a second time.
import Avatar from '@ui/Avatar.astro';
import people from '@data/people.json';
import photos from '@data/photos.json';
import Avatar from '@ui/Avatar.astro'
import people from '@data/people.json'
import photos from '@data/photos.json'
interface Person {
full_name?: string;
job_title?: string;
photo?: string;
[key: string]: unknown;
full_name?: string
job_title?: string
photo?: string
[key: string]: unknown
}
interface Photo {
file?: string;
[key: string]: unknown;
file?: string
[key: string]: unknown
}
interface Props {
personId?: number;
blurred?: boolean;
personId?: number
blurred?: boolean
}
const { personId = 0, blurred } = Astro.props;
const person = (people as Person[])[personId];
const photo = (photos as Photo[])[personId];
const { personId = 0, blurred } = Astro.props
const person = (people as Person[])[personId]
const photo = (photos as Photo[])[personId]
---
<a class="card card-link" href="#">
<div
class={`card-cover card-cover-blurred text-center${blurred ? ' card-cover-blurred' : ''}`}
style={`background-image: url(./static/photos/${photo.file})`}
>
<Avatar size="xl" person={person} thumb />
</div>
<div class="card-body text-center">
<div class="card-title mb-1">{person.full_name}</div>
<div class="text-secondary">{person.job_title}</div>
</div>
<div class={`card-cover card-cover-blurred text-center${blurred ? ' card-cover-blurred' : ''}`} style={`background-image: url(./static/photos/${photo.file})`}>
<Avatar size="xl" person={person} thumb />
</div>
<div class="card-body text-center">
<div class="card-title mb-1">{person.full_name}</div>
<div class="text-secondary">{person.job_title}</div>
</div>
</a>
+17 -17
View File
@@ -1,29 +1,29 @@
---
import Avatar from '@ui/Avatar.astro';
import people from '@data/people.json';
import Avatar from '@ui/Avatar.astro'
import people from '@data/people.json'
interface Person {
full_name?: string;
job_title?: string;
photo?: string;
[key: string]: unknown;
full_name?: string
job_title?: string
photo?: string
[key: string]: unknown
}
interface Props {
personId?: number;
personId?: number
}
const { personId = 0 } = Astro.props;
const person = (people as Person[])[personId];
const { personId = 0 } = Astro.props
const person = (people as Person[])[personId]
---
<div class="card">
<div class="card-body text-center">
<div class="mb-3">
<Avatar size="xl" person={person} />
</div>
<div class="card-title mb-1">{person.full_name}</div>
<div class="text-secondary">{person.job_title}</div>
</div>
<a href="#" class="card-btn">View full profile</a>
<div class="card-body text-center">
<div class="mb-3">
<Avatar size="xl" person={person} />
</div>
<div class="card-title mb-1">{person.full_name}</div>
<div class="text-secondary">{person.job_title}</div>
</div>
<a href="#" class="card-btn">View full profile</a>
</div>
+42 -42
View File
@@ -1,55 +1,55 @@
---
// person = people[person-id - 1] (default person-id = 25 → people[24]).
import Icon from '@ui/Icon.astro';
import Flag from '@ui/Flag.astro';
import people from '@data/people.json';
import Icon from '@ui/Icon.astro'
import Flag from '@ui/Flag.astro'
import people from '@data/people.json'
interface Person {
university?: string;
company?: string;
city?: string;
country?: string;
country_code?: string;
birth_date?: string;
time_zone?: string;
[key: string]: unknown;
university?: string
company?: string
city?: string
country?: string
country_code?: string
birth_date?: string
time_zone?: string
[key: string]: unknown
}
interface Props {
title?: string;
personId?: number;
title?: string
personId?: number
}
const { title = 'Basic info', personId = 25 } = Astro.props;
const person = (people as Person[])[personId - 1];
const { title = 'Basic info', personId = 25 } = Astro.props
const person = (people as Person[])[personId - 1]
---
<div class="card">
<div class="card-body">
<div class="card-title">{title}</div>
<div class="mb-2">
<Icon name="book" class="me-2 text-secondary" />
Went to: <strong>{person.university}</strong>
</div>
<div class="mb-2">
<Icon name="briefcase" class="me-2 text-secondary" />
Worked at: <strong>{person.company}</strong>
</div>
<div class="mb-2">
<Icon name="home" class="me-2 text-secondary" />
Lives in: <strong>{person.city}, {person.country}</strong>
</div>
<div class="mb-2">
<Icon name="map-pin" class="me-2 text-secondary" />
From: <strong><Flag size="xs" flag={person.country_code} /> {person.country}</strong>
</div>
<div class="mb-2">
<Icon name="calendar" class="me-2 text-secondary" />
Birth date: <strong>{person.birth_date}</strong>
</div>
<div>
<Icon name="clock" class="me-2 text-secondary" />
Time zone: <strong>{person.time_zone}</strong>
</div>
</div>
<div class="card-body">
<div class="card-title">{title}</div>
<div class="mb-2">
<Icon name="book" class="me-2 text-secondary" />
Went to: <strong>{person.university}</strong>
</div>
<div class="mb-2">
<Icon name="briefcase" class="me-2 text-secondary" />
Worked at: <strong>{person.company}</strong>
</div>
<div class="mb-2">
<Icon name="home" class="me-2 text-secondary" />
Lives in: <strong>{person.city}, {person.country}</strong>
</div>
<div class="mb-2">
<Icon name="map-pin" class="me-2 text-secondary" />
From: <strong><Flag size="xs" flag={person.country_code} /> {person.country}</strong>
</div>
<div class="mb-2">
<Icon name="calendar" class="me-2 text-secondary" />
Birth date: <strong>{person.birth_date}</strong>
</div>
<div>
<Icon name="clock" class="me-2 text-secondary" />
Time zone: <strong>{person.time_zone}</strong>
</div>
</div>
</div>
+81 -87
View File
@@ -1,103 +1,97 @@
---
import Avatar from '@ui/Avatar.astro';
import Icon from '@ui/Icon.astro';
import ListGroup from '@ui/ListGroup.astro';
import ListGroupItem from '@ui/ListGroupItem.astro';
import CardTitle from '@ui/CardTitle.astro';
import people from '@data/people.json';
import commits from '@data/commits.json';
import { randomNumber } from '@shared/lib/pseudo-random';
import Avatar from '@ui/Avatar.astro'
import Icon from '@ui/Icon.astro'
import ListGroup from '@ui/ListGroup.astro'
import ListGroupItem from '@ui/ListGroupItem.astro'
import CardTitle from '@ui/CardTitle.astro'
import people from '@data/people.json'
import commits from '@data/commits.json'
import { randomNumber } from '@shared/lib/pseudo-random'
interface Person {
full_name?: string;
photo?: string;
last_name?: string;
[key: string]: unknown;
full_name?: string
photo?: string
last_name?: string
[key: string]: unknown
}
interface Props {
limit?: number;
offset?: number;
hoverable?: boolean;
checkbox?: boolean;
/** checked-ids — 1-based indices, e.g. [2, 5, 8] */
checkedIds?: number[];
title?: string;
class?: string;
/**
* Inert. The lists.html Contacts card passes `hover=true`, but
* users-list.html never reads it — only `hoverable` toggles the star column.
* Declared for call-site compatibility.
*/
hover?: boolean;
limit?: number
offset?: number
hoverable?: boolean
checkbox?: boolean
/** checked-ids — 1-based indices, e.g. [2, 5, 8] */
checkedIds?: number[]
title?: string
class?: string
/**
* Inert. The lists.html Contacts card passes `hover=true`, but
* users-list.html never reads it — only `hoverable` toggles the star column.
* Declared for call-site compatibility.
*/
hover?: boolean
}
const {
limit = 8,
offset = 0,
hoverable = false,
checkbox,
checkedIds,
title = 'Last commits',
class: className,
} = Astro.props;
const { limit = 8, offset = 0, hoverable = false, checkbox, checkedIds, title = 'Last commits', class: className } = Astro.props
const colors = ['green', 'red', 'yellow', 'x', 'x'];
const commitList = commits as { description: string }[];
const colors = ['green', 'red', 'yellow', 'x', 'x']
const commitList = commits as { description: string }[]
const rows = (people as Person[]).slice(offset, offset + limit).map((person, idx) => {
const index = idx + 1; // forloop.index (1-based)
const color = colors[randomNumber(index + 5, 0, colors.length - 1)];
const checked = checkedIds?.includes(index) ?? false;
const i = index + offset;
return {
person,
color,
checked,
description: commitList[i]?.description,
starColor: checked ? 'text-yellow' : 'text-secondary',
};
});
const index = idx + 1 // forloop.index (1-based)
const color = colors[randomNumber(index + 5, 0, colors.length - 1)]
const checked = checkedIds?.includes(index) ?? false
const i = index + offset
return {
person,
color,
checked,
description: commitList[i]?.description,
starColor: checked ? 'text-yellow' : 'text-secondary',
}
})
---
<div class={`card${className ? ` ${className}` : ''}`}>
<div class="card-header">
<CardTitle>{title}</CardTitle>
</div>
<ListGroup flush hoverable={hoverable}>
{
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="#">
<Avatar person={row.person} />
</a>
</div>
<div class="card-header">
<CardTitle>{title}</CardTitle>
</div>
<ListGroup flush hoverable={hoverable}>
{
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="#">
<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' : ''}`}>
<Icon name="star" class={row.starColor} />
</a>
</div>
)}
</div>
</ListGroupItem>
))
}
</ListGroup>
<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' : ''}`}>
<Icon name="star" class={row.starColor} />
</a>
</div>
)}
</div>
</ListGroupItem>
))
}
</ListGroup>
</div>
+44 -42
View File
@@ -1,57 +1,59 @@
---
import Avatar from '@ui/Avatar.astro';
import CardTitle from '@ui/CardTitle.astro';
import people from '@data/people.json';
import { randomNumber, timeagoLabel } from '@shared/lib/pseudo-random';
import Avatar from '@ui/Avatar.astro'
import CardTitle from '@ui/CardTitle.astro'
import people from '@data/people.json'
import { randomNumber, timeagoLabel } from '@shared/lib/pseudo-random'
interface Person {
full_name?: string;
photo?: string;
[key: string]: unknown;
full_name?: string
photo?: string
[key: string]: unknown
}
interface Props {
limit?: number;
offset?: number;
title?: string;
class?: string;
limit?: number
offset?: number
title?: string
class?: string
}
const { limit = 10, offset = 0, title = 'Top users', class: className } = Astro.props;
const { limit = 10, offset = 0, title = 'Top users', class: className } = Astro.props
const colors = 'green,red,yellow,x,x'.split(',');
const colors = 'green,red,yellow,x,x'.split(',')
const rows = (people as Person[]).slice(offset, offset + limit).map((person, idx) => {
const index = idx + 1; // forloop.index (1-based)
return {
person,
status: colors[randomNumber(index + 5, 0, colors.length - 1)],
timeago: timeagoLabel(index, 6),
};
});
const index = idx + 1 // forloop.index (1-based)
return {
person,
status: colors[randomNumber(index + 5, 0, colors.length - 1)],
timeago: timeagoLabel(index, 6),
}
})
---
<div class={`card${className ? ` ${className}` : ''}`}>
<div class="card-header">
<CardTitle>{title}</CardTitle>
</div>
<div class="card-body">
<div class="row g-3">
{
rows.map((row) => (
<div class="col-6">
<div class="row g-3 align-items-center">
<a href="#" class="col-auto">
<Avatar person={row.person} status={row.status} />
</a>
<div class="col text-truncate">
<a href="#" class="text-reset d-block text-truncate">{row.person.full_name}</a>
<div class="text-secondary text-truncate mt-n1">{row.timeago}</div>
</div>
</div>
</div>
))
}
</div>
</div>
<div class="card-header">
<CardTitle>{title}</CardTitle>
</div>
<div class="card-body">
<div class="row g-3">
{
rows.map((row) => (
<div class="col-6">
<div class="row g-3 align-items-center">
<a href="#" class="col-auto">
<Avatar person={row.person} status={row.status} />
</a>
<div class="col text-truncate">
<a href="#" class="text-reset d-block text-truncate">
{row.person.full_name}
</a>
<div class="text-secondary text-truncate mt-n1">{row.timeago}</div>
</div>
</div>
</div>
))
}
</div>
</div>
</div>
+53 -51
View File
@@ -1,70 +1,72 @@
---
import Avatar from '@ui/Avatar.astro';
import ListGroup from '@ui/ListGroup.astro';
import ListGroupItem from '@ui/ListGroupItem.astro';
import ListGroupHeader from '@ui/ListGroupHeader.astro';
import CardTitle from '@ui/CardTitle.astro';
import people from '@data/people.json';
import commits from '@data/commits.json';
import { sortBy } from '@shared/lib/string-format';
import Avatar from '@ui/Avatar.astro'
import ListGroup from '@ui/ListGroup.astro'
import ListGroupItem from '@ui/ListGroupItem.astro'
import ListGroupHeader from '@ui/ListGroupHeader.astro'
import CardTitle from '@ui/CardTitle.astro'
import people from '@data/people.json'
import commits from '@data/commits.json'
import { sortBy } from '@shared/lib/string-format'
interface Person {
full_name?: string;
photo?: string;
last_name?: string;
[key: string]: unknown;
full_name?: string
photo?: string
last_name?: string
[key: string]: unknown
}
interface Props {
title?: string;
title?: string
}
const { title = 'People' } = Astro.props;
const { title = 'People' } = Astro.props
const commitList = commits as { description: string }[];
const commitList = commits as { description: string }[]
// Sort by last_name (case-sensitive).
const sorted = sortBy(people as Person[], (p) => p.last_name ?? '');
const sorted = sortBy(people as Person[], (p) => p.last_name ?? '')
// offset is unset in the include, so `forloop.index | plus: offset` == forloop.index.
let prevLetter = '';
let prevLetter = ''
const rows = sorted.map((person, idx) => {
const index = idx + 1; // forloop.index (1-based)
const firstLetter = (person.last_name ?? '').slice(0, 1);
let header: string | null = null;
if (prevLetter !== firstLetter) {
prevLetter = firstLetter;
header = firstLetter;
}
return { person, header, description: commitList[index]?.description };
});
const index = idx + 1 // forloop.index (1-based)
const firstLetter = (person.last_name ?? '').slice(0, 1)
let header: string | null = null
if (prevLetter !== firstLetter) {
prevLetter = firstLetter
header = firstLetter
}
return { person, header, description: commitList[index]?.description }
})
---
<div class="card">
<div class="card-header">
<CardTitle>{title}</CardTitle>
</div>
<div class="card-header">
<CardTitle>{title}</CardTitle>
</div>
<ListGroup flush class="overflow-auto" style="max-height: 35rem">
{
rows.map((row) => (
<>
{row.header && <ListGroupHeader class="sticky-top">{row.header}</ListGroupHeader>}
<ListGroupItem>
<div class="row">
<div class="col-auto">
<a href="#">
<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>
</ListGroupItem>
</>
))
}
</ListGroup>
<ListGroup flush class="overflow-auto" style="max-height: 35rem">
{
rows.map((row) => (
<>
{row.header && <ListGroupHeader class="sticky-top">{row.header}</ListGroupHeader>}
<ListGroupItem>
<div class="row">
<div class="col-auto">
<a href="#">
<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>
</ListGroupItem>
</>
))
}
</ListGroup>
</div>
@@ -1,18 +1,18 @@
---
// legend=true at call site is inert — Chart reads legend from chart data, not props.
import CardTitle from '@ui/CardTitle.astro';
import Chart from '@ui/Chart.astro';
import DropdownDays from '@ui/DropdownDays.astro';
import CardTitle from '@ui/CardTitle.astro'
import Chart from '@ui/Chart.astro'
import DropdownDays from '@ui/DropdownDays.astro'
---
<div class="card">
<div class="card-body">
<div class="d-flex">
<CardTitle>Social referrals</CardTitle>
<div class="ms-auto">
<DropdownDays id="social-referrals" label="Select time range for sales data" />
</div>
</div>
<Chart chartId="social-referrals" />
</div>
<div class="card-body">
<div class="d-flex">
<CardTitle>Social referrals</CardTitle>
<div class="ms-auto">
<DropdownDays id="social-referrals" label="Select time range for sales data" />
</div>
</div>
<Chart chartId="social-referrals" />
</div>
</div>
+46 -48
View File
@@ -1,55 +1,53 @@
---
// Switch-icon heart variant inlined: icon-b="heart", icon-b-class="icon-filled", icon-a-color="muted", icon-b-color="red".
import Icon from '@ui/Icon.astro';
import DropdownMenu from '@ui/DropdownMenu.astro';
import ListGroup from '@ui/ListGroup.astro';
import ListGroupItem from '@ui/ListGroupItem.astro';
import tracks from '@data/tracks.json';
import { millisecondsToMinutes } from '@shared/lib/string-format';
import Icon from '@ui/Icon.astro'
import DropdownMenu from '@ui/DropdownMenu.astro'
import ListGroup from '@ui/ListGroup.astro'
import ListGroupItem from '@ui/ListGroupItem.astro'
import tracks from '@data/tracks.json'
import { millisecondsToMinutes } from '@shared/lib/string-format'
const items = tracks.slice(0, 12);
const items = tracks.slice(0, 12)
---
<div class="card">
<ListGroup class="card-list-group">
{items.map((track, index) => (
<ListGroupItem>
<div class="row g-2 align-items-center">
<div class="col-auto fs-3">
{index + 1}
</div>
<div class="col-auto">
<img src={`./static/tracks/${track.album.images[1].path}`} class="rounded" alt={track.name} width="40" height="40" />
</div>
<div class="col">
{track.name}
<div class="text-secondary">
{track.artists.map((artist) => artist.name).join(', ')}
</div>
</div>
<div class="col-auto text-secondary">
{millisecondsToMinutes(track.duration_ms)}
</div>
<div class="col-auto">
<a href="#" class="link-secondary">
<button class="switch-icon" data-bs-toggle="switch-icon">
<span class="switch-icon-a text-muted">
<Icon name="heart" />
</span>
<span class="switch-icon-b text-red">
<Icon name="heart" class="icon-filled" />
</span>
</button>
</a>
</div>
<div class="col-auto lh-1">
<div class="dropdown">
<a href="#" class="link-secondary" data-bs-toggle="dropdown"><Icon name="dots" /></a>
<DropdownMenu right />
</div>
</div>
</div>
</ListGroupItem>
))}
</ListGroup>
<ListGroup class="card-list-group">
{
items.map((track, index) => (
<ListGroupItem>
<div class="row g-2 align-items-center">
<div class="col-auto fs-3">{index + 1}</div>
<div class="col-auto">
<img src={`./static/tracks/${track.album.images[1].path}`} class="rounded" alt={track.name} width="40" height="40" />
</div>
<div class="col">
{track.name}
<div class="text-secondary">{track.artists.map((artist) => artist.name).join(', ')}</div>
</div>
<div class="col-auto text-secondary">{millisecondsToMinutes(track.duration_ms)}</div>
<div class="col-auto">
<a href="#" class="link-secondary">
<button class="switch-icon" data-bs-toggle="switch-icon">
<span class="switch-icon-a text-muted">
<Icon name="heart" />
</span>
<span class="switch-icon-b text-red">
<Icon name="heart" class="icon-filled" />
</span>
</button>
</a>
</div>
<div class="col-auto lh-1">
<div class="dropdown">
<a href="#" class="link-secondary" data-bs-toggle="dropdown">
<Icon name="dots" />
</a>
<DropdownMenu right />
</div>
</div>
</div>
</ListGroupItem>
))
}
</ListGroup>
</div>
@@ -1,4 +1,5 @@
---
---
<div class="card placeholder-glow">
@@ -1,4 +1,5 @@
---
---
<div class="card">
@@ -1,4 +1,5 @@
---
---
<div class="card">
@@ -1,4 +1,5 @@
---
---
<div class="card">
@@ -1,4 +1,5 @@
---
---
<div class="card">
+24 -24
View File
@@ -1,30 +1,30 @@
---
import ListGroup from '@ui/ListGroup.astro';
import ListGroupItem from '@ui/ListGroupItem.astro';
import ListGroup from '@ui/ListGroup.astro'
import ListGroupItem from '@ui/ListGroupItem.astro'
const items = [1, 2, 3, 4];
const items = [1, 2, 3, 4]
---
<div class="card">
<ListGroup as="ul" flush class="placeholder-glow">
{
items.map(() => (
<ListGroupItem as="li">
<div class="row align-items-center">
<div class="col-auto">
<div class="avatar avatar-rounded placeholder" />
</div>
<div class="col-7">
<div class="placeholder placeholder-xs col-9" />
<div class="placeholder placeholder-xs col-7" />
</div>
<div class="col-2 ms-auto text-end">
<div class="placeholder placeholder-xs col-8" />
<div class="placeholder placeholder-xs col-10" />
</div>
</div>
</ListGroupItem>
))
}
</ListGroup>
<ListGroup as="ul" flush class="placeholder-glow">
{
items.map(() => (
<ListGroupItem as="li">
<div class="row align-items-center">
<div class="col-auto">
<div class="avatar avatar-rounded placeholder" />
</div>
<div class="col-7">
<div class="placeholder placeholder-xs col-9" />
<div class="placeholder placeholder-xs col-7" />
</div>
<div class="col-2 ms-auto text-end">
<div class="placeholder placeholder-xs col-8" />
<div class="placeholder placeholder-xs col-10" />
</div>
</div>
</ListGroupItem>
))
}
</ListGroup>
</div>