@nadicodeai/design-system 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -41,4 +41,4 @@ Handoff gate: `npm run lint:design && npm run lint && npm run build && npm run t
41
41
  - Authoring is contract-first: a visual behavior comes from `DESIGN.md`, from our own shipped surfaces, or from an explicit user approval — never from another product's rendered output (ADR 0047). Screenshots verify, they never author. Ask when the contract leaves a decision open, and stop and re-read `DESIGN.md` before a second visual patch to the same component.
42
42
  - Name everything as NadicodeAI-owned primitives. A source brand appears only in a `../docs/adr/` decision record, as the history of what once inspired a choice; never in a name, a comment, a contract, or any living design document (ADR 0047).
43
43
  - Any change to package-consumed output ends with an explicit release decision, a version bump or a `[no-publish]` commit marker (enforced by the pre-push release gate; procedure: `../docs/internal/npm-publishing.md`).
44
- - The deleted `design-canvas/` prototypes live only in git history. `src/css/motion.css` owns shared runtime motion primitives, while `src/js/chat-autoplay.js` is the single allowed runtime JS enhancer; never reconstruct the prototypes or add a second runtime JS surface.
44
+ - The deleted `design-canvas/` prototypes and static chat/autoplay surface live only in git history. `src/css/motion.css` owns shared runtime motion primitives; never reconstruct the prototypes or add a runtime JS surface.
package/DESIGN.md CHANGED
@@ -482,7 +482,7 @@ Kernel compositions arrange approved primitives for a content role. They must no
482
482
  Each reusable concern has one home and one authoring surface, so a member is documented and shipped in exactly one place:
483
483
 
484
484
  - **Tokens** — exact reusable values live in this contract's `colors`, `spacing`, `rounded`, and `typography`; they export to the generated token layer (with a hand-authored local token layer beside it). No component or section may re-encode a token value.
485
- - **Framework-agnostic CSS components** — the brand/display CSS components that remain in this package (the agent-chat family, the run-state `state-tag` family, the entity-status `status-tag` family, `image`, and the small label/badge/link metadata components). Their names and semantics live once in `## Components`; design-system CSS realises that catalog without a second recipe or component-token hierarchy.
485
+ - **Framework-agnostic CSS components** — the brand/display CSS components that remain in this package (agent-work artifacts, the run-state `state-tag` family, the entity-status `status-tag` family, `image`, and the small label/badge/link metadata components). Their names and semantics live once in `## Components`; design-system CSS realises that catalog without a second recipe or component-token hierarchy.
486
486
  - **Reusable compositions and sections** — patterns that arrange the page grammar and lower tiers for a content role. They ship as React shadcn compositions from `@nadicodeai/ui`, composed over the kept page grammar; they are not a pure-CSS section layer and do not add a second component catalog here.
487
487
  - **Layout grammar** — the page-frame, row, lane, seam, stage, and marker system documented across this `## Layout` section, carried by the layout CSS layer.
488
488
  - **Specimens** — static demonstrations of already-defined primitives. They invent no geometry, seams, heights, or responsive behavior; they only assemble shipped tiers for inspection.
@@ -563,7 +563,7 @@ On coarse or non-hover input capability, every interactive target is at least 44
563
563
  Corners are restrained and assigned by material role.
564
564
 
565
565
  - Use `{rounded.none}` for page rows, frame edges, grid cells, and structural modules.
566
- - Use `{rounded.md}` (6 px) for resting content surfaces, controls, buttons, chips, status tags, tooltips, chat bubbles, and small artifacts.
566
+ - Use `{rounded.md}` (6 px) for resting content surfaces, controls, buttons, chips, status tags, tooltips, and small artifacts.
567
567
  - Use `{rounded.2xl}` (12 px) for menus, popovers, and dialogs.
568
568
  - Use `{rounded.3xl}` (16 px) for inset sheets and takeovers.
569
569
  - Use `{rounded.full}` only for intrinsically circular identity/avatar chrome and compact control marks whose geometry is inherently round or pill-shaped. It never turns a content surface, button, badge, or navigation item into a pill by default.
@@ -596,15 +596,13 @@ Interactive CSS components must expose a visible `:focus-visible` state using th
596
596
 
597
597
  Content surfaces display specific content types inside the grammar. They are valid when the content exists; they do not replace the page frame, row model, or seam rules.
598
598
 
599
- **`image`** — editorial media mount, the single home for art-directed imagery inside the grammar. A bounded media fill (img, picture, video, or inline SVG) with an optional mono tag chip overlaid on the media and an optional body-tone caption bar below it, both typed by the generated caption utilities applied in markup. Square-cornered (`{rounded.none}`): a mounted print, not a floating card — the parent frame supplies the visual system. With `data-frame="true"` it draws exactly one hairline (its own `{colors.line}` border), so it sits inside a borderless `panel-cell` or a visual slot without repairing or stacking onto any grid seam. The aspect ratio is the per-instance `--nc-image-ratio` knob (default 4 / 3) — set the prop on the element, never re-derive geometry inline. Variant: `data-strip="true"` is the full-width divider form for thin editorial bands between page rows (thread, texture, panorama) — the ratio unlocks and the height resolves from the responsive `{spacing.h-strip-*}` tokens through the layout-layer collapsed alias. Use `image` for every illustrated image, photographic surface, or image divider; use `artifact-preview` when the visual is an agent-produced document surface.
599
+ **`image`** — editorial media mount, the single home for art-directed imagery inside the grammar. A bounded media fill (img, picture, video, or inline SVG) with an optional mono tag chip overlaid on the media and an optional body-tone caption bar below it, both typed by the generated caption utilities applied in markup. Square-cornered (`{rounded.none}`): a mounted print, not a floating card — the parent frame supplies the visual system. With `data-frame="true"` it draws exactly one hairline (its own `{colors.line}` border), so it sits inside a borderless `panel-cell` or a visual slot without repairing or stacking onto any grid seam. The aspect ratio is the per-instance `--nc-image-ratio` knob (default 4 / 3) — set the prop on the element, never re-derive geometry inline. Variant: `data-strip="true"` is the full-width divider form for thin editorial bands between page rows (thread, texture, panorama) — the ratio unlocks and the height resolves from the responsive `{spacing.h-strip-*}` tokens through the layout-layer collapsed alias. Use `image` for every illustrated image, photographic surface, or image divider. Conversation artifacts render through assistant-ui rather than a design-system CSS family.
600
600
 
601
601
  ### Agentic Work Surfaces
602
602
 
603
- Agentic work surfaces are the NadicodeAI-specific layer. They show people and agents working together through conversations, artifacts, approvals, memory, state, tools, and handoffs. Use these when there is concrete work output or workflow state to inspect.
603
+ Agentic work surfaces are the NadicodeAI-specific display layer outside the conversation renderer. They show concrete work output or workflow state such as memory, state, run progress, identity, and handoffs. Conversation messages, tools, artifacts, approvals, and pending indicators belong to assistant-ui.
604
604
 
605
- **`artifact-surface`** — inspectable output area. Use for proposal previews, reports, plans, spreadsheets, schedules, approval states, source traces, run logs, workflow maps, and conversation artifacts.
606
-
607
- **`artifact-preview`** — compact inspectable artifact card for an agent-produced document, sheet, report, or code excerpt. The header identifies the artifact kind and the body carries the preview; use `artifact-surface` for a larger work area.
605
+ **`artifact-surface`** — inspectable output area outside a conversation. Use for proposal previews, reports, plans, spreadsheets, schedules, run logs, and workflow maps.
608
606
 
609
607
  Status-tag variants share the same compact sans rectangle at `{typography.caption-strong}`, `{rounded.md}` — a small subtle-tinted rectangle, never a capsule — and differentiate by tone: a paired background+text ramp step, not a border-left rail. Tone is `[data-tone]`, not BEM modifiers. Gray is the default tone; color is reserved for marking health and exceptions, so every terminal or no-action entity status (inactive, archived, rejected, canceled, expired, revoked, denied) renders gray. Subtle-only (no solid/filled variant), no icons inside the badge, no text-transform; labels render verbatim from the source's own i18n dictionary. `status-tag` is the entity-status vocabulary — agent, credential, activation, customer, invitation, subscription, and health values that live on a record — and stays a sibling of `state-tag`: the two families never blend, and `status-tag` never touches the `{colors.state-*}` tokens.
610
608
 
