@snaptrude/plugin-core 0.7.0 → 0.8.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 (133) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/api-manifest.full.json +360 -19
  3. package/api-manifest.json +915 -37
  4. package/dist/api/core/camera/index.d.ts +208 -0
  5. package/dist/api/core/camera/index.d.ts.map +1 -0
  6. package/dist/api/core/comment/index.d.ts +105 -2
  7. package/dist/api/core/comment/index.d.ts.map +1 -1
  8. package/dist/api/core/geom/create/index.d.ts +13 -13
  9. package/dist/api/core/geom/delete/index.d.ts +8 -2
  10. package/dist/api/core/geom/delete/index.d.ts.map +1 -1
  11. package/dist/api/core/geom/query/arc.d.ts +5 -5
  12. package/dist/api/core/geom/query/brep.d.ts +18 -18
  13. package/dist/api/core/geom/query/circle.d.ts +18 -18
  14. package/dist/api/core/geom/query/contour.d.ts +20 -20
  15. package/dist/api/core/geom/query/curve.d.ts +49 -49
  16. package/dist/api/core/geom/query/edge.d.ts +5 -5
  17. package/dist/api/core/geom/query/face.d.ts +16 -16
  18. package/dist/api/core/geom/query/halfedge.d.ts +8 -8
  19. package/dist/api/core/geom/query/profile.d.ts +19 -19
  20. package/dist/api/core/geom/query/vertex.d.ts +8 -8
  21. package/dist/api/core/geom/update/contour.d.ts +14 -14
  22. package/dist/api/core/geom/update/curve.d.ts +7 -7
  23. package/dist/api/core/geom/update/profile.d.ts +16 -16
  24. package/dist/api/core/handles/index.d.ts +210 -0
  25. package/dist/api/core/handles/index.d.ts.map +1 -0
  26. package/dist/api/core/index.d.ts +15 -0
  27. package/dist/api/core/index.d.ts.map +1 -1
  28. package/dist/api/core/io/export/index.d.ts +132 -0
  29. package/dist/api/core/io/export/index.d.ts.map +1 -0
  30. package/dist/api/core/io/import/index.d.ts +1 -1
  31. package/dist/api/core/io/index.d.ts +5 -0
  32. package/dist/api/core/io/index.d.ts.map +1 -1
  33. package/dist/api/core/layers.d.ts +7 -7
  34. package/dist/api/core/proposals/index.d.ts +65 -9
  35. package/dist/api/core/proposals/index.d.ts.map +1 -1
  36. package/dist/api/core/user.d.ts +44 -0
  37. package/dist/api/core/user.d.ts.map +1 -0
  38. package/dist/api/design/boolean/index.d.ts +4 -4
  39. package/dist/api/design/create/index.d.ts +139 -44
  40. package/dist/api/design/create/index.d.ts.map +1 -1
  41. package/dist/api/design/doors/index.d.ts +36 -0
  42. package/dist/api/design/doors/index.d.ts.map +1 -1
  43. package/dist/api/design/edit/index.d.ts +1 -1
  44. package/dist/api/design/erase/index.d.ts +2 -2
  45. package/dist/api/design/furniture/index.d.ts +114 -3
  46. package/dist/api/design/furniture/index.d.ts.map +1 -1
  47. package/dist/api/design/index.d.ts +10 -0
  48. package/dist/api/design/index.d.ts.map +1 -1
  49. package/dist/api/design/materials/index.d.ts +111 -14
  50. package/dist/api/design/materials/index.d.ts.map +1 -1
  51. package/dist/api/design/query/index.d.ts +31 -1
  52. package/dist/api/design/query/index.d.ts.map +1 -1
  53. package/dist/api/design/query/spaces.d.ts +5 -5
  54. package/dist/api/design/transform/index.d.ts +83 -14
  55. package/dist/api/design/transform/index.d.ts.map +1 -1
  56. package/dist/api/design/types/index.d.ts +181 -0
  57. package/dist/api/design/types/index.d.ts.map +1 -0
  58. package/dist/api/design/update/index.d.ts +335 -2
  59. package/dist/api/design/update/index.d.ts.map +1 -1
  60. package/dist/api/design/visibility.d.ts +98 -0
  61. package/dist/api/design/visibility.d.ts.map +1 -0
  62. package/dist/api/entity/referenceLine.d.ts +2 -2
  63. package/dist/api/entity/space.d.ts +19 -19
  64. package/dist/api/entity/story.d.ts +128 -15
  65. package/dist/api/entity/story.d.ts.map +1 -1
  66. package/dist/api/presentation/aiInspiration.d.ts +1967 -7
  67. package/dist/api/presentation/aiInspiration.d.ts.map +1 -1
  68. package/dist/api/presentation/annotate.d.ts +448 -0
  69. package/dist/api/presentation/annotate.d.ts.map +1 -0
  70. package/dist/api/presentation/diagrams.d.ts +49 -8
  71. package/dist/api/presentation/diagrams.d.ts.map +1 -1
  72. package/dist/api/presentation/export.d.ts +104 -0
  73. package/dist/api/presentation/export.d.ts.map +1 -0
  74. package/dist/api/presentation/index.d.ts +40 -3
  75. package/dist/api/presentation/index.d.ts.map +1 -1
  76. package/dist/api/presentation/sheets.d.ts +410 -13
  77. package/dist/api/presentation/sheets.d.ts.map +1 -1
  78. package/dist/api/presentation/views.d.ts +165 -10
  79. package/dist/api/presentation/views.d.ts.map +1 -1
  80. package/dist/api/program/areas.d.ts +63 -3
  81. package/dist/api/program/areas.d.ts.map +1 -1
  82. package/dist/api/program/cores.d.ts +3 -99
  83. package/dist/api/program/cores.d.ts.map +1 -1
  84. package/dist/api/program/index.d.ts +2 -2
  85. package/dist/api/program/index.d.ts.map +1 -1
  86. package/dist/api/program/layout.d.ts +172 -12
  87. package/dist/api/program/layout.d.ts.map +1 -1
  88. package/dist/api/program/site.d.ts +11 -8
  89. package/dist/api/program/site.d.ts.map +1 -1
  90. package/dist/api/program/spreadsheet.d.ts +105 -13
  91. package/dist/api/program/spreadsheet.d.ts.map +1 -1
  92. package/dist/handles.d.ts +64 -25
  93. package/dist/handles.d.ts.map +1 -1
  94. package/dist/index.cjs +2502 -1466
  95. package/dist/index.cjs.map +1 -1
  96. package/dist/index.js +2369 -1461
  97. package/dist/index.js.map +1 -1
  98. package/package.json +1 -1
  99. package/src/api/core/camera/index.ts +212 -0
  100. package/src/api/core/comment/index.ts +120 -2
  101. package/src/api/core/geom/delete/index.ts +6 -0
  102. package/src/api/core/handles/index.ts +233 -0
  103. package/src/api/core/index.ts +15 -0
  104. package/src/api/core/io/export/index.ts +124 -0
  105. package/src/api/core/io/index.ts +5 -0
  106. package/src/api/core/proposals/index.ts +71 -11
  107. package/src/api/core/user.ts +46 -0
  108. package/src/api/design/create/index.ts +166 -36
  109. package/src/api/design/doors/index.ts +40 -0
  110. package/src/api/design/furniture/index.ts +127 -3
  111. package/src/api/design/index.ts +10 -0
  112. package/src/api/design/materials/index.ts +157 -30
  113. package/src/api/design/query/index.ts +33 -7
  114. package/src/api/design/transform/index.ts +89 -12
  115. package/src/api/design/types/index.ts +156 -0
  116. package/src/api/design/update/index.ts +402 -6
  117. package/src/api/design/visibility.ts +109 -0
  118. package/src/api/entity/story.ts +141 -15
  119. package/src/api/presentation/aiInspiration.ts +761 -7
  120. package/src/api/presentation/annotate.ts +360 -0
  121. package/src/api/presentation/diagrams.ts +53 -8
  122. package/src/api/presentation/export.ts +104 -0
  123. package/src/api/presentation/index.ts +48 -3
  124. package/src/api/presentation/sheets.ts +346 -13
  125. package/src/api/presentation/views.ts +164 -12
  126. package/src/api/program/areas.ts +57 -6
  127. package/src/api/program/cores.ts +3 -91
  128. package/src/api/program/index.ts +2 -2
  129. package/src/api/program/layout.ts +182 -12
  130. package/src/api/program/site.ts +11 -8
  131. package/src/api/program/spreadsheet.ts +125 -29
  132. package/src/handles.ts +77 -13
  133. package/tsconfig.json +7 -2
