@snaptrude/plugin-core 0.9.0 → 0.9.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 (38) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/api-manifest.json +312 -0
  3. package/dist/api/core/io/terrain/index.d.ts +132 -0
  4. package/dist/api/core/io/terrain/index.d.ts.map +1 -1
  5. package/dist/api/presentation/annotate.d.ts +23 -4
  6. package/dist/api/presentation/annotate.d.ts.map +1 -1
  7. package/dist/api/presentation/diagrams.d.ts +64 -2
  8. package/dist/api/presentation/diagrams.d.ts.map +1 -1
  9. package/dist/api/presentation/index.d.ts +11 -1
  10. package/dist/api/presentation/index.d.ts.map +1 -1
  11. package/dist/api/presentation/placedViews.d.ts +772 -3
  12. package/dist/api/presentation/placedViews.d.ts.map +1 -1
  13. package/dist/api/presentation/shapes.d.ts +2 -2
  14. package/dist/api/presentation/sheets.d.ts +42 -0
  15. package/dist/api/presentation/sheets.d.ts.map +1 -1
  16. package/dist/api/presentation/slideshow.d.ts +125 -0
  17. package/dist/api/presentation/slideshow.d.ts.map +1 -0
  18. package/dist/api/presentation/tables.d.ts +81 -0
  19. package/dist/api/presentation/tables.d.ts.map +1 -0
  20. package/dist/api/program/site.d.ts +166 -2
  21. package/dist/api/program/site.d.ts.map +1 -1
  22. package/dist/api/workspace/index.d.ts +46 -1
  23. package/dist/api/workspace/index.d.ts.map +1 -1
  24. package/dist/index.cjs +829 -553
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.js +797 -553
  27. package/dist/index.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/api/core/io/terrain/index.ts +143 -0
  30. package/src/api/presentation/annotate.ts +27 -2
  31. package/src/api/presentation/diagrams.ts +67 -2
  32. package/src/api/presentation/index.ts +11 -1
  33. package/src/api/presentation/placedViews.ts +760 -3
  34. package/src/api/presentation/sheets.ts +54 -0
  35. package/src/api/presentation/slideshow.ts +134 -0
  36. package/src/api/presentation/tables.ts +84 -0
  37. package/src/api/program/site.ts +110 -2
  38. package/src/api/workspace/index.ts +48 -1
@@ -126,6 +126,34 @@ export abstract class PluginPresentationSheetsApi {
126
126
  orientation?: PluginSheetOrientation,
127
127
  ): PluginApiReturn<PluginPresentationSheet>
128
128
 
