@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
@@ -13,17 +13,17 @@
13
13
  * - Color animation (text.subtle → primary with blend.focusSaturate)
14
14
  * - Offset animation (translateY)
15
15
  * - Trailing icon support (error, success, info)
16
+ * - Trailing content slot for semantic variants (e.g., password toggle)
16
17
  * - Respects reduce motion settings
17
18
  * - WCAG 2.1 AA compliant
18
- * - Uses theme-aware blend utilities for state colors (focus, disabled)
19
- *
19
+ * - Uses theme-aware blend utilities for state colors (focus)
20
+ *
20
21
  * Behavioral Contracts:
21
22
  * - focusable: Can receive keyboard focus
22
23
  * - float_label_animation: Label animates on focus
23
24
  * - validates_on_blur: Validation triggers on blur
24
25
  * - error_state_display: Shows error message and styling
25
26
  * - success_state_display: Shows success styling
26
- * - disabled_state: Prevents interaction when disabled
27
27
  * - trailing_icon_display: Shows contextual trailing icons
28
28
  * - focus_ring: WCAG 2.4.7 focus visible indicator
29
29
  * - reduced_motion_support: Respects prefers-reduced-motion
@@ -57,6 +57,7 @@ import androidx.compose.ui.platform.LocalDensity
57
57
  import androidx.compose.ui.semantics.contentDescription
58
58
  import androidx.compose.ui.semantics.error
59
59
  import androidx.compose.ui.semantics.semantics
60
+ import androidx.compose.ui.semantics.stateDescription
60
61
  import androidx.compose.ui.text.TextStyle
61
62
  import androidx.compose.ui.text.font.FontWeight
62
63
  import androidx.compose.ui.text.input.ImeAction
@@ -68,7 +69,6 @@ import androidx.compose.ui.unit.dp
68
69
  import androidx.compose.ui.unit.sp
69
70
  import android.provider.Settings
70
71
  import com.designerpunk.tokens.focusBlend
71
- import com.designerpunk.tokens.disabledBlend
72
72
 
73
73
  /**
74
74
  * Input type enumeration
@@ -99,13 +99,17 @@ enum class InputType {
99
99
  * @param isSuccess Success state indicator
100
100
  * @param showInfoIcon Info icon support
101
101
  * @param type Input type
102
+ * @param visualTransformation Override for the input's visual transformation.
103
+ * When null, derived from type (PasswordVisualTransformation for PASSWORD).
104
+ * Semantic variants use this to control masking (e.g., password visibility toggle).
102
105
  * @param placeholder Placeholder text (only shown when label is floated and input is empty)
103
106
  * @param readOnly Read-only state
104
107
  * @param required Required field indicator
105
108
  * @param maxLength Maximum length for input value
106
109
  * @param imeAction IME action for keyboard
107
110
  * @param keyboardActions Keyboard actions
108
- * @param isDisabled Disabled state
111
+ * @param trailingContent Trailing content slot rendered after the status icons
112
+ * (e.g., password visibility toggle)
109
113
  */
110
114
  @Composable
