From 734ffd3208302cfe87a874391fcdebf6e7f34ebf Mon Sep 17 00:00:00 2001 From: Andrei Tuicu Date: Tue, 8 Sep 2026 18:26:15 +0200 Subject: [PATCH 1/5] feat: Port the Page Evaluation Coworker custom renderer --- nx2/blocks/chat-ao/artifacts/index.js | 1 + .../chat-ao/artifacts/page-evaluation.css | 268 ++++++++++++ .../chat-ao/artifacts/page-evaluation.js | 397 ++++++++++++++++++ 3 files changed, 666 insertions(+) create mode 100644 nx2/blocks/chat-ao/artifacts/page-evaluation.css create mode 100644 nx2/blocks/chat-ao/artifacts/page-evaluation.js diff --git a/nx2/blocks/chat-ao/artifacts/index.js b/nx2/blocks/chat-ao/artifacts/index.js index 164ee7f4c..ac382c473 100644 --- a/nx2/blocks/chat-ao/artifacts/index.js +++ b/nx2/blocks/chat-ao/artifacts/index.js @@ -6,6 +6,7 @@ import './column.js'; import './card.js'; import './data-table.js'; import './metric-card.js'; +import './page-evaluation.js'; import './code-block.js'; import './alert.js'; diff --git a/nx2/blocks/chat-ao/artifacts/page-evaluation.css b/nx2/blocks/chat-ao/artifacts/page-evaluation.css new file mode 100644 index 000000000..b83dd0091 --- /dev/null +++ b/nx2/blocks/chat-ao/artifacts/page-evaluation.css @@ -0,0 +1,268 @@ +.ui-artifact-pe { + display: flex; + flex-direction: column; + gap: var(--s2-spacing-300); +} + +.ui-artifact-pe-title { + margin: 0; + font-size: var(--s2-component-l-bold-font-size); + font-weight: var(--s2-component-l-bold-font-weight); + line-height: var(--s2-component-l-bold-line-height); + color: var(--s2-gray-900); +} + +.ui-artifact-pe-icon { + display: inline-block; + flex-shrink: 0; + background-color: currentcolor; + mask-image: var(--pe-icon); + mask-size: contain; + mask-repeat: no-repeat; + mask-position: center; +} + +/* --- tone text colors --- */ + +.ui-artifact-pe-tone-negative { color: var(--s2-orange-900); } +.ui-artifact-pe-tone-positive { color: var(--s2-green-900); } +.ui-artifact-pe-tone-neutral { color: var(--s2-gray-800); } + +/* --- tone chip backgrounds --- */ + +.ui-artifact-pe-tone-negative-bg { + background: var(--s2-orange-100); + color: var(--s2-orange-900); +} + +.ui-artifact-pe-tone-positive-bg { + background: var(--s2-green-100); + color: var(--s2-green-900); +} + +.ui-artifact-pe-tone-neutral-bg { + background: var(--s2-gray-100); + color: var(--s2-gray-800); +} + +/* --- summary tiles --- */ + +.ui-artifact-pe-summary { + display: flex; + border: 1px solid var(--s2-gray-200); + border-radius: var(--s2-corner-radius-300); + overflow: hidden; +} + +.ui-artifact-pe-tile { + flex: 1; + display: flex; + flex-direction: column; + gap: var(--s2-spacing-75); + padding: var(--s2-spacing-300); + + & + & { + border-left: 1px solid var(--s2-gray-200); + } +} + +.ui-artifact-pe-tile-label { + font-size: var(--s2-body-size-xs); + color: var(--s2-gray-800); +} + +.ui-artifact-pe-tile-value { + font-size: var(--s2-heading-size-s); + font-weight: var(--s2-heading-font-weight); + line-height: var(--s2-heading-line-height); +} + +.ui-artifact-pe-tile-desc { + font-size: var(--s2-body-size-xs); + color: var(--s2-gray-600); +} + +/* --- collapsible sections --- */ + +.ui-artifact-pe-section { + border: 1px solid var(--s2-gray-200); + border-radius: var(--s2-corner-radius-300); + overflow: hidden; +} + +.ui-artifact-pe-section-header { + display: flex; + align-items: center; + gap: var(--s2-spacing-200); + padding: var(--s2-spacing-200) var(--s2-spacing-300); + cursor: pointer; + user-select: none; + list-style: none; + + &::marker, + &::-webkit-details-marker { + display: none; + } +} + +.ui-artifact-pe-section-chip { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border-radius: var(--s2-corner-radius-300); + flex-shrink: 0; +} + +.ui-artifact-pe-section-labels { + flex: 1; + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.ui-artifact-pe-section-label { + font-size: var(--s2-body-size-s); + font-weight: bold; + color: var(--s2-gray-900); +} + +.ui-artifact-pe-section-sublabel { + font-size: var(--s2-body-size-xs); + color: var(--s2-gray-600); +} + +.ui-artifact-pe-chevron { + width: 16px; + height: 16px; + flex-shrink: 0; + background-color: var(--s2-gray-600); + mask-image: url("/img/icons/s2-icon-chevronup-20-n.svg"); + mask-size: contain; + mask-repeat: no-repeat; + mask-position: center; + transform: rotate(180deg); + transition: transform 0.15s ease; +} + +.ui-artifact-pe-section[open] > .ui-artifact-pe-section-header > .ui-artifact-pe-chevron { + transform: rotate(0deg); +} + +/* --- item rows --- */ + +.ui-artifact-pe-items { + border-top: 1px solid var(--s2-gray-200); +} + +.ui-artifact-pe-item { + display: flex; + align-items: flex-start; + gap: var(--s2-spacing-100); + padding: var(--s2-spacing-200) var(--s2-spacing-300); + + &[hidden] { + display: none; + } +} + +.ui-artifact-pe-item-body { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: var(--s2-spacing-50); +} + +.ui-artifact-pe-item-title { + font-size: var(--s2-body-size-s); + font-weight: bold; + color: var(--s2-gray-900); +} + +.ui-artifact-pe-item-desc { + font-size: var(--s2-body-size-xs); + color: var(--s2-gray-600); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ui-artifact-pe-item-action { + flex-shrink: 0; + align-self: center; + font-size: var(--s2-body-size-xs); + font-weight: 500; + color: var(--s2-gray-900); + background: none; + border: 1px solid var(--s2-gray-300); + border-radius: var(--s2-corner-radius-200); + padding: var(--s2-spacing-50) var(--s2-spacing-100); + cursor: pointer; + white-space: nowrap; + + &:hover { + background: var(--s2-gray-100); + } +} + +/* --- pagination --- */ + +.ui-artifact-pe-paging { + display: flex; + align-items: center; + justify-content: center; + gap: var(--s2-spacing-100); + padding: var(--s2-spacing-100) var(--s2-spacing-300) var(--s2-spacing-200); +} + +.ui-artifact-pe-paging-label { + font-size: var(--s2-body-size-xs); + font-weight: bold; + color: var(--s2-gray-800); +} + +.ui-artifact-pe-paging-btn { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border: none; + border-radius: var(--s2-corner-radius-200); + background: none; + cursor: pointer; + color: var(--s2-gray-600); + padding: 0; + + &:disabled { + opacity: 0.4; + cursor: default; + } + + &:hover:not(:disabled) { + background: var(--s2-gray-100); + } + + &::after { + content: ""; + display: block; + width: 14px; + height: 14px; + background-color: currentcolor; + mask-image: url("/img/icons/s2-icon-chevronup-20-n.svg"); + mask-size: contain; + mask-repeat: no-repeat; + mask-position: center; + } +} + +.ui-artifact-pe-paging-prev::after { + transform: rotate(-90deg); +} + +.ui-artifact-pe-paging-next::after { + transform: rotate(90deg); +} diff --git a/nx2/blocks/chat-ao/artifacts/page-evaluation.js b/nx2/blocks/chat-ao/artifacts/page-evaluation.js new file mode 100644 index 000000000..2e90632ea --- /dev/null +++ b/nx2/blocks/chat-ao/artifacts/page-evaluation.js @@ -0,0 +1,397 @@ +import { html, nothing } from 'da-lit'; +import { registerArtifact } from './registry.js'; + +const PAGE_SIZE = 5; + +const TONE_MAP = { + negative: { cls: 'negative', icon: 's2-icon-closecircle-20-n' }, + positive: { cls: 'positive', icon: 's2-icon-checkmarkcircle-20-n' }, + neutral: { cls: 'neutral', icon: 's2-icon-removecircle-20-n' }, +}; +const TONE_FALLBACK = { cls: 'informative', icon: 's2-icon-infocircle-20-n' }; + +const toneOf = (tone) => TONE_MAP[tone] ?? TONE_FALLBACK; + +const iconUrl = (name) => `/img/icons/${name}.svg`; + +const toneIcon = (tone, size = 20) => { + const { icon } = toneOf(tone); + return html``; +}; + +/* ---- lazy CSS adoption ---- */ + +const PE_STYLE_URL = new URL('./page-evaluation.css', import.meta.url).href; +let styleSheet; +let styleAdopted; + +function adoptStyle(host) { + if (styleAdopted) return; + styleAdopted = true; + (async () => { + // eslint-disable-next-line import/no-unresolved + const { loadStyle } = await import('../../../utils/utils.js'); + styleSheet = await loadStyle(PE_STYLE_URL); + const root = host?.getRootNode?.(); + if (root?.adoptedStyleSheets && !root.adoptedStyleSheets.includes(styleSheet)) { + root.adoptedStyleSheets = [...root.adoptedStyleSheets, styleSheet]; + } + })(); +} + +/* ---- summary ---- */ + +function renderTile({ label = '', value = '', description = '', tone }) { + const { cls } = toneOf(tone); + return html` +
+ ${label} + ${value} + ${description ? html`${description}` : nothing} +
+ `; +} + +function renderSummary(summary) { + return html` +
${summary.map(renderTile)}
+ `; +} + +/* ---- inline pagination ---- */ + +function updatePage(container, page) { + const items = container.querySelectorAll('.ui-artifact-pe-item'); + const total = items.length; + const start = page * PAGE_SIZE; + const end = Math.min(start + PAGE_SIZE, total); + items.forEach((row, i) => { row.hidden = i < start || i >= end; }); + const label = container.querySelector('.ui-artifact-pe-paging-label'); + if (label) label.textContent = `${end} of ${total} checks`; + const prev = container.querySelector('.ui-artifact-pe-paging-prev'); + const next = container.querySelector('.ui-artifact-pe-paging-next'); + if (prev) prev.disabled = page === 0; + if (next) next.disabled = end >= total; + container.dataset.page = page; +} + +const onPage = (dir) => (e) => { + const container = e.target.closest('.ui-artifact-pe-items'); + const page = Number(container.dataset.page) + dir; + const total = container.querySelectorAll('.ui-artifact-pe-item').length; + if (page < 0 || page * PAGE_SIZE >= total) return; + updatePage(container, page); +}; + +function renderPaging(total) { + const shown = Math.min(PAGE_SIZE, total); + return html` +
+ + ${shown} of ${total} checks + +
+ `; +} + +/* ---- modals ---- */ + +const MODAL_STYLES = ` + .pe-modal-content { font-family: var(--s2-font-family, 'Adobe Clean', adobe-clean, sans-serif); } + .pe-modal-asset-label { font-size: 18px; font-weight: bold; color: #222; margin: 0 0 16px; } + .pe-modal-section { margin-bottom: 20px; } + .pe-modal-heading { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; } + .pe-modal-heading-icon { + width: 20px; height: 20px; flex-shrink: 0; + background-color: currentcolor; + mask-size: contain; mask-repeat: no-repeat; mask-position: center; + } + .pe-modal-heading-icon.issue { color: #c05621; mask-image: url('/img/icons/s2-icon-closecircle-20-n.svg'); } + .pe-modal-heading-icon.fix { color: #107c41; mask-image: url('/img/icons/s2-icon-checkmarkcircle-20-n.svg'); } + .pe-modal-heading-text { font-weight: bold; font-size: 14px; color: #222; } + .pe-modal-body { font-size: 14px; line-height: 1.6; color: #4b4b4b; margin: 0; } + .pe-modal-copy { + display: inline-flex; align-items: center; gap: 6px; + font-size: 13px; color: #4b4b4b; background: none; border: none; + cursor: pointer; padding: 4px 0; margin-top: 4px; + } + .pe-modal-copy:hover { color: #222; } + .pe-modal-context-title { font-size: 16px; font-weight: bold; color: #222; margin: 24px 0 12px; } + .pe-modal-context-card { + border: 1px solid #ddd; border-radius: 12px; padding: 16px; + } + .pe-modal-chip { + display: inline-block; background: #f0f0f0; border-radius: 4px; + padding: 2px 10px; font-size: 12px; color: #4b4b4b; margin-bottom: 12px; + } + .pe-modal-context-desc { font-size: 14px; line-height: 1.6; color: #4b4b4b; margin: 0; } + .pe-modal-paging { + display: flex; justify-content: center; align-items: center; gap: 12px; + margin-top: 20px; padding-top: 16px; border-top: 1px solid #eee; + } + .pe-modal-paging-btn { + display: flex; align-items: center; justify-content: center; + width: 28px; height: 28px; border: none; border-radius: 6px; + background: none; cursor: pointer; color: #666; padding: 0; + } + .pe-modal-paging-btn:hover:not(:disabled) { background: #f0f0f0; } + .pe-modal-paging-btn:disabled { opacity: 0.3; cursor: default; } + .pe-modal-paging-btn::after { + content: ""; display: block; width: 16px; height: 16px; + background-color: currentcolor; + mask-image: url('/img/icons/s2-icon-chevronup-20-n.svg'); + mask-size: contain; mask-repeat: no-repeat; mask-position: center; + } + .pe-modal-paging-prev::after { transform: rotate(-90deg); } + .pe-modal-paging-next::after { transform: rotate(90deg); } + .pe-modal-paging-label { font-size: 13px; font-weight: bold; color: #444; } +`; + +function el(tag, cls, text) { + const node = document.createElement(tag); + if (cls) node.className = cls; + if (text) node.textContent = text; + return node; +} + +function buildContextCard(context) { + if (!context) return null; + const card = el('div', 'pe-modal-context-card'); + if (context.category) card.append(el('span', 'pe-modal-chip', context.category)); + if (context.description) card.append(el('p', 'pe-modal-context-desc', context.description)); + return card; +} + +function buildSuggestionBody(item) { + const frag = document.createDocumentFragment(); + const { suggestion } = item; + if (!suggestion) return frag; + + if (suggestion.label) frag.append(el('h3', 'pe-modal-asset-label', suggestion.label)); + + const issueSection = el('div', 'pe-modal-section'); + const issueHeading = el('div', 'pe-modal-heading'); + const issueIcon = el('span', 'pe-modal-heading-icon issue'); + issueHeading.append(issueIcon, el('span', 'pe-modal-heading-text', 'Issue identified')); + issueSection.append(issueHeading); + if (suggestion.issue) issueSection.append(el('p', 'pe-modal-body', suggestion.issue)); + frag.append(issueSection); + + const fixSection = el('div', 'pe-modal-section'); + const fixHeading = el('div', 'pe-modal-heading'); + const fixIcon = el('span', 'pe-modal-heading-icon fix'); + fixHeading.append(fixIcon, el('span', 'pe-modal-heading-text', 'Suggested change')); + fixSection.append(fixHeading); + if (suggestion.suggested) fixSection.append(el('p', 'pe-modal-body', suggestion.suggested)); + + const copyBtn = el('button', 'pe-modal-copy', '📋 Copy suggestion'); + copyBtn.addEventListener('click', () => { + navigator.clipboard.writeText(suggestion.suggested ?? ''); + }); + fixSection.append(copyBtn); + frag.append(fixSection); + + if (suggestion.context) { + frag.append(el('div', 'pe-modal-context-title', 'Context')); + const card = buildContextCard(suggestion.context); + if (card) frag.append(card); + } + + return frag; +} + +function buildCheckBody(item) { + const frag = document.createDocumentFragment(); + const { check } = item; + if (!check) return frag; + + if (check.label) frag.append(el('h3', 'pe-modal-asset-label', check.label)); + + if (item.description) { + const section = el('div', 'pe-modal-section'); + section.append(el('p', 'pe-modal-body', item.description)); + frag.append(section); + } + + if (check.context) { + frag.append(el('div', 'pe-modal-context-title', 'Context')); + const card = buildContextCard(check.context); + if (card) frag.append(card); + } + + return frag; +} + +function openModal({ componentTitle, items, startIndex, sectionLabel, buildBody }) { + import('../../shared/dialog/dialog.js').then(() => { + const navigable = items; + const total = navigable.length; + if (!total) return; + + let current = Math.min(startIndex, total - 1); + const dialog = document.createElement('nx-dialog'); + dialog.title = componentTitle || 'Page evaluation'; + dialog.style.setProperty('--nx-dialog-max-width', '600px'); + + const styleEl = document.createElement('style'); + styleEl.textContent = MODAL_STYLES; + + const contentWrap = el('div', 'pe-modal-content'); + const bodyWrap = el('div', 'pe-modal-body-wrap'); + + const pagingLabel = el('span', 'pe-modal-paging-label'); + const prevBtn = el('button', 'pe-modal-paging-btn pe-modal-paging-prev'); + const nextBtn = el('button', 'pe-modal-paging-btn pe-modal-paging-next'); + + function render() { + bodyWrap.replaceChildren(buildBody(navigable[current])); + pagingLabel.textContent = `${current + 1} of ${total} ${sectionLabel}`; + prevBtn.disabled = current === 0; + nextBtn.disabled = current >= total - 1; + } + + prevBtn.addEventListener('click', () => { + if (current > 0) { + current -= 1; + render(); + } + }); + nextBtn.addEventListener('click', () => { + if (current < total - 1) { + current += 1; + render(); + } + }); + + const pagingRow = el('div', 'pe-modal-paging'); + pagingRow.append(prevBtn, pagingLabel, nextBtn); + + contentWrap.append(bodyWrap, pagingRow); + dialog.append(styleEl, contentWrap); + dialog.addEventListener('close', () => dialog.remove()); + render(); + document.body.append(dialog); + }); +} + +function openSuggestionModal(componentTitle, items, startIndex, sectionLabel) { + const suggestItems = items.filter((it) => it.suggestion); + const idx = suggestItems.indexOf(items[startIndex]); + openModal({ + componentTitle, + items: suggestItems, + startIndex: idx >= 0 ? idx : 0, + sectionLabel, + buildBody: buildSuggestionBody, + }); +} + +function openCheckModal(componentTitle, items, startIndex, sectionLabel) { + const checkItems = items.filter((it) => it.check); + const idx = checkItems.indexOf(items[startIndex]); + openModal({ + componentTitle, + items: checkItems, + startIndex: idx >= 0 ? idx : 0, + sectionLabel, + buildBody: buildCheckBody, + }); +} + +/* ---- item rows ---- */ + +function renderItem(item, tone, hidden, onAction) { + return html` +
+ ${toneIcon(tone)} +
+ ${item.title ?? ''} + ${item.description + ? html`${item.description}` + : nothing} +
+ ${item.suggestion ? html` + + ` : nothing} + ${!item.suggestion && item.check ? html` + + ` : nothing} +
+ `; +} + +/* ---- section ---- */ + +function sectionLabelFromTone(tone) { + if (tone === 'negative') return 'failures'; + if (tone === 'positive') return 'passes'; + return 'checks'; +} + +function renderItems(items, tone, componentTitle) { + const total = items.length; + const label = sectionLabelFromTone(tone); + + return html` +
+ ${items.map((item, i) => renderItem( + item, + tone, + i >= PAGE_SIZE, + () => { + if (item.suggestion) openSuggestionModal(componentTitle, items, i, label); + else if (item.check) openCheckModal(componentTitle, items, i, label); + }, + ))} + ${total > PAGE_SIZE ? renderPaging(total) : nothing} +
+ `; +} + +function renderSection(section, componentTitle) { + const { + label = '', subLabel = '', tone, defaultOpen = false, items = [], + } = section; + const { cls } = toneOf(tone); + return html` +
+ + + ${toneIcon(tone)} + + + + + ${items.length ? renderItems(items, tone, componentTitle) : nothing} +
+ `; +} + +/* ---- registration ---- */ + +registerArtifact('PageEvaluationWithIcons', ({ + title = '', summary = [], sections = [], +}) => { + const root = html` +
+ ${title ? html`

${title}

` : nothing} + ${summary.length ? renderSummary(summary) : nothing} + ${sections.map((s) => renderSection(s, title))} +
+ `; + + if (!styleAdopted) { + queueMicrotask(() => { + const peEl = document.querySelector('nx-chat-ao')?.shadowRoot?.querySelector('.ui-artifact-pe'); + if (peEl) adoptStyle(peEl); + }); + } + + return root; +}); From bdfce1a3e1b109abecb510828469a81e37df038d Mon Sep 17 00:00:00 2001 From: Andrei Tuicu Date: Tue, 8 Sep 2026 21:21:32 +0200 Subject: [PATCH 2/5] feat: Port the Page Evaluation Coworker custom renderer --- nx2/blocks/chat-ao/artifacts/page-evaluation.css | 3 ++- nx2/blocks/chat-ao/artifacts/page-evaluation.js | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/nx2/blocks/chat-ao/artifacts/page-evaluation.css b/nx2/blocks/chat-ao/artifacts/page-evaluation.css index b83dd0091..5cba45870 100644 --- a/nx2/blocks/chat-ao/artifacts/page-evaluation.css +++ b/nx2/blocks/chat-ao/artifacts/page-evaluation.css @@ -88,6 +88,7 @@ border: 1px solid var(--s2-gray-200); border-radius: var(--s2-corner-radius-300); overflow: hidden; + margin: 0; } .ui-artifact-pe-section-header { @@ -160,7 +161,7 @@ .ui-artifact-pe-item { display: flex; align-items: flex-start; - gap: var(--s2-spacing-100); + gap: var(--s2-spacing-200); padding: var(--s2-spacing-200) var(--s2-spacing-300); &[hidden] { diff --git a/nx2/blocks/chat-ao/artifacts/page-evaluation.js b/nx2/blocks/chat-ao/artifacts/page-evaluation.js index 2e90632ea..614eafcfa 100644 --- a/nx2/blocks/chat-ao/artifacts/page-evaluation.js +++ b/nx2/blocks/chat-ao/artifacts/page-evaluation.js @@ -15,9 +15,9 @@ const toneOf = (tone) => TONE_MAP[tone] ?? TONE_FALLBACK; const iconUrl = (name) => `/img/icons/${name}.svg`; const toneIcon = (tone, size = 20) => { - const { icon } = toneOf(tone); + const { icon, cls } = toneOf(tone); return html``; }; From 85d50c095de9d59c6a5456dd255543107adbb1fd Mon Sep 17 00:00:00 2001 From: Andrei Tuicu Date: Tue, 8 Sep 2026 22:08:36 +0200 Subject: [PATCH 3/5] feat: Port the Page Evaluation Coworker custom renderer --- nx2/blocks/chat-ao/artifacts/page-evaluation.css | 10 +--------- nx2/blocks/chat-ao/artifacts/page-evaluation.js | 1 - 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/nx2/blocks/chat-ao/artifacts/page-evaluation.css b/nx2/blocks/chat-ao/artifacts/page-evaluation.css index 5cba45870..a68571b36 100644 --- a/nx2/blocks/chat-ao/artifacts/page-evaluation.css +++ b/nx2/blocks/chat-ao/artifacts/page-evaluation.css @@ -1,15 +1,7 @@ .ui-artifact-pe { display: flex; flex-direction: column; - gap: var(--s2-spacing-300); -} - -.ui-artifact-pe-title { - margin: 0; - font-size: var(--s2-component-l-bold-font-size); - font-weight: var(--s2-component-l-bold-font-weight); - line-height: var(--s2-component-l-bold-line-height); - color: var(--s2-gray-900); + gap: var(--s2-spacing-200); } .ui-artifact-pe-icon { diff --git a/nx2/blocks/chat-ao/artifacts/page-evaluation.js b/nx2/blocks/chat-ao/artifacts/page-evaluation.js index 614eafcfa..79ebd7bfa 100644 --- a/nx2/blocks/chat-ao/artifacts/page-evaluation.js +++ b/nx2/blocks/chat-ao/artifacts/page-evaluation.js @@ -380,7 +380,6 @@ registerArtifact('PageEvaluationWithIcons', ({ }) => { const root = html`
- ${title ? html`

${title}

` : nothing} ${summary.length ? renderSummary(summary) : nothing} ${sections.map((s) => renderSection(s, title))}
From 8e736deff301c9b90d306000b3118a6c19ef02d2 Mon Sep 17 00:00:00 2001 From: Andrei Tuicu Date: Tue, 8 Sep 2026 22:56:49 +0200 Subject: [PATCH 4/5] feat: Port the Page Evaluation Coworker custom renderer --- nx2/blocks/chat-ao/artifacts/page-evaluation.css | 11 +++++++---- nx2/blocks/chat-ao/artifacts/page-evaluation.js | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/nx2/blocks/chat-ao/artifacts/page-evaluation.css b/nx2/blocks/chat-ao/artifacts/page-evaluation.css index a68571b36..ee295c878 100644 --- a/nx2/blocks/chat-ao/artifacts/page-evaluation.css +++ b/nx2/blocks/chat-ao/artifacts/page-evaluation.css @@ -41,8 +41,6 @@ .ui-artifact-pe-summary { display: flex; - border: 1px solid var(--s2-gray-200); - border-radius: var(--s2-corner-radius-300); overflow: hidden; } @@ -146,8 +144,13 @@ /* --- item rows --- */ -.ui-artifact-pe-items { - border-top: 1px solid var(--s2-gray-200); +.ui-artifact-pe-item-chip { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + flex-shrink: 0; } .ui-artifact-pe-item { diff --git a/nx2/blocks/chat-ao/artifacts/page-evaluation.js b/nx2/blocks/chat-ao/artifacts/page-evaluation.js index 79ebd7bfa..470eb0cc7 100644 --- a/nx2/blocks/chat-ao/artifacts/page-evaluation.js +++ b/nx2/blocks/chat-ao/artifacts/page-evaluation.js @@ -306,7 +306,7 @@ function openCheckModal(componentTitle, items, startIndex, sectionLabel) { function renderItem(item, tone, hidden, onAction) { return html`
- ${toneIcon(tone)} + ${toneIcon(tone)}
${item.title ?? ''} ${item.description From d642213a93bee900752a34395e8d206746a60405 Mon Sep 17 00:00:00 2001 From: Andrei Tuicu Date: Wed, 9 Sep 2026 10:25:24 +0200 Subject: [PATCH 5/5] feat: Port the Page Evaluation Coworker custom renderer --- .../chat-ao/artifacts/page-evaluation.css | 206 ++++++- .../chat-ao/artifacts/page-evaluation.js | 571 ++++++++---------- 2 files changed, 444 insertions(+), 333 deletions(-) diff --git a/nx2/blocks/chat-ao/artifacts/page-evaluation.css b/nx2/blocks/chat-ao/artifacts/page-evaluation.css index ee295c878..674d4a48f 100644 --- a/nx2/blocks/chat-ao/artifacts/page-evaluation.css +++ b/nx2/blocks/chat-ao/artifacts/page-evaluation.css @@ -1,3 +1,11 @@ +:host { + display: block; + font-family: var(--s2-font-family); + color: var(--s2-gray-800); + font-size: var(--s2-body-size-s); + line-height: normal; +} + .ui-artifact-pe { display: flex; flex-direction: column; @@ -7,13 +15,30 @@ .ui-artifact-pe-icon { display: inline-block; flex-shrink: 0; + width: 20px; + height: 20px; background-color: currentcolor; - mask-image: var(--pe-icon); mask-size: contain; mask-repeat: no-repeat; mask-position: center; } +.ui-artifact-pe-icon.ui-artifact-pe-tone-negative { + mask-image: url("/img/icons/s2-icon-closecircle-20-n.svg"); +} + +.ui-artifact-pe-icon.ui-artifact-pe-tone-positive { + mask-image: url("/img/icons/s2-icon-checkmarkcircle-20-n.svg"); +} + +.ui-artifact-pe-icon.ui-artifact-pe-tone-neutral { + mask-image: url("/img/icons/s2-icon-removecircle-20-n.svg"); +} + +.ui-artifact-pe-icon.ui-artifact-pe-tone-informative { + mask-image: url("/img/icons/s2-icon-infocircle-20-n.svg"); +} + /* --- tone text colors --- */ .ui-artifact-pe-tone-negative { color: var(--s2-orange-900); } @@ -110,7 +135,7 @@ flex: 1; display: flex; flex-direction: column; - gap: 2px; + gap: var(--s2-spacing-50); min-width: 0; } @@ -262,3 +287,180 @@ .ui-artifact-pe-paging-next::after { transform: rotate(90deg); } + +/* --- modal --- */ + +/* The modal spec carries several off-token-scale values (13px, 20px, 28px, 6px + gap, 10px, 12px radius, 1.6 line-height); those stay raw to match the design. */ + +.ui-artifact-pe-modal-content { + font-family: var(--s2-font-family, 'Adobe Clean', adobe-clean, sans-serif); +} + +.ui-artifact-pe-modal-asset-label { + font-size: var(--s2-body-size-l); + font-weight: bold; + color: var(--s2-gray-900); + margin: 0 0 var(--s2-spacing-300); +} + +.ui-artifact-pe-modal-section { + margin-bottom: 20px; +} + +.ui-artifact-pe-modal-heading { + display: flex; + align-items: center; + gap: var(--s2-spacing-100); + margin-bottom: var(--s2-spacing-100); +} + +.ui-artifact-pe-modal-heading-icon { + width: 20px; + height: 20px; + flex-shrink: 0; + background-color: currentcolor; + mask-size: contain; + mask-repeat: no-repeat; + mask-position: center; +} + +.ui-artifact-pe-modal-heading-icon.issue { + color: var(--s2-orange-900); + mask-image: url("/img/icons/s2-icon-closecircle-20-n.svg"); +} + +.ui-artifact-pe-modal-heading-icon.fix { + color: var(--s2-green-900); + mask-image: url("/img/icons/s2-icon-checkmarkcircle-20-n.svg"); +} + +.ui-artifact-pe-modal-heading-text { + font-weight: bold; + font-size: var(--s2-body-size-s); + color: var(--s2-gray-900); +} + +.ui-artifact-pe-modal-body { + font-size: var(--s2-body-size-s); + line-height: 1.6; + color: var(--s2-gray-800); + margin: 0; +} + +.ui-artifact-pe-modal-copy { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 13px; + color: var(--s2-gray-800); + background: none; + border: none; + cursor: pointer; + padding: var(--s2-spacing-75) 0; + margin-top: var(--s2-spacing-75); + + &:hover { + color: var(--s2-gray-900); + } +} + +.ui-artifact-pe-modal-copy-icon { + width: 16px; + height: 16px; + flex-shrink: 0; + background-color: currentcolor; + mask-image: url("/img/icons/s2-icon-copy-20-n.svg"); + mask-size: contain; + mask-repeat: no-repeat; + mask-position: center; +} + +.ui-artifact-pe-modal-context-title { + font-size: var(--s2-body-size-m); + font-weight: bold; + color: var(--s2-gray-900); + margin: var(--s2-spacing-400) 0 var(--s2-spacing-200); +} + +.ui-artifact-pe-modal-context-card { + border: 1px solid var(--s2-gray-200); + border-radius: 12px; + padding: var(--s2-spacing-300); +} + +.ui-artifact-pe-modal-chip { + display: inline-block; + background: var(--s2-gray-100); + border-radius: var(--s2-corner-radius-100); + padding: var(--s2-spacing-50) 10px; + font-size: var(--s2-body-size-xs); + color: var(--s2-gray-800); + margin-bottom: var(--s2-spacing-200); +} + +.ui-artifact-pe-modal-context-desc { + font-size: var(--s2-body-size-s); + line-height: 1.6; + color: var(--s2-gray-800); + margin: 0; +} + +.ui-artifact-pe-modal-paging { + display: flex; + justify-content: center; + align-items: center; + gap: var(--s2-spacing-200); + margin-top: 20px; + padding-top: var(--s2-spacing-300); + border-top: 1px solid var(--s2-gray-200); +} + +.ui-artifact-pe-modal-paging-btn { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border: none; + border-radius: var(--s2-corner-radius-300); + background: none; + cursor: pointer; + color: var(--s2-gray-600); + padding: 0; + + &:disabled { + opacity: 0.3; + cursor: default; + } + + &:hover:not(:disabled) { + background: var(--s2-gray-100); + } + + &::after { + content: ""; + display: block; + width: 16px; + height: 16px; + background-color: currentcolor; + mask-image: url("/img/icons/s2-icon-chevronup-20-n.svg"); + mask-size: contain; + mask-repeat: no-repeat; + mask-position: center; + } +} + +.ui-artifact-pe-modal-paging-prev::after { + transform: rotate(-90deg); +} + +.ui-artifact-pe-modal-paging-next::after { + transform: rotate(90deg); +} + +.ui-artifact-pe-modal-paging-label { + font-size: 13px; + font-weight: bold; + color: var(--s2-gray-800); +} diff --git a/nx2/blocks/chat-ao/artifacts/page-evaluation.js b/nx2/blocks/chat-ao/artifacts/page-evaluation.js index 470eb0cc7..8650573aa 100644 --- a/nx2/blocks/chat-ao/artifacts/page-evaluation.js +++ b/nx2/blocks/chat-ao/artifacts/page-evaluation.js @@ -1,47 +1,37 @@ -import { html, nothing } from 'da-lit'; +/* + * Copyright 2026 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import { LitElement, html, nothing } from 'da-lit'; +import { loadStyle } from '../../../utils/utils.js'; import { registerArtifact } from './registry.js'; +const styles = await loadStyle(import.meta.url); + const PAGE_SIZE = 5; const TONE_MAP = { - negative: { cls: 'negative', icon: 's2-icon-closecircle-20-n' }, - positive: { cls: 'positive', icon: 's2-icon-checkmarkcircle-20-n' }, - neutral: { cls: 'neutral', icon: 's2-icon-removecircle-20-n' }, + negative: { cls: 'negative' }, + positive: { cls: 'positive' }, + neutral: { cls: 'neutral' }, }; -const TONE_FALLBACK = { cls: 'informative', icon: 's2-icon-infocircle-20-n' }; +const TONE_FALLBACK = { cls: 'informative' }; const toneOf = (tone) => TONE_MAP[tone] ?? TONE_FALLBACK; -const iconUrl = (name) => `/img/icons/${name}.svg`; - -const toneIcon = (tone, size = 20) => { - const { icon, cls } = toneOf(tone); - return html``; +const toneIcon = (tone) => { + const { cls } = toneOf(tone); + return html``; }; -/* ---- lazy CSS adoption ---- */ - -const PE_STYLE_URL = new URL('./page-evaluation.css', import.meta.url).href; -let styleSheet; -let styleAdopted; - -function adoptStyle(host) { - if (styleAdopted) return; - styleAdopted = true; - (async () => { - // eslint-disable-next-line import/no-unresolved - const { loadStyle } = await import('../../../utils/utils.js'); - styleSheet = await loadStyle(PE_STYLE_URL); - const root = host?.getRootNode?.(); - if (root?.adoptedStyleSheets && !root.adoptedStyleSheets.includes(styleSheet)) { - root.adoptedStyleSheets = [...root.adoptedStyleSheets, styleSheet]; - } - })(); -} - /* ---- summary ---- */ function renderTile({ label = '', value = '', description = '', tone }) { @@ -61,336 +51,255 @@ function renderSummary(summary) { `; } -/* ---- inline pagination ---- */ - -function updatePage(container, page) { - const items = container.querySelectorAll('.ui-artifact-pe-item'); - const total = items.length; - const start = page * PAGE_SIZE; - const end = Math.min(start + PAGE_SIZE, total); - items.forEach((row, i) => { row.hidden = i < start || i >= end; }); - const label = container.querySelector('.ui-artifact-pe-paging-label'); - if (label) label.textContent = `${end} of ${total} checks`; - const prev = container.querySelector('.ui-artifact-pe-paging-prev'); - const next = container.querySelector('.ui-artifact-pe-paging-next'); - if (prev) prev.disabled = page === 0; - if (next) next.disabled = end >= total; - container.dataset.page = page; -} - -const onPage = (dir) => (e) => { - const container = e.target.closest('.ui-artifact-pe-items'); - const page = Number(container.dataset.page) + dir; - const total = container.querySelectorAll('.ui-artifact-pe-item').length; - if (page < 0 || page * PAGE_SIZE >= total) return; - updatePage(container, page); -}; +/* ---- modal bodies ---- */ -function renderPaging(total) { - const shown = Math.min(PAGE_SIZE, total); +function renderContextCard(context) { + if (!context) return nothing; return html` -
- - ${shown} of ${total} checks - +
+ ${context.category ? html`${context.category}` : nothing} + ${context.description ? html`

${context.description}

` : nothing}
`; } -/* ---- modals ---- */ - -const MODAL_STYLES = ` - .pe-modal-content { font-family: var(--s2-font-family, 'Adobe Clean', adobe-clean, sans-serif); } - .pe-modal-asset-label { font-size: 18px; font-weight: bold; color: #222; margin: 0 0 16px; } - .pe-modal-section { margin-bottom: 20px; } - .pe-modal-heading { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; } - .pe-modal-heading-icon { - width: 20px; height: 20px; flex-shrink: 0; - background-color: currentcolor; - mask-size: contain; mask-repeat: no-repeat; mask-position: center; - } - .pe-modal-heading-icon.issue { color: #c05621; mask-image: url('/img/icons/s2-icon-closecircle-20-n.svg'); } - .pe-modal-heading-icon.fix { color: #107c41; mask-image: url('/img/icons/s2-icon-checkmarkcircle-20-n.svg'); } - .pe-modal-heading-text { font-weight: bold; font-size: 14px; color: #222; } - .pe-modal-body { font-size: 14px; line-height: 1.6; color: #4b4b4b; margin: 0; } - .pe-modal-copy { - display: inline-flex; align-items: center; gap: 6px; - font-size: 13px; color: #4b4b4b; background: none; border: none; - cursor: pointer; padding: 4px 0; margin-top: 4px; - } - .pe-modal-copy:hover { color: #222; } - .pe-modal-context-title { font-size: 16px; font-weight: bold; color: #222; margin: 24px 0 12px; } - .pe-modal-context-card { - border: 1px solid #ddd; border-radius: 12px; padding: 16px; - } - .pe-modal-chip { - display: inline-block; background: #f0f0f0; border-radius: 4px; - padding: 2px 10px; font-size: 12px; color: #4b4b4b; margin-bottom: 12px; - } - .pe-modal-context-desc { font-size: 14px; line-height: 1.6; color: #4b4b4b; margin: 0; } - .pe-modal-paging { - display: flex; justify-content: center; align-items: center; gap: 12px; - margin-top: 20px; padding-top: 16px; border-top: 1px solid #eee; - } - .pe-modal-paging-btn { - display: flex; align-items: center; justify-content: center; - width: 28px; height: 28px; border: none; border-radius: 6px; - background: none; cursor: pointer; color: #666; padding: 0; - } - .pe-modal-paging-btn:hover:not(:disabled) { background: #f0f0f0; } - .pe-modal-paging-btn:disabled { opacity: 0.3; cursor: default; } - .pe-modal-paging-btn::after { - content: ""; display: block; width: 16px; height: 16px; - background-color: currentcolor; - mask-image: url('/img/icons/s2-icon-chevronup-20-n.svg'); - mask-size: contain; mask-repeat: no-repeat; mask-position: center; - } - .pe-modal-paging-prev::after { transform: rotate(-90deg); } - .pe-modal-paging-next::after { transform: rotate(90deg); } - .pe-modal-paging-label { font-size: 13px; font-weight: bold; color: #444; } -`; - -function el(tag, cls, text) { - const node = document.createElement(tag); - if (cls) node.className = cls; - if (text) node.textContent = text; - return node; +function renderSuggestionBody(item) { + const { suggestion } = item; + if (!suggestion) return nothing; + return html` + ${suggestion.label ? html`

${suggestion.label}

` : nothing} +
+
+ + Issue identified +
+ ${suggestion.issue ? html`

${suggestion.issue}

` : nothing} +
+
+
+ + Suggested change +
+ ${suggestion.suggested ? html`

${suggestion.suggested}

` : nothing} + +
+ ${suggestion.context ? html` +
Context
+ ${renderContextCard(suggestion.context)} + ` : nothing} + `; } -function buildContextCard(context) { - if (!context) return null; - const card = el('div', 'pe-modal-context-card'); - if (context.category) card.append(el('span', 'pe-modal-chip', context.category)); - if (context.description) card.append(el('p', 'pe-modal-context-desc', context.description)); - return card; +function renderCheckBody(item) { + const { check } = item; + if (!check) return nothing; + return html` + ${check.label ? html`

${check.label}

` : nothing} + ${item.description ? html` +
+

${item.description}

+
+ ` : nothing} + ${check.context ? html` +
Context
+ ${renderContextCard(check.context)} + ` : nothing} + `; } -function buildSuggestionBody(item) { - const frag = document.createDocumentFragment(); - const { suggestion } = item; - if (!suggestion) return frag; - - if (suggestion.label) frag.append(el('h3', 'pe-modal-asset-label', suggestion.label)); - - const issueSection = el('div', 'pe-modal-section'); - const issueHeading = el('div', 'pe-modal-heading'); - const issueIcon = el('span', 'pe-modal-heading-icon issue'); - issueHeading.append(issueIcon, el('span', 'pe-modal-heading-text', 'Issue identified')); - issueSection.append(issueHeading); - if (suggestion.issue) issueSection.append(el('p', 'pe-modal-body', suggestion.issue)); - frag.append(issueSection); - - const fixSection = el('div', 'pe-modal-section'); - const fixHeading = el('div', 'pe-modal-heading'); - const fixIcon = el('span', 'pe-modal-heading-icon fix'); - fixHeading.append(fixIcon, el('span', 'pe-modal-heading-text', 'Suggested change')); - fixSection.append(fixHeading); - if (suggestion.suggested) fixSection.append(el('p', 'pe-modal-body', suggestion.suggested)); - - const copyBtn = el('button', 'pe-modal-copy', '📋 Copy suggestion'); - copyBtn.addEventListener('click', () => { - navigator.clipboard.writeText(suggestion.suggested ?? ''); - }); - fixSection.append(copyBtn); - frag.append(fixSection); +/* ---- section labels ---- */ - if (suggestion.context) { - frag.append(el('div', 'pe-modal-context-title', 'Context')); - const card = buildContextCard(suggestion.context); - if (card) frag.append(card); - } - - return frag; +function sectionLabelFromTone(tone) { + if (tone === 'negative') return 'failures'; + if (tone === 'positive') return 'passes'; + return 'checks'; } -function buildCheckBody(item) { - const frag = document.createDocumentFragment(); - const { check } = item; - if (!check) return frag; - - if (check.label) frag.append(el('h3', 'pe-modal-asset-label', check.label)); +class NxPageEval extends LitElement { + static properties = { + data: { attribute: false }, + _pages: { state: true }, + _modal: { state: true }, + }; - if (item.description) { - const section = el('div', 'pe-modal-section'); - section.append(el('p', 'pe-modal-body', item.description)); - frag.append(section); + constructor() { + super(); + this._pages = {}; } - if (check.context) { - frag.append(el('div', 'pe-modal-context-title', 'Context')); - const card = buildContextCard(check.context); - if (card) frag.append(card); + connectedCallback() { + super.connectedCallback(); + this.shadowRoot.adoptedStyleSheets = [styles]; } - return frag; -} - -function openModal({ componentTitle, items, startIndex, sectionLabel, buildBody }) { - import('../../shared/dialog/dialog.js').then(() => { - const navigable = items; - const total = navigable.length; - if (!total) return; - - let current = Math.min(startIndex, total - 1); - const dialog = document.createElement('nx-dialog'); - dialog.title = componentTitle || 'Page evaluation'; - dialog.style.setProperty('--nx-dialog-max-width', '600px'); - - const styleEl = document.createElement('style'); - styleEl.textContent = MODAL_STYLES; - - const contentWrap = el('div', 'pe-modal-content'); - const bodyWrap = el('div', 'pe-modal-body-wrap'); - - const pagingLabel = el('span', 'pe-modal-paging-label'); - const prevBtn = el('button', 'pe-modal-paging-btn pe-modal-paging-prev'); - const nextBtn = el('button', 'pe-modal-paging-btn pe-modal-paging-next'); + _setPage(sectionIndex, page, total) { + if (page < 0 || page * PAGE_SIZE >= total) return; + this._pages = { ...this._pages, [sectionIndex]: page }; + } - function render() { - bodyWrap.replaceChildren(buildBody(navigable[current])); - pagingLabel.textContent = `${current + 1} of ${total} ${sectionLabel}`; - prevBtn.disabled = current === 0; - nextBtn.disabled = current >= total - 1; + async _openModal(item, items, tone, componentTitle) { + let kind; + let filtered; + if (item.suggestion) { + kind = 'suggestion'; + filtered = items.filter((it) => it.suggestion); + } else if (item.check) { + kind = 'check'; + filtered = items.filter((it) => it.check); + } else { + return; } + if (!filtered.length) return; + const idx = filtered.indexOf(item); + await import('../../shared/dialog/dialog.js'); + this._modal = { + kind, + items: filtered, + index: idx >= 0 ? idx : 0, + sectionLabel: sectionLabelFromTone(tone), + title: componentTitle || 'Page evaluation', + }; + } - prevBtn.addEventListener('click', () => { - if (current > 0) { - current -= 1; - render(); - } - }); - nextBtn.addEventListener('click', () => { - if (current < total - 1) { - current += 1; - render(); - } - }); - - const pagingRow = el('div', 'pe-modal-paging'); - pagingRow.append(prevBtn, pagingLabel, nextBtn); - - contentWrap.append(bodyWrap, pagingRow); - dialog.append(styleEl, contentWrap); - dialog.addEventListener('close', () => dialog.remove()); - render(); - document.body.append(dialog); - }); -} - -function openSuggestionModal(componentTitle, items, startIndex, sectionLabel) { - const suggestItems = items.filter((it) => it.suggestion); - const idx = suggestItems.indexOf(items[startIndex]); - openModal({ - componentTitle, - items: suggestItems, - startIndex: idx >= 0 ? idx : 0, - sectionLabel, - buildBody: buildSuggestionBody, - }); -} - -function openCheckModal(componentTitle, items, startIndex, sectionLabel) { - const checkItems = items.filter((it) => it.check); - const idx = checkItems.indexOf(items[startIndex]); - openModal({ - componentTitle, - items: checkItems, - startIndex: idx >= 0 ? idx : 0, - sectionLabel, - buildBody: buildCheckBody, - }); -} - -/* ---- item rows ---- */ + _setModalIndex(index) { + if (!this._modal || index < 0 || index >= this._modal.items.length) return; + this._modal = { ...this._modal, index }; + } -function renderItem(item, tone, hidden, onAction) { - return html` -
- ${toneIcon(tone)} -
- ${item.title ?? ''} - ${item.description + _renderItem(item, tone, hidden, onAction) { + return html` +
+ ${toneIcon(tone)} +
+ ${item.title ?? ''} + ${item.description ? html`${item.description}` : nothing} +
+ ${item.suggestion ? html` + + ` : nothing} + ${!item.suggestion && item.check ? html` + + ` : nothing}
- ${item.suggestion ? html` - - ` : nothing} - ${!item.suggestion && item.check ? html` - - ` : nothing} -
- `; -} - -/* ---- section ---- */ - -function sectionLabelFromTone(tone) { - if (tone === 'negative') return 'failures'; - if (tone === 'positive') return 'passes'; - return 'checks'; -} + `; + } -function renderItems(items, tone, componentTitle) { - const total = items.length; - const label = sectionLabelFromTone(tone); + _renderPaging(sectionIndex, end, total) { + const page = this._pages[sectionIndex] ?? 0; + return html` +
+ + ${end} of ${total} checks + +
+ `; + } - return html` -
- ${items.map((item, i) => renderItem( + _renderItems(items, tone, sectionIndex, componentTitle) { + const total = items.length; + const page = this._pages[sectionIndex] ?? 0; + const start = page * PAGE_SIZE; + const end = Math.min(start + PAGE_SIZE, total); + return html` +
+ ${items.map((item, i) => this._renderItem( item, tone, - i >= PAGE_SIZE, - () => { - if (item.suggestion) openSuggestionModal(componentTitle, items, i, label); - else if (item.check) openCheckModal(componentTitle, items, i, label); - }, + i < start || i >= end, + () => this._openModal(item, items, tone, componentTitle), ))} - ${total > PAGE_SIZE ? renderPaging(total) : nothing} -
- `; -} - -function renderSection(section, componentTitle) { - const { - label = '', subLabel = '', tone, defaultOpen = false, items = [], - } = section; - const { cls } = toneOf(tone); - return html` -
- - - ${toneIcon(tone)} - - - - - ${items.length ? renderItems(items, tone, componentTitle) : nothing} -
- `; -} + ${total > PAGE_SIZE ? this._renderPaging(sectionIndex, end, total) : nothing} +
+ `; + } -/* ---- registration ---- */ + _renderSection(section, sectionIndex, componentTitle) { + const { + label = '', subLabel = '', tone, defaultOpen = false, items = [], + } = section; + const { cls } = toneOf(tone); + return html` +
+ + + ${toneIcon(tone)} + + + + + ${items.length ? this._renderItems(items, tone, sectionIndex, componentTitle) : nothing} +
+ `; + } -registerArtifact('PageEvaluationWithIcons', ({ - title = '', summary = [], sections = [], -}) => { - const root = html` -
- ${summary.length ? renderSummary(summary) : nothing} - ${sections.map((s) => renderSection(s, title))} -
- `; + _renderModal() { + const { + kind, items, index, sectionLabel, title, + } = this._modal; + const total = items.length; + const renderBody = kind === 'suggestion' ? renderSuggestionBody : renderCheckBody; + return html` + { this._modal = undefined; }} + > +
+ ${renderBody(items[index])} +
+ + + ${index + 1} of ${total} ${sectionLabel} + + +
+
+
+ `; + } - if (!styleAdopted) { - queueMicrotask(() => { - const peEl = document.querySelector('nx-chat-ao')?.shadowRoot?.querySelector('.ui-artifact-pe'); - if (peEl) adoptStyle(peEl); - }); + render() { + if (!this.data) return nothing; + const { title = '', summary = [], sections = [] } = this.data; + return html` +
+ ${summary.length ? renderSummary(summary) : nothing} + ${sections.map((s, i) => this._renderSection(s, i, title))} +
+ ${this._modal ? this._renderModal() : nothing} + `; } +} + +if (!customElements.get('nx-page-eval')) customElements.define('nx-page-eval', NxPageEval); - return root; -}); +registerArtifact('PageEvaluationWithIcons', (props) => html` + +`);