diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index f1d8dadcf1..3c6d7a8b01 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -25,6 +25,9 @@ export { SquiggleErrorAlert } from "./components/ui/SquiggleErrorAlert.js"; export { RelativeValueCell } from "./widgets/PlotWidget/RelativeValuesGridChart/RelativeValueCell.js"; +export { MarkdownViewer } from "./lib/MarkdownViewer.js"; +export { CodeSyntaxHighlighter } from "./lib/CodeSyntaxHighlighter.js"; + // for use in relative values export { type DrawContext, diff --git a/packages/components/src/lib/CodeSyntaxHighlighter.tsx b/packages/components/src/lib/CodeSyntaxHighlighter.tsx new file mode 100644 index 0000000000..1077d6c078 --- /dev/null +++ b/packages/components/src/lib/CodeSyntaxHighlighter.tsx @@ -0,0 +1,79 @@ +import React, { FC, HTMLAttributes, useEffect, useState } from "react"; +import { + type BundledLanguage, + bundledLanguages, + getHighlighter, + type Highlighter, + type LanguageRegistration, +} from "shikiji"; + +// Import assertion here would be nice, but storybook doesn't support "assert" syntax yet, only "with" syntax, +// and our import sorter auto-replaces with the newer "assert" syntax. +// This will be fixed in storybook eventually, https://github.com/storybookjs/storybook/issues/23599 +import squiggleGrammar from "@quri/squiggle-textmate-grammar/dist/squiggle.tmLanguage.json"; + +type SupportedLanguage = BundledLanguage | "squiggle"; + +let _shiki: Highlighter; // cached singleton +type Theme = "vitesse-light" | "github-light"; + +async function codeToHtml(params: { + code: string; + language: SupportedLanguage; + theme?: Theme; +}) { + const _theme = params.theme || "vitesse-light"; + if (!_shiki) { + _shiki = await getHighlighter({ + themes: ["vitesse-light", "github-light"], + langs: [ + { + name: "squiggle", + ...squiggleGrammar, + } as unknown as LanguageRegistration, // shikiji requires repository.$self and repository.$base that we don't have + ], + }); + } + if ( + params.language !== "squiggle" && + !_shiki.getLoadedLanguages().includes(params.language) + ) { + await _shiki.loadLanguage(params.language); + await _shiki.loadLanguage(params.language); // somehow repeating it twice fixes the loading issue + } + + return _shiki.codeToHtml(params.code, { + theme: _theme, // TODO - write a custom theme that would match Codemirror styles + lang: params.language, + }); +} + +function isSupportedLanguage(language: string): language is BundledLanguage { + return language === "squiggle" || language in bundledLanguages; +} + +export const CodeSyntaxHighlighter: FC< + { children: string; language: string; theme?: Theme } & Omit< + HTMLAttributes, + "children" + > +> = ({ children, language, theme, ...rest }) => { + const [html, setHtml] = useState(children); + + // Syntax-highlighted blocks will start unstyled, that's fine. + useEffect(() => { + (async () => { + if (isSupportedLanguage(language)) { + setHtml(await codeToHtml({ code: children, language, theme })); + } + })(); + }); + + return ( +
+  );
+};
diff --git a/packages/components/src/lib/MarkdownViewer.tsx b/packages/components/src/lib/MarkdownViewer.tsx
index 82571e5dbc..f0ae817105 100644
--- a/packages/components/src/lib/MarkdownViewer.tsx
+++ b/packages/components/src/lib/MarkdownViewer.tsx
@@ -1,58 +1,12 @@
 import clsx from "clsx";
 import { Element } from "hast";
-import React, { FC, HTMLAttributes, useEffect, useState } from "react";
+import React from "react";
 import ReactMarkdown from "react-markdown";
 import remarkGfm from "remark-gfm";
-import {
-  type BundledLanguage,
-  bundledLanguages,
-  getHighlighter,
-  type Highlighter,
-  type LanguageRegistration,
-} from "shikiji";
 import { Node, Parent } from "unist";
 import { visitParents } from "unist-util-visit-parents";
 
