@inkra/react 0.3.0 → 0.5.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 (245) hide show
  1. package/README.md +113 -3
  2. package/dist/Button.js +3 -2
  3. package/dist/PencilIcon.d.ts +10 -0
  4. package/dist/PencilIcon.js +83 -39
  5. package/dist/PencilLine.js +9 -1
  6. package/dist/PencilText.js +15 -9
  7. package/dist/accordion/Accordion.js +65 -5
  8. package/dist/alert-dialog/index.js +14 -4
  9. package/dist/audio-player/index.d.ts +2 -0
  10. package/dist/audio-player/index.js +131 -34
  11. package/dist/breadcrumb/index.js +4 -1
  12. package/dist/calendar/index.d.ts +4 -1
  13. package/dist/calendar/index.js +37 -12
  14. package/dist/calendar/locales.d.ts +7 -0
  15. package/dist/calendar/locales.js +2 -0
  16. package/dist/carousel/index.js +20 -10
  17. package/dist/choice-grid/index.d.ts +6 -1
  18. package/dist/choice-grid/index.js +63 -33
  19. package/dist/color-picker/GradientEditor.d.ts +40 -0
  20. package/dist/color-picker/GradientEditor.js +64 -0
  21. package/dist/color-picker/gradient.d.ts +25 -0
  22. package/dist/color-picker/gradient.js +60 -0
  23. package/dist/color-picker/index.d.ts +27 -7
  24. package/dist/color-picker/index.js +144 -40
  25. package/dist/color-swatch/index.d.ts +10 -0
  26. package/dist/color-swatch/index.js +9 -0
  27. package/dist/command/index.js +27 -4
  28. package/dist/content/index.d.ts +5 -0
  29. package/dist/content/index.js +15 -3
  30. package/dist/context-menu/index.js +1 -1
  31. package/dist/data-table/index.d.ts +7 -1
  32. package/dist/data-table/index.js +67 -16
  33. package/dist/data-table/model.d.ts +2 -0
  34. package/dist/data-table/model.js +7 -0
  35. package/dist/date-picker/index.d.ts +2 -0
  36. package/dist/date-picker/index.js +20 -21
  37. package/dist/day-agenda/index.d.ts +6 -1
  38. package/dist/day-agenda/index.js +49 -21
  39. package/dist/day-agenda/layout.js +65 -9
  40. package/dist/display/ProgressRing.js +7 -3
  41. package/dist/display/index.d.ts +4 -1
  42. package/dist/display/index.js +3 -2
  43. package/dist/dockable-panel/index.js +15 -13
  44. package/dist/drawer/index.js +11 -5
  45. package/dist/editable/index.d.ts +24 -2
  46. package/dist/editable/index.js +148 -40
  47. package/dist/empty/illustrations.d.ts +8 -0
  48. package/dist/empty/illustrations.js +78 -0
  49. package/dist/empty/index.d.ts +6 -1
  50. package/dist/empty/index.js +11 -3
  51. package/dist/flip/index.js +28 -8
  52. package/dist/form/Field.js +11 -10
  53. package/dist/form/Input.js +4 -1
  54. package/dist/form/Textarea.d.ts +3 -1
  55. package/dist/form/Textarea.js +9 -5
  56. package/dist/heatmap/index.js +4 -1
  57. package/dist/hotkey/index.d.ts +1 -0
  58. package/dist/hotkey/index.js +9 -4
  59. package/dist/hover-card/index.js +1 -1
  60. package/dist/i18n/catalog.d.ts +5 -0
  61. package/dist/i18n/catalog.js +289 -0
  62. package/dist/i18n/index.d.ts +23 -0
  63. package/dist/i18n/index.js +50 -0
  64. package/dist/image-cropper/index.js +21 -17
  65. package/dist/image-viewer/VideoResource.d.ts +21 -0
  66. package/dist/image-viewer/VideoResource.js +196 -0
  67. package/dist/image-viewer/index.d.ts +14 -0
  68. package/dist/image-viewer/index.js +141 -86
  69. package/dist/image-viewer/lucideGeometry.d.ts +3 -0
  70. package/dist/image-viewer/lucideGeometry.js +19 -0
  71. package/dist/index.d.ts +4 -0
  72. package/dist/index.js +4 -0
  73. package/dist/input-number/index.js +13 -5
  74. package/dist/internal/cache.d.ts +4 -0
  75. package/dist/internal/cache.js +18 -1
  76. package/dist/internal/iconGeometry.d.ts +2 -0
  77. package/dist/internal/iconGeometry.js +32 -0
  78. package/dist/internal/renderTokens.d.ts +4 -0
  79. package/dist/internal/renderTokens.js +4 -0
  80. package/dist/internal/stroke.d.ts +7 -0
  81. package/dist/internal/stroke.js +63 -7
  82. package/dist/internal/surfaceBudget.d.ts +20 -0
  83. package/dist/internal/surfaceBudget.js +69 -0
  84. package/dist/internal/surfaceRenderer.js +87 -28
  85. package/dist/internal/surfaceVisibility.d.ts +3 -0
  86. package/dist/internal/surfaceVisibility.js +60 -0
  87. package/dist/internal/textRenderer.d.ts +3 -0
  88. package/dist/internal/textRenderer.js +13 -5
  89. package/dist/internal/usePencilSurface.js +17 -1
  90. package/dist/internal/wash.js +4 -3
  91. package/dist/journey-map/index.d.ts +1 -1
  92. package/dist/journey-map/index.js +6 -3
  93. package/dist/journey-map/terrain.js +10 -5
  94. package/dist/keypad/index.js +4 -1
  95. package/dist/layout/index.js +1 -1
  96. package/dist/log/index.js +42 -33
  97. package/dist/markdown/CodeBlock.d.ts +19 -0
  98. package/dist/markdown/CodeBlock.js +86 -0
  99. package/dist/markdown/Deletion.d.ts +2 -0
  100. package/dist/markdown/Deletion.js +69 -0
  101. package/dist/markdown/icons.d.ts +14 -0
  102. package/dist/markdown/icons.js +16 -0
  103. package/dist/markdown/index.d.ts +17 -0
  104. package/dist/markdown/index.js +177 -0
  105. package/dist/node-graph/index.d.ts +3 -1
  106. package/dist/node-graph/index.js +63 -28
  107. package/dist/number-ticker/index.js +4 -1
  108. package/dist/number-ticker/precision.js +9 -2
  109. package/dist/overflow-list/index.js +9 -6
  110. package/dist/overlay/Overlay.js +8 -3
  111. package/dist/overlay/Popover.js +3 -3
  112. package/dist/overlay/themeBridge.js +14 -2
  113. package/dist/pagination/index.d.ts +10 -1
  114. package/dist/pagination/index.js +13 -9
  115. package/dist/pan-zoom/index.d.ts +2 -0
  116. package/dist/pan-zoom/index.js +12 -8
  117. package/dist/pencil.d.ts +1 -0
  118. package/dist/pencil.js +4 -4
  119. package/dist/radar/index.d.ts +2 -0
  120. package/dist/radar/index.js +44 -21
  121. package/dist/range-slider/index.js +4 -3
  122. package/dist/rating/index.d.ts +4 -0
  123. package/dist/rating/index.js +131 -24
  124. package/dist/resizable/index.d.ts +1 -1
  125. package/dist/resizable/index.js +5 -2
  126. package/dist/scroll-area/index.d.ts +1 -1
  127. package/dist/scroll-area/index.js +5 -2
  128. package/dist/select/MultipleSelect.js +5 -4
  129. package/dist/select/SearchableSelect.js +64 -7
  130. package/dist/select/Select.d.ts +1 -0
  131. package/dist/select/Select.js +6 -2
  132. package/dist/select/StepperSelect.d.ts +29 -0
  133. package/dist/select/StepperSelect.js +122 -0
  134. package/dist/select/index.d.ts +2 -0
  135. package/dist/select/index.js +1 -0
  136. package/dist/select/stepperModel.d.ts +7 -0
  137. package/dist/select/stepperModel.js +29 -0
  138. package/dist/selection/Checkbox.js +4 -3
  139. package/dist/selection/RadioGroup.js +29 -13
  140. package/dist/selection/Switch.js +4 -3
  141. package/dist/selection/useNativeChecked.d.ts +2 -1
  142. package/dist/selection/useNativeChecked.js +26 -8
  143. package/dist/slider/index.js +50 -6
  144. package/dist/sortable/index.js +17 -11
  145. package/dist/sparkline/index.js +21 -6
  146. package/dist/sparkline/model.js +11 -4
  147. package/dist/steps/index.js +4 -1
  148. package/dist/styles/accordion.css +228 -0
  149. package/dist/styles/alert-dialog.css +139 -0
  150. package/dist/styles/audio-player.css +22 -0
  151. package/dist/styles/base.css +638 -0
  152. package/dist/styles/breadcrumb.css +7 -0
  153. package/dist/styles/calendar.css +62 -0
  154. package/dist/styles/carousel.css +173 -0
  155. package/dist/styles/choice-grid.css +226 -0
  156. package/dist/styles/color-picker.css +281 -0
  157. package/dist/styles/color-swatch.css +47 -0
  158. package/dist/styles/command.css +52 -0
  159. package/dist/styles/content.css +79 -0
  160. package/dist/styles/context-menu.css +35 -0
  161. package/dist/styles/data-table.css +31 -0
  162. package/dist/styles/date-picker.css +107 -0
  163. package/dist/styles/day-agenda.css +30 -0
  164. package/dist/styles/display.css +304 -0
  165. package/dist/styles/dockable-panel.css +110 -0
  166. package/dist/styles/drawer.css +133 -0
  167. package/dist/styles/dropdown-menu.css +68 -0
  168. package/dist/styles/editable.css +145 -0
  169. package/dist/styles/empty.css +108 -0
  170. package/dist/styles/flip.css +101 -0
  171. package/dist/styles/form.css +634 -0
  172. package/dist/styles/heatmap.css +256 -0
  173. package/dist/styles/hotkey.css +189 -0
  174. package/dist/styles/hover-card.css +27 -0
  175. package/dist/styles/image-cropper.css +205 -0
  176. package/dist/styles/image-viewer.css +422 -0
  177. package/dist/styles/input-number.css +21 -0
  178. package/dist/styles/journey-map.css +19 -0
  179. package/dist/styles/keypad.css +92 -0
  180. package/dist/styles/layout.css +72 -0
  181. package/dist/styles/leaflet.css +661 -0
  182. package/dist/styles/log.css +203 -0
  183. package/dist/styles/markdown.css +379 -0
  184. package/dist/styles/node-graph.css +142 -0
  185. package/dist/styles/number-ticker.css +83 -0
  186. package/dist/styles/overflow-list.css +51 -0
  187. package/dist/styles/overlay.css +284 -0
  188. package/dist/styles/pagination.css +21 -0
  189. package/dist/styles/pan-zoom.css +136 -0
  190. package/dist/styles/radar.css +1 -0
  191. package/dist/styles/rating.css +121 -0
  192. package/dist/styles/resizable.css +108 -0
  193. package/dist/styles/scroll-area.css +122 -0
  194. package/dist/styles/select.css +554 -0
  195. package/dist/styles/selection.css +264 -0
  196. package/dist/styles/slider.css +145 -0
  197. package/dist/styles/slot.css +109 -0
  198. package/dist/styles/sortable.css +159 -0
  199. package/dist/styles/sparkline.css +24 -0
  200. package/dist/styles/steps.css +27 -0
  201. package/dist/styles/tabs.css +79 -0
  202. package/dist/styles/tag-input.css +213 -0
  203. package/dist/styles/time-picker.css +66 -0
  204. package/dist/styles/timeline.css +24 -0
  205. package/dist/styles/toast.css +160 -0
  206. package/dist/styles/toggle-group.css +107 -0
  207. package/dist/styles/tooltip.css +78 -0
  208. package/dist/styles/transfer.css +148 -0
  209. package/dist/styles/tree-select.css +131 -0
  210. package/dist/styles/tree.css +59 -0
  211. package/dist/styles/typewriter.css +62 -0
  212. package/dist/styles/upload.css +60 -0
  213. package/dist/styles/virtual-list.css +112 -0
  214. package/dist/styles.css +2091 -421
  215. package/dist/tabs/index.d.ts +9 -0
  216. package/dist/tabs/index.js +69 -16
  217. package/dist/tabs/useOverflow.d.ts +20 -0
  218. package/dist/tabs/useOverflow.js +145 -0
  219. package/dist/tag-input/index.d.ts +7 -2
  220. package/dist/tag-input/index.js +81 -42
  221. package/dist/theme/index.d.ts +11 -0
  222. package/dist/theme/index.js +23 -0
  223. package/dist/time-picker/index.js +45 -27
  224. package/dist/timeline/index.js +4 -1
  225. package/dist/toast/index.d.ts +19 -11
  226. package/dist/toast/index.js +51 -8
  227. package/dist/toast/manager.d.ts +42 -0
  228. package/dist/toast/manager.js +85 -0
  229. package/dist/toast/useToastNavigation.d.ts +3 -0
  230. package/dist/toast/useToastNavigation.js +75 -0
  231. package/dist/tokens.css +4 -0
  232. package/dist/tooltip/Tooltip.js +3 -3
  233. package/dist/transfer/index.d.ts +5 -0
  234. package/dist/transfer/index.js +120 -53
  235. package/dist/transfer/model.d.ts +4 -0
  236. package/dist/transfer/model.js +37 -11
  237. package/dist/tree/index.d.ts +5 -1
  238. package/dist/tree/index.js +11 -6
  239. package/dist/tree-select/index.js +70 -31
  240. package/dist/tree-select/model.d.ts +11 -5
  241. package/dist/tree-select/model.js +111 -83
  242. package/dist/typewriter/index.js +10 -10
  243. package/dist/upload/index.js +50 -33
  244. package/dist/virtual-list/index.js +11 -6
  245. package/package.json +423 -3
