sanity-context 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +188 -0
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# sanity-context
|
|
2
|
+
|
|
3
|
+
A [Sanity Studio](https://www.sanity.io) plugin that adds a persistent context switcher to the navbar — letting editors switch between dimensions like brand, locale, market, or environment without changing the URL or dataset.
|
|
4
|
+
|
|
5
|
+
The selected context is stored in `localStorage` and made available to any part of your Studio (document lists, previews, custom tools) via a simple subscription API.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- Adds a toggle button to the Studio navbar
|
|
12
|
+
- Supports any number of named context dimensions (brand, locale, market, etc.)
|
|
13
|
+
- Each dimension has a list of options and can be independently enabled/toggled
|
|
14
|
+
- Selection persists across sessions via `localStorage`
|
|
15
|
+
- Resolver function API: derive available options from the current user's roles and workspace at runtime
|
|
16
|
+
- Subscribe to context changes from anywhere in your Studio
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npm install sanity-context
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Basic usage
|
|
25
|
+
|
|
26
|
+
Static options — all editors see the same choices:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
// sanity.config.ts
|
|
30
|
+
import {defineConfig} from 'sanity'
|
|
31
|
+
import {contextPlugin} from 'sanity-context'
|
|
32
|
+
|
|
33
|
+
export default defineConfig({
|
|
34
|
+
// ...
|
|
35
|
+
plugins: [
|
|
36
|
+
contextPlugin({
|
|
37
|
+
contexts: [
|
|
38
|
+
{
|
|
39
|
+
id: 'brand',
|
|
40
|
+
title: 'Brand',
|
|
41
|
+
options: [
|
|
42
|
+
{value: 'acme', title: 'Acme'},
|
|
43
|
+
{value: 'globex', title: 'Globex'},
|
|
44
|
+
],
|
|
45
|
+
defaultValue: 'acme',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'locale',
|
|
49
|
+
title: 'Locale',
|
|
50
|
+
options: [
|
|
51
|
+
{value: 'en-US', title: 'English (US)'},
|
|
52
|
+
{value: 'de-DE', title: 'German'},
|
|
53
|
+
{value: 'fr-FR', title: 'French'},
|
|
54
|
+
],
|
|
55
|
+
defaultValue: 'en-US',
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
}),
|
|
59
|
+
],
|
|
60
|
+
})
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Role-based options
|
|
64
|
+
|
|
65
|
+
Pass a resolver function instead of a static array to derive options from the current user and workspace. The resolver is called once after the user authenticates.
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import {contextPlugin} from 'sanity-context'
|
|
69
|
+
import type {ContextsResolver} from 'sanity-context'
|
|
70
|
+
|
|
71
|
+
const BRAND_ROLES: Record<string, string[]> = {
|
|
72
|
+
acme: ['administrator', 'editor-acme'],
|
|
73
|
+
globex: ['administrator', 'editor-globex'],
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const resolver: ContextsResolver = ({currentUser}) => {
|
|
77
|
+
const userRoles = currentUser?.roles.map((r) => r.name) ?? []
|
|
78
|
+
|
|
79
|
+
const allowedBrands = Object.entries(BRAND_ROLES)
|
|
80
|
+
.filter(([, roles]) => roles.some((r) => userRoles.includes(r)))
|
|
81
|
+
.map(([brand]) => brand)
|
|
82
|
+
|
|
83
|
+
return [
|
|
84
|
+
{
|
|
85
|
+
id: 'brand',
|
|
86
|
+
title: 'Brand',
|
|
87
|
+
options: allowedBrands.map((b) => ({value: b, title: b})),
|
|
88
|
+
defaultValue: allowedBrands[0] ?? 'acme',
|
|
89
|
+
},
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export default defineConfig({
|
|
94
|
+
plugins: [contextPlugin({contexts: resolver})],
|
|
95
|
+
})
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The resolver receives `{ currentUser, workspace }`:
|
|
99
|
+
|
|
100
|
+
| Property | Type | Description |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| `currentUser` | `CurrentUser \| null` | The authenticated Sanity user, including `roles` |
|
|
103
|
+
| `workspace` | `Workspace` | The active Studio workspace, including `name`, `dataset`, `projectId` |
|
|
104
|
+
|
|
105
|
+
## Reading context in your Studio
|
|
106
|
+
|
|
107
|
+
Use `getContext()` and `subscribeToContext()` to read the current context from document views, custom components, or structure builders.
|
|
108
|
+
|
|
109
|
+
### In a React component
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
import {useSyncExternalStore} from 'react'
|
|
113
|
+
import {getContext, subscribeToContext} from 'sanity-context'
|
|
114
|
+
|
|
115
|
+
function useStudioContext() {
|
|
116
|
+
return useSyncExternalStore(subscribeToContext, getContext, getContext)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function MyPreview() {
|
|
120
|
+
const ctx = useStudioContext()
|
|
121
|
+
|
|
122
|
+
if (!ctx.brand?.enabled) return <DefaultPreview />
|
|
123
|
+
|
|
124
|
+
return <BrandPreview brand={ctx.brand.value} locale={ctx.locale?.value} />
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### In a structure builder
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import {getContext} from 'sanity-context'
|
|
132
|
+
|
|
133
|
+
export function createStructure(S) {
|
|
134
|
+
const ctx = getContext()
|
|
135
|
+
const brand = ctx.brand?.enabled ? ctx.brand.value : null
|
|
136
|
+
|
|
137
|
+
return S.list()
|
|
138
|
+
.title('Content')
|
|
139
|
+
.items(
|
|
140
|
+
brand
|
|
141
|
+
? [S.documentTypeListItem('article').filter(`brand == "${brand}"`)]
|
|
142
|
+
: S.documentTypeListItems()
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
> Note: structure builders run once on load. For reactive filtering, read context inside document list components instead.
|
|
148
|
+
|
|
149
|
+
## `ContextState` shape
|
|
150
|
+
|
|
151
|
+
`getContext()` returns a `Record<string, ContextEntry>` keyed by context `id`:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
interface ContextEntry {
|
|
155
|
+
enabled: boolean // whether the user has switched this context on
|
|
156
|
+
value: string // the selected option value
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
A context must be **enabled** to be considered active. This lets editors see the full unfiltered Studio when the toggle is off.
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
const ctx = getContext()
|
|
164
|
+
|
|
165
|
+
const brand = ctx.brand?.enabled ? ctx.brand.value : null
|
|
166
|
+
// null → show all content
|
|
167
|
+
// 'acme' → filter to Acme brand
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Options
|
|
171
|
+
|
|
172
|
+
| Option | Type | Required | Description |
|
|
173
|
+
|---|---|---|---|
|
|
174
|
+
| `contexts` | `ContextDefinition[] \| ContextsResolver` | Yes | Static list or resolver function |
|
|
175
|
+
| `storageKey` | `string` | No | `localStorage` key (default: `"sanity-context"`) |
|
|
176
|
+
|
|
177
|
+
Each `ContextDefinition`:
|
|
178
|
+
|
|
179
|
+
| Field | Type | Description |
|
|
180
|
+
|---|---|---|
|
|
181
|
+
| `id` | `string` | Unique identifier, used as the key in `ContextState` |
|
|
182
|
+
| `title` | `string` | Label shown in the navbar UI |
|
|
183
|
+
| `options` | `ContextOption[]` | Available choices (`{value, title}`) |
|
|
184
|
+
| `defaultValue` | `string` | Selected value when no stored preference exists |
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sanity-context",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Sanity Studio plugin for managing studio-wide context (brand, locale, market, etc.)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sanity",
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
"sideEffects": false,
|
|
13
13
|
"exports": {
|
|
14
14
|
".": {
|
|
15
|
-
"source": "./src/index.ts",
|
|
16
15
|
"import": "./dist/index.js",
|
|
17
16
|
"require": "./dist/index.cjs",
|
|
17
|
+
"source": "./src/index.ts",
|
|
18
18
|
"default": "./dist/index.js"
|
|
19
19
|
},
|
|
20
20
|
"./package.json": "./package.json"
|