sanity-plugin-sgntech-interview 1.0.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/LICENSE +21 -0
- package/README.md +173 -0
- package/dist/_chunks-es/index.js +17543 -0
- package/dist/_chunks-es/index.js.map +1 -0
- package/dist/_chunks-es/refractor.js +4653 -0
- package/dist/_chunks-es/refractor.js.map +1 -0
- package/dist/_chunks-es/speakers.js +36 -0
- package/dist/_chunks-es/speakers.js.map +1 -0
- package/dist/index.d.ts +517 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/render.d.ts +382 -0
- package/dist/render.js +165 -0
- package/dist/render.js.map +1 -0
- package/package.json +97 -0
package/dist/render.d.ts
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-side entry point: the React renderer for interviews. This entry
|
|
3
|
+
* imports neither `sanity` nor `@sanity/ui`, so it can be used in a Next.js,
|
|
4
|
+
* Remix or Astro app without pulling the Studio into the bundle.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import {Interview, ensureInterviewStyles} from 'sanity-plugin-sgntech-interview/render'
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* @packageDocumentation
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { JSX } from "react";
|
|
14
|
+
import type { ReactNode } from "react";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Any object with an `_type` that is a string. Can hold any other properties.
|
|
18
|
+
* @public
|
|
19
|
+
*/
|
|
20
|
+
declare type ArbitraryTypedObject = TypedObject & {
|
|
21
|
+
[key: string]: any;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Who is asking: the block's own choice, otherwise the first speaker marked as
|
|
26
|
+
* an interviewer.
|
|
27
|
+
*
|
|
28
|
+
* @public
|
|
29
|
+
*/
|
|
30
|
+
export declare function askingSpeaker(
|
|
31
|
+
speakers: Speaker[] | undefined,
|
|
32
|
+
value: InterviewValue | undefined,
|
|
33
|
+
): Speaker | undefined;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Same as {@link interviewPortableTextComponents}, but for a custom type name
|
|
37
|
+
* or with fixed options — the speaker list and the recording URL, typically.
|
|
38
|
+
*
|
|
39
|
+
* @public
|
|
40
|
+
*/
|
|
41
|
+
export declare function createInterviewPortableTextComponents(
|
|
42
|
+
options?: RenderOptions & {
|
|
43
|
+
typeName?: string;
|
|
44
|
+
answer?: (value: InterviewValue) => React.ReactNode;
|
|
45
|
+
},
|
|
46
|
+
): {
|
|
47
|
+
types: Record<
|
|
48
|
+
string,
|
|
49
|
+
(props: { value: InterviewValue }) => React.JSX.Element | null
|
|
50
|
+
>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Adds {@link interviewStyles} to the document once.
|
|
55
|
+
*
|
|
56
|
+
* Only needed when the stylesheet is not already part of the site's own CSS.
|
|
57
|
+
*
|
|
58
|
+
* @public
|
|
59
|
+
*/
|
|
60
|
+
export declare function ensureInterviewStyles(): void;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The speaker a block refers to, or the first interviewer when it asks for one
|
|
64
|
+
* without naming it.
|
|
65
|
+
*
|
|
66
|
+
* @public
|
|
67
|
+
*/
|
|
68
|
+
export declare function findSpeaker(
|
|
69
|
+
speakers: Speaker[] | undefined,
|
|
70
|
+
id: string | undefined,
|
|
71
|
+
): Speaker | undefined;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* One question-and-answer pair.
|
|
75
|
+
*
|
|
76
|
+
* The answer's rich text is passed in as children, so the site keeps using its
|
|
77
|
+
* own Portable Text setup rather than a second one bundled here.
|
|
78
|
+
*
|
|
79
|
+
* @public
|
|
80
|
+
*/
|
|
81
|
+
export declare function Interview(props: InterviewProps): React.JSX.Element;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Portable Text block renderer for the `interviewBlock` object type.
|
|
85
|
+
*
|
|
86
|
+
* The answer is rendered with `@portabletext/react` unless the site passes its
|
|
87
|
+
* own renderer through `answer`.
|
|
88
|
+
*
|
|
89
|
+
* @public
|
|
90
|
+
*/
|
|
91
|
+
export declare function InterviewBlock(props: {
|
|
92
|
+
value: InterviewValue;
|
|
93
|
+
options?: RenderOptions & {
|
|
94
|
+
answer?: (value: InterviewValue) => React.ReactNode;
|
|
95
|
+
};
|
|
96
|
+
}): React.JSX.Element | null;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Words the renderer prints, so a site can translate them.
|
|
100
|
+
*
|
|
101
|
+
* @public
|
|
102
|
+
*/
|
|
103
|
+
export declare interface InterviewLabels {
|
|
104
|
+
translated: string;
|
|
105
|
+
originalWording: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Drop-in `components` fragment for `@portabletext/react`.
|
|
110
|
+
*
|
|
111
|
+
* @public
|
|
112
|
+
*/
|
|
113
|
+
export declare const interviewPortableTextComponents: {
|
|
114
|
+
types: {
|
|
115
|
+
interviewBlock: ({ value }: { value: InterviewValue }) => JSX.Element;
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Props for {@link Interview}.
|
|
121
|
+
*
|
|
122
|
+
* @public
|
|
123
|
+
*/
|
|
124
|
+
export declare interface InterviewProps {
|
|
125
|
+
value: InterviewValue;
|
|
126
|
+
/** The document's speaker list. */
|
|
127
|
+
speakers?: Speaker[];
|
|
128
|
+
/** Recording the timestamp points into; without it the time is plain text. */
|
|
129
|
+
source?: string;
|
|
130
|
+
labels?: Partial<InterviewLabels>;
|
|
131
|
+
/** Renders the answer's Portable Text. */
|
|
132
|
+
children?: ReactNode;
|
|
133
|
+
className?: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Structural CSS for an interview: the speaker column and the turns.
|
|
138
|
+
*
|
|
139
|
+
* No colours, no fonts. Every value worth changing is a custom property with
|
|
140
|
+
* its default at the point of use — a default declared on the block itself
|
|
141
|
+
* would beat the same variable set on an ancestor, so theming from a wrapper
|
|
142
|
+
* would silently do nothing.
|
|
143
|
+
*
|
|
144
|
+
* Set `--sgn-interview-layout: inline` on any ancestor to get the classic
|
|
145
|
+
* `NAME: text` form instead of two columns.
|
|
146
|
+
*
|
|
147
|
+
* @public
|
|
148
|
+
*/
|
|
149
|
+
export declare const interviewStyles =
|
|
150
|
+
"\n.sgn-interview {\n margin: 0;\n color: inherit;\n font: inherit;\n}\n\n.sgn-interview__turn {\n display: grid;\n grid-template-columns: var(--sgn-interview-speaker-width, 7.5rem) minmax(0, 1fr);\n gap: var(--sgn-interview-column-gap, 1.25rem);\n align-items: baseline;\n}\n\n.sgn-interview__turn + .sgn-interview__turn {\n margin-top: var(--sgn-interview-turn-gap, 0.75rem);\n}\n\n.sgn-interview__turn--answer + .sgn-interview__turn--question {\n margin-top: var(--sgn-interview-pair-gap, 1.75rem);\n}\n\n.sgn-interview__speaker {\n margin: 0;\n font-size: var(--sgn-interview-speaker-size, 0.8125em);\n font-weight: var(--sgn-interview-speaker-weight, 600);\n letter-spacing: var(--sgn-interview-speaker-tracking, 0.06em);\n text-transform: var(--sgn-interview-speaker-transform, uppercase);\n opacity: var(--sgn-interview-speaker-opacity, 0.72);\n overflow-wrap: break-word;\n}\n\n.sgn-interview__role {\n display: block;\n font-weight: 400;\n text-transform: none;\n letter-spacing: normal;\n opacity: 0.75;\n}\n\n.sgn-interview__text {\n margin: 0;\n}\n\n.sgn-interview__text > p {\n margin: 0;\n}\n\n.sgn-interview__text > p + p {\n margin-top: var(--sgn-interview-paragraph-gap, 0.75em);\n}\n\n.sgn-interview__turn--question .sgn-interview__text {\n font-weight: var(--sgn-interview-question-weight, 600);\n}\n\n.sgn-interview__meta {\n margin: var(--sgn-interview-meta-gap, 0.35rem) 0 0;\n font-size: var(--sgn-interview-meta-size, 0.8125em);\n opacity: var(--sgn-interview-meta-opacity, 0.65);\n}\n\n.sgn-interview__meta > * + *::before {\n content: ' \u00B7 ';\n}\n\n.sgn-interview__original {\n margin: var(--sgn-interview-meta-gap, 0.35rem) 0 0;\n font-size: var(--sgn-interview-meta-size, 0.8125em);\n font-style: italic;\n opacity: var(--sgn-interview-meta-opacity, 0.65);\n}\n\n/* One column once the speaker column would squeeze the text, and whenever the\n inline layout is asked for. */\n@container (max-width: 30rem) {\n .sgn-interview__turn {\n grid-template-columns: minmax(0, 1fr);\n gap: var(--sgn-interview-stacked-gap, 0.2rem);\n }\n}\n\n@media (max-width: 30rem) {\n .sgn-interview__turn {\n grid-template-columns: minmax(0, 1fr);\n gap: var(--sgn-interview-stacked-gap, 0.2rem);\n }\n}\n";
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* One question and its answer.
|
|
154
|
+
*
|
|
155
|
+
* @public
|
|
156
|
+
*/
|
|
157
|
+
export declare interface InterviewValue {
|
|
158
|
+
_key?: string;
|
|
159
|
+
_type?: string;
|
|
160
|
+
question?: string;
|
|
161
|
+
answer?: PortableTextBlock[];
|
|
162
|
+
/** Who is answering; matches {@link Speaker.id}. */
|
|
163
|
+
speaker?: string;
|
|
164
|
+
/** Who is asking, when it is not the document's default interviewer. */
|
|
165
|
+
askedBy?: string;
|
|
166
|
+
/** `12:34` or `1:02:33`, pointing into the recording. */
|
|
167
|
+
timestamp?: string;
|
|
168
|
+
/** Set when the answer was translated rather than quoted verbatim. */
|
|
169
|
+
translated?: boolean;
|
|
170
|
+
/** The answer as it was actually said. */
|
|
171
|
+
originalAnswer?: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* A Portable Text Block can be thought of as one paragraph, quote or list item.
|
|
176
|
+
* In other words, it is a container for text, that can have a visual style associated with it.
|
|
177
|
+
* The actual text value is stored in portable text spans inside of the `childen` array.
|
|
178
|
+
*
|
|
179
|
+
* @typeParam M - Mark types that be used for text spans
|
|
180
|
+
* @typeParam C - Types allowed as children of this block
|
|
181
|
+
* @typeParam S - Allowed block styles (eg `normal`, `blockquote`, `h3` etc)
|
|
182
|
+
* @typeParam L - Allowed list item types (eg `number`, `bullet` etc)
|
|
183
|
+
* @public
|
|
184
|
+
*/
|
|
185
|
+
declare interface PortableTextBlock<
|
|
186
|
+
M extends PortableTextMarkDefinition = PortableTextMarkDefinition,
|
|
187
|
+
C extends TypedObject = ArbitraryTypedObject | PortableTextSpan,
|
|
188
|
+
S extends string = PortableTextBlockStyle,
|
|
189
|
+
L extends string = PortableTextListItemType,
|
|
190
|
+
> extends TypedObject {
|
|
191
|
+
/**
|
|
192
|
+
* Type name identifying this as a portable text block.
|
|
193
|
+
* All items within a portable text array should have a `_type` property.
|
|
194
|
+
*
|
|
195
|
+
* Usually 'block', but can be customized to other values
|
|
196
|
+
*/
|
|
197
|
+
_type: "block" | (string & {});
|
|
198
|
+
/**
|
|
199
|
+
* A key that identifies this block uniquely within the parent array. Used to more easily address
|
|
200
|
+
* the block when editing collaboratively, but is also very useful for keys inside of React and
|
|
201
|
+
* other rendering frameworks that can use keys to optimize operations.
|
|
202
|
+
*/
|
|
203
|
+
_key?: string;
|
|
204
|
+
/**
|
|
205
|
+
* Array of inline items for this block. Usually contain text spans, but can be
|
|
206
|
+
* configured to include inline objects of other types as well.
|
|
207
|
+
*/
|
|
208
|
+
children: C[];
|
|
209
|
+
/**
|
|
210
|
+
* Array of mark definitions used in child text spans. By having them be on the block level,
|
|
211
|
+
* the same mark definition can be reused for multiple text spans, which is often the case
|
|
212
|
+
* with nested marks.
|
|
213
|
+
*/
|
|
214
|
+
markDefs?: M[];
|
|
215
|
+
/**
|
|
216
|
+
* Visual style of the block
|
|
217
|
+
* Common values: 'normal', 'blockquote', 'h1'...'h6'
|
|
218
|
+
*/
|
|
219
|
+
style?: S;
|
|
220
|
+
/**
|
|
221
|
+
* If this block is a list item, identifies which style of list item this is
|
|
222
|
+
* Common values: 'bullet', 'number', but can be configured
|
|
223
|
+
*/
|
|
224
|
+
listItem?: L;
|
|
225
|
+
/**
|
|
226
|
+
* If this block is a list item, identifies which level of nesting it belongs within
|
|
227
|
+
*/
|
|
228
|
+
level?: number;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* A set of _common_ (but not required/standarized) block styles
|
|
233
|
+
* @public
|
|
234
|
+
*/
|
|
235
|
+
declare type PortableTextBlockStyle =
|
|
236
|
+
| "normal"
|
|
237
|
+
| "blockquote"
|
|
238
|
+
| "h1"
|
|
239
|
+
| "h2"
|
|
240
|
+
| "h3"
|
|
241
|
+
| "h4"
|
|
242
|
+
| "h5"
|
|
243
|
+
| "h6"
|
|
244
|
+
| (string & {});
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* A set of _common_ (but not required/standardized) list item types
|
|
248
|
+
* @public
|
|
249
|
+
*/
|
|
250
|
+
declare type PortableTextListItemType = "bullet" | "number" | (string & {});
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* A mark definition holds information for marked text. For instance, a text span could reference
|
|
254
|
+
* a mark definition for a hyperlink, a geoposition, a reference to a document or anything that is
|
|
255
|
+
* representable as a JSON object.
|
|
256
|
+
* @public
|
|
257
|
+
*/
|
|
258
|
+
declare interface PortableTextMarkDefinition {
|
|
259
|
+
/**
|
|
260
|
+
* Unknown properties
|
|
261
|
+
*/
|
|
262
|
+
[key: string]: unknown;
|
|
263
|
+
/**
|
|
264
|
+
* Identifies the type of mark this is, and is used to pick the correct React components to use
|
|
265
|
+
* when rendering a text span marked with this mark type.
|
|
266
|
+
*/
|
|
267
|
+
_type: string;
|
|
268
|
+
/**
|
|
269
|
+
* Uniquely identifies this mark definition within the block
|
|
270
|
+
*/
|
|
271
|
+
_key: string;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* A Portable Text Span holds a chunk of the actual text value of a Portable Text Block
|
|
276
|
+
* @public
|
|
277
|
+
*/
|
|
278
|
+
declare interface PortableTextSpan {
|
|
279
|
+
/**
|
|
280
|
+
* Type is always `span` for portable text spans, as these don't vary in shape
|
|
281
|
+
*/
|
|
282
|
+
_type: "span";
|
|
283
|
+
/**
|
|
284
|
+
* Unique (within parent block) key for this portable text span
|
|
285
|
+
*/
|
|
286
|
+
_key?: string;
|
|
287
|
+
/**
|
|
288
|
+
* The actual text value of this text span
|
|
289
|
+
*/
|
|
290
|
+
text: string;
|
|
291
|
+
/**
|
|
292
|
+
* An array of marks this text span is annotated with, identified by its `_key`.
|
|
293
|
+
* If the key cannot be found in the parent blocks mark definition, the mark is assumed to be a
|
|
294
|
+
* decorator (a simpler mark without any properties - for instance `strong` or `em`)
|
|
295
|
+
*/
|
|
296
|
+
marks?: string[];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
declare type RenderOptions = Omit<InterviewProps, "value" | "children">;
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Someone taking part in the interview, defined once on the document.
|
|
303
|
+
*
|
|
304
|
+
* @public
|
|
305
|
+
*/
|
|
306
|
+
export declare interface Speaker {
|
|
307
|
+
_key?: string;
|
|
308
|
+
/** Short id the blocks refer to, e.g. `till`. */
|
|
309
|
+
id?: string;
|
|
310
|
+
name?: string;
|
|
311
|
+
/** Shown under the name, e.g. `Guitarist` or `Editor`. */
|
|
312
|
+
role?: string;
|
|
313
|
+
/** Marks who is asking, so a renderer can style questions differently. */
|
|
314
|
+
interviewer?: boolean;
|
|
315
|
+
/** Set when the speaker is a reference to a person document. */
|
|
316
|
+
person?: {
|
|
317
|
+
_ref?: string;
|
|
318
|
+
_type?: string;
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* What to print for a speaker, falling back through name, id and a dash so a
|
|
324
|
+
* half-filled interview still renders.
|
|
325
|
+
*
|
|
326
|
+
* @public
|
|
327
|
+
*/
|
|
328
|
+
export declare function speakerLabel(
|
|
329
|
+
speaker: Speaker | undefined,
|
|
330
|
+
fallback?: string,
|
|
331
|
+
): string;
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Turns `12:34` or `1:02:33` into seconds, for links into a recording.
|
|
335
|
+
*
|
|
336
|
+
* Returns `undefined` for anything it cannot read, so a typo becomes a plain
|
|
337
|
+
* label rather than a link to second zero.
|
|
338
|
+
*
|
|
339
|
+
* @public
|
|
340
|
+
*/
|
|
341
|
+
export declare function timestampSeconds(
|
|
342
|
+
input: string | undefined,
|
|
343
|
+
): number | undefined;
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Adds a start time to a media URL, in the form each service expects.
|
|
347
|
+
*
|
|
348
|
+
* @public
|
|
349
|
+
*/
|
|
350
|
+
export declare function timestampUrl(
|
|
351
|
+
source: string | undefined,
|
|
352
|
+
seconds: number | undefined,
|
|
353
|
+
): string | undefined;
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Any object with an `_type` property (which is required in portable text arrays),
|
|
357
|
+
* as well as a _potential_ `_key` (highly encouraged)
|
|
358
|
+
* @public
|
|
359
|
+
*/
|
|
360
|
+
declare interface TypedObject {
|
|
361
|
+
/**
|
|
362
|
+
* Identifies the type of object/span this is, and is used to pick the correct React components
|
|
363
|
+
* to use when rendering a span or inline object with this type.
|
|
364
|
+
*/
|
|
365
|
+
_type: string;
|
|
366
|
+
/**
|
|
367
|
+
* Uniquely identifies this object within its parent block.
|
|
368
|
+
* Not _required_, but highly encouraged.
|
|
369
|
+
*/
|
|
370
|
+
_key?: string;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Speakers that can actually be offered in a picker.
|
|
375
|
+
*
|
|
376
|
+
* @public
|
|
377
|
+
*/
|
|
378
|
+
export declare function usableSpeakers(
|
|
379
|
+
speakers: Speaker[] | undefined,
|
|
380
|
+
): Speaker[];
|
|
381
|
+
|
|
382
|
+
export {};
|
package/dist/render.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import { findSpeaker, askingSpeaker, timestampSeconds, timestampUrl, speakerLabel } from "./_chunks-es/speakers.js";
|
|
3
|
+
import { usableSpeakers } from "./_chunks-es/speakers.js";
|
|
4
|
+
import { PortableText } from "@portabletext/react";
|
|
5
|
+
const DEFAULT_LABELS = {
|
|
6
|
+
translated: "Translated",
|
|
7
|
+
originalWording: "Originally"
|
|
8
|
+
};
|
|
9
|
+
function Turn(props) {
|
|
10
|
+
const { kind, speaker, children, meta } = props;
|
|
11
|
+
return /* @__PURE__ */ jsxs("div", { className: `sgn-interview__turn sgn-interview__turn--${kind}`, children: [
|
|
12
|
+
/* @__PURE__ */ jsxs("p", { className: "sgn-interview__speaker", children: [
|
|
13
|
+
speakerLabel(speaker),
|
|
14
|
+
speaker?.role && /* @__PURE__ */ jsx("span", { className: "sgn-interview__role", children: speaker.role })
|
|
15
|
+
] }),
|
|
16
|
+
/* @__PURE__ */ jsxs("div", { className: "sgn-interview__text", children: [
|
|
17
|
+
children,
|
|
18
|
+
meta
|
|
19
|
+
] })
|
|
20
|
+
] });
|
|
21
|
+
}
|
|
22
|
+
function Interview(props) {
|
|
23
|
+
const { value, speakers, source, labels, children, className } = props, words = { ...DEFAULT_LABELS, ...labels }, answering = findSpeaker(speakers, value.speaker), asking = askingSpeaker(speakers, value), seconds = timestampSeconds(value.timestamp), link = timestampUrl(source, seconds), meta = value.timestamp || value.translated ? /* @__PURE__ */ jsxs("p", { className: "sgn-interview__meta", children: [
|
|
24
|
+
value.timestamp && /* @__PURE__ */ jsx("span", { children: link && seconds !== void 0 ? /* @__PURE__ */ jsx("a", { href: link, children: value.timestamp }) : value.timestamp }),
|
|
25
|
+
value.translated && /* @__PURE__ */ jsx("span", { children: words.translated })
|
|
26
|
+
] }) : void 0;
|
|
27
|
+
return /* @__PURE__ */ jsxs("div", { className: ["sgn-interview", className].filter(Boolean).join(" "), children: [
|
|
28
|
+
value.question && /* @__PURE__ */ jsx(Turn, { kind: "question", speaker: asking, children: /* @__PURE__ */ jsx("p", { children: value.question }) }),
|
|
29
|
+
/* @__PURE__ */ jsxs(Turn, { kind: "answer", speaker: answering, meta, children: [
|
|
30
|
+
children,
|
|
31
|
+
value.translated && value.originalAnswer && /* @__PURE__ */ jsxs("p", { className: "sgn-interview__original", children: [
|
|
32
|
+
words.originalWording,
|
|
33
|
+
": \u201C",
|
|
34
|
+
value.originalAnswer,
|
|
35
|
+
"\u201D"
|
|
36
|
+
] })
|
|
37
|
+
] })
|
|
38
|
+
] });
|
|
39
|
+
}
|
|
40
|
+
function InterviewBlock(props) {
|
|
41
|
+
const { value, options } = props, { answer, ...rest } = options || {};
|
|
42
|
+
return /* @__PURE__ */ jsx(Interview, { value, ...rest, children: answer ? answer(value) : /* @__PURE__ */ jsx(PortableText, { value: value.answer ?? [] }) });
|
|
43
|
+
}
|
|
44
|
+
const interviewPortableTextComponents = {
|
|
45
|
+
types: {
|
|
46
|
+
interviewBlock: ({ value }) => /* @__PURE__ */ jsx(InterviewBlock, { value })
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
function createInterviewPortableTextComponents(options) {
|
|
50
|
+
const { typeName = "interviewBlock", ...rest } = options || {};
|
|
51
|
+
return {
|
|
52
|
+
types: {
|
|
53
|
+
[typeName]: ({ value }) => /* @__PURE__ */ jsx(InterviewBlock, { value, options: rest })
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const interviewStyles = `
|
|
58
|
+
.sgn-interview {
|
|
59
|
+
margin: 0;
|
|
60
|
+
color: inherit;
|
|
61
|
+
font: inherit;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.sgn-interview__turn {
|
|
65
|
+
display: grid;
|
|
66
|
+
grid-template-columns: var(--sgn-interview-speaker-width, 7.5rem) minmax(0, 1fr);
|
|
67
|
+
gap: var(--sgn-interview-column-gap, 1.25rem);
|
|
68
|
+
align-items: baseline;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.sgn-interview__turn + .sgn-interview__turn {
|
|
72
|
+
margin-top: var(--sgn-interview-turn-gap, 0.75rem);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.sgn-interview__turn--answer + .sgn-interview__turn--question {
|
|
76
|
+
margin-top: var(--sgn-interview-pair-gap, 1.75rem);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.sgn-interview__speaker {
|
|
80
|
+
margin: 0;
|
|
81
|
+
font-size: var(--sgn-interview-speaker-size, 0.8125em);
|
|
82
|
+
font-weight: var(--sgn-interview-speaker-weight, 600);
|
|
83
|
+
letter-spacing: var(--sgn-interview-speaker-tracking, 0.06em);
|
|
84
|
+
text-transform: var(--sgn-interview-speaker-transform, uppercase);
|
|
85
|
+
opacity: var(--sgn-interview-speaker-opacity, 0.72);
|
|
86
|
+
overflow-wrap: break-word;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.sgn-interview__role {
|
|
90
|
+
display: block;
|
|
91
|
+
font-weight: 400;
|
|
92
|
+
text-transform: none;
|
|
93
|
+
letter-spacing: normal;
|
|
94
|
+
opacity: 0.75;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.sgn-interview__text {
|
|
98
|
+
margin: 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.sgn-interview__text > p {
|
|
102
|
+
margin: 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.sgn-interview__text > p + p {
|
|
106
|
+
margin-top: var(--sgn-interview-paragraph-gap, 0.75em);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.sgn-interview__turn--question .sgn-interview__text {
|
|
110
|
+
font-weight: var(--sgn-interview-question-weight, 600);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.sgn-interview__meta {
|
|
114
|
+
margin: var(--sgn-interview-meta-gap, 0.35rem) 0 0;
|
|
115
|
+
font-size: var(--sgn-interview-meta-size, 0.8125em);
|
|
116
|
+
opacity: var(--sgn-interview-meta-opacity, 0.65);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.sgn-interview__meta > * + *::before {
|
|
120
|
+
content: ' \xB7 ';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.sgn-interview__original {
|
|
124
|
+
margin: var(--sgn-interview-meta-gap, 0.35rem) 0 0;
|
|
125
|
+
font-size: var(--sgn-interview-meta-size, 0.8125em);
|
|
126
|
+
font-style: italic;
|
|
127
|
+
opacity: var(--sgn-interview-meta-opacity, 0.65);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* One column once the speaker column would squeeze the text, and whenever the
|
|
131
|
+
inline layout is asked for. */
|
|
132
|
+
@container (max-width: 30rem) {
|
|
133
|
+
.sgn-interview__turn {
|
|
134
|
+
grid-template-columns: minmax(0, 1fr);
|
|
135
|
+
gap: var(--sgn-interview-stacked-gap, 0.2rem);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@media (max-width: 30rem) {
|
|
140
|
+
.sgn-interview__turn {
|
|
141
|
+
grid-template-columns: minmax(0, 1fr);
|
|
142
|
+
gap: var(--sgn-interview-stacked-gap, 0.2rem);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
`, STYLE_ID = "sgn-interview-styles";
|
|
146
|
+
function ensureInterviewStyles() {
|
|
147
|
+
if (typeof document > "u" || document.getElementById(STYLE_ID)) return;
|
|
148
|
+
const element = document.createElement("style");
|
|
149
|
+
element.id = STYLE_ID, element.textContent = interviewStyles, document.head.appendChild(element);
|
|
150
|
+
}
|
|
151
|
+
export {
|
|
152
|
+
Interview,
|
|
153
|
+
InterviewBlock,
|
|
154
|
+
askingSpeaker,
|
|
155
|
+
createInterviewPortableTextComponents,
|
|
156
|
+
ensureInterviewStyles,
|
|
157
|
+
findSpeaker,
|
|
158
|
+
interviewPortableTextComponents,
|
|
159
|
+
interviewStyles,
|
|
160
|
+
speakerLabel,
|
|
161
|
+
timestampSeconds,
|
|
162
|
+
timestampUrl,
|
|
163
|
+
usableSpeakers
|
|
164
|
+
};
|
|
165
|
+
//# sourceMappingURL=render.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.js","sources":["../src/render/Interview.tsx","../src/render/portableText.tsx","../src/render/styles.ts"],"sourcesContent":["import type {ReactNode} from 'react'\n\nimport {askingSpeaker, findSpeaker, speakerLabel, timestampSeconds, timestampUrl} from '../lib/speakers'\nimport type {InterviewValue, Speaker} from '../lib/types'\n\n/**\n * Words the renderer prints, so a site can translate them.\n *\n * @public\n */\nexport interface InterviewLabels {\n translated: string\n originalWording: string\n}\n\nconst DEFAULT_LABELS: InterviewLabels = {\n translated: 'Translated',\n originalWording: 'Originally',\n}\n\n/**\n * Props for {@link Interview}.\n *\n * @public\n */\nexport interface InterviewProps {\n value: InterviewValue\n /** The document's speaker list. */\n speakers?: Speaker[]\n /** Recording the timestamp points into; without it the time is plain text. */\n source?: string\n labels?: Partial<InterviewLabels>\n /** Renders the answer's Portable Text. */\n children?: ReactNode\n className?: string\n}\n\nfunction Turn(props: {\n kind: 'question' | 'answer'\n speaker: Speaker | undefined\n children: ReactNode\n meta?: ReactNode\n}): React.JSX.Element {\n const {kind, speaker, children, meta} = props\n\n return (\n <div className={`sgn-interview__turn sgn-interview__turn--${kind}`}>\n <p className=\"sgn-interview__speaker\">\n {speakerLabel(speaker)}\n {speaker?.role && <span className=\"sgn-interview__role\">{speaker.role}</span>}\n </p>\n <div className=\"sgn-interview__text\">\n {children}\n {meta}\n </div>\n </div>\n )\n}\n\n/**\n * One question-and-answer pair.\n *\n * The answer's rich text is passed in as children, so the site keeps using its\n * own Portable Text setup rather than a second one bundled here.\n *\n * @public\n */\nexport function Interview(props: InterviewProps): React.JSX.Element {\n const {value, speakers, source, labels, children, className} = props\n const words = {...DEFAULT_LABELS, ...labels}\n\n const answering = findSpeaker(speakers, value.speaker)\n const asking = askingSpeaker(speakers, value)\n const seconds = timestampSeconds(value.timestamp)\n const link = timestampUrl(source, seconds)\n\n const meta =\n value.timestamp || value.translated ? (\n <p className=\"sgn-interview__meta\">\n {value.timestamp && (\n <span>\n {link && seconds !== undefined ? <a href={link}>{value.timestamp}</a> : value.timestamp}\n </span>\n )}\n {value.translated && <span>{words.translated}</span>}\n </p>\n ) : undefined\n\n return (\n <div className={['sgn-interview', className].filter(Boolean).join(' ')}>\n {value.question && (\n <Turn kind=\"question\" speaker={asking}>\n <p>{value.question}</p>\n </Turn>\n )}\n\n <Turn kind=\"answer\" speaker={answering} meta={meta}>\n {children}\n {value.translated && value.originalAnswer && (\n <p className=\"sgn-interview__original\">\n {words.originalWording}: “{value.originalAnswer}”\n </p>\n )}\n </Turn>\n </div>\n )\n}\n","import {PortableText} from '@portabletext/react'\n\nimport type {InterviewValue} from '../lib/types'\nimport {Interview, type InterviewProps} from './Interview'\n\ntype RenderOptions = Omit<InterviewProps, 'value' | 'children'>\n\n/**\n * Portable Text block renderer for the `interviewBlock` object type.\n *\n * The answer is rendered with `@portabletext/react` unless the site passes its\n * own renderer through `answer`.\n *\n * @public\n */\nexport function InterviewBlock(props: {\n value: InterviewValue\n options?: RenderOptions & {answer?: (value: InterviewValue) => React.ReactNode}\n}): React.JSX.Element | null {\n const {value, options} = props\n const {answer, ...rest} = options || {}\n\n return (\n <Interview value={value} {...rest}>\n {answer ? answer(value) : <PortableText value={value.answer ?? []} />}\n </Interview>\n )\n}\n\n/**\n * Drop-in `components` fragment for `@portabletext/react`.\n *\n * @public\n */\nexport const interviewPortableTextComponents = {\n types: {\n interviewBlock: ({value}: {value: InterviewValue}) => <InterviewBlock value={value} />,\n },\n}\n\n/**\n * Same as {@link interviewPortableTextComponents}, but for a custom type name\n * or with fixed options — the speaker list and the recording URL, typically.\n *\n * @public\n */\nexport function createInterviewPortableTextComponents(\n options?: RenderOptions & {\n typeName?: string\n answer?: (value: InterviewValue) => React.ReactNode\n },\n): {types: Record<string, (props: {value: InterviewValue}) => React.JSX.Element | null>} {\n const {typeName = 'interviewBlock', ...rest} = options || {}\n\n return {\n types: {\n [typeName]: ({value}: {value: InterviewValue}) => (\n <InterviewBlock value={value} options={rest} />\n ),\n },\n }\n}\n","/**\n * Structural CSS for an interview: the speaker column and the turns.\n *\n * No colours, no fonts. Every value worth changing is a custom property with\n * its default at the point of use — a default declared on the block itself\n * would beat the same variable set on an ancestor, so theming from a wrapper\n * would silently do nothing.\n *\n * Set `--sgn-interview-layout: inline` on any ancestor to get the classic\n * `NAME: text` form instead of two columns.\n *\n * @public\n */\nexport const interviewStyles = `\n.sgn-interview {\n margin: 0;\n color: inherit;\n font: inherit;\n}\n\n.sgn-interview__turn {\n display: grid;\n grid-template-columns: var(--sgn-interview-speaker-width, 7.5rem) minmax(0, 1fr);\n gap: var(--sgn-interview-column-gap, 1.25rem);\n align-items: baseline;\n}\n\n.sgn-interview__turn + .sgn-interview__turn {\n margin-top: var(--sgn-interview-turn-gap, 0.75rem);\n}\n\n.sgn-interview__turn--answer + .sgn-interview__turn--question {\n margin-top: var(--sgn-interview-pair-gap, 1.75rem);\n}\n\n.sgn-interview__speaker {\n margin: 0;\n font-size: var(--sgn-interview-speaker-size, 0.8125em);\n font-weight: var(--sgn-interview-speaker-weight, 600);\n letter-spacing: var(--sgn-interview-speaker-tracking, 0.06em);\n text-transform: var(--sgn-interview-speaker-transform, uppercase);\n opacity: var(--sgn-interview-speaker-opacity, 0.72);\n overflow-wrap: break-word;\n}\n\n.sgn-interview__role {\n display: block;\n font-weight: 400;\n text-transform: none;\n letter-spacing: normal;\n opacity: 0.75;\n}\n\n.sgn-interview__text {\n margin: 0;\n}\n\n.sgn-interview__text > p {\n margin: 0;\n}\n\n.sgn-interview__text > p + p {\n margin-top: var(--sgn-interview-paragraph-gap, 0.75em);\n}\n\n.sgn-interview__turn--question .sgn-interview__text {\n font-weight: var(--sgn-interview-question-weight, 600);\n}\n\n.sgn-interview__meta {\n margin: var(--sgn-interview-meta-gap, 0.35rem) 0 0;\n font-size: var(--sgn-interview-meta-size, 0.8125em);\n opacity: var(--sgn-interview-meta-opacity, 0.65);\n}\n\n.sgn-interview__meta > * + *::before {\n content: ' · ';\n}\n\n.sgn-interview__original {\n margin: var(--sgn-interview-meta-gap, 0.35rem) 0 0;\n font-size: var(--sgn-interview-meta-size, 0.8125em);\n font-style: italic;\n opacity: var(--sgn-interview-meta-opacity, 0.65);\n}\n\n/* One column once the speaker column would squeeze the text, and whenever the\n inline layout is asked for. */\n@container (max-width: 30rem) {\n .sgn-interview__turn {\n grid-template-columns: minmax(0, 1fr);\n gap: var(--sgn-interview-stacked-gap, 0.2rem);\n }\n}\n\n@media (max-width: 30rem) {\n .sgn-interview__turn {\n grid-template-columns: minmax(0, 1fr);\n gap: var(--sgn-interview-stacked-gap, 0.2rem);\n }\n}\n`\n\nconst STYLE_ID = 'sgn-interview-styles'\n\n/**\n * Adds {@link interviewStyles} to the document once.\n *\n * Only needed when the stylesheet is not already part of the site's own CSS.\n *\n * @public\n */\nexport function ensureInterviewStyles(): void {\n if (typeof document === 'undefined' || document.getElementById(STYLE_ID)) return\n\n const element = document.createElement('style')\n element.id = STYLE_ID\n element.textContent = interviewStyles\n document.head.appendChild(element)\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sanity-plugin-sgntech-interview",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Interview blocks for Sanity Studio — question and answer pairs with speakers, timestamps and a note when an answer was translated.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"sanity",
|
|
7
|
+
"sanity-plugin",
|
|
8
|
+
"interview",
|
|
9
|
+
"portable-text",
|
|
10
|
+
"editorial",
|
|
11
|
+
"qa"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/netz-sg/sanity-plugin-sgntech-interview#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/netz-sg/sanity-plugin-sgntech-interview/issues"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/netz-sg/sanity-plugin-sgntech-interview.git"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "SGNTech <kontakt@lifad.world>",
|
|
23
|
+
"type": "module",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"source": "./src/index.ts",
|
|
27
|
+
"default": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./render": {
|
|
30
|
+
"source": "./src/render.ts",
|
|
31
|
+
"default": "./dist/render.js"
|
|
32
|
+
},
|
|
33
|
+
"./package.json": "./package.json"
|
|
34
|
+
},
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"files": [
|
|
37
|
+
"dist"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "plugin-kit verify-package --silent && pkg-utils build --strict --check --clean",
|
|
41
|
+
"format": "oxfmt",
|
|
42
|
+
"link-watch": "plugin-kit link-watch",
|
|
43
|
+
"lint": "oxlint",
|
|
44
|
+
"prepublishOnly": "npm run build",
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"test:watch": "vitest",
|
|
47
|
+
"watch": "pkg-utils watch --strict"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@sanity/icons": "^5.2.1"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@sanity/browserslist-config": "^1.0.5",
|
|
54
|
+
"@sanity/pkg-utils": "^11.0.17",
|
|
55
|
+
"@sanity/plugin-kit": "^10.0.3",
|
|
56
|
+
"@types/react": "^19.2.18",
|
|
57
|
+
"oxfmt": "^0.61.0",
|
|
58
|
+
"oxlint": "^1.77.0",
|
|
59
|
+
"oxlint-tsgolint": "^7.0.2001",
|
|
60
|
+
"react": "^19.2.8",
|
|
61
|
+
"react-dom": "^19.2.8",
|
|
62
|
+
"sanity": "^6.9.0",
|
|
63
|
+
"styled-components": "^6.5.0",
|
|
64
|
+
"typescript": "^6.0.3",
|
|
65
|
+
"vitest": "^3.2.4",
|
|
66
|
+
"@portabletext/react": "^7.0.1",
|
|
67
|
+
"@portabletext/types": "^4.0.2"
|
|
68
|
+
},
|
|
69
|
+
"peerDependencies": {
|
|
70
|
+
"react": "^18 || ^19",
|
|
71
|
+
"sanity": "^5 || ^6.0.0-0",
|
|
72
|
+
"@portabletext/react": "^3 || ^4 || ^5 || ^6 || ^7"
|
|
73
|
+
},
|
|
74
|
+
"engines": {
|
|
75
|
+
"node": ">=20.19 <22 || >=22.12"
|
|
76
|
+
},
|
|
77
|
+
"browserslist": "extends @sanity/browserslist-config",
|
|
78
|
+
"publishConfig": {
|
|
79
|
+
"exports": {
|
|
80
|
+
".": {
|
|
81
|
+
"default": "./dist/index.js"
|
|
82
|
+
},
|
|
83
|
+
"./render": {
|
|
84
|
+
"default": "./dist/render.js"
|
|
85
|
+
},
|
|
86
|
+
"./package.json": "./package.json"
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
"allowScripts": {
|
|
90
|
+
"esbuild@0.28.1": true
|
|
91
|
+
},
|
|
92
|
+
"peerDependenciesMeta": {
|
|
93
|
+
"@portabletext/react": {
|
|
94
|
+
"optional": true
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|