framer-dalton 0.0.5 → 0.0.7
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.
- package/README.md +54 -10
- package/dist/cli.js +260 -74
- package/dist/start-relay-server.js +2 -3
- package/docs/skills/framer-canvas-editing-project.md +65 -0
- package/docs/{property-controls.md → skills/framer-code-components.md} +991 -0
- package/docs/{server-api.md → skills/framer.md} +98 -182
- package/package.json +1 -4
- package/docs/all-skills.md +0 -6
- package/docs/code-components.md +0 -115
- package/docs/component-examples.md +0 -869
|
@@ -1,3 +1,124 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: framer-code-components
|
|
3
|
+
description: "Framer code component implementation guidance, platform constraints, layout annotations, property controls, and authoring best practices. Use only after loading the framer skill first in the same task; never load this skill directly as the entry point."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Framer Code Components
|
|
7
|
+
|
|
8
|
+
## Best Practices
|
|
9
|
+
|
|
10
|
+
### Component Structure
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { addPropertyControls, ControlType } from "framer";
|
|
14
|
+
import { motion } from "framer-motion"; // NOT from "framer"
|
|
15
|
+
|
|
16
|
+
interface MyComponentProps {
|
|
17
|
+
/* typed props */
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @framerSupportedLayoutWidth any-prefer-fixed
|
|
22
|
+
* @framerSupportedLayoutHeight any-prefer-fixed
|
|
23
|
+
*/
|
|
24
|
+
export default function MyComponent(props: MyComponentProps) {
|
|
25
|
+
// component
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
addPropertyControls(MyComponent, {
|
|
29
|
+
/* controls */
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Platform Constraints
|
|
34
|
+
|
|
35
|
+
These will cause errors if violated:
|
|
36
|
+
|
|
37
|
+
1. **Single file, default export** - Use named `function` syntax (not arrow functions), no named exports
|
|
38
|
+
2. **Imports** - Only `react`, `react-dom`, `framer`, `framer-motion`. Import `motion` from `"framer-motion"`, not `"framer"`
|
|
39
|
+
3. **Position** - Use `position: relative` on the root element, never `fixed`
|
|
40
|
+
4. **SSR** - Guard `window`/`document` access: `if (typeof window !== "undefined")`
|
|
41
|
+
5. **Annotations** - Include `@framerSupportedLayoutWidth/Height` in a `/** */` block comment immediately above the component function
|
|
42
|
+
6. **Types** - Provide a typed props interface (e.g. `MyComponentProps`). Avoid NodeJS types like `Timeout` — use `number` instead
|
|
43
|
+
|
|
44
|
+
### Layout Annotations
|
|
45
|
+
|
|
46
|
+
| Content | Width | Height |
|
|
47
|
+
| ----------------- | ------------------ | ------------------ |
|
|
48
|
+
| No intrinsic size | `fixed` | `fixed` |
|
|
49
|
+
| Text/auto-sizing | `auto` | `auto` |
|
|
50
|
+
| Flexible | `any-prefer-fixed` | `any-prefer-fixed` |
|
|
51
|
+
|
|
52
|
+
Detect auto vs fixed sizing: check if `style.width` or `style.height` is `"100%"`.
|
|
53
|
+
|
|
54
|
+
### Property Controls
|
|
55
|
+
|
|
56
|
+
To make components configurable in Framer's properties panel, add property controls:
|
|
57
|
+
|
|
58
|
+
- To make colors customizable, use `ControlType.Color`. Reuse the same prop for elements sharing a color.
|
|
59
|
+
- To make text styling customizable, use `ControlType.Font` with `controls: "extended"` and `defaultFontType: "sans-serif"`.
|
|
60
|
+
- For images, use `ControlType.ResponsiveImage`. Set defaults in the component body via destructuring (the control doesn't support `defaultValue`).
|
|
61
|
+
- Provide a `defaultValue` for every prop so components render correctly in the Framer canvas. Include at least one item in `ControlType.Array` controls.
|
|
62
|
+
- `ComponentName.defaultProps` is not supported in Framer — use `defaultValue` on the property control instead.
|
|
63
|
+
- Use `hidden` for conditional visibility: `hidden: (props) => !props.showFeature`
|
|
64
|
+
- Prefer sliders over steppers unless step values are large.
|
|
65
|
+
- Keep controls focused — make key elements configurable, hardcode the rest.
|
|
66
|
+
- Full property control reference is included below in this same skill.
|
|
67
|
+
|
|
68
|
+
### Image Defaults (in component body)
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
const {
|
|
72
|
+
image = {
|
|
73
|
+
src: "https://framerusercontent.com/images/GfGkADagM4KEibNcIiRUWlfrR0.jpg",
|
|
74
|
+
alt: "Default",
|
|
75
|
+
},
|
|
76
|
+
} = props;
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Animation Performance
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
import { useIsStaticRenderer } from "framer";
|
|
83
|
+
import { useInView } from "framer-motion";
|
|
84
|
+
|
|
85
|
+
const isStatic = useIsStaticRenderer();
|
|
86
|
+
const ref = useRef(null);
|
|
87
|
+
const isInView = useInView(ref);
|
|
88
|
+
|
|
89
|
+
if (isStatic) return <StaticPreview />; // Show useful static state
|
|
90
|
+
// Pause animations when out of viewport
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
- For very complex animations, consider WebGL instead of `framer-motion`.
|
|
94
|
+
- Static preview should include visual effects, not just text.
|
|
95
|
+
- Wrapping state updates in `startTransition()` prevents UI blocking and keeps interactions smooth.
|
|
96
|
+
|
|
97
|
+
### Text
|
|
98
|
+
|
|
99
|
+
- For auto-sized components with text, apply `width: max-content` or `minWidth: max-content` to prevent text from collapsing.
|
|
100
|
+
|
|
101
|
+
### Common Errors
|
|
102
|
+
|
|
103
|
+
- WebGL cross-origin: handle `SecurityError: Failed to execute 'texImage2D'` for cross-origin images.
|
|
104
|
+
- Inverted Y-axis: check if WebGL images render upside down and accommodate.
|
|
105
|
+
|
|
106
|
+
### Accessibility
|
|
107
|
+
|
|
108
|
+
- `aria` roles on interactive elements
|
|
109
|
+
- Semantic HTML (`<nav>`, `<article>`, `<section>`)
|
|
110
|
+
- `alt=""` on decorative images
|
|
111
|
+
- 4.5:1 color contrast
|
|
112
|
+
|
|
113
|
+
## Term Interpretation
|
|
114
|
+
|
|
115
|
+
- "responsive" → width/height 100%
|
|
116
|
+
- "modern" → 8px radius, 16px spacing, subtle shadows
|
|
117
|
+
- "minimal" → limited colors, whitespace
|
|
118
|
+
- "interactive" → hover/active states
|
|
119
|
+
- "accessible" → ARIA, semantic HTML
|
|
120
|
+
- "props"/"properties" → Framer property controls
|
|
121
|
+
|
|
1
122
|
# Framer Property Controls
|
|
2
123
|
|
|
3
124
|
Control types for property controls in Framer code components. Each control type specifies a different user interface for receiving input. All control types accept `title`, `description`, and `hidden` properties.
|
|
@@ -1533,3 +1654,873 @@ addPropertyControls(MyComponent, {
|
|
|
1533
1654
|
// Additional file controls...
|
|
1534
1655
|
})
|
|
1535
1656
|
```
|
|
1657
|
+
|
|
1658
|
+
# Framer Code Component Examples
|
|
1659
|
+
|
|
1660
|
+
Reference implementations demonstrating best practices for code components.
|
|
1661
|
+
|
|
1662
|
+
## Cookie Banner
|
|
1663
|
+
|
|
1664
|
+
Location-aware cookie consent with timezone detection.
|
|
1665
|
+
|
|
1666
|
+
```tsx
|
|
1667
|
+
// Cookie banner with opt-in for Europe, opt-out elsewhere, based on time zone
|
|
1668
|
+
import {
|
|
1669
|
+
useEffect,
|
|
1670
|
+
useState,
|
|
1671
|
+
startTransition,
|
|
1672
|
+
type CSSProperties,
|
|
1673
|
+
} from "react";
|
|
1674
|
+
import { addPropertyControls, ControlType, RenderTarget } from "framer";
|
|
1675
|
+
|
|
1676
|
+
interface CookiebannerProps {
|
|
1677
|
+
message: string;
|
|
1678
|
+
acceptLabel: string;
|
|
1679
|
+
declineLabel: string;
|
|
1680
|
+
backgroundColor: string;
|
|
1681
|
+
textColor: string;
|
|
1682
|
+
buttonColor: string;
|
|
1683
|
+
buttonTextColor: string;
|
|
1684
|
+
font: any;
|
|
1685
|
+
borderRadius: number;
|
|
1686
|
+
buttonFont: any;
|
|
1687
|
+
style?: CSSProperties;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
/**
|
|
1691
|
+
* Cookies
|
|
1692
|
+
*
|
|
1693
|
+
* @framerIntrinsicWidth 400
|
|
1694
|
+
* @framerIntrinsicHeight 100
|
|
1695
|
+
*
|
|
1696
|
+
* @framerSupportedLayoutWidth any-prefer-fixed
|
|
1697
|
+
* @framerSupportedLayoutHeight any-prefer-fixed
|
|
1698
|
+
*/
|
|
1699
|
+
export default function Cookiebanner(props: CookiebannerProps) {
|
|
1700
|
+
const {
|
|
1701
|
+
message,
|
|
1702
|
+
acceptLabel,
|
|
1703
|
+
declineLabel,
|
|
1704
|
+
backgroundColor,
|
|
1705
|
+
textColor,
|
|
1706
|
+
buttonColor,
|
|
1707
|
+
buttonTextColor,
|
|
1708
|
+
font,
|
|
1709
|
+
borderRadius,
|
|
1710
|
+
} = props;
|
|
1711
|
+
|
|
1712
|
+
// Guess if user is in Europe based on timezone offset
|
|
1713
|
+
const [show, setShow] = useState(true);
|
|
1714
|
+
const [isEurope, setIsEurope] = useState(false);
|
|
1715
|
+
useEffect(() => {
|
|
1716
|
+
if (typeof window !== "undefined") {
|
|
1717
|
+
const offset = new Date().getTimezoneOffset();
|
|
1718
|
+
// Europe: UTC+0 to UTC+3 (offset -0 to -180)
|
|
1719
|
+
startTransition(() => setIsEurope(offset <= 0 && offset >= -180));
|
|
1720
|
+
}
|
|
1721
|
+
}, []);
|
|
1722
|
+
|
|
1723
|
+
// Hide on accept/decline
|
|
1724
|
+
function handleAccept() {
|
|
1725
|
+
startTransition(() => setShow(false));
|
|
1726
|
+
}
|
|
1727
|
+
function handleDecline() {
|
|
1728
|
+
startTransition(() => setShow(false));
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
if (!show || RenderTarget.current() === RenderTarget.thumbnail) return null;
|
|
1732
|
+
|
|
1733
|
+
const buttonBaseStyles = {
|
|
1734
|
+
borderRadius: 10,
|
|
1735
|
+
flex: 1,
|
|
1736
|
+
border: `1px solid ${buttonColor}`,
|
|
1737
|
+
padding: "8px 18px",
|
|
1738
|
+
cursor: "pointer",
|
|
1739
|
+
...props.buttonFont,
|
|
1740
|
+
};
|
|
1741
|
+
|
|
1742
|
+
const isFixedWidth = props?.style && props.style.width === "100%";
|
|
1743
|
+
|
|
1744
|
+
return (
|
|
1745
|
+
<div
|
|
1746
|
+
style={{
|
|
1747
|
+
...props.style,
|
|
1748
|
+
overflow: "hidden",
|
|
1749
|
+
position: "relative",
|
|
1750
|
+
...(isFixedWidth ? { ...props?.style } : { minWidth: "max-content" }),
|
|
1751
|
+
background: backgroundColor,
|
|
1752
|
+
color: textColor,
|
|
1753
|
+
borderRadius,
|
|
1754
|
+
display: "flex",
|
|
1755
|
+
flexDirection: "column",
|
|
1756
|
+
justifyContent: "space-between",
|
|
1757
|
+
padding: 20,
|
|
1758
|
+
boxShadow: "0 2px 8px rgba(0,0,0,0.08)",
|
|
1759
|
+
gap: 20,
|
|
1760
|
+
|
|
1761
|
+
...props.font,
|
|
1762
|
+
}}
|
|
1763
|
+
>
|
|
1764
|
+
<span style={{ flex: 1 }}>{message}</span>
|
|
1765
|
+
<div style={{ width: "100%", display: "flex", gap: 10 }}>
|
|
1766
|
+
<button
|
|
1767
|
+
style={{
|
|
1768
|
+
...buttonBaseStyles,
|
|
1769
|
+
background: "transparent",
|
|
1770
|
+
color: buttonColor,
|
|
1771
|
+
}}
|
|
1772
|
+
onClick={handleDecline}
|
|
1773
|
+
>
|
|
1774
|
+
{declineLabel}
|
|
1775
|
+
</button>
|
|
1776
|
+
<button
|
|
1777
|
+
style={{
|
|
1778
|
+
...buttonBaseStyles,
|
|
1779
|
+
background: buttonColor,
|
|
1780
|
+
color: buttonTextColor,
|
|
1781
|
+
}}
|
|
1782
|
+
onClick={handleAccept}
|
|
1783
|
+
>
|
|
1784
|
+
{acceptLabel}
|
|
1785
|
+
</button>
|
|
1786
|
+
</div>
|
|
1787
|
+
</div>
|
|
1788
|
+
);
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
addPropertyControls(Cookiebanner, {
|
|
1792
|
+
message: {
|
|
1793
|
+
type: ControlType.String,
|
|
1794
|
+
title: "Message",
|
|
1795
|
+
defaultValue: "We use cookies to improve your website experience.",
|
|
1796
|
+
displayTextArea: true,
|
|
1797
|
+
},
|
|
1798
|
+
acceptLabel: {
|
|
1799
|
+
type: ControlType.String,
|
|
1800
|
+
title: "Accept Label",
|
|
1801
|
+
defaultValue: "Accept",
|
|
1802
|
+
},
|
|
1803
|
+
declineLabel: {
|
|
1804
|
+
type: ControlType.String,
|
|
1805
|
+
title: "Decline Label",
|
|
1806
|
+
defaultValue: "Decline",
|
|
1807
|
+
},
|
|
1808
|
+
backgroundColor: {
|
|
1809
|
+
type: ControlType.Color,
|
|
1810
|
+
title: "Background",
|
|
1811
|
+
defaultValue: "#fff",
|
|
1812
|
+
},
|
|
1813
|
+
textColor: {
|
|
1814
|
+
type: ControlType.Color,
|
|
1815
|
+
title: "Text Color",
|
|
1816
|
+
defaultValue: "#222",
|
|
1817
|
+
},
|
|
1818
|
+
buttonColor: {
|
|
1819
|
+
type: ControlType.Color,
|
|
1820
|
+
title: "Button Color",
|
|
1821
|
+
defaultValue: "#111",
|
|
1822
|
+
},
|
|
1823
|
+
buttonTextColor: {
|
|
1824
|
+
type: ControlType.Color,
|
|
1825
|
+
title: "Button Text",
|
|
1826
|
+
defaultValue: "#fff",
|
|
1827
|
+
},
|
|
1828
|
+
font: {
|
|
1829
|
+
type: ControlType.Font,
|
|
1830
|
+
title: "Font",
|
|
1831
|
+
controls: "extended",
|
|
1832
|
+
defaultFontType: "sans-serif",
|
|
1833
|
+
defaultValue: {
|
|
1834
|
+
variant: "Medium",
|
|
1835
|
+
fontSize: "14px",
|
|
1836
|
+
letterSpacing: "-0.01em",
|
|
1837
|
+
lineHeight: "1em",
|
|
1838
|
+
},
|
|
1839
|
+
},
|
|
1840
|
+
buttonFont: {
|
|
1841
|
+
type: ControlType.Font,
|
|
1842
|
+
title: "Font",
|
|
1843
|
+
controls: "extended",
|
|
1844
|
+
defaultFontType: "sans-serif",
|
|
1845
|
+
defaultValue: {
|
|
1846
|
+
variant: "Medium",
|
|
1847
|
+
fontSize: "14px",
|
|
1848
|
+
letterSpacing: "-0.01em",
|
|
1849
|
+
lineHeight: "1em",
|
|
1850
|
+
},
|
|
1851
|
+
},
|
|
1852
|
+
borderRadius: {
|
|
1853
|
+
type: ControlType.Number,
|
|
1854
|
+
title: "Radius",
|
|
1855
|
+
defaultValue: 8,
|
|
1856
|
+
min: 0,
|
|
1857
|
+
max: 32,
|
|
1858
|
+
},
|
|
1859
|
+
});
|
|
1860
|
+
```
|
|
1861
|
+
|
|
1862
|
+
## Tweemoji
|
|
1863
|
+
|
|
1864
|
+
Convert emoji to Twitter's Twemoji SVGs.
|
|
1865
|
+
|
|
1866
|
+
````tsx
|
|
1867
|
+
import { useMemo, useEffect, useState, type CSSProperties } from "react";
|
|
1868
|
+
import { addPropertyControls, ControlType, withCSS } from "framer";
|
|
1869
|
+
import twemojiParser from "https://jspm.dev/twemoji-parser@14.0.0";
|
|
1870
|
+
|
|
1871
|
+
const fireSrc =
|
|
1872
|
+
"https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/svg/1f525.svg";
|
|
1873
|
+
|
|
1874
|
+
interface TwemojiProps {
|
|
1875
|
+
/** Emoji to convert such as 🍐, 🐙 or 🐸 */
|
|
1876
|
+
search?: string;
|
|
1877
|
+
isSelection?: boolean;
|
|
1878
|
+
[prop: string]: any;
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
const baseURL = "https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/svg/";
|
|
1882
|
+
|
|
1883
|
+
/**
|
|
1884
|
+
* TWEMOJI
|
|
1885
|
+
*
|
|
1886
|
+
* Convert any emoji into a Twemoji from Twitter. Choose a preset or type in your emoji and the Twemoji will automatically appear on the canvas.
|
|
1887
|
+
*
|
|
1888
|
+
* ```jsx
|
|
1889
|
+
* <Twemoji search="🍐" />
|
|
1890
|
+
* ```
|
|
1891
|
+
*
|
|
1892
|
+
* @framerIntrinsicWidth 100
|
|
1893
|
+
* @framerIntrinsicHeight 100
|
|
1894
|
+
*
|
|
1895
|
+
* @framerSupportedLayoutWidth fixed
|
|
1896
|
+
* @framerSupportedLayoutHeight fixed
|
|
1897
|
+
*/
|
|
1898
|
+
export default function Twemoji(props: TwemojiProps) {
|
|
1899
|
+
const { search, isSelection, selection, style, alt = "" } = props;
|
|
1900
|
+
|
|
1901
|
+
const emoji = useMemo(() => {
|
|
1902
|
+
if (isSelection) return selection;
|
|
1903
|
+
if (!search) return "⭐️";
|
|
1904
|
+
return search;
|
|
1905
|
+
}, [search, isSelection, selection]);
|
|
1906
|
+
|
|
1907
|
+
const src = useMemo(() => {
|
|
1908
|
+
const parsedTwemoji = twemojiParser.parse(emoji, {
|
|
1909
|
+
buildUrl: (icon) => `${baseURL}${icon}.svg`,
|
|
1910
|
+
});
|
|
1911
|
+
return parsedTwemoji[0].url;
|
|
1912
|
+
}, [emoji]);
|
|
1913
|
+
|
|
1914
|
+
return (
|
|
1915
|
+
<div style={containerStyle}>
|
|
1916
|
+
<img src={src} style={containerStyle} alt={alt} />
|
|
1917
|
+
</div>
|
|
1918
|
+
);
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
addPropertyControls<TwemojiProps>(Twemoji, {
|
|
1922
|
+
isSelection: {
|
|
1923
|
+
type: ControlType.Boolean,
|
|
1924
|
+
title: "Select",
|
|
1925
|
+
enabledTitle: "Preset",
|
|
1926
|
+
disabledTitle: "Search",
|
|
1927
|
+
},
|
|
1928
|
+
selection: {
|
|
1929
|
+
type: ControlType.Enum,
|
|
1930
|
+
title: " ",
|
|
1931
|
+
options: ["🔥", "💖", "😆", "👍", "👎"],
|
|
1932
|
+
defaultValue: "🔥",
|
|
1933
|
+
displaySegmentedControl: true,
|
|
1934
|
+
hidden: ({ isSelection }) => !isSelection,
|
|
1935
|
+
},
|
|
1936
|
+
search: {
|
|
1937
|
+
type: ControlType.String,
|
|
1938
|
+
title: " ",
|
|
1939
|
+
placeholder: "Paste Emoji…",
|
|
1940
|
+
defaultValue: "⭐️",
|
|
1941
|
+
hidden: ({ isSelection }) => isSelection,
|
|
1942
|
+
},
|
|
1943
|
+
});
|
|
1944
|
+
|
|
1945
|
+
const containerStyle: CSSProperties = {
|
|
1946
|
+
height: "100%",
|
|
1947
|
+
width: "100%",
|
|
1948
|
+
objectFit: "contain",
|
|
1949
|
+
textAlign: "center",
|
|
1950
|
+
overflow: "hidden",
|
|
1951
|
+
backgroundColor: "transparent",
|
|
1952
|
+
};
|
|
1953
|
+
````
|
|
1954
|
+
|
|
1955
|
+
## Image Compare
|
|
1956
|
+
|
|
1957
|
+
Before/after image comparison slider.
|
|
1958
|
+
|
|
1959
|
+
```tsx
|
|
1960
|
+
import {
|
|
1961
|
+
addPropertyControls,
|
|
1962
|
+
ControlType,
|
|
1963
|
+
RenderTarget,
|
|
1964
|
+
useIsStaticRenderer,
|
|
1965
|
+
} from "framer";
|
|
1966
|
+
import {
|
|
1967
|
+
useCallback,
|
|
1968
|
+
useEffect,
|
|
1969
|
+
useRef,
|
|
1970
|
+
useState,
|
|
1971
|
+
startTransition,
|
|
1972
|
+
type CSSProperties,
|
|
1973
|
+
} from "react";
|
|
1974
|
+
|
|
1975
|
+
interface Image {
|
|
1976
|
+
src: string;
|
|
1977
|
+
alt: string;
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
interface ImageCompareProps {
|
|
1981
|
+
beforeImage: Image;
|
|
1982
|
+
afterImage: Image;
|
|
1983
|
+
orientation: "horizontal" | "vertical";
|
|
1984
|
+
initialPosition: number;
|
|
1985
|
+
dividerColor: string;
|
|
1986
|
+
dividerWidth: number;
|
|
1987
|
+
dividerShadow: boolean;
|
|
1988
|
+
showHandle: boolean;
|
|
1989
|
+
handleColor: string;
|
|
1990
|
+
handleSize: number;
|
|
1991
|
+
style?: CSSProperties;
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
/**
|
|
1995
|
+
* Image Comparison Slider
|
|
1996
|
+
*
|
|
1997
|
+
* A component that allows users to compare two images by dragging a divider.
|
|
1998
|
+
*
|
|
1999
|
+
* @framerIntrinsicWidth 500
|
|
2000
|
+
* @framerIntrinsicHeight 300
|
|
2001
|
+
*
|
|
2002
|
+
* @framerSupportedLayoutWidth fixed
|
|
2003
|
+
* @framerSupportedLayoutHeight fixed
|
|
2004
|
+
*/
|
|
2005
|
+
export default function ImageCompare(props: ImageCompareProps) {
|
|
2006
|
+
const {
|
|
2007
|
+
beforeImage = {
|
|
2008
|
+
src: "https://framerusercontent.com/images/GfGkADagM4KEibNcIiRUWlfrR0.jpg",
|
|
2009
|
+
alt: "Before image",
|
|
2010
|
+
},
|
|
2011
|
+
afterImage = {
|
|
2012
|
+
src: "https://framerusercontent.com/images/aNsAT3jCvt4zglbWCUoFe33Q.jpg",
|
|
2013
|
+
alt: "After image",
|
|
2014
|
+
},
|
|
2015
|
+
orientation = "horizontal",
|
|
2016
|
+
initialPosition = 50,
|
|
2017
|
+
dividerColor = "#FFFFFF",
|
|
2018
|
+
dividerWidth = 2,
|
|
2019
|
+
dividerShadow = true,
|
|
2020
|
+
showHandle = false,
|
|
2021
|
+
handleColor = "#FFFFFF",
|
|
2022
|
+
handleSize = 40,
|
|
2023
|
+
} = props;
|
|
2024
|
+
|
|
2025
|
+
const isHorizontal = orientation === "horizontal";
|
|
2026
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
2027
|
+
const [position, setPosition] = useState(initialPosition);
|
|
2028
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
2029
|
+
const isStatic = useIsStaticRenderer();
|
|
2030
|
+
|
|
2031
|
+
const updatePositionFromEvent = useCallback(
|
|
2032
|
+
(e) => {
|
|
2033
|
+
if (!containerRef.current) return;
|
|
2034
|
+
|
|
2035
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
2036
|
+
|
|
2037
|
+
if (isHorizontal) {
|
|
2038
|
+
const x = e.clientX - rect.left;
|
|
2039
|
+
const newPosition = Math.max(0, Math.min(100, (x / rect.width) * 100));
|
|
2040
|
+
startTransition(() => setPosition(newPosition));
|
|
2041
|
+
} else {
|
|
2042
|
+
const y = e.clientY - rect.top;
|
|
2043
|
+
const newPosition = Math.max(0, Math.min(100, (y / rect.height) * 100));
|
|
2044
|
+
startTransition(() => setPosition(newPosition));
|
|
2045
|
+
}
|
|
2046
|
+
},
|
|
2047
|
+
[isHorizontal],
|
|
2048
|
+
);
|
|
2049
|
+
|
|
2050
|
+
const handleClick = useCallback(
|
|
2051
|
+
(e) => {
|
|
2052
|
+
// Only handle as a click if we're not dragging
|
|
2053
|
+
if (!isDragging) {
|
|
2054
|
+
updatePositionFromEvent(e);
|
|
2055
|
+
}
|
|
2056
|
+
},
|
|
2057
|
+
[isDragging, updatePositionFromEvent],
|
|
2058
|
+
);
|
|
2059
|
+
|
|
2060
|
+
const handleDoubleClick = () => {
|
|
2061
|
+
startTransition(() => setPosition(initialPosition));
|
|
2062
|
+
};
|
|
2063
|
+
|
|
2064
|
+
const handleMouseDown = (e) => {
|
|
2065
|
+
e.preventDefault();
|
|
2066
|
+
startTransition(() => setIsDragging(true));
|
|
2067
|
+
};
|
|
2068
|
+
|
|
2069
|
+
const handleMouseMove = useCallback(
|
|
2070
|
+
(e) => {
|
|
2071
|
+
if (!isDragging || !containerRef.current) return;
|
|
2072
|
+
updatePositionFromEvent(e);
|
|
2073
|
+
},
|
|
2074
|
+
[isDragging, updatePositionFromEvent],
|
|
2075
|
+
);
|
|
2076
|
+
|
|
2077
|
+
const handleMouseUp = useCallback(() => {
|
|
2078
|
+
startTransition(() => setIsDragging(false));
|
|
2079
|
+
}, []);
|
|
2080
|
+
|
|
2081
|
+
// Add global event listeners for drag
|
|
2082
|
+
useEffect(() => {
|
|
2083
|
+
if (isStatic) return;
|
|
2084
|
+
|
|
2085
|
+
const handleGlobalMouseMove = (e) => handleMouseMove(e);
|
|
2086
|
+
const handleGlobalMouseUp = () => handleMouseUp();
|
|
2087
|
+
|
|
2088
|
+
if (isDragging) {
|
|
2089
|
+
window.addEventListener("mousemove", handleGlobalMouseMove);
|
|
2090
|
+
window.addEventListener("mouseup", handleGlobalMouseUp);
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
return () => {
|
|
2094
|
+
window.removeEventListener("mousemove", handleGlobalMouseMove);
|
|
2095
|
+
window.removeEventListener("mouseup", handleGlobalMouseUp);
|
|
2096
|
+
};
|
|
2097
|
+
}, [isDragging, handleMouseMove, handleMouseUp, isStatic]);
|
|
2098
|
+
|
|
2099
|
+
return (
|
|
2100
|
+
<div
|
|
2101
|
+
ref={containerRef}
|
|
2102
|
+
style={{
|
|
2103
|
+
position: "relative",
|
|
2104
|
+
width: "100%",
|
|
2105
|
+
height: "100%",
|
|
2106
|
+
overflow: "hidden",
|
|
2107
|
+
cursor: isDragging
|
|
2108
|
+
? isHorizontal
|
|
2109
|
+
? "ew-resize"
|
|
2110
|
+
: "ns-resize"
|
|
2111
|
+
: "pointer",
|
|
2112
|
+
userSelect: "none",
|
|
2113
|
+
}}
|
|
2114
|
+
onClick={isStatic ? undefined : handleClick}
|
|
2115
|
+
onMouseMove={isStatic ? undefined : handleMouseMove}
|
|
2116
|
+
onMouseDown={isStatic ? undefined : handleMouseDown}
|
|
2117
|
+
onMouseUp={isStatic ? undefined : handleMouseUp}
|
|
2118
|
+
onDoubleClick={handleDoubleClick}
|
|
2119
|
+
onKeyDown={(e) => {
|
|
2120
|
+
if (e.key === " " || e.key === "Enter") {
|
|
2121
|
+
handleClick(e);
|
|
2122
|
+
}
|
|
2123
|
+
}}
|
|
2124
|
+
tabIndex={0}
|
|
2125
|
+
role="slider"
|
|
2126
|
+
aria-valuenow={position}
|
|
2127
|
+
aria-valuemin={0}
|
|
2128
|
+
aria-valuemax={100}
|
|
2129
|
+
aria-orientation={orientation}
|
|
2130
|
+
>
|
|
2131
|
+
{/* After Image (Full) */}
|
|
2132
|
+
<div
|
|
2133
|
+
style={{
|
|
2134
|
+
position: "absolute",
|
|
2135
|
+
top: 0,
|
|
2136
|
+
left: 0,
|
|
2137
|
+
width: "100%",
|
|
2138
|
+
height: "100%",
|
|
2139
|
+
backgroundImage: `url(${afterImage.src})`,
|
|
2140
|
+
backgroundSize: "cover",
|
|
2141
|
+
backgroundPosition: "center",
|
|
2142
|
+
}}
|
|
2143
|
+
aria-label={afterImage.alt}
|
|
2144
|
+
role="img"
|
|
2145
|
+
/>
|
|
2146
|
+
|
|
2147
|
+
{/* Before Image (Clipped) */}
|
|
2148
|
+
<div
|
|
2149
|
+
style={{
|
|
2150
|
+
position: "absolute",
|
|
2151
|
+
top: 0,
|
|
2152
|
+
left: 0,
|
|
2153
|
+
width: "100%",
|
|
2154
|
+
height: "100%",
|
|
2155
|
+
backgroundImage: `url(${beforeImage.src})`,
|
|
2156
|
+
backgroundSize: "cover",
|
|
2157
|
+
backgroundPosition: "center",
|
|
2158
|
+
clipPath: isHorizontal
|
|
2159
|
+
? `inset(0 ${100 - position}% 0 0)`
|
|
2160
|
+
: `inset(0 0 ${100 - position}% 0)`,
|
|
2161
|
+
}}
|
|
2162
|
+
aria-label={beforeImage.alt}
|
|
2163
|
+
role="img"
|
|
2164
|
+
/>
|
|
2165
|
+
|
|
2166
|
+
{/* Divider */}
|
|
2167
|
+
<div
|
|
2168
|
+
style={{
|
|
2169
|
+
position: "absolute",
|
|
2170
|
+
top: isHorizontal ? 0 : `${position}%`,
|
|
2171
|
+
left: isHorizontal ? `${position}%` : 0,
|
|
2172
|
+
width: isHorizontal ? `${dividerWidth}px` : "100%",
|
|
2173
|
+
height: isHorizontal ? "100%" : `${dividerWidth}px`,
|
|
2174
|
+
backgroundColor: dividerColor,
|
|
2175
|
+
boxShadow: dividerShadow ? "0 0 5px rgba(0, 0, 0, 0.7)" : "none",
|
|
2176
|
+
transform: isHorizontal
|
|
2177
|
+
? `translateX(-${dividerWidth / 2}px)`
|
|
2178
|
+
: `translateY(-${dividerWidth / 2}px)`,
|
|
2179
|
+
cursor: isHorizontal ? "ew-resize" : "ns-resize",
|
|
2180
|
+
zIndex: 2,
|
|
2181
|
+
}}
|
|
2182
|
+
onMouseDown={isStatic ? undefined : handleMouseDown}
|
|
2183
|
+
/>
|
|
2184
|
+
|
|
2185
|
+
{/* Handle */}
|
|
2186
|
+
{showHandle && (
|
|
2187
|
+
<div
|
|
2188
|
+
style={{
|
|
2189
|
+
position: "absolute",
|
|
2190
|
+
top: isHorizontal
|
|
2191
|
+
? `calc(50% - ${handleSize / 2}px)`
|
|
2192
|
+
: `${position}%`,
|
|
2193
|
+
left: isHorizontal
|
|
2194
|
+
? `${position}%`
|
|
2195
|
+
: `calc(50% - ${handleSize / 2}px)`,
|
|
2196
|
+
width: `${handleSize}px`,
|
|
2197
|
+
height: `${handleSize}px`,
|
|
2198
|
+
borderRadius: "50%",
|
|
2199
|
+
backgroundColor: handleColor,
|
|
2200
|
+
border: `2px solid ${handleColor}`,
|
|
2201
|
+
boxShadow: "0 0 5px rgba(0, 0, 0, 0.5)",
|
|
2202
|
+
transform: isHorizontal
|
|
2203
|
+
? `translateX(-${handleSize / 2}px)`
|
|
2204
|
+
: `translateY(-${handleSize / 2}px)`,
|
|
2205
|
+
cursor: isHorizontal ? "ew-resize" : "ns-resize",
|
|
2206
|
+
zIndex: 3,
|
|
2207
|
+
display: "flex",
|
|
2208
|
+
justifyContent: "center",
|
|
2209
|
+
alignItems: "center",
|
|
2210
|
+
}}
|
|
2211
|
+
onMouseDown={isStatic ? undefined : handleMouseDown}
|
|
2212
|
+
>
|
|
2213
|
+
<div
|
|
2214
|
+
style={{
|
|
2215
|
+
display: "flex",
|
|
2216
|
+
justifyContent: "center",
|
|
2217
|
+
alignItems: "center",
|
|
2218
|
+
width: "100%",
|
|
2219
|
+
height: "100%",
|
|
2220
|
+
transform: isHorizontal ? "rotate(90deg)" : "rotate(0deg)",
|
|
2221
|
+
}}
|
|
2222
|
+
>
|
|
2223
|
+
<svg
|
|
2224
|
+
viewBox="0 0 24 24"
|
|
2225
|
+
width={handleSize * 0.5}
|
|
2226
|
+
height={handleSize * 0.5}
|
|
2227
|
+
strokeWidth="2"
|
|
2228
|
+
stroke="#000"
|
|
2229
|
+
fill="none"
|
|
2230
|
+
aria-label="Drag handle"
|
|
2231
|
+
>
|
|
2232
|
+
<title>Drag handle</title>
|
|
2233
|
+
<path d="M13 5l6 6m-6 6l6-6m-6 0l-6 6m6-6l-6-6" />
|
|
2234
|
+
</svg>
|
|
2235
|
+
</div>
|
|
2236
|
+
</div>
|
|
2237
|
+
)}
|
|
2238
|
+
</div>
|
|
2239
|
+
);
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
addPropertyControls(ImageCompare, {
|
|
2243
|
+
beforeImage: {
|
|
2244
|
+
type: ControlType.ResponsiveImage,
|
|
2245
|
+
title: "Before Image",
|
|
2246
|
+
},
|
|
2247
|
+
afterImage: {
|
|
2248
|
+
type: ControlType.ResponsiveImage,
|
|
2249
|
+
title: "After Image",
|
|
2250
|
+
},
|
|
2251
|
+
orientation: {
|
|
2252
|
+
type: ControlType.Enum,
|
|
2253
|
+
title: "Orientation",
|
|
2254
|
+
options: ["horizontal", "vertical"],
|
|
2255
|
+
optionTitles: ["Horizontal", "Vertical"],
|
|
2256
|
+
defaultValue: "horizontal",
|
|
2257
|
+
displaySegmentedControl: true,
|
|
2258
|
+
},
|
|
2259
|
+
initialPosition: {
|
|
2260
|
+
type: ControlType.Number,
|
|
2261
|
+
title: "Initial Position",
|
|
2262
|
+
defaultValue: 50,
|
|
2263
|
+
min: 0,
|
|
2264
|
+
max: 100,
|
|
2265
|
+
step: 1,
|
|
2266
|
+
unit: "%",
|
|
2267
|
+
},
|
|
2268
|
+
dividerColor: {
|
|
2269
|
+
type: ControlType.Color,
|
|
2270
|
+
title: "Divider Color",
|
|
2271
|
+
defaultValue: "#FFFFFF",
|
|
2272
|
+
},
|
|
2273
|
+
dividerWidth: {
|
|
2274
|
+
type: ControlType.Number,
|
|
2275
|
+
title: "Divider Width",
|
|
2276
|
+
defaultValue: 2,
|
|
2277
|
+
min: 1,
|
|
2278
|
+
max: 20,
|
|
2279
|
+
step: 1,
|
|
2280
|
+
unit: "px",
|
|
2281
|
+
},
|
|
2282
|
+
dividerShadow: {
|
|
2283
|
+
type: ControlType.Boolean,
|
|
2284
|
+
title: "Divider Shadow",
|
|
2285
|
+
defaultValue: true,
|
|
2286
|
+
enabledTitle: "On",
|
|
2287
|
+
disabledTitle: "Off",
|
|
2288
|
+
},
|
|
2289
|
+
showHandle: {
|
|
2290
|
+
type: ControlType.Boolean,
|
|
2291
|
+
title: "Show Handle",
|
|
2292
|
+
defaultValue: false,
|
|
2293
|
+
enabledTitle: "Show",
|
|
2294
|
+
disabledTitle: "Hide",
|
|
2295
|
+
},
|
|
2296
|
+
handleColor: {
|
|
2297
|
+
type: ControlType.Color,
|
|
2298
|
+
title: "Handle Color",
|
|
2299
|
+
defaultValue: "#FFFFFF",
|
|
2300
|
+
hidden: ({ showHandle }) => !showHandle,
|
|
2301
|
+
},
|
|
2302
|
+
handleSize: {
|
|
2303
|
+
type: ControlType.Number,
|
|
2304
|
+
title: "Handle Size",
|
|
2305
|
+
defaultValue: 40,
|
|
2306
|
+
min: 20,
|
|
2307
|
+
max: 80,
|
|
2308
|
+
step: 1,
|
|
2309
|
+
unit: "px",
|
|
2310
|
+
hidden: ({ showHandle }) => !showHandle,
|
|
2311
|
+
},
|
|
2312
|
+
});
|
|
2313
|
+
```
|
|
2314
|
+
|
|
2315
|
+
## Notes (Sticky Note)
|
|
2316
|
+
|
|
2317
|
+
Colorful sticky note with font options.
|
|
2318
|
+
|
|
2319
|
+
```tsx
|
|
2320
|
+
import { type MouseEventHandler, type CSSProperties, useMemo } from "react";
|
|
2321
|
+
import { addPropertyControls, ControlType, RenderTarget, Color } from "framer";
|
|
2322
|
+
|
|
2323
|
+
const colors = {
|
|
2324
|
+
blue: "#0099FF",
|
|
2325
|
+
darkBlue: "#0066FF",
|
|
2326
|
+
purple: "#8855FF",
|
|
2327
|
+
red: "#FF5588",
|
|
2328
|
+
green: "#22CC66",
|
|
2329
|
+
yellow: "#FFBB00",
|
|
2330
|
+
};
|
|
2331
|
+
|
|
2332
|
+
interface NotesProps {
|
|
2333
|
+
note: string;
|
|
2334
|
+
shadow: boolean;
|
|
2335
|
+
color: string;
|
|
2336
|
+
preview: boolean;
|
|
2337
|
+
alignment: "left" | "center" | "right";
|
|
2338
|
+
smallFont: boolean;
|
|
2339
|
+
onClick?: MouseEventHandler<HTMLDivElement>;
|
|
2340
|
+
onMouseEnter?: MouseEventHandler<HTMLDivElement>;
|
|
2341
|
+
onMouseLeave?: MouseEventHandler<HTMLDivElement>;
|
|
2342
|
+
onMouseDown?: MouseEventHandler<HTMLDivElement>;
|
|
2343
|
+
onMouseUp?: MouseEventHandler<HTMLDivElement>;
|
|
2344
|
+
useScriptFont: boolean;
|
|
2345
|
+
font: CSSProperties;
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
/**
|
|
2349
|
+
* STICKY
|
|
2350
|
+
*
|
|
2351
|
+
* @framerIntrinsicWidth 150
|
|
2352
|
+
* @framerIntrinsicHeight 150
|
|
2353
|
+
*
|
|
2354
|
+
* @framerSupportedLayoutWidth any-prefer-fixed
|
|
2355
|
+
* @framerSupportedLayoutHeight any-prefer-fixed
|
|
2356
|
+
*/
|
|
2357
|
+
export default function Notes(props: NotesProps) {
|
|
2358
|
+
const {
|
|
2359
|
+
note = "",
|
|
2360
|
+
shadow,
|
|
2361
|
+
color,
|
|
2362
|
+
preview,
|
|
2363
|
+
alignment,
|
|
2364
|
+
smallFont,
|
|
2365
|
+
onClick,
|
|
2366
|
+
onMouseEnter,
|
|
2367
|
+
onMouseLeave,
|
|
2368
|
+
onMouseDown,
|
|
2369
|
+
onMouseUp,
|
|
2370
|
+
useScriptFont,
|
|
2371
|
+
font,
|
|
2372
|
+
} = props;
|
|
2373
|
+
|
|
2374
|
+
const [baseColorString, backgroundColorString] = useMemo(() => {
|
|
2375
|
+
const baseColor = Color(colors[color]);
|
|
2376
|
+
const hslColor = Color.toHsl(baseColor);
|
|
2377
|
+
hslColor.l = 0.95;
|
|
2378
|
+
|
|
2379
|
+
const baseColorString = Color(colors[color]).toValue();
|
|
2380
|
+
const backgroundColorString = Color(hslColor).toValue();
|
|
2381
|
+
|
|
2382
|
+
return [baseColorString, backgroundColorString];
|
|
2383
|
+
}, [color]);
|
|
2384
|
+
|
|
2385
|
+
const centerAligned = alignment === "center";
|
|
2386
|
+
const hasContent = note.length > 0;
|
|
2387
|
+
|
|
2388
|
+
return (
|
|
2389
|
+
<div
|
|
2390
|
+
style={{
|
|
2391
|
+
flex: 1,
|
|
2392
|
+
width: "100%",
|
|
2393
|
+
height: "100%",
|
|
2394
|
+
display: "flex",
|
|
2395
|
+
alignItems: centerAligned ? "center" : "flex-start",
|
|
2396
|
+
backgroundColor: backgroundColorString,
|
|
2397
|
+
overflow: "hidden",
|
|
2398
|
+
paddingLeft: smallFont ? 15 : 18,
|
|
2399
|
+
paddingTop: useScriptFont ? 12 : 14,
|
|
2400
|
+
paddingBottom: useScriptFont ? 12 : 14,
|
|
2401
|
+
paddingRight: smallFont ? 15 : 18,
|
|
2402
|
+
borderRadius: 8,
|
|
2403
|
+
visibility:
|
|
2404
|
+
RenderTarget.current() === RenderTarget.preview && !preview
|
|
2405
|
+
? "hidden"
|
|
2406
|
+
: "visible",
|
|
2407
|
+
...(useScriptFont ? { fontFamily: "Nanum Pen Script" } : font),
|
|
2408
|
+
//@ts-ignore
|
|
2409
|
+
fontDisplay: "fallback",
|
|
2410
|
+
boxShadow: shadow ? "0 4px 10px rgba(0,0,0,0.08)" : "none",
|
|
2411
|
+
}}
|
|
2412
|
+
{...{ onClick, onMouseEnter, onMouseLeave, onMouseDown, onMouseUp }}
|
|
2413
|
+
>
|
|
2414
|
+
{useScriptFont && (
|
|
2415
|
+
<link
|
|
2416
|
+
href="https://fonts.googleapis.com/css?family=Nanum+Pen+Script&display=swap"
|
|
2417
|
+
rel="stylesheet"
|
|
2418
|
+
/>
|
|
2419
|
+
)}
|
|
2420
|
+
<p
|
|
2421
|
+
style={{
|
|
2422
|
+
width: "max-content",
|
|
2423
|
+
wordBreak: "break-word",
|
|
2424
|
+
overflowWrap: "break-word",
|
|
2425
|
+
overflow: "hidden",
|
|
2426
|
+
whiteSpace: "pre-wrap",
|
|
2427
|
+
margin: 0,
|
|
2428
|
+
fontSize: smallFont
|
|
2429
|
+
? useScriptFont
|
|
2430
|
+
? 18
|
|
2431
|
+
: 12
|
|
2432
|
+
: useScriptFont
|
|
2433
|
+
? 32
|
|
2434
|
+
: 24,
|
|
2435
|
+
|
|
2436
|
+
lineHeight: smallFont
|
|
2437
|
+
? useScriptFont
|
|
2438
|
+
? 1.15
|
|
2439
|
+
: 1.4
|
|
2440
|
+
: useScriptFont
|
|
2441
|
+
? 1.08
|
|
2442
|
+
: 1.3,
|
|
2443
|
+
textAlign: alignment,
|
|
2444
|
+
color: baseColorString,
|
|
2445
|
+
display: "-webkit-box",
|
|
2446
|
+
opacity: hasContent ? 1 : 0.5,
|
|
2447
|
+
WebkitBoxOrient: "vertical",
|
|
2448
|
+
}}
|
|
2449
|
+
>
|
|
2450
|
+
{hasContent ? note : "Write something..."}
|
|
2451
|
+
</p>
|
|
2452
|
+
</div>
|
|
2453
|
+
);
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
addPropertyControls(Notes, {
|
|
2457
|
+
note: {
|
|
2458
|
+
type: ControlType.String,
|
|
2459
|
+
displayTextArea: true,
|
|
2460
|
+
placeholder: `Write something… \n\n\n`,
|
|
2461
|
+
},
|
|
2462
|
+
color: {
|
|
2463
|
+
type: ControlType.Enum,
|
|
2464
|
+
defaultValue: "blue",
|
|
2465
|
+
options: Object.keys(colors),
|
|
2466
|
+
optionTitles: Object.keys(colors).map((c) =>
|
|
2467
|
+
c.replace(/^\w/, (c) => c.toUpperCase()),
|
|
2468
|
+
),
|
|
2469
|
+
},
|
|
2470
|
+
|
|
2471
|
+
alignment: {
|
|
2472
|
+
title: "Text Align",
|
|
2473
|
+
type: ControlType.Enum,
|
|
2474
|
+
displaySegmentedControl: true,
|
|
2475
|
+
optionTitles: ["Left", "Center", "Right"],
|
|
2476
|
+
options: ["left", "center", "right"],
|
|
2477
|
+
},
|
|
2478
|
+
useScriptFont: {
|
|
2479
|
+
type: ControlType.Boolean,
|
|
2480
|
+
disabledTitle: "Custom",
|
|
2481
|
+
enabledTitle: "Script",
|
|
2482
|
+
title: "Font",
|
|
2483
|
+
defaultTitle: true,
|
|
2484
|
+
},
|
|
2485
|
+
font: {
|
|
2486
|
+
type: ControlType.Font,
|
|
2487
|
+
defaultFontType: "sans-serif",
|
|
2488
|
+
controls: "extended",
|
|
2489
|
+
hidden: ({ useScriptFont }) => useScriptFont,
|
|
2490
|
+
},
|
|
2491
|
+
smallFont: {
|
|
2492
|
+
type: ControlType.Boolean,
|
|
2493
|
+
disabledTitle: "Big",
|
|
2494
|
+
enabledTitle: "Small",
|
|
2495
|
+
title: "Text Size",
|
|
2496
|
+
defaultValue: true,
|
|
2497
|
+
},
|
|
2498
|
+
preview: {
|
|
2499
|
+
type: ControlType.Boolean,
|
|
2500
|
+
defaultValue: true,
|
|
2501
|
+
title: "In Preview",
|
|
2502
|
+
enabledTitle: "Show",
|
|
2503
|
+
disabledTitle: "Hide",
|
|
2504
|
+
},
|
|
2505
|
+
shadow: {
|
|
2506
|
+
type: ControlType.Boolean,
|
|
2507
|
+
defaultValue: false,
|
|
2508
|
+
title: "Shadow",
|
|
2509
|
+
enabledTitle: "Show",
|
|
2510
|
+
disabledTitle: "Hide",
|
|
2511
|
+
},
|
|
2512
|
+
});
|
|
2513
|
+
|
|
2514
|
+
Notes.displayName = "Sticky Note";
|
|
2515
|
+
```
|
|
2516
|
+
|
|
2517
|
+
## Key Patterns Demonstrated
|
|
2518
|
+
|
|
2519
|
+
1. **SSR Safety**: `if (typeof window !== "undefined")` guards
|
|
2520
|
+
2. **State Transitions**: `setState` wrapped in `startTransition()` for smooth interactions
|
|
2521
|
+
3. **Static Renderer**: `useIsStaticRenderer()` to skip animations on canvas
|
|
2522
|
+
4. **Image Defaults**: Set in destructuring, not in property controls
|
|
2523
|
+
5. **Font Controls**: `controls: "extended"` with `defaultFontType: "sans-serif"` for full typography customization
|
|
2524
|
+
6. **Conditional Controls**: `hidden: (props) => !props.showFeature`
|
|
2525
|
+
7. **Accessibility**: `role`, `aria-*`, semantic HTML, keyboard support
|
|
2526
|
+
8. **Color Utilities**: Using `Color` from framer for color manipulation
|