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
45 changes: 45 additions & 0 deletions apps/web/src/lab/entries/kit/buttons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { Html, HtmlBuilder } from "foldkit/html"
import { defineMessageUnion } from "foldkit/message"
import { modifyFields } from "foldkit/struct"
import { button } from "../../../ui/button"
import { cardButton } from "../../../ui/card-button"
import * as CopyButton from "../../../ui/copy-button"
import { badge, kbd, kbdGroup } from "../../../ui/display"
import { filterChip } from "../../../ui/filter-chip"
Expand All @@ -12,6 +13,7 @@ import { iconButton } from "../../../ui/icon-button"
import { inlineCode } from "../../../ui/inline-code"
import { refreshButton } from "../../../ui/refresh-button"
import { statusDot } from "../../../ui/status-dot"
import { textLink } from "../../../ui/text-link"
import * as Toggle from "../../../ui/toggle"
import * as ToggleGroup from "../../../ui/toggle-group"
import { defineLab } from "../../define"
Expand All @@ -27,6 +29,7 @@ const VARIANTS = [
"destructive",
"destructive-outline",
"link",
"dashed",
] as const
const TEXT_SIZES = ["xs", "sm", "default", "lg", "xl"] as const
const ICON_SIZES = ["icon-2xs", "icon-xs", "icon-sm", "icon", "icon-lg", "icon-xl"] as const
Expand Down Expand Up @@ -358,6 +361,46 @@ const kbdAndInlineCode = (h: HtmlBuilder<Message>): Html =>
),
])

const textLinks = (h: HtmlBuilder<Message>): Html =>
kitRow(h, undefined, [
textLink(h, { onClick: Message.ClickedRefresh() }, ["View all →"]),
textLink(h, { href: "#text-link" }, ["Open settings"]),
textLink(h, { tone: "muted", onClick: Message.ClickedRefresh() }, ["Clear filters"]),
textLink(
h,
{ tone: "muted", isMono: true, className: "text-3xs", onClick: Message.ClickedRefresh() },
["+4 more"],
),
textLink(h, { onClick: Message.ClickedRefresh(), isDisabled: true }, ["Disabled"]),
])

const cardButtons = (h: HtmlBuilder<Message>): Html =>
kitRow(
h,
undefined,
(
[
["Resting", false, false],
["Selected", true, false],
["Disabled", false, true],
] as const
).map(([title, isSelected, isDisabled]) =>
cardButton(
h,
{
onClick: Message.ClickedRefresh(),
isSelected,
isDisabled,
className: "flex w-56 flex-col gap-1 p-3",
},
[
h.span([h.Class("text-sm font-medium")], [title]),
h.span([h.Class("text-xs text-muted-foreground")], ["A whole card as one click target."]),
],
),
),
)

