Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 42 additions & 63 deletions apps/desktop-ui/src/components/cron-builder/cron-builder-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { format } from 'date-fns';
import { useMemo, useState } from 'react';
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Expand All @@ -26,9 +25,7 @@ import {
validateCron,
} from '@/lib/cron-utils';
import { IconRepeat } from '@tabler/icons-react';
import { CATEGORY_ACCENT } from '@/components/dashboard/types';
import { RevealItem } from '@/components/dashboard/dashboard-reveal';
import { ToolPageHeader } from '@/components/tools/tool-page-header';
import { ToolShell } from '@/components/tools/tool-shell';
import { CopyTextButton } from '@/components/tools/copy-text-button';
import { ToolErrorBanner } from '@/components/tools/tool-error-banner';
import { useLocale, useTranslations } from 'next-intl';
Expand All @@ -44,12 +41,12 @@ function QuickPick({
}) {
return (
<Select onValueChange={onPick}>
<SelectTrigger className="h-8 w-[min(100%,11rem)] text-xs shrink-0">
<SelectTrigger className="h-8 w-[min(100%,11rem)] shrink-0 text-xs">
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent className="max-h-60">
{options.map((o) => (
<SelectItem key={`${o.value}-${o.label}`} value={o.value} className="text-xs font-mono">
<SelectItem key={`${o.value}-${o.label}`} value={o.value} className="font-mono text-xs">
{o.label}
</SelectItem>
))}
Expand Down Expand Up @@ -168,20 +165,15 @@ export function CronBuilderLayout() {
}, [t, monthShort, weekdayShort]);

return (
<div className="relative flex flex-col h-full gap-4 min-h-0 overflow-auto dashboard-grid-bg">
<div className="dash-ambient -z-10" aria-hidden />
<RevealItem index={0}>
<ToolPageHeader
icon={IconRepeat}
title={t('title')}
description={t.rich('subtitle', {
mono: (chunks) => <span className="font-mono text-foreground">{chunks}</span>,
code: (chunks) => <code className="text-foreground">{chunks}</code>,
})}
accent={CATEGORY_ACCENT.Generators}
/>
</RevealItem>

<ToolShell
icon={IconRepeat}
title={t('title')}
description={t.rich('subtitle', {
mono: (chunks) => <span className="font-mono text-foreground">{chunks}</span>,
code: (chunks) => <code className="text-foreground">{chunks}</code>,
})}
contentClassName="gap-4 overflow-auto"
>
<Tabs
defaultValue="builder"
className="flex flex-col gap-4"
Expand All @@ -201,60 +193,51 @@ export function CronBuilderLayout() {
</TabsList>

<TabsContent value="builder" className="mt-0 space-y-4">
<Card className="p-4 space-y-3">
<Label className="text-xs text-muted-foreground uppercase tracking-wider">{t('presetsLabel')}</Label>
<div className="space-y-3 rounded-lg border border-border bg-card p-4">
<Label className="text-xs uppercase tracking-wider text-muted-foreground">{t('presetsLabel')}</Label>
<div className="flex flex-wrap gap-2">
{CRON_PRESETS.map((p) => (
<Button
key={p.key}
type="button"
variant="outline"
size="sm"
className="text-xs h-8"
className="h-8 text-xs"
onClick={() => commitExpression(p.cron)}
>
{t(`presets.${p.key}` as never)}
</Button>
))}
</div>
</Card>
</div>

<Card className="p-4 space-y-4">
<Label className="text-xs text-muted-foreground uppercase tracking-wider">{t('fieldsLabel')}</Label>
<div className="space-y-4 rounded-lg border border-border bg-card p-4">
<Label className="text-xs uppercase tracking-wider text-muted-foreground">{t('fieldsLabel')}</Label>
<div className="space-y-3">
{CRON_FIELD_LABELS.map((label, i) => {
const idx = i as 0 | 1 | 2 | 3 | 4;
const uiLabel = t(`fields.${label}` as never);
return (
<div
key={label}
className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3"
>
<span className="text-xs font-medium text-muted-foreground w-36 shrink-0">
{uiLabel}
</span>
<div key={label} className="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
<span className="w-36 shrink-0 text-xs font-medium text-muted-foreground">{uiLabel}</span>
<Input
value={five[idx]}
onChange={(e) => updateField(idx, e.target.value)}
className="font-mono text-sm h-9 flex-1 min-w-0"
className="h-9 min-w-0 flex-1 font-mono text-sm"
spellCheck={false}
aria-label={uiLabel}
/>
<QuickPick
options={PICKS[idx]!}
onPick={(v) => updateField(idx, v)}
placeholder={t('quickPick')}
/>
<QuickPick options={PICKS[idx]!} onPick={(v) => updateField(idx, v)} placeholder={t('quickPick')} />
</div>
);
})}
</div>
</Card>
</div>
</TabsContent>

<TabsContent value="expression" className="mt-0 space-y-3">
<Card className="p-4 space-y-2">
<Label htmlFor="cron-raw" className="text-xs text-muted-foreground uppercase tracking-wider">
<div className="space-y-2 rounded-lg border border-border bg-card p-4">
<Label htmlFor="cron-raw" className="text-xs uppercase tracking-wider text-muted-foreground">
{t('rawExpressionLabel')}
</Label>
<textarea
Expand All @@ -263,18 +246,16 @@ export function CronBuilderLayout() {
onChange={(e) => setRawDraft(e.target.value)}
onBlur={() => commitExpression(rawDraft)}
spellCheck={false}
className="w-full min-h-[100px] rounded-md border border-input bg-transparent px-3 py-2 text-sm font-mono shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
className="min-h-[100px] w-full rounded-md border border-input bg-transparent px-3 py-2 font-mono text-sm shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
/>
<p className="text-[11px] text-muted-foreground">
{t('expressionHint', { count: tokenCount })}
</p>
</Card>
<p className="text-[11px] text-muted-foreground">{t('expressionHint', { count: tokenCount })}</p>
</div>
</TabsContent>
</Tabs>

<Card className="p-4 space-y-2 shrink-0">
<div className="shrink-0 space-y-2 rounded-lg border border-border bg-card p-4">
<div className="flex flex-wrap items-center justify-between gap-2">
<Label className="text-xs text-muted-foreground uppercase tracking-wider">{t('currentExpressionLabel')}</Label>
<Label className="text-xs uppercase tracking-wider text-muted-foreground">{t('currentExpressionLabel')}</Label>
<CopyTextButton
onCopy={copyExpr}
copied={copied}
Expand All @@ -283,10 +264,10 @@ export function CronBuilderLayout() {
className="h-8 gap-1 text-xs"
/>
</div>
<p className="font-mono text-sm break-all rounded-md bg-muted/50 px-3 py-2">
<p className="break-all rounded-md bg-muted/50 px-3 py-2 font-mono text-sm">
{expression.trim() || t('emptyExpression')}
</p>
</Card>
</div>

{validation.ok === false && (
<ToolErrorBanner
Expand All @@ -296,35 +277,33 @@ export function CronBuilderLayout() {
{validation.errorKey === 'invalid' ? t('errors.invalid') : t(`errors.${validation.errorKey}` as never)}
</p>
{validation.errorKey === 'invalid' && validation.detail ? (
<p className="text-xs text-destructive/80 font-mono">{validation.detail}</p>
<p className="font-mono text-xs text-destructive/80">{validation.detail}</p>
) : null}
</div>
}
/>
)}

{validation.ok && human && (
<Card className="p-4 space-y-2 border-primary/20 bg-primary/5">
<Label className="text-xs text-muted-foreground uppercase tracking-wider">{t('plainLanguageLabel')}</Label>
<div className="space-y-2 rounded-lg border border-primary/20 bg-primary/5 p-4">
<Label className="text-xs uppercase tracking-wider text-muted-foreground">{t('plainLanguageLabel')}</Label>
<p className="text-sm leading-relaxed">{human}</p>
</Card>
</div>
)}

{validation.ok && nextRuns && nextRuns.length > 0 && (
<Card className="p-4 space-y-2">
<Label className="text-xs text-muted-foreground uppercase tracking-wider">
{t('nextRunsLabel')}
</Label>
<ul className="space-y-1.5 text-sm font-mono text-muted-foreground">
<div className="space-y-2 rounded-lg border border-border bg-card p-4">
<Label className="text-xs uppercase tracking-wider text-muted-foreground">{t('nextRunsLabel')}</Label>
<ul className="space-y-1.5 font-mono text-sm text-muted-foreground">
{nextRuns.map((d, i) => (
<li key={i} className="flex gap-2">
<span className="text-[10px] w-5 shrink-0 text-muted-foreground/70">{i + 1}.</span>
<span className="w-5 shrink-0 text-[10px] text-muted-foreground/70">{i + 1}.</span>
<span className="text-foreground">{format(d, 'PPpp')}</span>
</li>
))}
</ul>
</Card>
</div>
)}
</div>
</ToolShell>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
import * as React from 'react'
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
import { useTranslations } from 'next-intl'
import { Card } from '@/components/ui/card'
import { Label } from '@/components/ui/label'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import { Input } from '@/components/ui/input'
Expand All @@ -18,10 +16,9 @@ import {
} from '@/lib/docker-compose-generator'
import { Download, Search, X } from 'lucide-react'
import { IconBrandDocker } from '@tabler/icons-react'
import { CATEGORY_ACCENT } from '@/components/dashboard/types'
import { RevealItem } from '@/components/dashboard/dashboard-reveal'
import { ToolPageHeader } from '@/components/tools/tool-page-header'
import { CopyTextButton } from '@/components/tools/copy-text-button'
import { ToolShell } from '@/components/tools/tool-shell'
import { ToolPanels, IOPanel, ToolTextArea } from '@/components/tools/io-panel'

const PRESETS: { id: string; services: string[] }[] = [
{ id: 'stackWeb', services: ['postgres', 'redis', 'nginx'] },
Expand Down Expand Up @@ -108,20 +105,14 @@ export function DockerComposeGeneratorLayout() {
}

return (
<div className="relative flex flex-col h-full gap-4 min-h-0 overflow-hidden dashboard-grid-bg">
<div className="dash-ambient -z-10" aria-hidden />
<RevealItem index={0}>
<ToolPageHeader
icon={IconBrandDocker}
title={t('title')}
description={t('subtitle')}
accent={CATEGORY_ACCENT.Generators}
/>
</RevealItem>

<div className="grid grid-cols-1 xl:grid-cols-2 gap-4 flex-1 min-h-0 overflow-auto pb-4">
<ToolShell
icon={IconBrandDocker}
title={t('title')}
description={t('subtitle')}
>
<ToolPanels className="md:grid-cols-1 lg:grid-cols-2 overflow-auto pb-4">
<div className="flex flex-col gap-4 min-h-0">
<Card className="p-4 flex flex-col gap-3 shrink-0">
<div className="rounded-lg border border-border bg-card p-4 flex flex-col gap-3 shrink-0">
<div className="flex flex-wrap gap-2">
{PRESETS.map((p) => (
<Button
Expand All @@ -140,9 +131,9 @@ export function DockerComposeGeneratorLayout() {
</Button>
</div>
<p className="text-xs text-muted-foreground">{t('hintPorts')}</p>
</Card>
</div>

<Card className="p-4 flex flex-col flex-1 min-h-[280px] gap-3 overflow-hidden">
<div className="rounded-lg border border-border bg-card p-4 flex flex-col flex-1 min-h-[280px] gap-3 overflow-hidden">
<div className="relative">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
Expand Down Expand Up @@ -221,13 +212,13 @@ export function DockerComposeGeneratorLayout() {
))}
</div>
) : null}
</Card>
</div>
</div>

<Card className="flex flex-col flex-1 p-4 overflow-hidden min-h-[320px]">
<div className="flex items-center justify-between mb-3 gap-2">
<Label className="text-xs text-muted-foreground uppercase tracking-wider">{t('outputTitle')}</Label>
<div className="flex gap-2 shrink-0">
<IOPanel
label={t('outputTitle')}
actions={
<>
<Button
type="button"
variant="ghost"
Expand All @@ -247,17 +238,17 @@ export function DockerComposeGeneratorLayout() {
copiedLabel={t('copied')}
className="h-7 px-2 text-xs"
/>
</div>
</div>
<textarea
</>
}
>
<ToolTextArea
readOnly
value={yaml}
placeholder={t('emptyState')}
spellCheck={false}
className="flex-1 min-h-0 w-full rounded-md border bg-muted/30 p-3 font-mono text-xs leading-relaxed resize-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
className="text-xs leading-relaxed"
/>
</Card>
</div>
</div>
</IOPanel>
</ToolPanels>
</ToolShell>
)
}
Loading
Loading