import './dcx-settings.js'; import { dcxAsset } from './assets.js'; (() => { "use strict"; const renameIconographyToComponents = () => { const tile = document.querySelector('.dcx-tile[data-category="iconography"]'); if (tile) { tile.dataset.category = "components"; tile.dataset.name = "Components"; tile.setAttribute("aria-label", "Open Components"); const title = tile.querySelector(".dcx-tile-title"); if (title) title.textContent = "Components"; } const shellTemplate = document.querySelector("#dcx-shell-template"); const navItem = shellTemplate?.content.querySelector('li[data-category="iconography"]'); const navLink = navItem?.querySelector(".dcx-nav-link"); if (navItem && navLink) { navItem.dataset.category = "components"; navLink.href = "#components"; navLink.dataset.dcxNav = "components"; navLink.textContent = "Components"; } }; renameIconographyToComponents(); const CATEGORIES = [ "audience", "product", "brand", "color", "typography", "components", "material", "hooks", ]; const REDUCED_MOTION = window.matchMedia("(prefers-reduced-motion: reduce)"); const MOBILE_NAV = window.matchMedia("(max-width: 920px)"); const CATEGORY_SET = new Set(CATEGORIES); const categoryFromHash = (hash) => { if (hash === "interface" || hash === "dcx-category-interface" || hash.startsWith("dcx-interface-")) return "hooks"; if (hash === "iconography" || hash === "dcx-category-iconography" || hash.startsWith("dcx-iconography-")) return "material"; return CATEGORIES.find((category) => hash === category || hash === `dcx-category-${category}` || hash.startsWith(`dcx-${category}-`)) || ""; }; const INITIAL_HASH = location.hash.slice(1); let pendingInitialCategory = categoryFromHash(INITIAL_HASH); let pendingInitialHash = pendingInitialCategory ? INITIAL_HASH : ""; let documentState = null; let revealFrame = 0; let pendingObserver = null; const slugify = (value) => value .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-|-$/g, ""); const categoryName = (category) => document .querySelector(`.dcx-tile[data-category="${category}"]`) ?.dataset.name || category; const sourceCategory = (category) => category === "components" ? "interface" : category; const templatesReady = () => CATEGORIES.every((category) => document .querySelector(`#dcx-detail-${sourceCategory(category)}`) ?.content.querySelector(".dcx-article")) && Boolean(document.querySelector("#dcx-detail-iconography") ?.content.querySelector(".dcx-article")); const getScrollTargetTop = (state, target) => { if (!target) return 0; const mainRect = state.main.getBoundingClientRect(); return Math.max(0, target.getBoundingClientRect().top - mainRect.top + state.main.scrollTop - 18); }; const targetFromHash = (state, hash, category) => { const article = state.main.querySelector(`:scope > .dcx-article[data-dcx-category="${category}"]`); if (hash === "iconography" || hash === "dcx-category-iconography" || hash.startsWith("dcx-iconography-")) { return state.main.querySelector("#dcx-material-iconography") || article; } if (!hash || hash === category) return article; return state.main.querySelector(`#${CSS.escape(hash)}`) || article; }; const copyText = async (value) => { try { if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(value); return true; } } catch {} const previousFocus = document.activeElement; const textarea = document.createElement("textarea"); textarea.value = value; textarea.setAttribute("readonly", ""); textarea.style.position = "fixed"; textarea.style.opacity = "0"; document.body.appendChild(textarea); textarea.select(); let copied = false; try { copied = document.execCommand("copy"); } catch {} textarea.remove(); if (previousFocus instanceof HTMLElement && previousFocus.isConnected) { previousFocus.focus({ preventScroll: true }); } return copied; }; const initializeColorFans = (root) => { root.querySelectorAll(".dcx-fan:not([data-dcx-document-ready])").forEach((fan) => { const panels = [...fan.querySelectorAll(".dcx-fan-panel")]; if (!panels.length) return; fan.dataset.dcxDocumentReady = "true"; const engage = (activeIndex) => { fan.classList.add("is-engaged"); panels.forEach((panel, index) => { panel.classList.toggle("is-active", index === activeIndex); panel.classList.toggle("is-neighbor", Math.abs(index - activeIndex) === 1); }); }; const reset = () => { fan.classList.remove("is-engaged"); panels.forEach((panel) => panel.classList.remove("is-active", "is-neighbor")); }; fan.addEventListener("mousemove", (event) => { const rect = fan.getBoundingClientRect(); const relativeX = Math.min(0.999, Math.max(0, (event.clientX - rect.left) / rect.width)); let activeIndex = 0; panels.forEach((panel, index) => { const left = Number.parseFloat(panel.style.getPropertyValue("--panel-left")) / 100; if (relativeX >= left) activeIndex = index; }); engage(activeIndex); }); fan.addEventListener("mouseleave", reset); panels.forEach((panel, index) => { panel.addEventListener("focus", () => engage(index)); panel.addEventListener("blur", () => { if (!fan.matches(":focus-within")) reset(); }); panel.addEventListener("click", () => { const color = panel.dataset.copyColor; if (!color) return; copyText(color); const name = panel.querySelector(".dcx-fan-name"); const original = panel.dataset.colorName || "Color"; panel.classList.add("is-copied"); if (name) name.textContent = "Copied!"; window.clearTimeout(panel._dcxCopyTimer); panel._dcxCopyTimer = window.setTimeout(() => { panel.classList.remove("is-copied"); if (name) name.textContent = original; }, 900); }); }); }); }; const createDocumentSection = (label, markup) => { const section = document.createElement("section"); section.className = "dcx-block"; section.dataset.label = label; const blockLabel = document.createElement("span"); blockLabel.className = "dcx-block-label"; blockLabel.textContent = label; const content = document.createElement("template"); content.innerHTML = markup; section.append(blockLabel, content.content); return section; }; const BRAND_PLACEHOLDER_ASSETS = [ { src: "/assets/brand/placeholders/hanazono-primary-mark.png", kind: "logo", title: "Primary mark", alt: "Abstract botanical primary mark in textured gold leaf and patina", width: 768, height: 768, }, { src: "/assets/brand/placeholders/hanazono-atelier-seal.png", kind: "logo", title: "Atelier seal", alt: "Circular floral atelier seal in textured gold leaf and patina", width: 768, height: 768, }, { src: "/assets/brand/placeholders/hanazono-seasonal-moodboard.webp", kind: "moodboard", title: "Seasonal composition", alt: "Editorial moodboard of flowers, handmade paper, gold leaf, and vermilion thread", width: 960, height: 720, }, { src: "/assets/brand/placeholders/hanazono-material-reference.webp", kind: "reference", title: "Material direction", alt: "Material study of lacquer, washi paper, gold leaf, and verdigris patina", width: 960, height: 720, }, ]; const humanizeAssetName = (value, fallback) => { const name = String(value || "") .split(/[\\/]/) .pop() ?.replace(/\.[a-z0-9]+$/i, "") .replace(/[-_]+/g, " ") .trim(); if (!name) return fallback; return name.replace(/\b\w/g, (letter) => letter.toUpperCase()); }; const renderedBrandAssets = (article) => { const assets = []; const collect = (selector, kind, fallback) => { article.querySelectorAll(selector).forEach((figure, index) => { const image = figure.querySelector("img"); if (!image) return; const note = figure.querySelector(".dcx-asset-caption p")?.textContent.trim(); const file = figure.querySelector(".dcx-asset-caption code")?.textContent.trim() || image.getAttribute("src") || ""; const title = note || humanizeAssetName(file, `${fallback} ${index + 1}`); assets.push({ src: image.getAttribute("src") || image.src, kind, title, alt: note || `${title} ${kind === "logo" ? "logo" : "brand image"}`, width: kind === "logo" ? 1200 : 1600, height: kind === "logo" ? 1200 : 1200, }); }); }; collect(':scope > .dcx-block[data-label="Marks"] .dcx-mark', "logo", "Logo"); collect(':scope > .dcx-block[data-label="Boards and references"] .dcx-board', "image", "Brand image"); article.querySelectorAll(':scope > .dcx-block[data-label="Marks"], :scope > .dcx-block[data-label="Boards and references"], :scope > .dcx-block[data-label="Assets provided"]') .forEach((block) => block.remove()); return assets; }; const composeBrandAssetsArticle = (article) => { const uploaded = renderedBrandAssets(article); const assets = uploaded.length ? uploaded : BRAND_PLACEHOLDER_ASSETS; const section = createDocumentSection("Brand assets", ""); section.dataset.dcxBrandAssets = uploaded.length ? "uploaded" : "placeholder"; const grid = document.createElement("ul"); grid.className = "dcx-brand-assets-grid"; grid.setAttribute("aria-label", "Available brand assets"); assets.forEach((asset) => { const item = document.createElement("li"); item.className = "dcx-brand-asset"; item.dataset.assetKind = asset.kind; const figure = document.createElement("figure"); const frame = document.createElement("div"); frame.className = "dcx-brand-asset-frame"; const image = document.createElement("img"); image.src = asset.src.startsWith('/assets/') ? dcxAsset(asset.src) : asset.src; image.alt = asset.alt; image.width = asset.width; image.height = asset.height; image.loading = "lazy"; image.decoding = "async"; frame.appendChild(image); const caption = document.createElement("figcaption"); const title = document.createElement("strong"); title.textContent = asset.title; const kind = document.createElement("span"); kind.textContent = asset.kind === "logo" ? "Logo" : asset.kind === "moodboard" ? "Moodboard" : asset.kind === "reference" ? "Reference" : "Image"; caption.append(title, kind); figure.append(frame, caption); item.appendChild(figure); grid.appendChild(item); }); section.appendChild(grid); article.appendChild(section); const lede = article.querySelector(":scope > header > .dcx-lede"); if (lede) lede.textContent = "Identity, voice, references, taste boundaries, and available assets."; }; const FALLBACK_CARD_IMAGE = "/assets/components/hanazono-ikebana-card.jpg"; const escapeSpecimen = (value) => String(value).replace(/[&<>"]/g, (character) => ( { "&": "&", "<": "<", ">": ">", '"': """ }[character] )); /* The run's own product name, published by the data layer (design-context.js renderDocument). The specimen fields show a real value rather than a label, so a run with no name on record keeps a neutral stand-in. */ const productName = () => { const name = typeof window.dcxProductName === "string" ? window.dcxProductName.trim() : ""; return escapeSpecimen(name || "Untitled project"); }; const productDomain = () => { const name = typeof window.dcxProductName === "string" ? window.dcxProductName : ""; /* The strip leaves only [a-z0-9], so this needs no escaping of its own. */ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, ""); return slug || "example"; }; /* Cue-first card media: the data layer publishes the run's chosen cue on window.dcxCueImageSrc, and the vendored photo is the fallback, swapped in by the delegated error listener when the cue cannot load. A run whose palette named no cue gets the vendored photo directly. */ const componentCardImage = (fallbackAlt, cueAlt) => { const cue = typeof window.dcxCueImageSrc === "string" ? window.dcxCueImageSrc : ""; const fallbackSrc = dcxAsset(FALLBACK_CARD_IMAGE); if (!cue) return `src="${fallbackSrc}" alt="${fallbackAlt}"`; return `src="${cue}" alt="${cueAlt}" data-dcx-swap-src="${fallbackSrc}" data-dcx-swap-alt="${fallbackAlt}"`; }; const composeComponentsArticle = (article) => { article.querySelectorAll(":scope > .dcx-block[data-label]").forEach((block) => block.remove()); const buttons = createDocumentSection("Buttons", `

Variants

Primary Secondary Tertiary Outline Ghost Danger Danger soft

Sizes

Small Medium Large

With icons

Search Add member Email Delete

Icon only

Loading

Uploading…
`); const fields = createDocumentSection("Input fields", `

States

Email address name@example.com Default
Project type Quarterly report Filled
Start date Soon Enter a date in DD/MM/YYYY format.

Sizes

Small Nova
Medium ${productName()}
Large Quarterly planning

With leading content

Search Recent documents
Budget $4,800
Website ${productDomain()}.com

Multiline

Tell us about the project A short brief describing the setting, the audience, and the constraints this piece of work has to hold. 240 characters remaining
`); const cards = createDocumentSection("Cards", `

Content cards

Editorial
Seasonal notes

Materials, timing, and the decisions behind the work.

Read the story
Testimonial
“It fits the way we already work, and it still feels like ours.”
A long-time customer
Details
Project details
Timeline
Six weeks
Setting
Distributed team

With media

Case study
Line, pause, and negative space

A calm composition built around one deliberate gesture.

Material note
Detail, closely cropped

The same image can lead with subject or material detail.

Horizontal

Featured story
A study in asymmetry

How a single focal point, a quiet ground, and generous space hold the composition together.

Explore the study

With actions

Consultation
Plan the first conversation

Share the date and setting so the team can prepare.

Book now
Process guide
Understand the process

Read the stages, timing, and what the team needs from you.

View the guide
`); article.append(buttons, fields, cards); }; const mountArticles = (state) => { const fragment = document.createDocumentFragment(); const documentTitle = document.createElement("h1"); documentTitle.className = "dcx-document-title"; documentTitle.textContent = "Design context"; fragment.appendChild(documentTitle); CATEGORIES.forEach((category) => { const template = document.querySelector(`#dcx-detail-${sourceCategory(category)}`); const clone = template.content.cloneNode(true); const article = clone.querySelector(".dcx-article"); if (!article) return; if (category === "components") { composeComponentsArticle(article); const title = article.querySelector(":scope > header > .dcx-title"); const lede = article.querySelector(":scope > header > .dcx-lede"); if (title) title.textContent = "Components"; if (lede) lede.textContent = "A compact visual inventory of reusable interface patterns."; } if (category === "brand") composeBrandAssetsArticle(article); if (category === "material") { const accessibility = createDocumentSection("Accessibility", `
Contrast on paper
The accent color is reserved for display-scale text. Small links, labels, and inline commands use high-contrast ink on the page ground.
Reading comfort
Body copy is set in the committed body face at a 1.65–1.8 line height and a 65–75ch reading measure.
Keyboard focus
Interactive elements show a visible focus outline with a clear offset.
Texture & text
Text never sits directly on high-contrast texture. Move texture to an edge, or put a solid veil behind the copy.
`); const motion = article.querySelector(':scope > .dcx-block[data-label="Motion per surface"], :scope > .dcx-block[data-label="Motion"]'); const layout = article.querySelector(':scope > .dcx-block[data-label="Layout structure"]'); if (motion) motion.after(accessibility); else if (layout) layout.before(accessibility); else article.appendChild(accessibility); const iconographyArticle = document.querySelector("#dcx-detail-iconography") ?.content.querySelector(".dcx-article"); const library = iconographyArticle ?.querySelector(':scope > .dcx-block[data-label="Library"]') ?.cloneNode(true); if (library) { library.dataset.label = "Iconography"; library.dataset.dcxMovedIconography = "true"; const label = library.querySelector(":scope > .dcx-block-label"); if (label) label.textContent = "Iconography"; article.appendChild(library); } const lede = article.querySelector(":scope > header > .dcx-lede"); if (lede) lede.textContent = "Motion, accessibility, layout structure, boundaries, corners, depth, and iconography."; } article.querySelector(":scope > header > .dcx-eyebrow")?.remove(); article.dataset.dcxCategory = category; article.id = `dcx-category-${category}`; article.classList.add("dcx-document-article"); article.setAttribute("aria-label", categoryName(category)); article.querySelectorAll(".dcx-block[data-label]").forEach((block) => { block.id = `dcx-${category}-${slugify(block.dataset.label || "section")}`; }); fragment.appendChild(article); }); state.main.replaceChildren(fragment); state.main.dataset.dcxContinuous = "true"; state.expander.dataset.dcxDocument = "true"; initializeColorFans(state.main); state.expander.dispatchEvent(new CustomEvent("dcx:document-mounted", { bubbles: true })); }; const audienceSubnav = (article) => { const blocks = [...article.querySelectorAll(":scope > .dcx-block[data-label]")]; const byLabel = new Map(blocks.map((block) => [block.dataset.label, block])); return [ { label: "People", target: byLabel.get("Who they are") }, { label: "Decision factors", target: byLabel.get("Needs") || byLabel.get("Trust triggers") }, { label: "Inclusion", target: byLabel.get("Who must not be excluded") }, ].filter((item) => item.target); }; const genericSubnav = (article) => [...article.querySelectorAll(":scope > .dcx-block[data-label]")] .map((block) => ({ label: block.dataset.label, target: block })); const renderSubnav = (state, category, article) => { const descriptors = category === "audience" ? audienceSubnav(article) : genericSubnav(article); const activeItem = state.nav.querySelector(`.dcx-nav-list > li[data-category="${category}"]`); const subnav = activeItem?.querySelector(":scope > .dcx-subnav"); if (!subnav) return; const signature = descriptors.map(({ label, target }) => `${label}:${target.id}`).join("|"); const measure = () => { const links = [...subnav.querySelectorAll(":scope > .dcx-sub-link")]; const heights = links.map((link) => link.getBoundingClientRect().height); if (!heights.some((height) => height > 0)) return; const summary = category === "audience"; const gap = summary ? 0 : 2; const padding = summary ? 14 : 10; const height = heights.reduce((total, value) => total + value, 0) + Math.max(0, links.length - 1) * gap + padding; subnav.style.setProperty("--dcx-subnav-height", `${Math.ceil(height)}px`); }; if (subnav.dataset.dcxDocumentSignature === signature) { measure(); return; } const links = descriptors.map(({ label, target }) => { const link = document.createElement("a"); link.className = "dcx-sub-link"; link.href = `#${target.id}`; link.textContent = label; link.dataset.dcxDocumentTarget = target.id; if (category === "audience") link.dataset.dcxRailSummary = ""; return link; }); subnav.replaceChildren(...links); subnav.dataset.dcxDocumentSignature = signature; subnav.classList.toggle("dcx-subnav--summary", category === "audience"); measure(); }; const prepareSubnavs = (state) => { CATEGORIES.forEach((category) => { const article = state.main.querySelector(`:scope > .dcx-article[data-dcx-category="${category}"]`); if (article) renderSubnav(state, category, article); }); }; const setActiveCategory = (state, category, { updateHash = false } = {}) => { if (!CATEGORY_SET.has(category)) return; const article = state.main.querySelector(`:scope > .dcx-article[data-dcx-category="${category}"]`); if (!article) return; const previousCategory = state.currentCategory; if (previousCategory === category) { renderSubnav(state, category, article); state.expander.querySelector(".dcx-current").textContent = categoryName(category); return; } if (previousCategory) { state.expander.dispatchEvent(new CustomEvent("dcx:continuouscategorywillchange", { bubbles: true, detail: { from: previousCategory, to: category, source: state.pinnedCategory === category ? "navigation" : "scroll", }, })); } state.currentCategory = category; state.expander.dataset.dcxCurrentCategory = category; state.expander.querySelector(".dcx-current").textContent = categoryName(category); state.nav.querySelectorAll(".dcx-nav-list > li[data-category]").forEach((item) => { const active = item.dataset.category === category; item.classList.toggle("is-active", active); const link = item.querySelector(":scope > .dcx-nav-link"); const subnav = item.querySelector(":scope > .dcx-subnav"); if (active) { link?.setAttribute("aria-current", "page"); link?.setAttribute("aria-expanded", "true"); subnav?.removeAttribute("aria-hidden"); if (subnav) subnav.inert = false; } else { link?.removeAttribute("aria-current"); link?.removeAttribute("aria-expanded"); subnav?.setAttribute("aria-hidden", "true"); if (subnav) subnav.inert = true; } }); renderSubnav(state, category, article); if (MOBILE_NAV.matches) { const activeItem = state.nav.querySelector(`.dcx-nav-list > li[data-category="${category}"]`); requestAnimationFrame(() => { activeItem?.scrollIntoView({ behavior: REDUCED_MOTION.matches ? "auto" : "smooth", block: "nearest", inline: "nearest", }); }); } state.expander.dispatchEvent(new CustomEvent("dcx:continuouscategorychange", { bubbles: true, detail: { category, article }, })); if (updateHash) { history.replaceState({ category }, "", `#${category}`); } }; const findVisibleCategory = (state) => { const articles = [...state.main.querySelectorAll(":scope > .dcx-article[data-dcx-category]")]; if (!articles.length) return "audience"; if (state.pinnedCategory) return state.pinnedCategory; const mainRect = state.main.getBoundingClientRect(); const marker = mainRect.top + state.main.clientHeight * 0.24; let visible = articles[0]; articles.forEach((article) => { if (article.getBoundingClientRect().top <= marker) visible = article; }); if (state.main.scrollTop + state.main.clientHeight >= state.main.scrollHeight - 4) { visible = articles.at(-1); } return visible.dataset.dcxCategory; }; const handleMainScroll = (state) => { if (state.scrollFrame) return; state.scrollFrame = requestAnimationFrame(() => { state.scrollFrame = 0; const category = findVisibleCategory(state); if (category !== state.currentCategory) setActiveCategory(state, category, { updateHash: true }); }); }; const releasePinnedCategory = (state) => { state.pinnedCategory = ""; window.clearTimeout(state.pinTimer); }; const scrollToTarget = (state, target, category, { instant = false, historyMode = "push", hash = category, } = {}) => { if (!target || !CATEGORY_SET.has(category)) return; state.pinnedCategory = category; setActiveCategory(state, category); const top = target === state.main.firstElementChild ? 0 : getScrollTargetTop(state, target); state.main.scrollTo({ top, behavior: instant || REDUCED_MOTION.matches ? "auto" : "smooth", }); if (historyMode === "push") history.pushState({ category }, "", `#${hash}`); if (historyMode === "replace") history.replaceState({ category }, "", `#${hash}`); window.clearTimeout(state.pinTimer); state.pinTimer = window.setTimeout(() => { state.pinnedCategory = ""; handleMainScroll(state); }, instant ? 80 : 760); }; const scrollToCategory = (state, category, options = {}) => { const article = state.main.querySelector(`:scope > .dcx-article[data-dcx-category="${category}"]`); scrollToTarget(state, article, category, options); }; const handleNavigation = (state, event) => { const categoryLink = event.target.closest(".dcx-nav-link[data-dcx-nav]"); if (categoryLink && state.nav.contains(categoryLink)) { event.preventDefault(); const category = categoryLink.dataset.dcxNav; scrollToCategory(state, category); return; } const subsection = event.target.closest(".dcx-sub-link[data-dcx-document-target]"); if (!subsection || !state.nav.contains(subsection)) return; event.preventDefault(); const target = state.main.querySelector(`#${CSS.escape(subsection.dataset.dcxDocumentTarget)}`); scrollToTarget(state, target, state.currentCategory, { historyMode: "replace", hash: subsection.dataset.dcxDocumentTarget, }); }; const closeDocument = (state, { updateHistory = true } = {}) => { if (!state || state.closing) return; window.closeDcxSettings?.({ restoreFocus: false, instant: true }); state.closing = true; state.expander.dataset.dcxClosing = "true"; releasePinnedCategory(state); if (state.scrollFrame) cancelAnimationFrame(state.scrollFrame); if (state.contentFrame) cancelAnimationFrame(state.contentFrame); if (state.layoutFrame) cancelAnimationFrame(state.layoutFrame); state.contentObserver?.disconnect(); state.layoutObserver?.disconnect(); state.main.removeEventListener("scroll", state.onScroll); state.main.removeEventListener("wheel", state.onManualScroll); state.main.removeEventListener("touchstart", state.onManualScroll); state.main.removeEventListener("pointerdown", state.onManualScroll); state.nav.removeEventListener("click", state.onNavClick); const tile = document.querySelector(`.dcx-tile[data-category="${state.currentCategory}"]`) || state.opener; const rect = tile?.getBoundingClientRect() || state.originRect; state.expander.classList.remove("is-ready", "is-full"); state.expander.style.top = `${rect.top}px`; state.expander.style.left = `${rect.left}px`; state.expander.style.width = `${rect.width}px`; state.expander.style.height = `${rect.height}px`; if (updateHistory) { history.pushState(null, "", `${location.pathname}${location.search}`); } const finish = () => { state.expander.remove(); document.body.classList.remove("is-locked"); if (state.background) { state.background.inert = false; state.background.removeAttribute("aria-hidden"); } state.opener?.focus({ preventScroll: true }); if (documentState === state) documentState = null; }; if (REDUCED_MOTION.matches) finish(); else state.closeTimer = window.setTimeout(finish, 540); }; const openDocument = (category, { historyMode = "push", opener = null, targetHash = category } = {}) => { if (!CATEGORY_SET.has(category) || !templatesReady()) return false; if (documentState) { scrollToCategory(documentState, category, { historyMode }); return true; } const sourceTile = opener || document.querySelector(`.dcx-tile[data-category="${category}"]`); const shellTemplate = document.querySelector("#dcx-shell-template"); if (!sourceTile || !shellTemplate) return false; const rect = sourceTile.getBoundingClientRect(); const expander = document.createElement("section"); expander.className = "dcx-expander"; expander.dataset.dcxDocument = "true"; expander.setAttribute("role", "dialog"); expander.setAttribute("aria-modal", "true"); expander.setAttribute("aria-label", "Design context document"); expander.style.top = `${rect.top}px`; expander.style.left = `${rect.left}px`; expander.style.width = `${rect.width}px`; expander.style.height = `${rect.height}px`; expander.appendChild(shellTemplate.content.cloneNode(true)); const background = document.querySelector("[data-dcx-shell]"); if (background) { background.inert = true; background.setAttribute("aria-hidden", "true"); } document.body.appendChild(expander); document.body.classList.add("is-locked"); const state = { expander, main: expander.querySelector(".dcx-main"), nav: expander.querySelector(".dcx-nav"), opener: sourceTile, originRect: rect, background, currentCategory: "", pinnedCategory: category, pinTimer: 0, scrollFrame: 0, contentFrame: 0, layoutFrame: 0, initialCategory: category, initialHash: targetHash, settleUntil: performance.now() + 3200, userInteracted: false, closing: false, }; documentState = state; mountArticles(state); prepareSubnavs(state); state.onScroll = () => handleMainScroll(state); state.onManualScroll = () => { state.userInteracted = true; state.layoutObserver?.disconnect(); releasePinnedCategory(state); }; state.onNavClick = (event) => { state.userInteracted = true; handleNavigation(state, event); }; state.contentObserver = new MutationObserver(() => { if (state.contentFrame) return; state.contentFrame = requestAnimationFrame(() => { state.contentFrame = 0; initializeColorFans(state.main); setActiveCategory(state, state.currentCategory); if (!state.userInteracted && performance.now() < state.settleUntil) { const target = targetFromHash(state, state.initialHash, state.initialCategory); scrollToTarget(state, target, state.initialCategory, { instant: true, historyMode: "replace", hash: state.initialHash, }); } }); }); state.contentObserver.observe(state.main, { childList: true, subtree: true, attributes: true, attributeFilter: ["id", "data-dcx-audience-enhanced", "data-dcx-detail-enhanced"], }); state.layoutObserver = new ResizeObserver(() => { if (state.layoutFrame || state.userInteracted || performance.now() >= state.settleUntil) return; state.layoutFrame = requestAnimationFrame(() => { state.layoutFrame = 0; const target = targetFromHash(state, state.initialHash, state.initialCategory); scrollToTarget(state, target, state.initialCategory, { instant: true, historyMode: "replace", hash: state.initialHash, }); }); }); state.main.querySelectorAll(":scope > .dcx-article[data-dcx-category]").forEach((article) => { state.layoutObserver.observe(article); }); state.main.addEventListener("scroll", state.onScroll, { passive: true }); state.main.addEventListener("wheel", state.onManualScroll, { passive: true }); state.main.addEventListener("touchstart", state.onManualScroll, { passive: true }); state.main.addEventListener("pointerdown", state.onManualScroll, { passive: true }); state.nav.addEventListener("click", state.onNavClick); expander.querySelector(".dcx-close")?.addEventListener("click", () => closeDocument(state)); setActiveCategory(state, category); if (historyMode === "push") history.pushState({ category }, "", `#${targetHash}`); if (historyMode === "replace") history.replaceState({ category }, "", `#${targetHash}`); requestAnimationFrame(() => { expander.classList.add("is-full"); const target = targetFromHash(state, targetHash, category); scrollToTarget(state, target, category, { instant: true, historyMode: "none", hash: targetHash }); window.setTimeout(() => { expander.classList.add("is-ready"); expander.querySelector(".dcx-close")?.focus({ preventScroll: true }); }, REDUCED_MOTION.matches ? 0 : 260); }); [180, 520, 1100, 1800, 3000].forEach((delay) => { window.setTimeout(() => { if (documentState === state && !state.userInteracted) { const target = targetFromHash(state, state.initialHash, category); scrollToTarget(state, target, category, { instant: true, historyMode: "replace", hash: state.initialHash, }); } }, delay); }); return true; }; const handleTileClick = (event) => { const tile = event.target.closest(".dcx-tile[data-category]"); if (!tile || !document.querySelector("[data-dcx-shell]")?.contains(tile)) return; if (!CATEGORY_SET.has(tile.dataset.category)) return; if (!templatesReady()) return; event.preventDefault(); event.stopImmediatePropagation(); openDocument(tile.dataset.category, { opener: tile }); }; const handleKeydown = (event) => { if (event.key === "Escape" && document.querySelector("#dcx-context-settings[open]")) return; if (event.key === "Escape" && documentState) { event.preventDefault(); event.stopImmediatePropagation(); closeDocument(documentState); return; } if (documentState && ["ArrowDown", "ArrowUp", "PageDown", "PageUp", "Home", "End", " "].includes(event.key)) { documentState.userInteracted = true; releasePinnedCategory(documentState); } }; const handlePopstate = (event) => { const hash = location.hash.slice(1); const category = categoryFromHash(hash); if (documentState?.closing && category) { event.stopImmediatePropagation(); window.closeDcxSettings?.({ restoreFocus: false, instant: true }); const closingState = documentState; window.clearTimeout(closingState.closeTimer); closingState.expander.remove(); if (closingState.background) { closingState.background.inert = false; closingState.background.removeAttribute("aria-hidden"); } document.body.classList.remove("is-locked"); documentState = null; openDocument(category, { historyMode: "none", targetHash: hash }); return; } if (!documentState) { if (!category) { pendingInitialCategory = ""; pendingInitialHash = ""; return; } event.stopImmediatePropagation(); if (templatesReady()) { openDocument(category, { historyMode: "none", targetHash: hash }); } else { pendingInitialCategory = category; pendingInitialHash = hash; if (!pendingObserver) { pendingObserver = new MutationObserver(schedulePendingDocument); pendingObserver.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["class", "hidden"], }); } schedulePendingDocument(); } return; } event.stopImmediatePropagation(); if (category) { const target = targetFromHash(documentState, hash, category); scrollToTarget(documentState, target, category, { historyMode: "none", hash }); } else { closeDocument(documentState, { updateHistory: false }); } }; const openPendingDocument = () => { revealFrame = 0; if (!pendingInitialCategory) { if (document.body.classList.contains("dcx-open") && templatesReady()) { pendingObserver?.disconnect(); pendingObserver = null; } return; } if (documentState) return; const shell = document.querySelector("[data-dcx-shell]"); if (!document.body.classList.contains("dcx-open") || shell?.hidden || !templatesReady()) return; const category = pendingInitialCategory; const targetHash = pendingInitialHash || category; if (openDocument(category, { historyMode: "replace", targetHash })) { pendingInitialCategory = ""; pendingInitialHash = ""; pendingObserver?.disconnect(); pendingObserver = null; } }; const schedulePendingDocument = () => { if (revealFrame) return; revealFrame = requestAnimationFrame(openPendingDocument); }; const handleCommandCopy = async (event) => { const button = event.target.closest?.("[data-dcx-copy-command]"); if (!button) return; const command = button.dataset.dcxCopyCommand; const status = button.closest("[data-dcx-command-context]") ?.querySelector(".dcx-command-status"); const copied = await copyText(command); window.clearTimeout(button._dcxCopyTimer); button.classList.remove("copied"); void button.offsetWidth; button.classList.toggle("copied", copied); button.dataset.copied = String(copied); if (status) status.textContent = copied ? "Command copied." : "The command could not be copied."; button._dcxCopyTimer = window.setTimeout(() => { button.classList.remove("copied"); button.removeAttribute("data-copied"); if (status) status.textContent = ""; }, 1200); }; /* Integration hook: the data layer re-renders the detail templates when the store moves (a request the agent finished, a value it settled). The mounted document is built from clones, so it re-mounts here, holding the reader's place. Enhancers re-run on their own observers; scroll is restored after two frames so their layout has landed. */ const remountDocument = () => { if (!documentState || documentState.closing) return; const state = documentState; const category = state.currentCategory || state.initialCategory; const scrollTop = state.main.scrollTop; state.userInteracted = true; releasePinnedCategory(state); mountArticles(state); prepareSubnavs(state); state.currentCategory = ""; setActiveCategory(state, category); requestAnimationFrame(() => { requestAnimationFrame(() => { state.main.scrollTop = scrollTop; }); }); }; window.dcxDocument = { isOpen: () => Boolean(documentState), currentCategory: () => documentState?.currentCategory || "", remount: remountDocument, }; if (pendingInitialCategory) { history.replaceState(history.state, "", `${location.pathname}${location.search}`); } document.addEventListener("click", handleTileClick, true); document.addEventListener("click", handleCommandCopy); document.addEventListener("keydown", handleKeydown, true); window.addEventListener("popstate", handlePopstate, true); pendingObserver = new MutationObserver(schedulePendingDocument); pendingObserver.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["class", "hidden"] }); schedulePendingDocument(); })();