canicode 0.9.0 → 0.10.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.
@@ -0,0 +1,263 @@
1
+ var CanICodeRoundtrip = (function (exports) {
2
+ 'use strict';
3
+
4
+ // src/core/roundtrip/annotations.ts
5
+ function stripAnnotations(annotations) {
6
+ const input = annotations ?? [];
7
+ const out = [];
8
+ for (const a of input) {
9
+ const hasLM = typeof a.labelMarkdown === "string" && a.labelMarkdown.length > 0;
10
+ const hasLabel = typeof a.label === "string" && a.label.length > 0;
11
+ if (!hasLM && !hasLabel) continue;
12
+ const base = hasLM ? { labelMarkdown: a.labelMarkdown } : { label: a.label };
13
+ if (a.categoryId) base.categoryId = a.categoryId;
14
+ if (Array.isArray(a.properties) && a.properties.length > 0) {
15
+ base.properties = a.properties;
16
+ }
17
+ out.push(base);
18
+ }
19
+ return out;
20
+ }
21
+ async function ensureCanicodeCategories() {
22
+ const api = figma.annotations;
23
+ const existing = await api.getAnnotationCategoriesAsync();
24
+ const byLabel = new Map(existing.map((c) => [c.label, c.id]));
25
+ async function ensure(label, color) {
26
+ const cached = byLabel.get(label);
27
+ if (cached) return cached;
28
+ const created = await api.addAnnotationCategoryAsync({ label, color });
29
+ byLabel.set(label, created.id);
30
+ return created.id;
31
+ }
32
+ return {
33
+ gotcha: await ensure("canicode:gotcha", "blue"),
34
+ autoFix: await ensure("canicode:auto-fix", "green"),
35
+ fallback: await ensure("canicode:fallback", "yellow")
36
+ };
37
+ }
38
+ function upsertCanicodeAnnotation(node, input) {
39
+ if (!node || !("annotations" in node)) return false;
40
+ const { ruleId, markdown, categoryId, properties } = input;
41
+ const prefix = `**[canicode] ${ruleId}**`;
42
+ const body = markdown.startsWith(prefix) ? markdown : `${prefix}
43
+
44
+ ${markdown}`;
45
+ const existing = stripAnnotations(node.annotations);
46
+ const entry = { labelMarkdown: body };
47
+ if (categoryId) entry.categoryId = categoryId;
48
+ if (properties && properties.length > 0) entry.properties = properties;
49
+ const idx = existing.findIndex((a) => {
50
+ const lm = a.labelMarkdown;
51
+ const lb = a.label;
52
+ return typeof lm === "string" && lm.startsWith(prefix) || typeof lb === "string" && lb.startsWith(prefix);
53
+ });
54
+ if (idx >= 0) existing[idx] = entry;
55
+ else existing.push(entry);
56
+ try {
57
+ node.annotations = existing;
58
+ return true;
59
+ } catch (e) {
60
+ const msg = String(e?.message ?? e);
61
+ const isNodeTypeReject = /invalid property .+ for a .+ node/i.test(msg);
62
+ if (!entry.properties || !isNodeTypeReject) throw e;
63
+ delete entry.properties;
64
+ if (idx >= 0) existing[idx] = entry;
65
+ node.annotations = existing;
66
+ return true;
67
+ }
68
+ }
69
+
70
+ // src/core/roundtrip/apply-with-instance-fallback.ts
71
+ var DEFINITION_WRITE_SKIPPED_EVENT = "cic_roundtrip_definition_write_skipped";
72
+ function resolveSourceComponentName(definition, question) {
73
+ if (definition && typeof definition.name === "string" && definition.name) {
74
+ return definition.name;
75
+ }
76
+ const ic = question.instanceContext;
77
+ if (ic && typeof ic.sourceComponentName === "string" && ic.sourceComponentName) {
78
+ return ic.sourceComponentName;
79
+ }
80
+ return "the source component";
81
+ }
82
+ async function routeToDefinitionOrAnnotate(definition, writeFn, ctx) {
83
+ if (definition && !ctx.allowDefinitionWrite && ctx.reason !== "non-override-error") {
84
+ const componentName = resolveSourceComponentName(definition, ctx.question);
85
+ if (ctx.categories) {
86
+ upsertCanicodeAnnotation(ctx.scene, {
87
+ ruleId: ctx.question.ruleId,
88
+ markdown: `Apply this fix on the source component **${componentName}** to share across all instances. This instance kept its current value to avoid unintended fan-out. Re-run with \`allowDefinitionWrite: true\` to propagate.`,
89
+ categoryId: ctx.categories.fallback
90
+ });
91
+ }
92
+ ctx.telemetry?.(DEFINITION_WRITE_SKIPPED_EVENT, {
93
+ ruleId: ctx.question.ruleId,
94
+ reason: ctx.reason
95
+ });
96
+ return {
97
+ icon: "\u{1F4DD}",
98
+ label: "definition write skipped (opt-in disabled)"
99
+ };
100
+ }
101
+ if (!definition) {
102
+ if (ctx.categories) {
103
+ const markdown = ctx.reason === "silent-ignore" ? "write accepted but value unchanged; no definition available" : ctx.reason === "override-error" ? `could not apply automatically: ${ctx.errorMessage ?? ""}` : `could not apply automatically: ${ctx.errorMessage ?? ""}`;
104
+ upsertCanicodeAnnotation(ctx.scene, {
105
+ ruleId: ctx.question.ruleId,
106
+ markdown,
107
+ categoryId: ctx.categories.fallback
108
+ });
109
+ }
110
+ return ctx.reason === "silent-ignore" ? { icon: "\u{1F4DD}", label: "silent-ignore, annotated" } : { icon: "\u{1F4DD}", label: `error: ${ctx.errorMessage ?? ""}` };
111
+ }
112
+ try {
113
+ await writeFn(definition);
114
+ return {
115
+ icon: "\u{1F310}",
116
+ label: ctx.reason === "silent-ignore" ? "source definition (silent-ignore fallback)" : "source definition"
117
+ };
118
+ } catch (defErr) {
119
+ const defMsg = String(defErr?.message ?? defErr);
120
+ const isRemoteReadOnly = definition.remote === true || /read-only/i.test(defMsg);
121
+ if (ctx.categories) {
122
+ upsertCanicodeAnnotation(ctx.scene, {
123
+ ruleId: ctx.question.ruleId,
124
+ markdown: isRemoteReadOnly ? "source component lives in an external library and is read-only from this file \u2014 apply the fix in the library file itself." : `could not apply at source definition: ${defMsg}`,
125
+ categoryId: ctx.categories.fallback
126
+ });
127
+ }
128
+ return {
129
+ icon: "\u{1F4DD}",
130
+ label: isRemoteReadOnly ? "external library (read-only)" : `definition error: ${defMsg}`
131
+ };
132
+ }
133
+ }
134
+ async function applyWithInstanceFallback(question, writeFn, context = {}) {
135
+ const { categories, allowDefinitionWrite = false, telemetry } = context;
136
+ const scene = await figma.getNodeByIdAsync(question.nodeId);
137
+ if (!scene) return { icon: "\u{1F4DD}", label: "missing node" };
138
+ const definition = question.sourceChildId ? await figma.getNodeByIdAsync(question.sourceChildId) : null;
139
+ try {
140
+ const changed = await writeFn(scene);
141
+ if (changed === false) {
142
+ return routeToDefinitionOrAnnotate(definition, writeFn, {
143
+ question,
144
+ scene,
145
+ categories,
146
+ reason: "silent-ignore",
147
+ allowDefinitionWrite,
148
+ telemetry
149
+ });
150
+ }
151
+ return { icon: "\u2705", label: "instance/scene" };
152
+ } catch (e) {
153
+ const msg = String(e?.message ?? e);
154
+ const looksLikeInstanceOverride = /cannot be overridden/i.test(msg) || /override/i.test(msg);
155
+ if (!looksLikeInstanceOverride) {
156
+ return routeToDefinitionOrAnnotate(null, writeFn, {
157
+ question,
158
+ scene,
159
+ categories,
160
+ reason: "non-override-error",
161
+ errorMessage: msg,
162
+ allowDefinitionWrite,
163
+ telemetry
164
+ });
165
+ }
166
+ return routeToDefinitionOrAnnotate(definition, writeFn, {
167
+ question,
168
+ scene,
169
+ categories,
170
+ reason: "override-error",
171
+ errorMessage: msg,
172
+ allowDefinitionWrite,
173
+ telemetry
174
+ });
175
+ }
176
+ }
177
+
178
+ // src/core/roundtrip/apply-property-mod.ts
179
+ async function resolveVariableByName(name) {
180
+ const locals = await figma.variables.getLocalVariablesAsync();
181
+ return locals.find((v) => v.name === name) ?? null;
182
+ }
183
+ function parseValue(raw) {
184
+ if (raw && typeof raw === "object" && "variable" in raw) {
185
+ const v = raw;
186
+ const parsed = { kind: "binding", name: v.variable };
187
+ if ("fallback" in v) parsed.fallback = v.fallback;
188
+ return parsed;
189
+ }
190
+ if (raw && typeof raw === "object" && "fallback" in raw) {
191
+ return { kind: "scalar", scalar: raw.fallback };
192
+ }
193
+ return { kind: "scalar", scalar: raw };
194
+ }
195
+ function isPaintProp(prop) {
196
+ return prop === "fills" || prop === "strokes";
197
+ }
198
+ function applyPropertyBinding(target, prop, variable) {
199
+ if (isPaintProp(prop)) {
200
+ const current = target[prop];
201
+ if (current === figma.mixed || !Array.isArray(current)) return false;
202
+ const paints = current;
203
+ const bound = paints.map(
204
+ (paint) => figma.variables.setBoundVariableForPaint(paint, "color", variable)
205
+ );
206
+ target[prop] = bound;
207
+ return true;
208
+ }
209
+ target.setBoundVariable(prop, variable);
210
+ return true;
211
+ }
212
+ function applyPropertyScalar(target, prop, scalar) {
213
+ const rec = target;
214
+ const before = rec[prop];
215
+ rec[prop] = scalar;
216
+ if (rec[prop] === before && before !== scalar) return false;
217
+ return true;
218
+ }
219
+ async function applyPropertyMod(question, answerValue, context = {}) {
220
+ const props = Array.isArray(question.targetProperty) ? question.targetProperty : question.targetProperty !== void 0 ? [question.targetProperty] : [];
221
+ return applyWithInstanceFallback(
222
+ question,
223
+ async (target) => {
224
+ if (!target) return void 0;
225
+ let changed = void 0;
226
+ for (const prop of props) {
227
+ if (!(prop in target)) continue;
228
+ const perProp = answerValue && typeof answerValue === "object" && !("variable" in answerValue) && !Array.isArray(answerValue) ? answerValue[prop] : answerValue;
229
+ const parsed = parseValue(perProp);
230
+ if (parsed.kind === "binding") {
231
+ const variable = await resolveVariableByName(parsed.name);
232
+ if (variable) {
233
+ applyPropertyBinding(target, prop, variable);
234
+ continue;
235
+ }
236
+ if (parsed.fallback !== void 0) {
237
+ if (!applyPropertyScalar(target, prop, parsed.fallback)) {
238
+ changed = false;
239
+ }
240
+ }
241
+ continue;
242
+ }
243
+ if (parsed.scalar === void 0) continue;
244
+ if (!applyPropertyScalar(target, prop, parsed.scalar)) {
245
+ changed = false;
246
+ }
247
+ }
248
+ return changed;
249
+ },
250
+ context
251
+ );
252
+ }
253
+
254
+ exports.applyPropertyMod = applyPropertyMod;
255
+ exports.applyWithInstanceFallback = applyWithInstanceFallback;
256
+ exports.ensureCanicodeCategories = ensureCanicodeCategories;
257
+ exports.resolveVariableByName = resolveVariableByName;
258
+ exports.stripAnnotations = stripAnnotations;
259
+ exports.upsertCanicodeAnnotation = upsertCanicodeAnnotation;
260
+
261
+ return exports;
262
+
263
+ })({});
@@ -1,143 +0,0 @@
1
- # Design to Code — Standard Prompt
2
-
3
- This prompt is used by all code generation pipelines:
4
- - Calibration Converter
5
- - Ablation experiments (API-based)
6
- - User-facing `canicode implement` command (default prompt)
7
-
8
- ## Stack
9
- - HTML + CSS (single file)
10
- - No frameworks, no build step
11
-
12
- ## Conventions
13
- - Semantic HTML elements
14
- - Flexbox / Grid for layout
15
- - Always include `* { box-sizing: border-box; margin: 0; padding: 0; }` reset
16
-
17
- ## CRITICAL: Do NOT Interpret. Reproduce Exactly.
18
-
19
- Every pixel in the Figma file is intentional. A designer made each decision deliberately.
20
- Your job is to translate the Figma data to HTML+CSS — nothing more.
21
-
22
- ### Priority Order
23
- 1. **Pixel-exact reproduction** — match every dimension, color, spacing, font exactly
24
- 2. **Component reuse** — same component annotation → shared CSS class
25
- 3. **Design tokens** — repeated values → CSS custom properties
26
-
27
- Never sacrifice #1 for #2 or #3. Reuse and tokens are structural improvements only — they must not change the visual output.
28
-
29
- ### Rules
30
- - Do NOT add any value that isn't in the Figma data (no extra padding, margin, gap, transition)
31
- - Do NOT add hover effects unless `[hover]:` data is provided in the design tree
32
- - Do NOT change any value from the Figma data (if it says 160px padding, use 160px)
33
- - Do NOT "improve" the design — if something looks wrong, reproduce it anyway
34
- - Do NOT add responsive behavior unless the Figma data explicitly shows it
35
- - Do NOT use min-height or min-width unless the design tree explicitly includes them — use exact height and width from the data
36
- - Do NOT add overflow: auto or scroll unless specified
37
- - Fonts: load via Google Fonts CDN (`<link>` tag). Do NOT use system font fallbacks as primary — the exact font from the data must render.
38
-
39
- ### Component Reuse
40
-
41
- Nodes annotated with `[component: ComponentName]` are instances of the same design component.
42
-
43
- - Define a CSS class for each unique component name (e.g., `[component: Review Card]` → `.review-card { ... }`)
44
- - If the same component appears multiple times, define the shared styles once in the class, then apply it to each instance
45
- - `component-properties:` lines show variant overrides — use them to differentiate instances (e.g., different text content, sizes) while keeping shared styles in the class
46
- - Component name → class name: lowercase, spaces to hyphens (e.g., `Review Card` → `.review-card`)
47
- - Use CSS classes only — no Web Components, no JavaScript templates
48
-
49
- ### Design Tokens
50
-
51
- Extract repeated values into CSS custom properties in `:root { }`.
52
-
53
- **Colors**: When the same hex color appears 3+ times, define it as a CSS variable:
54
- ```css
55
- :root {
56
- --color-2C2C2C: #2C2C2C;
57
- --color-0066CC: #0066CC;
58
- }
59
- ```
60
- Then use `var(--color-2C2C2C)` instead of inline `#2C2C2C`.
61
-
62
- Naming: if a `/* var:... */` comment is present next to a color value, it means the designer bound this color to a design token — always extract these as CSS variables.
63
-
64
- **Typography**: When `/* text-style: StyleName */` appears in a text node's styles, nodes sharing the same text style name should use a shared CSS class:
65
- ```css
66
- .text-heading-large { font-family: "Inter"; font-weight: 700; font-size: 32px; line-height: 40px; }
67
- ```
68
- Style name → class name: lowercase, spaces/slashes to hyphens, prefix with `text-` (e.g., `Heading / Large` → `.text-heading-large`).
69
-
70
- ### SVG Vectors
71
-
72
- When a node's style includes `svg: <svg>...</svg>`, render it as an inline `<svg>` element:
73
- - Use the SVG markup exactly as provided — do not modify paths or attributes
74
- - Preserve the node's dimensions (`width` and `height` from the node header)
75
- - The `<svg>` replaces the node's HTML element (do not wrap it in an extra `<div>` unless the node has other styles like background or border)
76
-
77
- ### Hover States
78
-
79
- When a node includes a `[hover]:` line, generate a `:hover` CSS rule with the provided style changes:
80
-
81
- ```text
82
- Button (INSTANCE, 120x40) [component: Button]
83
- style: background: #2C2C2C
84
- [hover]: background: #1E1E1E; Icon: fill: #FFFFFF
85
- ```
86
-
87
- → generates:
88
-
89
- ```css
90
- .button { background: #2C2C2C; }
91
- .button:hover { background: #1E1E1E; }
92
- .button:hover .icon { fill: #FFFFFF; }
93
- ```
94
-
95
- - Only use hover data that is explicitly provided in `[hover]:` — do not invent hover effects
96
- - Child styles (e.g., `Icon: fill: #FFFFFF`) use `.parent:hover .child` selector pattern
97
-
98
- ### Image Assets
99
-
100
- The design tree uses two distinct image types:
101
-
102
- **`content-image:`** — leaf node, no children → render as `<img>` tag
103
- - `content-image: url(images/...)` → `<img src="images/..." />`
104
- - `object-fit: cover` or `object-fit: contain` is provided alongside — use it directly
105
-
106
- **`background-image:`** — node has children on top → keep as CSS `background-image`
107
- - `background-image: url(images/...)` → `background-image: url(images/...)` in CSS
108
- - `background-size`, `background-position`, `background-repeat` are provided alongside — use as-is
109
-
110
- **`[IMAGE]` placeholder** — image asset unavailable → use a placeholder color matching the surrounding design
111
-
112
- ### If data is missing
113
- When the Figma data does not specify a value, you MUST list it as an interpretation.
114
- Do not silently guess — always declare what you assumed.
115
-
116
- ## Output
117
-
118
- ### 1. Code
119
- - Place the entire design inside a single root `<div>` as the first child of `<body>` — do NOT apply design styles directly to `<body>` or `<html>`
120
- - Output as a code block with filename:
121
-
122
- ```html
123
- // filename: index.html
124
- <!DOCTYPE html>
125
- ...
126
- ```
127
-
128
- ### 2. Interpretations
129
- After the code block, list every value you had to guess or assume.
130
- Keep this list to **only genuine ambiguities** — do not list standard defaults (e.g., `body { margin: 0 }` is always expected, not an interpretation).
131
- **Maximum 10 items.** If you have more than 10, keep only the highest-impact ones.
132
-
133
- ```text
134
- // interpretations:
135
- - Used placeholder gray (#CCCCCC) for unavailable image asset
136
- - Chose "Inter" font weight 500 for ambiguous "Medium" style reference
137
- ```
138
-
139
- If you did not interpret anything, write:
140
-
141
- ```text
142
- // interpretations: none
143
- ```