111
115
  fun InputTextBase(
@@ -121,13 +125,14 @@ fun InputTextBase(
121
125
  isSuccess: Boolean = false,
122
126
  showInfoIcon: Boolean = false,
123
127
  type: InputType = InputType.TEXT,
128
+ visualTransformation: VisualTransformation? = null,
124
129
  placeholder: String? = null,
125
130
  readOnly: Boolean = false,
126
131
  required: Boolean = false,
127
132
  maxLength: Int? = null,
128
133
  imeAction: ImeAction = ImeAction.Done,
129
134
  keyboardActions: KeyboardActions = KeyboardActions.Default,
130
- isDisabled: Boolean = false
135
+ trailingContent: (@Composable () -> Unit)? = null
131
136
  ) {
132
137
  // State
133
138
  var isFocused by remember { mutableStateOf(false) }
@@ -192,7 +197,6 @@ fun InputTextBase(
192
197
  targetValue = when {
193
198
  hasError -> colorFeedbackErrorText
194
199
  isSuccess -> colorFeedbackSuccessText
195
- isDisabled -> colorActionPrimary.disabledBlend()
196
200
  isFocused -> colorActionPrimary.focusBlend()
197
201
  else -> colorTextMuted
198
202
  },
@@ -204,7 +208,6 @@ fun InputTextBase(
204
208
  targetValue = when {
205
209
  hasError -> colorFeedbackErrorText
206
210
  isSuccess -> colorFeedbackSuccessText
207
- isDisabled -> colorActionPrimary.disabledBlend()
208
211
  isFocused -> colorActionPrimary.focusBlend()
209
212
  else -> colorBorder
210
213
  },
@@ -230,6 +233,14 @@ fun InputTextBase(
230
233
  if (errorMessage != null) {
231
234
  error(errorMessage)
232
235
  }
236
+ if (readOnly) {
237
+ // Explicit TalkBack read-only announcement — label, value,
238
+ // and "read only", never a disabled/dimmed state
239
+ // (state_readonly contract, announcement-parity condition 3:
240
+ // .kiro/issues/input-text-base-ios-readonly-adjudication.md,
241
+ // RULED B-prime, Peter 2026-07-15).
242
+ stateDescription = "read only"
243
+ }
233
244
  }
234
245
  ) {
235
246
  // Input wrapper with label and trailing icon
@@ -278,7 +289,7 @@ fun InputTextBase(
278
289
  shape = RoundedCornerShape(radius150)
279
290
  )
280
291
  .then(
281
- if (isFocused && !isDisabled) {
292
+ if (isFocused) {
282
293
  Modifier.border(
283
294
  width = accessibilityFocusWidth,
284
295
  color = accessibilityFocusColor,
@@ -295,7 +306,7 @@ fun InputTextBase(
295
306
  lineHeight = typographyInputLineHeight.sp,
296
307
  fontWeight = FontWeight(typographyInputFontWeight),
297
308
  letterSpacing = typographyInputLetterSpacing.sp,
298
- color = if (isDisabled) colorTextMuted else colorTextDefault
309
+ color = colorTextDefault
299
310
  ),
300
311
  keyboardOptions = KeyboardOptions(
301
312
  keyboardType = getKeyboardType(type),
@@ -303,8 +314,8 @@ fun InputTextBase(
303
314
  ),
304
315
  keyboardActions = keyboardActions,
305
316
  singleLine = true,
306
- readOnly = readOnly || isDisabled,
307
- visualTransformation = if (type == InputType.PASSWORD) {
317
+ readOnly = readOnly,
318
+ visualTransformation = visualTransformation ?: if (type == InputType.PASSWORD) {
308
319
  PasswordVisualTransformation()
309
320
  } else {
310
321
  VisualTransformation.None
@@ -373,6 +384,9 @@ fun InputTextBase(
373
384
  )
374
385
  }
375
386
  }
387
+
388
+ // Trailing content slot (e.g., password visibility toggle)
389
+ trailingContent?.invoke()
376
390
  }
377
391
 
378
392
  // Helper text (persistent)
@@ -13,21 +13,33 @@
13
13
  * - Color animation (text.subtle → primary with blend.focusSaturate)
14
14
  * - Offset animation (translateY)
15
15
  * - Trailing icon support (error, success, info)
16
+ * - Trailing content slot for semantic variants (e.g., password toggle)
17
+ * - Read-only rendering (B-prime): selectable Text inside the shared field
18
+ * chrome — never disabled semantics
16
19
  * - Respects accessibilityReduceMotion
17
20
  * - WCAG 2.1 AA compliant
18
- * - Uses theme-aware blend utilities for state colors (focus, disabled)
19
- *
21
+ * - Uses theme-aware blend utilities for state colors (focus)
22
+ *
20
23
  * Behavioral Contracts:
21
- * - focusable: Can receive keyboard focus
22
- * - float_label_animation: Label animates on focus
23
- * - validates_on_blur: Validation triggers on blur
24
- * - error_state_display: Shows error message and styling
25
- * - success_state_display: Shows success styling
26
- * - disabled_state: Prevents interaction when disabled
27
- * - trailing_icon_display: Shows contextual trailing icons
28
- * - focus_ring: WCAG 2.4.7 focus visible indicator
29
- * - reduced_motion_support: Respects prefers-reduced-motion
30
- *
24
+ * - interaction_focusable: Can receive keyboard focus (iOS readOnly carve-out
25
+ * declared in contracts.yaml — readOnly content is not in FKA focus order)
26
+ * - content_float_label: Label animates on focus
27
+ * - validation_on_blur: Validation triggers on blur
28
+ * - state_error: Shows error message and styling
29
+ * - state_success: Shows success styling
30
+ * - state_readonly: Read-only display — selectable/copyable, non-editable,
31
+ * announced read-only, never a disabled/dimmed trait (concept ballot
32
+ * pending ratification; see contracts.yaml)
33
+ * - content_trailing_icon: Shows contextual trailing icons
34
+ * - interaction_focus_ring: WCAG 2.4.7 focus visible indicator
35
+ * - accessibility_reduced_motion: Respects prefers-reduced-motion
36
+ *
37
+ * Read-only (B-prime ruling — Peter, 2026-07-15,
38
+ * .kiro/issues/input-text-base-ios-readonly-adjudication.md):
39
+ * readOnly renders as `Text(value).textSelection(.enabled)` inside the same
40
+ * field chrome as the editable field. The SwiftUI disabled modifier is never
41
+ * applied. onFocus/onBlur are DECLARED not-fired on the iOS readOnly path.
42
+ *
31
43
  * Requirements: 1.1, 1.2, 1.3, 1.5, 4.1, 4.2, 4.3, 8.1, 8.2, 8.4, 11.1, 11.2, 11.3, 13.1
32
44
  */
33
45
 
@@ -81,7 +93,9 @@ struct InputTextBase: View {
81
93
  /// Placeholder text (only shown when label is floated and input is empty)
82
94
  let placeholder: String?
83
95
 
84
- /// Read-only state
96
+ /// Read-only state (state_readonly contract): renders the value as
97
+ /// selectable Text inside the field chrome — never disabled semantics.
98
+ /// Ignored for secure fields (see isReadOnlyDisplay).
85
99
  let readOnly: Bool
86
100
 
87
101
  /// Required field indicator
@@ -89,10 +103,12 @@ struct InputTextBase: View {
89
103
 
90
104
  /// Maximum length for input value
91
105
  let maxLength: Int?
92
-
93
- /// Disabled state
94
- let isDisabled: Bool
95
-
106
+
107
+ /// Trailing content slot rendered after the status icons
108
+ /// (e.g., password visibility toggle). Declared as `var` with a
109
+ /// default so the memberwise initializer keeps it optional and last.
110
+ var trailingContent: AnyView? = nil
111
+
96
112
  // MARK: - State
97
113
 
98
114
  /// Whether input currently has focus
@@ -112,6 +128,22 @@ struct InputTextBase: View {
112
128
  private var isFilled: Bool {
113
129
  !value.isEmpty
114
130
  }
131
+
132
+ /// Whether to render the read-only display path (B-prime).
133
+ ///
134
+ /// SECURITY-CRITICAL GATE: the read-only path renders the value as
135
+ /// selectable plaintext `Text`. It MUST be unreachable for secure fields
136
+ /// (`type == .password`), or a readOnly password would render its secret
137
+ /// in plaintext. readOnly is contracted OUT of Input-Text-Password
138
+ /// entirely (adjudication condition 4:
139
+ /// .kiro/issues/input-text-base-ios-readonly-adjudication.md — RULED
140
+ /// B-prime, Peter 2026-07-15); the Password variant additionally strips
141
+ /// readOnly before it reaches this component. On a secure field, readOnly
142
+ /// is ignored (the field stays an editable SecureField) rather than ever
143
+ /// routing to the plaintext Text branch.
144
+ private var isReadOnlyDisplay: Bool {
145
+ readOnly && type != .password
146
+ }
115
147
 
116
148
  /// Whether input has error
117
149
  private var hasError: Bool {
@@ -140,8 +172,6 @@ struct InputTextBase: View {
140
172
  return theme.colorFeedbackErrorText
141
173
  } else if isSuccess {
142
174
  return theme.colorFeedbackSuccessText
143
- } else if isDisabled {
144
- return theme.colorActionPrimary.disabledBlend()
145
175
  } else if isFocused {
146
176
  return theme.colorActionPrimary.focusBlend()
147
177
  } else {
@@ -164,8 +194,6 @@ struct InputTextBase: View {
164
194
  return theme.colorFeedbackErrorText
165
195
  } else if isSuccess {
166
196
  return theme.colorFeedbackSuccessText
167
- } else if isDisabled {
168
- return theme.colorActionPrimary.disabledBlend()
169
197
  } else if isFocused {
170
198
  return theme.colorActionPrimary.focusBlend()
171
199
  } else {
@@ -213,18 +241,45 @@ struct InputTextBase: View {
213
241
  }
214
242
 
215
243
  // Input field
216
- if type == .password {
244
+ if isReadOnlyDisplay {
245
+ // Read-only rendering (B-prime): selectable Text inside
246
+ // the shared field chrome. Never the SwiftUI disabled
247
+ // modifier — it produces disabled semantics (unfocusable,
248
+ // uncopyable, VoiceOver "dimmed"), which DesignerPunk bans.
249
+ // Long-press → select/copy (whole-string copy is the
250
+ // contracted iOS behavior). No keyboard is raised.
251
+ //
252
+ // Declared behavior (state_readonly contract):
253
+ // - NOT in keyboard/FKA focus order (mitigable declared
254
+ // exception — .focusable(interactions:) prototype
255
+ // tracked; contract tightens if it verifies on-device)
256
+ // - onFocus/onBlur DECLARED not-fired on this path
257
+ // (there is no focus system here — nothing simulates
258
+ // or pretends otherwise)
259
+ // - Empty value: label rests in placeholder position
260
+ // (cross-platform parity); the chrome reserves full
261
+ // height unconditionally.
262
+ Text(value)
263
+ .textSelection(.enabled)
264
+ .modifier(InputTextBaseFieldChrome(
265
+ borderColor: borderColor,
266
+ isFocused: false,
267
+ hasTrailingIcon: showErrorIcon || showSuccessIcon || showInfoIconVisible || trailingContent != nil
268
+ ))
269
+ } else if type == .password {
270
+ // Secure path: readOnly is IGNORED here (see
271
+ // isReadOnlyDisplay) — a secure field never routes to
272
+ // the plaintext read-only Text branch, and disabled
273
+ // semantics are never applied.
217
274
  SecureField(isLabelFloated && placeholder != nil ? placeholder! : "", text: $value)
218
275
  .textFieldStyle(InputTextBaseFieldStyle(
219
276
  borderColor: borderColor,
220
277
  isFocused: isFocused,
221
278
  hasError: hasError,
222
279
  isSuccess: isSuccess,
223
- isDisabled: isDisabled,
224
- hasTrailingIcon: showErrorIcon || showSuccessIcon || showInfoIconVisible
280
+ hasTrailingIcon: showErrorIcon || showSuccessIcon || showInfoIconVisible || trailingContent != nil
225
281
  ))
226
282
  .focused($isFocused)
227
- .disabled(readOnly || isDisabled)
228
283
  .textContentType(autocomplete)
229
284
  .onChange(of: value) { newValue in
230
285
  if let maxLength = maxLength, newValue.count > maxLength {
@@ -246,11 +301,9 @@ struct InputTextBase: View {
246
301
  isFocused: isFocused,
247
302
  hasError: hasError,
248
303
  isSuccess: isSuccess,
249
- isDisabled: isDisabled,
250
- hasTrailingIcon: showErrorIcon || showSuccessIcon || showInfoIconVisible
304
+ hasTrailingIcon: showErrorIcon || showSuccessIcon || showInfoIconVisible || trailingContent != nil
251
305
  ))
252
306
  .focused($isFocused)
253
- .disabled(readOnly || isDisabled)
254
307
  .textContentType(autocomplete)
255
308
  .keyboardType(keyboardTypeForInputType(type))
256
309
  .onChange(of: value) { newValue in
@@ -295,6 +348,12 @@ struct InputTextBase: View {
295
348
  value: showInfoIconVisible
296
349
  )
297
350
  }
351
+
352
+ // Trailing content slot (e.g., password visibility toggle)
353
+ if let trailingContent = trailingContent {
354
+ trailingContent
355
+ .padding(.trailing, DesignTokens.spaceInset100)
356
+ }
298
357
  }
299
358
  .frame(minHeight: DesignTokens.tapAreaRecommended)
300
359
 
@@ -319,7 +378,25 @@ struct InputTextBase: View {
319
378
  .accessibilityElement(children: .contain)
320
379
  .accessibilityLabel(label)
321
380
  .accessibilityValue(value)
322
- .accessibilityHint(helperText ?? "")
381
+ .accessibilityHint(accessibilityHintText)
382
+ }
383
+
384
+ /// Composed accessibility hint.
385
+ ///
386
+ /// On the read-only path this composes the read-only indication with any
387
+ /// helper text, so VoiceOver announces label + value + "Read only"
388
+ /// (state_readonly contract) — never a disabled/dimmed trait. iOS has no
389
+ /// read-only accessibility trait; the hint is the correct announcement
390
+ /// route (adjudication, Kenya R1).
391
+ private var accessibilityHintText: String {
392
+ var parts: [String] = []
393
+ if isReadOnlyDisplay {
394
+ parts.append("Read only")
395
+ }
396
+ if let helperText = helperText, !helperText.isEmpty {
397
+ parts.append(helperText)
398
+ }
399
+ return parts.joined(separator: ". ")
323
400
  }
324
401
 
325
402
  // MARK: - Helper Methods
@@ -353,30 +430,48 @@ enum InputType {
353
430
  case url
354
431
  }
355
432
 
356
- // MARK: - Custom Text Field Style
433
+ // MARK: - Shared Field Chrome
357
434
 
358
435
  /**
359
- * Custom text field style for consistent appearance
436
+ * Shared field chrome (B-prime chrome extraction, adjudication 2026-07-15).
437
+ *
438
+ * The visual treatment of the field box — typography, text color, insets,
439
+ * canvas background, border overlay, and focus-ring overlay — extracted from
440
+ * InputTextBaseFieldStyle so it applies to BOTH rendering paths: the editable
441
+ * TextField/SecureField (via InputTextBaseFieldStyle, which delegates here)
442
+ * and the read-only Text (applied directly). One chrome, two contents — the
443
+ * two paths cannot drift visually.
360
444
  */
361
- struct InputTextBaseFieldStyle: TextFieldStyle {
445
+ struct InputTextBaseFieldChrome: ViewModifier {
362
446
  let borderColor: Color
363
447
  let isFocused: Bool
364
- let hasError: Bool
365
- let isSuccess: Bool
366
- let isDisabled: Bool
367
448
  let hasTrailingIcon: Bool
368
-
449
+
369
450
  @Environment(\.accessibilityReduceMotion) var reduceMotion
370
451
  @Environment(\.dpTheme) private var theme
371
-
372
- func _body(configuration: TextField<Self._Label>) -> some View {
373
- configuration
452
+
453
+ /// Minimum content height, reserved UNCONDITIONALLY on both paths:
454
+ /// input line height (fontSize × unitless lineHeight multiplier, the
455
+ /// Badge/Nav token-derived pattern) + vertical insets. This deliberately
456
+ /// does NOT reason about empty `Text` intrinsic size (undocumented, has
457
+ /// varied across SwiftUI releases): an empty readOnly value keeps the
458
+ /// same field height as a filled editable field. Note: the field uses a
459
+ /// fixed token font size (no Dynamic Type) today — if the family adopts
460
+ /// Dynamic Type, this minHeight must scale with it.
461
+ private var minFieldHeight: CGFloat {
462
+ (DesignTokens.typographyInput.fontSize * DesignTokens.typographyInput.lineHeight)
463
+ + (DesignTokens.spaceInset100 * 2)
464
+ }
465
+
466
+ func body(content: Content) -> some View {
467
+ content
374
468
  .font(Font.system(size: DesignTokens.typographyInput.fontSize)
375
469
  .weight(DesignTokens.typographyInput.fontWeight))
376
- .foregroundColor(isDisabled ? theme.colorTextMuted : theme.colorTextDefault)
470
+ .foregroundColor(theme.colorTextDefault)
377
471
  .padding(.leading, DesignTokens.spaceInset100)
378
472
  .padding(.vertical, DesignTokens.spaceInset100)
379
473
  .padding(.trailing, hasTrailingIcon ? 0 : DesignTokens.spaceInset100)
474
+ .frame(maxWidth: .infinity, minHeight: minFieldHeight, alignment: .leading)
380
475
  .background(theme.colorStructureCanvas)
381
476
  .overlay(
382
477
  RoundedRectangle(cornerRadius: DesignTokens.radius150)
@@ -386,7 +481,7 @@ struct InputTextBaseFieldStyle: TextFieldStyle {
386
481
  RoundedRectangle(cornerRadius: DesignTokens.radius150)
387
482
  .stroke(theme.colorActionPrimary, lineWidth: DesignTokens.accessibilityFocusWidth)
388
483
  .padding(-DesignTokens.accessibilityFocusOffset)
389
- .opacity(isFocused && !isDisabled ? 1 : 0)
484
+ .opacity(isFocused ? 1 : 0)
390
485
  .animation(
391
486
  reduceMotion ? .none : Animation.timingCurve(0.4, 0.0, 0.2, 1.0, duration: DesignTokens.MotionFocusTransition.duration),
392
487
  value: isFocused
@@ -395,6 +490,30 @@ struct InputTextBaseFieldStyle: TextFieldStyle {
395
490
  }
396
491
  }
397
492
 
493
+ // MARK: - Custom Text Field Style
494
+
495
+ /**
496
+ * Custom text field style for consistent appearance.
497
+ * Delegates all visual treatment to InputTextBaseFieldChrome (shared with
498
+ * the read-only rendering path).
499
+ */
500
+ struct InputTextBaseFieldStyle: TextFieldStyle {
501
+ let borderColor: Color
502
+ let isFocused: Bool
503
+ let hasError: Bool
504
+ let isSuccess: Bool
505
+ let hasTrailingIcon: Bool
506
+
507
+ func _body(configuration: TextField<Self._Label>) -> some View {
508
+ configuration
509
+ .modifier(InputTextBaseFieldChrome(
510
+ borderColor: borderColor,
511
+ isFocused: isFocused,
512
+ hasTrailingIcon: hasTrailingIcon
513
+ ))
514
+ }
515
+ }
516
+
398
517
  // MARK: - Preview
399
518
 
400
519
  #if DEBUG
@@ -417,8 +536,7 @@ struct InputTextBase_Previews: PreviewProvider {
417
536
  placeholder: nil,
418
537
  readOnly: false,
419
538
  required: false,
420
- maxLength: nil,
421
- isDisabled: false
539
+ maxLength: nil
422
540
  )
423
541
 
424
542
  InputTextBase(
@@ -437,8 +555,7 @@ struct InputTextBase_Previews: PreviewProvider {
437
555
  placeholder: nil,
438
556
  readOnly: false,
439
557
  required: false,
440
- maxLength: nil,
441
- isDisabled: false
558
+ maxLength: nil
442
559
  )
443
560
 
444
561
  InputTextBase(
@@ -457,8 +574,7 @@ struct InputTextBase_Previews: PreviewProvider {
457
574
  placeholder: nil,
458
575
  readOnly: false,
459
576
  required: true,
460
- maxLength: nil,
461
- isDisabled: false
577
+ maxLength: nil
462
578
  )
463
579
 
464
580
  InputTextBase(
@@ -477,28 +593,48 @@ struct InputTextBase_Previews: PreviewProvider {
477
593
  placeholder: nil,
478
594
  readOnly: false,
479
595
  required: false,
480
- maxLength: nil,
481
- isDisabled: false
596
+ maxLength: nil
482
597
  )
483
-
598
+
599
+ // Read-only (B-prime): selectable Text in the shared chrome
484
600
  InputTextBase(
485
- id: "preview-disabled",
486
- label: "Email",
487
- value: .constant("user@example.com"),
601
+ id: "preview-read-only",
602
+ label: "Reference number",
603
+ value: .constant("REF-2026-07-15"),
488
604
  onChange: nil,
489
605
  onFocus: nil,
490
606
  onBlur: nil,
491
- helperText: "This field is disabled",
607
+ helperText: "Long-press to copy",
492
608
  errorMessage: nil,
493
609
  isSuccess: false,
494
610
  showInfoIcon: false,
495
- type: .email,
496
- autocomplete: .emailAddress,
611
+ type: .text,
612
+ autocomplete: nil,
497
613
  placeholder: nil,
498
- readOnly: false,
614
+ readOnly: true,
615
+ required: false,
616
+ maxLength: nil
617
+ )
618
+
619
+ // Read-only, empty: label rests in placeholder position,
620
+ // chrome reserves full field height
621
+ InputTextBase(
622
+ id: "preview-read-only-empty",
623
+ label: "Reference number",
624
+ value: .constant(""),
625
+ onChange: nil,
626
+ onFocus: nil,
627
+ onBlur: nil,
628
+ helperText: nil,
629
+ errorMessage: nil,
630
+ isSuccess: false,
631
+ showInfoIcon: false,
632
+ type: .text,
633
+ autocomplete: nil,
634
+ placeholder: nil,
635
+ readOnly: true,
499
636
  required: false,
500
- maxLength: nil,
501
- isDisabled: true
637
+ maxLength: nil
502
638
  )
503
639
  }
504
640
  .padding()
@@ -54,7 +54,19 @@ export interface InputTextBaseProps {
54
54
  /** Placeholder text (only shown when label is floated and input is empty) */
55
55
  placeholder?: string;
56
56
 
57
- /** Read-only state (alternative to disabled) */
57
+ /**
58
+ * Read-only state (state_readonly contract): the value is displayed
59
+ * non-editably while remaining perceivable, selectable, and copyable —
60
+ * never rendered with disabled semantics on any platform.
61
+ *
62
+ * Platform note: web and Android readOnly fields participate in keyboard
63
+ * focus order; iOS renders readOnly as selectable static text in the
64
+ * field chrome and is not in focus order (declared carve-out — see
65
+ * contracts.yaml interaction_focusable).
66
+ *
67
+ * NOT supported by Input-Text-Password: readOnly is contracted out of the
68
+ * Password variant (see its contracts.yaml excludes) and is ignored there.
69
+ */
58
70
  readOnly?: boolean;
59
71
 
60
72
  /** Required field indicator */
@@ -101,7 +101,11 @@ properties:
101
101
  type: boolean
102
102
  required: false
103
103
  default: false
104
- description: Read-only state (alternative to disabled)
104
+ description: >-
105
+ Read-only display state (state_readonly contract) — the value is
106
+ non-editable but stays perceivable, selectable, and copyable; no
107
+ keyboard/IME is raised and disabled semantics are never applied. iOS
108
+ renders it as selectable static text inside the field chrome.
105
109
 
106
110
  required:
107
111
  type: boolean
@@ -97,7 +97,7 @@ InputTextEmail(
97
97
  | `isSuccess` | boolean | No | false | Success state indicator |
98
98
  | `showInfoIcon` | boolean | No | false | Show info icon |
99
99
  | `placeholder` | string | No | - | Placeholder text |
100
- | `readOnly` | boolean | No | false | Read-only state |
100
+ | `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 |
101
101
  | `required` | boolean | No | false | Required field indicator |
102
102
  | `maxLength` | number | No | - | Maximum input length |
103
103
  | `testID` | string | No | - | Test ID for automation |
@@ -126,15 +126,16 @@ InputTextEmail(
126
126
  3. **validates_on_blur**: Validation triggers on blur
127
127
  4. **error_state_display**: Shows error message and styling
128
128
  5. **success_state_display**: Shows success styling
129
- 6. **disabled_state**: Prevents interaction when disabled
130
- 7. **trailing_icon_display**: Shows contextual trailing icons
131
- 8. **focus_ring**: WCAG 2.4.7 focus visible indicator
132
- 9. **reduced_motion_support**: Respects prefers-reduced-motion
129
+ 6. **trailing_icon_display**: Shows contextual trailing icons
130
+ 7. **focus_ring**: WCAG 2.4.7 focus visible indicator
131
+ 8. **reduced_motion_support**: Respects prefers-reduced-motion
132
+
133
+ **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).
133
134
 
134
135
  ### Email-Specific Contracts
135
136
 
136
- 10. **validates_email_format**: Validates email against RFC 5322 pattern
137
- 11. **provides_email_autocomplete**: Enables browser/platform email autofill
137
+ 9. **validates_email_format**: Validates email against RFC 5322 pattern
138
+ 10. **provides_email_autocomplete**: Enables browser/platform email autofill
138
139
 
139
140
  ## Validation
140
141
 
@@ -78,7 +78,6 @@ private fun isValidEmail(email: String): Boolean {
78
78
  * @param maxLength Maximum length for input value
79
79
  * @param imeAction IME action for keyboard
80
80
  * @param keyboardActions Keyboard actions
81
- * @param isDisabled Disabled state
82
81
  * @param invalidEmailMessage Custom invalid email message
83
82
  * @param customValidator Custom email validator function
84
83
  */
@@ -102,7 +101,6 @@ fun InputTextEmail(
102
101
  maxLength: Int? = null,
103
102
  imeAction: ImeAction = ImeAction.Done,
104
103
  keyboardActions: KeyboardActions = KeyboardActions.Default,
105
- isDisabled: Boolean = false,
106
104
  invalidEmailMessage: String = DEFAULT_INVALID_EMAIL_MESSAGE,
107
105
  customValidator: ((String) -> Boolean)? = null
108
106
  ) {
@@ -158,8 +156,7 @@ fun InputTextEmail(
158
156
  required = required,
159
157
  maxLength = maxLength,
160
158
  imeAction = imeAction,
161
- keyboardActions = keyboardActions,
162
- isDisabled = isDisabled
159
+ keyboardActions = keyboardActions
163
160
  )
164
161
  }
165
162