diff --git a/apps/cli/example/pg/schema.ts b/apps/cli/example/pg/schema.ts
index 642d633..ee90501 100644
--- a/apps/cli/example/pg/schema.ts
+++ b/apps/cli/example/pg/schema.ts
@@ -1,7 +1,8 @@
-import { randomUUID } from "crypto";
+import { randomUUID } from "node:crypto";
import { explain } from "@drizzle-lab/api/extensions";
import { relations, sql, getTableColumns } from "drizzle-orm";
+import type { AnyPgColumn } from "drizzle-orm/pg-core";
import {
pgTable,
serial,
@@ -13,6 +14,7 @@ import {
primaryKey,
check,
pgView,
+ numeric,
} from "drizzle-orm/pg-core";
import { info } from "@/example/pg/external";
@@ -149,3 +151,149 @@ export const postsRelations = relations(posts, ({ one }) => ({
relationName: "reviewer",
}),
}));
+
+export const products = pgTable("products", {
+ id: serial("id").primaryKey(),
+ name: text("name").notNull(),
+ price: numeric("price", { precision: 10, scale: 2 }).notNull(),
+ description: text("description"),
+ categoryId: integer("category_id").references(() => categories.id),
+});
+
+export const categories = pgTable("categories", {
+ id: serial("id").primaryKey(),
+ name: text("name").notNull(),
+ parentId: integer("parent_id").references((): AnyPgColumn => categories.id),
+});
+
+export const orders = pgTable("orders", {
+ id: serial("id").primaryKey(),
+ userId: integer("user_id")
+ .references(() => users.id)
+ .notNull(),
+ status: text("status", {
+ enum: ["pending", "processing", "shipped", "delivered"],
+ }).notNull(),
+ orderDate: timestamp("order_date").defaultNow(),
+});
+
+export const orderItems = pgTable("order_items", {
+ id: serial("id").primaryKey(),
+ orderId: integer("order_id")
+ .references(() => orders.id)
+ .notNull(),
+ productId: integer("product_id")
+ .references(() => products.id)
+ .notNull(),
+ quantity: integer("quantity").notNull(),
+ price: numeric("price", { precision: 10, scale: 2 }).notNull(),
+});
+
+export const reviews = pgTable("reviews", {
+ id: serial("id").primaryKey(),
+ userId: integer("user_id")
+ .references(() => users.id)
+ .notNull(),
+ productId: integer("product_id")
+ .references(() => products.id)
+ .notNull(),
+ rating: integer("rating").notNull(),
+ comment: text("comment"),
+ createdAt: timestamp("created_at").defaultNow(),
+});
+
+export const inventory = pgTable("inventory", {
+ id: serial("id").primaryKey(),
+ productId: integer("product_id")
+ .references(() => products.id)
+ .notNull(),
+ quantity: integer("quantity").notNull(),
+ lastUpdated: timestamp("last_updated").defaultNow(),
+});
+
+export const suppliers = pgTable("suppliers", {
+ id: serial("id").primaryKey(),
+ name: text("name").notNull(),
+ contactPerson: text("contact_person"),
+ email: text("email"),
+ phone: text("phone"),
+});
+
+export const productSuppliers = pgTable("product_suppliers", {
+ id: serial("id").primaryKey(),
+ productId: integer("product_id")
+ .references(() => products.id)
+ .notNull(),
+ supplierId: integer("supplier_id")
+ .references(() => suppliers.id)
+ .notNull(),
+ cost: numeric("cost", { precision: 10, scale: 2 }).notNull(),
+});
+
+export const shippingAddresses = pgTable("shipping_addresses", {
+ id: serial("id").primaryKey(),
+ userId: integer("user_id")
+ .references(() => users.id)
+ .notNull(),
+ address: text("address").notNull(),
+ city: text("city").notNull(),
+ state: text("state").notNull(),
+ country: text("country").notNull(),
+ postalCode: text("postal_code").notNull(),
+});
+
+export const promotions = pgTable("promotions", {
+ id: serial("id").primaryKey(),
+ code: text("code").notNull().unique(),
+ discountPercentage: numeric("discount_percentage", {
+ precision: 5,
+ scale: 2,
+ }).notNull(),
+ startDate: timestamp("start_date").notNull(),
+ endDate: timestamp("end_date").notNull(),
+});
+
+export const wishlist = pgTable("wishlist", {
+ id: serial("id").primaryKey(),
+ userId: integer("user_id")
+ .references(() => users.id)
+ .notNull(),
+ productId: integer("product_id")
+ .references(() => products.id)
+ .notNull(),
+ addedAt: timestamp("added_at").defaultNow(),
+});
+
+export const productTags = pgTable("product_tags", {
+ id: serial("id").primaryKey(),
+ productId: integer("product_id")
+ .references(() => products.id)
+ .notNull(),
+ tag: text("tag").notNull(),
+});
+
+export const tableWithLongColumnName1 = pgTable(
+ "table_with_long_column_name_1",
+ {
+ id: serial("id").primaryKey(),
+ thisIsAReallyLongColumnNameThatIsExactlySixtyFourCharactersLong: text(
+ "this_is_a_really_long_column_name_that_is_exactly_sixty_four_characters_long",
+ ),
+ authorId: integer("author_id")
+ .references(() => users.id)
+ .notNull(),
+ },
+);
+
+export const tableWithLongColumnName2 = pgTable(
+ "table_with_long_column_name_2",
+ {
+ id: serial("id").primaryKey(),
+ anotherExtremelyLongColumnNameThatIsAlsoSixtyFourCharactersLong: integer(
+ "another_extremely_long_column_name_that_is_also_sixty_four_characters_long",
+ ),
+ authorId: integer("author_id")
+ .references(() => users.id)
+ .notNull(),
+ },
+);
diff --git a/apps/cli/package.json b/apps/cli/package.json
index 5b6c8f3..04ee160 100644
--- a/apps/cli/package.json
+++ b/apps/cli/package.json
@@ -1,6 +1,6 @@
{
"name": "drizzle-lab",
- "version": "0.8.0",
+ "version": "0.9.0",
"description": "Drizzle Lab CLI",
"sideEffects": false,
"type": "module",
diff --git a/apps/cli/visualizer/routes/_index.tsx b/apps/cli/visualizer/routes/_index.tsx
index 9c0410f..085afba 100644
--- a/apps/cli/visualizer/routes/_index.tsx
+++ b/apps/cli/visualizer/routes/_index.tsx
@@ -147,10 +147,12 @@ export default function Index() {
{/*
Drizzle Lab - Visualizer
*/}
-
- Drizzle Lab - Visualizer
-
- This is a beta version. It can still have bugs!
+
+
+ Drizzle Lab - Visualizer
+
+
+ It can still have bugs!
Loading...
}>
diff --git a/apps/cli/biome.json b/biome.json
similarity index 84%
rename from apps/cli/biome.json
rename to biome.json
index 45e9cf1..767ae00 100644
--- a/apps/cli/biome.json
+++ b/biome.json
@@ -1,5 +1,8 @@
{
- "$schema": "../../node_modules/@biomejs/biome/configuration_schema.json",
+ "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
+ "files": {
+ "include": ["apps/cli/**/*"]
+ },
"vcs": {
"enabled": true,
"clientKind": "git",
@@ -17,7 +20,6 @@
"enabled": true
},
"linter": {
- "ignore": ["test-apps"],
"enabled": true,
"rules": {
"recommended": true,
@@ -28,7 +30,8 @@
},
"style": {
"recommended": true,
- "noParameterAssign": "info"
+ "noParameterAssign": "info",
+ "noNonNullAssertion": "warn"
},
"complexity": {
"recommended": true
diff --git a/package-lock.json b/package-lock.json
index 0e82649..110d8d2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -23,7 +23,7 @@
},
"apps/cli": {
"name": "drizzle-lab",
- "version": "0.8.0",
+ "version": "0.9.0",
"license": "MIT",
"dependencies": {
"@drizzle-lab/api": "*",
diff --git a/packages/api/README.md b/packages/api/README.md
index cd52196..013b17b 100644
--- a/packages/api/README.md
+++ b/packages/api/README.md
@@ -35,26 +35,25 @@ Works for tables and views.
import { explain } from "@drizzle-lab/api/extensions";
import { pgTable, text, jsonb } from "drizzle-orm/pg-core";
- export const users = explain(
- pgTable("users", {
- id: text("id").primaryKey(),
- name: text("name").notNull(),
- metadata: jsonb("metadata").$type<{ role: string }>(),
- }), // or your table object
- {
- description: "Users table storing core user information",
- columns: {
- id: "Unique identifier for the user",
- name: "User's full name",
- metadata: "Additional user metadata stored as JSON"
+ export const users = pgTable("users", {
+ id: text("id").primaryKey(),
+ name: text("name").notNull(),
+ metadata: jsonb("metadata").$type<{ role: string }>(),
+ });
+
+ explain(users, {
+ description: "Users table storing core user information",
+ columns: {
+ id: "Unique identifier for the user",
+ name: "User's full name",
+ metadata: "Additional user metadata stored as JSON",
+ },
+ jsonShapes: {
+ metadata: {
+ role: "string",
},
- jsonShapes: {
- metadata: {
- role: "string"
- }
- }
- }
- );
+ },
+ });
```
### PostgreSQL API
diff --git a/packages/visualizer/src/compute.ts b/packages/visualizer/src/compute.ts
index 1f0f739..2860b25 100644
--- a/packages/visualizer/src/compute.ts
+++ b/packages/visualizer/src/compute.ts
@@ -106,8 +106,112 @@ export type ViewNodeDefinition = Node<
>;
// ReactFlow is scaling everything by the factor of 2
-const NODE_WIDTH = 1000;
-const NODE_ROW_HEIGHT = 150;
+const NODE_WIDTH = 600;
+const NODE_ROW_HEIGHT = 100;
+
+// Calculate the maximum width needed for a node based on its column names
+const getNodeWidth = (node: TableNodeDefinition | ViewNodeDefinition) => {
+ const columnWidths = node.data.columns.map(
+ (col) => (col.name.length + col.dataType.length) * 8,
+ );
+ const headerWidth = node.data.name.length * 8;
+ return Math.max(NODE_WIDTH, Math.max(...columnWidths, headerWidth) + 40); // Add padding just in case
+};
+
+const ITEM_HEIGHT = 100;
+
+// Calculate the height needed for a node based on its content
+const getNodeHeight = (node: TableNodeDefinition | ViewNodeDefinition) => {
+ // Base height for header
+ const baseHeight = NODE_ROW_HEIGHT;
+
+ if (node.type === "view") {
+ // Views, only have columns
+ const columnsHeight = Math.max(
+ node.data.columns.length * ITEM_HEIGHT,
+ NODE_ROW_HEIGHT,
+ );
+ return baseHeight + columnsHeight;
+ }
+
+ const tableNode = node as TableNodeDefinition;
+
+ // Calculate height for each component
+ const columnsHeight = node.data.columns.length * ITEM_HEIGHT;
+ const relationsHeight = tableNode.data.relations.length * ITEM_HEIGHT;
+ const policiesHeight = tableNode.data.policies.length * ITEM_HEIGHT;
+ const checksHeight = tableNode.data.checks.length * ITEM_HEIGHT;
+ const indexesHeight = tableNode.data.indexes.length * ITEM_HEIGHT;
+ const foreignKeysHeight = tableNode.data.foreignKeys.length * ITEM_HEIGHT;
+ const uniqueConstraintsHeight =
+ tableNode.data.uniqueConstraints.length * ITEM_HEIGHT;
+ const compositePrimaryKeysHeight =
+ tableNode.data.compositePrimaryKeys.length * ITEM_HEIGHT;
+
+ // Sum up all components
+ const totalComponentsHeight =
+ columnsHeight +
+ relationsHeight +
+ policiesHeight +
+ checksHeight +
+ indexesHeight +
+ foreignKeysHeight +
+ uniqueConstraintsHeight +
+ compositePrimaryKeysHeight;
+
+ return Math.max(baseHeight + totalComponentsHeight, NODE_ROW_HEIGHT);
+};
+
+// Determine optimal edge positions based on node connections
+const getNodeEdgePositions = (
+ nodeId: string,
+ edges: Edge[],
+ dagreGraph: dagre.graphlib.Graph,
+) => {
+ const currentNode = dagreGraph.node(nodeId);
+ const currentX = currentNode.x;
+
+ // Get connected nodes and their positions
+ const connectedNodes = edges
+ .filter((e) => e.source === nodeId || e.target === nodeId)
+ .map((e) => {
+ const connectedId = e.source === nodeId ? e.target : e.source;
+ const connectedNode = dagreGraph.node(connectedId);
+ // Filter out edges where the connected node doesn't exist in the graph (like auth.users)
+ if (!connectedNode) {
+ return null;
+ }
+
+ return {
+ id: connectedId,
+ isSource: e.source === nodeId,
+ x: connectedNode.x,
+ };
+ })
+ .filter((node): node is NonNullable => node !== null);
+
+ // If there's only one connection, align both positions to that side
+ if (connectedNodes.length === 1) {
+ const position =
+ connectedNodes[0].x > currentX ? Position.Right : Position.Left;
+ return { sourcePos: position, targetPos: position };
+ }
+
+ // Count nodes on each side
+ const leftNodes = connectedNodes.filter((n) => n.x < currentX);
+ const rightNodes = connectedNodes.filter((n) => n.x > currentX);
+
+ // If all connections are on one side, align both positions to that side
+ if (leftNodes.length > 0 && rightNodes.length === 0) {
+ return { sourcePos: Position.Left, targetPos: Position.Left };
+ }
+ if (rightNodes.length > 0 && leftNodes.length === 0) {
+ return { sourcePos: Position.Right, targetPos: Position.Right };
+ }
+
+ // Default case: connections on both sides
+ return { sourcePos: Position.Right, targetPos: Position.Left };
+};
// Supabase, thanks!
const getLayoutedElements = (
@@ -116,38 +220,85 @@ const getLayoutedElements = (
) => {
const dagreGraph = new dagre.graphlib.Graph();
dagreGraph.setDefaultEdgeLabel(() => ({}));
+
+ const RANK_GROUP_SIZE = 3; // Number of nodes per rank group
+
dagreGraph.setGraph({
rankdir: "LR",
- align: "UR",
- nodesep: 25,
- ranksep: 50,
+ align: "DL",
+ nodesep: 120, // Increased for better horizontal spacing
+ ranksep: 200, // Increased for better rank separation
+ ranker: "network-simplex",
+ marginx: 50,
+ marginy: 50,
});
+ // First, add all nodes to the graph
nodes.forEach((node) => {
+ const width = getNodeWidth(node);
+ const height = getNodeHeight(node);
dagreGraph.setNode(node.id, {
- width: NODE_WIDTH / 2.5,
- height: (NODE_ROW_HEIGHT / 2.5) * (node.data.columns.length + 1), // columns + header
+ width: width / 2.5,
+ height: height / 2.5,
});
});
+ // Add edges to the graph
edges.forEach((edge) => {
dagreGraph.setEdge(edge.source, edge.target);
});
+ // Find nodes with no relations
+ const connectedNodes = new Set();
+ edges.forEach((edge) => {
+ connectedNodes.add(edge.source);
+ connectedNodes.add(edge.target);
+ });
+
+ // Group nodes into ranks to create a more horizontal layout
+ const connectedNodesList = nodes.filter((node) =>
+ connectedNodes.has(node.id),
+ );
+ const unconnectedNodesList = nodes.filter(
+ (node) => !connectedNodes.has(node.id),
+ );
+
+ // Assign ranks to connected nodes to spread them horizontally
+ connectedNodesList.forEach((node, index) => {
+ const rankGroup = Math.floor(index / RANK_GROUP_SIZE);
+ dagreGraph.setNode(node.id, {
+ ...dagreGraph.node(node.id),
+ rank: rankGroup * 2,
+ });
+ });
+
+ // Place unconnected nodes on the far right
+ unconnectedNodesList.forEach((node) => {
+ dagreGraph.setNode(node.id, {
+ ...dagreGraph.node(node.id),
+ rank: 1000,
+ });
+ });
+
+ // Layout the graph
dagre.layout(dagreGraph);
+ // Apply the layout positions to the nodes
nodes.forEach((node) => {
const nodeWithPosition = dagreGraph.node(node.id);
- node.targetPosition = Position.Left;
- node.sourcePosition = Position.Right;
- // We are shifting the dagre node position (anchor=center center) to the top left
- // so it matches the React Flow node anchor point (top left).
+ const { sourcePos, targetPos } = getNodeEdgePositions(
+ node.id,
+ edges,
+ dagreGraph,
+ );
+
+ node.targetPosition = targetPos;
+ node.sourcePosition = sourcePos;
+
node.position = {
x: nodeWithPosition.x - nodeWithPosition.width / 2,
y: nodeWithPosition.y - nodeWithPosition.height / 2,
};
-
- return node;
});
return { nodes, edges };
diff --git a/packages/visualizer/src/visualizer.tsx b/packages/visualizer/src/visualizer.tsx
index db61549..515ce6d 100644
--- a/packages/visualizer/src/visualizer.tsx
+++ b/packages/visualizer/src/visualizer.tsx
@@ -5,14 +5,19 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { Badge } from "@repo/ui/components/badge";
import { Button } from "@repo/ui/components/button";
import { Icon } from "@repo/ui/components/icon";
-import { Label } from "@repo/ui/components/label";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@repo/ui/components/popover";
import { Separator } from "@repo/ui/components/separator";
-import { Switch } from "@repo/ui/components/switch";
+import { Toggle } from "@repo/ui/components/toggle";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@repo/ui/components/tooltip";
import { Typography } from "@repo/ui/components/typography";
import { cn } from "@repo/ui/utils/cn";
import type {
@@ -23,7 +28,6 @@ import type {
} from "@xyflow/react";
import {
Background,
- Controls,
Handle,
Position,
ReactFlow,
@@ -33,6 +37,9 @@ import {
applyNodeChanges,
PanOnScrollMode,
useKeyPress,
+ getNodesBounds,
+ useReactFlow,
+ getViewportForBounds,
} from "@xyflow/react";
import { toPng } from "html-to-image";
@@ -166,59 +173,66 @@ export function DrizzleVisualizer({
fitView
fitViewOptions={{ maxZoom: 1 }}
minZoom={0.05}
+ proOptions={{ hideAttribution: true }}
>
{loading && (
loading...
)}
-
-
- {hasDescription && (
-
-
-
{
+
+
+
+
{
+ compute(snapshot).then(({ nodes }) => {
+ onNodesChange(
+ nodes.map((node) => ({
+ id: node.id,
+ position: node.position,
+ type: "position",
+ })),
+ );
+ });
+ }}
+ />
+
+
+
+ {hasDescription && (
+ {
setNodes((prev) => {
return prev.map((node) => {
const update = {
...node,
};
- update.data.withExplain = checked;
+ update.data.withExplain = pressed;
return update;
});
});
- setWithExplain(checked);
+ setWithExplain(pressed);
}}
/>
-
- )}
-
-
+ )}
+
+
+
+
-
- {showMiniMap &&
}
+ {showMiniMap && (
+
+ )}
);
@@ -231,45 +245,45 @@ function TableNode({ data }: NodeProps) {
return (
<>
-
-
-
-
-
- {data.schema ? `${data.schema}.${data.name}` : data.name}
-
+
+
+
+
+
+
+ {data.schema ? `${data.schema}.${data.name}` : data.name}
+
+
+ {data.provider && (
+
+ {!data.isRLSEnabled && (
+
+ )}
+ RLS {data.isRLSEnabled ? "enabled" : "disabled"}
+
+ )}
- {data.provider && (
-
- {!data.isRLSEnabled && (
-
- )}
- RLS {data.isRLSEnabled ? "enabled" : "disabled"}
-
+ {data.withExplain && data.description && (
+
)}
- {data.withExplain && data.description && (
-
- {data.description}
-
- )}
{data.columns.map((column) => {
return (
-
-
+
+
{column.isPrimaryKey && (
@@ -370,12 +384,7 @@ function TableNode({ data }: NodeProps
) {
)}
{data.withExplain && column.description && (
-
- {column.description}
-
+
)}
) {
return (
<>
-
-
-
+
+
+
@@ -604,7 +613,7 @@ function ViewNode({ data }: NodeProps) {
data-no-print
variant="ghost"
size="sm"
- className="border-none"
+ className="h-6 border-none"
>
Definition
@@ -629,20 +638,18 @@ function ViewNode({ data }: NodeProps) {
{data.withExplain && data.description && (
-
- {data.description}
-
+
)}
{data.columns.map((column) => {
return (
-
-
+
+
{column.isPrimaryKey && (
@@ -709,12 +716,7 @@ function ViewNode({ data }: NodeProps) {
)}
{data.withExplain && column.description && (
-
- {column.description}
-
+
)}
) {
);
}
+function Description({ description }: { description: string }) {
+ return (
+
+
+
+
+
+ {description}
+
+
+ );
+}
+
+function AutoLayoutButton(props: React.ComponentPropsWithoutRef<"button">) {
+ return (
+
+
+
+
+
+
+ Automatically layout the nodes
+
+
+
+ );
+}
+
+function FitViewButton() {
+ const { fitView } = useReactFlow();
+ return (
+
+
+
+
+
+
+ Set the viewport to fit the diagram
+
+
+
+ );
+}
+
+function InfoButton() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function ExplainToggle(props: React.ComponentPropsWithoutRef) {
+ return (
+
+
+
+
+
+
+
+
+ Display the schema documentation
+
+
+
+ );
+}
+
function downloadImage(dataUrl: string) {
const a = document.createElement("a");
@@ -752,37 +871,125 @@ function downloadImage(dataUrl: string) {
a.click();
}
-// const imageWidth = 1024;
-// const imageHeight = 768;
+const PADDING = 100; // Add padding around the nodes
+const MIN_DIMENSION = 1024; // Minimum dimension to ensure quality
+const MAX_DIMENSION = 4096; // Maximum dimension to prevent excessive file size
export function DownloadSchemaButton() {
- // const { getNodes } = useReactFlow();
+ const { getNodes } = useReactFlow();
+ const [isGenerating, setIsGenerating] = useState(false);
+
const onClick = async () => {
- // we calculate a transform for the nodes so that all nodes are visible
- // we then overwrite the transform of the `.react-flow__viewport` element
- // with the style option of the html-to-image library
- // const nodesBounds = getNodesBounds(getNodes());
- // const viewport = getViewportForBounds(
- // nodesBounds,
- // imageWidth,
- // imageHeight,
- // 0.5,
- // 2,
- // 2,
- // );
-
- toPng(document.querySelector(".react-flow__viewport") as HTMLElement, {
- skipFonts: true,
- backgroundColor: "#0f0f14",
- filter: (node) => {
- return !node.dataset?.noPrint;
- },
- }).then(downloadImage);
+ setIsGenerating(true);
+
+ try {
+ const nodes = getNodes();
+ const nodesBounds = getNodesBounds(nodes);
+
+ // Add padding to the bounds
+ nodesBounds.x -= PADDING;
+ nodesBounds.y -= PADDING;
+ nodesBounds.width += 2 * PADDING;
+ nodesBounds.height += 2 * PADDING;
+
+ // Calculate dimensions while maintaining aspect ratio
+ const aspectRatio = nodesBounds.width / nodesBounds.height;
+ let imageWidth, imageHeight;
+
+ if (aspectRatio > 1) {
+ // Wider than tall
+ imageWidth = Math.min(
+ Math.max(nodesBounds.width, MIN_DIMENSION),
+ MAX_DIMENSION,
+ );
+ imageHeight = imageWidth / aspectRatio;
+ } else {
+ // Taller than wide
+ imageHeight = Math.min(
+ Math.max(nodesBounds.height, MIN_DIMENSION),
+ MAX_DIMENSION,
+ );
+ imageWidth = imageHeight * aspectRatio;
+ }
+
+ // Round dimensions to integers
+ imageWidth = Math.round(imageWidth);
+ imageHeight = Math.round(imageHeight);
+
+ // Create a hidden container - Prevents UI flickering while generating
+ const hiddenContainer = document.createElement("div");
+ hiddenContainer.style.position = "absolute";
+ hiddenContainer.style.left = "-99999px";
+ hiddenContainer.style.width = `${imageWidth}px`;
+ hiddenContainer.style.height = `${imageHeight}px`;
+ document.body.appendChild(hiddenContainer);
+
+ // Clone the viewport into the hidden container
+ const viewport = document.querySelector(
+ ".react-flow__viewport",
+ ) as HTMLElement;
+
+ if (!viewport) {
+ return;
+ }
+
+ const viewportClone = viewport.cloneNode(true) as HTMLElement;
+ hiddenContainer.appendChild(viewportClone);
+
+ // Calculate and apply transform
+ const transform = getViewportForBounds(
+ nodesBounds,
+ imageWidth,
+ imageHeight,
+ 0.1, // Lower minZoom to handle spread out tables better
+ 1, // maxZoom
+ 1.2, // Slightly increase padding factor
+ );
+
+ viewportClone.style.transform = `translate(${transform.x}px, ${transform.y}px) scale(${transform.zoom})`;
+
+ try {
+ const dataUrl = await toPng(viewportClone, {
+ width: imageWidth,
+ height: imageHeight,
+ skipFonts: true,
+ backgroundColor: "#0f0f14",
+ filter: (node) => {
+ return !node.dataset?.noPrint;
+ },
+ });
+
+ downloadImage(dataUrl);
+ } finally {
+ // Clean up
+ document.body.removeChild(hiddenContainer);
+ }
+ } finally {
+ setIsGenerating(false);
+ }
};
return (
-
+
+
+
+
+
+
+ Download the diagram as a PNG image
+
+
+
);
}
diff --git a/shared/ui/other/sly.json b/shared/ui/other/sly.json
index e974c7c..b7961ff 100644
--- a/shared/ui/other/sly.json
+++ b/shared/ui/other/sly.json
@@ -4,7 +4,7 @@
{
"name": "lucide-icons",
"directory": "./other/svg-icons",
- "postinstall": ["npm", "run", "build"],
+ "postinstall": ["npm", "run", "build:icons"],
"transformers": []
}
]
diff --git a/shared/ui/other/svg-icons/captions-off.svg b/shared/ui/other/svg-icons/captions-off.svg
new file mode 100644
index 0000000..8f018c3
--- /dev/null
+++ b/shared/ui/other/svg-icons/captions-off.svg
@@ -0,0 +1,20 @@
+
+
+
diff --git a/shared/ui/other/svg-icons/captions.svg b/shared/ui/other/svg-icons/captions.svg
new file mode 100644
index 0000000..12111df
--- /dev/null
+++ b/shared/ui/other/svg-icons/captions.svg
@@ -0,0 +1,16 @@
+
+
+
\ No newline at end of file
diff --git a/shared/ui/other/svg-icons/info.svg b/shared/ui/other/svg-icons/info.svg
new file mode 100644
index 0000000..280a014
--- /dev/null
+++ b/shared/ui/other/svg-icons/info.svg
@@ -0,0 +1,17 @@
+
+
+
diff --git a/shared/ui/other/svg-icons/shrink.svg b/shared/ui/other/svg-icons/shrink.svg
new file mode 100644
index 0000000..d566c70
--- /dev/null
+++ b/shared/ui/other/svg-icons/shrink.svg
@@ -0,0 +1,18 @@
+
+
+
diff --git a/shared/ui/src/components/button.tsx b/shared/ui/src/components/button.tsx
index 43f5814..6535436 100644
--- a/shared/ui/src/components/button.tsx
+++ b/shared/ui/src/components/button.tsx
@@ -19,7 +19,7 @@ const buttonVariants = cva(
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost:
- "hover:bg-accent hover:text-accent-foreground group-[.active]:bg-accent group-[.active]:text-accent-foreground",
+ "group-[.active]:bg-accent group-[.active]:text-accent-foreground hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
@@ -28,6 +28,7 @@ const buttonVariants = cva(
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "size-9",
+ ["icon:sm"]: "size-8",
},
},
defaultVariants: {
diff --git a/shared/ui/src/components/toggle.tsx b/shared/ui/src/components/toggle.tsx
index f54a2e6..d4e6025 100644
--- a/shared/ui/src/components/toggle.tsx
+++ b/shared/ui/src/components/toggle.tsx
@@ -6,7 +6,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "../utils/cn";
const toggleVariants = cva(
- "group inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
+ "group inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors data-[state=on]:bg-accent data-[state=on]:text-accent-foreground hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
@@ -18,6 +18,8 @@ const toggleVariants = cva(
default: "h-9 px-3",
sm: "h-8 px-2",
lg: "h-10 px-3",
+ icon: "size-9",
+ ["icon:sm"]: "size-8",
},
},
defaultVariants: {
diff --git a/shared/ui/src/icons/name.d.ts b/shared/ui/src/icons/name.d.ts
index 29033e7..7d1da40 100644
--- a/shared/ui/src/icons/name.d.ts
+++ b/shared/ui/src/icons/name.d.ts
@@ -13,6 +13,8 @@
| "bug"
| "cable"
| "camera"
+ | "captions-off"
+ | "captions"
| "check"
| "chevron-right"
| "chevrons-up-down"
@@ -36,6 +38,7 @@
| "github"
| "history"
| "image-down"
+ | "info"
| "key-round"
| "link"
| "list-restart"
@@ -51,6 +54,7 @@
| "sheet"
| "shield-check"
| "shield"
+ | "shrink"
| "sprout"
| "square-terminal"
| "telescope"
diff --git a/shared/ui/src/icons/sprite.svg b/shared/ui/src/icons/sprite.svg
index fdc4898..9306ff4 100644
--- a/shared/ui/src/icons/sprite.svg
+++ b/shared/ui/src/icons/sprite.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/vscode-extension/CHANGELOG.md b/vscode-extension/CHANGELOG.md
index 9d7bd28..0382d36 100644
--- a/vscode-extension/CHANGELOG.md
+++ b/vscode-extension/CHANGELOG.md
@@ -4,6 +4,11 @@ All notable changes to the "drizzle-orm" extension will be documented in this fi
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
+## [0.8.0]
+
+- New visualizer UI
+- Auto layout improvements
+
## [0.7.0]
- Add support for MySQL
diff --git a/vscode-extension/README.md b/vscode-extension/README.md
index 754ba26..06d01da 100644
--- a/vscode-extension/README.md
+++ b/vscode-extension/README.md
@@ -1,6 +1,4 @@
# Drizzle ORM VSCode Extension
-## Features
-- Drizzle Schema Visualizer
-
+
diff --git a/vscode-extension/package-lock.json b/vscode-extension/package-lock.json
index 5960940..3376377 100644
--- a/vscode-extension/package-lock.json
+++ b/vscode-extension/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "vscode-drizzle-orm",
- "version": "0.7.0",
+ "version": "0.8.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "vscode-drizzle-orm",
- "version": "0.7.0",
+ "version": "0.8.0",
"license": "MIT",
"devDependencies": {
"@types/mocha": "^10.0.9",
diff --git a/vscode-extension/package.json b/vscode-extension/package.json
index 65f5899..d25c11f 100644
--- a/vscode-extension/package.json
+++ b/vscode-extension/package.json
@@ -3,7 +3,7 @@
"displayName": "Drizzle ORM",
"description": "Adds schema visualizer for Drizzle ORM",
"preview": true,
- "version": "0.7.0",
+ "version": "0.8.0",
"private": true,
"icon": "icon.png",
"license": "MIT",