https://github.com/rodydavis/figma_flutter_plugin
Example of how to use Flutter Web in a Figma Pluigin
https://github.com/rodydavis/figma_flutter_plugin
figjam figjam-plugin figma figma-plugin flutter
Last synced: 20 days ago
JSON representation
Example of how to use Flutter Web in a Figma Pluigin
- Host: GitHub
- URL: https://github.com/rodydavis/figma_flutter_plugin
- Owner: rodydavis
- License: apache-2.0
- Created: 2023-05-12T21:09:32.000Z (almost 2 years ago)
- Default Branch: master
- Last Pushed: 2023-06-01T23:22:59.000Z (almost 2 years ago)
- Last Synced: 2025-04-18T17:30:34.110Z (24 days ago)
- Topics: figjam, figjam-plugin, figma, figma-plugin, flutter
- Language: Dart
- Homepage:
- Size: 9.94 MB
- Stars: 11
- Watchers: 3
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# figma_flutter_plugin
This is an example of how to build a Figma plugin using Flutter web and inline all the assets in the single html file as required by the Figma plugin API.
## Features
- Tree shaking for Material Icons
- Inline assets / fonts
- Offline support in Figma (Not using remote views)
- Two way communication between Flutter and Figma
- Figma and FigJam support
- Build scripts for generating figma project `/build/figma`
- Typings for Figma API
- Light and Dark mode support## Prerequisites
### Plugin ID
You will need to get a new Figma plugin ID by opening Figma Desktop App and creating a new plugin. Then copy the ID to the `figma/manifest.json` and also update the name key.
### Typings
Make sure to check out the repo with submodules if you want the Figma typings.
```bash
git submodule update --init --recursive
```### Flutter
You need to update the name and description in `pubspec.yaml`.
### Figma
To use the plugin you need to import the manifest from the `build/figma` folder, not the top level `figma` folder.
Run the build script:
```bash
dart scripts/build.dart
```Then open figma and import the manifest.json from the `build/figma` folder.
## Screenshots
### Figma

### FigJam