const view = (model: Model, h: HtmlBuilder<Message>): Html =>
kitPage(
h,
Expand All @@ -369,6 +412,8 @@ const view = (model: Model, h: HtmlBuilder<Message>): Html =>
kitSection(h, "Button variants x sizes", buttonVariantRows(h)),
kitSection(h, "Button states", [buttonStates(h)]),
kitSection(h, "Icon button", [iconButtons(h)]),
kitSection(h, "Text link", [textLinks(h)]),
kitSection(h, "Card button", [cardButtons(h)]),
kitSection(h, "Toggle", [toggles(model, h)]),
kitSection(h, "Toggle group", toggleGroups(model, h)),
kitSection(h, "Copy and refresh buttons", [copyAndRefresh(model, h)]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,14 +153,14 @@ const argumentsBlock = (
label: "Arguments",
attribute: "gen_ai.tool.call.arguments",
bytes: row.argumentsBytes,
action: h.button(
[
h.Class(
"font-mono text-2xs text-muted-foreground transition-colors hover:text-foreground",
),
h.Type("button"),
h.OnClick(Message.ClickedCopyArguments({ text: row.arguments })),
],
action: button(
h,
{
variant: "ghost",
size: "xs",
className: "font-mono text-2xs text-muted-foreground hover:text-foreground sm:text-2xs",
attributes: [h.OnClick(Message.ClickedCopyArguments({ text: row.arguments }))],
},
[copyLabel],
),
}),
Expand Down
30 changes: 14 additions & 16 deletions apps/web/src/page/alerts/destination/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { isClerkAuthEnabled } from "../../../lib/services/common/auth-mode"
import { Submodel } from "foldkit"
import type { Html, HtmlBuilder } from "foldkit/html"
import { button } from "../../../ui/button"
import { cardButton } from "../../../ui/card-button"
import * as DialogView from "../../../ui/dialog-view"
import { titleId } from "../../../ui/dialog"
import { eyebrow } from "../../../ui/eyebrow"
Expand Down Expand Up @@ -110,23 +111,20 @@ const providerTile = (
}>,
): Html => {
const provider = destinationProvider(options.type, options.chatConnector)
return h.button(
[
h.Key(options.key),
h.Type("button"),
h.OnClick(options.onSelect),
...(options.isBusy === true ? [h.Disabled(true)] : []),
h.AriaPressed(options.isSelected ? "true" : "false"),
h.Class(
cn(
"relative flex flex-col items-start gap-2 overflow-hidden rounded-md border p-3 text-left transition-colors",
options.isSelected
? "border-(--tile-accent) bg-surface-sunken"
: "border-border-subtle bg-card hover:border-border hover:bg-surface-sunken",
),
return cardButton(
h,
{
onClick: options.onSelect,
isSelected: options.isSelected,
isDisabled: options.isBusy === true,
// Selection takes the provider's accent; a busy tile stays solid so its spinner reads.
className: cn(
"relative flex flex-col items-start gap-2 overflow-hidden p-3 disabled:opacity-100",
options.isSelected &&
"border-(--tile-accent) bg-surface-sunken hover:border-(--tile-accent) hover:bg-surface-sunken",
),
h.Style({ "--tile-accent": provider.accent }),
],
attributes: [h.Key(options.key), h.Style({ "--tile-accent": provider.accent })],
},
[
h.span([
h.AriaHidden(true),
Expand Down
16 changes: 8 additions & 8 deletions apps/web/src/page/alerts/editor/view-details.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,14 +122,14 @@ const nameField = (h: H, model: DetailsView, form: RuleForm): Html => {

const notes = (h: H, model: DetailsView, form: RuleForm): Html => {
if (!model.isNotesOpen)
return h.button(
[
h.Type("button"),
h.OnClick(Message.ClickedAddNotes()),
h.Class(
"flex w-full items-center gap-1.5 rounded-md border border-dashed border-border-subtle px-3 py-1.5 text-left text-xs text-muted-foreground hover:border-border hover:text-foreground",
),
],
return button(
h,
{
variant: "dashed",
size: "xs",
className: "w-full justify-start",
attributes: [h.OnClick(Message.ClickedAddNotes())],
},
[icon(h, "PlusIcon", { size: 12 }), "Add notes"],
)
const state = fieldStateOf(form.notes, model.mountedForm?.notes)
Expand Down
62 changes: 25 additions & 37 deletions apps/web/src/page/alerts/editor/view-templates.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { cn } from "@maple/ui/lib/utils"
import { createLazy, type Html, type HtmlBuilder } from "foldkit/html"
import { AppRoute, hrefOf } from "../../../route"
import { button } from "../../../ui/button"
import { cardButton } from "../../../ui/card-button"
import * as DialogView from "../../../ui/dialog-view"
import { icon, type IconName } from "../../../ui/icon"
import { inlineCode } from "../../../ui/inline-code"
Expand All @@ -13,12 +13,7 @@ import type { Model } from "./model"

type H = HtmlBuilder<Message>

const TILE_CLASS = cn(
"group flex h-full flex-col gap-3 rounded-md border bg-card p-4 text-left transition-colors",
"hover:border-primary/40 hover:bg-primary/[0.03] focus-visible:outline-none",
// Focus reads like hover (the primary tint), not a neutral ring that looks like a selection.
"focus-visible:border-primary/60 focus-visible:bg-primary/[0.03] focus-visible:ring-2 focus-visible:ring-primary/20",
)
const TILE_LAYOUT = "flex h-full flex-col gap-3 p-4"

const tileHead = (h: H, iconName: IconName, title: string, subtitle: string): Html =>
h.div(
Expand Down Expand Up @@ -50,44 +45,37 @@ const chevron = (h: H): Html =>

const tiles = (h: H): ReadonlyArray<Html> => [
...ALERT_TEMPLATES.map((template) =>
h.button(
[h.Type("button"), h.Class(TILE_CLASS), h.OnClick(Message.ClickedTemplate({ id: template.id }))],
[
tileHead(h, template.icon, template.title, template.description),
h.div(
[h.Class("mt-auto flex min-w-0 items-center justify-between gap-2")],
[
inlineCode(
h,
{
className:
"min-w-0 truncate whitespace-nowrap text-2xs text-muted-foreground",
},
[template.summary],
),
chevron(h),
],
),
],
),
),
h.button(
[h.Type("button"), h.Class(TILE_CLASS), h.OnClick(Message.ClickedStartBlank())],
[
tileHead(h, "SquarePlusIcon", "Start blank", "Build a rule from scratch."),
cardButton(h, { onClick: Message.ClickedTemplate({ id: template.id }), className: TILE_LAYOUT }, [
tileHead(h, template.icon, template.title, template.description),
h.div(
[h.Class("mt-auto flex min-w-0 items-center justify-between gap-2")],
[
// One line, so the chevron sits on the same row as the siblings' chips.
h.p(
[h.Class("min-w-0 truncate font-prose text-muted-foreground text-xs")],
["Choose a signal"],
inlineCode(
h,
{
className: "min-w-0 truncate whitespace-nowrap text-2xs text-muted-foreground",
},
[template.summary],
),
chevron(h),
],
),
],
]),
),
cardButton(h, { onClick: Message.ClickedStartBlank(), className: TILE_LAYOUT }, [
tileHead(h, "SquarePlusIcon", "Start blank", "Build a rule from scratch."),
h.div(
[h.Class("mt-auto flex min-w-0 items-center justify-between gap-2")],
[
// One line, so the chevron sits on the same row as the siblings' chips.
h.p(
[h.Class("min-w-0 truncate font-prose text-muted-foreground text-xs")],
["Choose a signal"],
),
chevron(h),
],
),
]),
// Instructions, not a choice: a note under the tiles rather than another card.
h.p(
[h.Class("mt-1 flex items-center gap-2 font-prose text-muted-foreground text-xs md:col-span-3")],
Expand Down
10 changes: 3 additions & 7 deletions apps/web/src/page/alerts/hub/view-empty.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { cn } from "@maple/ui/lib/utils"
import type { Html, HtmlBuilder } from "foldkit/html"
import { button, linkButton } from "../../../ui/button"
import { cardButtonClass } from "../../../ui/card-button"
import { empty, emptyDescription, emptyHeader, emptyTitle } from "../../../ui/empty"
import { icon } from "../../../ui/icon"
import { inlineCode } from "../../../ui/inline-code"
Expand Down Expand Up @@ -126,13 +127,8 @@ const templateTile = <M>(h: HtmlBuilder<M>, template: AlertTemplate, serviceName
return h.a(
[
h.Href(createHref({ template: template.id, serviceName })),
h.Class(
cn(
"group flex flex-col gap-1.5 rounded-md border bg-card p-3 text-left transition-colors",
"hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
tone?.hoverBorder,
),
),
// A link, so it keeps the anchor and borrows the card button's surface.
h.Class(cardButtonClass(false, cn("flex flex-col gap-1.5 p-3", tone?.hoverBorder))),
],
[
h.div(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ describe("uptime monitor form", () => {
scene(
{ update: pageUpdate, view },
given(fresh()),
click(role("radio", { name: /TCP port/u })),
click(role("button", { name: /TCP port/u })),
type(role("textbox", { name: "Host" }), "db.acme.dev"),
type(role("textbox", { name: "Port" }), "99999"),
expectView(text("The port must be a whole number from 1 to 65535.")).toExist(),
Expand All @@ -93,7 +93,7 @@ describe("uptime monitor form", () => {
scene(
{ update: pageUpdate, view },
given(fresh()),
click(role("radio", { name: /Heartbeat/u })),
click(role("button", { name: /Heartbeat/u })),
expectView(text("The ping URL appears after you create the monitor.")).toExist(),
expectView(role("textbox", { name: "Grace period" })).toExist(),
expectView(role("textbox", { name: "URL" })).not.toExist(),
Expand Down Expand Up @@ -262,7 +262,7 @@ describe("uptime monitor form", () => {
scene(
{ update: pageUpdate, view },
given(fresh()),
click(role("radio", { name: /TCP port/u })),
click(role("button", { name: /TCP port/u })),
type(role("textbox", { name: "Host" }), "db.acme.dev"),
type(role("textbox", { name: "Port" }), "5432"),
expectView(role("textbox", { name: "Name" })).toHaveValue("db.acme.dev:5432"),
Expand Down
56 changes: 17 additions & 39 deletions apps/web/src/page/alerts/uptime-form/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { errorState } from "../../../ui/error-state"
import { icon, type IconName } from "../../../ui/icon"
import { alert, alertDescription, alertTitle } from "../../../ui/alert"
import { panel, panelHeader } from "../../../ui/panel"
import { segmentedSelect } from "../../../ui/segmented-select"
import { skeleton } from "../../../ui/skeleton"
import type { PageViewInputs } from "../../contract"
import { dashboardPage } from "../../kit/dashboard-page"
Expand Down Expand Up @@ -51,7 +52,7 @@ const KINDS: ReadonlyArray<{ kind: MonitorKind; label: string; icon: IconName; d
},
]

/** The kind as one row of radios; the picked kind's sentence sits under it. */
/** The kind as a segmented control; the picked kind's sentence sits under it. */
const kindPicker = (h: H, model: Model): Html =>
h.section(
[h.AriaLabel("Type"), h.Class("grid gap-x-8 gap-y-2 px-4 py-4 sm:grid-cols-[136px_minmax(0,1fr)]")],
Expand All @@ -60,44 +61,21 @@ const kindPicker = (h: H, model: Model): Html =>
h.div(
[h.Class("flex min-w-0 flex-col gap-2")],
[
h.div(
[
h.Role("radiogroup"),
h.AriaLabel("Monitor type"),
h.Class("flex w-fit flex-wrap gap-1 rounded-md border bg-surface-sunken p-0.5"),
],
KINDS.map((option) => {
const isSelected = model.form.kind === option.kind
const isDisabled = model.editingId !== null && !isSelected
return h.button(
[
h.Key(option.kind),
h.Type("button"),
h.Role("radio"),
h.AriaChecked(isSelected),
...(isDisabled ? [h.Disabled(true)] : []),
h.OnClick(Message.PickedKind({ kind: option.kind })),
h.Class(
cn(
"flex h-7 items-center gap-1.5 rounded-[5px] px-2.5 text-sm transition-colors",
isSelected
? "bg-background text-foreground shadow-xs ring-1 ring-border"
: "text-muted-foreground hover:text-foreground",
isDisabled &&
"cursor-not-allowed opacity-50 hover:text-muted-foreground",
),
),
],
[
icon(h, option.icon, {
size: 14,
className: isSelected ? "text-primary" : undefined,
}),
option.label,
],
)
}),
),
segmentedSelect(h, {
Comment thread
maple-review-bot[bot] marked this conversation as resolved.
Resolved
ariaLabel: "Monitor type",
size: "sm",
// Four labelled kinds outgrow a phone column, so the track wraps like the old picker.
className: "w-fit flex-wrap",
value: model.form.kind,
onChange: (kind) => Message.PickedKind({ kind }),
// Editing keeps the kind: the others stay visible but can't be picked.
options: KINDS.map((option) => ({
value: option.kind,
label: option.label,
icon: icon(h, option.icon, { size: 14 }),
isDisabled: model.editingId !== null && option.kind !== model.form.kind,
})),
}),
h.p(
[h.Class("text-xs text-muted-foreground")],
[KINDS.find((option) => option.kind === model.form.kind)?.description ?? ""],
Expand Down
Loading
Loading