tempest-react-sdk 0.63.0 → 0.65.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/README.md +5 -5
- package/dist/components/Chat/Chat.cjs +1 -1
- package/dist/components/Chat/Chat.cjs.map +1 -1
- package/dist/components/Chat/Chat.js +71 -96
- package/dist/components/Chat/Chat.js.map +1 -1
- package/dist/components/Chat/Chat.module.cjs +1 -1
- package/dist/components/Chat/Chat.module.cjs.map +1 -1
- package/dist/components/Chat/Chat.module.js +30 -8
- package/dist/components/Chat/Chat.module.js.map +1 -1
- package/dist/components/Chat/ChatActionsMenu.cjs +2 -0
- package/dist/components/Chat/ChatActionsMenu.cjs.map +1 -0
- package/dist/components/Chat/ChatActionsMenu.js +14 -0
- package/dist/components/Chat/ChatActionsMenu.js.map +1 -0
- package/dist/components/Chat/ChatAttachments.cjs +2 -0
- package/dist/components/Chat/ChatAttachments.cjs.map +1 -0
- package/dist/components/Chat/ChatAttachments.js +80 -0
- package/dist/components/Chat/ChatAttachments.js.map +1 -0
- package/dist/components/Chat/ChatBubble.cjs +2 -0
- package/dist/components/Chat/ChatBubble.cjs.map +1 -0
- package/dist/components/Chat/ChatBubble.js +155 -0
- package/dist/components/Chat/ChatBubble.js.map +1 -0
- package/dist/components/Chat/chat-groups.cjs +1 -1
- package/dist/components/Chat/chat-groups.cjs.map +1 -1
- package/dist/components/Chat/chat-groups.js +42 -2
- package/dist/components/Chat/chat-groups.js.map +1 -1
- package/dist/components/Chat/chat-receipt.cjs +2 -0
- package/dist/components/Chat/chat-receipt.cjs.map +1 -0
- package/dist/components/Chat/chat-receipt.js +14 -0
- package/dist/components/Chat/chat-receipt.js.map +1 -0
- package/dist/components/Command/Command.module.cjs.map +1 -1
- package/dist/components/Command/Command.module.js.map +1 -1
- package/dist/components/ContextMenu/ContextMenu.cjs +1 -1
- package/dist/components/ContextMenu/ContextMenu.cjs.map +1 -1
- package/dist/components/ContextMenu/ContextMenu.js +100 -48
- package/dist/components/ContextMenu/ContextMenu.js.map +1 -1
- package/dist/components/ContextMenu/ContextMenu.module.cjs.map +1 -1
- package/dist/components/ContextMenu/ContextMenu.module.js.map +1 -1
- package/dist/components/DataTable/DataTable.cjs +1 -1
- package/dist/components/DataTable/DataTable.cjs.map +1 -1
- package/dist/components/DataTable/DataTable.js +1 -0
- package/dist/components/DataTable/DataTable.js.map +1 -1
- package/dist/components/DataTable/use-dev-warnings.cjs +1 -1
- package/dist/components/DataTable/use-dev-warnings.cjs.map +1 -1
- package/dist/components/DataTable/use-dev-warnings.js +4 -3
- package/dist/components/DataTable/use-dev-warnings.js.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.module.cjs.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.module.js.map +1 -1
- package/dist/components/InstallBanner/InstallBanner.cjs +1 -1
- package/dist/components/InstallBanner/InstallBanner.cjs.map +1 -1
- package/dist/components/InstallBanner/InstallBanner.js +62 -37
- package/dist/components/InstallBanner/InstallBanner.js.map +1 -1
- package/dist/components/InstallBanner/InstallBanner.module.cjs +1 -1
- package/dist/components/InstallBanner/InstallBanner.module.cjs.map +1 -1
- package/dist/components/InstallBanner/InstallBanner.module.js +4 -3
- package/dist/components/InstallBanner/InstallBanner.module.js.map +1 -1
- package/dist/components/InstallButton/InstallButton.cjs +1 -1
- package/dist/components/InstallButton/InstallButton.cjs.map +1 -1
- package/dist/components/InstallButton/InstallButton.js +34 -10
- package/dist/components/InstallButton/InstallButton.js.map +1 -1
- package/dist/components/InstallButton/InstallButton.module.cjs +3 -0
- package/dist/components/InstallButton/InstallButton.module.cjs.map +1 -0
- package/dist/components/InstallButton/InstallButton.module.js +10 -0
- package/dist/components/InstallButton/InstallButton.module.js.map +1 -0
- package/dist/components/InstallButton/install-hints.cjs +2 -0
- package/dist/components/InstallButton/install-hints.cjs.map +1 -0
- package/dist/components/InstallButton/install-hints.js +32 -0
- package/dist/components/InstallButton/install-hints.js.map +1 -0
- package/dist/components/SegmentedControl/SegmentedControl.module.cjs.map +1 -1
- package/dist/components/SegmentedControl/SegmentedControl.module.js.map +1 -1
- package/dist/components/Select/Select.cjs +1 -1
- package/dist/components/Select/Select.cjs.map +1 -1
- package/dist/components/Select/Select.js +43 -40
- package/dist/components/Select/Select.js.map +1 -1
- package/dist/components/Select/Select.module.cjs +1 -1
- package/dist/components/Select/Select.module.cjs.map +1 -1
- package/dist/components/Select/Select.module.js +5 -3
- package/dist/components/Select/Select.module.js.map +1 -1
- package/dist/hooks/use-install-prompt.cjs +1 -1
- package/dist/hooks/use-install-prompt.cjs.map +1 -1
- package/dist/hooks/use-install-prompt.js +9 -2
- package/dist/hooks/use-install-prompt.js.map +1 -1
- package/dist/http/api-client.cjs +1 -1
- package/dist/http/api-client.cjs.map +1 -1
- package/dist/http/api-client.js +80 -74
- package/dist/http/api-client.js.map +1 -1
- package/dist/http/decode-response.cjs +2 -0
- package/dist/http/decode-response.cjs.map +1 -0
- package/dist/http/decode-response.js +28 -0
- package/dist/http/decode-response.js.map +1 -0
- package/dist/styles/InstallButton.css +2 -0
- package/dist/styles/actions.css +6 -1
- package/dist/styles/advanced.css +4 -2
- package/dist/styles/chat.css +28 -0
- package/dist/styles/component/Chat.css +28 -0
- package/dist/styles/component/Command.css +1 -1
- package/dist/styles/component/ContextMenu.css +3 -1
- package/dist/styles/component/DropdownMenu.css +1 -1
- package/dist/styles/component/InstallBanner.css +2 -0
- package/dist/styles/component/InstallButton.css +4 -0
- package/dist/styles/component/SegmentedControl.css +1 -1
- package/dist/styles/component/Select.css +4 -0
- package/dist/styles/core.css +2 -2
- package/dist/styles/forms.css +4 -0
- package/dist/styles/manifest.json +14 -1
- package/dist/styles/navigation.css +1 -1
- package/dist/styles/tokens.css +2 -2
- package/dist/styles.css +1 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +511 -36
- package/dist/tempest-react-sdk.js +319 -316
- package/dist/theme/ThemeProvider.cjs +1 -1
- package/dist/theme/ThemeProvider.cjs.map +1 -1
- package/dist/theme/ThemeProvider.js +40 -45
- package/dist/theme/ThemeProvider.js.map +1 -1
- package/dist/theme/create-theme.cjs +3 -3
- package/dist/theme/create-theme.cjs.map +1 -1
- package/dist/theme/create-theme.js +109 -57
- package/dist/theme/create-theme.js.map +1 -1
- package/dist/theme/initial-theme.cjs +2 -2
- package/dist/theme/initial-theme.cjs.map +1 -1
- package/dist/theme/initial-theme.js +7 -12
- package/dist/theme/initial-theme.js.map +1 -1
- package/dist/utils/format.cjs +1 -1
- package/dist/utils/format.cjs.map +1 -1
- package/dist/utils/format.js +5 -3
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/storage.cjs +1 -1
- package/dist/utils/storage.cjs.map +1 -1
- package/dist/utils/storage.js +13 -0
- package/dist/utils/storage.js.map +1 -1
- package/loader/css-loader-hooks.mjs +37 -0
- package/loader/css-loader.mjs +46 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -159,7 +159,7 @@ Via `package.json`:
|
|
|
159
159
|
|
|
160
160
|
Requires React `>=18` and Node `>=22.12` to build.
|
|
161
161
|
|
|
162
|
-
**A bundler is
|
|
162
|
+
**A bundler is assumed — outside one, load the shipped `.css` loader.** Every component module imports its own stylesheet (see [CSS import](#css-import)), and `.css` is not a format Node can load — `node -e "import('tempest-react-sdk')"` fails with `ERR_UNKNOWN_FILE_EXTENSION`. Vite, webpack, Rspack, Parcel and esbuild all resolve that import, and the SDK is client-side only, so no real app consumer is affected. For everything that is not a bundler — a script, a Jest suite, reading the surface by introspection — the package ships the loader: `node --import tempest-react-sdk/node-css-loader your-script.mjs` resolves every stylesheet to an empty module, for `import` and `require` both. A **Jest** suite can use it or the usual `moduleNameMapper: { "\\.css$": "<rootDir>/test/css-stub.js" }`. Vitest needs nothing.
|
|
163
163
|
|
|
164
164
|
### Peer & bundled dependencies
|
|
165
165
|
|
|
@@ -284,7 +284,7 @@ Every module is re-exported from the package root — `import { Button, useDebou
|
|
|
284
284
|
| `push` | `WebPushClient`, `WebPushUnsupportedError`, `WebPushPermissionDeniedError`, `usePushSubscription`, `urlBase64ToUint8Array`, `isPushSupported` |
|
|
285
285
|
| `sw` _(also subpath `tempest-react-sdk/sw`)_ | `registerServiceWorker` (opt-in `autoUpdate` poll + reload, no `vite-plugin-pwa`), `skipWaiting`, `unregisterAllServiceWorkers`, `installPushHandler`, `installNotificationClickHandler`, `installSkipWaitingListener`, `installPrecache` (app-shell offline + Navigation Preload), `installRuntimeCache` (per-route caching, incl. `rangeRequests`), `createPartialResponse` (206 range slicing), `installBackgroundSync` (offline mutation queue + `periodicsync`), `registerPeriodicSync` (main-thread periodic sync), `inspectCaches`/`clearCaches` (cache observability) — the React-free `tempest-react-sdk/sw` subpath is ideal for bundling into your own `sw.ts` |
|
|
286
286
|
| `charts` _(subpath `tempest-react-sdk/charts`, opt. peer `recharts`)_ | `AreaChart`, `BarChart`, `LineChart`, `PieChart`, `RadarChart`, `DEFAULT_CHART_COLORS` — themed recharts wrappers (recharts externalized; install it only if you use charts) |
|
|
287
|
-
| `icons` _(subpath `tempest-react-sdk/icons`)_ | `Icon`, `IconProvider`, `createIconRegistry`, `useIcon`, `preloadIcons`, `iconStatus`, `peekIcon`, `loadIcon`, `resolveIconAlias`, `isIconName`, `iconNames`, `iconAliases`, type `IconName` — render any of lucide's **
|
|
287
|
+
| `icons` _(subpath `tempest-react-sdk/icons`)_ | `Icon`, `IconProvider`, `createIconRegistry`, `useIcon`, `preloadIcons`, `iconStatus`, `peekIcon`, `loadIcon`, `resolveIconAlias`, `isIconName`, `iconNames`, `iconAliases`, type `IconName` — render any of lucide's **2065 icons by kebab-case slug**. A literal slug becomes a static import via the `tempestIcons()` Vite plugin (zero extra requests); a runtime slug loads **one of 46 balanced shards** (40 slugs each) instead of the ~2000 chunk boundaries `lucide-react`'s own `DynamicIcon` forces — sharding by range rather than by initial letter took the worst fetch from 19.10 KB to 4.78 KB brotli. Unknown slug renders `fallback` and never throws; lucide's 258 deprecated aliases keep resolving |
|
|
288
288
|
| `audio` | `createAudioPlayer`, `playAudio`, `stopAudio`, `useAudio`, `createAudioBus` / `useAudioBus` (gain above 100%, post-mix limiter, `setSinkId` routing), `DEFAULT_MAX_GAIN` |
|
|
289
289
|
| `capture` | `BarcodeScanner`, `useBarcodeScanner`, `useTorch`, `isBarcodeDetectionSupported`, `getSupportedBarcodeFormats`, `createBarcodeDetector`, `normalizeBarcode`, `useVideoRecorder`, `createVideoRecorder`, `pickVideoMimeType`, `isVideoRecordingSupported`, `useScreenCapture`, `isScreenCaptureSupported`, `useSpeechRecognition`, `isSpeechRecognitionSupported`, `createMediaRecorder` — barcode/QR reading over the native `BarcodeDetector` (Chromium-only, so inject a polyfill through `detector` or render the `unsupported` fallback), video + screen recording on the same engine as the audio recorder, and Web Speech dictation (**Chromium streams the audio to a Google server**) |
|
|
290
290
|
| `offline` _(peer: `dexie`)_ | `createOfflineStore`, `createOfflineSync` (outbox + delta-pull + watermark engine, now with `subscribe`/`getState`/`dispose` + cross-tab `crossTab`), `useOfflineSync`/`useSyncStatus` (reactive hooks), `lastWriteWins`/`higherVersionWins` (conflict resolvers), types: `OfflineStore`, `OfflineStoreConfig`, `ListOptions`, `OfflineSync`, `OfflineSyncConfig`, `OutboxEntry`, `PullPage`, `SyncPhase`, `SyncState`, `SyncRunSummary`, `SyncTrigger`, `SyncStatus`, `SyncTone`, `WatermarkStore` |
|
|
@@ -316,7 +316,7 @@ Every module is re-exported from the package root — `import { Button, useDebou
|
|
|
316
316
|
| `webrtc` | `tuneOpus` (Opus `fmtp` per audio m-line: per-key merge, `stereo` + `sprop-stereo`, inserts a missing `fmtp`), `setTunedLocalDescription` (falls back to the untouched SDP when the browser refuses the edit), `setSenderBitrate`, `createLinkStatsSampler` / `useLinkStats` / `readRoundTripMs` (throughput as a delta, round trip from the **selected** candidate pair) |
|
|
317
317
|
| `geo` _(opt. peer `leaflet`)_ | `haversineKm`, `pathLengthKm`, `bearingDeg`, `estimateTravel`, `createOSRMBackend`, `boundingBox`, `projectMercator`, `fitProjection`, `createPositionTracker`, `usePositionTracker`, `TrajectoryMap` — tile-free SVG map + trajectory tracking (no external/paid API; Leaflet is an opt-in, lazy-loaded tile layer for self-hosted tiles), types: `Coordinate`, `TrackPoint`, `TravelEstimate`, `TravelMode`, `GeoBounds`, `RoutingBackend` |
|
|
318
318
|
| `br` _(subpath `tempest-react-sdk/br`)_ | `BrazilMap`, `BrazilStateMap`, `BrazilStateCitySelect`, `listStates`, `getState`, `citiesByUf`, `statesByRegion`, `ufChoices`, `cityChoices`, `isValidUf`, `normalizeUf`, `isValidCity`, `loadBrUfGeoJson`, `loadStateMunicipalities`, types `UF`, `BrRegion`, `BrazilState` — clickable 27-UF SVG map + per-state municipality submaps (bundled simplified IBGE GeoJSON, lazy per UF) + states/cities dataset, no external API; mirrors the FastAPI SDK `utils/locations` |
|
|
319
|
-
| `icons` _(subpath `tempest-react-sdk/icons`)_ | `Icon`, `IconProvider`, `createIconRegistry`, `useIcon`, `preloadIcons`, `iconStatus`, `peekIcon`, `loadIcon`, `resolveIconAlias`, `isIconName`, `iconNames`, `iconAliases`, type `IconName` — render any of lucide's **
|
|
319
|
+
| `icons` _(subpath `tempest-react-sdk/icons`)_ | `Icon`, `IconProvider`, `createIconRegistry`, `useIcon`, `preloadIcons`, `iconStatus`, `peekIcon`, `loadIcon`, `resolveIconAlias`, `isIconName`, `iconNames`, `iconAliases`, type `IconName` — render any of lucide's **2065 icons by kebab-case slug**. A literal slug becomes a static import via the `tempestIcons()` Vite plugin (zero extra requests); a runtime slug loads **one of 46 balanced shards** (40 slugs each) instead of the ~2000 chunk boundaries `lucide-react`'s own `DynamicIcon` forces — sharding by range rather than by initial letter took the worst fetch from 19.10 KB to 4.78 KB brotli. Unknown slug renders `fallback` and never throws; lucide's 258 deprecated aliases keep resolving |
|
|
320
320
|
| `push` | `WebPushClient`, `WebPushUnsupportedError`, `WebPushPermissionDeniedError`, `usePushSubscription`, `urlBase64ToUint8Array`, `isPushSupported` |
|
|
321
321
|
| `sw` _(also subpath `tempest-react-sdk/sw`)_ | `registerServiceWorker` (opt-in `autoUpdate` poll + reload, no `vite-plugin-pwa`), `skipWaiting`, `unregisterAllServiceWorkers`, `installPushHandler`, `installNotificationClickHandler`, `installSkipWaitingListener`, `installPrecache` (app-shell offline + Navigation Preload), `installRuntimeCache` (per-route caching, incl. `rangeRequests`), `createPartialResponse` (206 range slicing), `installBackgroundSync` (offline mutation queue + `periodicsync`), `registerPeriodicSync` (main-thread periodic sync), `inspectCaches`/`clearCaches` (cache observability) — the React-free `tempest-react-sdk/sw` subpath is ideal for bundling into your own `sw.ts` |
|
|
322
322
|
| `editor` _(subpath `tempest-react-sdk/editor`, opt. peers `@tiptap/react`+`@tiptap/starter-kit`)_ | `RichTextEditor` — themed tiptap v3 rich-text editor with toolbar (tiptap externalized; install it only if you use the editor) |
|
|
@@ -1938,8 +1938,8 @@ npm run clean # rm -rf dist coverage
|
|
|
1938
1938
|
Snapshot of current health:
|
|
1939
1939
|
|
|
1940
1940
|
- 4705 tests / 485 files — 98.7% lines, 97.4% statements, 97.1% functions, 94.6% branches (CI floors: 98/97/96/94).
|
|
1941
|
-
- What your app actually pays (brotli, tree-shaken slices measured by `npm run size`): `cn` alone
|
|
1942
|
-
- Full-barrel ceiling — nobody imports this, it is the no-tree-shaking worst case:
|
|
1941
|
+
- What your app actually pays (brotli, tree-shaken slices measured by `npm run size` on 0.65.0): `cn` alone 203 B · one `Button` 853 B · a typical app shell (5 components + router + providers + HTTP + auth + a hook) 9.94 KB · offline/PWA surface 4.64 KB · `Chat` 10.53 KB · `AIChat` 10.16 KB · audio capture 5.88 KB · device capture (barcode + video + screen + speech) 5.46 KB · BR payment rails (Pix + boleto + NFe + holidays) 9.21 KB · passkeys 2.27 KB · `styles.css` 29.75 KB · the opt-in `utilities.css` 1.36 KB.
|
|
1942
|
+
- Full-barrel ceiling — nobody imports this, it is the no-tree-shaking worst case: 130.22 KB ESM / 155.07 KB CJS.
|
|
1943
1943
|
- Husky pre-commit runs `lint-staged` (eslint --fix + prettier --write) on staged files.
|
|
1944
1944
|
|
|
1945
1945
|
The demo gallery lives in `examples/gallery` and consumes the local SDK via `file:../..`:
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../../utils/cn.cjs"),t=require("../EmptyState/EmptyState.cjs"),n=require("
|
|
1
|
+
const e=require("../../utils/cn.cjs"),t=require("../EmptyState/EmptyState.cjs"),n=require("./chat-groups.cjs"),r=require("./Chat.module.cjs"),i=require("./ChatBubble.cjs"),a=require("./ChatComposer.cjs");let o=require("react"),s=require("react/jsx-runtime");function c({messages:c,currentUserId:l,onSend:u,onRetry:d,typing:f=[],renderAvatar:p,header:m,emptyState:h,groupWindowMs:g,locale:_=`pt-BR`,now:v,placeholder:y,composerActions:b,composerDisabled:x,onSendError:S,onReact:C,messageActions:w,onQuoteClick:T,className:E,...D}){let O=n.chatStrings(_),k=n.groupMessages({messages:c,currentUserId:l,windowMs:g}),A=(0,o.useRef)(null),j=(0,o.useRef)(null),M=(0,o.useRef)(!0),N=()=>{let e=j.current;if(!e)return;let t=e.scrollHeight-e.scrollTop-e.clientHeight;M.current=t<48};(0,o.useLayoutEffect)(()=>{let e=j.current;e&&M.current&&(e.scrollTop=e.scrollHeight)},[c,f]),(0,o.useEffect)(()=>{let e=j.current;e&&(e.scrollTop=e.scrollHeight)},[]);let P=n.typingLabel(f,_);return(0,s.jsxs)(`div`,{className:e.cn(r.default.panel,E),...D,children:[m&&(0,s.jsx)(`header`,{className:r.default.header,children:m}),(0,s.jsx)(`div`,{ref:j,className:r.default.thread,onScroll:N,role:`log`,"aria-live":`polite`,"aria-relevant":`additions text`,tabIndex:0,"aria-label":O.thread,children:c.length===0?h??(0,s.jsx)(t.EmptyState,{title:O.empty}):k.map(t=>t.kind===`day`?(0,s.jsx)(`div`,{className:r.default.day,children:(0,s.jsx)(`span`,{className:r.default.dayLabel,children:n.dayLabel(t.date,{locale:_,now:v})})},t.key):(0,s.jsxs)(`div`,{className:e.cn(r.default.run,t.own&&r.default.ownRun),children:[p&&(0,s.jsx)(`div`,{className:r.default.avatar,"aria-hidden":`true`,children:p(t.messages[0])}),(0,s.jsxs)(`div`,{className:r.default.runBody,children:[(0,s.jsx)(`span`,{className:r.default.author,children:t.own?O.you:t.authorName??t.authorId}),(0,s.jsx)(`ul`,{className:r.default.bubbles,children:t.messages.map(e=>(0,s.jsx)(i.ChatBubble,{message:e,own:t.own,locale:_,onRetry:d,onReact:C,messageActions:w,onQuoteClick:T},e.id))})]})]},t.key))}),P&&(0,s.jsx)(`p`,{className:r.default.typing,"aria-live":`polite`,children:P}),u&&(0,s.jsx)(a.ChatComposer,{ref:A,onSend:u,locale:_,placeholder:y,actions:b,disabled:x,onError:S})]})}exports.Chat=c;
|
|
2
2
|
//# sourceMappingURL=Chat.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Chat.cjs","names":[],"sources":["../../../src/components/Chat/Chat.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — message grouping needs\n * more context than a list does: currentUserId to decide sides, groupWindowMs and\n * now to decide where a bubble group breaks, typing for the indicator. The remaining\n * props are the composer's (placeholder, composerActions, composerDisabled, onSend,\n * onSendError) and the slots (renderAvatar, header, emptyState).\n */\nimport { 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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"}
|
|
1
|
+
{"version":3,"file":"Chat.cjs","names":[],"sources":["../../../src/components/Chat/Chat.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — message grouping needs\n * more context than a list does: currentUserId to decide sides, groupWindowMs and\n * now to decide where a bubble group breaks, typing for the indicator. The remaining\n * props are the composer's (placeholder, composerActions, composerDisabled, onSend,\n * onSendError) and the slots (renderAvatar, header, emptyState).\n */\nimport { useEffect, useLayoutEffect, useRef, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport type { ContextMenuItem } from \"../ContextMenu\";\nimport { EmptyState } from \"../EmptyState\";\nimport { ChatBubble } from \"./ChatBubble\";\nimport { ChatComposer, type ChatComposerHandle } from \"./ChatComposer\";\nimport { chatStrings, dayLabel, groupMessages, typingLabel, type ChatMessage } 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 * Toggles a reaction on a message. Enables the reaction chips.\n *\n * Called with the emoji that was pressed, including one the user already\n * reacted with: reacting again clears it, rather than stacking a second copy.\n * The app owns that rule — the component only reports the press.\n */\n onReact?: (message: ChatMessage, emoji: string) => void;\n /**\n * Per-message actions: reply, forward, star, edit, delete.\n *\n * Returns the items for one message, opened by right click, by long press on\n * touch, and by a `⋮` button on the bubble. Buttons painted on every bubble\n * turn a conversation into a toolbar, which is why this is a menu.\n */\n messageActions?: (message: ChatMessage) => ContextMenuItem[];\n /** Called when a reply stub is activated — scroll to the quoted message. */\n onQuoteClick?: (message: ChatMessage) => void;\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 onReact,\n messageActions,\n onQuoteClick,\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 <ChatBubble\n key={message.id}\n message={message}\n own={section.own}\n locale={locale}\n onRetry={onRetry}\n onReact={onReact}\n messageActions={messageActions}\n onQuoteClick={onQuoteClick}\n />\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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"}
|
|
@@ -1,126 +1,101 @@
|
|
|
1
1
|
import { cn as e } from "../../utils/cn.js";
|
|
2
2
|
import { EmptyState as t } from "../EmptyState/EmptyState.js";
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import { ChatComposer as
|
|
7
|
-
import { useEffect as
|
|
8
|
-
import { jsx as
|
|
3
|
+
import { chatStrings as n, dayLabel as r, groupMessages as i, typingLabel as a } from "./chat-groups.js";
|
|
4
|
+
import o from "./Chat.module.js";
|
|
5
|
+
import { ChatBubble as s } from "./ChatBubble.js";
|
|
6
|
+
import { ChatComposer as c } from "./ChatComposer.js";
|
|
7
|
+
import { useEffect as l, useLayoutEffect as u, useRef as d } from "react";
|
|
8
|
+
import { jsx as f, jsxs as p } from "react/jsx-runtime";
|
|
9
9
|
//#region src/components/Chat/Chat.tsx
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
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;
|
|
10
|
+
function m({ messages: m, currentUserId: h, onSend: g, onRetry: _, typing: v = [], renderAvatar: y, header: b, emptyState: x, groupWindowMs: S, locale: C = "pt-BR", now: w, placeholder: T, composerActions: E, composerDisabled: D, onSendError: O, onReact: k, messageActions: A, onQuoteClick: j, className: M, ...N }) {
|
|
11
|
+
let P = n(C), F = i({
|
|
12
|
+
messages: m,
|
|
13
|
+
currentUserId: h,
|
|
14
|
+
windowMs: S
|
|
15
|
+
}), I = d(null), L = d(null), R = d(!0), z = () => {
|
|
16
|
+
let e = L.current;
|
|
23
17
|
if (!e) return;
|
|
24
18
|
let t = e.scrollHeight - e.scrollTop - e.clientHeight;
|
|
25
|
-
|
|
19
|
+
R.current = t < 48;
|
|
26
20
|
};
|
|
27
|
-
|
|
28
|
-
let e =
|
|
29
|
-
e &&
|
|
30
|
-
}, [
|
|
31
|
-
let e =
|
|
21
|
+
u(() => {
|
|
22
|
+
let e = L.current;
|
|
23
|
+
e && R.current && (e.scrollTop = e.scrollHeight);
|
|
24
|
+
}, [m, v]), l(() => {
|
|
25
|
+
let e = L.current;
|
|
32
26
|
e && (e.scrollTop = e.scrollHeight);
|
|
33
27
|
}, []);
|
|
34
|
-
let
|
|
35
|
-
return /* @__PURE__ */
|
|
36
|
-
className: e(
|
|
37
|
-
...
|
|
28
|
+
let B = a(v, C);
|
|
29
|
+
return /* @__PURE__ */ p("div", {
|
|
30
|
+
className: e(o.panel, M),
|
|
31
|
+
...N,
|
|
38
32
|
children: [
|
|
39
|
-
|
|
40
|
-
className:
|
|
41
|
-
children:
|
|
33
|
+
b && /* @__PURE__ */ f("header", {
|
|
34
|
+
className: o.header,
|
|
35
|
+
children: b
|
|
42
36
|
}),
|
|
43
|
-
/* @__PURE__ */
|
|
44
|
-
ref:
|
|
45
|
-
className:
|
|
46
|
-
onScroll:
|
|
37
|
+
/* @__PURE__ */ f("div", {
|
|
38
|
+
ref: L,
|
|
39
|
+
className: o.thread,
|
|
40
|
+
onScroll: z,
|
|
47
41
|
role: "log",
|
|
48
42
|
"aria-live": "polite",
|
|
49
43
|
"aria-relevant": "additions text",
|
|
50
44
|
tabIndex: 0,
|
|
51
|
-
"aria-label":
|
|
52
|
-
children:
|
|
53
|
-
className:
|
|
54
|
-
children: /* @__PURE__ */
|
|
55
|
-
className:
|
|
56
|
-
children:
|
|
57
|
-
locale:
|
|
58
|
-
now:
|
|
45
|
+
"aria-label": P.thread,
|
|
46
|
+
children: m.length === 0 ? x ?? /* @__PURE__ */ f(t, { title: P.empty }) : F.map((t) => t.kind === "day" ? /* @__PURE__ */ f("div", {
|
|
47
|
+
className: o.day,
|
|
48
|
+
children: /* @__PURE__ */ f("span", {
|
|
49
|
+
className: o.dayLabel,
|
|
50
|
+
children: r(t.date, {
|
|
51
|
+
locale: C,
|
|
52
|
+
now: w
|
|
59
53
|
})
|
|
60
54
|
})
|
|
61
|
-
}, t.key) : /* @__PURE__ */
|
|
62
|
-
className: e(
|
|
63
|
-
children: [
|
|
64
|
-
className:
|
|
55
|
+
}, t.key) : /* @__PURE__ */ p("div", {
|
|
56
|
+
className: e(o.run, t.own && o.ownRun),
|
|
57
|
+
children: [y && /* @__PURE__ */ f("div", {
|
|
58
|
+
className: o.avatar,
|
|
65
59
|
"aria-hidden": "true",
|
|
66
|
-
children:
|
|
67
|
-
}), /* @__PURE__ */
|
|
68
|
-
className:
|
|
69
|
-
children: [/* @__PURE__ */
|
|
70
|
-
className:
|
|
71
|
-
children: t.own ?
|
|
72
|
-
}), /* @__PURE__ */
|
|
73
|
-
className:
|
|
74
|
-
children: t.messages.map((
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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))
|
|
60
|
+
children: y(t.messages[0])
|
|
61
|
+
}), /* @__PURE__ */ p("div", {
|
|
62
|
+
className: o.runBody,
|
|
63
|
+
children: [/* @__PURE__ */ f("span", {
|
|
64
|
+
className: o.author,
|
|
65
|
+
children: t.own ? P.you : t.authorName ?? t.authorId
|
|
66
|
+
}), /* @__PURE__ */ f("ul", {
|
|
67
|
+
className: o.bubbles,
|
|
68
|
+
children: t.messages.map((e) => /* @__PURE__ */ f(s, {
|
|
69
|
+
message: e,
|
|
70
|
+
own: t.own,
|
|
71
|
+
locale: C,
|
|
72
|
+
onRetry: _,
|
|
73
|
+
onReact: k,
|
|
74
|
+
messageActions: A,
|
|
75
|
+
onQuoteClick: j
|
|
76
|
+
}, e.id))
|
|
102
77
|
})]
|
|
103
78
|
})]
|
|
104
79
|
}, t.key))
|
|
105
80
|
}),
|
|
106
|
-
|
|
107
|
-
className:
|
|
81
|
+
B && /* @__PURE__ */ f("p", {
|
|
82
|
+
className: o.typing,
|
|
108
83
|
"aria-live": "polite",
|
|
109
|
-
children:
|
|
84
|
+
children: B
|
|
110
85
|
}),
|
|
111
|
-
|
|
112
|
-
ref:
|
|
113
|
-
onSend:
|
|
114
|
-
locale:
|
|
115
|
-
placeholder:
|
|
116
|
-
actions:
|
|
117
|
-
disabled:
|
|
118
|
-
onError:
|
|
86
|
+
g && /* @__PURE__ */ f(c, {
|
|
87
|
+
ref: I,
|
|
88
|
+
onSend: g,
|
|
89
|
+
locale: C,
|
|
90
|
+
placeholder: T,
|
|
91
|
+
actions: E,
|
|
92
|
+
disabled: D,
|
|
93
|
+
onError: O
|
|
119
94
|
})
|
|
120
95
|
]
|
|
121
96
|
});
|
|
122
97
|
}
|
|
123
98
|
//#endregion
|
|
124
|
-
export {
|
|
99
|
+
export { m as Chat };
|
|
125
100
|
|
|
126
101
|
//# sourceMappingURL=Chat.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Chat.js","names":[],"sources":["../../../src/components/Chat/Chat.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — message grouping needs\n * more context than a list does: currentUserId to decide sides, groupWindowMs and\n * now to decide where a bubble group breaks, typing for the indicator. The remaining\n * props are the composer's (placeholder, composerActions, composerDisabled, onSend,\n * onSendError) and the slots (renderAvatar, header, emptyState).\n */\nimport { 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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"}
|
|
1
|
+
{"version":3,"file":"Chat.js","names":[],"sources":["../../../src/components/Chat/Chat.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — message grouping needs\n * more context than a list does: currentUserId to decide sides, groupWindowMs and\n * now to decide where a bubble group breaks, typing for the indicator. The remaining\n * props are the composer's (placeholder, composerActions, composerDisabled, onSend,\n * onSendError) and the slots (renderAvatar, header, emptyState).\n */\nimport { useEffect, useLayoutEffect, useRef, type HTMLAttributes, type ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport type { ContextMenuItem } from \"../ContextMenu\";\nimport { EmptyState } from \"../EmptyState\";\nimport { ChatBubble } from \"./ChatBubble\";\nimport { ChatComposer, type ChatComposerHandle } from \"./ChatComposer\";\nimport { chatStrings, dayLabel, groupMessages, typingLabel, type ChatMessage } 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 * Toggles a reaction on a message. Enables the reaction chips.\n *\n * Called with the emoji that was pressed, including one the user already\n * reacted with: reacting again clears it, rather than stacking a second copy.\n * The app owns that rule — the component only reports the press.\n */\n onReact?: (message: ChatMessage, emoji: string) => void;\n /**\n * Per-message actions: reply, forward, star, edit, delete.\n *\n * Returns the items for one message, opened by right click, by long press on\n * touch, and by a `⋮` button on the bubble. Buttons painted on every bubble\n * turn a conversation into a toolbar, which is why this is a menu.\n */\n messageActions?: (message: ChatMessage) => ContextMenuItem[];\n /** Called when a reply stub is activated — scroll to the quoted message. */\n onQuoteClick?: (message: ChatMessage) => void;\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 onReact,\n messageActions,\n onQuoteClick,\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 <ChatBubble\n key={message.id}\n message={message}\n own={section.own}\n locale={locale}\n onRetry={onRetry}\n onReact={onReact}\n messageActions={messageActions}\n onQuoteClick={onQuoteClick}\n />\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":";;;;;;;;;AA8FA,SAAgB,EAAK,EACjB,aACA,kBACA,WACA,YACA,YAAS,CAAC,GACV,iBACA,WACA,eACA,kBACA,YAAS,SACT,QACA,gBACA,oBACA,qBACA,gBACA,YACA,mBACA,iBACA,cACA,GAAG,KACO;CACV,IAAM,IAAU,EAAY,CAAM,GAC5B,IAAW,EAAc;EAAE;EAAU;EAAe,UAAU;CAAc,CAAC,GAC7E,IAAW,EAAkC,IAAI,GACjD,IAAS,EAA8B,IAAI,GAC3C,IAAgB,EAAO,EAAI,GAU3B,UAA4B;EAC9B,IAAM,IAAO,EAAO;EACpB,IAAI,CAAC,GAAM;EACX,IAAM,IAAW,EAAK,eAAe,EAAK,YAAY,EAAK;EAC3D,EAAc,UAAU,IAAW;CACvC;CAQA,AANA,QAAsB;EAClB,IAAM,IAAO,EAAO;EAChB,AAAC,KAAS,EAAc,YAC5B,EAAK,YAAY,EAAK;CAC1B,GAAG,CAAC,GAAU,CAAM,CAAC,GAErB,QAAgB;EACZ,IAAM,IAAO,EAAO;EACf,MACL,EAAK,YAAY,EAAK;CAE1B,GAAG,CAAC,CAAC;CAEL,IAAM,IAAa,EAAY,GAAQ,CAAM;CAE7C,OACI,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,OAAO,CAAS;EAAG,GAAI;EAAjD,UAAA;GACK,KAAU,kBAAC,UAAD;IAAQ,WAAW,EAAO;IAAS,UAAA;GAAe,CAAA;GAE7D,kBAAC,OAAD;IACI,KAAK;IACL,WAAW,EAAO;IAClB,UAAU;IACV,MAAK;IACL,aAAU;IACV,iBAAc;IACd,UAAU;IACV,cAAY,EAAQ;IAEnB,UAAA,EAAS,WAAW,IACd,KAAc,kBAAC,GAAD,EAAY,OAAO,EAAQ,MAAQ,CAAA,IAClD,EAAS,KAAK,MACV,EAAQ,SAAS,QACb,kBAAC,OAAD;KAAuB,WAAW,EAAO;KACrC,UAAA,kBAAC,QAAD;MAAM,WAAW,EAAO;MACnB,UAAA,EAAS,EAAQ,MAAM;OAAE;OAAQ;MAAI,CAAC;KACrC,CAAA;IACL,GAJK,EAAQ,GAIb,IAEL,kBAAC,OAAD;KAEI,WAAW,EAAG,EAAO,KAAK,EAAQ,OAAO,EAAO,MAAM;KAF1D,UAAA,CAIK,KACG,kBAAC,OAAD;MAAK,WAAW,EAAO;MAAQ,eAAY;MACtC,UAAA,EAAa,EAAQ,SAAS,EAAE;KAChC,CAAA,GAET,kBAAC,OAAD;MAAK,WAAW,EAAO;MAAvB,UAAA,CACI,kBAAC,QAAD;OAAM,WAAW,EAAO;OACnB,UAAA,EAAQ,MACH,EAAQ,MACP,EAAQ,cAAc,EAAQ;MACnC,CAAA,GACN,kBAAC,MAAD;OAAI,WAAW,EAAO;OACjB,UAAA,EAAQ,SAAS,KAAK,MACnB,kBAAC,GAAD;QAEa;QACT,KAAK,EAAQ;QACL;QACC;QACA;QACO;QACF;OACjB,GARQ,EAAQ,EAQhB,CACJ;MACD,CAAA,CACH;KACJ,CAAA,CAAA;IA7BI,GAAA,EAAQ,GA6BZ,CAEb;GACL,CAAA;GAEJ,KACG,kBAAC,KAAD;IAAG,WAAW,EAAO;IAAQ,aAAU;IAClC,UAAA;GACF,CAAA;GAGN,KACG,kBAAC,GAAD;IACI,KAAK;IACG;IACA;IACK;IACb,SAAS;IACT,UAAU;IACV,SAAS;GACZ,CAAA;EAEJ;;AAEb"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
require("../../styles/component/Chat.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=`
|
|
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_reactions_D4iGE`,b=`tempest_reaction_WkkNQ`,x=`tempest_reactionMine_81mXj`,S=`tempest_actionsButton_v8oqH`,C=`tempest_quote_1muxm`,w=`tempest_quoteButton_eD0Dx`,T=`tempest_quoteAuthor_Jpx15`,E=`tempest_quoteText_9KI-m`,D=`tempest_quoteRevoked_RQ-5u`,O=`tempest_tombstone_7Iiw-`,k=`tempest_deletedBubble_dZQFE`,A=`tempest_edited_kTire`,j=`tempest_statusRead_Wj77L`,M=`tempest_attachments_h8wIJ`,N=`tempest_attachmentsFallback_YT9RT`,P=`tempest_imageAttachment_PggzU`,F=`tempest_videoAttachment_0TsUC`,I=`tempest_voiceAttachment_1EUcu`,L=`tempest_waveform_5K-qs`,R=`tempest_fileAttachment_H803b`,z=`tempest_fileName_2Fwix`,B=`tempest_fileMeta_T1fdS`,V=`tempest_retry_DAbIj`,H=`tempest_typing_zdp3O`,U=`tempest_composer_krOGl`,W=`tempest_field_fBY-q`,G=`tempest_composerActions_MYwQx`,K=`tempest_send_NwcaR`,q={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,reactions:y,reaction:b,reactionMine:x,actionsButton:S,quote:C,quoteButton:w,quoteAuthor:T,quoteText:E,quoteRevoked:D,tombstone:O,deletedBubble:k,edited:A,statusRead:j,attachments:M,attachmentsFallback:N,imageAttachment:P,videoAttachment:F,voiceAttachment:I,waveform:L,fileAttachment:R,fileName:z,fileMeta:B,retry:V,typing:H,composer:U,field:W,composerActions:G,send:K};exports.actionsButton=S,exports.attachments=M,exports.attachmentsFallback=N,exports.author=l,exports.avatar=s,exports.body=h,exports.bubble=f,exports.bubbleRow=d,exports.bubbles=u,exports.composer=U,exports.composerActions=G,exports.day=r,exports.dayLabel=i,exports.default=q,exports.deletedBubble=k,exports.edited=A,exports.failedBubble=m,exports.field=W,exports.fileAttachment=R,exports.fileMeta=B,exports.fileName=z,exports.header=t,exports.imageAttachment=P,exports.meta=g,exports.ownBubble=p,exports.ownRun=o,exports.panel=e,exports.quote=C,exports.quoteAuthor=T,exports.quoteButton=w,exports.quoteRevoked=D,exports.quoteText=E,exports.reaction=b,exports.reactionMine=x,exports.reactions=y,exports.retry=V,exports.run=a,exports.runBody=c,exports.send=K,exports.status=_,exports.statusFailed=v,exports.statusRead=j,exports.thread=n,exports.tombstone=O,exports.typing=H,exports.videoAttachment=F,exports.voiceAttachment=I,exports.waveform=L;
|
|
3
3
|
//# sourceMappingURL=Chat.module.cjs.map
|
|
@@ -1 +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":";"}
|
|
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/*\n * A bubble row carries more than a bubble now: the actions button sits beside it\n * and the reaction chips hang under it, so the row wraps and the chips are given\n * the whole width to fall onto.\n */\n.bubbleRow {\n flex-wrap: wrap;\n}\n\n.reactions {\n display: flex;\n flex-basis: 100%;\n flex-wrap: wrap;\n gap: var(--tempest-space-1);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.ownRun .reactions {\n justify-content: flex-end;\n}\n\n.reaction {\n display: inline-flex;\n align-items: center;\n gap: var(--tempest-space-1);\n padding: 0 var(--tempest-space-2);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-full, 999px);\n background-color: var(--tempest-surface);\n color: var(--tempest-text);\n font: inherit;\n font-size: var(--tempest-text-xs);\n line-height: 1.8;\n cursor: pointer;\n}\n\n.reaction:disabled {\n cursor: default;\n}\n\n/*\n * Your own reaction is marked by a tinted chip and its own foreground, never by\n * `--tempest-text-subtle` over the tint: that token is solved against the page\n * background and fails the text floor on a tinted surface.\n */\n.reactionMine {\n border-color: transparent;\n background-color: var(--tempest-primary-soft);\n color: var(--tempest-primary-on-soft);\n font-weight: var(--tempest-weight-medium);\n}\n\n/*\n * The ⋮ button is always in the DOM — on touch there is no hover to reveal it,\n * and a control that only appears on hover is a control a phone does not have.\n * A fine pointer gets it faded until the row is hovered or it is focused.\n */\n.actionsButton {\n flex: 0 0 auto;\n padding: 0 var(--tempest-space-1);\n border: none;\n background: none;\n color: var(--tempest-text-muted);\n font: inherit;\n line-height: 1;\n cursor: pointer;\n}\n\n@media (hover: hover) and (pointer: fine) {\n .actionsButton {\n opacity: 0;\n transition: var(--tempest-transition-opacity, opacity 120ms ease);\n }\n\n .bubbleRow:hover .actionsButton,\n .actionsButton:focus-visible {\n opacity: 1;\n }\n}\n\n/* The reply stub: a tinted strip with a bar on the side it is anchored to. */\n.quote {\n display: flex;\n flex-direction: column;\n gap: 2px;\n width: 100%;\n padding: var(--tempest-space-1) var(--tempest-space-2);\n border-left: 3px solid var(--tempest-primary);\n border-radius: var(--tempest-radius-sm);\n background-color: var(--tempest-surface-2);\n color: var(--tempest-text);\n font-size: var(--tempest-text-xs);\n text-align: left;\n}\n\n.quoteButton {\n border-top: none;\n border-right: none;\n border-bottom: none;\n font: inherit;\n font-size: var(--tempest-text-xs);\n cursor: pointer;\n}\n\n/*\n * The author of a quote is text, so it is coloured like text.\n *\n * `--tempest-primary` here measured **4.35:1** against `--tempest-surface-2` in a\n * real browser — below the 4.5:1 floor for a 12px label, and invisible to the\n * jsdom axe sweep, which disables `color-contrast` because nothing is painted.\n * The brand still marks the quote: it is the bar down the side, which is a\n * non-text indicator and only owes 3:1.\n */\n.quoteAuthor {\n color: var(--tempest-text);\n font-weight: var(--tempest-weight-medium);\n}\n\n.quoteText {\n overflow: hidden;\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 2;\n line-clamp: 2;\n overflow-wrap: anywhere;\n}\n\n/*\n * A revoked quote reads as absent, which is the point: the text of a deleted\n * message must not survive inside the reply to it.\n */\n.quoteRevoked {\n color: var(--tempest-text-muted);\n font-style: italic;\n}\n\n.tombstone {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-1);\n color: var(--tempest-text-muted);\n font-style: italic;\n}\n\n.deletedBubble {\n border-style: dashed;\n background-color: transparent;\n}\n\n.edited {\n font-style: italic;\n}\n\n/* Read by everyone is the only state that turns the ticks. */\n.statusRead {\n color: var(--tempest-info, var(--tempest-primary));\n}\n\n.attachments {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n max-width: 320px;\n}\n\n/* Holds the row's height while the attachments chunk is still loading. */\n.attachmentsFallback {\n min-height: var(--tempest-space-8, 2rem);\n}\n\n.imageAttachment {\n padding: 0;\n border: none;\n border-radius: var(--tempest-radius-md);\n background: none;\n line-height: 0;\n cursor: pointer;\n}\n\n.imageAttachment img {\n max-width: 100%;\n max-height: 260px;\n border-radius: var(--tempest-radius-md);\n object-fit: cover;\n}\n\n.videoAttachment {\n max-width: 100%;\n border-radius: var(--tempest-radius-md);\n}\n\n.voiceAttachment {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n}\n\n/*\n * The waveform is the difference between a voice note and a grey rectangle: it\n * is what tells somebody whether this is a word or a two-minute monologue.\n */\n.waveform {\n display: flex;\n align-items: flex-end;\n gap: 2px;\n height: 28px;\n}\n\n.waveform span {\n flex: 1 1 auto;\n min-width: 2px;\n border-radius: 1px;\n background-color: var(--tempest-primary);\n opacity: 0.7;\n}\n\n.fileAttachment {\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: var(--tempest-space-2);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n color: var(--tempest-text);\n text-decoration: none;\n}\n\n.fileName {\n overflow-wrap: anywhere;\n font-weight: var(--tempest-weight-medium);\n}\n\n.fileMeta {\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-2xs);\n font-variant-numeric: tabular-nums;\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":";"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import "../../styles/component/Chat.css";
|
|
2
2
|
//#region src/components/Chat/Chat.module.css
|
|
3
|
-
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 = "
|
|
3
|
+
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_reactions_D4iGE", b = "tempest_reaction_WkkNQ", x = "tempest_reactionMine_81mXj", S = "tempest_actionsButton_v8oqH", C = "tempest_quote_1muxm", w = "tempest_quoteButton_eD0Dx", T = "tempest_quoteAuthor_Jpx15", E = "tempest_quoteText_9KI-m", D = "tempest_quoteRevoked_RQ-5u", O = "tempest_tombstone_7Iiw-", k = "tempest_deletedBubble_dZQFE", A = "tempest_edited_kTire", j = "tempest_statusRead_Wj77L", M = "tempest_attachments_h8wIJ", N = "tempest_attachmentsFallback_YT9RT", P = "tempest_imageAttachment_PggzU", F = "tempest_videoAttachment_0TsUC", I = "tempest_voiceAttachment_1EUcu", L = "tempest_waveform_5K-qs", R = "tempest_fileAttachment_H803b", z = "tempest_fileName_2Fwix", B = "tempest_fileMeta_T1fdS", V = "tempest_retry_DAbIj", H = "tempest_typing_zdp3O", U = "tempest_composer_krOGl", W = "tempest_field_fBY-q", G = "tempest_composerActions_MYwQx", K = "tempest_send_NwcaR", q = {
|
|
4
4
|
panel: e,
|
|
5
5
|
header: t,
|
|
6
6
|
thread: n,
|
|
@@ -20,14 +20,36 @@ var e = "tempest_panel_xUVw0", t = "tempest_header_muoPu", n = "tempest_thread_-
|
|
|
20
20
|
meta: g,
|
|
21
21
|
status: _,
|
|
22
22
|
statusFailed: v,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
reactions: y,
|
|
24
|
+
reaction: b,
|
|
25
|
+
reactionMine: x,
|
|
26
|
+
actionsButton: S,
|
|
27
|
+
quote: C,
|
|
28
|
+
quoteButton: w,
|
|
29
|
+
quoteAuthor: T,
|
|
30
|
+
quoteText: E,
|
|
31
|
+
quoteRevoked: D,
|
|
32
|
+
tombstone: O,
|
|
33
|
+
deletedBubble: k,
|
|
34
|
+
edited: A,
|
|
35
|
+
statusRead: j,
|
|
36
|
+
attachments: M,
|
|
37
|
+
attachmentsFallback: N,
|
|
38
|
+
imageAttachment: P,
|
|
39
|
+
videoAttachment: F,
|
|
40
|
+
voiceAttachment: I,
|
|
41
|
+
waveform: L,
|
|
42
|
+
fileAttachment: R,
|
|
43
|
+
fileName: z,
|
|
44
|
+
fileMeta: B,
|
|
45
|
+
retry: V,
|
|
46
|
+
typing: H,
|
|
47
|
+
composer: U,
|
|
48
|
+
field: W,
|
|
49
|
+
composerActions: G,
|
|
50
|
+
send: K
|
|
29
51
|
};
|
|
30
52
|
//#endregion
|
|
31
|
-
export { l as author, s as avatar, h as body, f as bubble, d as bubbleRow, u as bubbles,
|
|
53
|
+
export { S as actionsButton, M as attachments, N as attachmentsFallback, l as author, s as avatar, h as body, f as bubble, d as bubbleRow, u as bubbles, U as composer, G as composerActions, r as day, i as dayLabel, q as default, k as deletedBubble, A as edited, m as failedBubble, W as field, R as fileAttachment, B as fileMeta, z as fileName, t as header, P as imageAttachment, g as meta, p as ownBubble, o as ownRun, e as panel, C as quote, T as quoteAuthor, w as quoteButton, D as quoteRevoked, E as quoteText, b as reaction, x as reactionMine, y as reactions, V as retry, a as run, c as runBody, K as send, _ as status, v as statusFailed, j as statusRead, n as thread, O as tombstone, H as typing, F as videoAttachment, I as voiceAttachment, L as waveform };
|
|
32
54
|
|
|
33
55
|
//# sourceMappingURL=Chat.module.js.map
|
|
@@ -1 +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":";"}
|
|
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/*\n * A bubble row carries more than a bubble now: the actions button sits beside it\n * and the reaction chips hang under it, so the row wraps and the chips are given\n * the whole width to fall onto.\n */\n.bubbleRow {\n flex-wrap: wrap;\n}\n\n.reactions {\n display: flex;\n flex-basis: 100%;\n flex-wrap: wrap;\n gap: var(--tempest-space-1);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.ownRun .reactions {\n justify-content: flex-end;\n}\n\n.reaction {\n display: inline-flex;\n align-items: center;\n gap: var(--tempest-space-1);\n padding: 0 var(--tempest-space-2);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-full, 999px);\n background-color: var(--tempest-surface);\n color: var(--tempest-text);\n font: inherit;\n font-size: var(--tempest-text-xs);\n line-height: 1.8;\n cursor: pointer;\n}\n\n.reaction:disabled {\n cursor: default;\n}\n\n/*\n * Your own reaction is marked by a tinted chip and its own foreground, never by\n * `--tempest-text-subtle` over the tint: that token is solved against the page\n * background and fails the text floor on a tinted surface.\n */\n.reactionMine {\n border-color: transparent;\n background-color: var(--tempest-primary-soft);\n color: var(--tempest-primary-on-soft);\n font-weight: var(--tempest-weight-medium);\n}\n\n/*\n * The ⋮ button is always in the DOM — on touch there is no hover to reveal it,\n * and a control that only appears on hover is a control a phone does not have.\n * A fine pointer gets it faded until the row is hovered or it is focused.\n */\n.actionsButton {\n flex: 0 0 auto;\n padding: 0 var(--tempest-space-1);\n border: none;\n background: none;\n color: var(--tempest-text-muted);\n font: inherit;\n line-height: 1;\n cursor: pointer;\n}\n\n@media (hover: hover) and (pointer: fine) {\n .actionsButton {\n opacity: 0;\n transition: var(--tempest-transition-opacity, opacity 120ms ease);\n }\n\n .bubbleRow:hover .actionsButton,\n .actionsButton:focus-visible {\n opacity: 1;\n }\n}\n\n/* The reply stub: a tinted strip with a bar on the side it is anchored to. */\n.quote {\n display: flex;\n flex-direction: column;\n gap: 2px;\n width: 100%;\n padding: var(--tempest-space-1) var(--tempest-space-2);\n border-left: 3px solid var(--tempest-primary);\n border-radius: var(--tempest-radius-sm);\n background-color: var(--tempest-surface-2);\n color: var(--tempest-text);\n font-size: var(--tempest-text-xs);\n text-align: left;\n}\n\n.quoteButton {\n border-top: none;\n border-right: none;\n border-bottom: none;\n font: inherit;\n font-size: var(--tempest-text-xs);\n cursor: pointer;\n}\n\n/*\n * The author of a quote is text, so it is coloured like text.\n *\n * `--tempest-primary` here measured **4.35:1** against `--tempest-surface-2` in a\n * real browser — below the 4.5:1 floor for a 12px label, and invisible to the\n * jsdom axe sweep, which disables `color-contrast` because nothing is painted.\n * The brand still marks the quote: it is the bar down the side, which is a\n * non-text indicator and only owes 3:1.\n */\n.quoteAuthor {\n color: var(--tempest-text);\n font-weight: var(--tempest-weight-medium);\n}\n\n.quoteText {\n overflow: hidden;\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 2;\n line-clamp: 2;\n overflow-wrap: anywhere;\n}\n\n/*\n * A revoked quote reads as absent, which is the point: the text of a deleted\n * message must not survive inside the reply to it.\n */\n.quoteRevoked {\n color: var(--tempest-text-muted);\n font-style: italic;\n}\n\n.tombstone {\n display: flex;\n align-items: center;\n gap: var(--tempest-space-1);\n color: var(--tempest-text-muted);\n font-style: italic;\n}\n\n.deletedBubble {\n border-style: dashed;\n background-color: transparent;\n}\n\n.edited {\n font-style: italic;\n}\n\n/* Read by everyone is the only state that turns the ticks. */\n.statusRead {\n color: var(--tempest-info, var(--tempest-primary));\n}\n\n.attachments {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n max-width: 320px;\n}\n\n/* Holds the row's height while the attachments chunk is still loading. */\n.attachmentsFallback {\n min-height: var(--tempest-space-8, 2rem);\n}\n\n.imageAttachment {\n padding: 0;\n border: none;\n border-radius: var(--tempest-radius-md);\n background: none;\n line-height: 0;\n cursor: pointer;\n}\n\n.imageAttachment img {\n max-width: 100%;\n max-height: 260px;\n border-radius: var(--tempest-radius-md);\n object-fit: cover;\n}\n\n.videoAttachment {\n max-width: 100%;\n border-radius: var(--tempest-radius-md);\n}\n\n.voiceAttachment {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n}\n\n/*\n * The waveform is the difference between a voice note and a grey rectangle: it\n * is what tells somebody whether this is a word or a two-minute monologue.\n */\n.waveform {\n display: flex;\n align-items: flex-end;\n gap: 2px;\n height: 28px;\n}\n\n.waveform span {\n flex: 1 1 auto;\n min-width: 2px;\n border-radius: 1px;\n background-color: var(--tempest-primary);\n opacity: 0.7;\n}\n\n.fileAttachment {\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: var(--tempest-space-2);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-radius-md);\n color: var(--tempest-text);\n text-decoration: none;\n}\n\n.fileName {\n overflow-wrap: anywhere;\n font-weight: var(--tempest-weight-medium);\n}\n\n.fileMeta {\n color: var(--tempest-text-muted);\n font-size: var(--tempest-text-2xs);\n font-variant-numeric: tabular-nums;\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":";"}
|