@jxsuite/studio 6.0.0 → 6.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.
Files changed (208) hide show
  1. package/dist/chunks/2020-va2wwra4.js +4 -0
  2. package/dist/chunks/adapter-f93pafd7.js +4 -0
  3. package/dist/chunks/{cf-account-picker-bb2yq946.js → cf-account-picker-af1h381v.js} +3 -3
  4. package/dist/chunks/cf-settings-125tdm0w.js +4 -0
  5. package/dist/chunks/components-ax2f1fnn.js +4 -0
  6. package/dist/chunks/content-source-108aqtk1.js +4 -0
  7. package/dist/chunks/{convert-to-repeater-vqkt7s3s.js → convert-to-repeater-ry5rmppr.js} +3 -3
  8. package/dist/chunks/editor-pfgdmkb0.js +4 -0
  9. package/dist/chunks/entry-commands-t0k1q0kx.js +4 -0
  10. package/dist/chunks/{extension-sections-8beq9ac3.js → extension-sections-6mwh2hf8.js} +3 -3
  11. package/dist/chunks/file-ops-meej5mqf.js +4 -0
  12. package/dist/chunks/files-44vp8a2s.js +4 -0
  13. package/dist/chunks/format-host-7q1petb9.js +4 -0
  14. package/dist/chunks/fs-events-h2mwnxaz.js +4 -0
  15. package/dist/chunks/git-panel-zngbymvd.js +4 -0
  16. package/dist/chunks/{grid-view-ca10def6.js → grid-view-sx9vynsp.js} +15 -15
  17. package/dist/chunks/icons-p44xhjf0.js +4 -0
  18. package/dist/chunks/iframe-host-qk3ycq87.js +4 -0
  19. package/dist/chunks/{index-9ydfe04y.js → index-2veqmnhr.js} +3 -3
  20. package/dist/chunks/index-wkyfjeqz.js +4 -0
  21. package/dist/chunks/{javascript-d9pqgc2p.js → javascript-8nbvs12s.js} +3 -3
  22. package/dist/chunks/jsonMode-s8kt9dsj.js +13 -0
  23. package/dist/chunks/jx-validate-psgd7vsx.js +4 -0
  24. package/dist/chunks/layers-panel-1kq2jyfk.js +4 -0
  25. package/dist/chunks/layers-xd92fg8d.js +4 -0
  26. package/dist/chunks/lit-html-ac4cwwr4.js +4 -0
  27. package/dist/chunks/media-picker-0svzkdyb.js +4 -0
  28. package/dist/chunks/menu-5c55ea3d.js +4 -0
  29. package/dist/chunks/{monaco-binding-62dtmgbf.js → monaco-binding-z6r70pdt.js} +3 -3
  30. package/dist/chunks/monaco-buffer-fz6gq5y7.js +4 -0
  31. package/dist/chunks/monaco-setup-q4axkgj7.js +4 -0
  32. package/dist/chunks/notify-0xgtn8q1.js +4 -0
  33. package/dist/chunks/platform-v1qctbe6.js +4 -0
  34. package/dist/chunks/publish-panel-ym1fmgq5.js +4 -0
  35. package/dist/chunks/quick-search-a54e6t5n.js +4 -0
  36. package/dist/chunks/references-gmqp82fs.js +4 -0
  37. package/dist/chunks/right-panel-v7t9awbk.js +4 -0
  38. package/dist/chunks/section-registry-vhyf3zsr.js +4 -0
  39. package/dist/chunks/studio-0bhx5hz6.js +5 -0
  40. package/dist/chunks/studio-0k1ddnyy.js +6 -0
  41. package/dist/chunks/studio-1b04pd0j.js +64 -0
  42. package/dist/chunks/{studio-sxc0zvpt.js → studio-1q1sh2ax.js} +3 -3
  43. package/dist/chunks/studio-4dwbj6gb.js +5 -0
  44. package/dist/chunks/studio-4tanh828.js +5 -0
  45. package/dist/chunks/{studio-fqq890ap.js → studio-55h4mstj.js} +3 -3
  46. package/dist/chunks/studio-5tnvjkz4.js +5 -0
  47. package/dist/chunks/studio-60dqgb0c.js +5 -0
  48. package/dist/chunks/studio-61dp09qg.js +9 -0
  49. package/dist/chunks/studio-63epcrj2.js +5 -0
  50. package/dist/chunks/studio-74k29dy9.js +5 -0
  51. package/dist/chunks/{studio-qa73881a.js → studio-7vg6jcd3.js} +4 -4
  52. package/dist/chunks/studio-7zx31knw.js +9 -0
  53. package/dist/chunks/studio-8bxk97km.js +5 -0
  54. package/dist/chunks/{studio-edbtvcd1.js → studio-8grwxtqk.js} +4 -4
  55. package/dist/chunks/studio-933e30n4.js +5 -0
  56. package/dist/chunks/{studio-rr4xkv81.js → studio-98wnwhbg.js} +4 -4
  57. package/dist/chunks/studio-9mccjwjh.js +14 -0
  58. package/dist/chunks/studio-a2ke1e4t.js +5 -0
  59. package/dist/chunks/studio-e3a2a6v3.js +4 -0
  60. package/dist/chunks/{studio-pwqfmt4p.js → studio-e81q1vb9.js} +4 -4
  61. package/dist/chunks/{studio-7pgy3xaf.js → studio-epgrmqqm.js} +4 -4
  62. package/dist/chunks/studio-g1shz58g.js +5 -0
  63. package/dist/chunks/studio-gf0fz03c.js +5 -0
  64. package/dist/chunks/studio-gjtpe62q.js +6 -0
  65. package/dist/chunks/{studio-tzegmy4w.js → studio-gkskhg7m.js} +4 -4
  66. package/dist/chunks/studio-gx4dnjzd.js +5 -0
  67. package/dist/chunks/{studio-8xcwewjq.js → studio-h46vtzk8.js} +4 -4
  68. package/dist/chunks/studio-j03zga3y.js +9 -0
  69. package/dist/chunks/{studio-4r0sjtx8.js → studio-jhpxeckz.js} +4 -4
  70. package/dist/chunks/{studio-zd4kayn2.js → studio-k01r7ykp.js} +4 -4
  71. package/dist/chunks/{studio-03wwfvyt.js → studio-kggey1jt.js} +4 -4
  72. package/dist/chunks/{studio-hnvnfbdv.js → studio-kk7h30ne.js} +6 -6
  73. package/dist/chunks/{studio-01qza1n3.js → studio-kt1zqney.js} +4 -4
  74. package/dist/chunks/{studio-r3kaka4d.js → studio-m3cwhvye.js} +4 -4
  75. package/dist/chunks/{studio-evwgp1ym.js → studio-mcayt6hz.js} +4 -4
  76. package/dist/chunks/{studio-5vy2wfm5.js → studio-myjcghv3.js} +4 -4
  77. package/dist/chunks/studio-n8bxwc3y.js +4 -0
  78. package/dist/chunks/{studio-ztvcxm3w.js → studio-ngm8gvpb.js} +4 -4
  79. package/dist/chunks/{studio-01kbbqg3.js → studio-nj5a01jy.js} +196 -196
  80. package/dist/chunks/{studio-xkfs0dp2.js → studio-pcgz8heb.js} +4 -4
  81. package/dist/chunks/{studio-0bbxhhce.js → studio-pre0skq6.js} +4 -4
  82. package/dist/chunks/{studio-n78gjfy8.js → studio-qcsq0t15.js} +4 -4
  83. package/dist/chunks/studio-qkstvve9.js +5 -0
  84. package/dist/chunks/studio-qv1pwd3c.js +5 -0
  85. package/dist/chunks/{studio-rmn2dz59.js → studio-rc6dvxjb.js} +5 -5
  86. package/dist/chunks/{studio-st75kn99.js → studio-rv11kfwa.js} +7 -7
  87. package/dist/chunks/studio-s8dmmz66.js +5 -0
  88. package/dist/chunks/studio-sp5xzbj4.js +6 -0
  89. package/dist/chunks/{studio-tmeq6mjb.js → studio-szsjbj0g.js} +4 -4
  90. package/dist/chunks/studio-t1xzawhr.js +16 -0
  91. package/dist/chunks/{studio-mk00zd5d.js → studio-t6b6qe31.js} +4 -4
  92. package/dist/chunks/{studio-bftp4n4b.js → studio-tdcprbqb.js} +4 -4
  93. package/dist/chunks/{studio-ge5fqjth.js → studio-vrmcjf9g.js} +5 -5
  94. package/dist/chunks/studio-vyc0b4he.js +10 -0
  95. package/dist/chunks/{studio-semd1s8s.js → studio-w0cgf6jg.js} +4 -4
  96. package/dist/chunks/{studio-gp02nqnw.js → studio-w3hskzqk.js} +4 -4
  97. package/dist/chunks/{studio-1wvfxkyy.js → studio-wszjyh0j.js} +4 -4
  98. package/dist/chunks/studio-xgrgcdyh.js +9 -0
  99. package/dist/chunks/{studio-y7jehjrp.js → studio-xz2yxfcb.js} +4 -4
  100. package/dist/chunks/{studio-aje966s9.js → studio-y6n42tgs.js} +4 -4
  101. package/dist/chunks/{studio-yr04mv2j.js → studio-yb560vxr.js} +4 -4
  102. package/dist/chunks/{studio-tp642n5g.js → studio-yn9f8vtx.js} +4 -4
  103. package/dist/chunks/studio-yw05w8f0.js +7 -0
  104. package/dist/chunks/{studio-cbr9czkn.js → studio-z16sgavc.js} +4 -4
  105. package/dist/chunks/studio-zey7zsdw.js +12 -0
  106. package/dist/chunks/{studio-1q6amtph.js → studio-zyqn40w4.js} +4 -4
  107. package/dist/chunks/tsMode-2frah6h1.js +14 -0
  108. package/dist/chunks/workspace-zt81g1ms.js +4 -0
  109. package/dist/chunks/ws-client-n2bh4hkf.js +4 -0
  110. package/dist/iframe-entry.js +13 -13
  111. package/dist/manifest.json +62 -62
  112. package/dist/studio.js +87 -87
  113. package/dist/workers/ts.worker.js +3 -3
  114. package/package.json +10 -10
  115. package/src/canvas/canvas-render.ts +4 -1
  116. package/src/canvas/canvas-utils.ts +73 -15
  117. package/src/canvas/iframe-host.ts +67 -21
  118. package/src/canvas/iframe-inline-edit.ts +22 -4
  119. package/src/canvas/iframe-interaction.ts +7 -1
  120. package/src/canvas/iframe-protocol.ts +3 -0
  121. package/src/canvas/iframe-render.ts +5 -2
  122. package/src/commands/defaults.ts +4 -1
  123. package/src/commands/live-context.ts +3 -1
  124. package/src/editor/shortcuts.ts +55 -27
  125. package/src/files/media-paths.ts +3 -1
  126. package/src/new-project/add-repo-modal.ts +459 -59
  127. package/src/panels/editors.ts +3 -1
  128. package/src/panels/style-panel.ts +36 -11
  129. package/src/platforms/cloud.ts +95 -18
  130. package/src/platforms/devserver.ts +47 -12
  131. package/src/publish/pages-service.ts +7 -0
  132. package/src/publish/publish-panel.ts +16 -2
  133. package/src/studio.ts +14 -2
  134. package/src/surfaces/add-repo.json +721 -224
  135. package/src/surfaces/add-repo.ts +196 -31
  136. package/src/surfaces/library-pane.ts +6 -0
  137. package/src/surfaces/rail.json +126 -73
  138. package/src/surfaces/rail.ts +31 -5
  139. package/src/tabs/typing.ts +31 -0
  140. package/src/types.ts +42 -3
  141. package/src/utils/edit-display.ts +30 -1
  142. package/src/utils/project-dir.ts +24 -0
  143. package/src/workspace/workspace.ts +7 -0
  144. package/dist/chunks/2020-cs2sb9t1.js +0 -4
  145. package/dist/chunks/adapter-y5gdq19c.js +0 -4
  146. package/dist/chunks/cf-settings-20dznc3j.js +0 -4
  147. package/dist/chunks/components-q037dya7.js +0 -4
  148. package/dist/chunks/content-source-t0q184js.js +0 -4
  149. package/dist/chunks/editor-j5sq149q.js +0 -4
  150. package/dist/chunks/entry-commands-fxzgcc4k.js +0 -4
  151. package/dist/chunks/file-ops-2n77759h.js +0 -4
  152. package/dist/chunks/files-fjy7zpxt.js +0 -4
  153. package/dist/chunks/format-host-wj3mf3n8.js +0 -4
  154. package/dist/chunks/fs-events-7kcgn4e9.js +0 -4
  155. package/dist/chunks/git-panel-7z2mpt2d.js +0 -4
  156. package/dist/chunks/icons-qbh8akb1.js +0 -4
  157. package/dist/chunks/iframe-host-a0zjd6xp.js +0 -4
  158. package/dist/chunks/index-m0x5ztax.js +0 -4
  159. package/dist/chunks/jsonMode-800yvbem.js +0 -13
  160. package/dist/chunks/jx-validate-zc9zp77s.js +0 -4
  161. package/dist/chunks/layers-p677nrh5.js +0 -4
  162. package/dist/chunks/layers-panel-y4w1dq90.js +0 -4
  163. package/dist/chunks/lit-html-cfn61dn7.js +0 -4
  164. package/dist/chunks/media-picker-xg7yjpz9.js +0 -4
  165. package/dist/chunks/menu-fpekzj6w.js +0 -4
  166. package/dist/chunks/monaco-buffer-nn4dtx27.js +0 -4
  167. package/dist/chunks/monaco-setup-tj13ndtm.js +0 -4
  168. package/dist/chunks/notify-ahs57a2z.js +0 -4
  169. package/dist/chunks/platform-v9xyfkbx.js +0 -4
  170. package/dist/chunks/publish-panel-xtbfgg2r.js +0 -4
  171. package/dist/chunks/quick-search-d3a6m7j3.js +0 -4
  172. package/dist/chunks/references-99ws0b0m.js +0 -4
  173. package/dist/chunks/right-panel-31vbk5xp.js +0 -4
  174. package/dist/chunks/section-registry-b2yzp9sw.js +0 -4
  175. package/dist/chunks/studio-1a99w39s.js +0 -5
  176. package/dist/chunks/studio-3ck2a5c4.js +0 -10
  177. package/dist/chunks/studio-4fde4a0x.js +0 -5
  178. package/dist/chunks/studio-4jj9gme2.js +0 -6
  179. package/dist/chunks/studio-4m50vh74.js +0 -16
  180. package/dist/chunks/studio-4xppwncr.js +0 -4
  181. package/dist/chunks/studio-5d7wfmnp.js +0 -5
  182. package/dist/chunks/studio-5e9zsamh.js +0 -9
  183. package/dist/chunks/studio-5gs9th4b.js +0 -5
  184. package/dist/chunks/studio-7qvs7v0m.js +0 -5
  185. package/dist/chunks/studio-84rtc120.js +0 -5
  186. package/dist/chunks/studio-aekhcdzw.js +0 -7
  187. package/dist/chunks/studio-aqmph6k4.js +0 -5
  188. package/dist/chunks/studio-c6zdm25z.js +0 -9
  189. package/dist/chunks/studio-capk0583.js +0 -9
  190. package/dist/chunks/studio-chdmwwzw.js +0 -5
  191. package/dist/chunks/studio-cp1gbff9.js +0 -14
  192. package/dist/chunks/studio-cqmtbg2p.js +0 -5
  193. package/dist/chunks/studio-e47215hh.js +0 -5
  194. package/dist/chunks/studio-fbx6twps.js +0 -6
  195. package/dist/chunks/studio-fthwn4kf.js +0 -5
  196. package/dist/chunks/studio-g8dsj3d2.js +0 -6
  197. package/dist/chunks/studio-gjknvzam.js +0 -9
  198. package/dist/chunks/studio-hjbg8d5y.js +0 -12
  199. package/dist/chunks/studio-nbxqcgvf.js +0 -5
  200. package/dist/chunks/studio-t1eedsyx.js +0 -4
  201. package/dist/chunks/studio-vavmwshr.js +0 -5
  202. package/dist/chunks/studio-x5g1xk77.js +0 -5
  203. package/dist/chunks/studio-x92tgnee.js +0 -64
  204. package/dist/chunks/studio-xg1942as.js +0 -5
  205. package/dist/chunks/studio-zev8ssp1.js +0 -5
  206. package/dist/chunks/tsMode-k9s9fcag.js +0 -14
  207. package/dist/chunks/workspace-5vmgsagj.js +0 -4
  208. package/dist/chunks/ws-client-0ja08haa.js +0 -4
