@hjmds/design-contracts 1.19.0 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/dist/authoring-tools.d.ts +38 -0
  2. package/dist/authoring-tools.d.ts.map +1 -0
  3. package/dist/authoring-tools.js +35 -0
  4. package/dist/authoring-tools.js.map +1 -0
  5. package/dist/behaviors.d.ts +34 -3
  6. package/dist/behaviors.d.ts.map +1 -1
  7. package/dist/behaviors.js +2 -0
  8. package/dist/behaviors.js.map +1 -1
  9. package/dist/bottom-navigation.d.ts +10 -0
  10. package/dist/bottom-navigation.d.ts.map +1 -1
  11. package/dist/bottom-navigation.js +10 -0
  12. package/dist/bottom-navigation.js.map +1 -1
  13. package/dist/calendar.d.ts +9 -1
  14. package/dist/calendar.d.ts.map +1 -1
  15. package/dist/calendar.js +4 -2
  16. package/dist/calendar.js.map +1 -1
  17. package/dist/catalog.d.ts +148 -2
  18. package/dist/catalog.d.ts.map +1 -1
  19. package/dist/catalog.js +6 -2
  20. package/dist/catalog.js.map +1 -1
  21. package/dist/chart.d.ts +398 -0
  22. package/dist/chart.d.ts.map +1 -0
  23. package/dist/chart.js +414 -0
  24. package/dist/chart.js.map +1 -0
  25. package/dist/collection-editing.d.ts +38 -0
  26. package/dist/collection-editing.d.ts.map +1 -0
  27. package/dist/collection-editing.js +53 -0
  28. package/dist/collection-editing.js.map +1 -0
  29. package/dist/communication.d.ts +29 -0
  30. package/dist/communication.d.ts.map +1 -0
  31. package/dist/communication.js +11 -0
  32. package/dist/communication.js.map +1 -0
  33. package/dist/component-definitions.d.ts +1 -0
  34. package/dist/component-definitions.d.ts.map +1 -1
  35. package/dist/component-definitions.js +1 -0
  36. package/dist/component-definitions.js.map +1 -1
  37. package/dist/component-recipes.d.ts +25 -0
  38. package/dist/component-recipes.d.ts.map +1 -1
  39. package/dist/component-recipes.js +13 -0
  40. package/dist/component-recipes.js.map +1 -1
  41. package/dist/data-table.d.ts +229 -5
  42. package/dist/data-table.d.ts.map +1 -1
  43. package/dist/data-table.js +242 -6
  44. package/dist/data-table.js.map +1 -1
  45. package/dist/data-workflows.d.ts +70 -0
  46. package/dist/data-workflows.d.ts.map +1 -0
  47. package/dist/data-workflows.js +109 -0
  48. package/dist/data-workflows.js.map +1 -0
  49. package/dist/dataviz.d.ts +7 -6
  50. package/dist/dataviz.d.ts.map +1 -1
  51. package/dist/dataviz.js +7 -6
  52. package/dist/dataviz.js.map +1 -1
  53. package/dist/gooey-navigation.d.ts +7 -0
  54. package/dist/gooey-navigation.d.ts.map +1 -1
  55. package/dist/gooey-navigation.js +10 -0
  56. package/dist/gooey-navigation.js.map +1 -1
  57. package/dist/index.d.ts +2 -1
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +1 -0
  60. package/dist/index.js.map +1 -1
  61. package/dist/media-transport.d.ts +16 -0
  62. package/dist/media-transport.d.ts.map +1 -0
  63. package/dist/media-transport.js +17 -0
  64. package/dist/media-transport.js.map +1 -0
  65. package/dist/recipes.d.ts +1 -0
  66. package/dist/recipes.d.ts.map +1 -1
  67. package/dist/recipes.js +1 -0
  68. package/dist/recipes.js.map +1 -1
  69. package/dist/semantic-colors.d.ts +9 -0
  70. package/dist/semantic-colors.d.ts.map +1 -1
  71. package/dist/semantic-colors.js +5 -0
  72. package/dist/semantic-colors.js.map +1 -1
  73. package/dist/showcase.d.ts.map +1 -1
  74. package/dist/showcase.js +3 -1
  75. package/dist/showcase.js.map +1 -1
  76. package/dist/social-compositions.d.ts +19 -0
  77. package/dist/social-compositions.d.ts.map +1 -0
  78. package/dist/social-compositions.js +33 -0
  79. package/dist/social-compositions.js.map +1 -0
  80. package/dist/streaming-answer.d.ts +18 -0
  81. package/dist/streaming-answer.d.ts.map +1 -0
  82. package/dist/streaming-answer.js +61 -0
  83. package/dist/streaming-answer.js.map +1 -0
  84. package/dist/task-controls.d.ts +31 -0
  85. package/dist/task-controls.d.ts.map +1 -0
  86. package/dist/task-controls.js +25 -0
  87. package/dist/task-controls.js.map +1 -0
  88. package/dist/toggle-feedback.d.ts +22 -0
  89. package/dist/toggle-feedback.d.ts.map +1 -0
  90. package/dist/toggle-feedback.js +32 -0
  91. package/dist/toggle-feedback.js.map +1 -0
  92. package/dist/transfer-task.d.ts +13 -0
  93. package/dist/transfer-task.d.ts.map +1 -0
  94. package/dist/transfer-task.js +13 -0
  95. package/dist/transfer-task.js.map +1 -0
  96. package/dist/version.d.ts +1 -1
  97. package/dist/version.js +1 -1
  98. package/dist/version.js.map +1 -1
  99. package/docs/authoring-tools.md +31 -0
  100. package/docs/bottom-navigation.md +11 -0
  101. package/docs/calendar.md +3 -1
  102. package/docs/catalog-freeze.json +5 -0
  103. package/docs/chart.md +206 -0
  104. package/docs/collection-editing.md +83 -0
  105. package/docs/communication.md +35 -0
  106. package/docs/data-table.md +56 -6
  107. package/docs/data-workflows.md +26 -0
  108. package/docs/generated/component-maturity.md +6 -5
  109. package/docs/generated/renderer-evidence.json +95 -5
  110. package/docs/generated/renderer-evidence.md +6 -3
  111. package/docs/generated/showcase-manifest.json +66 -3
  112. package/docs/gooey-navigation.md +20 -0
  113. package/docs/library-gap-analysis.md +4 -2
  114. package/docs/media-transport.md +37 -0
  115. package/docs/social-compositions.md +51 -0
  116. package/docs/streaming-answer.md +57 -0
  117. package/docs/task-controls.md +38 -0
  118. package/docs/toggle-feedback.md +56 -0
  119. package/docs/transfer-task.md +33 -0
  120. package/docs/usage/README.md +15 -4
  121. package/docs/usage/components/activity-chip.md +2 -2
  122. package/docs/usage/components/bottom-navigation.md +14 -1
  123. package/docs/usage/components/button.md +39 -0
  124. package/docs/usage/components/calendar.md +4 -2
  125. package/docs/usage/components/chart.md +277 -0
  126. package/docs/usage/components/data-table.md +92 -18
  127. package/docs/usage/components/screen-layout.md +3 -1
  128. package/docs/usage/components/tabs.md +25 -0
  129. package/docs/usage/components/toast.md +1 -1
  130. package/docs/usage/compositions/action-feedback.md +18 -2
  131. package/docs/usage/compositions/activity-status.md +2 -2
  132. package/docs/usage/compositions/authoring-tools.md +90 -0
  133. package/docs/usage/compositions/collection-editing.md +110 -0
  134. package/docs/usage/compositions/communication.md +100 -0
  135. package/docs/usage/compositions/data-workflows.md +91 -0
  136. package/docs/usage/compositions/media-transport.md +87 -0
  137. package/docs/usage/compositions/social-compositions.md +91 -0
  138. package/docs/usage/compositions/streaming-answer.md +97 -0
  139. package/docs/usage/compositions/task-controls.md +93 -0
  140. package/docs/usage/compositions/toggle-feedback.md +84 -0
  141. package/docs/usage/compositions/transfer-task.md +86 -0
  142. package/docs/usage/compositions/web-additions.md +2 -2
  143. package/docs/usage/screens/common-shell.md +4 -0
  144. package/package.json +67 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"social-compositions.js","sourceRoot":"","sources":["../src/social-compositions.ts"],"names":[],"mappings":"AAIA,SAAS,KAAK,CAAC,KAAa;IAC1B,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,iDAAiD,CAAC,CAAC;IACvH,OAAO,KAAK,CAAC;AACf,CAAC;AACD,MAAM,UAAU,qBAAqB,CAAC,KAAyB,EAAE,SAAkB;IACjF,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACnB,IAAI,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,KAAK,KAAK,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,4DAA4D,CAAC,CAAC;IAC7H,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC,SAAS,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC;AAC/G,CAAC;AACD,MAAM,UAAU,uBAAuB,CAAC,OAAoC,EAAE,EAAiB;IAC7F,MAAM,GAAG,GAAG,IAAI,GAAG,EAAU,CAAC;IAC9B,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;YAAE,MAAM,IAAI,SAAS,CAAC,+CAA+C,CAAC,CAAC;QAClH,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACrB,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,SAAS,CAAC,sCAAsC,CAAC,CAAC;IACtF,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;IACxD,IAAI,EAAE,KAAK,IAAI,IAAI,CAAC,MAAM;QAAE,MAAM,IAAI,SAAS,CAAC,6BAA6B,CAAC,CAAC;IAC/E,OAAO,MAAM,CAAC;AAChB,CAAC;AACD,MAAM,UAAU,yBAAyB,CAAC,KAA2B,EAAE,OAAoC,EAAE,UAAyB;IACpI,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACnB,MAAM,QAAQ,GAAG,uBAAuB,CAAC,OAAO,EAAE,KAAK,CAAC,UAAU,CAAC,EAAE,IAAI,GAAG,uBAAuB,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IACzH,IAAI,QAAQ,EAAE,SAAS,IAAI,KAAK,CAAC,KAAK,KAAK,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,+DAA+D,CAAC,CAAC;IACpI,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAC,GAAG,MAAM,CAAC,QAAQ,EAAE,SAAS,KAAK,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC;AAC5I,CAAC","sourcesContent":["/** Product-confirmed counts. No network, authentication or persistence lives here. */\nexport type FollowProfileState = Readonly<{ following: boolean; count: number }>;\nexport type AttendanceChoice = Readonly<{ id: string; label: string; attending: boolean; disabled?: boolean }>;\nexport type EventAttendanceState = Readonly<{ responseId: string | null; count: number }>;\nfunction count(value: number) {\n if (!Number.isSafeInteger(value) || value < 0) throw new RangeError(\"Social counts must be nonnegative safe integers\");\n return value;\n}\nexport function resolveFollowProposal(state: FollowProfileState, following: boolean): FollowProfileState {\n count(state.count);\n if (state.following && state.count === 0) throw new RangeError(\"A followed profile must include this follower in its count\");\n return Object.freeze({ following, count: count(state.count + Number(following) - Number(state.following)) });\n}\nexport function resolveAttendanceChoice(choices: readonly AttendanceChoice[], id: string | null): AttendanceChoice | undefined {\n const ids = new Set<string>();\n for (const choice of choices) {\n if (!choice.id.trim() || ids.has(choice.id)) throw new TypeError(\"Attendance choices need distinct nonempty ids\");\n ids.add(choice.id);\n }\n if (choices.length === 0) throw new TypeError(\"Attendance choices must not be empty\");\n const choice = choices.find(choice => choice.id === id);\n if (id !== null && !choice) throw new TypeError(\"Unknown attendance response\");\n return choice;\n}\nexport function resolveAttendanceProposal(state: EventAttendanceState, choices: readonly AttendanceChoice[], responseId: string | null): EventAttendanceState {\n count(state.count);\n const previous = resolveAttendanceChoice(choices, state.responseId), next = resolveAttendanceChoice(choices, responseId);\n if (previous?.attending && state.count === 0) throw new RangeError(\"An attending response must include this attendee in its count\");\n return Object.freeze({ responseId, count: count(state.count + Number(next?.attending === true) - Number(previous?.attending === true)) });\n}\n"]}
