@redocly/theme 0.61.1 → 0.62.0-custom.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.
Files changed (188) hide show
  1. package/lib/components/AsyncApiDocs/hooks/AfterAsyncApiChannelDescription.d.ts +1 -0
  2. package/lib/components/AsyncApiDocs/hooks/AfterAsyncApiChannelDescription.js +12 -0
  3. package/lib/components/Badge/Badge.d.ts +2 -1
  4. package/lib/components/Badge/Badge.js +24 -2
  5. package/lib/components/Banner/Banner.js +19 -1
  6. package/lib/components/Banner/variables.js +1 -0
  7. package/lib/components/Breadcrumbs/Breadcrumb.js +1 -1
  8. package/lib/components/Breadcrumbs/BreadcrumbDropdown.js +9 -6
  9. package/lib/components/Breadcrumbs/Breadcrumbs.js +24 -15
  10. package/lib/components/Buttons/AIAssistantButton.js +7 -4
  11. package/lib/components/Catalog/CatalogEntities.js +10 -8
  12. package/lib/components/Catalog/CatalogEntity/CatalogEntity.js +2 -2
  13. package/lib/components/Catalog/CatalogEntity/CatalogEntityHistory/CatalogEntityHistorySidebar.js +1 -1
  14. package/lib/components/Catalog/CatalogEntity/CatalogEntityProperties/TagsProperty.js +2 -2
  15. package/lib/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityRelationsTable.js +13 -11
  16. package/lib/components/Catalog/CatalogEntity/CatalogEntitySchema.js +7 -5
  17. package/lib/components/Catalog/CatalogFilter/CatalogFilterCheckboxes.js +9 -7
  18. package/lib/components/Catalog/CatalogTableView/CatalogTableViewRow.js +1 -1
  19. package/lib/components/Catalog/CatalogTagsWithTooltip.js +2 -2
  20. package/lib/components/CatalogClassic/CatalogClassicInfoBlock.js +1 -1
  21. package/lib/components/CodeBlock/CodeBlockControls.js +8 -6
  22. package/lib/components/Filter/FilterCheckboxes.js +1 -1
  23. package/lib/components/JsonViewer/JsonViewer.js +2 -2
  24. package/lib/components/JsonViewer/{Helpers.js → helpers.js} +2 -1
  25. package/lib/components/LanguagePicker/LanguagePicker.js +1 -1
  26. package/lib/components/Markdown/Markdown.js +2 -2
  27. package/lib/components/Menu/MenuItem.js +41 -15
  28. package/lib/components/Navbar/NavbarItem.js +1 -1
  29. package/lib/components/OpenApiDocs/hooks/AdditionalOverviewInfo.d.ts +1 -0
  30. package/lib/components/OpenApiDocs/hooks/AdditionalOverviewInfo.js +12 -0
  31. package/lib/components/OpenApiDocs/hooks/AfterOpenApiDescription.d.ts +1 -0
  32. package/lib/components/OpenApiDocs/hooks/AfterOpenApiDescription.js +6 -0
  33. package/lib/components/PageActions/PageActions.js +25 -8
  34. package/lib/components/Search/SearchAiDialog.d.ts +4 -2
  35. package/lib/components/Search/SearchAiDialog.js +23 -4
  36. package/lib/components/Search/SearchAiMessage.d.ts +4 -2
  37. package/lib/components/Search/SearchAiMessage.js +82 -23
  38. package/lib/components/Search/SearchDialog.js +50 -25
  39. package/lib/components/Select/variables.js +2 -2
  40. package/lib/components/SvgViewer/SvgViewer.d.ts +15 -0
  41. package/lib/components/SvgViewer/SvgViewer.js +312 -0
  42. package/lib/components/SvgViewer/variables.d.ts +1 -0
  43. package/lib/components/SvgViewer/variables.dark.d.ts +1 -0
  44. package/lib/components/SvgViewer/variables.dark.js +8 -0
  45. package/lib/components/SvgViewer/variables.js +17 -0
  46. package/lib/components/Tag/Tag.js +1 -1
  47. package/lib/components/Tag/variables.dark.js +6 -0
  48. package/lib/components/Tag/variables.js +6 -0
  49. package/lib/components/Tooltip/Tooltip.d.ts +2 -3
  50. package/lib/components/Tooltip/Tooltip.js +66 -113
  51. package/lib/components/Tooltip/variables.dark.js +4 -0
  52. package/lib/components/Tooltip/variables.js +3 -3
  53. package/lib/components/UserMenu/LoginButton.d.ts +8 -2
  54. package/lib/components/UserMenu/LoginButton.js +4 -3
  55. package/lib/core/constants/search.d.ts +5 -1
  56. package/lib/core/constants/search.js +24 -1
  57. package/lib/core/hooks/search/use-search-dialog.js +2 -2
  58. package/lib/core/hooks/use-color-switcher.js +3 -1
  59. package/lib/core/hooks/use-mcp-config.js +2 -1
  60. package/lib/core/hooks/use-modal-scroll-lock.js +24 -10
  61. package/lib/core/hooks/use-outside-click.d.ts +3 -1
  62. package/lib/core/hooks/use-outside-click.js +8 -4
  63. package/lib/core/hooks/use-page-actions.d.ts +1 -1
  64. package/lib/core/hooks/use-page-actions.js +44 -11
  65. package/lib/core/hooks/use-product-picker.js +1 -1
  66. package/lib/core/hooks/use-unique-svg-ids.d.ts +6 -0
  67. package/lib/core/hooks/use-unique-svg-ids.js +15 -0
  68. package/lib/core/openapi/index.d.ts +1 -0
  69. package/lib/core/openapi/index.js +3 -1
  70. package/lib/core/styles/dark.js +2 -0
  71. package/lib/core/styles/global.js +31 -15
  72. package/lib/core/types/catalog.d.ts +1 -1
  73. package/lib/core/types/hooks.d.ts +23 -2
  74. package/lib/core/types/l10n.d.ts +1 -1
  75. package/lib/core/types/search.d.ts +24 -0
  76. package/lib/core/types/search.js +9 -1
  77. package/lib/core/utils/content-segments.d.ts +2 -0
  78. package/lib/core/utils/content-segments.js +22 -0
  79. package/lib/core/utils/index.d.ts +1 -0
  80. package/lib/core/utils/index.js +1 -0
  81. package/lib/core/utils/transform-revisions-to-version-history.js +8 -51
  82. package/lib/ext/process-scorecard.d.ts +5 -0
  83. package/lib/ext/process-scorecard.js +11 -0
  84. package/lib/icons/FitToViewIcon/FitToViewIcon.d.ts +9 -0
  85. package/lib/icons/FitToViewIcon/FitToViewIcon.js +25 -0
  86. package/lib/index.d.ts +8 -0
  87. package/lib/index.js +8 -0
  88. package/lib/layouts/DocumentationLayout.js +4 -25
  89. package/lib/layouts/DocumentationLayoutBottom.d.ts +11 -0
  90. package/lib/layouts/DocumentationLayoutBottom.js +28 -0
  91. package/lib/layouts/DocumentationLayoutTop.d.ts +13 -0
  92. package/lib/layouts/DocumentationLayoutTop.js +33 -0
  93. package/lib/layouts/Forbidden.js +22 -18
  94. package/lib/markdoc/components/Cards/Card.js +1 -0
  95. package/lib/markdoc/components/CodeWalkthrough/CodeFilters.js +1 -1
  96. package/lib/markdoc/components/Heading/Heading.js +40 -2
  97. package/lib/markdoc/components/LoginButton/LoginButton.d.ts +9 -0
  98. package/lib/markdoc/components/LoginButton/LoginButton.js +48 -0
  99. package/lib/markdoc/components/Mermaid/Mermaid.js +70 -2
  100. package/lib/markdoc/components/default.d.ts +1 -0
  101. package/lib/markdoc/components/default.js +1 -0
  102. package/lib/markdoc/default.d.ts +6 -0
  103. package/lib/markdoc/default.js +2 -0
  104. package/lib/markdoc/tags/login-button.d.ts +2 -0
  105. package/lib/markdoc/tags/login-button.js +32 -0
  106. package/package.json +8 -8
  107. package/src/components/AsyncApiDocs/hooks/AfterAsyncApiChannelDescription.tsx +10 -0
  108. package/src/components/Badge/Badge.tsx +18 -2
  109. package/src/components/Banner/Banner.tsx +23 -1
  110. package/src/components/Banner/variables.ts +1 -0
  111. package/src/components/Breadcrumbs/Breadcrumb.tsx +3 -3
  112. package/src/components/Breadcrumbs/BreadcrumbDropdown.tsx +11 -8
  113. package/src/components/Breadcrumbs/Breadcrumbs.tsx +24 -15
  114. package/src/components/Buttons/AIAssistantButton.tsx +7 -4
  115. package/src/components/Catalog/CatalogEntities.tsx +10 -8
  116. package/src/components/Catalog/CatalogEntity/CatalogEntity.tsx +1 -1
  117. package/src/components/Catalog/CatalogEntity/CatalogEntityHistory/CatalogEntityHistorySidebar.tsx +1 -2
  118. package/src/components/Catalog/CatalogEntity/CatalogEntityProperties/TagsProperty.tsx +1 -1
  119. package/src/components/Catalog/CatalogEntity/CatalogEntityRelations/CatalogEntityRelationsTable.tsx +13 -11
  120. package/src/components/Catalog/CatalogEntity/CatalogEntitySchema.tsx +7 -5
  121. package/src/components/Catalog/CatalogFilter/CatalogFilterCheckboxes.tsx +9 -7
  122. package/src/components/Catalog/CatalogTableView/CatalogTableViewRow.tsx +1 -2
  123. package/src/components/Catalog/CatalogTagsWithTooltip.tsx +9 -5
  124. package/src/components/CatalogClassic/CatalogClassicInfoBlock.tsx +3 -1
  125. package/src/components/CodeBlock/CodeBlockControls.tsx +16 -10
  126. package/src/components/Filter/FilterCheckboxes.tsx +1 -1
  127. package/src/components/JsonViewer/JsonViewer.tsx +1 -2
  128. package/src/components/JsonViewer/{Helpers.tsx → helpers.tsx} +1 -0
  129. package/src/components/LanguagePicker/LanguagePicker.tsx +1 -1
  130. package/src/components/Markdown/Markdown.tsx +2 -2
  131. package/src/components/Menu/MenuItem.tsx +61 -16
  132. package/src/components/Navbar/NavbarItem.tsx +3 -1
  133. package/src/components/OpenApiDocs/hooks/AdditionalOverviewInfo.tsx +10 -0
  134. package/src/components/OpenApiDocs/hooks/AfterOpenApiDescription.tsx +2 -0
  135. package/src/components/PageActions/PageActions.tsx +38 -15
  136. package/src/components/Search/SearchAiDialog.tsx +31 -2
  137. package/src/components/Search/SearchAiMessage.tsx +103 -17
  138. package/src/components/Search/SearchDialog.tsx +70 -37
  139. package/src/components/Select/variables.ts +2 -2
  140. package/src/components/SvgViewer/SvgViewer.tsx +405 -0
  141. package/src/components/SvgViewer/variables.dark.ts +5 -0
  142. package/src/components/SvgViewer/variables.ts +14 -0
  143. package/src/components/Tag/Tag.tsx +2 -1
  144. package/src/components/Tag/variables.dark.ts +6 -0
  145. package/src/components/Tag/variables.ts +6 -0
  146. package/src/components/Tooltip/Tooltip.tsx +77 -120
  147. package/src/components/Tooltip/variables.dark.ts +4 -0
  148. package/src/components/Tooltip/variables.ts +3 -3
  149. package/src/components/UserMenu/LoginButton.tsx +23 -8
  150. package/src/core/constants/search.ts +27 -1
  151. package/src/core/hooks/__mocks__/use-theme-hooks.ts +10 -1
  152. package/src/core/hooks/search/use-search-dialog.ts +2 -2
  153. package/src/core/hooks/use-color-switcher.ts +3 -1
  154. package/src/core/hooks/use-mcp-config.ts +2 -1
  155. package/src/core/hooks/use-modal-scroll-lock.ts +29 -10
  156. package/src/core/hooks/use-outside-click.ts +16 -5
  157. package/src/core/hooks/use-page-actions.ts +66 -25
  158. package/src/core/hooks/use-product-picker.ts +1 -1
  159. package/src/core/hooks/use-unique-svg-ids.ts +12 -0
  160. package/src/core/openapi/index.ts +1 -0
  161. package/src/core/styles/dark.ts +2 -0
  162. package/src/core/styles/global.ts +31 -15
  163. package/src/core/types/catalog.ts +1 -1
  164. package/src/core/types/hooks.ts +29 -1
  165. package/src/core/types/l10n.ts +12 -1
  166. package/src/core/types/search.ts +19 -0
  167. package/src/core/utils/content-segments.ts +27 -0
  168. package/src/core/utils/index.ts +1 -0
  169. package/src/core/utils/transform-revisions-to-version-history.ts +8 -80
  170. package/src/ext/process-scorecard.ts +14 -0
  171. package/src/icons/FitToViewIcon/FitToViewIcon.tsx +26 -0
  172. package/src/index.ts +8 -0
  173. package/src/layouts/DocumentationLayout.tsx +4 -30
  174. package/src/layouts/DocumentationLayoutBottom.tsx +42 -0
  175. package/src/layouts/DocumentationLayoutTop.tsx +52 -0
  176. package/src/layouts/Forbidden.tsx +36 -21
  177. package/src/markdoc/components/Cards/Card.tsx +1 -0
  178. package/src/markdoc/components/CodeWalkthrough/CodeFilters.tsx +1 -1
  179. package/src/markdoc/components/Heading/Heading.tsx +52 -4
  180. package/src/markdoc/components/LoginButton/LoginButton.tsx +38 -0
  181. package/src/markdoc/components/Mermaid/Mermaid.tsx +57 -8
  182. package/src/markdoc/components/default.ts +1 -0
  183. package/src/markdoc/default.ts +2 -0
  184. package/src/markdoc/tags/login-button.ts +30 -0
  185. package/lib/components/Tooltip/TooltipWrapper.d.ts +0 -12
  186. package/lib/components/Tooltip/TooltipWrapper.js +0 -34
  187. package/src/components/Tooltip/TooltipWrapper.tsx +0 -70
  188. /package/lib/components/JsonViewer/{Helpers.d.ts → helpers.d.ts} +0 -0