@@ -618,14 +616,6 @@ Status-tag variants share the same compact sans rectangle at `{typography.captio
618
616
 
619
617
  **`status-tag-blue`** — an in-progress, pending, or trial entity status still moving toward a resolution.
620
618
 
621
- **`chat-thread`** — conversation container showing how people instruct, correct, review, or approve agents. Static markup always renders the complete conversation. The package's optional `chat-autoplay.js` progressive enhancer activates only on `data-nc-autoplay`, reveals messages in order while keeping the newest turn visible, loops while the thread is in view, and injects its own pause/resume control. It shows the complete thread immediately when reduced motion is requested. Consumers never author the enhancer control in static markup.
622
-
623
- **`chat-bubble-user`** — human message bubble. Dark surface, white text.
624
-
625
- **`chat-bubble-agent`** — agent message bubble. Light surface, ink text.
626
-
627
- **`tool-call-trace`** — compact code/trace row. Use only when tool use is relevant to the explanation.
628
-
629
619
  State-tag variants share the same compact rectangular shape at `{typography.caption-mono}` and differentiate by a 3 px colored `border-left` consuming the matching `{colors.state-*}` token. Shape carries meaning; color carries variant.
630
620
 
631
621
  **`state-tag-ready`** — workflow state tag for work that is ready or waiting to start. Uses `{colors.state-ready}` for the left edge.
@@ -640,12 +630,6 @@ State-tag variants share the same compact rectangular shape at `{typography.capt
640
630
 
641
631
  **`state-row`** — full-width workflow-state divider inside an agentic work surface. Its bounded hairlines belong to the row and its dot consumes the same state token as the matching `state-tag` variant.
642
632
 
643
- **`typing-dots`** — compact agent typing indicator. It uses the agent accent rail and the shared motion hooks; reduced-motion behavior comes from the package motion layer.
644
-
645
- **`approval-gate`** — human-in-the-loop pause surface. Use when an agent action requires explicit review, correction, approval, or rejection.
646
-
647
- **`agent-avatar`** — identity slot for an agent personality or role. The chrome is reusable; the visual asset is project-specific.
648
-
649
633
  **`usage-meter`** — token, cost, latency, or run-count display.
650
634
 
651
635
  **`run-timeline`** — vertical step thread for an agent run, handoff, or workflow execution.
package/README.md CHANGED
@@ -84,14 +84,6 @@ Consumer overrides belong in an explicit later layer:
84
84
 
85
85
  The exact layer order and ownership rules are defined in `DESIGN.md`.
86
86
 
87
- ### Optional chat auto-play
88
-
89
- Static chat markup needs no JavaScript. To opt a `data-nc-autoplay` thread into
90
- progressive playback, include the public
91
- `@nadicodeai/design-system/css/chat-autoplay.js` export. The `chat-thread`
92
- entry in [`DESIGN.md`](./DESIGN.md#agentic-work-surfaces) owns the behavior and
93
- accessibility contract.
94
-
95
87
  ### Worked example (static HTML)
96
88
 
97
89
  ```html
@@ -109,26 +101,13 @@ accessibility contract.
109
101
  <div class="nc-page-frame">
110
102
  <section class="nc-page-row">
111
103
  <div class="nc-row-lane nc-row-lane-active">
112
- <!-- A playing chat thread: opt in with data-nc-autoplay -->
113
- <div class="nc-chat-thread" data-nc-autoplay>
114
- <div class="nc-chat-thread-body">
115
- <div class="nc-chat-bubble-user">Which contract template should I use?</div>
116
- <div class="nc-chat-bubble-agent" data-accent="green">
117
- Use the approved 2026 revision in Drive · Legal · Templates. It already has the updated privacy clauses.
118
- </div>
119
- </div>
120
- </div>
104
+ <div class="nc-artifact-surface">Inspectable agent output</div>
121
105
  </div>
122
106
  </section>
123
107
  </div>
124
108
  </main>
125
-
126
- <!-- Optional: omit this script and the thread above stays a full static conversation. -->
127
- <script src="https://unpkg.com/@nadicodeai/design-system/css/chat-autoplay.js"></script>
128
109
  </body>
129
110
  </html>
130
111
  ```
131
112
 
132
- `examples/chat-showcase.html` is the live proof page for the chat CSS components and auto-play enhancer.
133
-
134
113
  Use the monorepo's `brand/brand-book/` for NadicodeAI story, positioning, buyer, promise, method, use cases, technology story, trust doctrine, identity, voice, visual direction, sales behavior, language, and boundaries.
@@ -1753,265 +1753,6 @@
1753
1753
  }
1754
1754
  }
1755
1755
 
