@atelic-action/ui 0.17.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +428 -44
- package/bin/atelic-check-base-path.ts +5 -0
- package/bin/atelic-clean-sitemap.ts +5 -0
- package/bin/atelic-contact-canary.ts +5 -0
- package/bin/atelic-print-sheets.sh +1 -1
- package/bin/atelic-seal-document.mjs +1 -1
- package/bin/atelic-serve-static.ts +1 -1
- package/dist/gate/index.d.ts +1 -1
- package/dist/gate/index.js +1 -1
- package/dist/gate/middleware.d.ts +4 -1
- package/dist/gate/middleware.js +14 -5
- package/dist/gate/wall.d.ts +25 -0
- package/dist/gate/wall.js +41 -2
- package/dist/routing/basePath.d.ts +24 -0
- package/dist/routing/basePath.js +54 -0
- package/dist/vite/index.d.ts +3 -2
- package/dist/vite/index.js +3 -2
- package/dist/vite/marketing-config.d.ts +50 -0
- package/dist/vite/marketing-config.js +83 -0
- package/package.json +164 -5
- package/src/app/AppProvider.tsx +41 -0
- package/src/app/AuthGuard.tsx +27 -0
- package/src/app/CardErrorBoundary.tsx +39 -0
- package/src/app/ErrorFallback.tsx +39 -0
- package/src/app/GoogleGlyph.tsx +22 -0
- package/src/app/config.ts +54 -0
- package/src/app/configure.ts +18 -0
- package/src/app/context/AppContext.tsx +52 -0
- package/src/app/context/SidebarContext.tsx +124 -0
- package/src/app/context/UserContext.tsx +139 -0
- package/src/app/hooks/useDebounce.ts +12 -0
- package/src/app/hooks/useIsDesktop.ts +31 -0
- package/src/app/hooks/useWakeLock.ts +49 -0
- package/src/app/index.ts +64 -0
- package/src/app/layout/ActionButton.tsx +67 -0
- package/src/app/layout/AppLayout.tsx +110 -0
- package/src/app/layout/AppPage.tsx +268 -0
- package/src/app/layout/Breadcrumbs.tsx +57 -0
- package/src/app/layout/DesktopRail.tsx +168 -0
- package/src/app/layout/FormActionButtons.tsx +69 -0
- package/src/app/layout/MobileMenu.tsx +156 -0
- package/src/app/layout/MobileTabBar.tsx +43 -0
- package/src/app/layout/PageError.tsx +30 -0
- package/src/app/layout/PageSkeleton.tsx +65 -0
- package/src/app/layout/SectionCards.tsx +33 -0
- package/src/app/layout/UserMenu.tsx +104 -0
- package/src/app/layout/identity.ts +22 -0
- package/src/app/layout/index.ts +20 -0
- package/src/app/layout/nav.ts +52 -0
- package/src/app/lib/dates.ts +57 -0
- package/src/app/lib/env.ts +37 -0
- package/src/app/lib/format.ts +61 -0
- package/src/app/lib/http.ts +205 -0
- package/src/app/lib/lastAuth.ts +45 -0
- package/src/app/lib/oauth.ts +32 -0
- package/src/app/lib/publicRoutes.ts +33 -0
- package/src/app/lib/queryClient.ts +38 -0
- package/src/app/lib/search.ts +17 -0
- package/src/app/lib/session.ts +36 -0
- package/src/app/lib/tokenRefresh.ts +137 -0
- package/src/app/lib/transformKeys.ts +60 -0
- package/src/app/lib/units.ts +228 -0
- package/src/app/lib/unwrap.ts +48 -0
- package/src/app/lib/utils.ts +10 -0
- package/src/app/stores/authStore.ts +80 -0
- package/src/app/stores/storage.ts +40 -0
- package/src/app/stores/unitsStore.ts +54 -0
- package/src/app/types/index.ts +72 -0
- package/src/app/types/query.ts +11 -0
- package/src/app/ui/accordion.tsx +54 -0
- package/src/app/ui/alert-dialog.tsx +114 -0
- package/src/app/ui/alert.tsx +49 -0
- package/src/app/ui/avatar.tsx +45 -0
- package/src/app/ui/badge-list.tsx +51 -0
- package/src/app/ui/badge.tsx +33 -0
- package/src/app/ui/button.tsx +49 -0
- package/src/app/ui/callout.tsx +22 -0
- package/src/app/ui/card.tsx +55 -0
- package/src/app/ui/checkbox.tsx +26 -0
- package/src/app/ui/clear-button.tsx +42 -0
- package/src/app/ui/command.tsx +140 -0
- package/src/app/ui/country-dropdown.tsx +184 -0
- package/src/app/ui/dialog.tsx +104 -0
- package/src/app/ui/dropdown-menu.tsx +185 -0
- package/src/app/ui/form.tsx +172 -0
- package/src/app/ui/index.ts +36 -0
- package/src/app/ui/input.tsx +22 -0
- package/src/app/ui/label.tsx +19 -0
- package/src/app/ui/option-select.tsx +56 -0
- package/src/app/ui/pagination.tsx +90 -0
- package/src/app/ui/popover.tsx +29 -0
- package/src/app/ui/progress.tsx +25 -0
- package/src/app/ui/search-input.tsx +63 -0
- package/src/app/ui/select.tsx +151 -0
- package/src/app/ui/separator.tsx +24 -0
- package/src/app/ui/sheet.tsx +119 -0
- package/src/app/ui/slider.tsx +23 -0
- package/src/app/ui/switch.tsx +27 -0
- package/src/app/ui/textarea.tsx +21 -0
- package/src/app/ui/tooltip.tsx +32 -0
- package/src/artifact/ArtifactShell.tsx +2 -2
- package/src/artifact/Gate.tsx +2 -2
- package/src/artifact/Writeup.tsx +7 -7
- package/src/chrome/SiteShell.tsx +141 -0
- package/src/chrome/index.ts +2 -0
- package/src/components/Disclosure.tsx +80 -14
- package/src/components/GateLogin.tsx +1 -1
- package/src/components/Watermark.tsx +1 -1
- package/src/components/index.ts +6 -2
- package/src/gate/index.ts +1 -1
- package/src/gate/middleware.ts +17 -6
- package/src/gate/wall.ts +60 -2
- package/src/lib/basePathAudit.ts +96 -0
- package/src/lib/displayDate.ts +25 -0
- package/src/lib/honeypot.ts +11 -0
- package/src/lib/hours.ts +28 -0
- package/src/lib/sitemap.ts +34 -0
- package/src/lib/trackEvent.ts +26 -0
- package/src/lib/useOpenHeight.ts +28 -0
- package/src/primitives/Button.tsx +3 -1
- package/src/primitives/Chip.tsx +3 -1
- package/src/primitives/ImageFrame.tsx +48 -0
- package/src/primitives/ProseBlocks.tsx +67 -0
- package/src/primitives/QuoteCard.tsx +20 -0
- package/src/primitives/StarRow.tsx +37 -0
- package/src/primitives/TickList.tsx +24 -0
- package/src/primitives/index.ts +5 -0
- package/src/routing/BasePathProvider.tsx +47 -0
- package/src/routing/basePath.ts +57 -0
- package/src/routing/index.ts +8 -0
- package/src/scripts/check-base-path.ts +130 -0
- package/src/scripts/clean-sitemap.ts +27 -0
- package/src/scripts/contact-canary.ts +98 -0
- package/src/scripts/serve-static.ts +29 -3
- package/src/sections/BlogIndex.tsx +57 -0
- package/src/sections/BlogPost.tsx +65 -0
- package/src/sections/CTABanner.tsx +93 -0
- package/src/sections/CTASection.tsx +60 -0
- package/src/sections/ComparisonTable.tsx +114 -0
- package/src/sections/ContactForm.tsx +163 -0
- package/src/sections/ContactInfoCard.tsx +92 -0
- package/src/sections/CredentialList.tsx +28 -0
- package/src/sections/DeviceShowcase.tsx +39 -0
- package/src/sections/ExpandList.tsx +58 -0
- package/src/sections/FAQBrowser.tsx +283 -0
- package/src/sections/FeatureDetail.tsx +60 -0
- package/src/sections/FeatureGrid.tsx +83 -0
- package/src/sections/Gallery.tsx +84 -0
- package/src/sections/Hero.tsx +110 -0
- package/src/sections/HoursTable.tsx +23 -0
- package/src/sections/JumpRail.tsx +35 -0
- package/src/sections/LegalPage.tsx +43 -0
- package/src/sections/LocationSection.tsx +66 -0
- package/src/sections/Locations.tsx +71 -0
- package/src/sections/LogoMarquee.tsx +27 -0
- package/src/sections/MapEmbed.tsx +75 -0
- package/src/sections/MediaBand.tsx +20 -0
- package/src/sections/MenuList.tsx +44 -0
- package/src/sections/PageHero.tsx +25 -0
- package/src/sections/PressQuotes.tsx +51 -0
- package/src/sections/PricingBand.tsx +43 -0
- package/src/sections/PricingCard.tsx +36 -0
- package/src/sections/ProcessSteps.tsx +45 -0
- package/src/sections/ProfileCards.tsx +73 -0
- package/src/sections/RevealSlider.tsx +156 -0
- package/src/sections/ReviewCarousel.tsx +223 -0
- package/src/sections/ReviewWall.tsx +220 -0
- package/src/sections/SocialStrip.tsx +83 -0
- package/src/sections/SplitSection.tsx +56 -0
- package/src/sections/index.ts +76 -0
- package/src/sections/siteConfig.ts +201 -0
- package/src/styles/app.css +673 -0
- package/src/styles/artifact.css +472 -399
- package/src/styles/base.css +49 -47
- package/src/styles/chrome.css +71 -71
- package/src/styles/components.css +150 -140
- package/src/styles/gate.css +29 -29
- package/src/styles/primitives.css +190 -25
- package/src/styles/sections.css +2652 -0
- package/src/styles/star-rating.css +2 -2
- package/src/vite/index.ts +9 -2
- package/src/vite/marketing-config.ts +128 -0
- package/src/components/Collapsible.tsx +0 -43
- /package/src/scripts/{build-gate-payload.mjs → seal-document.mjs} +0 -0
package/README.md
CHANGED
|
@@ -4,13 +4,117 @@ Shared UI for the Atelic templates: the site chrome (header, menu, footer, stick
|
|
|
4
4
|
|
|
5
5
|
The package ships source, not a build. Its TSX and CSS arrive as written and compile inside each site's own Vite. The one exception is the access gate's server side, which ships compiled (see [The Access Gate](#the-access-gate)).
|
|
6
6
|
|
|
7
|
+
## Component Index
|
|
8
|
+
|
|
9
|
+
Every export, grouped by the entry it is imported from. The gallery column links the section of [ui.atelic.me](https://ui.atelic.me) that shows it; none marks an export that draws nothing.
|
|
10
|
+
|
|
11
|
+
| Export | Entry | What it is | Gallery |
|
|
12
|
+
|---|---|---|---|
|
|
13
|
+
| `SiteHeader` | `@atelic-action/ui/chrome` | The fixed bar: the lockup, the nav or a page's own stops, the call to action, the burger; transparent, solid, or dark | [#site-header](https://ui.atelic.me/#site-header) |
|
|
14
|
+
| `SiteMenu` | `@atelic-action/ui/chrome` | The phone's full screen menu, on a native modal dialog | [#site-menu](https://ui.atelic.me/#site-menu) |
|
|
15
|
+
| `Footer` | `@atelic-action/ui/chrome` | The site's foot: brand column, nav, address, hours, copyright, credit band | [#footer](https://ui.atelic.me/#footer) |
|
|
16
|
+
| `CreditBar` | `@atelic-action/ui/chrome` | The developer credit band | [#credit-bar](https://ui.atelic.me/#credit-bar) |
|
|
17
|
+
| `StickyCTABar` | `@atelic-action/ui/chrome` | The phone's bottom bar: the call to action and a call shortcut | [#sticky-cta-bar](https://ui.atelic.me/#sticky-cta-bar) |
|
|
18
|
+
| `BrandLockup` | `@atelic-action/ui/chrome` | The header's identity as one link, with the recipient's mark on a sent document | [#brand-lockup](https://ui.atelic.me/#brand-lockup) |
|
|
19
|
+
| `SkipLink` | `@atelic-action/ui/chrome` | The keyboard user's jump past the chrome | [#skip-link](https://ui.atelic.me/#skip-link) |
|
|
20
|
+
| `Brand`, `BrandLogo`, `CallToAction`, `HoursRow`, `MenuContact`, `NavLink`, `Phone`, `PostalAddress`, `SiteLink`, `SocialLink`, `PageStop` | `@atelic-action/ui/chrome` | The shapes the chrome renders from (types) | none |
|
|
21
|
+
| `NotFound` | `@atelic-action/ui/components` | The body of the page an unknown path renders | [#not-found](https://ui.atelic.me/#not-found) |
|
|
22
|
+
| `Checklist` | `@atelic-action/ui/components` | A short list of checks, each passed or failed | [#checklist](https://ui.atelic.me/#checklist) |
|
|
23
|
+
| `DataTable` | `@atelic-action/ui/components` | A table whose first column names each row, sortable, stacking on a phone | [#data-table](https://ui.atelic.me/#data-table) |
|
|
24
|
+
| `StatRow` | `@atelic-action/ui/components` | A row of large figures with periods, deltas, and trends | [#stat-row](https://ui.atelic.me/#stat-row) |
|
|
25
|
+
| `Callout` | `@atelic-action/ui/components` | One finding: an icon in its tone, the title, the explainer | [#callout](https://ui.atelic.me/#callout) |
|
|
26
|
+
| `Disclosure` | `@atelic-action/ui/components` | A section that folds: controlled, an app's card; uncontrolled, a native details fold that prints open | [#disclosure](https://ui.atelic.me/#disclosure) |
|
|
27
|
+
| `StackedBar` | `@atelic-action/ui/components` | One bar split into the parts of a whole, with a key | [#stacked-bar](https://ui.atelic.me/#stacked-bar) |
|
|
28
|
+
| `ColumnChart` | `@atelic-action/ui/components` | A bar a period, the periods in labeled groups | [#column-chart](https://ui.atelic.me/#column-chart) |
|
|
29
|
+
| `ThresholdScale` | `@atelic-action/ui/components` | Zones on a scale and a pin for each reading | [#threshold-scale](https://ui.atelic.me/#threshold-scale) |
|
|
30
|
+
| `StarRating`, `describeStarRating` | `@atelic-action/ui/components` | A row of stars in half steps, read only or editable, and its spoken name | [#star-rating](https://ui.atelic.me/#star-rating) |
|
|
31
|
+
| `GateLogin` | `@atelic-action/ui/components` | The access gate's sign in screen | [#gate-login](https://ui.atelic.me/#gate-login) |
|
|
32
|
+
| `Watermark` | `@atelic-action/ui/components` | The signed in reader's confidential stamp, pill or tiled | [#watermark](https://ui.atelic.me/#watermark) |
|
|
33
|
+
| `readCookie`, `Tone` | `@atelic-action/ui/components` | The cookie reader the watermark uses, and the tone type | none |
|
|
34
|
+
| `Button`, `Chip`, `Eyebrow`, `Lead`, `SectionHeading` | `@atelic-action/ui/primitives` | The small pieces a page is set in | [#primitives](https://ui.atelic.me/#primitives) |
|
|
35
|
+
| `Writeup`, `ratingCell` | `@atelic-action/ui/artifact` | The graded document an owner gets, and its star rating table cell | [#writeup](https://ui.atelic.me/#writeup) |
|
|
36
|
+
| `Report` | `@atelic-action/ui/artifact` | The monthly page an Operate client gets | [#report](https://ui.atelic.me/#report) |
|
|
37
|
+
| `Note` | `@atelic-action/ui/artifact` | The first touch page, for a business with no published email | [#note](https://ui.atelic.me/#note) |
|
|
38
|
+
| `Gate`, `normalizeEmail` | `@atelic-action/ui/artifact` | The sealed document, opened in the browser by the reader's own address | [#gate](https://ui.atelic.me/#gate) |
|
|
39
|
+
| `GradeChip`, `gradeLabel`, `SignOff` | `@atelic-action/ui/artifact` | The grade letter and the sign off the documents share | [#writeup](https://ui.atelic.me/#writeup) |
|
|
40
|
+
| `ArtifactShell` | `@atelic-action/ui/artifact` | The chrome around a document page: skip link, dark header, credit band, watermark | [#site-header](https://ui.atelic.me/#site-header) (its dark bar) |
|
|
41
|
+
| `AppErrorBoundary`, `ErrorFallback` | `@atelic-action/ui/artifact` | The error boundary and the page it falls back to | none |
|
|
42
|
+
| `buildPageHead`, `canonicalUrl`, `newTabProps`, the `ArtifactConfig` types | `@atelic-action/ui/artifact` | A page's head, a link's new tab props, and a site's contract | none |
|
|
43
|
+
| `mintToken`, `sealReader`, `openReader`, `readerId`, `readerLink`, `whoIsReading` | `@atelic-action/ui/artifact` and `@atelic-action/ui/artifact/readers` | Who opened a document; the second entry is the same six, built, for what Node runs | none |
|
|
44
|
+
| `Masthead`, `TitleCard`, `Footer`, `FailurePage` | `@atelic-action/ui/email` | An email's frame: the wordmark row, the title card, the foot, the failure page | [#email](https://ui.atelic.me/#email) |
|
|
45
|
+
| `Card`, `Row`, `FoldRow`, `Eyebrow`, `SubEyebrow`, `Fold`, `BigFold` | `@atelic-action/ui/email` | The containers and labels an email's sections sit in | [#email](https://ui.atelic.me/#email) |
|
|
46
|
+
| `Item`, `TitleLine`, `Note`, `EmptyRow`, `Stat`, `StatsRow`, `List`, `ListRow`, `LeadRow`, `MonoTable`, `ReadBlock`, `Badge` | `@atelic-action/ui/email` | The rows inside a card | [#email](https://ui.atelic.me/#email) |
|
|
47
|
+
| `Scoreboard`, `GroupRow`, `TargetRow`, `Bar`, `WhatMoved`, `DayStrip`, `StatStrip` | `@atelic-action/ui/email` | Targets against what was logged, what moved, and the week by day | [#email](https://ui.atelic.me/#email) |
|
|
48
|
+
| `RecordStack`, `RecordTimelineStrip`, `RecordTimelineLegend`, `Records` | `@atelic-action/ui/email` | Records stacked for a phone, their timeline, and the fixed column table | [#email](https://ui.atelic.me/#email) |
|
|
49
|
+
| `EmailThemeProvider`, `useEmailTheme`, `eyebrowStyle`, the plain text helpers (`recordStackText`, `wrap`, `textTable`, and the rest) | `@atelic-action/ui/email` | The palette context and the plain text twin (see [Email](#email)) | [#email](https://ui.atelic.me/#email) (the text twin) |
|
|
50
|
+
| `renderEmail`, `renderFailureEmail` | `@atelic-action/ui/email/render` | The whole email document, and the failure page's | [#email](https://ui.atelic.me/#email) |
|
|
51
|
+
| `atelicPalette`, `atelicFonts`, `toThemeCSS`, `themeTokenMap`, `Palette`, `Fonts` | `@atelic-action/ui/tokens` | The palette and font stacks, with no React | none |
|
|
52
|
+
| `useScrollSpy` | `@atelic-action/ui/hooks` | Which section is in the reading band; the header's stops use it | [#site-header](https://ui.atelic.me/#site-header) (the dark bar's stops) |
|
|
53
|
+
| `staticNotFoundRouting` | `@atelic-action/ui/routing` | The router options that keep a prerendered not found page alive | none |
|
|
54
|
+
| `signToken`, `verifyToken`, `safeNext`, `needsSession` | `@atelic-action/ui/gate` | The access gate's tokens, the safe post sign in path, and the wall's test | none |
|
|
55
|
+
| `POST`, `createRequestLink` | `@atelic-action/ui/gate/request-link` | The function that emails a sign in link | none |
|
|
56
|
+
| `GET` | `@atelic-action/ui/gate/verify` | The function that turns a link into a session | none |
|
|
57
|
+
| default, `createMiddleware` | `@atelic-action/ui/gate/middleware` | The middleware that holds a host behind the gate | none |
|
|
58
|
+
| `artifactConfig`, `artifactPages` | `@atelic-action/ui/vite` | An artifact site's whole Vite configuration, and its page list | none |
|
|
59
|
+
| `privateChunks`, `isPrivateContent`, `DOC_DIR` | `@atelic-action/ui/vite` and `@atelic-action/ui/vite/private-chunks` | The plugin that keeps private content behind the wall; the second entry loads neither optional peer | none |
|
|
60
|
+
| `base.css`, `chrome.css`, `components.css`, `primitives.css`, `gate.css`, `artifact.css`, `star-rating.css` | `@atelic-action/ui/styles/*` | The stylesheets, in the order under [CSS Import Order](#css-import-order) | every section |
|
|
61
|
+
| `BlogIndex` | `@atelic-action/ui/sections` | The post listing as a readable stack | [#blog-index](https://ui.atelic.me/#blog-index) |
|
|
62
|
+
| `BlogPost` | `@atelic-action/ui/sections` | One article: back link, title, byline and dateline, body | [#blog-post](https://ui.atelic.me/#blog-post) |
|
|
63
|
+
| `CTABanner` | `@atelic-action/ui/sections` | A mid page banner behind a directional wash, with the token gradient before any photography | [#c-t-a-banner](https://ui.atelic.me/#c-t-a-banner) |
|
|
64
|
+
| `CTASection` | `@atelic-action/ui/sections` | The standard closer: the site's own primary action and an optional ghost second | [#c-t-a-section](https://ui.atelic.me/#c-t-a-section) |
|
|
65
|
+
| `ComparisonTable` | `@atelic-action/ui/sections` | A graded before and after that scrolls sideways on a phone rather than reflowing | [#comparison-table](https://ui.atelic.me/#comparison-table) |
|
|
66
|
+
| `ContactForm` | `@atelic-action/ui/sections` | Validated form with a honeypot that posts to `endpoint` (or hands off to mail); `onLead` replaces the default GA4 `generate_lead` event | [#contact-form](https://ui.atelic.me/#contact-form) |
|
|
67
|
+
| `ContactInfoCard` | `@atelic-action/ui/sections` | A dark card of direct contact rows: call, email, visit, portal | [#contact-info-card](https://ui.atelic.me/#contact-info-card) |
|
|
68
|
+
| `CredentialList` | `@atelic-action/ui/sections` | Credential cards with a mono code before each | [#credential-list](https://ui.atelic.me/#credential-list) |
|
|
69
|
+
| `DeviceShowcase` | `@atelic-action/ui/sections` | A desktop browser frame with a phone overlapping it | [#device-showcase](https://ui.atelic.me/#device-showcase) |
|
|
70
|
+
| `ExpandList` | `@atelic-action/ui/sections` | A light accordion of numbered rows whose bodies stay in the HTML while closed | [#expand-list](https://ui.atelic.me/#expand-list) |
|
|
71
|
+
| `FAQBrowser` | `@atelic-action/ui/sections` | The help page: live search, a sticky `JumpRail` of categories with scroll spy, expand all, deep links; `contactHref` sets where the no match line points | [#f-a-q-browser](https://ui.atelic.me/#f-a-q-browser) |
|
|
72
|
+
| `FeatureDetail` | `@atelic-action/ui/sections` | Alternating copy and media rows with tags and a tick list | [#feature-detail](https://ui.atelic.me/#feature-detail) |
|
|
73
|
+
| `FeatureGrid` | `@atelic-action/ui/sections` | Numbered cards with a hover lift; a card is a link or holds one, never both | [#feature-grid](https://ui.atelic.me/#feature-grid) |
|
|
74
|
+
| `Gallery` | `@atelic-action/ui/sections` | A captioned wall of the business's own work, a shot brief where a photo is missing | [#gallery](https://ui.atelic.me/#gallery) |
|
|
75
|
+
| `Hero` | `@atelic-action/ui/sections` | The home page opener: a full bleed image (or the token gradient), a headline with a serif accent, a pair of actions, trust figures, and a scroll cue | [#hero](https://ui.atelic.me/#hero) |
|
|
76
|
+
| `HoursTable` | `@atelic-action/ui/sections` | Day rows with dashed rules and mono times | [#hours-table](https://ui.atelic.me/#hours-table) |
|
|
77
|
+
| `JumpRail` | `@atelic-action/ui/sections` | Sticky in page pill tabs with scroll spy, used by `FAQBrowser` and on its own | [#jump-rail](https://ui.atelic.me/#jump-rail) |
|
|
78
|
+
| `LegalPage` | `@atelic-action/ui/sections` | A legal document from data, its last revised date first | [#legal-page](https://ui.atelic.me/#legal-page) |
|
|
79
|
+
| `LocationSection` | `@atelic-action/ui/sections` | The local search section: service areas as prose, hours, directions, and the map | [#location-section](https://ui.atelic.me/#location-section) |
|
|
80
|
+
| `Locations` | `@atelic-action/ui/sections` | More than one address: the flagship's map beside a card for each location | [#locations](https://ui.atelic.me/#locations) |
|
|
81
|
+
| `LogoMarquee` | `@atelic-action/ui/sections` | A dark band of chips scrolling without end: credentials, clients, or press | [#logo-marquee](https://ui.atelic.me/#logo-marquee) |
|
|
82
|
+
| `MapEmbed` | `@atelic-action/ui/sections` | The map: a screenshot linking to directions, or a click to load facade over the live embed | [#map-embed](https://ui.atelic.me/#map-embed) |
|
|
83
|
+
| `MediaBand` | `@atelic-action/ui/sections` | A plain full bleed image band, cropped from the center | [#media-band](https://ui.atelic.me/#media-band) |
|
|
84
|
+
| `MenuList` | `@atelic-action/ui/sections` | A priced menu group of dotted rows, with fine print under it | [#menu-list](https://ui.atelic.me/#menu-list) |
|
|
85
|
+
| `PageHero` | `@atelic-action/ui/sections` | The interior page opener on the alternate band; children sit under the lead | [#page-hero](https://ui.atelic.me/#page-hero) |
|
|
86
|
+
| `PressQuotes` | `@atelic-action/ui/sections` | Rows of press quotes and awards with source and year, newest first (`sortPress` beside it) | [#press-quotes](https://ui.atelic.me/#press-quotes) |
|
|
87
|
+
| `PricingBand` | `@atelic-action/ui/sections` | A brand colored band with the price in a card beside it, informational by design | [#pricing-band](https://ui.atelic.me/#pricing-band) |
|
|
88
|
+
| `PricingCard` | `@atelic-action/ui/sections` | A standalone price card with a tick list and its booking action | [#pricing-card](https://ui.atelic.me/#pricing-card) |
|
|
89
|
+
| `ProcessSteps` | `@atelic-action/ui/sections` | A dark band of numbered steps beside a frame | [#process-steps](https://ui.atelic.me/#process-steps) |
|
|
90
|
+
| `ProfileCards` | `@atelic-action/ui/sections` | Equal weight cards of media beside a short bio | [#profile-cards](https://ui.atelic.me/#profile-cards) |
|
|
91
|
+
| `RevealSlider` | `@atelic-action/ui/sections` | Drag or arrow keys to compare two frames, each with its own toned shot brief before photography | [#reveal-slider](https://ui.atelic.me/#reveal-slider) |
|
|
92
|
+
| `ReviewCarousel` | `@atelic-action/ui/sections` | The aggregate beside one large review at a time, stepped by dots and arrows | [#review-carousel](https://ui.atelic.me/#review-carousel) |
|
|
93
|
+
| `ReviewWall` | `@atelic-action/ui/sections` | The reviews page: tag filter and sort when the data carries them, clamped quotes behind Read more | [#review-wall](https://ui.atelic.me/#review-wall) |
|
|
94
|
+
| `SocialStrip` | `@atelic-action/ui/sections` | A static grid of the business's social photos linking to the profile, never a live embed | [#social-strip](https://ui.atelic.me/#social-strip) |
|
|
95
|
+
| `SplitSection` | `@atelic-action/ui/sections` | The two column band: copy beside copy, or media beside copy | [#split-section](https://ui.atelic.me/#split-section) |
|
|
96
|
+
| `SiteConfig` and its member types, `formatClock`, `formatHoursRange`, `isClosed`, `isHoneypotFilled`, `trackEvent` | `@atelic-action/ui/sections` | The site contract the sections read, and the hours, honeypot, and lead event helpers | none |
|
|
97
|
+
| `ImageFrame`, `ProseBlocks`, `QuoteCard`, `StarRow`, `TickList` | `@atelic-action/ui/primitives` | The marketing primitives: a framed image, prose blocks, a quote card, a row of filled stars, a tick list | [#marketing-primitives](https://ui.atelic.me/#marketing-primitives) |
|
|
98
|
+
| `SiteShell` | `@atelic-action/ui/chrome` | A marketing page's whole chrome: skip link, header, main, footer, sticky bar, mounting the base path provider | none |
|
|
99
|
+
| `BasePathProvider`, `useBasePath`, `useWithBase`, `joinBase`, `stripBase`, `hasBase`, `normalizeBasePath` | `@atelic-action/ui/routing` | The base path a site is served under, read by Button, Chip, and every section that builds a link | none |
|
|
100
|
+
| `joinBase`, `stripBase`, `hasBase`, `normalizeBasePath` | `@atelic-action/ui/routing/base-path` | The same four pure helpers, built, for what Node runs (a Playwright spec, a script) | none |
|
|
101
|
+
| `marketingConfig` | `@atelic-action/ui/vite` | A marketing site's whole Vite configuration as one call | none |
|
|
102
|
+
| `sections.css` | `@atelic-action/ui/styles/sections.css` | Styles for everything under `sections`, imported after `primitives.css` | none |
|
|
103
|
+
| `AppProvider`, `AppConfigProvider`, `useAppConfig`, `useNav`, `useUser`, `configureApp`, `getAppConfig`, `AppConfig`, `NavItem`, `AppUser`, `Register` | `@atelic-action/ui/app` | The app shell's seam: the app hands in its config and nav, registers its user type, and reads them back | [app](https://ui.atelic.me/app.html) |
|
|
104
|
+
| `AppLayout`, `AppPage`, `AppPageToolbar`, `Breadcrumbs`, `SectionCards`, `DesktopRail`, `MobileTabBar`, `MobileMenu`, `UserMenu`, `PageSkeleton`, `PageError`, `ActionButton`, `FormActionButtons`, `activeSection` | `@atelic-action/ui/app` | The app frame: the rails, the tab bar, the page scaffold with its toolbar and right rail | [app](https://ui.atelic.me/app.html) |
|
|
105
|
+
| `AuthGuard`, `CardErrorBoundary`, `ErrorFallback`, `GoogleGlyph`, `useAuthStore`, `useUnitsStore`, `apiFetch`, `publicFetch`, `session`, the hooks and the wire types | `@atelic-action/ui/app` | Auth, transport, the two shell stores, and the helpers the shell runs on | [app](https://ui.atelic.me/app.html) |
|
|
106
|
+
| The shadcn kit (`Button`, `Dialog`, `Select`, `OptionSelect`, `SearchInput`, `BadgeList`, `Callout`, and the rest) | `@atelic-action/ui/app/ui` | The kit the shell is set in; a new kit component lands here, never in an app | [app](https://ui.atelic.me/app.html) |
|
|
107
|
+
| `app.css` | `@atelic-action/ui/styles/app.css` | The shell's Tailwind sheet: the token mapping, the default tokens, the base layer, the `ap-*` scaffold, and the `@source` that points Tailwind at the shell | none |
|
|
108
|
+
| The eleven `atelic-*` commands | the `bin` map | The build guard, health stamp, link and wall checks, reader link, static server, print, sealing, and the marketing site's sitemap clean, base path check, and contact canary | none |
|
|
109
|
+
|
|
110
|
+
|
|
7
111
|
## Install
|
|
8
112
|
|
|
9
113
|
```bash
|
|
10
114
|
bun add @atelic-action/ui
|
|
11
115
|
```
|
|
12
116
|
|
|
13
|
-
|
|
117
|
+
From 1.0.0 a caret range admits every minor and patch, so a plain `bun update @atelic-action/ui` takes them; a new major still needs `--latest`. A site still on a `^0.x` range takes 1.0 with `--latest` once.
|
|
14
118
|
|
|
15
119
|
Peer dependencies: `react` and `react-dom` at `^19.0.0`, and `lucide-react` at `^1.23.0`. Three more are optional, needed only by a site whose `vite.config.ts` imports `@atelic-action/ui/vite`: `vite`, `@tanstack/react-start`, and `@vitejs/plugin-react`. The commands the package installs run under `bun`, with `node` for the sealing command.
|
|
16
120
|
|
|
@@ -39,42 +143,18 @@ Import the stylesheets in this order, from the root route or the site's base she
|
|
|
39
143
|
8. The site's own CSS
|
|
40
144
|
9. The site's `theme.css`, last
|
|
41
145
|
|
|
146
|
+
Every rule in the package sheets is scoped under `.atelic-ui`, the one root class a consumer wears: a site puts it on its outermost wrapper, an app on the region that holds a package component. Which sheets it imports decides what it gets, so a site importing `base.css` gets the resets and typography under that class and an app importing `components.css` alone gets none of them.
|
|
147
|
+
|
|
42
148
|
The package sheets sit inside `@layer atelic-ui`, so any rule a site writes outside a layer wins over them whatever its specificity. The package reads the theme tokens (`--ink`, `--surface`, `--primary`, `--nav-height`, and the rest) and defines none, so `theme.css` stays the one file a site edits to rebrand. The chrome's buttons wear the `.btn` classes, which `primitives.css` defines and a site may restyle.
|
|
43
149
|
|
|
44
|
-
##
|
|
45
|
-
|
|
46
|
-
| Import | Exports |
|
|
47
|
-
|---|---|
|
|
48
|
-
| `@atelic-action/ui/chrome` | `SiteHeader`, `SiteMenu`, `Footer`, `CreditBar`, `StickyCTABar`, `BrandLockup`, `SkipLink`, and their prop types |
|
|
49
|
-
| `@atelic-action/ui/components` | `NotFound`, the access gate's `GateLogin` and `Watermark` (with `readCookie`), and the page components (`Checklist`, `DataTable`, `StatRow`, `Callout`, `Collapsible`, `StackedBar`, `ColumnChart`, `ThresholdScale`, `StarRating`, `Disclosure`), with their prop types (see [The Not Found Page](#the-not-found-page) and [Page Components](#page-components)) |
|
|
50
|
-
| `@atelic-action/ui/artifact` | `Writeup`, `Report`, `Gate`, `GradeChip`, `SignOff`, `ArtifactShell`, the `ArtifactConfig` types, `buildPageHead`, the reader helpers, `AppErrorBoundary`, `newTabProps`, and their prop types (see [The Artifact Documents](#the-artifact-documents)) |
|
|
51
|
-
| `@atelic-action/ui/artifact/readers` | The reader helpers alone, built, for what Node runs as shipped: a site's browser tests |
|
|
52
|
-
| `@atelic-action/ui/primitives` | `Button`, `Chip`, `Eyebrow`, `Lead`, `SectionHeading`, and their prop types (see [Primitives](#primitives)) |
|
|
53
|
-
| `@atelic-action/ui/email` | The email components, the theme provider, the plain text helpers, and their prop types (see [Email](#email)) |
|
|
54
|
-
| `@atelic-action/ui/email/render` | `renderEmail` and `renderFailureEmail`, the only entry that imports `react-dom/server` |
|
|
55
|
-
| `@atelic-action/ui/tokens` | `Palette`, `atelicPalette`, `Fonts`, `atelicFonts`, `toThemeCSS`, `themeTokenMap` |
|
|
56
|
-
| `@atelic-action/ui/hooks` | `useScrollSpy` and its `PageStop` type |
|
|
57
|
-
| `@atelic-action/ui/styles/primitives.css` | Styles for everything under `primitives`, the `.btn` classes included |
|
|
58
|
-
| `@atelic-action/ui/styles/gate.css` | Styles for `GateLogin` and `Watermark` |
|
|
59
|
-
| `@atelic-action/ui/styles/artifact.css` | Styles for everything under `artifact`: the writeup, the monthly page, the sealed document stage, the grade chip, and the sign off |
|
|
60
|
-
| `@atelic-action/ui/vite` | `artifactConfig`, the whole Vite configuration of an artifact site as one call, with `artifactPages`; and `privateChunks`, the Vite plugin that keeps private content in `/assets/doc/`, with `isPrivateContent` and `DOC_DIR` (see [The Artifact Vite Config](#the-artifact-vite-config)) |
|
|
61
|
-
| The `bin` commands | Eight commands a site's `package.json` calls by name, from `atelic-check-access` to `atelic-seal-document` (see [The Commands](#the-commands)) |
|
|
62
|
-
| `@atelic-action/ui/gate` | `signToken`, `verifyToken`, `safeNext`, `needsSession`, and the token types |
|
|
63
|
-
| `@atelic-action/ui/gate/request-link` | `POST`, the handler that emails a sign in link, and `createRequestLink` |
|
|
64
|
-
| `@atelic-action/ui/gate/verify` | `GET`, the handler that turns a link into a session |
|
|
65
|
-
| `@atelic-action/ui/gate/middleware` | The routing middleware as the default export, and `createMiddleware` |
|
|
66
|
-
| `@atelic-action/ui/routing` | `staticNotFoundRouting`, the router options behind the not found page |
|
|
67
|
-
| `@atelic-action/ui/styles/base.css` | Resets, the `.mkt` canvas, typography, and layout helpers |
|
|
68
|
-
| `@atelic-action/ui/styles/chrome.css` | Styles for everything under `chrome` |
|
|
69
|
-
| `@atelic-action/ui/styles/components.css` | Layout defaults for everything under `components` |
|
|
70
|
-
| `@atelic-action/ui/styles/star-rating.css` | `StarRating`'s styles alone, for an app that wears none of the site chrome; `components.css` already imports it |
|
|
150
|
+
## Rendering From Props
|
|
71
151
|
|
|
72
152
|
Every component renders from props alone. None reads a config file or a router, so a site maps its own config onto the props in its shell:
|
|
73
153
|
|
|
74
154
|
```tsx
|
|
75
155
|
import { Footer, SiteHeader, SkipLink, StickyCTABar } from "@atelic-action/ui/chrome";
|
|
76
156
|
|
|
77
|
-
<div className="
|
|
157
|
+
<div className="atelic-ui">
|
|
78
158
|
<SkipLink />
|
|
79
159
|
<SiteHeader
|
|
80
160
|
brand={{ name: site.name, logo: site.logo }}
|
|
@@ -146,7 +226,7 @@ import { Button, SectionHeading } from "@atelic-action/ui/primitives";
|
|
|
146
226
|
</Button>;
|
|
147
227
|
```
|
|
148
228
|
|
|
149
|
-
`Button` is an anchor when it has an `href` and a real button otherwise. It
|
|
229
|
+
`Button` is an anchor when it has an `href` and a real button otherwise. It puts the base path from `BasePathProvider` on a root path `href` (0.19.0), so a site served under a prefix needs no wrapper; with no provider above it the `href` renders as given. Its arrow is hidden from the name a screen reader hears.
|
|
150
230
|
|
|
151
231
|
## The Access Gate
|
|
152
232
|
|
|
@@ -197,17 +277,39 @@ What it holds to, each with a test in `tst/gate/`:
|
|
|
197
277
|
- The post sign in destination is held to a path on the same host, 512 characters at most (`safeNext`).
|
|
198
278
|
- An open path opens only as written; the held corner is held in any case. Under `/api/`, only `/api/auth/` is open.
|
|
199
279
|
|
|
200
|
-
What it does not do, by having no store: a link can be used more than once until it expires, and
|
|
280
|
+
What it does not do, by having no store: a link can be used more than once until it expires, and the code does not rate limit link requests. That is handled outside the code, by a Vercel firewall rule on `/api/auth/request-link`. Rotating the secret ends every session at once. The `gate_email` and `gate_wm` cookies that feed the watermark are readable and unsigned on purpose: the watermark is drawn in the reader's own browser, so it deters and attributes nothing a reader set on removing it could not already remove.
|
|
201
281
|
|
|
202
282
|
This is one of the two corners of the package that ship compiled (the other is the Vite entry, under [The Artifact Vite Config](#the-artifact-vite-config)). Vercel runs a function's and a middleware's package imports as shipped and compiles nothing inside `node_modules`, so `bun run build` emits `src/gate/` to `dist/gate/`, and `prepack` runs it before every publish.
|
|
203
283
|
|
|
284
|
+
### A Marketing Site's Matcher
|
|
285
|
+
|
|
286
|
+
A website's other functions (the contact relay) are public by design, so a marketing site keeps all of `api/` and the login page away from the middleware and leaves the rest of the deciding to it (0.19.0):
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
// middleware.ts
|
|
290
|
+
export { default } from "@atelic-action/ui/gate/middleware";
|
|
291
|
+
|
|
292
|
+
// Everything but api/ and the login page reaches the middleware, which lets a
|
|
293
|
+
// request through with no session only when the wall's open paths name it.
|
|
294
|
+
export const config = { matcher: ["/((?!api/|login$|login/).*)"] };
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
The default middleware holds by allowlist: with the wall armed, a request passes with no session only when the wall's open exact paths and open prefixes name it (`OPEN_PATHS` in `@atelic-action/ui/gate`), which covers what a login page loads (its hashed assets, fonts, and favicon). Anything else is held, so a page whose last path segment carries a dot (`/blog/release-2.0`) or one asked for by its file name (`/about/index.html`) stays behind the wall, where the old matcher let any path with a file extension through. A site with an open file of its own adds it, and never opens the held corner by doing so:
|
|
298
|
+
|
|
299
|
+
```ts
|
|
300
|
+
import { createMiddleware } from "@atelic-action/ui/gate/middleware";
|
|
301
|
+
export default createMiddleware({ open: { exact: ["/apple-touch-icon.png"], prefixes: ["/icons/"] } });
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
`createNeedsSession` in `@atelic-action/ui/gate` is the same test on its own. A `needsSession` a site passes still wins over both, for a matcher that already leaves its open paths out.
|
|
305
|
+
|
|
204
306
|
### The Screen and the Watermark
|
|
205
307
|
|
|
206
308
|
The two pieces a reader sees are ordinary components, in `@atelic-action/ui/components`, styled by `@atelic-action/ui/styles/gate.css` (import it after `components.css`):
|
|
207
309
|
|
|
208
310
|
```tsx
|
|
209
311
|
// src/routes/login.tsx: the screen alone inside the site's scope, no header
|
|
210
|
-
<div className="
|
|
312
|
+
<div className="atelic-ui">
|
|
211
313
|
<GateLogin
|
|
212
314
|
brandName={site.recipient.name}
|
|
213
315
|
eyebrow={site.gate?.eyebrow}
|
|
@@ -218,7 +320,7 @@ The two pieces a reader sees are ordinary components, in `@atelic-action/ui/comp
|
|
|
218
320
|
/>
|
|
219
321
|
</div>;
|
|
220
322
|
|
|
221
|
-
// the site's shell, once, inside its `.
|
|
323
|
+
// the site's shell, once, inside its `.atelic-ui` wrapper, so every page behind
|
|
222
324
|
// the gate wears it (the styles are scoped there, like every sheet's)
|
|
223
325
|
<Watermark />;
|
|
224
326
|
```
|
|
@@ -230,7 +332,7 @@ The two pieces a reader sees are ordinary components, in `@atelic-action/ui/comp
|
|
|
230
332
|
`@atelic-action/ui/artifact` is everything the artifact template shared across its cuts, so a fix reaches every business's documents with one version bump:
|
|
231
333
|
|
|
232
334
|
- **The documents:** `Writeup`, `Report`, `Note`, and `Gate` (the sealed document, with its `GatePayload` type; a reader's address is the key, so a payload never lists it, and the component identifies a reader to HubSpot by the address they typed), plus the `GradeChip` and `SignOff` they share, and every content type they take (`WriteupProps`, `ReportProps`, `NoteProps`, `GateProps`, and the rest).
|
|
233
|
-
- **The shell:** `ArtifactShell`, the `.
|
|
335
|
+
- **The shell:** `ArtifactShell`, the `.atelic-ui` scope with the skip link, the dark header, the credit band, and the watermark around a page.
|
|
234
336
|
- **The contract and the head:** the `ArtifactConfig` types, and `buildPageHead` and `canonicalUrl` with their `PageMeta` type.
|
|
235
337
|
- **Who opened it:** `mintToken`, `sealReader`, `openReader`, `readerId`, `readerLink`, and `whoIsReading`. The same six are also built and exported alone as `@atelic-action/ui/artifact/readers`, because a Playwright spec and a script run under Node, which compiles nothing inside `node_modules`.
|
|
236
338
|
- **The error boundary:** `AppErrorBoundary` and its `ErrorFallback`.
|
|
@@ -250,6 +352,8 @@ export function ArtifactShell(props: Omit<ArtifactShellProps, "currentPath">) {
|
|
|
250
352
|
|
|
251
353
|
**The note** is the first touch for a business whose site publishes no email address, served at `/notes` and wrapped in `ArtifactShell` exactly as the writeup is. It is the writeup's sibling, not a variant: a title and a dateline, the opener, two lanes of findings (what the customer sees, what sits under the hood), the steps the work would start with, and the close over the shared `SignOff`. Every prop is plain data, so a routine can write a note as JSON; a paragraph is a string or an array of strings and `{ text, href }` links, and the sign off person's `role` is the one field that also takes a node. A finding is a `Callout` with its `WriteupShot` under it. Its column is the writeup's 920px, centered under the header with the header's `--gutter`, and its text runs the column's full width. A finding sets its screen under its words on a phone and beside them, on the right, when wide and on paper. It reads on a phone first, stacks the two lanes as separate sections top to bottom at every width and on paper, never side by side, and prints to one letter sheet under the writeup's `@page`.
|
|
252
354
|
|
|
355
|
+
**The sealed document's classes.** `Gate` renders the opened document as the HTML it was sealed from, so its markup is the content's, and `artifact.css` styles what that HTML may wear inside `.gate-content`: the tier row (`.gate-tiers` of `.gate-tier` cards, `.is-picked` on the recommended one, `.gate-tier-name`, `.gate-tier-price`, `.gate-tier-note`, and the `.gate-tier-math` sum column of `.gate-tier-was`, `.gate-tier-credit`, `.gate-tier-mathlabel`, and the price), `ul.icon-list` for a list led by inline lucide icons (sized for the card inside a tier), `ul.gate-checks`, and, from 1.0.0 (ATE-599, pulled up from the Wynkoop and Outdoors Geek hosts), the `.gate-addons` row that seats two `.gate-tier.is-addon` monthly cards side by side under the builds, an inline SVG mark inside `.gate-tier-mathlabel` that stays on the label's line, and the `.gate-footnote` line under the tiers that the mark points at. A folder stylesheet that carried any of these deletes it.
|
|
356
|
+
|
|
253
357
|
The documents import no stylesheet of their own. A site imports `@atelic-action/ui/styles/artifact.css` in its base sheet after `gate.css` and before its `theme.css`, so every page carries the rules whichever route it is. The sheet holds two `@page` rules, the monthly page's and then the writeup's, and the later one wins for every page a site prints; the order inside the sheet is the order the template's build bundled them in and is load bearing.
|
|
254
358
|
|
|
255
359
|
### The Artifact Vite Config
|
|
@@ -276,7 +380,11 @@ export default artifactConfig({
|
|
|
276
380
|
|
|
277
381
|
`privateChunks` is the plugin inside it, exported for a site that writes its own configuration: it writes every chunk carrying private content to `/assets/doc/`, where the access wall holds it, and fails the build when one lands anywhere else.
|
|
278
382
|
|
|
279
|
-
A `vite.config.ts` is run by Node as shipped, so `src/vite/` is built to `dist/vite/` like the gate.
|
|
383
|
+
A `vite.config.ts` is run by Node as shipped, so `src/vite/` is built to `dist/vite/` like the gate. `@atelic-action/ui/vite` imports `@tanstack/react-start` and `@vitejs/plugin-react` when it loads, so a site that imports anything from it has both installed. A site that writes its own configuration and wants only the plugin imports it from `@atelic-action/ui/vite/private-chunks` (0.18.0), which loads nothing from Vite but its types and needs neither peer:
|
|
384
|
+
|
|
385
|
+
```ts
|
|
386
|
+
import { privateChunks } from "@atelic-action/ui/vite/private-chunks";
|
|
387
|
+
```
|
|
280
388
|
|
|
281
389
|
### The Commands
|
|
282
390
|
|
|
@@ -292,6 +400,9 @@ The package installs the scripts every artifact site used to carry as files, so
|
|
|
292
400
|
| `atelic-serve-static` | Serves `dist/client` on `127.0.0.1` the way Vercel does, on `PORT` or 4173 | The `webServer` command in `playwright.config.ts` |
|
|
293
401
|
| `atelic-print-sheets <url or dist/client> <out.pdf> [sheets] [page] [port]` | Prints a page to PDF with headless Chrome, reports the sheet count, and fails when a count is given and missed | `print` |
|
|
294
402
|
| `atelic-seal-document <config.json> [out.json]` | Seals a document into the payload `Gate` reads, one wrapped key for each reader. The payload names no one: no address, and readers numbered `r1`, `r2` whatever the config calls them | `seal` |
|
|
403
|
+
| `atelic-clean-sitemap` | Strips fragment and trailing slash duplicates out of `dist/client/sitemap.xml`; says so and passes when a based build wrote none | A marketing site's `build`, after vite |
|
|
404
|
+
| `atelic-check-base-path` | Fails the build when a root path in `dist/client` lacks the base path, or `vercel.json` lacks (or strays from) the two rewrites a based cut needs; a `VITE_BASE_PATH` rehearsal only warns about `vercel.json` off Vercel | Last step of a marketing site's `build` |
|
|
405
|
+
| `atelic-contact-canary` | Posts a token bearing test message to `$SITE_URL/api/contact` and passes only on a Resend id and the expected destination (`CONTACT_EXPECTED_TO`, `CONTACT_CANARY_TOKEN`); a `.invalid` `SITE_URL` is the off switch | `check:contact`, run daily by the canary workflow |
|
|
295
406
|
|
|
296
407
|
A site's scripts, whole:
|
|
297
408
|
|
|
@@ -306,7 +417,145 @@ A site's scripts, whole:
|
|
|
306
417
|
}
|
|
307
418
|
```
|
|
308
419
|
|
|
309
|
-
The logic lives in `src/scripts/` as modules with their functions exported, which is what the tests import, and each file in `bin/` only reads the command line and calls one. The TypeScript commands open on `#!/usr/bin/env bun` and run as source, since `bun` runs TypeScript inside `node_modules`. The sealing command is plain `.mjs` under `#!/usr/bin/env node`, and the print command is `bash`; it needs Chrome (the `CHROME` variable names another binary), `python3`, and `curl`.
|
|
420
|
+
The logic lives in `src/scripts/` as modules with their functions exported, which is what the tests import, and each file in `bin/` only reads the command line and calls one. The TypeScript commands open on `#!/usr/bin/env bun` and run as source, since `bun` runs TypeScript inside `node_modules`. The sealing command is plain `.mjs` under `#!/usr/bin/env node`, its logic in `src/scripts/seal-document.mjs` (named `build-gate-payload.mjs` before 0.18.0), and the print command is `bash`; it needs Chrome (the `CHROME` variable names another binary), `python3`, and `curl`.
|
|
421
|
+
|
|
422
|
+
## Sections
|
|
423
|
+
|
|
424
|
+
`@atelic-action/ui/sections` is the marketing template's page library, moved here in 0.19.0 (ATE-643) so a fix reaches every website with one version bump. A section renders from props alone: the copy, the images, and the links come in as data, and the business's facts come in through a `site` prop typed from the `SiteConfig` contract, which the entry exports. A section that reads the config takes only the keys it uses (`MapEmbedSite`, `LocationSectionSite`, `ContactInfoCardSite`, `Pick<SiteConfig, "cta">` for `CTASection`), so a whole `SiteConfig` passes as it is and an app can pass less. No section imports a router, a site file, or a stylesheet.
|
|
425
|
+
|
|
426
|
+
```tsx
|
|
427
|
+
import { SiteShell } from "@atelic-action/ui/chrome";
|
|
428
|
+
import { CTASection, Hero } from "@atelic-action/ui/sections";
|
|
429
|
+
|
|
430
|
+
<SiteShell site={site} currentPath={pathname} heroNav>
|
|
431
|
+
<Hero title="Bread worth the early line." primaryCta={site.cta} />
|
|
432
|
+
<CTASection site={site} title="Skip the line, keep the bread." />
|
|
433
|
+
</SiteShell>;
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### The Stylesheet
|
|
437
|
+
|
|
438
|
+
Every rule sits in `@layer atelic-ui` and answers to `.atelic-ui`. A site imports `@atelic-action/ui/styles/sections.css` in its base sheet right after `primitives.css` and before its own sheets, so the order runs fonts, `base.css`, `chrome.css`, `components.css`, `primitives.css`, `sections.css`, `gate.css`, the site's own CSS, then `theme.css`. The five marketing primitives' styles joined `primitives.css`. A section that composes another sits after it in the sheet, the order the template's bundle gave them. One change against the template: a contact card's value column may break a long email address rather than push the page wider than a phone.
|
|
439
|
+
|
|
440
|
+
Moving into the layer changes one thing for a site: a rule in its own unlayered CSS now wins over a section's rule whatever the specificity, where before the section's rule could win by being more specific or later. Polish a site made to a section moves to `patterns.css` and keeps working; a site rule that only ever lost to a section's should be checked against the page.
|
|
441
|
+
|
|
442
|
+
### The Base Path
|
|
443
|
+
|
|
444
|
+
A site served under a prefix (`/proto`) says so once. `BasePathProvider` (from `@atelic-action/ui/routing`, a React context whose default base is "") hands the base to everything below it, and `Button`, `Chip`, `ImageFrame`, and every section that builds a link or an image source read it through `useBasePath` and `useWithBase`. A root absolute path gets the prefix; an external URL, a protocol link, a fragment, and a path that already carries the base pass through. `SiteShell` mounts the provider from its `basePath` prop, which defaults to `site.basePath`, so a page rendered inside the shell needs nothing more. The pure helpers (`normalizeBasePath`, `joinBase`, `stripBase`, `hasBase`) are exported beside it for a site's own code.
|
|
445
|
+
|
|
446
|
+
`@atelic-action/ui/routing` is TypeScript source like the rest, so Node cannot load it, and a Playwright spec runs under Node. The four helpers are therefore also built and exported alone as `@atelic-action/ui/routing/base-path` (1.0.0), the way the readers are, and a site's `e2e/` imports them from there:
|
|
447
|
+
|
|
448
|
+
```ts
|
|
449
|
+
// e2e/base.ts
|
|
450
|
+
import { marketingBasePath } from "@atelic-action/ui/vite";
|
|
451
|
+
export { hasBase } from "@atelic-action/ui/routing/base-path";
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
The provider stays in the source entry on purpose. A built copy of it would be a second React context, and a site mounting that copy would hand its base to none of the sections, which read the source one.
|
|
455
|
+
|
|
456
|
+
### SiteShell
|
|
457
|
+
|
|
458
|
+
`SiteShell` (in `@atelic-action/ui/chrome`) is the page wrapper: the `.atelic-ui` scope around the skip link, the header, `<main>`, the sticky CTA bar, the footer with its credit band, and the watermark, all mapped from the `SiteConfig`. It takes `site`, `currentPath` (the package imports no router), `basePath`, and `nav` (defaults to `site.nav`), plus `heroNav`, `stickyCta`, `footer`, `pageNav`, `navOnDark`, `hideNavCta`, and `credit` as before. A site keeps a wrapper a few lines long:
|
|
459
|
+
|
|
460
|
+
```tsx
|
|
461
|
+
import { SiteShell as Shell, type SiteShellProps } from "@atelic-action/ui/chrome";
|
|
462
|
+
import { useLocation } from "@tanstack/react-router";
|
|
463
|
+
|
|
464
|
+
export function SiteShell(props: Omit<SiteShellProps, "currentPath" | "basePath">) {
|
|
465
|
+
const currentPath = useLocation({ select: (location) => location.pathname });
|
|
466
|
+
return <Shell {...props} currentPath={currentPath} basePath={import.meta.env.BASE_URL} />;
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### Every Section
|
|
471
|
+
|
|
472
|
+
| Section | What it is |
|
|
473
|
+
|---|---|
|
|
474
|
+
| `Hero` | The home page opener: a full bleed image (or the token gradient), a headline with a serif accent, a pair of actions, trust figures, and a scroll cue |
|
|
475
|
+
| `PageHero` | The interior page opener on the alternate band; children sit under the lead |
|
|
476
|
+
| `LogoMarquee` | A dark band of chips scrolling without end: credentials, clients, or press |
|
|
477
|
+
| `SplitSection` | The two column band: copy beside copy, or media beside copy |
|
|
478
|
+
| `PricingBand` | A brand colored band with the price in a card beside it, informational by design |
|
|
479
|
+
| `MediaBand` | A plain full bleed image band, cropped from the center |
|
|
480
|
+
| `CTABanner` | A mid page banner behind a directional wash, with the token gradient before any photography |
|
|
481
|
+
| `CTASection` | The standard closer: the site's own primary action and an optional ghost second |
|
|
482
|
+
| `FeatureGrid` | Numbered cards with a hover lift; a card is a link or holds one, never both |
|
|
483
|
+
| `FeatureDetail` | Alternating copy and media rows with tags and a tick list |
|
|
484
|
+
| `ProcessSteps` | A dark band of numbered steps beside a frame |
|
|
485
|
+
| `PricingCard` | A standalone price card with a tick list and its booking action |
|
|
486
|
+
| `MenuList` | A priced menu group of dotted rows, with fine print under it |
|
|
487
|
+
| `ExpandList` | A light accordion of numbered rows whose bodies stay in the HTML while closed |
|
|
488
|
+
| `ComparisonTable` | A graded before and after that scrolls sideways on a phone rather than reflowing |
|
|
489
|
+
| `RevealSlider` | Drag or arrow keys to compare two frames, each with its own toned shot brief before photography |
|
|
490
|
+
| `DeviceShowcase` | A desktop browser frame with a phone overlapping it |
|
|
491
|
+
| `ReviewCarousel` | The aggregate beside one large review at a time, stepped by dots and arrows |
|
|
492
|
+
| `ReviewWall` | The reviews page: tag filter and sort when the data carries them, clamped quotes behind Read more |
|
|
493
|
+
| `PressQuotes` | Rows of press quotes and awards with source and year, newest first (`sortPress` beside it) |
|
|
494
|
+
| `ProfileCards` | Equal weight cards of media beside a short bio |
|
|
495
|
+
| `Gallery` | A captioned wall of the business's own work, a shot brief where a photo is missing |
|
|
496
|
+
| `CredentialList` | Credential cards with a mono code before each |
|
|
497
|
+
| `SocialStrip` | A static grid of the business's social photos linking to the profile, never a live embed |
|
|
498
|
+
| `LocationSection` | The local search section: service areas as prose, hours, directions, and the map |
|
|
499
|
+
| `Locations` | More than one address: the flagship's map beside a card for each location |
|
|
500
|
+
| `MapEmbed` | The map: a screenshot linking to directions, or a click to load facade over the live embed |
|
|
501
|
+
| `HoursTable` | Day rows with dashed rules and mono times |
|
|
502
|
+
| `ContactForm` | Validated form with a honeypot that posts to `endpoint` (or hands off to mail); `onLead` replaces the default GA4 `generate_lead` event |
|
|
503
|
+
| `ContactInfoCard` | A dark card of direct contact rows: call, email, visit, portal |
|
|
504
|
+
| `FAQBrowser` | The help page: live search, a sticky `JumpRail` of categories with scroll spy, expand all, deep links; `contactHref` sets where the no match line points |
|
|
505
|
+
| `JumpRail` | Sticky in page pill tabs with scroll spy, used by `FAQBrowser` and on its own |
|
|
506
|
+
| `BlogIndex` | The post listing as a readable stack |
|
|
507
|
+
| `BlogPost` | One article: back link, title, byline and dateline, body |
|
|
508
|
+
| `LegalPage` | A legal document from data, its last revised date first |
|
|
509
|
+
|
|
510
|
+
The primitives that came with them, in `@atelic-action/ui/primitives`: `ImageFrame`, `ProseBlocks`, `QuoteCard`, `StarRow`, and `TickList`. `StarRow` stays beside `StarRating` on purpose: it draws only the filled stars a review shows, in whole steps and the brand color, named "4 out of 5 stars", where `StarRating` draws the whole scale with outlines in half steps and can take a rating.
|
|
511
|
+
|
|
512
|
+
The entry also exports the helpers the sections use, for a site's own code: `formatClock`, `formatHoursRange`, and `isClosed` for hours, `isHoneypotFilled`, and `trackEvent`.
|
|
513
|
+
|
|
514
|
+
### The Marketing Vite Config and Commands
|
|
515
|
+
|
|
516
|
+
```ts
|
|
517
|
+
// vite.config.ts
|
|
518
|
+
import { marketingConfig } from "@atelic-action/ui/vite";
|
|
519
|
+
import { site } from "./src/site.config";
|
|
520
|
+
|
|
521
|
+
export default marketingConfig({ site, pages: [{ path: "/blog/first-post" }] });
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
`marketingConfig` carries what the template's `vite.config.ts` did: Vite's `base` and the router's `basepath` from `site.basePath` (or a `VITE_BASE_PATH` rehearsal), a refusal when a based `site.url` does not end with its base, TanStack Start prerendering with the crawler and the 404 page (copied to `404.html` by the build), the pages a site names, a sitemap on the origin of `site.url` (`sitemapHost` to change it) and none for a based cut, React, the `@` alias, and `ssr.noExternal`. `extend` merges a site's own needs over it. It leaves `build.cssCodeSplit` at Vite's default (1.0.0): every sheet rides in the site's one base sheet, so no component imports a stylesheet, and with the flag forced off TanStack Start fails the build looking for a `style.css` that is never written. A site that gives a component its own stylesheet again passes `extend: { build: { cssCodeSplit: false } }`. A site's scripts:
|
|
525
|
+
|
|
526
|
+
```json
|
|
527
|
+
{
|
|
528
|
+
"build": "tsc -b && vite build && cp dist/client/404/index.html dist/client/404.html && atelic-clean-sitemap && atelic-stamp-health && atelic-check-base-path",
|
|
529
|
+
"check:links": "atelic-check-links",
|
|
530
|
+
"check:contact": "atelic-contact-canary"
|
|
531
|
+
}
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
`playwright.config.ts` serves the build with `atelic-serve-static`, which serves a based build both under its prefix and without it, as the template's copy did.
|
|
535
|
+
|
|
536
|
+
### Thinning a Marketing Site
|
|
537
|
+
|
|
538
|
+
A site that takes 0.19.0 deletes its copies and repoints its own files. The template's `src/components/sections/`, the five primitives and their sheets, the `Button` and `Chip` wrappers, `chrome/SiteShell.tsx`'s body, `siteConfig.ts`, and the scripts all go; a route or section the site added that imports one of them repoints:
|
|
539
|
+
|
|
540
|
+
| Old import | New import |
|
|
541
|
+
|---|---|
|
|
542
|
+
| `@/components/sections/<Name>` (all 35, their types included) | `@atelic-action/ui/sections` |
|
|
543
|
+
| `@/components/primitives/Button`, `Chip` | `@atelic-action/ui/primitives` (the wrappers die; the base comes from `BasePathProvider`) |
|
|
544
|
+
| `@/components/primitives/ImageFrame`, `ProseBlocks`, `QuoteCard`, `StarRow`, `TickList` | `@atelic-action/ui/primitives` |
|
|
545
|
+
| `@/components/chrome/SiteShell` | The site's few line wrapper above, around `SiteShell` from `@atelic-action/ui/chrome` |
|
|
546
|
+
| `@/components/siteConfig` | `@atelic-action/ui/sections` (`SiteConfig`, `NavItem`, `CTA`, `OpeningHours`, `Address`, `Founder`, `GateConfig`, `SiteLink`, `SocialLink`) |
|
|
547
|
+
| `@/components/newTabProps` | `@atelic-action/ui/artifact` (`newTabProps`) |
|
|
548
|
+
| `@/shared/lib/hours` | `@atelic-action/ui/sections` (`formatClock`, `formatHoursRange`, `isClosed`) |
|
|
549
|
+
| `@/shared/lib/honeypot` | `@atelic-action/ui/sections` in a component; `api/contact.ts` keeps its own copy, since Vercel runs a function's package imports uncompiled |
|
|
550
|
+
| `@/shared/lib/analytics` (`trackEvent` only) | `@atelic-action/ui/sections`; the rest of the module, the click listener, stays in the site |
|
|
551
|
+
| `@/shared/lib/basePath` (`normalizeBasePath`, `joinBase`, `stripBase`, `hasBase`) | `@atelic-action/ui/routing`; in a component, `useWithBase()` in place of `withBase`; in `e2e/`, `@atelic-action/ui/routing/base-path` |
|
|
552
|
+
| `@/shared/lib/basePathAudit`, `@/shared/lib/sitemap` | Gone, inside `atelic-check-base-path` and `atelic-clean-sitemap` |
|
|
553
|
+
| `scripts/clean-sitemap.ts`, `check-base-path.ts`, `contact-canary.ts`, `stamp-health.ts`, `check-links.ts`, `serve-static.ts` | `atelic-clean-sitemap`, `atelic-check-base-path`, `atelic-contact-canary`, `atelic-stamp-health`, `atelic-check-links`, `atelic-serve-static` |
|
|
554
|
+
| `vite.config.ts` | `marketingConfig`, above |
|
|
555
|
+
| The `import "./<name>.css"` in each component | `@atelic-action/ui/styles/sections.css` in the base sheet, after `primitives.css` |
|
|
556
|
+
| `middleware.ts` | The marketing matcher in [The Access Gate](#a-marketing-sites-matcher) |
|
|
557
|
+
|
|
558
|
+
The `.mkt` wrapper a site's own routes render becomes `.atelic-ui`, the scope every package sheet answers to.
|
|
310
559
|
|
|
311
560
|
## Email
|
|
312
561
|
|
|
@@ -442,7 +691,7 @@ The palette carries three optional keys beyond the jq's eight: `up` (`#2F7A4B`,
|
|
|
442
691
|
|
|
443
692
|
## Page Components
|
|
444
693
|
|
|
445
|
-
The blocks a page composes, each styled in `styles/components.css` inside the `atelic-ui` layer, so a site's own unlayered rule resizes or recolors any of them. They were cut from the SkySpec baseline writeup on 2026-10-02 (ATE-600), which is the worked example of
|
|
694
|
+
The blocks a page composes, each styled in `styles/components.css` inside the `atelic-ui` layer, so a site's own unlayered rule resizes or recolors any of them. They were cut from the SkySpec baseline writeup on 2026-10-02 (ATE-600), which is the worked example of them on one page.
|
|
446
695
|
|
|
447
696
|
| Component | What it is | Notes |
|
|
448
697
|
|---|---|---|
|
|
@@ -450,24 +699,23 @@ The blocks a page composes, each styled in `styles/components.css` inside the `a
|
|
|
450
699
|
| `DataTable` | A table whose first column names each row, sortable by any column marked `sortable` | A click on a heading sorts on a wide screen. On a phone each row stacks, every fact wearing its column's label, and a dropdown sorts. A cell is a node, or `{ display, sort }` when what it shows is not what it sorts by; a formatted number in a string (`"$12,047"`, `"0.24%"`) sorts as a number. It renders in `defaultSort` on the server |
|
|
451
700
|
| `StatRow` | A row of large figures, each over what it counts and its `period`. `variant="quiet"` is the glance row for a working page: a light figure in the text color over a small capitalized label, hairlines between; `unit` sets a figure's unit small beside it | `trend` draws an arrow before a figure that is itself a change. `deltas` list comparisons beneath: an arrow, how far, against what (`basis`), and the earlier figure (`was`), each in its own tone, since a spend falling and a cost falling read differently |
|
|
452
701
|
| `Callout` | One finding: an icon in its tone beside the title, the explainer under it | `variant="box"` sets it apart on a tinted ground, for something missing or owed |
|
|
453
|
-
| `Disclosure` | A section the
|
|
454
|
-
| `Collapsible` | A labeled line the reader opens for the detail behind it | A native `details`, so it works before any script loads and find in page opens it. `summary` carries the point, so the detail needs no explaining once open. It prints open |
|
|
702
|
+
| `Disclosure` | A section the reader opens and closes, in two forms chosen by whether `open` is passed | Controlled (`open` and `onToggle`), it is an app's card: the caller remembers what was folded, a closed body is not rendered, the label is the toggle, `meta` (a count, a flag) sits beside it outside its name, and `headingLevel` makes the label a real heading. Uncontrolled, it is a document's fold: a native `details` with a `summary` line that carries the point and `defaultOpen`, so it works before any script loads, find in page opens it, and it prints open. `Collapsible`, the fold's old name, was folded in at 0.18.0 |
|
|
455
703
|
| `StackedBar` | One bar split into the parts of a whole, with a key | A part of zero is keyed and not drawn |
|
|
456
704
|
| `ColumnChart` | A bar a period, the periods in groups that carry a label and a total | Months in quarters is the case it was cut for: a quarter reads against the one before it and the same one a year earlier |
|
|
457
705
|
| `ThresholdScale` | Zones on a scale and a pin for each reading | For a number judged against lines, like a load time against good and poor |
|
|
458
706
|
| `StarRating` | A row of stars showing a rating in half star steps, and taking one when given `onChange` | Each star is two hit targets; picking the current rating clears it. Editable, it is one slider to a keyboard and a screen reader; read only, a picture named by the rating. `fillColor` and `outlineColor` take any CSS color, `size` and `max` set the star and the scale |
|
|
459
707
|
|
|
460
|
-
**StarRating outside a site.** It is the one component that does not sit under `.
|
|
708
|
+
**StarRating outside a site.** It is the one component that does not sit under `.atelic-ui` or read a theme token: its styles are in `styles/star-rating.css`, and its two colors are the `--star-fill` and `--star-outline` custom properties, falling back to the text color and a faint tint of it. An app that installs the package for this component alone imports that one sheet, not `components.css`.
|
|
461
709
|
|
|
462
710
|
**A part's own color.** `StackedBar` takes a `color` on a segment, any CSS color, for parts that are not good or bad (the protein, carbs, and fat of a meal), and it wins over `tone`. `showKey={false}` draws the bar alone for a page that keys the parts its own way; the bar keeps its spoken summary.
|
|
463
711
|
|
|
464
|
-
**Tones.** `good`, `warn`, `bad`, `info`, and `muted` are the one palette the package carries, because good, a warning, and broken read green, amber, and red whatever the brand is. A component sets `data-tone`, and `--tone`, `--tone-fill`, and `--tone-bg` follow for whatever sits inside. A site's theme may restate `--tone-good` and the rest on `.
|
|
712
|
+
**Tones.** `good`, `warn`, `bad`, `info`, and `muted` are the one palette the package carries, because good, a warning, and broken read green, amber, and red whatever the brand is. A component sets `data-tone`, and `--tone`, `--tone-fill`, and `--tone-bg` follow for whatever sits inside. A site's theme may restate `--tone-good` and the rest on `.atelic-ui`; `info` and `muted` come from the theme's own primary and neutral tokens.
|
|
465
713
|
|
|
466
714
|
## In an App
|
|
467
715
|
|
|
468
716
|
The page components also work in an app that has its own base styles (Tailwind, shadcn) and wears none of the site chrome. Three things make that so (0.10.0):
|
|
469
717
|
|
|
470
|
-
1. **
|
|
718
|
+
1. **The same root class, without the site base.** Every rule in `styles/components.css` answers to `.atelic-ui`. An app imports no `base.css`, so the class brings no resets with it, and an app puts the class on the region that holds a package component and nothing else in it restyles. Prefer a region to `body`: the components use plain class names (`.stat .num`, `.callout`, `.note`), and an app with markup of its own under those names would have a page wide scope reach it. `body` is right only for an app that has checked it has none, and it is the one way to cover a component rendered in a portal.
|
|
471
719
|
2. **The app imports one sheet**, `@atelic-action/ui/styles/components.css`, and neither `base.css` nor `chrome.css`. An app's own unlayered CSS always wins over the package. With Tailwind the app **must declare the layer order before any stylesheet loads**, in a `<style>` in the `<head>` of its `index.html`: `@layer properties, theme, base, atelic-ui, components, utilities;`. A layer ranks by where it is first named, and a bundler is free to name the package's first, which ranks it below Tailwind's reset; the reset then strips the padding, margins, and borders off every component, while colors and type still look right, so it is easy to miss. Named there, the package ranks above the reset and below the app's utilities.
|
|
472
720
|
3. **The app maps its tokens** onto the ones the components read, in its own stylesheet on the same scope:
|
|
473
721
|
|
|
@@ -503,16 +751,152 @@ The page components also work in an app that has its own base styles (Tailwind,
|
|
|
503
751
|
|
|
504
752
|
`StarRating` needs none of this: its sheet is unscoped and reads no token. The chrome (`SiteHeader`, `Footer`, and the rest) and `NotFound` remain site shaped and are not meant for a signed in app.
|
|
505
753
|
|
|
754
|
+
## The App Shell
|
|
755
|
+
|
|
756
|
+
`@atelic-action/ui/app` is the shell every signed in Atelic app is built on, moved here from `template-application`'s `src/shared` and its two stores in 1.0.0: the mobile first frame (the desktop rail, the tab bar, the menu sheet), the `AppPage` scaffold and its toolbar, the auth guard, Google sign in with a JWT and refresh session against a JSON api, the transport and query layers, the auth and units stores, and the helpers the template carried (dates, phrasing, units, search, envelopes). `@atelic-action/ui/app/ui` is the shadcn kit the shell is set in. Unlike the rest of the package the shell imports TanStack Router and is styled with Tailwind, since an app is built on both. [ui.atelic.me/app](https://ui.atelic.me/app) shows all of it running a made up bakery.
|
|
757
|
+
|
|
758
|
+
### The Provider
|
|
759
|
+
|
|
760
|
+
The shell reads nothing from the app's files. The app hands it its facts once, through `AppProvider`, rendered around the router so the stores are hydrated before the first route loads:
|
|
761
|
+
|
|
762
|
+
```tsx
|
|
763
|
+
// src/main.tsx
|
|
764
|
+
import { AppProvider, ErrorFallback } from "@atelic-action/ui/app";
|
|
765
|
+
import { appConfig } from "@/app/config";
|
|
766
|
+
import { navItems } from "@/app/nav";
|
|
767
|
+
|
|
768
|
+
createRoot(rootElement).render(
|
|
769
|
+
<StrictMode>
|
|
770
|
+
<Sentry.ErrorBoundary
|
|
771
|
+
fallback={(props) => <ErrorFallback {...props} showDetails={import.meta.env.DEV} />}
|
|
772
|
+
>
|
|
773
|
+
<AppProvider config={appConfig} nav={navItems}>
|
|
774
|
+
<RouterProvider router={router} />
|
|
775
|
+
</AppProvider>
|
|
776
|
+
</Sentry.ErrorBoundary>
|
|
777
|
+
</StrictMode>,
|
|
778
|
+
);
|
|
779
|
+
|
|
780
|
+
// src/routes/__root.tsx
|
|
781
|
+
function RootLayout() {
|
|
782
|
+
return (
|
|
783
|
+
<AuthGuard>
|
|
784
|
+
<AppLayout>
|
|
785
|
+
<Outlet />
|
|
786
|
+
</AppLayout>
|
|
787
|
+
</AuthGuard>
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
| Prop | What it is |
|
|
793
|
+
|---|---|
|
|
794
|
+
| `config` | An `AppConfig`: `name` (aria labels), `mark` (the wordmark), `storagePrefix` (every storage key and cookie), `publicRoutes` (exact paths a signed out visitor may load), `publicPages` (prefix patterns ending in `/*`, rendered in the public chrome), and `apiBaseUrl`, which the app resolves itself (`env.VITE_API_BASE_URL ?? "https://api.example.com"`), since the package never reads `import.meta.env` |
|
|
795
|
+
| `nav` | The `NavItem[]` every nav surface reads, sub pages as `children`; the one place a destination is declared |
|
|
796
|
+
| `queryClient` | Optional; defaults to the shell's, whose mutation cache raises `meta.toast` |
|
|
797
|
+
| `toaster` | Optional; sonner's `Toaster` at the top right by default, `false` for none |
|
|
798
|
+
|
|
799
|
+
Inside it, `useAppConfig()` and `useNav()` read the two facts, and `useUser()` the signed in user. The transport and the stores run outside React, so the provider also records the config where they read it; code that calls the api before the provider first renders calls `configureApp(appConfig)` itself. `AppConfigProvider` is the config and nav alone, for a test that mounts one surface.
|
|
800
|
+
|
|
801
|
+
**The user type.** The shell knows the fields it reads (`AppUser`: the name, the avatar, the username, the unit preferences). An app registers its own user once, and `useUser`, `useAuthStore`, and `session.me` hand it back typed in full:
|
|
802
|
+
|
|
803
|
+
```ts
|
|
804
|
+
// src/app/types.ts
|
|
805
|
+
declare module "@atelic-action/ui/app" {
|
|
806
|
+
interface Register {
|
|
807
|
+
user: User;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
**The stores moved with the shell**, because the transport cannot work without the auth store and the user context keeps the units store in step with the account. Both persist under the app's prefix exactly as before (`<storagePrefix>-auth`, `<storagePrefix>-units`), so a signed in browser stays signed in across the move. An app's own stores (`cookingStore` in pinole) stay in the app.
|
|
813
|
+
|
|
814
|
+
### The Stylesheet
|
|
815
|
+
|
|
816
|
+
The shell is Tailwind and shadcn, shipped as source like everything else here. `@atelic-action/ui/styles/app.css` carries the shadcn token mapping (`@theme`), the default light and dark token values, the base layer, and the `ap-*` scaffold; the app imports it straight after Tailwind, in its own Tailwind entry, and overrides any value in its own theme after it:
|
|
817
|
+
|
|
818
|
+
```css
|
|
819
|
+
/* src/styles/globals.css */
|
|
820
|
+
@import "tailwindcss";
|
|
821
|
+
@import "@atelic-action/ui/styles/app.css";
|
|
822
|
+
|
|
823
|
+
@plugin "tailwindcss-animate";
|
|
824
|
+
@plugin "@tailwindcss/typography";
|
|
825
|
+
|
|
826
|
+
/* The app's @font-face rules (it serves the files), then its own tokens. */
|
|
827
|
+
```
|
|
828
|
+
|
|
829
|
+
The import has to come before any other rule, or it is dropped without a word: the build passes and the shell renders unstyled. Tailwind never scans `node_modules`, so the sheet carries `@source "../app";`, which points Tailwind at the shell's components relative to the sheet, and the app adds nothing. An app that keeps its own tokens and does not import the sheet adds the equivalent line from its own stylesheet instead:
|
|
830
|
+
|
|
831
|
+
```css
|
|
832
|
+
@source "../../node_modules/@atelic-action/ui/src/app";
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
Proven on a copy of pinole-app thinned onto a packed tarball: with the line or the import, the built CSS holds every utility the shell uses; with neither, 165 of them are missing. The sheet sits outside the `atelic-ui` layer on purpose: its tokens and resets are in Tailwind's base layer and the scaffold is unlayered, as the template carried them, so the shell renders as it did.
|
|
836
|
+
|
|
837
|
+
### The Kit
|
|
838
|
+
|
|
839
|
+
`@atelic-action/ui/app/ui` exports the shadcn components the template carried: `Accordion`, `Alert`, `AlertDialog`, `Avatar`, `Badge`, `BadgeList`, `Button`, `Callout`, `Card`, `Checkbox`, `ClearButton`, `Command`, `CountryDropdown`, `Dialog`, `DropdownMenu`, `Form`, `Input`, `Label`, `OptionSelect`, `Pagination`, `Popover`, `Progress`, `SearchInput`, `Select`, `Separator`, `Sheet`, `Slider`, `Switch`, `Textarea`, and `Tooltip`, each with its parts. A new kit component lands here, not in an app, and it is written by hand into `src/app/ui/` and exported from its `index.ts`: the package carries no `components.json`, because `bunx shadcn add` writes its imports through the `@/` aliases the package never uses (consumers resolve none of ours), so a component taken from the shadcn registry has its imports made relative before it lands. The main entry adds the frame and its pieces (`AppLayout`, `AppPage`, `AppPageToolbar`, `Breadcrumbs`, `SectionCards`, `ActionButton`, `FormActionButtons`, `PageSkeleton`, `PageError`, `AuthGuard`, `CardErrorBoundary`, `ErrorFallback`, `GoogleGlyph`).
|
|
840
|
+
|
|
841
|
+
The app's stack is optional peer dependencies, so a marketing site installs none of it: TanStack Router and Query, Tailwind, `sonner`, `zustand`, `zod`, `react-hook-form`, `class-variance-authority`, `clsx`, `tailwind-merge`, `cmdk`, the Radix primitives the kit uses, `country-data-list`, and `react-circle-flags`, at the ranges `template-application` already pins. An app that imports the shell has them all already.
|
|
842
|
+
|
|
843
|
+
### Thinning an App onto It
|
|
844
|
+
|
|
845
|
+
Delete `src/shared/`, `src/stores/authStore.ts`, `src/stores/unitsStore.ts`, and their tests (`tst/shared/`, `tst/stores/authStore.test.ts`, `tst/stores/unitsStore.test.ts`), then repoint every import:
|
|
846
|
+
|
|
847
|
+
| Old Path | New Path |
|
|
848
|
+
|---|---|
|
|
849
|
+
| `@/shared/components/layout` and every file in it | `@atelic-action/ui/app` |
|
|
850
|
+
| `@/shared/components/ui/<name>` | `@atelic-action/ui/app/ui` |
|
|
851
|
+
| `@/shared/components/AuthGuard`, `CardErrorBoundary`, `ErrorFallback`, `GoogleGlyph` | `@atelic-action/ui/app` |
|
|
852
|
+
| `@/shared/context/SidebarContext`, `@/shared/context/UserContext` | `@atelic-action/ui/app` |
|
|
853
|
+
| `@/shared/hooks/useDebounce`, `useIsDesktop`, `useWakeLock` | `@atelic-action/ui/app` |
|
|
854
|
+
| `@/shared/lib/<name>`, every file but `env` | `@atelic-action/ui/app` |
|
|
855
|
+
| `@/shared/lib/env` | The app's own `src/app/env.ts`: `export const env = parseEnv(import.meta.env);` |
|
|
856
|
+
| `@/shared/types`, `@/shared/types/query` | `@atelic-action/ui/app` |
|
|
857
|
+
| `@/stores/authStore`, `@/stores/unitsStore` | `@atelic-action/ui/app` |
|
|
858
|
+
|
|
859
|
+
Then the handful of edits the seam asks for:
|
|
860
|
+
|
|
861
|
+
1. **`src/app/config.ts`** resolves `apiBaseUrl` itself (`env.VITE_API_BASE_URL ?? "https://api.example.com"`).
|
|
862
|
+
2. **`src/app/types.ts`** registers its `User` (above).
|
|
863
|
+
3. **`src/main.tsx`** wraps the router in `AppProvider`, and passes `showDetails={import.meta.env.DEV}` to `ErrorFallback`, which no longer reads the environment.
|
|
864
|
+
4. **`src/routes/__root.tsx`** keeps only `AuthGuard` and `AppLayout` around the `Outlet`; the query client, the rail state, the user, and the toaster are the provider's.
|
|
865
|
+
5. **`src/styles/globals.css`** keeps Tailwind, its plugins, its fonts, and any token of its own, and imports `app.css` in place of the `@theme` block, the token sets, the base layer, and the `ap-*` scaffold.
|
|
866
|
+
6. **`activeSection(pathname)`** takes the items now: `activeSection(pathname, navItems)`.
|
|
867
|
+
7. **`components.json`** no longer has a `src/shared` to add to; a new kit component is written by hand in the package's `src/app/ui/` (see [The Kit](#the-kit)).
|
|
868
|
+
|
|
869
|
+
**Tests.** Mocking `@atelic-action/ui/app` replaces what the app's own code imports from it, and nothing the shell imports from itself. A component that calls `session.me` sees a mock written over the entry (`vi.mock(import("@atelic-action/ui/app"), async (original) => ({ ...(await original()), session: mockSession }))`), but the transport reads the auth store, the token refresh, and the config through its own relative imports, so a mocked `useAuthStore` or `ensureValidToken` never reaches `apiFetch`. A test that runs through the transport (a feature's api module, a query hook) therefore drives the real shell and mocks only `fetch`:
|
|
870
|
+
|
|
871
|
+
```ts
|
|
872
|
+
import { configureApp, useAuthStore } from "@atelic-action/ui/app";
|
|
873
|
+
import { appConfig } from "@/app/config";
|
|
874
|
+
|
|
875
|
+
beforeEach(() => {
|
|
876
|
+
configureApp(appConfig);
|
|
877
|
+
useAuthStore.setState({
|
|
878
|
+
token: "test-token",
|
|
879
|
+
refreshToken: "test-refresh",
|
|
880
|
+
expiresAt: Date.now() + 60 * 60 * 1000,
|
|
881
|
+
user: testUser, // the app's own fixture user
|
|
882
|
+
isAuthenticated: true,
|
|
883
|
+
});
|
|
884
|
+
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(Response.json({ data: [] })));
|
|
885
|
+
});
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
An `expiresAt` an hour out keeps the refresh path quiet; one inside five minutes sends the transport to the refresh endpoint first, through the same mocked `fetch`.
|
|
889
|
+
|
|
506
890
|
## The Gallery
|
|
507
891
|
|
|
508
|
-
`gallery/` is
|
|
892
|
+
`gallery/` is two static pages. The Components shows everything the package exports that draws (the chrome, the page components, the sections, the documents, and the email) on its own with made up data for a fictional bakery, wearing the practice's own palette and type. It lives at [ui.atelic.me](https://ui.atelic.me), noindexed and disallowed in its `robots.txt`, since it is for the people building with the package and never search inventory. It imports the components and stylesheets from `src/` by relative path rather than from npm, so it always shows the code in the repo, and it reads the package name and version from `package.json` at build time. It sits outside `src/`, so it is never published. The second page, [ui.atelic.me/app](https://ui.atelic.me/app) (`gallery/app.html`), runs the app shell for a made up bakery's ops app, on Tailwind through `@tailwindcss/vite`, with its own stylesheet set up the way an app's `globals.css` is; the plugin leaves The Components' plain sheets as they are.
|
|
509
893
|
|
|
510
894
|
```bash
|
|
511
895
|
bun run gallery:dev # a dev server on the repo's own source
|
|
512
896
|
bun run gallery:build # the static page, written to gallery/dist
|
|
513
897
|
```
|
|
514
898
|
|
|
515
|
-
`vercel.json` builds it the same way and serves `gallery/dist`. Typecheck and lint cover it, and CI builds it, so a component change that breaks the gallery fails CI. **A new
|
|
899
|
+
`vercel.json` builds it the same way and serves `gallery/dist`. Typecheck and lint cover it, and CI builds it, so a component change that breaks the gallery fails CI. **A new component gets a gallery section in the same change**, with each of its meaningfully different forms shown. `gallery/parts.tsx` holds the frame pieces (`Piece`, `Example`, and `Frame`, which holds a fixed piece inside the page), and a group of pieces lives in a file of its own beside `Gallery.tsx` (`chrome.tsx`, `documents.tsx`, `email.tsx`), mounted there with one element. Each document is one whole fictional document, and the sealed one was sealed with the package's own sealing command (`gallery/gate-payload.json`, reader `reader@example.test`).
|
|
516
900
|
|
|
517
901
|
## Releasing
|
|
518
902
|
|