@specbook/ui 0.2.0 → 0.4.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-viewer/assets/Host-DsO9W5X8.js +41 -0
- package/dist-viewer/assets/index-D59fkVA9.js +1 -0
- package/dist-viewer/assets/main-i06Egg5h.js +14 -0
- package/dist-viewer/assets/{mount-CGL2tOpN.js → mount-CLHBtIzR.js} +2 -2
- package/dist-viewer/assets/panel-l1xvY4kI.css +1 -0
- package/dist-viewer/assets/{panel-C8Ve3LeS.js → panel-nV09I7Wr.js} +1 -1
- package/dist-viewer/assets/registry-b_jzfqy0.js +2 -0
- package/dist-viewer/assets/worker-BmvGpsJS.js +181 -0
- package/dist-viewer/index.html +4 -4
- package/dist-viewer/specbook.js +1 -1
- package/package.json +6 -5
- package/src/app.tsx +67 -12
- package/src/branding.ts +12 -4
- package/src/components/Catalog.tsx +128 -51
- package/src/components/EnvBadge.tsx +21 -3
- package/src/components/EnvironmentMenu.tsx +237 -127
- package/src/components/Fields.tsx +18 -1
- package/src/components/NewEnvironmentDialog.tsx +118 -0
- package/src/components/Palette.tsx +7 -2
- package/src/custom-environments.ts +63 -0
- package/src/embed/panel.tsx +286 -44
- package/src/embed/styles.ts +2 -0
- package/src/embed/target.ts +82 -0
- package/src/embed/theme.ts +75 -0
- package/src/embed.ts +44 -15
- package/src/engine.ts +274 -32
- package/src/environment-list.ts +16 -0
- package/src/main.tsx +7 -2
- package/src/nav.ts +12 -1
- package/src/oauth.ts +9 -8
- package/src/panel/Body.tsx +27 -4
- package/src/panel/Checks.tsx +79 -0
- package/src/panel/CodeSample.tsx +16 -9
- package/src/panel/Combo.tsx +52 -13
- package/src/panel/ControlsGrid.tsx +54 -9
- package/src/panel/ErrorBoundary.tsx +36 -0
- package/src/panel/Host.tsx +14 -2
- package/src/panel/OAuthSignIn.tsx +6 -2
- package/src/panel/OperationDocs.tsx +1 -1
- package/src/panel/Panel.tsx +43 -10
- package/src/panel/ParamGrid.tsx +15 -8
- package/src/panel/RequestPanel.tsx +102 -39
- package/src/panel/RequestView.tsx +2 -1
- package/src/panel/ResponseView.tsx +25 -14
- package/src/panel/SecretsBar.tsx +10 -9
- package/src/panel/SequenceDocs.tsx +7 -6
- package/src/panel/SequencePanel.tsx +124 -32
- package/src/panel/announce.ts +17 -0
- package/src/panel/icons.tsx +9 -0
- package/src/panel/short.ts +24 -0
- package/src/routing.ts +0 -9
- package/src/runtime.ts +19 -1
- package/src/secrets.ts +4 -2
- package/src/storage.ts +58 -0
- package/src/store.ts +8 -0
- package/src/styles/app.css +82 -6
- package/src/styles/embed.css +116 -0
- package/src/styles/panel.css +235 -4
- package/src/viewer/embed.ts +9 -2
- package/src/viewer/manifests.ts +2 -0
- package/src/viewer/mount.ts +2 -0
- package/src/viewer/protocol.ts +6 -0
- package/src/viewer/registry.ts +48 -10
- package/src/viewer/worker.ts +9 -1
- package/src/virtual.d.ts +8 -0
- package/dist-viewer/assets/Host-CSrM9wY1.js +0 -37
- package/dist-viewer/assets/index-ez9c20v3.js +0 -1
- package/dist-viewer/assets/main-38QGGpNT.js +0 -14
- package/dist-viewer/assets/panel-C-vBZrep.css +0 -1
- package/dist-viewer/assets/registry-ChKCFBt1.js +0 -2
- package/dist-viewer/assets/worker-ClGb0tE1.js +0 -181
package/src/styles/app.css
CHANGED
|
@@ -146,6 +146,8 @@
|
|
|
146
146
|
.list-col {
|
|
147
147
|
min-height: 0;
|
|
148
148
|
overflow: auto;
|
|
149
|
+
/* folding a group changes how tall the list is: keep the scrollbar's space, so the list does not widen and narrow */
|
|
150
|
+
scrollbar-gutter: stable;
|
|
149
151
|
/* scrolling to the end of the list must not drag the whole page along */
|
|
150
152
|
overscroll-behavior: contain;
|
|
151
153
|
padding: var(--sb-space-3);
|
|
@@ -287,6 +289,37 @@
|
|
|
287
289
|
font-size: 15px;
|
|
288
290
|
outline: none;
|
|
289
291
|
}
|
|
292
|
+
/* New environment: a small form in the palette's frame, above a docked panel and the settings popover */
|
|
293
|
+
.newenv-backdrop {
|
|
294
|
+
z-index: 60;
|
|
295
|
+
}
|
|
296
|
+
.newenv {
|
|
297
|
+
/* the palette stretches to fill its height; a short form should not */
|
|
298
|
+
align-self: flex-start;
|
|
299
|
+
}
|
|
300
|
+
.newenv form {
|
|
301
|
+
display: flex;
|
|
302
|
+
flex-direction: column;
|
|
303
|
+
gap: var(--sb-space-2);
|
|
304
|
+
padding: var(--sb-space-4);
|
|
305
|
+
}
|
|
306
|
+
.newenv h2 {
|
|
307
|
+
margin: 0;
|
|
308
|
+
font-size: 15px;
|
|
309
|
+
}
|
|
310
|
+
.newenv label {
|
|
311
|
+
display: flex;
|
|
312
|
+
flex-direction: column;
|
|
313
|
+
gap: 4px;
|
|
314
|
+
}
|
|
315
|
+
.newenv input,
|
|
316
|
+
.newenv select {
|
|
317
|
+
border: 1px solid var(--sb-border);
|
|
318
|
+
border-radius: var(--sb-radius-control);
|
|
319
|
+
padding: 8px 10px;
|
|
320
|
+
/* 16px, so iOS does not zoom the page when a field is focused */
|
|
321
|
+
font-size: 16px;
|
|
322
|
+
}
|
|
290
323
|
.palette ul {
|
|
291
324
|
list-style: none;
|
|
292
325
|
margin: 0;
|
|
@@ -427,7 +460,11 @@
|
|
|
427
460
|
padding: var(--sb-space-3) 0 0;
|
|
428
461
|
border-top: 1px solid var(--sb-border);
|
|
429
462
|
}
|
|
463
|
+
/* the tick says it passed; the sentence is muted, so a list of checks reads calmly and a failure stands out */
|
|
430
464
|
.checks .ok {
|
|
465
|
+
color: var(--sb-text-muted);
|
|
466
|
+
}
|
|
467
|
+
.checks .ok .tick {
|
|
431
468
|
color: var(--sb-success);
|
|
432
469
|
}
|
|
433
470
|
.checks .bad {
|
|
@@ -647,6 +684,19 @@
|
|
|
647
684
|
text-overflow: ellipsis;
|
|
648
685
|
white-space: nowrap;
|
|
649
686
|
}
|
|
687
|
+
/* room for it: a name of ordinary length stays whole and the search field gives way instead, while a
|
|
688
|
+
longer one still ends in an ellipsis. Narrower than this only the logo is left. */
|
|
689
|
+
@media (min-width: 360px) {
|
|
690
|
+
.brand {
|
|
691
|
+
flex: 0 0 auto;
|
|
692
|
+
}
|
|
693
|
+
.brand-name {
|
|
694
|
+
max-width: 9ch;
|
|
695
|
+
}
|
|
696
|
+
.header .palette-btn {
|
|
697
|
+
min-width: 64px;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
650
700
|
.palette-btn {
|
|
651
701
|
flex: 1;
|
|
652
702
|
min-width: 0;
|
|
@@ -657,7 +707,7 @@
|
|
|
657
707
|
white-space: nowrap;
|
|
658
708
|
}
|
|
659
709
|
.palette-btn kbd,
|
|
660
|
-
.header .spacer {
|
|
710
|
+
.header > .spacer {
|
|
661
711
|
display: none;
|
|
662
712
|
}
|
|
663
713
|
.header .menu-btn {
|
|
@@ -693,12 +743,37 @@
|
|
|
693
743
|
justify-content: space-between;
|
|
694
744
|
flex-wrap: nowrap;
|
|
695
745
|
}
|
|
696
|
-
|
|
697
|
-
|
|
746
|
+
/* every labelled control in the menu: the label to the left, the control the same width on the right */
|
|
747
|
+
.header-controls .row > select {
|
|
748
|
+
flex: 0 0 56%;
|
|
749
|
+
width: 56%;
|
|
750
|
+
min-width: 0;
|
|
751
|
+
}
|
|
752
|
+
/* Synthetic data is a row inside a row (its label and, once fixed, a seed field): the inner one fills it */
|
|
753
|
+
.header-controls .row > .row {
|
|
754
|
+
flex: 1 1 100%;
|
|
755
|
+
}
|
|
756
|
+
.header-controls .row.muted {
|
|
757
|
+
flex-wrap: wrap;
|
|
758
|
+
}
|
|
759
|
+
.header-controls .seed-input {
|
|
760
|
+
flex: 0 0 56%;
|
|
761
|
+
margin-left: auto;
|
|
762
|
+
}
|
|
763
|
+
.header-controls .envmenu,
|
|
764
|
+
.header-controls .envmenu-btn {
|
|
765
|
+
width: 100%;
|
|
766
|
+
}
|
|
767
|
+
.header-controls .envmenu-btn {
|
|
768
|
+
display: flex;
|
|
769
|
+
align-items: center;
|
|
770
|
+
justify-content: center;
|
|
771
|
+
gap: var(--sb-space-2);
|
|
698
772
|
}
|
|
699
773
|
/* in the menu there is room for the words, and each link is a full row */
|
|
700
774
|
.header-controls .gh-label,
|
|
701
|
-
.header-controls .share-label
|
|
775
|
+
.header-controls .share-label,
|
|
776
|
+
.header-controls .envmenu-label {
|
|
702
777
|
display: inline;
|
|
703
778
|
}
|
|
704
779
|
.header-controls .gh-star,
|
|
@@ -800,10 +875,11 @@
|
|
|
800
875
|
min-width: 120px;
|
|
801
876
|
max-width: 360px;
|
|
802
877
|
}
|
|
803
|
-
/* GitHub and
|
|
878
|
+
/* GitHub, Share and Environment settings keep their icons and lose their words where the header is tight */
|
|
804
879
|
@media (max-width: 1360px) {
|
|
805
880
|
.gh-label,
|
|
806
|
-
.share-label
|
|
881
|
+
.share-label,
|
|
882
|
+
.envmenu-label {
|
|
807
883
|
display: none;
|
|
808
884
|
}
|
|
809
885
|
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* How the panel sits on someone else's page. Unlayered on purpose: it must beat the app's layered rules (the full-screen
|
|
3
|
+
* phone sheet, the sheet from the right) which this presentation replaces.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/* the browser's top layer when it has one (so no host z-index can cover the panel), else a very high fixed box */
|
|
7
|
+
.sb-top {
|
|
8
|
+
position: fixed;
|
|
9
|
+
inset: 0;
|
|
10
|
+
width: 100%;
|
|
11
|
+
height: 100%;
|
|
12
|
+
max-width: none;
|
|
13
|
+
max-height: none;
|
|
14
|
+
margin: 0;
|
|
15
|
+
padding: 0;
|
|
16
|
+
border: 0;
|
|
17
|
+
overflow: visible;
|
|
18
|
+
background: transparent;
|
|
19
|
+
color: inherit;
|
|
20
|
+
}
|
|
21
|
+
.sb-top.sb-fallback {
|
|
22
|
+
z-index: 2147483000;
|
|
23
|
+
}
|
|
24
|
+
.sb-top[hidden] {
|
|
25
|
+
display: none;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.panel-backdrop {
|
|
29
|
+
backdrop-filter: blur(2px);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* desktop: a centered dialog */
|
|
33
|
+
.panel.popover {
|
|
34
|
+
position: fixed;
|
|
35
|
+
inset: 0;
|
|
36
|
+
margin: auto;
|
|
37
|
+
width: min(880px, calc(100vw - 48px));
|
|
38
|
+
min-width: 0;
|
|
39
|
+
height: min(86vh, 780px);
|
|
40
|
+
overflow: hidden;
|
|
41
|
+
border: 1px solid var(--sb-border);
|
|
42
|
+
border-radius: 14px;
|
|
43
|
+
box-shadow:
|
|
44
|
+
0 24px 64px color-mix(in srgb, black 35%, transparent),
|
|
45
|
+
0 2px 8px color-mix(in srgb, black 20%, transparent);
|
|
46
|
+
animation: sb-popover-in 180ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
47
|
+
}
|
|
48
|
+
@keyframes sb-popover-in {
|
|
49
|
+
from {
|
|
50
|
+
opacity: 0;
|
|
51
|
+
transform: translateY(8px) scale(0.985);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
@keyframes sb-sheet-in {
|
|
55
|
+
from {
|
|
56
|
+
opacity: 0;
|
|
57
|
+
transform: translateY(24px);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
@media (prefers-reduced-motion: reduce) {
|
|
61
|
+
.panel.popover {
|
|
62
|
+
animation: none;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* phone: a sheet from the bottom, with a handle, clear of the notch and the home bar */
|
|
67
|
+
@media (max-width: 820px) {
|
|
68
|
+
.panel.popover {
|
|
69
|
+
inset: auto 0 0 0;
|
|
70
|
+
margin: 0;
|
|
71
|
+
width: 100%;
|
|
72
|
+
height: min(92dvh, calc(100dvh - env(safe-area-inset-top, 0px) - 12px));
|
|
73
|
+
border-width: 1px 0 0;
|
|
74
|
+
border-radius: 16px 16px 0 0;
|
|
75
|
+
padding-bottom: env(safe-area-inset-bottom, 0px);
|
|
76
|
+
animation-name: sb-sheet-in;
|
|
77
|
+
}
|
|
78
|
+
.panel.popover::before {
|
|
79
|
+
content: "";
|
|
80
|
+
flex: none;
|
|
81
|
+
width: 36px;
|
|
82
|
+
height: 4px;
|
|
83
|
+
margin: 8px auto 0;
|
|
84
|
+
border-radius: 2px;
|
|
85
|
+
background: var(--sb-border);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* a finger, not a pointer: the controls of the header are comfortable targets */
|
|
90
|
+
@media (pointer: coarse) {
|
|
91
|
+
.panel.popover .panel-head .icon {
|
|
92
|
+
min-width: 44px;
|
|
93
|
+
min-height: 44px;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.embed-problem {
|
|
98
|
+
margin: var(--sb-space-4);
|
|
99
|
+
color: var(--sb-text);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* what the page's attributes asked for that could not be done, over the foot of the panel */
|
|
103
|
+
.embed-banner {
|
|
104
|
+
position: fixed;
|
|
105
|
+
left: 50%;
|
|
106
|
+
bottom: 12px;
|
|
107
|
+
transform: translateX(-50%);
|
|
108
|
+
z-index: 22;
|
|
109
|
+
max-width: min(640px, calc(100vw - 24px));
|
|
110
|
+
padding: 6px 12px;
|
|
111
|
+
border: 1px solid var(--sb-border);
|
|
112
|
+
border-radius: 8px;
|
|
113
|
+
background: var(--sb-surface-raised, var(--sb-surface));
|
|
114
|
+
color: var(--sb-text);
|
|
115
|
+
font-size: 13px;
|
|
116
|
+
}
|
package/src/styles/panel.css
CHANGED
|
@@ -377,6 +377,46 @@
|
|
|
377
377
|
gap: var(--sb-space-2);
|
|
378
378
|
padding: 8px;
|
|
379
379
|
}
|
|
380
|
+
.panel-error {
|
|
381
|
+
margin: var(--sb-space-4);
|
|
382
|
+
padding: var(--sb-space-4);
|
|
383
|
+
border: 1px solid var(--sb-border);
|
|
384
|
+
border-left: 3px solid var(--sb-danger, #dc2626);
|
|
385
|
+
border-radius: var(--sb-radius-card);
|
|
386
|
+
background: var(--sb-surface);
|
|
387
|
+
}
|
|
388
|
+
.panel-error pre {
|
|
389
|
+
margin: var(--sb-space-2) 0 var(--sb-space-3);
|
|
390
|
+
white-space: pre-wrap;
|
|
391
|
+
overflow-wrap: anywhere;
|
|
392
|
+
font: 12px var(--sb-font-mono);
|
|
393
|
+
color: var(--sb-text-muted);
|
|
394
|
+
}
|
|
395
|
+
button.flows-title {
|
|
396
|
+
width: 100%;
|
|
397
|
+
border: 0;
|
|
398
|
+
background: none;
|
|
399
|
+
color: inherit;
|
|
400
|
+
font: inherit;
|
|
401
|
+
text-align: left;
|
|
402
|
+
cursor: pointer;
|
|
403
|
+
}
|
|
404
|
+
@media (pointer: coarse) {
|
|
405
|
+
button.flows-title {
|
|
406
|
+
min-height: 44px;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
button.flows-title:hover {
|
|
410
|
+
background: var(--sb-surface-raised);
|
|
411
|
+
}
|
|
412
|
+
button.flows-title:focus-visible {
|
|
413
|
+
outline: 2px solid var(--sb-focus-ring);
|
|
414
|
+
outline-offset: -2px;
|
|
415
|
+
}
|
|
416
|
+
.flows-title .caret {
|
|
417
|
+
width: 1em;
|
|
418
|
+
color: var(--sb-text-muted);
|
|
419
|
+
}
|
|
380
420
|
.flows-title .count {
|
|
381
421
|
margin-left: auto;
|
|
382
422
|
font-size: 12px;
|
|
@@ -390,6 +430,20 @@
|
|
|
390
430
|
.flows .op-row:last-child {
|
|
391
431
|
border-bottom: 0;
|
|
392
432
|
}
|
|
433
|
+
/* saved requests: the same block as flows, in the neutral border colour, so flows stay the one accent */
|
|
434
|
+
.flows.requests {
|
|
435
|
+
border-color: var(--sb-border);
|
|
436
|
+
border-left-color: var(--sb-text-muted);
|
|
437
|
+
background: var(--sb-surface-raised);
|
|
438
|
+
}
|
|
439
|
+
.method.req {
|
|
440
|
+
color: var(--sb-text-muted);
|
|
441
|
+
}
|
|
442
|
+
.req-tag {
|
|
443
|
+
margin-left: 6px;
|
|
444
|
+
font-size: 11px;
|
|
445
|
+
font-weight: 500;
|
|
446
|
+
}
|
|
393
447
|
.flow-play {
|
|
394
448
|
font-size: 10px;
|
|
395
449
|
text-align: center;
|
|
@@ -970,6 +1024,99 @@
|
|
|
970
1024
|
border: 0;
|
|
971
1025
|
}
|
|
972
1026
|
|
|
1027
|
+
/* ---- a failed check: what was expected, what came back, as a colored diff */
|
|
1028
|
+
.visually-hidden {
|
|
1029
|
+
position: absolute;
|
|
1030
|
+
width: 1px;
|
|
1031
|
+
height: 1px;
|
|
1032
|
+
overflow: hidden;
|
|
1033
|
+
clip-path: inset(50%);
|
|
1034
|
+
white-space: nowrap;
|
|
1035
|
+
}
|
|
1036
|
+
.checks .bad {
|
|
1037
|
+
margin-top: var(--sb-space-2);
|
|
1038
|
+
color: inherit;
|
|
1039
|
+
}
|
|
1040
|
+
.check-fail {
|
|
1041
|
+
display: grid;
|
|
1042
|
+
gap: var(--sb-space-2);
|
|
1043
|
+
margin: 0;
|
|
1044
|
+
min-width: 0;
|
|
1045
|
+
padding: var(--sb-space-3);
|
|
1046
|
+
border: 1px solid color-mix(in srgb, var(--sb-danger) 35%, var(--sb-border));
|
|
1047
|
+
border-left: 3px solid var(--sb-danger);
|
|
1048
|
+
border-radius: var(--sb-radius-control);
|
|
1049
|
+
background: color-mix(in srgb, var(--sb-danger) 5%, var(--sb-surface));
|
|
1050
|
+
}
|
|
1051
|
+
.check-head {
|
|
1052
|
+
display: flex;
|
|
1053
|
+
flex-wrap: wrap;
|
|
1054
|
+
align-items: center;
|
|
1055
|
+
gap: var(--sb-space-2);
|
|
1056
|
+
color: var(--sb-danger);
|
|
1057
|
+
}
|
|
1058
|
+
.check-head strong {
|
|
1059
|
+
min-width: 0;
|
|
1060
|
+
overflow-wrap: anywhere;
|
|
1061
|
+
}
|
|
1062
|
+
.diff-legend {
|
|
1063
|
+
display: flex;
|
|
1064
|
+
gap: var(--sb-space-3);
|
|
1065
|
+
font-size: 12px;
|
|
1066
|
+
font-weight: 600;
|
|
1067
|
+
}
|
|
1068
|
+
.diff-key.diff-expected,
|
|
1069
|
+
.diff-line.diff-expected {
|
|
1070
|
+
color: var(--sb-success);
|
|
1071
|
+
}
|
|
1072
|
+
.diff-key.diff-received,
|
|
1073
|
+
.diff-line.diff-received {
|
|
1074
|
+
color: var(--sb-danger);
|
|
1075
|
+
}
|
|
1076
|
+
.diff {
|
|
1077
|
+
margin: 0;
|
|
1078
|
+
padding: var(--sb-space-2) 0;
|
|
1079
|
+
white-space: pre;
|
|
1080
|
+
max-height: 18em;
|
|
1081
|
+
overflow: auto;
|
|
1082
|
+
border-radius: var(--sb-radius-control);
|
|
1083
|
+
background: var(--sb-surface);
|
|
1084
|
+
border: 1px solid var(--sb-border);
|
|
1085
|
+
font: 12.5px / 1.5 var(--sb-font-mono);
|
|
1086
|
+
}
|
|
1087
|
+
.diff-line {
|
|
1088
|
+
display: flex;
|
|
1089
|
+
min-width: max-content;
|
|
1090
|
+
padding: 0 var(--sb-space-3) 0 0;
|
|
1091
|
+
}
|
|
1092
|
+
.diff-line.diff-expected {
|
|
1093
|
+
background: var(--sb-success-subtle);
|
|
1094
|
+
}
|
|
1095
|
+
.diff-line.diff-received {
|
|
1096
|
+
background: var(--sb-danger-subtle);
|
|
1097
|
+
}
|
|
1098
|
+
.diff-line.diff-same {
|
|
1099
|
+
color: var(--sb-text-muted);
|
|
1100
|
+
}
|
|
1101
|
+
.diff-line.diff-skip {
|
|
1102
|
+
color: var(--sb-text-muted);
|
|
1103
|
+
font-style: italic;
|
|
1104
|
+
}
|
|
1105
|
+
.diff-gutter {
|
|
1106
|
+
flex: none;
|
|
1107
|
+
width: 2em;
|
|
1108
|
+
text-align: center;
|
|
1109
|
+
user-select: none;
|
|
1110
|
+
}
|
|
1111
|
+
.diff-text {
|
|
1112
|
+
white-space: pre;
|
|
1113
|
+
}
|
|
1114
|
+
.check-where {
|
|
1115
|
+
font: 12px var(--sb-font-mono);
|
|
1116
|
+
color: var(--sb-text-muted);
|
|
1117
|
+
overflow-wrap: anywhere;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
973
1120
|
/* ---- shared small controls */
|
|
974
1121
|
.link {
|
|
975
1122
|
background: none;
|
|
@@ -1462,6 +1609,66 @@
|
|
|
1462
1609
|
width: 60%;
|
|
1463
1610
|
}
|
|
1464
1611
|
}
|
|
1612
|
+
.controls-head {
|
|
1613
|
+
display: flex;
|
|
1614
|
+
align-items: center;
|
|
1615
|
+
justify-content: space-between;
|
|
1616
|
+
gap: var(--sb-space-2);
|
|
1617
|
+
}
|
|
1618
|
+
.controls-head .gen-all {
|
|
1619
|
+
display: inline-flex;
|
|
1620
|
+
align-items: center;
|
|
1621
|
+
gap: 6px;
|
|
1622
|
+
text-transform: none;
|
|
1623
|
+
letter-spacing: 0;
|
|
1624
|
+
font-size: 12px;
|
|
1625
|
+
padding: 4px 10px;
|
|
1626
|
+
}
|
|
1627
|
+
/* a value with a generator: the cell and the button that makes another, never wider than the column */
|
|
1628
|
+
.cell-gen {
|
|
1629
|
+
display: flex;
|
|
1630
|
+
align-items: center;
|
|
1631
|
+
gap: 4px;
|
|
1632
|
+
min-width: 0;
|
|
1633
|
+
}
|
|
1634
|
+
.cell-gen > :first-child {
|
|
1635
|
+
flex: 1 1 auto;
|
|
1636
|
+
min-width: 0;
|
|
1637
|
+
}
|
|
1638
|
+
.cell-gen .cell {
|
|
1639
|
+
width: 100%;
|
|
1640
|
+
min-width: 0;
|
|
1641
|
+
}
|
|
1642
|
+
.gen {
|
|
1643
|
+
flex: none;
|
|
1644
|
+
display: inline-flex;
|
|
1645
|
+
align-items: center;
|
|
1646
|
+
justify-content: center;
|
|
1647
|
+
width: 28px;
|
|
1648
|
+
height: 28px;
|
|
1649
|
+
margin-right: 4px;
|
|
1650
|
+
padding: 0;
|
|
1651
|
+
color: var(--sb-text-muted);
|
|
1652
|
+
background: transparent;
|
|
1653
|
+
border: 1px solid var(--sb-border);
|
|
1654
|
+
border-radius: var(--sb-radius-control);
|
|
1655
|
+
cursor: pointer;
|
|
1656
|
+
}
|
|
1657
|
+
.gen:hover,
|
|
1658
|
+
.gen:focus-visible {
|
|
1659
|
+
color: var(--sb-accent);
|
|
1660
|
+
border-color: var(--sb-accent);
|
|
1661
|
+
}
|
|
1662
|
+
/* a finger, not a pointer: the button is a comfortable target */
|
|
1663
|
+
@media (pointer: coarse) {
|
|
1664
|
+
.gen {
|
|
1665
|
+
width: 44px;
|
|
1666
|
+
height: 44px;
|
|
1667
|
+
}
|
|
1668
|
+
.controls-head .gen-all {
|
|
1669
|
+
min-height: 44px;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1465
1672
|
.multi-cell {
|
|
1466
1673
|
display: flex;
|
|
1467
1674
|
flex-wrap: wrap;
|
|
@@ -1578,6 +1785,8 @@
|
|
|
1578
1785
|
max-height: min(70vh, 640px);
|
|
1579
1786
|
overflow: auto;
|
|
1580
1787
|
display: grid;
|
|
1788
|
+
/* one column that cannot outgrow the popover: Safari sizes an auto track to its widest field */
|
|
1789
|
+
grid-template-columns: minmax(0, 1fr);
|
|
1581
1790
|
gap: var(--sb-space-3);
|
|
1582
1791
|
padding: var(--sb-space-3) var(--sb-space-4);
|
|
1583
1792
|
background: var(--sb-surface);
|
|
@@ -1651,6 +1860,23 @@
|
|
|
1651
1860
|
background: var(--sb-danger-subtle);
|
|
1652
1861
|
color: var(--sb-danger);
|
|
1653
1862
|
}
|
|
1863
|
+
/* a variable's value, where it comes from */
|
|
1864
|
+
.envrow .src {
|
|
1865
|
+
flex: none;
|
|
1866
|
+
padding: 0 8px;
|
|
1867
|
+
border-radius: 999px;
|
|
1868
|
+
font-size: 11px;
|
|
1869
|
+
font-weight: 600;
|
|
1870
|
+
}
|
|
1871
|
+
.src.inherited {
|
|
1872
|
+
background: var(--sb-surface-raised);
|
|
1873
|
+
color: var(--sb-text-muted);
|
|
1874
|
+
}
|
|
1875
|
+
.envuse {
|
|
1876
|
+
margin: -2px 0 var(--sb-space-2);
|
|
1877
|
+
padding-left: 2px;
|
|
1878
|
+
overflow-wrap: anywhere;
|
|
1879
|
+
}
|
|
1654
1880
|
.op-lock {
|
|
1655
1881
|
display: inline-block;
|
|
1656
1882
|
margin-left: 6px;
|
|
@@ -1745,12 +1971,10 @@
|
|
|
1745
1971
|
.panel-head .spacer {
|
|
1746
1972
|
display: none;
|
|
1747
1973
|
}
|
|
1748
|
-
|
|
1974
|
+
/* the Environment badge in the panel is the one switch on a phone: a bare, unlabeled copy here only confuses */
|
|
1975
|
+
.panel-head label.row {
|
|
1749
1976
|
display: none;
|
|
1750
1977
|
}
|
|
1751
|
-
.panel-head select {
|
|
1752
|
-
max-width: 34%;
|
|
1753
|
-
}
|
|
1754
1978
|
.stepper {
|
|
1755
1979
|
gap: 0;
|
|
1756
1980
|
}
|
|
@@ -1805,6 +2029,13 @@
|
|
|
1805
2029
|
.envrow {
|
|
1806
2030
|
flex-wrap: wrap;
|
|
1807
2031
|
}
|
|
2032
|
+
/* a variable: its name above, then the value (an address needs the room) beside its source */
|
|
2033
|
+
.envrow code {
|
|
2034
|
+
flex: 1 0 100%;
|
|
2035
|
+
}
|
|
2036
|
+
.envrow input {
|
|
2037
|
+
flex: 1 1 0;
|
|
2038
|
+
}
|
|
1808
2039
|
.envrow input,
|
|
1809
2040
|
.envrow select {
|
|
1810
2041
|
min-width: 0;
|
package/src/viewer/embed.ts
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* <script type="module" src="https://ui.specbook.dev/v1/specbook.js"></script>
|
|
8
8
|
*
|
|
9
9
|
* The script takes the page over (it owns the document, as `/docs` does). `spec` may be relative.
|
|
10
|
-
* `baseUrl` gives the API address when the document has none, `target` names another container
|
|
10
|
+
* `baseUrl` gives the API address when the document has none, `target` names another container, and
|
|
11
|
+
* `defaultEnv` (`"live"` or the name of a server) picks the environment shown first instead of `mock`.
|
|
11
12
|
*/
|
|
12
13
|
import { handleOAuthCallback } from '../oauth-callback'
|
|
13
14
|
import { SpecWorker } from './client'
|
|
@@ -17,6 +18,7 @@ interface EmbedConfig {
|
|
|
17
18
|
spec?: string
|
|
18
19
|
baseUrl?: string
|
|
19
20
|
target?: string
|
|
21
|
+
defaultEnv?: string
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
function readConfig(): EmbedConfig {
|
|
@@ -29,7 +31,11 @@ function readConfig(): EmbedConfig {
|
|
|
29
31
|
}
|
|
30
32
|
}
|
|
31
33
|
const script = document.querySelector<HTMLScriptElement>('script[src$="specbook.js"][data-spec]')
|
|
32
|
-
return {
|
|
34
|
+
return {
|
|
35
|
+
spec: script?.dataset.spec,
|
|
36
|
+
baseUrl: script?.dataset.baseUrl,
|
|
37
|
+
defaultEnv: script?.dataset.defaultEnv,
|
|
38
|
+
}
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
function container(sel?: string): HTMLElement {
|
|
@@ -72,6 +78,7 @@ async function start() {
|
|
|
72
78
|
worker,
|
|
73
79
|
source: { kind: 'url', url },
|
|
74
80
|
baseUrl: cfg.baseUrl,
|
|
81
|
+
defaultEnv: typeof cfg.defaultEnv === 'string' ? cfg.defaultEnv : undefined,
|
|
75
82
|
shareable: false,
|
|
76
83
|
setTitle: false,
|
|
77
84
|
})
|
package/src/viewer/mount.ts
CHANGED
|
@@ -12,6 +12,7 @@ export async function mountOpened(o: {
|
|
|
12
12
|
worker: SpecWorker
|
|
13
13
|
source: Source
|
|
14
14
|
baseUrl?: string
|
|
15
|
+
defaultEnv?: string
|
|
15
16
|
shareable?: boolean
|
|
16
17
|
setTitle?: boolean
|
|
17
18
|
}): Promise<void> {
|
|
@@ -19,6 +20,7 @@ export async function mountOpened(o: {
|
|
|
19
20
|
const g: ViewerGlobal = {
|
|
20
21
|
open: o.result,
|
|
21
22
|
baseUrl: o.baseUrl,
|
|
23
|
+
defaultEnv: o.defaultEnv,
|
|
22
24
|
source:
|
|
23
25
|
o.shareable === false
|
|
24
26
|
? undefined
|
package/src/viewer/protocol.ts
CHANGED
|
@@ -25,6 +25,12 @@ export interface OpenResult {
|
|
|
25
25
|
auth: Record<string, AuthProfileLike>
|
|
26
26
|
secrets: Record<string, Record<string, never>>
|
|
27
27
|
defaultProfile?: string
|
|
28
|
+
/**
|
|
29
|
+
* Sign-in addresses the document gives relative to its servers, as variables: the values for `live`,
|
|
30
|
+
* and per other environment. The profiles in `auth` read them as `{{name}}`.
|
|
31
|
+
*/
|
|
32
|
+
authVars: Record<string, string>
|
|
33
|
+
authEnvironmentVars: Record<string, Record<string, string>>
|
|
28
34
|
/** absolute API address, from `servers` (resolved against the spec URL) or `host` */
|
|
29
35
|
baseUrl?: string
|
|
30
36
|
/** every usable server the document lists, as environments; the first is `live` */
|