mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-25 10:18:31 +02:00
Tweak workspace settings dialog and Markdown editor
This commit is contained in:
@@ -1,10 +1,10 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import {createWorkspace} from "../lib/commands";
|
import { createWorkspace } from '../lib/commands';
|
||||||
import { Button } from './core/Button';
|
import { Button } from './core/Button';
|
||||||
import { PlainInput } from './core/PlainInput';
|
import { PlainInput } from './core/PlainInput';
|
||||||
import { VStack } from './core/Stacks';
|
import { VStack } from './core/Stacks';
|
||||||
import { MarkdownEditor } from './MarkdownEditor';
|
import type { SyncToFilesystemSettingProps } from './SyncToFilesystemSetting';
|
||||||
import { SelectFile } from './SelectFile';
|
import { SyncToFilesystemSetting } from './SyncToFilesystemSetting';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
hide: () => void;
|
hide: () => void;
|
||||||
@@ -12,8 +12,9 @@ interface Props {
|
|||||||
|
|
||||||
export function CreateWorkspaceDialog({ hide }: Props) {
|
export function CreateWorkspaceDialog({ hide }: Props) {
|
||||||
const [name, setName] = useState<string>('');
|
const [name, setName] = useState<string>('');
|
||||||
const [description, setDescription] = useState<string>('');
|
const [settingSyncDir, setSettingSyncDir] = useState<
|
||||||
const [settingSyncDir, setSettingSyncDir] = useState<string | null>(null);
|
Parameters<SyncToFilesystemSettingProps['onChange']>[0]
|
||||||
|
>({ value: null, enabled: false });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack
|
<VStack
|
||||||
@@ -23,31 +24,23 @@ export function CreateWorkspaceDialog({ hide }: Props) {
|
|||||||
className="pb-3 max-h-[50vh]"
|
className="pb-3 max-h-[50vh]"
|
||||||
onSubmit={async (e) => {
|
onSubmit={async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
await createWorkspace.mutateAsync({ name, description, settingSyncDir });
|
const { enabled, value } = settingSyncDir ?? {};
|
||||||
|
if (enabled && !value) return;
|
||||||
|
await createWorkspace.mutateAsync({ name, settingSyncDir: value });
|
||||||
hide();
|
hide();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PlainInput require label="Workspace Name" defaultValue={name} onChange={setName} />
|
<PlainInput require label="Workspace Name" defaultValue={name} onChange={setName} />
|
||||||
|
|
||||||
<MarkdownEditor
|
<SyncToFilesystemSetting onChange={setSettingSyncDir} value={settingSyncDir.value} />
|
||||||
name="workspace-description"
|
<Button
|
||||||
placeholder="Workspace description"
|
type="submit"
|
||||||
className="min-h-[10rem] max-h-[25rem] border border-border px-2"
|
color="primary"
|
||||||
defaultValue={description}
|
className="ml-auto mt-3"
|
||||||
stateKey={null}
|
disabled={settingSyncDir.enabled && !settingSyncDir.value}
|
||||||
onChange={setDescription}
|
>
|
||||||
heightMode="auto"
|
Create Workspace
|
||||||
/>
|
</Button>
|
||||||
|
|
||||||
<div>
|
|
||||||
<SelectFile
|
|
||||||
directory
|
|
||||||
noun="Sync Directory"
|
|
||||||
filePath={settingSyncDir}
|
|
||||||
onChange={({ filePath }) => setSettingSyncDir(filePath)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Button type="submit" color="primary" className="ml-auto">Create Workspace</Button>
|
|
||||||
</VStack>
|
</VStack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,17 +31,18 @@ export function GlobalHooks() {
|
|||||||
useNotificationToast();
|
useNotificationToast();
|
||||||
useActiveWorkspaceChangedToast();
|
useActiveWorkspaceChangedToast();
|
||||||
|
|
||||||
// Listen for toasts
|
|
||||||
useListenToTauriEvent<ShowToastRequest>('show_toast', (event) => {
|
|
||||||
showToast({ ...event.payload });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Trigger workspace sync operation when workspace files change
|
// Trigger workspace sync operation when workspace files change
|
||||||
const activeWorkspace = useActiveWorkspace();
|
const activeWorkspace = useActiveWorkspace();
|
||||||
const { debouncedSync } = useSyncWorkspace(activeWorkspace, { debounceMillis: 1000 });
|
const { debouncedSync } = useSyncWorkspace(activeWorkspace, { debounceMillis: 1000 });
|
||||||
useListenToTauriEvent('upserted_model', debouncedSync);
|
useListenToTauriEvent('upserted_model', debouncedSync);
|
||||||
useWatchWorkspace(activeWorkspace, debouncedSync);
|
useWatchWorkspace(activeWorkspace, debouncedSync);
|
||||||
|
|
||||||
|
// Listen for toasts
|
||||||
|
useListenToTauriEvent<ShowToastRequest>('show_toast', (event) => {
|
||||||
|
showToast({ ...event.payload });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for prompts
|
||||||
useListenToTauriEvent<{ replyId: string; args: PromptTextRequest }>(
|
useListenToTauriEvent<{ replyId: string; args: PromptTextRequest }>(
|
||||||
'show_prompt',
|
'show_prompt',
|
||||||
async (event) => {
|
async (event) => {
|
||||||
|
|||||||
@@ -1,51 +1,51 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useRef } from 'react';
|
import { atom, useAtom } from 'jotai';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
import Markdown from 'react-markdown';
|
import Markdown from 'react-markdown';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
import { useContainerSize } from '../hooks/useContainerQuery';
|
import { Button } from './core/Button';
|
||||||
import { useKeyValue } from '../hooks/useKeyValue';
|
|
||||||
import type { EditorProps } from './core/Editor/Editor';
|
import type { EditorProps } from './core/Editor/Editor';
|
||||||
import { Editor } from './core/Editor/Editor';
|
import { Editor } from './core/Editor/Editor';
|
||||||
import { IconButton } from './core/IconButton';
|
import { HStack, VStack } from './core/Stacks';
|
||||||
import { SplitLayout } from './core/SplitLayout';
|
|
||||||
import { VStack } from './core/Stacks';
|
|
||||||
import { Prose } from './Prose';
|
import { Prose } from './Prose';
|
||||||
|
|
||||||
|
type ViewMode = 'edit' | 'preview';
|
||||||
|
|
||||||
interface Props extends Pick<EditorProps, 'heightMode' | 'stateKey' | 'forceUpdateKey'> {
|
interface Props extends Pick<EditorProps, 'heightMode' | 'stateKey' | 'forceUpdateKey'> {
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
defaultValue: string;
|
defaultValue: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
name: string;
|
name: string;
|
||||||
|
defaultMode?: ViewMode;
|
||||||
|
doneButtonLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MarkdownEditor({ className, defaultValue, onChange, name, ...editorProps }: Props) {
|
const viewModeAtom = atom<Record<string, ViewMode>>({});
|
||||||
|
|
||||||
|
export function MarkdownEditor({
|
||||||
|
className,
|
||||||
|
defaultValue,
|
||||||
|
onChange,
|
||||||
|
name,
|
||||||
|
defaultMode = 'preview',
|
||||||
|
doneButtonLabel = 'Save',
|
||||||
|
...editorProps
|
||||||
|
}: Props) {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [rawViewMode, setViewMode] = useAtom(viewModeAtom);
|
||||||
const { width } = useContainerSize(containerRef);
|
const viewMode = rawViewMode[name] ?? defaultMode;
|
||||||
const wideEnoughForSplit = width > 600;
|
const [value, setValue] = useState<string>(defaultValue);
|
||||||
|
|
||||||
const { set: setViewMode, value: rawViewMode } = useKeyValue<'edit' | 'preview' | 'both'>({
|
|
||||||
namespace: 'global',
|
|
||||||
key: ['md_view', name],
|
|
||||||
fallback: 'edit',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (rawViewMode == null) return null;
|
|
||||||
|
|
||||||
let viewMode = rawViewMode;
|
|
||||||
if (rawViewMode === 'both' && !wideEnoughForSplit) {
|
|
||||||
viewMode = 'edit';
|
|
||||||
}
|
|
||||||
|
|
||||||
const editor = (
|
const editor = (
|
||||||
<Editor
|
<Editor
|
||||||
hideGutter
|
hideGutter
|
||||||
wrapLines
|
wrapLines
|
||||||
className="max-w-2xl max-h-full"
|
className="max-w-2xl max-h-full"
|
||||||
language="markdown"
|
language="markdown"
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
onChange={onChange}
|
onChange={setValue}
|
||||||
|
autoFocus
|
||||||
{...editorProps}
|
{...editorProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -70,28 +70,12 @@ export function MarkdownEditor({ className, defaultValue, onChange, name, ...edi
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{defaultValue}
|
{value}
|
||||||
</Markdown>
|
</Markdown>
|
||||||
</Prose>
|
</Prose>
|
||||||
);
|
);
|
||||||
|
|
||||||
const contents =
|
const contents = viewMode === 'preview' ? preview : editor;
|
||||||
viewMode === 'both' ? (
|
|
||||||
<SplitLayout
|
|
||||||
name="markdown-editor"
|
|
||||||
layout="horizontal"
|
|
||||||
firstSlot={({ style }) => <div style={style}>{editor}</div>}
|
|
||||||
secondSlot={({ style }) => (
|
|
||||||
<div style={style} className="border-l border-border-subtle pl-6">
|
|
||||||
{preview}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
) : viewMode === 'preview' ? (
|
|
||||||
preview
|
|
||||||
) : (
|
|
||||||
editor
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -106,32 +90,43 @@ export function MarkdownEditor({ className, defaultValue, onChange, name, ...edi
|
|||||||
space={1}
|
space={1}
|
||||||
className="bg-surface opacity-20 group-hover:opacity-100 transition-opacity transform-gpu"
|
className="bg-surface opacity-20 group-hover:opacity-100 transition-opacity transform-gpu"
|
||||||
>
|
>
|
||||||
<IconButton
|
{viewMode === 'preview' && (
|
||||||
size="xs"
|
<Button
|
||||||
icon="text"
|
|
||||||
title="Switch to edit mode"
|
|
||||||
className={classNames(viewMode === 'edit' && 'bg-surface-highlight !text-text')}
|
|
||||||
event={{ id: 'md_mode', mode: viewMode }}
|
|
||||||
onClick={() => setViewMode('edit')}
|
|
||||||
/>
|
|
||||||
{wideEnoughForSplit && (
|
|
||||||
<IconButton
|
|
||||||
size="xs"
|
size="xs"
|
||||||
icon="columns_2"
|
variant="border"
|
||||||
title="Switch to edit mode"
|
|
||||||
className={classNames(viewMode === 'both' && 'bg-surface-highlight !text-text')}
|
|
||||||
event={{ id: 'md_mode', mode: viewMode }}
|
event={{ id: 'md_mode', mode: viewMode }}
|
||||||
onClick={() => setViewMode('both')}
|
onClick={() => setViewMode((prev) => ({ ...prev, [name]: 'edit' }))}
|
||||||
/>
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{viewMode === 'edit' && (
|
||||||
|
<HStack space={2}>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
event={{ id: 'md_mode', mode: viewMode }}
|
||||||
|
color="secondary"
|
||||||
|
variant="border"
|
||||||
|
onClick={() => {
|
||||||
|
setViewMode((prev) => ({ ...prev, [name]: 'preview' }));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="border"
|
||||||
|
color="primary"
|
||||||
|
event={{ id: 'md_mode', mode: viewMode }}
|
||||||
|
onClick={() => {
|
||||||
|
onChange(value);
|
||||||
|
setViewMode((prev) => ({ ...prev, [name]: 'preview' }));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{doneButtonLabel}
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
)}
|
)}
|
||||||
<IconButton
|
|
||||||
size="xs"
|
|
||||||
icon="eye"
|
|
||||||
title="Switch to preview mode"
|
|
||||||
className={classNames(viewMode === 'preview' && 'bg-surface-highlight !text-text')}
|
|
||||||
event={{ id: 'md_mode', mode: viewMode }}
|
|
||||||
onClick={() => setViewMode('preview')}
|
|
||||||
/>
|
|
||||||
</VStack>
|
</VStack>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -46,9 +46,9 @@ export function SelectFile({
|
|||||||
const selectOrChange = (filePath ? 'Change ' : 'Select ') + itemLabel;
|
const selectOrChange = (filePath ? 'Change ' : 'Select ') + itemLabel;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HStack space={1.5} className="group relative justify-stretch overflow-hidden">
|
<HStack className="group relative justify-stretch overflow-hidden">
|
||||||
<Button
|
<Button
|
||||||
className={classNames(className, 'font-mono text-xs rtl', inline && 'w-full')}
|
className={classNames(className, 'font-mono text-xs rtl mr-1.5', inline && 'w-full')}
|
||||||
color="secondary"
|
color="secondary"
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
size={size}
|
size={size}
|
||||||
@@ -57,6 +57,7 @@ export function SelectFile({
|
|||||||
{rtlEscapeChar}
|
{rtlEscapeChar}
|
||||||
{inline ? filePath || selectOrChange : selectOrChange}
|
{inline ? filePath || selectOrChange : selectOrChange}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{!inline && (
|
{!inline && (
|
||||||
<>
|
<>
|
||||||
{filePath && (
|
{filePath && (
|
||||||
|
|||||||
45
src-web/components/SyncToFilesystemSetting.tsx
Normal file
45
src-web/components/SyncToFilesystemSetting.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Checkbox } from './core/Checkbox';
|
||||||
|
import { VStack } from './core/Stacks';
|
||||||
|
import { SelectFile } from './SelectFile';
|
||||||
|
|
||||||
|
export interface SyncToFilesystemSettingProps {
|
||||||
|
onChange: (args: { value: string | null; enabled: boolean }) => void;
|
||||||
|
value: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SyncToFilesystemSetting({ onChange, value }: SyncToFilesystemSettingProps) {
|
||||||
|
const [useSyncDir, setUseSyncDir] = useState<boolean>(!!value);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack space={1.5} className="w-full">
|
||||||
|
<Checkbox
|
||||||
|
checked={useSyncDir}
|
||||||
|
onChange={(enabled) => {
|
||||||
|
setUseSyncDir(enabled);
|
||||||
|
if (!enabled) {
|
||||||
|
// Set value to null when disabling
|
||||||
|
onChange({ value: null, enabled });
|
||||||
|
} else {
|
||||||
|
onChange({ value, enabled });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title="Sync to a filesystem directory"
|
||||||
|
/>
|
||||||
|
{useSyncDir && (
|
||||||
|
<>
|
||||||
|
<SelectFile
|
||||||
|
directory
|
||||||
|
size="xs"
|
||||||
|
noun="Directory"
|
||||||
|
filePath={value}
|
||||||
|
onChange={({ filePath }) => {
|
||||||
|
if (filePath == null) setUseSyncDir(false);
|
||||||
|
onChange({ value: filePath, enabled: useSyncDir });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import { Button } from './core/Button';
|
|||||||
import { Input } from './core/Input';
|
import { Input } from './core/Input';
|
||||||
import { VStack } from './core/Stacks';
|
import { VStack } from './core/Stacks';
|
||||||
import { MarkdownEditor } from './MarkdownEditor';
|
import { MarkdownEditor } from './MarkdownEditor';
|
||||||
import { SelectFile } from './SelectFile';
|
import { SyncToFilesystemSetting } from './SyncToFilesystemSetting';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
workspaceId: string | null;
|
workspaceId: string | null;
|
||||||
@@ -21,7 +21,7 @@ export function WorkspaceSettingsDialog({ workspaceId, hide }: Props) {
|
|||||||
if (workspace == null) return null;
|
if (workspace == null) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack space={3} alignItems="start" className="pb-3 max-h-[50vh]">
|
<VStack space={3} alignItems="start" className="pb-3 h-full">
|
||||||
<Input
|
<Input
|
||||||
label="Workspace Name"
|
label="Workspace Name"
|
||||||
defaultValue={workspace.name}
|
defaultValue={workspace.name}
|
||||||
@@ -40,11 +40,11 @@ export function WorkspaceSettingsDialog({ workspaceId, hide }: Props) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<VStack space={3} className="mt-3" alignItems="start">
|
<VStack space={3} className="mt-3" alignItems="start">
|
||||||
<SelectFile
|
<SyncToFilesystemSetting
|
||||||
directory
|
value={workspace.settingSyncDir}
|
||||||
noun="Sync Directory"
|
onChange={({ value: settingSyncDir }) => {
|
||||||
filePath={workspace.settingSyncDir}
|
updateWorkspace({ settingSyncDir });
|
||||||
onChange={({ filePath: settingSyncDir }) => updateWorkspace({ settingSyncDir })}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
|
|||||||
@@ -53,72 +53,74 @@ export function Dialog({
|
|||||||
return (
|
return (
|
||||||
<Overlay open={open} onClose={onClose} portalName="dialog">
|
<Overlay open={open} onClose={onClose} portalName="dialog">
|
||||||
<div
|
<div
|
||||||
|
role="dialog"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'x-theme-dialog absolute inset-0 flex flex-col items-center pointer-events-none',
|
'x-theme-dialog absolute inset-0 pointer-events-none',
|
||||||
|
'h-full flex flex-col items-center justify-center',
|
||||||
vAlign === 'top' && 'justify-start',
|
vAlign === 'top' && 'justify-start',
|
||||||
vAlign === 'center' && 'justify-center',
|
vAlign === 'center' && 'justify-center',
|
||||||
)}
|
)}
|
||||||
|
aria-labelledby={titleId}
|
||||||
|
aria-describedby={descriptionId}
|
||||||
>
|
>
|
||||||
<div role="dialog" aria-labelledby={titleId} aria-describedby={descriptionId}>
|
<motion.div
|
||||||
<motion.div
|
initial={{ top: 5, scale: 0.97 }}
|
||||||
initial={{ top: 5, scale: 0.97 }}
|
animate={{ top: 0, scale: 1 }}
|
||||||
animate={{ top: 0, scale: 1 }}
|
className={classNames(
|
||||||
|
className,
|
||||||
|
'grid grid-rows-[auto_auto_minmax(0,1fr)]',
|
||||||
|
'grid-cols-1', // must be here for inline code blocks to correctly break words
|
||||||
|
'relative bg-surface pointer-events-auto',
|
||||||
|
'rounded-lg',
|
||||||
|
'border border-border-subtle shadow-lg shadow-[rgba(0,0,0,0.1)]',
|
||||||
|
'max-w-[calc(100vw-5rem)] max-h-[calc(100vh-5rem)]',
|
||||||
|
size === 'sm' && 'w-[28rem]',
|
||||||
|
size === 'md' && 'w-[45rem]',
|
||||||
|
size === 'lg' && 'w-[65rem]',
|
||||||
|
size === 'full' && 'w-[100vw] h-[100vh]',
|
||||||
|
size === 'dynamic' && 'min-w-[20rem] max-w-[100vw] w-full',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{title ? (
|
||||||
|
<Heading className="px-6 mt-4 mb-2" size={1} id={titleId}>
|
||||||
|
{title}
|
||||||
|
</Heading>
|
||||||
|
) : (
|
||||||
|
<span />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{description ? (
|
||||||
|
<div className="px-6 text-text-subtle" id={descriptionId}>
|
||||||
|
{description}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
className,
|
'h-full w-full grid grid-cols-[minmax(0,1fr)] grid-rows-1',
|
||||||
'grid grid-rows-[auto_auto_minmax(0,1fr)]',
|
!noPadding && 'px-6 py-2',
|
||||||
'grid-cols-1', // must be here for inline code blocks to correctly break words
|
!noScroll && 'overflow-y-auto overflow-x-hidden',
|
||||||
'relative bg-surface pointer-events-auto',
|
|
||||||
'rounded-lg',
|
|
||||||
'border border-border-subtle shadow-lg shadow-[rgba(0,0,0,0.1)]',
|
|
||||||
'max-w-[calc(100vw-5rem)] max-h-[calc(100vh-4rem)]',
|
|
||||||
size === 'sm' && 'w-[28rem] max-h-[80vh]',
|
|
||||||
size === 'md' && 'w-[45rem] max-h-[80vh]',
|
|
||||||
size === 'lg' && 'w-[65rem] max-h-[80vh]',
|
|
||||||
size === 'full' && 'w-[100vw] h-[100vh]',
|
|
||||||
size === 'dynamic' && 'min-w-[20rem] max-w-[100vw] w-full mt-8',
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{title ? (
|
{children}
|
||||||
<Heading className="px-6 mt-4 mb-2" size={1} id={titleId}>
|
</div>
|
||||||
{title}
|
|
||||||
</Heading>
|
|
||||||
) : (
|
|
||||||
<span />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{description ? (
|
{/*Put close at the end so that it's the last thing to be tabbed to*/}
|
||||||
<div className="px-6 text-text-subtle" id={descriptionId}>
|
{!hideX && (
|
||||||
{description}
|
<div className="ml-auto absolute right-1 top-1">
|
||||||
</div>
|
<IconButton
|
||||||
) : (
|
className="opacity-70 hover:opacity-100"
|
||||||
<span />
|
onClick={onClose}
|
||||||
)}
|
title="Close dialog (Esc)"
|
||||||
|
aria-label="Close"
|
||||||
<div
|
size="sm"
|
||||||
className={classNames(
|
icon="x"
|
||||||
'h-full w-full grid grid-cols-[minmax(0,1fr)] grid-rows-1',
|
/>
|
||||||
!noPadding && 'px-6 py-2',
|
|
||||||
!noScroll && 'overflow-y-auto overflow-x-hidden',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
{/*Put close at the end so that it's the last thing to be tabbed to*/}
|
</motion.div>
|
||||||
{!hideX && (
|
|
||||||
<div className="ml-auto absolute right-1 top-1">
|
|
||||||
<IconButton
|
|
||||||
className="opacity-70 hover:opacity-100"
|
|
||||||
onClick={onClose}
|
|
||||||
title="Close dialog (Esc)"
|
|
||||||
aria-label="Close"
|
|
||||||
size="sm"
|
|
||||||
icon="x"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useStateWithDeps } from '../../hooks/useStateWithDeps';
|
|||||||
import type { EditorProps } from './Editor/Editor';
|
import type { EditorProps } from './Editor/Editor';
|
||||||
import { Editor } from './Editor/Editor';
|
import { Editor } from './Editor/Editor';
|
||||||
import { IconButton } from './IconButton';
|
import { IconButton } from './IconButton';
|
||||||
|
import { Label } from './Label';
|
||||||
import { HStack } from './Stacks';
|
import { HStack } from './Stacks';
|
||||||
|
|
||||||
export type InputProps = Pick<
|
export type InputProps = Pick<
|
||||||
@@ -136,16 +137,9 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
|
|||||||
labelPosition === 'top' && 'flex-row gap-0.5',
|
labelPosition === 'top' && 'flex-row gap-0.5',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<label
|
<Label htmlFor={id} className={classNames(labelClassName, hideLabel && 'sr-only')}>
|
||||||
htmlFor={id}
|
|
||||||
className={classNames(
|
|
||||||
labelClassName,
|
|
||||||
'text-text-subtle whitespace-nowrap',
|
|
||||||
hideLabel && 'sr-only',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</Label>
|
||||||
<HStack
|
<HStack
|
||||||
alignItems="stretch"
|
alignItems="stretch"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
|||||||
16
src-web/components/core/Label.tsx
Normal file
16
src-web/components/core/Label.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import type { HTMLAttributes } from 'react';
|
||||||
|
|
||||||
|
export function Label({
|
||||||
|
htmlFor,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: HTMLAttributes<HTMLLabelElement> & { htmlFor: string }) {
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
className={classNames(className, 'text-text-subtle whitespace-nowrap')}
|
||||||
|
htmlFor={htmlFor}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { useCallback, useMemo, useRef, useState } from 'react';
|
|||||||
import { useStateWithDeps } from '../../hooks/useStateWithDeps';
|
import { useStateWithDeps } from '../../hooks/useStateWithDeps';
|
||||||
import { IconButton } from './IconButton';
|
import { IconButton } from './IconButton';
|
||||||
import type { InputProps } from './Input';
|
import type { InputProps } from './Input';
|
||||||
|
import { Label } from './Label';
|
||||||
import { HStack } from './Stacks';
|
import { HStack } from './Stacks';
|
||||||
|
|
||||||
export type PlainInputProps = Omit<InputProps, 'wrapLines' | 'onKeyDown' | 'type' | 'stateKey'> &
|
export type PlainInputProps = Omit<InputProps, 'wrapLines' | 'onKeyDown' | 'type' | 'stateKey'> &
|
||||||
@@ -45,15 +46,18 @@ export function PlainInput({
|
|||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
const handleFocus = useCallback((e: FocusEvent<HTMLInputElement>) => {
|
const handleFocus = useCallback(
|
||||||
onFocusRaw?.(e);
|
(e: FocusEvent<HTMLInputElement>) => {
|
||||||
setFocused(true);
|
onFocusRaw?.(e);
|
||||||
if (autoSelect) {
|
setFocused(true);
|
||||||
inputRef.current?.select();
|
if (autoSelect) {
|
||||||
textareaRef.current?.select();
|
inputRef.current?.select();
|
||||||
}
|
textareaRef.current?.select();
|
||||||
onFocus?.();
|
}
|
||||||
}, [autoSelect, onFocus, onFocusRaw]);
|
onFocus?.();
|
||||||
|
},
|
||||||
|
[autoSelect, onFocus, onFocusRaw],
|
||||||
|
);
|
||||||
|
|
||||||
const handleBlur = useCallback(() => {
|
const handleBlur = useCallback(() => {
|
||||||
setFocused(false);
|
setFocused(false);
|
||||||
@@ -94,16 +98,9 @@ export function PlainInput({
|
|||||||
labelPosition === 'top' && 'flex-row gap-0.5',
|
labelPosition === 'top' && 'flex-row gap-0.5',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<label
|
<Label htmlFor={id} className={classNames(labelClassName, hideLabel && 'sr-only')}>
|
||||||
htmlFor={id}
|
|
||||||
className={classNames(
|
|
||||||
labelClassName,
|
|
||||||
'text-text-subtle whitespace-nowrap flex-shrink-0',
|
|
||||||
hideLabel && 'sr-only',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</Label>
|
||||||
<HStack
|
<HStack
|
||||||
alignItems="stretch"
|
alignItems="stretch"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export function useCreateWorkspace() {
|
|||||||
showDialog({
|
showDialog({
|
||||||
id: 'create-workspace',
|
id: 'create-workspace',
|
||||||
title: 'Create Workspace',
|
title: 'Create Workspace',
|
||||||
size: 'md',
|
size: 'sm',
|
||||||
render: ({ hide }) => <CreateWorkspaceDialog hide={hide} />,
|
render: ({ hide }) => <CreateWorkspaceDialog hide={hide} />,
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
Reference in New Issue
Block a user