129
+ /**
130
+ * Set a sheet's print margin.
131
+ *
132
+ * Applies one of the canonical margin presets — exactly what the sheet
133
+ * header's margin dropdown does. `margin` is unit-agnostic: `0 | 0.25 |
134
+ * 0.5 | 0.75 | 1`, displayed as None / 1/4″ / 1/2″ / 3/4″ / 1″ on imperial
135
+ * projects and None / 5mm / 10mm / 15mm / 20mm on metric ones. One undo
136
+ * step. Requires Present mode to be open.
137
+ *
138
+ * @param sheetId - The id of the sheet.
139
+ * @param margin - The margin preset ({@linkcode PluginSheetMargin}).
140
+ * @returns The updated {@linkcode PluginPresentationSheet}.
141
+ * @throws If Present mode is not open, or the sheet id is invalid.
142
+ *
143
+ * @examplePrompt Set the sheet margin to half an inch
144
+ * @examplePrompt Remove the margins on sheet_1
145
+ * @examplePrompt Give the cover sheet a 10mm margin
146
+ *
147
+ * # Example
148
+ * ```ts
149
+ * const sheet = await snaptrude.presentation.sheets.setMargin("sheet_1", 0.5)
150
+ * ```
151
+ */
152
+ public abstract setMargin(
153
+ sheetId: string,
154
+ margin: PluginSheetMargin,
155
+ ): PluginApiReturn<PluginPresentationSheet>
156
+
129
157
  /**
130
158
  * Place a saved view onto a sheet.
131
159
  *
@@ -331,6 +359,21 @@ export type PluginSheetSize = z.infer<typeof PluginSheetSize>
331
359
  export const PluginSheetOrientation = z.enum(["landscape", "portrait"])
332
360
  export type PluginSheetOrientation = z.infer<typeof PluginSheetOrientation>
333
361
 
362
+ /**
363
+ * Canonical sheet margin presets — unit-agnostic values matching the sheet
364
+ * header's margin dropdown: `0` (None), `0.25`, `0.5`, `0.75`, `1`.
365
+ * Displayed as None / 1/4″ / 1/2″ / 3/4″ / 1″ on imperial projects and
366
+ * None / 5mm / 10mm / 15mm / 20mm on metric ones.
367
+ */
368
+ export const PluginSheetMargin = z.union([
369
+ z.literal(0),
370
+ z.literal(0.25),
371
+ z.literal(0.5),
372
+ z.literal(0.75),
373
+ z.literal(1),
374
+ ])
375
+ export type PluginSheetMargin = z.infer<typeof PluginSheetMargin>
376
+
334
377
  /**
335
378
  * A layout sheet in the presentation.
336
379
  *
@@ -340,12 +383,14 @@ export type PluginSheetOrientation = z.infer<typeof PluginSheetOrientation>
340
383
  * | `name` | `string` | Display name |
341
384
  * | `size` | {@linkcode PluginSheetSize}` \| null` | Paper preset (`null` for a legacy sheet without one) |
342
385
  * | `orientation` | {@linkcode PluginSheetOrientation}` \| null` | Orientation (`null` for a legacy sheet) |
386
+ * | `margin` | {@linkcode PluginSheetMargin}` \| null` | Margin preset (`null` when absent/unrecognized) |
343
387
  */
344
388
  export const PluginPresentationSheet = z.object({
345
389
  id: z.string(),
346
390
  name: z.string(),
347
391
  size: PluginSheetSize.nullable(),
348
392
  orientation: PluginSheetOrientation.nullable(),
393
+ margin: PluginSheetMargin.nullable(),
349
394
  })
350
395
  export type PluginPresentationSheet = z.infer<typeof PluginPresentationSheet>
351
396
 
@@ -406,6 +451,15 @@ export type PluginPresentationSheetsSetSizeArgs = z.infer<
406
451
  typeof PluginPresentationSheetsSetSizeArgs
407
452
  >
408
453
 