@@ -0,0 +1,124 @@
1
+ import * as z from "zod"
2
+ import { PluginApiReturn } from "../../../../types"
3
+
4
+ /**
5
+ * Serialization format for {@link PluginCoreIoExportApi.model}.
6
+ *
7
+ * `glb` / `obj` serialize in the browser; `fbx` / `3ds` run the same server
8
+ * conversion the app's Export dialog uses and return a **zip** containing the
9
+ * converted file.
10
+ */
11
+ export const ExportModelFormat = z.enum(["glb", "obj", "fbx", "3ds"])
12
+ export type ExportModelFormat = z.infer<typeof ExportModelFormat>
13
+
14
+ /**
15
+ * What to export: the whole model (`"scene"`, default) or only the current
16
+ * selection (`"selection"`).
17
+ */
18
+ export const ExportScope = z.enum(["scene", "selection"])
19
+ export type ExportScope = z.infer<typeof ExportScope>
20
+
21
+ /**
22
+ * A serialized model file returned by {@link PluginCoreIoExportApi.model}.
23
+ *
24
+ * | Property | Type | Description |
25
+ * |---|---|---|
26
+ * | `fileName` | `string` | Suggested file name incl. extension (e.g. `"model.glb"`) |
27
+ * | `mimeType` | `string` | MIME type (`"model/gltf-binary"` for glb, `"text/plain"` for obj) |
28
+ * | `dataBase64` | `string` | The file's bytes, base64-encoded (decode to save/download) |
29
+ */
30
+ export const ExportedFile = z.object({
31
+ fileName: z.string(),
32
+ mimeType: z.string(),
33
+ dataBase64: z.string(),
34
+ })
35
+ export type ExportedFile = z.infer<typeof ExportedFile>
36
+
37
+ /**
38
+ * Model export — serialize the scene (or selection) to a downloadable 3D file.
39
+ *
40
+ * `core.io.export.model` hands you the file bytes as base64 — no download
41
+ * dialog opens; the plugin decides what to do with the result (save it, POST
42
+ * it, diff it). Four formats:
43
+ *
44
+ * - `"glb"` — binary glTF (`model/gltf-binary`); geometry + materials,
45
+ * serialized in the browser (nothing is uploaded).
46
+ * - `"obj"` — Wavefront OBJ text (`text/plain`); geometry only, serialized in
47
+ * the browser.
48
+ * - `"fbx"` / `"3ds"` — Autodesk FBX / 3D Studio; the scene is uploaded to the
49
+ * same `/exportmodel/` conversion service the app's Export dialog uses, and
50
+ * the call resolves once the converted artifact is fetched back — a **zip**
51
+ * (`application/zip`) containing the `.fbx`/`.3ds` file. Whole-scene only
52
+ * (`scope: "selection"` is rejected). Conversion runs on the server and is
53
+ * bounded by the per-call plugin timeout (60 s) — very large models may
54
+ * exceed it and reject with `TIMEOUT`.
55
+ *
56
+ * For the browser formats, only real model geometry is exported (walls, slabs,
57
+ * roofs, floors, doors, windows, furniture, staircases, masses, ceilings,
58
+ * mullions, beams, columns, terrain); UI helpers, hidden objects, and
59
+ * hidden-bucket objects are excluded — matching the app's GLB export. The
60
+ * server formats convert the full serialized scene (hidden-bucket objects
61
+ * excluded), exactly like the Export dialog. This is a **read**: it never
62
+ * mutates the model.
63
+ *
64
+ * > Server-backed BIM exports (RVT / IFC / DWG) are **not** exposed here — those
65
+ * > run as fire-and-forget Forge jobs that stream a browser download and return
66
+ * > no retrievable bytes. Use the app's Export menu for those.
67
+ *
68
+ * Accessed via `snaptrude.core.io.export`.
69
+ */
70
+ export abstract class PluginCoreIoExportApi {
71
+ constructor() {}
72
+
73
+ /**
74
+ * Export the model to a 3D file and return its bytes as base64.
75
+ *
76
+ * @param format - `"glb"` (binary glTF, geometry + materials), `"obj"`
77
+ * (Wavefront OBJ text, geometry only) — both serialized in the browser —
78
+ * or `"fbx"` / `"3ds"`, converted by the app's export service and returned
79
+ * as a zip containing the converted file.
80
+ * @param scope - `"scene"` (default) for the whole model, or `"selection"` for
81
+ * only the currently selected objects (browser formats only — `"fbx"`/`"3ds"`
82
+ * reject it). An empty selection yields an empty file.
83
+ * @returns an {@linkcode ExportedFile} — `{ fileName, mimeType, dataBase64 }`.
84
+ * @throws if the format is unsupported, `scope: "selection"` is combined with a
85
+ * server format, or the serialization/conversion fails.
86
+ *
87
+ * @examplePrompt Export the model as a GLB
88
+ * @examplePrompt Give me the whole scene as an OBJ file
89
+ * @examplePrompt Export just my selection to GLB
90
+ * @examplePrompt Download the model as an FBX file
91
+ * @examplePrompt Export the scene to 3DS for 3ds Max
92
+ *
93
+ * # Example
94
+ * ```ts
95
+ * const file = await snaptrude.core.io.export.model("glb")
96
+ * // decode base64 → bytes and save/download in the plugin UI
97
+ * const bytes = Uint8Array.from(atob(file.dataBase64), (c) => c.charCodeAt(0))
98
+ * const blob = new Blob([bytes], { type: file.mimeType })
99
+ * // ...offer blob for download as file.fileName
100
+ *
101
+ * // Only the current selection, as OBJ text:
102
+ * const obj = await snaptrude.core.io.export.model("obj", "selection")
103
+ * const text = atob(obj.dataBase64)
104
+ * ```
105
+ */
106
+ public abstract model(
107
+ format: ExportModelFormat,
108
+ scope?: ExportScope,
109
+ ): PluginApiReturn<ExportedFile>
110
+ }
111
+
112
+ /**
113
+ * Arguments for {@link PluginCoreIoExportApi.model}.
114
+ *
115
+ * | Property | Type | Description |
116
+ * |---|---|---|
117
+ * | `format` | {@link ExportModelFormat} | `"glb"`, `"obj"`, `"fbx"`, or `"3ds"` |
118
+ * | `scope` | {@link ExportScope} | `"scene"` (default) or `"selection"` (browser formats only) |
119
+ */
120
+ export const PluginExportModelArgs = z.object({
121
+ format: ExportModelFormat,
122
+ scope: ExportScope.optional(),
123
+ })
124
+ export type PluginExportModelArgs = z.infer<typeof PluginExportModelArgs>
@@ -3,6 +3,7 @@ import { PluginCoreIoJobApi } from "./job"
3
3
  import { PluginCoreIoUnderlayApi } from "./underlay"