@@ -0,0 +1,18 @@
1
+ export type AnswerStreamEvent = Readonly<{
2
+ type: "delta";
3
+ text: string;
4
+ }> | Readonly<{
5
+ type: "done";
6
+ }>;
7
+ export type AnswerStreamResult = Readonly<{
8
+ phase: "complete" | "stopped";
9
+ text: string;
10
+ }>;
11
+ export type AnswerGenerationContext = Readonly<{
12
+ signal: AbortSignal;
13
+ previousText: string;
14
+ regenerate: boolean;
15
+ }>;
16
+ /** Consume actual provider events, never a timer or character-replay queue. */
17
+ export declare function consumeAnswerStream(source: AsyncIterable<AnswerStreamEvent>, signal: AbortSignal, previousText: string, report: (text: string) => void): Promise<AnswerStreamResult>;
18
+ //# sourceMappingURL=streaming-answer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streaming-answer.d.ts","sourceRoot":"","sources":["../src/streaming-answer.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,QAAQ,CAAC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AACvG,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;IAAE,KAAK,EAAE,UAAU,GAAG,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAC3F,MAAM,MAAM,uBAAuB,GAAG,QAAQ,CAAC;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,CAAC,CAAC;AAOnH,+EAA+E;AAC/E,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,aAAa,CAAC,iBAAiB,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAwB1L"}
@@ -0,0 +1,61 @@
1
+ async function closeIterator(iterator) {
2
+ if (!iterator.return)
3
+ return;
4
+ // Async generators can yield from finally; return() alone is not a closed-stream acknowledgement.
5
+ let result = await iterator.return();
6
+ while (!result.done)
7
+ result = await iterator.next();
8
+ }
9
+ /** Consume actual provider events, never a timer or character-replay queue. */
10
+ export async function consumeAnswerStream(source, signal, previousText, report) {
11
+ const iterator = source[Symbol.asyncIterator]();
12
+ let text = previousText, received = false, ended = false;
13
+ try {
14
+ while (true) {
15
+ // A provider must connect signal to its transport. return() acknowledges early stream closure.
16
+ if (signal.aborted && iterator.return) {
17
+ await closeIterator(iterator);
18
+ ended = true;
19
+ return { phase: "stopped", text };
20
+ }
21
+ const next = await iterator.next();
22
+ if (next.done) {
23
+ ended = true;
24
+ if (signal.aborted)
25
+ return { phase: "stopped", text };
26
+ throw new Error("Answer stream ended without its provider completion event");
27
+ }
28
+ if (next.value.type === "done")
29
+ return { phase: "complete", text: received ? text : "" };
30
+ if (signal.aborted)
31
+ continue;
32
+ if (next.value.type !== "delta" || typeof next.value.text !== "string")
33
+ throw new TypeError("Invalid answer stream event");
34
+ if (!next.value.text)
35
+ continue;
36
+ if (!received) {
37
+ text = "";
38
+ received = true;
39
+ }
40
+ text += next.value.text;
41
+ report(text);
42
+ }
43
+ }
44
+ catch (error) {
45
+ if (signal.aborted && error && typeof error === "object" && "name" in error && error.name === "AbortError")
46
+ return { phase: "stopped", text };
47
+ throw error;
48
+ }
49
+ finally {
50
+ if (!ended) {
51
+ try {
52
+ await closeIterator(iterator);
53
+ }
54
+ catch (error) {
55
+ if (!(signal.aborted && error && typeof error === "object" && "name" in error && error.name === "AbortError"))
56
+ throw error;
57
+ }
58
+ }
59
+ }
60
+ }
61
+ //# sourceMappingURL=streaming-answer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streaming-answer.js","sourceRoot":"","sources":["../src/streaming-answer.ts"],"names":[],"mappings":"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","sourcesContent":["export type AnswerStreamEvent = Readonly<{ type: \"delta\"; text: string }> | Readonly<{ type: \"done\" }>;\nexport type AnswerStreamResult = Readonly<{ phase: \"complete\" | \"stopped\"; text: string }>;\nexport type AnswerGenerationContext = Readonly<{ signal: AbortSignal; previousText: string; regenerate: boolean }>;\nasync function closeIterator(iterator: AsyncIterator<AnswerStreamEvent>) {\n if (!iterator.return) return;\n // Async generators can yield from finally; return() alone is not a closed-stream acknowledgement.\n let result = await iterator.return();\n while (!result.done) result = await iterator.next();\n}\n/** Consume actual provider events, never a timer or character-replay queue. */\nexport async function consumeAnswerStream(source: AsyncIterable<AnswerStreamEvent>, signal: AbortSignal, previousText: string, report: (text: string) => void): Promise<AnswerStreamResult> {\n const iterator = source[Symbol.asyncIterator](); let text = previousText, received = false, ended = false;\n try {\n while (true) {\n // A provider must connect signal to its transport. return() acknowledges early stream closure.\n if (signal.aborted && iterator.return) { await closeIterator(iterator); ended = true; return { phase: \"stopped\", text }; }\n const next = await iterator.next();\n if (next.done) { ended = true; if (signal.aborted) return { phase: \"stopped\", text }; throw new Error(\"Answer stream ended without its provider completion event\"); }\n if (next.value.type === \"done\") return { phase: \"complete\", text: received ? text : \"\" };\n if (signal.aborted) continue;\n if (next.value.type !== \"delta\" || typeof next.value.text !== \"string\") throw new TypeError(\"Invalid answer stream event\");\n if (!next.value.text) continue;\n if (!received) { text = \"\"; received = true; }\n text += next.value.text; report(text);\n }\n } catch (error) {\n if (signal.aborted && error && typeof error === \"object\" && \"name\" in error && error.name === \"AbortError\") return { phase: \"stopped\", text };\n throw error;\n } finally {\n if (!ended) {\n try { await closeIterator(iterator); }\n catch (error) { if (!(signal.aborted && error && typeof error === \"object\" && \"name\" in error && error.name === \"AbortError\")) throw error; }\n }\n }\n}\n"]}
@@ -0,0 +1,31 @@
1
+ export type TaskChoice = Readonly<{
2
+ id: string;
3
+ label: string;
4
+ description?: string;
5
+ disabled?: boolean;
6
+ }>;
7
+ export declare function resolveTaskChoice<T extends TaskChoice>(choices: readonly T[], selectedId: string): T;
8
+ export type PasswordRule = Readonly<{
9
+ id: string;
10
+ label: string;
11
+ test: (value: string) => boolean;
12
+ }>;
13
+ /** Counts supplied requirements only; entropy and password security are not inferred. */
14
+ export declare function resolvePasswordRules(value: string, rules: readonly PasswordRule[]): {
15
+ rules: {
16
+ id: string;
17
+ label: string;
18
+ satisfied: boolean;
19
+ }[];
20
+ satisfied: number;
21
+ total: number;
22
+ };
23
+ export type PriceOption = TaskChoice & Readonly<{
24
+ amountMinor: number;
25
+ currency: string;
26
+ interval: "month" | "year";
27
+ intervalCount: number;
28
+ benefits: readonly string[];
29
+ }>;
30
+ export declare function resolvePriceOption(options: readonly PriceOption[], selectedId: string): PriceOption;
31
+ //# sourceMappingURL=task-controls.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task-controls.d.ts","sourceRoot":"","sources":["../src/task-controls.ts"],"names":[],"mappings":"AACA,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CAAC;AAC3G,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,UAAU,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,CAAC,CAKpG;AACD,MAAM,MAAM,YAAY,GAAG,QAAQ,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAA;CAAE,CAAC,CAAC;AACrG,yFAAyF;AACzF,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,YAAY,EAAE;;;;;;;;EAKjF;AACD,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,QAAQ,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAAC,CAAC;AAC3K,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,EAAE,UAAU,EAAE,MAAM,eAMrF"}
@@ -0,0 +1,25 @@
1
+ import { assertDistinctIds } from "./collection-editing.js";
2
+ export function resolveTaskChoice(choices, selectedId) {
3
+ assertDistinctIds(choices);
4
+ const selected = choices.find(choice => choice.id === selectedId);
5
+ if (!selected)
6
+ throw new TypeError("The selected choice must exist");
7
+ return selected;
8
+ }
9
+ /** Counts supplied requirements only; entropy and password security are not inferred. */
10
+ export function resolvePasswordRules(value, rules) {
11
+ assertDistinctIds(rules);
12
+ if (!rules.length)
13
+ throw new TypeError("Password rules must not be empty");
14
+ const results = rules.map(rule => ({ id: rule.id, label: rule.label, satisfied: rule.test(value) }));
15
+ return { rules: results, satisfied: results.filter(rule => rule.satisfied).length, total: results.length };
16
+ }
17
+ export function resolvePriceOption(options, selectedId) {
18
+ const selected = resolveTaskChoice(options, selectedId);
19
+ for (const option of options) {
20
+ if (!Number.isSafeInteger(option.amountMinor) || option.amountMinor < 0 || !/^[A-Z]{3}$/.test(option.currency) || !Number.isSafeInteger(option.intervalCount) || option.intervalCount < 1 || !["month", "year"].includes(option.interval))
21
+ throw new RangeError("Price options require actual minor-unit amounts, currency and billing periods");
22
+ }
23
+ return selected;
24
+ }
25
+ //# sourceMappingURL=task-controls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task-controls.js","sourceRoot":"","sources":["../src/task-controls.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAE5D,MAAM,UAAU,iBAAiB,CAAuB,OAAqB,EAAE,UAAkB;IAC/F,iBAAiB,CAAC,OAAO,CAAC,CAAC;IAC3B,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,KAAK,UAAU,CAAC,CAAC;IAClE,IAAI,CAAC,QAAQ;QAAE,MAAM,IAAI,SAAS,CAAC,gCAAgC,CAAC,CAAC;IACrE,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,oBAAoB,CAAC,KAAa,EAAE,KAA8B;IAChF,iBAAiB,CAAC,KAAK,CAAC,CAAC;IACzB,IAAI,CAAC,KAAK,CAAC,MAAM;QAAE,MAAM,IAAI,SAAS,CAAC,kCAAkC,CAAC,CAAC;IAC3E,MAAM,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;IACrG,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC;AAC7G,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,OAA+B,EAAE,UAAkB;IACpF,MAAM,QAAQ,GAAG,iBAAiB,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IACxD,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,MAAM,CAAC,WAAW,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,MAAM,CAAC,aAAa,GAAG,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC;YAAE,MAAM,IAAI,UAAU,CAAC,+EAA+E,CAAC,CAAC;IACnV,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC","sourcesContent":["import { assertDistinctIds } from \"./collection-editing.js\";\nexport type TaskChoice = Readonly<{ id: string; label: string; description?: string; disabled?: boolean }>;\nexport function resolveTaskChoice<T extends TaskChoice>(choices: readonly T[], selectedId: string): T {\n assertDistinctIds(choices);\n const selected = choices.find(choice => choice.id === selectedId);\n if (!selected) throw new TypeError(\"The selected choice must exist\");\n return selected;\n}\nexport type PasswordRule = Readonly<{ id: string; label: string; test: (value: string) => boolean }>;\n/** Counts supplied requirements only; entropy and password security are not inferred. */\nexport function resolvePasswordRules(value: string, rules: readonly PasswordRule[]) {\n assertDistinctIds(rules);\n if (!rules.length) throw new TypeError(\"Password rules must not be empty\");\n const results = rules.map(rule => ({ id: rule.id, label: rule.label, satisfied: rule.test(value) }));\n return { rules: results, satisfied: results.filter(rule => rule.satisfied).length, total: results.length };\n}\nexport type PriceOption = TaskChoice & Readonly<{ amountMinor: number; currency: string; interval: \"month\" | \"year\"; intervalCount: number; benefits: readonly string[] }>;\nexport function resolvePriceOption(options: readonly PriceOption[], selectedId: string) {\n const selected = resolveTaskChoice(options, selectedId);\n for (const option of options) {\n if (!Number.isSafeInteger(option.amountMinor) || option.amountMinor < 0 || !/^[A-Z]{3}$/.test(option.currency) || !Number.isSafeInteger(option.intervalCount) || option.intervalCount < 1 || ![\"month\", \"year\"].includes(option.interval)) throw new RangeError(\"Price options require actual minor-unit amounts, currency and billing periods\");\n }\n return selected;\n}\n"]}
@@ -0,0 +1,22 @@
1
+ export type ToggleFeedbackEffect = "ripple" | "ribbon" | "burst";
2
+ export type ToggleFeedbackState = Readonly<{
3
+ selected: boolean;
4
+ count: number;
5
+ }>;
6
+ /** The product confirms the resulting count; this immutable proposal is pending feedback only. */
7
+ export declare function resolveToggleFeedbackProposal(state: ToggleFeedbackState, selected: boolean): ToggleFeedbackState;
8
+ export declare const toggleFeedbackRecipe: {
9
+ readonly duration: 320;
10
+ readonly glyphSize: 24;
11
+ readonly effectSize: 40;
12
+ };
13
+ export declare const toggleFeedbackPaths: Readonly<Record<ToggleFeedbackEffect, string>>;
14
+ /** Host geometry only, never product state or a timer-driven counter. p=1 clears every transient. */
15
+ export declare function resolveToggleFeedbackFrame(effect: ToggleFeedbackEffect, progress: number): Readonly<{
16
+ radius: number;
17
+ opacity: number;
18
+ rays: string;
19
+ translateY: number;
20
+ scale: number;
21
+ }>;
22
+ //# sourceMappingURL=toggle-feedback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toggle-feedback.d.ts","sourceRoot":"","sources":["../src/toggle-feedback.ts"],"names":[],"mappings":"AACA,MAAM,MAAM,oBAAoB,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;AACjE,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CAAC;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AACjF,kGAAkG;AAClG,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,mBAAmB,EAAE,QAAQ,EAAE,OAAO,GAAG,mBAAmB,CAKhH;AACD,eAAO,MAAM,oBAAoB;;;;CAAoE,CAAC;AACtG,eAAO,MAAM,mBAAmB,EAAE,QAAQ,CAAC,MAAM,CAAC,oBAAoB,EAAE,MAAM,CAAC,CAI9E,CAAC;AACF,qGAAqG;AACrG,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,oBAAoB,EAAE,QAAQ,EAAE,MAAM;;;;;;GAWxF"}
@@ -0,0 +1,32 @@
1
+ import { motion } from "./foundations.js";
2
+ /** The product confirms the resulting count; this immutable proposal is pending feedback only. */
3
+ export function resolveToggleFeedbackProposal(state, selected) {
4
+ if (!Number.isSafeInteger(state.count) || state.count < 0 || (state.selected && state.count === 0))
5
+ throw new RangeError("Invalid confirmed selection count");
6
+ const count = state.count + Number(selected) - Number(state.selected);
7
+ if (!Number.isSafeInteger(count) || count < 0)
8
+ throw new RangeError("Invalid proposed selection count");
9
+ return Object.freeze({ selected, count });
10
+ }
11
+ export const toggleFeedbackRecipe = { duration: motion.slow, glyphSize: 24, effectSize: 40 };
12
+ export const toggleFeedbackPaths = {
13
+ ripple: "M6 10C6 2 18 2 18 10V15L20 18H4L6 15ZM9 21H15",
14
+ ribbon: "M6 3H18V21L12 17L6 21Z",
15
+ burst: "M12 21L3.8 12.8C-2 7 6 0 12 7C18 0 26 7 20.2 12.8Z",
16
+ };
17
+ /** Host geometry only, never product state or a timer-driven counter. p=1 clears every transient. */
18
+ export function resolveToggleFeedbackFrame(effect, progress) {
19
+ if (!Object.hasOwn(toggleFeedbackPaths, effect))
20
+ throw new TypeError("Unknown toggle feedback effect");
21
+ if (!Number.isFinite(progress))
22
+ throw new TypeError("Feedback progress must be finite");
23
+ const p = Math.min(1, Math.max(0, progress)), opacity = 1 - p;
24
+ const rays = Array.from({ length: 8 }, (_, index) => {
25
+ const a = index * Math.PI / 4, inner = 9 + 7 * p, outer = 12 + 7 * p;
26
+ return `M${20 + Math.cos(a) * inner} ${20 + Math.sin(a) * inner}L${20 + Math.cos(a) * outer} ${20 + Math.sin(a) * outer}`;
27
+ }).join(" ");
28
+ const pulse = p === 1 ? 0 : Math.sin(Math.PI * p);
29
+ return Object.freeze({ radius: 2 + p * 16, opacity: effect === "ribbon" ? 0 : opacity,
30
+ rays, translateY: effect === "ribbon" && pulse !== 0 ? -6 * pulse : 0, scale: effect === "ripple" ? 1 : 1 + .12 * pulse });
31
+ }
32
+ //# sourceMappingURL=toggle-feedback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toggle-feedback.js","sourceRoot":"","sources":["../src/toggle-feedback.ts"],"names":[],"mappings":"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","sourcesContent":["import { motion } from \"./foundations.js\";\nexport type ToggleFeedbackEffect = \"ripple\" | \"ribbon\" | \"burst\";\nexport type ToggleFeedbackState = Readonly<{ selected: boolean; count: number }>;\n/** The product confirms the resulting count; this immutable proposal is pending feedback only. */\nexport function resolveToggleFeedbackProposal(state: ToggleFeedbackState, selected: boolean): ToggleFeedbackState {\n if (!Number.isSafeInteger(state.count) || state.count < 0 || (state.selected && state.count === 0)) throw new RangeError(\"Invalid confirmed selection count\");\n const count = state.count + Number(selected) - Number(state.selected);\n if (!Number.isSafeInteger(count) || count < 0) throw new RangeError(\"Invalid proposed selection count\");\n return Object.freeze({ selected, count });\n}\nexport const toggleFeedbackRecipe = { duration: motion.slow, glyphSize: 24, effectSize: 40 } as const;\nexport const toggleFeedbackPaths: Readonly<Record<ToggleFeedbackEffect, string>> = {\n ripple: \"M6 10C6 2 18 2 18 10V15L20 18H4L6 15ZM9 21H15\",\n ribbon: \"M6 3H18V21L12 17L6 21Z\",\n burst: \"M12 21L3.8 12.8C-2 7 6 0 12 7C18 0 26 7 20.2 12.8Z\",\n};\n/** Host geometry only, never product state or a timer-driven counter. p=1 clears every transient. */\nexport function resolveToggleFeedbackFrame(effect: ToggleFeedbackEffect, progress: number) {\n if (!Object.hasOwn(toggleFeedbackPaths, effect)) throw new TypeError(\"Unknown toggle feedback effect\");\n if (!Number.isFinite(progress)) throw new TypeError(\"Feedback progress must be finite\");\n const p = Math.min(1, Math.max(0, progress)), opacity = 1 - p;\n const rays = Array.from({ length: 8 }, (_, index) => {\n const a = index * Math.PI / 4, inner = 9 + 7 * p, outer = 12 + 7 * p;\n return `M${20 + Math.cos(a) * inner} ${20 + Math.sin(a) * inner}L${20 + Math.cos(a) * outer} ${20 + Math.sin(a) * outer}`;\n }).join(\" \");\n const pulse = p === 1 ? 0 : Math.sin(Math.PI * p);\n return Object.freeze({ radius: 2 + p * 16, opacity: effect === \"ribbon\" ? 0 : opacity,\n rays, translateY: effect === \"ribbon\" && pulse !== 0 ? -6 * pulse : 0, scale: effect === \"ripple\" ? 1 : 1 + .12 * pulse });\n}\n"]}
@@ -0,0 +1,13 @@
1
+ export type TransferObservation = Readonly<{
2
+ completed: number;
3
+ total: number | null;
4
+ }>;
5
+ /** Actual transport counts only. Learning the total never manufactures completed work. */
6
+ export declare function resolveTransferObservation(next: TransferObservation, previous?: TransferObservation | null): TransferObservation;
7
+ export type TransferContext = Readonly<{
8
+ signal: AbortSignal;
9
+ report: (observation: TransferObservation) => void;
10
+ }>;
11
+ /** An abort request is not proof of cancellation: the adapter must reject with AbortError. */
12
+ export declare function isConfirmedTransferAbort(error: unknown, signal: AbortSignal): boolean;
13
+ //# sourceMappingURL=transfer-task.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transfer-task.d.ts","sourceRoot":"","sources":["../src/transfer-task.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CAAC;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,CAAC;AACxF,0FAA0F;AAC1F,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,mBAAmB,EAAE,QAAQ,GAAE,mBAAmB,GAAG,IAAW,GAAG,mBAAmB,CAItI;AACD,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE,CAAC,WAAW,EAAE,mBAAmB,KAAK,IAAI,CAAA;CAAE,CAAC,CAAC;AACpH,8FAA8F;AAC9F,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAErF"}
@@ -0,0 +1,13 @@
1
+ /** Actual transport counts only. Learning the total never manufactures completed work. */
2
+ export function resolveTransferObservation(next, previous = null) {
3
+ if (!Number.isFinite(next.completed) || next.completed < 0 || (next.total !== null && (!Number.isFinite(next.total) || next.total <= 0 || next.completed > next.total)))
4
+ throw new RangeError("Transfer observations require nonnegative completed work and a positive known total");
5
+ if (previous && next.completed < previous.completed)
6
+ throw new RangeError("Transfer progress cannot rewind within one operation");
7
+ return Object.freeze({ completed: next.completed, total: next.total });
8
+ }
9
+ /** An abort request is not proof of cancellation: the adapter must reject with AbortError. */
10
+ export function isConfirmedTransferAbort(error, signal) {
11
+ return signal.aborted && error !== null && typeof error === "object" && "name" in error && error.name === "AbortError";
12
+ }
13
+ //# sourceMappingURL=transfer-task.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transfer-task.js","sourceRoot":"","sources":["../src/transfer-task.ts"],"names":[],"mappings":"AACA,0FAA0F;AAC1F,MAAM,UAAU,0BAA0B,CAAC,IAAyB,EAAE,WAAuC,IAAI;IAC/G,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,SAAS,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,KAAK,IAAI,IAAI,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,qFAAqF,CAAC,CAAC;IACrR,IAAI,QAAQ,IAAI,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC,SAAS;QAAE,MAAM,IAAI,UAAU,CAAC,sDAAsD,CAAC,CAAC;IAClI,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;AACzE,CAAC;AAED,8FAA8F;AAC9F,MAAM,UAAU,wBAAwB,CAAC,KAAc,EAAE,MAAmB;IAC1E,OAAO,MAAM,CAAC,OAAO,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,IAAI,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY,CAAC;AACzH,CAAC","sourcesContent":["export type TransferObservation = Readonly<{ completed: number; total: number | null }>;\n/** Actual transport counts only. Learning the total never manufactures completed work. */\nexport function resolveTransferObservation(next: TransferObservation, previous: TransferObservation | null = null): TransferObservation {\n if (!Number.isFinite(next.completed) || next.completed < 0 || (next.total !== null && (!Number.isFinite(next.total) || next.total <= 0 || next.completed > next.total))) throw new RangeError(\"Transfer observations require nonnegative completed work and a positive known total\");\n if (previous && next.completed < previous.completed) throw new RangeError(\"Transfer progress cannot rewind within one operation\");\n return Object.freeze({ completed: next.completed, total: next.total });\n}\nexport type TransferContext = Readonly<{ signal: AbortSignal; report: (observation: TransferObservation) => void }>;\n/** An abort request is not proof of cancellation: the adapter must reject with AbortError. */\nexport function isConfirmedTransferAbort(error: unknown, signal: AbortSignal): boolean {\n return signal.aborted && error !== null && typeof error === \"object\" && \"name\" in error && error.name === \"AbortError\";\n}\n"]}
package/dist/version.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  /** Package release shown by documentation surfaces. Kept in sync by a test. */
2
- export declare const designSystemVersion: "1.19.0";
2
+ export declare const designSystemVersion: "1.21.0";
3
3
  //# sourceMappingURL=version.d.ts.map
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  /** Package release shown by documentation surfaces. Kept in sync by a test. */
2
- export const designSystemVersion = "1.19.0";
2
+ export const designSystemVersion = "1.21.0";
3
3
  //# sourceMappingURL=version.js.map
