Render Modal as real markup; drain scripts via CaptureScript (#2742)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
Paweł Kuna
2026-08-03 00:55:32 +02:00
committed by GitHub
co-authored by StarDev
parent 66dc336726
commit 575d68572f
200 changed files with 10670 additions and 10096 deletions
+5 -7
View File
@@ -65,16 +65,14 @@ additively instead of creating parallel variants.
## Page scripts and modals ## Page scripts and modals
- Register per-page scripts with `addPageScript()` and modals with - Capture markup with `CaptureScript` / `CaptureModal` (HTML in the slot, not
`addPageModal()` (`@shared/lib/page-scripts.ts` / `page-modals.ts`). template strings). They register via `addPageScript()` / `addPageModal()`
(`@shared/lib/page-scripts.ts` / `page-modals.ts`).
Wrap at the call site, e.g. `<CaptureModal><Modal …>…</Modal></CaptureModal>`.
Registration MUST be synchronous in the component frontmatter (before the Registration MUST be synchronous in the component frontmatter (before the
first `await`) — Astro renders siblings concurrently, and a registration first `await`) — Astro renders siblings concurrently, and a registration
after `await Astro.slots.render()` loses the race against the drain in after `await Astro.slots.render()` loses the race against the drain in
`PageScripts`/`PageModals` (emitted by `BaseLayout`). `PageScripts`/`PageModals` (emitted by `BaseLayout` / `DocsLayout`).
- Script-emitting components also render `<InlineScript code={script} />`.
Its behavior is chosen per package by the vite define
`import.meta.env.INLINE_PAGE_SCRIPTS`: preview drains registered scripts at
the end of the page; docs inlines them next to the example.
- Third-party page libraries: list names in the layout's `pageLibs` prop — - Third-party page libraries: list names in the layout's `pageLibs` prop —
resolved via `@tabler/core/libs.json` (a full `http` URL in there is emitted resolved via `@tabler/core/libs.json` (a full `http` URL in there is emitted
verbatim; `head: true` libs go into `<head>`). verbatim; `head: true` libs go into `<head>`).
+1 -1
View File
@@ -34,7 +34,7 @@ export default defineConfig({
}, },
integrations: [mdx()], integrations: [mdx()],
markdown: { markdown: {
// markdown-it in Eleventy did not produce typographic quotes — neither do we // No typographic quote rewriting.
processor: satteri({ features: { smartPunctuation: false } }), processor: satteri({ features: { smartPunctuation: false } }),
shikiConfig: { shikiConfig: {
theme: 'github-dark', theme: 'github-dark',
+1 -1
View File
@@ -1,5 +1,5 @@
--- ---
// Equivalent of docs/logo.html — the Tabler logo with wordmark (120x32), pasted 1:1. // Tabler logo with wordmark (120x32).
--- ---
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="32" fill="none"> <svg xmlns="http://www.w3.org/2000/svg" width="120" height="32" fill="none">
+65 -57
View File
@@ -1,78 +1,86 @@
--- ---
// Equivalent of docs/menu.html. In Eleventy the menu is built from `collections.docs | // Menu tree from shared/data/docs.json.
// collection-tree` (the tree of content/**/*.md files of the @tabler/docs app); in the PoC import Subheader from '@ui/Subheader.astro';
// the tree is frozen in shared/data/docs.json (extracted from the reference import docs from '@data/docs.json';
// development docs build). import { pathSlug as slug } from '@shared/lib/string-format';
import Subheader from '@ui/Subheader.astro'
import docs from '@data/docs.json'
import { pathSlug as slug } from '@shared/lib/string-format'
interface MenuLeaf { interface MenuLeaf {
title: string title: string;
url: string url: string;
} }
interface MenuGroup extends MenuLeaf { interface MenuGroup extends MenuLeaf {
children?: MenuLeaf[] children?: MenuLeaf[];
} }
interface MenuSection { interface MenuSection {
title: string title: string;
children?: MenuGroup[] children?: MenuGroup[];
} }
interface Props { interface Props {
/** Page URL in the docs namespace (equivalent of page.url), e.g. "/ui/components/alert/". */ /** Page URL in the docs namespace (equivalent of page.url), e.g. "/ui/components/alert/". */
url: string url: string;
} }
const { url } = Astro.props const { url } = Astro.props;
const menu = docs.menu as MenuSection[] const menu = docs.menu as MenuSection[];
--- ---
<!-- BEGIN DOCS MENU --> <!-- BEGIN DOCS MENU -->
<nav class="space-y space-y-5" id="menu"> <nav class="space-y space-y-5" id="menu">
{ {
menu.map((level1) => ( menu.map((level1) => (
<div> <div>
<Subheader class="mb-2">{level1.title}</Subheader> <Subheader class="mb-2">{level1.title}</Subheader>
{level1.children && level1.children.length > 0 && ( {level1.children && level1.children.length > 0 && (
<nav class="nav nav-vertical"> <nav class="nav nav-vertical">
{level1.children.map((level2) => { {level1.children.map((level2) => {
const hasChildren = Boolean(level2.children && level2.children.length > 0) const hasChildren = Boolean(level2.children && level2.children.length > 0);
// Expanded only for the group's own page or one of its direct children — // Expanded only for the group's own page or one of its direct children —
// a prefix check would also expand "Introduction" on /ui/getting-started/frameworks/* pages. // a prefix check would also expand "Introduction" on /ui/getting-started/frameworks/* pages.
const expanded = url === level2.url || Boolean(level2.children?.some((child) => child.url === url)) const expanded = url === level2.url || Boolean(level2.children?.some((child) => child.url === url));
return ( return (
<div> <div>
{/* as in Liquid: href/data-bs-* only when the element has children */} {/* href/data-bs-* only when the element has children */}
{hasChildren ? ( {hasChildren ? (
<a class={`nav-link${expanded ? ' active' : ''}`} href={level2.url} data-bs-toggle="collapse" data-bs-target={`#collapse-${slug(level2.url)}`} aria-expanded={expanded ? 'true' : 'false'}> <a
{level2.title} <span class="nav-link-toggle" /> class={`nav-link${expanded ? ' active' : ''}`}
</a> href={level2.url}
) : ( data-bs-toggle="collapse"
<a class={`nav-link${expanded ? ' active' : ''}`}>{level2.title}</a> data-bs-target={`#collapse-${slug(level2.url)}`}
)} aria-expanded={expanded ? 'true' : 'false'}
>
{level2.title} <span class="nav-link-toggle" />
</a>
) : (
<a class={`nav-link${expanded ? ' active' : ''}`}>{level2.title}</a>
)}
{hasChildren && ( {hasChildren && (
<nav class={`nav nav-vertical collapse${expanded ? ' show' : ''}`} id={`collapse-${slug(level2.url)}`}> <nav
{level2.children!.map((level3) => ( class={`nav nav-vertical collapse${expanded ? ' show' : ''}`}
<div> id={`collapse-${slug(level2.url)}`}
<a class={`nav-link${url === level3.url ? ' active' : ''}`} href={level3.url}> >
{level3.title} {level2.children!.map((level3) => (
</a> <div>
</div> <a
))} class={`nav-link${url === level3.url ? ' active' : ''}`}
</nav> href={level3.url}
)} >
</div> {level3.title}
) </a>
})} </div>
</nav> ))}
)} </nav>
</div> )}
)) </div>
} );
})}
</nav>
)}
</div>
))
}
</nav> </nav>
<!-- END DOCS MENU --> <!-- END DOCS MENU -->
+53 -54
View File
@@ -1,63 +1,62 @@
--- ---
// Equivalent of docs/navbar.html. import Icon from '@ui/Icon.astro';
import Icon from '@ui/Icon.astro' import DocsLogo from './DocsLogo.astro';
import DocsLogo from './DocsLogo.astro' import { site } from '@shared/lib/site';
import { site } from '@shared/lib/site'
// Eventually belongs in src/lib/site.ts (out of scope for this task). // Eventually belongs in src/lib/site.ts (out of scope for this task).
const previewUrl = 'https://preview.tabler.io' const previewUrl = 'https://preview.tabler.io';
const changelogUrl = 'https://tabler.io/changelog' const changelogUrl = 'https://tabler.io/changelog';
--- ---
<!-- BEGIN DOCS NAVBAR --> <!-- BEGIN DOCS NAVBAR -->
<div class="navbar navbar-expand sticky-top"> <div class="navbar navbar-expand sticky-top">
<div class="container"> <div class="container">
<div class="row flex-fill align-items-md-center"> <div class="row flex-fill align-items-md-center">
<div class="col"> <div class="col">
<div class="d-flex align-items-center gap-4"> <div class="d-flex align-items-center gap-4">
{/* href=".": Liquid `page.url | relative` receives a string, so the filter always returns "." */} {/* href="." — relative link for docs root */}
<a href="." class="navbar-brand navbar-brand-autodark gap-4"> <a href="." class="navbar-brand navbar-brand-autodark gap-4">
<DocsLogo /> <DocsLogo />
</a> </a>
<div> <div>
<span class="badge">v{site.version}</span> <span class="badge">v{site.version}</span>
</div> </div>
</div> </div>
</div> </div>
<div class="d-none d-md-block col"> <div class="d-none d-md-block col">
<div id="docsearch"></div> <div id="docsearch"></div>
</div> </div>
<div class="col d-flex"> <div class="col d-flex">
<ul class="navbar-nav ms-auto gap-2 align-items-center"> <ul class="navbar-nav ms-auto gap-2 align-items-center">
<div class="nav-item d-none d-md-block"> <div class="nav-item d-none d-md-block">
<a href={previewUrl} class="nav-link">Preview</a> <a href={previewUrl} class="nav-link">Preview</a>
</div> </div>
<div class="nav-item d-none d-md-block"> <div class="nav-item d-none d-md-block">
<a href={changelogUrl} class="nav-link">Changelog</a> <a href={changelogUrl} class="nav-link">Changelog</a>
</div> </div>
<li class="nav-item hide-theme-dark"> <li class="nav-item hide-theme-dark">
<a href="?theme=dark" class="btn btn-icon"> <a href="?theme=dark" class="btn btn-icon">
<Icon name="moon" /> <Icon name="moon" />
</a> </a>
</li> </li>
<li class="nav-item hide-theme-light"> <li class="nav-item hide-theme-light">
<a href="?theme=light" class="btn btn-icon"> <a href="?theme=light" class="btn btn-icon">
<Icon name="sun" /> <Icon name="sun" />
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a href={site.githubUrl} class="btn btn-icon" target="_blank"> <a href={site.githubUrl} class="btn btn-icon" target="_blank">
<Icon name="brand-github" /> <Icon name="brand-github" />
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a href={previewUrl} class="btn btn-primary" target="_blank"> <a href={previewUrl} class="btn btn-primary" target="_blank">
<Icon name="eye" /> Preview <Icon name="eye" /> Preview
</a> </a>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- END DOCS NAVBAR --> <!-- END DOCS NAVBAR -->
+89 -84
View File
@@ -1,114 +1,119 @@
--- ---
// Equivalent of docs/pagination.html: child-page cards or next/prev siblings. import DocsCard from './DocsCard.astro';
import DocsCard from './DocsCard.astro' import docs from '@data/docs.json';
import docs from '@data/docs.json' import Icon from '@ui/Icon.astro';
import Icon from '@ui/Icon.astro' import { getDocsChildren } from '@shared/lib/docs-children';
import { getDocsChildren } from '@shared/lib/docs-children' import type { DocsChildPage } from '@shared/lib/docs-children';
import type { DocsChildPage } from '@shared/lib/docs-children'
interface MenuLeaf { interface MenuLeaf {
title: string title: string;
url: string url: string;
} }
interface MenuNode { interface MenuNode {
title: string title: string;
url?: string url?: string;
children?: MenuNode[] children?: MenuNode[];
} }
interface Props { interface Props {
/** Page URL in the docs namespace (equivalent of page.url), e.g. "/ui/components/alert/". */ /** Page URL in the docs namespace (equivalent of page.url), e.g. "/ui/components/alert/". */
url: string url: string;
} }
const { url } = Astro.props const { url } = Astro.props;
const children = getDocsChildren(url) const children = getDocsChildren(url);
type Pagination = { type Pagination = {
prev: MenuLeaf | null prev: MenuLeaf | null;
next: MenuLeaf | null next: MenuLeaf | null;
found: boolean found: boolean;
} };
function findPage(nodes: MenuNode[]): Pagination | null { function findPage(nodes: MenuNode[]): Pagination | null {
const index = nodes.findIndex((item) => item.url === url) const index = nodes.findIndex((item) => item.url === url);
if (index !== -1) { if (index !== -1) {
const previousNode = index > 0 ? nodes[index - 1] : null const previousNode = index > 0 ? nodes[index - 1] : null;
const nextNode = index < nodes.length - 1 ? nodes[index + 1] : null const nextNode = index < nodes.length - 1 ? nodes[index + 1] : null;
return { return {
prev: previousNode?.url ? { title: previousNode.title, url: previousNode.url } : null, prev: previousNode?.url ? { title: previousNode.title, url: previousNode.url } : null,
next: nextNode?.url ? { title: nextNode.title, url: nextNode.url } : null, next: nextNode?.url ? { title: nextNode.title, url: nextNode.url } : null,
found: true, found: true,
} };
} }
for (const node of nodes) { for (const node of nodes) {
const result = node.children ? findPage(node.children) : null const result = node.children ? findPage(node.children) : null;
if (result) return result if (result) return result;
} }
return null return null;
} }
const { prev, next, found } = findPage(docs.menu as MenuNode[]) ?? { const { prev, next, found } = findPage(docs.menu as MenuNode[]) ?? {
prev: null, prev: null,
next: null, next: null,
found: false, found: false,
} };
--- ---
<!-- BEGIN DOCS PAGINATION -->{ <!-- BEGIN DOCS PAGINATION -->
children.length > 0 && ( {
<div class="mt-6 pt-6"> children.length > 0 && (
<div class="row row-deck row-cards"> <div class="mt-6 pt-6">
{children.map((child: DocsChildPage) => ( <div class="row row-deck row-cards">
<DocsCard href={child.url} title={child.title} description={child.description} icon={child.icon} /> {children.map((child: DocsChildPage) => (
))} <DocsCard
</div> href={child.url}
</div> title={child.title}
) description={child.description}
icon={child.icon}
/>
))}
</div>
</div>
)
} }
{ {
children.length === 0 && found && ( children.length === 0 && found && (
<div class="mt-6 pt-6"> <div class="mt-6 pt-6">
<ul class="pagination"> <ul class="pagination">
{prev && ( {prev && (
<li class="page-item page-prev"> <li class="page-item page-prev">
<a class="page-link" href={prev.url}> <a class="page-link" href={prev.url}>
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col-auto"> <div class="col-auto">
<Icon name="chevron-left" /> <Icon name="chevron-left" />
</div> </div>
<div class="col"> <div class="col">
<div class="page-item-subtitle">previous</div> <div class="page-item-subtitle">previous</div>
<div class="page-item-title">{prev.title}</div> <div class="page-item-title">{prev.title}</div>
</div> </div>
</div> </div>
</a> </a>
</li> </li>
)} )}
{next && ( {next && (
<li class="page-item page-next"> <li class="page-item page-next">
<a class="page-link" href={next.url}> <a class="page-link" href={next.url}>
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col"> <div class="col">
<div class="page-item-subtitle">next</div> <div class="page-item-subtitle">next</div>
<div class="page-item-title">{next.title}</div> <div class="page-item-title">{next.title}</div>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<Icon name="chevron-right" /> <Icon name="chevron-right" />
</div> </div>
</div> </div>
</a> </a>
</li> </li>
)} )}
</ul> </ul>
</div> </div>
) )
} }
<!-- END DOCS PAGINATION --> <!-- END DOCS PAGINATION -->
+29 -29
View File
@@ -1,46 +1,46 @@
--- ---
// Equivalent of docs/toc.html. In Eleventy the TOC is computed by the `content | toc` // TOC comes in as a prop from the consuming page (h2/h3 headings from the
// filter (h2/h3 headings from the markdown content, excluding <!--EXAMPLE-->...<!--/EXAMPLE--> blocks); // markdown content, excluding <!--EXAMPLE-->...<!--/EXAMPLE--> blocks).
// here it comes in as a prop from the consuming page.
export interface TocItem { export interface TocItem {
/** 2 or 3 (h2/h3); h3 gets the ms-3 indent */ /** 2 or 3 (h2/h3); h3 gets the ms-3 indent */
level: number level: number;
text: string text: string;
/** anchor without '#', e.g. "default-markup" */ /** anchor without '#', e.g. "default-markup" */
id: string id: string;
} }
interface Props { interface Props {
toc?: TocItem[] toc?: TocItem[];
} }
const { toc = [] } = Astro.props const { toc = [] } = Astro.props;
// Equivalent of `illustrations | size` (shared/data/illustrations.json has 100 entries). // shared/data/illustrations.json has 100 entries.
const illustrationsCount = 100 const illustrationsCount = 100;
--- ---
<!-- BEGIN DOCS TOC -->{ <!-- BEGIN DOCS TOC -->
toc.length > 0 && ( {
<Fragment> toc.length > 0 && (
<h3>Table of Contents</h3> <Fragment>
<div class="nav nav-vertical" id="toc"> <h3>Table of Contents</h3>
{toc.map((item) => ( <div class="nav nav-vertical" id="toc">
<a href={`#${item.id}`} class={`nav-link${item.level === 3 ? ' ms-3' : ''}`}> {toc.map((item) => (
{item.text} <a href={`#${item.id}`} class={`nav-link${item.level === 3 ? ' ms-3' : ''}`}>
</a> {item.text}
))} </a>
</div> ))}
</Fragment> </div>
) </Fragment>
)
} }
<a href="https://r.tabler.io/illustrations" class="card card-sm mt-6 shadow-none" target="_blank"> <a href="https://r.tabler.io/illustrations" class="card card-sm mt-6 shadow-none" target="_blank">
<div class="card-body"> <div class="card-body">
<img src="/img/banner-carbon.png" class="mb-3" alt="" /> <img src="/img/banner-carbon.png" class="mb-3" alt="" />
<h4>{illustrationsCount} sleek illustrations for your startup's visual identity.</h4> <h4>{illustrationsCount} sleek illustrations for your startup's visual identity.</h4>
</div> </div>
</a> </a>
<!-- END DOCS TOC --> <!-- END DOCS TOC -->
+77 -48
View File
@@ -1,68 +1,97 @@
--- ---
// copy button with data-clipboard-text (equivalent of escape_attribute). // Example code from the `html` prop or the rendered slot; copy button uses data-clipboard-text.
// The example code comes from the `html` prop or from the rendered slot. import Icon from '@ui/Icon.astro';
import Icon from '@ui/Icon.astro' import { beautifyHtml, highlightCode, removeHref } from '@shared/lib/code-example';
import { beautifyHtml, highlightCode, removeHref } from '@shared/lib/code-example'
interface Props { interface Props {
/** raw HTML of the example; when absent — the slot is rendered */ /** raw HTML of the example; when absent — the slot is rendered */
html?: string html?: string;
/** code shown in the panel instead of html (the code param in Liquid) */ /** code shown in the panel instead of html */
code?: string code?: string;
raw?: boolean raw?: boolean;
overflow?: string overflow?: string;
bg?: string bg?: string;
class?: string class?: string;
height?: string height?: string;
column?: boolean column?: boolean;
centered?: boolean centered?: boolean;
vertical?: boolean vertical?: boolean;
columnFullWidth?: boolean columnFullWidth?: boolean;
hideCode?: boolean hideCode?: boolean;
codeOnly?: boolean codeOnly?: boolean;
} }
const { html: htmlProp, code, raw, overflow = 'auto', bg, class: className, height, column, centered, vertical, columnFullWidth, hideCode, codeOnly } = Astro.props const {
html: htmlProp,
code,
raw,
overflow = 'auto',
bg,
class: className,
height,
column,
centered,
vertical,
columnFullWidth,
hideCode,
codeOnly,
} = Astro.props;
// equivalent of {% removeemptylines %} around include output (e.g. ui/alert.html) // Strip empty lines from the example HTML.
let html = (htmlProp ?? (await Astro.slots.render('default'))).replace(/^\s*[\r\n]/gm, '') let html = (htmlProp ?? (await Astro.slots.render('default'))).replace(/^\s*[\r\n]/gm, '');
// JSX reserializes the slot markup: empty SVG elements get explicit closing // JSX reserializes the slot markup: empty SVG elements get explicit closing
// tags, and apostrophes in text are escaped. We restore the source form so the // tags, and apostrophes in text are escaped. We restore the source form so the
// code in the panel looks like hand-written HTML. // code in the panel looks like hand-written HTML.
html = html.replace(/<(path|circle|line|polyline|polygon|rect|ellipse)([^>]*?)\s*><\/\1>/g, '<$1$2 />').replace(/&#39;/g, "'") html = html
.replace(/<(path|circle|line|polyline|polygon|rect|ellipse)([^>]*?)\s*><\/\1>/g, '<$1$2 />')
.replace(/&#39;/g, "'");
const exampleClasses = ['example fs-base border rounded my-5', !raw && 'd-flex flex-wrap justify-content-center', `overflow-${overflow}`, 'position-relative', bg ? `bg-${bg}` : 'bg-pattern-rectangles', className] const exampleClasses = [
'example fs-base border rounded my-5',
!raw && 'd-flex flex-wrap justify-content-center',
`overflow-${overflow}`,
'position-relative',
bg ? `bg-${bg}` : 'bg-pattern-rectangles',
className,
]
const innerClasses = ['p-6 w-full', column && 'd-flex gap-3 flex-column', !column && centered && `d-flex flex-fill flex-wrap gap-2 justify-content-center${vertical ? ' align-items-center flex-column' : ' justify-content-center'}`, columnFullWidth && 'd-flex flex-fill flex-column gap-2'] const innerClasses = [
'p-6 w-full',
column && 'd-flex gap-3 flex-column',
!column && centered && `d-flex flex-fill flex-wrap gap-2 justify-content-center${vertical ? ' align-items-center flex-column' : ' justify-content-center'}`,
columnFullWidth && 'd-flex flex-fill flex-column gap-2',
]
const highlighted = hideCode ? '' : await highlightCode(beautifyHtml(code ?? html), 'html') const highlighted = hideCode ? '' : await highlightCode(beautifyHtml(code ?? html), 'html');
--- ---
<!--EXAMPLE-->{ <!--EXAMPLE-->
!codeOnly && ( {
<div class:list={exampleClasses} style={height ? `height: ${height}` : undefined}> !codeOnly && (
{raw ? ( <div class:list={exampleClasses} style={height ? `height: ${height}` : undefined}>
<Fragment set:html={removeHref(html)} /> {
) : ( raw ? (
<div class:list={innerClasses} style={column ? 'max-width: 25rem;' : undefined}> <Fragment set:html={removeHref(html)} />
<Fragment set:html={removeHref(html)} /> ) : (
</div> <div class:list={innerClasses} style={column ? 'max-width: 25rem;' : undefined}>
)} <Fragment set:html={removeHref(html)} />
</div> </div>
) )
}
</div>
)
} }
{ {
!hideCode && ( !hideCode && (
<div class="position-relative"> <div class="position-relative">
{/* Astro escapes the attribute value itself (equivalent of escape_attribute) */} <a class="btn btn-icon btn-dark position-absolute m-2 top-0 end-0 z-3" data-clipboard-text={html}>
<a class="btn btn-icon btn-dark position-absolute m-2 top-0 end-0 z-3" data-clipboard-text={html}> <Icon name="clipboard" />
<Icon name="clipboard" /> <Icon name="check" class="d-none" />
<Icon name="check" class="d-none" /> </a>
</a> <Fragment set:html={highlighted} />
<Fragment set:html={highlighted} /> </div>
</div> )
)
} }
<!--/EXAMPLE--> <!--/EXAMPLE-->
+15 -14
View File
@@ -1,5 +1,5 @@
--- ---
// Docs layout — port of shared/layouts/docs/default.html (the @tabler/docs app). // Docs layout for the @tabler/docs app.
// Standalone layout with its own <head>; does NOT use BaseLayout.astro. // Standalone layout with its own <head>; does NOT use BaseLayout.astro.
// Development uses the unminified assets; production additionally emits the SEO metadata. // Development uses the unminified assets; production additionally emits the SEO metadata.
import { site } from '@shared/lib/site'; import { site } from '@shared/lib/site';
@@ -12,6 +12,7 @@ import DocsToc from '@components/DocsToc.astro';
import type { TocItem } from '@components/DocsToc.astro'; import type { TocItem } from '@components/DocsToc.astro';
import DocsPagination from '@components/DocsPagination.astro'; import DocsPagination from '@components/DocsPagination.astro';
import Prose from '@ui/Prose.astro'; import Prose from '@ui/Prose.astro';
import PageScripts from '@shared/components/PageScripts.astro';
interface Props { interface Props {
/** front matter: the page's h1 heading */ /** front matter: the page's h1 heading */
@@ -26,16 +27,16 @@ interface Props {
seoDescription?: string; seoDescription?: string;
/** front matter `added-in`: renders the "Added in X" badge next to the h1 */ /** front matter `added-in`: renders the "Added in X" badge next to the h1 */
addedIn?: string; addedIn?: string;
/** table of contents (h2/h3 from the markdown content) — see DocsToc.astro */ /** Table of contents (h2/h3 from the markdown content) — see DocsToc.astro */
toc?: TocItem[]; toc?: TocItem[];
/** /**
* Page URL in the docs namespace (equivalent of Eleventy's page.url), e.g. * Page URL in the docs namespace, e.g. "/ui/components/alert/".
* "/ui/components/alert/". By default derived from Astro.url.pathname by * By default derived from Astro.url.pathname by stripping the /docs prefix.
* stripping the /docs prefix. Drives the active menu entry, pagination, * Drives the active menu entry, pagination, the "Edit this page" link, and
* the "Edit this page" link, and the relative favicon path. * the relative favicon path.
*/ */
url?: string; url?: string;
/** front matter `docs-libs`: extra libraries from src/data/libs.json (css+js) */ /** Extra libraries from libs.json (css+js) */
docsLibs?: string[]; docsLibs?: string[];
/** hides child and previous/next page navigation */ /** hides child and previous/next page navigation */
hidePagination?: boolean; hidePagination?: boolean;
@@ -57,7 +58,7 @@ const {
} = Astro.props; } = Astro.props;
const environment = process.env.NODE_ENV || 'production'; const environment = process.env.NODE_ENV || 'production';
// URL in the docs namespace: /docs/ui/components/alert(.html) -> /ui/components/alert/ // Strip /docs prefix (and optional .html) for the docs namespace URL.
const docsUrl = const docsUrl =
Astro.props.url ?? Astro.props.url ??
(() => { (() => {
@@ -80,7 +81,7 @@ const metaDescription = seoDescription ?? description;
const siteName = pageSection ? `Tabler ${pageSection} Documentation` : 'Tabler Documentation'; const siteName = pageSection ? `Tabler ${pageSection} Documentation` : 'Tabler Documentation';
const canonicalUrl = new URL(docsUrl, Astro.site ?? site.docsUrl).href; const canonicalUrl = new URL(docsUrl, Astro.site ?? site.docsUrl).href;
// Equivalent of `{{ page | relative }}`: for /ui/components/alert/ -> "../../.." // Relative asset base: for /ui/components/alert/ -> "../../.."
const depth = docsUrl.split('/').filter(Boolean).length; const depth = docsUrl.split('/').filter(Boolean).length;
const relative = depth === 0 ? '.' : '../'.repeat(depth).slice(0, -1); const relative = depth === 0 ? '.' : '../'.repeat(depth).slice(0, -1);
@@ -88,8 +89,8 @@ const relative = depth === 0 ? '.' : '../'.repeat(depth).slice(0, -1);
// MDX layout; fall back to the flat-page convention (docs/pages/<url>.mdx). // MDX layout; fall back to the flat-page convention (docs/pages/<url>.mdx).
const editFilePath = editPath ?? `docs/pages${docsUrl.replace(/\/$/, '')}.mdx`; const editFilePath = editPath ?? `docs/pages${docsUrl.replace(/\/$/, '')}.mdx`;
// JS/CSS libraries as in Eleventy: docs-libs + clipboard (always included). // docs-libs + clipboard (always included). In dev, file names stay unchanged
// In dev file names stay unchanged (e.g. dist/clipboard.min.js keeps .min — the reference has it so). // (e.g. dist/clipboard.min.js keeps .min).
type Lib = { npm?: string; js?: string[]; css?: string[] }; type Lib = { npm?: string; js?: string[]; css?: string[] };
const libEntries = Object.entries(libs as Record<string, Lib>); const libEntries = Object.entries(libs as Record<string, Lib>);
const libUrl = (lib: Lib, file: string) => const libUrl = (lib: Lib, file: string) =>
@@ -234,7 +235,6 @@ const docsLinks = docs.links as DocsLink[];
{ {
addedIn && ( addedIn && (
<div class="ms-auto"> <div class="ms-auto">
{/* equivalent of ui/badge.html color="primary" light=true */}
<span class="badge bg-primary-lt text-primary-lt-fg">Added in {addedIn}</span> <span class="badge bg-primary-lt text-primary-lt-fg">Added in {addedIn}</span>
</div> </div>
) )
@@ -243,8 +243,7 @@ const docsLinks = docs.links as DocsLink[];
<p class="text-secondary fs-3 lh-3">{summary}</p> <p class="text-secondary fs-3 lh-3">{summary}</p>
{/* markdown content; equivalent of {{ content | headings-id }} — the consuming {/* Page must supply headings with ids (MDX does this by default). */}
page must supply headings with ids (MDX does this by default) */}
<slot /> <slot />
{!hidePagination && <DocsPagination url={docsUrl} />} {!hidePagination && <DocsPagination url={docsUrl} />}
@@ -340,6 +339,8 @@ const docsLinks = docs.links as DocsLink[];
<script is:inline src="/dist/js/tabler.js"></script> <script is:inline src="/dist/js/tabler.js"></script>
<!-- END GLOBAL MANDATORY SCRIPTS --> <!-- END GLOBAL MANDATORY SCRIPTS -->
<PageScripts />
<!-- BEGIN DOCS SCRIPTS --> <!-- BEGIN DOCS SCRIPTS -->
<script is:inline src="/js/docs.js" defer></script> <script is:inline src="/js/docs.js" defer></script>
<!-- END DOCS SCRIPTS --> <!-- END DOCS SCRIPTS -->
+1 -6
View File
@@ -1,10 +1,5 @@
--- ---
// docs/content/ui/base/markdown.md sets `layout: redirect` + `redirect.to: ui/base/prose/`, import RedirectLayout from '@shared/layouts/RedirectLayout.astro';
// but shared/includes/redirect.html reads `page.redirect.to` — undefined in Eleventy —
// so the include gets an empty url and the target collapses to `{{ page | relative }}`.
// For this 3-deep page that is "../../..", which is exactly what the reference
// build (ui/base/markdown/index.html) redirects to.
import RedirectLayout from '@shared/layouts/RedirectLayout.astro'
--- ---
<RedirectLayout base="../../.." /> <RedirectLayout base="../../.." />
+1 -1
View File
@@ -111,7 +111,7 @@ To edit settings, press <kbd>ctrl</kbd> + <kbd>,</kbd> or <kbd>ctrl</kbd> + <kbd
## Prose ## Prose
If you can't use the CSS classes you want, or you just want to use HTML tags, use the `.prose` class in a container. It will apply the default styles for markdown elements. The `.markdown` class is an alias and will be removed in a future release. The `.wysiwyg` integration (`ui/wysiwyg.html`) is deprecated and will be removed in a future release. See the Prose page for full examples. If you can't use the CSS classes you want, or you just want to use HTML tags, use the `.prose` class in a container. It will apply the default styles for markdown elements. The `.markdown` class is an alias and will be removed in a future release. The `.wysiwyg` integration is deprecated and will be removed in a future release. See the Prose page for full examples.
<Example> <Example>
<Prose> <h1>Hello World</h1> <p> Lorem ipsum<sup>[1]</sup> dolor sit amet, consectetur adipiscing elit. Nulla accumsan, metus ultrices eleifend gravida, nulla nunc varius lectus, nec rutrum justo nibh eu lectus. Ut vulputate semper dui. Fusce erat odio, sollicitudin vel erat vel, interdum mattis neque. Sub<sub >script</sub > works as well! </p> <h2>Second level</h2> <p> Curabitur accumsan turpis pharetra <strong>augue tincidunt</strong> blandit. Quisque condimentum maximus mi, sit amet commodo arcu rutrum id. Proin pretium urna vel cursus venenatis. Suspendisse potenti. Etiam mattis sem rhoncus lacus dapibus facilisis. Donec at dignissim dui. Ut et neque nisl. </p> <ul> <li>In fermentum leo eu lectus mollis, quis dictum mi aliquet.</li> <li>Morbi eu nulla lobortis, lobortis est in, fringilla felis.</li> <li>Aliquam nec felis in sapien venenatis viverra fermentum nec lectus.</li> <li>Ut non enim metus.</li> </ul> </Prose> <Prose> <h1>Hello World</h1> <p> Lorem ipsum<sup>[1]</sup> dolor sit amet, consectetur adipiscing elit. Nulla accumsan, metus ultrices eleifend gravida, nulla nunc varius lectus, nec rutrum justo nibh eu lectus. Ut vulputate semper dui. Fusce erat odio, sollicitudin vel erat vel, interdum mattis neque. Sub<sub >script</sub > works as well! </p> <h2>Second level</h2> <p> Curabitur accumsan turpis pharetra <strong>augue tincidunt</strong> blandit. Quisque condimentum maximus mi, sit amet commodo arcu rutrum id. Proin pretium urna vel cursus venenatis. Suspendisse potenti. Etiam mattis sem rhoncus lacus dapibus facilisis. Donec at dignissim dui. Ut et neque nisl. </p> <ul> <li>In fermentum leo eu lectus mollis, quis dictum mi aliquet.</li> <li>Morbi eu nulla lobortis, lobortis est in, fringilla felis.</li> <li>Aliquam nec felis in sapien venenatis viverra fermentum nec lectus.</li> <li>Ut non enim metus.</li> </ul> </Prose>
+102 -110
View File
@@ -1,120 +1,112 @@
// @ts-check // @ts-check
import { defineConfig } from 'astro/config' import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx' import mdx from '@astrojs/mdx';
import { satteri } from '@astrojs/markdown-satteri' import { satteri } from '@astrojs/markdown-satteri';
import beautify from 'js-beautify' import beautify from 'js-beautify';
import { execFileSync } from 'node:child_process' import { execFileSync } from 'node:child_process';
import { devNull } from 'node:os' import { devNull } from 'node:os';
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url';
/** /**
* Equivalent of the Eleventy "html-prettify" step (@tabler/preview): the * After build, format page HTML with prettier (users copy it 1:1).
* generated HTML is the product (users copy it 1:1), so after the build we
* format it with prettier per .prettierrc.
* @returns {import('astro').AstroIntegration} * @returns {import('astro').AstroIntegration}
*/ */
function prettifyHtml() { function prettifyHtml() {
return { return {
name: 'prettify-html', name: 'prettify-html',
hooks: { hooks: {
'astro:build:done': async ({ dir, logger }) => { 'astro:build:done': async ({ dir, logger }) => {
const outDir = fileURLToPath(dir) const outDir = fileURLToPath(dir);
// dist/preview/ and dist/dist/ are copy-assets.mjs's copies of public/{preview,dist} // dist/preview/ and dist/dist/ are copy-assets.mjs's copies of public/{preview,dist}
// (demo css/js and @tabler/core's dist, including vendored libs) — not pages, and // (demo css/js and @tabler/core's dist, including vendored libs) — not pages, and
// some vendored libs ship their own malformed docs/*.html that trips the parser below. // some vendored libs ship their own malformed docs/*.html that trips the parser below.
/** @param {string} file */ /** @param {string} file */
const isVendorCopy = (file) => file.includes(`${outDir}preview/`) || file.includes(`${outDir}dist/`) const isVendorCopy = (file) => file.includes(`${outDir}preview/`) || file.includes(`${outDir}dist/`);
// Astro appends "overflow-x: auto" to the shiki <pre> style — the // Strip Astro's extra "overflow-x: auto" on shiki <pre> styles.
// Eleventy pipeline doesn't have it and HTML is the product: restore 1:1. const { globSync } = await import('node:fs');
const { globSync } = await import('node:fs') const { readFileSync, writeFileSync } = await import('node:fs');
const { readFileSync, writeFileSync } = await import('node:fs') for (const file of globSync(`${outDir}**/*.html`, { exclude: isVendorCopy })) {
for (const file of globSync(`${outDir}**/*.html`, { exclude: isVendorCopy })) { const content = readFileSync(file, 'utf8');
const content = readFileSync(file, 'utf8') const cleaned = content.replaceAll('; overflow-x: auto;', '');
const cleaned = content.replaceAll('; overflow-x: auto;', '') if (cleaned !== content) writeFileSync(file, cleaned);
if (cleaned !== content) writeFileSync(file, cleaned) }
} execFileSync(
execFileSync( 'npx',
'npx', [
[ 'prettier',
'prettier', '--write',
'--write', '--parser',
'--parser', 'html',
'html', // Prettier's default ignore-path is [.gitignore, .prettierignore], and both
// Prettier's default ignore-path is [.gitignore, .prettierignore], and both // list "dist" (needed elsewhere so normal lint/format passes skip build
// list "dist" (needed elsewhere so normal lint/format passes skip build // output) — that silently no-ops this pass on the very directory it targets.
// output) — that silently no-ops this pass on the very directory it targets. // Point at devNull to opt this one deliberate pass out of those ignores.
// Point at devNull to opt this one deliberate pass out of those ignores. '--ignore-path',
'--ignore-path', devNull,
devNull, `${outDir}**/*.html`,
`${outDir}**/*.html`, `!${outDir}preview/**`,
`!${outDir}preview/**`, `!${outDir}dist/**`,
`!${outDir}dist/**`, ],
], { stdio: 'inherit' },
{ stdio: 'inherit' }, );
) logger.info('HTML formatted with prettier');
logger.info('HTML formatted with prettier') },
}, },
}, };
}
} }
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
// pages live at the package root (./pages) — all components/lib/data are // pages live at the package root (./pages) — all components/lib/data are
// shared (see the @shared alias) // shared (see the @shared alias)
srcDir: '.', srcDir: '.',
server: { server: {
port: 3000, port: 3000,
// bind on all interfaces so the dev server is reachable from Docker // bind on all interfaces so the dev server is reachable from Docker
// port mappings and other devices on the local network // port mappings and other devices on the local network
host: true, host: true,
}, },
vite: { vite: {
resolve: { resolve: {
alias: { alias: {
// demo data lives in the monorepo's shared/data single source of // Demo data in shared/data (single source of truth).
// truth shared with the Eleventy packages (no copies in src/data) '@data': fileURLToPath(new URL('../shared/data', import.meta.url)),
'@data': fileURLToPath(new URL('../shared/data', import.meta.url)), // Components/lib shared with docs.
// Astro components/lib shared with docs (single source of truth) '@shared': fileURLToPath(new URL('../shared', import.meta.url)),
'@shared': fileURLToPath(new URL('../shared', import.meta.url)), '@ui': fileURLToPath(new URL('../shared/ui', import.meta.url)),
'@ui': fileURLToPath(new URL('../shared/ui', import.meta.url)), '@components': fileURLToPath(new URL('../shared/components', import.meta.url)),
'@components': fileURLToPath(new URL('../shared/components', import.meta.url)), // Used by @shared/lib/docs-children's glob.
// this package's pages dir — used by @shared/lib/docs-children's glob '@pages': fileURLToPath(new URL('./pages', import.meta.url)),
'@pages': fileURLToPath(new URL('./pages', import.meta.url)), },
}, },
}, },
}, build: {
build: { // Emit sign-in.html instead of sign-in/index.html (HTML is the product).
// emit sign-in.html instead of sign-in/index.html — matches the Eleventy format: 'file',
// preview package layout, where the HTML files are the distributed product },
format: 'file', // Keep readable HTML; prettier formats after the build.
}, compressHTML: false,
// Do not collapse whitespace in the output — the HTML must stay readable integrations: [mdx(), prettifyHtml()],
// (like the Eleventy build); prettier finalizes formatting after the build. markdown: {
compressHTML: false, // No typographic quote rewriting.
integrations: [mdx(), prettifyHtml()], processor: satteri({ features: { smartPunctuation: false } }),
markdown: { shikiConfig: {
// markdown-it in Eleventy does not produce typographic quotes — neither do we theme: 'github-dark',
processor: satteri({ features: { smartPunctuation: false } }), transformers: [
shikiConfig: { {
theme: 'github-dark', // Beautify html fences before highlighting.
transformers: [ preprocess(code) {
{ if (this.options.lang === 'html') {
// The Eleventy docs pipeline beautifies html fences before highlighting return beautify.html(code, { indent_size: 2, wrap_line_length: 80 });
preprocess(code) { }
if (this.options.lang === 'html') { },
return beautify.html(code, { indent_size: 2, wrap_line_length: 80 }) // Keep shiki classes only (drop Astro's astro-code / data-language).
} pre(node) {
}, node.properties.class = 'shiki github-dark';
// Eleventy docs emits raw shiki output: <pre class="shiki github-dark">. delete node.properties.dataLanguage;
// Astro adds its own astro-code class and data-language — restore the },
// exact markdown-it + shiki pipeline markup. },
pre(node) { ],
node.properties.class = 'shiki github-dark' },
delete node.properties.dataLanguage },
}, });
},
],
},
},
})
+80 -66
View File
@@ -1,79 +1,93 @@
--- ---
import FormFooter from '@ui/FormFooter.astro' import FormFooter from '@ui/FormFooter.astro';
import SingleLayout from '@shared/layouts/SingleLayout.astro' import SingleLayout from '@shared/layouts/SingleLayout.astro';
import Button from '@ui/Button.astro' import Button from '@ui/Button.astro';
import ButtonList from '@ui/ButtonList.astro' import ButtonList from '@ui/ButtonList.astro';
import CaptureScript from '@shared/components/CaptureScript.astro';
--- ---
<SingleLayout title="2-Step Verification"> <SingleLayout title="2-Step Verification">
<form class="card card-md" action="./" method="get" autocomplete="off" novalidate> <form class="card card-md" action="./" method="get" autocomplete="off" novalidate>
<div class="card-body"> <div class="card-body">
<h2 class="card-title card-title-lg text-center mb-4">Authenticate Your Account</h2> <h2 class="card-title card-title-lg text-center mb-4">Authenticate Your Account</h2>
<p class="my-4 text-center"> <p class="my-4 text-center">
Please confirm your account by entering the authorization code sent to <strong>+1 856-672-8552</strong>. Please confirm your account by entering the authorization code sent to <strong
</p> >+1 856-672-8552</strong
>.
</p>
<div class="my-5"> <div class="my-5">
<div class="row g-4"> <div class="row g-4">
{ {
Array.from({ length: 2 }).map(() => ( Array.from({ length: 2 }).map(() => (
<div class="col"> <div class="col">
<div class="row g-2"> <div class="row g-2">
{Array.from({ length: 3 }).map(() => ( {Array.from({ length: 3 }).map(() => (
<div class="col"> <div class="col">
<input type="text" class="form-control form-control-lg text-center px-3 py-3" maxlength="1" inputmode="numeric" pattern="[0-9]*" data-code-input /> <input
</div> type="text"
))} class="form-control form-control-lg text-center px-3 py-3"
</div> maxlength="1"
</div> inputmode="numeric"
)) pattern="[0-9]*"
} data-code-input
</div> />
</div> </div>
))}
</div>
</div>
))
}
</div>
</div>
<div class="my-4"> <div class="my-4">
<label class="form-check"> <label class="form-check">
<input type="checkbox" class="form-check-input" /> <input type="checkbox" class="form-check-input" />
Dont't ask for codes again on this device Dont't ask for codes again on this device
</label> </label>
</div> </div>
<FormFooter> <FormFooter>
<ButtonList class="flex-nowrap"> <ButtonList class="flex-nowrap">
<Button text="Cancel" block href="2-step-verification.html" /> <Button text="Cancel" block href="2-step-verification.html" />
<Button text="Verify" block color="primary" /> <Button text="Verify" block color="primary" />
</ButtonList> </ButtonList>
</FormFooter> </FormFooter>
</div> </div>
</form> </form>
<!-- BEGIN CODE INPUT SCRIPT --> <CaptureScript>
<script> <!-- BEGIN CODE INPUT SCRIPT -->
const inputs = document.querySelectorAll<HTMLInputElement>('[data-code-input]') <script is:inline>
const inputs = document.querySelectorAll('[data-code-input]');
// Attach an event listener to each input element // Attach an event listener to each input element
inputs.forEach((input, i) => { inputs.forEach((input, i) => {
input.addEventListener('input', (e) => { input.addEventListener('input', (e) => {
const target = e.target as HTMLInputElement const target = e.target;
// If the input field has a character, and there is a next input field, focus it // If the input field has a character, and there is a next input field, focus it
if (target.value.length === target.maxLength && i + 1 < inputs.length) { if (target.value.length === target.maxLength && i + 1 < inputs.length) {
inputs[i + 1].focus() inputs[i + 1].focus();
} }
}) });
input.addEventListener('keydown', (e: KeyboardEvent) => { input.addEventListener('keydown', (e) => {
const target = e.target as HTMLInputElement const target = e.target;
// If the input field is empty and Backspace is pressed, and there is a previous input field, focus it // If the input field is empty and Backspace is pressed, and there is a previous input field, focus it
if (target.value.length === 0 && e.key === 'Backspace' && i > 0) { if (target.value.length === 0 && e.key === 'Backspace' && i > 0) {
inputs[i - 1].focus() inputs[i - 1].focus();
} }
}) });
}) });
</script> </script>
<!-- END CODE INPUT SCRIPT --> <!-- END CODE INPUT SCRIPT -->
</CaptureScript>
<div class="text-center text-secondary mt-3"> <div class="text-center text-secondary mt-3">
It may take a minute to receive your code. Haven't received it? <a href="./">Resend a new code.</a> It may take a minute to receive your code. Haven't received it? <a href="./"
</div> >Resend a new code.</a
>
</div>
</SingleLayout> </SingleLayout>
+1 -3
View File
@@ -1,7 +1,5 @@
--- ---
// Liquid: {% if country.code == 'US' %} selected{% endif %} — flags.json entries // flags.json entries have no `code` property — value="" and never `selected`.
// have no `code` property, so the reference renders value="" and never `selected`.
// We mirror that output exactly.
import FormFooter from '@ui/FormFooter.astro' import FormFooter from '@ui/FormFooter.astro'
import SingleLayout from '@shared/layouts/SingleLayout.astro' import SingleLayout from '@shared/layouts/SingleLayout.astro'
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -1,9 +1,9 @@
--- ---
// title really is "Forgot password" in the Liquid source) // Title is "Forgot password" (matches auth-lock card, not page name).
import SingleLayout from '@shared/layouts/SingleLayout.astro' import SingleLayout from '@shared/layouts/SingleLayout.astro';
import AuthLockCard from '@shared/components/cards/AuthLockCard.astro' import AuthLockCard from '@shared/components/cards/AuthLockCard.astro';
--- ---
<SingleLayout title="Forgot password"> <SingleLayout title="Forgot password">
<AuthLockCard /> <AuthLockCard />
</SingleLayout> </SingleLayout>
+156 -159
View File
@@ -1,169 +1,166 @@
--- ---
// Note: the `description` front matter key is inert in the dev build (no import DefaultLayout from '@shared/layouts/DefaultLayout.astro';
// <meta name="description"> in the reference output), so it is not ported. import Avatar from '@ui/Avatar.astro';
import DefaultLayout from '@shared/layouts/DefaultLayout.astro' import AvatarUpload from '@ui/AvatarUpload.astro';
import Avatar from '@ui/Avatar.astro' import AvatarList from '@ui/AvatarList.astro';
import AvatarUpload from '@ui/AvatarUpload.astro' import Card from '@ui/Card.astro';
import AvatarList from '@ui/AvatarList.astro' import CardBody from '@ui/CardBody.astro';
import Card from '@ui/Card.astro' import CardTitle from '@ui/CardTitle.astro';
import CardBody from '@ui/CardBody.astro' import people from '@data/people.json';
import CardTitle from '@ui/CardTitle.astro' import { site } from '@shared/lib/site';
import people from '@data/people.json' import { firstLetters } from '@shared/lib/string-format';
import { site } from '@shared/lib/site'
import { firstLetters } from '@shared/lib/string-format'
const iconIcons = ['user', 'settings', 'car', 'balloon', 'users', 'users-group', 'apps', 'ghost'] const iconIcons = ['user', 'settings', 'car', 'balloon', 'users', 'users-group', 'apps', 'ghost'];
// Liquid: {% for color in site.colors %} — the iterated keys match site.themeColors // themeColors keys (blue..cyan).
// (blue..cyan), confirmed against the reference output. const colors = site.themeColors;
const colors = site.themeColors
const people8 = people.slice(0, 8) const people8 = people.slice(0, 8);
const people5 = people.slice(0, 5) const people5 = people.slice(0, 5);
const sizes = ['xxs', 'xs', 'sm', 'md', 'lg', 'xl'] const sizes = ['xxs', 'xs', 'sm', 'md', 'lg', 'xl'];
const listSizes = ['xxs', 'xs', 'sm', 'md', 'lg'] const listSizes = ['xxs', 'xs', 'sm', 'md', 'lg'];
const uploadSizes = ['xxs', 'xs', 'sm', 'md', 'lg', 'xl', '2xl'] const uploadSizes = ['xxs', 'xs', 'sm', 'md', 'lg', 'xl', '2xl'];
const statusColors = ['red', 'green', 'blue', 'yellow', 'secondary'] const statusColors = ['red', 'green', 'blue', 'yellow', 'secondary'];
const brands = ['netflix', 'amazon', 'messenger', 'figma', 'twitch'] const brands = ['netflix', 'amazon', 'messenger', 'figma', 'twitch'];
--- ---
<DefaultLayout title="Avatars" pageMenu="base.avatars" pageHeader="Avatars"> <DefaultLayout title="Avatars" pageMenu="base.avatars" pageHeader="Avatars">
<div class="row row-cards"> <div class="row row-cards">
<div class="col-4"> <div class="col-4">
<Card> <Card>
<CardBody> <CardBody>
<CardTitle as="h2">Default avatar</CardTitle> <CardTitle as="h2">Default avatar</CardTitle>
<Avatar /> <Avatar />
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col-4"> <div class="col-4">
<Card> <Card>
<CardBody> <CardBody>
<CardTitle as="h2">Avatar with icon</CardTitle> <CardTitle as="h2">Avatar with icon</CardTitle>
<div class="avatar-list"> <div class="avatar-list">
{iconIcons.map((icon) => <Avatar icon={icon} />)} {iconIcons.map((icon) => <Avatar icon={icon} />)}
</div> </div>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col-4"> <div class="col-4">
<Card> <Card>
<CardBody> <CardBody>
<CardTitle as="h2">Avatar with icon</CardTitle> <CardTitle as="h2">Avatar with icon</CardTitle>
<div class="avatar-list"> <div class="avatar-list">
{colors.map((color) => <Avatar icon="user" color={color} />)} {colors.map((color) => <Avatar icon="user" color={color} />)}
</div> </div>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col-4"> <div class="col-4">
<Card> <Card>
<CardBody> <CardBody>
<CardTitle as="h2">Simple avatar</CardTitle> <CardTitle as="h2">Simple avatar</CardTitle>
<div class="avatar-list"> <div class="avatar-list">
{people8.map((person) => <Avatar person={person} />)} {people8.map((person) => <Avatar person={person} />)}
</div> </div>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col-4"> <div class="col-4">
<Card> <Card>
<CardBody> <CardBody>
<CardTitle as="h2">Avatar placeholder</CardTitle> <CardTitle as="h2">Avatar placeholder</CardTitle>
<div class="avatar-list"> <div class="avatar-list">
{people8.map((person) => <Avatar placeholder={firstLetters(person.full_name)} />)} {people8.map((person) => <Avatar placeholder={firstLetters(person.full_name)} />)}
</div> </div>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col-4"> <div class="col-4">
<Card> <Card>
<CardBody> <CardBody>
<CardTitle as="h2">Avatar shapes</CardTitle> <CardTitle as="h2">Avatar shapes</CardTitle>
<div class="avatar-list"> <div class="avatar-list">
<Avatar /> <Avatar />
<Avatar class="rounded-circle" /> <Avatar class="rounded-circle" />
<Avatar class="rounded-0" /> <Avatar class="rounded-0" />
</div> </div>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col-6"> <div class="col-6">
<Card> <Card>
<CardBody> <CardBody>
<CardTitle as="h2">Avatar sizes</CardTitle> <CardTitle as="h2">Avatar sizes</CardTitle>
<div class="row"> <div class="row">
<div class="col-6"> <div class="col-6">
<div class="avatar-list"> <div class="avatar-list">
{sizes.map((size) => <Avatar size={size} />)} {sizes.map((size) => <Avatar size={size} />)}
</div> </div>
</div> </div>
<div class="col-6"> <div class="col-6">
<div class="avatar-list"> <div class="avatar-list">
{sizes.map((size) => <Avatar placeholder="PK" size={size} />)} {sizes.map((size) => <Avatar placeholder="PK" size={size} />)}
</div> </div>
</div> </div>
</div> </div>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col-6"> <div class="col-6">
<Card> <Card>
<CardBody> <CardBody>
<CardTitle as="h2">Avatar lists</CardTitle> <CardTitle as="h2">Avatar lists</CardTitle>
<div class="row g-3"> <div class="row g-3">
{ {
listSizes.map((size) => ( listSizes.map((size) => (
<> <>
<div class="col-6"> <div class="col-6">
<AvatarList stacked={true} size={size}> <AvatarList stacked={true} size={size}>
{people5.map((person) => ( {people5.map((person) => (
<Avatar person={person} /> <Avatar person={person} />
))} ))}
<Avatar icon="plus" link /> <Avatar icon="plus" link />
</AvatarList> </AvatarList>
</div> </div>
<div class="col-6"> <div class="col-6">
<AvatarList stacked={true} size={size}> <AvatarList stacked={true} size={size}>
{people5.map((person) => ( {people5.map((person) => (
<Avatar person={person} class="rounded-circle" /> <Avatar person={person} class="rounded-circle" />
))} ))}
<Avatar icon="plus" link class="rounded-circle" /> <Avatar icon="plus" link class="rounded-circle" />
</AvatarList> </AvatarList>
</div> </div>
</> </>
)) ))
} }
</div> </div>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col-4"> <div class="col-4">
<Card> <Card>
<CardBody> <CardBody>
<CardTitle as="h2">Avatar placeholder</CardTitle> <CardTitle as="h2">Avatar placeholder</CardTitle>
{uploadSizes.map((size) => <AvatarUpload size={size} />)} {uploadSizes.map((size) => <AvatarUpload size={size} />)}
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col-4"> <div class="col-4">
<Card> <Card>
<CardBody> <CardBody>
<CardTitle as="h2">Avatar statuses</CardTitle> <CardTitle as="h2">Avatar statuses</CardTitle>
{statusColors.map((color, i) => <Avatar personId={i + 1} class="rounded-circle" status={color} />)} {statusColors.map((color, i) => <Avatar personId={i + 1} class="rounded-circle" status={color} />)}
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col-4"> <div class="col-4">
<Card> <Card>
<CardBody> <CardBody>
<CardTitle as="h2">Avatar brands</CardTitle> <CardTitle as="h2">Avatar brands</CardTitle>
{brands.map((brand, i) => <Avatar personId={i + 1} brand={brand} />)} {brands.map((brand, i) => <Avatar personId={i + 1} brand={brand} />)}
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
</div> </div>
</DefaultLayout> </DefaultLayout>
+192 -191
View File
@@ -1,205 +1,206 @@
--- ---
// Liquid: colors = ['default'] + site.colors keys + ['dark', 'light']
import BadgesList from '@ui/BadgesList.astro' // colors = ['default'] + site.colors keys + ['dark', 'light']
import DefaultLayout from '@shared/layouts/DefaultLayout.astro'
import Icon from '@ui/Icon.astro'
import ButtonList from '@ui/ButtonList.astro'
import Card from '@ui/Card.astro'
import CardBody from '@ui/CardBody.astro'
import CardTitle from '@ui/CardTitle.astro'
import DropdownMenu from '@ui/DropdownMenu.astro'
import site from '@data/site.json'
import { ucFirst } from '@shared/lib/string-format'
const colors = ['default', ...Object.keys(site.colors), 'dark', 'light'] import BadgesList from '@ui/BadgesList.astro';
const sizes = ['sm', 'md', 'lg'] import DefaultLayout from '@shared/layouts/DefaultLayout.astro';
import Icon from '@ui/Icon.astro';
import ButtonList from '@ui/ButtonList.astro';
import Card from '@ui/Card.astro';
import CardBody from '@ui/CardBody.astro';
import CardTitle from '@ui/CardTitle.astro';
import DropdownMenu from '@ui/DropdownMenu.astro';
import site from '@data/site.json';
import { ucFirst } from '@shared/lib/string-format';
const colors = ['default', ...Object.keys(site.colors), 'dark', 'light'];
const sizes = ['sm', 'md', 'lg'];
--- ---
<DefaultLayout title="Badges" pageHeader="Badges" pageMenu="base.badges"> <DefaultLayout title="Badges" pageHeader="Badges" pageMenu="base.badges">
<div class="row row-cards"> <div class="row row-cards">
<div class="col-4"> <div class="col-4">
<div class="row row-cards"> <div class="row row-cards">
<div class="col-12"> <div class="col-12">
<Card> <Card>
<CardBody> <CardBody>
<h1>Example heading <span class="badge">New</span></h1> <h1>Example heading <span class="badge">New</span></h1>
<h2>Example heading <span class="badge">New</span></h2> <h2>Example heading <span class="badge">New</span></h2>
<h3>Example heading <span class="badge">New</span></h3> <h3>Example heading <span class="badge">New</span></h3>
<h4>Example heading <span class="badge">New</span></h4> <h4>Example heading <span class="badge">New</span></h4>
<h5>Example heading <span class="badge">New</span></h5> <h5>Example heading <span class="badge">New</span></h5>
<h6>Example heading <span class="badge">New</span></h6> <h6>Example heading <span class="badge">New</span></h6>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col-12"> <div class="col-12">
<Card> <Card>
<CardBody> <CardBody>
<CardTitle>Badge sizes</CardTitle> <CardTitle>Badge sizes</CardTitle>
<div class="space-y"> <div class="space-y">
{ {
sizes.map((size) => ( sizes.map((size) => (
<BadgesList> <BadgesList>
<span class={`badge${size !== 'md' ? ` badge-${size}` : ''}`}>Default</span> <span class={`badge${size !== 'md' ? ` badge-${size}` : ''}`}>Default</span>
<span class={`badge${size !== 'md' ? ` badge-${size}` : ''}`}> <span class={`badge${size !== 'md' ? ` badge-${size}` : ''}`}>
<Icon name="check" /> Left icon <Icon name="check" /> Left icon
</span> </span>
<span class={`badge${size !== 'md' ? ` badge-${size}` : ''}`}> <span class={`badge${size !== 'md' ? ` badge-${size}` : ''}`}>
Right icon Right icon<Icon name="arrow-right" />
<Icon name="arrow-right" /> </span>
</span> <span class={`badge badge-icononly${size !== 'md' ? ` badge-${size}` : ''}`}>
<span class={`badge badge-icononly${size !== 'md' ? ` badge-${size}` : ''}`}> <Icon name="star" type="filled" />
<Icon name="star" type="filled" /> </span>
</span> </BadgesList>
</BadgesList> ))
)) }
} </div>
</div> </CardBody>
</CardBody> </Card>
</Card> </div>
</div> <div class="col-12">
<div class="col-12"> <Card>
<Card> <CardBody>
<CardBody> <CardTitle>Positioned badges</CardTitle>
<CardTitle>Positioned badges</CardTitle>
<ButtonList> <ButtonList>
<button type="button" class="btn">Notifications <span class="badge text-bg-secondary ms-2">4</span></button> <button type="button" class="btn">Notifications <span class="badge text-bg-secondary ms-2">4</span></button>
<button type="button" class="btn"> <button type="button" class="btn">
Inbox Inbox
<span class="badge bg-red badge-notification text-red-fg"> <span class="badge bg-red badge-notification text-red-fg">
9+ 9+
<span class="visually-hidden">unread messages</span> <span class="visually-hidden">unread messages</span>
</span> </span>
</button> </button>
<button type="button" class="btn"> <button type="button" class="btn">
Profile Profile
<span class="badge badge-dot bg-red badge-notification"></span> <span class="badge badge-dot bg-red badge-notification"></span>
</button> </button>
<button type="button" class="btn"> <button type="button" class="btn">
Settings Settings
<span class="badge badge-dot bg-red badge-notification badge-blink"></span> <span class="badge badge-dot bg-red badge-notification badge-blink"></span>
</button> </button>
<button type="button" class="btn btn-icon"> <button type="button" class="btn btn-icon">
<Icon name="bell" /> <Icon name="bell" />
<span class="badge badge-dot bg-red badge-notification badge-blink"></span> <span class="badge badge-dot bg-red badge-notification badge-blink"></span>
</button> </button>
<button type="button" class="btn btn-icon btn-action"> <button type="button" class="btn btn-icon btn-action">
<Icon name="bell" /> <Icon name="bell" />
<span class="badge badge-dot bg-red badge-notification"></span> <span class="badge badge-dot bg-red badge-notification"></span>
</button> </button>
</ButtonList> </ButtonList>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
</div> </div>
</div> </div>
<div class="col-8"> <div class="col-8">
<div class="row row-cards"> <div class="row row-cards">
<div class="col-12"> <div class="col-12">
<Card> <Card>
<CardBody> <CardBody>
<CardTitle>Basic badges</CardTitle> <CardTitle>Basic badges</CardTitle>
<BadgesList> <BadgesList>
{colors.map((color) => <span class={`badge bg-${color} text-${color}-fg`}>{ucFirst(color)}</span>)} {colors.map((color) => <span class={`badge bg-${color} text-${color}-fg`}>{ucFirst(color)}</span>)}
</BadgesList> </BadgesList>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col-12"> <div class="col-12">
<Card> <Card>
<CardBody> <CardBody>
<CardTitle>Light badges</CardTitle> <CardTitle>Light badges</CardTitle>
<BadgesList> <BadgesList>
{colors.map((color) => <span class={`badge bg-${color}-lt`}>{ucFirst(color)}</span>)} {colors.map((color) => <span class={`badge bg-${color}-lt`}>{ucFirst(color)}</span>)}
</BadgesList> </BadgesList>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col-12"> <div class="col-12">
<Card> <Card>
<CardBody> <CardBody>
<CardTitle>Outline badges</CardTitle> <CardTitle>Outline badges</CardTitle>
<BadgesList> <BadgesList>
{colors.map((color) => <span class={`badge badge-outline text-${color}`}>{ucFirst(color)}</span>)} {colors.map((color) => <span class={`badge badge-outline text-${color}`}>{ucFirst(color)}</span>)}
</BadgesList> </BadgesList>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col-12"> <div class="col-12">
<Card> <Card>
<CardBody> <CardBody>
<CardTitle>Badges with icons</CardTitle> <CardTitle>Badges with icons</CardTitle>
<BadgesList> <BadgesList>
{ {
colors.map((color) => ( colors.map((color) => (
<span class={`badge bg-${color} text-${color}-fg`}> <span class={`badge bg-${color} text-${color}-fg`}>
{' '} {' '}
<Icon name="star" type="filled" /> {ucFirst(color)}{' '} <Icon name="star" type="filled" /> {ucFirst(color)}{' '}
</span> </span>
)) ))
} }
</BadgesList> </BadgesList>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
</div> </div>
</div> </div>
<div class="col-sm-6 col-lg-3"><DropdownMenu show badge arrow /></div> <div class="col-sm-6 col-lg-3"><DropdownMenu show badge arrow /></div>
<div class="col-sm-6 col-lg-9"> <div class="col-sm-6 col-lg-9">
<div class="row row-cards"> <div class="row row-cards">
<div class="col-12"> <div class="col-12">
<Card> <Card>
<CardBody> <CardBody>
<ButtonList> <ButtonList>
{ {
colors.map((color, index) => ( colors.map((color, index) => (
<button class="btn"> <button class="btn">
{ucFirst(color)} badge <span class={`badge bg-${color} text-${color}-fg ms-2`}>{index + 1}</span> {ucFirst(color)} badge <span class={`badge bg-${color} text-${color}-fg ms-2`}>{index + 1}</span>
</button> </button>
)) ))
} }
</ButtonList> </ButtonList>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col-12"> <div class="col-12">
<Card> <Card>
<CardBody> <CardBody>
<ButtonList> <ButtonList>
{ {
colors.map((color, index) => ( colors.map((color, index) => (
<button class="btn position-relative"> <button class="btn position-relative">
{ucFirst(color)} badge <span class={`badge bg-${color} text-${color}-fg badge-notification badge-pill`}>{index + 1}</span> {ucFirst(color)} badge{' '}
</button> <span class={`badge bg-${color} text-${color}-fg badge-notification badge-pill`}>{index + 1}</span>
)) </button>
} ))
</ButtonList> }
</CardBody> </ButtonList>
</Card> </CardBody>
</div> </Card>
<div class="col-12"> </div>
<Card> <div class="col-12">
<CardBody> <Card>
<ButtonList> <CardBody>
{ <ButtonList>
colors.map((color) => ( {
<button class="btn position-relative"> colors.map((color) => (
{ucFirst(color)} badge <span class={`badge bg-${color} badge-notification badge-blink`} /> <button class="btn position-relative">
</button> {ucFirst(color)} badge <span class={`badge bg-${color} badge-notification badge-blink`} />
)) </button>
} ))
</ButtonList> }
</CardBody> </ButtonList>
</Card> </CardBody>
</div> </Card>
</div> </div>
</div> </div>
</div> </div>
</div>
</DefaultLayout> </DefaultLayout>
+4 -5
View File
@@ -1,10 +1,9 @@
--- ---
// front matter is inert in the Liquid templates (never referenced) — the // front matter keys unused — empty page header comes from missing `page-header` key.
// empty page header comes from the missing `page-header` key. import DefaultLayout from '@shared/layouts/DefaultLayout.astro';
import DefaultLayout from '@shared/layouts/DefaultLayout.astro' import Empty from '@ui/Empty.astro';
import Empty from '@ui/Empty.astro'
--- ---
<DefaultLayout title="Blank page" pageMenu="base.blank" containerCentered> <DefaultLayout title="Blank page" pageMenu="base.blank" containerCentered>
<Empty buttonText="Add your first client" buttonIcon="plus" illustration="computer-fix.svg" /> <Empty buttonText="Add your first client" buttonIcon="plus" illustration="computer-fix.svg" />
</DefaultLayout> </DefaultLayout>
+183 -193
View File
@@ -1,201 +1,191 @@
--- ---
import DefaultLayout from '@shared/layouts/DefaultLayout.astro' import DefaultLayout from '@shared/layouts/DefaultLayout.astro';
import Icon from '@ui/Icon.astro' import Icon from '@ui/Icon.astro';
import Button from '@ui/Button.astro' import Button from '@ui/Button.astro';
import ButtonList from '@ui/ButtonList.astro' import ButtonList from '@ui/ButtonList.astro';
import Card from '@ui/Card.astro' import Card from '@ui/Card.astro';
import CardHeader from '@ui/CardHeader.astro' import CardHeader from '@ui/CardHeader.astro';
import CardBody from '@ui/CardBody.astro' import CardBody from '@ui/CardBody.astro';
import site from '@data/site.json' import site from '@data/site.json';
type ColorEntry = [string, { title: string; icon?: string }] type ColorEntry = [string, { title: string; icon?: string }];
// Liquid iterates the site.json objects as [key, value] pairs. // site.json objects iterated as [key, value] pairs.
// themeColors/colors entries have no icon — ui/icon.html renders nothing there. // themeColors/colors entries have no icon — Icon renders nothing there.
const themeColors = Object.entries(site.themeColors) as ColorEntry[] const themeColors = Object.entries(site.themeColors) as ColorEntry[];
const colors = Object.entries(site.colors) as ColorEntry[] const colors = Object.entries(site.colors) as ColorEntry[];
const socialColors = Object.entries(site.socialColors) as ColorEntry[] const socialColors = Object.entries(site.socialColors) as ColorEntry[];
const actions = ['edit', 'copy', 'settings', 'clipboard', 'x'] const actions = ['edit', 'copy', 'settings', 'clipboard', 'x'];
const sizes = ['sm', 'md', 'lg', 'xl'] const sizes = ['sm', 'md', 'lg', 'xl'];
--- ---
<DefaultLayout title="Buttons" pageHeader="Buttons" pageMenu="base.buttons"> <DefaultLayout title="Buttons" pageHeader="Buttons" pageMenu="base.buttons">
<div class="row row-cards"> <div class="row row-cards">
<div class="col-md-6"> <div class="col-md-6">
<Card> <Card>
<CardHeader title="Standard Buttons" /> <CardHeader title="Standard Buttons" />
<CardBody> <CardBody>
<ButtonList> <ButtonList>
{ {
themeColors.map(([name, color]) => ( themeColors.map(([name, color]) => (
<a class={`btn btn-${name}`}> <a class={`btn btn-${name}`}>{color.icon && <Icon name={color.icon} />} {color.title}</a>
{color.icon && <Icon name={color.icon} />} {color.title} ))
</a> }
)) </ButtonList>
} </CardBody>
</ButtonList> </Card>
</CardBody> </div>
</Card> <div class="col-md-6">
</div> <Card>
<div class="col-md-6"> <CardHeader title="Outline Buttons" />
<Card> <CardBody>
<CardHeader title="Outline Buttons" /> <ButtonList>
<CardBody> {
<ButtonList> themeColors.map(([name, color]) => (
{ <a class={`btn btn-outline btn-${name}`}>{color.icon && <Icon name={color.icon} />} {color.title}</a>
themeColors.map(([name, color]) => ( ))
<a class={`btn btn-outline btn-${name}`}> }
{color.icon && <Icon name={color.icon} />} {color.title} </ButtonList>
</a> </CardBody>
)) </Card>
} </div>
</ButtonList> <div class="col-md-6">
</CardBody> <Card>
</Card> <CardHeader title="Ghost Buttons" />
</div> <CardBody>
<div class="col-md-6"> <ButtonList>
<Card> {
<CardHeader title="Ghost Buttons" /> themeColors.map(([name, color]) => (
<CardBody> <a class={`btn btn-ghost btn-${name}`}>{color.icon && <Icon name={color.icon} />} {color.title}</a>
<ButtonList> ))
{ }
themeColors.map(([name, color]) => ( </ButtonList>
<a class={`btn btn-ghost btn-${name}`}> </CardBody>
{color.icon && <Icon name={color.icon} />} {color.title} </Card>
</a> </div>
)) <div class="col-md-6">
} <Card>
</ButtonList> <CardHeader title="Square Buttons" />
</CardBody> <CardBody>
</Card> <ButtonList>
</div> {
<div class="col-md-6"> themeColors.map(([name, color]) => (
<Card> <a class={`btn btn-square btn-${name}`}>{color.icon && <Icon name={color.icon} />} {color.title}</a>
<CardHeader title="Square Buttons" /> ))
<CardBody> }
<ButtonList> </ButtonList>
{ </CardBody>
themeColors.map(([name, color]) => ( </Card>
<a class={`btn btn-square btn-${name}`}> </div>
{color.icon && <Icon name={color.icon} />} {color.title} <div class="col-md-6">
</a> <Card>
)) <CardHeader title="Pill Buttons" />
} <CardBody>
</ButtonList> <ButtonList>
</CardBody> {
</Card> themeColors.map(([name, color]) => (
</div> <a class={`btn btn-pill btn-${name}`}>{color.icon && <Icon name={color.icon} />} {color.title}</a>
<div class="col-md-6"> ))
<Card> }
<CardHeader title="Pill Buttons" /> </ButtonList>
<CardBody> </CardBody>
<ButtonList> </Card>
{ </div>
themeColors.map(([name, color]) => ( <div class="col-md-6">
<a class={`btn btn-pill btn-${name}`}> <Card>
{color.icon && <Icon name={color.icon} />} {color.title} <CardHeader title="Extra colors" />
</a> <CardBody>
)) <ButtonList>
} {
</ButtonList> colors.map(([name, color]) => (
</CardBody> <a class={`btn btn-${name}`}>{color.icon && <Icon name={color.icon} />} {color.title}</a>
</Card> ))
</div> }
<div class="col-md-6"> </ButtonList>
<Card> </CardBody>
<CardHeader title="Extra colors" /> </Card>
<CardBody> </div>
<ButtonList> <div class="col-md-6">
{ <Card>
colors.map(([name, color]) => ( <CardHeader title="Icon buttons" />
<a class={`btn btn-${name}`}> <CardBody>
{color.icon && <Icon name={color.icon} />} {color.title} <ButtonList>
</a> {
)) socialColors.map(([name, app]) => (
} <a class={`btn btn-icon btn-${name}`}>{app.icon && <Icon name={app.icon} />}</a>
</ButtonList> ))
</CardBody> }
</Card> </ButtonList>
</div> </CardBody>
<div class="col-md-6"> </Card>
<Card> </div>
<CardHeader title="Icon buttons" /> <div class="col-md-6">
<CardBody> <Card>
<ButtonList> <CardHeader title="Social colors" />
{socialColors.map(([name, app]) => <a class={`btn btn-icon btn-${name}`}>{app.icon && <Icon name={app.icon} />}</a>)} <CardBody>
</ButtonList> <ButtonList>
</CardBody> {
</Card> socialColors.map(([name, app]) => (
</div> <a class={`btn btn-${name}`}>{app.icon && <Icon name={app.icon} />} {app.title}</a>
<div class="col-md-6"> ))
<Card> }
<CardHeader title="Social colors" /> </ButtonList>
<CardBody> </CardBody>
<ButtonList> </Card>
{ </div>
socialColors.map(([name, app]) => ( <div class="col-md-6">
<a class={`btn btn-${name}`}> <Card>
{app.icon && <Icon name={app.icon} />} {app.title} <CardHeader title="Action buttons" />
</a> <CardBody>
)) <div class="btn-actions">
} {
</ButtonList> actions.map((action) => (
</CardBody> <a class="btn btn-action">
</Card> <Icon name={action} />
</div> </a>
<div class="col-md-6"> ))
<Card> }
<CardHeader title="Action buttons" /> </div>
<CardBody> </CardBody>
<div class="btn-actions"> </Card>
{ </div>
actions.map((action) => ( <div class="col-md-6">
<a class="btn btn-action"> <Card>
<Icon name={action} /> <CardHeader title="Buttons with icon" />
</a> <CardBody>
)) <ButtonList>
} <a class="btn btn-animate-icon">Save <Icon name="arrow-right" class="icon-end" /></a>
</div> <a class="btn btn-animate-icon btn-animate-icon-rotate"><Icon name="plus" /> Add</a>
</CardBody> <a class="btn btn-animate-icon btn-animate-icon-shake"><Icon name="bell" /> Notifications</a>
</Card> <a class="btn btn-animate-icon btn-animate-icon-rotate"><Icon name="settings" /> Settings</a>
</div> <a class="btn btn-animate-icon btn-animate-icon-pulse"><Icon name="heart" /> Love</a>
<div class="col-md-6"> <a class="btn btn-animate-icon btn-animate-icon-rotate"><Icon name="x" /> Close</a>
<Card> <a class="btn btn-animate-icon btn-animate-icon-tada"><Icon name="check" /> Confirm</a>
<CardHeader title="Buttons with icon" /> <a class="btn btn-animate-icon">Next <Icon name="chevron-right" class="icon-end" /></a>
<CardBody> <a class="btn btn-animate-icon btn-animate-icon-move-start"><Icon name="chevron-left" /> Previous</a>
<ButtonList> </ButtonList>
<a class="btn btn-animate-icon">Save <Icon name="arrow-right" class="icon-end" /></a> </CardBody>
<a class="btn btn-animate-icon btn-animate-icon-rotate"><Icon name="plus" /> Add</a> </Card>
<a class="btn btn-animate-icon btn-animate-icon-shake"><Icon name="bell" /> Notifications</a> </div>
<a class="btn btn-animate-icon btn-animate-icon-rotate"><Icon name="settings" /> Settings</a> <div class="col-md-6">
<a class="btn btn-animate-icon btn-animate-icon-pulse"><Icon name="heart" /> Love</a> <Card>
<a class="btn btn-animate-icon btn-animate-icon-rotate"><Icon name="x" /> Close</a> <CardHeader title="Buttons size" />
<a class="btn btn-animate-icon btn-animate-icon-tada"><Icon name="check" /> Confirm</a> <CardBody>
<a class="btn btn-animate-icon">Next <Icon name="chevron-right" class="icon-end" /></a> <div class="space-y">
<a class="btn btn-animate-icon btn-animate-icon-move-start"><Icon name="chevron-left" /> Previous</a> {
</ButtonList> sizes.map((size) => (
</CardBody> <ButtonList>
</Card> <Button size={size} text="Button" />
</div> <Button size={size} icon="star" iconOnly />
<div class="col-md-6"> <Button size={size} icon="star" text="Button" />
<Card> <Button size={size} iconEnd="star" text="Button" />
<CardHeader title="Buttons size" /> </ButtonList>
<CardBody> ))
<div class="space-y"> }
{ </div>
sizes.map((size) => ( </CardBody>
<ButtonList> </Card>
<Button size={size} text="Button" /> </div>
<Button size={size} icon="star" iconOnly /> </div>
<Button size={size} icon="star" text="Button" />
<Button size={size} iconEnd="star" text="Button" />
</ButtonList>
))
}
</div>
</CardBody>
</Card>
</div>
</div>
</DefaultLayout> </DefaultLayout>
+5 -12
View File
@@ -1,18 +1,11 @@
--- ---
import ProseLayout from '@shared/layouts/ProseLayout.astro' import ProseLayout from '@shared/layouts/ProseLayout.astro';
import { renderMarkdown } from '@shared/lib/render-markdown' import { renderMarkdown } from '@shared/lib/render-markdown';
// Liquid: {{ changelog | renderContent: "md" }} — `changelog` is the raw import changelog from '../../core/CHANGELOG.md?raw';
// content of core/CHANGELOG.md (shared/e11ty/data.mjs), rendered by
// Eleventy's markdown-it. renderMarkdown() mirrors that engine exactly
// (markdown-it 14.3.0, { html: true }, indented code blocks disabled) —
// output verified byte-identical to the reference build. Injected via
// set:html: the fragment contains entities and a literal `{$prefix}` token
// that must not be re-escaped or parsed as JSX.
import changelog from '../../core/CHANGELOG.md?raw'
const changelogHtml = renderMarkdown(changelog) const changelogHtml = renderMarkdown(changelog);
--- ---
<ProseLayout title="Changelog" pageMenu="changelog"> <ProseLayout title="Changelog" pageMenu="changelog">
<Fragment set:html={changelogHtml} /> <Fragment set:html={changelogHtml} />
</ProseLayout> </ProseLayout>
+29 -26
View File
@@ -1,31 +1,34 @@
--- ---
// Liquid: {% for color in site.colors %} — forloop.index (1-based) → id, import DefaultLayout from '@shared/layouts/DefaultLayout.astro';
// color[1].hex → value. import Card from '@ui/Card.astro';
import DefaultLayout from '@shared/layouts/DefaultLayout.astro' import CardBody from '@ui/CardBody.astro';
import Card from '@ui/Card.astro' import CardTitle from '@ui/CardTitle.astro';
import CardBody from '@ui/CardBody.astro' import Colorpicker from '@ui/Colorpicker.astro';
import CardTitle from '@ui/CardTitle.astro' import site from '@data/site.json';
import Colorpicker from '@ui/Colorpicker.astro'
import site from '@data/site.json'
const colors = Object.values(site.colors) as { hex: string }[] const colors = Object.values(site.colors) as { hex: string }[];
--- ---
<DefaultLayout title="Color picker" pageHeader="Color picker" pageMenu="plugins.colorpicker" pageLibs={['coloris.js']}> <DefaultLayout
<Card> title="Color picker"
<CardBody> pageHeader="Color picker"
<CardTitle>Basic</CardTitle> pageMenu="plugins.colorpicker"
<div class="row g-3"> pageLibs={['coloris.js']}
{ >
colors.map((color, index) => ( <Card>
<div class="col-2"> <CardBody>
<div> <CardTitle>Basic</CardTitle>
<Colorpicker value={color.hex} id={index + 1} format="hex" /> <div class="row g-3">
</div> {
</div> colors.map((color, index) => (
)) <div class="col-2">
} <div>
</div> <Colorpicker value={color.hex} id={index + 1} format="hex" />
</CardBody> </div>
</Card> </div>
))
}
</div>
</CardBody>
</Card>
</DefaultLayout> </DefaultLayout>
+231 -219
View File
@@ -1,235 +1,247 @@
--- ---
import DefaultLayout from '@shared/layouts/DefaultLayout.astro' import DefaultLayout from '@shared/layouts/DefaultLayout.astro';
import FormGroup from '@ui/FormGroup.astro' import CaptureScript from '@shared/components/CaptureScript.astro';
import Card from '@ui/Card.astro' import FormGroup from '@ui/FormGroup.astro';
import CardBody from '@ui/CardBody.astro' import Card from '@ui/Card.astro';
import Avatar from '@ui/Avatar.astro' import CardBody from '@ui/CardBody.astro';
import CardTitle from '@ui/CardTitle.astro' import Avatar from '@ui/Avatar.astro';
import site from '@data/site.json' import CardTitle from '@ui/CardTitle.astro';
import site from '@data/site.json';
type ColorEntry = [string, { title: string; hex: string; abbr?: string; icon?: string }] type ColorEntry = [string, { title: string; hex: string; abbr?: string; icon?: string }];
// Liquid iterates the site.json objects as [key, value] pairs. const colors = Object.entries(site.colors) as ColorEntry[];
const colors = Object.entries(site.colors) as ColorEntry[] const lightColors = Object.entries(site.lightColors) as ColorEntry[];
const lightColors = Object.entries(site.lightColors) as ColorEntry[] const grayColors = Object.entries(site.grayColors) as ColorEntry[];
const grayColors = Object.entries(site.grayColors) as ColorEntry[] const socialColors = Object.entries(site.socialColors) as ColorEntry[];
const socialColors = Object.entries(site.socialColors) as ColorEntry[]
// Liquid: colors keys + inverted, white, transparent (pushed before the gradient loops). const gradientColors = [...Object.keys(site.colors), 'inverted', 'white', 'transparent'];
const gradientColors = [...Object.keys(site.colors), 'inverted', 'white', 'transparent']
--- ---
<DefaultLayout title="Colors" pageHeader="Colors" pageMenu="base.colors"> <DefaultLayout title="Colors" pageHeader="Colors" pageMenu="base.colors">
<div class="row row-cards"> <div class="row row-cards">
<div class="col-3"> <div class="col-3">
<Card> <Card>
<CardBody> <CardBody>
<div class="row g-3"> <div class="row g-3">
{ {
colors.map(([name, color]) => ( colors.map(([name, color]) => (
<div class="col-12"> <div class="col-12">
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col-auto"> <div class="col-auto">
<Avatar square class={`bg-${name} text-${name}-fg`} placeholder={color.abbr} /> <Avatar square class={`bg-${name} text-${name}-fg`} placeholder={color.abbr} />
</div> </div>
<div class="col"> <div class="col">
{color.title} {color.title}
<br /> <br />
<code>{color.hex}</code> <code>{color.hex}</code>
</div> </div>
</div> </div>
</div> </div>
)) ))
} }
</div> </div>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col-3"> <div class="col-3">
<Card> <Card>
<CardBody> <CardBody>
<div class="row g-3"> <div class="row g-3">
{ {
lightColors.map(([name, color]) => ( lightColors.map(([name, color]) => (
<div class="col-12"> <div class="col-12">
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col-auto"> <div class="col-auto">
<Avatar square class={`bg-${name}-lt text-${name}-lt-fg`} placeholder={color.abbr} /> <Avatar square class={`bg-${name}-lt text-${name}-lt-fg`} placeholder={color.abbr} />
</div> </div>
<div class="col"> <div class="col">
{color.title} {color.title}
<br /> <br />
<code>{color.hex}</code> <code>{color.hex}</code>
</div> </div>
</div> </div>
</div> </div>
)) ))
} }
</div> </div>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col-3"> <div class="col-3">
<Card> <Card>
<CardBody> <CardBody>
<div class="row g-3"> <div class="row g-3">
{ {
grayColors.map(([name, color]) => ( grayColors.map(([name, color]) => (
<div class="col-12"> <div class="col-12">
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col-auto"> <div class="col-auto">
<Avatar square class={`bg-${name} text-${name}-fg`} placeholder={color.abbr} /> <Avatar square class={`bg-${name} text-${name}-fg`} placeholder={color.abbr} />
</div> </div>
<div class="col"> <div class="col">
{color.title} {color.title}
<br /> <br />
<code>{color.hex}</code> <code>{color.hex}</code>
</div> </div>
</div> </div>
</div> </div>
)) ))
} }
</div> </div>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col-3"> <div class="col-3">
<Card> <Card>
<CardBody> <CardBody>
<div class="row g-3"> <div class="row g-3">
{ {
socialColors.map(([name, color]) => ( socialColors.map(([name, color]) => (
<div class="col-12"> <div class="col-12">
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col-auto"> <div class="col-auto">
<Avatar square class={`bg-${name} text-${name}-fg`} icon={color.icon} /> <Avatar square class={`bg-${name} text-${name}-fg`} icon={color.icon} />
</div> </div>
<div class="col"> <div class="col">
{color.title} {color.title}
<br /> <br />
<code>{color.hex}</code> <code>{color.hex}</code>
</div> </div>
</div> </div>
</div> </div>
)) ))
} }
</div> </div>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col-12"> <div class="col-12">
<div class="row"> <div class="row">
<div class="col-6"> <div class="col-6">
<Card> <Card>
<CardBody> <CardBody>
<CardTitle>Gradient</CardTitle> <CardTitle>Gradient</CardTitle>
<form action=""> <form action="">
<div class="row g-4"> <div class="row g-4">
<div class="col"> <div class="col">
<FormGroup label="From" class=""> <FormGroup label="From" class="">
<select class="form-select" name="color-from"> <select class="form-select" name="color-from">
{gradientColors.map((color) => <option value={color}>{color}</option>)} {gradientColors.map((color) => <option value={color}>{color}</option>)}
</select> </select>
</FormGroup> </FormGroup>
<FormGroup label="To" class="mt-3"> <FormGroup label="To" class="mt-3">
<select class="form-select" name="color-to"> <select class="form-select" name="color-to">
{ {
gradientColors.map((color) => ( gradientColors.map((color) => (
<option value={color} selected={color === 'transparent'}> <option value={color} selected={color === 'transparent'}>
{color} {color}
</option> </option>
)) ))
} }
</select> </select>
</FormGroup> </FormGroup>
</div> </div>
<div class="col"> <div class="col">
<FormGroup label="Via" class=""> <FormGroup label="Via" class="">
<select class="form-select" name="color-via"> <select class="form-select" name="color-via">
<option></option> <option></option>
{gradientColors.map((color) => <option value={color}>{color}</option>)} {gradientColors.map((color) => <option value={color}>{color}</option>)}
</select> </select>
</FormGroup> </FormGroup>
<FormGroup label="Direction" class="mt-3"> <FormGroup label="Direction" class="mt-3">
<select class="form-select" name="color-direction"> <select class="form-select" name="color-direction">
<option value="to-t">to top</option> <option value="to-t">to top</option>
<option value="to-te">to top right</option> <option value="to-te">to top right</option>
<option value="to-r" selected>to right</option> <option value="to-r" selected>to right</option>
<option value="to-be">to bottom right</option> <option value="to-be">to bottom right</option>
<option value="to-b">to bottom</option> <option value="to-b">to bottom</option>
<option value="to-bs">to bottom left</option> <option value="to-bs">to bottom left</option>
<option value="to-s">to left</option> <option value="to-s">to left</option>
<option value="to-ts">to top left</option> <option value="to-ts">to top left</option>
</select> </select>
</FormGroup> </FormGroup>
</div> </div>
<div class="col-12"> <div class="col-12">
<div class="border rounded bg-pattern-transparent overflow-hidden"> <div class="border rounded bg-pattern-transparent overflow-hidden">
<div id="gradient-preview" class="border rounded bg-gradient bg-gradient-from-primary bg-gradient-to-transparent"> <div
<div class="px-4 py-5"></div> id="gradient-preview"
</div> class="border rounded bg-gradient bg-gradient-from-primary bg-gradient-to-transparent"
</div> >
</div> <div class=" px-4 py-5"></div>
</div> </div>
</form> </div>
</CardBody> </div>
</Card> </div>
</div> </form>
<div class="col-6"> </CardBody>
<Card> </Card>
<CardBody> </div>
<div class="row"> <div class="col-6">
<div class="col"> <Card>
<div class="space-y"> <CardBody>
{gradientColors.map((color) => <div class={`border rounded bg-gradient bg-gradient-from-${color} bg-gradient-to-transparent px-4 py-2`} />)} <div class="row">
</div> <div class="col">
</div> <div class="space-y">
<div class="col"> {
<div class="space-y"> gradientColors.map((color) => (
{gradientColors.map((color) => <div class={`border rounded bg-gradient bg-gradient-to-${color} bg-gradient-from-transparent px-4 py-2`} />)} <div class={`border rounded bg-gradient bg-gradient-from-${color} bg-gradient-to-transparent px-4 py-2`} />
</div> ))
</div> }
</div> </div>
</CardBody> </div>
</Card> <div class="col">
</div> <div class="space-y">
</div> {
</div> gradientColors.map((color) => (
</div> <div class={`border rounded bg-gradient bg-gradient-to-${color} bg-gradient-from-transparent px-4 py-2`} />
))
}
</div>
</div>
</div>
</CardBody>
</Card>
</div>
</div>
</div>
</div>
<!-- BEGIN GRADIENT SCRIPT --> <CaptureScript>
<script is:inline> <!-- BEGIN GRADIENT SCRIPT -->
document.addEventListener('DOMContentLoaded', function () { <script is:inline>
var gradientPreview = document.getElementById('gradient-preview') document.addEventListener('DOMContentLoaded', function () {
var colorFrom = document.querySelector('[name="color-from"]') var gradientPreview = document.getElementById('gradient-preview');
var colorTo = document.querySelector('[name="color-to"]') var colorFrom = document.querySelector('[name="color-from"]');
var colorVia = document.querySelector('[name="color-via"]') var colorTo = document.querySelector('[name="color-to"]');
var colorDirection = document.querySelector('[name="color-direction"]') var colorVia = document.querySelector('[name="color-via"]');
var colorDirection = document.querySelector('[name="color-direction"]');
function updateGradient() { function updateGradient() {
var from = colorFrom.value var from = colorFrom.value;
var to = colorTo.value var to = colorTo.value;
var via = colorVia.value var via = colorVia.value;
var direction = colorDirection.value var direction = colorDirection.value;
var gradient = 'bg-gradient bg-gradient-from-' + from + ' bg-gradient-to-' + to var gradient = 'bg-gradient bg-gradient-from-' + from + ' bg-gradient-to-' + to;
if (via) { if (via) {
gradient += ' bg-gradient-via-' + via gradient += ' bg-gradient-via-' + via;
} }
gradient += ' bg-gradient-' + direction gradient += ' bg-gradient-' + direction;
gradientPreview.className = gradient gradientPreview.className = gradient;
} }
colorFrom.addEventListener('change', updateGradient) colorFrom.addEventListener('change', updateGradient);
colorTo.addEventListener('change', updateGradient) colorTo.addEventListener('change', updateGradient);
colorVia.addEventListener('change', updateGradient) colorVia.addEventListener('change', updateGradient);
colorDirection.addEventListener('change', updateGradient) colorDirection.addEventListener('change', updateGradient);
updateGradient() updateGradient();
}) });
</script> </script>
<!-- END GRADIENT SCRIPT --> <!-- END GRADIENT SCRIPT -->
</CaptureScript>
</DefaultLayout> </DefaultLayout>
+314 -309
View File
@@ -1,332 +1,337 @@
--- ---
import CardActions from '@ui/CardActions.astro' import CardActions from '@ui/CardActions.astro';
import DefaultLayout from '@shared/layouts/DefaultLayout.astro' import DefaultLayout from '@shared/layouts/DefaultLayout.astro';
import Avatar from '@ui/Avatar.astro' import Avatar from '@ui/Avatar.astro';
import Trending from '@ui/Trending.astro' import Trending from '@ui/Trending.astro';
import CardDropdown from '@ui/CardDropdown.astro' import CardDropdown from '@ui/CardDropdown.astro';
import Card from '@ui/Card.astro' import Card from '@ui/Card.astro';
import CardHeader from '@ui/CardHeader.astro' import CardHeader from '@ui/CardHeader.astro';
import CardBody from '@ui/CardBody.astro' import CardBody from '@ui/CardBody.astro';
import NavSegmented from '@ui/NavSegmented.astro' import NavSegmented from '@ui/NavSegmented.astro';
import Chart from '@ui/Chart.astro' import Chart from '@ui/Chart.astro';
import SwitchIcon from '@ui/SwitchIcon.astro' import SwitchIcon from '@ui/SwitchIcon.astro';
import cryptoCurrencies from '@data/crypto-currencies.json' import cryptoCurrencies from '@data/crypto-currencies.json';
import cryptoMarkets from '@data/crypto-markets.json' import cryptoMarkets from '@data/crypto-markets.json';
import cryptoOrders from '@data/crypto-orders.json' import cryptoOrders from '@data/crypto-orders.json';
import { parseCurrency, roundTo } from '@shared/lib/string-format' import { parseCurrency, roundTo } from '@shared/lib/string-format';
interface Currency { interface Currency {
'symbol': string symbol: string;
'price': string price: string;
'p24h': number p24h: number;
'volume-24h': string 'volume-24h': string;
} }
const currencies = cryptoCurrencies as Currency[] const currencies = cryptoCurrencies as Currency[];
const find = (symbol: string) => currencies.find((c) => c.symbol === symbol)! const find = (symbol: string) => currencies.find((c) => c.symbol === symbol)!;
const btc = find('BTC') const btc = find('BTC');
const ltc = find('LTC') const ltc = find('LTC');
const eth = find('ETH') const eth = find('ETH');
const xmr = find('XMR') const xmr = find('XMR');
const btcBalance = 2.3 const btcBalance = 2.3;
const btcPriceNum = parseCurrency(btc.price) const btcPriceNum = parseCurrency(btc.price);
const totalUsd = Math.round(btcPriceNum * btcBalance).toLocaleString('en-US') const totalUsd = Math.round(btcPriceNum * btcBalance).toLocaleString('en-US');
// Liquid `divided_by` then `round: 8` (trailing zeros dropped by number output) // divided_by then round to 8 decimals (trailing zeros dropped in output)
const ltcBtc = roundTo(parseCurrency(ltc.price) / btcPriceNum) const ltcBtc = roundTo(parseCurrency(ltc.price) / btcPriceNum);
const ethBtc = roundTo(parseCurrency(eth.price) / btcPriceNum) const ethBtc = roundTo(parseCurrency(eth.price) / btcPriceNum);
const xmrBtc = roundTo(parseCurrency(xmr.price) / btcPriceNum) const xmrBtc = roundTo(parseCurrency(xmr.price) / btcPriceNum);
const markets = (cryptoMarkets as { coin: string; price: string; volume: string; change: string }[]).slice(0, 10) const markets = (cryptoMarkets as { coin: string; price: string; volume: string; change: string }[]).slice(0, 10);
const orders = cryptoOrders as { sell_orders: { price: string; btc: string; sum: string }[]; buy_orders: { price: string; btc: string; sum: string }[] } const orders = cryptoOrders as { sell_orders: { price: string; btc: string; sum: string }[]; buy_orders: { price: string; btc: string; sum: string }[] };
const operationCurrencies = currencies.slice(0, 20) const operationCurrencies = currencies.slice(0, 20);
--- ---
<DefaultLayout title="Crypto Dashboard" pageHeader="Crypto Dashboard" pageMenu="dashboards.crypto" pageLibs={['apexcharts']}> <DefaultLayout
<div class="row row-cards"> title="Crypto Dashboard"
<div class="col-12"> pageHeader="Crypto Dashboard"
<div class="row row-cards"> pageMenu="dashboards.crypto"
<div class="col-12 col-md-6 col-xxl"> pageLibs={['apexcharts']}
<Card> >
<CardBody> <div class="row row-cards">
<div class="row"> <div class="col-12">
<div class="col mt-0"> <div class="row row-cards">
<h5 class="card-title">Total balance</h5> <div class="col-12 col-md-6 col-xxl">
</div> <Card>
<div class="col-auto"> <CardBody>
<Avatar icon="currency-dollar" /> <div class="row">
</div> <div class="col mt-0">
</div> <h5 class="card-title">Total balance</h5>
<div class="mb-1"> </div>
<span class="h3">${totalUsd}</span> <div class="col-auto">
<span class="text-muted">{btcBalance} {btc.symbol}</span> <Avatar icon="currency-dollar" />
</div> </div>
<div class="mb-0"> </div>
<Trending value={btc.p24h} /> <div class="mb-1">
<span class="text-muted">Since last week</span> <span class="h3">${totalUsd}</span>
</div> <span class="text-muted">{btcBalance} {btc.symbol}</span>
</CardBody> </div>
</Card> <div class="mb-0">
</div> <Trending value={btc.p24h} />
<span class="text-muted">Since last week</span>
</div>
</CardBody>
</Card>
</div>
<div class="col-12 col-md-6 col-xxl"> <div class="col-12 col-md-6 col-xxl">
<Card> <Card>
<CardBody> <CardBody>
<div class="row"> <div class="row">
<div class="col mt-0"> <div class="col mt-0">
<h5 class="card-title">USD/BTC</h5> <h5 class="card-title">USD/BTC</h5>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<Avatar icon="currency-bitcoin" /> <Avatar icon="currency-bitcoin" />
</div> </div>
</div> </div>
<div class="mb-1"> <div class="mb-1">
<span class="h3">{btc.price}</span> <span class="h3">{btc.price}</span>
<span class="text-muted">{btc.price}</span> <span class="text-muted">{btc.price}</span>
</div> </div>
<div class="mb-0"> <div class="mb-0">
<span class="text-muted">Volume: {btc['volume-24h']}</span> <span class="text-muted">Volume: {btc['volume-24h']}</span>
</div> </div>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col-12 col-md-6 col-xxl"> <div class="col-12 col-md-6 col-xxl">
<Card> <Card>
<CardBody> <CardBody>
<div class="row"> <div class="row">
<div class="col mt-0"> <div class="col mt-0">
<h5 class="card-title">LTC/BTC</h5> <h5 class="card-title">LTC/BTC</h5>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<Avatar icon="currency-litecoin" /> <Avatar icon="currency-litecoin" />
</div> </div>
</div> </div>
<div class="mb-1"> <div class="mb-1">
<span class="h3">{ltcBtc}</span> <span class="h3">{ltcBtc}</span>
<span class="text-muted">{ltc.price}</span> <span class="text-muted">{ltc.price}</span>
</div> </div>
<div class="mb-0"> <div class="mb-0">
<span class="text-muted">Volume: {ltc['volume-24h'].replace('$', '')}</span> <span class="text-muted">Volume: {ltc['volume-24h'].replace('$', '')}</span>
</div> </div>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col-12 col-md-6 col-xxl"> <div class="col-12 col-md-6 col-xxl">
<Card> <Card>
<CardBody> <CardBody>
<div class="row"> <div class="row">
<div class="col mt-0"> <div class="col mt-0">
<h5 class="card-title">ETH/BTC</h5> <h5 class="card-title">ETH/BTC</h5>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<Avatar icon="currency-ethereum" /> <Avatar icon="currency-ethereum" />
</div> </div>
</div> </div>
<div class="mb-1"> <div class="mb-1">
<span class="h3">{ethBtc}</span> <span class="h3">{ethBtc}</span>
<span class="text-muted">{eth.price}</span> <span class="text-muted">{eth.price}</span>
</div> </div>
<div class="mb-0"> <div class="mb-0">
<span class="text-muted">Volume: {eth['volume-24h'].replace('$', '')}</span> <span class="text-muted">Volume: {eth['volume-24h'].replace('$', '')}</span>
</div> </div>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col-12 col-md-6 col-xxl"> <div class="col-12 col-md-6 col-xxl">
<Card> <Card>
<CardBody> <CardBody>
<div class="row"> <div class="row">
<div class="col mt-0"> <div class="col mt-0">
<h5 class="card-title">XMR/BTC</h5> <h5 class="card-title">XMR/BTC</h5>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<Avatar icon="currency-monero" /> <Avatar icon="currency-monero" />
</div> </div>
</div> </div>
<div class="mb-1"> <div class="mb-1">
<span class="h3">{xmrBtc}</span> <span class="h3">{xmrBtc}</span>
<span class="text-muted">{xmr.price}</span> <span class="text-muted">{xmr.price}</span>
</div> </div>
<div class="mb-0"> <div class="mb-0">
<span class="text-muted">Volume: {xmr['volume-24h'].replace('$', '')}</span> <span class="text-muted">Volume: {xmr['volume-24h'].replace('$', '')}</span>
</div> </div>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
</div> </div>
</div> </div>
<div class="col-12"> <div class="col-12">
<div class="row row-cards"> <div class="row row-cards">
<div class="col-12 col-lg-5"> <div class="col-12 col-lg-5">
<Card> <Card>
<CardHeader> <CardHeader>
<h5 class="card-title mb-0">Markets</h5> <h5 class="card-title mb-0">Markets</h5>
<CardActions> <CardActions>
<CardDropdown /> <CardDropdown />
</CardActions> </CardActions>
</CardHeader> </CardHeader>
<table class="table card-table table-striped"> <table class="table card-table table-striped">
<thead> <thead>
<tr> <tr>
<th> <th>
<SwitchIcon icon="star" iconBColor="yellow" variant="slide-up" /> <SwitchIcon icon="star" iconBColor="yellow" variant="slide-up" />
</th> </th>
<th>Coin</th> <th>Coin</th>
<th>Price</th> <th>Price</th>
<th class="d-none d-xl-table-cell">Volume</th> <th class="d-none d-xl-table-cell">Volume</th>
<th class="d-none d-xl-table-cell text-end">Change</th> <th class="d-none d-xl-table-cell text-end">Change</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{ {
markets.map((market) => ( markets.map((market) => (
<tr> <tr>
<td> <td>
<SwitchIcon icon="star" iconBColor="yellow" variant="slide-up" /> <SwitchIcon icon="star" iconBColor="yellow" variant="slide-up" />
</td> </td>
<td>{market.coin}</td> <td>{market.coin}</td>
<td class="">{market.price}</td> <td class="">{market.price}</td>
<td class="d-none d-xl-table-cell">{market.volume}</td> <td class="d-none d-xl-table-cell">{market.volume}</td>
<td class="d-none d-xl-table-cell text-end"> <td class="d-none d-xl-table-cell text-end">
<Trending value={market.change as unknown as number} /> <Trending value={market.change as unknown as number} />
</td> </td>
</tr> </tr>
)) ))
} }
</tbody> </tbody>
</table> </table>
</Card> </Card>
</div> </div>
<div class="col-12 col-lg-7"> <div class="col-12 col-lg-7">
<Card> <Card>
<CardHeader> <CardHeader>
<h5 class="card-title mb-0">LTC/BTC</h5> <h5 class="card-title mb-0">LTC/BTC</h5>
<CardActions> <CardActions>
<NavSegmented items={['1m', '5m', '30m', '1h', '1d']} name="timeframe" size="sm" default={2} /> <NavSegmented items={['1m', '5m', '30m', '1h', '1d']} name="timeframe" size="sm" default={2} />
</CardActions> </CardActions>
</CardHeader> </CardHeader>
<CardBody> <CardBody>
<Chart chartId="dashboard-crypto-candlestick" height={28} /> <Chart chartId="dashboard-crypto-candlestick" height={28} />
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
</div> </div>
</div> </div>
<div class="col-12"> <div class="col-12">
<div class="row row-cards"> <div class="row row-cards">
<div class="col-12 col-lg-12 col-xxl-3"> <div class="col-12 col-lg-12 col-xxl-3">
<Card> <Card>
<CardHeader> <CardHeader>
<h5 class="card-title mb-0">Operations</h5> <h5 class="card-title mb-0">Operations</h5>
<CardActions> <CardActions>
<NavSegmented items={['Buy', 'Sell', 'Send']} name="operations" size="sm" /> <NavSegmented items={['Buy', 'Sell', 'Send']} name="operations" size="sm" />
</CardActions> </CardActions>
</CardHeader> </CardHeader>
<CardBody> <CardBody>
<p>Place new order:</p> <p>Place new order:</p>
<div class="input-group mb-3"> <div class="input-group mb-3">
<label class="input-group-text">Amount</label> <label class="input-group-text">Amount</label>
<select class="form-select"> <select class="form-select">
{ {
operationCurrencies.map((currency, i) => ( operationCurrencies.map((currency, i) => (
<option value={currency.symbol} selected={i === 0}> <option value={currency.symbol} selected={i === 0}>
{currency.symbol} {currency.symbol}
</option> </option>
)) ))
} }
</select> </select>
<input type="text" class="form-control" value="0.25" /> <input type="text" class="form-control" value="0.25" />
</div> </div>
<div class="input-group mb-3"> <div class="input-group mb-3">
<label class="input-group-text">Price</label> <label class="input-group-text">Price</label>
<input type="text" class="form-control" readonly="" value="23,077.05" /> <input type="text" class="form-control" readonly="" value="23,077.05" />
<label class="input-group-text">$</label> <label class="input-group-text">$</label>
</div> </div>
<div class="input-group mb-3"> <div class="input-group mb-3">
<label class="input-group-text">Total</label> <label class="input-group-text">Total</label>
<input type="text" class="form-control" readonly="" value="5,769.27" /> <input type="text" class="form-control" readonly="" value="5,769.27" />
<label class="input-group-text">$</label> <label class="input-group-text">$</label>
</div> </div>
<div class="d-grid"> <div class="d-grid">
<button type="button" class="btn btn-primary mb-3">Process to wallet</button> <button type="button" class="btn btn-primary mb-3">Process to wallet</button>
</div> </div>
<p class="text-muted mb-0 small">The final amount could change depending on current market conditions.</p> <p class="text-muted mb-0 small">The final amount could change depending on current market conditions.</p>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col-12 col-lg-6 col-xxl"> <div class="col-12 col-lg-6 col-xxl">
<Card> <Card>
<CardHeader> <CardHeader>
<h5 class="card-title mb-0">Sell Orders</h5> <h5 class="card-title mb-0">Sell Orders</h5>
<CardActions> <CardActions>
<button class="btn btn-sm">View all</button> <button class="btn btn-sm">View all</button>
</CardActions> </CardActions>
</CardHeader> </CardHeader>
<table class="table card-table table-striped"> <table class="table card-table table-striped">
<thead> <thead>
<tr> <tr>
<th>Price</th> <th>Price</th>
<th class="d-none d-xl-table-cell">BTC</th> <th class="d-none d-xl-table-cell">BTC</th>
<th>Sum(BTC)</th> <th>Sum(BTC)</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{ {
orders.sell_orders.map((order) => ( orders.sell_orders.map((order) => (
<tr> <tr>
<td>{order.price}</td> <td>{order.price}</td>
<td class="d-none d-xl-table-cell">{order.btc}</td> <td class="d-none d-xl-table-cell">{order.btc}</td>
<td>{order.sum}</td> <td>{order.sum}</td>
</tr> </tr>
)) ))
} }
</tbody> </tbody>
</table> </table>
</Card> </Card>
</div> </div>
<div class="col-12 col-lg-6 col-xxl"> <div class="col-12 col-lg-6 col-xxl">
<Card> <Card>
<CardHeader> <CardHeader>
<h5 class="card-title mb-0">Buy Orders</h5> <h5 class="card-title mb-0">Buy Orders</h5>
<CardActions> <CardActions>
<button class="btn btn-sm">View all</button> <button class="btn btn-sm">View all</button>
</CardActions> </CardActions>
</CardHeader> </CardHeader>
<table class="table card-table table-striped"> <table class="table card-table table-striped">
<thead> <thead>
<tr> <tr>
<th>Price</th> <th>Price</th>
<th class="d-none d-xl-table-cell">BTC</th> <th class="d-none d-xl-table-cell">BTC</th>
<th>Sum(BTC)</th> <th>Sum(BTC)</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{ {
orders.buy_orders.map((order) => ( orders.buy_orders.map((order) => (
<tr> <tr>
<td>{order.price}</td> <td>{order.price}</td>
<td class="d-none d-xl-table-cell">{order.btc}</td> <td class="d-none d-xl-table-cell">{order.btc}</td>
<td>{order.sum}</td> <td>{order.sum}</td>
</tr> </tr>
)) ))
} }
</tbody> </tbody>
</table> </table>
</Card> </Card>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</DefaultLayout> </DefaultLayout>
+191 -195
View File
@@ -1,212 +1,208 @@
--- ---
import Subheader from '@ui/Subheader.astro'
import ButtonGroup from '@ui/ButtonGroup.astro'
import DefaultLayout from '@shared/layouts/DefaultLayout.astro'
import Button from '@ui/Button.astro'
import Icon from '@ui/Icon.astro'
import Card from '@ui/Card.astro'
import Offcanvas from '@ui/Offcanvas.astro'
import Progress from '@ui/Progress.astro'
import Modal from '@shared/components/modals/Modal.astro'
import NewEmailModalContent from '@shared/components/modals/NewEmailModalContent.astro'
import mails from '@data/mails.json'
---
<DefaultLayout title="Email inbox" pageHeader="Inbox" pageMenu="extra.email-inbox" pageLibs={['hugerte']}> import Subheader from '@ui/Subheader.astro';
<Card> import ButtonGroup from '@ui/ButtonGroup.astro';
<div class="row g-0"> import DefaultLayout from '@shared/layouts/DefaultLayout.astro';
<div class="col-xxl-3 email-border border-end"> import Button from '@ui/Button.astro';
<Offcanvas size="xxl" direction="start" class="h-100 file-offcanvas" tabindex="-1" id="emailSidebaroffcanvas"> import Icon from '@ui/Icon.astro';
<div class="card-body h-100"> import Card from '@ui/Card.astro';
<div> import Offcanvas from '@ui/Offcanvas.astro';
<Button icon="pencil" text="Compose" color="primary" class="d-none d-sm-block" modalId="new-email" /> import Progress from '@ui/Progress.astro';
</div> import Modal from '@shared/components/modals/Modal.astro';
import CaptureModal from '@shared/components/CaptureModal.astro';
import NewEmailModalContent from '@shared/components/modals/NewEmailModalContent.astro';
import mails from '@data/mails.json';
---
<div class="mt-3 nav nav-vertical"> <DefaultLayout
<a href="#" class="nav-link text-danger fw-bold"> title="Email inbox"
<Icon name="inbox" class="me-2" /> pageHeader="Inbox"
Inbox<span class="badge badge-danger ms-auto">{mails.length}</span> pageMenu="extra.email-inbox"
</a> pageLibs={['hugerte']}
<a href="#" class="nav-link"><Icon name="star" class="me-2" />Starred</a> >
<a href="#" class="nav-link"><Icon name="clock" class="me-2" />Snoozed</a> <Card>
<a href="#" class="nav-link"><Icon name="file" class="me-2" />Draft<span class="badge badge-info ms-auto">32</span></a> <div class="row g-0">
<a href="#" class="nav-link"><Icon name="mail-up" class="me-2" />Sent Mail</a> <div class="col-xxl-3 email-border border-end">
<a href="#" class="nav-link"><Icon name="trash" class="me-2" />Trash</a> <Offcanvas size="xxl" direction="start" class="h-100 file-offcanvas" tabindex="-1" id="emailSidebaroffcanvas">
<a href="#" class="nav-link"><Icon name="tag" class="me-2" />Important</a> <div class="card-body h-100">
<a href="#" class="nav-link"><Icon name="alert-octagon" class="me-2" />Spam</a> <div>
</div> <Button icon="pencil" text="Compose" color="primary" class="d-none d-sm-block" modalId="new-email" />
</div>
<div class="mt-4"> <div class="mt-3 nav nav-vertical">
<Subheader as="h6">Labels</Subheader> <a href="#" class="nav-link text-danger fw-bold">
<div class="mt-2 nav nav-vertical"> <Icon name="inbox" class="me-2" />
<a href="#" class="nav-link"> Inbox<span class="badge badge-danger ms-auto">{mails.length}</span>
<div class="badge bg-info me-2"></div> Updates </a>
</a> <a href="#" class="nav-link"><Icon name="star" class="me-2" />Starred</a>
<a href="#" class="nav-link"> <a href="#" class="nav-link"><Icon name="clock" class="me-2" />Snoozed</a>
<div class="badge bg-warning me-2"></div> Friends <a href="#" class="nav-link "><Icon name="file" class="me-2" />Draft<span class="badge badge-info ms-auto">32</span></a>
</a> <a href="#" class="nav-link"><Icon name="mail-up" class="me-2" />Sent Mail</a>
<a href="#" class="nav-link"> <a href="#" class="nav-link"><Icon name="trash" class="me-2" />Trash</a>
<div class="badge bg-success me-2"></div> Family <a href="#" class="nav-link"><Icon name="tag" class="me-2" />Important</a>
</a> <a href="#" class="nav-link"><Icon name="alert-octagon" class="me-2" />Spam</a>
<a href="#" class="nav-link"> </div>
<div class="badge bg-primary me-2"></div> Social
</a>
<a href="#" class="nav-link">
<div class="badge bg-danger me-2"></div> Important
</a>
<a href="#" class="nav-link">
<div class="badge bg-purple me-2"></div> Promotions
</a>
</div>
</div>
<div class="mt-5"> <div class="mt-4">
<Subheader as="h6">Storage</Subheader> <Subheader as="h6">Labels</Subheader>
<Progress value={46} class="my-2" /> <div class="mt-2 nav nav-vertical">
<a href="#" class="nav-link">
<div class="badge bg-info me-2"></div> Updates
</a>
<a href="#" class="nav-link">
<div class="badge bg-warning me-2"></div> Friends
</a>
<a href="#" class="nav-link">
<div class="badge bg-success me-2"></div> Family
</a>
<a href="#" class="nav-link">
<div class="badge bg-primary me-2"></div> Social
</a>
<a href="#" class="nav-link">
<div class="badge bg-danger me-2"></div> Important
</a>
<a href="#" class="nav-link">
<div class="badge bg-purple me-2"></div> Promotions
</a>
</div>
</div>
<p class="text-muted font-13 mb-0">7.02 GB (46%) of 15 GB used</p> <div class="mt-5">
</div> <Subheader as="h6">Storage</Subheader>
</div> <Progress value={46} class="my-2" />
</Offcanvas>
</div>
<div class="col-xxl-9"> <p class="text-muted font-13 mb-0">7.02 GB (46%) of 15 GB used</p>
<div class="card-body"> </div>
<div class="d-flex flex-wrap align-items-center gap-2">
<div class="d-xxl-none d-inline-flex">
<button class="btn btn-icon" type="button" data-bs-toggle="offcanvas" data-bs-target="#emailSidebaroffcanvas" aria-controls="emailSidebaroffcanvas">
<Icon name="menu-2" />
</button>
</div>
<ButtonGroup>
<Button icon="archive" iconOnly />
<Button icon="alert-octagon" iconOnly />
<Button icon="trash" iconOnly />
</ButtonGroup>
<ButtonGroup> </div>
<button type="button" class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false"> </Offcanvas>
<Icon name="folder" /> </div>
</button>
<div class="dropdown-menu">
<span class="dropdown-header">Move to</span>
<a class="dropdown-item" href="#">Social</a>
<a class="dropdown-item" href="#">Promotions</a>
<a class="dropdown-item" href="#">Updates</a>
<a class="dropdown-item" href="#">Forums</a>
</div>
</ButtonGroup>
<ButtonGroup> <div class="col-xxl-9">
<button type="button" class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false"> <div class="card-body">
<Icon name="tag" /> <div class="d-flex flex-wrap align-items-center gap-2">
</button> <div class="d-xxl-none d-inline-flex">
<div class="dropdown-menu"> <button class="btn btn-icon" type="button" data-bs-toggle="offcanvas"
<span class="dropdown-header">Label as:</span> data-bs-target="#emailSidebaroffcanvas" aria-controls="emailSidebaroffcanvas">
<a class="dropdown-item" href="#">Updates</a> <Icon name="menu-2" />
<a class="dropdown-item" href="#">Social</a> </button>
<a class="dropdown-item" href="#">Promotions</a> </div>
<a class="dropdown-item" href="#">Forums</a>
</div>
</ButtonGroup>
<ButtonGroup> <ButtonGroup>
<button type="button" class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false"> <Button icon="archive" iconOnly />
<Icon name="dots" /> More <Button icon="alert-octagon" iconOnly />
</button> <Button icon="trash" iconOnly />
<div class="dropdown-menu"> </ButtonGroup>
<span class="dropdown-header">More Options :</span>
<a class="dropdown-item" href="#">Mark as Unread</a>
<a class="dropdown-item" href="#">Add to Tasks</a>
<a class="dropdown-item" href="#">Add Star</a>
<a class="dropdown-item" href="#">Mute</a>
</div>
</ButtonGroup>
</div>
<div class="mt-3"> <ButtonGroup>
<ul class="email-list"> <button type="button" class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
{ <Icon name="folder" />
mails && mails.length > 0 ? ( </button>
mails.map((mail) => ( <div class="dropdown-menu">
<li> <span class="dropdown-header">Move to</span>
<div class="email-sender-info"> <a class="dropdown-item" href="#">Social</a>
<div class="checkbox-wrapper-mail"> <a class="dropdown-item" href="#">Promotions</a>
<div class="form-check"> <a class="dropdown-item" href="#">Updates</a>
<input type="checkbox" class="form-check-input" id={`mail-${mail.id}`} /> <a class="dropdown-item" href="#">Forums</a>
<label class="form-check-label" for={`mail-${mail.id}`} /> </div>
</div> </ButtonGroup>
</div>
<span class="star-toggle">
<Icon name="star" />
</span>
<a href="#" class="email-title">
{mail.sender}
</a>
</div>
<div class="email-content"> <ButtonGroup>
<a href="#" class="email-subject"> <button type="button" class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
{mail.subject} &nbsp;&nbsp; <Icon name="tag" />
<span>{mail.preview}</span> </button>
</a> <div class="dropdown-menu">
<div class="email-date">{mail.date}</div> <span class="dropdown-header">Label as:</span>
</div> <a class="dropdown-item" href="#">Updates</a>
<a class="dropdown-item" href="#">Social</a>
<a class="dropdown-item" href="#">Promotions</a>
<a class="dropdown-item" href="#">Forums</a>
</div>
</ButtonGroup>
<div class="email-action-icons"> <ButtonGroup>
<ul class="list-inline"> <button type="button" class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<li class="list-inline-item"> <Icon name="dots" /> More
<a href="#"> </button>
<Icon name="archive" /> <div class="dropdown-menu">
</a> <span class="dropdown-header">More Options :</span>
</li> <a class="dropdown-item" href="#">Mark as Unread</a>
<li class="list-inline-item"> <a class="dropdown-item" href="#">Add to Tasks</a>
<a href="#"> <a class="dropdown-item" href="#">Add Star</a>
<Icon name="trash" /> <a class="dropdown-item" href="#">Mute</a>
</a> </div>
</li> </ButtonGroup>
<li class="list-inline-item"> </div>
<a href="#">
<Icon name="mail-opened" class="email-action-icons-item" />
</a>
</li>
<li class="list-inline-item">
<a href="#">
<Icon name="clock" />
</a>
</li>
</ul>
</div>
</li>
))
) : (
<li class="text-muted">No emails</li>
)
}
</ul>
</div>
<div class="row"> <div class="mt-3">
<div class="col-7 mt-1"> <ul class="email-list">
Showing 1 - {mails.length} of {mails.length} {mails && mails.length > 0 ? (
</div> mails.map((mail) => (
<!-- end col--> <li>
<div class="col-5"> <div class="email-sender-info">
<ButtonGroup class="float-end"> <div class="checkbox-wrapper-mail">
<button type="button" class="btn btn-icon"><Icon name="chevron-left" /></button> <div class="form-check">
<button type="button" class="btn btn-icon"><Icon name="chevron-right" /></button> <input type="checkbox" class="form-check-input" id={`mail-${mail.id}`} />
</ButtonGroup> <label class="form-check-label" for={`mail-${mail.id}`}></label>
</div> </div>
<!-- end col--> </div>
</div> <span class="star-toggle"><Icon name="star" /></span>
<!-- end row--> <a href="#" class="email-title">{mail.sender}</a>
</div> </div>
</div>
</div>
</Card>
<Modal modalId="new-email"> <div class="email-content">
<NewEmailModalContent /> <a href="#" class="email-subject">{mail.subject} &nbsp;&nbsp;
</Modal> <span>{mail.preview}</span>
</a>
<div class="email-date">{mail.date}</div>
</div>
<div class="email-action-icons">
<ul class="list-inline">
<li class="list-inline-item">
<a href="#"><Icon name="archive" /></a>
</li>
<li class="list-inline-item">
<a href="#"><Icon name="trash" /></a>
</li>
<li class="list-inline-item">
<a href="#">
<Icon name="mail-opened" class="email-action-icons-item" />
</a>
</li>
<li class="list-inline-item">
<a href="#"><Icon name="clock" /></a>
</li>
</ul>
</div>
</li>
))
) : (
<li class="text-muted">No emails</li>
)}
</ul>
</div>
<div class="row">
<div class="col-7 mt-1">
Showing 1 - {mails.length} of {mails.length}
</div> <!-- end col-->
<div class="col-5">
<ButtonGroup class="float-end">
<button type="button" class="btn btn-icon"><Icon name="chevron-left" /></button>
<button type="button" class="btn btn-icon"><Icon name="chevron-right" /></button>
</ButtonGroup>
</div> <!-- end col-->
</div> <!-- end row-->
</div>
</div>
</div>
</Card>
<CaptureModal>
<Modal modalId="new-email">
<NewEmailModalContent />
</Modal>
</CaptureModal>
</DefaultLayout> </DefaultLayout>
+96 -84
View File
@@ -1,95 +1,107 @@
--- ---
// Masonry auto-inits from the data-masonry attribute (page-lib, no init script, // Masonry auto-inits from the data-masonry attribute (page-lib, no init script,
// like cards-masonry.astro); fslightbox is a pure page-lib (no init script). // like cards-masonry.astro); fslightbox is a pure page-lib (no init script).
import DefaultLayout from '@shared/layouts/DefaultLayout.astro' import DefaultLayout from '@shared/layouts/DefaultLayout.astro';
import CardStamp from '@ui/CardStamp.astro' import CardStamp from '@ui/CardStamp.astro';
import Prose from '@ui/Prose.astro' import Prose from '@ui/Prose.astro';
import { site } from '@shared/lib/site' import CaptureScript from '@shared/components/CaptureScript.astro';
import emails from '@data/emails.json' import { site } from '@shared/lib/site';
import emails from '@data/emails.json';
const emailEntries = Object.entries(emails) const emailEntries = Object.entries(emails);
--- ---
<DefaultLayout pageHeader="Email templates" pageMenu="addons.emails" pageLibs={['masonry', 'fslightbox']}> <DefaultLayout
<div class="row row-cards"> pageHeader="Email templates"
<div class="col-12"> pageMenu="addons.emails"
<div class="card card-md"> pageLibs={['masonry', 'fslightbox']}
<CardStamp size="lg" icon="mail" color="primary" /> >
<div class="card-body"> <div class="row row-cards">
<div class="row align-items-center"> <div class="col-12">
<div class="col-10"> <div class="card card-md">
<h3 class="h1">Tabler Emails</h3> <CardStamp size="lg" icon="mail" color="primary" />
<Prose class="text-secondary fs-3"> <div class="card-body">
{emailEntries.length} eye-catching, customizable and responsive email templates to improve your email communication. No coding skills needed. <div class="row align-items-center">
</Prose> <div class="col-10">
<div class="mt-3"> <h3 class="h1">Tabler Emails</h3>
<a href={site.emails.buy_link} class="btn btn-primary" target="_blank">Buy all emails for {site.emails.price}</a> <Prose class="text-secondary fs-3">
</div> {emailEntries.length} eye-catching, customizable and responsive email templates to improve your email communication. No coding skills needed.
</div> </Prose>
</div> <div class="mt-3">
</div> <a href={site.emails.buy_link} class="btn btn-primary" target="_blank">Buy all emails for {site.emails.price}</a>
</div> </div>
</div> </div>
<div class="col-12"> </div>
<div class="row row-cards" data-masonry={'{"percentPosition": true }'}> </div>
{ </div>
emailEntries.map(([key, email]) => ( </div>
<div class="col-4"> <div class="col-12">
<a href={`./static/emails/${key}.jpg`} data-bs-toggle="modal" data-bs-target="#email-modal" data-bs-title={email.descriptionShort} data-bs-description={email.descriptionLong} data-bs-image={`./static/emails/${key}.jpg`}> <div class="row row-cards" data-masonry={'{"percentPosition": true }'}>
<img src={`./static/emails/${key}.jpg`} class="img-fluid rounded" alt={email.descriptionShort} width={email.width} height={email.height} /> {emailEntries.map(([key, email]) => (
</a> <div class="col-4">
</div> <a
)) href={`./static/emails/${key}.jpg`}
} data-bs-toggle="modal"
</div> data-bs-target="#email-modal"
</div> data-bs-title={email.descriptionShort}
</div> data-bs-description={email.descriptionLong}
data-bs-image={`./static/emails/${key}.jpg`}
>
<img src={`./static/emails/${key}.jpg`} class="img-fluid rounded" alt={email.descriptionShort} width={email.width} height={email.height} />
</a>
</div>
))}
</div>
</div>
</div>
<div class="modal fade" id="email-modal" aria-hidden="true" aria-labelledby="email-modal-label" tabindex="-1"> <div class="modal fade" id="email-modal" aria-hidden="true" aria-labelledby="email-modal-label" tabindex="-1">
<div class="modal-dialog modal-xl modal-dialog-centered"> <div class="modal-dialog modal-xl modal-dialog-centered">
<div class="modal-content"> <div class="modal-content">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<div class="modal-body p-0"> <div class="modal-body p-0">
<div class="row g-0"> <div class="row g-0">
<div class="col-6"> <div class="col-6">
<div class="p-6 bg-surface-secondary rounded-start"> <div class="p-6 bg-surface-secondary rounded-start">
<img src="" class="img-fluid rounded-start" data-email-image /> <img src="" class="img-fluid rounded-start" data-email-image />
</div> </div>
</div> </div>
<div class="col-6"> <div class="col-6">
<div class="p-7"> <div class="p-7">
<Prose> <Prose>
<h3 data-email-title></h3> <h3 data-email-title></h3>
<p data-email-description></p> <p data-email-description></p>
</Prose> </Prose>
<div class="mt-6"> <div class="mt-6">
<a href={site.emails.buy_link} class="btn btn-primary w-100" target="_blank">Buy {site.emails.count} emails for {site.emails.price}</a> <a href={site.emails.buy_link} class="btn btn-primary w-100" target="_blank">Buy {site.emails.count} emails for {site.emails.price}</a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- BEGIN EMAIL MODAL SCRIPT --> <CaptureScript>
<script> <!-- BEGIN EMAIL MODAL SCRIPT -->
const emailModal = document.getElementById('email-modal') <script is:inline>
if (emailModal) { const emailModal = document.getElementById('email-modal');
emailModal.addEventListener('show.bs.modal', function (e) { if (emailModal) {
const button = (e as Event & { relatedTarget?: EventTarget | null }).relatedTarget emailModal.addEventListener('show.bs.modal', function (e) {
if (!(button instanceof HTMLElement)) return const button = e.relatedTarget;
if (!(button instanceof HTMLElement)) return;
const image = button.getAttribute('data-bs-image'), const image = button.getAttribute('data-bs-image'),
title = button.getAttribute('data-bs-title'), title = button.getAttribute('data-bs-title'),
description = button.getAttribute('data-bs-description') description = button.getAttribute('data-bs-description');
emailModal.querySelector('[data-email-title]')!.textContent = title emailModal.querySelector('[data-email-title]').textContent = title;
emailModal.querySelector('[data-email-description]')!.textContent = description emailModal.querySelector('[data-email-description]').textContent = description;
;(emailModal.querySelector('[data-email-image]') as HTMLImageElement).src = image ?? '' emailModal.querySelector('[data-email-image]').src = image;
}) });
} }
</script> </script>
<!-- END EMAIL MODAL SCRIPT --> <!-- END EMAIL MODAL SCRIPT -->
</CaptureScript>
</DefaultLayout> </DefaultLayout>
+31 -26
View File
@@ -1,32 +1,37 @@
--- ---
import DefaultLayout from '@shared/layouts/DefaultLayout.astro' import DefaultLayout from '@shared/layouts/DefaultLayout.astro';
import Card from '@ui/Card.astro' import Card from '@ui/Card.astro';
import CardHeader from '@ui/CardHeader.astro' import CardHeader from '@ui/CardHeader.astro';
import CardBody from '@ui/CardBody.astro' import CardBody from '@ui/CardBody.astro';
import flags from '@data/flags.json' import flags from '@data/flags.json';
// Liquid: {% for icon in (0..20) %}<div></div>{% endfor %} — 21 filler divs // 21 filler divs for flexbox layout.
const fillers = Array.from({ length: 21 }) const fillers = Array.from({ length: 21 });
--- ---
<DefaultLayout title="Flags" pageHeader="Flags" pageMenu="addons.flags"> <DefaultLayout title="Flags" pageHeader="Flags" pageMenu="addons.flags">
<Card> <Card>
<CardHeader> <CardHeader>
<div class="card-title">List of all flags</div> <div class="card-title">List of all flags</div>
</CardHeader> </CardHeader>
<CardBody class="p-0"> <CardBody class="p-0">
<div class="demo-icons-list-wrap"> <div class="demo-icons-list-wrap">
<div class="demo-icons-list"> <div class="demo-icons-list">
{ {
flags.map((country) => ( flags.map((country) => (
<span class="demo-icons-list-item" title={country.name} data-bs-toggle="tooltip" data-bs-placement="top"> <span
<span class={`flag flag-country-${country.flag.toLowerCase()}`} /> class="demo-icons-list-item"
</span> title={country.name}
)) data-bs-toggle="tooltip"
} data-bs-placement="top"
{fillers.map(() => <div />)} >
</div> <span class={`flag flag-country-${country.flag.toLowerCase()}`} />
</div> </span>
</CardBody> ))
</Card> }
{fillers.map(() => <div></div>)}
</div>
</div>
</CardBody>
</Card>
</DefaultLayout> </DefaultLayout>
+26 -20
View File
@@ -1,26 +1,32 @@
--- ---
import DefaultLayout from '@shared/layouts/DefaultLayout.astro' import DefaultLayout from '@shared/layouts/DefaultLayout.astro';
import GalleryPhoto from '@shared/components/cards/GalleryPhoto.astro' import GalleryPhoto from '@shared/components/cards/GalleryPhoto.astro';
import Pagination from '@ui/Pagination.astro' import Pagination from '@ui/Pagination.astro';
import photos from '@data/photos.json' import photos from '@data/photos.json';
import people from '@data/people.json' import people from '@data/people.json';
// Liquid: photos | where: "horizontal", true — limit: 15; person = people[forloop.index0] // Horizontal photos, limit 15; person = people[loop index].
const galleryPhotos = photos.filter((photo) => photo.horizontal).slice(0, 15) const galleryPhotos = photos.filter((photo) => photo.horizontal).slice(0, 15);
--- ---
<DefaultLayout title="Gallery" pageHeader="Gallery" description="1-15 of 241 photos" actions="photos" pageMenu="extra.gallery"> <DefaultLayout
<div class="row row-cards"> title="Gallery"
{ pageHeader="Gallery"
galleryPhotos.map((photo, index) => ( description="1-15 of 241 photos"
<div class="col-sm-6 col-lg-4"> actions="photos"
<GalleryPhoto photo={photo} person={people[index]} index={index + 1} /> pageMenu="extra.gallery"
</div> >
)) <div class="row row-cards">
} {
</div> galleryPhotos.map((photo, index) => (
<div class="col-sm-6 col-lg-4">
<GalleryPhoto photo={photo} person={people[index]} index={index + 1} />
</div>
))
}
</div>
<div class="d-flex mt-5"> <div class="d-flex mt-5">
<Pagination class="ms-auto" /> <Pagination class="ms-auto" />
</div> </div>
</DefaultLayout> </DefaultLayout>
+188 -176
View File
@@ -1,195 +1,207 @@
--- ---
import DefaultLayout from '@shared/layouts/DefaultLayout.astro' import DefaultLayout from '@shared/layouts/DefaultLayout.astro';
import Icon from '@ui/Icon.astro' import Icon from '@ui/Icon.astro';
import CardStamp from '@ui/CardStamp.astro' import CardStamp from '@ui/CardStamp.astro';
import Card from '@ui/Card.astro' import Card from '@ui/Card.astro';
import CardBody from '@ui/CardBody.astro' import CardBody from '@ui/CardBody.astro';
import Prose from '@ui/Prose.astro' import Prose from '@ui/Prose.astro';
import freeIllustrations from '@data/free-illustrations.json' import CaptureScript from '@shared/components/CaptureScript.astro';
import illustrationsList from '@data/illustrations.json' import freeIllustrations from '@data/free-illustrations.json';
import siteData from '@data/site.json' import illustrationsList from '@data/illustrations.json';
import siteData from '@data/site.json';
const autodark = (freeIllustrations as { autodark: Record<string, string> }).autodark const autodark = (freeIllustrations as { autodark: Record<string, string> }).autodark;
const autodarkEntries = Object.entries(autodark) const autodarkEntries = Object.entries(autodark);
// first-illustration: the Liquid loop overwrites each pass → last value wins. // Last autodark entry (loop overwrites each pass).
const firstIllustration = autodarkEntries.length ? autodarkEntries[autodarkEntries.length - 1][1] : '' const firstIllustration = autodarkEntries.length
? autodarkEntries[autodarkEntries.length - 1][1]
: '';
// Page-local transform: replace: '<svg ' , '<svg class="w-100 h-auto" ' const withClass = (svg: string) => svg.replaceAll('<svg ', '<svg class="w-100 h-auto" ');
const withClass = (svg: string) => svg.replaceAll('<svg ', '<svg class="w-100 h-auto" ')
const colors = siteData.colors as Record<string, { hex: string; class: string; prop: string }> const colors = siteData.colors as Record<string, { hex: string; class: string; prop: string }>;
const skinColors = siteData.skinColors as Record<string, { hex: string; class: string }> const skinColors = siteData.skinColors as Record<string, { hex: string; class: string }>;
const colorEntries = Object.values(colors) const colorEntries = Object.values(colors);
const skinEntries = Object.values(skinColors) const skinEntries = Object.values(skinColors);
// skinColor = site.skinColors | first → the first value object (Rose). const skinFirst = skinEntries[0];
const skinFirst = skinEntries[0] const buyLink = (siteData.illustrations as { buy_link: string }).buy_link;
const buyLink = (siteData.illustrations as { buy_link: string }).buy_link
// {{ illustrations | size | minus: 4 }} const moreCount = illustrationsList.length - 4;
const moreCount = illustrationsList.length - 4
// {% capture_script %} — build the illustrations JS map from the same data. const illustrationsData = Object.fromEntries(
// skin_color / color[1].prop resolve to empty in Liquid → literal "var()". autodarkEntries.map(([key, svg]) => [key, { svg: withClass(svg) }]),
const illustrationsData = Object.fromEntries(autodarkEntries.map(([key, svg]) => [key, { svg: withClass(svg) }])) );
--- ---
<DefaultLayout title="SVG Illustrations" pageHeader="SVG Illustrations" pageMenu="addons.illustrations"> <DefaultLayout
<div class="mb-7" style={`--tblr-illustrations-primary: var(--tblr-color-primary); --tblr-illustrations-skin: ${skinFirst.hex};`} id="current-illustration-style"> title="SVG Illustrations"
<div class="row row-cards"> pageHeader="SVG Illustrations"
<div class="col-12"> pageMenu="addons.illustrations"
<div class="row row-cards row-deck g-4"> >
<div class="col-md-7"> <div
<Card> class="mb-7"
<CardBody class="d-flex align-items-center"> style={`--tblr-illustrations-primary: var(--tblr-color-primary); --tblr-illustrations-skin: ${skinFirst.hex};`}
<div id="current-illustration" set:html={withClass(firstIllustration)} /> id="current-illustration-style"
</CardBody> >
</Card> <div class="row row-cards">
</div> <div class="col-12">
<div class="col-md-5"> <div class="row row-cards row-deck g-4">
<div class="row"> <div class="col-md-7">
<div class="col-12"> <Card>
<Card> <CardBody class="d-flex align-items-center">
<CardBody> <div id="current-illustration" set:html={withClass(firstIllustration)} />
<div> </CardBody>
<div class="form-label">Primary color</div> </Card>
<div class="row g-2"> </div>
<div class="col-auto"> <div class="col-md-5">
<label class="form-colorinput"> <div class="row">
<input name="color" type="radio" value="var(--tblr-color-primary)" class="form-colorinput-input js-select-color" checked /> <div class="col-12">
<span class="form-colorinput-color bg-primary"></span> <Card>
</label> <CardBody>
</div> <div>
<div class="col-auto"> <div class="form-label">Primary color</div>
<label class="form-colorinput"> <div class="row g-2">
<input name="color" type="radio" value="var(--tblr-bg-surface-inverted)" class="form-colorinput-input js-select-color" /> <div class="col-auto">
<span class="form-colorinput-color bg-inverted"></span> <label class="form-colorinput">
</label> <input name="color" type="radio" value="var(--tblr-color-primary)" class="form-colorinput-input js-select-color" checked />
</div> <span class="form-colorinput-color bg-primary"></span>
{ </label>
colorEntries.map((color) => ( </div>
<div class="col-auto"> <div class="col-auto">
<label class="form-colorinput"> <label class="form-colorinput">
<input name="color" type="radio" value={color.hex} class="form-colorinput-input js-select-color" /> <input name="color" type="radio" value="var(--tblr-bg-surface-inverted)" class="form-colorinput-input js-select-color" />
<span class={`form-colorinput-color bg-${color.class}`} /> <span class="form-colorinput-color bg-inverted"></span>
</label> </label>
</div> </div>
)) {
} colorEntries.map((color) => (
</div> <div class="col-auto">
<label class="form-colorinput">
<input name="color" type="radio" value={color.hex} class="form-colorinput-input js-select-color" />
<span class={`form-colorinput-color bg-${color.class}`} />
</label>
</div>
))
}
</div>
<div class="form-label mt-4">Skin color</div> <div class="form-label mt-4">Skin color</div>
<div class="row g-2"> <div class="row g-2">
{ {
skinEntries.map((color, i) => ( skinEntries.map((color, i) => (
<div class="col-auto"> <div class="col-auto">
<label class="form-colorinput"> <label class="form-colorinput">
<input name="skin-color" type="radio" value={color.hex} class="form-colorinput-input js-select-skin-color" checked={i === 0} /> <input name="skin-color" type="radio" value={color.hex} class="form-colorinput-input js-select-skin-color" checked={i === 0} />
<span class="form-colorinput-color" style={`background-color: ${color.hex}`} /> <span class="form-colorinput-color" style={`background-color: ${color.hex}`} />
</label> </label>
</div> </div>
)) ))
} }
</div> </div>
<div class="form-label mt-4">Select SVG illustration</div> <div class="form-label mt-4">Select SVG illustration</div>
<div class="row"> <div class="row">
{ {
autodarkEntries.map(([key, svg], i) => ( autodarkEntries.map(([key, svg], i) => (
<div class="col-3"> <div class="col-3">
<label class="form-imagecheck mb-2"> <label class="form-imagecheck mb-2">
<input name="form-imagecheck" type="radio" value={key} class="form-imagecheck-input js-select-illustration" checked={i === autodarkEntries.length - 1} /> <input name="form-imagecheck" type="radio" value={key} class="form-imagecheck-input js-select-illustration" checked={i === autodarkEntries.length - 1} />
<span class="form-imagecheck-figure" set:html={withClass(svg)} /> <span class="form-imagecheck-figure" set:html={withClass(svg)} />
</label> </label>
</div> </div>
)) ))
} }
</div> </div>
</div> </div>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<h2 class="page-title my-5"> <h2 class="page-title my-5">
{moreCount} more SVG Illustrations {moreCount} more SVG Illustrations
</h2> </h2>
<div class="row row-cards"> <div class="row row-cards">
<div class="col-lg-4"> <div class="col-lg-4">
<Card size="md" class="sticky-top"> <Card size="md" class="sticky-top">
<CardStamp size="lg" icon="brand-figma" color="primary" /> <CardStamp size="lg" icon="brand-figma" color="primary" />
<CardBody> <CardBody>
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col-10"> <div class="col-10">
<h3 class="h1">Tabler Illustrations</h3> <h3 class="h1">Tabler Illustrations</h3>
<Prose class="text-secondary"> Access a wide range of SVG illustrations for various projects. Effortlessly customize any illustration to align perfectly with your chosen color scheme! </Prose> <Prose class="text-secondary">
<div class="mt-3"> Access a wide range of SVG illustrations for various projects. Effortlessly customize any illustration to align perfectly with your chosen color scheme!
<a href={buyLink} class="btn btn-primary" target="_blank" rel="noopener"> </Prose>
<Icon name="download" /> <div class="mt-3">
Get lifetime access <a href={buyLink} class="btn btn-primary" target="_blank" rel="noopener">
</a> <Icon name="download" />
</div> Get lifetime access
</div> </a>
</div> </div>
</CardBody> </div>
</Card> </div>
</div> </CardBody>
<div class="col-lg-8"> </Card>
<div class="row row-cards"> </div>
{ <div class="col-lg-8">
illustrationsList.map((illustration) => ( <div class="row row-cards">
<div class="col-6 col-md-3"> {
<Card> illustrationsList.map((illustration) => (
<CardBody> <div class="col-6 col-md-3">
<a href={buyLink} target="_blank"> <Card>
<img src={`./static/illustrations/light/${illustration}.png`} alt={illustration} class="img-light" /> <CardBody>
<img src={`./static/illustrations/dark/${illustration}.png`} alt={illustration} class="img-dark" /> <a href={buyLink} target="_blank">
</a> <img src={`./static/illustrations/light/${illustration}.png`} alt={illustration} class="img-light" />
</CardBody> <img src={`./static/illustrations/dark/${illustration}.png`} alt={illustration} class="img-dark" />
</Card> </a>
</div> </CardBody>
)) </Card>
} </div>
</div> ))
</div> }
</div> </div>
<!-- BEGIN ILLUSTRATIONS SCRIPT --> </div>
<script is:inline define:vars={{ illustrationsData }}> </div>
// @ts-nocheck — illustrationsData is injected at runtime by define:vars, which <CaptureScript>
// the type checker can't see since is:inline skips TS processing of this script. <!-- BEGIN ILLUSTRATIONS SCRIPT -->
let skinColor = 'var()', <script is:inline define:vars={{ illustrationsData }}>
primaryColor = 'var()' // @ts-nocheck — illustrationsData is injected at runtime by define:vars, which
// the type checker can't see since is:inline skips TS processing of this script.
let skinColor = 'var()',
primaryColor = 'var()';
const illustrations = illustrationsData const illustrations = illustrationsData;
const currentIllustration = document.getElementById('current-illustration') const currentIllustration = document.getElementById('current-illustration');
document.querySelectorAll('.js-select-illustration').forEach((elem) => { document.querySelectorAll('.js-select-illustration').forEach((elem) => {
elem.addEventListener('change', (e) => { elem.addEventListener('change', (e) => {
const selectedIllustration = illustrations[e.target.value] const selectedIllustration = illustrations[e.target.value];
currentIllustration.innerHTML = selectedIllustration.svg currentIllustration.innerHTML = selectedIllustration.svg;
}) });
}) });
document.querySelectorAll('.js-select-color').forEach((elem) => { document.querySelectorAll('.js-select-color').forEach((elem) => {
elem.addEventListener('change', (e) => { elem.addEventListener('change', (e) => {
primaryColor = e.target.value primaryColor = e.target.value;
document.getElementById('current-illustration-style').style.setProperty('--tblr-illustrations-primary', primaryColor) document.getElementById('current-illustration-style').style.setProperty('--tblr-illustrations-primary', primaryColor);
}) });
}) });
document.querySelectorAll('.js-select-skin-color').forEach((elem) => { document.querySelectorAll('.js-select-skin-color').forEach((elem) => {
elem.addEventListener('change', (e) => { elem.addEventListener('change', (e) => {
skinColor = e.target.value skinColor = e.target.value;
document.getElementById('current-illustration-style').style.setProperty('--tblr-illustrations-skin', skinColor) document.getElementById('current-illustration-style').style.setProperty('--tblr-illustrations-skin', skinColor);
}) });
}) });
</script> </script>
<!-- END ILLUSTRATIONS SCRIPT --> <!-- END ILLUSTRATIONS SCRIPT -->
</CaptureScript>
</DefaultLayout> </DefaultLayout>
+28 -23
View File
@@ -1,29 +1,34 @@
--- ---
// Liquid: {% for provider in inline-players %} → one card per provider. // One card per inline-player provider.
import DefaultLayout from '@shared/layouts/DefaultLayout.astro' import DefaultLayout from '@shared/layouts/DefaultLayout.astro';
import Card from '@ui/Card.astro' import Card from '@ui/Card.astro';
import CardBody from '@ui/CardBody.astro' import CardBody from '@ui/CardBody.astro';
import CardTitle from '@ui/CardTitle.astro' import CardTitle from '@ui/CardTitle.astro';
import InlinePlayer from '@ui/InlinePlayer.astro' import InlinePlayer from '@ui/InlinePlayer.astro';
import players from '@data/inline-players.json' import players from '@data/inline-players.json';
type Provider = { 'title': string; 'type': string; 'id': string; 'embed-id': string | number } type Provider = { title: string; type: string; id: string; 'embed-id': string | number };
--- ---
<DefaultLayout title="Inline Player" pageHeader="Inline Player" pageMenu="plugins.plyr" pageLibs={['plyr']}> <DefaultLayout
<div class="row row-cards"> title="Inline Player"
{ pageHeader="Inline Player"
(players as Provider[]).map((provider) => ( pageMenu="plugins.plyr"
<div class="col-lg-6"> pageLibs={['plyr']}
<Card> >
<CardBody> <div class="row row-cards">
<CardTitle>{provider.title}</CardTitle> {
(players as Provider[]).map((provider) => (
<div class="col-lg-6">
<Card>
<CardBody>
<CardTitle>{provider.title}</CardTitle>
<InlinePlayer id={provider.id} type={provider.type} embedId={provider['embed-id']} /> <InlinePlayer id={provider.id} type={provider.type} embedId={provider['embed-id']} />
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
)) ))
} }
</div> </div>
</DefaultLayout> </DefaultLayout>
+135 -136
View File
@@ -1,152 +1,151 @@
--- ---
// NOTE: page-header-actions "add-job" requires PageHeader.astro to dispatch it import DefaultLayout from '@shared/layouts/DefaultLayout.astro';
// to HeaderActionsAddJob — see the migration report (PageHeader.astro is a import Icon from '@ui/Icon.astro';
// shared component and was not modified here). import Card from '@ui/Card.astro';
import DefaultLayout from '@shared/layouts/DefaultLayout.astro' import Avatar from '@ui/Avatar.astro';
import Icon from '@ui/Icon.astro' import Check from '@ui/form/Check.astro';
import Card from '@ui/Card.astro' import jobsData from '@data/jobs.json';
import Avatar from '@ui/Avatar.astro'
import Check from '@ui/form/Check.astro'
import jobsData from '@data/jobs.json'
interface Job { interface Job {
company: string company: string;
location: string location: string;
title: string title: string;
type: string type: string;
image: string image: string;
salary?: string salary?: string;
tags: string[] tags: string[];
} }
const jobs = jobsData as Job[] const jobs = jobsData as Job[];
const types = ['Programming', 'Design', 'Management / Finance', 'Customer Support', 'Sales / Marketing'] const types = ['Programming', 'Design', 'Management / Finance', 'Customer Support', 'Sales / Marketing'];
const salaries = ['$20K - $50K', '$50K - $100K', '> $100K', 'Drawing / Painting'] const salaries = ['$20K - $50K', '$50K - $100K', '> $100K', 'Drawing / Painting'];
--- ---
<DefaultLayout title="Search for Jobs" pageHeader="Search for Jobs" actions="add-job" pageMenu="extra.job-listing"> <DefaultLayout title="Search for Jobs" pageHeader="Search for Jobs" actions="add-job" pageMenu="extra.job-listing">
<div class="row g-4"> <div class="row g-4">
<div class="col-md-3"> <div class="col-md-3">
<form action="./" method="get" autocomplete="off" novalidate class="sticky-top"> <form action="./" method="get" autocomplete="off" novalidate class="sticky-top">
<div class="form-label">Job Types</div> <div class="form-label">Job Types</div>
<div class="mb-4"> <div class="mb-4">
{ {
types.map((type, i) => ( types.map((type, i) => (
<label class="form-check"> <label class="form-check">
<input type="checkbox" class="form-check-input" name="form-type[]" value={i + 1} checked={i + 1 <= 2} /> <input type="checkbox" class="form-check-input" name="form-type[]" value={i + 1} checked={i + 1 <= 2} />
<span class="form-check-label">{type}</span> <span class="form-check-label">{type}</span>
</label> </label>
)) ))
} }
</div> </div>
<div class="form-label">Remote</div> <div class="form-label">Remote</div>
<div class="mb-4"> <div class="mb-4">
<Check switch={true} titleOn="On" titleOff="Off" /> <Check switch={true} titleOn="On" titleOff="Off" />
</div> </div>
<div class="form-label">Salary Range</div> <div class="form-label">Salary Range</div>
<div class="mb-4"> <div class="mb-4">
{ {
salaries.map((salary, i) => ( salaries.map((salary, i) => (
<label class="form-check"> <label class="form-check">
<input type="radio" class="form-check-input" name="form-salary" value={i + 1} checked={i + 1 <= 2} /> <input type="radio" class="form-check-input" name="form-salary" value={i + 1} checked={i + 1 <= 2} />
<span class="form-check-label">{salary}</span> <span class="form-check-label">{salary}</span>
</label> </label>
)) ))
} }
</div> </div>
<div class="form-label">Immigration</div> <div class="form-label">Immigration</div>
<div class="mb-4"> <div class="mb-4">
<Check switch={true} titleOn="On" titleOff="Off" /> <Check switch={true} titleOn="On" titleOff="Off" />
<div class="small text-secondary">Only show companies that can sponsor a visa</div> <div class="small text-secondary">Only show companies that can sponsor a visa</div>
</div> </div>
<label class="form-label" for="job-location">Location</label> <label class="form-label" for="job-location">Location</label>
<div class="mb-4"> <div class="mb-4">
<select class="form-select" id="job-location" name="location"> <select class="form-select" id="job-location" name="location">
<option>Anywhere</option> <option>Anywhere</option>
<option>London</option> <option>London</option>
<option>San Francisco</option> <option>San Francisco</option>
<option>New York</option> <option>New York</option>
<option>Berlin</option> <option>Berlin</option>
</select> </select>
</div> </div>
<div class="mt-5"> <div class="mt-5">
<button class="btn btn-primary w-100"> Confirm changes </button> <button class="btn btn-primary w-100">
<a href="#" class="btn btn-link w-100"> Reset to defaults </a> Confirm changes
</div> </button>
</form> <a href="#" class="btn btn-link w-100">
</div> Reset to defaults
<div class="col-md-9"> </a>
<div class="row row-cards"> </div>
<div class="space-y"> </form>
{ </div>
jobs.map((job) => ( <div class="col-md-9">
<Card> <div class="row row-cards">
<div class="row g-0"> <div class="space-y">
<div class="col-auto"> {
<div class="card-body"> jobs.map((job) => (
<Avatar src={`static/jobs/${job.image}`} size="md" /> <Card>
</div> <div class="row g-0">
</div> <div class="col-auto">
<div class="col"> <div class="card-body">
<div class="card-body ps-0"> <Avatar src={`static/jobs/${job.image}`} size="md" />
<div class="row"> </div>
<div class="col"> </div>
<h3 class="mb-0"> <div class="col">
<a href="#">{job.title}</a> <div class="card-body ps-0">
</h3> <div class="row">
</div> <div class="col">
{job.salary && <div class="col-auto fs-3 text-green">{job.salary}</div>} <h3 class="mb-0">
</div> <a href="#">{job.title}</a>
<div class="row"> </h3>
<div class="col-md"> </div>
<div class="mt-3 list-inline list-inline-dots mb-0 text-secondary d-sm-block d-none"> {job.salary && <div class="col-auto fs-3 text-green">{job.salary}</div>}
<div class="list-inline-item"> </div>
<Icon name="building-community" class="icon-inline" /> {job.company} <div class="row">
</div> <div class="col-md">
<div class="list-inline-item"> <div class="mt-3 list-inline list-inline-dots mb-0 text-secondary d-sm-block d-none">
<Icon name="license" class="icon-inline" /> {job.type} <div class="list-inline-item">
</div> <Icon name="building-community" class="icon-inline" /> {job.company}
<div class="list-inline-item"> </div>
<Icon name="map-pin" class="icon-inline" /> {job.location} <div class="list-inline-item">
</div> <Icon name="license" class="icon-inline" /> {job.type}
</div> </div>
<div class="mt-3 list mb-0 text-secondary d-block d-sm-none"> <div class="list-inline-item">
<div class="list-item"> <Icon name="map-pin" class="icon-inline" /> {job.location}
<Icon name="building-community" class="icon-inline" /> {job.company} </div>
</div> </div>
<div class="list-item"> <div class="mt-3 list mb-0 text-secondary d-block d-sm-none">
<Icon name="license" class="icon-inline" /> {job.type} <div class="list-item">
</div> <Icon name="building-community" class="icon-inline" /> {job.company}
<div class="list-item"> </div>
<Icon name="map-pin" class="icon-inline" /> {job.location} <div class="list-item">
</div> <Icon name="license" class="icon-inline" /> {job.type}
</div> </div>
</div> <div class="list-item">
<div class="col-md-auto"> <Icon name="map-pin" class="icon-inline" /> {job.location}
<div class="mt-3 badges"> </div>
{job.tags.map((tag) => ( </div>
<a href="#" class="badge badge-outline text-secondary fw-normal badge-pill"> </div>
{tag} <div class="col-md-auto">
</a> <div class="mt-3 badges">
))} {job.tags.map((tag) => (
</div> <a href="#" class="badge badge-outline text-secondary fw-normal badge-pill">{tag}</a>
</div> ))}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</Card> </div>
)) </div>
} </Card>
</div> ))
</div> }
</div> </div>
</div> </div>
</div>
</div>
</DefaultLayout> </DefaultLayout>
+60 -60
View File
@@ -1,16 +1,10 @@
--- ---
// The prose is shared/includes/license.md rendered via `renderContent: "md"` import DefaultLayout from '@shared/layouts/DefaultLayout.astro';
// (markdown-it). The rendered HTML is inlined verbatim from the reference build import Icon from '@ui/Icon.astro';
// to guarantee a 1:1 DOM match — Astro's markdown pipeline (remark + smartypants) import Card from '@ui/Card.astro';
// would produce different quote characters and structure. import CardBody from '@ui/CardBody.astro';
// TODO: source of truth remains shared/includes/license.md; regenerate this import CardFooter from '@ui/CardFooter.astro';
// block if the markdown changes. import Prose from '@ui/Prose.astro';
import DefaultLayout from '@shared/layouts/DefaultLayout.astro'
import Icon from '@ui/Icon.astro'
import Card from '@ui/Card.astro'
import CardBody from '@ui/CardBody.astro'
import CardFooter from '@ui/CardFooter.astro'
import Prose from '@ui/Prose.astro'
const licenseHtml = ` const licenseHtml = `
<p>This is a legal agreement between you, the Purchaser, and Tabler. Purchasing or downloading of any Tabler product (Tabler Free, Tabler PRO, Tabler Email), constitutes your acceptance of the terms of this license, <a href="https://tabler.io/terms-of-service.html">Tabler terms of service</a> and <a href="https://tabler.io/privacy-policy.html">Tabler private policy</a>.</p> <p>This is a legal agreement between you, the Purchaser, and Tabler. Purchasing or downloading of any Tabler product (Tabler Free, Tabler PRO, Tabler Email), constitutes your acceptance of the terms of this license, <a href="https://tabler.io/terms-of-service.html">Tabler terms of service</a> and <a href="https://tabler.io/privacy-policy.html">Tabler private policy</a>.</p>
@@ -37,60 +31,66 @@ const licenseHtml = `
<li>You cannot add our source code to any open source repository.</li> <li>You cannot add our source code to any open source repository.</li>
<li>The source code may not be placed on any website in a complete or archived downloadable format.</li> <li>The source code may not be placed on any website in a complete or archived downloadable format.</li>
</ol> </ol>
` `;
--- ---
<DefaultLayout title="License" pageHeader="Tabler License" pageMenu="extra.license"> <DefaultLayout title="License" pageHeader="Tabler License" pageMenu="extra.license">
<div class="row row-cards"> <div class="row row-cards">
<div class="col-lg-8"> <div class="col-lg-8">
<Card size="lg"> <Card size="lg">
<CardBody> <CardBody>
<Prose> <Prose>
<Fragment set:html={licenseHtml} /> <Fragment set:html={licenseHtml} />
</Prose> </Prose>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col-lg-4"> <div class="col-lg-4">
<Card> <Card>
<CardBody> <CardBody>
<div class="d-flex align-items-center mb-3"> <div class="d-flex align-items-center mb-3">
<div class="me-3"> <div class="me-3">
<Icon name="scale" size="md" /> <Icon name="scale" size="md" />
</div> </div>
<div> <div>
<small class="text-secondary">tabler/tabler is licensed under the</small> <small class="text-secondary">tabler/tabler is licensed under the</small>
<h3 class="lh-1">MIT License</h3> <h3 class="lh-1">MIT License</h3>
</div> </div>
</div> </div>
<div class="text-secondary mb-3">A short and simple permissive license with conditions only requiring preservation of copyright and license notices. Licensed works, modifications, and larger works may be distributed under different terms and without source code.</div> <div class="text-secondary mb-3">
A short and simple permissive license with conditions only requiring preservation of copyright and
license notices. Licensed works, modifications, and larger works may be distributed under different terms
and without source code.
</div>
<h4>Permissions</h4>
<ul class="list-unstyled space-y-1"> <h4>Permissions</h4>
<li><Icon name="check" class="text-green" /> Commercial use</li>
<li><Icon name="check" class="text-green" /> Modification</li>
<li><Icon name="check" class="text-green" /> Distribution</li>
<li><Icon name="check" class="text-green" /> Private use</li>
</ul>
<h4>Limitations</h4> <ul class="list-unstyled space-y-1">
<ul class="list-unstyled space-y-1"> <li><Icon name="check" class="text-green" /> Commercial use</li>
<li><Icon name="x" class="text-red" /> Liability</li> <li><Icon name="check" class="text-green" /> Modification</li>
<li><Icon name="x" class="text-red" /> Warranty</li> <li><Icon name="check" class="text-green" /> Distribution</li>
</ul> <li><Icon name="check" class="text-green" /> Private use</li>
</ul>
<h4>Conditions</h4>
<ul class="list-unstyled space-y-1"> <h4>Limitations</h4>
<li><Icon name="info-circle" class="text-blue" /> License and copyright notice</li> <ul class="list-unstyled space-y-1">
</ul> <li><Icon name="x" class="text-red" /> Liability</li>
</CardBody> <li><Icon name="x" class="text-red" /> Warranty</li>
<CardFooter> </ul>
This is not legal advice.
<a href="#" target="_blank">Learn more about repository licenses.</a> <h4>Conditions</h4>
</CardFooter> <ul class="list-unstyled space-y-1">
</Card> <li><Icon name="info-circle" class="text-blue" /> License and copyright notice</li>
</div> </ul>
</div> </CardBody>
<CardFooter>
This is not legal advice.
<a href="#" target="_blank">Learn more about repository licenses.</a>
</CardFooter>
</Card>
</div>
</div>
</DefaultLayout> </DefaultLayout>
+22 -18
View File
@@ -1,23 +1,27 @@
--- ---
// Liquid: photos | where: "horizontal", true — the gallery iterates the filtered // Gallery uses horizontal photos only.
// list for both the fslightbox href and the Photo include. import DefaultLayout from '@shared/layouts/DefaultLayout.astro';
import DefaultLayout from '@shared/layouts/DefaultLayout.astro' import Photo from '@ui/Photo.astro';
import Photo from '@ui/Photo.astro' import photos from '@data/photos.json';
import photos from '@data/photos.json'
const filteredPhotos = photos.filter((photo) => photo.horizontal) const filteredPhotos = photos.filter((photo) => photo.horizontal);
--- ---
<DefaultLayout title="Lightbox" pageHeader="Lightbox" pageMenu="plugins.lightbox" pageLibs={['fslightbox']}> <DefaultLayout
<div class="row row-cols-3 row-cols-md-4 row-cols-lg-6 g-3"> title="Lightbox"
{ pageHeader="Lightbox"
filteredPhotos.map((photo) => ( pageMenu="plugins.lightbox"
<div class="col"> pageLibs={['fslightbox']}
<a data-fslightbox="gallery" href={`./static/photos/${photo.file}`}> >
<Photo photo={photo} class="rounded border" ratio="1x1" /> <div class="row row-cols-3 row-cols-md-4 row-cols-lg-6 g-3">
</a> {
</div> filteredPhotos.map((photo) => (
)) <div class="col">
} <a data-fslightbox="gallery" href={`./static/photos/${photo.file}`}>
</div> <Photo photo={photo} class="rounded border" ratio="1x1" />
</a>
</div>
))
}
</div>
</DefaultLayout> </DefaultLayout>
+26 -29
View File
@@ -1,40 +1,37 @@
--- ---
// Front matter: layout-wrapper-full + layout-sidebar + layout-hide-topbar, import DefaultLayout from '@shared/layouts/DefaultLayout.astro';
// page-libs: [google-maps], page-menu: plugins.map-fullsize. No title / no import CaptureScript from '@shared/components/CaptureScript.astro';
// page-header (the page-header block renders empty in the reference).
//
// {% assign map-id = 'google' %} → id="map-google".
// The {% capture_script %} block is registered synchronously (before any await).
// We mirror the development build (environment == 'development'), so the
// `window.tabler_map` bookkeeping lines are emitted (as in the reference).
//
// REQUIRES DefaultLayout `wrapperFull` support (see report): page-wrapper-full
// class + slot rendered WITHOUT the .container-xl wrapper. Not implemented in
// the shared DefaultLayout yet — flagged rather than modified here.
import DefaultLayout from '@shared/layouts/DefaultLayout.astro'
--- ---
<DefaultLayout pageMenu="plugins.map-fullsize" pageLibs={['google-maps']} sidebar hideTopbar wrapperFull> <DefaultLayout
<div class="map flex-fill" id="map-google"></div> pageMenu="plugins.map-fullsize"
<!-- pageLibs={['google-maps']}
sidebar
hideTopbar
wrapperFull
>
<div class="map flex-fill" id="map-google"></div>
<!--
is:inline: google-maps loads via a deferred page-lib <script>; see is:inline: google-maps loads via a deferred page-lib <script>; see
FormElements1.astro for why a processed (module) script here would run too FormElements1.astro for why a processed (module) script here would run too
early and find window.google undefined. early and find window.google undefined.
--> -->
<!-- BEGIN MAP SCRIPT --> <CaptureScript>
<script is:inline> <!-- BEGIN MAP SCRIPT -->
window.tabler_map ??= {} <script is:inline>
window.tabler_map ??= {};
function initMap() { function initMap() {
const map = new google.maps.Map(document.getElementById('map-google'), { const map = new google.maps.Map(document.getElementById('map-google'), {
center: { lat: -34.397, lng: 150.644 }, center: { lat: -34.397, lng: 150.644 },
zoom: 8, zoom: 8,
}) });
window.tabler_map['map-google'] = map window.tabler_map['map-google'] = map;
} }
document.readyState !== 'loading' ? initMap() : document.addEventListener('DOMContentLoaded', initMap, { once: true }) document.readyState !== 'loading' ? initMap() : document.addEventListener('DOMContentLoaded', initMap, { once: true });
</script> </script>
<!-- END MAP SCRIPT --> <!-- END MAP SCRIPT -->
</CaptureScript>
</DefaultLayout> </DefaultLayout>
+30 -31
View File
@@ -1,37 +1,36 @@
--- ---
// Liquid: {% for map in maps %} — card maps span a full-width column with no // Card maps span full-width column with no card-body; non-card maps get card-body with title.
// card-body; non-card maps get a card-body with a title. import DefaultLayout from '@shared/layouts/DefaultLayout.astro';
import DefaultLayout from '@shared/layouts/DefaultLayout.astro' import Map from '@ui/Map.astro';
import Map from '@ui/Map.astro' import Card from '@ui/Card.astro';
import Card from '@ui/Card.astro' import CardBody from '@ui/CardBody.astro';
import CardBody from '@ui/CardBody.astro' import maps from '@data/maps.json';
import maps from '@data/maps.json'
type MapData = { title?: string; card?: boolean } type MapData = { title?: string; card?: boolean };
const mapEntries = Object.entries(maps as Record<string, MapData>) const mapEntries = Object.entries(maps as Record<string, MapData>);
--- ---
<DefaultLayout title="Maps" pageHeader="Maps" pageMenu="plugins.maps" pageLibs={['mapbox']}> <DefaultLayout title="Maps" pageHeader="Maps" pageMenu="plugins.maps" pageLibs={['mapbox']}>
<div class="row row-cards"> <div class="row row-cards">
{ {
mapEntries.map(([mapId, data]) => mapEntries.map(([mapId, data]) =>
data.card ? ( data.card ? (
<div class="col-lg-12"> <div class="col-lg-12">
<Card> <Card>
<Map mapId={mapId} ratio="21x9" /> <Map mapId={mapId} ratio="21x9" />
</Card> </Card>
</div> </div>
) : ( ) : (
<div class="col-lg-6"> <div class="col-lg-6">
<Card> <Card>
<CardBody> <CardBody>
<div class="card-title">{data.title}</div> <div class="card-title">{data.title}</div>
<Map mapId={mapId} /> <Map mapId={mapId} />
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
), ),
) )
} }
</div> </div>
</DefaultLayout> </DefaultLayout>
+1 -7
View File
@@ -1,11 +1,5 @@
--- ---
import RedirectLayout from '@shared/layouts/RedirectLayout.astro' import RedirectLayout from '@shared/layouts/RedirectLayout.astro';
// Front matter in preview/pages/markdown.html sets `redirect.to: prose.html`,
// but shared/includes/redirect.html reads `page.redirect.to` — and in Eleventy
// `page` is the built-in page object (no front matter), so the include gets an
// empty url. The reference build (markdown.html) therefore redirects to the
// relative root ".". We reproduce that exactly by passing no url.
--- ---
<RedirectLayout /> <RedirectLayout />
+13 -15
View File
@@ -1,21 +1,19 @@
--- ---
// The `description` front matter is not emitted by the base layout (no import MarketingLayout from '@shared/layouts/MarketingLayout.astro';
// page-specific <meta name="description"> in the reference build) — no prop. import Pricing from '@shared/components/marketing/sections/Pricing.astro';
import MarketingLayout from '@shared/layouts/MarketingLayout.astro' import PricingBanner from '@shared/components/marketing/sections/PricingBanner.astro';
import Pricing from '@shared/components/marketing/sections/Pricing.astro' import Faq from '@shared/components/marketing/sections/Faq.astro';
import PricingBanner from '@shared/components/marketing/sections/PricingBanner.astro'
import Faq from '@shared/components/marketing/sections/Faq.astro'
--- ---
<MarketingLayout title="Pricing"> <MarketingLayout title="Pricing">
<header class="hero"> <header class="hero">
<div class="container"> <div class="container">
<h2 class="hero-title">Simple, transparent pricing</h2> <h2 class="hero-title">Simple, transparent pricing</h2>
<p class="hero-description">Get early access to 100+ components and free updates every month. Make it yours today!</p> <p class="hero-description">Get early access to 100+ components and free updates every month. Make it yours today!</p>
</div> </div>
</header> </header>
<Pricing class="pt-0" /> <Pricing class="pt-0" />
<PricingBanner /> <PricingBanner />
<Faq background="light" /> <Faq background="light" />
</MarketingLayout> </MarketingLayout>
+163 -158
View File
@@ -1,164 +1,169 @@
--- ---
// Every modal is rendered inline (`{% include "ui/modal.html" ... inline show %}`) // Every modal is rendered inline via ModalInline (in place, unlike Modal.astro
// via ModalInline (renders in place, unlike Modal.astro which drains to <body>). // which drains to <body>).
import DefaultLayout from '@shared/layouts/DefaultLayout.astro' import DefaultLayout from '@shared/layouts/DefaultLayout.astro';
import Card from '@ui/Card.astro' import Card from '@ui/Card.astro';
import CardBody from '@ui/CardBody.astro' import CardBody from '@ui/CardBody.astro';
import ModalInline from '@shared/components/modals/ModalInline.astro' import ModalInline from '@shared/components/modals/ModalInline.astro';
import SimpleModalContent from '@shared/components/modals/SimpleModalContent.astro' import SimpleModalContent from '@shared/components/modals/SimpleModalContent.astro';
import LargeModalContent from '@shared/components/modals/LargeModalContent.astro' import LargeModalContent from '@shared/components/modals/LargeModalContent.astro';
import SmallModalContent from '@shared/components/modals/SmallModalContent.astro' import SmallModalContent from '@shared/components/modals/SmallModalContent.astro';
import FullWidthModalContent from '@shared/components/modals/FullWidthModalContent.astro' import FullWidthModalContent from '@shared/components/modals/FullWidthModalContent.astro';
import ScrollableModalContent from '@shared/components/modals/ScrollableModalContent.astro' import ScrollableModalContent from '@shared/components/modals/ScrollableModalContent.astro';
import ReportModalContent from '@shared/components/modals/ReportModalContent.astro' import ReportModalContent from '@shared/components/modals/ReportModalContent.astro';
import SuccessModalContent from '@shared/components/modals/SuccessModalContent.astro' import SuccessModalContent from '@shared/components/modals/SuccessModalContent.astro';
import DangerModalContent from '@shared/components/modals/DangerModalContent.astro' import DangerModalContent from '@shared/components/modals/DangerModalContent.astro';
import TeamModalContent from '@shared/components/modals/TeamModalContent.astro' import TeamModalContent from '@shared/components/modals/TeamModalContent.astro';
import SignatureModalContent from '@shared/components/modals/SignatureModalContent.astro' import SignatureModalContent from '@shared/components/modals/SignatureModalContent.astro';
import NewEmailModalContent from '@shared/components/modals/NewEmailModalContent.astro' import NewEmailModalContent from '@shared/components/modals/NewEmailModalContent.astro';
import NewEventModalContent from '@shared/components/modals/NewEventModalContent.astro' import NewEventModalContent from '@shared/components/modals/NewEventModalContent.astro';
import NewTaskModalContent from '@shared/components/modals/NewTaskModalContent.astro' import NewTaskModalContent from '@shared/components/modals/NewTaskModalContent.astro';
import EditProfileModalContent from '@shared/components/modals/EditProfileModalContent.astro' import EditProfileModalContent from '@shared/components/modals/EditProfileModalContent.astro';
import ConfirmDeleteModalContent from '@shared/components/modals/ConfirmDeleteModalContent.astro' import ConfirmDeleteModalContent from '@shared/components/modals/ConfirmDeleteModalContent.astro';
import ChangePasswordModalContent from '@shared/components/modals/ChangePasswordModalContent.astro' import ChangePasswordModalContent from '@shared/components/modals/ChangePasswordModalContent.astro';
import AddTaskModalContent from '@shared/components/modals/AddTaskModalContent.astro' import AddTaskModalContent from '@shared/components/modals/AddTaskModalContent.astro';
const cardClass = 'position-relative rounded d-block bg-surface-backdrop py-6 w-auto h-auto z-0' const cardClass = 'position-relative rounded d-block bg-surface-backdrop py-6 w-auto h-auto z-0';
// add-task bakes `show` into the class string (no `show` flag) → aria-hidden="true". // add-task bakes `show` into the class string (no `show` flag) → aria-hidden="true".
const addTaskClass = 'position-relative rounded d-block show bg-surface-backdrop py-6 w-auto h-auto z-0' const addTaskClass = 'position-relative rounded d-block show bg-surface-backdrop py-6 w-auto h-auto z-0';
--- ---
<DefaultLayout title="Modals" pageHeader="Modals" pageMenu="base.modals" pageLibs={['signature_pad', 'hugerte', 'litepicker']}> <DefaultLayout
<Card> title="Modals"
<CardBody> pageHeader="Modals"
<div class="row g-5"> pageMenu="base.modals"
<div class="d-none d-md-block col-3"> pageLibs={['signature_pad', 'hugerte', 'litepicker']}
<div class="nav nav-vertical sticky-top pt-4"> >
<a href="#modal-simple" class="nav-link">Simple modal</a> <Card>
<a href="#modal-large" class="nav-link">Large modal</a> <CardBody>
<a href="#modal-small" class="nav-link">Small modal</a> <div class="row g-5">
<a href="#modal-full-width" class="nav-link">Full width modal</a> <div class="d-none d-md-block col-3">
<a href="#modal-scrollable" class="nav-link">Scrollable modal</a> <div class="nav nav-vertical sticky-top pt-4">
<a href="#modal-report" class="nav-link">Modal with form</a> <a href="#modal-simple" class="nav-link">Simple modal</a>
<a href="#modal-success" class="nav-link">Success modal</a> <a href="#modal-large" class="nav-link">Large modal</a>
<a href="#modal-danger" class="nav-link">Danger modal</a> <a href="#modal-small" class="nav-link">Small modal</a>
<a href="#modal-team" class="nav-link">Modal with simple form</a> <a href="#modal-full-width" class="nav-link">Full width modal</a>
<a href="#modal-signature" class="nav-link">Modal with signature form</a> <a href="#modal-scrollable" class="nav-link">Scrollable modal</a>
<a href="#modal-new-email" class="nav-link">New email modal</a> <a href="#modal-report" class="nav-link">Modal with form</a>
<a href="#modal-new-event" class="nav-link">New event modal</a> <a href="#modal-success" class="nav-link">Success modal</a>
<a href="#modal-new-task" class="nav-link">New task modal</a> <a href="#modal-danger" class="nav-link">Danger modal</a>
<a href="#modal-edit-profile" class="nav-link">Edit profile modal</a> <a href="#modal-team" class="nav-link">Modal with simple form</a>
<a href="#modal-confirm-delete" class="nav-link">Confirm delete modal</a> <a href="#modal-signature" class="nav-link">Modal with signature form</a>
<a href="#modal-change-password" class="nav-link">Change password modal</a> <a href="#modal-new-email" class="nav-link">New email modal</a>
</div> <a href="#modal-new-event" class="nav-link">New event modal</a>
</div> <a href="#modal-new-task" class="nav-link">New task modal</a>
<div class="col"> <a href="#modal-edit-profile" class="nav-link">Edit profile modal</a>
<div class="space-y-6"> <a href="#modal-confirm-delete" class="nav-link">Confirm delete modal</a>
<div> <a href="#modal-change-password" class="nav-link">Change password modal</a>
<h3>Simple modal</h3> </div>
<ModalInline class={cardClass} modalId="simple" show> </div>
<SimpleModalContent /> <div class="col">
</ModalInline> <div class="space-y-6">
</div> <div>
<div> <h3>Simple modal</h3>
<h3>Large modal</h3> <ModalInline class={cardClass} modalId="simple" show>
<ModalInline class={cardClass} modalId="large" size="lg" show> <SimpleModalContent />
<LargeModalContent /> </ModalInline>
</ModalInline> </div>
</div> <div>
<div> <h3>Large modal</h3>
<h3>Small modal</h3> <ModalInline class={cardClass} modalId="large" size="lg" show>
<ModalInline class={cardClass} modalId="small" size="sm" show> <LargeModalContent />
<SmallModalContent /> </ModalInline>
</ModalInline> </div>
</div> <div>
<div> <h3>Small modal</h3>
<h3>Full width modal</h3> <ModalInline class={cardClass} modalId="small" size="sm" show>
<ModalInline class={cardClass} modalId="full-width" size="full-width" show> <SmallModalContent />
<FullWidthModalContent /> </ModalInline>
</ModalInline> </div>
</div> <div>
<div> <h3>Full width modal</h3>
<h3>Scrollable modal</h3> <ModalInline class={cardClass} modalId="full-width" size="full-width" show>
<ModalInline class={cardClass} modalId="scrollable" scrollable style="max-height: 30rem" show> <FullWidthModalContent />
<ScrollableModalContent /> </ModalInline>
</ModalInline> </div>
</div> <div>
<div> <h3>Scrollable modal</h3>
<h3>Modal with form</h3> <ModalInline class={cardClass} modalId="scrollable" scrollable style="max-height: 30rem" show>
<ModalInline class={cardClass} modalId="report" size="lg" show> <ScrollableModalContent />
<ReportModalContent /> </ModalInline>
</ModalInline> </div>
</div> <div>
<div> <h3>Modal with form</h3>
<h3>Success modal</h3> <ModalInline class={cardClass} modalId="report" size="lg" show>
<ModalInline class={cardClass} modalId="success" size="sm" show> <ReportModalContent />
<SuccessModalContent /> </ModalInline>
</ModalInline> </div>
</div> <div>
<div> <h3>Success modal</h3>
<h3>Danger modal</h3> <ModalInline class={cardClass} modalId="success" size="sm" show>
<ModalInline class={cardClass} modalId="danger" size="sm" show> <SuccessModalContent />
<DangerModalContent /> </ModalInline>
</ModalInline> </div>
</div> <div>
<div> <h3>Danger modal</h3>
<h3>Modal with simple form</h3> <ModalInline class={cardClass} modalId="danger" size="sm" show>
<ModalInline class={cardClass} modalId="team" show> <DangerModalContent />
<TeamModalContent /> </ModalInline>
</ModalInline> </div>
</div> <div>
<div> <h3>Modal with simple form</h3>
<h3>Modal with signature form</h3> <ModalInline class={cardClass} modalId="team" show>
<ModalInline class={cardClass} modalId="signature" show> <TeamModalContent />
<SignatureModalContent /> </ModalInline>
</ModalInline> </div>
</div> <div>
<div> <h3>Modal with signature form</h3>
<h3>New email modal</h3> <ModalInline class={cardClass} modalId="signature" show>
<ModalInline class={cardClass} modalId="new-email" show> <SignatureModalContent />
<NewEmailModalContent /> </ModalInline>
</ModalInline> </div>
</div> <div>
<div> <h3>New email modal</h3>
<h3>New event modal</h3> <ModalInline class={cardClass} modalId="new-email" show>
<ModalInline class={cardClass} modalId="new-event" show> <NewEmailModalContent />
<NewEventModalContent /> </ModalInline>
</ModalInline> </div>
</div> <div>
<div> <h3>New event modal</h3>
<h3>New task modal</h3> <ModalInline class={cardClass} modalId="new-event" show>
<ModalInline class={cardClass} modalId="new-task" show> <NewEventModalContent />
<NewTaskModalContent /> </ModalInline>
</ModalInline> </div>
</div> <div>
<div> <h3>New task modal</h3>
<h3>Edit profile modal</h3> <ModalInline class={cardClass} modalId="new-task" show>
<ModalInline class={cardClass} modalId="edit-profile" size="lg" show> <NewTaskModalContent />
<EditProfileModalContent /> </ModalInline>
</ModalInline> </div>
</div> <div>
<div> <h3>Edit profile modal</h3>
<h3>Confirm delete modal</h3> <ModalInline class={cardClass} modalId="edit-profile" size="lg" show>
<ModalInline class={cardClass} modalId="confirm-delete" size="sm" show> <EditProfileModalContent />
<ConfirmDeleteModalContent /> </ModalInline>
</ModalInline> </div>
</div> <div>
<div> <h3>Confirm delete modal</h3>
<h3>Change password modal</h3> <ModalInline class={cardClass} modalId="confirm-delete" size="sm" show>
<ModalInline class={cardClass} modalId="change-password" show> <ConfirmDeleteModalContent />
<ChangePasswordModalContent /> </ModalInline>
</ModalInline> </div>
</div> <div>
<div> <h3>Change password modal</h3>
<h3>Add task modal</h3> <ModalInline class={cardClass} modalId="change-password" show>
<ModalInline class={addTaskClass} modalId="add-task"> <ChangePasswordModalContent />
<AddTaskModalContent /> </ModalInline>
</ModalInline> </div>
</div> <div>
</div> <h3>Add task modal</h3>
</div> <ModalInline class={addTaskClass} modalId="add-task">
</div> <AddTaskModalContent />
</CardBody> </ModalInline>
</Card> </div>
</div>
</div>
</div>
</CardBody>
</Card>
</DefaultLayout> </DefaultLayout>
+21 -22
View File
@@ -1,29 +1,28 @@
--- ---
import DefaultLayout from '@shared/layouts/DefaultLayout.astro' import DefaultLayout from '@shared/layouts/DefaultLayout.astro';
import TracksList from '@shared/components/cards/music/TracksList.astro' import TracksList from '@shared/components/cards/music/TracksList.astro';
import TrackInfo from '@shared/components/cards/music/TrackInfo.astro' import TrackInfo from '@shared/components/cards/music/TrackInfo.astro';
// Liquid: {% for i in (8..13) %} const topTrackIds = [8, 9, 10, 11, 12, 13];
const topTrackIds = [8, 9, 10, 11, 12, 13]
--- ---
<DefaultLayout title="Music components" pageHeader="Music components" pageMenu="extra.music"> <DefaultLayout title="Music components" pageHeader="Music components" pageMenu="extra.music">
<div class="row"> <div class="row">
<div class="col-lg-8"> <div class="col-lg-8">
<TracksList /> <TracksList />
</div> </div>
<div class="col-lg-4"> <div class="col-lg-4">
<h3 class="mb-3">Top tracks</h3> <h3 class="mb-3">Top tracks</h3>
<div class="row row-cards"> <div class="row row-cards">
{ {
topTrackIds.map((trackId) => ( topTrackIds.map((trackId) => (
<div class="col-md-6 col-lg-12"> <div class="col-md-6 col-lg-12">
<TrackInfo trackId={trackId} /> <TrackInfo trackId={trackId} />
</div> </div>
)) ))
} }
</div> </div>
</div> </div>
</div> </div>
</DefaultLayout> </DefaultLayout>
+36 -28
View File
@@ -1,41 +1,49 @@
--- ---
// with variant params, rendered with the shared Navbar component. // with variant params, rendered with the shared Navbar component.
// //
// Note: fluid-search (variant 5) is a no-op — the search block in the Liquid // fluid-search (variant 5) is a no-op — search block in condensed branch is dead code.
// condensed branch is dead code (`unless condensed` inside `if condensed`); import DefaultLayout from '@shared/layouts/DefaultLayout.astro';
// the prop exists on Navbar only to mirror the include signature. import Navbar from '@shared/components/navbar/Navbar.astro';
import DefaultLayout from '@shared/layouts/DefaultLayout.astro'
import Navbar from '@shared/components/navbar/Navbar.astro'
--- ---
<DefaultLayout title="Navigation" pageHeader="Navigation" pageMenu="base.navigation"> <DefaultLayout title="Navigation" pageHeader="Navigation" pageMenu="base.navigation">
<div class="box"> <div class="box">
<div class="mb-3"> <div class="mb-3">
<Navbar sample condensed transparent personId={3} /> <Navbar sample condensed transparent personId={3} />
</div> </div>
<div class="mb-3"> <div class="mb-3">
<Navbar sample condensed personId={4} /> <Navbar sample condensed personId={4} />
</div> </div>
<div class="mb-3"> <div class="mb-3">
<Navbar sample condensed dark hideLogo showTitle hideIcons hideUsername personId={5} /> <Navbar sample condensed dark hideLogo showTitle hideIcons hideUsername personId={5} />
</div> </div>
<div class="mb-3"> <div class="mb-3">
<Navbar sample condensed dark smallLogo backgroundColor="#7952b3" hideSearch personId={6} /> <Navbar sample condensed dark smallLogo backgroundColor="#7952b3" hideSearch personId={6} />
</div> </div>
<div class="mb-3"> <div class="mb-3">
<Navbar sample condensed dark background="primary" hideBrand hideIcons fluidSearch hideUsername personId={7} /> <Navbar
</div> sample
condensed
dark
background="primary"
hideBrand
hideIcons
fluidSearch
hideUsername
personId={7}
/>
</div>
<div class="mb-3"> <div class="mb-3">
<Navbar sample smallLogo showTitle personId={8} /> <Navbar sample smallLogo showTitle personId={8} />
</div> </div>
<div class="mb-3"> <div class="mb-3">
<Navbar sample dark personId={9} /> <Navbar sample dark personId={9} />
</div> </div>
</div> </div>
</DefaultLayout> </DefaultLayout>
+119 -121
View File
@@ -1,128 +1,126 @@
--- ---
// NOTE: the Liquid source passes `current=2` to ui/progress-steps.html, but that // NOTE: `current=2` passed to ProgressSteps is a no-op (reads `active`, not `current`) — only step 1 is active.
// include reads `active` (not `current`) — so the arg is a no-op and only the import BaseLayout from '@shared/layouts/BaseLayout.astro';
// first step is active. The reference reflects this (Step 1 = bg-primary). import NavbarLogo from '@shared/components/navbar/NavbarLogo.astro';
import BaseLayout from '@shared/layouts/BaseLayout.astro' import ProgressSteps from '@ui/ProgressSteps.astro';
import NavbarLogo from '@shared/components/navbar/NavbarLogo.astro' import Button from '@ui/Button.astro';
import ProgressSteps from '@ui/ProgressSteps.astro' import ButtonList from '@ui/ButtonList.astro';
import Button from '@ui/Button.astro' import FormGroup from '@ui/FormGroup.astro';
import ButtonList from '@ui/ButtonList.astro'
import FormGroup from '@ui/FormGroup.astro'
--- ---
<BaseLayout title="Onboarding"> <BaseLayout title="Onboarding">
<nav class="navbar"> <nav class="navbar">
<div class="container"> <div class="container">
<div class="row w-100 align-items-center"> <div class="row w-100 align-items-center">
<div class="col me-auto"> <div class="col me-auto">
<NavbarLogo class="logo-gray" /> <NavbarLogo class="logo-gray" />
</div> </div>
<div class="col-2"> <div class="col-2">
<ProgressSteps count={5} /> <ProgressSteps count={5} />
</div> </div>
<div class="col text-end"> <div class="col text-end">
<a href="." class="btn btn-ghost">Skip<span class="d-none d-md-inline">&nbsp;to dashboard</span></a> <a href="." class="btn btn-ghost">Skip<span class="d-none d-md-inline">&nbsp;to dashboard</span></a>
</div> </div>
</div> </div>
</div> </div>
</nav> </nav>
<main class="py-5"> <main class="py-5">
<div class="container container-tight"> <div class="container container-tight">
<div class="page-header"> <div class="page-header">
<h1 class="page-title">Let's set up your account</h1> <h1 class="page-title"> Let's set up your account </h1>
</div> </div>
<div class="card mt-5"> <div class="card mt-5">
<div class="card-body space-y-4"> <div class="card-body space-y-4">
<FormGroup label="Full name" class=""> <FormGroup label="Full name" class="">
<input type="text" class="form-control" placeholder="Enter your full name" /> <input type="text" class="form-control" placeholder="Enter your full name" />
</FormGroup> </FormGroup>
<FormGroup label="Company name" class=""> <FormGroup label="Company name" class="">
<input type="text" class="form-control" placeholder="Enter your company name" /> <input type="text" class="form-control" placeholder="Enter your company name" />
</FormGroup> </FormGroup>
<FormGroup label="Role" class=""> <FormGroup label="Role" class="">
<select class="form-select"> <select class="form-select">
<option value="">Select your role</option> <option value="">Select your role</option>
<option value="developer">Developer</option> <option value="developer">Developer</option>
<option value="designer">Designer</option> <option value="designer">Designer</option>
<option value="manager">Manager</option> <option value="manager">Manager</option>
<option value="founder">Founder</option> <option value="founder">Founder</option>
<option value="other">Other</option> <option value="other">Other</option>
</select> </select>
</FormGroup> </FormGroup>
<FormGroup label="Team size" class=""> <FormGroup label="Team size" class="">
<div class="form-selectgroup"> <div class="form-selectgroup">
<label class="form-selectgroup-item"> <label class="form-selectgroup-item">
<input type="radio" name="team-size" value="1" class="form-selectgroup-input" checked /> <input type="radio" name="team-size" value="1" class="form-selectgroup-input" checked />
<span class="form-selectgroup-label">Just me</span> <span class="form-selectgroup-label">Just me</span>
</label> </label>
<label class="form-selectgroup-item"> <label class="form-selectgroup-item">
<input type="radio" name="team-size" value="2-10" class="form-selectgroup-input" /> <input type="radio" name="team-size" value="2-10" class="form-selectgroup-input" />
<span class="form-selectgroup-label">2-10 people</span> <span class="form-selectgroup-label">2-10 people</span>
</label> </label>
<label class="form-selectgroup-item"> <label class="form-selectgroup-item">
<input type="radio" name="team-size" value="11-50" class="form-selectgroup-input" /> <input type="radio" name="team-size" value="11-50" class="form-selectgroup-input" />
<span class="form-selectgroup-label">11-50 people</span> <span class="form-selectgroup-label">11-50 people</span>
</label> </label>
<label class="form-selectgroup-item"> <label class="form-selectgroup-item">
<input type="radio" name="team-size" value="50+" class="form-selectgroup-input" /> <input type="radio" name="team-size" value="50+" class="form-selectgroup-input" />
<span class="form-selectgroup-label">50+ people</span> <span class="form-selectgroup-label">50+ people</span>
</label> </label>
</div> </div>
</FormGroup> </FormGroup>
<FormGroup label="What are you planning to use this for?" class=""> <FormGroup label="What are you planning to use this for?" class="">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="radio" value="personal" id="use-personal" checked /> <input class="form-check-input" type="radio" value="personal" id="use-personal" checked />
<label class="form-check-label" for="use-personal"> Personal projects </label> <label class="form-check-label" for="use-personal"> Personal projects </label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="radio" value="business" id="use-business" /> <input class="form-check-input" type="radio" value="business" id="use-business" />
<label class="form-check-label" for="use-business"> Business applications </label> <label class="form-check-label" for="use-business"> Business applications </label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="radio" value="client" id="use-client" /> <input class="form-check-input" type="radio" value="client" id="use-client" />
<label class="form-check-label" for="use-client"> Client work </label> <label class="form-check-label" for="use-client"> Client work </label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="radio" value="learning" id="use-learning" /> <input class="form-check-input" type="radio" value="learning" id="use-learning" />
<label class="form-check-label" for="use-learning"> Learning and experimentation </label> <label class="form-check-label" for="use-learning"> Learning and experimentation </label>
</div> </div>
</FormGroup> </FormGroup>
<FormGroup label="How did you hear about us?" class=""> <FormGroup label="How did you hear about us?" class="">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" name="referral" value="search" id="ref-search" /> <input class="form-check-input" type="checkbox" name="referral" value="search" id="ref-search" />
<label class="form-check-label" for="ref-search"> Search engine </label> <label class="form-check-label" for="ref-search"> Search engine </label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" name="referral" value="social" id="ref-social" /> <input class="form-check-input" type="checkbox" name="referral" value="social" id="ref-social" />
<label class="form-check-label" for="ref-social"> Social media </label> <label class="form-check-label" for="ref-social"> Social media </label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" name="referral" value="friend" id="ref-friend" /> <input class="form-check-input" type="checkbox" name="referral" value="friend" id="ref-friend" />
<label class="form-check-label" for="ref-friend"> Friend or colleague </label> <label class="form-check-label" for="ref-friend"> Friend or colleague </label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" name="referral" value="blog" id="ref-blog" /> <input class="form-check-input" type="checkbox" name="referral" value="blog" id="ref-blog" />
<label class="form-check-label" for="ref-blog"> Blog or article </label> <label class="form-check-label" for="ref-blog"> Blog or article </label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" name="referral" value="other" id="ref-other" /> <input class="form-check-input" type="checkbox" name="referral" value="other" id="ref-other" />
<label class="form-check-label" for="ref-other"> Other </label> <label class="form-check-label" for="ref-other"> Other </label>
</div> </div>
</FormGroup> </FormGroup>
<div> <div>
<div class="form-check form-switch"> <div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="notifications" checked /> <input class="form-check-input" type="checkbox" id="notifications" checked />
<label class="form-check-label" for="notifications"> Send me product updates and tips via email </label> <label class="form-check-label" for="notifications"> Send me product updates and tips via email </label>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<ButtonList class="justify-content-between mt-4"> <ButtonList class="justify-content-between mt-4">
<Button text="Back" color="link link-secondary" /> <Button text="Back" color="link link-secondary" />
<Button text="Continue" color="primary" /> <Button text="Continue" color="primary" />
</ButtonList> </ButtonList>
</div> </div>
</main> </main>
</BaseLayout> </BaseLayout>
+120 -79
View File
@@ -1,92 +1,133 @@
--- ---
// page-libs [tabler-payments, imask]: `tabler-payments` is not a key in // tabler-payments is not in core/libs.json — only imask emits a script tag.
// core/libs.json, so it resolves to nothing (only imask emits a <script>) — import PayLayout from '@shared/layouts/PayLayout.astro';
// matches the Eleventy reference. import Avatar from '@ui/Avatar.astro';
import PayLayout from '@shared/layouts/PayLayout.astro' import Payment from '@ui/Payment.astro';
import Avatar from '@ui/Avatar.astro' import FormGroup from '@ui/FormGroup.astro';
import Payment from '@ui/Payment.astro' import Icon from '@ui/Icon.astro';
import FormGroup from '@ui/FormGroup.astro'
import Icon from '@ui/Icon.astro'
--- ---
<PayLayout title="Pay" pageLibs={['tabler-payments', 'imask']}> <PayLayout title="Pay" pageLibs={['tabler-payments', 'imask']}>
<div class="bg-dark bg-cover" style="background-image: url(./static/bg-cover.jpg)"> <div class="bg-dark bg-cover" style="background-image: url(./static/bg-cover.jpg)">
<div class="bg-dark bg-opacity-75 pt-5 bg-blur"> <div class="bg-dark bg-opacity-75 pt-5 bg-blur">
<div class="container container-tight bg-overlay"> <div class="container container-tight bg-overlay">
<img src="./static/bg-cover.jpg" alt="" class="img-fluid rounded-lg rounded-top" /> <img src="./static/bg-cover.jpg" alt="" class="img-fluid rounded-lg rounded-top" />
</div> </div>
</div> </div>
</div> </div>
<div class="container container-tight"> <div class="container container-tight">
<div class="card rounded-top-0 border-top-0"> <div class="card rounded-top-0 border-top-0">
<div class="card-body pt-0"> <div class="card-body pt-0">
<div class="mb-3 text-center"> <div class="mb-3 text-center">
<Avatar personId={1} size="xl" class="avatar-cover rounded-circle mb-3" /> <Avatar personId={1} size="xl" class="avatar-cover rounded-circle mb-3" />
<div class="mb-4"> <div class="mb-4">
<h2 class="h2">Front-End Learning</h2> <h2 class="h2">Front-End Learning</h2>
<div class="text-secondary">Learn to build web apps with HTML & CSS. Get started quickly with included templates.</div> <div class="text-secondary">
</div> Learn to build web apps with HTML & CSS. Get started quickly with included templates.
</div> </div>
</div>
</div>
<nav class="nav nav-segmented w-100 mb-4" role="tablist">
<button data-bs-target="#tab-card" class="nav-link active" data-bs-toggle="tab" role="tab" aria-controls="tab-card" aria-selected="true">
<Icon name="credit-card" />
<span>Pay With Card</span>
</button>
<button data-bs-target="#tab-paypal" class="nav-link" data-bs-toggle="tab" role="tab" aria-controls="tab-paypal" aria-selected="false">
<Icon name="brand-paypal" />
<span>Pay With PayPal</span>
</button>
</nav>
<div class="tab-content"> <nav class="nav nav-segmented w-100 mb-4" role="tablist">
<div class="tab-pane active show" id="tab-card" role="tabpanel"> <button
<form> data-bs-target="#tab-card"
<div class="space-y"> class="nav-link active"
<FormGroup label="Card Number" for="card-number" class=""> data-bs-toggle="tab"
<div class="input-group input-group-flat"> role="tab"
<span class="input-group-text"> aria-controls="tab-card"
<Payment payment="visa" size="xs" /> aria-selected="true"
</span> >
<input type="text" class="form-control" placeholder="0000 0000 0000 0000" autocomplete="off" id="card-number-addon" /> <Icon name="credit-card" />
</div> <span>Pay With Card</span>
</FormGroup> </button>
<button
data-bs-target="#tab-paypal"
class="nav-link"
data-bs-toggle="tab"
role="tab"
aria-controls="tab-paypal"
aria-selected="false"
>
<Icon name="brand-paypal" />
<span>Pay With PayPal</span>
</button>
</nav>
<div class="row g-3"> <div class="tab-content">
<FormGroup label="Expiry Date" for="card-expiry" class="col-sm-6"> <div class="tab-pane active show" id="tab-card" role="tabpanel">
<input type="text" class="form-control" id="card-expiry" placeholder="MM/YY" inputmode="numeric" aria-required="true" maxlength="5" /> <form>
</FormGroup> <div class="space-y">
<FormGroup label="CVC" for="card-cvc" class="col-sm-6"> <FormGroup label="Card Number" for="card-number" class="">
<input type="text" class="form-control" id="card-cvc" placeholder="CVC" inputmode="numeric" aria-required="true" maxlength="3" /> <div class="input-group input-group-flat">
</FormGroup> <span class="input-group-text">
</div> <Payment payment="visa" size="xs" />
</span>
<input
type="text"
class="form-control"
placeholder="0000 0000 0000 0000"
autocomplete="off"
id="card-number-addon"
/>
</div>
</FormGroup>
<FormGroup label="Name on Card" for="card-name" class=""> <div class="row g-3">
<input type="text" class="form-control" id="card-name" placeholder="Full name" aria-required="true" /> <FormGroup label="Expiry Date" for="card-expiry" class="col-sm-6">
</FormGroup> <input
type="text"
class="form-control"
id="card-expiry"
placeholder="MM/YY"
inputmode="numeric"
aria-required="true"
maxlength="5"
/>
</FormGroup>
<FormGroup label="CVC" for="card-cvc" class="col-sm-6">
<input
type="text"
class="form-control"
id="card-cvc"
placeholder="CVC"
inputmode="numeric"
aria-required="true"
maxlength="3"
/>
</FormGroup>
</div>
<FormGroup label="Email" for="card-email" class=""> <FormGroup label="Name on Card" for="card-name" class="">
<input type="email" class="form-control" id="card-email" placeholder="you@example.com" aria-required="true" /> <input type="text" class="form-control" id="card-name" placeholder="Full name" aria-required="true" />
</FormGroup> </FormGroup>
<div> <FormGroup label="Email" for="card-email" class="">
<button type="button" class="btn btn-primary w-100"> Pay $253 </button> <input type="email" class="form-control" id="card-email" placeholder="you@example.com" aria-required="true" />
<div class="text-secondary text-center small mt-3">You'll be charged $253, including $48 for VAT in Poland</div> </FormGroup>
</div>
</div>
</form>
</div>
<div class="tab-pane" id="tab-paypal" role="tabpanel"> <div>
<button type="button" class="btn btn-primary w-100"> <button type="button" class="btn btn-primary w-100"> Pay $253 </button>
<Icon name="brand-paypal" /> <div class="text-secondary text-center small mt-3">
<span class="ms-2">Pay with PayPal - $253</span> You'll be charged $253, including $48 for VAT in Poland
</button> </div>
<div class="text-secondary text-center small mt-3">You'll be charged $253, including $48 for VAT in Poland</div> </div>
</div> </div>
</div> </form>
</div> </div>
</div>
</div> <div class="tab-pane" id="tab-paypal" role="tabpanel">
<button type="button" class="btn btn-primary w-100">
<Icon name="brand-paypal" />
<span class="ms-2">Pay with PayPal - $253</span>
</button>
<div class="text-secondary text-center small mt-3">
You'll be charged $253, including $48 for VAT in Poland
</div>
</div>
</div>
</div>
</div>
</div>
</PayLayout> </PayLayout>
+309 -325
View File
@@ -1,340 +1,324 @@
--- ---
// Reuses committed components: Icon, Button, Badge, Pagination (+ Avatar). import ButtonGroup from '@ui/ButtonGroup.astro';
// import DefaultLayout from '@shared/layouts/DefaultLayout.astro';
// NOTE (no-container): the front matter is `no-container: true`, but Liquid's import Icon from '@ui/Icon.astro';
// `page.no-container` (hyphen key via dot access) does NOT resolve, so the import Button from '@ui/Button.astro';
// reference build renders the normal `.container-xl` wrapper (default slot). import Badge from '@ui/Badge.astro';
// We therefore use DefaultLayout unchanged — no special no-container handling. import Avatar from '@ui/Avatar.astro';
// import AvatarList from '@ui/AvatarList.astro';
// NOTE (page-menu): none in the front matter → pageMenu is intentionally NOT import Pagination from '@ui/Pagination.astro';
// passed. import Card from '@ui/Card.astro';
// import CardBody from '@ui/CardBody.astro';
// TODO (Button.astro missing params): the two sort-direction buttons use
// `element="label"` + `html_for` + `tooltip`, none of which Button.astro
// supports (element type is 'a'|'button'; html_for/tooltip absent). The shared
// ui/button.html ignores html_for/tooltip too, and the reference emits a plain
// <label> with no `for`. Inlined below to match the reference.
import ButtonGroup from '@ui/ButtonGroup.astro'
import DefaultLayout from '@shared/layouts/DefaultLayout.astro'
import Icon from '@ui/Icon.astro'
import Button from '@ui/Button.astro'
import Badge from '@ui/Badge.astro'
import Avatar from '@ui/Avatar.astro'
import AvatarList from '@ui/AvatarList.astro'
import Pagination from '@ui/Pagination.astro'
import Card from '@ui/Card.astro'
import CardBody from '@ui/CardBody.astro'
--- ---
<DefaultLayout title="Categories playground"> <DefaultLayout title="Categories playground">
<!-- Header --> <!-- Header -->
<div class="row align-items-center gy-3 mb-5"> <div class="row align-items-center gy-3 mb-5">
<div class="col"> <div class="col">
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb mb-1"> <ol class="breadcrumb mb-1">
<li class="breadcrumb-item"> <li class="breadcrumb-item">
<a class="text-secondary" href="#"> <a class="text-secondary" href="#">
<Icon name="arrow-left" class="icon-inline me-1" /> <Icon name="arrow-left" class="icon-inline me-1" />
Back Back
</a> </a>
</li> </li>
</ol> </ol>
</nav> </nav>
<h1 class="fs-3 mb-0">Categories</h1> <h1 class="fs-3 mb-0">Categories</h1>
</div> </div>
<div class="col-12 col-lg-auto order-last order-lg-0"> <div class="col-12 col-lg-auto order-last order-lg-0">
<div class="row align-items-center gx-3"> <div class="row align-items-center gx-3">
<div class="col col-lg-auto"> <div class="col col-lg-auto">
<div class="input-group input-group-flat rounded-pill"> <div class="input-group input-group-flat rounded-pill">
<input type="text" class="form-control" placeholder="Search" aria-label="Search" aria-describedby="playground-categories-search" /> <input type="text" class="form-control" placeholder="Search" aria-label="Search" aria-describedby="playground-categories-search">
<span class="input-group-text" id="playground-categories-search"> <span class="input-group-text" id="playground-categories-search">
<Icon name="search" /> <Icon name="search" />
</span> </span>
</div> </div>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<div class="dropdown"> <div class="dropdown">
<Button element="button" type="button" icon="filter" iconOnly text="Open filters" /> <Button element="button" type="button" icon="filter" iconOnly text="Open filters" />
<div class="dropdown-menu rounded-3 p-6"> <div class="dropdown-menu rounded-3 p-6">
<h4 class="fs-lg mb-4">Filter</h4> <h4 class="fs-lg mb-4">Filter</h4>
<form id="playgroundCategoriesFilterForm" style="width: 350px"> <form id="playgroundCategoriesFilterForm" style="width: 350px">
<div class="row align-items-center mb-3"> <div class="row align-items-center mb-3">
<div class="col-3"> <div class="col-3">
<label class="form-label mb-0" for="playgroundFilterUser">User</label> <label class="form-label mb-0" for="playgroundFilterUser">User</label>
</div> </div>
<div class="col-9"> <div class="col-9">
<select class="form-select" id="playgroundFilterUser" aria-label="User"> <select class="form-select" id="playgroundFilterUser" aria-label="User">
<option value="Emily Thompson" selected>Emily Thompson</option> <option value="Emily Thompson" selected>Emily Thompson</option>
<option value="Michael Johnson">Michael Johnson</option> <option value="Michael Johnson">Michael Johnson</option>
<option value="Robert Garcia">Robert Garcia</option> <option value="Robert Garcia">Robert Garcia</option>
<option value="Jessica Miller">Jessica Miller</option> <option value="Jessica Miller">Jessica Miller</option>
</select> </select>
</div> </div>
</div> </div>
<div class="row align-items-center mb-3"> <div class="row align-items-center mb-3">
<div class="col-3"> <div class="col-3">
<label class="form-label mb-0" for="playgroundFilterCompany">Company</label> <label class="form-label mb-0" for="playgroundFilterCompany">Company</label>
</div> </div>
<div class="col-9"> <div class="col-9">
<select class="form-select" id="playgroundFilterCompany" aria-label="Company"> <select class="form-select" id="playgroundFilterCompany" aria-label="Company">
<option value="TechPinnacle Solutions" selected>TechPinnacle Solutions</option> <option value="TechPinnacle Solutions" selected>TechPinnacle Solutions</option>
<option value="Quantum Dynamics">Quantum Dynamics</option> <option value="Quantum Dynamics">Quantum Dynamics</option>
<option value="Pinnacle Technologies">Pinnacle Technologies</option> <option value="Pinnacle Technologies">Pinnacle Technologies</option>
<option value="Apex Innovations">Apex Innovations</option> <option value="Apex Innovations">Apex Innovations</option>
</select> </select>
</div> </div>
</div> </div>
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col-3"> <div class="col-3">
<label class="form-label mb-0" for="playgroundFilterLocation">Location</label> <label class="form-label mb-0" for="playgroundFilterLocation">Location</label>
</div> </div>
<div class="col-9"> <div class="col-9">
<select class="form-select" id="playgroundFilterLocation" aria-label="Location"> <select class="form-select" id="playgroundFilterLocation" aria-label="Location">
<option value="San Francisco, CA" selected>San Francisco, CA</option> <option value="San Francisco, CA" selected>San Francisco, CA</option>
<option value="Austin, TX">Austin, TX</option> <option value="Austin, TX">Austin, TX</option>
<option value="Miami, FL">Miami, FL</option> <option value="Miami, FL">Miami, FL</option>
<option value="Seattle, WA">Seattle, WA</option> <option value="Seattle, WA">Seattle, WA</option>
</select> </select>
</div> </div>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
<div class="col-auto ms-n2"> <div class="col-auto ms-n2">
<div class="dropdown"> <div class="dropdown">
<Button element="button" type="button" icon="sort-ascending-letters" iconOnly text="Open sort options" /> <Button element="button" type="button" icon="sort-ascending-letters" iconOnly text="Open sort options" />
<div class="dropdown-menu rounded-3 p-6"> <div class="dropdown-menu rounded-3 p-6">
<h4 class="fs-lg mb-4">Sort</h4> <h4 class="fs-lg mb-4">Sort</h4>
<form id="playgroundCategoriesSortForm" style="width: 350px"> <form id="playgroundCategoriesSortForm" style="width: 350px">
<div class="row gx-3"> <div class="row gx-3">
<div class="col"> <div class="col">
<select class="form-select" id="playgroundSortField" aria-label="Sort by"> <select class="form-select" id="playgroundSortField" aria-label="Sort by">
<option value="user" selected>User</option> <option value="user" selected>User</option>
<option value="company">Company</option> <option value="company">Company</option>
<option value="phone">Phone</option> <option value="phone">Phone</option>
<option value="location">Location</option> <option value="location">Location</option>
</select> </select>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<ButtonGroup aria-label="Sort direction"> <ButtonGroup aria-label="Sort direction">
<input type="radio" class="btn-check" name="playgroundSortDir" id="playgroundSortAsc" autocomplete="off" checked /> <input type="radio" class="btn-check" name="playgroundSortDir" id="playgroundSortAsc" autocomplete="off" checked>
<label class="btn btn-light px-0 btn-icon" aria-label="Ascending"><Icon name="arrow-up" /></label> <label class="btn btn-light px-0 btn-icon" aria-label="Ascending"><Icon name="arrow-up" /></label>
<input type="radio" class="btn-check" name="playgroundSortDir" id="playgroundSortDesc" autocomplete="off" /> <input type="radio" class="btn-check" name="playgroundSortDir" id="playgroundSortDesc" autocomplete="off">
<label class="btn btn-light px-0 btn-icon" aria-label="Descending"><Icon name="arrow-down" /></label> <label class="btn btn-light px-0 btn-icon" aria-label="Descending"><Icon name="arrow-down" /></label>
</ButtonGroup> </ButtonGroup>
</div> </div>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<button class="btn btn-primary" type="button">New category</button> <button class="btn btn-primary" type="button">New category</button>
</div> </div>
</div> </div>
<hr class="my-7" /> <hr class="my-7">
<div class="row row-cards"> <div class="row row-cards">
<div class="col-12 col-lg-6"> <div class="col-12 col-lg-6">
<Card> <Card>
<CardBody> <CardBody>
<div class="row align-items-center mb-4"> <div class="row align-items-center mb-4">
<div class="col"> <div class="col">
<Badge text="Featured" color="primary" /> <Badge text="Featured" color="primary" />
</div> </div>
<div class="col-auto"> <div class="col-auto">
<Icon name="confetti" class="icon-lg text-primary opacity-50" /> <Icon name="confetti" class="icon-lg text-primary opacity-50" />
</div> </div>
</div> </div>
<h3 class="fs-5 mb-1 text-truncate"><a class="text-body" href="./empty.html">Announcements</a></h3> <h3 class="fs-5 mb-1 text-truncate"><a class="text-body" href="./empty.html">Announcements</a></h3>
<p class="text-secondary text-truncate">For company updates, system changes, or important notices that everyone should see.</p> <p class="text-secondary text-truncate">For company updates, system changes, or important notices that everyone should see.</p>
<p class="fs-sm text-secondary mb-0">Last post 2 days ago</p> <p class="fs-sm text-secondary mb-0">Last post 2 days ago</p>
<hr /> <hr>
<div class="d-flex align-items-center justify-content-between"> <div class="d-flex align-items-center justify-content-between">
<AvatarList stacked={true}> <AvatarList stacked={true}>
<Avatar src="static/avatars/001f.jpg" size="xs" data-bs-toggle="tooltip" data-bs-title="Emily Thompson" /> <Avatar src="static/avatars/001f.jpg" size="xs" data-bs-toggle="tooltip" data-bs-title="Emily Thompson" />
<Avatar src="static/avatars/002m.jpg" size="xs" data-bs-toggle="tooltip" data-bs-title="Michael Johnson" /> <Avatar src="static/avatars/002m.jpg" size="xs" data-bs-toggle="tooltip" data-bs-title="Michael Johnson" />
<Avatar src="static/avatars/003m.jpg" size="xs" data-bs-toggle="tooltip" data-bs-title="Robert Garcia" /> <Avatar src="static/avatars/003m.jpg" size="xs" data-bs-toggle="tooltip" data-bs-title="Robert Garcia" />
</AvatarList> </AvatarList>
<div class="text-end"> <div class="text-end">
<Badge text="20 posts" class="bg-light text-body" /> <Badge text="20 posts" class="bg-light text-body" />
</div> </div>
</div> </div>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col-12 col-lg-6"> <div class="col-12 col-lg-6">
<Card> <Card>
<CardBody> <CardBody>
<div class="row align-items-center mb-4"> <div class="row align-items-center mb-4">
<div class="col"> <div class="col">
<Badge text="Featured" color="success" /> <Badge text="Featured" color="success" />
</div> </div>
<div class="col-auto"> <div class="col-auto">
<Icon name="slideshow" class="icon-lg text-success opacity-50" /> <Icon name="slideshow" class="icon-lg text-success opacity-50" />
</div> </div>
</div> </div>
<h3 class="fs-5 mb-1 text-truncate"><a class="text-body" href="./empty.html">Marketing campaigns</a></h3> <h3 class="fs-5 mb-1 text-truncate"><a class="text-body" href="./empty.html">Marketing campaigns</a></h3>
<p class="text-secondary text-truncate">Posts related to new campaigns, marketing strategies, and promotions.</p> <p class="text-secondary text-truncate">Posts related to new campaigns, marketing strategies, and promotions.</p>
<p class="fs-sm text-secondary mb-0">Last post 1 day ago</p> <p class="fs-sm text-secondary mb-0">Last post 1 day ago</p>
<hr /> <hr>
<div class="d-flex align-items-center justify-content-between"> <div class="d-flex align-items-center justify-content-between">
<AvatarList stacked={true} size="xs" offset={3} limit={3} /> <AvatarList stacked={true} size="xs" offset={3} limit={3} />
<div class="text-end"> <div class="text-end">
<Badge text="62 posts" class="bg-light text-body" /> <Badge text="62 posts" class="bg-light text-body" />
</div> </div>
</div> </div>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col-12 col-md-6 col-lg-4"> <div class="col-12 col-md-6 col-lg-4">
<Card class="position-relative overflow-hidden"> <Card class="position-relative overflow-hidden">
<Icon name="chart-pie" class="icon-lg text-danger opacity-25 position-absolute top-0 end-0 m-3" /> <Icon name="chart-pie" class="icon-lg text-danger opacity-25 position-absolute top-0 end-0 m-3" />
<CardBody> <CardBody>
<small class="text-secondary d-block mb-4">12 followers</small> <small class="text-secondary d-block mb-4">12 followers</small>
<h3 class="fs-6 mb-1 text-truncate"><a class="text-body" href="./empty.html">Reports &amp; analytics</a></h3> <h3 class="fs-6 mb-1 text-truncate"><a class="text-body" href="./empty.html">Reports &amp; analytics</a></h3>
<p class="text-secondary text-truncate">Data-driven insights and performance reports across all departments.</p> <p class="text-secondary text-truncate">Data-driven insights and performance reports across all departments.</p>
<p class="fs-sm text-secondary mb-0">Last post 5 days ago</p> <p class="fs-sm text-secondary mb-0">Last post 5 days ago</p>
<hr /> <hr>
<div class="d-flex align-items-center justify-content-between"> <div class="d-flex align-items-center justify-content-between">
<AvatarList stacked={true}> <AvatarList stacked={true}>
<Avatar src="static/avatars/001f.jpg" size="xs" data-bs-toggle="tooltip" data-bs-title="Emily Thompson" /> <Avatar src="static/avatars/001f.jpg" size="xs" data-bs-toggle="tooltip" data-bs-title="Emily Thompson" />
<Avatar src="static/avatars/003m.jpg" size="xs" data-bs-toggle="tooltip" data-bs-title="Robert Garcia" /> <Avatar src="static/avatars/003m.jpg" size="xs" data-bs-toggle="tooltip" data-bs-title="Robert Garcia" />
</AvatarList> </AvatarList>
<div class="text-end"> <div class="text-end">
<Badge text="28 posts" class="bg-light text-body" /> <Badge text="28 posts" class="bg-light text-body" />
</div> </div>
</div> </div>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col-12 col-md-6 col-lg-4"> <div class="col-12 col-md-6 col-lg-4">
<Card class="position-relative overflow-hidden"> <Card class="position-relative overflow-hidden">
<Icon name="id-badge" class="icon-lg text-warning opacity-25 position-absolute top-0 end-0 m-3" /> <Icon name="id-badge" class="icon-lg text-warning opacity-25 position-absolute top-0 end-0 m-3" />
<CardBody> <CardBody>
<small class="text-secondary d-block mb-4">16 followers</small> <small class="text-secondary d-block mb-4">16 followers</small>
<h3 class="fs-6 mb-1 text-truncate"><a class="text-body" href="./empty.html">Events &amp; webinars</a></h3> <h3 class="fs-6 mb-1 text-truncate"><a class="text-body" href="./empty.html">Events &amp; webinars</a></h3>
<p class="text-secondary text-truncate">Upcoming events, webinar recordings, and community meetups.</p> <p class="text-secondary text-truncate">Upcoming events, webinar recordings, and community meetups.</p>
<p class="fs-sm text-secondary mb-0">Last post 3 days ago</p> <p class="fs-sm text-secondary mb-0">Last post 3 days ago</p>
<hr /> <hr>
<div class="d-flex align-items-center justify-content-between"> <div class="d-flex align-items-center justify-content-between">
<AvatarList stacked={true}> <AvatarList stacked={true}>
<Avatar src="static/avatars/005f.jpg" size="xs" data-bs-toggle="tooltip" data-bs-title="Olivia Davis" /> <Avatar src="static/avatars/005f.jpg" size="xs" data-bs-toggle="tooltip" data-bs-title="Olivia Davis" />
<Avatar src="static/avatars/002m.jpg" size="xs" data-bs-toggle="tooltip" data-bs-title="Michael Johnson" /> <Avatar src="static/avatars/002m.jpg" size="xs" data-bs-toggle="tooltip" data-bs-title="Michael Johnson" />
</AvatarList> </AvatarList>
<div class="text-end"> <div class="text-end">
<Badge text="34 posts" class="bg-light text-body" /> <Badge text="34 posts" class="bg-light text-body" />
</div> </div>
</div> </div>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col-12 col-md-6 col-lg-4"> <div class="col-12 col-md-6 col-lg-4">
<Card class="position-relative overflow-hidden"> <Card class="position-relative overflow-hidden">
<Icon name="user" class="icon-lg text-warning opacity-25 position-absolute top-0 end-0 m-3" /> <Icon name="user" class="icon-lg text-warning opacity-25 position-absolute top-0 end-0 m-3" />
<CardBody> <CardBody>
<small class="text-secondary d-block mb-4">9 followers</small> <small class="text-secondary d-block mb-4">9 followers</small>
<h3 class="fs-6 mb-1 text-truncate"><a class="text-body" href="./empty.html">Client success stories</a></h3> <h3 class="fs-6 mb-1 text-truncate"><a class="text-body" href="./empty.html">Client success stories</a></h3>
<p class="text-secondary text-truncate">Case studies and testimonials from our most satisfied customers.</p> <p class="text-secondary text-truncate">Case studies and testimonials from our most satisfied customers.</p>
<p class="fs-sm text-secondary mb-0">Last post 1 week ago</p> <p class="fs-sm text-secondary mb-0">Last post 1 week ago</p>
<hr /> <hr>
<div class="d-flex align-items-center justify-content-between"> <div class="d-flex align-items-center justify-content-between">
<AvatarList stacked={true}> <AvatarList stacked={true}>
<Avatar src="static/avatars/006m.jpg" size="xs" data-bs-toggle="tooltip" data-bs-title="James Wilson" /> <Avatar src="static/avatars/006m.jpg" size="xs" data-bs-toggle="tooltip" data-bs-title="James Wilson" />
<Avatar src="static/avatars/004f.jpg" size="xs" data-bs-toggle="tooltip" data-bs-title="Jessica Miller" /> <Avatar src="static/avatars/004f.jpg" size="xs" data-bs-toggle="tooltip" data-bs-title="Jessica Miller" />
</AvatarList> </AvatarList>
<div class="text-end"> <div class="text-end">
<Badge text="64 posts" class="bg-light text-body" /> <Badge text="64 posts" class="bg-light text-body" />
</div> </div>
</div> </div>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col-12 col-md-6 col-lg-4"> <div class="col-12 col-md-6 col-lg-4">
<Card class="position-relative overflow-hidden"> <Card class="position-relative overflow-hidden">
<Icon name="message" class="icon-lg text-primary opacity-25 position-absolute top-0 end-0 m-3" /> <Icon name="message" class="icon-lg text-primary opacity-25 position-absolute top-0 end-0 m-3" />
<CardBody> <CardBody>
<small class="text-secondary d-block mb-4">10 followers</small> <small class="text-secondary d-block mb-4">10 followers</small>
<h3 class="fs-6 mb-1 text-truncate"><a class="text-body" href="./empty.html">Team highlights</a></h3> <h3 class="fs-6 mb-1 text-truncate"><a class="text-body" href="./empty.html">Team highlights</a></h3>
<p class="text-secondary text-truncate">Spotlights, shoutouts, and stories from people across the team.</p> <p class="text-secondary text-truncate">Spotlights, shoutouts, and stories from people across the team.</p>
<p class="fs-sm text-secondary mb-0">Last post 4 days ago</p> <p class="fs-sm text-secondary mb-0">Last post 4 days ago</p>
<hr /> <hr>
<div class="d-flex align-items-center justify-content-between"> <div class="d-flex align-items-center justify-content-between">
<AvatarList stacked={true}> <AvatarList stacked={true}>
<Avatar src="static/avatars/002m.jpg" size="xs" data-bs-toggle="tooltip" data-bs-title="Michael Johnson" /> <Avatar src="static/avatars/002m.jpg" size="xs" data-bs-toggle="tooltip" data-bs-title="Michael Johnson" />
<Avatar src="static/avatars/001f.jpg" size="xs" data-bs-toggle="tooltip" data-bs-title="Emily Thompson" /> <Avatar src="static/avatars/001f.jpg" size="xs" data-bs-toggle="tooltip" data-bs-title="Emily Thompson" />
</AvatarList> </AvatarList>
<div class="text-end"> <div class="text-end">
<Badge text="42 posts" class="bg-light text-body" /> <Badge text="42 posts" class="bg-light text-body" />
</div> </div>
</div> </div>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col-12 col-md-6 col-lg-4"> <div class="col-12 col-md-6 col-lg-4">
<Card class="position-relative overflow-hidden"> <Card class="position-relative overflow-hidden">
<Icon name="building-bank" class="icon-lg text-danger opacity-25 position-absolute top-0 end-0 m-3" /> <Icon name="building-bank" class="icon-lg text-danger opacity-25 position-absolute top-0 end-0 m-3" />
<CardBody> <CardBody>
<small class="text-secondary d-block mb-4">26 followers</small> <small class="text-secondary d-block mb-4">26 followers</small>
<h3 class="fs-6 mb-1 text-truncate"><a class="text-body" href="./empty.html">Training &amp; resources</a></h3> <h3 class="fs-6 mb-1 text-truncate"><a class="text-body" href="./empty.html">Training &amp; resources</a></h3>
<p class="text-secondary text-truncate">Access to learning materials, tutorials, or resources for users and staff.</p> <p class="text-secondary text-truncate">Access to learning materials, tutorials, or resources for users and staff.</p>
<p class="fs-sm text-secondary mb-0">Last post 6 days ago</p> <p class="fs-sm text-secondary mb-0">Last post 6 days ago</p>
<hr /> <hr>
<div class="d-flex align-items-center justify-content-between"> <div class="d-flex align-items-center justify-content-between">
<AvatarList stacked={true}> <AvatarList stacked={true}>
<Avatar src="static/avatars/003m.jpg" size="xs" data-bs-toggle="tooltip" data-bs-title="Robert Garcia" /> <Avatar src="static/avatars/003m.jpg" size="xs" data-bs-toggle="tooltip" data-bs-title="Robert Garcia" />
<Avatar src="static/avatars/005f.jpg" size="xs" data-bs-toggle="tooltip" data-bs-title="Olivia Davis" /> <Avatar src="static/avatars/005f.jpg" size="xs" data-bs-toggle="tooltip" data-bs-title="Olivia Davis" />
</AvatarList> </AvatarList>
<div class="text-end"> <div class="text-end">
<Badge text="38 posts" class="bg-light text-body" /> <Badge text="38 posts" class="bg-light text-body" />
</div> </div>
</div> </div>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col-12 col-md-6 col-lg-4"> <div class="col-12 col-md-6 col-lg-4">
<Card class="position-relative overflow-hidden"> <Card class="position-relative overflow-hidden">
<Icon name="brand-apple" class="icon-lg text-primary opacity-25 position-absolute top-0 end-0 m-3" /> <Icon name="brand-apple" class="icon-lg text-primary opacity-25 position-absolute top-0 end-0 m-3" />
<CardBody> <CardBody>
<small class="text-secondary d-block mb-4">6 followers</small> <small class="text-secondary d-block mb-4">6 followers</small>
<h3 class="fs-6 mb-1 text-truncate"><a class="text-body" href="./empty.html">Industry news</a></h3> <h3 class="fs-6 mb-1 text-truncate"><a class="text-body" href="./empty.html">Industry news</a></h3>
<p class="text-secondary text-truncate">Trends, news, and commentary from around the industry landscape.</p> <p class="text-secondary text-truncate">Trends, news, and commentary from around the industry landscape.</p>
<p class="fs-sm text-secondary mb-0">Last post 2 weeks ago</p> <p class="fs-sm text-secondary mb-0">Last post 2 weeks ago</p>
<hr /> <hr>
<div class="d-flex align-items-center justify-content-between"> <div class="d-flex align-items-center justify-content-between">
<AvatarList stacked={true}> <AvatarList stacked={true}>
<Avatar src="static/avatars/006m.jpg" size="xs" data-bs-toggle="tooltip" data-bs-title="James Wilson" /> <Avatar src="static/avatars/006m.jpg" size="xs" data-bs-toggle="tooltip" data-bs-title="James Wilson" />
<Avatar src="static/avatars/004f.jpg" size="xs" data-bs-toggle="tooltip" data-bs-title="Jessica Miller" /> <Avatar src="static/avatars/004f.jpg" size="xs" data-bs-toggle="tooltip" data-bs-title="Jessica Miller" />
</AvatarList> </AvatarList>
<div class="text-end"> <div class="text-end">
<Badge text="59 posts" class="bg-light text-body" /> <Badge text="59 posts" class="bg-light text-body" />
</div> </div>
</div> </div>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
</div> </div>
<div class="row align-items-center mt-7"> <div class="row align-items-center mt-7">
<div class="col"> <div class="col">
<p class="text-secondary mb-0">1 8 (8 total)</p> <p class="text-secondary mb-0">1 8 (8 total)</p>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<nav aria-label="Page navigation"> <nav aria-label="Page navigation">
<Pagination count={3} offset={3} activeItem={1} class="mb-0" /> <Pagination count={3} offset={3} activeItem={1} class="mb-0" />
</nav> </nav>
</div> </div>
</div> </div>
</DefaultLayout> </DefaultLayout>
+21 -20
View File
@@ -1,24 +1,25 @@
--- ---
// Front matter: layout-sidebar + layout-sidebar-dark + page-header + pretitle. import DefaultLayout from '@shared/layouts/DefaultLayout.astro';
// Note: layout-navbar-toolbar is an inert front-matter key — it is not import Card from '@ui/Card.astro';
// referenced by default.html or any include, so there is no prop for it. import CardBody from '@ui/CardBody.astro';
// No page-menu front matter → pageMenu is intentionally not passed (nothing import CardTitle from '@ui/CardTitle.astro';
// active in the navbar/sidebar, matching the Eleventy output).
import DefaultLayout from '@shared/layouts/DefaultLayout.astro'
import Card from '@ui/Card.astro'
import CardBody from '@ui/CardBody.astro'
import CardTitle from '@ui/CardTitle.astro'
--- ---
<DefaultLayout title="Layout playground" pageHeader="Layout playground" pretitle="Playground" sidebar sidebarDark> <DefaultLayout
<div class="row row-cards"> title="Layout playground"
<div class="col-12"> pageHeader="Layout playground"
<Card> pretitle="Playground"
<CardBody> sidebar
<CardTitle as="h2">Sandbox</CardTitle> sidebarDark
<p class="text-secondary mb-0">Edit this page to try layout options, components, and styles.</p> >
</CardBody> <div class="row row-cards">
</Card> <div class="col-12">
</div> <Card>
</div> <CardBody>
<CardTitle as="h2">Sandbox</CardTitle>
<p class="text-secondary mb-0">Edit this page to try layout options, components, and styles.</p>
</CardBody>
</Card>
</div>
</div>
</DefaultLayout> </DefaultLayout>
+232 -237
View File
@@ -1,255 +1,250 @@
--- ---
// Reuses committed components: ProgressSteps, Datepicker, Trending. import Subheader from '@ui/Subheader.astro';
// import DefaultLayout from '@shared/layouts/DefaultLayout.astro';
// NOTE (datepicker params): the source passes many params to ui/datepicker.html import ButtonList from '@ui/ButtonList.astro';
// (date-min, date-max, selection-mode, placement, display-months-count, import Card from '@ui/Card.astro';
// first-weekday, selected-dates). The current shared ui/datepicker.html is import CardHeader from '@ui/CardHeader.astro';
// Litepicker-based and IGNORES all of them — the reference build renders only a import CardBody from '@ui/CardBody.astro';
// plain input (id/value/layout/inline). Datepicker.astro mirrors that include, so import ProgressSteps from '@ui/ProgressSteps.astro';
// we pass only the supported props and drop the rest, matching the reference. import Datepicker from '@ui/Datepicker.astro';
// import FormGroup from '@ui/FormGroup.astro';
// NOTE (page-libs): front matter is `page-libs: [vanilla-calendar-pro]`, but that import Trending from '@ui/Trending.astro';
// lib is NOT registered in @tabler/core/libs.json, so BaseLayout emits no assets
// for it (matches the reference: empty PAGE LEVEL STYLES, no vanilla output).
// Passed through faithfully anyway.
//
// NOTE: the `page-header-link` front matter key ("Go to Team Settings") is inert —
// nothing in the layout/page-header consumes it, and it is absent from the
// reference. No prop for it.
import Subheader from '@ui/Subheader.astro'
import DefaultLayout from '@shared/layouts/DefaultLayout.astro'
import ButtonList from '@ui/ButtonList.astro'
import Card from '@ui/Card.astro'
import CardHeader from '@ui/CardHeader.astro'
import CardBody from '@ui/CardBody.astro'
import ProgressSteps from '@ui/ProgressSteps.astro'
import Datepicker from '@ui/Datepicker.astro'
import FormGroup from '@ui/FormGroup.astro'
import Trending from '@ui/Trending.astro'
--- ---
<DefaultLayout title="Playground" pageHeader="Project Settings" pageLibs={['vanilla-calendar-pro']}> <DefaultLayout title="Playground" pageHeader="Project Settings" pageLibs={['vanilla-calendar-pro']}>
<div class="row row-cards row-deck"> <div class="row row-cards row-deck">
<div class="col-12">
<Card>
<CardBody>
<div class="d-flex flex-wrap gap-4">
<div class="bg-pattern-diagonal w-10 h-10 border rounded"></div>
<div class="bg-pattern-diagonal-2 w-10 h-10 border rounded"></div>
<div class="bg-pattern-dots w-10 h-10 border rounded"></div>
<div class="bg-pattern-lines w-10 h-10 border rounded"></div>
<div class="bg-pattern-rectangles w-10 h-10 border rounded"></div>
<div class="bg-pattern-lines-vertical w-10 h-10 border rounded"></div>
<div class="bg-pattern-grid w-10 h-10 border rounded"></div>
<div class="bg-pattern-grid-diagonal w-10 h-10 border rounded"></div>
<div class="bg-pattern-blueprint w-10 h-10 border rounded"></div>
<div class="bg-pattern-cross-dots w-10 h-10 border rounded"></div>
<div class="bg-pattern-diagonal-stripes w-10 h-10 border rounded"></div>
<div class="bg-pattern-diagonal-stripes-2 w-10 h-10 border rounded"></div>
<div class="bg-pattern-zigzag w-10 h-10 border rounded"></div>
<div class="bg-pattern-vertical-stripes w-10 h-10 border rounded"></div>
<div class="bg-pattern-horizontal-stripes w-10 h-10 border rounded"></div>
</div>
</CardBody>
</Card>
</div>
<div class="col-6">
<Card>
<CardBody>
<ButtonList>
<a href="" class="btn" style="border-color: var(--tblr-border-color);">Button</a>
<a href="" class="btn" style="border-color: var(--tblr-border-color-translucent);">Button</a>
<a href="" class="btn" style="border-color: var(--tblr-border-dark-color);">Button</a>
<a href="" class="btn" style="border-color: var(--tblr-border-dark-color-translucent);">Button</a>
<a href="" class="btn btn-primary">Button</a>
</ButtonList>
<ButtonList class="mt-4">
<a href="" class="btn">Button</a>
<a href="" class="btn">Button</a>
<a href="" class="btn">Button</a>
<a href="" class="btn">Button</a>
<a href="" class="btn btn-primary">Button</a>
</ButtonList>
</CardBody>
</Card>
</div>
<div class="col-3">
<Card>
<CardBody> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. </CardBody>
</Card>
</div>
<div class="col-6"> <div class="col-12">
<Card> <Card>
<CardBody> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. </CardBody> <CardBody>
</Card> <div class="d-flex flex-wrap gap-4">
</div> <div class="bg-pattern-diagonal w-10 h-10 border rounded"></div>
<div class="bg-pattern-diagonal-2 w-10 h-10 border rounded"></div>
<div class="bg-pattern-dots w-10 h-10 border rounded"></div>
<div class="bg-pattern-lines w-10 h-10 border rounded"></div>
<div class="bg-pattern-rectangles w-10 h-10 border rounded"></div>
<div class="bg-pattern-lines-vertical w-10 h-10 border rounded"></div>
<div class="bg-pattern-grid w-10 h-10 border rounded"></div>
<div class="bg-pattern-grid-diagonal w-10 h-10 border rounded"></div>
<div class="bg-pattern-blueprint w-10 h-10 border rounded"></div>
<div class="bg-pattern-cross-dots w-10 h-10 border rounded"></div>
<div class="bg-pattern-diagonal-stripes w-10 h-10 border rounded"></div>
<div class="bg-pattern-diagonal-stripes-2 w-10 h-10 border rounded"></div>
<div class="bg-pattern-zigzag w-10 h-10 border rounded"></div>
<div class="bg-pattern-vertical-stripes w-10 h-10 border rounded"></div>
<div class="bg-pattern-horizontal-stripes w-10 h-10 border rounded"></div>
<div class="col-12"> </div>
<Card> </CardBody>
<CardBody> Lorem ipsum dolor sit amet consectetur adipisicing elit. Consequuntur earum perferendis expedita suscipit quasi maiores quas sint harum dolor! Dolorem cumque autem error ea fuga. Ea excepturi temporibus officiis perspiciatis. </CardBody> </Card>
</Card> </div>
</div> <div class="col-6">
<Card>
<CardBody>
<ButtonList>
<a href="" class="btn" style="border-color: var(--tblr-border-color);">Button</a>
<a href="" class="btn" style="border-color: var(--tblr-border-color-translucent);">Button</a>
<a href="" class="btn" style="border-color: var(--tblr-border-dark-color);">Button</a>
<a href="" class="btn" style="border-color: var(--tblr-border-dark-color-translucent);">Button</a>
<a href="" class="btn btn-primary">Button</a>
</ButtonList>
<ButtonList class="mt-4">
<a href="" class="btn">Button</a>
<a href="" class="btn">Button</a>
<a href="" class="btn">Button</a>
<a href="" class="btn">Button</a>
<a href="" class="btn btn-primary">Button</a>
</ButtonList>
</CardBody>
</Card>
</div>
<div class="col-12"> <div class="col-3">
<Card> <Card>
<CardBody> <CardBody>
<div class="space-y"> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore
<ProgressSteps labels={['Step A', 'Step B', 'Step C']} active={2} /> magna aliqua.
<ProgressSteps count={9} active={4} /> </CardBody>
<ProgressSteps count={9} active={9} color="success" /> </Card>
</div> </div>
</CardBody>
</Card>
</div>
<div class="col-12"> <div class="col-6">
<Card> <Card>
<CardHeader title="Datepicker Examples" /> <CardBody>
<CardBody> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore
<div class="row g-4"> magna aliqua.
<div class="col-md-6"> </CardBody>
<h4 class="mb-3">Basic Datepicker</h4> </Card>
<FormGroup label="Default datepicker"> </div>
<Datepicker id="playground-default" value="2024-01-15" />
</FormGroup>
<FormGroup label="With icon">
<Datepicker id="playground-icon" layout="icon" value="2024-01-15" />
</FormGroup>
<FormGroup label="Icon prepend">
<Datepicker id="playground-icon-prepend" layout="icon-prepend" value="2024-01-15" />
</FormGroup>
</div>
<div class="col-md-6">
<h4 class="mb-3">Datepicker Options</h4>
<FormGroup label="With date range (min/max)">
<Datepicker id="playground-range" value="2024-06-15" />
</FormGroup>
<FormGroup label="Multiple selection">
<Datepicker id="playground-multiple" value="2024-01-15" />
</FormGroup>
<FormGroup label="Date range selection">
<Datepicker id="playground-range-selection" value="2024-01-15" />
</FormGroup>
</div>
<div class="col-md-6">
<h4 class="mb-3">Placement Options</h4>
<FormGroup label="Left placement">
<Datepicker id="playground-placement-left" value="2024-01-15" />
</FormGroup>
<FormGroup label="Center placement">
<Datepicker id="playground-placement-center" value="2024-01-15" />
</FormGroup>
<FormGroup label="Right placement">
<Datepicker id="playground-placement-right" value="2024-01-15" />
</FormGroup>
</div>
<div class="col-md-6">
<h4 class="mb-3">Advanced Options</h4>
<FormGroup label="Multiple months">
<Datepicker id="playground-multiple-months" value="2024-01-15" />
</FormGroup>
<FormGroup label="Custom first weekday (Monday)">
<Datepicker id="playground-weekday" value="2024-01-15" />
</FormGroup>
<FormGroup label="With pre-selected dates">
<Datepicker id="playground-preselected" />
</FormGroup>
</div>
<div class="col-12">
<h4 class="mb-3">Inline Datepicker</h4>
<FormGroup label="Inline calendar">
<Datepicker id="playground-inline" inline value="2024-01-15" />
</FormGroup>
<FormGroup label="Inline with multiple selection">
<Datepicker id="playground-inline-multiple" inline value="2024-01-15" />
</FormGroup>
</div>
</div>
</CardBody>
</Card>
</div>
<div class="col-8">
<Card>
<CardBody>
<p class="card-title mb-4">Cohort Statistics</p>
<dl class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
<div class="col">
<Subheader as="dt">Total Users</Subheader>
<dd class="mt-1 d-flex align-items-baseline">
<span class="h2 mb-0">25,784</span>
<Trending value={23} class="ms-2" />
</dd>
</div>
<div class="col"> <div class="col-12">
<Subheader as="dt">Average CSAT Score</Subheader> <Card>
<dd class="mt-1 d-flex align-items-baseline"> <CardBody>
<span class="h2 mb-0">86.9</span> Lorem ipsum dolor sit amet consectetur adipisicing elit. Consequuntur earum perferendis expedita suscipit quasi maiores quas sint harum dolor! Dolorem cumque autem error ea fuga. Ea excepturi temporibus officiis perspiciatis.
<Trending value={-4} class="ms-2" /> </CardBody>
</dd> </Card>
</div> </div>
<div class="col"> <div class="col-12">
<Subheader as="dt">Average Response Time</Subheader> <Card>
<dd class="mt-1 d-flex align-items-baseline"> <CardBody>
<span class="h2 mb-0">7.2m</span> <div class="space-y">
<Trending value={8} class="ms-2" /> <ProgressSteps labels={['Step A', 'Step B', 'Step C']} active={2} />
</dd> <ProgressSteps count={9} active={4} />
</div> <ProgressSteps count={9} active={9} color="success" />
</div>
</CardBody>
</Card>
</div>
<div class="col"> <div class="col-12">
<Subheader as="dt">Total Tickets</Subheader> <Card>
<dd class="mt-1 d-flex align-items-baseline"> <CardHeader title="Datepicker Examples" />
<span class="h2 mb-0">51,960</span> <CardBody>
<Trending value={-7} class="ms-2" /> <div class="row g-4">
</dd> <div class="col-md-6">
</div> <h4 class="mb-3">Basic Datepicker</h4>
<FormGroup label="Default datepicker">
<Datepicker id="playground-default" value="2024-01-15" />
</FormGroup>
<FormGroup label="With icon">
<Datepicker id="playground-icon" layout="icon" value="2024-01-15" />
</FormGroup>
<FormGroup label="Icon prepend">
<Datepicker id="playground-icon-prepend" layout="icon-prepend" value="2024-01-15" />
</FormGroup>
</div>
<div class="col-md-6">
<h4 class="mb-3">Datepicker Options</h4>
<FormGroup label="With date range (min/max)">
<Datepicker id="playground-range" value="2024-06-15" />
</FormGroup>
<FormGroup label="Multiple selection">
<Datepicker id="playground-multiple" value="2024-01-15" />
</FormGroup>
<FormGroup label="Date range selection">
<Datepicker id="playground-range-selection" value="2024-01-15" />
</FormGroup>
</div>
<div class="col-md-6">
<h4 class="mb-3">Placement Options</h4>
<FormGroup label="Left placement">
<Datepicker id="playground-placement-left" value="2024-01-15" />
</FormGroup>
<FormGroup label="Center placement">
<Datepicker id="playground-placement-center" value="2024-01-15" />
</FormGroup>
<FormGroup label="Right placement">
<Datepicker id="playground-placement-right" value="2024-01-15" />
</FormGroup>
</div>
<div class="col-md-6">
<h4 class="mb-3">Advanced Options</h4>
<FormGroup label="Multiple months">
<Datepicker id="playground-multiple-months" value="2024-01-15" />
</FormGroup>
<FormGroup label="Custom first weekday (Monday)">
<Datepicker id="playground-weekday" value="2024-01-15" />
</FormGroup>
<FormGroup label="With pre-selected dates">
<Datepicker id="playground-preselected" />
</FormGroup>
</div>
<div class="col-12">
<h4 class="mb-3">Inline Datepicker</h4>
<FormGroup label="Inline calendar">
<Datepicker id="playground-inline" inline value="2024-01-15" />
</FormGroup>
<FormGroup label="Inline with multiple selection">
<Datepicker id="playground-inline-multiple" inline value="2024-01-15" />
</FormGroup>
</div>
</div>
</CardBody>
</Card>
</div>
<div class="col-8">
<Card>
<CardBody>
<p class="card-title mb-4">Cohort Statistics</p>
<dl class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
<div class="col"> <div class="col">
<Subheader as="dt">Resolution Rate</Subheader> <Subheader as="dt">Total Users</Subheader>
<dd class="mt-1 d-flex align-items-baseline"> <dd class="mt-1 d-flex align-items-baseline">
<span class="h2 mb-0">90.0%</span> <span class="h2 mb-0">25,784</span>
<Trending value={0} class="ms-2" /> <Trending value={23} class="ms-2" />
</dd> </dd>
</div> </div>
<div class="col"> <div class="col">
<Subheader as="dt">Total Cohorts</Subheader> <Subheader as="dt">Average CSAT Score</Subheader>
<dd class="mt-1 d-flex align-items-baseline"> <dd class="mt-1 d-flex align-items-baseline">
<span class="h2 mb-0">10</span> <span class="h2 mb-0">86.9</span>
<Trending value={15} class="ms-2" /> <Trending value={-4} class="ms-2" />
</dd> </dd>
</div> </div>
<div class="col"> <div class="col">
<Subheader as="dt">Avg. Handling Time</Subheader> <Subheader as="dt">Average Response Time</Subheader>
<dd class="mt-1 d-flex align-items-baseline"> <dd class="mt-1 d-flex align-items-baseline">
<span class="h2 mb-0">14.1m</span> <span class="h2 mb-0">7.2m</span>
<Trending value={-12} class="ms-2" /> <Trending value={8} class="ms-2" />
</dd> </dd>
</div> </div>
<div class="col"> <div class="col">
<Subheader as="dt">First Contact Resolution</Subheader> <Subheader as="dt">Total Tickets</Subheader>
<dd class="mt-1 d-flex align-items-baseline"> <dd class="mt-1 d-flex align-items-baseline">
<span class="h2 mb-0">80.0%</span> <span class="h2 mb-0">51,960</span>
<Trending value={6} class="ms-2" /> <Trending value={-7} class="ms-2" />
</dd> </dd>
</div> </div>
<div class="col"> <div class="col">
<Subheader as="dt">Retention Rate</Subheader> <Subheader as="dt">Resolution Rate</Subheader>
<dd class="mt-1 d-flex align-items-baseline"> <dd class="mt-1 d-flex align-items-baseline">
<span class="h2 mb-0">28.0%</span> <span class="h2 mb-0">90.0%</span>
<Trending value={-3} class="ms-2" /> <Trending value={0} class="ms-2" />
</dd> </dd>
</div> </div>
</dl>
</CardBody> <div class="col">
</Card> <Subheader as="dt">Total Cohorts</Subheader>
</div> <dd class="mt-1 d-flex align-items-baseline">
</div> <span class="h2 mb-0">10</span>
<Trending value={15} class="ms-2" />
</dd>
</div>
<div class="col">
<Subheader as="dt">Avg. Handling Time</Subheader>
<dd class="mt-1 d-flex align-items-baseline">
<span class="h2 mb-0">14.1m</span>
<Trending value={-12} class="ms-2" />
</dd>
</div>
<div class="col">
<Subheader as="dt">First Contact Resolution</Subheader>
<dd class="mt-1 d-flex align-items-baseline">
<span class="h2 mb-0">80.0%</span>
<Trending value={6} class="ms-2" />
</dd>
</div>
<div class="col">
<Subheader as="dt">Retention Rate</Subheader>
<dd class="mt-1 d-flex align-items-baseline">
<span class="h2 mb-0">28.0%</span>
<Trending value={-3} class="ms-2" />
</dd>
</div>
</dl>
</CardBody>
</Card>
</div>
</div>
</DefaultLayout> </DefaultLayout>
+235 -218
View File
@@ -1,233 +1,250 @@
--- ---
import DefaultLayout from '@shared/layouts/DefaultLayout.astro' import DefaultLayout from '@shared/layouts/DefaultLayout.astro';
import ButtonList from '@ui/ButtonList.astro' import CaptureScript from '@shared/components/CaptureScript.astro';
import Card from '@ui/Card.astro' import ButtonList from '@ui/ButtonList.astro';
import CardBody from '@ui/CardBody.astro' import Card from '@ui/Card.astro';
import CardTitle from '@ui/CardTitle.astro' import CardBody from '@ui/CardBody.astro';
import Progress from '@ui/Progress.astro' import CardTitle from '@ui/CardTitle.astro';
import ProgressSteps from '@ui/ProgressSteps.astro' import Progress from '@ui/Progress.astro';
import ProgressBg from '@ui/ProgressBg.astro' import ProgressSteps from '@ui/ProgressSteps.astro';
import ProgressDescription from '@ui/ProgressDescription.astro' import ProgressBg from '@ui/ProgressBg.astro';
import ProgressDescription from '@ui/ProgressDescription.astro';
--- ---
<DefaultLayout title="Progress" pageHeader="Progress" pageMenu="base.progress"> <DefaultLayout title="Progress" pageHeader="Progress" pageMenu="base.progress">
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3"> <div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3">
<div class="col"> <div class="col">
<Card> <Card>
<CardBody> <CardBody>
<CardTitle>Default</CardTitle> <CardTitle>Default</CardTitle>
<div class="space-y"> <div class="space-y">
<Progress value="0" /> <Progress value="0" />
<Progress value="20" /> <Progress value="20" />
<Progress value="40" /> <Progress value="40" />
<Progress value="100" /> <Progress value="100" />
</div> </div>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col"> <div class="col">
<Card> <Card>
<CardBody> <CardBody>
<CardTitle>With value</CardTitle> <CardTitle>With value</CardTitle>
<div class="space-y"> <div class="space-y">
<Progress value="10" showValue size="lg" /> <Progress value="10" showValue size="lg" />
<Progress value="20" showValue size="lg" /> <Progress value="20" showValue size="lg" />
<Progress value="90" showValue size="lg" /> <Progress value="90" showValue size="lg" />
</div> </div>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col"> <div class="col">
<Card> <Card>
<CardBody> <CardBody>
<CardTitle>Colors</CardTitle> <CardTitle>Colors</CardTitle>
<div class="space-y"> <div class="space-y">
<Progress color="blue" value="20" /> <Progress color="blue" value="20" />
<Progress color="green" value="40" /> <Progress color="green" value="40" />
<Progress color="yellow" value="60" /> <Progress color="yellow" value="60" />
<Progress color="red" value="80" /> <Progress color="red" value="80" />
</div> </div>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col"> <div class="col">
<Card> <Card>
<CardBody> <CardBody>
<CardTitle>Sizes</CardTitle> <CardTitle>Sizes</CardTitle>
<div class="space-y"> <div class="space-y">
<Progress value="20" size="sm" /> <Progress value="20" size="sm" />
<Progress value="40" /> <Progress value="40" />
<Progress value="60" size="lg" /> <Progress value="60" size="lg" />
<Progress value="80" size="xl" /> <Progress value="80" size="xl" />
</div> </div>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col"> <div class="col">
<Card> <Card>
<CardBody> <CardBody>
<CardTitle>Indeterminate</CardTitle> <CardTitle>Indeterminate</CardTitle>
<div class="space-y"> <div class="space-y">
<Progress indeterminate /> <Progress indeterminate />
</div> </div>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col"> <div class="col">
<Card> <Card>
<CardBody> <CardBody>
<CardTitle>Multiple values</CardTitle> <CardTitle>Multiple values</CardTitle>
<div class="space-y"> <div class="space-y">
<Progress values={[20, 30, 10]} /> <Progress values={[20, 30, 10]} />
<Progress values={[10, 20, 30, 40]} class="progress-separated" /> <Progress values={[10, 20, 30, 40]} class="progress-separated" />
</div> </div>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col"> <div class="col">
<Card> <Card>
<CardBody> <CardBody>
<CardTitle>Striped</CardTitle> <CardTitle>Striped</CardTitle>
<div class="space-y"> <div class="space-y">
<Progress color="blue" value="20" striped /> <Progress color="blue" value="20" striped />
<Progress color="green" value="40" striped /> <Progress color="green" value="40" striped />
<Progress color="yellow" value="60" striped /> <Progress color="yellow" value="60" striped />
<Progress color="red" value="80" striped /> <Progress color="red" value="80" striped />
</div> </div>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col"> <div class="col">
<Card> <Card>
<CardBody> <CardBody>
<CardTitle>Animated</CardTitle> <CardTitle>Animated</CardTitle>
<div class="space-y"> <div class="space-y">
<Progress value="20" striped animated /> <Progress value="20" striped animated />
<Progress value="40" color="green" striped animated /> <Progress value="40" color="green" striped animated />
<Progress value="60" color="yellow" striped animated /> <Progress value="60" color="yellow" striped animated />
<Progress value="80" color="red" striped animated /> <Progress value="80" color="red" striped animated />
</div> </div>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col"> <div class="col">
<Card> <Card>
<CardBody> <CardBody>
<CardTitle> Animated with JavaScript </CardTitle> <CardTitle>
<div class="row align-items-center g-0"> Animated with JavaScript
<div class="col"> </CardTitle>
<Progress value="0" id="progress-animated" /> <div class="row align-items-center g-0">
</div> <div class="col">
<div class="col-2 text-end" id="progress-animated-value">0%</div> <Progress value="0" id="progress-animated" />
</div> </div>
<ButtonList class="mt-3"> <div class="col-2 text-end" id="progress-animated-value">
<button class="btn btn-sm" id="progress-animated-0">0%</button> 0%
<button class="btn btn-sm" id="progress-animated-10">10%</button> </div>
<button class="btn btn-sm" id="progress-animated-50">50%</button> </div>
<button class="btn btn-sm" id="progress-animated-100">100%</button> <ButtonList class="mt-3">
<button class="btn btn-sm ms-3" id="progress-animated-minus-10">-10%</button> <button class="btn btn-sm" id="progress-animated-0">0%</button>
<button class="btn btn-sm" id="progress-animated-add-10">+10%</button> <button class="btn btn-sm" id="progress-animated-10">10%</button>
</ButtonList> <button class="btn btn-sm" id="progress-animated-50">50%</button>
<!-- BEGIN SCRIPT OF ANIMATION --> <button class="btn btn-sm" id="progress-animated-100">100%</button>
<script> <button class="btn btn-sm ms-3" id="progress-animated-minus-10">-10%</button>
/* <button class="btn btn-sm" id="progress-animated-add-10">+10%</button>
</ButtonList>
<CaptureScript>
<!-- BEGIN SCRIPT OF ANIMATION -->
<script is:inline>
/*
This script is for animation of the last progress bar. This script is for animation of the last progress bar.
It increases the progress bar value by a random amount every 2 seconds until it reaches 100%. It increases the progress bar value by a random amount every 2 seconds until it reaches 100%.
When it reaches 100%, it changes the color to green and stops the animation. When it reaches 100%, it changes the color to green and stops the animation.
This is just a demo script to show how to animate the progress bar. You can modify it as needed. This is just a demo script to show how to animate the progress bar. You can modify it as needed.
*/ */
const progress = document.getElementById('progress-animated')! const progress = document.getElementById('progress-animated')!;
let width = 0 let width = 0;
const setWidth = (w: number) => { const setWidth = (w: number) => {
width = Math.min(Math.max(w, 0), 100) width = Math.min(Math.max(w, 0), 100);
const bar = progress.querySelector<HTMLElement>('.progress-bar')! const bar = progress.querySelector<HTMLElement>('.progress-bar')!;
bar.style.width = `${width}%` bar.style.width = `${width}%`;
bar.setAttribute('aria-valuenow', `${width}`) bar.setAttribute('aria-valuenow', `${width}`);
document.getElementById('progress-animated-value')!.innerText = `${width}%` document.getElementById('progress-animated-value')!.innerText = `${width}%`;
bar.classList.toggle('bg-green', width >= 100) bar.classList.toggle('bg-green', width >= 100);
} };
const interval = setInterval(() => { const interval = setInterval(() => {
setWidth(width + Math.ceil(Math.random() * 10)) setWidth(width + Math.ceil(Math.random() * 10));
if (width >= 100) clearInterval(interval) if (width >= 100) clearInterval(interval);
}, 2000) }, 2000);
document.getElementById('progress-animated-0')!.addEventListener('click', () => setWidth(0)) document.getElementById('progress-animated-0')!.addEventListener('click', () => setWidth(0));
document.getElementById('progress-animated-add-10')!.addEventListener('click', () => setWidth(width + 10)) document.getElementById('progress-animated-add-10')!.addEventListener('click', () => setWidth(width + 10));
document.getElementById('progress-animated-minus-10')!.addEventListener('click', () => setWidth(width - 10)) document.getElementById('progress-animated-minus-10')!.addEventListener('click', () => setWidth(width - 10));
document.getElementById('progress-animated-100')!.addEventListener('click', () => setWidth(100)) document.getElementById('progress-animated-100')!.addEventListener('click', () => setWidth(100));
</script> </script>
<!-- END SCRIPT OF ANIMATION --> <!-- END SCRIPT OF ANIMATION -->
</CardBody> </CaptureScript>
</Card> </CardBody>
</div> </Card>
<div class="col"> </div>
<Card> <div class="col">
<CardBody> <Card>
<CardTitle> Steps Progress </CardTitle> <CardBody>
<div class="space-y"> <CardTitle>
<ProgressSteps count={3} /> Steps Progress
<ProgressSteps count={5} active={4} /> </CardTitle>
<ProgressSteps count={10} color="red" /> <div class="space-y">
<ProgressSteps count={8} color="green" active={8} /> <ProgressSteps count={3} />
</div> <ProgressSteps count={5} active={4} />
</CardBody> <ProgressSteps count={10} color="red" />
</Card> <ProgressSteps count={8} color="green" active={8} />
</div> </div>
<div class="col"> </CardBody>
<Card> </Card>
<CardBody> </div>
<CardTitle> Progress Background </CardTitle> <div class="col">
<div class="space-y"> <Card>
<ProgressBg value="85" text="Poland" showValue /> <CardBody>
<ProgressBg value="65" text="Germany" showValue /> <CardTitle>
<ProgressBg value="45" text="United States" showValue /> Progress Background
<ProgressBg value="25" text="France" showValue /> </CardTitle>
</div> <div class="space-y">
</CardBody> <ProgressBg value="85" text="Poland" showValue />
</Card> <ProgressBg value="65" text="Germany" showValue />
</div> <ProgressBg value="45" text="United States" showValue />
<div class="col"> <ProgressBg value="25" text="France" showValue />
<Card> </div>
<CardBody> </CardBody>
<CardTitle> Progress Background Colors </CardTitle> </Card>
<div class="space-y"> </div>
<ProgressBg value="75" text="Success" color="success-lt" showValue /> <div class="col">
<ProgressBg value="60" text="Warning" color="warning-lt" showValue /> <Card>
<ProgressBg value="40" text="Danger" color="danger-lt" showValue /> <CardBody>
<ProgressBg value="90" text="Info" color="info-lt" showValue /> <CardTitle>
</div> Progress Background Colors
</CardBody> </CardTitle>
</Card> <div class="space-y">
</div> <ProgressBg value="75" text="Success" color="success-lt" showValue />
<div class="col"> <ProgressBg value="60" text="Warning" color="warning-lt" showValue />
<Card> <ProgressBg value="40" text="Danger" color="danger-lt" showValue />
<CardBody> <ProgressBg value="90" text="Info" color="info-lt" showValue />
<CardTitle> Progress Description </CardTitle> </div>
<div class="space-y"> </CardBody>
<ProgressDescription label="Project completion" value="85" color="green" /> </Card>
<ProgressDescription label="Storage usage" description="2.4GB of 5GB" value="48" color="blue" /> </div>
<ProgressDescription label="Download progress" value="75" color="yellow" /> <div class="col">
<ProgressDescription label="Skills assessment" description="HTML/CSS" value="92" color="red" /> <Card>
</div> <CardBody>
</CardBody> <CardTitle>
</Card> Progress Description
</div> </CardTitle>
<div class="col"> <div class="space-y">
<Card> <ProgressDescription label="Project completion" value="85" color="green" />
<CardBody> <ProgressDescription label="Storage usage" description="2.4GB of 5GB" value="48" color="blue" />
<CardTitle> Progress Description Sizes </CardTitle> <ProgressDescription label="Download progress" value="75" color="yellow" />
<div class="space-y"> <ProgressDescription label="Skills assessment" description="HTML/CSS" value="92" color="red" />
<ProgressDescription label="Small progress" value="60" size="sm" color="blue" /> </div>
<ProgressDescription label="Default progress" value="70" color="green" /> </CardBody>
<ProgressDescription label="Large progress" value="80" size="lg" color="orange" /> </Card>
<ProgressDescription label="Extra large" value="90" size="xl" color="purple" /> </div>
</div> <div class="col">
</CardBody> <Card>
</Card> <CardBody>
</div> <CardTitle>
</div> Progress Description Sizes
</CardTitle>
<div class="space-y">
<ProgressDescription label="Small progress" value="60" size="sm" color="blue" />
<ProgressDescription label="Default progress" value="70" color="green" />
<ProgressDescription label="Large progress" value="80" size="lg" color="orange" />
<ProgressDescription label="Extra large" value="90" size="xl" color="purple" />
</div>
</CardBody>
</Card>
</div>
</div>
</DefaultLayout> </DefaultLayout>
+64 -66
View File
@@ -1,78 +1,76 @@
--- ---
// The Liquid `{% for color in site.colors %}` swatches map to site.themeColors // Color swatches use site.themeColors (blue…cyan).
// (blue…cyan), matching the reference bg-gradient-from-{color} output. // Large prose/nav-segmented block from source template omitted (was commented out).
// The large `{% comment %}…{% endcomment %}` prose/nav-segmented block in the import BaseLayout from '@shared/layouts/BaseLayout.astro';
// source is a Liquid comment (not rendered) — intentionally omitted. import NavbarLogo from '@shared/components/navbar/NavbarLogo.astro';
import BaseLayout from '@shared/layouts/BaseLayout.astro' import { site } from '@shared/lib/site';
import NavbarLogo from '@shared/components/navbar/NavbarLogo.astro'
import { site } from '@shared/lib/site'
--- ---
<BaseLayout pageLibs={['signature_pad']}> <BaseLayout pageLibs={['signature_pad']}>
<style is:inline> <style is:inline>
.screenshot { .screenshot {
width: 1024px; width: 1024px;
height: 768px; height: 768px;
background: var(--tblr-bg-surface-tertiary) linear-gradient(to top left, rgba(0, 0, 0, 0.04), rgba(0, 0, 0, 0)); background: var(--tblr-bg-surface-tertiary) linear-gradient(to top left, rgba(0, 0, 0, 0.04), rgba(0, 0, 0, 0));
position: relative; position: relative;
} }
.screenshot-card { .screenshot-card {
padding: 64px; padding: 64px;
border-radius: 16px; border-radius: 16px;
box-shadow: box-shadow:
0 1px 1px 0 rgba(0, 0, 0, 0.02), 0 1px 1px 0 rgba(0, 0, 0, 0.02),
0 8px 24px -4px rgba(0, 0, 0, 0.04), 0 8px 24px -4px rgba(0, 0, 0, 0.04),
0 24px 40px -8px rgba(0, 0, 0, 0.1); 0 24px 40px -8px rgba(0, 0, 0, 0.1);
} }
.screenshot-logo { .screenshot-logo {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 0 5rem; padding: 0 5rem;
flex: 1; flex: 1;
} }
.cursor { .cursor {
position: absolute; position: absolute;
width: 26px; width: 26px;
height: 37px; height: 37px;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 26 37'%3E%3Cg filter='url(%23a)'%3E%3Cpath fill='%23fff' fill-rule='evenodd' d='M8.56 13.902c-.284-.358-.63-1.092-1.243-1.983-.348-.505-1.211-1.454-1.468-1.935-.223-.426-.2-.617-.146-.97.094-.628.738-1.117 1.425-1.051.519.049.959.392 1.355.716.239.195.533.574.71.788.163.196.203.277.377.509.23.306.302.458.214.12-.071-.495-.187-1.342-.355-2.091-.128-.568-.16-.657-.281-1.093-.13-.464-.195-.79-.316-1.281-.084-.348-.235-1.06-.276-1.46-.057-.546-.087-1.438.264-1.848.275-.321.906-.418 1.296-.22.513.259.803 1.003.937 1.3.238.534.386 1.15.515 1.96.165 1.032.466 2.463.476 2.764.024-.37-.068-1.146-.004-1.5.059-.321.329-.694.667-.795.285-.085.62-.116.915-.055.313.064.643.288.767.499.362.624.369 1.899.384 1.83.085-.375.07-1.228.284-1.583.14-.234.496-.445.686-.48.295-.051.655-.067.964-.007.25.049.587.345.677.487.218.344.343 1.317.38 1.658.015.14.073-.392.293-.736.405-.64 1.843-.763 1.898.639.025.654.02.624.02 1.064 0 .517-.013.828-.04 1.202-.031.4-.117 1.303-.242 1.742-.087.3-.372.977-.652 1.383 0 0-1.075 1.25-1.192 1.814-.117.562-.079.566-.102.964-.023.398.122.922.122.922s-.802.104-1.235.035c-.39-.063-.875-.84-1-1.079-.171-.328-.539-.264-.681-.023-.226.383-.71 1.07-1.051 1.113-.668.084-2.054.032-3.14.02 0 0 .185-1.01-.226-1.357-.306-.26-.83-.784-1.144-1.06l-.832-.921Z' clip-rule='evenodd'/%3E%3Cpath stroke='%23000' stroke-linecap='round' stroke-linejoin='round' d='M16.794 14.257v-3.459m-2.015 3.47-.016-3.472m-1.98.031.02 3.426m-4.243-.35c-.284-.36-.63-1.094-1.243-1.985-.348-.503-1.211-1.452-1.468-1.934-.223-.426-.2-.617-.146-.97.094-.628.738-1.117 1.425-1.051.519.049.959.392 1.355.716.239.195.533.574.71.788.163.196.203.277.377.509.23.306.302.458.214.12-.071-.495-.187-1.342-.355-2.091-.128-.568-.16-.657-.281-1.093-.13-.464-.195-.79-.316-1.281-.084-.348-.235-1.06-.276-1.46-.057-.546-.087-1.438.264-1.848.275-.321.906-.418 1.296-.22.513.259.803 1.003.937 1.3.238.534.386 1.15.515 1.96.165 1.032.466 2.463.476 2.764.024-.37-.068-1.146-.004-1.5.059-.321.329-.694.667-.795.285-.085.62-.116.915-.055.313.064.643.288.767.499.362.624.369 1.899.384 1.83.085-.375.07-1.228.284-1.583.14-.234.496-.445.686-.48.295-.051.655-.067.964-.007.25.049.587.345.677.487.218.344.343 1.317.38 1.658.015.14.073-.392.293-.736.405-.64 1.843-.763 1.898.639.025.654.02.624.02 1.064 0 .517-.013.828-.04 1.202-.031.4-.117 1.303-.242 1.742-.087.3-.372.977-.652 1.383 0 0-1.075 1.25-1.192 1.814-.117.562-.079.566-.102.964-.023.398.122.922.122.922s-.802.104-1.235.035c-.39-.063-.875-.84-1-1.079-.171-.328-.539-.264-.681-.023-.226.383-.71 1.07-1.051 1.113-.668.084-2.054.032-3.14.02 0 0 .185-1.01-.226-1.357-.306-.26-.83-.784-1.144-1.06l-.832-.921Z'/%3E%3C/g%3E%3Cdefs%3E%3Cfilter id='a' width='30' height='40' x='-2' y='-1' color-interpolation-filters='sRGB' filterUnits='userSpaceOnUse'%3E%3CfeFlood flood-opacity='0' result='BackgroundImageFix'/%3E%3CfeColorMatrix in='SourceAlpha' result='hardAlpha' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0'/%3E%3CfeOffset dy='1'/%3E%3CfeGaussianBlur stdDeviation='1'/%3E%3CfeColorMatrix values='0 0 0 0 0.156863 0 0 0 0 0.156863 0 0 0 0 0.2 0 0 0 0.05 0'/%3E%3CfeBlend in2='BackgroundImageFix' result='effect1_dropShadow_55_1571'/%3E%3CfeColorMatrix in='SourceAlpha' result='hardAlpha' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0'/%3E%3CfeOffset dy='3'/%3E%3CfeGaussianBlur stdDeviation='1.5'/%3E%3CfeColorMatrix values='0 0 0 0 0.156863 0 0 0 0 0.156863 0 0 0 0 0.2 0 0 0 0.04 0'/%3E%3CfeBlend in2='effect1_dropShadow_55_1571' result='effect2_dropShadow_55_1571'/%3E%3CfeColorMatrix in='SourceAlpha' result='hardAlpha' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0'/%3E%3CfeOffset dy='8'/%3E%3CfeGaussianBlur stdDeviation='2.5'/%3E%3CfeColorMatrix values='0 0 0 0 0.156863 0 0 0 0 0.156863 0 0 0 0 0.2 0 0 0 0.03 0'/%3E%3CfeBlend in2='effect2_dropShadow_55_1571' result='effect3_dropShadow_55_1571'/%3E%3CfeColorMatrix in='SourceAlpha' result='hardAlpha' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0'/%3E%3CfeOffset dy='14'/%3E%3CfeGaussianBlur stdDeviation='2.5'/%3E%3CfeColorMatrix values='0 0 0 0 0.156863 0 0 0 0 0.156863 0 0 0 0 0.2 0 0 0 0.01 0'/%3E%3CfeBlend in2='effect3_dropShadow_55_1571' result='effect4_dropShadow_55_1571'/%3E%3CfeBlend in='SourceGraphic' in2='effect4_dropShadow_55_1571' result='shape'/%3E%3C/filter%3E%3C/defs%3E%3C/svg%3E"); background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 26 37'%3E%3Cg filter='url(%23a)'%3E%3Cpath fill='%23fff' fill-rule='evenodd' d='M8.56 13.902c-.284-.358-.63-1.092-1.243-1.983-.348-.505-1.211-1.454-1.468-1.935-.223-.426-.2-.617-.146-.97.094-.628.738-1.117 1.425-1.051.519.049.959.392 1.355.716.239.195.533.574.71.788.163.196.203.277.377.509.23.306.302.458.214.12-.071-.495-.187-1.342-.355-2.091-.128-.568-.16-.657-.281-1.093-.13-.464-.195-.79-.316-1.281-.084-.348-.235-1.06-.276-1.46-.057-.546-.087-1.438.264-1.848.275-.321.906-.418 1.296-.22.513.259.803 1.003.937 1.3.238.534.386 1.15.515 1.96.165 1.032.466 2.463.476 2.764.024-.37-.068-1.146-.004-1.5.059-.321.329-.694.667-.795.285-.085.62-.116.915-.055.313.064.643.288.767.499.362.624.369 1.899.384 1.83.085-.375.07-1.228.284-1.583.14-.234.496-.445.686-.48.295-.051.655-.067.964-.007.25.049.587.345.677.487.218.344.343 1.317.38 1.658.015.14.073-.392.293-.736.405-.64 1.843-.763 1.898.639.025.654.02.624.02 1.064 0 .517-.013.828-.04 1.202-.031.4-.117 1.303-.242 1.742-.087.3-.372.977-.652 1.383 0 0-1.075 1.25-1.192 1.814-.117.562-.079.566-.102.964-.023.398.122.922.122.922s-.802.104-1.235.035c-.39-.063-.875-.84-1-1.079-.171-.328-.539-.264-.681-.023-.226.383-.71 1.07-1.051 1.113-.668.084-2.054.032-3.14.02 0 0 .185-1.01-.226-1.357-.306-.26-.83-.784-1.144-1.06l-.832-.921Z' clip-rule='evenodd'/%3E%3Cpath stroke='%23000' stroke-linecap='round' stroke-linejoin='round' d='M16.794 14.257v-3.459m-2.015 3.47-.016-3.472m-1.98.031.02 3.426m-4.243-.35c-.284-.36-.63-1.094-1.243-1.985-.348-.503-1.211-1.452-1.468-1.934-.223-.426-.2-.617-.146-.97.094-.628.738-1.117 1.425-1.051.519.049.959.392 1.355.716.239.195.533.574.71.788.163.196.203.277.377.509.23.306.302.458.214.12-.071-.495-.187-1.342-.355-2.091-.128-.568-.16-.657-.281-1.093-.13-.464-.195-.79-.316-1.281-.084-.348-.235-1.06-.276-1.46-.057-.546-.087-1.438.264-1.848.275-.321.906-.418 1.296-.22.513.259.803 1.003.937 1.3.238.534.386 1.15.515 1.96.165 1.032.466 2.463.476 2.764.024-.37-.068-1.146-.004-1.5.059-.321.329-.694.667-.795.285-.085.62-.116.915-.055.313.064.643.288.767.499.362.624.369 1.899.384 1.83.085-.375.07-1.228.284-1.583.14-.234.496-.445.686-.48.295-.051.655-.067.964-.007.25.049.587.345.677.487.218.344.343 1.317.38 1.658.015.14.073-.392.293-.736.405-.64 1.843-.763 1.898.639.025.654.02.624.02 1.064 0 .517-.013.828-.04 1.202-.031.4-.117 1.303-.242 1.742-.087.3-.372.977-.652 1.383 0 0-1.075 1.25-1.192 1.814-.117.562-.079.566-.102.964-.023.398.122.922.122.922s-.802.104-1.235.035c-.39-.063-.875-.84-1-1.079-.171-.328-.539-.264-.681-.023-.226.383-.71 1.07-1.051 1.113-.668.084-2.054.032-3.14.02 0 0 .185-1.01-.226-1.357-.306-.26-.83-.784-1.144-1.06l-.832-.921Z'/%3E%3C/g%3E%3Cdefs%3E%3Cfilter id='a' width='30' height='40' x='-2' y='-1' color-interpolation-filters='sRGB' filterUnits='userSpaceOnUse'%3E%3CfeFlood flood-opacity='0' result='BackgroundImageFix'/%3E%3CfeColorMatrix in='SourceAlpha' result='hardAlpha' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0'/%3E%3CfeOffset dy='1'/%3E%3CfeGaussianBlur stdDeviation='1'/%3E%3CfeColorMatrix values='0 0 0 0 0.156863 0 0 0 0 0.156863 0 0 0 0 0.2 0 0 0 0.05 0'/%3E%3CfeBlend in2='BackgroundImageFix' result='effect1_dropShadow_55_1571'/%3E%3CfeColorMatrix in='SourceAlpha' result='hardAlpha' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0'/%3E%3CfeOffset dy='3'/%3E%3CfeGaussianBlur stdDeviation='1.5'/%3E%3CfeColorMatrix values='0 0 0 0 0.156863 0 0 0 0 0.156863 0 0 0 0 0.2 0 0 0 0.04 0'/%3E%3CfeBlend in2='effect1_dropShadow_55_1571' result='effect2_dropShadow_55_1571'/%3E%3CfeColorMatrix in='SourceAlpha' result='hardAlpha' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0'/%3E%3CfeOffset dy='8'/%3E%3CfeGaussianBlur stdDeviation='2.5'/%3E%3CfeColorMatrix values='0 0 0 0 0.156863 0 0 0 0 0.156863 0 0 0 0 0.2 0 0 0 0.03 0'/%3E%3CfeBlend in2='effect2_dropShadow_55_1571' result='effect3_dropShadow_55_1571'/%3E%3CfeColorMatrix in='SourceAlpha' result='hardAlpha' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0'/%3E%3CfeOffset dy='14'/%3E%3CfeGaussianBlur stdDeviation='2.5'/%3E%3CfeColorMatrix values='0 0 0 0 0.156863 0 0 0 0 0.156863 0 0 0 0 0.2 0 0 0 0.01 0'/%3E%3CfeBlend in2='effect3_dropShadow_55_1571' result='effect4_dropShadow_55_1571'/%3E%3CfeBlend in='SourceGraphic' in2='effect4_dropShadow_55_1571' result='shape'/%3E%3C/filter%3E%3C/defs%3E%3C/svg%3E");
top: calc(100% - 10px); top: calc(100% - 10px);
left: calc(50% - 10px); left: calc(50% - 10px);
} }
.avatar-2 { .avatar-2 {
background-image: url(/static/avatars/032f.jpg); background-image: url(/static/avatars/032f.jpg);
} }
</style> </style>
<div class="page page-center align-items-center"> <div class="page page-center align-items-center">
<div class="screenshot d-flex flex-column justify-content-center align-items-center px-12"> <div class="screenshot d-flex flex-column justify-content-center align-items-center px-12">
<div class="screenshot-logo"> <div class="screenshot-logo">
<NavbarLogo class="brand-gray" /> <NavbarLogo class="brand-gray" />
</div> </div>
<div class="screenshot-card card p-6"> <div class="screenshot-card card p-6">
<div class="row g-4"> <div class="row g-4">
{ {
site.themeColors.map((color) => ( site.themeColors.map((color) => (
<div class="col-2"> <div class="col-2">
<div class={`p-5 bg-gradient bg-gradient-from-${color} bg-gradient-to-transparent bg-gradient-to-ts rounded ratio ratio-1x1 border`} /> <div class={`p-5 bg-gradient bg-gradient-from-${color} bg-gradient-to-transparent bg-gradient-to-ts rounded ratio ratio-1x1 border`} />
</div> </div>
)) ))
} }
</div> </div>
</div> </div>
<div class="screenshot-logo"></div> <div class="screenshot-logo"></div>
</div> </div>
<nav class="nav-segmented mt-4"> <nav class="nav-segmented mt-4">
<a href="?theme=light" class="nav-link hide-theme-light">Light</a> <a href="?theme=light" class="nav-link hide-theme-light">Light</a>
<a href="?theme=light" class="nav-link active hide-theme-dark">Light</a> <a href="?theme=light" class="nav-link active hide-theme-dark">Light</a>
<a href="?theme=dark" class="nav-link hide-theme-dark">Dark</a> <a href="?theme=dark" class="nav-link hide-theme-dark">Dark</a>
<a href="?theme=dark" class="nav-link active hide-theme-light">Dark</a> <a href="?theme=dark" class="nav-link active hide-theme-light">Dark</a>
</nav> </nav>
</div> </div>
</BaseLayout> </BaseLayout>
+23 -24
View File
@@ -1,32 +1,31 @@
--- ---
// Photo id=11 → the 12th entry of the unfiltered photos list (Liquid // Photo id=11 → 12th entry of the unfiltered photos list; no horizontal filter.
// filtered-photos[11]); no `horizontal` filter is applied here. import BaseLayout from '@shared/layouts/BaseLayout.astro';
import BaseLayout from '@shared/layouts/BaseLayout.astro' import NavbarLogo from '@shared/components/navbar/NavbarLogo.astro';
import NavbarLogo from '@shared/components/navbar/NavbarLogo.astro' import SignInForm from '@shared/components/cards/SignInForm.astro';
import SignInForm from '@shared/components/cards/SignInForm.astro' import Photo from '@ui/Photo.astro';
import Photo from '@ui/Photo.astro' import photos from '@data/photos.json';
import photos from '@data/photos.json'
--- ---
<BaseLayout title="Sign in with cover" bodyClass="d-flex flex-column bg-white"> <BaseLayout title="Sign in with cover" bodyClass="d-flex flex-column bg-white">
<div class="row g-0 flex-fill"> <div class="row g-0 flex-fill">
<div class="col-12 col-lg-6 col-xl-4 border-top-wide border-primary d-flex flex-column justify-content-center"> <div class="col-12 col-lg-6 col-xl-4 border-top-wide border-primary d-flex flex-column justify-content-center">
<div class="container container-tight my-5 px-lg-5"> <div class="container container-tight my-5 px-lg-5">
<div class="text-center mb-4"> <div class="text-center mb-4">
<NavbarLogo /> <NavbarLogo />
</div> </div>
<h2 class="h3 text-center mb-3">Login to your account</h2> <h2 class="h3 text-center mb-3"> Login to your account </h2>
<SignInForm /> <SignInForm />
<div class="text-center text-secondary mt-3"> <div class="text-center text-secondary mt-3">
Don't have account yet? <a href="./sign-up.html" tabindex="-1">Sign up</a> Don't have account yet? <a href="./sign-up.html" tabindex="-1">Sign up</a>
</div> </div>
</div> </div>
</div> </div>
<div class="col-12 col-lg-6 col-xl-8 d-none d-lg-block"> <div class="col-12 col-lg-6 col-xl-8 d-none d-lg-block">
<Photo photo={photos[11]} class="bg-cover h-100 min-vh-100" background /> <Photo photo={photos[11]} class="bg-cover h-100 min-vh-100" background />
</div> </div>
</div> </div>
</BaseLayout> </BaseLayout>
+1 -2
View File
@@ -4,8 +4,7 @@ import NavbarLogo from '@shared/components/navbar/NavbarLogo.astro'
import SignInCard from '@shared/components/cards/SignInCard.astro' import SignInCard from '@shared/components/cards/SignInCard.astro'
import Illustration from '@ui/Illustration.astro' import Illustration from '@ui/Illustration.astro'
// Liquid passes show-header="1" to cards/sign-in.html, but the include renders // show-header prop ignored — SignInCard renders header unconditionally.
// the header unconditionally — SignInCard.astro mirrors that (no prop needed).
--- ---
<SingleLayout title="Sign in with illustration" containerSize="normal" hideLogo> <SingleLayout title="Sign in with illustration" containerSize="normal" hideLogo>
+16 -17
View File
@@ -1,25 +1,24 @@
--- ---
import SingleLayout from '@shared/layouts/SingleLayout.astro' import SingleLayout from '@shared/layouts/SingleLayout.astro';
// TODO: Liquid uses {{ site.email }} — add `email` to src/lib/site.ts once it is // TODO: add `email` to src/lib/site.ts — kept local here to avoid cross-agent conflicts.
// safe to touch shared files (kept local here to avoid cross-agent conflicts). const siteEmail = 'support@tabler.io';
const siteEmail = 'support@tabler.io'
--- ---
<SingleLayout title="Sign in link"> <SingleLayout title="Sign in link">
<div class="text-center"> <div class="text-center">
<div class="my-5"> <div class="my-5">
<h2 class="h1">Check your inbox</h2> <h2 class="h1">Check your inbox</h2>
<p class="fs-h3 text-secondary"> <p class="fs-h3 text-secondary">
We've sent you a magic link to <strong>{siteEmail}</strong>.<br /> We've sent you a magic link to <strong>{siteEmail}</strong>.<br />
Please click the link to confirm your address. Please click the link to confirm your address.
</p> </p>
</div> </div>
<div class="text-center text-secondary mt-3"> <div class="text-center text-secondary mt-3">
Can't see the email? Please check the spam folder.<br /> Can't see the email? Please check the spam folder.<br />
Wrong email? Please <a href="#">re-enter your address</a>. Wrong email? Please <a href="#">re-enter your address</a>.
</div> </div>
</div> </div>
</SingleLayout> </SingleLayout>
File diff suppressed because one or more lines are too long
+2 -5
View File
@@ -3,9 +3,7 @@ import { site } from '@shared/lib/site';
export const prerender = true; export const prerender = true;
// (build.format 'file'), index pages collapse to their directory URL. // Index pages collapse to directory URLs (build.format 'file').
// Entry order mirrors the Eleventy `pages` collection: top-level pages first,
// then nested ones, each group in reverse-alphabetical order.
const pages = import.meta.glob('./**/*.astro'); const pages = import.meta.glob('./**/*.astro');
const urls = Object.keys(pages) const urls = Object.keys(pages)
@@ -14,7 +12,6 @@ const urls = Object.keys(pages)
const depthA = a.split('/').length; const depthA = a.split('/').length;
const depthB = b.split('/').length; const depthB = b.split('/').length;
if (depthA !== depthB) return depthA - depthB; if (depthA !== depthB) return depthA - depthB;
// byte-wise, descending — matches the Eleventy `pages` collection order
return a < b ? 1 : -1; return a < b ? 1 : -1;
}) })
.map((path) => { .map((path) => {
@@ -34,7 +31,7 @@ const escapeXml = (value: string) =>
export const GET: APIRoute = () => { export const GET: APIRoute = () => {
const environment = process.env.NODE_ENV || 'production'; const environment = process.env.NODE_ENV || 'production';
const baseUrl = environment !== 'development' ? site.previewUrl : ''; const baseUrl = environment !== 'development' ? site.previewUrl : '';
// same shape as Liquid's `'now' | date_to_xmlschema` (UTC, +00:00 suffix) // ISO 8601 UTC timestamp (+00:00 suffix)
const lastModified = new Date().toISOString().replace(/\.\d{3}Z$/, '+00:00'); const lastModified = new Date().toISOString().replace(/\.\d{3}Z$/, '+00:00');
const entries = urls const entries = urls
.map( .map(
+91 -88
View File
@@ -1,101 +1,104 @@
--- ---
// Note: the Liquid front matter `plugins: ['social']` is inert for the output — // plugins: ['social'] front matter is inert — .social styles ship globally in tabler-socials.css.
// the `.social` styles ship in tabler-socials.css which is loaded globally on import DefaultLayout from '@shared/layouts/DefaultLayout.astro';
// every page; no extra page lib is emitted. So no pageLibs is passed. import ButtonList from '@ui/ButtonList.astro';
import DefaultLayout from '@shared/layouts/DefaultLayout.astro' import Trending from '@ui/Trending.astro';
import ButtonList from '@ui/ButtonList.astro' import Card from '@ui/Card.astro';
import Trending from '@ui/Trending.astro' import CardHeader from '@ui/CardHeader.astro';
import Card from '@ui/Card.astro' import CardBody from '@ui/CardBody.astro';
import CardHeader from '@ui/CardHeader.astro' import socialTiles from '@data/social-tiles.json';
import CardBody from '@ui/CardBody.astro' import socials from '@data/socials.json';
import socialTiles from '@data/social-tiles.json'
import socials from '@data/socials.json'
interface SocialTile { interface SocialTile {
icon: string icon: string;
title: string title: string;
description: string description: string;
trending: number trending: number;
} }
interface Social { interface Social {
name: string name: string;
file: string file: string;
} }
const tiles = socialTiles as SocialTile[] const tiles = socialTiles as SocialTile[];
const socialList = socials as Social[] const socialList = socials as Social[];
const fillers = Array.from({ length: 21 }) const fillers = Array.from({ length: 21 });
--- ---
<DefaultLayout title="Social icons" pageHeader="Social icons" pageMenu="base.social"> <DefaultLayout title="Social icons" pageHeader="Social icons" pageMenu="base.social">
<div class="row row-cards"> <div class="row row-cards">
<div class="col-12"> <div class="col-12">
<div class="row g-3"> <div class="row g-3">
{ {
tiles.map((tile) => ( tiles.map((tile) => (
<div class="col-3"> <div class="col-3">
<Card> <Card>
<CardBody> <CardBody>
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col-auto"> <div class="col-auto">
<span class={`social social-md social-app-${tile.icon}`} /> <span class={`social social-md social-app-${tile.icon}`} />
</div> </div>
<div class="col"> <div class="col">
<div>{tile.title}</div> <div>{tile.title}</div>
<div class="text-secondary">{tile.description}</div> <div class="text-secondary">{tile.description}</div>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<Trending value={tile.trending} /> <Trending value={tile.trending} />
</div> </div>
</div> </div>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
)) ))
} }
</div> </div>
</div> </div>
<div class="col-12"> <div class="col-12">
<Card> <Card>
<CardHeader> <CardHeader>
<div class="card-title">Sign in with social media</div> <div class="card-title">Sign in with social media</div>
</CardHeader> </CardHeader>
<CardBody> <CardBody>
<ButtonList> <ButtonList>
{ {
socialList.map((social) => ( socialList.map((social) => (
<button class="btn"> <button class="btn">
<span class={`icon social social-app-${social.file}`} /> <span class={`icon social social-app-${social.file}`} />
Sign in with {social.name} Sign in with {social.name}
</button> </button>
)) ))
} }
</ButtonList> </ButtonList>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col-12"> <div class="col-12">
<Card> <Card>
<CardHeader> <CardHeader>
<div class="card-title">List of all social media icons</div> <div class="card-title">List of all social media icons</div>
</CardHeader> </CardHeader>
<CardBody class="p-0"> <CardBody class="p-0">
<div class="demo-icons-list-wrap"> <div class="demo-icons-list-wrap">
<div class="demo-icons-list"> <div class="demo-icons-list">
{ {
socialList.map((social) => ( socialList.map((social) => (
<span class="demo-icons-list-item" title={social.name} data-bs-toggle="tooltip" data-bs-placement="top"> <span
<span class={`social social-app-${social.file}`} /> class="demo-icons-list-item"
</span> title={social.name}
)) data-bs-toggle="tooltip"
} data-bs-placement="top"
{fillers.map(() => <div />)} >
</div> <span class={`social social-app-${social.file}`} />
</div> </span>
</CardBody> ))
</Card> }
</div> {fillers.map(() => <div />)}
</div> </div>
</div>
</CardBody>
</Card>
</div>
</div>
</DefaultLayout> </DefaultLayout>
+98 -99
View File
@@ -1,115 +1,114 @@
--- ---
import DefaultLayout from '@shared/layouts/DefaultLayout.astro' import DefaultLayout from '@shared/layouts/DefaultLayout.astro';
import Tag from '@ui/Tag.astro' import Tag from '@ui/Tag.astro';
import TagsList from '@ui/TagsList.astro' import TagsList from '@ui/TagsList.astro';
import Card from '@ui/Card.astro' import Card from '@ui/Card.astro';
import CardBody from '@ui/CardBody.astro' import CardBody from '@ui/CardBody.astro';
import CardTitle from '@ui/CardTitle.astro' import CardTitle from '@ui/CardTitle.astro';
import people from '@data/people.json' import people from '@data/people.json';
import flags from '@data/flags.json' import flags from '@data/flags.json';
import siteData from '@data/site.json' import siteData from '@data/site.json';
const tagIcons = ['bold', 'italic', 'underline', 'copy', 'scissors', 'file-plus', 'file-minus', 'ghost', 'star', 'script', 'photo', 'dog', 'piano'] const tagIcons = ['bold', 'italic', 'underline', 'copy', 'scissors', 'file-plus', 'file-minus', 'ghost', 'star', 'script', 'photo', 'dog', 'piano'];
const range = (a: number, b: number) => Array.from({ length: b - a + 1 }, (_, i) => a + i) const range = (a: number, b: number) => Array.from({ length: b - a + 1 }, (_, i) => a + i);
const flags9 = flags.slice(0, 9) const flags9 = flags.slice(0, 9);
const people8 = people.slice(0, 8) const people8 = people.slice(0, 8);
// site.colors is an object keyed by colour name; the Liquid loop yields the // site.colors yields value objects with .class / .title.
// value objects with .class / .title. const colors = Object.values(siteData.colors) as { class: string; title: string }[];
const colors = Object.values(siteData.colors) as { class: string; title: string }[]
--- ---
<DefaultLayout title="Tags" pageHeader="Tags" pageMenu="base.tags"> <DefaultLayout title="Tags" pageHeader="Tags" pageMenu="base.tags">
<div class="row row-cards row-cols-1 row-cols-md-2 row-cols-lg-3"> <div class="row row-cards row-cols-1 row-cols-md-2 row-cols-lg-3">
<div class="col"> <div class="col">
<Card> <Card>
<CardBody> <CardBody>
<CardTitle>Default tags</CardTitle> <CardTitle>Default tags</CardTitle>
<TagsList> <TagsList>
{range(1, 14).map((i) => <Tag text={`Label ${i}`} />)} {range(1, 14).map((i) => <Tag text={`Label ${i}`} />)}
</TagsList> </TagsList>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col"> <div class="col">
<Card> <Card>
<CardBody> <CardBody>
<CardTitle>Tags with flag</CardTitle> <CardTitle>Tags with flag</CardTitle>
<TagsList> <TagsList>
{flags9.map((country) => <Tag text={country.name} flag={(country as { code?: string }).code} />)} {flags9.map((country) => <Tag text={country.name} flag={(country as { code?: string }).code} />)}
</TagsList> </TagsList>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col"> <div class="col">
<Card> <Card>
<CardBody> <CardBody>
<CardTitle>Tags with icon</CardTitle> <CardTitle>Tags with icon</CardTitle>
<TagsList> <TagsList>
{tagIcons.map((icon) => <Tag text={icon} icon={icon} />)} {tagIcons.map((icon) => <Tag text={icon} icon={icon} />)}
</TagsList> </TagsList>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col"> <div class="col">
<Card> <Card>
<CardBody> <CardBody>
<CardTitle>Tags with avatar</CardTitle> <CardTitle>Tags with avatar</CardTitle>
<TagsList> <TagsList>
{people8.map((person) => <Tag text={person.full_name} person={person} />)} {people8.map((person) => <Tag text={person.full_name} person={person} />)}
</TagsList> </TagsList>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col"> <div class="col">
<Card> <Card>
<CardBody> <CardBody>
<CardTitle>Tags with status</CardTitle> <CardTitle>Tags with status</CardTitle>
<TagsList> <TagsList>
{colors.map((color) => <Tag text={color.title} status={color.class} />)} {colors.map((color) => <Tag text={color.title} status={color.class} />)}
</TagsList> </TagsList>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col"> <div class="col">
<Card> <Card>
<CardBody> <CardBody>
<CardTitle>Tags with legend</CardTitle> <CardTitle>Tags with legend</CardTitle>
<TagsList> <TagsList>
{colors.map((color) => <Tag text={color.title} legend={color.class} />)} {colors.map((color) => <Tag text={color.title} legend={color.class} />)}
</TagsList> </TagsList>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col"> <div class="col">
<Card> <Card>
<CardBody> <CardBody>
<CardTitle>Default tags</CardTitle> <CardTitle>Default tags</CardTitle>
<TagsList> <TagsList>
{range(1, 6).map((i) => <Tag text={`Label ${i}`} checkbox={true} />)} {range(1, 6).map((i) => <Tag text={`Label ${i}`} checkbox={true} />)}
{range(7, 12).map((i) => <Tag text={`Label ${i}`} checkbox={true} checked={true} />)} {range(7, 12).map((i) => <Tag text={`Label ${i}`} checkbox={true} checked={true} />)}
</TagsList> </TagsList>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col"> <div class="col">
<Card> <Card>
<CardBody> <CardBody>
<CardTitle>Default tags</CardTitle> <CardTitle>Default tags</CardTitle>
<TagsList> <TagsList>
{range(1, 12).map((i) => <Tag text="Label" badge={i} />)} {range(1, 12).map((i) => <Tag text="Label" badge={i} />)}
</TagsList> </TagsList>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
</div> </div>
</DefaultLayout> </DefaultLayout>
+163 -133
View File
@@ -1,152 +1,182 @@
--- ---
import CardActions from '@ui/CardActions.astro'
import DefaultLayout from '@shared/layouts/DefaultLayout.astro' import CardActions from '@ui/CardActions.astro';
import Button from '@ui/Button.astro' import DefaultLayout from '@shared/layouts/DefaultLayout.astro';
import Icon from '@ui/Icon.astro' import Button from '@ui/Button.astro';
import Avatar from '@ui/Avatar.astro' import Icon from '@ui/Icon.astro';
import Badge from '@ui/Badge.astro' import Avatar from '@ui/Avatar.astro';
import Modal from '@shared/components/modals/Modal.astro' import Badge from '@ui/Badge.astro';
import FormGroup from '@ui/FormGroup.astro' import Modal from '@shared/components/modals/Modal.astro';
import tasks from '@data/tasks.json' import CaptureModal from '@shared/components/CaptureModal.astro';
import people from '@data/people.json' import FormGroup from '@ui/FormGroup.astro';
import tasks from '@data/tasks.json';
import people from '@data/people.json';
interface Person { interface Person {
id?: string id?: string;
full_name?: string full_name?: string;
photo?: string photo?: string;
[key: string]: unknown [key: string]: unknown;
} }
interface Task { interface Task {
'name'?: string name?: string;
'assigned_to'?: number assigned_to?: number;
'due_date'?: string due_date?: string;
'due-date'?: string 'due-date'?: string;
'priority'?: string priority?: string;
[key: string]: unknown [key: string]: unknown;
} }
interface Column { interface Column {
name: string name: string;
tasks: Task[] tasks: Task[];
} }
const columns = (tasks as { columns: Column[] }).columns const columns = (tasks as { columns: Column[] }).columns;
const peopleList = people as Person[] const peopleList = people as Person[];
// add-task modal: assignable people (parts/modals/add-task.html — "5,6,2,3") // Assignable people for the add-task modal.
const selectedPeople = [5, 6, 2, 3].map((id) => peopleList[id - 1]) const selectedPeople = [5, 6, 2, 3].map((id) => peopleList[id - 1]);
--- ---
<DefaultLayout title="Task List" pageHeader="Task List" pageMenu="extra.tasks.list"> <DefaultLayout title="Task List" pageHeader="Task List" pageMenu="extra.tasks.list">
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
{ {
columns.map((section) => ( columns.map((section) => (
<div class="card mb-4"> <div class="card mb-4">
<div class="card-header"> <div class="card-header">
<h3 class="card-title mb-0">{section.name}</h3> <h3 class="card-title mb-0">{section.name}</h3>
<CardActions> <CardActions>
<Button text="New Task" icon="plus" modalId="add-task" size="sm" /> <Button text="New Task" icon="plus" modalId="add-task" size="sm" />
</CardActions> </CardActions>
</div> </div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-vcenter card-table table-selectable"> <table class="table table-vcenter card-table table-selectable">
<thead> <thead>
<tr> <tr>
<th class="w-1"> <th class="w-1">
<input type="checkbox" class="form-check-input align-middle table-selectable-check" aria-label="Select all tasks" /> <input
</th> type="checkbox"
<th class="w-50">Name</th> class="form-check-input align-middle table-selectable-check"
<th class="d-none d-xl-table-cell">Assigned To</th> aria-label="Select all tasks"
<th class="d-none d-xxl-table-cell">Due Date</th> />
<th>Priority</th> </th>
<th class="text-end">Actions</th> <th class="w-50">Name</th>
</tr> <th class="d-none d-xl-table-cell">Assigned To</th>
</thead> <th class="d-none d-xxl-table-cell">Due Date</th>
<tbody> <th>Priority</th>
{section.tasks.map((task) => { <th class="text-end">Actions</th>
const person = task.assigned_to ? peopleList[task.assigned_to - 1] : undefined </tr>
return ( </thead>
<tr> <tbody>
<td> {section.tasks.map((task) => {
<input type="checkbox" class="form-check-input align-middle table-selectable-check" aria-label="Select task" /> const person = task.assigned_to
</td> ? peopleList[task.assigned_to - 1]
<td>{task.name}</td> : undefined;
<td> return (
{task.assigned_to && person ? ( <tr>
<div class="d-flex align-items-center"> <td>
<Avatar personId={task.assigned_to} size="xs" class="me-2" /> <input
<span>{person.full_name}</span> type="checkbox"
</div> class="form-check-input align-middle table-selectable-check"
) : ( aria-label="Select task"
<span class="text-secondary">Unassigned</span> />
)} </td>
</td> <td>{task.name}</td>
<td class="text-secondary"> <td>
{task.due_date ? ( {task.assigned_to && person ? (
<Fragment> <div class="d-flex align-items-center">
<Icon name="calendar" class="me-1" /> <Avatar personId={task.assigned_to} size="xs" class="me-2" />
{task.due_date} <span>{person.full_name}</span>
</Fragment> </div>
) : task['due-date'] ? ( ) : (
<Fragment> <span class="text-secondary">Unassigned</span>
<Icon name="calendar" class="me-1" /> )}
{task['due-date']} </td>
</Fragment> <td class="text-secondary">
) : ( {task.due_date ? (
<span class="text-muted">—</span> <Fragment>
)} <Icon name="calendar" class="me-1" />
</td> {task.due_date}
<td>{task.priority === 'High' ? <Badge text="High" color="red" light /> : task.priority === 'Medium' ? <Badge text="Medium" color="yellow" light /> : task.priority === 'Low' ? <Badge text="Low" color="blue" light /> : <Badge text="—" light />}</td> </Fragment>
<td class="text-end"> ) : task['due-date'] ? (
<Button text="View" size="sm" /> <Fragment>
</td> <Icon name="calendar" class="me-1" />
</tr> {task['due-date']}
) </Fragment>
})} ) : (
</tbody> <span class="text-muted">—</span>
</table> )}
</div> </td>
</div> <td>
)) {task.priority === 'High' ? (
} <Badge text="High" color="red" light />
</div> ) : task.priority === 'Medium' ? (
</div> <Badge text="Medium" color="yellow" light />
) : task.priority === 'Low' ? (
<Badge text="Low" color="blue" light />
) : (
<Badge text="—" light />
)}
</td>
<td class="text-end">
<Button text="View" size="sm" />
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
))
}
</div>
</div>
<Modal modalId="add-task"> <CaptureModal>
<div class="modal-header"> <Modal modalId="add-task">
<h4 class="modal-title">Add task</h4> <div class="modal-header">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <h4 class="modal-title">Add task</h4>
</div> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body"> <div class="modal-body">
<form> <form>
<FormGroup label="Name"> <FormGroup label="Name">
<input type="text" class="form-control" placeholder="Task name" /> <input type="text" class="form-control" placeholder="Task name" />
</FormGroup> </FormGroup>
<FormGroup label="Assigned To"> <FormGroup label="Assigned To">
<select class="form-select"> <select class="form-select">
<option value="">Select person</option> <option value="">Select person</option>
{selectedPeople.map((person) => <option value={person.id}>{person.full_name}</option>)} {
</select> selectedPeople.map((person) => (
</FormGroup> <option value={person.id}>{person.full_name}</option>
<FormGroup label="Priority"> ))
<select class="form-select"> }
<option value="Low">Low</option> </select>
<option value="Medium">Medium</option> </FormGroup>
<option value="High">High</option> <FormGroup label="Priority">
</select> <select class="form-select">
</FormGroup> <option value="Low">Low</option>
<FormGroup label="Description"> <option value="Medium">Medium</option>
<textarea class="form-control" rows="3" placeholder="Task description"></textarea> <option value="High">High</option>
</FormGroup> </select>
</form> </FormGroup>
</div> <FormGroup label="Description">
<textarea class="form-control" rows="3" placeholder="Task description"></textarea>
</FormGroup>
</form>
</div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn" data-bs-dismiss="modal">Cancel</button> <button type="button" class="btn" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary ms-auto" data-bs-dismiss="modal"> Save </button> <button type="button" class="btn btn-primary ms-auto" data-bs-dismiss="modal">
</div> Save
</Modal> </button>
</div>
</Modal>
</CaptureModal>
</DefaultLayout> </DefaultLayout>
+1 -10
View File
@@ -3,16 +3,7 @@ import SingleLayout from '@shared/layouts/SingleLayout.astro'
import CardTitle from '@ui/CardTitle.astro' import CardTitle from '@ui/CardTitle.astro'
import Prose from '@ui/Prose.astro' import Prose from '@ui/Prose.astro'
// Liquid renders shared/includes/terms-of-service.md via `renderContent: "md"` // Card title is empty on purpose.
// (markdown-it). The rendered HTML below is inlined verbatim from the reference
// build to guarantee a 1:1 DOM match — Astro's markdown pipeline (remark +
// smartypants) would produce different quote characters and list structure.
// TODO: source of truth remains shared/includes/terms-of-service.md; regenerate
// this block if the markdown changes.
//
// The card title is empty on purpose: the Liquid template prints {{ page.title }},
// which is Eleventy's `page` object (no `title` property) — the reference output
// is an empty <h3>.
--- ---
<SingleLayout title="Terms of service" containerSize="narrow"> <SingleLayout title="Terms of service" containerSize="narrow">
+60 -75
View File
@@ -1,98 +1,83 @@
--- ---
// The right column renders headings h1..h6 via a Liquid {% for i in (1..6) %} // Right column renders headings h1..h6 with dynamic tag names. Homepage URL from site.json.
// loop → dynamic tag name here. {{ site.homepage }} → @data/site.json homepage. import DefaultLayout from '@shared/layouts/DefaultLayout.astro';
import DefaultLayout from '@shared/layouts/DefaultLayout.astro' import Prose from '@ui/Prose.astro';
import Prose from '@ui/Prose.astro' import siteData from '@data/site.json';
import siteData from '@data/site.json'
const homepage = siteData.homepage const homepage = siteData.homepage;
--- ---
<DefaultLayout title="Text features" pageHeader="Text features" pageMenu="extra.text-features"> <DefaultLayout title="Text features" pageHeader="Text features" pageMenu="extra.text-features">
<div class="row"> <div class="row">
<div class="col-7"> <div class="col-7">
<div class="card card-lg"> <div class="card card-lg">
<div class="card-body"> <div class="card-body">
<Prose> <Prose>
<h3>Text features</h3> <h3>Text features</h3>
<p>HTML provides various tags to format text and add meaning. For example, <strong>important words</strong> can be highlighted, and <em>emphasized text</em> can be italicized.</p> <p>HTML provides various tags to format text and add meaning. For example, <strong>important words</strong> can be highlighted, and <em>emphasized text</em> can be italicized.</p>
<p>If you want to visit an interesting website, check out <a href={homepage} target="_blank">this page</a>.</p> <p>If you want to visit an interesting website, check out <a href={homepage} target="_blank">this page</a>.</p>
<p>The term <abbr data-bs-toggle="tooltip" data-bs-placement="top" title="Hypertext Markup Language">HTML</abbr> is widely used in web development.</p> <p>The term <abbr data-bs-toggle="tooltip" data-bs-placement="top" title="Hypertext Markup Language">HTML</abbr> is widely used in web development.</p>
<p>Previously, the instruction said <del>"Do not include images."</del> However, <ins>"You may now add images."</ins></p> <p>Previously, the instruction said <del>"Do not include images."</del> However, <ins>"You may now add images."</ins></p>
<blockquote cite={homepage}>"The best way to predict the future is to create it." Peter Drucker</blockquote> <blockquote cite={homepage}>"The best way to predict the future is to create it." Peter Drucker</blockquote>
<p>Sometimes, <mark>highlighting important text</mark> can improve readability.</p> <p>Sometimes, <mark>highlighting important text</mark> can improve readability.</p>
<p>In JavaScript, you can log messages using the following code: <code>console.log('Hello, world!');</code></p> <p>In JavaScript, you can log messages using the following code: <code>console.log('Hello, world!');</code></p>
<p>To copy text on Windows, use <kbd>Ctrl + C</kbd>. On macOS, use <kbd>Cmd + C</kbd>.</p> <p>To copy text on Windows, use <kbd>Ctrl + C</kbd>. On macOS, use <kbd>Cmd + C</kbd>.</p>
<p>Water is written chemically as H<sub>2</sub>O, while Einsteins famous equation is E = mc<sup>2</sup>.</p> <p>Water is written chemically as H<sub>2</sub>O, while Einsteins famous equation is E = mc<sup>2</sup>.</p>
<p>Many people mistakenly spell <span class="text-incorrect">"recieve"</span> instead of <span class="text-correct">"receive"</span>.</p> <p>Many people mistakenly spell <span class="text-incorrect">"recieve"</span> instead of <span class="text-correct">"receive"</span>.</p>
<p>The correct way to write the date format is <span class="text-correct">"February 12, 2026"</span>, not <span class="text-incorrect">"12th February, 2026"</span> in American English.</p> <p>The correct way to write the date format is <span class="text-correct">"February 12, 2026"</span>, not <span class="text-incorrect">"12th February, 2026"</span> in American English.</p>
<p> <p>
If you need select text, you can use your mouse or keyboard. To select text using your mouse, click and drag the cursor over the text <span class="text-selected">you want to highlight</span>. If you need select text, you can use your mouse or keyboard. To select text using your mouse, click and drag the cursor over the text <span class="text-selected">you want to highlight</span>.
</p> </p>
<p><small>Disclaimer: This text is for demonstration purposes only.</small></p> <p><small>Disclaimer: This text is for demonstration purposes only.</small></p>
</Prose> </Prose>
</div> </div>
</div> </div>
</div> </div>
<div class="col"> <div class="col">
<div class="card card-lg"> <div class="card card-lg">
<div class="card-body"> <div class="card-body">
<Prose> <Prose>
{ {
[1, 2, 3, 4, 5, 6].map((i) => { [1, 2, 3, 4, 5, 6].map((i) => {
const Heading = `h${i}` const Heading = `h${i}`;
return ( return (
<Heading> <Heading>
Heading {i} by{' '} Heading {i} by <a class="mention"><span class="mention-avatar" style="background-image: url(/static/avatars/035f.jpg)"></span><span class="visually-hidden">@</span>JohnDoe</a>
<a class="mention"> </Heading>
<> );
<span class="mention-avatar" style="background-image: url(/static/avatars/035f.jpg)" /> })
<span class="visually-hidden">@</span> }
</>
JohnDoe
</a>
</Heading>
)
})
}
<p> <p>
Tabler is a modern UI framework which <span class="text-incorrect" data-bs-toggle="tooltip" data-bs-placement="top" title="Tooltip on top">provide</span> developers with a lot of <span class="text-incorrect">pre-build</span> components and customizable options. It is Tabler is a modern UI framework which <span class="text-incorrect" data-bs-toggle="tooltip" data-bs-placement="top" title="Tooltip on top">provide</span> developers with a lot of <span class="text-incorrect">pre-build</span> components and customizable options. It is
<span class="text-incorrect">build</span> on Bootstrap, making it easy to integrate into existing projects. The design is clean, responsive, and accessible, ensuring that <span class="text-incorrect">user</span> can navigate through <span class="text-incorrect">interface</span> easily. Tabler also <span <span class="text-incorrect">build</span> on Bootstrap, making it easy to integrate into existing projects. The design is clean, responsive, and accessible, ensuring that <span class="text-incorrect">user</span> can navigate
class="text-incorrect">support</span through <span class="text-incorrect">interface</span> easily. Tabler also <span class="text-incorrect">support</span> all modern browsers, but some features may not work properly on Internet Explorer. With
> all modern browsers, but some features may not work properly on Internet Explorer. With <span class="text-incorrect">it's</span> lightweight structure and optimized performance, Tabler helps developers create stunning web applications faster.
<span class="text-incorrect">it's</span> lightweight structure and optimized performance, Tabler helps developers create stunning web applications faster. </p>
</p>
<p> <p>Hey <a class="mention"><span class="mention-avatar" style="background-image: url(/static/avatars/035f.jpg)"></span><span class="visually-hidden">@</span>JohnDoe</a>, have you seen the latest updates on <a class="mention">#WebDevelopment<span class="mention-count">16</span></a>? <a class="mention"><span class="mention-avatar" style="background-image: url(/static/avatars/035f.jpg)"></span><span class="visually-hidden">@</span>JaneSmith</a> just shared an interesting article about <a class="mention"><span class="mention-app" style="background-image: url(/static/brands/messenger.svg)"></span>Messenger</a> and <a class="mention"><span class="mention-app" style="background-image: url(/static/brands/netflix.svg)"></span>Netflix</a>!</p>
Hey <a class="mention"><span class="mention-avatar" style="background-image: url(/static/avatars/035f.jpg)"></span><span class="visually-hidden">@</span>JohnDoe</a>, have you seen the latest updates on <a class="mention">#WebDevelopment<span class="mention-count">16</span></a>? <a class="mention"
><span class="mention-avatar" style="background-image: url(/static/avatars/035f.jpg)"></span><span class="visually-hidden">@</span>JaneSmith</a
> just shared an interesting article about <a class="mention"><span class="mention-app" style="background-image: url(/static/brands/messenger.svg)"></span>Messenger</a> and <a class="mention"><span class="mention-app" style="background-image: url(/static/brands/netflix.svg)"></span>Netflix</a>!
</p>
<p> <p>
The sky is <span class="mention"><span class="mention-color bg-blue"></span>#066fd1</span>, the grass is <span class="mention"><span class="mention-color bg-green"></span>rgb(47, 179, 68)</span>, fire trucks are often <span class="mention"><span class="mention-color bg-red"></span>red</span>, oranges are <span The sky is <span class="mention"><span class="mention-color bg-blue"></span>#066fd1</span>, the grass is <span class="mention"><span class="mention-color bg-green"></span>rgb(47, 179, 68)</span>, fire trucks are often <span class="mention"><span class="mention-color bg-red"></span>red</span>, oranges are <span class="mention"><span class="mention-color bg-orange"></span>hsl(24deg, 94.49%, 49.8%)</span>. Some flowers are <span class="mention"><span class="mention-color bg-purple"></span>hwb(288.35deg, 24.31%, 21.18%)</span>.
class="mention"><span class="mention-color bg-orange"></span>hsl(24deg, 94.49%, 49.8%)</span </p>
>. Some flowers are <span class="mention"><span class="mention-color bg-purple"></span>hwb(288.35deg, 24.31%, 21.18%)</span>.
</p>
<hr /> <hr />
</Prose> </Prose>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</DefaultLayout> </DefaultLayout>
+221 -209
View File
@@ -1,219 +1,231 @@
--- ---
import CardActions from '@ui/CardActions.astro' import CardActions from '@ui/CardActions.astro';
import DefaultLayout from '@shared/layouts/DefaultLayout.astro' import DefaultLayout from '@shared/layouts/DefaultLayout.astro';
import Button from '@ui/Button.astro' import CaptureScript from '@shared/components/CaptureScript.astro';
import ButtonList from '@ui/ButtonList.astro' import Button from '@ui/Button.astro';
import Card from '@ui/Card.astro' import ButtonList from '@ui/ButtonList.astro';
import CardHeader from '@ui/CardHeader.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 CardBody from '@ui/CardBody.astro';
import Badge from '@ui/Badge.astro' import CardTitle from '@ui/CardTitle.astro';
import Icon from '@ui/Icon.astro' import Badge from '@ui/Badge.astro';
import Icon from '@ui/Icon.astro';
--- ---
<DefaultLayout title="Driver Tour" pageHeader="Driver Tour" pageMenu="plugins.tour" pageLibs={['driver.js']}> <DefaultLayout
<div class="row row-cards"> title="Driver Tour"
<div class="col-12"> pageHeader="Driver Tour"
<Card> pageMenu="plugins.tour"
<CardHeader title="Product Tour Example"> pageLibs={['driver.js']}
<CardActions> >
<Button id="start-tour" text="Start Tour" icon="play" color="primary" element="button" /> <div class="row row-cards">
</CardActions> <div class="col-12">
</CardHeader> <Card>
<CardBody> <CardHeader title="Product Tour Example">
<p>Click the "Start Tour" button to begin an interactive tour of this page. The tour will guide you through different elements and features.</p> <CardActions>
</CardBody> <Button id="start-tour" text="Start Tour" icon="play" color="primary" element="button" />
</Card> </CardActions>
</div> </CardHeader>
<div class="col-md-6"> <CardBody>
<Card id="tour-card-1"> <p>
<CardHeader title="Welcome Section" /> Click the "Start Tour" button to begin an interactive tour of this page.
<CardBody> The tour will guide you through different elements and features.
<p>This is the first card in our tour. It demonstrates how Driver.js highlights elements on the page.</p> </p>
<ButtonList> </CardBody>
<Button text="Action Button" color="primary" element="button" /> </Card>
<Button text="Secondary" element="button" /> </div>
</ButtonList> <div class="col-md-6">
</CardBody> <Card id="tour-card-1">
</Card> <CardHeader title="Welcome Section" />
</div> <CardBody>
<p>This is the first card in our tour. It demonstrates how Driver.js highlights elements on the page.</p>
<ButtonList>
<Button text="Action Button" color="primary" element="button" />
<Button text="Secondary" element="button" />
</ButtonList>
</CardBody>
</Card>
</div>
<div class="col-md-6"> <div class="col-md-6">
<Card id="tour-card-2"> <Card id="tour-card-2">
<CardHeader title="Features Section" /> <CardHeader title="Features Section" />
<CardBody> <CardBody>
<p>This card shows additional features and demonstrates the tour's ability to navigate between different elements.</p> <p>This card shows additional features and demonstrates the tour's ability to navigate between different
<div class="form-selectgroup"> elements.</p>
<label class="form-selectgroup-item"> <div class="form-selectgroup">
<input type="radio" name="options" value="1" class="form-selectgroup-input" checked /> <label class="form-selectgroup-item">
<span class="form-selectgroup-label">Option 1</span> <input type="radio" name="options" value="1" class="form-selectgroup-input" checked>
</label> <span class="form-selectgroup-label">Option 1</span>
<label class="form-selectgroup-item"> </label>
<input type="radio" name="options" value="2" class="form-selectgroup-input" /> <label class="form-selectgroup-item">
<span class="form-selectgroup-label">Option 2</span> <input type="radio" name="options" value="2" class="form-selectgroup-input">
</label> <span class="form-selectgroup-label">Option 2</span>
</div> </label>
</CardBody> </div>
</Card> </CardBody>
</div> </Card>
<div class="col-12"> </div>
<Card id="tour-card-3"> <div class="col-12">
<CardHeader title="Navigation Example" /> <Card id="tour-card-3">
<CardBody> <CardHeader title="Navigation Example" />
<p>This is a full-width card that demonstrates how the tour works with larger elements.</p> <CardBody>
<div class="table-responsive"> <p>This is a full-width card that demonstrates how the tour works with larger elements.</p>
<table class="table table-vcenter"> <div class="table-responsive">
<thead> <table class="table table-vcenter">
<tr> <thead>
<th>Name</th> <tr>
<th>Status</th> <th>Name</th>
<th>Role</th> <th>Status</th>
<th class="w-1"></th> <th>Role</th>
</tr> <th class="w-1"></th>
</thead> </tr>
<tbody> </thead>
<tr> <tbody>
<td>John Doe</td> <tr>
<td><Badge text="Active" color="success" /></td> <td>John Doe</td>
<td>Developer</td> <td><Badge text="Active" color="success" /></td>
<td> <td>Developer</td>
<Button text="Edit" size="sm" href="#" /> <td>
</td> <Button text="Edit" size="sm" href="#" />
</tr> </td>
<tr> </tr>
<td>Jane Smith</td> <tr>
<td><Badge text="Pending" color="warning" /></td> <td>Jane Smith</td>
<td>Designer</td> <td><Badge text="Pending" color="warning" /></td>
<td> <td>Designer</td>
<Button text="Edit" size="sm" href="#" /> <td>
</td> <Button text="Edit" size="sm" href="#" />
</tr> </td>
</tbody> </tr>
</table> </tbody>
</div> </table>
</CardBody> </div>
</Card> </CardBody>
</div> </Card>
</div>
<div class="col-md-4"> <div class="col-md-4">
<Card id="tour-card-4"> <Card id="tour-card-4">
<CardBody class="text-center"> <CardBody class="text-center">
<div class="mb-3"> <div class="mb-3">
<Icon name="settings" size="48" /> <Icon name="settings" size="48" />
</div> </div>
<CardTitle>Settings</CardTitle> <CardTitle>Settings</CardTitle>
<p class="text-secondary">Configure your application settings here.</p> <p class="text-secondary">Configure your application settings here.</p>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<Card id="tour-card-5"> <Card id="tour-card-5">
<CardBody class="text-center"> <CardBody class="text-center">
<div class="mb-3"> <div class="mb-3">
<Icon name="users" size="48" /> <Icon name="users" size="48" />
</div> </div>
<CardTitle>Users</CardTitle> <CardTitle>Users</CardTitle>
<p class="text-secondary">Manage your team members and permissions.</p> <p class="text-secondary">Manage your team members and permissions.</p>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<Card id="tour-card-6"> <Card id="tour-card-6">
<CardBody class="text-center"> <CardBody class="text-center">
<div class="mb-3"> <div class="mb-3">
<Icon name="chart-bar" size="48" /> <Icon name="chart-bar" size="48" />
</div> </div>
<CardTitle>Analytics</CardTitle> <CardTitle>Analytics</CardTitle>
<p class="text-secondary">View your application statistics and reports.</p> <p class="text-secondary">View your application statistics and reports.</p>
</CardBody> </CardBody>
</Card> </Card>
</div> </div>
</div> </div>
<!-- BEGIN TOUR SCRIPT --> <CaptureScript>
<script is:inline> <!-- BEGIN TOUR SCRIPT -->
document.addEventListener('DOMContentLoaded', function () { <script is:inline>
const driverObj = driver.js.driver({ document.addEventListener('DOMContentLoaded', function () {
allowClose: true, const driverObj = driver.js.driver({
overlayClickNext: true, allowClose: true,
showButtons: ['next', 'previous', 'close'], overlayClickNext: true,
showProgress: true, showButtons: ['next', 'previous', 'close'],
steps: [ showProgress: true,
{ steps: [
element: '#start-tour', {
popover: { element: '#start-tour',
title: 'Welcome to the Tour!', popover: {
description: 'This button starts the interactive tour. Click it to begin exploring the page.', title: 'Welcome to the Tour!',
side: 'bottom', description: 'This button starts the interactive tour. Click it to begin exploring the page.',
align: 'start', side: 'bottom',
}, align: 'start'
}, }
{ },
element: '#tour-card-1', {
popover: { element: '#tour-card-1',
title: 'Welcome Section', popover: {
description: 'This is the first card in our tour. It demonstrates how Driver.js highlights elements on the page.', title: 'Welcome Section',
side: 'right', description: 'This is the first card in our tour. It demonstrates how Driver.js highlights elements on the page.',
align: 'start', side: 'right',
}, align: 'start'
}, }
{ },
element: '#tour-card-2', {
popover: { element: '#tour-card-2',
title: 'Features Section', popover: {
description: "This card shows additional features and demonstrates the tour's ability to navigate between different elements.", title: 'Features Section',
side: 'left', description: 'This card shows additional features and demonstrates the tour\'s ability to navigate between different elements.',
align: 'start', side: 'left',
}, align: 'start'
}, }
{ },
element: '#tour-card-3', {
popover: { element: '#tour-card-3',
title: 'Data Table', popover: {
description: 'This table shows how Driver.js works with larger elements. You can highlight entire sections of your page.', title: 'Data Table',
side: 'top', description: 'This table shows how Driver.js works with larger elements. You can highlight entire sections of your page.',
align: 'center', side: 'top',
}, align: 'center'
}, }
{ },
element: '#tour-card-4', {
popover: { element: '#tour-card-4',
title: 'Settings Card', popover: {
description: 'This card demonstrates how the tour works with smaller, centered elements.', title: 'Settings Card',
side: 'top', description: 'This card demonstrates how the tour works with smaller, centered elements.',
align: 'center', side: 'top',
}, align: 'center'
}, }
{ },
element: '#tour-card-5', {
popover: { element: '#tour-card-5',
title: 'Users Card', popover: {
description: 'Another example card showing user management features.', title: 'Users Card',
side: 'top', description: 'Another example card showing user management features.',
align: 'center', side: 'top',
}, align: 'center'
}, }
{ },
element: '#tour-card-6', {
popover: { element: '#tour-card-6',
title: 'Analytics Card', popover: {
description: 'The final step of the tour. This card shows analytics features.', title: 'Analytics Card',
side: 'top', description: 'The final step of the tour. This card shows analytics features.',
align: 'center', side: 'top',
}, align: 'center'
}, }
], }
}) ]
});
const startButton = document.getElementById('start-tour') const startButton = document.getElementById('start-tour');
if (startButton) { if (startButton) {
startButton.addEventListener('click', function () { startButton.addEventListener('click', function () {
driverObj.drive() driverObj.drive();
}) });
} }
}) });
</script> </script>
<!-- END TOUR SCRIPT --> <!-- END TOUR SCRIPT -->
</CaptureScript>
</DefaultLayout> </DefaultLayout>
+59 -49
View File
@@ -1,59 +1,69 @@
--- ---
// Note: the Liquid template assigns `progress` and `online_counter` per person // progress and online_counter assigned per person but never rendered — omitted.
// but never renders them — omitted here. // rounded=true on avatar has no effect — Avatar has no `rounded` prop.
// Note: the avatar include passes `rounded=true`, which ui/avatar.html ignores import DefaultLayout from '@shared/layouts/DefaultLayout.astro';
// (no such param) — omitted; Avatar.astro has no `rounded` prop. import Avatar from '@ui/Avatar.astro';
import DefaultLayout from '@shared/layouts/DefaultLayout.astro' import Card from '@ui/Card.astro';
import Avatar from '@ui/Avatar.astro' import CardBody from '@ui/CardBody.astro';
import Card from '@ui/Card.astro' import Icon from '@ui/Icon.astro';
import CardBody from '@ui/CardBody.astro' import Pagination from '@ui/Pagination.astro';
import Icon from '@ui/Icon.astro' import people from '@data/people.json';
import Pagination from '@ui/Pagination.astro'
import people from '@data/people.json'
interface Person { interface Person {
full_name?: string full_name?: string;
job_title?: string job_title?: string;
photo?: string photo?: string;
[key: string]: unknown [key: string]: unknown;
} }
const users = (people as Person[]).slice(0, 18) const users = (people as Person[]).slice(0, 18);
--- ---
<DefaultLayout title="Users list" pageHeader="Users" actions="users" description="1-18 of 413 people" pageMenu="extra.users"> <DefaultLayout
<div class="row row-cards"> title="Users list"
{ pageHeader="Users"
users.map((person, idx) => { actions="users"
const index = idx + 1 description="1-18 of 413 people"
return ( pageMenu="extra.users"
<div class="col-md-6 col-lg-3"> >
<Card> <div class="row row-cards">
<CardBody class="p-4 text-center"> {
<Avatar size="xl" person={person} class="mb-3" /> users.map((person, idx) => {
<h3 class="m-0 mb-1"> const index = idx + 1;
<a href="#">{person.full_name}</a> return (
</h3> <div class="col-md-6 col-lg-3">
<div class="text-secondary">{person.job_title}</div> <Card>
<CardBody class="p-4 text-center">
<Avatar size="xl" person={person} class="mb-3" />
<h3 class="m-0 mb-1">
<a href="#">{person.full_name}</a>
</h3>
<div class="text-secondary">{person.job_title}</div>
<div class="mt-3">{index === 1 ? <span class="badge bg-purple-lt">Owner</span> : index < 5 ? <span class="badge bg-green-lt">Admin</span> : null}</div> <div class="mt-3">
</CardBody> {index === 1 ? (
<div class="d-flex"> <span class="badge bg-purple-lt">Owner</span>
<a href="#" class="card-btn"> ) : index < 5 ? (
<Icon name="mail" color="muted" class="me-2" /> Email <span class="badge bg-green-lt">Admin</span>
</a> ) : null}
<a href="#" class="card-btn"> </div>
<Icon name="phone" color="muted" class="me-2" /> Call </CardBody>
</a> <div class="d-flex">
</div> <a href="#" class="card-btn">
</Card> <Icon name="mail" color="muted" class="me-2" /> Email
</div> </a>
) <a href="#" class="card-btn">
}) <Icon name="phone" color="muted" class="me-2" /> Call
} </a>
</div> </div>
</Card>
</div>
);
})
}
</div>
<div class="d-flex mt-4"> <div class="d-flex mt-4">
<Pagination class="ms-auto" /> <Pagination class="ms-auto" />
</div> </div>
</DefaultLayout> </DefaultLayout>
+1 -2
View File
@@ -10,8 +10,7 @@ import Progress from '@ui/Progress.astro'
import { site } from '@shared/lib/site' import { site } from '@shared/lib/site'
import timezones from '@data/timezones.json' import timezones from '@data/timezones.json'
// TODO: front matter `page-menu: extra.wizard` is not ported — it is only read // TODO: page-menu (extra.wizard) is unused — SingleLayout has no navbar menu.
// by layout/navbar-menu.html, which the `single` layout does not include.
--- ---
<SingleLayout title="Wizard"> <SingleLayout title="Wizard">
+8
View File
@@ -0,0 +1,8 @@
---
// Captures slot markup (typically a modal) into page-modals; BaseLayout emits
// it at the end of <body> via <PageModals />.
// Registration is synchronous (a promise) — see page-modals.ts.
import { addPageModal } from '@shared/lib/page-modals';
addPageModal(Astro.slots.render('default'));
---
+8
View File
@@ -0,0 +1,8 @@
---
// Captures slot markup (typically <script>) into page-scripts; BaseLayout /
// DocsLayout emit it at the end of <body> via <PageScripts />.
// Registration is synchronous (a promise) — see page-scripts.ts.
import { addPageScript } from '@shared/lib/page-scripts';
addPageScript(Astro.slots.render('default'));
---
+10
View File
@@ -0,0 +1,10 @@
---
// Separate component so drainPageScripts() executes only at this point of the
// render stream (after the page content render has STARTED — registrations are
// synchronous, script content may be a promise).
import { drainPageScripts } from '@shared/lib/page-scripts';
const scripts = await Promise.all(drainPageScripts());
---
{scripts.length > 0 && <Fragment set:html={scripts.join('\n')} />}
+21 -22
View File
@@ -1,31 +1,30 @@
--- ---
// Equivalent of cards/auth-lock.html import Button from '@ui/Button.astro';
import Button from '@ui/Button.astro' import Avatar from '@ui/Avatar.astro';
import Avatar from '@ui/Avatar.astro' import CardTitle from '@ui/CardTitle.astro';
import CardTitle from '@ui/CardTitle.astro' import people from '@data/people.json';
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> <form class="card card-md" action="./" method="get" autocomplete="off" novalidate>
<div class="card-body text-center"> <div class="card-body text-center">
<div class="mb-4"> <div class="mb-4">
<CardTitle as="h2">Account Locked</CardTitle> <CardTitle as="h2">Account Locked</CardTitle>
<p class="text-secondary">Please enter your password to unlock your account</p> <p class="text-secondary">Please enter your password to unlock your account</p>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<Avatar person={person} size="xl" class="mb-3" /> <Avatar person={person} size="xl" class="mb-3" />
<h3>{person.full_name}</h3> <h3>{person.full_name}</h3>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<input type="password" class="form-control" placeholder="Password" /> <input type="password" class="form-control" placeholder="Password&hellip;" />
</div> </div>
<div> <div>
<Button color="primary" block icon="lock-open" text="Unlock" /> <Button color="primary" block icon="lock-open" text="Unlock" />
</div> </div>
</div> </div>
</form> </form>
@@ -1,8 +1,4 @@
--- ---
// Equivalent of cards/body-placeholder.html (ui/svg.html inlined).
// TODO: the ui/svg.html `border` branch (rect + inline style) is not ported —
// body-placeholder.html never sets it (the "border" token in the class
// list is just a CSS class).
interface Props { interface Props {
width?: number width?: number
height?: number height?: number
+2 -14
View File
@@ -1,14 +1,4 @@
--- ---
// Equivalent of cards/card.html
// TODO: ui/nav.html is inlined here only in its header variant (tabs/pills
// inside .card-header) — the standalone nav variants are not ported.
// TODO: ui/photo.html is inlined only in its ratio branch with the fixed
// photo ids from the Liquid source (img-top → id=7, img-bottom → id=11;
// the `id=img-id` argument in card.html is always undefined and the
// trailing literal id wins).
// TODO: ui/form/check.html is inlined only in the empty+checked (switch)
// variants used by footer-elements; other check params are not ported.
import CardSubtitle from '@ui/CardSubtitle.astro'; import CardSubtitle from '@ui/CardSubtitle.astro';
import Icon from '@ui/Icon.astro'; import Icon from '@ui/Icon.astro';
import AvatarList from '@ui/AvatarList.astro'; import AvatarList from '@ui/AvatarList.astro';
@@ -40,8 +30,7 @@ interface Props {
footer?: boolean; footer?: boolean;
footerButton?: boolean; footerButton?: boolean;
footerButtons?: boolean; footerButtons?: boolean;
/** a ">" prefix pushes the element (and, per the Liquid loop-variable leak, /** a ">" prefix pushes the element (and every following one) to the right — `right` is never reset */
* every following one) to the right */
footerElements?: string[]; footerElements?: string[];
progress?: boolean; progress?: boolean;
} }
@@ -83,8 +72,7 @@ const classes = [
className, className,
] ]
// footer-elements parsing — mirrors the Liquid loop, including the fact that // footer-elements parsing — `right` is never reset once set across iterations.
// `right` is never reset once set (Liquid assign leaks across iterations).
let right = false; let right = false;
const footerEls = (footerElements ?? []).map((element: string) => { const footerEls = (footerElements ?? []).map((element: string) => {
let el = element; let el = element;
@@ -1,13 +1,15 @@
--- ---
// Equivalent of cards/card-background-icon.html import CardStamp from '@ui/CardStamp.astro';
import CardStamp from '@ui/CardStamp.astro' import CardTitle from '@ui/CardTitle.astro';
import CardTitle from '@ui/CardTitle.astro'
--- ---
<div class="card bg-primary text-primary-fg"> <div class="card bg-primary text-primary-fg">
<CardStamp icon="star" color="white" textColor="primary" /> <CardStamp icon="star" color="white" textColor="primary" />
<div class="card-body"> <div class="card-body">
<CardTitle>Card with background and icon</CardTitle> <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> <p>
</div> 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> </div>
+22 -22
View File
@@ -1,32 +1,32 @@
--- ---
// Equivalent of cards/card-image.html import photos from '@data/photos.json';
// TODO: ui/photo.html is inlined only in its plain <img> branch (no
// background/ratio params are used by card-image.html).
import photos from '@data/photos.json'
interface Props { interface Props {
title?: string title?: string;
right?: boolean right?: boolean;
imgId?: number 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 photo = (photos as { file: string; title: string }[])[imgId];
const imgClass = `w-100 h-100 object-cover ${right ? 'card-img-end' : 'card-img-start'}` const imgClass = `w-100 h-100 object-cover ${right ? 'card-img-end' : 'card-img-start'}`;
--- ---
<div class="card"> <div class="card">
<div class="row row-0"> <div class="row row-0">
<div class={`col-3${right ? ' order-md-last' : ''}`}> <div class={`col-3${right ? ' order-md-last' : ''}`}>
<!-- Photo --> <!-- Photo -->
<img src={`./static/photos/${photo.file}`} class={imgClass} alt={photo.title} /> <img src={`./static/photos/${photo.file}`} class={imgClass} alt={photo.title} />
</div> </div>
<div class="col"> <div class="col">
<div class="card-body"> <div class="card-body">
{title && <h3 class="card-title" set:html={title} />} {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> <p class="text-secondary">
</div> Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aperiam deleniti fugit incidunt, iste, itaque minima
</div> neque pariatur perferendis sed suscipit velit vitae voluptatem.
</div> </p>
</div>
</div>
</div>
</div> </div>
+9 -7
View File
@@ -1,12 +1,14 @@
--- ---
// Equivalent of cards/card-ribbon-text.html import CardTitle from '@ui/CardTitle.astro';
import CardTitle from '@ui/CardTitle.astro'
--- ---
<div class="card"> <div class="card">
<div class="ribbon bg-red">NEW</div> <div class="ribbon bg-red">NEW</div>
<div class="card-body"> <div class="card-body">
<CardTitle>Card with text ribbon</CardTitle> <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> <p class="text-secondary">
</div> 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> </div>
+10 -8
View File
@@ -1,13 +1,15 @@
--- ---
// Equivalent of cards/card-ribbon-top.html import Icon from '@ui/Icon.astro';
import Icon from '@ui/Icon.astro' import CardTitle from '@ui/CardTitle.astro';
import CardTitle from '@ui/CardTitle.astro'
--- ---
<div class="card"> <div class="card">
<div class="ribbon ribbon-top bg-yellow"><Icon name="star" /></div> <div class="ribbon ribbon-top bg-yellow"><Icon name="star" /></div>
<div class="card-body"> <div class="card-body">
<CardTitle>Card with top ribbon</CardTitle> <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> <p class="text-secondary">
</div> 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> </div>
+33 -30
View File
@@ -1,30 +1,33 @@
--- ---
// Equivalent of cards/card-tabs.html // Tabs/content blocks are HTML strings (different order when `bottom`).
// The tabs/content blocks are built as HTML strings to mirror the Liquid
// {% capture %} blocks (they are emitted in a different order for `bottom`).
interface Props { interface Props {
/** present in the Liquid source (tabs-count) but never used there */ /** tabs-count — present but unused */
count?: number count?: number;
id?: string id?: string;
bottom?: boolean bottom?: boolean;
borderless?: 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 = ` const tabsHtml = `
<!-- Cards navigation --> <!-- Cards navigation -->
<ul class="nav nav-tabs${bottom ? ' nav-tabs-bottom' : ''}"> <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')} ${tabs
</ul>` .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 = ` const tabsContentHtml = `
<div class="tab-content"> <div class="tab-content">
${tabs ${tabs
.map( .map(
(tab, i) => ` <!-- Content of card #${tab} --> (tab, i) => ` <!-- Content of card #${tab} -->
<div id="tab-${id}-${i + 1}" class="card tab-pane${i === 0 ? ' active show' : ''}"> <div id="tab-${id}-${i + 1}" class="card tab-pane${i === 0 ? ' active show' : ''}">
<div class="card-body"> <div class="card-body">
<div class="card-title">${tab}</div> <div class="card-title">${tab}</div>
@@ -33,24 +36,24 @@ ${tabs
</p> </p>
</div> </div>
</div>`, </div>`,
) )
.join('\n')} .join('\n')}
</div>` </div>`;
--- ---
<!-- Cards with tabs component --> <!-- Cards with tabs component -->
<div class={`card-tabs${borderless ? ' border-0' : ''}`}> <div class={`card-tabs${borderless ? ' border-0' : ''}`}>
{ {
bottom ? ( bottom ? (
<Fragment> <Fragment>
<Fragment set:html={tabsContentHtml} /> <Fragment set:html={tabsContentHtml} />
<Fragment set:html={tabsHtml} /> <Fragment set:html={tabsHtml} />
</Fragment> </Fragment>
) : ( ) : (
<Fragment> <Fragment>
<Fragment set:html={tabsHtml} /> <Fragment set:html={tabsHtml} />
<Fragment set:html={tabsContentHtml} /> <Fragment set:html={tabsContentHtml} />
</Fragment> </Fragment>
) )
} }
</div> </div>
+90 -68
View File
@@ -1,81 +1,103 @@
--- ---
// ui/carousel.html is inlined here (not a shared component in this task's // Photos filtered to horizontal=true, then sliced by offset/limit.
// scope). Photos are filtered to horizontal=true, then sliced by offset/limit. import photos from '@data/photos.json';
import photos from '@data/photos.json' import CardTitle from '@ui/CardTitle.astro';
import CardTitle from '@ui/CardTitle.astro'
interface Props { interface Props {
id?: string id?: string;
title?: string title?: string;
indicators?: boolean indicators?: boolean;
indicatorsThumb?: boolean indicatorsThumb?: boolean;
indicatorsThumbRatio?: boolean indicatorsThumbRatio?: boolean;
indicatorsDot?: boolean indicatorsDot?: boolean;
indicatorsVertical?: boolean indicatorsVertical?: boolean;
controls?: boolean controls?: boolean;
captions?: boolean captions?: boolean;
offset?: number offset?: number;
limit?: number limit?: number;
fade?: boolean 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 carouselId = idProp ?? 'carousel';
const filteredPhotos = (photos as { file: string; horizontal?: boolean }[]).filter((p) => p.horizontal) const filteredPhotos = (photos as { file: string; horizontal?: boolean }[]).filter((p) => p.horizontal);
const slides = filteredPhotos.slice(offset, offset + limit) const slides = filteredPhotos.slice(offset, offset + limit);
--- ---
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<CardTitle>{title}</CardTitle> <CardTitle>{title}</CardTitle>
</div> </div>
<div class="card-body"> <div class="card-body">
<div id={`carousel-${carouselId}`} class={`carousel slide${fade ? ' carousel-fade' : ''}`} data-bs-ride="carousel"> <div id={`carousel-${carouselId}`} class={`carousel slide${fade ? ' carousel-fade' : ''}`} data-bs-ride="carousel">
{ {
indicators && ( indicators && (
<div class={`carousel-indicators${indicatorsVertical ? ' carousel-indicators-vertical' : ''}${indicatorsDot ? ' carousel-indicators-dot' : indicatorsThumb ? ' carousel-indicators-thumb' : ''}`}> <div
{slides.map((photo, i) => ( class={`carousel-indicators${indicatorsVertical ? ' carousel-indicators-vertical' : ''}${
<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} /> indicatorsDot ? ' carousel-indicators-dot' : indicatorsThumb ? ' carousel-indicators-thumb' : ''
))} }`}
</div> >
) {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"> <div class="carousel-inner">
{ {
slides.map((photo, i) => ( slides.map((photo, i) => (
<div class={`carousel-item${i === 0 ? ' active' : ''}`}> <div class={`carousel-item${i === 0 ? ' active' : ''}`}>
<img class="d-block w-100" alt="" src={`./static/photos/${photo.file}`} /> <img class="d-block w-100" alt="" src={`./static/photos/${photo.file}`} />
{captions && ( {captions && (
<Fragment> <Fragment>
<div class="carousel-caption-background d-none d-md-block" /> <div class="carousel-caption-background d-none d-md-block" />
<div class="carousel-caption d-none d-md-block"> <div class="carousel-caption d-none d-md-block">
<h3>Slide label</h3> <h3>Slide label</h3>
<p>Nulla vitae elit libero, a pharetra augue mollis interdum.</p> <p>Nulla vitae elit libero, a pharetra augue mollis interdum.</p>
</div> </div>
</Fragment> </Fragment>
)} )}
</div> </div>
)) ))
} }
</div> </div>
{ {
controls && ( controls && (
<Fragment> <Fragment>
<a class="carousel-control-prev" href={`#carousel-${carouselId}`} role="button" data-bs-slide="prev"> <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="carousel-control-prev-icon" aria-hidden="true" />
<span class="visually-hidden">Previous</span> <span class="visually-hidden">Previous</span>
</a> </a>
<a class="carousel-control-next" href={`#carousel-${carouselId}`} role="button" data-bs-slide="next"> <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="carousel-control-next-icon" aria-hidden="true" />
<span class="visually-hidden">Next</span> <span class="visually-hidden">Next</span>
</a> </a>
</Fragment> </Fragment>
) )
} }
</div> </div>
</div> </div>
</div> </div>
+7 -8
View File
@@ -1,7 +1,6 @@
--- ---
// The `{% highlight css %}` block emits the raw CSS text (no highlight markup in // Raw CSS text inside .card-code (no highlight markup).
// the dev build), rendered here as a text node inside .card-code. import CardTitle from '@ui/CardTitle.astro';
import CardTitle from '@ui/CardTitle.astro'
const code = ` const code = `
.card-footer { .card-footer {
@@ -11,12 +10,12 @@ const code = `
border-radius: 0 0 1 2; border-radius: 0 0 1 2;
} }
} }
` `;
--- ---
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<CardTitle>Card with code</CardTitle> <CardTitle>Card with code</CardTitle>
</div> </div>
<div class="card-code">{code}</div> <div class="card-code">{code}</div>
</div> </div>
+49 -44
View File
@@ -1,53 +1,58 @@
--- ---
// Equivalent of cards/credit-card.html import Button from '@ui/Button.astro';
// TODO: ui/form/input-mask.html is inlined only with the params used here import FormGroup from '@ui/FormGroup.astro';
// (mask + visible; placeholder falls back to the mask value).
import Button from '@ui/Button.astro'
import FormGroup from '@ui/FormGroup.astro'
const months = Array.from({ length: 12 }, (_, i) => i + 1) const months = Array.from({ length: 12 }, (_, i) => i + 1);
const years = Array.from({ length: 11 }, (_, i) => 2020 + i) const years = Array.from({ length: 11 }, (_, i) => 2020 + i);
--- ---
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<div class="mb-3"> <div class="mb-3">
<div class="form-label">Card number</div> <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" /> <input
</div> 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="mb-3">
<div class="form-label">Card name</div> <div class="form-label">Card name</div>
<input type="text" class="form-control" /> <input type="text" class="form-control" />
</div> </div>
<div class="row"> <div class="row">
<div class="col-8"> <div class="col-8">
<FormGroup label="Expiration date"> <FormGroup label="Expiration date">
<div class="row g-2"> <div class="row g-2">
<div class="col"> <div class="col">
<select class="form-select"> <select class="form-select">
{months.map((month) => <option value={month}>{month}</option>)} {months.map((month) => <option value={month}>{month}</option>)}
</select> </select>
</div> </div>
<div class="col"> <div class="col">
<select class="form-select"> <select class="form-select">
{years.map((year) => <option value={year}>{year}</option>)} {years.map((year) => <option value={year}>{year}</option>)}
</select> </select>
</div> </div>
</div> </div>
</FormGroup> </FormGroup>
</div> </div>
<div class="col"> <div class="col">
<div class="mb-3"> <div class="mb-3">
<div class="form-label">CVV</div> <div class="form-label">CVV</div>
<input type="number" class="form-control" /> <input type="number" class="form-control" />
</div> </div>
</div> </div>
</div> </div>
<div class="mt-2"> <div class="mt-2">
<Button text="Pay now" color="primary" block /> <Button text="Pay now" color="primary" block />
</div> </div>
</div> </div>
</div> </div>
+47 -45
View File
@@ -1,57 +1,59 @@
--- ---
// TODO: unported ui/crm-stats.html branches (unused on dashboard-crm): import Subheader from '@ui/Subheader.astro';
// person-id (avatar), chart-data + chart-position/chart-label/chart-label-icon import Icon from '@ui/Icon.astro';
// (sparkline), small-icon, description-value/description-value-color, trending, import Trending from '@ui/Trending.astro';
// button, chart-type/chart-color.
import Subheader from '@ui/Subheader.astro'
import Icon from '@ui/Icon.astro'
import Trending from '@ui/Trending.astro'
interface Props { interface Props {
color?: string color?: string;
icon?: string icon?: string;
/** lt — light avatar background (bg-{color}-lt) instead of bg-{color} text-white */ /** lt — light avatar background (bg-{color}-lt) instead of bg-{color} text-white */
lt?: boolean lt?: boolean;
title?: string title?: string;
description?: string description?: string;
/** change-value — passed straight to ui/trending.html (keeps its +/- sign) */ /** change-value — passed to Trending (keeps its +/- sign) */
changeValue?: string changeValue?: string;
/** change-value-unit — trending unit; defaults to '%' in Trending when unset */ /** change-value-unit — trending unit; defaults to '%' in Trending when unset */
changeValueUnit?: string changeValueUnit?: string;
class?: 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:list={['card card-sm', className]}>
<div class="card-body p-3"> <div class="card-body p-3">
<div class="row align-items-center"> <div class="row align-items-center">
{ {
icon && ( icon && (
<div class="col-auto"> <div class="col-auto">
<span class:list={avatarClass}> <span class:list={avatarClass}><Icon name={icon} /></span>
<Icon name={icon} /> </div>
</span> )
</div> }
)
}
<div class="col"> <div class="col">
<Subheader> <Subheader>
{title} {title}
</Subheader> </Subheader>
<div class="h3 m-0 p-0"> <div class="h3 m-0 p-0">
{description} {description}
</div> </div>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<Trending value={changeValue as unknown as number} unit={changeValueUnit} /> <Trending value={changeValue as unknown as number} unit={changeValueUnit} />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
+13 -16
View File
@@ -1,21 +1,18 @@
--- ---
// Equivalent of cards/empty-team.html import AvatarList from '@ui/AvatarList.astro';
// ui/avatar-list.html is inlined (stacked, no explicit size — the `avatar-1` import Button from '@ui/Button.astro';
// class in the Eleventy output is the include['size'] bug and is skipped). 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 card-lg card-dashed card-transparent">
<div class="card-body text-center"> <div class="card-body text-center">
<div class="mb-3"> <div class="mb-3">
<AvatarList stacked={true} /> <AvatarList stacked={true} />
</div> </div>
<CardTitle>No Team Members</CardTitle> <CardTitle>No Team Members</CardTitle>
<p class="text-secondary">Invite your team to<br />collaborate on this project.</p> <p class="text-secondary">Invite your team to<br />collaborate on this project.</p>
<div class="mt-4"> <div class="mt-4">
<Button text="Invite Members" color="primary" icon="plus" /> <Button text="Invite Members" color="primary" icon="plus" />
</div> </div>
</div> </div>
</div> </div>
@@ -1,5 +1,4 @@
--- ---
// Equivalent of cards/forgot-password.html
import FormFooter from '@ui/FormFooter.astro' import FormFooter from '@ui/FormFooter.astro'
import Button from '@ui/Button.astro' import Button from '@ui/Button.astro'
+41 -43
View File
@@ -1,59 +1,57 @@
--- ---
// Equivalent of cards/gallery-photo.html. // `photo` and `index` are passed explicitly from the parent loop.
// The Liquid include reads `photo` and `forloop.index` from the parent loop scope — import Avatar from '@ui/Avatar.astro';
// in Astro they are passed explicitly as the `photo` and `index` props. import Icon from '@ui/Icon.astro';
import Avatar from '@ui/Avatar.astro' import { randomNumber, timeagoLabel } from '@shared/lib/pseudo-random';
import Icon from '@ui/Icon.astro'
import { randomNumber, timeagoLabel } from '@shared/lib/pseudo-random'
interface Photo { interface Photo {
file: string file: string;
[key: string]: unknown [key: string]: unknown;
} }
interface Person { interface Person {
full_name?: string full_name?: string;
photo?: string photo?: string;
[key: string]: unknown [key: string]: unknown;
} }
interface Props { interface Props {
photo: Photo photo: Photo;
person: Person person: Person;
/** 1-based loop index (forloop.index in Liquid) — seeds the deterministic pseudo-random values */ /** 1-based loop index — seeds the deterministic pseudo-random values */
index: number index: number;
hideLikes?: boolean hideLikes?: boolean;
} }
const { photo, person, index, hideLikes } = Astro.props const { photo, person, index, hideLikes } = Astro.props;
// Liquid: {% if forloop.index > 2 and forloop.index < 9 or forloop.index == 10 %} // 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"> <div class="card card-sm">
<a href="#" class="d-block"><img src={`./static/photos/${photo.file}`} class="card-img-top" /></a> <a href="#" class="d-block"><img src={`./static/photos/${photo.file}`} class="card-img-top" /></a>
<div class="card-body"> <div class="card-body">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<Avatar person={person} class="me-3 rounded" /> <Avatar person={person} class="me-3 rounded" />
<div> <div>
<div>{person.full_name}</div> <div>{person.full_name}</div>
<div class="text-secondary">{timeagoLabel(index, 10)}</div> <div class="text-secondary">{timeagoLabel(index, 10)}</div>
</div> </div>
{ {
!hideLikes && ( !hideLikes && (
<div class="ms-auto"> <div class="ms-auto">
<a href="#" class="text-secondary"> <a href="#" class="text-secondary">
<Icon name="eye" /> <Icon name="eye" />
{randomNumber(index, 300, 600)} {randomNumber(index, 300, 600)}
</a> </a>
<a href="#" class="ms-3 text-secondary"> <a href="#" class="ms-3 text-secondary">
<Icon name="heart" class={heartClass} /> <Icon name="heart" class={heartClass} />
{randomNumber(index, 20, 100)} {randomNumber(index, 20, 100)}
</a> </a>
</div> </div>
) )
} }
</div> </div>
</div> </div>
</div> </div>
+41 -32
View File
@@ -1,43 +1,52 @@
--- ---
import Icon from '@ui/Icon.astro' import Icon from '@ui/Icon.astro';
import icons from '@data/icons.json' import icons from '@data/icons.json';
interface Props { interface Props {
title?: string title?: string;
type?: 'outline' | 'filled' type?: 'outline' | 'filled';
} }
const { title, type = 'outline' } = Astro.props const { title, type = 'outline' } = Astro.props;
// Liquid: limit is Infinity, but 20 when environment == 'development'. // Dev preview limits icons to 20 (see BaseLayout).
// The PoC mirrors the development build (see BaseLayout), so limit = 20. // The limit applies before the svg[type] filter — the "filled" card shows only filled icons among the first 20.
// NOTE: the limit applies to the ITERATION, before the svg[type] filter — const limit = 20;
// the "filled" card therefore shows only the filled icons among the first 20.
const limit = 20
const entries = Object.entries(icons as unknown as Record<string, { svg?: Record<string, string> }>).slice(0, limit) const entries = Object.entries(icons as Record<string, { svg?: Record<string, string> }>).slice(
0,
limit,
);
--- ---
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<div class="card-title">{title}</div> <div class="card-title">{title}</div>
</div> </div>
<div class="card-body p-0"> <div class="card-body p-0">
<div class="demo-icons-list-wrap"> <div class="demo-icons-list-wrap">
<div class="demo-icons-list"> <div class="demo-icons-list">
{ {
entries.map( entries.map(
([iconName, icon]) => ([iconName, icon]) =>
icon.svg?.[type] && ( 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"> <a
<Icon name={iconName} type={type} /> href={`https://tabler.io/icons/icon/${iconName}`}
</a> target="_blank"
), rel="noopener"
) class="demo-icons-list-item"
} title={iconName}
{/* Liquid: {% for icon in (0..20) %} → 21 empty divs (flexbox filler). */} data-bs-toggle="tooltip"
{Array.from({ length: 21 }).map(() => <div />)} data-bs-placement="top"
</div> >
</div> <Icon name={iconName} type={type} />
</div> </a>
),
)
}
{/* 21 empty divs (flexbox filler) */}
{Array.from({ length: 21 }).map(() => <div />)}
</div>
</div>
</div>
</div> </div>
@@ -1,5 +1,4 @@
--- ---
// Equivalent of cards/invoice.html (static markup, no parameters)
--- ---
<div class="card card-lg"> <div class="card card-lg">
+40 -40
View File
@@ -1,54 +1,54 @@
--- ---
// The feature names are the fixed list from the Liquid source; the // Feature names are a fixed list; the `features` string is split per-character into a 1/0 availability mask.
// `features` string is split per-character into a 1/0 availability mask. import Icon from '@ui/Icon.astro';
// ui/ribbon.html is inlined here in its bookmark+top+color variant (the only
// one used by this card); its star icon renders as the outline `star`
// (ui/icon.html ignores the `use-svg`/`filled` args).
import Icon from '@ui/Icon.astro'
interface Props { interface Props {
price?: string price?: string;
users?: string | number users?: string | number;
category?: string category?: string;
features?: string features?: string;
/** featured-color — ribbon + button color (e.g. "green") */ /** featured-color — ribbon + button color (e.g. "green") */
featuredColor?: string 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 featureNames = ['Sharing Tools', 'Design Tools', 'Private Messages', 'Twitter API'];
const availableFeatures = features.split('') const availableFeatures = features.split('');
--- ---
<div class="card card-md"> <div class="card card-md">
{ {
featuredColor && ( featuredColor && (
<div class={`ribbon ribbon-top ribbon-bookmark bg-${featuredColor}`}> <div class={`ribbon ribbon-top ribbon-bookmark bg-${featuredColor}`}>
<Icon name="star" /> <Icon name="star" />
</div> </div>
) )
} }
<div class="card-body text-center"> <div class="card-body text-center">
<div class="text-uppercase text-secondary fw-medium">{category}</div> <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"> <ul class="list-unstyled lh-lg">
<li><strong>{users}</strong> Users</li> <li><strong>{users}</strong> Users</li>
{ {
featureNames.map((feature, i) => ( featureNames.map((feature, i) => (
<li> <li>
{availableFeatures[i] === '1' ? <Icon name="check" class="me-1 text-success" /> : <Icon name="x" class="me-1 text-danger" />} {availableFeatures[i] === '1' ? (
{feature} <Icon name="check" class="me-1 text-success" />
</li> ) : (
)) <Icon name="x" class="me-1 text-danger" />
} )}
</ul> {feature}
</li>
))
}
</ul>
<div class="text-center mt-4"> <div class="text-center mt-4">
<a href="#" class={`btn${featuredColor ? ` btn-${featuredColor}` : ''} w-100`}>Choose plan</a> <a href="#" class={`btn${featuredColor ? ` btn-${featuredColor}` : ''} w-100`}>Choose plan</a>
</div> </div>
</div> </div>
</div> </div>
+25 -27
View File
@@ -1,40 +1,38 @@
--- ---
// TODO: Liquid default `person = include.person | default: people[1]` — the // TODO: fallback to people[1] when person omitted — unused on card-gradients, so `person` is required here.
// no-person fallback branch is unused on card-gradients (person is always import Icon from '@ui/Icon.astro';
// passed), so `person` is required here. import Avatar from '@ui/Avatar.astro';
import Icon from '@ui/Icon.astro' import ButtonList from '@ui/ButtonList.astro';
import Avatar from '@ui/Avatar.astro'
import ButtonList from '@ui/ButtonList.astro'
interface Person { interface Person {
full_name?: string full_name?: string;
job_title?: string job_title?: string;
[key: string]: unknown [key: string]: unknown;
} }
interface Props { interface Props {
person: Person person: Person;
color?: string 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 card-gradient card-gradient-${color}`}>
<div class="card-body text-center py-6"> <div class="card-body text-center py-6">
<div class="position-absolute top-0 end-0 p-1"> <div class="position-absolute top-0 end-0 p-1">
<div class="btn btn-action"><Icon name="star" color="yellow" type="filled" /></div> <div class="btn btn-action"><Icon name="star" color="yellow" type="filled" /></div>
</div> </div>
<div> <div>
<Avatar size="2xl" person={person} shape="rounded-circle" /> <Avatar size="2xl" person={person} shape="rounded-circle" />
</div> </div>
<div class="h1 mt-4 mb-1">{person.full_name}</div> <div class="h1 mt-4 mb-1">{person.full_name}</div>
<div class="text-secondary">{person.job_title}</div> <div class="text-secondary">{person.job_title}</div>
<ButtonList class="justify-content-center mt-3"> <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="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="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> <a href="#" class="btn btn-icon btn-pill" title="Email" data-bs-toggle="tooltip" data-bs-placement="top"><Icon name="mail" /></a>
</ButtonList> </ButtonList>
</div> </div>
</div> </div>
+38 -39
View File
@@ -1,50 +1,49 @@
--- ---
// NOTE: the widgets page passes a `value=` argument, but the Liquid source only // NOTE: widgets page passes `value=` but only `percentage` (default 20) drives the bar — `value` is accepted but ignored.
// reads `percentage` (default 20) for the progress bar — `value` is never used. import Progress from '@ui/Progress.astro';
// It is accepted here for signature parity but intentionally ignored. import AvatarList from '@ui/AvatarList.astro';
import Progress from '@ui/Progress.astro' import Icon from '@ui/Icon.astro';
import AvatarList from '@ui/AvatarList.astro' import CardTitle from '@ui/CardTitle.astro';
import Icon from '@ui/Icon.astro'
import CardTitle from '@ui/CardTitle.astro'
interface Props { interface Props {
title?: string title?: string;
badge?: string badge?: string;
offset?: number offset?: number;
limit?: number limit?: number;
percentage?: number | string percentage?: number | string;
percentageColor?: string percentageColor?: string;
due?: string due?: string;
/** unused — see note above */ /** unused — see note above */
value?: number | string 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"> <div class="card">
<Progress value={percentage} class="card-progress" color={percentageColor} /> <Progress value={percentage} class="card-progress" color={percentageColor} />
<div class="card-body"> <div class="card-body">
<CardTitle> <CardTitle>
<a href="#">{title}</a> <a href="#">{title}</a>
{ {badge && <Fragment> <span class="badge ms-2">{badge}</span></Fragment>}
badge && ( </CardTitle>
<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="card-meta d-flex justify-content-between">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<Icon name="check" class="me-2" /> <Icon name="check" class="me-2" />
<span>5/10</span> <span>5/10</span>
</div> </div>
<span>Due {due}</span> <span>Due {due}</span>
</div> </div>
</div> </div>
</div> </div>
+40 -39
View File
@@ -1,52 +1,53 @@
--- ---
// NOTE: the `days-ago` param is assigned in the Liquid source but never rendered // NOTE: `days-ago` param accepted but unused ("Updated 2 hours ago" is hardcoded).
// ("Updated 2 hours ago" is hardcoded) — accepted here but unused. import Progress from '@ui/Progress.astro';
import Progress from '@ui/Progress.astro' import CardDropdown from '@ui/CardDropdown.astro';
import CardDropdown from '@ui/CardDropdown.astro' import projects from '@data/projects.json';
import projects from '@data/projects.json'
interface Project { interface Project {
title?: string title?: string;
image?: string image?: string;
[key: string]: unknown [key: string]: unknown;
} }
interface Props { interface Props {
projectId?: number projectId?: number;
progress?: number progress?: number;
daysAgo?: number daysAgo?: number;
} }
const { projectId = 0, progress = 25 } = Astro.props const { projectId = 0, progress = 25 } = Astro.props;
const project = (projects as Project[])[projectId] const project = (projects as Project[])[projectId];
--- ---
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col-3"> <div class="col-3">
<img src={project.image} alt={project.title} class="rounded" /> <img src={project.image} alt={project.title} class="rounded" />
</div> </div>
<div class="col"> <div class="col">
<h3 class="card-title mb-1"> <h3 class="card-title mb-1">
<a href="#" class="text-reset">{project.title}</a> <a href="#" class="text-reset">{project.title}</a>
</h3> </h3>
<div class="text-secondary">Updated 2 hours ago</div> <div class="text-secondary">
Updated 2 hours ago
</div>
<div class="mt-3"> <div class="mt-3">
<div class="row g-2 align-items-center"> <div class="row g-2 align-items-center">
<div class="col-auto"> <div class="col-auto">
{progress}% {progress}%
</div> </div>
<div class="col"> <div class="col">
<Progress value={progress} size="sm" /> <Progress value={progress} size="sm" />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<CardDropdown /> <CardDropdown />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
+33 -37
View File
@@ -1,43 +1,39 @@
--- ---
// NOTE: the Liquid source passes class="icon text-{{ item.icon_color }}" as an // Icon class is a literal template string (color token is not interpolated).
// include argument to ui/icon.html; Liquid does NOT re-interpolate include-arg import CardActions from '@ui/CardActions.astro';
// strings, so the reference output contains the LITERAL text import Icon from '@ui/Icon.astro';
// `icon text-{{ item.icon_color }}` in the icon class. We reproduce it verbatim. import CardTitle from '@ui/CardTitle.astro';
import crmDashboard from '@data/crm-dashboard.json';
import CardActions from '@ui/CardActions.astro' const activity = crmDashboard.recent_activity;
import Icon from '@ui/Icon.astro'
import CardTitle from '@ui/CardTitle.astro'
import crmDashboard from '@data/crm-dashboard.json'
const activity = crmDashboard.recent_activity
--- ---
<div class="card" style="height: 28rem"> <div class="card" style="height: 28rem">
<div class="card-header"> <div class="card-header">
<CardTitle>{activity.title}</CardTitle> <CardTitle>{activity.title}</CardTitle>
<CardActions> <CardActions>
<a href="#" class="small fw-medium text-primary">{activity.view_all_text}</a> <a href="#" class="small fw-medium text-primary">{activity.view_all_text}</a>
</CardActions> </CardActions>
</div> </div>
<div class="card-body card-body-scrollable card-body-scrollable-shadow"> <div class="card-body card-body-scrollable card-body-scrollable-shadow">
<div class="divide-y"> <div class="divide-y">
{ {
activity.items.map((item) => ( activity.items.map((item) => (
<div class="py-3"> <div class="py-3">
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col-auto"> <div class="col-auto">
<span class={`avatar bg-${item.icon_color}-lt`}> <span class={`avatar bg-${item.icon_color}-lt`}>
<Icon name={item.icon} class={'icon text-{{ item.icon_color }}'} /> <Icon name={item.icon} class={'icon text-{{ item.icon_color }}'} />
</span> </span>
</div> </div>
<div class="col"> <div class="col">
<div class="fw-medium">{item.text}</div> <div class="fw-medium">{item.text}</div>
<div class="small text-secondary">{item.time}</div> <div class="small text-secondary">{item.time}</div>
</div> </div>
</div> </div>
</div> </div>
)) ))
} }
</div> </div>
</div> </div>
</div> </div>
-1
View File
@@ -1,5 +1,4 @@
--- ---
// Equivalent of cards/sign-up.html
import FormFooter from '@ui/FormFooter.astro' import FormFooter from '@ui/FormFooter.astro'
import InputGroup from '@ui/InputGroup.astro' import InputGroup from '@ui/InputGroup.astro'
+33 -33
View File
@@ -1,39 +1,39 @@
--- ---
// Port cards/social-traffic.html import { formatNumber } from '@shared/lib/string-format';
import { formatNumber } from '@shared/lib/string-format' import CardTitle from '@ui/CardTitle.astro';
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 services = 'Instagram:3550,Twitter:1798,Facebook:1245,TikTok:986,Pinterest:854,VK:650,Pinterest:420'
const [name, visitors] = service.split(':') .split(',')
return { name, visitors: Number(visitors) } .map((service) => {
}) const [name, visitors] = service.split(':');
return { name, visitors: Number(visitors) };
});
--- ---
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<CardTitle>Social Media Traffic</CardTitle> <CardTitle>Social Media Traffic</CardTitle>
</div> </div>
<table class="table card-table table-vcenter"> <table class="table card-table table-vcenter">
<thead> <thead>
<tr> <tr>
<th>Network</th> <th>Network</th>
<th colspan="2">Visitors</th> <th colspan="2">Visitors</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{ {services.map((service) => (
services.map((service) => ( <tr>
<tr> <td>{service.name}</td>
<td>{service.name}</td> <td>{formatNumber(service.visitors)}</td>
<td>{formatNumber(service.visitors)}</td> <td class="w-50">
<td class="w-50"> <div class="progress progress-xs">
<div class="progress progress-xs">
<div class="progress-bar bg-primary" style={`width: ${(service.visitors / 5000.0) * 100}%`} /> <div class="progress-bar bg-primary" style={`width: ${(service.visitors / 5000.0) * 100}%`}></div>
</div> </div>
</td> </td>
</tr> </tr>
)) ))}
} </tbody>
</tbody> </table>
</table>
</div> </div>
+83 -94
View File
@@ -1,107 +1,96 @@
--- ---
import Icon from '@ui/Icon.astro' import Icon from '@ui/Icon.astro';
import DropdownMenu from '@ui/DropdownMenu.astro' import DropdownMenu from '@ui/DropdownMenu.astro';
interface Props { interface Props {
id: string id: string;
icons?: boolean icons?: boolean;
/** hide-text param; not exercised on tabs.html, ported 1:1 from the Liquid source */ /** hide-text param; not used by current call sites */
hideText?: boolean hideText?: boolean;
reverse?: boolean reverse?: boolean;
justified?: boolean justified?: boolean;
activity?: boolean activity?: boolean;
disabled?: boolean disabled?: boolean;
dropdown?: boolean dropdown?: boolean;
settings?: boolean settings?: boolean;
animation?: 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 iconClass = hideText ? undefined : 'me-2';
const navClass = `nav nav-tabs card-header-tabs${reverse ? ' flex-row-reverse' : ''}${justified ? ' nav-fill' : ''}` const navClass = `nav nav-tabs card-header-tabs${reverse ? ' flex-row-reverse' : ''}${justified ? ' nav-fill' : ''}`;
const paneClass = `tab-pane${animation ? ' fade' : ''}` const paneClass = `tab-pane${animation ? ' fade' : ''}`;
--- ---
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<ul class={navClass} data-bs-toggle="tabs"> <ul class={navClass} data-bs-toggle="tabs">
<li class="nav-item"> <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> <a href={`#tabs-home-${id}`} class="nav-link active" data-bs-toggle="tab">{icons && <Icon name="home" class={iconClass} />}{!hideText && 'Home'}</a>
</li> </li>
<li class="nav-item"> <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> <a href={`#tabs-profile-${id}`} class="nav-link" data-bs-toggle="tab">{icons && <Icon name="user" class={iconClass} />}{!hideText && 'Profile'}</a>
</li> </li>
{ {activity && (
activity && ( <li class="nav-item">
<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>
<a href={`#tabs-activity-${id}`} class="nav-link" data-bs-toggle="tab"> </li>
{icons && <Icon name="activity" class={iconClass} />} )}
{!hideText && 'Activity'}
</a>
</li>
)
}
{ {disabled && (
disabled && ( <li class="nav-item">
<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>
<a href={`#tabs-activity-${id}`} class="nav-link disabled" data-bs-toggle="tab"> </li>
{icons && <Icon name="x" class={iconClass} />} )}
{!hideText && 'Disabled'}
</a>
</li>
)
}
{ {dropdown && (
dropdown && ( <li class="nav-item 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>
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"> <DropdownMenu />
Dropdown </li>
</a> )}
<DropdownMenu /> {settings && (
</li> <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>
{ )}
settings && ( </ul>
<li class={`nav-item ${reverse ? 'me-auto' : 'ms-auto'}`}> </div>
<a href={`#tabs-settings-${id}`} class="nav-link" title="Settings" data-bs-toggle="tab"> <div class="card-body">
<Icon name="settings" /> <div class="tab-content">
</a> <div class={`${paneClass} active show`} id={`tabs-home-${id}`}>
</li> <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>
</ul> <div class={paneClass} id={`tabs-profile-${id}`}>
</div> <h4>Profile tab</h4>
<div class="card-body"> <div>Fringilla egestas nunc quis tellus diam rhoncus ultricies tristique enim at diam, sem nunc amet, pellentesque id egestas velit sed</div>
<div class="tab-content"> </div>
<div class={`${paneClass} active show`} id={`tabs-home-${id}`}> {settings && (
<h4>Home tab</h4> <div class={paneClass} id={`tabs-settings-${id}`}>
<div>Cursus turpis vestibulum, dui in pharetra vulputate id sed non turpis ultricies fringilla at sed facilisis lacus pellentesque purus nibh</div> <h4>Settings tab</h4>
</div> <div>Donec ac vitae diam amet vel leo egestas consequat rhoncus in luctus amet, facilisi sit mauris accumsan nibh habitant senectus</div>
<div class={paneClass} id={`tabs-profile-${id}`}> </div>
<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> {activity && (
</div> <div class={paneClass} id={`tabs-activity-${id}`}>
{ <h4>Activity tab</h4>
settings && ( <div>Donec ac vitae diam amet vel leo egestas consequat rhoncus in luctus amet, facilisi sit mauris accumsan nibh habitant senectus</div>
<div class={paneClass} id={`tabs-settings-${id}`}> </div>
<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>
</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> </div>
+25 -22
View File
@@ -1,39 +1,42 @@
--- ---
// person = people[person-id]; the cover photo is photos[person-id].file. // 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 2nd time
// (faithful to the Liquid source). // Base card-cover already carries `card-cover-blurred`; `blurred` adds it a second time.
import Avatar from '@ui/Avatar.astro' import Avatar from '@ui/Avatar.astro';
import people from '@data/people.json' import people from '@data/people.json';
import photos from '@data/photos.json' import photos from '@data/photos.json';
interface Person { interface Person {
full_name?: string full_name?: string;
job_title?: string job_title?: string;
photo?: string photo?: string;
[key: string]: unknown [key: string]: unknown;
} }
interface Photo { interface Photo {
file?: string file?: string;
[key: string]: unknown [key: string]: unknown;
} }
interface Props { interface Props {
personId?: number personId?: number;
blurred?: boolean blurred?: boolean;
} }
const { personId = 0, blurred } = Astro.props const { personId = 0, blurred } = Astro.props;
const person = (people as Person[])[personId] const person = (people as Person[])[personId];
const photo = (photos as Photo[])[personId] const photo = (photos as Photo[])[personId];
--- ---
<a class="card card-link" href="#"> <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})`}> <div
<Avatar size="xl" person={person} thumb /> class={`card-cover card-cover-blurred text-center${blurred ? ' card-cover-blurred' : ''}`}
</div> style={`background-image: url(./static/photos/${photo.file})`}
<div class="card-body text-center"> >
<div class="card-title mb-1">{person.full_name}</div> <Avatar size="xl" person={person} thumb />
<div class="text-secondary">{person.job_title}</div> </div>
</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> </a>
+17 -18
View File
@@ -1,30 +1,29 @@
--- ---
// person = people[person-id] (object passed to Avatar; `rounded` is a no-op in avatar.html). import Avatar from '@ui/Avatar.astro';
import Avatar from '@ui/Avatar.astro' import people from '@data/people.json';
import people from '@data/people.json'
interface Person { interface Person {
full_name?: string full_name?: string;
job_title?: string job_title?: string;
photo?: string photo?: string;
[key: string]: unknown [key: string]: unknown;
} }
interface Props { interface Props {
personId?: number personId?: number;
} }
const { personId = 0 } = Astro.props const { personId = 0 } = Astro.props;
const person = (people as Person[])[personId] const person = (people as Person[])[personId];
--- ---
<div class="card"> <div class="card">
<div class="card-body text-center"> <div class="card-body text-center">
<div class="mb-3"> <div class="mb-3">
<Avatar size="xl" person={person} /> <Avatar size="xl" person={person} />
</div> </div>
<div class="card-title mb-1">{person.full_name}</div> <div class="card-title mb-1">{person.full_name}</div>
<div class="text-secondary">{person.job_title}</div> <div class="text-secondary">{person.job_title}</div>
</div> </div>
<a href="#" class="card-btn">View full profile</a> <a href="#" class="card-btn">View full profile</a>
</div> </div>
+43 -43
View File
@@ -1,55 +1,55 @@
--- ---
// person = people[person-id - 1] (Liquid default person-id = 25 → people[24]). // person = people[person-id - 1] (default person-id = 25 → people[24]).
import Icon from '@ui/Icon.astro' import Icon from '@ui/Icon.astro';
import Flag from '@ui/Flag.astro' import Flag from '@ui/Flag.astro';
import people from '@data/people.json' import people from '@data/people.json';
interface Person { interface Person {
university?: string university?: string;
company?: string company?: string;
city?: string city?: string;
country?: string country?: string;
country_code?: string country_code?: string;
birth_date?: string birth_date?: string;
time_zone?: string time_zone?: string;
[key: string]: unknown [key: string]: unknown;
} }
interface Props { interface Props {
title?: string title?: string;
personId?: number personId?: number;
} }
const { title = 'Basic info', personId = 25 } = Astro.props const { title = 'Basic info', personId = 25 } = Astro.props;
const person = (people as Person[])[personId - 1] const person = (people as Person[])[personId - 1];
--- ---
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<div class="card-title">{title}</div> <div class="card-title">{title}</div>
<div class="mb-2"> <div class="mb-2">
<Icon name="book" class="me-2 text-secondary" /> <Icon name="book" class="me-2 text-secondary" />
Went to: <strong>{person.university}</strong> Went to: <strong>{person.university}</strong>
</div> </div>
<div class="mb-2"> <div class="mb-2">
<Icon name="briefcase" class="me-2 text-secondary" /> <Icon name="briefcase" class="me-2 text-secondary" />
Worked at: <strong>{person.company}</strong> Worked at: <strong>{person.company}</strong>
</div> </div>
<div class="mb-2"> <div class="mb-2">
<Icon name="home" class="me-2 text-secondary" /> <Icon name="home" class="me-2 text-secondary" />
Lives in: <strong>{person.city}, {person.country}</strong> Lives in: <strong>{person.city}, {person.country}</strong>
</div> </div>
<div class="mb-2"> <div class="mb-2">
<Icon name="map-pin" class="me-2 text-secondary" /> <Icon name="map-pin" class="me-2 text-secondary" />
From: <strong><Flag size="xs" flag={person.country_code} /> {person.country}</strong> From: <strong><Flag size="xs" flag={person.country_code} /> {person.country}</strong>
</div> </div>
<div class="mb-2"> <div class="mb-2">
<Icon name="calendar" class="me-2 text-secondary" /> <Icon name="calendar" class="me-2 text-secondary" />
Birth date: <strong>{person.birth_date}</strong> Birth date: <strong>{person.birth_date}</strong>
</div> </div>
<div> <div>
<Icon name="clock" class="me-2 text-secondary" /> <Icon name="clock" class="me-2 text-secondary" />
Time zone: <strong>{person.time_zone}</strong> Time zone: <strong>{person.time_zone}</strong>
</div> </div>
</div> </div>
</div> </div>
+87 -82
View File
@@ -1,98 +1,103 @@
--- ---
// Equivalent of cards/users-list.html import Avatar from '@ui/Avatar.astro';
import Avatar from '@ui/Avatar.astro' import Icon from '@ui/Icon.astro';
import Icon from '@ui/Icon.astro' import ListGroup from '@ui/ListGroup.astro';
import ListGroup from '@ui/ListGroup.astro' import ListGroupItem from '@ui/ListGroupItem.astro';
import ListGroupItem from '@ui/ListGroupItem.astro' import CardTitle from '@ui/CardTitle.astro';
import CardTitle from '@ui/CardTitle.astro' import people from '@data/people.json';
import people from '@data/people.json' import commits from '@data/commits.json';
import commits from '@data/commits.json' import { randomNumber } from '@shared/lib/pseudo-random';
import { randomNumber } from '@shared/lib/pseudo-random'
interface Person { interface Person {
full_name?: string full_name?: string;
photo?: string photo?: string;
last_name?: string last_name?: string;
[key: string]: unknown [key: string]: unknown;
} }
interface Props { interface Props {
limit?: number limit?: number;
offset?: number offset?: number;
hoverable?: boolean hoverable?: boolean;
checkbox?: boolean checkbox?: boolean;
/** checked-ids — 1-based indices, e.g. [2, 5, 8] */ /** checked-ids — 1-based indices, e.g. [2, 5, 8] */
checkedIds?: number[] checkedIds?: number[];
title?: string title?: string;
class?: string class?: string;
/** /**
* Inert. The lists.html Contacts card passes `hover=true`, but * Inert. The lists.html Contacts card passes `hover=true`, but
* users-list.html never reads it — only `hoverable` toggles the star column. * users-list.html never reads it — only `hoverable` toggles the star column.
* Declared so the call site can mirror the Liquid source faithfully. * Declared for call-site compatibility.
*/ */
hover?: boolean 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 colors = ['green', 'red', 'yellow', 'x', 'x'];
const commitList = commits as { description: string }[] const commitList = commits as { description: string }[];
const rows = (people as Person[]).slice(offset, offset + limit).map((person, idx) => { const rows = (people as Person[]).slice(offset, offset + limit).map((person, idx) => {
const index = idx + 1 // forloop.index (1-based) const index = idx + 1; // forloop.index (1-based)
const color = colors[randomNumber(index + 5, 0, colors.length - 1)] const color = colors[randomNumber(index + 5, 0, colors.length - 1)];
const checked = checkedIds?.includes(index) ?? false const checked = checkedIds?.includes(index) ?? false;
const i = index + offset const i = index + offset;
return { return {
person, person,
color, color,
checked, checked,
description: commitList[i]?.description, description: commitList[i]?.description,
starColor: checked ? 'text-yellow' : 'text-secondary', starColor: checked ? 'text-yellow' : 'text-secondary',
} };
}) });
--- ---
<div class={`card${className ? ` ${className}` : ''}`}> <div class={`card${className ? ` ${className}` : ''}`}>
<div class="card-header"> <div class="card-header">
<CardTitle>{title}</CardTitle> <CardTitle>{title}</CardTitle>
</div> </div>
<ListGroup flush hoverable={hoverable}> <ListGroup flush hoverable={hoverable}>
{ {
rows.map((row) => ( rows.map((row) => (
<ListGroupItem active={row.checked}> <ListGroupItem active={row.checked}>
<div class="row align-items-center"> <div class="row align-items-center">
{checkbox ? ( {checkbox ? (
<div class="col-auto"> <div class="col-auto">
<input type="checkbox" class="form-check-input" checked={row.checked} /> <input type="checkbox" class="form-check-input" checked={row.checked} />
</div> </div>
) : ( ) : (
<div class="col-auto"> <div class="col-auto">
<span class={`badge${row.color !== 'x' ? ` bg-${row.color}` : ''}`} /> <span class={`badge${row.color !== 'x' ? ` bg-${row.color}` : ''}`} />
</div> </div>
)} )}
<div class="col-auto"> <div class="col-auto">
<a href="#"> <a href="#">
<Avatar person={row.person} /> <Avatar person={row.person} />
</a> </a>
</div> </div>
<div class="col text-truncate"> <div class="col text-truncate">
<a href="#" class="text-reset d-block"> <a href="#" class="text-reset d-block">{row.person.full_name}</a>
{row.person.full_name} <div class="d-block text-secondary text-truncate mt-n1">{row.description}</div>
</a> </div>
<div class="d-block text-secondary text-truncate mt-n1">{row.description}</div> {hoverable && (
</div> <div class="col-auto">
{hoverable && ( <a href="#" class={`list-group-item-actions${row.checked ? ' show' : ''}`}>
<div class="col-auto"> <Icon name="star" class={row.starColor} />
<a href="#" class={`list-group-item-actions${row.checked ? ' show' : ''}`}> </a>
<Icon name="star" class={row.starColor} /> </div>
</a> )}
</div> </div>
)} </ListGroupItem>
</div> ))
</ListGroupItem> }
)) </ListGroup>
}
</ListGroup>
</div> </div>
+42 -45
View File
@@ -1,60 +1,57 @@
--- ---
// Equivalent of cards/users-list-2.html import Avatar from '@ui/Avatar.astro';
import Avatar from '@ui/Avatar.astro' import CardTitle from '@ui/CardTitle.astro';
import CardTitle from '@ui/CardTitle.astro' import people from '@data/people.json';
import people from '@data/people.json' import { randomNumber, timeagoLabel } from '@shared/lib/pseudo-random';
import { randomNumber, timeagoLabel } from '@shared/lib/pseudo-random'
interface Person { interface Person {
full_name?: string full_name?: string;
photo?: string photo?: string;
[key: string]: unknown [key: string]: unknown;
} }
interface Props { interface Props {
limit?: number limit?: number;
offset?: number offset?: number;
title?: string title?: string;
class?: 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 rows = (people as Person[]).slice(offset, offset + limit).map((person, idx) => {
const index = idx + 1 // forloop.index (1-based) const index = idx + 1; // forloop.index (1-based)
return { return {
person, person,
status: colors[randomNumber(index + 5, 0, colors.length - 1)], status: colors[randomNumber(index + 5, 0, colors.length - 1)],
timeago: timeagoLabel(index, 6), timeago: timeagoLabel(index, 6),
} };
}) });
--- ---
<div class={`card${className ? ` ${className}` : ''}`}> <div class={`card${className ? ` ${className}` : ''}`}>
<div class="card-header"> <div class="card-header">
<CardTitle>{title}</CardTitle> <CardTitle>{title}</CardTitle>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row g-3"> <div class="row g-3">
{ {
rows.map((row) => ( rows.map((row) => (
<div class="col-6"> <div class="col-6">
<div class="row g-3 align-items-center"> <div class="row g-3 align-items-center">
<a href="#" class="col-auto"> <a href="#" class="col-auto">
<Avatar person={row.person} status={row.status} /> <Avatar person={row.person} status={row.status} />
</a> </a>
<div class="col text-truncate"> <div class="col text-truncate">
<a href="#" class="text-reset d-block text-truncate"> <a href="#" class="text-reset d-block text-truncate">{row.person.full_name}</a>
{row.person.full_name} <div class="text-secondary text-truncate mt-n1">{row.timeago}</div>
</a> </div>
<div class="text-secondary text-truncate mt-n1">{row.timeago}</div> </div>
</div> </div>
</div> ))
</div> }
)) </div>
} </div>
</div>
</div>
</div> </div>
+52 -55
View File
@@ -1,73 +1,70 @@
--- ---
// Equivalent of cards/users-list-headers.html import Avatar from '@ui/Avatar.astro';
import Avatar from '@ui/Avatar.astro' import ListGroup from '@ui/ListGroup.astro';
import ListGroup from '@ui/ListGroup.astro' import ListGroupItem from '@ui/ListGroupItem.astro';
import ListGroupItem from '@ui/ListGroupItem.astro' import ListGroupHeader from '@ui/ListGroupHeader.astro';
import ListGroupHeader from '@ui/ListGroupHeader.astro' import CardTitle from '@ui/CardTitle.astro';
import CardTitle from '@ui/CardTitle.astro' import people from '@data/people.json';
import people from '@data/people.json' import commits from '@data/commits.json';
import commits from '@data/commits.json' import { sortBy } from '@shared/lib/string-format';
import { sortBy } from '@shared/lib/string-format'
interface Person { interface Person {
full_name?: string full_name?: string;
photo?: string photo?: string;
last_name?: string last_name?: string;
[key: string]: unknown [key: string]: unknown;
} }
interface Props { 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 }[];
// Equivalent of `people | sort: 'last_name'` (LiquidJS case-sensitive sort). // 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. // offset is unset in the include, so `forloop.index | plus: offset` == forloop.index.
let prevLetter = '' let prevLetter = '';
const rows = sorted.map((person, idx) => { const rows = sorted.map((person, idx) => {
const index = idx + 1 // forloop.index (1-based) const index = idx + 1; // forloop.index (1-based)
const firstLetter = (person.last_name ?? '').slice(0, 1) const firstLetter = (person.last_name ?? '').slice(0, 1);
let header: string | null = null let header: string | null = null;
if (prevLetter !== firstLetter) { if (prevLetter !== firstLetter) {
prevLetter = firstLetter prevLetter = firstLetter;
header = firstLetter header = firstLetter;
} }
return { person, header, description: commitList[index]?.description } return { person, header, description: commitList[index]?.description };
}) });
--- ---
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<CardTitle>{title}</CardTitle> <CardTitle>{title}</CardTitle>
</div> </div>
<ListGroup flush class="overflow-auto" style="max-height: 35rem"> <ListGroup flush class="overflow-auto" style="max-height: 35rem">
{ {
rows.map((row) => ( rows.map((row) => (
<> <>
{row.header && <ListGroupHeader class="sticky-top">{row.header}</ListGroupHeader>} {row.header && <ListGroupHeader class="sticky-top">{row.header}</ListGroupHeader>}
<ListGroupItem> <ListGroupItem>
<div class="row"> <div class="row">
<div class="col-auto"> <div class="col-auto">
<a href="#"> <a href="#">
<Avatar person={row.person} /> <Avatar person={row.person} />
</a> </a>
</div> </div>
<div class="col text-truncate"> <div class="col text-truncate">
<a href="#" class="text-body d-block"> <a href="#" class="text-body d-block">{row.person.full_name}</a>
{row.person.full_name} <div class="text-secondary text-truncate mt-n1">{row.description}</div>
</a> </div>
<div class="text-secondary text-truncate mt-n1">{row.description}</div> </div>
</div> </ListGroupItem>
</div> </>
</ListGroupItem> ))
</> }
)) </ListGroup>
}
</ListGroup>
</div> </div>
@@ -1,20 +1,18 @@
--- ---
// Note: the Liquid include passes legend=true to ui/chart.html, but ui/chart.html // legend=true at call site is inert — Chart reads legend from chart data, not props.
// reads the legend flag from the chart data (data.legend), not the include arg — import CardTitle from '@ui/CardTitle.astro';
// so the arg is inert and Chart.astro needs no legend prop. import Chart from '@ui/Chart.astro';
import CardTitle from '@ui/CardTitle.astro' import DropdownDays from '@ui/DropdownDays.astro';
import Chart from '@ui/Chart.astro'
import DropdownDays from '@ui/DropdownDays.astro'
--- ---
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<div class="d-flex"> <div class="d-flex">
<CardTitle>Social referrals</CardTitle> <CardTitle>Social referrals</CardTitle>
<div class="ms-auto"> <div class="ms-auto">
<DropdownDays id="social-referrals" label="Select time range for sales data" /> <DropdownDays id="social-referrals" label="Select time range for sales data" />
</div> </div>
</div> </div>
<Chart chartId="social-referrals" /> <Chart chartId="social-referrals" />
</div> </div>
</div> </div>
+49 -51
View File
@@ -1,57 +1,55 @@
--- ---
// The nested ui/switch-icon.html include (icon="heart") is inlined below — // Switch-icon heart variant inlined: icon-b="heart", icon-b-class="icon-filled", icon-a-color="muted", icon-b-color="red".
// it has no Astro component yet and this is its only use so far. For import Icon from '@ui/Icon.astro';
// icon="heart" the Liquid include resolves to: icon-b="heart", import DropdownMenu from '@ui/DropdownMenu.astro';
// icon-b-class="icon-filled", icon-a-color="muted", icon-b-color="red". import ListGroup from '@ui/ListGroup.astro';
import Icon from '@ui/Icon.astro' import ListGroupItem from '@ui/ListGroupItem.astro';
import DropdownMenu from '@ui/DropdownMenu.astro' import tracks from '@data/tracks.json';
import ListGroup from '@ui/ListGroup.astro' import { millisecondsToMinutes } from '@shared/lib/string-format';
import ListGroupItem from '@ui/ListGroupItem.astro'
import tracks from '@data/tracks.json'
import { millisecondsToMinutes } from '@shared/lib/string-format'
// {% for track in tracks limit: 12 %} const items = tracks.slice(0, 12);
const items = tracks.slice(0, 12)
--- ---
<div class="card"> <div class="card">
<ListGroup class="card-list-group"> <ListGroup class="card-list-group">
{ {items.map((track, index) => (
items.map((track, index) => ( <ListGroupItem>
<ListGroupItem> <div class="row g-2 align-items-center">
<div class="row g-2 align-items-center"> <div class="col-auto fs-3">
<div class="col-auto fs-3">{index + 1}</div> {index + 1}
<div class="col-auto"> </div>
<img src={`./static/tracks/${track.album.images[1].path}`} class="rounded" alt={track.name} width="40" height="40" /> <div class="col-auto">
</div> <img src={`./static/tracks/${track.album.images[1].path}`} class="rounded" alt={track.name} width="40" height="40" />
<div class="col"> </div>
{track.name} <div class="col">
<div class="text-secondary">{track.artists.map((artist) => artist.name).join(', ')}</div> {track.name}
</div> <div class="text-secondary">
<div class="col-auto text-secondary">{millisecondsToMinutes(track.duration_ms)}</div> {track.artists.map((artist) => artist.name).join(', ')}
<div class="col-auto"> </div>
<a href="#" class="link-secondary"> </div>
<button class="switch-icon" data-bs-toggle="switch-icon"> <div class="col-auto text-secondary">
<span class="switch-icon-a text-muted"> {millisecondsToMinutes(track.duration_ms)}
<Icon name="heart" /> </div>
</span> <div class="col-auto">
<span class="switch-icon-b text-red"> <a href="#" class="link-secondary">
<Icon name="heart" class="icon-filled" /> <button class="switch-icon" data-bs-toggle="switch-icon">
</span> <span class="switch-icon-a text-muted">
</button> <Icon name="heart" />
</a> </span>
</div> <span class="switch-icon-b text-red">
<div class="col-auto lh-1"> <Icon name="heart" class="icon-filled" />
<div class="dropdown"> </span>
<a href="#" class="link-secondary" data-bs-toggle="dropdown"> </button>
<Icon name="dots" /> </a>
</a> </div>
<DropdownMenu right /> <div class="col-auto lh-1">
</div> <div class="dropdown">
</div> <a href="#" class="link-secondary" data-bs-toggle="dropdown"><Icon name="dots" /></a>
</div> <DropdownMenu right />
</ListGroupItem> </div>
)) </div>
} </div>
</ListGroup> </ListGroupItem>
))}
</ListGroup>
</div> </div>
@@ -1,5 +1,4 @@
--- ---
// Equivalent of cards/placeholder/card-1.html
--- ---
<div class="card placeholder-glow"> <div class="card placeholder-glow">
@@ -1,5 +1,4 @@
--- ---
// Equivalent of cards/placeholder/card-2.html
--- ---
<div class="card"> <div class="card">
@@ -1,5 +1,4 @@
--- ---
// Equivalent of cards/placeholder/card-3.html
--- ---
<div class="card"> <div class="card">
@@ -1,5 +1,4 @@
--- ---
// Equivalent of cards/placeholder/card-4.html
--- ---
<div class="card"> <div class="card">

Some files were not shown because too many files have changed in this diff Show More