diff --git a/.github/assets/cover.jpg b/.github/assets/cover.jpg new file mode 100644 index 0000000..fcd32a5 Binary files /dev/null and b/.github/assets/cover.jpg differ diff --git a/.gitignore b/.gitignore index b6fa06e..ce26aa3 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,6 @@ Chatballs.zip # Служебный каталог агента: локальные настройки запуска, а не часть продукта. .claude/ + +# Дизайн-хендоффы: рабочие материалы, а не часть продукта. +design/ diff --git a/README.md b/README.md index be8648c..17160fc 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,20 @@ --- +

+ Chatballs workspace: dialogs, conversation and contact card +

+ +

+ Telegram channel +  ·  + Website +  ·  + Help center +

+ +--- + ## Table of contents - [Overview](#overview) diff --git a/README.ru.md b/README.ru.md index 6d639b9..32ac906 100644 --- a/README.ru.md +++ b/README.ru.md @@ -23,6 +23,20 @@ --- +

+ Рабочее место Chatballs: список диалогов, переписка и карточка контакта +

+ +

+ Телеграм-канал +  ·  + Сайт +  ·  + Центр помощи +

+ +--- + ## Содержание - [Что это](#что-это) diff --git a/design/baseline/# UI для онлайн звонка/.thumbnail b/design/baseline/# UI для онлайн звонка/.thumbnail deleted file mode 100644 index 0914090..0000000 Binary files a/design/baseline/# UI для онлайн звонка/.thumbnail and /dev/null differ diff --git a/design/baseline/# UI для онлайн звонка/_ds/design-system/_adherence.oxlintrc.json b/design/baseline/# UI для онлайн звонка/_ds/design-system/_adherence.oxlintrc.json deleted file mode 100644 index 8419c5c..0000000 --- a/design/baseline/# UI для онлайн звонка/_ds/design-system/_adherence.oxlintrc.json +++ /dev/null @@ -1,508 +0,0 @@ -{ - "plugins": [ - "react", - "import" - ], - "rules": { - "react/forbid-elements": [ - "warn", - { - "forbid": [] - } - ], - "no-restricted-imports": [ - "warn", - { - "patterns": [ - { - "group": [ - "components/cards/**", - "components/core/**", - "components/inbox/**", - "components/status/**", - "ui_kits/checkout/**", - "ui_kits/internal-hub/**", - "ui_kits/web-chat/**" - ], - "message": "Import design-system components from 'index.js', not component internals." - } - ] - } - ], - "no-restricted-syntax": [ - "warn", - { - "selector": "Literal[value=/#[0-9a-fA-F]{3,8}\\b/]", - "message": "Raw hex color — use a design-system color token via var()." - }, - { - "selector": "Literal[value=/\\b\\d+px\\b/]", - "message": "Raw px value — use a design-system spacing token via var()." - }, - { - "selector": "JSXOpeningElement[name.name='ActorBadge'] > JSXAttribute > JSXIdentifier[name!=/^(?:actor|initials|color|key|ref|className|style|children)$/]", - "message": " doesn't accept that prop. Declared props: actor, initials, color." - }, - { - "selector": "JSXOpeningElement[name.name='ActorBadge'] > JSXAttribute[name.name='actor'] > Literal[value!=/^(?:ai|human)$/]", - "message": " actor must be one of 'ai' | 'human'." - }, - { - "selector": "JSXOpeningElement[name.name='AttentionStatus'] > JSXAttribute > JSXIdentifier[name!=/^(?:level|label|key|ref|className|style|children)$/]", - "message": " doesn't accept that prop. Declared props: level, label." - }, - { - "selector": "JSXOpeningElement[name.name='AttentionStatus'] > JSXAttribute[name.name='level'] > Literal[value!=/^(?:ok|attention|critical)$/]", - "message": " level must be one of 'ok' | 'attention' | 'critical'." - }, - { - "selector": "JSXOpeningElement[name.name='Avatar'] > JSXAttribute > JSXIdentifier[name!=/^(?:initials|color|size|key|ref|className|style|children)$/]", - "message": " doesn't accept that prop. Declared props: initials, color, size." - }, - { - "selector": "JSXOpeningElement[name.name='Button'] > JSXAttribute > JSXIdentifier[name!=/^(?:children|variant|size|disabled|onClick|type|key|ref|className|style|children)$/]", - "message": "' + - ' ' + - ''; - this._frame = root.querySelector('.frame'); - this._ring = root.querySelector('.ring'); - this._img = root.querySelector('.frame img'); - this._empty = root.querySelector('.empty'); - this._cap = root.querySelector('.cap'); - this._sub = root.querySelector('.sub'); - this._spill = root.querySelector('.spill'); - this._ctl = root.querySelector('.ctl'); - this._credit = root.querySelector('.credit'); - this._attrError = root.querySelector('.attr-error'); - // Credit clicks open the link, not browse/reframe. - this._credit.addEventListener('click', (e) => e.stopPropagation()); - this._credit.addEventListener('dblclick', (e) => e.stopPropagation()); - this._ghost = root.querySelector('.ghost'); - this._err = null; - this._input = root.querySelector('input'); - this._depth = 0; - this._gen = 0; - // Encode-in-flight marker (the owning _ingest generation): while set, - // the same-src "nothing in flight" clear in _render must not fire — - // the stored value still points at the OLD image until the encode - // lands, so that clear would unmask the stale image mid-replace. - this._swapGen = 0; - // Render-owned swap in flight: set when _render assigns a new src, - // cleared only by the img's own load/error (or the empty branch). - // img.complete CANNOT stand in for this — setting src only QUEUES - // the current-request swap (a microtask), so synchronously after an - // assignment, complete still reports the OLD settled request. The - // pick path does exactly that: the host sets src, credit, and - // credit-href back-to-back in one task, and renders #2/#3 would - // read the stale complete === true and drop the mask one render - // after it was set. - this._loadPending = false; - // See _render's empty branch: a transient attribution-error wipe of a - // showing image must make the follow-up render a replacement (spinner), - // not a first fill (blank frame). - this._hidShowing = false; - this._view = { s: 1, x: 0, y: 0 }; - this._subFn = () => this._render(); - // Shadow-DOM listeners live with the shadow DOM — bound once here so - // disconnect/reconnect (e.g. React remount) doesn't stack handlers. - this._empty.addEventListener('click', () => this._input.click()); - root.addEventListener('click', (e) => { - const act = e.target && e.target.getAttribute && e.target.getAttribute('data-act'); - if (!act) return; - // The hidden controls are opacity-0 but still tabbable — without - // this gate a keyboard user could drive them on a read-only share - // link (mirrors the dblclick handler's editable gate). - if (!this.hasAttribute('data-editable')) return; - if (act === 'replace') { - this._exitReframe(true); - // Host-owned picker (Unsplash modal; it also offers local import). - this.dispatchEvent(new CustomEvent('image-slot:pick', { - bubbles: true, composed: true, detail: { id: this.id || null } - })); - } - if (act === 'edit') { - if (!this._reframes()) return; - if (this.hasAttribute('data-reframe')) this._exitReframe(true); - else this._enterReframe(); - } - }); - this._input.addEventListener('change', () => { - const f = this._input.files && this._input.files[0]; - if (f) this._ingest(f); - this._input.value = ''; - }); - // naturalWidth/Height aren't known until load — re-apply so the cover - // baseline is computed from real dimensions, not the 100%×100% fallback. - // load/error also release the replacement-in-flight mask (via the - // single discipline in _releaseMask): the swap is only revealed once - // the new image can actually paint (on error the frame shows its - // background, same as a fresh slot with a broken src). - this._img.addEventListener('load', () => { - this._loadPending = false; - this._releaseMask(true); - this._applyView(); - }); - this._img.addEventListener('error', () => { - this._loadPending = false; - this._releaseMask(true); - }); - // Gated only on editable — any filled slot can be repositioned/scaled, - // regardless of fit. Share links (no writeFile) stay static. - this.addEventListener('dblclick', (e) => { - if (!this.hasAttribute('data-editable') || !this._reframes()) return; - e.preventDefault(); - if (this.hasAttribute('data-reframe')) this._exitReframe(true); - else this._enterReframe(); - }); - // Pan + resize both originate on the spill layer. A handle pointerdown - // drives an aspect-locked resize anchored at the opposite corner; any - // other pointerdown on the spill pans. Offsets are frame-% so a - // reframed slot survives responsive resize / PPTX export. - this._spill.addEventListener('pointerdown', (e) => { - if (e.button !== 0 || !this.hasAttribute('data-reframe')) return; - e.preventDefault(); - e.stopPropagation(); - this._spill.setPointerCapture(e.pointerId); - const rect = this.getBoundingClientRect(); - const fw = rect.width || 1, fh = rect.height || 1; - const corner = e.target.getAttribute && e.target.getAttribute('data-c'); - let move; - if (corner) { - // Resize about the OPPOSITE corner. Viewport-px throughout (rect - // fw/fh, not clientWidth) so the math survives a transform:scale() - // ancestor — deck_stage renders slides scaled-to-fit. - const iw = this._img.naturalWidth || 1, ih = this._img.naturalHeight || 1; - const contain = (this.getAttribute('fit') || 'cover').toLowerCase() === 'contain'; - const base = contain ? Math.min(fw / iw, fh / ih) : Math.max(fw / iw, fh / ih); - const sx = corner.includes('e') ? 1 : -1; - const sy = corner.includes('s') ? 1 : -1; - const s0 = this._view.s; - const w0 = iw * base * s0, h0 = ih * base * s0; - const cx0 = (50 + this._view.x) / 100 * fw; - const cy0 = (50 + this._view.y) / 100 * fh; - const ox = cx0 - sx * w0 / 2, oy = cy0 - sy * h0 / 2; - const diag0 = Math.hypot(w0, h0); - const ux = sx * w0 / diag0, uy = sy * h0 / diag0; - move = (ev) => { - const proj = (ev.clientX - rect.left - ox) * ux + - (ev.clientY - rect.top - oy) * uy; - const s = clampS(s0 * proj / diag0); - const d = diag0 * s / s0; - this._view.s = s; - this._view.x = (ox + ux * d / 2) / fw * 100 - 50; - this._view.y = (oy + uy * d / 2) / fh * 100 - 50; - this._clampView(); - this._applyView(); - }; - } else { - this.setAttribute('data-panning', ''); - const start = { px: e.clientX, py: e.clientY, x: this._view.x, y: this._view.y }; - move = (ev) => { - this._view.x = start.x + (ev.clientX - start.px) / fw * 100; - this._view.y = start.y + (ev.clientY - start.py) / fh * 100; - this._clampView(); - this._applyView(); - }; - } - const up = () => { - try { this._spill.releasePointerCapture(e.pointerId); } catch {} - this._spill.removeEventListener('pointermove', move); - this._spill.removeEventListener('pointerup', up); - this._spill.removeEventListener('pointercancel', up); - this.removeAttribute('data-panning'); - this._dragUp = null; - }; - // Stashed so _exitReframe (Escape / outside-click mid-drag) can - // tear the capture + listeners down synchronously. - this._dragUp = up; - this._spill.addEventListener('pointermove', move); - this._spill.addEventListener('pointerup', up); - this._spill.addEventListener('pointercancel', up); - }); - // Wheel zoom stays available inside reframe mode as a trackpad nicety — - // zooms toward the cursor (offset' = cursor·(1-k) + offset·k). - this.addEventListener('wheel', (e) => { - if (!this.hasAttribute('data-reframe')) return; - e.preventDefault(); - const r = this.getBoundingClientRect(); - const cx = (e.clientX - r.left) / r.width * 100 - 50; - const cy = (e.clientY - r.top) / r.height * 100 - 50; - const prev = this._view.s; - const next = clampS(prev * Math.pow(1.0015, -e.deltaY)); - if (next === prev) return; - const k = next / prev; - this._view.s = next; - this._view.x = cx * (1 - k) + this._view.x * k; - this._view.y = cy * (1 - k) + this._view.y * k; - this._clampView(); - this._applyView(); - }, { passive: false }); - } - - connectedCallback() { - // Warn once per page — an id-less slot works for the session but - // cannot persist, and two id-less slots would share nothing. - if (!this.id && !ImageSlot._warned) { - ImageSlot._warned = true; - console.warn(' without an id will not persist its dropped image.'); - } - this.addEventListener('dragenter', this); - this.addEventListener('dragover', this); - this.addEventListener('dragleave', this); - this.addEventListener('drop', this); - subs.add(this._subFn); - // The host may inject window.omelette.writeFile AFTER the first render; - // re-render on hover so the editable-gated controls reliably appear. - this.addEventListener('pointerenter', this._subFn); - // width%/height% in _applyView encode the frame aspect at call time — - // a host resize (responsive grid, pane divider) would stretch the - // image until the next _render. Re-render on size change: _render() - // re-seeds _view from stored before clamp/apply, so a shrink→grow - // cycle round-trips instead of ratcheting x/y toward the narrower - // frame's clamp range. - this._ro = new ResizeObserver(() => this._render()); - this._ro.observe(this); - load(); - this._render(); - } - - disconnectedCallback() { - subs.delete(this._subFn); - this.removeEventListener('pointerenter', this._subFn); - this.removeEventListener('dragenter', this); - this.removeEventListener('dragover', this); - this.removeEventListener('dragleave', this); - this.removeEventListener('drop', this); - if (this._ro) { this._ro.disconnect(); this._ro = null; } - // commit=false: a disconnect is not a user intent — committing here - // would persist whatever half-finished drag a React remount or DOM - // splice happened to interrupt. Deliberate exits commit on their own - // paths (Escape/click-out/toggle), and unloads commit via pagehide. - this._exitReframe(false); - } - - _enterReframe() { - if (this.hasAttribute('data-reframe')) return; - this.setAttribute('data-reframe', ''); - this._signalReframe(true); - // Best-effort commit when the document unloads mid-reframe (a host - // navigation racing the enter signal, a manual reload, tab close): - // the sidecar write rides the host bridge, which outlives this - // document, so the crop survives even though the mode dies with the - // DOM. Held on the instance so _exitReframe detaches exactly what - // was attached. - this._pagehide = () => { this._exitReframe(true); flushNow(); }; - window.addEventListener('pagehide', this._pagehide); - // Promote spill to the top layer, then keep it pinned over the frame: - // scroll/resize cover the common cases, and a per-frame rect check - // catches layout shifts that fire neither (an image above finishing - // load, streamed DOM pushing the slot down, an ancestor transform - // change) so the overlay can't detach from the frame. - try { this._spill.showPopover(); } catch {} - // After the spill, so the controls stack above it in the top layer. - try { this._ctl.showPopover(); } catch {} - this._reposition = () => { if (this.hasAttribute('data-reframe')) this._applyView(); }; - window.addEventListener('scroll', this._reposition, true); - window.addEventListener('resize', this._reposition); - this._lastRect = ''; - this._watch = () => { - if (!this.hasAttribute('data-reframe')) return; - const r = this.getBoundingClientRect(); - const key = r.left + ',' + r.top + ',' + r.width + ',' + r.height; - if (key !== this._lastRect) { this._lastRect = key; this._applyView(); } - this._watchId = requestAnimationFrame(this._watch); - }; - this._watchId = requestAnimationFrame(this._watch); - this._applyView(); - // Close on click outside (the spill handler stopPropagation()s so - // in-image drags don't reach this) and on Escape. Listeners are held - // on the instance so _exitReframe / disconnectedCallback can detach - // exactly what was attached. - this._outside = (e) => { - if (e.composedPath && e.composedPath().includes(this)) return; - this._exitReframe(true); - }; - this._esc = (e) => { if (e.key === 'Escape') this._exitReframe(true); }; - document.addEventListener('pointerdown', this._outside, true); - document.addEventListener('keydown', this._esc, true); - } - - _exitReframe(commit) { - if (!this.hasAttribute('data-reframe')) return; - if (this._dragUp) this._dragUp(); - this.removeAttribute('data-reframe'); - this.removeAttribute('data-panning'); - if (this._outside) document.removeEventListener('pointerdown', this._outside, true); - if (this._esc) document.removeEventListener('keydown', this._esc, true); - this._outside = this._esc = null; - if (this._reposition) { - window.removeEventListener('scroll', this._reposition, true); - window.removeEventListener('resize', this._reposition); - this._reposition = null; - } - if (this._watchId) { cancelAnimationFrame(this._watchId); this._watchId = 0; } - if (this._pagehide) { - window.removeEventListener('pagehide', this._pagehide); - this._pagehide = null; - } - try { this._spill.hidePopover(); } catch {} - try { this._ctl.hidePopover(); } catch {} - this._ctl.style.left = ''; this._ctl.style.top = ''; - if (commit) this._commitView(); - this._signalReframe(false); - } - - // Reframe state lives only in this DOM until commit, invisible to the - // host's dirty signals — announce enter/exit so the host can hold - // auto-reloads for exactly the gesture (the guest bundle forwards - // image-slot:reframe to the host as imageSlotReframe). Dispatched on - // the element (composed, so it escapes shadow roots) while connected; - // a disconnected exit (disconnectedCallback) falls back to document so - // the host still hears it. - _signalReframe(active) { - const target = this.isConnected ? this : document; - target.dispatchEvent(new CustomEvent('image-slot:reframe', { - bubbles: true, composed: true, - detail: { active: active, id: this.id || null } - })); - } - - // Public: host's "Import from computer" calls this to run local browse. - openFilePicker() { this._exitReframe(true); this._input.click(); } - - attributeChangedCallback() { if (this.shadowRoot) this._render(); } - - // handleEvent — one listener object for all four drag events keeps the - // add/remove symmetric and the depth counter correct. - handleEvent(e) { - if (e.type === 'dragenter' || e.type === 'dragover') { - // Without preventDefault the browser never fires 'drop'. - e.preventDefault(); - e.stopPropagation(); - if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'; - if (e.type === 'dragenter') this._depth++; - this.setAttribute('data-over', ''); - } else if (e.type === 'dragleave') { - // dragenter/leave fire for every descendant crossing — count depth - // so hovering the icon inside the empty state doesn't flicker. - if (--this._depth <= 0) { this._depth = 0; this.removeAttribute('data-over'); } - } else if (e.type === 'drop') { - e.preventDefault(); - e.stopPropagation(); - this._depth = 0; - this.removeAttribute('data-over'); - const f = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0]; - if (f) this._ingest(f); - } - } - - async _ingest(file) { - this._setError(null); - if (!file || ACCEPT.indexOf(file.type) < 0) { - this._setError('Drop a PNG, JPEG, WebP, or AVIF image.'); - return; - } - // toDataUrl can take hundreds of ms on a large photo. A Clear or a - // newer drop during that window would be clobbered when this await - // resumes — bump + capture a generation so stale encodes bail. - const gen = ++this._gen; - // Replacing a shown image: surface the swap through the encode too, - // not just the decode — otherwise the old photo sits there with no - // feedback while the canvas re-encode runs. An empty slot keeps its - // placeholder (no spinner) until the encode lands, as before. - // _swapGen guards the mask against re-renders DURING the encode - // (pointerenter, ResizeObserver, another slot's store write): the - // stored value still resolves to the old image there, so _render's - // same-src clear would otherwise unmask it mid-replace. - if (this.hasAttribute('data-filled')) { - this.setAttribute('data-swapping', ''); - this._swapGen = gen; - } - try { - const w = this.clientWidth || this.offsetWidth || MAX_DIM; - const url = await toDataUrl(file, w); - if (gen !== this._gen) return; - // Only exit reframe once the new image is in hand — a rejected type - // or decode failure leaves the in-progress crop untouched. - this._exitReframe(false); - // Clear BEFORE setSlot: its synchronous re-render must see no - // pending encode, so a byte-identical re-upload (same data URL, no - // load event coming) still clears the mask via the complete branch. - this._swapGen = 0; - const val = { u: url, s: 1, x: 0, y: 0 }; - setSlot(this.id || '', val); - // Keep a session-local copy for id-less slots so the drop still - // shows, even though it cannot persist. - if (!this.id) { this._local = val; this._render(); } - } catch (err) { - if (gen !== this._gen) return; - this._swapGen = 0; - // Reveal the kept old image — unless another replacement (a - // remote pick's src swap) is still in flight, in which case the - // mask stays until THAT image settles (its load/error releases). - this._releaseMask(); - this._setError('Could not read that image.'); - console.warn(' ingest failed:', err); - } - } - - _setError(msg) { - if (this._err) { this._err.remove(); this._err = null; } - if (!msg) return; - const d = document.createElement('div'); - d.className = 'err'; d.textContent = msg; - this.shadowRoot.appendChild(d); - this._err = d; - setTimeout(() => { if (this._err === d) { d.remove(); this._err = null; } }, 3000); - } - - // Reframing (pan/resize) is available on any filled slot — the user can - // always reposition/scale. `fit` only sets the initial baseline (see - // _geom): contain starts fully-visible, cover starts frame-filling. - _reframes() { - return this.hasAttribute('data-filled'); - } - - // The single release discipline for the replacement-in-flight mask - // (data-swapping). The mask comes off only when BOTH hold: - // - no encode is pending (_swapGen) — mid-encode the stored value - // still resolves to the old image, so any reveal paints it; - // - the frame img has settled on its current src — an unsettled src - // means some replacement is still in flight (e.g. a remote pick), - // whoever started it, and revealing would paint the previous - // frame. The load/error listeners pass settled=true (the event IS - // the settlement signal, per spec complete is true by then); - // other callers rely on the complete flag (covers loaded AND - // failed). - // Every release path funnels through here EXCEPT _render's empty - // branch (the img is being cleared — nothing will ever settle). - _releaseMask(settled) { - if ( - !this._swapGen && - !this._loadPending && - (settled || this._img.complete) - ) { - this.removeAttribute('data-swapping'); - } - } - - // Baseline geometry, shared by clamp/apply/resize. `base` is the scale at - // view-scale s=1: cover = fill the frame (overflow on the looser axis), - // contain = fit fully inside (letterboxed). Zooming a contain image past - // s where it overflows naturally becomes a crop. Null until the img has - // loaded (naturalWidth is 0 before that) or when the slot has no layout - // box — ResizeObserver fires with a 0×0 rect under display:none, and - // clamping against a degenerate 1×1 frame would silently pull the stored - // pan toward zero. - _geom() { - const iw = this._img.naturalWidth, ih = this._img.naturalHeight; - const fw = this.clientWidth, fh = this.clientHeight; - if (!iw || !ih || !fw || !fh) return null; - const contain = (this.getAttribute('fit') || 'cover').toLowerCase() === 'contain'; - const base = contain - ? Math.min(fw / iw, fh / ih) - : Math.max(fw / iw, fh / ih); - return { iw, ih, fw, fh, base }; - } - - _clampView() { - // Pan range on each axis is half the overflow past the frame edge. - const g = this._geom(); - if (!g) return; - const mx = Math.max(0, (g.iw * g.base * this._view.s / g.fw - 1) * 50); - const my = Math.max(0, (g.ih * g.base * this._view.s / g.fh - 1) * 50); - this._view.x = Math.max(-mx, Math.min(mx, this._view.x)); - this._view.y = Math.max(-my, Math.min(my, this._view.y)); - } - - _applyView() { - const g = this._geom(); - // Top-layer controls: pin to the frame's top-right in viewport px - // (the same 8px inset as the in-frame layout; unscaled — top-layer UI - // reads as chrome, not page content). BEFORE the geometry branch: - // placement needs only the frame rect, and a not-yet-loaded or broken - // src must not leave the promoted strip floating unpositioned. Gated - // on the popover actually being open: without the Popover API, - // showPopover() threw (swallowed in _enterReframe), .ctl stays in - // its in-frame absolute layout, and viewport-px coordinates would - // shove it off-frame — and matches(':popover-open') itself throws - // there (unknown pseudo-class), hence the try/catch. - if (this.hasAttribute('data-reframe')) { - let onTop = false; - try { onTop = this._ctl.matches(':popover-open'); } catch {} - if (onTop) { - const r = this.getBoundingClientRect(); - this._ctl.style.left = (r.right - 8) + 'px'; - this._ctl.style.top = (r.top + 8) + 'px'; - } - } - if (!g) { - // Dimensions not known yet (before img load) — centered fit so there - // is no flash of an unpositioned image before the geometry lands. - const contain = (this.getAttribute('fit') || 'cover').toLowerCase() === 'contain'; - this._img.style.width = '100%'; - this._img.style.height = '100%'; - this._img.style.left = '50%'; - this._img.style.top = '50%'; - this._img.style.objectFit = contain ? 'contain' : 'cover'; - return; - } - // Baseline (cover-fill or contain-fit) × view scale. Width/height and - // left/top are all frame-% — depends only on the frame aspect ratio, so - // a responsive resize keeps the same crop. The spill layer mirrors the - // same box so its corners = image corners. - const k = g.base * this._view.s; - const w = (g.iw * k / g.fw * 100) + '%'; - const h = (g.ih * k / g.fh * 100) + '%'; - const l = (50 + this._view.x) + '%'; - const t = (50 + this._view.y) + '%'; - this._img.style.width = w; this._img.style.height = h; - this._img.style.left = l; this._img.style.top = t; - this._img.style.objectFit = ''; - if (this.hasAttribute('data-reframe')) { - // Top-layer spill: position in viewport px over the frame. The top - // layer escapes ancestor transforms entirely, so EVERY term must be - // in viewport units: getBoundingClientRect gives the frame's scaled - // origin AND size, and the rect/layout ratio rescales the ghost — - // sizing from layout px alone renders it 1/scale too large under a - // scaled deck slide. Inner ghost + handles stay box-relative. - const r = this.getBoundingClientRect(); - const sx = g.fw ? r.width / g.fw : 1; - const sy = g.fh ? r.height / g.fh : 1; - this._spill.style.width = (g.iw * k * sx) + 'px'; - this._spill.style.height = (g.ih * k * sy) + 'px'; - this._spill.style.left = (r.left + (50 + this._view.x) / 100 * r.width) + 'px'; - this._spill.style.top = (r.top + (50 + this._view.y) / 100 * r.height) + 'px'; - } - } - - _commitView() { - const v = { s: this._view.s, x: this._view.x, y: this._view.y }; - if (this._userUrl) v.u = this._userUrl; - // Framing-only (no u) persists too so an author-src slot remembers its - // crop; clearing the sidecar still falls through to src=. - if (this.id) setSlot(this.id, v); - else { this._local = v; } - } - - _render() { - // Shape / mask. Presets use border-radius so the dashed ring can - // follow the rounded outline; clip-path is only applied for an - // explicit `mask` (the ring is hidden there since a rectangle - // dashed border chopped by an arbitrary polygon looks broken). - const mask = this.getAttribute('mask'); - const shape = (this.getAttribute('shape') || 'rounded').toLowerCase(); - let radius = ''; - if (shape === 'circle') radius = '50%'; - else if (shape === 'pill') radius = '9999px'; - else if (shape === 'rounded') { - const n = parseFloat(this.getAttribute('radius')); - radius = (Number.isFinite(n) ? n : 12) + 'px'; - } - this._frame.style.borderRadius = mask ? '' : radius; - this._frame.style.clipPath = mask || ''; - this._ring.style.borderRadius = mask ? '' : radius; - this._ring.style.display = mask ? 'none' : ''; - - // Controls and reframe entry gate on this so share links stay read-only. - const editable = !!(window.omelette && window.omelette.writeFile); - this.toggleAttribute('data-editable', editable); - this._sub.style.display = editable ? '' : 'none'; - - // Content. The sidecar is also writable by the agent's write_file - // tool, so its value isn't guaranteed canvas-originated — only accept - // data:image/ URLs from it. The `src` attribute is author-controlled - // (Claude wrote it into the HTML) so it passes through unchanged. - let stored = this.id ? getSlot(this.id) : this._local; - if (stored && stored.u && !/^data:image\//i.test(stored.u)) stored = null; - const srcAttr = this.getAttribute('src') || ''; - this._userUrl = (stored && stored.u) || null; - const url = this._userUrl || srcAttr; - // Don't clobber an in-flight reframe with a store-triggered re-render. - if (!this.hasAttribute('data-reframe')) { - this._view = { - s: stored && Number.isFinite(stored.s) ? clampS(stored.s) : 1, - x: stored && Number.isFinite(stored.x) ? stored.x : 0, - y: stored && Number.isFinite(stored.y) ? stored.y : 0, - }; - } - this._cap.textContent = this.getAttribute('placeholder') || 'Drop an image'; - // Toggle via style.display — the [hidden] attribute alone loses to - // the display:flex / display:block rules in the stylesheet above. - // An Unsplash src with no credit attribute must NOT render — showing - // the photo uncredited is the Unsplash-terms violation itself. The - // error tile replaces the photo until the credit is written. A - // user-dropped image is the user's own content and always renders. - // Trimmed: credit is agent/user-editable content, and a whitespace- - // only value must count as missing — otherwise it would suppress the - // error tile AND render an empty credit box (no text, no links), - // exactly the unattributed state this gate exists to prevent. - const credit = (this.getAttribute('credit') || '').trim(); - const attrError = !!( - !credit && !this._userUrl && srcAttr && isUnsplashHost(srcAttr) - ); - this.toggleAttribute('data-attribution-error', attrError); - if (url && !attrError) { - const prev = this._img.getAttribute('src'); - if (prev !== url) { - // Replacing an already-shown image: mark the swap BEFORE setting - // src so the stale frame is never revealed (see the data-swapping - // stylesheet rules). First fill (prev empty) keeps the existing - // placeholder-until-load behavior — no spinner. _hidShowing - // covers the pick path's transient attribution-error wipe: prev - // is gone, but an image WAS showing, so this is a replacement. - if (prev || this._hidShowing) this.setAttribute('data-swapping', ''); - // Mark the swap BEFORE assigning src: complete keeps reporting - // the old settled request until the browser's - // update-the-image-data microtask runs, so same-task re-renders - // (the pick path's credit/credit-href setAttributes) need this - // flag, not complete, to know a load is in flight. - this._loadPending = true; - this._img.src = url; - this._ghost.src = url; - } else { - // Same-src re-render — release if settled, so an ingest-set - // spinner can't stick after a byte-identical re-upload (same - // data URL, no further load event ever fires). - this._releaseMask(); - } - this._hidShowing = false; - this._img.style.display = 'block'; - this._empty.style.display = 'none'; - this.setAttribute('data-filled', ''); - this._clampView(); - this._applyView(); - } else { - this.removeAttribute('data-swapping'); - // The src is being removed — no load/error will ever fire for it. - this._loadPending = false; - // A transient attribution-error wipe of a showing image happens on - // the pick path: the host sets src one setAttribute before credit, - // so render N hides the old image (attrError) and render N+1 - // restores a URL. Remember the wipe so that restore renders as a - // replacement (spinner), not a first fill (blank frame). - this._hidShowing = attrError && !!this._img.getAttribute('src'); - this._img.style.display = 'none'; - this._img.removeAttribute('src'); - this._ghost.removeAttribute('src'); - // The error tile owns the blocked-photo state; .empty stays for - // the genuinely-empty slot. - this._empty.style.display = attrError ? 'none' : 'flex'; - this.removeAttribute('data-filled'); - } - - // Credit belongs to the author src, so a user drop hides it. - // textContent + the http(s)-only funnel keep external strings inert. - const showCredit = !!(url && credit && !this._userUrl && !attrError); - this._credit.textContent = ''; - if (showCredit) { - // Validate once (resolved against the document, http(s) only), - // then append the terms-required utm referral params to links - // that point back at unsplash.com. - let href = ''; - const rawHref = this.getAttribute('credit-href') || ''; - if (rawHref) { - try { - const u = new URL(rawHref, document.baseURI); - if (u.protocol === 'http:' || u.protocol === 'https:') { - href = withReferral(u.href); - } - } catch {} - } - const mkLink = (text, linkHref) => { - const a = document.createElement('a'); - a.setAttribute('target', '_blank'); - a.setAttribute('rel', 'noopener noreferrer'); - a.setAttribute('href', linkHref); - a.textContent = text; - return a; - }; - // Unsplash's prescribed credit is TWO links — the photographer's - // name to their profile (credit-href) and 'Unsplash' to the - // homepage. Render that split whenever the text has the canonical - // shape; other text keeps the legacy single-link rendering. - const m = /^Photo by (.+) on Unsplash$/.exec(credit); - if (m) { - this._credit.appendChild(document.createTextNode('Photo by ')); - this._credit.appendChild( - href ? mkLink(m[1], href) : document.createTextNode(m[1]) - ); - this._credit.appendChild(document.createTextNode(' on ')); - this._credit.appendChild(mkLink('Unsplash', UNSPLASH_HOMEPAGE_HREF)); - } else if (href) { - this._credit.appendChild(mkLink(credit, href)); - } else { - this._credit.textContent = credit; - } - } - this.toggleAttribute('data-credit', showCredit); - } - } - - if (!customElements.get('image-slot')) { - customElements.define('image-slot', ImageSlot); - } -})(); diff --git a/design/baseline/# UI для онлайн звонка/support.js b/design/baseline/# UI для онлайн звонка/support.js deleted file mode 100644 index 6ca00e3..0000000 --- a/design/baseline/# UI для онлайн звонка/support.js +++ /dev/null @@ -1,1768 +0,0 @@ -// GENERATED from dc-runtime/src/*.ts — do not edit. Rebuild with `cd dc-runtime && bun run build`. -"use strict"; -(() => { - var __defProp = Object.defineProperty; - var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; - var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); - - // src/react.ts - function getReact() { - const R = window.React; - if (!R) throw new Error("dc-runtime: window.React is not available yet"); - return R; - } - function getReactDOM() { - const RD = window.ReactDOM; - if (!RD) throw new Error("dc-runtime: window.ReactDOM is not available yet"); - return RD; - } - var h = ((...args) => getReact().createElement( - ...args - )); - - // src/parse.ts - function parseDcDocument(doc) { - const dc = doc.querySelector("x-dc"); - if (!dc) return null; - const scriptEl = doc.querySelector("script[data-dc-script]"); - const { props, preview } = parseDataProps( - scriptEl?.getAttribute("data-props") ?? null - ); - return { - template: dc.innerHTML, - js: scriptEl ? scriptEl.textContent || "" : "", - props, - preview - }; - } - function parseDcText(src) { - const openMatch = /]*)?>/.exec(src); - if (!openMatch) return null; - const close = src.lastIndexOf(""); - if (close === -1 || close < openMatch.index) return null; - const template = src.slice(openMatch.index + openMatch[0].length, close); - const doc = new DOMParser().parseFromString(src, "text/html"); - const scriptEl = doc.querySelector("script[data-dc-script]"); - const { props, preview } = parseDataProps( - scriptEl?.getAttribute("data-props") ?? null - ); - return { - template, - js: scriptEl ? scriptEl.textContent || "" : "", - props, - preview - }; - } - function parseDataProps(raw) { - if (!raw) return { props: null, preview: null }; - let parsed; - try { - parsed = JSON.parse(raw); - } catch { - return { props: null, preview: null }; - } - if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { - return { props: null, preview: null }; - } - const obj = parsed; - const preview = obj.$preview && typeof obj.$preview === "object" ? obj.$preview : null; - const rest = {}; - for (const k of Object.keys(obj)) { - if (k[0] !== "$") rest[k] = obj[k]; - } - return { props: Object.keys(rest).length ? rest : null, preview }; - } - function dcNameFromPath(pathname) { - let p = pathname || ""; - try { - p = decodeURIComponent(p); - } catch { - } - const base = p.split("/").pop() || "Root"; - return base.replace(/\.dc\.html$/, "").replace(/\.html?$/, "") || "Root"; - } - - // src/boot.ts - var BASE_CSS = ` - .sc-placeholder{background:color-mix(in srgb,currentColor 8%,transparent); - border:1px solid color-mix(in srgb,currentColor 50%,transparent); - border-radius:2px;box-sizing:border-box;overflow:hidden} - @keyframes sc-shine{0%{background-position:100% 50%}100%{background-position:0% 50%}} - html.sc-dc-streaming .sc-placeholder, - html.sc-dc-streaming .sc-interp.sc-missing{position:relative; - background:color-mix(in srgb,currentColor 5%,transparent); - border-color:transparent} - html.sc-dc-streaming .sc-placeholder::before, - html.sc-dc-streaming .sc-interp.sc-missing::before{content:''; - position:absolute;inset:0;pointer-events:none; - background:linear-gradient(90deg,rgba(217,119,87,0) 25%,rgba(247,225,211,.95) 37%,rgba(217,119,87,0) 63%); - background-size:400% 100%;animation:sc-shine 1.4s ease infinite} - html.sc-dc-streaming .sc-placeholder:nth-child(n+9 of .sc-placeholder)::before, - html.sc-dc-streaming .sc-interp.sc-missing:nth-child(n+9 of .sc-interp.sc-missing)::before{animation:none; - background:color-mix(in srgb,currentColor 8%,transparent)} - .sc-placeholder-error{padding:4px 8px;font:11px/1.4 ui-monospace,monospace; - color:color-mix(in srgb,currentColor 70%,transparent);word-break:break-word} - .sc-interp.sc-missing{display:inline-block;width:2em;height:1em;overflow:hidden; - vertical-align:text-bottom;background:rgba(255,255,255,.3);border:1px solid rgba(0,0,0,.5); - border-radius:2px;box-sizing:border-box;color:transparent; - user-select:none} - .sc-interp.sc-unresolved{font-family:ui-monospace,monospace;font-size:.85em; - color:color-mix(in srgb,currentColor 50%,transparent); - background:color-mix(in srgb,currentColor 10%,transparent);border-radius:3px; - padding:0 3px} - .sc-host.sc-has-error{position:relative} - .sc-logic-error{position:absolute;top:8px;left:8px;z-index:2147483647;max-width:60ch; - padding:6px 10px;background:#b00020;color:#fff;font:12px/1.4 ui-monospace,monospace; - border-radius:4px;white-space:pre-wrap;pointer-events:none} - /* Mirrors PRINT_BASELINE_CSS in apps/web deck-stage-export.ts \u2014 keep both - in sync until dc-runtime regains a build step. */ - @media print { - @page { margin: 0.5cm; } - figure, table { break-inside: avoid; } - #dc-root, #dc-root > .sc-host { height: auto; } - *, *::before, *::after { - print-color-adjust: exact; -webkit-print-color-adjust: exact; - backdrop-filter: none !important; -webkit-backdrop-filter: none !important; - animation-delay: -99s !important; animation-duration: .001s !important; - animation-iteration-count: 1 !important; animation-fill-mode: both !important; - animation-play-state: running !important; transition-duration: 0s !important; - } - } - `; - var FULL_PAGE_CSS = "html,body{height:100%;margin:0}#dc-root,#dc-root>.sc-host{height:100%}"; - function rootNameForDocument(doc, loc) { - let bootPath = loc.pathname || ""; - if (!/\.dc\.html?$/i.test(safeDecode(bootPath))) { - try { - bootPath = new URL(doc.baseURI || "/").pathname; - } catch { - } - } - return dcNameFromPath(bootPath); - } - function safeDecode(s) { - try { - return decodeURIComponent(s); - } catch { - return s; - } - } - function boot(runtime, doc = document) { - const parsed = parseDcDocument(doc); - if (!parsed) return null; - const React = getReact(); - const rootName = rootNameForDocument(doc, location); - runtime.markFetched(rootName); - runtime.setRootName(rootName); - runtime.adoptParsed(rootName, parsed); - if (!window.__resources) { - fetch(location.href).then((res) => res.ok ? res.text() : "").then((t) => { - const raw = t ? parseDcText(t) : null; - if (raw?.template) runtime.updateHtml(rootName, raw.template); - }).catch(() => { - }); - } - const dc = doc.querySelector("x-dc"); - const hostEl = doc.createElement("div"); - hostEl.id = "dc-root"; - dc.replaceWith(hostEl); - if (!parsed.preview) { - const s = doc.createElement("style"); - s.textContent = FULL_PAGE_CSS; - doc.head.appendChild(s); - } - const Root = runtime.getDC(rootName); - const entry = runtime.registry.get(rootName); - function StandaloneRoot() { - const [, setTick] = React.useState(0); - React.useEffect(() => { - const sub = () => setTick((n) => n + 1); - entry.subs.add(sub); - return () => { - entry.subs.delete(sub); - }; - }, []); - const defaults = React.useMemo(() => { - const d = {}; - for (const k in entry.propsMeta || {}) { - const v = entry.propsMeta?.[k]?.default; - if (v !== void 0) d[k] = v; - } - return d; - }, [entry.propsMeta]); - return h(Root, { ...defaults, ...entry.propOverrides || {} }); - } - const ReactDOM = getReactDOM(); - if (ReactDOM.createRoot) - ReactDOM.createRoot(hostEl).render(h(StandaloneRoot)); - else ReactDOM.render(h(StandaloneRoot), hostEl); - return rootName; - } - - // src/expr.ts - var IDENT_RE = /^[A-Za-z_$][A-Za-z0-9_$]*/; - var NUMBER_RE = /^-?\d+(\.\d+)?$/; - function resolve(vals, src) { - const expr = String(src).trim(); - if (!expr) return void 0; - if (expr[0] === "(" && expr[expr.length - 1] === ")" && parensWrapWhole(expr)) { - return resolve(vals, expr.slice(1, -1)); - } - const eq = findTopLevelEquality(expr); - if (eq) { - const lv = resolve(vals, expr.slice(0, eq.index)); - const rv = resolve(vals, expr.slice(eq.index + eq.op.length)); - switch (eq.op) { - case "===": - return lv === rv; - case "!==": - return lv !== rv; - case "==": - return lv == rv; - default: - return lv != rv; - } - } - if (expr[0] === "!") return !resolve(vals, expr.slice(1)); - if (expr === "true") return true; - if (expr === "false") return false; - if (expr === "null") return null; - if (expr === "undefined") return void 0; - if (NUMBER_RE.test(expr)) return Number(expr); - if (expr.length >= 2 && (expr[0] === '"' || expr[0] === "'") && expr[expr.length - 1] === expr[0]) { - return expr.slice(1, -1); - } - return resolvePath(vals, expr); - } - function parensWrapWhole(expr) { - let depth = 0; - for (let i = 0; i < expr.length - 1; i++) { - if (expr[i] === "(") depth++; - else if (expr[i] === ")") { - depth--; - if (depth === 0) return false; - } - } - return true; - } - function findTopLevelEquality(expr) { - let depth = 0; - for (let i = 0; i < expr.length; i++) { - const c = expr[i]; - if (c === "[" || c === "(") depth++; - else if (c === "]" || c === ")") depth--; - else if (depth === 0 && (c === "=" || c === "!") && expr[i + 1] === "=") { - if (i > 0 && (expr[i - 1] === "=" || expr[i - 1] === "!")) continue; - if (!expr.slice(0, i).trim()) continue; - const op = expr[i + 2] === "=" ? c + "==" : c + "="; - return { index: i, op }; - } - } - return null; - } - function resolvePath(vals, expr) { - const head = expr.match(IDENT_RE); - if (!head) return void 0; - let cur = vals == null ? void 0 : vals[head[0]]; - let i = head[0].length; - while (i < expr.length) { - if (expr[i] === ".") { - const m = expr.slice(i + 1).match(IDENT_RE) || expr.slice(i + 1).match(/^\d+/); - if (!m) return void 0; - cur = cur == null ? void 0 : cur[m[0]]; - i += 1 + m[0].length; - } else if (expr[i] === "[") { - let depth = 1; - let j = i + 1; - while (j < expr.length && depth > 0) { - if (expr[j] === "[") depth++; - else if (expr[j] === "]") { - depth--; - if (depth === 0) break; - } - j++; - } - if (depth !== 0) return void 0; - const key = resolve(vals, expr.slice(i + 1, j)); - cur = cur == null ? void 0 : cur[key]; - i = j + 1; - } else { - return void 0; - } - } - return cur; - } - - // src/encode.ts - var CAMEL_ATTR = "sc-camel-"; - var INLINE_TEXT_TAGS = new Set( - "a abbr b bdi bdo br cite code del dfn em i ins kbd mark q s samp small span strike strong sub sup u var wbr".split( - " " - ) - ); - var RAW_WRAP = { - select: "sc-raw-select", - table: "sc-raw-table", - tbody: "sc-raw-tbody", - thead: "sc-raw-thead", - tfoot: "sc-raw-tfoot", - tr: "sc-raw-tr", - td: "sc-raw-td", - th: "sc-raw-th", - caption: "sc-raw-caption" - }; - var RAW_UNWRAP = Object.fromEntries( - Object.entries(RAW_WRAP).map(([k, v]) => [v, k]) - ); - var EVENT_MAP = { - onclick: "onClick", - onchange: "onChange", - oninput: "onInput", - onsubmit: "onSubmit", - onkeydown: "onKeyDown", - onkeyup: "onKeyUp", - onkeypress: "onKeyPress", - onmousedown: "onMouseDown", - onmouseup: "onMouseUp", - onmouseenter: "onMouseEnter", - onmouseleave: "onMouseLeave", - onfocus: "onFocus", - onblur: "onBlur", - ondoubleclick: "onDoubleClick", - oncontextmenu: "onContextMenu", - onmousemove: "onMouseMove", - onmouseover: "onMouseOver", - onmouseout: "onMouseOut", - onpointerdown: "onPointerDown", - onpointerup: "onPointerUp", - onpointermove: "onPointerMove", - onpointerenter: "onPointerEnter", - onpointerleave: "onPointerLeave", - onpointercancel: "onPointerCancel", - onpointerover: "onPointerOver", - onpointerout: "onPointerOut", - ongotpointercapture: "onGotPointerCapture", - onlostpointercapture: "onLostPointerCapture", - ontouchstart: "onTouchStart", - ontouchend: "onTouchEnd", - ontouchmove: "onTouchMove", - ontouchcancel: "onTouchCancel", - ondragstart: "onDragStart", - ondragend: "onDragEnd", - ondragenter: "onDragEnter", - ondragleave: "onDragLeave", - ondragover: "onDragOver", - onanimationstart: "onAnimationStart", - onanimationend: "onAnimationEnd", - onanimationiteration: "onAnimationIteration", - ontransitionend: "onTransitionEnd" - }; - var ATTRS = `(?:[^>"']|"[^"]*"|'[^']*')*`; - var IMPORT_SELF_CLOSE_RE = new RegExp( - "<(x-import|dc-import)(" + ATTRS + ")/>", - "gi" - ); - var CAMEL_ATTR_RE = /(\s)([a-z]+[A-Z][A-Za-z0-9]*)(\s*=)/g; - function encodeCamelAttrs(html) { - return html.replace( - CAMEL_ATTR_RE, - (_, sp, name, eq) => sp + CAMEL_ATTR + name.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase()) + eq - ); - } - function encodeCase(html) { - html = html.replace( - IMPORT_SELF_CLOSE_RE, - (_, t, a) => "<" + t + a + ">" - ); - html = html.replace(/)/gi, "/gi, ""); - html = encodeCamelAttrs(html); - for (const [real, alias] of Object.entries(RAW_WRAP)) { - html = html.replace( - new RegExp("(])", "gi"), - "$1" + alias - ); - } - return html; - } - function kebabToCamel(s) { - return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase()); - } - function cssToObj(css) { - const o = {}; - for (const decl of css.split(";")) { - const i = decl.indexOf(":"); - if (i < 0) continue; - const prop = decl.slice(0, i).trim(); - o[prop.startsWith("--") ? prop : kebabToCamel(prop)] = decl.slice(i + 1).trim(); - } - return o; - } - function compileAttr(raw) { - const whole = raw.match(/^\s*\{\{([\s\S]+?)\}\}\s*$/); - if (whole) { - const path = whole[1]; - return (vals) => resolve(vals, path); - } - if (raw.includes("{{")) { - const parts = raw.split(/\{\{([\s\S]+?)\}\}/g); - return (vals) => parts.map((s, i) => i & 1 ? resolve(vals, s) ?? "" : s).join(""); - } - return () => raw; - } - - // src/compile.ts - function collectProps(node, kind, host) { - const propGetters = []; - const pseudoClasses = []; - let hintSize = null; - for (const { name, value } of [...node.attributes]) { - if (name === "sc-name" || name === "data-dc-tpl") continue; - let key = name; - if (key.startsWith(CAMEL_ATTR)) - key = kebabToCamel(key.slice(CAMEL_ATTR.length)); - if (key === "hint-size") { - hintSize = value; - continue; - } - if (key.startsWith("style-")) { - pseudoClasses.push(host.pseudoClass(key.slice(6), value)); - continue; - } - if (kind !== "dom") { - if (key.includes("-") && !(kind === "x-import" && (key.startsWith("aria-") || key.startsWith("data-")))) - key = kebabToCamel(key); - } else { - if (key === "class") key = "className"; - else if (key === "for") key = "htmlFor"; - else if (key.startsWith("on")) - key = EVENT_MAP[key] || "on" + key[2].toUpperCase() + key.slice(3); - } - propGetters.push([key, compileAttr(value)]); - } - return { propGetters, pseudoClasses, hintSize }; - } - var HOST_STYLE_PROPS = /* @__PURE__ */ new Set([ - "position", - "left", - "right", - "top", - "bottom", - "inset", - "width", - "height", - "z-index", - "transform" - ]); - function hostPositionStyle(style) { - const all = typeof style === "string" ? cssToObj(style) : style != null && typeof style === "object" ? style : null; - if (!all) return void 0; - const out = {}; - for (const [k, v] of Object.entries(all)) { - const kebab = k.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase()); - if (HOST_STYLE_PROPS.has(kebab)) out[k] = v; - } - return Object.keys(out).length ? out : void 0; - } - function compileTemplate(html, host) { - const tpl = document.createElement("template"); - //! nosemgrep: direct-inner-html-assignment - tpl.innerHTML = encodeCase(html); - let tplN = 0; - (function stamp(node) { - if (node.nodeType === Node.ELEMENT_NODE) { - node.setAttribute("data-dc-tpl", String(tplN++)); - } - for (const c of node.childNodes) stamp(c); - })(tpl.content); - const builders = walkChildren(tpl.content, host); - const render = ((vals, ctx) => builders.map((b, i) => b(vals || {}, ctx, i))); - render.__annotated = tpl.innerHTML; - return render; - } - function walkChildren(node, host) { - return [...node.childNodes].map((c) => walk(c, host)).filter((b) => b != null); - } - function walk(node, host) { - if (node.nodeType === Node.TEXT_NODE) return walkText(node); - if (node.nodeType !== Node.ELEMENT_NODE) return null; - const el = node; - const tag = el.tagName.toLowerCase(); - if (tag === "sc-for") return walkFor(el, host); - if (tag === "sc-if") return walkIf(el, host); - if (tag === "x-import") return walkXImport(el, host); - if (tag === "sc-helmet") return host.helmet(el); - if (tag === "dc-import") return walkComponent(el, host); - return walkElement(el, host); - } - var warnedHoles = /* @__PURE__ */ new Set(); - function warnUnresolved(ctx, what) { - const key = (ctx?.__name || "?") + "\0" + what; - if (warnedHoles.has(key)) return; - warnedHoles.add(key); - console.warn("[dc-runtime] " + (ctx?.__name || "template") + ": " + what); - } - function walkText(node) { - const txt = node.nodeValue ?? ""; - if (!txt.includes("{{")) { - if (!txt.trim() && !txt.includes(" ")) return null; - return () => txt; - } - const parts = txt.split(/\{\{([\s\S]+?)\}\}/g); - return (vals, ctx, key) => h( - getReact().Fragment, - { key }, - ...parts.map((p, i) => { - if (!(i & 1)) return p; - const v = resolve(vals, p); - if (v === void 0) { - if (!ctx?.__streamingNow) { - if (document.body?.hasAttribute("data-dc-editor-on")) { - return h( - "span", - { key: i, className: "sc-interp sc-unresolved" }, - "{{ " + p.trim() + " }}" - ); - } - warnUnresolved( - ctx, - "{{ " + p.trim() + " }} never resolved \u2014 rendered as empty" - ); - return null; - } - return h( - "span", - { key: i, className: "sc-interp sc-missing" }, - p.trim() - ); - } - if (getReact().isValidElement(v) || Array.isArray(v)) { - return h(getReact().Fragment, { key: i }, v); - } - if (v === null || typeof v === "boolean") return null; - return h("span", { key: i, className: "sc-interp" }, String(v)); - }) - ); - } - function walkFor(el, host) { - const listGet = compileAttr(el.getAttribute("list") || ""); - const asName = el.getAttribute("as") || "item"; - const hintN = parseInt(el.getAttribute("hint-placeholder-count") || "0", 10); - const kids = walkChildren(el, host); - const listSrc = el.getAttribute("list") || ""; - return (vals, ctx, key) => { - let list = listGet(vals); - if (!Array.isArray(list)) { - if (!ctx?.__streamingNow) { - if (list !== void 0 && list !== null) { - warnUnresolved( - ctx, - 'sc-for list="' + listSrc + '" is not an array (' + typeof list + ")" - ); - } - list = []; - } else { - list = hintN > 0 ? Array(hintN).fill(void 0) : []; - } - } - return h( - getReact().Fragment, - { key }, - list.map((item, i) => { - const sub = { ...vals, [asName]: item, $index: i }; - return h( - getReact().Fragment, - { key: i }, - kids.map((b, j) => b(sub, ctx, j)) - ); - }) - ); - }; - } - function walkIf(el, host) { - const valGet = compileAttr(el.getAttribute("value") || ""); - const hintRaw = el.getAttribute("hint-placeholder-val"); - const hintGet = hintRaw != null ? compileAttr(hintRaw) : null; - const kids = walkChildren(el, host); - return (vals, ctx, key) => { - let v = valGet(vals); - if (v === void 0 && hintGet && ctx?.__streamingNow) v = hintGet(vals); - return v ? h( - getReact().Fragment, - { key }, - kids.map((b, j) => b(vals, ctx, j)) - ) : null; - }; - } - function walkComponent(el, host) { - const name = el.getAttribute("name") || el.getAttribute("component") || ""; - el.removeAttribute("name"); - el.removeAttribute("component"); - const tplId = el.getAttribute("data-dc-tpl"); - const styleRaw = el.getAttribute("style"); - el.removeAttribute("style"); - const styleGet = styleRaw != null ? compileAttr(styleRaw) : null; - const { propGetters, hintSize } = collectProps(el, "dc-import", host); - const kids = walkChildren(el, host); - return (vals, ctx, key) => { - const props = { - key, - __hintSize: hintSize, - __tplId: tplId, - __hostStyle: styleGet ? hostPositionStyle(styleGet(vals)) : void 0 - }; - for (const [k, g] of propGetters) { - const v = g(vals); - if (k === "dcProps") { - if (v && typeof v === "object") Object.assign(props, v); - continue; - } - props[k] = v; - } - if (kids.length) props.children = kids.map((b, j) => b(vals, ctx, j)); - return h(host.component(name), props); - }; - } - function walkXImport(el, host) { - const globalNameGet = compileAttr( - el.getAttribute("component-from-global-scope") || "" - ); - const exportNameGet = compileAttr( - el.getAttribute("component") || el.getAttribute("name") || "" - ); - const fromRaw = el.getAttribute("from") || (el.getAttribute("component-from-global-scope") ? "" : el.getAttribute("src") || el.getAttribute("import") || ""); - const urls = fromRaw.trim() ? fromRaw.trim().split(/\s+/) : []; - const url = urls.length ? urls[urls.length - 1] : ""; - const kindOf = (u) => /\.(jsx|tsx)(\?|#|$)/i.test(u) ? "jsx" : "js"; - const tplId = el.getAttribute("data-dc-tpl"); - const styleRaw = el.getAttribute("style"); - el.removeAttribute("style"); - const styleGet = styleRaw != null ? compileAttr(styleRaw) : null; - const wrap = tplId != null || styleGet != null; - const { propGetters, hintSize } = collectProps(el, "x-import", host); - const hasContent = el.children.length > 0 || !!(el.textContent || "").trim(); - const kids = hasContent ? walkChildren(el, host) : []; - const urlBindable = fromRaw.includes("{{"); - if (urls.length && !urlBindable) { - let prev; - for (const u of urls) prev = host.loadExternal(kindOf(u), u, prev); - } - const evalName = (g, vals) => { - const v = g(vals); - const s = v == null ? "" : String(v); - return s.includes("{{") ? "" : s; - }; - return (vals, ctx, key) => { - const globalName = evalName(globalNameGet, vals); - const name = globalName || evalName(exportNameGet, vals); - const C = !name || urlBindable ? null : globalName ? host.resolveExternalGlobal(url, globalName) : host.resolveExternal(url, name); - const hostStyle = styleGet ? hostPositionStyle(styleGet(vals)) : void 0; - const wrapper = wrap ? { - key, - className: "sc-host-x", - "data-dc-tpl": tplId, - style: hostStyle || { display: "contents" } - } : null; - if (!C) { - const error = urlBindable ? "x-import `from` cannot contain {{ \u2026 }} \u2014 module URLs are resolved at parse time; use a literal URL" : host.resolveExternalError(url, name); - const ph = host.placeholder({ - key: wrapper ? void 0 : key, - name, - hintSize, - error - }); - return wrapper ? h("div", wrapper, ph) : ph; - } - const props = wrapper ? {} : { key }; - let unresolvedHole = false; - for (const [k, g] of propGetters) { - if (k === "component" || k === "componentFromGlobalScope" || k === "from") { - continue; - } - const v = g(vals); - if (v === void 0) unresolvedHole = true; - if (k === "dcProps") { - if (v && typeof v === "object") Object.assign(props, v); - continue; - } - props[k] = v; - } - if (unresolvedHole && ctx?.__htmlStreamingNow) { - const ph = host.placeholder({ - key: wrapper ? void 0 : key, - name, - hintSize, - error: null - }); - return wrapper ? h("div", wrapper, ph) : ph; - } - if (kids.length) props.children = kids.map((b, j) => b(vals, ctx, j)); - return wrapper ? h("div", wrapper, h(C, props)) : h(C, props); - }; - } - function contentKey(el) { - const clone = el.cloneNode(true); - for (const d of clone.querySelectorAll("*")) { - while (d.attributes.length) d.removeAttribute(d.attributes[0].name); - } - const s = clone.innerHTML; - let h2 = 5381; - for (let i = 0; i < s.length; i++) h2 = (h2 << 5) + h2 + s.charCodeAt(i) | 0; - return s.length + "." + (h2 >>> 0).toString(36); - } - var NEVER_CONTENT_KEYED = new Set( - "script style textarea option title select canvas iframe video audio".split( - " " - ) - ); - var NOT_INLINE_SELECTOR = ":not(" + [...INLINE_TEXT_TAGS].join(",") + ")"; - function walkElement(el, host) { - const realTag = RAW_UNWRAP[el.localName] || el.localName; - const tplId = el.getAttribute("data-dc-tpl"); - const inlineOnly = el.childNodes.length > 0 && !NEVER_CONTENT_KEYED.has(realTag) && el.querySelector(NOT_INLINE_SELECTOR) === null; - const keySuffix = inlineOnly ? "|" + contentKey(el) : ""; - const { propGetters, pseudoClasses } = collectProps(el, "dom", host); - const kids = walkChildren(el, host); - return (vals, ctx, key) => { - const props = { - key: key + keySuffix, - "data-dc-tpl": tplId - }; - for (const [k, g] of propGetters) { - let v = g(vals); - if (k === "style" && typeof v === "string") v = cssToObj(v); - if ((k === "value" || k === "checked") && v === void 0) { - v = k === "checked" ? false : ""; - } - props[k] = v; - } - if (pseudoClasses.length) { - props.className = [props.className, ...pseudoClasses].filter(Boolean).join(" "); - } - return h(realTag, props, ...kids.map((b, j) => b(vals, ctx, j))); - }; - } - - // src/logic.ts - var StreamableLogic = class { - constructor(props) { - __publicField(this, "props"); - __publicField(this, "state", {}); - /** Back-pointer to the wrapper component, installed after construction. */ - __publicField(this, "__host"); - this.props = props || {}; - } - setState(update, cb) { - this.__host && this.__host.__setLogicState(update, cb); - } - forceUpdate() { - this.__host && this.__host.forceUpdate(); - } - componentDidMount() { - } - componentDidUpdate(_prevProps) { - } - componentWillUnmount() { - } - /** The flat object the template renders against (merged over props). */ - renderVals() { - return {}; - } - }; - function evalDcLogic(src) { - //! nosemgrep: eval-and-function-constructor - const fn = new Function( - "DCLogic", - "StreamableLogic", - "React", - src + '\n;return (typeof Component!=="undefined"&&Component)||undefined;' - ); - return fn(StreamableLogic, StreamableLogic, getReact()); - } - - // src/component.ts - function shallowEqual(a, b) { - if (!b) return false; - const ak = Object.keys(a).filter((k) => k !== "children"); - const bk = Object.keys(b).filter((k) => k !== "children"); - if (ak.length !== bk.length) return false; - for (const k of ak) if (a[k] !== b[k]) return false; - return true; - } - function Placeholder({ - name, - hintSize, - streaming, - error - }) { - const [w, hgt] = (hintSize || "100%,60px").split(","); - return h( - "div", - { - className: "sc-placeholder" + (streaming ? " sc-streaming" : ""), - style: { width: w.trim(), height: hgt && hgt.trim() }, - title: name - }, - error ? h( - "div", - { className: "sc-placeholder-error" }, - (name ? name + ": " : "") + error - ) : null - ); - } - function hintToMin(hint) { - if (!hint) return void 0; - const [w, hgt] = hint.split(","); - return { minWidth: w.trim(), minHeight: hgt && hgt.trim() }; - } - function createComponentFactory(registry, ensureFetched) { - const React = getReact(); - const AncestorContext = React.createContext([]); - class StreamableComponent extends React.Component { - constructor(props) { - super(props); - __publicField(this, "__name"); - __publicField(this, "__sub"); - __publicField(this, "__needsDidMount", false); - /** Snapshot of the registry's streaming flags taken at render time — - * builders read it off the RenderCtx (this) to pick placeholder vs - * render-nothing for unresolved values. */ - __publicField(this, "__streamingNow", false); - __publicField(this, "__htmlStreamingNow", false); - /** When a construct throws, remember the (class, registry.ver, props) - * triple so render-time reconcile doesn't re-attempt it on every parent - * re-render. A registry bump (new class, template, external module - * resolving via bumpAll) changes `ver` and breaks the memo so an - * env-dependent constructor can self-heal. */ - __publicField(this, "__failedLogic", null); - __publicField(this, "__failedUserProps", null); - __publicField(this, "__failedVer", -1); - /** Per-instance constructor error — kept here (not on the registry entry) - * so one instance's successful construct can't hide a sibling's failure, - * and a construct can never wipe an eval error `updateJs` recorded on - * `r.logicError`. */ - __publicField(this, "__ctorError", null); - __publicField(this, "logic"); - this.__name = props.__name; - this.state = { __v: 0, __err: null }; - this.__sub = () => { - if (this.state.__err) this.setState({ __err: null }); - this.forceUpdate(); - }; - this.__makeLogic(registry.get(this.__name).Logic, null); - ensureFetched(this.__name); - } - /** Error-boundary hook: a render crash anywhere in this DC's subtree - * (its own template, an x-import'd component, a child DC without its - * own deeper boundary) lands here instead of unmounting the page. */ - static getDerivedStateFromError(e) { - return { __err: e instanceof Error && e.message ? e.message : String(e) }; - } - componentDidCatch(e, info) { - console.error( - "[dc-runtime] render error in <" + this.__name + ">:", - e, - info?.componentStack || "" - ); - } - /** Instantiate the logic class (or the no-op base) and adopt `prevState` - * over its initial state — used both at mount and on hot-swap. */ - __makeLogic(Logic, prevState) { - const L = Logic || StreamableLogic; - try { - this.logic = new L(this.__userProps()); - this.__failedLogic = null; - this.__failedUserProps = null; - this.__ctorError = null; - } catch (e) { - console.error(e); - this.__failedLogic = Logic; - this.__failedUserProps = this.__userProps(); - this.__failedVer = registry.get(this.__name).ver; - this.__ctorError = this.__name + ": " + (e instanceof Error && e.message ? e.message : String(e)); - this.logic = new StreamableLogic( - this.__userProps() - ); - } - this.logic.__host = this; - if (prevState) - this.logic.state = { ...this.logic.state || {}, ...prevState }; - } - /** The props the author's logic + template see — internal __-prefixed - * wiring stripped. */ - __userProps() { - const { __name, __hintSize, __tplId, __hostStyle, ...rest } = this.props; - return rest; - } - __setLogicState(update, cb) { - const prev = this.logic.state; - const patch = typeof update === "function" ? update(prev) : update; - this.logic.state = { ...prev, ...patch }; - this.setState((s) => ({ __v: s.__v + 1 }), cb); - } - /** Swap the logic instance when the registry's Logic class changed - * (streaming completion, hot reload). State carries over; didMount - * re-fires after the swap commits so refs exist. */ - __reconcileLogic() { - const r = registry.get(this.__name); - const Next = r.Logic; - const Cur = this.logic.constructor; - if (Next === Cur || !Next && Cur === StreamableLogic || Next === this.__failedLogic && r.ver === this.__failedVer && shallowEqual(this.__userProps(), this.__failedUserProps)) { - return; - } - if (!this.__needsDidMount) { - try { - this.logic.componentWillUnmount(); - } catch (e) { - console.error(e); - } - } - this.__makeLogic(Next, this.logic.state); - this.__needsDidMount = true; - } - componentDidMount() { - registry.get(this.__name).subs.add(this.__sub); - try { - this.logic.componentDidMount(); - } catch (e) { - console.error(e); - } - } - componentDidUpdate(prevProps) { - this.logic.props = this.__userProps(); - if (this.__needsDidMount) { - if (this.state.__err || !registry.get(this.__name).tpl) return; - this.__needsDidMount = false; - try { - this.logic.componentDidMount(); - } catch (e) { - console.error(e); - } - } else { - try { - this.logic.componentDidUpdate(prevProps); - } catch (e) { - console.error(e); - } - } - } - componentWillUnmount() { - registry.get(this.__name).subs.delete(this.__sub); - if (!this.__needsDidMount) { - try { - this.logic.componentWillUnmount(); - } catch (e) { - console.error(e); - } - } - } - render() { - const r = registry.get(this.__name); - const cls = "sc-host" + (r.htmlStreaming ? " sc-streaming-html" : "") + (r.jsStreaming ? " sc-streaming-js" : ""); - const hintStyle = r.htmlStreaming ? hintToMin(this.props.__hintSize) : void 0; - const hostStyle = this.props.__hostStyle || hintStyle ? { ...hintStyle || {}, ...this.props.__hostStyle || {} } : void 0; - const hostBase = { - className: cls, - style: hostStyle, - "data-sc-name": this.__name, - "data-dc-tpl": this.props.__tplId - }; - const chain = Array.isArray(this.context) ? this.context : []; - if (chain.includes(this.__name)) { - const cycle = [ - ...chain.slice(chain.indexOf(this.__name)), - this.__name - ].join(" \u2192 "); - return h( - "div", - { ...hostBase, className: cls + " sc-has-error" }, - h(Placeholder, { - name: this.__name, - hintSize: this.props.__hintSize, - error: "circular import: " + cycle - }) - ); - } - if (this.state.__err) { - return h( - "div", - { ...hostBase, className: cls + " sc-has-error" }, - h( - "div", - { className: "sc-logic-error", "data-omelette-chrome": "" }, - this.__name + ": " + this.state.__err - ), - h(Placeholder, { - name: this.__name, - hintSize: this.props.__hintSize, - error: this.state.__err - }) - ); - } - this.__reconcileLogic(); - if (!r.tpl) { - return h( - "div", - hostBase, - h(Placeholder, { name: this.__name, hintSize: this.props.__hintSize }) - ); - } - const userProps = this.__userProps(); - this.logic.props = userProps; - let vals = userProps; - let renderErr = r.logicError || this.__ctorError; - try { - vals = { ...userProps, ...this.logic.renderVals() || {} }; - } catch (e) { - console.error(e); - renderErr = this.__name + ".renderVals(): " + (e instanceof Error && e.message ? e.message : String(e)); - } - this.__streamingNow = !!(r.htmlStreaming || r.jsStreaming); - this.__htmlStreamingNow = !!r.htmlStreaming; - return h( - "div", - { ...hostBase, className: cls + (renderErr ? " sc-has-error" : "") }, - renderErr && h( - "div", - { className: "sc-logic-error", "data-omelette-chrome": "" }, - renderErr - ), - h( - AncestorContext.Provider, - { value: [...chain, this.__name] }, - r.tpl(vals, this) - ) - ); - } - } - __publicField(StreamableComponent, "contextType", AncestorContext); - const named = /* @__PURE__ */ new Map(); - function getDC(name) { - const hit = named.get(name); - if (hit) return hit; - function Dispatcher(p) { - const [, setTick] = React.useState(0); - React.useEffect(() => { - const sub = () => setTick((n) => n + 1); - registry.get(name).subs.add(sub); - return () => { - registry.get(name).subs.delete(sub); - }; - }, []); - ensureFetched(name); - return h(StreamableComponent, { ...p, __name: name }); - } - Dispatcher.displayName = name; - named.set(name, Dispatcher); - return Dispatcher; - } - return { - getDC, - StreamableComponent - }; - } - - // src/bundled.ts - function bundledBlob(url) { - const blobs = window.__resourceBlobs; - const b = blobs ? blobs[url.split("#")[0]] : void 0; - return b instanceof Blob ? b : null; - } - - // src/cdn.ts - var REACT_URL = "https://unpkg.com/react@18.3.1/umd/react.production.min.js"; - var REACT_SRI = "sha384-DGyLxAyjq0f9SPpVevD6IgztCFlnMF6oW/XQGmfe+IsZ8TqEiDrcHkMLKI6fiB/Z"; - var REACT_DOM_URL = "https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"; - var REACT_DOM_SRI = "sha384-gTGxhz21lVGYNMcdJOyq01Edg0jhn/c22nsx0kyqP0TxaV5WVdsSH1fSDUf5YJj1"; - var BABEL_URL = "https://unpkg.com/@babel/standalone@7.29.0/babel.min.js"; - var BABEL_SRI = "sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y"; - function cdnScriptFor(url, sri) { - const res = window.__resources; - const v = res ? res[url] : void 0; - return typeof v === "string" && v ? { src: v } : { src: url, integrity: sri }; - } - - // src/external.ts - var isCustomElementName = (n) => !n.includes(".") && n.includes("-"); - function isRenderableType(g) { - if (typeof g === "function") return !isElementClass(g); - return typeof g === "object" && g !== null && typeof g.$$typeof === "symbol"; - } - function resolveDottedPath(root, name) { - let cur = root; - for (const seg of name.split(".")) { - if (cur == null) return void 0; - cur = cur[seg]; - } - return cur; - } - var GLOBAL_POLL_INTERVAL_MS = 50; - var GLOBAL_POLL_TIMEOUT_MS = 3e4; - function createExternalModules(onResolved) { - const cache = /* @__PURE__ */ new Map(); - let babelLoading = null; - const reportedMissing = /* @__PURE__ */ new Map(); - const polling = /* @__PURE__ */ new Set(); - function ensureBabel() { - if (window.Babel) return Promise.resolve(); - if (babelLoading) return babelLoading; - const babel = cdnScriptFor(BABEL_URL, BABEL_SRI); - babelLoading = new Promise((res, rej) => { - const s = document.createElement("script"); - s.src = babel.src; - if (babel.integrity) { - s.integrity = babel.integrity; - s.crossOrigin = "anonymous"; - } - s.onload = () => res(); - s.onerror = rej; - document.head.appendChild(s); - }); - return babelLoading; - } - const pending = /* @__PURE__ */ new Map(); - function load(kind, url, after) { - const existing = pending.get(url); - if (existing) return existing; - cache.set(url, null); - console.info("[dc-runtime] x-import: loading", url, "(" + kind + ")"); - const ready = Promise.all([ - kind === "jsx" ? ensureBabel() : Promise.resolve(), - after ?? Promise.resolve() - ]); - const p = ready.then(() => { - const pre = bundledBlob(url); - if (pre) return pre.text(); - return fetch(url).then((r) => { - if (!r.ok) throw new Error("HTTP " + r.status); - return r.text(); - }); - }).then((src) => { - const code = kind === "jsx" ? window.Babel.transform(src, { - filename: url, - presets: ["react", "typescript"] - }).code : src; - const module = { exports: {} }; - const before = new Set(Object.keys(window)); - //! nosemgrep: eval-and-function-constructor - new Function("React", "module", "exports", "require", code)( - getReact(), - module, - module.exports, - () => ({}) - ); - const globals = {}; - for (const k of Object.keys(window)) { - if (!before.has(k) && typeof window[k] === "function") { - globals[k] = window[k]; - } - } - cache.set(url, { mod: module.exports, globals }); - console.info( - "[dc-runtime] x-import: loaded", - url, - "\u2014 exports:", - Object.keys(module.exports), - "window globals:", - Object.keys(globals) - ); - onResolved(); - }).catch((e) => { - cache.set(url, { - mod: {}, - globals: {}, - error: "failed to load: " + (e instanceof Error && e.message ? e.message : String(e)) - }); - console.error( - "[dc-runtime] x-import: FAILED to load", - url, - "(" + kind + ")", - e - ); - onResolved(); - }); - pending.set(url, p); - return p; - } - function resolve2(url, name) { - const entry = cache.get(url); - if (!entry) return null; - const { mod, globals } = entry; - const C = mod && mod[name] || globals && globals[name] || typeof window !== "undefined" && window[name] || mod && mod.default; - if (typeof C === "function") return C; - const key = url + "\0" + name; - if (!reportedMissing.has(key)) { - reportedMissing.set( - key, - entry.error || 'no export named "' + name + '" (has: ' + Object.keys(mod).join(", ") + ")" - ); - console.error( - "[dc-runtime] x-import: module", - url, - "loaded but has no component named", - JSON.stringify(name), - "\u2014 available exports:", - Object.keys(mod), - "window globals:", - Object.keys(globals), - ". The module must `module.exports = {" + name + "}` or set `window." + name + "`." - ); - } - return null; - } - function waitForGlobal(name) { - if (polling.has(name)) return; - polling.add(name); - const started = Date.now(); - const isCE = isCustomElementName(name); - const tick = () => { - const found = isCE ? customElements.get(name) : isRenderableType(resolveDottedPath(window, name)); - if (found) { - polling.delete(name); - onResolved(); - return; - } - if (Date.now() - started >= GLOBAL_POLL_TIMEOUT_MS) { - console.warn( - "[dc-runtime] x-import: global", - JSON.stringify(name), - "never appeared on window after " + GLOBAL_POLL_TIMEOUT_MS + "ms" - ); - return; - } - setTimeout(tick, GLOBAL_POLL_INTERVAL_MS); - }; - setTimeout(tick, GLOBAL_POLL_INTERVAL_MS); - } - function resolveGlobal(url, name) { - const isCE = isCustomElementName(name); - if (!url) { - if (isCE) { - if (customElements.get(name)) return name; - waitForGlobal(name); - return null; - } - const g2 = resolveDottedPath(window, name); - if (isRenderableType(g2)) return g2; - waitForGlobal(name); - return null; - } - const entry = cache.get(url); - if (!entry) return null; - if (isCE && customElements.get(name)) return name; - const g = entry.globals[name] ?? resolveDottedPath(window, name); - if (isRenderableType(g)) return g; - if (name.includes(".")) return null; - const key = url + "\0global\0" + name; - if (!reportedMissing.has(key)) { - reportedMissing.set(key, null); - if (isCE && !customElements.get(name)) { - console.warn( - "[dc-runtime] x-import:", - url, - "loaded but no custom element", - JSON.stringify(name), - "is registered and window." + name + " is not a function \u2014 rendering <" + name + "> as an unknown element." - ); - } - } - return name; - } - function getError(url, name) { - const entry = cache.get(url); - if (entry?.error) return entry.error; - return reportedMissing.get(url + "\0" + name) || null; - } - return { load, resolve: resolve2, resolveGlobal, getError }; - } - function isElementClass(g) { - try { - return typeof g === "function" && typeof HTMLElement !== "undefined" && g.prototype instanceof HTMLElement; - } catch { - return false; - } - } - - // src/atomics.ts - var ATOMIC_CSS = ( - // layout - ".fx{display:flex}.col{display:flex;flex-direction:column}.grid{display:grid}.ac{align-items:center}.jc{justify-content:center}.jb{justify-content:space-between}.f1{flex:1}.noshrink{flex-shrink:0}.wrap{flex-wrap:wrap}.fw5{font-weight:500}.fw6{font-weight:600}.fw7{font-weight:700}.fw8{font-weight:800}.fs11{font-size:11px}.fs12{font-size:12px}.fs13{font-size:13px}.fs14{font-size:14px}.fs15{font-size:15px}.fs16{font-size:16px}.fs20{font-size:20px}.fs22{font-size:22px}.upper{text-transform:uppercase}.tc{text-align:center}.nowrap{white-space:nowrap}.gap8{gap:8px}.gap10{gap:10px}.gap12{gap:12px}.gap16{gap:16px}.gap24{gap:24px}.m0{margin:0}.mt8{margin-top:8px}.mt12{margin-top:12px}.mt16{margin-top:16px}.mb8{margin-bottom:8px}.mb12{margin-bottom:12px}.mb16{margin-bottom:16px}.posrel{position:relative}.posabs{position:absolute}.round{border-radius:50%}.ohide{overflow:hidden}.bbox{box-sizing:border-box}.pointer{cursor:pointer}.w100{width:100%}.b0{border:none}" - ); - - // src/helmet.ts - var DESIGN_DOC_MODE_RE = /]*\bname\s*=\s*["']design_doc_mode["'][^>]*\b(?:content|value)\s*=\s*["'](\w+)["']/i; - var CANVAS_BG_LIGHT = "#f0eee6"; - var CANVAS_BG_DARK = "#2e2c26"; - function createHelmetManager(doc, isStreaming) { - const mounted = /* @__PURE__ */ new Set(); - const live = /* @__PURE__ */ new Map(); - let designDocMode = null; - let canvasStyleEl = null; - let appTheme = "light"; - try { - const ds = doc.documentElement.dataset.theme; - appTheme = ds === "dark" || ds === "light" ? ds : new URLSearchParams(doc.defaultView?.location.search ?? "").get( - "theme" - ) === "dark" ? "dark" : "light"; - } catch { - } - function applyCanvasBg() { - if (!canvasStyleEl) return; - const bg = appTheme === "dark" ? CANVAS_BG_DARK : CANVAS_BG_LIGHT; - canvasStyleEl.textContent = `html,body{background:${bg}}#dc-root>.sc-host{position:relative}`; - } - function postDesignMode(mode) { - if (window.parent === window) return; - try { - window.parent.postMessage({ type: "__dc_design_mode", mode }, "*"); - } catch { - } - } - function setDesignDocMode(mode) { - if (mode === designDocMode) return; - designDocMode = mode; - postDesignMode(mode); - if (mode === "canvas") { - doc.documentElement.setAttribute("data-dc-canvas", ""); - canvasStyleEl = doc.createElement("style"); - canvasStyleEl.setAttribute("data-dc-canvas", ""); - applyCanvasBg(); - doc.head.appendChild(canvasStyleEl); - } else { - doc.documentElement.removeAttribute("data-dc-canvas"); - canvasStyleEl?.remove(); - canvasStyleEl = null; - } - } - window.addEventListener("message", (e) => { - const type = e.data && e.data.type; - if (type === "__dc_theme") { - const t = e.data.theme; - if (t === "light" || t === "dark") { - appTheme = t; - applyCanvasBg(); - } - return; - } - if (!designDocMode || type !== "__dc_probe") return; - postDesignMode(designDocMode); - }); - function compile(node) { - const raw = [...node.children]; - const helmetClosed = node.nextSibling != null || node.parentNode?.nextSibling != null; - if (node.hasAttribute("data-dc-atomics") && !mounted.has("__dc-atomics")) { - mounted.add("__dc-atomics"); - const el = doc.createElement("style"); - el.id = "__dc-atomics"; - el.textContent = ATOMIC_CSS; - doc.head.appendChild(el); - } - return (_vals, ctx) => { - const name = ctx && ctx.__name || ""; - const streaming = !!(name && isStreaming(name)); - for (let i = 0; i < raw.length; i++) { - const child = raw[i]; - const tag = child.tagName; - const mayBePartial = streaming && !helmetClosed && i === raw.length - 1; - if (tag === "SCRIPT") { - if (mayBePartial) continue; - const key = "SCRIPT|" + (child.getAttribute("src") || child.textContent || ""); - if (mounted.has(key)) continue; - mounted.add(key); - const el = doc.createElement("script"); - for (const { name: an, value } of [...child.attributes]) - el.setAttribute(an, value); - if (child.textContent) el.textContent = child.textContent; - doc.head.appendChild(el); - } else if (tag === "LINK" || tag === "META") { - if (mayBePartial) continue; - const key = tag + "|" + (child.getAttribute("href") || child.getAttribute("src") || child.outerHTML); - if (mounted.has(key)) continue; - mounted.add(key); - if (tag === "LINK") { - const rel = (child.getAttribute("rel") || "").toLowerCase().split(/\s+/); - const href = (child.getAttribute("href") || "").trim(); - const res = window.__resources; - const pre = res && rel.includes("stylesheet") && !rel.includes("alternate") ? res[href] : void 0; - const blob = typeof pre === "string" && pre ? bundledBlob(pre) : null; - if (blob) { - const el = doc.createElement("style"); - if (child.hasAttribute("disabled")) { - el.setAttribute("media", "not all"); - } else if (child.getAttribute("media")) { - el.setAttribute("media", child.getAttribute("media")); - } - if (child.getAttribute("title")) - el.setAttribute("title", child.getAttribute("title")); - void blob.text().then((css) => { - el.textContent = css; - }); - doc.head.appendChild(el); - continue; - } - } - doc.head.appendChild(child.cloneNode(true)); - } else { - const key = name + "|" + i; - let el = live.get(key); - if (!el || el.tagName !== tag) { - if (el) el.remove(); - el = doc.createElement(tag.toLowerCase()); - live.set(key, el); - doc.head.appendChild(el); - } - for (const { name: an, value } of [...child.attributes]) { - if (el.getAttribute(an) !== value) el.setAttribute(an, value); - } - if (el.textContent !== child.textContent) - el.textContent = child.textContent; - } - } - return null; - }; - } - return { compile, setDesignDocMode }; - } - - // src/pseudo.ts - function createPseudoSheet(doc) { - let el = null; - const cache = /* @__PURE__ */ new Map(); - let n = 0; - return (pseudo, css) => { - const k = pseudo + "|" + css; - const hit = cache.get(k); - if (hit) return hit; - if (!el) { - el = doc.createElement("style"); - doc.head.appendChild(el); - } - const cls = "scp" + (n++).toString(36); - const sel = pseudo === "before" || pseudo === "after" ? "." + cls + "::" + pseudo : "." + cls + ":" + pseudo; - el.sheet.insertRule(sel + "{" + css + "}", el.sheet.cssRules.length); - cache.set(k, cls); - return cls; - }; - } - - // src/registry.ts - function createRegistry() { - const entries = /* @__PURE__ */ Object.create(null); - function get(name) { - return entries[name] || (entries[name] = { - html: "", - tpl: null, - Logic: null, - jsStreaming: false, - htmlStreaming: false, - ver: 0, - subs: /* @__PURE__ */ new Set(), - fetched: false - }); - } - function bump(name) { - const r = get(name); - r.ver++; - for (const fn of r.subs) fn(); - } - return { - entries, - get, - bump, - bumpAll() { - for (const n in entries) bump(n); - } - }; - } - - // src/runtime.ts - var COMPONENT_DIR = "."; - function createRuntime(doc = document) { - const registry = createRegistry(); - const pseudoClass = createPseudoSheet(doc); - const helmet = createHelmetManager( - doc, - (name) => registry.get(name).htmlStreaming - ); - const external = createExternalModules(() => registry.bumpAll()); - const factory = createComponentFactory(registry, ensureFetched); - const host = { - component: (name) => factory.getDC(name), - placeholder: (props) => h(Placeholder, props), - helmet: (node) => helmet.compile(node), - loadExternal: (kind, url, after) => external.load(kind, url, after), - resolveExternal: (url, name) => external.resolve(url, name), - resolveExternalGlobal: (url, name) => external.resolveGlobal(url, name), - resolveExternalError: (url, name) => external.getError(url, name), - pseudoClass - }; - function ensureFetched(name) { - const r = registry.get(name); - if (r.fetched) return; - r.fetched = true; - const url = COMPONENT_DIR + "/" + encodeURIComponent(name) + ".dc.html"; - const res = window.__resources; - const pre = res ? res[url] : void 0; - const target = typeof pre === "string" && pre ? pre : url; - const blob = bundledBlob(target); - (blob ? blob.text() : fetch(target).then((res2) => { - if (!res2.ok) { - console.error( - '[dc-runtime] sibling fetch for "' + name + '" failed:', - url, - "returned", - res2.status, - "\u2014 the reference renders as an empty placeholder." - ); - return ""; - } - return res2.text(); - })).then((t) => { - if (!t) return; - const parsed = parseDcText(t); - if (!parsed) { - console.error( - '[dc-runtime] sibling fetch for "' + name + '":', - url, - "has no block \u2014 not a Design Component." - ); - return; - } - if (parsed.props) r.propsMeta = parsed.props; - if (parsed.preview) r.preview = parsed.preview; - if (parsed.template && !r.html) updateHtml(name, parsed.template); - if (parsed.js && !r.Logic) updateJs(name, parsed.js); - }).catch( - (e) => console.error( - '[dc-runtime] sibling fetch for "' + name + '" threw:', - url, - e - ) - ); - } - let rootName = null; - function updateHtml(name, html) { - const r = registry.get(name); - r.html = html; - if (name === rootName) { - const mode = DESIGN_DOC_MODE_RE.exec(html)?.[1] ?? null; - if (mode || !r.htmlStreaming) helmet.setDesignDocMode(mode); - } - try { - r.tpl = compileTemplate(html, host); - } catch (e) { - console.error("[dc-runtime] template compile FAILED for", name, e); - } - registry.bump(name); - } - function updateJs(name, src) { - const r = registry.get(name); - const seq = r.jsSeq = (r.jsSeq || 0) + 1; - try { - const Cls = evalDcLogic(src); - if (r.jsSeq !== seq) return; - if (typeof Cls !== "function") { - r.logicError = name + ".dc.html: - - - - - - - - - - - - - -
- -
- - - -
- -
- -
-
-
-
-
-
- -
- -
- {{ statusLabel }} - - {{ channelLabel }} - -
- - {{ timer }} - -
- -
- -
- - - - -
{{ initials }}
-
-
-
{{ peerName }}
-
{{ subCaption }}
-
-
- - - - -
- - Длительность {{ finalDuration }} -
-
-
-
- - -
-
{{ statusIcon }}
-
-
{{ statusTitle }}
-
{{ statusCaption }}
-
-
-
-
- - -
-
- - Отклонить -
-
- - Принять -
-
-
- - -
-
-
- - Микрофон -
-
- - Динамик -
-
-
- -
-
-
- - -
- - Отменить -
-
- - -
- - -
-
- - -
- -
-
- - -
- - -
-
- - -
-
- - Восстанавливаем связь… -
-
- -
-
-
- - -
- - -
-
- - -
- - Отменить -
-
- -
-
- -
Полноэкранный аудиозвонок · тёмная тема, живая осциллограмма собеседника. Переключайте состояния выше.
- -
-
- - - diff --git a/design/baseline/# UI для онлайн звонка/Публичная страница звонка.dc.html b/design/baseline/# UI для онлайн звонка/Публичная страница звонка.dc.html deleted file mode 100644 index 2db593b..0000000 --- a/design/baseline/# UI для онлайн звонка/Публичная страница звонка.dc.html +++ /dev/null @@ -1,135 +0,0 @@ - - - - - - - - - - - - - - - - - - - -
- -
-
E
- Chatballs -
- Защищённое соединение -
- -
-
- -
-
-
- - -
{{ initials }}
-
-
{{ orgName }}
-
приглашает вас на видеозвонок
-
- -
-
-
{{ agentInitials }}
-
-
{{ agentName }}
-
{{ agentRole }} · {{ orgName }}
-
-
- Онлайн -
- -
-
Готовы присоединиться?
-
Проверьте камеру и микрофон перед входом. Мы не записываем видео без предупреждения.
-
- -
-
- - Микрофон -
-
- - Камера -
-
- -
- - -
- -
- - Звонок защищён сквозным шифрованием -
-
-
-
- -
Работает на платформе Chatballs
- -
-
- - - diff --git a/design/baseline/# UI для онлайн звонка/Экран звонка.dc.html b/design/baseline/# UI для онлайн звонка/Экран звонка.dc.html deleted file mode 100644 index 55b9635..0000000 --- a/design/baseline/# UI для онлайн звонка/Экран звонка.dc.html +++ /dev/null @@ -1,410 +0,0 @@ - - - - - - - - - - - - - - - - - - - -
- -
-
Экран звонка
-
Встраиваемый компонент видеозвонка оператора и клиента — для модалки или страницы Hub. Полный жизненный цикл вызова.
-
- -
-
-
- Сторона -
- - -
-
-
- Основные состояния -
- - - -
-
-
-
- Ошибки и завершение -
- - - -
-
- -
- Собеседник - - -
-
-
- -
- -
-
{{ peerInitials }}
-
-
- {{ peerName }} - - - -
- {{ subtitle }} -
-
- -
- - {{ timer }} -
-
- -
- -
- - - - - - -
-
- - - - -
{{ mainInitials }}
-
-
-
{{ mainName }}
-
{{ mainCaption }}
-
-
-
- - -
- - Связь прервана · восстанавливаем соединение… -
-
- - -
-
{{ statusIcon }}
-
-
{{ statusTitle }}
-
{{ statusCaption }}
-
- -
- - - -
-
-
-
- - -
- - - - {{ peerName }} -
-
- - -
- - - - -
-
ВЫ
-
-
- Вы -
-
- -
- - -
-
- - Отклонить -
-
- - Принять -
-
-
- - -
- - Отменить -
-
- - -
-
-
- - Микрофон -
-
- - Камера -
-
-
- - -
-
-
- - -
- - - - -
-
- -
- -
Переключайте сторону и состояние выше. В активном звонке доступны тумблеры «Собеседник», чтобы проверить индикаторы удалённого микрофона и камеры.
- -
-
- - - diff --git a/design/baseline/Chatballs Baseline v2/README.md b/design/baseline/Chatballs Baseline v2/README.md deleted file mode 100644 index 0d5b13a..0000000 --- a/design/baseline/Chatballs Baseline v2/README.md +++ /dev/null @@ -1,195 +0,0 @@ -# Chatbolls — design baseline (SPEC-HUB-0031) - -Два макета: чат сотрудника (роль EMPLOYEE) и оболочка владельца/админа (OWNER = ADMIN). Основания: SPEC-HUB-0031 (§3–6, §8.1, §9), ADR-HUB-0002/0003 (режимы диалога, перехват), ADR-HUB-0041/0043 (контакт-центр, группы, ответственный, видимость). - -## Как открыть - -Любой `*.dc.html` открывается в браузере напрямую (нужен `support.js` рядом). Тема и акцент задаются через Tweaks (`theme`, `accent`) или правкой `default` в `data-props`. - -## Состав - -``` -Чат сотрудника Baseline.dc.html — EMPLOYEE: кадры H, A–G, S1–S2, M1–M2 -Админ Baseline.dc.html — OWNER/ADMIN: кадры A1–A2 -Настройки Baseline.dc.html — OWNER/ADMIN: кадры N1–N7, M -Профиль Baseline.dc.html — все роли: кадры P1–P2, M -Контакты Baseline.dc.html — OWNER/ADMIN: кадры K1–K5, S1 -Агенты Baseline.dc.html — OWNER/ADMIN: кадры G1–G5, S1 -Сотрудники Baseline.dc.html — OWNER/ADMIN: кадры E1–E10 -Порталы Baseline.dc.html — OWNER/ADMIN: кадры PT1–PT8 -_ds/ — токены дизайн-системы -assets/logo-mark.svg — знак -assets/av-1..4.png — аватары (Дмитрий Орлов · Анна Ким · Сергей Крылов · Елена Кузнецова) -support.js — рантайм макетов -``` - -## Админ Baseline — карта кадров - -- **A1 Владелец · Чат** — стартовый экран. Сайдбар: логотип + переключатель организации; навигация ровно 6 пунктов плоским списком (Чат с бейджем очереди · Контакты · Агенты · Сотрудники · Доска · Настройки); блок «Запуск» (3 шага, автоотметка, прогресс, primary на текущий шаг, «Скрыть»); профиль с ролью внизу. В списке диалогов заголовок — **переключатель охвата** (поповер: Все диалоги · Группы · Агенты · Ответственный) — дерево из сайдбара сотрудника у админа переезжает сюда, чтобы навигация осталась плоской (§4.1). Inline-`Hint` первого визита под вкладками. -- **A2 Админ · рабочий режим** — «Запуск» завершён и скрыт, подсказка закрыта, охват «Операторы». От владельца отличается только подписью роли (§3). - -## Настройки Baseline — карта кадров - -Оболочка та же (сайдбар 220, «Настройки» активен). Вместо сплошной ленты из девяти секций — **субменю разделов 240px** и один раздел на экране; ниже субменю — переходы на отдельные экраны «База знаний» и «Аудит действий». Контент 820px, табличные разделы — 1000px. - -- **N1 Организация** — логотип (загрузить/убрать), название, часовой пояс, валюта; «Сохранить» с датой последнего сохранения. -- **N2 Группы** — строки: цветная точка · имя · состав · диалоги · изменить/удалить; создание — одно поле внизу списка. Состав группы — на карточке сотрудника. -- **N3 AI-провайдер** — таблица: название · секрет (маска) · конфиг (endpoint + модель) · статус · «Проверить» · ⋯. Primary «Добавить провайдера» в шапке раздела. -- **N4 Интеграции** — таблица: название (фирменная плитка) · агент · статус · действия. Ошибка почты — красный бейдж и точка-маркер у пункта субменю. -- **N5 Голосовые и звонки** — матрица точка входа × (Голосовые · Аудиозвонки · Видеозвонки), переключатели сохраняются сразу; почта — «недоступно», выключенная точка входа приглушена. -- **N6 Хранилище файлов** — сегмент «На диске установки / Внешнее S3», поля S3 (секреты маской), статус проверки/переноса, «Перенести · Проверить доступ · Сохранить». -- **N7 Демо-данные** — статус, счётчик записей, учётки демо-сотрудников (email/пароль моноширинно), «Удалить демо-данные» danger-outline. -- **M** — мобильный: список разделов ↔ раздел; поля 44px, primary в липком футере. - -### Решения по настройкам - -1. **Профиль — не настройки.** Тема, акцент, аватар, пароль, 2FA, сессии — отдельная страница «Профиль» из меню пользователя (`SidebarUserMenu`). Из «Настроек» убран; SPEC-HUB-0031 §8.6 в части «Профиль» требует уточнения. -2. **Субменю вместо ленты** — отклонение от §8.6 («один экран с вертикальными секциями») по решению владельца: девять секций не помещаются на экран. -3. **OWNER == ADMIN** (§3): макет один для обеих ролей, никаких `canManage`-разветвлений в UI. -4. Порядок разделов и их состав — как в `features/settings/SettingsPage.tsx`; таблицы — `table-card`/`baseline-table`, матрица — `communication-matrix`. - -## Профиль Baseline — карта кадров - -Отдельная страница из меню пользователя (`SidebarUserMenu` → «Профиль»). Шапка: аватар 72 · имя + бейдж роли · email, дата вступления, группы · «Выйти». Две колонки: слева «кто я и как выглядит интерфейс» (Личные данные + Фото · Внешний вид · Уведомления в мессенджер), справа «безопасность» (Смена пароля · 2FA · Активные сессии). - -- **P1 Владелец** — меню пользователя открыто (Уведомления · Профиль · Выйти), навигация видна; Telegram привязан с типами событий (ADR-0015), MAX — «Привязать бота»; TOTP включена; три сессии, текущая отмечена. -- **P2 Сотрудник** — сайдбар без навигации (§3): логотип, организация, профиль. Та же страница; TOTP выключена — жёлтое предупреждение. -- **M** — мобильный: личные данные и внешний вид сразу; уведомления, пароль, 2FA, сессии — список подэкранов. - -Решения: тема — сегмент Светлая / Тёмная / Как в системе (§7.2); акцент — 4 пресета из `shared/appearance.ts` + HEX (§7.3); всё на странице принадлежит учётной записи, а не организации (ADR-0029) — владелец не меняет это у других. Поле «Роль» из формы убрано — роль показана бейджем в шапке. - -## Контакты Baseline — карта кадров - -Источник — `features/sales/clients` и `client-detail` (раздел в коде всё ещё называется `salesClients`). Из текущей таблицы убраны колонка «Продукты» и фильтр по продуктам (ADR-0041, §8.2 «без коммерции»), добавлен фильтр по агенту. Из карточки убраны вкладка «Согласия» (заглушка в коде) и блок «Связанные продукты»; добавлены «Ответственный» и «Заметка» — те же поля, что в контекст-панели чата. - -- **K1 Список** — колонки: Контакт (аватар · имя · CID) · Как связаться (телефон/email + логин) · Каналы (плитки-глифы) · Последний диалог (точка режима · кто ведёт · когда, сортируемая) · Открытые · ⋯. Фильтры: поиск, Каналы, Агенты, чип «С открытым диалогом»; «Экспорт CSV»; пагинация в футере карточки. -- **K2 Список · фильтр** — чип активен, счётчик «6 из 128», «Сбросить». -- **K3 Карточка · Обзор** — шапка (аватар 64 · имя · CID · карандаш · описание · телефон/email/компания/город с «копировать» · бейджи каналов · primary «Открыть диалог»); `UnderlineTabs` Обзор · Диалоги · Идентификаторы · Аудит; сводка 4 числа · лента активности; справа — текущий диалог, ответственный, заметка. -- **K4 Карточка · Диалоги** — тема + превью · агент · канал · группа · статус · дата; клик — в чат. -- **K5 Карточка · Идентификаторы** — идентичности по подключениям со статусом подтверждения; справа — «Возможный дубликат» с «Сравнить и объединить» (ручное объединение с причиной и аудитом, ADR-0006). -- **S1 Пустой список** — «Контакты появятся, когда клиенты напишут вашему агенту» + «Настроить подключения». - -Мобильной версии нет — §9: админ-разделы с телефона в первой итерации не адаптируются. Вкладка «Аудит» — таблица Время · Действие · Объект · Актор · Результат как в коде, отдельно не перерисована. - -## Агенты Baseline — карта кадров - -Источник — `features/agents` (AgentsPage, AgentDetailPage). Агент = канал + AI-конфигурация (ADR-0041 §4); изменения применяются сразу, без релизов (ADR-0023). Вертикальная лента секций из кода пересобрана в две колонки: слева «что он знает и как говорит» (Инструкции · Знания), справа «как работает» (Назначение · Модель · Подключения). - -- **G1 Список** — агент (плитка-бот в цвете агента · имя · код) · группа · статус (AI отвечает / Без AI / Выключен) · модель · подключения (плитки с красным маркером ошибки) · открытые · ⋯. Колонка «Модель» добавлена к §8.3. -- **G2 Создание** — модалка одного шага: название + группа (§4.3). -- **G3 Карточка · Консультант** — рабочий агент. Шапка: плитка · имя · статус · группа · код · открытые · «Остановить AI». Инструкции — три поля (кто он · как говорит · правила). Знания — список с чипами «Выключено» / «Портал» и кнопкой «Выбрать». Подключения: привязанные со статусом и «Отвязать», свободные с «Привязать». -- **G4 Карточка · Поддержка сайта** — виджет привязан: блок «Код вставки виджета» с «Копировать» и подсказкой «вставьте перед » (§8.3); почта в ошибке. -- **G5 Карточка · новый агент** — blocker «провайдер не выбран» с переходом в настройки (ADR-0017), плейсхолдеры инструкций, строка «несохранённые изменения — применятся сразу», «Запустить AI». -- **S1 Пустой список** — «Агент отвечает клиентам первым. Создайте первого» + «Создать агента» (§5.2). - -Решения: лимит дневной стоимости (`dailyCostUsd`, ADR-0023) вынесен в блок «Модель» — в коде поля нет, требует реализации. «Выключить агента» / «Удалить» — вторичные кнопки внизу карточки, как в коде. - -## Порталы Baseline — карта кадров - -Источник — `features/support-portals` + SPEC-HUB-0028 (§1.1–1.3, §3, §6). Переделаны три больные точки: модалка настроек, публичная ссылка и редактор статьи с файлами. - -- **PT1 Список** — таблица: портал (плитка-глобус · имя · язык/дата) · **публичный адрес** (моношрифт + «копировать» + бейдж «свой домен») · статус · материалы (разделы · статьи) · обновлён · ⋯. Меню строки открыто: Открыть материалы · Настройки портала · Копировать ссылку · Открыть портал · Перенести в архив. Архивный портал приглушён и уходит в конец. Поиск и фильтр статуса в шапке карточки, счётчик и пагинация в футере. -- **PT2 Пусто** — «Создайте первый портал поддержки» + «Создать портал» и «Как это выглядит у клиента»; создание доступно всегда (§1.1). -- **PT3 Карточка · материалы** — шапка: хлебные крошки, имя, статус, «6 разделов · 48 статей»; **блок публичного адреса** — глобус в цвете статуса, домен, бейдж «свой домен · подтверждён», кнопки «копировать» и «открыть», рядом резервный hosted-адрес. Действия: «Настройки» (переход на PT4, не модалка), primary «Новая статья», ⋯ (публикация/архив). Ниже — библиотека без вкладок: дерево разделов 260px с вложенностью и счётчиками + тулбар (поиск, язык, статус, «Импорт статей») + таблица Статья · Раздел · Язык · Редакция · Статус · Обновлено · ⋯ (скрепка = файлы в статье). -- **PT4 Настройки · Основные** — субменю 250px (Основные · Адрес и домен · Оформление · Веб-виджет · Публикация и архив), контент 820px: название, **адрес на этой установке** (поле `{portalKey}` + суффикс базового домена инсталляции), основной язык, карточка «Итоговая ссылка» с «Копировать/Открыть». -- **PT5 Настройки · Адрес и домен** — поле домена + статус «Домен ведёт сюда · проверено», одна A-запись на IP установки с копированием, «Сохранить домен · Проверить DNS», «Отключить домен» danger-outline. -- **PT6 Настройки · Оформление** — две темы из каталога установки (`features/help-center/themes/`: `classic` «Классическая», `ocean` «Ocean») карточками с мини-превью на цветах их `manifest.preview`; сегмент схемы Светлая / Тёмная / Как в системе (`manifest.schemes`), «Предпросмотр портала», дата сохранения. Названия и описания тем в UI не придумываются — только из манифеста; отсутствующая в сборке тема показывается как недоступная. -- **PT7 Редактор статьи** — полноэкранный, без модалок. Шапка: «Материалы» назад, крошки, бейдж «черновая редакция 8 · не опубликована», сегмент Текст / Вместе / Просмотр, «Сохранить черновик», primary «Опубликовать». Тело: слева заголовок-инпут + адрес + панель Markdown (H1/H2 · B/I/ссылка/код · списки/цитата/таблица · изображение/файл) + монополе текста + статусная строка (знаки, позиция курсора, автосохранение); в центре живой предпросмотр в теме портала («Как у посетителя»); справа рейка 300px: раздел, язык, краткое описание, **файлы статьи** (drop-зона + список с «вставить в текст» и «копировать Markdown»), **версии** (неизменяемые, опубликованная отмечена). -- **PT8 Редактор · перетаскивание файла** — drop-оверлей поверх текста: «Отпустите, чтобы вставить в текст», подпись про типы и лимит 25 МБ; в рейке новый файл с прогрессом загрузки. - -### Решения по порталам - -1. **Настройки — страница, не модалка.** `Modal width={960}` с пятью секциями подряд заменён маршрутом `/portals/{id}/settings` с субменю разделов; §1.2 в части «modal surface» требует уточнения. -2. **Публичная ссылка — постоянный элемент шапки**, а не текст `` в подзаголовке: копирование, открытие, статус домена и резервный hosted-адрес видны всегда, в том числе у черновика. -3. **Редактор статьи** — split-режим с предпросмотром GFM и панелью Markdown вместо `textarea rows={10}` в карточке. Версии и файлы — в рейке, публикация редакции — из того же экрана. -4. **Файлы статьи требуют API.** В коде вложений у статей портала нет (есть только у `ai/knowledge`): нужны `portals/{id}/articles/{articleId}/attachments/` с возвратом публичного URL для подстановки в Markdown. Импорт YAML остаётся отдельным действием «Импорт статей» в тулбаре. -5. **Hosted-адрес — домен установки, не домен продукта.** Продукт ставится на любой инфраструктуре, базовый домен берётся из `CHATBALLS_HELP_BASE_DOMAIN` (`support_portals/addressing.py`, `portal_views.py` → `address.baseDomain`). В макете он показан как «адрес на этой установке» с примером `help.nord-crm.ru` — в UI нельзя зашивать ни `help.example.com`, ни домен вендора; суффикс и итоговый URL всегда из ответа API. -6. **Никакого подтверждения владения доменом.** Продукт self-hosted: домен и установка у одного владельца — доказывать права самому себе нечего. Остаётся одна A-запись на IP установки и техническая проверка «ведёт ли домен сюда» (нужна для выписки TLS). TXT-запись `_chatballs.…`, `customDomainVerification` и `customDomainVerifiedAt` из UI убраны — их стоит выключить и в коде либо оставить только для мультитенантного SaaS-режима с общим ingress. -7. **Мобильной версии нет** — §9: админ-разделы с телефона в первой итерации не адаптируются. - -## База знаний Baseline — карта кадров - -Источник — `features/ai/knowledge` + SPEC-HUB-0026 §9 (иерархия категорий) и SPEC-HUB-0029 §7 (прикрепление к агенту). Раздел сверстан как карточка портала: та же шапка с состоянием, то же дерево слева, тот же split-редактор. Отделы упразднены ADR-HUB-0041, поэтому вместо `visibility` (`ORGANIZATION`/`DEPARTMENTS`) — состояние «участвует в ответах» и явный список агентов. - -- **KB1 Библиотека** — шапка: «База знаний», бейдж «Индекс актуален», «9 категорий · 84 знания · 1 240 фрагментов», действия «Импорт YAML» и primary «Добавить знание». Ниже — дерево категорий 260px со счётчиками и вложенностью + тулбар (поиск, «В ответах», «Агент») + таблица: чекбокс · знание (иконка типа · заголовок · описание · скрепка вложений) · категория (с родителем) · фрагменты · агенты · в ответах · обновлено (кто) · ⋯. Меню строки: Открыть · Редактировать · Прикрепить к агенту · Переместить · Выключить в ответах · Переиндексировать · Удалить. -- **KB2 Массовый выбор** — тёмная панель операций по центру снизу: «Выбрано 3» + Прикрепить к агенту (primary) · Переместить · Включить · Выключить · Удалить · снять выделение. Выбранные строки подсвечены акцентом. -- **KB3 Прикрепить к агенту** — диалог 560px: сегмент Прикрепить / Открепить, выбор **одного** агента (радио, у агента без AI — статус «без AI»), предупреждение «2 из 3 будут пропущены» с причиной по каждому, примечание про атомарность и `ai.manage`, primary «Прикрепить 1». -- **KB4 Карточка знания** — шапка как у портала: крошки «База знаний / Регламенты / Пошив», заголовок, бейдж «В ответах агента», чипы «12 фрагментов · переиндексировано» и «3 агента», автор правки; действия «Прикрепить к агенту», primary «Редактировать», ⋯. Тело: рендер содержимого (≤720px) + рейка 320px: категория, тумблер «Участвует в ответах», вложения с копированием публичной ссылки, агенты со статусами, блок «Индексация» с «Переиндексировать». -- **KB5 Редактор знания** — тот же полноэкранный split, что PT7: слева заголовок + панель Markdown + монополе + статусная строка (знаки, ≈фрагменты, автосохранение); в центре предпросмотр «внутренний материал» и «Как ответит агент»; справа рейка 300px: категория, краткое описание, тумблер ответов, вложения (drop-зона + список), агенты (только чтение). Primary — «Сохранить и переиндексировать». -- **KB6 Редактор · перетаскивание файла** — drop-оверлей «Отпустите, чтобы приложить к знанию» (файл = вложение + ссылка в тексте), в рейке загрузка с прогрессом, в шапке бейдж «Есть несохранённые правки». -- **KB7 Категории** — дерево строками с ручкой перетаскивания, счётчиками, инлайн-переименованием, «+ вложенная»; удаление недоступно у системной и непустой категории (тайтл объясняет причину), справа карточка правил и предупреждение о непустой категории. -- **KB8 Импорт YAML** — разбор до применения: файл, сводка «создать 5 / обновить 2», таблица документ · `categoryPath` · действие · замечание; недостающие уровни категорий создаются автоматически с пояснением в замечании; primary «Импортировать 7 документов». -- **KB9 Пустая категория** — «В категории «Обучение» пока нет знаний» + «Добавить знание» / «Импорт YAML»; счётчики не подменяются нулями из другой категории. - -### Решения по базе знаний - -1. **Раздел вынесен в сайдбар** — пункт «База знаний» сразу после «Порталов»: внутренние материалы и публичные порталы — соседние сущности, а не вложенность в «Агентов». -2. **`visibility` заменена состоянием «участвует в ответах»** — после ADR-0041 отделов нет, скрывать знание от части организации нечем. Доступность = прикреплённые агенты + флаг участия; в UI это одна колонка и один тумблер вместо матрицы отделов. -3. **Фрагменты показываются как цифра, а не абстракция** — в строке и карточке видно число фрагментов и состояние пересборки: это единственный способ объяснить, почему агент отвечает старым текстом. -4. **Прикрепление — только массовым действием** (SPEC-0029 §6: операция атомарна и журналируется). В карточке и редакторе список агентов — только чтение, изменение уходит в диалог KB3, который честно показывает пропуски. -5. **Импорт разбирается до применения.** Спецификация запрещает неявное создание категорий, поэтому ошибка пути должна быть видна до записи — таблица предпросмотра с построчным действием. -6. **Вложения знания уже есть в API** (`ai/knowledge` attachments), в отличие от статей портала — поэтому drop-зона в редакторе знания реализуема без новых эндпоинтов. - -## Сайдбар — актуально по коду (`layout/Sidebar.tsx`) - -Во всех макетах обновлено 2026-09-06: - -- **Знак Chatballs** — инлайн-SVG из `shared/icons.tsx` (`LogoIcon`): два круга и перемычка с вырезами сверху и снизу, 26px, `currentColor` по `--c-n1`, без подложки. Файл `assets/logo-mark.svg` больше не используется. -- **Пункты** — Чат (badge очереди) · Контакты · Агенты · Сотрудники · **Порталы** · **База знаний** · Настройки. Пункт «Доска» из ранних макетов — ошибка: в коде это `supportPortals` с иконкой `globe`. -- **Иконки** — ровно из `Icon`: `message` · `user` · `robot` · `team` · `globe` · `settings`, stroke-width 1.8. -- **Ширина тянется** — `pane-resizer` на правом краю: невидимая полоса 6px, подсветка акцентом 2px при наведении и перетаскивании; 180–320px, по умолчанию 220, запоминается в браузере; двойной клик — сброс. На ≤1024px ручка скрыта. -- **Шапка** — 56px: знак + переключатель организации (32px, имя + шеврон). -- **Рейка 60px** — только ≤1024px: знак · чат · «развернуть» · аватар; полный сайдбар выезжает поверх контента (кадр E7). -- **Блок «Запуск»** — три шага с автоотметкой, прогресс, primary текущего шага, «×» — скрыть (localStorage). -- **Меню пользователя** — аватар · имя + email · точка непрочитанных + ⋯ (Уведомления · Профиль · Выйти). -- У сотрудника вместо навигации — дерево «Диалоги» (Все · Группы · Агенты); на странице «Профиль» сайдбар без навигации вообще (кадр P2). - -## Сотрудники Baseline — карта кадров - -Источник — `features/employees`. Роли: OWNER = ADMIN по правам, EMPLOYEE — только чат; группа задаёт только видимость диалогов (ADR-0043). - -- **E1 Список** — таблица сотрудник · роль · должность · группы · доступ · статус · последний вход; фильтры поиск/роль/группа, «Добавить сотрудника»; в футере счётчик и напоминание про группы. Здесь же показан сайдбар целиком: блок «Запуск» и подсвеченная ручка ширины. -- **E2 Список · меню строки** — фильтр «Администратор»; действия: карточка · завершить сессии · сбросить пароль · заблокировать. Передачи владения в меню нет. -- **E3 Карточка сотрудника** — Основные данные · Должность и группы · Системная роль (три варианта, владелец замкнут) · Статус и безопасность · Опасная зона (блокировка); справа Учётная запись и Аудит. -- **E4 Карточка владельца** — «Только просмотр», баннер про правило роли, все роли замкнуты; единственное опасное действие — **Передача владения** в опасной зоне. -- **E5 Новый сотрудник** — выезжающая панель 520px, четыре шага: учётные данные · роль (Сотрудник / Администратор) · должность и группы · **пароль первичного доступа**: отправить на рабочую почту (по умолчанию) либо сгенерировать и показать. -- **E6 Новый сотрудник · пароль показать** — выбран второй вариант (на случай проблем с почтой); подпись футера меняется на «Пароль покажем сразу после создания». -- **E7 Пароль показан** — диалог после создания: пароль моноширинно, «Копировать», маскирование глазом, «Отправить на почту вместо этого», предупреждение о единственном показе, «Скопировал, закрыть». Действие в аудите. -- **E8 Сброс пароля** — тот же диалог для сброса владельцем или админом (из карточки сотрудника или меню строки): сессии завершены, при первом входе сотрудник задаёт свой пароль. -- **E9 Передача владения** — открывается только из опасной зоны карточки владельца. Кандидаты — только активные администраторы (чтобы передать сотруднику, его сначала делают админом): выбор, стрелка перехода, новая роль прежнего владельца, предупреждение, чекбокс подтверждения. -- **E10 Сайдбар · рейка 1024** — состояние ≤1024px. - -Решение по паролю: сервер отдаёт пароль в ответе один раз и в открытом виде не хранит; UI показывает его в диалоге с копированием. Требует поддержки в API (`employees/{id}/reset-password/` и создание сотрудника должны возвращать сгенерированный пароль при выборе режима «показать»). - -## Чат сотрудника — карта кадров - -- **A** — рабочий экран, диалог ведёт AI. Композер активен; подсказка над полем: отправка перехватит диалог. Primary «Взять диалог» в строке действий. -- **B** — диалог ждёт человека (AI передал). Фильтр «Ждут человека» активен, системное событие в ленте, подсказка над композером. -- **C** — ведёте вы. Строка действий: «Вернуть AI» · ⋯. Меню ⋯ открыто: Вернуть в очередь · Закрыть диалог · Пометить как спам · Удалить диалог. -- **D** — ведёт другой сотрудник. Композер заблокирован (замок, кто ведёт), вторичное действие «Взять диалог». -- **E** — закрытый диалог. Вкладка «Мои», звонки в карточке и ⋯ скрыты, композер заменён сообщением о закрытии. -- **F** — контекст-панель, вкладка «История»: диалоги контакта, текущий выделен. -- **G** — фильтр по группе: в сайдбаре выбрана «Поддержка», список отфильтрован. В контекст-панели открыто меню переноса в группу. -- **H** (первый кадр) — голосовые: в списке превью «Голосовое сообщение · 0:14» с микрофоном; в ленте плеер (play 30 · волна 2px · длительность) и три состояния расшифровки под волной: текст + «Расшифровка AI · Скрыть» · «Расшифровываем…» · кнопка «Расшифровать». Расшифровка доступна и для своих голосовых. Композер в режиме записи: корзина · красный таймер · живая волна · пауза · Отправить (Esc — отмена, Enter — отправить). -- **S1** — пустой список: «Диалоги появятся, когда клиенты напишут вашему агенту», без кнопки (сотрудник). -- **S2** — ширина 1024: сайдбар сворачивается в рейку 60px, контекст-панель — выезжающая, открывается кнопкой «Контакт» в шапке ленты. -- **M1 / M2** — мобильный: список ↔ диалог. Контакт — по кнопке в шапке. - -## Зафиксированные решения - -1. **Сайдбар 220px с деревом «Диалоги».** У сотрудника нет разделов (§3), поэтому сайдбар — это фильтр списка: Все диалоги · Группы (с цветной точкой и счётчиком) · Агенты. Внизу — профиль. Верхней панели нет. На 1024 сворачивается в рейку. -2. **Одно действие взятия.** «Взять диалог» одинаково для очереди и AI — атомарный перехват (ADR-0003). Композер активен всегда: первое отправленное сообщение выполняет тот же перехват; над полем — одна строка-предупреждение. -3. **Фильтры.** Статусы — `UnderlineTabs` над списком (Все · Мои · Ждут оператора; закрытые попадают в Все/Мои); Группа и Агент — пункты дерева в сайдбаре (v1 чипы под поиском убраны). Заголовок списка = выбранный пункт дерева. -4. **Строка диалога** (ориентир — Chatwoot): иконка канала (Telegram/MAX — фирменные глифы из `shared/icons.tsx`, Email/Web — линейные) + имя агента в одну строку серым (`--c-n4`) · индикатор приоритета справа → имя · время → превью (↩ если последнее сообщение наше) · непрочитанные → бейджи: таймер ожидания (иконка + время, без слова «ждёт»), группа, метки. Активная строка — фон `--c-n9`, без акцентной полосы. -4a. **Сообщения**: у клиента (слева) аватара нет — он в шапке; фон входящих `--c-msg-in` (светлее/темнее подложки по теме), без обводки. Исходящие справа: аватар AI/сотрудника (в диалоге по очереди могут отвечать AI и разные люди), фон — акцент 22% на подложке ленты, AI — `--c-ai` 16%. -5. **Карточка контакта**: аватар 64 · канал · имя + карандаш · описание · поля с иконками и «копировать» (мессенджер/почта, телефон, компания, город). Кнопки «Позвонить» · «Видеозвонок» — в карточке контакта под полями (не в шапке ленты). Блок «Диалог»: Ответственный, Группа, Приоритет (Высокий/Средний/Низкий/Не задан, индикатор-столбики) — полноширинные селекты, Агент и Режим — read-only в две колонки, Метки — чипы с «+ Добавить», Начат. Заметка — жёлтая карточка. -6. **Метки и Приоритет диалога** — по образцу конкурентов; в SPEC-HUB-0031 этих полей нет. Требует решения: добавить в модель или убрать из макета. -6a. Агент везде подписан иконкой бота в цвете агента (Консультант — фиолетовый, Поддержка сайта — бирюзовый). Скроллбары скрыты. -7. **Ссылка «Открыть профиль контакта»** сотруднику не показывается — раздела «Контакты» у него нет. -8. Подсказки `Hint` сотруднику не показываются (§6). -9. Композер: поле сверху, строка инструментов снизу (эмодзи · вложение · голосовое · Шаблоны «/») и кнопка отправки с ⏎. - -## Технические примечания - -- Инлайн-стили поверх переменных темы `--c-*`, задаваемых на корне из Tweaks: **Тема** (light/dark) и **Акцент** (синий · индиго · бирюзовый · оранжевый). Полутона (фон акцента, предупреждений, AI) считаются через `color-mix`, поэтому одинаково работают в обеих темах. Данные — нейтральные примеры («Ателье Норд»). -- Статусные цвета режимов: AI `--ai`, ждёт `--warning`, человек `--primary`, закрыт `--n-5` — как в текущем `conversations/data.ts`. diff --git a/design/baseline/Chatballs Baseline v2/_ds/styles.css b/design/baseline/Chatballs Baseline v2/_ds/styles.css deleted file mode 100644 index 0cd17b1..0000000 --- a/design/baseline/Chatballs Baseline v2/_ds/styles.css +++ /dev/null @@ -1,4 +0,0 @@ -@import "./tokens/colors.css"; -@import "./tokens/typography.css"; -@import "./tokens/spacing.css"; -@import "./tokens/effects.css"; diff --git a/design/baseline/Chatballs Baseline v2/_ds/tokens/colors.css b/design/baseline/Chatballs Baseline v2/_ds/tokens/colors.css deleted file mode 100644 index 9044d34..0000000 --- a/design/baseline/Chatballs Baseline v2/_ds/tokens/colors.css +++ /dev/null @@ -1,88 +0,0 @@ -/* Chatballs — color tokens - Source: packages/ui/src/theme.ts (Ant Design ThemeConfig), apps/internal-ui/src/styles/base.css, - and design/baseline/*.dc.html (approved HTML baseline). Ant Design's default light palette, - customized with a single accent (#1677ff) per ADR-HUB-0013. */ - -:root { - /* ---- base neutrals (Ant Design neutral scale as used in the baseline) ---- */ - --n-1: #1f1f1f; /* page/heading titles */ - --n-2: #262626; /* primary body text */ - --n-3: #595959; /* secondary text */ - --n-4: #8c8c8c; /* tertiary text / captions */ - --n-5: #bfbfbf; /* disabled / placeholder / faint */ - --n-6: #d9d9d9; /* strong border, control border */ - --n-7: #e8e8e8; /* input border */ - --n-8: #f0f0f0; /* hairline border */ - --n-9: #f5f5f5; /* hover surface */ - --n-10: #fafafa; /* table header surface */ - --n-11: #f0f2f5; /* app/page background */ - --n-white: #ffffff; - - /* ---- primary (blue-6 / blue-7) ---- */ - --primary: #1677ff; - --primary-hover: #0958d9; - --primary-bg: #e6f4ff; - --primary-border: #91caff; - --primary-shadow: rgba(22, 119, 255, 0.3); - - /* ---- semantic: success / warning / error / info ---- */ - --success: #52c41a; - --success-text: #389e0d; - --success-bg: #f6ffed; - --success-bg-strong: #f1f9ec; - --success-border: #b7eb8f; - - --warning: #faad14; - --warning-text: #d48806; - --warning-bg: #fffbe6; - --warning-bg-strong: #fff7e6; - --warning-border: #ffe58f; - - --error: #ff4d4f; - --error-text: #cf1322; - --error-bg: #fff2f0; - --error-border: #ffccc7; - - --info: #1677ff; - - /* ---- role accents: AI vs human ---- */ - --ai: #722ed1; - --ai-light: #9254de; - --ai-lighter: #b37feb; - --ai-bg: #f9f0ff; - --human: #1677ff; - - /* ---- channel accents (MAX / Telegram / Web Chat) ---- */ - --channel-max: #6b5be0; - --channel-max-bg: #f2f0ff; - --channel-telegram: #2f8fd0; - --channel-telegram-bg: #eaf6fd; - --channel-webchat: #0f9b8e; - --channel-webchat-bg: #e8f7f4; - - /* ---- product accents (as tagged in tables/lists) ---- */ - --product-firepage: #0958d9; - --product-firepage-bg: #e6f4ff; - --product-foxray: #722ed1; - --product-foxray-bg: #f9f0ff; - - /* ---- surfaces ---- */ - --surface-page: var(--n-11); - --surface-card: var(--n-white); - --surface-sunken: #fafafa; - --surface-chip: #f7f8fa; - - /* ---- text aliases ---- */ - --text-heading: var(--n-1); - --text-body: var(--n-2); - --text-secondary: var(--n-3); - --text-tertiary: var(--n-4); - --text-disabled: var(--n-5); - --text-inverse: var(--n-white); - --text-link: var(--primary); - - /* ---- borders ---- */ - --border-default: var(--n-8); - --border-input: var(--n-7); - --border-strong: var(--n-6); -} diff --git a/design/baseline/Chatballs Baseline v2/_ds/tokens/effects.css b/design/baseline/Chatballs Baseline v2/_ds/tokens/effects.css deleted file mode 100644 index cd157ec..0000000 --- a/design/baseline/Chatballs Baseline v2/_ds/tokens/effects.css +++ /dev/null @@ -1,40 +0,0 @@ -/* Chatballs — radii, shadows, motion. "Премиальный минимализм": no gradients, - no glassmorphism/blur, no neon — shadows stay soft and near-black at low opacity. */ - -:root { - --radius-xs: 5px; - --radius-sm: 7px; - --radius-md: 8px; - --radius-lg: 9px; - --radius-xl: 10px; - --radius-2xl: 12px; - --radius-3xl: 13px; - --radius-4xl: 14px; - --radius-pill: 999px; - --radius-circle: 50%; - - --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.03); - --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.04); - --shadow-md: 0 2px 12px rgba(0, 0, 0, 0.05); - --shadow-lg: 0 6px 20px rgba(0, 0, 0, 0.12); - --shadow-sticky: 0 -4px 16px rgba(0, 0, 0, 0.04); - --shadow-primary: 0 1px 2px rgba(22, 119, 255, 0.3); - - --ease-standard: cubic-bezier(0.4, 0, 0.2, 1); /* @kind other */ - --duration-fast: 0.15s; /* @kind other */ - --duration-base: 0.2s; /* @kind other */ -} - -@keyframes hub-typing { - 0%, 80%, 100% { opacity: 0.25; transform: translateY(0); } - 40% { opacity: 1; transform: translateY(-2px); } -} - -@keyframes hub-spin { - to { transform: rotate(360deg); } -} - -@keyframes hub-pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } -} diff --git a/design/baseline/Chatballs Baseline v2/_ds/tokens/spacing.css b/design/baseline/Chatballs Baseline v2/_ds/tokens/spacing.css deleted file mode 100644 index ba5ca9f..0000000 --- a/design/baseline/Chatballs Baseline v2/_ds/tokens/spacing.css +++ /dev/null @@ -1,36 +0,0 @@ -/* Chatballs — spacing tokens. Compact-enterprise density (ADR-HUB-0013): - "большие пустые поля и oversized-карточки запрещены". Values are the exact - paddings/gaps observed in the baseline, not snapped to a rounder grid. */ - -:root { - --space-1: 2px; - --space-2: 4px; - --space-3: 6px; - --space-4: 8px; - --space-5: 9px; - --space-6: 10px; - --space-7: 11px; - --space-8: 12px; - --space-9: 14px; - --space-10: 16px; - --space-11: 18px; - --space-12: 20px; - --space-13: 22px; - --space-14: 24px; - --space-15: 28px; - --space-16: 32px; - --space-17: 36px; - - /* control heights */ - --h-control-sm: 30px; - --h-control-md: 34px; - --h-control: 36px; - --h-control-lg: 42px; - --h-control-xl: 44px; - --h-control-2xl: 48px; - --h-topbar: 56px; - - /* layout */ - --sidebar-width: 248px; - --content-max: 1340px; -} diff --git a/design/baseline/Chatballs Baseline v2/_ds/tokens/typography.css b/design/baseline/Chatballs Baseline v2/_ds/tokens/typography.css deleted file mode 100644 index 119e53e..0000000 --- a/design/baseline/Chatballs Baseline v2/_ds/tokens/typography.css +++ /dev/null @@ -1,42 +0,0 @@ -/* Chatballs — typography tokens - The internal Hub uses the Ant Design default system font stack — no bespoke webfont - is loaded (see ADR-HUB-0013: "используется системный font stack Ant Design без - обязательной внешней зависимости от загружаемого шрифта"). Technical identifiers - (order/release/event IDs, emails) use a monospace stack. */ - -:root { - --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; - --font-mono: "SF Mono", ui-monospace, Menlo, Consolas, monospace; - - /* type scale, taken verbatim from the baseline (px, not snapped to a 4/8 grid) */ - --text-2xl: 26px; /* page hero title (Command Center) */ - --text-xl: 24px; /* page title */ - --text-lg: 20px; /* auth title */ - --text-md: 18px; /* card title */ - --text-base-lg: 16px; - --text-base: 14.5px; /* buttons, primary body */ - --text-sm-lg: 13.5px; - --text-sm: 13px; /* table body */ - --text-sm-tight: 12.5px; - --text-xs: 12px; /* meta */ - --text-xs-tight: 11.5px; - --text-2xs: 11px; /* table header / eyebrow */ - --text-2xs-tight: 10.5px; - - --weight-regular: 400; - --weight-medium: 500; - --weight-semibold: 600; - --weight-bold: 700; - --weight-black: 800; - - --tracking-tight: -0.02em; /* large headings */ - --tracking-tight-sm: -0.01em; - --tracking-wide: 0.03em; /* table header eyebrows */ - --tracking-wider: 0.06em; /* nav section labels */ - - --leading-tight: 1.15; - --leading-snug: 1.2; - --leading-normal: 1.4; - --leading-relaxed: 1.5; - --leading-loose: 1.55; -} diff --git a/design/baseline/Chatballs Baseline v2/assets/av-1.png b/design/baseline/Chatballs Baseline v2/assets/av-1.png deleted file mode 100644 index 716ee3c..0000000 Binary files a/design/baseline/Chatballs Baseline v2/assets/av-1.png and /dev/null differ diff --git a/design/baseline/Chatballs Baseline v2/assets/av-2.png b/design/baseline/Chatballs Baseline v2/assets/av-2.png deleted file mode 100644 index 866d506..0000000 Binary files a/design/baseline/Chatballs Baseline v2/assets/av-2.png and /dev/null differ diff --git a/design/baseline/Chatballs Baseline v2/assets/av-3.png b/design/baseline/Chatballs Baseline v2/assets/av-3.png deleted file mode 100644 index e9994c0..0000000 Binary files a/design/baseline/Chatballs Baseline v2/assets/av-3.png and /dev/null differ diff --git a/design/baseline/Chatballs Baseline v2/assets/av-4.png b/design/baseline/Chatballs Baseline v2/assets/av-4.png deleted file mode 100644 index 9a121e0..0000000 Binary files a/design/baseline/Chatballs Baseline v2/assets/av-4.png and /dev/null differ diff --git a/design/baseline/Chatballs Baseline v2/assets/logo-mark.svg b/design/baseline/Chatballs Baseline v2/assets/logo-mark.svg deleted file mode 100644 index 64ed098..0000000 --- a/design/baseline/Chatballs Baseline v2/assets/logo-mark.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/design/baseline/Chatballs Baseline v2/support.js b/design/baseline/Chatballs Baseline v2/support.js deleted file mode 100644 index cb009b6..0000000 --- a/design/baseline/Chatballs Baseline v2/support.js +++ /dev/null @@ -1,1911 +0,0 @@ -// GENERATED from dc-runtime/src/*.ts — do not edit. Rebuild with `cd dc-runtime && bun run build`. -"use strict"; -(() => { - var __defProp = Object.defineProperty; - var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; - var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); - - // src/react.ts - function getReact() { - const R = window.React; - if (!R) throw new Error("dc-runtime: window.React is not available yet"); - return R; - } - function getReactDOM() { - const RD = window.ReactDOM; - if (!RD) throw new Error("dc-runtime: window.ReactDOM is not available yet"); - return RD; - } - var h = ((...args) => getReact().createElement( - ...args - )); - - // src/parse.ts - function parseDcDocument(doc) { - const dc = doc.querySelector("x-dc"); - if (!dc) return null; - const scriptEl = doc.querySelector("script[data-dc-script]"); - const { props, preview } = parseDataProps( - scriptEl?.getAttribute("data-props") ?? null - ); - return { - template: dc.innerHTML, - js: scriptEl ? scriptEl.textContent || "" : "", - props, - preview - }; - } - function parseDcText(src) { - const openMatch = /]*)?>/.exec(src); - if (!openMatch) return null; - const close = src.lastIndexOf("
"); - if (close === -1 || close < openMatch.index) return null; - const template = src.slice(openMatch.index + openMatch[0].length, close); - const doc = new DOMParser().parseFromString(src, "text/html"); - const scriptEl = doc.querySelector("script[data-dc-script]"); - const { props, preview } = parseDataProps( - scriptEl?.getAttribute("data-props") ?? null - ); - return { - template, - js: scriptEl ? scriptEl.textContent || "" : "", - props, - preview - }; - } - function parseDataProps(raw) { - if (!raw) return { props: null, preview: null }; - let parsed; - try { - parsed = JSON.parse(raw); - } catch { - return { props: null, preview: null }; - } - if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { - return { props: null, preview: null }; - } - const obj = parsed; - const preview = obj.$preview && typeof obj.$preview === "object" ? obj.$preview : null; - const rest = {}; - for (const k of Object.keys(obj)) { - if (k[0] !== "$") rest[k] = obj[k]; - } - return { props: Object.keys(rest).length ? rest : null, preview }; - } - function dcNameFromPath(pathname) { - let p = pathname || ""; - try { - p = decodeURIComponent(p); - } catch { - } - const base = p.split("/").pop() || "Root"; - return base.replace(/\.dc\.html$/, "").replace(/\.html?$/, "") || "Root"; - } - - // src/boot.ts - var BASE_CSS = ` - .sc-placeholder{background:color-mix(in srgb,currentColor 8%,transparent); - border:1px solid color-mix(in srgb,currentColor 50%,transparent); - border-radius:2px;box-sizing:border-box;overflow:hidden} - @keyframes sc-shine{0%{background-position:100% 50%}100%{background-position:0% 50%}} - html.sc-dc-streaming .sc-placeholder, - html.sc-dc-streaming .sc-interp.sc-missing{position:relative; - background:color-mix(in srgb,currentColor 5%,transparent); - border-color:transparent} - html.sc-dc-streaming .sc-placeholder::before, - html.sc-dc-streaming .sc-interp.sc-missing::before{content:''; - position:absolute;inset:0;pointer-events:none; - background:linear-gradient(90deg,rgba(217,119,87,0) 25%,rgba(247,225,211,.95) 37%,rgba(217,119,87,0) 63%); - background-size:400% 100%;animation:sc-shine 1.4s ease infinite} - html.sc-dc-streaming .sc-placeholder:nth-child(n+9 of .sc-placeholder)::before, - html.sc-dc-streaming .sc-interp.sc-missing:nth-child(n+9 of .sc-interp.sc-missing)::before{animation:none; - background:color-mix(in srgb,currentColor 8%,transparent)} - .sc-placeholder-error{padding:4px 8px;font:11px/1.4 ui-monospace,monospace; - color:color-mix(in srgb,currentColor 70%,transparent);word-break:break-word} - .sc-interp.sc-missing{display:inline-block;width:2em;height:1em;overflow:hidden; - vertical-align:text-bottom;background:rgba(255,255,255,.3);border:1px solid rgba(0,0,0,.5); - border-radius:2px;box-sizing:border-box;color:transparent; - user-select:none} - .sc-interp.sc-unresolved{font-family:ui-monospace,monospace;font-size:.85em; - color:color-mix(in srgb,currentColor 50%,transparent); - background:color-mix(in srgb,currentColor 10%,transparent);border-radius:3px; - padding:0 3px} - .sc-host.sc-has-error{position:relative} - .sc-logic-error{position:absolute;top:8px;left:8px;z-index:2147483647;max-width:60ch; - padding:6px 10px;background:#b00020;color:#fff;font:12px/1.4 ui-monospace,monospace; - border-radius:4px;white-space:pre-wrap;pointer-events:none} - /* Mirrors PRINT_BASELINE_CSS in apps/web deck-stage-export.ts \u2014 keep both - in sync until dc-runtime regains a build step. */ - @media print { - @page { margin: 0.5cm; } - figure, table { break-inside: avoid; } - #dc-root, #dc-root > .sc-host { height: auto; } - *, *::before, *::after { - print-color-adjust: exact; -webkit-print-color-adjust: exact; - backdrop-filter: none !important; -webkit-backdrop-filter: none !important; - animation-delay: -99s !important; animation-duration: .001s !important; - animation-iteration-count: 1 !important; animation-fill-mode: both !important; - animation-play-state: running !important; transition-duration: 0s !important; - } - } - `; - var FULL_PAGE_CSS = "html,body{height:100%;margin:0}#dc-root,#dc-root>.sc-host{height:100%}"; - function rootNameForDocument(doc, loc) { - let bootPath = loc.pathname || ""; - if (!/\.dc\.html?$/i.test(safeDecode(bootPath))) { - try { - bootPath = new URL(doc.baseURI || "/").pathname; - } catch { - } - } - return dcNameFromPath(bootPath); - } - function safeDecode(s) { - try { - return decodeURIComponent(s); - } catch { - return s; - } - } - function boot(runtime, doc = document) { - const parsed = parseDcDocument(doc); - if (!parsed) return null; - const React = getReact(); - const rootName = rootNameForDocument(doc, location); - runtime.markFetched(rootName); - runtime.setRootName(rootName); - runtime.adoptParsed(rootName, parsed); - if (!window.__resources) { - fetch(location.href).then((res) => res.ok ? res.text() : "").then((t) => { - const raw = t ? parseDcText(t) : null; - if (raw?.template) runtime.updateHtml(rootName, raw.template); - }).catch(() => { - }); - } - const dc = doc.querySelector("x-dc"); - const hostEl = doc.createElement("div"); - hostEl.id = "dc-root"; - dc.replaceWith(hostEl); - if (!parsed.preview) { - const s = doc.createElement("style"); - s.textContent = FULL_PAGE_CSS; - doc.head.appendChild(s); - } - const Root = runtime.getDC(rootName); - const entry = runtime.registry.get(rootName); - function StandaloneRoot() { - const [, setTick] = React.useState(0); - React.useEffect(() => { - const sub = () => setTick((n) => n + 1); - entry.subs.add(sub); - return () => { - entry.subs.delete(sub); - }; - }, []); - const defaults = React.useMemo(() => { - const d = {}; - for (const k in entry.propsMeta || {}) { - const v = entry.propsMeta?.[k]?.default; - if (v !== void 0) d[k] = v; - } - return d; - }, [entry.propsMeta]); - return h(Root, { ...defaults, ...entry.propOverrides || {} }); - } - const ReactDOM = getReactDOM(); - if (ReactDOM.createRoot) - ReactDOM.createRoot(hostEl).render(h(StandaloneRoot)); - else ReactDOM.render(h(StandaloneRoot), hostEl); - return rootName; - } - - // src/expr.ts - var IDENT_RE = /^[A-Za-z_$][A-Za-z0-9_$]*/; - var NUMBER_RE = /^-?\d+(\.\d+)?$/; - function resolve(vals, src) { - const expr = String(src).trim(); - if (!expr) return void 0; - if (expr[0] === "(" && expr[expr.length - 1] === ")" && parensWrapWhole(expr)) { - return resolve(vals, expr.slice(1, -1)); - } - const eq = findTopLevelEquality(expr); - if (eq) { - const lv = resolve(vals, expr.slice(0, eq.index)); - const rv = resolve(vals, expr.slice(eq.index + eq.op.length)); - switch (eq.op) { - case "===": - return lv === rv; - case "!==": - return lv !== rv; - case "==": - return lv == rv; - default: - return lv != rv; - } - } - if (expr[0] === "!") return !resolve(vals, expr.slice(1)); - if (expr === "true") return true; - if (expr === "false") return false; - if (expr === "null") return null; - if (expr === "undefined") return void 0; - if (NUMBER_RE.test(expr)) return Number(expr); - if (expr.length >= 2 && (expr[0] === '"' || expr[0] === "'") && expr[expr.length - 1] === expr[0]) { - return expr.slice(1, -1); - } - return resolvePath(vals, expr); - } - function parensWrapWhole(expr) { - let depth = 0; - for (let i = 0; i < expr.length - 1; i++) { - if (expr[i] === "(") depth++; - else if (expr[i] === ")") { - depth--; - if (depth === 0) return false; - } - } - return true; - } - function findTopLevelEquality(expr) { - let depth = 0; - for (let i = 0; i < expr.length; i++) { - const c = expr[i]; - if (c === "[" || c === "(") depth++; - else if (c === "]" || c === ")") depth--; - else if (depth === 0 && (c === "=" || c === "!") && expr[i + 1] === "=") { - if (i > 0 && (expr[i - 1] === "=" || expr[i - 1] === "!")) continue; - if (!expr.slice(0, i).trim()) continue; - const op = expr[i + 2] === "=" ? c + "==" : c + "="; - return { index: i, op }; - } - } - return null; - } - function resolvePath(vals, expr) { - const head = expr.match(IDENT_RE); - if (!head) return void 0; - let cur = vals == null ? void 0 : vals[head[0]]; - let i = head[0].length; - while (i < expr.length) { - if (expr[i] === ".") { - const m = expr.slice(i + 1).match(IDENT_RE) || expr.slice(i + 1).match(/^\d+/); - if (!m) return void 0; - cur = cur == null ? void 0 : cur[m[0]]; - i += 1 + m[0].length; - } else if (expr[i] === "[") { - let depth = 1; - let j = i + 1; - while (j < expr.length && depth > 0) { - if (expr[j] === "[") depth++; - else if (expr[j] === "]") { - depth--; - if (depth === 0) break; - } - j++; - } - if (depth !== 0) return void 0; - const key = resolve(vals, expr.slice(i + 1, j)); - cur = cur == null ? void 0 : cur[key]; - i = j + 1; - } else { - return void 0; - } - } - return cur; - } - - // src/encode.ts - var CAMEL_ATTR = "sc-camel-"; - var INLINE_TEXT_TAGS = new Set( - "a abbr b bdi bdo br cite code del dfn em i ins kbd mark q s samp small span strike strong sub sup u var wbr".split( - " " - ) - ); - var RAW_WRAP = { - select: "sc-raw-select", - table: "sc-raw-table", - tbody: "sc-raw-tbody", - thead: "sc-raw-thead", - tfoot: "sc-raw-tfoot", - tr: "sc-raw-tr", - td: "sc-raw-td", - th: "sc-raw-th", - caption: "sc-raw-caption" - }; - var RAW_UNWRAP = Object.fromEntries( - Object.entries(RAW_WRAP).map(([k, v]) => [v, k]) - ); - var EVENT_MAP = { - onclick: "onClick", - onchange: "onChange", - oninput: "onInput", - onsubmit: "onSubmit", - onkeydown: "onKeyDown", - onkeyup: "onKeyUp", - onkeypress: "onKeyPress", - onmousedown: "onMouseDown", - onmouseup: "onMouseUp", - onmouseenter: "onMouseEnter", - onmouseleave: "onMouseLeave", - onfocus: "onFocus", - onblur: "onBlur", - ondoubleclick: "onDoubleClick", - oncontextmenu: "onContextMenu", - onmousemove: "onMouseMove", - onmouseover: "onMouseOver", - onmouseout: "onMouseOut", - onpointerdown: "onPointerDown", - onpointerup: "onPointerUp", - onpointermove: "onPointerMove", - onpointerenter: "onPointerEnter", - onpointerleave: "onPointerLeave", - onpointercancel: "onPointerCancel", - onpointerover: "onPointerOver", - onpointerout: "onPointerOut", - ongotpointercapture: "onGotPointerCapture", - onlostpointercapture: "onLostPointerCapture", - ontouchstart: "onTouchStart", - ontouchend: "onTouchEnd", - ontouchmove: "onTouchMove", - ontouchcancel: "onTouchCancel", - ondragstart: "onDragStart", - ondragend: "onDragEnd", - ondragenter: "onDragEnter", - ondragleave: "onDragLeave", - ondragover: "onDragOver", - onanimationstart: "onAnimationStart", - onanimationend: "onAnimationEnd", - onanimationiteration: "onAnimationIteration", - ontransitionend: "onTransitionEnd" - }; - var ATTRS = `(?:[^>"']|"[^"]*"|'[^']*')*`; - var IMPORT_SELF_CLOSE_RE = new RegExp( - "<(x-import|dc-import)(" + ATTRS + ")/>", - "gi" - ); - var CAMEL_ATTR_RE = /(\s)([a-z]+[A-Z][A-Za-z0-9]*)(\s*=)/g; - function encodeCamelAttrs(html) { - return html.replace( - CAMEL_ATTR_RE, - (_, sp, name, eq) => sp + CAMEL_ATTR + name.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase()) + eq - ); - } - function encodeCase(html) { - html = html.replace( - IMPORT_SELF_CLOSE_RE, - (_, t, a) => "<" + t + a + ">" - ); - html = html.replace(/)/gi, "/gi, ""); - html = encodeCamelAttrs(html); - for (const [real, alias] of Object.entries(RAW_WRAP)) { - html = html.replace( - new RegExp("(])", "gi"), - "$1" + alias - ); - } - return html; - } - function kebabToCamel(s) { - return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase()); - } - function cssToObj(css) { - const o = {}; - for (const decl of css.split(";")) { - const i = decl.indexOf(":"); - if (i < 0) continue; - const prop = decl.slice(0, i).trim(); - o[prop.startsWith("--") ? prop : kebabToCamel(prop)] = decl.slice(i + 1).trim(); - } - return o; - } - function compileAttr(raw) { - const whole = raw.match(/^\s*\{\{([\s\S]+?)\}\}\s*$/); - if (whole) { - const path = whole[1]; - return (vals) => resolve(vals, path); - } - if (raw.includes("{{")) { - const parts = raw.split(/\{\{([\s\S]+?)\}\}/g); - return (vals) => parts.map((s, i) => i & 1 ? resolve(vals, s) ?? "" : s).join(""); - } - return () => raw; - } - - // src/compile.ts - function collectProps(node, kind, host) { - const propGetters = []; - const pseudoClasses = []; - let hintSize = null; - for (const { name, value } of [...node.attributes]) { - if (name === "sc-name" || name === "data-dc-tpl") continue; - let key = name; - if (key.startsWith(CAMEL_ATTR)) - key = kebabToCamel(key.slice(CAMEL_ATTR.length)); - if (key === "hint-size") { - hintSize = value; - continue; - } - if (key.startsWith("style-")) { - pseudoClasses.push(host.pseudoClass(key.slice(6), value)); - continue; - } - if (kind !== "dom") { - if (key.includes("-") && !(kind === "x-import" && (key.startsWith("aria-") || key.startsWith("data-")))) - key = kebabToCamel(key); - } else { - if (key === "class") key = "className"; - else if (key === "for") key = "htmlFor"; - else if (key.startsWith("on")) - key = EVENT_MAP[key] || "on" + key[2].toUpperCase() + key.slice(3); - } - propGetters.push([key, compileAttr(value)]); - } - return { propGetters, pseudoClasses, hintSize }; - } - var HOST_STYLE_PROPS = /* @__PURE__ */ new Set([ - "position", - "left", - "right", - "top", - "bottom", - "inset", - "width", - "height", - "z-index", - "transform" - ]); - function hostPositionStyle(style) { - const all = typeof style === "string" ? cssToObj(style) : style != null && typeof style === "object" ? style : null; - if (!all) return void 0; - const out = {}; - for (const [k, v] of Object.entries(all)) { - const kebab = k.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase()); - if (HOST_STYLE_PROPS.has(kebab)) out[k] = v; - } - return Object.keys(out).length ? out : void 0; - } - function compileTemplate(html, host) { - const tpl = document.createElement("template"); - //! nosemgrep: direct-inner-html-assignment - tpl.innerHTML = encodeCase(html); - let tplN = 0; - (function stamp(node) { - if (node.nodeType === Node.ELEMENT_NODE) { - node.setAttribute("data-dc-tpl", String(tplN++)); - } - for (const c of node.childNodes) stamp(c); - })(tpl.content); - const builders = walkChildren(tpl.content, host); - const render = ((vals, ctx) => builders.map((b, i) => b(vals || {}, ctx, i))); - render.__annotated = tpl.innerHTML; - return render; - } - function walkChildren(node, host) { - return [...node.childNodes].map((c) => walk(c, host)).filter((b) => b != null); - } - var SLIDE_ID_VALUE_RE = /^[0-9a-f]{8}$/; - var DECK_CONTROL_FLOW_RE = /^(sc-if|sc-for|sc-else|dc-import|x-import)$/; - var DECK_AUX_RE = /^(template|script|style|sc-helmet|helmet)$/; - function isDeckMountTag(el) { - if (el.localName === "deck-stage") return true; - return el.localName === "x-import" && (el.getAttribute("component-from-global-scope") || "") === "deck-stage"; - } - function walkDeckChildren(el, host) { - const pairs = [...el.childNodes].map((c) => ({ c, b: walk(c, host) })).filter((p) => p.b !== null); - const kids = pairs.map((p) => p.b); - const seen = /* @__PURE__ */ new Set(); - const wsSeen = /* @__PURE__ */ new Map(); - const keys = []; - const nextSlideId = new Array(pairs.length); - { - let upcoming = null; - for (let j = pairs.length - 1; j >= 0; j--) { - const n = pairs[j].c; - if (n.nodeType === Node.ELEMENT_NODE) { - const t = n.localName; - upcoming = !DECK_AUX_RE.test(t) && !DECK_CONTROL_FLOW_RE.test(t) ? n.getAttribute("data-om-slide-id") : null; - } - nextSlideId[j] = upcoming; - } - } - for (let j = 0; j < pairs.length; j++) { - const { c } = pairs[j]; - if (c.nodeType === Node.TEXT_NODE) { - if ((c.nodeValue ?? "").trim() === "") { - const base = nextSlideId[j] ? "omid-ws:" + nextSlideId[j] : "omid-ws:aux"; - const n = wsSeen.get(base) ?? 0; - wsSeen.set(base, n + 1); - keys.push(n === 0 ? base : base + ":" + n); - continue; - } - return { kids, keys: null }; - } - if (c.nodeType !== Node.ELEMENT_NODE) { - keys.push(j); - continue; - } - const child = c; - const tag = child.localName; - if (DECK_AUX_RE.test(tag)) { - keys.push(j); - continue; - } - if (DECK_CONTROL_FLOW_RE.test(tag)) return { kids, keys: null }; - const v = child.getAttribute("data-om-slide-id"); - if (!v || !SLIDE_ID_VALUE_RE.test(v) || seen.has(v)) { - return { kids, keys: null }; - } - seen.add(v); - keys.push("omid:" + v); - } - return { kids, keys }; - } - function renderDeckKids(kids, kidKeys, vals, ctx) { - return kids.map((b, j) => { - const k = kidKeys ? kidKeys[j] : j; - const out = b(vals, ctx, k); - return kidKeys != null && typeof out === "string" ? h(getReact().Fragment, { key: k }, out) : out; - }); - } - function walk(node, host) { - if (node.nodeType === Node.TEXT_NODE) return walkText(node); - if (node.nodeType !== Node.ELEMENT_NODE) return null; - const el = node; - const tag = el.tagName.toLowerCase(); - if (tag === "sc-for") return walkFor(el, host); - if (tag === "sc-if") return walkIf(el, host); - if (tag === "x-import") return walkXImport(el, host); - if (tag === "sc-helmet") return host.helmet(el); - if (tag === "dc-import") return walkComponent(el, host); - return walkElement(el, host); - } - var warnedHoles = /* @__PURE__ */ new Set(); - function warnUnresolved(ctx, what) { - const key = (ctx?.__name || "?") + "\0" + what; - if (warnedHoles.has(key)) return; - warnedHoles.add(key); - console.warn("[dc-runtime] " + (ctx?.__name || "template") + ": " + what); - } - function walkText(node) { - const txt = node.nodeValue ?? ""; - if (!txt.includes("{{")) { - if (!txt.trim() && !txt.includes(" ")) return null; - return () => txt; - } - const parts = txt.split(/\{\{([\s\S]+?)\}\}/g); - return (vals, ctx, key) => h( - getReact().Fragment, - { key }, - ...parts.map((p, i) => { - if (!(i & 1)) return p; - const v = resolve(vals, p); - if (v === void 0) { - if (!ctx?.__streamingNow) { - if (document.body?.hasAttribute("data-dc-editor-on")) { - return h( - "span", - { key: i, className: "sc-interp sc-unresolved" }, - "{{ " + p.trim() + " }}" - ); - } - warnUnresolved( - ctx, - "{{ " + p.trim() + " }} never resolved \u2014 rendered as empty" - ); - return null; - } - return h( - "span", - { key: i, className: "sc-interp sc-missing" }, - p.trim() - ); - } - if (getReact().isValidElement(v) || Array.isArray(v)) { - return h(getReact().Fragment, { key: i }, v); - } - if (v === null || typeof v === "boolean") return null; - return h("span", { key: i, className: "sc-interp" }, String(v)); - }) - ); - } - function walkFor(el, host) { - const listGet = compileAttr(el.getAttribute("list") || ""); - const asName = el.getAttribute("as") || "item"; - const hintN = parseInt(el.getAttribute("hint-placeholder-count") || "0", 10); - const kids = walkChildren(el, host); - const listSrc = el.getAttribute("list") || ""; - return (vals, ctx, key) => { - let list = listGet(vals); - if (!Array.isArray(list)) { - if (!ctx?.__streamingNow) { - if (list !== void 0 && list !== null) { - warnUnresolved( - ctx, - 'sc-for list="' + listSrc + '" is not an array (' + typeof list + ")" - ); - } - list = []; - } else { - list = hintN > 0 ? Array(hintN).fill(void 0) : []; - } - } - return h( - getReact().Fragment, - { key }, - list.map((item, i) => { - const sub = { ...vals, [asName]: item, $index: i }; - return h( - getReact().Fragment, - { key: i }, - kids.map((b, j) => b(sub, ctx, j)) - ); - }) - ); - }; - } - function walkIf(el, host) { - const valGet = compileAttr(el.getAttribute("value") || ""); - const hintRaw = el.getAttribute("hint-placeholder-val"); - const hintGet = hintRaw != null ? compileAttr(hintRaw) : null; - const kids = walkChildren(el, host); - return (vals, ctx, key) => { - let v = valGet(vals); - if (v === void 0 && hintGet && ctx?.__streamingNow) v = hintGet(vals); - return v ? h( - getReact().Fragment, - { key }, - kids.map((b, j) => b(vals, ctx, j)) - ) : null; - }; - } - function walkComponent(el, host) { - const name = el.getAttribute("name") || el.getAttribute("component") || ""; - el.removeAttribute("name"); - el.removeAttribute("component"); - const tplId = el.getAttribute("data-dc-tpl"); - const styleRaw = el.getAttribute("style"); - el.removeAttribute("style"); - const styleGet = styleRaw != null ? compileAttr(styleRaw) : null; - const { propGetters, hintSize } = collectProps(el, "dc-import", host); - const kids = walkChildren(el, host); - return (vals, ctx, key) => { - const props = { - key, - __hintSize: hintSize, - __tplId: tplId, - __hostStyle: styleGet ? hostPositionStyle(styleGet(vals)) : void 0 - }; - for (const [k, g] of propGetters) { - const v = g(vals); - if (k === "dcProps") { - if (v && typeof v === "object") Object.assign(props, v); - continue; - } - props[k] = v; - } - if (kids.length) props.children = kids.map((b, j) => b(vals, ctx, j)); - return h(host.component(name), props); - }; - } - function walkXImport(el, host) { - const globalNameGet = compileAttr( - el.getAttribute("component-from-global-scope") || "" - ); - const exportNameGet = compileAttr( - el.getAttribute("component") || el.getAttribute("name") || "" - ); - const fromRaw = el.getAttribute("from") || (el.getAttribute("component-from-global-scope") ? "" : el.getAttribute("src") || el.getAttribute("import") || ""); - const urls = fromRaw.trim() ? fromRaw.trim().split(/\s+/) : []; - const url = urls.length ? urls[urls.length - 1] : ""; - const kindOf = (u) => /\.(jsx|tsx)(\?|#|$)/i.test(u) ? "jsx" : "js"; - const tplId = el.getAttribute("data-dc-tpl"); - const styleRaw = el.getAttribute("style"); - el.removeAttribute("style"); - const styleGet = styleRaw != null ? compileAttr(styleRaw) : null; - const wrap = tplId != null || styleGet != null; - const { propGetters, hintSize } = collectProps(el, "x-import", host); - const hasContent = el.children.length > 0 || !!(el.textContent || "").trim(); - const deckKeyed = hasContent && isDeckMountTag(el) ? walkDeckChildren(el, host) : null; - const kids = deckKeyed ? deckKeyed.kids : hasContent ? walkChildren(el, host) : []; - const kidKeys = deckKeyed?.keys ?? null; - const urlBindable = fromRaw.includes("{{"); - if (urls.length && !urlBindable) { - let prev; - for (const u of urls) prev = host.loadExternal(kindOf(u), u, prev); - } - const evalName = (g, vals) => { - const v = g(vals); - const s = v == null ? "" : String(v); - return s.includes("{{") ? "" : s; - }; - return (vals, ctx, key) => { - const globalName = evalName(globalNameGet, vals); - const name = globalName || evalName(exportNameGet, vals); - const C = !name || urlBindable ? null : globalName ? host.resolveExternalGlobal(url, globalName) : host.resolveExternal(url, name); - const hostStyle = styleGet ? hostPositionStyle(styleGet(vals)) : void 0; - const wrapper = wrap ? { - key, - className: "sc-host-x", - "data-dc-tpl": tplId, - style: hostStyle || { display: "contents" } - } : null; - if (!C) { - const error = urlBindable ? "x-import `from` cannot contain {{ \u2026 }} \u2014 module URLs are resolved at parse time; use a literal URL" : host.resolveExternalError(url, name); - const ph = host.placeholder({ - key: wrapper ? void 0 : key, - name, - hintSize, - error - }); - return wrapper ? h("div", wrapper, ph) : ph; - } - const props = wrapper ? {} : { key }; - let unresolvedHole = false; - for (const [k, g] of propGetters) { - if (k === "component" || k === "componentFromGlobalScope" || k === "from") { - continue; - } - const v = g(vals); - if (v === void 0) unresolvedHole = true; - if (k === "dcProps") { - if (v && typeof v === "object") Object.assign(props, v); - continue; - } - props[k] = v; - } - if (unresolvedHole && ctx?.__htmlStreamingNow) { - const ph = host.placeholder({ - key: wrapper ? void 0 : key, - name, - hintSize, - error: null - }); - return wrapper ? h("div", wrapper, ph) : ph; - } - if (kids.length) { - props.children = renderDeckKids(kids, kidKeys, vals, ctx); - } - return wrapper ? h("div", wrapper, h(C, props)) : h(C, props); - }; - } - function contentKey(el) { - const clone = el.cloneNode(true); - for (const d of clone.querySelectorAll("*")) { - while (d.attributes.length) d.removeAttribute(d.attributes[0].name); - } - const s = clone.innerHTML; - let h2 = 5381; - for (let i = 0; i < s.length; i++) h2 = (h2 << 5) + h2 + s.charCodeAt(i) | 0; - return s.length + "." + (h2 >>> 0).toString(36); - } - var NEVER_CONTENT_KEYED = new Set( - "script style textarea option title select canvas iframe video audio".split( - " " - ) - ); - var NOT_INLINE_SELECTOR = ":not(" + [...INLINE_TEXT_TAGS].join(",") + ")"; - function walkElement(el, host) { - const realTag = RAW_UNWRAP[el.localName] || el.localName; - const tplId = el.getAttribute("data-dc-tpl"); - const inlineOnly = el.childNodes.length > 0 && !NEVER_CONTENT_KEYED.has(realTag) && el.querySelector(NOT_INLINE_SELECTOR) === null; - const keySuffix = inlineOnly ? "|" + contentKey(el) : ""; - const { propGetters, pseudoClasses } = collectProps(el, "dom", host); - const deckKeyed = isDeckMountTag(el) ? walkDeckChildren(el, host) : null; - const kids = deckKeyed ? deckKeyed.kids : walkChildren(el, host); - const kidKeys = deckKeyed?.keys ?? null; - return (vals, ctx, key) => { - const props = { - key: key + keySuffix, - "data-dc-tpl": tplId - }; - for (const [k, g] of propGetters) { - let v = g(vals); - if (k === "style" && typeof v === "string") v = cssToObj(v); - if ((k === "value" || k === "checked") && v === void 0) { - v = k === "checked" ? false : ""; - } - props[k] = v; - } - if (pseudoClasses.length) { - props.className = [props.className, ...pseudoClasses].filter(Boolean).join(" "); - } - return h(realTag, props, ...renderDeckKids(kids, kidKeys, vals, ctx)); - }; - } - - // src/logic.ts - var StreamableLogic = class { - constructor(props) { - __publicField(this, "props"); - __publicField(this, "state", {}); - /** Back-pointer to the wrapper component, installed after construction. */ - __publicField(this, "__host"); - this.props = props || {}; - } - setState(update, cb) { - this.__host && this.__host.__setLogicState(update, cb); - } - forceUpdate() { - this.__host && this.__host.forceUpdate(); - } - componentDidMount() { - } - componentDidUpdate(_prevProps) { - } - componentWillUnmount() { - } - /** The flat object the template renders against (merged over props). */ - renderVals() { - return {}; - } - }; - function evalDcLogic(src) { - //! nosemgrep: eval-and-function-constructor - const fn = new Function( - "DCLogic", - "StreamableLogic", - "React", - src + '\n;return (typeof Component!=="undefined"&&Component)||undefined;' - ); - return fn(StreamableLogic, StreamableLogic, getReact()); - } - - // src/component.ts - function shallowEqual(a, b) { - if (!b) return false; - const ak = Object.keys(a).filter((k) => k !== "children"); - const bk = Object.keys(b).filter((k) => k !== "children"); - if (ak.length !== bk.length) return false; - for (const k of ak) if (a[k] !== b[k]) return false; - return true; - } - function Placeholder({ - name, - hintSize, - streaming, - error - }) { - const [w, hgt] = (hintSize || "100%,60px").split(","); - return h( - "div", - { - className: "sc-placeholder" + (streaming ? " sc-streaming" : ""), - style: { width: w.trim(), height: hgt && hgt.trim() }, - title: name - }, - error ? h( - "div", - { className: "sc-placeholder-error" }, - (name ? name + ": " : "") + error - ) : null - ); - } - function hintToMin(hint) { - if (!hint) return void 0; - const [w, hgt] = hint.split(","); - return { minWidth: w.trim(), minHeight: hgt && hgt.trim() }; - } - function createComponentFactory(registry, ensureFetched) { - const React = getReact(); - const AncestorContext = React.createContext([]); - class StreamableComponent extends React.Component { - constructor(props) { - super(props); - __publicField(this, "__name"); - __publicField(this, "__sub"); - __publicField(this, "__needsDidMount", false); - /** Snapshot of the registry's streaming flags taken at render time — - * builders read it off the RenderCtx (this) to pick placeholder vs - * render-nothing for unresolved values. */ - __publicField(this, "__streamingNow", false); - __publicField(this, "__htmlStreamingNow", false); - /** When a construct throws, remember the (class, registry.ver, props) - * triple so render-time reconcile doesn't re-attempt it on every parent - * re-render. A registry bump (new class, template, external module - * resolving via bumpAll) changes `ver` and breaks the memo so an - * env-dependent constructor can self-heal. */ - __publicField(this, "__failedLogic", null); - __publicField(this, "__failedUserProps", null); - __publicField(this, "__failedVer", -1); - /** Per-instance constructor error — kept here (not on the registry entry) - * so one instance's successful construct can't hide a sibling's failure, - * and a construct can never wipe an eval error `updateJs` recorded on - * `r.logicError`. */ - __publicField(this, "__ctorError", null); - __publicField(this, "logic"); - this.__name = props.__name; - this.state = { __v: 0, __err: null }; - this.__sub = () => { - if (this.state.__err) this.setState({ __err: null }); - this.forceUpdate(); - }; - this.__makeLogic(registry.get(this.__name).Logic, null); - ensureFetched(this.__name); - } - /** Error-boundary hook: a render crash anywhere in this DC's subtree - * (its own template, an x-import'd component, a child DC without its - * own deeper boundary) lands here instead of unmounting the page. */ - static getDerivedStateFromError(e) { - return { __err: e instanceof Error && e.message ? e.message : String(e) }; - } - componentDidCatch(e, info) { - console.error( - "[dc-runtime] render error in <" + this.__name + ">:", - e, - info?.componentStack || "" - ); - } - /** Instantiate the logic class (or the no-op base) and adopt `prevState` - * over its initial state — used both at mount and on hot-swap. */ - __makeLogic(Logic, prevState) { - const L = Logic || StreamableLogic; - try { - this.logic = new L(this.__userProps()); - this.__failedLogic = null; - this.__failedUserProps = null; - this.__ctorError = null; - } catch (e) { - console.error(e); - this.__failedLogic = Logic; - this.__failedUserProps = this.__userProps(); - this.__failedVer = registry.get(this.__name).ver; - this.__ctorError = this.__name + ": " + (e instanceof Error && e.message ? e.message : String(e)); - this.logic = new StreamableLogic( - this.__userProps() - ); - } - this.logic.__host = this; - if (prevState) - this.logic.state = { ...this.logic.state || {}, ...prevState }; - } - /** The props the author's logic + template see — internal __-prefixed - * wiring stripped. */ - __userProps() { - const { __name, __hintSize, __tplId, __hostStyle, ...rest } = this.props; - return rest; - } - __setLogicState(update, cb) { - const prev = this.logic.state; - const patch = typeof update === "function" ? update(prev) : update; - this.logic.state = { ...prev, ...patch }; - this.setState((s) => ({ __v: s.__v + 1 }), cb); - } - /** Swap the logic instance when the registry's Logic class changed - * (streaming completion, hot reload). State carries over; didMount - * re-fires after the swap commits so refs exist. */ - __reconcileLogic() { - const r = registry.get(this.__name); - const Next = r.Logic; - const Cur = this.logic.constructor; - if (Next === Cur || !Next && Cur === StreamableLogic || Next === this.__failedLogic && r.ver === this.__failedVer && shallowEqual(this.__userProps(), this.__failedUserProps)) { - return; - } - if (!this.__needsDidMount) { - try { - this.logic.componentWillUnmount(); - } catch (e) { - console.error(e); - } - } - this.__makeLogic(Next, this.logic.state); - this.__needsDidMount = true; - } - componentDidMount() { - registry.get(this.__name).subs.add(this.__sub); - try { - this.logic.componentDidMount(); - } catch (e) { - console.error(e); - } - } - componentDidUpdate(prevProps) { - this.logic.props = this.__userProps(); - if (this.__needsDidMount) { - if (this.state.__err || !registry.get(this.__name).tpl) return; - this.__needsDidMount = false; - try { - this.logic.componentDidMount(); - } catch (e) { - console.error(e); - } - } else { - try { - this.logic.componentDidUpdate(prevProps); - } catch (e) { - console.error(e); - } - } - } - componentWillUnmount() { - registry.get(this.__name).subs.delete(this.__sub); - if (!this.__needsDidMount) { - try { - this.logic.componentWillUnmount(); - } catch (e) { - console.error(e); - } - } - } - render() { - const r = registry.get(this.__name); - const cls = "sc-host" + (r.htmlStreaming ? " sc-streaming-html" : "") + (r.jsStreaming ? " sc-streaming-js" : ""); - const hintStyle = r.htmlStreaming ? hintToMin(this.props.__hintSize) : void 0; - const hostStyle = this.props.__hostStyle || hintStyle ? { ...hintStyle || {}, ...this.props.__hostStyle || {} } : void 0; - const hostBase = { - className: cls, - style: hostStyle, - "data-sc-name": this.__name, - "data-dc-tpl": this.props.__tplId - }; - const chain = Array.isArray(this.context) ? this.context : []; - if (chain.includes(this.__name)) { - const cycle = [ - ...chain.slice(chain.indexOf(this.__name)), - this.__name - ].join(" \u2192 "); - return h( - "div", - { ...hostBase, className: cls + " sc-has-error" }, - h(Placeholder, { - name: this.__name, - hintSize: this.props.__hintSize, - error: "circular import: " + cycle - }) - ); - } - if (this.state.__err) { - return h( - "div", - { ...hostBase, className: cls + " sc-has-error" }, - h( - "div", - { className: "sc-logic-error", "data-omelette-chrome": "" }, - this.__name + ": " + this.state.__err - ), - h(Placeholder, { - name: this.__name, - hintSize: this.props.__hintSize, - error: this.state.__err - }) - ); - } - this.__reconcileLogic(); - if (!r.tpl) { - return h( - "div", - hostBase, - h(Placeholder, { name: this.__name, hintSize: this.props.__hintSize }) - ); - } - const userProps = this.__userProps(); - this.logic.props = userProps; - let vals = userProps; - let renderErr = r.logicError || this.__ctorError; - try { - vals = { ...userProps, ...this.logic.renderVals() || {} }; - } catch (e) { - console.error(e); - renderErr = this.__name + ".renderVals(): " + (e instanceof Error && e.message ? e.message : String(e)); - } - this.__streamingNow = !!(r.htmlStreaming || r.jsStreaming); - this.__htmlStreamingNow = !!r.htmlStreaming; - return h( - "div", - { ...hostBase, className: cls + (renderErr ? " sc-has-error" : "") }, - renderErr && h( - "div", - { className: "sc-logic-error", "data-omelette-chrome": "" }, - renderErr - ), - h( - AncestorContext.Provider, - { value: [...chain, this.__name] }, - r.tpl(vals, this) - ) - ); - } - } - __publicField(StreamableComponent, "contextType", AncestorContext); - const named = /* @__PURE__ */ new Map(); - function getDC(name) { - const hit = named.get(name); - if (hit) return hit; - function Dispatcher(p) { - const [, setTick] = React.useState(0); - React.useEffect(() => { - const sub = () => setTick((n) => n + 1); - registry.get(name).subs.add(sub); - return () => { - registry.get(name).subs.delete(sub); - }; - }, []); - ensureFetched(name); - return h(StreamableComponent, { ...p, __name: name }); - } - Dispatcher.displayName = name; - named.set(name, Dispatcher); - return Dispatcher; - } - return { - getDC, - StreamableComponent - }; - } - - // src/bundled.ts - function bundledBlob(url) { - const blobs = window.__resourceBlobs; - const b = blobs ? blobs[url.split("#")[0]] : void 0; - return b instanceof Blob ? b : null; - } - - // src/cdn.ts - var REACT_URL = "https://unpkg.com/react@18.3.1/umd/react.production.min.js"; - var REACT_SRI = "sha384-DGyLxAyjq0f9SPpVevD6IgztCFlnMF6oW/XQGmfe+IsZ8TqEiDrcHkMLKI6fiB/Z"; - var REACT_DOM_URL = "https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"; - var REACT_DOM_SRI = "sha384-gTGxhz21lVGYNMcdJOyq01Edg0jhn/c22nsx0kyqP0TxaV5WVdsSH1fSDUf5YJj1"; - var BABEL_URL = "https://unpkg.com/@babel/standalone@7.29.0/babel.min.js"; - var BABEL_SRI = "sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y"; - function cdnScriptFor(url, sri) { - const res = window.__resources; - const v = res ? res[url] : void 0; - return typeof v === "string" && v ? { src: v } : { src: url, integrity: sri }; - } - - // src/external.ts - var isCustomElementName = (n) => !n.includes(".") && n.includes("-"); - function isRenderableType(g) { - if (typeof g === "function") return !isElementClass(g); - return typeof g === "object" && g !== null && typeof g.$$typeof === "symbol"; - } - function resolveDottedPath(root, name) { - let cur = root; - for (const seg of name.split(".")) { - if (cur == null) return void 0; - cur = cur[seg]; - } - return cur; - } - var GLOBAL_POLL_INTERVAL_MS = 50; - var GLOBAL_POLL_TIMEOUT_MS = 3e4; - function createExternalModules(onResolved) { - const cache = /* @__PURE__ */ new Map(); - let babelLoading = null; - const reportedMissing = /* @__PURE__ */ new Map(); - const polling = /* @__PURE__ */ new Set(); - function ensureBabel() { - if (window.Babel) return Promise.resolve(); - if (babelLoading) return babelLoading; - const babel = cdnScriptFor(BABEL_URL, BABEL_SRI); - babelLoading = new Promise((res, rej) => { - const s = document.createElement("script"); - s.src = babel.src; - if (babel.integrity) { - s.integrity = babel.integrity; - s.crossOrigin = "anonymous"; - } - s.onload = () => res(); - s.onerror = rej; - document.head.appendChild(s); - }); - return babelLoading; - } - const pending = /* @__PURE__ */ new Map(); - function load(kind, url, after) { - const existing = pending.get(url); - if (existing) return existing; - cache.set(url, null); - console.info("[dc-runtime] x-import: loading", url, "(" + kind + ")"); - const ready = Promise.all([ - kind === "jsx" ? ensureBabel() : Promise.resolve(), - after ?? Promise.resolve() - ]); - const p = ready.then(() => { - const pre = bundledBlob(url); - if (pre) return pre.text(); - return fetch(url).then((r) => { - if (!r.ok) throw new Error("HTTP " + r.status); - return r.text(); - }); - }).then((src) => { - const code = kind === "jsx" ? window.Babel.transform(src, { - filename: url, - presets: ["react", "typescript"] - }).code : src; - const module = { exports: {} }; - const before = new Set(Object.keys(window)); - //! nosemgrep: eval-and-function-constructor - new Function("React", "module", "exports", "require", code)( - getReact(), - module, - module.exports, - () => ({}) - ); - const globals = {}; - for (const k of Object.keys(window)) { - if (!before.has(k) && typeof window[k] === "function") { - globals[k] = window[k]; - } - } - cache.set(url, { mod: module.exports, globals }); - console.info( - "[dc-runtime] x-import: loaded", - url, - "\u2014 exports:", - Object.keys(module.exports), - "window globals:", - Object.keys(globals) - ); - onResolved(); - }).catch((e) => { - cache.set(url, { - mod: {}, - globals: {}, - error: "failed to load: " + (e instanceof Error && e.message ? e.message : String(e)) - }); - console.error( - "[dc-runtime] x-import: FAILED to load", - url, - "(" + kind + ")", - e - ); - onResolved(); - }); - pending.set(url, p); - return p; - } - function resolve2(url, name) { - const entry = cache.get(url); - if (!entry) return null; - const { mod, globals } = entry; - const C = mod && mod[name] || globals && globals[name] || typeof window !== "undefined" && window[name] || mod && mod.default; - if (typeof C === "function") return C; - const key = url + "\0" + name; - if (!reportedMissing.has(key)) { - reportedMissing.set( - key, - entry.error || 'no export named "' + name + '" (has: ' + Object.keys(mod).join(", ") + ")" - ); - console.error( - "[dc-runtime] x-import: module", - url, - "loaded but has no component named", - JSON.stringify(name), - "\u2014 available exports:", - Object.keys(mod), - "window globals:", - Object.keys(globals), - ". The module must `module.exports = {" + name + "}` or set `window." + name + "`." - ); - } - return null; - } - function waitForGlobal(name) { - if (polling.has(name)) return; - polling.add(name); - const started = Date.now(); - const isCE = isCustomElementName(name); - const tick = () => { - const found = isCE ? customElements.get(name) : isRenderableType(resolveDottedPath(window, name)); - if (found) { - polling.delete(name); - onResolved(); - return; - } - if (Date.now() - started >= GLOBAL_POLL_TIMEOUT_MS) { - console.warn( - "[dc-runtime] x-import: global", - JSON.stringify(name), - "never appeared on window after " + GLOBAL_POLL_TIMEOUT_MS + "ms" - ); - return; - } - setTimeout(tick, GLOBAL_POLL_INTERVAL_MS); - }; - setTimeout(tick, GLOBAL_POLL_INTERVAL_MS); - } - function resolveGlobal(url, name) { - const isCE = isCustomElementName(name); - if (!url) { - if (isCE) { - if (customElements.get(name)) return name; - waitForGlobal(name); - return null; - } - const g2 = resolveDottedPath(window, name); - if (isRenderableType(g2)) return g2; - waitForGlobal(name); - return null; - } - const entry = cache.get(url); - if (!entry) return null; - if (isCE && customElements.get(name)) return name; - const g = entry.globals[name] ?? resolveDottedPath(window, name); - if (isRenderableType(g)) return g; - if (name.includes(".")) return null; - const key = url + "\0global\0" + name; - if (!reportedMissing.has(key)) { - reportedMissing.set(key, null); - if (isCE && !customElements.get(name)) { - console.warn( - "[dc-runtime] x-import:", - url, - "loaded but no custom element", - JSON.stringify(name), - "is registered and window." + name + " is not a function \u2014 rendering <" + name + "> as an unknown element." - ); - } - } - return name; - } - function getError(url, name) { - const entry = cache.get(url); - if (entry?.error) return entry.error; - return reportedMissing.get(url + "\0" + name) || null; - } - return { load, resolve: resolve2, resolveGlobal, getError }; - } - function isElementClass(g) { - try { - return typeof g === "function" && typeof HTMLElement !== "undefined" && g.prototype instanceof HTMLElement; - } catch { - return false; - } - } - - // src/atomics.ts - var ATOMIC_CSS = ( - // layout - ".fx{display:flex}.col{display:flex;flex-direction:column}.grid{display:grid}.ac{align-items:center}.jc{justify-content:center}.jb{justify-content:space-between}.f1{flex:1}.noshrink{flex-shrink:0}.wrap{flex-wrap:wrap}.fw5{font-weight:500}.fw6{font-weight:600}.fw7{font-weight:700}.fw8{font-weight:800}.fs11{font-size:11px}.fs12{font-size:12px}.fs13{font-size:13px}.fs14{font-size:14px}.fs15{font-size:15px}.fs16{font-size:16px}.fs20{font-size:20px}.fs22{font-size:22px}.upper{text-transform:uppercase}.tc{text-align:center}.nowrap{white-space:nowrap}.gap8{gap:8px}.gap10{gap:10px}.gap12{gap:12px}.gap16{gap:16px}.gap24{gap:24px}.m0{margin:0}.mt8{margin-top:8px}.mt12{margin-top:12px}.mt16{margin-top:16px}.mb8{margin-bottom:8px}.mb12{margin-bottom:12px}.mb16{margin-bottom:16px}.posrel{position:relative}.posabs{position:absolute}.round{border-radius:50%}.ohide{overflow:hidden}.bbox{box-sizing:border-box}.pointer{cursor:pointer}.w100{width:100%}.b0{border:none}" - ); - - // src/helmet.ts - var DESIGN_DOC_MODE_RE = /]*\bname\s*=\s*["']design_doc_mode["'][^>]*\b(?:content|value)\s*=\s*["'](\w+)["']/i; - var CANVAS_BG_LIGHT = "#f0eee6"; - var CANVAS_BG_DARK = "#2e2c26"; - function createHelmetManager(doc, isStreaming) { - const mounted = /* @__PURE__ */ new Set(); - const live = /* @__PURE__ */ new Map(); - let designDocMode = null; - let canvasStyleEl = null; - let appTheme = "light"; - try { - const ds = doc.documentElement.dataset.theme; - appTheme = ds === "dark" || ds === "light" ? ds : new URLSearchParams(doc.defaultView?.location.search ?? "").get( - "theme" - ) === "dark" ? "dark" : "light"; - } catch { - } - function applyCanvasBg() { - if (!canvasStyleEl) return; - const bg = appTheme === "dark" ? CANVAS_BG_DARK : CANVAS_BG_LIGHT; - canvasStyleEl.textContent = `html,body{background:${bg}}#dc-root>.sc-host{position:relative}`; - } - function postDesignMode(mode) { - if (window.parent === window) return; - try { - window.parent.postMessage({ type: "__dc_design_mode", mode }, "*"); - } catch { - } - } - function setDesignDocMode(mode) { - if (mode === designDocMode) return; - designDocMode = mode; - postDesignMode(mode); - if (mode === "canvas") { - doc.documentElement.setAttribute("data-dc-canvas", ""); - canvasStyleEl = doc.createElement("style"); - canvasStyleEl.setAttribute("data-dc-canvas", ""); - applyCanvasBg(); - doc.head.appendChild(canvasStyleEl); - } else { - doc.documentElement.removeAttribute("data-dc-canvas"); - canvasStyleEl?.remove(); - canvasStyleEl = null; - } - } - window.addEventListener("message", (e) => { - const type = e.data && e.data.type; - if (type === "__dc_theme") { - const t = e.data.theme; - if (t === "light" || t === "dark") { - appTheme = t; - applyCanvasBg(); - } - return; - } - if (!designDocMode || type !== "__dc_probe") return; - postDesignMode(designDocMode); - }); - function compile(node) { - const raw = [...node.children]; - const helmetClosed = node.nextSibling != null || node.parentNode?.nextSibling != null; - if (node.hasAttribute("data-dc-atomics") && !mounted.has("__dc-atomics")) { - mounted.add("__dc-atomics"); - const el = doc.createElement("style"); - el.id = "__dc-atomics"; - el.textContent = ATOMIC_CSS; - doc.head.appendChild(el); - } - return (_vals, ctx) => { - const name = ctx && ctx.__name || ""; - const streaming = !!(name && isStreaming(name)); - for (let i = 0; i < raw.length; i++) { - const child = raw[i]; - const tag = child.tagName; - const mayBePartial = streaming && !helmetClosed && i === raw.length - 1; - if (tag === "SCRIPT") { - if (mayBePartial) continue; - const key = "SCRIPT|" + (child.getAttribute("src") || child.textContent || ""); - if (mounted.has(key)) continue; - mounted.add(key); - const el = doc.createElement("script"); - for (const { name: an, value } of [...child.attributes]) - el.setAttribute(an, value); - if (child.textContent) el.textContent = child.textContent; - doc.head.appendChild(el); - } else if (tag === "LINK" || tag === "META") { - if (mayBePartial) continue; - const key = tag + "|" + (child.getAttribute("href") || child.getAttribute("src") || child.outerHTML); - if (mounted.has(key)) continue; - mounted.add(key); - if (tag === "LINK") { - const rel = (child.getAttribute("rel") || "").toLowerCase().split(/\s+/); - const href = (child.getAttribute("href") || "").trim(); - const res = window.__resources; - const pre = res && rel.includes("stylesheet") && !rel.includes("alternate") ? res[href] : void 0; - const blob = typeof pre === "string" && pre ? bundledBlob(pre) : null; - if (blob) { - const el = doc.createElement("style"); - if (child.hasAttribute("disabled")) { - el.setAttribute("media", "not all"); - } else if (child.getAttribute("media")) { - el.setAttribute("media", child.getAttribute("media")); - } - if (child.getAttribute("title")) - el.setAttribute("title", child.getAttribute("title")); - void blob.text().then((css) => { - el.textContent = css; - }); - doc.head.appendChild(el); - continue; - } - } - doc.head.appendChild(child.cloneNode(true)); - } else { - const key = name + "|" + i; - let el = live.get(key); - if (!el || el.tagName !== tag) { - if (el) el.remove(); - el = doc.createElement(tag.toLowerCase()); - live.set(key, el); - doc.head.appendChild(el); - } - for (const { name: an, value } of [...child.attributes]) { - if (el.getAttribute(an) !== value) el.setAttribute(an, value); - } - if (el.textContent !== child.textContent) - el.textContent = child.textContent; - } - } - return null; - }; - } - return { compile, setDesignDocMode }; - } - - // src/pseudo.ts - function scanUnquotedUrl(css, i) { - if (css[i] !== "u" && css[i] !== "U" || css.slice(i, i + 4).toLowerCase() !== "url(" || /[a-z0-9_-]/i.test(css[i - 1] ?? "")) { - return -1; - } - let j = i + 4; - while (j < css.length && /\s/.test(css[j])) j++; - if (css[j] === '"' || css[j] === "'") return -1; - while (j < css.length && css[j] !== ")") { - if (css[j] === "\\") j++; - j++; - } - return j < css.length ? j + 1 : css.length; - } - function stripComments(css) { - let out = ""; - let quote = ""; - for (let i = 0; i < css.length; i++) { - const c = css[i]; - if (quote) { - if (c === "\\") { - out += c + (css[i + 1] ?? ""); - i++; - continue; - } - if (c === quote) quote = ""; - out += c; - } else if (c === "'" || c === '"') { - quote = c; - out += c; - } else if (c === "/" && css[i + 1] === "*") { - const end = css.indexOf("*/", i + 2); - i = end === -1 ? css.length : end + 1; - out += " "; - } else { - const end = scanUnquotedUrl(css, i); - if (end === -1) out += c; - else { - out += css.slice(i, end); - i = end - 1; - } - } - } - return out; - } - function importantify(css) { - css = stripComments(css); - const decls = []; - let start = 0; - let depth = 0; - let quote = ""; - for (let i = 0; i < css.length; i++) { - const c = css[i]; - if (quote) { - if (c === "\\") i++; - else if (c === quote) quote = ""; - } else if (c === "'" || c === '"') quote = c; - else if (c === "(") depth++; - else if (c === ")") depth = Math.max(0, depth - 1); - else if (c === ";" && depth === 0) { - decls.push(css.slice(start, i)); - start = i + 1; - } else { - const end = scanUnquotedUrl(css, i); - if (end !== -1) i = end - 1; - } - } - decls.push(css.slice(start)); - return decls.map((d) => d.trim()).filter(Boolean).map((d) => /!\s*important$/i.test(d) ? d : d + " !important").join(";"); - } - function createPseudoSheet(doc) { - let el = null; - const cache = /* @__PURE__ */ new Map(); - let n = 0; - return (pseudo, css) => { - const k = pseudo + "|" + css; - const hit = cache.get(k); - if (hit) return hit; - if (!el) { - el = doc.createElement("style"); - doc.head.appendChild(el); - } - const cls = "scp" + (n++).toString(36); - const isPseudoElement = pseudo === "before" || pseudo === "after"; - const sel = isPseudoElement ? "." + cls + "::" + pseudo : "." + cls + ":" + pseudo; - el.sheet.insertRule( - sel + "{" + (isPseudoElement ? css : importantify(css)) + "}", - el.sheet.cssRules.length - ); - cache.set(k, cls); - return cls; - }; - } - - // src/registry.ts - function createRegistry() { - const entries = /* @__PURE__ */ Object.create(null); - function get(name) { - return entries[name] || (entries[name] = { - html: "", - tpl: null, - Logic: null, - jsStreaming: false, - htmlStreaming: false, - ver: 0, - subs: /* @__PURE__ */ new Set(), - fetched: false - }); - } - function bump(name) { - const r = get(name); - r.ver++; - for (const fn of r.subs) fn(); - } - return { - entries, - get, - bump, - bumpAll() { - for (const n in entries) bump(n); - } - }; - } - - // src/runtime.ts - var COMPONENT_DIR = "."; - function createRuntime(doc = document) { - const registry = createRegistry(); - const pseudoClass = createPseudoSheet(doc); - const helmet = createHelmetManager( - doc, - (name) => registry.get(name).htmlStreaming - ); - const external = createExternalModules(() => registry.bumpAll()); - const factory = createComponentFactory(registry, ensureFetched); - const host = { - component: (name) => factory.getDC(name), - placeholder: (props) => h(Placeholder, props), - helmet: (node) => helmet.compile(node), - loadExternal: (kind, url, after) => external.load(kind, url, after), - resolveExternal: (url, name) => external.resolve(url, name), - resolveExternalGlobal: (url, name) => external.resolveGlobal(url, name), - resolveExternalError: (url, name) => external.getError(url, name), - pseudoClass - }; - function ensureFetched(name) { - const r = registry.get(name); - if (r.fetched) return; - r.fetched = true; - const url = COMPONENT_DIR + "/" + encodeURIComponent(name) + ".dc.html"; - const res = window.__resources; - const pre = res ? res[url] : void 0; - const target = typeof pre === "string" && pre ? pre : url; - const blob = bundledBlob(target); - (blob ? blob.text() : fetch(target).then((res2) => { - if (!res2.ok) { - console.error( - '[dc-runtime] sibling fetch for "' + name + '" failed:', - url, - "returned", - res2.status, - "\u2014 the reference renders as an empty placeholder." - ); - return ""; - } - return res2.text(); - })).then((t) => { - if (!t) return; - const parsed = parseDcText(t); - if (!parsed) { - console.error( - '[dc-runtime] sibling fetch for "' + name + '":', - url, - "has no block \u2014 not a Design Component." - ); - return; - } - if (parsed.props) r.propsMeta = parsed.props; - if (parsed.preview) r.preview = parsed.preview; - if (parsed.template && !r.html) updateHtml(name, parsed.template); - if (parsed.js && !r.Logic) updateJs(name, parsed.js); - }).catch( - (e) => console.error( - '[dc-runtime] sibling fetch for "' + name + '" threw:', - url, - e - ) - ); - } - let rootName = null; - function updateHtml(name, html) { - const r = registry.get(name); - r.html = html; - if (name === rootName) { - const mode = DESIGN_DOC_MODE_RE.exec(html)?.[1] ?? null; - if (mode || !r.htmlStreaming) helmet.setDesignDocMode(mode); - } - try { - r.tpl = compileTemplate(html, host); - } catch (e) { - console.error("[dc-runtime] template compile FAILED for", name, e); - } - registry.bump(name); - } - function updateJs(name, src) { - const r = registry.get(name); - const seq = r.jsSeq = (r.jsSeq || 0) + 1; - try { - const Cls = evalDcLogic(src); - if (r.jsSeq !== seq) return; - if (typeof Cls !== "function") { - r.logicError = name + ".dc.html: - - - - - - - - -
-
-