@@ -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.19.0\" as const;\n"]}
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.21.0\" as const;\n"]}
@@ -0,0 +1,31 @@
1
+ # 편집 도구 구성 계약
2
+
3
+ 검토일: 2026-10-15 · Lucian 116·141 · Storybook 배포 · 미게시
4
+
5
+ `@hjmds/design-contracts/authoring-tools`, `@hjmds/react/authoring-tools`,
6
+ `@hjmds/react-native/authoring-tools` 선택 subpath로 제공한다.
7
+
8
+ ## BezierEditor (Web·Native)
9
+
10
+ - `value`는 `[x1,y1,x2,y2]` controlled 초안이다. 두 제어점의 네 좌표를 모두 Slider로 편집한다.
11
+ - `x1,x2`는 CSS와 같은 `[0,1]`, y는 기본 `[-2,2]` 안에서 overshoot를 허용한다. `yRange`로 실제 제품 범위를 지정한다.
12
+ - `validateBezier`는 유한 좌표와 x 범위를, renderer는 yRange와 양의 유한 duration을 검사한다. `cubicBezierPoint`는 매개변수 t의 실제 기하 좌표이며 시간 기반 easing 출력과 혼동하지 않는다.
13
+ - 곡선에는 제어선과 두 제어점이 표시된다. 미리 보기 버튼은 현재 네 좌표로 실제 움직임을 한 번 재생한다. Web은 CSS cubic-bezier/WAAPI, Native는 Easing.bezier/Animated를 사용한다. 프레임마다 React state를 갱신하지 않는다.
14
+ - 좌표·duration·활성/disabled·reduced motion 변경, unmount, 백그라운드 진입에 재생을 멈춘다. reduced motion에서는 이동을 재생하지 않는다.
15
+ - 미리 보기 트랙은 이동을 내부에서 clip한다. y overshoot의 실제 easing을 유지하면서 좁은 편집 패널 밖으로 도형이 넘치지 않게 한다.
16
+ - 편집은 `onValueChange`만 호출한다. 제품 모션 토큰·저장소를 자동으로 수정하지 않는다. 제품이 별도 검토·저장 동작을 소유한다.
17
+
18
+ ## ShortcutRecorder (Web 전용)
19
+
20
+ - 명시적 기록 버튼으로 포커스 영역에서 실제 KeyboardEvent의 물리적 `code`와 Ctrl/Alt/Shift/Meta를 기록한다. 전역 listener를 자동 등록하지 않는다.
21
+ - IME 조합·반복·AltGraph·수정키 단독은 무시하고, 일반 입력/탐색키 단독은 거부한다. Escape는 기록을 취소하며 Tab은 포커스 이동을 유지한다. 영역을 벗어나면 기록이 종료된다.
22
+ - `platform`은 제품이 지정한다. 일반 브라우저/OS 예약 조합과 제품 `reserved`, 현재 `conflicts`를 검사한다. 등록 직전 최신 props로 다시 검사한다. OS가 브라우저에 보내지 않는 키는 기록할 수 없으며 예약 목록은 모든 OS/브라우저 조합의 완전한 목록이 아니다.
23
+ - `normalizeShortcut`의 예: `Ctrl+Shift+KeyK`. 화면 표기는 제품이 현재 키보드 레이아웃에 맞춰 `formatShortcut`으로 제공한다.
24
+ - 기록된 초안과 실제 등록값 `value`는 분리한다. 명시적 등록 버튼의 `onRegister`가 실제 앱 단축키 서비스를 호출한다. Promise 완료 전 초안을 지우지 않고 중복 요청을 막는다. 실패하면 초안·기존 등록값을 보존하고 중앙 오류를 표시한다.
25
+ - 등록 서비스의 실행 범위(계정·작업공간·문서)가 바뀌면 제품이 해당 식별자로 recorder host에 React `key`를 지정한다. unmount는 구성의 늦은 상태 갱신을 무시하지만 이미 호출한 외부 등록을 취소하지 않는다. 제품 서비스는 AbortSignal 또는 세대/범위 확인으로 늦은 등록 완료를 거부하고 이전 listener를 해제해야 한다.
26
+ - Native는 이 API를 내보내지 않는다. `authoringToolsCapabilities.shortcutRecorder === false`로 명시한다. 네이티브 하드웨어 키보드 등록은 플랫폼별 제품 adapter의 책임이다.
27
+
28
+ ## 근거와 확인 범위
29
+
30
+ 원본 116의 고정 둘째 제어점과 141의 표시 중심 기록을 실제 controlled 편집·등록 계약으로 확장했다. 기존 Slider/Button/Stack/Text와 비동기 action helper를 재사용한다.
31
+ 계약·Web browser·Native host targeted 검사는 실제 좌표 변경, 예약/충돌, 실패 보존·중복 요청, 실제 preview 호출을 확인한다. Native 기기 애니메이션과 실제 제품 단축키 서비스·토큰 저장·npm 게시는 별도다.
@@ -169,3 +169,14 @@ Web·Native 모두 막대에는 surfaceAccent, 선택한 항목에는 primary/on
169
169
  연한 선택 카드가 도크와 구분되지 않는 사용자 피드백에 따라 선택한 카드 전체를 브랜드 색으로 채운다.