@@ -16,6 +16,7 @@ import {
16
16
  useDialogHotKeys,
17
17
  useSearchFilter,
18
18
  useRecentSearches,
19
+ useModalScrollLock,
19
20
  } from '@redocly/theme/core/hooks';
20
21
  import { Tag } from '@redocly/theme/components/Tag/Tag';
21
22
  import { CloseIcon } from '@redocly/theme/icons/CloseIcon/CloseIcon';
@@ -75,7 +76,7 @@ export function SearchDialog({
75
76
  } = useSearchFilter(filter, setFilter);
76
77
  const { addSearchHistoryItem } = useRecentSearches();
77
78
  const aiSearch = useAiSearch({ filter });
78
-
79
+ useModalScrollLock(true);
79
80
  const searchInputRef = useRef<HTMLInputElement>(null);
80
81
  const modalRef = useRef<HTMLDivElement>(null);
81
82
 
@@ -163,14 +164,17 @@ export function SearchDialog({
163
164
  innerRef={innerRef}
164
165
  onClick={() => {
165
166
  addSearchHistoryItem(query);
166
- telemetry.sendSearchResultClickedMessage({
167
- query,
168
- url: item.document.url,
169
- totalResults: results.length.toString(),
170
- index: index.toString(),
171
- searchEngine: mode,
172
- searchSessionId,
173
- });
167
+ telemetry.sendSearchResultClickedMessage([
168
+ {
169
+ object: 'search',
170
+ query: query,
171
+ url: item.document.url,
172
+ totalResults: results.length.toString(),
173
+ index: index.toString(),
174
+ searchEngine: mode,
175
+ searchSessionId,
176
+ },
177
+ ]);
174
178
  onClose();
175
179
  }}
176
180
  />
@@ -246,9 +250,12 @@ export function SearchDialog({
246
250
  if (query.trim()) {
247
251
  aiSearch.askQuestion(query);
248
252
  }
249
- telemetry.sendSearchAiOpenedMessage({
250
- method: 'ai_search_button',
251
- });
253
+ telemetry.sendSearchAiOpenedMessage([
254
+ {
255
+ object: 'search',
256
+ method: 'ai_search_button',
257
+ },
258
+ ]);
252
259
  }}
253
260
  >
254
261
  {translate('search.ai.button', 'Search with AI')}
@@ -262,20 +269,27 @@ export function SearchDialog({
262
269
  </>
263
270
  ) : (
264
271
  <AiDialogHeaderWrapper>
265
- <Button
266
- variant="secondary"
267
- onClick={() => {
268
- setMode('search');
269
- aiSearch.clearConversation();
270
- focusSearchInput();
271
- }}
272
- tabIndex={0}
273
- icon={<ChevronLeftIcon />}
274
- >
275
- {isMobile
276
- ? translate('search.ai.back', 'Back')
277
- : translate('search.ai.backToSearch', 'Back to search')}
278
- </Button>
272
+ {initialMode === 'ai-dialog' ? (
273
+ <AiDialogHeaderTitle>
274
+ <AiStarsGradientIcon color="var(--search-ai-button-icon-color)" size="1.25rem" />
275
+ {translate('search.ai.assistant', 'Assistant')}
276
+ </AiDialogHeaderTitle>
277
+ ) : (
278
+ <Button
279
+ variant="secondary"
280
+ onClick={() => {
281
+ setMode('search');
282
+ aiSearch.clearConversation();
283
+ focusSearchInput();
284
+ }}
285
+ tabIndex={0}
286
+ icon={<ChevronLeftIcon />}
287
+ >
288
+ {isMobile
289
+ ? translate('search.ai.back', 'Back')
290
+ : translate('search.ai.backToSearch', 'Back to search')}
291
+ </Button>
292
+ )}
279
293
  <AiDialogHeaderActionsWrapper>
280
294
  <Button
281
295
  variant="secondary"
@@ -327,9 +341,12 @@ export function SearchDialog({
327
341
  if (query.trim()) {
328
342
  aiSearch.askQuestion(query);
329
343
  }
330
- telemetry.sendSearchAiOpenedMessage({
331
- method: 'ai_search_input',
332
- });
344
+ telemetry.sendSearchAiOpenedMessage([
345
+ {
346
+ object: 'search',
347
+ method: 'ai_search_input',
348
+ },
349
+ ]);
333
350
  }}
334
351
  onKeyDown={(e) => {
335
352
  if (e.key === 'Enter') {
@@ -337,9 +354,12 @@ export function SearchDialog({
337
354
  if (query.trim()) {
338
355
  aiSearch.askQuestion(query);
339
356
  }
340
- telemetry.sendSearchAiOpenedMessage({
341
- method: 'ai_search_input',
342
- });
357
+ telemetry.sendSearchAiOpenedMessage([
358
+ {
359
+ object: 'search',
360
+ method: 'ai_search_input',
361
+ },
362
+ ]);
343
363
  }
344
364
  }}
345
365
  ref={aiQueryRef}
@@ -423,11 +443,14 @@ export function SearchDialog({
423
443
  <>
424
444
  <SearchRecent
425
445
  onSelect={(query, index) => {
426
- telemetry.sendSearchRecentClickedMessage({
427
- query,
428
- index: index.toString(),
429
- searchSessionId,
430
- });
446
+ telemetry.sendSearchRecentClickedMessage([
447
+ {
448
+ object: 'search',
449
+ query,
450
+ index: index.toString(),
451
+ searchSessionId,
452
+ },
453
+ ]);
431
454
  setQuery(query);
432
455
  focusSearchInput();
433
456
  }}
@@ -447,6 +470,8 @@ export function SearchDialog({
447
470
  conversation={aiSearch.conversation}
448
471
  setConversation={aiSearch.setConversation}
449
472
  onMessageSent={aiSearch.askQuestion}
473
+ toolCalls={aiSearch.toolCalls}
474
+ contentSegments={aiSearch.contentSegments}
450
475
  />
451
476
  )}
452
477
  </SearchDialogBody>
@@ -567,6 +592,14 @@ const AiDialogHeaderActionsWrapper = styled.div`
567
592
  gap: var(--spacing-xxs);
568
593
  `;
569
594
 
595
+ const AiDialogHeaderTitle = styled.span`
596
+ display: flex;
597
+ align-items: center;
598
+ gap: var(--spacing-xs);
599
+ font-weight: var(--font-weight-semibold);
600
+ font-size: var(--font-size-lg);
601
+ `;
602
+
570
603
  const SearchDialogBody = styled.div`
571
604
  display: flex;
572
605
  flex-direction: row-reverse;
@@ -10,7 +10,7 @@ export const select = css`
10
10
  --select-text-color: var(--text-color-secondary); // @presenter Color
11
11
  --select-border-radius: var(--border-radius); // @presenter BorderRadius
12
12
  --select-border: var(--border-width) var(--border-style) var(--border-color-primary); // @presenter Border
13
-
13
+
14
14
  --select-input-padding-vertical: 6px; // @presenter Spacing
15
15
  --select-input-padding-horizontal: 6px; // @presenter Spacing
16
16
  --select-input-padding: var(--select-input-padding-vertical) var(--select-input-padding-horizontal);
@@ -38,7 +38,7 @@ export const select = css`
38
38
  --select-list-item-border-radius: var(--border-radius); // @presenter BorderRadius
39
39
  --select-list-item-bg-color-active: transparent; // @presenter Color
40
40
  --select-list-item-bg-color-hover: var(--menu-item-bg-color-hover); // @presenter Color
41
- --select-list-item-font-weight-active: var(--font-weight-medium); // @presenter Color
41
+ --select-list-item-font-weight-active: var(--font-weight-medium); // @presenter Color
42
42
 
43
43
  --select-placeholder-color: var(--color-warm-grey-6); // @presenter Color
44
44
  // @tokens End
@@ -0,0 +1,405 @@
1
+ import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
2
+ import styled, { keyframes } from 'styled-components';
3
+
4
+ import type { JSX, TouchEvent as ReactTouchEvent, WheelEvent, MouseEvent, ReactNode } from 'react';
5
+
6
+ import { useModalScrollLock } from '@redocly/theme/core/hooks';
7
+ import { Button } from '@redocly/theme/components/Button/Button';
8
+ import { Tooltip } from '@redocly/theme/components/Tooltip/Tooltip';
9
+ import { AddIcon } from '@redocly/theme/icons/AddIcon/AddIcon';
10
+ import { SubtractIcon } from '@redocly/theme/icons/SubtractIcon/SubtractIcon';
11
+ import { CloseIcon } from '@redocly/theme/icons/CloseIcon/CloseIcon';
12
+ import { FitToViewIcon } from '@redocly/theme/icons/FitToViewIcon/FitToViewIcon';
13
+
14
+ export type SvgViewerLabels = {
15
+ zoomIn?: string;
16
+ zoomOut?: string;
17
+ fitToView?: string;
18
+ close?: string;
19
+ dialogLabel?: string;
20
+ };
21
+
22
+ export type SvgViewerProps = {
23
+ isOpen: boolean;
24
+ onClose: () => void;
25
+ children: ReactNode;
26
+ labels?: SvgViewerLabels;
27
+ };
28
+
29
+ type Position = { x: number; y: number };
30
+
31
+ const MIN_SCALE_FACTOR = 0.1;
32
+ const MAX_SCALE_FACTOR = 5;
33
+ const ZOOM_STEP = 0.1;
34
+ const WHEEL_SENSITIVITY = 0.002;
35
+ const VIEWPORT_PADDING = 60;
36
+ const FIT_SCALE_FACTOR = 0.9;
37
+
38
+ export function SvgViewer({
39
+ isOpen,
40
+ onClose,
41
+ children,
42
+ labels = {},
43
+ }: SvgViewerProps): JSX.Element | null {
44
+ const [scale, setScale] = useState(1);
45
+ const [baseScale, setBaseScale] = useState(1);
46
+ const [position, setPosition] = useState<Position>({ x: 0, y: 0 });
47
+ const [isDragging, setIsDragging] = useState(false);
48
+ const [dragStart, setDragStart] = useState<Position>({ x: 0, y: 0 });
49
+ const [pinchState, setPinchState] = useState<{ distance: number; scale: number } | null>(null);
50
+ const [isWheelZooming, setIsWheelZooming] = useState(false);
51
+
52
+ const wheelTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
53
+ const overlayRef = useRef<HTMLDivElement>(null);
54
+ const viewportRef = useRef<HTMLDivElement>(null);
55
+ const contentRef = useRef<HTMLDivElement>(null);
56
+ const renderedScaleRef = useRef(scale);
57
+
58
+ useModalScrollLock(isOpen);
59
+
60
+ // Keep track of the actually rendered scale for accurate measurements
61
+ useLayoutEffect(() => {
62
+ renderedScaleRef.current = scale;
63
+ }, [scale]);
64
+
65
+ const minScale = baseScale * MIN_SCALE_FACTOR;
66
+ const maxScale = baseScale * MAX_SCALE_FACTOR;
67
+
68
+ const clampScale = useCallback(
69
+ (value: number) => Math.min(maxScale, Math.max(minScale, value)),
70
+ [minScale, maxScale],
71
+ );
72
+
73
+ const calculateFitScale = useCallback(() => {
74
+ if (!viewportRef.current || !contentRef.current) return 1;
75
+
76
+ const viewport = viewportRef.current.getBoundingClientRect();
77
+ const svg = contentRef.current.querySelector('svg');
78
+ if (!svg) return 1;
79
+
80
+ const svgRect = svg.getBoundingClientRect();
81
+ if (!svgRect.width || !svgRect.height) return 1;
82
+
83
+ // getBoundingClientRect returns transformed size, so compensate for current scale
84
+ const currentScale = renderedScaleRef.current || 1;
85
+ const naturalWidth = svgRect.width / currentScale;
86
+ const naturalHeight = svgRect.height / currentScale;
87
+
88
+ const availableWidth = viewport.width - VIEWPORT_PADDING * 2;
89
+ const availableHeight = viewport.height - VIEWPORT_PADDING * 2;
90
+
91
+ return (
92
+ Math.min(availableWidth / naturalWidth, availableHeight / naturalHeight) * FIT_SCALE_FACTOR
93
+ );
94
+ }, []);
95
+
96
+ const resetView = useCallback(() => {
97
+ setScale(baseScale);
98
+ setPosition({ x: 0, y: 0 });
99
+ }, [baseScale]);
100
+
101
+ const zoomIn = useCallback(() => {
102
+ setScale((s) => clampScale(s + baseScale * ZOOM_STEP));
103
+ }, [baseScale, clampScale]);
104
+
105
+ const zoomOut = useCallback(() => {
106
+ setScale((s) => clampScale(s - baseScale * ZOOM_STEP));
107
+ }, [baseScale, clampScale]);
108
+
109
+ const handleKeyDown = useCallback(
110
+ (e: React.KeyboardEvent) => {
111
+ switch (e.key) {
112
+ case 'Escape':
113
+ onClose();
114
+ break;
115
+ case '+':
116
+ case '=':
117
+ zoomIn();
118
+ break;
119
+ case '-':
120
+ zoomOut();
121
+ break;
122
+ case '0':
123
+ resetView();
124
+ break;
125
+ }
126
+ },
127
+ [onClose, zoomIn, zoomOut, resetView],
128
+ );
129
+
130
+ const handleWheel = useCallback(
131
+ (e: WheelEvent) => {
132
+ e.preventDefault();
133
+ e.stopPropagation();
134
+
135
+ setIsWheelZooming(true);
136
+ if (wheelTimeoutRef.current) clearTimeout(wheelTimeoutRef.current);
137
+ wheelTimeoutRef.current = setTimeout(() => setIsWheelZooming(false), 150);
138
+
139
+ const delta = -e.deltaY * WHEEL_SENSITIVITY;
140
+ setScale((s) => clampScale(s + s * delta));
141
+ },
142
+ [clampScale],
143
+ );
144
+
145
+ const handleMouseDown = useCallback(
146
+ (e: MouseEvent) => {
147
+ if (e.button !== 0) return;
148
+ e.preventDefault();
149
+ setIsDragging(true);
150
+ setDragStart({ x: e.clientX - position.x, y: e.clientY - position.y });
151
+ },
152
+ [position],
153
+ );
154
+
155
+ const handleMouseMove = useCallback(
156
+ (e: MouseEvent) => {
157
+ if (!isDragging) return;
158
+ setPosition({ x: e.clientX - dragStart.x, y: e.clientY - dragStart.y });
159
+ },
160
+ [isDragging, dragStart],
161
+ );
162
+
163
+ const handleMouseUp = useCallback(() => setIsDragging(false), []);
164
+
165
+ const getTouchDistance = (touches: React.TouchList): number => {
166
+ if (touches.length !== 2) return 0;
167
+ const dx = touches[0].clientX - touches[1].clientX;
168
+ const dy = touches[0].clientY - touches[1].clientY;
169
+ return Math.hypot(dx, dy);
170
+ };
171
+
172
+ const handleTouchStart = useCallback(
173
+ (e: ReactTouchEvent) => {
174
+ if (e.touches.length === 2) {
175
+ setPinchState({ distance: getTouchDistance(e.touches), scale });
176
+ } else if (e.touches.length === 1) {
177
+ setIsDragging(true);
178
+ setDragStart({
179
+ x: e.touches[0].clientX - position.x,
180
+ y: e.touches[0].clientY - position.y,
181
+ });
182
+ }
183
+ },
184
+ [position, scale],
185
+ );
186
+
187
+ const handleTouchMove = useCallback(
188
+ (e: ReactTouchEvent) => {
189
+ e.preventDefault();
190
+ if (e.touches.length === 2 && pinchState) {
191
+ const distance = getTouchDistance(e.touches);
192
+ setScale(clampScale(pinchState.scale * (distance / pinchState.distance)));
193
+ } else if (e.touches.length === 1 && isDragging) {
194
+ setPosition({
195
+ x: e.touches[0].clientX - dragStart.x,
196
+ y: e.touches[0].clientY - dragStart.y,
197
+ });
198
+ }
199
+ },
200
+ [pinchState, isDragging, dragStart, clampScale],
201
+ );
202
+
203
+ const handleTouchEnd = useCallback(() => {
204
+ setIsDragging(false);
205
+ setPinchState(null);
206
+ }, []);
207
+
208
+ useEffect(() => {
209
+ if (!isOpen) return;
210
+
211
+ setPosition({ x: 0, y: 0 });
212
+ overlayRef.current?.focus();
213
+
214
+ // Wait for DOM to be ready before measuring
215
+ requestAnimationFrame(() => {
216
+ const fitScale = calculateFitScale();
217
+ setBaseScale(fitScale);
218
+ setScale(fitScale);
219
+ });
220
+ }, [isOpen, calculateFitScale]);
221
+
222
+ if (!isOpen) return null;
223
+
224
+ const zoomPercentage = baseScale > 0 ? Math.round((scale / baseScale) * 100) : 100;
225
+ const isAnimating = !isDragging && !isWheelZooming && !pinchState;
226
+
227
+ return (
228
+ <Overlay
229
+ ref={overlayRef}
230
+ onClick={onClose}
231
+ onKeyDown={handleKeyDown}
232
+ tabIndex={0}
233
+ aria-modal="true"
234
+ role="dialog"
235
+ aria-label={labels.dialogLabel || 'SVG viewer'}
236
+ >
237
+ <Viewport
238
+ ref={viewportRef}
239
+ onClick={(e) => e.stopPropagation()}
240
+ onWheel={handleWheel}
241
+ onMouseDown={handleMouseDown}
242
+ onMouseMove={handleMouseMove}
243
+ onMouseUp={handleMouseUp}
244
+ onMouseLeave={handleMouseUp}
245
+ onTouchStart={handleTouchStart}
246
+ onTouchMove={handleTouchMove}
247
+ onTouchEnd={handleTouchEnd}
248
+ $isDragging={isDragging}
249
+ >
250
+ <Content
251
+ ref={contentRef}
252
+ $isAnimating={isAnimating}
253
+ style={{
254
+ transform: `translate(calc(-50% + ${position.x}px), calc(-50% + ${position.y}px)) scale(${scale})`,
255
+ }}
256
+ >
257
+ {children}
258
+ </Content>
259
+ <Controls>
260
+ <ControlGroup>
261
+ <Tooltip tip={labels.zoomOut || 'Zoom out'} placement="top">
262
+ <ControlButton
263
+ variant="text"
264
+ size="small"
265
+ icon={<SubtractIcon />}
266
+ onClick={zoomOut}
267
+ disabled={scale <= minScale}
268
+ />
269
+ </Tooltip>
270
+ <ZoomLabel>{zoomPercentage}%</ZoomLabel>
271
+ <Tooltip tip={labels.zoomIn || 'Zoom in'} placement="top">
272
+ <ControlButton
273
+ variant="text"
274
+ size="small"
275
+ icon={<AddIcon />}
276
+ onClick={zoomIn}
277
+ disabled={scale >= maxScale}
278
+ />
279
+ </Tooltip>
280
+ <Divider />
281
+ <Tooltip tip={labels.fitToView || 'Fit to view'} placement="top">
282
+ <ControlButton
283
+ variant="text"
284
+ size="small"
285
+ icon={<FitToViewIcon />}
286
+ onClick={resetView}
287
+ />
288
+ </Tooltip>
289
+ <Tooltip tip={labels.close || 'Close'} placement="top">
290
+ <ControlButton variant="text" size="small" icon={<CloseIcon />} onClick={onClose} />
291
+ </Tooltip>
292
+ </ControlGroup>
293
+ </Controls>
294
+ </Viewport>
295
+ </Overlay>
296
+ );
297
+ }
298
+
299
+ const scaleIn = keyframes`
300
+ from {
301
+ transform: scale(0.9);
302
+ }
303
+ to {
304
+ transform: scale(1);
305
+ }
306
+ `;
307
+
308
+ const slideUp = keyframes`
309
+ from {
310
+ opacity: 0;
311
+ transform: translateX(-50%) translateY(10px);
312
+ }
313
+ to {
314
+ opacity: 1;
315
+ transform: translateX(-50%) translateY(0);
316
+ }
317
+ `;
318
+
319
+ const Overlay = styled.div`
320
+ position: fixed;
321
+ inset: 0;
322
+ background-color: var(--svg-viewer-overlay-bg-color);
323
+ backdrop-filter: blur(var(--spacing-unit));
324
+ z-index: var(--z-index-overlay, 1000);
325
+ display: flex;
326
+ align-items: center;
327
+ justify-content: center;
328
+ padding: var(--spacing-xxl);
329
+
330
+ &:focus {
331
+ outline: none;
332
+ }
333
+
334
+ @media (max-width: 768px) {
335
+ padding: var(--spacing-md);
336
+ }
337
+ `;
338
+
339
+ const Viewport = styled.div<{ $isDragging: boolean }>`
340
+ position: relative;
341
+ width: 100%;
342
+ height: 100%;
343
+ background-color: var(--svg-viewer-bg-color);
344
+ border-radius: var(--svg-viewer-border-radius);
345
+ overflow: hidden;
346
+ cursor: ${({ $isDragging }) => ($isDragging ? 'grabbing' : 'grab')};
347
+ touch-action: none;
348
+ box-shadow: var(--svg-viewer-box-shadow);
349
+ animation: ${scaleIn} 0.25s ease-in-out forwards;
350
+ `;
351
+
352
+ const Content = styled.div<{ $isAnimating: boolean }>`
353
+ position: absolute;
354
+ top: 50%;
355
+ left: 50%;
356
+ transform-origin: center center;
357
+ user-select: none;
358
+ pointer-events: none;
359
+ transition: ${({ $isAnimating }) => ($isAnimating ? 'transform 0.25s ease-in-out' : 'none')};
360
+
361
+ svg {
362
+ display: block;
363
+ max-width: none !important;
364
+ }
365
+ `;
366
+
367
+ const Controls = styled.div`
368
+ position: absolute;
369
+ bottom: var(--spacing-sm);
370
+ left: 50%;
371
+ transform: translateX(-50%);
372
+ z-index: 10;
373
+ animation: ${slideUp} 0.3s ease-out 0.1s backwards;
374
+ `;
375
+
376
+ const ControlGroup = styled.div`
377
+ display: flex;
378
+ align-items: center;
379
+ gap: 2px;
380
+ padding: var(--spacing-xxs);
381
+ background: var(--bg-color-raised);
382
+ border: 1px solid var(--border-color-primary);
383
+ border-radius: var(--border-radius-lg);
384
+ box-shadow: var(--bg-raised-shadow);
385
+ `;
386
+
387
+ const ControlButton = styled(Button)`
388
+ --button-icon-size: 16px;
389
+ `;
390
+
391
+ const ZoomLabel = styled.span`
392
+ min-width: 40px;
393
+ font-size: var(--font-size-sm);
394
+ font-weight: var(--font-weight-semibold);
395
+ color: var(--text-color-secondary);
396
+ text-align: center;
397
+ font-variant-numeric: tabular-nums;
398
+ `;
399
+
400
+ const Divider = styled.div`
401
+ width: 1px;
402
+ height: var(--spacing-base);
403
+ background: var(--border-color-primary);
404
+ margin: 0 var(--spacing-xxs);
405
+ `;
@@ -0,0 +1,5 @@
1
+ import { css } from 'styled-components';
2
+
3
+ export const svgViewerDarkMode = css`
4
+ --svg-viewer-bg-color: var(--color-warm-grey-9);
5
+ `;
@@ -0,0 +1,14 @@
1
+ import { css } from 'styled-components';
2
+
3
+ export const svgViewer = css`
4
+ /**
5
+ * @tokens SVG Viewer
6
+ */
7
+
8
+ --svg-viewer-overlay-bg-color: var(--bg-color-modal-overlay); // @presenter Color
9
+ --svg-viewer-bg-color: var(--bg-color); // @presenter Color
10
+ --svg-viewer-border-radius: var(--border-radius-xl); // @presenter BorderRadius
11
+ --svg-viewer-box-shadow: var(--bg-raised-shadow); // @presenter BoxShadow
12
+
13
+ // @tokens End
14
+ `;
@@ -243,7 +243,8 @@ const TagWrapper = styled.div.attrs(({ className, color, size, variant }: TagPro
243
243
  `text-transform: ${textTransform ? `${textTransform}` : 'var(--tag-text-transform)'};`}
244
244
 
245
245
  color: var(--tag-color);
246
- background-color: var(--tag-bg-color);
246
+ background-color: ${({ variant }) =>
247
+ variant === 'filled' ? 'var(--tag-bg-color)' : 'transparent'};
247
248
  ${({ borderless }) =>
248
249
  borderless
249
250
  ? ''
@@ -182,6 +182,12 @@ export const tagDarkMode = css`
182
182
  --tag-bg-color-hover: #3A465F; // @presenter Color
183
183
  }
184
184
 
185
+ .tag-query {
186
+ --tag-color: #68cc97; // @presenter Color
187
+ --tag-bg-color: #1F3D2D; // @presenter Color
188
+ --tag-bg-color-hover: #34654B; // @presenter Color
189
+ }
190
+
185
191
  .tag-put {
186
192
  --tag-color: #e0a663; // @presenter Color
187
193
  --tag-bg-color: #3D2D1B; // @presenter Color
@@ -295,6 +295,12 @@ export const tag = css`
295
295
  --tag-bg-color-hover: #CEDDFD; // @presenter Color
296
296
  }
297
297
 
298
+ .tag-query {
299
+ --tag-color: #25b869; // @presenter Color
300
+ --tag-bg-color: #e5faef; // @presenter Color
301
+ --tag-bg-color-hover: #D4F7E5; // @presenter Color
302
+ }
303
+
298
304
  .tag-put {
299
305
  --tag-color: #f5901d; // @presenter Color
300
306
  --tag-bg-color: #fef1e2; // @presenter Color