Chatbolls · Агенты

- design baseline · OWNER = ADMIN · SPEC-HUB-0031 §4.3, §8.3; ADR-0017, ADR-0023, ADR-0041 -
-

Агент — главный объект настройки: один экран с именем и статусом, группой, инструкциями из трёх частей, моделью, знаниями и подключениями. Изменения применяются сразу, без публикации (ADR-0023). Создание — мастер одного шага: имя и группа. Код вставки виджета копируется в один клик прямо в блоке подключений.

- -
- -
-
- {{ f.badge }} - {{ f.title }} - {{ f.note }} -
- -
- - -
- - -
-
-

Агенты

Агент отвечает клиентам первым и собирает диалоги в группу

- -
- -
-
- {{ h.label }} -
- -
-
{{ a.name }}{{ a.code }}
- {{ a.group }} - {{ a.status }} - {{ a.model }} -
—
-
{{ a.open }}
- -
-
-
-
- -
- -
Агент отвечает клиентам первым. Создайте первого

Имя и группа — всё, что нужно для старта. Инструкции, знания и подключения настраиваются потом на карточке.

- -
-
-
- - - -
-
-

Создать агента

-

Один шаг. Инструкции, модель, знания и подключения — потом, на карточке агента.

-
- - -
-
- - -
-
-
-
-
- - - -
- -
- -
-