170
170
  안쪽 여백과 카드 사이 간격은 spacing.xxs, 카드 모서리는 radius.lg다.
171
171
  라우팅·누름 방지·키보드·접근성·배지는 기존 BottomNavigation 계약을 그대로 따른다.
172
+
173
+ ### 두께 있는 팻말 (2026-10-13)
174
+
175
+ Spint가 하단 탐색을 들판 게임의 나무 팻말처럼 보이게 요청해 `selection: "tile"`에 `surface: "raised"`를 추가했다.
176
+ 기본 `surface: "flat"`은 기존 선택 카드 외형을 유지한다. `raised`는 `selection: "tile"`에서만 허용된다.
177
+ 막대 바탕은 surfaceAlt(`surface.sunken`), 테두리는 borderControl이며 아래 테두리가 4로 두꺼워져 옆면이 된다.
178
+ floating 그림자는 그리지 않는다. 옆면이 높이를 표현하므로 그림자를 함께 두면 두 물체로 보인다.
179
+ 선택한 카드는 primary 위에 아래 테두리 3을 text 45% 반투명으로 겹쳐 더 진한 옆면을 만든다.
180
+ 팔레트에 "눌린 브랜드" 역할이 없어서 새 역할을 추가하지 않고 기존 역할을 겹쳐 쓴다.
181
+ 선택하지 않은 카드도 같은 두께의 투명 테두리를 둔다. 그래서 선택이 바뀌어도 아이콘·라벨 줄이 움직이지 않는다.
182
+ 제품은 나무 같은 색을 surfaceAlt·borderControl 팔레트 역할로 공급한다. deprecated 시각 style로 덮지 않는다.
package/docs/calendar.md CHANGED
@@ -49,7 +49,7 @@ import { Calendar } from "@hjmds/react/calendar";
49
49
  />
