diff --git a/BUILD-NOTES.md b/BUILD-NOTES.md
index 9ae6443..bfd8735 100644
--- a/BUILD-NOTES.md
+++ b/BUILD-NOTES.md
@@ -878,3 +878,15 @@ separates fresh final checks, applied-asset viewer proof and final sandbox check
unchanged-source16main/48boundary/no-JS/growth/contrast/fallback evidence. Private fixture databases,
authentication, logs and backup material are excluded. No analytics, customer/financial data,
launch, billing, schema, infrastructure or unrelated runtime behavior changes.
+
+## Limited customer UI approval: artifact detail and template library
+
+The user rejected broad admin redesign adoption. This change adopts only the artifact-detail **arrangement**, keeping the current product shell, type, palette, red Sparkline and StatCard/Audience implementations. The document/sharing pair uses a scoped 2:1 layout at the existing 760px breakpoint and stacks in DOM order below it; native focusable anchors lead to Document, Sharing, Audience, History and Save as template. Real forms, confirmation dialogs, preview security and domain behavior are unchanged.
+
+Only the authenticated `/dashboard/templates` cards are simplified to image, linked title and tags. The separate public catalog renderer is unchanged. Stored/API/preview descriptions and all 20 canonical template/source/thumbnail options are untouched. The redundant Preview button is removed, not the preview action: the existing stretched title link opens it from the whole card.
+
+Library navigation uses native links, styled as tabs, with `library=mine|builtin` and `aria-current=page`; Tab/Enter, reload, back and no-JavaScript navigation work without a new client script. Built-in remains the default because it was the first group before this change, including for accounts with personal templates. Legacy preview URLs select the preview's library; explicit mismatched library/preview pairs show only the chosen library, not a confusing cross-library preview. Successful promotion's existing notice selects My templates. Preview/close preserve the library; switching library clears preview. There was no template-list filter or pagination to replace.
+
+Scoped integration red controls fail on the previous source for missing detail groups, missing library selection and visible card descriptions. Existing assertions move to the explicit personal destination / new query-preserving preview URLs. Browser regressions exercise the existing seven responsive projects, real sandboxed owner preview and JavaScript-disabled library navigation. The style guide documents these two targeted patterns using current primitives; no global redesign, dependency, schema, provider or launch-flag change is included.
+
+Homepage activation is separately authorized after this UI rollout is verified: existing `AA_COMING_SOON=false` selects the already-built HomePage. This PR does not merge/deploy or change that runtime flag. Existing production deployment and rollback safeguards remain required.
diff --git a/src/routes/dashboard.ts b/src/routes/dashboard.ts
index f5378c8..3714f71 100644
--- a/src/routes/dashboard.ts
+++ b/src/routes/dashboard.ts
@@ -672,6 +672,14 @@ export function registerHumanRoutes(app: HumanApp, context: HumanRoutesContext):
DashboardTemplatesPage({
account: accountView(session.account),
templates,
+ library:
+ routeContext.req.query('library') === 'mine' ||
+ (!routeContext.req.query('library') &&
+ routeContext.req.query('notice') === 'template_promoted')
+ ? 'mine'
+ : routeContext.req.query('library') === 'builtin'
+ ? 'builtin'
+ : undefined,
previewTemplate: await getTemplatePreview(
services,
session.account.id,
diff --git a/src/ui/assets/app.css b/src/ui/assets/app.css
index faeeb57..8d90648 100644
--- a/src/ui/assets/app.css
+++ b/src/ui/assets/app.css
@@ -573,6 +573,26 @@
grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
}
+ /* Artifact detail only: the document leads, sharing sits beside it when there is room.
+ Audience keeps its existing widgets and chart below this pair, not a third narrow column. */
+ .aa-artifact-detail__columns {
+ display: grid;
+ min-width: 0;
+ gap: var(--spacing-aa-4);
+ align-items: start;
+ grid-template-columns: minmax(0, 1fr);
+ }
+
+ .aa-artifact-detail__columns > * {
+ min-width: 0;
+ }
+
+ @media (min-width: 760px) {
+ .aa-artifact-detail__columns {
+ grid-template-columns: minmax(0, 2fr) minmax(16rem, 1fr);
+ }
+ }
+
.aa-grid--3 {
grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
}
@@ -3511,10 +3531,7 @@
height: 3rem;
}
- /* A column, so the actions row can be pushed to the bottom edge of the tallest card in the row.
- Cards already stretch to a common height; without this the Preview buttons sat wherever each
- description happened to end, and a one-line description next to a two-line one put two
- identical buttons on two different baselines. */
+ /* Listing cards show only their image, linked title and tags. Full copy stays in the preview. */
.aa-template-card__body {
display: flex;
min-width: 0;
@@ -3539,8 +3556,7 @@
text-decoration: none;
}
- /* The whole card opens the preview; the Preview button is the same destination, named out loud
- for a reader scanning for a control. The button sits above this overlay on its own z-index. */
+ /* One link opens the preview from the whole card; no duplicate Preview control. */
.aa-template-card__link::after {
position: absolute;
inset: 0;
@@ -3573,26 +3589,16 @@
gap: var(--spacing-aa-2);
}
- .aa-template-card__subline {
- min-width: 0;
- margin: 0;
- color: var(--color-aa-muted);
- font-size: var(--text-aa-sm);
- overflow-wrap: anywhere;
+ /* Native page links, not script-dependent tabs: reload/back/no-JS retain the library. */
+ .aa-template-tabs .aa-tab {
+ text-decoration: none;
+ white-space: normal;
+ text-align: center;
}
- .aa-template-card__actions {
- position: relative;
- z-index: 1;
- display: flex;
- min-width: 0;
- max-width: 100%;
- flex-wrap: wrap;
- justify-content: flex-start;
- /* The pin: whatever the description's length, the button sits on the card's bottom edge. */
- margin-top: auto;
- padding-top: var(--spacing-aa-3);
- gap: var(--spacing-aa-2);
+ .aa-template-tabs .aa-tab[aria-current="page"] {
+ color: var(--color-aa-accent);
+ border-bottom-color: var(--color-aa-accent);
}
/* The template preview frame. A framed document has no intrinsic size, so without this it falls
diff --git a/src/ui/pages/dashboard.tsx b/src/ui/pages/dashboard.tsx
index 37d8132..6f3434e 100644
--- a/src/ui/pages/dashboard.tsx
+++ b/src/ui/pages/dashboard.tsx
@@ -534,35 +534,67 @@ export function DashboardArtifactPage({
-
-
- {artifact.type === 'markdown' && artifact.htmlPreview ? (
-
- ) : previewUrl ? (
- /*
- * Absolute and cross-origin on cloud, absolute and same-origin self-hosted — the URL
- * the route handed down, never one built here. A relative `src` is what broke this
- * card: it resolves to the dashboard origin, and the dashboard's own CSP admits only
- * the sandbox host to `frame-src`, so on cloud the browser refused the load and the
- * "Rendered preview" was blank. `sandbox="allow-scripts"` stays: the attribute keeps
- * the document in an opaque origin whichever host served it.
- */
-
+
-
- {stats ? : null}
+
+
+
+
+
+
+
+ {artifact.type === 'markdown' && artifact.htmlPreview ? (
+
+ ) : previewUrl ? (
+ /*
+ * Absolute and cross-origin on cloud, absolute and same-origin self-hosted — the URL
+ * the route handed down, never one built here. A relative `src` is what broke this
+ * card: it resolves to the dashboard origin, and the dashboard's own CSP admits only
+ * the sandbox host to `frame-src`, so on cloud the browser refused the load and the
+ * "Rendered preview" was blank. `sandbox="allow-scripts"` stays: the attribute keeps
+ * the document in an opaque origin whichever host served it.
+ */
+
+ ) : null}
+
+
+
+
+
+
+ {stats ? (
+
+
+
+ ) : null}
+
+
-
-
{diff ? : null}
-
+
+
+
);
@@ -661,7 +693,10 @@ export function DashboardBotsPage({
);
}
+export type TemplateLibrary = 'builtin' | 'mine';
+
export interface DashboardTemplatesPageProps {
+ library?: TemplateLibrary | undefined;
account: DashboardAccountView;
templates: DashboardTemplateView[];
previewTemplate?: DashboardTemplatePreview | null | undefined;
@@ -671,6 +706,7 @@ export interface DashboardTemplatesPageProps {
export function DashboardTemplatesPage({
account,
+ library,
templates,
previewTemplate,
extensionNavItems,
@@ -679,6 +715,12 @@ export function DashboardTemplatesPage({
const starters = templates.filter((template) => template.builtIn);
const personal = templates.filter((template) => !template.builtIn);
+ // Keep the old first group as the default. Legacy preview links open their own library;
+ // a successful promotion opens My templates so the saved result is not hidden on arrival.
+ const selected = library ?? (previewTemplate && !previewTemplate.builtIn ? 'mine' : 'builtin');
+ const visiblePreview =
+ previewTemplate?.builtIn === (selected === 'builtin') ? previewTemplate : null;
+
return (
+
{/* GROUPED BY THE JOB, not listed by name.
The same six categories the public gallery and `GET /v1/templates?category=` use, so a
person browsing here and an agent filtering there are looking at one taxonomy. A flat
@@ -709,40 +767,49 @@ export function DashboardTemplatesPage({
has is "what do I start from for the thing I am doing".
Empty categories are not rendered — a heading over nothing is a heading that has to be
read to discover it says nothing. */}
- {TEMPLATE_CATEGORY_ORDER.map((category) => {
- const group = starters.filter((template) => template.category === category);
- return group.length === 0 ? null : (
-
- );
- })}
- {starters.length === 0 ? (
+ {selected === 'builtin'
+ ? TEMPLATE_CATEGORY_ORDER.map((category) => {
+ const group = starters.filter((template) => template.category === category);
+ return group.length === 0 ? null : (
+
+ );
+ })
+ : null}
+ {selected === 'builtin' && starters.length === 0 ? (
) : null}
-
- Pick an artifact to promote →
-
- }
- />
- {previewTemplate ? : null}
+ {selected === 'mine' ? (
+
+ Pick an artifact to promote →
+
+ }
+ />
+ ) : null}
+ {visiblePreview ? (
+
+ ) : null}
);
@@ -2083,7 +2150,13 @@ function BotActions({ bot }: { bot: DashboardBotView }) {
* Slots are listed only when there are any. "Slots: none" on the three HTML examples that declare
* none was a field-form frame on something that is not a form.
*/
-function TemplatePreviewPanel({ template }: { template: DashboardTemplatePreview }) {
+function TemplatePreviewPanel({
+ template,
+ library,
+}: {
+ template: DashboardTemplatePreview;
+ library: TemplateLibrary;
+}) {
const isHtml = template.type === 'html';
return (
@@ -2132,7 +2205,7 @@ function TemplatePreviewPanel({ template }: { template: DashboardTemplatePreview
value={template.content}
/>
-
@@ -2154,6 +2227,7 @@ function TemplateGroup({
id,
title,
templates,
+ library,
emptyTitle,
empty,
emptyAction,
@@ -2161,6 +2235,7 @@ function TemplateGroup({
id: string;
title: string;
templates: DashboardTemplateView[];
+ library: TemplateLibrary;
emptyTitle: string;
empty: string;
emptyAction?: Child | undefined;
@@ -2179,7 +2254,7 @@ function TemplateGroup({
{templates.map((template) => (
-
+
))}
@@ -2199,8 +2274,14 @@ function TemplateGroup({
* It is a template-specific card rather than `DashboardCard` because the cover image changes the
* card's shape, not its trim, and Artifacts and Bots must not inherit that.
*/
-function TemplateCard({ template }: { template: DashboardTemplateView }) {
- const href = `/dashboard/templates?preview=${template.id}#template-preview`;
+function TemplateCard({
+ template,
+ library,
+}: {
+ template: DashboardTemplateView;
+ library: TemplateLibrary;
+}) {
+ const href = `/dashboard/templates?library=${library}&preview=${template.id}#template-preview`;
return (
{/* The cover is the card's own link target via the stretched pseudo-element below, so it
@@ -2225,14 +2306,6 @@ function TemplateCard({ template }: { template: DashboardTemplateView }) {
{template.type === 'markdown' ? 'md' : 'html'}
-
- {template.description ?? 'No description yet — open the preview to see the example.'}
-
-
-
- Preview
-
-
);
diff --git a/src/ui/pages/style-guide.tsx b/src/ui/pages/style-guide.tsx
index e7d85c8..e728c91 100644
--- a/src/ui/pages/style-guide.tsx
+++ b/src/ui/pages/style-guide.tsx
@@ -1092,57 +1092,87 @@ function dangerCardSection() {
function cardTableSection() {
return (
-
-
-
- Continue
-
-
- Cancel
-
-
- }
- >
-
Use cards for grouped forms, setup steps, and dashboard side panels.
- The region is focusable, named from its caption, and carries an inline-end fade plus a
- hint whenever scrollWidth > clientWidth — measured, never assumed, and
- re-measured on resize. A table that fits shows neither.
-
-
- {tableColumnPrioritySection()}
+
+
+
+
Primary preview. Existing Audience widgets follow the pair.
+
+
+
Adjacent on desktop; follows the document on mobile.
+
+
+
+
+ Library tabs are native page links: Tab then Enter, with selection kept in the URL. Cards
+ contain image, title and tags; descriptions remain in previews.
+
+
+
+
+ Continue
+
+
+ Cancel
+
+
+ }
+ >
+
+ Use cards for grouped forms, setup steps, and dashboard side panels.
+
+ The region is focusable, named from its caption, and carries an inline-end fade plus a
+ hint whenever scrollWidth > clientWidth — measured, never assumed, and
+ re-measured on resize. A table that fits shows neither.
+