1756
- /* src/css/components/chat-thread.css */
1757
- @layer components {
1758
- .nc-chat-thread {
1759
- display: flex;
1760
- flex-direction: column;
1761
- overflow: hidden;
1762
- border: var(--nc-guide) solid var(--nc-line);
1763
- border-radius: var(--nc-rounded-md);
1764
- background: var(--nc-canvas);
1765
- color: var(--nc-ink);
1766
- font-family:
1767
- "Geist",
1768
- "Inter",
1769
- system-ui,
1770
- -apple-system,
1771
- "Segoe UI",
1772
- sans-serif;
1773
- }
1774
- .nc-chat-thread-header {
1775
- display: flex;
1776
- align-items: center;
1777
- gap: 12px;
1778
- padding: 14px 16px;
1779
- border-bottom: var(--nc-guide) solid var(--nc-line);
1780
- background: var(--nc-canvas);
1781
- }
1782
- .nc-chat-thread-domain {
1783
- font-family:
1784
- "Geist Mono",
1785
- ui-monospace,
1786
- SFMono-Regular,
1787
- Menlo,
1788
- Monaco,
1789
- monospace;
1790
- font-size: 11px;
1791
- font-weight: 500;
1792
- line-height: 16px;
1793
- color: var(--nc-muted);
1794
- text-transform: uppercase;
1795
- letter-spacing: 0.04em;
1796
- white-space: nowrap;
1797
- overflow: hidden;
1798
- text-overflow: ellipsis;
1799
- }
1800
- .nc-chat-thread-identity {
1801
- min-width: 0;
1802
- flex: 1;
1803
- }
1804
- .nc-chat-thread-name {
1805
- display: flex;
1806
- align-items: baseline;
1807
- gap: 6px;
1808
- font-size: 14px;
1809
- font-weight: 500;
1810
- line-height: 20px;
1811
- color: var(--nc-ink);
1812
- white-space: nowrap;
1813
- overflow: hidden;
1814
- text-overflow: ellipsis;
1815
- }
1816
- .nc-chat-thread-name .nc-chat-thread-role {
1817
- min-width: 0;
1818
- overflow: hidden;
1819
- font-weight: 400;
1820
- color: var(--nc-muted);
1821
- text-overflow: ellipsis;
1822
- white-space: nowrap;
1823
- }
1824
- .nc-chat-thread-workflow {
1825
- flex: 0 1 auto;
1826
- min-width: 0;
1827
- overflow: hidden;
1828
- font-family:
1829
- "Geist Mono",
1830
- ui-monospace,
1831
- SFMono-Regular,
1832
- Menlo,
1833
- Monaco,
1834
- monospace;
1835
- font-size: 11px;
1836
- line-height: 16px;
1837
- color: var(--nc-body);
1838
- text-transform: uppercase;
1839
- letter-spacing: 0.04em;
1840
- text-overflow: ellipsis;
1841
- white-space: nowrap;
1842
- }
1843
- .nc-chat-thread-body {
1844
- flex: 1;
1845
- min-height: 0;
1846
- overflow: hidden auto;
1847
- display: flex;
1848
- flex-direction: column;
1849
- gap: 10px;
1850
- padding: 16px 18px;
1851
- background: radial-gradient(var(--nc-line) 0.8px, transparent 0.8px) 8px 8px / 12px 12px, var(--nc-canvas);
1852
- }
1853
- .nc-chat-pause {
1854
- display: block;
1855
- align-self: flex-end;
1856
- margin: var(--nc-xs) var(--nc-xs) 0;
1857
- padding: var(--nc-xs) var(--nc-sm);
1858
- background: var(--nc-canvas);
1859
- color: var(--nc-muted);
1860
- border: var(--nc-guide) solid var(--nc-line);
1861
- border-radius: var(--nc-rounded-xs);
1862
- font-family: var(--nc-type-caption-mono-font-family);
1863
- font-size: var(--nc-type-caption-mono-font-size);
1864
- font-weight: var(--nc-type-caption-mono-font-weight);
1865
- line-height: var(--nc-type-caption-mono-line-height);
1866
- letter-spacing: var(--nc-type-caption-mono-letter-spacing);
1867
- cursor: pointer;
1868
- transition:
1869
- background-color var(--nc-duration-confirm) var(--nc-ease-out-strong),
1870
- border-color var(--nc-duration-confirm) var(--nc-ease-out-strong),
1871
- color var(--nc-duration-confirm) var(--nc-ease-out-strong),
1872
- opacity var(--nc-duration-confirm) var(--nc-ease-out-strong);
1873
- }
1874
- @media (hover: hover) and (pointer: fine) {
1875
- .nc-chat-pause:hover {
1876
- color: var(--nc-ink);
1877
- border-color: var(--nc-body);
1878
- }
1879
- }
1880
- .nc-chat-pause[aria-pressed=true] {
1881
- color: var(--nc-ink);
1882
- border-color: var(--nc-body);
1883
- }
1884
- .nc-chat-pause:active {
1885
- opacity: 0.9;
1886
- }
1887
- .nc-chat-thread-footer {
1888
- display: flex;
1889
- flex-direction: column;
1890
- gap: 8px;
1891
- padding: 10px 16px;
1892
- border-top: var(--nc-guide) solid var(--nc-line);
1893
- background: var(--nc-canvas);
1894
- }
1895
- }
1896
-
1897
- /* src/css/components/chat-bubble-user.css */
1898
- @layer components {
1899
- .nc-chat-bubble-user {
1900
- align-self: flex-end;
1901
- width: fit-content;
1902
- max-width: 84%;
1903
- padding: 9px 13px;
1904
- border: var(--nc-guide) solid var(--nc-ink);
1905
- border-radius: var(--nc-rounded-md);
1906
- background: var(--nc-ink);
1907
- color: var(--nc-canvas);
1908
- font-family:
1909
- "Geist",
1910
- "Inter",
1911
- system-ui,
1912
- -apple-system,
1913
- "Segoe UI",
1914
- sans-serif;
1915
- font-size: 14px;
1916
- line-height: 20px;
1917
- white-space: pre-wrap;
1918
- word-break: break-word;
1919
- animation-name: nc-msg-in;
1920
- }
1921
- }
1922
-
1923
- /* src/css/components/chat-bubble-agent.css */
1924
- @layer components {
1925
- .nc-chat-bubble-agent {
1926
- --nc-accent: var(--nc-link);
1927
- align-self: flex-start;
1928
- width: fit-content;
1929
- max-width: 84%;
1930
- padding: 9px 13px 9px 15px;
1931
- border: var(--nc-guide) solid var(--nc-line);
1932
- border-radius: var(--nc-rounded-md);
1933
- box-shadow: inset 3px 0 0 0 var(--nc-accent);
1934
- background: var(--nc-canvas);
1935
- color: var(--nc-ink);
1936
- font-family:
1937
- "Geist",
1938
- "Inter",
1939
- system-ui,
1940
- -apple-system,
1941
- "Segoe UI",
1942
- sans-serif;
1943
- font-size: 14px;
1944
- line-height: 20px;
1945
- white-space: pre-wrap;
1946
- word-break: break-word;
1947
- animation-name: nc-msg-in;
1948
- }
1949
- .nc-chat-bubble-agent[data-accent=green] {
1950
- --nc-accent: var(--nc-link);
1951
- }
1952
- .nc-chat-bubble-agent[data-accent=cyan] {
1953
- --nc-accent: var(--nc-info);
1954
- }
1955
- .nc-chat-bubble-agent[data-accent=amber] {
1956
- --nc-accent: var(--nc-warning-deep);
1957
- }
1958
- .nc-chat-bubble-agent[data-accent=ink] {
1959
- --nc-accent: var(--nc-ink);
1960
- }
1961
- .nc-chat-bubble-agent[data-accent=red] {
1962
- --nc-accent: var(--nc-error);
1963
- }
1964
- .nc-chat-bubble-agent code {
1965
- padding: 1px 5px;
1966
- border-radius: var(--nc-rounded-xs);
1967
- background: var(--nc-canvas-soft-2);
1968
- color: var(--nc-ink);
1969
- font-family:
1970
- "Geist Mono",
1971
- ui-monospace,
1972
- SFMono-Regular,
1973
- Menlo,
1974
- Monaco,
1975
- monospace;
1976
- font-size: 12px;
1977
- }
1978
- }
1979
-
1980
- /* src/css/components/tool-call-trace.css */
1981
- @layer components {
1982
- .nc-tool-call-trace {
1983
- align-self: flex-start;
1984
- width: fit-content;
1985
- max-width: 92%;
1986
- padding: 6px 10px;
1987
- border: var(--nc-guide) solid var(--nc-line);
1988
- border-radius: var(--nc-rounded-sm);
1989
- background: var(--nc-canvas-soft-2);
1990
- color: var(--nc-body);
1991
- font-family:
1992
- "Geist Mono",
1993
- ui-monospace,
1994
- SFMono-Regular,
1995
- Menlo,
1996
- Monaco,
1997
- monospace;
1998
- font-size: 12px;
1999
- line-height: 17px;
2000
- animation-name: nc-msg-in;
2001
- }
2002
- .nc-tool-call-trace::before {
2003
- content: "";
2004
- display: inline-block;
2005
- width: 13px;
2006
- height: 13px;
2007
- margin-right: 8px;
2008
- vertical-align: -2px;
2009
- background-color: var(--nc-muted);
2010
- -webkit-mask: url('data:image/svg+xml,<!-- @license lucide-static v1.21.0 - ISC -->%0A<svg%0A class="lucide lucide-chevron-right"%0A xmlns="http://www.w3.org/2000/svg"%0A width="24"%0A height="24"%0A viewBox="0 0 24 24"%0A fill="none"%0A stroke="currentColor"%0A stroke-width="1.6"%0A stroke-linecap="round"%0A stroke-linejoin="round"%0A>%0A <path d="m9 18 6-6-6-6" />%0A</svg>%0A') center / contain no-repeat;
2011
- mask: url('data:image/svg+xml,<!-- @license lucide-static v1.21.0 - ISC -->%0A<svg%0A class="lucide lucide-chevron-right"%0A xmlns="http://www.w3.org/2000/svg"%0A width="24"%0A height="24"%0A viewBox="0 0 24 24"%0A fill="none"%0A stroke="currentColor"%0A stroke-width="1.6"%0A stroke-linecap="round"%0A stroke-linejoin="round"%0A>%0A <path d="m9 18 6-6-6-6" />%0A</svg>%0A') center / contain no-repeat;
2012
- }
2013
- }
2014
-
2015
1756
  /* src/css/components/memory-item.css */
2016
1757
  @layer components {
2017
1758
  .nc-memory-item {
@@ -2091,241 +1832,6 @@
2091
1832
  }
2092
1833
  }
2093
1834
 
