@hjmds/design-contracts 1.13.1 → 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 +4 -0
- package/dist/catalog.d.ts.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/task-list.md +15 -1
- package/docs/text-annotation.md +85 -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 +5 -0
- 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 +5 -0
- package/docs/usage/components/segmented-control.md +17 -0
- 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/flow-onboarding.md +5 -3
- package/docs/usage/screens/product-bento.md +117 -0
- package/package.json +37 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type RatingDescriptor = Readonly<{
|
|
2
|
+
label: string;
|
|
3
|
+
value: number | null;
|
|
4
|
+
max?: number;
|
|
5
|
+
readOnly?: boolean;
|
|
6
|
+
}>;
|
|
7
|
+
/** A bounded row remains a rating rather than an unbounded repeated-icon list.
|
|
8
|
+
* Null means unrated; zero and fractional averages are meaningful only in read-only summaries. */
|
|
9
|
+
export declare function resolveRating({ label, value, max, readOnly }: RatingDescriptor): {
|
|
10
|
+
max: number;
|
|
11
|
+
value: number | null;
|
|
12
|
+
fractions: number[];
|
|
13
|
+
};
|
|
14
|
+
export type ComparisonImage = Readonly<{
|
|
15
|
+
src: string;
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
label: string;
|
|
19
|
+
}>;
|
|
20
|
+
export type ImageComparisonDescriptor = Readonly<{
|
|
21
|
+
label: string;
|
|
22
|
+
before: ComparisonImage;
|
|
23
|
+
after: ComparisonImage;
|
|
24
|
+
value: number;
|
|
25
|
+
}>;
|
|
26
|
+
/** Both pictures share one coordinate system. Reject mismatched aspect ratios
|
|
27
|
+
* instead of silently stretching or cropping the evidence being compared. */
|
|
28
|
+
export declare function resolveImageComparison({ label, before, after, value }: ImageComparisonDescriptor): {
|
|
29
|
+
aspectRatio: number;
|
|
30
|
+
fraction: number;
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=reference-controls.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reference-controls.d.ts","sourceRoot":"","sources":["../src/reference-controls.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CAAC;AACnH;kGACkG;AAClG,wBAAgB,aAAa,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,GAAO,EAAE,QAAgB,EAAE,EAAE,gBAAgB;;;;EAO1F;AACD,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AACtG,MAAM,MAAM,yBAAyB,GAAG,QAAQ,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,eAAe,CAAC;IAAC,KAAK,EAAE,eAAe,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AACpI;6EAC6E;AAC7E,wBAAgB,sBAAsB,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,yBAAyB;;;EAOhG"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { validateImageDescriptor } from "./image.js";
|
|
2
|
+
/** A bounded row remains a rating rather than an unbounded repeated-icon list.
|
|
3
|
+
* Null means unrated; zero and fractional averages are meaningful only in read-only summaries. */
|
|
4
|
+
export function resolveRating({ label, value, max = 5, readOnly = false }) {
|
|
5
|
+
if (!label.trim())
|
|
6
|
+
throw new TypeError("Rating requires a label");
|
|
7
|
+
if (!Number.isInteger(max) || max < 1 || max > 10)
|
|
8
|
+
throw new RangeError("Rating max must be an integer from 1 to 10");
|
|
9
|
+
if (value !== null && (!Number.isFinite(value) || value < 0 || value > max || (!readOnly && (!Number.isInteger(value) || value === 0)))) {
|
|
10
|
+
throw new RangeError("Interactive ratings require whole scores from 1 to max, or null");
|
|
11
|
+
}
|
|
12
|
+
return { max, value, fractions: Array.from({ length: max }, (_, index) => Math.max(0, Math.min(1, (value ?? 0) - index))) };
|
|
13
|
+
}
|
|
14
|
+
/** Both pictures share one coordinate system. Reject mismatched aspect ratios
|
|
15
|
+
* instead of silently stretching or cropping the evidence being compared. */
|
|
16
|
+
export function resolveImageComparison({ label, before, after, value }) {
|
|
17
|
+
if (!label.trim())
|
|
18
|
+
throw new TypeError("ImageComparison requires a label");
|
|
19
|
+
for (const image of [before, after])
|
|
20
|
+
validateImageDescriptor({ ...image, decorative: false, accessibilityLabel: image.label });
|
|
21
|
+
if (!Number.isFinite(value) || value < 0 || value > 100)
|
|
22
|
+
throw new RangeError("ImageComparison value must be from 0 to 100");
|
|
23
|
+
const aspectRatio = before.width / before.height;
|
|
24
|
+
if (Math.abs(aspectRatio - after.width / after.height) > 0.000001)
|
|
25
|
+
throw new RangeError("Compared images require the same aspect ratio");
|
|
26
|
+
return { aspectRatio, fraction: value / 100 };
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=reference-controls.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reference-controls.js","sourceRoot":"","sources":["../src/reference-controls.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAGrD;kGACkG;AAClG,MAAM,UAAU,aAAa,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,GAAG,CAAC,EAAE,QAAQ,GAAG,KAAK,EAAoB;IACzF,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE;QAAE,MAAM,IAAI,SAAS,CAAC,yBAAyB,CAAC,CAAC;IAClE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,GAAG,GAAG,EAAE;QAAE,MAAM,IAAI,UAAU,CAAC,4CAA4C,CAAC,CAAC;IACtH,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,IAAI,KAAK,GAAG,GAAG,IAAI,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACxI,MAAM,IAAI,UAAU,CAAC,iEAAiE,CAAC,CAAC;IAC1F,CAAC;IACD,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;AAC9H,CAAC;AAGD;6EAC6E;AAC7E,MAAM,UAAU,sBAAsB,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAA6B;IAC/F,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE;QAAE,MAAM,IAAI,SAAS,CAAC,kCAAkC,CAAC,CAAC;IAC3E,KAAK,MAAM,KAAK,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC;QAAE,uBAAuB,CAAC,EAAE,GAAG,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,kBAAkB,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC;IAC/H,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,IAAI,KAAK,GAAG,GAAG;QAAE,MAAM,IAAI,UAAU,CAAC,6CAA6C,CAAC,CAAC;IAC7H,MAAM,WAAW,GAAG,MAAM,CAAC,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC;IACjD,IAAI,IAAI,CAAC,GAAG,CAAC,WAAW,GAAG,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,GAAG,QAAQ;QAAE,MAAM,IAAI,UAAU,CAAC,+CAA+C,CAAC,CAAC;IACzI,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,KAAK,GAAG,GAAG,EAAE,CAAC;AAChD,CAAC","sourcesContent":["import { validateImageDescriptor } from \"./image.js\";\n\nexport type RatingDescriptor = Readonly<{ label: string; value: number | null; max?: number; readOnly?: boolean }>;\n/** A bounded row remains a rating rather than an unbounded repeated-icon list.\n * Null means unrated; zero and fractional averages are meaningful only in read-only summaries. */\nexport function resolveRating({ label, value, max = 5, readOnly = false }: RatingDescriptor) {\n if (!label.trim()) throw new TypeError(\"Rating requires a label\");\n if (!Number.isInteger(max) || max < 1 || max > 10) throw new RangeError(\"Rating max must be an integer from 1 to 10\");\n if (value !== null && (!Number.isFinite(value) || value < 0 || value > max || (!readOnly && (!Number.isInteger(value) || value === 0)))) {\n throw new RangeError(\"Interactive ratings require whole scores from 1 to max, or null\");\n }\n return { max, value, fractions: Array.from({ length: max }, (_, index) => Math.max(0, Math.min(1, (value ?? 0) - index))) };\n}\nexport type ComparisonImage = Readonly<{ src: string; width: number; height: number; label: string }>;\nexport type ImageComparisonDescriptor = Readonly<{ label: string; before: ComparisonImage; after: ComparisonImage; value: number }>;\n/** Both pictures share one coordinate system. Reject mismatched aspect ratios\n * instead of silently stretching or cropping the evidence being compared. */\nexport function resolveImageComparison({ label, before, after, value }: ImageComparisonDescriptor) {\n if (!label.trim()) throw new TypeError(\"ImageComparison requires a label\");\n for (const image of [before, after]) validateImageDescriptor({ ...image, decorative: false, accessibilityLabel: image.label });\n if (!Number.isFinite(value) || value < 0 || value > 100) throw new RangeError(\"ImageComparison value must be from 0 to 100\");\n const aspectRatio = before.width / before.height;\n if (Math.abs(aspectRatio - after.width / after.height) > 0.000001) throw new RangeError(\"Compared images require the same aspect ratio\");\n return { aspectRatio, fraction: value / 100 };\n}\n"]}
|
|
@@ -4,5 +4,11 @@ export type ScrollMetrics = Readonly<{
|
|
|
4
4
|
contentSize: number;
|
|
5
5
|
viewportSize: number;
|
|
6
6
|
}>;
|
|
7
|
-
export declare function resolveScrollProgress(
|
|
7
|
+
export declare function resolveScrollProgress(metrics: ScrollMetrics): number;
|
|
8
|
+
export type ScrollEdges = Readonly<{
|
|
9
|
+
before: boolean;
|
|
10
|
+
after: boolean;
|
|
11
|
+
}>;
|
|
12
|
+
/** Whether more content exists in either logical direction; not a scroll action. */
|
|
13
|
+
export declare function resolveScrollEdges(metrics: ScrollMetrics): ScrollEdges;
|
|
8
14
|
//# sourceMappingURL=scroll-progress.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scroll-progress.d.ts","sourceRoot":"","sources":["../src/scroll-progress.ts"],"names":[],"mappings":"AAAA,mFAAmF;AACnF,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"scroll-progress.d.ts","sourceRoot":"","sources":["../src/scroll-progress.ts"],"names":[],"mappings":"AAAA,mFAAmF;AACnF,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAMpG,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAOpE;AAED,MAAM,MAAM,WAAW,GAAG,QAAQ,CAAC;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC,CAAC;AACxE,oFAAoF;AACpF,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,aAAa,GAAG,WAAW,CAWtE"}
|
package/dist/scroll-progress.js
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
if (![offset, contentSize, viewportSize].every(Number.isFinite) || contentSize < 0 || viewportSize < 0)
|
|
1
|
+
function validateScrollMetrics({ offset, contentSize, viewportSize }) {
|
|
2
|
+
if (![offset, contentSize, viewportSize].every(Number.isFinite) || contentSize < 0 || viewportSize < 0) {
|
|
3
3
|
throw new RangeError('Scroll metrics must be finite with nonnegative sizes');
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
export function resolveScrollProgress(metrics) {
|
|
7
|
+
validateScrollMetrics(metrics);
|
|
8
|
+
const { offset, contentSize, viewportSize } = metrics;
|
|
4
9
|
// Unmeasured hosts are not complete; content that fits a measured viewport is.
|
|
5
10
|
if (viewportSize === 0)
|
|
6
11
|
return 0;
|
|
7
12
|
const extent = contentSize - viewportSize;
|
|
8
13
|
return extent <= 0 ? 1 : Math.min(1, Math.max(0, offset / extent));
|
|
9
14
|
}
|
|
15
|
+
/** Whether more content exists in either logical direction; not a scroll action. */
|
|
16
|
+
export function resolveScrollEdges(metrics) {
|
|
17
|
+
validateScrollMetrics(metrics);
|
|
18
|
+
const { offset, contentSize, viewportSize } = metrics;
|
|
19
|
+
// The reference blur obscured the last row even at the end. One logical pixel
|
|
20
|
+
// tolerates integer content/client dimensions paired with fractional offsets;
|
|
21
|
+
// exact equality would leave that edge covered. See progressive-blur adoption.
|
|
22
|
+
const tolerance = 1;
|
|
23
|
+
const extent = contentSize - viewportSize;
|
|
24
|
+
if (viewportSize === 0 || extent <= tolerance)
|
|
25
|
+
return { before: false, after: false };
|
|
26
|
+
const boundedOffset = Math.min(extent, Math.max(0, offset));
|
|
27
|
+
return { before: boundedOffset > tolerance, after: extent - boundedOffset > tolerance };
|
|
28
|
+
}
|
|
10
29
|
//# sourceMappingURL=scroll-progress.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scroll-progress.js","sourceRoot":"","sources":["../src/scroll-progress.ts"],"names":[],"mappings":"AAEA,
|
|
1
|
+
{"version":3,"file":"scroll-progress.js","sourceRoot":"","sources":["../src/scroll-progress.ts"],"names":[],"mappings":"AAEA,SAAS,qBAAqB,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,YAAY,EAAiB;IACjF,IAAI,CAAC,CAAC,MAAM,EAAE,WAAW,EAAE,YAAY,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,WAAW,GAAG,CAAC,IAAI,YAAY,GAAG,CAAC,EAAE,CAAC;QACvG,MAAM,IAAI,UAAU,CAAC,sDAAsD,CAAC,CAAC;IAC/E,CAAC;AACH,CAAC;AACD,MAAM,UAAU,qBAAqB,CAAC,OAAsB;IAC1D,qBAAqB,CAAC,OAAO,CAAC,CAAC;IAC/B,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC;IACtD,+EAA+E;IAC/E,IAAI,YAAY,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IACjC,MAAM,MAAM,GAAG,WAAW,GAAG,YAAY,CAAC;IAC1C,OAAO,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC;AACrE,CAAC;AAGD,oFAAoF;AACpF,MAAM,UAAU,kBAAkB,CAAC,OAAsB;IACvD,qBAAqB,CAAC,OAAO,CAAC,CAAC;IAC/B,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC;IACtD,8EAA8E;IAC9E,8EAA8E;IAC9E,+EAA+E;IAC/E,MAAM,SAAS,GAAG,CAAC,CAAC;IACpB,MAAM,MAAM,GAAG,WAAW,GAAG,YAAY,CAAC;IAC1C,IAAI,YAAY,KAAK,CAAC,IAAI,MAAM,IAAI,SAAS;QAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IACtF,MAAM,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;IAC5D,OAAO,EAAE,MAAM,EAAE,aAAa,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,EAAE,CAAC;AAC1F,CAAC","sourcesContent":["/** Logical forward offset. Horizontal RTL hosts normalize their own scrollLeft. */\nexport type ScrollMetrics = Readonly<{ offset: number; contentSize: number; viewportSize: number }>;\nfunction validateScrollMetrics({ offset, contentSize, viewportSize }: ScrollMetrics): void {\n if (![offset, contentSize, viewportSize].every(Number.isFinite) || contentSize < 0 || viewportSize < 0) {\n throw new RangeError('Scroll metrics must be finite with nonnegative sizes');\n }\n}\nexport function resolveScrollProgress(metrics: ScrollMetrics): number {\n validateScrollMetrics(metrics);\n const { offset, contentSize, viewportSize } = metrics;\n // Unmeasured hosts are not complete; content that fits a measured viewport is.\n if (viewportSize === 0) return 0;\n const extent = contentSize - viewportSize;\n return extent <= 0 ? 1 : Math.min(1, Math.max(0, offset / extent));\n}\n\nexport type ScrollEdges = Readonly<{ before: boolean; after: boolean }>;\n/** Whether more content exists in either logical direction; not a scroll action. */\nexport function resolveScrollEdges(metrics: ScrollMetrics): ScrollEdges {\n validateScrollMetrics(metrics);\n const { offset, contentSize, viewportSize } = metrics;\n // The reference blur obscured the last row even at the end. One logical pixel\n // tolerates integer content/client dimensions paired with fractional offsets;\n // exact equality would leave that edge covered. See progressive-blur adoption.\n const tolerance = 1;\n const extent = contentSize - viewportSize;\n if (viewportSize === 0 || extent <= tolerance) return { before: false, after: false };\n const boundedOffset = Math.min(extent, Math.max(0, offset));\n return { before: boundedOffset > tolerance, after: extent - boundedOffset > tolerance };\n}\n"]}
|
|
@@ -0,0 +1,43 @@
|
|
|
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 declare const textAnnotationActions: readonly ["highlight", "underline", "box", "circle", "strike-through", "crossed-off", "bracket"];
|
|
8
|
+
export type TextAnnotationAction = typeof textAnnotationActions[number];
|
|
9
|
+
export type TextAnnotationRect = Readonly<{
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
}>;
|
|
15
|
+
export type TextAnnotationFragment = TextAnnotationRect & Readonly<{
|
|
16
|
+
lineIndex: number;
|
|
17
|
+
}>;
|
|
18
|
+
export type TextAnnotationPath = Readonly<{
|
|
19
|
+
d: string;
|
|
20
|
+
/** Filled marker strokes belong behind the text; outlines are unfilled. */
|
|
21
|
+
paint: 'fill' | 'stroke';
|
|
22
|
+
strokeWidth: number;
|
|
23
|
+
lineIndex: number;
|
|
24
|
+
}>;
|
|
25
|
+
export type TextAnnotationGeometry = Readonly<{
|
|
26
|
+
paths: readonly TextAnnotationPath[];
|
|
27
|
+
bounds: TextAnnotationRect | null;
|
|
28
|
+
}>;
|
|
29
|
+
/** Merge adjacent font/bidi runs only within their measured visual line. A gap
|
|
30
|
+
* in a partial selection stays unpainted even when both runs share a line.
|
|
31
|
+
* Hosts supply line identity from the same layout engine that paints the text.
|
|
32
|
+
*/
|
|
33
|
+
export declare function mergeTextAnnotationFragments(fragments: readonly TextAnnotationFragment[]): TextAnnotationRect[];
|
|
34
|
+
/**
|
|
35
|
+
* Small deterministic offsets give the two passes a hand-drawn character.
|
|
36
|
+
* Random-per-render geometry was rejected: unrelated state updates must not
|
|
37
|
+
* make the annotation jump or create server/client output differences.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveTextAnnotationGeometry(lines: readonly TextAnnotationRect[], action: TextAnnotationAction, options?: Readonly<{
|
|
40
|
+
strokeWidth?: number;
|
|
41
|
+
padding?: number;
|
|
42
|
+
}>): TextAnnotationGeometry;
|
|
43
|
+
//# sourceMappingURL=text-annotation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-annotation.d.ts","sourceRoot":"","sources":["../src/text-annotation.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,kGAExB,CAAC;AACX,MAAM,MAAM,oBAAoB,GAAG,OAAO,qBAAqB,CAAC,MAAM,CAAC,CAAC;AACxE,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AACnG,MAAM,MAAM,sBAAsB,GAAG,kBAAkB,GAAG,QAAQ,CAAC;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAC1F,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;IACxC,CAAC,EAAE,MAAM,CAAC;IACV,2EAA2E;IAC3E,KAAK,EAAE,MAAM,GAAG,QAAQ,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC;IAC5C,KAAK,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACrC,MAAM,EAAE,kBAAkB,GAAG,IAAI,CAAC;CACnC,CAAC,CAAC;AAMH;;;GAGG;AACH,wBAAgB,4BAA4B,CAAC,SAAS,EAAE,SAAS,sBAAsB,EAAE,GAAG,kBAAkB,EAAE,CAqB/G;AAED;;;;GAIG;AACH,wBAAgB,6BAA6B,CAC3C,KAAK,EAAE,SAAS,kBAAkB,EAAE,EACpC,MAAM,EAAE,oBAAoB,EAC5B,OAAO,GAAE,QAAQ,CAAC;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAM,GACjE,sBAAsB,CAoExB"}
|
|
@@ -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":"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","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를 사용한다.
|