454
+ /** Arguments for {@linkcode PluginPresentationSheetsApi.setMargin}. */
455
+ export const PluginPresentationSheetsSetMarginArgs = z.object({
456
+ sheetId: z.string(),
457
+ margin: PluginSheetMargin,
458
+ })
459
+ export type PluginPresentationSheetsSetMarginArgs = z.infer<
460
+ typeof PluginPresentationSheetsSetMarginArgs
461
+ >
462
+
409
463
  /**
410
464
  * Arguments for {@linkcode PluginPresentationSheetsApi.place}.
411
465
  *
@@ -0,0 +1,134 @@
1
+ import * as z from "zod"
2
+ import { PluginApiReturn } from "../../types"
3
+
4
+ /**
5
+ * Presentation slideshow — run the Present-mode slideshow programmatically.
6
+ *
7
+ * The scriptable counterpart of the Present toolbar's slideshow: the
8
+ * slideshow plays **all** layout sheets in sheet order (hidden sheets are
9
+ * excluded) — `start` options only pick the slide it opens on. Slide
10
+ * navigation stays with the user (arrow keys / on-screen controls); there is
11
+ * intentionally no `next`/`prev`/`goTo`.
12
+ *
13
+ * **Fullscreen caveat:** browsers only grant fullscreen on a user gesture. A
14
+ * plugin-triggered `start` usually fails that check, in which case the
15
+ * slideshow catches the rejection and runs non-fullscreen (same slides, same
16
+ * controls, windowed).
17
+ *
18
+ * `start`/`stop` are write-gated; `getState` is a never-throw read. Requires
19
+ * Present mode to be open (except `getState`, which reports not-running).
20
+ *
21
+ * Accessed via `snaptrude.presentation.slideshow`.
22
+ */
23
+ export abstract class PluginPresentationSlideshowApi {
24
+ constructor() {}
25
+
26
+ /**
27
+ * Start the slideshow.
28
+ *
29
+ * Launches the Present-mode slideshow over all layout sheets in sheet
30
+ * order. `options.startIndex` opens on that slide (clamped into range);
31
+ * otherwise `options.sheetIds` naming exactly one sheet opens on that
32
+ * sheet; otherwise it opens on the first slide. May run non-fullscreen —
33
+ * see the fullscreen caveat on
34
+ * {@linkcode PluginPresentationSlideshowApi}. Requires Present mode to be
35
+ * open with at least one sheet.
36
+ *
37
+ * @param options - Optional `sheetIds` (open on this sheet when exactly one
38
+ * id is given) and `startIndex` (0-based slide to open on; wins over
39
+ * `sheetIds`; clamped into range).
40
+ * @returns The {@linkcode PluginPresentationSlideshowState} after the
41
+ * launch (`running: true` and the opening `index`).
42
+ * @throws If Present mode is not open or there are no sheets to present.
43
+ *
44
+ * @examplePrompt Start the slideshow
45
+ * @examplePrompt Present the sheets from the beginning
46
+ * @examplePrompt Start the slideshow on sheet 3
47
+ * @examplePrompt Play the presentation starting at the cover sheet
48
+ *
49
+ * # Example
50
+ * ```ts
51
+ * const state = await snaptrude.presentation.slideshow.start({ startIndex: 2 })
52
+ * console.log(state.running, state.index) // true, 2
53
+ * ```
54
+ */
55
+ public abstract start(options?: {
56
+ sheetIds?: string[]
57
+ startIndex?: number
58
+ }): PluginApiReturn<PluginPresentationSlideshowState>
59
+
60
+ /**
61
+ * Stop the slideshow.
62
+ *
63
+ * Closes the running slideshow (exiting fullscreen if it entered it) and
64
+ * returns to the Present canvas. A no-op when no slideshow is running.
65
+ *
66
+ * @returns The {@linkcode PluginPresentationSlideshowState} after the stop
67
+ * (`running: false`).
68
+ * @throws If Present mode is not open.
69
+ *
70
+ * @examplePrompt Stop the slideshow
71
+ * @examplePrompt Exit the presentation
72
+ * @examplePrompt Close the slideshow and go back to the sheets
73
+ *
74
+ * # Example
75
+ * ```ts
76
+ * await snaptrude.presentation.slideshow.stop()
77
+ * ```
78
+ */
79
+ public abstract stop(): PluginApiReturn<PluginPresentationSlideshowState>
80
+
81
+ /**
82
+ * Read the slideshow state.
83
+ *
84
+ * A never-throw read: reports whether a slideshow is running and, when it
85
+ * is, the current 0-based slide index (`index` is `null` when not
86
+ * running — including when Present mode is closed).
87
+ *
88
+ * @returns The current {@linkcode PluginPresentationSlideshowState}.
89
+ *
90
+ * @examplePrompt Is the slideshow running?
91
+ * @examplePrompt Which slide is the presentation on?
92
+ * @examplePrompt Check the slideshow state
93
+ *
94
+ * # Example
95
+ * ```ts
96
+ * const { running, index } = await snaptrude.presentation.slideshow.getState()
97
+ * ```
98
+ */
99
+ public abstract getState(): PluginApiReturn<PluginPresentationSlideshowState>
100
+ }
101
+
102
+ /**
103
+ * Arguments for {@linkcode PluginPresentationSlideshowApi.start}.
104
+ *
105
+ * | Property | Type | Description |
106
+ * |---|---|---|
107
+ * | `sheetIds` | `string[] \| undefined` | Open on this sheet when exactly one id is given |
108
+ * | `startIndex` | `number \| undefined` | 0-based slide to open on (wins over `sheetIds`; clamped) |
109
+ */
110
+ export const PluginPresentationSlideshowStartArgs = z.object({
111
+ sheetIds: z.array(z.string().min(1)).optional(),
112
+ startIndex: z.number().int().min(0).optional(),
113
+ })
114
+ export type PluginPresentationSlideshowStartArgs = z.infer<
115
+ typeof PluginPresentationSlideshowStartArgs
116
+ >
117
+
118
+ /**
119
+ * The slideshow state — result of {@linkcode PluginPresentationSlideshowApi.start},
120
+ * {@linkcode PluginPresentationSlideshowApi.stop}, and
121
+ * {@linkcode PluginPresentationSlideshowApi.getState}.
122
+ *
123
+ * | Property | Type | Description |
124
+ * |---|---|---|
125
+ * | `running` | `boolean` | Whether a slideshow is currently running |
126
+ * | `index` | `number \| null` | Current 0-based slide index (`null` when not running) |
127
+ */
128
+ export const PluginPresentationSlideshowState = z.object({
129
+ running: z.boolean(),
130
+ index: z.number().int().nullable(),
131
+ })
132
+ export type PluginPresentationSlideshowState = z.infer<
133
+ typeof PluginPresentationSlideshowState
134
+ >
@@ -0,0 +1,84 @@
1
+ import * as z from "zod"
2
+ import { PluginApiReturn } from "../../types"
3
+ import type { PluginCanvasShape } from "./placedViews"
4
+
5
+ /**
6
+ * Presentation tables — place data tables on Present-mode sheets.
7
+ *
8
+ * The scriptable counterpart of the Present canvas's table paste (the flow
9
+ * that turns a Program-mode / Google Sheets / Excel paste into a table
10
+ * shape): {@linkcode PluginPresentationTablesApi.place} takes plain string
11
+ * rows and creates one table shape on a sheet. Requires Present mode to be
12
+ * open.
13
+ *
14
+ * Accessed via `snaptrude.presentation.tables`.
15
+ */
16
+ export abstract class PluginPresentationTablesApi {
17
+ constructor() {}
18
+
19
+ /**
20
+ * Place a table on a sheet.
21
+ *
22
+ * Creates one table shape from `table.rows` — the same shape the Present
23
+ * canvas creates when a spreadsheet table is pasted — and returns the
24
+ * created shape as a canvas-shape record. Cells are plain strings; rows may
25
+ * be ragged (each row needs at least one cell), capped at 10000 cells
26
+ * total. `options.position` is sheet-local (relative to the sheet's
27
+ * top-left, in canvas units — the same convention as `annotate.*`); when
28
+ * omitted the table lands where the paste flow would auto-place it (the
29
+ * center of the current viewport). One undo step. Requires Present mode to
30
+ * be open.
31
+ *
32
+ * @param sheetId - The sheet to place the table on.
33
+ * @param table - The table content: `rows`, an array of string-cell rows.
34
+ * @param options - Optional `position` (`{ x, y }`, relative to the sheet's
35
+ * top-left — defaults to the paste flow's auto-placement).
36
+ * @returns The created table shape as a {@linkcode PluginCanvasShape}.
37
+ * @throws If Present mode is not open, the sheet id is invalid, or the
38
+ * table is empty / exceeds the 10000-cell cap.
39
+ *
40
+ * @examplePrompt Put the area schedule as a table on Sheet 1
41
+ * @examplePrompt Add a table of the room programs to the cover sheet
42
+ * @examplePrompt Paste this data as a table on the sheet
43
+ * @examplePrompt Place a two-column table of space names and areas
44
+ *
45
+ * # Example
46
+ * ```ts
47
+ * const shape = await snaptrude.presentation.tables.place("sheet_1", {
48
+ * rows: [
49
+ * ["Room", "Area"],
50
+ * ["Kitchen", "12.4 m²"],
51
+ * ["Living", "28.0 m²"],
52
+ * ],
53
+ * }, { position: { x: 40, y: 40 } })
54
+ * ```
55
+ */
56
+ public abstract place(
57
+ sheetId: string,
58
+ table: { rows: string[][] },
59
+ options?: { position?: { x: number; y: number } },
60
+ ): PluginApiReturn<PluginCanvasShape>
61
+ }
62
+
63
+ /**
64
+ * Arguments for {@linkcode PluginPresentationTablesApi.place}.
65
+ *
66
+ * | Property | Type | Description |
67
+ * |---|---|---|
68
+ * | `sheetId` | `string` | The sheet to place the table on |
69
+ * | `rows` | `string[][]` | Table cells as string rows (non-empty; ≤ 10000 cells total) |
70
+ * | `position` | `{ x: number; y: number } \| undefined` | Sheet-local position (default: paste auto-placement) |
71
+ */
72
+ export const PluginPresentationTablesPlaceArgs = z.object({
73
+ sheetId: z.string().min(1),
74
+ rows: z
75
+ .array(z.array(z.string()).min(1))
76
+ .min(1)
77
+ .refine((rows) => rows.reduce((n, r) => n + r.length, 0) <= 10000, {
78
+ message: "table exceeds the 10000-cell cap",
79
+ }),
80
+ position: z.object({ x: z.number(), y: z.number() }).optional(),
81
+ })
82
+ export type PluginPresentationTablesPlaceArgs = z.infer<
83
+ typeof PluginPresentationTablesPlaceArgs
84
+ >
@@ -10,8 +10,10 @@ import { PluginAreaUnit } from "./metrics"
10
10
  * in Snaptrude units — the same plan space as a space's `planPoints`), and —
