ugcinc 4.5.73 → 4.5.74

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.
@@ -15,12 +15,16 @@ export interface TextElementProps {
15
15
  * - Font family, size, weight, color
16
16
  * - Letter spacing, line height
17
17
  * - Horizontal and vertical alignment
18
- * - Text stroke/outline
18
+ * - Text stroke/outline (via -webkit-text-stroke + paint-order)
19
19
  * - Background with opacity and border radius
20
20
  * - Padding (uniform and individual)
21
21
  * - Auto-width with box alignment
22
22
  * - Rotation
23
23
  *
24
+ * Stroke rendering uses -webkit-text-stroke with paint-order: stroke fill.
25
+ * This naturally excludes emoji bitmaps from the outline (only text glyphs
26
+ * are stroked) and avoids the layout issues of a separate stroke layer.
27
+ *
24
28
  * When autoWidth is enabled:
25
29
  * - Text wraps at maxWidth (the element's width)
26
30
  * - Background shrinks to the widest line
@@ -45,7 +45,6 @@ const react_1 = __importStar(require("react"));
45
45
  const defaults_1 = require("../utils/defaults");
46
46
  const fonts_1 = require("../utils/fonts");
47
47
  const text_1 = require("../utils/text");
48
- const emoji_1 = require("../utils/emoji");
49
48
  /**
50
49
  * Calculate the actual width for auto-width text AND the line breaks.
51
50
  * Uses DOM-based measurement to ensure the same fonts are used as CSS rendering.
@@ -162,73 +161,21 @@ function calculateAutoWidthAndLines({ text, maxWidth, paddingLeft, paddingRight,
162
161
  const calculatedWidth = Math.min(widestLineWidth + paddingLeft + paddingRight, maxWidth);
163
162
  return { width: calculatedWidth, lines };
164
163
  }
165
- /** Word Joiner (U+2060) — zero-width character that prevents line breaks */
166
- const WJ = '\u2060';
167
- /** Style for emoji spans in the fill layer — no visual change */
168
- const emojiSpanStyle = { display: 'inline' };
169
- /** Style for emoji spans in the stroke layer — hidden from SVG filter */
170
- const emojiSpanStrokeStyle = { display: 'inline', opacity: 0 };
171
- /**
172
- * Group consecutive parts from splitTextAndEmojis into merged groups.
173
- * Consecutive emoji parts are joined into a single group so they share one span,
174
- * reducing break opportunities.
175
- */
176
- function groupEmojiParts(parts) {
177
- const groups = [];
178
- for (const part of parts) {
179
- const last = groups[groups.length - 1];
180
- if (last && last.type === part.type) {
181
- last.content += part.content;
182
- }
183
- else {
184
- groups.push({ type: part.type, content: part.content });
185
- }
186
- }
187
- return groups;
188
- }
189
- /**
190
- * Render text with emojis wrapped in spans + Word Joiners at span boundaries.
191
- *
192
- * Both fill and stroke divs use this so they have identical DOM structure
193
- * (matching vertical metrics and line breaks). The only difference is the
194
- * emoji span style: fill spans are visible, stroke spans have opacity: 0.
195
- *
196
- * Word Joiners (U+2060) are inserted at boundaries between text nodes and
197
- * emoji spans to prevent the browser from introducing break opportunities
198
- * that don't exist in the original plain text.
199
- */
200
- function renderTextWithEmojiSpans(text, forStroke) {
201
- if (!(0, emoji_1.hasEmoji)(text))
202
- return text;
203
- const parts = (0, emoji_1.splitTextAndEmojis)(text);
204
- const groups = groupEmojiParts(parts);
205
- const style = forStroke ? emojiSpanStrokeStyle : emojiSpanStyle;
206
- const result = [];
207
- for (let i = 0; i < groups.length; i++) {
208
- const group = groups[i];
209
- // Insert Word Joiner between adjacent groups to prevent unwanted line breaks
210
- if (i > 0)
211
- result.push(WJ);
212
- if (group.type === 'emoji') {
213
- result.push((0, jsx_runtime_1.jsx)("span", { style: style, children: group.content }, i));
214
- }
215
- else {
216
- result.push(group.content);
217
- }
218
- }
219
- return result;
220
- }
221
164
  /**
222
165
  * TextElement renders text with full styling support including:
223
166
  * - Font family, size, weight, color
224
167
  * - Letter spacing, line height
225
168
  * - Horizontal and vertical alignment
226
- * - Text stroke/outline
169
+ * - Text stroke/outline (via -webkit-text-stroke + paint-order)
227
170
  * - Background with opacity and border radius
228
171
  * - Padding (uniform and individual)
229
172
  * - Auto-width with box alignment
230
173
  * - Rotation
231
174
  *
175
+ * Stroke rendering uses -webkit-text-stroke with paint-order: stroke fill.
176
+ * This naturally excludes emoji bitmaps from the outline (only text glyphs
177
+ * are stroked) and avoids the layout issues of a separate stroke layer.
178
+ *
232
179
  * When autoWidth is enabled:
233
180
  * - Text wraps at maxWidth (the element's width)
234
181
  * - Background shrinks to the widest line
@@ -376,8 +323,7 @@ function TextElement({ segment, scale = 1 }) {
376
323
  };
377
324
  }, [autoWidth, calculatedWidth, width, backgroundColor, backgroundOpacity, borderRadiusStyle]);
378
325
  const hasStroke = strokeWidth > 0 && !!strokeColor;
379
- const filterId = `te-stroke-${react_1.default.useId().replace(/:/g, '')}`;
380
- // Text style (fill layer)
326
+ // Text style — single div handles both fill and stroke via paint-order
381
327
  const textStyle = (0, react_1.useMemo)(() => ({
382
328
  fontFamily,
383
329
  fontSize,
@@ -396,45 +342,27 @@ function TextElement({ segment, scale = 1 }) {
396
342
  flex: 1,
397
343
  minHeight: 0,
398
344
  }),
399
- ...(hasStroke && { position: 'relative', zIndex: 1 }),
345
+ ...(hasStroke && {
346
+ WebkitTextStroke: `${strokeWidth}px ${strokeColor}`,
347
+ paintOrder: 'stroke fill',
348
+ }),
400
349
  }), [
401
350
  fontFamily, fontSize, fontWeight, color, lineHeight, letterSpacing, alignment,
402
351
  paddingTop, paddingRight, paddingBottom, paddingLeft,
403
- autoWidth, verticalAlign, hasStroke
352
+ autoWidth, verticalAlign, hasStroke, strokeWidth, strokeColor
404
353
  ]);
405
- // Stroke underlay style — same size as fill div, SVG filter handles overflow.
406
- const strokeTextStyle = (0, react_1.useMemo)(() => {
407
- if (!hasStroke)
408
- return undefined;
409
- return {
410
- ...textStyle,
411
- color: strokeColor,
412
- filter: `url(#${filterId})`,
413
- position: 'absolute',
414
- zIndex: 0,
415
- top: 0,
416
- left: 0,
417
- right: 0,
418
- bottom: 0,
419
- };
420
- }, [hasStroke, textStyle, strokeColor, filterId]);
421
- // SVG filter for smooth text outline (avoids miter spikes from -webkit-text-stroke)
422
- const strokePadPct = Math.max(50, Math.ceil((strokeWidth / fontSize) * 200));
423
- const strokeFilterSvg = hasStroke ? ((0, jsx_runtime_1.jsx)("svg", { style: { position: 'absolute', width: 0, height: 0, overflow: 'hidden' }, "aria-hidden": "true", children: (0, jsx_runtime_1.jsx)("defs", { children: (0, jsx_runtime_1.jsxs)("filter", { id: filterId, x: `-${strokePadPct}%`, y: `-${strokePadPct}%`, width: `${100 + 2 * strokePadPct}%`, height: `${100 + 2 * strokePadPct}%`, children: [(0, jsx_runtime_1.jsx)("feMorphology", { in: "SourceAlpha", operator: "dilate", radius: strokeWidth, result: "dilated" }), (0, jsx_runtime_1.jsx)("feFlood", { floodColor: strokeColor, result: "color" }), (0, jsx_runtime_1.jsx)("feComposite", { in: "color", in2: "dilated", operator: "in" })] }) }) })) : null;
424
354
  if (autoWidth) {
425
- const textContent = calculatedLines.map((line, index) => ((0, jsx_runtime_1.jsxs)(react_1.default.Fragment, { children: [renderTextWithEmojiSpans(line, false), index < calculatedLines.length - 1 && (0, jsx_runtime_1.jsx)("br", {})] }, index)));
426
- const strokeContent = calculatedLines.map((line, index) => ((0, jsx_runtime_1.jsxs)(react_1.default.Fragment, { children: [renderTextWithEmojiSpans(line, true), index < calculatedLines.length - 1 && (0, jsx_runtime_1.jsx)("br", {})] }, index)));
355
+ const textContent = calculatedLines.map((line, index) => ((0, jsx_runtime_1.jsxs)(react_1.default.Fragment, { children: [line, index < calculatedLines.length - 1 && (0, jsx_runtime_1.jsx)("br", {})] }, index)));
427
356
  if (backgroundColor) {
428
- return ((0, jsx_runtime_1.jsx)("div", { style: positioningContainerStyle, children: (0, jsx_runtime_1.jsxs)("div", { style: {
357
+ return ((0, jsx_runtime_1.jsx)("div", { style: positioningContainerStyle, children: (0, jsx_runtime_1.jsx)("div", { style: {
429
358
  width: calculatedWidth,
430
359
  maxWidth: width,
431
360
  backgroundColor: (0, text_1.hexToRgba)(backgroundColor, backgroundOpacity),
432
361
  borderRadius: borderRadiusStyle,
433
- ...(hasStroke && { position: 'relative' }),
434
- }, children: [strokeFilterSvg, strokeTextStyle && (0, jsx_runtime_1.jsx)("div", { style: strokeTextStyle, "aria-hidden": "true", children: strokeContent }), (0, jsx_runtime_1.jsx)("div", { style: textStyle, children: textContent })] }) }));
362
+ }, children: (0, jsx_runtime_1.jsx)("div", { style: textStyle, children: textContent }) }) }));
435
363
  }
436
- return ((0, jsx_runtime_1.jsx)("div", { style: positioningContainerStyle, children: (0, jsx_runtime_1.jsxs)("div", { style: { width: calculatedWidth, maxWidth: width, ...(hasStroke && { position: 'relative' }) }, children: [strokeFilterSvg, strokeTextStyle && (0, jsx_runtime_1.jsx)("div", { style: strokeTextStyle, "aria-hidden": "true", children: strokeContent }), (0, jsx_runtime_1.jsx)("div", { style: textStyle, children: textContent })] }) }));
364
+ return ((0, jsx_runtime_1.jsx)("div", { style: positioningContainerStyle, children: (0, jsx_runtime_1.jsx)("div", { style: { width: calculatedWidth, maxWidth: width }, children: (0, jsx_runtime_1.jsx)("div", { style: textStyle, children: textContent }) }) }));
437
365
  }
438
- return ((0, jsx_runtime_1.jsx)("div", { style: positioningContainerStyle, children: (0, jsx_runtime_1.jsxs)("div", { style: { ...backgroundBoxStyle, ...(hasStroke && { position: 'relative' }) }, children: [strokeFilterSvg, strokeTextStyle && (0, jsx_runtime_1.jsx)("div", { style: strokeTextStyle, "aria-hidden": "true", children: renderTextWithEmojiSpans(segment.text, true) }), (0, jsx_runtime_1.jsx)("div", { style: textStyle, children: renderTextWithEmojiSpans(segment.text, false) })] }) }));
366
+ return ((0, jsx_runtime_1.jsx)("div", { style: positioningContainerStyle, children: (0, jsx_runtime_1.jsx)("div", { style: backgroundBoxStyle, children: (0, jsx_runtime_1.jsx)("div", { style: textStyle, children: segment.text }) }) }));
439
367
  }
440
368
  exports.default = TextElement;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ugcinc",
3
- "version": "4.5.73",
3
+ "version": "4.5.74",
4
4
  "description": "TypeScript/JavaScript client for the UGC Inc API",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",