@domicile-desktop/component-library 0.0.0-alpha-59b63cdb292c → 0.0.0-alpha-452a746292aa

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/package.json CHANGED
@@ -2,8 +2,8 @@
2
2
  "dependencies": {
3
3
  "@base-ui/react": "^1.8.0",
4
4
  "@cprussin/option-result": "^2.0.1",
5
- "@domicile-desktop/sdk": "0.0.0-alpha-59b63cdb292c",
6
- "@domicile-desktop/system-apps": "0.0.0-alpha-59b63cdb292c",
5
+ "@domicile-desktop/sdk": "0.0.0-alpha-452a746292aa",
6
+ "@domicile-desktop/system-apps": "0.0.0-alpha-452a746292aa",
7
7
  "@pandacss/dev": "^1.12.1",
8
8
  "@phosphor-icons/react": "^2.1.10"
9
9
  },
@@ -115,5 +115,5 @@
115
115
  "test:unit": "bun test"
116
116
  },
117
117
  "type": "module",
118
- "version": "0.0.0-alpha-59b63cdb292c"
118
+ "version": "0.0.0-alpha-452a746292aa"
119
119
  }
@@ -89,6 +89,11 @@ export const ScreenCastDialog = ({
89
89
  );
90
90
  // An area of the desk shot dragged in place of the picks.
91
91
  const [dragged, setDragged] = useState<ShotRect | undefined>(undefined);
92
+ // A part dragged over the window picked last, drawn alone as it is not on
93
+ // screen, by the window's index.
94
+ const [part, setPart] = useState<
95
+ { index: number; area: ShotRect } | undefined
96
+ >(undefined);
92
97
  const [drawing, setDrawing] = useState(false);
93
98
  const screens: Offered[] = body.sources.flatMap((source) =>
94
99
  source.kind === CastSourceKind.Monitor
@@ -173,7 +178,13 @@ export const ScreenCastDialog = ({
173
178
  desk !== undefined && dragged !== undefined
174
179
  ? [Source.Region(onDesk(desk, dragged))]
175
180
  : offered.flatMap(({ source }, index) =>
176
- picked.includes(index) ? [source] : [],
181
+ picked.includes(index)
182
+ ? [
183
+ part?.index === index
184
+ ? partOf(source, part.area)
185
+ : source,
186
+ ]
187
+ : [],
177
188
  ),
178
189
  ),
179
190
  );
@@ -195,9 +206,10 @@ export const ScreenCastDialog = ({
195
206
  >
196
207
  <p className={askerStyles}>{`${asker} wants to record`}</p>
197
208
  <SourcePicker
198
- caption={caption(offered, picked, dragged, body.region)}
209
+ caption={caption(offered, picked, dragged, part, body.region)}
199
210
  desk={desk}
200
211
  dragged={dragged}
212
+ draggedPart={part?.area}
201
213
  label="What to share"
202
214
  multiple={body.multiple}
203
215
  onDrag={
@@ -205,12 +217,20 @@ export const ScreenCastDialog = ({
205
217
  ? (area) => {
206
218
  setDragged(area);
207
219
  setPicked([]);
220
+ setPart(undefined);
208
221
  }
209
222
  : undefined
210
223
  }
224
+ onDragPart={(area) => {
225
+ const index = picked.at(-1);
226
+ if (index !== undefined) {
227
+ setPart({ area, index });
228
+ }
229
+ }}
211
230
  onPick={(indices) => {
212
231
  setPicked(indices);
213
232
  setDragged(undefined);
233
+ setPart(undefined);
214
234
  }}
215
235
  picked={picked}
216
236
  previewWindow={previewWindow}
@@ -241,14 +261,27 @@ const title = (
241
261
  }
242
262
  };
243
263
 
264
+ /** `source`, or `area` of it when it is a window. */
265
+ const partOf = (source: CastSource, area: ShotRect): CastSource => {
266
+ switch (source.kind) {
267
+ case CastSourceKind.Window:
268
+ case CastSourceKind.BrowserWindow:
269
+ return { ...source, area };
270
+ case CastSourceKind.Monitor:
271
+ case CastSourceKind.Region:
272
+ return source;
273
+ }
274
+ };
275
+
244
276
  /**
245
- * What the desk says of the sources `picked` from `offered`, or of the area
246
- * `dragged`, and whether a `region` may be dragged.
277
+ * What the desk says of the sources `picked` from `offered`, of the area
278
+ * `dragged` or of a window's `part`, and whether a `region` may be dragged.
247
279
  */
248
280
  const caption = (
249
281
  offered: readonly Offered[],
250
282
  picked: readonly number[],
251
283
  dragged: ShotRect | undefined,
284
+ part: { index: number; area: ShotRect } | undefined,
252
285
  region: boolean,
253
286
  ): string => {
254
287
  if (dragged !== undefined) {
@@ -257,8 +290,10 @@ const caption = (
257
290
  return region
258
291
  ? "Pick what to share, or drag over the desk for an area."
259
292
  : "Pick what to share.";
293
+ } else if (part !== undefined) {
294
+ return "A part of the window, shared as it draws itself.";
260
295
  } else if (picked.some((index) => offered[index]?.area === undefined)) {
261
- return "Not on screen: shared as it draws itself.";
296
+ return "Not on screen: shared as it draws itself. Drag over it to pick a part.";
262
297
  } else {
263
298
  return "Shared live, as it changes.";
264
299
  }
@@ -52,12 +52,20 @@ const Pick = {
52
52
  * A browser window from the engine's capture of its page, without what is
53
53
  * over it. A hidden one draws while it is captured.
54
54
  */
55
- BrowserWindow: (id: string) => ({
55
+ BrowserWindow: (id: string, part?: ShotRect | undefined) => ({
56
56
  id,
57
57
  kind: PickKind.BrowserWindow as const,
58
+ part,
59
+ }),
60
+ /**
61
+ * A window from its own last frame, without what is over it, or `part` of
62
+ * it, in the frame's pixels.
63
+ */
64
+ Window: (id: string, part?: ShotRect | undefined) => ({
65
+ id,
66
+ kind: PickKind.Window as const,
67
+ part,
58
68
  }),
59
- /** A window from its own last frame, without what is over it. */
60
- Window: (id: string) => ({ id, kind: PickKind.Window as const }),
61
69
  };
62
70
 
63
71
  type Pick = ReturnType<(typeof Pick)[keyof typeof Pick]>;
@@ -116,9 +124,12 @@ export const ScreenshotDialog = ({
116
124
  const choices = [whole, ...screens, ...windows];
117
125
  // The index of the choice picked, or the area dragged.
118
126
  const [picked, setPicked] = useState<number | ShotRect>(0);
127
+ // A part dragged over the window picked, drawn alone as it is not on
128
+ // screen.
129
+ const [part, setPart] = useState<ShotRect | undefined>(undefined);
119
130
  const pick =
120
131
  typeof picked === "number"
121
- ? pickAt(choices, picked).pick
132
+ ? partOf(pickAt(choices, picked).pick, part)
122
133
  : Pick.Area(picked);
123
134
  useEffect(() => {
124
135
  const copy = (event: KeyboardEvent) => {
@@ -174,22 +185,27 @@ export const ScreenshotDialog = ({
174
185
  >
175
186
  {asker !== undefined && <p className={askerStyles}>{`${asker} asks`}</p>}
176
187
  <SourcePicker
177
- caption={
188
+ caption={caption(
178
189
  typeof picked === "number" &&
179
- pickAt(choices, picked).area === undefined
180
- ? "Not on screen: saved as it last drew itself."
181
- : "Drag over the desk to pick an area."
182
- }
190
+ pickAt(choices, picked).area === undefined,
191
+ part,
192
+ )}
183
193
  desk={body}
184
194
  dragged={typeof picked === "number" ? undefined : picked}
195
+ draggedPart={part}
185
196
  label="What to save"
186
197
  multiple={false}
187
- onDrag={setPicked}
198
+ onDrag={(area) => {
199
+ setPicked(area);
200
+ setPart(undefined);
201
+ }}
202
+ onDragPart={setPart}
188
203
  onPick={([index]) => {
189
204
  if (index === undefined) {
190
205
  throw new Error("a screenshot pick of nothing");
191
206
  } else {
192
207
  setPicked(index);
208
+ setPart(undefined);
193
209
  }
194
210
  }}
195
211
  picked={typeof picked === "number" ? [picked] : []}
@@ -216,15 +232,38 @@ const pickAt = (choices: readonly Picked[], index: number): Picked => {
216
232
  }
217
233
  };
218
234
 
235
+ /** `pick`, or `part` of it when it is a window and a part is dragged. */
236
+ const partOf = (pick: Pick, part: ShotRect | undefined): Pick => {
237
+ switch (pick.kind) {
238
+ case PickKind.Area:
239
+ return pick;
240
+ case PickKind.Window:
241
+ return Pick.Window(pick.id, part);
242
+ case PickKind.BrowserWindow:
243
+ return Pick.BrowserWindow(pick.id, part);
244
+ }
245
+ };
246
+
247
+ /** Under the desk, for a window `hidden` from it, with a `part` of it. */
248
+ const caption = (hidden: boolean, part: ShotRect | undefined): string => {
249
+ if (part !== undefined) {
250
+ return "A part of the window, as it last drew itself.";
251
+ } else if (hidden) {
252
+ return "Not on screen: saved as it last drew itself. Drag over it to pick a part.";
253
+ } else {
254
+ return "Drag over the desk to pick an area.";
255
+ }
256
+ };
257
+
219
258
  /** The answer that saves `pick`, and copies it too when `copy` is set. */
220
259
  const answerOf = (pick: Pick, copy: boolean): PortalAnswer => {
221
260
  switch (pick.kind) {
222
261
  case PickKind.Area:
223
262
  return Answer.Screenshot(pick.area, copy);
224
263
  case PickKind.Window:
225
- return Answer.ScreenshotWindow(pick.id, copy);
264
+ return Answer.ScreenshotWindow(pick.id, copy, pick.part);
226
265
  case PickKind.BrowserWindow:
227
- return Answer.ScreenshotBrowserWindow(pick.id, copy);
266
+ return Answer.ScreenshotBrowserWindow(pick.id, copy, pick.part);
228
267
  }
229
268
  };
230
269
 
@@ -50,6 +50,13 @@ type Props = {
50
50
  dragged?: ShotRect | undefined;
51
51
  /** Called with a dragged area; without it, the desk cannot be dragged. */
52
52
  onDrag?: ((area: ShotRect) => void) | undefined;
53
+ /**
54
+ * A part dragged over a window drawn alone, in its frame's pixels, outlined
55
+ * on it.
56
+ */
57
+ draggedPart?: ShotRect | undefined;
58
+ /** Called with a part dragged over a window drawn alone. */
59
+ onDragPart?: ((area: ShotRect) => void) | undefined;
53
60
  onPick: (picked: number[]) => void;
54
61
  /** The indexes, across every section, of the choices picked. */
55
62
  picked: readonly number[];
@@ -61,16 +68,19 @@ type Props = {
61
68
  /**
62
69
  * Picks from a list of sources beside one preview of the frozen desk, which
63
70
  * outlines each pick with its size. A window picked last that the desk does
64
- * not show is drawn alone instead. One source is a radio, several are
65
- * checkboxes. The screenshot and screen cast pickers share it.
71
+ * not show is drawn alone instead, and a part of it may be dragged over it.
72
+ * One source is a radio, several are checkboxes. The screenshot and screen
73
+ * cast pickers share it.
66
74
  */
67
75
  export const SourcePicker = ({
68
76
  caption,
69
77
  desk,
70
78
  dragged,
79
+ draggedPart,
71
80
  label,
72
81
  multiple,
73
82
  onDrag,
83
+ onDragPart,
74
84
  onPick,
75
85
  picked,
76
86
  previewWindow,
@@ -121,9 +131,11 @@ export const SourcePicker = ({
121
131
  <figure className={stageStyles}>
122
132
  <Stage
123
133
  desk={desk}
134
+ draggedPart={draggedPart}
124
135
  frame={frame}
125
136
  hidden={hidden}
126
137
  onDrag={onDrag}
138
+ onDragPart={onDragPart}
127
139
  outlined={outlined}
128
140
  />
129
141
  <figcaption className={captionStyles}>{caption}</figcaption>
@@ -138,15 +150,19 @@ export const SourcePicker = ({
138
150
  */
139
151
  const Stage = ({
140
152
  desk,
153
+ draggedPart,
141
154
  frame,
142
155
  hidden,
143
156
  onDrag,
157
+ onDragPart,
144
158
  outlined,
145
159
  }: {
146
160
  desk: FrozenDesk | undefined;
161
+ draggedPart: ShotRect | undefined;
147
162
  frame: Result<string, SystemError> | undefined;
148
163
  hidden: Choice | undefined;
149
164
  onDrag: ((area: ShotRect) => void) | undefined;
165
+ onDragPart: ((area: ShotRect) => void) | undefined;
150
166
  outlined: readonly Outline[];
151
167
  }) => {
152
168
  if (hidden?.preview !== undefined) {
@@ -157,7 +173,15 @@ const Stage = ({
157
173
  frame.match({
158
174
  Err: () => <span className={noDeskStyles}>No preview</span>,
159
175
  Ok: (src) => (
160
- <img alt={hidden.label} className={previewStyles} src={src} />
176
+ <Preview
177
+ alt={hidden.label}
178
+ box={desk}
179
+ dragged={draggedPart}
180
+ // Keyed so a part outlined on one window is not kept on the next.
181
+ key={src}
182
+ onDrag={onDragPart}
183
+ src={src}
184
+ />
161
185
  ),
162
186
  })
163
187
  );
@@ -257,6 +281,57 @@ const Row = ({
257
281
  );
258
282
  };
259
283
 
284
+ /**
285
+ * A window drawn alone from `src`, fitted in `box`. With `onDrag`, a part of
286
+ * it may be dragged, in its frame's pixels, and `dragged` is outlined.
287
+ */
288
+ const Preview = ({
289
+ alt,
290
+ box,
291
+ dragged,
292
+ onDrag,
293
+ src,
294
+ }: {
295
+ alt: string;
296
+ box: { width: number; height: number } | undefined;
297
+ dragged: ShotRect | undefined;
298
+ onDrag: ((area: ShotRect) => void) | undefined;
299
+ src: string;
300
+ }) => {
301
+ // The frame's size, which the drag is in, once it loads.
302
+ const [frame, setFrame] = useState<
303
+ { width: number; height: number } | undefined
304
+ >(undefined);
305
+ return (
306
+ <>
307
+ {/* biome-ignore lint/a11y/noNoninteractiveElementInteractions: reads the frame's size as it loads, which is no interaction */}
308
+ <img
309
+ alt={alt}
310
+ className={previewStyles}
311
+ draggable={false}
312
+ onLoad={(event) => {
313
+ setFrame({
314
+ height: event.currentTarget.naturalHeight,
315
+ width: event.currentTarget.naturalWidth,
316
+ });
317
+ }}
318
+ src={src}
319
+ />
320
+ {frame !== undefined && box !== undefined && onDrag !== undefined && (
321
+ <Dragged
322
+ frame={frame}
323
+ label="Drag to pick a part of the window"
324
+ onDrag={onDrag}
325
+ outlined={
326
+ dragged === undefined ? [] : [{ area: dragged, key: "dragged" }]
327
+ }
328
+ style={{ ...contained(frame, box), position: "absolute" }}
329
+ />
330
+ )}
331
+ </>
332
+ );
333
+ };
334
+
260
335
  /** The frozen desk, outlining `outlined`, and dragged when `onDrag` is set. */
261
336
  const Desk = ({
262
337
  desk,
@@ -266,45 +341,62 @@ const Desk = ({
266
341
  desk: FrozenDesk;
267
342
  onDrag: ((area: ShotRect) => void) | undefined;
268
343
  outlined: readonly Outline[];
344
+ }) => {
345
+ const image = (
346
+ <img
347
+ alt="The desk"
348
+ className={frameStyles}
349
+ draggable={false}
350
+ src={desk.frame}
351
+ />
352
+ );
353
+ return onDrag === undefined ? (
354
+ <div className={deskStyles} style={fitted(desk)}>
355
+ {image}
356
+ <Outlines frame={desk} outlined={outlined} />
357
+ </div>
358
+ ) : (
359
+ <Dragged
360
+ frame={desk}
361
+ label="Drag to pick an area"
362
+ onDrag={onDrag}
363
+ outlined={outlined}
364
+ style={fitted(desk)}
365
+ >
366
+ {image}
367
+ </Dragged>
368
+ );
369
+ };
370
+
371
+ /**
372
+ * A box over a frame of `frame`'s size that an area is dragged on, in the
373
+ * frame's pixels, outlining `outlined`.
374
+ */
375
+ const Dragged = ({
376
+ children,
377
+ frame,
378
+ label,
379
+ onDrag,
380
+ outlined,
381
+ style,
382
+ }: {
383
+ children?: ReactNode;
384
+ frame: { width: number; height: number };
385
+ label: string;
386
+ onDrag: (area: ShotRect) => void;
387
+ outlined: readonly Outline[];
388
+ style: CSSProperties;
269
389
  }) => {
270
390
  const [dragFrom, setDragFrom] = useState<FramePoint | undefined>(undefined);
271
391
  const pointAt = (event: PointerEvent<HTMLDivElement>) =>
272
392
  framePoint(
273
393
  { x: event.clientX, y: event.clientY },
274
394
  event.currentTarget.getBoundingClientRect(),
275
- desk,
395
+ frame,
276
396
  );
277
- const frame = (
278
- <>
279
- <img
280
- alt="The desk"
281
- className={frameStyles}
282
- draggable={false}
283
- src={desk.frame}
284
- />
285
- {outlined.map(({ area, key }) => (
286
- <div
287
- className={pickedStyles}
288
- // One pick shades the rest of the desk; several would shade each
289
- // other.
290
- data-shaded={outlined.length === 1 ? "" : undefined}
291
- key={key}
292
- style={placed(area, desk)}
293
- >
294
- <span className={sizeStyles}>
295
- {`${String(area.width)} × ${String(area.height)}`}
296
- </span>
297
- </div>
298
- ))}
299
- </>
300
- );
301
- return onDrag === undefined ? (
302
- <div className={deskStyles} style={fitted(desk)}>
303
- {frame}
304
- </div>
305
- ) : (
397
+ return (
306
398
  <div
307
- aria-label="Drag to pick an area"
399
+ aria-label={label}
308
400
  className={deskStyles}
309
401
  data-draggable=""
310
402
  onPointerDown={(event) => {
@@ -321,13 +413,37 @@ const Desk = ({
321
413
  setDragFrom(undefined);
322
414
  }}
323
415
  role="group"
324
- style={fitted(desk)}
416
+ style={style}
325
417
  >
326
- {frame}
418
+ {children}
419
+ <Outlines frame={frame} outlined={outlined} />
327
420
  </div>
328
421
  );
329
422
  };
330
423
 
424
+ /** `outlined` over a frame of `frame`'s size, each with its size. */
425
+ const Outlines = ({
426
+ frame,
427
+ outlined,
428
+ }: {
429
+ frame: { width: number; height: number };
430
+ outlined: readonly Outline[];
431
+ }) =>
432
+ outlined.map(({ area, key }) => (
433
+ <div
434
+ className={pickedStyles}
435
+ // One pick shades the rest of the frame; several would shade each
436
+ // other.
437
+ data-shaded={outlined.length === 1 ? "" : undefined}
438
+ key={key}
439
+ style={placed(area, frame)}
440
+ >
441
+ <span className={sizeStyles}>
442
+ {`${String(area.width)} × ${String(area.height)}`}
443
+ </span>
444
+ </div>
445
+ ));
446
+
331
447
  const layoutStyles = css({
332
448
  alignItems: "stretch",
333
449
  display: "grid",
@@ -495,6 +611,24 @@ const fitted = (frame: { width: number; height: number }): CSSProperties => {
495
611
  };
496
612
  };
497
613
 
614
+ /**
615
+ * Where a frame of `frame`'s size is drawn in `box`, scaled to fit and
616
+ * centered as `object-fit: contain` draws it, in percent of the box.
617
+ */
618
+ const contained = (
619
+ frame: { width: number; height: number },
620
+ box: { width: number; height: number },
621
+ ): CSSProperties => {
622
+ const wider = frame.width / frame.height / (box.width / box.height);
623
+ const [width, height] = wider > 1 ? [100, 100 / wider] : [100 * wider, 100];
624
+ return {
625
+ height: `${String(height)}%`,
626
+ left: `${String((100 - width) / 2)}%`,
627
+ top: `${String((100 - height) / 2)}%`,
628
+ width: `${String(width)}%`,
629
+ };
630
+ };
631
+
498
632
  /** `area`'s place over the drawn frame, in percent of `frame`. */
499
633
  const placed = (
500
634
  area: ShotRect,