@@ -3,29 +3,36 @@
3
3
  * The repository picker as a mounted document.
4
4
  *
5
5
  * `new-project/add-repo-modal.ts` is the flow — it asks the platform which repositories the account
6
- * link reaches, decides which of them Open Project may offer, adopts the one that was chosen and
6
+ * link reaches, which projects a chosen repository holds, adopts the one that was confirmed and
7
7
  * resolves the promise its two entry points return — and this is the surface it draws into: the
8
- * reactive scope the document reads, the one discriminant it branches on, and the mount that lives
9
- * in the dialog layer until the flow takes it down.
8
+ * reactive scope the document reads, the discriminants it branches on, and the mount that lives in
9
+ * the dialog layer until the flow takes it down.
10
+ *
11
+ * **Two fields, each with the list it drives.** The filter drives the repository list and the
12
+ * project-folder field drives the list of folders that repository holds a `project.json` in. A
13
+ * `jx-listbox` never takes focus (ui.md §5.3), so a list a reader can only click is a list a
14
+ * keyboard cannot reach; pairing each list with a field is what makes both of them operable from
15
+ * the keys — the palette's shape, twice. The folder field is also the answer when a list cannot be:
16
+ * a repository too large to scan, or a platform with no way to scan one.
10
17
  *
11
18
  * **The flow states facts; the surface derives what the document branches on.** Which repositories
