From e5885fcae43afad7d194c812c5e5b1454ebf7ab7 Mon Sep 17 00:00:00 2001 From: David Anthony Date: Tue, 29 Sep 2026 09:04:48 -0700 Subject: [PATCH] docs(apollo-wind): add Code and Code w/ Text modal stories Add two Modal stories that open a Monaco editor in a default-sized modal and expand into the takeover variant. Collapsing from the takeover header returns to the default modal, and edits persist across the switch. - Code: header with Run action, styled after the Code Editors Layout Pattern - Code w/ Text: title, description, labeled JSON Variables field, helper text and Cancel/Run footer (Run Single Step pattern) Extract the Monaco demo helpers (theme registry, samples, LiveMonacoEditor, theme sync hook) into code-editors.shared.tsx so both story files share one TypeScript-defaults ref count. LiveMonacoEditor gains an optional language prop for JSON. Reorder the Modal sidebar: Basic, Complex, Link, Delete, Code, Code w/ Text, Takeover, then the remaining stories. Co-Authored-By: Claude Opus 5.5 --- .../src/components/ui/code-editors.shared.tsx | 342 ++++++++++++++ .../components/ui/code-editors.stories.tsx | 329 +------------ .../src/components/ui/dialog.stories.tsx | 438 +++++++++++++----- 3 files changed, 675 insertions(+), 434 deletions(-) create mode 100644 packages/apollo-wind/src/components/ui/code-editors.shared.tsx diff --git a/packages/apollo-wind/src/components/ui/code-editors.shared.tsx b/packages/apollo-wind/src/components/ui/code-editors.shared.tsx new file mode 100644 index 000000000..ae3d43e11 --- /dev/null +++ b/packages/apollo-wind/src/components/ui/code-editors.shared.tsx @@ -0,0 +1,342 @@ +// Shared Monaco demo helpers for Storybook stories (Code Editors patterns, Modal Code). +// Kept in one module because the TypeScript-defaults ref count below is global state: +// two copies would each restore defaults the other still depends on. +import MonacoEditor, { type EditorProps } from '@monaco-editor/react'; +import { useEffect, useId, useRef, useState } from 'react'; +import { + apolloCoreDarkCodeMirror, + apolloCoreDarkHCCodeMirror, + apolloCoreDarkHCMonaco, + apolloCoreDarkMonaco, + apolloCoreLightCodeMirror, + apolloCoreLightHCCodeMirror, + apolloCoreLightHCMonaco, + apolloCoreLightMonaco, + apolloFutureDarkCodeMirror, + apolloFutureDarkMonaco, + apolloFutureLightCodeMirror, + apolloFutureLightMonaco, +} from '../../editor-themes'; + +// ============================================================================ +// Theme registry — all Apollo editor themes in one place +// ============================================================================ + +export const editorThemeConfigs = [ + { + key: 'future-dark', + label: 'Future Dark', + monacoThemeName: 'apollo-future-dark', + monacoExport: 'apolloFutureDarkMonaco', + cmExport: 'apolloFutureDarkCodeMirror', + monacoThemeObj: apolloFutureDarkMonaco, + cmTokens: apolloFutureDarkCodeMirror, + isDark: true, + family: 'Future', + }, + { + key: 'future-light', + label: 'Future Light', + monacoThemeName: 'apollo-future-light', + monacoExport: 'apolloFutureLightMonaco', + cmExport: 'apolloFutureLightCodeMirror', + monacoThemeObj: apolloFutureLightMonaco, + cmTokens: apolloFutureLightCodeMirror, + isDark: false, + family: 'Future', + }, + { + key: 'dark', + label: 'Dark', + monacoThemeName: 'apollo-core-dark', + monacoExport: 'apolloCoreDarkMonaco', + cmExport: 'apolloCoreDarkCodeMirror', + monacoThemeObj: apolloCoreDarkMonaco, + cmTokens: apolloCoreDarkCodeMirror, + isDark: true, + family: 'Core', + }, + { + key: 'light', + label: 'Light', + monacoThemeName: 'apollo-core-light', + monacoExport: 'apolloCoreLightMonaco', + cmExport: 'apolloCoreLightCodeMirror', + monacoThemeObj: apolloCoreLightMonaco, + cmTokens: apolloCoreLightCodeMirror, + isDark: false, + family: 'Core', + }, + { + key: 'dark-hc', + label: 'Dark High Contrast', + monacoThemeName: 'apollo-core-dark-hc', + monacoExport: 'apolloCoreDarkHCMonaco', + cmExport: 'apolloCoreDarkHCCodeMirror', + monacoThemeObj: apolloCoreDarkHCMonaco, + cmTokens: apolloCoreDarkHCCodeMirror, + isDark: true, + family: 'Core HC', + }, + { + key: 'light-hc', + label: 'Light High Contrast', + monacoThemeName: 'apollo-core-light-hc', + monacoExport: 'apolloCoreLightHCMonaco', + cmExport: 'apolloCoreLightHCCodeMirror', + monacoThemeObj: apolloCoreLightHCMonaco, + cmTokens: apolloCoreLightHCCodeMirror, + isDark: false, + family: 'Core HC', + }, +] as const; + +export type ThemeConfig = (typeof editorThemeConfigs)[number]; + +// ============================================================================ +// Sample code snippets +// ============================================================================ + +export const monacoFullSample = + `import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; + +type SortDirection = 'asc' | 'desc'; + +interface Column { + key: keyof T; + header: string; + sortable?: boolean; + render?: (value: T[keyof T], row: T) => ReactNode; +} + +interface TableProps { + data: T[]; + columns: Column[]; + pageSize?: number; +} + +export function DataTable({ + data, + columns, + pageSize = 10, +}: TableProps) { + const [query, setQuery] = useState(''); + const [sortKey, setSortKey] = useState(null); + const [sortDir, setSortDir] = useState('asc'); + const [page, setPage] = useState(1); + + useEffect(() => { setPage(1); }, [query, sortKey, sortDir]); + + const filtered = useMemo(() => { + if (!query.trim()) return data; + const q = query.toLowerCase(); + return data.filter((row) => + columns.some((col) => String(row[col.key]).toLowerCase().includes(q)) + ); + }, [data, columns, query]); + + const sorted = useMemo(() => { + if (!sortKey) return filtered; + return [...filtered].sort((a, b) => { + const cmp = a[sortKey] < b[sortKey] ? -1 : a[sortKey] > b[sortKey] ? 1 : 0; + return sortDir === 'asc' ? cmp : -cmp; + }); + }, [filtered, sortKey, sortDir]); + + return ( +
+ setQuery(e.target.value)} + /> + + + + {columns.map((col) => ( + + ))} + + +
+ {col.header} +
+
+ ); +}`.trim(); + +export const monacoSample = `interface WorkflowNode { + id: string; + type: 'action' | 'condition' | 'trigger'; + executionStatus: 'NotExecuted' | 'InProgress' | 'Completed' | 'Failed'; +} + +function getNextNodes(node: WorkflowNode): string[] { + if (node.executionStatus === 'Completed') { + return node.type === 'condition' + ? ['true-branch', 'false-branch'] + : ['next']; + } + return []; +}`.trim(); + +// ============================================================================ +// Shared helpers +// ============================================================================ + +let monacoThemesRegistered = false; + +type MonacoInstance = Parameters>[0]; +type TypescriptDefaults = MonacoInstance['languages']['typescript']['typescriptDefaults']; + +export function registerAllMonacoThemes(monaco: MonacoInstance) { + if (monacoThemesRegistered) return; + for (const cfg of editorThemeConfigs) { + monaco.editor.defineTheme(cfg.monacoThemeName, cfg.monacoThemeObj); + } + monacoThemesRegistered = true; +} + +// TypeScript defaults are global to the Monaco instance, not per model, and Storybook +// reuses one preview runtime across stories. Apply the demo defaults while any demo +// editor is mounted and restore the previous ones when the last unmounts, so other +// stories' editors keep their own diagnostics. +let demoDefaultsUsers = 0; +let savedTypescriptDefaults: { + diagnostics: ReturnType; + compiler: ReturnType; +} | null = null; + +function acquireDemoTypescriptDefaults(monaco: MonacoInstance) { + demoDefaultsUsers += 1; + if (demoDefaultsUsers > 1) return; + const ts = monaco.languages.typescript.typescriptDefaults; + savedTypescriptDefaults = { + diagnostics: ts.getDiagnosticsOptions(), + compiler: ts.getCompilerOptions(), + }; + // These snippets are illustrative fragments, not complete programs, so undeclared + // identifiers are expected. Disable only semantic validation; syntax validation + // stays on so genuinely malformed code still surfaces. + ts.setDiagnosticsOptions({ ...ts.getDiagnosticsOptions(), noSemanticValidation: true }); + // The full sample is a React component, so parse models as TSX; otherwise syntax + // validation flags every JSX line. + ts.setCompilerOptions({ + ...ts.getCompilerOptions(), + jsx: monaco.languages.typescript.JsxEmit.Preserve, + }); +} + +function releaseDemoTypescriptDefaults(monaco: MonacoInstance) { + demoDefaultsUsers -= 1; + if (demoDefaultsUsers > 0 || !savedTypescriptDefaults) return; + const ts = monaco.languages.typescript.typescriptDefaults; + ts.setDiagnosticsOptions(savedTypescriptDefaults.diagnostics); + ts.setCompilerOptions(savedTypescriptDefaults.compiler); + savedTypescriptDefaults = null; +} + +// ============================================================================ +// Live Monaco editor +// ============================================================================ + +export function LiveMonacoEditor({ + themeConfig, + height = '220px', + value, + onChange, + onMount, + language = 'typescript', + options = {}, +}: { + themeConfig: ThemeConfig; + height?: string; + language?: 'typescript' | 'json'; + value?: string; + onChange?: (value: string | undefined) => void; + onMount?: EditorProps['onMount']; + options?: Record; +}) { + // Each editor needs its own model; a `.tsx` URI lets TypeScript parse JSX. + const extension = language === 'json' ? 'json' : 'tsx'; + const modelPath = `file:///editor-${useId().replace(/[^a-zA-Z0-9]/g, '')}.${extension}`; + const monacoRef = useRef(null); + + useEffect( + () => () => { + if (monacoRef.current) releaseDemoTypescriptDefaults(monacoRef.current); + monacoRef.current = null; + }, + [] + ); + + return ( + { + registerAllMonacoThemes(monaco); + if (!monacoRef.current) { + monacoRef.current = monaco; + acquireDemoTypescriptDefaults(monaco); + } + }} + onChange={onChange} + onMount={onMount} + options={{ + fontSize: 13, + lineHeight: 20, + minimap: { enabled: false }, + scrollBeyondLastLine: false, + wordWrap: 'on', + fontFamily: + 'ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace', + padding: { top: 16, bottom: 16 }, + lineNumbers: 'on', + glyphMargin: false, + folding: false, + renderLineHighlight: 'line', + hideCursorInOverviewRuler: true, + overviewRulerBorder: false, + scrollbar: { vertical: 'auto', horizontal: 'hidden', alwaysConsumeMouseWheel: false }, + automaticLayout: true, + // Suggestion/hover widgets render in a fixed overlay layer instead of being + // clipped by the `overflow-hidden` containers these demos wrap the editor in. + fixedOverflowWidgets: true, + ...options, + }} + /> + ); +} + +// ============================================================================ +// Storybook theme sync +// ============================================================================ + +const futureDarkConfig = editorThemeConfigs[0]; + +function getStorybookThemeConfig(): ThemeConfig { + const keys = editorThemeConfigs.map((c) => c.key); + const found = Array.from(document.body.classList).find((c) => + keys.includes(c as ThemeConfig['key']) + ); + return editorThemeConfigs.find((c) => c.key === found) ?? futureDarkConfig; +} + +export function useEditorThemeConfig(): ThemeConfig { + const [themeConfig, setThemeConfig] = useState(getStorybookThemeConfig); + + useEffect(() => { + const observer = new MutationObserver(() => setThemeConfig(getStorybookThemeConfig())); + observer.observe(document.body, { attributes: true, attributeFilter: ['class'] }); + return () => observer.disconnect(); + }, []); + + return themeConfig; +} diff --git a/packages/apollo-wind/src/components/ui/code-editors.stories.tsx b/packages/apollo-wind/src/components/ui/code-editors.stories.tsx index b9f39c1f2..48eacf349 100644 --- a/packages/apollo-wind/src/components/ui/code-editors.stories.tsx +++ b/packages/apollo-wind/src/components/ui/code-editors.stories.tsx @@ -3,29 +3,23 @@ import { HighlightStyle, syntaxHighlighting } from '@codemirror/language'; import { EditorState } from '@codemirror/state'; import { drawSelection, EditorView, highlightActiveLine, lineNumbers } from '@codemirror/view'; import { tags as t } from '@lezer/highlight'; -import MonacoEditor, { type EditorProps } from '@monaco-editor/react'; +import type { EditorProps } from '@monaco-editor/react'; import type { Meta } from '@storybook/react-vite'; import { Code2, Mail, Maximize2, Play } from 'lucide-react'; -import { type ComponentProps, type ReactNode, useEffect, useId, useRef, useState } from 'react'; +import { type ComponentProps, type ReactNode, useEffect, useRef, useState } from 'react'; import { NodePropertyPanel } from '../../../../apollo-react/src/canvas/components/NodePropertyPanel/NodePropertyPanel'; import { FullWorkbenchComposition } from '../../../../apollo-react/src/canvas/stories/templates/Flow.stories'; import { withCanvasProviders } from '../../../../apollo-react/src/canvas/storybook-utils'; import type { ApolloCodeMirrorTheme } from '../../editor-themes'; -import { - apolloCoreDarkCodeMirror, - apolloCoreDarkHCCodeMirror, - apolloCoreDarkHCMonaco, - apolloCoreDarkMonaco, - apolloCoreLightCodeMirror, - apolloCoreLightHCCodeMirror, - apolloCoreLightHCMonaco, - apolloCoreLightMonaco, - apolloFutureDarkCodeMirror, - apolloFutureDarkMonaco, - apolloFutureLightCodeMirror, - apolloFutureLightMonaco, -} from '../../editor-themes'; import { Button } from './button'; +import { + editorThemeConfigs, + LiveMonacoEditor, + monacoFullSample, + monacoSample, + type ThemeConfig, + useEditorThemeConfig, +} from './code-editors.shared'; import { Modal, ModalContent } from './dialog'; import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from './input-group'; import { Label } from './label'; @@ -51,168 +45,10 @@ const meta = { export default meta; -// ============================================================================ -// Theme registry — all Apollo editor themes in one place -// ============================================================================ - -const editorThemeConfigs = [ - { - key: 'future-dark', - label: 'Future Dark', - monacoThemeName: 'apollo-future-dark', - monacoExport: 'apolloFutureDarkMonaco', - cmExport: 'apolloFutureDarkCodeMirror', - monacoThemeObj: apolloFutureDarkMonaco, - cmTokens: apolloFutureDarkCodeMirror, - isDark: true, - family: 'Future', - }, - { - key: 'future-light', - label: 'Future Light', - monacoThemeName: 'apollo-future-light', - monacoExport: 'apolloFutureLightMonaco', - cmExport: 'apolloFutureLightCodeMirror', - monacoThemeObj: apolloFutureLightMonaco, - cmTokens: apolloFutureLightCodeMirror, - isDark: false, - family: 'Future', - }, - { - key: 'dark', - label: 'Dark', - monacoThemeName: 'apollo-core-dark', - monacoExport: 'apolloCoreDarkMonaco', - cmExport: 'apolloCoreDarkCodeMirror', - monacoThemeObj: apolloCoreDarkMonaco, - cmTokens: apolloCoreDarkCodeMirror, - isDark: true, - family: 'Core', - }, - { - key: 'light', - label: 'Light', - monacoThemeName: 'apollo-core-light', - monacoExport: 'apolloCoreLightMonaco', - cmExport: 'apolloCoreLightCodeMirror', - monacoThemeObj: apolloCoreLightMonaco, - cmTokens: apolloCoreLightCodeMirror, - isDark: false, - family: 'Core', - }, - { - key: 'dark-hc', - label: 'Dark High Contrast', - monacoThemeName: 'apollo-core-dark-hc', - monacoExport: 'apolloCoreDarkHCMonaco', - cmExport: 'apolloCoreDarkHCCodeMirror', - monacoThemeObj: apolloCoreDarkHCMonaco, - cmTokens: apolloCoreDarkHCCodeMirror, - isDark: true, - family: 'Core HC', - }, - { - key: 'light-hc', - label: 'Light High Contrast', - monacoThemeName: 'apollo-core-light-hc', - monacoExport: 'apolloCoreLightHCMonaco', - cmExport: 'apolloCoreLightHCCodeMirror', - monacoThemeObj: apolloCoreLightHCMonaco, - cmTokens: apolloCoreLightHCCodeMirror, - isDark: false, - family: 'Core HC', - }, -] as const; - -type ThemeConfig = (typeof editorThemeConfigs)[number]; - // ============================================================================ // Sample code snippets // ============================================================================ -const monacoFullSample = `import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; - -type SortDirection = 'asc' | 'desc'; - -interface Column { - key: keyof T; - header: string; - sortable?: boolean; - render?: (value: T[keyof T], row: T) => ReactNode; -} - -interface TableProps { - data: T[]; - columns: Column[]; - pageSize?: number; -} - -export function DataTable({ - data, - columns, - pageSize = 10, -}: TableProps) { - const [query, setQuery] = useState(''); - const [sortKey, setSortKey] = useState(null); - const [sortDir, setSortDir] = useState('asc'); - const [page, setPage] = useState(1); - - useEffect(() => { setPage(1); }, [query, sortKey, sortDir]); - - const filtered = useMemo(() => { - if (!query.trim()) return data; - const q = query.toLowerCase(); - return data.filter((row) => - columns.some((col) => String(row[col.key]).toLowerCase().includes(q)) - ); - }, [data, columns, query]); - - const sorted = useMemo(() => { - if (!sortKey) return filtered; - return [...filtered].sort((a, b) => { - const cmp = a[sortKey] < b[sortKey] ? -1 : a[sortKey] > b[sortKey] ? 1 : 0; - return sortDir === 'asc' ? cmp : -cmp; - }); - }, [filtered, sortKey, sortDir]); - - return ( -
- setQuery(e.target.value)} - /> - - - - {columns.map((col) => ( - - ))} - - -
- {col.header} -
-
- ); -}`.trim(); - -const monacoSample = `interface WorkflowNode { - id: string; - type: 'action' | 'condition' | 'trigger'; - executionStatus: 'NotExecuted' | 'InProgress' | 'Completed' | 'Failed'; -} - -function getNextNodes(node: WorkflowNode): string[] { - if (node.executionStatus === 'Completed') { - return node.type === 'condition' - ? ['true-branch', 'false-branch'] - : ['next']; - } - return []; -}`.trim(); - const codemirrorSample = `workflow.status === "active" && user.role !== "viewer" ? user.displayName + " — " + workflow.name : "Access restricted"`.trim(); @@ -221,58 +57,6 @@ const codemirrorSample = `workflow.status === "active" && user.role !== "viewer" // Shared helpers // ============================================================================ -let monacoThemesRegistered = false; - -type MonacoInstance = Parameters>[0]; -type TypescriptDefaults = MonacoInstance['languages']['typescript']['typescriptDefaults']; - -function registerAllMonacoThemes(monaco: MonacoInstance) { - if (monacoThemesRegistered) return; - for (const cfg of editorThemeConfigs) { - monaco.editor.defineTheme(cfg.monacoThemeName, cfg.monacoThemeObj); - } - monacoThemesRegistered = true; -} - -// TypeScript defaults are global to the Monaco instance, not per model, and Storybook -// reuses one preview runtime across stories. Apply the demo defaults while any demo -// editor is mounted and restore the previous ones when the last unmounts, so other -// stories' editors keep their own diagnostics. -let demoDefaultsUsers = 0; -let savedTypescriptDefaults: { - diagnostics: ReturnType; - compiler: ReturnType; -} | null = null; - -function acquireDemoTypescriptDefaults(monaco: MonacoInstance) { - demoDefaultsUsers += 1; - if (demoDefaultsUsers > 1) return; - const ts = monaco.languages.typescript.typescriptDefaults; - savedTypescriptDefaults = { - diagnostics: ts.getDiagnosticsOptions(), - compiler: ts.getCompilerOptions(), - }; - // These snippets are illustrative fragments, not complete programs, so undeclared - // identifiers are expected. Disable only semantic validation; syntax validation - // stays on so genuinely malformed code still surfaces. - ts.setDiagnosticsOptions({ ...ts.getDiagnosticsOptions(), noSemanticValidation: true }); - // The full sample is a React component, so parse models as TSX; otherwise syntax - // validation flags every JSX line. - ts.setCompilerOptions({ - ...ts.getCompilerOptions(), - jsx: monaco.languages.typescript.JsxEmit.Preserve, - }); -} - -function releaseDemoTypescriptDefaults(monaco: MonacoInstance) { - demoDefaultsUsers -= 1; - if (demoDefaultsUsers > 0 || !savedTypescriptDefaults) return; - const ts = monaco.languages.typescript.typescriptDefaults; - ts.setDiagnosticsOptions(savedTypescriptDefaults.diagnostics); - ts.setCompilerOptions(savedTypescriptDefaults.compiler); - savedTypescriptDefaults = null; -} - function buildCMExtensions(tokens: ApolloCodeMirrorTheme, isDark: boolean, compact = false) { const { syntax, ui } = tokens; const theme = EditorView.theme( @@ -344,77 +128,6 @@ function buildCMExtensions(tokens: ApolloCodeMirrorTheme, isDark: boolean, compa // Live editor components // ============================================================================ -function LiveMonacoEditor({ - themeConfig, - height = '220px', - value, - onChange, - onMount, - options = {}, -}: { - themeConfig: ThemeConfig; - height?: string; - value?: string; - onChange?: (value: string | undefined) => void; - onMount?: EditorProps['onMount']; - options?: Record; -}) { - // Each editor needs its own model; a `.tsx` URI lets TypeScript parse JSX. - const modelPath = `file:///editor-${useId().replace(/[^a-zA-Z0-9]/g, '')}.tsx`; - const monacoRef = useRef(null); - - useEffect( - () => () => { - if (monacoRef.current) releaseDemoTypescriptDefaults(monacoRef.current); - monacoRef.current = null; - }, - [] - ); - - return ( - { - registerAllMonacoThemes(monaco); - if (!monacoRef.current) { - monacoRef.current = monaco; - acquireDemoTypescriptDefaults(monaco); - } - }} - onChange={onChange} - onMount={onMount} - options={{ - fontSize: 13, - lineHeight: 20, - minimap: { enabled: false }, - scrollBeyondLastLine: false, - wordWrap: 'on', - fontFamily: - 'ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace', - padding: { top: 16, bottom: 16 }, - lineNumbers: 'on', - glyphMargin: false, - folding: false, - renderLineHighlight: 'line', - hideCursorInOverviewRuler: true, - overviewRulerBorder: false, - scrollbar: { vertical: 'auto', horizontal: 'hidden', alwaysConsumeMouseWheel: false }, - automaticLayout: true, - // Suggestion/hover widgets render in a fixed overlay layer instead of being - // clipped by the `overflow-hidden` containers these demos wrap the editor in. - fixedOverflowWidgets: true, - ...options, - }} - /> - ); -} - function LiveCodeMirrorEditor({ themeConfig, value, @@ -679,28 +392,6 @@ const { syntax, ui } = ${cmExport};`} // Input Editor demo components // ============================================================================ -const futureDarkConfig = editorThemeConfigs[0]; - -function useEditorThemeConfig(): ThemeConfig { - const getTheme = () => { - const keys = editorThemeConfigs.map((c) => c.key); - const found = Array.from(document.body.classList).find((c) => - keys.includes(c as ThemeConfig['key']) - ); - return editorThemeConfigs.find((c) => c.key === found) ?? futureDarkConfig; - }; - - const [themeConfig, setThemeConfig] = useState(getTheme); - - useEffect(() => { - const observer = new MutationObserver(() => setThemeConfig(getTheme())); - observer.observe(document.body, { attributes: true, attributeFilter: ['class'] }); - return () => observer.disconnect(); - }, []); - - return themeConfig; -} - function MonacoInputDemo({ themeConfig }: { themeConfig: ThemeConfig }) { const [mode, setMode] = useState<'text' | 'code'>('text'); // Shared by both modes so edits made in the code editor survive switching back to Text. diff --git a/packages/apollo-wind/src/components/ui/dialog.stories.tsx b/packages/apollo-wind/src/components/ui/dialog.stories.tsx index db0f00ee2..8101c9ff6 100644 --- a/packages/apollo-wind/src/components/ui/dialog.stories.tsx +++ b/packages/apollo-wind/src/components/ui/dialog.stories.tsx @@ -1,7 +1,10 @@ import type { Meta } from '@storybook/react-vite'; -import { ExternalLink, Plus, Trash2 } from 'lucide-react'; +import { ExternalLink, Maximize2, Play, Plus, Trash2, X } from 'lucide-react'; import * as React from 'react'; +import { cn } from '@/lib/index'; +import { Badge } from './badge'; import { Button } from './button'; +import { LiveMonacoEditor, monacoFullSample, useEditorThemeConfig } from './code-editors.shared'; import { Dialog, DialogClose, @@ -147,6 +150,325 @@ export const ComplexDialog = { render: () => , }; +// ============================================================================ +// Link Dialog +// ============================================================================ + +function LinkDialogStory() { + const viewMode = React.useContext(ViewModeContext); + return ( + + + + + + + Share Link + Anyone with the link can view this document. + +
+ + +
+
+ +