{{ f.c.name }}

{{ f.c.status }}
-

{{ f.c.group }}{{ f.c.code }}{{ f.c.openLine }}создан {{ f.c.created }}

-
-
- - -
-
- - -
- -
AI не может отвечать: провайдер не выбранДобавьте ключ в «Настройки → AI-провайдер» и выберите провайдера в блоке «Модель». До этого диалоги агента ждут человека.
- -
-
- -
-
- -
-

Инструкции

Системный промпт собирается из трёх частей в этом порядке
-
- - - -
- -
Есть несохранённые изменения — применятся в runtime сразу
-
-
- -
-

Знания

{{ f.c.knowLine }}
- -
- -
{{ k.title }}{{ k.chip }}{{ k.meta }}
-
-
-
-

Знания не прикреплены — агент отвечает только по инструкциям. Выберите статьи из библиотеки или портала поддержки.

-
-
- -
- -
-

Назначение

-

Группа определяет, кто из сотрудников видит диалоги агента; без группы — видны всем.

-
- - -
-
- -
-

Модель

-

Ключ провайдера — в «Настройки → AI-провайдер». Модель выбирается из каталога провайдера.

-
- - -
Лимит в день{{ f.c.limit }}USD
-
-
- -
-

Подключения

-

Через них клиенты попадают к агенту. Одно подключение — один агент.

