-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
-
23
-
24
-
25
-
26
-
27
-
28
-
29
-
30
-
31
-
32
-
33
-
34
-
35
-
36
-
37
-
38
-
39
-
40
-
41
-
42
-
43
-
44
-
45
-
46
-
47
-
48
-
49
-
50
-
51
-
52
-
53
-
54
-
55
-
56
-
57
-
58
-
59
-
60
-
61
-
62
-
63
-
64
-
65
-
66
-
67
-
68
-
69
-
70
-
71
-
72
-
73
-
74
-
75
-
76
-
77
-
78
-
79
-
80
-
81
-
82
-
83
-
84
-
85
-
86
-
87
-
88
-
89
-
90
-
91
-
92
-
93
-
94
-
95
-
96
-
97
-
98
-
99
-
100
-
101
-
102
-
103
-
104
-
105
-
106
-
107
-
108
-
109
-
110
-
111
-
112
-
113
-
114
-
115
-
116
-
117
-
118
-
119
-
120
-
121
-
122
-
123
-
124
-
125
-
126
-
127
-
128
-
129
-
130
-
131
-
132
-
133
-
134
-
135
-
136
-
137
-
138
-
139
-
140
-
141
-
142
-
143
-
144
-
145
-
146
-
147
-
148
-
149
-
150
-
151
-
152
-
153
-
154
-
155
-
156
-
157
-
158
-
159
-
160
-
161
-
162
-
163
-
164
-
165
-
166
-
167
-
168
-
169
-
170
-
171
-
172
-
173
-
174
-
175
-
176
-
177
-
178
-
179
-
180
-
181
-
182
-
183
-
184
-
185
-
186
-
187
-
188
-
189
-
190
-
191
-
192
-
193
-
194
-
195
-
196
-
197
-
198
-
199
-
200
-
201
-
202
-
203
-
204
-
205
-
206
-
207
-
208
-
209
-
210
-
211
-
212
-
213
-
214
-
215
-
216
-
217
-
218
-
219
-
220
-
221
-
222
-
223
-
224
-
225
-
226
-
227
-
228
-
229
-
230
-
231
-
232
-
233
-
234
-
235
-
236
-
237
-
238
-
239
-
240
-
241
-
242
-
243
-
244
-
245
-
246
-
247
-
248
-
249
-
250
-
251
-
252
-
253
-
254
-
255
-
256
-
257
-
258
-
259
-
260
-
261
-
262
-
263
-
264
-
265
-
266
-
267
-
268
-
269
-
270
-
271
-
272
-
273
-
274
-
275
-
276
-
277
-
278
-
279
-
280
-
281
-
282
-
283
-
284
-
285
-
286
-
287
-
288
-
289
-
290
-
291
-
292
-
293
-
294
-
295
-
296
-
297
-
298
-
299
-
300
-
301
-
302
-
303
-
304
-
305
-
306
-
307
-
308
-
309
-
310
-
311
-
312
-
313
-
314
-
315
-
316
-
317
-
318
-
319
-
320
-
321
-
322
-
323
-
324
-
325
-
326
-
327
-
328
-
329
-
330
-
331
-
332
-
333
-
334
-
335
-
336
-
337
-
338
-
339
-
340
-
341
-
342
-
343
-
344
-
345
-
346
-
347
-
348
-
349
-
350
-
351
-
352
-
353
-
354
-
355
-
356
-
357
-
358
-
359
-
360
-
361
-
362
-
363
-
364
-
365
-
366
-
367
-
368
-
369
-
370
-
371
-
372
-
373
-
374
-
375
-
376
-
377
-
378
-
379
-
380
-
381
-
382
-
383
-
384
-
385
-
386
-
387
-
388
-
389
-
390
-
391
-
392
-
393
-
394
-
395
-
396
-
397
-
398
-
399
-
400
-
401
-
402
-
403
-
404
-
405
-
406
-
407
-
408
-
409
-
410
-
411
-
412
-
413
-
414
-
415
-
416
-
417
-
418
-
419
-
420
-
421
-
422
-
423
-
424
-
425
-
426
-
427
-
428
-
429
-
430
-
431
-
432
import { attr, className, el, on } from "../../dom";
import * as figma from "../../figma";
import { roundTo } from "../../math";
import { type Preferences } from "../../preferences";
import { compute, effect, Signal } from "../../signal";
import { type SnackbarContent } from "../snackbar/snackbar";
import { cssCode, styles as cssCodeStyles } from "./cssCode";
import * as cssgen from "./cssgen/cssgen";
import { section } from "./section";
export const styles =
/* css */ `
.ip-root {
position: absolute;
height: 100%;
width: 300px;
right: 0;
border-left: var(--panel-border);
background: var(--bg);
color: var(--fg);
overflow-y: auto;
z-index: calc(var(--z-index) + 10);
}
.ip-root:focus-visible {
box-shadow: inset 0 0 0 2px SelectedItem;
outline: none;
}
.ip-section {
padding: 16px;
border-bottom: var(--panel-border);
}
.ip-section-heading {
display: flex;
align-items: center;
margin: 0;
margin-bottom: 12px;
}
.ip-section-heading-title {
flex-grow: 1;
flex-shrink: 1;
font-size: calc(var(--font-size) * 1);
margin: 0;
}
.ip-style-section {
margin-bottom: 12px;
}
.ip-overview {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px 24px;
margin: 0;
margin-top: 16px;
}
.ip-prop {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
margin: 0;
gap: 2px;
}
.ip-prop-label {
font-weight: bold;
font-size: calc(var(--font-size) * 0.7);
color: var(--subtle-fg);
}
.ip-prop-value {
font-size: calc(var(--font-size) * 0.9);
color: var(--fg);
user-select: text;
}
.ip-text-content {
display: block;
width: 100%;
padding: 8px;
box-sizing: border-box;
font-family: var(--font-family-mono);
font-size: calc(var(--font-size) * 0.8);
background: var(--code-bg);
border-radius: var(--panel-radii);
color: var(--code-text);
user-select: text;
}
.ip-options {
display: flex;
justify-content: flex-end;
margin-top: 8px;
}
.ip-pref-action {
appearance: none;
border: 1px solid var(--action-border);
padding: var(--action-vertical-padding) var(--action-horizontal-padding);
background: transparent;
border-radius: var(--action-radius);
color: var(--fg);
cursor: pointer;
}
.ip-pref-action:hover {
background-color: var(--action-overlay);
}
.ip-pref-action:focus {
outline: none;
}
.ip-pref-action:focus-visible {
border-color: SelectedItem;
outline: 1px solid SelectedItem;
}
` + cssCodeStyles;
interface InspectorPanelProps {
snackbar: Signal<SnackbarContent>;
preferences: Signal<Readonly<Preferences>>;
selected: Signal<figma.Node | null>;
onOpenPreferencesPanel(): void;
}
export function inspectorPanel({
snackbar: $snackbar,
preferences: $preferences,
selected: $selected,
onOpenPreferencesPanel,
}: InspectorPanelProps): Signal<HTMLElement | null> {
effect(() => {
// No need to rerun this effect on node-to-node changes
if (!compute(() => !!$selected.get()).get()) {
return;
}
const onEsc = (ev: KeyboardEvent) => {
if (ev.key !== "Escape" || ev.isComposing) {
return;
}
ev.preventDefault();
ev.stopPropagation();
$selected.set(null);
};
document.addEventListener("keydown", onEsc);
return () => {
document.removeEventListener("keydown", onEsc);
};
});
return compute(() => {
const node = $selected.get();
if (!node) {
return null;
}
return el(
"div",
[className("ip-root")],
[
section({
title: node.name,
body: [
el(
"div",
[className("ip-overview")],
[
el(
"p",
[className("ip-prop")],
[
el("span", [className("ip-prop-label")], ["Type:"]),
el("span", [className("ip-prop-value")], [node.type]),
],
),
],
),
figma.hasBoundingBox(node)
? el(
"div",
[className("ip-overview")],
[
el(
"p",
[className("ip-prop")],
[
el("span", [className("ip-prop-label")], ["Width:"]),
el(
"span",
[className("ip-prop-value")],
[
compute(
() =>
roundTo(
node.absoluteBoundingBox.width,
$preferences.get().decimalPlaces,
) + "px",
),
],
),
],
),
el(
"p",
[className("ip-prop")],
[
el("span", [className("ip-prop-label")], ["Height:"]),
el(
"span",
[className("ip-prop-value")],
[
compute(
() =>
roundTo(
node.absoluteBoundingBox.height,
$preferences.get().decimalPlaces,
) + "px",
),
],
),
],
),
],
)
: null,
figma.hasTypeStyle(node)
? el(
"div",
[className("ip-overview")],
[
el(
"p",
[className("ip-prop")],
[
el("span", [className("ip-prop-label")], ["Font:"]),
el(
"span",
[className("ip-prop-value")],
[
node.style.fontPostScriptName ||
node.style.fontFamily,
],
),
],
),
],
)
: null,
],
icon: "close",
onIconClick: () => {
$selected.set(null);
},
}),
figma.hasPadding(node) &&
(node.paddingTop > 0 ||
node.paddingRight > 0 ||
node.paddingBottom > 0 ||
node.paddingLeft > 0)
? section({
title: "Padding",
body: [
el(
"p",
[className("ip-prop")],
[
el("span", [className("ip-prop-label")], ["Top:"]),
el(
"span",
[className("ip-prop-value")],
[node.paddingTop.toString(10)],
),
],
),
el(
"p",
[className("ip-prop")],
[
el("span", [className("ip-prop-label")], ["Right:"]),
el(
"span",
[className("ip-prop-value")],
[node.paddingRight.toString(10)],
),
],
),
el(
"p",
[className("ip-prop")],
[
el("span", [className("ip-prop-label")], ["Bottom:"]),
el(
"span",
[className("ip-prop-value")],
[node.paddingBottom.toString(10)],
),
],
),
el(
"p",
[className("ip-prop")],
[
el("span", [className("ip-prop-label")], ["Left:"]),
el(
"span",
[className("ip-prop-value")],
[node.paddingLeft.toString(10)],
),
],
),
],
})
: figma.hasLegacyPadding(node) &&
(node.horizontalPadding > 0 || node.verticalPadding > 0)
? section({
title: "Layout",
body: [
node.horizontalPadding > 0
? el(
"p",
[className("ip-prop")],
[
el("span", [], ["Padding(H): "]),
node.horizontalPadding.toString(10),
],
)
: null,
node.verticalPadding > 0
? el(
"p",
[className("ip-prop")],
[
el("span", [], ["Padding(V): "]),
node.verticalPadding.toString(10),
],
)
: null,
],
})
: null,
figma.hasCharacters(node)
? section({
title: "Content",
body: [
el("code", [className("ip-text-content")], [node.characters]),
],
icon: "copy",
async onIconClick() {
try {
await navigator.clipboard.writeText(node.characters);
$snackbar.set(["Copied text content to clipboard"]);
} catch (error) {
console.error("Failed to copy text content", error);
$snackbar.set([
"Failed to copy text content: ",
error instanceof Error ? error.message : String(error),
]);
}
},
})
: null,
section({
title: "CSS",
body: [
compute(() => {
const preferences = $preferences.get();
const css = cssgen.fromNode(node, preferences);
return cssCode(css, preferences);
}),
el(
"div",
[className("ip-options")],
[
el(
"button",
[
className("ip-pref-action"),
on("click", () => {
onOpenPreferencesPanel();
}),
],
["Customize"],
),
],
),
],
icon: "copy",
onIconClick: async () => {
const preferences = $preferences.once();
const css = cssgen.fromNode(node, preferences);
const code = css
.map((style) => cssgen.serializeStyle(style, preferences))
.join("\n");
try {
await navigator.clipboard.writeText(code);
$snackbar.set(["Copied CSS code to clipboard"]);
} catch (error) {
console.error("Failed to copy CSS code", error);
$snackbar.set([
"Failed to copy CSS code: ",
error instanceof Error ? error.message : String(error),
]);
}
},
}),
],
);
});
}