@3fn/core 13.0.0 → 14.0.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 (236) hide show
  1. package/.kiro/agents/ada-prompt.md +80 -132
  2. package/.kiro/agents/ada-prompt.md.attribution.json +45 -0
  3. package/.kiro/agents/ada.json +44 -59
  4. package/.kiro/agents/ada.json.attribution.json +13 -0
  5. package/.kiro/agents/data-prompt.md +83 -74
  6. package/.kiro/agents/data-prompt.md.attribution.json +53 -0
  7. package/.kiro/agents/data.json +31 -38
  8. package/.kiro/agents/data.json.attribution.json +13 -0
  9. package/.kiro/agents/kenya-prompt.md +83 -72
  10. package/.kiro/agents/kenya-prompt.md.attribution.json +53 -0
  11. package/.kiro/agents/kenya.json +27 -35
  12. package/.kiro/agents/kenya.json.attribution.json +13 -0
  13. package/.kiro/agents/leonardo-prompt.md +176 -234
  14. package/.kiro/agents/leonardo-prompt.md.attribution.json +45 -0
  15. package/.kiro/agents/leonardo.json +28 -36
  16. package/.kiro/agents/leonardo.json.attribution.json +13 -0
  17. package/.kiro/agents/lina-prompt.md +110 -151
  18. package/.kiro/agents/lina-prompt.md.attribution.json +53 -0
  19. package/.kiro/agents/lina.json +46 -59
  20. package/.kiro/agents/lina.json.attribution.json +13 -0
  21. package/.kiro/agents/sparky-prompt.md +89 -72
  22. package/.kiro/agents/sparky-prompt.md.attribution.json +53 -0
  23. package/.kiro/agents/sparky.json +37 -37
  24. package/.kiro/agents/sparky.json.attribution.json +13 -0
  25. package/.kiro/agents/stacy-prompt.md +74 -48
  26. package/.kiro/agents/stacy-prompt.md.attribution.json +45 -0
  27. package/.kiro/agents/stacy.json +28 -30
  28. package/.kiro/agents/stacy.json.attribution.json +13 -0
  29. package/.kiro/agents/thurgood-prompt.md +94 -138
  30. package/.kiro/agents/thurgood-prompt.md.attribution.json +45 -0
  31. package/.kiro/agents/thurgood.json +31 -35
  32. package/.kiro/agents/thurgood.json.attribution.json +13 -0
  33. package/.kiro/steering/AI-Collaboration-Principles.md +3 -3
  34. package/.kiro/steering/Civitas-System-Overview.md +5 -5
  35. package/.kiro/steering/DesignerPunk-Systems-Overview.md +6 -6
  36. package/.kiro/steering/Task-Completion-Protocol.md +98 -17
  37. package/.kiro/steering/core-goals.md +3 -3
  38. package/.kiro/steering/personal-note.md +1 -1
  39. package/.kiro/steering/start-up-tasks.md +14 -3
  40. package/application-mcp-server/src/index.ts +26 -0
  41. package/dist/ComponentTokens.android.kt +1 -1
  42. package/dist/ComponentTokens.ios.swift +1 -1
  43. package/dist/ComponentTokens.web.css +1 -1
  44. package/dist/DesignTokens.android.kt +1 -1
  45. package/dist/DesignTokens.dtcg.json +8 -5
  46. package/dist/DesignTokens.figma.json +2 -2
  47. package/dist/DesignTokens.ios.swift +1 -1
  48. package/dist/DesignTokens.web.css +1 -1
  49. package/dist/android/DesignTokens.android.kt +1 -1
  50. package/dist/blend/OklchBlendCalculator.js +1 -0
  51. package/dist/blend/ThemeAwareBlendUtilities.web.d.ts +13 -2
  52. package/dist/blend/ThemeAwareBlendUtilities.web.js +6 -1
  53. package/dist/browser/designerpunk.esm.js +22 -81
  54. package/dist/browser/designerpunk.esm.min.js +29 -32
  55. package/dist/browser/designerpunk.umd.js +22 -81
  56. package/dist/browser/designerpunk.umd.min.js +42 -45
  57. package/dist/browser/tokens.css +1 -1
  58. package/dist/components/core/Avatar-Base/platforms/web/Avatar.web.js +24 -5
  59. package/dist/components/core/Button-CTA/examples/BasicUsage.d.ts +16 -28
  60. package/dist/components/core/Button-CTA/examples/BasicUsage.js +18 -43
  61. package/dist/components/core/Button-CTA/platforms/web/ButtonCTA.web.d.ts +3 -15
  62. package/dist/components/core/Button-CTA/platforms/web/ButtonCTA.web.js +9 -58
  63. package/dist/components/core/Button-CTA/types.d.ts +0 -24
  64. package/dist/components/core/Button-CTA/types.js +6 -0
  65. package/dist/components/core/Input-Text-Base/types.d.ts +13 -1
  66. package/dist/components/core/Input-Text-Password/platforms/web/InputTextPassword.web.js +11 -2
  67. package/dist/generators/DTCGFormatGenerator.js +8 -0
  68. package/dist/integration/BuildErrorHandler.js +2 -2
  69. package/dist/ios/DesignTokens.ios.swift +1 -1
  70. package/dist/mcp/application-mcp.js +24 -0
  71. package/dist/mcp/docs-mcp.js +130 -15
  72. package/dist/mcp/product-mcp.js +25 -0
  73. package/dist/tokens/OpacityTokens.js +1 -1
  74. package/dist/tokens/semantic/BlendTokens.d.ts +10 -3
  75. package/dist/tokens/semantic/BlendTokens.js +17 -5
  76. package/dist/tokens/semantic/OpacityTokens.d.ts +4 -4
  77. package/dist/tokens/semantic/OpacityTokens.js +4 -4
  78. package/dist/types/ComponentTypes.d.ts +1 -1
  79. package/dist/types/generated/TokenTypes.d.ts +1 -1
  80. package/dist/types/generated/TokenTypes.js +1 -1
  81. package/dist/validators/StemmaTokenUsageValidator.js +3 -2
  82. package/dist/web/DesignTokens.web.css +1 -1
  83. package/governance/Component-Development-Guide.md +22 -12
  84. package/governance/Component-Development-Standards.md +2 -2
  85. package/governance/Component-Family-Avatar.md +0 -1
  86. package/governance/Component-Family-Badge.md +0 -1
  87. package/governance/Component-Family-Button.md +4 -17
  88. package/governance/Component-Family-Chip.md +0 -1
  89. package/governance/Component-Family-Container.md +0 -1
  90. package/governance/Component-Family-Data-Display.md +1 -2
  91. package/governance/Component-Family-Divider.md +1 -2
  92. package/governance/Component-Family-Form-Inputs.md +5 -5
  93. package/governance/Component-Family-Icon.md +1 -2
  94. package/governance/Component-Family-Loading.md +1 -2
  95. package/governance/Component-Family-Modal.md +1 -2
  96. package/governance/Component-Family-Navigation.md +0 -1
  97. package/governance/Component-Family-Progress.md +0 -1
  98. package/governance/Component-Inheritance-Structures.md +195 -76
  99. package/governance/Component-MCP-Document-Template.md +6 -5
  100. package/governance/Component-Primitive-vs-Semantic-Philosophy.md +1 -1
  101. package/governance/Component-Quick-Reference.md +33 -33
  102. package/governance/Component-Readiness-Status.md +54 -38
  103. package/governance/Component-Templates.md +32 -36
  104. package/governance/Contract-System-Reference.md +6 -6
  105. package/governance/MCP-Integration-Guide.md +1 -1
  106. package/governance/Process-Cross-Reference-Standards.md +31 -13
  107. package/governance/Process-Development-Workflow.md +49 -59
  108. package/governance/Process-File-Organization.md +24 -24
  109. package/governance/Process-Hook-Operations.md +19 -7
  110. package/governance/Process-Orchestration-Model-Selection.md +92 -0
  111. package/governance/Process-Spec-Planning.md +91 -39
  112. package/governance/Process-Task-Type-Definitions.md +80 -4
  113. package/governance/Product-Handoff-Protocol.md +2 -0
  114. package/governance/Rosetta-System-Architecture.md +6 -6
  115. package/governance/Test-Behavioral-Contract-Validation.md +38 -31
  116. package/governance/Test-Failure-Audit-Methodology.md +1 -1
  117. package/governance/Token-Family-Accessibility.md +1 -2
  118. package/governance/Token-Family-Blend.md +18 -16
  119. package/governance/Token-Family-Blur.md +0 -1
  120. package/governance/Token-Family-Border.md +1 -2
  121. package/governance/Token-Family-Color.md +0 -1
  122. package/governance/Token-Family-Glow.md +1 -2
  123. package/governance/Token-Family-Layering.md +0 -1
  124. package/governance/Token-Family-Motion.md +1 -2
  125. package/governance/Token-Family-Opacity.md +0 -1
  126. package/governance/Token-Family-Radius.md +1 -2
  127. package/governance/Token-Family-Responsive.md +1 -2
  128. package/governance/Token-Family-Shadow.md +1 -2
  129. package/governance/Token-Family-Sizing.md +0 -1
  130. package/governance/Token-Family-Spacing.md +1 -2
  131. package/governance/Token-Family-Typography.md +1 -2
  132. package/governance/Token-Governance.md +8 -8
  133. package/governance/Token-Quick-Reference.md +21 -21
  134. package/governance/Token-Resolution-Patterns.md +1 -1
  135. package/governance/Token-Semantic-Structure.md +1 -1
  136. package/governance/Web-Authoring-Standards.md +5 -5
  137. package/governance/browser-distribution-guide.md +1 -4
  138. package/governance/classification-map.md +368 -0
  139. package/governance/completion-documentation-guide.md +11 -8
  140. package/governance/component-meta-authoring-guide.md +1 -1
  141. package/governance/cross-platform-vs-platform-specific-decision-framework.md +1 -1
  142. package/governance/platform-implementation-guidelines.md +1 -1
  143. package/governance/release-management-system.md +2 -2
  144. package/governance/rosetta-system-principles.md +8 -6
  145. package/governance/stemma-system-principles.md +18 -17
  146. package/mcp-server/src/index.ts +24 -6
  147. package/mcp-server/src/indexer/DocumentIndexer.ts +119 -9
  148. package/mcp-server/src/indexer/__tests__/bare-id-crossrefs.test.ts +250 -0
  149. package/mcp-server/src/indexer/cross-ref-parser.ts +29 -1
  150. package/mcp-server/src/indexer/index-health.ts +27 -2
  151. package/mcp-server/src/query/__tests__/find-docs-calibration.test.ts +11 -26
  152. package/mcp-server/src/relocation-integrity-gate/__tests__/relocation-integrity-gate.test.ts +72 -5
  153. package/mcp-server/src/relocation-integrity-gate/relocation-integrity-gate.ts +81 -24
  154. package/mcp-server/src/tools/list-cross-references.ts +2 -2
  155. package/package.json +23 -21
  156. package/src/__tests__/browser-distribution/css-bundling.test.ts +6 -4
  157. package/src/__tests__/console-allowlist.json +14 -0
  158. package/src/__tests__/console-fail-setup.ts +169 -0
  159. package/src/__tests__/integration/Spec107-DesignLanguageContext.test.ts +16 -0
  160. package/src/__tests__/stemma-system/behavioral-contract-validation.test.ts +70 -17
  161. package/src/__tests__/stemma-system/contract-catalog-name-validation.test.ts +28 -0
  162. package/src/__tests__/stemma-system/form-inputs-contracts.test.ts +223 -16
  163. package/src/__tests__/stemma-system/input-text-native-base-call-alignment.test.ts +298 -0
  164. package/src/blend/OklchBlendCalculator.ts +3 -0
  165. package/src/blend/ThemeAwareBlendUtilities.android.kt +3 -0
  166. package/src/blend/ThemeAwareBlendUtilities.ios.swift +3 -0
  167. package/src/blend/ThemeAwareBlendUtilities.web.ts +9 -1
  168. package/src/blend/__tests__/InteractionStateAudit.test.ts +12 -9
  169. package/src/build/errors/__tests__/ErrorHandler.integration.test.ts +8 -0
  170. package/src/build/errors/__tests__/ErrorHandler.test.ts +5 -0
  171. package/src/build/workflow/__tests__/CICDIntegration.test.ts +12 -1
  172. package/src/cli/__tests__/init.test.ts +45 -11
  173. package/src/components/core/Avatar-Base/Avatar-Base.schema.yaml +1 -1
  174. package/src/components/core/Avatar-Base/__tests__/Avatar.accessibility.test.ts +121 -7
  175. package/src/components/core/Avatar-Base/__tests__/Avatar.image.test.ts +3 -0
  176. package/src/components/core/Avatar-Base/__tests__/Avatar.test.ts +15 -6
  177. package/src/components/core/Avatar-Base/contracts.yaml +11 -1
  178. package/src/components/core/Avatar-Base/platforms/web/Avatar.web.ts +24 -5
  179. package/src/components/core/Badge-Count-Base/contracts.yaml +1 -1
  180. package/src/components/core/Badge-Label-Base/contracts.yaml +1 -1
  181. package/src/components/core/Button-CTA/Button-CTA.schema.yaml +2 -12
  182. package/src/components/core/Button-CTA/README.md +3 -6
  183. package/src/components/core/Button-CTA/__tests__/ButtonCTA.test.ts +35 -89
  184. package/src/components/core/Button-CTA/__tests__/setup.test.ts +0 -2
  185. package/src/components/core/Button-CTA/__tests__/test-utils.ts +0 -2
  186. package/src/components/core/Button-CTA/contracts.yaml +6 -29
  187. package/src/components/core/Button-CTA/examples/BasicUsage.html +2 -14
  188. package/src/components/core/Button-CTA/examples/BasicUsage.tsx +17 -44
  189. package/src/components/core/Button-CTA/platforms/android/ButtonCTA.android.kt +12 -20
  190. package/src/components/core/Button-CTA/platforms/ios/ButtonCTA.ios.swift +12 -51
  191. package/src/components/core/Button-CTA/platforms/web/ButtonCTA.web.css +2 -26
  192. package/src/components/core/Button-CTA/platforms/web/ButtonCTA.web.ts +18 -71
  193. package/src/components/core/Button-CTA/types.ts +10 -28
  194. package/src/components/core/Chip-Base/__tests__/ChipBase.test.ts +13 -0
  195. package/src/components/core/Chip-Filter/__tests__/ChipFilter.test.ts +13 -0
  196. package/src/components/core/Chip-Input/__tests__/ChipInput.test.ts +13 -0
  197. package/src/components/core/Input-Text-Base/Input-Text-Base.schema.yaml +30 -2
  198. package/src/components/core/Input-Text-Base/README.md +25 -2
  199. package/src/components/core/Input-Text-Base/__tests__/focusIndicators.test.ts +16 -15
  200. package/src/components/core/Input-Text-Base/contracts.yaml +90 -0
  201. package/src/components/core/Input-Text-Base/platforms/android/InputTextBase.android.kt +26 -12
  202. package/src/components/core/Input-Text-Base/platforms/ios/InputTextBase.ios.swift +195 -59
  203. package/src/components/core/Input-Text-Base/types.ts +13 -1
  204. package/src/components/core/Input-Text-Email/Input-Text-Email.schema.yaml +5 -1
  205. package/src/components/core/Input-Text-Email/README.md +8 -7
  206. package/src/components/core/Input-Text-Email/platforms/android/InputTextEmail.android.kt +1 -4
  207. package/src/components/core/Input-Text-Email/platforms/ios/InputTextEmail.ios.swift +2 -16
  208. package/src/components/core/Input-Text-Password/Input-Text-Password.schema.yaml +10 -3
  209. package/src/components/core/Input-Text-Password/README.md +9 -8
  210. package/src/components/core/Input-Text-Password/contracts.yaml +5 -0
  211. package/src/components/core/Input-Text-Password/platforms/android/InputTextPassword.android.kt +17 -7
  212. package/src/components/core/Input-Text-Password/platforms/ios/InputTextPassword.ios.swift +22 -20
  213. package/src/components/core/Input-Text-Password/platforms/web/InputTextPassword.web.ts +11 -2
  214. package/src/components/core/Input-Text-PhoneNumber/Input-Text-PhoneNumber.schema.yaml +5 -1
  215. package/src/components/core/Input-Text-PhoneNumber/README.md +9 -8
  216. package/src/components/core/Input-Text-PhoneNumber/platforms/android/InputTextPhoneNumber.android.kt +2 -5
  217. package/src/components/core/Input-Text-PhoneNumber/platforms/ios/InputTextPhoneNumber.ios.swift +3 -17
  218. package/src/components/core/Nav-Header-App/contracts.yaml +1 -1
  219. package/src/components/core/Nav-SegmentedChoice-Base/contracts.yaml +1 -1
  220. package/src/components/core/Progress-Indicator-Connector-Base/contracts.yaml +1 -1
  221. package/src/components/core/Progress-Indicator-Label-Base/contracts.yaml +1 -1
  222. package/src/components/core/Progress-Indicator-Node-Base/contracts.yaml +1 -1
  223. package/src/components/core/Progress-Stepper-Base/__tests__/StepperBase.test.ts +5 -2
  224. package/src/components/core/Progress-Stepper-Detailed/__tests__/StepperDetailed.test.ts +5 -2
  225. package/src/generators/DTCGFormatGenerator.ts +6 -0
  226. package/src/generators/__tests__/DTCGConfigOptions.test.ts +14 -5
  227. package/src/integration/BuildErrorHandler.ts +2 -2
  228. package/src/tokens/OpacityTokens.ts +1 -1
  229. package/src/tokens/__tests__/OpacityTokens.test.ts +3 -1
  230. package/src/tokens/semantic/BlendTokens.ts +26 -5
  231. package/src/tokens/semantic/OpacityTokens.ts +4 -4
  232. package/src/tools/release/__tests__/ReleasePipeline.test.ts +1 -1
  233. package/src/types/ComponentTypes.ts +1 -1
  234. package/src/types/generated/TokenTypes.ts +1 -1
  235. package/src/validators/StemmaTokenUsageValidator.ts +3 -2
  236. package/token-index/semantics.yaml +1 -2