- -
- -
{{ c.name }}{{ c.sub }}
- {{ c.status }} - -
-
- -
-
Код вставки виджета
- <script src="https://chat.atelie-nord.ru/widget.js" data-key="wk_9f3a12c4"></script> - Вставьте перед </body> на сайте -
-
-
-
-
- -
- - - Удалить можно только без диалогов и подключений -
-
-
-
-
-
-
-
-
-
- - - diff --git a/design/baseline/Chatballs Baseline v2/Админ Baseline.dc.html b/design/baseline/Chatballs Baseline v2/Админ Baseline.dc.html deleted file mode 100644 index 2428763..0000000 --- a/design/baseline/Chatballs Baseline v2/Админ Baseline.dc.html +++ /dev/null @@ -1,276 +0,0 @@ - - - - - - - - - - - - - - -
-
-

Chatbolls · Владелец / Админ

- design baseline · роли OWNER = ADMIN · SPEC-HUB-0031 §3–6, ADR-0043 -
-

Та же оболочка, что у сотрудника, плюс навигация: ровно 6 пунктов плоским списком, у «Чат» — бейдж очереди. Дерево «Все диалоги · Группы · Агенты» из сайдбара сотрудника у админа переезжает в заголовок списка как переключатель охвата — сайдбар остаётся плоским. Блок «Запуск» показывается до выполнения трёх шагов. Владелец от админа отличается только бейджем роли.