2094
- /* src/css/components/approval-gate.css */
2095
- @layer components {
2096
- .nc-approval-gate {
2097
- align-self: stretch;
2098
- display: grid;
2099
- gap: 6px;
2100
- padding: 12px;
2101
- border: var(--nc-guide) solid color-mix(in srgb, var(--nc-warning-deep) 40%, var(--nc-warning-soft));
2102
- border-radius: var(--nc-rounded-md);
2103
- background: var(--nc-warning-soft);
2104
- color: var(--nc-ink);
2105
- font-family:
2106
- "Geist",
2107
- "Inter",
2108
- system-ui,
2109
- -apple-system,
2110
- "Segoe UI",
2111
- sans-serif;
2112
- animation-name: nc-msg-in;
2113
- transition:
2114
- background-color var(--nc-duration-confirm) var(--nc-ease-out-strong),
2115
- border-color var(--nc-duration-confirm) var(--nc-ease-out-strong),
2116
- color var(--nc-duration-confirm) var(--nc-ease-out-strong);
2117
- }
2118
- .nc-approval-gate-label {
2119
- font-family:
2120
- "Geist Mono",
2121
- ui-monospace,
2122
- SFMono-Regular,
2123
- Menlo,
2124
- Monaco,
2125
- monospace;
2126
- font-size: 10px;
2127
- font-weight: 600;
2128
- line-height: 16px;
2129
- color: var(--nc-warning-deep);
2130
- text-transform: uppercase;
2131
- letter-spacing: 0.06em;
2132
- }
2133
- .nc-approval-gate-title {
2134
- font-size: 14px;
2135
- font-weight: 500;
2136
- line-height: 20px;
2137
- color: var(--nc-ink);
2138
- }
2139
- .nc-approval-gate-copy {
2140
- font-size: 13px;
2141
- line-height: 18px;
2142
- color: var(--nc-body);
2143
- }
2144
- .nc-approval-gate-actions {
2145
- display: flex;
2146
- flex-wrap: wrap;
2147
- gap: 6px;
2148
- margin-top: 2px;
2149
- }
2150
- .nc-approval-gate-actions button {
2151
- padding: 5px 10px;
2152
- border: var(--nc-guide) solid var(--nc-ink);
2153
- border-radius: var(--nc-rounded-sm);
2154
- background: var(--nc-ink);
2155
- color: var(--nc-canvas);
2156
- font-family: inherit;
2157
- font-size: 12px;
2158
- font-weight: 500;
2159
- line-height: 16px;
2160
- cursor: pointer;
2161
- }
2162
- .nc-approval-gate-actions button[data-variant=secondary] {
2163
- border-color: var(--nc-line);
2164
- background: var(--nc-canvas);
2165
- color: var(--nc-ink);
2166
- }
2167
- .nc-approval-gate-actions button[data-variant=outline] {
2168
- border-color: var(--nc-line);
2169
- background: transparent;
2170
- color: var(--nc-ink);
2171
- }
2172
- .nc-approval-gate-actions button[data-variant=destructive] {
2173
- border-color: var(--nc-error);
2174
- background: var(--nc-canvas);
2175
- color: var(--nc-error-deep);
2176
- }
2177
- .nc-approval-gate-actions button::before {
2178
- content: "";
2179
- display: inline-block;
2180
- width: 13px;
2181
- height: 13px;
2182
- margin-right: 5px;
2183
- vertical-align: -2px;
2184
- background-color: currentColor;
2185
- -webkit-mask: url('data:image/svg+xml,<!-- @license lucide-static v1.21.0 - ISC -->%0A<svg%0A class="lucide lucide-check"%0A xmlns="http://www.w3.org/2000/svg"%0A width="24"%0A height="24"%0A viewBox="0 0 24 24"%0A fill="none"%0A stroke="currentColor"%0A stroke-width="1.6"%0A stroke-linecap="round"%0A stroke-linejoin="round"%0A>%0A <path d="M20 6 9 17l-5-5" />%0A</svg>%0A') center / contain no-repeat;
2186
- mask: url('data:image/svg+xml,<!-- @license lucide-static v1.21.0 - ISC -->%0A<svg%0A class="lucide lucide-check"%0A xmlns="http://www.w3.org/2000/svg"%0A width="24"%0A height="24"%0A viewBox="0 0 24 24"%0A fill="none"%0A stroke="currentColor"%0A stroke-width="1.6"%0A stroke-linecap="round"%0A stroke-linejoin="round"%0A>%0A <path d="M20 6 9 17l-5-5" />%0A</svg>%0A') center / contain no-repeat;
2187
- }
2188
- .nc-approval-gate-actions button[data-variant=secondary]::before {
2189
- -webkit-mask-image: url('data:image/svg+xml,<!-- @license lucide-static v1.21.0 - ISC -->%0A<svg%0A class="lucide lucide-square-pen"%0A xmlns="http://www.w3.org/2000/svg"%0A width="24"%0A height="24"%0A viewBox="0 0 24 24"%0A fill="none"%0A stroke="currentColor"%0A stroke-width="1.6"%0A stroke-linecap="round"%0A stroke-linejoin="round"%0A>%0A <path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />%0A <path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z" />%0A</svg>%0A');
2190
- mask-image: url('data:image/svg+xml,<!-- @license lucide-static v1.21.0 - ISC -->%0A<svg%0A class="lucide lucide-square-pen"%0A xmlns="http://www.w3.org/2000/svg"%0A width="24"%0A height="24"%0A viewBox="0 0 24 24"%0A fill="none"%0A stroke="currentColor"%0A stroke-width="1.6"%0A stroke-linecap="round"%0A stroke-linejoin="round"%0A>%0A <path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />%0A <path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z" />%0A</svg>%0A');
2191
- }
2192
- .nc-approval-gate-actions button[data-approval-choice=deny]::before {
2193
- -webkit-mask-image: url('data:image/svg+xml,<!-- @license lucide-static v1.21.0 - ISC -->%0A<svg%0A class="lucide lucide-ban"%0A xmlns="http://www.w3.org/2000/svg"%0A width="24"%0A height="24"%0A viewBox="0 0 24 24"%0A fill="none"%0A stroke="currentColor"%0A stroke-width="1.6"%0A stroke-linecap="round"%0A stroke-linejoin="round"%0A>%0A <circle cx="12" cy="12" r="10" />%0A <path d="M4.929 4.929 19.07 19.071" />%0A</svg>%0A');
2194
- mask-image: url('data:image/svg+xml,<!-- @license lucide-static v1.21.0 - ISC -->%0A<svg%0A class="lucide lucide-ban"%0A xmlns="http://www.w3.org/2000/svg"%0A width="24"%0A height="24"%0A viewBox="0 0 24 24"%0A fill="none"%0A stroke="currentColor"%0A stroke-width="1.6"%0A stroke-linecap="round"%0A stroke-linejoin="round"%0A>%0A <circle cx="12" cy="12" r="10" />%0A <path d="M4.929 4.929 19.07 19.071" />%0A</svg>%0A');
2195
- }
2196
- .nc-approval-gate-actions button[data-approval-choice=session]::before {
2197
- -webkit-mask-image: url('data:image/svg+xml,<!-- @license lucide-static v1.21.0 - ISC -->%0A<svg%0A class="lucide lucide-clock"%0A xmlns="http://www.w3.org/2000/svg"%0A width="24"%0A height="24"%0A viewBox="0 0 24 24"%0A fill="none"%0A stroke="currentColor"%0A stroke-width="1.6"%0A stroke-linecap="round"%0A stroke-linejoin="round"%0A>%0A <circle cx="12" cy="12" r="10" />%0A <path d="M12 6v6l4 2" />%0A</svg>%0A');
2198
- mask-image: url('data:image/svg+xml,<!-- @license lucide-static v1.21.0 - ISC -->%0A<svg%0A class="lucide lucide-clock"%0A xmlns="http://www.w3.org/2000/svg"%0A width="24"%0A height="24"%0A viewBox="0 0 24 24"%0A fill="none"%0A stroke="currentColor"%0A stroke-width="1.6"%0A stroke-linecap="round"%0A stroke-linejoin="round"%0A>%0A <circle cx="12" cy="12" r="10" />%0A <path d="M12 6v6l4 2" />%0A</svg>%0A');
2199
- }
2200
- .nc-approval-gate-actions button[data-approval-choice=always]::before {
2201
- -webkit-mask-image: url('data:image/svg+xml,<!-- @license lucide-static v1.21.0 - ISC -->%0A<svg%0A class="lucide lucide-shield"%0A xmlns="http://www.w3.org/2000/svg"%0A width="24"%0A height="24"%0A viewBox="0 0 24 24"%0A fill="none"%0A stroke="currentColor"%0A stroke-width="1.6"%0A stroke-linecap="round"%0A stroke-linejoin="round"%0A>%0A <path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" />%0A</svg>%0A');
2202
- mask-image: url('data:image/svg+xml,<!-- @license lucide-static v1.21.0 - ISC -->%0A<svg%0A class="lucide lucide-shield"%0A xmlns="http://www.w3.org/2000/svg"%0A width="24"%0A height="24"%0A viewBox="0 0 24 24"%0A fill="none"%0A stroke="currentColor"%0A stroke-width="1.6"%0A stroke-linecap="round"%0A stroke-linejoin="round"%0A>%0A <path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" />%0A</svg>%0A');
2203
- }
2204
- .nc-approval-gate-actions button:focus-visible {
2205
- outline: var(--nc-focus-ring-width) solid var(--nc-focus-ring);
2206
- outline-offset: 0;
2207
- }
2208
- }
2209
-
2210
- /* src/css/components/artifact-preview.css */
2211
- @layer components {
2212
- .nc-artifact-preview {
2213
- align-self: stretch;
2214
- overflow: hidden;
2215
- border: var(--nc-guide) solid var(--nc-line);
2216
- border-radius: var(--nc-rounded-md);
2217
- background: var(--nc-canvas);
2218
- font-family:
2219
- "Geist",
2220
- "Inter",
2221
- system-ui,
2222
- -apple-system,
2223
- "Segoe UI",
2224
- sans-serif;
2225
- animation-name: nc-msg-in;
2226
- }
2227
- .nc-artifact-preview-header {
2228
- display: flex;
2229
- align-items: center;
2230
- gap: 8px;
2231
- padding: 7px 10px;
2232
- border-bottom: var(--nc-guide) solid var(--nc-line);
2233
- background: var(--nc-canvas-soft);
2234
- }
2235
- .nc-artifact-preview-header::before {
2236
- content: "";
2237
- flex: none;
2238
- width: 14px;
2239
- height: 14px;
2240
- background-color: var(--nc-muted);
2241
- -webkit-mask: url('data:image/svg+xml,<!-- @license lucide-static v1.21.0 - ISC -->%0A<svg%0A class="lucide lucide-file-text"%0A xmlns="http://www.w3.org/2000/svg"%0A width="24"%0A height="24"%0A viewBox="0 0 24 24"%0A fill="none"%0A stroke="currentColor"%0A stroke-width="1.6"%0A stroke-linecap="round"%0A stroke-linejoin="round"%0A>%0A <path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" />%0A <path d="M14 2v5a1 1 0 0 0 1 1h5" />%0A <path d="M10 9H8" />%0A <path d="M16 13H8" />%0A <path d="M16 17H8" />%0A</svg>%0A') center / contain no-repeat;
2242
- mask: url('data:image/svg+xml,<!-- @license lucide-static v1.21.0 - ISC -->%0A<svg%0A class="lucide lucide-file-text"%0A xmlns="http://www.w3.org/2000/svg"%0A width="24"%0A height="24"%0A viewBox="0 0 24 24"%0A fill="none"%0A stroke="currentColor"%0A stroke-width="1.6"%0A stroke-linecap="round"%0A stroke-linejoin="round"%0A>%0A <path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" />%0A <path d="M14 2v5a1 1 0 0 0 1 1h5" />%0A <path d="M10 9H8" />%0A <path d="M16 13H8" />%0A <path d="M16 17H8" />%0A</svg>%0A') center / contain no-repeat;
2243
- }
2244
- .nc-artifact-preview[data-kind=sheet] .nc-artifact-preview-header::before {
2245
- -webkit-mask-image: url('data:image/svg+xml,<!-- @license lucide-static v1.21.0 - ISC -->%0A<svg%0A class="lucide lucide-table"%0A xmlns="http://www.w3.org/2000/svg"%0A width="24"%0A height="24"%0A viewBox="0 0 24 24"%0A fill="none"%0A stroke="currentColor"%0A stroke-width="1.6"%0A stroke-linecap="round"%0A stroke-linejoin="round"%0A>%0A <path d="M12 3v18" />%0A <rect width="18" height="18" x="3" y="3" rx="2" />%0A <path d="M3 9h18" />%0A <path d="M3 15h18" />%0A</svg>%0A');
2246
- mask-image: url('data:image/svg+xml,<!-- @license lucide-static v1.21.0 - ISC -->%0A<svg%0A class="lucide lucide-table"%0A xmlns="http://www.w3.org/2000/svg"%0A width="24"%0A height="24"%0A viewBox="0 0 24 24"%0A fill="none"%0A stroke="currentColor"%0A stroke-width="1.6"%0A stroke-linecap="round"%0A stroke-linejoin="round"%0A>%0A <path d="M12 3v18" />%0A <rect width="18" height="18" x="3" y="3" rx="2" />%0A <path d="M3 9h18" />%0A <path d="M3 15h18" />%0A</svg>%0A');
2247
- }
2248
- .nc-artifact-preview[data-kind=code] .nc-artifact-preview-header::before {
2249
- -webkit-mask-image: url('data:image/svg+xml,<!-- @license lucide-static v1.21.0 - ISC -->%0A<svg%0A class="lucide lucide-code"%0A xmlns="http://www.w3.org/2000/svg"%0A width="24"%0A height="24"%0A viewBox="0 0 24 24"%0A fill="none"%0A stroke="currentColor"%0A stroke-width="1.6"%0A stroke-linecap="round"%0A stroke-linejoin="round"%0A>%0A <path d="m16 18 6-6-6-6" />%0A <path d="m8 6-6 6 6 6" />%0A</svg>%0A');
2250
- mask-image: url('data:image/svg+xml,<!-- @license lucide-static v1.21.0 - ISC -->%0A<svg%0A class="lucide lucide-code"%0A xmlns="http://www.w3.org/2000/svg"%0A width="24"%0A height="24"%0A viewBox="0 0 24 24"%0A fill="none"%0A stroke="currentColor"%0A stroke-width="1.6"%0A stroke-linecap="round"%0A stroke-linejoin="round"%0A>%0A <path d="m16 18 6-6-6-6" />%0A <path d="m8 6-6 6 6 6" />%0A</svg>%0A');
2251
- }
2252
- .nc-artifact-preview[data-kind=pdf] .nc-artifact-preview-header::before {
2253
- -webkit-mask-image: url('data:image/svg+xml,<!-- @license lucide-static v1.21.0 - ISC -->%0A<svg%0A class="lucide lucide-file"%0A xmlns="http://www.w3.org/2000/svg"%0A width="24"%0A height="24"%0A viewBox="0 0 24 24"%0A fill="none"%0A stroke="currentColor"%0A stroke-width="1.6"%0A stroke-linecap="round"%0A stroke-linejoin="round"%0A>%0A <path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" />%0A <path d="M14 2v5a1 1 0 0 0 1 1h5" />%0A</svg>%0A');
2254
- mask-image: url('data:image/svg+xml,<!-- @license lucide-static v1.21.0 - ISC -->%0A<svg%0A class="lucide lucide-file"%0A xmlns="http://www.w3.org/2000/svg"%0A width="24"%0A height="24"%0A viewBox="0 0 24 24"%0A fill="none"%0A stroke="currentColor"%0A stroke-width="1.6"%0A stroke-linecap="round"%0A stroke-linejoin="round"%0A>%0A <path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" />%0A <path d="M14 2v5a1 1 0 0 0 1 1h5" />%0A</svg>%0A');
2255
- }
2256
- .nc-artifact-preview[data-kind=report] .nc-artifact-preview-header::before {
2257
- -webkit-mask-image: url('data:image/svg+xml,<!-- @license lucide-static v1.21.0 - ISC -->%0A<svg%0A class="lucide lucide-chart-column"%0A xmlns="http://www.w3.org/2000/svg"%0A width="24"%0A height="24"%0A viewBox="0 0 24 24"%0A fill="none"%0A stroke="currentColor"%0A stroke-width="1.6"%0A stroke-linecap="round"%0A stroke-linejoin="round"%0A>%0A <path d="M3 3v16a2 2 0 0 0 2 2h16" />%0A <path d="M18 17V9" />%0A <path d="M13 17V5" />%0A <path d="M8 17v-3" />%0A</svg>%0A');
2258
- mask-image: url('data:image/svg+xml,<!-- @license lucide-static v1.21.0 - ISC -->%0A<svg%0A class="lucide lucide-chart-column"%0A xmlns="http://www.w3.org/2000/svg"%0A width="24"%0A height="24"%0A viewBox="0 0 24 24"%0A fill="none"%0A stroke="currentColor"%0A stroke-width="1.6"%0A stroke-linecap="round"%0A stroke-linejoin="round"%0A>%0A <path d="M3 3v16a2 2 0 0 0 2 2h16" />%0A <path d="M18 17V9" />%0A <path d="M13 17V5" />%0A <path d="M8 17v-3" />%0A</svg>%0A');
2259
- }
2260
- .nc-artifact-preview-title {
2261
- font-size: 13px;
2262
- font-weight: 500;
2263
- line-height: 18px;
2264
- color: var(--nc-ink);
2265
- }
2266
- .nc-artifact-preview-kind {
2267
- margin-left: auto;
2268
- font-family:
2269
- "Geist Mono",
2270
- ui-monospace,
2271
- SFMono-Regular,
2272
- Menlo,
2273
- Monaco,
2274
- monospace;
2275
- font-size: 10px;
2276
- font-weight: 500;
2277
- line-height: 16px;
2278
- color: var(--nc-muted);
2279
- text-transform: uppercase;
2280
- letter-spacing: 0.05em;
2281
- }
2282
- .nc-artifact-preview-body {
2283
- padding: 10px 12px;
2284
- color: var(--nc-body);
2285
- font-size: 13px;
2286
- line-height: 18px;
2287
- white-space: pre-wrap;
2288
- }
2289
- }
2290
-
2291
- /* src/css/components/typing-dots.css */
2292
- @layer components {
2293
- .nc-typing-dots {
2294
- --nc-accent: var(--nc-link);
2295
- align-self: flex-start;
2296
- display: inline-flex;
2297
- gap: 4px;
2298
- align-items: center;
2299
- width: fit-content;
2300
- padding: 10px 14px 10px 15px;
2301
- border: var(--nc-guide) solid var(--nc-line);
2302
- border-radius: var(--nc-rounded-md);
2303
- box-shadow: inset 3px 0 0 0 var(--nc-accent);
2304
- background: var(--nc-canvas);
2305
- }
2306
- .nc-typing-dots[data-accent=green] {
2307
- --nc-accent: var(--nc-link);
2308
- }
2309
- .nc-typing-dots[data-accent=cyan] {
2310
- --nc-accent: var(--nc-info);
2311
- }
2312
- .nc-typing-dots[data-accent=amber] {
2313
- --nc-accent: var(--nc-warning-deep);
2314
- }
2315
- .nc-typing-dots[data-accent=ink] {
2316
- --nc-accent: var(--nc-ink);
2317
- }
2318
- .nc-typing-dots[data-accent=red] {
2319
- --nc-accent: var(--nc-error);
2320
- }
2321
- .nc-typing-dots-dot {
2322
- width: 5px;
2323
- height: 5px;
2324
- border-radius: var(--nc-rounded-full);
2325
- background: var(--nc-muted);
2326
- }
2327
- }
2328
-
2329
1835
  /* src/css/components/state-row.css */