-// Import assertion here would be nice, but storybook doesn't support "assert" syntax yet, only "with" syntax,
-// and our import sorter auto-replaces with the newer "assert" syntax.
-// This will be fixed in storybook eventually, https://github.com/storybookjs/storybook/issues/23599
-import squiggleGrammar from "@quri/squiggle-textmate-grammar/dist/squiggle.tmLanguage.json";
-
-type SupportedLanguage = BundledLanguage | "squiggle";
-
-let _shiki: Highlighter; // cached singleton
-async function codeToHtml(params: {
-  code: string;
-  language: SupportedLanguage;
-}) {
-  if (!_shiki) {
-    _shiki = await getHighlighter({
-      themes: ["vitesse-light"],
-      langs: [
-        {
-          name: "squiggle",
-          ...squiggleGrammar,
-        } as unknown as LanguageRegistration, // shikiji requires repository.$self and repository.$base that we don't have
-      ],
-    });
-  }
-  if (
-    params.language !== "squiggle" &&
-    !_shiki.getLoadedLanguages().includes(params.language)
-  ) {
-    await _shiki.loadLanguage(params.language);
-    await _shiki.loadLanguage(params.language); // somehow repeating it twice fixes the loading issue
-  }
-
-  return _shiki.codeToHtml(params.code, {
-    theme: "vitesse-light", // TODO - write a custom theme that would match Codemirror styles
-    lang: params.language,
-  });
-}
-
-function isSupportedLanguage(language: string): language is BundledLanguage {
-  return language === "squiggle" || language in bundledLanguages;
-}
+import { CodeSyntaxHighlighter } from "./CodeSyntaxHighlighter.js";
 
 // Adds `inline` property to `code` elements, to distinguish between inline and block code snippets.
 function rehypeInlineCodeProperty() {
@@ -71,43 +25,19 @@ function rehypeInlineCodeProperty() {
   };
 }
 