- -
- -
-
- {{ f.badge }} - {{ f.title }} - {{ f.note }} -
- -
- - - - - -
-
-
- -
- - -
-
-
- - Поиск по контакту, сообщению… - / -
-
- - - -
- -
{{ g.head }}
-
- - - -
-
-
- -
- - - -
- - - -
-

Вы видите все диалоги организации. Сотрудники видят только диалоги своих групп, без группы и те, где они ответственные.

- -
-
- -
- - - -
-
- - -
-
-
- МС -
- Мария Соколова -

AI отвечаетTelegramКонсультантОператоры

-
-
-
- - - -
-
- -
-
-
Сегодня
- -
-
-
- {{ m.actor }} -
{{ m.text }}
- {{ m.time }} -
-
-
-
-
- -
-
-

AI ведёт диалог. Ваше сообщение перехватит его — AI перестанет отвечать

-
- - Написать сообщение… - - -
-
-
-
-
-
-
-
-
-
- - - diff --git a/design/baseline/Chatballs Baseline v2/База знаний Baseline.dc.html b/design/baseline/Chatballs Baseline v2/База знаний Baseline.dc.html deleted file mode 100644 index 1afd176..0000000 --- a/design/baseline/Chatballs Baseline v2/База знаний Baseline.dc.html +++ /dev/null @@ -1,879 +0,0 @@ - - - - - - - - - - - - - - -
-
-

Chatbolls · База знаний

- design baseline · OWNER = ADMIN · SPEC-HUB-0026 §9, SPEC-HUB-0029 §7 -
-

Внутренняя база знаний агентов сверстана как карточка портала: дерево категорий слева, таблица материалов справа, шапка с состоянием индекса. Редактор знания — тот же полноэкранный split, что и у статьи портала. Отделы упразднены (ADR-0041), поэтому область доступности заменена состоянием «в ответах агента» и явным списком агентов.

- -
- -
-
- {{ f.id }} - {{ f.title }} - {{ f.note }} -
- -
- - - - - -
- -
-
-
-
-

База знаний

- Индекс актуален - 9 категорий · 84 знания · 1 240 фрагментов -
-

Материалы для AI-агентов: регламенты, прайсы, скрипты. Агент отвечает только по включённым знаниям, которые к нему прикреплены. Клиент их не видит — публичное живёт в порталах.

-
-
- - - -
-
-
- -
-
-
КАТЕГОРИИ
- -
- -
-
- -
-
-
- - - - - {{ f.scopeNote }} -
- - -
-
- -

В категории «Обучение» пока нет знаний

-

Добавьте материал вручную или загрузите пачкой из YAML. Пустую категорию без вложенных можно удалить в «Управлять».

-
- - -
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
ЗНАНИЕКАТЕГОРИЯФРАГМЕНТЫАГЕНТЫВ ОТВЕТАХОБНОВЛЕНО
- - - - {{ k.title }} - {{ k.desc }} - - {{ k.attach }} - - {{ k.cat }}{{ k.catParent }}{{ k.frags }}{{ k.fragNote }}{{ k.agents }}не прикреплено{{ k.status }}{{ k.updated }}{{ k.author }} - - -
- -
- - - -
-
-
-
-
- 84 знания · показаны 7 · агент отвечает только по включённым и прикреплённым - - 1 - - - - -
-
-
-
-
- - -
- Выбрано 3знания - - - - - -
-
- - -
-
-
-
-

Прикрепить к агенту

Выбрано 3 знания. Один агент за операцию — так видно, что именно изменилось.

- -
- - - - - -
-
- АГЕНТ -
- - - -
-
-
- - 2 из 3 будут пропущены: «Прайс-лист 2026» уже прикреплён к этому агенту, «Скрипт: недовольная посадка» выключен в ответах. Состав знаний агента не изменится по этим двум. -
-
- Операция атомарна: при недоступном идентификаторе не меняется ничего. Требуется право ai.manage. - - -
-
-
-
-
-
- - - -
-
-
-
- /Регламенты/Пошив -
-
-
-
-

Срок пошива и переносы примерок

- В ответах агента -
-
- - - 12 фрагментов · переиндексировано сегодня, 11:24 - - - - 3 агента - - обновила Анна Ким · 5 сентября, 11:20 -
-
-
- - - -
-
-
-
- -
-
-
-

