@reekon-tools/boldr-utils 1.8.4 → 1.9.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 (81) hide show
  1. package/dist/annotation/canvas/AnnotationCanvasInner.native.js +41 -10
  2. package/dist/annotation/canvas/measurementGeometry.js +8 -8
  3. package/dist/annotation/canvas/stampLayout.js +59 -54
  4. package/dist/annotation/canvas/useAnnotationCanvasState.d.ts +4 -1
  5. package/dist/annotation/canvas/useAnnotationCanvasState.js +88 -38
  6. package/dist/calculator/evaluate.js +38 -4
  7. package/dist/calculator/expressionUnits.d.ts +84 -0
  8. package/dist/calculator/expressionUnits.js +439 -0
  9. package/dist/calculator/index.d.ts +1 -0
  10. package/dist/calculator/index.js +1 -0
  11. package/dist/calculator/schema.d.ts +18 -0
  12. package/dist/calculator/schema.js +26 -5
  13. package/dist/calculator/validate.d.ts +12 -2
  14. package/dist/calculator/validate.js +116 -43
  15. package/dist/theme/index.d.ts +3 -0
  16. package/dist/theme/index.js +3 -0
  17. package/dist/theme/preset.d.ts +120 -0
  18. package/dist/theme/preset.js +25 -0
  19. package/dist/theme/semantic.d.ts +136 -0
  20. package/dist/theme/semantic.js +116 -0
  21. package/package.json +1 -1
  22. package/dist/canvas/AnnotationCanvas.d.ts +0 -11
  23. package/dist/canvas/AnnotationCanvas.js +0 -10
  24. package/dist/canvas/AnnotationCanvas.native.d.ts +0 -8
  25. package/dist/canvas/AnnotationCanvas.native.js +0 -6
  26. package/dist/canvas/AnnotationCanvasInner.d.ts +0 -39
  27. package/dist/canvas/AnnotationCanvasInner.js +0 -219
  28. package/dist/canvas/AnnotationCanvasInner.native.d.ts +0 -35
  29. package/dist/canvas/AnnotationCanvasInner.native.js +0 -138
  30. package/dist/canvas/AnnotationCanvasSkia.d.ts +0 -27
  31. package/dist/canvas/AnnotationCanvasSkia.js +0 -20
  32. package/dist/canvas/Tool.d.ts +0 -38
  33. package/dist/canvas/Tool.js +0 -1
  34. package/dist/canvas/elements/BackgroundImageElement.d.ts +0 -9
  35. package/dist/canvas/elements/BackgroundImageElement.js +0 -37
  36. package/dist/canvas/elements/MeasurementStampElement.d.ts +0 -13
  37. package/dist/canvas/elements/MeasurementStampElement.js +0 -30
  38. package/dist/canvas/elements/ShapeElement.d.ts +0 -7
  39. package/dist/canvas/elements/ShapeElement.js +0 -62
  40. package/dist/canvas/elements/StrokeElement.d.ts +0 -7
  41. package/dist/canvas/elements/StrokeElement.js +0 -18
  42. package/dist/canvas/measurementPicker.d.ts +0 -10
  43. package/dist/canvas/measurementPicker.js +0 -1
  44. package/dist/canvas/measurementStampOverlay.d.ts +0 -11
  45. package/dist/canvas/measurementStampOverlay.js +0 -1
  46. package/dist/canvas/pointerAdapter.d.ts +0 -3
  47. package/dist/canvas/pointerAdapter.js +0 -19
  48. package/dist/canvas/stampLayout.d.ts +0 -5
  49. package/dist/canvas/stampLayout.js +0 -14
  50. package/dist/canvas/tools/measurementStampTool.d.ts +0 -9
  51. package/dist/canvas/tools/measurementStampTool.js +0 -37
  52. package/dist/canvas/tools/panTool.d.ts +0 -5
  53. package/dist/canvas/tools/panTool.js +0 -25
  54. package/dist/canvas/tools/penTool.d.ts +0 -13
  55. package/dist/canvas/tools/penTool.js +0 -68
  56. package/dist/canvas/tools/selectTool.d.ts +0 -2
  57. package/dist/canvas/tools/selectTool.js +0 -182
  58. package/dist/canvas/useAnnotationCanvasState.d.ts +0 -54
  59. package/dist/canvas/useAnnotationCanvasState.js +0 -210
  60. package/dist/canvas/viewport.d.ts +0 -16
  61. package/dist/canvas/viewport.js +0 -54
  62. package/dist/data/AnnotationDataContext.d.ts +0 -8
  63. package/dist/data/AnnotationDataContext.js +0 -11
  64. package/dist/data/AnnotationDataProvider.d.ts +0 -65
  65. package/dist/data/AnnotationDataProvider.js +0 -4
  66. package/dist/data/InMemoryAnnotationProvider.d.ts +0 -30
  67. package/dist/data/InMemoryAnnotationProvider.js +0 -197
  68. package/dist/data/canvasPersistence.d.ts +0 -3
  69. package/dist/data/canvasPersistence.js +0 -26
  70. package/dist/data/hooks/useAnnotationCanvasDoc.d.ts +0 -33
  71. package/dist/data/hooks/useAnnotationCanvasDoc.js +0 -314
  72. package/dist/data/hooks/useAnnotationDoc.d.ts +0 -7
  73. package/dist/data/hooks/useAnnotationDoc.js +0 -33
  74. package/dist/data/hooks/useAnnotationList.d.ts +0 -7
  75. package/dist/data/hooks/useAnnotationList.js +0 -26
  76. package/dist/data/hooks/useAnnotationMutations.d.ts +0 -9
  77. package/dist/data/hooks/useAnnotationMutations.js +0 -11
  78. package/dist/hooks/useParseMeasurement.d.ts +0 -4
  79. package/dist/hooks/useParseMeasurement.js +0 -14
  80. package/dist/utils/evaluateFormula.d.ts +0 -20
  81. package/dist/utils/evaluateFormula.js +0 -31