Link expires in 7 days

+
+ + + + + +
+
+ ); +} + +export const LinkDialog = { + name: 'Link', + render: () => , +}; + +// ============================================================================ +// Delete Confirmation Dialog +// ============================================================================ + +function DeleteConfirmationExample() { + const viewMode = React.useContext(ViewModeContext); + const [open, setOpen] = React.useState(viewMode === 'story'); + const [deleted, setDeleted] = React.useState(false); + + const handleDelete = () => { + setDeleted(true); + setOpen(false); + }; + + const handleReset = () => { + setDeleted(false); + setOpen(true); + }; + + return ( +
+ {deleted ? ( +
+ + + John Doe has been permanently deleted. + + +
+ ) : ( + + + + + + + Delete User + + Are you sure you want to delete John Doe? This action cannot be + undone and will permanently remove the user and all associated data. + + + + + + + + + + + )} +
+ ); +} + +export const DeleteConfirmation = { + name: 'Delete', + render: () => , +}; + +// ============================================================================ +// Code Modal +// ============================================================================ + +// Header actions for the collapsed code modals. The takeover variant renders its own. +function ExpandCloseActions({ className, onExpand }: { className?: string; onExpand: () => void }) { + return ( +
+ + + + +
+ ); +} + +function CodeModalStory() { + const viewMode = React.useContext(ViewModeContext); + const themeConfig = useEditorThemeConfig(); + const [open, setOpen] = React.useState(viewMode === 'story'); + const [expanded, setExpanded] = React.useState(false); + // Held here so edits survive the editor remounting when the modal expands or collapses. + const [code, setCode] = React.useState(monacoFullSample); + + const handleOpenChange = (nextOpen: boolean) => { + setOpen(nextOpen); + if (!nextOpen) setExpanded(false); + }; + + const runAction = ( + + ); + + const editor = ( +
+ setCode(value ?? '')} + /> +
+ ); + + return ( +
+
+ {!open && } +
+ + {expanded ? ( + // Collapsing from the takeover header returns to the default code modal. + + + Edit the expression. Collapse the modal to return to the default size. + +
{editor}
+
+ ) : ( + +
+ + Expression + + + Edit the expression. Expand the modal for a full screen editor. + + {runAction} + setExpanded(true)} /> +
+
{editor}
+
+ )} +
+
+ ); +} + +export const CodeModal = { + name: 'Code', + tags: ['!autodocs'], + render: () => , +}; + +// ============================================================================ +// Code w/ Text Modal +// ============================================================================ + +const runStepVariables = JSON.stringify( + { manualTrigger1: { output: { num1: '1', num2: '2' } } }, + null, + 2 +); + +function CodeWithTextModalStory() { + const viewMode = React.useContext(ViewModeContext); + const themeConfig = useEditorThemeConfig(); + const [open, setOpen] = React.useState(viewMode === 'story'); + const [expanded, setExpanded] = React.useState(false); + // Held here so edits survive the editor remounting when the modal expands or collapses. + const [variables, setVariables] = React.useState(runStepVariables); + + const handleOpenChange = (nextOpen: boolean) => { + setOpen(nextOpen); + if (!nextOpen) setExpanded(false); + }; + + const title = 'Run Single Step: Web Search'; + const description = + 'Provide input variable values for this node. Pre-populated from upstream data.'; + + const variablesField = ( +
+
+ Variables + any +
+
+ setVariables(value ?? '')} + options={{ ariaLabel: 'Variables' }} + /> +
+

