@inkra/react 0.12.0 → 0.14.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.
@@ -0,0 +1,144 @@
1
+ export function journeyAnimationMs(animate, duration, reducedMotion) {
2
+ if (!animate || reducedMotion)
3
+ return 0;
4
+ return Number.isFinite(duration) ? Math.max(0, Math.min(duration, 5000)) : 420;
5
+ }
6
+ export function journeyFocusZoom(overviewZoom, scale) {
7
+ const ratio = Number.isFinite(scale) && scale >= 1 ? Math.min(scale, 64) : 2;
8
+ return overviewZoom + Math.log2(ratio);
9
+ }
10
+ export function resolveJourneyMapRendering(rendering = 'auto') {
11
+ if (rendering === 'legacy')
12
+ return { boundaryViewport: false, terrainViewport: false, reuseCanvas: false, cacheBase: false };
13
+ const options = typeof rendering === 'object' ? rendering : {};
14
+ return {
15
+ boundaryViewport: options.boundaryViewport ?? true,
16
+ terrainViewport: options.terrainViewport ?? true,
17
+ reuseCanvas: options.reuseCanvas ?? true,
18
+ cacheBase: options.cacheBase ?? true,
19
+ };
20
+ }
21
+ const MAX_SIDE = 8192;
22
+ const MAX_LAYER_PIXELS = 4 * 1024 * 1024;
23
+ // One base bitmap per map; together with the visible bitmap, at most 6 Mi pixels.
24
+ const MAX_BASE_PIXELS = 3 * 1024 * 1024;
25
+ /** Non-overlapping newly exposed strips after moving an existing base bitmap. */
26
+ export function journeyPanRegions(width, height, dx, dy) {
27
+ if (Math.abs(dx) >= width || Math.abs(dy) >= height)
28
+ return [[0, 0, width, height]];
29
+ const regions = [];
30
+ if (dx)
31
+ regions.push([dx > 0 ? 0 : width + dx, 0, Math.abs(dx), height]);
32
+ if (dy)
33
+ regions.push([Math.max(0, dx), dy > 0 ? 0 : height + dy, width - Math.abs(dx), Math.abs(dy)]);
34
+ return regions;
35
+ }
36
+ /** Pure and SSR safe: browser DPR is supplied by the caller. */
37
+ export function journeyCanvasSize(width, height, requestedDpr) {
38
+ if (![width, height].every(value => Number.isFinite(value) && value > 0))
39
+ return;
40
+ const requested = Number.isFinite(requestedDpr) ? Math.max(1, Math.min(requestedDpr, 2)) : 1;
41
+ const dpr = Math.min(requested, MAX_SIDE / width, MAX_SIDE / height, Math.sqrt(MAX_LAYER_PIXELS / (width * height)));
42
+ if (dpr < 1)
43
+ return;
44
+ const pixelWidth = Math.round(width * dpr), pixelHeight = Math.round(height * dpr);
45
+ // Round-down adjustment prevents rounding the two sides over the pixel budget.
46
+ const boundedDpr = pixelWidth * pixelHeight > MAX_LAYER_PIXELS
47
+ ? Math.min(dpr, Math.floor(width * dpr) / width, Math.floor(height * dpr) / height) : dpr;
48
+ return { width, height, pixelWidth: Math.round(width * boundedDpr), pixelHeight: Math.round(height * boundedDpr), dpr: boundedDpr };
49
+ }
50
+ function releaseCanvas(canvas) {
51
+ canvas.width = 0;
52
+ canvas.height = 0;
53
+ }
54
+ export function prepareJourneyCanvas(canvas, size, reuse) {
55
+ const changed = canvas.width !== size.pixelWidth || canvas.height !== size.pixelHeight;
56
+ if (changed)
57
+ releaseCanvas(canvas); // Avoid an oversized intermediate width/height swap.
58
+ if (changed || !reuse) {
59
+ canvas.width = size.pixelWidth;
60
+ canvas.height = size.pixelHeight;
61
+ }
62
+ canvas.style.width = `${size.width}px`;
63
+ canvas.style.height = `${size.height}px`;
64
+ const context = canvas.getContext('2d');
65
+ if (!context || context.isContextLost?.())
66
+ return null;
67
+ // Never accumulate DPR transforms when the backing store is reused.
68
+ context.setTransform(1, 0, 0, 1, 0, 0);
69
+ context.globalAlpha = 1;
70
+ context.globalCompositeOperation = 'source-over';
71
+ context.clearRect(0, 0, size.pixelWidth, size.pixelHeight);
72
+ context.setTransform(size.dpr, 0, 0, size.dpr, 0, 0);
73
+ return context;
74
+ }
75
+ /** Per-map, memory-only, single-entry cache. No DOM or allocation until capture. */
76
+ export class JourneyBaseBitmap {
77
+ constructor() {
78
+ this.invalidate = () => { this.key = undefined; };
79
+ }
80
+ restore(context, key, offset = [0, 0]) {
81
+ if (key !== this.key || !this.canvas)
82
+ return false;
83
+ const cachedContext = this.canvas.getContext('2d');
84
+ if (!cachedContext || cachedContext.isContextLost?.()) {
85
+ this.clear();
86
+ return false;
87
+ }
88
+ try {
89
+ context.save();
90
+ context.setTransform(1, 0, 0, 1, 0, 0);
91
+ context.drawImage(this.canvas, offset[0], offset[1]);
92
+ return true;
93
+ }
94
+ catch {
95
+ this.clear();
96
+ return false;
97
+ }
98
+ finally {
99
+ context.restore();
100
+ }
101
+ }
102
+ capture(source, size, key) {
103
+ if (size.pixelWidth * size.pixelHeight > MAX_BASE_PIXELS) {
104
+ this.clear();
105
+ return;
106
+ }
107
+ try {
108
+ const canvas = this.canvas ?? document.createElement('canvas');
109
+ if (!this.canvas) {
110
+ canvas.addEventListener('contextlost', this.invalidate);
111
+ canvas.addEventListener('contextrestored', this.invalidate);
112
+ this.canvas = canvas;
113
+ }
114
+ this.key = undefined;
115
+ if (canvas.width !== size.pixelWidth || canvas.height !== size.pixelHeight) {
116
+ releaseCanvas(canvas);
117
+ canvas.width = size.pixelWidth;
118
+ canvas.height = size.pixelHeight;
119
+ }
120
+ const context = canvas.getContext('2d');
121
+ if (!context || context.isContextLost?.()) {
122
+ this.clear();
123
+ return;
124
+ }
125
+ context.setTransform(1, 0, 0, 1, 0, 0);
126
+ context.clearRect(0, 0, size.pixelWidth, size.pixelHeight);
127
+ context.drawImage(source, 0, 0);
128
+ this.key = key;
129
+ }
130
+ catch {
131
+ // Allocation/copy failures affect optimization, never the already drawn map.
132
+ this.clear();
133
+ }
134
+ }
135
+ clear() {
136
+ this.key = undefined;
137
+ if (!this.canvas)
138
+ return;
139
+ this.canvas.removeEventListener('contextlost', this.invalidate);
140
+ this.canvas.removeEventListener('contextrestored', this.invalidate);
141
+ releaseCanvas(this.canvas);
142
+ this.canvas = undefined;
143
+ }
144
+ }
@@ -3,6 +3,9 @@ import { type PencilPoint } from '../internal/stroke.js';
3
3
  type Project = (position: Position) => PencilPoint;