@@ -1,10 +1,9 @@
1
1
  import { z } from 'zod';
2
- import { create, all } from 'mathjs';
3
2
  import { ColumnType, DecimalTolerance, FractionalTolerance, } from '../types/firestore.js';
4
- import { CALCULATOR_SCHEMA_VERSION, equationForField, isEquationTargetKind, isNumericFieldKind, } from './schema.js';
5
- import { CALCULATOR_UNIT_INFO, unitDimension } from './units.js';
3
+ import { CALCULATOR_SCHEMA_VERSION, equationForField, fieldDimension, isEquationTargetKind, isNumericFieldKind, } from './schema.js';
4
+ import { CALCULATOR_UNIT_INFO, unitDimension, } from './units.js';
5
+ import { MATHJS_CONSTANTS, inferExpressionUnit, isEquivalentUnit, parseExpression, producesCanonicalValue, unitScaleRatio, } from './expressionUnits.js';
6
6
  import { isCalculatorCategoryId } from './categories.js';
7
- const math = create(all);
8
7
  // ---------------------------------------------------------------------------
9
8
  // Definition validation: zod for structure, plus the cross-field invariants
10
9
  // zod can't express (reference resolution, cycles, solve wiring). Callable
@@ -94,6 +93,8 @@ const calculatorEquationSchema = z.object({
94
93
  targetFieldId: z.string().min(1),
95
94
  expression: z.string().min(1),
96
95
  variableToFieldId: z.record(z.string(), z.string()),
96
+ variableUnits: z.record(z.string(), calculatorUnitSchema).optional(),
97
+ resultUnit: calculatorUnitSchema.optional(),
97
98
  });
