sanity-context 0.1.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/dist/index.cjs +226 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +64 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +230 -0
- package/dist/index.js.map +1 -0
- package/package.json +62 -0
- package/src/components/ContextIcon.tsx +46 -0
- package/src/components/ContextItem.tsx +33 -0
- package/src/components/ContextNavbar.tsx +28 -0
- package/src/components/ContextPopover.tsx +114 -0
- package/src/index.ts +12 -0
- package/src/plugin.ts +20 -0
- package/src/store.ts +84 -0
- package/src/types.ts +39 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: !0 });
|
|
3
|
+
var sanity = require("sanity"), jsxRuntime = require("react/jsx-runtime"), react = require("react"), ui = require("@sanity/ui"), icons = require("@sanity/icons");
|
|
4
|
+
const DEFAULT_STORAGE_KEY = "sanity-context";
|
|
5
|
+
let _definitions = [], _storageKey = DEFAULT_STORAGE_KEY, _state = {}, _resolver = null;
|
|
6
|
+
const _listeners = /* @__PURE__ */ new Set();
|
|
7
|
+
function defaultState(definitions) {
|
|
8
|
+
return Object.fromEntries(definitions.map((d) => [d.id, { enabled: !1, value: d.defaultValue }]));
|
|
9
|
+
}
|
|
10
|
+
function loadFromStorage(definitions) {
|
|
11
|
+
if (typeof window > "u") return defaultState(definitions);
|
|
12
|
+
try {
|
|
13
|
+
const raw = localStorage.getItem(_storageKey);
|
|
14
|
+
if (!raw) return defaultState(definitions);
|
|
15
|
+
const saved = JSON.parse(raw);
|
|
16
|
+
return Object.fromEntries(
|
|
17
|
+
definitions.map((d) => [
|
|
18
|
+
d.id,
|
|
19
|
+
{
|
|
20
|
+
enabled: !!saved[d.id]?.enabled,
|
|
21
|
+
value: d.options.find((o) => o.value === saved[d.id]?.value)?.value ?? d.defaultValue
|
|
22
|
+
}
|
|
23
|
+
])
|
|
24
|
+
);
|
|
25
|
+
} catch {
|
|
26
|
+
return defaultState(definitions);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function persist() {
|
|
30
|
+
typeof window < "u" && localStorage.setItem(_storageKey, JSON.stringify(_state));
|
|
31
|
+
}
|
|
32
|
+
function notify() {
|
|
33
|
+
_listeners.forEach((fn) => fn());
|
|
34
|
+
}
|
|
35
|
+
function initContextStore(definitions, storageKey) {
|
|
36
|
+
_definitions = definitions, _storageKey = storageKey ?? DEFAULT_STORAGE_KEY, _state = loadFromStorage(definitions), notify();
|
|
37
|
+
}
|
|
38
|
+
function setContextsResolver(resolver, storageKey) {
|
|
39
|
+
_resolver = resolver, _storageKey = storageKey ?? DEFAULT_STORAGE_KEY;
|
|
40
|
+
}
|
|
41
|
+
function resolveContexts(ctx) {
|
|
42
|
+
_resolver && initContextStore(_resolver(ctx), _storageKey);
|
|
43
|
+
}
|
|
44
|
+
function getContextDefinitions() {
|
|
45
|
+
return _definitions;
|
|
46
|
+
}
|
|
47
|
+
function getContext() {
|
|
48
|
+
return _state;
|
|
49
|
+
}
|
|
50
|
+
function setContextEntry(id, patch) {
|
|
51
|
+
_state[id] && (_state = { ..._state, [id]: { ..._state[id], ...patch } }, persist(), notify());
|
|
52
|
+
}
|
|
53
|
+
function subscribeToContext(listener) {
|
|
54
|
+
return _listeners.add(listener), () => {
|
|
55
|
+
_listeners.delete(listener);
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const ContextIcon = react.forwardRef(
|
|
59
|
+
function(_props, _ref) {
|
|
60
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "18", height: "18", viewBox: "0 0 128 128", xmlns: "http://www.w3.org/2000/svg", children: [
|
|
61
|
+
/* @__PURE__ */ jsxRuntime.jsx("defs", { children: /* @__PURE__ */ jsxRuntime.jsxs("linearGradient", { id: "g", x1: "0", y1: "0", x2: "1", y2: "0", children: [
|
|
62
|
+
/* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "0%", "stop-color": "#6366F1" }),
|
|
63
|
+
/* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "100%", "stop-color": "#06B6D4" })
|
|
64
|
+
] }) }),
|
|
65
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
66
|
+
"rect",
|
|
67
|
+
{
|
|
68
|
+
x: "16",
|
|
69
|
+
y: "20",
|
|
70
|
+
width: "32",
|
|
71
|
+
height: "88",
|
|
72
|
+
rx: "8",
|
|
73
|
+
fill: "none",
|
|
74
|
+
stroke: "#6366F1",
|
|
75
|
+
"stroke-width": "6"
|
|
76
|
+
}
|
|
77
|
+
),
|
|
78
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
79
|
+
"rect",
|
|
80
|
+
{
|
|
81
|
+
x: "80",
|
|
82
|
+
y: "20",
|
|
83
|
+
width: "32",
|
|
84
|
+
height: "88",
|
|
85
|
+
rx: "8",
|
|
86
|
+
fill: "none",
|
|
87
|
+
stroke: "#06B6D4",
|
|
88
|
+
"stroke-width": "6"
|
|
89
|
+
}
|
|
90
|
+
),
|
|
91
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
92
|
+
"path",
|
|
93
|
+
{
|
|
94
|
+
d: "M48 64 C60 40, 68 88, 80 64",
|
|
95
|
+
fill: "none",
|
|
96
|
+
stroke: "url(#g)",
|
|
97
|
+
"stroke-width": "6",
|
|
98
|
+
"stroke-linecap": "round"
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
] });
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
function ContextItem({ id, label, enabled, onToggle, children }) {
|
|
105
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", justify: "space-between", gap: 4, children: [
|
|
106
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
107
|
+
ui.Flex,
|
|
108
|
+
{
|
|
109
|
+
align: "center",
|
|
110
|
+
gap: 2,
|
|
111
|
+
as: "label",
|
|
112
|
+
htmlFor: id,
|
|
113
|
+
style: { cursor: "pointer", flexShrink: 0 },
|
|
114
|
+
children: [
|
|
115
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
116
|
+
ui.Switch,
|
|
117
|
+
{
|
|
118
|
+
id,
|
|
119
|
+
checked: enabled,
|
|
120
|
+
onChange: (e) => onToggle(e.currentTarget.checked)
|
|
121
|
+
}
|
|
122
|
+
),
|
|
123
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Label, { size: 1, children: label })
|
|
124
|
+
]
|
|
125
|
+
}
|
|
126
|
+
),
|
|
127
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Box, { style: { width: 150, opacity: enabled ? 1 : 0.35, pointerEvents: enabled ? "auto" : "none" }, children })
|
|
128
|
+
] });
|
|
129
|
+
}
|
|
130
|
+
function useContextState() {
|
|
131
|
+
return react.useSyncExternalStore(subscribeToContext, getContext, getContext);
|
|
132
|
+
}
|
|
133
|
+
function ContextPopover() {
|
|
134
|
+
const [open, setOpen] = react.useState(!1), definitions = getContextDefinitions(), state = useContextState(), triggerRef = react.useRef(null), cardRef = react.useRef(null), [coords, setCoords] = react.useState({ top: 0, right: 0 }), hasActive = Object.values(state).some((e) => e.enabled);
|
|
135
|
+
return react.useLayoutEffect(() => {
|
|
136
|
+
if (!open || !triggerRef.current) return;
|
|
137
|
+
const rect = triggerRef.current.getBoundingClientRect();
|
|
138
|
+
setCoords({ top: rect.bottom + 4, right: window.innerWidth - rect.right });
|
|
139
|
+
}, [open]), react.useEffect(() => {
|
|
140
|
+
if (!open) return;
|
|
141
|
+
const handler = (e) => {
|
|
142
|
+
const target = e.target;
|
|
143
|
+
triggerRef.current?.contains(target) || cardRef.current?.contains(target) || target.closest?.("[data-context-ui]") || setOpen(!1);
|
|
144
|
+
};
|
|
145
|
+
return document.addEventListener("mousedown", handler), () => document.removeEventListener("mousedown", handler);
|
|
146
|
+
}, [open]), /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
147
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { ref: triggerRef, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Tooltip, { content: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, children: "Sanity context" }), placement: "bottom", portal: !0, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
148
|
+
ui.Button,
|
|
149
|
+
{
|
|
150
|
+
mode: "bleed",
|
|
151
|
+
icon: ContextIcon,
|
|
152
|
+
selected: open,
|
|
153
|
+
tone: hasActive ? "primary" : "default",
|
|
154
|
+
onClick: () => setOpen((v) => !v)
|
|
155
|
+
}
|
|
156
|
+
) }) }),
|
|
157
|
+
open && /* @__PURE__ */ jsxRuntime.jsx(ui.Portal, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Layer, { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
158
|
+
"div",
|
|
159
|
+
{
|
|
160
|
+
ref: cardRef,
|
|
161
|
+
style: { position: "fixed", top: coords.top, right: coords.right },
|
|
162
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { "data-context-ui": !0, padding: 3, shadow: 2, style: { minWidth: 260 }, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 3, children: definitions.map((def) => {
|
|
163
|
+
const entry = state[def.id], currentTitle = def.options.find((o) => o.value === entry?.value)?.title ?? def.options[0]?.title;
|
|
164
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
165
|
+
ContextItem,
|
|
166
|
+
{
|
|
167
|
+
id: `sanity-context-${def.id}`,
|
|
168
|
+
label: def.title,
|
|
169
|
+
enabled: entry?.enabled ?? !1,
|
|
170
|
+
onToggle: (enabled) => setContextEntry(def.id, { enabled }),
|
|
171
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
172
|
+
ui.MenuButton,
|
|
173
|
+
{
|
|
174
|
+
button: /* @__PURE__ */ jsxRuntime.jsx(
|
|
175
|
+
ui.Button,
|
|
176
|
+
{
|
|
177
|
+
text: currentTitle,
|
|
178
|
+
fontSize: 1,
|
|
179
|
+
padding: 2,
|
|
180
|
+
mode: "ghost",
|
|
181
|
+
style: { width: "100%" }
|
|
182
|
+
}
|
|
183
|
+
),
|
|
184
|
+
id: `sanity-context-menu-${def.id}`,
|
|
185
|
+
menu: /* @__PURE__ */ jsxRuntime.jsx(ui.Menu, { "data-context-ui": !0, children: def.options.map((opt) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
186
|
+
ui.MenuItem,
|
|
187
|
+
{
|
|
188
|
+
text: opt.title,
|
|
189
|
+
icon: entry?.value === opt.value ? icons.CheckmarkIcon : void 0,
|
|
190
|
+
onClick: () => setContextEntry(def.id, { value: opt.value })
|
|
191
|
+
},
|
|
192
|
+
opt.value
|
|
193
|
+
)) }),
|
|
194
|
+
popover: { placement: "bottom-start" }
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
},
|
|
198
|
+
def.id
|
|
199
|
+
);
|
|
200
|
+
}) }) })
|
|
201
|
+
}
|
|
202
|
+
) }) })
|
|
203
|
+
] });
|
|
204
|
+
}
|
|
205
|
+
function ContextNavbar(props) {
|
|
206
|
+
const currentUser = sanity.useCurrentUser(), workspace = sanity.useWorkspace(), resolved = react.useRef(!1);
|
|
207
|
+
return react.useEffect(() => {
|
|
208
|
+
resolved.current || !currentUser || (resolveContexts({ currentUser, workspace }), resolved.current = !0);
|
|
209
|
+
}, [currentUser, workspace]), /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { style: { width: "100%" }, children: [
|
|
210
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Box, { flex: 1, style: { minWidth: 0 }, children: props.renderDefault(props) }),
|
|
211
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Card, { borderBottom: !0, style: { display: "flex", alignItems: "center", paddingRight: 8 }, children: /* @__PURE__ */ jsxRuntime.jsx(ContextPopover, {}) })
|
|
212
|
+
] });
|
|
213
|
+
}
|
|
214
|
+
function contextPlugin(config) {
|
|
215
|
+
return typeof config.contexts == "function" ? setContextsResolver(config.contexts, config.storageKey) : initContextStore(config.contexts, config.storageKey), sanity.definePlugin({
|
|
216
|
+
name: "sanity-context",
|
|
217
|
+
studio: {
|
|
218
|
+
components: { navbar: ContextNavbar }
|
|
219
|
+
}
|
|
220
|
+
})();
|
|
221
|
+
}
|
|
222
|
+
exports.ContextIcon = ContextIcon;
|
|
223
|
+
exports.contextPlugin = contextPlugin;
|
|
224
|
+
exports.getContext = getContext;
|
|
225
|
+
exports.subscribeToContext = subscribeToContext;
|
|
226
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../src/store.ts","../src/components/ContextIcon.tsx","../src/components/ContextItem.tsx","../src/components/ContextPopover.tsx","../src/components/ContextNavbar.tsx","../src/plugin.ts"],"sourcesContent":["import type {ContextDefinition, ContextEntry, ContextResolverContext, ContextsResolver, ContextState} from './types'\n\nconst DEFAULT_STORAGE_KEY = 'sanity-context'\n\nlet _definitions: ContextDefinition[] = []\nlet _storageKey = DEFAULT_STORAGE_KEY\nlet _state: ContextState = {}\nlet _resolver: ContextsResolver | null = null\nconst _listeners = new Set<() => void>()\n\nfunction defaultState(definitions: ContextDefinition[]): ContextState {\n return Object.fromEntries(definitions.map((d) => [d.id, {enabled: false, value: d.defaultValue}]))\n}\n\nfunction loadFromStorage(definitions: ContextDefinition[]): ContextState {\n if (typeof window === 'undefined') return defaultState(definitions)\n try {\n const raw = localStorage.getItem(_storageKey)\n if (!raw) return defaultState(definitions)\n const saved = JSON.parse(raw) as ContextState\n return Object.fromEntries(\n definitions.map((d) => [\n d.id,\n {\n enabled: !!saved[d.id]?.enabled,\n value: d.options.find((o) => o.value === saved[d.id]?.value)?.value ?? d.defaultValue,\n },\n ]),\n )\n } catch {\n return defaultState(definitions)\n }\n}\n\nfunction persist(): void {\n if (typeof window !== 'undefined') {\n localStorage.setItem(_storageKey, JSON.stringify(_state))\n }\n}\n\nfunction notify(): void {\n _listeners.forEach((fn) => fn())\n}\n\nexport function initContextStore(definitions: ContextDefinition[], storageKey?: string): void {\n _definitions = definitions\n _storageKey = storageKey ?? DEFAULT_STORAGE_KEY\n _state = loadFromStorage(definitions)\n notify()\n}\n\nexport function setContextsResolver(resolver: ContextsResolver, storageKey?: string): void {\n _resolver = resolver\n _storageKey = storageKey ?? DEFAULT_STORAGE_KEY\n}\n\nexport function resolveContexts(ctx: ContextResolverContext): void {\n if (!_resolver) return\n initContextStore(_resolver(ctx), _storageKey)\n}\n\nexport function getContextDefinitions(): ContextDefinition[] {\n return _definitions\n}\n\n/** @public */\nexport function getContext(): ContextState {\n return _state\n}\n\nexport function setContextEntry(id: string, patch: Partial<ContextEntry>): void {\n if (!_state[id]) return\n _state = {..._state, [id]: {..._state[id], ...patch}}\n persist()\n notify()\n}\n\n/** @public */\nexport function subscribeToContext(listener: () => void): () => void {\n _listeners.add(listener)\n return () => {\n _listeners.delete(listener)\n }\n}\n","import {forwardRef} from 'react'\n\n/** @public */\nexport const ContextIcon = forwardRef<SVGSVGElement, React.SVGProps<SVGSVGElement>>(\n function ContextIcon(_props, _ref) {\n return (\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 128 128\" xmlns=\"http://www.w3.org/2000/svg\">\n <defs>\n <linearGradient id=\"g\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0\">\n <stop offset=\"0%\" stop-color=\"#6366F1\" />\n <stop offset=\"100%\" stop-color=\"#06B6D4\" />\n </linearGradient>\n </defs>\n\n <rect\n x=\"16\"\n y=\"20\"\n width=\"32\"\n height=\"88\"\n rx=\"8\"\n fill=\"none\"\n stroke=\"#6366F1\"\n stroke-width=\"6\"\n />\n <rect\n x=\"80\"\n y=\"20\"\n width=\"32\"\n height=\"88\"\n rx=\"8\"\n fill=\"none\"\n stroke=\"#06B6D4\"\n stroke-width=\"6\"\n />\n\n <path\n d=\"M48 64 C60 40, 68 88, 80 64\"\n fill=\"none\"\n stroke=\"url(#g)\"\n stroke-width=\"6\"\n stroke-linecap=\"round\"\n />\n </svg>\n )\n },\n)\n","import {Box, Flex, Label, Switch} from '@sanity/ui'\n\ninterface Props {\n id: string\n label: string\n enabled: boolean\n onToggle: (enabled: boolean) => void\n children: React.ReactNode\n}\n\nexport function ContextItem({id, label, enabled, onToggle, children}: Props) {\n return (\n <Flex align=\"center\" justify=\"space-between\" gap={4}>\n <Flex\n align=\"center\"\n gap={2}\n as=\"label\"\n htmlFor={id}\n style={{cursor: 'pointer', flexShrink: 0}}\n >\n <Switch\n id={id}\n checked={enabled}\n onChange={(e) => onToggle(e.currentTarget.checked)}\n />\n <Label size={1}>{label}</Label>\n </Flex>\n <Box style={{width: 150, opacity: enabled ? 1 : 0.35, pointerEvents: enabled ? 'auto' : 'none'}}>\n {children}\n </Box>\n </Flex>\n )\n}\n","import {useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore} from 'react'\nimport {Button, Card, Layer, Menu, MenuButton, MenuItem, Portal, Stack, Text, Tooltip} from '@sanity/ui'\nimport {CheckmarkIcon} from '@sanity/icons'\nimport {ContextIcon} from './ContextIcon'\n\nimport {ContextItem} from './ContextItem'\nimport {getContext, getContextDefinitions, setContextEntry, subscribeToContext} from '../store'\n\nfunction useContextState() {\n return useSyncExternalStore(subscribeToContext, getContext, getContext)\n}\n\nexport function ContextPopover() {\n const [open, setOpen] = useState(false)\n const definitions = getContextDefinitions()\n const state = useContextState()\n const triggerRef = useRef<HTMLDivElement>(null)\n const cardRef = useRef<HTMLDivElement>(null)\n const [coords, setCoords] = useState({top: 0, right: 0})\n\n const hasActive = Object.values(state).some((e) => e.enabled)\n\n useLayoutEffect(() => {\n if (!open || !triggerRef.current) return\n const rect = triggerRef.current.getBoundingClientRect()\n setCoords({top: rect.bottom + 4, right: window.innerWidth - rect.right})\n }, [open])\n\n useEffect(() => {\n if (!open) return\n const handler = (e: MouseEvent) => {\n const target = e.target as Element\n if (triggerRef.current?.contains(target)) return\n if (cardRef.current?.contains(target)) return\n if (target.closest?.('[data-context-ui]')) return\n setOpen(false)\n }\n document.addEventListener('mousedown', handler)\n return () => document.removeEventListener('mousedown', handler)\n }, [open])\n\n return (\n <>\n <div ref={triggerRef}>\n <Tooltip content={<Text size={1}>Sanity context</Text>} placement=\"bottom\" portal>\n <Button\n mode=\"bleed\"\n icon={ContextIcon}\n selected={open}\n tone={hasActive ? 'primary' : 'default'}\n onClick={() => setOpen((v) => !v)}\n />\n </Tooltip>\n </div>\n {open && (\n <Portal>\n <Layer>\n <div\n ref={cardRef}\n style={{position: 'fixed', top: coords.top, right: coords.right}}\n >\n <Card data-context-ui padding={3} shadow={2} style={{minWidth: 260}}>\n <Stack space={3}>\n {definitions.map((def) => {\n const entry = state[def.id]\n const currentTitle =\n def.options.find((o) => o.value === entry?.value)?.title ??\n def.options[0]?.title\n\n return (\n <ContextItem\n key={def.id}\n id={`sanity-context-${def.id}`}\n label={def.title}\n enabled={entry?.enabled ?? false}\n onToggle={(enabled) => setContextEntry(def.id, {enabled})}\n >\n <MenuButton\n button={\n <Button\n text={currentTitle}\n fontSize={1}\n padding={2}\n mode=\"ghost\"\n style={{width: '100%'}}\n />\n }\n id={`sanity-context-menu-${def.id}`}\n menu={\n <Menu data-context-ui>\n {def.options.map((opt) => (\n <MenuItem\n key={opt.value}\n text={opt.title}\n icon={entry?.value === opt.value ? CheckmarkIcon : undefined}\n onClick={() => setContextEntry(def.id, {value: opt.value})}\n />\n ))}\n </Menu>\n }\n popover={{placement: 'bottom-start'}}\n />\n </ContextItem>\n )\n })}\n </Stack>\n </Card>\n </div>\n </Layer>\n </Portal>\n )}\n </>\n )\n}\n","import {useEffect, useRef} from 'react'\nimport {type NavbarProps, useCurrentUser, useWorkspace} from 'sanity'\nimport {Box, Card, Flex} from '@sanity/ui'\nimport {resolveContexts} from '../store'\nimport {ContextPopover} from './ContextPopover'\n\nexport function ContextNavbar(props: NavbarProps) {\n const currentUser = useCurrentUser()\n const workspace = useWorkspace()\n const resolved = useRef(false)\n\n useEffect(() => {\n if (resolved.current || !currentUser) return\n resolveContexts({currentUser, workspace})\n resolved.current = true\n }, [currentUser, workspace])\n\n return (\n <Flex style={{width: '100%'}}>\n <Box flex={1} style={{minWidth: 0}}>\n {props.renderDefault(props)}\n </Box>\n <Card borderBottom style={{display: 'flex', alignItems: 'center', paddingRight: 8}}>\n <ContextPopover />\n </Card>\n </Flex>\n )\n}\n","import {definePlugin} from 'sanity'\nimport {initContextStore, setContextsResolver} from './store'\nimport {ContextNavbar} from './components/ContextNavbar'\nimport type {ContextPluginConfig} from './types'\n\n/** @public */\nexport function contextPlugin(config: ContextPluginConfig) {\n if (typeof config.contexts === 'function') {\n setContextsResolver(config.contexts, config.storageKey)\n } else {\n initContextStore(config.contexts, config.storageKey)\n }\n\n return definePlugin({\n name: 'sanity-context',\n studio: {\n components: {navbar: ContextNavbar},\n },\n })()\n}\n"],"names":["forwardRef","jsxs","jsx","Flex","Switch","Label","Box","useSyncExternalStore","useState","useRef","useLayoutEffect","useEffect","Fragment","Tooltip","Text","Button","Portal","Layer","Card","Stack","MenuButton","Menu","MenuItem","CheckmarkIcon","useCurrentUser","useWorkspace","definePlugin"],"mappings":"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"}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { CurrentUser } from "sanity";
|
|
2
|
+
import { ForwardRefExoticComponent } from "react";
|
|
3
|
+
import { PluginOptions } from "sanity";
|
|
4
|
+
import { RefAttributes } from "react";
|
|
5
|
+
import { SVGProps } from "react";
|
|
6
|
+
import type { Workspace } from "sanity";
|
|
7
|
+
|
|
8
|
+
/** @public */
|
|
9
|
+
export declare interface ContextDefinition {
|
|
10
|
+
id: string;
|
|
11
|
+
title: string;
|
|
12
|
+
options: ContextOption[];
|
|
13
|
+
defaultValue: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** @public */
|
|
17
|
+
export declare interface ContextEntry {
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
value: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** @public */
|
|
23
|
+
export declare const ContextIcon: ForwardRefExoticComponent<
|
|
24
|
+
Omit<SVGProps<SVGSVGElement>, "ref"> & RefAttributes<SVGSVGElement>
|
|
25
|
+
>;
|
|
26
|
+
|
|
27
|
+
/** @public */
|
|
28
|
+
export declare interface ContextOption {
|
|
29
|
+
value: string;
|
|
30
|
+
title: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** @public */
|
|
34
|
+
export declare function contextPlugin(
|
|
35
|
+
config: ContextPluginConfig,
|
|
36
|
+
): PluginOptions;
|
|
37
|
+
|
|
38
|
+
/** @public */
|
|
39
|
+
export declare interface ContextPluginConfig {
|
|
40
|
+
contexts: ContextDefinition[] | ContextsResolver;
|
|
41
|
+
storageKey?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** @public */
|
|
45
|
+
export declare interface ContextResolverContext {
|
|
46
|
+
currentUser: CurrentUser | null;
|
|
47
|
+
workspace: Workspace;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** @public */
|
|
51
|
+
export declare type ContextsResolver = (
|
|
52
|
+
ctx: ContextResolverContext,
|
|
53
|
+
) => ContextDefinition[];
|
|
54
|
+
|
|
55
|
+
/** @public */
|
|
56
|
+
export declare type ContextState = Record<string, ContextEntry>;
|
|
57
|
+
|
|
58
|
+
/** @public */
|
|
59
|
+
export declare function getContext(): ContextState;
|
|
60
|
+
|
|
61
|
+
/** @public */
|
|
62
|
+
export declare function subscribeToContext(listener: () => void): () => void;
|
|
63
|
+
|
|
64
|
+
export {};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { CurrentUser } from "sanity";
|
|
2
|
+
import { ForwardRefExoticComponent } from "react";
|
|
3
|
+
import { PluginOptions } from "sanity";
|
|
4
|
+
import { RefAttributes } from "react";
|
|
5
|
+
import { SVGProps } from "react";
|
|
6
|
+
import type { Workspace } from "sanity";
|
|
7
|
+
|
|
8
|
+
/** @public */
|
|
9
|
+
export declare interface ContextDefinition {
|
|
10
|
+
id: string;
|
|
11
|
+
title: string;
|
|
12
|
+
options: ContextOption[];
|
|
13
|
+
defaultValue: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** @public */
|
|
17
|
+
export declare interface ContextEntry {
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
value: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** @public */
|
|
23
|
+
export declare const ContextIcon: ForwardRefExoticComponent<
|
|
24
|
+
Omit<SVGProps<SVGSVGElement>, "ref"> & RefAttributes<SVGSVGElement>
|
|
25
|
+
>;
|
|
26
|
+
|
|
27
|
+
/** @public */
|
|
28
|
+
export declare interface ContextOption {
|
|
29
|
+
value: string;
|
|
30
|
+
title: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** @public */
|
|
34
|
+
export declare function contextPlugin(
|
|
35
|
+
config: ContextPluginConfig,
|
|
36
|
+
): PluginOptions;
|
|
37
|
+
|
|
38
|
+
/** @public */
|
|
39
|
+
export declare interface ContextPluginConfig {
|
|
40
|
+
contexts: ContextDefinition[] | ContextsResolver;
|
|
41
|
+
storageKey?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** @public */
|
|
45
|
+
export declare interface ContextResolverContext {
|
|
46
|
+
currentUser: CurrentUser | null;
|
|
47
|
+
workspace: Workspace;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** @public */
|
|
51
|
+
export declare type ContextsResolver = (
|
|
52
|
+
ctx: ContextResolverContext,
|
|
53
|
+
) => ContextDefinition[];
|
|
54
|
+
|
|
55
|
+
/** @public */
|
|
56
|
+
export declare type ContextState = Record<string, ContextEntry>;
|
|
57
|
+
|
|
58
|
+
/** @public */
|
|
59
|
+
export declare function getContext(): ContextState;
|
|
60
|
+
|
|
61
|
+
/** @public */
|
|
62
|
+
export declare function subscribeToContext(listener: () => void): () => void;
|
|
63
|
+
|
|
64
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { useCurrentUser, useWorkspace, definePlugin } from "sanity";
|
|
2
|
+
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef, useState, useRef, useLayoutEffect, useEffect, useSyncExternalStore } from "react";
|
|
4
|
+
import { Flex, Switch, Label, Box, Tooltip, Button, Text, Portal, Layer, Card, Stack, MenuButton, Menu, MenuItem } from "@sanity/ui";
|
|
5
|
+
import { CheckmarkIcon } from "@sanity/icons";
|
|
6
|
+
const DEFAULT_STORAGE_KEY = "sanity-context";
|
|
7
|
+
let _definitions = [], _storageKey = DEFAULT_STORAGE_KEY, _state = {}, _resolver = null;
|
|
8
|
+
const _listeners = /* @__PURE__ */ new Set();
|
|
9
|
+
function defaultState(definitions) {
|
|
10
|
+
return Object.fromEntries(definitions.map((d) => [d.id, { enabled: !1, value: d.defaultValue }]));
|
|
11
|
+
}
|
|
12
|
+
function loadFromStorage(definitions) {
|
|
13
|
+
if (typeof window > "u") return defaultState(definitions);
|
|
14
|
+
try {
|
|
15
|
+
const raw = localStorage.getItem(_storageKey);
|
|
16
|
+
if (!raw) return defaultState(definitions);
|
|
17
|
+
const saved = JSON.parse(raw);
|
|
18
|
+
return Object.fromEntries(
|
|
19
|
+
definitions.map((d) => [
|
|
20
|
+
d.id,
|
|
21
|
+
{
|
|
22
|
+
enabled: !!saved[d.id]?.enabled,
|
|
23
|
+
value: d.options.find((o) => o.value === saved[d.id]?.value)?.value ?? d.defaultValue
|
|
24
|
+
}
|
|
25
|
+
])
|
|
26
|
+
);
|
|
27
|
+
} catch {
|
|
28
|
+
return defaultState(definitions);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function persist() {
|
|
32
|
+
typeof window < "u" && localStorage.setItem(_storageKey, JSON.stringify(_state));
|
|
33
|
+
}
|
|
34
|
+
function notify() {
|
|
35
|
+
_listeners.forEach((fn) => fn());
|
|
36
|
+
}
|
|
37
|
+
function initContextStore(definitions, storageKey) {
|
|
38
|
+
_definitions = definitions, _storageKey = storageKey ?? DEFAULT_STORAGE_KEY, _state = loadFromStorage(definitions), notify();
|
|
39
|
+
}
|
|
40
|
+
function setContextsResolver(resolver, storageKey) {
|
|
41
|
+
_resolver = resolver, _storageKey = storageKey ?? DEFAULT_STORAGE_KEY;
|
|
42
|
+
}
|
|
43
|
+
function resolveContexts(ctx) {
|
|
44
|
+
_resolver && initContextStore(_resolver(ctx), _storageKey);
|
|
45
|
+
}
|
|
46
|
+
function getContextDefinitions() {
|
|
47
|
+
return _definitions;
|
|
48
|
+
}
|
|
49
|
+
function getContext() {
|
|
50
|
+
return _state;
|
|
51
|
+
}
|
|
52
|
+
function setContextEntry(id, patch) {
|
|
53
|
+
_state[id] && (_state = { ..._state, [id]: { ..._state[id], ...patch } }, persist(), notify());
|
|
54
|
+
}
|
|
55
|
+
function subscribeToContext(listener) {
|
|
56
|
+
return _listeners.add(listener), () => {
|
|
57
|
+
_listeners.delete(listener);
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const ContextIcon = forwardRef(
|
|
61
|
+
function(_props, _ref) {
|
|
62
|
+
return /* @__PURE__ */ jsxs("svg", { width: "18", height: "18", viewBox: "0 0 128 128", xmlns: "http://www.w3.org/2000/svg", children: [
|
|
63
|
+
/* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs("linearGradient", { id: "g", x1: "0", y1: "0", x2: "1", y2: "0", children: [
|
|
64
|
+
/* @__PURE__ */ jsx("stop", { offset: "0%", "stop-color": "#6366F1" }),
|
|
65
|
+
/* @__PURE__ */ jsx("stop", { offset: "100%", "stop-color": "#06B6D4" })
|
|
66
|
+
] }) }),
|
|
67
|
+
/* @__PURE__ */ jsx(
|
|
68
|
+
"rect",
|
|
69
|
+
{
|
|
70
|
+
x: "16",
|
|
71
|
+
y: "20",
|
|
72
|
+
width: "32",
|
|
73
|
+
height: "88",
|
|
74
|
+
rx: "8",
|
|
75
|
+
fill: "none",
|
|
76
|
+
stroke: "#6366F1",
|
|
77
|
+
"stroke-width": "6"
|
|
78
|
+
}
|
|
79
|
+
),
|
|
80
|
+
/* @__PURE__ */ jsx(
|
|
81
|
+
"rect",
|
|
82
|
+
{
|
|
83
|
+
x: "80",
|
|
84
|
+
y: "20",
|
|
85
|
+
width: "32",
|
|
86
|
+
height: "88",
|
|
87
|
+
rx: "8",
|
|
88
|
+
fill: "none",
|
|
89
|
+
stroke: "#06B6D4",
|
|
90
|
+
"stroke-width": "6"
|
|
91
|
+
}
|
|
92
|
+
),
|
|
93
|
+
/* @__PURE__ */ jsx(
|
|
94
|
+
"path",
|
|
95
|
+
{
|
|
96
|
+
d: "M48 64 C60 40, 68 88, 80 64",
|
|
97
|
+
fill: "none",
|
|
98
|
+
stroke: "url(#g)",
|
|
99
|
+
"stroke-width": "6",
|
|
100
|
+
"stroke-linecap": "round"
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
] });
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
function ContextItem({ id, label, enabled, onToggle, children }) {
|
|
107
|
+
return /* @__PURE__ */ jsxs(Flex, { align: "center", justify: "space-between", gap: 4, children: [
|
|
108
|
+
/* @__PURE__ */ jsxs(
|
|
109
|
+
Flex,
|
|
110
|
+
{
|
|
111
|
+
align: "center",
|
|
112
|
+
gap: 2,
|
|
113
|
+
as: "label",
|
|
114
|
+
htmlFor: id,
|
|
115
|
+
style: { cursor: "pointer", flexShrink: 0 },
|
|
116
|
+
children: [
|
|
117
|
+
/* @__PURE__ */ jsx(
|
|
118
|
+
Switch,
|
|
119
|
+
{
|
|
120
|
+
id,
|
|
121
|
+
checked: enabled,
|
|
122
|
+
onChange: (e) => onToggle(e.currentTarget.checked)
|
|
123
|
+
}
|
|
124
|
+
),
|
|
125
|
+
/* @__PURE__ */ jsx(Label, { size: 1, children: label })
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
),
|
|
129
|
+
/* @__PURE__ */ jsx(Box, { style: { width: 150, opacity: enabled ? 1 : 0.35, pointerEvents: enabled ? "auto" : "none" }, children })
|
|
130
|
+
] });
|
|
131
|
+
}
|
|
132
|
+
function useContextState() {
|
|
133
|
+
return useSyncExternalStore(subscribeToContext, getContext, getContext);
|
|
134
|
+
}
|
|
135
|
+
function ContextPopover() {
|
|
136
|
+
const [open, setOpen] = useState(!1), definitions = getContextDefinitions(), state = useContextState(), triggerRef = useRef(null), cardRef = useRef(null), [coords, setCoords] = useState({ top: 0, right: 0 }), hasActive = Object.values(state).some((e) => e.enabled);
|
|
137
|
+
return useLayoutEffect(() => {
|
|
138
|
+
if (!open || !triggerRef.current) return;
|
|
139
|
+
const rect = triggerRef.current.getBoundingClientRect();
|
|
140
|
+
setCoords({ top: rect.bottom + 4, right: window.innerWidth - rect.right });
|
|
141
|
+
}, [open]), useEffect(() => {
|
|
142
|
+
if (!open) return;
|
|
143
|
+
const handler = (e) => {
|
|
144
|
+
const target = e.target;
|
|
145
|
+
triggerRef.current?.contains(target) || cardRef.current?.contains(target) || target.closest?.("[data-context-ui]") || setOpen(!1);
|
|
146
|
+
};
|
|
147
|
+
return document.addEventListener("mousedown", handler), () => document.removeEventListener("mousedown", handler);
|
|
148
|
+
}, [open]), /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
149
|
+
/* @__PURE__ */ jsx("div", { ref: triggerRef, children: /* @__PURE__ */ jsx(Tooltip, { content: /* @__PURE__ */ jsx(Text, { size: 1, children: "Sanity context" }), placement: "bottom", portal: !0, children: /* @__PURE__ */ jsx(
|
|
150
|
+
Button,
|
|
151
|
+
{
|
|
152
|
+
mode: "bleed",
|
|
153
|
+
icon: ContextIcon,
|
|
154
|
+
selected: open,
|
|
155
|
+
tone: hasActive ? "primary" : "default",
|
|
156
|
+
onClick: () => setOpen((v) => !v)
|
|
157
|
+
}
|
|
158
|
+
) }) }),
|
|
159
|
+
open && /* @__PURE__ */ jsx(Portal, { children: /* @__PURE__ */ jsx(Layer, { children: /* @__PURE__ */ jsx(
|
|
160
|
+
"div",
|
|
161
|
+
{
|
|
162
|
+
ref: cardRef,
|
|
163
|
+
style: { position: "fixed", top: coords.top, right: coords.right },
|
|
164
|
+
children: /* @__PURE__ */ jsx(Card, { "data-context-ui": !0, padding: 3, shadow: 2, style: { minWidth: 260 }, children: /* @__PURE__ */ jsx(Stack, { space: 3, children: definitions.map((def) => {
|
|
165
|
+
const entry = state[def.id], currentTitle = def.options.find((o) => o.value === entry?.value)?.title ?? def.options[0]?.title;
|
|
166
|
+
return /* @__PURE__ */ jsx(
|
|
167
|
+
ContextItem,
|
|
168
|
+
{
|
|
169
|
+
id: `sanity-context-${def.id}`,
|
|
170
|
+
label: def.title,
|
|
171
|
+
enabled: entry?.enabled ?? !1,
|
|
172
|
+
onToggle: (enabled) => setContextEntry(def.id, { enabled }),
|
|
173
|
+
children: /* @__PURE__ */ jsx(
|
|
174
|
+
MenuButton,
|
|
175
|
+
{
|
|
176
|
+
button: /* @__PURE__ */ jsx(
|
|
177
|
+
Button,
|
|
178
|
+
{
|
|
179
|
+
text: currentTitle,
|
|
180
|
+
fontSize: 1,
|
|
181
|
+
padding: 2,
|
|
182
|
+
mode: "ghost",
|
|
183
|
+
style: { width: "100%" }
|
|
184
|
+
}
|
|
185
|
+
),
|
|
186
|
+
id: `sanity-context-menu-${def.id}`,
|
|
187
|
+
menu: /* @__PURE__ */ jsx(Menu, { "data-context-ui": !0, children: def.options.map((opt) => /* @__PURE__ */ jsx(
|
|
188
|
+
MenuItem,
|
|
189
|
+
{
|
|
190
|
+
text: opt.title,
|
|
191
|
+
icon: entry?.value === opt.value ? CheckmarkIcon : void 0,
|
|
192
|
+
onClick: () => setContextEntry(def.id, { value: opt.value })
|
|
193
|
+
},
|
|
194
|
+
opt.value
|
|
195
|
+
)) }),
|
|
196
|
+
popover: { placement: "bottom-start" }
|
|
197
|
+
}
|
|
198
|
+
)
|
|
199
|
+
},
|
|
200
|
+
def.id
|
|
201
|
+
);
|
|
202
|
+
}) }) })
|
|
203
|
+
}
|
|
204
|
+
) }) })
|
|
205
|
+
] });
|
|
206
|
+
}
|
|
207
|
+
function ContextNavbar(props) {
|
|
208
|
+
const currentUser = useCurrentUser(), workspace = useWorkspace(), resolved = useRef(!1);
|
|
209
|
+
return useEffect(() => {
|
|
210
|
+
resolved.current || !currentUser || (resolveContexts({ currentUser, workspace }), resolved.current = !0);
|
|
211
|
+
}, [currentUser, workspace]), /* @__PURE__ */ jsxs(Flex, { style: { width: "100%" }, children: [
|
|
212
|
+
/* @__PURE__ */ jsx(Box, { flex: 1, style: { minWidth: 0 }, children: props.renderDefault(props) }),
|
|
213
|
+
/* @__PURE__ */ jsx(Card, { borderBottom: !0, style: { display: "flex", alignItems: "center", paddingRight: 8 }, children: /* @__PURE__ */ jsx(ContextPopover, {}) })
|
|
214
|
+
] });
|
|
215
|
+
}
|
|
216
|
+
function contextPlugin(config) {
|
|
217
|
+
return typeof config.contexts == "function" ? setContextsResolver(config.contexts, config.storageKey) : initContextStore(config.contexts, config.storageKey), definePlugin({
|
|
218
|
+
name: "sanity-context",
|
|
219
|
+
studio: {
|
|
220
|
+
components: { navbar: ContextNavbar }
|
|
221
|
+
}
|
|
222
|
+
})();
|
|
223
|
+
}
|
|
224
|
+
export {
|
|
225
|
+
ContextIcon,
|
|
226
|
+
contextPlugin,
|
|
227
|
+
getContext,
|
|
228
|
+
subscribeToContext
|
|
229
|
+
};
|
|
230
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/store.ts","../src/components/ContextIcon.tsx","../src/components/ContextItem.tsx","../src/components/ContextPopover.tsx","../src/components/ContextNavbar.tsx","../src/plugin.ts"],"sourcesContent":["import type {ContextDefinition, ContextEntry, ContextResolverContext, ContextsResolver, ContextState} from './types'\n\nconst DEFAULT_STORAGE_KEY = 'sanity-context'\n\nlet _definitions: ContextDefinition[] = []\nlet _storageKey = DEFAULT_STORAGE_KEY\nlet _state: ContextState = {}\nlet _resolver: ContextsResolver | null = null\nconst _listeners = new Set<() => void>()\n\nfunction defaultState(definitions: ContextDefinition[]): ContextState {\n return Object.fromEntries(definitions.map((d) => [d.id, {enabled: false, value: d.defaultValue}]))\n}\n\nfunction loadFromStorage(definitions: ContextDefinition[]): ContextState {\n if (typeof window === 'undefined') return defaultState(definitions)\n try {\n const raw = localStorage.getItem(_storageKey)\n if (!raw) return defaultState(definitions)\n const saved = JSON.parse(raw) as ContextState\n return Object.fromEntries(\n definitions.map((d) => [\n d.id,\n {\n enabled: !!saved[d.id]?.enabled,\n value: d.options.find((o) => o.value === saved[d.id]?.value)?.value ?? d.defaultValue,\n },\n ]),\n )\n } catch {\n return defaultState(definitions)\n }\n}\n\nfunction persist(): void {\n if (typeof window !== 'undefined') {\n localStorage.setItem(_storageKey, JSON.stringify(_state))\n }\n}\n\nfunction notify(): void {\n _listeners.forEach((fn) => fn())\n}\n\nexport function initContextStore(definitions: ContextDefinition[], storageKey?: string): void {\n _definitions = definitions\n _storageKey = storageKey ?? DEFAULT_STORAGE_KEY\n _state = loadFromStorage(definitions)\n notify()\n}\n\nexport function setContextsResolver(resolver: ContextsResolver, storageKey?: string): void {\n _resolver = resolver\n _storageKey = storageKey ?? DEFAULT_STORAGE_KEY\n}\n\nexport function resolveContexts(ctx: ContextResolverContext): void {\n if (!_resolver) return\n initContextStore(_resolver(ctx), _storageKey)\n}\n\nexport function getContextDefinitions(): ContextDefinition[] {\n return _definitions\n}\n\n/** @public */\nexport function getContext(): ContextState {\n return _state\n}\n\nexport function setContextEntry(id: string, patch: Partial<ContextEntry>): void {\n if (!_state[id]) return\n _state = {..._state, [id]: {..._state[id], ...patch}}\n persist()\n notify()\n}\n\n/** @public */\nexport function subscribeToContext(listener: () => void): () => void {\n _listeners.add(listener)\n return () => {\n _listeners.delete(listener)\n }\n}\n","import {forwardRef} from 'react'\n\n/** @public */\nexport const ContextIcon = forwardRef<SVGSVGElement, React.SVGProps<SVGSVGElement>>(\n function ContextIcon(_props, _ref) {\n return (\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 128 128\" xmlns=\"http://www.w3.org/2000/svg\">\n <defs>\n <linearGradient id=\"g\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0\">\n <stop offset=\"0%\" stop-color=\"#6366F1\" />\n <stop offset=\"100%\" stop-color=\"#06B6D4\" />\n </linearGradient>\n </defs>\n\n <rect\n x=\"16\"\n y=\"20\"\n width=\"32\"\n height=\"88\"\n rx=\"8\"\n fill=\"none\"\n stroke=\"#6366F1\"\n stroke-width=\"6\"\n />\n <rect\n x=\"80\"\n y=\"20\"\n width=\"32\"\n height=\"88\"\n rx=\"8\"\n fill=\"none\"\n stroke=\"#06B6D4\"\n stroke-width=\"6\"\n />\n\n <path\n d=\"M48 64 C60 40, 68 88, 80 64\"\n fill=\"none\"\n stroke=\"url(#g)\"\n stroke-width=\"6\"\n stroke-linecap=\"round\"\n />\n </svg>\n )\n },\n)\n","import {Box, Flex, Label, Switch} from '@sanity/ui'\n\ninterface Props {\n id: string\n label: string\n enabled: boolean\n onToggle: (enabled: boolean) => void\n children: React.ReactNode\n}\n\nexport function ContextItem({id, label, enabled, onToggle, children}: Props) {\n return (\n <Flex align=\"center\" justify=\"space-between\" gap={4}>\n <Flex\n align=\"center\"\n gap={2}\n as=\"label\"\n htmlFor={id}\n style={{cursor: 'pointer', flexShrink: 0}}\n >\n <Switch\n id={id}\n checked={enabled}\n onChange={(e) => onToggle(e.currentTarget.checked)}\n />\n <Label size={1}>{label}</Label>\n </Flex>\n <Box style={{width: 150, opacity: enabled ? 1 : 0.35, pointerEvents: enabled ? 'auto' : 'none'}}>\n {children}\n </Box>\n </Flex>\n )\n}\n","import {useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore} from 'react'\nimport {Button, Card, Layer, Menu, MenuButton, MenuItem, Portal, Stack, Text, Tooltip} from '@sanity/ui'\nimport {CheckmarkIcon} from '@sanity/icons'\nimport {ContextIcon} from './ContextIcon'\n\nimport {ContextItem} from './ContextItem'\nimport {getContext, getContextDefinitions, setContextEntry, subscribeToContext} from '../store'\n\nfunction useContextState() {\n return useSyncExternalStore(subscribeToContext, getContext, getContext)\n}\n\nexport function ContextPopover() {\n const [open, setOpen] = useState(false)\n const definitions = getContextDefinitions()\n const state = useContextState()\n const triggerRef = useRef<HTMLDivElement>(null)\n const cardRef = useRef<HTMLDivElement>(null)\n const [coords, setCoords] = useState({top: 0, right: 0})\n\n const hasActive = Object.values(state).some((e) => e.enabled)\n\n useLayoutEffect(() => {\n if (!open || !triggerRef.current) return\n const rect = triggerRef.current.getBoundingClientRect()\n setCoords({top: rect.bottom + 4, right: window.innerWidth - rect.right})\n }, [open])\n\n useEffect(() => {\n if (!open) return\n const handler = (e: MouseEvent) => {\n const target = e.target as Element\n if (triggerRef.current?.contains(target)) return\n if (cardRef.current?.contains(target)) return\n if (target.closest?.('[data-context-ui]')) return\n setOpen(false)\n }\n document.addEventListener('mousedown', handler)\n return () => document.removeEventListener('mousedown', handler)\n }, [open])\n\n return (\n <>\n <div ref={triggerRef}>\n <Tooltip content={<Text size={1}>Sanity context</Text>} placement=\"bottom\" portal>\n <Button\n mode=\"bleed\"\n icon={ContextIcon}\n selected={open}\n tone={hasActive ? 'primary' : 'default'}\n onClick={() => setOpen((v) => !v)}\n />\n </Tooltip>\n </div>\n {open && (\n <Portal>\n <Layer>\n <div\n ref={cardRef}\n style={{position: 'fixed', top: coords.top, right: coords.right}}\n >\n <Card data-context-ui padding={3} shadow={2} style={{minWidth: 260}}>\n <Stack space={3}>\n {definitions.map((def) => {\n const entry = state[def.id]\n const currentTitle =\n def.options.find((o) => o.value === entry?.value)?.title ??\n def.options[0]?.title\n\n return (\n <ContextItem\n key={def.id}\n id={`sanity-context-${def.id}`}\n label={def.title}\n enabled={entry?.enabled ?? false}\n onToggle={(enabled) => setContextEntry(def.id, {enabled})}\n >\n <MenuButton\n button={\n <Button\n text={currentTitle}\n fontSize={1}\n padding={2}\n mode=\"ghost\"\n style={{width: '100%'}}\n />\n }\n id={`sanity-context-menu-${def.id}`}\n menu={\n <Menu data-context-ui>\n {def.options.map((opt) => (\n <MenuItem\n key={opt.value}\n text={opt.title}\n icon={entry?.value === opt.value ? CheckmarkIcon : undefined}\n onClick={() => setContextEntry(def.id, {value: opt.value})}\n />\n ))}\n </Menu>\n }\n popover={{placement: 'bottom-start'}}\n />\n </ContextItem>\n )\n })}\n </Stack>\n </Card>\n </div>\n </Layer>\n </Portal>\n )}\n </>\n )\n}\n","import {useEffect, useRef} from 'react'\nimport {type NavbarProps, useCurrentUser, useWorkspace} from 'sanity'\nimport {Box, Card, Flex} from '@sanity/ui'\nimport {resolveContexts} from '../store'\nimport {ContextPopover} from './ContextPopover'\n\nexport function ContextNavbar(props: NavbarProps) {\n const currentUser = useCurrentUser()\n const workspace = useWorkspace()\n const resolved = useRef(false)\n\n useEffect(() => {\n if (resolved.current || !currentUser) return\n resolveContexts({currentUser, workspace})\n resolved.current = true\n }, [currentUser, workspace])\n\n return (\n <Flex style={{width: '100%'}}>\n <Box flex={1} style={{minWidth: 0}}>\n {props.renderDefault(props)}\n </Box>\n <Card borderBottom style={{display: 'flex', alignItems: 'center', paddingRight: 8}}>\n <ContextPopover />\n </Card>\n </Flex>\n )\n}\n","import {definePlugin} from 'sanity'\nimport {initContextStore, setContextsResolver} from './store'\nimport {ContextNavbar} from './components/ContextNavbar'\nimport type {ContextPluginConfig} from './types'\n\n/** @public */\nexport function contextPlugin(config: ContextPluginConfig) {\n if (typeof config.contexts === 'function') {\n setContextsResolver(config.contexts, config.storageKey)\n } else {\n initContextStore(config.contexts, config.storageKey)\n }\n\n return definePlugin({\n name: 'sanity-context',\n studio: {\n components: {navbar: ContextNavbar},\n },\n })()\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sanity-context",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Sanity Studio plugin for managing studio-wide context (brand, locale, market, etc.)",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"sanity",
|
|
7
|
+
"sanity-plugin"
|
|
8
|
+
],
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"author": "Tommy Ljungberg",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"sideEffects": false,
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"source": "./src/index.ts",
|
|
16
|
+
"import": "./dist/index.js",
|
|
17
|
+
"require": "./dist/index.cjs",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./package.json": "./package.json"
|
|
21
|
+
},
|
|
22
|
+
"main": "./dist/index.cjs",
|
|
23
|
+
"module": "./dist/index.js",
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"src"
|
|
28
|
+
],
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "plugin-kit verify-package --silent && pkg-utils build --strict --check --clean",
|
|
34
|
+
"link-watch": "plugin-kit link-watch",
|
|
35
|
+
"prepublishOnly": "npm run build",
|
|
36
|
+
"watch": "pkg-utils watch --strict"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@sanity/icons": ">=2",
|
|
40
|
+
"@sanity/ui": ">=2",
|
|
41
|
+
"react": "^18",
|
|
42
|
+
"sanity": "^3"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@sanity/icons": "^2",
|
|
46
|
+
"@sanity/pkg-utils": "^6",
|
|
47
|
+
"@sanity/plugin-kit": "^4",
|
|
48
|
+
"@sanity/ui": "^2",
|
|
49
|
+
"@types/react": "^18",
|
|
50
|
+
"eslint": "^9",
|
|
51
|
+
"react": "^18",
|
|
52
|
+
"sanity": "^3",
|
|
53
|
+
"typescript": "^5"
|
|
54
|
+
},
|
|
55
|
+
"sanityPlugin": {
|
|
56
|
+
"verifyPackage": {
|
|
57
|
+
"packageName": false,
|
|
58
|
+
"sanityV2Json": false,
|
|
59
|
+
"eslintImports": false
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {forwardRef} from 'react'
|
|
2
|
+
|
|
3
|
+
/** @public */
|
|
4
|
+
export const ContextIcon = forwardRef<SVGSVGElement, React.SVGProps<SVGSVGElement>>(
|
|
5
|
+
function ContextIcon(_props, _ref) {
|
|
6
|
+
return (
|
|
7
|
+
<svg width="18" height="18" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
|
|
8
|
+
<defs>
|
|
9
|
+
<linearGradient id="g" x1="0" y1="0" x2="1" y2="0">
|
|
10
|
+
<stop offset="0%" stop-color="#6366F1" />
|
|
11
|
+
<stop offset="100%" stop-color="#06B6D4" />
|
|
12
|
+
</linearGradient>
|
|
13
|
+
</defs>
|
|
14
|
+
|
|
15
|
+
<rect
|
|
16
|
+
x="16"
|
|
17
|
+
y="20"
|
|
18
|
+
width="32"
|
|
19
|
+
height="88"
|
|
20
|
+
rx="8"
|
|
21
|
+
fill="none"
|
|
22
|
+
stroke="#6366F1"
|
|
23
|
+
stroke-width="6"
|
|
24
|
+
/>
|
|
25
|
+
<rect
|
|
26
|
+
x="80"
|
|
27
|
+
y="20"
|
|
28
|
+
width="32"
|
|
29
|
+
height="88"
|
|
30
|
+
rx="8"
|
|
31
|
+
fill="none"
|
|
32
|
+
stroke="#06B6D4"
|
|
33
|
+
stroke-width="6"
|
|
34
|
+
/>
|
|
35
|
+
|
|
36
|
+
<path
|
|
37
|
+
d="M48 64 C60 40, 68 88, 80 64"
|
|
38
|
+
fill="none"
|
|
39
|
+
stroke="url(#g)"
|
|
40
|
+
stroke-width="6"
|
|
41
|
+
stroke-linecap="round"
|
|
42
|
+
/>
|
|
43
|
+
</svg>
|
|
44
|
+
)
|
|
45
|
+
},
|
|
46
|
+
)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {Box, Flex, Label, Switch} from '@sanity/ui'
|
|
2
|
+
|
|
3
|
+
interface Props {
|
|
4
|
+
id: string
|
|
5
|
+
label: string
|
|
6
|
+
enabled: boolean
|
|
7
|
+
onToggle: (enabled: boolean) => void
|
|
8
|
+
children: React.ReactNode
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function ContextItem({id, label, enabled, onToggle, children}: Props) {
|
|
12
|
+
return (
|
|
13
|
+
<Flex align="center" justify="space-between" gap={4}>
|
|
14
|
+
<Flex
|
|
15
|
+
align="center"
|
|
16
|
+
gap={2}
|
|
17
|
+
as="label"
|
|
18
|
+
htmlFor={id}
|
|
19
|
+
style={{cursor: 'pointer', flexShrink: 0}}
|
|
20
|
+
>
|
|
21
|
+
<Switch
|
|
22
|
+
id={id}
|
|
23
|
+
checked={enabled}
|
|
24
|
+
onChange={(e) => onToggle(e.currentTarget.checked)}
|
|
25
|
+
/>
|
|
26
|
+
<Label size={1}>{label}</Label>
|
|
27
|
+
</Flex>
|
|
28
|
+
<Box style={{width: 150, opacity: enabled ? 1 : 0.35, pointerEvents: enabled ? 'auto' : 'none'}}>
|
|
29
|
+
{children}
|
|
30
|
+
</Box>
|
|
31
|
+
</Flex>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {useEffect, useRef} from 'react'
|
|
2
|
+
import {type NavbarProps, useCurrentUser, useWorkspace} from 'sanity'
|
|
3
|
+
import {Box, Card, Flex} from '@sanity/ui'
|
|
4
|
+
import {resolveContexts} from '../store'
|
|
5
|
+
import {ContextPopover} from './ContextPopover'
|
|
6
|
+
|
|
7
|
+
export function ContextNavbar(props: NavbarProps) {
|
|
8
|
+
const currentUser = useCurrentUser()
|
|
9
|
+
const workspace = useWorkspace()
|
|
10
|
+
const resolved = useRef(false)
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (resolved.current || !currentUser) return
|
|
14
|
+
resolveContexts({currentUser, workspace})
|
|
15
|
+
resolved.current = true
|
|
16
|
+
}, [currentUser, workspace])
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<Flex style={{width: '100%'}}>
|
|
20
|
+
<Box flex={1} style={{minWidth: 0}}>
|
|
21
|
+
{props.renderDefault(props)}
|
|
22
|
+
</Box>
|
|
23
|
+
<Card borderBottom style={{display: 'flex', alignItems: 'center', paddingRight: 8}}>
|
|
24
|
+
<ContextPopover />
|
|
25
|
+
</Card>
|
|
26
|
+
</Flex>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore} from 'react'
|
|
2
|
+
import {Button, Card, Layer, Menu, MenuButton, MenuItem, Portal, Stack, Text, Tooltip} from '@sanity/ui'
|
|
3
|
+
import {CheckmarkIcon} from '@sanity/icons'
|
|
4
|
+
import {ContextIcon} from './ContextIcon'
|
|
5
|
+
|
|
6
|
+
import {ContextItem} from './ContextItem'
|
|
7
|
+
import {getContext, getContextDefinitions, setContextEntry, subscribeToContext} from '../store'
|
|
8
|
+
|
|
9
|
+
function useContextState() {
|
|
10
|
+
return useSyncExternalStore(subscribeToContext, getContext, getContext)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ContextPopover() {
|
|
14
|
+
const [open, setOpen] = useState(false)
|
|
15
|
+
const definitions = getContextDefinitions()
|
|
16
|
+
const state = useContextState()
|
|
17
|
+
const triggerRef = useRef<HTMLDivElement>(null)
|
|
18
|
+
const cardRef = useRef<HTMLDivElement>(null)
|
|
19
|
+
const [coords, setCoords] = useState({top: 0, right: 0})
|
|
20
|
+
|
|
21
|
+
const hasActive = Object.values(state).some((e) => e.enabled)
|
|
22
|
+
|
|
23
|
+
useLayoutEffect(() => {
|
|
24
|
+
if (!open || !triggerRef.current) return
|
|
25
|
+
const rect = triggerRef.current.getBoundingClientRect()
|
|
26
|
+
setCoords({top: rect.bottom + 4, right: window.innerWidth - rect.right})
|
|
27
|
+
}, [open])
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (!open) return
|
|
31
|
+
const handler = (e: MouseEvent) => {
|
|
32
|
+
const target = e.target as Element
|
|
33
|
+
if (triggerRef.current?.contains(target)) return
|
|
34
|
+
if (cardRef.current?.contains(target)) return
|
|
35
|
+
if (target.closest?.('[data-context-ui]')) return
|
|
36
|
+
setOpen(false)
|
|
37
|
+
}
|
|
38
|
+
document.addEventListener('mousedown', handler)
|
|
39
|
+
return () => document.removeEventListener('mousedown', handler)
|
|
40
|
+
}, [open])
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<>
|
|
44
|
+
<div ref={triggerRef}>
|
|
45
|
+
<Tooltip content={<Text size={1}>Sanity context</Text>} placement="bottom" portal>
|
|
46
|
+
<Button
|
|
47
|
+
mode="bleed"
|
|
48
|
+
icon={ContextIcon}
|
|
49
|
+
selected={open}
|
|
50
|
+
tone={hasActive ? 'primary' : 'default'}
|
|
51
|
+
onClick={() => setOpen((v) => !v)}
|
|
52
|
+
/>
|
|
53
|
+
</Tooltip>
|
|
54
|
+
</div>
|
|
55
|
+
{open && (
|
|
56
|
+
<Portal>
|
|
57
|
+
<Layer>
|
|
58
|
+
<div
|
|
59
|
+
ref={cardRef}
|
|
60
|
+
style={{position: 'fixed', top: coords.top, right: coords.right}}
|
|
61
|
+
>
|
|
62
|
+
<Card data-context-ui padding={3} shadow={2} style={{minWidth: 260}}>
|
|
63
|
+
<Stack space={3}>
|
|
64
|
+
{definitions.map((def) => {
|
|
65
|
+
const entry = state[def.id]
|
|
66
|
+
const currentTitle =
|
|
67
|
+
def.options.find((o) => o.value === entry?.value)?.title ??
|
|
68
|
+
def.options[0]?.title
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<ContextItem
|
|
72
|
+
key={def.id}
|
|
73
|
+
id={`sanity-context-${def.id}`}
|
|
74
|
+
label={def.title}
|
|
75
|
+
enabled={entry?.enabled ?? false}
|
|
76
|
+
onToggle={(enabled) => setContextEntry(def.id, {enabled})}
|
|
77
|
+
>
|
|
78
|
+
<MenuButton
|
|
79
|
+
button={
|
|
80
|
+
<Button
|
|
81
|
+
text={currentTitle}
|
|
82
|
+
fontSize={1}
|
|
83
|
+
padding={2}
|
|
84
|
+
mode="ghost"
|
|
85
|
+
style={{width: '100%'}}
|
|
86
|
+
/>
|
|
87
|
+
}
|
|
88
|
+
id={`sanity-context-menu-${def.id}`}
|
|
89
|
+
menu={
|
|
90
|
+
<Menu data-context-ui>
|
|
91
|
+
{def.options.map((opt) => (
|
|
92
|
+
<MenuItem
|
|
93
|
+
key={opt.value}
|
|
94
|
+
text={opt.title}
|
|
95
|
+
icon={entry?.value === opt.value ? CheckmarkIcon : undefined}
|
|
96
|
+
onClick={() => setContextEntry(def.id, {value: opt.value})}
|
|
97
|
+
/>
|
|
98
|
+
))}
|
|
99
|
+
</Menu>
|
|
100
|
+
}
|
|
101
|
+
popover={{placement: 'bottom-start'}}
|
|
102
|
+
/>
|
|
103
|
+
</ContextItem>
|
|
104
|
+
)
|
|
105
|
+
})}
|
|
106
|
+
</Stack>
|
|
107
|
+
</Card>
|
|
108
|
+
</div>
|
|
109
|
+
</Layer>
|
|
110
|
+
</Portal>
|
|
111
|
+
)}
|
|
112
|
+
</>
|
|
113
|
+
)
|
|
114
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export {contextPlugin} from './plugin'
|
|
2
|
+
export {getContext, subscribeToContext} from './store'
|
|
3
|
+
export {ContextIcon} from './components/ContextIcon'
|
|
4
|
+
export type {
|
|
5
|
+
ContextDefinition,
|
|
6
|
+
ContextEntry,
|
|
7
|
+
ContextOption,
|
|
8
|
+
ContextPluginConfig,
|
|
9
|
+
ContextResolverContext,
|
|
10
|
+
ContextsResolver,
|
|
11
|
+
ContextState,
|
|
12
|
+
} from './types'
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {definePlugin} from 'sanity'
|
|
2
|
+
import {initContextStore, setContextsResolver} from './store'
|
|
3
|
+
import {ContextNavbar} from './components/ContextNavbar'
|
|
4
|
+
import type {ContextPluginConfig} from './types'
|
|
5
|
+
|
|
6
|
+
/** @public */
|
|
7
|
+
export function contextPlugin(config: ContextPluginConfig) {
|
|
8
|
+
if (typeof config.contexts === 'function') {
|
|
9
|
+
setContextsResolver(config.contexts, config.storageKey)
|
|
10
|
+
} else {
|
|
11
|
+
initContextStore(config.contexts, config.storageKey)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return definePlugin({
|
|
15
|
+
name: 'sanity-context',
|
|
16
|
+
studio: {
|
|
17
|
+
components: {navbar: ContextNavbar},
|
|
18
|
+
},
|
|
19
|
+
})()
|
|
20
|
+
}
|
package/src/store.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type {ContextDefinition, ContextEntry, ContextResolverContext, ContextsResolver, ContextState} from './types'
|
|
2
|
+
|
|
3
|
+
const DEFAULT_STORAGE_KEY = 'sanity-context'
|
|
4
|
+
|
|
5
|
+
let _definitions: ContextDefinition[] = []
|
|
6
|
+
let _storageKey = DEFAULT_STORAGE_KEY
|
|
7
|
+
let _state: ContextState = {}
|
|
8
|
+
let _resolver: ContextsResolver | null = null
|
|
9
|
+
const _listeners = new Set<() => void>()
|
|
10
|
+
|
|
11
|
+
function defaultState(definitions: ContextDefinition[]): ContextState {
|
|
12
|
+
return Object.fromEntries(definitions.map((d) => [d.id, {enabled: false, value: d.defaultValue}]))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function loadFromStorage(definitions: ContextDefinition[]): ContextState {
|
|
16
|
+
if (typeof window === 'undefined') return defaultState(definitions)
|
|
17
|
+
try {
|
|
18
|
+
const raw = localStorage.getItem(_storageKey)
|
|
19
|
+
if (!raw) return defaultState(definitions)
|
|
20
|
+
const saved = JSON.parse(raw) as ContextState
|
|
21
|
+
return Object.fromEntries(
|
|
22
|
+
definitions.map((d) => [
|
|
23
|
+
d.id,
|
|
24
|
+
{
|
|
25
|
+
enabled: !!saved[d.id]?.enabled,
|
|
26
|
+
value: d.options.find((o) => o.value === saved[d.id]?.value)?.value ?? d.defaultValue,
|
|
27
|
+
},
|
|
28
|
+
]),
|
|
29
|
+
)
|
|
30
|
+
} catch {
|
|
31
|
+
return defaultState(definitions)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function persist(): void {
|
|
36
|
+
if (typeof window !== 'undefined') {
|
|
37
|
+
localStorage.setItem(_storageKey, JSON.stringify(_state))
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function notify(): void {
|
|
42
|
+
_listeners.forEach((fn) => fn())
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function initContextStore(definitions: ContextDefinition[], storageKey?: string): void {
|
|
46
|
+
_definitions = definitions
|
|
47
|
+
_storageKey = storageKey ?? DEFAULT_STORAGE_KEY
|
|
48
|
+
_state = loadFromStorage(definitions)
|
|
49
|
+
notify()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function setContextsResolver(resolver: ContextsResolver, storageKey?: string): void {
|
|
53
|
+
_resolver = resolver
|
|
54
|
+
_storageKey = storageKey ?? DEFAULT_STORAGE_KEY
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function resolveContexts(ctx: ContextResolverContext): void {
|
|
58
|
+
if (!_resolver) return
|
|
59
|
+
initContextStore(_resolver(ctx), _storageKey)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getContextDefinitions(): ContextDefinition[] {
|
|
63
|
+
return _definitions
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** @public */
|
|
67
|
+
export function getContext(): ContextState {
|
|
68
|
+
return _state
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function setContextEntry(id: string, patch: Partial<ContextEntry>): void {
|
|
72
|
+
if (!_state[id]) return
|
|
73
|
+
_state = {..._state, [id]: {..._state[id], ...patch}}
|
|
74
|
+
persist()
|
|
75
|
+
notify()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** @public */
|
|
79
|
+
export function subscribeToContext(listener: () => void): () => void {
|
|
80
|
+
_listeners.add(listener)
|
|
81
|
+
return () => {
|
|
82
|
+
_listeners.delete(listener)
|
|
83
|
+
}
|
|
84
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type {CurrentUser, Workspace} from 'sanity'
|
|
2
|
+
|
|
3
|
+
/** @public */
|
|
4
|
+
export interface ContextOption {
|
|
5
|
+
value: string
|
|
6
|
+
title: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** @public */
|
|
10
|
+
export interface ContextDefinition {
|
|
11
|
+
id: string
|
|
12
|
+
title: string
|
|
13
|
+
options: ContextOption[]
|
|
14
|
+
defaultValue: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** @public */
|
|
18
|
+
export interface ContextEntry {
|
|
19
|
+
enabled: boolean
|
|
20
|
+
value: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** @public */
|
|
24
|
+
export type ContextState = Record<string, ContextEntry>
|
|
25
|
+
|
|
26
|
+
/** @public */
|
|
27
|
+
export interface ContextResolverContext {
|
|
28
|
+
currentUser: CurrentUser | null
|
|
29
|
+
workspace: Workspace
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** @public */
|
|
33
|
+
export type ContextsResolver = (ctx: ContextResolverContext) => ContextDefinition[]
|
|
34
|
+
|
|
35
|
+
/** @public */
|
|
36
|
+
export interface ContextPluginConfig {
|
|
37
|
+
contexts: ContextDefinition[] | ContextsResolver
|
|
38
|
+
storageKey?: string
|
|
39
|
+
}
|