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.
@@ -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