@hjmds/design-contracts 1.39.0 → 1.41.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 (87) hide show
  1. package/dist/agreement.d.ts +2 -2
  2. package/dist/base-recipes.d.ts +11 -3
  3. package/dist/base-recipes.d.ts.map +1 -1
  4. package/dist/base-recipes.js +9 -4
  5. package/dist/base-recipes.js.map +1 -1
  6. package/dist/behaviors.d.ts +4 -4
  7. package/dist/card.d.ts +1 -1
  8. package/dist/card.js +1 -1
  9. package/dist/card.js.map +1 -1
  10. package/dist/catalog.d.ts +119 -85
  11. package/dist/catalog.d.ts.map +1 -1
  12. package/dist/choice-row.d.ts +26 -0
  13. package/dist/choice-row.d.ts.map +1 -0
  14. package/dist/choice-row.js +24 -0
  15. package/dist/choice-row.js.map +1 -0
  16. package/dist/colors.d.ts +2 -0
  17. package/dist/colors.d.ts.map +1 -1
  18. package/dist/colors.js +3 -1
  19. package/dist/colors.js.map +1 -1
  20. package/dist/command-palette.d.ts +2 -2
  21. package/dist/component-contracts.d.ts +2 -2
  22. package/dist/component-contracts.js +2 -2
  23. package/dist/component-contracts.js.map +1 -1
  24. package/dist/component-recipes.d.ts +76 -64
  25. package/dist/component-recipes.d.ts.map +1 -1
  26. package/dist/component-recipes.js +18 -16
  27. package/dist/component-recipes.js.map +1 -1
  28. package/dist/data-table.d.ts +1 -1
  29. package/dist/floating-action-button.d.ts +14 -2
  30. package/dist/floating-action-button.d.ts.map +1 -1
  31. package/dist/floating-action-button.js +5 -3
  32. package/dist/floating-action-button.js.map +1 -1
  33. package/dist/icon-button-recipe.d.ts +2 -2
  34. package/dist/icon-button-recipe.js +3 -3
  35. package/dist/icon-button-recipe.js.map +1 -1
  36. package/dist/menubar.d.ts +2 -2
  37. package/dist/palette-contrast.d.ts.map +1 -1
  38. package/dist/palette-contrast.js +2 -0
  39. package/dist/palette-contrast.js.map +1 -1
  40. package/dist/semantic-colors.d.ts +18 -1
  41. package/dist/semantic-colors.d.ts.map +1 -1
  42. package/dist/semantic-colors.js +7 -1
  43. package/dist/semantic-colors.js.map +1 -1
  44. package/dist/sidebar.d.ts +2 -2
  45. package/dist/time-picker.d.ts +16 -4
  46. package/dist/time-picker.d.ts.map +1 -1
  47. package/dist/time-picker.js +26 -5
  48. package/dist/time-picker.js.map +1 -1
  49. package/dist/toggle-group.d.ts +3 -3
  50. package/dist/toggle-group.js +3 -3
  51. package/dist/toggle-group.js.map +1 -1
  52. package/dist/transfer-list.d.ts +2 -2
  53. package/dist/tree.d.ts +2 -2
  54. package/dist/version.d.ts +1 -1
  55. package/dist/version.js +1 -1
  56. package/dist/version.js.map +1 -1
  57. package/docs/ant-design-coverage.md +6 -1
  58. package/docs/brand-boundary.md +1 -0
  59. package/docs/floating-action-button.md +6 -4
  60. package/docs/generated/component-maturity.md +1 -1
  61. package/docs/generated/renderer-evidence.json +3 -3
  62. package/docs/generated/renderer-evidence.md +1 -1
  63. package/docs/generated/showcase-manifest.json +1 -1
  64. package/docs/optional-adapters.md +1 -1
  65. package/docs/theme-palette.md +60 -0
  66. package/docs/time-picker.md +21 -0
  67. package/docs/toast.md +11 -0
  68. package/docs/usage/FINDING.md +71 -0
  69. package/docs/usage/README.md +5 -3
  70. package/docs/usage/SELECTION.md +8 -2
  71. package/docs/usage/components/bottom-navigation.md +5 -0
  72. package/docs/usage/components/card.md +2 -2
  73. package/docs/usage/components/chart.md +3 -1
  74. package/docs/usage/components/chip.md +1 -1
  75. package/docs/usage/components/choice-row.md +77 -0
  76. package/docs/usage/components/floating-action-button.md +5 -4
  77. package/docs/usage/components/image.md +18 -3
  78. package/docs/usage/components/keyboard-avoidance-context.md +3 -3
  79. package/docs/usage/components/segmented-control.md +1 -1
  80. package/docs/usage/components/sheet.md +1 -0
  81. package/docs/usage/components/surface.md +7 -1
  82. package/docs/usage/components/tabs.md +5 -0
  83. package/docs/usage/components/time-picker.md +20 -2
  84. package/docs/usage/components/toggle-group.md +5 -0
  85. package/docs/usage/compositions/save-feedback.md +43 -21
  86. package/docs/usage/tokens/color.md +17 -3
  87. package/package.json +7 -1
@@ -152,7 +152,7 @@ export declare const dataTableRecipe: {
152
152
  }>;
153
153
  readonly selectedBackground: Readonly<{
154
154
  source: "theme";
155
- key: "bg";
155
+ key: "surfaceSelected";
156
156
  alpha?: number;
157
157
  }>;
158
158
  };
