@liberation-data/desk 0.1.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 (172) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +4 -0
  3. package/README.md +436 -0
  4. package/dist/core/commands.d.ts +40 -0
  5. package/dist/core/commands.d.ts.map +1 -0
  6. package/dist/core/commands.js +92 -0
  7. package/dist/core/commands.js.map +1 -0
  8. package/dist/core/desk.d.ts +46 -0
  9. package/dist/core/desk.d.ts.map +1 -0
  10. package/dist/core/desk.js +178 -0
  11. package/dist/core/desk.js.map +1 -0
  12. package/dist/core/events.d.ts +42 -0
  13. package/dist/core/events.d.ts.map +1 -0
  14. package/dist/core/events.js +54 -0
  15. package/dist/core/events.js.map +1 -0
  16. package/dist/core/index.d.ts +13 -0
  17. package/dist/core/index.d.ts.map +1 -0
  18. package/dist/core/index.js +7 -0
  19. package/dist/core/index.js.map +1 -0
  20. package/dist/core/location.d.ts +30 -0
  21. package/dist/core/location.d.ts.map +1 -0
  22. package/dist/core/location.js +99 -0
  23. package/dist/core/location.js.map +1 -0
  24. package/dist/core/shortcuts.d.ts +36 -0
  25. package/dist/core/shortcuts.d.ts.map +1 -0
  26. package/dist/core/shortcuts.js +73 -0
  27. package/dist/core/shortcuts.js.map +1 -0
  28. package/dist/core/titles.d.ts +3 -0
  29. package/dist/core/titles.d.ts.map +1 -0
  30. package/dist/core/titles.js +26 -0
  31. package/dist/core/titles.js.map +1 -0
  32. package/dist/core/types.d.ts +51 -0
  33. package/dist/core/types.d.ts.map +1 -0
  34. package/dist/core/types.js +2 -0
  35. package/dist/core/types.js.map +1 -0
  36. package/dist/desk.css +2550 -0
  37. package/dist/react/Desktop.d.ts +24 -0
  38. package/dist/react/Desktop.d.ts.map +1 -0
  39. package/dist/react/Desktop.js +212 -0
  40. package/dist/react/Desktop.js.map +1 -0
  41. package/dist/react/Dock.d.ts +42 -0
  42. package/dist/react/Dock.d.ts.map +1 -0
  43. package/dist/react/Dock.js +103 -0
  44. package/dist/react/Dock.js.map +1 -0
  45. package/dist/react/MenuBar.d.ts +64 -0
  46. package/dist/react/MenuBar.d.ts.map +1 -0
  47. package/dist/react/MenuBar.js +245 -0
  48. package/dist/react/MenuBar.js.map +1 -0
  49. package/dist/react/appFrame.d.ts +37 -0
  50. package/dist/react/appFrame.d.ts.map +1 -0
  51. package/dist/react/appFrame.js +119 -0
  52. package/dist/react/appFrame.js.map +1 -0
  53. package/dist/react/arrange.d.ts +24 -0
  54. package/dist/react/arrange.d.ts.map +1 -0
  55. package/dist/react/arrange.js +61 -0
  56. package/dist/react/arrange.js.map +1 -0
  57. package/dist/react/commands.d.ts +23 -0
  58. package/dist/react/commands.d.ts.map +1 -0
  59. package/dist/react/commands.js +41 -0
  60. package/dist/react/commands.js.map +1 -0
  61. package/dist/react/context.d.ts +25 -0
  62. package/dist/react/context.d.ts.map +1 -0
  63. package/dist/react/context.js +28 -0
  64. package/dist/react/context.js.map +1 -0
  65. package/dist/react/controls.d.ts +111 -0
  66. package/dist/react/controls.d.ts.map +1 -0
  67. package/dist/react/controls.js +83 -0
  68. package/dist/react/controls.js.map +1 -0
  69. package/dist/react/conversation.d.ts +41 -0
  70. package/dist/react/conversation.d.ts.map +1 -0
  71. package/dist/react/conversation.js +73 -0
  72. package/dist/react/conversation.js.map +1 -0
  73. package/dist/react/dnd.d.ts +51 -0
  74. package/dist/react/dnd.d.ts.map +1 -0
  75. package/dist/react/dnd.js +138 -0
  76. package/dist/react/dnd.js.map +1 -0
  77. package/dist/react/dragContext.d.ts +32 -0
  78. package/dist/react/dragContext.d.ts.map +1 -0
  79. package/dist/react/dragContext.js +14 -0
  80. package/dist/react/dragContext.js.map +1 -0
  81. package/dist/react/events.d.ts +41 -0
  82. package/dist/react/events.d.ts.map +1 -0
  83. package/dist/react/events.js +91 -0
  84. package/dist/react/events.js.map +1 -0
  85. package/dist/react/index.d.ts +43 -0
  86. package/dist/react/index.d.ts.map +1 -0
  87. package/dist/react/index.js +22 -0
  88. package/dist/react/index.js.map +1 -0
  89. package/dist/react/layers.d.ts +5 -0
  90. package/dist/react/layers.d.ts.map +1 -0
  91. package/dist/react/layers.js +65 -0
  92. package/dist/react/layers.js.map +1 -0
  93. package/dist/react/overlays.d.ts +67 -0
  94. package/dist/react/overlays.d.ts.map +1 -0
  95. package/dist/react/overlays.js +98 -0
  96. package/dist/react/overlays.js.map +1 -0
  97. package/dist/react/popup.d.ts +21 -0
  98. package/dist/react/popup.d.ts.map +1 -0
  99. package/dist/react/popup.js +94 -0
  100. package/dist/react/popup.js.map +1 -0
  101. package/dist/react/search.d.ts +33 -0
  102. package/dist/react/search.d.ts.map +1 -0
  103. package/dist/react/search.js +82 -0
  104. package/dist/react/search.js.map +1 -0
  105. package/dist/react/setupProgress.d.ts +35 -0
  106. package/dist/react/setupProgress.d.ts.map +1 -0
  107. package/dist/react/setupProgress.js +68 -0
  108. package/dist/react/setupProgress.js.map +1 -0
  109. package/dist/react/shell.d.ts +17 -0
  110. package/dist/react/shell.d.ts.map +1 -0
  111. package/dist/react/shell.js +13 -0
  112. package/dist/react/shell.js.map +1 -0
  113. package/dist/react/sidebar.d.ts +23 -0
  114. package/dist/react/sidebar.d.ts.map +1 -0
  115. package/dist/react/sidebar.js +26 -0
  116. package/dist/react/sidebar.js.map +1 -0
  117. package/dist/react/table.d.ts +39 -0
  118. package/dist/react/table.d.ts.map +1 -0
  119. package/dist/react/table.js +86 -0
  120. package/dist/react/table.js.map +1 -0
  121. package/dist/react/tasks.d.ts +41 -0
  122. package/dist/react/tasks.d.ts.map +1 -0
  123. package/dist/react/tasks.js +79 -0
  124. package/dist/react/tasks.js.map +1 -0
  125. package/dist/react/tour.d.ts +42 -0
  126. package/dist/react/tour.d.ts.map +1 -0
  127. package/dist/react/tour.js +68 -0
  128. package/dist/react/tour.js.map +1 -0
  129. package/dist/react/windowBoundary.d.ts +22 -0
  130. package/dist/react/windowBoundary.d.ts.map +1 -0
  131. package/dist/react/windowBoundary.js +50 -0
  132. package/dist/react/windowBoundary.js.map +1 -0
  133. package/dist/react/wizard.d.ts +41 -0
  134. package/dist/react/wizard.d.ts.map +1 -0
  135. package/dist/react/wizard.js +60 -0
  136. package/dist/react/wizard.js.map +1 -0
  137. package/llms.txt +410 -0
  138. package/package.json +64 -0
  139. package/src/core/commands.ts +142 -0
  140. package/src/core/desk.ts +229 -0
  141. package/src/core/events.ts +99 -0
  142. package/src/core/index.ts +43 -0
  143. package/src/core/location.ts +127 -0
  144. package/src/core/shortcuts.ts +119 -0
  145. package/src/core/titles.ts +28 -0
  146. package/src/core/types.ts +49 -0
  147. package/src/desk.css +2550 -0
  148. package/src/react/Desktop.tsx +349 -0
  149. package/src/react/Dock.tsx +258 -0
  150. package/src/react/MenuBar.tsx +350 -0
  151. package/src/react/appFrame.tsx +183 -0
  152. package/src/react/arrange.ts +82 -0
  153. package/src/react/commands.ts +60 -0
  154. package/src/react/context.tsx +55 -0
  155. package/src/react/controls.tsx +364 -0
  156. package/src/react/conversation.tsx +208 -0
  157. package/src/react/dnd.tsx +189 -0
  158. package/src/react/dragContext.tsx +60 -0
  159. package/src/react/events.tsx +149 -0
  160. package/src/react/index.ts +64 -0
  161. package/src/react/layers.ts +66 -0
  162. package/src/react/overlays.tsx +254 -0
  163. package/src/react/popup.tsx +176 -0
  164. package/src/react/search.tsx +177 -0
  165. package/src/react/setupProgress.ts +113 -0
  166. package/src/react/shell.tsx +27 -0
  167. package/src/react/sidebar.tsx +84 -0
  168. package/src/react/table.tsx +176 -0
  169. package/src/react/tasks.tsx +176 -0
  170. package/src/react/tour.tsx +183 -0
  171. package/src/react/windowBoundary.tsx +110 -0
  172. package/src/react/wizard.tsx +150 -0
