@nnao45/figma-use 0.1.1 → 0.1.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nnao45/figma-use",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
4
4
  "description": "Control Figma from the command line. Full read/write access for AI agents.",
5
5
  "keywords": [
6
6
  "ai",
@@ -55,6 +55,11 @@
55
55
  "build:fast": "bun build packages/cli/src/fast.ts --outfile dist/fast.mjs --target node --minify"
56
56
  },
57
57
  "dependencies": {
58
+ "@iconify-json/heroicons": "^1.2.3",
59
+ "@iconify-json/lucide": "^1.2.88",
60
+ "@iconify-json/mdi": "^1.2.3",
61
+ "@iconify-json/ph": "^1.2.2",
62
+ "@iconify-json/tabler": "^1.2.26",
58
63
  "agentfmt": "^0.1.3",
59
64
  "citty": "^0.1.6",
60
65
  "consola": "^3.4.2",
@@ -5,9 +5,25 @@ import { iconToSVG } from '@iconify/utils'
5
5
 
6
6
  import type { ReactNode } from './mini-react.ts'
7
7
  import type { Props, ReactElement } from './tree.ts'
8
- import type { IconifyIcon } from '@iconify/types'
8
+ import type { IconifyIcon, IconifyJSON } from '@iconify/types'
9
+
10
+ // Bundled icon sets (loaded on first use)
11
+ import { icons as lucideIcons } from '@iconify-json/lucide'
12
+ import { icons as mdiIcons } from '@iconify-json/mdi'
13
+ import { icons as tablerIcons } from '@iconify-json/tabler'
14
+ import { icons as heroiconsIcons } from '@iconify-json/heroicons'
15
+ import { icons as phIcons } from '@iconify-json/ph'
16
+
17
+ // Map of bundled icon sets
18
+ const bundledIconSets: Record<string, IconifyJSON> = {
19
+ lucide: lucideIcons,
20
+ mdi: mdiIcons,
21
+ tabler: tablerIcons,
22
+ heroicons: heroiconsIcons,
23
+ ph: phIcons,
24
+ }
9
25
 
10
- // Initialize API module
26
+ // Initialize API module (for fallback to remote API)
11
27
  setAPIModule('', fetchAPIModule)
12
28
 
13
29
  export interface IconData {
@@ -23,14 +39,44 @@ const iconCache = new Map<string, IconData>()
23
39
  // Raw icon data cache (before size transformation)
24
40
  const rawIconCache = new Map<string, IconifyIcon>()
25
41
 
42
+ /**
43
+ * Try to get icon from bundled icon sets
44
+ */
45
+ function getBundledIcon(name: string): IconifyIcon | null {
46
+ const [prefix, iconName] = name.split(':')
47
+ if (!prefix || !iconName) return null
48
+
49
+ const iconSet = bundledIconSets[prefix]
50
+ if (!iconSet) return null
51
+
52
+ const iconData = iconSet.icons[iconName]
53
+ if (!iconData) return null
54
+
55
+ // Merge with default values from the icon set
56
+ return {
57
+ ...iconData,
58
+ width: iconData.width ?? iconSet.width ?? 24,
59
+ height: iconData.height ?? iconSet.height ?? 24,
60
+ }
61
+ }
62
+
26
63
  /**
27
64
  * Load raw icon data (without size transformation)
65
+ * Tries bundled icon sets first, falls back to API
28
66
  */
29
67
  async function loadRawIcon(name: string): Promise<IconifyIcon | null> {
30
68
  if (rawIconCache.has(name)) {
31
69
  return rawIconCache.get(name)!
32
70
  }
33
71
 
72
+ // Try bundled icon sets first (no network needed)
73
+ const bundledIcon = getBundledIcon(name)
74
+ if (bundledIcon) {
75
+ rawIconCache.set(name, bundledIcon)
76
+ return bundledIcon
77
+ }
78
+
79
+ // Fallback to API for non-bundled icon sets
34
80
  const icon = await loadIcon(name)
35
81
  if (!icon) {
36
82
  return null
@@ -95,21 +141,34 @@ export async function preloadIcons(icons: Array<{ name: string; size?: number }>
95
141
  await Promise.all(icons.map(({ name, size }) => loadIconSvg(name, size || 24)))
96
142
  }
97
143
 
144
+ /**
145
+ * Get list of bundled icon set prefixes
146
+ */
147
+ export const bundledIconSetPrefixes = Object.keys(bundledIconSets)
148
+
149
+ /**
150
+ * Check if an icon set is bundled (available offline)
151
+ */
152
+ export function isIconSetBundled(prefix: string): boolean {
153
+ return prefix in bundledIconSets
154
+ }
155
+
98
156
  /**
99
157
  * Get list of popular icon sets
158
+ * Sets marked with (bundled) are available offline
100
159
  */
101
160
  export const iconSets = {
102
- mdi: 'Material Design Icons',
103
- lucide: 'Lucide',
104
- heroicons: 'Heroicons',
161
+ lucide: 'Lucide (bundled)',
162
+ mdi: 'Material Design Icons (bundled)',
163
+ tabler: 'Tabler Icons (bundled)',
164
+ heroicons: 'Heroicons (bundled)',
165
+ ph: 'Phosphor (bundled)',
105
166
  'heroicons-outline': 'Heroicons Outline',
106
167
  'heroicons-solid': 'Heroicons Solid',
107
- tabler: 'Tabler Icons',
108
168
  'fa-solid': 'Font Awesome Solid',
109
169
  'fa-regular': 'Font Awesome Regular',
110
170
  'fa-brands': 'Font Awesome Brands',
111
171
  ri: 'Remix Icon',
112
- ph: 'Phosphor',
113
172
  'ph-bold': 'Phosphor Bold',
114
173
  'ph-fill': 'Phosphor Fill',
115
174
  carbon: 'Carbon',
@@ -125,6 +125,26 @@ export interface StyleProps {
125
125
  // Other
126
126
  src?: string
127
127
  href?: string
128
+
129
+ // Line stroke caps (for start/end independently)
130
+ startCap?:
131
+ | 'none'
132
+ | 'round'
133
+ | 'square'
134
+ | 'arrow'
135
+ | 'arrow-equilateral'
136
+ | 'triangle'
137
+ | 'diamond'
138
+ | 'circle'
139
+ endCap?:
140
+ | 'none'
141
+ | 'round'
142
+ | 'square'
143
+ | 'arrow'
144
+ | 'arrow-equilateral'
145
+ | 'triangle'
146
+ | 'diamond'
147
+ | 'circle'
128
148
  }
129
149
 
130
150
  // Custom child type that allows TreeNode in JSX
@@ -57,6 +57,42 @@ function loadFont(family: string, style: string): Promise<void> | void {
57
57
  return promise
58
58
  }
59
59
 
60
+ const LINE_CAP_VALUES = [
61
+ 'none',
62
+ 'round',
63
+ 'square',
64
+ 'arrow',
65
+ 'arrow-lines',
66
+ 'arrow-equilateral',
67
+ 'triangle',
68
+ 'diamond',
69
+ 'circle'
70
+ ]
71
+
72
+ const LINE_CAP_MAP: Record<string, StrokeCap> = {
73
+ none: 'NONE',
74
+ round: 'ROUND',
75
+ square: 'SQUARE',
76
+ arrow: 'ARROW_LINES',
77
+ 'arrow-lines': 'ARROW_LINES',
78
+ 'arrow-equilateral': 'ARROW_EQUILATERAL',
79
+ triangle: 'TRIANGLE_FILLED',
80
+ diamond: 'DIAMOND_FILLED',
81
+ circle: 'CIRCLE_FILLED'
82
+ }
83
+
84
+ function normalizeLineCap(cap?: string): StrokeCap | undefined {
85
+ if (!cap) return undefined
86
+ const key = cap.toLowerCase()
87
+ const mapped = LINE_CAP_MAP[key]
88
+ if (!mapped) {
89
+ throw new Error(
90
+ `Invalid stroke cap "${cap}". Allowed: ${LINE_CAP_VALUES.map((v) => `"${v}"`).join(', ')}`
91
+ )
92
+ }
93
+ return mapped
94
+ }
95
+
60
96
  // Fast node creation for batch operations - skips full serialization
61
97
  async function createNodeFast(
62
98
  command: string,
@@ -589,16 +625,50 @@ async function handleCommand(command: string, args?: unknown): Promise<unknown>
589
625
  }
590
626
 
591
627
  case 'create-line': {
592
- const { x, y, length, rotation, name, parentId, stroke, strokeWeight } = args as {
593
- x: number
594
- y: number
595
- length: number
596
- rotation?: number
597
- name?: string
598
- parentId?: string
599
- stroke?: string
600
- strokeWeight?: number
628
+ const { x, y, length, rotation, name, parentId, stroke, strokeWeight, startCap, endCap } =
629
+ args as {
630
+ x: number
631
+ y: number
632
+ length: number
633
+ rotation?: number
634
+ name?: string
635
+ parentId?: string
636
+ stroke?: string
637
+ strokeWeight?: number
638
+ startCap?: string
639
+ endCap?: string
640
+ }
641
+ const startCapValue = normalizeLineCap(startCap)
642
+ const endCapValue = normalizeLineCap(endCap)
643
+ const useVector =
644
+ (startCapValue && startCapValue !== 'NONE') || (endCapValue && endCapValue !== 'NONE')
645
+
646
+ // If caps are specified, use VectorNetwork for independent start/end caps
647
+ if (useVector) {
648
+ const vector = figma.createVector()
649
+ vector.x = x
650
+ vector.y = y
651
+
652
+ vector.vectorNetwork = {
653
+ vertices: [
654
+ { x: 0, y: 0, strokeCap: startCapValue ?? 'NONE' },
655
+ { x: length, y: 0, strokeCap: endCapValue ?? 'NONE' }
656
+ ],
657
+ segments: [
658
+ { start: 0, end: 1, tangentStart: { x: 0, y: 0 }, tangentEnd: { x: 0, y: 0 } }
659
+ ],
660
+ regions: []
661
+ }
662
+
663
+ if (stroke) vector.strokes = [await createSolidPaint(stroke)]
664
+ if (strokeWeight !== undefined) vector.strokeWeight = strokeWeight
665
+ if (rotation) vector.rotation = rotation
666
+ if (name) vector.name = name
667
+ await appendToParent(vector, parentId)
668
+ return serializeNode(vector)
601
669
  }
670
+
671
+ // No caps specified - use simple Line
602
672
  const line = figma.createLine()
603
673
  line.x = x
604
674
  line.y = y
@@ -55,6 +55,42 @@ function loadFont(family: string, style: string): Promise<void> | void {
55
55
  return promise
56
56
  }
57
57
 
58
+ const LINE_CAP_VALUES = [
59
+ 'none',
60
+ 'round',
61
+ 'square',
62
+ 'arrow',
63
+ 'arrow-lines',
64
+ 'arrow-equilateral',
65
+ 'triangle',
66
+ 'diamond',
67
+ 'circle'
68
+ ]
69
+
70
+ const LINE_CAP_MAP: Record<string, StrokeCap> = {
71
+ none: 'NONE',
72
+ round: 'ROUND',
73
+ square: 'SQUARE',
74
+ arrow: 'ARROW_LINES',
75
+ 'arrow-lines': 'ARROW_LINES',
76
+ 'arrow-equilateral': 'ARROW_EQUILATERAL',
77
+ triangle: 'TRIANGLE_FILLED',
78
+ diamond: 'DIAMOND_FILLED',
79
+ circle: 'CIRCLE_FILLED'
80
+ }
81
+
82
+ function normalizeLineCap(cap?: string): StrokeCap | undefined {
83
+ if (!cap) return undefined
84
+ const key = cap.toLowerCase()
85
+ const mapped = LINE_CAP_MAP[key]
86
+ if (!mapped) {
87
+ throw new Error(
88
+ `Invalid stroke cap "${cap}". Allowed: ${LINE_CAP_VALUES.map((v) => `"${v}"`).join(', ')}`
89
+ )
90
+ }
91
+ return mapped
92
+ }
93
+
58
94
  // Fast node creation for batch operations - skips full serialization
59
95
  async function createNodeFast(
60
96
  command: string,
@@ -863,16 +899,50 @@ async function handleCommand(command: string, args?: unknown): Promise<unknown>
863
899
  }
864
900
 
865
901
  case 'create-line': {
866
- const { x, y, length, rotation, name, parentId, stroke, strokeWeight } = args as {
867
- x: number
868
- y: number
869
- length: number
870
- rotation?: number
871
- name?: string
872
- parentId?: string
873
- stroke?: string
874
- strokeWeight?: number
875
- }
902
+ const { x, y, length, rotation, name, parentId, stroke, strokeWeight, startCap, endCap } =
903
+ args as {
904
+ x: number
905
+ y: number
906
+ length: number
907
+ rotation?: number
908
+ name?: string
909
+ parentId?: string
910
+ stroke?: string
911
+ strokeWeight?: number
912
+ startCap?: string
913
+ endCap?: string
914
+ }
915
+ const startCapValue = normalizeLineCap(startCap)
916
+ const endCapValue = normalizeLineCap(endCap)
917
+ const useVector =
918
+ (startCapValue && startCapValue !== 'NONE') || (endCapValue && endCapValue !== 'NONE')
919
+
920
+ // If caps are specified, use VectorNetwork for independent start/end caps
921
+ if (useVector) {
922
+ const vector = figma.createVector()
923
+ vector.x = x
924
+ vector.y = y
925
+
926
+ vector.vectorNetwork = {
927
+ vertices: [
928
+ { x: 0, y: 0, strokeCap: startCapValue ?? 'NONE' },
929
+ { x: length, y: 0, strokeCap: endCapValue ?? 'NONE' }
930
+ ],
931
+ segments: [
932
+ { start: 0, end: 1, tangentStart: { x: 0, y: 0 }, tangentEnd: { x: 0, y: 0 } }
933
+ ],
934
+ regions: []
935
+ }
936
+
937
+ if (stroke) vector.strokes = [await createSolidPaint(stroke)]
938
+ if (strokeWeight !== undefined) vector.strokeWeight = strokeWeight
939
+ if (rotation) vector.rotation = rotation
940
+ if (name) vector.name = name
941
+ await appendToParent(vector, parentId)
942
+ return serializeNode(vector)
943
+ }
944
+
945
+ // No caps specified - use simple Line
876
946
  const line = figma.createLine()
877
947
  line.x = x
878
948
  line.y = y
@@ -2390,7 +2460,9 @@ async function handleCommand(command: string, args?: unknown): Promise<unknown>
2390
2460
  overflow: '__overflow',
2391
2461
  shadow: '__shadow',
2392
2462
  blur: '__blur',
2393
- blendMode: '__blendMode'
2463
+ blendMode: '__blendMode',
2464
+ startCap: '__startCap',
2465
+ endCap: '__endCap'
2394
2466
  }
2395
2467
 
2396
2468
  const DIRECTION_MAP: Record<string, string> = {
@@ -2433,6 +2505,17 @@ async function handleCommand(command: string, args?: unknown): Promise<unknown>
2433
2505
  shadow?: string
2434
2506
  blur?: number
2435
2507
  blendMode?: string
2508
+ startCap?: string
2509
+ endCap?: string
2510
+ }> = []
2511
+
2512
+ // Nodes that need vector replacement for stroke caps
2513
+ const lineCapNodes: Array<{
2514
+ path: number[]
2515
+ startCap?: StrokeCap
2516
+ endCap?: StrokeCap
2517
+ stroke?: string
2518
+ strokeWidth?: number
2436
2519
  }> = []
2437
2520
 
2438
2521
  function processProps(
@@ -2548,6 +2631,33 @@ async function handleCommand(command: string, args?: unknown): Promise<unknown>
2548
2631
  const isText = type === 'text'
2549
2632
  const processed = processProps(props || {}, isText)
2550
2633
 
2634
+ // Handle <line> with startCap/endCap - mark for VectorNetwork replacement
2635
+ if (type === 'line' && (props.startCap || props.endCap)) {
2636
+ const startCapValue = normalizeLineCap(props.startCap as string | undefined)
2637
+ const endCapValue = normalizeLineCap(props.endCap as string | undefined)
2638
+ const useVector =
2639
+ (startCapValue && startCapValue !== 'NONE') || (endCapValue && endCapValue !== 'NONE')
2640
+
2641
+ if (useVector) {
2642
+ lineCapNodes.push({
2643
+ path: [...path],
2644
+ startCap: startCapValue,
2645
+ endCap: endCapValue,
2646
+ stroke: processed.stroke as string | undefined,
2647
+ strokeWidth: processed.strokeWidth as number | undefined
2648
+ })
2649
+ }
2650
+
2651
+ // Don't pass startCap/endCap to Widget Line
2652
+ const cleanProps = { ...processed }
2653
+ delete cleanProps.startCap
2654
+ delete cleanProps.endCap
2655
+ delete cleanProps.__startCap
2656
+ delete cleanProps.__endCap
2657
+ // Create normal Line as placeholder
2658
+ return h(Line, cleanProps)
2659
+ }
2660
+
2551
2661
  // Track wrap nodes
2552
2662
  if (processed.__wrap) {
2553
2663
  wrapNodes.push({ path: [...path], rowGap: processed.__rowGap as number | undefined })
@@ -2579,7 +2689,9 @@ async function handleCommand(command: string, args?: unknown): Promise<unknown>
2579
2689
  '__overflow',
2580
2690
  '__shadow',
2581
2691
  '__blur',
2582
- '__blendMode'
2692
+ '__blendMode',
2693
+ '__startCap',
2694
+ '__endCap'
2583
2695
  ]
2584
2696
  for (const key of postKeys) {
2585
2697
  if (processed[key] !== undefined) {
@@ -2602,7 +2714,7 @@ async function handleCommand(command: string, args?: unknown): Promise<unknown>
2602
2714
  }
2603
2715
 
2604
2716
  const widgetTree = buildTree(tree)
2605
- const node = await figma.createNodeFromJSXAsync(
2717
+ let node = await figma.createNodeFromJSXAsync(
2606
2718
  widgetTree as Parameters<typeof figma.createNodeFromJSXAsync>[0]
2607
2719
  )
2608
2720
 
@@ -2797,6 +2909,94 @@ async function handleCommand(command: string, args?: unknown): Promise<unknown>
2797
2909
  }
2798
2910
  }
2799
2911
 
2912
+ // Replace Line placeholders with Vector nodes for stroke cap support
2913
+ const copyLineStyle = (from: LineNode, to: VectorNode) => {
2914
+ const keys = [
2915
+ 'visible',
2916
+ 'locked',
2917
+ 'opacity',
2918
+ 'blendMode',
2919
+ 'effects',
2920
+ 'effectStyleId',
2921
+ 'fills',
2922
+ 'fillStyleId',
2923
+ 'strokes',
2924
+ 'strokeStyleId',
2925
+ 'strokeAlign',
2926
+ 'strokeJoin',
2927
+ 'strokeMiterLimit',
2928
+ 'dashPattern',
2929
+ 'layoutAlign',
2930
+ 'layoutGrow',
2931
+ 'layoutPositioning',
2932
+ 'constraints'
2933
+ ]
2934
+ for (const key of keys) {
2935
+ const value = (from as Record<string, unknown>)[key]
2936
+ if (value !== undefined && value !== figma.mixed) {
2937
+ ;(to as Record<string, unknown>)[key] = value
2938
+ }
2939
+ }
2940
+ }
2941
+ for (const { path, startCap, endCap, stroke, strokeWidth } of lineCapNodes) {
2942
+ let target: SceneNode = node
2943
+ let parent: FrameNode | null = null
2944
+ let childIndex = 0
2945
+ for (let i = 0; i < path.length; i++) {
2946
+ if ('children' in target) {
2947
+ parent = target as FrameNode
2948
+ childIndex = path[i]
2949
+ target = parent.children[childIndex]
2950
+ }
2951
+ }
2952
+ if (target && target.type === 'LINE') {
2953
+ const lineNode = target as LineNode
2954
+ const vector = figma.createVector()
2955
+ vector.x = lineNode.x
2956
+ vector.y = lineNode.y
2957
+ vector.name = lineNode.name
2958
+ vector.rotation = lineNode.rotation
2959
+ const length = lineNode.width
2960
+
2961
+ vector.vectorNetwork = {
2962
+ vertices: [
2963
+ { x: 0, y: 0, strokeCap: startCap ?? 'NONE' },
2964
+ { x: length, y: 0, strokeCap: endCap ?? 'NONE' }
2965
+ ],
2966
+ segments: [
2967
+ { start: 0, end: 1, tangentStart: { x: 0, y: 0 }, tangentEnd: { x: 0, y: 0 } }
2968
+ ],
2969
+ regions: []
2970
+ }
2971
+
2972
+ // Copy stroke properties
2973
+ if (stroke) {
2974
+ vector.strokes = [await createSolidPaint(stroke)]
2975
+ } else if (lineNode.strokes.length > 0) {
2976
+ vector.strokes = [...lineNode.strokes]
2977
+ }
2978
+ if (strokeWidth !== undefined) {
2979
+ vector.strokeWeight = strokeWidth
2980
+ } else {
2981
+ vector.strokeWeight = lineNode.strokeWeight
2982
+ }
2983
+
2984
+ copyLineStyle(lineNode, vector)
2985
+
2986
+ const lineParent = lineNode.parent
2987
+ if (lineParent && 'insertChild' in lineParent) {
2988
+ const index = lineParent.children.indexOf(lineNode as SceneNode)
2989
+ lineParent.insertChild(index === -1 ? childIndex : index, vector)
2990
+ lineNode.remove()
2991
+ if (path.length === 0) node = vector
2992
+ } else {
2993
+ figma.currentPage.appendChild(vector)
2994
+ lineNode.remove()
2995
+ if (path.length === 0) node = vector
2996
+ }
2997
+ }
2998
+ }
2999
+
2800
3000
  // Attach to parent
2801
3001
  if (parentId) {
2802
3002
  const parent = await figma.getNodeByIdAsync(parentId)