@textcortex/slidewise 1.0.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 (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +112 -0
  3. package/dist/__vite-browser-external-DYxpcVy9.js +5 -0
  4. package/dist/__vite-browser-external-DYxpcVy9.js.map +1 -0
  5. package/dist/file.svg +1 -0
  6. package/dist/globe.svg +1 -0
  7. package/dist/index.mjs +16697 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/dist/slidewise.css +1 -0
  10. package/dist/types/SlidewiseEditor.d.ts +47 -0
  11. package/dist/types/SlidewiseFileEditor.d.ts +54 -0
  12. package/dist/types/components/editor/BottomToolbar.d.ts +1 -0
  13. package/dist/types/components/editor/Canvas.d.ts +1 -0
  14. package/dist/types/components/editor/Editor.d.ts +8 -0
  15. package/dist/types/components/editor/ElementView.d.ts +6 -0
  16. package/dist/types/components/editor/FloatingToolbar.d.ts +6 -0
  17. package/dist/types/components/editor/GridView.d.ts +1 -0
  18. package/dist/types/components/editor/PlayMode.d.ts +1 -0
  19. package/dist/types/components/editor/SelectionFrame.d.ts +8 -0
  20. package/dist/types/components/editor/SlideRail.d.ts +1 -0
  21. package/dist/types/components/editor/SlideView.d.ts +5 -0
  22. package/dist/types/components/editor/TopBar.d.ts +7 -0
  23. package/dist/types/index.d.ts +7 -0
  24. package/dist/types/lib/StoreProvider.d.ts +8 -0
  25. package/dist/types/lib/fonts.d.ts +9 -0
  26. package/dist/types/lib/pptx/deckToPptx.d.ts +9 -0
  27. package/dist/types/lib/pptx/index.d.ts +3 -0
  28. package/dist/types/lib/pptx/pptxToDeck.d.ts +18 -0
  29. package/dist/types/lib/pptx/types.d.ts +15 -0
  30. package/dist/types/lib/pptx/units.d.ts +25 -0
  31. package/dist/types/lib/schema/migrate.d.ts +25 -0
  32. package/dist/types/lib/seed.d.ts +2 -0
  33. package/dist/types/lib/store.d.ts +55 -0
  34. package/dist/types/lib/types.d.ts +141 -0
  35. package/dist/window.svg +1 -0
  36. package/package.json +86 -0
  37. package/src/App.tsx +261 -0
  38. package/src/SlidewiseEditor.css +146 -0
  39. package/src/SlidewiseEditor.tsx +214 -0
  40. package/src/SlidewiseFileEditor.tsx +242 -0
  41. package/src/components/editor/BottomToolbar.tsx +216 -0
  42. package/src/components/editor/Canvas.tsx +467 -0
  43. package/src/components/editor/Editor.tsx +53 -0
  44. package/src/components/editor/ElementView.tsx +588 -0
  45. package/src/components/editor/FloatingToolbar.tsx +729 -0
  46. package/src/components/editor/GridView.tsx +232 -0
  47. package/src/components/editor/PlayMode.tsx +260 -0
  48. package/src/components/editor/SelectionFrame.tsx +241 -0
  49. package/src/components/editor/SlideRail.tsx +285 -0
  50. package/src/components/editor/SlideView.tsx +55 -0
  51. package/src/components/editor/TopBar.tsx +240 -0
  52. package/src/fonts.css +2 -0
  53. package/src/index.css +13 -0
  54. package/src/index.ts +36 -0
  55. package/src/lib/StoreProvider.tsx +43 -0
  56. package/src/lib/__tests__/css-scope.test.ts +133 -0
  57. package/src/lib/fonts.ts +104 -0
  58. package/src/lib/pptx/__tests__/roundtrip.test.ts +240 -0
  59. package/src/lib/pptx/deckToPptx.ts +300 -0
  60. package/src/lib/pptx/index.ts +3 -0
  61. package/src/lib/pptx/pptxToDeck.ts +1515 -0
  62. package/src/lib/pptx/types.ts +17 -0
  63. package/src/lib/pptx/units.ts +32 -0
  64. package/src/lib/schema/__tests__/migrate.test.ts +70 -0
  65. package/src/lib/schema/migrate.ts +102 -0
  66. package/src/lib/seed.ts +777 -0
  67. package/src/lib/store.ts +384 -0
  68. package/src/lib/types.ts +185 -0
  69. package/src/main.tsx +10 -0
  70. package/src/vite-env.d.ts +3 -0
@@ -0,0 +1,240 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { parsePptx, serializeDeck } from "../index";
3
+ import { CURRENT_DECK_VERSION } from "@/lib/schema/migrate";
4
+ import type { Deck } from "@/lib/types";
5
+
6
+ const baseElement = {
7
+ rotation: 0,
8
+ z: 1,
9
+ };
10
+
11
+ function makeDeck(slideElements: Deck["slides"][number]["elements"]): Deck {
12
+ return {
13
+ version: CURRENT_DECK_VERSION,
14
+ title: "Round-trip fixture",
15
+ slides: [
16
+ {
17
+ id: "slide-1",
18
+ background: "#FFFFFF",
19
+ elements: slideElements,
20
+ },
21
+ ],
22
+ };
23
+ }
24
+
25
+ async function roundtrip(deck: Deck): Promise<Deck> {
26
+ const blob = await serializeDeck(deck);
27
+ const buffer = await blob.arrayBuffer();
28
+ return parsePptx(buffer);
29
+ }
30
+
31
+ describe("pptx round-trip", () => {
32
+ it("preserves a deck with a single text element", async () => {
33
+ const deck = makeDeck([
34
+ {
35
+ ...baseElement,
36
+ id: "t1",
37
+ type: "text",
38
+ x: 200,
39
+ y: 240,
40
+ w: 1200,
41
+ h: 200,
42
+ text: "Hello, Slidewise",
43
+ fontFamily: "Inter",
44
+ fontSize: 64,
45
+ fontWeight: 700,
46
+ italic: false,
47
+ underline: false,
48
+ strike: false,
49
+ color: "#0E1330",
50
+ align: "left",
51
+ vAlign: "top",
52
+ lineHeight: 1.2,
53
+ letterSpacing: 0,
54
+ },
55
+ ]);
56
+
57
+ const out = await roundtrip(deck);
58
+ expect(out.slides.length).toBe(1);
59
+ expect(out.slides[0].elements.length).toBeGreaterThanOrEqual(1);
60
+ const text = out.slides[0].elements.find((e) => e.type === "text");
61
+ expect(text).toBeTruthy();
62
+ if (text && text.type === "text") {
63
+ expect(text.text).toBe("Hello, Slidewise");
64
+ expect(text.fontWeight).toBeGreaterThanOrEqual(600);
65
+ expect(text.color.toUpperCase()).toBe("#0E1330");
66
+ // Position survives within rounding tolerance (1 px).
67
+ expect(Math.abs(text.x - 200)).toBeLessThanOrEqual(2);
68
+ expect(Math.abs(text.y - 240)).toBeLessThanOrEqual(2);
69
+ }
70
+ });
71
+
72
+ it("preserves shape kind, position, and fill", async () => {
73
+ const deck = makeDeck([
74
+ {
75
+ ...baseElement,
76
+ id: "s1",
77
+ type: "shape",
78
+ x: 100,
79
+ y: 100,
80
+ w: 400,
81
+ h: 300,
82
+ shape: "rounded",
83
+ fill: "#4F5BD5",
84
+ radius: 24,
85
+ },
86
+ {
87
+ ...baseElement,
88
+ id: "s2",
89
+ type: "shape",
90
+ x: 600,
91
+ y: 100,
92
+ w: 300,
93
+ h: 300,
94
+ shape: "circle",
95
+ fill: "#F2B544",
96
+ },
97
+ ]);
98
+
99
+ const out = await roundtrip(deck);
100
+ const shapes = out.slides[0].elements.filter((e) => e.type === "shape");
101
+ expect(shapes.length).toBe(2);
102
+ const rounded = shapes.find((e) => e.type === "shape" && e.shape === "rounded");
103
+ const circle = shapes.find((e) => e.type === "shape" && e.shape === "circle");
104
+ expect(rounded).toBeTruthy();
105
+ expect(circle).toBeTruthy();
106
+ if (rounded && rounded.type === "shape") {
107
+ expect(rounded.fill.toUpperCase()).toBe("#4F5BD5");
108
+ expect(Math.abs(rounded.w - 400)).toBeLessThanOrEqual(2);
109
+ expect(Math.abs(rounded.h - 300)).toBeLessThanOrEqual(2);
110
+ }
111
+ });
112
+
113
+ it("preserves slide background colour", async () => {
114
+ const deck: Deck = {
115
+ version: CURRENT_DECK_VERSION,
116
+ title: "Bg",
117
+ slides: [
118
+ { id: "s", background: "#FAEEDC", elements: [] },
119
+ { id: "s2", background: "#0E1330", elements: [] },
120
+ ],
121
+ };
122
+ const out = await roundtrip(deck);
123
+ expect(out.slides.length).toBe(2);
124
+ expect(out.slides[0].background.toUpperCase()).toBe("#FAEEDC");
125
+ expect(out.slides[1].background.toUpperCase()).toBe("#0E1330");
126
+ });
127
+
128
+ it("preserves multiple slides with mixed elements", async () => {
129
+ const deck: Deck = {
130
+ version: CURRENT_DECK_VERSION,
131
+ title: "Multi",
132
+ slides: [
133
+ {
134
+ id: "s1",
135
+ background: "#FFFFFF",
136
+ elements: [
137
+ {
138
+ ...baseElement,
139
+ id: "t",
140
+ type: "text",
141
+ x: 80,
142
+ y: 80,
143
+ w: 1200,
144
+ h: 100,
145
+ text: "Slide one",
146
+ fontFamily: "Inter",
147
+ fontSize: 48,
148
+ fontWeight: 700,
149
+ italic: false,
150
+ underline: false,
151
+ strike: false,
152
+ color: "#0E1330",
153
+ align: "left",
154
+ vAlign: "top",
155
+ lineHeight: 1.2,
156
+ letterSpacing: 0,
157
+ },
158
+ ],
159
+ },
160
+ {
161
+ id: "s2",
162
+ background: "#0E1330",
163
+ elements: [
164
+ {
165
+ ...baseElement,
166
+ id: "sh",
167
+ type: "shape",
168
+ x: 200,
169
+ y: 200,
170
+ w: 400,
171
+ h: 400,
172
+ shape: "rect",
173
+ fill: "#FFFFFF",
174
+ },
175
+ ],
176
+ },
177
+ ],
178
+ };
179
+ const out = await roundtrip(deck);
180
+ expect(out.slides.length).toBe(2);
181
+ expect(
182
+ out.slides[0].elements.find((e) => e.type === "text")
183
+ ).toBeTruthy();
184
+ expect(
185
+ out.slides[1].elements.find((e) => e.type === "shape")
186
+ ).toBeTruthy();
187
+ });
188
+
189
+ it("preserves the deck title", async () => {
190
+ const deck = makeDeck([]);
191
+ deck.title = "My Wonderful Deck";
192
+ const out = await roundtrip(deck);
193
+ expect(out.title).toBe("My Wonderful Deck");
194
+ });
195
+
196
+ it("round-trips multi-color text via runs[]", async () => {
197
+ const deck = makeDeck([
198
+ {
199
+ ...baseElement,
200
+ id: "t1",
201
+ type: "text",
202
+ x: 100,
203
+ y: 100,
204
+ w: 1500,
205
+ h: 220,
206
+ text: "ELDORAUI",
207
+ fontFamily: "Inter",
208
+ fontSize: 120,
209
+ fontWeight: 700,
210
+ italic: false,
211
+ underline: false,
212
+ strike: false,
213
+ color: "#FFFFFF",
214
+ align: "left",
215
+ vAlign: "top",
216
+ lineHeight: 1,
217
+ letterSpacing: 0,
218
+ runs: [
219
+ { text: "ELDORA", color: "#FFFFFF" },
220
+ { text: "UI", color: "#0F1B3D" },
221
+ ],
222
+ },
223
+ ]);
224
+
225
+ const out = await roundtrip(deck);
226
+ const text = out.slides[0].elements.find((e) => e.type === "text");
227
+ expect(text?.type).toBe("text");
228
+ if (text?.type !== "text") return;
229
+ // Concatenated text survives
230
+ expect(text.text.replace(/\s+/g, "")).toBe("ELDORAUI");
231
+ // The two distinct colors come back as separate runs
232
+ expect(text.runs).toBeTruthy();
233
+ expect(text.runs!.length).toBeGreaterThanOrEqual(2);
234
+ const colors = (text.runs ?? [])
235
+ .map((r) => (r.color ?? "").toUpperCase())
236
+ .filter(Boolean);
237
+ expect(colors).toContain("#FFFFFF");
238
+ expect(colors).toContain("#0F1B3D");
239
+ });
240
+ });
@@ -0,0 +1,300 @@
1
+ import pptxgen from "pptxgenjs";
2
+ import type {
3
+ Deck,
4
+ Slide,
5
+ SlideElement,
6
+ TextElement,
7
+ ShapeElement,
8
+ ShapeKind,
9
+ ImageElement,
10
+ LineElement,
11
+ TableElement,
12
+ IconElement,
13
+ EmbedElement,
14
+ } from "@/lib/types";
15
+ import { pxToInches, pxToPoints } from "./units";
16
+
17
+ /**
18
+ * Serialize a Slidewise Deck to a real PPTX blob. Round-trips well for the
19
+ * element types Slidewise natively supports (text, shape, image, line,
20
+ * table, icon, embed). UnknownElement and entrance animations are dropped
21
+ * with a warning — proper preservation requires bypassing pptxgenjs and
22
+ * is out of scope for v1.
23
+ */
24
+ export async function serializeDeck(deck: Deck): Promise<Blob> {
25
+ const pptx = new pptxgen();
26
+ pptx.title = deck.title || "Untitled";
27
+ pptx.layout = "LAYOUT_WIDE"; // 13.333 × 7.5 in
28
+
29
+ for (const slide of deck.slides) {
30
+ addSlide(pptx, slide);
31
+ }
32
+
33
+ // pptxgenjs returns the requested type; outputType: "blob" → Blob.
34
+ const result = (await pptx.write({ outputType: "blob" })) as Blob;
35
+ return result;
36
+ }
37
+
38
+ function addSlide(pptx: pptxgen, slide: Slide): void {
39
+ const s = pptx.addSlide();
40
+ s.background = { color: hexNoHash(slide.background) };
41
+
42
+ const sorted = [...slide.elements].sort((a, b) => a.z - b.z);
43
+ for (const el of sorted) {
44
+ try {
45
+ addElement(s, el);
46
+ } catch (err) {
47
+ console.warn(
48
+ `[slidewise/pptx] failed to write element ${el.id} (${el.type}):`,
49
+ err
50
+ );
51
+ }
52
+ }
53
+ }
54
+
55
+ function addElement(s: pptxgen.Slide, el: SlideElement): void {
56
+ switch (el.type) {
57
+ case "text":
58
+ addText(s, el);
59
+ return;
60
+ case "shape":
61
+ addShape(s, el);
62
+ return;
63
+ case "image":
64
+ addImage(s, el);
65
+ return;
66
+ case "line":
67
+ addLine(s, el);
68
+ return;
69
+ case "table":
70
+ addTable(s, el);
71
+ return;
72
+ case "icon":
73
+ addIcon(s, el);
74
+ return;
75
+ case "embed":
76
+ addEmbed(s, el);
77
+ return;
78
+ case "unknown":
79
+ // Lossy: pptxgenjs has no public API for raw OOXML injection.
80
+ // Future work: post-process the generated zip to re-inject UnknownElement
81
+ // XML into the appropriate slide files for true round-trip.
82
+ return;
83
+ }
84
+ }
85
+
86
+ function geometry(el: SlideElement): {
87
+ x: number;
88
+ y: number;
89
+ w: number;
90
+ h: number;
91
+ rotate?: number;
92
+ } {
93
+ return {
94
+ x: pxToInches(el.x),
95
+ y: pxToInches(el.y),
96
+ w: pxToInches(el.w),
97
+ h: pxToInches(el.h),
98
+ rotate: el.rotation || undefined,
99
+ };
100
+ }
101
+
102
+ function addText(s: pptxgen.Slide, el: TextElement): void {
103
+ const baseOpts = {
104
+ ...geometry(el),
105
+ fontFace: el.fontFamily,
106
+ fontSize: pxToPoints(el.fontSize),
107
+ color: hexNoHash(el.color),
108
+ bold: el.fontWeight >= 600,
109
+ italic: el.italic,
110
+ underline: el.underline ? ({ style: "sng" } as const) : undefined,
111
+ strike: el.strike ? ("sngStrike" as const) : undefined,
112
+ align: el.align,
113
+ valign: el.vAlign,
114
+ charSpacing: el.letterSpacing
115
+ ? Math.round(el.letterSpacing * 100)
116
+ : undefined,
117
+ paraSpaceBefore: 0,
118
+ paraSpaceAfter: 0,
119
+ };
120
+
121
+ if (!el.runs || !el.runs.length) {
122
+ s.addText(el.text, baseOpts);
123
+ return;
124
+ }
125
+
126
+ // Multi-run: pptxgenjs accepts an array of {text, options} objects and emits
127
+ // them as separate <a:r> within the same <a:p>. A run whose text contains
128
+ // "\n" is split so each piece becomes its own paragraph (we use a per-run
129
+ // `breakLine` flag on the trailing pieces).
130
+ const items: pptxgen.TextProps[] = [];
131
+ for (const r of el.runs) {
132
+ const pieces = r.text.split("\n");
133
+ for (let i = 0; i < pieces.length; i++) {
134
+ const isLast = i === pieces.length - 1;
135
+ items.push({
136
+ text: pieces[i],
137
+ options: {
138
+ fontFace: r.fontFamily ?? el.fontFamily,
139
+ fontSize: pxToPoints(r.fontSize ?? el.fontSize),
140
+ color: hexNoHash(r.color ?? el.color),
141
+ bold: (r.fontWeight ?? el.fontWeight) >= 600,
142
+ italic: r.italic ?? el.italic,
143
+ underline: (r.underline ?? el.underline)
144
+ ? ({ style: "sng" } as const)
145
+ : undefined,
146
+ strike: (r.strike ?? el.strike) ? ("sngStrike" as const) : undefined,
147
+ charSpacing: r.letterSpacing ?? el.letterSpacing
148
+ ? Math.round((r.letterSpacing ?? el.letterSpacing) * 100)
149
+ : undefined,
150
+ breakLine: !isLast,
151
+ },
152
+ });
153
+ }
154
+ }
155
+ s.addText(items, baseOpts);
156
+ }
157
+
158
+ const SHAPE_MAP: Record<ShapeKind, string> = {
159
+ rect: "rect",
160
+ rounded: "roundRect",
161
+ circle: "ellipse",
162
+ triangle: "triangle",
163
+ diamond: "diamond",
164
+ star: "star5",
165
+ };
166
+
167
+ function addShape(s: pptxgen.Slide, el: ShapeElement): void {
168
+ const shapeName = SHAPE_MAP[el.shape] ?? "rect";
169
+ // pptxgenjs accepts shape names as strings; the typed ShapeType enum is
170
+ // also exposed. Pass via `as unknown as` to bypass strict enum typing.
171
+ s.addShape(shapeName as unknown as Parameters<typeof s.addShape>[0], {
172
+ ...geometry(el),
173
+ fill: { color: hexNoHash(el.fill) },
174
+ line: el.stroke
175
+ ? {
176
+ color: hexNoHash(el.stroke),
177
+ width: el.strokeWidth ?? 1,
178
+ }
179
+ : { type: "none" },
180
+ rectRadius:
181
+ el.shape === "rounded" && el.radius != null
182
+ ? clamp01(el.radius / Math.min(el.w, el.h))
183
+ : undefined,
184
+ });
185
+ }
186
+
187
+ function addImage(s: pptxgen.Slide, el: ImageElement): void {
188
+ const opts: Parameters<typeof s.addImage>[0] = {
189
+ ...geometry(el),
190
+ sizing:
191
+ el.fit === "cover"
192
+ ? { type: "cover", w: pxToInches(el.w), h: pxToInches(el.h) }
193
+ : el.fit === "contain"
194
+ ? { type: "contain", w: pxToInches(el.w), h: pxToInches(el.h) }
195
+ : undefined,
196
+ };
197
+ if (isDataUrl(el.src)) {
198
+ opts.data = el.src;
199
+ } else {
200
+ opts.path = el.src;
201
+ }
202
+ s.addImage(opts);
203
+ }
204
+
205
+ function addLine(s: pptxgen.Slide, el: LineElement): void {
206
+ s.addShape(
207
+ "line" as unknown as Parameters<typeof s.addShape>[0],
208
+ {
209
+ ...geometry(el),
210
+ line: {
211
+ color: hexNoHash(el.stroke),
212
+ width: el.strokeWidth,
213
+ dashType: el.dashed ? "dash" : "solid",
214
+ endArrowType: el.arrow ? "triangle" : "none",
215
+ },
216
+ }
217
+ );
218
+ }
219
+
220
+ function addTable(s: pptxgen.Slide, el: TableElement): void {
221
+ if (!el.rows.length) return;
222
+ const rows = el.rows.map((row, ri) =>
223
+ row.map((cell) => ({
224
+ text: cell,
225
+ options: {
226
+ bold: ri === 0,
227
+ fill: { color: hexNoHash(ri === 0 ? el.headerFill : el.rowFill) },
228
+ color: hexNoHash(el.textColor),
229
+ fontSize: pxToPoints(el.fontSize),
230
+ valign: "middle" as const,
231
+ },
232
+ }))
233
+ );
234
+ s.addTable(rows, {
235
+ ...geometry(el),
236
+ border: { type: "none", pt: 0, color: "FFFFFF" },
237
+ fontFace: "Inter",
238
+ });
239
+ }
240
+
241
+ function addIcon(s: pptxgen.Slide, el: IconElement): void {
242
+ // Render the icon as a centered text box with the unicode glyph.
243
+ const fontSize = Math.min(el.w, el.h) * 0.7;
244
+ s.addText(el.icon, {
245
+ ...geometry(el),
246
+ fontFace: "Segoe UI Symbol",
247
+ fontSize: pxToPoints(fontSize),
248
+ color: hexNoHash(el.color),
249
+ align: "center",
250
+ valign: "middle",
251
+ });
252
+ }
253
+
254
+ function addEmbed(s: pptxgen.Slide, el: EmbedElement): void {
255
+ // Render embed as a labelled placeholder. PPTX has no first-class equivalent
256
+ // for "an arbitrary URL embed"; we capture intent as text + URL.
257
+ s.addText(
258
+ [
259
+ { text: "Embed\n", options: { fontSize: 10, color: "9CA3AF" } },
260
+ { text: `${el.label}\n`, options: { bold: true, fontSize: 18 } },
261
+ { text: el.url, options: { fontSize: 10, color: "9CA3AF" } },
262
+ ],
263
+ {
264
+ ...geometry(el),
265
+ fill: { color: "0E1330" },
266
+ color: "FFFFFF",
267
+ align: "center",
268
+ valign: "middle",
269
+ }
270
+ );
271
+ }
272
+
273
+ // -- helpers ----------------------------------------------------------------
274
+
275
+ function hexNoHash(color: string): string {
276
+ if (!color) return "000000";
277
+ const c = color.trim();
278
+ if (c.startsWith("#")) return c.slice(1).toUpperCase();
279
+ // rgba()/rgb() → strip; pptxgenjs only accepts hex.
280
+ const rgb = c.match(/^rgba?\(([^)]+)\)$/i);
281
+ if (rgb) {
282
+ const parts = rgb[1].split(",").map((p) => parseInt(p.trim(), 10));
283
+ if (parts.length >= 3) {
284
+ return parts
285
+ .slice(0, 3)
286
+ .map((n) => Math.max(0, Math.min(255, n)).toString(16).padStart(2, "0"))
287
+ .join("")
288
+ .toUpperCase();
289
+ }
290
+ }
291
+ return c.toUpperCase();
292
+ }
293
+
294
+ function isDataUrl(src: string): boolean {
295
+ return /^data:image\//i.test(src);
296
+ }
297
+
298
+ function clamp01(n: number): number {
299
+ return Math.max(0, Math.min(1, n));
300
+ }
@@ -0,0 +1,3 @@
1
+ export { parsePptx } from "./pptxToDeck";
2
+ export { serializeDeck } from "./deckToPptx";
3
+ export type { ParseDiagnostics, ParseResult } from "./types";