@djangocfg/ui-tools 2.1.433 → 2.1.434

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/ui-tools",
3
- "version": "2.1.433",
3
+ "version": "2.1.434",
4
4
  "description": "Heavy React tools with lazy loading - for Electron, Vite, CRA, Next.js apps",
5
5
  "keywords": [
6
6
  "ui-tools",
@@ -334,8 +334,8 @@
334
334
  "test:watch": "vitest"
335
335
  },
336
336
  "peerDependencies": {
337
- "@djangocfg/i18n": "^2.1.433",
338
- "@djangocfg/ui-core": "^2.1.433",
337
+ "@djangocfg/i18n": "^2.1.434",
338
+ "@djangocfg/ui-core": "^2.1.434",
339
339
  "consola": "^3.4.2",
340
340
  "lodash-es": "^4.18.1",
341
341
  "lucide-react": "^0.545.0",
@@ -418,9 +418,9 @@
418
418
  "@maplibre/maplibre-gl-geocoder": "^1.7.0"
419
419
  },
420
420
  "devDependencies": {
421
- "@djangocfg/i18n": "^2.1.433",
422
- "@djangocfg/typescript-config": "^2.1.433",
423
- "@djangocfg/ui-core": "^2.1.433",
421
+ "@djangocfg/i18n": "^2.1.434",
422
+ "@djangocfg/typescript-config": "^2.1.434",
423
+ "@djangocfg/ui-core": "^2.1.434",
424
424
  "@types/lodash-es": "^4.17.12",
425
425
  "@types/mapbox__mapbox-gl-draw": "^1.4.8",
426
426
  "@types/node": "^25.2.3",
@@ -0,0 +1,51 @@
1
+ # chat — `@djangocfg/ui-tools/chat`
2
+
3
+ Decomposed, transport-agnostic chat UI: bubbles, composer, tool-call panels,
4
+ message blocks, link previews. Consumed from npm AND vendored into cmdop
5
+ desktop via `pnpm sync:cfg`. See `README.md` for the full API; `@docs/` for
6
+ design + troubleshooting.
7
+
8
+ ## Public entry — `lazy.tsx` (the `./chat` subpath)
9
+
10
+ `package.json` maps `"./chat"` → `src/tools/chat/lazy.tsx`. It does
11
+ `export * from './messages'` (+ composer/shell/launcher) and a few **explicit**
12
+ re-exports.
13
+
14
+ **Invariant — explicit-export gotcha.** A newly added **value** export that an
15
+ external host imports from `@djangocfg/ui-tools/chat` MUST be listed explicitly
16
+ in `lazy.tsx` (like `MarkdownMessage`, `ToolPayloadValue`). `export *` forwards
17
+ types but drops a fresh value export at runtime under `'use client'` + Vite →
18
+ `does not provide an export named X`. Not a cache bug. See
19
+ [`@docs/troubleshooting.md`](./@docs/troubleshooting.md) §1.
20
+
21
+ ## Shared slots — extract, don't fork
22
+
23
+ A host can fork `MessageBubble` (cmdop does, to inject `@machine` chips). To
24
+ stop forks silently losing features, bubble features are **extracted
25
+ components** the fork re-renders, not inline code:
26
+
27
+ - `ToolPayloadValue` — the one payload renderer (object→JsonTree, string→`<pre>`,
28
+ XSS-safe). Used by the default `ToolCalls` panel AND any host card that owns
29
+ the panel via `renderToolCall`. [`@docs/tool-call-rendering.md`](./@docs/tool-call-rendering.md)
30
+ - `AutoLinkPreview` — auto-detect URL→preview slot, rendered by `MessageBubble`
31
+ and every fork. [`@docs/link-preview.md`](./@docs/link-preview.md)
32
+
33
+ **Rule:** adding a shared bubble feature → extract a component + export it (and
34
+ list it in `lazy.tsx` if a host imports it directly).
35
+
36
+ ## Producer contract — tool payloads
37
+
38
+ `ChatToolCall.input`/`.output` are `unknown` so a real object flows through and
39
+ renders as a JSON tree. A producer that sends a JSON **string** (or
40
+ double-encodes one) gets a flat/escaped `<pre>` — the frontend never
41
+ un-escapes (sanitization). Fix on the producer: send a value, encode once.
42
+ [`@docs/troubleshooting.md`](./@docs/troubleshooting.md) §2-3.
43
+
44
+ ## Docs
45
+
46
+ | Doc | Covers |
47
+ |---|---|
48
+ | [`@docs/tool-call-rendering.md`](./@docs/tool-call-rendering.md) | `ToolCalls` panels, `renderToolCall` vs payload props, `ToolPayloadValue`, JSON-tree contract |
49
+ | [`@docs/link-preview.md`](./@docs/link-preview.md) | `AutoLinkPreview` auto-detect, `LinkPreviewCard`, fork-drift fix |
50
+ | [`@docs/message-blocks.md`](./@docs/message-blocks.md) | typed `blocks[]` registry (`json`/`link`/`map`/… renderers) |
51
+ | [`@docs/troubleshooting.md`](./@docs/troubleshooting.md) | export gotcha, double-encoding, `<pre>`-vs-tree, fork drift, sync:cfg |
@@ -2,6 +2,12 @@
2
2
 
3
3
  Decomposed, transport-agnostic chat. Streaming-aware, markdown-native, mobile-ready, role-aware styling, pluggable backends.
4
4
 
5
+ > **Docs & gotchas:** [`CLAUDE.md`](./CLAUDE.md) (quick map + invariants) ·
6
+ > [`@docs/tool-call-rendering.md`](./@docs/tool-call-rendering.md) ·
7
+ > [`@docs/link-preview.md`](./@docs/link-preview.md) ·
8
+ > [`@docs/message-blocks.md`](./@docs/message-blocks.md) ·
9
+ > [`@docs/troubleshooting.md`](./@docs/troubleshooting.md)
10
+
5
11
  ## TL;DR
6
12
 
7
13
  ```tsx
@@ -85,6 +85,15 @@ export {
85
85
  type MarkdownMessageProps,
86
86
  } from '../dev/code/MarkdownMessage';
87
87
 
88
+ // Same gotcha as above: a host that owns its own tool-call card (cmdop's
89
+ // `<ToolResult>` via the `renderToolCall` override) imports ToolPayloadValue
90
+ // from `@djangocfg/ui-tools/chat` → THIS file. The `export * from './messages'`
91
+ // above re-exports it for TYPES, but under `'use client'` + Vite the runtime
92
+ // barrel drops the freshly-added symbol — so list it explicitly, like
93
+ // MarkdownMessage. (Symbols imported directly by MessageBubble — e.g.
94
+ // AutoLinkPreview — ride its chunk and don't need this.)
95
+ export { ToolPayloadValue } from './messages/ToolPayloadValue';
96
+
88
97
  // Bridge — AI-driven `point` directives (lightweight, no heavy deps).
89
98
  export {
90
99
  HighlightOverlay,
@@ -0,0 +1,74 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * AutoLinkPreview — the auto-detect link-preview slot, extracted so EVERY
5
+ * bubble surface shares ONE implementation.
6
+ *
7
+ * The default `MessageBubble` and any forked bubble (e.g. cmdop's
8
+ * mention-aware bubble, which owns its own content `<MarkdownMessage>` to
9
+ * inject `@machine` chips) both render this. Before extraction the logic
10
+ * lived inline in `MessageBubble` only, so a fork silently lost link
11
+ * previews — this component is the anti-drift fix: import it, render it, done.
12
+ *
13
+ * Behaviour (unchanged from the old inline path): when the host enables
14
+ * `config.linkPreview.autoDetect` AND provides a `resolve`-r, surface ONE
15
+ * preview card under the bubble for the FIRST URL in the message text.
16
+ * Suppressed while streaming (text isn't final), for error turns, and when
17
+ * the message already carries an explicit `link` block (so we never double
18
+ * up). Reads everything off the chat context — the caller passes only the
19
+ * message + presentational flags.
20
+ */
21
+
22
+ import { useMemo } from 'react';
23
+
24
+ import { cn } from '@djangocfg/ui-core/lib';
25
+
26
+ import { useChatContextOptional } from '../context';
27
+ import { extractFirstUrl } from '../core/extractFirstUrl';
28
+ import type { ChatMessage } from '../types';
29
+ import type { ComposerAppearance } from '../composer/types';
30
+ import { LinkPreviewCard } from '../../../common/link-preview';
31
+
32
+ export interface AutoLinkPreviewProps {
33
+ message: ChatMessage;
34
+ /** Streaming turn → suppress (text not final yet). */
35
+ isStreaming?: boolean;
36
+ /** Error turn → suppress. */
37
+ isError?: boolean;
38
+ /** Right-align the card under a user bubble. */
39
+ isUser?: boolean;
40
+ appearance?: ComposerAppearance;
41
+ }
42
+
43
+ export function AutoLinkPreview({
44
+ message,
45
+ isStreaming = false,
46
+ isError = false,
47
+ isUser = false,
48
+ appearance = 'compact',
49
+ }: AutoLinkPreviewProps) {
50
+ const ctx = useChatContextOptional();
51
+ const linkCfg = ctx?.config.linkPreview;
52
+
53
+ const url = useMemo(() => {
54
+ if (!linkCfg?.autoDetect || !linkCfg.resolve) return null;
55
+ if (isStreaming || isError) return null;
56
+ if (message.blocks?.some((b) => b.kind === 'link')) return null;
57
+ return extractFirstUrl(message.content);
58
+ }, [
59
+ linkCfg?.autoDetect,
60
+ linkCfg?.resolve,
61
+ isStreaming,
62
+ isError,
63
+ message.blocks,
64
+ message.content,
65
+ ]);
66
+
67
+ if (!url || !linkCfg?.resolve) return null;
68
+
69
+ return (
70
+ <div className={cn('mt-1.5 w-full', isUser && 'flex justify-end')}>
71
+ <LinkPreviewCard url={url} resolver={linkCfg.resolve} appearance={appearance} />
72
+ </div>
73
+ );
74
+ }
@@ -33,8 +33,7 @@ import {
33
33
  type BubbleMenuItem,
34
34
  } from './BubbleContextMenu';
35
35
  import { MessageBlocks } from './blocks';
36
- import { LinkPreviewCard } from '../../../common/link-preview';
37
- import { extractFirstUrl } from '../core/extractFirstUrl';
36
+ import { AutoLinkPreview } from './AutoLinkPreview';
38
37
  import type { BlockRegistry } from './blocks';
39
38
  import type { ToolCallsProps } from './ToolCalls';
40
39
  import type { ComposerAppearance } from '../composer/types';
@@ -212,18 +211,9 @@ const MessageBubbleInner = ({
212
211
  // registry when both are absent.
213
212
  const resolvedBlockRegistry = blockRegistry ?? ctx?.blockRegistry;
214
213
 
215
- // Auto link-preview (Telegram-style): when the host opts in and provides a
216
- // resolver, surface ONE preview card under the bubble for the first URL in
217
- // the text. Suppressed while streaming (the text isn't final), for error
218
- // turns, and when the message already carries an explicit `link` block (so
219
- // we never double up).
220
- const linkCfg = ctx?.config.linkPreview;
221
- const autoPreviewUrl = useMemo(() => {
222
- if (!linkCfg?.autoDetect || !linkCfg.resolve) return null;
223
- if (isStreaming || isErr) return null;
224
- if (message.blocks?.some((b) => b.kind === 'link')) return null;
225
- return extractFirstUrl(message.content);
226
- }, [linkCfg?.autoDetect, linkCfg?.resolve, isStreaming, isErr, message.blocks, message.content]);
214
+ // Auto link-preview (Telegram-style) is rendered below the content via the
215
+ // shared <AutoLinkPreview> slot (so a forked bubble shares the exact same
216
+ // detect+render and can't drift). See AutoLinkPreview.tsx.
227
217
 
228
218
  // URL chips (plan22): when the host opts in via `config.linkChips`, render
229
219
  // bare web URLs in the bubble as compact favicon chips (matching the
@@ -397,15 +387,13 @@ const MessageBubbleInner = ({
397
387
  />
398
388
  ) : null}
399
389
 
400
- {autoPreviewUrl && linkCfg?.resolve ? (
401
- <div className={cn('mt-1.5 w-full', isUser && 'flex justify-end')}>
402
- <LinkPreviewCard
403
- url={autoPreviewUrl}
404
- resolver={linkCfg.resolve}
405
- appearance={appearance}
406
- />
407
- </div>
408
- ) : null}
390
+ <AutoLinkPreview
391
+ message={message}
392
+ isStreaming={isStreaming}
393
+ isError={isErr}
394
+ isUser={isUser}
395
+ appearance={appearance}
396
+ />
409
397
 
410
398
  {message.toolCalls?.length
411
399
  ? toolCallsRenderer
@@ -6,8 +6,11 @@ import { ChevronDown, ChevronRight, Loader2 } from 'lucide-react';
6
6
  import { cn } from '@djangocfg/ui-core/lib';
7
7
 
8
8
  import type { ChatToolCall } from '../types';
9
+ import { ToolPayloadValue, type ToolPayloadKind } from './ToolPayloadValue';
9
10
 
10
- export type ToolPayloadKind = 'input' | 'output' | 'streaming';
11
+ // Re-exported for back-compat: `ToolPayloadKind` is now OWNED by
12
+ // ToolPayloadValue (breaking the old ToolCalls↔ToolPayloadValue import cycle).
13
+ export type { ToolPayloadKind };
11
14
 
12
15
  export interface ToolCallsProps {
13
16
  calls: ChatToolCall[];
@@ -207,26 +210,10 @@ const ToolCallItem = memo(function ToolCallItem({
207
210
  );
208
211
  });
209
212
 
213
+ // The default payload renderer delegates to the shared <ToolPayloadValue>
214
+ // (object → JsonTree, string/streaming → <pre>) so a host that owns its own
215
+ // tool-call card via `renderToolCall` can render the exact same payload with
216
+ // no duplicated routing. See ToolPayloadValue.tsx.
210
217
  function DefaultPayload({ value, kind }: { value: unknown; kind: ToolPayloadKind }) {
211
- const isStreamingOrString = kind === 'streaming' || typeof value === 'string';
212
- const muted = kind === 'input';
213
- return (
214
- <pre
215
- className={cn(
216
- 'overflow-auto rounded bg-background/60 p-1.5 font-mono',
217
- kind === 'input' ? 'max-h-32' : 'max-h-48',
218
- muted ? 'text-muted-foreground' : 'text-foreground/90',
219
- )}
220
- >
221
- {isStreamingOrString ? String(value) : safeStringify(value)}
222
- </pre>
223
- );
224
- }
225
-
226
- function safeStringify(value: unknown): string {
227
- try {
228
- return JSON.stringify(value, null, 2);
229
- } catch {
230
- return String(value);
231
- }
218
+ return <ToolPayloadValue value={value} kind={kind} />;
232
219
  }
@@ -0,0 +1,106 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * ToolPayloadValue — the single, shared renderer for a tool call's
5
+ * `input` / `output` / live `streaming` payload.
6
+ *
7
+ * Routing (one implementation, reused everywhere):
8
+ * - object / array (and NOT streaming) → interactive collapsible
9
+ * `LazyJsonTree` (the same viewer the chat `json` block + document
10
+ * inspector use). It renders values as React children — never
11
+ * `innerHTML` — so a tool result carrying HTML/markdown (e.g. a fetched
12
+ * web page) is shown as text, not executed: XSS-safe.
13
+ * - string / scalar / streaming → plain `<pre>` text. A streaming payload
14
+ * arrives as a partial string and must NOT be parsed mid-flight; prose /
15
+ * scalars have no tree to show.
16
+ *
17
+ * Both the default `DefaultPayload` (inside `ToolCalls`) AND a host that owns
18
+ * its own tool-call card (e.g. cmdop's `<ToolResult>` action card, reached via
19
+ * the `renderToolCall` override) render THROUGH this — so the object→tree
20
+ * behaviour can't drift between the two. `surfaceClassName` lets a host match
21
+ * its own card's surface token (the only thing that differed when the two
22
+ * implementations were separate).
23
+ */
24
+
25
+ import { cn } from '@djangocfg/ui-core/lib';
26
+
27
+ import { LazyJsonTree } from '../../data/JsonTree/lazy';
28
+
29
+ // Defined locally (NOT imported from ./ToolCalls) on purpose: ToolCalls
30
+ // imports ToolPayloadValue, so importing the type back would form an import
31
+ // cycle. Under the `'use client'` + subpath-barrel (`./chat` → lazy.tsx) setup
32
+ // that cycle makes Vite drop this module's symbol from the re-export graph at
33
+ // runtime (the export typechecks via index.ts but fails to bundle — the exact
34
+ // `MarkdownMessage` gotcha documented in lazy.tsx). The union is the SAME shape
35
+ // ToolCalls re-exports as `ToolPayloadKind`; keep them in sync (trivial).
36
+ export type ToolPayloadKind = 'input' | 'output' | 'streaming';
37
+
38
+ /** A structured value worth showing in the JSON tree (object or array). */
39
+ function isStructured(value: unknown): value is object {
40
+ return value !== null && typeof value === 'object';
41
+ }
42
+
43
+ function safeStringify(value: unknown): string {
44
+ try {
45
+ return JSON.stringify(value, null, 2);
46
+ } catch {
47
+ return String(value);
48
+ }
49
+ }
50
+
51
+ export interface ToolPayloadValueProps {
52
+ value: unknown;
53
+ /** Drives expand depth (input collapsed, output 1 level), muted text, and
54
+ * whether a streaming partial is parsed (it never is). */
55
+ kind: ToolPayloadKind;
56
+ /** Surface token for the tree wrapper + `<pre>` background. Defaults to the
57
+ * chat panel's `bg-background/60`; a host card can pass e.g. `bg-muted/40`. */
58
+ surfaceClassName?: string;
59
+ /**
60
+ * Show the JsonTree toolbar (search / expand / copy). Default **false**:
61
+ * the tool-call panel already has its own header (tool name + status), so a
62
+ * second toolbar is visual noise — and in `'auto'` mode JsonTree RESERVES the
63
+ * toolbar's height even while hidden, leaving an empty gap above the tree.
64
+ * Opt in (`toolbar`) only when copy/search on the raw payload is worth the
65
+ * extra chrome. When true the toolbar is hover-revealed (`'auto'`).
66
+ */
67
+ toolbar?: boolean;
68
+ }
69
+
70
+ export function ToolPayloadValue({
71
+ value,
72
+ kind,
73
+ surfaceClassName = 'bg-background/60',
74
+ toolbar = false,
75
+ }: ToolPayloadValueProps) {
76
+ if (kind !== 'streaming' && isStructured(value)) {
77
+ return (
78
+ <div className={cn('overflow-auto rounded', surfaceClassName)}>
79
+ <LazyJsonTree
80
+ data={value}
81
+ bordered={false}
82
+ size="sm"
83
+ // Input collapsed, output 1 level. Toolbar OFF by default — the
84
+ // tool-call panel owns the header; `auto` would reserve height and
85
+ // leave an empty gap above the tree (see the `toolbar` prop note).
86
+ defaultExpandedDepth={kind === 'input' ? 0 : 1}
87
+ actions={['search', 'expand', 'copy']}
88
+ toolbar={toolbar ? 'auto' : 'never'}
89
+ />
90
+ </div>
91
+ );
92
+ }
93
+
94
+ return (
95
+ <pre
96
+ className={cn(
97
+ 'overflow-auto rounded p-1.5 font-mono',
98
+ surfaceClassName,
99
+ kind === 'input' ? 'max-h-32' : 'max-h-48',
100
+ kind === 'input' ? 'text-muted-foreground' : 'text-foreground/90',
101
+ )}
102
+ >
103
+ {typeof value === 'string' ? value : safeStringify(value)}
104
+ </pre>
105
+ );
106
+ }
@@ -153,6 +153,13 @@ export default function MapBlockRenderer({ block, ctx }: BlockRendererProps<MapB
153
153
  <LazyMapContainer
154
154
  initialViewport={initialViewport}
155
155
  fullscreenTitle={block.caption ?? 'Map'}
156
+ // A chat transcript column is < 768px, which would otherwise trip the
157
+ // map's width-based "mobile" collapse (gestures off, scroll-lock
158
+ // overlay invisible, just a static tile) even on a desktop with a fine
159
+ // pointer. `0` opts out of width-based collapse — the map keeps its
160
+ // desktop scroll-lock + controls in the narrow column, and a REAL
161
+ // touchscreen still degrades to the mobile preview (coarse pointer).
162
+ mobileBreakpoint={0}
156
163
  {...mapOptions}
157
164
  >
158
165
  {/* Filled zones first so routes/markers stack above them. */}
@@ -8,6 +8,9 @@ export {
8
8
  type MessageListHandle,
9
9
  } from './MessageList';
10
10
  export { MessageBubble, type MessageBubbleProps } from './MessageBubble';
11
+ // The auto-detect link-preview slot — shared by MessageBubble and any forked
12
+ // bubble (e.g. cmdop's mention bubble) so previews never silently drift.
13
+ export { AutoLinkPreview, type AutoLinkPreviewProps } from './AutoLinkPreview';
11
14
  // Re-exported from the shared `common/link-preview` location (the card was
12
15
  // hoisted out of chat so the NotionEditor bookmark node can reuse it). The
13
16
  // chat public surface (`@djangocfg/ui-tools/chat`) keeps exporting it from
@@ -24,6 +27,9 @@ export {
24
27
  } from './BubbleContextMenu';
25
28
  export { Sources, type SourcesProps } from './Sources';
26
29
  export { ToolCalls, type ToolCallsProps, type ToolPayloadKind } from './ToolCalls';
30
+ // Shared payload renderer (object → JsonTree, string/streaming → <pre>) so a
31
+ // host that overrides `renderToolCall` renders payloads identically.
32
+ export { ToolPayloadValue, type ToolPayloadValueProps } from './ToolPayloadValue';
27
33
  export {
28
34
  Attachments,
29
35
  AttachmentsGrid,
@@ -40,6 +40,7 @@ export function MapInner({
40
40
  showResetButton = false,
41
41
  scrollProtection = true,
42
42
  scrollProtectionHint = 'Click to interact',
43
+ mobileBreakpoint,
43
44
  fullscreenButton = true,
44
45
  fullscreenLabel = 'Expand map',
45
46
  fullscreenTitle = 'Map',
@@ -148,7 +149,11 @@ export function MapInner({
148
149
  // keeps the Google-Maps scroll-isolation. The expanded map is always
149
150
  // live. Detection is `matchMedia`-based so it reacts to DevTools device
150
151
  // emulation, real touchscreens and window resizes (see useMapTouchMode).
151
- const isTouch = useMapTouchMode()
152
+ // `mobileBreakpoint` lets a narrow-column embed (chat transcript < md) opt
153
+ // out of width-based mobile collapse: pass 0 → only a real coarse pointer
154
+ // (touchscreen) triggers the mobile preview, not the column's width. When
155
+ // undefined, useMapTouchMode keeps its 768px default.
156
+ const isTouch = useMapTouchMode(mobileBreakpoint)
152
157
  const protectionMode = !scrollProtection || isFullscreenInstance
153
158
  ? 'off'
154
159
  : isTouch
@@ -94,6 +94,18 @@ export interface MapContainerProps {
94
94
  scrollProtection?: boolean
95
95
  /** Hint shown on the lock overlay while scroll-zoom is blocked. */
96
96
  scrollProtectionHint?: string
97
+ /**
98
+ * Width (px) at/under which the map collapses to the touch/"mobile"
99
+ * preview (gestures off, tap-to-expand) even with a fine pointer. Passed
100
+ * to `useMapTouchMode`. Default 768 (Tailwind `md`).
101
+ *
102
+ * Set `0` to OPT OUT of width-based collapse — the map then only goes
103
+ * mobile on a real coarse pointer (touchscreen), not because its column
104
+ * is narrow. Use this for a map embedded in a narrow column (e.g. a chat
105
+ * transcript < 768px) that should keep the desktop scroll-lock + controls.
106
+ * @default 768
107
+ */
108
+ mobileBreakpoint?: number
97
109
  /**
98
110
  * Show a button that opens the map full-screen in a large dialog
99
111
  * (Google-Maps-style "expand"). Scroll protection is off inside the
@@ -14,7 +14,7 @@ import { useIsTouch, useMediaQuery, MEDIA_BREAKPOINTS } from '@djangocfg/ui-core
14
14
  * @param breakpoint width (px) at/under which the map is treated as mobile
15
15
  * even with a fine pointer. Default 768 (Tailwind `md`).
16
16
  */
17
- export function useMapTouchMode(breakpoint = MEDIA_BREAKPOINTS.md): boolean {
17
+ export function useMapTouchMode(breakpoint: number = MEDIA_BREAKPOINTS.md): boolean {
18
18
  const isTouch = useIsTouch()
19
19
  const isNarrow = useMediaQuery(`(max-width: ${breakpoint - 1}px)`)
20
20
  return isTouch || isNarrow