Как агент отвечает на вопросы о сроках: базовые сроки по типам изделий, правила переноса примерки и что делать при задержке мастера.

-

Базовые сроки

- - - - - - - -
ИзделиеСрокПримерок
Рубашка10 рабочих дней1
Пальто25 рабочих дней2
Костюм-тройка35 рабочих дней3
-

Перенос примерки

-
    -
  1. Клиент предупредил за сутки — переносим бесплатно, срок сдвигается на те же дни.
  2. -
  3. Меньше суток или не пришёл — предлагаем ближайшее окно, срок пересчитывает мастер.
  4. -
  5. Третий перенос подряд — заказ ставится на паузу до звонка администратора.
  6. -
-
Агент не называет точную дату сдачи — только диапазон из таблицы и ближайшее окно примерки. Дату подтверждает мастер.
-

Если срок сорван

-
    -
  • Извиняемся и называем новый срок одной фразой, без объяснения внутренних причин.
  • -
  • Задержка больше 5 дней — скидка 10 % и передача администратору.
  • -
-
-
- -
-
-
КАТЕГОРИЯРегламенты / Пошив
-
- Участвует в ответахвыключенное знание не попадает в retrieval - -
-
- -
-
ВЛОЖЕНИЯссылки уходят в ответ агента
-
- -
- - {{ file.name }}{{ file.meta }} - -
-
-
-
- -
-
АГЕНТЫ
-
- -
- - {{ a.name }}{{ a.meta }} - {{ a.tag }} -
-
-
-
- -
- ИНДЕКСАЦИЯ -
-
12фрагментов · 4 280 знаков
-

Фрагменты пересобираются при сохранении. Пока идёт пересборка, агент отвечает по прежней версии.

- -
-
-
-
-
-
- - - -
-
- - Регламенты / Пошив - - {{ f.indexLabel }} - - - - - - - -
- -
-
-
- -
Заголовок цитируется агентом в ответе клиенту — пишите его как ответ, не как папку.
-
-
- - - - - - - - GFM -
-
-
{{ mdSource }}
- -
- - Отпустите, чтобы приложить к знанию - Файл станет вложением и ссылкой в тексте — агент отдаёт её клиенту. PNG, JPG, PDF, DOCX, XLSX · до 25 МБ -
-
-
-
- 4 280 знаков · ≈12 фрагментовСтрока 22, столбец 18{{ f.saveNote }} -
-
- -
-
- ПРЕДПРОСМОТР · ВНУТРЕННИЙ МАТЕРИАЛ - - -
-
-
-

Срок пошива и переносы примерок

-

Базовые сроки по типам изделий, правила переноса примерки и что делать при задержке.

-

Базовые сроки

- - - - - - -
ИзделиеСрокПримерок
Рубашка10 рабочих дней1
Пальто25 рабочих дней2
-

Перенос примерки

-
    -
  1. Предупредил за сутки — переносим бесплатно.
  2. -
  3. Меньше суток — ближайшее свободное окно.
  4. -
  5. Третий перенос — пауза до звонка администратора.
  6. -
-
Агент не называет точную дату сдачи — только диапазон и ближайшее окно примерки.
-
- - Регламент сроков 2026.pdfвложение · ссылку агент отдаёт клиенту -
-
-
-
- -
-
- - -
- Участвует в ответахможно выключить, не удаляя - -
-
- -
-
ВЛОЖЕНИЯ
-
- -

Перетащите файлы

-

или выберите на диске · до 25 МБ

-
-
- -
- - {{ file.name }}{{ file.meta }} - - - - - - -
-
-
-
- -
-
АГЕНТЫменяется массовым действием
-
- -
- - {{ a.name }}{{ a.meta }} -
-
-
-

Прикрепление и открепление — только через «Прикрепить к агенту»: операция атомарна и пишется в журнал.

-
-
-
-
-
- - - -
-
-
/Категории
-

Категории

-

Дерево до трёх уровней. Перетащите строку, чтобы сменить родителя или порядок.

-
-
-
-
- -
- - - - - - - - - {{ c.name }} - {{ c.tag }} - - {{ c.count }} - - - - - - -
-
-
- -
-
-
-
- Правила -
    -
  • Имя уникально внутри родителя.
  • -
  • Категорию нельзя перенести внутрь себя.
  • -
  • Удаление — только для пустой категории без вложенных.
  • -
  • «Без категории» системная: не переименовывается и не удаляется.
  • -
-
-
- - «Прайсы и оплата» удалить нельзя: 12 знаний и 2 вложенные категории. Сначала перенесите содержимое. -
-
-
-
-
-
- - - -
-
-
/Импорт YAML
-

Импорт материалов

-

Файл разбирается до применения: видно, что создастся, что обновится и где ошибка.

-
-
-
-
- - knowledge-atelie-2026.yaml42 КБ · 7 документов · разобран без ошибок формата - -
- -
-
- Что произойдёт - - - {{ s.label }} - - -
- - - - - - - - - - - - - - - - - -
ДОКУМЕНТCATEGORYPATHДЕЙСТВИЕЗАМЕЧАНИЕ
{{ d.title }}{{ d.path }}{{ d.action }}{{ d.note }}
-
- -
- Недостающие категории из файла создаются автоматически. Без указания пути новый документ попадает в «Без категории», существующий сохраняет свою категорию. - - -
-
-
-
-
- -
-
-
-
-
-
- - - diff --git a/design/baseline/Chatballs Baseline v2/Контакты Baseline.dc.html b/design/baseline/Chatballs Baseline v2/Контакты Baseline.dc.html deleted file mode 100644 index 58dbe2a..0000000 --- a/design/baseline/Chatballs Baseline v2/Контакты Baseline.dc.html +++ /dev/null @@ -1,326 +0,0 @@ - - - - - - - - - - - - - - -
-
-

Chatbolls · Контакты

- design baseline · OWNER = ADMIN · SPEC-HUB-0031 §8.2, ADR-0006, ADR-0041 -
-

Одна плотная таблица без коммерции: контакт · как связаться · каналы · последний диалог · открытые. Колонка «Продукты», фильтр по продуктам и статусы «лид/клиент» из текущего кода убраны (ADR-0041). Карточка контакта — шапка с теми же полями, что в контекст-панели чата, и `UnderlineTabs`: Обзор · Диалоги · Идентификаторы · Аудит. Вкладка «Согласия» убрана — журнал согласий не ведётся отдельной сущностью.

- -
- -
-
- {{ f.badge }} - {{ f.title }} - {{ f.note }} -
- -
- - -
- - -
-
-

Контакты

{{ f.countLine }}
- -
- - -
-
Имя, email, телефон или логин…/
- - - -
- -
- -
-
- Контакт - Как связаться - Каналы - - Открытые - -
- -
-
{{ r.initials }}{{ r.name }}{{ r.cid }}
-
{{ r.c1 }}{{ r.c2 }}
-
-
{{ r.lastWho }}{{ r.lastWhen }}
-
{{ r.open }}
- -
-
-
- {{ f.pageLine }} -
- - - -
-
-
-
- - -
- -
Контакты появятся, когда клиенты напишут вашему агенту

Каждый, кто написал через Telegram, MAX, почту или виджет, становится контактом с историей диалогов.

- -
-
-
-
- - - -
- -
- МС -
-

Мария Соколова

C-1042
-

Постоянная клиентка, заказывает пошив 2–3 раза в год. Предпочитает Telegram.

-
- {{ m.value }} - {{ c.full }} -
-
-
- - -
-
- -
- -
- - - -
-
-
-
{{ s.label }}{{ s.value }}
-
-
-

Последняя активность

- -
- -
{{ a.title }}
-
-
-
-
-
-
-

Текущий диалог

AI отвечает
-

«Заказ 4471 — когда доставка?»

-

КонсультантTelegramОператоры17:10

- -
-
-

Ответственный

-
Анна Кимпо текущему диалогу
-
-
-

Заметка

Анна Ким · 2 сен
-

Звонить после 18:00. Доставку всегда просит на утро.

-
-
-
-
- - - -
-
- -
- -
{{ i.name }}{{ i.value }}
- {{ i.since }} - {{ i.status }} -
-
-

Идентичности разных подключений не объединяются автоматически (ADR-0006). Подтверждённой считается только та, что подключение отдало с проверенным телефоном.

-
-
-

Возможный дубликат

предложение
-

Совпадает подтверждённый телефон. Объединение перенесёт идентичности и диалоги; операция аудируется и требует причины.

-
Г
Гость · 8f3a12Web-виджет · 1 диалог · +7 916 ••• 44-10
-
-
-
-
- - - -
- - - -
-
-
-
-
-
-
-
-
-
-
- - - diff --git a/design/baseline/Chatballs Baseline v2/Настройки Baseline.dc.html b/design/baseline/Chatballs Baseline v2/Настройки Baseline.dc.html deleted file mode 100644 index db2c2c4..0000000 --- a/design/baseline/Chatballs Baseline v2/Настройки Baseline.dc.html +++ /dev/null @@ -1,369 +0,0 @@ - - - - - - - - - - - - - - -
-
-

Chatbolls · Настройки

- design baseline · OWNER = ADMIN · SPEC-HUB-0031 §8.6 (уточнено), ADR-0043 -
-

Вместо сплошной ленты из девяти секций — субменю разделов слева и один раздел на экране. Порядок: Организация · Группы · AI-провайдер · Интеграции · Голосовые и звонки · Хранилище файлов · Демо-данные; ниже — переходы «База знаний» и «Аудит действий». Профиль пользователя (тема, акцент, безопасность) в настройки не входит — это отдельная страница из меню пользователя. Владелец и админ видят одно и то же.

- -
- -
-
- {{ f.badge }} - {{ f.title }} - {{ f.note }} -
- - - -
- - - - -
-

Настройки

- -
- - -
-
-
-

{{ f.heading }}

{{ f.lead }}

- -
- - - -
-
- АН -
ЛоготипPNG или SVG, квадрат от 128 px. Виден в сайдбаре, виджете и письмах.
-
- - -
-
-
- - - -
-
- Сохранено 2 сен, 14:12 - -
-
-
- - - -
- -
- - {{ g.name }} - {{ g.members }} - {{ g.dialogs }} -
- - -
-
-
-
- Название новой группы - -
-
-

Состав группы редактируется на карточке сотрудника. Диалоги без группы видят все сотрудники.

-
- - - -
-
- {{ h }} -
- -
-
- - {{ r.name }}{{ r.sub }} -
-
{{ r.c2 }}{{ r.c2 }}{{ r.c2 }}
-
{{ r.c3 }}{{ r.c3sub }}
-
{{ r.status }}{{ r.checked }}
-
- - -
-
-
-
-

{{ f.tableNote }}

-
- - - -
-
- Точка входа - Голосовые - Аудиозвонки - Видеозвонки -
- -
-
{{ c.name }}{{ c.sub }}
- -
- недоступно - -
-
-
-
-
-

Голосовые, присланные клиентом из мессенджера, принимаются всегда. Почта звонки не поддерживает. Изменения применяются сразу.

-
- - - -
-
-
Где хранить файлыВложения знаний, голосовые, фото и логотипы
-
- - -
-
-

Новые файлы будут записываться в бакет под префиксами организаций. Уже загруженные остаются доступны с диска, пока не перенесены.

-
- - - -
-
Доступ проверен 5 сен, 09:41 · перенос завершён: 1 284 файла
-
- - - -
-
-
- - - -
-
-
Ателье «Норд»Установлены3 412 записей · 1 сен
- -
-

Сотрудники и группы, агенты с подключениями, знания, диалоги во всех состояниях, голосовые, шаблоны, портал поддержки, звонки и уведомления. Удаляются целиком — ваши данные не затрагиваются.

-
Войдите под демо-сотрудником в приватном окне, чтобы увидеть систему его глазами: -
- -
{{ a.name }}{{ a.role }}
- {{ a.email }} - {{ a.pass }} -
-
-
-
-
-
-
-
-
- - - -
-
-

Настройки

-
- - - -
Отдельные экраны
- - - -
-
-
-

Организация

-
-
-
АН
Логотип
- - - -
-
-
-
-
-
-
-
-
-
-
- - - diff --git a/design/baseline/Chatballs Baseline v2/Порталы Baseline.dc.html b/design/baseline/Chatballs Baseline v2/Порталы Baseline.dc.html deleted file mode 100644 index 4e07960..0000000 --- a/design/baseline/Chatballs Baseline v2/Порталы Baseline.dc.html +++ /dev/null @@ -1,764 +0,0 @@ - - - - - - - - - - - - - - -
- - - - diff --git a/design/baseline/Chatballs Baseline v2/Профиль Baseline.dc.html b/design/baseline/Chatballs Baseline v2/Профиль Baseline.dc.html deleted file mode 100644 index 46f1fcc..0000000 --- a/design/baseline/Chatballs Baseline v2/Профиль Baseline.dc.html +++ /dev/null @@ -1,279 +0,0 @@ - - - - - - - - - - - - - - -
-
-

Chatbolls · Профиль пользователя

- design baseline · все роли · SPEC-HUB-0031 §3, §7; ADR-0015, ADR-0029 -
-

Отдельная страница из меню пользователя (аватар в сайдбаре → «Профиль»), не раздел настроек. Слева — кто вы и как выглядит ваш интерфейс: личные данные с фото, тема и акцент, уведомления в мессенджер. Справа — безопасность: пароль, двухфакторная аутентификация, сессии. Всё здесь принадлежит учётной записи, а не организации (ADR-0029): владелец не меняет это у других.

- -
- -
-
- {{ f.badge }} - {{ f.title }} - {{ f.note }} -
- - -
- - - -
-
- -
- -
-

{{ f.name }}

{{ f.role }}
-

{{ f.email }} · в организации с {{ f.since }} · группы: {{ f.groups }}

-
- -
- -
- -
- -
-

Личные данные

-
- -
ФотоPNG, JPEG или WebP · до 2 МБ. Видно коллегам в чате, подписях сообщений и выборе ответственного.
-
- - -
-
-
- - -
-
-
- - -
-

Внешний вид

-

Настройка личная и хранится в учётной записи: применяется на всех ваших устройствах, без перезагрузки.

-
- Тема -
- -
-
-
- Акцентный цвет -
- - - #1677ff -
-
-
- - -
-

Уведомления в мессенджер

-

Рабочие события приходят и при закрытом браузере. Один мессенджер: привязка нового заменяет текущий.

- -
-
- -
{{ b.name }}{{ b.sub }}
- Подключено - Привязать бота -
- -
- -
-
-
-
-

Нажмите «Привязать бота» и в открывшемся чате — «Начать»: привязка подтвердится сама.

-
-
- - -
-
-

Смена пароля

-
- - - -
-
-
- -
-
-

Двухфакторная аутентификация

{{ f.totpText }}

- -
-
{{ f.totpNote }}
-
- -
-

Активные сессии

- -
- -
{{ s.device }}{{ s.where }}
- текущая - {{ s.time }} -
-
-
-
-
-
-
-
- - - - -
-

Профиль

-
-
Дмитрий ОрловСотрудник
d.orlov@atelie-nord.ru
-
-

Личные данные

- - - -
-
-

Внешний вид

-
-
Акцентный цвет
-
-
- - - - -
-
-
-
-
- -
-
- - - - diff --git a/design/baseline/Chatballs Baseline v2/Сотрудники Baseline.dc.html b/design/baseline/Chatballs Baseline v2/Сотрудники Baseline.dc.html deleted file mode 100644 index 63f9a2d..0000000 --- a/design/baseline/Chatballs Baseline v2/Сотрудники Baseline.dc.html +++ /dev/null @@ -1,433 +0,0 @@ - - - - - - - - - - - - - - -
-
-

Chatbolls · Сотрудники

- design baseline · OWNER = ADMIN · SPEC-HUB-0031 §3, §8.4; ADR-0027, ADR-0029, ADR-0043 -
-

Таблица: человек · роль · должность · группы · доступ · статус · последний вход. Роль решает права: владелец и админ идентичны, сотрудник работает только в чате; группа задаёт лишь видимость диалогов. Роль владельца меняется единственным способом — передачей владения. Сайдбар приведён к коду: знак Chatballs (два круга с перемычкой), пункты Чат · Контакты · Агенты · Сотрудники · Порталы · Настройки, ручка изменения ширины на правом крае (180–320px), блок «Запуск», меню пользователя.

- -
- -
-
- {{ f.badge }} - {{ f.title }} - {{ f.note }} -
- -
- - - - - - - - - - - -
- - -
-
-

Сотрудники

Роли и группы · показано {{ f.shown }} из {{ f.total }}

- -
- -
-
Поиск по имени, email, должности…
-
Роль
-
Группа{{ f.groupFilter }}
-
- -
- -
-
- {{ h }} -
- -
-
{{ r.initials }}{{ r.name }}{{ r.email }}
- {{ r.role }} - {{ r.position }} -
{{ g.name }}Без группы
- {{ r.access }} - {{ r.status }} - {{ r.login }} - -
-
-
- Показано {{ f.shown }} из {{ f.total }} - Группа задаёт только видимость диалогов и не выдаёт прав -
-
-
- - -
- -
- - - -
-
-
-
- - - -
- -
- {{ f.p.initials }} -
-

{{ f.p.name }}

{{ f.p.role }}{{ f.p.status }}
-

{{ f.p.email }} · {{ f.p.position }} · {{ f.p.groupLine }}

-
-
- Только просмотр - -
-
- - -
Владельца нельзя удалить, заблокировать или сменить ему роль — единственный путь изменения роли владельца это передача владения.
-
- -
-
-
-

Основные данные

-
- - - -
-
-
-

Должность и группы

-

Группа задаёт только видимость диалогов и не выдаёт прав.

- -
- - - -
-
-
-

Системная роль

-

{{ f.p.roleNote }}

-
- - - -
-
-
-

Статус и безопасность

-
ПарольПоследняя смена: {{ f.p.pwd }} · новый можно отправить письмом или показать и скопировать
-
Двухфакторная аутентификация (TOTP){{ f.p.totpText }}
-
Активные сессии{{ f.p.sessions }}
-
- -
-

Опасная зона

- -
- {{ d.title }} -

{{ d.text }}

- - {{ d.note }} -
-
-
-
-
- -
-
-

Учётная запись

- -
{{ a.label }}{{ a.value }}
-
-
-
-

Аудит

- -
{{ a.text }}{{ a.code }} · {{ a.time }}
-
-
-
-
-
-
- - - -
- -
- - - -
-
-

{{ f.pwd.title }}

{{ f.pwd.sub }}

-
-
{{ f.pwd.initials }}{{ f.pwd.name }}{{ f.pwd.email }}
- -

Пароль показывается один раз и на сервере не хранится в открытом виде. Скопируйте и передайте сотруднику лично — при первом входе он сменит его сам. Если окно закрыть, останется только сбросить пароль заново.

- -
-
Действие записано в аудит
-
-
-
- - - -
-
-

Передача владения

Атомарная операция · в организации остаётся ровно один владелец

-
- -
-
Елена КузнецоваВладелец
- -
Анна Ким→ Владелец
-
-
Новая роль прежнего владельца
-

Новый владелец получит полный доступ. Вы потеряете права владельца, включая передачу владения и управление администраторами. Операция аудируется и необратима без повторной передачи.

- -
-
-
-
-
-
-
-
-
-
-
-
- - - diff --git a/design/baseline/Chatballs Baseline v2/Чат сотрудника Baseline.dc.html b/design/baseline/Chatballs Baseline v2/Чат сотрудника Baseline.dc.html deleted file mode 100644 index 41f208b..0000000 --- a/design/baseline/Chatballs Baseline v2/Чат сотрудника Baseline.dc.html +++ /dev/null @@ -1,673 +0,0 @@ - - - - - - - - - - - - - - - - - - -
-
-

Chatbolls · Чат сотрудника

- design baseline v2 · роль EMPLOYEE · единственный экран -
-

Сотрудник открывается сразу в чат. Слева сайдбар с деревом «Диалоги»: Все диалоги · Группы · Агенты — это и есть фильтр списка. Статусы — вкладки над списком. Три рабочие колонки: список · лента · контекст (Контакт | История). Композер активен всегда — первое отправленное сообщение перехватывает диалог.

- -
- -
-
- {{ f.badge }} - {{ f.title }} - {{ f.note }} -
- - -
- - - - - - - - - - -
-
-
-

{{ f.side.scope }}

{{ f.total }}
-
- - -
-
-
- - Поиск по контакту, сообщению… - / -
-
-
- - - -
-
- - - - -
-
-
Диалоги появятся, когда клиенты напишут вашему агенту
-
-
-
-
- - -
- -
Выберите диалог
-
- -
-
- {{ f.sel.initials }} -
- {{ f.sel.name }} -

{{ f.sel.stLabel }}· {{ f.sel.waitShort }}{{ f.sel.chLabel }}{{ f.sel.agent }}

-
-
-
- - - - -
- -
- - -
- - -
-
-
- -
-
-
{{ f.day }}
- - -
{{ m.text }} · {{ m.time }}
-
- -
-
{{ m.initials }}
-
- {{ m.actor }} -
-
- - - - - {{ m.dur }} -
- -

{{ m.transcript }}

-
-
- {{ m.time }} -
-
-
- -
-
{{ m.initials }}
-
- {{ m.actor }} -
{{ m.text }}
- {{ m.time }} -
-
-
-
-
-
- - -
-
- - -
{{ f.composer.hint }}
-
-
-
{{ f.composer.placeholder }}
-
- - - - - - -
-
-
- -
Идёт запись. Esc — отменить, Enter — отправить
-
- - {{ f.composer.recTime }} - - - -
-
- -
- -
{{ f.composer.lockedTitle }}

{{ f.composer.lockedText }}

- -
-
-
-
-
- - -
-
-
- - - - - -
-
-
- - - -
-
M1Мобильный · список390 · дерево «Диалоги» — в выезжающем меню
-
-
- - - -
-
-
Поиск
-
-
- - - -
-
- -
- {{ d.initials }} - - {{ d.agent }} - {{ d.name }}{{ d.time }} - {{ d.preview }}{{ d.unread }} - - - {{ d.waitShort }} - {{ d.group }} - {{ l.text }} - - - -
-
-
-
-
- - -
-
M2Мобильный · диалогконтакт — по кнопке в шапке
-
-
- - {{ md.initials }} -
{{ md.name }}{{ md.stLabel }}
- - -
-
-
Сегодня
- -
{{ m.text }} · {{ m.time }}
- -
-
{{ m.initials }}
-
- {{ m.actor }} -
{{ m.text }}
- {{ m.time }} -
-
-
-
-
-
-
Отправка сообщения возьмёт диалог на вас
-
- Сообщение… - -
-
-
-
-
-
-
-
- - - diff --git a/design/baseline/Знания/Knowledge Library Baseline.dc.html b/design/baseline/Знания/Knowledge Library Baseline.dc.html deleted file mode 100644 index 6a43401..0000000 --- a/design/baseline/Знания/Knowledge Library Baseline.dc.html +++ /dev/null @@ -1,667 +0,0 @@ - - - - - - - - - - - - - - - - - - - - -
- - - - - -
-
- A -

Библиотека знаний — основной вид

- дерево категорий · поиск и фильтры · таблица с областью доступности -
- -
- - - - -
-
-
- ООО «Эдевс»/ - AI/ - Знания -
-
-
3
-
-
- -
- -
-
-

Знания

-

Иерархическая библиотека · категория задаёт размещение, область доступности — какие отделы могут использовать знание

-
-
- - -
-
- - -
- - -
-
- КАТЕГОРИИ - -
-
- -
- - - Все знания - 55 -
- -
- - Без категории - СИСТ. - 4 -
- - -
- - Продукты - 14 -
- -
- - FoxRay - - 8 -
-
Лицензирование3
-
Клинические кейсы5
- -
- - FirePage - - 6 -
- - -
- - Продажи - - 9 -
- -
- - Поддержка - - 14 -
- -
- - Общие - 5 -
-
-
- - -
- -
-
- - Поиск по заголовку и описанию -
-
Отдел: Все
-
Доступность: Все
-
Статус: Все
-
- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ЗНАНИЕКАТЕГОРИЯДОСТУПНОСТЬВЛОЖ.АГЕНТЫСТАТУСОБНОВЛЕНО
FoxRay — правила лицензирования
Условия лицензии, продление, ограничения на клинику
FoxRay / ЛицензированиеПродажиПоддержка23Активно14 июл
FoxRay — интерпретация ТРГ
Разбор частых артефактов и трассировки
FoxRay / Клинические кейсыПоддержка12Активно12 июл
FirePage — тарифы и лицензия
Стоимость разовой лицензии, что входит
FirePage / ТарифыОрганизация—4Активно15 июл
FirePage — установка и настройка CMS
Пошаговая инструкция по развёртыванию
FirePage / CMS и шаблоныПоддержка31Активно10 июл
Скрипт первичного контакта
Как начать диалог и квалифицировать запрос
Продажи / СкриптыПродажи—2Активно13 июл
Работа с возражением «дорого»
Черновик · не используется агентами
Продажи / СкриптыПродажи1—Выключено09 июл
Возврат и гарантия
Общая политика для всех продуктов
ОбщиеОрганизация—6Активно15 июл
Реквизиты и договор оферты
ИНН, ОГРН, публичная оферта
Без категорииОрганизация15Активно08 июл
-
- 8 из 55 знаний - Изменения знаний применяются к агентам сразу -
-
-
-
-
-
-
-
- - -
-
- B -

Массовый выбор — панель операций

- выбор нескольких знаний · переместить / изменить доступность / заменить отделы (атомарно) -
- -
- -
- - Выбрано 3 знания - - - - - - -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ЗНАНИЕКАТЕГОРИЯДОСТУПНОСТЬСТАТУСОБНОВЛЕНО
FoxRay — правила лицензированияFoxRay / ЛицензированиеПродажиПоддержкаАктивно14 июл
FoxRay — интерпретация ТРГFoxRay / Клинические кейсыПоддержкаАктивно12 июл
FirePage — тарифы и лицензияFirePage / ТарифыОрганизацияАктивно15 июл
Скрипт первичного контактаПродажи / СкриптыПродажиАктивно13 июл
-
-

Операция атомарна: при одной недопустимой записи или конфликтующем агенте ничего не изменяется. Выбор снимается только после успеха.

-
-
- - -
-
- C -

Карточка знания

- категория · область доступности · отделы · содержимое · вложения · агенты -
- -
- -
- Знания/FoxRay / Лицензирование/Правила лицензирования -
- -
- -
-
-
-

Знание

- Активно - обновлено 14 июл -
- -
- - - - -
- -
Область доступности -
- Организация - Отделы -
-
-
- - - - - -
- -
- - - - -
-
- - -
-
-

Вложения

- -
-
-
foxray-pricing.pdf210 КБ · текст в поиске
-
license-terms.docx130 КБ · текст в поиске
-
-
-
- - -
-
-

Агенты, использующие знание

-

Выбор знания хранится у агента явно. Смена области может затронуть этих агентов.

-
-
Продажник FoxRay
Web Chat · Продажи
-
Поддержка FoxRay
Telegram · Поддержка
-
MAX-консультант
MAX · Продажи
-
-
-
-

Сведения

-
-
IDKN-00042
-
Фрагментов в индексе18
-
Создано02 июл 2026
-
АвторИван Петров
-
-
-
-
-
-
- - -
-
- D -

Управление категориями

- создать · переименовать (инлайн) · переместить (перетаскивание) · безопасное удаление -
- -
-
- -
-
-

Управление категориями

Категория определяет только размещение, не доступ

- -
-
- -
- - - Без категории - СИСТ. - -
- -
- - - Продукты - 14 - - - -
- -
- - FoxRay - - 8 -
- -
-
Лицензирование
- - -
- -
- Клинические кейсы - 5 -
- -
- - Продажи - - 9 -
- -
- - Поддержка - - 14 -
- -
- -
- - Общие -
-
-
- - - -
-
-
-
- - -
-
- E -

Конфликт при смене доступности

- операция блокируется · список затронутых агентов · переход к явной правке -
- -
-
-
-
- -
-

Нельзя ограничить доступность до отделов

-

Знание «FoxRay — правила лицензирования» используют агенты вне отделов Продажи и Поддержка. Смена области отключила бы у них это знание. Изменение заблокировано — сначала скорректируйте наборы знаний этих агентов.

-
-
-
-
КОНФЛИКТУЮЩИЕ АГЕНТЫ · 2
-
-
- -
MAX-консультант
MAX · без отдела · AG-0007
- Открыть -
-
- -
Маркетинг-бот
Web Chat · Маркетинг · AG-0011
- Открыть -
-
-
-
- - - -
-
-
-
- - -
-
- S -

Служебные состояния

- пустая категория · ограниченный доступ · выключенное выбранное знание -
-
- -
-
-
В категории пока нет знаний
-

Создайте знание в этой категории или переместите существующие.

- -
- -
-
-
Доступ ограничен отделом
-

Ваш доступ ai.view охватывает отдел «Поддержка». Знания других отделов не показываются и недоступны для изменения.

-
- -
-
ВЫКЛЮЧЕННОЕ ВЫБРАННОЕ ЗНАНИЕ
-
- -
Работа с возражением «дорого»
Продажи / Скрипты
- Выключено -
-

Знание выбрано агентом, но выключено — в runtime не используется. Остаётся видимым в карточке агента с явным статусом.

-
-
-
- -
- - - diff --git a/design/baseline/Знания/README.md b/design/baseline/Знания/README.md deleted file mode 100644 index e28e3a3..0000000 --- a/design/baseline/Знания/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# База знаний AI-агентов — design baseline (DG-01) - -Макет раздела **AI → Знания** (Chatballs / ООО «Эдевс») по SPEC-HUB-0026 / ADR-HUB-0036. - -## Состав - -``` -Knowledge Library Baseline.dc.html — макет (6 кадров: A–E + состояния) -support.js — рантайм Design Component (шаблон + логика) -_ds/chatballs-hub-design-system-…/ — дизайн-система: токены, styles.css, _ds_bundle.js -assets/chatballs-mark.svg — логотип Chatballs -``` - -## Запуск - -Открыть `Knowledge Library Baseline.dc.html` через локальный HTTP-сервер -(из-за загрузки внешних файлов `file://` не подойдёт): - -```bash -python3 -m http.server 8080 -# → http://localhost:8080/Knowledge%20Library%20Baseline.dc.html -``` - -## Кадры - -- **A** — библиотека: дерево категорий, поиск, фильтры (отдел / доступность / статус), таблица. -- **B** — массовый выбор: панель операций (переместить / сменить доступность / заменить отделы). -- **C** — карточка знания: категория, область доступности (Организация / Отделы), вложения, агенты. -- **D** — управление категориями: инлайн-переименование, reorder, безопасное удаление. -- **E** — конфликт при смене scope: блокировка + список затронутых агентов. -- **S** — состояния: пустая категория, ограниченный доступ, выключенное выбранное знание. - -## Технические примечания - -- Вёрстка — инлайн-стили поверх токенов дизайн-системы (`var(--*)` из `_ds/.../tokens`). - Цвета/типографика/отступы менять только через токены. -- Данные в макете — примеры; авторизация и доступность вычисляются на backend - (frontend не источник прав), см. SPEC-HUB-0026 §6. diff --git a/design/baseline/Знания/_ds/design-system/_adherence.oxlintrc.json b/design/baseline/Знания/_ds/design-system/_adherence.oxlintrc.json deleted file mode 100644 index 8419c5c..0000000 --- a/design/baseline/Знания/_ds/design-system/_adherence.oxlintrc.json +++ /dev/null @@ -1,508 +0,0 @@ -{ - "plugins": [ - "react", - "import" - ], - "rules": { - "react/forbid-elements": [ - "warn", - { - "forbid": [] - } - ], - "no-restricted-imports": [ - "warn", - { - "patterns": [ - { - "group": [ - "components/cards/**", - "components/core/**", - "components/inbox/**", - "components/status/**", - "ui_kits/checkout/**", - "ui_kits/internal-hub/**", - "ui_kits/web-chat/**" - ], - "message": "Import design-system components from 'index.js', not component internals." - } - ] - } - ], - "no-restricted-syntax": [ - "warn", - { - "selector": "Literal[value=/#[0-9a-fA-F]{3,8}\\b/]", - "message": "Raw hex color — use a design-system color token via var()." - }, - { - "selector": "Literal[value=/\\b\\d+px\\b/]", - "message": "Raw px value — use a design-system spacing token via var()." - }, - { - "selector": "JSXOpeningElement[name.name='ActorBadge'] > JSXAttribute > JSXIdentifier[name!=/^(?:actor|initials|color|key|ref|className|style|children)$/]", - "message": " doesn't accept that prop. Declared props: actor, initials, color." - }, - { - "selector": "JSXOpeningElement[name.name='ActorBadge'] > JSXAttribute[name.name='actor'] > Literal[value!=/^(?:ai|human)$/]", - "message": " actor must be one of 'ai' | 'human'." - }, - { - "selector": "JSXOpeningElement[name.name='AttentionStatus'] > JSXAttribute > JSXIdentifier[name!=/^(?:level|label|key|ref|className|style|children)$/]", - "message": " doesn't accept that prop. Declared props: level, label." - }, - { - "selector": "JSXOpeningElement[name.name='AttentionStatus'] > JSXAttribute[name.name='level'] > Literal[value!=/^(?:ok|attention|critical)$/]", - "message": " level must be one of 'ok' | 'attention' | 'critical'." - }, - { - "selector": "JSXOpeningElement[name.name='Avatar'] > JSXAttribute > JSXIdentifier[name!=/^(?:initials|color|size|key|ref|className|style|children)$/]", - "message": " doesn't accept that prop. Declared props: initials, color, size." - }, - { - "selector": "JSXOpeningElement[name.name='Button'] > JSXAttribute > JSXIdentifier[name!=/^(?:children|variant|size|disabled|onClick|type|key|ref|className|style|children)$/]", - "message": " - - - - - - - -
- -
-
-
-
-
-

Диалоги0

- - - - -
- -
-
- - - -
-
-
- -

Диалоги появятся, когда клиенты напишут

-
-
-
-
-
Выберите диалог
-
-
-
-
- - -
-
- -
-
-
-
-

{{ secHeading }}

-

{{ secLead }}

-
- -
-
-
{{ secEmpty }}
-
-
-
-
-
-
-
- - -
-
-
- - - - -

Установка готова к настройке

-

Восемь шагов до первого ответа клиенту

-

Покажем по порядку, что и где настроить: ключ модели, агента, знания, точку входа и команду. На каждом шаге — точный путь в интерфейсе и подсветка нужного элемента на экране.

-
-
-
- ≈ 25 минут - Можно прерваться и вернуться -
-
- Свой ключ модели - Или демо-провайдер без ключа -
-
- Данные у вас - Всё на вашем сервере -
-
-
- - -
-
-
-
- - -
-
- -
-
-
- - - - Начало работы -
-
- {{ progressLabel }} - {{ percentLabel }} -
-
-
-
- - - -
-
- -
-
- -
-
- {{ eyebrow }} - -
- -
-

{{ curTitle }}

-

{{ curLead }}

- -
- - - {{ crumb.label }} - - - -
- -
    - -
  1. - {{ item.n }} - {{ item.text }} -
  2. -
    -
- -
- -

{{ curTip }}

-
- -
-
- - - {{ previewCaption }} - - - - - {{ previewChip }} - - - - {{ previewChip }} - -
-
- -
- {{ row.label }} - {{ row.value }} -
-
-
- -
- {{ previewCode }} -
-
-
-
- -
- -
- - - -
-
-
-
-
-
- - -
-
-
- - - -

Всё готово — можно принимать клиентов

-

Агент отвечает по вашим знаниям, сложное передаёт команде. Вернуться к шагам можно в «Настройки → Начало работы».

-
-
- -
- - - - {{ item.label }} -
-
-
-
- - -
-
-
-
- - - - - - -
- -
-
-
-
-
-
-
-
-

{{ tourBadge }}

- {{ tourTitle }} -

{{ tourText }}

