@krizaka/ui 3.18.2 → 3.19.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/dist/blocks.css +11 -0
- package/dist/card-grid.d.ts +14 -1
- package/dist/card-grid.js +29 -6
- package/dist/card-grid.js.map +1 -1
- package/dist/chat.d.ts +51 -3
- package/dist/chat.js +63 -31
- package/dist/chat.js.map +1 -1
- package/package.json +5 -5
- package/registry/card-grid.json +54 -3
- package/registry/chat.json +101 -7
- package/registry/examples/card-grid/foundation.d.ts +4 -0
- package/registry/examples/card-grid/foundation.js +38 -0
- package/registry/examples/card-grid/foundation.tsx +35 -0
- package/registry/examples/chat/pill-composer.d.ts +4 -0
- package/registry/examples/chat/pill-composer.js +34 -0
- package/registry/examples/chat/pill-composer.tsx +39 -0
- package/registry/index.json +6 -6
- package/registry/map.json +1 -1
- package/registry/nav-bar.json +1 -1
- package/registry/site-footer.json +1 -1
package/dist/blocks.css
CHANGED
|
@@ -177,6 +177,17 @@ a.kz-fact:focus-visible { outline: 2px solid var(--kz-ring, var(--kz-accent)); o
|
|
|
177
177
|
.kz-cards[data-align="start"] { align-items: start; }
|
|
178
178
|
.kz-cards-cell { display: grid; min-width: 0; }
|
|
179
179
|
|
|
180
|
+
/* A foundation (CardGrid `foundation`): one wide card, a hairline stem, and a frame around the cards that stand on it. */
|
|
181
|
+
.kz-foundation { --kz-rail: color-mix(in srgb, var(--kz-hue, var(--kz-accent)) 50%, var(--kz-border-default)); display: grid; }
|
|
182
|
+
.kz-foundation-base { position: relative; display: grid; min-width: 0; }
|
|
183
|
+
.kz-foundation-base::after { content: ""; position: absolute; left: 50%; top: 100%; width: 1px; height: 28px; background: var(--kz-rail); }
|
|
184
|
+
.kz-foundation-group { position: relative; margin-top: 28px; padding: 22px 14px 14px; border: 1px solid var(--kz-rail); border-radius: 24px; }
|
|
185
|
+
.kz-foundation-label { position: absolute; top: -9px; left: 22px; padding: 0 8px; font: 500 11px/18px var(--kz-font-mono, ui-monospace, monospace); letter-spacing: 0.06em; text-transform: uppercase; color: var(--kz-hue-text, var(--kz-accent-text)); background: var(--kz-surface-0); }
|
|
186
|
+
.kz-foundation[data-position="end"] .kz-foundation-base { order: 2; }
|
|
187
|
+
.kz-foundation[data-position="end"] .kz-foundation-group { order: 1; margin-top: 0; margin-bottom: 28px; }
|
|
188
|
+
.kz-foundation[data-position="end"] .kz-foundation-base::after { top: auto; bottom: 100%; }
|
|
189
|
+
@media (max-width: 640px) { .kz-foundation-group { padding: 20px 8px 8px; border-radius: 20px; } }
|
|
190
|
+
|
|
180
191
|
/* The shared surface of the cards: a rail of the hue along the top, the light that follows the pointer (PointerLight
|
|
181
192
|
writes --kz-pointer-x/y), a lift and a lit border under the pointer or the focus. */
|
|
182
193
|
.kz-block-card {
|
package/dist/card-grid.d.ts
CHANGED
|
@@ -35,8 +35,21 @@ type CardGridProps = Omit<React.HTMLAttributes<HTMLElement>, "children"> & {
|
|
|
35
35
|
light?: boolean;
|
|
36
36
|
/** `ul` when the cards are a list (each cell an `li`). */
|
|
37
37
|
as?: "div" | "ul" | "ol";
|
|
38
|
+
/**
|
|
39
|
+
* A foundation: one wide card that the cards of the grid stand on (an engine, a platform, a shared service). It spans
|
|
40
|
+
* the full width beside the grid, and a hairline joins it to a frame around the cards — "everything inside runs on it".
|
|
41
|
+
* The frame takes the hue of `className` (a `data-hue` or a brand class on the grid sets it for the whole block).
|
|
42
|
+
*/
|
|
43
|
+
foundation?: React.ReactNode;
|
|
44
|
+
/**
|
|
45
|
+
* Where the foundation sits: above the cards, or under them.
|
|
46
|
+
* @default "start"
|
|
47
|
+
*/
|
|
48
|
+
foundationPosition?: "start" | "end";
|
|
49
|
+
/** A short mono label on the frame's edge, naming the relation (`Powered by Orazaka`). Only with `foundation`. */
|
|
50
|
+
foundationLabel?: React.ReactNode;
|
|
38
51
|
};
|
|
39
52
|
/** A responsive grid of cards: equal rows, a staggered reveal, the pointer's light. */
|
|
40
|
-
declare function CardGrid({ children, min, fit, align, reveal, columns, light, as: Tag, className, style, ...props }: CardGridProps): React.JSX.Element;
|
|
53
|
+
declare function CardGrid({ children, min, fit, align, reveal, columns, light, as: Tag, foundation, foundationPosition, foundationLabel, className, style, ...props }: CardGridProps): React.JSX.Element;
|
|
41
54
|
|
|
42
55
|
export { CardGrid, type CardGridProps };
|
package/dist/card-grid.js
CHANGED
|
@@ -9,20 +9,35 @@ import {
|
|
|
9
9
|
|
|
10
10
|
// src/card-grid/card-grid.tsx
|
|
11
11
|
import * as React from "react";
|
|
12
|
-
import { jsx } from "react/jsx-runtime";
|
|
12
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
13
13
|
var STEP_MS = 70;
|
|
14
|
-
function CardGrid({
|
|
14
|
+
function CardGrid({
|
|
15
|
+
children,
|
|
16
|
+
min = 300,
|
|
17
|
+
fit = "fill",
|
|
18
|
+
align = "stretch",
|
|
19
|
+
reveal = true,
|
|
20
|
+
columns = 3,
|
|
21
|
+
light = true,
|
|
22
|
+
as: Tag = "div",
|
|
23
|
+
foundation,
|
|
24
|
+
foundationPosition = "start",
|
|
25
|
+
foundationLabel,
|
|
26
|
+
className,
|
|
27
|
+
style,
|
|
28
|
+
...props
|
|
29
|
+
}) {
|
|
15
30
|
const ref = React.useRef(null);
|
|
16
31
|
usePointerLight(light ? ref : NO_REF, POINTER_LIGHT_TARGET);
|
|
17
32
|
const Cell = Tag === "div" ? "div" : "li";
|
|
18
|
-
|
|
33
|
+
const grid = /* @__PURE__ */ jsx(
|
|
19
34
|
Tag,
|
|
20
35
|
{
|
|
21
|
-
ref,
|
|
22
|
-
className: cn("kz-cards", className),
|
|
36
|
+
ref: foundation ? void 0 : ref,
|
|
37
|
+
className: cn("kz-cards", !foundation && className),
|
|
23
38
|
"data-fit": fit === "fit" ? "" : void 0,
|
|
24
39
|
"data-align": align === "start" ? "start" : void 0,
|
|
25
|
-
style: { "--kz-cards-min": `${min}px`, ...style },
|
|
40
|
+
style: { "--kz-cards-min": `${min}px`, ...foundation ? null : style },
|
|
26
41
|
...props,
|
|
27
42
|
children: React.Children.toArray(children).map((child, i) => {
|
|
28
43
|
const step = Math.min(i % Math.max(columns, 1), 5);
|
|
@@ -39,6 +54,14 @@ function CardGrid({ children, min = 300, fit = "fill", align = "stretch", reveal
|
|
|
39
54
|
})
|
|
40
55
|
}
|
|
41
56
|
);
|
|
57
|
+
if (!foundation) return grid;
|
|
58
|
+
return /* @__PURE__ */ jsxs("div", { ref, className: cn("kz-foundation", className), "data-position": foundationPosition, style, children: [
|
|
59
|
+
/* @__PURE__ */ jsx("div", { className: "kz-foundation-base", "data-reveal": reveal ? "" : void 0, children: foundation }),
|
|
60
|
+
/* @__PURE__ */ jsxs("div", { className: "kz-foundation-group", children: [
|
|
61
|
+
foundationLabel && /* @__PURE__ */ jsx("span", { className: "kz-foundation-label", children: foundationLabel }),
|
|
62
|
+
grid
|
|
63
|
+
] })
|
|
64
|
+
] });
|
|
42
65
|
}
|
|
43
66
|
var NO_REF = { current: null };
|
|
44
67
|
export {
|
package/dist/card-grid.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/card-grid/card-grid.tsx"],"sourcesContent":["// Client (the pointer's light). A responsive grid of cards (blocks.css): one column on a phone, the cells of a row\n// share their height, each card rises once as it enters the viewport — staggered by column — and the pointer carries a\n// light over the card it is on.\nimport * as React from \"react\";\n\nimport { cn } from \"../cn\";\nimport { POINTER_LIGHT_TARGET, usePointerLight } from \"../pointer-light/pointer-light\";\n\nconst STEP_MS = 70;\n\nexport type CardGridProps = Omit<React.HTMLAttributes<HTMLElement>, \"children\"> & {\n /** The cards: one cell each. */\n children: React.ReactNode;\n /**\n * The narrowest a column may be, in pixels: the grid fits as many columns as the width allows.\n * @default 300\n */\n min?: number;\n /**\n * `fill` keeps empty tracks (a lone card keeps its width); `fit` stretches the cards of a short row across the width.\n * @default \"fill\"\n */\n fit?: \"fill\" | \"fit\";\n /**\n * `stretch` gives the cells of a row one height; `start` lets each card keep its own (cards that open a panel).\n * @default \"stretch\"\n */\n align?: \"stretch\" | \"start\";\n /**\n * The cards rise once into place (`[data-reveal]`, MotionObserver), staggered by column. `false` on the first screen.\n * @default true\n */\n reveal?: boolean;\n /**\n * The columns the stagger counts (the delay restarts on every row).\n * @default 3\n */\n columns?: number;\n /**\n * The light that follows the pointer over the cards (`[data-kz-light]`).\n * @default true\n */\n light?: boolean;\n /** `ul` when the cards are a list (each cell an `li`). */\n as?: \"div\" | \"ul\" | \"ol\";\n};\n\n/** A responsive grid of cards: equal rows, a staggered reveal, the pointer's light. */\nexport function CardGrid({
|
|
1
|
+
{"version":3,"sources":["../src/card-grid/card-grid.tsx"],"sourcesContent":["// Client (the pointer's light). A responsive grid of cards (blocks.css): one column on a phone, the cells of a row\n// share their height, each card rises once as it enters the viewport — staggered by column — and the pointer carries a\n// light over the card it is on.\nimport * as React from \"react\";\n\nimport { cn } from \"../cn\";\nimport { POINTER_LIGHT_TARGET, usePointerLight } from \"../pointer-light/pointer-light\";\n\nconst STEP_MS = 70;\n\nexport type CardGridProps = Omit<React.HTMLAttributes<HTMLElement>, \"children\"> & {\n /** The cards: one cell each. */\n children: React.ReactNode;\n /**\n * The narrowest a column may be, in pixels: the grid fits as many columns as the width allows.\n * @default 300\n */\n min?: number;\n /**\n * `fill` keeps empty tracks (a lone card keeps its width); `fit` stretches the cards of a short row across the width.\n * @default \"fill\"\n */\n fit?: \"fill\" | \"fit\";\n /**\n * `stretch` gives the cells of a row one height; `start` lets each card keep its own (cards that open a panel).\n * @default \"stretch\"\n */\n align?: \"stretch\" | \"start\";\n /**\n * The cards rise once into place (`[data-reveal]`, MotionObserver), staggered by column. `false` on the first screen.\n * @default true\n */\n reveal?: boolean;\n /**\n * The columns the stagger counts (the delay restarts on every row).\n * @default 3\n */\n columns?: number;\n /**\n * The light that follows the pointer over the cards (`[data-kz-light]`).\n * @default true\n */\n light?: boolean;\n /** `ul` when the cards are a list (each cell an `li`). */\n as?: \"div\" | \"ul\" | \"ol\";\n /**\n * A foundation: one wide card that the cards of the grid stand on (an engine, a platform, a shared service). It spans\n * the full width beside the grid, and a hairline joins it to a frame around the cards — \"everything inside runs on it\".\n * The frame takes the hue of `className` (a `data-hue` or a brand class on the grid sets it for the whole block).\n */\n foundation?: React.ReactNode;\n /**\n * Where the foundation sits: above the cards, or under them.\n * @default \"start\"\n */\n foundationPosition?: \"start\" | \"end\";\n /** A short mono label on the frame's edge, naming the relation (`Powered by Orazaka`). Only with `foundation`. */\n foundationLabel?: React.ReactNode;\n};\n\n/** A responsive grid of cards: equal rows, a staggered reveal, the pointer's light. */\nexport function CardGrid({\n children,\n min = 300,\n fit = \"fill\",\n align = \"stretch\",\n reveal = true,\n columns = 3,\n light = true,\n as: Tag = \"div\",\n foundation,\n foundationPosition = \"start\",\n foundationLabel,\n className,\n style,\n ...props\n}: CardGridProps) {\n const ref = React.useRef<HTMLElement>(null);\n usePointerLight(light ? ref : NO_REF, POINTER_LIGHT_TARGET);\n const Cell = Tag === \"div\" ? \"div\" : \"li\";\n const grid = (\n <Tag\n ref={foundation ? undefined : (ref as React.Ref<never>)}\n className={cn(\"kz-cards\", !foundation && className)}\n data-fit={fit === \"fit\" ? \"\" : undefined}\n data-align={align === \"start\" ? \"start\" : undefined}\n style={{ \"--kz-cards-min\": `${min}px`, ...(foundation ? null : style) } as React.CSSProperties}\n {...props}\n >\n {React.Children.toArray(children).map((child, i) => {\n const step = Math.min(i % Math.max(columns, 1), 5);\n return (\n <Cell\n key={React.isValidElement(child) && child.key != null ? child.key : i}\n className=\"kz-cards-cell\"\n data-reveal={reveal ? \"\" : undefined}\n style={reveal && step ? ({ \"--kz-delay\": `${step * STEP_MS}ms` } as React.CSSProperties) : undefined}\n >\n {child}\n </Cell>\n );\n })}\n </Tag>\n );\n if (!foundation) return grid;\n return (\n <div ref={ref as React.Ref<HTMLDivElement>} className={cn(\"kz-foundation\", className)} data-position={foundationPosition} style={style}>\n <div className=\"kz-foundation-base\" data-reveal={reveal ? \"\" : undefined}>\n {foundation}\n </div>\n <div className=\"kz-foundation-group\">\n {foundationLabel && <span className=\"kz-foundation-label\">{foundationLabel}</span>}\n {grid}\n </div>\n </div>\n );\n}\n\nconst NO_REF: React.RefObject<HTMLElement | null> = { current: null };\n"],"mappings":";;;;;;;;;;AAGA,YAAY,WAAW;AAyFb,cAkBJ,YAlBI;AApFV,IAAM,UAAU;AAqDT,SAAS,SAAS;AAAA,EACvB;AAAA,EACA,MAAM;AAAA,EACN,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,IAAI,MAAM;AAAA,EACV;AAAA,EACA,qBAAqB;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAkB;AAChB,QAAM,MAAY,aAAoB,IAAI;AAC1C,kBAAgB,QAAQ,MAAM,QAAQ,oBAAoB;AAC1D,QAAM,OAAO,QAAQ,QAAQ,QAAQ;AACrC,QAAM,OACJ;AAAA,IAAC;AAAA;AAAA,MACC,KAAK,aAAa,SAAa;AAAA,MAC/B,WAAW,GAAG,YAAY,CAAC,cAAc,SAAS;AAAA,MAClD,YAAU,QAAQ,QAAQ,KAAK;AAAA,MAC/B,cAAY,UAAU,UAAU,UAAU;AAAA,MAC1C,OAAO,EAAE,kBAAkB,GAAG,GAAG,MAAM,GAAI,aAAa,OAAO,MAAO;AAAA,MACrE,GAAG;AAAA,MAEH,UAAM,eAAS,QAAQ,QAAQ,EAAE,IAAI,CAAC,OAAO,MAAM;AAClD,cAAM,OAAO,KAAK,IAAI,IAAI,KAAK,IAAI,SAAS,CAAC,GAAG,CAAC;AACjD,eACE;AAAA,UAAC;AAAA;AAAA,YAEC,WAAU;AAAA,YACV,eAAa,SAAS,KAAK;AAAA,YAC3B,OAAO,UAAU,OAAQ,EAAE,cAAc,GAAG,OAAO,OAAO,KAAK,IAA4B;AAAA,YAE1F;AAAA;AAAA,UALU,qBAAe,KAAK,KAAK,MAAM,OAAO,OAAO,MAAM,MAAM;AAAA,QAMtE;AAAA,MAEJ,CAAC;AAAA;AAAA,EACH;AAEF,MAAI,CAAC,WAAY,QAAO;AACxB,SACE,qBAAC,SAAI,KAAuC,WAAW,GAAG,iBAAiB,SAAS,GAAG,iBAAe,oBAAoB,OACxH;AAAA,wBAAC,SAAI,WAAU,sBAAqB,eAAa,SAAS,KAAK,QAC5D,sBACH;AAAA,IACA,qBAAC,SAAI,WAAU,uBACZ;AAAA,yBAAmB,oBAAC,UAAK,WAAU,uBAAuB,2BAAgB;AAAA,MAC1E;AAAA,OACH;AAAA,KACF;AAEJ;AAEA,IAAM,SAA8C,EAAE,SAAS,KAAK;","names":[]}
|
package/dist/chat.d.ts
CHANGED
|
@@ -14,8 +14,14 @@ declare const chat: tailwind_variants.TVReturnType<{
|
|
|
14
14
|
typing?: tailwind_variants.ClassValue;
|
|
15
15
|
dot?: tailwind_variants.ClassValue;
|
|
16
16
|
composer?: tailwind_variants.ClassValue;
|
|
17
|
+
pillRoot?: tailwind_variants.ClassValue;
|
|
18
|
+
pillLeading?: tailwind_variants.ClassValue;
|
|
19
|
+
pillStatus?: tailwind_variants.ClassValue;
|
|
17
20
|
item?: tailwind_variants.ClassValue;
|
|
18
21
|
count?: tailwind_variants.ClassValue;
|
|
22
|
+
pill?: tailwind_variants.ClassValue;
|
|
23
|
+
pillInput?: tailwind_variants.ClassValue;
|
|
24
|
+
pillSend?: tailwind_variants.ClassValue;
|
|
19
25
|
};
|
|
20
26
|
};
|
|
21
27
|
} | {
|
|
@@ -31,8 +37,14 @@ declare const chat: tailwind_variants.TVReturnType<{
|
|
|
31
37
|
typing?: tailwind_variants.ClassValue;
|
|
32
38
|
dot?: tailwind_variants.ClassValue;
|
|
33
39
|
composer?: tailwind_variants.ClassValue;
|
|
40
|
+
pillRoot?: tailwind_variants.ClassValue;
|
|
41
|
+
pillLeading?: tailwind_variants.ClassValue;
|
|
42
|
+
pillStatus?: tailwind_variants.ClassValue;
|
|
34
43
|
item?: tailwind_variants.ClassValue;
|
|
35
44
|
count?: tailwind_variants.ClassValue;
|
|
45
|
+
pill?: tailwind_variants.ClassValue;
|
|
46
|
+
pillInput?: tailwind_variants.ClassValue;
|
|
47
|
+
pillSend?: tailwind_variants.ClassValue;
|
|
36
48
|
};
|
|
37
49
|
};
|
|
38
50
|
} | {}, {
|
|
@@ -46,6 +58,12 @@ declare const chat: tailwind_variants.TVReturnType<{
|
|
|
46
58
|
composer: string;
|
|
47
59
|
input: string;
|
|
48
60
|
send: string;
|
|
61
|
+
pillRoot: string;
|
|
62
|
+
pill: string;
|
|
63
|
+
pillLeading: string;
|
|
64
|
+
pillInput: string;
|
|
65
|
+
pillSend: string;
|
|
66
|
+
pillStatus: string;
|
|
49
67
|
item: string;
|
|
50
68
|
count: string;
|
|
51
69
|
}, undefined, {
|
|
@@ -61,8 +79,14 @@ declare const chat: tailwind_variants.TVReturnType<{
|
|
|
61
79
|
typing?: tailwind_variants.ClassValue;
|
|
62
80
|
dot?: tailwind_variants.ClassValue;
|
|
63
81
|
composer?: tailwind_variants.ClassValue;
|
|
82
|
+
pillRoot?: tailwind_variants.ClassValue;
|
|
83
|
+
pillLeading?: tailwind_variants.ClassValue;
|
|
84
|
+
pillStatus?: tailwind_variants.ClassValue;
|
|
64
85
|
item?: tailwind_variants.ClassValue;
|
|
65
86
|
count?: tailwind_variants.ClassValue;
|
|
87
|
+
pill?: tailwind_variants.ClassValue;
|
|
88
|
+
pillInput?: tailwind_variants.ClassValue;
|
|
89
|
+
pillSend?: tailwind_variants.ClassValue;
|
|
66
90
|
};
|
|
67
91
|
};
|
|
68
92
|
} | {}, {
|
|
@@ -76,6 +100,12 @@ declare const chat: tailwind_variants.TVReturnType<{
|
|
|
76
100
|
composer: string;
|
|
77
101
|
input: string;
|
|
78
102
|
send: string;
|
|
103
|
+
pillRoot: string;
|
|
104
|
+
pill: string;
|
|
105
|
+
pillLeading: string;
|
|
106
|
+
pillInput: string;
|
|
107
|
+
pillSend: string;
|
|
108
|
+
pillStatus: string;
|
|
79
109
|
item: string;
|
|
80
110
|
count: string;
|
|
81
111
|
}, tailwind_variants.TVReturnTypeLike<unknown, {
|
|
@@ -89,6 +119,12 @@ declare const chat: tailwind_variants.TVReturnType<{
|
|
|
89
119
|
composer: string;
|
|
90
120
|
input: string;
|
|
91
121
|
send: string;
|
|
122
|
+
pillRoot: string;
|
|
123
|
+
pill: string;
|
|
124
|
+
pillLeading: string;
|
|
125
|
+
pillInput: string;
|
|
126
|
+
pillSend: string;
|
|
127
|
+
pillStatus: string;
|
|
92
128
|
item: string;
|
|
93
129
|
count: string;
|
|
94
130
|
}>>;
|
|
@@ -144,17 +180,29 @@ type MessageComposerProps = {
|
|
|
144
180
|
placeholder: string;
|
|
145
181
|
/** The send button's name (“Send”). */
|
|
146
182
|
sendLabel: string;
|
|
147
|
-
/** Before the input: an attach button. */
|
|
183
|
+
/** Before the input: an attach button (`bar`), or the mode/model chip and attach, inside the field (`pill`). */
|
|
148
184
|
leading?: React.ReactNode;
|
|
149
185
|
/** Cannot write: the reason in its place. */
|
|
150
186
|
disabledReason?: string;
|
|
151
187
|
/** The longest message. Default 4000. */
|
|
152
188
|
maxLength?: number;
|
|
189
|
+
/** `bar`: a docked bar with a square-ish field. `pill`: one rounded field holding `leading`, the text and a round send button — no inner border. Default `bar`. */
|
|
190
|
+
variant?: "bar" | "pill";
|
|
191
|
+
/** `pill`: the quiet line under the field (privacy, cost estimate). Its row is always reserved, so a changing line never moves the field. Read politely. */
|
|
192
|
+
status?: React.ReactNode;
|
|
193
|
+
/** An answer is on its way: the send is held (and `aria-busy` is set) while the field stays where it is, size included. */
|
|
194
|
+
busy?: boolean;
|
|
195
|
+
/** Overrides “the draft has text” — e.g. an attachment alone can be sent. A busy composer never sends. */
|
|
196
|
+
canSend?: boolean;
|
|
197
|
+
/** Attributes (`data-testid`, `autoFocus`, `id`…) forwarded to the textarea. */
|
|
198
|
+
inputProps?: Omit<React.ComponentPropsWithoutRef<"textarea">, "value" | "onChange" | "placeholder" | "maxLength" | "className" | "rows">;
|
|
199
|
+
/** Attributes forwarded to the send button. */
|
|
200
|
+
sendProps?: Omit<React.ComponentPropsWithoutRef<"button">, "type" | "disabled" | "className" | "aria-label">;
|
|
153
201
|
/** Classes merged on the root. */
|
|
154
202
|
className?: string;
|
|
155
203
|
};
|
|
156
|
-
/** Where a message is written: a growing textarea (Enter sends, Shift+Enter a new line), an attach slot, a send button. */
|
|
157
|
-
declare function MessageComposer({ value, onValueChange, onSend, placeholder, sendLabel, leading, disabledReason, maxLength, className }: MessageComposerProps): React.JSX.Element;
|
|
204
|
+
/** Where a message is written: a growing textarea (Enter sends, Shift+Enter a new line), an attach slot, a send button. The `pill` variant is the one-field look of the Krizaka chat card. */
|
|
205
|
+
declare function MessageComposer({ value, onValueChange, onSend, placeholder, sendLabel, leading, disabledReason, maxLength, variant, status, busy, canSend, inputProps, sendProps, className, }: MessageComposerProps): React.JSX.Element;
|
|
158
206
|
type ConversationItemProps = {
|
|
159
207
|
/** The other person's name. */
|
|
160
208
|
name: string;
|
package/dist/chat.js
CHANGED
|
@@ -19,6 +19,12 @@ var chat = tv({
|
|
|
19
19
|
composer: "flex items-end gap-2 border-t border-border-subtle bg-surface-0 px-3 py-2",
|
|
20
20
|
input: "max-h-32 min-h-10 flex-1 resize-none rounded-2xl border border-border-default bg-surface-1 px-3.5 py-2.5 text-sm text-fg placeholder:text-fg-muted focus-visible:border-accent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring",
|
|
21
21
|
send: "inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-accent text-on-accent hover:bg-accent-hover focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring disabled:bg-surface-3 disabled:text-fg-muted [&>svg]:h-[18px] [&>svg]:w-[18px]",
|
|
22
|
+
pillRoot: "flex w-full flex-col gap-2",
|
|
23
|
+
pill: "flex items-end gap-1.5 rounded-[1.75rem] border border-border-default bg-surface-1 py-1.5 pl-2 pr-1.5 transition-colors focus-within:border-accent focus-within:ring-2 focus-within:ring-ring",
|
|
24
|
+
pillLeading: "flex min-h-9 shrink-0 items-center gap-1 self-end",
|
|
25
|
+
pillInput: "max-h-32 min-h-9 min-w-0 flex-1 resize-none border-0 bg-transparent px-1.5 py-[7px] text-sm leading-snug text-fg placeholder:text-fg-muted focus:outline-hidden focus-visible:outline-hidden",
|
|
26
|
+
pillSend: "inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-accent text-on-accent shadow-sm transition-colors hover:bg-accent-hover focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-surface-1 disabled:cursor-not-allowed disabled:bg-surface-3 disabled:text-fg-muted disabled:shadow-none [&>svg]:h-4 [&>svg]:w-4",
|
|
27
|
+
pillStatus: "m-0 flex min-h-5 items-center gap-1.5 px-3 text-xs text-fg-secondary [&_svg]:h-3.5 [&_svg]:w-3.5 [&_svg]:shrink-0",
|
|
22
28
|
item: "flex w-full items-center gap-3 px-4 py-2.5 text-left hover:bg-surface-2 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring",
|
|
23
29
|
count: "inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-accent px-1.5 text-xs font-extrabold text-on-accent"
|
|
24
30
|
}
|
|
@@ -60,42 +66,68 @@ function TypingIndicator({ label, showLabel, className }) {
|
|
|
60
66
|
/* @__PURE__ */ jsx("span", { className: showLabel ? "text-xs text-fg-secondary" : "sr-only", children: label })
|
|
61
67
|
] });
|
|
62
68
|
}
|
|
63
|
-
function MessageComposer({
|
|
69
|
+
function MessageComposer({
|
|
70
|
+
value,
|
|
71
|
+
onValueChange,
|
|
72
|
+
onSend,
|
|
73
|
+
placeholder,
|
|
74
|
+
sendLabel,
|
|
75
|
+
leading,
|
|
76
|
+
disabledReason,
|
|
77
|
+
maxLength = 4e3,
|
|
78
|
+
variant = "bar",
|
|
79
|
+
status,
|
|
80
|
+
busy = false,
|
|
81
|
+
canSend,
|
|
82
|
+
inputProps,
|
|
83
|
+
sendProps,
|
|
84
|
+
className
|
|
85
|
+
}) {
|
|
64
86
|
const s = chat();
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
87
|
+
const pill = variant === "pill";
|
|
88
|
+
const ready = !busy && (canSend ?? value.trim().length > 0);
|
|
89
|
+
const textarea = /* @__PURE__ */ jsx(
|
|
90
|
+
"textarea",
|
|
68
91
|
{
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
92
|
+
"aria-label": placeholder,
|
|
93
|
+
placeholder,
|
|
94
|
+
rows: 1,
|
|
95
|
+
maxLength,
|
|
96
|
+
...inputProps,
|
|
97
|
+
value,
|
|
98
|
+
onChange: (e) => onValueChange(e.target.value),
|
|
99
|
+
onKeyDown: (e) => {
|
|
100
|
+
inputProps?.onKeyDown?.(e);
|
|
101
|
+
if (e.defaultPrevented) return;
|
|
102
|
+
if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
|
|
103
|
+
e.preventDefault();
|
|
104
|
+
if (ready) onSend();
|
|
105
|
+
}
|
|
73
106
|
},
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
/* @__PURE__ */ jsx(
|
|
77
|
-
"textarea",
|
|
78
|
-
{
|
|
79
|
-
"aria-label": placeholder,
|
|
80
|
-
placeholder,
|
|
81
|
-
rows: 1,
|
|
82
|
-
maxLength,
|
|
83
|
-
value,
|
|
84
|
-
onChange: (e) => onValueChange(e.target.value),
|
|
85
|
-
onKeyDown: (e) => {
|
|
86
|
-
if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
|
|
87
|
-
e.preventDefault();
|
|
88
|
-
if (ready) onSend();
|
|
89
|
-
}
|
|
90
|
-
},
|
|
91
|
-
className: s.input(),
|
|
92
|
-
style: { fieldSizing: "content" }
|
|
93
|
-
}
|
|
94
|
-
),
|
|
95
|
-
/* @__PURE__ */ jsx("button", { type: "submit", "aria-label": sendLabel, disabled: !ready, className: s.send(), children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": true, children: /* @__PURE__ */ jsx("path", { d: "M4 12l16-8-6 16-2.5-6.5L4 12z" }) }) })
|
|
96
|
-
] })
|
|
107
|
+
className: pill ? s.pillInput() : s.input(),
|
|
108
|
+
style: { fieldSizing: "content" }
|
|
97
109
|
}
|
|
98
110
|
);
|
|
111
|
+
const send = /* @__PURE__ */ jsx("button", { type: "submit", "aria-label": sendLabel, ...sendProps, disabled: !ready, className: pill ? s.pillSend() : s.send(), children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": true, children: /* @__PURE__ */ jsx("path", { d: "M4 12l16-8-6 16-2.5-6.5L4 12z" }) }) });
|
|
112
|
+
const submit = (e) => {
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
if (ready) onSend();
|
|
115
|
+
};
|
|
116
|
+
if (pill) {
|
|
117
|
+
return /* @__PURE__ */ jsxs("form", { className: s.pillRoot({ className }), onSubmit: submit, "aria-busy": busy || void 0, children: [
|
|
118
|
+
disabledReason ? /* @__PURE__ */ jsx("p", { className: "m-0 rounded-[1.75rem] border border-border-default bg-surface-1 px-4 py-2.5 text-sm text-fg-secondary", children: disabledReason }) : /* @__PURE__ */ jsxs("div", { className: s.pill(), children: [
|
|
119
|
+
leading ? /* @__PURE__ */ jsx("div", { className: s.pillLeading(), children: leading }) : null,
|
|
120
|
+
textarea,
|
|
121
|
+
send
|
|
122
|
+
] }),
|
|
123
|
+
/* @__PURE__ */ jsx("p", { role: "status", "aria-live": "polite", className: s.pillStatus(), children: status })
|
|
124
|
+
] });
|
|
125
|
+
}
|
|
126
|
+
return /* @__PURE__ */ jsx("form", { className: s.composer({ className }), onSubmit: submit, "aria-busy": busy || void 0, children: disabledReason ? /* @__PURE__ */ jsx("p", { className: "m-0 flex-1 py-2 text-sm text-fg-secondary", children: disabledReason }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
127
|
+
leading,
|
|
128
|
+
textarea,
|
|
129
|
+
send
|
|
130
|
+
] }) });
|
|
99
131
|
}
|
|
100
132
|
function ConversationItem({ name, avatar, preview, typing, time, unread = 0, unreadLabel, online, onlineLabel, href, onSelect, className }) {
|
|
101
133
|
const s = chat();
|
package/dist/chat.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/chat/chat.tsx"],"sourcesContent":["// Client (the composer's draft). A conversation's parts — the bubble with its time and receipt, the typing dots, the\n// composer, a conversation in a list — the same API as React Native. Words are props.\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { Avatar } from \"../avatar\";\n\nexport const chat = tv({\n slots: {\n row: \"flex flex-col gap-0.5\",\n bubble: \"max-w-[80%] rounded-2xl px-3 py-2 text-sm leading-snug\",\n context: \"mb-0.5 block text-xs opacity-85\",\n time: \"mt-0.5 block text-right text-[11px] tabular-nums\",\n status: \"flex items-center gap-1 text-xs text-fg-secondary data-[status=failed]:text-fg-danger [&>svg]:h-3.5 [&>svg]:w-3.5\",\n typing: \"inline-flex items-center gap-1 self-start rounded-2xl bg-surface-2 px-3.5 py-3\",\n dot: \"h-[7px] w-[7px] rounded-full bg-fg-secondary motion-safe:animate-[kz-typing_1.2s_ease-in-out_infinite]\",\n composer: \"flex items-end gap-2 border-t border-border-subtle bg-surface-0 px-3 py-2\",\n input:\n \"max-h-32 min-h-10 flex-1 resize-none rounded-2xl border border-border-default bg-surface-1 px-3.5 py-2.5 text-sm text-fg placeholder:text-fg-muted \" +\n \"focus-visible:border-accent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n send:\n \"inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-accent text-on-accent hover:bg-accent-hover focus-visible:outline-hidden \" +\n \"focus-visible:ring-2 focus-visible:ring-ring disabled:bg-surface-3 disabled:text-fg-muted [&>svg]:h-[18px] [&>svg]:w-[18px]\",\n item: \"flex w-full items-center gap-3 px-4 py-2.5 text-left hover:bg-surface-2 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring\",\n count: \"inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-accent px-1.5 text-xs font-extrabold text-on-accent\",\n },\n});\n\nexport type MessageStatus = \"sending\" | \"sent\" | \"delivered\" | \"read\" | \"failed\";\n\nexport type MessageBubbleLabels = {\n /** The words of each status, shown under the last message of yours. */\n status?: Partial<Record<MessageStatus, string>>;\n /** A failed message's retry button (“Retry”). */\n retry?: string;\n};\n\nconst STATUS: Record<MessageStatus, string> = { sending: \"Sending\", sent: \"Sent\", delivered: \"Delivered\", read: \"Read\", failed: \"Not sent\" };\n\nexport type MessageBubbleProps = {\n /** The text. */\n text: string;\n /** Yours (right, in the accent) or theirs (left). */\n mine?: boolean;\n /** The time, written by the app (“14:02”). */\n time?: string;\n /** Where a message of yours stands; shown when `showStatus`. */\n status?: MessageStatus;\n /** Show the status under the bubble (the last message of yours). */\n showStatus?: boolean;\n /** A line above the text: what it answers. */\n context?: string;\n /** Sends a failed message again (a button under it). */\n onRetry?: () => void;\n /** Part of a run of the same sender: the corner towards the next one is tighter. */\n grouped?: \"first\" | \"middle\" | \"last\" | \"single\";\n /** The words, passed translated. */\n labels?: MessageBubbleLabels;\n /** Classes merged on the row. */\n className?: string;\n};\n\nconst CORNERS = {\n mine: { first: \"rounded-br-md\", middle: \"rounded-r-md\", last: \"rounded-tr-md\", single: \"\" },\n theirs: { first: \"rounded-bl-md\", middle: \"rounded-l-md\", last: \"rounded-tl-md\", single: \"\" },\n} as const;\n\n/** A message: yours in the accent on the right, theirs on the left, the time inside, the receipt under the last of yours. */\nexport function MessageBubble({ text, mine, time, status, showStatus, context, onRetry, grouped = \"single\", labels, className }: MessageBubbleProps) {\n const s = chat();\n const words = { ...STATUS, ...labels?.status };\n return (\n <div className={s.row({ className: [mine ? \"items-end\" : \"items-start\", className].filter(Boolean).join(\" \") })}>\n <p\n className={s.bubble({\n className: [mine ? \"bg-accent text-on-accent\" : \"bg-surface-2 text-fg\", CORNERS[mine ? \"mine\" : \"theirs\"][grouped], status === \"failed\" ? \"ring-2 ring-danger\" : \"\", status === \"sending\" ? \"opacity-70\" : \"\"].join(\" \"),\n })}\n >\n {context ? <span className={s.context()}>{context}</span> : null}\n {text}\n {time ? <span className={s.time()}>{time}</span> : null}\n </p>\n {showStatus && status ? (\n <p className={s.status()} data-status={status}>\n {status === \"sent\" || status === \"delivered\" || status === \"read\" ? (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" strokeWidth=\"2.2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden className={status === \"read\" ? \"stroke-accent\" : \"stroke-fg-secondary\"}>\n <path d={status === \"sent\" ? \"M5 12.5l4.5 4.5L19 7.5\" : \"M2 12.5l4.5 4.5L16 7.5M9 15l2 2 9.5-9.5\"} />\n </svg>\n ) : null}\n {words[status]}\n {status === \"failed\" && onRetry ? (\n <button type=\"button\" onClick={onRetry} className=\"ml-1 font-semibold text-fg underline underline-offset-2 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\">\n {labels?.retry ?? \"Retry\"}\n </button>\n ) : null}\n </p>\n ) : null}\n </div>\n );\n}\n\nexport type TypingIndicatorProps = {\n /** What is read and shown beside the dots (“Elena is typing”) — passed translated. */\n label: string;\n /** Show the words beside the dots. */\n showLabel?: boolean;\n /** Classes merged on the root. */\n className?: string;\n};\n\n/** Three dots that breathe while the other person types, announced politely; still under reduced motion. */\nexport function TypingIndicator({ label, showLabel, className }: TypingIndicatorProps) {\n const s = chat();\n return (\n <div role=\"status\" className={[\"flex items-center gap-2\", className].filter(Boolean).join(\" \")}>\n <style href=\"krizaka-typing\" precedence=\"default\">{\"@keyframes kz-typing{0%,60%,100%{opacity:.35;transform:none}30%{opacity:1;transform:translateY(-2px)}}\"}</style>\n <span className={s.typing()} aria-hidden>\n {[0, 1, 2].map((i) => (\n <span key={i} className={s.dot()} style={{ animationDelay: `${i * 0.18}s` }} />\n ))}\n </span>\n <span className={showLabel ? \"text-xs text-fg-secondary\" : \"sr-only\"}>{label}</span>\n </div>\n );\n}\n\nexport type MessageComposerProps = {\n /** The draft (controlled). */\n value: string;\n /** Called on every key. */\n onValueChange: (text: string) => void;\n /** Sends the draft (Enter, or the button); Shift+Enter adds a line. */\n onSend: () => void;\n /** The input's name and placeholder (“Message”). */\n placeholder: string;\n /** The send button's name (“Send”). */\n sendLabel: string;\n /** Before the input: an attach button. */\n leading?: React.ReactNode;\n /** Cannot write: the reason in its place. */\n disabledReason?: string;\n /** The longest message. Default 4000. */\n maxLength?: number;\n /** Classes merged on the root. */\n className?: string;\n};\n\n/** Where a message is written: a growing textarea (Enter sends, Shift+Enter a new line), an attach slot, a send button. */\nexport function MessageComposer({ value, onValueChange, onSend, placeholder, sendLabel, leading, disabledReason, maxLength = 4000, className }: MessageComposerProps) {\n const s = chat();\n const ready = value.trim().length > 0;\n return (\n <form\n className={s.composer({ className })}\n onSubmit={(e) => {\n e.preventDefault();\n if (ready) onSend();\n }}\n >\n {disabledReason ? (\n <p className=\"m-0 flex-1 py-2 text-sm text-fg-secondary\">{disabledReason}</p>\n ) : (\n <>\n {leading}\n <textarea\n aria-label={placeholder}\n placeholder={placeholder}\n rows={1}\n maxLength={maxLength}\n value={value}\n onChange={(e) => onValueChange(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" && !e.shiftKey && !e.nativeEvent.isComposing) {\n e.preventDefault();\n if (ready) onSend();\n }\n }}\n className={s.input()}\n style={{ fieldSizing: \"content\" } as React.CSSProperties}\n />\n <button type=\"submit\" aria-label={sendLabel} disabled={!ready} className={s.send()}>\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden>\n <path d=\"M4 12l16-8-6 16-2.5-6.5L4 12z\" />\n </svg>\n </button>\n </>\n )}\n </form>\n );\n}\n\nexport type ConversationItemProps = {\n /** The other person's name. */\n name: string;\n /** Their avatar's URL. */\n avatar?: string | null;\n /** The last message, or “typing…”. */\n preview: string;\n /** The preview is the typing signal: in the accent. */\n typing?: boolean;\n /** When (“14:02”, “Mon”). */\n time?: string;\n /** Unread messages: a count, the name in bold. */\n unread?: number;\n /** The unread count in words (“3 unread”). */\n unreadLabel?: string;\n /** Before the name: a presence dot (online). */\n online?: boolean;\n /** The presence in words (“online”). */\n onlineLabel?: string;\n /** Opens the conversation (a link). */\n href?: string;\n /** Opens the conversation (a button). */\n onSelect?: () => void;\n /** Classes merged on the row. */\n className?: string;\n};\n\n/** A conversation in the list: avatar, name, the last message or typing, the time, the unread count. */\nexport function ConversationItem({ name, avatar, preview, typing, time, unread = 0, unreadLabel, online, onlineLabel, href, onSelect, className }: ConversationItemProps) {\n const s = chat();\n const body = (\n <>\n <span className=\"relative shrink-0\">\n <Avatar src={avatar ?? null} alt=\"\" fallback={name.charAt(0)} size=\"lg\" />\n {online ? <span aria-hidden className=\"absolute bottom-0 right-0 h-3 w-3 rounded-full bg-success ring-2 ring-surface-0\" /> : null}\n </span>\n <span className=\"flex min-w-0 flex-1 flex-col\">\n <span className=\"flex items-center gap-2\">\n <span className={[\"min-w-0 flex-1 truncate text-sm text-fg\", unread > 0 ? \"font-extrabold\" : \"font-semibold\"].join(\" \")}>\n {name}\n {online && onlineLabel ? <span className=\"sr-only\">{`, ${onlineLabel}`}</span> : null}\n </span>\n {time ? <span className={[\"shrink-0 text-xs tabular-nums\", unread > 0 ? \"text-fg-accent\" : \"text-fg-secondary\"].join(\" \")}>{time}</span> : null}\n </span>\n <span className=\"flex items-center gap-2\">\n <span className={[\"min-w-0 flex-1 truncate text-xs\", typing ? \"text-fg-accent\" : unread > 0 ? \"text-fg\" : \"text-fg-secondary\"].join(\" \")}>{preview}</span>\n {unread > 0 ? (\n <span className={s.count()}>\n <span aria-hidden>{unread > 99 ? \"99+\" : unread}</span>\n <span className=\"sr-only\">{unreadLabel ?? String(unread)}</span>\n </span>\n ) : null}\n </span>\n </span>\n </>\n );\n return href ? (\n <a href={href} className={s.item({ className })}>\n {body}\n </a>\n ) : (\n <button type=\"button\" onClick={onSelect} className={s.item({ className })}>\n {body}\n </button>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/chat/chat.tsx"],"sourcesContent":["// Client (the composer's draft). A conversation's parts — the bubble with its time and receipt, the typing dots, the\n// composer, a conversation in a list — the same API as React Native. Words are props.\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { Avatar } from \"../avatar\";\n\nexport const chat = tv({\n slots: {\n row: \"flex flex-col gap-0.5\",\n bubble: \"max-w-[80%] rounded-2xl px-3 py-2 text-sm leading-snug\",\n context: \"mb-0.5 block text-xs opacity-85\",\n time: \"mt-0.5 block text-right text-[11px] tabular-nums\",\n status: \"flex items-center gap-1 text-xs text-fg-secondary data-[status=failed]:text-fg-danger [&>svg]:h-3.5 [&>svg]:w-3.5\",\n typing: \"inline-flex items-center gap-1 self-start rounded-2xl bg-surface-2 px-3.5 py-3\",\n dot: \"h-[7px] w-[7px] rounded-full bg-fg-secondary motion-safe:animate-[kz-typing_1.2s_ease-in-out_infinite]\",\n composer: \"flex items-end gap-2 border-t border-border-subtle bg-surface-0 px-3 py-2\",\n input:\n \"max-h-32 min-h-10 flex-1 resize-none rounded-2xl border border-border-default bg-surface-1 px-3.5 py-2.5 text-sm text-fg placeholder:text-fg-muted \" +\n \"focus-visible:border-accent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n send:\n \"inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-accent text-on-accent hover:bg-accent-hover focus-visible:outline-hidden \" +\n \"focus-visible:ring-2 focus-visible:ring-ring disabled:bg-surface-3 disabled:text-fg-muted [&>svg]:h-[18px] [&>svg]:w-[18px]\",\n pillRoot: \"flex w-full flex-col gap-2\",\n pill:\n \"flex items-end gap-1.5 rounded-[1.75rem] border border-border-default bg-surface-1 py-1.5 pl-2 pr-1.5 transition-colors \" +\n \"focus-within:border-accent focus-within:ring-2 focus-within:ring-ring\",\n pillLeading: \"flex min-h-9 shrink-0 items-center gap-1 self-end\",\n pillInput:\n \"max-h-32 min-h-9 min-w-0 flex-1 resize-none border-0 bg-transparent px-1.5 py-[7px] text-sm leading-snug text-fg placeholder:text-fg-muted \" +\n \"focus:outline-hidden focus-visible:outline-hidden\",\n pillSend:\n \"inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-accent text-on-accent shadow-sm transition-colors hover:bg-accent-hover \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-surface-1 \" +\n \"disabled:cursor-not-allowed disabled:bg-surface-3 disabled:text-fg-muted disabled:shadow-none [&>svg]:h-4 [&>svg]:w-4\",\n pillStatus: \"m-0 flex min-h-5 items-center gap-1.5 px-3 text-xs text-fg-secondary [&_svg]:h-3.5 [&_svg]:w-3.5 [&_svg]:shrink-0\",\n item: \"flex w-full items-center gap-3 px-4 py-2.5 text-left hover:bg-surface-2 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring\",\n count: \"inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-accent px-1.5 text-xs font-extrabold text-on-accent\",\n },\n});\n\nexport type MessageStatus = \"sending\" | \"sent\" | \"delivered\" | \"read\" | \"failed\";\n\nexport type MessageBubbleLabels = {\n /** The words of each status, shown under the last message of yours. */\n status?: Partial<Record<MessageStatus, string>>;\n /** A failed message's retry button (“Retry”). */\n retry?: string;\n};\n\nconst STATUS: Record<MessageStatus, string> = { sending: \"Sending\", sent: \"Sent\", delivered: \"Delivered\", read: \"Read\", failed: \"Not sent\" };\n\nexport type MessageBubbleProps = {\n /** The text. */\n text: string;\n /** Yours (right, in the accent) or theirs (left). */\n mine?: boolean;\n /** The time, written by the app (“14:02”). */\n time?: string;\n /** Where a message of yours stands; shown when `showStatus`. */\n status?: MessageStatus;\n /** Show the status under the bubble (the last message of yours). */\n showStatus?: boolean;\n /** A line above the text: what it answers. */\n context?: string;\n /** Sends a failed message again (a button under it). */\n onRetry?: () => void;\n /** Part of a run of the same sender: the corner towards the next one is tighter. */\n grouped?: \"first\" | \"middle\" | \"last\" | \"single\";\n /** The words, passed translated. */\n labels?: MessageBubbleLabels;\n /** Classes merged on the row. */\n className?: string;\n};\n\nconst CORNERS = {\n mine: { first: \"rounded-br-md\", middle: \"rounded-r-md\", last: \"rounded-tr-md\", single: \"\" },\n theirs: { first: \"rounded-bl-md\", middle: \"rounded-l-md\", last: \"rounded-tl-md\", single: \"\" },\n} as const;\n\n/** A message: yours in the accent on the right, theirs on the left, the time inside, the receipt under the last of yours. */\nexport function MessageBubble({ text, mine, time, status, showStatus, context, onRetry, grouped = \"single\", labels, className }: MessageBubbleProps) {\n const s = chat();\n const words = { ...STATUS, ...labels?.status };\n return (\n <div className={s.row({ className: [mine ? \"items-end\" : \"items-start\", className].filter(Boolean).join(\" \") })}>\n <p\n className={s.bubble({\n className: [mine ? \"bg-accent text-on-accent\" : \"bg-surface-2 text-fg\", CORNERS[mine ? \"mine\" : \"theirs\"][grouped], status === \"failed\" ? \"ring-2 ring-danger\" : \"\", status === \"sending\" ? \"opacity-70\" : \"\"].join(\" \"),\n })}\n >\n {context ? <span className={s.context()}>{context}</span> : null}\n {text}\n {time ? <span className={s.time()}>{time}</span> : null}\n </p>\n {showStatus && status ? (\n <p className={s.status()} data-status={status}>\n {status === \"sent\" || status === \"delivered\" || status === \"read\" ? (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" strokeWidth=\"2.2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden className={status === \"read\" ? \"stroke-accent\" : \"stroke-fg-secondary\"}>\n <path d={status === \"sent\" ? \"M5 12.5l4.5 4.5L19 7.5\" : \"M2 12.5l4.5 4.5L16 7.5M9 15l2 2 9.5-9.5\"} />\n </svg>\n ) : null}\n {words[status]}\n {status === \"failed\" && onRetry ? (\n <button type=\"button\" onClick={onRetry} className=\"ml-1 font-semibold text-fg underline underline-offset-2 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\">\n {labels?.retry ?? \"Retry\"}\n </button>\n ) : null}\n </p>\n ) : null}\n </div>\n );\n}\n\nexport type TypingIndicatorProps = {\n /** What is read and shown beside the dots (“Elena is typing”) — passed translated. */\n label: string;\n /** Show the words beside the dots. */\n showLabel?: boolean;\n /** Classes merged on the root. */\n className?: string;\n};\n\n/** Three dots that breathe while the other person types, announced politely; still under reduced motion. */\nexport function TypingIndicator({ label, showLabel, className }: TypingIndicatorProps) {\n const s = chat();\n return (\n <div role=\"status\" className={[\"flex items-center gap-2\", className].filter(Boolean).join(\" \")}>\n <style href=\"krizaka-typing\" precedence=\"default\">{\"@keyframes kz-typing{0%,60%,100%{opacity:.35;transform:none}30%{opacity:1;transform:translateY(-2px)}}\"}</style>\n <span className={s.typing()} aria-hidden>\n {[0, 1, 2].map((i) => (\n <span key={i} className={s.dot()} style={{ animationDelay: `${i * 0.18}s` }} />\n ))}\n </span>\n <span className={showLabel ? \"text-xs text-fg-secondary\" : \"sr-only\"}>{label}</span>\n </div>\n );\n}\n\nexport type MessageComposerProps = {\n /** The draft (controlled). */\n value: string;\n /** Called on every key. */\n onValueChange: (text: string) => void;\n /** Sends the draft (Enter, or the button); Shift+Enter adds a line. */\n onSend: () => void;\n /** The input's name and placeholder (“Message”). */\n placeholder: string;\n /** The send button's name (“Send”). */\n sendLabel: string;\n /** Before the input: an attach button (`bar`), or the mode/model chip and attach, inside the field (`pill`). */\n leading?: React.ReactNode;\n /** Cannot write: the reason in its place. */\n disabledReason?: string;\n /** The longest message. Default 4000. */\n maxLength?: number;\n /** `bar`: a docked bar with a square-ish field. `pill`: one rounded field holding `leading`, the text and a round send button — no inner border. Default `bar`. */\n variant?: \"bar\" | \"pill\";\n /** `pill`: the quiet line under the field (privacy, cost estimate). Its row is always reserved, so a changing line never moves the field. Read politely. */\n status?: React.ReactNode;\n /** An answer is on its way: the send is held (and `aria-busy` is set) while the field stays where it is, size included. */\n busy?: boolean;\n /** Overrides “the draft has text” — e.g. an attachment alone can be sent. A busy composer never sends. */\n canSend?: boolean;\n /** Attributes (`data-testid`, `autoFocus`, `id`…) forwarded to the textarea. */\n inputProps?: Omit<React.ComponentPropsWithoutRef<\"textarea\">, \"value\" | \"onChange\" | \"placeholder\" | \"maxLength\" | \"className\" | \"rows\">;\n /** Attributes forwarded to the send button. */\n sendProps?: Omit<React.ComponentPropsWithoutRef<\"button\">, \"type\" | \"disabled\" | \"className\" | \"aria-label\">;\n /** Classes merged on the root. */\n className?: string;\n};\n\n/** Where a message is written: a growing textarea (Enter sends, Shift+Enter a new line), an attach slot, a send button. The `pill` variant is the one-field look of the Krizaka chat card. */\nexport function MessageComposer({\n value,\n onValueChange,\n onSend,\n placeholder,\n sendLabel,\n leading,\n disabledReason,\n maxLength = 4000,\n variant = \"bar\",\n status,\n busy = false,\n canSend,\n inputProps,\n sendProps,\n className,\n}: MessageComposerProps) {\n const s = chat();\n const pill = variant === \"pill\";\n const ready = !busy && (canSend ?? value.trim().length > 0);\n const textarea = (\n <textarea\n aria-label={placeholder}\n placeholder={placeholder}\n rows={1}\n maxLength={maxLength}\n {...inputProps}\n value={value}\n onChange={(e) => onValueChange(e.target.value)}\n onKeyDown={(e) => {\n inputProps?.onKeyDown?.(e);\n if (e.defaultPrevented) return;\n if (e.key === \"Enter\" && !e.shiftKey && !e.nativeEvent.isComposing) {\n e.preventDefault();\n if (ready) onSend();\n }\n }}\n className={pill ? s.pillInput() : s.input()}\n style={{ fieldSizing: \"content\" } as React.CSSProperties}\n />\n );\n const send = (\n <button type=\"submit\" aria-label={sendLabel} {...sendProps} disabled={!ready} className={pill ? s.pillSend() : s.send()}>\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden>\n <path d=\"M4 12l16-8-6 16-2.5-6.5L4 12z\" />\n </svg>\n </button>\n );\n const submit = (e: React.FormEvent) => {\n e.preventDefault();\n if (ready) onSend();\n };\n if (pill) {\n return (\n <form className={s.pillRoot({ className })} onSubmit={submit} aria-busy={busy || undefined}>\n {disabledReason ? (\n <p className=\"m-0 rounded-[1.75rem] border border-border-default bg-surface-1 px-4 py-2.5 text-sm text-fg-secondary\">{disabledReason}</p>\n ) : (\n <div className={s.pill()}>\n {leading ? <div className={s.pillLeading()}>{leading}</div> : null}\n {textarea}\n {send}\n </div>\n )}\n <p role=\"status\" aria-live=\"polite\" className={s.pillStatus()}>\n {status}\n </p>\n </form>\n );\n }\n return (\n <form className={s.composer({ className })} onSubmit={submit} aria-busy={busy || undefined}>\n {disabledReason ? (\n <p className=\"m-0 flex-1 py-2 text-sm text-fg-secondary\">{disabledReason}</p>\n ) : (\n <>\n {leading}\n {textarea}\n {send}\n </>\n )}\n </form>\n );\n}\n\nexport type ConversationItemProps = {\n /** The other person's name. */\n name: string;\n /** Their avatar's URL. */\n avatar?: string | null;\n /** The last message, or “typing…”. */\n preview: string;\n /** The preview is the typing signal: in the accent. */\n typing?: boolean;\n /** When (“14:02”, “Mon”). */\n time?: string;\n /** Unread messages: a count, the name in bold. */\n unread?: number;\n /** The unread count in words (“3 unread”). */\n unreadLabel?: string;\n /** Before the name: a presence dot (online). */\n online?: boolean;\n /** The presence in words (“online”). */\n onlineLabel?: string;\n /** Opens the conversation (a link). */\n href?: string;\n /** Opens the conversation (a button). */\n onSelect?: () => void;\n /** Classes merged on the row. */\n className?: string;\n};\n\n/** A conversation in the list: avatar, name, the last message or typing, the time, the unread count. */\nexport function ConversationItem({ name, avatar, preview, typing, time, unread = 0, unreadLabel, online, onlineLabel, href, onSelect, className }: ConversationItemProps) {\n const s = chat();\n const body = (\n <>\n <span className=\"relative shrink-0\">\n <Avatar src={avatar ?? null} alt=\"\" fallback={name.charAt(0)} size=\"lg\" />\n {online ? <span aria-hidden className=\"absolute bottom-0 right-0 h-3 w-3 rounded-full bg-success ring-2 ring-surface-0\" /> : null}\n </span>\n <span className=\"flex min-w-0 flex-1 flex-col\">\n <span className=\"flex items-center gap-2\">\n <span className={[\"min-w-0 flex-1 truncate text-sm text-fg\", unread > 0 ? \"font-extrabold\" : \"font-semibold\"].join(\" \")}>\n {name}\n {online && onlineLabel ? <span className=\"sr-only\">{`, ${onlineLabel}`}</span> : null}\n </span>\n {time ? <span className={[\"shrink-0 text-xs tabular-nums\", unread > 0 ? \"text-fg-accent\" : \"text-fg-secondary\"].join(\" \")}>{time}</span> : null}\n </span>\n <span className=\"flex items-center gap-2\">\n <span className={[\"min-w-0 flex-1 truncate text-xs\", typing ? \"text-fg-accent\" : unread > 0 ? \"text-fg\" : \"text-fg-secondary\"].join(\" \")}>{preview}</span>\n {unread > 0 ? (\n <span className={s.count()}>\n <span aria-hidden>{unread > 99 ? \"99+\" : unread}</span>\n <span className=\"sr-only\">{unreadLabel ?? String(unread)}</span>\n </span>\n ) : null}\n </span>\n </span>\n </>\n );\n return href ? (\n <a href={href} className={s.item({ className })}>\n {body}\n </a>\n ) : (\n <button type=\"button\" onClick={onSelect} className={s.item({ className })}>\n {body}\n </button>\n );\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@krizaka/ui",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.19.0",
|
|
4
4
|
"description": "The Krizaka front-end platform's components: the primitives (button, card, dialog, toast, menu, field, tabs, chip, switch, slider, command palette, theme, countdown…) on the semantic --kz-* tokens, the animated marks (Krizaka, Orazaka, Orochia, Yasakia, Yororis) and the motion signature — web and /native.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"krizaka",
|
|
@@ -629,7 +629,7 @@
|
|
|
629
629
|
],
|
|
630
630
|
"peerDependencies": {
|
|
631
631
|
"@krizaka/query": ">=0.2.1",
|
|
632
|
-
"@krizaka/tailwind": ">=3.
|
|
632
|
+
"@krizaka/tailwind": ">=3.19.0",
|
|
633
633
|
"@maplibre/maplibre-react-native": ">=11",
|
|
634
634
|
"@shopify/flash-list": ">=2",
|
|
635
635
|
"expo-camera": ">=16",
|
|
@@ -1557,7 +1557,7 @@
|
|
|
1557
1557
|
{
|
|
1558
1558
|
"name": "./card-grid",
|
|
1559
1559
|
"path": "dist/card-grid.js",
|
|
1560
|
-
"limit": "1 kB",
|
|
1560
|
+
"limit": "1.2 kB",
|
|
1561
1561
|
"gzip": true,
|
|
1562
1562
|
"ignore": [
|
|
1563
1563
|
"tailwind-variants",
|
|
@@ -2143,8 +2143,8 @@
|
|
|
2143
2143
|
"sonner": "^2.0.8",
|
|
2144
2144
|
"tailwind-variants": "^3.3.1",
|
|
2145
2145
|
"@krizaka/intl": "^0.2.1",
|
|
2146
|
-
"@krizaka/
|
|
2147
|
-
"@krizaka/
|
|
2146
|
+
"@krizaka/pacer": "^0.1.0",
|
|
2147
|
+
"@krizaka/tokens": "^3.19.0"
|
|
2148
2148
|
},
|
|
2149
2149
|
"scripts": {
|
|
2150
2150
|
"build": "node -e \"const fs=require('node:fs');fs.rmSync('dist',{recursive:true,force:true});for(const f of fs.readdirSync('registry/examples',{recursive:true}))if(/\\.(js|d\\.ts)$/.test(f))fs.rmSync('registry/examples/'+f)\" && tsup && node -e \"require('node:fs').copyFileSync('src/motion.css','dist/motion.css');require('node:fs').copyFileSync('src/blocks.css','dist/blocks.css');require('node:fs').copyFileSync('src/story.css','dist/story.css')\" && node scripts/build-registry.mjs",
|
package/registry/card-grid.json
CHANGED
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"`fit=\"fit\"` for two or three cards that should share the whole width; `fill` keeps a lone card at its size.",
|
|
28
28
|
"Keep `reveal` on below the first screen, off on it; `columns` makes the stagger restart on each row.",
|
|
29
29
|
"The default `align=\"stretch\"` gives a row one height; the cards pin their foot to the bottom (`LinkCard` facts + way in, `ArgumentCard` actions) so the footers line up. `align=\"start\"` for cards that unfold.",
|
|
30
|
-
"`as=\"ul\"` when the cards are a list of links."
|
|
30
|
+
"`as=\"ul\"` when the cards are a list of links.",
|
|
31
|
+
"`foundation` when one thing (an engine, a platform) carries all the others: it takes its own card, wide, above (`foundationPosition=\"end\"` for under), and the cards sit in a hairline frame joined to it. Say the relation with `foundationLabel`; set the hue on `className`."
|
|
31
32
|
],
|
|
32
33
|
"accessibility": {
|
|
33
34
|
"keyboard": [],
|
|
@@ -85,6 +86,27 @@
|
|
|
85
86
|
"description": "`fill` keeps empty tracks (a lone card keeps its width); `fit` stretches the cards of a short row across the width.",
|
|
86
87
|
"required": false
|
|
87
88
|
},
|
|
89
|
+
{
|
|
90
|
+
"name": "foundation",
|
|
91
|
+
"type": "ReactNode",
|
|
92
|
+
"default": null,
|
|
93
|
+
"description": "A foundation: one wide card that the cards of the grid stand on (an engine, a platform, a shared service). It spans\nthe full width beside the grid, and a hairline joins it to a frame around the cards — \"everything inside runs on it\".\nThe frame takes the hue of `className` (a `data-hue` or a brand class on the grid sets it for the whole block).",
|
|
94
|
+
"required": false
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"name": "foundationLabel",
|
|
98
|
+
"type": "ReactNode",
|
|
99
|
+
"default": null,
|
|
100
|
+
"description": "A short mono label on the frame's edge, naming the relation (`Powered by Orazaka`). Only with `foundation`.",
|
|
101
|
+
"required": false
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"name": "foundationPosition",
|
|
105
|
+
"type": "\"start\" | \"end\"",
|
|
106
|
+
"default": "start",
|
|
107
|
+
"description": "Where the foundation sits: above the cards, or under them.",
|
|
108
|
+
"required": false
|
|
109
|
+
},
|
|
88
110
|
{
|
|
89
111
|
"name": "light",
|
|
90
112
|
"type": "boolean",
|
|
@@ -126,6 +148,14 @@
|
|
|
126
148
|
"module": "@krizaka/ui/registry/examples/card-grid/aligned",
|
|
127
149
|
"code": "import { ProductLogo } from \"@krizaka/ui\";\nimport { CardGrid } from \"@krizaka/ui/card-grid\";\nimport { LinkCard } from \"@krizaka/ui/link-card\";\nimport { Facts } from \"@krizaka/ui/page\";\n\n// A row of cards whose sentences have different lengths: the cells share one height and each card pins its facts\n// and its way in to the bottom, so the status and the link of every card sit on the same line.\nexport default function CardGridAligned() {\n return (\n <CardGrid className=\"w-full max-w-4xl\" min={220} columns={3} reveal={false}>\n <LinkCard layout=\"stack\" href=\"#orazaka\" icon={<ProductLogo id=\"orazaka\" size={28} animated={false} />} title=\"Orazaka\" description=\"Sovereign AI on your own servers: chat, image, video and voice.\" extra={<Facts items={[{ label: \"Dev preview\", tone: \"ok\" }]} />} cta=\"Open orazaka\" />\n <LinkCard layout=\"stack\" href=\"#orochia\" icon={<ProductLogo id=\"orochia\" size={28} animated={false} />} title=\"Orochia\" description=\"Creators, paid.\" extra={<Facts items={[{ label: \"Coming soon\", tone: \"wip\" }]} />} cta=\"Open orochia\" />\n <LinkCard layout=\"stack\" href=\"#yororis\" icon={<ProductLogo id=\"yororis\" size={28} animated={false} />} title=\"Yororis\" description=\"A quiet place to keep what matters to you, built around the moments of a day and the threads between them.\" extra={<Facts items={[{ label: \"Under construction\", tone: \"wip\" }]} />} cta=\"Open yororis\" />\n </CardGrid>\n );\n}\n"
|
|
128
150
|
},
|
|
151
|
+
{
|
|
152
|
+
"name": "foundation",
|
|
153
|
+
"title": "A foundation",
|
|
154
|
+
"description": "`foundation`: one wide card the others stand on, a hairline to a frame around them.",
|
|
155
|
+
"path": "examples/card-grid/foundation.tsx",
|
|
156
|
+
"module": "@krizaka/ui/registry/examples/card-grid/foundation",
|
|
157
|
+
"code": "import { ProductLogo } from \"@krizaka/ui\";\nimport { ArgumentCard } from \"@krizaka/ui/argument-card\";\nimport { CardGrid } from \"@krizaka/ui/card-grid\";\nimport { LinkCard } from \"@krizaka/ui/link-card\";\n\nconst products = [\n { id: \"orochia\", name: \"Orochia\", hook: \"Recommendations, search and captions.\" },\n { id: \"yomizaka\", name: \"Yomizaka\", hook: \"Document checks and signed reports.\" },\n { id: \"kachizaka\", name: \"Kachizaka\", hook: \"Live match feeds.\" },\n] as const;\n\n// `foundation`: one wide card that the cards below stand on, a hairline stem to a frame around them. One column on a phone.\nexport default function CardGridFoundation() {\n return (\n <CardGrid\n className=\"w-full max-w-5xl\"\n min={240}\n reveal={false}\n foundationLabel=\"Powered by Orazaka\"\n foundation={\n <LinkCard\n href=\"#orazaka\"\n icon={<ProductLogo id=\"orazaka\" size={28} animated={false} />}\n title=\"The engine\"\n description=\"Every product's AI runs on Orazaka: one pipeline, your models, your network.\"\n cta=\"Open the engine\"\n />\n }\n >\n {products.map((p) => (\n <ArgumentCard key={p.id} id={`product-${p.id}`} icon={<ProductLogo id={p.id} size={32} animated={false} />} title={p.name} hook={{ label: \"Uses it for\", text: p.hook }} more={{ href: `#${p.id}`, label: \"Details\" }} />\n ))}\n </CardGrid>\n );\n}\n"
|
|
158
|
+
},
|
|
129
159
|
{
|
|
130
160
|
"name": "reveal",
|
|
131
161
|
"title": "A list of sections",
|
|
@@ -140,7 +170,7 @@
|
|
|
140
170
|
"files": [
|
|
141
171
|
{
|
|
142
172
|
"path": "card-grid/card-grid.tsx",
|
|
143
|
-
"content": "// Client (the pointer's light). A responsive grid of cards (blocks.css): one column on a phone, the cells of a row\n// share their height, each card rises once as it enters the viewport — staggered by column — and the pointer carries a\n// light over the card it is on.\nimport * as React from \"react\";\n\nimport { cn } from \"../cn\";\nimport { POINTER_LIGHT_TARGET, usePointerLight } from \"../pointer-light/pointer-light\";\n\nconst STEP_MS = 70;\n\nexport type CardGridProps = Omit<React.HTMLAttributes<HTMLElement>, \"children\"> & {\n /** The cards: one cell each. */\n children: React.ReactNode;\n /**\n * The narrowest a column may be, in pixels: the grid fits as many columns as the width allows.\n * @default 300\n */\n min?: number;\n /**\n * `fill` keeps empty tracks (a lone card keeps its width); `fit` stretches the cards of a short row across the width.\n * @default \"fill\"\n */\n fit?: \"fill\" | \"fit\";\n /**\n * `stretch` gives the cells of a row one height; `start` lets each card keep its own (cards that open a panel).\n * @default \"stretch\"\n */\n align?: \"stretch\" | \"start\";\n /**\n * The cards rise once into place (`[data-reveal]`, MotionObserver), staggered by column. `false` on the first screen.\n * @default true\n */\n reveal?: boolean;\n /**\n * The columns the stagger counts (the delay restarts on every row).\n * @default 3\n */\n columns?: number;\n /**\n * The light that follows the pointer over the cards (`[data-kz-light]`).\n * @default true\n */\n light?: boolean;\n /** `ul` when the cards are a list (each cell an `li`). */\n as?: \"div\" | \"ul\" | \"ol\";\n};\n\n/** A responsive grid of cards: equal rows, a staggered reveal, the pointer's light. */\nexport function CardGrid({
|
|
173
|
+
"content": "// Client (the pointer's light). A responsive grid of cards (blocks.css): one column on a phone, the cells of a row\n// share their height, each card rises once as it enters the viewport — staggered by column — and the pointer carries a\n// light over the card it is on.\nimport * as React from \"react\";\n\nimport { cn } from \"../cn\";\nimport { POINTER_LIGHT_TARGET, usePointerLight } from \"../pointer-light/pointer-light\";\n\nconst STEP_MS = 70;\n\nexport type CardGridProps = Omit<React.HTMLAttributes<HTMLElement>, \"children\"> & {\n /** The cards: one cell each. */\n children: React.ReactNode;\n /**\n * The narrowest a column may be, in pixels: the grid fits as many columns as the width allows.\n * @default 300\n */\n min?: number;\n /**\n * `fill` keeps empty tracks (a lone card keeps its width); `fit` stretches the cards of a short row across the width.\n * @default \"fill\"\n */\n fit?: \"fill\" | \"fit\";\n /**\n * `stretch` gives the cells of a row one height; `start` lets each card keep its own (cards that open a panel).\n * @default \"stretch\"\n */\n align?: \"stretch\" | \"start\";\n /**\n * The cards rise once into place (`[data-reveal]`, MotionObserver), staggered by column. `false` on the first screen.\n * @default true\n */\n reveal?: boolean;\n /**\n * The columns the stagger counts (the delay restarts on every row).\n * @default 3\n */\n columns?: number;\n /**\n * The light that follows the pointer over the cards (`[data-kz-light]`).\n * @default true\n */\n light?: boolean;\n /** `ul` when the cards are a list (each cell an `li`). */\n as?: \"div\" | \"ul\" | \"ol\";\n /**\n * A foundation: one wide card that the cards of the grid stand on (an engine, a platform, a shared service). It spans\n * the full width beside the grid, and a hairline joins it to a frame around the cards — \"everything inside runs on it\".\n * The frame takes the hue of `className` (a `data-hue` or a brand class on the grid sets it for the whole block).\n */\n foundation?: React.ReactNode;\n /**\n * Where the foundation sits: above the cards, or under them.\n * @default \"start\"\n */\n foundationPosition?: \"start\" | \"end\";\n /** A short mono label on the frame's edge, naming the relation (`Powered by Orazaka`). Only with `foundation`. */\n foundationLabel?: React.ReactNode;\n};\n\n/** A responsive grid of cards: equal rows, a staggered reveal, the pointer's light. */\nexport function CardGrid({\n children,\n min = 300,\n fit = \"fill\",\n align = \"stretch\",\n reveal = true,\n columns = 3,\n light = true,\n as: Tag = \"div\",\n foundation,\n foundationPosition = \"start\",\n foundationLabel,\n className,\n style,\n ...props\n}: CardGridProps) {\n const ref = React.useRef<HTMLElement>(null);\n usePointerLight(light ? ref : NO_REF, POINTER_LIGHT_TARGET);\n const Cell = Tag === \"div\" ? \"div\" : \"li\";\n const grid = (\n <Tag\n ref={foundation ? undefined : (ref as React.Ref<never>)}\n className={cn(\"kz-cards\", !foundation && className)}\n data-fit={fit === \"fit\" ? \"\" : undefined}\n data-align={align === \"start\" ? \"start\" : undefined}\n style={{ \"--kz-cards-min\": `${min}px`, ...(foundation ? null : style) } as React.CSSProperties}\n {...props}\n >\n {React.Children.toArray(children).map((child, i) => {\n const step = Math.min(i % Math.max(columns, 1), 5);\n return (\n <Cell\n key={React.isValidElement(child) && child.key != null ? child.key : i}\n className=\"kz-cards-cell\"\n data-reveal={reveal ? \"\" : undefined}\n style={reveal && step ? ({ \"--kz-delay\": `${step * STEP_MS}ms` } as React.CSSProperties) : undefined}\n >\n {child}\n </Cell>\n );\n })}\n </Tag>\n );\n if (!foundation) return grid;\n return (\n <div ref={ref as React.Ref<HTMLDivElement>} className={cn(\"kz-foundation\", className)} data-position={foundationPosition} style={style}>\n <div className=\"kz-foundation-base\" data-reveal={reveal ? \"\" : undefined}>\n {foundation}\n </div>\n <div className=\"kz-foundation-group\">\n {foundationLabel && <span className=\"kz-foundation-label\">{foundationLabel}</span>}\n {grid}\n </div>\n </div>\n );\n}\n\nconst NO_REF: React.RefObject<HTMLElement | null> = { current: null };\n"
|
|
144
174
|
},
|
|
145
175
|
{
|
|
146
176
|
"path": "card-grid/index.ts",
|
|
@@ -148,7 +178,7 @@
|
|
|
148
178
|
},
|
|
149
179
|
{
|
|
150
180
|
"path": "card-grid/meta.ts",
|
|
151
|
-
"content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Card grid\",\n summary:\n \"A responsive grid of cards: as many columns as the width allows (one on a phone), the cells of a row at one height, a staggered reveal and the light that follows the pointer.\",\n status: \"beta\",\n category: \"blocks\",\n platforms: \"web\",\n whenToUse: [\n \"Entry points of a hub (`LinkCard`), the reasons of a product page (`ArgumentCard`), plans, products.\",\n \"Any set of peer cards that should read as one group with one rhythm.\",\n ],\n whenNotToUse: [\n { when: \"For a sequence the reader follows in order.\", use: \"stepper\" },\n { when: \"For rows of data to compare or sort.\", use: \"data-table\" },\n ],\n bestPractices: [\n \"Set `min` to the narrowest a card still reads well; the grid does the rest at every width.\",\n \"`fit=\\\"fit\\\"` for two or three cards that should share the whole width; `fill` keeps a lone card at its size.\",\n \"Keep `reveal` on below the first screen, off on it; `columns` makes the stagger restart on each row.\",\n \"The default `align=\\\"stretch\\\"` gives a row one height; the cards pin their foot to the bottom (`LinkCard` facts + way in, `ArgumentCard` actions) so the footers line up. `align=\\\"start\\\"` for cards that unfold.\",\n \"`as=\\\"ul\\\"` when the cards are a list of links.\",\n ],\n accessibility: {\n keyboard: [],\n notes: [\n \"The grid adds no role; `as=\\\"ul\\\"` makes it a list (each cell an `li`).\",\n \"The pointer's light is decorative and never runs on touch screens or under `prefers-reduced-motion`.\",\n \"Revealed cells are only hidden visually until they enter the viewport; under reduced motion they never move.\",\n ],\n },\n related: [\"link-card\", \"argument-card\", \"pointer-light\"],\n web: {\n imports: [\"CardGrid\"],\n examples: [\n { name: \"products\", title: \"Two halves\", description: \"`fit=\\\"fit\\\"`: two cards share the whole width.\" },\n { name: \"aligned\", title: \"An aligned row\", description: \"Cards with sentences of different lengths: one height, and the facts and the way in of every card on the same line.\" },\n { name: \"reveal\", title: \"A list of sections\", description: \"`as=\\\"ul\\\"`, six links that rise staggered by column on a real page.\" },\n ],\n },\n} satisfies ComponentMeta;\n"
|
|
181
|
+
"content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Card grid\",\n summary:\n \"A responsive grid of cards: as many columns as the width allows (one on a phone), the cells of a row at one height, a staggered reveal and the light that follows the pointer.\",\n status: \"beta\",\n category: \"blocks\",\n platforms: \"web\",\n whenToUse: [\n \"Entry points of a hub (`LinkCard`), the reasons of a product page (`ArgumentCard`), plans, products.\",\n \"Any set of peer cards that should read as one group with one rhythm.\",\n ],\n whenNotToUse: [\n { when: \"For a sequence the reader follows in order.\", use: \"stepper\" },\n { when: \"For rows of data to compare or sort.\", use: \"data-table\" },\n ],\n bestPractices: [\n \"Set `min` to the narrowest a card still reads well; the grid does the rest at every width.\",\n \"`fit=\\\"fit\\\"` for two or three cards that should share the whole width; `fill` keeps a lone card at its size.\",\n \"Keep `reveal` on below the first screen, off on it; `columns` makes the stagger restart on each row.\",\n \"The default `align=\\\"stretch\\\"` gives a row one height; the cards pin their foot to the bottom (`LinkCard` facts + way in, `ArgumentCard` actions) so the footers line up. `align=\\\"start\\\"` for cards that unfold.\",\n \"`as=\\\"ul\\\"` when the cards are a list of links.\",\n \"`foundation` when one thing (an engine, a platform) carries all the others: it takes its own card, wide, above (`foundationPosition=\\\"end\\\"` for under), and the cards sit in a hairline frame joined to it. Say the relation with `foundationLabel`; set the hue on `className`.\",\n ],\n accessibility: {\n keyboard: [],\n notes: [\n \"The grid adds no role; `as=\\\"ul\\\"` makes it a list (each cell an `li`).\",\n \"The pointer's light is decorative and never runs on touch screens or under `prefers-reduced-motion`.\",\n \"Revealed cells are only hidden visually until they enter the viewport; under reduced motion they never move.\",\n ],\n },\n related: [\"link-card\", \"argument-card\", \"pointer-light\"],\n web: {\n imports: [\"CardGrid\"],\n examples: [\n { name: \"products\", title: \"Two halves\", description: \"`fit=\\\"fit\\\"`: two cards share the whole width.\" },\n { name: \"aligned\", title: \"An aligned row\", description: \"Cards with sentences of different lengths: one height, and the facts and the way in of every card on the same line.\" },\n { name: \"foundation\", title: \"A foundation\", description: \"`foundation`: one wide card the others stand on, a hairline to a frame around them.\" },\n { name: \"reveal\", title: \"A list of sections\", description: \"`as=\\\"ul\\\"`, six links that rise staggered by column on a real page.\" },\n ],\n },\n} satisfies ComponentMeta;\n"
|
|
152
182
|
},
|
|
153
183
|
{
|
|
154
184
|
"path": "meta.ts",
|
|
@@ -204,6 +234,27 @@
|
|
|
204
234
|
"description": "`fill` keeps empty tracks (a lone card keeps its width); `fit` stretches the cards of a short row across the width.",
|
|
205
235
|
"required": false
|
|
206
236
|
},
|
|
237
|
+
{
|
|
238
|
+
"name": "foundation",
|
|
239
|
+
"type": "ReactNode",
|
|
240
|
+
"default": null,
|
|
241
|
+
"description": "A foundation: one wide card that the cards of the grid stand on (an engine, a platform, a shared service). It spans\nthe full width beside the grid, and a hairline joins it to a frame around the cards — \"everything inside runs on it\".\nThe frame takes the hue of `className` (a `data-hue` or a brand class on the grid sets it for the whole block).",
|
|
242
|
+
"required": false
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
"name": "foundationLabel",
|
|
246
|
+
"type": "ReactNode",
|
|
247
|
+
"default": null,
|
|
248
|
+
"description": "A short mono label on the frame's edge, naming the relation (`Powered by Orazaka`). Only with `foundation`.",
|
|
249
|
+
"required": false
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
"name": "foundationPosition",
|
|
253
|
+
"type": "\"start\" | \"end\"",
|
|
254
|
+
"default": "start",
|
|
255
|
+
"description": "Where the foundation sits: above the cards, or under them.",
|
|
256
|
+
"required": false
|
|
257
|
+
},
|
|
207
258
|
{
|
|
208
259
|
"name": "light",
|
|
209
260
|
"type": "boolean",
|
package/registry/chat.json
CHANGED
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"bestPractices": [
|
|
26
26
|
"Group the runs of one sender (`grouped`); show the receipt under the last message of yours only.",
|
|
27
27
|
"Keep the server's order and merge (the app's), the bubble only draws.",
|
|
28
|
-
"The composer never clears itself: clear the draft once the message is sent."
|
|
28
|
+
"The composer never clears itself: clear the draft once the message is sent.",
|
|
29
|
+
"`variant=\"pill\"` for a chat page: put the mode/model chip and attach in `leading`, privacy and cost in `status`, and pass `busy` while an answer streams — the field's box does not move."
|
|
29
30
|
],
|
|
30
31
|
"accessibility": {
|
|
31
32
|
"keyboard": [
|
|
@@ -36,6 +37,7 @@
|
|
|
36
37
|
],
|
|
37
38
|
"notes": [
|
|
38
39
|
"Each bubble is read with its time; a failed one says so and offers a retry.",
|
|
40
|
+
"The `pill` status line is a polite live region; a busy composer holds the send (`aria-busy`) and says nothing else.",
|
|
39
41
|
"The typing dots are announced politely by their words.",
|
|
40
42
|
"A conversation row reads name, presence, last message, time and unread count."
|
|
41
43
|
]
|
|
@@ -217,7 +219,7 @@
|
|
|
217
219
|
},
|
|
218
220
|
{
|
|
219
221
|
"component": "MessageComposer",
|
|
220
|
-
"description": "Where a message is written: a growing textarea (Enter sends, Shift+Enter a new line), an attach slot, a send button.",
|
|
222
|
+
"description": "Where a message is written: a growing textarea (Enter sends, Shift+Enter a new line), an attach slot, a send button. The `pill` variant is the one-field look of the Krizaka chat card.",
|
|
221
223
|
"props": [
|
|
222
224
|
{
|
|
223
225
|
"name": "onSend",
|
|
@@ -254,6 +256,20 @@
|
|
|
254
256
|
"description": "The draft (controlled).",
|
|
255
257
|
"required": true
|
|
256
258
|
},
|
|
259
|
+
{
|
|
260
|
+
"name": "busy",
|
|
261
|
+
"type": "boolean",
|
|
262
|
+
"default": "false",
|
|
263
|
+
"description": "An answer is on its way: the send is held (and `aria-busy` is set) while the field stays where it is, size included.",
|
|
264
|
+
"required": false
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
"name": "canSend",
|
|
268
|
+
"type": "boolean",
|
|
269
|
+
"default": null,
|
|
270
|
+
"description": "Overrides “the draft has text” — e.g. an attachment alone can be sent. A busy composer never sends.",
|
|
271
|
+
"required": false
|
|
272
|
+
},
|
|
257
273
|
{
|
|
258
274
|
"name": "className",
|
|
259
275
|
"type": "string",
|
|
@@ -268,11 +284,18 @@
|
|
|
268
284
|
"description": "Cannot write: the reason in its place.",
|
|
269
285
|
"required": false
|
|
270
286
|
},
|
|
287
|
+
{
|
|
288
|
+
"name": "inputProps",
|
|
289
|
+
"type": "Omit<Omit<DetailedHTMLProps<TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>, \"ref\">, \"className\" | ... 4 more ... | \"rows\">",
|
|
290
|
+
"default": null,
|
|
291
|
+
"description": "Attributes (`data-testid`, `autoFocus`, `id`…) forwarded to the textarea.",
|
|
292
|
+
"required": false
|
|
293
|
+
},
|
|
271
294
|
{
|
|
272
295
|
"name": "leading",
|
|
273
296
|
"type": "ReactNode",
|
|
274
297
|
"default": null,
|
|
275
|
-
"description": "Before the input: an attach button.",
|
|
298
|
+
"description": "Before the input: an attach button (`bar`), or the mode/model chip and attach, inside the field (`pill`).",
|
|
276
299
|
"required": false
|
|
277
300
|
},
|
|
278
301
|
{
|
|
@@ -281,6 +304,27 @@
|
|
|
281
304
|
"default": "4000",
|
|
282
305
|
"description": "The longest message. Default 4000.",
|
|
283
306
|
"required": false
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
"name": "sendProps",
|
|
310
|
+
"type": "Omit<Omit<DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, \"ref\">, \"className\" | ... 2 more ... | \"disabled\">",
|
|
311
|
+
"default": null,
|
|
312
|
+
"description": "Attributes forwarded to the send button.",
|
|
313
|
+
"required": false
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
"name": "status",
|
|
317
|
+
"type": "ReactNode",
|
|
318
|
+
"default": null,
|
|
319
|
+
"description": "`pill`: the quiet line under the field (privacy, cost estimate). Its row is always reserved, so a changing line never moves the field. Read politely.",
|
|
320
|
+
"required": false
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
"name": "variant",
|
|
324
|
+
"type": "\"bar\" | \"pill\"",
|
|
325
|
+
"default": "bar",
|
|
326
|
+
"description": "`bar`: a docked bar with a square-ish field. `pill`: one rounded field holding `leading`, the text and a round send button — no inner border. Default `bar`.",
|
|
327
|
+
"required": false
|
|
284
328
|
}
|
|
285
329
|
]
|
|
286
330
|
},
|
|
@@ -328,6 +372,14 @@
|
|
|
328
372
|
"path": "examples/chat/inbox.tsx",
|
|
329
373
|
"module": "@krizaka/ui/registry/examples/chat/inbox",
|
|
330
374
|
"code": "import { ConversationItem } from \"@krizaka/ui/chat\";\n\n// The inbox: online, the last message or “typing…”, the time, the unread count.\nexport default function ChatInbox() {\n return (\n <div className=\"flex max-w-md flex-col rounded-xl border border-border-default bg-surface-0 py-1\">\n <ConversationItem name=\"Elena\" online onlineLabel=\"online\" preview=\"typing…\" typing time=\"19:06\" unread={2} unreadLabel=\"2 unread\" href=\"#/chats/elena\" />\n <ConversationItem name=\"Noah\" preview=\"See you Saturday then!\" time=\"Mon\" href=\"#/chats/noah\" />\n <ConversationItem name=\"Mina\" preview=\"You: haha same\" time=\"Sun\" href=\"#/chats/mina\" />\n </div>\n );\n}\n"
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
"name": "pill-composer",
|
|
378
|
+
"title": "Pill composer",
|
|
379
|
+
"description": "One rounded field: chip and attach inside, round send inside, a status line under it.",
|
|
380
|
+
"path": "examples/chat/pill-composer.tsx",
|
|
381
|
+
"module": "@krizaka/ui/registry/examples/chat/pill-composer",
|
|
382
|
+
"code": "\"use client\";\n\nimport { MessageComposer } from \"@krizaka/ui/chat\";\nimport { useState } from \"react\";\n\n// The chat card's composer: one rounded field — the mode chip and attach inside on the left, the round send inside on\n// the right — and a quiet status line under it (privacy, cost). The field keeps its box while an answer streams.\nexport default function ChatPillComposer() {\n const [draft, setDraft] = useState(\"\");\n const [busy, setBusy] = useState(false);\n return (\n <div className=\"mx-auto w-full max-w-xl\">\n <MessageComposer\n variant=\"pill\"\n value={draft}\n busy={busy}\n onValueChange={setDraft}\n onSend={() => {\n setDraft(\"\");\n setBusy(true);\n setTimeout(() => setBusy(false), 1500);\n }}\n placeholder=\"Ask your private AI…\"\n sendLabel=\"Send\"\n leading={\n <>\n <button type=\"button\" className=\"h-8 rounded-full border border-border-default px-3 text-xs font-medium text-fg-secondary\">\n Chat\n </button>\n <button type=\"button\" aria-label=\"Attach a file\" className=\"grid h-8 w-8 place-items-center rounded-full text-fg-secondary hover:bg-surface-2\">\n +\n </button>\n </>\n }\n status={busy ? \"Answering…\" : \"Runs locally · nothing leaves your network · ~30 credits\"}\n />\n </div>\n );\n}\n"
|
|
331
383
|
}
|
|
332
384
|
]
|
|
333
385
|
},
|
|
@@ -626,7 +678,7 @@
|
|
|
626
678
|
"files": [
|
|
627
679
|
{
|
|
628
680
|
"path": "chat/chat.tsx",
|
|
629
|
-
"content": "// Client (the composer's draft). A conversation's parts — the bubble with its time and receipt, the typing dots, the\n// composer, a conversation in a list — the same API as React Native. Words are props.\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { Avatar } from \"../avatar\";\n\nexport const chat = tv({\n slots: {\n row: \"flex flex-col gap-0.5\",\n bubble: \"max-w-[80%] rounded-2xl px-3 py-2 text-sm leading-snug\",\n context: \"mb-0.5 block text-xs opacity-85\",\n time: \"mt-0.5 block text-right text-[11px] tabular-nums\",\n status: \"flex items-center gap-1 text-xs text-fg-secondary data-[status=failed]:text-fg-danger [&>svg]:h-3.5 [&>svg]:w-3.5\",\n typing: \"inline-flex items-center gap-1 self-start rounded-2xl bg-surface-2 px-3.5 py-3\",\n dot: \"h-[7px] w-[7px] rounded-full bg-fg-secondary motion-safe:animate-[kz-typing_1.2s_ease-in-out_infinite]\",\n composer: \"flex items-end gap-2 border-t border-border-subtle bg-surface-0 px-3 py-2\",\n input:\n \"max-h-32 min-h-10 flex-1 resize-none rounded-2xl border border-border-default bg-surface-1 px-3.5 py-2.5 text-sm text-fg placeholder:text-fg-muted \" +\n \"focus-visible:border-accent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n send:\n \"inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-accent text-on-accent hover:bg-accent-hover focus-visible:outline-hidden \" +\n \"focus-visible:ring-2 focus-visible:ring-ring disabled:bg-surface-3 disabled:text-fg-muted [&>svg]:h-[18px] [&>svg]:w-[18px]\",\n item: \"flex w-full items-center gap-3 px-4 py-2.5 text-left hover:bg-surface-2 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring\",\n count: \"inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-accent px-1.5 text-xs font-extrabold text-on-accent\",\n },\n});\n\nexport type MessageStatus = \"sending\" | \"sent\" | \"delivered\" | \"read\" | \"failed\";\n\nexport type MessageBubbleLabels = {\n /** The words of each status, shown under the last message of yours. */\n status?: Partial<Record<MessageStatus, string>>;\n /** A failed message's retry button (“Retry”). */\n retry?: string;\n};\n\nconst STATUS: Record<MessageStatus, string> = { sending: \"Sending\", sent: \"Sent\", delivered: \"Delivered\", read: \"Read\", failed: \"Not sent\" };\n\nexport type MessageBubbleProps = {\n /** The text. */\n text: string;\n /** Yours (right, in the accent) or theirs (left). */\n mine?: boolean;\n /** The time, written by the app (“14:02”). */\n time?: string;\n /** Where a message of yours stands; shown when `showStatus`. */\n status?: MessageStatus;\n /** Show the status under the bubble (the last message of yours). */\n showStatus?: boolean;\n /** A line above the text: what it answers. */\n context?: string;\n /** Sends a failed message again (a button under it). */\n onRetry?: () => void;\n /** Part of a run of the same sender: the corner towards the next one is tighter. */\n grouped?: \"first\" | \"middle\" | \"last\" | \"single\";\n /** The words, passed translated. */\n labels?: MessageBubbleLabels;\n /** Classes merged on the row. */\n className?: string;\n};\n\nconst CORNERS = {\n mine: { first: \"rounded-br-md\", middle: \"rounded-r-md\", last: \"rounded-tr-md\", single: \"\" },\n theirs: { first: \"rounded-bl-md\", middle: \"rounded-l-md\", last: \"rounded-tl-md\", single: \"\" },\n} as const;\n\n/** A message: yours in the accent on the right, theirs on the left, the time inside, the receipt under the last of yours. */\nexport function MessageBubble({ text, mine, time, status, showStatus, context, onRetry, grouped = \"single\", labels, className }: MessageBubbleProps) {\n const s = chat();\n const words = { ...STATUS, ...labels?.status };\n return (\n <div className={s.row({ className: [mine ? \"items-end\" : \"items-start\", className].filter(Boolean).join(\" \") })}>\n <p\n className={s.bubble({\n className: [mine ? \"bg-accent text-on-accent\" : \"bg-surface-2 text-fg\", CORNERS[mine ? \"mine\" : \"theirs\"][grouped], status === \"failed\" ? \"ring-2 ring-danger\" : \"\", status === \"sending\" ? \"opacity-70\" : \"\"].join(\" \"),\n })}\n >\n {context ? <span className={s.context()}>{context}</span> : null}\n {text}\n {time ? <span className={s.time()}>{time}</span> : null}\n </p>\n {showStatus && status ? (\n <p className={s.status()} data-status={status}>\n {status === \"sent\" || status === \"delivered\" || status === \"read\" ? (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" strokeWidth=\"2.2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden className={status === \"read\" ? \"stroke-accent\" : \"stroke-fg-secondary\"}>\n <path d={status === \"sent\" ? \"M5 12.5l4.5 4.5L19 7.5\" : \"M2 12.5l4.5 4.5L16 7.5M9 15l2 2 9.5-9.5\"} />\n </svg>\n ) : null}\n {words[status]}\n {status === \"failed\" && onRetry ? (\n <button type=\"button\" onClick={onRetry} className=\"ml-1 font-semibold text-fg underline underline-offset-2 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\">\n {labels?.retry ?? \"Retry\"}\n </button>\n ) : null}\n </p>\n ) : null}\n </div>\n );\n}\n\nexport type TypingIndicatorProps = {\n /** What is read and shown beside the dots (“Elena is typing”) — passed translated. */\n label: string;\n /** Show the words beside the dots. */\n showLabel?: boolean;\n /** Classes merged on the root. */\n className?: string;\n};\n\n/** Three dots that breathe while the other person types, announced politely; still under reduced motion. */\nexport function TypingIndicator({ label, showLabel, className }: TypingIndicatorProps) {\n const s = chat();\n return (\n <div role=\"status\" className={[\"flex items-center gap-2\", className].filter(Boolean).join(\" \")}>\n <style href=\"krizaka-typing\" precedence=\"default\">{\"@keyframes kz-typing{0%,60%,100%{opacity:.35;transform:none}30%{opacity:1;transform:translateY(-2px)}}\"}</style>\n <span className={s.typing()} aria-hidden>\n {[0, 1, 2].map((i) => (\n <span key={i} className={s.dot()} style={{ animationDelay: `${i * 0.18}s` }} />\n ))}\n </span>\n <span className={showLabel ? \"text-xs text-fg-secondary\" : \"sr-only\"}>{label}</span>\n </div>\n );\n}\n\nexport type MessageComposerProps = {\n /** The draft (controlled). */\n value: string;\n /** Called on every key. */\n onValueChange: (text: string) => void;\n /** Sends the draft (Enter, or the button); Shift+Enter adds a line. */\n onSend: () => void;\n /** The input's name and placeholder (“Message”). */\n placeholder: string;\n /** The send button's name (“Send”). */\n sendLabel: string;\n /** Before the input: an attach button. */\n leading?: React.ReactNode;\n /** Cannot write: the reason in its place. */\n disabledReason?: string;\n /** The longest message. Default 4000. */\n maxLength?: number;\n /** Classes merged on the root. */\n className?: string;\n};\n\n/** Where a message is written: a growing textarea (Enter sends, Shift+Enter a new line), an attach slot, a send button. */\nexport function MessageComposer({ value, onValueChange, onSend, placeholder, sendLabel, leading, disabledReason, maxLength = 4000, className }: MessageComposerProps) {\n const s = chat();\n const ready = value.trim().length > 0;\n return (\n <form\n className={s.composer({ className })}\n onSubmit={(e) => {\n e.preventDefault();\n if (ready) onSend();\n }}\n >\n {disabledReason ? (\n <p className=\"m-0 flex-1 py-2 text-sm text-fg-secondary\">{disabledReason}</p>\n ) : (\n <>\n {leading}\n <textarea\n aria-label={placeholder}\n placeholder={placeholder}\n rows={1}\n maxLength={maxLength}\n value={value}\n onChange={(e) => onValueChange(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" && !e.shiftKey && !e.nativeEvent.isComposing) {\n e.preventDefault();\n if (ready) onSend();\n }\n }}\n className={s.input()}\n style={{ fieldSizing: \"content\" } as React.CSSProperties}\n />\n <button type=\"submit\" aria-label={sendLabel} disabled={!ready} className={s.send()}>\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden>\n <path d=\"M4 12l16-8-6 16-2.5-6.5L4 12z\" />\n </svg>\n </button>\n </>\n )}\n </form>\n );\n}\n\nexport type ConversationItemProps = {\n /** The other person's name. */\n name: string;\n /** Their avatar's URL. */\n avatar?: string | null;\n /** The last message, or “typing…”. */\n preview: string;\n /** The preview is the typing signal: in the accent. */\n typing?: boolean;\n /** When (“14:02”, “Mon”). */\n time?: string;\n /** Unread messages: a count, the name in bold. */\n unread?: number;\n /** The unread count in words (“3 unread”). */\n unreadLabel?: string;\n /** Before the name: a presence dot (online). */\n online?: boolean;\n /** The presence in words (“online”). */\n onlineLabel?: string;\n /** Opens the conversation (a link). */\n href?: string;\n /** Opens the conversation (a button). */\n onSelect?: () => void;\n /** Classes merged on the row. */\n className?: string;\n};\n\n/** A conversation in the list: avatar, name, the last message or typing, the time, the unread count. */\nexport function ConversationItem({ name, avatar, preview, typing, time, unread = 0, unreadLabel, online, onlineLabel, href, onSelect, className }: ConversationItemProps) {\n const s = chat();\n const body = (\n <>\n <span className=\"relative shrink-0\">\n <Avatar src={avatar ?? null} alt=\"\" fallback={name.charAt(0)} size=\"lg\" />\n {online ? <span aria-hidden className=\"absolute bottom-0 right-0 h-3 w-3 rounded-full bg-success ring-2 ring-surface-0\" /> : null}\n </span>\n <span className=\"flex min-w-0 flex-1 flex-col\">\n <span className=\"flex items-center gap-2\">\n <span className={[\"min-w-0 flex-1 truncate text-sm text-fg\", unread > 0 ? \"font-extrabold\" : \"font-semibold\"].join(\" \")}>\n {name}\n {online && onlineLabel ? <span className=\"sr-only\">{`, ${onlineLabel}`}</span> : null}\n </span>\n {time ? <span className={[\"shrink-0 text-xs tabular-nums\", unread > 0 ? \"text-fg-accent\" : \"text-fg-secondary\"].join(\" \")}>{time}</span> : null}\n </span>\n <span className=\"flex items-center gap-2\">\n <span className={[\"min-w-0 flex-1 truncate text-xs\", typing ? \"text-fg-accent\" : unread > 0 ? \"text-fg\" : \"text-fg-secondary\"].join(\" \")}>{preview}</span>\n {unread > 0 ? (\n <span className={s.count()}>\n <span aria-hidden>{unread > 99 ? \"99+\" : unread}</span>\n <span className=\"sr-only\">{unreadLabel ?? String(unread)}</span>\n </span>\n ) : null}\n </span>\n </span>\n </>\n );\n return href ? (\n <a href={href} className={s.item({ className })}>\n {body}\n </a>\n ) : (\n <button type=\"button\" onClick={onSelect} className={s.item({ className })}>\n {body}\n </button>\n );\n}\n"
|
|
681
|
+
"content": "// Client (the composer's draft). A conversation's parts — the bubble with its time and receipt, the typing dots, the\n// composer, a conversation in a list — the same API as React Native. Words are props.\nimport * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\n\nimport { Avatar } from \"../avatar\";\n\nexport const chat = tv({\n slots: {\n row: \"flex flex-col gap-0.5\",\n bubble: \"max-w-[80%] rounded-2xl px-3 py-2 text-sm leading-snug\",\n context: \"mb-0.5 block text-xs opacity-85\",\n time: \"mt-0.5 block text-right text-[11px] tabular-nums\",\n status: \"flex items-center gap-1 text-xs text-fg-secondary data-[status=failed]:text-fg-danger [&>svg]:h-3.5 [&>svg]:w-3.5\",\n typing: \"inline-flex items-center gap-1 self-start rounded-2xl bg-surface-2 px-3.5 py-3\",\n dot: \"h-[7px] w-[7px] rounded-full bg-fg-secondary motion-safe:animate-[kz-typing_1.2s_ease-in-out_infinite]\",\n composer: \"flex items-end gap-2 border-t border-border-subtle bg-surface-0 px-3 py-2\",\n input:\n \"max-h-32 min-h-10 flex-1 resize-none rounded-2xl border border-border-default bg-surface-1 px-3.5 py-2.5 text-sm text-fg placeholder:text-fg-muted \" +\n \"focus-visible:border-accent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\",\n send:\n \"inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-accent text-on-accent hover:bg-accent-hover focus-visible:outline-hidden \" +\n \"focus-visible:ring-2 focus-visible:ring-ring disabled:bg-surface-3 disabled:text-fg-muted [&>svg]:h-[18px] [&>svg]:w-[18px]\",\n pillRoot: \"flex w-full flex-col gap-2\",\n pill:\n \"flex items-end gap-1.5 rounded-[1.75rem] border border-border-default bg-surface-1 py-1.5 pl-2 pr-1.5 transition-colors \" +\n \"focus-within:border-accent focus-within:ring-2 focus-within:ring-ring\",\n pillLeading: \"flex min-h-9 shrink-0 items-center gap-1 self-end\",\n pillInput:\n \"max-h-32 min-h-9 min-w-0 flex-1 resize-none border-0 bg-transparent px-1.5 py-[7px] text-sm leading-snug text-fg placeholder:text-fg-muted \" +\n \"focus:outline-hidden focus-visible:outline-hidden\",\n pillSend:\n \"inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-accent text-on-accent shadow-sm transition-colors hover:bg-accent-hover \" +\n \"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-surface-1 \" +\n \"disabled:cursor-not-allowed disabled:bg-surface-3 disabled:text-fg-muted disabled:shadow-none [&>svg]:h-4 [&>svg]:w-4\",\n pillStatus: \"m-0 flex min-h-5 items-center gap-1.5 px-3 text-xs text-fg-secondary [&_svg]:h-3.5 [&_svg]:w-3.5 [&_svg]:shrink-0\",\n item: \"flex w-full items-center gap-3 px-4 py-2.5 text-left hover:bg-surface-2 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring\",\n count: \"inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-accent px-1.5 text-xs font-extrabold text-on-accent\",\n },\n});\n\nexport type MessageStatus = \"sending\" | \"sent\" | \"delivered\" | \"read\" | \"failed\";\n\nexport type MessageBubbleLabels = {\n /** The words of each status, shown under the last message of yours. */\n status?: Partial<Record<MessageStatus, string>>;\n /** A failed message's retry button (“Retry”). */\n retry?: string;\n};\n\nconst STATUS: Record<MessageStatus, string> = { sending: \"Sending\", sent: \"Sent\", delivered: \"Delivered\", read: \"Read\", failed: \"Not sent\" };\n\nexport type MessageBubbleProps = {\n /** The text. */\n text: string;\n /** Yours (right, in the accent) or theirs (left). */\n mine?: boolean;\n /** The time, written by the app (“14:02”). */\n time?: string;\n /** Where a message of yours stands; shown when `showStatus`. */\n status?: MessageStatus;\n /** Show the status under the bubble (the last message of yours). */\n showStatus?: boolean;\n /** A line above the text: what it answers. */\n context?: string;\n /** Sends a failed message again (a button under it). */\n onRetry?: () => void;\n /** Part of a run of the same sender: the corner towards the next one is tighter. */\n grouped?: \"first\" | \"middle\" | \"last\" | \"single\";\n /** The words, passed translated. */\n labels?: MessageBubbleLabels;\n /** Classes merged on the row. */\n className?: string;\n};\n\nconst CORNERS = {\n mine: { first: \"rounded-br-md\", middle: \"rounded-r-md\", last: \"rounded-tr-md\", single: \"\" },\n theirs: { first: \"rounded-bl-md\", middle: \"rounded-l-md\", last: \"rounded-tl-md\", single: \"\" },\n} as const;\n\n/** A message: yours in the accent on the right, theirs on the left, the time inside, the receipt under the last of yours. */\nexport function MessageBubble({ text, mine, time, status, showStatus, context, onRetry, grouped = \"single\", labels, className }: MessageBubbleProps) {\n const s = chat();\n const words = { ...STATUS, ...labels?.status };\n return (\n <div className={s.row({ className: [mine ? \"items-end\" : \"items-start\", className].filter(Boolean).join(\" \") })}>\n <p\n className={s.bubble({\n className: [mine ? \"bg-accent text-on-accent\" : \"bg-surface-2 text-fg\", CORNERS[mine ? \"mine\" : \"theirs\"][grouped], status === \"failed\" ? \"ring-2 ring-danger\" : \"\", status === \"sending\" ? \"opacity-70\" : \"\"].join(\" \"),\n })}\n >\n {context ? <span className={s.context()}>{context}</span> : null}\n {text}\n {time ? <span className={s.time()}>{time}</span> : null}\n </p>\n {showStatus && status ? (\n <p className={s.status()} data-status={status}>\n {status === \"sent\" || status === \"delivered\" || status === \"read\" ? (\n <svg viewBox=\"0 0 24 24\" fill=\"none\" strokeWidth=\"2.2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden className={status === \"read\" ? \"stroke-accent\" : \"stroke-fg-secondary\"}>\n <path d={status === \"sent\" ? \"M5 12.5l4.5 4.5L19 7.5\" : \"M2 12.5l4.5 4.5L16 7.5M9 15l2 2 9.5-9.5\"} />\n </svg>\n ) : null}\n {words[status]}\n {status === \"failed\" && onRetry ? (\n <button type=\"button\" onClick={onRetry} className=\"ml-1 font-semibold text-fg underline underline-offset-2 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring\">\n {labels?.retry ?? \"Retry\"}\n </button>\n ) : null}\n </p>\n ) : null}\n </div>\n );\n}\n\nexport type TypingIndicatorProps = {\n /** What is read and shown beside the dots (“Elena is typing”) — passed translated. */\n label: string;\n /** Show the words beside the dots. */\n showLabel?: boolean;\n /** Classes merged on the root. */\n className?: string;\n};\n\n/** Three dots that breathe while the other person types, announced politely; still under reduced motion. */\nexport function TypingIndicator({ label, showLabel, className }: TypingIndicatorProps) {\n const s = chat();\n return (\n <div role=\"status\" className={[\"flex items-center gap-2\", className].filter(Boolean).join(\" \")}>\n <style href=\"krizaka-typing\" precedence=\"default\">{\"@keyframes kz-typing{0%,60%,100%{opacity:.35;transform:none}30%{opacity:1;transform:translateY(-2px)}}\"}</style>\n <span className={s.typing()} aria-hidden>\n {[0, 1, 2].map((i) => (\n <span key={i} className={s.dot()} style={{ animationDelay: `${i * 0.18}s` }} />\n ))}\n </span>\n <span className={showLabel ? \"text-xs text-fg-secondary\" : \"sr-only\"}>{label}</span>\n </div>\n );\n}\n\nexport type MessageComposerProps = {\n /** The draft (controlled). */\n value: string;\n /** Called on every key. */\n onValueChange: (text: string) => void;\n /** Sends the draft (Enter, or the button); Shift+Enter adds a line. */\n onSend: () => void;\n /** The input's name and placeholder (“Message”). */\n placeholder: string;\n /** The send button's name (“Send”). */\n sendLabel: string;\n /** Before the input: an attach button (`bar`), or the mode/model chip and attach, inside the field (`pill`). */\n leading?: React.ReactNode;\n /** Cannot write: the reason in its place. */\n disabledReason?: string;\n /** The longest message. Default 4000. */\n maxLength?: number;\n /** `bar`: a docked bar with a square-ish field. `pill`: one rounded field holding `leading`, the text and a round send button — no inner border. Default `bar`. */\n variant?: \"bar\" | \"pill\";\n /** `pill`: the quiet line under the field (privacy, cost estimate). Its row is always reserved, so a changing line never moves the field. Read politely. */\n status?: React.ReactNode;\n /** An answer is on its way: the send is held (and `aria-busy` is set) while the field stays where it is, size included. */\n busy?: boolean;\n /** Overrides “the draft has text” — e.g. an attachment alone can be sent. A busy composer never sends. */\n canSend?: boolean;\n /** Attributes (`data-testid`, `autoFocus`, `id`…) forwarded to the textarea. */\n inputProps?: Omit<React.ComponentPropsWithoutRef<\"textarea\">, \"value\" | \"onChange\" | \"placeholder\" | \"maxLength\" | \"className\" | \"rows\">;\n /** Attributes forwarded to the send button. */\n sendProps?: Omit<React.ComponentPropsWithoutRef<\"button\">, \"type\" | \"disabled\" | \"className\" | \"aria-label\">;\n /** Classes merged on the root. */\n className?: string;\n};\n\n/** Where a message is written: a growing textarea (Enter sends, Shift+Enter a new line), an attach slot, a send button. The `pill` variant is the one-field look of the Krizaka chat card. */\nexport function MessageComposer({\n value,\n onValueChange,\n onSend,\n placeholder,\n sendLabel,\n leading,\n disabledReason,\n maxLength = 4000,\n variant = \"bar\",\n status,\n busy = false,\n canSend,\n inputProps,\n sendProps,\n className,\n}: MessageComposerProps) {\n const s = chat();\n const pill = variant === \"pill\";\n const ready = !busy && (canSend ?? value.trim().length > 0);\n const textarea = (\n <textarea\n aria-label={placeholder}\n placeholder={placeholder}\n rows={1}\n maxLength={maxLength}\n {...inputProps}\n value={value}\n onChange={(e) => onValueChange(e.target.value)}\n onKeyDown={(e) => {\n inputProps?.onKeyDown?.(e);\n if (e.defaultPrevented) return;\n if (e.key === \"Enter\" && !e.shiftKey && !e.nativeEvent.isComposing) {\n e.preventDefault();\n if (ready) onSend();\n }\n }}\n className={pill ? s.pillInput() : s.input()}\n style={{ fieldSizing: \"content\" } as React.CSSProperties}\n />\n );\n const send = (\n <button type=\"submit\" aria-label={sendLabel} {...sendProps} disabled={!ready} className={pill ? s.pillSend() : s.send()}>\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden>\n <path d=\"M4 12l16-8-6 16-2.5-6.5L4 12z\" />\n </svg>\n </button>\n );\n const submit = (e: React.FormEvent) => {\n e.preventDefault();\n if (ready) onSend();\n };\n if (pill) {\n return (\n <form className={s.pillRoot({ className })} onSubmit={submit} aria-busy={busy || undefined}>\n {disabledReason ? (\n <p className=\"m-0 rounded-[1.75rem] border border-border-default bg-surface-1 px-4 py-2.5 text-sm text-fg-secondary\">{disabledReason}</p>\n ) : (\n <div className={s.pill()}>\n {leading ? <div className={s.pillLeading()}>{leading}</div> : null}\n {textarea}\n {send}\n </div>\n )}\n <p role=\"status\" aria-live=\"polite\" className={s.pillStatus()}>\n {status}\n </p>\n </form>\n );\n }\n return (\n <form className={s.composer({ className })} onSubmit={submit} aria-busy={busy || undefined}>\n {disabledReason ? (\n <p className=\"m-0 flex-1 py-2 text-sm text-fg-secondary\">{disabledReason}</p>\n ) : (\n <>\n {leading}\n {textarea}\n {send}\n </>\n )}\n </form>\n );\n}\n\nexport type ConversationItemProps = {\n /** The other person's name. */\n name: string;\n /** Their avatar's URL. */\n avatar?: string | null;\n /** The last message, or “typing…”. */\n preview: string;\n /** The preview is the typing signal: in the accent. */\n typing?: boolean;\n /** When (“14:02”, “Mon”). */\n time?: string;\n /** Unread messages: a count, the name in bold. */\n unread?: number;\n /** The unread count in words (“3 unread”). */\n unreadLabel?: string;\n /** Before the name: a presence dot (online). */\n online?: boolean;\n /** The presence in words (“online”). */\n onlineLabel?: string;\n /** Opens the conversation (a link). */\n href?: string;\n /** Opens the conversation (a button). */\n onSelect?: () => void;\n /** Classes merged on the row. */\n className?: string;\n};\n\n/** A conversation in the list: avatar, name, the last message or typing, the time, the unread count. */\nexport function ConversationItem({ name, avatar, preview, typing, time, unread = 0, unreadLabel, online, onlineLabel, href, onSelect, className }: ConversationItemProps) {\n const s = chat();\n const body = (\n <>\n <span className=\"relative shrink-0\">\n <Avatar src={avatar ?? null} alt=\"\" fallback={name.charAt(0)} size=\"lg\" />\n {online ? <span aria-hidden className=\"absolute bottom-0 right-0 h-3 w-3 rounded-full bg-success ring-2 ring-surface-0\" /> : null}\n </span>\n <span className=\"flex min-w-0 flex-1 flex-col\">\n <span className=\"flex items-center gap-2\">\n <span className={[\"min-w-0 flex-1 truncate text-sm text-fg\", unread > 0 ? \"font-extrabold\" : \"font-semibold\"].join(\" \")}>\n {name}\n {online && onlineLabel ? <span className=\"sr-only\">{`, ${onlineLabel}`}</span> : null}\n </span>\n {time ? <span className={[\"shrink-0 text-xs tabular-nums\", unread > 0 ? \"text-fg-accent\" : \"text-fg-secondary\"].join(\" \")}>{time}</span> : null}\n </span>\n <span className=\"flex items-center gap-2\">\n <span className={[\"min-w-0 flex-1 truncate text-xs\", typing ? \"text-fg-accent\" : unread > 0 ? \"text-fg\" : \"text-fg-secondary\"].join(\" \")}>{preview}</span>\n {unread > 0 ? (\n <span className={s.count()}>\n <span aria-hidden>{unread > 99 ? \"99+\" : unread}</span>\n <span className=\"sr-only\">{unreadLabel ?? String(unread)}</span>\n </span>\n ) : null}\n </span>\n </span>\n </>\n );\n return href ? (\n <a href={href} className={s.item({ className })}>\n {body}\n </a>\n ) : (\n <button type=\"button\" onClick={onSelect} className={s.item({ className })}>\n {body}\n </button>\n );\n}\n"
|
|
630
682
|
},
|
|
631
683
|
{
|
|
632
684
|
"path": "chat/index.ts",
|
|
@@ -634,7 +686,7 @@
|
|
|
634
686
|
},
|
|
635
687
|
{
|
|
636
688
|
"path": "chat/meta.ts",
|
|
637
|
-
"content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Chat\",\n summary: \"A conversation's parts — `MessageBubble` (time, receipt, retry), `TypingIndicator`, `MessageComposer`, `ConversationItem` — the same API on the web and React Native.\",\n status: \"beta\",\n category: \"data-display\",\n platforms: \"both\",\n whenToUse: [\"Orochia's Messages, a dating app's chats, support threads.\", \"An inbox of conversations with presence and unread counts.\"],\n whenNotToUse: [\n { when: \"For a comments thread under a post.\", use: \"list\" },\n { when: \"For notifications.\", use: \"toast\" },\n ],\n bestPractices: [\"Group the runs of one sender (`grouped`); show the receipt under the last message of yours only.\", \"Keep the server's order and merge (the app's), the bubble only draws.\", \"The composer never clears itself: clear the draft once the message is sent.\"],\n accessibility: {\n keyboard: [{ keys: \"Enter / Shift+Enter\", action: \"In the web composer: send, or a new line.\" }],\n notes: [\"Each bubble is read with its time; a failed one says so and offers a retry.\", \"The typing dots are announced politely by their words.\", \"A conversation row reads name, presence, last message, time and unread count.\"],\n },\n related: [\"avatar\", \"list\", \"toast\"],\n web: {\n imports: [\"MessageBubble\", \"TypingIndicator\", \"MessageComposer\", \"ConversationItem\"],\n examples: [\n { name: \"conversation\", title: \"Conversation\", description: \"Grouped runs, a read receipt, typing, the composer.\" },\n { name: \"inbox\", title: \"Inbox\", description: \"Online, typing, unread counts.\" },\n ],\n },\n native: {\n imports: [\"MessageBubble\", \"TypingIndicator\", \"MessageComposer\", \"ConversationItem\"],\n differences: [\"The composer keeps clear of the home indicator; the send button plays a light tap.\", \"A failed bubble is itself the retry button.\", \"`ConversationItem` takes `presence` (a dot on the avatar) instead of `online`.\"],\n examples: [\n { name: \"conversation\", title: \"Conversation\", description: \"Grouped runs, a read receipt, typing, the composer.\" },\n { name: \"inbox\", title: \"Inbox\", description: \"Presence, typing, unread counts.\" },\n ],\n },\n} satisfies ComponentMeta;\n"
|
|
689
|
+
"content": "import type { ComponentMeta } from \"../meta\";\n\nexport const meta = {\n title: \"Chat\",\n summary: \"A conversation's parts — `MessageBubble` (time, receipt, retry), `TypingIndicator`, `MessageComposer`, `ConversationItem` — the same API on the web and React Native.\",\n status: \"beta\",\n category: \"data-display\",\n platforms: \"both\",\n whenToUse: [\"Orochia's Messages, a dating app's chats, support threads.\", \"An inbox of conversations with presence and unread counts.\"],\n whenNotToUse: [\n { when: \"For a comments thread under a post.\", use: \"list\" },\n { when: \"For notifications.\", use: \"toast\" },\n ],\n bestPractices: [\"Group the runs of one sender (`grouped`); show the receipt under the last message of yours only.\", \"Keep the server's order and merge (the app's), the bubble only draws.\", \"The composer never clears itself: clear the draft once the message is sent.\", \"`variant=\\\"pill\\\"` for a chat page: put the mode/model chip and attach in `leading`, privacy and cost in `status`, and pass `busy` while an answer streams — the field's box does not move.\"],\n accessibility: {\n keyboard: [{ keys: \"Enter / Shift+Enter\", action: \"In the web composer: send, or a new line.\" }],\n notes: [\"Each bubble is read with its time; a failed one says so and offers a retry.\", \"The `pill` status line is a polite live region; a busy composer holds the send (`aria-busy`) and says nothing else.\", \"The typing dots are announced politely by their words.\", \"A conversation row reads name, presence, last message, time and unread count.\"],\n },\n related: [\"avatar\", \"list\", \"toast\"],\n web: {\n imports: [\"MessageBubble\", \"TypingIndicator\", \"MessageComposer\", \"ConversationItem\"],\n examples: [\n { name: \"conversation\", title: \"Conversation\", description: \"Grouped runs, a read receipt, typing, the composer.\" },\n { name: \"inbox\", title: \"Inbox\", description: \"Online, typing, unread counts.\" },\n { name: \"pill-composer\", title: \"Pill composer\", description: \"One rounded field: chip and attach inside, round send inside, a status line under it.\" },\n ],\n },\n native: {\n imports: [\"MessageBubble\", \"TypingIndicator\", \"MessageComposer\", \"ConversationItem\"],\n differences: [\"The composer keeps clear of the home indicator; the send button plays a light tap.\", \"A failed bubble is itself the retry button.\", \"`ConversationItem` takes `presence` (a dot on the avatar) instead of `online`.\"],\n examples: [\n { name: \"conversation\", title: \"Conversation\", description: \"Grouped runs, a read receipt, typing, the composer.\" },\n { name: \"inbox\", title: \"Inbox\", description: \"Presence, typing, unread counts.\" },\n ],\n },\n} satisfies ComponentMeta;\n"
|
|
638
690
|
},
|
|
639
691
|
{
|
|
640
692
|
"path": "meta.ts",
|
|
@@ -820,7 +872,7 @@
|
|
|
820
872
|
},
|
|
821
873
|
{
|
|
822
874
|
"component": "MessageComposer",
|
|
823
|
-
"description": "Where a message is written: a growing textarea (Enter sends, Shift+Enter a new line), an attach slot, a send button.",
|
|
875
|
+
"description": "Where a message is written: a growing textarea (Enter sends, Shift+Enter a new line), an attach slot, a send button. The `pill` variant is the one-field look of the Krizaka chat card.",
|
|
824
876
|
"props": [
|
|
825
877
|
{
|
|
826
878
|
"name": "onSend",
|
|
@@ -857,6 +909,20 @@
|
|
|
857
909
|
"description": "The draft (controlled).",
|
|
858
910
|
"required": true
|
|
859
911
|
},
|
|
912
|
+
{
|
|
913
|
+
"name": "busy",
|
|
914
|
+
"type": "boolean",
|
|
915
|
+
"default": "false",
|
|
916
|
+
"description": "An answer is on its way: the send is held (and `aria-busy` is set) while the field stays where it is, size included.",
|
|
917
|
+
"required": false
|
|
918
|
+
},
|
|
919
|
+
{
|
|
920
|
+
"name": "canSend",
|
|
921
|
+
"type": "boolean",
|
|
922
|
+
"default": null,
|
|
923
|
+
"description": "Overrides “the draft has text” — e.g. an attachment alone can be sent. A busy composer never sends.",
|
|
924
|
+
"required": false
|
|
925
|
+
},
|
|
860
926
|
{
|
|
861
927
|
"name": "className",
|
|
862
928
|
"type": "string",
|
|
@@ -871,11 +937,18 @@
|
|
|
871
937
|
"description": "Cannot write: the reason in its place.",
|
|
872
938
|
"required": false
|
|
873
939
|
},
|
|
940
|
+
{
|
|
941
|
+
"name": "inputProps",
|
|
942
|
+
"type": "Omit<Omit<DetailedHTMLProps<TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>, \"ref\">, \"className\" | ... 4 more ... | \"rows\">",
|
|
943
|
+
"default": null,
|
|
944
|
+
"description": "Attributes (`data-testid`, `autoFocus`, `id`…) forwarded to the textarea.",
|
|
945
|
+
"required": false
|
|
946
|
+
},
|
|
874
947
|
{
|
|
875
948
|
"name": "leading",
|
|
876
949
|
"type": "ReactNode",
|
|
877
950
|
"default": null,
|
|
878
|
-
"description": "Before the input: an attach button.",
|
|
951
|
+
"description": "Before the input: an attach button (`bar`), or the mode/model chip and attach, inside the field (`pill`).",
|
|
879
952
|
"required": false
|
|
880
953
|
},
|
|
881
954
|
{
|
|
@@ -884,6 +957,27 @@
|
|
|
884
957
|
"default": "4000",
|
|
885
958
|
"description": "The longest message. Default 4000.",
|
|
886
959
|
"required": false
|
|
960
|
+
},
|
|
961
|
+
{
|
|
962
|
+
"name": "sendProps",
|
|
963
|
+
"type": "Omit<Omit<DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, \"ref\">, \"className\" | ... 2 more ... | \"disabled\">",
|
|
964
|
+
"default": null,
|
|
965
|
+
"description": "Attributes forwarded to the send button.",
|
|
966
|
+
"required": false
|
|
967
|
+
},
|
|
968
|
+
{
|
|
969
|
+
"name": "status",
|
|
970
|
+
"type": "ReactNode",
|
|
971
|
+
"default": null,
|
|
972
|
+
"description": "`pill`: the quiet line under the field (privacy, cost estimate). Its row is always reserved, so a changing line never moves the field. Read politely.",
|
|
973
|
+
"required": false
|
|
974
|
+
},
|
|
975
|
+
{
|
|
976
|
+
"name": "variant",
|
|
977
|
+
"type": "\"bar\" | \"pill\"",
|
|
978
|
+
"default": "bar",
|
|
979
|
+
"description": "`bar`: a docked bar with a square-ish field. `pill`: one rounded field holding `leading`, the text and a round send button — no inner border. Default `bar`.",
|
|
980
|
+
"required": false
|
|
887
981
|
}
|
|
888
982
|
]
|
|
889
983
|
},
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// registry/examples/card-grid/foundation.tsx
|
|
4
|
+
import { ProductLogo } from "@krizaka/ui";
|
|
5
|
+
import { ArgumentCard } from "@krizaka/ui/argument-card";
|
|
6
|
+
import { CardGrid } from "@krizaka/ui/card-grid";
|
|
7
|
+
import { LinkCard } from "@krizaka/ui/link-card";
|
|
8
|
+
import { jsx } from "react/jsx-runtime";
|
|
9
|
+
var products = [
|
|
10
|
+
{ id: "orochia", name: "Orochia", hook: "Recommendations, search and captions." },
|
|
11
|
+
{ id: "yomizaka", name: "Yomizaka", hook: "Document checks and signed reports." },
|
|
12
|
+
{ id: "kachizaka", name: "Kachizaka", hook: "Live match feeds." }
|
|
13
|
+
];
|
|
14
|
+
function CardGridFoundation() {
|
|
15
|
+
return /* @__PURE__ */ jsx(
|
|
16
|
+
CardGrid,
|
|
17
|
+
{
|
|
18
|
+
className: "w-full max-w-5xl",
|
|
19
|
+
min: 240,
|
|
20
|
+
reveal: false,
|
|
21
|
+
foundationLabel: "Powered by Orazaka",
|
|
22
|
+
foundation: /* @__PURE__ */ jsx(
|
|
23
|
+
LinkCard,
|
|
24
|
+
{
|
|
25
|
+
href: "#orazaka",
|
|
26
|
+
icon: /* @__PURE__ */ jsx(ProductLogo, { id: "orazaka", size: 28, animated: false }),
|
|
27
|
+
title: "The engine",
|
|
28
|
+
description: "Every product's AI runs on Orazaka: one pipeline, your models, your network.",
|
|
29
|
+
cta: "Open the engine"
|
|
30
|
+
}
|
|
31
|
+
),
|
|
32
|
+
children: products.map((p) => /* @__PURE__ */ jsx(ArgumentCard, { id: `product-${p.id}`, icon: /* @__PURE__ */ jsx(ProductLogo, { id: p.id, size: 32, animated: false }), title: p.name, hook: { label: "Uses it for", text: p.hook }, more: { href: `#${p.id}`, label: "Details" } }, p.id))
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
export {
|
|
37
|
+
CardGridFoundation as default
|
|
38
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ProductLogo } from "@krizaka/ui";
|
|
2
|
+
import { ArgumentCard } from "@krizaka/ui/argument-card";
|
|
3
|
+
import { CardGrid } from "@krizaka/ui/card-grid";
|
|
4
|
+
import { LinkCard } from "@krizaka/ui/link-card";
|
|
5
|
+
|
|
6
|
+
const products = [
|
|
7
|
+
{ id: "orochia", name: "Orochia", hook: "Recommendations, search and captions." },
|
|
8
|
+
{ id: "yomizaka", name: "Yomizaka", hook: "Document checks and signed reports." },
|
|
9
|
+
{ id: "kachizaka", name: "Kachizaka", hook: "Live match feeds." },
|
|
10
|
+
] as const;
|
|
11
|
+
|
|
12
|
+
// `foundation`: one wide card that the cards below stand on, a hairline stem to a frame around them. One column on a phone.
|
|
13
|
+
export default function CardGridFoundation() {
|
|
14
|
+
return (
|
|
15
|
+
<CardGrid
|
|
16
|
+
className="w-full max-w-5xl"
|
|
17
|
+
min={240}
|
|
18
|
+
reveal={false}
|
|
19
|
+
foundationLabel="Powered by Orazaka"
|
|
20
|
+
foundation={
|
|
21
|
+
<LinkCard
|
|
22
|
+
href="#orazaka"
|
|
23
|
+
icon={<ProductLogo id="orazaka" size={28} animated={false} />}
|
|
24
|
+
title="The engine"
|
|
25
|
+
description="Every product's AI runs on Orazaka: one pipeline, your models, your network."
|
|
26
|
+
cta="Open the engine"
|
|
27
|
+
/>
|
|
28
|
+
}
|
|
29
|
+
>
|
|
30
|
+
{products.map((p) => (
|
|
31
|
+
<ArgumentCard key={p.id} id={`product-${p.id}`} icon={<ProductLogo id={p.id} size={32} animated={false} />} title={p.name} hook={{ label: "Uses it for", text: p.hook }} more={{ href: `#${p.id}`, label: "Details" }} />
|
|
32
|
+
))}
|
|
33
|
+
</CardGrid>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// registry/examples/chat/pill-composer.tsx
|
|
4
|
+
import { MessageComposer } from "@krizaka/ui/chat";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
function ChatPillComposer() {
|
|
8
|
+
const [draft, setDraft] = useState("");
|
|
9
|
+
const [busy, setBusy] = useState(false);
|
|
10
|
+
return /* @__PURE__ */ jsx("div", { className: "mx-auto w-full max-w-xl", children: /* @__PURE__ */ jsx(
|
|
11
|
+
MessageComposer,
|
|
12
|
+
{
|
|
13
|
+
variant: "pill",
|
|
14
|
+
value: draft,
|
|
15
|
+
busy,
|
|
16
|
+
onValueChange: setDraft,
|
|
17
|
+
onSend: () => {
|
|
18
|
+
setDraft("");
|
|
19
|
+
setBusy(true);
|
|
20
|
+
setTimeout(() => setBusy(false), 1500);
|
|
21
|
+
},
|
|
22
|
+
placeholder: "Ask your private AI\u2026",
|
|
23
|
+
sendLabel: "Send",
|
|
24
|
+
leading: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
25
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "h-8 rounded-full border border-border-default px-3 text-xs font-medium text-fg-secondary", children: "Chat" }),
|
|
26
|
+
/* @__PURE__ */ jsx("button", { type: "button", "aria-label": "Attach a file", className: "grid h-8 w-8 place-items-center rounded-full text-fg-secondary hover:bg-surface-2", children: "+" })
|
|
27
|
+
] }),
|
|
28
|
+
status: busy ? "Answering\u2026" : "Runs locally \xB7 nothing leaves your network \xB7 ~30 credits"
|
|
29
|
+
}
|
|
30
|
+
) });
|
|
31
|
+
}
|
|
32
|
+
export {
|
|
33
|
+
ChatPillComposer as default
|
|
34
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { MessageComposer } from "@krizaka/ui/chat";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
|
|
6
|
+
// The chat card's composer: one rounded field — the mode chip and attach inside on the left, the round send inside on
|
|
7
|
+
// the right — and a quiet status line under it (privacy, cost). The field keeps its box while an answer streams.
|
|
8
|
+
export default function ChatPillComposer() {
|
|
9
|
+
const [draft, setDraft] = useState("");
|
|
10
|
+
const [busy, setBusy] = useState(false);
|
|
11
|
+
return (
|
|
12
|
+
<div className="mx-auto w-full max-w-xl">
|
|
13
|
+
<MessageComposer
|
|
14
|
+
variant="pill"
|
|
15
|
+
value={draft}
|
|
16
|
+
busy={busy}
|
|
17
|
+
onValueChange={setDraft}
|
|
18
|
+
onSend={() => {
|
|
19
|
+
setDraft("");
|
|
20
|
+
setBusy(true);
|
|
21
|
+
setTimeout(() => setBusy(false), 1500);
|
|
22
|
+
}}
|
|
23
|
+
placeholder="Ask your private AI…"
|
|
24
|
+
sendLabel="Send"
|
|
25
|
+
leading={
|
|
26
|
+
<>
|
|
27
|
+
<button type="button" className="h-8 rounded-full border border-border-default px-3 text-xs font-medium text-fg-secondary">
|
|
28
|
+
Chat
|
|
29
|
+
</button>
|
|
30
|
+
<button type="button" aria-label="Attach a file" className="grid h-8 w-8 place-items-center rounded-full text-fg-secondary hover:bg-surface-2">
|
|
31
|
+
+
|
|
32
|
+
</button>
|
|
33
|
+
</>
|
|
34
|
+
}
|
|
35
|
+
status={busy ? "Answering…" : "Runs locally · nothing leaves your network · ~30 credits"}
|
|
36
|
+
/>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
package/registry/index.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@krizaka/ui",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.19.0",
|
|
4
4
|
"templatePurposes": {
|
|
5
5
|
"convince": {
|
|
6
6
|
"title": "Convince",
|
|
@@ -683,7 +683,7 @@
|
|
|
683
683
|
"category": "blocks",
|
|
684
684
|
"platforms": "web",
|
|
685
685
|
"examples": {
|
|
686
|
-
"web":
|
|
686
|
+
"web": 4,
|
|
687
687
|
"native": 0
|
|
688
688
|
},
|
|
689
689
|
"dependencies": [],
|
|
@@ -740,7 +740,7 @@
|
|
|
740
740
|
"category": "data-display",
|
|
741
741
|
"platforms": "both",
|
|
742
742
|
"examples": {
|
|
743
|
-
"web":
|
|
743
|
+
"web": 3,
|
|
744
744
|
"native": 2
|
|
745
745
|
},
|
|
746
746
|
"dependencies": [
|
|
@@ -1750,7 +1750,7 @@
|
|
|
1750
1750
|
"native": 2
|
|
1751
1751
|
},
|
|
1752
1752
|
"dependencies": [
|
|
1753
|
-
"@krizaka/tokens@^3.
|
|
1753
|
+
"@krizaka/tokens@^3.19.0",
|
|
1754
1754
|
"@protomaps/basemaps@5.7.2",
|
|
1755
1755
|
"tailwind-variants@^3.3.1"
|
|
1756
1756
|
],
|
|
@@ -1978,7 +1978,7 @@
|
|
|
1978
1978
|
"native": 0
|
|
1979
1979
|
},
|
|
1980
1980
|
"dependencies": [
|
|
1981
|
-
"@krizaka/tokens@^3.
|
|
1981
|
+
"@krizaka/tokens@^3.19.0",
|
|
1982
1982
|
"radix-ui@^1.7.0",
|
|
1983
1983
|
"tailwind-variants@^3.3.1"
|
|
1984
1984
|
],
|
|
@@ -2494,7 +2494,7 @@
|
|
|
2494
2494
|
"native": 0
|
|
2495
2495
|
},
|
|
2496
2496
|
"dependencies": [
|
|
2497
|
-
"@krizaka/tokens@^3.
|
|
2497
|
+
"@krizaka/tokens@^3.19.0",
|
|
2498
2498
|
"radix-ui@^1.7.0",
|
|
2499
2499
|
"tailwind-variants@^3.3.1"
|
|
2500
2500
|
],
|
package/registry/map.json
CHANGED
package/registry/nav-bar.json
CHANGED