@@ -71,9 +71,9 @@ export declare function resolveFloatingActionButtonLayoutMode(signal: FloatingAc
71
71
  * inset is additive, matching every other bottom-anchored recipe in this
72
72
  * package (BottomNavigation, Sheet, Toast) rather than a `max()` clamp.
73
73
  */
74
- export declare function resolveFloatingActionButtonContentClearance(safeAreaBottomInset: number): number;
74
+ export declare function resolveFloatingActionButtonContentClearance(safeAreaBottomInset: number, size?: "medium" | "large"): number;
75
75
  /**
76
- * Both layout modes reuse Button/IconButton's `large` tier so the control
76
+ * Both layout modes reuse the selected Button/IconButton tier so the control
77
77
  * never changes height while morphing between a circle and a pill — only
78
78
  * shape and content change, per the brief's "don't fork the Button recipe."
79
79
  */
@@ -87,6 +87,18 @@ export declare const floatingActionButtonRecipe: {
87
87
  readonly hitSlop: 0;
88
88
  readonly glyph: "lg";
89
89
  };
90
+ readonly sizes: {
91
+ readonly medium: {
92
+ readonly diameter: 44;
93
+ readonly hitSlop: 0;
94
+ readonly glyph: "md";
95
+ };
96
+ readonly large: {
97
+ readonly diameter: 52;
98
+ readonly hitSlop: 0;
99
+ readonly glyph: "lg";
100
+ };
101
+ };
90
102
  readonly tone: {
91
103
  readonly background: Readonly<{
92
104
  source: "theme";
@@ -1 +1 @@
1
- {"version":3,"file":"floating-action-button.d.ts","sourceRoot":"","sources":["../src/floating-action-button.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAElD;;;;;;;GAOG;AACH,MAAM,MAAM,8BAA8B,GAAG,UAAU,GAAG,WAAW,CAAC;AAEtE;;;;;;GAMG;AACH,MAAM,MAAM,gCAAgC,GACxC,iBAAiB,GACjB,cAAc,GACd,MAAM,CAAC;AAEX,MAAM,MAAM,kCAAkC,CAC5C,QAAQ,SAAS,MAAM,GAAG,gBAAgB,IACxC,QAAQ,CAAC;IACX,IAAI,EAAE,QAAQ,CAAC;IACf,UAAU,CAAC,EAAE,IAAI,CAAC;IAClB,kBAAkB,CAAC,EAAE,KAAK,CAAC;IAC3B,IAAI,CAAC,EAAE,KAAK,CAAC;IACb,IAAI,CAAC,EAAE,KAAK,CAAC;IACb,MAAM,CAAC,EAAE,KAAK,CAAC;IACf,cAAc,CAAC,EAAE,KAAK,CAAC;CACxB,CAAC,CAAC;AAEH,MAAM,MAAM,0CAA0C,CACpD,QAAQ,SAAS,MAAM,GAAG,gBAAgB,IACxC,QAAQ,CAAC;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,UAAU,EAAE,IAAI,CAAA;CAAE,CAAC,CAAC;AAEnD;;;;;GAKG;AACH,MAAM,MAAM,8BAA8B,CACxC,QAAQ,SAAS,MAAM,GAAG,gBAAgB,IACxC,QAAQ,CAAC;IACX,IAAI,EAAE,kCAAkC,CAAC,QAAQ,CAAC,CAAC;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,8BAA8B,CAAC;CAC7C,CAAC,CAAC;AAEH,MAAM,MAAM,sCAAsC,CAChD,QAAQ,SAAS,MAAM,GAAG,gBAAgB,IACxC,QAAQ,CAAC;IACX,IAAI,EAAE,0CAA0C,CAAC,QAAQ,CAAC,CAAC;IAC3D,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,8BAA8B,CAAC;IAC3C,0BAA0B,EAAE,MAAM,CAAC;CACpC,CAAC,CAAC;AAEH,eAAO,MAAM,oCAAoC;;CAE4B,CAAC;AAoC9E,wBAAgB,sCAAsC,CAAC,QAAQ,SAAS,MAAM,EAC5E,UAAU,EAAE,8BAA8B,CAAC,QAAQ,CAAC,GACnD,IAAI,CAsBN;AAED;;;;GAIG;AACH,wBAAgB,qCAAqC,CAAC,QAAQ,SAAS,MAAM,EAC3E,UAAU,EAAE,8BAA8B,CAAC,QAAQ,CAAC,GACnD,sCAAsC,CAAC,QAAQ,CAAC,CASlD;AAED;;;;;;GAMG;AACH,wBAAgB,qCAAqC,CACnD,MAAM,EAAE,gCAAgC,EACxC,YAAY,GAAE,8BAAgF,GAC7F,8BAA8B,CAYhC;AAED;;;;;;GAMG;AACH,wBAAgB,2CAA2C,CACzD,mBAAmB,EAAE,MAAM,GAC1B,MAAM,CAWR;AAED;;;;GAIG;AACH,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;IAMrC;;;;;;;;;OASG;;;;;;;;;;;;IAOH;;;;;OAKG;;;;;;CAEK,CAAC;AAEX;;;;GAIG;AACH,eAAO,MAAM,qCAAqC,kaAUxC,CAAC"}
1
+ {"version":3,"file":"floating-action-button.d.ts","sourceRoot":"","sources":["../src/floating-action-button.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAElD;;;;;;;GAOG;AACH,MAAM,MAAM,8BAA8B,GAAG,UAAU,GAAG,WAAW,CAAC;AAEtE;;;;;;GAMG;AACH,MAAM,MAAM,gCAAgC,GACxC,iBAAiB,GACjB,cAAc,GACd,MAAM,CAAC;AAEX,MAAM,MAAM,kCAAkC,CAC5C,QAAQ,SAAS,MAAM,GAAG,gBAAgB,IACxC,QAAQ,CAAC;IACX,IAAI,EAAE,QAAQ,CAAC;IACf,UAAU,CAAC,EAAE,IAAI,CAAC;IAClB,kBAAkB,CAAC,EAAE,KAAK,CAAC;IAC3B,IAAI,CAAC,EAAE,KAAK,CAAC;IACb,IAAI,CAAC,EAAE,KAAK,CAAC;IACb,MAAM,CAAC,EAAE,KAAK,CAAC;IACf,cAAc,CAAC,EAAE,KAAK,CAAC;CACxB,CAAC,CAAC;AAEH,MAAM,MAAM,0CAA0C,CACpD,QAAQ,SAAS,MAAM,GAAG,gBAAgB,IACxC,QAAQ,CAAC;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,UAAU,EAAE,IAAI,CAAA;CAAE,CAAC,CAAC;AAEnD;;;;;GAKG;AACH,MAAM,MAAM,8BAA8B,CACxC,QAAQ,SAAS,MAAM,GAAG,gBAAgB,IACxC,QAAQ,CAAC;IACX,IAAI,EAAE,kCAAkC,CAAC,QAAQ,CAAC,CAAC;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,8BAA8B,CAAC;CAC7C,CAAC,CAAC;AAEH,MAAM,MAAM,sCAAsC,CAChD,QAAQ,SAAS,MAAM,GAAG,gBAAgB,IACxC,QAAQ,CAAC;IACX,IAAI,EAAE,0CAA0C,CAAC,QAAQ,CAAC,CAAC;IAC3D,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,8BAA8B,CAAC;IAC3C,0BAA0B,EAAE,MAAM,CAAC;CACpC,CAAC,CAAC;AAEH,eAAO,MAAM,oCAAoC;;CAE4B,CAAC;AAoC9E,wBAAgB,sCAAsC,CAAC,QAAQ,SAAS,MAAM,EAC5E,UAAU,EAAE,8BAA8B,CAAC,QAAQ,CAAC,GACnD,IAAI,CAsBN;AAED;;;;GAIG;AACH,wBAAgB,qCAAqC,CAAC,QAAQ,SAAS,MAAM,EAC3E,UAAU,EAAE,8BAA8B,CAAC,QAAQ,CAAC,GACnD,sCAAsC,CAAC,QAAQ,CAAC,CASlD;AAED;;;;;;GAMG;AACH,wBAAgB,qCAAqC,CACnD,MAAM,EAAE,gCAAgC,EACxC,YAAY,GAAE,8BAAgF,GAC7F,8BAA8B,CAYhC;AAED;;;;;;GAMG;AACH,wBAAgB,2CAA2C,CACzD,mBAAmB,EAAE,MAAM,EAC3B,IAAI,GAAE,QAAQ,GAAG,OAAiB,GACjC,MAAM,CAWR;AAED;;;;GAIG;AACH,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAQrC;;;;;;;;;OASG;;;;;;;;;;;;IAOH;;;;;OAKG;;;;;;CAEK,CAAC;AAEX;;;;GAIG;AACH,eAAO,MAAM,qCAAqC,kaAUxC,CAAC"}
@@ -89,16 +89,16 @@ export function resolveFloatingActionButtonLayoutMode(signal, previousMode = flo
89
89
  * inset is additive, matching every other bottom-anchored recipe in this
90
90
  * package (BottomNavigation, Sheet, Toast) rather than a `max()` clamp.
91
91
  */
92
- export function resolveFloatingActionButtonContentClearance(safeAreaBottomInset) {
92
+ export function resolveFloatingActionButtonContentClearance(safeAreaBottomInset, size = "large") {
93
93
  if (!Number.isFinite(safeAreaBottomInset) || safeAreaBottomInset < 0) {
94
94
  throw new RangeError("FloatingActionButton safeAreaBottomInset must be a non-negative finite number");
95
95
  }
96
- return (floatingActionButtonRecipe.circle.diameter +
96
+ return (floatingActionButtonRecipe.sizes[size].diameter +
97
97
  floatingActionButtonRecipe.margin * 2 +
98
98
  safeAreaBottomInset);
99
99
  }
100
100
  /**
101
- * Both layout modes reuse Button/IconButton's `large` tier so the control
101
+ * Both layout modes reuse the selected Button/IconButton tier so the control
102
102
  * never changes height while morphing between a circle and a pill — only
103
103
  * shape and content change, per the brief's "don't fork the Button recipe."
104
104
  */
@@ -106,6 +106,8 @@ export const floatingActionButtonRecipe = {
106
106
  slots: ["root", "icon", "label"],
107
107
  defaults: { layoutMode: "expanded" },
108
108
  circle: iconButtonRecipe.sizes.large,
109
+ // BurnTok requested a smaller FAB; reuse existing control tiers rather than product overrides.
110
+ sizes: { medium: iconButtonRecipe.sizes.medium, large: iconButtonRecipe.sizes.large },
109
111
  tone: iconButtonRecipe.tones.primary,
110
112
  shape: iconButtonRecipe.shapes.circle,
111
113
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"floating-action-button.js","sourceRoot":"","sources":["../src/floating-action-button.ts"],"names":[],"mappings":"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","sourcesContent":["import { iconButtonRecipe } from \"./icon-button-recipe.js\";\nimport { motionPreset, shadow, spacing } from \"./foundations.js\";\nimport type { SemanticIconName } from \"./icon.js\";\n\n/**\n * FloatingActionButton is not a smaller BottomCTA. BottomCTA is the screen's\n * conclusion (full-width, end of a flow, `bottomCtaRecipe`'s full-bleed\n * anatomy). FloatingActionButton is a single create action that floats above\n * scrollable content the user is still browsing — it never spans the width\n * and never claims to be the screen's only action. That is the entire\n * justification for a second contract instead of a BottomCTA variant.\n */\nexport type FloatingActionButtonLayoutMode = \"expanded\" | \"collapsed\";\n\n/**\n * The renderer owns scroll listening and thresholding (same boundary as\n * LoadMore's IntersectionObserver/onEndReached and Popover's AnchoredOverlay —\n * this package never touches a scroll container). It only reports a discrete\n * signal; `resolveFloatingActionButtonLayoutMode` turns that into the next\n * mode without the renderer re-deriving the collapse/expand rule itself.\n */\nexport type FloatingActionButtonScrollSignal =\n | \"away-from-start\"\n | \"toward-start\"\n | \"idle\";\n\nexport type FloatingActionButtonIconDescriptor<\n IconName extends string = SemanticIconName,\n> = Readonly<{\n name: IconName;\n decorative?: true;\n accessibilityLabel?: never;\n size?: never;\n tone?: never;\n weight?: never;\n directionality?: never;\n}>;\n\nexport type ResolvedFloatingActionButtonIconDescriptor<\n IconName extends string = SemanticIconName,\n> = Readonly<{ name: IconName; decorative: true }>;\n\n/**\n * `label` is required even though `collapsed` mode shows only the icon.\n * The action's name must never shrink to nothing just because its visible\n * chrome did — `resolveFloatingActionButtonDescriptor` always reports the\n * full label as the accessible name in both layout modes.\n */\nexport type FloatingActionButtonDescriptor<\n IconName extends string = SemanticIconName,\n> = Readonly<{\n icon: FloatingActionButtonIconDescriptor<IconName>;\n label: string;\n layoutMode?: FloatingActionButtonLayoutMode;\n}>;\n\nexport type ResolvedFloatingActionButtonDescriptor<\n IconName extends string = SemanticIconName,\n> = Readonly<{\n icon: ResolvedFloatingActionButtonIconDescriptor<IconName>;\n label: string;\n layoutMode: FloatingActionButtonLayoutMode;\n resolvedAccessibilityLabel: string;\n}>;\n\nexport const floatingActionButtonBehaviorDefaults = {\n layoutMode: \"expanded\",\n} as const satisfies Readonly<{ layoutMode: FloatingActionButtonLayoutMode }>;\n\nfunction validateTrimmedCopy(value: unknown, field: string): asserts value is string {\n if (typeof value !== \"string\" || value.trim().length === 0) {\n throw new TypeError(`${field} must not be empty`);\n }\n if (value !== value.trim()) {\n throw new TypeError(`${field} must not start or end with whitespace`);\n }\n}\n\nfunction isObject(value: unknown): value is Readonly<Record<string, unknown>> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction rejectUnknownKeys(\n value: Readonly<Record<string, unknown>>,\n allowed: ReadonlySet<string>,\n field: string,\n): void {\n for (const key of Object.keys(value)) {\n if (!allowed.has(key)) {\n throw new TypeError(`Unsupported FloatingActionButton ${field} field: ${key}`);\n }\n }\n}\n\nconst descriptorKeys = new Set([\"icon\", \"label\", \"layoutMode\"]);\nconst iconKeys = new Set([\"name\", \"decorative\"]);\nconst layoutModes = new Set<FloatingActionButtonLayoutMode>([\"expanded\", \"collapsed\"]);\nconst scrollSignals = new Set<FloatingActionButtonScrollSignal>([\n \"away-from-start\",\n \"toward-start\",\n \"idle\",\n]);\n\nexport function validateFloatingActionButtonDescriptor<IconName extends string>(\n descriptor: FloatingActionButtonDescriptor<IconName>,\n): void {\n if (!isObject(descriptor)) {\n throw new TypeError(\"FloatingActionButton descriptor must be an object\");\n }\n rejectUnknownKeys(descriptor, descriptorKeys, \"descriptor\");\n validateTrimmedCopy(descriptor.label, \"FloatingActionButton label\");\n if (!isObject(descriptor.icon)) {\n throw new TypeError(\"FloatingActionButton icon must be an object\");\n }\n rejectUnknownKeys(descriptor.icon, iconKeys, \"icon\");\n if (descriptor.icon.decorative !== undefined && descriptor.icon.decorative !== true) {\n throw new TypeError(\"FloatingActionButton icon must be decorative\");\n }\n validateTrimmedCopy(descriptor.icon.name, \"FloatingActionButton icon name\");\n if (\n descriptor.layoutMode !== undefined &&\n !layoutModes.has(descriptor.layoutMode)\n ) {\n throw new TypeError(\n `Unsupported FloatingActionButton layoutMode: ${String(descriptor.layoutMode)}`,\n );\n }\n}\n\n/**\n * The label is the accessible name in every mode — `collapsed` never falls\n * back to the icon name, because an icon-only glyph is not a stable\n * accessible name across products or locales.\n */\nexport function resolveFloatingActionButtonDescriptor<IconName extends string>(\n descriptor: FloatingActionButtonDescriptor<IconName>,\n): ResolvedFloatingActionButtonDescriptor<IconName> {\n validateFloatingActionButtonDescriptor(descriptor);\n return {\n icon: { name: descriptor.icon.name, decorative: true },\n label: descriptor.label,\n layoutMode:\n descriptor.layoutMode ?? floatingActionButtonBehaviorDefaults.layoutMode,\n resolvedAccessibilityLabel: descriptor.label,\n };\n}\n\n/**\n * Collapsing on `away-from-start` and expanding on `toward-start` mirrors the\n * common list-scroll convention (hide the label while the user reads,\n * reintroduce it once they scroll back toward the top). `idle` intentionally\n * keeps whatever mode is already active instead of guessing, so a renderer\n * that reports \"no delta since last frame\" cannot cause the button to flicker.\n */\nexport function resolveFloatingActionButtonLayoutMode(\n signal: FloatingActionButtonScrollSignal,\n previousMode: FloatingActionButtonLayoutMode = floatingActionButtonBehaviorDefaults.layoutMode,\n): FloatingActionButtonLayoutMode {\n if (!scrollSignals.has(signal)) {\n throw new TypeError(`Unsupported FloatingActionButton scroll signal: ${String(signal)}`);\n }\n if (!layoutModes.has(previousMode)) {\n throw new TypeError(\n `Unsupported FloatingActionButton layoutMode: ${String(previousMode)}`,\n );\n }\n if (signal === \"away-from-start\") return \"collapsed\";\n if (signal === \"toward-start\") return \"expanded\";\n return previousMode;\n}\n\n/**\n * Answers \"who reserves the content's bottom padding\" so both platform\n * renderers compute the same number instead of each re-deriving\n * diameter + margin arithmetic independently and drifting apart. The safe-area\n * inset is additive, matching every other bottom-anchored recipe in this\n * package (BottomNavigation, Sheet, Toast) rather than a `max()` clamp.\n */\nexport function resolveFloatingActionButtonContentClearance(\n safeAreaBottomInset: number,\n): number {\n if (!Number.isFinite(safeAreaBottomInset) || safeAreaBottomInset < 0) {\n throw new RangeError(\n \"FloatingActionButton safeAreaBottomInset must be a non-negative finite number\",\n );\n }\n return (\n floatingActionButtonRecipe.circle.diameter +\n floatingActionButtonRecipe.margin * 2 +\n safeAreaBottomInset\n );\n}\n\n/**\n * Both layout modes reuse Button/IconButton's `large` tier so the control\n * never changes height while morphing between a circle and a pill — only\n * shape and content change, per the brief's \"don't fork the Button recipe.\"\n */\nexport const floatingActionButtonRecipe = {\n slots: [\"root\", \"icon\", \"label\"] as const,\n defaults: { layoutMode: \"expanded\" } as const,\n circle: iconButtonRecipe.sizes.large,\n tone: iconButtonRecipe.tones.primary,\n shape: iconButtonRecipe.shapes.circle,\n /**\n * Button의 large 티어와 같은 값이다. 그런데 `buttonRecipe`를 import하지는 않는다 —\n * 그것은 `recipes.ts`에 있고 그 파일은 `component-recipes.ts`를 import하는 **하류**라,\n * 여기서 거슬러 올라가면 순환이 된다(ESM은 재수출을 본문보다 먼저 평가해서\n * `buttonRecipe`가 undefined인 채로 이 모듈이 실행된다).\n *\n * 대신 같은 foundations 토큰을 직접 읽고, **두 값이 어긋나지 않는다는 것은\n * `test/floating-action-button.test.ts`의 불변식으로 강제한다.** 이 저장소가 규칙을\n * 다루는 방식대로 — 지켜야 할 것은 import가 아니라 테스트로 묶는다.\n */\n expandedLabel: {\n textVariant: \"bodyLarge\",\n paddingHorizontal: spacing.lg,\n },\n margin: spacing.md,\n shadow: shadow.floating,\n /**\n * `micro` (120ms, instant under Reduce Motion) rather than `enter`/`exit`:\n * this transition can fire on every scroll-direction change, and a state\n * driven by continuous scrolling should use the fastest state-change tier\n * so it cannot visibly queue behind fast scroll gestures.\n */\n transition: motionPreset.micro,\n} as const;\n\n/**\n * Literal scenario names for behaviorRegistry.floatingActionButton (lead\n * wires into src/behaviors.ts). Kept here, not there, so this module stays\n * self-contained per the authoring brief.\n */\nexport const floatingActionButtonBehaviorScenarios = [\n \"collapses-on-scroll-away-from-start-without-losing-the-44-unit-target\",\n \"expands-on-scroll-toward-start\",\n \"idle-scroll-signal-does-not-change-the-current-mode\",\n \"accessible-name-is-the-full-label-in-both-layout-modes\",\n \"content-clearance-accounts-for-the-safe-area-inset-additively\",\n \"root-sits-after-scrollable-content-in-reading-and-tab-order\",\n // The micro recipe already specifies instant reduced motion; the old\n // crossfade scenario contradicted that choice. See docs/floating-action-button.md.\n \"reduced-motion-switches-instantly-without-a-shape-tween\",\n] as const;\n"]}
1
+ {"version":3,"file":"floating-action-button.js","sourceRoot":"","sources":["../src/floating-action-button.ts"],"names":[],"mappings":"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","sourcesContent":["import { iconButtonRecipe } from \"./icon-button-recipe.js\";\nimport { motionPreset, shadow, spacing } from \"./foundations.js\";\nimport type { SemanticIconName } from \"./icon.js\";\n\n/**\n * FloatingActionButton is not a smaller BottomCTA. BottomCTA is the screen's\n * conclusion (full-width, end of a flow, `bottomCtaRecipe`'s full-bleed\n * anatomy). FloatingActionButton is a single create action that floats above\n * scrollable content the user is still browsing — it never spans the width\n * and never claims to be the screen's only action. That is the entire\n * justification for a second contract instead of a BottomCTA variant.\n */\nexport type FloatingActionButtonLayoutMode = \"expanded\" | \"collapsed\";\n\n/**\n * The renderer owns scroll listening and thresholding (same boundary as\n * LoadMore's IntersectionObserver/onEndReached and Popover's AnchoredOverlay —\n * this package never touches a scroll container). It only reports a discrete\n * signal; `resolveFloatingActionButtonLayoutMode` turns that into the next\n * mode without the renderer re-deriving the collapse/expand rule itself.\n */\nexport type FloatingActionButtonScrollSignal =\n | \"away-from-start\"\n | \"toward-start\"\n | \"idle\";\n\nexport type FloatingActionButtonIconDescriptor<\n IconName extends string = SemanticIconName,\n> = Readonly<{\n name: IconName;\n decorative?: true;\n accessibilityLabel?: never;\n size?: never;\n tone?: never;\n weight?: never;\n directionality?: never;\n}>;\n\nexport type ResolvedFloatingActionButtonIconDescriptor<\n IconName extends string = SemanticIconName,\n> = Readonly<{ name: IconName; decorative: true }>;\n\n/**\n * `label` is required even though `collapsed` mode shows only the icon.\n * The action's name must never shrink to nothing just because its visible\n * chrome did — `resolveFloatingActionButtonDescriptor` always reports the\n * full label as the accessible name in both layout modes.\n */\nexport type FloatingActionButtonDescriptor<\n IconName extends string = SemanticIconName,\n> = Readonly<{\n icon: FloatingActionButtonIconDescriptor<IconName>;\n label: string;\n layoutMode?: FloatingActionButtonLayoutMode;\n}>;\n\nexport type ResolvedFloatingActionButtonDescriptor<\n IconName extends string = SemanticIconName,\n> = Readonly<{\n icon: ResolvedFloatingActionButtonIconDescriptor<IconName>;\n label: string;\n layoutMode: FloatingActionButtonLayoutMode;\n resolvedAccessibilityLabel: string;\n}>;\n\nexport const floatingActionButtonBehaviorDefaults = {\n layoutMode: \"expanded\",\n} as const satisfies Readonly<{ layoutMode: FloatingActionButtonLayoutMode }>;\n\nfunction validateTrimmedCopy(value: unknown, field: string): asserts value is string {\n if (typeof value !== \"string\" || value.trim().length === 0) {\n throw new TypeError(`${field} must not be empty`);\n }\n if (value !== value.trim()) {\n throw new TypeError(`${field} must not start or end with whitespace`);\n }\n}\n\nfunction isObject(value: unknown): value is Readonly<Record<string, unknown>> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction rejectUnknownKeys(\n value: Readonly<Record<string, unknown>>,\n allowed: ReadonlySet<string>,\n field: string,\n): void {\n for (const key of Object.keys(value)) {\n if (!allowed.has(key)) {\n throw new TypeError(`Unsupported FloatingActionButton ${field} field: ${key}`);\n }\n }\n}\n\nconst descriptorKeys = new Set([\"icon\", \"label\", \"layoutMode\"]);\nconst iconKeys = new Set([\"name\", \"decorative\"]);\nconst layoutModes = new Set<FloatingActionButtonLayoutMode>([\"expanded\", \"collapsed\"]);\nconst scrollSignals = new Set<FloatingActionButtonScrollSignal>([\n \"away-from-start\",\n \"toward-start\",\n \"idle\",\n]);\n\nexport function validateFloatingActionButtonDescriptor<IconName extends string>(\n descriptor: FloatingActionButtonDescriptor<IconName>,\n): void {\n if (!isObject(descriptor)) {\n throw new TypeError(\"FloatingActionButton descriptor must be an object\");\n }\n rejectUnknownKeys(descriptor, descriptorKeys, \"descriptor\");\n validateTrimmedCopy(descriptor.label, \"FloatingActionButton label\");\n if (!isObject(descriptor.icon)) {\n throw new TypeError(\"FloatingActionButton icon must be an object\");\n }\n rejectUnknownKeys(descriptor.icon, iconKeys, \"icon\");\n if (descriptor.icon.decorative !== undefined && descriptor.icon.decorative !== true) {\n throw new TypeError(\"FloatingActionButton icon must be decorative\");\n }\n validateTrimmedCopy(descriptor.icon.name, \"FloatingActionButton icon name\");\n if (\n descriptor.layoutMode !== undefined &&\n !layoutModes.has(descriptor.layoutMode)\n ) {\n throw new TypeError(\n `Unsupported FloatingActionButton layoutMode: ${String(descriptor.layoutMode)}`,\n );\n }\n}\n\n/**\n * The label is the accessible name in every mode — `collapsed` never falls\n * back to the icon name, because an icon-only glyph is not a stable\n * accessible name across products or locales.\n */\nexport function resolveFloatingActionButtonDescriptor<IconName extends string>(\n descriptor: FloatingActionButtonDescriptor<IconName>,\n): ResolvedFloatingActionButtonDescriptor<IconName> {\n validateFloatingActionButtonDescriptor(descriptor);\n return {\n icon: { name: descriptor.icon.name, decorative: true },\n label: descriptor.label,\n layoutMode:\n descriptor.layoutMode ?? floatingActionButtonBehaviorDefaults.layoutMode,\n resolvedAccessibilityLabel: descriptor.label,\n };\n}\n\n/**\n * Collapsing on `away-from-start` and expanding on `toward-start` mirrors the\n * common list-scroll convention (hide the label while the user reads,\n * reintroduce it once they scroll back toward the top). `idle` intentionally\n * keeps whatever mode is already active instead of guessing, so a renderer\n * that reports \"no delta since last frame\" cannot cause the button to flicker.\n */\nexport function resolveFloatingActionButtonLayoutMode(\n signal: FloatingActionButtonScrollSignal,\n previousMode: FloatingActionButtonLayoutMode = floatingActionButtonBehaviorDefaults.layoutMode,\n): FloatingActionButtonLayoutMode {\n if (!scrollSignals.has(signal)) {\n throw new TypeError(`Unsupported FloatingActionButton scroll signal: ${String(signal)}`);\n }\n if (!layoutModes.has(previousMode)) {\n throw new TypeError(\n `Unsupported FloatingActionButton layoutMode: ${String(previousMode)}`,\n );\n }\n if (signal === \"away-from-start\") return \"collapsed\";\n if (signal === \"toward-start\") return \"expanded\";\n return previousMode;\n}\n\n/**\n * Answers \"who reserves the content's bottom padding\" so both platform\n * renderers compute the same number instead of each re-deriving\n * diameter + margin arithmetic independently and drifting apart. The safe-area\n * inset is additive, matching every other bottom-anchored recipe in this\n * package (BottomNavigation, Sheet, Toast) rather than a `max()` clamp.\n */\nexport function resolveFloatingActionButtonContentClearance(\n safeAreaBottomInset: number,\n size: \"medium\" | \"large\" = \"large\",\n): number {\n if (!Number.isFinite(safeAreaBottomInset) || safeAreaBottomInset < 0) {\n throw new RangeError(\n \"FloatingActionButton safeAreaBottomInset must be a non-negative finite number\",\n );\n }\n return (\n floatingActionButtonRecipe.sizes[size].diameter +\n floatingActionButtonRecipe.margin * 2 +\n safeAreaBottomInset\n );\n}\n\n/**\n * Both layout modes reuse the selected Button/IconButton tier so the control\n * never changes height while morphing between a circle and a pill — only\n * shape and content change, per the brief's \"don't fork the Button recipe.\"\n */\nexport const floatingActionButtonRecipe = {\n slots: [\"root\", \"icon\", \"label\"] as const,\n defaults: { layoutMode: \"expanded\" } as const,\n circle: iconButtonRecipe.sizes.large,\n // BurnTok requested a smaller FAB; reuse existing control tiers rather than product overrides.\n sizes: { medium: iconButtonRecipe.sizes.medium, large: iconButtonRecipe.sizes.large },\n tone: iconButtonRecipe.tones.primary,\n shape: iconButtonRecipe.shapes.circle,\n /**\n * Button의 large 티어와 같은 값이다. 그런데 `buttonRecipe`를 import하지는 않는다 —\n * 그것은 `recipes.ts`에 있고 그 파일은 `component-recipes.ts`를 import하는 **하류**라,\n * 여기서 거슬러 올라가면 순환이 된다(ESM은 재수출을 본문보다 먼저 평가해서\n * `buttonRecipe`가 undefined인 채로 이 모듈이 실행된다).\n *\n * 대신 같은 foundations 토큰을 직접 읽고, **두 값이 어긋나지 않는다는 것은\n * `test/floating-action-button.test.ts`의 불변식으로 강제한다.** 이 저장소가 규칙을\n * 다루는 방식대로 — 지켜야 할 것은 import가 아니라 테스트로 묶는다.\n */\n expandedLabel: {\n textVariant: \"bodyLarge\",\n paddingHorizontal: spacing.lg,\n },\n margin: spacing.md,\n shadow: shadow.floating,\n /**\n * `micro` (120ms, instant under Reduce Motion) rather than `enter`/`exit`:\n * this transition can fire on every scroll-direction change, and a state\n * driven by continuous scrolling should use the fastest state-change tier\n * so it cannot visibly queue behind fast scroll gestures.\n */\n transition: motionPreset.micro,\n} as const;\n\n/**\n * Literal scenario names for behaviorRegistry.floatingActionButton (lead\n * wires into src/behaviors.ts). Kept here, not there, so this module stays\n * self-contained per the authoring brief.\n */\nexport const floatingActionButtonBehaviorScenarios = [\n \"collapses-on-scroll-away-from-start-without-losing-the-44-unit-target\",\n \"expands-on-scroll-toward-start\",\n \"idle-scroll-signal-does-not-change-the-current-mode\",\n \"accessible-name-is-the-full-label-in-both-layout-modes\",\n \"content-clearance-accounts-for-the-safe-area-inset-additively\",\n \"root-sits-after-scrollable-content-in-reading-and-tab-order\",\n // The micro recipe already specifies instant reduced motion; the old\n // crossfade scenario contradicted that choice. See docs/floating-action-button.md.\n \"reduced-motion-switches-instantly-without-a-shape-tween\",\n] as const;\n"]}
@@ -105,12 +105,12 @@ export declare const iconButtonRecipe: {
105
105
  readonly selected: {
106
106
  readonly background: Readonly<{
107
107
  source: "theme";
108
- key: "bg";
108
+ key: "surfaceSelected";
109
109
  alpha?: number;
110
110
  }>;
111
111
  readonly content: Readonly<{
112
112
  source: "theme";
113
- key: "contentBrand";
113
+ key: "text";
114
114
  alpha?: number;
115
115
  }>;
116
116
  readonly border: null;
@@ -46,9 +46,9 @@ export const iconButtonRecipe = {
46
46
  // state; without a paired visual every consumer painted the pressed icon
47
47
  // button in product styles. Mirrors `buttonRecipe.states.selected`.
48
48
  selected: {
49
- background: semanticColors.canvas,
50
- content: semanticColors.content.brand,
51
- // Match Button: selection changes color without introducing an outline.
49
+ background: semanticColors.selection.background,
50
+ content: semanticColors.selection.content,
51
+ // Match Button: neutral selection does not introduce an outline.
52
52
  border: null,
53
53
  },
54
54
  pressedOpacity: opacity.pressed,
@@ -1 +1 @@
1
- {"version":3,"file":"icon-button-recipe.js","sourceRoot":"","sources":["../src/icon-button-recipe.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n resolveColorReference,\n type ColorReference,\n type ColorReferencePalette,\n} from \"./color-references.js\";\nimport {\n glyph,\n opacity,\n radius,\n type GlyphSize,\n} from \"./foundations.js\";\nimport { semanticColors } from \"./semantic-colors.js\";\n\nexport type IconButtonSize = \"small\" | \"medium\" | \"large\";\nexport type IconButtonShape = \"rounded\" | \"circle\";\nexport type IconButtonTone = \"primary\" | \"secondary\" | \"ghost\" | \"danger\";\n/**\n * Which container edge a ghost IconButton sits on (logical: `start`/`end` follow the reading direction). The\n * button keeps its full touch target but overhangs that edge so its glyph slot, not the transparent hit area,\n * sets the visible inset (2026-10-17 measured audit: glyphs sat 10–15px inside the row/header inset).\n */\nexport type IconButtonEdge = \"start\" | \"end\" | \"both\";\nexport const iconButtonEdges = [\"start\", \"end\", \"both\"] as const satisfies readonly IconButtonEdge[];\n\nexport const iconButtonRecipe = {\n slots: [\"root\", \"icon\", \"spinner\"] as const,\n defaults: { tone: \"ghost\", size: \"medium\", shape: \"rounded\" },\n tones: {\n primary: {\n background: semanticColors.action.brand.background,\n content: semanticColors.action.brand.content,\n border: null,\n },\n secondary: {\n background: semanticColors.action.neutral.background,\n content: semanticColors.action.neutral.content,\n border: semanticColors.action.neutral.border,\n },\n ghost: {\n background: null,\n content: semanticColors.content.secondary,\n border: null,\n },\n danger: {\n background: semanticColors.action.danger.background,\n content: semanticColors.action.danger.content,\n border: null,\n },\n },\n sizes: {\n small: { diameter: 36, hitSlop: 4, glyph: \"sm\" },\n medium: { diameter: 44, hitSlop: 0, glyph: \"md\" },\n large: { diameter: 52, hitSlop: 0, glyph: \"lg\" },\n },\n shapes: { rounded: \"md\", circle: \"full\" },\n /**\n * Edge alignment (2026-10-17 measured audit): a ghost IconButton at a row/header edge overhangs that edge by\n * (visible diameter − glyph slot) / 2, so the glyph slot lines up with the container inset like text does while\n * the hit area stays whole. Only `ghost` overhangs — filled and outlined tones show their frame, and the frame is\n * what aligns. Renderers read `resolveIconButtonEdgeOverhang` so a raised visible diameter (minimumVisualTarget)\n * moves the glyph slot by the same amount on Web and Native.\n */\n edge: { tones: [\"ghost\"] as const },\n states: {\n // `aria-pressed` / `accessibilityState.selected` already carried the toggle\n // state; without a paired visual every consumer painted the pressed icon\n // button in product styles. Mirrors `buttonRecipe.states.selected`.\n selected: {\n background: semanticColors.canvas,\n content: semanticColors.content.brand,\n // Match Button: selection changes color without introducing an outline.\n border: null,\n },\n pressedOpacity: opacity.pressed,\n disabledOpacity: opacity.disabled,\n },\n} as const satisfies {\n slots: readonly string[];\n defaults: {\n tone: IconButtonTone;\n size: IconButtonSize;\n shape: IconButtonShape;\n };\n tones: Record<\n IconButtonTone,\n { background: ColorReference | null; content: ColorReference; border: ColorReference | null }\n >;\n sizes: Record<IconButtonSize, { diameter: number; hitSlop: number; glyph: GlyphSize }>;\n shapes: Record<IconButtonShape, keyof typeof radius>;\n edge: { tones: readonly IconButtonTone[] };\n states: {\n selected: { background: ColorReference; content: ColorReference; border: ColorReference | null };\n pressedOpacity: number;\n disabledOpacity: number;\n };\n};\n\nexport type ResolvedIconButtonPresentation = Readonly<{\n background: string | null;\n content: string;\n border: string | null;\n}>;\n\n/** Resolve one recipe tone for non-CSS renderers without a second tone table. */\nexport function resolveIconButtonPresentation(\n tone: IconButtonTone,\n palette: ColorReferencePalette,\n selected = false,\n): ResolvedIconButtonPresentation {\n const contract: {\n background: ColorReference | null;\n content: ColorReference;\n border: ColorReference | null;\n } = selected ? iconButtonRecipe.states.selected : iconButtonRecipe.tones[tone];\n return {\n background:\n contract.background === null\n ? null\n : resolveColorReference(contract.background, palette),\n content: resolveColorReference(contract.content, palette),\n border:\n contract.border === null\n ? null\n : resolveColorReference(contract.border, palette),\n };\n}\n\n/**\n * Negative margin a ghost IconButton applies on its `edge` side(s): (visible diameter − glyph slot) / 2, e.g. 10 for\n * medium (44 − 24). 0 for tones that draw a frame. `visibleDiameter` is the rendered size after the Provider's\n * minimumVisualTarget axis (`visibleControlHeight`), so the overhang tracks what is actually drawn.\n */\nexport function resolveIconButtonEdgeOverhang(\n tone: IconButtonTone,\n size: IconButtonSize,\n visibleDiameter: number = iconButtonRecipe.sizes[size].diameter,\n): number {\n if (!(iconButtonRecipe.edge.tones as readonly IconButtonTone[]).includes(tone)) return 0;\n return Math.max(0, (visibleDiameter - glyph[iconButtonRecipe.sizes[size].glyph]) / 2);\n}\n"]}
1
+ {"version":3,"file":"icon-button-recipe.js","sourceRoot":"","sources":["../src/icon-button-recipe.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n resolveColorReference,\n type ColorReference,\n type ColorReferencePalette,\n} from \"./color-references.js\";\nimport {\n glyph,\n opacity,\n radius,\n type GlyphSize,\n} from \"./foundations.js\";\nimport { semanticColors } from \"./semantic-colors.js\";\n\nexport type IconButtonSize = \"small\" | \"medium\" | \"large\";\nexport type IconButtonShape = \"rounded\" | \"circle\";\nexport type IconButtonTone = \"primary\" | \"secondary\" | \"ghost\" | \"danger\";\n/**\n * Which container edge a ghost IconButton sits on (logical: `start`/`end` follow the reading direction). The\n * button keeps its full touch target but overhangs that edge so its glyph slot, not the transparent hit area,\n * sets the visible inset (2026-10-17 measured audit: glyphs sat 10–15px inside the row/header inset).\n */\nexport type IconButtonEdge = \"start\" | \"end\" | \"both\";\nexport const iconButtonEdges = [\"start\", \"end\", \"both\"] as const satisfies readonly IconButtonEdge[];\n\nexport const iconButtonRecipe = {\n slots: [\"root\", \"icon\", \"spinner\"] as const,\n defaults: { tone: \"ghost\", size: \"medium\", shape: \"rounded\" },\n tones: {\n primary: {\n background: semanticColors.action.brand.background,\n content: semanticColors.action.brand.content,\n border: null,\n },\n secondary: {\n background: semanticColors.action.neutral.background,\n content: semanticColors.action.neutral.content,\n border: semanticColors.action.neutral.border,\n },\n ghost: {\n background: null,\n content: semanticColors.content.secondary,\n border: null,\n },\n danger: {\n background: semanticColors.action.danger.background,\n content: semanticColors.action.danger.content,\n border: null,\n },\n },\n sizes: {\n small: { diameter: 36, hitSlop: 4, glyph: \"sm\" },\n medium: { diameter: 44, hitSlop: 0, glyph: \"md\" },\n large: { diameter: 52, hitSlop: 0, glyph: \"lg\" },\n },\n shapes: { rounded: \"md\", circle: \"full\" },\n /**\n * Edge alignment (2026-10-17 measured audit): a ghost IconButton at a row/header edge overhangs that edge by\n * (visible diameter − glyph slot) / 2, so the glyph slot lines up with the container inset like text does while\n * the hit area stays whole. Only `ghost` overhangs — filled and outlined tones show their frame, and the frame is\n * what aligns. Renderers read `resolveIconButtonEdgeOverhang` so a raised visible diameter (minimumVisualTarget)\n * moves the glyph slot by the same amount on Web and Native.\n */\n edge: { tones: [\"ghost\"] as const },\n states: {\n // `aria-pressed` / `accessibilityState.selected` already carried the toggle\n // state; without a paired visual every consumer painted the pressed icon\n // button in product styles. Mirrors `buttonRecipe.states.selected`.\n selected: {\n background: semanticColors.selection.background,\n content: semanticColors.selection.content,\n // Match Button: neutral selection does not introduce an outline.\n border: null,\n },\n pressedOpacity: opacity.pressed,\n disabledOpacity: opacity.disabled,\n },\n} as const satisfies {\n slots: readonly string[];\n defaults: {\n tone: IconButtonTone;\n size: IconButtonSize;\n shape: IconButtonShape;\n };\n tones: Record<\n IconButtonTone,\n { background: ColorReference | null; content: ColorReference; border: ColorReference | null }\n >;\n sizes: Record<IconButtonSize, { diameter: number; hitSlop: number; glyph: GlyphSize }>;\n shapes: Record<IconButtonShape, keyof typeof radius>;\n edge: { tones: readonly IconButtonTone[] };\n states: {\n selected: { background: ColorReference; content: ColorReference; border: ColorReference | null };\n pressedOpacity: number;\n disabledOpacity: number;\n };\n};\n\nexport type ResolvedIconButtonPresentation = Readonly<{\n background: string | null;\n content: string;\n border: string | null;\n}>;\n\n/** Resolve one recipe tone for non-CSS renderers without a second tone table. */\nexport function resolveIconButtonPresentation(\n tone: IconButtonTone,\n palette: ColorReferencePalette,\n selected = false,\n): ResolvedIconButtonPresentation {\n const contract: {\n background: ColorReference | null;\n content: ColorReference;\n border: ColorReference | null;\n } = selected ? iconButtonRecipe.states.selected : iconButtonRecipe.tones[tone];\n return {\n background:\n contract.background === null\n ? null\n : resolveColorReference(contract.background, palette),\n content: resolveColorReference(contract.content, palette),\n border:\n contract.border === null\n ? null\n : resolveColorReference(contract.border, palette),\n };\n}\n\n/**\n * Negative margin a ghost IconButton applies on its `edge` side(s): (visible diameter − glyph slot) / 2, e.g. 10 for\n * medium (44 − 24). 0 for tones that draw a frame. `visibleDiameter` is the rendered size after the Provider's\n * minimumVisualTarget axis (`visibleControlHeight`), so the overhang tracks what is actually drawn.\n */\nexport function resolveIconButtonEdgeOverhang(\n tone: IconButtonTone,\n size: IconButtonSize,\n visibleDiameter: number = iconButtonRecipe.sizes[size].diameter,\n): number {\n if (!(iconButtonRecipe.edge.tones as readonly IconButtonTone[]).includes(tone)) return 0;\n return Math.max(0, (visibleDiameter - glyph[iconButtonRecipe.sizes[size].glyph]) / 2);\n}\n"]}
package/dist/menubar.d.ts CHANGED
@@ -81,12 +81,12 @@ export declare const menubarRecipe: {
81
81
  };
82
82
  readonly selectedBackground: Readonly<{
83
83
  source: "theme";
84
- key: "bg";
84
+ key: "surfaceSelected";
85
85
  alpha?: number;
86
86
  }>;
87
87
  readonly selectedIndicator: Readonly<{
88
88
  source: "theme";
89
- key: "contentBrand";
89
+ key: "text";
90
90
  alpha?: number;
91
91
  }>;
92
92
  readonly danger: Readonly<{
@@ -1 +1 @@
1
- {"version":3,"file":"palette-contrast.d.ts","sourceRoot":"","sources":["../src/palette-contrast.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,UAAU,EAAE,KAAK,aAAa,EAAE,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAExG,mEAAmE;AACnE,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CAAC;IACzC,UAAU,EAAE,MAAM,WAAW,CAAC;IAC9B,UAAU,EAAE,MAAM,WAAW,GAAG,cAAc,UAAU,EAAE,CAAC;IAC3D,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,GAAG,UAAU,CAAC;CAC3B,CAAC,CAAC;AAEH,MAAM,MAAM,sBAAsB,GAAG,mBAAmB,GAAG,QAAQ,CAAC;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAKvF,eAAO,MAAM,oBAAoB,EAAE,SAAS,mBAAmB,EAe9D,CAAC;AAaF,gEAAgE;AAChE,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAI5E;AAED,iGAAiG;AACjG,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,QAAQ,CAAC,WAAW,CAAC,EAAE,GAAG,EAAE,mBAAmB,CAAC,YAAY,CAAC,GAAG,MAAM,CAExH;AAED,gGAAgG;AAChG,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,QAAQ,CAAC,WAAW,CAAC,GAC7B,SAAS,sBAAsB,EAAE,CAOnC;AAED,wGAAwG;AACxG,wBAAgB,yBAAyB,CACvC,YAAY,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE,QAAQ,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,GACrF,QAAQ,CAAC,MAAM,CAAC,aAAa,EAAE,SAAS,sBAAsB,EAAE,CAAC,CAAC,CAKpE;AAED;;;;oFAIoF;AACpF,wBAAgB,yBAAyB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,CAAC,WAAW,CAAC,GAAG,MAAM,CASnG"}
1
+ {"version":3,"file":"palette-contrast.d.ts","sourceRoot":"","sources":["../src/palette-contrast.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,UAAU,EAAE,KAAK,aAAa,EAAE,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAExG,mEAAmE;AACnE,MAAM,MAAM,mBAAmB,GAAG,QAAQ,CAAC;IACzC,UAAU,EAAE,MAAM,WAAW,CAAC;IAC9B,UAAU,EAAE,MAAM,WAAW,GAAG,cAAc,UAAU,EAAE,CAAC;IAC3D,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,GAAG,UAAU,CAAC;CAC3B,CAAC,CAAC;AAEH,MAAM,MAAM,sBAAsB,GAAG,mBAAmB,GAAG,QAAQ,CAAC;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAKvF,eAAO,MAAM,oBAAoB,EAAE,SAAS,mBAAmB,EAiB9D,CAAC;AAaF,gEAAgE;AAChE,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAI5E;AAED,iGAAiG;AACjG,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,QAAQ,CAAC,WAAW,CAAC,EAAE,GAAG,EAAE,mBAAmB,CAAC,YAAY,CAAC,GAAG,MAAM,CAExH;AAED,gGAAgG;AAChG,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,QAAQ,CAAC,WAAW,CAAC,GAC7B,SAAS,sBAAsB,EAAE,CAOnC;AAED,wGAAwG;AACxG,wBAAgB,yBAAyB,CACvC,YAAY,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE,QAAQ,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,GACrF,QAAQ,CAAC,MAAM,CAAC,aAAa,EAAE,SAAS,sBAAsB,EAAE,CAAC,CAAC,CAKpE;AAED;;;;oFAIoF;AACpF,wBAAgB,yBAAyB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,CAAC,WAAW,CAAC,GAAG,MAAM,CASnG"}
@@ -3,6 +3,8 @@ const readable = ["text", "textBody", "textMuted", "textSub", "contentBrand", "d
3
3
  const surfaces = ["bg", "surface"];
4
4
  export const paletteContrastRules = [
5
5
  ...readable.flatMap((foreground) => surfaces.map((background) => ({ foreground, background, minimum: 4.5, kind: "text" }))),
6
+ // Selected text must remain readable when a product overrides the shared neutral fill.
7
+ ...readable.map(foreground => ({ foreground, background: "surfaceSelected", minimum: 4.5, kind: "text" })),
6
8
  { foreground: "onPrimary", background: "primary", minimum: 4.5, kind: "text" },
7
9
  { foreground: "onDanger", background: "dangerFill", minimum: 4.5, kind: "text" },
8
10
  // Filled badges use a separate ink/fill pair, independent of primary buttons.
@@ -1 +1 @@
1
- {"version":3,"file":"palette-contrast.js","sourceRoot":"","sources":["../src/palette-contrast.ts"],"names":[],"mappings":"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","sourcesContent":["import { THEMES, accentFill, type AccentTone, type ResolvedTheme, type ThemeColors } from \"./colors.js\";\n\n/** Pair thresholds and their rationale: docs/brand-boundary.md. */\nexport type PaletteContrastRule = Readonly<{\n foreground: keyof ThemeColors;\n background: keyof ThemeColors | `accentFill.${AccentTone}`;\n minimum: number;\n kind: \"text\" | \"non-text\";\n}>;\n\nexport type PaletteContrastFinding = PaletteContrastRule & Readonly<{ ratio: number }>;\n\nconst readable = [\"text\", \"textBody\", \"textMuted\", \"textSub\", \"contentBrand\", \"danger\"] as const;\nconst surfaces = [\"bg\", \"surface\"] as const;\n\nexport const paletteContrastRules: readonly PaletteContrastRule[] = [\n ...readable.flatMap((foreground) =>\n surfaces.map((background) => ({ foreground, background, minimum: 4.5, kind: \"text\" as const })),\n ),\n { foreground: \"onPrimary\", background: \"primary\", minimum: 4.5, kind: \"text\" },\n { foreground: \"onDanger\", background: \"dangerFill\", minimum: 4.5, kind: \"text\" },\n // Filled badges use a separate ink/fill pair, independent of primary buttons.\n ...([\"neutralFill\", \"strongFill\"] as const).map(background => ({ foreground: \"onStatusFill\" as const, background, minimum: 4.5, kind: \"text\" as const })),\n ...(Object.keys(accentFill) as AccentTone[]).map(tone => ({ foreground: \"onStatusFill\" as const, background: `accentFill.${tone}` as const, minimum: 4.5, kind: \"text\" as const })),\n ...([\"primary\", \"borderControl\"] as const).flatMap((foreground) =>\n surfaces.map((background) => ({ foreground, background, minimum: 3, kind: \"non-text\" as const })),\n ),\n // textWeak is the disabled/decorative/placeholder tier, exempt from text AA;\n // it also paints border.strong on canvas, which must stay a visible boundary.\n { foreground: \"textWeak\", background: \"bg\", minimum: 3, kind: \"non-text\" },\n];\n\nconst sixDigitHex = /^#[0-9a-f]{6}$/i;\n\nfunction luminance(hex: string): number {\n if (!sixDigitHex.test(hex)) throw new TypeError(`Expected a six-digit hex color, got ${hex}`);\n const [r, g, b] = [1, 3, 5].map((offset) => {\n const channel = Number.parseInt(hex.slice(offset, offset + 2), 16) / 255;\n return channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4;\n }) as [number, number, number];\n return 0.2126 * r + 0.7152 * g + 0.0722 * b;\n}\n\n/** WCAG 2.x contrast ratio between two six-digit hex colors. */\nexport function contrastRatio(foreground: string, background: string): number {\n const a = luminance(foreground);\n const b = luminance(background);\n return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);\n}\n\n/** Fixed status fills remain outside the product palette, but participate in the same report. */\nexport function resolveContrastBackground(palette: Readonly<ThemeColors>, key: PaletteContrastRule[\"background\"]): string {\n return key.startsWith(\"accentFill.\") ? accentFill[key.slice(\"accentFill.\".length) as AccentTone] : palette[key as keyof ThemeColors];\n}\n\n/** Pairs of a resolved theme palette that fall below their threshold; empty means it passes. */\nexport function checkPaletteContrast(\n palette: Readonly<ThemeColors>,\n): readonly PaletteContrastFinding[] {\n const findings: PaletteContrastFinding[] = [];\n for (const rule of paletteContrastRules) {\n const ratio = contrastRatio(palette[rule.foreground], resolveContrastBackground(palette, rule.background));\n if (ratio < rule.minimum) findings.push({ ...rule, ratio: Math.round(ratio * 100) / 100 });\n }\n return findings;\n}\n\n/** Checks a `brandPalette` the way the Provider applies it: each theme merged over the HJM defaults. */\nexport function checkBrandPaletteContrast(\n brandPalette: Readonly<Partial<Record<ResolvedTheme, Readonly<Partial<ThemeColors>>>>>,\n): Readonly<Record<ResolvedTheme, readonly PaletteContrastFinding[]>> {\n return {\n light: checkPaletteContrast({ ...THEMES.light, ...brandPalette.light }),\n dark: checkPaletteContrast({ ...THEMES.dark, ...brandPalette.dark }),\n };\n}\n\n/** Shared whole-surface fill adaptation. This palette-only entry keeps ordinary\n * renderer imports independent of the optional preset registry/effect descriptors.\n * The supported 0.85..1 interval keeps readable foregrounds outside the backdrop\n * luminance interval; testing its black/white endpoints bounds any opaque backdrop.\n * See docs/design-profile.md for why a reference card's low alpha is not copied. */\nexport function resolveSurfaceFillOpacity(requested: number, palette: Readonly<ThemeColors>): number {\n if (!Number.isFinite(requested) || requested < 0.85 || requested > 1) throw new RangeError(\"Surface fill opacity must be between 0.85 and 1\");\n const channels = [1, 3, 5].map(offset => Number.parseInt(palette.bg.slice(offset, offset + 2), 16));\n const rules = paletteContrastRules.filter(rule => rule.background === \"bg\");\n for (let opacity = requested; opacity < 1; opacity = Math.min(1, opacity + 0.01)) {\n const endpoints = [0, 255].map(backdrop => \"#\" + channels.map(channel => Math.round(channel * opacity + backdrop * (1 - opacity)).toString(16).padStart(2, \"0\")).join(\"\"));\n if (endpoints.every(color => rules.every(rule => contrastRatio(palette[rule.foreground], color) >= rule.minimum))) return opacity;\n }\n return 1;\n}\n"]}
1
+ {"version":3,"file":"palette-contrast.js","sourceRoot":"","sources":["../src/palette-contrast.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,UAAU,EAAyD,MAAM,aAAa,CAAC;AAYxG,MAAM,QAAQ,GAAG,CAAC,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,SAAS,EAAE,cAAc,EAAE,QAAQ,CAAU,CAAC;AACjG,MAAM,QAAQ,GAAG,CAAC,IAAI,EAAE,SAAS,CAAU,CAAC;AAE5C,MAAM,CAAC,MAAM,oBAAoB,GAAmC;IAClE,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,UAAU,EAAE,EAAE,CACjC,QAAQ,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAe,EAAE,CAAC,CAAC,CAChG;IACD,uFAAuF;IACvF,GAAG,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,UAAU,EAAE,iBAA0B,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAe,EAAE,CAAC,CAAC;IAC5H,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE;IAC9E,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE;IAChF,8EAA8E;IAC9E,GAAI,CAAC,aAAa,EAAE,YAAY,CAAW,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,cAAuB,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAe,EAAE,CAAC,CAAC;IACzJ,GAAI,MAAM,CAAC,IAAI,CAAC,UAAU,CAAkB,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,cAAuB,EAAE,UAAU,EAAE,cAAc,IAAI,EAAW,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAe,EAAE,CAAC,CAAC;IACnL,GAAI,CAAC,SAAS,EAAE,eAAe,CAAW,CAAC,OAAO,CAAC,CAAC,UAAU,EAAE,EAAE,CAChE,QAAQ,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,UAAmB,EAAE,CAAC,CAAC,CAClG;IACD,6EAA6E;IAC7E,8EAA8E;IAC9E,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE;CAC3E,CAAC;AAEF,MAAM,WAAW,GAAG,iBAAiB,CAAC;AAEtC,SAAS,SAAS,CAAC,GAAW;IAC5B,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,MAAM,IAAI,SAAS,CAAC,uCAAuC,GAAG,EAAE,CAAC,CAAC;IAC9F,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE;QACzC,MAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC;QACzE,OAAO,OAAO,IAAI,OAAO,CAAC,CAAC,CAAC,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,GAAG,KAAK,CAAC,GAAG,KAAK,CAAC,IAAI,GAAG,CAAC;IACnF,CAAC,CAA6B,CAAC;IAC/B,OAAO,MAAM,GAAG,CAAC,GAAG,MAAM,GAAG,CAAC,GAAG,MAAM,GAAG,CAAC,CAAC;AAC9C,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,aAAa,CAAC,UAAkB,EAAE,UAAkB;IAClE,MAAM,CAAC,GAAG,SAAS,CAAC,UAAU,CAAC,CAAC;IAChC,MAAM,CAAC,GAAG,SAAS,CAAC,UAAU,CAAC,CAAC;IAChC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;AAC3D,CAAC;AAED,iGAAiG;AACjG,MAAM,UAAU,yBAAyB,CAAC,OAA8B,EAAE,GAAsC;IAC9G,OAAO,GAAG,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,aAAa,CAAC,MAAM,CAAe,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAwB,CAAC,CAAC;AACvI,CAAC;AAED,gGAAgG;AAChG,MAAM,UAAU,oBAAoB,CAClC,OAA8B;IAE9B,MAAM,QAAQ,GAA6B,EAAE,CAAC;IAC9C,KAAK,MAAM,IAAI,IAAI,oBAAoB,EAAE,CAAC;QACxC,MAAM,KAAK,GAAG,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,yBAAyB,CAAC,OAAO,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;QAC3G,IAAI,KAAK,GAAG,IAAI,CAAC,OAAO;YAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,GAAG,CAAC,GAAG,GAAG,EAAE,CAAC,CAAC;IAC7F,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,wGAAwG;AACxG,MAAM,UAAU,yBAAyB,CACvC,YAAsF;IAEtF,OAAO;QACL,KAAK,EAAE,oBAAoB,CAAC,EAAE,GAAG,MAAM,CAAC,KAAK,EAAE,GAAG,YAAY,CAAC,KAAK,EAAE,CAAC;QACvE,IAAI,EAAE,oBAAoB,CAAC,EAAE,GAAG,MAAM,CAAC,IAAI,EAAE,GAAG,YAAY,CAAC,IAAI,EAAE,CAAC;KACrE,CAAC;AACJ,CAAC;AAED;;;;oFAIoF;AACpF,MAAM,UAAU,yBAAyB,CAAC,SAAiB,EAAE,OAA8B;IACzF,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,SAAS,GAAG,IAAI,IAAI,SAAS,GAAG,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,iDAAiD,CAAC,CAAC;IAC9I,MAAM,QAAQ,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IACpG,MAAM,KAAK,GAAG,oBAAoB,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,UAAU,KAAK,IAAI,CAAC,CAAC;IAC5E,KAAK,IAAI,OAAO,GAAG,SAAS,EAAE,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC,EAAE,CAAC;QACjF,MAAM,SAAS,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;QAC3K,IAAI,SAAS,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,CAAC;YAAE,OAAO,OAAO,CAAC;IACpI,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC","sourcesContent":["import { THEMES, accentFill, type AccentTone, type ResolvedTheme, type ThemeColors } from \"./colors.js\";\n\n/** Pair thresholds and their rationale: docs/brand-boundary.md. */\nexport type PaletteContrastRule = Readonly<{\n foreground: keyof ThemeColors;\n background: keyof ThemeColors | `accentFill.${AccentTone}`;\n minimum: number;\n kind: \"text\" | \"non-text\";\n}>;\n\nexport type PaletteContrastFinding = PaletteContrastRule & Readonly<{ ratio: number }>;\n\nconst readable = [\"text\", \"textBody\", \"textMuted\", \"textSub\", \"contentBrand\", \"danger\"] as const;\nconst surfaces = [\"bg\", \"surface\"] as const;\n\nexport const paletteContrastRules: readonly PaletteContrastRule[] = [\n ...readable.flatMap((foreground) =>\n surfaces.map((background) => ({ foreground, background, minimum: 4.5, kind: \"text\" as const })),\n ),\n // Selected text must remain readable when a product overrides the shared neutral fill.\n ...readable.map(foreground => ({ foreground, background: \"surfaceSelected\" as const, minimum: 4.5, kind: \"text\" as const })),\n { foreground: \"onPrimary\", background: \"primary\", minimum: 4.5, kind: \"text\" },\n { foreground: \"onDanger\", background: \"dangerFill\", minimum: 4.5, kind: \"text\" },\n // Filled badges use a separate ink/fill pair, independent of primary buttons.\n ...([\"neutralFill\", \"strongFill\"] as const).map(background => ({ foreground: \"onStatusFill\" as const, background, minimum: 4.5, kind: \"text\" as const })),\n ...(Object.keys(accentFill) as AccentTone[]).map(tone => ({ foreground: \"onStatusFill\" as const, background: `accentFill.${tone}` as const, minimum: 4.5, kind: \"text\" as const })),\n ...([\"primary\", \"borderControl\"] as const).flatMap((foreground) =>\n surfaces.map((background) => ({ foreground, background, minimum: 3, kind: \"non-text\" as const })),\n ),\n // textWeak is the disabled/decorative/placeholder tier, exempt from text AA;\n // it also paints border.strong on canvas, which must stay a visible boundary.\n { foreground: \"textWeak\", background: \"bg\", minimum: 3, kind: \"non-text\" },\n];\n\nconst sixDigitHex = /^#[0-9a-f]{6}$/i;\n\nfunction luminance(hex: string): number {\n if (!sixDigitHex.test(hex)) throw new TypeError(`Expected a six-digit hex color, got ${hex}`);\n const [r, g, b] = [1, 3, 5].map((offset) => {\n const channel = Number.parseInt(hex.slice(offset, offset + 2), 16) / 255;\n return channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4;\n }) as [number, number, number];\n return 0.2126 * r + 0.7152 * g + 0.0722 * b;\n}\n\n/** WCAG 2.x contrast ratio between two six-digit hex colors. */\nexport function contrastRatio(foreground: string, background: string): number {\n const a = luminance(foreground);\n const b = luminance(background);\n return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);\n}\n\n/** Fixed status fills remain outside the product palette, but participate in the same report. */\nexport function resolveContrastBackground(palette: Readonly<ThemeColors>, key: PaletteContrastRule[\"background\"]): string {\n return key.startsWith(\"accentFill.\") ? accentFill[key.slice(\"accentFill.\".length) as AccentTone] : palette[key as keyof ThemeColors];\n}\n\n/** Pairs of a resolved theme palette that fall below their threshold; empty means it passes. */\nexport function checkPaletteContrast(\n palette: Readonly<ThemeColors>,\n): readonly PaletteContrastFinding[] {\n const findings: PaletteContrastFinding[] = [];\n for (const rule of paletteContrastRules) {\n const ratio = contrastRatio(palette[rule.foreground], resolveContrastBackground(palette, rule.background));\n if (ratio < rule.minimum) findings.push({ ...rule, ratio: Math.round(ratio * 100) / 100 });\n }\n return findings;\n}\n\n/** Checks a `brandPalette` the way the Provider applies it: each theme merged over the HJM defaults. */\nexport function checkBrandPaletteContrast(\n brandPalette: Readonly<Partial<Record<ResolvedTheme, Readonly<Partial<ThemeColors>>>>>,\n): Readonly<Record<ResolvedTheme, readonly PaletteContrastFinding[]>> {\n return {\n light: checkPaletteContrast({ ...THEMES.light, ...brandPalette.light }),\n dark: checkPaletteContrast({ ...THEMES.dark, ...brandPalette.dark }),\n };\n}\n\n/** Shared whole-surface fill adaptation. This palette-only entry keeps ordinary\n * renderer imports independent of the optional preset registry/effect descriptors.\n * The supported 0.85..1 interval keeps readable foregrounds outside the backdrop\n * luminance interval; testing its black/white endpoints bounds any opaque backdrop.\n * See docs/design-profile.md for why a reference card's low alpha is not copied. */\nexport function resolveSurfaceFillOpacity(requested: number, palette: Readonly<ThemeColors>): number {\n if (!Number.isFinite(requested) || requested < 0.85 || requested > 1) throw new RangeError(\"Surface fill opacity must be between 0.85 and 1\");\n const channels = [1, 3, 5].map(offset => Number.parseInt(palette.bg.slice(offset, offset + 2), 16));\n const rules = paletteContrastRules.filter(rule => rule.background === \"bg\");\n for (let opacity = requested; opacity < 1; opacity = Math.min(1, opacity + 0.01)) {\n const endpoints = [0, 255].map(backdrop => \"#\" + channels.map(channel => Math.round(channel * opacity + backdrop * (1 - opacity)).toString(16).padStart(2, \"0\")).join(\"\"));\n if (endpoints.every(color => rules.every(rule => contrastRatio(palette[rule.foreground], color) >= rule.minimum))) return opacity;\n }\n return 1;\n}\n"]}
@@ -50,6 +50,23 @@ export declare const semanticColors: {
50
50
  alpha?: number;
51
51
  }>;
52
52
  };
53
+ readonly selection: {
54
+ readonly background: Readonly<{
55
+ source: "theme";
56
+ key: "surfaceSelected";
57
+ alpha?: number;
58
+ }>;
59
+ readonly content: Readonly<{
60
+ source: "theme";
61
+ key: "text";
62
+ alpha?: number;
63
+ }>;
64
+ readonly border: Readonly<{
65
+ source: "theme";
66
+ key: "borderControl";
67
+ alpha?: number;
68
+ }>;
69
+ };
53
70
  readonly content: {
54
71
  readonly primary: Readonly<{
55
72
  source: "theme";
@@ -303,7 +320,7 @@ export declare const semanticColors: {
303
320
  }>;
304
321
  readonly selected: Readonly<{
305
322
  source: "theme";
306
- key: "primary";
323
+ key: "surfaceSelected";
307
324
  alpha?: number;
308
325
  }>;
309
326
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"semantic-colors.d.ts","sourceRoot":"","sources":["../src/semantic-colors.ts"],"names":[],"mappings":"AAIA;;;GAGG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAUvB;;;;WAIG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAkBH,iDAAiD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QA4DjD;;;WAGG;;;;;;;CAGG,CAAC"}
1
+ {"version":3,"file":"semantic-colors.d.ts","sourceRoot":"","sources":["../src/semantic-colors.ts"],"names":[],"mappings":"AAIA;;;GAGG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAUvB;;;;WAIG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAwBH,iDAAiD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QA4DjD;;;WAGG;;;;;;;CAGG,CAAC"}
@@ -22,6 +22,12 @@ export const semanticColors = {
22
22
  */
23
23
  placeholder: themeColor("text", 0.12),
24
24
  },
25
+ // 2026-10-23: a neutral selection must not recolor brand surfaces or primary actions.
26
+ selection: {
27
+ background: themeColor("surfaceSelected"),
28
+ content: themeColor("text"),
29
+ border: themeColor("borderControl"),
30
+ },
25
31
  content: {
26
32
  primary: themeColor("text"),
27
33
  body: themeColor("textBody"),
@@ -96,7 +102,7 @@ export const semanticColors = {
96
102
  hover: themeColor("text", stateLayer.hover),
97
103
  focus: themeColor("contentBrand", stateLayer.focus),
98
104
  pressed: themeColor("text", stateLayer.pressed),
99
- selected: themeColor("primary", stateLayer.selected),
105
+ selected: themeColor("surfaceSelected"),
100
106
  /**
101
107
  * Lower edge of a raised object, composited over its own fill so one shade
102
108
  * darkens any brand color (BottomNavigation raised tile, Spint 2026-10-13).
@@ -1 +1 @@
1
- {"version":3,"file":"semantic-colors.js","sourceRoot":"","sources":["../src/semantic-colors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAEhE;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,MAAM,EAAE,UAAU,CAAC,IAAI,CAAC;IACxB,OAAO,EAAE;QACP,OAAO,EAAE,UAAU,CAAC,SAAS,CAAC;QAC9B,MAAM,EAAE,UAAU,CAAC,YAAY,CAAC;QAChC,MAAM,EAAE,UAAU,CAAC,IAAI,CAAC;QACxB,KAAK,EAAE,UAAU,CAAC,eAAe,CAAC;QAClC,6EAA6E;QAC7E,aAAa,EAAE,UAAU,CAAC,aAAa,CAAC;QACxC,YAAY,EAAE,UAAU,CAAC,YAAY,CAAC;QACtC;;;;WAIG;QACH,WAAW,EAAE,UAAU,CAAC,MAAM,EAAE,IAAI,CAAC;KACtC;IACD,OAAO,EAAE;QACP,OAAO,EAAE,UAAU,CAAC,MAAM,CAAC;QAC3B,IAAI,EAAE,UAAU,CAAC,UAAU,CAAC;QAC5B,SAAS,EAAE,UAAU,CAAC,WAAW,CAAC;QAClC,QAAQ,EAAE,UAAU,CAAC,SAAS,CAAC;QAC/B,UAAU,EAAE,UAAU,CAAC,UAAU,CAAC;QAClC,KAAK,EAAE,UAAU,CAAC,cAAc,CAAC;QACjC,MAAM,EAAE,UAAU,CAAC,QAAQ,CAAC;QAC5B,OAAO,EAAE,UAAU,CAAC,WAAW,CAAC;QAChC,4EAA4E;QAC5E,aAAa,EAAE,UAAU,CAAC,cAAc,CAAC;KAC1C;IACD,MAAM,EAAE;QACN,MAAM,EAAE,UAAU,CAAC,QAAQ,EAAE,GAAG,CAAC;QACjC,OAAO,EAAE,UAAU,CAAC,QAAQ,CAAC;QAC7B,iDAAiD;QACjD,OAAO,EAAE,UAAU,CAAC,eAAe,CAAC;QACpC,MAAM,EAAE,UAAU,CAAC,UAAU,CAAC;QAC9B,KAAK,EAAE,UAAU,CAAC,cAAc,CAAC;QACjC,MAAM,EAAE,UAAU,CAAC,QAAQ,CAAC;KAC7B;IACD,MAAM,EAAE;QACN,KAAK,EAAE;YACL,UAAU,EAAE,UAAU,CAAC,SAAS,CAAC;YACjC,OAAO,EAAE,UAAU,CAAC,WAAW,CAAC;SACjC;QACD,OAAO,EAAE;YACP,UAAU,EAAE,UAAU,CAAC,IAAI,CAAC;YAC5B,OAAO,EAAE,UAAU,CAAC,MAAM,CAAC;YAC3B,uEAAuE;YACvE,2DAA2D;YAC3D,MAAM,EAAE,UAAU,CAAC,eAAe,CAAC;SACpC;QACD,MAAM,EAAE;YACN,UAAU,EAAE,UAAU,CAAC,YAAY,CAAC;YACpC,OAAO,EAAE,UAAU,CAAC,UAAU,CAAC;SAChC;KACF;IACD,QAAQ,EAAE;QACR,IAAI,EAAE;YACJ,UAAU,EAAE,WAAW,CAAC,MAAM,CAAC;YAC/B,UAAU,EAAE,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,IAAI,CAAC;YAChD,eAAe,EAAE,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,IAAI,CAAC;YACrD,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,MAAM,CAAC;SAC/C;QACD,OAAO,EAAE;YACP,UAAU,EAAE,WAAW,CAAC,SAAS,CAAC;YAClC,UAAU,EAAE,WAAW,CAAC,SAAS,EAAE,UAAU,CAAC,IAAI,CAAC;YACnD,eAAe,EAAE,WAAW,CAAC,SAAS,EAAE,UAAU,CAAC,IAAI,CAAC;YACxD,MAAM,EAAE,WAAW,CAAC,SAAS,EAAE,UAAU,CAAC,MAAM,CAAC;SAClD;QACD,OAAO,EAAE;YACP,UAAU,EAAE,WAAW,CAAC,SAAS,CAAC;YAClC,UAAU,EAAE,WAAW,CAAC,SAAS,EAAE,UAAU,CAAC,IAAI,CAAC;YACnD,eAAe,EAAE,WAAW,CAAC,SAAS,EAAE,UAAU,CAAC,IAAI,CAAC;YACxD,MAAM,EAAE,WAAW,CAAC,SAAS,EAAE,UAAU,CAAC,MAAM,CAAC;SAClD;QACD,SAAS,EAAE;YACT,UAAU,EAAE,WAAW,CAAC,WAAW,CAAC;YACpC,UAAU,EAAE,WAAW,CAAC,WAAW,EAAE,UAAU,CAAC,IAAI,CAAC;YACrD,eAAe,EAAE,WAAW,CAAC,WAAW,EAAE,UAAU,CAAC,IAAI,CAAC;YAC1D,MAAM,EAAE,WAAW,CAAC,WAAW,EAAE,UAAU,CAAC,MAAM,CAAC;SACpD;QACD,MAAM,EAAE;YACN,UAAU,EAAE,UAAU,CAAC,QAAQ,CAAC;YAChC,UAAU,EAAE,UAAU,CAAC,QAAQ,EAAE,UAAU,CAAC,IAAI,CAAC;YACjD,eAAe,EAAE,UAAU,CAAC,QAAQ,EAAE,UAAU,CAAC,IAAI,CAAC;YACtD,MAAM,EAAE,UAAU,CAAC,QAAQ,EAAE,UAAU,CAAC,MAAM,CAAC;SAChD;KACF;IACD,WAAW,EAAE;QACX,KAAK,EAAE,UAAU,CAAC,MAAM,EAAE,UAAU,CAAC,KAAK,CAAC;QAC3C,KAAK,EAAE,UAAU,CAAC,cAAc,EAAE,UAAU,CAAC,KAAK,CAAC;QACnD,OAAO,EAAE,UAAU,CAAC,MAAM,EAAE,UAAU,CAAC,OAAO,CAAC;QAC/C,QAAQ,EAAE,UAAU,CAAC,SAAS,EAAE,UAAU,CAAC,QAAQ,CAAC;QACpD;;;WAGG;QACH,KAAK,EAAE,UAAU,CAAC,MAAM,EAAE,IAAI,CAAC;KAChC;CACO,CAAC","sourcesContent":["import { stateLayer } from \"./foundations.js\";\nimport { accentTint } from \"./colors.js\";\nimport { accentColor, themeColor } from \"./color-references.js\";\n\n/**\n * Stable role aliases above the legacy ThemeColors key set. New recipes should\n * prefer these roles so the underlying theme shape can evolve independently.\n */\nexport const semanticColors = {\n canvas: themeColor(\"bg\"),\n surface: {\n default: themeColor(\"surface\"),\n sunken: themeColor(\"surfaceAlt\"),\n raised: themeColor(\"bg\"),\n brand: themeColor(\"surfaceAccent\"),\n // Opaque neutral status plates are surfaces, independent of notice severity.\n statusNeutral: themeColor(\"neutralFill\"),\n statusStrong: themeColor(\"strongFill\"),\n /**\n * Loading placeholder fill (Skeleton, ListRow `loading`). A translucent text tint instead of an opaque\n * neutral: it darkens/lightens whatever it sits on, so it stays visible on canvas, Card surfaces and\n * profile palettes whose `surfaceAlt` is as light as `bg` (2026-10-17 Skeleton review, skeleton.md 근거).\n */\n placeholder: themeColor(\"text\", 0.12),\n },\n content: {\n primary: themeColor(\"text\"),\n body: themeColor(\"textBody\"),\n secondary: themeColor(\"textMuted\"),\n tertiary: themeColor(\"textSub\"),\n decorative: themeColor(\"textWeak\"),\n brand: themeColor(\"contentBrand\"),\n danger: themeColor(\"danger\"),\n inverse: themeColor(\"onPrimary\"),\n // Status fills are independent of a product's primary button contrast pair.\n statusInverse: themeColor(\"onStatusFill\"),\n },\n border: {\n subtle: themeColor(\"border\", 0.7),\n default: themeColor(\"border\"),\n /** Resting outline of an interactive control. */\n control: themeColor(\"borderControl\"),\n strong: themeColor(\"textWeak\"),\n focus: themeColor(\"contentBrand\"),\n danger: themeColor(\"danger\"),\n },\n action: {\n brand: {\n background: themeColor(\"primary\"),\n content: themeColor(\"onPrimary\"),\n },\n neutral: {\n background: themeColor(\"bg\"),\n content: themeColor(\"text\"),\n // A control outline is a border role. `textSub` made a resting neutral\n // action read heavier than the selected control beside it.\n border: themeColor(\"borderControl\"),\n },\n danger: {\n background: themeColor(\"dangerFill\"),\n content: themeColor(\"onDanger\"),\n },\n },\n feedback: {\n info: {\n foreground: accentColor(\"info\"),\n background: accentColor(\"info\", accentTint.weak),\n badgeBackground: accentColor(\"info\", accentTint.weak),\n border: accentColor(\"info\", accentTint.border),\n },\n success: {\n foreground: accentColor(\"success\"),\n background: accentColor(\"success\", accentTint.weak),\n badgeBackground: accentColor(\"success\", accentTint.weak),\n border: accentColor(\"success\", accentTint.border),\n },\n warning: {\n foreground: accentColor(\"warning\"),\n background: accentColor(\"warning\", accentTint.weak),\n badgeBackground: accentColor(\"warning\", accentTint.weak),\n border: accentColor(\"warning\", accentTint.border),\n },\n attention: {\n foreground: accentColor(\"attention\"),\n background: accentColor(\"attention\", accentTint.weak),\n badgeBackground: accentColor(\"attention\", accentTint.weak),\n border: accentColor(\"attention\", accentTint.border),\n },\n danger: {\n foreground: themeColor(\"danger\"),\n background: themeColor(\"danger\", accentTint.weak),\n badgeBackground: themeColor(\"danger\", accentTint.weak),\n border: themeColor(\"danger\", accentTint.border),\n },\n },\n interaction: {\n hover: themeColor(\"text\", stateLayer.hover),\n focus: themeColor(\"contentBrand\", stateLayer.focus),\n pressed: themeColor(\"text\", stateLayer.pressed),\n selected: themeColor(\"primary\", stateLayer.selected),\n /**\n * Lower edge of a raised object, composited over its own fill so one shade\n * darkens any brand color (BottomNavigation raised tile, Spint 2026-10-13).\n */\n depth: themeColor(\"text\", 0.45),\n },\n} as const;\n"]}
1
+ {"version":3,"file":"semantic-colors.js","sourceRoot":"","sources":["../src/semantic-colors.ts"],"names":[],"mappings":"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","sourcesContent":["import { stateLayer } from \"./foundations.js\";\nimport { accentTint } from \"./colors.js\";\nimport { accentColor, themeColor } from \"./color-references.js\";\n\n/**\n * Stable role aliases above the legacy ThemeColors key set. New recipes should\n * prefer these roles so the underlying theme shape can evolve independently.\n */\nexport const semanticColors = {\n canvas: themeColor(\"bg\"),\n surface: {\n default: themeColor(\"surface\"),\n sunken: themeColor(\"surfaceAlt\"),\n raised: themeColor(\"bg\"),\n brand: themeColor(\"surfaceAccent\"),\n // Opaque neutral status plates are surfaces, independent of notice severity.\n statusNeutral: themeColor(\"neutralFill\"),\n statusStrong: themeColor(\"strongFill\"),\n /**\n * Loading placeholder fill (Skeleton, ListRow `loading`). A translucent text tint instead of an opaque\n * neutral: it darkens/lightens whatever it sits on, so it stays visible on canvas, Card surfaces and\n * profile palettes whose `surfaceAlt` is as light as `bg` (2026-10-17 Skeleton review, skeleton.md 근거).\n */\n placeholder: themeColor(\"text\", 0.12),\n },\n // 2026-10-23: a neutral selection must not recolor brand surfaces or primary actions.\n selection: {\n background: themeColor(\"surfaceSelected\"),\n content: themeColor(\"text\"),\n border: themeColor(\"borderControl\"),\n },\n content: {\n primary: themeColor(\"text\"),\n body: themeColor(\"textBody\"),\n secondary: themeColor(\"textMuted\"),\n tertiary: themeColor(\"textSub\"),\n decorative: themeColor(\"textWeak\"),\n brand: themeColor(\"contentBrand\"),\n danger: themeColor(\"danger\"),\n inverse: themeColor(\"onPrimary\"),\n // Status fills are independent of a product's primary button contrast pair.\n statusInverse: themeColor(\"onStatusFill\"),\n },\n border: {\n subtle: themeColor(\"border\", 0.7),\n default: themeColor(\"border\"),\n /** Resting outline of an interactive control. */\n control: themeColor(\"borderControl\"),\n strong: themeColor(\"textWeak\"),\n focus: themeColor(\"contentBrand\"),\n danger: themeColor(\"danger\"),\n },\n action: {\n brand: {\n background: themeColor(\"primary\"),\n content: themeColor(\"onPrimary\"),\n },\n neutral: {\n background: themeColor(\"bg\"),\n content: themeColor(\"text\"),\n // A control outline is a border role. `textSub` made a resting neutral\n // action read heavier than the selected control beside it.\n border: themeColor(\"borderControl\"),\n },\n danger: {\n background: themeColor(\"dangerFill\"),\n content: themeColor(\"onDanger\"),\n },\n },\n feedback: {\n info: {\n foreground: accentColor(\"info\"),\n background: accentColor(\"info\", accentTint.weak),\n badgeBackground: accentColor(\"info\", accentTint.weak),\n border: accentColor(\"info\", accentTint.border),\n },\n success: {\n foreground: accentColor(\"success\"),\n background: accentColor(\"success\", accentTint.weak),\n badgeBackground: accentColor(\"success\", accentTint.weak),\n border: accentColor(\"success\", accentTint.border),\n },\n warning: {\n foreground: accentColor(\"warning\"),\n background: accentColor(\"warning\", accentTint.weak),\n badgeBackground: accentColor(\"warning\", accentTint.weak),\n border: accentColor(\"warning\", accentTint.border),\n },\n attention: {\n foreground: accentColor(\"attention\"),\n background: accentColor(\"attention\", accentTint.weak),\n badgeBackground: accentColor(\"attention\", accentTint.weak),\n border: accentColor(\"attention\", accentTint.border),\n },\n danger: {\n foreground: themeColor(\"danger\"),\n background: themeColor(\"danger\", accentTint.weak),\n badgeBackground: themeColor(\"danger\", accentTint.weak),\n border: themeColor(\"danger\", accentTint.border),\n },\n },\n interaction: {\n hover: themeColor(\"text\", stateLayer.hover),\n focus: themeColor(\"contentBrand\", stateLayer.focus),\n pressed: themeColor(\"text\", stateLayer.pressed),\n selected: themeColor(\"surfaceSelected\"),\n /**\n * Lower edge of a raised object, composited over its own fill so one shade\n * darkens any brand color (BottomNavigation raised tile, Spint 2026-10-13).\n */\n depth: themeColor(\"text\", 0.45),\n },\n} as const;\n"]}
package/dist/sidebar.d.ts CHANGED
@@ -111,12 +111,12 @@ export declare const sidebarRecipe: {
111
111
  };
112
112
  readonly selectedBackground: Readonly<{
113
113
  source: "theme";
114
- key: "bg";
114
+ key: "surfaceSelected";
115
115
  alpha?: number;
116
116
  }>;
117
117
  readonly selectedIndicator: Readonly<{
118
118
  source: "theme";
119
- key: "contentBrand";
119
+ key: "text";
120
120
  alpha?: number;
121
121
  }>;
122
122
  readonly danger: Readonly<{
@@ -17,6 +17,8 @@ export type TimePickerOptions = Readonly<{
17
17
  labels: TimePickerLabels;
18
18
  /** Inline changes update the parent's draft; its enclosing form owns confirmation. */
19
19
  presentation?: "field" | "inline";
20
+ /** Keep tap-list behavior by default; wheel selection shares the same draft and confirmation. */
21
+ selectionStyle?: "list" | "wheel";
20
22
  minuteStep?: number;
21
23
  isTimeUnavailable?: (value: string) => boolean;
22
24
  disabled?: boolean;
@@ -33,12 +35,22 @@ export declare const timePickerRecipe: {
33
35
  readonly readoutSize: 20;
34
36
  readonly readoutLineHeight: 28;
35
37
  readonly panelWidth: 304;
38
+ readonly selectionStroke: 1;
36
39
  readonly defaults: {
37
40
  readonly presentation: "field";
41
+ readonly selectionStyle: "list";
38
42
  readonly minuteStep: 1;
39
43
  };
40
44
  };
41
45
  export declare function validateTimePicker(options: TimePickerOptions): void;
46
+ /** Offset 0 centers the first row because both renderers add half a viewport of end padding. */
47
+ export declare function resolveTimePickerWheelIndex(items: readonly Readonly<{
48
+ disabled: boolean;
49
+ }>[], offset: number): number | null;
50
+ /** Keyboard and native adjustable actions use the same available slots, without wrapping. */
51
+ export declare function moveTimePickerWheelIndex(items: readonly Readonly<{
52
+ disabled: boolean;
53
+ }>[], from: number, direction: 1 | -1): number | null;
42
54
  export declare function timePickerDraft(value: string | null): TimePickerDraft;
43
55
  export declare function resolveTimePickerValue(draft: TimePickerDraft, options: Pick<TimePickerOptions, "minuteStep" | "isTimeUnavailable">): string | null;
44
56
  export declare function timePickerItems(part: TimePickerPart, draft: TimePickerDraft, options: Pick<TimePickerOptions, "minuteStep" | "isTimeUnavailable">): {
@@ -47,7 +59,7 @@ export declare function timePickerItems(part: TimePickerPart, draft: TimePickerD
47
59
  }[];
48
60
  export declare const timePickerBehavior: {
49
61
  readonly controlled: readonly ["value", "onValueChange"];
50
- readonly inputs: readonly ["labels", "minuteStep", "isTimeUnavailable", "presentation"];
62
+ readonly inputs: readonly ["labels", "minuteStep", "isTimeUnavailable", "presentation", "selectionStyle"];
51
63
  readonly stateAxes: {
52
64
  readonly availability: readonly ["enabled", "disabled", "readOnly"];
53
65
  readonly value: readonly ["empty", "partial", "selected"];
@@ -60,11 +72,11 @@ export declare const timePickerBehavior: {
60
72
  readonly dismiss: readonly ["action", "escape", "outside", "blur"];
61
73
  };
62
74
  readonly native: {
63
- readonly roles: readonly ["button", "dialog", "radio"];
75
+ readonly roles: readonly ["button", "dialog", "radio", "adjustable"];
64
76
  readonly states: readonly ["disabled", "expanded", "checked"];
65
- readonly actions: readonly ["activate", "dismiss"];
77
+ readonly actions: readonly ["activate", "dismiss", "increment", "decrement"];
66
78
  readonly dismiss: readonly ["action", "back", "outside"];
67
79
  };
68
- readonly scenarios: readonly ["two-columns-share-one-draft-and-overlay", "column-selection-never-dismisses", "confirm-commits-a-complete-available-HH:mm", "cancel-discards-edits", "external-value-reconciles-draft", "inline-parent-owns-confirmation", "unavailable-options-never-activate", "midnight-is-a-valid-value"];
80
+ readonly scenarios: readonly ["two-columns-share-one-draft-and-overlay", "column-selection-never-dismisses", "confirm-commits-a-complete-available-HH:mm", "cancel-discards-edits", "external-value-reconciles-draft", "inline-parent-owns-confirmation", "unavailable-options-never-activate", "midnight-is-a-valid-value", "wheel-centers-available-slots-without-initial-commit", "wheel-adjustment-preserves-confirm-and-cancel"];
69
81
  };
70
82
  //# sourceMappingURL=time-picker.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"time-picker.d.ts","sourceRoot":"","sources":["../src/time-picker.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,QAAQ,CAAC;AAC/C,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC;IAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,CAAC;AACvF,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC;IACtC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;CACnG,CAAC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IACvC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1C,MAAM,EAAE,gBAAgB,CAAC;IACzB,sFAAsF;IACtF,YAAY,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;IAC/C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC,CAAC;AAKH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;CAInB,CAAC;AAEX,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAMnE;AACD,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,eAAe,CAErE;AACD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,eAAe,EAAE,OAAO,EAAE,IAAI,CAAC,iBAAiB,EAAE,YAAY,GAAG,mBAAmB,CAAC,GAAG,MAAM,GAAG,IAAI,CAKlJ;AACD,wBAAgB,eAAe,CAAC,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,eAAe,EAAE,OAAO,EAAE,IAAI,CAAC,iBAAiB,EAAE,YAAY,GAAG,mBAAmB,CAAC;;;IAUjJ;AAED,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;CAMM,CAAC"}
1
+ {"version":3,"file":"time-picker.d.ts","sourceRoot":"","sources":["../src/time-picker.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,QAAQ,CAAC;AAC/C,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC;IAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,CAAC;AACvF,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC;IACtC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;CACnG,CAAC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IACvC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1C,MAAM,EAAE,gBAAgB,CAAC;IACzB,sFAAsF;IACtF,YAAY,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;IAClC,iGAAiG;IACjG,cAAc,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;IAC/C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC,CAAC;AAKH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;CAKnB,CAAC;AAEX,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAOnE;AAED,gGAAgG;AAChG,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,SAAS,QAAQ,CAAC;IAAE,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAO5H;AAED,6FAA6F;AAC7F,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,SAAS,QAAQ,CAAC;IAAE,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAC,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,MAAM,GAAG,IAAI,CAK1I;AACD,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,eAAe,CAErE;AACD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,eAAe,EAAE,OAAO,EAAE,IAAI,CAAC,iBAAiB,EAAE,YAAY,GAAG,mBAAmB,CAAC,GAAG,MAAM,GAAG,IAAI,CAKlJ;AACD,wBAAgB,eAAe,CAAC,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,eAAe,EAAE,OAAO,EAAE,IAAI,CAAC,iBAAiB,EAAE,YAAY,GAAG,mBAAmB,CAAC;;;IAUjJ;AAED,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;CAMM,CAAC"}
@@ -1,11 +1,12 @@
1
- import { control, spacing, typography } from "./foundations.js";
1
+ import { control, spacing, stroke, typography } from "./foundations.js";
2
2
  // A single two-column session replaces independently closing Select overlays (2026-10-19).
3
3
  // Keep the five 44px selection rows, but use a compact readout: the 40px summary exceeded the shared
4
4
  // 24px cap and dominated the mobile departure Sheet (2026-10-20 review; APP_VISUAL_QUALITY_STANDARD §4).
5
5
  export const timePickerRecipe = {
6
6
  rowHeight: control.minTouchTarget, visibleRows: 5, gap: spacing.sm, sectionGap: spacing.md,
7
7
  radius: "md", readoutSize: typography.titleLarge.fontSize, readoutLineHeight: typography.titleLarge.lineHeight, panelWidth: 304,
8
- defaults: { presentation: "field", minuteStep: 1 },
8
+ selectionStroke: stroke.default,
9
+ defaults: { presentation: "field", selectionStyle: "list", minuteStep: 1 },
9
10
  };
10
11
  export function validateTimePicker(options) {
11
12
  if (options.value !== null && !/^([01]\d|2[0-3]):[0-5]\d$/.test(options.value))
@@ -18,6 +19,26 @@ export function validateTimePicker(options) {
18
19
  throw new TypeError("TimePicker labels must not be empty");
19
20
  if (options.presentation !== undefined && options.presentation !== "field" && options.presentation !== "inline")
20
21
  throw new TypeError("Invalid TimePicker presentation");
22
+ if (options.selectionStyle !== undefined && options.selectionStyle !== "list" && options.selectionStyle !== "wheel")
23
+ throw new TypeError("Invalid TimePicker selectionStyle");
24
+ }
25
+ /** Offset 0 centers the first row because both renderers add half a viewport of end padding. */
26
+ export function resolveTimePickerWheelIndex(items, offset) {
27
+ const position = Math.max(0, offset) / timePickerRecipe.rowHeight;
28
+ let nearest = null;
29
+ for (let index = 0; index < items.length; index++) {
30
+ if (!items[index].disabled && (nearest === null || Math.abs(index - position) < Math.abs(nearest - position)))
31
+ nearest = index;
32
+ }
33
+ return nearest;
34
+ }
35
+ /** Keyboard and native adjustable actions use the same available slots, without wrapping. */
36
+ export function moveTimePickerWheelIndex(items, from, direction) {
37
+ for (let index = from < 0 ? (direction === 1 ? 0 : items.length - 1) : from + direction; index >= 0 && index < items.length; index += direction) {
38
+ if (!items[index].disabled)
39
+ return index;
40
+ }
41
+ return null;
21
42
  }
22
43
  export function timePickerDraft(value) {
23
44
  return { hour: value?.slice(0, 2) ?? null, minute: value?.slice(3, 5) ?? null };
@@ -42,10 +63,10 @@ export function timePickerItems(part, draft, options) {
42
63
  });
43
64
  }
44
65
  export const timePickerBehavior = {
45
- controlled: ["value", "onValueChange"], inputs: ["labels", "minuteStep", "isTimeUnavailable", "presentation"],
66
+ controlled: ["value", "onValueChange"], inputs: ["labels", "minuteStep", "isTimeUnavailable", "presentation", "selectionStyle"],
46
67
  stateAxes: { availability: ["enabled", "disabled", "readOnly"], value: ["empty", "partial", "selected"], interaction: ["closed", "editing"] },
47
68
  web: { roles: ["button", "dialog", "listbox", "option"], keyboard: ["Tab", "Enter", "Space", "Escape", "ArrowUp", "ArrowDown", "Home", "End"], focus: "roving", dismiss: ["action", "escape", "outside", "blur"] },
48
- native: { roles: ["button", "dialog", "radio"], states: ["disabled", "expanded", "checked"], actions: ["activate", "dismiss"], dismiss: ["action", "back", "outside"] },
49
- scenarios: ["two-columns-share-one-draft-and-overlay", "column-selection-never-dismisses", "confirm-commits-a-complete-available-HH:mm", "cancel-discards-edits", "external-value-reconciles-draft", "inline-parent-owns-confirmation", "unavailable-options-never-activate", "midnight-is-a-valid-value"],
69
+ native: { roles: ["button", "dialog", "radio", "adjustable"], states: ["disabled", "expanded", "checked"], actions: ["activate", "dismiss", "increment", "decrement"], dismiss: ["action", "back", "outside"] },
70
+ scenarios: ["two-columns-share-one-draft-and-overlay", "column-selection-never-dismisses", "confirm-commits-a-complete-available-HH:mm", "cancel-discards-edits", "external-value-reconciles-draft", "inline-parent-owns-confirmation", "unavailable-options-never-activate", "midnight-is-a-valid-value", "wheel-centers-available-slots-without-initial-commit", "wheel-adjustment-preserves-confirm-and-cancel"],
50
71
  };
51
72
  //# sourceMappingURL=time-picker.js.map