- -
-
-
-
- - - - - diff --git a/design/baseline/Очередь и уведомления/README.md b/design/baseline/Очередь и уведомления/README.md deleted file mode 100644 index 1150b38..0000000 --- a/design/baseline/Очередь и уведомления/README.md +++ /dev/null @@ -1,54 +0,0 @@ -# Очередь, ожидание и уведомления — решения по макету - -Файл: `Очередь и уведомления Baseline.dc.html` (тема/акцент — в Tweaks, как у остальных baseline-файлов). Фреймы Q1–Q5 соответствуют пяти пунктам задачи. Тексты — без слов «эскалация», «порог», «SLA». - -## Q1 · Профиль → карточка уведомлений - -- **Заголовок:** «Уведомления о рабочих событиях». -- **Подводка:** «Выберите, где вас звать. Браузер — на этом устройстве, пока приложение открыто или свёрнуто. Мессенджер — где угодно, в том числе при закрытом браузере; мессенджер один: привязка нового заменяет текущий.» Первая фраза честна для обоих способов, ограничение «один мессенджер» отнесено к мессенджеру. -- **Порядок:** браузер **выше** ботов. Он включается одним нажатием и без внешнего аккаунта — это первый разумный шаг; боты ниже как «доставка наружу». -- **Три состояния строки (Q1b):** - 1. *не запрашивали* — серая плитка, текст «Не включены. Браузер спросит разрешение по нажатию», primary «Включить»; - 2. *выдано и включено* — зелёная плашка «Включены», «Отключить», галочки типов событий; - 3. *браузер отказал* — красная плашка «Браузер запретил» + подсказка «Снять запрет можно только в браузере» с шагами (замок в адресной строке → «Уведомления» → «Разрешать» → обновить) и кнопкой «Проверить снова». Кнопки «Включить» в этом состоянии нет — она бы ничего не сделала. -- Под включённой строкой — примечание, что «Отключить» выключает показ, а не разрешение. -- В типах событий добавлен пункт «Диалог назначили на меня» — иначе новый вид ожидания никак не доходит до сотрудника. -- **Дополнено после хендоффа (решение владельца):** первым пунктом вернулся «Новый диалог». В присланном наборе такого события не было, а в продукте оклик о новом диалоге уходит и тогда, когда диалог ведёт агент; называть его «клиент запросил оператора» — неправда. По умолчанию пункт выключен: новый диалог, которым занимается робот, лично никого не касается. Тем, у кого он был включён раньше, он сохранён при обновлении. - -## Q2 · Настройки → «Когда звать на помощь» - -- **Новый раздел субменю** сразу после «Групп» (сроки касаются групп), не внутри «Организации»: там реквизиты организации, а здесь правила работы очереди — смешивать их значит спрятать сроки. -- Название раздела — по сути: «Когда звать на помощь». -- Четыре поля, каждое с пояснением: напомнить группе (5), позвать всех (15), сообщить руководителю (30), сколько ждём назначенного сотрудника (10). Единица «мин» вынесена из поля. -- Футер: дата и автор сохранения, «Вернуть обычные сроки», primary «Сохранить». -- Предупреждение внизу: если у группы не настроен ни один способ доставки, останется только счётчик в списке. - -## Q3 · Чат → два вида ожидания - -Показаны два варианта, **рекомендуем А**: - -- **А · две вкладки.** «Ждут оператора» разделена на «Общая очередь» и «На мне». Разные действия (взять чужое / не потерять своё) — разные списки; счётчик «На мне» становится личной нормой, а не частью общего числа. -- **Б · одна вкладка с метками.** Меньше вкладок, но два вида ожидания перемешаны, и своё приходится выцеплять глазами. - -В обоих вариантах строка получила синюю метку «на вас · вернётся через N мин». - -## Q4 · Строка и карточка диалога - -- **Время ожидания — и там и там.** В строке это уже существующий тёплый бейдж с таймером (без слова «ждёт»), в карточке — новый блок «Ожидание» сверху: «Ждёт человека 7 минут». -- **«Назначен, но не взят» показываем явно** — с аватаром и оставшимся временем: «Назначен на Анну Ким, не взят» + «Если не возьмёт — через 3 минуты диалог вернётся в общую очередь». Это единственное, что объясняет, почему диалог виден, но брать его не нужно. -- Поле «Начат» в карточке заменено на «В очереди с» — момент постановки в очередь информативнее для оператора. -- **Системные события** — обычный вид. Единственное исключение: «не взял — вернулся в очередь» окрашен как предупреждение (тёплая плашка): это не факт из истории, а сорванная договорённость. - -## Q5 · Ответственный → присутствие - -- В списке две группы: «На месте» и «Сейчас нет», внутри — сотрудники с точкой присутствия (зелёная / пустая обводка) и подписью «на месте» / «не в приложении · 25 мин». -- Отсутствующие **не блокируются и не скрываются** — только уходят в конец списка; в футере списка честная оговорка: «„На месте“ — приложение открыто. Признак приблизительный: назначить можно любого». -- Справа у строки — текущая загрузка («4 диалога»): при выборе ответственного это второй по важности признак после присутствия. -- В закрытом поле «Ответственный» присутствие показано той же точкой справа от имени. - -## Что требует бэкенда - -- Разрешение браузера и «желание получать» — два разных флага: `browser_push_enabled` на устройстве/сессии + типы событий, как у ботов. -- Четыре срока организации — в API настроек (сейчас только служебная админка). -- В списке диалогов нужны `assignee`, `assigned_at` и момент постановки в очередь, чтобы считать «вернётся через N мин». -- Признак присутствия сотрудника (последняя активность) в выборе ответственного. diff --git a/design/baseline/Очередь и уведомления/support.js b/design/baseline/Очередь и уведомления/support.js deleted file mode 100644 index cb009b6..0000000 --- a/design/baseline/Очередь и уведомления/support.js +++ /dev/null @@ -1,1911 +0,0 @@ -// GENERATED from dc-runtime/src/*.ts — do not edit. Rebuild with `cd dc-runtime && bun run build`. -"use strict"; -(() => { - var __defProp = Object.defineProperty; - var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; - var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); - - // src/react.ts - function getReact() { - const R = window.React; - if (!R) throw new Error("dc-runtime: window.React is not available yet"); - return R; - } - function getReactDOM() { - const RD = window.ReactDOM; - if (!RD) throw new Error("dc-runtime: window.ReactDOM is not available yet"); - return RD; - } - var h = ((...args) => getReact().createElement( - ...args - )); - - // src/parse.ts - function parseDcDocument(doc) { - const dc = doc.querySelector("x-dc"); - if (!dc) return null; - const scriptEl = doc.querySelector("script[data-dc-script]"); - const { props, preview } = parseDataProps( - scriptEl?.getAttribute("data-props") ?? null - ); - return { - template: dc.innerHTML, - js: scriptEl ? scriptEl.textContent || "" : "", - props, - preview - }; - } - function parseDcText(src) { - const openMatch = /]*)?>/.exec(src); - if (!openMatch) return null; - const close = src.lastIndexOf(""); - if (close === -1 || close < openMatch.index) return null; - const template = src.slice(openMatch.index + openMatch[0].length, close); - const doc = new DOMParser().parseFromString(src, "text/html"); - const scriptEl = doc.querySelector("script[data-dc-script]"); - const { props, preview } = parseDataProps( - scriptEl?.getAttribute("data-props") ?? null - ); - return { - template, - js: scriptEl ? scriptEl.textContent || "" : "", - props, - preview - }; - } - function parseDataProps(raw) { - if (!raw) return { props: null, preview: null }; - let parsed; - try { - parsed = JSON.parse(raw); - } catch { - return { props: null, preview: null }; - } - if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { - return { props: null, preview: null }; - } - const obj = parsed; - const preview = obj.$preview && typeof obj.$preview === "object" ? obj.$preview : null; - const rest = {}; - for (const k of Object.keys(obj)) { - if (k[0] !== "$") rest[k] = obj[k]; - } - return { props: Object.keys(rest).length ? rest : null, preview }; - } - function dcNameFromPath(pathname) { - let p = pathname || ""; - try { - p = decodeURIComponent(p); - } catch { - } - const base = p.split("/").pop() || "Root"; - return base.replace(/\.dc\.html$/, "").replace(/\.html?$/, "") || "Root"; - } - - // src/boot.ts - var BASE_CSS = ` - .sc-placeholder{background:color-mix(in srgb,currentColor 8%,transparent); - border:1px solid color-mix(in srgb,currentColor 50%,transparent); - border-radius:2px;box-sizing:border-box;overflow:hidden} - @keyframes sc-shine{0%{background-position:100% 50%}100%{background-position:0% 50%}} - html.sc-dc-streaming .sc-placeholder, - html.sc-dc-streaming .sc-interp.sc-missing{position:relative; - background:color-mix(in srgb,currentColor 5%,transparent); - border-color:transparent} - html.sc-dc-streaming .sc-placeholder::before, - html.sc-dc-streaming .sc-interp.sc-missing::before{content:''; - position:absolute;inset:0;pointer-events:none; - background:linear-gradient(90deg,rgba(217,119,87,0) 25%,rgba(247,225,211,.95) 37%,rgba(217,119,87,0) 63%); - background-size:400% 100%;animation:sc-shine 1.4s ease infinite} - html.sc-dc-streaming .sc-placeholder:nth-child(n+9 of .sc-placeholder)::before, - html.sc-dc-streaming .sc-interp.sc-missing:nth-child(n+9 of .sc-interp.sc-missing)::before{animation:none; - background:color-mix(in srgb,currentColor 8%,transparent)} - .sc-placeholder-error{padding:4px 8px;font:11px/1.4 ui-monospace,monospace; - color:color-mix(in srgb,currentColor 70%,transparent);word-break:break-word} - .sc-interp.sc-missing{display:inline-block;width:2em;height:1em;overflow:hidden; - vertical-align:text-bottom;background:rgba(255,255,255,.3);border:1px solid rgba(0,0,0,.5); - border-radius:2px;box-sizing:border-box;color:transparent; - user-select:none} - .sc-interp.sc-unresolved{font-family:ui-monospace,monospace;font-size:.85em; - color:color-mix(in srgb,currentColor 50%,transparent); - background:color-mix(in srgb,currentColor 10%,transparent);border-radius:3px; - padding:0 3px} - .sc-host.sc-has-error{position:relative} - .sc-logic-error{position:absolute;top:8px;left:8px;z-index:2147483647;max-width:60ch; - padding:6px 10px;background:#b00020;color:#fff;font:12px/1.4 ui-monospace,monospace; - border-radius:4px;white-space:pre-wrap;pointer-events:none} - /* Mirrors PRINT_BASELINE_CSS in apps/web deck-stage-export.ts \u2014 keep both - in sync until dc-runtime regains a build step. */ - @media print { - @page { margin: 0.5cm; } - figure, table { break-inside: avoid; } - #dc-root, #dc-root > .sc-host { height: auto; } - *, *::before, *::after { - print-color-adjust: exact; -webkit-print-color-adjust: exact; - backdrop-filter: none !important; -webkit-backdrop-filter: none !important; - animation-delay: -99s !important; animation-duration: .001s !important; - animation-iteration-count: 1 !important; animation-fill-mode: both !important; - animation-play-state: running !important; transition-duration: 0s !important; - } - } - `; - var FULL_PAGE_CSS = "html,body{height:100%;margin:0}#dc-root,#dc-root>.sc-host{height:100%}"; - function rootNameForDocument(doc, loc) { - let bootPath = loc.pathname || ""; - if (!/\.dc\.html?$/i.test(safeDecode(bootPath))) { - try { - bootPath = new URL(doc.baseURI || "/").pathname; - } catch { - } - } - return dcNameFromPath(bootPath); - } - function safeDecode(s) { - try { - return decodeURIComponent(s); - } catch { - return s; - } - } - function boot(runtime, doc = document) { - const parsed = parseDcDocument(doc); - if (!parsed) return null; - const React = getReact(); - const rootName = rootNameForDocument(doc, location); - runtime.markFetched(rootName); - runtime.setRootName(rootName); - runtime.adoptParsed(rootName, parsed); - if (!window.__resources) { - fetch(location.href).then((res) => res.ok ? res.text() : "").then((t) => { - const raw = t ? parseDcText(t) : null; - if (raw?.template) runtime.updateHtml(rootName, raw.template); - }).catch(() => { - }); - } - const dc = doc.querySelector("x-dc"); - const hostEl = doc.createElement("div"); - hostEl.id = "dc-root"; - dc.replaceWith(hostEl); - if (!parsed.preview) { - const s = doc.createElement("style"); - s.textContent = FULL_PAGE_CSS; - doc.head.appendChild(s); - } - const Root = runtime.getDC(rootName); - const entry = runtime.registry.get(rootName); - function StandaloneRoot() { - const [, setTick] = React.useState(0); - React.useEffect(() => { - const sub = () => setTick((n) => n + 1); - entry.subs.add(sub); - return () => { - entry.subs.delete(sub); - }; - }, []); - const defaults = React.useMemo(() => { - const d = {}; - for (const k in entry.propsMeta || {}) { - const v = entry.propsMeta?.[k]?.default; - if (v !== void 0) d[k] = v; - } - return d; - }, [entry.propsMeta]); - return h(Root, { ...defaults, ...entry.propOverrides || {} }); - } - const ReactDOM = getReactDOM(); - if (ReactDOM.createRoot) - ReactDOM.createRoot(hostEl).render(h(StandaloneRoot)); - else ReactDOM.render(h(StandaloneRoot), hostEl); - return rootName; - } - - // src/expr.ts - var IDENT_RE = /^[A-Za-z_$][A-Za-z0-9_$]*/; - var NUMBER_RE = /^-?\d+(\.\d+)?$/; - function resolve(vals, src) { - const expr = String(src).trim(); - if (!expr) return void 0; - if (expr[0] === "(" && expr[expr.length - 1] === ")" && parensWrapWhole(expr)) { - return resolve(vals, expr.slice(1, -1)); - } - const eq = findTopLevelEquality(expr); - if (eq) { - const lv = resolve(vals, expr.slice(0, eq.index)); - const rv = resolve(vals, expr.slice(eq.index + eq.op.length)); - switch (eq.op) { - case "===": - return lv === rv; - case "!==": - return lv !== rv; - case "==": - return lv == rv; - default: - return lv != rv; - } - } - if (expr[0] === "!") return !resolve(vals, expr.slice(1)); - if (expr === "true") return true; - if (expr === "false") return false; - if (expr === "null") return null; - if (expr === "undefined") return void 0; - if (NUMBER_RE.test(expr)) return Number(expr); - if (expr.length >= 2 && (expr[0] === '"' || expr[0] === "'") && expr[expr.length - 1] === expr[0]) { - return expr.slice(1, -1); - } - return resolvePath(vals, expr); - } - function parensWrapWhole(expr) { - let depth = 0; - for (let i = 0; i < expr.length - 1; i++) { - if (expr[i] === "(") depth++; - else if (expr[i] === ")") { - depth--; - if (depth === 0) return false; - } - } - return true; - } - function findTopLevelEquality(expr) { - let depth = 0; - for (let i = 0; i < expr.length; i++) { - const c = expr[i]; - if (c === "[" || c === "(") depth++; - else if (c === "]" || c === ")") depth--; - else if (depth === 0 && (c === "=" || c === "!") && expr[i + 1] === "=") { - if (i > 0 && (expr[i - 1] === "=" || expr[i - 1] === "!")) continue; - if (!expr.slice(0, i).trim()) continue; - const op = expr[i + 2] === "=" ? c + "==" : c + "="; - return { index: i, op }; - } - } - return null; - } - function resolvePath(vals, expr) { - const head = expr.match(IDENT_RE); - if (!head) return void 0; - let cur = vals == null ? void 0 : vals[head[0]]; - let i = head[0].length; - while (i < expr.length) { - if (expr[i] === ".") { - const m = expr.slice(i + 1).match(IDENT_RE) || expr.slice(i + 1).match(/^\d+/); - if (!m) return void 0; - cur = cur == null ? void 0 : cur[m[0]]; - i += 1 + m[0].length; - } else if (expr[i] === "[") { - let depth = 1; - let j = i + 1; - while (j < expr.length && depth > 0) { - if (expr[j] === "[") depth++; - else if (expr[j] === "]") { - depth--; - if (depth === 0) break; - } - j++; - } - if (depth !== 0) return void 0; - const key = resolve(vals, expr.slice(i + 1, j)); - cur = cur == null ? void 0 : cur[key]; - i = j + 1; - } else { - return void 0; - } - } - return cur; - } - - // src/encode.ts - var CAMEL_ATTR = "sc-camel-"; - var INLINE_TEXT_TAGS = new Set( - "a abbr b bdi bdo br cite code del dfn em i ins kbd mark q s samp small span strike strong sub sup u var wbr".split( - " " - ) - ); - var RAW_WRAP = { - select: "sc-raw-select", - table: "sc-raw-table", - tbody: "sc-raw-tbody", - thead: "sc-raw-thead", - tfoot: "sc-raw-tfoot", - tr: "sc-raw-tr", - td: "sc-raw-td", - th: "sc-raw-th", - caption: "sc-raw-caption" - }; - var RAW_UNWRAP = Object.fromEntries( - Object.entries(RAW_WRAP).map(([k, v]) => [v, k]) - ); - var EVENT_MAP = { - onclick: "onClick", - onchange: "onChange", - oninput: "onInput", - onsubmit: "onSubmit", - onkeydown: "onKeyDown", - onkeyup: "onKeyUp", - onkeypress: "onKeyPress", - onmousedown: "onMouseDown", - onmouseup: "onMouseUp", - onmouseenter: "onMouseEnter", - onmouseleave: "onMouseLeave", - onfocus: "onFocus", - onblur: "onBlur", - ondoubleclick: "onDoubleClick", - oncontextmenu: "onContextMenu", - onmousemove: "onMouseMove", - onmouseover: "onMouseOver", - onmouseout: "onMouseOut", - onpointerdown: "onPointerDown", - onpointerup: "onPointerUp", - onpointermove: "onPointerMove", - onpointerenter: "onPointerEnter", - onpointerleave: "onPointerLeave", - onpointercancel: "onPointerCancel", - onpointerover: "onPointerOver", - onpointerout: "onPointerOut", - ongotpointercapture: "onGotPointerCapture", - onlostpointercapture: "onLostPointerCapture", - ontouchstart: "onTouchStart", - ontouchend: "onTouchEnd", - ontouchmove: "onTouchMove", - ontouchcancel: "onTouchCancel", - ondragstart: "onDragStart", - ondragend: "onDragEnd", - ondragenter: "onDragEnter", - ondragleave: "onDragLeave", - ondragover: "onDragOver", - onanimationstart: "onAnimationStart", - onanimationend: "onAnimationEnd", - onanimationiteration: "onAnimationIteration", - ontransitionend: "onTransitionEnd" - }; - var ATTRS = `(?:[^>"']|"[^"]*"|'[^']*')*`; - var IMPORT_SELF_CLOSE_RE = new RegExp( - "<(x-import|dc-import)(" + ATTRS + ")/>", - "gi" - ); - var CAMEL_ATTR_RE = /(\s)([a-z]+[A-Z][A-Za-z0-9]*)(\s*=)/g; - function encodeCamelAttrs(html) { - return html.replace( - CAMEL_ATTR_RE, - (_, sp, name, eq) => sp + CAMEL_ATTR + name.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase()) + eq - ); - } - function encodeCase(html) { - html = html.replace( - IMPORT_SELF_CLOSE_RE, - (_, t, a) => "<" + t + a + ">" - ); - html = html.replace(/)/gi, "/gi, ""); - html = encodeCamelAttrs(html); - for (const [real, alias] of Object.entries(RAW_WRAP)) { - html = html.replace( - new RegExp("(])", "gi"), - "$1" + alias - ); - } - return html; - } - function kebabToCamel(s) { - return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase()); - } - function cssToObj(css) { - const o = {}; - for (const decl of css.split(";")) { - const i = decl.indexOf(":"); - if (i < 0) continue; - const prop = decl.slice(0, i).trim(); - o[prop.startsWith("--") ? prop : kebabToCamel(prop)] = decl.slice(i + 1).trim(); - } - return o; - } - function compileAttr(raw) { - const whole = raw.match(/^\s*\{\{([\s\S]+?)\}\}\s*$/); - if (whole) { - const path = whole[1]; - return (vals) => resolve(vals, path); - } - if (raw.includes("{{")) { - const parts = raw.split(/\{\{([\s\S]+?)\}\}/g); - return (vals) => parts.map((s, i) => i & 1 ? resolve(vals, s) ?? "" : s).join(""); - } - return () => raw; - } - - // src/compile.ts - function collectProps(node, kind, host) { - const propGetters = []; - const pseudoClasses = []; - let hintSize = null; - for (const { name, value } of [...node.attributes]) { - if (name === "sc-name" || name === "data-dc-tpl") continue; - let key = name; - if (key.startsWith(CAMEL_ATTR)) - key = kebabToCamel(key.slice(CAMEL_ATTR.length)); - if (key === "hint-size") { - hintSize = value; - continue; - } - if (key.startsWith("style-")) { - pseudoClasses.push(host.pseudoClass(key.slice(6), value)); - continue; - } - if (kind !== "dom") { - if (key.includes("-") && !(kind === "x-import" && (key.startsWith("aria-") || key.startsWith("data-")))) - key = kebabToCamel(key); - } else { - if (key === "class") key = "className"; - else if (key === "for") key = "htmlFor"; - else if (key.startsWith("on")) - key = EVENT_MAP[key] || "on" + key[2].toUpperCase() + key.slice(3); - } - propGetters.push([key, compileAttr(value)]); - } - return { propGetters, pseudoClasses, hintSize }; - } - var HOST_STYLE_PROPS = /* @__PURE__ */ new Set([ - "position", - "left", - "right", - "top", - "bottom", - "inset", - "width", - "height", - "z-index", - "transform" - ]); - function hostPositionStyle(style) { - const all = typeof style === "string" ? cssToObj(style) : style != null && typeof style === "object" ? style : null; - if (!all) return void 0; - const out = {}; - for (const [k, v] of Object.entries(all)) { - const kebab = k.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase()); - if (HOST_STYLE_PROPS.has(kebab)) out[k] = v; - } - return Object.keys(out).length ? out : void 0; - } - function compileTemplate(html, host) { - const tpl = document.createElement("template"); - //! nosemgrep: direct-inner-html-assignment - tpl.innerHTML = encodeCase(html); - let tplN = 0; - (function stamp(node) { - if (node.nodeType === Node.ELEMENT_NODE) { - node.setAttribute("data-dc-tpl", String(tplN++)); - } - for (const c of node.childNodes) stamp(c); - })(tpl.content); - const builders = walkChildren(tpl.content, host); - const render = ((vals, ctx) => builders.map((b, i) => b(vals || {}, ctx, i))); - render.__annotated = tpl.innerHTML; - return render; - } - function walkChildren(node, host) { - return [...node.childNodes].map((c) => walk(c, host)).filter((b) => b != null); - } - var SLIDE_ID_VALUE_RE = /^[0-9a-f]{8}$/; - var DECK_CONTROL_FLOW_RE = /^(sc-if|sc-for|sc-else|dc-import|x-import)$/; - var DECK_AUX_RE = /^(template|script|style|sc-helmet|helmet)$/; - function isDeckMountTag(el) { - if (el.localName === "deck-stage") return true; - return el.localName === "x-import" && (el.getAttribute("component-from-global-scope") || "") === "deck-stage"; - } - function walkDeckChildren(el, host) { - const pairs = [...el.childNodes].map((c) => ({ c, b: walk(c, host) })).filter((p) => p.b !== null); - const kids = pairs.map((p) => p.b); - const seen = /* @__PURE__ */ new Set(); - const wsSeen = /* @__PURE__ */ new Map(); - const keys = []; - const nextSlideId = new Array(pairs.length); - { - let upcoming = null; - for (let j = pairs.length - 1; j >= 0; j--) { - const n = pairs[j].c; - if (n.nodeType === Node.ELEMENT_NODE) { - const t = n.localName; - upcoming = !DECK_AUX_RE.test(t) && !DECK_CONTROL_FLOW_RE.test(t) ? n.getAttribute("data-om-slide-id") : null; - } - nextSlideId[j] = upcoming; - } - } - for (let j = 0; j < pairs.length; j++) { - const { c } = pairs[j]; - if (c.nodeType === Node.TEXT_NODE) { - if ((c.nodeValue ?? "").trim() === "") { - const base = nextSlideId[j] ? "omid-ws:" + nextSlideId[j] : "omid-ws:aux"; - const n = wsSeen.get(base) ?? 0; - wsSeen.set(base, n + 1); - keys.push(n === 0 ? base : base + ":" + n); - continue; - } - return { kids, keys: null }; - } - if (c.nodeType !== Node.ELEMENT_NODE) { - keys.push(j); - continue; - } - const child = c; - const tag = child.localName; - if (DECK_AUX_RE.test(tag)) { - keys.push(j); - continue; - } - if (DECK_CONTROL_FLOW_RE.test(tag)) return { kids, keys: null }; - const v = child.getAttribute("data-om-slide-id"); - if (!v || !SLIDE_ID_VALUE_RE.test(v) || seen.has(v)) { - return { kids, keys: null }; - } - seen.add(v); - keys.push("omid:" + v); - } - return { kids, keys }; - } - function renderDeckKids(kids, kidKeys, vals, ctx) { - return kids.map((b, j) => { - const k = kidKeys ? kidKeys[j] : j; - const out = b(vals, ctx, k); - return kidKeys != null && typeof out === "string" ? h(getReact().Fragment, { key: k }, out) : out; - }); - } - function walk(node, host) { - if (node.nodeType === Node.TEXT_NODE) return walkText(node); - if (node.nodeType !== Node.ELEMENT_NODE) return null; - const el = node; - const tag = el.tagName.toLowerCase(); - if (tag === "sc-for") return walkFor(el, host); - if (tag === "sc-if") return walkIf(el, host); - if (tag === "x-import") return walkXImport(el, host); - if (tag === "sc-helmet") return host.helmet(el); - if (tag === "dc-import") return walkComponent(el, host); - return walkElement(el, host); - } - var warnedHoles = /* @__PURE__ */ new Set(); - function warnUnresolved(ctx, what) { - const key = (ctx?.__name || "?") + "\0" + what; - if (warnedHoles.has(key)) return; - warnedHoles.add(key); - console.warn("[dc-runtime] " + (ctx?.__name || "template") + ": " + what); - } - function walkText(node) { - const txt = node.nodeValue ?? ""; - if (!txt.includes("{{")) { - if (!txt.trim() && !txt.includes(" ")) return null; - return () => txt; - } - const parts = txt.split(/\{\{([\s\S]+?)\}\}/g); - return (vals, ctx, key) => h( - getReact().Fragment, - { key }, - ...parts.map((p, i) => { - if (!(i & 1)) return p; - const v = resolve(vals, p); - if (v === void 0) { - if (!ctx?.__streamingNow) { - if (document.body?.hasAttribute("data-dc-editor-on")) { - return h( - "span", - { key: i, className: "sc-interp sc-unresolved" }, - "{{ " + p.trim() + " }}" - ); - } - warnUnresolved( - ctx, - "{{ " + p.trim() + " }} never resolved \u2014 rendered as empty" - ); - return null; - } - return h( - "span", - { key: i, className: "sc-interp sc-missing" }, - p.trim() - ); - } - if (getReact().isValidElement(v) || Array.isArray(v)) { - return h(getReact().Fragment, { key: i }, v); - } - if (v === null || typeof v === "boolean") return null; - return h("span", { key: i, className: "sc-interp" }, String(v)); - }) - ); - } - function walkFor(el, host) { - const listGet = compileAttr(el.getAttribute("list") || ""); - const asName = el.getAttribute("as") || "item"; - const hintN = parseInt(el.getAttribute("hint-placeholder-count") || "0", 10); - const kids = walkChildren(el, host); - const listSrc = el.getAttribute("list") || ""; - return (vals, ctx, key) => { - let list = listGet(vals); - if (!Array.isArray(list)) { - if (!ctx?.__streamingNow) { - if (list !== void 0 && list !== null) { - warnUnresolved( - ctx, - 'sc-for list="' + listSrc + '" is not an array (' + typeof list + ")" - ); - } - list = []; - } else { - list = hintN > 0 ? Array(hintN).fill(void 0) : []; - } - } - return h( - getReact().Fragment, - { key }, - list.map((item, i) => { - const sub = { ...vals, [asName]: item, $index: i }; - return h( - getReact().Fragment, - { key: i }, - kids.map((b, j) => b(sub, ctx, j)) - ); - }) - ); - }; - } - function walkIf(el, host) { - const valGet = compileAttr(el.getAttribute("value") || ""); - const hintRaw = el.getAttribute("hint-placeholder-val"); - const hintGet = hintRaw != null ? compileAttr(hintRaw) : null; - const kids = walkChildren(el, host); - return (vals, ctx, key) => { - let v = valGet(vals); - if (v === void 0 && hintGet && ctx?.__streamingNow) v = hintGet(vals); - return v ? h( - getReact().Fragment, - { key }, - kids.map((b, j) => b(vals, ctx, j)) - ) : null; - }; - } - function walkComponent(el, host) { - const name = el.getAttribute("name") || el.getAttribute("component") || ""; - el.removeAttribute("name"); - el.removeAttribute("component"); - const tplId = el.getAttribute("data-dc-tpl"); - const styleRaw = el.getAttribute("style"); - el.removeAttribute("style"); - const styleGet = styleRaw != null ? compileAttr(styleRaw) : null; - const { propGetters, hintSize } = collectProps(el, "dc-import", host); - const kids = walkChildren(el, host); - return (vals, ctx, key) => { - const props = { - key, - __hintSize: hintSize, - __tplId: tplId, - __hostStyle: styleGet ? hostPositionStyle(styleGet(vals)) : void 0 - }; - for (const [k, g] of propGetters) { - const v = g(vals); - if (k === "dcProps") { - if (v && typeof v === "object") Object.assign(props, v); - continue; - } - props[k] = v; - } - if (kids.length) props.children = kids.map((b, j) => b(vals, ctx, j)); - return h(host.component(name), props); - }; - } - function walkXImport(el, host) { - const globalNameGet = compileAttr( - el.getAttribute("component-from-global-scope") || "" - ); - const exportNameGet = compileAttr( - el.getAttribute("component") || el.getAttribute("name") || "" - ); - const fromRaw = el.getAttribute("from") || (el.getAttribute("component-from-global-scope") ? "" : el.getAttribute("src") || el.getAttribute("import") || ""); - const urls = fromRaw.trim() ? fromRaw.trim().split(/\s+/) : []; - const url = urls.length ? urls[urls.length - 1] : ""; - const kindOf = (u) => /\.(jsx|tsx)(\?|#|$)/i.test(u) ? "jsx" : "js"; - const tplId = el.getAttribute("data-dc-tpl"); - const styleRaw = el.getAttribute("style"); - el.removeAttribute("style"); - const styleGet = styleRaw != null ? compileAttr(styleRaw) : null; - const wrap = tplId != null || styleGet != null; - const { propGetters, hintSize } = collectProps(el, "x-import", host); - const hasContent = el.children.length > 0 || !!(el.textContent || "").trim(); - const deckKeyed = hasContent && isDeckMountTag(el) ? walkDeckChildren(el, host) : null; - const kids = deckKeyed ? deckKeyed.kids : hasContent ? walkChildren(el, host) : []; - const kidKeys = deckKeyed?.keys ?? null; - const urlBindable = fromRaw.includes("{{"); - if (urls.length && !urlBindable) { - let prev; - for (const u of urls) prev = host.loadExternal(kindOf(u), u, prev); - } - const evalName = (g, vals) => { - const v = g(vals); - const s = v == null ? "" : String(v); - return s.includes("{{") ? "" : s; - }; - return (vals, ctx, key) => { - const globalName = evalName(globalNameGet, vals); - const name = globalName || evalName(exportNameGet, vals); - const C = !name || urlBindable ? null : globalName ? host.resolveExternalGlobal(url, globalName) : host.resolveExternal(url, name); - const hostStyle = styleGet ? hostPositionStyle(styleGet(vals)) : void 0; - const wrapper = wrap ? { - key, - className: "sc-host-x", - "data-dc-tpl": tplId, - style: hostStyle || { display: "contents" } - } : null; - if (!C) { - const error = urlBindable ? "x-import `from` cannot contain {{ \u2026 }} \u2014 module URLs are resolved at parse time; use a literal URL" : host.resolveExternalError(url, name); - const ph = host.placeholder({ - key: wrapper ? void 0 : key, - name, - hintSize, - error - }); - return wrapper ? h("div", wrapper, ph) : ph; - } - const props = wrapper ? {} : { key }; - let unresolvedHole = false; - for (const [k, g] of propGetters) { - if (k === "component" || k === "componentFromGlobalScope" || k === "from") { - continue; - } - const v = g(vals); - if (v === void 0) unresolvedHole = true; - if (k === "dcProps") { - if (v && typeof v === "object") Object.assign(props, v); - continue; - } - props[k] = v; - } - if (unresolvedHole && ctx?.__htmlStreamingNow) { - const ph = host.placeholder({ - key: wrapper ? void 0 : key, - name, - hintSize, - error: null - }); - return wrapper ? h("div", wrapper, ph) : ph; - } - if (kids.length) { - props.children = renderDeckKids(kids, kidKeys, vals, ctx); - } - return wrapper ? h("div", wrapper, h(C, props)) : h(C, props); - }; - } - function contentKey(el) { - const clone = el.cloneNode(true); - for (const d of clone.querySelectorAll("*")) { - while (d.attributes.length) d.removeAttribute(d.attributes[0].name); - } - const s = clone.innerHTML; - let h2 = 5381; - for (let i = 0; i < s.length; i++) h2 = (h2 << 5) + h2 + s.charCodeAt(i) | 0; - return s.length + "." + (h2 >>> 0).toString(36); - } - var NEVER_CONTENT_KEYED = new Set( - "script style textarea option title select canvas iframe video audio".split( - " " - ) - ); - var NOT_INLINE_SELECTOR = ":not(" + [...INLINE_TEXT_TAGS].join(",") + ")"; - function walkElement(el, host) { - const realTag = RAW_UNWRAP[el.localName] || el.localName; - const tplId = el.getAttribute("data-dc-tpl"); - const inlineOnly = el.childNodes.length > 0 && !NEVER_CONTENT_KEYED.has(realTag) && el.querySelector(NOT_INLINE_SELECTOR) === null; - const keySuffix = inlineOnly ? "|" + contentKey(el) : ""; - const { propGetters, pseudoClasses } = collectProps(el, "dom", host); - const deckKeyed = isDeckMountTag(el) ? walkDeckChildren(el, host) : null; - const kids = deckKeyed ? deckKeyed.kids : walkChildren(el, host); - const kidKeys = deckKeyed?.keys ?? null; - return (vals, ctx, key) => { - const props = { - key: key + keySuffix, - "data-dc-tpl": tplId - }; - for (const [k, g] of propGetters) { - let v = g(vals); - if (k === "style" && typeof v === "string") v = cssToObj(v); - if ((k === "value" || k === "checked") && v === void 0) { - v = k === "checked" ? false : ""; - } - props[k] = v; - } - if (pseudoClasses.length) { - props.className = [props.className, ...pseudoClasses].filter(Boolean).join(" "); - } - return h(realTag, props, ...renderDeckKids(kids, kidKeys, vals, ctx)); - }; - } - - // src/logic.ts - var StreamableLogic = class { - constructor(props) { - __publicField(this, "props"); - __publicField(this, "state", {}); - /** Back-pointer to the wrapper component, installed after construction. */ - __publicField(this, "__host"); - this.props = props || {}; - } - setState(update, cb) { - this.__host && this.__host.__setLogicState(update, cb); - } - forceUpdate() { - this.__host && this.__host.forceUpdate(); - } - componentDidMount() { - } - componentDidUpdate(_prevProps) { - } - componentWillUnmount() { - } - /** The flat object the template renders against (merged over props). */ - renderVals() { - return {}; - } - }; - function evalDcLogic(src) { - //! nosemgrep: eval-and-function-constructor - const fn = new Function( - "DCLogic", - "StreamableLogic", - "React", - src + '\n;return (typeof Component!=="undefined"&&Component)||undefined;' - ); - return fn(StreamableLogic, StreamableLogic, getReact()); - } - - // src/component.ts - function shallowEqual(a, b) { - if (!b) return false; - const ak = Object.keys(a).filter((k) => k !== "children"); - const bk = Object.keys(b).filter((k) => k !== "children"); - if (ak.length !== bk.length) return false; - for (const k of ak) if (a[k] !== b[k]) return false; - return true; - } - function Placeholder({ - name, - hintSize, - streaming, - error - }) { - const [w, hgt] = (hintSize || "100%,60px").split(","); - return h( - "div", - { - className: "sc-placeholder" + (streaming ? " sc-streaming" : ""), - style: { width: w.trim(), height: hgt && hgt.trim() }, - title: name - }, - error ? h( - "div", - { className: "sc-placeholder-error" }, - (name ? name + ": " : "") + error - ) : null - ); - } - function hintToMin(hint) { - if (!hint) return void 0; - const [w, hgt] = hint.split(","); - return { minWidth: w.trim(), minHeight: hgt && hgt.trim() }; - } - function createComponentFactory(registry, ensureFetched) { - const React = getReact(); - const AncestorContext = React.createContext([]); - class StreamableComponent extends React.Component { - constructor(props) { - super(props); - __publicField(this, "__name"); - __publicField(this, "__sub"); - __publicField(this, "__needsDidMount", false); - /** Snapshot of the registry's streaming flags taken at render time — - * builders read it off the RenderCtx (this) to pick placeholder vs - * render-nothing for unresolved values. */ - __publicField(this, "__streamingNow", false); - __publicField(this, "__htmlStreamingNow", false); - /** When a construct throws, remember the (class, registry.ver, props) - * triple so render-time reconcile doesn't re-attempt it on every parent - * re-render. A registry bump (new class, template, external module - * resolving via bumpAll) changes `ver` and breaks the memo so an - * env-dependent constructor can self-heal. */ - __publicField(this, "__failedLogic", null); - __publicField(this, "__failedUserProps", null); - __publicField(this, "__failedVer", -1); - /** Per-instance constructor error — kept here (not on the registry entry) - * so one instance's successful construct can't hide a sibling's failure, - * and a construct can never wipe an eval error `updateJs` recorded on - * `r.logicError`. */ - __publicField(this, "__ctorError", null); - __publicField(this, "logic"); - this.__name = props.__name; - this.state = { __v: 0, __err: null }; - this.__sub = () => { - if (this.state.__err) this.setState({ __err: null }); - this.forceUpdate(); - }; - this.__makeLogic(registry.get(this.__name).Logic, null); - ensureFetched(this.__name); - } - /** Error-boundary hook: a render crash anywhere in this DC's subtree - * (its own template, an x-import'd component, a child DC without its - * own deeper boundary) lands here instead of unmounting the page. */ - static getDerivedStateFromError(e) { - return { __err: e instanceof Error && e.message ? e.message : String(e) }; - } - componentDidCatch(e, info) { - console.error( - "[dc-runtime] render error in <" + this.__name + ">:", - e, - info?.componentStack || "" - ); - } - /** Instantiate the logic class (or the no-op base) and adopt `prevState` - * over its initial state — used both at mount and on hot-swap. */ - __makeLogic(Logic, prevState) { - const L = Logic || StreamableLogic; - try { - this.logic = new L(this.__userProps()); - this.__failedLogic = null; - this.__failedUserProps = null; - this.__ctorError = null; - } catch (e) { - console.error(e); - this.__failedLogic = Logic; - this.__failedUserProps = this.__userProps(); - this.__failedVer = registry.get(this.__name).ver; - this.__ctorError = this.__name + ": " + (e instanceof Error && e.message ? e.message : String(e)); - this.logic = new StreamableLogic( - this.__userProps() - ); - } - this.logic.__host = this; - if (prevState) - this.logic.state = { ...this.logic.state || {}, ...prevState }; - } - /** The props the author's logic + template see — internal __-prefixed - * wiring stripped. */ - __userProps() { - const { __name, __hintSize, __tplId, __hostStyle, ...rest } = this.props; - return rest; - } - __setLogicState(update, cb) { - const prev = this.logic.state; - const patch = typeof update === "function" ? update(prev) : update; - this.logic.state = { ...prev, ...patch }; - this.setState((s) => ({ __v: s.__v + 1 }), cb); - } - /** Swap the logic instance when the registry's Logic class changed - * (streaming completion, hot reload). State carries over; didMount - * re-fires after the swap commits so refs exist. */ - __reconcileLogic() { - const r = registry.get(this.__name); - const Next = r.Logic; - const Cur = this.logic.constructor; - if (Next === Cur || !Next && Cur === StreamableLogic || Next === this.__failedLogic && r.ver === this.__failedVer && shallowEqual(this.__userProps(), this.__failedUserProps)) { - return; - } - if (!this.__needsDidMount) { - try { - this.logic.componentWillUnmount(); - } catch (e) { - console.error(e); - } - } - this.__makeLogic(Next, this.logic.state); - this.__needsDidMount = true; - } - componentDidMount() { - registry.get(this.__name).subs.add(this.__sub); - try { - this.logic.componentDidMount(); - } catch (e) { - console.error(e); - } - } - componentDidUpdate(prevProps) { - this.logic.props = this.__userProps(); - if (this.__needsDidMount) { - if (this.state.__err || !registry.get(this.__name).tpl) return; - this.__needsDidMount = false; - try { - this.logic.componentDidMount(); - } catch (e) { - console.error(e); - } - } else { - try { - this.logic.componentDidUpdate(prevProps); - } catch (e) { - console.error(e); - } - } - } - componentWillUnmount() { - registry.get(this.__name).subs.delete(this.__sub); - if (!this.__needsDidMount) { - try { - this.logic.componentWillUnmount(); - } catch (e) { - console.error(e); - } - } - } - render() { - const r = registry.get(this.__name); - const cls = "sc-host" + (r.htmlStreaming ? " sc-streaming-html" : "") + (r.jsStreaming ? " sc-streaming-js" : ""); - const hintStyle = r.htmlStreaming ? hintToMin(this.props.__hintSize) : void 0; - const hostStyle = this.props.__hostStyle || hintStyle ? { ...hintStyle || {}, ...this.props.__hostStyle || {} } : void 0; - const hostBase = { - className: cls, - style: hostStyle, - "data-sc-name": this.__name, - "data-dc-tpl": this.props.__tplId - }; - const chain = Array.isArray(this.context) ? this.context : []; - if (chain.includes(this.__name)) { - const cycle = [ - ...chain.slice(chain.indexOf(this.__name)), - this.__name - ].join(" \u2192 "); - return h( - "div", - { ...hostBase, className: cls + " sc-has-error" }, - h(Placeholder, { - name: this.__name, - hintSize: this.props.__hintSize, - error: "circular import: " + cycle - }) - ); - } - if (this.state.__err) { - return h( - "div", - { ...hostBase, className: cls + " sc-has-error" }, - h( - "div", - { className: "sc-logic-error", "data-omelette-chrome": "" }, - this.__name + ": " + this.state.__err - ), - h(Placeholder, { - name: this.__name, - hintSize: this.props.__hintSize, - error: this.state.__err - }) - ); - } - this.__reconcileLogic(); - if (!r.tpl) { - return h( - "div", - hostBase, - h(Placeholder, { name: this.__name, hintSize: this.props.__hintSize }) - ); - } - const userProps = this.__userProps(); - this.logic.props = userProps; - let vals = userProps; - let renderErr = r.logicError || this.__ctorError; - try { - vals = { ...userProps, ...this.logic.renderVals() || {} }; - } catch (e) { - console.error(e); - renderErr = this.__name + ".renderVals(): " + (e instanceof Error && e.message ? e.message : String(e)); - } - this.__streamingNow = !!(r.htmlStreaming || r.jsStreaming); - this.__htmlStreamingNow = !!r.htmlStreaming; - return h( - "div", - { ...hostBase, className: cls + (renderErr ? " sc-has-error" : "") }, - renderErr && h( - "div", - { className: "sc-logic-error", "data-omelette-chrome": "" }, - renderErr - ), - h( - AncestorContext.Provider, - { value: [...chain, this.__name] }, - r.tpl(vals, this) - ) - ); - } - } - __publicField(StreamableComponent, "contextType", AncestorContext); - const named = /* @__PURE__ */ new Map(); - function getDC(name) { - const hit = named.get(name); - if (hit) return hit; - function Dispatcher(p) { - const [, setTick] = React.useState(0); - React.useEffect(() => { - const sub = () => setTick((n) => n + 1); - registry.get(name).subs.add(sub); - return () => { - registry.get(name).subs.delete(sub); - }; - }, []); - ensureFetched(name); - return h(StreamableComponent, { ...p, __name: name }); - } - Dispatcher.displayName = name; - named.set(name, Dispatcher); - return Dispatcher; - } - return { - getDC, - StreamableComponent - }; - } - - // src/bundled.ts - function bundledBlob(url) { - const blobs = window.__resourceBlobs; - const b = blobs ? blobs[url.split("#")[0]] : void 0; - return b instanceof Blob ? b : null; - } - - // src/cdn.ts - var REACT_URL = "https://unpkg.com/react@18.3.1/umd/react.production.min.js"; - var REACT_SRI = "sha384-DGyLxAyjq0f9SPpVevD6IgztCFlnMF6oW/XQGmfe+IsZ8TqEiDrcHkMLKI6fiB/Z"; - var REACT_DOM_URL = "https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"; - var REACT_DOM_SRI = "sha384-gTGxhz21lVGYNMcdJOyq01Edg0jhn/c22nsx0kyqP0TxaV5WVdsSH1fSDUf5YJj1"; - var BABEL_URL = "https://unpkg.com/@babel/standalone@7.29.0/babel.min.js"; - var BABEL_SRI = "sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y"; - function cdnScriptFor(url, sri) { - const res = window.__resources; - const v = res ? res[url] : void 0; - return typeof v === "string" && v ? { src: v } : { src: url, integrity: sri }; - } - - // src/external.ts - var isCustomElementName = (n) => !n.includes(".") && n.includes("-"); - function isRenderableType(g) { - if (typeof g === "function") return !isElementClass(g); - return typeof g === "object" && g !== null && typeof g.$$typeof === "symbol"; - } - function resolveDottedPath(root, name) { - let cur = root; - for (const seg of name.split(".")) { - if (cur == null) return void 0; - cur = cur[seg]; - } - return cur; - } - var GLOBAL_POLL_INTERVAL_MS = 50; - var GLOBAL_POLL_TIMEOUT_MS = 3e4; - function createExternalModules(onResolved) { - const cache = /* @__PURE__ */ new Map(); - let babelLoading = null; - const reportedMissing = /* @__PURE__ */ new Map(); - const polling = /* @__PURE__ */ new Set(); - function ensureBabel() { - if (window.Babel) return Promise.resolve(); - if (babelLoading) return babelLoading; - const babel = cdnScriptFor(BABEL_URL, BABEL_SRI); - babelLoading = new Promise((res, rej) => { - const s = document.createElement("script"); - s.src = babel.src; - if (babel.integrity) { - s.integrity = babel.integrity; - s.crossOrigin = "anonymous"; - } - s.onload = () => res(); - s.onerror = rej; - document.head.appendChild(s); - }); - return babelLoading; - } - const pending = /* @__PURE__ */ new Map(); - function load(kind, url, after) { - const existing = pending.get(url); - if (existing) return existing; - cache.set(url, null); - console.info("[dc-runtime] x-import: loading", url, "(" + kind + ")"); - const ready = Promise.all([ - kind === "jsx" ? ensureBabel() : Promise.resolve(), - after ?? Promise.resolve() - ]); - const p = ready.then(() => { - const pre = bundledBlob(url); - if (pre) return pre.text(); - return fetch(url).then((r) => { - if (!r.ok) throw new Error("HTTP " + r.status); - return r.text(); - }); - }).then((src) => { - const code = kind === "jsx" ? window.Babel.transform(src, { - filename: url, - presets: ["react", "typescript"] - }).code : src; - const module = { exports: {} }; - const before = new Set(Object.keys(window)); - //! nosemgrep: eval-and-function-constructor - new Function("React", "module", "exports", "require", code)( - getReact(), - module, - module.exports, - () => ({}) - ); - const globals = {}; - for (const k of Object.keys(window)) { - if (!before.has(k) && typeof window[k] === "function") { - globals[k] = window[k]; - } - } - cache.set(url, { mod: module.exports, globals }); - console.info( - "[dc-runtime] x-import: loaded", - url, - "\u2014 exports:", - Object.keys(module.exports), - "window globals:", - Object.keys(globals) - ); - onResolved(); - }).catch((e) => { - cache.set(url, { - mod: {}, - globals: {}, - error: "failed to load: " + (e instanceof Error && e.message ? e.message : String(e)) - }); - console.error( - "[dc-runtime] x-import: FAILED to load", - url, - "(" + kind + ")", - e - ); - onResolved(); - }); - pending.set(url, p); - return p; - } - function resolve2(url, name) { - const entry = cache.get(url); - if (!entry) return null; - const { mod, globals } = entry; - const C = mod && mod[name] || globals && globals[name] || typeof window !== "undefined" && window[name] || mod && mod.default; - if (typeof C === "function") return C; - const key = url + "\0" + name; - if (!reportedMissing.has(key)) { - reportedMissing.set( - key, - entry.error || 'no export named "' + name + '" (has: ' + Object.keys(mod).join(", ") + ")" - ); - console.error( - "[dc-runtime] x-import: module", - url, - "loaded but has no component named", - JSON.stringify(name), - "\u2014 available exports:", - Object.keys(mod), - "window globals:", - Object.keys(globals), - ". The module must `module.exports = {" + name + "}` or set `window." + name + "`." - ); - } - return null; - } - function waitForGlobal(name) { - if (polling.has(name)) return; - polling.add(name); - const started = Date.now(); - const isCE = isCustomElementName(name); - const tick = () => { - const found = isCE ? customElements.get(name) : isRenderableType(resolveDottedPath(window, name)); - if (found) { - polling.delete(name); - onResolved(); - return; - } - if (Date.now() - started >= GLOBAL_POLL_TIMEOUT_MS) { - console.warn( - "[dc-runtime] x-import: global", - JSON.stringify(name), - "never appeared on window after " + GLOBAL_POLL_TIMEOUT_MS + "ms" - ); - return; - } - setTimeout(tick, GLOBAL_POLL_INTERVAL_MS); - }; - setTimeout(tick, GLOBAL_POLL_INTERVAL_MS); - } - function resolveGlobal(url, name) { - const isCE = isCustomElementName(name); - if (!url) { - if (isCE) { - if (customElements.get(name)) return name; - waitForGlobal(name); - return null; - } - const g2 = resolveDottedPath(window, name); - if (isRenderableType(g2)) return g2; - waitForGlobal(name); - return null; - } - const entry = cache.get(url); - if (!entry) return null; - if (isCE && customElements.get(name)) return name; - const g = entry.globals[name] ?? resolveDottedPath(window, name); - if (isRenderableType(g)) return g; - if (name.includes(".")) return null; - const key = url + "\0global\0" + name; - if (!reportedMissing.has(key)) { - reportedMissing.set(key, null); - if (isCE && !customElements.get(name)) { - console.warn( - "[dc-runtime] x-import:", - url, - "loaded but no custom element", - JSON.stringify(name), - "is registered and window." + name + " is not a function \u2014 rendering <" + name + "> as an unknown element." - ); - } - } - return name; - } - function getError(url, name) { - const entry = cache.get(url); - if (entry?.error) return entry.error; - return reportedMissing.get(url + "\0" + name) || null; - } - return { load, resolve: resolve2, resolveGlobal, getError }; - } - function isElementClass(g) { - try { - return typeof g === "function" && typeof HTMLElement !== "undefined" && g.prototype instanceof HTMLElement; - } catch { - return false; - } - } - - // src/atomics.ts - var ATOMIC_CSS = ( - // layout - ".fx{display:flex}.col{display:flex;flex-direction:column}.grid{display:grid}.ac{align-items:center}.jc{justify-content:center}.jb{justify-content:space-between}.f1{flex:1}.noshrink{flex-shrink:0}.wrap{flex-wrap:wrap}.fw5{font-weight:500}.fw6{font-weight:600}.fw7{font-weight:700}.fw8{font-weight:800}.fs11{font-size:11px}.fs12{font-size:12px}.fs13{font-size:13px}.fs14{font-size:14px}.fs15{font-size:15px}.fs16{font-size:16px}.fs20{font-size:20px}.fs22{font-size:22px}.upper{text-transform:uppercase}.tc{text-align:center}.nowrap{white-space:nowrap}.gap8{gap:8px}.gap10{gap:10px}.gap12{gap:12px}.gap16{gap:16px}.gap24{gap:24px}.m0{margin:0}.mt8{margin-top:8px}.mt12{margin-top:12px}.mt16{margin-top:16px}.mb8{margin-bottom:8px}.mb12{margin-bottom:12px}.mb16{margin-bottom:16px}.posrel{position:relative}.posabs{position:absolute}.round{border-radius:50%}.ohide{overflow:hidden}.bbox{box-sizing:border-box}.pointer{cursor:pointer}.w100{width:100%}.b0{border:none}" - ); - - // src/helmet.ts - var DESIGN_DOC_MODE_RE = /]*\bname\s*=\s*["']design_doc_mode["'][^>]*\b(?:content|value)\s*=\s*["'](\w+)["']/i; - var CANVAS_BG_LIGHT = "#f0eee6"; - var CANVAS_BG_DARK = "#2e2c26"; - function createHelmetManager(doc, isStreaming) { - const mounted = /* @__PURE__ */ new Set(); - const live = /* @__PURE__ */ new Map(); - let designDocMode = null; - let canvasStyleEl = null; - let appTheme = "light"; - try { - const ds = doc.documentElement.dataset.theme; - appTheme = ds === "dark" || ds === "light" ? ds : new URLSearchParams(doc.defaultView?.location.search ?? "").get( - "theme" - ) === "dark" ? "dark" : "light"; - } catch { - } - function applyCanvasBg() { - if (!canvasStyleEl) return; - const bg = appTheme === "dark" ? CANVAS_BG_DARK : CANVAS_BG_LIGHT; - canvasStyleEl.textContent = `html,body{background:${bg}}#dc-root>.sc-host{position:relative}`; - } - function postDesignMode(mode) { - if (window.parent === window) return; - try { - window.parent.postMessage({ type: "__dc_design_mode", mode }, "*"); - } catch { - } - } - function setDesignDocMode(mode) { - if (mode === designDocMode) return; - designDocMode = mode; - postDesignMode(mode); - if (mode === "canvas") { - doc.documentElement.setAttribute("data-dc-canvas", ""); - canvasStyleEl = doc.createElement("style"); - canvasStyleEl.setAttribute("data-dc-canvas", ""); - applyCanvasBg(); - doc.head.appendChild(canvasStyleEl); - } else { - doc.documentElement.removeAttribute("data-dc-canvas"); - canvasStyleEl?.remove(); - canvasStyleEl = null; - } - } - window.addEventListener("message", (e) => { - const type = e.data && e.data.type; - if (type === "__dc_theme") { - const t = e.data.theme; - if (t === "light" || t === "dark") { - appTheme = t; - applyCanvasBg(); - } - return; - } - if (!designDocMode || type !== "__dc_probe") return; - postDesignMode(designDocMode); - }); - function compile(node) { - const raw = [...node.children]; - const helmetClosed = node.nextSibling != null || node.parentNode?.nextSibling != null; - if (node.hasAttribute("data-dc-atomics") && !mounted.has("__dc-atomics")) { - mounted.add("__dc-atomics"); - const el = doc.createElement("style"); - el.id = "__dc-atomics"; - el.textContent = ATOMIC_CSS; - doc.head.appendChild(el); - } - return (_vals, ctx) => { - const name = ctx && ctx.__name || ""; - const streaming = !!(name && isStreaming(name)); - for (let i = 0; i < raw.length; i++) { - const child = raw[i]; - const tag = child.tagName; - const mayBePartial = streaming && !helmetClosed && i === raw.length - 1; - if (tag === "SCRIPT") { - if (mayBePartial) continue; - const key = "SCRIPT|" + (child.getAttribute("src") || child.textContent || ""); - if (mounted.has(key)) continue; - mounted.add(key); - const el = doc.createElement("script"); - for (const { name: an, value } of [...child.attributes]) - el.setAttribute(an, value); - if (child.textContent) el.textContent = child.textContent; - doc.head.appendChild(el); - } else if (tag === "LINK" || tag === "META") { - if (mayBePartial) continue; - const key = tag + "|" + (child.getAttribute("href") || child.getAttribute("src") || child.outerHTML); - if (mounted.has(key)) continue; - mounted.add(key); - if (tag === "LINK") { - const rel = (child.getAttribute("rel") || "").toLowerCase().split(/\s+/); - const href = (child.getAttribute("href") || "").trim(); - const res = window.__resources; - const pre = res && rel.includes("stylesheet") && !rel.includes("alternate") ? res[href] : void 0; - const blob = typeof pre === "string" && pre ? bundledBlob(pre) : null; - if (blob) { - const el = doc.createElement("style"); - if (child.hasAttribute("disabled")) { - el.setAttribute("media", "not all"); - } else if (child.getAttribute("media")) { - el.setAttribute("media", child.getAttribute("media")); - } - if (child.getAttribute("title")) - el.setAttribute("title", child.getAttribute("title")); - void blob.text().then((css) => { - el.textContent = css; - }); - doc.head.appendChild(el); - continue; - } - } - doc.head.appendChild(child.cloneNode(true)); - } else { - const key = name + "|" + i; - let el = live.get(key); - if (!el || el.tagName !== tag) { - if (el) el.remove(); - el = doc.createElement(tag.toLowerCase()); - live.set(key, el); - doc.head.appendChild(el); - } - for (const { name: an, value } of [...child.attributes]) { - if (el.getAttribute(an) !== value) el.setAttribute(an, value); - } - if (el.textContent !== child.textContent) - el.textContent = child.textContent; - } - } - return null; - }; - } - return { compile, setDesignDocMode }; - } - - // src/pseudo.ts - function scanUnquotedUrl(css, i) { - if (css[i] !== "u" && css[i] !== "U" || css.slice(i, i + 4).toLowerCase() !== "url(" || /[a-z0-9_-]/i.test(css[i - 1] ?? "")) { - return -1; - } - let j = i + 4; - while (j < css.length && /\s/.test(css[j])) j++; - if (css[j] === '"' || css[j] === "'") return -1; - while (j < css.length && css[j] !== ")") { - if (css[j] === "\\") j++; - j++; - } - return j < css.length ? j + 1 : css.length; - } - function stripComments(css) { - let out = ""; - let quote = ""; - for (let i = 0; i < css.length; i++) { - const c = css[i]; - if (quote) { - if (c === "\\") { - out += c + (css[i + 1] ?? ""); - i++; - continue; - } - if (c === quote) quote = ""; - out += c; - } else if (c === "'" || c === '"') { - quote = c; - out += c; - } else if (c === "/" && css[i + 1] === "*") { - const end = css.indexOf("*/", i + 2); - i = end === -1 ? css.length : end + 1; - out += " "; - } else { - const end = scanUnquotedUrl(css, i); - if (end === -1) out += c; - else { - out += css.slice(i, end); - i = end - 1; - } - } - } - return out; - } - function importantify(css) { - css = stripComments(css); - const decls = []; - let start = 0; - let depth = 0; - let quote = ""; - for (let i = 0; i < css.length; i++) { - const c = css[i]; - if (quote) { - if (c === "\\") i++; - else if (c === quote) quote = ""; - } else if (c === "'" || c === '"') quote = c; - else if (c === "(") depth++; - else if (c === ")") depth = Math.max(0, depth - 1); - else if (c === ";" && depth === 0) { - decls.push(css.slice(start, i)); - start = i + 1; - } else { - const end = scanUnquotedUrl(css, i); - if (end !== -1) i = end - 1; - } - } - decls.push(css.slice(start)); - return decls.map((d) => d.trim()).filter(Boolean).map((d) => /!\s*important$/i.test(d) ? d : d + " !important").join(";"); - } - function createPseudoSheet(doc) { - let el = null; - const cache = /* @__PURE__ */ new Map(); - let n = 0; - return (pseudo, css) => { - const k = pseudo + "|" + css; - const hit = cache.get(k); - if (hit) return hit; - if (!el) { - el = doc.createElement("style"); - doc.head.appendChild(el); - } - const cls = "scp" + (n++).toString(36); - const isPseudoElement = pseudo === "before" || pseudo === "after"; - const sel = isPseudoElement ? "." + cls + "::" + pseudo : "." + cls + ":" + pseudo; - el.sheet.insertRule( - sel + "{" + (isPseudoElement ? css : importantify(css)) + "}", - el.sheet.cssRules.length - ); - cache.set(k, cls); - return cls; - }; - } - - // src/registry.ts - function createRegistry() { - const entries = /* @__PURE__ */ Object.create(null); - function get(name) { - return entries[name] || (entries[name] = { - html: "", - tpl: null, - Logic: null, - jsStreaming: false, - htmlStreaming: false, - ver: 0, - subs: /* @__PURE__ */ new Set(), - fetched: false - }); - } - function bump(name) { - const r = get(name); - r.ver++; - for (const fn of r.subs) fn(); - } - return { - entries, - get, - bump, - bumpAll() { - for (const n in entries) bump(n); - } - }; - } - - // src/runtime.ts - var COMPONENT_DIR = "."; - function createRuntime(doc = document) { - const registry = createRegistry(); - const pseudoClass = createPseudoSheet(doc); - const helmet = createHelmetManager( - doc, - (name) => registry.get(name).htmlStreaming - ); - const external = createExternalModules(() => registry.bumpAll()); - const factory = createComponentFactory(registry, ensureFetched); - const host = { - component: (name) => factory.getDC(name), - placeholder: (props) => h(Placeholder, props), - helmet: (node) => helmet.compile(node), - loadExternal: (kind, url, after) => external.load(kind, url, after), - resolveExternal: (url, name) => external.resolve(url, name), - resolveExternalGlobal: (url, name) => external.resolveGlobal(url, name), - resolveExternalError: (url, name) => external.getError(url, name), - pseudoClass - }; - function ensureFetched(name) { - const r = registry.get(name); - if (r.fetched) return; - r.fetched = true; - const url = COMPONENT_DIR + "/" + encodeURIComponent(name) + ".dc.html"; - const res = window.__resources; - const pre = res ? res[url] : void 0; - const target = typeof pre === "string" && pre ? pre : url; - const blob = bundledBlob(target); - (blob ? blob.text() : fetch(target).then((res2) => { - if (!res2.ok) { - console.error( - '[dc-runtime] sibling fetch for "' + name + '" failed:', - url, - "returned", - res2.status, - "\u2014 the reference renders as an empty placeholder." - ); - return ""; - } - return res2.text(); - })).then((t) => { - if (!t) return; - const parsed = parseDcText(t); - if (!parsed) { - console.error( - '[dc-runtime] sibling fetch for "' + name + '":', - url, - "has no block \u2014 not a Design Component." - ); - return; - } - if (parsed.props) r.propsMeta = parsed.props; - if (parsed.preview) r.preview = parsed.preview; - if (parsed.template && !r.html) updateHtml(name, parsed.template); - if (parsed.js && !r.Logic) updateJs(name, parsed.js); - }).catch( - (e) => console.error( - '[dc-runtime] sibling fetch for "' + name + '" threw:', - url, - e - ) - ); - } - let rootName = null; - function updateHtml(name, html) { - const r = registry.get(name); - r.html = html; - if (name === rootName) { - const mode = DESIGN_DOC_MODE_RE.exec(html)?.[1] ?? null; - if (mode || !r.htmlStreaming) helmet.setDesignDocMode(mode); - } - try { - r.tpl = compileTemplate(html, host); - } catch (e) { - console.error("[dc-runtime] template compile FAILED for", name, e); - } - registry.bump(name); - } - function updateJs(name, src) { - const r = registry.get(name); - const seq = r.jsSeq = (r.jsSeq || 0) + 1; - try { - const Cls = evalDcLogic(src); - if (r.jsSeq !== seq) return; - if (typeof Cls !== "function") { - r.logicError = name + ".dc.html: - - - - - - - - -
-
-

