@specbook/ui 0.0.0-stage → 0.2.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.
Files changed (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -2
  3. package/dist-viewer/THIRD-PARTY-NOTICES.txt +581 -0
  4. package/dist-viewer/apple-touch-icon.png +0 -0
  5. package/dist-viewer/assets/Host-CSrM9wY1.js +37 -0
  6. package/dist-viewer/assets/faker-B3c03STC.js +11 -0
  7. package/dist-viewer/assets/index-Ds6DZpGP.css +1 -0
  8. package/dist-viewer/assets/index-ez9c20v3.js +1 -0
  9. package/dist-viewer/assets/main-38QGGpNT.js +14 -0
  10. package/dist-viewer/assets/mount-CGL2tOpN.js +2 -0
  11. package/dist-viewer/assets/panel-C-vBZrep.css +1 -0
  12. package/dist-viewer/assets/panel-C8Ve3LeS.js +1 -0
  13. package/dist-viewer/assets/registry-ChKCFBt1.js +2 -0
  14. package/dist-viewer/assets/worker-ClGb0tE1.js +181 -0
  15. package/dist-viewer/favicon.svg +1 -0
  16. package/dist-viewer/index.html +19 -0
  17. package/dist-viewer/specbook.js +1 -0
  18. package/index.html +12 -0
  19. package/package.json +61 -3
  20. package/src/app.tsx +368 -0
  21. package/src/assets/mark.svg +1 -0
  22. package/src/badge.ts +14 -0
  23. package/src/branding.ts +92 -0
  24. package/src/components/Catalog.tsx +324 -0
  25. package/src/components/EnvBadge.tsx +48 -0
  26. package/src/components/EnvironmentMenu.tsx +262 -0
  27. package/src/components/Fields.tsx +220 -0
  28. package/src/components/GitHubStar.tsx +22 -0
  29. package/src/components/ListResizer.tsx +70 -0
  30. package/src/components/ModeHint.tsx +17 -0
  31. package/src/components/Palette.tsx +85 -0
  32. package/src/components/PoweredBy.tsx +15 -0
  33. package/src/components/ShareMenu.tsx +94 -0
  34. package/src/components/ThemeToggle.tsx +52 -0
  35. package/src/embed/panel.tsx +115 -0
  36. package/src/embed/styles.ts +27 -0
  37. package/src/embed.ts +65 -0
  38. package/src/engine.ts +574 -0
  39. package/src/extension.ts +101 -0
  40. package/src/fielddoc.ts +83 -0
  41. package/src/main.tsx +24 -0
  42. package/src/markdown.ts +133 -0
  43. package/src/nav.ts +118 -0
  44. package/src/oauth-callback.ts +87 -0
  45. package/src/oauth.ts +194 -0
  46. package/src/panel/Body.tsx +182 -0
  47. package/src/panel/BodyEditor.tsx +92 -0
  48. package/src/panel/CodeSample.tsx +238 -0
  49. package/src/panel/Combo.tsx +124 -0
  50. package/src/panel/ControlsGrid.tsx +189 -0
  51. package/src/panel/FieldDoc.tsx +42 -0
  52. package/src/panel/FieldTable.tsx +113 -0
  53. package/src/panel/Host.tsx +100 -0
  54. package/src/panel/OAuthSignIn.tsx +163 -0
  55. package/src/panel/OperationDocs.tsx +181 -0
  56. package/src/panel/Panel.tsx +197 -0
  57. package/src/panel/ParamGrid.tsx +396 -0
  58. package/src/panel/RequestPanel.tsx +627 -0
  59. package/src/panel/RequestView.tsx +108 -0
  60. package/src/panel/ResponseView.tsx +182 -0
  61. package/src/panel/SecretsBar.tsx +189 -0
  62. package/src/panel/SequenceDocs.tsx +132 -0
  63. package/src/panel/SequencePanel.tsx +247 -0
  64. package/src/panel/StepExchange.tsx +118 -0
  65. package/src/panel/completions.ts +114 -0
  66. package/src/panel/draft.ts +106 -0
  67. package/src/panel/fieldnotes-split.ts +55 -0
  68. package/src/panel/fieldnotes.ts +40 -0
  69. package/src/panel/graphql.ts +31 -0
  70. package/src/panel/icons.tsx +85 -0
  71. package/src/panel/jsonCursor.ts +115 -0
  72. package/src/panel/stepnotes-split.ts +38 -0
  73. package/src/panel/stepnotes.ts +58 -0
  74. package/src/routing.ts +120 -0
  75. package/src/runtime.ts +32 -0
  76. package/src/schema.ts +359 -0
  77. package/src/secrets.ts +61 -0
  78. package/src/status.ts +27 -0
  79. package/src/store.ts +12 -0
  80. package/src/styles/app.css +821 -0
  81. package/src/styles/panel.css +1939 -0
  82. package/src/styles/tokens.css +122 -0
  83. package/src/theme.ts +37 -0
  84. package/src/viewer/Opening.tsx +43 -0
  85. package/src/viewer/bundles.ts +74 -0
  86. package/src/viewer/client.ts +54 -0
  87. package/src/viewer/embed.ts +83 -0
  88. package/src/viewer/faker-en.ts +3 -0
  89. package/src/viewer/main.tsx +289 -0
  90. package/src/viewer/mount.ts +52 -0
  91. package/src/viewer/protocol.ts +54 -0
  92. package/src/viewer/recent.ts +22 -0
  93. package/src/viewer/registry-data.ts +8 -0
  94. package/src/viewer/registry.ts +163 -0
  95. package/src/viewer/share.ts +134 -0
  96. package/src/viewer/site.ts +3 -0
  97. package/src/viewer/uploads.ts +53 -0
  98. package/src/viewer/viewer.css +184 -0
  99. package/src/viewer/worker-factory.ts +14 -0
  100. package/src/viewer/worker.ts +176 -0
  101. package/src/virtual.d.ts +104 -0
@@ -0,0 +1,124 @@
1
+ import { useEffect, useRef, useState } from 'preact/hooks'
2
+
3
+ interface Props {
4
+ value: string
5
+ options: string[]
6
+ onChange: (value: string) => void
7
+ label: string
8
+ placeholder?: string
9
+ /** forwarded so the grid can move between rows with Enter and the arrow keys */
10
+ onKeyDown?: (e: KeyboardEvent) => void
11
+ cell?: string
12
+ }
13
+
14
+ /** A text input with the spec's allowed values as a dropdown; any other value can still be typed. */
15
+ export function Combo({ value, options, onChange, label, placeholder, onKeyDown, cell }: Props) {
16
+ const [open, setOpen] = useState(false)
17
+ const [at, setAt] = useState(-1)
18
+ const [pos, setPos] = useState<Record<string, string>>()
19
+ const box = useRef<HTMLDivElement>(null)
20
+
21
+ /** The list is `fixed` so the grid's rounded, clipped table and the scrolling pane can't cut it off. */
22
+ const place = () => {
23
+ const r = box.current?.getBoundingClientRect()
24
+ if (!r) return
25
+ const below = innerHeight - r.bottom - 8
26
+ const flip = below < 160 && r.top > below
27
+ // strings with units: Preact 11 sets style properties as given
28
+ setPos({
29
+ left: `${r.left}px`,
30
+ width: `${Math.max(r.width, 160)}px`,
31
+ maxHeight: `${Math.min(240, (flip ? r.top : below) - 4)}px`,
32
+ ...(flip ? { bottom: `${innerHeight - r.top + 2}px` } : { top: `${r.bottom + 2}px` }),
33
+ })
34
+ }
35
+
36
+ useEffect(() => {
37
+ if (!open) return
38
+ place()
39
+ const close = (e: Event) => {
40
+ if (!(e.target instanceof Node && box.current?.contains(e.target))) setOpen(false)
41
+ }
42
+ addEventListener('scroll', close, true)
43
+ addEventListener('resize', close)
44
+ return () => {
45
+ removeEventListener('scroll', close, true)
46
+ removeEventListener('resize', close)
47
+ }
48
+ }, [open])
49
+
50
+ useEffect(() => {
51
+ if (!open) return
52
+ const away = (e: MouseEvent) => {
53
+ if (!e.composedPath().includes(box.current as Node)) setOpen(false)
54
+ }
55
+ document.addEventListener('mousedown', away, true)
56
+ return () => document.removeEventListener('mousedown', away, true)
57
+ }, [open])
58
+
59
+ const pick = (v: string) => {
60
+ onChange(v)
61
+ setOpen(false)
62
+ setAt(-1)
63
+ }
64
+
65
+ return (
66
+ <div class="combo" ref={box}>
67
+ <input
68
+ class="cell"
69
+ data-cell={cell}
70
+ value={value}
71
+ placeholder={placeholder}
72
+ aria-label={label}
73
+ aria-expanded={open}
74
+ role="combobox"
75
+ autocomplete="off"
76
+ onFocus={() => setOpen(true)}
77
+ onInput={(e) => onChange((e.target as HTMLInputElement).value)}
78
+ onKeyDown={(e) => {
79
+ if (open && e.key === 'ArrowDown') {
80
+ e.preventDefault()
81
+ setAt((at + 1) % options.length)
82
+ } else if (open && e.key === 'ArrowUp') {
83
+ e.preventDefault()
84
+ setAt((at - 1 + options.length) % options.length)
85
+ } else if (open && e.key === 'Enter' && at >= 0) {
86
+ e.preventDefault()
87
+ pick(options[at] as string)
88
+ } else if (e.key === 'Escape' && open) {
89
+ e.stopPropagation()
90
+ setOpen(false)
91
+ } else {
92
+ if (e.key === 'Enter' || e.key === 'Tab') setOpen(false)
93
+ onKeyDown?.(e)
94
+ }
95
+ }}
96
+ />
97
+ <button
98
+ type="button"
99
+ class="combo-toggle"
100
+ tabIndex={-1}
101
+ aria-label={`Show ${label} options`}
102
+ onClick={() => setOpen(!open)}
103
+ >
104
+ ▾
105
+ </button>
106
+ {open && (
107
+ <div class="combo-list" role="listbox" style={pos}>
108
+ {options.map((o, i) => (
109
+ <button
110
+ type="button"
111
+ role="option"
112
+ aria-selected={o === value}
113
+ class={`${o === value ? 'on' : ''} ${i === at ? 'at' : ''}`}
114
+ onMouseDown={(e) => e.preventDefault()}
115
+ onClick={() => pick(o)}
116
+ >
117
+ {o}
118
+ </button>
119
+ ))}
120
+ </div>
121
+ )}
122
+ </div>
123
+ )
124
+ }
@@ -0,0 +1,189 @@
1
+ import { useState } from 'preact/hooks'
2
+ import { defaultFor, type FieldSpec } from '../components/Fields'
3
+ import { Combo } from './Combo'
4
+
5
+ interface Props {
6
+ title: string
7
+ specs: Record<string, FieldSpec>
8
+ values: Record<string, unknown>
9
+ onChange: (values: Record<string, unknown>) => void
10
+ }
11
+
12
+ const same = (a: unknown, b: unknown) => a === b || JSON.stringify(a) === JSON.stringify(b)
13
+
14
+ function hint(spec: FieldSpec): string {
15
+ const bits: string[] = [spec.kind]
16
+ if (spec.min !== undefined || spec.max !== undefined)
17
+ bits.push(`${spec.min ?? ''}–${spec.max ?? ''}`)
18
+ return bits.join(' · ')
19
+ }
20
+
21
+ /** One editable cell, by control kind. Nested objects and arrays are edited as JSON. */
22
+ function ValueCell({
23
+ name,
24
+ spec,
25
+ value,
26
+ onChange,
27
+ }: {
28
+ name: string
29
+ spec: FieldSpec
30
+ value: unknown
31
+ onChange: (v: unknown) => void
32
+ }) {
33
+ const [raw, setRaw] = useState<string>()
34
+ switch (spec.kind) {
35
+ case 'number':
36
+ return (
37
+ <input
38
+ class="cell"
39
+ type="number"
40
+ aria-label={name}
41
+ value={String(value ?? '')}
42
+ min={spec.min}
43
+ max={spec.max}
44
+ onInput={(e) => {
45
+ const v = (e.target as HTMLInputElement).value
46
+ onChange(v === '' ? undefined : Number(v))
47
+ }}
48
+ />
49
+ )
50
+ case 'boolean':
51
+ return (
52
+ <Combo
53
+ label={name}
54
+ value={String(Boolean(value))}
55
+ options={['true', 'false']}
56
+ onChange={(v) => (v === 'true' || v === 'false' ? onChange(v === 'true') : undefined)}
57
+ />
58
+ )
59
+ case 'date':
60
+ return (
61
+ <input
62
+ class="cell"
63
+ type="date"
64
+ aria-label={name}
65
+ value={String(value ?? '')}
66
+ onInput={(e) => onChange((e.target as HTMLInputElement).value)}
67
+ />
68
+ )
69
+ case 'select': {
70
+ const options = spec.options ?? []
71
+ const current = options.find((o) => same(o.value, value))
72
+ return (
73
+ <Combo
74
+ label={name}
75
+ value={current?.label ?? String(value ?? '')}
76
+ options={options.map((o) => o.label)}
77
+ onChange={(label) => {
78
+ const hit = options.find((o) => o.label === label)
79
+ if (hit) onChange(hit.value)
80
+ }}
81
+ />
82
+ )
83
+ }
84
+ case 'multiselect': {
85
+ const chosen = (value as unknown[]) ?? []
86
+ return (
87
+ <div class="multi-cell">
88
+ {(spec.options ?? []).map((o) => (
89
+ <label>
90
+ <input
91
+ type="checkbox"
92
+ checked={chosen.some((c) => same(c, o.value))}
93
+ onChange={(e) =>
94
+ onChange(
95
+ (e.target as HTMLInputElement).checked
96
+ ? [...chosen, o.value]
97
+ : chosen.filter((c) => !same(c, o.value)),
98
+ )
99
+ }
100
+ />
101
+ {o.label}
102
+ </label>
103
+ ))}
104
+ </div>
105
+ )
106
+ }
107
+ case 'object':
108
+ case 'array':
109
+ return (
110
+ <input
111
+ class="cell mono"
112
+ aria-label={name}
113
+ value={raw ?? JSON.stringify(value ?? defaultFor(spec))}
114
+ onInput={(e) => {
115
+ const text = (e.target as HTMLInputElement).value
116
+ setRaw(text)
117
+ try {
118
+ onChange(JSON.parse(text))
119
+ } catch {
120
+ /* keep typing; the last valid value stays */
121
+ }
122
+ }}
123
+ onBlur={() => setRaw(undefined)}
124
+ />
125
+ )
126
+ default:
127
+ return (
128
+ <input
129
+ class="cell"
130
+ aria-label={name}
131
+ value={String(value ?? '')}
132
+ onInput={(e) => onChange((e.target as HTMLInputElement).value)}
133
+ />
134
+ )
135
+ }
136
+ }
137
+
138
+ /**
139
+ * A sequence's controls (params, or the choices of an input step) as the same spreadsheet the request
140
+ * parameters use: name, value, description.
141
+ */
142
+ export function ControlsGrid({ title, specs, values, onChange }: Props) {
143
+ const names = Object.keys(specs)
144
+ if (names.length === 0) return null
145
+ return (
146
+ <section class="grid controls-grid" aria-label={title}>
147
+ <h3>{title}</h3>
148
+ <table>
149
+ <colgroup>
150
+ <col class="c-key" />
151
+ <col class="c-val" />
152
+ <col />
153
+ </colgroup>
154
+ <thead>
155
+ <tr>
156
+ <th class="g-key">Name</th>
157
+ <th class="g-val">Value</th>
158
+ <th class="g-doc">Description</th>
159
+ </tr>
160
+ </thead>
161
+ <tbody>
162
+ {names.map((name) => {
163
+ const spec = specs[name] as FieldSpec
164
+ return (
165
+ <tr key={name}>
166
+ <td class="g-key">
167
+ <span class="key">
168
+ <code>{spec.label ?? name}</code>
169
+ </span>
170
+ </td>
171
+ <td class="g-val">
172
+ <ValueCell
173
+ name={name}
174
+ spec={spec}
175
+ value={values[name]}
176
+ onChange={(v) => onChange({ ...values, [name]: v })}
177
+ />
178
+ </td>
179
+ <td class="g-doc" title={spec.description}>
180
+ <span class="chip">{hint(spec)}</span> {spec.description}
181
+ </td>
182
+ </tr>
183
+ )
184
+ })}
185
+ </tbody>
186
+ </table>
187
+ </section>
188
+ )
189
+ }
@@ -0,0 +1,42 @@
1
+ import type { FieldDoc as Doc } from '../fielddoc'
2
+ import { markdownToHtml } from '../markdown'
3
+
4
+ /** The documentation for the field the cursor is on, shown below the editor instead of beside each line. */
5
+ export function FieldDoc({ doc, hint }: { doc?: Doc; hint?: string }) {
6
+ if (!doc)
7
+ return (
8
+ <div class="field-doc empty" part="field-doc" aria-live="polite">
9
+ {hint ?? 'Put the cursor on a field to see what the API says about it.'}
10
+ </div>
11
+ )
12
+ return (
13
+ <div class="field-doc" part="field-doc" aria-live="polite">
14
+ <div class="field-doc-head">
15
+ <code>{doc.path}</code>
16
+ <span class="chip">{doc.type}</span>
17
+ {doc.required && <span class="chip req">required</span>}
18
+ {doc.deprecated && <span class="chip warn">deprecated</span>}
19
+ </div>
20
+ {doc.description ? (
21
+ <div
22
+ class="field-doc-text"
23
+ dangerouslySetInnerHTML={{ __html: markdownToHtml(doc.description) }}
24
+ />
25
+ ) : doc.note ? null : (
26
+ <p class="muted">
27
+ The spec has no description for this field.
28
+ {doc.parent && <> It is part of: {doc.parent}</>}
29
+ </p>
30
+ )}
31
+ {doc.note && <div class="field-note" dangerouslySetInnerHTML={{ __html: doc.note }} />}
32
+ {doc.enum && (
33
+ <p class="field-doc-enum">
34
+ {doc.enum.slice(0, 12).map((v) => (
35
+ <code>{v}</code>
36
+ ))}
37
+ {doc.enum.length > 12 && <span class="muted">+{doc.enum.length - 12} more</span>}
38
+ </p>
39
+ )}
40
+ </div>
41
+ )
42
+ }
@@ -0,0 +1,113 @@
1
+ import { inlineSummary } from '../markdown'
2
+ import { type Defs, describe, type JsonSchema, kindOf, propsOf, unwrap } from '../schema'
3
+ import { type FieldSection, noteKey } from './fieldnotes-split'
4
+
5
+ interface Row {
6
+ name: string
7
+ depth: number
8
+ type: string
9
+ required: boolean
10
+ description?: string
11
+ enum?: string[]
12
+ }
13
+
14
+ const MAX_ROWS = 60
15
+
16
+ function typeOf(u: JsonSchema, defs: Defs): string {
17
+ const kind = kindOf(u)
18
+ if (kind === 'array') return `${kindOf(unwrap(u.items, defs))}[]`
19
+ return u.format ? `${kind} · ${u.format}` : kind
20
+ }
21
+
22
+ export function flatten(schema: JsonSchema | undefined, defs: Defs, depth = 0, prefix = ''): Row[] {
23
+ if (!schema || depth > 2) return []
24
+ const rows: Row[] = []
25
+ for (const p of propsOf(schema, defs)) {
26
+ const u = unwrap(p.schema, defs)
27
+ const name = prefix ? `${prefix}.${p.name}` : p.name
28
+ rows.push({
29
+ name,
30
+ depth,
31
+ type: typeOf(u, defs),
32
+ required: p.required,
33
+ description: describe(u),
34
+ enum: Array.isArray(u.enum) && u.enum.length <= 10 ? u.enum.map(String) : undefined,
35
+ })
36
+ const nested = kindOf(u) === 'array' ? unwrap(u.items, defs) : u
37
+ if (kindOf(nested) === 'object') rows.push(...flatten(nested, defs, depth + 1, name))
38
+ }
39
+ return rows
40
+ }
41
+
42
+ export function FieldTable({
43
+ schema,
44
+ defs,
45
+ section = 'data',
46
+ notes,
47
+ }: {
48
+ schema?: JsonSchema
49
+ defs: Defs
50
+ /** where these fields sit in the request, for looking up the project's notes */
51
+ section?: FieldSection
52
+ notes?: Map<string, string>
53
+ }) {
54
+ const rows = flatten(schema, defs)
55
+ if (rows.length === 0) return null
56
+ return (
57
+ <table class="fields">
58
+ <thead>
59
+ <tr>
60
+ <th>Field</th>
61
+ <th>Type</th>
62
+ <th>Description</th>
63
+ </tr>
64
+ </thead>
65
+ <tbody>
66
+ {rows.slice(0, MAX_ROWS).map((r) => (
67
+ <tr>
68
+ <td style={{ paddingLeft: `${r.depth * 16 + 8}px` }}>
69
+ <code>{r.name.split('.').pop()}</code>
70
+ {r.required && (
71
+ <span class="req" title="required">
72
+ *
73
+ </span>
74
+ )}
75
+ </td>
76
+ <td>
77
+ <span class="chip">{r.type}</span>
78
+ </td>
79
+ <td>
80
+ <span
81
+ class="doc-inline wrap"
82
+ dangerouslySetInnerHTML={{ __html: inlineSummary(r.description, 220) }}
83
+ />
84
+ {notes?.get(noteKey(section, r.name)) && (
85
+ <div
86
+ class="field-note"
87
+ dangerouslySetInnerHTML={{
88
+ __html: notes.get(noteKey(section, r.name)) as string,
89
+ }}
90
+ />
91
+ )}
92
+ {r.enum && (
93
+ <span class="field-doc-enum">
94
+ {' '}
95
+ {r.enum.map((v) => (
96
+ <code>{v}</code>
97
+ ))}
98
+ </span>
99
+ )}
100
+ </td>
101
+ </tr>
102
+ ))}
103
+ {rows.length > MAX_ROWS && (
104
+ <tr>
105
+ <td colSpan={3} class="muted">
106
+ +{rows.length - MAX_ROWS} more fields in the spec
107
+ </td>
108
+ </tr>
109
+ )}
110
+ </tbody>
111
+ </table>
112
+ )
113
+ }
@@ -0,0 +1,100 @@
1
+ import type { SequenceInfo, Variant } from '../engine'
2
+ import type { PanelTab } from '../nav'
3
+ import { OperationDocs } from './OperationDocs'
4
+ import { Panel } from './Panel'
5
+ import { type OpInfo, RequestPanel } from './RequestPanel'
6
+ import { SequenceDocs } from './SequenceDocs'
7
+ import { SequencePanel } from './SequencePanel'
8
+
9
+ type Props = {
10
+ onClose: () => void
11
+ /** dims the page and traps focus; the standalone site docks the panel beside its list instead */
12
+ modal?: boolean
13
+ tab?: PanelTab
14
+ /** switch tab, optionally opening a named example */
15
+ onNavigate: (to: { tab: PanelTab; example?: string }) => void
16
+ onPrev?: () => void
17
+ onNext?: () => void
18
+ } & (
19
+ | { kind: 'op'; service: string; op: OpInfo; variants: Variant[]; example: string }
20
+ | { kind: 'seq'; info: SequenceInfo }
21
+ )
22
+
23
+ /** Lazy entry for everything the side panel needs, so the catalog carries no editor code. */
24
+ export function Host(p: Props) {
25
+ const tab = p.tab ?? 'try'
26
+ const tabs = (
27
+ <div class="rq-tabs panel-tabs" role="tablist">
28
+ {(
29
+ [
30
+ ['try', p.kind === 'seq' ? 'Run' : 'Try'],
31
+ ['docs', 'Docs'],
32
+ ] as const
33
+ ).map(([id, label]) => (
34
+ <button
35
+ type="button"
36
+ role="tab"
37
+ aria-selected={tab === id}
38
+ class={`rq-tab ${tab === id ? 'on' : ''}`}
39
+ onClick={() => p.onNavigate({ tab: id })}
40
+ >
41
+ {label}
42
+ </button>
43
+ ))}
44
+ </div>
45
+ )
46
+
47
+ if (p.kind === 'seq')
48
+ return (
49
+ <Panel
50
+ title={p.info.seq.meta.title}
51
+ subtitle="Sequence"
52
+ flush
53
+ modal={p.modal}
54
+ onPrev={p.onPrev}
55
+ onNext={p.onNext}
56
+ onClose={p.onClose}
57
+ >
58
+ <div class="panel-split">
59
+ {tabs}
60
+ <div class={`panel-main ${tab === 'docs' ? 'scroll' : ''}`}>
61
+ {tab === 'docs' ? <SequenceDocs info={p.info} /> : <SequencePanel info={p.info} />}
62
+ </div>
63
+ </div>
64
+ </Panel>
65
+ )
66
+ return (
67
+ <Panel
68
+ title={p.op.summary?.replace(/\.$/, '') || p.op.path}
69
+ subtitle={`${p.op.method} ${p.op.path}`}
70
+ flush
71
+ modal={p.modal}
72
+ onPrev={p.onPrev}
73
+ onNext={p.onNext}
74
+ onClose={p.onClose}
75
+ >
76
+ <div class="panel-split">
77
+ {tabs}
78
+ <div class={`panel-main ${tab === 'docs' ? 'scroll' : ''}`}>
79
+ {tab === 'docs' ? (
80
+ <OperationDocs
81
+ key={`${p.service}/${p.op.id}`}
82
+ service={p.service}
83
+ op={p.op}
84
+ variants={p.variants}
85
+ onTry={(example) => p.onNavigate({ tab: 'try', example })}
86
+ />
87
+ ) : (
88
+ <RequestPanel
89
+ key={`${p.service}/${p.op.id}`}
90
+ service={p.service}
91
+ op={p.op}
92
+ variants={p.variants}
93
+ initial={p.example}
94
+ />
95
+ )}
96
+ </div>
97
+ </div>
98
+ </Panel>
99
+ )
100
+ }