@3fn/core 13.0.0 → 14.1.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.
- package/.kiro/agents/ada-prompt.md +80 -132
- package/.kiro/agents/ada-prompt.md.attribution.json +45 -0
- package/.kiro/agents/ada.json +44 -59
- package/.kiro/agents/ada.json.attribution.json +13 -0
- package/.kiro/agents/data-prompt.md +83 -74
- package/.kiro/agents/data-prompt.md.attribution.json +53 -0
- package/.kiro/agents/data.json +31 -38
- package/.kiro/agents/data.json.attribution.json +13 -0
- package/.kiro/agents/kenya-prompt.md +83 -72
- package/.kiro/agents/kenya-prompt.md.attribution.json +53 -0
- package/.kiro/agents/kenya.json +27 -35
- package/.kiro/agents/kenya.json.attribution.json +13 -0
- package/.kiro/agents/leonardo-prompt.md +176 -234
- package/.kiro/agents/leonardo-prompt.md.attribution.json +45 -0
- package/.kiro/agents/leonardo.json +28 -36
- package/.kiro/agents/leonardo.json.attribution.json +13 -0
- package/.kiro/agents/lina-prompt.md +110 -151
- package/.kiro/agents/lina-prompt.md.attribution.json +53 -0
- package/.kiro/agents/lina.json +46 -59
- package/.kiro/agents/lina.json.attribution.json +13 -0
- package/.kiro/agents/sparky-prompt.md +89 -72
- package/.kiro/agents/sparky-prompt.md.attribution.json +53 -0
- package/.kiro/agents/sparky.json +37 -37
- package/.kiro/agents/sparky.json.attribution.json +13 -0
- package/.kiro/agents/stacy-prompt.md +74 -48
- package/.kiro/agents/stacy-prompt.md.attribution.json +45 -0
- package/.kiro/agents/stacy.json +28 -30
- package/.kiro/agents/stacy.json.attribution.json +13 -0
- package/.kiro/agents/thurgood-prompt.md +94 -138
- package/.kiro/agents/thurgood-prompt.md.attribution.json +45 -0
- package/.kiro/agents/thurgood.json +31 -35
- package/.kiro/agents/thurgood.json.attribution.json +13 -0
- package/.kiro/steering/AI-Collaboration-Principles.md +3 -3
- package/.kiro/steering/Civitas-System-Overview.md +7 -16
- package/.kiro/steering/DesignerPunk-Systems-Overview.md +6 -6
- package/.kiro/steering/Spec-Feedback-Protocol.md +2 -11
- package/.kiro/steering/Task-Completion-Protocol.md +98 -17
- package/.kiro/steering/core-goals.md +3 -3
- package/.kiro/steering/personal-note.md +1 -1
- package/.kiro/steering/start-up-tasks.md +17 -6
- package/application-mcp-server/src/index.ts +26 -0
- package/dist/ComponentTokens.android.kt +12 -12
- package/dist/ComponentTokens.ios.swift +12 -12
- package/dist/ComponentTokens.web.css +3 -3
- package/dist/DesignTokens.android.kt +1 -1
- package/dist/DesignTokens.dtcg.json +8 -5
- package/dist/DesignTokens.figma.json +2 -2
- package/dist/DesignTokens.ios.swift +1 -1
- package/dist/DesignTokens.web.css +1 -1
- package/dist/android/DesignTokens.android.kt +1 -1
- package/dist/blend/OklchBlendCalculator.js +1 -0
- package/dist/blend/ThemeAwareBlendUtilities.web.d.ts +13 -2
- package/dist/blend/ThemeAwareBlendUtilities.web.js +6 -1
- package/dist/browser/designerpunk.esm.js +36 -90
- package/dist/browser/designerpunk.esm.min.js +33 -36
- package/dist/browser/designerpunk.umd.js +36 -90
- package/dist/browser/designerpunk.umd.min.js +47 -50
- package/dist/browser/tokens.css +3 -3
- package/dist/build/tokens/defineComponentTokens.d.ts +10 -0
- package/dist/build/tokens/defineComponentTokens.js +26 -0
- package/dist/components/core/Avatar-Base/avatar.tokens.d.ts +21 -26
- package/dist/components/core/Avatar-Base/avatar.tokens.js +31 -34
- package/dist/components/core/Avatar-Base/index.d.ts +1 -1
- package/dist/components/core/Avatar-Base/index.js +2 -2
- package/dist/components/core/Avatar-Base/platforms/web/Avatar.web.js +24 -5
- package/dist/components/core/Button-CTA/examples/BasicUsage.d.ts +16 -28
- package/dist/components/core/Button-CTA/examples/BasicUsage.js +18 -43
- package/dist/components/core/Button-CTA/platforms/web/ButtonCTA.web.d.ts +3 -15
- package/dist/components/core/Button-CTA/platforms/web/ButtonCTA.web.js +9 -58
- package/dist/components/core/Button-CTA/types.d.ts +0 -24
- package/dist/components/core/Button-CTA/types.js +6 -0
- package/dist/components/core/Button-Icon/buttonIcon.tokens.d.ts +28 -14
- package/dist/components/core/Button-Icon/buttonIcon.tokens.js +35 -20
- package/dist/components/core/Input-Text-Base/types.d.ts +13 -1
- package/dist/components/core/Input-Text-Password/platforms/web/InputTextPassword.web.js +11 -2
- package/dist/generators/DTCGFormatGenerator.js +8 -0
- package/dist/generators/TokenFileGenerator.js +7 -2
- package/dist/integration/BuildErrorHandler.js +2 -2
- package/dist/ios/DesignTokens.ios.swift +1 -1
- package/dist/mcp/application-mcp.js +24 -0
- package/dist/mcp/docs-mcp.js +130 -15
- package/dist/mcp/product-mcp.js +25 -0
- package/dist/tokens/OpacityTokens.js +1 -1
- package/dist/tokens/component/progress.d.ts +65 -5
- package/dist/tokens/component/progress.js +79 -18
- package/dist/tokens/semantic/BlendTokens.d.ts +10 -3
- package/dist/tokens/semantic/BlendTokens.js +17 -5
- package/dist/tokens/semantic/OpacityTokens.d.ts +4 -4
- package/dist/tokens/semantic/OpacityTokens.js +4 -4
- package/dist/types/ComponentTypes.d.ts +1 -1
- package/dist/types/generated/TokenTypes.d.ts +1 -1
- package/dist/types/generated/TokenTypes.js +1 -1
- package/dist/validators/StemmaTokenUsageValidator.js +3 -2
- package/dist/web/DesignTokens.web.css +1 -1
- package/governance/BUILD-SYSTEM-SETUP.md +1 -2
- package/governance/Component-Development-Guide.md +23 -13
- package/governance/Component-Development-Standards.md +21 -20
- package/governance/Component-Family-Avatar.md +6 -7
- package/governance/Component-Family-Badge.md +19 -20
- package/governance/Component-Family-Button.md +30 -43
- package/governance/Component-Family-Chip.md +14 -15
- package/governance/Component-Family-Container.md +12 -13
- package/governance/Component-Family-Data-Display.md +1 -2
- package/governance/Component-Family-Divider.md +1 -2
- package/governance/Component-Family-Form-Inputs.md +65 -65
- package/governance/Component-Family-Icon.md +9 -10
- package/governance/Component-Family-Loading.md +1 -2
- package/governance/Component-Family-Modal.md +1 -2
- package/governance/Component-Family-Navigation.md +1 -2
- package/governance/Component-Family-Progress.md +0 -1
- package/governance/Component-Inheritance-Structures.md +219 -99
- package/governance/Component-MCP-Document-Template.md +6 -5
- package/governance/Component-Primitive-vs-Semantic-Philosophy.md +1 -1
- package/governance/Component-Quick-Reference.md +33 -33
- package/governance/Component-Readiness-Status.md +59 -44
- package/governance/Component-Templates.md +57 -61
- package/governance/Contract-System-Reference.md +7 -7
- package/governance/MCP-Integration-Guide.md +1 -1
- package/governance/Process-Cross-Reference-Standards.md +31 -13
- package/governance/Process-Development-Workflow.md +49 -59
- package/governance/Process-File-Organization.md +25 -28
- package/governance/Process-Hook-Operations.md +22 -11
- package/governance/Process-Orchestration-Model-Selection.md +92 -0
- package/governance/Process-Spec-Planning.md +98 -52
- package/governance/Process-Task-Type-Definitions.md +80 -4
- package/governance/Product-Handoff-Protocol.md +2 -0
- package/governance/Rosetta-System-Architecture.md +13 -11
- package/governance/Test-Behavioral-Contract-Validation.md +38 -31
- package/governance/Test-Failure-Audit-Methodology.md +1 -1
- package/governance/Token-Family-Accessibility.md +1 -2
- package/governance/Token-Family-Blend.md +18 -16
- package/governance/Token-Family-Blur.md +0 -1
- package/governance/Token-Family-Border.md +1 -2
- package/governance/Token-Family-Color.md +0 -1
- package/governance/Token-Family-Glow.md +1 -2
- package/governance/Token-Family-Layering.md +0 -1
- package/governance/Token-Family-Motion.md +1 -2
- package/governance/Token-Family-Opacity.md +0 -1
- package/governance/Token-Family-Radius.md +1 -2
- package/governance/Token-Family-Responsive.md +1 -2
- package/governance/Token-Family-Shadow.md +1 -2
- package/governance/Token-Family-Sizing.md +0 -1
- package/governance/Token-Family-Spacing.md +1 -2
- package/governance/Token-Family-Typography.md +1 -2
- package/governance/Token-Governance.md +8 -8
- package/governance/Token-Quick-Reference.md +47 -34
- package/governance/Token-Resolution-Patterns.md +1 -1
- package/governance/Token-Semantic-Structure.md +1 -1
- package/governance/Web-Authoring-Standards.md +5 -5
- package/governance/browser-distribution-guide.md +1 -4
- package/governance/classification-map.md +474 -0
- package/governance/completion-documentation-guide.md +23 -37
- package/governance/component-meta-authoring-guide.md +1 -1
- package/governance/cross-platform-vs-platform-specific-decision-framework.md +1 -1
- package/governance/platform-implementation-guidelines.md +2 -3
- package/governance/release-management-system.md +28 -63
- package/governance/rosetta-system-principles.md +8 -6
- package/governance/stemma-system-principles.md +18 -17
- package/mcp-server/src/index.ts +24 -6
- package/mcp-server/src/indexer/DocumentIndexer.ts +119 -9
- package/mcp-server/src/indexer/__tests__/bare-id-crossrefs.test.ts +250 -0
- package/mcp-server/src/indexer/cross-ref-parser.ts +29 -1
- package/mcp-server/src/indexer/index-health.ts +27 -2
- package/mcp-server/src/query/__tests__/find-docs-calibration.test.ts +11 -26
- package/mcp-server/src/relocation-integrity-gate/__tests__/relocation-integrity-gate.test.ts +72 -5
- package/mcp-server/src/relocation-integrity-gate/relocation-integrity-gate.ts +81 -24
- package/mcp-server/src/tools/list-cross-references.ts +2 -2
- package/package.json +24 -26
- package/src/__tests__/browser-distribution/css-bundling.test.ts +6 -4
- package/src/__tests__/console-allowlist.json +14 -0
- package/src/__tests__/console-fail-setup.ts +169 -0
- package/src/__tests__/integration/Spec107-DesignLanguageContext.test.ts +16 -0
- package/src/__tests__/stemma-system/behavioral-contract-validation.test.ts +70 -17
- package/src/__tests__/stemma-system/contract-catalog-name-validation.test.ts +28 -0
- package/src/__tests__/stemma-system/form-inputs-contracts.test.ts +223 -16
- package/src/__tests__/stemma-system/input-text-native-base-call-alignment.test.ts +298 -0
- package/src/blend/OklchBlendCalculator.ts +3 -0
- package/src/blend/ThemeAwareBlendUtilities.android.kt +3 -0
- package/src/blend/ThemeAwareBlendUtilities.ios.swift +3 -0
- package/src/blend/ThemeAwareBlendUtilities.web.ts +9 -1
- package/src/blend/__tests__/InteractionStateAudit.test.ts +12 -9
- package/src/build/errors/__tests__/ErrorHandler.integration.test.ts +8 -0
- package/src/build/errors/__tests__/ErrorHandler.test.ts +5 -0
- package/src/build/tokens/__tests__/defineComponentTokens.test.ts +113 -0
- package/src/build/tokens/defineComponentTokens.ts +43 -1
- package/src/build/workflow/__tests__/CICDIntegration.test.ts +12 -1
- package/src/cli/__tests__/init.test.ts +45 -11
- package/src/components/core/Avatar-Base/Avatar-Base.schema.yaml +1 -1
- package/src/components/core/Avatar-Base/__tests__/Avatar.accessibility.test.ts +121 -7
- package/src/components/core/Avatar-Base/__tests__/Avatar.image.test.ts +3 -0
- package/src/components/core/Avatar-Base/__tests__/Avatar.test.ts +15 -6
- package/src/components/core/Avatar-Base/avatar.tokens.ts +31 -34
- package/src/components/core/Avatar-Base/contracts.yaml +11 -1
- package/src/components/core/Avatar-Base/index.ts +1 -1
- package/src/components/core/Avatar-Base/platforms/web/Avatar.web.ts +24 -5
- package/src/components/core/Badge-Count-Base/contracts.yaml +1 -1
- package/src/components/core/Badge-Label-Base/contracts.yaml +1 -1
- package/src/components/core/Button-CTA/Button-CTA.schema.yaml +2 -12
- package/src/components/core/Button-CTA/README.md +3 -6
- package/src/components/core/Button-CTA/__tests__/ButtonCTA.test.ts +35 -89
- package/src/components/core/Button-CTA/__tests__/setup.test.ts +0 -2
- package/src/components/core/Button-CTA/__tests__/test-utils.ts +0 -2
- package/src/components/core/Button-CTA/contracts.yaml +6 -29
- package/src/components/core/Button-CTA/examples/BasicUsage.html +2 -14
- package/src/components/core/Button-CTA/examples/BasicUsage.tsx +17 -44
- package/src/components/core/Button-CTA/platforms/android/ButtonCTA.android.kt +12 -20
- package/src/components/core/Button-CTA/platforms/ios/ButtonCTA.ios.swift +12 -51
- package/src/components/core/Button-CTA/platforms/web/ButtonCTA.web.css +2 -26
- package/src/components/core/Button-CTA/platforms/web/ButtonCTA.web.ts +18 -71
- package/src/components/core/Button-CTA/types.ts +10 -28
- package/src/components/core/Button-Icon/buttonIcon.tokens.ts +43 -27
- package/src/components/core/Chip-Base/__tests__/ChipBase.test.ts +13 -0
- package/src/components/core/Chip-Filter/__tests__/ChipFilter.test.ts +13 -0
- package/src/components/core/Chip-Input/__tests__/ChipInput.test.ts +13 -0
- package/src/components/core/Input-Text-Base/Input-Text-Base.schema.yaml +30 -2
- package/src/components/core/Input-Text-Base/README.md +25 -2
- package/src/components/core/Input-Text-Base/__tests__/focusIndicators.test.ts +16 -15
- package/src/components/core/Input-Text-Base/contracts.yaml +90 -0
- package/src/components/core/Input-Text-Base/platforms/android/InputTextBase.android.kt +26 -12
- package/src/components/core/Input-Text-Base/platforms/ios/InputTextBase.ios.swift +195 -59
- package/src/components/core/Input-Text-Base/types.ts +13 -1
- package/src/components/core/Input-Text-Email/Input-Text-Email.schema.yaml +5 -1
- package/src/components/core/Input-Text-Email/README.md +8 -7
- package/src/components/core/Input-Text-Email/platforms/android/InputTextEmail.android.kt +1 -4
- package/src/components/core/Input-Text-Email/platforms/ios/InputTextEmail.ios.swift +2 -16
- package/src/components/core/Input-Text-Password/Input-Text-Password.schema.yaml +10 -3
- package/src/components/core/Input-Text-Password/README.md +9 -8
- package/src/components/core/Input-Text-Password/contracts.yaml +5 -0
- package/src/components/core/Input-Text-Password/platforms/android/InputTextPassword.android.kt +17 -7
- package/src/components/core/Input-Text-Password/platforms/ios/InputTextPassword.ios.swift +22 -20
- package/src/components/core/Input-Text-Password/platforms/web/InputTextPassword.web.ts +11 -2
- package/src/components/core/Input-Text-PhoneNumber/Input-Text-PhoneNumber.schema.yaml +5 -1
- package/src/components/core/Input-Text-PhoneNumber/README.md +9 -8
- package/src/components/core/Input-Text-PhoneNumber/platforms/android/InputTextPhoneNumber.android.kt +2 -5
- package/src/components/core/Input-Text-PhoneNumber/platforms/ios/InputTextPhoneNumber.ios.swift +3 -17
- package/src/components/core/Nav-Header-App/contracts.yaml +1 -1
- package/src/components/core/Nav-SegmentedChoice-Base/contracts.yaml +1 -1
- package/src/components/core/Progress-Indicator-Connector-Base/contracts.yaml +1 -1
- package/src/components/core/Progress-Indicator-Label-Base/contracts.yaml +1 -1
- package/src/components/core/Progress-Indicator-Node-Base/contracts.yaml +1 -1
- package/src/components/core/Progress-Stepper-Base/__tests__/StepperBase.test.ts +5 -2
- package/src/components/core/Progress-Stepper-Detailed/__tests__/StepperDetailed.test.ts +5 -2
- package/src/generators/DTCGFormatGenerator.ts +6 -0
- package/src/generators/TokenFileGenerator.ts +7 -2
- package/src/generators/__tests__/DTCGConfigOptions.test.ts +14 -5
- package/src/integration/BuildErrorHandler.ts +2 -2
- package/src/tokens/OpacityTokens.ts +1 -1
- package/src/tokens/__tests__/OpacityTokens.test.ts +3 -1
- package/src/tokens/__tests__/ProgressTokenCompliance.test.ts +5 -3
- package/src/tokens/__tests__/ProgressTokenFormulas.test.ts +11 -11
- package/src/tokens/__tests__/ProgressTokenTranslation.test.ts +22 -20
- package/src/tokens/component/progress.ts +83 -21
- package/src/tokens/semantic/BlendTokens.ts +26 -5
- package/src/tokens/semantic/OpacityTokens.ts +4 -4
- package/src/types/ComponentTypes.ts +1 -1
- package/src/types/generated/TokenTypes.ts +1 -1
- package/src/validators/StemmaTokenUsageValidator.ts +3 -2
- package/token-index/components.yaml +8 -8
- package/token-index/semantics.yaml +1 -2
- package/src/tools/release/__tests__/ChangeClassifier.test.ts +0 -133
- package/src/tools/release/__tests__/ChangeExtractor.test.ts +0 -222
- package/src/tools/release/__tests__/GitHubPublisher.test.ts +0 -240
- package/src/tools/release/__tests__/NotesRenderer.test.ts +0 -142
- package/src/tools/release/__tests__/NpmPublisher.test.ts +0 -289
- package/src/tools/release/__tests__/PipelineIntegration.test.ts +0 -188
- package/src/tools/release/__tests__/ReleasePipeline.test.ts +0 -192
- package/src/tools/release/__tests__/SemanticVersionValidator.test.ts +0 -49
- package/src/tools/release/__tests__/SummaryScanner.test.ts +0 -141
- package/src/tools/release/__tests__/TagResolver.test.ts +0 -91
- package/src/tools/release/__tests__/VersionCalculator.test.ts +0 -270
- package/src/tools/release/__tests__/helpers/NpmMockHelper.ts +0 -80
- package/src/tools/release/cli/ReleasePipeline.ts +0 -165
- package/src/tools/release/cli/release-tool.ts +0 -107
- package/src/tools/release/pipeline/ChangeClassifier.ts +0 -61
- package/src/tools/release/pipeline/ChangeExtractor.ts +0 -87
- package/src/tools/release/pipeline/NotesRenderer.ts +0 -66
- package/src/tools/release/pipeline/SummaryScanner.ts +0 -70
- package/src/tools/release/pipeline/TagResolver.ts +0 -40
- package/src/tools/release/pipeline/VersionCalculator.ts +0 -375
- package/src/tools/release/publishers/GitHubPublisher.ts +0 -228
- package/src/tools/release/publishers/NpmPublisher.ts +0 -196
- package/src/tools/release/release-config.json +0 -5
- package/src/tools/release/types/index.ts +0 -282
- package/src/tools/release/validators/SemanticVersionValidator.ts +0 -67
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input-Text Native Base-Call Alignment Validation
|
|
3
|
+
*
|
|
4
|
+
* Static analysis tests that validate the native (iOS/Android) Input-Text
|
|
5
|
+
* semantic variants call InputTextBase with parameters the base actually
|
|
6
|
+
* declares. The native platform files are NOT compiled in CI — Swift/Kotlin
|
|
7
|
+
* sources are only validated by static-analysis Jest tests — so a call-site
|
|
8
|
+
* passing an undeclared parameter (or referencing a nonexistent enum member)
|
|
9
|
+
* would not compile on a real platform build but previously went undetected.
|
|
10
|
+
*
|
|
11
|
+
* Catches:
|
|
12
|
+
* - Callers passing named arguments the base does not declare
|
|
13
|
+
* (e.g., `trailingContent` before the base grew that slot)
|
|
14
|
+
* - iOS callers passing memberwise-init arguments out of declaration order
|
|
15
|
+
* - References to InputType enum members that do not exist
|
|
16
|
+
* (e.g., `InputType.PHONE` when the enum defines TEL)
|
|
17
|
+
*
|
|
18
|
+
* Stemma System: Form Inputs Family
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import * as fs from 'fs';
|
|
22
|
+
import * as path from 'path';
|
|
23
|
+
|
|
24
|
+
const CORE = path.join(__dirname, '../../components/core');
|
|
25
|
+
|
|
26
|
+
const IOS_BASE = path.join(CORE, 'Input-Text-Base/platforms/ios/InputTextBase.ios.swift');
|
|
27
|
+
const ANDROID_BASE = path.join(CORE, 'Input-Text-Base/platforms/android/InputTextBase.android.kt');
|
|
28
|
+
|
|
29
|
+
const IOS_CALLERS = [
|
|
30
|
+
'Input-Text-Password/platforms/ios/InputTextPassword.ios.swift',
|
|
31
|
+
'Input-Text-Email/platforms/ios/InputTextEmail.ios.swift',
|
|
32
|
+
'Input-Text-PhoneNumber/platforms/ios/InputTextPhoneNumber.ios.swift',
|
|
33
|
+
].map((p) => path.join(CORE, p));
|
|
34
|
+
|
|
35
|
+
const ANDROID_CALLERS = [
|
|
36
|
+
'Input-Text-Password/platforms/android/InputTextPassword.android.kt',
|
|
37
|
+
'Input-Text-Email/platforms/android/InputTextEmail.android.kt',
|
|
38
|
+
'Input-Text-PhoneNumber/platforms/android/InputTextPhoneNumber.android.kt',
|
|
39
|
+
].map((p) => path.join(CORE, p));
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Source scanning helpers (string- and comment-aware, brace/paren-depth aware)
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Extract the balanced-parentheses body starting at `openParenIndex`
|
|
47
|
+
* (which must point at a '('). Skips string literals and line comments.
|
|
48
|
+
*/
|
|
49
|
+
function extractParenBody(source: string, openParenIndex: number): string {
|
|
50
|
+
let depth = 0;
|
|
51
|
+
for (let i = openParenIndex; i < source.length; i++) {
|
|
52
|
+
const ch = source[i];
|
|
53
|
+
if (ch === '"') {
|
|
54
|
+
// Skip string literal (handles escapes)
|
|
55
|
+
i++;
|
|
56
|
+
while (i < source.length && source[i] !== '"') {
|
|
57
|
+
if (source[i] === '\\') i++;
|
|
58
|
+
i++;
|
|
59
|
+
}
|
|
60
|
+
} else if (ch === '/' && source[i + 1] === '/') {
|
|
61
|
+
while (i < source.length && source[i] !== '\n') i++;
|
|
62
|
+
} else if (ch === '(') {
|
|
63
|
+
depth++;
|
|
64
|
+
} else if (ch === ')') {
|
|
65
|
+
depth--;
|
|
66
|
+
if (depth === 0) {
|
|
67
|
+
return source.slice(openParenIndex + 1, i);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
throw new Error('Unbalanced parentheses in source');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Split an argument/parameter list into top-level entries — commas nested
|
|
76
|
+
* inside (), {}, [], strings, or line comments do not split.
|
|
77
|
+
*/
|
|
78
|
+
function splitTopLevel(body: string): string[] {
|
|
79
|
+
const parts: string[] = [];
|
|
80
|
+
let depth = 0;
|
|
81
|
+
let current = '';
|
|
82
|
+
for (let i = 0; i < body.length; i++) {
|
|
83
|
+
const ch = body[i];
|
|
84
|
+
if (ch === '"') {
|
|
85
|
+
current += ch;
|
|
86
|
+
i++;
|
|
87
|
+
while (i < body.length && body[i] !== '"') {
|
|
88
|
+
if (body[i] === '\\') {
|
|
89
|
+
current += body[i];
|
|
90
|
+
i++;
|
|
91
|
+
}
|
|
92
|
+
current += body[i];
|
|
93
|
+
i++;
|
|
94
|
+
}
|
|
95
|
+
current += body[i] ?? '';
|
|
96
|
+
} else if (ch === '/' && body[i + 1] === '/') {
|
|
97
|
+
while (i < body.length && body[i] !== '\n') i++;
|
|
98
|
+
current += '\n';
|
|
99
|
+
} else if ('({['.includes(ch)) {
|
|
100
|
+
depth++;
|
|
101
|
+
current += ch;
|
|
102
|
+
} else if (')}]'.includes(ch)) {
|
|
103
|
+
depth--;
|
|
104
|
+
current += ch;
|
|
105
|
+
} else if (ch === ',' && depth === 0) {
|
|
106
|
+
parts.push(current);
|
|
107
|
+
current = '';
|
|
108
|
+
} else {
|
|
109
|
+
current += ch;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (current.trim()) parts.push(current);
|
|
113
|
+
return parts;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Find every `InputTextBase(...)` call in a source file and return its args. */
|
|
117
|
+
function extractBaseCalls(source: string): string[][] {
|
|
118
|
+
const calls: string[][] = [];
|
|
119
|
+
const re = /\bInputTextBase\(/g;
|
|
120
|
+
let match: RegExpExecArray | null;
|
|
121
|
+
while ((match = re.exec(source)) !== null) {
|
|
122
|
+
const body = extractParenBody(source, match.index + match[0].length - 1);
|
|
123
|
+
calls.push(splitTopLevel(body));
|
|
124
|
+
re.lastIndex = match.index + match[0].length;
|
|
125
|
+
}
|
|
126
|
+
return calls;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Labels of named arguments: Swift `label: value` / Kotlin `name = value`. */
|
|
130
|
+
function argLabel(arg: string, platform: 'ios' | 'android'): string | null {
|
|
131
|
+
const m =
|
|
132
|
+
platform === 'ios'
|
|
133
|
+
? arg.match(/^\s*(\w+)\s*:/)
|
|
134
|
+
: arg.match(/^\s*(\w+)\s*=(?!=)/);
|
|
135
|
+
return m ? m[1] : null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// Base declaration parsers
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* iOS base: settable stored properties of the InputTextBase struct, in
|
|
144
|
+
* declaration order. These define the memberwise initializer, so caller
|
|
145
|
+
* argument labels must be an in-order subsequence of this list.
|
|
146
|
+
* Excludes private state (@State/@FocusState/@Environment are all private)
|
|
147
|
+
* and the `body` computed property.
|
|
148
|
+
*/
|
|
149
|
+
function parseIOSBaseProperties(source: string): string[] {
|
|
150
|
+
const structStart = source.indexOf('struct InputTextBase: View {');
|
|
151
|
+
const bodyStart = source.indexOf('var body: some View', structStart);
|
|
152
|
+
const section = source.slice(structStart, bodyStart);
|
|
153
|
+
const props: string[] = [];
|
|
154
|
+
for (const line of section.split('\n')) {
|
|
155
|
+
if (line.includes('private')) continue;
|
|
156
|
+
const m = line.match(/^\s*(?:@Binding\s+)?(?:let|var)\s+(\w+)\s*[:=]/);
|
|
157
|
+
if (m) props.push(m[1]);
|
|
158
|
+
}
|
|
159
|
+
return props;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Android base: parameter names of the InputTextBase composable function. */
|
|
163
|
+
function parseAndroidBaseParams(source: string): string[] {
|
|
164
|
+
const funIndex = source.indexOf('fun InputTextBase(');
|
|
165
|
+
const body = extractParenBody(source, funIndex + 'fun InputTextBase'.length);
|
|
166
|
+
return splitTopLevel(body)
|
|
167
|
+
.map((p) => p.match(/^\s*(\w+)\s*:/)?.[1])
|
|
168
|
+
.filter((name): name is string => Boolean(name));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Enum members: Swift `case xyz` / Kotlin `enum class InputType { A, B }`. */
|
|
172
|
+
function parseInputTypeMembers(source: string, platform: 'ios' | 'android'): string[] {
|
|
173
|
+
if (platform === 'ios') {
|
|
174
|
+
const enumStart = source.indexOf('enum InputType {');
|
|
175
|
+
const enumBody = source.slice(enumStart, source.indexOf('}', enumStart));
|
|
176
|
+
return [...enumBody.matchAll(/case\s+(\w+)/g)].map((m) => m[1]);
|
|
177
|
+
}
|
|
178
|
+
const enumMatch = source.match(/enum class InputType \{([^}]*)\}/);
|
|
179
|
+
if (!enumMatch) return [];
|
|
180
|
+
return enumMatch[1]
|
|
181
|
+
.split(',')
|
|
182
|
+
.map((m) => m.trim())
|
|
183
|
+
.filter(Boolean);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Assert `subset` appears within `sequence` in order (subsequence check). */
|
|
187
|
+
function isInOrderSubsequence(subset: string[], sequence: string[]): boolean {
|
|
188
|
+
let cursor = 0;
|
|
189
|
+
for (const item of subset) {
|
|
190
|
+
const found = sequence.indexOf(item, cursor);
|
|
191
|
+
if (found === -1) return false;
|
|
192
|
+
cursor = found + 1;
|
|
193
|
+
}
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
// Tests
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
describe('Input-Text native base-call alignment', () => {
|
|
202
|
+
const iosBaseSource = fs.readFileSync(IOS_BASE, 'utf-8');
|
|
203
|
+
const androidBaseSource = fs.readFileSync(ANDROID_BASE, 'utf-8');
|
|
204
|
+
|
|
205
|
+
const iosBaseProps = parseIOSBaseProperties(iosBaseSource);
|
|
206
|
+
const androidBaseParams = parseAndroidBaseParams(androidBaseSource);
|
|
207
|
+
const iosEnumMembers = parseInputTypeMembers(iosBaseSource, 'ios');
|
|
208
|
+
const androidEnumMembers = parseInputTypeMembers(androidBaseSource, 'android');
|
|
209
|
+
|
|
210
|
+
describe('base declaration parsing (sanity)', () => {
|
|
211
|
+
it('parses the iOS base memberwise properties', () => {
|
|
212
|
+
expect(iosBaseProps).toEqual(
|
|
213
|
+
expect.arrayContaining(['id', 'label', 'value', 'type', 'readOnly', 'trailingContent'])
|
|
214
|
+
);
|
|
215
|
+
expect(iosBaseProps.length).toBeGreaterThanOrEqual(15);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('parses the Android base composable parameters', () => {
|
|
219
|
+
expect(androidBaseParams).toEqual(
|
|
220
|
+
expect.arrayContaining(['id', 'label', 'value', 'type', 'visualTransformation', 'trailingContent'])
|
|
221
|
+
);
|
|
222
|
+
expect(androidBaseParams.length).toBeGreaterThanOrEqual(15);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('parses the InputType enums on both platforms', () => {
|
|
226
|
+
expect(iosEnumMembers).toEqual(['text', 'email', 'password', 'tel', 'url']);
|
|
227
|
+
expect(androidEnumMembers).toEqual(['TEXT', 'EMAIL', 'PASSWORD', 'TEL', 'URL']);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe('iOS callers', () => {
|
|
232
|
+
// The iOS base preview also exercises the memberwise initializer
|
|
233
|
+
const files = [...IOS_CALLERS, IOS_BASE];
|
|
234
|
+
|
|
235
|
+
files.forEach((file) => {
|
|
236
|
+
const name = path.basename(file);
|
|
237
|
+
|
|
238
|
+
it(`${name}: every InputTextBase argument is a declared property, in declaration order`, () => {
|
|
239
|
+
const source = fs.readFileSync(file, 'utf-8');
|
|
240
|
+
const calls = extractBaseCalls(source);
|
|
241
|
+
expect(calls.length).toBeGreaterThan(0);
|
|
242
|
+
|
|
243
|
+
calls.forEach((args) => {
|
|
244
|
+
const labels = args
|
|
245
|
+
.map((a) => argLabel(a, 'ios'))
|
|
246
|
+
.filter((l): l is string => Boolean(l));
|
|
247
|
+
|
|
248
|
+
const undeclared = labels.filter((l) => !iosBaseProps.includes(l));
|
|
249
|
+
expect(undeclared).toEqual([]);
|
|
250
|
+
|
|
251
|
+
// Memberwise init requires arguments in property declaration order
|
|
252
|
+
expect(isInOrderSubsequence(labels, iosBaseProps)).toBe(true);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it(`${name}: type argument references a declared InputType case`, () => {
|
|
257
|
+
const source = fs.readFileSync(file, 'utf-8');
|
|
258
|
+
extractBaseCalls(source).forEach((args) => {
|
|
259
|
+
const typeArg = args.find((a) => argLabel(a, 'ios') === 'type');
|
|
260
|
+
if (!typeArg) return;
|
|
261
|
+
const caseMatch = typeArg.match(/:\s*\.(\w+)/);
|
|
262
|
+
if (caseMatch) {
|
|
263
|
+
expect(iosEnumMembers).toContain(caseMatch[1]);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe('Android callers', () => {
|
|
271
|
+
ANDROID_CALLERS.forEach((file) => {
|
|
272
|
+
const name = path.basename(file);
|
|
273
|
+
|
|
274
|
+
it(`${name}: every InputTextBase named argument is a declared parameter`, () => {
|
|
275
|
+
const source = fs.readFileSync(file, 'utf-8');
|
|
276
|
+
const calls = extractBaseCalls(source);
|
|
277
|
+
expect(calls.length).toBeGreaterThan(0);
|
|
278
|
+
|
|
279
|
+
calls.forEach((args) => {
|
|
280
|
+
const labels = args
|
|
281
|
+
.map((a) => argLabel(a, 'android'))
|
|
282
|
+
.filter((l): l is string => Boolean(l));
|
|
283
|
+
expect(labels.length).toBeGreaterThan(0);
|
|
284
|
+
|
|
285
|
+
const undeclared = labels.filter((l) => !androidBaseParams.includes(l));
|
|
286
|
+
expect(undeclared).toEqual([]);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it(`${name}: every InputType reference is a declared enum member`, () => {
|
|
291
|
+
const source = fs.readFileSync(file, 'utf-8');
|
|
292
|
+
const refs = [...source.matchAll(/\bInputType\.(\w+)/g)].map((m) => m[1]);
|
|
293
|
+
const unknown = refs.filter((r) => !androidEnumMembers.includes(r));
|
|
294
|
+
expect(unknown).toEqual([]);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
});
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
|
|
18
18
|
import type { Oklch } from '../color/OklchConverter';
|
|
19
19
|
|
|
20
|
+
// 'disabled' is deprecated calculator capability: DesignerPunk supports no disabled
|
|
21
|
+
// states (adjudicated 2026-07-15) — no component may use it; removal at next major.
|
|
20
22
|
export type InteractionState = 'hover' | 'pressed' | 'focused' | 'disabled';
|
|
21
23
|
|
|
22
24
|
/** Thresholds for interaction state blends. */
|
|
@@ -24,6 +26,7 @@ export const INTERACTION_THRESHOLDS = {
|
|
|
24
26
|
hover: { deltaL: { min: 0.02, max: 0.05 }, deltaC: 0 },
|
|
25
27
|
pressed: { deltaL: { min: 0.05, max: 0.10 }, deltaC: 0 },
|
|
26
28
|
focused: { deltaL: 0, deltaC: { min: 0.02 } },
|
|
29
|
+
// deprecated 2026-07-15 (no-disabled-states philosophy) — removal at next major
|
|
27
30
|
disabled: { deltaL: 0, deltaC: { min: 0.03 } },
|
|
28
31
|
} as const;
|
|
29
32
|
|
|
@@ -37,6 +37,7 @@ object BlendTokenValues {
|
|
|
37
37
|
const val focusSaturate: Float = 0.08f
|
|
38
38
|
|
|
39
39
|
/** Disabled state desaturation - blend300 (12%) */
|
|
40
|
+
@Deprecated("DesignerPunk does not support disabled states (adjudicated 2026-07-15). Removal at next major.")
|
|
40
41
|
const val disabledDesaturate: Float = 0.12f
|
|
41
42
|
|
|
42
43
|
/** Icon optical balance lightening - blend200 (8%) */
|
|
@@ -163,6 +164,7 @@ fun Color.focusBlend(): Color {
|
|
|
163
164
|
*
|
|
164
165
|
* @see Requirements: 11.4 - Theme-aware wrapper functions
|
|
165
166
|
*/
|
|
167
|
+
@Deprecated("DesignerPunk does not support disabled states (adjudicated 2026-07-15) — if an action is unavailable, do not render the component. Use desaturate() for non-disabled desaturation. Removal at next major.")
|
|
166
168
|
fun Color.disabledBlend(): Color {
|
|
167
169
|
return this.desaturate(BlendTokenValues.disabledDesaturate)
|
|
168
170
|
}
|
|
@@ -280,6 +282,7 @@ object BlendUtilitiesProvider {
|
|
|
280
282
|
* @param color Base color
|
|
281
283
|
* @return Desaturated color for disabled state
|
|
282
284
|
*/
|
|
285
|
+
@Deprecated("DesignerPunk does not support disabled states (adjudicated 2026-07-15) — if an action is unavailable, do not render the component. Removal at next major.")
|
|
283
286
|
fun disabledColor(color: Color): Color {
|
|
284
287
|
return color.disabledBlend()
|
|
285
288
|
}
|
|
@@ -28,6 +28,7 @@ public struct BlendTokenValues {
|
|
|
28
28
|
public static let focusSaturate: Double = 0.08
|
|
29
29
|
|
|
30
30
|
/// Disabled state desaturation - blend300 (12%)
|
|
31
|
+
@available(*, deprecated, message: "DesignerPunk does not support disabled states (adjudicated 2026-07-15). Removal at next major.")
|
|
31
32
|
public static let disabledDesaturate: Double = 0.12
|
|
32
33
|
|
|
33
34
|
/// Icon optical balance lightening - blend200 (8%)
|
|
@@ -172,6 +173,7 @@ extension Color {
|
|
|
172
173
|
/// ```
|
|
173
174
|
///
|
|
174
175
|
/// @see Requirements: 11.4 - Theme-aware wrapper functions
|
|
176
|
+
@available(*, deprecated, message: "DesignerPunk does not support disabled states (adjudicated 2026-07-15) — if an action is unavailable, do not render the component. Use desaturate(_:) for non-disabled desaturation. Removal at next major.")
|
|
175
177
|
public func disabledBlend() -> Color {
|
|
176
178
|
return self.desaturate(BlendTokenValues.disabledDesaturate)
|
|
177
179
|
}
|
|
@@ -276,6 +278,7 @@ public class BlendUtilitiesProvider {
|
|
|
276
278
|
/// Calculate disabled color for a given base color
|
|
277
279
|
/// - Parameter color: Base color
|
|
278
280
|
/// - Returns: Desaturated color for disabled state
|
|
281
|
+
@available(*, deprecated, message: "DesignerPunk does not support disabled states (adjudicated 2026-07-15) — if an action is unavailable, do not render the component. Removal at next major.")
|
|
279
282
|
public func disabledColor(_ color: Color) -> Color {
|
|
280
283
|
return color.disabledBlend()
|
|
281
284
|
}
|
|
@@ -54,7 +54,11 @@ export const BlendTokenValues = {
|
|
|
54
54
|
pressedDarker: 0.12,
|
|
55
55
|
/** Focus state saturation increase - blend200 (8%) */
|
|
56
56
|
focusSaturate: 0.08,
|
|
57
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* Disabled state desaturation - blend300 (12%)
|
|
59
|
+
* @deprecated 2026-07-15 — DesignerPunk does not support disabled states; if an action
|
|
60
|
+
* is unavailable, do not render the component. Scheduled for removal at the next major.
|
|
61
|
+
*/
|
|
58
62
|
disabledDesaturate: 0.12,
|
|
59
63
|
/** Icon optical balance lightening - blend200 (8%) */
|
|
60
64
|
iconLighter: 0.08
|
|
@@ -89,6 +93,9 @@ export interface BlendUtilitiesResult {
|
|
|
89
93
|
* Calculate disabled color by desaturating the base color
|
|
90
94
|
* @param baseColor - Base color as hex string (e.g., "#A855F7")
|
|
91
95
|
* @returns Desaturated hex color string for disabled state
|
|
96
|
+
* @deprecated 2026-07-15 — DesignerPunk does not support disabled states; if an action
|
|
97
|
+
* is unavailable, do not render the component. For non-disabled desaturation, use the
|
|
98
|
+
* generic desaturate() utility. Scheduled for removal at the next major.
|
|
92
99
|
*/
|
|
93
100
|
disabledColor: (baseColor: string) => string;
|
|
94
101
|
|
|
@@ -242,6 +249,7 @@ export function createBlendUtilities(): BlendUtilitiesResult {
|
|
|
242
249
|
hoverColor: (baseColor: string) => darkerBlend(baseColor, BlendTokenValues.hoverDarker),
|
|
243
250
|
pressedColor: (baseColor: string) => darkerBlend(baseColor, BlendTokenValues.pressedDarker),
|
|
244
251
|
focusColor: (baseColor: string) => saturate(baseColor, BlendTokenValues.focusSaturate),
|
|
252
|
+
// deprecated 2026-07-15 (no-disabled-states philosophy) — removal at next major
|
|
245
253
|
disabledColor: (baseColor: string) => desaturate(baseColor, BlendTokenValues.disabledDesaturate),
|
|
246
254
|
iconColor: (baseColor: string) => lighterBlend(baseColor, BlendTokenValues.iconLighter),
|
|
247
255
|
|
|
@@ -13,7 +13,10 @@
|
|
|
13
13
|
* hover: ΔL ∈ [0.02, 0.05], preserve chroma
|
|
14
14
|
* pressed: ΔL ∈ [0.05, 0.10], preserve chroma
|
|
15
15
|
* focused: ΔC ≥ 0.02 (chroma boost)
|
|
16
|
-
* disabled: ΔC ≥ 0.03 (chroma reduction)
|
|
16
|
+
* disabled: ΔC ≥ 0.03 (chroma reduction) — calculator capability only;
|
|
17
|
+
* no component declares a disabled state (state_disabled removed
|
|
18
|
+
* from Button-CTA 2026-07-15, completing the no-disabled-states
|
|
19
|
+
* philosophy across the corpus)
|
|
17
20
|
*
|
|
18
21
|
* Components audited:
|
|
19
22
|
* Button-CTA, Button-Icon, Button-VerticalList-Item, Chip-Base,
|
|
@@ -125,14 +128,14 @@ describe('Spec 112 Task 5.1: Interaction State Visual Audit', () => {
|
|
|
125
128
|
});
|
|
126
129
|
|
|
127
130
|
describe('Disabled state (ΔC ≥ 0.03 reduction)', () => {
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
it
|
|
134
|
-
const result = calc.interactionBlend(
|
|
135
|
-
const dc = deltaC(
|
|
131
|
+
// No component declares a disabled state (no-disabled-states philosophy).
|
|
132
|
+
// blend.disabledDesaturate was deprecated 2026-07-15 (PR #83, Button-CTA
|
|
133
|
+
// disabled-state adjudication). This test validates the calculator
|
|
134
|
+
// capability only, retained for backward compatibility; remove at the
|
|
135
|
+
// next major version alongside the token itself.
|
|
136
|
+
it('calculator disabled blend — ΔC reduction meets minimum', () => {
|
|
137
|
+
const result = calc.interactionBlend(primaryButton, 'disabled', lightSurface);
|
|
138
|
+
const dc = deltaC(primaryButton, result);
|
|
136
139
|
// dc should be negative (chroma reduced), absolute value ≥ 0.03
|
|
137
140
|
expect(dc).toBeLessThan(0);
|
|
138
141
|
expect(Math.abs(dc)).toBeGreaterThanOrEqual(INTERACTION_THRESHOLDS.disabled.deltaC.min);
|
|
@@ -13,6 +13,14 @@ import { createBuildError } from '../BuildError';
|
|
|
13
13
|
import { BuildResultSummary } from '../ErrorReporter';
|
|
14
14
|
|
|
15
15
|
describe('ErrorHandler - ErrorReporter Integration', () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
jest.restoreAllMocks();
|
|
22
|
+
});
|
|
23
|
+
|
|
16
24
|
describe('generateReport', () => {
|
|
17
25
|
it('should generate report from logged errors', () => {
|
|
18
26
|
const handler = new ErrorHandler();
|
|
@@ -13,9 +13,14 @@ describe('ErrorHandler', () => {
|
|
|
13
13
|
let errorHandler: ErrorHandler;
|
|
14
14
|
|
|
15
15
|
beforeEach(() => {
|
|
16
|
+
jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
16
17
|
errorHandler = new ErrorHandler();
|
|
17
18
|
});
|
|
18
19
|
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
jest.restoreAllMocks();
|
|
22
|
+
});
|
|
23
|
+
|
|
19
24
|
describe('constructor', () => {
|
|
20
25
|
it('should create ErrorHandler with default options', () => {
|
|
21
26
|
const handler = new ErrorHandler();
|
|
@@ -392,6 +392,119 @@ describe('defineComponentTokens', () => {
|
|
|
392
392
|
});
|
|
393
393
|
});
|
|
394
394
|
|
|
395
|
+
describe('Family-mismatch guard (reference path)', () => {
|
|
396
|
+
// A reference literal that carries `category`, as every real primitive in
|
|
397
|
+
// src/tokens/** does. The guard keys off this field.
|
|
398
|
+
const createMockPrimitiveWithCategory = (
|
|
399
|
+
name: string,
|
|
400
|
+
baseValue: number,
|
|
401
|
+
category: string
|
|
402
|
+
): PrimitiveTokenReference => ({ name, baseValue, category });
|
|
403
|
+
|
|
404
|
+
test('throws when a reference primitive belongs to a different family than declared', () => {
|
|
405
|
+
const size600 = createMockPrimitiveWithCategory('size600', 48, 'sizing');
|
|
406
|
+
|
|
407
|
+
expect(() =>
|
|
408
|
+
defineComponentTokens({
|
|
409
|
+
component: 'ButtonIcon',
|
|
410
|
+
family: 'spacing',
|
|
411
|
+
tokens: {
|
|
412
|
+
'size.large': { reference: size600, reasoning: 'Large button size' },
|
|
413
|
+
},
|
|
414
|
+
})
|
|
415
|
+
).toThrow(/Token family mismatch/);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test('error names the component, token, declared family, primitive and its real family', () => {
|
|
419
|
+
const size600 = createMockPrimitiveWithCategory('size600', 48, 'sizing');
|
|
420
|
+
|
|
421
|
+
let message = '';
|
|
422
|
+
try {
|
|
423
|
+
defineComponentTokens({
|
|
424
|
+
component: 'ButtonIcon',
|
|
425
|
+
family: 'spacing',
|
|
426
|
+
tokens: {
|
|
427
|
+
'size.large': { reference: size600, reasoning: 'Large button size' },
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
} catch (error) {
|
|
431
|
+
message = (error as Error).message;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
expect(message).toContain("component 'ButtonIcon'");
|
|
435
|
+
expect(message).toContain("token 'size.large'");
|
|
436
|
+
expect(message).toContain("'spacing' family call");
|
|
437
|
+
expect(message).toContain("'size600'");
|
|
438
|
+
expect(message).toContain("'sizing' family");
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test('regression: the real Button-Icon defect (PR #126) now fails at authoring time', () => {
|
|
442
|
+
// Before PR #126, buttonIcon.tokens.ts declared family 'spacing' while referencing
|
|
443
|
+
// sizing primitives. That produced `SpacingTokens.size600` — a member no generated
|
|
444
|
+
// platform file defines — and was caught only by reading generated Swift/Kotlin.
|
|
445
|
+
expect(() =>
|
|
446
|
+
defineComponentTokens({
|
|
447
|
+
component: 'ButtonIcon',
|
|
448
|
+
family: 'spacing',
|
|
449
|
+
tokens: {
|
|
450
|
+
'inset.large': {
|
|
451
|
+
reference: createMockPrimitiveWithCategory('space150', 12, 'spacing'),
|
|
452
|
+
reasoning: 'Correctly family-matched spacing token',
|
|
453
|
+
},
|
|
454
|
+
'size.large': {
|
|
455
|
+
reference: createMockPrimitiveWithCategory('size600', 48, 'sizing'),
|
|
456
|
+
reasoning: 'Mis-stamped sizing token — the defect',
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
})
|
|
460
|
+
).toThrow(/references primitive 'size600' from the 'sizing' family/);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
test('accepts a reference whose category matches the declared family', () => {
|
|
464
|
+
const size600 = createMockPrimitiveWithCategory('size600', 48, 'sizing');
|
|
465
|
+
|
|
466
|
+
const result = defineComponentTokens({
|
|
467
|
+
component: 'ButtonIcon',
|
|
468
|
+
family: 'sizing',
|
|
469
|
+
tokens: {
|
|
470
|
+
'size.large': { reference: size600, reasoning: 'Large button size' },
|
|
471
|
+
},
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
expect(result['size.large']).toBe(48);
|
|
475
|
+
expect((getTokenContract(result) ?? [])[0].family).toBe('sizing');
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
test('does not fire for reference literals without a category (back-compat)', () => {
|
|
479
|
+
const bareRef = createMockPrimitiveToken('space100', 8);
|
|
480
|
+
|
|
481
|
+
expect(() =>
|
|
482
|
+
defineComponentTokens({
|
|
483
|
+
component: 'ButtonIcon',
|
|
484
|
+
family: 'sizing',
|
|
485
|
+
tokens: {
|
|
486
|
+
'inset.small': { reference: bareRef, reasoning: 'Bare reference literal' },
|
|
487
|
+
},
|
|
488
|
+
})
|
|
489
|
+
).not.toThrow();
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
test('does not fire for value-path tokens (the Avatar icon-size case is NOT covered)', () => {
|
|
493
|
+
// Documents a real limitation: the Avatar `icon.size.*` gap fillers were mis-stamped
|
|
494
|
+
// `family: 'spacing'` on the VALUE path. No reference exists to cross-check, so this
|
|
495
|
+
// guard cannot catch that class of mislabel.
|
|
496
|
+
expect(() =>
|
|
497
|
+
defineComponentTokens({
|
|
498
|
+
component: 'Avatar',
|
|
499
|
+
family: 'spacing',
|
|
500
|
+
tokens: {
|
|
501
|
+
'icon.size.xs': { value: 12, reasoning: 'Dimensional value in a spacing call' },
|
|
502
|
+
},
|
|
503
|
+
})
|
|
504
|
+
).not.toThrow();
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
|
|
395
508
|
describe('Idempotent re-branding (Spec 124, caveat c)', () => {
|
|
396
509
|
test('re-applying the brand to the same return does not throw', () => {
|
|
397
510
|
const result = defineComponentTokens({
|
|
@@ -65,6 +65,16 @@ export interface PrimitiveTokenReference {
|
|
|
65
65
|
name: string;
|
|
66
66
|
/** Unitless base value from the primitive token */
|
|
67
67
|
baseValue: number;
|
|
68
|
+
/**
|
|
69
|
+
* Primitive token family (the `category` field of a `PrimitiveToken`, e.g. 'spacing',
|
|
70
|
+
* 'sizing', 'radius'). OPTIONAL for backward compatibility: minimal `{ name, baseValue }`
|
|
71
|
+
* reference literals remain valid.
|
|
72
|
+
*
|
|
73
|
+
* When present, it is cross-checked against the call's declared `family` by the
|
|
74
|
+
* family-mismatch guard in {@link defineComponentTokens}. Real primitives from
|
|
75
|
+
* src/tokens/** always carry it, so the guard is active for all production authoring.
|
|
76
|
+
*/
|
|
77
|
+
category?: string;
|
|
68
78
|
}
|
|
69
79
|
|
|
70
80
|
/**
|
|
@@ -216,8 +226,40 @@ export function defineComponentTokens<T extends Record<string, TokenDefinition>>
|
|
|
216
226
|
if (isTokenWithReference(definition)) {
|
|
217
227
|
// Token with primitive reference
|
|
218
228
|
const primitiveToken = definition.reference;
|
|
229
|
+
|
|
230
|
+
// FAMILY-MISMATCH GUARD.
|
|
231
|
+
// The call's `family` is stamped onto every token it registers, and the generator
|
|
232
|
+
// derives platform output from it — notably the primitive class name used for
|
|
233
|
+
// reference-path tokens (getFamilyClassName) and the Android `.dp` suffix. A call
|
|
234
|
+
// that declares one family but references another family's primitive therefore emits
|
|
235
|
+
// a member that does not exist on the target platform. That is exactly the Button-Icon
|
|
236
|
+
// defect: a `family: 'spacing'` call referencing `sizingTokens.size600` generated
|
|
237
|
+
// `SpacingTokens.size600`, a non-existent member (PR #126).
|
|
238
|
+
//
|
|
239
|
+
// Only enforced when the reference carries a `category` — minimal `{ name, baseValue }`
|
|
240
|
+
// literals (used widely in tests) are intentionally exempt rather than rejected.
|
|
241
|
+
//
|
|
242
|
+
// Escape hatch: a token that legitimately needs another family's VALUE should use the
|
|
243
|
+
// value path (`value:`), which asserts no cross-family token-chain claim.
|
|
244
|
+
if (
|
|
245
|
+
typeof primitiveToken.category === 'string' &&
|
|
246
|
+
primitiveToken.category !== family
|
|
247
|
+
) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
`Token family mismatch in defineComponentTokens() for component '${component}': ` +
|
|
250
|
+
`token '${key}' is declared in a '${family}' family call but references primitive ` +
|
|
251
|
+
`'${primitiveToken.name}' from the '${primitiveToken.category}' family. ` +
|
|
252
|
+
`Every token in a call is stamped with that call's family, which drives platform ` +
|
|
253
|
+
`output (e.g. the generated '${family.charAt(0).toUpperCase() + family.slice(1)}Tokens' ` +
|
|
254
|
+
`class reference), so this would emit a non-existent platform member. ` +
|
|
255
|
+
`Fix: move '${key}' into a separate defineComponentTokens() call with ` +
|
|
256
|
+
`family: '${primitiveToken.category}', or use the value path if no token-chain ` +
|
|
257
|
+
`relationship is intended.`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
219
261
|
const value = primitiveToken.baseValue;
|
|
220
|
-
|
|
262
|
+
|
|
221
263
|
values[key] = value;
|
|
222
264
|
registeredTokens.push({
|
|
223
265
|
name: tokenName,
|
|
@@ -490,9 +490,20 @@ describe('CICDIntegration', () => {
|
|
|
490
490
|
|
|
491
491
|
describe('Environment Detection', () => {
|
|
492
492
|
const originalEnv = process.env;
|
|
493
|
-
|
|
493
|
+
|
|
494
494
|
beforeEach(() => {
|
|
495
495
|
process.env = { ...originalEnv };
|
|
496
|
+
// Neutralize the HOST's own CI environment before each scenario. On a real CI
|
|
497
|
+
// runner (e.g. GitHub Actions) the ambient GITHUB_ACTIONS/CI vars leak into the
|
|
498
|
+
// copied env and win detectCICDEnvironment()'s precedence order, so the GitLab
|
|
499
|
+
// and generic-CI scenarios detected 'github' instead — these tests only ever
|
|
500
|
+
// passed on laptops because laptops aren't CI (125-A Task 6, lane-timing run #2
|
|
501
|
+
// finding). Each scenario sets exactly the vars it means to test.
|
|
502
|
+
delete process.env.GITHUB_ACTIONS;
|
|
503
|
+
delete process.env.GITHUB_WORKFLOW;
|
|
504
|
+
delete process.env.GITLAB_CI;
|
|
505
|
+
delete process.env.CI_PIPELINE_ID;
|
|
506
|
+
delete process.env.CI;
|
|
496
507
|
});
|
|
497
508
|
|
|
498
509
|
afterEach(() => {
|