tempest-react-sdk 0.29.1 → 0.31.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/components/Chat/Chat.cjs +2 -0
- package/dist/components/Chat/Chat.cjs.map +1 -0
- package/dist/components/Chat/Chat.js +126 -0
- package/dist/components/Chat/Chat.js.map +1 -0
- package/dist/components/Chat/Chat.module.cjs +2 -0
- package/dist/components/Chat/Chat.module.cjs.map +1 -0
- package/dist/components/Chat/Chat.module.js +32 -0
- package/dist/components/Chat/Chat.module.js.map +1 -0
- package/dist/components/Chat/ChatComposer.cjs +2 -0
- package/dist/components/Chat/ChatComposer.cjs.map +1 -0
- package/dist/components/Chat/ChatComposer.js +67 -0
- package/dist/components/Chat/ChatComposer.js.map +1 -0
- package/dist/components/Chat/chat-groups.cjs +2 -0
- package/dist/components/Chat/chat-groups.cjs.map +1 -0
- package/dist/components/Chat/chat-groups.js +89 -0
- package/dist/components/Chat/chat-groups.js.map +1 -0
- package/dist/components/FilterBar/FilterBar.cjs +2 -0
- package/dist/components/FilterBar/FilterBar.cjs.map +1 -0
- package/dist/components/FilterBar/FilterBar.js +238 -0
- package/dist/components/FilterBar/FilterBar.js.map +1 -0
- package/dist/components/FilterBar/FilterBar.module.cjs +2 -0
- package/dist/components/FilterBar/FilterBar.module.cjs.map +1 -0
- package/dist/components/FilterBar/FilterBar.module.js +20 -0
- package/dist/components/FilterBar/FilterBar.module.js.map +1 -0
- package/dist/components/FilterBar/filter-model.cjs +2 -0
- package/dist/components/FilterBar/filter-model.cjs.map +1 -0
- package/dist/components/FilterBar/filter-model.js +167 -0
- package/dist/components/FilterBar/filter-model.js.map +1 -0
- package/dist/components/Markdown/Markdown.cjs +2 -0
- package/dist/components/Markdown/Markdown.cjs.map +1 -0
- package/dist/components/Markdown/Markdown.js +124 -0
- package/dist/components/Markdown/Markdown.js.map +1 -0
- package/dist/components/Markdown/Markdown.module.cjs +2 -0
- package/dist/components/Markdown/Markdown.module.cjs.map +1 -0
- package/dist/components/Markdown/Markdown.module.js +23 -0
- package/dist/components/Markdown/Markdown.module.js.map +1 -0
- package/dist/components/Markdown/markdown-parse.cjs +7 -0
- package/dist/components/Markdown/markdown-parse.cjs.map +1 -0
- package/dist/components/Markdown/markdown-parse.js +269 -0
- package/dist/components/Markdown/markdown-parse.js.map +1 -0
- package/dist/components/Markdown/markdown-url.cjs +2 -0
- package/dist/components/Markdown/markdown-url.cjs.map +1 -0
- package/dist/components/Markdown/markdown-url.js +28 -0
- package/dist/components/Markdown/markdown-url.js.map +1 -0
- package/dist/components/Masonry/Masonry.cjs +2 -0
- package/dist/components/Masonry/Masonry.cjs.map +1 -0
- package/dist/components/Masonry/Masonry.js +70 -0
- package/dist/components/Masonry/Masonry.js.map +1 -0
- package/dist/components/Masonry/Masonry.module.cjs +2 -0
- package/dist/components/Masonry/Masonry.module.cjs.map +1 -0
- package/dist/components/Masonry/Masonry.module.js +10 -0
- package/dist/components/Masonry/Masonry.module.js.map +1 -0
- package/dist/components/Masonry/masonry-layout.cjs +2 -0
- package/dist/components/Masonry/masonry-layout.cjs.map +1 -0
- package/dist/components/Masonry/masonry-layout.js +20 -0
- package/dist/components/Masonry/masonry-layout.js.map +1 -0
- package/dist/components/Tour/Tour.cjs +2 -0
- package/dist/components/Tour/Tour.cjs.map +1 -0
- package/dist/components/Tour/Tour.js +186 -0
- package/dist/components/Tour/Tour.js.map +1 -0
- package/dist/components/Tour/Tour.module.cjs +2 -0
- package/dist/components/Tour/Tour.module.cjs.map +1 -0
- package/dist/components/Tour/Tour.module.js +21 -0
- package/dist/components/Tour/Tour.module.js.map +1 -0
- package/dist/components/Tour/tour-position.cjs +2 -0
- package/dist/components/Tour/tour-position.cjs.map +1 -0
- package/dist/components/Tour/tour-position.js +105 -0
- package/dist/components/Tour/tour-position.js.map +1 -0
- package/dist/components/Transfer/Transfer.cjs +2 -0
- package/dist/components/Transfer/Transfer.cjs.map +1 -0
- package/dist/components/Transfer/Transfer.js +163 -0
- package/dist/components/Transfer/Transfer.js.map +1 -0
- package/dist/components/Transfer/Transfer.module.cjs +2 -0
- package/dist/components/Transfer/Transfer.module.cjs.map +1 -0
- package/dist/components/Transfer/Transfer.module.js +19 -0
- package/dist/components/Transfer/Transfer.module.js.map +1 -0
- package/dist/components/Transfer/transfer-state.cjs +2 -0
- package/dist/components/Transfer/transfer-state.cjs.map +1 -0
- package/dist/components/Transfer/transfer-state.js +59 -0
- package/dist/components/Transfer/transfer-state.js.map +1 -0
- package/dist/styles.css +1 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +674 -0
- package/dist/tempest-react-sdk.js +117 -105
- package/package.json +1 -1
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("../../utils/cn.cjs"),t=require("../EmptyState/EmptyState.cjs"),n=require("../VisuallyHidden/VisuallyHidden.cjs"),r=require("./chat-groups.cjs"),i=require("./Chat.module.cjs"),a=require("./ChatComposer.cjs");let o=require("react"),s=require("react/jsx-runtime");var c={sending:`◌`,sent:`✓`,read:`✓✓`,failed:`!`};function l({messages:l,currentUserId:u,onSend:d,onRetry:f,typing:p=[],renderAvatar:m,header:h,emptyState:g,groupWindowMs:_,locale:v=`pt-BR`,now:y,placeholder:b,composerActions:x,composerDisabled:S,onSendError:C,className:w,...T}){let E=r.chatStrings(v),D=r.groupMessages({messages:l,currentUserId:u,windowMs:_}),O=(0,o.useRef)(null),k=(0,o.useRef)(null),A=(0,o.useRef)(!0),j=()=>{let e=k.current;if(!e)return;let t=e.scrollHeight-e.scrollTop-e.clientHeight;A.current=t<48};(0,o.useLayoutEffect)(()=>{let e=k.current;!e||!A.current||(e.scrollTop=e.scrollHeight)},[l,p]),(0,o.useEffect)(()=>{let e=k.current;e&&(e.scrollTop=e.scrollHeight)},[]);let M=r.typingLabel(p,v);return(0,s.jsxs)(`div`,{className:e.cn(i.default.panel,w),...T,children:[h&&(0,s.jsx)(`header`,{className:i.default.header,children:h}),(0,s.jsx)(`div`,{ref:k,className:i.default.thread,onScroll:j,role:`log`,"aria-live":`polite`,"aria-relevant":`additions text`,tabIndex:0,"aria-label":E.thread,children:l.length===0?g??(0,s.jsx)(t.EmptyState,{title:E.empty}):D.map(t=>t.kind===`day`?(0,s.jsx)(`div`,{className:i.default.day,children:(0,s.jsx)(`span`,{className:i.default.dayLabel,children:r.dayLabel(t.date,{locale:v,now:y})})},t.key):(0,s.jsxs)(`div`,{className:e.cn(i.default.run,t.own&&i.default.ownRun),children:[m&&(0,s.jsx)(`div`,{className:i.default.avatar,"aria-hidden":`true`,children:m(t.messages[0])}),(0,s.jsxs)(`div`,{className:i.default.runBody,children:[(0,s.jsx)(`span`,{className:i.default.author,children:t.own?E.you:t.authorName??t.authorId}),(0,s.jsx)(`ul`,{className:i.default.bubbles,children:t.messages.map(a=>(0,s.jsxs)(`li`,{className:i.default.bubbleRow,children:[(0,s.jsxs)(`div`,{className:e.cn(i.default.bubble,t.own&&i.default.ownBubble,a.status===`failed`&&i.default.failedBubble),children:[(0,s.jsx)(`div`,{className:i.default.body,children:a.body}),(0,s.jsxs)(`div`,{className:i.default.meta,children:[(0,s.jsx)(`time`,{dateTime:new Date(a.sentAt).toISOString(),children:r.timeLabel(a.sentAt,v)}),t.own&&a.status&&(0,s.jsxs)(`span`,{className:e.cn(i.default.status,a.status===`failed`&&i.default.statusFailed),title:E[a.status],children:[(0,s.jsx)(`span`,{"aria-hidden":`true`,children:c[a.status]}),(0,s.jsx)(n.VisuallyHidden,{children:E[a.status]})]})]})]}),a.status===`failed`&&f&&(0,s.jsx)(`button`,{type:`button`,className:i.default.retry,onClick:()=>f(a),children:E.retry})]},a.id))})]})]},t.key))}),M&&(0,s.jsx)(`p`,{className:i.default.typing,"aria-live":`polite`,children:M}),d&&(0,s.jsx)(a.ChatComposer,{ref:O,onSend:d,locale:v,placeholder:b,actions:x,disabled:S,onError:C})]})}exports.Chat=l;
|
|
2
|
+
//# sourceMappingURL=Chat.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Chat.cjs","names":[],"sources":["../../../src/components/Chat/Chat.tsx"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { EmptyState } from \"../EmptyState\";\nimport { VisuallyHidden } from \"../VisuallyHidden\";\nimport { ChatComposer, type ChatComposerHandle } from \"./ChatComposer\";\nimport {\n chatStrings,\n dayLabel,\n groupMessages,\n timeLabel,\n typingLabel,\n type ChatMessage,\n} from \"./chat-groups\";\nimport styles from \"./Chat.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onSubmit\";\n\nexport interface ChatProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /** The thread, **oldest first**. Never reordered by the component. */\n messages: readonly ChatMessage[];\n /** Author id treated as \"own\" — decides side, colour and status ticks. */\n currentUserId?: string;\n /** Renders the composer when given. Receives the trimmed text. */\n onSend?: (text: string) => void | Promise<void>;\n /** Enables the retry control on a `\"failed\"` message. */\n onRetry?: (message: ChatMessage) => void;\n /** Names currently typing. One, two or a count is phrased for you. */\n typing?: readonly string[];\n /** Avatar for the first message of a run — an `<Avatar>`, an `<Icon>`. */\n renderAvatar?: (message: ChatMessage) => ReactNode;\n /** Rendered above the thread, inside the panel. */\n header?: ReactNode;\n /** Shown when there are no messages. */\n emptyState?: ReactNode;\n /** Gap that still keeps consecutive messages in one run. Default 5 min. */\n groupWindowMs?: number;\n /** Locale for labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Reference instant for \"Hoje\"/\"Ontem\". Default: now, at render time. */\n now?: number;\n /** Placeholder for the composer. */\n placeholder?: string;\n /** Extra controls inside the composer, before the send button. */\n composerActions?: ReactNode;\n /** Disable the composer — no permission, thread archived, offline. */\n composerDisabled?: boolean;\n /** Called when `onSend` rejects. The draft stays in the field either way. */\n onSendError?: (error: unknown) => void;\n}\n\n/** Status glyph and label for an outgoing message. */\nconst STATUS_GLYPH: Record<NonNullable<ChatMessage[\"status\"]>, string> = {\n sending: \"◌\",\n sent: \"✓\",\n read: \"✓✓\",\n failed: \"!\",\n};\n\n/**\n * A message thread: grouped by author and by day, own messages on one side, with\n * delivery state, a typing indicator and an optional composer.\n *\n * Presentational and controlled, like the rest of the SDK: it takes a list and\n * emits intent (`onSend`, `onRetry`). Where messages come from — REST, the SDK's\n * `createWebSocket`, an SSE stream — and how an optimistic insert is done stay with\n * the app, because those differ per backend and a component that assumed one would\n * be wrong for most.\n *\n * It works as a comment thread too: that is the same component with\n * `currentUserId` set and no `typing`.\n *\n * @example\n * <Chat\n * messages={messages}\n * currentUserId={me.id}\n * typing={typingNames}\n * onSend={(text) => send({ text })}\n * onRetry={(message) => resend(message.id)}\n * />\n */\nexport function Chat({\n messages,\n currentUserId,\n onSend,\n onRetry,\n typing = [],\n renderAvatar,\n header,\n emptyState,\n groupWindowMs,\n locale = \"pt-BR\",\n now,\n placeholder,\n composerActions,\n composerDisabled,\n onSendError,\n className,\n ...rest\n}: ChatProps) {\n const strings = chatStrings(locale);\n const sections = groupMessages({ messages, currentUserId, windowMs: groupWindowMs });\n const composer = useRef<ChatComposerHandle | null>(null);\n const thread = useRef<HTMLDivElement | null>(null);\n const stuckToBottom = useRef(true);\n\n /**\n * Remember whether the reader is at the bottom, before the next batch lands.\n *\n * A thread that always scrolls to the newest message yanks somebody out of the\n * history they were reading, every time anyone types. So the jump only happens\n * when they were already at the bottom — the same rule every chat app converges\n * on. The 48px slack covers a partially visible last row.\n */\n const trackPosition = (): void => {\n const node = thread.current;\n if (!node) return;\n const distance = node.scrollHeight - node.scrollTop - node.clientHeight;\n stuckToBottom.current = distance < 48;\n };\n\n useLayoutEffect(() => {\n const node = thread.current;\n if (!node || !stuckToBottom.current) return;\n node.scrollTop = node.scrollHeight;\n }, [messages, typing]);\n\n useEffect(() => {\n const node = thread.current;\n if (!node) return;\n node.scrollTop = node.scrollHeight;\n // Mount lands on the newest message; from then on `trackPosition` decides.\n }, []);\n\n const typingText = typingLabel(typing, locale);\n\n return (\n <div className={cn(styles.panel, className)} {...rest}>\n {header && <header className={styles.header}>{header}</header>}\n\n <div\n ref={thread}\n className={styles.thread}\n onScroll={trackPosition}\n role=\"log\"\n aria-live=\"polite\"\n aria-relevant=\"additions text\"\n tabIndex={0}\n aria-label={strings.thread}\n >\n {messages.length === 0\n ? (emptyState ?? <EmptyState title={strings.empty} />)\n : sections.map((section) =>\n section.kind === \"day\" ? (\n <div key={section.key} className={styles.day}>\n <span className={styles.dayLabel}>\n {dayLabel(section.date, { locale, now })}\n </span>\n </div>\n ) : (\n <div\n key={section.key}\n className={cn(styles.run, section.own && styles.ownRun)}\n >\n {renderAvatar && (\n <div className={styles.avatar} aria-hidden=\"true\">\n {renderAvatar(section.messages[0])}\n </div>\n )}\n <div className={styles.runBody}>\n <span className={styles.author}>\n {section.own\n ? strings.you\n : (section.authorName ?? section.authorId)}\n </span>\n <ul className={styles.bubbles}>\n {section.messages.map((message) => (\n <li key={message.id} className={styles.bubbleRow}>\n <div\n className={cn(\n styles.bubble,\n section.own && styles.ownBubble,\n message.status === \"failed\" &&\n styles.failedBubble,\n )}\n >\n <div className={styles.body}>\n {message.body}\n </div>\n <div className={styles.meta}>\n <time\n dateTime={new Date(\n message.sentAt,\n ).toISOString()}\n >\n {timeLabel(message.sentAt, locale)}\n </time>\n {section.own && message.status && (\n <span\n className={cn(\n styles.status,\n message.status === \"failed\" &&\n styles.statusFailed,\n )}\n title={strings[message.status]}\n >\n <span aria-hidden=\"true\">\n {STATUS_GLYPH[message.status]}\n </span>\n <VisuallyHidden>\n {strings[message.status]}\n </VisuallyHidden>\n </span>\n )}\n </div>\n </div>\n {message.status === \"failed\" && onRetry && (\n <button\n type=\"button\"\n className={styles.retry}\n onClick={() => onRetry(message)}\n >\n {strings.retry}\n </button>\n )}\n </li>\n ))}\n </ul>\n </div>\n </div>\n ),\n )}\n </div>\n\n {typingText && (\n <p className={styles.typing} aria-live=\"polite\">\n {typingText}\n </p>\n )}\n\n {onSend && (\n <ChatComposer\n ref={composer}\n onSend={onSend}\n locale={locale}\n placeholder={placeholder}\n actions={composerActions}\n disabled={composerDisabled}\n onError={onSendError}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { cn as e } from "../../utils/cn.js";
|
|
2
|
+
import { EmptyState as t } from "../EmptyState/EmptyState.js";
|
|
3
|
+
import { VisuallyHidden as n } from "../VisuallyHidden/VisuallyHidden.js";
|
|
4
|
+
import { chatStrings as r, dayLabel as i, groupMessages as a, timeLabel as o, typingLabel as s } from "./chat-groups.js";
|
|
5
|
+
import c from "./Chat.module.js";
|
|
6
|
+
import { ChatComposer as l } from "./ChatComposer.js";
|
|
7
|
+
import { useEffect as u, useLayoutEffect as d, useRef as f } from "react";
|
|
8
|
+
import { jsx as p, jsxs as m } from "react/jsx-runtime";
|
|
9
|
+
//#region src/components/Chat/Chat.tsx
|
|
10
|
+
var h = {
|
|
11
|
+
sending: "◌",
|
|
12
|
+
sent: "✓",
|
|
13
|
+
read: "✓✓",
|
|
14
|
+
failed: "!"
|
|
15
|
+
};
|
|
16
|
+
function g({ messages: g, currentUserId: _, onSend: v, onRetry: y, typing: b = [], renderAvatar: x, header: S, emptyState: C, groupWindowMs: w, locale: T = "pt-BR", now: E, placeholder: D, composerActions: O, composerDisabled: k, onSendError: A, className: j, ...M }) {
|
|
17
|
+
let N = r(T), P = a({
|
|
18
|
+
messages: g,
|
|
19
|
+
currentUserId: _,
|
|
20
|
+
windowMs: w
|
|
21
|
+
}), F = f(null), I = f(null), L = f(!0), R = () => {
|
|
22
|
+
let e = I.current;
|
|
23
|
+
if (!e) return;
|
|
24
|
+
let t = e.scrollHeight - e.scrollTop - e.clientHeight;
|
|
25
|
+
L.current = t < 48;
|
|
26
|
+
};
|
|
27
|
+
d(() => {
|
|
28
|
+
let e = I.current;
|
|
29
|
+
!e || !L.current || (e.scrollTop = e.scrollHeight);
|
|
30
|
+
}, [g, b]), u(() => {
|
|
31
|
+
let e = I.current;
|
|
32
|
+
e && (e.scrollTop = e.scrollHeight);
|
|
33
|
+
}, []);
|
|
34
|
+
let z = s(b, T);
|
|
35
|
+
return /* @__PURE__ */ m("div", {
|
|
36
|
+
className: e(c.panel, j),
|
|
37
|
+
...M,
|
|
38
|
+
children: [
|
|
39
|
+
S && /* @__PURE__ */ p("header", {
|
|
40
|
+
className: c.header,
|
|
41
|
+
children: S
|
|
42
|
+
}),
|
|
43
|
+
/* @__PURE__ */ p("div", {
|
|
44
|
+
ref: I,
|
|
45
|
+
className: c.thread,
|
|
46
|
+
onScroll: R,
|
|
47
|
+
role: "log",
|
|
48
|
+
"aria-live": "polite",
|
|
49
|
+
"aria-relevant": "additions text",
|
|
50
|
+
tabIndex: 0,
|
|
51
|
+
"aria-label": N.thread,
|
|
52
|
+
children: g.length === 0 ? C ?? /* @__PURE__ */ p(t, { title: N.empty }) : P.map((t) => t.kind === "day" ? /* @__PURE__ */ p("div", {
|
|
53
|
+
className: c.day,
|
|
54
|
+
children: /* @__PURE__ */ p("span", {
|
|
55
|
+
className: c.dayLabel,
|
|
56
|
+
children: i(t.date, {
|
|
57
|
+
locale: T,
|
|
58
|
+
now: E
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
}, t.key) : /* @__PURE__ */ m("div", {
|
|
62
|
+
className: e(c.run, t.own && c.ownRun),
|
|
63
|
+
children: [x && /* @__PURE__ */ p("div", {
|
|
64
|
+
className: c.avatar,
|
|
65
|
+
"aria-hidden": "true",
|
|
66
|
+
children: x(t.messages[0])
|
|
67
|
+
}), /* @__PURE__ */ m("div", {
|
|
68
|
+
className: c.runBody,
|
|
69
|
+
children: [/* @__PURE__ */ p("span", {
|
|
70
|
+
className: c.author,
|
|
71
|
+
children: t.own ? N.you : t.authorName ?? t.authorId
|
|
72
|
+
}), /* @__PURE__ */ p("ul", {
|
|
73
|
+
className: c.bubbles,
|
|
74
|
+
children: t.messages.map((r) => /* @__PURE__ */ m("li", {
|
|
75
|
+
className: c.bubbleRow,
|
|
76
|
+
children: [/* @__PURE__ */ m("div", {
|
|
77
|
+
className: e(c.bubble, t.own && c.ownBubble, r.status === "failed" && c.failedBubble),
|
|
78
|
+
children: [/* @__PURE__ */ p("div", {
|
|
79
|
+
className: c.body,
|
|
80
|
+
children: r.body
|
|
81
|
+
}), /* @__PURE__ */ m("div", {
|
|
82
|
+
className: c.meta,
|
|
83
|
+
children: [/* @__PURE__ */ p("time", {
|
|
84
|
+
dateTime: new Date(r.sentAt).toISOString(),
|
|
85
|
+
children: o(r.sentAt, T)
|
|
86
|
+
}), t.own && r.status && /* @__PURE__ */ m("span", {
|
|
87
|
+
className: e(c.status, r.status === "failed" && c.statusFailed),
|
|
88
|
+
title: N[r.status],
|
|
89
|
+
children: [/* @__PURE__ */ p("span", {
|
|
90
|
+
"aria-hidden": "true",
|
|
91
|
+
children: h[r.status]
|
|
92
|
+
}), /* @__PURE__ */ p(n, { children: N[r.status] })]
|
|
93
|
+
})]
|
|
94
|
+
})]
|
|
95
|
+
}), r.status === "failed" && y && /* @__PURE__ */ p("button", {
|
|
96
|
+
type: "button",
|
|
97
|
+
className: c.retry,
|
|
98
|
+
onClick: () => y(r),
|
|
99
|
+
children: N.retry
|
|
100
|
+
})]
|
|
101
|
+
}, r.id))
|
|
102
|
+
})]
|
|
103
|
+
})]
|
|
104
|
+
}, t.key))
|
|
105
|
+
}),
|
|
106
|
+
z && /* @__PURE__ */ p("p", {
|
|
107
|
+
className: c.typing,
|
|
108
|
+
"aria-live": "polite",
|
|
109
|
+
children: z
|
|
110
|
+
}),
|
|
111
|
+
v && /* @__PURE__ */ p(l, {
|
|
112
|
+
ref: F,
|
|
113
|
+
onSend: v,
|
|
114
|
+
locale: T,
|
|
115
|
+
placeholder: D,
|
|
116
|
+
actions: O,
|
|
117
|
+
disabled: k,
|
|
118
|
+
onError: A
|
|
119
|
+
})
|
|
120
|
+
]
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
//#endregion
|
|
124
|
+
export { g as Chat };
|
|
125
|
+
|
|
126
|
+
//# sourceMappingURL=Chat.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Chat.js","names":[],"sources":["../../../src/components/Chat/Chat.tsx"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { EmptyState } from \"../EmptyState\";\nimport { VisuallyHidden } from \"../VisuallyHidden\";\nimport { ChatComposer, type ChatComposerHandle } from \"./ChatComposer\";\nimport {\n chatStrings,\n dayLabel,\n groupMessages,\n timeLabel,\n typingLabel,\n type ChatMessage,\n} from \"./chat-groups\";\nimport styles from \"./Chat.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onSubmit\";\n\nexport interface ChatProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /** The thread, **oldest first**. Never reordered by the component. */\n messages: readonly ChatMessage[];\n /** Author id treated as \"own\" — decides side, colour and status ticks. */\n currentUserId?: string;\n /** Renders the composer when given. Receives the trimmed text. */\n onSend?: (text: string) => void | Promise<void>;\n /** Enables the retry control on a `\"failed\"` message. */\n onRetry?: (message: ChatMessage) => void;\n /** Names currently typing. One, two or a count is phrased for you. */\n typing?: readonly string[];\n /** Avatar for the first message of a run — an `<Avatar>`, an `<Icon>`. */\n renderAvatar?: (message: ChatMessage) => ReactNode;\n /** Rendered above the thread, inside the panel. */\n header?: ReactNode;\n /** Shown when there are no messages. */\n emptyState?: ReactNode;\n /** Gap that still keeps consecutive messages in one run. Default 5 min. */\n groupWindowMs?: number;\n /** Locale for labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Reference instant for \"Hoje\"/\"Ontem\". Default: now, at render time. */\n now?: number;\n /** Placeholder for the composer. */\n placeholder?: string;\n /** Extra controls inside the composer, before the send button. */\n composerActions?: ReactNode;\n /** Disable the composer — no permission, thread archived, offline. */\n composerDisabled?: boolean;\n /** Called when `onSend` rejects. The draft stays in the field either way. */\n onSendError?: (error: unknown) => void;\n}\n\n/** Status glyph and label for an outgoing message. */\nconst STATUS_GLYPH: Record<NonNullable<ChatMessage[\"status\"]>, string> = {\n sending: \"◌\",\n sent: \"✓\",\n read: \"✓✓\",\n failed: \"!\",\n};\n\n/**\n * A message thread: grouped by author and by day, own messages on one side, with\n * delivery state, a typing indicator and an optional composer.\n *\n * Presentational and controlled, like the rest of the SDK: it takes a list and\n * emits intent (`onSend`, `onRetry`). Where messages come from — REST, the SDK's\n * `createWebSocket`, an SSE stream — and how an optimistic insert is done stay with\n * the app, because those differ per backend and a component that assumed one would\n * be wrong for most.\n *\n * It works as a comment thread too: that is the same component with\n * `currentUserId` set and no `typing`.\n *\n * @example\n * <Chat\n * messages={messages}\n * currentUserId={me.id}\n * typing={typingNames}\n * onSend={(text) => send({ text })}\n * onRetry={(message) => resend(message.id)}\n * />\n */\nexport function Chat({\n messages,\n currentUserId,\n onSend,\n onRetry,\n typing = [],\n renderAvatar,\n header,\n emptyState,\n groupWindowMs,\n locale = \"pt-BR\",\n now,\n placeholder,\n composerActions,\n composerDisabled,\n onSendError,\n className,\n ...rest\n}: ChatProps) {\n const strings = chatStrings(locale);\n const sections = groupMessages({ messages, currentUserId, windowMs: groupWindowMs });\n const composer = useRef<ChatComposerHandle | null>(null);\n const thread = useRef<HTMLDivElement | null>(null);\n const stuckToBottom = useRef(true);\n\n /**\n * Remember whether the reader is at the bottom, before the next batch lands.\n *\n * A thread that always scrolls to the newest message yanks somebody out of the\n * history they were reading, every time anyone types. So the jump only happens\n * when they were already at the bottom — the same rule every chat app converges\n * on. The 48px slack covers a partially visible last row.\n */\n const trackPosition = (): void => {\n const node = thread.current;\n if (!node) return;\n const distance = node.scrollHeight - node.scrollTop - node.clientHeight;\n stuckToBottom.current = distance < 48;\n };\n\n useLayoutEffect(() => {\n const node = thread.current;\n if (!node || !stuckToBottom.current) return;\n node.scrollTop = node.scrollHeight;\n }, [messages, typing]);\n\n useEffect(() => {\n const node = thread.current;\n if (!node) return;\n node.scrollTop = node.scrollHeight;\n // Mount lands on the newest message; from then on `trackPosition` decides.\n }, []);\n\n const typingText = typingLabel(typing, locale);\n\n return (\n <div className={cn(styles.panel, className)} {...rest}>\n {header && <header className={styles.header}>{header}</header>}\n\n <div\n ref={thread}\n className={styles.thread}\n onScroll={trackPosition}\n role=\"log\"\n aria-live=\"polite\"\n aria-relevant=\"additions text\"\n tabIndex={0}\n aria-label={strings.thread}\n >\n {messages.length === 0\n ? (emptyState ?? <EmptyState title={strings.empty} />)\n : sections.map((section) =>\n section.kind === \"day\" ? (\n <div key={section.key} className={styles.day}>\n <span className={styles.dayLabel}>\n {dayLabel(section.date, { locale, now })}\n </span>\n </div>\n ) : (\n <div\n key={section.key}\n className={cn(styles.run, section.own && styles.ownRun)}\n >\n {renderAvatar && (\n <div className={styles.avatar} aria-hidden=\"true\">\n {renderAvatar(section.messages[0])}\n </div>\n )}\n <div className={styles.runBody}>\n <span className={styles.author}>\n {section.own\n ? strings.you\n : (section.authorName ?? section.authorId)}\n </span>\n <ul className={styles.bubbles}>\n {section.messages.map((message) => (\n <li key={message.id} className={styles.bubbleRow}>\n <div\n className={cn(\n styles.bubble,\n section.own && styles.ownBubble,\n message.status === \"failed\" &&\n styles.failedBubble,\n )}\n >\n <div className={styles.body}>\n {message.body}\n </div>\n <div className={styles.meta}>\n <time\n dateTime={new Date(\n message.sentAt,\n ).toISOString()}\n >\n {timeLabel(message.sentAt, locale)}\n </time>\n {section.own && message.status && (\n <span\n className={cn(\n styles.status,\n message.status === \"failed\" &&\n styles.statusFailed,\n )}\n title={strings[message.status]}\n >\n <span aria-hidden=\"true\">\n {STATUS_GLYPH[message.status]}\n </span>\n <VisuallyHidden>\n {strings[message.status]}\n </VisuallyHidden>\n </span>\n )}\n </div>\n </div>\n {message.status === \"failed\" && onRetry && (\n <button\n type=\"button\"\n className={styles.retry}\n onClick={() => onRetry(message)}\n >\n {strings.retry}\n </button>\n )}\n </li>\n ))}\n </ul>\n </div>\n </div>\n ),\n )}\n </div>\n\n {typingText && (\n <p className={styles.typing} aria-live=\"polite\">\n {typingText}\n </p>\n )}\n\n {onSend && (\n <ChatComposer\n ref={composer}\n onSend={onSend}\n locale={locale}\n placeholder={placeholder}\n actions={composerActions}\n disabled={composerDisabled}\n onError={onSendError}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var e=`tempest_panel_xUVw0`,t=`tempest_header_muoPu`,n=`tempest_thread_-j8yS`,r=`tempest_day_uLsWA`,i=`tempest_dayLabel_m8rDy`,a=`tempest_run_jOUv7`,o=`tempest_ownRun_3XTG5`,s=`tempest_avatar_d9SYT`,c=`tempest_runBody_nHQC3`,l=`tempest_author_4MRkd`,u=`tempest_bubbles_VYaQ0`,d=`tempest_bubbleRow_QSc41`,f=`tempest_bubble_GGFcU`,p=`tempest_ownBubble_agOMW`,m=`tempest_failedBubble_kT3F5`,h=`tempest_body_1aH2F`,g=`tempest_meta_3fNx8`,_=`tempest_status_EeiUt`,v=`tempest_statusFailed_Bfro1`,y=`tempest_retry_DAbIj`,b=`tempest_typing_zdp3O`,x=`tempest_composer_krOGl`,S=`tempest_field_fBY-q`,C=`tempest_composerActions_MYwQx`,w=`tempest_send_NwcaR`,T={panel:e,header:t,thread:n,day:r,dayLabel:i,run:a,ownRun:o,avatar:s,runBody:c,author:l,bubbles:u,bubbleRow:d,bubble:f,ownBubble:p,failedBubble:m,body:h,meta:g,status:_,statusFailed:v,retry:y,typing:b,composer:x,field:S,composerActions:C,send:w};exports.author=l,exports.avatar=s,exports.body=h,exports.bubble=f,exports.bubbleRow=d,exports.bubbles=u,exports.composer=x,exports.composerActions=C,exports.day=r,exports.dayLabel=i,exports.default=T,exports.failedBubble=m,exports.field=S,exports.header=t,exports.meta=g,exports.ownBubble=p,exports.ownRun=o,exports.panel=e,exports.retry=y,exports.run=a,exports.runBody=c,exports.send=w,exports.status=_,exports.statusFailed=v,exports.thread=n,exports.typing=b;
|
|
2
|
+
//# sourceMappingURL=Chat.module.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Chat.module.cjs","names":[],"sources":["../../../src/components/Chat/Chat.module.css"],"sourcesContent":[".panel {\n display: flex;\n flex-direction: column;\n min-width: 0;\n min-height: 0;\n height: var(--tempest-chat-height, 100%);\n background-color: var(--tempest-bg);\n color: var(--tempest-text);\n font-family: var(--tempest-font-sans);\n}\n\n.header {\n flex: 0 0 auto;\n padding: var(--tempest-space-3) var(--tempest-space-4);\n border-bottom: 1px solid var(--tempest-border);\n}\n\n.thread {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n gap: var(--tempest-space-3);\n min-height: 0;\n padding: var(--tempest-space-4);\n overflow-y: auto;\n overscroll-behavior: contain;\n}\n\n.thread:focus-visible {\n outline: 2px solid var(--tempest-primary);\n outline-offset: -2px;\n}\n\n.day {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-3);\n}\n\n.day::before,\n.day::after {\n flex: 1 1 auto;\n height: 1px;\n background-color: var(--tempest-border);\n content: \"\";\n}\n\n.dayLabel {\n color: var(--tempest-text-subtle);\n font-size: var(--tempest-text-xs);\n text-transform: capitalize;\n}\n\n.run {\n display: flex;\n gap: var(--tempest-space-3);\n align-items: flex-start;\n max-width: 100%;\n}\n\n/*\n * Own messages mirror the row instead of moving to a second column: the avatar,\n * the name and the bubbles keep the same source order, so a screen reader still\n * reads \"who, then what\" while the eye reads it from the other side.\n */\n.ownRun {\n flex-direction: row-reverse;\n}\n\n.avatar {\n flex: 0 0 auto;\n}\n\n.runBody {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n min-width: 0;\n max-width: min(78%, 42rem);\n}\n\n.ownRun .runBody {\n align-items: flex-end;\n}\n\n.author {\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-medium);\n}\n\n.bubbles {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.bubbleRow {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n}\n\n.ownRun .bubbleRow {\n flex-direction: row-reverse;\n}\n\n.bubble {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n min-width: 0;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n background-color: var(--tempest-surface);\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-normal);\n}\n\n.ownBubble {\n border-color: transparent;\n background-color: var(--tempest-primary-soft);\n color: var(--tempest-primary-on-soft);\n}\n\n/*\n * A failed message keeps its text readable and states the failure at the border\n * and in the meta row. Tinting the whole bubble danger-red would make the message\n * itself hard to read — and re-reading it is exactly what somebody does before\n * deciding to retry.\n */\n.failedBubble {\n border-color: var(--tempest-danger);\n background-color: var(--tempest-danger-bg);\n color: var(--tempest-text);\n}\n\n.body {\n overflow-wrap: anywhere;\n white-space: pre-wrap;\n}\n\n/*\n * The timestamp uses the muted token, not the subtle one, and switches to the\n * bubble's own foreground when the bubble is tinted.\n *\n * `--tempest-text-subtle` is solved against the page and surface backgrounds. On\n * `--tempest-primary-soft` it is a different composite, and the real browser\n * measured it below the 4.5:1 text floor — the same trap the syntax tokens hit.\n * Size is what de-emphasises the meta row here; colour still has to pass.\n */\n.meta {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: var(--tempest-space-2);\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-2xs);\n font-variant-numeric: tabular-nums;\n}\n\n.ownBubble .meta {\n color: var(--tempest-primary-on-soft);\n}\n\n.status {\n display: inline-flex;\n align-items: center;\n}\n\n.statusFailed {\n color: var(--tempest-danger);\n font-weight: var(--tempest-weight-bold);\n}\n\n.retry {\n padding: 0;\n border: none;\n background: none;\n color: var(--tempest-danger);\n font: inherit;\n font-size: var(--tempest-text-xs);\n text-decoration: underline;\n cursor: pointer;\n}\n\n.typing {\n flex: 0 0 auto;\n margin: 0;\n padding: 0 var(--tempest-space-4) var(--tempest-space-2);\n color: var(--tempest-text-subtle);\n font-size: var(--tempest-text-xs);\n}\n\n.composer {\n display: flex;\n flex: 0 0 auto;\n align-items: flex-end;\n gap: var(--tempest-space-2);\n padding: var(--tempest-space-3) var(--tempest-space-4);\n border-top: 1px solid var(--tempest-border);\n}\n\n.field {\n flex: 1 1 auto;\n min-width: 0;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-bg);\n color: inherit;\n font: inherit;\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-normal);\n resize: none;\n}\n\n.field:focus-visible {\n outline: 2px solid var(--tempest-primary);\n outline-offset: -1px;\n}\n\n.field:disabled {\n background-color: var(--tempest-surface-2);\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n.composerActions {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n}\n\n.send {\n padding: var(--tempest-space-2) var(--tempest-space-4);\n border: none;\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-foreground);\n font: inherit;\n font-size: var(--tempest-text-sm);\n font-weight: var(--tempest-weight-medium);\n cursor: pointer;\n}\n\n.send:disabled {\n background-color: var(--tempest-surface-3);\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n@media (max-width: 480px) {\n .runBody {\n max-width: 88%;\n }\n}\n"],"mappings":""}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region src/components/Chat/Chat.module.css
|
|
2
|
+
var e = "tempest_panel_xUVw0", t = "tempest_header_muoPu", n = "tempest_thread_-j8yS", r = "tempest_day_uLsWA", i = "tempest_dayLabel_m8rDy", a = "tempest_run_jOUv7", o = "tempest_ownRun_3XTG5", s = "tempest_avatar_d9SYT", c = "tempest_runBody_nHQC3", l = "tempest_author_4MRkd", u = "tempest_bubbles_VYaQ0", d = "tempest_bubbleRow_QSc41", f = "tempest_bubble_GGFcU", p = "tempest_ownBubble_agOMW", m = "tempest_failedBubble_kT3F5", h = "tempest_body_1aH2F", g = "tempest_meta_3fNx8", _ = "tempest_status_EeiUt", v = "tempest_statusFailed_Bfro1", y = "tempest_retry_DAbIj", b = "tempest_typing_zdp3O", x = "tempest_composer_krOGl", S = "tempest_field_fBY-q", C = "tempest_composerActions_MYwQx", w = "tempest_send_NwcaR", T = {
|
|
3
|
+
panel: e,
|
|
4
|
+
header: t,
|
|
5
|
+
thread: n,
|
|
6
|
+
day: r,
|
|
7
|
+
dayLabel: i,
|
|
8
|
+
run: a,
|
|
9
|
+
ownRun: o,
|
|
10
|
+
avatar: s,
|
|
11
|
+
runBody: c,
|
|
12
|
+
author: l,
|
|
13
|
+
bubbles: u,
|
|
14
|
+
bubbleRow: d,
|
|
15
|
+
bubble: f,
|
|
16
|
+
ownBubble: p,
|
|
17
|
+
failedBubble: m,
|
|
18
|
+
body: h,
|
|
19
|
+
meta: g,
|
|
20
|
+
status: _,
|
|
21
|
+
statusFailed: v,
|
|
22
|
+
retry: y,
|
|
23
|
+
typing: b,
|
|
24
|
+
composer: x,
|
|
25
|
+
field: S,
|
|
26
|
+
composerActions: C,
|
|
27
|
+
send: w
|
|
28
|
+
};
|
|
29
|
+
//#endregion
|
|
30
|
+
export { l as author, s as avatar, h as body, f as bubble, d as bubbleRow, u as bubbles, x as composer, C as composerActions, r as day, i as dayLabel, T as default, m as failedBubble, S as field, t as header, g as meta, p as ownBubble, o as ownRun, e as panel, y as retry, a as run, c as runBody, w as send, _ as status, v as statusFailed, n as thread, b as typing };
|
|
31
|
+
|
|
32
|
+
//# sourceMappingURL=Chat.module.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Chat.module.js","names":[],"sources":["../../../src/components/Chat/Chat.module.css"],"sourcesContent":[".panel {\n display: flex;\n flex-direction: column;\n min-width: 0;\n min-height: 0;\n height: var(--tempest-chat-height, 100%);\n background-color: var(--tempest-bg);\n color: var(--tempest-text);\n font-family: var(--tempest-font-sans);\n}\n\n.header {\n flex: 0 0 auto;\n padding: var(--tempest-space-3) var(--tempest-space-4);\n border-bottom: 1px solid var(--tempest-border);\n}\n\n.thread {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n gap: var(--tempest-space-3);\n min-height: 0;\n padding: var(--tempest-space-4);\n overflow-y: auto;\n overscroll-behavior: contain;\n}\n\n.thread:focus-visible {\n outline: 2px solid var(--tempest-primary);\n outline-offset: -2px;\n}\n\n.day {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-3);\n}\n\n.day::before,\n.day::after {\n flex: 1 1 auto;\n height: 1px;\n background-color: var(--tempest-border);\n content: \"\";\n}\n\n.dayLabel {\n color: var(--tempest-text-subtle);\n font-size: var(--tempest-text-xs);\n text-transform: capitalize;\n}\n\n.run {\n display: flex;\n gap: var(--tempest-space-3);\n align-items: flex-start;\n max-width: 100%;\n}\n\n/*\n * Own messages mirror the row instead of moving to a second column: the avatar,\n * the name and the bubbles keep the same source order, so a screen reader still\n * reads \"who, then what\" while the eye reads it from the other side.\n */\n.ownRun {\n flex-direction: row-reverse;\n}\n\n.avatar {\n flex: 0 0 auto;\n}\n\n.runBody {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n min-width: 0;\n max-width: min(78%, 42rem);\n}\n\n.ownRun .runBody {\n align-items: flex-end;\n}\n\n.author {\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-xs);\n font-weight: var(--tempest-weight-medium);\n}\n\n.bubbles {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.bubbleRow {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n}\n\n.ownRun .bubbleRow {\n flex-direction: row-reverse;\n}\n\n.bubble {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n min-width: 0;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-lg);\n background-color: var(--tempest-surface);\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-normal);\n}\n\n.ownBubble {\n border-color: transparent;\n background-color: var(--tempest-primary-soft);\n color: var(--tempest-primary-on-soft);\n}\n\n/*\n * A failed message keeps its text readable and states the failure at the border\n * and in the meta row. Tinting the whole bubble danger-red would make the message\n * itself hard to read — and re-reading it is exactly what somebody does before\n * deciding to retry.\n */\n.failedBubble {\n border-color: var(--tempest-danger);\n background-color: var(--tempest-danger-bg);\n color: var(--tempest-text);\n}\n\n.body {\n overflow-wrap: anywhere;\n white-space: pre-wrap;\n}\n\n/*\n * The timestamp uses the muted token, not the subtle one, and switches to the\n * bubble's own foreground when the bubble is tinted.\n *\n * `--tempest-text-subtle` is solved against the page and surface backgrounds. On\n * `--tempest-primary-soft` it is a different composite, and the real browser\n * measured it below the 4.5:1 text floor — the same trap the syntax tokens hit.\n * Size is what de-emphasises the meta row here; colour still has to pass.\n */\n.meta {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: var(--tempest-space-2);\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-2xs);\n font-variant-numeric: tabular-nums;\n}\n\n.ownBubble .meta {\n color: var(--tempest-primary-on-soft);\n}\n\n.status {\n display: inline-flex;\n align-items: center;\n}\n\n.statusFailed {\n color: var(--tempest-danger);\n font-weight: var(--tempest-weight-bold);\n}\n\n.retry {\n padding: 0;\n border: none;\n background: none;\n color: var(--tempest-danger);\n font: inherit;\n font-size: var(--tempest-text-xs);\n text-decoration: underline;\n cursor: pointer;\n}\n\n.typing {\n flex: 0 0 auto;\n margin: 0;\n padding: 0 var(--tempest-space-4) var(--tempest-space-2);\n color: var(--tempest-text-subtle);\n font-size: var(--tempest-text-xs);\n}\n\n.composer {\n display: flex;\n flex: 0 0 auto;\n align-items: flex-end;\n gap: var(--tempest-space-2);\n padding: var(--tempest-space-3) var(--tempest-space-4);\n border-top: 1px solid var(--tempest-border);\n}\n\n.field {\n flex: 1 1 auto;\n min-width: 0;\n padding: var(--tempest-space-2) var(--tempest-space-3);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-bg);\n color: inherit;\n font: inherit;\n font-size: var(--tempest-text-sm);\n line-height: var(--tempest-leading-normal);\n resize: none;\n}\n\n.field:focus-visible {\n outline: 2px solid var(--tempest-primary);\n outline-offset: -1px;\n}\n\n.field:disabled {\n background-color: var(--tempest-surface-2);\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n.composerActions {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-2);\n}\n\n.send {\n padding: var(--tempest-space-2) var(--tempest-space-4);\n border: none;\n border-radius: var(--tempest-radius-md);\n background-color: var(--tempest-primary);\n color: var(--tempest-primary-foreground);\n font: inherit;\n font-size: var(--tempest-text-sm);\n font-weight: var(--tempest-weight-medium);\n cursor: pointer;\n}\n\n.send:disabled {\n background-color: var(--tempest-surface-3);\n color: var(--tempest-text-subtle);\n cursor: not-allowed;\n}\n\n@media (max-width: 480px) {\n .runBody {\n max-width: 88%;\n }\n}\n"],"mappings":""}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("../../utils/cn.cjs"),t=require("./chat-groups.cjs"),n=require("./Chat.module.cjs");let r=require("react"),i=require("react/jsx-runtime");var a=(0,r.forwardRef)(function({onSend:a,locale:o=`pt-BR`,actions:s,maxRows:c=6,sendLabel:l,onError:u,className:d,disabled:f,placeholder:p,onKeyDown:m,onChange:h,...g},_){let v=t.chatStrings(o),y=(0,r.useRef)(null),[b,x]=(0,r.useState)(``),[S,C]=(0,r.useState)(!1);(0,r.useImperativeHandle)(_,()=>({focus:()=>y.current?.focus(),setValue:e=>{x(e),y.current?.focus()}})),(0,r.useLayoutEffect)(()=>{let e=y.current;if(!e)return;e.style.height=`auto`;let t=(Number.parseFloat(getComputedStyle(e).lineHeight)||20)*c;e.style.height=`${Math.min(e.scrollHeight,t)}px`,e.style.overflowY=e.scrollHeight>t?`auto`:`hidden`},[b,c]);let w=async()=>{let e=b.trim();if(!(!e||S||f)){C(!0);try{await a(e),x(``)}catch(e){u?.(e)}finally{C(!1)}}},T=e=>{e.preventDefault(),w()},E=e=>{m?.(e),!e.defaultPrevented&&(e.key!==`Enter`||e.shiftKey||e.nativeEvent.isComposing||e.keyCode===229||(e.preventDefault(),w()))};return(0,i.jsxs)(`form`,{className:e.cn(n.default.composer,d),onSubmit:T,children:[(0,i.jsx)(`textarea`,{...g,ref:y,className:n.default.field,rows:1,value:b,disabled:f,placeholder:p??v.placeholder,onChange:e=>{x(e.target.value),h?.(e)},onKeyDown:E}),(0,i.jsxs)(`div`,{className:n.default.composerActions,children:[s,(0,i.jsx)(`button`,{type:`submit`,className:n.default.send,disabled:f||S||b.trim()===``,children:l??v.send})]})]})});exports.ChatComposer=a;
|
|
2
|
+
//# sourceMappingURL=ChatComposer.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChatComposer.cjs","names":[],"sources":["../../../src/components/Chat/ChatComposer.tsx"],"sourcesContent":["import {\n forwardRef,\n useImperativeHandle,\n useLayoutEffect,\n useRef,\n useState,\n type FormEvent,\n type KeyboardEvent,\n type ReactNode,\n type TextareaHTMLAttributes,\n} from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { chatStrings } from \"./chat-groups\";\nimport styles from \"./Chat.module.css\";\n\n/** DOM attributes the composer redefines. */\ntype OverriddenDomProps = \"onSubmit\" | \"value\" | \"defaultValue\" | \"rows\";\n\nexport interface ChatComposerProps extends Omit<\n TextareaHTMLAttributes<HTMLTextAreaElement>,\n OverriddenDomProps\n> {\n /** Called with the trimmed text. The field clears only when this does not throw. */\n onSend: (text: string) => void | Promise<void>;\n /** Locale for the placeholder and the send label. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Rendered to the left of the send button — an attach button, an emoji picker. */\n actions?: ReactNode;\n /** Largest height the field grows to, in lines. Default 6. */\n maxRows?: number;\n /** Send label. Defaults to the locale string. */\n sendLabel?: string;\n /**\n * Called when `onSend` rejects. The draft is kept either way.\n *\n * Without it the rejection is swallowed after the draft is preserved, because\n * the alternative is worse: re-throwing out of a DOM event handler surfaces as\n * an unhandled promise rejection — console noise for the developer, a hit in\n * whatever crash reporter the app runs, and nothing the user can act on. The\n * visible signal is the text still sitting in the field; wire this to a toast\n * to say why.\n */\n onError?: (error: unknown) => void;\n}\n\n/** Imperative handle, so a thread can focus the field after retrying a message. */\nexport interface ChatComposerHandle {\n focus: () => void;\n /** Replace the draft — used to put a failed message back in the field. */\n setValue: (text: string) => void;\n}\n\n/**\n * The message field of a thread: a textarea that grows with its content, sends on\n * `Enter` and keeps `Shift+Enter` for a newline.\n *\n * Uncontrolled on purpose. A chat draft changes on every keystroke, and lifting\n * that into app state re-renders the whole thread per character — the one place\n * where \"controlled by default\" costs something visible. Apps that need the draft\n * (a persisted composer, a slash-command menu) read it from `onChange` or drive it\n * through the ref.\n *\n * @example\n * <ChatComposer onSend={(text) => api.post(\"/messages\", { body: { text } })} />\n */\nexport const ChatComposer = forwardRef<ChatComposerHandle, ChatComposerProps>(function ChatComposer(\n {\n onSend,\n locale = \"pt-BR\",\n actions,\n maxRows = 6,\n sendLabel,\n onError,\n className,\n disabled,\n placeholder,\n onKeyDown,\n onChange,\n ...rest\n },\n ref,\n) {\n const strings = chatStrings(locale);\n const textarea = useRef<HTMLTextAreaElement | null>(null);\n const [value, setValue] = useState(\"\");\n const [busy, setBusy] = useState(false);\n\n useImperativeHandle(ref, () => ({\n focus: () => textarea.current?.focus(),\n setValue: (text: string) => {\n setValue(text);\n textarea.current?.focus();\n },\n }));\n\n /**\n * Grow the field to fit its content, up to `maxRows`.\n *\n * Measured from `scrollHeight` after resetting the height, because\n * `scrollHeight` on an element that is already tall enough reports the\n * current height and the field would never shrink back.\n */\n useLayoutEffect(() => {\n const node = textarea.current;\n if (!node) return;\n node.style.height = \"auto\";\n const lineHeight = Number.parseFloat(getComputedStyle(node).lineHeight) || 20;\n const max = lineHeight * maxRows;\n node.style.height = `${Math.min(node.scrollHeight, max)}px`;\n node.style.overflowY = node.scrollHeight > max ? \"auto\" : \"hidden\";\n }, [value, maxRows]);\n\n const submit = async (): Promise<void> => {\n const text = value.trim();\n if (!text || busy || disabled) return;\n setBusy(true);\n try {\n await onSend(text);\n setValue(\"\");\n } catch (error) {\n onError?.(error);\n } finally {\n setBusy(false);\n }\n };\n\n const handleSubmit = (event: FormEvent): void => {\n event.preventDefault();\n void submit();\n };\n\n /**\n * `Enter` sends, `Shift+Enter` breaks the line.\n *\n * The IME check is not optional: while composing Japanese or Korean, `Enter`\n * confirms the candidate word and `keyCode === 229` marks that keystroke.\n * Sending there would post half a word and eat the confirmation.\n */\n const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>): void => {\n onKeyDown?.(event);\n if (event.defaultPrevented) return;\n if (event.key !== \"Enter\" || event.shiftKey) return;\n if (event.nativeEvent.isComposing || event.keyCode === 229) return;\n event.preventDefault();\n void submit();\n };\n\n return (\n <form className={cn(styles.composer, className)} onSubmit={handleSubmit}>\n <textarea\n {...rest}\n ref={textarea}\n className={styles.field}\n rows={1}\n value={value}\n disabled={disabled}\n placeholder={placeholder ?? strings.placeholder}\n onChange={(event) => {\n setValue(event.target.value);\n onChange?.(event);\n }}\n onKeyDown={handleKeyDown}\n />\n <div className={styles.composerActions}>\n {actions}\n <button\n type=\"submit\"\n className={styles.send}\n disabled={disabled || busy || value.trim() === \"\"}\n >\n {sendLabel ?? strings.send}\n </button>\n </div>\n </form>\n );\n});\n"],"mappings":"0JAmEA,IAAa,GAAA,EAAA,EAAA,WAAA,CAAiE,SAC1E,CACI,SACA,SAAS,QACT,UACA,UAAU,EACV,YACA,UACA,YACA,WACA,cACA,YACA,WACA,GAAG,GAEP,EACF,CACE,IAAM,EAAU,EAAA,YAAY,CAAM,EAC5B,GAAA,EAAA,EAAA,OAAA,CAA8C,IAAI,EAClD,CAAC,EAAO,IAAA,EAAA,EAAA,SAAA,CAAqB,EAAE,EAC/B,CAAC,EAAM,IAAA,EAAA,EAAA,SAAA,CAAoB,EAAK,GAEtC,EAAA,EAAA,oBAAA,CAAoB,OAAY,CAC5B,UAAa,EAAS,SAAS,MAAM,EACrC,SAAW,GAAiB,CACxB,EAAS,CAAI,EACb,EAAS,SAAS,MAAM,CAC5B,CACJ,EAAE,GASF,EAAA,EAAA,gBAAA,KAAsB,CAClB,IAAM,EAAO,EAAS,QACtB,GAAI,CAAC,EAAM,OACX,EAAK,MAAM,OAAS,OAEpB,IAAM,GADa,OAAO,WAAW,iBAAiB,CAAI,CAAC,CAAC,UAAU,GAAK,IAClD,EACzB,EAAK,MAAM,OAAS,GAAG,KAAK,IAAI,EAAK,aAAc,CAAG,EAAE,IACxD,EAAK,MAAM,UAAY,EAAK,aAAe,EAAM,OAAS,QAC9D,EAAG,CAAC,EAAO,CAAO,CAAC,EAEnB,IAAM,EAAS,SAA2B,CACtC,IAAM,EAAO,EAAM,KAAK,EACpB,MAAC,GAAQ,GAAQ,GACrB,GAAQ,EAAI,EACZ,GAAI,CACA,MAAM,EAAO,CAAI,EACjB,EAAS,EAAE,CACf,OAAS,EAAO,CACZ,IAAU,CAAK,CACnB,QAAU,CACN,EAAQ,EAAK,CACjB,CARY,CAShB,EAEM,EAAgB,GAA2B,CAC7C,EAAM,eAAe,EACrB,EAAY,CAChB,EASM,EAAiB,GAAoD,CACvE,IAAY,CAAK,EACb,GAAM,mBACN,EAAM,MAAQ,SAAW,EAAM,UAC/B,EAAM,YAAY,aAAe,EAAM,UAAY,MACvD,EAAM,eAAe,EACrB,EAAY,GAChB,EAEA,OACI,EAAA,EAAA,KAAA,CAAC,OAAD,CAAM,UAAW,EAAA,GAAG,EAAA,QAAO,SAAU,CAAS,EAAG,SAAU,WAA3D,EACI,EAAA,EAAA,IAAA,CAAC,WAAD,CACI,GAAI,EACJ,IAAK,EACL,UAAW,EAAA,QAAO,MAClB,KAAM,EACC,QACG,WACV,YAAa,GAAe,EAAQ,YACpC,SAAW,GAAU,CACjB,EAAS,EAAM,OAAO,KAAK,EAC3B,IAAW,CAAK,CACpB,EACA,UAAW,CACd,CAAA,GACD,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,yBAAvB,CACK,GACD,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,KAClB,SAAU,GAAY,GAAQ,EAAM,KAAK,IAAM,YAE9C,GAAa,EAAQ,IAClB,CAAA,CACP,GACH,GAEd,CAAC"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { cn as e } from "../../utils/cn.js";
|
|
2
|
+
import { chatStrings as t } from "./chat-groups.js";
|
|
3
|
+
import n from "./Chat.module.js";
|
|
4
|
+
import { forwardRef as r, useImperativeHandle as i, useLayoutEffect as a, useRef as o, useState as s } from "react";
|
|
5
|
+
import { jsx as c, jsxs as l } from "react/jsx-runtime";
|
|
6
|
+
//#region src/components/Chat/ChatComposer.tsx
|
|
7
|
+
var u = r(function({ onSend: r, locale: u = "pt-BR", actions: d, maxRows: f = 6, sendLabel: p, onError: m, className: h, disabled: g, placeholder: _, onKeyDown: v, onChange: y, ...b }, x) {
|
|
8
|
+
let S = t(u), C = o(null), [w, T] = s(""), [E, D] = s(!1);
|
|
9
|
+
i(x, () => ({
|
|
10
|
+
focus: () => C.current?.focus(),
|
|
11
|
+
setValue: (e) => {
|
|
12
|
+
T(e), C.current?.focus();
|
|
13
|
+
}
|
|
14
|
+
})), a(() => {
|
|
15
|
+
let e = C.current;
|
|
16
|
+
if (!e) return;
|
|
17
|
+
e.style.height = "auto";
|
|
18
|
+
let t = (Number.parseFloat(getComputedStyle(e).lineHeight) || 20) * f;
|
|
19
|
+
e.style.height = `${Math.min(e.scrollHeight, t)}px`, e.style.overflowY = e.scrollHeight > t ? "auto" : "hidden";
|
|
20
|
+
}, [w, f]);
|
|
21
|
+
let O = async () => {
|
|
22
|
+
let e = w.trim();
|
|
23
|
+
if (!(!e || E || g)) {
|
|
24
|
+
D(!0);
|
|
25
|
+
try {
|
|
26
|
+
await r(e), T("");
|
|
27
|
+
} catch (e) {
|
|
28
|
+
m?.(e);
|
|
29
|
+
} finally {
|
|
30
|
+
D(!1);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}, k = (e) => {
|
|
34
|
+
e.preventDefault(), O();
|
|
35
|
+
}, A = (e) => {
|
|
36
|
+
v?.(e), !e.defaultPrevented && (e.key !== "Enter" || e.shiftKey || e.nativeEvent.isComposing || e.keyCode === 229 || (e.preventDefault(), O()));
|
|
37
|
+
};
|
|
38
|
+
return /* @__PURE__ */ l("form", {
|
|
39
|
+
className: e(n.composer, h),
|
|
40
|
+
onSubmit: k,
|
|
41
|
+
children: [/* @__PURE__ */ c("textarea", {
|
|
42
|
+
...b,
|
|
43
|
+
ref: C,
|
|
44
|
+
className: n.field,
|
|
45
|
+
rows: 1,
|
|
46
|
+
value: w,
|
|
47
|
+
disabled: g,
|
|
48
|
+
placeholder: _ ?? S.placeholder,
|
|
49
|
+
onChange: (e) => {
|
|
50
|
+
T(e.target.value), y?.(e);
|
|
51
|
+
},
|
|
52
|
+
onKeyDown: A
|
|
53
|
+
}), /* @__PURE__ */ l("div", {
|
|
54
|
+
className: n.composerActions,
|
|
55
|
+
children: [d, /* @__PURE__ */ c("button", {
|
|
56
|
+
type: "submit",
|
|
57
|
+
className: n.send,
|
|
58
|
+
disabled: g || E || w.trim() === "",
|
|
59
|
+
children: p ?? S.send
|
|
60
|
+
})]
|
|
61
|
+
})]
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
//#endregion
|
|
65
|
+
export { u as ChatComposer };
|
|
66
|
+
|
|
67
|
+
//# sourceMappingURL=ChatComposer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChatComposer.js","names":[],"sources":["../../../src/components/Chat/ChatComposer.tsx"],"sourcesContent":["import {\n forwardRef,\n useImperativeHandle,\n useLayoutEffect,\n useRef,\n useState,\n type FormEvent,\n type KeyboardEvent,\n type ReactNode,\n type TextareaHTMLAttributes,\n} from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { chatStrings } from \"./chat-groups\";\nimport styles from \"./Chat.module.css\";\n\n/** DOM attributes the composer redefines. */\ntype OverriddenDomProps = \"onSubmit\" | \"value\" | \"defaultValue\" | \"rows\";\n\nexport interface ChatComposerProps extends Omit<\n TextareaHTMLAttributes<HTMLTextAreaElement>,\n OverriddenDomProps\n> {\n /** Called with the trimmed text. The field clears only when this does not throw. */\n onSend: (text: string) => void | Promise<void>;\n /** Locale for the placeholder and the send label. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Rendered to the left of the send button — an attach button, an emoji picker. */\n actions?: ReactNode;\n /** Largest height the field grows to, in lines. Default 6. */\n maxRows?: number;\n /** Send label. Defaults to the locale string. */\n sendLabel?: string;\n /**\n * Called when `onSend` rejects. The draft is kept either way.\n *\n * Without it the rejection is swallowed after the draft is preserved, because\n * the alternative is worse: re-throwing out of a DOM event handler surfaces as\n * an unhandled promise rejection — console noise for the developer, a hit in\n * whatever crash reporter the app runs, and nothing the user can act on. The\n * visible signal is the text still sitting in the field; wire this to a toast\n * to say why.\n */\n onError?: (error: unknown) => void;\n}\n\n/** Imperative handle, so a thread can focus the field after retrying a message. */\nexport interface ChatComposerHandle {\n focus: () => void;\n /** Replace the draft — used to put a failed message back in the field. */\n setValue: (text: string) => void;\n}\n\n/**\n * The message field of a thread: a textarea that grows with its content, sends on\n * `Enter` and keeps `Shift+Enter` for a newline.\n *\n * Uncontrolled on purpose. A chat draft changes on every keystroke, and lifting\n * that into app state re-renders the whole thread per character — the one place\n * where \"controlled by default\" costs something visible. Apps that need the draft\n * (a persisted composer, a slash-command menu) read it from `onChange` or drive it\n * through the ref.\n *\n * @example\n * <ChatComposer onSend={(text) => api.post(\"/messages\", { body: { text } })} />\n */\nexport const ChatComposer = forwardRef<ChatComposerHandle, ChatComposerProps>(function ChatComposer(\n {\n onSend,\n locale = \"pt-BR\",\n actions,\n maxRows = 6,\n sendLabel,\n onError,\n className,\n disabled,\n placeholder,\n onKeyDown,\n onChange,\n ...rest\n },\n ref,\n) {\n const strings = chatStrings(locale);\n const textarea = useRef<HTMLTextAreaElement | null>(null);\n const [value, setValue] = useState(\"\");\n const [busy, setBusy] = useState(false);\n\n useImperativeHandle(ref, () => ({\n focus: () => textarea.current?.focus(),\n setValue: (text: string) => {\n setValue(text);\n textarea.current?.focus();\n },\n }));\n\n /**\n * Grow the field to fit its content, up to `maxRows`.\n *\n * Measured from `scrollHeight` after resetting the height, because\n * `scrollHeight` on an element that is already tall enough reports the\n * current height and the field would never shrink back.\n */\n useLayoutEffect(() => {\n const node = textarea.current;\n if (!node) return;\n node.style.height = \"auto\";\n const lineHeight = Number.parseFloat(getComputedStyle(node).lineHeight) || 20;\n const max = lineHeight * maxRows;\n node.style.height = `${Math.min(node.scrollHeight, max)}px`;\n node.style.overflowY = node.scrollHeight > max ? \"auto\" : \"hidden\";\n }, [value, maxRows]);\n\n const submit = async (): Promise<void> => {\n const text = value.trim();\n if (!text || busy || disabled) return;\n setBusy(true);\n try {\n await onSend(text);\n setValue(\"\");\n } catch (error) {\n onError?.(error);\n } finally {\n setBusy(false);\n }\n };\n\n const handleSubmit = (event: FormEvent): void => {\n event.preventDefault();\n void submit();\n };\n\n /**\n * `Enter` sends, `Shift+Enter` breaks the line.\n *\n * The IME check is not optional: while composing Japanese or Korean, `Enter`\n * confirms the candidate word and `keyCode === 229` marks that keystroke.\n * Sending there would post half a word and eat the confirmation.\n */\n const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>): void => {\n onKeyDown?.(event);\n if (event.defaultPrevented) return;\n if (event.key !== \"Enter\" || event.shiftKey) return;\n if (event.nativeEvent.isComposing || event.keyCode === 229) return;\n event.preventDefault();\n void submit();\n };\n\n return (\n <form className={cn(styles.composer, className)} onSubmit={handleSubmit}>\n <textarea\n {...rest}\n ref={textarea}\n className={styles.field}\n rows={1}\n value={value}\n disabled={disabled}\n placeholder={placeholder ?? strings.placeholder}\n onChange={(event) => {\n setValue(event.target.value);\n onChange?.(event);\n }}\n onKeyDown={handleKeyDown}\n />\n <div className={styles.composerActions}>\n {actions}\n <button\n type=\"submit\"\n className={styles.send}\n disabled={disabled || busy || value.trim() === \"\"}\n >\n {sendLabel ?? strings.send}\n </button>\n </div>\n </form>\n );\n});\n"],"mappings":";;;;;;AAmEA,IAAa,IAAe,EAAkD,SAC1E,EACI,WACA,YAAS,SACT,YACA,aAAU,GACV,cACA,YACA,cACA,aACA,gBACA,cACA,aACA,GAAG,KAEP,GACF;CACE,IAAM,IAAU,EAAY,CAAM,GAC5B,IAAW,EAAmC,IAAI,GAClD,CAAC,GAAO,KAAY,EAAS,EAAE,GAC/B,CAAC,GAAM,KAAW,EAAS,EAAK;CAiBtC,AAfA,EAAoB,UAAY;EAC5B,aAAa,EAAS,SAAS,MAAM;EACrC,WAAW,MAAiB;GAExB,AADA,EAAS,CAAI,GACb,EAAS,SAAS,MAAM;EAC5B;CACJ,EAAE,GASF,QAAsB;EAClB,IAAM,IAAO,EAAS;EACtB,IAAI,CAAC,GAAM;EACX,EAAK,MAAM,SAAS;EAEpB,IAAM,KADa,OAAO,WAAW,iBAAiB,CAAI,CAAC,CAAC,UAAU,KAAK,MAClD;EAEzB,AADA,EAAK,MAAM,SAAS,GAAG,KAAK,IAAI,EAAK,cAAc,CAAG,EAAE,KACxD,EAAK,MAAM,YAAY,EAAK,eAAe,IAAM,SAAS;CAC9D,GAAG,CAAC,GAAO,CAAO,CAAC;CAEnB,IAAM,IAAS,YAA2B;EACtC,IAAM,IAAO,EAAM,KAAK;EACpB,OAAC,KAAQ,KAAQ,IACrB;KAAQ,EAAI;GACZ,IAAI;IAEA,AADA,MAAM,EAAO,CAAI,GACjB,EAAS,EAAE;GACf,SAAS,GAAO;IACZ,IAAU,CAAK;GACnB,UAAU;IACN,EAAQ,EAAK;GACjB;EARY;CAShB,GAEM,KAAgB,MAA2B;EAE7C,AADA,EAAM,eAAe,GACrB,EAAY;CAChB,GASM,KAAiB,MAAoD;EACvE,IAAY,CAAK,GACb,GAAM,qBACN,EAAM,QAAQ,WAAW,EAAM,YAC/B,EAAM,YAAY,eAAe,EAAM,YAAY,QACvD,EAAM,eAAe,GACrB,EAAY;CAChB;CAEA,OACI,kBAAC,QAAD;EAAM,WAAW,EAAG,EAAO,UAAU,CAAS;EAAG,UAAU;YAA3D,CACI,kBAAC,YAAD;GACI,GAAI;GACJ,KAAK;GACL,WAAW,EAAO;GAClB,MAAM;GACC;GACG;GACV,aAAa,KAAe,EAAQ;GACpC,WAAW,MAAU;IAEjB,AADA,EAAS,EAAM,OAAO,KAAK,GAC3B,IAAW,CAAK;GACpB;GACA,WAAW;EACd,CAAA,GACD,kBAAC,OAAD;GAAK,WAAW,EAAO;aAAvB,CACK,GACD,kBAAC,UAAD;IACI,MAAK;IACL,WAAW,EAAO;IAClB,UAAU,KAAY,KAAQ,EAAM,KAAK,MAAM;cAE9C,KAAa,EAAQ;GAClB,CAAA,CACP;IACH;;AAEd,CAAC"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var e=300*1e3;function t(e){let t=new Date(e);return t.setHours(0,0,0,0),t.getTime()}function n({messages:n,currentUserId:r,windowMs:i=e}){let a=[],o=null,s=null;for(let e of n){let n=t(e.sentAt);n!==o&&(o=n,s=null,a.push({kind:`day`,key:`day-${n}`,date:n}));let c=s?.messages[s.messages.length-1];if(s!==null&&c!==void 0&&s.authorId===e.authorId&&e.sentAt-c.sentAt<=i&&s){s.messages.push(e);continue}s={kind:`run`,key:`${e.authorId}-${e.id}`,authorId:e.authorId,authorName:e.authorName,own:r!==void 0&&e.authorId===r,messages:[e]},a.push(s)}return a}var r={thread:`Conversa`,today:`Hoje`,yesterday:`Ontem`,you:`Você`,typingOne:e=>`${e} está digitando…`,typingTwo:(e,t)=>`${e} e ${t} estão digitando…`,typingMany:e=>`${e} pessoas estão digitando…`,sending:`Enviando`,sent:`Enviada`,read:`Lida`,failed:`Falhou ao enviar`,retry:`Tentar de novo`,empty:`Nenhuma mensagem ainda`,placeholder:`Escreva uma mensagem`,send:`Enviar`},i={thread:`Conversation`,today:`Today`,yesterday:`Yesterday`,you:`You`,typingOne:e=>`${e} is typing…`,typingTwo:(e,t)=>`${e} and ${t} are typing…`,typingMany:e=>`${e} people are typing…`,sending:`Sending`,sent:`Sent`,read:`Read`,failed:`Failed to send`,retry:`Try again`,empty:`No messages yet`,placeholder:`Write a message`,send:`Send`};function a(e){return e===`en`?i:r}function o(e,{locale:n=`pt-BR`,now:r}={}){let i=a(n),o=t(r??Date.now()),s=1440*60*1e3;return e===o?i.today:e===o-s?i.yesterday:new Date(e).toLocaleDateString(n===`en`?`en-US`:`pt-BR`,{day:`2-digit`,month:`short`,year:e<o-300*s?`numeric`:void 0})}function s(e,t=`pt-BR`){return new Date(e).toLocaleTimeString(t===`en`?`en-US`:`pt-BR`,{hour:`2-digit`,minute:`2-digit`})}function c(e,t=`pt-BR`){let n=a(t);return e.length===0?null:e.length===1?n.typingOne(e[0]):e.length===2?n.typingTwo(e[0],e[1]):n.typingMany(e.length)}exports.DEFAULT_GROUP_WINDOW_MS=e,exports.chatStrings=a,exports.dayLabel=o,exports.groupMessages=n,exports.timeLabel=s,exports.typingLabel=c;
|
|
2
|
+
//# sourceMappingURL=chat-groups.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat-groups.cjs","names":[],"sources":["../../../src/components/Chat/chat-groups.ts"],"sourcesContent":["import type { ReactNode } from \"react\";\n\n/** One entry in a thread. */\nexport interface ChatMessage {\n /** Stable identity. Used as the React key and by `onRetry`. */\n id: string;\n /** What was said. A node, so an app can render a link, an image or a quote. */\n body: ReactNode;\n /** Who said it. Compared against `currentUserId` to decide sides. */\n authorId: string;\n /** Display name. Falls back to `authorId` in the header of a run. */\n authorName?: string;\n /** Epoch milliseconds. */\n sentAt: number;\n /**\n * Delivery state of an outgoing message.\n *\n * `\"failed\"` is the one that matters: without it an app has to invent its own\n * way to say \"this never left\", and the user re-types a message that is\n * sitting right there.\n */\n status?: \"sending\" | \"sent\" | \"read\" | \"failed\";\n /** Anything the app wants to carry through to its own renderers. */\n data?: Record<string, unknown>;\n}\n\n/** A run of consecutive messages from one author, under one day. */\nexport interface ChatRun {\n kind: \"run\";\n /** `${authorId}-${first message id}` — stable across re-renders. */\n key: string;\n authorId: string;\n authorName?: string;\n /** Whether this run belongs to the current user. */\n own: boolean;\n messages: ChatMessage[];\n}\n\n/** A date heading between runs. */\nexport interface ChatDay {\n kind: \"day\";\n key: string;\n /** Midnight of that local day, epoch ms — the label is formatted by the view. */\n date: number;\n}\n\nexport type ChatSection = ChatDay | ChatRun;\n\n/** Default window in which consecutive messages from one author stay in a run. */\nexport const DEFAULT_GROUP_WINDOW_MS = 5 * 60 * 1000;\n\n/** Local midnight of an instant, epoch ms. */\nfunction startOfDay(timestamp: number): number {\n const date = new Date(timestamp);\n date.setHours(0, 0, 0, 0);\n return date.getTime();\n}\n\n/**\n * Turn a flat message list into the sections a thread renders: a date heading\n * whenever the local day changes, and runs of consecutive messages from the same\n * author.\n *\n * Grouping is what makes a thread readable — repeating the avatar and the name on\n * every line of a five-line burst turns a conversation into a list of receipts.\n * The run breaks on a different author, a different day, or a gap longer than\n * `windowMs`: a reply an hour later is a new beat in the conversation even when\n * nobody else spoke, and joining it to the earlier burst would put one timestamp\n * on messages an hour apart.\n *\n * Order is taken as given, oldest first, and never sorted here: a thread that\n * reorders what the server sent would fight optimistic inserts, where the\n * pending message is deliberately last.\n *\n * @param params.messages - Oldest first.\n * @param params.currentUserId - Author id treated as \"own\".\n * @param params.windowMs - Gap that still keeps a run together. Default 5 min.\n * @returns Sections in render order.\n */\nexport function groupMessages({\n messages,\n currentUserId,\n windowMs = DEFAULT_GROUP_WINDOW_MS,\n}: {\n messages: readonly ChatMessage[];\n currentUserId?: string;\n windowMs?: number;\n}): ChatSection[] {\n const sections: ChatSection[] = [];\n let day: number | null = null;\n let run: ChatRun | null = null;\n\n for (const message of messages) {\n const messageDay = startOfDay(message.sentAt);\n if (messageDay !== day) {\n day = messageDay;\n run = null;\n sections.push({ kind: \"day\", key: `day-${messageDay}`, date: messageDay });\n }\n\n const previous = run?.messages[run.messages.length - 1];\n const continues =\n run !== null &&\n previous !== undefined &&\n run.authorId === message.authorId &&\n message.sentAt - previous.sentAt <= windowMs;\n\n if (continues && run) {\n run.messages.push(message);\n continue;\n }\n\n run = {\n kind: \"run\",\n key: `${message.authorId}-${message.id}`,\n authorId: message.authorId,\n authorName: message.authorName,\n own: currentUserId !== undefined && message.authorId === currentUserId,\n messages: [message],\n };\n sections.push(run);\n }\n\n return sections;\n}\n\n/** Labels the thread needs, per locale. */\ninterface ChatStrings {\n thread: string;\n today: string;\n yesterday: string;\n you: string;\n typingOne: (name: string) => string;\n typingTwo: (a: string, b: string) => string;\n typingMany: (n: number) => string;\n sending: string;\n sent: string;\n read: string;\n failed: string;\n retry: string;\n empty: string;\n placeholder: string;\n send: string;\n}\n\nconst PT_BR: ChatStrings = {\n thread: \"Conversa\",\n today: \"Hoje\",\n yesterday: \"Ontem\",\n you: \"Você\",\n typingOne: (name) => `${name} está digitando…`,\n typingTwo: (a, b) => `${a} e ${b} estão digitando…`,\n typingMany: (n) => `${n} pessoas estão digitando…`,\n sending: \"Enviando\",\n sent: \"Enviada\",\n read: \"Lida\",\n failed: \"Falhou ao enviar\",\n retry: \"Tentar de novo\",\n empty: \"Nenhuma mensagem ainda\",\n placeholder: \"Escreva uma mensagem\",\n send: \"Enviar\",\n};\n\nconst EN: ChatStrings = {\n thread: \"Conversation\",\n today: \"Today\",\n yesterday: \"Yesterday\",\n you: \"You\",\n typingOne: (name) => `${name} is typing…`,\n typingTwo: (a, b) => `${a} and ${b} are typing…`,\n typingMany: (n) => `${n} people are typing…`,\n sending: \"Sending\",\n sent: \"Sent\",\n read: \"Read\",\n failed: \"Failed to send\",\n retry: \"Try again\",\n empty: \"No messages yet\",\n placeholder: \"Write a message\",\n send: \"Send\",\n};\n\n/** Locale strings for the thread. */\nexport function chatStrings(locale: \"pt-BR\" | \"en\"): ChatStrings {\n return locale === \"en\" ? EN : PT_BR;\n}\n\n/**\n * Label for a date heading: `\"Hoje\"`, `\"Ontem\"`, or the formatted date.\n *\n * @param date - Local midnight of the day being labelled.\n * @param params.now - Reference instant, so tests and SSR-free renders are stable.\n */\nexport function dayLabel(\n date: number,\n { locale = \"pt-BR\", now }: { locale?: \"pt-BR\" | \"en\"; now?: number } = {},\n): string {\n const strings = chatStrings(locale);\n const today = startOfDay(now ?? Date.now());\n const dayMs = 24 * 60 * 60 * 1000;\n if (date === today) return strings.today;\n if (date === today - dayMs) return strings.yesterday;\n return new Date(date).toLocaleDateString(locale === \"en\" ? \"en-US\" : \"pt-BR\", {\n day: \"2-digit\",\n month: \"short\",\n year: date < today - 300 * dayMs ? \"numeric\" : undefined,\n });\n}\n\n/** Clock label for a single message — the time, not a relative phrase. */\nexport function timeLabel(timestamp: number, locale: \"pt-BR\" | \"en\" = \"pt-BR\"): string {\n return new Date(timestamp).toLocaleTimeString(locale === \"en\" ? \"en-US\" : \"pt-BR\", {\n hour: \"2-digit\",\n minute: \"2-digit\",\n });\n}\n\n/** Sentence for the typing indicator, or `null` when nobody is typing. */\nexport function typingLabel(\n names: readonly string[],\n locale: \"pt-BR\" | \"en\" = \"pt-BR\",\n): string | null {\n const strings = chatStrings(locale);\n if (names.length === 0) return null;\n if (names.length === 1) return strings.typingOne(names[0]);\n if (names.length === 2) return strings.typingTwo(names[0], names[1]);\n return strings.typingMany(names.length);\n}\n"],"mappings":"AAiDA,IAAa,EAA0B,IAAS,IAGhD,SAAS,EAAW,EAA2B,CAC3C,IAAM,EAAO,IAAI,KAAK,CAAS,EAE/B,OADA,EAAK,SAAS,EAAG,EAAG,EAAG,CAAC,EACjB,EAAK,QAAQ,CACxB,CAuBA,SAAgB,EAAc,CAC1B,WACA,gBACA,WAAW,GAKG,CACd,IAAM,EAA0B,CAAC,EAC7B,EAAqB,KACrB,EAAsB,KAE1B,IAAK,IAAM,KAAW,EAAU,CAC5B,IAAM,EAAa,EAAW,EAAQ,MAAM,EACxC,IAAe,IACf,EAAM,EACN,EAAM,KACN,EAAS,KAAK,CAAE,KAAM,MAAO,IAAK,OAAO,IAAc,KAAM,CAAW,CAAC,GAG7E,IAAM,EAAW,GAAK,SAAS,EAAI,SAAS,OAAS,GAOrD,GALI,IAAQ,MACR,IAAa,IAAA,IACb,EAAI,WAAa,EAAQ,UACzB,EAAQ,OAAS,EAAS,QAAU,GAEvB,EAAK,CAClB,EAAI,SAAS,KAAK,CAAO,EACzB,QACJ,CAEA,EAAM,CACF,KAAM,MACN,IAAK,GAAG,EAAQ,SAAS,GAAG,EAAQ,KACpC,SAAU,EAAQ,SAClB,WAAY,EAAQ,WACpB,IAAK,IAAkB,IAAA,IAAa,EAAQ,WAAa,EACzD,SAAU,CAAC,CAAO,CACtB,EACA,EAAS,KAAK,CAAG,CACrB,CAEA,OAAO,CACX,CAqBA,IAAM,EAAqB,CACvB,OAAQ,WACR,MAAO,OACP,UAAW,QACX,IAAK,OACL,UAAY,GAAS,GAAG,EAAK,kBAC7B,WAAY,EAAG,IAAM,GAAG,EAAE,KAAK,EAAE,mBACjC,WAAa,GAAM,GAAG,EAAE,2BACxB,QAAS,WACT,KAAM,UACN,KAAM,OACN,OAAQ,mBACR,MAAO,iBACP,MAAO,yBACP,YAAa,uBACb,KAAM,QACV,EAEM,EAAkB,CACpB,OAAQ,eACR,MAAO,QACP,UAAW,YACX,IAAK,MACL,UAAY,GAAS,GAAG,EAAK,aAC7B,WAAY,EAAG,IAAM,GAAG,EAAE,OAAO,EAAE,cACnC,WAAa,GAAM,GAAG,EAAE,qBACxB,QAAS,UACT,KAAM,OACN,KAAM,OACN,OAAQ,iBACR,MAAO,YACP,MAAO,kBACP,YAAa,kBACb,KAAM,MACV,EAGA,SAAgB,EAAY,EAAqC,CAC7D,OAAO,IAAW,KAAO,EAAK,CAClC,CAQA,SAAgB,EACZ,EACA,CAAE,SAAS,QAAS,OAAmD,CAAC,EAClE,CACN,IAAM,EAAU,EAAY,CAAM,EAC5B,EAAQ,EAAW,GAAO,KAAK,IAAI,CAAC,EACpC,EAAQ,KAAU,GAAK,IAG7B,OAFI,IAAS,EAAc,EAAQ,MAC/B,IAAS,EAAQ,EAAc,EAAQ,UACpC,IAAI,KAAK,CAAI,CAAC,CAAC,mBAAmB,IAAW,KAAO,QAAU,QAAS,CAC1E,IAAK,UACL,MAAO,QACP,KAAM,EAAO,EAAQ,IAAM,EAAQ,UAAY,IAAA,EACnD,CAAC,CACL,CAGA,SAAgB,EAAU,EAAmB,EAAyB,QAAiB,CACnF,OAAO,IAAI,KAAK,CAAS,CAAC,CAAC,mBAAmB,IAAW,KAAO,QAAU,QAAS,CAC/E,KAAM,UACN,OAAQ,SACZ,CAAC,CACL,CAGA,SAAgB,EACZ,EACA,EAAyB,QACZ,CACb,IAAM,EAAU,EAAY,CAAM,EAIlC,OAHI,EAAM,SAAW,EAAU,KAC3B,EAAM,SAAW,EAAU,EAAQ,UAAU,EAAM,EAAE,EACrD,EAAM,SAAW,EAAU,EAAQ,UAAU,EAAM,GAAI,EAAM,EAAE,EAC5D,EAAQ,WAAW,EAAM,MAAM,CAC1C"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
//#region src/components/Chat/chat-groups.ts
|
|
2
|
+
var e = 300 * 1e3;
|
|
3
|
+
function t(e) {
|
|
4
|
+
let t = new Date(e);
|
|
5
|
+
return t.setHours(0, 0, 0, 0), t.getTime();
|
|
6
|
+
}
|
|
7
|
+
function n({ messages: n, currentUserId: r, windowMs: i = e }) {
|
|
8
|
+
let a = [], o = null, s = null;
|
|
9
|
+
for (let e of n) {
|
|
10
|
+
let n = t(e.sentAt);
|
|
11
|
+
n !== o && (o = n, s = null, a.push({
|
|
12
|
+
kind: "day",
|
|
13
|
+
key: `day-${n}`,
|
|
14
|
+
date: n
|
|
15
|
+
}));
|
|
16
|
+
let c = s?.messages[s.messages.length - 1];
|
|
17
|
+
if (s !== null && c !== void 0 && s.authorId === e.authorId && e.sentAt - c.sentAt <= i && s) {
|
|
18
|
+
s.messages.push(e);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
s = {
|
|
22
|
+
kind: "run",
|
|
23
|
+
key: `${e.authorId}-${e.id}`,
|
|
24
|
+
authorId: e.authorId,
|
|
25
|
+
authorName: e.authorName,
|
|
26
|
+
own: r !== void 0 && e.authorId === r,
|
|
27
|
+
messages: [e]
|
|
28
|
+
}, a.push(s);
|
|
29
|
+
}
|
|
30
|
+
return a;
|
|
31
|
+
}
|
|
32
|
+
var r = {
|
|
33
|
+
thread: "Conversa",
|
|
34
|
+
today: "Hoje",
|
|
35
|
+
yesterday: "Ontem",
|
|
36
|
+
you: "Você",
|
|
37
|
+
typingOne: (e) => `${e} está digitando…`,
|
|
38
|
+
typingTwo: (e, t) => `${e} e ${t} estão digitando…`,
|
|
39
|
+
typingMany: (e) => `${e} pessoas estão digitando…`,
|
|
40
|
+
sending: "Enviando",
|
|
41
|
+
sent: "Enviada",
|
|
42
|
+
read: "Lida",
|
|
43
|
+
failed: "Falhou ao enviar",
|
|
44
|
+
retry: "Tentar de novo",
|
|
45
|
+
empty: "Nenhuma mensagem ainda",
|
|
46
|
+
placeholder: "Escreva uma mensagem",
|
|
47
|
+
send: "Enviar"
|
|
48
|
+
}, i = {
|
|
49
|
+
thread: "Conversation",
|
|
50
|
+
today: "Today",
|
|
51
|
+
yesterday: "Yesterday",
|
|
52
|
+
you: "You",
|
|
53
|
+
typingOne: (e) => `${e} is typing…`,
|
|
54
|
+
typingTwo: (e, t) => `${e} and ${t} are typing…`,
|
|
55
|
+
typingMany: (e) => `${e} people are typing…`,
|
|
56
|
+
sending: "Sending",
|
|
57
|
+
sent: "Sent",
|
|
58
|
+
read: "Read",
|
|
59
|
+
failed: "Failed to send",
|
|
60
|
+
retry: "Try again",
|
|
61
|
+
empty: "No messages yet",
|
|
62
|
+
placeholder: "Write a message",
|
|
63
|
+
send: "Send"
|
|
64
|
+
};
|
|
65
|
+
function a(e) {
|
|
66
|
+
return e === "en" ? i : r;
|
|
67
|
+
}
|
|
68
|
+
function o(e, { locale: n = "pt-BR", now: r } = {}) {
|
|
69
|
+
let i = a(n), o = t(r ?? Date.now()), s = 1440 * 60 * 1e3;
|
|
70
|
+
return e === o ? i.today : e === o - s ? i.yesterday : new Date(e).toLocaleDateString(n === "en" ? "en-US" : "pt-BR", {
|
|
71
|
+
day: "2-digit",
|
|
72
|
+
month: "short",
|
|
73
|
+
year: e < o - 300 * s ? "numeric" : void 0
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function s(e, t = "pt-BR") {
|
|
77
|
+
return new Date(e).toLocaleTimeString(t === "en" ? "en-US" : "pt-BR", {
|
|
78
|
+
hour: "2-digit",
|
|
79
|
+
minute: "2-digit"
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function c(e, t = "pt-BR") {
|
|
83
|
+
let n = a(t);
|
|
84
|
+
return e.length === 0 ? null : e.length === 1 ? n.typingOne(e[0]) : e.length === 2 ? n.typingTwo(e[0], e[1]) : n.typingMany(e.length);
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
export { e as DEFAULT_GROUP_WINDOW_MS, a as chatStrings, o as dayLabel, n as groupMessages, s as timeLabel, c as typingLabel };
|
|
88
|
+
|
|
89
|
+
//# sourceMappingURL=chat-groups.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat-groups.js","names":[],"sources":["../../../src/components/Chat/chat-groups.ts"],"sourcesContent":["import type { ReactNode } from \"react\";\n\n/** One entry in a thread. */\nexport interface ChatMessage {\n /** Stable identity. Used as the React key and by `onRetry`. */\n id: string;\n /** What was said. A node, so an app can render a link, an image or a quote. */\n body: ReactNode;\n /** Who said it. Compared against `currentUserId` to decide sides. */\n authorId: string;\n /** Display name. Falls back to `authorId` in the header of a run. */\n authorName?: string;\n /** Epoch milliseconds. */\n sentAt: number;\n /**\n * Delivery state of an outgoing message.\n *\n * `\"failed\"` is the one that matters: without it an app has to invent its own\n * way to say \"this never left\", and the user re-types a message that is\n * sitting right there.\n */\n status?: \"sending\" | \"sent\" | \"read\" | \"failed\";\n /** Anything the app wants to carry through to its own renderers. */\n data?: Record<string, unknown>;\n}\n\n/** A run of consecutive messages from one author, under one day. */\nexport interface ChatRun {\n kind: \"run\";\n /** `${authorId}-${first message id}` — stable across re-renders. */\n key: string;\n authorId: string;\n authorName?: string;\n /** Whether this run belongs to the current user. */\n own: boolean;\n messages: ChatMessage[];\n}\n\n/** A date heading between runs. */\nexport interface ChatDay {\n kind: \"day\";\n key: string;\n /** Midnight of that local day, epoch ms — the label is formatted by the view. */\n date: number;\n}\n\nexport type ChatSection = ChatDay | ChatRun;\n\n/** Default window in which consecutive messages from one author stay in a run. */\nexport const DEFAULT_GROUP_WINDOW_MS = 5 * 60 * 1000;\n\n/** Local midnight of an instant, epoch ms. */\nfunction startOfDay(timestamp: number): number {\n const date = new Date(timestamp);\n date.setHours(0, 0, 0, 0);\n return date.getTime();\n}\n\n/**\n * Turn a flat message list into the sections a thread renders: a date heading\n * whenever the local day changes, and runs of consecutive messages from the same\n * author.\n *\n * Grouping is what makes a thread readable — repeating the avatar and the name on\n * every line of a five-line burst turns a conversation into a list of receipts.\n * The run breaks on a different author, a different day, or a gap longer than\n * `windowMs`: a reply an hour later is a new beat in the conversation even when\n * nobody else spoke, and joining it to the earlier burst would put one timestamp\n * on messages an hour apart.\n *\n * Order is taken as given, oldest first, and never sorted here: a thread that\n * reorders what the server sent would fight optimistic inserts, where the\n * pending message is deliberately last.\n *\n * @param params.messages - Oldest first.\n * @param params.currentUserId - Author id treated as \"own\".\n * @param params.windowMs - Gap that still keeps a run together. Default 5 min.\n * @returns Sections in render order.\n */\nexport function groupMessages({\n messages,\n currentUserId,\n windowMs = DEFAULT_GROUP_WINDOW_MS,\n}: {\n messages: readonly ChatMessage[];\n currentUserId?: string;\n windowMs?: number;\n}): ChatSection[] {\n const sections: ChatSection[] = [];\n let day: number | null = null;\n let run: ChatRun | null = null;\n\n for (const message of messages) {\n const messageDay = startOfDay(message.sentAt);\n if (messageDay !== day) {\n day = messageDay;\n run = null;\n sections.push({ kind: \"day\", key: `day-${messageDay}`, date: messageDay });\n }\n\n const previous = run?.messages[run.messages.length - 1];\n const continues =\n run !== null &&\n previous !== undefined &&\n run.authorId === message.authorId &&\n message.sentAt - previous.sentAt <= windowMs;\n\n if (continues && run) {\n run.messages.push(message);\n continue;\n }\n\n run = {\n kind: \"run\",\n key: `${message.authorId}-${message.id}`,\n authorId: message.authorId,\n authorName: message.authorName,\n own: currentUserId !== undefined && message.authorId === currentUserId,\n messages: [message],\n };\n sections.push(run);\n }\n\n return sections;\n}\n\n/** Labels the thread needs, per locale. */\ninterface ChatStrings {\n thread: string;\n today: string;\n yesterday: string;\n you: string;\n typingOne: (name: string) => string;\n typingTwo: (a: string, b: string) => string;\n typingMany: (n: number) => string;\n sending: string;\n sent: string;\n read: string;\n failed: string;\n retry: string;\n empty: string;\n placeholder: string;\n send: string;\n}\n\nconst PT_BR: ChatStrings = {\n thread: \"Conversa\",\n today: \"Hoje\",\n yesterday: \"Ontem\",\n you: \"Você\",\n typingOne: (name) => `${name} está digitando…`,\n typingTwo: (a, b) => `${a} e ${b} estão digitando…`,\n typingMany: (n) => `${n} pessoas estão digitando…`,\n sending: \"Enviando\",\n sent: \"Enviada\",\n read: \"Lida\",\n failed: \"Falhou ao enviar\",\n retry: \"Tentar de novo\",\n empty: \"Nenhuma mensagem ainda\",\n placeholder: \"Escreva uma mensagem\",\n send: \"Enviar\",\n};\n\nconst EN: ChatStrings = {\n thread: \"Conversation\",\n today: \"Today\",\n yesterday: \"Yesterday\",\n you: \"You\",\n typingOne: (name) => `${name} is typing…`,\n typingTwo: (a, b) => `${a} and ${b} are typing…`,\n typingMany: (n) => `${n} people are typing…`,\n sending: \"Sending\",\n sent: \"Sent\",\n read: \"Read\",\n failed: \"Failed to send\",\n retry: \"Try again\",\n empty: \"No messages yet\",\n placeholder: \"Write a message\",\n send: \"Send\",\n};\n\n/** Locale strings for the thread. */\nexport function chatStrings(locale: \"pt-BR\" | \"en\"): ChatStrings {\n return locale === \"en\" ? EN : PT_BR;\n}\n\n/**\n * Label for a date heading: `\"Hoje\"`, `\"Ontem\"`, or the formatted date.\n *\n * @param date - Local midnight of the day being labelled.\n * @param params.now - Reference instant, so tests and SSR-free renders are stable.\n */\nexport function dayLabel(\n date: number,\n { locale = \"pt-BR\", now }: { locale?: \"pt-BR\" | \"en\"; now?: number } = {},\n): string {\n const strings = chatStrings(locale);\n const today = startOfDay(now ?? Date.now());\n const dayMs = 24 * 60 * 60 * 1000;\n if (date === today) return strings.today;\n if (date === today - dayMs) return strings.yesterday;\n return new Date(date).toLocaleDateString(locale === \"en\" ? \"en-US\" : \"pt-BR\", {\n day: \"2-digit\",\n month: \"short\",\n year: date < today - 300 * dayMs ? \"numeric\" : undefined,\n });\n}\n\n/** Clock label for a single message — the time, not a relative phrase. */\nexport function timeLabel(timestamp: number, locale: \"pt-BR\" | \"en\" = \"pt-BR\"): string {\n return new Date(timestamp).toLocaleTimeString(locale === \"en\" ? \"en-US\" : \"pt-BR\", {\n hour: \"2-digit\",\n minute: \"2-digit\",\n });\n}\n\n/** Sentence for the typing indicator, or `null` when nobody is typing. */\nexport function typingLabel(\n names: readonly string[],\n locale: \"pt-BR\" | \"en\" = \"pt-BR\",\n): string | null {\n const strings = chatStrings(locale);\n if (names.length === 0) return null;\n if (names.length === 1) return strings.typingOne(names[0]);\n if (names.length === 2) return strings.typingTwo(names[0], names[1]);\n return strings.typingMany(names.length);\n}\n"],"mappings":";AAiDA,IAAa,IAA0B,MAAS;AAGhD,SAAS,EAAW,GAA2B;CAC3C,IAAM,IAAO,IAAI,KAAK,CAAS;CAE/B,OADA,EAAK,SAAS,GAAG,GAAG,GAAG,CAAC,GACjB,EAAK,QAAQ;AACxB;AAuBA,SAAgB,EAAc,EAC1B,aACA,kBACA,cAAW,KAKG;CACd,IAAM,IAA0B,CAAC,GAC7B,IAAqB,MACrB,IAAsB;CAE1B,KAAK,IAAM,KAAW,GAAU;EAC5B,IAAM,IAAa,EAAW,EAAQ,MAAM;EAC5C,AAAI,MAAe,MACf,IAAM,GACN,IAAM,MACN,EAAS,KAAK;GAAE,MAAM;GAAO,KAAK,OAAO;GAAc,MAAM;EAAW,CAAC;EAG7E,IAAM,IAAW,GAAK,SAAS,EAAI,SAAS,SAAS;EAOrD,IALI,MAAQ,QACR,MAAa,KAAA,KACb,EAAI,aAAa,EAAQ,YACzB,EAAQ,SAAS,EAAS,UAAU,KAEvB,GAAK;GAClB,EAAI,SAAS,KAAK,CAAO;GACzB;EACJ;EAUA,AARA,IAAM;GACF,MAAM;GACN,KAAK,GAAG,EAAQ,SAAS,GAAG,EAAQ;GACpC,UAAU,EAAQ;GAClB,YAAY,EAAQ;GACpB,KAAK,MAAkB,KAAA,KAAa,EAAQ,aAAa;GACzD,UAAU,CAAC,CAAO;EACtB,GACA,EAAS,KAAK,CAAG;CACrB;CAEA,OAAO;AACX;AAqBA,IAAM,IAAqB;CACvB,QAAQ;CACR,OAAO;CACP,WAAW;CACX,KAAK;CACL,YAAY,MAAS,GAAG,EAAK;CAC7B,YAAY,GAAG,MAAM,GAAG,EAAE,KAAK,EAAE;CACjC,aAAa,MAAM,GAAG,EAAE;CACxB,SAAS;CACT,MAAM;CACN,MAAM;CACN,QAAQ;CACR,OAAO;CACP,OAAO;CACP,aAAa;CACb,MAAM;AACV,GAEM,IAAkB;CACpB,QAAQ;CACR,OAAO;CACP,WAAW;CACX,KAAK;CACL,YAAY,MAAS,GAAG,EAAK;CAC7B,YAAY,GAAG,MAAM,GAAG,EAAE,OAAO,EAAE;CACnC,aAAa,MAAM,GAAG,EAAE;CACxB,SAAS;CACT,MAAM;CACN,MAAM;CACN,QAAQ;CACR,OAAO;CACP,OAAO;CACP,aAAa;CACb,MAAM;AACV;AAGA,SAAgB,EAAY,GAAqC;CAC7D,OAAO,MAAW,OAAO,IAAK;AAClC;AAQA,SAAgB,EACZ,GACA,EAAE,YAAS,SAAS,WAAmD,CAAC,GAClE;CACN,IAAM,IAAU,EAAY,CAAM,GAC5B,IAAQ,EAAW,KAAO,KAAK,IAAI,CAAC,GACpC,IAAQ,OAAU,KAAK;CAG7B,OAFI,MAAS,IAAc,EAAQ,QAC/B,MAAS,IAAQ,IAAc,EAAQ,YACpC,IAAI,KAAK,CAAI,CAAC,CAAC,mBAAmB,MAAW,OAAO,UAAU,SAAS;EAC1E,KAAK;EACL,OAAO;EACP,MAAM,IAAO,IAAQ,MAAM,IAAQ,YAAY,KAAA;CACnD,CAAC;AACL;AAGA,SAAgB,EAAU,GAAmB,IAAyB,SAAiB;CACnF,OAAO,IAAI,KAAK,CAAS,CAAC,CAAC,mBAAmB,MAAW,OAAO,UAAU,SAAS;EAC/E,MAAM;EACN,QAAQ;CACZ,CAAC;AACL;AAGA,SAAgB,EACZ,GACA,IAAyB,SACZ;CACb,IAAM,IAAU,EAAY,CAAM;CAIlC,OAHI,EAAM,WAAW,IAAU,OAC3B,EAAM,WAAW,IAAU,EAAQ,UAAU,EAAM,EAAE,IACrD,EAAM,WAAW,IAAU,EAAQ,UAAU,EAAM,IAAI,EAAM,EAAE,IAC5D,EAAQ,WAAW,EAAM,MAAM;AAC1C"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("../../utils/cn.cjs"),t=require("../VisuallyHidden/VisuallyHidden.cjs"),n=require("./filter-model.cjs"),r=require("./FilterBar.module.cjs");let i=require("react"),a=require("react/jsx-runtime");function o(e){return{field:e.name,operator:n.defaultOperator(e),value:``,to:``}}function s(e){return n.isValueless(e.operator)?{field:e.field,operator:e.operator}:e.operator===`between`?{field:e.field,operator:e.operator,value:[e.value,e.to]}:n.isMulti(e.operator)?{field:e.field,operator:e.operator,value:e.value.split(`,`).map(e=>e.trim()).filter(Boolean)}:{field:e.field,operator:e.operator,value:e.value}}function c({fields:c,value:u,onChange:d,locale:f=`pt-BR`,actions:p,className:m,...h}){let g=n.filterStrings(f),_=(0,i.useId)(),[v,y]=(0,i.useState)(null),b=v?c.find(e=>e.name===v.field):void 0,x=v?s(v):null,S=x!==null&&n.isComplete(x),C=()=>{c.length>0&&y(o(c[0]))},w=()=>{!x||!S||(d([...u,x]),y(null))},T=e=>{d(u.filter((t,n)=>n!==e))};return(0,a.jsxs)(`div`,{className:e.cn(r.default.bar,m),...h,children:[(0,a.jsxs)(`ul`,{className:r.default.chips,children:[u.length===0&&!v&&(0,a.jsx)(`li`,{className:r.default.empty,children:g.empty}),u.map((e,t)=>{let i=n.describeFilter(e,c,f);return(0,a.jsxs)(`li`,{className:r.default.chip,children:[(0,a.jsx)(`span`,{children:i}),(0,a.jsx)(`button`,{type:`button`,className:r.default.remove,"aria-label":g.remove(i),onClick:()=>T(t),children:`×`})]},`${e.field}-${e.operator}-${t}`)})]}),v&&b?(0,a.jsxs)(`div`,{className:r.default.editor,role:`group`,"aria-label":g.add,children:[(0,a.jsxs)(`label`,{className:r.default.control,children:[(0,a.jsx)(t.VisuallyHidden,{children:g.field}),(0,a.jsx)(`select`,{className:r.default.select,value:v.field,onChange:e=>{let t=c.find(t=>t.name===e.target.value);t&&y(o(t))},children:c.map(e=>(0,a.jsx)(`option`,{value:e.name,children:e.label},e.name))})]}),(0,a.jsxs)(`label`,{className:r.default.control,children:[(0,a.jsx)(t.VisuallyHidden,{children:g.operator}),(0,a.jsx)(`select`,{className:r.default.select,value:v.operator,onChange:e=>y({...v,operator:e.target.value,value:``,to:``}),children:n.operatorsFor(b).map(e=>(0,a.jsx)(`option`,{value:e,children:n.operatorLabel(e,f)},e))})]}),!n.isValueless(v.operator)&&(0,a.jsx)(l,{id:`${_}-value`,field:b,draft:v,strings:g,onChange:y}),(0,a.jsx)(`button`,{type:`button`,className:r.default.primary,disabled:!S,onClick:w,children:g.apply}),(0,a.jsx)(`button`,{type:`button`,className:r.default.ghost,onClick:()=>y(null),children:g.cancel})]}):(0,a.jsxs)(`div`,{className:r.default.actions,children:[(0,a.jsxs)(`button`,{type:`button`,className:r.default.secondary,disabled:c.length===0,onClick:C,children:[`+ `,g.add]}),u.length>0&&(0,a.jsx)(`button`,{type:`button`,className:r.default.ghost,onClick:()=>d([]),children:g.clear}),p]}),(0,a.jsx)(t.VisuallyHidden,{"aria-live":`polite`,role:`status`,children:g.active(u.length)})]})}function l({id:e,field:i,draft:o,strings:s,onChange:c}){let l=i.type===`number`?`number`:i.type===`date`?`date`:`text`;return i.type===`select`&&!n.isMulti(o.operator)?(0,a.jsxs)(`label`,{className:r.default.control,children:[(0,a.jsx)(t.VisuallyHidden,{children:s.value}),(0,a.jsxs)(`select`,{id:e,className:r.default.select,value:o.value,onChange:e=>c({...o,value:e.target.value}),children:[(0,a.jsx)(`option`,{value:``,children:`—`}),i.options?.map(e=>(0,a.jsx)(`option`,{value:e.value,children:e.label},e.value))]})]}):i.type===`boolean`?(0,a.jsxs)(`label`,{className:r.default.control,children:[(0,a.jsx)(t.VisuallyHidden,{children:s.value}),(0,a.jsxs)(`select`,{id:e,className:r.default.select,value:o.value,onChange:e=>c({...o,value:e.target.value}),children:[(0,a.jsx)(`option`,{value:``,children:`—`}),(0,a.jsx)(`option`,{value:`true`,children:s.yes}),(0,a.jsx)(`option`,{value:`false`,children:s.no})]})]}):o.operator===`between`?(0,a.jsxs)(a.Fragment,{children:[(0,a.jsxs)(`label`,{className:r.default.control,children:[(0,a.jsx)(t.VisuallyHidden,{children:s.from}),(0,a.jsx)(`input`,{id:e,className:r.default.input,type:l,value:o.value,placeholder:s.from,onChange:e=>c({...o,value:e.target.value})})]}),(0,a.jsxs)(`label`,{className:r.default.control,children:[(0,a.jsx)(t.VisuallyHidden,{children:s.to}),(0,a.jsx)(`input`,{className:r.default.input,type:l,value:o.to,placeholder:s.to,onChange:e=>c({...o,to:e.target.value})})]})]}):(0,a.jsxs)(`label`,{className:r.default.control,children:[(0,a.jsx)(t.VisuallyHidden,{children:s.value}),(0,a.jsx)(`input`,{id:e,className:r.default.input,type:l,value:o.value,placeholder:i.placeholder??s.value,onChange:e=>c({...o,value:e.target.value})})]})}exports.FilterBar=c;
|
|
2
|
+
//# sourceMappingURL=FilterBar.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FilterBar.cjs","names":[],"sources":["../../../src/components/FilterBar/FilterBar.tsx"],"sourcesContent":["import { useId, useState, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { VisuallyHidden } from \"../VisuallyHidden\";\nimport {\n defaultOperator,\n describeFilter,\n filterStrings,\n isComplete,\n isMulti,\n isValueless,\n operatorLabel,\n operatorsFor,\n type Filter,\n type FilterField,\n} from \"./filter-model\";\nimport styles from \"./FilterBar.module.css\";\n\n/** DOM attributes this component redefines. */\ntype OverriddenDomProps = \"children\" | \"onChange\";\n\nexport interface FilterBarProps extends Omit<HTMLAttributes<HTMLDivElement>, OverriddenDomProps> {\n /** Fields the user may filter by. */\n fields: readonly FilterField[];\n /** Applied filters. Controlled. */\n value: readonly Filter[];\n /** Next filter set. Combined with AND. */\n onChange: (filters: Filter[]) => void;\n /** Locale for the labels. Default `\"pt-BR\"`. */\n locale?: \"pt-BR\" | \"en\";\n /** Rendered at the end of the bar — a \"save this view\" button, a counter. */\n actions?: ReactNode;\n}\n\n/** The draft filter being built in the editor. */\ninterface Draft {\n field: string;\n operator: Filter[\"operator\"];\n value: string;\n to: string;\n}\n\n/** A fresh draft for a field. */\nfunction draftFor(field: FilterField): Draft {\n return { field: field.name, operator: defaultOperator(field), value: \"\", to: \"\" };\n}\n\n/** Turn a draft into a filter. */\nfunction toFilter(draft: Draft): Filter {\n if (isValueless(draft.operator)) return { field: draft.field, operator: draft.operator };\n if (draft.operator === \"between\") {\n return { field: draft.field, operator: draft.operator, value: [draft.value, draft.to] };\n }\n if (isMulti(draft.operator)) {\n return {\n field: draft.field,\n operator: draft.operator,\n value: draft.value\n .split(\",\")\n .map((part) => part.trim())\n .filter(Boolean),\n };\n }\n return { field: draft.field, operator: draft.operator, value: draft.value };\n}\n\n/**\n * A row of applied filters, and a small editor to add another.\n *\n * Filters are combined with **AND**, flat — no nested `(a OR b) AND c` groups. That\n * is the shape of nearly every admin list (\"status is paid, created after March,\n * text contains nota\"), and it is what serializes into a URL somebody can send to a\n * colleague. Nested groups need a tree UI with per-node operators and a different\n * serialization; a component trying to be both is clumsy at the common case.\n *\n * Controlled, like the rest of the SDK: it holds the half-typed draft in the editor\n * and nothing else. The applied set is the app's, which is what lets it come from —\n * and go back into — the URL.\n *\n * @example\n * const [filters, setFilters] = useState<Filter[]>(() =>\n * filtersFromSearchParams(new URLSearchParams(location.search), FIELDS),\n * );\n *\n * <FilterBar fields={FIELDS} value={filters} onChange={setFilters} />\n */\nexport function FilterBar({\n fields,\n value,\n onChange,\n locale = \"pt-BR\",\n actions,\n className,\n ...rest\n}: FilterBarProps) {\n const strings = filterStrings(locale);\n const baseId = useId();\n const [draft, setDraft] = useState<Draft | null>(null);\n\n const field = draft ? fields.find((candidate) => candidate.name === draft.field) : undefined;\n const pending = draft ? toFilter(draft) : null;\n const ready = pending !== null && isComplete(pending);\n\n const open = (): void => {\n if (fields.length > 0) setDraft(draftFor(fields[0]));\n };\n\n const apply = (): void => {\n if (!pending || !ready) return;\n onChange([...value, pending]);\n setDraft(null);\n };\n\n const removeAt = (index: number): void => {\n onChange(value.filter((_, position) => position !== index));\n };\n\n return (\n <div className={cn(styles.bar, className)} {...rest}>\n <ul className={styles.chips}>\n {value.length === 0 && !draft && <li className={styles.empty}>{strings.empty}</li>}\n {value.map((filter, index) => {\n const description = describeFilter(filter, fields, locale);\n return (\n <li\n key={`${filter.field}-${filter.operator}-${index}`}\n className={styles.chip}\n >\n <span>{description}</span>\n <button\n type=\"button\"\n className={styles.remove}\n aria-label={strings.remove(description)}\n onClick={() => removeAt(index)}\n >\n ×\n </button>\n </li>\n );\n })}\n </ul>\n\n {draft && field ? (\n <div className={styles.editor} role=\"group\" aria-label={strings.add}>\n <label className={styles.control}>\n <VisuallyHidden>{strings.field}</VisuallyHidden>\n <select\n className={styles.select}\n value={draft.field}\n onChange={(event) => {\n const next = fields.find(\n (candidate) => candidate.name === event.target.value,\n );\n if (next) setDraft(draftFor(next));\n }}\n >\n {fields.map((candidate) => (\n <option key={candidate.name} value={candidate.name}>\n {candidate.label}\n </option>\n ))}\n </select>\n </label>\n\n <label className={styles.control}>\n <VisuallyHidden>{strings.operator}</VisuallyHidden>\n <select\n className={styles.select}\n value={draft.operator}\n onChange={(event) =>\n setDraft({\n ...draft,\n operator: event.target.value as Filter[\"operator\"],\n value: \"\",\n to: \"\",\n })\n }\n >\n {operatorsFor(field).map((operator) => (\n <option key={operator} value={operator}>\n {operatorLabel(operator, locale)}\n </option>\n ))}\n </select>\n </label>\n\n {!isValueless(draft.operator) && (\n <ValueInput\n id={`${baseId}-value`}\n field={field}\n draft={draft}\n strings={strings}\n onChange={setDraft}\n />\n )}\n\n <button\n type=\"button\"\n className={styles.primary}\n disabled={!ready}\n onClick={apply}\n >\n {strings.apply}\n </button>\n <button type=\"button\" className={styles.ghost} onClick={() => setDraft(null)}>\n {strings.cancel}\n </button>\n </div>\n ) : (\n <div className={styles.actions}>\n <button\n type=\"button\"\n className={styles.secondary}\n disabled={fields.length === 0}\n onClick={open}\n >\n + {strings.add}\n </button>\n {value.length > 0 && (\n <button type=\"button\" className={styles.ghost} onClick={() => onChange([])}>\n {strings.clear}\n </button>\n )}\n {actions}\n </div>\n )}\n\n <VisuallyHidden aria-live=\"polite\" role=\"status\">\n {strings.active(value.length)}\n </VisuallyHidden>\n </div>\n );\n}\n\n/**\n * The value input for the draft.\n *\n * The input type follows the **field**, not the operator: a date field gets a date\n * picker even under `between`, where two of them are needed. Typing a date into a\n * text box is the fastest way to produce a filter the backend cannot parse.\n */\nfunction ValueInput({\n id,\n field,\n draft,\n strings,\n onChange,\n}: {\n id: string;\n field: FilterField;\n draft: Draft;\n strings: ReturnType<typeof filterStrings>;\n onChange: (draft: Draft) => void;\n}) {\n const type = field.type === \"number\" ? \"number\" : field.type === \"date\" ? \"date\" : \"text\";\n\n if (field.type === \"select\" && !isMulti(draft.operator)) {\n return (\n <label className={styles.control}>\n <VisuallyHidden>{strings.value}</VisuallyHidden>\n <select\n id={id}\n className={styles.select}\n value={draft.value}\n onChange={(event) => onChange({ ...draft, value: event.target.value })}\n >\n <option value=\"\">—</option>\n {field.options?.map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n </label>\n );\n }\n\n if (field.type === \"boolean\") {\n return (\n <label className={styles.control}>\n <VisuallyHidden>{strings.value}</VisuallyHidden>\n <select\n id={id}\n className={styles.select}\n value={draft.value}\n onChange={(event) => onChange({ ...draft, value: event.target.value })}\n >\n <option value=\"\">—</option>\n <option value=\"true\">{strings.yes}</option>\n <option value=\"false\">{strings.no}</option>\n </select>\n </label>\n );\n }\n\n if (draft.operator === \"between\") {\n return (\n <>\n <label className={styles.control}>\n <VisuallyHidden>{strings.from}</VisuallyHidden>\n <input\n id={id}\n className={styles.input}\n type={type}\n value={draft.value}\n placeholder={strings.from}\n onChange={(event) => onChange({ ...draft, value: event.target.value })}\n />\n </label>\n <label className={styles.control}>\n <VisuallyHidden>{strings.to}</VisuallyHidden>\n <input\n className={styles.input}\n type={type}\n value={draft.to}\n placeholder={strings.to}\n onChange={(event) => onChange({ ...draft, to: event.target.value })}\n />\n </label>\n </>\n );\n }\n\n return (\n <label className={styles.control}>\n <VisuallyHidden>{strings.value}</VisuallyHidden>\n <input\n id={id}\n className={styles.input}\n type={type}\n value={draft.value}\n placeholder={field.placeholder ?? strings.value}\n onChange={(event) => onChange({ ...draft, value: event.target.value })}\n />\n </label>\n );\n}\n"],"mappings":"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"}
|