@@ -9,6 +9,11 @@ export const pencilLettering = Object.freeze({
9
9
  });
10
10
  export const pencilHandwriting = '"Trip Map Hand","HanziPen SC","Hannotate SC","Kaiti SC","STKaiti",cursive';
11
11
  const TEXT_PIXEL_LIMIT = 524288;
12
+ export function boundedTextResolution(width, height, dpr) {
13
+ if (![width, height, dpr].every(value => Number.isFinite(value) && value > 0))
14
+ return 0;
15
+ return Math.floor(Math.min(Math.max(2, Math.min(4, Math.ceil(dpr * 2))), Math.sqrt(TEXT_PIXEL_LIMIT / (width * height)), 8192 / width, 8192 / height));
16
+ }
12
17
  const textCache = new BoundedCache(160, 16 * 1024 * 1024);
13
18
  const glyphCache = new BoundedCache(512, 8 * 1024 * 1024);
14
19
  const clamp = (value) => Math.max(0, Math.min(1, value));
@@ -75,14 +80,15 @@ export function renderPencilText(text, options) {
75
80
  if (text.length > 256 || !Number.isFinite(size) || size <= 0 || size > 128) {
76
81
  throw new RangeError('Pencil text exceeds the bounded renderer input');
77
82
  }
78
- const resolution = Math.max(2, Math.min(4, Math.ceil(dpr * 2)));
79
- const font = `400 ${size}px ${family}`;
83
+ let resolution = Math.max(2, Math.min(4, Math.ceil(dpr * 2)));
84
+ const font = `${options.style ?? 'normal'} ${options.weight ?? '400'} ${size}px ${family}`;
80
85
  const fontReady = document.fonts?.check(font, text) ?? false;
81
86
  const key = cache ? JSON.stringify([
82
87
  PENCIL_RENDERER_VERSION,
88
+ 'adaptive-text-raster-v1',
83
89
  text,
84
90
  size,
85
- family,
91
+ font,
86
92
  color,
87
93
  seed,
88
94
  dpr,
@@ -113,7 +119,9 @@ export function renderPencilText(text, options) {
113
119
  const descent = Math.ceil(size * 0.35);
114
120
  const width = advance + padding * 2;
115
121
  const height = ascent + descent + padding * 2;
116
- if (width * height * resolution ** 2 > TEXT_PIXEL_LIMIT) {
122
+ // Wide lines on Retina displays can retain pencil ink at a lower sampling rate.
123
+ resolution = boundedTextResolution(width, height, dpr);
124
+ if (resolution < 1) {
117
125
  throw new RangeError('Pencil text exceeds the bounded pixel budget');
118
126
  }
119
127
  mask.width = width * resolution;
@@ -131,7 +139,7 @@ export function renderPencilText(text, options) {
131
139
  const characterSeed = (options.glyphSeed ^ Math.imul(character.charCodeAt(0), 16777619)
132
140
  ^ Math.imul(index % 3, 374761393)) >>> 0;
133
141
  const glyphKey = JSON.stringify([
134
- PENCIL_RENDERER_VERSION, character, size, family, color, characterSeed,
142
+ PENCIL_RENDERER_VERSION, character, size, font, color, characterSeed,
135
143
  resolution, settings, document.fonts?.check(font, character),
136
144
  ]);
137
145
  let glyph = glyphCache.get(glyphKey);
@@ -3,9 +3,13 @@ import { PENCIL_RENDERER_VERSION, effectiveDpr } from './cache.js';
3
3
  import { observeCanvasRecovery } from './recovery.js';
4
4
  import { seedToNumber } from './random.js';
5
5
  import { drawPencilSurface } from './surfaceRenderer.js';
6
+ import { releaseSurfaceBitmaps } from './surfaceBudget.js';
7
+ import { observeSurfaceViewport, surfaceIsNearViewport } from './surfaceVisibility.js';
6
8
  import { maxStrokeRoughness, pencilRenderDefaults, readRenderToken } from './renderTokens.js';
7
9
  function token(style, name, fallback) {
8
- return style.getPropertyValue(name).trim() || fallback;
10
+ const value = style.getPropertyValue(name).trim() || fallback;
11
+ // Offscreen material canvases have no inherited DOM foreground.
12
+ return value.toLowerCase() === 'currentcolor' ? style.color : value;
9
13
  }
10
14
  export function usePencilSurface(rootRef, canvasRef, borderRef, variant, seed, clipContent, radius) {
11
15
  useBrowserLayoutEffect(() => {
@@ -16,10 +20,18 @@ export function usePencilSurface(rootRef, canvasRef, borderRef, variant, seed, c
16
20
  let frame = 0;
17
21
  let lastPaint = '';
18
22
  let disposed = false;
23
+ let deferOffscreen = false;
19
24
  const paint = () => {
20
25
  frame = 0;
21
26
  if (disposed)
22
27
  return;
28
+ if (deferOffscreen && !surfaceIsNearViewport(root)) {
29
+ // A new canvas otherwise retains its implicit 300x150 blank bitmap.
30
+ // Keep valid painted pixels when merely scrolling out of view.
31
+ if (!lastPaint)
32
+ releaseSurfaceBitmaps(canvas, borderRef.current);
33
+ return;
34
+ }
23
35
  const width = root.clientWidth;
24
36
  const height = root.clientHeight;
25
37
  if (width < 6 || height < 6)
@@ -80,6 +92,7 @@ export function usePencilSurface(rootRef, canvasRef, borderRef, variant, seed, c
80
92
  lastPaint = paintKey;
81
93
  }
82
94
  catch {
95
+ releaseSurfaceBitmaps(canvas, borderRef.current);
83
96
  root.dataset.pencilFallback = 'true';
84
97
  root.style.removeProperty('--inkra-pencil-clip');
85
98
  lastPaint = '';
@@ -89,6 +102,8 @@ export function usePencilSurface(rootRef, canvasRef, borderRef, variant, seed, c
89
102
  if (!frame)
90
103
  frame = requestAnimationFrame(paint);
91
104
  };
105
+ const stopViewport = observeSurfaceViewport(root, schedule);
106
+ deferOffscreen = stopViewport !== undefined;
92
107
  const resize = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(schedule);
93
108
  resize?.observe(root);
94
109
  window.addEventListener('resize', schedule, { passive: true });
@@ -106,6 +121,7 @@ export function usePencilSurface(rootRef, canvasRef, borderRef, variant, seed, c
106
121
  return () => {
107
122
  disposed = true;
108
123
  stopRecovery();
124
+ stopViewport?.();
109
125
  resize?.disconnect();
110
126
  inheritedStyle.disconnect();
111
127
  colorScheme.removeEventListener('change', schedule);
@@ -1,4 +1,4 @@
1
- import { BoundedCache, PENCIL_RENDERER_VERSION, canvasByteCost, canvasIsUsable } from './cache.js';
1
+ import { BoundedCache, PENCIL_RENDERER_VERSION, boundedCanvasSize, canvasByteCost, canvasIsUsable } from './cache.js';
2
2
  import { pencilStroke } from './stroke.js';
3
3
  const washCache = new BoundedCache(64, 32 * 1024 * 1024);
4
4
  function randomAt(index, seed) {
@@ -9,9 +9,10 @@ function randomAt(index, seed) {
9
9
  function makeWash(width, height, color, seed, dpr, settings) {
10
10
  const { strength, spacing, roughness, inset, radius, edgeReach, angle } = settings;
11
11
  const bleed = edgeReach ? Math.ceil(edgeReach + 4) : 0;
12
+ const pixels = boundedCanvasSize(width + bleed * 2, height + bleed * 2, dpr);
12
13
  const canvas = document.createElement('canvas');
13
- canvas.width = Math.ceil((width + bleed * 2) * dpr);
14
- canvas.height = Math.ceil((height + bleed * 2) * dpr);
14
+ canvas.width = pixels.width;
15
+ canvas.height = pixels.height;
15
16
  const context = canvas.getContext('2d');
16
17
  if (!context)
17
18
  return canvas;
@@ -36,4 +36,4 @@ export interface JourneyMapProps extends HTMLAttributes<HTMLDivElement> {
36
36
  attribution?: string;
37
37
  };
38
38
  }
39
- export declare function JourneyMap({ places, routes, geography, landCover, water, selectedPlace, onPlaceSelect, attribution, attributionDisplay, height, interactive, scrollWheelZoom, showControls, showPlaceList, focusSelectedPlace, labels, className, ...props }: JourneyMapProps): import("react").JSX.Element;
39
+ export declare function JourneyMap({ places, routes, geography, landCover, water, selectedPlace, onPlaceSelect, attribution, attributionDisplay, height, interactive, scrollWheelZoom, showControls, showPlaceList, focusSelectedPlace, labels: labelOverrides, className, ...props }: JourneyMapProps): import("react").JSX.Element;
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useInkraLocale } from '../i18n/index.js';
2
3
  import { useEffect, useRef, useState } from 'react';
3
4
  import { createPortal } from 'react-dom';
4
5
  import { Button } from '../Button.js';
@@ -9,10 +10,12 @@ import { TravelIcon } from '../internal/travelIcons.js';
9
10
  import { paintTerrain, paintStop, paintSea } from './terrain.js';
10
11
  import { MapScale } from './MapScale.js';
11
12
  import { seedToNumber } from '../internal/random.js';
12
- const defaults = { zoomIn: 'Zoom in', zoomOut: 'Zoom out', reset: 'Reset view', unavailable: 'Map unavailable. Choose a place below.' };
13
+ const defaults = (t) => ({ zoomIn: t('common.zoomIn'), zoomOut: t('common.zoomOut'), reset: t('common.resetView'), unavailable: t('map.unavailable') });
13
14
  const noRoutes = [];
14
15
  const valid = (place) => Number.isFinite(place.latitude) && Number.isFinite(place.longitude) && Math.abs(place.latitude) <= 85 && Math.abs(place.longitude) <= 180;
15
- export function JourneyMap({ places, routes = noRoutes, geography, landCover, water, selectedPlace, onPlaceSelect, attribution, attributionDisplay = 'inline', height = 480, interactive = true, scrollWheelZoom = false, showControls = true, showPlaceList = false, focusSelectedPlace = false, labels = defaults, className, ...props }) {
16
+ export function JourneyMap({ places, routes = noRoutes, geography, landCover, water, selectedPlace, onPlaceSelect, attribution, attributionDisplay = 'inline', height = 480, interactive = true, scrollWheelZoom = false, showControls = true, showPlaceList = false, focusSelectedPlace = false, labels: labelOverrides, className, ...props }) {
17
+ const { t } = useInkraLocale();
18
+ const labels = { ...defaults(t), ...labelOverrides };
16
19
  const root = useRef(null);
17
20
  const host = useRef(null);
18
21
  const mapRef = useRef(null);
@@ -193,5 +196,5 @@ export function JourneyMap({ places, routes = noRoutes, geography, landCover, wa
193
196
  else
194
197
  map.scrollWheelZoom.disable();
195
198
  }, [interactive, scrollWheelZoom, ready]);
196
- return _jsxs("div", { ...props, ref: root, className: ['inkra-journey-map', className].filter(Boolean).join(' '), children: [placeLabels.map(({ place, node }) => createPortal(_jsx(PencilText, { children: place.label }), node, place.id)), _jsxs("div", { className: "inkra-journey-map__viewport", style: { height: Math.max(240, Math.min(800, height)) }, children: [attributionDisplay === 'popover' && _jsx("div", { className: "inkra-journey-map__credits", children: _jsxs(Popover.Root, { children: [_jsx(Popover.Trigger, { asChild: true, children: _jsx(Button, { size: "icon", variant: "ghost", "aria-label": labels.attribution ?? 'Map credits', title: labels.attribution ?? 'Map credits', children: _jsx(TravelIcon, { name: "info" }) }) }), _jsx(Popover.Portal, { children: _jsxs(Popover.Content, { align: "end", sideOffset: 8, collisionPadding: 20, className: "inkra-journey-map__credits-content", children: [_jsx(Popover.Title, { children: labels.attribution ?? 'Map credits' }), _jsx(Popover.Description, { children: attribution })] }) })] }) }), _jsx("div", { ref: host, className: "inkra-journey-map__host", "aria-label": props['aria-label'] ?? 'Map' }), showControls && _jsxs("div", { className: "inkra-journey-map__tools", children: [_jsx(Button, { size: "icon", variant: "ghost", disabled: !ready, "aria-label": labels.zoomIn, title: labels.zoomIn, onClick: () => { mapRef.current?.stop(); mapRef.current?.zoomIn(); }, children: _jsx(TravelIcon, { name: "plus" }) }), _jsx(Button, { size: "icon", variant: "ghost", disabled: !ready, "aria-label": labels.zoomOut, title: labels.zoomOut, onClick: () => { mapRef.current?.stop(); mapRef.current?.zoomOut(); }, children: _jsx(TravelIcon, { name: "minus" }) }), _jsx(Button, { size: "icon", variant: "ghost", disabled: !ready, "aria-label": labels.reset, title: labels.reset, onClick: () => { mapRef.current?.stop(); fitRef.current(); }, children: _jsx(TravelIcon, { name: "reset" }) })] }), ready && _jsx(MapScale, { map: mapRef.current }), failed && _jsx("p", { role: "status", children: labels.unavailable })] }), attributionDisplay === 'inline' && _jsx("p", { className: "inkra-journey-map__attribution", children: attribution }), (showPlaceList || !ready || failed) && _jsx("ol", { className: "inkra-journey-map__places", children: places.filter(valid).map((place, index) => _jsx("li", { children: _jsxs("button", { type: "button", "aria-pressed": selectedPlace === place.id, onClick: () => { mapRef.current?.panTo([place.latitude, place.longitude], { animate: false }); onPlaceSelect?.(place); }, children: [_jsx("span", { "aria-hidden": "true", children: index + 1 }), _jsx(PencilText, { children: place.label })] }) }, place.id)) })] });
199
+ return _jsxs("div", { ...props, ref: root, className: ['inkra-journey-map', className].filter(Boolean).join(' '), children: [placeLabels.map(({ place, node }) => createPortal(_jsx(PencilText, { children: place.label }), node, place.id)), _jsxs("div", { className: "inkra-journey-map__viewport", style: { height: Math.max(240, Math.min(800, height)) }, children: [attributionDisplay === 'popover' && _jsx("div", { className: "inkra-journey-map__credits", children: _jsxs(Popover.Root, { children: [_jsx(Popover.Trigger, { asChild: true, children: _jsx(Button, { size: "icon", variant: "ghost", "aria-label": labels.attribution ?? t('map.credits'), title: labels.attribution ?? t('map.credits'), children: _jsx(TravelIcon, { name: "info" }) }) }), _jsx(Popover.Portal, { children: _jsxs(Popover.Content, { align: "end", sideOffset: 8, collisionPadding: 20, className: "inkra-journey-map__credits-content", children: [_jsx(Popover.Title, { children: labels.attribution ?? t('map.credits') }), _jsx(Popover.Description, { children: attribution })] }) })] }) }), _jsx("div", { ref: host, className: "inkra-journey-map__host", "aria-label": props['aria-label'] ?? t('map.label') }), showControls && _jsxs("div", { className: "inkra-journey-map__tools", children: [_jsx(Button, { size: "icon", variant: "ghost", disabled: !ready, "aria-label": labels.zoomIn, title: labels.zoomIn, onClick: () => { mapRef.current?.stop(); mapRef.current?.zoomIn(); }, children: _jsx(TravelIcon, { name: "plus" }) }), _jsx(Button, { size: "icon", variant: "ghost", disabled: !ready, "aria-label": labels.zoomOut, title: labels.zoomOut, onClick: () => { mapRef.current?.stop(); mapRef.current?.zoomOut(); }, children: _jsx(TravelIcon, { name: "minus" }) }), _jsx(Button, { size: "icon", variant: "ghost", disabled: !ready, "aria-label": labels.reset, title: labels.reset, onClick: () => { mapRef.current?.stop(); fitRef.current(); }, children: _jsx(TravelIcon, { name: "reset" }) })] }), ready && _jsx(MapScale, { map: mapRef.current }), failed && _jsx("p", { role: "status", children: labels.unavailable })] }), attributionDisplay === 'inline' && _jsx("p", { className: "inkra-journey-map__attribution", children: attribution }), (showPlaceList || !ready || failed) && _jsx("ol", { className: "inkra-journey-map__places", children: places.filter(valid).map((place, index) => _jsx("li", { children: _jsxs("button", { type: "button", "aria-pressed": selectedPlace === place.id, onClick: () => { mapRef.current?.panTo([place.latitude, place.longitude], { animate: false }); onPlaceSelect?.(place); }, children: [_jsx("span", { "aria-hidden": "true", children: index + 1 }), _jsx(PencilText, { children: place.label })] }) }, place.id)) })] });
197
200
  }
@@ -55,11 +55,16 @@ export function paintTerrain(ctx, data, project, width, height, dark, defaultKin
55
55
  const [fill, ink] = palette[kind] ?? palette.land;
56
56
  for (const [polygonIndex, polygon] of polygons.entries()) {
57
57
  const rings = polygon.map(ring => ring.map(project));
58
- const points = rings.flat();
59
- if (!points.length)
60
- continue;
61
- const left = Math.max(0, Math.min(...points.map(p => p[0]))), right = Math.min(width, Math.max(...points.map(p => p[0])));
62
- const top = Math.max(0, Math.min(...points.map(p => p[1]))), bottom = Math.min(height, Math.max(...points.map(p => p[1])));
58
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
59
+ for (const ring of rings)
60
+ for (const [x, y] of ring) {
61
+ minX = Math.min(minX, x);
62
+ maxX = Math.max(maxX, x);
63
+ minY = Math.min(minY, y);
64
+ maxY = Math.max(maxY, y);
65
+ }
66
+ const left = Math.max(0, minX), right = Math.min(width, maxX);
67
+ const top = Math.max(0, minY), bottom = Math.min(height, maxY);
63
68
  if (left >= right || top >= bottom)
64
69
  continue;
65
70
  ctx.save();
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useInkraLocale } from '../i18n/index.js';
2
3
  import { forwardRef, useId, useRef, useState } from 'react';
3
4
  import { PencilSurface } from '../PencilSurface.js';
4
5
  import { PencilText } from '../PencilText.js';
@@ -20,7 +21,9 @@ function keyVariant(key) {
20
21
  return 'secondary';
21
22
  return 'default';
22
23
  }
23
- export const Keypad = forwardRef(function Keypad({ 'aria-label': ariaLabel, className, defaultValue = '', disabled = false, emptyContent = null, label = 'Keypad', layout, maxLength, onConfirm, onKeyPress, onValueChange, value, ...props }, ref) {
24
+ export const Keypad = forwardRef(function Keypad(inkraProps, ref) {
25
+ const { t } = useInkraLocale();
26
+ const { 'aria-label': ariaLabel, className, defaultValue = '', disabled = false, emptyContent = null, label = t('keypad.label'), layout, maxLength, onConfirm, onKeyPress, onValueChange, value, ...props } = inkraProps;
24
27
  const id = useId();
25
28
  const controlled = value !== undefined;
26
29
  const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
@@ -56,7 +56,7 @@ const LayoutSidebar = forwardRef(function LayoutSidebar({ as: Element = 'aside',
56
56
  const LayoutContent = forwardRef(function LayoutContent({ as: Element = 'div', padding = 5, className, style, ...props }, ref) {
57
57
  return _jsx(Box, { as: Element, ...props, ref: ref, className: classes('inkra-layout__content', className), style: { padding: space(padding), ...style } });
58
58
  });
59
- export const Layout = Object.freeze({
59
+ export const Layout = /* @__PURE__ */ Object.freeze({
60
60
  Root: LayoutRoot, Header: LayoutHeader, Body: LayoutBody,
61
61
  Sidebar: LayoutSidebar, Content: LayoutContent, Footer: LayoutFooter,
62
62
  });
package/dist/log/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useInkraLocale } from '../i18n/index.js';
2
3
  import { forwardRef, useCallback, useMemo, useRef, useState, } from 'react';
3
4
  import { Button } from '../Button.js';
4
5
  import { PencilIcon } from '../PencilIcon.js';
@@ -7,16 +8,16 @@ import { PencilSurface } from '../PencilSurface.js';
7
8
  import { PencilText } from '../PencilText.js';
8
9
  import { useBrowserLayoutEffect } from '../internal/useBrowserLayoutEffect.js';
9
10
  import { isNearLogBottom, shouldFollowLogUpdate } from './model.js';
10
- const defaultLabels = {
11
- filters: 'Filter log levels',
12
- allLevels: 'All',
13
- pause: 'Pause automatic follow',
14
- resume: 'Resume automatic follow',
15
- latest: 'Go to latest record',
16
- empty: 'No records yet',
17
- noMatches: 'No records match the selected levels',
18
- list: 'Log records',
19
- };
11
+ const defaultLabels = (t) => ({
12
+ filters: t('log.filters'),
13
+ allLevels: t('common.all'),
14
+ pause: t('log.pause'),
15
+ resume: t('log.resume'),
16
+ latest: t('log.latest'),
17
+ empty: t('log.empty'),
18
+ noMatches: t('log.noMatches'),
19
+ list: t('log.list'),
20
+ });
20
21
  const defaultTones = {
21
22
  debug: 'neutral',
22
23
  info: 'info',
@@ -42,7 +43,8 @@ function normalizedThreshold(value) {
42
43
  return Number.isFinite(value) ? Math.max(0, value) : 32;
43
44
  }
44
45
  export const Log = forwardRef(function Log({ 'aria-label': ariaLabel, className, defaultFilteredLevels, defaultPaused = false, empty, filteredLevels, items, labels: labelOverrides, levels, nearBottomThreshold, noMatches, onFilteredLevelsChange, onPausedChange, paused, showFollowControl = true, showLevelFilter = true, viewportProps, ...props }, ref) {
45
- const labels = { ...defaultLabels, ...labelOverrides };
46
+ const { t } = useInkraLocale();
47
+ const labels = { ...defaultLabels(t), ...labelOverrides };
46
48
  const resolvedLevels = useMemo(() => {
47
49
  if (levels)
48
50
  return levels;
@@ -56,9 +58,11 @@ export const Log = forwardRef(function Log({ 'aria-label': ariaLabel, className,
56
58
  }, [items, levels]);
57
59
  const availableLevels = useMemo(() => new Set(resolvedLevels.map(level => level.value)), [resolvedLevels]);
58
60
  const [uncontrolledLevels, setUncontrolledLevels] = useState(defaultFilteredLevels);
59
- const selectedLevels = (filteredLevels ?? uncontrolledLevels)?.filter(level => availableLevels.has(level));
61
+ const requestedLevels = filteredLevels ?? uncontrolledLevels;
62
+ const selectedLevels = useMemo(() => requestedLevels?.filter(level => availableLevels.has(level)), [requestedLevels, availableLevels]);
63
+ const selectedLevelSet = useMemo(() => selectedLevels === undefined ? undefined : new Set(selectedLevels), [selectedLevels]);
60
64
  const allLevelsSelected = selectedLevels == null
61
- || (selectedLevels.length === resolvedLevels.length && resolvedLevels.every(level => selectedLevels.includes(level.value)));
65
+ || (selectedLevels.length === resolvedLevels.length && resolvedLevels.every(level => selectedLevelSet.has(level.value)));
62
66
  const [uncontrolledPaused, setUncontrolledPaused] = useState(defaultPaused);
63
67
  const resolvedPaused = paused ?? uncontrolledPaused;
64
68
  const [nearBottom, setNearBottom] = useState(true);
@@ -68,16 +72,20 @@ export const Log = forwardRef(function Log({ 'aria-label': ariaLabel, className,
68
72
  const previousPausedRef = useRef(resolvedPaused);
69
73
  const nearBottomRef = useRef(true);
70
74
  const threshold = normalizedThreshold(nearBottomThreshold);
71
- const visibleItems = selectedLevels == null
75
+ const visibleItems = useMemo(() => selectedLevelSet === undefined
72
76
  ? items
73
- : items.filter(item => item.level != null && selectedLevels.includes(item.level));
74
- const itemIds = items.map(item => item.id);
75
- const visibleIds = visibleItems.map(item => item.id);
77
+ : items.filter(item => item.level != null && selectedLevelSet.has(item.level)), [items, selectedLevelSet]);
78
+ const itemIds = useMemo(() => items.map(item => item.id), [items]);
79
+ const visibleIds = useMemo(() => visibleItems.map(item => item.id), [visibleItems]);
76
80
  const scrollToLatest = useCallback((behavior = 'smooth') => {
77
81
  const viewport = viewportRef.current;
78
82
  if (!viewport)
79
83
  return;
80
- viewport.scrollTo({ top: viewport.scrollHeight, behavior });
84
+ const resolvedBehavior = behavior === 'smooth'
85
+ && viewport.ownerDocument.defaultView?.matchMedia('(prefers-reduced-motion: reduce)').matches
86
+ ? 'auto'
87
+ : behavior;
88
+ viewport.scrollTo({ top: viewport.scrollHeight, behavior: resolvedBehavior });
81
89
  nearBottomRef.current = true;
82
90
  setNearBottom(true);
83
91
  }, []);
@@ -99,7 +107,7 @@ export const Log = forwardRef(function Log({ 'aria-label': ariaLabel, className,
99
107
  previousItemIdsRef.current = itemIds;
100
108
  previousVisibleIdsRef.current = visibleIds;
101
109
  previousPausedRef.current = resolvedPaused;
102
- }, [itemIds.join('\u0000'), resolvedPaused, scrollToLatest, visibleIds.join('\u0000')]);
110
+ }, [itemIds, resolvedPaused, scrollToLatest, visibleIds]);
103
111
  function changePaused(nextPaused) {
104
112
  if (paused === undefined)
105
113
  setUncontrolledPaused(nextPaused);
@@ -135,25 +143,26 @@ export const Log = forwardRef(function Log({ 'aria-label': ariaLabel, className,
135
143
  const viewportLabel = viewportProps?.['aria-label'] ?? ariaLabel ?? labels.list;
136
144
  const viewportClassName = classes('inkra-log__viewport', viewportProps?.className);
137
145
  const viewportTabIndex = viewportProps?.tabIndex ?? 0;
138
- const levelMap = new Map(resolvedLevels.map(level => [level.value, level]));
146
+ const levelMap = useMemo(() => new Map(resolvedLevels.map(level => [level.value, level])), [resolvedLevels]);
147
+ const showLatest = (!nearBottom || resolvedPaused) && visibleItems.length > 0;
139
148
  return (_jsxs(PencilSurface, { ...props, as: "div", className: classes('inkra-log', className), "data-paused": resolvedPaused || undefined, "data-toolbar": ((showLevelFilter && resolvedLevels.length > 0) || showFollowControl) || undefined, ref: ref, variant: "paper", children: [(showLevelFilter && resolvedLevels.length > 0) || showFollowControl ? _jsxs("div", { className: "inkra-log__toolbar", children: [showLevelFilter && resolvedLevels.length > 0 ? _jsxs("div", { "aria-label": labels.filters, className: "inkra-log__filters", role: "group", children: [_jsx(Button, { "aria-pressed": !hasFilter, size: "sm", variant: !hasFilter ? 'secondary' : 'ghost', onClick: () => changeLevels(undefined), children: labels.allLevels }), resolvedLevels.map(level => {
140
- const selected = allLevelsSelected || selectedLevels?.includes(level.value) === true;
149
+ const selected = allLevelsSelected || selectedLevelSet?.has(level.value) === true;
141
150
  return _jsx(Button, { "aria-pressed": selected, "data-level": level.value, "data-tone": level.tone ?? defaultTones[level.value] ?? 'neutral', size: "sm", variant: selected ? 'secondary' : 'ghost', onClick: () => {
142
151
  const active = selectedLevels ?? resolvedLevels.map(option => option.value);
143
152
  changeLevels(selected
144
153
  ? active.filter(value => value !== level.value)
145
154
  : [...active, level.value]);
146
155
  }, children: level.label }, level.value);
147
- })] }) : null, showFollowControl ? _jsx(Button, { "aria-pressed": resolvedPaused, className: "inkra-log__follow-control", size: "sm", startIcon: _jsx(PencilIcon, { paths: resolvedPaused ? followPaths : pausePaths, size: 18, sourceSize: 24 }), variant: "outline", onClick: () => changePaused(!resolvedPaused), children: resolvedPaused ? labels.resume : labels.pause }) : null, _jsx(PencilLine, { className: "inkra-log__toolbar-divider", seed: "inkra-log-toolbar-divider" })] }) : null, _jsx("div", { ...viewportProps, "aria-label": viewportLabel, className: viewportClassName, onKeyDown: handleViewportKeyDown, onScroll: handleScroll, ref: viewportRef, role: "region", tabIndex: viewportTabIndex, children: visibleItems.length > 0 ? _jsx("ol", { className: "inkra-log__list", children: visibleItems.map((item, index) => {
148
- const definition = item.level ? levelMap.get(item.level) : undefined;
149
- const tone = definition?.tone ?? (item.level ? defaultTones[item.level] : undefined) ?? 'neutral';
150
- return _jsxs("li", { className: "inkra-log__item", "data-has-icon": item.icon != null || undefined, "data-level": item.level, "data-tone": tone, children: [index > 0 ? _jsx(PencilLine, { className: "inkra-log__item-divider", seed: `inkra-log-item-${item.id}-divider` }) : null, item.icon == null ? null : _jsx("span", { "aria-hidden": "true", className: "inkra-log__icon", children: item.icon }), _jsxs("div", { className: "inkra-log__record", children: [item.time == null && item.level == null ? null : _jsxs("div", { className: "inkra-log__meta", children: [item.time == null ? null : item.dateTime
151
- ? _jsx("time", { dateTime: item.dateTime, children: renderText(item.time) })
152
- : _jsx("span", { children: renderText(item.time) }), item.level == null ? null : _jsx("span", { className: "inkra-log__level", children: renderText(definition?.label ?? item.level) })] }), _jsx("div", { className: "inkra-log__content", children: renderText(item.content) })] })] }, item.id);
153
- }) }) : _jsx("div", { className: "inkra-log__empty", children: renderText(isEmpty ? (empty ?? labels.empty) : (noMatches ?? labels.noMatches)) }) }), (!nearBottom || resolvedPaused) && visibleItems.length > 0 ? _jsx("div", { className: "inkra-log__latest", children: _jsx(Button, { size: "sm", startIcon: _jsx(PencilIcon, { paths: followPaths, size: 18, sourceSize: 24 }), variant: "secondary", onClick: () => {
154
- if (resolvedPaused)
155
- changePaused(false);
156
- else
157
- scrollToLatest();
158
- }, children: resolvedPaused ? labels.resume : labels.latest }) }) : null] }));
156
+ })] }) : null, showFollowControl ? _jsx(Button, { "aria-pressed": resolvedPaused, className: "inkra-log__follow-control", size: "sm", startIcon: _jsx(PencilIcon, { paths: resolvedPaused ? followPaths : pausePaths, size: 18, sourceSize: 24 }), variant: "outline", onClick: () => changePaused(!resolvedPaused), children: resolvedPaused ? labels.resume : labels.pause }) : null, _jsx(PencilLine, { className: "inkra-log__toolbar-divider", seed: "inkra-log-toolbar-divider" })] }) : null, _jsxs("div", { className: "inkra-log__viewport-frame", children: [_jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-log__focus-ring", variant: "outline", seed: "inkra-log-focus" }), _jsx("div", { ...viewportProps, "aria-label": viewportLabel, className: viewportClassName, onKeyDown: handleViewportKeyDown, onScroll: handleScroll, ref: viewportRef, role: "region", tabIndex: viewportTabIndex, children: visibleItems.length > 0 ? _jsx("ol", { className: "inkra-log__list", children: visibleItems.map((item, index) => {
157
+ const definition = item.level ? levelMap.get(item.level) : undefined;
158
+ const tone = definition?.tone ?? (item.level ? defaultTones[item.level] : undefined) ?? 'neutral';
159
+ return _jsxs("li", { className: "inkra-log__item", "data-has-icon": item.icon != null || undefined, "data-level": item.level, "data-tone": tone, children: [index > 0 ? _jsx(PencilLine, { className: "inkra-log__item-divider", seed: `inkra-log-item-${item.id}-divider` }) : null, item.icon == null ? null : _jsx("span", { "aria-hidden": "true", className: "inkra-log__icon", children: item.icon }), _jsxs("div", { className: "inkra-log__record", children: [item.time == null && item.level == null ? null : _jsxs("div", { className: "inkra-log__meta", children: [item.time == null ? null : item.dateTime
160
+ ? _jsx("time", { dateTime: item.dateTime, children: renderText(item.time) })
161
+ : _jsx("span", { children: renderText(item.time) }), item.level == null ? null : _jsx("span", { className: "inkra-log__level", children: renderText(definition?.label ?? item.level) })] }), _jsx("div", { className: "inkra-log__content", children: renderText(item.content) })] })] }, item.id);
162
+ }) }) : _jsx("div", { className: "inkra-log__empty", children: renderText(isEmpty ? (empty ?? labels.empty) : (noMatches ?? labels.noMatches)) }) })] }), showLatest ? _jsxs("div", { className: "inkra-log__latest", children: [_jsx(PencilLine, { className: "inkra-log__latest-divider", seed: "inkra-log-latest-divider" }), _jsx(Button, { size: "sm", startIcon: _jsx(PencilIcon, { paths: followPaths, size: 18, sourceSize: 24 }), variant: "secondary", onClick: () => {
163
+ if (resolvedPaused)
164
+ changePaused(false);
165
+ else
166
+ scrollToLatest();
167
+ }, children: resolvedPaused ? labels.resume : labels.latest })] }) : null] }));
159
168
  });
@@ -0,0 +1,19 @@
1
+ import 'prismjs/components/prism-jsx.js';
2
+ import 'prismjs/components/prism-typescript.js';
3
+ import 'prismjs/components/prism-tsx.js';
4
+ import 'prismjs/components/prism-bash.js';
5
+ import 'prismjs/components/prism-json.js';
6
+ import 'prismjs/components/prism-markdown.js';
7
+ import { type ComponentPropsWithoutRef } from 'react';
8
+ export interface MarkdownCodeLabels {
9
+ plainText: string;
10
+ copy: string;
11
+ copied: string;
12
+ copyFailed: string;
13
+ }
14
+ export interface MarkdownCodeBlockProps extends Omit<ComponentPropsWithoutRef<'pre'>, 'children'> {
15
+ code: string;
16
+ codeClassName?: string;
17
+ labels: MarkdownCodeLabels;
18
+ }
19
+ export declare function MarkdownCodeBlock({ code, codeClassName, className, labels, tabIndex: _tabIndex, ...props }: MarkdownCodeBlockProps): import("react").JSX.Element;
@@ -0,0 +1,86 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import Prism from 'prismjs';
3
+ import 'prismjs/components/prism-jsx.js';
4
+ import 'prismjs/components/prism-typescript.js';
5
+ import 'prismjs/components/prism-tsx.js';
6
+ import 'prismjs/components/prism-bash.js';
7
+ import 'prismjs/components/prism-json.js';
8
+ import 'prismjs/components/prism-markdown.js';
9
+ import { useEffect, useMemo, useRef, useState } from 'react';
10
+ import { Button } from '../Button.js';
11
+ import { Card } from '../content/index.js';
12
+ import { PencilIcon } from '../PencilIcon.js';
13
+ import { PencilSurface } from '../PencilSurface.js';
14
+ import { PencilText } from '../PencilText.js';
15
+ import { Tooltip } from '../tooltip/Tooltip.js';
16
+ import { codeIcons } from './icons.js';
17
+ Prism.manual = true;
18
+ const languageAliases = {
19
+ js: 'javascript',
20
+ md: 'markdown',
21
+ sh: 'bash',
22
+ shell: 'bash',
23
+ ts: 'typescript',
24
+ txt: 'plain',
25
+ text: 'plain',
26
+ plaintext: 'plain',
27
+ };
28
+ const languageNames = {
29
+ javascript: 'JavaScript', typescript: 'TypeScript', jsx: 'JSX', tsx: 'TSX',
30
+ bash: 'Bash', json: 'JSON', markdown: 'Markdown', css: 'CSS', html: 'HTML',
31
+ };
32
+ function classes(...values) {
33
+ return values.filter(Boolean).join(' ');
34
+ }
35
+ function flatten(stream, inheritedClasses = '') {
36
+ const tokens = Array.isArray(stream) ? stream : [stream];
37
+ return tokens.flatMap(token => {
38
+ if (typeof token === 'string')
39
+ return [{ text: token, classes: inheritedClasses }];
40
+ const aliases = Array.isArray(token.alias) ? token.alias : token.alias ? [token.alias] : [];
41
+ const tokenClasses = classes(inheritedClasses, `token-${token.type}`, ...aliases.map(alias => `token-${alias}`));
42
+ return flatten(token.content, tokenClasses);
43
+ });
44
+ }
45
+ function languageFromClassName(className) {
46
+ return /(?:^|\s)language-([^\s]+)/.exec(className ?? '')?.[1]?.toLowerCase();
47
+ }
48
+ function tokenize(code, language) {
49
+ const normalized = languageAliases[language ?? ''] ?? language ?? 'plain';
50
+ const grammar = Prism.languages[normalized] ?? Prism.languages.plain;
51
+ return flatten(Prism.tokenize(code, grammar)).flatMap(part => part.text.split(/(\r\n|\r|\n)/).filter(Boolean).map(text => ({ ...part, text })));
52
+ }
53
+ export function MarkdownCodeBlock({ code, codeClassName, className, labels, tabIndex: _tabIndex, ...props }) {
54
+ const language = languageFromClassName(codeClassName);
55
+ const normalizedLanguage = languageAliases[language ?? ''] ?? language ?? 'plain';
56
+ const languageLabel = normalizedLanguage === 'plain' ? labels.plainText : languageNames[normalizedLanguage] ?? language;
57
+ const parts = useMemo(() => tokenize(code, language), [code, language]);
58
+ const [status, setStatus] = useState('idle');
59
+ const request = useRef(0);
60
+ const timer = useRef(undefined);
61
+ useEffect(() => {
62
+ setStatus('idle');
63
+ return () => { request.current += 1; clearTimeout(timer.current); };
64
+ }, [code]);
65
+ const copyLabel = status === 'copied' ? labels.copied : status === 'failed' ? labels.copyFailed : labels.copy;
66
+ async function copyCode() {
67
+ const id = ++request.current;
68
+ clearTimeout(timer.current);
69
+ setStatus('copying');
70
+ try {
71
+ await navigator.clipboard.writeText(code);
72
+ if (request.current !== id)
73
+ return;
74
+ setStatus('copied');
75
+ }
76
+ catch {
77
+ if (request.current !== id)
78
+ return;
79
+ setStatus('failed');
80
+ }
81
+ timer.current = setTimeout(() => setStatus('idle'), 2000);
82
+ }
83
+ return _jsxs(Card.Root, { className: "inkra-markdown__code-frame", radius: 6, seed: `markdown-code-frame-${language ?? 'plain'}`, children: [_jsx(Card.Header, { className: "inkra-markdown__code-header", align: "center", action: _jsx(Tooltip.Provider, { children: _jsxs(Tooltip.Root, { children: [_jsx(Tooltip.Trigger, { asChild: true, children: _jsx(Button, { size: "icon", variant: "ghost", "aria-label": copyLabel, "aria-busy": status === 'copying' || undefined, disabled: status === 'copying', onClick: () => void copyCode(), children: _jsx(PencilIcon, { paths: status === 'copied' ? codeIcons.check : codeIcons.copy, size: 22 }) }) }), _jsx(Tooltip.Portal, { children: _jsx(Tooltip.Content, { children: copyLabel }) })] }) }), children: _jsxs("span", { className: "inkra-markdown__code-language", children: [_jsx(PencilIcon, { paths: normalizedLanguage === 'bash' ? codeIcons.terminal : codeIcons.code, size: 22 }), _jsx(PencilText, { children: languageLabel })] }) }), _jsx("pre", { ...props, className: classes('inkra-markdown__code-surface', className), "data-language": language ?? 'plain', tabIndex: 0, children: _jsx("code", { className: classes('inkra-markdown__code', codeClassName), children: parts.map((part, index) => /\S/.test(part.text)
84
+ ? _jsx(PencilText, { className: classes('inkra-markdown__code-token', part.classes), seed: `markdown-code-${language ?? 'plain'}-${index}-${part.text}`, children: part.text }, index)
85
+ : part.text) }) }), _jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-markdown__code-focus", radius: 6, seed: "markdown-code-focus", variant: "outline" }), _jsx("span", { className: "inkra-markdown__code-status", role: "status", children: status === 'copied' ? labels.copied : status === 'failed' ? labels.copyFailed : '' })] });
86
+ }
@@ -0,0 +1,2 @@
1
+ import { type ComponentPropsWithoutRef } from 'react';
2
+ export declare function Deletion({ children, className, ...props }: ComponentPropsWithoutRef<'del'>): import("react").JSX.Element;
@@ -0,0 +1,69 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRef, useState } from 'react';
3
+ import { PencilLine } from '../PencilLine.js';
4
+ import { useBrowserLayoutEffect } from '../internal/useBrowserLayoutEffect.js';
5
+ export function Deletion({ children, className, ...props }) {
6
+ const ref = useRef(null);
7
+ const [lines, setLines] = useState([]);
8
+ useBrowserLayoutEffect(() => {
9
+ const node = ref.current;
10
+ if (!node || !document.createElement('canvas').getContext('2d'))
11
+ return;
12
+ let disposed = false;
13
+ let frame = 0;
14
+ const measure = () => {
15
+ frame = 0;
16
+ if (disposed)
17
+ return;
18
+ const box = node.getBoundingClientRect();
19
+ const scaleX = node.offsetWidth ? box.width / node.offsetWidth : 1;
20
+ const scaleY = node.offsetHeight ? box.height / node.offsetHeight : 1;
21
+ const fragments = [];
22
+ const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT);
23
+ const range = document.createRange();
24
+ // Use native line fragments, not one line through the whole wrapped block.
25
+ for (let text = walker.nextNode(); text; text = walker.nextNode()) {
26
+ if (!text.textContent?.trim() || text.parentElement?.closest('[aria-hidden="true"]'))
27
+ continue;
28
+ range.selectNodeContents(text);
29
+ for (const rect of range.getClientRects()) {
30
+ if (!rect.width || !rect.height)
31
+ continue;
32
+ const top = (rect.top - box.top + rect.height * 0.52) / (scaleY || 1) - 3;
33
+ const left = (rect.left - box.left) / (scaleX || 1);
34
+ const width = rect.width / (scaleX || 1);
35
+ const line = fragments.find(item => Math.abs(item.top - top) < 3);
36
+ if (line) {
37
+ const end = Math.max(line.left + line.width, left + width);
38
+ line.left = Math.min(line.left, left);
39
+ line.width = end - line.left;
40
+ }
41
+ else
42
+ fragments.push({ left, top, width });
43
+ }
44
+ }
45
+ const next = fragments.map(line => ({
46
+ left: Math.round(line.left * 2) / 2,
47
+ top: Math.round(line.top * 2) / 2,
48
+ width: Math.round(line.width * 2) / 2,
49
+ }));
50
+ setLines(previous => JSON.stringify(previous) === JSON.stringify(next) ? previous : next);
51
+ };
52
+ const schedule = () => {
53
+ if (!disposed && !frame)
54
+ frame = requestAnimationFrame(measure);
55
+ };
56
+ const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(schedule);
57
+ observer?.observe(node);
58
+ document.fonts?.addEventListener('loadingdone', schedule);
59
+ document.fonts?.ready.then(schedule);
60
+ measure();
61
+ return () => {
62
+ disposed = true;
63
+ observer?.disconnect();
64
+ document.fonts?.removeEventListener('loadingdone', schedule);
65
+ cancelAnimationFrame(frame);
66
+ };
67
+ }, [children]);
68
+ return _jsxs("del", { ...props, className: ['inkra-markdown__deleted', className].filter(Boolean).join(' '), ref: ref, "data-pencil-strike-ready": lines.length > 0 || undefined, children: [children, lines.map((line, index) => _jsx(PencilLine, { className: "inkra-markdown__strike", seed: `markdown-strike-${index}`, weight: 0.7, roughness: 0.4, style: { left: line.left, top: line.top, width: line.width } }, index))] });
69
+ }
@@ -0,0 +1,14 @@
1
+ export declare const codeIcons: {
2
+ copy: {
3
+ d: string;
4
+ }[];
5
+ check: {
6
+ d: string;
7
+ }[];
8
+ terminal: {
9
+ d: string;
10
+ }[];
11
+ code: {
12
+ d: string;
13
+ }[];
14
+ };
@@ -0,0 +1,16 @@
1
+ import { __iconData as copy } from 'lucide-react/dist/esm/icons/copy.mjs';
2
+ import { __iconData as check } from 'lucide-react/dist/esm/icons/check.mjs';
3
+ import { __iconData as terminal } from 'lucide-react/dist/esm/icons/terminal.mjs';
4
+ import { __iconData as code } from 'lucide-react/dist/esm/icons/code-xml.mjs';
5
+ function paths(data) {
6
+ return data.node.flatMap(([tag, attributes]) => {
7
+ if (tag === 'path')
8
+ return [{ d: attributes.d }];
9
+ if (tag !== 'rect')
10
+ return [];
11
+ const { x = 0, y = 0, width = 0, height = 0, rx = 0 } = Object.fromEntries(Object.entries(attributes).map(([key, value]) => [key, Number(value)]));
12
+ const r = Math.min(rx, width / 2, height / 2);
13
+ return [{ d: `M${x + r} ${y}H${x + width - r}Q${x + width} ${y} ${x + width} ${y + r}V${y + height - r}Q${x + width} ${y + height} ${x + width - r} ${y + height}H${x + r}Q${x} ${y + height} ${x} ${y + height - r}V${y + r}Q${x} ${y} ${x + r} ${y}Z` }];
14
+ });
15
+ }
16
+ export const codeIcons = { copy: paths(copy), check: paths(check), terminal: paths(terminal), code: paths(code) };
@@ -0,0 +1,17 @@
1
+ import type { Components } from 'react-markdown';
2
+ import { type ComponentPropsWithoutRef } from 'react';
3
+ import { type MarkdownCodeLabels } from './CodeBlock.js';
4
+ export interface MarkdownLabels extends Partial<MarkdownCodeLabels> {
5
+ code?: string;
6
+ table?: string;
7
+ taskChecked?: string;
8
+ taskUnchecked?: string;
9
+ }
10
+ export interface MarkdownProps extends Omit<ComponentPropsWithoutRef<'div'>, 'children'> {
11
+ children: string;
12
+ components?: Components;
13
+ headingLevelOffset?: number;
14
+ labels?: MarkdownLabels;
15
+ transformUrl?: (url: string, kind: 'link' | 'image') => string | null | undefined;
16
+ }
17
+ export declare const Markdown: import("react").ForwardRefExoticComponent<MarkdownProps & import("react").RefAttributes<HTMLDivElement>>;