12
- * are visible, what each row says and whether the listing has landed are the flow's; whether that
13
- * adds up to a list, a spinner or one of four different absences is {@link derive}'s, so neither
14
- * the flow nor the document has to know that "nothing matched your filter" and "the App cannot see
15
- * anything" are two sentences with two different remedies.
19
+ * are visible, which one is chosen and which folders it holds are the flow's; whether that adds up
20
+ * to a list, a spinner or one of four different absences, which row id each listbox is told is
21
+ * active, and whether the dialog can be answered yet are {@link derive}'s.
16
22
  *
17
23
  * **Await the ELEMENT, not just the mount.** `mountSurface` resolving means the DOCUMENT rendered;
18
24
  * a `jx-dialog`'s own template is one `connectedCallback` later, and `showModal` in between finds
19
25
  * no `<dialog>` to open — the body is all there, correctly styled, and never shows. `whenReady` is
20
26
  * shared with `surfaces/dialog.ts` and `surfaces/cf-account-picker.ts` rather than reimplemented.
21
27
  *
22
- * @docs studio/projects/create
28
+ * @docs studio/interface/welcome-screen
23
29
  */
24
30
 
25
31
  import { reactive } from "../reactivity";
26
32
  import { mountSurface, registerSurface } from "../ui/surface";