4
4
  export declare function paintSea(ctx: CanvasRenderingContext2D, width: number, height: number, anchor: PencilPoint): void;
5
5
  export declare function mapFeatures(data: GeoJsonObject): Feature[];
6
- export declare function paintTerrain(ctx: CanvasRenderingContext2D, data: GeoJsonObject, project: Project, width: number, height: number, dark: boolean, defaultKind?: string): void;
6
+ /** Conservative x bounds of polygon edges inside a horizontal strip.
7
+ * Hole rings may widen the bounds, but never remove visible fill. */
8
+ export declare function terrainRowBounds(rings: readonly (readonly PencilPoint[])[], top: number, bottom: number): readonly [number, number] | undefined;
9
+ export declare function paintTerrain(ctx: CanvasRenderingContext2D, data: GeoJsonObject, project: Project, width: number, height: number, dark: boolean, defaultKind?: string, terrainViewport?: boolean): void;
7
10
  export declare function paintStop(ctx: CanvasRenderingContext2D, x: number, y: number, seed: number, color: string, paper: string, selected: boolean): void;
8
11
  export {};
@@ -45,14 +45,38 @@ export function mapFeatures(data) {
45
45
  return [data];
46
46
  return [{ type: 'Feature', properties: {}, geometry: data }];
47
47
  }
