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 ( + + + + + +
+ + Check how + + + you can document your schema! + +
+ +
+ + This diagram is powered by{" "} + + + React Flow + +
+
+
+ ); +} + +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 - image +image 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",