2330
1836
  @layer components {
2331
1837
  .nc-state-row {
@@ -2384,55 +1890,6 @@
2384
1890
  }
2385
1891
  }
2386
1892
 
2387
- /* src/css/components/agent-avatar.css */
2388
- @layer components {
2389
- .nc-agent-avatar {
2390
- --nc-accent: var(--nc-line);
2391
- display: grid;
2392
- place-items: center;
2393
- width: 32px;
2394
- height: 32px;
2395
- overflow: hidden;
2396
- border: var(--nc-guide) solid var(--nc-accent);
2397
- border-radius: var(--nc-rounded-full);
2398
- background: var(--nc-canvas-soft);
2399
- color: var(--nc-ink);
2400
- font-family:
2401
- "Geist Mono",
2402
- ui-monospace,
2403
- SFMono-Regular,
2404
- Menlo,
2405
- Monaco,
2406
- monospace;
2407
- font-size: 12px;
2408
- font-weight: 600;
2409
- line-height: 1;
2410
- letter-spacing: 0.02em;
2411
- }
2412
- .nc-agent-avatar img,
2413
- .nc-agent-avatar svg {
2414
- display: block;
2415
- width: 100%;
2416
- height: 100%;
2417
- object-fit: cover;
2418
- }
2419
- .nc-agent-avatar[data-accent=green] {
2420
- --nc-accent: var(--nc-link);
2421
- }
2422
- .nc-agent-avatar[data-accent=cyan] {
2423
- --nc-accent: var(--nc-info);
2424
- }
2425
- .nc-agent-avatar[data-accent=amber] {
2426
- --nc-accent: var(--nc-warning-deep);
2427
- }
2428
- .nc-agent-avatar[data-accent=ink] {
2429
- --nc-accent: var(--nc-ink);
2430
- }
2431
- .nc-agent-avatar[data-accent=red] {
2432
- --nc-accent: var(--nc-error);
2433
- }
2434
- }
2435
-
2436
1893
  /* src/css/components/usage-meter.css */