98
99
  export const calculatorDefinitionSchema = z.object({
99
100
  id: z.string().min(1),
@@ -127,45 +128,43 @@ export const calculatorDefinitionSchema = z.object({
127
128
  createdAt: z.unknown().optional(),
128
129
  updatedAt: z.unknown().optional(),
129
130
  });
130
- // Constants mathjs resolves without a scope entry.
131
- const KNOWN_SYMBOLS = new Set([
132
- 'pi',
133
- 'PI',
134
- 'e',
135
- 'E',
136
- 'tau',
137
- 'phi',
138
- 'i',
139
- 'true',
140
- 'false',
141
- 'Infinity',
142
- 'NaN',
143
- 'null',
144
- ]);
145
- /** Free variables of an expression (function names and constants excluded). */
146
- export const expressionSymbols = (expression) => {
147
- try {
148
- const node = math.parse(expression);
149
- const symbols = new Set();
150
- node.traverse((n, path, parent) => {
151
- const sym = n;
152
- const par = parent;
153
- if (sym.isSymbolNode &&
154
- sym.name &&
155
- !(par?.isFunctionNode && path === 'fn') &&
156
- !KNOWN_SYMBOLS.has(sym.name)) {
157
- symbols.add(sym.name);
158
- }
159
- });
160
- return { ok: true, symbols: [...symbols] };
161
- }
162
- catch (err) {
163
- return {
164
- ok: false,
165
- error: err instanceof Error ? err.message : String(err),
166
- };
167
- }
131
+ /**
132
+ * Free variables of an expression (function names and constants excluded).
133
+ *
134
+ * `knownVariables` are treated as variables even when mathjs would resolve
135
+ * them as constants: the authoring UI names variables A, B, C… and `E` is
136
+ * Euler's number, so without this a mapped `E` reads as a constant and gets
137
+ * reported as an unused variable. Evaluation is unaffected — a scope entry
138
+ * shadows the constant — so this only realigns validation with reality.
139
+ */
140
+ export const expressionSymbols = (expression, knownVariables) => {
141
+ const parsed = parseExpression(expression);
142
+ if (!parsed.ok)
143
+ return { ok: false, error: parsed.error };
144
+ const known = new Set(knownVariables ?? []);
145
+ const symbols = new Set();
146
+ parsed.node.traverse((n, path, parent) => {
147
+ const sym = n;
148
+ const par = parent;
149
+ if (sym.isSymbolNode &&
150
+ sym.name &&
151
+ !(par?.isFunctionNode && path === 'fn') &&
152
+ (!MATHJS_CONSTANTS.has(sym.name) || known.has(sym.name))) {
153
+ symbols.add(sym.name);
154
+ }
155
+ });
156
+ return { ok: true, symbols: [...symbols] };
168
157
  };
158
+ const DIMENSION_LABELS = {
159
+ length: 'a length',
160
+ area: 'an area',
161
+ volume: 'a volume',
162
+ angle: 'an angle',
163
+ none: 'a plain number',
164
+ };
165
+ const describeDimension = (dimension) => DIMENSION_LABELS[dimension];
166
+ // 1728 stays 1728; 57.29577951308232 becomes 57.3.
167
+ const trimRatio = (ratio) => Number.isInteger(ratio) ? String(ratio) : ratio.toFixed(1);
169
168
  // Does `equation` depend on `fieldId`, directly or through the equations of
170
169
  // output fields it references? (Chained solving: the unknown may reach the
171
170
  // governing equation through intermediate outputs.)
@@ -303,7 +302,81 @@ export const validateCalculatorDefinition = (input) => {
303
302
  push('self-referencing-equation', `${path}.variableToFieldId.${variable}`, `Equation for "${eq.targetFieldId}" references its own target`);
304
303
  }
305
304
  }
306
- const symbols = expressionSymbols(eq.expression);
305
+ // --- Unit annotations --------------------------------------------------
306
+ for (const [variable, unit] of Object.entries(eq.variableUnits ?? {})) {
307
+ const unitPath = `${path}.variableUnits.${variable}`;
308
+ const mappedId = eq.variableToFieldId[variable];
309
+ if (mappedId == null) {
310
+ push('unit-for-unmapped-variable', unitPath, `Variable ${variable} has a unit but no field mapping`, 'warning');
311
+ continue;
312
+ }
313
+ const mapped = fieldById.get(mappedId);
314
+ if (!mapped)
315
+ continue; // unknown-mapped-field already reported above
316
+ const dim = fieldDimension(mapped);
317
+ if (dim === 'none') {
318
+ push('variable-unit-on-dimensionless-field', unitPath, `Variable ${variable} maps to "${mapped.name}", which has no unit dimension`);
319
+ }
320
+ else if (unitDimension(unit) !== dim) {
321
+ push('variable-unit-dimension-mismatch', unitPath, `Unit "${unit}" is not a ${dim} unit (variable ${variable} maps to "${mapped.name}")`);
322
+ }
323
+ }
324
+ if (target) {
325
+ const dim = fieldDimension(target);
326
+ if (eq.resultUnit != null) {
327
+ if (dim === 'none') {
328
+ push('result-unit-on-number-target', `${path}.resultUnit`, `Equation target "${target.name}" is a number field and takes the result as-is; remove the result unit`);
329
+ }
330
+ else if (unitDimension(eq.resultUnit) !== dim) {
331
+ push('result-unit-dimension-mismatch', `${path}.resultUnit`, `Result unit "${eq.resultUnit}" is not a ${dim} unit (target "${target.name}")`);
332
+ }
333
+ }
334
+ // The unit the EXPRESSION produces, which is what `resultUnit` claims to
335
+ // be. Dimension checks against the target field can't see a same-
336
+ // dimension scale error: three inch variables multiplied are in³, so
337
+ // `cu_ft` passes every check above while reporting 1728× the truth.
338
+ const inferred = inferExpressionUnit(def.fields, eq);
339
+ if (inferred.ok && dim !== 'none') {
340
+ if (inferred.dimension !== dim) {
341
+ push('expression-dimension-mismatch', `${path}.expression`, `Expression produces ${describeDimension(inferred.dimension)} but "${target.name}" is ${describeDimension(dim)} — check the variable units and the expression`);
342
+ }
343
+ else if (eq.resultUnit == null) {
344
+ if (!producesCanonicalValue(inferred)) {
345
+ push('missing-result-unit', `${path}.resultUnit`, `Expression produces a value in ${inferred.unit ? `"${inferred.unit}"` : inferred.bases.join('/')} but no result unit is set, so it will be stored as a canonical value. Set the result unit${inferred.unit ? ` to "${inferred.unit}"` : ''}.`);
346
+ }
347
+ }
348
+ else if (inferred.unit != null &&
349
+ // A result unit from the wrong dimension is already reported above;
350
+ // quoting a scale factor across dimensions would be meaningless.
351
+ unitDimension(eq.resultUnit) === inferred.dimension &&
352
+ !isEquivalentUnit(eq.resultUnit, inferred.unit)) {
353
+ const ratio = unitScaleRatio(eq.resultUnit, inferred.unit);
354
+ const off = ratio > 1
355
+ ? `${trimRatio(ratio)}× too large`
356
+ : `${trimRatio(1 / ratio)}× too small`;
357
+ push('result-unit-scale-mismatch', `${path}.resultUnit`, `Expression produces "${inferred.unit}" but the result unit is "${eq.resultUnit}" — computed values will be ${off}. Use "${inferred.unit}", or change the variable units to match.`);
358
+ }
359
+ }
360
+ if (inferred.ok && inferred.bases.length > 1) {
361
+ push('mixed-base-units', `${path}.variableUnits`, `Variables mix ${inferred.bases.join(' and ')} in one expression, so the result has no single unit. Put them all in the same unit.`, 'warning');
362
+ }
363
+ if (!inferred.ok && inferred.reason === 'inconsistent-terms') {
364
+ push('expression-mixed-dimensions', `${path}.expression`, 'Expression adds or compares terms of different dimensions (a length and an area, say) — one of them is probably the wrong field', 'warning');
365
+ }
366
+ }
367
+ // The canonical-leak trap: a number target consuming a unit-carrying
368
+ // field without a variable unit receives raw µm-scale values.
369
+ if (target?.kind === ColumnType.Number) {
370
+ for (const [variable, fieldId] of Object.entries(eq.variableToFieldId)) {
371
+ const mapped = fieldById.get(fieldId);
372
+ if (!mapped || fieldDimension(mapped) === 'none')
373
+ continue;
374
+ if (eq.variableUnits?.[variable] == null) {
375
+ push('canonical-value-into-number-target', `${path}.variableToFieldId.${variable}`, `"${target.name}" is a number field but variable ${variable} ("${mapped.name}") has no display unit — its canonical (µm-scale) value will be used. Pick a unit for ${variable}.`, 'warning');
376
+ }
377
+ }
378
+ }
379
+ const symbols = expressionSymbols(eq.expression, Object.keys(eq.variableToFieldId));
307
380
  if (!symbols.ok) {
308
381
  push('expression-invalid', `${path}.expression`, `Expression does not parse: ${symbols.error}`);
309
382
  }
@@ -0,0 +1,3 @@
1
+ export * from './colors.js';
2
+ export * from './semantic.js';
3
+ export * from './preset.js';
@@ -0,0 +1,3 @@
1
+ export * from './colors.js';
2
+ export * from './semantic.js';
3
+ export * from './preset.js';
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Shared Tailwind preset. COLORS ONLY, by design.
3
+ *
4
+ * Must never define `content`, `plugins`, `presets`, or `darkMode`:
5
+ * NativeWind recurses `config.presets[].presets` hunting for its own marker
6
+ * preset, and a `content` key here would silently narrow the consuming app's
7
+ * globs.
8
+ *
9
+ * Consumed from CJS Tailwind configs, so this is a NAMED export with no
10
+ * default — that keeps ESM/CJS interop semantics irrelevant:
11
+ *
12
+ * const { boldrPreset } = require('@reekon-tools/boldr-utils/theme/preset');
13
+ * module.exports = { presets: [require('nativewind/preset'), boldrPreset] };
14
+ *
15
+ * Tailwind loads configs through jiti + sucrase, so requiring this ESM file
16
+ * from a CJS config works regardless of Node version.
17
+ *
18
+ * Note that Tailwind merges `theme.extend` per key with the *app* winning, and
19
+ * replaces a color key wholesale rather than deep-merging. An app that keeps
20
+ * its own `colors.background` will silently shadow this preset.
21
+ */
22
+ export declare const boldrPreset: {
23
+ theme: {
24
+ extend: {
25
+ colors: {
26
+ background: "#1D1F21";
27
+ foreground: {
28
+ DEFAULT: "#FFFFFF";
29
+ disabled: "#626364";
30
+ };
31
+ card: {
32
+ DEFAULT: "#2B2C2E";
33
+ foreground: "#FFFFFF";
34
+ };
35
+ popover: {
36
+ DEFAULT: "#2B2C2E";
37
+ foreground: "#FFFFFF";
38
+ };
39
+ muted: {
40
+ DEFAULT: "#424345";
41
+ foreground: "#9FA0A2";
42
+ };
43
+ accent: {
44
+ DEFAULT: "#424345";
45
+ foreground: "#FFFFFF";
46
+ };
47
+ secondary: {
48
+ DEFAULT: "#424345";
49
+ foreground: "#FFFFFF";
50
+ };
51
+ border: {
52
+ DEFAULT: "#424345";
53
+ strong: "#626364";
54
+ };
55
+ input: "#424345";
56
+ ring: "#FFAD00";
57
+ primary: {
58
+ DEFAULT: "#FFAD00";
59
+ foreground: "#1D1F21";
60
+ };
61
+ destructive: {
62
+ DEFAULT: "#DF6358";
63
+ foreground: "#1D1F21";
64
+ };
65
+ elevated: {
66
+ DEFAULT: "#424345";
67
+ foreground: "#FFFFFF";
68
+ };
69
+ interactive: {
70
+ DEFAULT: "#0066FF";
71
+ foreground: "#FFFFFF";
72
+ };
73
+ nav: {
74
+ DEFAULT: "#2D3843";
75
+ foreground: "#FFFFFF";
76
+ };
77
+ paper: {
78
+ DEFAULT: "#E8EAED";
79
+ foreground: "#1D1F21";
80
+ field: "#FFFFFF";
81
+ line: "#BCBEC2";
82
+ muted: "#626364";
83
+ };
84
+ tile: {
85
+ foreground: "#FFFFFF";
86
+ measurement: {
87
+ DEFAULT: "#0066FF";
88
+ badge: "#50A6FF";
89
+ };
90
+ formula: {
91
+ DEFAULT: "#BD30A8";
92
+ badge: "#6C155F";
93
+ };
94
+ count: {
95
+ DEFAULT: "#02274B";
96
+ foreground: "#FFFFFF";
97
+ };
98
+ };
99
+ diagram: {
100
+ 1: "#2E4C6A";
101
+ 2: "#4B6074";
102
+ 3: "#67819A";
103
+ };
104
+ tolerance: {
105
+ in: {
106
+ DEFAULT: "#58A942";
107
+ warning: "#00E920";
108
+ muted: "#8BB380";
109
+ };
110
+ out: {
111
+ DEFAULT: "#E83D2F";
112
+ warning: "#DA1100";
113
+ muted: "#DF6358";
114
+ };
115
+ };
116
+ };
117
+ };
118
+ };
119
+ };
120
+ export type BoldrPreset = typeof boldrPreset;
@@ -0,0 +1,25 @@
1
+ import { boldrColors } from './semantic.js';
2
+ /**
3
+ * Shared Tailwind preset. COLORS ONLY, by design.
4
+ *
5
+ * Must never define `content`, `plugins`, `presets`, or `darkMode`:
6
+ * NativeWind recurses `config.presets[].presets` hunting for its own marker
7
+ * preset, and a `content` key here would silently narrow the consuming app's
8
+ * globs.
9
+ *
10
+ * Consumed from CJS Tailwind configs, so this is a NAMED export with no
11
+ * default — that keeps ESM/CJS interop semantics irrelevant:
12
+ *
13
+ * const { boldrPreset } = require('@reekon-tools/boldr-utils/theme/preset');
14
+ * module.exports = { presets: [require('nativewind/preset'), boldrPreset] };
15
+ *
16
+ * Tailwind loads configs through jiti + sucrase, so requiring this ESM file
17
+ * from a CJS config works regardless of Node version.
18
+ *
19
+ * Note that Tailwind merges `theme.extend` per key with the *app* winning, and
20
+ * replaces a color key wholesale rather than deep-merging. An app that keeps
21
+ * its own `colors.background` will silently shadow this preset.
22
+ */
23
+ export const boldrPreset = {
24
+ theme: { extend: { colors: boldrColors } },
25
+ };
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Structural shape of a Tailwind `theme.extend.colors` tree.
3
+ *
4
+ * Declared locally on purpose: annotating with tailwindcss's own `Config` type
5
+ * would put `import type { Config } from 'tailwindcss'` into the emitted .d.ts
6
+ * and make tailwindcss a hard type dependency of this package.
7
+ */
8
+ export type ColorTree = {
9
+ [key: string]: string | ColorTree;
10
+ };
11
+ /**
12
+ * Semantic design tokens — the layer apps actually consume.
13
+ *
14
+ * `colors.ts` is the palette (raw Figma variables, named by value).
15
+ * This file maps those values onto *roles*, and is the only color surface the
16
+ * apps should reference. The raw palette is deliberately NOT exposed as Tailwind
17
+ * classes; naming by value is how both apps accumulated hundreds of arbitrary
18
+ * hex classes in the first place.
19
+ *
20
+ * The shadcn/ui token names (background, card, popover, foreground, muted,
21
+ * accent, secondary, border, input, ring, primary, destructive) are reused
22
+ * rather than paralleled, so every shadcn component is re-skinned without any
23
+ * component edits and `shadcn add` keeps generating markup that lands on these
24
+ * tokens.
25
+ *
26
+ * The design is dark-only. The light calculator / form surfaces are the
27
+ * explicitly-named `paper.*` tokens, not a light/dark variant pair — on native
28
+ * NativeWind's `dark:` variant is global, and portal-rendered surfaces
29
+ * (bottom sheets, dialogs, selects) mount outside any scoped subtree, so a
30
+ * scoped override would leak dark chrome onto a light page.
31
+ */
32
+ export declare const boldrColors: {
33
+ background: "#1D1F21";
34
+ foreground: {
35
+ DEFAULT: "#FFFFFF";
36
+ disabled: "#626364";
37
+ };
38
+ card: {
39
+ DEFAULT: "#2B2C2E";
40
+ foreground: "#FFFFFF";
41
+ };
42
+ popover: {
43
+ DEFAULT: "#2B2C2E";
44
+ foreground: "#FFFFFF";
45
+ };
46
+ muted: {
47
+ DEFAULT: "#424345";
48
+ foreground: "#9FA0A2";
49
+ };
50
+ accent: {
51
+ DEFAULT: "#424345";
52
+ foreground: "#FFFFFF";
53
+ };
54
+ secondary: {
55
+ DEFAULT: "#424345";
56
+ foreground: "#FFFFFF";
57
+ };
58
+ border: {
59
+ DEFAULT: "#424345";
60
+ strong: "#626364";
61
+ };
62
+ input: "#424345";
63
+ ring: "#FFAD00";
64
+ primary: {
65
+ DEFAULT: "#FFAD00";
66
+ foreground: "#1D1F21";
67
+ };
68
+ destructive: {
69
+ DEFAULT: "#DF6358";
70
+ foreground: "#1D1F21";
71
+ };
72
+ /** Chips, inset controls, thumbnail placeholders. */
73
+ elevated: {
74
+ DEFAULT: "#424345";
75
+ foreground: "#FFFFFF";
76
+ };
77
+ /**
78
+ * Steppers and other interactive fills. FILL ONLY — #0066FF as text on
79
+ * #2B2C2E is 2.89:1. Never `text-interactive`.
80
+ */
81
+ interactive: {
82
+ DEFAULT: "#0066FF";
83
+ foreground: "#FFFFFF";
84
+ };
85
+ /** Nav chrome (the web app's sidebar). */
86
+ nav: {
87
+ DEFAULT: "#2D3843";
88
+ foreground: "#FFFFFF";
89
+ };
90
+ paper: {
91
+ DEFAULT: "#E8EAED";
92
+ foreground: "#1D1F21";
93
+ field: "#FFFFFF";
94
+ line: "#BCBEC2";
95
+ muted: "#626364";
96
+ };
97
+ tile: {
98
+ /**
99
+ * Ink for any tile fill — `text-tile-foreground`. Shared across the
100
+ * measurement and formula variants because both carry white type; keeping it
101
+ * at the family level avoids two identical `foreground` keys that could
102
+ * drift apart by accident.
103
+ */
104
+ foreground: "#FFFFFF";
105
+ measurement: {
106
+ DEFAULT: "#0066FF";
107
+ badge: "#50A6FF";
108
+ };
109
+ formula: {
110
+ DEFAULT: "#BD30A8";
111
+ badge: "#6C155F";
112
+ };
113
+ count: {
114
+ DEFAULT: "#02274B";
115
+ foreground: "#FFFFFF";
116
+ };
117
+ };
118
+ diagram: {
119
+ 1: "#2E4C6A";
120
+ 2: "#4B6074";
121
+ 3: "#67819A";
122
+ };
123
+ tolerance: {
124
+ in: {
125
+ DEFAULT: "#58A942";
126
+ warning: "#00E920";
127
+ muted: "#8BB380";
128
+ };
129
+ out: {
130
+ DEFAULT: "#E83D2F";
131
+ warning: "#DA1100";
132
+ muted: "#DF6358";
133
+ };
134
+ };
135
+ };
136
+ export type BoldrColors = typeof boldrColors;
@@ -0,0 +1,116 @@
1
+ import { GeneralColors, SecondaryColors, FormColors, TileColors, FormulaColors, YellowColors, RedColors, GreenColors, } from './colors.js';
2
+ /**
3
+ * Semantic design tokens — the layer apps actually consume.
4
+ *
5
+ * `colors.ts` is the palette (raw Figma variables, named by value).
6
+ * This file maps those values onto *roles*, and is the only color surface the
7
+ * apps should reference. The raw palette is deliberately NOT exposed as Tailwind
8
+ * classes; naming by value is how both apps accumulated hundreds of arbitrary
9
+ * hex classes in the first place.
10
+ *
11
+ * The shadcn/ui token names (background, card, popover, foreground, muted,
12
+ * accent, secondary, border, input, ring, primary, destructive) are reused
13
+ * rather than paralleled, so every shadcn component is re-skinned without any
14
+ * component edits and `shadcn add` keeps generating markup that lands on these
15
+ * tokens.
16
+ *
17
+ * The design is dark-only. The light calculator / form surfaces are the
18
+ * explicitly-named `paper.*` tokens, not a light/dark variant pair — on native
19
+ * NativeWind's `dark:` variant is global, and portal-rendered surfaces
20
+ * (bottom sheets, dialogs, selects) mount outside any scoped subtree, so a
21
+ * scoped override would leak dark chrome onto a light page.
22
+ */
23
+ export const boldrColors = {
24
+ // ── dark app chrome — shadcn-compatible names, re-pointed ────────────────
25
+ background: GeneralColors.darkGrey, // screen + header
26
+ foreground: {
27
+ DEFAULT: GeneralColors.white,
28
+ disabled: GeneralColors.grey3, // text-foreground-disabled
29
+ },
30
+ card: { DEFAULT: GeneralColors.grey1, foreground: GeneralColors.white },
31
+ popover: { DEFAULT: GeneralColors.grey1, foreground: GeneralColors.white },
32
+ // muted / accent / secondary exist so unmodified shadcn components look
33
+ // right. Prefer `elevated` in new boldr components — it gives future
34
+ // divergence a seam.
35
+ muted: { DEFAULT: GeneralColors.grey2, foreground: GeneralColors.grey4 },
36
+ accent: { DEFAULT: GeneralColors.grey2, foreground: GeneralColors.white },
37
+ secondary: { DEFAULT: GeneralColors.grey2, foreground: GeneralColors.white },
38
+ border: { DEFAULT: GeneralColors.grey2, strong: GeneralColors.grey3 },
39
+ input: GeneralColors.grey2,
40
+ ring: YellowColors.mainYellow,
41
+ primary: {
42
+ DEFAULT: YellowColors.mainYellow,
43
+ // White on #FFAD00 is 1.87:1. Dark ink is 8.84:1, and matches the mockups.
44
+ foreground: GeneralColors.darkGrey,
45
+ },
46
+ destructive: {
47
+ DEFAULT: RedColors.mutedRed,
48
+ // Confirmed against the Settings frame: the Log Out label is dark ink.
49
+ // White on #DF6358 would be 3.46:1; #1D1F21 is 4.77:1.
50
+ foreground: GeneralColors.darkGrey,
51
+ },
52
+ // ── boldr additions the shadcn vocabulary has no word for ───────────────
53
+ /** Chips, inset controls, thumbnail placeholders. */
54
+ elevated: { DEFAULT: GeneralColors.grey2, foreground: GeneralColors.white },
55
+ /**
56
+ * Steppers and other interactive fills. FILL ONLY — #0066FF as text on
57
+ * #2B2C2E is 2.89:1. Never `text-interactive`.
58
+ */
59
+ interactive: {
60
+ DEFAULT: TileColors.activeBlue,
61
+ foreground: GeneralColors.white,
62
+ },
63
+ /** Nav chrome (the web app's sidebar). */
64
+ nav: {
65
+ DEFAULT: SecondaryColors.accentBlue1,
66
+ foreground: GeneralColors.white,
67
+ },
68
+ // ── light "paper": the calculator page and form fields ──────────────────
69
+ paper: {
70
+ DEFAULT: FormColors.lightGrey2, // bg-paper page / canvas
71
+ foreground: GeneralColors.darkGrey, // text-paper-foreground
72
+ field: GeneralColors.white, // bg-paper-field inputs, cards on paper
73
+ line: FormColors.lightGrey1, // border-paper-line field borders, dividers
74
+ // grey3, not lightGrey1: #BCBEC2 on #E8EAED is 1.54:1 — invisible.
75
+ // lightGrey1 is a border color only, which is why it's `paper.line`.
76
+ muted: GeneralColors.grey3, // text-paper-muted
77
+ },
78
+ // ── product tiles ───────────────────────────────────────────────────────
79
+ tile: {
80
+ /**
81
+ * Ink for any tile fill — `text-tile-foreground`. Shared across the
82
+ * measurement and formula variants because both carry white type; keeping it
83
+ * at the family level avoids two identical `foreground` keys that could
84
+ * drift apart by accident.
85
+ */
86
+ foreground: GeneralColors.white,
87
+ measurement: {
88
+ DEFAULT: TileColors.activeBlue,
89
+ badge: TileColors.lightBlue1,
90
+ },
91
+ // Note the badge is DARKER than the tile here, inverting the old app-local
92
+ // values. Confirmed in the mockups: a magenta tile with a darker magenta
93
+ // label bar.
94
+ formula: { DEFAULT: FormulaColors.purple, badge: FormulaColors.darkPurple },
95
+ count: { DEFAULT: TileColors.darkBlue, foreground: GeneralColors.white },
96
+ },
97
+ // ── 3D / diagram depth ramp (ordered; the steps have no individual meaning) ─
98
+ diagram: {
99
+ 1: SecondaryColors.accentBlue2,
100
+ 2: SecondaryColors.accentBlue3,
101
+ 3: SecondaryColors.accentBlue4,
102
+ },
103
+ // ── measurement tolerance ───────────────────────────────────────────────
104
+ tolerance: {
105
+ in: {
106
+ DEFAULT: GreenColors.green,
107
+ warning: GreenColors.warningGreen,
108
+ muted: GreenColors.mutedGreen,
109
+ },
110
+ out: {
111
+ DEFAULT: RedColors.red,
112
+ warning: RedColors.warningRed,
113
+ muted: RedColors.mutedRed,
114
+ },
115
+ },
116
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reekon-tools/boldr-utils",
3
- "version": "1.8.4",
3
+ "version": "1.9.0",
4
4
  "description": "Shared utilities for formulas and measurement conversion used in Reekon apps",
5
5
  "author": "REEKON Tools",
6
6
  "license": "MIT",
@@ -1,11 +0,0 @@
1
- import { type ReactNode } from 'react';
2
- import type { AnnotationCanvasInnerProps } from './AnnotationCanvasInner.js';
3
- export type { AnnotationCanvasHandle, GestureConfig, PanTrigger, } from './AnnotationCanvasInner.js';
4
- export interface CanvasKitOpts {
5
- locateFile?: (file: string) => string;
6
- }
7
- export type AnnotationCanvasProps = AnnotationCanvasInnerProps & {
8
- fallback?: ReactNode;
9
- canvasKitOpts?: CanvasKitOpts;
10
- };
11
- export declare const AnnotationCanvas: ({ fallback, canvasKitOpts, ...rest }: AnnotationCanvasProps) => import("react/jsx-runtime").JSX.Element;
@@ -1,10 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { WithSkiaWeb } from '@shopify/react-native-skia/lib/module/web/index.js';
3
- // Web-only entry point: lazy-loads CanvasKit wasm via WithSkiaWeb. Imperative
4
- // API (undo/redo/zoom) is exposed through the `imperativeRef` prop rather
5
- // than React refs, since WithSkiaWeb can't forward refs through its lazy
6
- // component boundary. The native build sits in `AnnotationCanvas.native.tsx`
7
- // and skips WithSkiaWeb entirely (no wasm to load).
8
- export const AnnotationCanvas = ({ fallback, canvasKitOpts, ...rest }) => (_jsx(WithSkiaWeb, { getComponent: () => import('./AnnotationCanvasInner.js').then((m) => ({
9
- default: m.AnnotationCanvasInner,
10
- })), fallback: fallback ?? null, componentProps: rest, opts: canvasKitOpts }));
@@ -1,8 +0,0 @@
1
- import type { AnnotationCanvasInnerProps } from './AnnotationCanvasInner.native.js';
2
- export type { AnnotationCanvasHandle, } from './useAnnotationCanvasState.js';
3
- export type { GestureConfig, PanTrigger, } from './AnnotationCanvasInner.js';
4
- export type AnnotationCanvasProps = AnnotationCanvasInnerProps & {
5
- fallback?: unknown;
6
- canvasKitOpts?: unknown;
7
- };
8
- export declare const AnnotationCanvas: (props: AnnotationCanvasProps) => import("react/jsx-runtime").JSX.Element;
@@ -1,6 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { AnnotationCanvasInner } from './AnnotationCanvasInner.native.js';
3
- export const AnnotationCanvas = (props) => {
4
- const { fallback: _f, canvasKitOpts: _o, ...rest } = props;
5
- return _jsx(AnnotationCanvasInner, { ...rest });
6
- };