-const SyntaxHighlighter: FC<
-  { children: string; language: string } & Omit<
-    HTMLAttributes,
-    "children"
-  >
-> = ({ children, language, ...rest }) => {
-  const [html, setHtml] = useState(children);
-
-  // Syntax-highlighted blocks will start unstyled, that's fine.
-  useEffect(() => {
-    (async () => {
-      if (isSupportedLanguage(language)) {
-        setHtml(await codeToHtml({ code: children, language }));
-      }
-    })();
-  });
-
-  return (
-    
- ); -}; - type MarkdownViewerProps = { md: string; textSize: "sm" | "xs"; textColor?: "prose-stone" | "prose-slate"; className?: string; + backgroundColor?: string; }; export const MarkdownViewer: React.FC = ({ md, className, textColor, textSize, + backgroundColor = "bg-slate-50", }) => { return ( = ({ components={{ pre({ children }) { return ( -
+            
               {children}
             
); @@ -142,9 +77,9 @@ export const MarkdownViewer: React.FC = ({ ); } return match ? ( - + {String(children).replace(/\n$/, "")} - + ) : ( {children} ); diff --git a/packages/components/src/stories/CodeSyntaxHighlighter.stories.tsx b/packages/components/src/stories/CodeSyntaxHighlighter.stories.tsx new file mode 100644 index 0000000000..e95fe62654 --- /dev/null +++ b/packages/components/src/stories/CodeSyntaxHighlighter.stories.tsx @@ -0,0 +1,45 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { CodeSyntaxHighlighter } from "../lib/CodeSyntaxHighlighter.js"; + +/** + * The number shower is a simple component to display a number. + * It uses the symbols "K", "M", "B", and "T", to represent thousands, millions, billions, and trillions. Outside of that range, it uses scientific notation. + */ +const meta = { + component: CodeSyntaxHighlighter, +} satisfies Meta; +export default meta; +type Story = StoryObj; + +export const Squiggle: Story = { + name: "Squiggle Code", + + args: { + language: "squiggle", + children: `/* This is a comment */ +const foo = "bar"; +normal(5, 1) +normal({ p5: 4, p95: 10 }) +normal({ p10: 5, p95: 9 }) +normal({ p25: 5, p75: 9 }) +normal({ mean: 5, stdev: 2 }) +normal(5 to 10, normal(3, 2)) +normal({ mean: uniform(5, 9), stdev: 3 }) +`, + }, +}; + +export const JS: Story = { + name: "Javascript", + + args: { + language: "javascript", + children: `const meta = { + component: CodeSyntaxHighlighter, + } satisfies Meta; + export default meta; + type Story = StoryObj; +`, + }, +}; diff --git a/packages/hub/src/app/FrontPageModelList.tsx b/packages/hub/src/app/FrontPageModelList.tsx index 3a6c172c9e..1f37ef31f9 100644 --- a/packages/hub/src/app/FrontPageModelList.tsx +++ b/packages/hub/src/app/FrontPageModelList.tsx @@ -12,7 +12,7 @@ const Fragment = graphql` fragment FrontPageModelList on Query @argumentDefinitions( cursor: { type: "String" } - count: { type: "Int", defaultValue: 20 } + count: { type: "Int", defaultValue: 8 } ) @refetchable(queryName: "FrontPageModelListPaginationQuery") { models(first: $count, after: $cursor) diff --git a/packages/hub/src/app/models/[owner]/[slug]/ModelEntityNodes.tsx b/packages/hub/src/app/models/[owner]/[slug]/ModelEntityNodes.tsx index 378f23964f..5ef879cc98 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/ModelEntityNodes.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/ModelEntityNodes.tsx @@ -3,7 +3,7 @@ import { useParams, usePathname } from "next/navigation"; import { FC } from "react"; -import { CodeBracketIcon, EmptyIcon, ScaleIcon, ShareIcon } from "@quri/ui"; +import { CodeBracketSquareIcon, EmptyIcon, ShareIcon } from "@quri/ui"; import { EntityInfo } from "@/components/EntityInfo"; import { type EntityNode } from "@/components/EntityLayout"; @@ -48,7 +48,7 @@ function entityNodes( { slug, href: modelRoute({ owner: owner.slug, slug }), - icon: CodeBracketIcon, + icon: CodeBracketSquareIcon, }, ]; @@ -60,7 +60,6 @@ function entityNodes( slug, variableName: variable.name, }), - icon: ScaleIcon, }); } diff --git a/packages/hub/src/app/models/[owner]/[slug]/ModelLayout.tsx b/packages/hub/src/app/models/[owner]/[slug]/ModelLayout.tsx index 728f534127..f0e05c6b7f 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/ModelLayout.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/ModelLayout.tsx @@ -3,7 +3,7 @@ import { FC, PropsWithChildren } from "react"; import { graphql } from "relay-runtime"; -import { CodeBracketIcon, RectangleStackIcon, ShareIcon } from "@quri/ui"; +import { CodeBracketSquareIcon, RectangleStackIcon, ShareIcon } from "@quri/ui"; import { EntityLayout } from "@/components/EntityLayout"; import { EntityTab } from "@/components/ui/EntityTab"; @@ -120,7 +120,11 @@ export const ModelLayout: FC< headerLeft={} headerRight={ - + {Boolean(_totalImportLength) && ( { + switch (status) { + case "Success": + return ; + case "Failure": + return ; + case "Pending": + return ; + case "Skipped": + return ; + } +}; +type VariableRevisions = VariablePage$data["revisions"]; const VariableRevisionsPanel: FC<{ revisions: VariableRevisions; selected: string; @@ -27,27 +44,47 @@ const VariableRevisionsPanel: FC<{ loadNext?: (count: number) => void; }> = ({ revisions, selected, changeId, loadNext }) => { return ( -
+

Revisions

-
    - {revisions.edges.map(({ node: revision }) => ( -
  • changeId(revision.id)} - className={clsx( - "cursor-pointer pb-0.5 pt-0.5 text-sm hover:text-gray-800 hover:underline", - revision.id === selected ? "text-blue-900" : "text-gray-400" - )} - > - {format( - new Date(revision.modelRevision.createdAtTimestamp), - "MMM dd, yyyy" - )} -
  • - ))} -
+
+ {revisions.edges.map(({ node: revision }) => { + const Icon = exportTypeIcon(revision.variableType || ""); + return ( +
+
+ changeId(revision.id)} + > + {format( + new Date(revision.modelRevision.createdAtTimestamp), + "MMM dd h:mm a" + )} + +
+
+ +
+
+ {buildStatusIcon(revision.modelRevision.buildStatus)} +
+
+ ); + })} +
{loadNext && }
); @@ -81,6 +118,7 @@ export const VariablePageBody: FC<{ node { id variableName + variableType modelRevision { id createdAtTimestamp @@ -88,6 +126,7 @@ export const VariablePageBody: FC<{ __typename ...SquiggleVariableRevisionPage } + buildStatus } } } diff --git a/packages/hub/src/components/EntityCard.tsx b/packages/hub/src/components/EntityCard.tsx index b01211dfc5..9116c1b2ea 100644 --- a/packages/hub/src/components/EntityCard.tsx +++ b/packages/hub/src/components/EntityCard.tsx @@ -1,14 +1,14 @@ +import clsx from "clsx"; import Link from "next/link"; import React, { FC, PropsWithChildren } from "react"; import { LockIcon } from "@quri/ui"; import { EntityNode } from "./EntityInfo"; -import { Card } from "./ui/Card"; export type { EntityNode }; -function formatDate(date: Date): string { +export function formatDate(date: Date): string { const options: Intl.DateTimeFormatOptions = { year: "numeric", month: "long", @@ -17,50 +17,102 @@ function formatDate(date: Date): string { return date.toLocaleDateString("en-US", options); } +export const entityCardBadgeCss = (presentAsLink: boolean) => { + return clsx( + "flex items-center", + presentAsLink && + "cursor-pointer hover:text-gray-900 hover:underline select-none" + ); +}; + +export const EntityCardBadge: FC< + PropsWithChildren<{ presentAsLink: boolean }> +> = ({ presentAsLink, children }) => ( +
{children}
+); + +export const PrivateBadge: FC = () => ( + + + Private + +); + +export const UpdatedStatus: FC<{ time: number }> = ({ time }) => ( + + {"Updated"} + + +); + +export const keepFirstNLines = (str: string, n: number) => + str.split("\n").slice(0, n).join("\n"); + +const InterspersedMenuItems: React.FC<{ + items: React.ReactNode[]; + interspercedItem: React.ReactNode; +}> = ({ items, interspercedItem }) => { + const filteredItems = items.filter(Boolean); + const interspersedItems = filteredItems.reduce( + (result: React.ReactNode[], item, index) => { + result.push(item); + if (index < filteredItems.length - 1) { + result.push(
{interspercedItem}
); + } + return result; + }, + [] as React.ReactNode[] + ); + + return <>{interspersedItems}; +}; + +export const InterspersedMenuItemsWithDots: React.FC<{ + items: React.ReactNode[]; +}> = ({ items }) => ( + {"\u00B7"}} + /> +); + type Props = PropsWithChildren<{ - updatedAtTimestamp: number; href: string; showOwner: boolean; - isPrivate?: boolean; ownerName?: string; slug: string; - footerItems?: React.ReactElement; + menuItems?: React.ReactElement; }>; export const EntityCard: FC = ({ - updatedAtTimestamp, href, showOwner, - isPrivate, ownerName, slug, children, - footerItems, + menuItems, }) => { return ( - +
-
+
{showOwner ? ownerName + "/" : ""} {slug}
- {
{children}
} -
- {footerItems} - {isPrivate && } -
- Updated - -
+
+ {menuItems}
+ {children && ( +
{children}
+ )}
- +
); }; diff --git a/packages/hub/src/components/EntityInfo.tsx b/packages/hub/src/components/EntityInfo.tsx index 03fdff203e..7076bcd784 100644 --- a/packages/hub/src/components/EntityInfo.tsx +++ b/packages/hub/src/components/EntityInfo.tsx @@ -35,14 +35,14 @@ const Entity: FC = ({ !isFirst && "pl-3" )} > - {Icon && ( + {!isLast && Icon && ( )}
{slug}
diff --git a/packages/hub/src/components/EntityLayout.tsx b/packages/hub/src/components/EntityLayout.tsx index 179bc4dfa8..dc5e6404b7 100644 --- a/packages/hub/src/components/EntityLayout.tsx +++ b/packages/hub/src/components/EntityLayout.tsx @@ -23,10 +23,7 @@ export const EntityLayout: FC = ({ }) => { return (
-
+
e !== ""); + return errors.length === 0 ? "Success" : "Failure"; } return revision.model.currentRevisionId === revision.id diff --git a/packages/hub/src/graphql/types/ModelRevisionBuild.ts b/packages/hub/src/graphql/types/ModelRevisionBuild.ts index aa77c3138a..56c5d7bb39 100644 --- a/packages/hub/src/graphql/types/ModelRevisionBuild.ts +++ b/packages/hub/src/graphql/types/ModelRevisionBuild.ts @@ -7,7 +7,9 @@ export const ModelRevisionBuild = builder.prismaNode("ModelRevisionBuild", { resolve: (build) => build.createdAt.getTime(), }), modelRevision: t.relation("modelRevision"), - errors: t.exposeStringList("errors"), + errors: t.stringList({ + resolve: (build) => build.errors.filter((e) => e !== ""), + }), runSeconds: t.exposeFloat("runSeconds"), }), }); diff --git a/packages/hub/src/groups/components/GroupCard.tsx b/packages/hub/src/groups/components/GroupCard.tsx index 596a8e20da..a18313a7fb 100644 --- a/packages/hub/src/groups/components/GroupCard.tsx +++ b/packages/hub/src/groups/components/GroupCard.tsx @@ -2,7 +2,7 @@ import { FC } from "react"; import { useFragment } from "react-relay"; import { graphql } from "relay-runtime"; -import { EntityCard } from "@/components/EntityCard"; +import { EntityCard, UpdatedStatus } from "@/components/EntityCard"; import { groupRoute } from "@/routes"; import { GroupCard$key } from "@/__generated__/GroupCard.graphql"; @@ -24,12 +24,12 @@ export const GroupCard: FC = ({ groupRef }) => { return ( } /> ); }; diff --git a/packages/hub/src/models/components/ModelCard.tsx b/packages/hub/src/models/components/ModelCard.tsx index 59aef2a065..a041cd9c2c 100644 --- a/packages/hub/src/models/components/ModelCard.tsx +++ b/packages/hub/src/models/components/ModelCard.tsx @@ -1,14 +1,24 @@ +import Link from "next/link"; import { FC } from "react"; import { useFragment } from "react-relay"; import { graphql } from "relay-runtime"; -import { EntityCard } from "@/components/EntityCard"; +import { CodeSyntaxHighlighter, NumberShower } from "@quri/squiggle-components"; +import { XIcon } from "@quri/ui"; + +import { + EntityCardBadge, + InterspersedMenuItemsWithDots, + keepFirstNLines, + PrivateBadge, + UpdatedStatus, +} from "@/components/EntityCard"; import { totalImportLength, VariableRevision, VariablesDropdown, } from "@/lib/VariablesDropdown"; -import { modelRoute } from "@/routes"; +import { modelRoute, ownerRoute } from "@/routes"; import { ModelCard$key } from "@/__generated__/ModelCard.graphql"; @@ -18,6 +28,7 @@ const Fragment = graphql` slug updatedAtTimestamp owner { + __typename slug } isPrivate @@ -29,12 +40,28 @@ const Fragment = graphql` } } currentRevision { + content { + __typename + ... on SquiggleSnippet { + id + code + version + seed + autorunMode + sampleCount + xyPointLength + } + } relativeValuesExports { variableName definition { slug } } + buildStatus + lastBuild { + runSeconds + } } } `; @@ -44,56 +71,120 @@ type Props = { showOwner?: boolean; }; +const OwnerLink: FC<{ owner: { __typename: string; slug: string } }> = ({ + owner, +}) => ( + + {owner.slug} + +); + +const ModelLink: FC<{ owner: string; slug: string }> = ({ owner, slug }) => ( + + {slug} + +); + +const BuildFailedBadge: FC = () => ( +
+ +
Build Failed
+
+); + +const RunTime: FC<{ seconds: number }> = ({ seconds }) => ( +
+ s +
+); + export const ModelCard: FC = ({ modelRef, showOwner = true }) => { const model = useFragment(Fragment, modelRef); + const { + owner, + slug, + updatedAtTimestamp, + isPrivate, + variables, + currentRevision, + } = model; - const modelUrl = modelRoute({ - owner: model.owner.slug, - slug: model.slug, - }); - - const variableRevisions: VariableRevision[] = model.variables.map((v) => ({ + const variableRevisions: VariableRevision[] = variables.map((v) => ({ variableName: v.variableName, variableType: v.currentRevision?.variableType, title: v.currentRevision?.title || undefined, docString: undefined, })); - const relativeValuesExports = model.currentRevision.relativeValuesExports.map( + const relativeValuesExports = currentRevision.relativeValuesExports.map( ({ variableName, definition: { slug } }) => ({ variableName, slug, }) ); - const _totalImportLength = totalImportLength( + const totalImportCount = totalImportLength( variableRevisions, relativeValuesExports ); + const { buildStatus, lastBuild, content } = currentRevision; + const body = + content.__typename === "SquiggleSnippet" ? content.code : undefined; - const footerItems = - _totalImportLength > 0 ? ( - -
- {`${_totalImportLength} variables`} -
-
- ) : undefined; - - return ( - 0 && ( + + {`${totalImportCount} variables`} + + ), + isPrivate && , + updatedAtTimestamp && ( + + ), + buildStatus === "Failure" && ( + + ), + lastBuild?.runSeconds && buildStatus !== "Failure" && ( + + ), + ]} /> ); + return ( +
+
+ {showOwner && } + {showOwner && /} + +
+
+ {menuItems} +
+ {body && ( +
+
+ + {keepFirstNLines(body, 10)} + +
+
+ )} +
+ ); }; diff --git a/packages/hub/src/models/components/ModelList.tsx b/packages/hub/src/models/components/ModelList.tsx index 295b392f45..88a98bac9a 100644 --- a/packages/hub/src/models/components/ModelList.tsx +++ b/packages/hub/src/models/components/ModelList.tsx @@ -37,7 +37,7 @@ export const ModelList: FC = ({ return (
-
+
{connection.edges.map((edge) => ( = ({ return ( = ({ showOwner={showOwner} ownerName={definition.owner.slug} slug={definition.slug} + menuItems={ + <> + + + } /> ); }; diff --git a/packages/hub/src/variables/components/VariableCard.tsx b/packages/hub/src/variables/components/VariableCard.tsx index 1f2ec960dd..8c68399f19 100644 --- a/packages/hub/src/variables/components/VariableCard.tsx +++ b/packages/hub/src/variables/components/VariableCard.tsx @@ -1,11 +1,19 @@ -import truncate from "lodash/truncate"; +import Link from "next/link"; import { FC } from "react"; -import ReactMarkdown from "react-markdown"; import { useFragment } from "react-relay"; import { graphql } from "relay-runtime"; -import remarkGfm from "remark-gfm"; -import { EntityCard } from "@/components/EntityCard"; +import { MarkdownViewer } from "@quri/squiggle-components"; +import { CodeBracketSquareIcon } from "@quri/ui"; + +import { + EntityCardBadge, + entityCardBadgeCss, + InterspersedMenuItemsWithDots, + keepFirstNLines, + PrivateBadge, + UpdatedStatus, +} from "@/components/EntityCard"; import { exportTypeIcon } from "@/lib/typeIcon"; import { modelRoute, variableRoute } from "@/routes"; @@ -50,52 +58,60 @@ export const VariableCard: FC = ({ variableRef }) => { const Icon = exportTypeIcon(currentRevision.variableType || ""); // This will have problems with markdown tags, but I looked into markdown-truncation packages, and they can get complicated. Will try this for now. - const docstring = - (currentRevision.docstring && - truncate(currentRevision.docstring, { - length: 500, - separator: " ", - omission: "...", - })) || - undefined; + + const { createdAtTimestamp } = currentRevision.modelRevision; return ( - +
+
+
+ + {variable.currentRevision?.title || variable.variableName} + +
+
+ {`${variable.owner.slug}/${variable.model.slug}`} -
- - {currentRevision.variableType} +
+
+ + + {currentRevision.variableType} + , + , + variable.model.isPrivate && , + ]} + /> +
+
+ {currentRevision.docstring && ( +
+
+
- - } - > - {docstring && ( - - {docstring} - +
)} - +
); }; diff --git a/packages/hub/src/variables/components/VariableList.tsx b/packages/hub/src/variables/components/VariableList.tsx index b1c3d9fd9f..00a7c31168 100644 --- a/packages/hub/src/variables/components/VariableList.tsx +++ b/packages/hub/src/variables/components/VariableList.tsx @@ -32,7 +32,7 @@ export const VariableList: FC = ({ connectionRef, loadNext }) => { return (
-
+
{connection.edges.map((edge) => ( ))} diff --git a/packages/ui/src/icons/HeroIcons.tsx b/packages/ui/src/icons/HeroIcons.tsx index 63898505dd..d2efb02a19 100644 --- a/packages/ui/src/icons/HeroIcons.tsx +++ b/packages/ui/src/icons/HeroIcons.tsx @@ -51,11 +51,14 @@ export const CodeBracketIcon: FC = (props) => ( ); export const CodeBracketSquareIcon: FC = (props) => ( - + ); diff --git a/packages/ui/src/stories/Icons/CodeBracketSquareIcon.stories.tsx b/packages/ui/src/stories/Icons/CodeBracketSquareIcon.stories.tsx new file mode 100644 index 0000000000..790698d6d7 --- /dev/null +++ b/packages/ui/src/stories/Icons/CodeBracketSquareIcon.stories.tsx @@ -0,0 +1,11 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { CodeBracketSquareIcon } from "../../index.js"; + +const meta = { component: CodeBracketSquareIcon } satisfies Meta< + typeof CodeBracketSquareIcon +>; +export default meta; +type Story = StoryObj; + +export const Primary: Story = {};