@adea-ai/ui 0.67.0 → 0.67.2

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/README.md CHANGED
@@ -180,9 +180,14 @@ transcript when its jump control disappears; automatic pins never move focus.
180
180
 
181
181
  `threshold` controls jump-button visibility (80px by default), independently of
182
182
  follow intent. `follow={false}` is fully inert: positioning belongs to the host,
183
- with no observer or mount pin. Re-enabling it explicitly re-arms at the bottom;
184
- it is not an automatic restore-and-resume flag. An optional `resetKey` also
185
- re-arms follow when that identity changes. Canonical
183
+ with no observer or mount pin. Re-enabling it explicitly re-arms at the bottom
184
+ unless the host supplies `initialReadingPosition: { top, following }`. That
185
+ snapshot is consumed only on mount, identity reset or follow re-enable; ordinary
186
+ snapshot prop changes never reposition the reader. A parked snapshot releases
187
+ follow until an explicit jump or native return to the bottom. The
188
+ `onReadingPositionChange` callback reports native offset and actual follow intent,
189
+ including small upward moves inside the jump visibility threshold. An optional
190
+ `resetKey` resets follow or consumes the current identity's snapshot. Canonical
186
191
  session identity, durable restoration and virtualized transcript windows remain
187
192
  application-owned. `ref` and `onScroll` still reach the inner scroller.
188
193
 
@@ -15,6 +15,10 @@ import type { ComponentProps, JSX } from 'solid-js';
15
15
  * than dragging the pane behind it — which in a desktop shell means the whole
16
16
  * window moves.
17
17
  */