2437
1894
  @layer components {
2438
1895
  .nc-usage-meter {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nadicodeai/design-system",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -12,7 +12,6 @@
12
12
  "./tokens/dtcg": "./dist/tokens/nadicode.dtcg.json",
13
13
  "./tailwind": "./dist/tailwind/nadicode.tailwind.json",
14
14
  "./css": "./dist/css/index.css",
15
- "./css/chat-autoplay.js": "./dist/css/chat-autoplay.js",
16
15
  "./icons/*": "./dist/icons/*",
17
16
  "./favicon/*": "./dist/favicon/*",
18
17
  "./assets/logo-mark": {
@@ -1,537 +0,0 @@
1
- /*
2
- * chat-autoplay.js — the single optional vanilla enhancer for NadicodeAI chat
3
- * threads. The ONLY JavaScript in @nadicodeai/design-system.
4
- *
5
- * It is pure progressive enhancement, framework-free, dependency-free, with no
6
- * module-loader statements and no third-party runtime. It turns an
7
- * already-static `.nc-chat-thread` into an auto-playing, looping agent
8
- * conversation.
9
- *
10
- * Runtime behavior:
11
- *
12
- * (a) Motion on: messages are hidden, then revealed in scripted source order by
13
- * toggling the `.nc-anim-msg-in` hook (the rise-in). A typing indicator
14
- * (`.nc-typing-dots`) shows between turns, the thread body auto-scrolls to
15
- * the newest message, the `.nc-usage-meter-fill` is driven via `--nc-usage`,
16
- * and after `LOOP_HOLD` the cycle restarts.
17
- *
18
- * (b) `prefers-reduced-motion: reduce`: the FULL thread is shown immediately,
19
- * every message visible, NO loop, no typing animation. (We also stop if the
20
- * preference changes to reduce mid-play.)
21
- *
22
- * (c) Script absent: the conversation renders FULLY and statically from CSS
23
- * alone. The atoms set `animation-name: nc-msg-in` but, without the
24
- * `.nc-anim-msg-in` hook supplying a duration, that is inert and every
25
- * message is visible. This file is the ONLY thing that ever hides a message
26
- * (via the per-element inline `display` it sets while playing). No hidden
27
- * class is ever present in static markup, so no-JS == full static thread.
28
- *
29
- * (d) Playback starts on scroll-into-view via `IntersectionObserver`; it pauses
30
- * (resets to the static full thread and re-arms) when scrolled fully out.
31
- *
32
- * Timing parity with `src/css/motion.css`: the loop reads the duration token
33
- * (`--nc-duration-stagger`) from the thread's computed style and uses the
34
- * per-role `defaultTiming()` rhythm below. The stagger unit comes from CSS, not
35
- * a re-typed magic number; this module owns the sequencing cadence because it
36
- * is runtime behavior rather than a reusable visual token.
37
- *
38
- * Opt-in: only `.nc-chat-thread[data-nc-autoplay]` elements are enhanced. A
39
- * thread without the attribute is left fully static.
40
- *
41
- * Usage (no bundler assumed):
42
- * <script src="…/css/chat-autoplay.js"></script>
43
- * It self-initializes on DOMContentLoaded and also exposes
44
- * `window.ncChatAutoplay.init(root?)` for manual / dynamic init.
45
- */
46
- (function () {
47
- "use strict";
48
-
49
- if (typeof window === "undefined" || typeof document === "undefined") return;
50
-
51
- // The CSS token owns visual stagger. These values own runtime sequencing.
52
- var LOOP_HOLD = 6000; // Keep the completed state visible for most of the loop.
53
- var START_DELAY = 240;
54
-
55
- function defaultTiming(role, textLen) {
56
- var len = textLen || 0;
57
- switch (role) {
58
- case "user":
59
- return { typingMs: 0, holdMs: 1100 + len * 8 };
60
- case "tool":
61
- return { typingMs: 380, holdMs: 700 };
62
- case "state":
63
- return { typingMs: 220, holdMs: 700 };
64
- case "memory":
65
- return { typingMs: 380, holdMs: 900 };
66
- case "handoff":
67
- return { typingMs: 420, holdMs: 1300 };
68
- case "approval":
69
- return { typingMs: 460, holdMs: 1600 };
70
- case "artifact":
71
- return { typingMs: 600, holdMs: 1800 };
72
- case "agent":
73
- default:
74
- return { typingMs: 680, holdMs: 900 + len * 12 };
75
- }
76
- }
77
-
78
- // Infer a message's role from its NadicodeAI atom class so authors don't have
79
- // to duplicate the role in a data attribute. An explicit `data-nc-role`
80
- // overrides. Falls back to "agent" (the slowest, safest cadence).
81
- var ROLE_BY_CLASS = [
82
- ["nc-chat-bubble-user", "user"],
83
- ["nc-chat-bubble-agent", "agent"],
84
- ["nc-tool-call-trace", "tool"],
85
- ["nc-memory-item", "memory"],
86
- ["nc-handoff-banner", "handoff"],
87
- ["nc-approval-gate", "approval"],
88
- ["nc-artifact-preview", "artifact"],
89
- ["nc-state-row", "state"],
90
- ];
91
-
92
- function roleOf(el) {
93
- var explicit = el.getAttribute("data-nc-role");
94
- if (explicit) return explicit;
95
- for (var i = 0; i < ROLE_BY_CLASS.length; i++) {
96
- if (el.classList.contains(ROLE_BY_CLASS[i][0])) return ROLE_BY_CLASS[i][1];
97
- }
98
- return "agent";
99
- }
100
-
101
- function reducedMotion() {
102
- return (
103
- typeof window.matchMedia === "function" &&
104
- window.matchMedia("(prefers-reduced-motion: reduce)").matches
105
- );
106
- }
107
-
108
- // Read a duration token (e.g. "--nc-duration-stagger") off a live element and
109
- // return milliseconds. Falls back to a documented default that equals the
110
- // motion.css value if the bundle is not loaded (graceful, never throws).
111
- function durationToken(el, name, fallbackMs) {
112
- var raw = getComputedStyle(el).getPropertyValue(name).trim();
113
- if (!raw) return fallbackMs;
114
- if (raw.slice(-2) === "ms") return parseFloat(raw) || fallbackMs;
115
- if (raw.slice(-1) === "s") return (parseFloat(raw) || 0) * 1000 || fallbackMs;
116
- var n = parseFloat(raw);
117
- return isNaN(n) ? fallbackMs : n;
118
- }
119
-
120
- // ── One thread's player ───────────────────────────────────────────────────
121
- function ChatThreadPlayer(thread) {
122
- this.thread = thread;
123
- this.body = thread.querySelector(".nc-chat-thread-body");
124
- // Stable, source-ordered list of message atoms inside the body. The typing
125
- // indicator and any element flagged to ignore are NOT messages.
126
- this.messages = this.body
127
- ? Array.prototype.filter.call(this.body.children, function (el) {
128
- return (
129
- el.nodeType === 1 &&
130
- !el.classList.contains("nc-typing-dots") &&
131
- el.getAttribute("data-nc-autoplay-ignore") === null
132
- );
133
- })
134
- : [];
135
- this.typing = this.body ? this.body.querySelector(".nc-typing-dots") : null;
136
- this.meterFill = thread.querySelector(".nc-usage-meter-fill");
137
- this.staggerMs = 160; // overwritten from the CSS token at enhance()
138
- this.timers = [];
139
- this.running = false;
140
- this.cancelled = false;
141
- // Monotonic loop epoch. `_runLoop` captures the value live at start and every
142
- // guard checks it. `stop()`/`rearm()` bump it so a stale continuation — a
143
- // `_sleep` timer that already fired but whose `.then` is still queued when the
144
- // player is stopped and immediately re-armed on scroll-in — sees a mismatched
145
- // epoch and dies permanently, instead of resuming once `cancelled` flips back
146
- // to false and racing the fresh loop. Single, uninterrupted playback never
147
- // changes the epoch mid-run, so the observed sequence is unchanged.
148
- this.generation = 0;
149
- // WCAG 2.2.2 user-pause latch. Set true when the user presses the pause button;
150
- // cleared when the user presses resume. play() and the in-view observer branch
151
- // both check it; a paused thread stays paused across scroll-out/in.
152
- this.userPaused = false;
153
- // Set to true once the sequence has reached its end at least once. Used by
154
- // the IntersectionObserver entry branch: a thread that has completed is shown
155
- // in its final visible state on re-entry rather than replayed from blank, so
156
- // a reader who scrolls back sees the payoff frame instead of an empty/typing state.
157
- this.completedOnce = false;
158
- // Accent for an auto-created typing bubble, inferred from the first agent
159
- // bubble's data-accent (default green, matching the prototype).
160
- this.accent = "green";
161
- var firstAgent = thread.querySelector(".nc-chat-bubble-agent[data-accent]");
162
- if (firstAgent) this.accent = firstAgent.getAttribute("data-accent") || "green";
163
- }
164
-
165
- ChatThreadPlayer.prototype._clearTimers = function () {
166
- for (var i = 0; i < this.timers.length; i++) clearTimeout(this.timers[i]);
167
- this.timers = [];
168
- };
169
-
170
- ChatThreadPlayer.prototype._sleep = function (ms) {
171
- var timers = this.timers;
172
- return new Promise(function (resolve) {
173
- var t = setTimeout(resolve, ms);
174
- timers.push(t);
175
- });
176
- };
177
-
178
- // Fully static, fully visible end-state. Used for no-loop (reduced-motion),
179
- // for pause-out-of-view, and as the baseline each loop resets from. Removes
180
- // every trace the player added so the DOM matches the no-JS static thread.
181
- ChatThreadPlayer.prototype._showAll = function () {
182
- for (var i = 0; i < this.messages.length; i++) {
183
- var m = this.messages[i];
184
- m.style.display = "";
185
- m.classList.remove("nc-anim-msg-in");
186
- }
187
- this._hideTyping();
188
- this.thread.removeAttribute("data-nc-playing");
189
- };
190
-
191
- ChatThreadPlayer.prototype._hideAllMessages = function () {
192
- for (var i = 0; i < this.messages.length; i++) {
193
- this.messages[i].style.display = "none";
194
- this.messages[i].classList.remove("nc-anim-msg-in");
195
- }
196
- };
197
-
198
- ChatThreadPlayer.prototype._scrollToBottom = function () {
199
- var body = this.body;
200
- if (!body) return;
201
- requestAnimationFrame(function () {
202
- body.scrollTop = body.scrollHeight;
203
- });
204
- };
205
-
206
- // Ensure a typing indicator exists; create one if the markup has none so the
207
- // "typing between turns" behaviour works on any thread. Created element is
208
- // tagged so _showAll can drop it.
209
- ChatThreadPlayer.prototype._ensureTyping = function () {
210
- if (this.typing) return this.typing;
211
- if (!this.body) return null;
212
- var el = document.createElement("div");
213
- el.className = "nc-typing-dots";
214
- el.setAttribute("data-accent", this.accent);
215
- el.setAttribute("aria-hidden", "true");
216
- el.setAttribute("data-nc-autoplay-created", "");
217
- for (var i = 0; i < 3; i++) {
218
- var dot = document.createElement("span");
219
- dot.className = "nc-typing-dots-dot nc-anim-typing";
220
- dot.style.setProperty("--nc-anim-delay", i * this.staggerMs + "ms");
221
- el.appendChild(dot);
222
- }
223
- el.style.display = "none";
224
- this.body.appendChild(el);
225
- this.typing = el;
226
- return el;
227
- };
228
-
229
- ChatThreadPlayer.prototype._showTyping = function () {
230
- var el = this._ensureTyping();
231
- if (!el) return;
232
- // Move the indicator to the end of the body so it sits below revealed turns.
233
- this.body.appendChild(el);
234
- el.style.display = "";
235
- this._scrollToBottom();
236
- };
237
-
238
- ChatThreadPlayer.prototype._hideTyping = function () {
239
- if (this.typing) this.typing.style.display = "none";
240
- };
241
-
242
- ChatThreadPlayer.prototype._setMeter = function (fraction) {
243
- if (!this.meterFill) return;
244
- var pct = Math.max(0, Math.min(1, fraction)) * 100;
245
- this.meterFill.style.setProperty("--nc-usage", pct + "%");
246
- };
247
-
248
- ChatThreadPlayer.prototype._reveal = function (msg, index) {
249
- msg.style.display = "";
250
- // Re-trigger the rise-in: the hook supplies duration/easing; the atom
251
- // already carries `animation-name: nc-msg-in`. Force a reflow so re-adding
252
- // the class restarts the animation on a re-loop.
253
- msg.style.setProperty("--nc-anim-delay", "0ms");
254
- msg.classList.remove("nc-anim-msg-in");
255
- void msg.offsetWidth;
256
- msg.classList.add("nc-anim-msg-in");
257
- this._setMeter((index + 1) / this.messages.length);
258
- this._scrollToBottom();
259
- };
260
-
261
- // A loop is "stale" once it is cancelled OR a newer epoch has started; either
262
- // way it must stop touching the DOM. `myGen` is captured once at start so a queued
263
- // continuation can detect that the player was stopped-and-re-armed underneath
264
- // it (see the `generation` note in the constructor).
265
- ChatThreadPlayer.prototype._runLoop = function () {
266
- var myGen = this.generation;
267
- function stale(player) {
268
- return player.cancelled || player.generation !== myGen;
269
- }
270
- function loop(player) {
271
- if (stale(player)) return Promise.resolve();
272
- player._hideAllMessages();
273
- player._hideTyping();
274
- player._setMeter(0);
275
- return player
276
- ._sleep(START_DELAY)
277
- .then(function () {
278
- var chain = Promise.resolve();
279
- player.messages.forEach(function (msg, i) {
280
- chain = chain.then(function () {
281
- if (stale(player)) return;
282
- var role = roleOf(msg);
283
- var t = defaultTiming(role, (msg.textContent || "").trim().length);
284
- var step = function () {
285
- if (stale(player)) return;
286
- player._hideTyping();
287
- player._reveal(msg, i);
288
- return player._sleep(t.holdMs);
289
- };
290
- if (t.typingMs > 0) {
291
- player._showTyping();
292
- return player._sleep(t.typingMs).then(step);
293
- }
294
- return step();
295
- });
296
- });
297
- return chain;
298
- })
299
- .then(function () {
300
- if (stale(player)) return;
301
- // Mark completion before the hold so the observer entry branch can
302
- // show the final state on re-entry without replaying from blank.
303
- player.completedOnce = true;
304
- return player._sleep(LOOP_HOLD);
305
- })
306
- .then(function () {
307
- if (stale(player)) return;
308
- return loop(player);
309
- });
310
- }
311
- return loop(this);
312
- };
313
-
314
- ChatThreadPlayer.prototype.play = function () {
315
- if (this.running || this.cancelled) return;
316
- // WCAG 2.2.2: user-pressed pause wins over every automatic restart.
317
- if (this.userPaused) return;
318
- // (b) Reduced motion: show the full thread immediately, no loop, no typing.
319
- if (reducedMotion()) {
320
- this._showAll();
321
- this._setMeter(1);
322
- return;
323
- }
324
- if (!this.messages.length) return;
325
- this.running = true;
326
- this.thread.setAttribute("data-nc-playing", "");
327
- this._runLoop();
328
- };
329
-
330
- // Stop and reset to the fully-visible static thread (re-armable).
331
- ChatThreadPlayer.prototype.stop = function () {
332
- this.cancelled = true;
333
- this.generation++; // retire the running loop's queued continuations
334
- this._clearTimers();
335
- this.running = false;
336
- this._showAll();
337
- this._setMeter(1);
338
- };
339
-
340
- // Re-arm a stopped player (used when the thread scrolls back into view). Bump
341
- // the epoch again so a still-queued continuation from the just-stopped loop can
342
- // never resume now that `cancelled` is cleared — only the loop `play()` starts
343
- // next will match the current epoch.
344
- ChatThreadPlayer.prototype.rearm = function () {
345
- this.cancelled = false;
346
- this.generation++;
347
- this.running = false;
348
- };
349
-
350
- // Inject the enhancer-owned pause/resume button into the thread card.
351
- // One button per thread, positioned top-right above the body. Uses
352
- // data-nc-autoplay-name for an accessible per-thread subject, e.g.
353
- // "Pause <value>"/"Resume <value>"; falls back to "Pause demonstration".
354
- // Never called in reduced-motion mode (contract (b): no button for a path
355
- // that plays nothing).
356
- ChatThreadPlayer.prototype._injectPauseButton = function () {
357
- var thread = this.thread;
358
- var subject = thread.getAttribute("data-nc-autoplay-name") || "demonstration";
359
- var btn = document.createElement("button");
360
- btn.type = "button";
361
- btn.className = "nc-chat-pause";
362
- btn.setAttribute("aria-pressed", "false");
363
- btn.textContent = "Pause " + subject;
364
- btn.addEventListener(
365
- "click",
366
- function () {
367
- if (this.userPaused) {
368
- // Resume: clear the latch, rearm and play.
369
- this.userPaused = false;
370
- btn.setAttribute("aria-pressed", "false");
371
- btn.textContent = "Pause " + subject;
372
- this.rearm();
373
- this.play();
374
- } else {
375
- // Pause: set the latch, stop and reveal full content.
376
- this.userPaused = true;
377
- btn.setAttribute("aria-pressed", "true");
378
- btn.textContent = "Resume " + subject;
379
- this.stop();
380
- }
381
- }.bind(this),
382
- );
383
- // Insert before the body so the button sits above the scrolling content.
384
- var body = thread.querySelector(".nc-chat-thread-body");
385
- if (body) {
386
- thread.insertBefore(btn, body);
387
- } else {
388
- thread.appendChild(btn);
389
- }
390
- this.pauseBtn = btn;
391
- };
392
-
393
- // ── Initialisation ──────────────────────────────────────────────────────
394
- var players = [];
395
- var observer = null;
396
-
397
- function enhance(thread) {
398
- if (thread.__ncAutoplayPlayer) return thread.__ncAutoplayPlayer;
399
- var player = new ChatThreadPlayer(thread);
400
- // Read the stagger token off the live thread (parity with motion.css);
401
- // fallback equals the motion.css default (60ms) if the bundle is absent.
402
- player.staggerMs = durationToken(thread, "--nc-duration-stagger", 60);
403
- thread.__ncAutoplayPlayer = player;
404
- players.push(player);
405
- // Inject the pause/resume button unless reduced-motion is on (that path
406
- // plays nothing and must inject no button — WCAG 2.2.2 scope follows the
407
- // animation).
408
- if (!reducedMotion()) {
409
- player._injectPauseButton();
410
- }
411
- return player;
412
- }
413
-
414
- function startObserving() {
415
- if (typeof window.IntersectionObserver !== "function") {
416
- // No IntersectionObserver: degrade by playing immediately (still respects
417
- // reduced motion inside play()).
418
- for (var i = 0; i < players.length; i++) players[i].play();
419
- return;
420
- }
421
- // Manual re-init (the documented `window.ncChatAutoplay.init(root)` path for
422
- // dynamic content) calls this again; tear down the prior observer first so a
423
- // repeat init replaces — rather than stacks — observers. A single self-init
424
- // (the common path) hits this once and `observer` is null, so behavior is
425
- // unchanged; only repeated manual init benefits (no leaked observers, no
426
- // duplicate intersection callbacks per thread).
427
- if (observer) observer.disconnect();
428
- observer = new IntersectionObserver(
429
- function (entries) {
430
- for (var i = 0; i < entries.length; i++) {
431
- var entry = entries[i];
432
- var player = entry.target.__ncAutoplayPlayer;
433
- if (!player) continue;
434
- if (entry.isIntersecting) {
435
- // (d) Start when scrolled into view — but only if the player is not
436
- // already running its loop. Re-arming a mid-loop player kills and
437
- // restarts it from blank (the long right thread reads as dead under
438
- // scroll crossings). A player is "running" once play() sets the flag
439
- // and until stop() clears it; the loop recursion holds this state.
440
- // WCAG 2.2.2: do not override a user-pressed pause on scroll-in.
441
- if (player.userPaused) continue;
442
- if (!player.running) {
443
- if (player.completedOnce) {
444
- // The sequence finished at least once. Show the full final state
445
- // immediately so the reader sees the payoff frame, not a blank/
446
- // typing restart. The loop resumes from this state on the next
447
- // natural cycle if the thread stays in view.
448
- player._showAll();
449
- player._setMeter(1);
450
- } else {
451
- player.rearm();
452
- player.play();
453
- }
454
- }
455
- } else if (entry.intersectionRatio === 0) {
456
- // Stop only when the thread is FULLY out of view (ratio exactly 0).
457
- // At partial visibility (e.g. ratio 0.228) do nothing — collapsing
458
- // to the finished card while the thread is still on screen is the
459
- // bug this guard fixes.
460
- if (player.running) {
461
- player.stop();
462
- }
463
- }
464
- }
465
- },
466
- { threshold: [0, 0.25] },
467
- );
468
- for (var j = 0; j < players.length; j++) observer.observe(players[j].thread);
469
- }
470
-
471
- // ── Enhancer-armed, run-once scroll entrance ─────────────────────────────
472
- // Arms .nc-settle elements that are fully below the viewport at init.
473
- // On first IntersectionObserver entry, switches data-nc-settle from "armed"
474
- // to "in" (triggers the CSS nc-msg-in animation) and immediately unobserves
475
- // so the rise-in fires once and is never reversed on scroll-back. Elements
476
- // already visible at load are left unarmed (no flash; hero region untouched).
477
- // prefers-reduced-motion: skip entirely — page fully visible without JS.
478
- function initSettle(scope) {
479
- if (reducedMotion()) return;
480
- if (typeof window.IntersectionObserver !== "function") return;
481
- var settleElements = (scope || document).querySelectorAll(".nc-settle");
482
- if (!settleElements.length) return;
483
- var settleObserver = new IntersectionObserver(
484
- function (entries, obs) {
485
- for (var i = 0; i < entries.length; i++) {
486
- if (entries[i].isIntersecting) {
487
- entries[i].target.setAttribute("data-nc-settle", "in");
488
- obs.unobserve(entries[i].target);
489
- }
490
- }
491
- },
492
- { threshold: 0.1 },
493
- );
494
- for (var i = 0; i < settleElements.length; i++) {
495
- var el = settleElements[i];
496
- var rect = el.getBoundingClientRect();
497
- // Arm only elements whose top edge is fully below the viewport.
498
- if (rect.top > window.innerHeight) {
499
- el.setAttribute("data-nc-settle", "armed");
500
- settleObserver.observe(el);
501
- }
502
- }
503
- }
504
-
505
- function init(root) {
506
- var scope = root || document;
507
- var threads = scope.querySelectorAll(".nc-chat-thread[data-nc-autoplay]");
508
- for (var i = 0; i < threads.length; i++) enhance(threads[i]);
509
- initSettle(scope);
510
- startObserving();
511
- return players;
512
- }
513
-
514
- // Respond to a runtime change of the reduced-motion preference: collapse any
515
- // running player to the static full thread.
516
- if (typeof window.matchMedia === "function") {
517
- var mq = window.matchMedia("(prefers-reduced-motion: reduce)");
518
- var onChange = function () {
519
- if (!mq.matches) return;
520
- for (var i = 0; i < players.length; i++) {
521
- if (players[i].running) players[i].stop();
522
- }
523
- };
524
- if (typeof mq.addEventListener === "function") mq.addEventListener("change", onChange);
525
- else if (typeof mq.addListener === "function") mq.addListener(onChange);
526
- }
527
-
528
- window.ncChatAutoplay = { init: init, players: players };
529
-
530
- if (document.readyState === "loading") {
531
- document.addEventListener("DOMContentLoaded", function () {
532
- init();
533
- });
534
- } else {
535
- init();
536
- }
537
- })();