@liberation-data/desk 0.4.0 → 0.7.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 (46) hide show
  1. package/README.md +63 -2
  2. package/dist/core/desk.d.ts +13 -0
  3. package/dist/core/desk.d.ts.map +1 -1
  4. package/dist/core/desk.js +55 -1
  5. package/dist/core/desk.js.map +1 -1
  6. package/dist/core/index.d.ts +2 -2
  7. package/dist/core/index.d.ts.map +1 -1
  8. package/dist/core/types.d.ts +7 -0
  9. package/dist/core/types.d.ts.map +1 -1
  10. package/dist/css/apps.css +57 -0
  11. package/dist/css/controls.css +975 -0
  12. package/dist/css/conversation.css +232 -0
  13. package/dist/css/dock.css +272 -0
  14. package/dist/css/menus.css +204 -0
  15. package/dist/css/overlays.css +237 -0
  16. package/dist/css/search.css +149 -0
  17. package/dist/css/setup.css +284 -0
  18. package/dist/css/tokens.css +90 -0
  19. package/dist/css/windows.css +321 -0
  20. package/dist/desk.css +1080 -1070
  21. package/dist/react/Desktop.d.ts +9 -2
  22. package/dist/react/Desktop.d.ts.map +1 -1
  23. package/dist/react/Desktop.js +85 -23
  24. package/dist/react/Desktop.js.map +1 -1
  25. package/dist/react/appFrame.d.ts +9 -2
  26. package/dist/react/appFrame.d.ts.map +1 -1
  27. package/dist/react/appFrame.js +26 -5
  28. package/dist/react/appFrame.js.map +1 -1
  29. package/llms.txt +12 -1
  30. package/package.json +4 -3
  31. package/src/core/desk.ts +67 -1
  32. package/src/core/index.ts +2 -1
  33. package/src/core/types.ts +8 -0
  34. package/src/css/apps.css +56 -0
  35. package/src/css/controls.css +974 -0
  36. package/src/css/conversation.css +231 -0
  37. package/src/css/dock.css +271 -0
  38. package/src/css/menus.css +203 -0
  39. package/src/css/overlays.css +236 -0
  40. package/src/css/search.css +148 -0
  41. package/src/css/setup.css +283 -0
  42. package/src/css/tokens.css +89 -0
  43. package/src/css/windows.css +320 -0
  44. package/src/react/Desktop.tsx +97 -26
  45. package/src/react/appFrame.tsx +48 -5
  46. package/src/desk.css +0 -2820
@@ -20,8 +20,27 @@ import { useBus } from './events.js'
20
20
  /** The protocol version. Every message carries it, so a mismatch is ignored rather than misread. */
21
21
  const PROTOCOL = 1
22
22
 
23
+ /*
24
+ * THE HOST'S LOOK, OFFERED TO THE APP.
25
+ *
26
+ * An app runs in a frame of its own, so by default it wears whatever its author hard-coded: a white
27
+ * page inside a dark desk, a dark one inside a light desk. Switching the desk's theme should carry
28
+ * the app too — the person switched the whole thing, not the chrome around it.
29
+ *
30
+ * So the host offers its palette, and the app takes it or ignores it. `mode` says which way round
31
+ * the page is, for an app that decides its own colours but wants to know; `tokens` are custom
32
+ * properties the bridge sets on the app's own root, so an app that reads them gets the desk's
33
+ * colours with nothing to write.
34
+ */
35
+ export interface AppTheme {
36
+ readonly mode: 'light' | 'dark'
37
+ /** Custom properties, named as they will be set: `{ '--sb-bg-dark': '#0b0f1a' }`. */
38
+ readonly tokens: Readonly<Record<string, string>>
39
+ }
40
+
23
41
  type HostMessage =
24
- | { readonly desk: 1; readonly kind: 'hello'; readonly window: string | null; readonly listens: readonly string[]; readonly says: readonly string[] }
42
+ | { readonly desk: 1; readonly kind: 'hello'; readonly window: string | null; readonly listens: readonly string[]; readonly says: readonly string[]; readonly theme: AppTheme | null }
43
+ | { readonly desk: 1; readonly kind: 'theme'; readonly theme: AppTheme }
25
44
  | { readonly desk: 1; readonly kind: 'event'; readonly topic: string; readonly payload: unknown; readonly from: string | null }
26
45
  | { readonly desk: 1; readonly kind: 'drop'; readonly type: string; readonly payload: unknown; readonly from: string | null }
27
46
 