@@ -12,7 +12,7 @@
12
12
  * composition pattern as iOS and Android platforms. This ensures cross-platform
13
13
  * consistency and single source of truth for icon rendering.
14
14
  *
15
- * Uses theme-aware blend utilities for state colors (hover, pressed, disabled, icon)
15
+ * Uses theme-aware blend utilities for state colors (hover, pressed, icon)
16
16
  * instead of opacity or filter workarounds. This ensures cross-platform consistency
17
17
  * with iOS and Android implementations.
18
18
  *
@@ -86,13 +86,12 @@ function getIconSizeForButton(buttonSize: ButtonSize): IconBaseSize {
86
86
  * - Semantic `<button>` element with proper accessibility
87
87
  * - Token-based styling via CSS custom properties
88
88
  * - Supports text wrapping by default (accessibility-first)
89
- * - Uses blend utilities for state colors (hover, pressed, disabled, icon)
89
+ * - Uses blend utilities for state colors (hover, pressed, icon)
90
90
  * - WCAG 2.1 AA compliant:
91
91
  * - Keyboard navigation (Tab, Enter, Space)
92
92
  * - Focus indicators with 3:1 contrast ratio
93
93
  * - Color contrast 4.5:1 for all styles
94
94
  * - Screen reader support with ARIA attributes
95
- * - Proper disabled state handling
96
95
  *
97
96
  * @example
98
97
  * ```html
@@ -106,7 +105,6 @@ function getIconSizeForButton(buttonSize: ButtonSize): IconBaseSize {
106
105
  * variant="primary"
107
106
  * icon="arrow-right"
108
107
  * no-wrap="false"
109
- * disabled="false"
110
108
  * test-id="submit-button"
111
109
  * ></button-cta>
112
110
  * ```
@@ -143,7 +141,6 @@ export class ButtonCTA extends HTMLElement {
143
141
  // Cached blend colors for state styling
144
142
  private _hoverColor: string = '';
145
143
  private _pressedColor: string = '';
146
- private _disabledColor: string = '';
147
144
  private _iconColor: string = '';
148
145
  private _iconOpticalBalanceColor: string = '';
149
146
 
@@ -153,7 +150,7 @@ export class ButtonCTA extends HTMLElement {
153
150
  * When these attributes change, attributeChangedCallback is invoked.
154
151
  */
155
152
  static get observedAttributes(): string[] {
156
- return ['label', 'size', 'variant', 'icon', 'no-wrap', 'disabled', 'test-id'];
153
+ return ['label', 'size', 'variant', 'icon', 'no-wrap', 'test-id'];
157
154
  }
158
155
 
159
156
  constructor() {
@@ -219,15 +216,14 @@ export class ButtonCTA extends HTMLElement {
219
216
  * Calculate blend colors from CSS custom properties.
220
217
  *
221
218
  * Reads base colors from CSS custom properties and applies theme-aware blend
222
- * utilities to generate state colors (hover, pressed, disabled, icon).
223
- *
219
+ * utilities to generate state colors (hover, pressed, icon).
220
+ *
224
221
  * Uses getBlendUtilities() factory functions instead of direct blend calculations
225
222
  * for cross-platform consistency with iOS and Android implementations.
226
- *
223
+ *
227
224
  * State color mappings:
228
225
  * - Hover: darkerBlend(color.action.primary, blend.hoverDarker) - 8% darker
229
226
  * - Pressed: darkerBlend(color.action.primary, blend.pressedDarker) - 12% darker
230
- * - Disabled: desaturate(color.action.primary, blend.disabledDesaturate) - 12% less saturated
231
227
  * - Icon: lighterBlend(color.contrast.onAction, blend.iconLighter) - 8% lighter
232
228
  *
233
229
  * @see Requirements: 7.1, 7.2, 7.3, 7.4 - Button-CTA state colors
@@ -258,10 +254,7 @@ export class ButtonCTA extends HTMLElement {
258
254
 
259
255
  // @see Requirements: 7.2 - Pressed uses darkerBlend(color.action.primary, blend.pressedDarker)
260
256
  this._pressedColor = this._blendUtils.pressedColor(primaryColor);
261
-
262
- // @see Requirements: 7.3 - Disabled uses desaturate(color.action.primary, blend.disabledDesaturate)
263
- this._disabledColor = this._blendUtils.disabledColor(primaryColor);
264
-
257
+
265
258
  // @see Requirements: 7.4 - Icon uses lighterBlend(color.contrast.onAction, blend.iconLighter)
266
259
  // Icon for primary buttons: provides optical balance for icons on primary background
267
260
  this._iconColor = this._blendUtils.iconColor(onActionColor);
@@ -371,24 +364,6 @@ export class ButtonCTA extends HTMLElement {
371
364
  this.setAttribute('no-wrap', value.toString());
372
365
  }
373
366
 
374
- /**
375
- * Get the disabled state.
376
- */
377
- get disabled(): boolean {
378
- return this.hasAttribute('disabled');
379
- }
380
-
381
- /**
382
- * Set the disabled state.
383
- */
384
- set disabled(value: boolean) {
385
- if (value) {
386
- this.setAttribute('disabled', '');
387
- } else {
388
- this.removeAttribute('disabled');
389
- }
390
- }
391
-
392
367
  /**
393
368
  * Get the test ID.
394
369
  */
@@ -451,17 +426,15 @@ export class ButtonCTA extends HTMLElement {
451
426
  const variant = this.buttonVariant;
452
427
  const icon = this.icon;
453
428
  const noWrap = this.noWrap;
454
- const disabled = this.disabled;
455
429
  const testID = this.testID;
456
-
430
+
457
431
  // Generate class names
458
432
  const buttonClasses = [
459
433
  'button-cta',
460
434
  `button-cta--${size}`,
461
- `button-cta--${variant}`,
462
- disabled ? 'button-cta--disabled' : ''
463
- ].filter(Boolean).join(' ');
464
-
435
+ `button-cta--${variant}`
436
+ ].join(' ');
437
+
465
438
  // Get icon size for button size variant
466
439
  // Icon sizes are type-safe mapped from button size:
467
440
  // - Small/Medium: 24px (iconBaseSize100)
@@ -480,7 +453,6 @@ export class ButtonCTA extends HTMLElement {
480
453
  const blendColorStyles = `
481
454
  --_cta-hover-bg: ${this._hoverColor};
482
455
  --_cta-pressed-bg: ${this._pressedColor};
483
- --_cta-disabled-bg: ${this._disabledColor};
484
456
  --_cta-icon-color: ${this._iconColor};
485
457
  --_cta-icon-optical: ${this._iconOpticalBalanceColor};
486
458
  `;
@@ -491,11 +463,10 @@ export class ButtonCTA extends HTMLElement {
491
463
  // @see Requirements: 8.2, 8.3 (components render correctly with interactivity)
492
464
  this._shadowRoot.innerHTML = `
493
465
  <style>${buttonStyles}</style>
494
- <button
466
+ <button
495
467
  class="${buttonClasses}"
496
468
  type="button"
497
469
  role="button"
498
- ${disabled ? 'disabled aria-disabled="true"' : 'aria-disabled="false"'}
499
470
  ${testIDAttr}
500
471
  aria-label="${label}"
501
472
  style="${blendColorStyles}"
@@ -536,34 +507,23 @@ export class ButtonCTA extends HTMLElement {
536
507
  const variant = this.buttonVariant;
537
508
  const icon = this.icon;
538
509
  const noWrap = this.noWrap;
539
- const disabled = this.disabled;
540
510
  const testID = this.testID;
541
-
511
+
542
512
  // Get icon size for button size variant
543
513
  const iconSize: IconBaseSize = getIconSizeForButton(size);
544
-
514
+
545
515
  // ─────────────────────────────────────────────────────────────────
546
516
  // Update button element
547
517
  // ─────────────────────────────────────────────────────────────────
548
-
518
+
549
519
  // Update CSS class (preserves element identity for transitions)
550
520
  const buttonClasses = [
551
521
  'button-cta',
552
522
  `button-cta--${size}`,
553
- `button-cta--${variant}`,
554
- disabled ? 'button-cta--disabled' : ''
555
- ].filter(Boolean).join(' ');
523
+ `button-cta--${variant}`
524
+ ].join(' ');
556
525
  this._button.className = buttonClasses;
557
-
558
- // Update disabled state
559
- if (disabled) {
560
- this._button.setAttribute('disabled', '');
561
- this._button.setAttribute('aria-disabled', 'true');
562
- } else {
563
- this._button.removeAttribute('disabled');
564
- this._button.setAttribute('aria-disabled', 'false');
565
- }
566
-
526
+
567
527
  // Update aria-label
568
528
  this._button.setAttribute('aria-label', label);
569
529
 
@@ -577,7 +537,6 @@ export class ButtonCTA extends HTMLElement {
577
537
  // Update blend color CSS custom properties
578
538
  this._button.style.setProperty('--_cta-hover-bg', this._hoverColor);
579
539
  this._button.style.setProperty('--_cta-pressed-bg', this._pressedColor);
580
- this._button.style.setProperty('--_cta-disabled-bg', this._disabledColor);
581
540
  this._button.style.setProperty('--_cta-icon-color', this._iconColor);
582
541
  this._button.style.setProperty('--_cta-icon-optical', this._iconOpticalBalanceColor);
583
542
 
@@ -638,13 +597,6 @@ export class ButtonCTA extends HTMLElement {
638
597
  * Dispatches a custom 'press' event that bubbles up to parent elements.
639
598
  */
640
599
  private _handleClick = (event: Event): void => {
641
- // Don't dispatch if disabled
642
- if (this.disabled) {
643
- event.preventDefault();
644
- event.stopPropagation();
645
- return;
646
- }
647
-
648
600
  // Dispatch custom 'press' event
649
601
  this.dispatchEvent(new CustomEvent('press', {
650
602
  bubbles: true,
@@ -663,11 +615,6 @@ export class ButtonCTA extends HTMLElement {
663
615
  * @param event - The keyboard event
664
616
  */
665
617
  private _handleKeyDown = (event: KeyboardEvent): void => {
666
- // Don't handle if disabled
667
- if (this.disabled) {
668
- return;
669
- }
670
-
671
618
  // Handle Enter and Space keys (WCAG 2.1 AA keyboard navigation)
672
619
  if (event.key === 'Enter' || event.key === ' ') {
673
620
  event.preventDefault(); // Prevent default space scrolling
@@ -82,7 +82,6 @@ export type ButtonStyle = 'primary' | 'secondary' | 'tertiary';
82
82
  * variant="primary"
83
83
  * icon="arrow-right"
84
84
  * noWrap={false}
85
- * disabled={false}
86
85
  * testID="submit-button"
87
86
  * onPress={handleSubmit}
88
87
  * />
@@ -197,15 +196,13 @@ export interface ButtonProps {
197
196
 
198
197
  /**
199
198
  * Click/tap handler (required)
200
- *
199
+ *
201
200
  * Callback function invoked when the button is clicked or tapped.
202
- * Not called when button is disabled.
203
- *
201
+ *
204
202
  * @remarks
205
203
  * - Function signature: `() => void`
206
- * - Not invoked when `disabled` is true
207
204
  * - Triggered by click, tap, Enter key, or Space key
208
- *
205
+ *
209
206
  * @example
210
207
  * ```typescript
211
208
  * onPress={() => console.log('Clicked')}
@@ -232,26 +229,11 @@ export interface ButtonProps {
232
229
  * ```
233
230
  */
234
231
  testID?: string;
235
-
236
- /**
237
- * Optional disabled state (default: false)
238
- *
239
- * When true, the button is non-interactive and visually indicates
240
- * disabled state. The onPress handler is not called when disabled.
241
- *
242
- * @defaultValue false
243
- *
244
- * @remarks
245
- * - Disabled buttons are not keyboard focusable
246
- * - Screen readers announce disabled state
247
- * - Visual styling indicates non-interactive state
248
- *
249
- * @example
250
- * ```typescript
251
- * disabled={false} // Interactive (default)
252
- * disabled={true} // Non-interactive
253
- * disabled={isSubmitting} // Conditional disable
254
- * ```
255
- */
256
- disabled?: boolean;
257
232
  }
233
+
234
+ // NO DISABLED STATES
235
+ // DesignerPunk does not support disabled states for usability and
236
+ // accessibility reasons. If an action is unavailable, the component
237
+ // should not be rendered. For in-flight async actions, use the loading
238
+ // state (state_loading) instead.
239
+ // Ruled by Peter 2026-07-15 (Button-CTA disabled-state adjudication).
@@ -40,11 +40,24 @@ describe('Chip-Base Web Component', () => {
40
40
  beforeEach(async () => {
41
41
  // Wait for custom element to be defined
42
42
  await customElements.whenDefined('chip-base');
43
+ // Silence ONLY the known environmental warn ("Could not calculate blend colors",
44
+ // fired async via rAF): jsdom never applies real CSS, so the token lookup cannot
45
+ // succeed here and the component's designed fallback fires every run, forever.
46
+ // Anything ELSE passes through to the real console — a NEW warn class stays audible.
47
+ const realWarn = console.warn.bind(console);
48
+ jest.spyOn(console, 'warn').mockImplementation((...args: unknown[]) => {
49
+ // Component-prefixed match (Lina review, PR #39): the bare substring is shared by
50
+ // Button components too — prefix-matching prevents accidentally swallowing a
51
+ // cross-component warn if this suite ever composes a Button.
52
+ if (typeof args[0] === 'string' && args[0].includes('ChipBase: Could not calculate blend colors')) return;
53
+ realWarn(...args);
54
+ });
43
55
  });
44
56
 
45
57
  afterEach(() => {
46
58
  // Clean up any created elements
47
59
  cleanupDOM();
60
+ jest.restoreAllMocks();
48
61
  });
49
62
 
50
63
  // ============================================================================
@@ -42,11 +42,24 @@ describe('Chip-Filter Web Component', () => {
42
42
  beforeEach(async () => {
43
43
  // Wait for custom element to be defined
44
44
  await customElements.whenDefined('chip-filter');
45
+ // Silence ONLY the known environmental warn ("Could not calculate blend colors",
46
+ // fired async via rAF): jsdom never applies real CSS, so the token lookup cannot
47
+ // succeed here and the component's designed fallback fires every run, forever.
48
+ // Anything ELSE passes through to the real console — a NEW warn class stays audible.
49
+ const realWarn = console.warn.bind(console);
50
+ jest.spyOn(console, 'warn').mockImplementation((...args: unknown[]) => {
51
+ // Component-prefixed match (Lina review, PR #39): the bare substring is shared by
52
+ // Button components too — prefix-matching prevents accidentally swallowing a
53
+ // cross-component warn if this suite ever composes a Button.
54
+ if (typeof args[0] === 'string' && args[0].includes('ChipFilter: Could not calculate blend colors')) return;
55
+ realWarn(...args);
56
+ });
45
57
  });
46
58
 
47
59
  afterEach(() => {
48
60
  // Clean up any created elements
49
61
  cleanupDOM();
62
+ jest.restoreAllMocks();
50
63
  });
51
64
 
52
65
  // ============================================================================
@@ -41,11 +41,24 @@ describe('Chip-Input Web Component', () => {
41
41
  beforeEach(async () => {
42
42
  // Wait for custom element to be defined
43
43
  await customElements.whenDefined('chip-input');
44
+ // Silence ONLY the known environmental warn ("Could not calculate blend colors",
45
+ // fired async via rAF): jsdom never applies real CSS, so the token lookup cannot
46
+ // succeed here and the component's designed fallback fires every run, forever.
47
+ // Anything ELSE passes through to the real console — a NEW warn class stays audible.
48
+ const realWarn = console.warn.bind(console);
49
+ jest.spyOn(console, 'warn').mockImplementation((...args: unknown[]) => {
50
+ // Component-prefixed match (Lina review, PR #39): the bare substring is shared by
51
+ // Button components too — prefix-matching prevents accidentally swallowing a
52
+ // cross-component warn if this suite ever composes a Button.
53
+ if (typeof args[0] === 'string' && args[0].includes('ChipInput: Could not calculate blend colors')) return;
54
+ realWarn(...args);
55
+ });
44
56
  });
45
57
 
46
58
  afterEach(() => {
47
59
  // Clean up any created elements
48
60
  cleanupDOM();
61
+ jest.restoreAllMocks();
49
62
  });
50
63
 
51
64
  // ============================================================================
@@ -11,7 +11,7 @@
11
11
  name: Input-Text-Base
12
12
  type: primitive
13
13
  family: FormInput
14
- version: "1.0.0"
14
+ version: "1.1.0"
15
15
  readiness:
16
16
  web:
17
17
  reviewed: true
@@ -25,6 +25,16 @@ composition:
25
25
  - component: Icon-Base
26
26
  role: Trailing contextual icon (error, success, info)
27
27
  required: false
28
+ slots:
29
+ - name: trailingContent
30
+ description: >-
31
+ Trailing content slot rendered after the status icons. Semantic
32
+ variants use it to inject interactive trailing controls (e.g.,
33
+ Input-Text-Password's visibility toggle). Native platforms only
34
+ (iOS: AnyView?, Android: @Composable lambda) — the web base has no
35
+ equivalent slot; web variants render trailing controls in their
36
+ own Shadow DOM.
37
+ required: false
28
38
 
29
39
  # Component description
30
40
  description: |
@@ -113,7 +123,11 @@ properties:
113
123
  type: boolean
114
124
  required: false
115
125
  default: false
116
- description: Read-only state (alternative to disabled)
126
+ description: >-
127
+ Read-only display state (state_readonly contract) — the value is
128
+ non-editable but stays perceivable, selectable, and copyable; no
129
+ keyboard/IME is raised and disabled semantics are never applied. iOS
130
+ renders it as selectable static text inside the field chrome.
117
131
 
118
132
  required:
119
133
  type: boolean
@@ -188,9 +202,23 @@ platform_notes:
188
202
  ios:
189
203
  implementation: SwiftUI View
190
204
  file: platforms/ios/InputTextBase.ios.swift
205
+ notes: |
206
+ Platform composition parameters (not part of the cross-platform props):
207
+ - trailingContent: AnyView? = nil — trailing content slot (see composition.slots)
208
+ Masking: type .password renders SecureField; other types render TextField.
209
+ Variants toggle visibility by switching type between .password and .text.
191
210
  android:
192
211
  implementation: Jetpack Compose Composable
193
212
  file: platforms/android/InputTextBase.android.kt
213
+ notes: |
214
+ Platform composition parameters (not part of the cross-platform props):
215
+ - modifier, imeAction, keyboardActions — standard Compose passthroughs
216
+ - visualTransformation: VisualTransformation? = null — masking override;
217
+ null derives from type (PasswordVisualTransformation for PASSWORD)
218
+ - trailingContent: (@Composable () -> Unit)? = null — trailing content
219
+ slot (see composition.slots)
220
+ Variants toggle password visibility via visualTransformation while
221
+ keeping type = PASSWORD (preserves the password keyboard type).
194
222
 
195
223
  # Semantic components that inherit from this primitive
196
224
  semantic_variants:
@@ -36,11 +36,12 @@ This component guarantees the following behaviors across all platforms:
36
36
  | `validates_on_blur` | Validation triggers on blur | 3.3.1 |
37
37
  | `error_state_display` | Shows error message and styling | 3.3.1, 1.4.1 |
38
38
  | `success_state_display` | Shows success styling | 1.4.1 |
39
- | `disabled_state` | Prevents interaction when disabled | 4.1.2 |
40
39
  | `trailing_icon_display` | Shows contextual trailing icons | 1.4.1 |
41
40
  | `focus_ring` | WCAG 2.4.7 focus visible indicator | 2.4.7 |
42
41
  | `reduced_motion_support` | Respects prefers-reduced-motion | 2.3.3 |
43
42
 
43
+ **Excluded**: `state_disabled` — DesignerPunk does not support disabled states for usability and accessibility reasons. If an action is unavailable, the component should not be rendered (adjudicated 2026-07-15).
44
+
44
45
  ---
45
46
 
46
47
  ## Related Documentation
@@ -145,12 +146,27 @@ InputTextBase(
145
146
  | `type` | `'text' \| 'email' \| 'password' \| 'tel' \| 'url'` | No | `'text'` | Input type |
146
147
  | `autocomplete` | `string` | No | - | Autocomplete attribute for browser autofill |
147
148
  | `placeholder` | `string` | No | - | Placeholder text (only shown when label is floated and input is empty) |
148
- | `readOnly` | `boolean` | No | `false` | Read-only state (alternative to disabled) |
149
+ | `readOnly` | `boolean` | No | `false` | Read-only display state (`state_readonly` contract): value is non-editable but selectable and copyable; no keyboard raised; iOS renders selectable text in the field chrome. Ignored for secure types; not supported by Input-Text-Password |
149
150
  | `required` | `boolean` | No | `false` | Required field indicator |
150
151
  | `maxLength` | `number` | No | - | Maximum length for input value |
151
152
  | `testID` | `string` | No | - | Test ID for automated testing |
152
153
  | `className` | `string` | No | - | Additional CSS class names (web only) |
153
154
 
155
+ ### Platform Composition Parameters (native only)
156
+
157
+ These parameters exist on the native implementations for platform composition —
158
+ they are not part of the cross-platform props contract above:
159
+
160
+ | Parameter | Platform | Type | Default | Description |
161
+ |-----------|----------|------|---------|-------------|
162
+ | `trailingContent` | iOS | `AnyView?` | `nil` | Trailing content slot rendered after the status icons (e.g., Input-Text-Password's visibility toggle) |
163
+ | `trailingContent` | Android | `(@Composable () -> Unit)?` | `null` | Same slot, Compose form |
164
+ | `visualTransformation` | Android | `VisualTransformation?` | `null` | Masking override; `null` derives from `type` (`PasswordVisualTransformation` for `PASSWORD`). Lets variants toggle password visibility while keeping the password keyboard type |
165
+ | `modifier`, `imeAction`, `keyboardActions` | Android | — | — | Standard Compose passthroughs |
166
+
167
+ The web base has no trailing slot equivalent — web semantic variants render
168
+ trailing controls inside their own Shadow DOM.
169
+
154
170
  ---
155
171
 
156
172
  ## Token Consumption
@@ -221,6 +237,13 @@ Legacy type aliases are provided for backward compatibility during migration.
221
237
 
222
238
  ## Changelog
223
239
 
240
+ ### v1.1.0 (July 2026)
241
+
242
+ - Native bases gain a `trailingContent` composition slot (iOS `AnyView?`, Android `@Composable` lambda) so semantic variants can inject trailing controls — required by Input-Text-Password's visibility toggle
243
+ - Android base gains a `visualTransformation` override (null = derived from `type`) so variants can toggle masking without changing the input type
244
+ - Fixed Input-Text-PhoneNumber native callers referencing nonexistent enum members (`.phone`/`PHONE` → `.tel`/`TEL`)
245
+ - Added static base-call alignment test (`src/__tests__/stemma-system/input-text-native-base-call-alignment.test.ts`) guarding native call sites against parameter/enum drift
246
+
224
247
  ### v1.0.0 (January 2026)
225
248
 
226
249
  - Initial release as Input-Text-Base (renamed from TextInputField)
@@ -165,8 +165,8 @@ describe('Input-Text-Base Focus Indicators', () => {
165
165
  expect(iosComponentContent).toContain('accessibilityFocusWidth');
166
166
  expect(iosComponentContent).toContain('accessibilityFocusOffset');
167
167
 
168
- // Verify focus ring is visible when focused (and not disabled)
169
- expect(iosComponentContent).toContain('.opacity(isFocused && !isDisabled ? 1 : 0)');
168
+ // Verify focus ring is visible when focused
169
+ expect(iosComponentContent).toContain('.opacity(isFocused ? 1 : 0)');
170
170
  });
171
171
 
172
172
  it('should have focus ring animation that respects reduce motion', () => {
@@ -194,8 +194,8 @@ describe('Input-Text-Base Focus Indicators', () => {
194
194
  expect(androidComponentContent).toContain('accessibilityFocusColor');
195
195
  expect(androidComponentContent).toContain('accessibilityFocusOffset');
196
196
 
197
- // Verify focus ring is conditional on focus state (and not disabled)
198
- expect(androidComponentContent).toContain('if (isFocused && !isDisabled)');
197
+ // Verify focus ring is conditional on focus state
198
+ expect(androidComponentContent).toContain('if (isFocused)');
199
199
  });
200
200
  });
201
201
 
@@ -219,16 +219,17 @@ describe('Input-Text-Base Focus Indicators', () => {
219
219
 
220
220
  // iOS: Verify focus ring is not conditional on error/success state
221
221
  // (it should always show when focused, regardless of validation state)
222
- // Note: Focus ring is hidden when disabled (isDisabled check is expected)
223
- // Find the focus ring section in the InputTextBaseFieldStyle
222
+ // Find the focus ring section in InputTextBaseFieldChrome (the shared
223
+ // field chrome, extracted from InputTextBaseFieldStyle per the B-prime
224
+ // readOnly ruling, 2026-07-15)
224
225
  const iosFocusRingSection = iosContent.substring(
225
- iosContent.indexOf('.opacity(isFocused && !isDisabled ? 1 : 0)'),
226
- iosContent.indexOf('.opacity(isFocused && !isDisabled ? 1 : 0)') + 200
226
+ iosContent.indexOf('.opacity(isFocused ? 1 : 0)'),
227
+ iosContent.indexOf('.opacity(isFocused ? 1 : 0)') + 200
227
228
  );
228
- expect(iosFocusRingSection).toContain('.opacity(isFocused && !isDisabled ? 1 : 0)');
229
-
230
- // Android: Verify focus ring is conditional on focus state (and not disabled)
231
- expect(androidContent).toContain('if (isFocused && !isDisabled)');
229
+ expect(iosFocusRingSection).toContain('.opacity(isFocused ? 1 : 0)');
230
+
231
+ // Android: Verify focus ring is conditional on focus state
232
+ expect(androidContent).toContain('if (isFocused)');
232
233
  });
233
234
  });
234
235
 
@@ -249,10 +250,10 @@ describe('Input-Text-Base Focus Indicators', () => {
249
250
  expect(cssContent).toContain(':focus-visible');
250
251
 
251
252
  // iOS: Uses overlay with opacity based on focus state
252
- expect(iosContent).toContain('.opacity(isFocused && !isDisabled ? 1 : 0)');
253
-
253
+ expect(iosContent).toContain('.opacity(isFocused ? 1 : 0)');
254
+
254
255
  // Android: Uses conditional border based on focus state
255
- expect(androidContent).toContain('if (isFocused && !isDisabled)');
256
+ expect(androidContent).toContain('if (isFocused)');
256
257
  });
257
258
 
258
259
  it('should document WCAG 2.4.7 compliance in behavioral contracts', () => {
@@ -9,12 +9,29 @@ contracts:
9
9
  behavior: |
10
10
  Component can receive focus via Tab key navigation.
11
11
  Focus state is visually indicated and triggers label animation.
12
+
13
+ Platform note — iOS readOnly carve-out (declared, adjudicated): when
14
+ readOnly is true, iOS renders the value as selectable static text
15
+ (see state_readonly) and does NOT participate in keyboard/Full
16
+ Keyboard Access focus order; onFocus/onBlur are declared not-fired on
17
+ that path. Web and Android readOnly fields affirmatively REMAIN in
18
+ focus order and fire focus events (removing an Android readOnly field
19
+ from focus order would require enabled=false — banned disabled
20
+ semantics — so normalizing web/Android down to iOS is not an
21
+ available move). The carve-out is a mitigable declared exception, not
22
+ a settled permanent difference: an on-device .focusable(interactions:)
23
+ prototype is tracked, and if it verifies (FKA reachability + keyboard
24
+ copy), iOS re-enters focus order and this note tightens to full
25
+ three-platform participation.
26
+ Ruling: .kiro/issues/input-text-base-ios-readonly-adjudication.md
27
+ (RULED B-prime, Peter 2026-07-15).
12
28
  wcag: "2.1.1 Keyboard"
13
29
  platforms: [web, ios, android]
14
30
  validation:
15
31
  - Tab key moves focus to input
16
32
  - Focus state is visually distinct
17
33
  - Focus triggers label float animation
34
+ - readOnly fields remain in keyboard focus order on web and Android
18
35
  test_approach: |
19
36
  - Press Tab, verify input receives focus
20
37
  - Verify label animates on focus
@@ -74,6 +91,79 @@ contracts:
74
91
  - Set error state, verify border/label/icon/message
75
92
  required: true
76
93
 
94
+ # state_readonly concept ratified into the Concept Catalog via ballot
95
+ # (.kiro/docs/ballots/2026-07-15-state-readonly-concept.md, RATIFIED Peter
96
+ # 2026-07-15). Authored under the B-prime ruling:
97
+ # .kiro/issues/input-text-base-ios-readonly-adjudication.md (RULED by Peter,
98
+ # 2026-07-15).
99
+ state_readonly:
100
+ category: state
101
+ description: Displays a non-editable value that stays perceivable, selectable, and copyable — never disabled semantics
102
+ behavior: |
103
+ When readOnly is true, the component displays its value without
104
+ allowing edits. Invariants on all platforms:
105
+ - Content is selectable and copyable (whole-string copy is the
106
+ accepted iOS behavior — long-press select/copy on static text).
107
+ - Content is not editable; no software keyboard/IME is raised.
108
+ - Assistive technology announces the label, the value, and a
109
+ read-only indication — NEVER a disabled/dimmed trait or state.
110
+ - No disabled semantics of any kind (no SwiftUI .disabled(), no
111
+ Compose enabled=false, no aria-disabled/disabled attribute).
112
+
113
+ Announcement per platform:
114
+ - Web: native readonly attribute semantics (implicit aria-readonly).
115
+ - Android: explicit stateDescription("read only") in semantics.
116
+ - iOS: composed accessibility — label + value + read-only hint.
117
+
118
+ Focus order per platform:
119
+ - Web and Android: readOnly fields affirmatively PARTICIPATE in
120
+ keyboard focus order and fire onFocus/onBlur.
121
+ - iOS: readOnly content is rendered as selectable static text inside
122
+ the shared field chrome (B-prime); it is NOT in keyboard/FKA focus
123
+ order and onFocus/onBlur are DECLARED not-fired — a mitigable
124
+ declared exception. A .focusable(interactions:) on-device prototype
125
+ is tracked; if it verifies, this contract auto-tightens and iOS
126
+ re-enters focus order.
127
+
128
+ Tap-acknowledgment delta (platform note): tapping an Android readOnly
129
+ field focuses it (focus ring engages; empty-field label floats);
130
+ tapping the iOS readOnly text gives no acknowledgment until
131
+ long-press (select/copy). Declared, cosmetic.
132
+
133
+ Empty-value behavior: the label rests in placeholder position on all
134
+ platforms (no platform force-floats it), and the field chrome
135
+ reserves its full height unconditionally.
136
+
137
+ Secure-type carve-out (declared): when Input-Text-Base is used
138
+ directly with a secure/password type, readOnly is IGNORED — the field
139
+ remains an editable secure field. The read-only display path renders
140
+ the value as selectable plaintext and must be unreachable for
141
+ secrets; read-only passwords are excluded at the variant level
142
+ (Input-Text-Password excludes state_readonly and never forwards
143
+ readOnly).
144
+ wcag: "4.1.2 Name, Role, Value"
145
+ platforms: [web, ios, android]
146
+ validation:
147
+ - Value is not editable when readOnly is true
148
+ - Content is selectable and copyable on all platforms
149
+ - No software keyboard/IME is raised
150
+ - Web renders the native readonly attribute
151
+ - Android passes readOnly to the text field with enabled=true and sets
152
+ stateDescription("read only")
153
+ - iOS renders selectable Text (textSelection enabled) inside the
154
+ shared field chrome and composes a read-only accessibility hint
155
+ - No platform applies disabled semantics (.disabled(), enabled=false,
156
+ aria-disabled, dimmed trait)
157
+ - readOnly fields remain in keyboard focus order on web and Android
158
+ test_approach: |
159
+ - Static (Jest): web readonly attribute wiring; Android
160
+ readOnly = readOnly pass-through; iOS textSelection path present and
161
+ no .disabled( anywhere in Form Inputs iOS sources
162
+ - Device: attempt edit (rejected), copy value, verify announcement
163
+ (VoiceOver: label + value + read-only hint, never "dimmed";
164
+ TalkBack: "read only")
165
+ required: true
166
+
77
167
  state_success:
78
168
  category: state
79
169
  description: Shows success styling