@inkra/react 0.1.0 → 0.2.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 (160) hide show
  1. package/README.md +104 -1
  2. package/dist/PencilIcon.js +14 -16
  3. package/dist/PencilLine.js +18 -12
  4. package/dist/PencilText.js +192 -59
  5. package/dist/alert-dialog/index.d.ts +64 -0
  6. package/dist/alert-dialog/index.js +109 -0
  7. package/dist/audio-player/index.d.ts +60 -0
  8. package/dist/audio-player/index.js +101 -0
  9. package/dist/carousel/index.d.ts +51 -0
  10. package/dist/carousel/index.js +290 -0
  11. package/dist/choice-grid/index.d.ts +38 -0
  12. package/dist/choice-grid/index.js +85 -0
  13. package/dist/color-picker/color.d.ts +15 -0
  14. package/dist/color-picker/color.js +55 -0
  15. package/dist/color-picker/index.d.ts +31 -0
  16. package/dist/color-picker/index.js +69 -0
  17. package/dist/command/index.d.ts +23 -0
  18. package/dist/command/index.js +39 -0
  19. package/dist/context-menu/index.d.ts +76 -0
  20. package/dist/context-menu/index.js +240 -0
  21. package/dist/data-table/index.d.ts +47 -0
  22. package/dist/data-table/index.js +91 -0
  23. package/dist/date-picker/index.d.ts +50 -0
  24. package/dist/date-picker/index.js +180 -0
  25. package/dist/display/ProgressRing.d.ts +6 -0
  26. package/dist/display/ProgressRing.js +146 -0
  27. package/dist/display/index.d.ts +27 -0
  28. package/dist/display/index.js +18 -2
  29. package/dist/dockable-panel/index.d.ts +41 -0
  30. package/dist/dockable-panel/index.js +253 -0
  31. package/dist/dockable-panel/model.d.ts +21 -0
  32. package/dist/dockable-panel/model.js +61 -0
  33. package/dist/drawer/index.d.ts +63 -0
  34. package/dist/drawer/index.js +86 -0
  35. package/dist/editable/index.d.ts +53 -0
  36. package/dist/editable/index.js +162 -0
  37. package/dist/empty/index.d.ts +21 -0
  38. package/dist/empty/index.js +23 -0
  39. package/dist/flip/index.d.ts +49 -0
  40. package/dist/flip/index.js +138 -0
  41. package/dist/form/EditablePencilText.js +157 -146
  42. package/dist/form/Textarea.d.ts +12 -0
  43. package/dist/form/Textarea.js +83 -4
  44. package/dist/form/index.d.ts +1 -1
  45. package/dist/heatmap/index.d.ts +59 -0
  46. package/dist/heatmap/index.js +248 -0
  47. package/dist/hotkey/index.d.ts +37 -0
  48. package/dist/hotkey/index.js +130 -0
  49. package/dist/hotkey/model.d.ts +27 -0
  50. package/dist/hotkey/model.js +97 -0
  51. package/dist/hover-card/index.d.ts +33 -0
  52. package/dist/hover-card/index.js +141 -0
  53. package/dist/image-cropper/index.d.ts +82 -0
  54. package/dist/image-cropper/index.js +261 -0
  55. package/dist/image-cropper/model.d.ts +42 -0
  56. package/dist/image-cropper/model.js +94 -0
  57. package/dist/image-viewer/index.d.ts +79 -0
  58. package/dist/image-viewer/index.js +220 -0
  59. package/dist/image-viewer/model.d.ts +9 -0
  60. package/dist/image-viewer/model.js +18 -0
  61. package/dist/index.d.ts +47 -0
  62. package/dist/index.js +47 -0
  63. package/dist/internal/cache.d.ts +1 -1
  64. package/dist/internal/cache.js +1 -1
  65. package/dist/internal/renderTokens.d.ts +10 -0
  66. package/dist/internal/renderTokens.js +13 -0
  67. package/dist/internal/surfaceRenderer.d.ts +14 -6
  68. package/dist/internal/surfaceRenderer.js +15 -12
  69. package/dist/internal/textRenderer.d.ts +1 -0
  70. package/dist/internal/textRenderer.js +33 -7
  71. package/dist/internal/usePencilSurface.js +16 -8
  72. package/dist/internal/useSelectionDrawing.js +15 -11
  73. package/dist/journey-map/MapScale.js +3 -1
  74. package/dist/journey-map/index.js +3 -1
  75. package/dist/keypad/index.d.ts +23 -0
  76. package/dist/keypad/index.js +63 -0
  77. package/dist/keypad/model.d.ts +20 -0
  78. package/dist/keypad/model.js +33 -0
  79. package/dist/layout/index.d.ts +63 -0
  80. package/dist/layout/index.js +62 -0
  81. package/dist/log/index.d.ts +45 -0
  82. package/dist/log/index.js +159 -0
  83. package/dist/log/model.d.ts +16 -0
  84. package/dist/log/model.js +13 -0
  85. package/dist/node-graph/index.d.ts +123 -0
  86. package/dist/node-graph/index.js +304 -0
  87. package/dist/number-ticker/index.d.ts +29 -0
  88. package/dist/number-ticker/index.js +132 -0
  89. package/dist/number-ticker/model.d.ts +8 -0
  90. package/dist/number-ticker/model.js +34 -0
  91. package/dist/overflow-list/index.d.ts +39 -0
  92. package/dist/overflow-list/index.js +230 -0
  93. package/dist/overflow-list/layout.d.ts +19 -0
  94. package/dist/overflow-list/layout.js +44 -0
  95. package/dist/pagination/index.d.ts +23 -0
  96. package/dist/pagination/index.js +41 -0
  97. package/dist/pagination/model.d.ts +4 -0
  98. package/dist/pagination/model.js +42 -0
  99. package/dist/pan-zoom/index.d.ts +71 -0
  100. package/dist/pan-zoom/index.js +175 -0
  101. package/dist/radar/index.d.ts +54 -0
  102. package/dist/radar/index.js +220 -0
  103. package/dist/range-slider/index.d.ts +16 -0
  104. package/dist/range-slider/index.js +22 -0
  105. package/dist/rating/index.d.ts +28 -0
  106. package/dist/rating/index.js +127 -0
  107. package/dist/resizable/index.d.ts +32 -0
  108. package/dist/resizable/index.js +50 -0
  109. package/dist/scroll-area/index.d.ts +36 -0
  110. package/dist/scroll-area/index.js +44 -0
  111. package/dist/segmented-progress/SegmentedProgress.d.ts +17 -0
  112. package/dist/segmented-progress/SegmentedProgress.js +42 -0
  113. package/dist/slider/index.js +1 -1
  114. package/dist/slot/index.d.ts +24 -0
  115. package/dist/slot/index.js +29 -0
  116. package/dist/sortable/index.d.ts +50 -0
  117. package/dist/sortable/index.js +278 -0
  118. package/dist/sortable/model.d.ts +30 -0
  119. package/dist/sortable/model.js +64 -0
  120. package/dist/sparkline/index.d.ts +19 -0
  121. package/dist/sparkline/index.js +240 -0
  122. package/dist/sparkline/model.d.ts +16 -0
  123. package/dist/sparkline/model.js +28 -0
  124. package/dist/steps/index.d.ts +16 -0
  125. package/dist/steps/index.js +37 -0
  126. package/dist/styles.css +4850 -287
  127. package/dist/tag-input/index.d.ts +69 -0
  128. package/dist/tag-input/index.js +325 -0
  129. package/dist/tag-input/model.d.ts +14 -0
  130. package/dist/tag-input/model.js +38 -0
  131. package/dist/time-picker/index.d.ts +15 -0
  132. package/dist/time-picker/index.js +168 -0
  133. package/dist/timeline/index.d.ts +14 -0
  134. package/dist/timeline/index.js +16 -0
  135. package/dist/toast/index.d.ts +27 -0
  136. package/dist/toast/index.js +36 -0
  137. package/dist/toggle-group/index.d.ts +17 -0
  138. package/dist/toggle-group/index.js +26 -0
  139. package/dist/tokens.css +10 -9
  140. package/dist/transfer/index.d.ts +64 -0
  141. package/dist/transfer/index.js +174 -0
  142. package/dist/transfer/model.d.ts +12 -0
  143. package/dist/transfer/model.js +33 -0
  144. package/dist/tree/index.d.ts +39 -0
  145. package/dist/tree/index.js +36 -0
  146. package/dist/tree-select/index.d.ts +59 -0
  147. package/dist/tree-select/index.js +142 -0
  148. package/dist/tree-select/model.d.ts +31 -0
  149. package/dist/tree-select/model.js +135 -0
  150. package/dist/typewriter/index.d.ts +21 -0
  151. package/dist/typewriter/index.js +101 -0
  152. package/dist/typewriter/model.d.ts +7 -0
  153. package/dist/typewriter/model.js +45 -0
  154. package/dist/upload/index.d.ts +59 -0
  155. package/dist/upload/index.js +230 -0
  156. package/dist/virtual-list/index.d.ts +44 -0
  157. package/dist/virtual-list/index.js +128 -0
  158. package/dist/virtual-list/model.d.ts +7 -0
  159. package/dist/virtual-list/model.js +27 -0
  160. package/package.json +22 -3