27
33
  import { overlayRegion, REGION_ATTR } from "../ui/regions";
28
34
  import { close as closeDialog, showModal } from "@jxsuite/ui/behaviors/dialog";
35
+ import { focusField } from "@jxsuite/ui/behaviors/textfield";
29
36
  import { whenReady } from "./dialog";
30
37
  import addRepoDoc from "./add-repo.json";
31
38
  import type { SurfaceHandle } from "../ui/surface";
@@ -35,17 +42,23 @@ registerSurface("add-repo", addRepoDoc as unknown as JxDocument);
35
42
 
36
43
  /** One repository, as the list draws it. Every field is already a string or a flag. */
37
44
  export interface AddRepoRow {
38
- /** `owner/name`, which is the row's key, its title and the text it leads with. */
45
+ /** `owner/name`, which is the row's key, its value, its title and the text it leads with. */
39
46
  fullName: string;
40
- /** The branch and permission line beside the name. */
47
+ /** The default branch, beside the name. */
41
48
  meta: string;
42
49
  /** The repository already carries the Jx topic, so it gets the badge. */
43
50
  isJx: boolean;
44
51
  isPrivate: boolean;
45
- /** This row is the one being adopted, so it says so. */
46
- importing: boolean;
47
- /** Any adoption is in flight, so no row may start another. */
48
- disabled: boolean;
52
+ }
53
+
54
+ /** One folder of the chosen repository that holds a `project.json`. */
55
+ export interface AddRepoLocation {
56
+ /** The folder, repository-relative and canonical; `""` is the root. The row's value. */
57
+ dir: string;
58
+ /** What the row says: the project's own name, else the folder's. */
59
+ label: string;
60
+ /** Where it is: `/` for the root, `sites/blog/` for a folder. */
61
+ path: string;
49
62
  }
50
63
 
51
64
  /** One way to widen what the GitHub App can reach. */