50
50
  ```
51
51
 
52
- 두 renderer는 `size="medium" | "large"`, `renderCellContent`,
52
+ 두 renderer는 `size="compact" | "medium" | "large"`, `renderCellContent`,
53
53
  `ref.current.focusDate(date)`를 지원한다. focus 요청 날짜가 다음 렌더에서 도착하면 그때 적용한다.
54
54
  Web의 `autoFocus`는 기본 false이며 DatePicker가 열린 격자에만 true로 설정한다.
55
55
 
@@ -83,3 +83,5 @@ Native 테스트는 개별 날짜의 접근성·비활성·선택, 명시적 월
83
83
  기기나 하드웨어 키보드 탐색 검증을 뜻하지 않는다.
84
84
  DatePicker의 기존 선택·닫기·방향키 회귀와 controlled-open 읽기 전용/disabled 활성화 방지도 검사한다.
85
85
  전체 패키지·Showcase 검증 및 브라우저 확인은 `react-native-completion.md` 실행 기록에 남긴다.
86
+
87
+ `compact`는 2026-10-14 경기 달력의 날짜 원 크기 피드백을 반영한 선택지다. 날짜 원은 28px로 표시하고, 날짜 버튼의 최소 44px 터치 영역은 유지한다. 기존 medium·large의 모양은 바꾸지 않는다.
@@ -8,6 +8,11 @@
8
8
  "name": "ThinkingOrb",
9
9
  "date": "2026-09-29",
10
10
  "rationale": "사용자가 Thinking Orbs의 HJM 흡수를 명시적으로 요청함. 기존 Spinner를 교체하지 않는 AI 상태 표시이며 선택형 경로로 격리. docs/thinking-orb.md"
11
+ },
12
+ {
13
+ "name": "Chart",
14
+ "date": "2026-10-14",
15
+ "rationale": "사용자가 차트·표 라이브러리 흡수를 명시적으로 요청함. 2026-09-29 토큰만 남기고 지운 Chart를 되살려 Line·Bar·Pie·Radar·Scatter·Sparkline·ChartLegend를 한 계약의 동반 API로 둔다. Yajalal이 같은 스크럽 선 차트·막대를 직접 다시 만든 것이 근거. docs/chart.md"
11
16
  }
12
17
  ],
13
18
  "names": [
package/docs/chart.md ADDED
@@ -0,0 +1,206 @@
1
+ # Chart contract
2
+
3
+ 검토일: 2026-10-14 · 계약: `src/chart.ts`(`@hjmds/design-contracts/components/chart`) · 색 토큰: `src/dataviz.ts`
4
+ · 사용 지침: [usage/components/chart.md](usage/components/chart.md)
5
+
6
+ **문제.** 사용자가 시간에 따른 값의 흐름(주가·기록·순위), 범주끼리의 크기 비교, 전체 안의 몫,
7
+ 여러 능력치의 모양, 두 수치의 관계를 한눈에 읽는다. 선 위를 손가락으로 훑어 특정 시점의 값을
8
+ 확인하고(스크럽), 누른 막대·조각의 값을 본다. 숫자 하나는 [Statistic](statistic.md), 진행 비율은
9
+ [Progress](progress.md), 연간 일별 밀도는 [ActivityHeatmap](activity-heatmap.md), 정확한 값의
10
+ 비교·정렬은 [DataTable](data-table.md)이 맡는다. 이 계약은 그 사이의 **그림으로 읽는 수치**를 맡는다.
11
+
12
+ ## 결정 번복 — 2026-10-14
13
+
14
+ **이전 결정.** 2026-09 라이브러리 격차 분석은 `Chart`를 "토큰만 채택"으로 닫았고
15
+ ([library-gap-analysis.md](library-gap-analysis.md)), 2026-09-29 카탈로그 정리에서 `Chart` 행을
16
+ 지웠다([catalog-cleanup.md](catalog-cleanup.md)). HJM은 계열 팔레트·축·격자·범례 토큰(`dataviz`)만
17
+ 고정하고 그리기는 제품 라이브러리(Recharts·victory-native·d3)에 맡겼다. 근거는 "축·툴팁·애니메이션·
18
+ 접근성을 다시 만들면 제품이 차트를 두 벌 갖게 된다"였다. `src/dataviz.ts` 머리 주석이 그 근거로 이
19
+ 문서를 가리켰지만 문서는 없었다.
20
+
21
+ **번복.** 2026-10-14 사용자 요청 "유명한 차트·표 라이브러리를 리버스 엔지니어링해서 우리 시스템에 흡수,
22
+ 여러 타입의 차트와 표 다 흡수"에 따라 그리기를 HJM으로 옮긴다. 이전 근거의 전제가 틀렸다는 실측이 있다.
23
+ 제품은 외부 라이브러리를 쓰지 않고 같은 것을 **직접 다시 만들었다** — Yajalal이 스크럽하는 선 차트
24
+ (`modules/mobile/src/components/market/StockChart.tsx`), 막대, 정렬되는 기록 표(`RecordTable.tsx`)를 자체 구현했다.
25
+ 빈 값 처리·축 눈금·정렬 규칙·접근성 문장을 제품 코드가 각자 정하게 된다. 두 벌이 생기는 것은 HJM이 그릴 때가
26
+ 아니라 HJM이 그리지 않을 때였다.
27
+
28
+ **바뀌지 않는 것.** `datavizSeriesPalette`·`resolveDatavizSeriesColor`·`datavizChromeTokens`의 값과 규칙은
29
+ 그대로다. HJM 렌더러가 이 토큰을 소비하는 첫 번째 사용자가 된다. 이 계약이 다루지 않는 종류(아래 "넣지 않은
30
+ 것")를 제품 라이브러리로 그리는 것은 여전히 허용하며, 그때도 색·축·격자는 `dataviz` 토큰을 쓴다.
31
+
32
+ ## 참고한 라이브러리
33
+
34
+ 공개 API와 사용 경험을 연구해 **개념만** 다시 유도했다. 어떤 라이브러리의 소스도 복사·번들하지 않았고
35
+ 렌더러는 새 런타임 의존성을 추가하지 않는다(Native는 기존 `react-native-svg` host를 쓴다).
36
+
37
+ | 라이브러리 | 가져온 개념 | 가져오지 않은 것 |
38
+ | --- | --- | --- |
39
+ | react-native-wagmi-charts | 선 + 커서 십자선, 포커스된 index가 가격·날짜 텍스트를 구동, 손을 떼면 최신 값으로 복귀 | Reanimated worklet 구조, 캔들스틱 |
40
+ | victory-native (XL) | x/y key로 묶는 cartesian 데이터, chart press state, 둥근 막대, domain padding | Skia 캔버스 의존, 임의 render-prop 레이어 |
41
+ | react-native-gifted-charts | 막대·선·파이·레이더의 prop 어휘(누르면 focus, 쌓은 막대, 도넛 가운데 내용) | 수백 개의 시각 prop, 내장 애니메이션 |
42
+ | Recharts | 계열/범주 데이터 모델, 쌓기, nice 눈금, 레이더 극좌표 격자 | 합성 child 컴포넌트 트리(`<XAxis>` 등), 임의 툴팁 컴포넌트 |
43
+ | d3-shape / d3-scale | monotone-X 3차 곡선(Fritsch–Carlson), "nice" 눈금 간격(1·2·2.5·5 × 10ⁿ), band scale | d3 패키지 자체(알고리즘만 `src/chart.ts`에 다시 썼다) |
44
+ | TanStack Table | `sortDescFirst`, `sortUndefined: "last"`, 안정 정렬 row model | headless 테이블 엔진 전체 |
45
+ | AG Grid | 열 고정(pinning) — 시작 쪽 연속 열만 | 그리드 편집·가상화·열 리사이즈 |
46
+
47
+ 표 두 줄(TanStack·AG Grid)은 [DataTable](data-table.md#정렬-helper와-native-renderer--2026-10-14)의 같은 날
48
+ 추가분이다. 차트와 표를 한 요청으로 흡수했기 때문에 출처를 한 곳에 적는다.
49
+
50
+ ## 차트 종류와 기하 함수
51
+
52
+ 모든 기하는 렌더러 중립 계산이다. Web(SVG DOM)과 Native(react-native-svg)는 같은 좌표·path·접근성 문장을 받아
53
+ 칠하기만 한다. 같은 입력이 두 플랫폼에서 다른 그림이 되는 것을 이 경계가 막는다.
54
+
55
+ | 종류 | 컴포넌트 | 기하 함수 | 데이터 |
56
+ | --- | --- | --- | --- |
57
+ | 선·영역(쌓은 영역 포함) | `LineChart` | `resolveLineGeometry`, `linePath`, `areaPath`, `splitSegments`, `stackValues` | `categories` + `ChartSeries[]` |
58
+ | 막대(세로·가로, 묶음·쌓기) | `BarChart` | `resolveBarGeometry`, `scaleBand`, `stackValues` | `categories` + `ChartSeries[]` |
59
+ | 파이·도넛 | `PieChart` | `resolvePieGeometry`, `arcPath` | `{ id, label, value, color? }[]` |
60
+ | 레이더 | `RadarChart` | `resolveRadarGeometry` | `categories`(3개 이상) + `ChartSeries[]` |
61
+ | 산점도 | `ScatterChart` | `resolveScatterGeometry`, `nearestScatterPoint` | `ScatterDatum[]` |
62
+ | 작은 추세선 | `Sparkline` | `linePath`, `resolveChartDomain`, `scaleLinear` | `(number \| null)[]` |
63
+ | 범례 | `ChartLegend` | — | `{ id, label, color, value? }[]` |
64
+
65
+ 공용 도구: `niceTicks`·`niceStep`(축 눈금), `resolveChartDomain`(값 범위), `scaleLinear`·`scaleBand`·`pointPositions`
66
+ (좌표 변환), `nearestChartIndex`·`stepChartIndex`·`observedIndices`(포커스), `seriesExtremes`(최고·최저 표시),
67
+ `describeChart`(텍스트 대안), `validateChartSeries`(입력 검증).
68
+
69
+ `ChartSeries`는 `{ id, label, values: (number | null)[], color? }`다. `values` 길이는 `categories` 길이와 같아야 하고,
70
+ `id`는 비어 있지 않고 중복이 없으며, `label`은 비어 있지 않아야 한다. 어기면 `TypeError`/`RangeError`가 난다.
71
+ 곡선은 `linear`(기본)·`monotone`·`step`, 막대 방향은 `vertical`(기본)·`horizontal`, 배치는 `grouped`(기본)·`stacked`다.
72
+
73
+ ## 그림 규칙
74
+
75
+ - **빈 값은 끊김이다. 0이 아니다.** `null`(또는 유한하지 않은 수)은 관측이 없다는 뜻이다. 선은 `splitSegments`로
76
+ 그 자리에서 끊기고 양옆을 잇지 않는다. 0으로 그리면 "기록 0"이라는 다른 사실을 말하고, 이으면 없는 추세를
77
+ 지어낸다. 막대는 그 자리를 비우고, 쌓기에서는 누적에 더하지 않는다. 텍스트 대안에서는 제품이 준
78
+ `missingLabel`로 읽힌다.
79
+ - **막대는 항상 0 기준선을 포함한다.** `resolveBarGeometry`는 `includeZero: true`로 범위를 잡는다. 막대 길이가
80
+ 값의 크기라서, 0이 아닌 곳에서 시작하면 두 배 차이가 열 배로 보인다. 선은 반대로 관측 범위로 확대한다
81
+ (주가처럼 작은 변화를 읽는 그림). 선에서도 0을 넣어야 하면 `includeZero`를 준다. 쌓은 선(영역)은 항상 0을 넣는다.
82
+ - **순위는 뒤집은 척도로 그린다.** `invert`(`scaleLinear`의 세 번째 인자)는 최솟값을 위에 둔다. 1위·평균자책점처럼
83
+ 작을수록 좋은 값이 위로 가야 "올라갔다 = 좋아졌다"가 유지된다.
84
+ - **레이더는 모든 축이 한 범위를 공유한다.** 기본 `[0, 모든 계열의 최댓값]`, 또는 `domain`으로 고정한다. 축마다 다른
85
+ 척도를 쓰면 다각형 넓이가 아무 뜻도 없어진다. 제품 평점이 한 0–100 척도라는 실측에 맞춘 선택이다. 축은 3개 이상이어야
86
+ 한다(`RangeError`).
87
+ - **파이는 음수와 빈 값을 무시한다.** 전체의 몫은 음수일 수 없다. `resolvePieGeometry`는 0 이하·빈 값을 합계와 조각에서
88
+ 모두 뺀다. 합계가 0이면 조각이 없다. 조각은 12시 방향부터 시계 방향이며 사이 간격은 `chartRecipe.pie.padAngle` 0.012rad다.
89
+ - **쌓기에서 음수는 0 아래로 따로 쌓인다**(`stackValues`의 양수·음수 누적 분리).
90
+ - **눈금은 읽기 좋은 수다.** `niceTicks`는 1·2·2.5·5 × 10ⁿ 간격만 쓴다(0/25/50, 0/23.7/47.4가 아니다).
91
+ - **곡선은 데이터를 넘어가지 않는다.** `monotone`은 Fritsch–Carlson monotone-X라 두 점 사이에서 최고·최저를 새로 만들지 않는다.
92
+ 일반 cubic/Catmull-Rom은 없는 고점을 그려 채택하지 않았다.
93
+ - **평평한 계열**은 위·아래 10%(값이 0이면 ±1)를 넓혀 가장자리에 붙지 않게 한다(`resolveChartDomain`).
94
+
95
+ ## 색
96
+
97
+ - 계열 색은 `series.color ?? resolveDatavizSeriesColor(theme, index)`다. 제품 브랜드 색이나 의미가 있는 계열(우리 팀 등)은
98
+ 제품이 `color`로 주고, 나머지는 `dataviz` 팔레트(light·dark 각 8색)를 index 순서로 쓴다. 9번째부터는 순환한다. 계열이 8개를
99
+ 넘는 화면은 대개 차트가 아니라 표가 맞다.
100
+ - **상태색(info·success·warning·attention)을 계열색으로 쓰지 않는다.** 상태색은 뜻을 가진 색이라 "3번 계열"에 쓰면
101
+ 사용자가 경고로 읽는다. 오르내림의 의미 표시는 차트 밖 [Statistic](statistic.md)의 trend가 맡는다.
102
+ - 축·격자·범례·커서 글자는 차트 전용 회색이 아니라 본문 토큰이다(`chartRecipe.chrome` = `datavizChromeTokens`: 축선 `border`,
103
+ 축 글자 `textMuted`, 격자 `border` 60%, 범례 `textBody`).
104
+ - 색만으로 계열을 구분하게 두지 않는다. 계열이 둘 이상이면 범례(`ChartLegend` 또는 내장 legend)에 이름을 쓰고, 값은 텍스트
105
+ 대안으로도 읽힌다.
106
+
107
+ ## 접근성
108
+
109
+ - **모든 차트는 이름과 텍스트 대안을 갖는다.** `accessibilityLabel`(Web에서는 `aria-label`로 나간다)은 필수다. 그림은
110
+ `role="img"`(Native image)이며, `describeChart(categories, series, formatValue, missingLabel)`가 "계열: 범주 값, 범주 값…"
111
+ 문장을 만든다. 숫자 서식과 빈 값 문구는 제품이 공급한다 — HJM은 숫자를 어떤 언어로도 직접 서식하지 않는다.
112
+ - **스크럽 가능한 선은 끌지 않고도 탐색된다.** Web은 `role="slider"`, `aria-valuetext`(포커스된 범주와 값), `ArrowLeft`/`ArrowRight`/
113
+ `Home`/`End`. Native는 `accessibilityRole="adjustable"`과 increment/decrement 동작이다. 둘 다 `stepChartIndex`로 **관측값이 있는
114
+ index만**(`observedIndices`) 이동한다 — 빈 칸에 멈추지 않는다.
115
+ - 막대·파이·산점도의 누름 선택은 같은 값을 텍스트 대안과 범례에서도 읽을 수 있으므로 별도 접근성 탐색 단위를 만들지 않는다.
116
+ - `Sparkline`은 `accessibilityLabel`이 없으면 장식으로 취급해 보조 기술에서 숨긴다. 옆에 같은 값을 말하는 Statistic이
117
+ 있는 자리(목록 행·표 셀)를 위한 기본값이다. 혼자 의미를 전달하면 이름을 준다.
118
+
119
+ ## 스크럽(LineChart)
120
+
121
+ 1. 누른 채 가로로 끌면 손가락 x에 가장 가까운 관측 index(`nearestChartIndex`, 후보 `observedIndices`)로 포커스가 옮겨 간다.
122
+ `onFocusChange(index, { scrubbing: true })`가 불리고, 커서 위에 `cursorLabel(index)`(기본 `categories[index]`) 말풍선이 뜬다.
123
+ 2. 손을 떼면 기본 포커스(마지막 관측 index)로 돌아가고 `onFocusChange(index, { scrubbing: false })`가 불린다. 제품의 "현재 값"
124
+ 텍스트는 이 콜백으로 갱신한다(wagmi-charts의 가격 텍스트와 같은 구조).
125
+ 3. 세로로 끄는 것은 페이지 스크롤이다. Native는 `|dy| > |dx|`일 때 PanResponder가 응답권을 넘기고, Web은 pointer event와
126
+ `touch-action: pan-y`로 같은 결과를 낸다. 스크롤 화면 안의 차트가 스크롤을 삼키지 않게 하는 규칙이다.
127
+ 4. 포커스된 점은 halo(`chartRecipe.line.focusHaloRadius` 9, 불투명도 0.25) + 점(반지름 4.5)과 세로 커서선으로 그린다. 끄는 중이
128
+ 아니면 커서선은 점선(`chartRecipe.cursor.dash` [2, 3])이다.
129
+ 5. 제어는 `focusIndex`(+ `onFocusChange`), 비제어는 `defaultFocusIndex`다. `extremes`를 주면 포커스가 없을 때 첫 계열의 최고·최저
130
+ (`seriesExtremes`)에 제품 문구 라벨을 붙인다.
131
+
132
+ `BarChart`는 누름·끌기로 범주를 고르고, 포커스가 있으면 나머지 막대를 `chartRecipe.bar.restOpacity` 0.4로 낮춘다.
133
+ `PieChart`는 조각 선택(`selectedId`/`onSelect`), `ScatterChart`는 가장 가까운 점(32px 안, `nearestScatterPoint`) 포커스를 쓴다.
134
+
135
+ ## HJM 기본값 (`chartRecipe`)
136
+
137
+ | 항목 | 값 |
138
+ | --- | --- |
139
+ | 높이 | `sparkline` 28 · `compact` 128 · `regular` 180(LineChart·BarChart 기본) · `tall` 240 |
140
+ | plot 여백(축 없음) | 위 `spacing.lg` 20 · 오른쪽 `spacing.xs` 8 · 아래 `spacing.xs` 8 · 왼쪽 `spacing.xs` 8 |
141
+ | plot 여백(축 있음) | 위 `spacing.md` 16 · 오른쪽 `spacing.xs` 8 · 아래 `spacing.lg` 20 · 왼쪽 36(y 눈금 글자 칸) |
142
+ | 선 | 두께 2.4 · 점 반지름 4.5 · 포커스 halo 9 / 0.25 |
143
+ | 영역 채움 | 위 0.32 → 아래 0 그라데이션 |
144
+ | 막대 | 모서리 `radius.sm`/2 = 4 · 최대 두께 28 · band padding 0.36 · 묶음 간격 2 · 비포커스 0.4 |
145
+ | 파이 | 도넛 안쪽 비율 0.62 · 조각 간격 0.012rad · 조각 라벨 최소 비율(`minLabelPercent`) 4% |
146
+ | 레이더 | 단계 4 · 선 두께 2 · 채움 0.22 · 라벨 거리 `spacing.sm` 12 |
147
+ | 산점도 | 점 반지름 5 · 포커스 8 |
148
+ | Sparkline | 72 × 28 · 선 두께 1.8 · 안쪽 여백 2 |
149
+ | 커서·기준선 | 커서 `stroke.subtle` 1, 점선 [2, 3] · 기준선 점선 [2, 4] |
150
+
151
+ ## 넣지 않은 것
152
+
153
+ 측정된 제품 요구가 없어 계약하지 않는다. 필요한 제품은 이 문서를 갱신하고 vertical slice로 축을 연다. 그 전에는 제품
154
+ 라이브러리를 써도 되고, 색·축은 `dataviz` 토큰을 쓴다.
155
+
156
+ - 캔들스틱(OHLC)·박스 플롯·히스토그램 bin 계산·지도·트리맵·sankey
157
+ - 확대·이동(zoom/pan), brush 범위 선택, 이중 y축, 로그 척도
158
+ - 임의 컴포넌트 툴팁 — 포커스 값은 제품이 `onFocusChange`로 받아 차트 밖 텍스트로 보여 준다(값이 화면 읽기 도구와 큰 글자
159
+ 배치에서 같은 자리에 남는다)
160
+ - 데이터 변경 사이 path morph 애니메이션, 실시간 스트리밍 창
161
+ - 차트 안 번역·숫자 서식 — 모든 문구와 서식은 제품 공급
162
+
163
+ ## 카탈로그
164
+
165
+ `Chart`는 2026-09-29에 지운 이름을 같은 이름으로 되살린다(`data-display`, Web·Native). 공개 컴포넌트 `LineChart`·`BarChart`·`PieChart`·
166
+ `RadarChart`·`ScatterChart`·`Sparkline`·`ChartLegend`는 이 한 계약의 동반 API다 — 같은 `ChartSeries` 모델·색 규칙·포커스·텍스트 대안을
167
+ 공유하므로 종류마다 계약을 따로 두지 않는다. 카탈로그 동결 예외는 `docs/catalog-freeze.json`에 기록한다.
168
+
169
+ ## Renderer와 검증
170
+
171
+ - Web `@hjmds/react/chart`(SVG DOM), Native `@hjmds/react-native/chart`(react-native-svg)는 같은 이름·prop을 쓴다.
172
+ - 계약 검사는 `test/chart.test.ts`(기하·빈 값·쌓기·척도·포커스·텍스트 대안)다.
173
+ - 2026-10-14 작성 시점에 두 renderer는 구현 중이다. renderer 테스트·Storybook(`배포/컴포넌트/데이터 표시/차트`)·기기 확인 결과는
174
+ 들어온 뒤 이 절에 따로 기록한다. 계약 검사 통과를 renderer 검증이나 제품 반영으로 보고하지 않는다.
175
+
176
+ ## 도넛 항목 제외 (2026-10-15, Lucian 097)
177
+
178
+ 선택 강조만으로 범주를 제외한 것처럼 보이던 차이를 해소하기 위해 PieChart에 controlled `exclusion`을 추가했다.
179
+ `excludedIds`, `onExcludedIdsChange`, 제품 문구 `label(datum,included)`를 전달한다. 기존 selectedId/onSelect는 강조만 소유하며 별도 범례 checkbox가 분모를 바꾼다.
180
+ 공유 `resolvePieVisibility`가 원본 인덱스를 보존해 색 변화를 막고, 합계와 geometry에서 제외값을 null로 처리한다.
181
+ `centerContent`는 기존 ReactNode 또는 `(summary) => ReactNode`를 받는다. summary.total은 실제 포함된 관측값 합계, observedCount는 실제 관측 수다.
182
+ 모두 제외/0/미관측이면 조각이 없고 합계 0이다. 100%나 가짜 조각을 만들지 않는다. 제외된 항목이 다시 포함되면 원래 색과 선택 강조를 되찾는다.
183
+ Native도 포함 여부 checkbox와 독립된 강조 행동을 제공한다. onExcludedIdsChange는 데이터 자체를 삭제하지 않는다.
184
+ 미게시 개발 API이며 제품 데이터/선택의 controlled 반영은 소비 제품이 소유한다.
185
+
186
+ ## 실제 데이터 교체와 스크럽 강조 (미게시 · 2026-10-15)
187
+
188
+ Lucian 095·096의 도형 전환을 별도 차트로 복제하지 않고 기존 `BarChart`·`LineChart`의
189
+ `animateChanges` 옵션으로 연결한다. 입력 계열·범주가 바뀌면 기존 기하 함수가 새 축·범위·실제
190
+ 값을 계산하고, 선/영역 경로 또는 막대 경계만 220ms 동안 이전 표현에서 새 표현으로 옮긴다.
191
+ 축·텍스트 대안·값 표시는 최신 입력을 즉시 사용한다. 전환 중간 경로는 관측값이 아니다.
192
+ Web은 SVG attribute, Native는 SVG `setNativeProps`를 갱신하며 프레임마다 React state를 만들지 않는다.
193
+ 같은 계열 id와 범주 문자열(+동일 문자열의 발생 순서)이 막대의 정체성이다. 비제어 선택도
194
+ 같은 범주를 따라가며 삭제되면 해제한다. 제어 선택은 소유자가 새 index로 갱신한다.
195
+
196
+ 기간의 점 개수가 다를 때 `prepareChartLineMorph`는 양쪽의 정규화된 꼭짓점 위치를 합친다.
197
+ `linear` 선은 양 끝의 관측 꼭짓점을 모두 보존한다. `monotone`·`step`도 같은 합친 점에서
198
+ 경로를 다시 계산하고 종료 프레임에서 원래 새 경로로 정확히 정착한다. 빈 관측은 기존의
199
+ 연속 구간별로 따로 처리하므로 없는 관측을 선으로 잇지 않는다. 선 구간 양 끝 점 합계가
200
+ 1,024개를 넘거나 막대가 200개를 넘으면 새 기하로 바로 교체해 프레임 비용을 제한한다.
201
+ reduced motion은 즉시 교체하고, background는 새 표현에 정착하며, unmount/새 입력은 이전
202
+ 프레임 작업을 취소한다. 고주기 실시간 데이터는 기본값 `animateChanges={false}`를 유지한다.
203
+
204
+ `LineChart.scrubEmphasis="before" | "after"`는 현재 포커스의 x에서 클립을 나누어 선택한
205
+ 쪽을 계열 색으로, 나머지를 축 레이블 색으로 표시한다. 새로운 점·수치·추천을 계산하지
206
+ 않으며 기존 드래그·키보드·접근성 포커스와 `holdOnRelease`를 그대로 사용한다.