4
4
  import { PluginCoreIoTerrainApi } from "./terrain"
5
5
  import { PluginCoreIoQueryApi } from "./query"
6
+ import { PluginCoreIoExportApi } from "./export"
6
7
 
7
8
  /**
8
9
  * I/O — bring external files into the project and manage what was imported.
@@ -12,6 +13,7 @@ import { PluginCoreIoQueryApi } from "./query"
12
13
  * - {@linkcode PluginCoreIoApi.job} — poll long-running async imports (e.g. DWG)
13
14
  * - {@linkcode PluginCoreIoApi.underlay} — inspect/scale/opacity/delete placed underlays
14
15
  * - {@linkcode PluginCoreIoApi.terrain} — inspect/edit the site terrain (datum, layers, delete)
16
+ * - {@linkcode PluginCoreIoApi.export} — serialize the model to glb/obj (base64 bytes)
15
17
  *
16
18
  * Accessed via `snaptrude.core.io`.
17
19
  */
@@ -26,6 +28,8 @@ export abstract class PluginCoreIoApi {
26
28
  public abstract underlay: PluginCoreIoUnderlayApi
27
29
  /** Inspect and manage the site terrain. See {@linkcode PluginCoreIoTerrainApi}. */
28
30
  public abstract terrain: PluginCoreIoTerrainApi
31
+ /** Serialize the model to a downloadable 3D file. See {@linkcode PluginCoreIoExportApi}. */
32
+ public abstract export: PluginCoreIoExportApi
29
33
 
30
34
  constructor() {}
31
35
  }
