@inkra/react 0.2.0 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -7
- package/dist/Button.js +3 -2
- package/dist/PencilIcon.d.ts +10 -0
- package/dist/PencilIcon.js +83 -39
- package/dist/PencilLine.js +9 -1
- package/dist/PencilText.js +15 -9
- package/dist/accordion/Accordion.js +65 -5
- package/dist/alert-dialog/index.d.ts +1 -1
- package/dist/alert-dialog/index.js +18 -6
- package/dist/audio-player/index.d.ts +2 -0
- package/dist/audio-player/index.js +126 -31
- package/dist/calendar/index.d.ts +4 -1
- package/dist/calendar/index.js +31 -12
- package/dist/carousel/index.js +13 -6
- package/dist/choice-grid/index.d.ts +6 -1
- package/dist/choice-grid/index.js +61 -33
- package/dist/color-picker/GradientEditor.d.ts +39 -0
- package/dist/color-picker/GradientEditor.js +63 -0
- package/dist/color-picker/gradient.d.ts +25 -0
- package/dist/color-picker/gradient.js +60 -0
- package/dist/color-picker/index.d.ts +27 -7
- package/dist/color-picker/index.js +134 -32
- package/dist/color-swatch/index.d.ts +10 -0
- package/dist/color-swatch/index.js +9 -0
- package/dist/command/index.d.ts +24 -2
- package/dist/command/index.js +29 -6
- package/dist/content/index.d.ts +5 -0
- package/dist/content/index.js +12 -3
- package/dist/context-menu/index.d.ts +2 -2
- package/dist/context-menu/index.js +4 -3
- package/dist/data-table/index.d.ts +7 -1
- package/dist/data-table/index.js +64 -16
- package/dist/data-table/model.d.ts +2 -0
- package/dist/data-table/model.js +7 -0
- package/dist/date-picker/index.d.ts +2 -0
- package/dist/date-picker/index.js +10 -9
- package/dist/day-agenda/index.d.ts +6 -1
- package/dist/day-agenda/index.js +46 -21
- package/dist/day-agenda/layout.js +65 -9
- package/dist/display/ProgressRing.js +7 -3
- package/dist/display/index.d.ts +4 -1
- package/dist/display/index.js +3 -2
- package/dist/dockable-panel/index.js +5 -5
- package/dist/drawer/index.d.ts +1 -1
- package/dist/drawer/index.js +9 -6
- package/dist/dropdown-menu/index.d.ts +1 -1
- package/dist/dropdown-menu/index.js +3 -2
- package/dist/editable/index.d.ts +24 -2
- package/dist/editable/index.js +142 -37
- package/dist/empty/illustrations.d.ts +8 -0
- package/dist/empty/illustrations.js +78 -0
- package/dist/empty/index.d.ts +6 -1
- package/dist/empty/index.js +11 -3
- package/dist/flip/index.js +28 -8
- package/dist/form/EditablePencilText.js +34 -0
- package/dist/form/Field.js +11 -10
- package/dist/form/Input.js +1 -1
- package/dist/form/Textarea.d.ts +3 -1
- package/dist/form/Textarea.js +6 -4
- package/dist/hotkey/index.d.ts +1 -0
- package/dist/hotkey/index.js +6 -4
- package/dist/hover-card/index.js +1 -1
- package/dist/image-cropper/index.js +4 -2
- package/dist/image-viewer/VideoResource.d.ts +21 -0
- package/dist/image-viewer/VideoResource.js +194 -0
- package/dist/image-viewer/index.d.ts +14 -0
- package/dist/image-viewer/index.js +118 -65
- package/dist/image-viewer/lucideGeometry.d.ts +3 -0
- package/dist/image-viewer/lucideGeometry.js +19 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/input-number/index.d.ts +6 -0
- package/dist/input-number/index.js +131 -15
- package/dist/input-number/model.d.ts +13 -0
- package/dist/input-number/model.js +50 -0
- package/dist/internal/cache.d.ts +4 -0
- package/dist/internal/cache.js +18 -1
- package/dist/internal/iconGeometry.d.ts +2 -0
- package/dist/internal/iconGeometry.js +32 -0
- package/dist/internal/renderTokens.d.ts +4 -0
- package/dist/internal/renderTokens.js +4 -0
- package/dist/internal/stroke.d.ts +7 -0
- package/dist/internal/stroke.js +63 -7
- package/dist/internal/surfaceBudget.d.ts +20 -0
- package/dist/internal/surfaceBudget.js +69 -0
- package/dist/internal/surfaceRenderer.js +87 -28
- package/dist/internal/surfaceVisibility.d.ts +3 -0
- package/dist/internal/surfaceVisibility.js +60 -0
- package/dist/internal/textRenderer.d.ts +2 -0
- package/dist/internal/textRenderer.js +3 -3
- package/dist/internal/usePencilSurface.js +17 -1
- package/dist/internal/wash.js +4 -3
- package/dist/journey-map/terrain.js +10 -5
- package/dist/layout/index.js +1 -1
- package/dist/log/index.js +29 -22
- package/dist/markdown/CodeBlock.d.ts +19 -0
- package/dist/markdown/CodeBlock.js +86 -0
- package/dist/markdown/Deletion.d.ts +2 -0
- package/dist/markdown/Deletion.js +69 -0
- package/dist/markdown/icons.d.ts +14 -0
- package/dist/markdown/icons.js +16 -0
- package/dist/markdown/index.d.ts +17 -0
- package/dist/markdown/index.js +175 -0
- package/dist/node-graph/index.d.ts +2 -0
- package/dist/node-graph/index.js +49 -25
- package/dist/number-ticker/index.d.ts +11 -9
- package/dist/number-ticker/index.js +14 -11
- package/dist/number-ticker/model.d.ts +38 -5
- package/dist/number-ticker/model.js +139 -10
- package/dist/number-ticker/precision.d.ts +7 -0
- package/dist/number-ticker/precision.js +141 -0
- package/dist/overflow-list/index.js +5 -2
- package/dist/overlay/Overlay.d.ts +1 -1
- package/dist/overlay/Overlay.js +5 -3
- package/dist/overlay/Popover.d.ts +1 -1
- package/dist/overlay/Popover.js +6 -5
- package/dist/overlay/themeBridge.d.ts +12 -1
- package/dist/overlay/themeBridge.js +41 -3
- package/dist/pagination/index.d.ts +10 -1
- package/dist/pagination/index.js +6 -4
- package/dist/pan-zoom/index.d.ts +2 -0
- package/dist/pan-zoom/index.js +7 -6
- package/dist/pencil.d.ts +1 -0
- package/dist/pencil.js +4 -4
- package/dist/radar/index.d.ts +4 -0
- package/dist/radar/index.js +41 -21
- package/dist/rating/index.d.ts +4 -0
- package/dist/rating/index.js +128 -24
- package/dist/resizable/index.js +1 -1
- package/dist/scroll-area/index.d.ts +1 -1
- package/dist/scroll-area/index.js +5 -2
- package/dist/select/MultipleSelect.js +5 -4
- package/dist/select/SearchableSelect.js +61 -6
- package/dist/select/Select.d.ts +1 -0
- package/dist/select/Select.js +3 -1
- package/dist/select/StepperSelect.d.ts +29 -0
- package/dist/select/StepperSelect.js +119 -0
- package/dist/select/index.d.ts +2 -0
- package/dist/select/index.js +1 -0
- package/dist/select/stepperModel.d.ts +7 -0
- package/dist/select/stepperModel.js +29 -0
- package/dist/selection/Checkbox.js +4 -3
- package/dist/selection/RadioGroup.js +29 -13
- package/dist/selection/Switch.js +4 -3
- package/dist/selection/useNativeChecked.d.ts +2 -1
- package/dist/selection/useNativeChecked.js +26 -8
- package/dist/slider/index.js +50 -6
- package/dist/sortable/index.js +15 -6
- package/dist/sparkline/index.js +17 -2
- package/dist/sparkline/model.js +11 -4
- package/dist/styles/accordion.css +228 -0
- package/dist/styles/alert-dialog.css +139 -0
- package/dist/styles/audio-player.css +22 -0
- package/dist/styles/base.css +638 -0
- package/dist/styles/breadcrumb.css +7 -0
- package/dist/styles/calendar.css +62 -0
- package/dist/styles/carousel.css +173 -0
- package/dist/styles/choice-grid.css +226 -0
- package/dist/styles/color-picker.css +281 -0
- package/dist/styles/color-swatch.css +47 -0
- package/dist/styles/command.css +52 -0
- package/dist/styles/content.css +79 -0
- package/dist/styles/context-menu.css +35 -0
- package/dist/styles/data-table.css +31 -0
- package/dist/styles/date-picker.css +107 -0
- package/dist/styles/day-agenda.css +30 -0
- package/dist/styles/display.css +304 -0
- package/dist/styles/dockable-panel.css +110 -0
- package/dist/styles/drawer.css +133 -0
- package/dist/styles/dropdown-menu.css +68 -0
- package/dist/styles/editable.css +145 -0
- package/dist/styles/empty.css +108 -0
- package/dist/styles/flip.css +101 -0
- package/dist/styles/form.css +634 -0
- package/dist/styles/heatmap.css +256 -0
- package/dist/styles/hotkey.css +189 -0
- package/dist/styles/hover-card.css +27 -0
- package/dist/styles/image-cropper.css +205 -0
- package/dist/styles/image-viewer.css +422 -0
- package/dist/styles/input-number.css +21 -0
- package/dist/styles/journey-map.css +19 -0
- package/dist/styles/keypad.css +92 -0
- package/dist/styles/layout.css +72 -0
- package/dist/styles/leaflet.css +661 -0
- package/dist/styles/log.css +203 -0
- package/dist/styles/markdown.css +379 -0
- package/dist/styles/node-graph.css +142 -0
- package/dist/styles/number-ticker.css +83 -0
- package/dist/styles/overflow-list.css +51 -0
- package/dist/styles/overlay.css +284 -0
- package/dist/styles/pagination.css +21 -0
- package/dist/styles/pan-zoom.css +136 -0
- package/dist/styles/radar.css +1 -0
- package/dist/styles/rating.css +121 -0
- package/dist/styles/resizable.css +108 -0
- package/dist/styles/scroll-area.css +122 -0
- package/dist/styles/select.css +557 -0
- package/dist/styles/selection.css +264 -0
- package/dist/styles/slider.css +145 -0
- package/dist/styles/slot.css +109 -0
- package/dist/styles/sortable.css +159 -0
- package/dist/styles/sparkline.css +24 -0
- package/dist/styles/steps.css +27 -0
- package/dist/styles/tabs.css +79 -0
- package/dist/styles/tag-input.css +213 -0
- package/dist/styles/time-picker.css +66 -0
- package/dist/styles/timeline.css +24 -0
- package/dist/styles/toast.css +160 -0
- package/dist/styles/toggle-group.css +107 -0
- package/dist/styles/tooltip.css +78 -0
- package/dist/styles/transfer.css +148 -0
- package/dist/styles/tree-select.css +131 -0
- package/dist/styles/tree.css +59 -0
- package/dist/styles/typewriter.css +62 -0
- package/dist/styles/upload.css +60 -0
- package/dist/styles/virtual-list.css +112 -0
- package/dist/styles.css +2241 -443
- package/dist/tabs/index.d.ts +9 -0
- package/dist/tabs/index.js +66 -16
- package/dist/tabs/useOverflow.d.ts +20 -0
- package/dist/tabs/useOverflow.js +145 -0
- package/dist/tag-input/index.d.ts +7 -2
- package/dist/tag-input/index.js +70 -26
- package/dist/time-picker/index.js +38 -20
- package/dist/toast/index.d.ts +19 -11
- package/dist/toast/index.js +48 -8
- package/dist/toast/manager.d.ts +42 -0
- package/dist/toast/manager.js +85 -0
- package/dist/toast/useToastNavigation.d.ts +3 -0
- package/dist/toast/useToastNavigation.js +75 -0
- package/dist/tokens.css +4 -0
- package/dist/tooltip/Tooltip.d.ts +1 -1
- package/dist/tooltip/Tooltip.js +6 -5
- package/dist/transfer/index.d.ts +5 -0
- package/dist/transfer/index.js +93 -29
- package/dist/transfer/model.d.ts +4 -0
- package/dist/transfer/model.js +37 -11
- package/dist/tree/index.d.ts +5 -1
- package/dist/tree/index.js +5 -5
- package/dist/tree-select/index.js +67 -30
- package/dist/tree-select/model.d.ts +11 -5
- package/dist/tree-select/model.js +111 -83
- package/dist/typewriter/index.js +10 -10
- package/dist/upload/index.js +30 -16
- package/dist/virtual-list/index.js +10 -7
- package/package.json +416 -5
|
@@ -2,6 +2,8 @@ import { pencilPaper } from './paper.js';
|
|
|
2
2
|
import { pencilStroke } from './stroke.js';
|
|
3
3
|
import { drawPencilWash } from './wash.js';
|
|
4
4
|
import { pencilRenderDefaults } from './renderTokens.js';
|
|
5
|
+
import { BoundedCache, PENCIL_RENDERER_VERSION, canvasByteCost, canvasIsUsable } from './cache.js';
|
|
6
|
+
import { releaseSurfaceBitmaps, resizeSurfaceBitmap, resolveSurfaceAllocation } from './surfaceBudget.js';
|
|
5
7
|
export const PENCIL_ACTION_WASH = Object.freeze({
|
|
6
8
|
lightStrength: 0.2,
|
|
7
9
|
darkStrength: 0.12,
|
|
@@ -71,19 +73,27 @@ function paperPattern(context, color, dark, dpr) {
|
|
|
71
73
|
}
|
|
72
74
|
return pattern;
|
|
73
75
|
}
|
|
74
|
-
|
|
76
|
+
function allocateSurface(canvas, borderCanvas, width, height, dpr, bleed) {
|
|
77
|
+
const allocation = resolveSurfaceAllocation(width, height, dpr, bleed, borderCanvas ? 2 : 1);
|
|
78
|
+
if (!allocation.ok) {
|
|
79
|
+
releaseSurfaceBitmaps(canvas, borderCanvas);
|
|
80
|
+
throw new RangeError(`PencilSurface allocation rejected: ${allocation.reason}`);
|
|
81
|
+
}
|
|
82
|
+
return allocation;
|
|
83
|
+
}
|
|
84
|
+
function paintPencilSurface(canvas, borderCanvas, width, height, variant, seed, allocation, theme, radius) {
|
|
75
85
|
const bleed = variant === 'wash' ? 12 : 0;
|
|
76
|
-
|
|
77
|
-
canvas.
|
|
86
|
+
const renderDpr = allocation.dpr;
|
|
87
|
+
resizeSurfaceBitmap(canvas, allocation.width, allocation.height);
|
|
78
88
|
const context = canvas.getContext('2d');
|
|
79
|
-
if (!context)
|
|
89
|
+
if (!context) {
|
|
90
|
+
releaseSurfaceBitmaps(canvas, borderCanvas);
|
|
80
91
|
throw new Error('Canvas 2D is unavailable');
|
|
92
|
+
}
|
|
93
|
+
if (borderCanvas)
|
|
94
|
+
resizeSurfaceBitmap(borderCanvas, allocation.width, allocation.height);
|
|
81
95
|
context.setTransform(canvas.width / (width + bleed * 2), 0, 0, canvas.height / (height + bleed * 2), 0, 0);
|
|
82
96
|
if (variant === 'wash') {
|
|
83
|
-
if (borderCanvas) {
|
|
84
|
-
borderCanvas.width = canvas.width;
|
|
85
|
-
borderCanvas.height = canvas.height;
|
|
86
|
-
}
|
|
87
97
|
context.translate(bleed, bleed);
|
|
88
98
|
drawPencilWash(context, { x: 0, y: 0, width, height }, theme.wash, seed, {
|
|
89
99
|
strength: theme.washSettings?.strength ?? pencilRenderDefaults.washStrength / 0.48,
|
|
@@ -93,40 +103,28 @@ export function drawPencilSurface(canvas, borderCanvas, width, height, variant,
|
|
|
93
103
|
inset: 2,
|
|
94
104
|
radius: 8,
|
|
95
105
|
edgeReach: 8,
|
|
96
|
-
dpr,
|
|
106
|
+
dpr: renderDpr,
|
|
97
107
|
});
|
|
98
108
|
return undefined;
|
|
99
109
|
}
|
|
100
110
|
if (variant === 'selection') {
|
|
101
|
-
if (borderCanvas) {
|
|
102
|
-
borderCanvas.width = canvas.width;
|
|
103
|
-
borderCanvas.height = canvas.height;
|
|
104
|
-
}
|
|
105
111
|
drawPencilWash(context, { x: 0, y: 0, width, height }, theme.wash, seed, {
|
|
106
112
|
strength: (theme.dark ? PENCIL_SELECTION_WASH.darkStrength : PENCIL_SELECTION_WASH.lightStrength) * (theme.washSettings?.strength ?? pencilRenderDefaults.washStrength / 0.48),
|
|
107
113
|
spacing: theme.washSettings?.spacing ?? PENCIL_SELECTION_WASH.spacing,
|
|
108
114
|
roughness: theme.washSettings?.roughness ?? PENCIL_SELECTION_WASH.roughness,
|
|
109
115
|
inset: PENCIL_SELECTION_WASH.inset,
|
|
110
|
-
radius: PENCIL_SELECTION_WASH.radius,
|
|
116
|
+
radius: radius !== undefined && Number.isFinite(radius) ? Math.max(0, Math.min(radius, 8)) : PENCIL_SELECTION_WASH.radius,
|
|
111
117
|
edgeReach: PENCIL_SELECTION_WASH.edgeReach,
|
|
112
118
|
angle: theme.washSettings?.angle ?? PENCIL_SELECTION_WASH.angle,
|
|
113
|
-
dpr,
|
|
119
|
+
dpr: renderDpr,
|
|
114
120
|
});
|
|
115
121
|
return undefined;
|
|
116
122
|
}
|
|
117
123
|
if (variant === 'ghost') {
|
|
118
|
-
if (borderCanvas) {
|
|
119
|
-
borderCanvas.width = canvas.width;
|
|
120
|
-
borderCanvas.height = canvas.height;
|
|
121
|
-
}
|
|
122
124
|
return undefined;
|
|
123
125
|
}
|
|
124
126
|
if (variant === 'full') {
|
|
125
|
-
|
|
126
|
-
borderCanvas.width = canvas.width;
|
|
127
|
-
borderCanvas.height = canvas.height;
|
|
128
|
-
}
|
|
129
|
-
context.fillStyle = paperPattern(context, theme.paper, theme.dark, dpr);
|
|
127
|
+
context.fillStyle = paperPattern(context, theme.paper, theme.dark, renderDpr);
|
|
130
128
|
context.fillRect(0, 0, width, height);
|
|
131
129
|
return undefined;
|
|
132
130
|
}
|
|
@@ -144,7 +142,7 @@ export function drawPencilSurface(canvas, borderCanvas, width, height, variant,
|
|
|
144
142
|
context.lineTo(...point);
|
|
145
143
|
context.closePath();
|
|
146
144
|
context.clip();
|
|
147
|
-
context.fillStyle = paperPattern(context, theme.paper, theme.dark,
|
|
145
|
+
context.fillStyle = paperPattern(context, theme.paper, theme.dark, renderDpr);
|
|
148
146
|
context.fillRect(0, 0, width, height);
|
|
149
147
|
if (strength) {
|
|
150
148
|
drawPencilWash(context, { x: 0, y: 0, width, height }, theme.wash, seed, {
|
|
@@ -152,15 +150,13 @@ export function drawPencilSurface(canvas, borderCanvas, width, height, variant,
|
|
|
152
150
|
...theme.washSettings,
|
|
153
151
|
strength: strength * (theme.washSettings?.strength ?? pencilRenderDefaults.washStrength / 0.48),
|
|
154
152
|
radius: resolvedRadius,
|
|
155
|
-
dpr,
|
|
153
|
+
dpr: renderDpr,
|
|
156
154
|
});
|
|
157
155
|
}
|
|
158
156
|
context.restore();
|
|
159
157
|
}
|
|
160
158
|
let borderContext = context;
|
|
161
159
|
if (borderCanvas) {
|
|
162
|
-
borderCanvas.width = canvas.width;
|
|
163
|
-
borderCanvas.height = canvas.height;
|
|
164
160
|
const separateContext = borderCanvas.getContext('2d');
|
|
165
161
|
if (separateContext) {
|
|
166
162
|
separateContext.setTransform(canvas.width / width, 0, 0, canvas.height / height, 0, 0);
|
|
@@ -176,3 +172,66 @@ export function drawPencilSurface(canvas, borderCanvas, width, height, variant,
|
|
|
176
172
|
});
|
|
177
173
|
return points;
|
|
178
174
|
}
|
|
175
|
+
const surfaceCache = new BoundedCache(64, 16 * 1024 * 1024);
|
|
176
|
+
// Admit only a repeat within the recent working set. Cold mounts and long scans
|
|
177
|
+
// must not pay for snapshots that will be evicted before they are ever reused.
|
|
178
|
+
const surfaceRequests = new BoundedCache(64, 64);
|
|
179
|
+
function copyCanvas(source, target) {
|
|
180
|
+
resizeSurfaceBitmap(target, source.width, source.height);
|
|
181
|
+
const context = target.getContext('2d');
|
|
182
|
+
if (!context || context.isContextLost?.())
|
|
183
|
+
throw new Error('Canvas 2D is unavailable');
|
|
184
|
+
context.drawImage(source, 0, 0);
|
|
185
|
+
}
|
|
186
|
+
export function drawPencilSurface(canvas, borderCanvas, width, height, variant, seed, dpr, theme, radius) {
|
|
187
|
+
// Transparent surfaces have no pixels to allocate or cache.
|
|
188
|
+
if (variant === 'ghost') {
|
|
189
|
+
canvas.width = canvas.height = 0;
|
|
190
|
+
if (borderCanvas)
|
|
191
|
+
borderCanvas.width = borderCanvas.height = 0;
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
const bleed = variant === 'wash' ? 12 : 0;
|
|
195
|
+
const allocation = allocateSurface(canvas, borderCanvas, width, height, dpr, bleed);
|
|
196
|
+
const key = JSON.stringify([
|
|
197
|
+
PENCIL_RENDERER_VERSION, variant, width, height, seed, allocation.dpr, Boolean(borderCanvas), radius,
|
|
198
|
+
theme.paper, theme.dark, theme.ink, theme.wash, theme.strokeWidth, theme.strokeRoughness,
|
|
199
|
+
theme.washSettings?.strength, theme.washSettings?.spacing,
|
|
200
|
+
theme.washSettings?.roughness, theme.washSettings?.angle,
|
|
201
|
+
]);
|
|
202
|
+
const cached = surfaceCache.get(key);
|
|
203
|
+
if (cached?.valid && canvasIsUsable(cached.ink) && (!cached.border || canvasIsUsable(cached.border))) {
|
|
204
|
+
copyCanvas(cached.ink, canvas);
|
|
205
|
+
if (cached.border && borderCanvas)
|
|
206
|
+
copyCanvas(cached.border, borderCanvas);
|
|
207
|
+
return cached.points?.map(([x, y]) => [x, y]);
|
|
208
|
+
}
|
|
209
|
+
if (cached)
|
|
210
|
+
surfaceCache.delete(key);
|
|
211
|
+
const points = paintPencilSurface(canvas, borderCanvas, width, height, variant, seed, allocation, theme, radius);
|
|
212
|
+
const cost = canvasByteCost(canvas) + (borderCanvas ? canvasByteCost(borderCanvas) : 0);
|
|
213
|
+
const repeated = surfaceRequests.get(key) === true;
|
|
214
|
+
surfaceRequests.set(key, true, 1);
|
|
215
|
+
// Keep the cache useful for repeated controls, not a single large panel.
|
|
216
|
+
if (repeated && cost <= 2 * 1024 * 1024) {
|
|
217
|
+
try {
|
|
218
|
+
const ink = document.createElement('canvas');
|
|
219
|
+
const border = borderCanvas ? document.createElement('canvas') : null;
|
|
220
|
+
copyCanvas(canvas, ink);
|
|
221
|
+
if (border && borderCanvas)
|
|
222
|
+
copyCanvas(borderCanvas, border);
|
|
223
|
+
const image = { ink, border, points: points?.map(([x, y]) => [x, y]), valid: true };
|
|
224
|
+
// A restored context can report healthy after losing all of its pixels.
|
|
225
|
+
// Keep that entry invalid even if loss and restoration precede the next hit.
|
|
226
|
+
const invalidate = () => { image.valid = false; };
|
|
227
|
+
ink.addEventListener?.('contextlost', invalidate, { once: true });
|
|
228
|
+
border?.addEventListener?.('contextlost', invalidate, { once: true });
|
|
229
|
+
surfaceCache.set(key, image, cost);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// Caching is optional: a snapshot allocation failure must not discard a
|
|
233
|
+
// successfully painted live surface.
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return points;
|
|
237
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare function surfaceIsNearViewport(root: HTMLElement, margin?: number): boolean;
|
|
2
|
+
/** One observer per document, explicitly released after the final subscription. */
|
|
3
|
+
export declare function observeSurfaceViewport(root: HTMLElement, refresh: () => void): (() => void) | undefined;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Keep visible/prewarm surfaces synchronous. An offscreen surface retains DOM,
|
|
2
|
+
// geometry and any existing bitmap; only its decorative paint is deferred.
|
|
3
|
+
export function surfaceIsNearViewport(root, margin = 240) {
|
|
4
|
+
const view = root.ownerDocument?.defaultView;
|
|
5
|
+
if (!view || !Number.isFinite(view.innerWidth) || !Number.isFinite(view.innerHeight)
|
|
6
|
+
|| typeof root.getBoundingClientRect !== 'function')
|
|
7
|
+
return true;
|
|
8
|
+
const rect = root.getBoundingClientRect();
|
|
9
|
+
if (![rect.left, rect.right, rect.top, rect.bottom].every(Number.isFinite))
|
|
10
|
+
return true;
|
|
11
|
+
return rect.bottom >= -margin && rect.top <= view.innerHeight + margin
|
|
12
|
+
&& rect.right >= -margin && rect.left <= view.innerWidth + margin;
|
|
13
|
+
}
|
|
14
|
+
const documents = new WeakMap();
|
|
15
|
+
/** One observer per document, explicitly released after the final subscription. */
|
|
16
|
+
export function observeSurfaceViewport(root, refresh) {
|
|
17
|
+
if (typeof IntersectionObserver === 'undefined' || !root.ownerDocument)
|
|
18
|
+
return undefined;
|
|
19
|
+
const document = root.ownerDocument;
|
|
20
|
+
let state = documents.get(document);
|
|
21
|
+
if (!state) {
|
|
22
|
+
const callbacks = new Map();
|
|
23
|
+
try {
|
|
24
|
+
const observer = new IntersectionObserver(entries => {
|
|
25
|
+
for (const entry of entries)
|
|
26
|
+
if (entry.isIntersecting)
|
|
27
|
+
callbacks.get(entry.target)?.();
|
|
28
|
+
}, { rootMargin: '240px', threshold: 0 });
|
|
29
|
+
state = { observer, callbacks };
|
|
30
|
+
documents.set(document, state);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const current = state;
|
|
37
|
+
current.callbacks.set(root, refresh);
|
|
38
|
+
try {
|
|
39
|
+
current.observer.observe(root);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
current.callbacks.delete(root);
|
|
43
|
+
if (!current.callbacks.size) {
|
|
44
|
+
current.observer.disconnect();
|
|
45
|
+
documents.delete(document);
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
return () => {
|
|
50
|
+
// Ignore a stale unsubscribe if a newer owner registered the same element.
|
|
51
|
+
if (current.callbacks.get(root) !== refresh)
|
|
52
|
+
return;
|
|
53
|
+
current.callbacks.delete(root);
|
|
54
|
+
current.observer.unobserve(root);
|
|
55
|
+
if (!current.callbacks.size) {
|
|
56
|
+
current.observer.disconnect();
|
|
57
|
+
documents.delete(document);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -19,6 +19,8 @@ export declare const pencilHandwriting = "\"Trip Map Hand\",\"HanziPen SC\",\"Ha
|
|
|
19
19
|
export declare function renderPencilText(text: string, options: {
|
|
20
20
|
size: number;
|
|
21
21
|
family?: string;
|
|
22
|
+
weight?: string;
|
|
23
|
+
style?: string;
|
|
22
24
|
color: string;
|
|
23
25
|
seed: number;
|
|
24
26
|
dpr: number;
|
|
@@ -76,13 +76,13 @@ export function renderPencilText(text, options) {
|
|
|
76
76
|
throw new RangeError('Pencil text exceeds the bounded renderer input');
|
|
77
77
|
}
|
|
78
78
|
const resolution = Math.max(2, Math.min(4, Math.ceil(dpr * 2)));
|
|
79
|
-
const font =
|
|
79
|
+
const font = `${options.style ?? 'normal'} ${options.weight ?? '400'} ${size}px ${family}`;
|
|
80
80
|
const fontReady = document.fonts?.check(font, text) ?? false;
|
|
81
81
|
const key = cache ? JSON.stringify([
|
|
82
82
|
PENCIL_RENDERER_VERSION,
|
|
83
83
|
text,
|
|
84
84
|
size,
|
|
85
|
-
|
|
85
|
+
font,
|
|
86
86
|
color,
|
|
87
87
|
seed,
|
|
88
88
|
dpr,
|
|
@@ -131,7 +131,7 @@ export function renderPencilText(text, options) {
|
|
|
131
131
|
const characterSeed = (options.glyphSeed ^ Math.imul(character.charCodeAt(0), 16777619)
|
|
132
132
|
^ Math.imul(index % 3, 374761393)) >>> 0;
|
|
133
133
|
const glyphKey = JSON.stringify([
|
|
134
|
-
PENCIL_RENDERER_VERSION, character, size,
|
|
134
|
+
PENCIL_RENDERER_VERSION, character, size, font, color, characterSeed,
|
|
135
135
|
resolution, settings, document.fonts?.check(font, character),
|
|
136
136
|
]);
|
|
137
137
|
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
|
-
|
|
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);
|
package/dist/internal/wash.js
CHANGED
|
@@ -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 =
|
|
14
|
-
canvas.height =
|
|
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;
|
|
@@ -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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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();
|
package/dist/layout/index.js
CHANGED
|
@@ -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
|
@@ -56,9 +56,11 @@ export const Log = forwardRef(function Log({ 'aria-label': ariaLabel, className,
|
|
|
56
56
|
}, [items, levels]);
|
|
57
57
|
const availableLevels = useMemo(() => new Set(resolvedLevels.map(level => level.value)), [resolvedLevels]);
|
|
58
58
|
const [uncontrolledLevels, setUncontrolledLevels] = useState(defaultFilteredLevels);
|
|
59
|
-
const
|
|
59
|
+
const requestedLevels = filteredLevels ?? uncontrolledLevels;
|
|
60
|
+
const selectedLevels = useMemo(() => requestedLevels?.filter(level => availableLevels.has(level)), [requestedLevels, availableLevels]);
|
|
61
|
+
const selectedLevelSet = useMemo(() => selectedLevels === undefined ? undefined : new Set(selectedLevels), [selectedLevels]);
|
|
60
62
|
const allLevelsSelected = selectedLevels == null
|
|
61
|
-
|| (selectedLevels.length === resolvedLevels.length && resolvedLevels.every(level =>
|
|
63
|
+
|| (selectedLevels.length === resolvedLevels.length && resolvedLevels.every(level => selectedLevelSet.has(level.value)));
|
|
62
64
|
const [uncontrolledPaused, setUncontrolledPaused] = useState(defaultPaused);
|
|
63
65
|
const resolvedPaused = paused ?? uncontrolledPaused;
|
|
64
66
|
const [nearBottom, setNearBottom] = useState(true);
|
|
@@ -68,16 +70,20 @@ export const Log = forwardRef(function Log({ 'aria-label': ariaLabel, className,
|
|
|
68
70
|
const previousPausedRef = useRef(resolvedPaused);
|
|
69
71
|
const nearBottomRef = useRef(true);
|
|
70
72
|
const threshold = normalizedThreshold(nearBottomThreshold);
|
|
71
|
-
const visibleItems =
|
|
73
|
+
const visibleItems = useMemo(() => selectedLevelSet === undefined
|
|
72
74
|
? items
|
|
73
|
-
: items.filter(item => item.level != null &&
|
|
74
|
-
const itemIds = items.map(item => item.id);
|
|
75
|
-
const visibleIds = visibleItems.map(item => item.id);
|
|
75
|
+
: items.filter(item => item.level != null && selectedLevelSet.has(item.level)), [items, selectedLevelSet]);
|
|
76
|
+
const itemIds = useMemo(() => items.map(item => item.id), [items]);
|
|
77
|
+
const visibleIds = useMemo(() => visibleItems.map(item => item.id), [visibleItems]);
|
|
76
78
|
const scrollToLatest = useCallback((behavior = 'smooth') => {
|
|
77
79
|
const viewport = viewportRef.current;
|
|
78
80
|
if (!viewport)
|
|
79
81
|
return;
|
|
80
|
-
|
|
82
|
+
const resolvedBehavior = behavior === 'smooth'
|
|
83
|
+
&& viewport.ownerDocument.defaultView?.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
84
|
+
? 'auto'
|
|
85
|
+
: behavior;
|
|
86
|
+
viewport.scrollTo({ top: viewport.scrollHeight, behavior: resolvedBehavior });
|
|
81
87
|
nearBottomRef.current = true;
|
|
82
88
|
setNearBottom(true);
|
|
83
89
|
}, []);
|
|
@@ -99,7 +105,7 @@ export const Log = forwardRef(function Log({ 'aria-label': ariaLabel, className,
|
|
|
99
105
|
previousItemIdsRef.current = itemIds;
|
|
100
106
|
previousVisibleIdsRef.current = visibleIds;
|
|
101
107
|
previousPausedRef.current = resolvedPaused;
|
|
102
|
-
}, [itemIds
|
|
108
|
+
}, [itemIds, resolvedPaused, scrollToLatest, visibleIds]);
|
|
103
109
|
function changePaused(nextPaused) {
|
|
104
110
|
if (paused === undefined)
|
|
105
111
|
setUncontrolledPaused(nextPaused);
|
|
@@ -135,25 +141,26 @@ export const Log = forwardRef(function Log({ 'aria-label': ariaLabel, className,
|
|
|
135
141
|
const viewportLabel = viewportProps?.['aria-label'] ?? ariaLabel ?? labels.list;
|
|
136
142
|
const viewportClassName = classes('inkra-log__viewport', viewportProps?.className);
|
|
137
143
|
const viewportTabIndex = viewportProps?.tabIndex ?? 0;
|
|
138
|
-
const levelMap = new Map(resolvedLevels.map(level => [level.value, level]));
|
|
144
|
+
const levelMap = useMemo(() => new Map(resolvedLevels.map(level => [level.value, level])), [resolvedLevels]);
|
|
145
|
+
const showLatest = (!nearBottom || resolvedPaused) && visibleItems.length > 0;
|
|
139
146
|
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 ||
|
|
147
|
+
const selected = allLevelsSelected || selectedLevelSet?.has(level.value) === true;
|
|
141
148
|
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
149
|
const active = selectedLevels ?? resolvedLevels.map(option => option.value);
|
|
143
150
|
changeLevels(selected
|
|
144
151
|
? active.filter(value => value !== level.value)
|
|
145
152
|
: [...active, level.value]);
|
|
146
153
|
}, 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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
154
|
+
})] }) : 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) => {
|
|
155
|
+
const definition = item.level ? levelMap.get(item.level) : undefined;
|
|
156
|
+
const tone = definition?.tone ?? (item.level ? defaultTones[item.level] : undefined) ?? 'neutral';
|
|
157
|
+
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
|
|
158
|
+
? _jsx("time", { dateTime: item.dateTime, children: renderText(item.time) })
|
|
159
|
+
: _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);
|
|
160
|
+
}) }) : _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: () => {
|
|
161
|
+
if (resolvedPaused)
|
|
162
|
+
changePaused(false);
|
|
163
|
+
else
|
|
164
|
+
scrollToLatest();
|
|
165
|
+
}, children: resolvedPaused ? labels.resume : labels.latest })] }) : null] }));
|
|
159
166
|
});
|
|
@@ -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
|
+
}
|