@sanity/themer 0.8.1 → 0.10.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.
- package/README.md +159 -19
- package/dist/AnimatedColorWheelIcon-CwWOVAsw.js +171 -0
- package/dist/AnimatedColorWheelIcon-CwWOVAsw.js.map +1 -0
- package/dist/{ResizableSidebar-DD787kQr.js → ResizableSidebar-C1nr3Ls2.js} +1023 -547
- package/dist/ResizableSidebar-C1nr3Ls2.js.map +1 -0
- package/dist/bundle.css +106 -318
- package/dist/index.d.ts +66 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/themes-au1BLKmY.js +139 -0
- package/dist/themes-au1BLKmY.js.map +1 -0
- package/dist/tool.d.ts +3 -14
- package/dist/tool.d.ts.map +1 -1
- package/dist/tool.js +853 -820
- package/dist/tool.js.map +1 -1
- package/package.json +11 -3
- package/dist/ResizableSidebar-DD787kQr.js.map +0 -1
- package/dist/ResizableSidebar.css.ts.vanilla-M9EuPu-q.js +0 -422
- package/dist/ResizableSidebar.css.ts.vanilla-M9EuPu-q.js.map +0 -1
- package/dist/options-CU-emn_p.d.ts +0 -69
- package/dist/options-CU-emn_p.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -2,13 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
Generate [Sanity Studio](https://www.sanity.io/studio) themes from a handful of colors.
|
|
4
4
|
|
|
5
|
+
**Pick your starting point:**
|
|
6
|
+
|
|
7
|
+
- **I want to pick colors visually** → [Use the Studio tool](#studio-tool).
|
|
8
|
+
- **I want to configure a theme in code** → [Build a theme](#usage).
|
|
9
|
+
- **I want a ready-made theme** → [Use a preset](#use-a-preset).
|
|
10
|
+
- **I use the hosted Themer service** → [Migrate an existing theme](#migrating-from-themersanitybuild).
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
5
14
|
```sh
|
|
6
15
|
npm install @sanity/themer
|
|
7
16
|
```
|
|
8
17
|
|
|
18
|
+
**Using the Studio tool?** It requires React and React DOM **19.3 or newer** within the supported React 19 range.
|
|
19
|
+
|
|
9
20
|
## Usage
|
|
10
21
|
|
|
11
|
-
|
|
22
|
+
### Build a theme
|
|
23
|
+
|
|
24
|
+
Call `buildTheme` with your colors, then pass the result to your Studio config's `theme` property.
|
|
25
|
+
|
|
26
|
+
Light and dark mode have separate settings. You can customize either or both.
|
|
12
27
|
|
|
13
28
|
```ts
|
|
14
29
|
import {buildTheme} from '@sanity/themer'
|
|
@@ -33,27 +48,84 @@ export default defineConfig({
|
|
|
33
48
|
})
|
|
34
49
|
```
|
|
35
50
|
|
|
36
|
-
|
|
51
|
+
**You don't need to set everything.** Both schemes and every option are optional.
|
|
52
|
+
|
|
53
|
+
- Leave out a scheme to keep its default Studio colors.
|
|
54
|
+
- Leave out `accent` or `background` to use the Studio default.
|
|
55
|
+
- Leave out `text` to derive a subtle tint from the accent.
|
|
56
|
+
- Use `buildTheme({})` to get the default Studio theme.
|
|
57
|
+
|
|
58
|
+
### What each option does
|
|
37
59
|
|
|
38
|
-
|
|
39
|
-
- `text` replaces the `gray` scale — text, icons, borders and neutral surfaces. When omitted it is derived from `accent`: a mostly desaturated version of it, the way the stock gray carries a hint of the stock blue.
|
|
40
|
-
- `background` replaces `black` in the dark scheme and `white` in the light scheme — the background that every other color of the scheme blends onto.
|
|
41
|
-
- `contrast` controls how strongly text and borders separate from the accent. The default `85` uses the text color as-is; `100` removes its tint entirely (a high contrast scheme with no mixing of text and accent), and lower values blend more and more of the accent into the text scale, giving text and borders more color.
|
|
60
|
+
Colors are hex values, such as `#f00` or `#ff0000`.
|
|
42
61
|
|
|
43
|
-
|
|
62
|
+
| Option | What it changes | Default |
|
|
63
|
+
| ------------ | ------------------------------------------ | -------------------------------------------- |
|
|
64
|
+
| `accent` | Primary buttons, focus rings, and links | Studio blue |
|
|
65
|
+
| `text` | Text, icons, borders, and neutral surfaces | A mostly desaturated accent |
|
|
66
|
+
| `background` | The background other colors blend onto | White in light mode; near-black in dark mode |
|
|
67
|
+
| `contrast` | How much accent tint text and borders have | `85` |
|
|
44
68
|
|
|
45
|
-
|
|
69
|
+
**The contrast slider runs from `15` to `100`:**
|
|
70
|
+
|
|
71
|
+
- **`85`** — keeps the text color's tint as-is.
|
|
72
|
+
- **`100`** — removes the tint for neutral text and borders.
|
|
73
|
+
- **Below `85`** — blends more accent color into text and borders.
|
|
74
|
+
|
|
75
|
+
Themer may adjust your colors to keep the palette usable:
|
|
76
|
+
|
|
77
|
+
- Very dark or very light accent and text colors are brought into a usable range.
|
|
78
|
+
- Dark backgrounds are darkened until they have enough contrast with text and accent colors.
|
|
79
|
+
- Light backgrounds are kept lighter than text and accent colors.
|
|
80
|
+
|
|
81
|
+
### Use a preset
|
|
82
|
+
|
|
83
|
+
`presets` includes the hosted Themer service's presets, ready to use with `buildTheme`.
|
|
46
84
|
|
|
47
85
|
```ts
|
|
48
86
|
import {buildTheme, presets} from '@sanity/themer'
|
|
49
87
|
|
|
50
88
|
const verdant = presets.find((preset) => preset.slug === 'verdant')
|
|
51
|
-
const theme = buildTheme(verdant
|
|
89
|
+
const theme = buildTheme(verdant?.options)
|
|
52
90
|
```
|
|
53
91
|
|
|
92
|
+
### Get palettes without building a theme
|
|
93
|
+
|
|
94
|
+
Use `buildPalette` if you only need the generated colors. It returns `{light, dark}` palettes in the `@sanity/color` shape.
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import {buildPalette} from '@sanity/themer'
|
|
98
|
+
|
|
99
|
+
const {light, dark} = buildPalette({
|
|
100
|
+
light: {accent: '#f00'},
|
|
101
|
+
dark: {accent: '#f66'},
|
|
102
|
+
})
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
<details>
|
|
106
|
+
<summary>How this relates to Sanity UI themes</summary>
|
|
107
|
+
|
|
108
|
+
`buildTheme` returns the same theme type as `buildTheme` from `@sanity/ui/theme`. Instead of design tokens, you provide a few colors per scheme.
|
|
109
|
+
|
|
110
|
+
Under the hood, Themer replaces parts of the default [`@sanity/color`](https://www.sanity.io/docs/color) palette:
|
|
111
|
+
|
|
112
|
+
- `accent` replaces the `blue` scale.
|
|
113
|
+
- `text` replaces the `gray` scale.
|
|
114
|
+
- `background` replaces `black` for dark mode and `white` for light mode.
|
|
115
|
+
|
|
116
|
+
The other color scales keep their defaults. With no options, the result matches `buildTheme()` from `@sanity/ui/theme` exactly.
|
|
117
|
+
|
|
118
|
+
</details>
|
|
119
|
+
|
|
54
120
|
## Studio tool
|
|
55
121
|
|
|
56
|
-
|
|
122
|
+
**Pick, edit, and preview themes without writing color values by hand.**
|
|
123
|
+
|
|
124
|
+
The tool adds a sidebar to your Studio. Selecting a theme previews it live across the Studio while you browse.
|
|
125
|
+
|
|
126
|
+
### 1. Add the tool
|
|
127
|
+
|
|
128
|
+
Add `themerTool()` to your Studio's plugins:
|
|
57
129
|
|
|
58
130
|
```ts
|
|
59
131
|
import {themerTool} from '@sanity/themer/tool'
|
|
@@ -65,21 +137,89 @@ export default defineConfig({
|
|
|
65
137
|
})
|
|
66
138
|
```
|
|
67
139
|
|
|
68
|
-
|
|
140
|
+
The navbar toggle is called **Themer**. To rename it, pass a title: `themerTool({title: 'Appearance'})`.
|
|
69
141
|
|
|
70
|
-
|
|
142
|
+
**No CSS setup needed.** The tool imports `@sanity/themer/bundle.css` automatically.
|
|
143
|
+
|
|
144
|
+
### 2. Try a theme
|
|
145
|
+
|
|
146
|
+
Open **Themer** in the navbar. The sidebar shows:
|
|
147
|
+
|
|
148
|
+
- Your configured Studio theme.
|
|
149
|
+
- Built-in presets.
|
|
150
|
+
- Your own themes.
|
|
151
|
+
|
|
152
|
+
Each card previews a tiny Studio in both light and dark mode. Select a card to apply its theme live.
|
|
153
|
+
|
|
154
|
+
**Want to compare modes?** Use the split-screen toggle in the sidebar header. Your Studio keeps its current appearance, and a second view shows the opposite mode beside it.
|
|
155
|
+
|
|
156
|
+
### 3. Make it yours
|
|
157
|
+
|
|
158
|
+
Start from scratch, duplicate a preset, or [use colors from an image](#use-colors-from-an-image).
|
|
159
|
+
|
|
160
|
+
Your custom theme has separate **light** and **dark** mode cards. Each has:
|
|
161
|
+
|
|
162
|
+
- Accent, text, and background color pickers.
|
|
163
|
+
- A contrast slider.
|
|
164
|
+
- An active marker when the Studio is showing that mode.
|
|
165
|
+
|
|
166
|
+
### 4. Make it permanent
|
|
167
|
+
|
|
168
|
+
**Previewing a theme is not the same as configuring it.**
|
|
169
|
+
|
|
170
|
+
Use the tool's code dialog to get the `buildTheme` snippet, then add it to your Studio config's `theme` property.
|
|
171
|
+
|
|
172
|
+
### Use colors from an image
|
|
173
|
+
|
|
174
|
+
Choose an image to generate a starting palette.
|
|
175
|
+
|
|
176
|
+
**Your image stays on your device. Nothing is uploaded.** The tool reads its colors locally using a canvas.
|
|
177
|
+
|
|
178
|
+
- A vibrant color becomes the accent, adjusted so button labels remain readable.
|
|
179
|
+
- A muted color becomes the text color.
|
|
180
|
+
- Light and dark muted colors tint the backgrounds.
|
|
181
|
+
|
|
182
|
+
Try the swatch variants to preview different starting colors, or select **I'm feeling lucky** to let the tool pick one.
|
|
183
|
+
|
|
184
|
+
### Organize and share themes
|
|
185
|
+
|
|
186
|
+
- **Reorder:** drag cards, or move them from their menus. The order is saved between sessions.
|
|
187
|
+
- **Share:** copy a theme's short code from its menu.
|
|
188
|
+
- **Import:** paste a code into the list, or use the paste button—even in another Studio.
|
|
189
|
+
- **Remove or restore:** themes can be removed and brought back.
|
|
190
|
+
|
|
191
|
+
<details>
|
|
192
|
+
<summary>Loading, animation, and reduced motion</summary>
|
|
193
|
+
|
|
194
|
+
The sidebar code loads only when needed. Loading starts when your pointer moves onto the navbar toggle. If you open it before loading finishes, the color wheel spins while you wait.
|
|
195
|
+
|
|
196
|
+
The sidebar slides in from the edge, with its header aligned to the Studio navbar. Closing it also closes the split preview.
|
|
197
|
+
|
|
198
|
+
The split preview uses React's `ViewTransition`, which is why the tool requires React 19.3.
|
|
199
|
+
|
|
200
|
+
With `prefers-reduced-motion`, both the sidebar and split preview change layouts without animation.
|
|
201
|
+
|
|
202
|
+
</details>
|
|
71
203
|
|
|
72
204
|
## Migrating from themer.sanity.build
|
|
73
205
|
|
|
74
|
-
|
|
206
|
+
**Want to keep your existing hosted theme?** Use [`@sanity/themer-legacy`](https://www.npmjs.com/package/@sanity/themer-legacy). It runs the same generator as [themer.sanity.build](https://themer.sanity.build), but locally.
|
|
75
207
|
|
|
76
|
-
|
|
208
|
+
1. Install the legacy package:
|
|
77
209
|
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
210
|
+
```sh
|
|
211
|
+
npm install @sanity/themer-legacy
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
2. Replace your hosted URL import:
|
|
215
|
+
|
|
216
|
+
```diff
|
|
217
|
+
-import {theme} from 'https://themer.sanity.build/api/hues?preset=verdant&primary=22fca8'
|
|
218
|
+
+import {buildThemeFromUrl} from '@sanity/themer-legacy'
|
|
219
|
+
+const theme = buildThemeFromUrl('https://themer.sanity.build/api/hues?preset=verdant&primary=22fca8')
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Already importing from `@sanity/themer/legacy`?** Switch to `@sanity/themer-legacy`. The old import is deprecated and will be removed in `@sanity/themer@1.0`.
|
|
83
223
|
|
|
84
224
|
## License
|
|
85
225
|
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { blue, cyan, green, magenta, orange, purple, red, yellow } from "@sanity/color";
|
|
2
|
+
import { c } from "react/compiler-runtime";
|
|
3
|
+
import { useEffect, useImperativeHandle } from "react";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { animate, motion, useMotionValue, useTransform } from "motion/react";
|
|
6
|
+
/**
|
|
7
|
+
* The colors the slices fill with, clockwise from 12 o'clock: the palette's
|
|
8
|
+
* hues in rainbow order, like a real color swatch
|
|
9
|
+
*/
|
|
10
|
+
const SLICE_COLORS = [
|
|
11
|
+
red[500].hex,
|
|
12
|
+
orange[500].hex,
|
|
13
|
+
yellow[400].hex,
|
|
14
|
+
green[500].hex,
|
|
15
|
+
cyan[500].hex,
|
|
16
|
+
blue[500].hex,
|
|
17
|
+
purple[500].hex,
|
|
18
|
+
magenta[500].hex
|
|
19
|
+
], TOTAL_ACTS = 2 + (1 - Math.cbrt(1 / 8)), ANIMATION_DURATION = .55 * TOTAL_ACTS;
|
|
20
|
+
function actAt(progress) {
|
|
21
|
+
return progress * TOTAL_ACTS;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* How far the wheel has turned at `progress` (0–1), in degrees.
|
|
25
|
+
*
|
|
26
|
+
* It stands still while the top half of the slices pop in. As the 6 o'clock
|
|
27
|
+
* slice pops in it starts spinning — one full turn with a cubic ease-in, from
|
|
28
|
+
* rest to top speed as the bottom half pop in — and it stands still again the
|
|
29
|
+
* moment it is back upright: the clearing of {@link isSliceFilled} takes off
|
|
30
|
+
* at the speed the spin reached, as if the wheel kept turning and eased out
|
|
31
|
+
* over a second turn, so the two read as one movement.
|
|
32
|
+
*/
|
|
33
|
+
function wheelRotation(progress) {
|
|
34
|
+
let spin = actAt(progress) - 1;
|
|
35
|
+
return spin <= 0 || spin >= 1 ? 0 : 360 * spin ** 3;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Whether slice `index` (clockwise from 12 o'clock) shows its color at
|
|
39
|
+
* `progress` (0–1). A slice pops in whole as the fill reaches its leading
|
|
40
|
+
* edge, and pops out whole as the clearing does — stepped, never partial.
|
|
41
|
+
*
|
|
42
|
+
* The fill goes clockwise at a steady pace over the first two acts, half of
|
|
43
|
+
* the slices in each. The clearing follows in the third act, the same way
|
|
44
|
+
* round, on a cubic ease-out that starts at the speed the spin of
|
|
45
|
+
* {@link wheelRotation} ends with: the slices at first pop out as quickly as
|
|
46
|
+
* they were passing by, then ever more slowly.
|
|
47
|
+
*/
|
|
48
|
+
function isSliceFilled(progress, index) {
|
|
49
|
+
let act = actAt(progress);
|
|
50
|
+
if (act * 8 / 2 <= index) return !1;
|
|
51
|
+
let clearing = act - 2;
|
|
52
|
+
return clearing <= 0 || (1 - (1 - clearing) ** 3) * 8 < index;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The geometry of `ColorWheelIcon` from `@sanity/icons`: a ring between two
|
|
56
|
+
* circles around the center of a 25×25 viewBox, cut into eight slices by the
|
|
57
|
+
* spokes of its outline path
|
|
58
|
+
*/
|
|
59
|
+
const CENTER = 12.5, INNER_RADIUS = 3.5;
|
|
60
|
+
/** A point on a circle around the center, at `degrees` clockwise from 3 o'clock */
|
|
61
|
+
function point(radius, degrees) {
|
|
62
|
+
let radians = degrees * Math.PI / 180;
|
|
63
|
+
return `${CENTER + radius * Math.cos(radians)} ${CENTER + radius * Math.sin(radians)}`;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The part of the ring a slice colors in: between its two spokes, from the
|
|
67
|
+
* inner circle out to the outer. The 12 o'clock slice is centered on 12, so
|
|
68
|
+
* it starts half a slice before it.
|
|
69
|
+
*/
|
|
70
|
+
function slicePath(index) {
|
|
71
|
+
let start = index * 45 - 45 / 2 - 90, end = start + 45;
|
|
72
|
+
return [
|
|
73
|
+
`M${point(8, start)}`,
|
|
74
|
+
`A8 8 0 0 1 ${point(8, end)}`,
|
|
75
|
+
`L${point(INNER_RADIUS, end)}`,
|
|
76
|
+
`A${INNER_RADIUS} ${INNER_RADIUS} 0 0 0 ${point(INNER_RADIUS, start)}`,
|
|
77
|
+
"Z"
|
|
78
|
+
].join("");
|
|
79
|
+
}
|
|
80
|
+
const SLICE_PATHS = SLICE_COLORS.map((_, index) => slicePath(index));
|
|
81
|
+
/**
|
|
82
|
+
* A filled shape, not a stroked one: the icon styles of `@sanity/ui` force
|
|
83
|
+
* `vector-effect: non-scaling-stroke` on every path in an icon, which fills
|
|
84
|
+
* are none the wiser about
|
|
85
|
+
*/
|
|
86
|
+
function Slice(props) {
|
|
87
|
+
let $ = c(8), { index, progress } = props, t0;
|
|
88
|
+
$[0] === index ? t0 = $[1] : (t0 = (value) => +!!isSliceFilled(value, index), $[0] = index, $[1] = t0);
|
|
89
|
+
let opacity = useTransform(progress, t0), t1 = SLICE_PATHS[index], t2 = SLICE_COLORS[index], t3;
|
|
90
|
+
$[2] === opacity ? t3 = $[3] : (t3 = { opacity }, $[2] = opacity, $[3] = t3);
|
|
91
|
+
let t4;
|
|
92
|
+
return $[4] !== t1 || $[5] !== t2 || $[6] !== t3 ? (t4 = /* @__PURE__ */ jsx(motion.path, {
|
|
93
|
+
d: t1,
|
|
94
|
+
fill: t2,
|
|
95
|
+
style: t3
|
|
96
|
+
}), $[4] = t1, $[5] = t2, $[6] = t3, $[7] = t4) : t4 = $[7], t4;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The `ColorWheelIcon` of `@sanity/icons`, with an animation driven by
|
|
100
|
+
* `progress` (0–1): the slices pop in with color one at a time, clockwise
|
|
101
|
+
* from 12 o'clock like the segments of a classic spinner, the wheel spins once
|
|
102
|
+
* as the 6 o'clock slice pops in, and the colors pop out the same way round,
|
|
103
|
+
* carrying the spin's speed to a rest (see `colorWheel.ts`). At rest — at
|
|
104
|
+
* either end — it looks exactly like the icon it stands in for. With
|
|
105
|
+
* `prefers-reduced-motion: reduce` the navbar renders that icon instead of
|
|
106
|
+
* this one, so nothing here moves.
|
|
107
|
+
*
|
|
108
|
+
* @internal
|
|
109
|
+
*/
|
|
110
|
+
function AnimatedColorWheelIcon(t0) {
|
|
111
|
+
let $ = c(18), { busy, ref } = t0, progress = useMotionValue(0), rotate = useTransform(progress, _temp), t1, t2;
|
|
112
|
+
$[0] === progress ? (t1 = $[1], t2 = $[2]) : (t1 = () => ({ spin: () => {
|
|
113
|
+
progress.isAnimating() || animate(progress, [0, 1], {
|
|
114
|
+
duration: ANIMATION_DURATION,
|
|
115
|
+
ease: "linear"
|
|
116
|
+
});
|
|
117
|
+
} }), t2 = [progress], $[0] = progress, $[1] = t1, $[2] = t2), useImperativeHandle(ref, t1, t2);
|
|
118
|
+
let t3, t4;
|
|
119
|
+
$[3] !== busy || $[4] !== progress ? (t3 = () => {
|
|
120
|
+
if (!busy) return;
|
|
121
|
+
let looping = !1, raf = setTimeout(() => {
|
|
122
|
+
looping = !0;
|
|
123
|
+
let lap = (from) => {
|
|
124
|
+
animate(progress, [from, 1], {
|
|
125
|
+
duration: (1 - from) * ANIMATION_DURATION,
|
|
126
|
+
ease: "linear",
|
|
127
|
+
onComplete: () => {
|
|
128
|
+
looping && lap(0);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
lap(progress.get());
|
|
133
|
+
}, 160);
|
|
134
|
+
return () => {
|
|
135
|
+
clearTimeout(raf), looping = !1;
|
|
136
|
+
};
|
|
137
|
+
}, t4 = [busy, progress], $[3] = busy, $[4] = progress, $[5] = t3, $[6] = t4) : (t3 = $[5], t4 = $[6]), useEffect(t3, t4);
|
|
138
|
+
let t5, t6;
|
|
139
|
+
$[7] === progress ? (t5 = $[8], t6 = $[9]) : (t5 = () => () => progress.stop(), t6 = [progress], $[7] = progress, $[8] = t5, $[9] = t6), useEffect(t5, t6);
|
|
140
|
+
let t7;
|
|
141
|
+
$[10] === rotate ? t7 = $[11] : (t7 = { rotate }, $[10] = rotate, $[11] = t7);
|
|
142
|
+
let t8;
|
|
143
|
+
$[12] === progress ? t8 = $[13] : (t8 = SLICE_COLORS.map((color, index) => /* @__PURE__ */ jsx(Slice, {
|
|
144
|
+
index,
|
|
145
|
+
progress
|
|
146
|
+
}, color)), $[12] = progress, $[13] = t8);
|
|
147
|
+
let t9;
|
|
148
|
+
$[14] === Symbol.for("react.memo_cache_sentinel") ? (t9 = /* @__PURE__ */ jsx("path", {
|
|
149
|
+
d: "M9.43853 5.10896L11.1606 9.26642M13.8394 15.7336L15.5615 19.891M15.7336 11.1606L19.891 9.43853M9.26642 13.8394L5.10896 15.5615M5.3139 9.52342L9.23359 11.147M15.7664 13.853L19.6861 15.4766M13.853 9.23359L15.4766 5.3139M9.52342 19.6861L11.147 15.7664M20.5 12.5C20.5 16.9183 16.9183 20.5 12.5 20.5C8.08172 20.5 4.5 16.9183 4.5 12.5C4.5 8.08172 8.08172 4.5 12.5 4.5C16.9183 4.5 20.5 8.08172 20.5 12.5ZM16 12.5C16 14.433 14.433 16 12.5 16C10.567 16 9 14.433 9 12.5C9 10.567 10.567 9 12.5 9C14.433 9 16 10.567 16 12.5Z",
|
|
150
|
+
stroke: "currentColor",
|
|
151
|
+
strokeLinejoin: "round",
|
|
152
|
+
strokeWidth: 1.2
|
|
153
|
+
}), $[14] = t9) : t9 = $[14];
|
|
154
|
+
let t10;
|
|
155
|
+
return $[15] !== t7 || $[16] !== t8 ? (t10 = /* @__PURE__ */ jsxs(motion.svg, {
|
|
156
|
+
"data-sanity-icon": "color-wheel",
|
|
157
|
+
fill: "none",
|
|
158
|
+
height: "1em",
|
|
159
|
+
style: t7,
|
|
160
|
+
viewBox: "0 0 25 25",
|
|
161
|
+
width: "1em",
|
|
162
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
163
|
+
children: [t8, t9]
|
|
164
|
+
}), $[15] = t7, $[16] = t8, $[17] = t10) : t10 = $[17], t10;
|
|
165
|
+
}
|
|
166
|
+
function _temp(value) {
|
|
167
|
+
return wheelRotation(value);
|
|
168
|
+
}
|
|
169
|
+
export { AnimatedColorWheelIcon as default };
|
|
170
|
+
|
|
171
|
+
//# sourceMappingURL=AnimatedColorWheelIcon-CwWOVAsw.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AnimatedColorWheelIcon-CwWOVAsw.js","names":["(value) => (isSliceFilled(value, index) ? 1 : 0)","{opacity}","<motion.path d={SLICE_PATHS[index]} fill={SLICE_COLORS[index]} style={{opacity}} />","SLICE_PATHS[index]","SLICE_COLORS[index]","motion.path","{\n busy,\n ref,\n}: {\n busy: boolean\n ref: React.Ref<{spin: () => void}>\n}","(value) => wheelRotation(value)","() => ({\n spin: () => {\n // A run in progress plays out — hovering again does not cut it short\n if (progress.isAnimating()) return\n\n animate(progress, [0, 1], {duration: ANIMATION_DURATION, ease: 'linear'})\n },\n })","[progress]","() => {\n if (!busy) return undefined\n\n let looping = false\n const raf = setTimeout(() => {\n looping = true\n const lap = (from: number) => {\n animate(progress, [from, 1], {\n duration: (1 - from) * ANIMATION_DURATION,\n ease: 'linear',\n onComplete: () => {\n if (looping) lap(0)\n },\n })\n }\n\n lap(progress.get())\n }, 160)\n\n return () => {\n clearTimeout(raf)\n looping = false\n }\n }","[busy, progress]","() => () => progress.stop()","{rotate}","SLICE_COLORS.map((color, index) => (\n <Slice key={color} index={index} progress={progress} />\n ))","<path d={OUTLINE} stroke=\"currentColor\" strokeLinejoin=\"round\" strokeWidth={1.2} />","<motion.svg\n data-sanity-icon=\"color-wheel\"\n fill=\"none\"\n height=\"1em\"\n style={{rotate}}\n viewBox=\"0 0 25 25\"\n width=\"1em\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n {SLICE_COLORS.map((color, index) => (\n <Slice key={color} index={index} progress={progress} />\n ))}\n <path d={OUTLINE} stroke=\"currentColor\" strokeLinejoin=\"round\" strokeWidth={1.2} />\n </motion.svg>","motion.svg","_c"],"sources":["../src/tool/colorWheel.ts","../src/tool/AnimatedColorWheelIcon.tsx"],"sourcesContent":["import {blue, cyan, green, magenta, orange, purple, red, yellow} from '@sanity/color'\n\n/**\n * The spokes of the color wheel icon (`ColorWheelIcon` from `@sanity/icons`)\n * cut its ring into this many slices, one of them centered on 12 o'clock\n */\nexport const SLICE_COUNT = 8\n\n/**\n * The colors the slices fill with, clockwise from 12 o'clock: the palette's\n * hues in rainbow order, like a real color swatch\n */\nexport const SLICE_COLORS: readonly string[] = [\n red[500].hex,\n orange[500].hex,\n yellow[400].hex,\n green[500].hex,\n cyan[500].hex,\n blue[500].hex,\n purple[500].hex,\n magenta[500].hex,\n]\n\n/**\n * The animation plays in acts of this length, in seconds. In the first act\n * the top half of the slices pop in, one at a time like the segments of a\n * classic spinner; in the second the wheel spins up while the bottom half pop\n * in; in the third the colors pop out again as the spin's speed carries over\n * into the clearing.\n */\nconst ACT_DURATION = 0.55\n\n/**\n * The clearing would take a whole act to come to rest, but its last slice\n * pops out as the clearing reaches that slice's leading edge — with nothing\n * left to show, the animation ends there\n */\nconst CLEARING_ACTS = 1 - Math.cbrt(1 / SLICE_COUNT)\n\nconst TOTAL_ACTS = 2 + CLEARING_ACTS\n\n/** How long the whole animation takes, in seconds */\nexport const ANIMATION_DURATION = ACT_DURATION * TOTAL_ACTS\n\nfunction actAt(progress: number): number {\n return progress * TOTAL_ACTS\n}\n\n/**\n * How far the wheel has turned at `progress` (0–1), in degrees.\n *\n * It stands still while the top half of the slices pop in. As the 6 o'clock\n * slice pops in it starts spinning — one full turn with a cubic ease-in, from\n * rest to top speed as the bottom half pop in — and it stands still again the\n * moment it is back upright: the clearing of {@link isSliceFilled} takes off\n * at the speed the spin reached, as if the wheel kept turning and eased out\n * over a second turn, so the two read as one movement.\n */\nexport function wheelRotation(progress: number): number {\n const spin = actAt(progress) - 1\n\n if (spin <= 0 || spin >= 1) return 0\n\n return 360 * spin ** 3\n}\n\n/**\n * Whether slice `index` (clockwise from 12 o'clock) shows its color at\n * `progress` (0–1). A slice pops in whole as the fill reaches its leading\n * edge, and pops out whole as the clearing does — stepped, never partial.\n *\n * The fill goes clockwise at a steady pace over the first two acts, half of\n * the slices in each. The clearing follows in the third act, the same way\n * round, on a cubic ease-out that starts at the speed the spin of\n * {@link wheelRotation} ends with: the slices at first pop out as quickly as\n * they were passing by, then ever more slowly.\n */\nexport function isSliceFilled(progress: number, index: number): boolean {\n const act = actAt(progress)\n const filling = (act * SLICE_COUNT) / 2\n\n if (filling <= index) return false\n\n const clearing = act - 2\n\n if (clearing <= 0) return true\n\n return (1 - (1 - clearing) ** 3) * SLICE_COUNT < index\n}\n","import {animate, motion, type MotionValue, useMotionValue, useTransform} from 'motion/react'\nimport {useEffect, useImperativeHandle} from 'react'\n\nimport {\n isSliceFilled,\n SLICE_COLORS,\n SLICE_COUNT,\n wheelRotation,\n ANIMATION_DURATION,\n} from './colorWheel'\n\n/**\n * The geometry of `ColorWheelIcon` from `@sanity/icons`: a ring between two\n * circles around the center of a 25×25 viewBox, cut into eight slices by the\n * spokes of its outline path\n */\nconst CENTER = 12.5\nconst OUTER_RADIUS = 8\nconst INNER_RADIUS = 3.5\nconst OUTLINE =\n 'M9.43853 5.10896L11.1606 9.26642M13.8394 15.7336L15.5615 19.891M15.7336 11.1606L19.891 9.43853M9.26642 13.8394L5.10896 15.5615M5.3139 9.52342L9.23359 11.147M15.7664 13.853L19.6861 15.4766M13.853 9.23359L15.4766 5.3139M9.52342 19.6861L11.147 15.7664M20.5 12.5C20.5 16.9183 16.9183 20.5 12.5 20.5C8.08172 20.5 4.5 16.9183 4.5 12.5C4.5 8.08172 8.08172 4.5 12.5 4.5C16.9183 4.5 20.5 8.08172 20.5 12.5ZM16 12.5C16 14.433 14.433 16 12.5 16C10.567 16 9 14.433 9 12.5C9 10.567 10.567 9 12.5 9C14.433 9 16 10.567 16 12.5Z'\n\nconst SLICE_ANGLE = 360 / SLICE_COUNT\n\n/** A point on a circle around the center, at `degrees` clockwise from 3 o'clock */\nfunction point(radius: number, degrees: number): string {\n const radians = (degrees * Math.PI) / 180\n\n return `${CENTER + radius * Math.cos(radians)} ${CENTER + radius * Math.sin(radians)}`\n}\n\n/**\n * The part of the ring a slice colors in: between its two spokes, from the\n * inner circle out to the outer. The 12 o'clock slice is centered on 12, so\n * it starts half a slice before it.\n */\nfunction slicePath(index: number): string {\n const start = index * SLICE_ANGLE - SLICE_ANGLE / 2 - 90\n const end = start + SLICE_ANGLE\n\n return [\n `M${point(OUTER_RADIUS, start)}`,\n `A${OUTER_RADIUS} ${OUTER_RADIUS} 0 0 1 ${point(OUTER_RADIUS, end)}`,\n `L${point(INNER_RADIUS, end)}`,\n `A${INNER_RADIUS} ${INNER_RADIUS} 0 0 0 ${point(INNER_RADIUS, start)}`,\n 'Z',\n ].join('')\n}\n\nconst SLICE_PATHS = SLICE_COLORS.map((_, index) => slicePath(index))\n\n/**\n * A filled shape, not a stroked one: the icon styles of `@sanity/ui` force\n * `vector-effect: non-scaling-stroke` on every path in an icon, which fills\n * are none the wiser about\n */\nfunction Slice(props: {index: number; progress: MotionValue<number>}) {\n const {index, progress} = props\n const opacity = useTransform(progress, (value) => (isSliceFilled(value, index) ? 1 : 0))\n\n return <motion.path d={SLICE_PATHS[index]} fill={SLICE_COLORS[index]} style={{opacity}} />\n}\n\n/**\n * The `ColorWheelIcon` of `@sanity/icons`, with an animation driven by\n * `progress` (0–1): the slices pop in with color one at a time, clockwise\n * from 12 o'clock like the segments of a classic spinner, the wheel spins once\n * as the 6 o'clock slice pops in, and the colors pop out the same way round,\n * carrying the spin's speed to a rest (see `colorWheel.ts`). At rest — at\n * either end — it looks exactly like the icon it stands in for. With\n * `prefers-reduced-motion: reduce` the navbar renders that icon instead of\n * this one, so nothing here moves.\n *\n * @internal\n */\nexport default function AnimatedColorWheelIcon({\n busy,\n ref,\n}: {\n busy: boolean\n ref: React.Ref<{spin: () => void}>\n}) {\n const progress = useMotionValue(0)\n const rotate = useTransform(progress, (value) => wheelRotation(value))\n\n useImperativeHandle(\n ref,\n () => ({\n spin: () => {\n // A run in progress plays out — hovering again does not cut it short\n if (progress.isAnimating()) return\n\n animate(progress, [0, 1], {duration: ANIMATION_DURATION, ease: 'linear'})\n },\n }),\n [progress],\n )\n\n // Laps from wherever a hover's run has got to, so the wheel never jumps,\n // and the lap in progress as the code arrives plays out: at rest, the wheel\n // looks like the icon again\n useEffect(() => {\n if (!busy) return undefined\n\n let looping = false\n const raf = setTimeout(() => {\n looping = true\n const lap = (from: number) => {\n animate(progress, [from, 1], {\n duration: (1 - from) * ANIMATION_DURATION,\n ease: 'linear',\n onComplete: () => {\n if (looping) lap(0)\n },\n })\n }\n\n lap(progress.get())\n }, 160)\n\n return () => {\n clearTimeout(raf)\n looping = false\n }\n }, [busy, progress])\n\n useEffect(() => () => progress.stop(), [progress])\n\n return (\n <motion.svg\n data-sanity-icon=\"color-wheel\"\n fill=\"none\"\n height=\"1em\"\n style={{rotate}}\n viewBox=\"0 0 25 25\"\n width=\"1em\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n {SLICE_COLORS.map((color, index) => (\n <Slice key={color} index={index} progress={progress} />\n ))}\n <path d={OUTLINE} stroke=\"currentColor\" strokeLinejoin=\"round\" strokeWidth={1.2} />\n </motion.svg>\n )\n}\n"],"mappings":"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"}
|