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 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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"}
@@ -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 {};
@@ -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
+ }