@@ -55,24 +68,50 @@ export interface AddRepoAccessLink {
55
68
  title: string;
56
69
  }
57
70
 
71
+ /** One entry of the account filter. `""` is every account. */
72
+ export interface AddRepoOwner {
73
+ value: string;
74
+ label: string;
75
+ }
76
+
58
77
  /**
59
- * Why the list is empty, when it is.
78
+ * Why the repository list is empty, when it is.
60
79
  *
61
80
  * Four, not one, because only two of them are the reader's to fix and each is fixed differently: a
62
81
  * filter is cleared, write access is asked for, and an App that was never installed is installed.
63
82
  */
64
83
  export type AddRepoEmptyState = "filter" | "write" | "install" | "none";
65
84
 
85
+ /**
86
+ * What the detail pane knows about the chosen repository's projects.
87
+ *
88
+ * - `none`: no repository is chosen yet.
89
+ * - `loading`: the platform is looking.
90
+ * - `listed`: it found at least one folder with a `project.json`.
91
+ * - `empty`: it looked and found none.
92
+ * - `unlisted`: the platform cannot look, so the folder is the reader's to name.
93
+ * - `failed`: the look itself failed; the folder field still works.
94
+ */
95
+ export type AddRepoDetailState = "none" | "loading" | "listed" | "empty" | "unlisted" | "failed";
96
+
66
97
  /** What the flow knows. Every one of these may change while the dialog is up. */