@@ -43,6 +62,8 @@ export interface AppFrameProps {
43
62
  readonly listens?: readonly string[]
44
63
  /** Topics the app may say. Anything else it publishes is dropped. Default: none. */
45
64
  readonly says?: readonly string[]
65
+ /** The desk's own look, offered to the app: it is applied to the app's root and re-sent on change. */
66
+ readonly theme?: AppTheme
46
67
  /** What may be dropped on it. Default: nothing. */
47
68
  readonly accepts?: Accepts
48
69
  /** Windows the app may ask to open. Default: none. */
@@ -61,6 +82,7 @@ export function AppFrame({
61
82
  srcDoc,
62
83
  listens = [],
63
84
  says = [],
85
+ theme,
64
86
  accepts,
65
87
  opens = [],
66
88
  sandbox = 'allow-scripts allow-forms',
@@ -79,8 +101,8 @@ export function AppFrame({
79
101
  frame.current?.contentWindow?.postMessage(message, '*')
80
102
  }
81
103
 
82
- const latest = useRef({ listens, says, opens, windowId })
83
- latest.current = { listens, says, opens, windowId }
104
+ const latest = useRef({ listens, says, opens, windowId, theme })
105
+ latest.current = { listens, says, opens, windowId, theme }
84
106
 
85
107
  // What the app says: only from this frame, only on topics it was granted.
86
108
  useEffect(() => {
@@ -89,7 +111,7 @@ export function AppFrame({
89
111
  const message = event.data
90
112
  const granted = latest.current
91
113
  if (message.kind === 'ready') {
92
- post({ desk: PROTOCOL, kind: 'hello', window: granted.windowId, listens: granted.listens, says: granted.says })
114
+ post({ desk: PROTOCOL, kind: 'hello', window: granted.windowId, listens: granted.listens, says: granted.says, theme: granted.theme ?? null })
93
115
  } else if (message.kind === 'publish') {
94
116
  if (typeof message.topic === 'string' && granted.says.some(pattern => topicMatches(pattern, message.topic))) {
95
117
  bus.publish(message.topic, message.payload, { from: granted.windowId })
@@ -102,6 +124,14 @@ export function AppFrame({
102
124
  return () => window.removeEventListener('message', onMessage)
103
125
  }, [bus, desk])
104
126
 
127
+ // The desk's look, again, whenever it changes: a person switching theme switches the app too.
128
+ const themeKey = theme ? `${theme.mode}|${Object.entries(theme.tokens).map(([k, v]) => `${k}:${v}`).join(';')}` : ''
129
+ useEffect(() => {
130
+ if (!latest.current.theme) return
131
+ post({ desk: PROTOCOL, kind: 'theme', theme: latest.current.theme })
132
+ // eslint-disable-next-line react-hooks/exhaustive-deps
133
+ }, [themeKey])
134
+
105
135
  // What the app hears: only the topics it was granted, and never its own words back.
106
136
  const listenKey = listens.join('|')
107
137
  useEffect(() => {
@@ -154,10 +184,22 @@ export const APP_BRIDGE_SCRIPT = `(() => {
154
184
  const handlers = new Map();
155
185
  const drops = [];
156
186
  const send = message => parent.postMessage(Object.assign({ desk: ${PROTOCOL} }, message), '*');
187
+ const applyTheme = theme => {
188
+ if (!theme) return;
189
+ desk.theme = theme;
190
+ const root = document.documentElement;
191
+ root.dataset.deskTheme = theme.mode;
192
+ root.style.colorScheme = theme.mode;
193
+ for (const [name, value] of Object.entries(theme.tokens || {})) root.style.setProperty(name, value);
194
+ themed.forEach(h => h(theme));
195
+ };
196
+ const themed = [];
157
197
  const desk = {
158
198
  window: null,
159
199
  listens: [],
160
200
  says: [],
201
+ theme: null,
202
+ onTheme(handler) { themed.push(handler); if (desk.theme) handler(desk.theme); },
161
203
  on(topic, handler) { (handlers.get(topic) || handlers.set(topic, []).get(topic)).push(handler); },
162
204
  onDrop(handler) { drops.push(handler); },
163
205
  publish(topic, payload) { send({ kind: 'publish', topic, payload }); },
@@ -168,7 +210,8 @@ export const APP_BRIDGE_SCRIPT = `(() => {
168
210
  addEventListener('message', event => {
169
211
  if (event.source !== parent || !event.data || event.data.desk !== ${PROTOCOL}) return;
170
212
  const message = event.data;
171
- if (message.kind === 'hello') { desk.window = message.window; desk.listens = message.listens; desk.says = message.says; }
213
+ if (message.kind === 'hello') { desk.window = message.window; desk.listens = message.listens; desk.says = message.says; applyTheme(message.theme); }
214
+ if (message.kind === 'theme') applyTheme(message.theme);
172
215
  if (message.kind === 'event') for (const [pattern, list] of handlers) if (matches(pattern, message.topic)) list.forEach(h => h(message));
173
216
  if (message.kind === 'drop') drops.forEach(h => h(message));
174
217
  });