Skip to content
Open
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
3 changes: 3 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions packages/app/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { Suspense, JSX } from "solid-js"

const Home = lazy(() => import("@/pages/home"))
const Session = lazy(() => import("@/pages/session"))
const Automations = lazy(() => import("@/pages/automations"))
const Loading = () => <div class="size-full" />

function UiI18nBridge(props: ParentProps) {
Expand Down Expand Up @@ -140,6 +141,14 @@ export function AppInterface(props: { defaultUrl?: string; children?: JSX.Elemen
</Suspense>
)}
/>
<Route
path="/automations"
component={() => (
<Suspense fallback={<Loading />}>
<Automations />
</Suspense>
)}
/>
<Route path="/:dir" component={DirectoryLayout}>
<Route path="/" component={() => <Navigate href="session" />} />
<Route
Expand Down
36 changes: 36 additions & 0 deletions packages/app/src/components/automation-day-toggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { Component } from "solid-js"

interface DayToggleProps {
label: string
active: boolean
onChange: (value: boolean) => void
}

export const AutomationDayToggle: Component<DayToggleProps> = (props) => {
const active = () => props.active
const short = () => {
return props.label.slice(0, 2)
}

const handleClick = () => {
props.onChange(!active())
}

return (
<button
type="button"
aria-pressed={active()}
aria-label={props.label}
title={props.label}
onClick={handleClick}
class="size-8 rounded-full border text-12-medium flex items-center justify-center transition-colors"
classList={{
"bg-surface-base-active border-border-base text-text-strong shadow-xs-border": active(),
"bg-surface-base border-border-weak-base text-text-weak hover:bg-surface-base-hover hover:border-border-base hover:text-text-base":
!active(),
}}
>
{short()}
</button>
)
}
80 changes: 80 additions & 0 deletions packages/app/src/components/automation-time-row.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Select } from "@opencode-ai/ui/select"
import { createMemo, type Component } from "solid-js"

interface TimeRowProps {
index: number
value: string
hours: string[]
minutes: string[]
removeLabel: string
onChange: (value: string) => void
onRemove: () => void
}

export const AutomationTimeRow: Component<TimeRowProps> = (props) => {
const parts = createMemo(() => props.value.split(":"))
const hour = () => {
const value = parts()[0] ?? "00"
return value.padStart(2, "0")
}
const minute = () => {
const value = parts()[1] ?? "00"
return value.padStart(2, "0")
}

const setHour = (value?: string) => {
if (!value) return

props.onChange(`${value}:${minute()}`)
}

const setMinute = (value?: string) => {
if (!value) return

props.onChange(`${hour()}:${value}`)
}

return (
<div class="group flex items-center gap-2 rounded-md bg-surface-base px-2.5 py-2 transition-colors hover:bg-surface-base-hover">
<div class="size-6 rounded-full border border-border-weak-base bg-surface-raised-base text-11-medium text-text-weak flex items-center justify-center">
{props.index + 1}
</div>
<div class="flex-1 flex items-center gap-2">
<Select
options={props.hours}
current={hour()}
value={(value) => value}
label={(value) => value}
onSelect={setHour}
size="small"
variant="secondary"
valueClass="font-mono text-12-medium"
triggerStyle={{ "min-width": "72px" }}
portal={false}
/>
<span class="text-12-medium text-text-weak">:</span>
<Select
options={props.minutes}
current={minute()}
value={(value) => value}
label={(value) => value}
onSelect={setMinute}
size="small"
variant="secondary"
valueClass="font-mono text-12-medium"
triggerStyle={{ "min-width": "72px" }}
portal={false}
/>
</div>
<IconButton
type="button"
icon="trash"
variant="ghost"
onClick={props.onRemove}
aria-label={props.removeLabel}
class="shrink-0 opacity-70 group-hover:opacity-100"
/>
</div>
)
}
22 changes: 22 additions & 0 deletions packages/app/src/components/dialog-automation-delete.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { DialogConfirm } from "@/components/dialog-confirm"
import { useLanguage } from "@/context/language"
import type { Automation } from "@opencode-ai/sdk/v2/client"

export function DialogAutomationDelete(props: {
automation: Automation
onDelete: (automation: Automation) => Promise<void>
}) {
const language = useLanguage()

const handleDelete = () => props.onDelete(props.automation)

return (
<DialogConfirm
title={language.t("automations.delete.title")}
message={language.t("automations.delete.confirm", { name: props.automation.name })}
confirmLabel={language.t("automations.delete.button")}
cancelLabel={language.t("common.cancel")}
onConfirm={handleDelete}
/>
)
}
Loading
Loading