Chatballs · Очередь, ожидание и уведомления

- дополнение к design baseline · пять мест -
-

Дорисованы места, которых не было в макете: уведомления браузера в профиле, сроки ожидания в настройках, разделение двух видов ожидания в списке диалогов, время ожидания в строке и карточке, присутствие сотрудника в выборе ответственного. Служебные слова («эскалация», «порог», «SLA») в текстах не используются.

- -
- - -
-
- Q1 - Профиль · карточка уведомлений - новый заголовок и подводка; строка браузера выше ботов -
- -
-

Уведомления о рабочих событиях

-

Выберите, где вас звать. Браузер — на этом устройстве, пока приложение открыто или свёрнуто. Мессенджер — где угодно, в том числе при закрытом браузере; мессенджер один: привязка нового заменяет текущий.

- - -
-
- -
Этот браузерChrome · macOS — видно, когда вкладка свёрнута
- Включены - -
-
- -
-

«Отключить» выключает показ уведомлений в этом браузере. Само разрешение остаётся выданным — снять его можно только в настройках браузера.

-
- - -
-
- -
Telegram@nord_notify_bot · сервисный бот уведомлений
- Подключено - -
-
- -
-
- - -
-
- -
MAXПривязка заменит Telegram
- Привязать бота -
-
-

Нажмите «Привязать бота» и в открывшемся чате — «Начать»: привязка подтвердится сама.

-
-
- - -
-
- Q1b - Строка браузера · три состояния -
- -
- -
-
1 · РАЗРЕШЕНИЕ НЕ ЗАПРАШИВАЛИ
-
- -
Этот браузерНе включены. Браузер спросит разрешение по нажатию.
- -
-
- - -
-
2 · РАЗРЕШЕНИЕ ВЫДАНО, ВКЛЮЧЕНО
-
- -
Этот браузерChrome · macOS — видно, когда вкладка свёрнута
- Включены - -
-
- - -
-
3 · БРАУЗЕР ОТКАЗАЛ
-
- -
Этот браузерChrome · macOS
- Браузер запретил -
-
- -
- Снять запрет можно только в браузере -

Нажмите значок замка слева от адреса → «Уведомления» → «Разрешать», затем обновите страницу. Сайт снять запрет не может.

- -
-
-
-
-
- - -
-
- Q2 - Настройки · новый раздел «Когда звать на помощь» - отдельный пункт субменю сразу после «Групп» -
- -
- - -
-
-

Когда звать на помощь

-

Если клиент ждёт человека, мы напоминаем о диалоге — сначала группе, потом всем, потом руководителю. Сроки считаются с момента, когда диалог встал в очередь. Действуют на всю организацию.

- -
- -
-
- {{ d.title }} - {{ d.hint }} -
-
- {{ d.value }} - мин -
-
-
-
- Сохранено 12 сентября, 14:20 · Елена Кузнецова -
- - -
-
-
- -
- -

Напоминания приходят туда, где сотрудник их получит: в браузер и в привязанный мессенджер. Если у группы не настроен ни один способ, останется только счётчик в списке диалогов.

-
-
-
-
-
- - -
-
- Q3 - Чат · два вида ожидания в списке - вариант А — две вкладки (рекомендуем) · вариант Б — одна вкладка с метками -
- -
- -
-
{{ v.title }}{{ v.note }}
-
-
-
Все диалоги{{ v.total }}
-
-
- - - -
-
- - - -
{{ v.footer }}
-
-
-
-
-
-
- - -
-
- Q4 - Карточка диалога · блок «Ожидание» и системные события -
- -
- -
-

Диалог

фрагмент контекст-панели
- -
-
Ждёт человека 7 минут
-
- АК - Назначен на Анну Ким, не взят -
-

Если не возьмёт — через 3 минуты диалог вернётся в общую очередь.

-
- - - - - - -
В очереди с14:12
-
- - -
-
Системные события в истории диалога — тот же вид, что у остальных; возврат в очередь окрашен как предупреждение.
- -
{{ m.text }} · {{ m.time }}
-
-
-
-
- - -
-
- Q5 - Ответственный · кто сейчас на месте - подсказка, а не запрет: выбрать отсутствующего можно -
- -
- - - -
-
Поиск сотрудника
- - -
{{ a.head }}
-
- - - -
-
«На месте» — приложение открыто. Признак приблизительный: назначить можно любого.
-
-
-
- -
-
-
- - - diff --git a/design/baseline/Сотрудники/.thumbnail b/design/baseline/Сотрудники/.thumbnail deleted file mode 100644 index 857931f..0000000 Binary files a/design/baseline/Сотрудники/.thumbnail and /dev/null differ diff --git a/design/baseline/Сотрудники/_ds/design-system/_adherence.oxlintrc.json b/design/baseline/Сотрудники/_ds/design-system/_adherence.oxlintrc.json deleted file mode 100644 index 8419c5c..0000000 --- a/design/baseline/Сотрудники/_ds/design-system/_adherence.oxlintrc.json +++ /dev/null @@ -1,508 +0,0 @@ -{ - "plugins": [ - "react", - "import" - ], - "rules": { - "react/forbid-elements": [ - "warn", - { - "forbid": [] - } - ], - "no-restricted-imports": [ - "warn", - { - "patterns": [ - { - "group": [ - "components/cards/**", - "components/core/**", - "components/inbox/**", - "components/status/**", - "ui_kits/checkout/**", - "ui_kits/internal-hub/**", - "ui_kits/web-chat/**" - ], - "message": "Import design-system components from 'index.js', not component internals." - } - ] - } - ], - "no-restricted-syntax": [ - "warn", - { - "selector": "Literal[value=/#[0-9a-fA-F]{3,8}\\b/]", - "message": "Raw hex color — use a design-system color token via var()." - }, - { - "selector": "Literal[value=/\\b\\d+px\\b/]", - "message": "Raw px value — use a design-system spacing token via var()." - }, - { - "selector": "JSXOpeningElement[name.name='ActorBadge'] > JSXAttribute > JSXIdentifier[name!=/^(?:actor|initials|color|key|ref|className|style|children)$/]", - "message": " doesn't accept that prop. Declared props: actor, initials, color." - }, - { - "selector": "JSXOpeningElement[name.name='ActorBadge'] > JSXAttribute[name.name='actor'] > Literal[value!=/^(?:ai|human)$/]", - "message": " actor must be one of 'ai' | 'human'." - }, - { - "selector": "JSXOpeningElement[name.name='AttentionStatus'] > JSXAttribute > JSXIdentifier[name!=/^(?:level|label|key|ref|className|style|children)$/]", - "message": " doesn't accept that prop. Declared props: level, label." - }, - { - "selector": "JSXOpeningElement[name.name='AttentionStatus'] > JSXAttribute[name.name='level'] > Literal[value!=/^(?:ok|attention|critical)$/]", - "message": " level must be one of 'ok' | 'attention' | 'critical'." - }, - { - "selector": "JSXOpeningElement[name.name='Avatar'] > JSXAttribute > JSXIdentifier[name!=/^(?:initials|color|size|key|ref|className|style|children)$/]", - "message": " doesn't accept that prop. Declared props: initials, color, size." - }, - { - "selector": "JSXOpeningElement[name.name='Button'] > JSXAttribute > JSXIdentifier[name!=/^(?:children|variant|size|disabled|onClick|type|key|ref|className|style|children)$/]", - "message": " - - - -
- - - -
- -
- -
-
-

Сотрудники

-

Системные роли, должности и размещение · показано {{ shownCount }} из {{ totalCount }}

-
-
- - -
-
- - -
-
- - -
-
Роль -
- - - - -
-
-
Размещение -
- - - -
-
-
- -
- - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - -
СОТРУДНИКРОЛЬДОЛЖНОСТЬРАЗМЕЩЕНИЕДОСТУПСТАТУСПОСЛЕДНИЙ ВХОД
-
-
{{ r.initials }}
-
{{ r.name }}
{{ r.email }}
-
-
{{ r.roleRu }}{{ r.position }}{{ r.placement }}{{ r.access }}{{ r.stLabel }}{{ r.lastLogin }} - - -
- Открыть карточку - Передать владение - - Завершить сессии - Сбросить пароль -
- {{ r.blockLabel }} -
-
Привилегированная учётная запись — изменяется только владельцем через отдельный flow.
-
-
-
-
- -
-
-
Сотрудники не найдены
-
Измените условия фильтра.
-
-
-
-
Показано {{ shownCount }} из {{ totalCount }}
-
Роль не выводится из должности или отдела · SPEC-HUB-0016
-
-
-
-
- - - -
- - Все сотрудники - - -
-
-
{{ c.initials }}
-
-
-

{{ c.name }}

- {{ c.roleRu }} - {{ c.stLabel }} -
-
{{ c.email }} · {{ c.position }} · {{ c.placement }}
-
-
- Только просмотр - - - - -
-
-
- - - -
- -
{{ c.roBanner }}
-
-
- -
- -
- - -
-

Основные данные

-
-
-
-
-
-
- - -
-

Должность и размещение

-

Оргструктура. Не выдаёт прав доступа.

-
-
-
-
-
-
-
Владелец всегда находится на уровне компании — отдел назначить нельзя.
-
- - -
-

Системная роль

-

Уровень в административной иерархии. {{ c.roleHint }}

-
- - - -
-
- - -
-
-

Профили доступа и scopes

- -
-

{{ c.accessHint }}

- -
- -
-
-
- -
{{ a.profile }}
{{ a.scopeLabel }}
-
-
- {{ a.scopeTag }} - -
-
-
- {{ cp }} -
-
-
-
-
- -
- -
Нет рабочего доступа. У сотрудника нет активных назначений — доступен только разрешённый self-service. Роль EMPLOYEE сама по себе прав не даёт.
-
-
- -
- -
ADMIN имеет все обычные capability организации без назначений. Профили не ограничивают его полный обычный доступ. Защищённые governance-capability остаются только у OWNER.
-
-
-
- - -
-

Статус и безопасность

-
-
Пароль
Последняя смена: {{ c.pwdDate }}
- -
-
-
Двухфакторная аутентификация (TOTP)
{{ totpLabel }}
-
-
-
-
Активные сессии
{{ c.sessions }}
- -
-
- - - -
-

Опасная зона

-
-
{{ c.dangerNote }}
- -
-
-
-
- - -
-
-

Учётная запись

-
Статус{{ c.stLabel }}
-
Роль{{ c.roleRu }}
-
Создан{{ c.created }}
-
Последний вход{{ c.lastLogin }}
-
- - -
-

Текущая нагрузка

-
-
Активные диалоги
{{ c.loadDialogs }}
-
В очереди
{{ c.loadQueue }}
-
Продажи · сегодня
{{ c.loadSales }}
-
Ср. ответ
{{ c.loadResp }}
-
- - - -
-

Аудит

-
- -
{{ ev.text }}
{{ ev.code }} · {{ ev.time }}
-
-
-
-
-
-
- - - - -
- Сотрудники - -
-
-

Профили доступа

-

Именованные наборы capability организации · назначаются сотрудникам со scope

-
- -
- -
- -
- -
-
-
-
{{ p.name }}SYSTEM
-
{{ p.desc }}
-
- ОТКЛ. -
-
- {{ p.capCount }} capability - {{ p.assignedCount }} назнач. -
-
-
-
- - -
-
-
-

{{ sp.name }}

SYSTEM
-

{{ sp.desc }}

-
-
- - -
-
- -
- Разрешённые scopes: {{ sp.scopes }} - Назначено сотрудникам: {{ sp.assignedCount }} -
- -
-
-

Capability

- Каталог задан кодом приложения · произвольные коды запрещены -
-
- -
-
{{ g.domain }}
-
- - - -
-
-
-
-
- -
Защищённые capability employees.manage_privileged и ownership.transfer нельзя включить в профиль — они доступны только владельцу.
-
-
-
-
-
-
- -
- - - - -
-
-
-
Новый сотрудник
Учётные данные · роль · размещение · доступ
- -
-
- - -
-
1

Учётные данные

-
-
-
-
-
-
- - -
-
2

Системная роль

-
- - -
-
- - -
-
3

Должность и размещение

-
-
-
-
-
Должность вводится вручную и не выводится из роли. Основной отдел описывает оргструктуру, но не выдаёт прав.
-
- - -
-
4

Профили доступа

- -
- - - -
Без назначенных профилей сотрудник не получит рабочего доступа — только разрешённый self-service. Отсутствие доступа здесь является явным.
-
-
- -
- -
ADMIN получает все обычные capability организации. Профили доступа не назначаются и не должны создавать иллюзию ограничения его полного доступа. Защищённые governance-capability остаются только у OWNER.
-
-
-
-
-
-
Пароль первичного доступа отправится на email
-
- - -
-
-
-
- - - -
-
-
-
- -

Передача владения

Атомарная операция · в организации остаётся ровно один владелец

-
-
-
- -
- -
-
ИП
Иван Петров
Владелец
- -
АК
Анна Котова
→ Владелец
-
- - -
- - -
- -
- -
Новый владелец получит полный доступ и company placement. Вы потеряете права владельца, включая передачу владения и управление администраторами. Операция аудируется и необратима без повторной передачи.
-
- - -
-
- - -
-
-
-
- - -
- - -
-
-

Chatbolls · Порталы

- design baseline · OWNER = ADMIN · SPEC-HUB-0028 §1.2–1.3, §3, §6 -
-

Модалка настроек разобрана на отдельную страницу с субменю разделов. Публичный адрес — постоянный блок в шапке карточки: копировать, открыть, статус домена. Редактор статьи — полноэкранный split: слева текст с панелью Markdown, справа живой предпросмотр, файлы и версии — в правой рейке.

- -
- -
-
- {{ f.id }} - {{ f.title }} - {{ f.note }} -
- -
- - - - - -
-
-
-

Порталы

Публичные базы знаний для клиентов. Каждый портал — свой адрес, оформление и материалы.

- -
- -
-
- - -
- - - - - - - - - - - - - - - - - - - - - - -
ПОРТАЛПУБЛИЧНЫЙ АДРЕССТАТУСМАТЕРИАЛЫОБНОВЛЁН
- - - {{ p.name }}{{ p.locale }} - - - - {{ p.url }} - - {{ p.domainBadge }} - - {{ p.status }}{{ p.content }}{{ p.updated }} - - -
- -
- - - -
-
-
-
- -
- 4 портала · архивные показываются последними - - - 1 - - -
-
-
-
-
- - - -
-
-

Порталы

Публичные базы знаний для клиентов.

- -
-
-
- -

Создайте первый портал поддержки

-

Портал — публичная страница с инструкциями и ответами на частые вопросы. Клиент читает статью и, если не нашёл ответ, пишет в чат прямо со страницы.

-
- - -
-
-
-
-
- - - -
- - - -
- - Ателье и мерки - - Редакция 8 · черновик - - - - - - - -
- -
- -
-
- -
Адрес:/articles/kak-snyat-merki
-
-
- - - - - - - - GFM -
-
-
{{ mdSource }}
- -
- - Отпустите, чтобы вставить в текст - Изображения вставятся картинкой, документы — ссылкой. PNG, JPG, WebP, PDF, DOCX · до 25 МБ -
-
-
-
- 4 280 знаков · 12 абзацевСтрока 18, столбец 24Черновик сохранён автоматически в 11:24 -
-
- - -
-
- ПРЕДПРОСМОТР · ТЕМА ПОРТАЛА CLASSIC - - -
-
-
-
Помощь/Ателье и мерки
-

Как снять мерки

-

Понадобится сантиметровая лента и помощник. Мерки снимают по белью: лента ложится плотно, но не стягивает.

-
- - measure-points.png -
-

Точки замера: грудь, талия, бёдра

-

Обхват груди

-
    -
  1. Лента идёт по самым выступающим точкам.
  2. -
  3. Сзади — горизонтально, без перекосов.
  4. -
  5. Запишите значение с точностью до 0,5 см.
  6. -
-
Если вы между двумя размерами — выбирайте больший: ушить проще, чем расставить.
- - - - - - -
МеркаОбозначениеТочность
Обхват грудиОГ0,5 см
Обхват талииОТ0,5 см
-
-
-
- - -
-
- - - -
- -
-
ФАЙЛЫ СТАТЬИ
-
- -

Перетащите файлы

-

или выберите на диске · до 25 МБ

-
-
- -
- - {{ file.name }}{{ file.meta }} - - - - - - -
-
-
-
- -
-
ВЕРСИИнеизменяемые
-
- - - -
-

Публикация выбирает одну редакцию. Посетители видят только опубликованную.

-
-
-
-
- - - -
- - - - -
-
-
РАЗДЕЛЫ
- -
- -
-
- -
-
-
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - -
СТАТЬЯРАЗДЕЛЯЗЫКРЕДАКЦИЯСТАТУСОБНОВЛЕНО
- - - {{ a.title }}{{ a.slug }} - {{ a.attach }} - - {{ a.cat }}{{ a.lang }}{{ a.rev }}{{ a.revNote }}{{ a.status }}{{ a.updated }}
-
- 48 статей · показаны 6 · агент отвечает только по опубликованным - - 1 - - - -
-
-
-
-
- - - -
- - -
-
- - -

Основные

Название, публичный адрес и язык материалов портала.

-
- -
- Адрес на этой установке -
- - .help.nord-crm.ru -
- Строчные латинские буквы, цифры и дефис. Базовый домен help.nord-crm.ru задан администратором при установке и одинаков для всех порталов этой инсталляции. Адрес входит в ссылки на статьи — после смены старые ссылки перестанут работать. -
- -
- Итоговая ссылкаhttps://nord.help.nord-crm.ru/ - - -
-
-
- - -

Свой домен

Портал будет открываться по адресу вашей компании. Адрес установки продолжит работать.

-
-
- - Домен ведёт сюда · проверено 3 сентября -
-
-
Запись у регистратора доменаобновление DNS занимает до 24 часов
- - - - - - - - - - - - -
ТИПИМЯЗНАЧЕНИЕ
{{ d.type }}{{ d.name }}{{ d.value }}
-

Больше ничего добавлять не нужно: домен ваш, установка ваша — проверка лишь смотрит, ведёт ли запись на этот сервер, и выписывает сертификат.

-
-
- - - - -
-
-
- - -

Оформление

Тема публичных страниц и цветовая схема. Применяется до первого кадра — посетитель не видит смены оформления. Список тем — из сборки установки.

-
-
- Тема -
- - - -
-
-
- Цветовая схема - - - - - -
-
- - - - сохранено 5 сентября, 11:04 -
-
-
- -
-
-
-
- -
- - -
-
- -
-