48
- export function paintTerrain(ctx, data, project, width, height, dark, defaultKind = 'land') {
48
+ /** Conservative x bounds of polygon edges inside a horizontal strip.
49
+ * Hole rings may widen the bounds, but never remove visible fill. */
50
+ export function terrainRowBounds(rings, top, bottom) {
51
+ let left = Infinity, right = -Infinity;
52
+ for (const ring of rings)
53
+ for (let i = 0; i < ring.length; i++) {
54
+ const [ax, ay] = ring[i], [bx, by] = ring[(i + 1) % ring.length];
55
+ if ((ay < top && by < top) || (ay > bottom && by > bottom))
56
+ continue;
57
+ if (!Number.isFinite(ax) || !Number.isFinite(ay) || !Number.isFinite(bx) || !Number.isFinite(by))
58
+ return [-Infinity, Infinity];
59
+ if (ay === by) {
60
+ left = Math.min(left, ax, bx);
61
+ right = Math.max(right, ax, bx);
62
+ continue;
63
+ }
64
+ const low = Math.max(0, Math.min(1, (top - ay) / (by - ay)));
65
+ const high = Math.max(0, Math.min(1, (bottom - ay) / (by - ay)));
66
+ const x1 = ax + (bx - ax) * low, x2 = ax + (bx - ax) * high;
67
+ left = Math.min(left, x1, x2);
68
+ right = Math.max(right, x1, x2);
69
+ }
70
+ return left <= right ? [left, right] : undefined;
71
+ }
72
+ export function paintTerrain(ctx, data, project, width, height, dark, defaultKind = 'land', terrainViewport = false) {
49
73
  for (const [featureIndex, feature] of mapFeatures(data).entries()) {
50
74
  const geometry = feature.geometry;
51
75
  if (!geometry)
52
76
  continue;
53
77
  const polygons = geometry.type === 'MultiPolygon' ? geometry.coordinates : geometry.type === 'Polygon' ? [geometry.coordinates] : [];
54
78
  const kind = String(feature.properties?.id ?? defaultKind);
55
- const [fill, ink] = palette[kind] ?? palette.land;
79
+ const [fill, ink] = palette[kind] ?? palette[defaultKind] ?? palette.land;
56
80
  for (const [polygonIndex, polygon] of polygons.entries()) {
57
81
  const rings = polygon.map(ring => ring.map(project));
58
82
  let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
@@ -87,7 +111,15 @@ export function paintTerrain(ctx, data, project, width, height, dark, defaultKin
87
111
  const spacing = kind === 'forest' ? 9 : 13;
88
112
  const seed = 21000 + featureIndex * 43 + polygonIndex;
89
113
  for (let row = Math.floor((top - origin[1]) / spacing); row <= Math.ceil((bottom - origin[1]) / spacing); row++) {
90
- for (let col = Math.floor((left - origin[0]) / 36); col <= Math.ceil((right - origin[0]) / 36); col++) {
114
+ // Jitter, the 44px hatch, category glyphs and pencil bleed all fit this
115
+ // envelope. Keep the original world-grid indices and per-cell seeds.
116
+ const rowY = origin[1] + row * spacing;
117
+ const band = terrainViewport ? terrainRowBounds(rings, rowY - 25, rowY + 20) : [left, right];
118
+ if (!band)
119
+ continue;
120
+ const first = Math.max(Math.floor((left - origin[0]) / 36), terrainViewport ? Math.ceil((band[0] - origin[0] - 68) / 36) : -Infinity);
121
+ const last = Math.min(Math.ceil((right - origin[0]) / 36), terrainViewport ? Math.floor((band[1] - origin[0] + 17) / 36) : Infinity);
122
+ for (let col = first; col <= last; col++) {
91
123
  const cell = (seed ^ Math.imul(col, 73856093) ^ Math.imul(row, 19349663)) >>> 0;
92
124
  const rng = seededRandom(cell);
93
125
  if (rng() < .2)
@@ -0,0 +1,3 @@
1
+ import type { Map } from 'leaflet';
2
+ /** Trackpad pinch is independent of ordinary wheel scrolling and native touch zoom. */
3
+ export declare function attachJourneyTrackpad(host: HTMLElement, map: Map): () => void;
@@ -0,0 +1,110 @@
1
+ /** Trackpad pinch is independent of ordinary wheel scrolling and native touch zoom. */
2
+ export function attachJourneyTrackpad(host, map) {
3
+ let frame = 0;
4
+ let pending;
5
+ let gesture;
6
+ let touching = false;
7
+ let remainder = 0;
8
+ let lastWheel = -Infinity;
9
+ const clamp = (zoom) => Math.max(map.getMinZoom(), Math.min(map.getMaxZoom(), zoom));
10
+ const apply = () => {
11
+ frame = 0;
12
+ if (!pending)
13
+ return;
14
+ const { point, zoom } = pending;
15
+ pending = undefined;
16
+ const snap = map.options.zoomSnap ?? 0;
17
+ // Snap before calculating the anchor; Leaflet otherwise snaps only the
18
+ // final view and the coordinate under the pointer can drift.
19
+ const target = snap > 0 ? clamp(Math.round(zoom / snap) * snap) : zoom;
20
+ if (target !== map.getZoom())
21
+ map.setZoomAround(point, target, { animate: false });
22
+ // Preserve sub-snap movement across a wheel burst instead of losing tiny deltas.
23
+ remainder = zoom - map.getZoom();
24
+ };
25
+ const queue = (zoom, point) => {
26
+ pending = { zoom: clamp(zoom), point };
27
+ if (!frame)
28
+ frame = requestAnimationFrame(apply);
29
+ };
30
+ const pointAt = (event) => {
31
+ const size = map.getSize();
32
+ if (Number.isFinite(event.clientX) && Number.isFinite(event.clientY)) {
33
+ const point = map.mouseEventToContainerPoint(event);
34
+ if (point.x >= 0 && point.y >= 0 && point.x <= size.x && point.y <= size.y)
35
+ return point;
36
+ }
37
+ return size.divideBy(2);
38
+ };
39
+ const stop = (event) => { event.preventDefault(); event.stopImmediatePropagation(); };
40
+ const wheel = (event) => {
41
+ if (!event.ctrlKey)
42
+ return; // Plain two-finger scrolling continues to the page.
43
+ if (!Number.isFinite(event.deltaY))
44
+ return;
45
+ stop(event);
46
+ if (gesture || touching || event.deltaY === 0)
47
+ return;
48
+ const now = performance.now();
49
+ if (now - lastWheel > 160)
50
+ remainder = 0;
51
+ lastWheel = now;
52
+ const unit = event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? host.clientHeight : 1;
53
+ const pixels = Math.max(-100, Math.min(100, event.deltaY * unit));
54
+ queue((pending?.zoom ?? map.getZoom() + remainder) - pixels / (100 * Math.LN2), pointAt(event));
55
+ };
56
+ const start = (event) => {
57
+ if (touching)
58
+ return; // Leaflet owns physical touchscreen pinch.
59
+ const input = event;
60
+ stop(event);
61
+ map.stop();
62
+ if (frame)
63
+ cancelAnimationFrame(frame);
64
+ frame = 0;
65
+ pending = undefined;
66
+ remainder = 0;
67
+ gesture = { zoom: map.getZoom(), scale: input.scale && input.scale > 0 && Number.isFinite(input.scale) ? input.scale : 1, point: pointAt(input) };
68
+ };
69
+ const change = (event) => {
70
+ if (!gesture || touching)
71
+ return;
72
+ stop(event);
73
+ const scale = event.scale;
74
+ if (scale && scale > 0 && Number.isFinite(scale))
75
+ queue(gesture.zoom + Math.log2(scale / gesture.scale), gesture.point);
76
+ };
77
+ const end = (event) => {
78
+ change(event);
79
+ gesture = undefined;
80
+ };
81
+ const touch = (event) => {
82
+ touching = event.touches.length > 0;
83
+ if (touching && gesture) {
84
+ gesture = undefined;
85
+ pending = undefined;
86
+ if (frame)
87
+ cancelAnimationFrame(frame);
88
+ frame = 0;
89
+ }
90
+ };
91
+ host.addEventListener('wheel', wheel, { capture: true, passive: false });
92
+ host.addEventListener('gesturestart', start, { passive: false });
93
+ host.addEventListener('gesturechange', change, { passive: false });
94
+ host.addEventListener('gestureend', end, { passive: false });
95
+ for (const type of ['touchstart', 'touchend', 'touchcancel'])
96
+ host.addEventListener(type, touch, { capture: true, passive: true });
97
+ const cleanup = () => {
98
+ map.off('unload', cleanup);
99
+ if (frame)
100
+ cancelAnimationFrame(frame);
101
+ host.removeEventListener('wheel', wheel, true);
102
+ host.removeEventListener('gesturestart', start);
103
+ host.removeEventListener('gesturechange', change);
104
+ host.removeEventListener('gestureend', end);
105
+ for (const type of ['touchstart', 'touchend', 'touchcancel'])
106
+ host.removeEventListener(type, touch, true);
107
+ };
108
+ map.on('unload', cleanup);
109
+ return cleanup;
110
+ }
@@ -1,9 +1,11 @@
1
1
  .inkra-journey-map { min-width: 0; width: 100%; color: var(--inkra-ink); }
2
2
  .inkra-journey-map__viewport { position: relative; overflow: hidden; isolation: isolate; background: var(--inkra-paper); }
3
3
  .inkra-journey-map__host { position: absolute; inset: 0; background: transparent; font: inherit; }
4
- .inkra-journey-map__ink { position: absolute; inset: 0; pointer-events: none; z-index: 350; }
4
+ .inkra-journey-map__ink { position: absolute; top: 0; left: 0; pointer-events: none; z-index: 350; }
5
5
  .inkra-journey-map__tools { position: absolute; z-index: 600; inset-block-end: 9px; inset-inline-start: 9px; display: grid; gap: 4px; }
6
6
  .inkra-journey-map__marker { display: grid; place-items: center; }
7
+ .inkra-journey-map__symbol, .inkra-journey-map__stop { display: block; width: 44px; height: 44px; }
8
+ .inkra-journey-map__stop { pointer-events: none; }
7
9
  .inkra-journey-map__pin { position: absolute; top: 37px; left: 50%; transform: translateX(-50%); width: max-content; max-width: 140px; text-align: center; color: var(--inkra-ink); font: 14px/1.4 var(--inkra-font-hand); }
8
10
  .inkra-journey-map__marker[aria-pressed="true"] .inkra-journey-map__pin { color: var(--inkra-accent-strong); }
9
11
  .inkra-journey-map__orientation { position: absolute; bottom: 18px; right: 18px; z-index: 600; display: flex; align-items: end; gap: 12px; pointer-events: none; }
@@ -17,3 +19,14 @@
17
19
  .inkra-journey-map__places button { display: flex; align-items: center; gap: 8px; min-height: 44px; padding: 8px; border: 0; background: transparent; color: inherit; font: inherit; cursor: pointer; }
18
20
  .inkra-journey-map__places button[aria-pressed="true"] { text-decoration: underline; text-underline-offset: 4px; }
19
21
  .inkra-journey-map button:focus-visible, .inkra-journey-map__marker:focus-visible { outline: 2px solid var(--inkra-focus); outline-offset: -2px; }
22
+
23
+ .inkra-journey-map__portal-owner { display: contents; }
24
+ .inkra-journey-map__custom-icon { display: grid; place-items: center; width: 44px; height: 44px; color: var(--inkra-accent-strong); }
25
+ .inkra-journey-map__custom-icon[hidden], .inkra-journey-map__stop[hidden] { display: none; }
26
+ .inkra-journey-map__caption { position: absolute; transform: translate(-50%, -50%); width: max-content; max-width: 150px; pointer-events: none; }
27
+ .inkra-journey-map__feature-label { padding: 3px 8px; font-size: 12px; line-height: 1.5; text-align: center; color: var(--inkra-ink); }
28
+ .inkra-journey-map__feature-label[data-selected="true"] { color: var(--inkra-accent-strong); }
29
+ .inkra-journey-map__features { display: flex; flex-wrap: wrap; list-style: none; gap: 8px; padding: 0; margin: 8px 0 0; }
30
+ .inkra-journey-map__features button { min-height: 44px; padding: 8px; border: 0; background: none; color: inherit; font: inherit; cursor: pointer; }
31
+ .inkra-journey-map__features [aria-pressed="true"] { text-decoration: underline; text-underline-offset: 4px; }
32
+ .inkra-journey-map__feature-hit:focus-visible { outline: 2px solid var(--inkra-focus); outline-offset: 2px; }
@@ -1,4 +1,5 @@
1
1
  .inkra-markdown {
2
+ inline-size: 100%;
2
3
  color: var(--inkra-ink);
3
4
  font-family: var(--inkra-font-body);
4
5
  font-size: var(--inkra-text-md);
@@ -203,6 +204,7 @@
203
204
  .inkra-markdown__code-text > .inkra-pencil-text__source { font-family: inherit; }
204
205
 
205
206
  .inkra-markdown__code-frame {
207
+ inline-size: 100%;
206
208
  margin-block: 0 var(--inkra-space-4);
207
209
  max-width: 100%;
208
210
  min-width: 0;
package/dist/styles.css CHANGED
@@ -8781,6 +8781,7 @@ div.inkra-choice-grid > .inkra-radio-group {
8781
8781
  }
8782
8782
 
8783
8783
  .inkra-markdown {
8784
+ inline-size: 100%;
8784
8785
  color: var(--inkra-ink);
8785
8786
  font-family: var(--inkra-font-body);
8786
8787
  font-size: var(--inkra-text-md);
@@ -8985,6 +8986,7 @@ div.inkra-choice-grid > .inkra-radio-group {
8985
8986
  .inkra-markdown__code-text > .inkra-pencil-text__source { font-family: inherit; }
8986
8987
 
8987
8988
  .inkra-markdown__code-frame {
8989
+ inline-size: 100%;
8988
8990
  margin-block: 0 var(--inkra-space-4);
8989
8991
  max-width: 100%;
8990
8992
  min-width: 0;
@@ -10018,9 +10020,11 @@ svg.leaflet-image-layer.leaflet-interactive path {
10018
10020
  .inkra-journey-map { min-width: 0; width: 100%; color: var(--inkra-ink); }
10019
10021
  .inkra-journey-map__viewport { position: relative; overflow: hidden; isolation: isolate; background: var(--inkra-paper); }
10020
10022
  .inkra-journey-map__host { position: absolute; inset: 0; background: transparent; font: inherit; }
10021
- .inkra-journey-map__ink { position: absolute; inset: 0; pointer-events: none; z-index: 350; }
10023
+ .inkra-journey-map__ink { position: absolute; top: 0; left: 0; pointer-events: none; z-index: 350; }
10022
10024
  .inkra-journey-map__tools { position: absolute; z-index: 600; inset-block-end: 9px; inset-inline-start: 9px; display: grid; gap: 4px; }
10023
10025
  .inkra-journey-map__marker { display: grid; place-items: center; }
10026
+ .inkra-journey-map__symbol, .inkra-journey-map__stop { display: block; width: 44px; height: 44px; }
10027
+ .inkra-journey-map__stop { pointer-events: none; }
10024
10028
  .inkra-journey-map__pin { position: absolute; top: 37px; left: 50%; transform: translateX(-50%); width: max-content; max-width: 140px; text-align: center; color: var(--inkra-ink); font: 14px/1.4 var(--inkra-font-hand); }
10025
10029
  .inkra-journey-map__marker[aria-pressed="true"] .inkra-journey-map__pin { color: var(--inkra-accent-strong); }
10026
10030
  .inkra-journey-map__orientation { position: absolute; bottom: 18px; right: 18px; z-index: 600; display: flex; align-items: end; gap: 12px; pointer-events: none; }
@@ -10035,6 +10039,17 @@ svg.leaflet-image-layer.leaflet-interactive path {
10035
10039
  .inkra-journey-map__places button[aria-pressed="true"] { text-decoration: underline; text-underline-offset: 4px; }
10036
10040
  .inkra-journey-map button:focus-visible, .inkra-journey-map__marker:focus-visible { outline: 2px solid var(--inkra-focus); outline-offset: -2px; }
10037
10041
 
10042
+ .inkra-journey-map__portal-owner { display: contents; }
10043
+ .inkra-journey-map__custom-icon { display: grid; place-items: center; width: 44px; height: 44px; color: var(--inkra-accent-strong); }
10044
+ .inkra-journey-map__custom-icon[hidden], .inkra-journey-map__stop[hidden] { display: none; }
10045
+ .inkra-journey-map__caption { position: absolute; transform: translate(-50%, -50%); width: max-content; max-width: 150px; pointer-events: none; }
10046
+ .inkra-journey-map__feature-label { padding: 3px 8px; font-size: 12px; line-height: 1.5; text-align: center; color: var(--inkra-ink); }
10047
+ .inkra-journey-map__feature-label[data-selected="true"] { color: var(--inkra-accent-strong); }
10048
+ .inkra-journey-map__features { display: flex; flex-wrap: wrap; list-style: none; gap: 8px; padding: 0; margin: 8px 0 0; }
10049
+ .inkra-journey-map__features button { min-height: 44px; padding: 8px; border: 0; background: none; color: inherit; font: inherit; cursor: pointer; }
10050
+ .inkra-journey-map__features [aria-pressed="true"] { text-decoration: underline; text-underline-offset: 4px; }
10051
+ .inkra-journey-map__feature-hit:focus-visible { outline: 2px solid var(--inkra-focus); outline-offset: 2px; }
10052
+
10038
10053
  .inkra-toast__theme-anchor { display: none; }
10039
10054
 
10040
10055
  .inkra-toast__viewport {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inkra/react",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "Hand-drawn React controls with native semantics and deterministic decoration.",
5
5
  "type": "module",
6
6
  "license": "MIT",