@gtkx/react 0.1.44 → 0.1.46

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.
@@ -1,7 +1,15 @@
1
1
  import type { GirClass, GirNamespace, TypeMapper } from "@gtkx/gir";
2
+ /**
3
+ * Configuration options for the JSX type generator.
4
+ */
2
5
  interface JsxGeneratorOptions {
6
+ /** Optional Prettier configuration for formatting output. */
3
7
  prettierConfig?: unknown;
4
8
  }
9
+ /**
10
+ * Generates JSX type definitions for React components from GTK widget classes.
11
+ * Creates TypeScript interfaces for props and augments React's JSX namespace.
12
+ */
5
13
  export declare class JsxGenerator {
6
14
  private typeMapper;
7
15
  private options;
@@ -11,7 +19,18 @@ export declare class JsxGenerator {
11
19
  private usedExternalNamespaces;
12
20
  private widgetPropertyNames;
13
21
  private widgetSignalNames;
22
+ /**
23
+ * Creates a new JSX generator.
24
+ * @param typeMapper - TypeMapper for converting GIR types to TypeScript
25
+ * @param options - Generator configuration options
26
+ */
14
27
  constructor(typeMapper: TypeMapper, options?: JsxGeneratorOptions);
28
+ /**
29
+ * Generates JSX type definitions for all widgets in a namespace.
30
+ * @param namespace - The parsed GIR namespace
31
+ * @param classMap - Map of class names to class definitions
32
+ * @returns Generated TypeScript code as a string
33
+ */
15
34
  generate(namespace: GirNamespace, classMap: Map<string, GirClass>): Promise<string>;
16
35
  private generateImports;
17
36
  private generateCommonTypes;
@@ -34,6 +53,8 @@ export declare class JsxGenerator {
34
53
  private addNamespacePrefix;
35
54
  private buildSignalHandlerType;
36
55
  private generateExports;
56
+ private getWrapperExportMembers;
57
+ private generateGenericWrapperComponents;
37
58
  private generateJsxNamespace;
38
59
  private formatCode;
39
60
  }
@@ -74,6 +74,10 @@ const isWidgetSubclass = (typeName, classMap, visited = new Set()) => {
74
74
  return true;
75
75
  return cls.parent ? isWidgetSubclass(cls.parent, classMap, visited) : false;
76
76
  };
77
+ /**
78
+ * Generates JSX type definitions for React components from GTK widget classes.
79
+ * Creates TypeScript interfaces for props and augments React's JSX namespace.
80
+ */
77
81
  export class JsxGenerator {
78
82
  typeMapper;
79
83
  options;
@@ -83,10 +87,21 @@ export class JsxGenerator {
83
87
  usedExternalNamespaces = new Set();
84
88
  widgetPropertyNames = new Set();
85
89
  widgetSignalNames = new Set();
90
+ /**
91
+ * Creates a new JSX generator.
92
+ * @param typeMapper - TypeMapper for converting GIR types to TypeScript
93
+ * @param options - Generator configuration options
94
+ */
86
95
  constructor(typeMapper, options = {}) {
87
96
  this.typeMapper = typeMapper;
88
97
  this.options = options;
89
98
  }
99
+ /**
100
+ * Generates JSX type definitions for all widgets in a namespace.
101
+ * @param namespace - The parsed GIR namespace
102
+ * @param classMap - Map of class names to class definitions
103
+ * @returns Generated TypeScript code as a string
104
+ */
90
105
  async generate(namespace, classMap) {
91
106
  this.classMap = classMap;
92
107
  this.interfaceMap = new Map(namespace.interfaces.map((iface) => [iface.name, iface]));
@@ -116,6 +131,7 @@ export class JsxGenerator {
116
131
  .map((ns) => `import type * as ${ns} from "@gtkx/ffi/${ns.toLowerCase()}";`);
117
132
  return [
118
133
  `import "react";`,
134
+ `import { createElement } from "react";`,
119
135
  `import type { ReactNode, Ref } from "react";`,
120
136
  ...externalImports,
121
137
  `import type * as Gtk from "@gtkx/ffi/gtk";`,
@@ -319,18 +335,22 @@ ${widgetPropsContent}
319
335
  lines.push(`\t * Render function for list items.`);
320
336
  lines.push(`\t * Called with null during setup (for loading state) and with the actual item during bind.`);
321
337
  lines.push(`\t */`);
322
- lines.push(`\t// biome-ignore lint/suspicious/noExplicitAny: allows typed renderItem callbacks`);
338
+ lines.push(`\t// biome-ignore lint/suspicious/noExplicitAny: Internal type, use generic ListView<T> export`);
323
339
  lines.push(`\trenderItem: (item: any) => import("react").ReactElement;`);
324
340
  }
325
341
  if (isDropDownWidget(widget.name)) {
326
342
  lines.push("");
327
343
  lines.push(`\t/** Function to convert item to display label */`);
344
+ lines.push(`\t// biome-ignore lint/suspicious/noExplicitAny: Internal type, use generic DropDown<T> export`);
328
345
  lines.push(`\titemLabel?: (item: any) => string;`);
329
346
  lines.push(`\t/** Called when selection changes */`);
347
+ lines.push(`\t// biome-ignore lint/suspicious/noExplicitAny: Internal type, use generic DropDown<T> export`);
330
348
  lines.push(`\tonSelectionChanged?: (item: any, index: number) => void;`);
331
349
  }
332
350
  if (isTextViewWidget(widget.name)) {
333
351
  lines.push("");
352
+ lines.push(`\t/** The contents of the text buffer. */`);
353
+ lines.push(`\ttext?: string;`);
334
354
  lines.push(`\t/** Called when the text buffer content changes */`);
335
355
  lines.push(`\tonChanged?: (text: string) => void;`);
336
356
  }
@@ -544,22 +564,30 @@ ${widgetPropsContent}
544
564
  isNotebookWidget(widget.name);
545
565
  const docComment = widget.doc ? formatDoc(widget.doc).trimEnd() : "";
546
566
  if (hasMeaningfulSlots) {
547
- const valueMembers = [
548
- `Root: "${widgetName}.Root" as const`,
549
- ...metadata.namedChildSlots.map((slot) => `${slot.slotName}: "${widgetName}.${slot.slotName}" as const`),
550
- ...(isListWidget(widget.name) ? [`Item: "${widgetName}.Item" as const`] : []),
551
- ...(isColumnViewWidget(widget.name)
552
- ? [`Column: "${widgetName}.Column" as const`, `Item: "${widgetName}.Item" as const`]
553
- : []),
554
- ...(isDropDownWidget(widget.name) ? [`Item: "${widgetName}.Item" as const`] : []),
555
- ...(isGridWidget(widget.name) ? [`Child: "${widgetName}.Child" as const`] : []),
556
- ...(isNotebookWidget(widget.name) ? [`Page: "${widgetName}.Page" as const`] : []),
557
- ];
558
- if (docComment) {
559
- lines.push(`${docComment}\nexport const ${widgetName} = {\n\t${valueMembers.join(",\n\t")},\n};`);
567
+ // For list widgets, generate wrapper components with proper generics
568
+ if (isListWidget(widget.name) || isColumnViewWidget(widget.name) || isDropDownWidget(widget.name)) {
569
+ const wrapperComponents = this.generateGenericWrapperComponents(widget.name, metadata);
570
+ const exportMembers = this.getWrapperExportMembers(widget.name, metadata);
571
+ if (docComment) {
572
+ lines.push(`${wrapperComponents}\n${docComment}\nexport const ${widgetName} = {\n\t${exportMembers.join(",\n\t")},\n};`);
573
+ }
574
+ else {
575
+ lines.push(`${wrapperComponents}\nexport const ${widgetName} = {\n\t${exportMembers.join(",\n\t")},\n};`);
576
+ }
560
577
  }
561
578
  else {
562
- lines.push(`export const ${widgetName} = {\n\t${valueMembers.join(",\n\t")},\n};`);
579
+ const valueMembers = [
580
+ `Root: "${widgetName}.Root" as const`,
581
+ ...metadata.namedChildSlots.map((slot) => `${slot.slotName}: "${widgetName}.${slot.slotName}" as const`),
582
+ ...(isGridWidget(widget.name) ? [`Child: "${widgetName}.Child" as const`] : []),
583
+ ...(isNotebookWidget(widget.name) ? [`Page: "${widgetName}.Page" as const`] : []),
584
+ ];
585
+ if (docComment) {
586
+ lines.push(`${docComment}\nexport const ${widgetName} = {\n\t${valueMembers.join(",\n\t")},\n};`);
587
+ }
588
+ else {
589
+ lines.push(`export const ${widgetName} = {\n\t${valueMembers.join(",\n\t")},\n};`);
590
+ }
563
591
  }
564
592
  }
565
593
  else {
@@ -573,6 +601,95 @@ ${widgetPropsContent}
573
601
  }
574
602
  return `${lines.join("\n")}\n`;
575
603
  }
604
+ getWrapperExportMembers(widgetName, metadata) {
605
+ const name = toPascalCase(widgetName);
606
+ const members = [`Root: ${name}Root`];
607
+ if (isListWidget(widgetName)) {
608
+ members.push(`Item: ${name}Item`);
609
+ }
610
+ else if (isColumnViewWidget(widgetName)) {
611
+ members.push(`Column: ${name}Column`);
612
+ members.push(`Item: ${name}Item`);
613
+ }
614
+ else if (isDropDownWidget(widgetName)) {
615
+ members.push(`Item: ${name}Item`);
616
+ }
617
+ // Add named child slots
618
+ for (const slot of metadata.namedChildSlots) {
619
+ members.push(`${slot.slotName}: ${name}${slot.slotName}`);
620
+ }
621
+ return members;
622
+ }
623
+ generateGenericWrapperComponents(widgetName, metadata) {
624
+ const name = toPascalCase(widgetName);
625
+ const lines = [];
626
+ if (isListWidget(widgetName)) {
627
+ // Props type for the generic Root component
628
+ lines.push(`interface ${name}RootProps<T> extends Omit<${name}Props, "renderItem"> {`);
629
+ lines.push(`\t/** Render function for list items. Called with null during setup. */`);
630
+ lines.push(`\trenderItem: (item: T | null) => import("react").ReactElement;`);
631
+ lines.push(`}`);
632
+ lines.push(``);
633
+ // Root wrapper component
634
+ lines.push(`function ${name}Root<T>(props: ${name}RootProps<T>): import("react").ReactElement {`);
635
+ lines.push(`\treturn createElement("${name}.Root", props);`);
636
+ lines.push(`}`);
637
+ lines.push(``);
638
+ // Item wrapper component
639
+ lines.push(`function ${name}Item<T>(props: ListItemProps<T>): import("react").ReactElement {`);
640
+ lines.push(`\treturn createElement("${name}.Item", props);`);
641
+ lines.push(`}`);
642
+ }
643
+ else if (isColumnViewWidget(widgetName)) {
644
+ // Root wrapper (non-generic)
645
+ lines.push(`function ${name}Root(props: ${name}Props): import("react").ReactElement {`);
646
+ lines.push(`\treturn createElement("${name}.Root", props);`);
647
+ lines.push(`}`);
648
+ lines.push(``);
649
+ // Column props type - use GenericColumnProps to avoid conflict with imported ColumnViewColumnProps
650
+ lines.push(`interface ${name}GenericColumnProps<T> extends Omit<ColumnViewColumnProps, "renderCell"> {`);
651
+ lines.push(`\t/** Render function for column cells. Called with null during setup. */`);
652
+ lines.push(`\trenderCell: (item: T | null) => import("react").ReactElement;`);
653
+ lines.push(`}`);
654
+ lines.push(``);
655
+ // Column wrapper component
656
+ lines.push(`function ${name}Column<T>(props: ${name}GenericColumnProps<T>): import("react").ReactElement {`);
657
+ lines.push(`\treturn createElement("${name}.Column", props);`);
658
+ lines.push(`}`);
659
+ lines.push(``);
660
+ // Item wrapper component
661
+ lines.push(`function ${name}Item<T>(props: ListItemProps<T>): import("react").ReactElement {`);
662
+ lines.push(`\treturn createElement("${name}.Item", props);`);
663
+ lines.push(`}`);
664
+ }
665
+ else if (isDropDownWidget(widgetName)) {
666
+ // Props type for the generic Root component
667
+ lines.push(`interface ${name}RootProps<T> extends Omit<${name}Props, "itemLabel" | "onSelectionChanged"> {`);
668
+ lines.push(`\t/** Function to convert item to display label */`);
669
+ lines.push(`\titemLabel?: (item: T) => string;`);
670
+ lines.push(`\t/** Called when selection changes */`);
671
+ lines.push(`\tonSelectionChanged?: (item: T, index: number) => void;`);
672
+ lines.push(`}`);
673
+ lines.push(``);
674
+ // Root wrapper component
675
+ lines.push(`function ${name}Root<T>(props: ${name}RootProps<T>): import("react").ReactElement {`);
676
+ lines.push(`\treturn createElement("${name}.Root", props);`);
677
+ lines.push(`}`);
678
+ lines.push(``);
679
+ // Item wrapper component
680
+ lines.push(`function ${name}Item<T>(props: ListItemProps<T>): import("react").ReactElement {`);
681
+ lines.push(`\treturn createElement("${name}.Item", props);`);
682
+ lines.push(`}`);
683
+ }
684
+ // Generate slot wrapper components
685
+ for (const slot of metadata.namedChildSlots) {
686
+ lines.push(``);
687
+ lines.push(`function ${name}${slot.slotName}(props: SlotProps): import("react").ReactElement {`);
688
+ lines.push(`\treturn createElement("${name}.${slot.slotName}", props);`);
689
+ lines.push(`}`);
690
+ }
691
+ return lines.join("\n");
692
+ }
576
693
  generateJsxNamespace(widgets, containerMetadata) {
577
694
  const elements = [];
578
695
  for (const widget of widgets) {
@@ -1,3 +1,8 @@
1
1
  import type * as Gtk from "@gtkx/ffi/gtk";
2
2
  import type Reconciler from "react-reconciler";
3
+ /**
4
+ * Creates a new fiber root container for rendering React elements.
5
+ * @param container - Optional GTK widget to use as the container. If not provided,
6
+ * uses the ROOT_NODE_CONTAINER sentinel for virtual roots.
7
+ */
3
8
  export declare const createFiberRoot: (container?: Gtk.Widget) => Reconciler.FiberRoot;
@@ -1,5 +1,10 @@
1
1
  import { ROOT_NODE_CONTAINER } from "./factory.js";
2
2
  import { reconciler } from "./reconciler.js";
3
+ /**
4
+ * Creates a new fiber root container for rendering React elements.
5
+ * @param container - Optional GTK widget to use as the container. If not provided,
6
+ * uses the ROOT_NODE_CONTAINER sentinel for virtual roots.
7
+ */
3
8
  export const createFiberRoot = (container) => {
4
9
  const instance = reconciler.getInstance();
5
10
  return instance.createContainer(container ?? ROOT_NODE_CONTAINER, 0, null, false, null, "", (error) => console.error("Fiber root render error:", error), () => { }, () => { }, () => { }, null);