package/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/NOTICE ADDED
@@ -0,0 +1,4 @@
1
+ desk
2
+ Copyright 2026 Jasper Blues
3
+
4
+ This product is licensed under the Apache License, Version 2.0.
package/README.md ADDED
@@ -0,0 +1,436 @@
1
+ # desk
2
+
3
+ A desktop for the web, in React: windows that fill the desk, layered, and arrange when asked.
4
+
5
+ ```bash
6
+ npm install @liberation-data/desk
7
+ ```
8
+
9
+ ## Window manager defaults
10
+
11
+ Every window opens filling the desk, over the ones already open, and keeps filling it when the browser
12
+ resizes. Nothing is laid out behind anyone's back.
13
+
14
+ **Window → Arrange** (⌥⌘A) lays the open windows out once: two side by side, three one tall and two stacked,
15
+ four a grid. After that they are ordinary windows. Move or resize one and only that one changes.
16
+
17
+ Drag a window by its title bar to free it where it sits. Drop it against the left or right edge and it takes
18
+ that half. Resize from the sides, the bottom or the corner. Double-click the title bar, or press the green
19
+ control, to fill the desk and back.
20
+
21
+ ## Use it
22
+
23
+ ```tsx
24
+ import { createDesk } from '@liberation-data/desk'
25
+ import { Desktop, DeskProvider } from '@liberation-data/desk/react'
26
+ import '@liberation-data/desk/desk.css'
27
+
28
+ const desk = createDesk()
29
+
30
+ export function App() {
31
+ return (
32
+ <DeskProvider desk={desk}>
33
+ <button onClick={() => desk.open('notes')}>Notes</button>
34
+ <Desktop
35
+ title={id => TITLES[id]}
36
+ renderWindow={id => SURFACES[id]}
37
+ empty={<p>Nothing open</p>}
38
+ />
39
+ </DeskProvider>
40
+ )
41
+ }
42
+ ```
43
+
44
+ Two windows onto the same thing are the same kind with different ids: `desk.openInstance('query')` gives
45
+ `query`, then `query#2`. `windowType(id)` says what to render; the whole id says which one it is.
46
+
47
+ A window's code can wait until the window opens, and each window loads and fails on its own:
48
+
49
+ ```tsx
50
+ const RoutePlanner = lazyWindow(() => import('./apps/RoutePlanner'))
51
+ ```
52
+
53
+ While it loads, that window says so and the rest of the desk carries on; if it cannot open, that window says
54
+ what went wrong and offers Reload, which tries the import again rather than remembering the failure.
55
+ `<Desktop loading={…} failed={…}>` replaces the wording.
56
+
57
+ `renderWindow` is called with a window's id. The desk never owns your content, and a window keeps its DOM
58
+ when focus moves, so scroll positions, carets and iframes survive.
59
+
60
+ ## Touch: one window at a time
61
+
62
+ `<Desktop layout="auto">` (the default) reads the device. A touch screen that cannot hover gets
63
+ `fullscreen`: the key window fills the stage, the dock stays as the way to switch, and windows are not
64
+ moved or arranged. Windows you are not looking at stay mounted — their drafts, scroll positions and carets survive —
65
+ and are simply offstage and `inert`. A tablet with a trackpad reports a fine pointer, so it gets the
66
+ desktop. Pass `layout="desktop"` or `layout="fullscreen"` to decide for yourself.
67
+
68
+ Links work the same everywhere: a link opens the same windows, shown one at a time on touch.
69
+
70
+ ## Dock
71
+
72
+ ```tsx
73
+ import { Dock, dockItem, dockSeparator, dockStack } from '@liberation-data/desk/react'
74
+
75
+ <Dock
76
+ entries={[
77
+ dockItem({ id: 'rides', label: 'Rides', icon: <RideIcon /> }),
78
+ dockItem({ id: 'service', label: 'Service', icon: <WrenchIcon />, badge: 2 }),
79
+ dockSeparator('stacks'),
80
+ dockStack({ id: 'plan', label: 'Plan', items: [routes, weather, calendar] }),
81
+ ]}
82
+ />
83
+ ```
84
+
85
+ An item opens the window with its id (or `window`), and shows a dot while that window is open. A stack fans
86
+ its items out above the dock and previews the first four icons. `onSelect` replaces opening a window.
87
+
88
+ The dock is one tab stop: arrow keys move along it, Enter opens, Escape folds a stack away and returns focus
89
+ to it. With `placement="overlay"` (the default) it floats over the bottom of its positioned parent; set
90
+ `--desk-inset-bottom` so windows stop above it.
91
+
92
+ ## Controls
93
+
94
+ ```tsx
95
+ import { Button, SegmentedControl, TextField, Toggle } from '@liberation-data/desk/react'
96
+
97
+ <Button intent="default" onClick={save}>Save ride</Button>
98
+ <SegmentedControl label="Filter by bike" options={BIKES} value={bike} onChange={setBike} />
99
+ <Toggle checked={metric} onChange={setMetric} label="Kilometres and metres" description="Off shows miles" />
100
+ <TextField label="Wheel size" value={wheel} onChange={e => setWheel(e.target.value)} error={wheelError} />
101
+ ```
102
+
103
+ Each control carries its own behaviour and accessibility; the look comes only from tokens, and there are few
104
+ style props on purpose. `intent="default"` marks the one action a view leads with. A segmented control is one
105
+ tab stop whose arrow keys move the selection past disabled options. A toggle is a `switch` for settings that
106
+ apply at once — a checkbox is for one that waits for Save. A text field always has a label (`labelHidden`
107
+ keeps it for screen readers), and an `error` marks it invalid and replaces the help text. A choice group
108
+ is the handful of cards a setup step turns on — an icon, a label, a line of why — with native radios
109
+ underneath, so it is one tab stop and the arrow keys move the choice.
110
+
111
+ ## Setup assistant
112
+
113
+ ```tsx
114
+ const steps: WizardStep[] = [
115
+ { id: 'welcome', name: 'Welcome', title: 'Welcome to the garage', continueLabel: 'Get started' },
116
+ { id: 'rider', name: 'Rider', title: 'Who is riding?', complete: name.length > 1, body: <TextField … /> },
117
+ { id: 'weather', name: 'Weather', title: 'Connect a weather service', complete: key.length > 0,
118
+ continueLabel: 'Connect', busyLabel: 'Checking the key…', onContinue: checkKey,
119
+ skip: { label: 'Set up later', onSkip: skipWeather } },
120
+ { id: 'finishing', name: 'Finishing', title: 'Setting up', working: true, complete: done, onEnter: install },
121
+ ]
122
+
123
+ <Wizard steps={steps} index={index} onIndexChange={setIndex} onFinish={start} />
124
+ ```
125
+
126
+ One pane, one question at a time, Back and Continue where the eye already is. `complete` gates Continue, so
127
+ a step cannot be passed until it is answered; `skip` is the quiet way past a step that can wait; `working`
128
+ is a step with nothing to do but wait, which offers no Back. `onContinue` is the work Continue does before
129
+ moving on — create the account, check the key: while it runs Continue shows `busyLabel` and nothing can be
130
+ pressed twice; throw to stay on the step with the message shown in it, or return `false` to stay quietly.
131
+ Focus moves to each step's heading as it arrives, so it is announced — and only when the step changes, so a
132
+ field in the pane keeps what is typed.
133
+
134
+ ```tsx
135
+ const setup = useTasks([
136
+ { id: 'profile', name: 'Save your profile', run: () => saveProfile(profile) },
137
+ { id: 'rides', name: 'Import your rides', run: async report => { for await (const n of importRides()) report(`${n} of 24`) } },
138
+ ])
139
+
140
+ { id: 'finishing', name: 'Finishing', title: 'Setting up', working: !setup.failed, complete: setup.done,
141
+ onEnter: setup.restart, body: <Checklist items={setup.items} onRetry={setup.start} /> }
142
+ ```
143
+
144
+ The jobs a setup does while someone watches run in order. Each says how it is getting on (`report`), and one
145
+ that throws stops the rest and shows why, with **Try again** picking up from the task that failed rather than
146
+ from the top. `working: !setup.failed` gives the step its Back again once something has gone wrong.
147
+
148
+ ```tsx
149
+ const progress = useSetupProgress({ key: 'garage', initial: { rider: '', bikes: [] }, store: serverStore })
150
+ if (!progress.loaded) return null
151
+ if (!progress.finished) return <Wizard index={progress.index} onIndexChange={progress.setIndex} onFinish={progress.finish} … />
152
+ ```
153
+
154
+ Setup is remembered: closing the tab on step four resumes on step four, and a finished setup is not shown
155
+ again until `reset()` ("Set up again"). The store is the app's to choose — this browser's local storage by
156
+ default, or a `SetupStore` with `load`, `save` and `clear` that talks to a server. Keep answers to what a
157
+ person would mind retyping; never a password or a key.
158
+
159
+ ## Tours
160
+
161
+ ```tsx
162
+ const TOUR: Tour = {
163
+ id: 'first-look',
164
+ name: 'What needs doing',
165
+ description: 'A minute on where the jobs are and what wore out.',
166
+ steps: [
167
+ { window: 'service', point: 'service-chain', caption: 'Service lists what the bikes need.' },
168
+ { window: 'rides', caption: 'Choose a ride in the table.', yourTurn: true, until: 'ride.selected' },
169
+ { window: 'chat', caption: 'Ask which job to do first.', yourTurn: true },
170
+ ],
171
+ }
172
+
173
+ {touring && <TourBar tour={TOUR} offer={firstRun} onFinish={done} onStop={done} />}
174
+ ```
175
+
176
+ A tour drives the desk: each step opens the window it is about and points at the control it is talking
177
+ about (`point` is a `data-tour` name or any selector), so the person watches the real app rather than
178
+ reading about it. A `yourTurn` step hands over and waits, offering Done and Skip. Give it `until` — an event
179
+ topic, or a topic and a test — and it moves on by itself when the person has done it, with no Done to press.
180
+ `offer` asks first ("Show me" or "Not now") and opens nothing until the person says yes: use it the first time
181
+ someone reaches the desktop, straight after setup, so nobody lands there without a word. The bar holds no
182
+ content: the steps are the app's.
183
+
184
+ ## Dragging between windows
185
+
186
+ ```tsx
187
+ // In the list — one source, because a hook cannot be called per row
188
+ const rides = useDragSource<Ride>({ type: 'ride' })
189
+ <Table rowProps={row => rides.dragProps(row, row.name)} … />
190
+
191
+ // In the map
192
+ const { dropProps, over, ready } = useDropTarget<Ride>({ accepts: 'ride', onDrop: ({ payload }) => draw(payload) })
193
+ <div {...dropProps}>{ready && <p>Drop a ride here</p>}</div>
194
+ ```
195
+
196
+ Built on Pointer Events, so touch and pen work the same way, the preview is drawn by us, and Escape calls
197
+ the drag off. A drag carries a `type` and a `payload` and says which window it came from; a target says which
198
+ types it takes and lights up while something acceptable is in the air. Dropping brings the window that took
199
+ it forward. `useDraggable` is the single-item form of the same thing.
200
+
201
+ ## Generated apps
202
+
203
+ A self-contained page — the kind an assistant writes when asked for an app — can run in a window and take
204
+ part like any other:
205
+
206
+ ```tsx
207
+ <AppFrame
208
+ title="Ride Card"
209
+ srcDoc={withAppBridge(generatedHtml)}
210
+ listens={['ride.selected']} // what it may hear
211
+ says={['ride.selected']} // what it may say
212
+ accepts="ride" // what may be dropped on it
213
+ opens={['map']} // windows it may ask for
214
+ />
215
+ ```
216
+
217
+ Inside the page, the bridge gives it a small `desk` object: `desk.on(topic, …)`, `desk.onDrop(…)`,
218
+ `desk.publish(topic, payload)` and `desk.open(window)`. The page runs sandboxed with no access to the host's
219
+ origin, and talks only through messages. The host grants each app its topics, drop types and windows;
220
+ anything else it sends is dropped, and messages from anywhere but its own frame are ignored.
221
+
222
+ ## Search
223
+
224
+ ```tsx
225
+ import { SearchPalette, windowResults } from '@liberation-data/desk/react'
226
+
227
+ <SearchPalette open={open} onOpenChange={setOpen} search={query => [
228
+ ...windowResults(desk, titleOf),
229
+ ...rides.filter(matching(query)).map(asResult),
230
+ ]} />
231
+ ```
232
+
233
+ The app says what the results are; the palette does the typing, grouping, keyboard and choosing. It binds
234
+ `mod+k` and answers the `desk.search` command, so a menu item can open it too (pass `shortcut={null}` when
235
+ the menu binds the key itself). An async `search` is safe: a slower answer to an older query never overtakes
236
+ a newer one.
237
+
238
+ ## Typing into the window you are in, and windows telling each other
239
+
240
+ One bar to type into, which reaches whichever window is key:
241
+
242
+ ```tsx
243
+ // In a window: take the text while this window is in front.
244
+ useWindowInput(setFilter, { placeholder: 'Filter rides by name…', target: 'Rides' })
245
+
246
+ // In the shell: the bar, and what happens when no window takes the text.
247
+ <InputBar onSubmit={askTheAssistant} fallbackPlaceholder="Ask anything…" fallbackTarget="Chat" />
248
+ ```
249
+
250
+ The bar is **summoned** (⌘J by default) and opens over the middle of the screen, the way Spotlight does;
251
+ Escape dismisses it, and sending closes it. `mode="inline"` puts it wherever you like instead. Either way it
252
+ asks the responder chain how to present itself, so its placeholder and the *goes to* line follow the key
253
+ window, and a window that registers nothing lets the fallback have the text.
254
+
255
+ Windows tell each other what happened rather than calling each other:
256
+
257
+ ```tsx
258
+ const publish = usePublish() // stamped with the window it came from
259
+ publish('ride.selected', ride)
260
+
261
+ useDeskEvent<Ride>('ride.selected', event => show(event.payload), { replay: true })
262
+ ```
263
+
264
+ Topics are dotted names; a subscriber can take a branch with `ride.*` or everything with `*`. A desk
265
+ carries its own bus, so this needs no extra provider. `replay`
266
+ delivers the last event on the topic straight away — the window that the event concerns is often opened by
267
+ that very event, and would otherwise miss it by a frame.
268
+
269
+ ## Conversation
270
+
271
+ ```tsx
272
+ import { Composer, Thread } from '@liberation-data/desk/react'
273
+
274
+ <Thread messages={messages} me="me" typing={thinking} onRetry={resend} />
275
+ <Composer value={draft} onChange={setDraft} onSubmit={send} busy={thinking} onStop={cancel} />
276
+ ```
277
+
278
+ Presentational only: no transport, and no opinion about who is answering. The thread groups a run of
279
+ messages from one author, marks yours apart, shows `sending` and `failed` states with Retry, and follows the
280
+ conversation only while you are at the end of it — scroll back and new messages become a *2 new messages*
281
+ button instead of yanking you away. The composer grows with the text, sends on Enter, keeps Shift+Enter for
282
+ a new line, never sends mid-composition in an input method, and turns Send into Stop while an answer is
283
+ arriving.
284
+
285
+ ## Lists and choices
286
+
287
+ ```tsx
288
+ <Table label="Rides" rows={rides} columns={COLUMNS} rowId={r => r.id}
289
+ selected={selected} onSelect={choose} onActivate={open}
290
+ sort={sort} onSortChange={setSort} empty={<p>No rides yet</p>} />
291
+
292
+ <Sidebar label="Bikes" sections={SECTIONS} value={bike} onChange={setBike} />
293
+
294
+ <PopUpButton label="Distance" options={UNITS} value={units} onChange={setUnits} />
295
+ <Checkbox checked={commutes} onChange={setCommutes} label="Count commutes towards wear" />
296
+ <Slider label="Warn at" value={90} onChange={setThreshold} format={v => `${v}% worn`} />
297
+ ```
298
+
299
+ A **table** renders what it is given — no paging, no fetching — and is one tab stop: arrow keys move the
300
+ selection, Enter or a double click opens a row, and a sortable heading says which way it is sorted. A
301
+ **sidebar** is navigation, so where you are is `aria-current` and stays put. A **pop-up button** is what a
302
+ segmented control becomes when there are too many options to show at once; unlike a menu it takes focus, and
303
+ typing jumps to an option. A **checkbox** is for a setting that waits for Save (a Toggle applies at once),
304
+ and can stand for a mixed set. A **slider** reads its value in words for people who cannot see it.
305
+
306
+ ## Overlays
307
+
308
+ Least interrupting to most (see [HIG.md](HIG.md) §7):
309
+
310
+ ```tsx
311
+ import { Alert, Popover, Sheet, ToastProvider, useToast } from '@liberation-data/desk/react'
312
+
313
+ <Popover open={open} onOpenChange={setOpen} label="More filters"
314
+ trigger={props => <Button {...props}>Filters</Button>}>…</Popover>
315
+
316
+ <Sheet open={exporting} onDismiss={cancel} title="Export rides" actions={…}>…</Sheet>
317
+
318
+ <Alert open={confirming} title="Clear these notes?" confirmLabel="Clear notes" destructive
319
+ onConfirm={clear} onCancel={stop} />
320
+
321
+ const toast = useToast()
322
+ toast.show({ message: 'Ride deleted', action: { label: 'Undo', onSelect: restore } })
323
+ ```
324
+
325
+ A **popover** belongs to one control and closes when you press outside. A **sheet** belongs to one window
326
+ and blocks only that window — it renders inside it, so the rest of the desk stays usable. An **alert** stops
327
+ the app and is for what cannot be undone; Cancel comes before the action, and a destructive action is never
328
+ the default button. A **toast** interrupts nothing, carries Undo, and waits while the pointer or focus is on
329
+ it. All three overlays trap focus, give it back on close, and answer Escape topmost-first.
330
+
331
+ ## Menu bar
332
+
333
+ ```tsx
334
+ import { MenuBar, menuAction, menuCommand, menuSeparator, windowMenuItems } from '@liberation-data/desk/react'
335
+
336
+ <MenuBar
337
+ leading={<AppMark />}
338
+ menus={[
339
+ { id: 'app', label: 'Garage', items: [menuAction('About Garage', showAbout), menuSeparator(),
340
+ menuCommand('Settings…', 'app.settings', { shortcut: 'mod+comma' })] },
341
+ { id: 'window', label: 'Window', items: () => [ // a function: re-read each time it opens
342
+ menuCommand('Arrange', DeskCommands.arrange),
343
+ menuSeparator(),
344
+ ...windowMenuItems(desk.getState(), desk.focus, titleOf),
345
+ ] },
346
+ ]}
347
+ status={[{ id: 'service', label: 'Service, 2 due', title: <WrenchWithBadge />, items: dueItems }]}
348
+ trailing={<Clock />}
349
+ />
350
+ ```
351
+
352
+ A `menuCommand` is enabled when something in the responder chain can perform it right now, and chooses the
353
+ same responder a shortcut would: opening a menu never takes focus from the window you were working in. Menus
354
+ own their shortcuts, as in AppKit — a command item's `shortcut` is shown and bound. Keyboard: Tab reaches
355
+ the bar, arrow keys move between menus and items (skipping separators and disabled items), a letter jumps
356
+ to an item, Enter chooses, Escape closes.
357
+
358
+ ## Commands and the responder chain
359
+
360
+ Some commands name no target — Copy, Close, Find. They go to whoever is responsible, the way AppKit's
361
+ responder chain works: the focused element, then its window, then the stage, then the app.
362
+
363
+ ```tsx
364
+ function RideList() {
365
+ const [selection, setSelection] = useState<Ride[]>([])
366
+ // Inside a window this answers when that window is key.
367
+ useCommand('edit.copy', () => copyRides(selection), { enabled: selection.length > 0 })
368
+
369
+ }
370
+
371
+ const perform = usePerform() // perform('edit.copy') → true if something handled it
372
+ const canPerform = useCanPerform() // what a menu asks before drawing the item enabled
373
+ useShortcuts(KEYMAP) // { 'mod+c': 'edit.copy', 'mod+shift+z': 'edit.redo' }
374
+ ```
375
+
376
+ `isInstalledApp()` says whether the app is running installed rather than in a tab. Only then may it take
377
+ shortcuts the browser owns: ⌘W for close window and ⌘\` for next window, and never ⌘T or ⌘N at all.
378
+
379
+ The chain is the DOM: commands travel as events that bubble from the focused element, so there is no second
380
+ tree to keep in step. The first responder that implements a command decides — if it is disabled, the
381
+ command stops there. `mod` means ⌘ on Apple devices and Ctrl elsewhere; a shortcut nothing handles leaves
382
+ the browser's default alone. The desk answers `DeskCommands` (close, zoom, arrange, next and previous
383
+ window) at the stage, and any window can override them.
384
+
385
+ ## The core has no React in it
386
+
387
+ `createDesk()` returns a plain object. Anything can drive it — a router, a tour, a keyboard shortcut, a
388
+ test:
389
+
390
+ ```ts
391
+ const { open, focus, close, float, fill, toggleMode, closeAll, subscribe, getState } = createDesk()
392
+ ```
393
+
394
+ State is immutable. `windows` is the order windows were opened, which is the order Arrange lays them out,
395
+ so focusing never reshuffles them. `stack` is back to front; `focusedId(state)` is its last entry.
396
+
397
+ ## Links and Back
398
+
399
+ ```ts
400
+ import { syncWithLocation } from '@liberation-data/desk'
401
+
402
+ const stop = syncWithLocation(desk, () => stageSize(), { isKnown: id => id in SURFACES })
403
+ ```
404
+
405
+ The hash carries which windows are open, which float, and which has focus — `#w=notes,clock~&f=clock` —
406
+ but never positions: a shared link opens the same things, not someone else's layout on a different
407
+ screen. Opening or closing a window pushes a history entry, so Back closes it; focus and mode changes
408
+ replace the entry. Other hash parameters are left alone.
409
+
410
+ ## Theming
411
+
412
+ Every value in `desk.css` reads a `--desk-*` custom property. Redefine the tokens; don't override the
413
+ selectors. Light and dark follow `prefers-color-scheme`, and `data-theme="light|dark"` on the root wins.
414
+
415
+ ## Docs
416
+
417
+ - [docs/guide.md](docs/guide.md) — building an app with it, and what catches people out
418
+ - [llms.txt](llms.txt) — the whole public surface in one file, for a coding agent
419
+ - [HIG.md](HIG.md) — how an app built with it should look and behave
420
+
421
+ ## Develop
422
+
423
+ ```bash
424
+ npm install
425
+ npm test # vitest
426
+ npm run typecheck
427
+ npm run example # the Garage sample in examples/garage
428
+ npm run size # the gzipped budget: core 10 KiB, react 46, css 10
429
+ npm run check # everything CI runs
430
+ npm run check:browser # the generated-app bridge in real Chrome, against a running sample
431
+ npm run build # dist/
432
+ ```
433
+
434
+ ## Licence
435
+
436
+ Apache-2.0. See [LICENSE](LICENSE) and [NOTICE](NOTICE).