## Example
### UI
```dart
import 'package:flutter/material.dart';import 'figma.dart';
class Example extends StatefulWidget {
const Example({
super.key,
required this.title,
required this.seedColor,
required this.isFigma,
});final String title;
final Color seedColor;
final bool isFigma;@override
State createState() => _ExampleState();
}class _ExampleState extends State {
final controller = TextEditingController(text: '5');
final formKey = GlobalKey();
final api = FigmaApi();late int red = widget.seedColor.red;
late int green = widget.seedColor.green;
late int blue = widget.seedColor.blue;@override
void initState() {
if (widget.isFigma) {
api.init().then((value) => setState(() {}));
} else {
api.initialized = true;
}
super.initState();
}@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colors = theme.colorScheme;
final color = Color.fromARGB(255, red, green, blue);
final onColor = color.onColor();
return Theme(
data: theme.copyWith(
colorScheme: ColorScheme.fromSeed(
seedColor: color,
brightness: colors.brightness,
),
),
child: Scaffold(
appBar: AppBar(
title: Text(widget.title),
actions: [
Builder(builder: (context) {
return IconButton(
icon: const Icon(Icons.settings_outlined),
onPressed: () async {
showBottomSheet(
context: context,
builder: (context) {
return Material(
child: Column(
children: [
ListTile(
leading: Icon(Icons.notifications),
title: Text('Show notification'),
onTap: () => api.notify('Hello from Flutter!'),
),
ListTile(
leading: Icon(Icons.close),
title: Text('Close Plugin'),
textColor: colors.error,
iconColor: colors.error,
onTap: () => api.closePlugin(),
),
],
),
);
},
);
},
);
}),
],
),
body: !api.initialized
? const CircularProgressIndicator()
: Form(
key: formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
ListTile(
leading: const Icon(Icons.color_lens, color: Colors.red),
title: Text('Red'),
subtitle: Slider(
value: red.toDouble(),
min: 0,
max: 255,
divisions: 255,
onChanged: (value) =>
setState(() => red = value.toInt()),
),
),
const SizedBox(height: 16),
ListTile(
leading:
const Icon(Icons.color_lens, color: Colors.green),
title: Text('Green'),
subtitle: Slider(
value: green.toDouble(),
min: 0,
max: 255,
divisions: 255,
onChanged: (value) =>
setState(() => green = value.toInt()),
),
),
const SizedBox(height: 16),
ListTile(
leading: const Icon(Icons.color_lens, color: Colors.blue),
title: Text('Blue'),
subtitle: Slider(
value: blue.toDouble(),
min: 0,
max: 255,
divisions: 255,
onChanged: (value) =>
setState(() => blue = value.toInt()),
),
),
const SizedBox(height: 32),
TextFormField(
controller: controller,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Number of rectangles',
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a number';
}
if (int.tryParse(value) == null) {
return 'Please enter a valid number';
}
return null;
},
),
const SizedBox(height: 16),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: color,
foregroundColor: onColor,
),
onPressed: () {
if (!formKey.currentState!.validate()) {
return;
}
formKey.currentState!.save();
api.createShapes(int.parse(controller.text), color);
},
label: const Text('Create rectangles'),
icon: const Icon(Icons.add, size: 18),
),
],
),
),
),
);
}
}extension on FigmaApi {
Future createShapes(int count, Color color) async {
final ids = [];
for (var i = 0; i < count; i++) {
if (type == FigmaEditorType.figma) {
final res = await execMethod(
'createRectangle',
attributes: {
'x': i * 150,
'fills': [
{
'type': 'SOLID',
'color': color.toFigma(),
},
],
},
keys: ['id', 'name'],
);
final result = res['result'] as Map;
final id = result['id']?.toString();
if (id != null) ids.add(id);
} else {
final res = await execMethod(
'createShapeWithText',
attributes: {
'shapeType': FigJamShapeType.ROUNDED_RECTANGLE.name,
'fills': [
{
'type': 'SOLID',
'color': color.toFigma(),
},
],
},
keys: ['id', 'name', 'width'],
);
print(res);
final result = res['result'] as Map;
final id = result['id']?.toString();
if (id != null) {
ids.add(id);
final width = num.parse(result['width'].toString());
await nodeOptions(id, attributes: {
'x': i * (width + 200),
});
}
}
}if (type == FigmaEditorType.figJam) {
for (var i = 0; i < ids.length - 1; i++) {
await execMethod(
'createConnector',
attributes: {
'strokeWeight': 8,
'connectorStart': {
'endpointNodeId': ids[i],
'magnet': 'AUTO',
},
'connectorEnd': {
'endpointNodeId': ids[i + 1],
'magnet': 'AUTO',
},
},
);
}
}await appendToCurrentPage(ids);
await setSelection(ids);
await scrollAndZoomIntoView(ids);
}
}```
### Figma
```dart
import 'dart:async';
import 'dart:html' as html;import 'package:flutter/material.dart';
class FigmaApi {
FigmaEditorType type = FigmaEditorType.figma;
String command = '';
String pluginId = '';
bool initialized = false;Future init() async {
final info = await _result('init');
final result = info['result'] as Map;
final editorType = (result['editorType'] ?? 'figma').toString();
if (editorType == 'figma') {
type = FigmaEditorType.figma;
} else {
type = FigmaEditorType.figJam;
}
command = result['command']?.toString() ?? '';
pluginId = result['id']?.toString() ?? '';
initialized = true;
print('Editor: $type');
print('Command: "$command"');
print('Plugin ID: $pluginId');
}Future execMethod(
String name, {
List args = const [],
FigmaJson attributes = const {},
List? keys,
}) async {
return _result('function', {
'name': name,
'attrs': attributes,
'args': args,
if (keys != null) 'keys': keys,
});
}Future notify(
String message, {
int? timeout,
bool? error,
}) async {
return execMethod(
'notify',
args: [message],
attributes: {
if (timeout != null) 'timeout': timeout,
if (error != null) 'error': error,
},
);
}Future execCallback(
String name, {
FigmaJson attributes = const {},
}) async {
return _result(name, attributes);
}Future nodeOptions(
String nodeId, {
FigmaJson attributes = const {},
List? keys,
}) async {
return _result('node-options', {
'node_id': nodeId,
'attrs': attributes,
if (keys != null) 'keys': keys,
});
}Future appendToCurrentPage(List ids) async {
await execCallback('append-to-current-page', attributes: {'ids': ids});
}Future setSelection(List ids) async {
await execCallback('set-selection', attributes: {'ids': ids});
}Future scrollAndZoomIntoView(List ids) async {
await execCallback('scroll-and-zoom-into-view', attributes: {'ids': ids});
}Future closePlugin([String? message]) async {
await execMethod('closePlugin', args: [
if (message != null) message,
]);
}void _send(
String type, [
FigmaJson data = const {},
]) {
final parent = html.window.parent!;
final message = {
'pluginMessage': {'msg_type': type, ...data}
};
parent.postMessage(message, '*');
}void _receive(ValueChanged callback) {
final parent = html.document.getElementById('output')!;
parent.addEventListener('figma', (event) {
final customEvent = event as html.CustomEvent;
final detail = customEvent.detail;
final result = detail;
callback(result);
});
}Future _result(
String type, [
FigmaJson data = const {},
]) async {
final completer = Completer();
final id = DateTime.now().millisecondsSinceEpoch.toString();
_receive((event) {
if (event['id'] == id) {
completer.complete(event);
}
});
_send(type, {'id': id, ...data});
return completer.future;
}
}extension FigmaColorUtils on Color {
/// RGB values between 0 and 1
Map toFigma() {
return {
'r': red / 255,
'g': green / 255,
'b': blue / 255,
};
}String toHex() {
final r = red.toRadixString(16).padLeft(2, '0');
final g = green.toRadixString(16).padLeft(2, '0');
final b = blue.toRadixString(16).padLeft(2, '0');
return '#$r$g$b';
}Color onColor() {
return computeLuminance() > 0.5 ? Colors.black : Colors.white;
}
}typedef FigmaJson = Map;
enum FigmaEditorType {
figma,
figJam,
}enum FigJamShapeType {
SQUARE,
ELLIPSE,
ROUNDED_RECTANGLE,
DIAMOND,
TRIANGLE_UP,
TRIANGLE_DOWN,
PARALLELOGRAM_RIGHT,
PARALLELOGRAM_LEFT,
}```