+ JSON object with variable values keyed by variable path (e.g. nodeId.outputKey). +

+
+ ); + + const footer = ( + + + + + + + ); + + return ( +
+
+ {!open && } +
+ + {expanded ? ( + // Collapsing from the takeover header returns to the default modal. + +
+ {description} + {variablesField} + {footer} +
+
+ ) : ( + + +
+ {title} + setExpanded(true)} /> +
+ {description} +
+
{variablesField}
+ {footer} +
+ )} +
+
+ ); +} + +export const CodeWithTextModal = { + name: 'Code w/ Text', + tags: ['!autodocs'], + render: () => , +}; + // ============================================================================ // Takeover Modal // ============================================================================ @@ -214,52 +536,6 @@ export const TakeoverNoSidebar = { render: () => , }; -// ============================================================================ -// Link Dialog -// ============================================================================ - -function LinkDialogStory() { - const viewMode = React.useContext(ViewModeContext); - return ( - - - - - - - Share Link - Anyone with the link can view this document. - -
- - -
-
- -

Link expires in 7 days

-
- - - - - -
-
- ); -} - -export const LinkDialog = { - name: 'Link', - render: () => , -}; - // ============================================================================ // No Button Dialog // ============================================================================ @@ -478,71 +754,3 @@ export const StickyFooterDialog = { name: 'Sticky Footer', render: () => , }; - -// ============================================================================ -// Delete Confirmation Dialog -// ============================================================================ - -function DeleteConfirmationExample() { - const viewMode = React.useContext(ViewModeContext); - const [open, setOpen] = React.useState(viewMode === 'story'); - const [deleted, setDeleted] = React.useState(false); - - const handleDelete = () => { - setDeleted(true); - setOpen(false); - }; - - const handleReset = () => { - setDeleted(false); - setOpen(true); - }; - - return ( -
- {deleted ? ( -
- - - John Doe has been permanently deleted. - - -
- ) : ( - - - - - - - Delete User - - Are you sure you want to delete John Doe? This action cannot be - undone and will permanently remove the user and all associated data. - - - - - - - - - - - )} -
- ); -} - -export const DeleteConfirmation = { - name: 'Delete', - render: () => , -};