@cosxai/ui 0.13.0 → 0.13.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosxai/ui",
3
- "version": "0.13.0",
3
+ "version": "0.13.2",
4
4
  "description": "COSX design system — React 19 component primitives shared across product-meta and other consumers",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -156,14 +156,53 @@ export const SignaturePad = forwardRef<SignaturePadHandle, SignaturePadProps>(
156
156
  return;
157
157
  }
158
158
  let cancelled = false;
159
- void fonts.ready.then(() => {
160
- if (cancelled) return;
161
- setFontReady(true);
162
- });
159
+ // Actively REQUEST the typed font. @font-face registration
160
+ // (fontsource CSS etc.) doesn't fetch the file until some
161
+ // rendered text uses it — and this component's only consumer
162
+ // of the font is the canvas, whose fillText silently falls
163
+ // back to the next family when the face isn't loaded yet.
164
+ // fonts.load() forces the fetch; fonts.ready is the safety
165
+ // net for browsers without it / faces not registered.
166
+ const kick =
167
+ typeof fonts.load === "function"
168
+ ? fonts.load(`32px ${typedFontFamily}`).catch(() => [])
169
+ : Promise.resolve([]);
170
+ void kick
171
+ .then(() => fonts.ready)
172
+ .then(() => {
173
+ if (cancelled) return;
174
+ setFontReady(true);
175
+ });
163
176
  return () => {
164
177
  cancelled = true;
165
178
  };
166
- }, [fontReady]);
179
+ // typedFontFamily is config-stable per consumer; re-kicking on
180
+ // change is correct anyway.
181
+ }, [fontReady, typedFontFamily]);
182
+
183
+ // Mode switch — clear the canvas so a stale typed baseline
184
+ // doesn't hang around behind a drawn signature (or vice versa).
185
+ // MUST be declared BEFORE the typed-repaint + drawn-restore
186
+ // effects: effects run in declaration order, and v0.13.0 had
187
+ // this clear declared LAST — on switching to typed mode with a
188
+ // prefilled name the repaint painted first and this then wiped
189
+ // it (QA 2026-07-17: Type tab showed an empty pad until the
190
+ // signer edited the name). Skips the mount run so a parent-held
191
+ // captured value isn't nulled on first render.
192
+ const prevModeRef = useRef(mode);
193
+ useEffect(() => {
194
+ if (prevModeRef.current === mode) return;
195
+ prevModeRef.current = mode;
196
+ const canvas = canvasRef.current;
197
+ if (!canvas) return;
198
+ const ctx = canvas.getContext("2d");
199
+ if (!ctx) return;
200
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
201
+ dirtyRef.current = false;
202
+ onChange(null);
203
+ // Only fire when mode itself changes.
204
+ // eslint-disable-next-line react-hooks/exhaustive-deps
205
+ }, [mode]);
167
206
 
168
207
  // Repaint on typedName / mode / fontReady change. Drawn mode
169
208
  // doesn't repaint imperatively — strokes are appended as the user
@@ -233,20 +272,6 @@ export const SignaturePad = forwardRef<SignaturePadHandle, SignaturePadProps>(
233
272
  // eslint-disable-next-line react-hooks/exhaustive-deps
234
273
  }, [mode]);
235
274
 
236
- // Mode switch — clear the canvas so a stale typed baseline
237
- // doesn't hang around behind a drawn signature (or vice versa).
238
- useEffect(() => {
239
- const canvas = canvasRef.current;
240
- if (!canvas) return;
241
- const ctx = canvas.getContext("2d");
242
- if (!ctx) return;
243
- ctx.clearRect(0, 0, canvas.width, canvas.height);
244
- dirtyRef.current = false;
245
- onChange(null);
246
- // Only fire when mode itself changes.
247
- // eslint-disable-next-line react-hooks/exhaustive-deps
248
- }, [mode]);
249
-
250
275
  const clear = useCallback(() => {
251
276
  const canvas = canvasRef.current;
252
277
  if (canvas) {