├── README.md ├── example ├── breaking-bad-season-one-comma.csv ├── breaking-bad-season-one-multiple-levels.json ├── breaking-bad-season-one-semicolon.csv ├── breaking-bad-season-one.json ├── songs-list.json ├── translations-multiple-levels.json └── translations.json ├── figma.d.ts ├── manifest.json ├── src ├── code.js ├── code.ts └── ui.html └── tsconfig.json /README.md: -------------------------------------------------------------------------------- 1 | ![figma-json-to-content-image](http://dev.lucasdietrich.de/lucasdietrich/plugins/figma/json-to-content/figma-json-to-content-image.jpg) 2 | 3 | # Figma Plugin: JSON To Content 4 | Easily map your data and layers together by using the same name. Either select specific entries from your overview or shuffle through them for a randomised dataset 5 | 6 | ### Tired of incoherent random data? 7 | Set up your data separately as a JSON or CSV file and start adding your content. The content of the datasets always remain together to create a seamless data retrieval experience. 8 | 9 | #### Easy mapping via layer name 10 | Map your data and your layers together simply by using the same name. The name of your text layers only needs to match the key of your object and the value will be parsed. 11 | 12 | #### Selection rewriting 13 | Overwrite multiple text layers within your selection and any data that’s attached to the entry will be written into layers that have the same name as the key. 14 | 15 | #### Choose between specific or randomised data 16 | When you have imported your data, the plugin will print a list with every entry that’s in your file. Click on an entry if you need specific data from that set. 17 | 18 | #### Multiple datasets via tab menu 19 | Push your productivity even more with the new tab menu feature. It's now possible to set up multiple datasets without switching between files. 20 | 21 | #### Iterate over random entries 22 | Iterate over multiple entries of your list to your selection. The next entry will be used as soon as the same data would be applied a second time. 23 | 24 | #### (New) Use multi-layered JSON files 25 | You can now utilize the power of layered JSON files if needed. This is a much more common way of writing JSON and allows for more flexibility. Files which have worked before will work as expected. 26 | 27 | #### Future plans: 28 | * Implement the ability to also link images 29 | * Create and save multiple tabs with different data (done with 1.5) 30 | * Create and save multiple tabs with different data (done with 1.4) 31 | * Upload .csv files (done with 1.3) -------------------------------------------------------------------------------- /example/breaking-bad-season-one-comma.csv: -------------------------------------------------------------------------------- 1 | episode,title,releaseDate,millionViewers,directedBy,writtenBy 2 | 1,Pilot,"January 20, 2008","1,41",Vince Gilligan,Vince Gilligan 3 | 2,Cat's in the Bag…,"January 27, 2008","1,49",Adam Bernstein,Vince Gilligan 4 | 3,…And the Bag's in the River,"February 10, 2008","1,08",Adam Bernstein,Vince Gilligan 5 | 4,Cancer Man,"February 17, 2008","1,08",Jim McKay,Vince Gilligan 6 | 5,Gray Matter,"February 24, 2008",N/A,Tricia Brock,Vince Gilligan 7 | 6,Crazy Handful of Nothin',"March 2, 2008","1,07",Bronwen Hughes,Vince Gilligan 8 | 7,A No-Rough-Stuff-Type Deal,"March 9, 2008","1,5",Tim Hunter,Vince Gilligan -------------------------------------------------------------------------------- /example/breaking-bad-season-one-multiple-levels.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "episode": "1", 4 | "title": "Pilot", 5 | "releaseDate": "January 20, 2008", 6 | "millionViewers": 1.41, 7 | "production": { 8 | "director": "Vince Gilligan", 9 | "writer": "Vince Gilligan" 10 | } 11 | }, 12 | { 13 | "episode": "2", 14 | "title": "Cat's in the Bag...", 15 | "releaseDate": "January 27, 2008", 16 | "millionViewers": 1.49, 17 | "production": { 18 | "director": "Adam Bernstein", 19 | "writer": "Vince Gilligan" 20 | } 21 | }, 22 | { 23 | "episode": "3", 24 | "title": "...And the Bag's in the River", 25 | "releaseDate": "February 10, 2008", 26 | "millionViewers": 1.08, 27 | "production": { 28 | "director": "Adam Bernstein", 29 | "writer": "Vince Gilligan" 30 | } 31 | }, 32 | { 33 | "episode": "4", 34 | "title": "Cancer Man", 35 | "releaseDate": "February 17, 2008", 36 | "millionViewers": 1.09, 37 | "production": { 38 | "director": "Jim McKay", 39 | "writer": "Vince Gilligan" 40 | } 41 | }, 42 | { 43 | "episode": "5", 44 | "title": "Gray Matter", 45 | "releaseDate": "February 24, 2008", 46 | "millionViewers": "N/A", 47 | "production": { 48 | "director": "Tricia Brock", 49 | "writer": "Vince Gilligan" 50 | } 51 | }, 52 | { 53 | "episode": "6", 54 | "title": "Crazy Handful of Nothin'", 55 | "releaseDate": "March 2, 2008", 56 | "millionViewers": 1.07, 57 | "production": { 58 | "director": "Bronwen Hughes", 59 | "writer": "Vince Gilligan" 60 | } 61 | }, 62 | { 63 | "episode": "7", 64 | "title": "A No-Rough-Stuff-Type Deal", 65 | "releaseDate": "March 9, 2008", 66 | "millionViewers": 1.50, 67 | "production": { 68 | "director": "Tim Hunter", 69 | "writer": "Vince Gilligan" 70 | } 71 | } 72 | ] -------------------------------------------------------------------------------- /example/breaking-bad-season-one-semicolon.csv: -------------------------------------------------------------------------------- 1 | episode;title;releaseDate;millionViewers;directedBy;writtenBy 2 | 1;Pilot;January 20, 2008;1,41;Vince Gilligan;Vince Gilligan 3 | 2;Cat's in the Bag…;January 27, 2008;1,49;Adam Bernstein;Vince Gilligan 4 | 3;…And the Bag's in the River;February 10, 2008;1,08;Adam Bernstein;Vince Gilligan 5 | 4;Cancer Man;February 17, 2008;1,08;Jim McKay;Vince Gilligan 6 | 5;Gray Matter;February 24, 2008;N/A;Tricia Brock;Vince Gilligan 7 | 6;Crazy Handful of Nothin';March 2, 2008;1,07;Bronwen Hughes;Vince Gilligan 8 | 7;A No-Rough-Stuff-Type Deal;March 9, 2008;1,5;Tim Hunter;Vince Gilligan -------------------------------------------------------------------------------- /example/breaking-bad-season-one.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "episode": "1", 4 | "title": "Pilot", 5 | "releaseDate": "January 20, 2008", 6 | "millionViewers": 1.41, 7 | "directedBy": "Vince Gilligan", 8 | "writtenBy": "Vince Gilligan" 9 | }, 10 | { 11 | "episode": "2", 12 | "title": "Cat's in the Bag...", 13 | "releaseDate": "January 27, 2008", 14 | "millionViewers": 1.49, 15 | "directedBy": "Adam Bernstein", 16 | "writtenBy": "Vince Gilligan" 17 | }, 18 | { 19 | "episode": "3", 20 | "title": "...And the Bag's in the River", 21 | "releaseDate": "February 10, 2008", 22 | "millionViewers": 1.08, 23 | "directedBy": "Adam Bernstein", 24 | "writtenBy": "Vince Gilligan" 25 | }, 26 | { 27 | "episode": "4", 28 | "title": "Cancer Man", 29 | "releaseDate": "February 17, 2008", 30 | "millionViewers": 1.09, 31 | "directedBy": "Jim McKay", 32 | "writtenBy": "Vince Gilligan" 33 | }, 34 | { 35 | "episode": "5", 36 | "title": "Gray Matter", 37 | "releaseDate": "February 24, 2008", 38 | "millionViewers": "N/A", 39 | "directedBy": "Tricia Brock", 40 | "writtenBy": "Vince Gilligan" 41 | }, 42 | { 43 | "episode": "6", 44 | "title": "Crazy Handful of Nothin'", 45 | "releaseDate": "March 2, 2008", 46 | "millionViewers": 1.07, 47 | "directedBy": "Bronwen Hughes", 48 | "writtenBy": "Vince Gilligan" 49 | }, 50 | { 51 | "episode": "7", 52 | "title": "A No-Rough-Stuff-Type Deal", 53 | "releaseDate": "March 9, 2008", 54 | "millionViewers": 1.50, 55 | "directedBy": "Tim Hunter", 56 | "writtenBy": "Vince Gilligan" 57 | } 58 | ] -------------------------------------------------------------------------------- /example/songs-list.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "sSong": "SOHO", 4 | "sArtists": "Jaden", 5 | "sAlbum": "The Sunset Tapes: A Cool Tape Story", 6 | "sDuration": "03:28", 7 | "sRating": 5, 8 | "sExplicit": "explicit", 9 | "sRelease": 2018 10 | }, 11 | { 12 | "sSong": "Losses", 13 | "sArtists": "Drake", 14 | "sAlbum": "Dark Lane Demo Tapes", 15 | "sDuration": "04:31", 16 | "sRating": 4, 17 | "sExplicit": "explicit", 18 | "sRelease": 2020 19 | }, 20 | { 21 | "sSong": "Fields", 22 | "sArtists": "Giveon", 23 | "sAlbum": "Fields", 24 | "sDuration": "03:40", 25 | "sRating": 4, 26 | "sExplicit": null, 27 | "sRelease": 2018 28 | }, 29 | { 30 | "sSong": "Venice Bitch", 31 | "sArtists": "Lana Del Ray", 32 | "sAlbum": "Norman Fucking Rockwell!", 33 | "sDuration": "09:37", 34 | "sRating": 4, 35 | "sExplicit": "explicit", 36 | "sRelease": 2019 37 | }, 38 | { 39 | "sSong": "Levitating", 40 | "sArtists": "Dua Lipa", 41 | "sAlbum": "Future Nostalgia", 42 | "sDuration": "03:24", 43 | "sRating": 5, 44 | "sExplicit": null, 45 | "sRelease": 2020 46 | }, 47 | { 48 | "sSong": "COPYCAT", 49 | "sArtists": "Billie Eilish", 50 | "sAlbum": "dont smile at me", 51 | "sDuration": "03:15", 52 | "sRating": 3, 53 | "sExplicit": null, 54 | "sRelease": 2017 55 | }, 56 | { 57 | "sSong": "Resonance", 58 | "sArtists": "Home", 59 | "sAlbum": "Odyssey", 60 | "sDuration": "03:33", 61 | "sRating": 5, 62 | "sExplicit": null, 63 | "sRelease": 2014 64 | }, 65 | { 66 | "sSong": "Winter", 67 | "sArtists": "Daughter", 68 | "sAlbum": "If You Leave", 69 | "sDuration": "04:43", 70 | "sRating": 4, 71 | "sExplicit": null, 72 | "sRelease": 2013 73 | }, 74 | { 75 | "sSong": "Fake Happy", 76 | "sArtists": "Paramore", 77 | "sAlbum": "After Laughter", 78 | "sDuration": "03:56", 79 | "sRating": 3, 80 | "sExplicit": null, 81 | "sRelease": 2017 82 | }, 83 | { 84 | "sSong": "Dust", 85 | "sArtists": "M|O|O|N", 86 | "sAlbum": "Particles", 87 | "sDuration": "05:02", 88 | "sRating": 5, 89 | "sExplicit": null, 90 | "sRelease": 2012 91 | }, 92 | { 93 | "sSong": "Amazing", 94 | "sArtists": "Kanye West, Jeezy", 95 | "sAlbum": "808s & Heartbreak", 96 | "sDuration": "03:58", 97 | "sRating": 3, 98 | "sExplicit": null, 99 | "sRelease": 2008 100 | }, 101 | { 102 | "sSong": "Breaking the Habit", 103 | "sArtists": "Linkin Park", 104 | "sAlbum": "Meteora", 105 | "sDuration": "03:17", 106 | "sRating": 4, 107 | "sExplicit": null, 108 | "sRelease": 2003 109 | }, 110 | { 111 | "sSong": "Thriller", 112 | "sArtists": "Michael Jackson", 113 | "sAlbum": "Thriller", 114 | "sDuration": "05:58", 115 | "sRating": 4, 116 | "sExplicit": null, 117 | "sRelease": 1982 118 | }, 119 | { 120 | "sSong": "Stuck In Traffic On Mars", 121 | "sArtists": "Purrple Cat, Chill Select", 122 | "sAlbum": "Fam Episode 01", 123 | "sDuration": "02:27", 124 | "sRating": 4, 125 | "sExplicit": null, 126 | "sRelease": 2019 127 | }, 128 | { 129 | "sSong": "Soundtrack 2 My Life", 130 | "sArtists": "Kid Cudi", 131 | "sAlbum": "Man On The Moon: The End Of Day", 132 | "sDuration": "03:56", 133 | "sRating": 4, 134 | "sExplicit": "explicit", 135 | "sRelease": 2009 136 | }, 137 | { 138 | "sSong": "Inner City Blues (Make Me Wanna Holler)", 139 | "sArtists": "Marvin Gaye", 140 | "sAlbum": "What's Going On", 141 | "sDuration": "05:32", 142 | "sRating": 5, 143 | "sExplicit": null, 144 | "sRelease": 1971 145 | }, 146 | { 147 | "sSong": "Nights", 148 | "sArtists": "Frank Ocean", 149 | "sAlbum": "Blonde", 150 | "sDuration": "05:07", 151 | "sRating": 5, 152 | "sExplicit": "explicit", 153 | "sRelease": 2016 154 | }, 155 | { 156 | "sSong": "YOSEMITE", 157 | "sArtists": "Travis Scott", 158 | "sAlbum": "ASTROWORLD", 159 | "sDuration": "02:30", 160 | "sRating": 4, 161 | "sExplicit": "explicit", 162 | "sRelease": 2018 163 | } 164 | ] -------------------------------------------------------------------------------- /example/translations-multiple-levels.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "lang": "English", 4 | "media": { 5 | "play": "Play", 6 | "stop": "Stop", 7 | "next": "Next song", 8 | "previous": "Previous song", 9 | "pause": "Pause", 10 | "repeat": "Repeat", 11 | "shuffle": "Shuffle" 12 | }, 13 | "sound": { 14 | "mute": "Mute", 15 | "louder": "Louder", 16 | "quieter": "Quieter" 17 | }, 18 | "search": { 19 | "placeholder": "Search for artists, playlists, ...", 20 | "results": "Results for" 21 | }, 22 | "edit": "Edit", 23 | "song": "Song", 24 | "songs": "Songs", 25 | "song-duration": "Duration", 26 | "artist": "Artist", 27 | "artists": "Artists", 28 | "album": "Album", 29 | "albums": "Albums", 30 | "album-by": "Album by", 31 | "album-cover": "Cover", 32 | "album-information": "Album information", 33 | "playlist": "Playlist", 34 | "playlists": "Playlists", 35 | "playlist-information": "Playlist information", 36 | "rooms": "Rooms", 37 | "room": { 38 | "title": "Room", 39 | "livingRoom": "Living room", 40 | "bedroom": "Bedroom", 41 | "bedroomGuests": "Guest bedroom", 42 | "kitchen": "Kitchen", 43 | "bath": "Bathroom", 44 | "outdoor": "Garden", 45 | "combined": "Bedroom + Guest bedroom" 46 | }, 47 | "settings": { 48 | "title": "Settings", 49 | "privacy": "Privacy settings", 50 | "account": "Your Account", 51 | "system": "System", 52 | "app": "App settings", 53 | "legal": "Legal information" 54 | }, 55 | "menu": { 56 | "mySonos": "My Sonos", 57 | "recentlyPlayed": "Recently played", 58 | "artists": "Artists", 59 | "albums": "Albums", 60 | "playlists": "Playlists", 61 | "likedSongs": "Liked Songs", 62 | "recentlySearched": "Recently searched", 63 | "discover": "Discover", 64 | "library": "Your Library", 65 | "spotify": "Spotify", 66 | "appleMusic": "Apple Music", 67 | "radio": "Radio", 68 | "podcasts": "Podcasts", 69 | "recommendations": "Recommendations" 70 | }, 71 | "button": { 72 | "sonos":{ 73 | "add": "Add Sonos", 74 | "remove": "Remove Sonos" 75 | }, 76 | "stream": "Start playing", 77 | "streamPlaylist": "Play on Sonos ...", 78 | "connectServices": "Connect more services" 79 | }, 80 | "sonos": { 81 | "controlCenter": "Sonos Control Center", 82 | "controlCenterText": "The Sonos Control Center lets you connect and disconnect your devices as you like. Simply drag the device to the desired position.", 83 | "model": { 84 | "oneSl": "Sonos One SL", 85 | "one": "Sonos One", 86 | "five": "Sonos Five" 87 | } 88 | } 89 | }, 90 | { 91 | "lang": "Deutsch", 92 | "media": { 93 | "play": "abspielen", 94 | "stop": "Stop", 95 | "next": "N\u00e4chstes Lied", 96 | "previous": "Vorheriges Lied", 97 | "pause": "Pause", 98 | "repeat": "Wiederholen", 99 | "shuffle": "Mischen" 100 | }, 101 | "sound": { 102 | "mute": "Stumm", 103 | "louder": "Lauter", 104 | "quieter": "Leiser" 105 | }, 106 | "search": { 107 | "placeholder": "Suche nach K\u00fcnstler, Playlisten...", 108 | "results": "Ergebnisse f\u00fcr" 109 | }, 110 | "edit": "Bearbeiten", 111 | "song": "Song", 112 | "songs": "Songs", 113 | "song-duration": "Dauer", 114 | "artist": "K\u00fcnstler", 115 | "artists": "K\u00fcnstler", 116 | "album": "Album", 117 | "albums": "Alben", 118 | "album-by": "Album von", 119 | "album-cover": "Cover", 120 | "album-information": "Informationen \u00fcber das Album", 121 | "playlist": "Playlist", 122 | "playlists": "Playlisten", 123 | "playlist-information": "Playlist-Informationen", 124 | "rooms": "R\u00e4ume", 125 | "room": { 126 | "title": "Zimmer", 127 | "livingRoom": "Wohnzimmer", 128 | "bedroom": "Schlafzimmer", 129 | "bedroomGuests": "G\u00e4stezimmer", 130 | "kitchen": "K\u00fcche", 131 | "bath": "Bad", 132 | "outdoor": "Garten", 133 | "combined": "Schlafzimmer + G\u00e4stezimmer" 134 | }, 135 | "settings": { 136 | "title": "Einstellungen", 137 | "privacy": "Datenschutzeinstellungen", 138 | "account": "Ihr Konto", 139 | "system": "System", 140 | "app": "App Einstellungen", 141 | "legal": "Rechtsinformation" 142 | }, 143 | "menu": { 144 | "mySonos": "Mein Sonos", 145 | "recentlyPlayed": "Letzte Wiedergaben", 146 | "artists": "K\u00fcnstler", 147 | "albums": "Alben", 148 | "playlists": "Wiedergabelisten", 149 | "likedSongs": "Gespeicherte Songs", 150 | "recentlySearched": "K\u00fcrzlich gesucht", 151 | "discover": "Entdecken", 152 | "library": "Ihre Bibliothek", 153 | "spotify": "Spotify", 154 | "appleMusic": "Apple Music", 155 | "radio": "Radio", 156 | "podcasts": "Podcasts", 157 | "recommendations": "Empfehlungen" 158 | }, 159 | "button": { 160 | "sonos": { 161 | "add": "Sonos hinzuf\u00fcgen", 162 | "remove": "Sonos entkoppeln" 163 | }, 164 | "stream": "Musik ausw\u00e4hlen", 165 | "streamPlaylist": "Auf Sonos abspielen ...", 166 | "connectServices": "Verbinde weitere Dienste" 167 | }, 168 | "sonos": { 169 | "controlCenter": "Sonos Control Center", 170 | "controlCenterText": "Im Sonos Control Center kannst du deine Ger\u00e4te beliebig miteinander verkn\u00fcpfen oder wieder voneinander trennen. Ziehe das Ger\u00e4t daf\u00fcr einfach an die gew\u00fcnschte Position.", 171 | "model": { 172 | "oneSl": "Sonos One SL", 173 | "one": "Sonos One", 174 | "five": "Sonos Five" 175 | } 176 | } 177 | }, 178 | { 179 | "lang": "\u0420\u0443\u0441\u0441\u043a\u0438\u0439", 180 | "media": { 181 | "play": "\u0418\u0433\u0440\u0430\u0442\u044c", 182 | "stop": "\u0421\u0442\u043e\u043f", 183 | "next": "\u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0430\u044f \u043f\u0435\u0441\u043d\u044f", 184 | "previous": "\u041f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0430\u044f \u043f\u0435\u0441\u043d\u044f", 185 | "pause": "\u041f\u0430\u0443\u0437\u0430", 186 | "repeat": "\u041f\u043e\u0432\u0442\u043e\u0440\u0435\u043d\u0438\u0435", 187 | "shuffle": "\u0448\u0430\u0440\u043a\u0430\u043d\u044c\u0435" 188 | }, 189 | "sound": { 190 | "mute": "\u0431\u0435\u0437\u0433\u043b\u0430\u0441\u043d\u044b\u0439", 191 | "louder": "\u0413\u0440\u043e\u043c\u0447\u0435", 192 | "quieter": "\u0422\u0438\u0448\u0435" 193 | }, 194 | "search": { 195 | "placeholder": "\u041f\u043e\u0438\u0441\u043a \u0438\u0441\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u0435\u0439, \u043f\u043b\u0435\u0439\u043b\u0438\u0441\u0442\u044b, ...", 196 | "results": "\u0420\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u044b \u043f\u043e\u0438\u0441\u043a\u0430 \u043f\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0443" 197 | }, 198 | "edit": "\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c", 199 | "song": "\u043f\u0435\u0441\u043d\u044f", 200 | "songs": "\u043f\u0435\u0441\u043d\u0438", 201 | "song-duration": "\u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c", 202 | "artist": "\u0445\u0443\u0434\u043e\u0436\u043d\u0438\u043a", 203 | "artists": "\u0425\u0443\u0434\u043e\u0436\u043d\u0438\u043a\u0438", 204 | "album": "\u0410\u043b\u044c\u0431\u043e\u043c", 205 | "albums": "\u0410\u043b\u044c\u0431\u043e\u043c\u044b", 206 | "album-by": "\u0410\u043b\u044c\u0431\u043e\u043c \u043f\u043e", 207 | "album-cover": "\u041e\u0431\u043b\u043e\u0436\u043a\u0430", 208 | "album-information": "\u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u043e\u0431 \u0430\u043b\u044c\u0431\u043e\u043c\u0435", 209 | "playlist": "\u041f\u043b\u0435\u0439\u043b\u0438\u0441\u0442", 210 | "playlists": "\u041f\u043b\u0435\u0439\u043b\u0438\u0441\u0442\u044b", 211 | "playlist-information": "\u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u043e \u0441\u043f\u0438\u0441\u043a\u0435 \u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0434\u0435\u043d\u0438\u044f", 212 | "rooms": "\u041d\u043e\u043c\u0435\u0440\u0430", 213 | "room": { 214 | "title": "\u041a\u043e\u043c\u043d\u0430\u0442\u0430", 215 | "livingRoom": "\u0413\u043e\u0441\u0442\u0438\u043d\u0430\u044f", 216 | "bedroom": "\u0441\u043f\u0430\u043b\u044c\u043d\u0430\u044f \u043a\u043e\u043c\u043d\u0430\u0442\u0430", 217 | "bedroomGuests": "\u0413\u043e\u0441\u0442\u0435\u0432\u0430\u044f \u0441\u043f\u0430\u043b\u044c\u043d\u044f", 218 | "kitchen": "\u041a\u0443\u0445\u043d\u044f", 219 | "bath": "\u0432\u0430\u043d\u043d\u0430\u044f \u043a\u043e\u043c\u043d\u0430\u0442\u0430", 220 | "outdoor": "\u0421\u0430\u0434", 221 | "combined": "\u0421\u043f\u0430\u043b\u044c\u043d\u044f + \u0413\u043e\u0441\u0442\u0435\u0432\u0430\u044f \u0441\u043f\u0430\u043b\u044c\u043d\u044f" 222 | }, 223 | "settings": { 224 | "title": "\u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438", 225 | "privacy": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043a\u043e\u043d\u0444\u0438\u0434\u0435\u043d\u0446\u0438\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u0438", 226 | "account": "\u0412\u0430\u0448 \u0441\u0447\u0435\u0442", 227 | "system": "\u0441\u0438\u0441\u0442\u0435\u043c\u0430", 228 | "app": "\u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f", 229 | "legal": "\u041b\u0435\u0433\u0430\u043b\u044c\u043d\u0430\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f" 230 | }, 231 | "menu": { 232 | "mySonos": "\u041c\u043e\u0439 Sonos", 233 | "recentlyPlayed": "\u041d\u0435\u0434\u0430\u0432\u043d\u043e \u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0434\u0435\u043d\u043d\u044b\u0435", 234 | "artists": "\u0425\u0443\u0434\u043e\u0436\u043d\u0438\u043a\u0438", 235 | "albums": "\u0410\u043b\u044c\u0431\u043e\u043c\u044b", 236 | "playlists": "\u041f\u043b\u0435\u0439\u043b\u0438\u0441\u0442\u044b", 237 | "likedSongs": "\u041f\u043e\u043d\u0440\u0430\u0432\u0438\u043b\u0441\u044f \u043f\u0435\u0441\u043d\u044f", 238 | "recentlySearched": "\u041d\u0435\u0434\u0430\u0432\u043d\u043e \u0438\u0441\u043a\u0430\u043b\u0438", 239 | "discover": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442\u044c", 240 | "library": "\u0412\u0430\u0448\u0430 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430", 241 | "spotify": "Spotify", 242 | "appleMusic": "Apple Music", 243 | "radio": "\u0420\u0430\u0434\u0438\u043e", 244 | "podcasts": "\u041f\u043e\u0434\u043a\u0430\u0441\u0442\u044b", 245 | "recommendations": "\u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0430\u0446\u0438\u0438" 246 | }, 247 | "button": { 248 | "sonos": { 249 | "add": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c Sonos", 250 | "remove": "\u0423\u0434\u0430\u043b\u0438\u0442\u044c Sonos" 251 | }, 252 | "stream": "\u041d\u0430\u0447\u0430\u043b\u043e \u0438\u0433\u0440\u044b", 253 | "streamPlaylist": "\u0418\u0433\u0440\u0430\u0442\u044c \u043d\u0430 Sonos ...", 254 | "connectServices": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u0443\u0441\u043b\u0443\u0433\u0438" 255 | }, 256 | "sonos": { 257 | "controlCenter": "\u0421\u043e\u043d\u043e\u0441 \u041a\u043e\u043d\u0442\u0440\u043e\u043b\u044c \u0426\u0435\u043d\u0442\u0440\u0435", 258 | "controlCenterText": "\u0426\u0435\u043d\u0442\u0440 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f Sonos \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0442\u044c \u0438 \u043e\u0442\u043a\u043b\u044e\u0447\u0430\u0442\u044c \u0432\u0430\u0448\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u043a\u0430\u043a \u0432\u0430\u043c \u043d\u0440\u0430\u0432\u0438\u0442\u0441\u044f. \u041f\u0440\u043e\u0441\u0442\u043e \u043f\u0435\u0440\u0435\u0442\u044f\u043d\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432 \u043d\u0443\u0436\u043d\u043e\u0435 \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435.", 259 | "model": { 260 | "oneSl": "Sonos One SL", 261 | "one": "Sonos One", 262 | "five": "Sonos Five" 263 | } 264 | } 265 | } 266 | ] -------------------------------------------------------------------------------- /example/translations.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "lang": "English", 4 | "media-play": "Play", 5 | "media-stop": "Stop", 6 | "media-next": "Next song", 7 | "media-previous": "Previous song", 8 | "media-pause": "Pause", 9 | "media-repeat": "Repeat", 10 | "media-shuffle": "Shuffle", 11 | "sound-mute": "Mute", 12 | "sound-louder": "Louder", 13 | "sound-quieter": "Quieter", 14 | "edit": "Edit", 15 | "search-placeholder": "Search for artists, playlists, ...", 16 | "search-results": "Results for", 17 | "song": "Song", 18 | "songs": "Songs", 19 | "song-duration": "Duration", 20 | "artist": "Artist", 21 | "artists": "Artists", 22 | "album": "Album", 23 | "albums": "Albums", 24 | "album-by": "Album by", 25 | "album-cover": "Cover", 26 | "album-information": "Album information", 27 | "playlist": "Playlist", 28 | "playlists": "Playlists", 29 | "playlist-information": "Playlist information", 30 | "room": "Room", 31 | "rooms": "Rooms", 32 | "room-living-room": "Living room", 33 | "room-bedroom": "Bedroom", 34 | "room-bedroom-guests": "Guest bedroom", 35 | "room-kitchen": "Kitchen", 36 | "room-bath": "Bathroom", 37 | "room-outdoor": "Garden", 38 | "room-combined": "Bedroom + Guest bedroom", 39 | "settings": "Settings", 40 | "settings-privacy": "Privacy settings", 41 | "settings-account": "Your Account", 42 | "settings-system": "System", 43 | "settings-app": "App settings", 44 | "settings-legal": "Legal information", 45 | "menu-my-sonos": "My Sonos", 46 | "menu-recently-played": "Recently played", 47 | "menu-artists": "Artists", 48 | "menu-albums": "Albums", 49 | "menu-playlists": "Playlists", 50 | "menu-liked-songs": "Liked Songs", 51 | "menu-recently-searched": "Recently searched", 52 | "menu-discover": "Discover", 53 | "menu-library": "Your Library", 54 | "menu-spotify": "Spotify", 55 | "menu-apple-music": "Apple Music", 56 | "menu-radio": "Radio", 57 | "menu-podcasts": "Podcasts", 58 | "menu-recommendations": "Recommendations", 59 | "button-sonos-add": "Add Sonos", 60 | "button-sonos-remove": "Remove Sonos", 61 | "button-stream": "Start playing", 62 | "button-stream-playlist": "Play on Sonos ...", 63 | "button-connect-services": "Connect more services", 64 | "sonos-control-center": "Sonos Control Center", 65 | "sonos-control-center-text": "The Sonos Control Center lets you connect and disconnect your devices as you like. Simply drag the device to the desired position.", 66 | "sonos-model-one-sl": "Sonos One SL", 67 | "sonos-model-one": "Sonos One", 68 | "sonos-model-five": "Sonos Five" 69 | }, 70 | { 71 | "lang": "Deutsch", 72 | "media-play": "abspielen", 73 | "media-stop": "Stop", 74 | "media-next": "N\u00e4chstes Lied", 75 | "media-previous": "Vorheriges Lied", 76 | "media-pause": "Pause", 77 | "media-repeat": "Wiederholen", 78 | "media-shuffle": "Mischen", 79 | "sound-mute": "Stumm", 80 | "sound-louder": "Lauter", 81 | "sound-quieter": "Leiser", 82 | "edit": "Bearbeiten", 83 | "search-placeholder": "Suche nach K\u00fcnstler, Playlisten...", 84 | "search-results": "Ergebnisse f\u00fcr", 85 | "song": "Song", 86 | "songs": "Songs", 87 | "song-duration": "Dauer", 88 | "artist": "K\u00fcnstler", 89 | "artists": "K\u00fcnstler", 90 | "album": "Album", 91 | "albums": "Alben", 92 | "album-by": "Album von", 93 | "album-cover": "Cover", 94 | "album-information": "Informationen \u00fcber das Album", 95 | "playlist": "Playlist", 96 | "playlists": "Playlisten", 97 | "playlist-information": "Playlist-Informationen", 98 | "room": "Zimmer", 99 | "rooms": "R\u00e4ume", 100 | "room-living-room": "Wohnzimmer", 101 | "room-bedroom": "Schlafzimmer", 102 | "room-bedroom-guests": "G\u00e4stezimmer", 103 | "room-kitchen": "K\u00fcche", 104 | "room-bath": "Bad", 105 | "room-outdoor": "Garten", 106 | "room-combined": "Schlafzimmer + G\u00e4stezimmer", 107 | "settings": "Einstellungen", 108 | "settings-privacy": "Datenschutzeinstellungen", 109 | "settings-account": "Ihr Konto", 110 | "settings-system": "System", 111 | "settings-app": "App Einstellungen", 112 | "settings-legal": "Rechtsinformation", 113 | "menu-my-sonos": "Mein Sonos", 114 | "menu-recently-played": "Letzte Wiedergaben", 115 | "menu-artists": "K\u00fcnstler", 116 | "menu-albums": "Alben", 117 | "menu-playlists": "Wiedergabelisten", 118 | "menu-liked-songs": "Gespeicherte Songs", 119 | "menu-recently-searched": "K\u00fcrzlich gesucht", 120 | "menu-discover": "Entdecken", 121 | "menu-library": "Ihre Bibliothek", 122 | "menu-spotify": "Spotify", 123 | "menu-apple-music": "Apple Music", 124 | "menu-radio": "Radio", 125 | "menu-podcasts": "Podcasts", 126 | "menu-recommendations": "Empfehlungen", 127 | "button-sonos-add": "Sonos hinzuf\u00fcgen", 128 | "button-sonos-remove": "Sonos entkoppeln", 129 | "button-stream": "Musik ausw\u00e4hlen", 130 | "button-stream-playlist": "Auf Sonos abspielen ...", 131 | "button-connect-services": "Verbinde weitere Dienste", 132 | "sonos-control-center": "Sonos Control Center", 133 | "sonos-control-center-text": "Im Sonos Control Center kannst du deine Ger\u00e4te beliebig miteinander verkn\u00fcpfen oder wieder voneinander trennen. Ziehe das Ger\u00e4t daf\u00fcr einfach an die gew\u00fcnschte Position.", 134 | "sonos-model-one-sl": "Sonos One SL", 135 | "sonos-model-one": "Sonos One", 136 | "sonos-model-five": "Sonos Five" 137 | }, 138 | { 139 | "lang": "\u0420\u0443\u0441\u0441\u043a\u0438\u0439", 140 | "media-play": "\u0418\u0433\u0440\u0430\u0442\u044c", 141 | "media-stop": "\u0421\u0442\u043e\u043f", 142 | "media-next": "\u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0430\u044f \u043f\u0435\u0441\u043d\u044f", 143 | "media-previous": "\u041f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0430\u044f \u043f\u0435\u0441\u043d\u044f", 144 | "media-pause": "\u041f\u0430\u0443\u0437\u0430", 145 | "media-repeat": "\u041f\u043e\u0432\u0442\u043e\u0440\u0435\u043d\u0438\u0435", 146 | "media-shuffle": "\u0448\u0430\u0440\u043a\u0430\u043d\u044c\u0435", 147 | "sound-mute": "\u0431\u0435\u0437\u0433\u043b\u0430\u0441\u043d\u044b\u0439", 148 | "sound-louder": "\u0413\u0440\u043e\u043c\u0447\u0435", 149 | "sound-quieter": "\u0422\u0438\u0448\u0435", 150 | "edit": "\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c", 151 | "search-placeholder": "\u041f\u043e\u0438\u0441\u043a \u0438\u0441\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u0435\u0439, \u043f\u043b\u0435\u0439\u043b\u0438\u0441\u0442\u044b, ...", 152 | "search-results": "\u0420\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u044b \u043f\u043e\u0438\u0441\u043a\u0430 \u043f\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0443", 153 | "song": "\u043f\u0435\u0441\u043d\u044f", 154 | "songs": "\u043f\u0435\u0441\u043d\u0438", 155 | "song-duration": "\u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c", 156 | "artist": "\u0445\u0443\u0434\u043e\u0436\u043d\u0438\u043a", 157 | "artists": "\u0425\u0443\u0434\u043e\u0436\u043d\u0438\u043a\u0438", 158 | "album": "\u0410\u043b\u044c\u0431\u043e\u043c", 159 | "albums": "\u0410\u043b\u044c\u0431\u043e\u043c\u044b", 160 | "album-by": "\u0410\u043b\u044c\u0431\u043e\u043c \u043f\u043e", 161 | "album-cover": "\u041e\u0431\u043b\u043e\u0436\u043a\u0430", 162 | "album-information": "\u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u043e\u0431 \u0430\u043b\u044c\u0431\u043e\u043c\u0435", 163 | "playlist": "\u041f\u043b\u0435\u0439\u043b\u0438\u0441\u0442", 164 | "playlists": "\u041f\u043b\u0435\u0439\u043b\u0438\u0441\u0442\u044b", 165 | "playlist-information": "\u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u043e \u0441\u043f\u0438\u0441\u043a\u0435 \u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0434\u0435\u043d\u0438\u044f", 166 | "room": "\u041a\u043e\u043c\u043d\u0430\u0442\u0430", 167 | "rooms": "\u041d\u043e\u043c\u0435\u0440\u0430", 168 | "room-living-room": "\u0413\u043e\u0441\u0442\u0438\u043d\u0430\u044f", 169 | "room-bedroom": "\u0441\u043f\u0430\u043b\u044c\u043d\u0430\u044f \u043a\u043e\u043c\u043d\u0430\u0442\u0430", 170 | "room-bedroom-guests": "\u0413\u043e\u0441\u0442\u0435\u0432\u0430\u044f \u0441\u043f\u0430\u043b\u044c\u043d\u044f", 171 | "room-kitchen": "\u041a\u0443\u0445\u043d\u044f", 172 | "room-bath": "\u0432\u0430\u043d\u043d\u0430\u044f \u043a\u043e\u043c\u043d\u0430\u0442\u0430", 173 | "room-outdoor": "\u0421\u0430\u0434", 174 | "room-combined": "\u0421\u043f\u0430\u043b\u044c\u043d\u044f + \u0413\u043e\u0441\u0442\u0435\u0432\u0430\u044f \u0441\u043f\u0430\u043b\u044c\u043d\u044f", 175 | "settings": "\u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438", 176 | "settings-privacy": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043a\u043e\u043d\u0444\u0438\u0434\u0435\u043d\u0446\u0438\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u0438", 177 | "settings-account": "\u0412\u0430\u0448 \u0441\u0447\u0435\u0442", 178 | "settings-system": "\u0441\u0438\u0441\u0442\u0435\u043c\u0430", 179 | "settings-app": "\u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f", 180 | "settings-legal": "\u041b\u0435\u0433\u0430\u043b\u044c\u043d\u0430\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f", 181 | "menu-my-sonos": "\u041c\u043e\u0439 Sonos", 182 | "menu-recently-played": "\u041d\u0435\u0434\u0430\u0432\u043d\u043e \u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0434\u0435\u043d\u043d\u044b\u0435", 183 | "menu-artists": "\u0425\u0443\u0434\u043e\u0436\u043d\u0438\u043a\u0438", 184 | "menu-albums": "\u0410\u043b\u044c\u0431\u043e\u043c\u044b", 185 | "menu-playlists": "\u041f\u043b\u0435\u0439\u043b\u0438\u0441\u0442\u044b", 186 | "menu-liked-songs": "\u041f\u043e\u043d\u0440\u0430\u0432\u0438\u043b\u0441\u044f \u043f\u0435\u0441\u043d\u044f", 187 | "menu-recently-searched": "\u041d\u0435\u0434\u0430\u0432\u043d\u043e \u0438\u0441\u043a\u0430\u043b\u0438", 188 | "menu-discover": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442\u044c", 189 | "menu-library": "\u0412\u0430\u0448\u0430 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430", 190 | "menu-spotify": "Spotify", 191 | "menu-apple-music": "Apple Music", 192 | "menu-radio": "\u0420\u0430\u0434\u0438\u043e", 193 | "menu-podcasts": "\u041f\u043e\u0434\u043a\u0430\u0441\u0442\u044b", 194 | "menu-recommendations": "\u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0430\u0446\u0438\u0438", 195 | "button-sonos-add": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c Sonos", 196 | "button-sonos-remove": "\u0423\u0434\u0430\u043b\u0438\u0442\u044c Sonos", 197 | "button-stream": "\u041d\u0430\u0447\u0430\u043b\u043e \u0438\u0433\u0440\u044b", 198 | "button-stream-playlist": "\u0418\u0433\u0440\u0430\u0442\u044c \u043d\u0430 Sonos ...", 199 | "button-connect-services": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u0443\u0441\u043b\u0443\u0433\u0438", 200 | "sonos-control-center": "\u0421\u043e\u043d\u043e\u0441 \u041a\u043e\u043d\u0442\u0440\u043e\u043b\u044c \u0426\u0435\u043d\u0442\u0440\u0435", 201 | "sonos-control-center-text": "\u0426\u0435\u043d\u0442\u0440 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f Sonos \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0442\u044c \u0438 \u043e\u0442\u043a\u043b\u044e\u0447\u0430\u0442\u044c \u0432\u0430\u0448\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u043a\u0430\u043a \u0432\u0430\u043c \u043d\u0440\u0430\u0432\u0438\u0442\u0441\u044f. \u041f\u0440\u043e\u0441\u0442\u043e \u043f\u0435\u0440\u0435\u0442\u044f\u043d\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432 \u043d\u0443\u0436\u043d\u043e\u0435 \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435.", 202 | "sonos-model-one-sl": "Sonos One SL", 203 | "sonos-model-one": "Sonos One", 204 | "sonos-model-five": "Sonos Five" 205 | } 206 | ] -------------------------------------------------------------------------------- /figma.d.ts: -------------------------------------------------------------------------------- 1 | // Figma Plugin API version 1, update 10 2 | 3 | declare global { 4 | // Global variable with Figma's plugin API. 5 | const figma: PluginAPI 6 | const __html__: string 7 | 8 | interface PluginAPI { 9 | readonly apiVersion: "1.0.0" 10 | readonly command: string 11 | readonly viewport: ViewportAPI 12 | closePlugin(message?: string): void 13 | 14 | notify(message: string, options?: NotificationOptions): NotificationHandler 15 | 16 | showUI(html: string, options?: ShowUIOptions): void 17 | readonly ui: UIAPI 18 | 19 | readonly clientStorage: ClientStorageAPI 20 | 21 | getNodeById(id: string): BaseNode | null 22 | getStyleById(id: string): BaseStyle | null 23 | 24 | readonly root: DocumentNode 25 | currentPage: PageNode 26 | 27 | on(type: "selectionchange" | "currentpagechange" | "close", callback: () => void): void 28 | once(type: "selectionchange" | "currentpagechange" | "close", callback: () => void): void 29 | off(type: "selectionchange" | "currentpagechange" | "close", callback: () => void): void 30 | 31 | readonly mixed: unique symbol 32 | 33 | createRectangle(): RectangleNode 34 | createLine(): LineNode 35 | createEllipse(): EllipseNode 36 | createPolygon(): PolygonNode 37 | createStar(): StarNode 38 | createVector(): VectorNode 39 | createText(): TextNode 40 | createFrame(): FrameNode 41 | createComponent(): ComponentNode 42 | createPage(): PageNode 43 | createSlice(): SliceNode 44 | /** 45 | * [DEPRECATED]: This API often fails to create a valid boolean operation. Use figma.union, figma.subtract, figma.intersect and figma.exclude instead. 46 | */ 47 | createBooleanOperation(): BooleanOperationNode 48 | 49 | createPaintStyle(): PaintStyle 50 | createTextStyle(): TextStyle 51 | createEffectStyle(): EffectStyle 52 | createGridStyle(): GridStyle 53 | 54 | // The styles are returned in the same order as displayed in the UI. Only 55 | // local styles are returned. Never styles from team library. 56 | getLocalPaintStyles(): PaintStyle[] 57 | getLocalTextStyles(): TextStyle[] 58 | getLocalEffectStyles(): EffectStyle[] 59 | getLocalGridStyles(): GridStyle[] 60 | 61 | importComponentByKeyAsync(key: string): Promise 62 | importStyleByKeyAsync(key: string): Promise 63 | 64 | listAvailableFontsAsync(): Promise 65 | loadFontAsync(fontName: FontName): Promise 66 | readonly hasMissingFont: boolean 67 | 68 | createNodeFromSvg(svg: string): FrameNode 69 | 70 | createImage(data: Uint8Array): Image 71 | getImageByHash(hash: string): Image 72 | 73 | group(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): GroupNode 74 | flatten(nodes: ReadonlyArray, parent?: BaseNode & ChildrenMixin, index?: number): VectorNode 75 | 76 | union(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): BooleanOperationNode 77 | subtract(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): BooleanOperationNode 78 | intersect(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): BooleanOperationNode 79 | exclude(nodes: ReadonlyArray, parent: BaseNode & ChildrenMixin, index?: number): BooleanOperationNode 80 | } 81 | 82 | interface ClientStorageAPI { 83 | getAsync(key: string): Promise 84 | setAsync(key: string, value: any): Promise 85 | } 86 | 87 | interface NotificationOptions { 88 | timeout?: number, 89 | } 90 | 91 | interface NotificationHandler { 92 | cancel: () => void, 93 | } 94 | 95 | interface ShowUIOptions { 96 | visible?: boolean, 97 | width?: number, 98 | height?: number, 99 | } 100 | 101 | interface UIPostMessageOptions { 102 | origin?: string, 103 | } 104 | 105 | interface OnMessageProperties { 106 | origin: string, 107 | } 108 | 109 | type MessageEventHandler = (pluginMessage: any, props: OnMessageProperties) => void 110 | 111 | interface UIAPI { 112 | show(): void 113 | hide(): void 114 | resize(width: number, height: number): void 115 | close(): void 116 | 117 | postMessage(pluginMessage: any, options?: UIPostMessageOptions): void 118 | onmessage: MessageEventHandler | undefined 119 | on(type: "message", callback: MessageEventHandler): void 120 | once(type: "message", callback: MessageEventHandler): void 121 | off(type: "message", callback: MessageEventHandler): void 122 | } 123 | 124 | interface ViewportAPI { 125 | center: { x: number, y: number } 126 | zoom: number 127 | scrollAndZoomIntoView(nodes: ReadonlyArray): void 128 | } 129 | 130 | //////////////////////////////////////////////////////////////////////////////// 131 | // Datatypes 132 | 133 | type Transform = [ 134 | [number, number, number], 135 | [number, number, number] 136 | ] 137 | 138 | interface Vector { 139 | readonly x: number 140 | readonly y: number 141 | } 142 | 143 | interface RGB { 144 | readonly r: number 145 | readonly g: number 146 | readonly b: number 147 | } 148 | 149 | interface RGBA { 150 | readonly r: number 151 | readonly g: number 152 | readonly b: number 153 | readonly a: number 154 | } 155 | 156 | interface FontName { 157 | readonly family: string 158 | readonly style: string 159 | } 160 | 161 | type TextCase = "ORIGINAL" | "UPPER" | "LOWER" | "TITLE" 162 | 163 | type TextDecoration = "NONE" | "UNDERLINE" | "STRIKETHROUGH" 164 | 165 | interface ArcData { 166 | readonly startingAngle: number 167 | readonly endingAngle: number 168 | readonly innerRadius: number 169 | } 170 | 171 | interface ShadowEffect { 172 | readonly type: "DROP_SHADOW" | "INNER_SHADOW" 173 | readonly color: RGBA 174 | readonly offset: Vector 175 | readonly radius: number 176 | readonly visible: boolean 177 | readonly blendMode: BlendMode 178 | } 179 | 180 | interface BlurEffect { 181 | readonly type: "LAYER_BLUR" | "BACKGROUND_BLUR" 182 | readonly radius: number 183 | readonly visible: boolean 184 | } 185 | 186 | type Effect = ShadowEffect | BlurEffect 187 | 188 | type ConstraintType = "MIN" | "CENTER" | "MAX" | "STRETCH" | "SCALE" 189 | 190 | interface Constraints { 191 | readonly horizontal: ConstraintType 192 | readonly vertical: ConstraintType 193 | } 194 | 195 | interface ColorStop { 196 | readonly position: number 197 | readonly color: RGBA 198 | } 199 | 200 | interface ImageFilters { 201 | readonly exposure?: number 202 | readonly contrast?: number 203 | readonly saturation?: number 204 | readonly temperature?: number 205 | readonly tint?: number 206 | readonly highlights?: number 207 | readonly shadows?: number 208 | } 209 | 210 | interface SolidPaint { 211 | readonly type: "SOLID" 212 | readonly color: RGB 213 | 214 | readonly visible?: boolean 215 | readonly opacity?: number 216 | readonly blendMode?: BlendMode 217 | } 218 | 219 | interface GradientPaint { 220 | readonly type: "GRADIENT_LINEAR" | "GRADIENT_RADIAL" | "GRADIENT_ANGULAR" | "GRADIENT_DIAMOND" 221 | readonly gradientTransform: Transform 222 | readonly gradientStops: ReadonlyArray 223 | 224 | readonly visible?: boolean 225 | readonly opacity?: number 226 | readonly blendMode?: BlendMode 227 | } 228 | 229 | interface ImagePaint { 230 | readonly type: "IMAGE" 231 | readonly scaleMode: "FILL" | "FIT" | "CROP" | "TILE" 232 | readonly imageHash: string | null 233 | readonly imageTransform?: Transform // setting for "CROP" 234 | readonly scalingFactor?: number // setting for "TILE" 235 | readonly filters?: ImageFilters 236 | 237 | readonly visible?: boolean 238 | readonly opacity?: number 239 | readonly blendMode?: BlendMode 240 | } 241 | 242 | type Paint = SolidPaint | GradientPaint | ImagePaint 243 | 244 | interface Guide { 245 | readonly axis: "X" | "Y" 246 | readonly offset: number 247 | } 248 | 249 | interface RowsColsLayoutGrid { 250 | readonly pattern: "ROWS" | "COLUMNS" 251 | readonly alignment: "MIN" | "MAX" | "STRETCH" | "CENTER" 252 | readonly gutterSize: number 253 | 254 | readonly count: number // Infinity when "Auto" is set in the UI 255 | readonly sectionSize?: number // Not set for alignment: "STRETCH" 256 | readonly offset?: number // Not set for alignment: "CENTER" 257 | 258 | readonly visible?: boolean 259 | readonly color?: RGBA 260 | } 261 | 262 | interface GridLayoutGrid { 263 | readonly pattern: "GRID" 264 | readonly sectionSize: number 265 | 266 | readonly visible?: boolean 267 | readonly color?: RGBA 268 | } 269 | 270 | type LayoutGrid = RowsColsLayoutGrid | GridLayoutGrid 271 | 272 | interface ExportSettingsConstraints { 273 | readonly type: "SCALE" | "WIDTH" | "HEIGHT" 274 | readonly value: number 275 | } 276 | 277 | interface ExportSettingsImage { 278 | readonly format: "JPG" | "PNG" 279 | readonly contentsOnly?: boolean // defaults to true 280 | readonly suffix?: string 281 | readonly constraint?: ExportSettingsConstraints 282 | } 283 | 284 | interface ExportSettingsSVG { 285 | readonly format: "SVG" 286 | readonly contentsOnly?: boolean // defaults to true 287 | readonly suffix?: string 288 | readonly svgOutlineText?: boolean // defaults to true 289 | readonly svgIdAttribute?: boolean // defaults to false 290 | readonly svgSimplifyStroke?: boolean // defaults to true 291 | } 292 | 293 | interface ExportSettingsPDF { 294 | readonly format: "PDF" 295 | readonly contentsOnly?: boolean // defaults to true 296 | readonly suffix?: string 297 | } 298 | 299 | type ExportSettings = ExportSettingsImage | ExportSettingsSVG | ExportSettingsPDF 300 | 301 | type WindingRule = "NONZERO" | "EVENODD" 302 | 303 | interface VectorVertex { 304 | readonly x: number 305 | readonly y: number 306 | readonly strokeCap?: StrokeCap 307 | readonly strokeJoin?: StrokeJoin 308 | readonly cornerRadius?: number 309 | readonly handleMirroring?: HandleMirroring 310 | } 311 | 312 | interface VectorSegment { 313 | readonly start: number 314 | readonly end: number 315 | readonly tangentStart?: Vector // Defaults to { x: 0, y: 0 } 316 | readonly tangentEnd?: Vector // Defaults to { x: 0, y: 0 } 317 | } 318 | 319 | interface VectorRegion { 320 | readonly windingRule: WindingRule 321 | readonly loops: ReadonlyArray> 322 | } 323 | 324 | interface VectorNetwork { 325 | readonly vertices: ReadonlyArray 326 | readonly segments: ReadonlyArray 327 | readonly regions?: ReadonlyArray // Defaults to [] 328 | } 329 | 330 | interface VectorPath { 331 | readonly windingRule: WindingRule | "NONE" 332 | readonly data: string 333 | } 334 | 335 | type VectorPaths = ReadonlyArray 336 | 337 | interface LetterSpacing { 338 | readonly value: number 339 | readonly unit: "PIXELS" | "PERCENT" 340 | } 341 | 342 | type LineHeight = { 343 | readonly value: number 344 | readonly unit: "PIXELS" | "PERCENT" 345 | } | { 346 | readonly unit: "AUTO" 347 | } 348 | 349 | type BlendMode = 350 | "PASS_THROUGH" | 351 | "NORMAL" | 352 | "DARKEN" | 353 | "MULTIPLY" | 354 | "LINEAR_BURN" | 355 | "COLOR_BURN" | 356 | "LIGHTEN" | 357 | "SCREEN" | 358 | "LINEAR_DODGE" | 359 | "COLOR_DODGE" | 360 | "OVERLAY" | 361 | "SOFT_LIGHT" | 362 | "HARD_LIGHT" | 363 | "DIFFERENCE" | 364 | "EXCLUSION" | 365 | "HUE" | 366 | "SATURATION" | 367 | "COLOR" | 368 | "LUMINOSITY" 369 | 370 | interface Font { 371 | fontName: FontName 372 | } 373 | 374 | type Reaction = { action: Action, trigger: Trigger } 375 | 376 | type Action = 377 | { readonly type: "BACK" | "CLOSE" } | 378 | { readonly type: "URL", url: string } | 379 | { readonly type: "NODE", 380 | readonly destinationId: string | null, 381 | readonly navigation: Navigation, 382 | readonly transition: Transition | null, 383 | readonly preserveScrollPosition: boolean, 384 | 385 | // Only present if navigation == "OVERLAY" and the destination uses 386 | // overlay position type "RELATIVE" 387 | readonly overlayRelativePosition?: Vector, 388 | } 389 | 390 | interface SimpleTransition { 391 | readonly type: "DISSOLVE" | "SMART_ANIMATE" 392 | readonly easing: Easing 393 | readonly duration: number 394 | } 395 | 396 | interface DirectionalTransition { 397 | readonly type: "MOVE_IN" | "MOVE_OUT" | "PUSH" | "SLIDE_IN" | "SLIDE_OUT" 398 | readonly direction: "LEFT" | "RIGHT" | "TOP" | "BOTTOM" 399 | readonly matchLayers: boolean 400 | 401 | readonly easing: Easing 402 | readonly duration: number 403 | } 404 | 405 | export type Transition = SimpleTransition | DirectionalTransition 406 | 407 | type Trigger = 408 | { readonly type: "ON_CLICK" | "ON_HOVER" | "ON_PRESS" | "ON_DRAG" } | 409 | { readonly type: "AFTER_TIMEOUT", readonly timeout: number } | 410 | { readonly type: "MOUSE_ENTER" | "MOUSE_LEAVE" | "MOUSE_UP" | "MOUSE_DOWN", 411 | readonly delay: number, 412 | } 413 | 414 | type Navigation = "NAVIGATE" | "SWAP" | "OVERLAY" 415 | 416 | interface Easing { 417 | readonly type: "EASE_IN" | "EASE_OUT" | "EASE_IN_AND_OUT" | "LINEAR" 418 | } 419 | 420 | type OverflowDirection = "NONE" | "HORIZONTAL" | "VERTICAL" | "BOTH" 421 | 422 | type OverlayPositionType = "CENTER" | "TOP_LEFT" | "TOP_CENTER" | "TOP_RIGHT" | "BOTTOM_LEFT" | "BOTTOM_CENTER" | "BOTTOM_RIGHT" | "MANUAL" 423 | 424 | type OverlayBackground = 425 | { readonly type: "NONE" } | 426 | { readonly type: "SOLID_COLOR", readonly color: RGBA } 427 | 428 | type OverlayBackgroundInteraction = "NONE" | "CLOSE_ON_CLICK_OUTSIDE" 429 | 430 | //////////////////////////////////////////////////////////////////////////////// 431 | // Mixins 432 | 433 | interface BaseNodeMixin { 434 | readonly id: string 435 | readonly parent: (BaseNode & ChildrenMixin) | null 436 | name: string // Note: setting this also sets `autoRename` to false on TextNodes 437 | readonly removed: boolean 438 | toString(): string 439 | remove(): void 440 | 441 | getPluginData(key: string): string 442 | setPluginData(key: string, value: string): void 443 | 444 | // Namespace is a string that must be at least 3 alphanumeric characters, and should 445 | // be a name related to your plugin. Other plugins will be able to read this data. 446 | getSharedPluginData(namespace: string, key: string): string 447 | setSharedPluginData(namespace: string, key: string, value: string): void 448 | } 449 | 450 | interface SceneNodeMixin { 451 | visible: boolean 452 | locked: boolean 453 | } 454 | 455 | interface ChildrenMixin { 456 | readonly children: ReadonlyArray 457 | 458 | appendChild(child: SceneNode): void 459 | insertChild(index: number, child: SceneNode): void 460 | 461 | findAll(callback?: (node: SceneNode) => boolean): SceneNode[] 462 | findOne(callback: (node: SceneNode) => boolean): SceneNode | null 463 | } 464 | 465 | interface ConstraintMixin { 466 | constraints: Constraints 467 | } 468 | 469 | interface LayoutMixin { 470 | readonly absoluteTransform: Transform 471 | relativeTransform: Transform 472 | x: number 473 | y: number 474 | rotation: number // In degrees 475 | 476 | readonly width: number 477 | readonly height: number 478 | 479 | layoutAlign: "MIN" | "CENTER" | "MAX" // applicable only inside auto-layout frames 480 | 481 | resize(width: number, height: number): void 482 | resizeWithoutConstraints(width: number, height: number): void 483 | } 484 | 485 | interface BlendMixin { 486 | opacity: number 487 | blendMode: BlendMode 488 | isMask: boolean 489 | effects: ReadonlyArray 490 | effectStyleId: string 491 | } 492 | 493 | interface ContainerMixin { 494 | backgrounds: ReadonlyArray // DEPRECATED: use 'fills' instead 495 | layoutGrids: ReadonlyArray 496 | clipsContent: boolean 497 | guides: ReadonlyArray 498 | gridStyleId: string 499 | backgroundStyleId: string // DEPRECATED: use 'fillStyleId' instead 500 | } 501 | 502 | type StrokeCap = "NONE" | "ROUND" | "SQUARE" | "ARROW_LINES" | "ARROW_EQUILATERAL" 503 | type StrokeJoin = "MITER" | "BEVEL" | "ROUND" 504 | type HandleMirroring = "NONE" | "ANGLE" | "ANGLE_AND_LENGTH" 505 | 506 | interface GeometryMixin { 507 | fills: ReadonlyArray | PluginAPI['mixed'] 508 | strokes: ReadonlyArray 509 | strokeWeight: number 510 | strokeAlign: "CENTER" | "INSIDE" | "OUTSIDE" 511 | strokeCap: StrokeCap | PluginAPI['mixed'] 512 | strokeJoin: StrokeJoin | PluginAPI['mixed'] 513 | dashPattern: ReadonlyArray 514 | fillStyleId: string | PluginAPI['mixed'] 515 | strokeStyleId: string 516 | } 517 | 518 | interface CornerMixin { 519 | cornerRadius: number | PluginAPI['mixed'] 520 | cornerSmoothing: number 521 | } 522 | 523 | interface RectangleCornerMixin { 524 | topLeftRadius: number 525 | topRightRadius: number 526 | bottomLeftRadius: number 527 | bottomRightRadius: number 528 | } 529 | 530 | interface ExportMixin { 531 | exportSettings: ReadonlyArray 532 | exportAsync(settings?: ExportSettings): Promise // Defaults to PNG format 533 | } 534 | 535 | interface ReactionMixin { 536 | readonly reactions: ReadonlyArray 537 | } 538 | 539 | interface DefaultShapeMixin extends 540 | BaseNodeMixin, SceneNodeMixin, ReactionMixin, 541 | BlendMixin, GeometryMixin, LayoutMixin, ExportMixin { 542 | } 543 | 544 | interface DefaultFrameMixin extends 545 | BaseNodeMixin, SceneNodeMixin, ReactionMixin, 546 | ChildrenMixin, ContainerMixin, 547 | GeometryMixin, CornerMixin, RectangleCornerMixin, 548 | BlendMixin, ConstraintMixin, LayoutMixin, ExportMixin { 549 | 550 | layoutMode: "NONE" | "HORIZONTAL" | "VERTICAL" 551 | counterAxisSizingMode: "FIXED" | "AUTO" // applicable only if layoutMode != "NONE" 552 | horizontalPadding: number // applicable only if layoutMode != "NONE" 553 | verticalPadding: number // applicable only if layoutMode != "NONE" 554 | itemSpacing: number // applicable only if layoutMode != "NONE" 555 | 556 | overflowDirection: OverflowDirection 557 | numberOfFixedChildren: number 558 | 559 | readonly overlayPositionType: OverlayPositionType 560 | readonly overlayBackground: OverlayBackground 561 | readonly overlayBackgroundInteraction: OverlayBackgroundInteraction 562 | } 563 | 564 | //////////////////////////////////////////////////////////////////////////////// 565 | // Nodes 566 | 567 | interface DocumentNode extends BaseNodeMixin { 568 | readonly type: "DOCUMENT" 569 | 570 | readonly children: ReadonlyArray 571 | 572 | appendChild(child: PageNode): void 573 | insertChild(index: number, child: PageNode): void 574 | 575 | findAll(callback?: (node: (PageNode | SceneNode)) => boolean): Array 576 | findOne(callback: (node: (PageNode | SceneNode)) => boolean): PageNode | SceneNode | null 577 | } 578 | 579 | interface PageNode extends BaseNodeMixin, ChildrenMixin, ExportMixin { 580 | readonly type: "PAGE" 581 | clone(): PageNode 582 | 583 | guides: ReadonlyArray 584 | selection: ReadonlyArray 585 | 586 | backgrounds: ReadonlyArray 587 | 588 | readonly prototypeStartNode: FrameNode | GroupNode | ComponentNode | InstanceNode | null 589 | } 590 | 591 | interface FrameNode extends DefaultFrameMixin { 592 | readonly type: "FRAME" 593 | clone(): FrameNode 594 | } 595 | 596 | interface GroupNode extends BaseNodeMixin, SceneNodeMixin, ReactionMixin, ChildrenMixin, ContainerMixin, BlendMixin, LayoutMixin, ExportMixin { 597 | readonly type: "GROUP" 598 | clone(): GroupNode 599 | } 600 | 601 | interface SliceNode extends BaseNodeMixin, SceneNodeMixin, LayoutMixin, ExportMixin { 602 | readonly type: "SLICE" 603 | clone(): SliceNode 604 | } 605 | 606 | interface RectangleNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin, RectangleCornerMixin { 607 | readonly type: "RECTANGLE" 608 | clone(): RectangleNode 609 | } 610 | 611 | interface LineNode extends DefaultShapeMixin, ConstraintMixin { 612 | readonly type: "LINE" 613 | clone(): LineNode 614 | } 615 | 616 | interface EllipseNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 617 | readonly type: "ELLIPSE" 618 | clone(): EllipseNode 619 | arcData: ArcData 620 | } 621 | 622 | interface PolygonNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 623 | readonly type: "POLYGON" 624 | clone(): PolygonNode 625 | pointCount: number 626 | } 627 | 628 | interface StarNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 629 | readonly type: "STAR" 630 | clone(): StarNode 631 | pointCount: number 632 | innerRadius: number 633 | } 634 | 635 | interface VectorNode extends DefaultShapeMixin, ConstraintMixin, CornerMixin { 636 | readonly type: "VECTOR" 637 | clone(): VectorNode 638 | vectorNetwork: VectorNetwork 639 | vectorPaths: VectorPaths 640 | handleMirroring: HandleMirroring | PluginAPI['mixed'] 641 | } 642 | 643 | interface TextNode extends DefaultShapeMixin, ConstraintMixin { 644 | readonly type: "TEXT" 645 | clone(): TextNode 646 | characters: string 647 | readonly hasMissingFont: boolean 648 | textAlignHorizontal: "LEFT" | "CENTER" | "RIGHT" | "JUSTIFIED" 649 | textAlignVertical: "TOP" | "CENTER" | "BOTTOM" 650 | textAutoResize: "NONE" | "WIDTH_AND_HEIGHT" | "HEIGHT" 651 | paragraphIndent: number 652 | paragraphSpacing: number 653 | autoRename: boolean 654 | 655 | textStyleId: string | PluginAPI['mixed'] 656 | fontSize: number | PluginAPI['mixed'] 657 | fontName: FontName | PluginAPI['mixed'] 658 | textCase: TextCase | PluginAPI['mixed'] 659 | textDecoration: TextDecoration | PluginAPI['mixed'] 660 | letterSpacing: LetterSpacing | PluginAPI['mixed'] 661 | lineHeight: LineHeight | PluginAPI['mixed'] 662 | 663 | getRangeFontSize(start: number, end: number): number | PluginAPI['mixed'] 664 | setRangeFontSize(start: number, end: number, value: number): void 665 | getRangeFontName(start: number, end: number): FontName | PluginAPI['mixed'] 666 | setRangeFontName(start: number, end: number, value: FontName): void 667 | getRangeTextCase(start: number, end: number): TextCase | PluginAPI['mixed'] 668 | setRangeTextCase(start: number, end: number, value: TextCase): void 669 | getRangeTextDecoration(start: number, end: number): TextDecoration | PluginAPI['mixed'] 670 | setRangeTextDecoration(start: number, end: number, value: TextDecoration): void 671 | getRangeLetterSpacing(start: number, end: number): LetterSpacing | PluginAPI['mixed'] 672 | setRangeLetterSpacing(start: number, end: number, value: LetterSpacing): void 673 | getRangeLineHeight(start: number, end: number): LineHeight | PluginAPI['mixed'] 674 | setRangeLineHeight(start: number, end: number, value: LineHeight): void 675 | getRangeFills(start: number, end: number): Paint[] | PluginAPI['mixed'] 676 | setRangeFills(start: number, end: number, value: Paint[]): void 677 | getRangeTextStyleId(start: number, end: number): string | PluginAPI['mixed'] 678 | setRangeTextStyleId(start: number, end: number, value: string): void 679 | getRangeFillStyleId(start: number, end: number): string | PluginAPI['mixed'] 680 | setRangeFillStyleId(start: number, end: number, value: string): void 681 | } 682 | 683 | interface ComponentNode extends DefaultFrameMixin { 684 | readonly type: "COMPONENT" 685 | clone(): ComponentNode 686 | 687 | createInstance(): InstanceNode 688 | description: string 689 | readonly remote: boolean 690 | readonly key: string // The key to use with "importComponentByKeyAsync" 691 | } 692 | 693 | interface InstanceNode extends DefaultFrameMixin { 694 | readonly type: "INSTANCE" 695 | clone(): InstanceNode 696 | masterComponent: ComponentNode 697 | } 698 | 699 | interface BooleanOperationNode extends DefaultShapeMixin, ChildrenMixin, CornerMixin { 700 | readonly type: "BOOLEAN_OPERATION" 701 | clone(): BooleanOperationNode 702 | booleanOperation: "UNION" | "INTERSECT" | "SUBTRACT" | "EXCLUDE" 703 | } 704 | 705 | type BaseNode = 706 | DocumentNode | 707 | PageNode | 708 | SceneNode 709 | 710 | type SceneNode = 711 | SliceNode | 712 | FrameNode | 713 | GroupNode | 714 | ComponentNode | 715 | InstanceNode | 716 | BooleanOperationNode | 717 | VectorNode | 718 | StarNode | 719 | LineNode | 720 | EllipseNode | 721 | PolygonNode | 722 | RectangleNode | 723 | TextNode 724 | 725 | type NodeType = 726 | "DOCUMENT" | 727 | "PAGE" | 728 | "SLICE" | 729 | "FRAME" | 730 | "GROUP" | 731 | "COMPONENT" | 732 | "INSTANCE" | 733 | "BOOLEAN_OPERATION" | 734 | "VECTOR" | 735 | "STAR" | 736 | "LINE" | 737 | "ELLIPSE" | 738 | "POLYGON" | 739 | "RECTANGLE" | 740 | "TEXT" 741 | 742 | //////////////////////////////////////////////////////////////////////////////// 743 | // Styles 744 | type StyleType = "PAINT" | "TEXT" | "EFFECT" | "GRID" 745 | 746 | interface BaseStyle { 747 | readonly id: string 748 | readonly type: StyleType 749 | name: string 750 | description: string 751 | remote: boolean 752 | readonly key: string // The key to use with "importStyleByKeyAsync" 753 | remove(): void 754 | } 755 | 756 | interface PaintStyle extends BaseStyle { 757 | type: "PAINT" 758 | paints: ReadonlyArray 759 | } 760 | 761 | interface TextStyle extends BaseStyle { 762 | type: "TEXT" 763 | fontSize: number 764 | textDecoration: TextDecoration 765 | fontName: FontName 766 | letterSpacing: LetterSpacing 767 | lineHeight: LineHeight 768 | paragraphIndent: number 769 | paragraphSpacing: number 770 | textCase: TextCase 771 | } 772 | 773 | interface EffectStyle extends BaseStyle { 774 | type: "EFFECT" 775 | effects: ReadonlyArray 776 | } 777 | 778 | interface GridStyle extends BaseStyle { 779 | type: "GRID" 780 | layoutGrids: ReadonlyArray 781 | } 782 | 783 | //////////////////////////////////////////////////////////////////////////////// 784 | // Other 785 | 786 | interface Image { 787 | readonly hash: string 788 | getBytesAsync(): Promise 789 | } 790 | } // declare global 791 | 792 | export {} 793 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "JSON → Content", 3 | "id": "797778700190966062", 4 | "api": "1.0.0", 5 | "main": "src/code.js", 6 | "ui": "src/ui.html", 7 | "editorType": [ 8 | "figma" 9 | ] 10 | } -------------------------------------------------------------------------------- /src/code.js: -------------------------------------------------------------------------------- 1 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 2 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 3 | return new (P || (P = Promise))(function (resolve, reject) { 4 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 5 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 6 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 7 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 8 | }); 9 | }; 10 | // This shows the HTML page in "ui.html". 11 | figma.showUI(__html__, { width: 320, height: 512 }); 12 | // Create empty data 13 | let datasets = {}; 14 | let data = []; 15 | let activeTabIndex = 0; 16 | let keys = []; 17 | let nestedObjectName = ""; 18 | let depthSeparator = '-'; 19 | let random; 20 | figma.ui.onmessage = msg => { 21 | // Load data 22 | if (msg.type === 'load') { 23 | data = []; 24 | data = msg.data; 25 | activeTabIndex = msg.activeTabIndex; 26 | datasets[activeTabIndex] = data; 27 | depthSeparator = msg.depthSeparator; 28 | // if single object, wrap in array 29 | if (datasets[activeTabIndex].length === undefined) { 30 | datasets[activeTabIndex] = [data]; 31 | } 32 | if (msg.data !== []) { 33 | // Load every text style in use once 34 | const textNodes = figma.root.findAll(node => node.type === "TEXT"); 35 | for (const textNode of textNodes) { 36 | loadFontsFrom(textNode); 37 | } 38 | } 39 | } 40 | // Success 41 | if (msg.type === 'success') { 42 | figma.notify('Data has been loaded successfully. Have fun!'); 43 | } 44 | // Separator changed 45 | if (msg.type === 'depth-separator-changed') { 46 | depthSeparator = msg.depthSeparator; 47 | } 48 | // Tab changed 49 | if (msg.type === 'tab-changed') { 50 | activeTabIndex = msg.activeTabIndex; 51 | } 52 | // Tab deleted 53 | if (msg.type === 'tab-deleted') { 54 | delete datasets[msg.clickedTabIndex]; 55 | } 56 | // Fill in random entry 57 | if (msg.type === 'fill') { 58 | getData(); 59 | selectData(random, false); 60 | } 61 | // Iterate over random entries 62 | if (msg.type === 'iterate') { 63 | getData(); 64 | selectData(random, true); 65 | } 66 | // Fill in specific entry 67 | if (msg.type === 'fill-specific') { 68 | getData(); 69 | selectData(msg.index, false); 70 | } 71 | // Close 72 | if (msg.type === 'close') { 73 | figma.closePlugin(); 74 | } 75 | // Error 76 | if (msg.type === 'warning') { 77 | // Empty object notification 78 | notifyWarning('emptyObject', 'I have discovered some empty objects and skipped those.'); 79 | // Empty object notification 80 | notifyWarning('nonArray', 'The input data was non-iterable. JSON To Content works best with an array of objects.'); 81 | // Last tab notification 82 | notifyWarning('lastTab', 'You may not delete the last available tab.'); 83 | } 84 | function loadFontsFrom(layer) { 85 | return new Promise(resolve => { 86 | resolve(figma.loadFontAsync({ family: layer.fontName.family, style: layer.fontName.style })); 87 | }); 88 | } 89 | // Data functions 90 | function getData() { 91 | // Reset values 92 | keys = []; 93 | // Get keys of data after import 94 | for (const dataNodes of datasets[activeTabIndex]) { 95 | keys = Object.keys(dataNodes); 96 | } 97 | // Get length and render a random number 98 | let len = datasets[activeTabIndex].length; 99 | random = Math.floor(Math.random() * len); 100 | } 101 | function selectData(index, iterate) { 102 | return __awaiter(this, void 0, void 0, function* () { 103 | // For iteration 104 | const length = datasets[activeTabIndex].length; 105 | let usedIndexes = [index]; 106 | let usedKeys = []; 107 | // Get selection 108 | for (const node of figma.currentPage.selection) { 109 | // If selection is single TextNode 110 | if (node.type === "TEXT") { 111 | yield loadFontsFrom(node); 112 | /** 113 | * Replace Function Text based on key in object 114 | * 1. Direct hit 115 | * 2. Virtual hit (based on object structure) 116 | */ 117 | const _isAvailable = keys.includes(node.name); 118 | const _isSet = typeof data[index][node.name] !== undefined; 119 | const _isNoObject = typeof data[index][node.name] !== 'object'; 120 | const _separatorInName = node.name.indexOf(depthSeparator) >= 0; 121 | // 1. Direct hit (doesn't matter of separators) 122 | if (_isAvailable && _isSet && _isNoObject) { 123 | node.characters = data[index][node.name].toString(); 124 | // 3. First Level hit (separator is only virtual) 125 | } 126 | else if (_separatorInName) { 127 | let separatorElements = node.name.split(depthSeparator); 128 | let firstSeparatorElement = separatorElements[0]; 129 | if (keys.includes(firstSeparatorElement) && data[index][firstSeparatorElement] !== undefined) { 130 | node.characters = changeTextByLayerName(node.name, data[index], firstSeparatorElement); 131 | } 132 | } 133 | } 134 | // If selection contains more TextNodes 135 | else { 136 | // Get keys of data 137 | for (const key of keys) { 138 | // Texts 139 | // Find elements in selection that match any key of data 140 | const textLayers = node.findAll(node => node.name.indexOf(key) >= 0 && node.type === 'TEXT'); 141 | // Rewrite layer text with value of each key but go the next index if key doubles 142 | if (iterate === true) { 143 | for (const layer of textLayers) { 144 | yield loadFontsFrom(layer); 145 | // If duplicates 146 | if (usedKeys.includes(key)) { 147 | index++; 148 | usedIndexes.push(index); 149 | usedKeys = []; 150 | } 151 | // If index reached its end 152 | index = (index >= length) ? 0 : index; 153 | /** 154 | * Replace Function Text based on key in object 155 | * 1. Direct hit 156 | * 2. Virtual hit (based on object structure) 157 | */ 158 | const _isAvailable = keys.includes(layer.name); 159 | const _isSet = datasets[activeTabIndex][index][layer.name] !== undefined; 160 | const _isNoObject = typeof datasets[activeTabIndex][index][layer.name] !== 'object'; 161 | const _separatorInName = layer.name.indexOf(depthSeparator) >= 0; 162 | // 1. Direct hit (doesn't matter of separators) 163 | if (_isAvailable && _isSet && _isNoObject) { 164 | layer.characters = datasets[activeTabIndex][index][layer.name].toString(); 165 | // 2. First Level hit (separator is only virtual) 166 | } 167 | else if (_separatorInName) { 168 | let separatorElements = layer.name.split(depthSeparator); 169 | let firstSeparatorElement = separatorElements[0]; 170 | if (keys.includes(firstSeparatorElement) && datasets[activeTabIndex][index][firstSeparatorElement] !== undefined) { 171 | layer.characters = changeTextByLayerName(layer.name, datasets[activeTabIndex][index], firstSeparatorElement); 172 | } 173 | } 174 | // Add key to used keys 175 | usedKeys.push(key); 176 | } 177 | } 178 | // Rewrite layer text with value of each key 179 | else { 180 | for (const layer of textLayers) { 181 | yield loadFontsFrom(layer); 182 | /** 183 | * Replace Function Text based on key in object 184 | * 1. Direct hit 185 | * 2. Virtual hit (based on object structure) 186 | */ 187 | const _isAvailable = keys.includes(layer.name); 188 | const _isSet = datasets[activeTabIndex][index][layer.name] !== undefined; 189 | const _isNoObject = typeof datasets[activeTabIndex][index][layer.name] !== 'object'; 190 | const _separatorInName = layer.name.indexOf(depthSeparator) >= 0; 191 | // 1. Direct hit (doesn't matter of separators) 192 | if (_isAvailable && _isSet && _isNoObject) { 193 | layer.characters = datasets[activeTabIndex][index][layer.name].toString(); 194 | // 2. First Level hit (separator is only virtual) 195 | } 196 | else if (_separatorInName) { 197 | let separatorElements = layer.name.split(depthSeparator); 198 | let firstSeparatorElement = separatorElements[0]; 199 | if (keys.includes(firstSeparatorElement) && datasets[activeTabIndex][index][firstSeparatorElement] !== undefined) { 200 | layer.characters = changeTextByLayerName(layer.name, datasets[activeTabIndex][index], firstSeparatorElement); 201 | } 202 | } 203 | } 204 | } 205 | } 206 | } 207 | } 208 | }); 209 | } 210 | /** 211 | * Functions for Depth Separator 212 | * --------------------- 213 | */ 214 | function traverseMultiLayerObject(baseObject, keysArray) { 215 | if (keysArray.length > 0) { 216 | const obj = baseObject[keysArray[0]]; 217 | const array = keysArray.slice(1); 218 | traverseMultiLayerObject(obj, array); 219 | } 220 | else { 221 | nestedObjectName = baseObject; 222 | return baseObject; 223 | } 224 | } 225 | function changeTextByLayerName(objProperty, data, key) { 226 | // If property doesn't exist, return old content 227 | if (objProperty === undefined && typeof objProperty !== 'string') { 228 | return objProperty; 229 | } 230 | if (objProperty === key) { 231 | if (data[key]) { 232 | return data[key].toString(); 233 | } 234 | } 235 | else if (objProperty.indexOf(depthSeparator) > 1) { 236 | nestedObjectName = ''; 237 | const nameAsObjectProperty = objProperty.split(depthSeparator); 238 | traverseMultiLayerObject(data, nameAsObjectProperty); 239 | return nestedObjectName; 240 | } 241 | } 242 | function notifyWarning(data, text) { 243 | if (msg.data === data) { 244 | figma.notify(text); 245 | } 246 | } 247 | }; 248 | -------------------------------------------------------------------------------- /src/code.ts: -------------------------------------------------------------------------------- 1 | // This shows the HTML page in "ui.html". 2 | figma.showUI(__html__, { width: 320, height: 512 }); 3 | 4 | // Create empty data 5 | let datasets = {}; 6 | let data = []; 7 | let activeTabIndex = 0; 8 | let keys = []; 9 | let nestedObjectName = ""; 10 | let depthSeparator = '-' 11 | let random; 12 | 13 | figma.ui.onmessage = msg => { 14 | 15 | // Load data 16 | if (msg.type === 'load') { 17 | data = []; 18 | data = msg.data; 19 | activeTabIndex = msg.activeTabIndex; 20 | datasets[activeTabIndex] = data; 21 | depthSeparator = msg.depthSeparator; 22 | 23 | // if single object, wrap in array 24 | if (datasets[activeTabIndex].length === undefined) { 25 | datasets[activeTabIndex] = [data]; 26 | } 27 | 28 | if (msg.data !== []) { 29 | 30 | // Load every text style in use once 31 | const textNodes = figma.root.findAll(node => node.type === "TEXT"); 32 | 33 | for (const textNode of textNodes) { 34 | loadFontsFrom(textNode); 35 | } 36 | } 37 | } 38 | 39 | // Success 40 | if (msg.type === 'success') { 41 | figma.notify('Data has been loaded successfully. Have fun!'); 42 | } 43 | 44 | // Separator changed 45 | if (msg.type === 'depth-separator-changed') { 46 | depthSeparator = msg.depthSeparator; 47 | } 48 | 49 | // Tab changed 50 | if (msg.type === 'tab-changed') { 51 | activeTabIndex = msg.activeTabIndex; 52 | } 53 | 54 | // Tab deleted 55 | if (msg.type === 'tab-deleted') { 56 | delete datasets[msg.clickedTabIndex]; 57 | } 58 | 59 | // Fill in random entry 60 | if (msg.type === 'fill') { 61 | getData(); 62 | selectData(random, false); 63 | } 64 | 65 | // Iterate over random entries 66 | if (msg.type === 'iterate') { 67 | getData(); 68 | selectData(random, true); 69 | } 70 | 71 | // Fill in specific entry 72 | if (msg.type === 'fill-specific') { 73 | getData(); 74 | selectData(msg.index, false); 75 | } 76 | 77 | // Close 78 | if (msg.type === 'close') { 79 | figma.closePlugin(); 80 | } 81 | 82 | // Error 83 | if (msg.type === 'warning') { 84 | 85 | // Empty object notification 86 | notifyWarning('emptyObject', 'I have discovered some empty objects and skipped those.'); 87 | 88 | // Empty object notification 89 | notifyWarning('nonArray', 'The input data was non-iterable. JSON To Content works best with an array of objects.'); 90 | 91 | // Last tab notification 92 | notifyWarning('lastTab', 'You may not delete the last available tab.'); 93 | } 94 | 95 | function loadFontsFrom(layer) { 96 | return new Promise(resolve => { 97 | resolve(figma.loadFontAsync({ family: layer.fontName.family, style: layer.fontName.style })); 98 | }); 99 | } 100 | 101 | // Data functions 102 | function getData() { 103 | 104 | // Reset values 105 | keys = [] 106 | 107 | // Get keys of data after import 108 | for (const dataNodes of datasets[activeTabIndex]) { 109 | keys = Object.keys(dataNodes); 110 | } 111 | 112 | // Get length and render a random number 113 | let len = datasets[activeTabIndex].length; 114 | random = Math.floor(Math.random() * len); 115 | } 116 | 117 | async function selectData(index, iterate) { 118 | 119 | // For iteration 120 | const length = datasets[activeTabIndex].length; 121 | let usedIndexes = [index]; 122 | let usedKeys = []; 123 | 124 | // Get selection 125 | for (const node of figma.currentPage.selection) { 126 | 127 | // If selection is single TextNode 128 | if (node.type === "TEXT") { 129 | await loadFontsFrom(node); 130 | 131 | /** 132 | * Replace Function Text based on key in object 133 | * 1. Direct hit 134 | * 2. Virtual hit (based on object structure) 135 | */ 136 | 137 | const _isAvailable = keys.includes(node.name); 138 | const _isSet = typeof data[index][node.name] !== undefined; 139 | const _isNoObject = typeof data[index][node.name] !== 'object'; 140 | const _separatorInName = node.name.indexOf(depthSeparator) >= 0; 141 | 142 | // 1. Direct hit (doesn't matter of separators) 143 | if (_isAvailable && _isSet && _isNoObject) { 144 | node.characters = data[index][node.name].toString(); 145 | 146 | // 3. First Level hit (separator is only virtual) 147 | } else if (_separatorInName) { 148 | 149 | let separatorElements = node.name.split(depthSeparator); 150 | let firstSeparatorElement = separatorElements[0]; 151 | 152 | if (keys.includes(firstSeparatorElement) && data[index][firstSeparatorElement] !== undefined) { 153 | node.characters = changeTextByLayerName(node.name, data[index], firstSeparatorElement); 154 | } 155 | } 156 | } 157 | 158 | // If selection contains more TextNodes 159 | else { 160 | 161 | // Get keys of data 162 | for (const key of keys) { 163 | 164 | // Texts 165 | // Find elements in selection that match any key of data 166 | const textLayers = node.findAll(node => node.name.indexOf(key) >= 0 && node.type === 'TEXT'); 167 | 168 | // Rewrite layer text with value of each key but go the next index if key doubles 169 | if (iterate === true) { 170 | for (const layer of textLayers) { 171 | await loadFontsFrom(layer); 172 | 173 | // If duplicates 174 | if (usedKeys.includes(key)) { 175 | index++; 176 | usedIndexes.push(index); 177 | usedKeys = []; 178 | } 179 | 180 | // If index reached its end 181 | index = (index >= length) ? 0 : index; 182 | 183 | /** 184 | * Replace Function Text based on key in object 185 | * 1. Direct hit 186 | * 2. Virtual hit (based on object structure) 187 | */ 188 | 189 | const _isAvailable = keys.includes(layer.name); 190 | const _isSet = datasets[activeTabIndex][index][layer.name] !== undefined; 191 | const _isNoObject = typeof datasets[activeTabIndex][index][layer.name] !== 'object'; 192 | const _separatorInName = layer.name.indexOf(depthSeparator) >= 0; 193 | 194 | // 1. Direct hit (doesn't matter of separators) 195 | if (_isAvailable && _isSet && _isNoObject) { 196 | layer.characters = datasets[activeTabIndex][index][layer.name].toString(); 197 | 198 | // 2. First Level hit (separator is only virtual) 199 | } else if (_separatorInName) { 200 | 201 | let separatorElements = layer.name.split(depthSeparator); 202 | let firstSeparatorElement = separatorElements[0]; 203 | 204 | if (keys.includes(firstSeparatorElement) && datasets[activeTabIndex][index][firstSeparatorElement] !== undefined) { 205 | layer.characters = changeTextByLayerName(layer.name, datasets[activeTabIndex][index], firstSeparatorElement); 206 | } 207 | } 208 | 209 | // Add key to used keys 210 | usedKeys.push(key); 211 | } 212 | } 213 | 214 | // Rewrite layer text with value of each key 215 | else { 216 | for (const layer of textLayers) { 217 | await loadFontsFrom(layer); 218 | 219 | /** 220 | * Replace Function Text based on key in object 221 | * 1. Direct hit 222 | * 2. Virtual hit (based on object structure) 223 | */ 224 | 225 | const _isAvailable = keys.includes(layer.name); 226 | const _isSet = datasets[activeTabIndex][index][layer.name] !== undefined; 227 | const _isNoObject = typeof datasets[activeTabIndex][index][layer.name] !== 'object'; 228 | const _separatorInName = layer.name.indexOf(depthSeparator) >= 0; 229 | 230 | // 1. Direct hit (doesn't matter of separators) 231 | if (_isAvailable && _isSet && _isNoObject) { 232 | layer.characters = datasets[activeTabIndex][index][layer.name].toString(); 233 | 234 | // 2. First Level hit (separator is only virtual) 235 | } else if (_separatorInName) { 236 | 237 | let separatorElements = layer.name.split(depthSeparator); 238 | let firstSeparatorElement = separatorElements[0]; 239 | 240 | if (keys.includes(firstSeparatorElement) && datasets[activeTabIndex][index][firstSeparatorElement] !== undefined) { 241 | layer.characters = changeTextByLayerName(layer.name, datasets[activeTabIndex][index], firstSeparatorElement); 242 | } 243 | } 244 | } 245 | } 246 | } 247 | } 248 | } 249 | } 250 | 251 | /** 252 | * Functions for Depth Separator 253 | * --------------------- 254 | */ 255 | function traverseMultiLayerObject(baseObject, keysArray) { 256 | if (keysArray.length > 0) { 257 | const obj = baseObject[keysArray[0]] 258 | const array = keysArray.slice(1) 259 | 260 | traverseMultiLayerObject(obj, array); 261 | } else { 262 | nestedObjectName = baseObject; 263 | return baseObject 264 | } 265 | } 266 | 267 | function changeTextByLayerName(objProperty, data, key) { 268 | 269 | // If property doesn't exist, return old content 270 | if (objProperty === undefined && typeof objProperty !== 'string') { 271 | return objProperty; 272 | } 273 | 274 | if (objProperty === key) { 275 | if (data[key]) { 276 | return data[key].toString(); 277 | } 278 | } else if (objProperty.indexOf(depthSeparator) > 1) { 279 | nestedObjectName = ''; 280 | 281 | const nameAsObjectProperty = objProperty.split(depthSeparator) 282 | 283 | traverseMultiLayerObject(data, nameAsObjectProperty); 284 | 285 | return nestedObjectName; 286 | } 287 | } 288 | 289 | function notifyWarning(data, text) { 290 | if (msg.data === data) { 291 | figma.notify(text); 292 | } 293 | } 294 | } -------------------------------------------------------------------------------- /src/ui.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |
6 | 7 | 8 |
9 | Choose your file settings: 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 | New Tab 53 |
54 | 55 |
56 |
57 | 58 |

59 | 60 | Find data examples on how to structure your files in my open repository. 61 | 62 | Overwrite multiple text layers with real and coherent data. Upload your file, select a group or a frame and click “Random” or a specific dataset from the list you'll receive.

63 | New: The depth separator¹ for .json files allows you to use multi-layered objects instead of putting all properties on the first layer. Define which character you want to use as part of the layer name to display the depth with the separator. Try to avoid the separator as part of your multi-layered object properties. The default value is -.

64 | For example:
65 | The layer name "select-label" will receive the text contents "Label of title" with the following .json:
66 | [ 67 | { 68 | "select": { 69 | "title": "My title", 70 | "label": "Label of title" 71 | } 72 | } 73 | ] 74 |

75 |

² Most .csv files are separated by , or by ;
The default value is ,

Optionally you can pass a character that indicates when the character you've set to be your separator should be printed, for example when it's surrounded by ". So a number like 1,000 can be printed within a comma-separated .csv if you write "1,000" instead.
The default value is "

76 |
77 | 78 |
79 |

80 | 81 | 82 |
83 |
    84 |
    85 |
    86 | 87 | 88 |
    89 | 90 | 91 | 92 |
    93 | 94 | 95 | 543 | 544 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es6", "dom", "es2017"], 4 | "target": "es6" 5 | } 6 | } --------------------------------------------------------------------------------