@@ -35,3 +39,4 @@ export * from "./job"
35
39
  export * from "./underlay"
36
40
  export * from "./terrain"
37
41
  export * from "./query"
42
+ export * from "./export"
@@ -136,30 +136,70 @@ export abstract class PluginCoreProposalsApi {
136
136
  public abstract isActive(proposalId: string): PluginApiReturn<boolean>
137
137
 
138
138
  /**
139
- * Create a new design option (proposal).
139
+ * Create a new design option (proposal) — blank, or configured from a base
140
+ * proposal (the New Proposal modal's two paths).
140
141
  *
141
142
  * **Pro-gated** (creating proposals requires a Pro plan, matching the UI) and
142
- * **not undoable**. Creates a bare proposal with a single empty default
143
- * bucket — it does **not** carry forward the current proposal's layers, views,
144
- * or targets. The name is capped at 24 characters.
143
+ * **not undoable**. The name is capped at 24 characters.
144
+ *
145
+ * **Blank** (no `options`) creates a bare proposal with a single empty
146
+ * default bucket; it does **not** carry forward the current proposal's
147
+ * layers, views, or targets, and the active proposal does **not** change.
148
+ *
149
+ * **Configured** (`options.baseProposalId`) — the modal's Configure path:
150
+ * carries the base proposal's buckets (layers) forward per `options.layers`,
151
+ * where each entry names a bucket of the base proposal and an action —
152
+ * `"share"` (the new proposal references the same live bucket) or
153
+ * `"duplicate"` (an independent copy of the bucket and its contents).
154
+ * Buckets not listed are left behind. Omitting `layers` carries **every**
155
+ * base bucket forward as a `"duplicate"` (the modal's default). The base
156
+ * proposal's views and area targets are copied either way, and the new
157
+ * proposal becomes the **active** one (implicit switch — blocked with
158
+ * `PROPOSAL_SWITCH_BLOCKED` while an in-canvas agent run is in flight, like
159
+ * {@linkcode PluginCoreProposalsApi.setActive}).
145
160
  *
146
161
  * @param name - Display name of the new proposal (capped at 24 chars).
162
+ * @param options - Optional configure path: `baseProposalId` (the proposal to
163
+ * carry forward from) and `layers` (per-bucket `"share"` / `"duplicate"`
164
+ * actions; omitted buckets are excluded, omitted `layers` duplicates all).
147
165
  * @returns The new proposal's id (read the full record back with
148
166
  * {@linkcode PluginCoreProposalsApi.get}).
149
- * @throws If the caller is not on a Pro plan, or the proposal could not be
150
- * created.
167
+ * @throws If the caller is not on a Pro plan, the name is empty/duplicate,
168
+ * `layers` is passed without `baseProposalId`, the base proposal or a
169
+ * listed bucket does not exist / does not belong to the base proposal, or
170
+ * (configured only) `PROPOSAL_SWITCH_BLOCKED` while a canvas-agent run is
171
+ * in flight.
151
172
  *
152
173
  * @examplePrompt Create a design option called Scheme B
153
174
  * @examplePrompt Add a new proposal named Option 2
154
- * @examplePrompt Make a fresh design variant to explore
175
+ * @examplePrompt Duplicate the current proposal as Scheme C
176
+ * @examplePrompt Create a design option based on Proposal 1 that shares its site layer
155
177
  *
156
178
  * # Example
157
179
  * ```ts
180
+ * // blank design option
158
181
  * const { id } = await snaptrude.core.proposals.create("Scheme B")
182
+ *
183
+ * // configured from the active proposal: duplicate every layer
184
+ * const base = await snaptrude.core.proposals.getActive()
185
+ * if (base) {
186
+ * const { id: dup } = await snaptrude.core.proposals.create("Scheme C", {
187
+ * baseProposalId: base.id,
188
+ * })
189
+ * // …or share one layer and leave the rest behind
190
+ * const { id: shared } = await snaptrude.core.proposals.create("Scheme D", {
191
+ * baseProposalId: base.id,
192
+ * layers: [{ bucketId: base.defaultBucketId, action: "share" }],
193
+ * })
194
+ * }
159
195
  * ```
160
196
  */
161
197
  public abstract create(
162
198
  name: string,
199
+ options?: {
200
+ baseProposalId: string
201
+ layers?: { bucketId: string; action: "share" | "duplicate" }[]
202
+ },
163
203
  ): PluginApiReturn<PluginCoreProposalsCreateResult>
164
204
 
165
205
  /**
@@ -383,15 +423,35 @@ export type PluginCoreProposalsIsActiveArgs = z.infer<
383
423
  >
384
424
 
385
425
  /**
386
- * Arguments for {@linkcode PluginCoreProposalsApi.create}.
426
+ * Arguments for {@linkcode PluginCoreProposalsApi.create}. A bare `name` makes
427
+ * a blank proposal; `baseProposalId` switches to the configured (carry-forward)
428
+ * path. `layers` requires `baseProposalId` — each entry names a bucket of the
429
+ * base proposal; omitted buckets are excluded, and omitting `layers` entirely
430
+ * duplicates every base bucket.
387
431
  *
388
432
  * | Property | Type | Description |
389
433
  * |---|---|---|
390
434
  * | `name` | `string` | Display name of the new proposal (capped at 24 chars) |
435
+ * | `baseProposalId` | `string \| undefined` | Proposal to carry forward from (configured path) |
436
+ * | `layers` | `{ bucketId: string; action: "share" \| "duplicate" }[] \| undefined` | Per-bucket carry-forward actions (requires `baseProposalId`) |
391
437
  */
392
- export const PluginCoreProposalsCreateArgs = z.object({
393
- name: z.string(),
394
- })
438
+ export const PluginCoreProposalsCreateArgs = z
439
+ .object({
440
+ name: z.string(),
441
+ baseProposalId: z.string().optional(),
442
+ layers: z
443
+ .array(
444
+ z.object({
445
+ bucketId: z.string(),
446
+ action: z.enum(["share", "duplicate"]),
447
+ }),
448
+ )
449
+ .optional(),
450
+ })
451
+ .refine((v) => v.layers === undefined || v.baseProposalId !== undefined, {
452
+ message:
453
+ "layers requires baseProposalId — the layers describe how to carry the base proposal's buckets forward",
454
+ })
395
455
  export type PluginCoreProposalsCreateArgs = z.infer<
396
456
  typeof PluginCoreProposalsCreateArgs
397
457
  >
@@ -0,0 +1,46 @@
1
+ import * as z from "zod"
2
+ import { PluginApiReturn } from "../../types"
3
+
4
+ /**
5
+ * User locale.
6
+ *
7
+ * Read the viewer's timezone and language — for date/number formatting and
8
+ * localized labels. This is locale only: NO precise geolocation, no IP, no
9
+ * account identity. Accessed via `snaptrude.core.user`.
10
+ */
11
+ export abstract class PluginUserApi {
12
+ constructor() {}
13
+
14
+ /**
15
+ * Get the viewer's locale: IANA timezone and BCP-47 language tag, resolved
16
+ * from the browser (`Intl.DateTimeFormat().resolvedOptions().timeZone` and
17
+ * `navigator.language`). Locale only — no precise location.
18
+ *
19
+ * @returns `{ timezone, language }` — e.g. `{ timezone: "Asia/Kolkata", language: "en-US" }`.
20
+ *
21
+ * @examplePrompt What timezone is the user in?
22
+ * @examplePrompt Get the viewer's language for localized labels
23
+ * @examplePrompt Format the report date in the user's local timezone
24
+ * @examplePrompt Which locale should I use for this plugin's UI?
25
+ *
26
+ * # Example
27
+ * ```ts
28
+ * const { timezone, language } = await snaptrude.core.user.getLocale()
29
+ * ```
30
+ */
31
+ public abstract getLocale(): PluginApiReturn<PluginUserLocale>
32
+ }
33
+
34
+ /**
35
+ * The viewer's locale.
36
+ *
37
+ * | Property | Type | Description |
38
+ * |---|---|---|
39
+ * | `timezone` | `string` | IANA timezone id (e.g. `"Asia/Kolkata"`) |
40
+ * | `language` | `string` | BCP-47 language tag (e.g. `"en-US"`) |
41
+ */
42
+ export const PluginUserLocale = z.object({
43
+ timezone: z.string(),
44
+ language: z.string(),
45
+ })
46
+ export type PluginUserLocale = z.infer<typeof PluginUserLocale>