@su-record/vibe 2.8.48 → 2.8.49

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.
@@ -103,6 +103,34 @@ function toCSS(c) {
103
103
  return `rgba(${r}, ${g}, ${b}, ${+a.toFixed(2)})`;
104
104
  }
105
105
 
106
+ // ─── Gradient Helpers ───────────────────────────────────────────────
107
+
108
+ function toLinearGradient(f) {
109
+ if (!f.gradientHandlePositions || !f.gradientStops) return null;
110
+ const [p0, p1] = f.gradientHandlePositions;
111
+ const angle = Math.round(Math.atan2(p1.y - p0.y, p1.x - p0.x) * 180 / Math.PI + 90);
112
+ const opacity = f.opacity ?? 1;
113
+ const stops = f.gradientStops.map(s => {
114
+ const c = opacity < 1 ? { ...s.color, a: (s.color.a ?? 1) * opacity } : s.color;
115
+ return `${toCSS(c)} ${Math.round(s.position * 100)}%`;
116
+ }).join(', ');
117
+ return `linear-gradient(${angle}deg, ${stops})`;
118
+ }
119
+
120
+ function toRadialGradient(f) {
121
+ if (!f.gradientStops) return null;
122
+ const opacity = f.opacity ?? 1;
123
+ const stops = f.gradientStops.map(s => {
124
+ const c = opacity < 1 ? { ...s.color, a: (s.color.a ?? 1) * opacity } : s.color;
125
+ return `${toCSS(c)} ${Math.round(s.position * 100)}%`;
126
+ }).join(', ');
127
+ return `radial-gradient(circle, ${stops})`;
128
+ }
129
+
130
+ // ─── Blend Mode Map ─────────────────────────────────────────────────
131
+
132
+ const BLEND_MODES = { MULTIPLY:'multiply', SCREEN:'screen', OVERLAY:'overlay', DARKEN:'darken', LIGHTEN:'lighten', COLOR_DODGE:'color-dodge', COLOR_BURN:'color-burn', HARD_LIGHT:'hard-light', SOFT_LIGHT:'soft-light', DIFFERENCE:'difference', EXCLUSION:'exclusion', HUE:'hue', SATURATION:'saturation', COLOR:'color', LUMINOSITY:'luminosity' };
133
+
106
134
  // ─── CSS Extraction ─────────────────────────────────────────────────
107
135
 
