@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
@@ -212,14 +212,20 @@ describe('Avatar Component Accessibility', () => {
212
212
  });
213
213
 
214
214
  it('should apply empty alt when alt is empty string', async () => {
215
+ // Spec 126 (O2): uses the raw-attribute form (setAttribute) to produce a
216
+ // genuine alt="" — the `alt` *property* setter treats '' as falsy and calls
217
+ // removeAttribute, collapsing `avatar.alt = ''` to the same "absent" state as
218
+ // omitting alt entirely (see Avatar.web.ts `set alt`). Only the attribute form
219
+ // yields a real empty-string alt, and under the settled warn condition
220
+ // (`alt == null`), a real empty-string alt does not warn — so no spy is needed.
215
221
  const avatar = document.createElement('avatar-base') as AvatarBaseElement;
216
222
  avatar.type = 'human';
217
223
  avatar.src = 'https://example.com/profile.jpg';
218
- avatar.alt = '';
224
+ avatar.setAttribute('alt', '');
219
225
  document.body.appendChild(avatar);
220
-
226
+
221
227
  await new Promise(resolve => setTimeout(resolve, 0));
222
-
228
+
223
229
  const img = avatar.shadowRoot?.querySelector('.avatar__image');
224
230
  expect(img).toBeTruthy();
225
231
  expect(img?.getAttribute('alt')).toBe('');
@@ -285,16 +291,124 @@ describe('Avatar Component Accessibility', () => {
285
291
 
286
292
  it('should NOT warn when no src is provided', async () => {
287
293
  const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
288
-
294
+
289
295
  const avatar = document.createElement('avatar-base') as AvatarBaseElement;
290
296
  avatar.type = 'human';
291
297
  // No src, no alt
292
298
  document.body.appendChild(avatar);
293
-
299
+
294
300
  await new Promise(resolve => setTimeout(resolve, 0));
295
-
301
+
296
302
  expect(consoleSpy).not.toHaveBeenCalled();
297
-
303
+
304
+ consoleSpy.mockRestore();
305
+ });
306
+ });
307
+
308
+ // ============================================================================
309
+ // Alt Text Warning — Decorative & Type-Scope Edge Semantics (Spec 126, O2)
310
+ // ============================================================================
311
+ //
312
+ // Settled warn condition: `type === 'human' && src && alt == null && !decorative`
313
+ // @see Requirements: 5.4 - Human type + src SHALL require alt for accessibility
314
+ // @see Requirements: 5 AC 5 - Agent type SHALL ignore src entirely
315
+ // @see .kiro/specs/126-avatar-decorative-warn/design-outline.md § 4/5
316
+ //
317
+ // Together with the sentinel test above ('should warn when src is provided
318
+ // without alt'), this block forms the six-test matrix settled by Spec 126 § 5
319
+ // item 2. The sentinel is left unchanged (Spec 126 § 7 non-regression signal).
320
+ describe('Alt Text Warning — Decorative & Type-Scope Edge Semantics (Spec 126)', () => {
321
+ it('should NOT warn when alt="" is set via raw attribute (explicit empty, not "absent")', async () => {
322
+ const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
323
+
324
+ const avatar = document.createElement('avatar-base') as AvatarBaseElement;
325
+ avatar.type = 'human';
326
+ avatar.src = 'https://example.com/profile.jpg';
327
+ // Raw attribute form is the only path that yields a genuine alt="" (getter
328
+ // returns '', not null) — the `alt` property setter collapses falsy values to
329
+ // removeAttribute, so this test must use setAttribute to exercise the real case.
330
+ avatar.setAttribute('alt', '');
331
+ document.body.appendChild(avatar);
332
+
333
+ await new Promise(resolve => setTimeout(resolve, 0));
334
+
335
+ expect(consoleSpy).not.toHaveBeenCalled();
336
+
337
+ consoleSpy.mockRestore();
338
+ });
339
+
340
+ it('should warn when alt is set to empty string via the property (collapses to null via the setter)', async () => {
341
+ const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
342
+
343
+ const avatar = document.createElement('avatar-base') as AvatarBaseElement;
344
+ avatar.type = 'human';
345
+ avatar.src = 'https://example.com/profile.jpg';
346
+ // Property-form empty string: the `alt` setter treats '' as falsy and calls
347
+ // removeAttribute('alt'), so this lands in the "alt absent" bucket and warns —
348
+ // pins the attribute-vs-property collapse explicitly (Spec 126 § 5 item 2).
349
+ avatar.alt = '';
350
+ document.body.appendChild(avatar);
351
+
352
+ await new Promise(resolve => setTimeout(resolve, 0));
353
+
354
+ expect(consoleSpy).toHaveBeenCalledWith(
355
+ expect.stringContaining('alt')
356
+ );
357
+
358
+ consoleSpy.mockRestore();
359
+ });
360
+
361
+ it('should NOT warn when decorative is true, even without alt (co-asserted with aria-hidden)', async () => {
362
+ const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
363
+
364
+ const avatar = document.createElement('avatar-base') as AvatarBaseElement;
365
+ avatar.type = 'human';
366
+ avatar.src = 'https://example.com/profile.jpg';
367
+ avatar.decorative = true;
368
+ // No alt provided
369
+ document.body.appendChild(avatar);
370
+
371
+ await new Promise(resolve => setTimeout(resolve, 0));
372
+
373
+ expect(consoleSpy).not.toHaveBeenCalled();
374
+
375
+ // Co-assert the semantic linkage (Req 9.2): the warn is correctly suppressed
376
+ // because the avatar is genuinely hidden from screen readers, not incidentally.
377
+ const container = avatar.shadowRoot?.querySelector('.avatar');
378
+ expect(container?.getAttribute('aria-hidden')).toBe('true');
379
+
380
+ consoleSpy.mockRestore();
381
+ });
382
+
383
+ it('should NOT warn for agent type with src and no alt (Req 5 AC 5 — agent ignores src entirely)', async () => {
384
+ const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
385
+
386
+ const avatar = document.createElement('avatar-base') as AvatarBaseElement;
387
+ avatar.type = 'agent';
388
+ avatar.src = 'https://example.com/profile.jpg';
389
+ // No alt provided
390
+ document.body.appendChild(avatar);
391
+
392
+ await new Promise(resolve => setTimeout(resolve, 0));
393
+
394
+ expect(consoleSpy).not.toHaveBeenCalled();
395
+
396
+ consoleSpy.mockRestore();
397
+ });
398
+
399
+ it('should NOT warn for agent type with src and alt both present (confirms silencing is type-driven, not alt-driven)', async () => {
400
+ const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
401
+
402
+ const avatar = document.createElement('avatar-base') as AvatarBaseElement;
403
+ avatar.type = 'agent';
404
+ avatar.src = 'https://example.com/profile.jpg';
405
+ avatar.alt = 'Ignored for agent type';
406
+ document.body.appendChild(avatar);
407
+
408
+ await new Promise(resolve => setTimeout(resolve, 0));
409
+
410
+ expect(consoleSpy).not.toHaveBeenCalled();
411
+
298
412
  consoleSpy.mockRestore();
299
413
  });
300
414
  });
@@ -287,6 +287,7 @@ describe('Avatar Component Image Handling', () => {
287
287
  const avatar = document.createElement('avatar-base') as AvatarBaseElement;
288
288
  avatar.type = 'agent';
289
289
  avatar.src = 'https://example.com/agent-image.jpg';
290
+ avatar.alt = 'Agent profile'; // honor the alt-with-src contract (Req 5.4)
290
291
  document.body.appendChild(avatar);
291
292
 
292
293
  await new Promise(resolve => setTimeout(resolve, 0));
@@ -299,6 +300,7 @@ describe('Avatar Component Image Handling', () => {
299
300
  const avatar = document.createElement('avatar-base') as AvatarBaseElement;
300
301
  avatar.type = 'agent';
301
302
  avatar.src = 'https://example.com/agent-image.jpg';
303
+ avatar.alt = 'Agent profile'; // honor the alt-with-src contract (Req 5.4)
302
304
  document.body.appendChild(avatar);
303
305
 
304
306
  await new Promise(resolve => setTimeout(resolve, 0));
@@ -311,6 +313,7 @@ describe('Avatar Component Image Handling', () => {
311
313
  const avatar = document.createElement('avatar-base') as AvatarBaseElement;
312
314
  avatar.type = 'agent';
313
315
  avatar.src = 'https://example.com/agent-image.jpg';
316
+ avatar.alt = 'Agent profile'; // honor the alt-with-src contract (Req 5.4)
314
317
  document.body.appendChild(avatar);
315
318
 
316
319
  await new Promise(resolve => setTimeout(resolve, 0));
@@ -218,27 +218,33 @@ describe('Avatar Component Core API', () => {
218
218
  // ============================================================================
219
219
 
220
220
  describe('Src Prop', () => {
221
+ // Fixtures set `alt` alongside `src` so they honor the component's own a11y
222
+ // contract (Req 5.4: alt is required with src) — otherwise the component
223
+ // rightly warns, polluting CI logs. The assertions below are about src only.
221
224
  it('should accept src value', () => {
222
225
  const avatar = document.createElement('avatar-base') as AvatarBaseElement;
223
226
  avatar.src = 'https://example.com/image.jpg';
227
+ avatar.alt = 'Test avatar';
224
228
  document.body.appendChild(avatar);
225
-
229
+
226
230
  expect(avatar.src).toBe('https://example.com/image.jpg');
227
231
  expect(avatar.getAttribute('src')).toBe('https://example.com/image.jpg');
228
232
  });
229
233
 
230
234
  it('should update src via attribute', () => {
231
235
  const avatar = document.createElement('avatar-base') as AvatarBaseElement;
236
+ avatar.alt = 'Test avatar';
232
237
  document.body.appendChild(avatar);
233
-
238
+
234
239
  avatar.setAttribute('src', 'https://example.com/new-image.jpg');
235
240
  expect(avatar.src).toBe('https://example.com/new-image.jpg');
236
241
  });
237
242
 
238
243
  it('should update src via property', () => {
239
244
  const avatar = document.createElement('avatar-base') as AvatarBaseElement;
245
+ avatar.alt = 'Test avatar';
240
246
  document.body.appendChild(avatar);
241
-
247
+
242
248
  avatar.src = 'https://example.com/profile.png';
243
249
  expect(avatar.getAttribute('src')).toBe('https://example.com/profile.png');
244
250
  });
@@ -246,8 +252,9 @@ describe('Avatar Component Core API', () => {
246
252
  it('should remove src attribute when set to null', () => {
247
253
  const avatar = document.createElement('avatar-base') as AvatarBaseElement;
248
254
  avatar.src = 'https://example.com/image.jpg';
255
+ avatar.alt = 'Test avatar';
249
256
  document.body.appendChild(avatar);
250
-
257
+
251
258
  avatar.src = null;
252
259
  expect(avatar.src).toBeNull();
253
260
  expect(avatar.hasAttribute('src')).toBe(false);
@@ -566,8 +573,9 @@ describe('Avatar Component Core API', () => {
566
573
 
567
574
  it('should reflect src property to attribute', () => {
568
575
  const avatar = document.createElement('avatar-base') as AvatarBaseElement;
576
+ avatar.alt = 'Test avatar'; // honor the alt-with-src contract (Req 5.4)
569
577
  document.body.appendChild(avatar);
570
-
578
+
571
579
  avatar.src = 'https://example.com/test.jpg';
572
580
  expect(avatar.getAttribute('src')).toBe('https://example.com/test.jpg');
573
581
  });
@@ -629,8 +637,9 @@ describe('Avatar Component Core API', () => {
629
637
  it('should read src attribute as property', () => {
630
638
  const avatar = document.createElement('avatar-base') as AvatarBaseElement;
631
639
  avatar.setAttribute('src', 'https://example.com/img.png');
640
+ avatar.setAttribute('alt', 'Test avatar'); // honor the alt-with-src contract (Req 5.4)
632
641
  document.body.appendChild(avatar);
633
-
642
+
634
643
  expect(avatar.src).toBe('https://example.com/img.png');
635
644
  });
636
645
 
@@ -162,7 +162,17 @@ contracts:
162
162
  When not decorative, Avatar provides accessible name:
163
163
  - If alt prop provided: uses alt text
164
164
  - If no alt prop (iOS/Android): "User avatar" for human, "AI agent avatar" for agent
165
- - Web requires alt prop when src is provided
165
+ - Web requires alt prop when src is provided, scoped to human type only
166
+ (agent type ignores src entirely — Req 5 AC 5) and only when decorative
167
+ is false (decorative is the component's own non-informative signal,
168
+ Req 9.2, and takes precedence over the alt requirement)
169
+ - Web edge semantics (settled Spec 126, O2, ratified 2026-07-09): the dev
170
+ warn fires iff `type === 'human' && src && alt == null && !decorative`.
171
+ A raw explicit empty alt (`setAttribute('alt', '')`) does not warn —
172
+ it is treated as the deliberate empty-alt signal. The `alt` property
173
+ setter collapses a falsy value (including '') to removeAttribute, so
174
+ setting `avatar.alt = ''` via the property lands in the "absent" bucket
175
+ and DOES warn — this is a setter-collapse detail, not a design gap.
166
176
  wcag: "1.1.1 Non-text Content"
167
177
  platforms: [web, ios, android]
168
178
  validation:
@@ -700,12 +700,31 @@ export class AvatarBaseElement extends HTMLElement {
700
700
  // @see Requirements: 9.2 - Apply aria-hidden="true" when decorative prop is true
701
701
  const ariaHiddenAttr = decorative ? ' aria-hidden="true"' : '';
702
702
 
703
- // Warn in development if src is provided without alt
704
- // @see Requirements: 5.4 - Require alt prop when src is provided
705
- if (src && !alt && typeof console !== 'undefined') {
703
+ // Warn in development if a human-type avatar is missing alt text for its image.
704
+ // @see Requirements: 5.4 - Human type + src SHALL require alt for accessibility
705
+ // @see Requirements: 5 AC 5 - Agent type SHALL ignore src entirely (src not
706
+ // rendered as an image), so agent-type avatars never warn regardless of alt.
707
+ //
708
+ // Settled edge semantics (Spec 126, O2, ratified 2026-07-09):
709
+ // - Scope is human-type only — Req 5.4 is human-type-scoped, and Req 5 AC 5
710
+ // affirmatively establishes that agent-type SHALL ignore src, so a src+no-alt
711
+ // agent avatar is not a violation of anything.
712
+ // - `alt == null` (loose equality — also excludes `undefined`), not `!alt`, is the
713
+ // test. The `alt` attribute getter returns `getAttribute('alt')`. Raw
714
+ // `setAttribute('alt', '')` yields `alt === ''`, which is the HTML-idiomatic
715
+ // explicit-empty-alt signal and must NOT warn. The `alt` *property setter*
716
+ // (see below) treats a falsy value as "remove the attribute," so
717
+ // `avatar.alt = ''` collapses to `alt === null` — indistinguishable from
718
+ // omitting alt entirely, and DOES warn under this condition.
719
+ // - `!decorative` is the operative fix for the confirmed false positive: a
720
+ // `decorative` avatar is the component's official signal that the image is
721
+ // non-informative (Req 9.2 applies `aria-hidden="true"`), so it must never warn
722
+ // regardless of alt.
723
+ if (type === 'human' && src && alt == null && !decorative && typeof console !== 'undefined') {
706
724
  console.warn(
707
- 'AvatarBaseElement: "alt" prop is required when "src" is provided for accessibility. ' +
708
- 'Please add an alt attribute describing the image.'
725
+ 'AvatarBaseElement: "alt" is required when "src" is provided. ' +
726
+ 'Add an alt description of the image; or, if this avatar is purely decorative ' +
727
+ '(non-informative), set the "decorative" prop to hide it from screen readers.'
709
728
  );
710
729
  }
711
730
 
@@ -102,7 +102,7 @@ contracts:
102
102
  - No hover or press visual feedback
103
103
  - pointer-events: none (web)
104
104
  - No gesture handlers (native)
105
- wcag: null
105
+ wcag: "N/A" # legitimate-null: non-interactive negative guarantee, no positive WCAG SC (125-B 4.2)
106
106
  platforms: [web, ios, android]
107
107
  validation:
108
108
  - Badge cannot receive keyboard focus
@@ -77,7 +77,7 @@ contracts:
77
77
  - No hover or press visual feedback
78
78
  - pointer-events: none (web)
79
79
  - No gesture handlers (native)
80
- wcag: null
80
+ wcag: "N/A" # legitimate-null: non-interactive negative guarantee, no positive WCAG SC (125-B 4.2)
81
81
  platforms: [web, ios, android]
82
82
  validation:
83
83
  - Badge cannot receive keyboard focus
@@ -90,20 +90,12 @@ properties:
90
90
  When true, text is truncated with ellipsis instead of wrapping.
91
91
  Default (false) allows text wrapping for accessibility and i18n support.
92
92
 
93
- disabled:
94
- type: boolean
95
- required: false
96
- default: false
97
- description: |
98
- When true, button is non-interactive with disabled visual styling.
99
- Disabled buttons are not keyboard focusable and onPress is not called.
100
-
101
93
  onPress:
102
94
  type: "() => void"
103
95
  required: true
104
96
  description: |
105
97
  Callback function invoked when button is clicked, tapped, or activated
106
- via keyboard (Enter/Space). Not called when disabled.
98
+ via keyboard (Enter/Space).
107
99
 
108
100
  testID:
109
101
  type: string
@@ -153,7 +145,6 @@ tokens:
153
145
  blend:
154
146
  - blend.hoverDarker
155
147
  - blend.pressedDarker
156
- - blend.disabledDesaturate
157
148
  - blend.iconLighter
158
149
  component:
159
150
  - buttonCTA.minWidth.small
@@ -247,11 +238,10 @@ accessibility:
247
238
  - "1.4.11 Non-text Contrast - Focus ring meets 3:1 contrast"
248
239
  - "2.1.1 Keyboard - Fully keyboard accessible (Tab, Enter, Space)"
249
240
  - "2.4.7 Focus Visible - Clear focus indicator with :focus-visible"
250
- - "4.1.2 Name, Role, Value - Proper ARIA attributes and disabled state"
241
+ - "4.1.2 Name, Role, Value - Proper ARIA attributes"
251
242
  - "2.5.5 Target Size - Touch targets meet 44px minimum"
252
243
  aria_attributes:
253
244
  - role: button (implicit from semantic element)
254
- - aria-disabled: true when disabled
255
245
  - aria-label: label prop value
256
246
  keyboard_navigation:
257
247
  - Tab: Move focus to/from button
@@ -41,10 +41,11 @@ This component guarantees 7 behavioral contracts across all platforms:
41
41
  | `pressable` | Responds to press/click events | web, ios, android | 2.1.1 |
42
42
  | `hover_state` | Visual feedback on hover | web | 1.4.13 |
43
43
  | `pressed_state` | Visual feedback when pressed | web, ios, android | 2.4.7 |
44
- | `disabled_state` | Prevents interaction when disabled | web, ios, android | 4.1.2 |
45
44
  | `loading_state` | Shows loading indicator | web, ios, android | 4.1.3 |
46
45
  | `focus_ring` | WCAG 2.4.7 focus visible indicator | web, ios, android | 2.4.7 |
47
46
 
47
+ **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).
48
+
48
49
  ---
49
50
 
50
51
  ## Usage
@@ -62,7 +63,6 @@ This component guarantees 7 behavioral contracts across all platforms:
62
63
  variant="primary"
63
64
  icon="arrow-right"
64
65
  no-wrap="false"
65
- disabled="false"
66
66
  test-id="submit-button"
67
67
  ></button-cta>
68
68
  ```
@@ -122,7 +122,6 @@ ButtonCTA(
122
122
  | `variant` | `'primary' \| 'secondary' \| 'tertiary'` | No | `'primary'` | Button visual variant |
123
123
  | `icon` | `IconName` | No | - | Optional leading icon |
124
124
  | `noWrap` | `boolean` | No | `false` | Prevent text wrapping |
125
- | `disabled` | `boolean` | No | `false` | Disable button interaction |
126
125
  | `onPress` | `() => void` | ✅ Yes | - | Press/click handler |
127
126
  | `testID` | `string` | No | - | Test identifier |
128
127
 
@@ -177,7 +176,6 @@ ButtonCTA(
177
176
  ### Blend
178
177
  - `blend.hoverDarker` - Hover state (8% darker)
179
178
  - `blend.pressedDarker` - Pressed state (12% darker)
180
- - `blend.disabledDesaturate` - Disabled state (12% less saturated)
181
179
  - `blend.iconLighter` - Icon optical balance (8% lighter)
182
180
 
183
181
  ### Accessibility
@@ -205,7 +203,7 @@ ButtonCTA(
205
203
  | 2.1.1 Keyboard | ✅ | Fully keyboard accessible (Tab, Enter, Space) |
206
204
  | 2.4.7 Focus Visible | ✅ | Clear focus indicator with :focus-visible |
207
205
  | 2.5.5 Target Size | ✅ | Touch targets meet 44px minimum |
208
- | 4.1.2 Name, Role, Value | ✅ | Proper ARIA attributes and disabled state |
206
+ | 4.1.2 Name, Role, Value | ✅ | Proper ARIA attributes |
209
207
 
210
208
  ### Keyboard Navigation
211
209
 
@@ -219,7 +217,6 @@ ButtonCTA(
219
217
 
220
218
  - Semantic `<button>` element provides implicit role
221
219
  - `aria-label` set from label prop
222
- - `aria-disabled="true"` when disabled
223
220
  - State changes announced to assistive technology
224
221
 
225
222
  ---
@@ -7,7 +7,7 @@
7
7
  * Unit tests for Button-CTA component rendering
8
8
  *
9
9
  * Tests component rendering with various props, size variants, variant styles,
10
- * icon integration, text wrapping, and disabled state.
10
+ * icon integration, and text wrapping.
11
11
  *
12
12
  * Stemma System Naming: [Family]-[Type] = Button-CTA
13
13
  * Component Type: Standalone (no behavioral variants)
@@ -66,7 +66,6 @@ describe('Button-CTA Component Rendering', () => {
66
66
  buttonVariant?: 'primary' | 'secondary' | 'tertiary';
67
67
  icon?: string;
68
68
  noWrap?: boolean;
69
- disabled?: boolean;
70
69
  testID?: string;
71
70
  }): ButtonCTA {
72
71
  // Create element - constructor runs and attaches shadow DOM
@@ -78,7 +77,6 @@ describe('Button-CTA Component Rendering', () => {
78
77
  if (props.buttonVariant) button.buttonVariant = props.buttonVariant;
79
78
  if (props.icon) button.icon = props.icon;
80
79
  if (props.noWrap !== undefined) button.noWrap = props.noWrap;
81
- if (props.disabled !== undefined) button.disabled = props.disabled;
82
80
  if (props.testID) button.testID = props.testID;
83
81
 
84
82
  // Append to container
@@ -287,40 +285,48 @@ describe('Button-CTA Component Rendering', () => {
287
285
  });
288
286
  });
289
287
 
290
- describe('Disabled State', () => {
291
- it('should render disabled button with correct attributes', () => {
292
- // Requirement: Disabled prop prevents interaction
293
- const button = createButton({ label: 'Disabled Button', disabled: true });
294
-
295
- const shadowButton = button.shadowRoot?.querySelector('button');
296
- expect(shadowButton?.hasAttribute('disabled')).toBe(true);
297
- expect(shadowButton?.getAttribute('aria-disabled')).toBe('true');
298
- expect(shadowButton?.className).toContain('button-cta--disabled');
288
+ describe('No Disabled State (philosophy exclusion)', () => {
289
+ // DesignerPunk does not support disabled states: if an action is
290
+ // unavailable, the component should not be rendered. state_disabled was
291
+ // removed from Button-CTA per the 2026-07-15 adjudication; these tests
292
+ // guard against the disabled state resurfacing.
293
+
294
+ it('should declare state_disabled under excludes (not contracts) in contracts.yaml', () => {
295
+ const fs = require('fs');
296
+ const path = require('path');
297
+ const contractsContent = fs.readFileSync(
298
+ path.join(__dirname, '../contracts.yaml'),
299
+ 'utf-8'
300
+ );
301
+
302
+ const contractsSection = contractsContent.split(/^excludes:/m)[0];
303
+ const excludesSection = contractsContent.split(/^excludes:/m)[1] ?? '';
304
+ expect(contractsSection).not.toContain('state_disabled');
305
+ expect(excludesSection).toContain('state_disabled');
299
306
  });
300
-
301
- it('should not have disabled attributes when disabled is false', () => {
302
- // Requirement: Enabled button is interactive
303
- const button = createButton({ label: 'Enabled Button', disabled: false });
304
-
305
- const shadowButton = button.shadowRoot?.querySelector('button');
306
- expect(shadowButton?.hasAttribute('disabled')).toBe(false);
307
- expect(shadowButton?.getAttribute('aria-disabled')).toBe('false');
308
- expect(shadowButton?.className).not.toContain('button-cta--disabled');
307
+
308
+ it('should not expose a disabled property or observe a disabled attribute', () => {
309
+ expect(ButtonCTA.observedAttributes).not.toContain('disabled');
310
+
311
+ const button = createButton({ label: 'Button' });
312
+ expect('disabled' in button).toBe(false);
309
313
  });
310
-
311
- it('should prevent press event when disabled', () => {
312
- // Requirement: Disabled button doesn't trigger onPress
313
- const button = createButton({ label: 'Disabled Button', disabled: true });
314
-
314
+
315
+ it('should ignore a disabled attribute set by consumers', () => {
316
+ const button = createButton({ label: 'Button' });
317
+ button.setAttribute('disabled', '');
318
+
315
319
  let pressCount = 0;
316
320
  button.addEventListener('press', () => {
317
321
  pressCount++;
318
322
  });
319
-
323
+
320
324
  const shadowButton = button.shadowRoot?.querySelector('button');
325
+ expect(shadowButton?.hasAttribute('disabled')).toBe(false);
326
+ expect(shadowButton?.className).not.toContain('button-cta--disabled');
327
+
321
328
  shadowButton?.click();
322
-
323
- expect(pressCount).toBe(0);
329
+ expect(pressCount).toBe(1);
324
330
  });
325
331
  });
326
332
 
@@ -409,21 +415,6 @@ describe('Button-CTA Component Rendering', () => {
409
415
  expect(pressCount).toBe(1);
410
416
  });
411
417
 
412
- it('should NOT call onPress when button disabled', () => {
413
- // Requirement 15.1: Disabled button doesn't trigger onPress
414
- const button = createButton({ label: 'Disabled Button', disabled: true });
415
-
416
- let pressCount = 0;
417
- button.addEventListener('press', () => {
418
- pressCount++;
419
- });
420
-
421
- const shadowButton = button.shadowRoot?.querySelector('button');
422
- shadowButton?.click();
423
-
424
- expect(pressCount).toBe(0);
425
- });
426
-
427
418
  it('should receive focus via Tab key', () => {
428
419
  // Requirement 15.2: Button is keyboard focusable
429
420
  const button = createButton({ label: 'Focusable Button' });
@@ -647,19 +638,6 @@ describe('Button-CTA Component Rendering', () => {
647
638
  expect(shadowButton?.getAttribute('role')).toBe('button');
648
639
  expect(shadowButton?.getAttribute('aria-label')).toBe('Accessible Button');
649
640
  expect(shadowButton?.getAttribute('type')).toBe('button');
650
- expect(shadowButton?.getAttribute('aria-disabled')).toBe('false');
651
- });
652
-
653
- it('should have proper ARIA attributes when disabled', () => {
654
- // Requirement 12.6: Disabled button has correct ARIA attributes
655
- const button = createButton({ label: 'Disabled Button', disabled: true });
656
-
657
- const shadowButton = button.shadowRoot?.querySelector('button');
658
-
659
- // Verify disabled ARIA attributes
660
- expect(shadowButton?.getAttribute('role')).toBe('button');
661
- expect(shadowButton?.getAttribute('aria-disabled')).toBe('true');
662
- expect(shadowButton?.hasAttribute('disabled')).toBe(true);
663
641
  });
664
642
 
665
643
  it('should support keyboard navigation with Enter key', () => {
@@ -716,37 +694,5 @@ describe('Button-CTA Component Rendering', () => {
716
694
  expect(pressCount).toBe(1);
717
695
  });
718
696
 
719
- it('should not activate on keyboard when disabled', () => {
720
- // Requirement 15.4: Disabled button doesn't respond to keyboard
721
- const button = createButton({ label: 'Disabled Keyboard', disabled: true });
722
-
723
- let pressCount = 0;
724
- button.addEventListener('press', () => {
725
- pressCount++;
726
- });
727
-
728
- const shadowButton = button.shadowRoot?.querySelector('button');
729
- shadowButton?.focus();
730
-
731
- // Try Enter key
732
- const enterEvent = new KeyboardEvent('keydown', {
733
- key: 'Enter',
734
- code: 'Enter',
735
- bubbles: true,
736
- cancelable: true
737
- });
738
- shadowButton?.dispatchEvent(enterEvent);
739
-
740
- // Try Space key
741
- const spaceEvent = new KeyboardEvent('keydown', {
742
- key: ' ',
743
- code: 'Space',
744
- bubbles: true,
745
- cancelable: true
746
- });
747
- shadowButton?.dispatchEvent(spaceEvent);
748
-
749
- expect(pressCount).toBe(0);
750
- });
751
697
  });
752
698
  });
@@ -123,7 +123,6 @@ describe('Button-CTA Test Infrastructure Setup', () => {
123
123
  buttonVariant: 'secondary',
124
124
  icon: 'arrow-right',
125
125
  noWrap: true,
126
- disabled: false,
127
126
  testID: 'test-button'
128
127
  });
129
128
 
@@ -132,7 +131,6 @@ describe('Button-CTA Test Infrastructure Setup', () => {
132
131
  expect(button.buttonVariant).toBe('secondary');
133
132
  expect(button.icon).toBe('arrow-right');
134
133
  expect(button.noWrap).toBe(true);
135
- expect(button.disabled).toBe(false);
136
134
  expect(button.testID).toBe('test-button');
137
135
  });
138
136
 
@@ -96,7 +96,6 @@ export async function createButtonCTA(props: {
96
96
  buttonVariant?: 'primary' | 'secondary' | 'tertiary';
97
97
  icon?: string;
98
98
  noWrap?: boolean;
99
- disabled?: boolean;
100
99
  testID?: string;
101
100
  }): Promise<ButtonCTA> {
102
101
  registerButtonCTA();
@@ -109,7 +108,6 @@ export async function createButtonCTA(props: {
109
108
  if (props.buttonVariant) button.buttonVariant = props.buttonVariant;
110
109
  if (props.icon) button.icon = props.icon;
111
110
  if (props.noWrap !== undefined) button.noWrap = props.noWrap;
112
- if (props.disabled !== undefined) button.disabled = props.disabled;
113
111
  if (props.testID) button.testID = props.testID;
114
112
 
115
113
  // Append to document to trigger rendering