11
11
  * when the project is geo-located on terrain — their geographic
12
12
  * (latitude/longitude) rings. This is the program-planning view of the site;
13
- * zoning numbers (setbacks, FAR/FSI, height limits) live in the site-analysis
14
- * sheet and are not read here.
13
+ * zoning numbers (FAR/FSI, height limits) live in the site-analysis sheet and
14
+ * are not read here — with one exception: `listEdges` reads each parcel's
15
+ * per-edge front/side/rear classification and effective base setbacks, the
16
+ * same resolution the buildable-envelope setback pills show on canvas.
15
17
  *
16
18
  * All methods are reads: they return plain records and never throw — `get`
17
19
  * returns an empty snapshot (zero totals) when there is no site, and the `list`
@@ -85,6 +87,47 @@ export abstract class PluginProgramSiteApi {
85
87
  */
86
88
  public abstract listPolygons(): PluginApiReturn<PluginProgramSiteListPolygonsResult>
87
89
 
90
+ /**
91
+ * List each site parcel's boundary edges with their front / side / rear
92
+ * classification — the same per-edge roles the buildable-envelope setback
93
+ * pills show on canvas, so a plugin can pick up the user's edge tagging
94
+ * without asking them to set it up again.
95
+ *
96
+ * Role resolution mirrors the canvas exactly: the user's per-edge overrides
97
+ * (set via the setback pills) win over the tangent heuristic
98
+ * (`roleSource: "override"` vs `"heuristic"`). `"street"` never appears — it
99
+ * resolves to `front`. When a parcel has a linked zoning buildable envelope,
100
+ * `setbackMeters` is the effective base setback of the edge (a per-edge
101
+ * exception when one exists, else the role's value); parcels without a
102
+ * zoning envelope classify by heuristic alone and report
103
+ * `setbackMeters: null`.
104
+ *
105
+ * Coordinates are **world meters** (`lengthUnit: "m"`) — unlike
106
+ * {@linkcode PluginProgramSiteApi.listPolygons} footprints, which are
107
+ * Snaptrude units. `edgeIndex` is keyed to the canonicalized (sanitized,
108
+ * CCW) boundary, the same index space the envelope's `edgeOverrides` use.
109
+ * `staleOverridesDropped: true` means the site was edited after the
110
+ * envelope was configured and the persisted overrides no longer align with
111
+ * the live boundary — the heuristic classification is reported instead.
112
+ *
113
+ * @returns A {@linkcode PluginProgramSiteListEdgesResult} with one entry per
114
+ * parcel. Empty when the project has no site.
115
+ *
116
+ * @examplePrompt Which edge of my site is the front?
117
+ * @examplePrompt Read the front / side / rear edges of the plot
118
+ * @examplePrompt What setback applies to each site edge?
119
+ * @examplePrompt Get the site edges the way the setback pills show them
120
+ *
121
+ * # Example
122
+ * ```ts
123
+ * const { sites } = await snaptrude.program.site.listEdges()
124
+ * for (const site of sites)
125
+ * for (const e of site.edges)
126
+ * console.log(e.edgeIndex, e.role, e.roleSource, e.setbackMeters)
127
+ * ```
128
+ */
129
+ public abstract listEdges(): PluginApiReturn<PluginProgramSiteListEdgesResult>
130
+
88
131
  /**
89
132
  * List the site parcels as geographic latitude/longitude rings.
90
133
  *
@@ -515,3 +558,68 @@ export const PluginProgramSiteWeatherResult =
515
558
  export type PluginProgramSiteWeatherResult = z.infer<
516
559
  typeof PluginProgramSiteWeatherResult
517
560
  >
561
+
562
+ /**
563
+ * Front / side / rear classification of one site-boundary edge. `"street"`
564
+ * never appears — the backend synonym resolves to `front`.
565
+ */
566
+ export const PluginProgramSiteEdgeRole = z.enum(["front", "side", "rear"])
567
+ export type PluginProgramSiteEdgeRole = z.infer<typeof PluginProgramSiteEdgeRole>
568
+
569
+ /**
570
+ * One boundary edge of a site parcel, as {@linkcode PluginProgramSiteApi.listEdges}
571
+ * returns it. Coordinates and lengths are world meters.
572
+ *
573
+ * | Property | Type | Description |
574
+ * |---|---|---|
575
+ * | `edgeIndex` | `number` | Index in the canonicalized (sanitized, CCW) boundary — the same index space the envelope's `edgeOverrides` use |
576
+ * | `start` / `end` | `{ x, z }` | Edge endpoints, world XZ meters |
577
+ * | `lengthMeters` | `number` | Edge length in meters |
578
+ * | `role` | `"front" \| "side" \| "rear"` | Resolved classification (user override wins over the heuristic) |
579
+ * | `roleSource` | `"override" \| "heuristic"` | Whether the user reassigned this edge's role or the tangent heuristic classified it |
580
+ * | `setbackMeters` | `number \| null` | Effective base setback (per-edge exception ?? role value); `null` when the parcel has no zoning envelope |
581
+ */
582
+ export const PluginProgramSiteEdge = z.object({
583
+ edgeIndex: z.number().int().nonnegative(),
584
+ start: z.object({ x: z.number(), z: z.number() }),
585
+ end: z.object({ x: z.number(), z: z.number() }),
586
+ lengthMeters: z.number(),
587
+ role: PluginProgramSiteEdgeRole,
588
+ roleSource: z.enum(["override", "heuristic"]),
589
+ setbackMeters: z.number().nullable(),
590
+ })
591
+ export type PluginProgramSiteEdge = z.infer<typeof PluginProgramSiteEdge>
592
+
593
+ /**
594
+ * Per-parcel entry of {@linkcode PluginProgramSiteApi.listEdges}.
595
+ *
596
+ * | Property | Type | Description |
597
+ * |---|---|---|
598
+ * | `siteId` | `string` | The parcel's component id (same id space as `listPolygons`) |
599
+ * | `buildableEnvelopeId` | `string \| null` | Linked zoning envelope, when one exists |
600
+ * | `usingLiveSiteBoundary` | `boolean` | `true` when edges come from the live site boundary; `false` when only the envelope's persisted source polygon was resolvable |
601
+ * | `staleOverridesDropped` | `boolean` | `true` when persisted edge overrides no longer index-align with the live boundary and were ignored |
602
+ * | `edges` | `PluginProgramSiteEdge[]` | The classified boundary edges |
603
+ */
604
+ export const PluginProgramSiteEdgesEntry = z.object({
605
+ siteId: z.string(),
606
+ buildableEnvelopeId: z.string().nullable(),
607
+ usingLiveSiteBoundary: z.boolean(),
608
+ staleOverridesDropped: z.boolean(),
609
+ edges: z.array(PluginProgramSiteEdge),
610
+ })
611
+ export type PluginProgramSiteEdgesEntry = z.infer<
612
+ typeof PluginProgramSiteEdgesEntry
613
+ >
614
+
615
+ /**
616
+ * Result of {@linkcode PluginProgramSiteApi.listEdges} — one entry per site
617
+ * parcel; empty when the project has no site. `lengthUnit` is always `"m"`.
618
+ */
619
+ export const PluginProgramSiteListEdgesResult = z.object({
620
+ lengthUnit: z.literal("m"),
621
+ sites: z.array(PluginProgramSiteEdgesEntry),
622
+ })
623
+ export type PluginProgramSiteListEdgesResult = z.infer<
624
+ typeof PluginProgramSiteListEdgesResult
625
+ >
@@ -10,10 +10,13 @@ import { PluginApiReturn } from "../../types"
10
10
  * Teams are **read-only** here (no create/invite/delete in v1, a deliberate
11
11
  * safety decision); there are no folders in v1. A `projectId` is the project's
12
12
  * floorkey. Plan limits are enforced by the backend — a limit rejection surfaces
13
- * as a normal host error.
13
+ * as a normal host error. It also carries the workspace-level mode switch for
14
+ * Present mode ({@linkcode PluginWorkspaceApi.openPresentMode} /
15
+ * {@linkcode PluginWorkspaceApi.closePresentMode}).
14
16
  *
15
17
  * - {@linkcode PluginWorkspaceApi.projects} — Create, copy, list, read & rename projects
16
18
  * - {@linkcode PluginWorkspaceApi.teams} — Read teams and their members
19
+ * - {@linkcode PluginWorkspaceApi.openPresentMode} / {@linkcode PluginWorkspaceApi.closePresentMode} — Open/close the Present-mode documentation editor
17
20
  */
18
21
  export abstract class PluginWorkspaceApi {
19
22
  /** Projects — create, copy, list, read & rename. See {@linkcode PluginWorkspaceProjectsApi}. */
@@ -21,6 +24,50 @@ export abstract class PluginWorkspaceApi {
21
24
  /** Teams — read teams and their members. See {@linkcode PluginWorkspaceTeamsApi}. */
22
25
  public abstract teams: PluginWorkspaceTeamsApi
23
26
 
27
+ /**
28
+ * Open Present mode — the documentation editor the `presentation.*`
29
+ * namespaces operate on.
30
+ *
31
+ * Switches the workspace into Present mode and resolves only once the
32
+ * Present canvas has finished mounting (waits up to ~15s; a mount that
33
+ * never completes is refused with a typed `PRECONDITION_FAILED`), so a
34
+ * `presentation.*` call issued right after this resolves finds Present
35
+ * mode open. Idempotent — resolves immediately when Present mode is
36
+ * already open. Write-gated: switching the mode under the user's feet is
37
+ * approval-controlled by design.
38
+ *
39
+ * @throws `PRECONDITION_FAILED` if the Present canvas does not mount within
40
+ * the timeout.
41
+ *
42
+ * @examplePrompt Open present mode
43
+ * @examplePrompt Switch to the presentation editor
44
+ * @examplePrompt Open the sheets so I can lay out views
45
+ *
46
+ * # Example
47
+ * ```ts
48
+ * await snaptrude.workspace.openPresentMode()
49
+ * const { sheets } = await snaptrude.presentation.sheets.list()
50
+ * ```
51
+ */
52
+ public abstract openPresentMode(): PluginApiReturn<void>
53
+
54
+ /**
55
+ * Close Present mode and return to the modeling canvas.
56
+ *
57
+ * Idempotent — a no-op when Present mode is not open. Write-gated, like
58
+ * {@linkcode PluginWorkspaceApi.openPresentMode}.
59
+ *
60
+ * @examplePrompt Close present mode
61
+ * @examplePrompt Exit the presentation editor
62
+ * @examplePrompt Go back to the modeling canvas
63
+ *
64
+ * # Example
65
+ * ```ts
66
+ * await snaptrude.workspace.closePresentMode()
67
+ * ```
68
+ */
69
+ public abstract closePresentMode(): PluginApiReturn<void>
70
+
24
71
  constructor() {}
25
72
  }
26
73