@rogieking/figui3 2.24.0 → 2.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/components.css +94 -8
  2. package/fig.js +894 -119
  3. package/index.html +369 -3
  4. package/package.json +1 -1
package/index.html CHANGED
@@ -9,6 +9,8 @@
9
9
  <link rel="stylesheet"
10
10
  type="text/css"
11
11
  href="fig.css">
12
+ <link rel="stylesheet"
13
+ href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism.min.css">
12
14
  <script src="fig.js"></script>
13
15
  <style>
14
16
  * {
@@ -161,6 +163,97 @@
161
163
  color: var(--figma-color-text);
162
164
  }
163
165
 
166
+ pre[class*="language-"] {
167
+ background: var(--figma-color-bg-secondary);
168
+ border: 1px solid var(--figma-color-border);
169
+ border-radius: 6px;
170
+ padding: 12px 16px;
171
+ margin: 0;
172
+ overflow-x: auto;
173
+ }
174
+
175
+ pre[class*="language-"]>code[class*="language-"] {
176
+ display: block;
177
+ background: transparent;
178
+ padding: 0;
179
+ font-family: "SF Mono", Monaco, Consolas, monospace;
180
+ font-size: 12px;
181
+ line-height: 1.5;
182
+ color: var(--figma-color-text);
183
+ text-shadow: none;
184
+ }
185
+
186
+ :root {
187
+ --code-token-comment: #6b7280;
188
+ --code-token-punctuation: #374151;
189
+ --code-token-primary: #b42318;
190
+ --code-token-string: #0f766e;
191
+ --code-token-operator: #1d4ed8;
192
+ --code-token-keyword: #6d28d9;
193
+ --code-token-function: #9a3412;
194
+ }
195
+
196
+ html[style*="color-scheme: dark"] {
197
+ --code-token-comment: #8b93a7;
198
+ --code-token-punctuation: #c7cfdd;
199
+ --code-token-primary: #ff8f8f;
200
+ --code-token-string: #6dd3b6;
201
+ --code-token-operator: #7fb4ff;
202
+ --code-token-keyword: #b79bff;
203
+ --code-token-function: #ffc27a;
204
+ }
205
+
206
+ /* Keep token colors aligned with theme and improve contrast in light mode */
207
+ .token.comment,
208
+ .token.prolog,
209
+ .token.doctype,
210
+ .token.cdata {
211
+ color: var(--code-token-comment);
212
+ }
213
+
214
+ .token.punctuation {
215
+ color: var(--code-token-punctuation);
216
+ }
217
+
218
+ .token.property,
219
+ .token.tag,
220
+ .token.boolean,
221
+ .token.number,
222
+ .token.constant,
223
+ .token.symbol,
224
+ .token.deleted {
225
+ color: var(--code-token-primary);
226
+ }
227
+
228
+ .token.selector,
229
+ .token.attr-name,
230
+ .token.string,
231
+ .token.char,
232
+ .token.builtin,
233
+ .token.inserted {
234
+ color: var(--code-token-string);
235
+ }
236
+
237
+ .token.operator,
238
+ .token.entity,
239
+ .token.url,
240
+ .language-css .token.string,
241
+ .style .token.string {
242
+ color: var(--code-token-operator);
243
+ background: transparent;
244
+ }
245
+
246
+ .token.atrule,
247
+ .token.attr-value,
248
+ .token.keyword {
249
+ color: var(--code-token-keyword);
250
+ }
251
+
252
+ .token.function,
253
+ .token.class-name {
254
+ color: var(--code-token-function);
255
+ }
256
+
164
257
  .toolbelt {
165
258
  position: fixed;
166
259
  bottom: 0.75rem;
@@ -1650,9 +1743,208 @@
1650
1743
  </fig-fill-picker>
1651
1744
 
1652
1745
  <h3>Custom Trigger</h3>
1746
+ <p class="description">Any child element can act as the trigger. If the child is not a
1747
+ <code>fig-chit</code>, it becomes click-only — the fill picker won't update its appearance.
1748
+ </p>
1749
+
1750
+ <h4>Button Trigger</h4>
1653
1751
  <fig-fill-picker value='{"type":"solid","color":"#95E1D3"}'>
1654
1752
  <fig-button>Edit Fill</fig-button>
1655
1753
  </fig-fill-picker>
1754
+ <pre
1755
+ style="background: var(--figma-color-bg-secondary); padding: 12px 16px; border-radius: 6px; overflow-x: auto; margin: 0;"><code style="font-family: monospace; font-size: 12px; color: var(--figma-color-text);">&lt;fig-fill-picker value='{"type":"solid","color":"#95E1D3"}'&gt;
1756
+ &lt;fig-button&gt;Edit Fill&lt;/fig-button&gt;
1757
+ &lt;/fig-fill-picker&gt;</code></pre>
1758
+
1759
+ <h4>Icon Button Trigger</h4>
1760
+ <fig-fill-picker value='{"type":"solid","color":"#667eea"}'>
1761
+ <fig-button icon
1762
+ variant="ghost"
1763
+ title="Pick a fill">
1764
+ <span class="fig-mask-icon"
1765
+ style="--icon: var(--icon-eyedropper)"></span>
1766
+ </fig-button>
1767
+ </fig-fill-picker>
1768
+ <pre
1769
+ style="background: var(--figma-color-bg-secondary); padding: 12px 16px; border-radius: 6px; overflow-x: auto; margin: 0;"><code style="font-family: monospace; font-size: 12px; color: var(--figma-color-text);">&lt;fig-fill-picker value='{"type":"solid","color":"#667eea"}'&gt;
1770
+ &lt;fig-button icon variant="ghost" title="Pick a fill"&gt;
1771
+ &lt;span class="fig-mask-icon" style="--icon: var(--icon-eyedropper)"&gt;&lt;/span&gt;
1772
+ &lt;/fig-button&gt;
1773
+ &lt;/fig-fill-picker&gt;</code></pre>
1774
+
1775
+ <h4>Inline Text Trigger</h4>
1776
+ <fig-fill-picker value='{"type":"solid","color":"#F38181"}'>
1777
+ <span style="cursor: pointer; text-decoration: underline; color: var(--figma-color-text-brand)">Change
1778
+ background</span>
1779
+ </fig-fill-picker>
1780
+ <pre
1781
+ style="background: var(--figma-color-bg-secondary); padding: 12px 16px; border-radius: 6px; overflow-x: auto; margin: 0;"><code style="font-family: monospace; font-size: 12px; color: var(--figma-color-text);">&lt;fig-fill-picker value='{"type":"solid","color":"#F38181"}'&gt;
1782
+ &lt;span style="cursor: pointer"&gt;Change background&lt;/span&gt;
1783
+ &lt;/fig-fill-picker&gt;</code></pre>
1784
+
1785
+ <h4>Swatch with Custom Size</h4>
1786
+ <fig-fill-picker id="fill-picker-custom-swatch"
1787
+ value='{"type":"gradient","gradient":{"type":"linear","angle":135,"stops":[{"position":0,"color":"#f093fb","opacity":100},{"position":100,"color":"#f5576c","opacity":100}]}}'>
1788
+ <div id="custom-swatch-preview"
1789
+ style="width: 3rem; height: 3rem; border-radius: 0.5rem; cursor: pointer; background: linear-gradient(135deg, #f093fb, #f5576c)">
1790
+ </div>
1791
+ </fig-fill-picker>
1792
+ <pre
1793
+ style="background: var(--figma-color-bg-secondary); padding: 12px 16px; border-radius: 6px; overflow-x: auto; margin: 0;"><code style="font-family: monospace; font-size: 12px; color: var(--figma-color-text);">&lt;fig-fill-picker value='{"type":"gradient",...}'&gt;
1794
+ &lt;div style="width: 3rem; height: 3rem; border-radius: 0.5rem;
1795
+ cursor: pointer; background: linear-gradient(...)"&gt;
1796
+ &lt;/div&gt;
1797
+ &lt;/fig-fill-picker&gt;</code></pre>
1798
+ <script>
1799
+ (() => {
1800
+ const picker = document.getElementById('fill-picker-custom-swatch');
1801
+ const swatch = document.getElementById('custom-swatch-preview');
1802
+ if (!picker || !swatch) return;
1803
+
1804
+ const getSizingForMedia = (scaleMode, scale) => {
1805
+ switch (scaleMode) {
1806
+ case 'fit':
1807
+ return { size: 'contain', position: 'center' };
1808
+ case 'tile':
1809
+ return { size: `${scale || 50}%`, position: 'top left' };
1810
+ case 'fill':
1811
+ case 'crop':
1812
+ default:
1813
+ return { size: 'cover', position: 'center' };
1814
+ }
1815
+ };
1816
+
1817
+ const updateSwatchPreview = (fill) => {
1818
+ if (!fill) return;
1819
+
1820
+ let background = 'transparent';
1821
+ let backgroundSize = 'cover';
1822
+ let backgroundPosition = 'center';
1823
+
1824
+ if (fill.type === 'solid') {
1825
+ background = fill.color || '#D9D9D9';
1826
+ } else if (fill.type === 'gradient') {
1827
+ background = fill.css || background;
1828
+ } else if (fill.type === 'image' && fill.image?.url) {
1829
+ background = `url(${fill.image.url})`;
1830
+ const sizing = getSizingForMedia(fill.image.scaleMode, fill.image.scale);
1831
+ backgroundSize = sizing.size;
1832
+ backgroundPosition = sizing.position;
1833
+ } else if (fill.type === 'video' && fill.video?.url) {
1834
+ background = `url(${fill.video.url})`;
1835
+ const sizing = getSizingForMedia(fill.video.scaleMode, fill.video.scale);
1836
+ backgroundSize = sizing.size;
1837
+ backgroundPosition = sizing.position;
1838
+ } else if (fill.type === 'webcam' && fill.image?.url) {
1839
+ background = `url(${fill.image.url})`;
1840
+ }
1841
+
1842
+ swatch.style.background = background;
1843
+ swatch.style.backgroundSize = backgroundSize;
1844
+ swatch.style.backgroundPosition = backgroundPosition;
1845
+ };
1846
+
1847
+ picker.addEventListener('input', (e) => updateSwatchPreview(e.detail));
1848
+ picker.addEventListener('change', (e) => updateSwatchPreview(e.detail));
1849
+ updateSwatchPreview(picker.value);
1850
+ })();
1851
+ </script>
1852
+
1853
+ <h3>Custom Mode (Slots)</h3>
1854
+ <p class="description">Add custom modes via <code>slot="mode-{name}"</code> children. The mode name must
1855
+ also appear in the <code>mode</code> attribute. Custom slot content manages its own UI; dispatch
1856
+ <code>input</code>/<code>change</code> events with a <code>detail</code> payload to relay values back.
1857
+ </p>
1858
+
1859
+ <h4>Custom "Shader" Mode</h4>
1860
+ <fig-fill-picker mode="solid,shader"
1861
+ value='{"type":"solid","color":"#95E1D3"}'
1862
+ experimental="modern">
1863
+ <div slot="mode-shader"
1864
+ label="Shader">
1865
+ <fig-field label="Fragment Shader">
1866
+ <fig-input-text multiline
1867
+ placeholder="void main() { ... }"
1868
+ value="void main() {&#10; gl_FragColor = vec4(1.0, 0.0, 0.5, 1.0);&#10;}"></fig-input-text>
1869
+ </fig-field>
1870
+ </div>
1871
+ </fig-fill-picker>
1872
+ <pre
1873
+ style="background: var(--figma-color-bg-secondary); padding: 12px 16px; border-radius: 6px; overflow-x: auto; margin: 0;"><code style="font-family: monospace; font-size: 12px; color: var(--figma-color-text);">&lt;fig-fill-picker mode="solid,shader"&gt;
1874
+ &lt;div slot="mode-shader" label="Shader"&gt;
1875
+ &lt;fig-field label="Fragment Shader"&gt;
1876
+ &lt;fig-input-text multiline
1877
+ placeholder="void main() { ... }"
1878
+ value="..."&gt;&lt;/fig-input-text&gt;
1879
+ &lt;/fig-field&gt;
1880
+ &lt;/div&gt;
1881
+ &lt;/fig-fill-picker&gt;</code></pre>
1882
+
1883
+ <h4>React Custom Mode (via <code>modeready</code>)</h4>
1884
+ <p class="description">Frameworks like React can listen for the <code>modeready</code> event and render
1885
+ directly into the provided container. The DOM is never moved — React owns its tree from the start.</p>
1886
+ <fig-fill-picker id="react-picker" mode="solid,react-demo">
1887
+ <div slot="mode-react-demo" label="React"></div>
1888
+ </fig-fill-picker>
1889
+ <script type="module">
1890
+ import React from 'https://esm.sh/react@18';
1891
+ import { createRoot } from 'https://esm.sh/react-dom@18/client';
1892
+
1893
+ const h = React.createElement;
1894
+ const picker = document.getElementById('react-picker');
1895
+
1896
+ picker.addEventListener('modeready', (e) => {
1897
+ if (e.detail.mode !== 'react-demo') return;
1898
+
1899
+ function ColorButtons() {
1900
+ const [color, setColor] = React.useState('#667eea');
1901
+ const colors = ['#FF6B6B', '#4ECDC4', '#667eea', '#f093fb', '#95E1D3'];
1902
+ return h('div', { style: { display: 'flex', flexDirection: 'column', gap: 8 } },
1903
+ h('p', {
1904
+ style: {
1905
+ fontSize: 11,
1906
+ color: 'var(--figma-color-text-secondary)',
1907
+ margin: 0,
1908
+ }
1909
+ }, `Selected: ${color}`),
1910
+ h('div', { style: { display: 'flex', gap: 4, flexWrap: 'wrap' } },
1911
+ colors.map((c) =>
1912
+ h('button', {
1913
+ key: c,
1914
+ onClick: () => setColor(c),
1915
+ style: {
1916
+ width: 28,
1917
+ height: 28,
1918
+ borderRadius: 4,
1919
+ border: c === color ? '2px solid var(--figma-color-text)' : '1px solid var(--figma-color-border)',
1920
+ background: c,
1921
+ cursor: 'pointer',
1922
+ padding: 0,
1923
+ }
1924
+ })
1925
+ )
1926
+ )
1927
+ );
1928
+ }
1929
+
1930
+ createRoot(e.detail.container).render(h(ColorButtons));
1931
+ });
1932
+ </script>
1933
+ <pre
1934
+ style="background: var(--figma-color-bg-secondary); padding: 12px 16px; border-radius: 6px; overflow-x: auto; margin: 0;"><code style="font-family: monospace; font-size: 12px; color: var(--figma-color-text);">&lt;fig-fill-picker id="react-picker" mode="solid,react-demo"&gt;
1935
+ &lt;div slot="mode-react-demo" label="React"&gt;&lt;/div&gt;
1936
+ &lt;/fig-fill-picker&gt;
1937
+
1938
+ &lt;script type="module"&gt;
1939
+ import React from 'https://esm.sh/react@18';
1940
+ import { createRoot } from 'https://esm.sh/react-dom@18/client';
1941
+
1942
+ const picker = document.getElementById('react-picker');
1943
+ picker.addEventListener('modeready', (e) =&gt; {
1944
+ if (e.detail.mode !== 'react-demo') return;
1945
+ createRoot(e.detail.container).render(&lt;MyComponent /&gt;);
1946
+ });
1947
+ &lt;/script&gt;</code></pre>
1656
1948
 
1657
1949
  <h3>Without Alpha</h3>
1658
1950
  <fig-fill-picker alpha="false"
@@ -3274,9 +3566,11 @@
3274
3566
  &lt;/dialog&gt;</code></pre>
3275
3567
 
3276
3568
  <h3>Viewport Margin</h3>
3277
- <p class="description">Use <code>viewport-margin</code> to define safe areas the popup should avoid (e.g. a bottom toolbar). Uses CSS margin shorthand: <code>top right bottom left</code>.</p>
3569
+ <p class="description">Use <code>viewport-margin</code> to define safe areas the popup should avoid (e.g. a
3570
+ bottom toolbar). Uses CSS margin shorthand: <code>top right bottom left</code>.</p>
3278
3571
  <fig-button id="popup-open-viewport-margin"
3279
- onclick="document.getElementById('popup-viewport-margin').open = true">Open (64px bottom margin)</fig-button>
3572
+ onclick="document.getElementById('popup-viewport-margin').open = true">Open (64px bottom
3573
+ margin)</fig-button>
3280
3574
  <dialog id="popup-viewport-margin"
3281
3575
  is="fig-popup"
3282
3576
  anchor="#popup-open-viewport-margin"
@@ -3285,7 +3579,9 @@
3285
3579
  viewport-margin="8 8 64 8">
3286
3580
  <vstack style="min-width: 14rem;">
3287
3581
  <strong style="padding: 0 var(--spacer-1);">Viewport Margin</strong>
3288
- <p style="padding: 0 var(--spacer-1); margin: 0; font-size: var(--font-size-small); color: var(--figma-color-text-secondary);">This popup won't overlap the bottom 64px of the viewport.</p>
3582
+ <p
3583
+ style="padding: 0 var(--spacer-1); margin: 0; font-size: var(--font-size-small); color: var(--figma-color-text-secondary);">
3584
+ This popup won't overlap the bottom 64px of the viewport.</p>
3289
3585
  <fig-input-text placeholder="Try scrolling down"></fig-input-text>
3290
3586
  </vstack>
3291
3587
  </dialog>
@@ -4043,6 +4339,32 @@
4043
4339
  <fig-button>No delay</fig-button>
4044
4340
  </fig-tooltip>
4045
4341
 
4342
+ <h3>Warmup (Rapid Hover)</h3>
4343
+ <p class="description">After the first tooltip appears, moving quickly between these buttons shows
4344
+ subsequent tooltips instantly — no repeated delay.</p>
4345
+ <hstack>
4346
+ <fig-tooltip text="Add" delay="500">
4347
+ <fig-button icon variant="ghost">
4348
+ <span class="fig-mask-icon" style="--icon: var(--icon-add)"></span>
4349
+ </fig-button>
4350
+ </fig-tooltip>
4351
+ <fig-tooltip text="Remove" delay="500">
4352
+ <fig-button icon variant="ghost">
4353
+ <span class="fig-mask-icon" style="--icon: var(--icon-minus)"></span>
4354
+ </fig-button>
4355
+ </fig-tooltip>
4356
+ <fig-tooltip text="Rotate" delay="500">
4357
+ <fig-button icon variant="ghost">
4358
+ <span class="fig-mask-icon" style="--icon: var(--icon-rotate)"></span>
4359
+ </fig-button>
4360
+ </fig-tooltip>
4361
+ <fig-tooltip text="Swap" delay="500">
4362
+ <fig-button icon variant="ghost">
4363
+ <span class="fig-mask-icon" style="--icon: var(--icon-swap)"></span>
4364
+ </fig-button>
4365
+ </fig-tooltip>
4366
+ </hstack>
4367
+
4046
4368
  <h3>Long Text</h3>
4047
4369
  <fig-tooltip text="This is a much longer tooltip that contains more detailed information about the element">
4048
4370
  <fig-button variant="secondary">Long tooltip</fig-button>
@@ -4565,7 +4887,50 @@ button.addEventListener('click', () => {
4565
4887
  </section>
4566
4888
  </div>
4567
4889
 
4890
+ <script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-core.min.js"></script>
4891
+ <script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-markup.min.js"></script>
4892
+ <script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-css.min.js"></script>
4893
+ <script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-clike.min.js"></script>
4894
+ <script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-javascript.min.js"></script>
4568
4895
  <script>
4896
+ function detectCodeLanguage(codeText) {
4897
+ const source = codeText.trim();
4898
+ if (!source) return 'javascript';
4899
+
4900
+ if (
4901
+ source.startsWith('<') ||
4902
+ source.startsWith('&lt;') ||
4903
+ source.includes('&lt;/') ||
4904
+ source.includes('</')
4905
+ ) {
4906
+ return 'markup';
4907
+ }
4908
+
4909
+ if (
4910
+ /(^|[\n\r])\s*[@.#a-zA-Z][\w\s.#:[\]-]*\{/.test(source) ||
4911
+ /--[\w-]+\s*:/.test(source)
4912
+ ) {
4913
+ return 'css';
4914
+ }
4915
+
4916
+ return 'javascript';
4917
+ }
4918
+
4919
+ function highlightCodeBlocks() {
4920
+ const blocks = document.querySelectorAll('pre > code');
4921
+ blocks.forEach((code) => {
4922
+ if (!code.className.includes('language-')) {
4923
+ const language = detectCodeLanguage(code.textContent || '');
4924
+ code.classList.add(`language-${language}`);
4925
+ code.parentElement?.classList.add(`language-${language}`);
4926
+ }
4927
+ });
4928
+
4929
+ if (window.Prism) {
4930
+ window.Prism.highlightAll();
4931
+ }
4932
+ }
4933
+
4569
4934
  // Highlight nav item based on hash
4570
4935
  function updateActiveNav() {
4571
4936
  const hash = location.hash || '#avatar';
@@ -4610,6 +4975,7 @@ button.addEventListener('click', () => {
4610
4975
 
4611
4976
  // Initial state
4612
4977
  window.addEventListener('load', () => {
4978
+ highlightCodeBlocks();
4613
4979
  updateActiveNav();
4614
4980
  if (location.hash) {
4615
4981
  document.querySelector(location.hash)?.scrollIntoView();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogieking/figui3",
3
- "version": "2.24.0",
3
+ "version": "2.26.0",
4
4
  "description": "A lightweight web components library for building Figma plugin and widget UIs with native look and feel",
5
5
  "author": "Rogie King",
6
6
  "license": "MIT",