@@ -1,15 +1,16 @@
1
- import { BoundedCache, PENCIL_RENDERER_VERSION, canvasByteCost } from './cache.js';
1
+ import { BoundedCache, PENCIL_RENDERER_VERSION, canvasByteCost, canvasIsUsable } from './cache.js';
2
2
  import { seededRandom } from './random.js';
3
3
  export const pencilLettering = Object.freeze({
4
- variation: 0.52,
5
- breaks: 0.18,
6
- outline: 0.96,
7
- fill: 0.34,
8
- grain: 0.45,
4
+ variation: 0.2,
5
+ breaks: 0.5,
6
+ outline: 0.9,
7
+ fill: 0.9,
8
+ grain: 0.6,
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
12
  const textCache = new BoundedCache(160, 16 * 1024 * 1024);
13
+ const glyphCache = new BoundedCache(512, 8 * 1024 * 1024);
13
14
  const clamp = (value) => Math.max(0, Math.min(1, value));
14
15
  function hash(x, y, seed) {
15
16
  let value = Math.imul(x ^ seed, 374761393) ^ Math.imul(y, 668265263);
@@ -88,10 +89,11 @@ export function renderPencilText(text, options) {
88
89
  resolution,
89
90
  settings,
90
91
  fontReady,
92
+ options.glyphSeed,
91
93
  ]) : '';
92
94
  if (cache) {
93
95
  const cached = textCache.get(key);
94
- if (cached)
96
+ if (cached && canvasIsUsable(cached.ink))
95
97
  return cached;
96
98
  }
97
99
  const mask = document.createElement('canvas');
@@ -120,6 +122,30 @@ export function renderPencilText(text, options) {
120
122
  if (!context)
121
123
  throw new Error('Canvas 2D is unavailable');
122
124
  context.scale(resolution, resolution);
125
+ // Plain Latin labels can reuse bounded character sprites. Complex scripts and
126
+ // private editable text retain the original whole-line rendering path.
127
+ if (cache && options.glyphSeed !== undefined && /^[\x20-\x7e]+$/.test(text)) {
128
+ let left = 0;
129
+ characters.forEach((character, index) => {
130
+ if (character !== ' ') {
131
+ const characterSeed = (options.glyphSeed ^ Math.imul(character.charCodeAt(0), 16777619)
132
+ ^ Math.imul(index % 3, 374761393)) >>> 0;
133
+ const glyphKey = JSON.stringify([
134
+ PENCIL_RENDERER_VERSION, character, size, family, color, characterSeed,
135
+ resolution, settings, document.fonts?.check(font, character),
136
+ ]);
137
+ let glyph = glyphCache.get(glyphKey);
138
+ if (!glyph || !canvasIsUsable(glyph.ink)) {
139
+ glyph = renderPencilText(character, { ...options, seed: characterSeed, glyphSeed: undefined, cache: false });
140
+ glyphCache.set(glyphKey, glyph, canvasByteCost(glyph.ink));
141
+ }
142
+ context.drawImage(glyph.ink, left, 0, glyph.width, glyph.height);
143
+ }
144
+ left += widths[index];
145
+ });
146
+ const sprite = { ink: mask, width, height, advance, ascent, descent, padding };
147
+ return textCache.set(key, sprite, canvasByteCost(mask));
148
+ }
123
149
  context.font = font;
124
150
  context.fontKerning = 'none';
125
151
  context.fillStyle = color;
@@ -3,6 +3,7 @@ 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 { maxStrokeRoughness, pencilRenderDefaults, readRenderToken } from './renderTokens.js';
6
7
  function token(style, name, fallback) {
7
8
  return style.getPropertyValue(name).trim() || fallback;
8
9
  }
@@ -32,6 +33,14 @@ export function usePencilSurface(rootRef, canvasRef, borderRef, variant, seed, c
32
33
  ? token(style, '--inkra-danger', '#873e36')
33
34
  : token(style, '--inkra-border-strong', '#778575');
34
35
  const dark = style.colorScheme.includes('dark');
36
+ const strokeWidth = readRenderToken(style, '--inkra-stroke-width', pencilRenderDefaults.strokeWidth, 0.3, 3);
37
+ const strokeRoughness = readRenderToken(style, '--inkra-stroke-amplitude', pencilRenderDefaults.strokeRoughness, 0, maxStrokeRoughness);
38
+ const washSettings = {
39
+ strength: readRenderToken(style, '--inkra-wash-strength', pencilRenderDefaults.washStrength, 0, 1) / 0.48,
40
+ spacing: readRenderToken(style, '--inkra-wash-spacing', pencilRenderDefaults.washSpacing, 1, 6),
41
+ roughness: readRenderToken(style, '--inkra-wash-roughness', pencilRenderDefaults.washRoughness, 0, 6),
42
+ angle: readRenderToken(style, '--inkra-wash-angle', pencilRenderDefaults.washAngle, -85, 85),
43
+ };
35
44
  const wash = variant === 'selection'
36
45
  ? token(style, '--inkra-selection-pigment', dark ? '#c6b873' : '#769566')
37
46
  : variant === 'danger'
@@ -55,11 +64,12 @@ export function usePencilSurface(rootRef, canvasRef, borderRef, variant, seed, c
55
64
  dark,
56
65
  clipContent,
57
66
  radius,
67
+ strokeWidth, strokeRoughness, washSettings,
58
68
  ]);
59
69
  if (paintKey === lastPaint && !canvas.getContext('2d')?.isContextLost?.())
60
70
  return;
61
71
  try {
62
- const points = drawPencilSurface(canvas, borderRef.current, width, height, variant, numericSeed, dpr, { paper, dark, ink, wash }, radius);
72
+ const points = drawPencilSurface(canvas, borderRef.current, width, height, variant, numericSeed, dpr, { paper, dark, ink, wash, strokeWidth, strokeRoughness, washSettings }, radius);
63
73
  if (clipContent && points) {
64
74
  root.style.setProperty('--inkra-pencil-clip', `polygon(${points.map(([x, y]) => `${x / width * 100}% ${y / height * 100}%`).join(',')})`);
65
75
  }
@@ -82,13 +92,11 @@ export function usePencilSurface(rootRef, canvasRef, borderRef, variant, seed, c
82
92
  const resize = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(schedule);
83
93
  resize?.observe(root);
84
94
  window.addEventListener('resize', schedule, { passive: true });
85
- const appearance = new MutationObserver(schedule);
86
- appearance.observe(document.documentElement, {
87
- attributes: true,
88
- subtree: true,
89
- attributeFilter: ['data-theme', 'data-inkra-theme'],
90
- });
91
95
  const colorScheme = window.matchMedia('(prefers-color-scheme: dark)');
96
+ const inheritedStyle = new MutationObserver(schedule);
97
+ for (let element = root; element; element = element.parentElement) {
98
+ inheritedStyle.observe(element, { attributes: true, attributeFilter: ['style', 'class', 'data-theme', 'data-inkra-theme'] });
99
+ }
92
100
  colorScheme.addEventListener('change', schedule);
93
101
  const stopRecovery = observeCanvasRecovery(() => {
94
102
  lastPaint = '';
@@ -99,7 +107,7 @@ export function usePencilSurface(rootRef, canvasRef, borderRef, variant, seed, c
99
107
  disposed = true;
100
108
  stopRecovery();
101
109
  resize?.disconnect();
102
- appearance.disconnect();
110
+ inheritedStyle.disconnect();
103
111
  colorScheme.removeEventListener('change', schedule);
104
112
  window.removeEventListener('resize', schedule);
105
113
  cancelAnimationFrame(frame);
@@ -1,9 +1,10 @@
1
1
  import { useRef } from 'react';
2
2
  import { useBrowserLayoutEffect } from './useBrowserLayoutEffect.js';
3
3
  import { PENCIL_SELECTION_WASH } from './surfaceRenderer.js';
4
+ import { readRenderToken } from './renderTokens.js';
4
5
  // Reveal the cached pigment in alternating diagonal strokes, without repainting it.
5
- function drawingFrames(width, height) {
6
- const angle = PENCIL_SELECTION_WASH.angle * Math.PI / 180;
6
+ function drawingFrames(width, height, degrees) {
7
+ const angle = degrees * Math.PI / 180;
7
8
  const cos = Math.cos(angle);
8
9
  const sin = Math.sin(angle);
9
10
  const u = (Math.abs(cos) * width + Math.abs(sin) * height) / 2 + 2;
@@ -16,14 +17,16 @@ function drawingFrames(width, height) {
16
17
  const bottom = -v + (index + 1) * 2 * v / strokes;
17
18
  // Limit travel to the visible section of this stripe, not its rotated bounds.
18
19
  const middle = (top + bottom) / 2;
19
- const ends = [
20
- (-width / 2 + sin * middle) / cos,
21
- (width / 2 + sin * middle) / cos,
22
- (-height / 2 - cos * middle) / sin,
23
- (height / 2 - cos * middle) / sin,
24
- ];
25
- const left = Math.max(Math.min(ends[0], ends[1]), Math.min(ends[2], ends[3]));
26
- const right = Math.min(Math.max(ends[0], ends[1]), Math.max(ends[2], ends[3]));
20
+ let left = -u;
21
+ let right = u;
22
+ for (const [direction, offset, halfExtent] of [[cos, -sin * middle, width / 2], [sin, cos * middle, height / 2]]) {
23
+ if (Math.abs(direction) < 1e-8)
24
+ continue;
25
+ const a = (-halfExtent - offset) / direction;
26
+ const b = (halfExtent - offset) / direction;
27
+ left = Math.max(left, Math.min(a, b));
28
+ right = Math.min(right, Math.max(a, b));
29
+ }
27
30
  for (const progress of [0, 0.5, 1]) {
28
31
  const x = progress === 0 ? (index % 2 ? u : -u) : progress === 1 ? (index % 2 ? -u : u) : (left + right) / 2;
29
32
  const outline = index % 2
@@ -54,7 +57,8 @@ export function useSelectionDrawing(ref, selected, animated = true) {
54
57
  return;
55
58
  }
56
59
  const duration = selected ? 480 : 320;
57
- const animation = node.animate(drawingFrames(node.clientWidth, node.clientHeight).map(frame => ({ ...frame, opacity: 1 })), { duration, direction: selected ? 'normal' : 'reverse', easing: 'linear' });
60
+ const angle = readRenderToken(getComputedStyle(node), '--inkra-wash-angle', PENCIL_SELECTION_WASH.angle, -85, 85);
61
+ const animation = node.animate(drawingFrames(node.clientWidth, node.clientHeight, angle).map(frame => ({ ...frame, opacity: 1 })), { duration, direction: selected ? 'normal' : 'reverse', easing: 'linear' });
58
62
  animation.currentTime = (selected ? progress.current : 1 - progress.current) * duration;
59
63
  let settled = false;
60
64
  const finish = () => {
@@ -43,7 +43,9 @@ export function MapScale({ map }) {
43
43
  draw();
44
44
  map.on('moveend zoomend resize', draw);
45
45
  const observer = new MutationObserver(draw);
46
- observer.observe(document.documentElement, { subtree: true, attributes: true, attributeFilter: ['data-theme', 'data-inkra-theme'] });
46
+ for (let element = bar.current; element; element = element.parentElement) {
47
+ observer.observe(element, { attributes: true, attributeFilter: ['data-theme', 'data-inkra-theme'] });
48
+ }
47
49
  const scheme = matchMedia('(prefers-color-scheme: dark)');
48
50
  scheme.addEventListener('change', draw);
49
51
  return () => { map.off('moveend zoomend resize', draw); observer.disconnect(); scheme.removeEventListener('change', draw); };
@@ -148,7 +148,9 @@ export function JourneyMap({ places, routes = noRoutes, geography, landCover, wa
148
148
  fit(); schedule(); });
149
149
  observer.observe(host.current);
150
150
  const appearance = new MutationObserver(schedule);
151
- appearance.observe(document.documentElement, { subtree: true, attributes: true, attributeFilter: ['data-theme', 'data-inkra-theme'] });
151
+ for (let element = host.current; element; element = element.parentElement) {
152
+ appearance.observe(element, { attributes: true, attributeFilter: ['data-theme', 'data-inkra-theme'] });
153
+ }
152
154
  const scheme = matchMedia('(prefers-color-scheme: dark)');
153
155
  scheme.addEventListener('change', schedule);
154
156
  fit();
@@ -0,0 +1,23 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react';
2
+ import type { KeypadAction, KeypadKey } from './model.js';
3
+ export interface KeypadPressDetails {
4
+ action: KeypadAction;
5
+ key: KeypadKey;
6
+ previousValue: string;
7
+ nextValue: string;
8
+ }
9
+ export interface KeypadProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange' | 'onKeyPress'> {
10
+ layout: readonly (readonly KeypadKey[])[];
11
+ value?: string;
12
+ defaultValue?: string;
13
+ onValueChange?: (value: string, details: KeypadPressDetails) => void;
14
+ onKeyPress?: (details: KeypadPressDetails) => void;
15
+ onConfirm?: (value: string, details: KeypadPressDetails) => void;
16
+ disabled?: boolean;
17
+ maxLength?: number;
18
+ label?: string;
19
+ emptyContent?: ReactNode;
20
+ }
21
+ export declare const Keypad: import("react").ForwardRefExoticComponent<KeypadProps & import("react").RefAttributes<HTMLDivElement>>;
22
+ export { applyKeypadKey, getKeypadAction, getKeypadColumnCount } from './model.js';
23
+ export type { KeypadAction, KeypadKey, KeypadKeyVariant, KeypadValueResult } from './model.js';
@@ -0,0 +1,63 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef, useId, useRef, useState } from 'react';
3
+ import { PencilSurface } from '../PencilSurface.js';
4
+ import { PencilText } from '../PencilText.js';
5
+ import { applyKeypadKey, getKeypadAction, getKeypadColumnCount } from './model.js';
6
+ function keyLabel(key) {
7
+ if (key.ariaLabel)
8
+ return key.ariaLabel;
9
+ if (typeof key.label === 'string' || typeof key.label === 'number')
10
+ return String(key.label);
11
+ return undefined;
12
+ }
13
+ function keyVariant(key) {
14
+ if (key.variant)
15
+ return key.variant;
16
+ const action = getKeypadAction(key);
17
+ if (action === 'confirm')
18
+ return 'confirm';
19
+ if (action === 'delete')
20
+ return 'secondary';
21
+ return 'default';
22
+ }
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
+ const id = useId();
25
+ const controlled = value !== undefined;
26
+ const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
27
+ const currentValue = controlled ? value : uncontrolledValue;
28
+ const valueRef = useRef(currentValue);
29
+ valueRef.current = currentValue;
30
+ const columns = getKeypadColumnCount(layout);
31
+ const classes = ['inkra-keypad', className].filter(Boolean).join(' ');
32
+ const style = { '--inkra-keypad-columns': columns, ...props.style };
33
+ const press = (key) => {
34
+ const previousValue = valueRef.current;
35
+ const result = applyKeypadKey(previousValue, key, maxLength);
36
+ const details = {
37
+ action: getKeypadAction(key),
38
+ key,
39
+ previousValue,
40
+ nextValue: result.nextValue,
41
+ };
42
+ if (result.changed) {
43
+ if (!controlled) {
44
+ valueRef.current = result.nextValue;
45
+ setUncontrolledValue(result.nextValue);
46
+ }
47
+ onValueChange?.(result.nextValue, details);
48
+ }
49
+ onKeyPress?.(details);
50
+ if (details.action === 'confirm')
51
+ onConfirm?.(previousValue, details);
52
+ };
53
+ return (_jsx("div", { ...props, "aria-disabled": disabled || undefined, "aria-label": ariaLabel ?? label, className: classes, "data-disabled": disabled || undefined, ref: ref, role: "group", style: style, children: layout.length === 0 ? emptyContent : layout.map((row, rowIndex) => (_jsx("div", { className: "inkra-keypad__row", children: row.map(key => {
54
+ const action = getKeypadAction(key);
55
+ const atLimit = action === 'input' && maxLength !== undefined && !applyKeypadKey(currentValue, key, maxLength).changed;
56
+ const keyDisabled = disabled || key.disabled === true || atLimit;
57
+ const accessibleName = keyLabel(key);
58
+ return (_jsxs("button", { "aria-label": accessibleName, className: "inkra-keypad__key", "data-action": action, "data-key-id": key.id, "data-variant": keyVariant(key), disabled: keyDisabled, onClick: () => press(key), style: { '--inkra-keypad-span': Math.max(1, Math.floor(key.span ?? 1)) }, type: "button", children: [_jsx(PencilSurface, { as: "span", className: "inkra-keypad__key-surface", radius: 6, seed: `inkra-keypad-${id}-${key.id}`, variant: keyVariant(key) === 'confirm' ? 'action' : keyVariant(key) === 'danger' ? 'danger' : keyVariant(key) === 'secondary' ? 'quiet' : 'outline', children: _jsx("span", { className: "inkra-keypad__key-label", children: typeof key.label === 'string' || typeof key.label === 'number'
59
+ ? _jsx(PencilText, { seed: `inkra-keypad-${id}-${key.id}-label`, children: String(key.label) })
60
+ : key.label }) }), _jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-keypad__focus", seed: `inkra-keypad-${id}-${key.id}-focus`, variant: "outline" })] }, key.id));
61
+ }) }, `row-${rowIndex}`))) }));
62
+ });
63
+ export { applyKeypadKey, getKeypadAction, getKeypadColumnCount } from './model.js';
@@ -0,0 +1,20 @@
1
+ import type { ReactNode } from 'react';
2
+ export type KeypadAction = 'input' | 'delete' | 'confirm' | 'custom';
3
+ export type KeypadKeyVariant = 'default' | 'secondary' | 'confirm' | 'danger';
4
+ export interface KeypadKey {
5
+ id: string;
6
+ label: ReactNode;
7
+ action?: KeypadAction;
8
+ value?: string;
9
+ ariaLabel?: string;
10
+ disabled?: boolean;
11
+ span?: number;
12
+ variant?: KeypadKeyVariant;
13
+ }
14
+ export interface KeypadValueResult {
15
+ nextValue: string;
16
+ changed: boolean;
17
+ }
18
+ export declare function getKeypadAction(key: KeypadKey): KeypadAction;
19
+ export declare function applyKeypadKey(value: string, key: KeypadKey, maxLength?: number): KeypadValueResult;
20
+ export declare function getKeypadColumnCount(layout: readonly (readonly KeypadKey[])[]): number;
@@ -0,0 +1,33 @@
1
+ function removeLastGrapheme(value) {
2
+ if (value.length === 0)
3
+ return value;
4
+ const Segmenter = Intl.Segmenter;
5
+ if (Segmenter) {
6
+ const segments = Array.from(new Segmenter(undefined, { granularity: 'grapheme' }).segment(value));
7
+ const last = segments[segments.length - 1];
8
+ return last ? value.slice(0, last.index) : '';
9
+ }
10
+ return Array.from(value).slice(0, -1).join('');
11
+ }
12
+ export function getKeypadAction(key) {
13
+ return key.action ?? 'input';
14
+ }
15
+ export function applyKeypadKey(value, key, maxLength) {
16
+ const action = getKeypadAction(key);
17
+ if (action === 'delete') {
18
+ const nextValue = removeLastGrapheme(value);
19
+ return { nextValue, changed: nextValue !== value };
20
+ }
21
+ if (action !== 'input')
22
+ return { nextValue: value, changed: false };
23
+ const addition = key.value ?? (typeof key.label === 'string' || typeof key.label === 'number' ? String(key.label) : '');
24
+ if (addition.length === 0)
25
+ return { nextValue: value, changed: false };
26
+ if (maxLength !== undefined && value.length + addition.length > maxLength) {
27
+ return { nextValue: value, changed: false };
28
+ }
29
+ return { nextValue: value + addition, changed: true };
30
+ }
31
+ export function getKeypadColumnCount(layout) {
32
+ return Math.max(1, ...layout.map(row => row.reduce((total, key) => total + Math.max(1, Math.floor(key.span ?? 1)), 0)));
33
+ }
@@ -0,0 +1,63 @@
1
+ import type { CSSProperties, HTMLAttributes } from 'react';
2
+ export type LayoutSpace = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10 | 12 | 16;
3
+ export type LayoutElement = 'div' | 'section' | 'article' | 'nav' | 'header' | 'footer' | 'main' | 'aside' | 'ul' | 'ol' | 'li';
4
+ export type LayoutAlign = 'start' | 'center' | 'end' | 'stretch' | 'baseline';
5
+ export type LayoutJustify = 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly';
6
+ interface LayoutElementProps extends HTMLAttributes<HTMLElement> {
7
+ as?: LayoutElement;
8
+ }
9
+ export interface FlexProps extends LayoutElementProps {
10
+ direction?: 'row' | 'column' | 'row-reverse' | 'column-reverse';
11
+ gap?: LayoutSpace;
12
+ align?: LayoutAlign;
13
+ justify?: LayoutJustify;
14
+ wrap?: boolean;
15
+ inline?: boolean;
16
+ }
17
+ export declare const Flex: import("react").ForwardRefExoticComponent<FlexProps & import("react").RefAttributes<HTMLElement>>;
18
+ export type StackProps = FlexProps;
19
+ export declare const Stack: import("react").ForwardRefExoticComponent<FlexProps & import("react").RefAttributes<HTMLElement>>;
20
+ export interface GridColumns {
21
+ base?: number;
22
+ sm?: number;
23
+ md?: number;
24
+ lg?: number;
25
+ }
26
+ export interface GridProps extends LayoutElementProps {
27
+ columns?: number | GridColumns;
28
+ gap?: LayoutSpace;
29
+ rowGap?: LayoutSpace;
30
+ columnGap?: LayoutSpace;
31
+ align?: LayoutAlign;
32
+ }
33
+ export declare const Grid: import("react").ForwardRefExoticComponent<GridProps & import("react").RefAttributes<HTMLElement>>;
34
+ export interface ContainerProps extends LayoutElementProps {
35
+ size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
36
+ padding?: LayoutSpace;
37
+ centered?: boolean;
38
+ }
39
+ export declare const Container: import("react").ForwardRefExoticComponent<ContainerProps & import("react").RefAttributes<HTMLElement>>;
40
+ export interface LayoutProps extends LayoutElementProps {
41
+ gap?: LayoutSpace;
42
+ collapseAt?: 'sm' | 'md' | 'never';
43
+ }
44
+ export interface LayoutBodyProps extends LayoutElementProps {
45
+ gap?: LayoutSpace;
46
+ }
47
+ export interface LayoutRegionProps extends LayoutElementProps {
48
+ padding?: LayoutSpace;
49
+ divided?: boolean;
50
+ }
51
+ export interface LayoutSidebarProps extends LayoutRegionProps {
52
+ width?: CSSProperties['width'];
53
+ }
54
+ export type LayoutContentProps = Omit<LayoutRegionProps, 'divided'>;
55
+ export declare const Layout: Readonly<{
56
+ Root: import("react").ForwardRefExoticComponent<LayoutProps & import("react").RefAttributes<HTMLElement>>;
57
+ Header: import("react").ForwardRefExoticComponent<LayoutRegionProps & import("react").RefAttributes<HTMLElement>>;
58
+ Body: import("react").ForwardRefExoticComponent<LayoutBodyProps & import("react").RefAttributes<HTMLElement>>;
59
+ Sidebar: import("react").ForwardRefExoticComponent<LayoutSidebarProps & import("react").RefAttributes<HTMLElement>>;
60
+ Content: import("react").ForwardRefExoticComponent<LayoutContentProps & import("react").RefAttributes<HTMLElement>>;
61
+ Footer: import("react").ForwardRefExoticComponent<LayoutRegionProps & import("react").RefAttributes<HTMLElement>>;
62
+ }>;
63
+ export {};
@@ -0,0 +1,62 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { createElement, forwardRef } from 'react';
3
+ import { PencilLine } from '../PencilLine.js';
4
+ const Box = forwardRef(function Box({ as = 'div', ...props }, ref) {
5
+ return createElement(as, { ...props, ref });
6
+ });
7
+ const classes = (...values) => values.filter(Boolean).join(' ');
8
+ const space = (value) => `var(--inkra-space-${value}, ${value * 4}px)`;
9
+ const justifyValues = { start: 'flex-start', center: 'center', end: 'flex-end', between: 'space-between', around: 'space-around', evenly: 'space-evenly' };
10
+ const alignValues = { start: 'flex-start', center: 'center', end: 'flex-end', stretch: 'stretch', baseline: 'baseline' };
11
+ export const Flex = forwardRef(function Flex({ as: Element = 'div', direction = 'row', gap = 3, align = 'stretch', justify = 'start', wrap = false, inline = false, className, style, ...props }, ref) {
12
+ return _jsx(Box, { as: Element, ...props, ref: ref, className: classes('inkra-flex', className), style: { display: inline ? 'inline-flex' : 'flex', flexDirection: direction,
13
+ gap: space(gap), alignItems: alignValues[align], justifyContent: justifyValues[justify],
14
+ flexWrap: wrap ? 'wrap' : 'nowrap', ...style } });
15
+ });
16
+ export const Stack = forwardRef(function Stack({ direction = 'column', className, ...props }, ref) {
17
+ return _jsx(Flex, { ...props, ref: ref, direction: direction, className: classes('inkra-stack', className) });
18
+ });
19
+ const columnCount = (value, fallback) => value === undefined || !Number.isFinite(value) ? fallback : Math.min(12, Math.max(1, Math.trunc(value)));
20
+ export const Grid = forwardRef(function Grid({ as: Element = 'div', columns = 1, gap = 3, rowGap = gap, columnGap = gap, align = 'stretch', className, style, ...props }, ref) {
21
+ const responsive = typeof columns === 'number' ? { base: columns } : columns;
22
+ const base = columnCount(responsive.base, 1);
23
+ const sm = columnCount(responsive.sm, base);
24
+ const md = columnCount(responsive.md, sm);
25
+ const lg = columnCount(responsive.lg, md);
26
+ const resolvedStyle = {
27
+ '--inkra-grid-base': base, '--inkra-grid-sm': sm, '--inkra-grid-md': md, '--inkra-grid-lg': lg,
28
+ rowGap: space(rowGap), columnGap: space(columnGap), alignItems: alignValues[align], ...style,
29
+ };
30
+ return _jsx(Box, { as: Element, ...props, ref: ref, className: classes('inkra-grid', className), style: resolvedStyle });
31
+ });
32
+ const containerWidths = { sm: '640px', md: '768px', lg: '1024px', xl: '1280px', full: 'none' };
33
+ export const Container = forwardRef(function Container({ as: Element = 'div', size = 'lg', padding = 5, centered = true, className, style, ...props }, ref) {
34
+ return _jsx(Box, { as: Element, ...props, ref: ref, className: classes('inkra-container', className), style: { maxWidth: containerWidths[size], paddingInline: space(padding),
35
+ marginInline: centered ? 'auto' : 0, ...style } });
36
+ });
37
+ const LayoutRoot = forwardRef(function LayoutRoot({ as: Element = 'div', gap = 0, collapseAt = 'sm', className, style, ...props }, ref) {
38
+ return _jsx(Box, { as: Element, ...props, ref: ref, className: classes('inkra-layout', className), "data-collapse-at": collapseAt, style: { gap: space(gap), ...style } });
39
+ });
40
+ const LayoutBody = forwardRef(function LayoutBody({ as: Element = 'div', gap = 0, className, style, ...props }, ref) {
41
+ return _jsx(Box, { as: Element, ...props, ref: ref, className: classes('inkra-layout__body', className), style: { gap: space(gap), ...style } });
42
+ });
43
+ const LayoutHeader = forwardRef(function LayoutHeader({ as: Element = 'header', padding = 5, divided = false, className, style, children, ...props }, ref) {
44
+ return _jsxs(Box, { as: Element, ...props, ref: ref, className: classes('inkra-layout__header', className), style: { padding: space(padding), ...style }, children: [children, divided && _jsx(PencilLine, { className: "inkra-layout__divider" })] });
45
+ });
46
+ const LayoutFooter = forwardRef(function LayoutFooter({ as: Element = 'footer', padding = 5, divided = false, className, style, children, ...props }, ref) {
47
+ return _jsxs(Box, { as: Element, ...props, ref: ref, className: classes('inkra-layout__footer', className), style: { padding: space(padding), ...style }, children: [divided && _jsx(PencilLine, { className: "inkra-layout__divider" }), children] });
48
+ });
49
+ const LayoutSidebar = forwardRef(function LayoutSidebar({ as: Element = 'aside', width = 240, padding = 5, divided = false, className, style, children, ...props }, ref) {
50
+ const resolvedStyle = {
51
+ '--inkra-layout-sidebar-width': typeof width === 'number' ? `${width}px` : width,
52
+ padding: space(padding), ...style,
53
+ };
54
+ return _jsxs(Box, { as: Element, ...props, ref: ref, className: classes('inkra-layout__sidebar', className), style: resolvedStyle, children: [children, divided && _jsxs(_Fragment, { children: [_jsx(PencilLine, { className: "inkra-layout__sidebar-divider", orientation: "vertical" }), _jsx(PencilLine, { className: "inkra-layout__sidebar-divider-mobile" })] })] });
55
+ });
56
+ const LayoutContent = forwardRef(function LayoutContent({ as: Element = 'div', padding = 5, className, style, ...props }, ref) {
57
+ return _jsx(Box, { as: Element, ...props, ref: ref, className: classes('inkra-layout__content', className), style: { padding: space(padding), ...style } });
58
+ });
59
+ export const Layout = Object.freeze({
60
+ Root: LayoutRoot, Header: LayoutHeader, Body: LayoutBody,
61
+ Sidebar: LayoutSidebar, Content: LayoutContent, Footer: LayoutFooter,
62
+ });
@@ -0,0 +1,45 @@
1
+ import { type HTMLAttributes, type ReactNode } from 'react';
2
+ export type LogLevelTone = 'neutral' | 'info' | 'success' | 'warning' | 'danger';
3
+ export interface LogLevelDefinition {
4
+ value: string;
5
+ label: ReactNode;
6
+ tone?: LogLevelTone;
7
+ }
8
+ export interface LogEntry {
9
+ /** Stable and unique within this Log. Changing an id replaces that record. */
10
+ id: string;
11
+ /** Entries render in the exact order supplied by the items array. */
12
+ level?: string;
13
+ time?: ReactNode;
14
+ dateTime?: string;
15
+ icon?: ReactNode;
16
+ content: ReactNode;
17
+ }
18
+ export interface LogLabels {
19
+ filters: string;
20
+ allLevels: string;
21
+ pause: string;
22
+ resume: string;
23
+ latest: string;
24
+ empty: string;
25
+ noMatches: string;
26
+ list: string;
27
+ }
28
+ export interface LogProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
29
+ items: readonly LogEntry[];
30
+ levels?: readonly LogLevelDefinition[];
31
+ filteredLevels?: readonly string[];
32
+ defaultFilteredLevels?: readonly string[];
33
+ onFilteredLevelsChange?: (levels: readonly string[]) => void;
34
+ paused?: boolean;
35
+ defaultPaused?: boolean;
36
+ onPausedChange?: (paused: boolean) => void;
37
+ showLevelFilter?: boolean;
38
+ showFollowControl?: boolean;
39
+ nearBottomThreshold?: number;
40
+ empty?: ReactNode;
41
+ noMatches?: ReactNode;
42
+ labels?: Partial<LogLabels>;
43
+ viewportProps?: Omit<HTMLAttributes<HTMLDivElement>, 'children'>;
44
+ }
45
+ export declare const Log: import("react").ForwardRefExoticComponent<LogProps & import("react").RefAttributes<HTMLDivElement>>;