@gtkx/react 0.1.45 → 0.1.47

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/README.md CHANGED
@@ -68,7 +68,7 @@ export const App = () => {
68
68
  defaultHeight={300}
69
69
  onCloseRequest={quit}
70
70
  >
71
- <Box orientation={Orientation.VERTICAL} spacing={12} margin={20}>
71
+ <Box orientation={Orientation.VERTICAL} spacing={12} marginStart={20} marginEnd={20} marginTop={20} marginBottom={20}>
72
72
  <Label.Root label={`Count: ${count}`} />
73
73
  <Button
74
74
  label="Increment"
@@ -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;
@@ -25,6 +44,7 @@ export declare class JsxGenerator {
25
44
  private getRequiredConstructorParams;
26
45
  private getConstructorParams;
27
46
  private generateConstructorArgsMetadata;
47
+ private generatePropSettersMap;
28
48
  private generateSetterGetterMap;
29
49
  private collectAllProperties;
30
50
  private getAncestorInterfaces;
@@ -34,6 +54,8 @@ export declare class JsxGenerator {
34
54
  private addNamespacePrefix;
35
55
  private buildSignalHandlerType;
36
56
  private generateExports;
57
+ private getWrapperExportMembers;
58
+ private generateGenericWrapperComponents;
37
59
  private generateJsxNamespace;
38
60
  private formatCode;
39
61
  }
@@ -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]));
@@ -103,6 +118,7 @@ export class JsxGenerator {
103
118
  this.generateCommonTypes(widgetClass),
104
119
  widgetPropsInterfaces,
105
120
  this.generateConstructorArgsMetadata(widgets),
121
+ this.generatePropSettersMap(widgets),
106
122
  this.generateSetterGetterMap(widgets),
107
123
  this.generateExports(widgets, containerMetadata),
108
124
  this.generateJsxNamespace(widgets, containerMetadata),
@@ -116,6 +132,7 @@ export class JsxGenerator {
116
132
  .map((ns) => `import type * as ${ns} from "@gtkx/ffi/${ns.toLowerCase()}";`);
117
133
  return [
118
134
  `import "react";`,
135
+ `import { createElement } from "react";`,
119
136
  `import type { ReactNode, Ref } from "react";`,
120
137
  ...externalImports,
121
138
  `import type * as Gtk from "@gtkx/ffi/gtk";`,
@@ -319,14 +336,16 @@ ${widgetPropsContent}
319
336
  lines.push(`\t * Render function for list items.`);
320
337
  lines.push(`\t * Called with null during setup (for loading state) and with the actual item during bind.`);
321
338
  lines.push(`\t */`);
322
- lines.push(`\t// biome-ignore lint/suspicious/noExplicitAny: allows typed renderItem callbacks`);
339
+ lines.push(`\t// biome-ignore lint/suspicious/noExplicitAny: Internal type, use generic ListView<T> export`);
323
340
  lines.push(`\trenderItem: (item: any) => import("react").ReactElement;`);
324
341
  }
325
342
  if (isDropDownWidget(widget.name)) {
326
343
  lines.push("");
327
344
  lines.push(`\t/** Function to convert item to display label */`);
345
+ lines.push(`\t// biome-ignore lint/suspicious/noExplicitAny: Internal type, use generic DropDown<T> export`);
328
346
  lines.push(`\titemLabel?: (item: any) => string;`);
329
347
  lines.push(`\t/** Called when selection changes */`);
348
+ lines.push(`\t// biome-ignore lint/suspicious/noExplicitAny: Internal type, use generic DropDown<T> export`);
330
349
  lines.push(`\tonSelectionChanged?: (item: any, index: number) => void;`);
331
350
  }
332
351
  if (isTextViewWidget(widget.name)) {
@@ -392,6 +411,28 @@ ${widgetPropsContent}
392
411
  }
393
412
  return `export const CONSTRUCTOR_PARAMS: Record<string, { name: string; hasDefault: boolean }[]> = {\n${entries.join(",\n")},\n};\n`;
394
413
  }
414
+ generatePropSettersMap(widgets) {
415
+ const widgetEntries = [];
416
+ for (const widget of widgets) {
417
+ const propSetterPairs = [];
418
+ const allProps = this.collectAllProperties(widget);
419
+ for (const prop of allProps) {
420
+ if (prop.setter) {
421
+ const propName = toCamelCase(prop.name);
422
+ const setterName = toCamelCase(prop.setter);
423
+ propSetterPairs.push(`"${propName}": "${setterName}"`);
424
+ }
425
+ }
426
+ if (propSetterPairs.length > 0) {
427
+ const widgetName = toPascalCase(widget.name);
428
+ widgetEntries.push(`\t${widgetName}: { ${propSetterPairs.join(", ")} }`);
429
+ }
430
+ }
431
+ if (widgetEntries.length === 0) {
432
+ return `export const PROP_SETTERS: Record<string, Record<string, string>> = {};\n`;
433
+ }
434
+ return `export const PROP_SETTERS: Record<string, Record<string, string>> = {\n${widgetEntries.join(",\n")},\n};\n`;
435
+ }
395
436
  generateSetterGetterMap(widgets) {
396
437
  const widgetEntries = [];
397
438
  for (const widget of widgets) {
@@ -422,7 +463,7 @@ ${widgetPropsContent}
422
463
  for (const prop of current.properties) {
423
464
  if (!seen.has(prop.name)) {
424
465
  seen.add(prop.name);
425
- props.push({ setter: prop.setter, getter: prop.getter });
466
+ props.push({ name: prop.name, setter: prop.setter, getter: prop.getter });
426
467
  }
427
468
  }
428
469
  for (const ifaceName of current.implements) {
@@ -431,7 +472,7 @@ ${widgetPropsContent}
431
472
  for (const prop of iface.properties) {
432
473
  if (!seen.has(prop.name)) {
433
474
  seen.add(prop.name);
434
- props.push({ setter: prop.setter, getter: prop.getter });
475
+ props.push({ name: prop.name, setter: prop.setter, getter: prop.getter });
435
476
  }
436
477
  }
437
478
  }
@@ -546,22 +587,30 @@ ${widgetPropsContent}
546
587
  isNotebookWidget(widget.name);
547
588
  const docComment = widget.doc ? formatDoc(widget.doc).trimEnd() : "";
548
589
  if (hasMeaningfulSlots) {
549
- const valueMembers = [
550
- `Root: "${widgetName}.Root" as const`,
551
- ...metadata.namedChildSlots.map((slot) => `${slot.slotName}: "${widgetName}.${slot.slotName}" as const`),
552
- ...(isListWidget(widget.name) ? [`Item: "${widgetName}.Item" as const`] : []),
553
- ...(isColumnViewWidget(widget.name)
554
- ? [`Column: "${widgetName}.Column" as const`, `Item: "${widgetName}.Item" as const`]
555
- : []),
556
- ...(isDropDownWidget(widget.name) ? [`Item: "${widgetName}.Item" as const`] : []),
557
- ...(isGridWidget(widget.name) ? [`Child: "${widgetName}.Child" as const`] : []),
558
- ...(isNotebookWidget(widget.name) ? [`Page: "${widgetName}.Page" as const`] : []),
559
- ];
560
- if (docComment) {
561
- lines.push(`${docComment}\nexport const ${widgetName} = {\n\t${valueMembers.join(",\n\t")},\n};`);
590
+ // For list widgets, generate wrapper components with proper generics
591
+ if (isListWidget(widget.name) || isColumnViewWidget(widget.name) || isDropDownWidget(widget.name)) {
592
+ const wrapperComponents = this.generateGenericWrapperComponents(widget.name, metadata);
593
+ const exportMembers = this.getWrapperExportMembers(widget.name, metadata);
594
+ if (docComment) {
595
+ lines.push(`${wrapperComponents}\n${docComment}\nexport const ${widgetName} = {\n\t${exportMembers.join(",\n\t")},\n};`);
596
+ }
597
+ else {
598
+ lines.push(`${wrapperComponents}\nexport const ${widgetName} = {\n\t${exportMembers.join(",\n\t")},\n};`);
599
+ }
562
600
  }
563
601
  else {
564
- lines.push(`export const ${widgetName} = {\n\t${valueMembers.join(",\n\t")},\n};`);
602
+ const valueMembers = [
603
+ `Root: "${widgetName}.Root" as const`,
604
+ ...metadata.namedChildSlots.map((slot) => `${slot.slotName}: "${widgetName}.${slot.slotName}" as const`),
605
+ ...(isGridWidget(widget.name) ? [`Child: "${widgetName}.Child" as const`] : []),
606
+ ...(isNotebookWidget(widget.name) ? [`Page: "${widgetName}.Page" as const`] : []),
607
+ ];
608
+ if (docComment) {
609
+ lines.push(`${docComment}\nexport const ${widgetName} = {\n\t${valueMembers.join(",\n\t")},\n};`);
610
+ }
611
+ else {
612
+ lines.push(`export const ${widgetName} = {\n\t${valueMembers.join(",\n\t")},\n};`);
613
+ }
565
614
  }
566
615
  }
567
616
  else {
@@ -575,6 +624,95 @@ ${widgetPropsContent}
575
624
  }
576
625
  return `${lines.join("\n")}\n`;
577
626
  }
627
+ getWrapperExportMembers(widgetName, metadata) {
628
+ const name = toPascalCase(widgetName);
629
+ const members = [`Root: ${name}Root`];
630
+ if (isListWidget(widgetName)) {
631
+ members.push(`Item: ${name}Item`);
632
+ }
633
+ else if (isColumnViewWidget(widgetName)) {
634
+ members.push(`Column: ${name}Column`);
635
+ members.push(`Item: ${name}Item`);
636
+ }
637
+ else if (isDropDownWidget(widgetName)) {
638
+ members.push(`Item: ${name}Item`);
639
+ }
640
+ // Add named child slots
641
+ for (const slot of metadata.namedChildSlots) {
642
+ members.push(`${slot.slotName}: ${name}${slot.slotName}`);
643
+ }
644
+ return members;
645
+ }
646
+ generateGenericWrapperComponents(widgetName, metadata) {
647
+ const name = toPascalCase(widgetName);
648
+ const lines = [];
649
+ if (isListWidget(widgetName)) {
650
+ // Props type for the generic Root component
651
+ lines.push(`interface ${name}RootProps<T> extends Omit<${name}Props, "renderItem"> {`);
652
+ lines.push(`\t/** Render function for list items. Called with null during setup. */`);
653
+ lines.push(`\trenderItem: (item: T | null) => import("react").ReactElement;`);
654
+ lines.push(`}`);
655
+ lines.push(``);
656
+ // Root wrapper component
657
+ lines.push(`function ${name}Root<T>(props: ${name}RootProps<T>): import("react").ReactElement {`);
658
+ lines.push(`\treturn createElement("${name}.Root", props);`);
659
+ lines.push(`}`);
660
+ lines.push(``);
661
+ // Item wrapper component
662
+ lines.push(`function ${name}Item<T>(props: ListItemProps<T>): import("react").ReactElement {`);
663
+ lines.push(`\treturn createElement("${name}.Item", props);`);
664
+ lines.push(`}`);
665
+ }
666
+ else if (isColumnViewWidget(widgetName)) {
667
+ // Root wrapper (non-generic)
668
+ lines.push(`function ${name}Root(props: ${name}Props): import("react").ReactElement {`);
669
+ lines.push(`\treturn createElement("${name}.Root", props);`);
670
+ lines.push(`}`);
671
+ lines.push(``);
672
+ // Column props type - use GenericColumnProps to avoid conflict with imported ColumnViewColumnProps
673
+ lines.push(`interface ${name}GenericColumnProps<T> extends Omit<ColumnViewColumnProps, "renderCell"> {`);
674
+ lines.push(`\t/** Render function for column cells. Called with null during setup. */`);
675
+ lines.push(`\trenderCell: (item: T | null) => import("react").ReactElement;`);
676
+ lines.push(`}`);
677
+ lines.push(``);
678
+ // Column wrapper component
679
+ lines.push(`function ${name}Column<T>(props: ${name}GenericColumnProps<T>): import("react").ReactElement {`);
680
+ lines.push(`\treturn createElement("${name}.Column", props);`);
681
+ lines.push(`}`);
682
+ lines.push(``);
683
+ // Item wrapper component
684
+ lines.push(`function ${name}Item<T>(props: ListItemProps<T>): import("react").ReactElement {`);
685
+ lines.push(`\treturn createElement("${name}.Item", props);`);
686
+ lines.push(`}`);
687
+ }
688
+ else if (isDropDownWidget(widgetName)) {
689
+ // Props type for the generic Root component
690
+ lines.push(`interface ${name}RootProps<T> extends Omit<${name}Props, "itemLabel" | "onSelectionChanged"> {`);
691
+ lines.push(`\t/** Function to convert item to display label */`);
692
+ lines.push(`\titemLabel?: (item: T) => string;`);
693
+ lines.push(`\t/** Called when selection changes */`);
694
+ lines.push(`\tonSelectionChanged?: (item: T, index: number) => void;`);
695
+ lines.push(`}`);
696
+ lines.push(``);
697
+ // Root wrapper component
698
+ lines.push(`function ${name}Root<T>(props: ${name}RootProps<T>): import("react").ReactElement {`);
699
+ lines.push(`\treturn createElement("${name}.Root", props);`);
700
+ lines.push(`}`);
701
+ lines.push(``);
702
+ // Item wrapper component
703
+ lines.push(`function ${name}Item<T>(props: ListItemProps<T>): import("react").ReactElement {`);
704
+ lines.push(`\treturn createElement("${name}.Item", props);`);
705
+ lines.push(`}`);
706
+ }
707
+ // Generate slot wrapper components
708
+ for (const slot of metadata.namedChildSlots) {
709
+ lines.push(``);
710
+ lines.push(`function ${name}${slot.slotName}(props: SlotProps): import("react").ReactElement {`);
711
+ lines.push(`\treturn createElement("${name}.${slot.slotName}", props);`);
712
+ lines.push(`}`);
713
+ }
714
+ return lines.join("\n");
715
+ }
578
716
  generateJsxNamespace(widgets, containerMetadata) {
579
717
  const elements = [];
580
718
  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);