@hjmds/design-contracts 1.13.0 → 1.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.
- package/dist/agreement.d.ts +7 -1
- package/dist/agreement.d.ts.map +1 -1
- package/dist/agreement.js +14 -3
- package/dist/agreement.js.map +1 -1
- package/dist/behaviors.d.ts +1 -1
- package/dist/catalog.d.ts +5 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/component-recipes.d.ts +14 -0
- package/dist/component-recipes.d.ts.map +1 -1
- package/dist/component-recipes.js +16 -1
- package/dist/component-recipes.js.map +1 -1
- package/dist/content-transition.d.ts +17 -0
- package/dist/content-transition.d.ts.map +1 -1
- package/dist/content-transition.js +21 -0
- package/dist/content-transition.js.map +1 -1
- package/dist/date-entry.d.ts +71 -0
- package/dist/date-entry.d.ts.map +1 -0
- package/dist/date-entry.js +78 -0
- package/dist/date-entry.js.map +1 -0
- package/dist/document-resource.d.ts +104 -0
- package/dist/document-resource.d.ts.map +1 -0
- package/dist/document-resource.js +77 -0
- package/dist/document-resource.js.map +1 -0
- package/dist/effect-surface.d.ts +6 -1
- package/dist/effect-surface.d.ts.map +1 -1
- package/dist/effect-surface.js +5 -2
- package/dist/effect-surface.js.map +1 -1
- package/dist/field-group.d.ts +47 -0
- package/dist/field-group.d.ts.map +1 -0
- package/dist/field-group.js +92 -0
- package/dist/field-group.js.map +1 -0
- package/dist/image.d.ts +17 -0
- package/dist/image.d.ts.map +1 -1
- package/dist/image.js +19 -0
- package/dist/image.js.map +1 -1
- package/dist/internal/effect-noise.d.ts +2 -0
- package/dist/internal/effect-noise.d.ts.map +1 -0
- package/dist/internal/effect-noise.js +4 -0
- package/dist/internal/effect-noise.js.map +1 -0
- package/dist/progressive-blur.d.ts +32 -0
- package/dist/progressive-blur.d.ts.map +1 -0
- package/dist/progressive-blur.js +28 -0
- package/dist/progressive-blur.js.map +1 -0
- package/dist/reference-controls.d.ts +32 -0
- package/dist/reference-controls.d.ts.map +1 -0
- package/dist/reference-controls.js +28 -0
- package/dist/reference-controls.js.map +1 -0
- package/dist/scroll-progress.d.ts +7 -1
- package/dist/scroll-progress.d.ts.map +1 -1
- package/dist/scroll-progress.js +21 -2
- package/dist/scroll-progress.js.map +1 -1
- package/dist/text-annotation.d.ts +43 -0
- package/dist/text-annotation.d.ts.map +1 -0
- package/dist/text-annotation.js +137 -0
- package/dist/text-annotation.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/docs/agreement.md +14 -0
- package/docs/dialog.md +16 -1
- package/docs/effect-surface.md +19 -3
- package/docs/generated/component-maturity.md +1 -1
- package/docs/generated/renderer-evidence.json +3 -3
- package/docs/generated/renderer-evidence.md +1 -1
- package/docs/generated/showcase-manifest.json +1 -1
- package/docs/image.md +17 -0
- package/docs/optional-adapters.md +25 -0
- package/docs/popover.md +15 -0
- package/docs/rating.md +6 -1
- package/docs/reference-controls.md +40 -0
- package/docs/screen-patterns.md +2 -0
- package/docs/sheet.md +9 -0
- package/docs/task-list.md +15 -1
- package/docs/text-annotation.md +85 -0
- package/docs/toggle-group.md +6 -0
- package/docs/usage/README.md +18 -1
- package/docs/usage/components/agreement.md +13 -3
- package/docs/usage/components/alert-dialog.md +7 -1
- package/docs/usage/components/avatar.md +33 -1
- package/docs/usage/components/card.md +7 -1
- package/docs/usage/components/carousel.md +5 -1
- package/docs/usage/components/chat-message.md +11 -1
- package/docs/usage/components/chip.md +10 -2
- package/docs/usage/components/content-transition.md +28 -4
- package/docs/usage/components/dialog.md +18 -1
- package/docs/usage/components/effect-surface.md +4 -2
- package/docs/usage/components/empty-state.md +10 -2
- package/docs/usage/components/field.md +23 -0
- package/docs/usage/components/form.md +6 -1
- package/docs/usage/components/image-comparison.md +82 -0
- package/docs/usage/components/image.md +81 -2
- package/docs/usage/components/keyboard-avoiding.md +6 -1
- package/docs/usage/components/link.md +3 -1
- package/docs/usage/components/list-row.md +3 -1
- package/docs/usage/components/list.md +6 -1
- package/docs/usage/components/message-composer.md +5 -0
- package/docs/usage/components/popover.md +7 -1
- package/docs/usage/components/progress.md +28 -0
- package/docs/usage/components/progressive-blur.md +113 -0
- package/docs/usage/components/rating.md +74 -0
- package/docs/usage/components/search-field.md +10 -0
- package/docs/usage/components/search-screen.md +16 -5
- package/docs/usage/components/segmented-control.md +29 -4
- package/docs/usage/components/sheet.md +7 -1
- package/docs/usage/components/statistic.md +11 -0
- package/docs/usage/components/tags-input.md +5 -0
- package/docs/usage/components/toast.md +5 -0
- package/docs/usage/components/upload-item.md +3 -1
- package/docs/usage/compositions/action-feedback.md +77 -0
- package/docs/usage/compositions/adaptive-content.md +81 -0
- package/docs/usage/compositions/context-toolbar.md +85 -0
- package/docs/usage/compositions/date-entry.md +108 -0
- package/docs/usage/compositions/document-resource.md +124 -0
- package/docs/usage/compositions/field-group.md +104 -0
- package/docs/usage/compositions/illustrated-outcome.md +91 -0
- package/docs/usage/compositions/live-list.md +104 -0
- package/docs/usage/compositions/optional-adapters.md +1 -1
- package/docs/usage/compositions/origin-dialog.md +108 -0
- package/docs/usage/compositions/selection-motion.md +73 -0
- package/docs/usage/compositions/texture-comparison.md +86 -0
- package/docs/usage/compositions/upload-recovery.md +80 -0
- package/docs/usage/compositions/video-dialog.md +100 -0
- package/docs/usage/screens/common-search.md +7 -2
- package/docs/usage/screens/flow-onboarding.md +5 -3
- package/docs/usage/screens/product-bento.md +117 -0
- package/package.json +37 -1
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Experimental drawing geometry, not a public TextFormat variant. A renderer
|
|
3
|
+
* supplies measured visual line rectangles; estimating widths from character
|
|
4
|
+
* counts would break proportional fonts, Korean wrapping, and bidirectional text.
|
|
5
|
+
* See docs/text-annotation.md for the unfinished renderer boundary.
|
|
6
|
+
*/
|
|
7
|
+
export const textAnnotationActions = [
|
|
8
|
+
'highlight', 'underline', 'box', 'circle', 'strike-through', 'crossed-off', 'bracket',
|
|
9
|
+
];
|
|
10
|
+
function finite(value, name) {
|
|
11
|
+
if (!Number.isFinite(value))
|
|
12
|
+
throw new RangeError(`${name} must be finite`);
|
|
13
|
+
}
|
|
14
|
+
/** Merge adjacent font/bidi runs only within their measured visual line. A gap
|
|
15
|
+
* in a partial selection stays unpainted even when both runs share a line.
|
|
16
|
+
* Hosts supply line identity from the same layout engine that paints the text.
|
|
17
|
+
*/
|
|
18
|
+
export function mergeTextAnnotationFragments(fragments) {
|
|
19
|
+
for (const fragment of fragments) {
|
|
20
|
+
for (const name of ['x', 'y', 'width', 'height'])
|
|
21
|
+
finite(fragment[name], name);
|
|
22
|
+
if (fragment.width < 0 || fragment.height < 0 || !Number.isSafeInteger(fragment.lineIndex) || fragment.lineIndex < 0)
|
|
23
|
+
throw new RangeError('Invalid text annotation fragment');
|
|
24
|
+
finite(fragment.x + fragment.width, 'Fragment right');
|
|
25
|
+
finite(fragment.y + fragment.height, 'Fragment bottom');
|
|
26
|
+
}
|
|
27
|
+
const ordered = fragments.filter(fragment => fragment.width > 0 && fragment.height > 0).map(fragment => ({ ...fragment })).sort((a, b) => a.lineIndex - b.lineIndex || a.x - b.x);
|
|
28
|
+
const merged = [];
|
|
29
|
+
for (const fragment of ordered) {
|
|
30
|
+
const previous = merged[merged.length - 1];
|
|
31
|
+
// Skia exposes Float32 edges: measured adjacent runs differed by ~0.00001px.
|
|
32
|
+
// 0.01 logical px absorbs this rounding, not a visible inter-word/selection gap.
|
|
33
|
+
if (previous && previous.lineIndex === fragment.lineIndex && fragment.x <= previous.x + previous.width + 0.01) {
|
|
34
|
+
const y = Math.min(previous.y, fragment.y);
|
|
35
|
+
merged[merged.length - 1] = { lineIndex: previous.lineIndex, x: previous.x, y, width: Math.max(previous.x + previous.width, fragment.x + fragment.width) - previous.x, height: Math.max(previous.y + previous.height, fragment.y + fragment.height) - y };
|
|
36
|
+
}
|
|
37
|
+
else
|
|
38
|
+
merged.push(fragment);
|
|
39
|
+
}
|
|
40
|
+
return merged.map(({ x, y, width, height }) => {
|
|
41
|
+
finite(width, 'Merged width');
|
|
42
|
+
finite(height, 'Merged height');
|
|
43
|
+
return { x, y, width, height };
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Small deterministic offsets give the two passes a hand-drawn character.
|
|
48
|
+
* Random-per-render geometry was rejected: unrelated state updates must not
|
|
49
|
+
* make the annotation jump or create server/client output differences.
|
|
50
|
+
*/
|
|
51
|
+
export function resolveTextAnnotationGeometry(lines, action, options = {}) {
|
|
52
|
+
if (!textAnnotationActions.includes(action))
|
|
53
|
+
throw new TypeError('Unknown text annotation action');
|
|
54
|
+
// These are drawing parameters, not layout spacing. 1.5px keeps a doubled
|
|
55
|
+
// outline lighter than a focus ring; the 2px gap avoids touching glyph edges.
|
|
56
|
+
const strokeWidth = options.strokeWidth ?? 1.5;
|
|
57
|
+
const padding = options.padding ?? 2;
|
|
58
|
+
finite(strokeWidth, 'strokeWidth');
|
|
59
|
+
finite(padding, 'padding');
|
|
60
|
+
if (strokeWidth <= 0)
|
|
61
|
+
throw new RangeError('strokeWidth must be positive');
|
|
62
|
+
if (padding < 0)
|
|
63
|
+
throw new RangeError('padding must not be negative');
|
|
64
|
+
const paths = [];
|
|
65
|
+
let bounds = null;
|
|
66
|
+
for (const [lineIndex, line] of lines.entries()) {
|
|
67
|
+
for (const name of ['x', 'y', 'width', 'height'])
|
|
68
|
+
finite(line[name], name);
|
|
69
|
+
if (line.width < 0 || line.height < 0)
|
|
70
|
+
throw new RangeError('Line dimensions must not be negative');
|
|
71
|
+
// Empty lines and a not-yet-laid-out host do not produce stray marker dots.
|
|
72
|
+
if (line.width === 0 || line.height === 0)
|
|
73
|
+
continue;
|
|
74
|
+
// Scale the wobble down for tiny fragments; it must not swallow punctuation.
|
|
75
|
+
const wobble = Math.min(1, line.width / 8, line.height / 8);
|
|
76
|
+
// The surrounding loop must clear the actual text even if callers choose
|
|
77
|
+
// zero padding or a thick pen. Strike-through/crossed-off intentionally do not.
|
|
78
|
+
const gap = action === 'circle' ? Math.max(padding, strokeWidth / 2 + wobble) : padding;
|
|
79
|
+
const l = line.x - gap, r = line.x + line.width + gap;
|
|
80
|
+
const t = line.y - gap, b = line.y + line.height + gap;
|
|
81
|
+
const cx = l + (r - l) / 2, cy = t + (b - t) / 2;
|
|
82
|
+
// A modest outward bow keeps a loop distinct from the box without extending
|
|
83
|
+
// an ellipse by 40% of a long line's width into neighboring text or the screen edge.
|
|
84
|
+
const bow = action === 'circle' ? Math.min(line.width, line.height) * 0.12 : 0;
|
|
85
|
+
const add = (d, paint) => paths.push({ d, paint, strokeWidth, lineIndex });
|
|
86
|
+
if (action === 'highlight') {
|
|
87
|
+
add(`M ${l} ${t + wobble} Q ${cx} ${t - wobble} ${r} ${t} L ${r - wobble} ${b} Q ${cx} ${b + wobble} ${l} ${b - wobble} Z`, 'fill');
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
for (const offset of [-wobble / 2, wobble / 2]) {
|
|
91
|
+
const x1 = l + offset, x2 = r + offset, y1 = t + offset, y2 = b + offset;
|
|
92
|
+
switch (action) {
|
|
93
|
+
case 'underline':
|
|
94
|
+
add(`M ${x1} ${y2} Q ${cx} ${y2 + wobble} ${x2} ${y2 - wobble / 2}`, 'stroke');
|
|
95
|
+
break;
|
|
96
|
+
case 'strike-through':
|
|
97
|
+
add(`M ${x1} ${cy + offset} Q ${cx} ${cy - wobble} ${x2} ${cy + offset}`, 'stroke');
|
|
98
|
+
break;
|
|
99
|
+
case 'crossed-off':
|
|
100
|
+
add(`M ${x1} ${y1} Q ${cx} ${cy - wobble} ${x2} ${y2} M ${x2} ${y1} Q ${cx} ${cy + wobble} ${x1} ${y2}`, 'stroke');
|
|
101
|
+
break;
|
|
102
|
+
case 'box':
|
|
103
|
+
add(`M ${x1} ${y1} Q ${cx} ${y1 - wobble} ${x2} ${y1} L ${x2} ${y2} Q ${cx} ${y2 + wobble} ${x1} ${y2} Z`, 'stroke');
|
|
104
|
+
break;
|
|
105
|
+
case 'circle':
|
|
106
|
+
// The original inscribed ellipse cut through end glyphs at 32px.
|
|
107
|
+
// Bow each edge outward through the padded corners instead: every
|
|
108
|
+
// segment stays outside the text rectangle while retaining a loose loop.
|
|
109
|
+
add(`M ${x1} ${cy} Q ${x1 - bow} ${y1} ${x1} ${y1} Q ${cx} ${y1 - bow} ${x2} ${y1} Q ${x2 + bow} ${cy} ${x2} ${y2} Q ${cx} ${y2 + bow} ${x1} ${y2} Q ${x1 - bow} ${cy} ${x1} ${cy} Z`, 'stroke');
|
|
110
|
+
break;
|
|
111
|
+
case 'bracket': {
|
|
112
|
+
const arm = Math.min(line.height / 3, line.width / 4);
|
|
113
|
+
add(`M ${x1 + arm} ${y1} L ${x1} ${y1} L ${x1} ${y2} L ${x1 + arm} ${y2} M ${x2 - arm} ${y1} L ${x2} ${y1} L ${x2} ${y2} L ${x2 - arm} ${y2}`, 'stroke');
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Include control points, both sketch passes and stroke caps so the SVG
|
|
120
|
+
// viewBox cannot clip an underline or a bracket at large text sizes.
|
|
121
|
+
const outset = Math.max(2 * wobble, bow + wobble / 2) + strokeWidth / 2;
|
|
122
|
+
const left = l - outset, top = t - outset, right = r + outset, bottom = b + outset;
|
|
123
|
+
if (!bounds)
|
|
124
|
+
bounds = { x: left, y: top, width: right - left, height: bottom - top };
|
|
125
|
+
else {
|
|
126
|
+
const x = Math.min(bounds.x, left), y = Math.min(bounds.y, top);
|
|
127
|
+
bounds = { x, y, width: Math.max(bounds.x + bounds.width, right) - x, height: Math.max(bounds.y + bounds.height, bottom) - y };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Overflow can occur even with finite inputs. Reject it here instead of
|
|
131
|
+
// handing NaN/Infinity geometry to a native SVG implementation.
|
|
132
|
+
if (bounds)
|
|
133
|
+
for (const value of Object.values(bounds))
|
|
134
|
+
finite(value, 'Annotation bounds');
|
|
135
|
+
return { paths, bounds };
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=text-annotation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-annotation.js","sourceRoot":"","sources":["../src/text-annotation.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC,WAAW,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ,EAAE,gBAAgB,EAAE,aAAa,EAAE,SAAS;CAC7E,CAAC;AAgBX,SAAS,MAAM,CAAC,KAAa,EAAE,IAAY;IACzC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,GAAG,IAAI,iBAAiB,CAAC,CAAC;AAC9E,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,4BAA4B,CAAC,SAA4C;IACvF,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,KAAK,MAAM,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,QAAQ,CAAU;YAAE,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC;QACxF,IAAI,QAAQ,CAAC,KAAK,GAAG,CAAC,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,QAAQ,CAAC,SAAS,GAAG,CAAC;YAAE,MAAM,IAAI,UAAU,CAAC,kCAAkC,CAAC,CAAC;QAC/K,MAAM,CAAC,QAAQ,CAAC,CAAC,GAAG,QAAQ,CAAC,KAAK,EAAE,gBAAgB,CAAC,CAAC;QAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IACjH,CAAC;IACD,MAAM,OAAO,GAAG,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,CAAC,KAAK,GAAG,CAAC,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,QAAQ,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAClL,MAAM,MAAM,GAA6B,EAAE,CAAC;IAC5C,KAAK,MAAM,QAAQ,IAAI,OAAO,EAAE,CAAC;QAC/B,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAC3C,6EAA6E;QAC7E,iFAAiF;QACjF,IAAI,QAAQ,IAAI,QAAQ,CAAC,SAAS,KAAK,QAAQ,CAAC,SAAS,IAAI,QAAQ,CAAC,CAAC,IAAI,QAAQ,CAAC,CAAC,GAAG,QAAQ,CAAC,KAAK,GAAG,IAAI,EAAE,CAAC;YAC9G,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;YAC3C,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,EAAE,SAAS,EAAE,QAAQ,CAAC,SAAS,EAAE,CAAC,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,GAAG,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,GAAG,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5P,CAAC;;YAAM,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC/B,CAAC;IACD,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE;QAC5C,MAAM,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC;QAAC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;QAC/D,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IACjC,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,6BAA6B,CAC3C,KAAoC,EACpC,MAA4B,EAC5B,UAAgE,EAAE;IAElE,IAAI,CAAE,qBAA2C,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,MAAM,IAAI,SAAS,CAAC,gCAAgC,CAAC,CAAC;IAC1H,0EAA0E;IAC1E,8EAA8E;IAC9E,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,GAAG,CAAC;IAC/C,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,CAAC,CAAC;IACrC,MAAM,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;IAAC,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IAC/D,IAAI,WAAW,IAAI,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,8BAA8B,CAAC,CAAC;IAC3E,IAAI,OAAO,GAAG,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,8BAA8B,CAAC,CAAC;IACtE,MAAM,KAAK,GAAyB,EAAE,CAAC;IACvC,IAAI,MAAM,GAA8B,IAAI,CAAC;IAC7C,KAAK,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,IAAI,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;QAChD,KAAK,MAAM,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,QAAQ,CAAU;YAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC;QACpF,IAAI,IAAI,CAAC,KAAK,GAAG,CAAC,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;YAAE,MAAM,IAAI,UAAU,CAAC,sCAAsC,CAAC,CAAC;QACpG,4EAA4E;QAC5E,IAAI,IAAI,CAAC,KAAK,KAAK,CAAC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QACpD,6EAA6E;QAC7E,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,GAAG,CAAC,EAAE,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAC5D,yEAAyE;QACzE,gFAAgF;QAChF,MAAM,GAAG,GAAG,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,WAAW,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;QACxF,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,GAAG,GAAG,CAAC;QACtD,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC;QACvD,MAAM,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;QACjD,4EAA4E;QAC5E,qFAAqF;QACrF,MAAM,GAAG,GAAG,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QAC/E,MAAM,GAAG,GAAG,CAAC,CAAS,EAAE,KAAwB,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,CAAC,CAAC;QACtG,IAAI,MAAM,KAAK,WAAW,EAAE,CAAC;YAC3B,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,MAAM,MAAM,EAAE,IAAI,CAAC,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,MAAM,IAAI,EAAE,MAAM,CAAC,CAAC;QACtI,CAAC;aAAM,CAAC;YACN,KAAK,MAAM,MAAM,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC;gBAC/C,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,EAAE,EAAE,GAAG,CAAC,GAAG,MAAM,EAAE,EAAE,GAAG,CAAC,GAAG,MAAM,EAAE,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC;gBACzE,QAAQ,MAAM,EAAE,CAAC;oBACf,KAAK,WAAW;wBACd,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,IAAI,EAAE,IAAI,EAAE,GAAG,MAAM,GAAG,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;wBAAC,MAAM;oBACxF,KAAK,gBAAgB;wBACnB,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,IAAI,EAAE,IAAI,EAAE,GAAG,MAAM,EAAE,EAAE,QAAQ,CAAC,CAAC;wBAAC,MAAM;oBAC7F,KAAK,aAAa;wBAChB,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,QAAQ,CAAC,CAAC;wBAAC,MAAM;oBAC5H,KAAK,KAAK;wBACR,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;wBAAC,MAAM;oBAC9H,KAAK,QAAQ;wBACX,iEAAiE;wBACjE,kEAAkE;wBAClE,yEAAyE;wBACzE,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,GAAG,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,GAAG,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,GAAG,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,GAAG,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,GAAG,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;wBAAC,MAAM;oBAC1M,KAAK,SAAS,CAAC,CAAC,CAAC;wBACf,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;wBACtD,GAAG,CAAC,KAAK,EAAE,GAAG,GAAG,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,GAAG,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,GAAG,IAAI,EAAE,EAAE,EAAE,QAAQ,CAAC,CAAC;wBAAC,MAAM;oBAClK,CAAC;gBACH,CAAC;YACH,CAAC;QACH,CAAC;QACD,wEAAwE;QACxE,qEAAqE;QACrE,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,MAAM,EAAE,GAAG,GAAG,MAAM,GAAG,CAAC,CAAC,GAAG,WAAW,GAAG,CAAC,CAAC;QACxE,MAAM,IAAI,GAAG,CAAC,GAAG,MAAM,EAAE,GAAG,GAAG,CAAC,GAAG,MAAM,EAAE,KAAK,GAAG,CAAC,GAAG,MAAM,EAAE,MAAM,GAAG,CAAC,GAAG,MAAM,CAAC;QACnF,IAAI,CAAC,MAAM;YAAE,MAAM,GAAG,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,GAAG,EAAE,CAAC;aAChF,CAAC;YACJ,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;YAChE,MAAM,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;QACjI,CAAC;IACH,CAAC;IACD,wEAAwE;IACxE,gEAAgE;IAChE,IAAI,MAAM;QAAE,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC;YAAE,MAAM,CAAC,KAAK,EAAE,mBAAmB,CAAC,CAAC;IAC1F,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;AAC3B,CAAC","sourcesContent":["/**\n * Experimental drawing geometry, not a public TextFormat variant. A renderer\n * supplies measured visual line rectangles; estimating widths from character\n * counts would break proportional fonts, Korean wrapping, and bidirectional text.\n * See docs/text-annotation.md for the unfinished renderer boundary.\n */\nexport const textAnnotationActions = [\n 'highlight', 'underline', 'box', 'circle', 'strike-through', 'crossed-off', 'bracket',\n] as const;\nexport type TextAnnotationAction = typeof textAnnotationActions[number];\nexport type TextAnnotationRect = Readonly<{ x: number; y: number; width: number; height: number }>;\nexport type TextAnnotationFragment = TextAnnotationRect & Readonly<{ lineIndex: number }>;\nexport type TextAnnotationPath = Readonly<{\n d: string;\n /** Filled marker strokes belong behind the text; outlines are unfilled. */\n paint: 'fill' | 'stroke';\n strokeWidth: number;\n lineIndex: number;\n}>;\nexport type TextAnnotationGeometry = Readonly<{\n paths: readonly TextAnnotationPath[];\n bounds: TextAnnotationRect | null;\n}>;\n\nfunction finite(value: number, name: string): void {\n if (!Number.isFinite(value)) throw new RangeError(`${name} must be finite`);\n}\n\n/** Merge adjacent font/bidi runs only within their measured visual line. A gap\n * in a partial selection stays unpainted even when both runs share a line.\n * Hosts supply line identity from the same layout engine that paints the text.\n */\nexport function mergeTextAnnotationFragments(fragments: readonly TextAnnotationFragment[]): TextAnnotationRect[] {\n for (const fragment of fragments) {\n for (const name of ['x', 'y', 'width', 'height'] as const) finite(fragment[name], name);\n if (fragment.width < 0 || fragment.height < 0 || !Number.isSafeInteger(fragment.lineIndex) || fragment.lineIndex < 0) throw new RangeError('Invalid text annotation fragment');\n finite(fragment.x + fragment.width, 'Fragment right'); finite(fragment.y + fragment.height, 'Fragment bottom');\n }\n const ordered = fragments.filter(fragment => fragment.width > 0 && fragment.height > 0).map(fragment => ({ ...fragment })).sort((a, b) => a.lineIndex - b.lineIndex || a.x - b.x);\n const merged: TextAnnotationFragment[] = [];\n for (const fragment of ordered) {\n const previous = merged[merged.length - 1];\n // Skia exposes Float32 edges: measured adjacent runs differed by ~0.00001px.\n // 0.01 logical px absorbs this rounding, not a visible inter-word/selection gap.\n if (previous && previous.lineIndex === fragment.lineIndex && fragment.x <= previous.x + previous.width + 0.01) {\n const y = Math.min(previous.y, fragment.y);\n merged[merged.length - 1] = { lineIndex: previous.lineIndex, x: previous.x, y, width: Math.max(previous.x + previous.width, fragment.x + fragment.width) - previous.x, height: Math.max(previous.y + previous.height, fragment.y + fragment.height) - y };\n } else merged.push(fragment);\n }\n return merged.map(({ x, y, width, height }) => {\n finite(width, 'Merged width'); finite(height, 'Merged height');\n return { x, y, width, height };\n });\n}\n\n/**\n * Small deterministic offsets give the two passes a hand-drawn character.\n * Random-per-render geometry was rejected: unrelated state updates must not\n * make the annotation jump or create server/client output differences.\n */\nexport function resolveTextAnnotationGeometry(\n lines: readonly TextAnnotationRect[],\n action: TextAnnotationAction,\n options: Readonly<{ strokeWidth?: number; padding?: number }> = {},\n): TextAnnotationGeometry {\n if (!(textAnnotationActions as readonly string[]).includes(action)) throw new TypeError('Unknown text annotation action');\n // These are drawing parameters, not layout spacing. 1.5px keeps a doubled\n // outline lighter than a focus ring; the 2px gap avoids touching glyph edges.\n const strokeWidth = options.strokeWidth ?? 1.5;\n const padding = options.padding ?? 2;\n finite(strokeWidth, 'strokeWidth'); finite(padding, 'padding');\n if (strokeWidth <= 0) throw new RangeError('strokeWidth must be positive');\n if (padding < 0) throw new RangeError('padding must not be negative');\n const paths: TextAnnotationPath[] = [];\n let bounds: TextAnnotationRect | null = null;\n for (const [lineIndex, line] of lines.entries()) {\n for (const name of ['x', 'y', 'width', 'height'] as const) finite(line[name], name);\n if (line.width < 0 || line.height < 0) throw new RangeError('Line dimensions must not be negative');\n // Empty lines and a not-yet-laid-out host do not produce stray marker dots.\n if (line.width === 0 || line.height === 0) continue;\n // Scale the wobble down for tiny fragments; it must not swallow punctuation.\n const wobble = Math.min(1, line.width / 8, line.height / 8);\n // The surrounding loop must clear the actual text even if callers choose\n // zero padding or a thick pen. Strike-through/crossed-off intentionally do not.\n const gap = action === 'circle' ? Math.max(padding, strokeWidth / 2 + wobble) : padding;\n const l = line.x - gap, r = line.x + line.width + gap;\n const t = line.y - gap, b = line.y + line.height + gap;\n const cx = l + (r - l) / 2, cy = t + (b - t) / 2;\n // A modest outward bow keeps a loop distinct from the box without extending\n // an ellipse by 40% of a long line's width into neighboring text or the screen edge.\n const bow = action === 'circle' ? Math.min(line.width, line.height) * 0.12 : 0;\n const add = (d: string, paint: 'fill' | 'stroke') => paths.push({ d, paint, strokeWidth, lineIndex });\n if (action === 'highlight') {\n add(`M ${l} ${t + wobble} Q ${cx} ${t - wobble} ${r} ${t} L ${r - wobble} ${b} Q ${cx} ${b + wobble} ${l} ${b - wobble} Z`, 'fill');\n } else {\n for (const offset of [-wobble / 2, wobble / 2]) {\n const x1 = l + offset, x2 = r + offset, y1 = t + offset, y2 = b + offset;\n switch (action) {\n case 'underline':\n add(`M ${x1} ${y2} Q ${cx} ${y2 + wobble} ${x2} ${y2 - wobble / 2}`, 'stroke'); break;\n case 'strike-through':\n add(`M ${x1} ${cy + offset} Q ${cx} ${cy - wobble} ${x2} ${cy + offset}`, 'stroke'); break;\n case 'crossed-off':\n add(`M ${x1} ${y1} Q ${cx} ${cy - wobble} ${x2} ${y2} M ${x2} ${y1} Q ${cx} ${cy + wobble} ${x1} ${y2}`, 'stroke'); break;\n case 'box':\n add(`M ${x1} ${y1} Q ${cx} ${y1 - wobble} ${x2} ${y1} L ${x2} ${y2} Q ${cx} ${y2 + wobble} ${x1} ${y2} Z`, 'stroke'); break;\n case 'circle':\n // The original inscribed ellipse cut through end glyphs at 32px.\n // Bow each edge outward through the padded corners instead: every\n // segment stays outside the text rectangle while retaining a loose loop.\n add(`M ${x1} ${cy} Q ${x1 - bow} ${y1} ${x1} ${y1} Q ${cx} ${y1 - bow} ${x2} ${y1} Q ${x2 + bow} ${cy} ${x2} ${y2} Q ${cx} ${y2 + bow} ${x1} ${y2} Q ${x1 - bow} ${cy} ${x1} ${cy} Z`, 'stroke'); break;\n case 'bracket': {\n const arm = Math.min(line.height / 3, line.width / 4);\n add(`M ${x1 + arm} ${y1} L ${x1} ${y1} L ${x1} ${y2} L ${x1 + arm} ${y2} M ${x2 - arm} ${y1} L ${x2} ${y1} L ${x2} ${y2} L ${x2 - arm} ${y2}`, 'stroke'); break;\n }\n }\n }\n }\n // Include control points, both sketch passes and stroke caps so the SVG\n // viewBox cannot clip an underline or a bracket at large text sizes.\n const outset = Math.max(2 * wobble, bow + wobble / 2) + strokeWidth / 2;\n const left = l - outset, top = t - outset, right = r + outset, bottom = b + outset;\n if (!bounds) bounds = { x: left, y: top, width: right - left, height: bottom - top };\n else {\n const x = Math.min(bounds.x, left), y = Math.min(bounds.y, top);\n bounds = { x, y, width: Math.max(bounds.x + bounds.width, right) - x, height: Math.max(bounds.y + bounds.height, bottom) - y };\n }\n }\n // Overflow can occur even with finite inputs. Reject it here instead of\n // handing NaN/Infinity geometry to a native SVG implementation.\n if (bounds) for (const value of Object.values(bounds)) finite(value, 'Annotation bounds');\n return { paths, bounds };\n}\n"]}
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/dist/version.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,MAAM,CAAC,MAAM,mBAAmB,GAAG,QAAiB,CAAC","sourcesContent":["/** Package release shown by documentation surfaces. Kept in sync by a test. */\nexport const designSystemVersion = \"1.
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,MAAM,CAAC,MAAM,mBAAmB,GAAG,QAAiB,CAAC","sourcesContent":["/** Package release shown by documentation surfaces. Kept in sync by a test. */\nexport const designSystemVersion = \"1.14.0\" as const;\n"]}
|
package/docs/agreement.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Agreement contract
|
|
2
2
|
|
|
3
|
+
검토일: 2026-10-07.
|
|
4
|
+
|
|
3
5
|
**문제.** 가입·결제 앞에 서는 약관 동의 묶음이다. 전체 동의 한 줄과 개별 항목들,
|
|
4
6
|
필수/선택 구분, 각 항목의 전문 보기가 한 덩어리로 움직인다.
|
|
5
7
|
|
|
@@ -36,3 +38,15 @@ accessibility state와 `openDetail` action이다. 두 표면 모두 전체 동
|
|
|
36
38
|
이전에는 토글에서만 불려 `defaultCheckedIds`·`checkedIds`가 처음부터 필수를 채운 경우 첫 `satisfied: true`가 오지 않아,
|
|
37
39
|
이 콜백에 연결한 제출 버튼이 꺼진 채 남았다. 파생 상태가 바뀔 때마다 부르는 대안은 호출 수가 렌더 횟수에 묶여 버렸다.
|
|
38
40
|
호출 수를 세던 소비자는 마운트 1회가 늘어난다(Changeset `recipe-parity-followups`).
|
|
41
|
+
|
|
42
|
+
**제출 중 묶음 잠금(2026-10-07).** Utilverse 가입 화면은 서버 요청 동안 필수 약관의
|
|
43
|
+
체크 값을 고정한다. 개별 `required + disabled`를 허용하면 필수/선택 분모 의미가 바뀌므로
|
|
44
|
+
`descriptor.disabled`를 별도로 제공한다. 기본 false이며 전체/개별 toggle은 잠겼을 때
|
|
45
|
+
동일 selection을 반환하고 renderer는 변경 callback을 발생시키지 않는다. `all`, `satisfied`,
|
|
46
|
+
`missingRequiredIds`와 전문 읽기는 그대로다. onStateChange의 마운트/사용자 토글 계약도
|
|
47
|
+
그대로이며 제어 값·약관 버전 변경 후 판정은 제품이 resolver로 다시 계산한다. UI 잠금은
|
|
48
|
+
서버의 동의 버전 검증·중복 요청 보호를 대신하지 않는다.
|
|
49
|
+
|
|
50
|
+
## 큰 글자 배치 보완 (2026-10-07)
|
|
51
|
+
|
|
52
|
+
긴 전문 버튼이 제목 폭을 빼앗아 한국어가 세로로 끊기는 문제를 확인했다. 제목 기준 폭을 70%로 두고 전문 버튼은 줄바꿈한다. Native는 flexBasis만으로 축소를 막지 못하므로 같은 최소 폭을 적용한다. 체크/혼합 표시도 글꼴 문자가 아닌 고정 크기 도형으로 그려 2배 글자에서 원 밖으로 나가지 않는다. 검증 범위는 [약관 QA](../../../docs/qa/2026-10-07-agreement-lock.md)에 기록한다.
|
package/docs/dialog.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Dialog
|
|
2
2
|
|
|
3
3
|
Dialog provides a modal boundary for a task that needs the user's attention. This
|
|
4
|
-
Reviewed: 2026-10-
|
|
4
|
+
Reviewed: 2026-10-07. The original
|
|
5
5
|
contract was recorded during the 2026-09-29 promotion audit because Web and Native
|
|
6
6
|
already had different title APIs and dismissal sources, while there was no single
|
|
7
7
|
component guide describing their shared behavior.
|
|
@@ -45,3 +45,18 @@ screen-reader behavior remains a separate consumer validation concern.
|
|
|
45
45
|
Native accessibility follow-up (2026-10-01): at 200% text scale, the close glyph was clipped inside the fixed IconButton frame. Dialog and Sheet now render that decorative glyph at a fixed icon size, matching Toast; title/body text still scales and the named close action and touch target are preserved. `sheet-viewport.test.tsx` checks both renderers and close callbacks.
|
|
46
46
|
|
|
47
47
|
Native viewport candidate (BT-QA-027, 2026-10-05): BurnTok local QA exposed a long confirmation extending beyond the visible screen. Dialog and AlertDialog now constrain the content surface and provide a shrinking scroll body, with actions outside that body and provider safe-area padding. Host tests cover those structural contracts and cancellation; they cannot prove Yoga geometry. Ordinary-size iOS confirmation and cancellation were checked. Enlarged-text device verification remains pending: the user stopped that QA scope before the candidate was confirmed, so this is not a completed device fix. Font caps and clipped copy were rejected because they remove readable content.
|
|
48
|
+
|
|
49
|
+
## Optional measured origin motion (unpublished experiment)
|
|
50
|
+
|
|
51
|
+
`motionOrigin` supplies a trigger rectangle measured immediately before opening in the same physical
|
|
52
|
+
viewport/window coordinate system as the destination. The existing content-transition resolver supplies
|
|
53
|
+
the inverse transform. Source investigation found lost trigger focus and discarded drafts in the external
|
|
54
|
+
Morphing Dialog/Popover demos, so this presentation reuses canonical Dialog state rather than replacing
|
|
55
|
+
the modal engine. It does not implement shared image/text identities or a new Popover engine.
|
|
56
|
+
|
|
57
|
+
Web retains one subtree through exit and suppresses stale animation completion when reopened.
|
|
58
|
+
Native uses the actual modal measurement and bounds its optional measurement wait by the existing
|
|
59
|
+
enter duration, falling back to ordinary presentation if the host fails to reply. Reduced motion skips
|
|
60
|
+
spatial interpolation. Device geometry, resizing/keyboard behavior, themes and performance remain
|
|
61
|
+
experimental until the QA report verifies them. See the usage guide and
|
|
62
|
+
[origin transition QA](../../../docs/qa/2026-10-07-overlay-origin-transition.md).
|
package/docs/effect-surface.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Composable decorative surfaces
|
|
2
2
|
|
|
3
|
-
2026-10-
|
|
3
|
+
2026-10-07. Optional Web/Native presentation, independent implementation inspired
|
|
4
4
|
by layered shader composition. No Shaders.com engine, presets or source is shipped.
|
|
5
5
|
|
|
6
6
|
Import `EffectSurface` from the renderer `/effect-surface` entry. The shared
|
|
7
|
-
`/effect-surface` contract accepts unique `layers` (`mesh`, `glow`, `grain`), a
|
|
7
|
+
`/effect-surface` contract accepts unique `layers` (`mesh`, `glow`, `grain`, `noise`), a
|
|
8
8
|
stable `seed`, `intensity` from 0 to 1, a cycle `period` from 2 to 120 seconds,
|
|
9
9
|
`active` (false by default), and three semantic color references. Web uses local
|
|
10
10
|
SVG/WAAPI; Native requires the existing optional SVG peer and core Animated.
|
|
@@ -24,7 +24,7 @@ The static seeded geometry and the base theme background stay visible when motio
|
|
|
24
24
|
stops. Effects never duplicate content, take focus, intercept touches or add an
|
|
25
25
|
accessible name. The content itself stays in normal layout.
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
The existing grain uses a seeded repeating small-dot tile rather than large spots. Geometry
|
|
28
28
|
is shared, while host rasterization and transform details can differ. This is
|
|
29
29
|
atmospheric parity, not a pixel-identical renderer promise. Motion is opt-in and
|
|
30
30
|
slow by default to avoid adding continuous work to ordinary content screens.
|
|
@@ -48,3 +48,19 @@ A failed Native foreground animation start keeps the static decoration and stops
|
|
|
48
48
|
retrying for that effect lifecycle. Descriptor validation stays outside this
|
|
49
49
|
fallback so invalid caller input is still reported. This implements integration
|
|
50
50
|
plan §5's requirement that decorative failures must not remove product content.
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
## Static noise comparison (unpublished after 1.13.1)
|
|
54
|
+
|
|
55
|
+
The opt-in `noise` layer uses an original 64×64 periodic six-octave value-noise
|
|
56
|
+
alpha tile, generated by `scripts/generate-effect-noise.py`. Both renderers tint
|
|
57
|
+
it with the current theme text color via an SVG mask. Its 64×64 tile repeats in physical host units outside
|
|
58
|
+
the existing geometric 100×100 viewBox; seed shifts the phase. It is not pixel-identical to
|
|
59
|
+
SVG feTurbulence/Perlin noise and grain remains unchanged.
|
|
60
|
+
|
|
61
|
+
Reason: the installed react-native-svg15.15.5 FeTurbulence.render warns and returns
|
|
62
|
+
null. A shared static mask keeps both hosts visible without a new native/GPU
|
|
63
|
+
peer or per-frame noise generation. Magic UI's reference motivated the comparison,
|
|
64
|
+
but no source or asset from that component is included. Check actual readability
|
|
65
|
+
and rasterization cost before using it over product text. The comparison is at
|
|
66
|
+
`실험/구성/정보 표시/질감 비교`; it is not a promotion of the new layer.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Component maturity and evidence manifest
|
|
2
2
|
|
|
3
|
-
> Generated from `@hjmds/design-contracts` 1.
|
|
3
|
+
> Generated from `@hjmds/design-contracts` 1.14.0. Do not edit this file directly; run `pnpm contracts:sync`.
|
|
4
4
|
|
|
5
5
|
This document is the human-readable projection of the same catalog and Showcase manifest consumed by product CI.
|
|
6
6
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"designSystemVersion": "1.
|
|
3
|
+
"designSystemVersion": "1.14.0",
|
|
4
4
|
"source": "packages/design-contracts/docs/generated/showcase-manifest.json",
|
|
5
5
|
"surfaces": {
|
|
6
6
|
"web": {
|
|
7
7
|
"packageName": "@hjmds/react",
|
|
8
|
-
"packageVersion": "1.
|
|
8
|
+
"packageVersion": "1.14.0",
|
|
9
9
|
"activeComponents": 103,
|
|
10
10
|
"claimedComponents": 103,
|
|
11
11
|
"completeScenarioComponents": 103,
|
|
@@ -5642,7 +5642,7 @@
|
|
|
5642
5642
|
},
|
|
5643
5643
|
"native": {
|
|
5644
5644
|
"packageName": "@hjmds/react-native",
|
|
5645
|
-
"packageVersion": "1.
|
|
5645
|
+
"packageVersion": "1.14.0",
|
|
5646
5646
|
"activeComponents": 83,
|
|
5647
5647
|
"claimedComponents": 83,
|
|
5648
5648
|
"completeScenarioComponents": 83,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Renderer evidence coverage
|
|
2
2
|
|
|
3
|
-
> Generated for HJM 1.
|
|
3
|
+
> Generated for HJM 1.14.0. Do not edit directly; run `pnpm evidence:sync`.
|
|
4
4
|
|
|
5
5
|
This projection joins the canonical surface maturity manifest with first-party renderer claims. Missing scenarios are explicit beta promotion debt; stable surfaces are blocked by CI until none remain.
|
|
6
6
|
|
package/docs/image.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Image contract
|
|
2
2
|
|
|
3
|
+
## 정확한 결과 크기 검사 — 2026-10-07, 미게시
|
|
4
|
+
|
|
5
|
+
Utilverse ADR-0020의 사진 결과 확인은 fit·2배·출력 크기를 구분한다. 기존 Image의 cover/contain/fill은
|
|
6
|
+
요소 안에서 이미지를 맞추는 방법이며, 확대 검사 viewport/이동 경계와 다르다. 새 이미지 primitive를
|
|
7
|
+
만들지 않고 기존 `components/image` subpath에 `resolveImageInspectionGeometry(image, viewport, mode)`를 추가한다.
|
|
8
|
+
|
|
9
|
+
- `mode`: `fit`은 남은 viewport에 전체 이미지를 맞추며 작은 이미지는 확대한다. `double`은 그 2배다.
|
|
10
|
+
- `pixels`: 출력 이미지의 width/height와 같은 layout 단위. 기기의 물리 pixel과 1:1이라는 뜻이 아니다.
|
|
11
|
+
- 반환은 scale/width/height와 중앙 기준 양·음 이동 한계 panBounds.x/y다. 모자라는 축의 이동 한계는 0이다.
|
|
12
|
+
- image/viewport 크기는 양수·유한값이어야 한다. 미측정 0이나 계산 overflow는 RangeError로 거절한다.
|
|
13
|
+
알려지지 않은 mode는 TypeError다. renderer는 실제 viewport 측정 후 호출해야 한다.
|
|
14
|
+
|
|
15
|
+
600×600 사진, 402×454 viewport의 2배는 804×804, 이동 한계는 x ±201/y ±175다. iOS의
|
|
16
|
+
zoom-toolkit Gallery는 scale=1 상태의 세로 pan을 pull로 처리해 손을 떼면 0으로 돌려보냈다.
|
|
17
|
+
같은 peer의 ResumableZoom에서는 위 한계를 실측했다. Gallery private scale을 바꾸거나 원본 크기를
|
|
18
|
+
속이지 않는다. 이 순수 geometry는 아직 ImageViewer의 공개 배율 UI에 연결되지 않았다.
|
|
19
|
+
|
|
3
20
|
**문제.** 이 시스템에 이미지가 하나도 없었습니다. 사진 콘텐츠(선수 프로필 사진, FA 등급
|
|
4
21
|
차트 이미지)를 레이아웃 밀림 없이, 로드 실패에도 의미를 잃지 않게 보여 주는 첫 계약입니다.
|
|
5
22
|
|
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Optional presentation adapters
|
|
2
2
|
|
|
3
|
+
## Image host extension — 2026-10-07 (unpublished)
|
|
4
|
+
|
|
5
|
+
Native ImageViewer accepts `renderImage` with the item, measured viewport width/height and
|
|
6
|
+
`onReady`/`onError`. This lets a product retain Expo display events and cache policy without
|
|
7
|
+
adding Expo to the adapter. `onImageStatusChange` reports loading/ready/error for every mounted
|
|
8
|
+
item, including offscreen pages. It is not current-page visibility or user review approval.
|
|
9
|
+
Default RN Image still uses onLoad; an Expo consumer can use onDisplay instead. Retry remounts
|
|
10
|
+
the image host, and callbacks from retired attempts or closed/replaced sessions are ignored.
|
|
11
|
+
Failure stays terminal until retry. Closing sends no final status event; the product owns
|
|
12
|
+
review invalidation on close, replacement and view-mode changes. This closes the host gap
|
|
13
|
+
identified in Utilverse ADR-0020. The optional controlled `inspection` config adds fit/2x/output-size
|
|
14
|
+
review using positive intrinsic item width/height, the shared image geometry resolver and ResumableZoom.
|
|
15
|
+
It fixes the image scale and provides clamped pan plus non-drag directional/center controls; the
|
|
16
|
+
ordinary Gallery keeps its pinch/paging behavior when inspection is absent. Inspection pages use
|
|
17
|
+
previous/next buttons, not swipe paging. Mode, viewport, item and retry changes remount the host,
|
|
18
|
+
retire callbacks and reset readiness/position. Output-size means layout units, not physical device pixels.
|
|
19
|
+
Host width/height are the computed image dimensions in inspection mode (possibly larger than the viewport).
|
|
20
|
+
The [image usage guide](usage/components/image.md) defines labels, placement and review invalidation.
|
|
21
|
+
This is unpublished and is not proof of Expo onDisplay validation or Utilverse adoption.
|
|
22
|
+
The Native Modal accepts supportedOrientations in fullScreen presentation. Optional left/right
|
|
23
|
+
safe-area insets protect controls and feedback in landscape while the gallery remains full width.
|
|
24
|
+
Products must supply updated insets and permit rotation in their manifests; OS rotation lock still applies.
|
|
25
|
+
The additive Native-only props keep this optional subpath and its existing peers; the base Image
|
|
26
|
+
renderers retain their own host APIs. There is no Web ImageViewer counterpart.
|
|
27
|
+
|
|
3
28
|
## Current evidence — 2026-09-30
|
|
4
29
|
|
|
5
30
|
The six opt-in entries are implemented. The earlier checkout verification below is a historical
|
package/docs/popover.md
CHANGED
|
@@ -154,3 +154,18 @@ Dialog 안의 Popover는 첫 Escape를 소유한다. 내부 Menu가 Escape를
|
|
|
154
154
|
재시도, dismiss 정책, 중첩 modal/menu, 선택형 hover 지연과 touch/keyboard press 경로,
|
|
155
155
|
320px·2배 글자·RTL·충돌 배치, exit 격리를 다룬다.
|
|
156
156
|
제품 채택·실제 보조기기 검증은 남아 있다.
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
## 선택적 출발 위치 전환 (미게시, 2026-10-07)
|
|
160
|
+
|
|
161
|
+
Motion Primitives의 Add Note 예제에서 닫기 후 초안과 트리거 초점이 사라지는 것을 확인했다.
|
|
162
|
+
별도 MorphingPopover 상태 엔진을 도입하는 대신 기존 Popover에 `motionOrigin?: TransitionRect`를
|
|
163
|
+
추가한다. viewport 좌표를 받고, 충돌 회피 위치가 확정된 후 Dialog와 공유하는 WAAPI 표현 helper를 쓴다.
|
|
164
|
+
시간·easing은 Popover recipe가 소유한다. 움직이는 크기를 충돌 계산에 다시 넣으면 스크롤 중 위치가
|
|
165
|
+
흔들리므로 측정에는 layout 크기를 쓴다. 기존 호출은 기존 측정·opacity 전환을 유지한다.
|
|
166
|
+
|
|
167
|
+
이 옵션은 비모달 의미를 바꾸지 않는다. Escape/닫기는 기존 시점에 트리거로 초점을 돌리고,
|
|
168
|
+
바깥 클릭/Tab은 이동한 대상의 초점을 유지한다. 닫는 동안 inert/aria-hidden이며 빠른 재열기는
|
|
169
|
+
이전 animation 완료를 무시한다. 초안과 서버 저장은 제품 소유이며 portal 밖에서 보존한다.
|
|
170
|
+
잘못된 좌표·모션 감소·WAAPI 미지원은 공간 전환 없이 기존 경로를 사용한다.
|
|
171
|
+
Native Popover는 여전히 제공하지 않는다. 모바일의 같은 편집 목적에는 Dialog/Sheet를 사용한다.
|
package/docs/rating.md
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
# Rating —
|
|
1
|
+
# Rating — 과거 판단과 현재 계약
|
|
2
|
+
|
|
3
|
+
> 2026-10-07 현재 상태 정정: 2026-10-06 사용자 레퍼런스 적용 요청으로 정수 선택·미평가·소수 평균을
|
|
4
|
+
> 구분하는 supplemental Rating을 추가했다. 현재 계약은 [reference-controls](reference-controls.md),
|
|
5
|
+
> import와 배치는 [사용 지침](usage/components/rating.md)을 따른다. 아래 “만들지 않는다”는 과거 판단이며
|
|
6
|
+
> 현재 API 부재를 뜻하지 않는다. Slider 기반 반점 입력 예제는 별도 용도로 유지한다.
|
|
2
7
|
|
|
3
8
|
> 2026-09-30 정리: 구현된 조합을 미구현으로 집계하지 않도록 독립 Planned 행과 catalog ID를 제거했다.
|
|
4
9
|
> 기존 조합 예제와 공개 helper는 유지하며, 참조표는 실제 구성 컴포넌트로 연결한다.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# 이미지 비교와 별점 계약
|
|
2
|
+
|
|
3
|
+
검토일: 2026-10-07 · 상태: 실험 · 변경 계기: 11개 UI 레퍼런스 조사에서 HJM의 전용 대응이 없는 두 기능을 확인했고 사용자가 적용을 요청했다.
|
|
4
|
+
|
|
5
|
+
`reference-controls`는 기존 Image와 Slider가 제공하지 않는 **동일 좌표 이미지 비교**의 조합과,
|
|
6
|
+
일반 RadioGroup이 구분하지 않는 **미평가·정수 입력·소수 평균**을 정의한다. canonical catalog는
|
|
7
|
+
늘리지 않으며 두 renderer의 `/image-comparison`, `/rating`에서만 제공한다. root export와 새 peer는 없다.
|
|
8
|
+
|
|
9
|
+
## Rating
|
|
10
|
+
|
|
11
|
+
- `label`, `value`, `getValueLabel`을 제품이 전달한다. `value`는 controlled이며 자동 저장하지 않는다.
|
|
12
|
+
- `null`은 미평가. 입력은 1~max의 정수, `readOnly` 평균은 0~max의 소수를 허용한다.
|
|
13
|
+
- `max` 기본 5, 범위 1~10. 무한한 별 행은 점수 척도가 아니라 목록이 되므로 거부한다.
|
|
14
|
+
- 입력형은 `onValueChange` 필수. `clearLabel`이 있으면 명시적으로 null로 지운다.
|
|
15
|
+
Web은 초기화 버튼이 비활성화되기 전에 첫 radio로 초점을 옮긴다. 2026-10-07 실제 키보드 검증에서
|
|
16
|
+
초기화 후 body로 초점이 빠져 재선택 위치를 잃는 문제를 확인했기 때문이다.
|
|
17
|
+
- Web은 실제 radio와 name으로 키보드/폼 제출을 사용한다. Native는 radio role과 checked/disabled
|
|
18
|
+
상태를 제공한다. 읽기 전용은 하나의 이름 있는 이미지이며 입력처럼 초점을 받지 않는다.
|
|
19
|
+
- 별은 값의 장식이고 현지화된 값 텍스트가 의미를 전달한다. 외부 SVG·폰트·아이콘 패키지를 추가하지 않았다.
|
|
20
|
+
- Web은 기존 styles.css가 필요하다. 큰 글자에서는 별 행을 감아 배치한다.
|
|
21
|
+
|
|
22
|
+
## ImageComparison
|
|
23
|
+
|
|
24
|
+
- `before`, `after`는 `{src,width,height,label}`이고 같은 aspect ratio가 필요하다. 자동 crop·늘이기는
|
|
25
|
+
비교 좌표를 바꾸므로 거부한다. 기본 Image가 로딩 오류의 이름과 fallback을 소유한다.
|
|
26
|
+
- controlled `value`는 0~100이며 **왼쪽에 보이는 before의 비율**이다. 0이면 after 전체, 100이면 before 전체.
|
|
27
|
+
이미지와 전후 라벨의 위치는 RTL에서도 물리 좌표를 유지하고 각 문구의 쓰기 방향은 제품 언어를 따른다.
|
|
28
|
+
2026-10-07 실제 RTL 화면에서 라벨만 반전되어 이미지와 불일치한 문제를 수정했다.
|
|
29
|
+
- `label`, `getValueText`, `onValueChange`를 전달한다. Native에는 `decrementLabel`·`incrementLabel`도 필수다.
|
|
30
|
+
- 구분선은 장식이며 드래그 핸들이 아니다. 아래의 기존 Slider로 드래그·키보드·Native adjustable
|
|
31
|
+
action을 제공한다. 독자 PanResponder를 만들지 않아 세로 스크롤 판정을 Slider와 공유한다.
|
|
32
|
+
- Native는 프레임 폭을 측정하고 두 이미지 모두 전체 폭으로 렌더링한 후 before를 잘라 표시한다.
|
|
33
|
+
- 서버 업로드·이미지 처리·원본 저장·기록 권한은 제품 소유다. 서로 다른 시점/대상을 비교한다고
|
|
34
|
+
자동으로 변화율이나 품질 점수를 계산하지 않는다.
|
|
35
|
+
|
|
36
|
+
## 참조와 검증 경계
|
|
37
|
+
|
|
38
|
+
[조사](../../../docs/plans/ui-reference-full-audit-2026-10-06.md)의 Motion Image Comparison과
|
|
39
|
+
Component Gallery Rating에서 사용자 문제를 확인했다. 원본 코드를 복사하지 않고 HJM Image/Slider,
|
|
40
|
+
semantic token, 플랫폼 입력 의미로 구현했다. Native 호스트 검사는 iOS/Android 실기기 증거가 아니다.
|
package/docs/screen-patterns.md
CHANGED
|
@@ -307,6 +307,8 @@ SearchScreen accepts a queryField slot for an existing accessible SearchField wi
|
|
|
307
307
|
| 정렬은 결과 머리 끝 Menu(single). 바꾸면 본문을 맨 위로 | Menu 지침이 정렬을 용도로 명시. 시트 안 정렬은 정렬 하나에 시트 왕복이 필요 | 필터 시트 안 정렬 |
|
|
308
308
|
| `queryLabelVisibility="hidden"`: 보이는 label을 빼고 `queryLabel`을 접근성 이름(Web `aria-label`, Native `accessibilityLabel`)과 placeholder로 쓴다 | 참고 서비스 공통 모양. 1배 약 22, 2배 약 40만큼 고정 영역을 아낀다(2026-10-06 실측). placeholder가 검색 대상을 알린다(Apple HIG) | placeholder 없이 숨김(보이는 이름이 없는 입력) |
|
|
309
309
|
| `searching`+`searchingLabel`: Web `loading`, Native `busy`+`busyLabel`을 한 이름으로. 둘 다 주거나 둘 다 생략(타입) | 2026-10-06 utilverse 적용 조사: 진행 표시 하나 때문에 제품이 입력을 `queryField`로 통째로 바꿨다 | 플랫폼별 이름 그대로 노출 |
|
|
310
|
+
| 확정하면 키보드를 닫는다(1.13.1). Native는 모든 확정에서 `Keyboard.dismiss()`(one-step 포함). Web은 고른 제안·최근·추천이면 그 행이 사라지기 전에 결과 영역(`.hjm-search-screen__results`, `tabIndex=-1`)으로 포커스를 옮기고, Enter 확정은 입력에 포커스를 남긴다 | utilverse 1.13.0 적용: 검색 키만 키보드를 닫아 추천 검색어를 고르면 키보드가 결과를 가렸다(제품이 `onSubmit`에서 dismiss). Web은 고른 행이 사라지며 포커스가 `<body>`로 떨어졌다. 결과 영역은 단계가 바뀌어도 남는 자리이고, 입력을 떠나 모바일 브라우저 키보드도 닫힌다. Enter는 입력이 그대로 보이고 바로 고쳐 다시 찾는 흐름이라 남긴다 | Web `blur()`만(포커스가 `<body>`로 가 낭독 위치를 잃음), 항상 입력으로 이동(모바일에서 키보드가 다시 열림), Native 포커스 이동(낭독 커서가 화면을 다시 읽음) |
|
|
311
|
+
| `hostGutter`(`ContainerGutter`, 기본 `none`, 1.13.1): `filtersOverflow="scroll"` 줄을 화면 여백(`contentInset="none"`이면 0)에 host 여백을 더한 만큼 넓힌다 | utilverse 1.13.0 적용: route(Container `regular`)와 Sheet가 여백을 주는 화면에서 `contentInset="none"`이면 줄이 host 여백에서 잘렸다. SearchScreen은 host 여백을 볼 수 없다. 숫자가 아니라 Container gutter 이름이라 제품 코드에 임의 간격이 들어가지 않는다 | 창 기준 위치 측정(첫 프레임 점프, 가운데 정렬된 최대 폭 host에서 틀림), `contentInset="none"`이면 항상 16 가정(host 여백 20·24와 어긋남) |
|
|
310
312
|
|
|
311
313
|
2026-10-06 릴리스 전 정리에서 Native `SearchField`도 `busy`인 동안 입력을 받게 했다(Web `loading`과 같다). 그 전에는 입력 변경을 무시해,
|
|
312
314
|
`searching`을 입력 중 제안 조회에 켜면 그동안 친 글자가 사라졌고 지침이 "확정 결과 요청에만"으로 제한했다. 진행 표시가 입력을
|
package/docs/sheet.md
CHANGED
|
@@ -75,3 +75,12 @@ Diairy QA W16 reproduced Chrome default backdrop blur undoing Dialog focus retur
|
|
|
75
75
|
- Web 여백은 Native와 같은 recipe 값이다: 위아래 `content.paddingTop/Bottom`(sm 12), 좌우 `paddingHorizontal`(lg 20),
|
|
76
76
|
머리·본문·footer 사이 `body.gap`(md 16), footer 위 `footer.paddingTop`(sm 12). Web은 Dialog 여백(20)을 쓰고 있었다.
|
|
77
77
|
본문 스크롤 상자는 자식 포커스 링이 잘리지 않도록 4px 안쪽 여백과 같은 크기의 음수 margin을 둔다(보이는 간격은 recipe 값).
|
|
78
|
+
|
|
79
|
+
## 고정 높이 본문 채우기 (1.13.1, 2026-10-06)
|
|
80
|
+
|
|
81
|
+
고정 높이 Native Sheet(`size` `medium`·`large`·`full`, 옆 시트)의 본문은 머리·footer를 뺀 남은 높이를 차지한다(`flexGrow: 1`, `minHeight: 0`).
|
|
82
|
+
`scrollable`이면 ScrollView가 늘고 그 내용 컨테이너는 늘지 않는다. Web `.hjm-sheet__body`는 이미 `flex: 1 1 auto`였다.
|
|
83
|
+
계기: utilverse가 1.13.0을 적용하며 `size="large"` Sheet 안에 SearchScreen을 넣었는데, 본문이 내용 높이 View라 ScreenLayout의 `flex: 1` 루트가
|
|
84
|
+
0pt가 되고 검색 입력만 그려졌다(iPhone 17 Pro · iOS 26.5, 이전 코드로도 재현). 제품은 창 높이 `flexBasis`로 우회했다.
|
|
85
|
+
`auto`는 내용 높이 그대로다(늘 남는 공간이 없다). 바뀌는 모습: 고정 높이 시트의 `footer`가 본문 바로 아래가 아니라 시트 아래에 붙는다(Web과 같다).
|
|
86
|
+
버린 대안: 새 `bodyLayout` prop(기본값을 그대로 두면 Web과 다른 동작이 남고 제품마다 켜야 한다).
|
package/docs/task-list.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Task list
|
|
2
2
|
|
|
3
|
-
Reviewed: 2026-10-
|
|
3
|
+
Reviewed: 2026-10-07. Web and Native expose TaskList through `/task-list`.
|
|
4
4
|
TaskList composes existing List and Checkbox rather than creating another selection
|
|
5
5
|
or gesture engine. Each controlled item has unique `id`, localized `label`, boolean
|
|
6
6
|
`completed`, optional `description` and `disabled`. Invalid identity/completion data
|
|
@@ -30,3 +30,17 @@ found that 200% text scaling enlarged Checkbox's decorative mark beyond its fixe
|
|
|
30
30
|
Native Checkbox now keeps the checked/mixed artwork at the recipe typography size
|
|
31
31
|
while its label and description continue scaling. TaskList inherits this correction
|
|
32
32
|
through Checkbox; it does not provide a second selection indicator.
|
|
33
|
+
|
|
34
|
+
## Independent item actions
|
|
35
|
+
|
|
36
|
+
`renderItemAction({item, disabled})` optionally supplies a product-localized control below
|
|
37
|
+
that item's checkbox. `disabled` combines the list and item disabled flags; pass it to
|
|
38
|
+
the control. The slot does not save, remove, or confirm anything itself. Keep asynchronous
|
|
39
|
+
storage and rollback in the product. The action is a sibling of Checkbox, never inside its
|
|
40
|
+
label, so it has its own press/focus target. It follows the checkbox with spacing.sm (12)
|
|
41
|
+
and remains part of renderItem when renderCollection supplies sorting.
|
|
42
|
+
|
|
43
|
+
The separate line preserves the full label width at large text sizes. Utilverse checklist
|
|
44
|
+
rows need deletion alongside completion (2026-10-07 consumer audit), which the previous
|
|
45
|
+
completion-only API could not express without rebuilding row layout. Both showcases now
|
|
46
|
+
include independent delete actions; device drag/focus/large-text action QA remains pending.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# 문장 주석 — 구현 중
|
|
2
|
+
|
|
3
|
+
검토일: 2026-10-07. Web 내부 renderer는 구현했고 공개 renderer·Storybook 실험은 아직 없다.
|
|
4
|
+
|
|
5
|
+
사용자가 제공한 Magic UI Highlighter를 조사한 결과 기존 TextFormat은 Web의
|
|
6
|
+
단축키·코드·인용 요소를 위한 계약이라 손그림 주석을 직접 대체하지 못했다.
|
|
7
|
+
조사 근거는 [원본 검토](../../../docs/qa/2026-10-07-highlighter-reference.md)다.
|
|
8
|
+
|
|
9
|
+
## 공통 구현
|
|
10
|
+
|
|
11
|
+
`src/text-annotation.ts`는 renderer가 측정한 시각적 줄 조각의 x/y/width/height에서
|
|
12
|
+
SVG 경로를 만든다. `@hjmds/design-contracts/text-annotation` subpath로 공개하며 root에는
|
|
13
|
+
추가하지 않는다. renderer에서 같은 경로 계산을 사용하면서 기본 소비 번들에는 들어가지
|
|
14
|
+
않도록 한 선택이다. 이 export 추가가 renderer 사용 가능 또는 npm 게시를 뜻하지 않는다.
|
|
15
|
+
|
|
16
|
+
- highlight는 뒤쪽 채움, underline/box/circle/strike-through/crossed-off/bracket은
|
|
17
|
+
두 번 그린 외곽 경로다. 무작위 값 대신 결정적인 작은 오프셋을 쓴다.
|
|
18
|
+
- 줄 사이 빈 영역을 한 덩어리로 칠하지 않는다. 동일 줄의 bidi 조각도 호스트가 측정한
|
|
19
|
+
물리 좌표 그대로 받으며 문자열 길이로 위치를 추측하거나 RTL을 한 번 더 뒤집지 않는다.
|
|
20
|
+
- 기본 strokeWidth=1.5, padding=2는 장식 경로 단위다. 문장 레이아웃이나 전역 토큰을
|
|
21
|
+
바꾸지 않는다. 작은 구두점에는 흔들림을 줄이고 빈 줄에는 경로를 만들지 않는다.
|
|
22
|
+
- 반환 bounds는 경로 제어점·두 번 그린 선·선 굵기를 포함한다. 호스트는 이를 SVG
|
|
23
|
+
영역에 반영해야 하며 원래 Text의 크기나 줄바꿈 폭을 늘려서는 안 된다.
|
|
24
|
+
- circle의 초기 내접 타원이 32px 글자의 양 끝을 가로질러 바깥으로 휜 루프로 수정했다.
|
|
25
|
+
여백 0이나 굵은 펜에서도 해당 줄의 글자 영역 밖에 선을 두도록 최소 간격을 계산한다.
|
|
26
|
+
현재 모양은 타원보다 둥근 테두리에 가깝다. 원본과의 시각적 차이, 조밀한 줄 간격에서
|
|
27
|
+
이웃 줄과의 충돌 및 팔레트 검증은 남아 있으며 원본 동등 표현으로 주장하지 않는다.
|
|
28
|
+
- 음수 크기, 비유한 좌표와 계산 overflow는 렌더링 전 거부한다. 스크롤·상대 위치 때문에
|
|
29
|
+
음수 x/y는 유효하며 소수 단위 측정도 유지한다.
|
|
30
|
+
- `mergeTextAnnotationFragments`는 같은 실제 줄(lineIndex)의 맞닿은 글꼴 조각을 합친다.
|
|
31
|
+
Native Skia가 한글·공백 fallback을 23개 조각으로 반환해 marker padding이 겹친 실측을
|
|
32
|
+
반영한 기능이다. Float32 경계 오차만 흡수하도록 0.01px 허용 오차를 사용하며 선택하지
|
|
33
|
+
않은 gap이나 다른 줄은 합치지 않는다. 이 함수가 줄 번호를 문자열에서 추측하지 않는다.
|
|
34
|
+
|
|
35
|
+
## Web 내부 renderer와 Native의 남은 계약
|
|
36
|
+
|
|
37
|
+
`packages/react/src/text-annotation.tsx`는 inline span의 실제 Range 사각형을 측정한다.
|
|
38
|
+
문장 조각을 inline-block으로 바꾸지 않는다. 절대 위치 기준점과 SVG를 접근성·hit-test에서
|
|
39
|
+
제외하고, 문구·앞 문장·조상 크기 및 스타일·글꼴 로드 변화에 재측정한다. 새 문구가 이전
|
|
40
|
+
문구 경로를 받지 않도록 측정 세대를 나눈다. 동일 geometry에는 state 갱신을 보내지 않는다.
|
|
41
|
+
진입 모션은 motion.slow=320ms, 모션 감소 또는 숨겨진 문서에서는 즉시 표시하며,
|
|
42
|
+
리사이즈에는 다시 재생하지 않는다. highlight의 20% brand wash는 실험값이다. HJM 기본·Utilverse·BurnTok의
|
|
43
|
+
light/dark × bg/surface × body/muted 24조합에서는 합성 뒤 4.5:1을 통과했으며,
|
|
44
|
+
다른 표면·제품 색·이미지 배경의 대비를 보장하지 않는다. renderer는 아직 package exports에 넣지 않았다.
|
|
45
|
+
|
|
46
|
+
Native 0.86.2/Expo Go 57.0.9/iOS 26.5에서 진단 fixture를 실행했다. 부모의 onTextLayout은
|
|
47
|
+
폭 280→184에서 2→3줄을 보고했으나 중첩 Text의 onTextLayout/onLayout은 관찰되지 않았고
|
|
48
|
+
measure는 0×0이었다. 이 경로를 실제 줄별 측정으로 사용할 수 없다. 진단 소스는
|
|
49
|
+
`showcase/native/src/devtools/TextAnnotationMeasurementProbe.tsx`에 보존했다.
|
|
50
|
+
|
|
51
|
+
후속 `TextAnnotationSkiaProbe.tsx`는 Skia 2.6.2의 Paragraph로 측정하고 **같은 Paragraph**를
|
|
52
|
+
그린다. getRectsForRange와 실제 line metrics를 이용해 한글 23→3, emoji 9→2, 혼합 RTL
|
|
53
|
+
21→3개의 표시 영역을 얻었다. RTL 폭 280→184에서 5줄로 다시 배치했다. 이 좌표를 다른
|
|
54
|
+
Native Text 위에 올리는 것은 금지한다. 비교 fixture에서 두 엔진의 줄바꿈이 달랐다.
|
|
55
|
+
이는 Native renderer 후보의 측정 증거이며 일반 Text 대체가 아니다. Canvas의 읽기 이름만으로
|
|
56
|
+
Native Text 선택·복사 기능이 생기지 않는다. 폰트·스케일·외곽 잘림·선택·접근성·Android를
|
|
57
|
+
해결하고 공개 API/실험으로 연결하는 일이 남았다.
|
|
58
|
+
|
|
59
|
+
Web은 inline 문장과 Range.getClientRects의 줄 조각, Native는 실제 텍스트 레이아웃에서
|
|
60
|
+
문장 일부의 줄별 위치를 얻어야 한다. 전체 문단을 주석으로 바꾸는 것으로 이 요구를
|
|
61
|
+
대체하지 않는다. 글꼴 로드·문구 교체·글자 크기·폭 변경마다 최신 geometry를 공급한다.
|
|
62
|
+
|
|
63
|
+
본문은 처음부터 읽고 선택할 수 있어야 한다. 주석은 접근성 트리와 pointer hit-testing에서
|
|
64
|
+
제외하며 highlight는 글자 뒤에 둔다. 색은 제품 팔레트의 의미 색으로 공급하고 대비를
|
|
65
|
+
검증한다. 취소선이 데이터의 삭제 상태를 대신 전달하지 않도록 제품 문구를 유지한다.
|
|
66
|
+
모션 감소에서는 즉시 완성된 주석을 보인다. 비활성 화면·unmount 시 animation과 관찰자를
|
|
67
|
+
정리한다. 리사이즈로 geometry만 바뀔 때 진입 모션을 반복하지 않는다.
|
|
68
|
+
|
|
69
|
+
## 검증 범위
|
|
70
|
+
|
|
71
|
+
현재 계약 테스트 15개는 줄 조각 분리/병합, RTL 물리 좌표 유지, 7가지 경로의 결정성, 소수·음수
|
|
72
|
+
좌표, 빈 상태, 재측정, 비정상 입력을 확인한다. 실제 글자 측정, 글자 대비, 모션, 성능,
|
|
73
|
+
스크린리더 또는 소비 앱 채택의 증거는 아니다. 별도 Web 브라우저 테스트 7개는 실제
|
|
74
|
+
DOM 측정·줄바꿈·선택·RTL·폭 변경·문구 교체·모션 감소를 검증했다. 전체 스타일·기기·성능
|
|
75
|
+
검증을 마쳤다는 뜻은 아니다. 추가된 큰 글자 회귀는 선 경로를 0.5px 간격으로 샘플링해
|
|
76
|
+
선 두께까지 해당 줄의 글자 사각형 바깥에 있는지 검사한다. 이는 이웃 줄까지 포함한
|
|
77
|
+
모든 배치 조합의 가독성 검증은 아니다.
|
|
78
|
+
|
|
79
|
+
공개 export·양 renderer·사용 지침·실험 스토리·UI 및 행동 증거가 연결된 다음에 실험 수에
|
|
80
|
+
포함한다. 안정화·npm 게시·Utilverse 적용은 사용자 요청의 후속 단계로 남아 있다.
|
|
81
|
+
|
|
82
|
+
Web의 같은 top/height 수직 대역에서 맞닿은 bidi run은 기존 fragment 병합 계약으로
|
|
83
|
+
한 주석을 그린다. 390px 아랍어/영어 혼합 문장에서 내부 세로선이 생긴 실측에 따른
|
|
84
|
+
수정이다. 수직 대역이 다르거나 실제 수평 gap이 있으면 합치지 않는다. 추가된 회귀·
|
|
85
|
+
제품 대비·진입 모션 재생 검사까지 Web 브라우저 테스트는 10개다.
|