18
+ export type ConversationReadingPosition = Readonly<{
19
+ top: number;
20
+ following: boolean;
21
+ }>;
18
22
  export type ConversationSurfaceProps = ComponentProps<'div'> & {
19
23
  /** How far from the bottom still counts as "at the bottom", in px. */
20
24
  threshold?: number;
@@ -22,6 +26,10 @@ export type ConversationSurfaceProps = ComponentProps<'div'> & {
22
26
  follow?: boolean;
23
27
  /** Optional conversation identity: changing it re-arms follow at the bottom. */
24
28
  resetKey?: string;
29
+ /** Host snapshot consumed on mount/identity reset, never on ordinary prop updates. */
30
+ initialReadingPosition?: ConversationReadingPosition;
31
+ /** Actual follow intent and native offset; storage/identity remain host-owned. */
32
+ onReadingPositionChange?: (position: ConversationReadingPosition) => void;
25
33
  /** Rendered above the transcript when there is nothing in it. */
26
34
  empty?: JSX.Element;
27
35
  /** A header that scrolls with the transcript, e.g. a day divider or a banner. */
@@ -1 +1 @@
1
- {"version":3,"file":"conversation-surface.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/conversation-surface.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,UAAU,CAAA;AAMnD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,wBAAwB,GAAG,cAAc,CAAC,KAAK,CAAC,GAAG;IAC7D,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,iEAAiE;IACjE,KAAK,CAAC,EAAE,GAAG,CAAC,OAAO,CAAA;IACnB,iFAAiF;IACjF,MAAM,CAAC,EAAE,GAAG,CAAC,OAAO,CAAA;CACrB,CAAA;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,wBAAwB,eAmElE"}
1
+ {"version":3,"file":"conversation-surface.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/conversation-surface.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,UAAU,CAAA;AAMnD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,2BAA2B,GAAG,QAAQ,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAAC,CAAA;AAEvF,MAAM,MAAM,wBAAwB,GAAG,cAAc,CAAC,KAAK,CAAC,GAAG;IAC7D,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,sFAAsF;IACtF,sBAAsB,CAAC,EAAE,2BAA2B,CAAA;IACpD,kFAAkF;IAClF,uBAAuB,CAAC,EAAE,CAAC,QAAQ,EAAE,2BAA2B,KAAK,IAAI,CAAA;IACzE,iEAAiE;IACjE,KAAK,CAAC,EAAE,GAAG,CAAC,OAAO,CAAA;IACnB,iFAAiF;IACjF,MAAM,CAAC,EAAE,GAAG,CAAC,OAAO,CAAA;CACrB,CAAA;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,wBAAwB,eAuElE"}
@@ -14,6 +14,8 @@ function ConversationSurface(props) {
14
14
  "threshold",
15
15
  "follow",
16
16
  "resetKey",
17
+ "initialReadingPosition",
18
+ "onReadingPositionChange",
17
19
  "empty",
18
20
  "header",
19
21
  "children",
@@ -24,7 +26,9 @@ function ConversationSurface(props) {
24
26
  const follow = createScrollFollow({
25
27
  enabled: () => local.follow !== false,
26
28
  resetKey: () => local.resetKey,
27
- threshold: () => local.threshold ?? 80
29
+ threshold: () => local.threshold ?? 80,
30
+ initialPosition: () => local.initialReadingPosition,
31
+ onPositionChange: (position) => local.onReadingPositionChange?.(position)
28
32
  });
29
33
  const onScroll = (event) => {
30
34
  follow.onScroll();
@@ -1 +1 @@
1
- {"version":3,"file":"conversation-surface.js","names":["ArrowDown","ComponentProps","JSX","Show","splitProps","cn","Button","createScrollFollow","ConversationSurfaceProps","threshold","follow","resetKey","empty","Element","header","ConversationSurface","props","local","rest","enabled","onScroll","EventHandlerUnion","HTMLDivElement","Event","event","handler","_el$","_tmpl$2","_el$2","firstChild","_el$3","addEventListener","_$use","element","bindScroller","ref","_$spread","_$mergeProps","tabindex","_ref$","bindContent","_$insert","_$createComponent","when","children","fallback","_el$5","_tmpl$3","atBottom","_el$4","_tmpl$","type","size","variant","onClick","jump","_$effect","_$className","class","_$mergeProps","_$use","_$createComponent","_$className"],"sources":["../../../src/components/conversation/conversation-surface.tsx"],"sourcesContent":["import { ArrowDown } from 'lucide-solid'\nimport type { ComponentProps, JSX } from 'solid-js'\nimport { Show, splitProps } from 'solid-js'\nimport { cn } from '#lib/utils'\nimport { Button } from '../ui/button/button'\nimport { createScrollFollow } from './scroll-follow'\n\n/**\n * ConversationSurface.\n *\n * The scrolling transcript. Its job is small and it is the difference between a\n * transcript that stays readable as it grows and one that fights the reader:\n *\n * - **Stick to the bottom.** New messages scroll into view only when the reader is\n * already at the bottom. Someone reading back through history must not be yanked\n * to the end by an arriving message, and that is the single most common way a\n * chat surface is made unusable.\n * - **Offer a way back.** Once the reader is not at the bottom, a control appears\n * that returns them. Pixel distance cannot prove an unread message count.\n * - **`overscroll-contain`.** The transcript scrolls to its end and stops, rather\n * than dragging the pane behind it — which in a desktop shell means the whole\n * window moves.\n */\nexport type ConversationSurfaceProps = ComponentProps<'div'> & {\n /** How far from the bottom still counts as \"at the bottom\", in px. */\n threshold?: number\n /** Disable automatic positioning while the host owns scroll restoration. */\n follow?: boolean\n /** Optional conversation identity: changing it re-arms follow at the bottom. */\n resetKey?: string\n /** Rendered above the transcript when there is nothing in it. */\n empty?: JSX.Element\n /** A header that scrolls with the transcript, e.g. a day divider or a banner. */\n header?: JSX.Element\n}\n\nexport function ConversationSurface(props: ConversationSurfaceProps) {\n const [local, rest] = splitProps(props, [\n 'class',\n 'threshold',\n 'follow',\n 'resetKey',\n 'empty',\n 'header',\n 'children',\n 'onScroll',\n 'ref',\n 'tabindex',\n ])\n // Kobalte provides the control's semantics; transcript follow is a native\n // geometry contract. KiroCrew's plain-scroller controller handles streaming,\n // content collapse and pane resizing without treating them as user consent.\n const follow = createScrollFollow({\n enabled: () => local.follow !== false,\n resetKey: () => local.resetKey,\n threshold: () => local.threshold ?? 80,\n })\n const onScroll: JSX.EventHandlerUnion<HTMLDivElement, Event> = (event) => {\n follow.onScroll()\n const handler = local.onScroll\n if (typeof handler === 'function') handler(event)\n else if (handler) handler[0](handler[1], event)\n }\n\n return (\n <div\n data-slot=\"conversation-surface\"\n class={cn('relative flex min-h-0 flex-1 flex-col', local.class)}\n >\n <div\n ref={(element) => {\n follow.bindScroller(element)\n if (typeof local.ref === 'function') local.ref(element)\n }}\n onScroll={onScroll}\n tabindex={local.tabindex ?? 0}\n class=\"min-h-0 flex-1 overflow-y-auto overscroll-contain\"\n {...rest}\n >\n <div ref={follow.bindContent} data-slot=\"conversation-content\">\n <Show when={local.header}>{local.header}</Show>\n <Show when={local.children} fallback={<div class=\"p-4\">{local.empty}</div>}>\n {local.children}\n </Show>\n </div>\n </div>\n\n <Show when={!follow.atBottom()}>\n <div class=\"pointer-events-none absolute inset-x-0 bottom-3 flex justify-center\">\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"secondary\"\n class=\"pointer-events-auto gap-1.5 shadow-md\"\n onClick={follow.jump}\n >\n <ArrowDown />\n Jump to latest\n </Button>\n </div>\n </Show>\n </div>\n )\n}\n"],"mappings":";;;;;;;;;;AAoCA,SAAgBe,oBAAoBC,OAAiC;CACnE,MAAM,CAACC,OAAOC,QAAQd,WAAWY,OAAO;EACtC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CAAU,CACX;CAID,MAAMN,SAASH,mBAAmB;EAChCY,eAAeF,MAAMP,WAAW;EAChCC,gBAAgBM,MAAMN;EACtBF,iBAAiBQ,MAAMR,aAAa;CACtC,CAAC;CACD,MAAMW,YAA0DI,UAAU;EACxEd,OAAOU,SAAS;EAChB,MAAMK,UAAUR,MAAMG;EACtB,IAAI,OAAOK,YAAY,YAAYA,QAAQD,KAAK;OAC3C,IAAIC,SAASA,QAAQ,EAAE,CAACA,QAAQ,IAAID,KAAK;CAChD;CAEA,cAAA;EAAA,IAAAE,OAAAC,QAAA,GAAAC,QAAAF,KAAAG,YAAAC,QAAAF,MAAAC;EAAAD,MAAAG,iBAAA,UAUgBX,QAAQ;EAAAY,KAJZC,YAAY;GAChBvB,OAAOwB,aAAaD,OAAO;GAC3B,IAAI,OAAOhB,MAAMkB,QAAQ,YAAYlB,MAAMkB,IAAIF,OAAO;EACxD,GAACL,KAAA;EAAAQ,OAAAR,OAAAS,WAAA,EAAA,IAEDC,WAAQ;GAAA,OAAErB,MAAMqB,YAAY;EAAC,EAAA,GAEzBpB,IAAI,GAAA,OAAA,IAAA;EAAA,IAAAqB,QAEE7B,OAAO8B;EAAW,OAAAD,UAAA,aAAAP,IAAAO,OAAAT,KAAA,IAAlBpB,OAAO8B,cAAWV;EAAAW,OAAAX,OAAAY,gBACzBvC,MAAI;GAAA,IAACwC,OAAI;IAAA,OAAE1B,MAAMH;GAAM;GAAA,IAAA8B,WAAA;IAAA,OAAG3B,MAAMH;GAAM;EAAA,CAAA,GAAA,IAAA;EAAA2B,OAAAX,OAAAY,gBACtCvC,MAAI;GAAA,IAACwC,OAAI;IAAA,OAAE1B,MAAM2B;GAAQ;GAAA,IAAEC,WAAQ;IAAA,cAAA;KAAA,IAAAC,QAAAC,QAAA;KAAAN,OAAAK,aAAoB7B,MAAML,KAAK;KAAA,OAAAkC;IAAA,EAAA,CAAA;GAAA;GAAA,IAAAF,WAAA;IAAA,OAChE3B,MAAM2B;GAAQ;EAAA,CAAA,GAAA,IAAA;EAAAH,OAAAf,MAAAgB,gBAKpBvC,MAAI;GAAA,IAACwC,OAAI;IAAA,OAAE,CAACjC,OAAOsC,SAAS;GAAC;GAAA,IAAAJ,WAAA;IAAA,IAAAK,QAAAC,OAAA;IAAAT,OAAAQ,OAAAP,gBAEzBpC,QAAM;KACL6C,MAAI;KACJC,MAAI;KACJC,SAAO;KAAA,SAAA;KAAA,IAEPC,UAAO;MAAA,OAAE5C,OAAO6C;KAAI;KAAA,IAAAX,WAAA;MAAA,OAAA,CAAAF,gBAEnB1C,WAAS,CAAA,CAAA,GAAA,gBAAA;KAAA;IAAA,CAAA,CAAA;IAAA,OAAAiD;GAAA;EAAA,CAAA,GAAA,IAAA;EAAAO,aAAAC,UAAA/B,MA7BTrB,GAAG,yCAAyCY,MAAMyC,KAAK,CAAC,CAAA;EAAA,OAAAhC;CAAA,EAAA,CAAA;AAoCrE"}
1
+ {"version":3,"file":"conversation-surface.js","names":["ArrowDown","ComponentProps","JSX","Show","splitProps","cn","Button","createScrollFollow","ConversationReadingPosition","Readonly","top","following","ConversationSurfaceProps","threshold","follow","resetKey","initialReadingPosition","onReadingPositionChange","position","empty","Element","header","ConversationSurface","props","local","rest","enabled","initialPosition","onPositionChange","onScroll","EventHandlerUnion","HTMLDivElement","Event","event","handler","_el$","_tmpl$2","_el$2","firstChild","_el$3","addEventListener","_$use","element","bindScroller","ref","_$spread","_$mergeProps","tabindex","_ref$","bindContent","_$insert","_$createComponent","when","children","fallback","_el$5","_tmpl$3","atBottom","_el$4","_tmpl$","type","size","variant","onClick","jump","_$effect","_$className","class","_$mergeProps","_$use","_$createComponent","_$className"],"sources":["../../../src/components/conversation/conversation-surface.tsx"],"sourcesContent":["import { ArrowDown } from 'lucide-solid'\nimport type { ComponentProps, JSX } from 'solid-js'\nimport { Show, splitProps } from 'solid-js'\nimport { cn } from '#lib/utils'\nimport { Button } from '../ui/button/button'\nimport { createScrollFollow } from './scroll-follow'\n\n/**\n * ConversationSurface.\n *\n * The scrolling transcript. Its job is small and it is the difference between a\n * transcript that stays readable as it grows and one that fights the reader:\n *\n * - **Stick to the bottom.** New messages scroll into view only when the reader is\n * already at the bottom. Someone reading back through history must not be yanked\n * to the end by an arriving message, and that is the single most common way a\n * chat surface is made unusable.\n * - **Offer a way back.** Once the reader is not at the bottom, a control appears\n * that returns them. Pixel distance cannot prove an unread message count.\n * - **`overscroll-contain`.** The transcript scrolls to its end and stops, rather\n * than dragging the pane behind it — which in a desktop shell means the whole\n * window moves.\n */\nexport type ConversationReadingPosition = Readonly<{ top: number; following: boolean }>\n\nexport type ConversationSurfaceProps = ComponentProps<'div'> & {\n /** How far from the bottom still counts as \"at the bottom\", in px. */\n threshold?: number\n /** Disable automatic positioning while the host owns scroll restoration. */\n follow?: boolean\n /** Optional conversation identity: changing it re-arms follow at the bottom. */\n resetKey?: string\n /** Host snapshot consumed on mount/identity reset, never on ordinary prop updates. */\n initialReadingPosition?: ConversationReadingPosition\n /** Actual follow intent and native offset; storage/identity remain host-owned. */\n onReadingPositionChange?: (position: ConversationReadingPosition) => void\n /** Rendered above the transcript when there is nothing in it. */\n empty?: JSX.Element\n /** A header that scrolls with the transcript, e.g. a day divider or a banner. */\n header?: JSX.Element\n}\n\nexport function ConversationSurface(props: ConversationSurfaceProps) {\n const [local, rest] = splitProps(props, [\n 'class',\n 'threshold',\n 'follow',\n 'resetKey',\n 'initialReadingPosition',\n 'onReadingPositionChange',\n 'empty',\n 'header',\n 'children',\n 'onScroll',\n 'ref',\n 'tabindex',\n ])\n // Kobalte provides the control's semantics; transcript follow is a native\n // geometry contract. KiroCrew's plain-scroller controller handles streaming,\n // content collapse and pane resizing without treating them as user consent.\n const follow = createScrollFollow({\n enabled: () => local.follow !== false,\n resetKey: () => local.resetKey,\n threshold: () => local.threshold ?? 80,\n initialPosition: () => local.initialReadingPosition,\n onPositionChange: (position) => local.onReadingPositionChange?.(position),\n })\n const onScroll: JSX.EventHandlerUnion<HTMLDivElement, Event> = (event) => {\n follow.onScroll()\n const handler = local.onScroll\n if (typeof handler === 'function') handler(event)\n else if (handler) handler[0](handler[1], event)\n }\n\n return (\n <div\n data-slot=\"conversation-surface\"\n class={cn('relative flex min-h-0 flex-1 flex-col', local.class)}\n >\n <div\n ref={(element) => {\n follow.bindScroller(element)\n if (typeof local.ref === 'function') local.ref(element)\n }}\n onScroll={onScroll}\n tabindex={local.tabindex ?? 0}\n class=\"min-h-0 flex-1 overflow-y-auto overscroll-contain\"\n {...rest}\n >\n <div ref={follow.bindContent} data-slot=\"conversation-content\">\n <Show when={local.header}>{local.header}</Show>\n <Show when={local.children} fallback={<div class=\"p-4\">{local.empty}</div>}>\n {local.children}\n </Show>\n </div>\n </div>\n\n <Show when={!follow.atBottom()}>\n <div class=\"pointer-events-none absolute inset-x-0 bottom-3 flex justify-center\">\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"secondary\"\n class=\"pointer-events-auto gap-1.5 shadow-md\"\n onClick={follow.jump}\n >\n <ArrowDown />\n Jump to latest\n </Button>\n </div>\n </Show>\n </div>\n )\n}\n"],"mappings":";;;;;;;;;;AA0CA,SAAgBsB,oBAAoBC,OAAiC;CACnE,MAAM,CAACC,OAAOC,QAAQrB,WAAWmB,OAAO;EACtC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CAAU,CACX;CAID,MAAMT,SAASP,mBAAmB;EAChCmB,eAAeF,MAAMV,WAAW;EAChCC,gBAAgBS,MAAMT;EACtBF,iBAAiBW,MAAMX,aAAa;EACpCc,uBAAuBH,MAAMR;EAC7BY,mBAAmBV,aAAaM,MAAMP,0BAA0BC,QAAQ;CAC1E,CAAC;CACD,MAAMW,YAA0DI,UAAU;EACxEnB,OAAOe,SAAS;EAChB,MAAMK,UAAUV,MAAMK;EACtB,IAAI,OAAOK,YAAY,YAAYA,QAAQD,KAAK;OAC3C,IAAIC,SAASA,QAAQ,EAAE,CAACA,QAAQ,IAAID,KAAK;CAChD;CAEA,cAAA;EAAA,IAAAE,OAAAC,QAAA,GAAAC,QAAAF,KAAAG,YAAAC,QAAAF,MAAAC;EAAAD,MAAAG,iBAAA,UAUgBX,QAAQ;EAAAY,KAJZC,YAAY;GAChB5B,OAAO6B,aAAaD,OAAO;GAC3B,IAAI,OAAOlB,MAAMoB,QAAQ,YAAYpB,MAAMoB,IAAIF,OAAO;EACxD,GAACL,KAAA;EAAAQ,OAAAR,OAAAS,WAAA,EAAA,IAEDC,WAAQ;GAAA,OAAEvB,MAAMuB,YAAY;EAAC,EAAA,GAEzBtB,IAAI,GAAA,OAAA,IAAA;EAAA,IAAAuB,QAEElC,OAAOmC;EAAW,OAAAD,UAAA,aAAAP,IAAAO,OAAAT,KAAA,IAAlBzB,OAAOmC,cAAWV;EAAAW,OAAAX,OAAAY,gBACzBhD,MAAI;GAAA,IAACiD,OAAI;IAAA,OAAE5B,MAAMH;GAAM;GAAA,IAAAgC,WAAA;IAAA,OAAG7B,MAAMH;GAAM;EAAA,CAAA,GAAA,IAAA;EAAA6B,OAAAX,OAAAY,gBACtChD,MAAI;GAAA,IAACiD,OAAI;IAAA,OAAE5B,MAAM6B;GAAQ;GAAA,IAAEC,WAAQ;IAAA,cAAA;KAAA,IAAAC,QAAAC,QAAA;KAAAN,OAAAK,aAAoB/B,MAAML,KAAK;KAAA,OAAAoC;IAAA,EAAA,CAAA;GAAA;GAAA,IAAAF,WAAA;IAAA,OAChE7B,MAAM6B;GAAQ;EAAA,CAAA,GAAA,IAAA;EAAAH,OAAAf,MAAAgB,gBAKpBhD,MAAI;GAAA,IAACiD,OAAI;IAAA,OAAE,CAACtC,OAAO2C,SAAS;GAAC;GAAA,IAAAJ,WAAA;IAAA,IAAAK,QAAAC,OAAA;IAAAT,OAAAQ,OAAAP,gBAEzB7C,QAAM;KACLsD,MAAI;KACJC,MAAI;KACJC,SAAO;KAAA,SAAA;KAAA,IAEPC,UAAO;MAAA,OAAEjD,OAAOkD;KAAI;KAAA,IAAAX,WAAA;MAAA,OAAA,CAAAF,gBAEnBnD,WAAS,CAAA,CAAA,GAAA,gBAAA;KAAA;IAAA,CAAA,CAAA;IAAA,OAAA0D;GAAA;EAAA,CAAA,GAAA,IAAA;EAAAO,aAAAC,UAAA/B,MA7BT9B,GAAG,yCAAyCmB,MAAM2C,KAAK,CAAC,CAAA;EAAA,OAAAhC;CAAA,EAAA,CAAA;AAoCrE"}
@@ -1,7 +1,7 @@
1
1
  export { ChatComposer, type ChatComposerProps, type ChatComposerControl } from './chat-composer';
2
2
  export { BusySendButton, type BusySendMode, type BusySendButtonProps } from './busy-send-button';
3
3
  export { ConversationAvatar, type ConversationAvatarKind, type ConversationAvatarProps, } from './conversation-avatar';
4
- export { ConversationSurface, type ConversationSurfaceProps } from './conversation-surface';
4
+ export { ConversationSurface, type ConversationSurfaceProps, type ConversationReadingPosition, } from './conversation-surface';
5
5
  export { ComposerAttachmentButton, ComposerHint, MessageComposer, type MessageComposerProps, } from './message-composer';
6
6
  export { MessageBody, MessageDayDivider, MessageGroup, MessageRow, type MessageRowProps, } from './message-row';
7
7
  export { AttachmentCard, ThreadPanel, type AttachmentCardProps, type ThreadPanelProps, } from './thread-panel';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,KAAK,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAChG,OAAO,EAAE,cAAc,EAAE,KAAK,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AAChG,OAAO,EACL,kBAAkB,EAClB,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,GAC7B,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAE,mBAAmB,EAAE,KAAK,wBAAwB,EAAE,MAAM,wBAAwB,CAAA;AAC3F,OAAO,EACL,wBAAwB,EACxB,YAAY,EACZ,eAAe,EACf,KAAK,oBAAoB,GAC1B,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACL,WAAW,EACX,iBAAiB,EACjB,YAAY,EACZ,UAAU,EACV,KAAK,eAAe,GACrB,MAAM,eAAe,CAAA;AACtB,OAAO,EACL,cAAc,EACd,WAAW,EACX,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,GACtB,MAAM,gBAAgB,CAAA;AAEvB,OAAO,EACL,UAAU,EACV,SAAS,EACT,eAAe,EACf,WAAW,EACX,YAAY,EACZ,OAAO,EACP,WAAW,EACX,gBAAgB,EAChB,kBAAkB,EAClB,cAAc,EACd,uBAAuB,EACvB,YAAY,EACZ,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,KAAK,UAAU,GAChB,MAAM,gBAAgB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,KAAK,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAChG,OAAO,EAAE,cAAc,EAAE,KAAK,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AAChG,OAAO,EACL,kBAAkB,EAClB,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,GAC7B,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EACL,mBAAmB,EACnB,KAAK,wBAAwB,EAC7B,KAAK,2BAA2B,GACjC,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EACL,wBAAwB,EACxB,YAAY,EACZ,eAAe,EACf,KAAK,oBAAoB,GAC1B,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACL,WAAW,EACX,iBAAiB,EACjB,YAAY,EACZ,UAAU,EACV,KAAK,eAAe,GACrB,MAAM,eAAe,CAAA;AACtB,OAAO,EACL,cAAc,EACd,WAAW,EACX,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,GACtB,MAAM,gBAAgB,CAAA;AAEvB,OAAO,EACL,UAAU,EACV,SAAS,EACT,eAAe,EACf,WAAW,EACX,YAAY,EACZ,OAAO,EACP,WAAW,EACX,gBAAgB,EAChB,kBAAkB,EAClB,cAAc,EACd,uBAAuB,EACvB,YAAY,EACZ,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,KAAK,UAAU,GAChB,MAAM,gBAAgB,CAAA"}
@@ -1,8 +1,11 @@
1
+ import type { ConversationReadingPosition } from './conversation-surface';
1
2
  /** Internal plain-scroller binding; host-owned restoration can disable follow. */
2
3
  export declare function createScrollFollow(options: {
3
4
  enabled: () => boolean;
4
5
  resetKey: () => string | undefined;
5
6
  threshold: () => number;
7
+ initialPosition?: () => ConversationReadingPosition | undefined;
8
+ onPositionChange?: (position: ConversationReadingPosition) => void;
6
9
  }): {
7
10
  atBottom: import("solid-js").Accessor<boolean>;
8
11
  onScroll: () => void;
@@ -1 +1 @@
1
- {"version":3,"file":"scroll-follow.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/scroll-follow.ts"],"names":[],"mappings":"AA8BA,kFAAkF;AAClF,wBAAgB,kBAAkB,CAAC,OAAO,EAAE;IAC1C,OAAO,EAAE,MAAM,OAAO,CAAA;IACtB,QAAQ,EAAE,MAAM,MAAM,GAAG,SAAS,CAAA;IAClC,SAAS,EAAE,MAAM,MAAM,CAAA;CACxB;;;;4BA0F2B,cAAc;2BAGf,cAAc;EAIxC"}
1
+ {"version":3,"file":"scroll-follow.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/scroll-follow.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,wBAAwB,CAAA;AAezE,kFAAkF;AAClF,wBAAgB,kBAAkB,CAAC,OAAO,EAAE;IAC1C,OAAO,EAAE,MAAM,OAAO,CAAA;IACtB,QAAQ,EAAE,MAAM,MAAM,GAAG,SAAS,CAAA;IAClC,SAAS,EAAE,MAAM,MAAM,CAAA;IACvB,eAAe,CAAC,EAAE,MAAM,2BAA2B,GAAG,SAAS,CAAA;IAC/D,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,2BAA2B,KAAK,IAAI,CAAA;CACnE;;;;4BAsH2B,cAAc;2BAGf,cAAc;EAIxC"}
@@ -1,5 +1,5 @@
1
1
  import { bottomTarget, computeAtBottom, evaluateAutoPin, isSelfScroll, resolveUserScrollStick } from "./scroll-follow-core.js";
2
- import { createEffect, createSignal, onCleanup } from "solid-js";
2
+ import { createEffect, createSignal, onCleanup, untrack } from "solid-js";
3
3
  //#region src/components/conversation/scroll-follow.ts
4
4
  var geometry = (element) => ({
5
5
  scrollTop: element.scrollTop,
@@ -16,6 +16,13 @@ function createScrollFollow(options) {
16
16
  let lastWriteClientHeight = -1;
17
17
  let previousTop = -1;
18
18
  let lastScrollClientHeight = 0;
19
+ const reportPosition = () => {
20
+ if (scroller) untrack(() => options.onPositionChange?.({
21
+ top: scroller.scrollTop,
22
+ following: options.enabled() && stick
23
+ }));
24
+ };
25
+ onCleanup(reportPosition);
19
26
  const writePin = (element, target) => {
20
27
  element.scrollTop = target;
21
28
  lastWriteTop = target;
@@ -38,9 +45,14 @@ function createScrollFollow(options) {
38
45
  lastWriteClientHeight = geom.clientHeight;
39
46
  }
40
47
  setAtBottom(computeAtBottom(geometry(scroller), options.threshold()));
48
+ reportPosition();
41
49
  };
42
50
  const onScroll = () => {
43
- if (!scroller || !options.enabled()) return;
51
+ if (!scroller) return;
52
+ if (!options.enabled()) {
53
+ reportPosition();
54
+ return;
55
+ }
44
56
  const geom = geometry(scroller);
45
57
  setAtBottom(computeAtBottom(geom, options.threshold()));
46
58
  if (!isSelfScroll(geom.scrollTop, lastWriteTop)) {
@@ -59,17 +71,20 @@ function createScrollFollow(options) {
59
71
  }
60
72
  previousTop = geom.scrollTop;
61
73
  lastScrollClientHeight = geom.clientHeight;
74
+ reportPosition();
62
75
  };
63
76
  const jump = () => {
64
77
  if (!scroller || !options.enabled()) return;
65
78
  stick = true;
66
79
  writePin(scroller, bottomTarget(geometry(scroller)));
67
80
  setAtBottom(true);
81
+ reportPosition();
68
82
  scroller.focus({ preventScroll: true });
69
83
  };
70
84
  createEffect(() => {
71
85
  options.resetKey();
72
86
  const enabled = options.enabled();
87
+ const initial = untrack(() => options.initialPosition?.());
73
88
  stick = true;
74
89
  lastWriteTop = -1;
75
90
  lastWriteClientHeight = -1;
@@ -77,7 +92,14 @@ function createScrollFollow(options) {
77
92
  lastScrollClientHeight = 0;
78
93
  setAtBottom(true);
79
94
  if (!enabled || !scroller) return;
80
- writePin(scroller, bottomTarget(geometry(scroller)));
95
+ if (initial && Number.isFinite(initial.top) && initial.top >= 0) {
96
+ stick = initial.following;
97
+ writePin(scroller, stick ? bottomTarget(geometry(scroller)) : initial.top);
98
+ previousTop = scroller.scrollTop;
99
+ lastScrollClientHeight = scroller.clientHeight;
100
+ } else writePin(scroller, bottomTarget(geometry(scroller)));
101
+ setAtBottom(computeAtBottom(geometry(scroller), options.threshold()));
102
+ reportPosition();
81
103
  if (typeof ResizeObserver === "undefined") return;
82
104
  const observer = new ResizeObserver(pinAuto);
83
105
  observer.observe(scroller);
@@ -1 +1 @@
1
- {"version":3,"file":"scroll-follow.js","names":[],"sources":["../../../src/components/conversation/scroll-follow.ts"],"sourcesContent":["/*\n * Substantially translated from KiroCrew website/src/app-sdk/useChatScrollFollow.ts,\n * pinned at 283e136c0f902e965a535a7c9548c57c7504fed0.\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * This product includes software developed at Amazon.com, Inc. (https://www.amazon.com/).\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy at http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software distributed\n * under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\n * CONDITIONS OF ANY KIND, either express or implied. See the License for the\n * specific language governing permissions and limitations under the License.\n * Solid owner effects replace React refs/effects; native geometry and the\n * content/viewport observer contract are retained. No session store is copied.\n */\nimport { createEffect, createSignal, onCleanup } from 'solid-js'\nimport {\n bottomTarget,\n computeAtBottom,\n evaluateAutoPin,\n isSelfScroll,\n resolveUserScrollStick,\n} from './scroll-follow-core'\n\nconst geometry = (element: HTMLDivElement) => ({\n scrollTop: element.scrollTop,\n scrollHeight: element.scrollHeight,\n clientHeight: element.clientHeight,\n})\n\n/** Internal plain-scroller binding; host-owned restoration can disable follow. */\nexport function createScrollFollow(options: {\n enabled: () => boolean\n resetKey: () => string | undefined\n threshold: () => number\n}) {\n const [atBottom, setAtBottom] = createSignal(true)\n let scroller: HTMLDivElement | undefined\n let content: HTMLDivElement | undefined\n let stick = true\n let lastWriteTop = -1\n let lastWriteClientHeight = -1\n let previousTop = -1\n let lastScrollClientHeight = 0\n\n const writePin = (element: HTMLDivElement, target: number) => {\n // Instant writes keep the self-scroll reference synchronized. A smooth\n // animation would produce intermediate positions that resemble user input.\n element.scrollTop = target\n lastWriteTop = target\n lastWriteClientHeight = element.clientHeight\n previousTop = target\n }\n const pinAuto = () => {\n if (!scroller || !options.enabled()) return\n const geom = geometry(scroller)\n const result = evaluateAutoPin({\n stick,\n geom,\n lastWriteTop,\n viewportShrink: lastWriteClientHeight >= 0 ? lastWriteClientHeight - geom.clientHeight : 0,\n })\n stick = result.stick\n if (result.pin) writePin(scroller, result.target)\n else if (result.stick) {\n lastWriteTop = result.target\n lastWriteClientHeight = geom.clientHeight\n }\n setAtBottom(computeAtBottom(geometry(scroller), options.threshold()))\n }\n const onScroll = () => {\n if (!scroller || !options.enabled()) return\n const geom = geometry(scroller)\n setAtBottom(computeAtBottom(geom, options.threshold()))\n if (!isSelfScroll(geom.scrollTop, lastWriteTop)) {\n stick = resolveUserScrollStick({\n stick,\n followOutput: true,\n scrollTop: geom.scrollTop,\n prevScrollTop: previousTop,\n geom,\n viewportGrowth: lastScrollClientHeight > 0 ? geom.clientHeight - lastScrollClientHeight : 0,\n })\n if (!stick) {\n lastWriteTop = -1\n lastWriteClientHeight = -1\n }\n }\n previousTop = geom.scrollTop\n // This baseline belongs only to scroll events, not observer callbacks.\n // Advancing it during resize would erase the viewport-clamp evidence.\n lastScrollClientHeight = geom.clientHeight\n }\n const jump = () => {\n if (!scroller || !options.enabled()) return\n stick = true\n writePin(scroller, bottomTarget(geometry(scroller)))\n setAtBottom(true)\n // The jump control disappears at the bottom. Return its keyboard focus to\n // the transcript instead of leaving the reader on a detached button.\n scroller.focus({ preventScroll: true })\n }\n\n createEffect(() => {\n options.resetKey()\n const enabled = options.enabled()\n stick = true\n lastWriteTop = -1\n lastWriteClientHeight = -1\n previousTop = -1\n lastScrollClientHeight = 0\n setAtBottom(true)\n if (!enabled || !scroller) return\n writePin(scroller, bottomTarget(geometry(scroller)))\n if (typeof ResizeObserver === 'undefined') return\n const observer = new ResizeObserver(pinAuto)\n observer.observe(scroller)\n if (content) observer.observe(content)\n onCleanup(() => observer.disconnect())\n })\n\n return {\n atBottom,\n onScroll,\n jump,\n bindScroller: (element: HTMLDivElement) => {\n scroller = element\n },\n bindContent: (element: HTMLDivElement) => {\n content = element\n },\n }\n}\n"],"mappings":";;;AAwBA,IAAM,YAAY,aAA6B;CAC7C,WAAW,QAAQ;CACnB,cAAc,QAAQ;CACtB,cAAc,QAAQ;AACxB;;AAGA,SAAgB,mBAAmB,SAIhC;CACD,MAAM,CAAC,UAAU,eAAe,aAAa,IAAI;CACjD,IAAI;CACJ,IAAI;CACJ,IAAI,QAAQ;CACZ,IAAI,eAAe;CACnB,IAAI,wBAAwB;CAC5B,IAAI,cAAc;CAClB,IAAI,yBAAyB;CAE7B,MAAM,YAAY,SAAyB,WAAmB;EAG5D,QAAQ,YAAY;EACpB,eAAe;EACf,wBAAwB,QAAQ;EAChC,cAAc;CAChB;CACA,MAAM,gBAAgB;EACpB,IAAI,CAAC,YAAY,CAAC,QAAQ,QAAQ,GAAG;EACrC,MAAM,OAAO,SAAS,QAAQ;EAC9B,MAAM,SAAS,gBAAgB;GAC7B;GACA;GACA;GACA,gBAAgB,yBAAyB,IAAI,wBAAwB,KAAK,eAAe;EAC3F,CAAC;EACD,QAAQ,OAAO;EACf,IAAI,OAAO,KAAK,SAAS,UAAU,OAAO,MAAM;OAC3C,IAAI,OAAO,OAAO;GACrB,eAAe,OAAO;GACtB,wBAAwB,KAAK;EAC/B;EACA,YAAY,gBAAgB,SAAS,QAAQ,GAAG,QAAQ,UAAU,CAAC,CAAC;CACtE;CACA,MAAM,iBAAiB;EACrB,IAAI,CAAC,YAAY,CAAC,QAAQ,QAAQ,GAAG;EACrC,MAAM,OAAO,SAAS,QAAQ;EAC9B,YAAY,gBAAgB,MAAM,QAAQ,UAAU,CAAC,CAAC;EACtD,IAAI,CAAC,aAAa,KAAK,WAAW,YAAY,GAAG;GAC/C,QAAQ,uBAAuB;IAC7B;IACA,cAAc;IACd,WAAW,KAAK;IAChB,eAAe;IACf;IACA,gBAAgB,yBAAyB,IAAI,KAAK,eAAe,yBAAyB;GAC5F,CAAC;GACD,IAAI,CAAC,OAAO;IACV,eAAe;IACf,wBAAwB;GAC1B;EACF;EACA,cAAc,KAAK;EAGnB,yBAAyB,KAAK;CAChC;CACA,MAAM,aAAa;EACjB,IAAI,CAAC,YAAY,CAAC,QAAQ,QAAQ,GAAG;EACrC,QAAQ;EACR,SAAS,UAAU,aAAa,SAAS,QAAQ,CAAC,CAAC;EACnD,YAAY,IAAI;EAGhB,SAAS,MAAM,EAAE,eAAe,KAAK,CAAC;CACxC;CAEA,mBAAmB;EACjB,QAAQ,SAAS;EACjB,MAAM,UAAU,QAAQ,QAAQ;EAChC,QAAQ;EACR,eAAe;EACf,wBAAwB;EACxB,cAAc;EACd,yBAAyB;EACzB,YAAY,IAAI;EAChB,IAAI,CAAC,WAAW,CAAC,UAAU;EAC3B,SAAS,UAAU,aAAa,SAAS,QAAQ,CAAC,CAAC;EACnD,IAAI,OAAO,mBAAmB,aAAa;EAC3C,MAAM,WAAW,IAAI,eAAe,OAAO;EAC3C,SAAS,QAAQ,QAAQ;EACzB,IAAI,SAAS,SAAS,QAAQ,OAAO;EACrC,gBAAgB,SAAS,WAAW,CAAC;CACvC,CAAC;CAED,OAAO;EACL;EACA;EACA;EACA,eAAe,YAA4B;GACzC,WAAW;EACb;EACA,cAAc,YAA4B;GACxC,UAAU;EACZ;CACF;AACF"}
1
+ {"version":3,"file":"scroll-follow.js","names":[],"sources":["../../../src/components/conversation/scroll-follow.ts"],"sourcesContent":["/*\n * Substantially translated from KiroCrew website/src/app-sdk/useChatScrollFollow.ts,\n * pinned at 283e136c0f902e965a535a7c9548c57c7504fed0.\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * This product includes software developed at Amazon.com, Inc. (https://www.amazon.com/).\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy at http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software distributed\n * under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR\n * CONDITIONS OF ANY KIND, either express or implied. See the License for the\n * specific language governing permissions and limitations under the License.\n * Solid owner effects replace React refs/effects; native geometry and the\n * content/viewport observer contract are retained. No session store is copied.\n */\nimport { createEffect, createSignal, onCleanup, untrack } from 'solid-js'\nimport type { ConversationReadingPosition } from './conversation-surface'\nimport {\n bottomTarget,\n computeAtBottom,\n evaluateAutoPin,\n isSelfScroll,\n resolveUserScrollStick,\n} from './scroll-follow-core'\n\nconst geometry = (element: HTMLDivElement) => ({\n scrollTop: element.scrollTop,\n scrollHeight: element.scrollHeight,\n clientHeight: element.clientHeight,\n})\n\n/** Internal plain-scroller binding; host-owned restoration can disable follow. */\nexport function createScrollFollow(options: {\n enabled: () => boolean\n resetKey: () => string | undefined\n threshold: () => number\n initialPosition?: () => ConversationReadingPosition | undefined\n onPositionChange?: (position: ConversationReadingPosition) => void\n}) {\n const [atBottom, setAtBottom] = createSignal(true)\n let scroller: HTMLDivElement | undefined\n let content: HTMLDivElement | undefined\n let stick = true\n let lastWriteTop = -1\n let lastWriteClientHeight = -1\n let previousTop = -1\n let lastScrollClientHeight = 0\n const reportPosition = () => {\n if (scroller)\n untrack(() =>\n options.onPositionChange?.({\n top: scroller!.scrollTop,\n following: options.enabled() && stick,\n })\n )\n }\n onCleanup(reportPosition)\n\n const writePin = (element: HTMLDivElement, target: number) => {\n // Instant writes keep the self-scroll reference synchronized. A smooth\n // animation would produce intermediate positions that resemble user input.\n element.scrollTop = target\n lastWriteTop = target\n lastWriteClientHeight = element.clientHeight\n previousTop = target\n }\n const pinAuto = () => {\n if (!scroller || !options.enabled()) return\n const geom = geometry(scroller)\n const result = evaluateAutoPin({\n stick,\n geom,\n lastWriteTop,\n viewportShrink: lastWriteClientHeight >= 0 ? lastWriteClientHeight - geom.clientHeight : 0,\n })\n stick = result.stick\n if (result.pin) writePin(scroller, result.target)\n else if (result.stick) {\n lastWriteTop = result.target\n lastWriteClientHeight = geom.clientHeight\n }\n setAtBottom(computeAtBottom(geometry(scroller), options.threshold()))\n reportPosition()\n }\n const onScroll = () => {\n if (!scroller) return\n if (!options.enabled()) {\n reportPosition()\n return\n }\n const geom = geometry(scroller)\n setAtBottom(computeAtBottom(geom, options.threshold()))\n if (!isSelfScroll(geom.scrollTop, lastWriteTop)) {\n stick = resolveUserScrollStick({\n stick,\n followOutput: true,\n scrollTop: geom.scrollTop,\n prevScrollTop: previousTop,\n geom,\n viewportGrowth: lastScrollClientHeight > 0 ? geom.clientHeight - lastScrollClientHeight : 0,\n })\n if (!stick) {\n lastWriteTop = -1\n lastWriteClientHeight = -1\n }\n }\n previousTop = geom.scrollTop\n // This baseline belongs only to scroll events, not observer callbacks.\n // Advancing it during resize would erase the viewport-clamp evidence.\n lastScrollClientHeight = geom.clientHeight\n reportPosition()\n }\n const jump = () => {\n if (!scroller || !options.enabled()) return\n stick = true\n writePin(scroller, bottomTarget(geometry(scroller)))\n setAtBottom(true)\n reportPosition()\n // The jump control disappears at the bottom. Return its keyboard focus to\n // the transcript instead of leaving the reader on a detached button.\n scroller.focus({ preventScroll: true })\n }\n\n createEffect(() => {\n options.resetKey()\n const enabled = options.enabled()\n // Capture before any reporting callback can replace the host snapshot.\n const initial = untrack(() => options.initialPosition?.())\n stick = true\n lastWriteTop = -1\n lastWriteClientHeight = -1\n previousTop = -1\n lastScrollClientHeight = 0\n setAtBottom(true)\n if (!enabled || !scroller) return\n // Snapshot updates must not become another positioning owner. Consume the\n // host's snapshot only at entry/reset, then native reader intent owns follow.\n if (initial && Number.isFinite(initial.top) && initial.top >= 0) {\n stick = initial.following\n writePin(scroller, stick ? bottomTarget(geometry(scroller)) : initial.top)\n previousTop = scroller.scrollTop\n lastScrollClientHeight = scroller.clientHeight\n } else writePin(scroller, bottomTarget(geometry(scroller)))\n setAtBottom(computeAtBottom(geometry(scroller), options.threshold()))\n reportPosition()\n if (typeof ResizeObserver === 'undefined') return\n const observer = new ResizeObserver(pinAuto)\n observer.observe(scroller)\n if (content) observer.observe(content)\n onCleanup(() => observer.disconnect())\n })\n\n return {\n atBottom,\n onScroll,\n jump,\n bindScroller: (element: HTMLDivElement) => {\n scroller = element\n },\n bindContent: (element: HTMLDivElement) => {\n content = element\n },\n }\n}\n"],"mappings":";;;AAyBA,IAAM,YAAY,aAA6B;CAC7C,WAAW,QAAQ;CACnB,cAAc,QAAQ;CACtB,cAAc,QAAQ;AACxB;;AAGA,SAAgB,mBAAmB,SAMhC;CACD,MAAM,CAAC,UAAU,eAAe,aAAa,IAAI;CACjD,IAAI;CACJ,IAAI;CACJ,IAAI,QAAQ;CACZ,IAAI,eAAe;CACnB,IAAI,wBAAwB;CAC5B,IAAI,cAAc;CAClB,IAAI,yBAAyB;CAC7B,MAAM,uBAAuB;EAC3B,IAAI,UACF,cACE,QAAQ,mBAAmB;GACzB,KAAK,SAAU;GACf,WAAW,QAAQ,QAAQ,KAAK;EAClC,CAAC,CACH;CACJ;CACA,UAAU,cAAc;CAExB,MAAM,YAAY,SAAyB,WAAmB;EAG5D,QAAQ,YAAY;EACpB,eAAe;EACf,wBAAwB,QAAQ;EAChC,cAAc;CAChB;CACA,MAAM,gBAAgB;EACpB,IAAI,CAAC,YAAY,CAAC,QAAQ,QAAQ,GAAG;EACrC,MAAM,OAAO,SAAS,QAAQ;EAC9B,MAAM,SAAS,gBAAgB;GAC7B;GACA;GACA;GACA,gBAAgB,yBAAyB,IAAI,wBAAwB,KAAK,eAAe;EAC3F,CAAC;EACD,QAAQ,OAAO;EACf,IAAI,OAAO,KAAK,SAAS,UAAU,OAAO,MAAM;OAC3C,IAAI,OAAO,OAAO;GACrB,eAAe,OAAO;GACtB,wBAAwB,KAAK;EAC/B;EACA,YAAY,gBAAgB,SAAS,QAAQ,GAAG,QAAQ,UAAU,CAAC,CAAC;EACpE,eAAe;CACjB;CACA,MAAM,iBAAiB;EACrB,IAAI,CAAC,UAAU;EACf,IAAI,CAAC,QAAQ,QAAQ,GAAG;GACtB,eAAe;GACf;EACF;EACA,MAAM,OAAO,SAAS,QAAQ;EAC9B,YAAY,gBAAgB,MAAM,QAAQ,UAAU,CAAC,CAAC;EACtD,IAAI,CAAC,aAAa,KAAK,WAAW,YAAY,GAAG;GAC/C,QAAQ,uBAAuB;IAC7B;IACA,cAAc;IACd,WAAW,KAAK;IAChB,eAAe;IACf;IACA,gBAAgB,yBAAyB,IAAI,KAAK,eAAe,yBAAyB;GAC5F,CAAC;GACD,IAAI,CAAC,OAAO;IACV,eAAe;IACf,wBAAwB;GAC1B;EACF;EACA,cAAc,KAAK;EAGnB,yBAAyB,KAAK;EAC9B,eAAe;CACjB;CACA,MAAM,aAAa;EACjB,IAAI,CAAC,YAAY,CAAC,QAAQ,QAAQ,GAAG;EACrC,QAAQ;EACR,SAAS,UAAU,aAAa,SAAS,QAAQ,CAAC,CAAC;EACnD,YAAY,IAAI;EAChB,eAAe;EAGf,SAAS,MAAM,EAAE,eAAe,KAAK,CAAC;CACxC;CAEA,mBAAmB;EACjB,QAAQ,SAAS;EACjB,MAAM,UAAU,QAAQ,QAAQ;EAEhC,MAAM,UAAU,cAAc,QAAQ,kBAAkB,CAAC;EACzD,QAAQ;EACR,eAAe;EACf,wBAAwB;EACxB,cAAc;EACd,yBAAyB;EACzB,YAAY,IAAI;EAChB,IAAI,CAAC,WAAW,CAAC,UAAU;EAG3B,IAAI,WAAW,OAAO,SAAS,QAAQ,GAAG,KAAK,QAAQ,OAAO,GAAG;GAC/D,QAAQ,QAAQ;GAChB,SAAS,UAAU,QAAQ,aAAa,SAAS,QAAQ,CAAC,IAAI,QAAQ,GAAG;GACzE,cAAc,SAAS;GACvB,yBAAyB,SAAS;EACpC,OAAO,SAAS,UAAU,aAAa,SAAS,QAAQ,CAAC,CAAC;EAC1D,YAAY,gBAAgB,SAAS,QAAQ,GAAG,QAAQ,UAAU,CAAC,CAAC;EACpE,eAAe;EACf,IAAI,OAAO,mBAAmB,aAAa;EAC3C,MAAM,WAAW,IAAI,eAAe,OAAO;EAC3C,SAAS,QAAQ,QAAQ;EACzB,IAAI,SAAS,SAAS,QAAQ,OAAO;EACrC,gBAAgB,SAAS,WAAW,CAAC;CACvC,CAAC;CAED,OAAO;EACL;EACA;EACA;EACA,eAAe,YAA4B;GACzC,WAAW;EACb;EACA,cAAc,YAA4B;GACxC,UAAU;EACZ;CACF;AACF"}
@@ -21,6 +21,8 @@ import { createScrollFollow } from './scroll-follow'
21
21
  * than dragging the pane behind it — which in a desktop shell means the whole
22
22
  * window moves.
23
23
  */
24
+ export type ConversationReadingPosition = Readonly<{ top: number; following: boolean }>
25
+
24
26
  export type ConversationSurfaceProps = ComponentProps<'div'> & {
25
27
  /** How far from the bottom still counts as "at the bottom", in px. */
26
28
  threshold?: number
@@ -28,6 +30,10 @@ export type ConversationSurfaceProps = ComponentProps<'div'> & {
28
30
  follow?: boolean
29
31
  /** Optional conversation identity: changing it re-arms follow at the bottom. */
30
32
  resetKey?: string
33
+ /** Host snapshot consumed on mount/identity reset, never on ordinary prop updates. */
34
+ initialReadingPosition?: ConversationReadingPosition
35
+ /** Actual follow intent and native offset; storage/identity remain host-owned. */
36
+ onReadingPositionChange?: (position: ConversationReadingPosition) => void
31
37
  /** Rendered above the transcript when there is nothing in it. */
32
38
  empty?: JSX.Element
33
39
  /** A header that scrolls with the transcript, e.g. a day divider or a banner. */
@@ -40,6 +46,8 @@ export function ConversationSurface(props: ConversationSurfaceProps) {
40
46
  'threshold',
41
47
  'follow',
42
48
  'resetKey',
49
+ 'initialReadingPosition',
50
+ 'onReadingPositionChange',
43
51
  'empty',
44
52
  'header',
45
53
  'children',
@@ -54,6 +62,8 @@ export function ConversationSurface(props: ConversationSurfaceProps) {
54
62
  enabled: () => local.follow !== false,
55
63
  resetKey: () => local.resetKey,
56
64
  threshold: () => local.threshold ?? 80,
65
+ initialPosition: () => local.initialReadingPosition,
66
+ onPositionChange: (position) => local.onReadingPositionChange?.(position),
57
67
  })
58
68
  const onScroll: JSX.EventHandlerUnion<HTMLDivElement, Event> = (event) => {
59
69
  follow.onScroll()
@@ -5,7 +5,11 @@ export {
5
5
  type ConversationAvatarKind,
6
6
  type ConversationAvatarProps,
7
7
  } from './conversation-avatar'
8
- export { ConversationSurface, type ConversationSurfaceProps } from './conversation-surface'
8
+ export {
9
+ ConversationSurface,
10
+ type ConversationSurfaceProps,
11
+ type ConversationReadingPosition,
12
+ } from './conversation-surface'
9
13
  export {
10
14
  ComposerAttachmentButton,
11
15
  ComposerHint,
@@ -13,7 +13,8 @@
13
13
  * Solid owner effects replace React refs/effects; native geometry and the
14
14
  * content/viewport observer contract are retained. No session store is copied.
15
15
  */
16
- import { createEffect, createSignal, onCleanup } from 'solid-js'
16
+ import { createEffect, createSignal, onCleanup, untrack } from 'solid-js'
17
+ import type { ConversationReadingPosition } from './conversation-surface'
17
18
  import {
18
19
  bottomTarget,
19
20
  computeAtBottom,
@@ -33,6 +34,8 @@ export function createScrollFollow(options: {
33
34
  enabled: () => boolean
34
35
  resetKey: () => string | undefined
35
36
  threshold: () => number
37
+ initialPosition?: () => ConversationReadingPosition | undefined
38
+ onPositionChange?: (position: ConversationReadingPosition) => void
36
39
  }) {
37
40
  const [atBottom, setAtBottom] = createSignal(true)
38
41
  let scroller: HTMLDivElement | undefined
@@ -42,6 +45,16 @@ export function createScrollFollow(options: {
42
45
  let lastWriteClientHeight = -1
43
46
  let previousTop = -1
44
47
  let lastScrollClientHeight = 0
48
+ const reportPosition = () => {
49
+ if (scroller)
50
+ untrack(() =>
51
+ options.onPositionChange?.({
52
+ top: scroller!.scrollTop,
53
+ following: options.enabled() && stick,
54
+ })
55
+ )
56
+ }
57
+ onCleanup(reportPosition)
45
58
 
46
59
  const writePin = (element: HTMLDivElement, target: number) => {
47
60
  // Instant writes keep the self-scroll reference synchronized. A smooth
@@ -67,9 +80,14 @@ export function createScrollFollow(options: {
67
80
  lastWriteClientHeight = geom.clientHeight
68
81
  }
69
82
  setAtBottom(computeAtBottom(geometry(scroller), options.threshold()))
83
+ reportPosition()
70
84
  }
71
85
  const onScroll = () => {
72
- if (!scroller || !options.enabled()) return
86
+ if (!scroller) return
87
+ if (!options.enabled()) {
88
+ reportPosition()
89
+ return
90
+ }
73
91
  const geom = geometry(scroller)
74
92
  setAtBottom(computeAtBottom(geom, options.threshold()))
75
93
  if (!isSelfScroll(geom.scrollTop, lastWriteTop)) {
@@ -90,12 +108,14 @@ export function createScrollFollow(options: {
90
108
  // This baseline belongs only to scroll events, not observer callbacks.
91
109
  // Advancing it during resize would erase the viewport-clamp evidence.
92
110
  lastScrollClientHeight = geom.clientHeight
111
+ reportPosition()
93
112
  }
94
113
  const jump = () => {
95
114
  if (!scroller || !options.enabled()) return
96
115
  stick = true
97
116
  writePin(scroller, bottomTarget(geometry(scroller)))
98
117
  setAtBottom(true)
118
+ reportPosition()
99
119
  // The jump control disappears at the bottom. Return its keyboard focus to
100
120
  // the transcript instead of leaving the reader on a detached button.
101
121
  scroller.focus({ preventScroll: true })
@@ -104,6 +124,8 @@ export function createScrollFollow(options: {
104
124
  createEffect(() => {
105
125
  options.resetKey()
106
126
  const enabled = options.enabled()
127
+ // Capture before any reporting callback can replace the host snapshot.
128
+ const initial = untrack(() => options.initialPosition?.())
107
129
  stick = true
108
130
  lastWriteTop = -1
109
131
  lastWriteClientHeight = -1
@@ -111,7 +133,16 @@ export function createScrollFollow(options: {
111
133
  lastScrollClientHeight = 0
112
134
  setAtBottom(true)
113
135
  if (!enabled || !scroller) return
114
- writePin(scroller, bottomTarget(geometry(scroller)))
136
+ // Snapshot updates must not become another positioning owner. Consume the
137
+ // host's snapshot only at entry/reset, then native reader intent owns follow.
138
+ if (initial && Number.isFinite(initial.top) && initial.top >= 0) {
139
+ stick = initial.following
140
+ writePin(scroller, stick ? bottomTarget(geometry(scroller)) : initial.top)
141
+ previousTop = scroller.scrollTop
142
+ lastScrollClientHeight = scroller.clientHeight
143
+ } else writePin(scroller, bottomTarget(geometry(scroller)))
144
+ setAtBottom(computeAtBottom(geometry(scroller), options.threshold()))
145
+ reportPosition()
115
146
  if (typeof ResizeObserver === 'undefined') return
116
147
  const observer = new ResizeObserver(pinAuto)
117
148
  observer.observe(scroller)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adea-ai/ui",
3
- "version": "0.67.0",
3
+ "version": "0.67.2",
4
4
  "description": "The Adea design system: Kobalte-backed Solid components, semantic OKLCH tokens, and the app-shell layout primitives shared by Adea and Cortana.",
5
5
  "keywords": [
6
6
  "adea",
@@ -21,6 +21,8 @@ import { createScrollFollow } from './scroll-follow'
21
21
  * than dragging the pane behind it — which in a desktop shell means the whole
22
22
  * window moves.
23
23
  */
24
+ export type ConversationReadingPosition = Readonly<{ top: number; following: boolean }>
25
+
24
26
  export type ConversationSurfaceProps = ComponentProps<'div'> & {
25
27
  /** How far from the bottom still counts as "at the bottom", in px. */
26
28
  threshold?: number
@@ -28,6 +30,10 @@ export type ConversationSurfaceProps = ComponentProps<'div'> & {
28
30
  follow?: boolean
29
31
  /** Optional conversation identity: changing it re-arms follow at the bottom. */
30
32
  resetKey?: string
33
+ /** Host snapshot consumed on mount/identity reset, never on ordinary prop updates. */
34
+ initialReadingPosition?: ConversationReadingPosition
35
+ /** Actual follow intent and native offset; storage/identity remain host-owned. */
36
+ onReadingPositionChange?: (position: ConversationReadingPosition) => void
31
37
  /** Rendered above the transcript when there is nothing in it. */
32
38
  empty?: JSX.Element
33
39
  /** A header that scrolls with the transcript, e.g. a day divider or a banner. */
@@ -40,6 +46,8 @@ export function ConversationSurface(props: ConversationSurfaceProps) {
40
46
  'threshold',
41
47
  'follow',
42
48
  'resetKey',
49
+ 'initialReadingPosition',
50
+ 'onReadingPositionChange',
43
51
  'empty',
44
52
  'header',
45
53
  'children',
@@ -54,6 +62,8 @@ export function ConversationSurface(props: ConversationSurfaceProps) {
54
62
  enabled: () => local.follow !== false,
55
63
  resetKey: () => local.resetKey,
56
64
  threshold: () => local.threshold ?? 80,
65
+ initialPosition: () => local.initialReadingPosition,
66
+ onPositionChange: (position) => local.onReadingPositionChange?.(position),
57
67
  })
58
68
  const onScroll: JSX.EventHandlerUnion<HTMLDivElement, Event> = (event) => {
59
69
  follow.onScroll()
@@ -5,7 +5,11 @@ export {
5
5
  type ConversationAvatarKind,
6
6
  type ConversationAvatarProps,
7
7
  } from './conversation-avatar'
8
- export { ConversationSurface, type ConversationSurfaceProps } from './conversation-surface'
8
+ export {
9
+ ConversationSurface,
10
+ type ConversationSurfaceProps,
11
+ type ConversationReadingPosition,
12
+ } from './conversation-surface'
9
13
  export {
10
14
  ComposerAttachmentButton,
11
15
  ComposerHint,
@@ -13,7 +13,8 @@
13
13
  * Solid owner effects replace React refs/effects; native geometry and the
14
14
  * content/viewport observer contract are retained. No session store is copied.
15
15
  */
16
- import { createEffect, createSignal, onCleanup } from 'solid-js'
16
+ import { createEffect, createSignal, onCleanup, untrack } from 'solid-js'
17
+ import type { ConversationReadingPosition } from './conversation-surface'
17
18
  import {
18
19
  bottomTarget,
19
20
  computeAtBottom,
@@ -33,6 +34,8 @@ export function createScrollFollow(options: {
33
34
  enabled: () => boolean
34
35
  resetKey: () => string | undefined
35
36
  threshold: () => number
37
+ initialPosition?: () => ConversationReadingPosition | undefined
38
+ onPositionChange?: (position: ConversationReadingPosition) => void
36
39
  }) {
37
40
  const [atBottom, setAtBottom] = createSignal(true)
38
41
  let scroller: HTMLDivElement | undefined
@@ -42,6 +45,16 @@ export function createScrollFollow(options: {
42
45
  let lastWriteClientHeight = -1
43
46
  let previousTop = -1
44
47
  let lastScrollClientHeight = 0
48
+ const reportPosition = () => {
49
+ if (scroller)
50
+ untrack(() =>
51
+ options.onPositionChange?.({
52
+ top: scroller!.scrollTop,
53
+ following: options.enabled() && stick,
54
+ })
55
+ )
56
+ }
57
+ onCleanup(reportPosition)
45
58
 
46
59
  const writePin = (element: HTMLDivElement, target: number) => {
47
60
  // Instant writes keep the self-scroll reference synchronized. A smooth
@@ -67,9 +80,14 @@ export function createScrollFollow(options: {
67
80
  lastWriteClientHeight = geom.clientHeight
68
81
  }
69
82
  setAtBottom(computeAtBottom(geometry(scroller), options.threshold()))
83
+ reportPosition()
70
84
  }
71
85
  const onScroll = () => {
72
- if (!scroller || !options.enabled()) return
86
+ if (!scroller) return
87
+ if (!options.enabled()) {
88
+ reportPosition()
89
+ return
90
+ }
73
91
  const geom = geometry(scroller)
74
92
  setAtBottom(computeAtBottom(geom, options.threshold()))
75
93
  if (!isSelfScroll(geom.scrollTop, lastWriteTop)) {
@@ -90,12 +108,14 @@ export function createScrollFollow(options: {
90
108
  // This baseline belongs only to scroll events, not observer callbacks.
91
109
  // Advancing it during resize would erase the viewport-clamp evidence.
92
110
  lastScrollClientHeight = geom.clientHeight
111
+ reportPosition()
93
112
  }
94
113
  const jump = () => {
95
114
  if (!scroller || !options.enabled()) return
96
115
  stick = true
97
116
  writePin(scroller, bottomTarget(geometry(scroller)))
98
117
  setAtBottom(true)
118
+ reportPosition()
99
119
  // The jump control disappears at the bottom. Return its keyboard focus to
100
120
  // the transcript instead of leaving the reader on a detached button.
101
121
  scroller.focus({ preventScroll: true })
@@ -104,6 +124,8 @@ export function createScrollFollow(options: {
104
124
  createEffect(() => {
105
125
  options.resetKey()
106
126
  const enabled = options.enabled()
127
+ // Capture before any reporting callback can replace the host snapshot.
128
+ const initial = untrack(() => options.initialPosition?.())
107
129
  stick = true
108
130
  lastWriteTop = -1
109
131
  lastWriteClientHeight = -1
@@ -111,7 +133,16 @@ export function createScrollFollow(options: {
111
133
  lastScrollClientHeight = 0
112
134
  setAtBottom(true)
113
135
  if (!enabled || !scroller) return
114
- writePin(scroller, bottomTarget(geometry(scroller)))
136
+ // Snapshot updates must not become another positioning owner. Consume the
137
+ // host's snapshot only at entry/reset, then native reader intent owns follow.
138
+ if (initial && Number.isFinite(initial.top) && initial.top >= 0) {
139
+ stick = initial.following
140
+ writePin(scroller, stick ? bottomTarget(geometry(scroller)) : initial.top)
141
+ previousTop = scroller.scrollTop
142
+ lastScrollClientHeight = scroller.clientHeight
143
+ } else writePin(scroller, bottomTarget(geometry(scroller)))
144
+ setAtBottom(computeAtBottom(geometry(scroller), options.threshold()))
145
+ reportPosition()
115
146
  if (typeof ResizeObserver === 'undefined') return
116
147
  const observer = new ResizeObserver(pinAuto)
117
148
  observer.observe(scroller)