@yschimke/compose-design-map 0.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.
- package/LICENSE +201 -0
- package/README.md +110 -0
- package/design-map.mjs +259 -0
- package/emit-design-map.mjs +155 -0
- package/package.json +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright [yyyy] [name of copyright owner]
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# @yschimke/compose-design-map
|
|
2
|
+
|
|
3
|
+
`design-map.json` is [design-parity](https://github.com/yschimke/design-parity)'s correspondence
|
|
4
|
+
file: it says which design node a code component is meant to look like. This package **writes**
|
|
5
|
+
one, from the catalog annotations [compose-ai-tools](https://github.com/yschimke/compose-ai-tools)
|
|
6
|
+
defines.
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
./gradlew :<module>:composePreviewDiscover
|
|
10
|
+
npx --yes @yschimke/compose-design-map \
|
|
11
|
+
--previews <module>/build/compose-previews/previews.json
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Dependency-free and Node-only, so `npx` is the whole install. Both outputs are generated —
|
|
15
|
+
regenerate rather than edit. `--check` regenerates in memory and exits non-zero if a committed copy
|
|
16
|
+
has drifted, which is the CI posture.
|
|
17
|
+
|
|
18
|
+
Pin the version in CI. Both files are committed and checked, so the projection's version is an
|
|
19
|
+
input to a checked-in artifact: float it, and a release here turns a downstream repo red for a
|
|
20
|
+
change nobody there made.
|
|
21
|
+
|
|
22
|
+
## Why the producer lives here
|
|
23
|
+
|
|
24
|
+
Every field the projection reads is defined in this repository:
|
|
25
|
+
|
|
26
|
+
| Field on `previews.json` | Declared by |
|
|
27
|
+
| --- | --- |
|
|
28
|
+
| `catalog.reference`, `referenceSet`, `noReference`, `referenceContentsOnly` | [`@CatalogComponent`](https://github.com/yschimke/compose-ai-tools/blob/main/api/preview-annotations/src/commonMain/kotlin/ee/schimke/composeai/preview/CatalogComponent.kt) |
|
|
29
|
+
| `catalog.props`, `catalog.state` | `@CatalogVariant` |
|
|
30
|
+
| `overrides.seeds`, `overrides.props` | `@OverrideVariant` / `@PreviewAxis` |
|
|
31
|
+
|
|
32
|
+
Rename one of those and the projection has to change in the same commit. Keeping the two on
|
|
33
|
+
opposite sides of a repo boundary is how a manifest reader goes quietly stale — and the reference
|
|
34
|
+
belongs on the annotation rather than in a JSON map for the same reason: a map keyed on preview
|
|
35
|
+
names drifts the moment a preview is renamed, and fails silently when it does.
|
|
36
|
+
|
|
37
|
+
The consuming half already lived here too — [`design-references.mjs`](https://github.com/yschimke/compose-ai-tools/blob/main/scripts/design-artifacts/design-references.mjs)
|
|
38
|
+
reads a `design-map.json` to build a published catalog's `references/index.json`. Until now nothing
|
|
39
|
+
in the ecosystem wrote one except a hand-maintained script in a downstream catalog repo.
|
|
40
|
+
|
|
41
|
+
## Where the split falls
|
|
42
|
+
|
|
43
|
+
A catalog picturing `Button` at three sizes and two shapes has six renders and **one** reference.
|
|
44
|
+
Pairing the other five means answering "which kit node is `size=l`?" — and that is not a question
|
|
45
|
+
this repo can answer:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
previews.json
|
|
49
|
+
│
|
|
50
|
+
│ compose-design-map ← THIS PACKAGE. Knows what the annotations mean.
|
|
51
|
+
│
|
|
52
|
+
├──▶ design-map.json base references, one per component. Valid on its own.
|
|
53
|
+
│
|
|
54
|
+
└──▶ design-map-variants.json "these previews are the same component with these knobs
|
|
55
|
+
turned" — unresolved, because `size=l` is a fact about
|
|
56
|
+
the Compose API and `Size=Large` is a fact about somebody's
|
|
57
|
+
design kit
|
|
58
|
+
│
|
|
59
|
+
│ @design-parity/kit-index ← THE OTHER REPO. Knows what the KIT means.
|
|
60
|
+
▼
|
|
61
|
+
design-map.json with a tagged ref/previewId pair per variant
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`size=l` → `Size=Large` is a translation against a kit's published vocabulary. That vocabulary is a
|
|
65
|
+
Figma concern, it needs a Figma credential to derive, and it differs per kit — none of which this
|
|
66
|
+
repo has any business holding. So the variant renders come out as **declarations** and a resolver
|
|
67
|
+
that owns a kit index turns them into node ids.
|
|
68
|
+
|
|
69
|
+
The two halves are separable because the first is useful alone: a repo with no kit index still gets
|
|
70
|
+
a valid map of base references, which is most of the value at none of the cost.
|
|
71
|
+
|
|
72
|
+
## The sidecar
|
|
73
|
+
|
|
74
|
+
`design-map-variants.json` carries `schema: "compose-preview-design-map-variants/v1"`; a resolver
|
|
75
|
+
must match that string before reading it. One entry per component that has variant renders:
|
|
76
|
+
|
|
77
|
+
```jsonc
|
|
78
|
+
{
|
|
79
|
+
"schema": "compose-preview-design-map-variants/v1",
|
|
80
|
+
"components": [
|
|
81
|
+
{
|
|
82
|
+
"code": "catalog/Catalog.kt#FilledButton", // the design-map entry these belong to
|
|
83
|
+
"componentId": "Button/Filled",
|
|
84
|
+
"reference": "figma:AbCdEf/1:2", // the node a resolver walks from
|
|
85
|
+
"basePreviewId": "…FilledButton_Light",
|
|
86
|
+
"renders": [
|
|
87
|
+
{ "previewId": "…FilledButton_Light_VARIANT_l", "name": "l",
|
|
88
|
+
"seeds": [{ "key": "size", "raw": "l" }, { "key": "shape", "raw": "round" }] }
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
It is a separate file rather than another key on the map because the design-map schema sets
|
|
96
|
+
`additionalProperties: false` — a map carrying an extra key would fail its own validator. No file is
|
|
97
|
+
written when nothing declares an axis.
|
|
98
|
+
|
|
99
|
+
## Two things worth knowing
|
|
100
|
+
|
|
101
|
+
**Only the light capture is mapped.** One entry per component, not per rendered mode, and the light
|
|
102
|
+
one — because that is the mode design kits draw their frames in. Diffing a dark render against a
|
|
103
|
+
light reference reports the whole palette as a finding.
|
|
104
|
+
|
|
105
|
+
**`overrides.props` beats `overrides.seeds` where both exist.** They are not the same list. `seeds`
|
|
106
|
+
holds only the values that differ from the composable's defaults; `props` — emitted for a
|
|
107
|
+
`@PreviewAxis` cross product — carries the full axis assignment, defaults included. A cell that
|
|
108
|
+
knows its own axes pairs by construction, which is exactly what `OverrideVariantSpec.props` was
|
|
109
|
+
added for. A cell described only by its non-default seeds is missing the axes it happens to sit at,
|
|
110
|
+
and a kit that spells its default size explicitly in a combination cell then has nothing to match.
|
package/design-map.mjs
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project a discovery manifest into a `design-map.json` — the correspondence file design-parity
|
|
3
|
+
* reads to know which design node a code component is meant to look like.
|
|
4
|
+
*
|
|
5
|
+
* ## Why this is a projection and not a config file
|
|
6
|
+
*
|
|
7
|
+
* design-parity joins a code subject to a design reference through a `design-map.json` entry:
|
|
8
|
+
* `{ code, source, ref, previewId }`. Hand-maintaining that for a catalog's worth of previews is
|
|
9
|
+
* exactly the mapping-config sprawl a catalog exists to avoid — and it drifts the moment a preview
|
|
10
|
+
* is renamed, silently, because the join keys on the fully-qualified preview id.
|
|
11
|
+
*
|
|
12
|
+
* So the map is DERIVED. Every catalogued component already carries its seed-kit handle on the
|
|
13
|
+
* annotation this repo defines:
|
|
14
|
+
*
|
|
15
|
+
* @CatalogComponent(id = "Button/Filled", reference = "figma:<fileKey>/<nodeId>")
|
|
16
|
+
*
|
|
17
|
+
* `composePreviewDiscover` writes that through to `previews.json` as `catalog.reference`, so this
|
|
18
|
+
* module is a pure projection of the annotations. Keeping the ref in code is the point: a JSON map
|
|
19
|
+
* keyed on preview names drifts when a preview is renamed, and fails silently when it does.
|
|
20
|
+
*
|
|
21
|
+
* ## Why this lives HERE and not in design-parity
|
|
22
|
+
*
|
|
23
|
+
* Every field it reads — `catalog.reference`, `referenceSet`, `noReference`,
|
|
24
|
+
* `referenceContentsOnly`, `catalog.props`, `overrides.seeds` — is defined in this repository, by
|
|
25
|
+
* `@CatalogComponent` / `@CatalogVariant` / `@OverrideVariant` and emitted by this repository's
|
|
26
|
+
* discovery. Rename a field and the projection has to change in the same commit; putting the two
|
|
27
|
+
* on opposite sides of a repo boundary is how a manifest reader goes quietly stale.
|
|
28
|
+
*
|
|
29
|
+
* ## What this deliberately does NOT do
|
|
30
|
+
*
|
|
31
|
+
* It does not resolve a variant knob to a design node. `size=l` is a fact about the Compose API;
|
|
32
|
+
* `Size=Large` is a fact about somebody's design kit, and translating between them needs that kit's
|
|
33
|
+
* published vocabulary — which this repo has no business holding, and which
|
|
34
|
+
* [`@design-parity/kit-index`](https://github.com/yschimke/design-parity/tree/main/packages/kit-index)
|
|
35
|
+
* does hold.
|
|
36
|
+
*
|
|
37
|
+
* So the variant renders come out as **declarations**, in a sidecar
|
|
38
|
+
* ({@link DESIGN_MAP_VARIANTS_SCHEMA}): "this preview is the same component with these knobs
|
|
39
|
+
* turned". A resolver that owns a kit index turns each into a tagged `ref`/`previewId` pair beside
|
|
40
|
+
* the base one. A repo with no kit index still gets a valid map of base references, which is the
|
|
41
|
+
* majority of the value and costs no design-tool credential.
|
|
42
|
+
*
|
|
43
|
+
* The sidecar is a separate file rather than another key on the map because the design-map schema
|
|
44
|
+
* sets `additionalProperties: false` — a map carrying an extra key would fail its own validator.
|
|
45
|
+
*
|
|
46
|
+
* Pure and dependency-free (no `@design-parity/*`, no I/O) so it unit-tests without an `npm ci`,
|
|
47
|
+
* like its siblings `catalog-image-path.mjs` / `catalog-variants.mjs`. The I/O around it is
|
|
48
|
+
* `emit-design-map.mjs`.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/** The sidecar `schema` string a resolver must match before reading variant declarations. */
|
|
52
|
+
export const DESIGN_MAP_VARIANTS_SCHEMA = "compose-preview-design-map-variants/v1";
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The capture a component's base reference pairs with.
|
|
56
|
+
*
|
|
57
|
+
* One entry per component, not per rendered mode — and the LIGHT capture, because that is the mode
|
|
58
|
+
* design kits draw their frames in. Diffing a dark render against a light reference reports the
|
|
59
|
+
* whole palette as a finding.
|
|
60
|
+
*/
|
|
61
|
+
const LIGHT_CAPTURE = /_Light$/;
|
|
62
|
+
|
|
63
|
+
/** A light capture that is also an `@OverrideVariant` render: `…_Light_VARIANT_<name>`. */
|
|
64
|
+
const LIGHT_VARIANT_CAPTURE = /_Light_VARIANT_/;
|
|
65
|
+
|
|
66
|
+
/** design-parity addresses a code subject as `<path>#<function>`. */
|
|
67
|
+
export function codeHandle(preview, { prefix = "catalog" } = {}) {
|
|
68
|
+
const path = preview.sourceFile ? `${prefix}/${preview.sourceFile}` : prefix;
|
|
69
|
+
return `${path}#${preview.functionName}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The design source a reference handle names. design-parity dispatches its adapter on this, so a
|
|
74
|
+
* wrong answer picks a driver that cannot read the ref at all.
|
|
75
|
+
*/
|
|
76
|
+
export function sourceForRef(ref) {
|
|
77
|
+
const scheme = String(ref).split(":")[0];
|
|
78
|
+
return scheme === "figma" ? "figma" : "claude-design";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The knobs one variant render turns, normalised to `{ key, raw }`.
|
|
83
|
+
*
|
|
84
|
+
* A variant reaches us two ways and both NAME their axis — nothing is inferred from a function
|
|
85
|
+
* name:
|
|
86
|
+
*
|
|
87
|
+
* `@OverrideVariant(name = "l", strings = ["size=l"])` — a reseeded render of the same
|
|
88
|
+
* composable. Arrives as role COMPONENT with `_VARIANT_` in the id and the knobs on
|
|
89
|
+
* `overrides`.
|
|
90
|
+
*
|
|
91
|
+
* `@CatalogVariant(of = "Fab/Standard", props = ["size=large"])` — its own composable, because
|
|
92
|
+
* the difference is more than a knob. Arrives as role VARIANT with the knobs in `catalog.props`.
|
|
93
|
+
*
|
|
94
|
+
* For the first form, `overrides.props` is preferred over `overrides.seeds` when present. They are
|
|
95
|
+
* not the same list: `seeds` holds only the values that differ from the composable's defaults,
|
|
96
|
+
* while `props` — emitted for a `@PreviewAxis` cross product — carries the FULL axis assignment,
|
|
97
|
+
* defaults included. A cell that knows its own axes pairs by construction; one described only by
|
|
98
|
+
* its non-default seeds is missing the axes it happens to sit at, and a kit that spells its default
|
|
99
|
+
* size explicitly in a combination cell then has nothing to match against.
|
|
100
|
+
*/
|
|
101
|
+
export function variantSeeds(preview) {
|
|
102
|
+
const catalog = preview.catalog;
|
|
103
|
+
if (catalog?.role === "VARIANT") {
|
|
104
|
+
// `props` names the axis; `state` is the annotation's shorthand for the one axis common enough
|
|
105
|
+
// to have its own parameter. Either is a declaration, so neither is inferred —
|
|
106
|
+
// `@CatalogVariant(state = "disabled")` says the state axis as plainly as
|
|
107
|
+
// `props = ["state=disabled"]` would.
|
|
108
|
+
const props = [...(catalog.props ?? [])];
|
|
109
|
+
if (catalog.state && !props.some((p) => p.key === "state")) {
|
|
110
|
+
props.push({ key: "state", value: catalog.state });
|
|
111
|
+
}
|
|
112
|
+
return props.map((p) => ({ key: p.key, raw: p.value }));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const overrides = preview.overrides;
|
|
116
|
+
if (!overrides) return [];
|
|
117
|
+
|
|
118
|
+
const seeds = overrides.props?.length
|
|
119
|
+
? overrides.props.map((p) => ({ key: p.key, raw: p.value }))
|
|
120
|
+
: (overrides.seeds ?? []).map((s) => ({ key: s.key, raw: s.raw }));
|
|
121
|
+
|
|
122
|
+
// An interaction variant seeds no knob — the renderer drives real hover, focus or press against
|
|
123
|
+
// the composed node instead, so the difference lives in the harness rather than in the data.
|
|
124
|
+
// A design kit models it as a value of the same `State` axis that carries Enabled and Disabled,
|
|
125
|
+
// so it enters resolution as a seed of the `state` knob and reaches the kit through the alias
|
|
126
|
+
// that knob already has. Without this the variant declares nothing: `seeds` is empty, an empty
|
|
127
|
+
// vector matches every sibling, and the render is dropped as "names no axis".
|
|
128
|
+
const interaction = overrides.interaction;
|
|
129
|
+
if (interaction && interaction !== "None" && !seeds.some((s) => s.key === "state")) {
|
|
130
|
+
seeds.push({ key: "state", raw: String(interaction).toLowerCase() });
|
|
131
|
+
}
|
|
132
|
+
return seeds;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The name a variant render goes by, for a report and for the design-map `state` slot. */
|
|
136
|
+
function variantName(preview, seeds) {
|
|
137
|
+
const catalog = preview.catalog;
|
|
138
|
+
if (catalog?.role === "VARIANT") {
|
|
139
|
+
return catalog.state ?? seeds.map((s) => s.raw).join("-");
|
|
140
|
+
}
|
|
141
|
+
return preview.overrides?.name ?? seeds.map((s) => `${s.key}=${s.raw}`).join(", ");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Every variant render, grouped by the component it folds under.
|
|
146
|
+
*
|
|
147
|
+
* Both annotation forms are collected. The `@CatalogVariant` form was invisible to the first cut of
|
|
148
|
+
* this projection, which is why a FAB size axis read as unauthored while `FabSmall`/`FabMedium`/
|
|
149
|
+
* `FabLarge` sat in the catalog all along.
|
|
150
|
+
*/
|
|
151
|
+
export function variantRendersByComponent(previews) {
|
|
152
|
+
const byComponent = new Map();
|
|
153
|
+
for (const preview of previews) {
|
|
154
|
+
const catalog = preview.catalog;
|
|
155
|
+
if (!catalog) continue;
|
|
156
|
+
|
|
157
|
+
// An `@OverrideVariant` render is a reseed of the SAME composable, so it keeps the parent's
|
|
158
|
+
// COMPONENT role and is distinguished only by the `_VARIANT_` tag discovery puts in its id.
|
|
159
|
+
// A `@CatalogVariant` render is its own composable, so it carries the VARIANT role and an
|
|
160
|
+
// ordinary light-capture id. Either way only the light capture participates.
|
|
161
|
+
const isOverrideVariant =
|
|
162
|
+
catalog.role === "COMPONENT" && LIGHT_VARIANT_CAPTURE.test(preview.id);
|
|
163
|
+
const isCatalogVariant = catalog.role === "VARIANT" && LIGHT_CAPTURE.test(preview.id);
|
|
164
|
+
if (!isOverrideVariant && !isCatalogVariant) continue;
|
|
165
|
+
|
|
166
|
+
// A variant that names no axis says only "this is different", which is not enough to look
|
|
167
|
+
// anything up in a kit. Dropped rather than guessed at from the function name.
|
|
168
|
+
const seeds = variantSeeds(preview);
|
|
169
|
+
if (!seeds.length) continue;
|
|
170
|
+
|
|
171
|
+
const list = byComponent.get(catalog.componentId) ?? [];
|
|
172
|
+
list.push({ previewId: preview.id, name: variantName(preview, seeds), seeds });
|
|
173
|
+
byComponent.set(catalog.componentId, list);
|
|
174
|
+
}
|
|
175
|
+
return byComponent;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Project a discovery manifest into a design map plus its unresolved variant declarations.
|
|
180
|
+
*
|
|
181
|
+
* @param {Array<object>} previews `previews.json`'s `previews` array.
|
|
182
|
+
* @param {{prefix?: string}} [opts] `prefix` is the path segment prepended to each `sourceFile` to
|
|
183
|
+
* form the code handle — the module the previews live in, as a reviewer would name it.
|
|
184
|
+
* @returns {{map: object, variants: object, diagnostics: object}} the map, the sidecar, and what
|
|
185
|
+
* was skipped and why. Nothing is thrown for a missing reference: an unmapped component is a
|
|
186
|
+
* fact to report, not a failure.
|
|
187
|
+
*/
|
|
188
|
+
export function projectDesignMap(previews, opts = {}) {
|
|
189
|
+
const variantRenders = variantRendersByComponent(previews);
|
|
190
|
+
|
|
191
|
+
const components = [];
|
|
192
|
+
const declarations = [];
|
|
193
|
+
/** Components carrying neither a reference nor a stated reason for its absence. */
|
|
194
|
+
const unmapped = [];
|
|
195
|
+
/**
|
|
196
|
+
* Components whose reference is absent for a STATED reason. Reported apart from `unmapped`
|
|
197
|
+
* because they are the opposite situation: someone looked, and what they found is that the kit
|
|
198
|
+
* has nothing live to point at. Rolling the two together is what made a retired pattern read as
|
|
199
|
+
* neglect.
|
|
200
|
+
*/
|
|
201
|
+
const statedAbsent = [];
|
|
202
|
+
|
|
203
|
+
for (const preview of previews) {
|
|
204
|
+
const catalog = preview.catalog;
|
|
205
|
+
if (!catalog || catalog.role !== "COMPONENT") continue;
|
|
206
|
+
if (!LIGHT_CAPTURE.test(preview.id)) continue;
|
|
207
|
+
|
|
208
|
+
if (!catalog.reference) {
|
|
209
|
+
if (catalog.noReference) {
|
|
210
|
+
statedAbsent.push({ componentId: catalog.componentId, reason: catalog.noReference });
|
|
211
|
+
} else {
|
|
212
|
+
unmapped.push(catalog.componentId);
|
|
213
|
+
}
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const code = codeHandle(preview, opts);
|
|
218
|
+
components.push({
|
|
219
|
+
code,
|
|
220
|
+
source: sourceForRef(catalog.reference),
|
|
221
|
+
ref: catalog.reference,
|
|
222
|
+
// The component SET, when the annotation names one. `ref` stays the one variant parity diffs
|
|
223
|
+
// against; `refSet` is what a whole-screen import matches an instance through, since a screen
|
|
224
|
+
// rarely uses the exact variant this sticker pictures. Absent unless the annotation says so.
|
|
225
|
+
...(catalog.referenceSet ? { refSet: catalog.referenceSet } : {}),
|
|
226
|
+
// Figma normally exports only the referenced node. Preserve an explicit per-component opt-out
|
|
227
|
+
// when the annotation says this reference intentionally relies on overlapping sheet content.
|
|
228
|
+
...(catalog.referenceContentsOnly === false ? { referenceContentsOnly: false } : {}),
|
|
229
|
+
previewId: preview.id,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const renders = variantRenders.get(catalog.componentId) ?? [];
|
|
233
|
+
if (renders.length) {
|
|
234
|
+
declarations.push({
|
|
235
|
+
code,
|
|
236
|
+
componentId: catalog.componentId,
|
|
237
|
+
reference: catalog.reference,
|
|
238
|
+
basePreviewId: preview.id,
|
|
239
|
+
renders,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
components.sort((a, b) => a.code.localeCompare(b.code));
|
|
245
|
+
declarations.sort((a, b) => a.code.localeCompare(b.code));
|
|
246
|
+
unmapped.sort();
|
|
247
|
+
statedAbsent.sort((a, b) => a.componentId.localeCompare(b.componentId));
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
map: { components },
|
|
251
|
+
variants: { schema: DESIGN_MAP_VARIANTS_SCHEMA, components: declarations },
|
|
252
|
+
diagnostics: {
|
|
253
|
+
unmapped,
|
|
254
|
+
statedAbsent,
|
|
255
|
+
variantRenders: declarations.reduce((n, d) => n + d.renders.length, 0),
|
|
256
|
+
withSet: components.filter((c) => c.refSet).length,
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Write a repo's `design-map.json` from its discovery manifest — the I/O around `design-map.mjs`.
|
|
4
|
+
*
|
|
5
|
+
* npx @yschimke/compose-design-map [--previews <path>] [--out design-map.json]
|
|
6
|
+
* [--variants design-map-variants.json] [--prefix catalog]
|
|
7
|
+
* [--check] [--strict]
|
|
8
|
+
*
|
|
9
|
+
* Run `./gradlew :<module>:composePreviewDiscover` first so the manifest exists.
|
|
10
|
+
*
|
|
11
|
+
* ## Two files out
|
|
12
|
+
*
|
|
13
|
+
* `--out` is the design map design-parity reads. `--variants` is the sidecar of **unresolved**
|
|
14
|
+
* variant declarations: which other previews are the same component with knobs turned, and which
|
|
15
|
+
* knobs. Turning those into design nodes needs a design kit's published vocabulary, which this repo
|
|
16
|
+
* does not hold — see the module KDoc in `design-map.mjs` for why the split falls here. A repo with
|
|
17
|
+
* no resolver still gets a valid map of base references from this alone.
|
|
18
|
+
*
|
|
19
|
+
* Both are **outputs**: regenerate rather than edit. `--check` is the CI posture — it regenerates
|
|
20
|
+
* in memory and exits non-zero if either committed file has drifted, without writing.
|
|
21
|
+
*
|
|
22
|
+
* ## Failure posture
|
|
23
|
+
*
|
|
24
|
+
* An unmapped component is reported, never fatal by default: a catalog is allowed to contain
|
|
25
|
+
* components nobody has mapped yet, and failing the build over one would make adding a component a
|
|
26
|
+
* breaking change.
|
|
27
|
+
*
|
|
28
|
+
* `--strict` is the opposite posture, for a catalog whose whole purpose is to reproduce a kit —
|
|
29
|
+
* there, a component with no kit node to compare against does not belong in the published
|
|
30
|
+
* inventory at all, and publishing it means shipping a sticker that can never be checked. It gates
|
|
31
|
+
* on BOTH kinds of absence: a missing `reference`, and one explained by `noReference`. The
|
|
32
|
+
* annotation still earns its keep in the default mode, where the two are reported apart so a
|
|
33
|
+
* retired pattern does not read as neglect; `--strict` simply says there are no exceptions.
|
|
34
|
+
*/
|
|
35
|
+
import fs from "node:fs";
|
|
36
|
+
import path from "node:path";
|
|
37
|
+
|
|
38
|
+
import { projectDesignMap } from "./design-map.mjs";
|
|
39
|
+
|
|
40
|
+
function arg(name, def = undefined) {
|
|
41
|
+
const i = process.argv.indexOf(`--${name}`);
|
|
42
|
+
return i >= 0 && i + 1 < process.argv.length && !process.argv[i + 1].startsWith("--")
|
|
43
|
+
? process.argv[i + 1]
|
|
44
|
+
: def;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const PREVIEWS = arg("previews", "build/compose-previews/previews.json");
|
|
48
|
+
const OUT = arg("out", "design-map.json");
|
|
49
|
+
const VARIANTS_OUT = arg("variants", "design-map-variants.json");
|
|
50
|
+
const PREFIX = arg("prefix", "catalog");
|
|
51
|
+
const CHECK = process.argv.includes("--check");
|
|
52
|
+
const STRICT = process.argv.includes("--strict");
|
|
53
|
+
|
|
54
|
+
if (!fs.existsSync(PREVIEWS)) {
|
|
55
|
+
console.error(
|
|
56
|
+
`No discovery manifest at ${PREVIEWS}.\n` +
|
|
57
|
+
`Run \`./gradlew :<module>:composePreviewDiscover\` first, or pass --previews <path>.`,
|
|
58
|
+
);
|
|
59
|
+
process.exit(2);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const manifest = JSON.parse(fs.readFileSync(PREVIEWS, "utf8"));
|
|
63
|
+
const { map, variants, diagnostics } = projectDesignMap(manifest.previews ?? [], {
|
|
64
|
+
prefix: PREFIX,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Gate BEFORE writing, not after. A run that fails should leave the committed map intact rather
|
|
68
|
+
// than replacing it with one CI would then report as merely stale — and an author who dropped a
|
|
69
|
+
// whole group's references wants the list, not one name at a time.
|
|
70
|
+
if (STRICT) {
|
|
71
|
+
const missing = [
|
|
72
|
+
...diagnostics.unmapped.map((id) => `${id} — no reference, and no reason given`),
|
|
73
|
+
...diagnostics.statedAbsent.map((s) => `${s.componentId} — ${s.reason}`),
|
|
74
|
+
];
|
|
75
|
+
if (missing.length) {
|
|
76
|
+
console.error(
|
|
77
|
+
`::error::--strict: ${missing.length} component(s) carry no ` +
|
|
78
|
+
`@CatalogComponent(reference = …):`,
|
|
79
|
+
);
|
|
80
|
+
for (const line of missing) console.error(` - ${line}`);
|
|
81
|
+
console.error(
|
|
82
|
+
`A catalog that reproduces a kit has nothing to compare these against — remove them, ` +
|
|
83
|
+
`or drop --strict to publish them unmapped.`,
|
|
84
|
+
);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const serialize = (value) => `${JSON.stringify(value, null, 2)}\n`;
|
|
90
|
+
const mapText = serialize(map);
|
|
91
|
+
// A component with no variant renders needs no sidecar at all. Writing an empty one would put a
|
|
92
|
+
// file in the repo whose only content is the assertion that it has nothing to say.
|
|
93
|
+
const wantsVariants = variants.components.length > 0;
|
|
94
|
+
const variantsText = serialize(variants);
|
|
95
|
+
|
|
96
|
+
let drifted = false;
|
|
97
|
+
function reconcile(file, text, wanted) {
|
|
98
|
+
const exists = fs.existsSync(file);
|
|
99
|
+
if (CHECK) {
|
|
100
|
+
const current = exists ? fs.readFileSync(file, "utf8") : null;
|
|
101
|
+
const expected = wanted ? text : null;
|
|
102
|
+
if (current !== expected) {
|
|
103
|
+
drifted = true;
|
|
104
|
+
const what = !wanted && exists ? "is stale and should be removed" : "is out of date";
|
|
105
|
+
console.error(`::error::${file} ${what} — regenerate with \`compose-design-map\`.`);
|
|
106
|
+
}
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (!wanted) {
|
|
110
|
+
if (exists) {
|
|
111
|
+
fs.rmSync(file);
|
|
112
|
+
console.log(`Removed ${file} (no variant renders declare an axis).`);
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
fs.mkdirSync(path.dirname(path.resolve(file)), { recursive: true });
|
|
117
|
+
fs.writeFileSync(file, text);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
reconcile(OUT, mapText, true);
|
|
121
|
+
reconcile(VARIANTS_OUT, variantsText, wantsVariants);
|
|
122
|
+
|
|
123
|
+
if (!CHECK) {
|
|
124
|
+
console.log(
|
|
125
|
+
`Wrote ${OUT}: ${map.components.length} mapped component(s), ` +
|
|
126
|
+
`${diagnostics.withSet} naming their component set.`,
|
|
127
|
+
);
|
|
128
|
+
if (wantsVariants) {
|
|
129
|
+
console.log(
|
|
130
|
+
`Wrote ${VARIANTS_OUT}: ${diagnostics.variantRenders} variant render(s) across ` +
|
|
131
|
+
`${variants.components.length} component(s), awaiting a kit resolver.`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (diagnostics.statedAbsent.length) {
|
|
137
|
+
console.log(
|
|
138
|
+
`\n${diagnostics.statedAbsent.length} component(s) have no reference for a stated reason — ` +
|
|
139
|
+
`the kit has nothing live to point at, which is a fact about the kit rather than a gap in ` +
|
|
140
|
+
`this catalog:`,
|
|
141
|
+
);
|
|
142
|
+
for (const s of diagnostics.statedAbsent) {
|
|
143
|
+
console.log(` - ${s.componentId} — ${s.reason}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (diagnostics.unmapped.length) {
|
|
148
|
+
console.log(
|
|
149
|
+
`\n${diagnostics.unmapped.length} component(s) carry neither ` +
|
|
150
|
+
`@CatalogComponent(reference = …) nor a noReference explaining why, and were skipped:`,
|
|
151
|
+
);
|
|
152
|
+
for (const id of diagnostics.unmapped) console.log(` - ${id}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (drifted) process.exit(1);
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yschimke/compose-design-map",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Project a compose-preview discovery manifest into design-parity's design-map.json, plus a sidecar of unresolved variant declarations. Dependency-free.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./design-map.mjs",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./design-map.mjs"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"compose-design-map": "./emit-design-map.mjs"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"design-map.mjs",
|
|
16
|
+
"emit-design-map.mjs",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=22"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"test": "node --test"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"compose",
|
|
28
|
+
"compose-preview",
|
|
29
|
+
"design-parity",
|
|
30
|
+
"design-map",
|
|
31
|
+
"figma"
|
|
32
|
+
],
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/yschimke/compose-ai-tools.git",
|
|
36
|
+
"directory": "design-map"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/yschimke/compose-ai-tools/tree/main/design-map#readme",
|
|
39
|
+
"bugs": "https://github.com/yschimke/compose-ai-tools/issues"
|
|
40
|
+
}
|