Changes
6 changed files (+251/-22)
-
-
@@ -92,6 +92,19 @@ /* Panel */--panel-border: 1px solid rgb(var(--color-gray-5) / 0.5); --panel-radii: 2px; /* Snackbar */ --default-snackbar-bg: rgb(var(--color-gray-8)); --default-snackbar-fg: rgb(var(--color-gray-0)); --snackbar-fg: var(--figspec-snackbar-fg, var(--default-snackbar-fg)); --snackbar-bg: var(--figspec-snackbar-bg, var(--default-snackbar-bg)); --snackbar-radius: var(--figspec-snackbar-radius, 6px); --snackbar-font-size: var(--figspec-snackbar-font-size, calc(var(--font-size) * 0.9)); --snackbar-font-family: var(--figspec-snackbar-font-family, var(--font-family-sans)); --snackbar-shadow: var(--figspec-snackbar-shadow, 0 1px 3px rgb(0 0 0 / 0.3)); --snackbar-margin: var(--figspec-snackbar-margin, 2px 4px); --snackbar-padding: var(--figspec-snackbar-padding, 4px 8px); --snackbar-border: var(--figspec-snackbar-border, none); --guide-thickness: var(--figspec-guide-thickness, 1.5px); --guide-color: var(--figspec-guide-color, rgb(var(--color-orange-9))); --guide-selected-color: var(
-
-
-
@@ -4,6 +4,8 @@ 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";
-
@@ -124,6 +126,8 @@ }` + cssCodeStyles; interface InspectorPanelProps { snackbar: Signal<SnackbarContent>; preferences: Signal<Readonly<Preferences>>; selected: Signal<figma.Node | null>;
-
@@ -132,6 +136,7 @@ onOpenPreferencesPanel(): void;} export function inspectorPanel({ snackbar: $snackbar, preferences: $preferences, selected: $selected, onOpenPreferencesPanel,
-
@@ -357,16 +362,18 @@ body: [el("code", [className("ip-text-content")], [node.characters]), ], icon: "copy", onIconClick() { // TODO: Show feedback UI after success/failure navigator.clipboard .writeText(node.characters) .then(() => { console.info("Copied to clipboard"); }) .catch((error) => { console.error("Failed to copy to clipboard", error); }); 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,
-
@@ -397,7 +404,7 @@ ],), ], icon: "copy", onIconClick: () => { onIconClick: async () => { const preferences = $preferences.once(); const css = cssgen.fromNode(node, preferences);
-
@@ -406,15 +413,17 @@ const code = css.map((style) => cssgen.serializeStyle(style, preferences)) .join("\n"); // TODO: Show feedback UI after success/failure navigator.clipboard .writeText(code) .then(() => { console.info("Copied to clipboard"); }) .catch((error) => { console.error("Failed to copy to clipboard", error); }); 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), ]); } }, }), ],
-
-
-
@@ -0,0 +1,128 @@import { attr, className, el, type ElementFn } from "../../dom"; import { compute, effect, type Signal } from "../../signal"; export const styles = /* css */ ` .sn-container { position: absolute; top: 0; left: 0; width: 100%; height: 100%; overflow: hidden; pointer-events: none; } @keyframes slidein { 0% { opacity: 0; transform: translateY(30%); } 70% { opacity: 1; } 100% { transform: translateY(0px); } } @keyframes fadein { from { opacity: 0; } to { opacity: 1; } } .sn-wrapper { position: absolute; right: 0; bottom: 0; left: 0; padding: var(--snackbar-margin); min-width: 0; width: 100%; display: inline-flex; justify-content: center; align-items: center; } .sn-bar { margin: 0; min-width: 0; display: inline-block; padding: var(--snackbar-padding); font-family: var(--snackbar-font-family); font-size: var(--snackbar-font-size); border: var(--snackbar-border); box-sizing: border-box; background-color: var(--snackbar-bg); border-radius: var(--snackbar-radius); box-shadow: var(--snackbar-shadow); color: var(--snackbar-fg); pointer-events: all; z-index: calc(var(--z-index) + 11); animation: 0.15s ease-in 0s 1 forwards slidein; } @media (prefers-reduced-motion: reduce) { .sn-bar { animation-name: fadein; } } `; export type SnackbarContent = NonNullable<Parameters<typeof el>[2]> | null; interface SnackbarProps { signal: Signal<SnackbarContent>; lifetimeMs: number; attrs?: readonly ElementFn<HTMLDivElement>[]; } export function snackbar({ signal, lifetimeMs, attrs = [], }: SnackbarProps): HTMLDivElement { effect(() => { if (!signal.get()) { return; } const timerId = setTimeout(() => { signal.set(null); }, lifetimeMs); return () => { clearTimeout(timerId); }; }); return el( "div", [...attrs, className("sn-container"), attr("aria-live", "polite")], [ compute(() => { const value = signal.get(); if (!value) { return null; } return el( "div", [className("sn-wrapper")], [el("p", [className("sn-bar")], value)], ); }), ], ); }
-
-
-
@@ -6,6 +6,7 @@ import { styles as inspectorPanel } from "./inspectorPanel/inspectorPanel";import { styles as menuBar } from "./menuBar/menuBar"; import { styles as preferencesPanel } from "./preferencesPanel/preferencesPanel"; import { styles as selectBox } from "./selectBox/selectBox"; import { styles as snackbar } from "./snackbar/snackbar"; export const styles: string = inspectorPanel +
-
@@ -15,4 +16,5 @@ fullscreenPanel +menuBar + infoItems + empty + preferencesPanel; preferencesPanel + snackbar;
-
-
-
@@ -19,6 +19,9 @@ import { fullscreenPanel } from "./fullscreenPanel/fullscreenPanel";import { inspectorPanel } from "./inspectorPanel/inspectorPanel"; import { menuBar } from "./menuBar/menuBar"; import { preferencesPanel } from "./preferencesPanel/preferencesPanel"; import { snackbar, type SnackbarContent } from "./snackbar/snackbar"; const SNACKBAR_LIFETIME = 3000; type ElementChild = NonNullable<Parameters<typeof el>[2]>[number];
-
@@ -45,6 +48,8 @@ frameCanvas: createFrameCanvas,menuSlot: createMenuSlot, caller, }: UIProps<T>): Signal<HTMLElement> { const $snackbar = new Signal<SnackbarContent>(null); return compute(() => { const s = $state.get();
-
@@ -136,6 +141,7 @@ }),inspectorPanel({ selected: $selected, preferences: $preferences, snackbar: $snackbar, onOpenPreferencesPanel() { $loadedState.set(preferences); },
-
@@ -144,7 +150,15 @@ ],); }); const layer = el("div", [], [frameCanvas, perState]); const layer = el( "div", [], [ frameCanvas, perState, snackbar({ signal: $snackbar, lifetimeMs: SNACKBAR_LIFETIME }), ], ); return layer; });
-
-
-
@@ -362,6 +362,69 @@ </td><td><code><length></code></td> <td>Font size of tooltips.</td> </tr> <tr> <td> <code>--figspec-snackbar-fg</code> </td> <td><code><color></code></td> <td>Text color of snackbars.</td> </tr> <tr> <td> <code>--figspec-snackbar-bg</code> </td> <td><code><color></code></td> <td>Background color of snackbars.</td> </tr> <tr> <td> <code>--figspec-snackbar-radius</code> </td> <td><code><length></code></td> <td>Border radius of snackbars.</td> </tr> <tr> <td> <code>--figspec-snackbar-font-size</code> </td> <td><code><length></code></td> <td>Font size of snackbars.</td> </tr> <tr> <td> <code>--figspec-snackbar-font-family</code> </td> <td>Comma separated list of font families</td> <td>Font family used in snackbars.</td> </tr> <tr> <td> <code>--figspec-snackbar-shadow</code> </td> <td>Valid value for <code>box-shadow</code></td> <td>Box shadow applies to a snackbar.</td> </tr> <tr> <td> <code>--figspec-snackbar-margin</code> </td> <td>Valid value for <code>padding</code></td> <td>Margin applies to a snackbar.</td> </tr> <tr> <td> <code>--figspec-snackbar-padding</code> </td> <td>Valid value for <code>padding</code></td> <td>Pading applies to a snackbar.</td> </tr> <tr> <td> <code>--figspec-snackbar-border</code> </td> <td>Valid value for <code>border</code> shorthand</td> <td>Border applies to a snackbar.</td> </tr> </tbody> </table> <h3 id="api_figspec_frame_viewer"><figspec-frame-viewer /></h3>
-