@at-flux/astroflare
v2.0.0
Published
Reusable headless components, styles, and utilities for Astro + Tailwind v4 + Cloudflare projects
Maintainers
Readme
@at-flux/astroflare
Reusable headless components, styles, and utilities for Astro + Tailwind v4 + Cloudflare projects.
For type-safe DOM helpers, use the separate package @at-flux/dom.
Package entrypoints
| Subpath | Contents |
| ---------------------------------- | --------------------------------------------- |
| @at-flux/astroflare | Core — forms utilities (same as ./core) |
| @at-flux/astroflare/core | Forms; also exposes the forms namespace |
| @at-flux/astroflare/forms | Resend email + form HTML helpers |
| @at-flux/astroflare/components/* | Astro components (source) |
| @at-flux/astroflare/styles/* | CSS (source) |
Examples
// Flat imports from core
import { sendEmail } from "@at-flux/astroflare";
// Explicit subpaths
import { renderEmailTemplate } from "@at-flux/astroflare/forms";
// Namespaced (from core / root)
import { forms } from "@at-flux/astroflare/core";Contents
Components (Astro)
Modal.astro— Headless modal using native<dialog>and<app-modal>web component (classapplies to the panel)ModalTrigger.astro— Trigger that opens a modal by ID using<modal-trigger>web componentContactModalCta.astro— Opinionated contact button (solid pill or text link) wrapped inModalTriggerInstagramProfileLink.astro— Small Instagram icon +@handlelink with safe defaultsSection.astro— Page section with optionalnarrowandcontentOnly(inner width wrapper without outer padding)SectionHeader.astro— Medallion / title / subtitle / tagline block that opens a section or a page; every visible class is a propEmojiIcon.astro— One emoji in a round medallion, decorative unless given alabel, tinted from--af-emoji-accentThemeToggle.astro— Dark/light mode toggle using<theme-toggle>web componentIconButton.astro— Accessible icon-only control that renders<button>or<a>ClientRouterLoadingSpinner.astro— Loading spinner for Astro view transitionsSuspense.astro— One primitive for anything a page waits on: media that loads, a block gated on the viewport or on a modal opening, an HTML fragment fetched on demand, or a wait you resolve yourselfTooltip.astro— Lightweight hover/focus tooltip wrapper for compact metadata summariesListSummary.astro— Generic inline list truncation with+Noverflow and tooltip detailsTagSummary.astro— Generic deterministic tag pill list with+Ntooltip overflowMediaProtect.astro— Style-agnostic media wrapper that applies no-save classes with delegated drag/context-menu protectionFilterPills.astro— Tag-colored filter chips with analloption and active-state stylingPager.astro— Pagination UI primitive for both browser-only and link-driven query paginationCollectionQuery.astro— Unified collection filtering/pagination component (client mode by default; URL-driven server mode withuseServer)PagedGrid.astro— Client-paged grid with declared column steps and filler cells, so every page is the same height and no row is part-filledCollectionFooterControls.astro— Server-only row: optionalsummaryslot,Pager, and page-size<form>(same query contract asCollectionQueryserver mode)
Component props reference
Modal.astro:id,class,backdropClass,panelClass,closeButtonClass,contentClassModalTrigger.astro:modalId,classContactModalCta.astro:modalId,label,variant,classInstagramProfileLink.astro:handle,href,class,aria-labelSection.astro:id,class,narrow,contentOnlySectionHeader.astro:title,titleId,subtitle,tagline,emoji,emojiSize,emojiLabel,as,titleClass,subtitleClass,taglineClass,iconWrapClass,class— slotsicon,subtitle,taglineEmojiIcon.astro:emoji,size,label,class, passthrough attributesThemeToggle.astro:classIconButton.astro:label,href,class,id, passthrough attributesTooltip.astro:text,position,class,panelClassListSummary.astro:items,visibleCount,separator,class,emptyLabel,overflowClass,tooltipPosition,itemCaseTagSummary.astro:items,visibleCount,class,itemClass,overflowClass,itemCase,colorOverridesMediaProtect.astro:class,containerClass,drag,contextMenuFilterPills.astro:items,includeAll,allLabel,allHref,active,itemCase,colorOverrides,classPager.astro:pageCount,activePage,items,classSuspense.astro:when,ready,src,rootMargin,minDisplay,graceDelay,timeout,aspectRatio,minHeight,duration,skeleton,accent,background,frame,rounded,loadingLabel,id,class— slotsplaceholderanderrorCollectionQuery.astro:useServer,pathname,query,totalPages,currentPage,filters,maxPageButtons,filtersClass,pagerClass,perPage,classPagedGrid.astro:columns,rows,perPage,pad,placeholderAspect,gap,gridClass,pagerClass,maxPageButtons,class- when
useServeristrue,pathname,query,totalPages, andcurrentPageare required
- when
CollectionFooterControls.astro:pathname,query,totalPages,currentPage,sizeOptions,maxPageButtons,class— slotsummaryfor “Showing X–Y of Z” text
Server Islands Pattern
CollectionQuery.astro supports two modes:
- client mode (default): static cards are filtered/paged in the browser
- server mode (
useServer): renders querystring links for filters + pager
For server mode, mount with server:defer at the page callsite:
<CollectionQuery
useServer
pathname={Astro.url.pathname}
query={activeQuery}
totalPages={pageData.totalPages}
currentPage={currentPage}
filters={filters}
server:defer
/>The package styleguide uses the Node adapter, so the server-island pattern can be exercised there with server:defer.
Slot customization (headless overrides)
Use named slots to replace the default filter/pager rendering:
<CollectionQuery useServer {...props}>
<div slot="filters">
<!-- your custom filter UI -->
</div>
<!-- default slot: your collection items -->
<div>...</div>
<div slot="pager">
<!-- your custom pager UI -->
</div>
</CollectionQuery>Paged grid
A gallery paged in the browser, where the grid geometry is declared rather than derived:
<PagedGrid columns={[2, 4]} rows={2}>
{tiles.map((tile) => (
<a data-card href={tile.href}>
<img src={tile.src} alt={tile.alt} loading="lazy" />
</a>
))}
</PagedGrid>- Column steps, not
auto-fill.[2, 4]means two columns below48remand four above it, and never three, so no tile is left alone on a row. Each step must divide the page size or the build fails with the arithmetic. - Every page the same height. A last page holding three of eight tiles is
topped up with outlined filler cells, so paging back and forth never moves the
content below the grid.
pad={false}turns that off. - Later pages cost nothing. Hidden pages are
display: none, and browsers do not fetch aloading="lazy"image inside one, so mark gallery images that way. - Paging is progressive enhancement. Without JavaScript every tile renders and the fillers stay hidden.
Waiting states
Suspense.astro covers every case: wrap an <img> and it holds the frame until
the image settles, add when="visible" and it gates on the viewport, add src
and it fetches an HTML fragment on demand. The placeholder waits graceDelay ms
before it paints, so anything already cached swaps in without a flash, and stays
minDisplay ms once painted, so a resource that is slow by a hair cannot strobe.
Slotted content is faded rather than removed, so it is still there without
JavaScript.
<Suspense aspectRatio="16/9" rounded="rounded-2xl">
<Image src={hero} alt="…" />
</Suspense>
<Suspense src="/fragments/policy/" when="dialog-open" minHeight="12rem">
<p slot="error">That didn't load. Reload the page to try again.</p>
</Suspense>Removed: ImageSuspense.astro, its ImageFade alias, and
LazyContent.astro were presets over Suspense exposing a subset of its props.
Rewrite call sites this way:
| Was | Write instead |
| ---------------------- | ------------------------------------------------------- |
| <ImageSuspense …> | <Suspense frame …>, spinnerColor → accent |
| <LazyContent src=… > | <Suspense src=… when="dialog-open" minHeight="12rem"> |
| when="immediate" | when="eager" |
| loading slot | placeholder slot |
Styles (CSS)
styles/prose.css— Markdown prose styling using CSS custom propertiesstyles/no-save.css— Image protection utilities (prevent right-click, drag, select)styles/accessibility.css— Focus styles, reduced motion, selection stylingstyles/scrollbar.css— Branded scrollbar styling
Utilities
now()/nowMs()/setClock(source)/resetClock()/withClock(source, fn)— one source of "now" for the package, so a test can pin the instant and a styleguide can preview a date-dependent component out of season. Used by the email footer stamp and the submit-time timestamp fieldsgetTagPalette(tag, options?)— Deterministic, readable tag color assignment with optional explicit overridesformatDisplayDate(date, config?)— Consistent card/detail date formatting with locale override supportparseCollectionQuery+paginateCollection+buildCollectionHref+buildPageSequence+matchesCollectionFilters+formatCollectionRangeLabel+resolveIslandSearchString— URL-driven filtering and pagination (filtersas stringified JSON).resolveIslandSearchStringis for server islands (pass the page’s search from the page; fall back toReferer).formatCollectionRangeLabelis for “Showing result N of T” / “Showing results a–b of T” footers.astroflare-link-localis shipped in this package; run it from any project that depends on@at-flux/astroflare(it walks up to the nearestpackage.jsonthat lists the dep).statusshows whether a local overlay is active. See Local checkout below.
Usage
Local checkout without changing package.json or lockfile
Keep @at-flux/astroflare on a normal semver range in package.json and run pnpm install so the lockfile records the registry version. Then overlay the install with a symlink (only under node_modules):
pnpm exec astroflare-link-local link /absolute/or/relative/path/to/astroflare/packages/astroflare
# or
ASTROFLARE_LOCAL_PATH=../../ts-libs/astroflare/packages/astroflare pnpm exec astroflare-link-localUSE_LOCAL_ASTROFLARE=1(ortrue/yes) is supported only together withASTROFLARE_LOCAL_PATHorUSE_LOCAL_ASTROFLARE_PATH(path topackages/astroflare).unlinkremoves the overlay and runspnpm installagain sonode_modulesmatches the lockfile:
pnpm exec astroflare-link-local unlinkstatusshows whether an overlay is active.
Re-running pnpm install may replace the symlink with the store copy; run link again if that happens.
Local Import (file: protocol) — alternative
{
"dependencies": {
"@at-flux/astroflare": "file:../../ts-libs/astroflare/packages/astroflare"
}
}Components
---
import Modal from '@at-flux/astroflare/components/Modal.astro';
import ModalTrigger from '@at-flux/astroflare/components/ModalTrigger.astro';
---Styles
@import "@at-flux/astroflare/styles/prose.css";
@import "@at-flux/astroflare/styles/no-save.css";Testing
From repo root:
pnpm install
pnpm --filter @at-flux/astroflare testOr in packages/astroflare:
pnpm testStyleguide (dev-only)
Use the package-local styleguide to preview all astroflare components in one place.
pnpm styleguide:dev