108
136
  function extractCSS(n) {
@@ -115,38 +143,108 @@ function extractCSS(n) {
115
143
  if (n.primaryAxisAlignItems && axM[n.primaryAxisAlignItems]) css.justifyContent = axM[n.primaryAxisAlignItems];
116
144
  if (n.counterAxisAlignItems && crM[n.counterAxisAlignItems]) css.alignItems = crM[n.counterAxisAlignItems];
117
145
  if (n.itemSpacing > 0) css.gap = `${n.itemSpacing}px`;
146
+ // layoutGrow
147
+ if (n.layoutGrow === 1) css.flexGrow = '1';
118
148
  // Padding
119
149
  const pt=n.paddingTop||0, pr=n.paddingRight||0, pb=n.paddingBottom||0, pl=n.paddingLeft||0;
120
150
  if (pt||pr||pb||pl) css.padding = `${pt}px ${pr}px ${pb}px ${pl}px`;
121
151
  // Size
122
152
  if (n.absoluteBoundingBox) { css.width = `${Math.round(n.absoluteBoundingBox.width)}px`; css.height = `${Math.round(n.absoluteBoundingBox.height)}px`; }
153
+ // layoutSizing — HUG removes fixed dimensions, FILL handled by converter with parent context
154
+ if (n.layoutSizingHorizontal === 'HUG') delete css.width;
155
+ if (n.layoutSizingVertical === 'HUG') delete css.height;
123
156
  // Position / overflow / opacity
124
157
  if (n.layoutPositioning === 'ABSOLUTE') css.position = 'absolute';
125
158
  if (n.clipsContent) css.overflow = 'hidden';
126
159
  if (n.opacity != null && n.opacity < 1) css.opacity = n.opacity.toFixed(2);
127
- // Blend
128
- const bm = { MULTIPLY:'multiply', SCREEN:'screen', OVERLAY:'overlay', DARKEN:'darken', LIGHTEN:'lighten', COLOR_DODGE:'color-dodge', COLOR_BURN:'color-burn', HARD_LIGHT:'hard-light', SOFT_LIGHT:'soft-light', DIFFERENCE:'difference', EXCLUSION:'exclusion', HUE:'hue', SATURATION:'saturation', COLOR:'color', LUMINOSITY:'luminosity' };
129
- if (n.blendMode && bm[n.blendMode]) css.mixBlendMode = bm[n.blendMode];
160
+ // Rotation
161
+ if (n.rotation != null && Math.abs(n.rotation) > 0.05) {
162
+ const deg = +((-n.rotation) % 360).toFixed(2);
163
+ css.transform = `rotate(${deg}deg)`;
164
+ }
165
+ // Node-level blend mode
166
+ if (n.blendMode && BLEND_MODES[n.blendMode]) css.mixBlendMode = BLEND_MODES[n.blendMode];
130
167
  // Radius
131
168
  if (n.cornerRadius > 0) css.borderRadius = `${n.cornerRadius}px`;
132
169
  else if (n.rectangleCornerRadii) { const [a,b,c,d] = n.rectangleCornerRadii; css.borderRadius = `${a}px ${b}px ${c}px ${d}px`; }
133
- // Fills
134
- let imgRef;
135
- for (const f of (n.fills||[]).filter(f=>f.visible!==false)) {
136
- if (f.type === 'SOLID') css.backgroundColor = toCSS({ ...f.color, a: f.opacity ?? f.color?.a ?? 1 });
137
- else if (f.type === 'IMAGE') imgRef = f.imageRef;
170
+ // Fills — multi-fill aware
171
+ let imgRef, imgScaleMode;
172
+ const visibleFills = (n.fills||[]).filter(f=>f.visible!==false);
173
+ // Backward compat: single-fill css properties
174
+ const firstSolid = visibleFills.find(f=>f.type==='SOLID');
175
+ if (firstSolid) css.backgroundColor = toCSS({ ...firstSolid.color, a: firstSolid.opacity ?? firstSolid.color?.a ?? 1 });
176
+ const firstImage = visibleFills.find(f=>f.type==='IMAGE');
177
+ if (firstImage) { imgRef = firstImage.imageRef; imgScaleMode = firstImage.scaleMode; }
178
+ // Gradient → backgroundImage
179
+ const gradients = [];
180
+ for (const f of visibleFills) {
181
+ if (f.type === 'GRADIENT_LINEAR') { const g = toLinearGradient(f); if (g) gradients.push(g); }
182
+ else if (f.type === 'GRADIENT_RADIAL' || f.type === 'GRADIENT_ANGULAR' || f.type === 'GRADIENT_DIAMOND') {
183
+ const g = toRadialGradient(f); if (g) gradients.push(g);
184
+ }
185
+ }
186
+ if (gradients.length) css.backgroundImage = gradients.join(', ');
187
+ // Per-fill blendMode → backgroundBlendMode
188
+ for (const f of visibleFills) {
189
+ if (f.blendMode && f.blendMode !== 'NORMAL' && BLEND_MODES[f.blendMode]) {
190
+ css.backgroundBlendMode = BLEND_MODES[f.blendMode]; break;
191
+ }
138
192
  }
139
- // Strokes
193
+ // Fill filters (saturation → grayscale/saturate)
194
+ for (const f of visibleFills) {
195
+ if (f.filters?.saturation != null && f.filters.saturation !== 0) {
196
+ const sat = f.filters.saturation;
197
+ const filterVal = sat < 0 ? `grayscale(${Math.round(Math.abs(sat) * 100)}%)` : `saturate(${Math.round((1 + sat) * 100)}%)`;
198
+ css.filter = (css.filter ? css.filter + ' ' : '') + filterVal;
199
+ }
200
+ }
201
+ // Multi-fill structured array (when 2+ visible fills — for converter)
202
+ let _fills;
203
+ if (visibleFills.length > 1) {
204
+ _fills = visibleFills.map(f => {
205
+ const entry = { type: f.type };
206
+ if (f.type === 'SOLID') entry.color = toCSS({ ...f.color, a: f.opacity ?? f.color?.a ?? 1 });
207
+ if (f.type === 'IMAGE') { entry.imageRef = f.imageRef; if (f.scaleMode) entry.scaleMode = f.scaleMode; }
208
+ if (f.type?.startsWith('GRADIENT_')) {
209
+ entry.gradient = f.type === 'GRADIENT_LINEAR' ? toLinearGradient(f) : toRadialGradient(f);
210
+ }
211
+ if (f.blendMode && f.blendMode !== 'NORMAL' && BLEND_MODES[f.blendMode]) entry.blendMode = BLEND_MODES[f.blendMode];
212
+ if (f.filters?.saturation != null && f.filters.saturation !== 0) entry.filters = f.filters;
213
+ return entry;
214
+ });
215
+ }
216
+ // Strokes — strokeAlign aware + individual strokes + dashes
140
217
  const stroke = (n.strokes||[]).find(s=>s.visible!==false&&s.type==='SOLID');
141
- if (stroke && n.strokeWeight) css.border = `${n.strokeWeight}px solid ${toCSS(stroke.color)}`;
218
+ const isw = n.individualStrokeWeights;
219
+ const hasStroke = stroke && (n.strokeWeight || isw);
220
+ if (hasStroke) {
221
+ const strokeColor = toCSS({ ...stroke.color, a: stroke.opacity ?? stroke.color?.a ?? 1 });
222
+ // Individual stroke weights (different per side)
223
+ if (isw && (isw.top !== isw.bottom || isw.left !== isw.right || isw.top !== isw.left)) {
224
+ if (isw.top) css.borderTop = `${isw.top}px solid ${strokeColor}`;
225
+ if (isw.right) css.borderRight = `${isw.right}px solid ${strokeColor}`;
226
+ if (isw.bottom) css.borderBottom = `${isw.bottom}px solid ${strokeColor}`;
227
+ if (isw.left) css.borderLeft = `${isw.left}px solid ${strokeColor}`;
228
+ } else if (n.strokeAlign === 'OUTSIDE') {
229
+ css.outline = `${n.strokeWeight}px solid ${strokeColor}`;
230
+ } else {
231
+ css.border = `${n.strokeWeight}px solid ${strokeColor}`;
232
+ if (n.strokeAlign === 'INSIDE') css.boxSizing = 'border-box';
233
+ }
234
+ // Dashed strokes
235
+ if (n.strokeDashes?.length) {
236
+ css.borderStyle = 'dashed';
237
+ }
238
+ }
142
239
  // Effects
143
240
  const shadows = [];
144
241
  for (const e of (n.effects||[]).filter(e=>e.visible!==false)) {
145
242
  if (e.type==='DROP_SHADOW'||e.type==='INNER_SHADOW') {
146
243
  const ins = e.type==='INNER_SHADOW'?'inset ':'';
147
244
  shadows.push(`${ins}${e.offset?.x||0}px ${e.offset?.y||0}px ${e.radius||0}px ${e.spread||0}px ${toCSS(e.color)}`);
148
- } else if (e.type==='LAYER_BLUR') css.filter = `blur(${e.radius}px)`;
149
- else if (e.type==='BACKGROUND_BLUR') css.backdropFilter = `blur(${e.radius}px)`;
245
+ } else if (e.type==='LAYER_BLUR') {
246
+ css.filter = (css.filter ? css.filter + ' ' : '') + `blur(${e.radius}px)`;
247
+ } else if (e.type==='BACKGROUND_BLUR') css.backdropFilter = `blur(${e.radius}px)`;
150
248
  }
151
249
  if (shadows.length) css.boxShadow = shadows.join(', ');
152
250
  // Text
@@ -159,26 +257,53 @@ function extractCSS(n) {
159
257
  if (s.letterSpacing) css.letterSpacing = `${s.letterSpacing}px`;
160
258
  const ta = { LEFT:'left', CENTER:'center', RIGHT:'right', JUSTIFIED:'justify' };
161
259
  if (s.textAlignHorizontal && ta[s.textAlignHorizontal]) css.textAlign = ta[s.textAlignHorizontal];
162
- const tf = (n.fills||[]).find(f=>f.visible!==false&&f.type==='SOLID');
260
+ // textCase text-transform
261
+ const tc = { UPPER:'uppercase', LOWER:'lowercase', TITLE:'capitalize' };
262
+ if (s.textCase && tc[s.textCase]) css.textTransform = tc[s.textCase];
263
+ // textTruncation → ellipsis
264
+ if (s.textTruncation === 'ENDING') {
265
+ css.overflow = 'hidden'; css.textOverflow = 'ellipsis'; css.whiteSpace = 'nowrap';
266
+ }
267
+ // paragraphSpacing
268
+ if (s.paragraphSpacing > 0) css.marginBottom = `${s.paragraphSpacing}px`;
269
+ const tf = visibleFills.find(f=>f.type==='SOLID');
163
270
  if (tf) css.color = toCSS(tf.color);
164
271
  }
165
- return imgRef ? { ...css, _imageRef: imgRef } : css;
272
+ const result = { ...css };
273
+ if (imgRef) result._imageRef = imgRef;
274
+ if (imgScaleMode) result._imageScaleMode = imgScaleMode;
275
+ if (_fills) result._fills = _fills;
276
+ return result;
166
277
  }
167
278
 
168
279
  // ─── Tree ───────────────────────────────────────────────────────────
169
280
 
170
- function walk(node) {
281
+ function walk(node, parentAbsBBox) {
171
282
  const css = extractCSS(node);
172
283
  const r = { nodeId: node.id, name: node.name||'', type: node.type, size: null, css: {...css}, children: [] };
173
284
  if (node.type==='TEXT' && node.characters) r.text = node.characters;
174
285
  if (node.absoluteBoundingBox) r.size = { width: Math.round(node.absoluteBoundingBox.width), height: Math.round(node.absoluteBoundingBox.height) };
286
+ // Unpack internal fields → top-level metadata
175
287
  if (css._imageRef) { r.imageRef = css._imageRef; delete r.css._imageRef; }
176
- if (node.children?.length) r.children = node.children.map(walk);
288
+ if (css._imageScaleMode) { r.imageScaleMode = css._imageScaleMode; delete r.css._imageScaleMode; }
289
+ if (css._fills) { r.fills = css._fills; delete r.css._fills; }
290
+ // Mask flag (converter skips or applies clip-path)
291
+ if (node.isMask) r.isMask = true;
292
+ // layoutSizing metadata (converter uses with parent context)
293
+ if (node.layoutSizingHorizontal) r.layoutSizingH = node.layoutSizingHorizontal;
294
+ if (node.layoutSizingVertical) r.layoutSizingV = node.layoutSizingVertical;
295
+ // Absolute positioning: parent-relative top/left
296
+ if (node.layoutPositioning === 'ABSOLUTE' && node.absoluteBoundingBox && parentAbsBBox) {
297
+ r.css.top = `${Math.round(node.absoluteBoundingBox.y - parentAbsBBox.y)}px`;
298
+ r.css.left = `${Math.round(node.absoluteBoundingBox.x - parentAbsBBox.x)}px`;
299
+ }
300
+ if (node.children?.length) r.children = node.children.map(c => walk(c, node.absoluteBoundingBox));
177
301
  return r;
178
302
  }
179
303
 
180
304
  function collectRefs(node, set = new Set()) {
181
305
  if (node.imageRef) set.add(node.imageRef);
306
+ if (node.fills) { for (const f of node.fills) { if (f.imageRef) set.add(f.imageRef); } }
182
307
  (node.children||[]).forEach(c => collectRefs(c, set));
183
308
  return set;
184
309
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@su-record/vibe",
3
- "version": "2.8.48",
3
+ "version": "2.8.49",
4
4
  "description": "AI Coding Framework for Claude Code — 56 agents, 45 skills, multi-LLM orchestration",
5
5
  "type": "module",
6
6
  "main": "dist/cli/index.js",