@kanzo-tech/ai 0.10.0 → 0.11.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.
@@ -22,8 +22,8 @@ export declare const Conversation: (props: React.ComponentProps<typeof ark.div>)
22
22
  export declare const ConversationContent: (props: React.ComponentProps<typeof ark.div>) => React.JSX.Element;
23
23
  /**
24
24
  * The stage for the zero state, not the zero state itself: a centred column with a reading measure,
25
- * which `Item` and its parts are then composed into. There is deliberately no `title` / `description`
26
- * / `icon` prop — `EmptyState` was exactly that and it is a tombstone in `@kanzo-tech/ui`.
25
+ * which the caller's markup is composed into. There is deliberately no `title` / `description` /
26
+ * `icon` prop — `EmptyState` was exactly that and it is a tombstone in `@kanzo-tech/ui`.
27
27
  */
28
28
  export declare const ConversationEmpty: (props: React.ComponentProps<typeof ark.div>) => React.JSX.Element;
29
29
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"conversation.js","sources":["../src/conversation.tsx"],"sourcesContent":["\"use client\";\n\nimport { ark } from \"@ark-ui/react/factory\";\nimport { ArrowDownIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport { Button, cn } from \"@kanzo-tech/ui\";\n\ninterface ConversationCtx {\n viewportRef: (el: HTMLDivElement | null) => void;\n contentRef: (el: HTMLDivElement | null) => void;\n pinned: boolean;\n scrollToEnd: () => void;\n}\n\nconst Ctx = React.createContext<ConversationCtx | null>(null);\nconst useCtx = (part: string) => {\n const c = React.useContext(Ctx);\n if (!c) throw new Error(`${part} must render inside <Conversation>`);\n return c;\n};\n\n/** A scroll position is fractional and rarely lands on the end exactly; this is the allowance. */\nconst AT_END = 24;\n\n/**\n * The transcript's scroll container, and the pin is the whole component.\n *\n * **The naive version, which both real consumers wrote:** an effect on the message array calling\n * `scrollTo({ top: scrollHeight, behavior: \"smooth\" })`. It follows the tail unconditionally, so a\n * reader who scrolled up to re-read something is dragged back down by the next token, every token,\n * for as long as the answer is streaming. Following is only correct while the reader is *already*\n * at the tail: scrolling up releases the pin, and `ConversationScrollButton` is how they take it\n * back. Roughly forty lines of listener and observer, and no dependency.\n */\nexport const Conversation = (props: React.ComponentProps<typeof ark.div>) => {\n const { className, children, slot, ...rest } = props;\n const [viewport, setViewport] = React.useState<HTMLDivElement | null>(null);\n const [content, setContent] = React.useState<HTMLDivElement | null>(null);\n const [pinned, setPinned] = React.useState(true);\n const pinnedRef = React.useRef(true);\n\n const scrollToEnd = React.useCallback(() => {\n if (!viewport) return;\n pinnedRef.current = true;\n setPinned(true);\n viewport.scrollTop = viewport.scrollHeight;\n }, [viewport]);\n\n React.useEffect(() => {\n if (!viewport) return;\n\n const sync = () => {\n const atEnd = viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight <= AT_END;\n pinnedRef.current = atEnd;\n setPinned(atEnd);\n };\n sync();\n viewport.addEventListener(\"scroll\", sync, { passive: true });\n\n // A stream produces *growth*, not scrolling: the content gets taller under a stationary\n // `scrollTop`, and no scroll event is fired for that. Observing the viewport alone never sees\n // it either — its own box did not change — which is why the inner element is observed too.\n const follow = () => {\n if (pinnedRef.current) viewport.scrollTop = viewport.scrollHeight;\n };\n const observer = typeof ResizeObserver === \"undefined\" ? null : new ResizeObserver(follow);\n observer?.observe(viewport);\n if (content) observer?.observe(content);\n\n return () => {\n viewport.removeEventListener(\"scroll\", sync);\n observer?.disconnect();\n };\n }, [viewport, content]);\n\n return (\n <Ctx.Provider\n value={{ viewportRef: setViewport, contentRef: setContent, pinned, scrollToEnd }}\n >\n <ark.div\n className={cn(\"relative flex min-h-0 flex-1 flex-col\", className)}\n {...rest}\n data-slot={slot ?? \"conversation\"}\n >\n {children}\n </ark.div>\n </Ctx.Provider>\n );\n};\n\n/**\n * The scrolling region.\n *\n * ARIA: `role=\"log\"` carries an implicit `aria-live=\"polite\"`, so a message appended while the\n * reader is somewhere else is announced after what they are on rather than over it. Nothing inside\n * declares a role — a message is content, not a widget — so there is no keyboard contract to owe.\n */\nexport const ConversationContent = (props: React.ComponentProps<typeof ark.div>) => {\n const { className, children, slot, ...rest } = props;\n const ctx = useCtx(\"ConversationContent\");\n\n return (\n <ark.div\n className={cn(\n \"flex min-h-0 flex-1 flex-col overflow-y-auto overscroll-contain\",\n \"px-4 py-6\",\n className\n )}\n ref={ctx.viewportRef}\n role=\"log\"\n {...rest}\n data-slot={slot ?? \"conversation-content\"}\n >\n {/* `flex-1` gives the inner element the viewport's height when the transcript is short, which\n is what lets `ConversationEmpty` centre itself in the panel rather than sit at the top. */}\n <ark.div\n className=\"mx-auto flex w-full max-w-3xl flex-1 flex-col gap-6\"\n ref={ctx.contentRef}\n >\n {children}\n </ark.div>\n </ark.div>\n );\n};\n\n/**\n * The stage for the zero state, not the zero state itself: a centred column with a reading measure,\n * which `Item` and its parts are then composed into. There is deliberately no `title` / `description`\n * / `icon` prop — `EmptyState` was exactly that and it is a tombstone in `@kanzo-tech/ui`.\n */\nexport const ConversationEmpty = (props: React.ComponentProps<typeof ark.div>) => {\n const { className, slot, ...rest } = props;\n\n return (\n <ark.div\n className={cn(\n \"flex flex-1 flex-col items-center justify-center gap-3\",\n \"mx-auto max-w-md px-4 py-12\",\n \"text-balance text-center text-muted-foreground text-sm\",\n \"[&_svg:not([class*='size-'])]:size-6\",\n className\n )}\n {...rest}\n data-slot={slot ?? \"conversation-empty\"}\n />\n );\n};\n\n/**\n * Present only while the pin is released, and pressing it takes the pin back.\n *\n * It floats *over* a transcript that is still growing underneath it, which is why it enters rather\n * than appears and why it paints its own ground: `variant=\"outline\"` sets `--btn-bg: transparent`,\n * so without a ground of its own the message it covers reads straight through it.\n *\n * It asks for that ground by **moving the variable**, not by writing `bg-background` on top. The\n * two look identical at rest and diverge the moment you hover: the wash is\n * `color-mix(…, var(--btn-bg), …)`, so a `bg-background` that only overrode the resting fill left\n * the hover mixing from `transparent` and the transcript showed through the button exactly when the\n * pointer was on it. `--btn-bg` is the seam the recipe documents; this is what it is for.\n */\nexport const ConversationScrollButton = (props: React.ComponentProps<typeof Button>) => {\n const { className, children, onClick, slot, ...rest } = props;\n const ctx = useCtx(\"ConversationScrollButton\");\n\n if (ctx.pinned) return null;\n\n return (\n <Button\n aria-label={children === undefined ? \"Scroll to the latest message\" : undefined}\n className={cn(\n \"absolute inset-x-0 bottom-4 z-10 mx-auto\",\n // `inset-x-0` is what centres it, and it also stretches it, so the width has to be stated.\n // The icon size states its own — `w-(--size)`, square — and a `w-fit` here would win the\n // merge and hand it the icon's width against the control's height.\n children === undefined ? undefined : \"w-fit\",\n \"[--btn-bg:var(--background)] shadow-lg/5\",\n \"fade-in-0 zoom-in-95 slide-in-from-bottom-2 animate-in\",\n \"motion-reduce:animate-none!\",\n className\n )}\n onClick={(event) => {\n onClick?.(event);\n if (!event.defaultPrevented) ctx.scrollToEnd();\n }}\n pill\n // Icon-only by default; give it words and it becomes a pill wide enough to hold them, the way\n // `PromptInputSubmit` does.\n size={children === undefined ? \"icon-sm\" : \"sm\"}\n variant=\"outline\"\n {...rest}\n slot={slot ?? \"conversation-scroll-button\"}\n >\n {children ?? <ArrowDownIcon />}\n </Button>\n );\n};\n"],"names":[],"mappings":";;;;;;AAcA;AAEE;AACA;AACA;AACF;AAgBE;AAOE;AAG8B;AAGhC;AACE;AAEA;AACE;AACA;AACe;AAEjB;AAMA;AACE;AAAqD;AAGvD;AAIE;AACU;AACZ;AAIA;AAAK;AAAJ;AACoE;AAEnE;AAAK;AAAJ;AACiE;AAC5D;AACe;AAElB;AAAA;AAAA;AACH;AAGN;AAUE;AAGA;AACE;AAAK;AAAJ;AACY;AACT;AACA;AACA;AAAA;AAEO;AACJ;AACD;AACe;AAInB;AAAK;AAAJ;AACW;AACD;AAER;AAAA;AAAA;AACH;AAGN;AAQE;AAEA;AACE;AAAK;AAAJ;AACY;AACT;AACA;AACA;AACA;AACA;AAAA;AAEE;AACe;AAAA;AAGzB;AAgBE;AAGA;AAGE;AAAC;AAAA;AACuE;AAC3D;AACT;AAAA;AAAA;AAAA;AAIqC;AACrC;AACA;AACA;AACA;AAAA;AAGA;AACiC;AACnC;AACI;AAGuC;AACnC;AACJ;AACU;AAEc;AAAA;AAGlC;;;;;;;"}
1
+ {"version":3,"file":"conversation.js","sources":["../src/conversation.tsx"],"sourcesContent":["\"use client\";\n\nimport { ark } from \"@ark-ui/react/factory\";\nimport { ArrowDownIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport { Button, cn } from \"@kanzo-tech/ui\";\n\ninterface ConversationCtx {\n viewportRef: (el: HTMLDivElement | null) => void;\n contentRef: (el: HTMLDivElement | null) => void;\n pinned: boolean;\n scrollToEnd: () => void;\n}\n\nconst Ctx = React.createContext<ConversationCtx | null>(null);\nconst useCtx = (part: string) => {\n const c = React.useContext(Ctx);\n if (!c) throw new Error(`${part} must render inside <Conversation>`);\n return c;\n};\n\n/** A scroll position is fractional and rarely lands on the end exactly; this is the allowance. */\nconst AT_END = 24;\n\n/**\n * The transcript's scroll container, and the pin is the whole component.\n *\n * **The naive version, which both real consumers wrote:** an effect on the message array calling\n * `scrollTo({ top: scrollHeight, behavior: \"smooth\" })`. It follows the tail unconditionally, so a\n * reader who scrolled up to re-read something is dragged back down by the next token, every token,\n * for as long as the answer is streaming. Following is only correct while the reader is *already*\n * at the tail: scrolling up releases the pin, and `ConversationScrollButton` is how they take it\n * back. Roughly forty lines of listener and observer, and no dependency.\n */\nexport const Conversation = (props: React.ComponentProps<typeof ark.div>) => {\n const { className, children, slot, ...rest } = props;\n const [viewport, setViewport] = React.useState<HTMLDivElement | null>(null);\n const [content, setContent] = React.useState<HTMLDivElement | null>(null);\n const [pinned, setPinned] = React.useState(true);\n const pinnedRef = React.useRef(true);\n\n const scrollToEnd = React.useCallback(() => {\n if (!viewport) return;\n pinnedRef.current = true;\n setPinned(true);\n viewport.scrollTop = viewport.scrollHeight;\n }, [viewport]);\n\n React.useEffect(() => {\n if (!viewport) return;\n\n const sync = () => {\n const atEnd = viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight <= AT_END;\n pinnedRef.current = atEnd;\n setPinned(atEnd);\n };\n sync();\n viewport.addEventListener(\"scroll\", sync, { passive: true });\n\n // A stream produces *growth*, not scrolling: the content gets taller under a stationary\n // `scrollTop`, and no scroll event is fired for that. Observing the viewport alone never sees\n // it either — its own box did not change — which is why the inner element is observed too.\n const follow = () => {\n if (pinnedRef.current) viewport.scrollTop = viewport.scrollHeight;\n };\n const observer = typeof ResizeObserver === \"undefined\" ? null : new ResizeObserver(follow);\n observer?.observe(viewport);\n if (content) observer?.observe(content);\n\n return () => {\n viewport.removeEventListener(\"scroll\", sync);\n observer?.disconnect();\n };\n }, [viewport, content]);\n\n return (\n <Ctx.Provider\n value={{ viewportRef: setViewport, contentRef: setContent, pinned, scrollToEnd }}\n >\n <ark.div\n className={cn(\"relative flex min-h-0 flex-1 flex-col\", className)}\n {...rest}\n data-slot={slot ?? \"conversation\"}\n >\n {children}\n </ark.div>\n </Ctx.Provider>\n );\n};\n\n/**\n * The scrolling region.\n *\n * ARIA: `role=\"log\"` carries an implicit `aria-live=\"polite\"`, so a message appended while the\n * reader is somewhere else is announced after what they are on rather than over it. Nothing inside\n * declares a role — a message is content, not a widget — so there is no keyboard contract to owe.\n */\nexport const ConversationContent = (props: React.ComponentProps<typeof ark.div>) => {\n const { className, children, slot, ...rest } = props;\n const ctx = useCtx(\"ConversationContent\");\n\n return (\n <ark.div\n className={cn(\n \"flex min-h-0 flex-1 flex-col overflow-y-auto overscroll-contain\",\n \"px-4 py-6\",\n className\n )}\n ref={ctx.viewportRef}\n role=\"log\"\n {...rest}\n data-slot={slot ?? \"conversation-content\"}\n >\n {/* `flex-1` gives the inner element the viewport's height when the transcript is short, which\n is what lets `ConversationEmpty` centre itself in the panel rather than sit at the top. */}\n <ark.div\n className=\"mx-auto flex w-full max-w-3xl flex-1 flex-col gap-6\"\n ref={ctx.contentRef}\n >\n {children}\n </ark.div>\n </ark.div>\n );\n};\n\n/**\n * The stage for the zero state, not the zero state itself: a centred column with a reading measure,\n * which the caller's markup is composed into. There is deliberately no `title` / `description` /\n * `icon` prop — `EmptyState` was exactly that and it is a tombstone in `@kanzo-tech/ui`.\n */\nexport const ConversationEmpty = (props: React.ComponentProps<typeof ark.div>) => {\n const { className, slot, ...rest } = props;\n\n return (\n <ark.div\n className={cn(\n \"flex flex-1 flex-col items-center justify-center gap-3\",\n \"mx-auto max-w-md px-4 py-12\",\n \"text-balance text-center text-muted-foreground text-sm\",\n \"[&_svg:not([class*='size-'])]:size-6\",\n className\n )}\n {...rest}\n data-slot={slot ?? \"conversation-empty\"}\n />\n );\n};\n\n/**\n * Present only while the pin is released, and pressing it takes the pin back.\n *\n * It floats *over* a transcript that is still growing underneath it, which is why it enters rather\n * than appears and why it paints its own ground: `variant=\"outline\"` sets `--btn-bg: transparent`,\n * so without a ground of its own the message it covers reads straight through it.\n *\n * It asks for that ground by **moving the variable**, not by writing `bg-background` on top. The\n * two look identical at rest and diverge the moment you hover: the wash is\n * `color-mix(…, var(--btn-bg), …)`, so a `bg-background` that only overrode the resting fill left\n * the hover mixing from `transparent` and the transcript showed through the button exactly when the\n * pointer was on it. `--btn-bg` is the seam the recipe documents; this is what it is for.\n */\nexport const ConversationScrollButton = (props: React.ComponentProps<typeof Button>) => {\n const { className, children, onClick, slot, ...rest } = props;\n const ctx = useCtx(\"ConversationScrollButton\");\n\n if (ctx.pinned) return null;\n\n return (\n <Button\n aria-label={children === undefined ? \"Scroll to the latest message\" : undefined}\n className={cn(\n \"absolute inset-x-0 bottom-4 z-10 mx-auto\",\n // `inset-x-0` is what centres it, and it also stretches it, so the width has to be stated.\n // The icon size states its own — `w-(--size)`, square — and a `w-fit` here would win the\n // merge and hand it the icon's width against the control's height.\n children === undefined ? undefined : \"w-fit\",\n \"[--btn-bg:var(--background)] shadow-lg/5\",\n \"fade-in-0 zoom-in-95 slide-in-from-bottom-2 animate-in\",\n \"motion-reduce:animate-none!\",\n className\n )}\n onClick={(event) => {\n onClick?.(event);\n if (!event.defaultPrevented) ctx.scrollToEnd();\n }}\n pill\n // Icon-only by default; give it words and it becomes a pill wide enough to hold them, the way\n // `PromptInputSubmit` does.\n size={children === undefined ? \"icon-sm\" : \"sm\"}\n variant=\"outline\"\n {...rest}\n slot={slot ?? \"conversation-scroll-button\"}\n >\n {children ?? <ArrowDownIcon />}\n </Button>\n );\n};\n"],"names":[],"mappings":";;;;;;AAcA;AAEE;AACA;AACA;AACF;AAgBE;AAOE;AAG8B;AAGhC;AACE;AAEA;AACE;AACA;AACe;AAEjB;AAMA;AACE;AAAqD;AAGvD;AAIE;AACU;AACZ;AAIA;AAAK;AAAJ;AACoE;AAEnE;AAAK;AAAJ;AACiE;AAC5D;AACe;AAElB;AAAA;AAAA;AACH;AAGN;AAUE;AAGA;AACE;AAAK;AAAJ;AACY;AACT;AACA;AACA;AAAA;AAEO;AACJ;AACD;AACe;AAInB;AAAK;AAAJ;AACW;AACD;AAER;AAAA;AAAA;AACH;AAGN;AAQE;AAEA;AACE;AAAK;AAAJ;AACY;AACT;AACA;AACA;AACA;AACA;AAAA;AAEE;AACe;AAAA;AAGzB;AAgBE;AAGA;AAGE;AAAC;AAAA;AACuE;AAC3D;AACT;AAAA;AAAA;AAAA;AAIqC;AACrC;AACA;AACA;AACA;AAAA;AAGA;AACiC;AACnC;AACI;AAGuC;AACnC;AACJ;AACU;AAEc;AAAA;AAGlC;;;;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kanzo-tech/ai",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Kanzo AI surfaces — the transcript, the composer, a model's reasoning and the calls it makes, plus the field affordances that stream into a value. Sibling of @kanzo-tech/ui, not part of it: a Message has a role and one of the roles is `assistant`, which is domain knowledge the generic vocabulary does not carry.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -33,7 +33,7 @@
33
33
  "react": ">=19",
34
34
  "react-dom": ">=19",
35
35
  "streamdown": ">=2",
36
- "@kanzo-tech/ui": "0.10.0"
36
+ "@kanzo-tech/ui": "0.11.0"
37
37
  },
38
38
  "dependencies": {
39
39
  "tailwind-variants": "^3.2.2"
@@ -51,8 +51,8 @@
51
51
  "react-dom": "^19.0.0",
52
52
  "rollup-plugin-preserve-directives": "^0.4.0",
53
53
  "streamdown": "^2.5.0",
54
- "@kanzo-tech/theme": "0.10.0",
55
- "@kanzo-tech/ui": "0.10.0"
54
+ "@kanzo-tech/ui": "0.11.0",
55
+ "@kanzo-tech/theme": "0.11.0"
56
56
  },
57
57
  "//size-limit": "First measurement taken at the split, so this budget is a change detector from day one rather than a number inherited from somewhere. The four AI-assist modules that moved here cost @kanzo-tech/ui's root barrel about 4 kB when they arrived (38 -> 42 kB, recorded in that package's own note); the barrel is rebaselined down by the same move.",
58
58
  "size-limit": [