67
98
  export interface AddRepoView {
68
99
  /** The headline, which is also the dialog's accessible name: the two flows name themselves. */
69
100
  title: string;
101
+ /** The primary answer: Open or Add, and the progressive form while an adoption runs. */
102
+ confirmLabel: string;
70
103
  /** The filter text, held by the flow so a repaint cannot lose what the reader typed. */
71
104
  filter: string;
105
+ /** The account the list is narrowed to; `""` is every account. */
106
+ owner: string;
107
+ /** The account filter's entries, "All accounts" first; empty hides it (one account, or none). */
108
+ owners: AddRepoOwner[];
72
109
  /** The listing is in flight. Its own state, because "not yet asked" is not "reaches nothing". */
73
110
  loading: boolean;
74
111
  /** The repositories to draw, already filtered and ordered. */
75
112
  rows: AddRepoRow[];
113
+ /** The chosen repository's full name; `""` when none is. It may be filtered out of `rows`. */
114
+ selected: string;
76
115
  /** Which absence to say, read only when `rows` is empty and the listing has landed. */
77
116
  emptyState: AddRepoEmptyState;
78
117
  /** Where the GitHub App is installed from, for the sentence that offers to install it. */
@@ -81,6 +120,30 @@ export interface AddRepoView {
81
120
  failure: string;
82
121
  /** The access footer's links; empty for a platform with no account status to widen. */
83
122
  access: AddRepoAccessLink[];
123
+ /** The chosen repository's projects, as far as the platform has looked. */
124
+ detailState: AddRepoDetailState;
125
+ /** The chosen repository's full name and the line beside it, for the pane's heading. */
126
+ detailName: string;
127
+ detailMeta: string;
128
+ /** The folders the chosen repository holds a `project.json` in. */
129
+ locations: AddRepoLocation[];
130
+ /** The folder field's text: the folder the dialog will open, `""` for the repository root. */
131
+ folder: string;
132
+ /**
133
+ * The same folder in its canonical spelling (`utils/project-dir.ts`), which is what a folder row
134
+ * is matched against: `sites/blog/` typed by hand is the `sites/blog` row.
135
+ */
136
+ chosenFolder: string;
137
+ /** Why the folder field's text cannot be opened; `""` when it can. */
138
+ folderError: string;
139
+ /** The sentence under the folder field. */
140
+ folderHelp: string;
141
+ /** What confirming will do, said in one line; `""` when nothing is chosen. */
142
+ summary: string;
143
+ /** The dialog can be answered: a repository is chosen, its folder is usable, nothing is running. */
144
+ canConfirm: boolean;
145
+ /** An adoption is in flight. */
146
+ busy: boolean;
84
147
  }
85
148
 
86
149
  export interface AddRepoSurfaceOptions {
@@ -90,8 +153,22 @@ export interface AddRepoSurfaceOptions {
90
153
  view: AddRepoView;
91
154
  /** The reader typed in the filter field. The flow decides what it hides. */
92
155
  onFilter: (value: string) => void;
93
- /** A row was pressed: this repository, please. */
94
- onChoose: (fullName: string) => void;
156
+ /** The reader narrowed the list to one account, or widened it to all of them (`""`). */
157
+ onOwner: (value: string) => void;
158
+ /** A repository row was chosen, by pointer or by the filter's arrows. */
159
+ onSelect: (fullName: string) => void;
160
+ /** The caret moved by `delta` rows through the visible repositories. */
161
+ onMove: (delta: number) => void;
162
+ /** The reader typed in the folder field. */
163
+ onFolder: (value: string) => void;
164
+ /** A folder row was chosen. */
165
+ onLocation: (dir: string) => void;
166
+ /** The caret moved by `delta` rows through the chosen repository's folders. */
167
+ onMoveLocation: (delta: number) => void;
168
+ /** Open what is chosen: the confirm button, Enter in either field, or a double-click on a row. */
169
+ onConfirm: () => void;
170
+ /** A repository row was double-clicked: choose it and open it as soon as that is unambiguous. */
171
+ onOpenRepo: (fullName: string) => void;
95
172
  /** Read the listing again, after access was granted in another tab. */
96
173
  onRefresh: () => void;
97
174
  /** The dialog closed, for any reason the platform owns as well as this module's `close`. */
@@ -109,43 +186,101 @@ export interface AddRepoSurfaceHandle {
109
186
  close: () => void;
110
187
  }
111
188
 
189
+ /** A repository row with the id its listbox addresses it by. */
190
+ interface DrawnRow extends AddRepoRow {
191
+ id: string;
192
+ }
193
+
194
+ /** A folder row with the id its listbox addresses it by. */
195
+ interface DrawnLocation extends AddRepoLocation {
196
+ id: string;
197
+ }
198
+
112
199
  /** What the document discriminates on, and the flags it reads. Derived, never handed in. */
113
200
  interface AddRepoFlags {
114
201
  /**
115
- * Which body to draw.
202
+ * Which body the repository pane draws.
116
203
  *
117
204
  * Six, because a spinner, a list and four absences are six different things to say and only one
118
205
  * of them is fixed by looking again.
119
206
  */
120
207
  listState: "loading" | "listed" | "empty-filter" | "empty-write" | "empty-install" | "empty-none";
121
- /** Whether a refusal sits above the list that survived it. */
208
+ rows: DrawnRow[];
209
+ locations: DrawnLocation[];
210
+ /** The id each listbox is told is active: one string per list (ui.md §5.3); `""` for none. */
211
+ activeRepoId: string;
212
+ activeLocationId: string;
213
+ /** Whether there is more than one account to narrow by. */
214
+ hasOwners: boolean;
215
+ /** Whether a refusal sits above the panes that survived it. */
122
216
  hasFailure: boolean;
123
217
  /** Whether there is anything to offer in the access footer. */
124
218
  hasAccess: boolean;
125
- /** Refresh cannot re-ask while the answer is still coming, or while a row is being adopted. */
219
+ /** Whether a repository is chosen, so the folder field has something to name a folder of. */
220
+ hasDetail: boolean;
221
+ /** Whether the folder field's text is refused. */
222
+ folderInvalid: boolean;
223
+ /** Refresh cannot re-ask while the answer is still coming, or while an adoption runs. */
126
224
  refreshDisabled: boolean;
225
+ /** The confirm button's state, the inverse of `canConfirm`. */
226
+ confirmDisabled: boolean;
127
227
  }
128
228
 
129
- interface AddRepoScope extends Record<string, unknown>, AddRepoView, AddRepoFlags {
229
+ interface AddRepoScope
230
+ extends Record<string, unknown>, Omit<AddRepoView, "rows" | "locations">, AddRepoFlags {
130
231
  setFilter: (value: string) => void;
131
- choose: (fullName: string) => void;
232
+ setOwner: (value: string) => void;
233
+ select: (fullName: string) => void;
234
+ move: (delta: number) => void;
235
+ setFolder: (value: string) => void;
236
+ pickLocation: (dir: string) => void;
237
+ moveLocation: (delta: number) => void;
238
+ confirm: () => void;
239
+ openRepo: (fullName: string) => void;
132
240
  refresh: () => void;
133
241
  closed: () => void;
134
242
  }
135
243
 
244
+ /** The id the repository list addresses row `index` by. */
245
+ export function repoRowId(index: number): string {
246
+ return `add-repo-repo-${index}`;
247
+ }
248
+
249
+ /** The id the folder list addresses row `index` by. */
250
+ export function locationRowId(index: number): string {
251
+ return `add-repo-location-${index}`;
252
+ }
253
+
136
254
  /** The view plus the flags the document renders from. */
137
- function derive(view: AddRepoView): AddRepoView & AddRepoFlags {
138
- const importing = view.rows.some((row) => row.importing);
255
+ function derive(view: AddRepoView): Omit<AddRepoView, "rows" | "locations"> & AddRepoFlags {
256
+ const rows = view.rows.map((row, index) => ({ ...row, id: repoRowId(index) }));
257
+ const locations = view.locations.map((location, index) => ({
258
+ ...location,
259
+ id: locationRowId(index),
260
+ }));
261
+ const activeRow = rows.find((row) => row.fullName === view.selected);
262
+ const activeLocation =
263
+ view.folderError === ""
264
+ ? locations.find((location) => location.dir === view.chosenFolder)
265
+ : undefined;
139
266
  return {
140
267
  ...view,
268
+ activeLocationId: activeLocation?.id ?? "",
269
+ activeRepoId: activeRow?.id ?? "",
270
+ confirmDisabled: !view.canConfirm,
271
+ folderInvalid: view.folderError !== "",
141
272
  hasAccess: view.access.length > 0,
273
+ hasDetail: view.detailState !== "none",
142
274
  hasFailure: view.failure !== "",
275
+ hasOwners: view.owners.length > 0,
143
276
  listState: view.loading
144
277
  ? "loading"
145
- : view.rows.length > 0
278
+ : rows.length > 0
146
279
  ? "listed"
147
280
  : (`empty-${view.emptyState}` as AddRepoFlags["listState"]),
148
- refreshDisabled: view.loading || importing,
281
+ locations,
282
+ refreshDisabled: view.loading || view.busy,
283
+ rows,
149
284
  };
150
285
  }
151
286
 
@@ -177,21 +312,44 @@ export function openAddRepoSurface(options: AddRepoSurfaceOptions): AddRepoSurfa
177
312
 
178
313
  const scope = reactive({
179
314
  ...derive(options.view),
180
- choose: (fullName: string) => {
181
- options.onChoose(fullName);
182
- },
183
315
  closed: finish,
316
+ confirm: () => {
317
+ options.onConfirm();
318
+ },
319
+ move: (delta: number) => {
320
+ options.onMove(delta);
321
+ },
322
+ moveLocation: (delta: number) => {
323
+ options.onMoveLocation(delta);
324
+ },
325
+ openRepo: (fullName: string) => {
326
+ options.onOpenRepo(fullName);
327
+ },
328
+ pickLocation: (dir: string) => {
329
+ options.onLocation(dir);
330
+ },
184
331
  refresh: () => {
185
332
  options.onRefresh();
186
333
  },
334
+ select: (fullName: string) => {
335
+ options.onSelect(fullName);
336
+ },
187
337
  setFilter: (value: string) => {
188
338
  /* What the control holds, first and unconditionally. A binding only writes when the scope
189
339
  CHANGES, so a surface that decided a value without announcing the raw one would leave the
190
- field showing text the scope does not have (`studio-ui-guidelines.md` §9.3). Nothing
191
- refuses a filter today; the echo is what makes a refusal possible at all. */
340
+ field showing text the scope does not have (`studio-ui-guidelines.md` §9.3). */
192
341
  scope.filter = value;
193
342
  options.onFilter(value);
194
343
  },
344
+ setFolder: (value: string) => {
345
+ // The same echo as the filter's, for the same reason.
346
+ scope.folder = value;
347
+ options.onFolder(value);
348
+ },
349
+ setOwner: (value: string) => {
350
+ scope.owner = value;
351
+ options.onOwner(value);
352
+ },
195
353
  }) as AddRepoScope;
196
354
 
197
355
  let mounted: SurfaceHandle | null = null;
@@ -207,6 +365,13 @@ export function openAddRepoSurface(options: AddRepoSurfaceOptions): AddRepoSurfa
207
365
  return element;
208
366
  }
209
367
  showModal(element);
368
+ /* The body claims focus (studio-ui-guidelines.md §8.7): the filter is where every way of
369
+ choosing starts, and the confirm button that would otherwise take it is disabled until a
370
+ repository is chosen, which leaves the platform nothing to land on. */
371
+ const filter = element.querySelector<HTMLElement>('[part="filter"]');
372
+ if (filter) {
373
+ focusField(filter);
374
+ }
210
375
  return element;
211
376
  });
212
377
 
@@ -30,6 +30,7 @@
30
30
 
31
31
  import { reactive } from "../reactivity";
32
32
  import { mountSurface, registerSurface } from "../ui/surface";
33
+ import { placeholderBrokenImages } from "../utils/edit-display";
33
34
  import libraryDoc from "./library-pane.json";
34
35
  import type { JxDocument, JxElement } from "@jxsuite/schema/types";
35
36
  import type { JxScope } from "@jxsuite/runtime/types";
@@ -267,6 +268,10 @@ export function mountLibrarySurface(
267
268
  actions: LibraryActions,
268
269
  ): LibrarySurfaceHandle {
269
270
  host.replaceChildren();
271
+ // A file that will not load, or a preview image with nothing to bind, draws a placeholder rather
272
+ // Than the browser's broken-image glyph.
273
+ const listening = new AbortController();
274
+ placeholderBrokenImages(host, listening.signal);
270
275
  const scope = reactive<LibraryScope>({
271
276
  bannerState: "hidden",
272
277
  bannerText: "",
@@ -355,6 +360,7 @@ export function mountLibrarySurface(
355
360
  },
356
361
  dispose() {
357
362
  disposed = true;
363
+ listening.abort();
358
364
  mounted?.dispose();
359
365
  mounted = null;
360
366
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://jxsuite.com/schema/v1",
3
3
  "$id": "StudioRail",
4
- "$description": "The Navigator rail: a rendering of the panel registry. The adapter hands in `groups`, every rail-able panel grouped by level with its selected state and badge, and `settingsVisible`/`settingsOpen` for the foot's gear. Nothing here is a list of panels. Each button prints its record's short `railLabel` under the glyph, falling back to `title`, and carries the full `title` as its accessible name and tooltip: the visible words lead the name a reader hears (WCAG 2.5.3), and the tooltip restores what a short label leaves out.",
4
+ "$description": "The Navigator rail: a rendering of the panel registry. The adapter hands in `groups`, every rail-able panel grouped by level with its selected state and badge, and, for the foot, `libraryVisible`/`libraryOpen` for the Library button and `settingsVisible`/`settingsOpen` for the gear. Nothing here is a list of panels. Each button prints its record's short `railLabel` under the glyph, falling back to `title`, and carries the full `title` as its accessible name and tooltip: the visible words lead the name a reader hears (WCAG 2.5.3), and the tooltip restores what a short label leaves out.",
5
5
  "tagName": "div",
6
6
  "attributes": {
7
7
  "part": "rail"
@@ -162,94 +162,147 @@
162
162
  "attributes": {
163
163
  "part": "foot"
164
164
  },
165
- "$switch": {
166
- "$ref": "#/state/settingsVisible"
167
- },
168
- "cases": {
169
- "true": {
170
- "tagName": "jx-action-button",
171
- "$props": {
172
- "label": "Settings",
173
- "hint": "Settings",
174
- "icon": "gear",
175
- "stacked": true,
176
- "haspopup": "menu",
177
- "expanded": {
178
- "$ref": "#/state/settingsOpen"
179
- }
165
+ "children": [
166
+ {
167
+ "$switch": {
168
+ "$ref": "#/state/libraryVisible"
180
169
  },
170
+ "tagName": "div",
181
171
  "attributes": {
182
- "data-rail-menu": "settings",
183
- "data-expanded": "${state.settingsOpen ? '' : null}"
172
+ "part": "foot-library"
184
173
  },
185
- "onclick": {
186
- "$prototype": "Function",
187
- "body": [
188
- {
189
- "operator": "call",
190
- "target": {
191
- "$ref": "#/state/openSettings"
192
- },
193
- "value": [
174
+ "cases": {
175
+ "true": {
176
+ "tagName": "jx-action-button",
177
+ "$props": {
178
+ "label": "Library",
179
+ "hint": "Library",
180
+ "icon": "books",
181
+ "stacked": true,
182
+ "selected": {
183
+ "$ref": "#/state/libraryOpen"
184
+ }
185
+ },
186
+ "attributes": {
187
+ "data-rail-action": "library.open"
188
+ },
189
+ "onclick": {
190
+ "$prototype": "Function",
191
+ "body": [
194
192
  {
195
- "$ref": "event#/currentTarget"
193
+ "operator": "call",
194
+ "target": {
195
+ "$ref": "#/state/openLibrary"
196
+ },
197
+ "value": []
196
198
  }
197
199
  ]
198
- }
199
- ]
200
+ },
201
+ "children": [
202
+ {
203
+ "tagName": "span",
204
+ "textContent": "Library"
205
+ }
206
+ ]
207
+ }
208
+ }
209
+ },
210
+ {
211
+ "tagName": "div",
212
+ "attributes": {
213
+ "part": "foot-settings"
200
214
  },
201
- "onkeydown": {
202
- "$prototype": "Function",
203
- "body": [
204
- {
205
- "$switch": {
206
- "$ref": "event#/key"
207
- },
208
- "cases": {
209
- "ArrowUp": [
210
- {
211
- "operator": "call",
212
- "target": {
213
- "$ref": "#/state/openSettings"
214
- },
215
- "value": [
215
+ "$switch": {
216
+ "$ref": "#/state/settingsVisible"
217
+ },
218
+ "cases": {
219
+ "true": {
220
+ "tagName": "jx-action-button",
221
+ "$props": {
222
+ "label": "Settings",
223
+ "hint": "Settings",
224
+ "icon": "gear",
225
+ "stacked": true,
226
+ "haspopup": "menu",
227
+ "expanded": {
228
+ "$ref": "#/state/settingsOpen"
229
+ }
230
+ },
231
+ "attributes": {
232
+ "data-rail-menu": "settings",
233
+ "data-expanded": "${state.settingsOpen ? '' : null}"
234
+ },
235
+ "onclick": {
236
+ "$prototype": "Function",
237
+ "body": [
238
+ {
239
+ "operator": "call",
240
+ "target": {
241
+ "$ref": "#/state/openSettings"
242
+ },
243
+ "value": [
244
+ {
245
+ "$ref": "event#/currentTarget"
246
+ }
247
+ ]
248
+ }
249
+ ]
250
+ },
251
+ "onkeydown": {
252
+ "$prototype": "Function",
253
+ "body": [
254
+ {
255
+ "$switch": {
256
+ "$ref": "event#/key"
257
+ },
258
+ "cases": {
259
+ "ArrowUp": [
260
+ {
261
+ "operator": "call",
262
+ "target": {
263
+ "$ref": "#/state/openSettings"
264
+ },
265
+ "value": [
266
+ {
267
+ "$ref": "event#/currentTarget"
268
+ }
269
+ ]
270
+ },
216
271
  {
217
- "$ref": "event#/currentTarget"
272
+ "preventDefault": true
218
273
  }
219
- ]
220
- },
221
- {
222
- "preventDefault": true
223
- }
224
- ],
225
- "ArrowDown": [
226
- {
227
- "operator": "call",
228
- "target": {
229
- "$ref": "#/state/openSettings"
230
- },
231
- "value": [
274
+ ],
275
+ "ArrowDown": [
276
+ {
277
+ "operator": "call",
278
+ "target": {
279
+ "$ref": "#/state/openSettings"
280
+ },
281
+ "value": [
282
+ {
283
+ "$ref": "event#/currentTarget"
284
+ }
285
+ ]
286
+ },
232
287
  {
233
- "$ref": "event#/currentTarget"
288
+ "preventDefault": true
234
289
  }
235
290
  ]
236
- },
237
- {
238
- "preventDefault": true
239
291
  }
240
- ]
292
+ }
293
+ ]
294
+ },
295
+ "children": [
296
+ {
297
+ "tagName": "span",
298
+ "textContent": "Settings"
241
299
  }
242
- }
243
- ]
244
- },
245
- "children": [
246
- {
247
- "tagName": "span",
248
- "textContent": "Settings"
300
+ ]
249
301
  }
250
- ]
302
+ }
251
303
  }
252
- }
304
+ ],
305
+ "$description": "The two rail buttons that open no Navigator panel: the Library, which is a canvas tab rather than a panel and so sits with the other exception, above the gear."
253
306
  }
254
307
  ]
255
308
  }