figma-to-html-pixel-perfect 1.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 +21 -0
- package/README.md +217 -0
- package/SKILL.md +1855 -0
- package/bin/install.js +42 -0
- package/package.json +23 -0
- package/references/accessibility-checklist.md +68 -0
- package/references/animation-guidelines.md +113 -0
- package/references/visual-review-checklist.md +121 -0
- package/scripts/figma_discover.py +218 -0
- package/scripts/figma_fonts.py +90 -0
- package/scripts/figma_icons.py +391 -0
- package/scripts/figma_lint.py +246 -0
- package/scripts/figma_pull.py +153 -0
- package/scripts/figma_report.py +1320 -0
- package/scripts/figma_spec.py +93 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Print an accurate layout spec for a Figma node, from cached node JSON.
|
|
3
|
+
|
|
4
|
+
Implements the §6.5.3 reading contract:
|
|
5
|
+
- text content comes from `characters`, never `name`
|
|
6
|
+
- `visible: false` subtrees are skipped
|
|
7
|
+
- reports textCase / textAlign / colour / stroke / radius / layout / image refs
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
python3 figma_spec.py <path-to-node.json> [max_depth]
|
|
11
|
+
|
|
12
|
+
Where <path-to-node.json> is one entry of GET /v1/files/:key/nodes?ids=…
|
|
13
|
+
(i.e. a dict with a "document" key), as written by figma_pull.py.
|
|
14
|
+
"""
|
|
15
|
+
import json, sys
|
|
16
|
+
|
|
17
|
+
if len(sys.argv) < 2:
|
|
18
|
+
sys.exit(__doc__)
|
|
19
|
+
|
|
20
|
+
path = sys.argv[1]
|
|
21
|
+
maxd = int(sys.argv[2]) if len(sys.argv) > 2 else 6
|
|
22
|
+
|
|
23
|
+
doc = json.load(open(path))
|
|
24
|
+
doc = doc["document"] if "document" in doc else doc
|
|
25
|
+
root = doc.get("absoluteBoundingBox") or {}
|
|
26
|
+
ox, oy = root.get("x", 0), root.get("y", 0)
|
|
27
|
+
print(f"### {path} {int(root.get('width',0))}x{int(root.get('height',0))}\n")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _col(c, opacity=1):
|
|
31
|
+
a = opacity * c.get("a", 1)
|
|
32
|
+
r, g, b = (round(c[k] * 255) for k in "rgb")
|
|
33
|
+
return f"#{r:02x}{g:02x}{b:02x}" + (f"@{a:.2f}" if a < 0.99 else "")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def paint(node, key="fills"):
|
|
37
|
+
for f in node.get(key) or []:
|
|
38
|
+
if f.get("visible") is False:
|
|
39
|
+
continue
|
|
40
|
+
t = f.get("type")
|
|
41
|
+
if t == "SOLID":
|
|
42
|
+
return _col(f["color"], f.get("opacity", 1))
|
|
43
|
+
if t == "IMAGE":
|
|
44
|
+
# imageTransform may rotate/flip the photo — flag it, do not ignore it.
|
|
45
|
+
flag = "*" if f.get("imageTransform") else ""
|
|
46
|
+
return f"IMG({f.get('imageRef','')[:8]}{flag})"
|
|
47
|
+
if t and "GRADIENT" in t:
|
|
48
|
+
return "GRADIENT"
|
|
49
|
+
return ""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def walk(n, depth=0):
|
|
53
|
+
if n.get("visible") is False: # hidden variants hold stale copy — skip
|
|
54
|
+
return
|
|
55
|
+
if depth > maxd:
|
|
56
|
+
return
|
|
57
|
+
bb = n.get("absoluteBoundingBox") or {}
|
|
58
|
+
if not bb:
|
|
59
|
+
return
|
|
60
|
+
x, y = int(bb.get("x", 0) - ox), int(bb.get("y", 0) - oy)
|
|
61
|
+
w, h = int(bb.get("width", 0)), int(bb.get("height", 0))
|
|
62
|
+
t = n.get("type", "")
|
|
63
|
+
pad = " " * depth
|
|
64
|
+
|
|
65
|
+
if t == "TEXT":
|
|
66
|
+
s = n.get("style", {})
|
|
67
|
+
chars = (n.get("characters") or "").replace("\n", "⏎").replace("\r", "")[:58]
|
|
68
|
+
bits = [f'{s.get("fontFamily")} {s.get("fontWeight")} {round(s.get("fontSize",0))}px']
|
|
69
|
+
if s.get("lineHeightPx"): bits.append(f'lh{round(s["lineHeightPx"])}')
|
|
70
|
+
if s.get("letterSpacing"): bits.append(f'ls{s["letterSpacing"]:.1f}')
|
|
71
|
+
if s.get("textCase"): bits.append(s["textCase"]) # UPPER is real
|
|
72
|
+
if s.get("textAlignHorizontal"): bits.append(s["textAlignHorizontal"])
|
|
73
|
+
c = paint(n)
|
|
74
|
+
if c: bits.append(c)
|
|
75
|
+
print(f'{pad}TEXT {x:>5},{y:<5} {w:>4}x{h:<4} "{chars}" [{" ".join(bits)}]')
|
|
76
|
+
else:
|
|
77
|
+
bits = []
|
|
78
|
+
f = paint(n)
|
|
79
|
+
if f: bits.append(f)
|
|
80
|
+
st = paint(n, "strokes")
|
|
81
|
+
if st: bits.append(f'stroke {st} {n.get("strokeWeight","")}')
|
|
82
|
+
if n.get("cornerRadius"): bits.append(f'r{int(n["cornerRadius"])}')
|
|
83
|
+
if n.get("layoutMode"): bits.append(f'{n["layoutMode"].lower()} gap{int(n.get("itemSpacing",0))}')
|
|
84
|
+
pads = [n.get(k) for k in ("paddingLeft", "paddingTop", "paddingRight", "paddingBottom")]
|
|
85
|
+
if any(pads): bits.append("pad " + "/".join(str(int(p or 0)) for p in pads))
|
|
86
|
+
name = (n.get("name") or "")[:30]
|
|
87
|
+
print(f'{pad}{t[:5]:<5} {x:>5},{y:<5} {w:>4}x{h:<4} {name}{" ["+" ".join(bits)+"]" if bits else ""}')
|
|
88
|
+
|
|
89
|
+
for c in n.get("children", []) or []:
|
|
90
|
+
walk(c, depth + 1)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
walk(doc)
|