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
5 changes: 2 additions & 3 deletions backend/druks/durable/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,8 @@ def from_run(
# takes either and is always a string.
SubjectId = Annotated[str, BeforeValidator(str)]

# The one line a board row and a detail page show a subject as. Blank is rejected
# rather than rendered: a title nobody can read is the thing this field exists to
# prevent.
# The one line a board row and a detail page show a subject as; blank is
# rejected rather than rendered.
SubjectLabel = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]


Expand Down
10 changes: 3 additions & 7 deletions frontend/src/components/Control.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@ import type {
TextareaHTMLAttributes,
} from 'react'

// The control frames the shell draws, as components. Omitting ``className`` is
// the point: the class name stops being a contract typed by hand, so it can be
// renamed without breaking a caller. Layout stays the page's — position a
// control from the parent's own rules, not by passing it a class.
// Dropping ``className`` is the point: the class name stops being a contract
// callers type by hand. Position a control from the parent's own rules.
type Bare<P> = Omit<P, 'className'>

export function TextInput(props: Bare<InputHTMLAttributes<HTMLInputElement>>) {
Expand All @@ -29,7 +27,6 @@ interface ButtonProps extends Bare<ButtonHTMLAttributes<HTMLButtonElement>> {
}

export function Button({ variant = 'ghost', ...props }: ButtonProps) {
// ``type`` ahead of the spread so a form's submit button can override it.
return <button type="button" {...props} className={`set-btn ${variant}`} />
}

Expand All @@ -40,8 +37,7 @@ interface FieldProps {
children?: ReactNode
}

// A labelled control with its help and error lines. Every slot is optional, so
// a bare <Field error={…} /> is the error line on its own.
// Every slot is optional: a bare <Field error={…} /> is the error line alone.
export function Field({ label, help, error, children }: FieldProps) {
return (
<div className="set-field">
Expand Down
16 changes: 6 additions & 10 deletions frontend/src/components/SettingField.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
import { CronField } from './CronField'
import { Field, Select, Textarea, TextInput } from './Control'

// One declared field, rendered from its kind. Both places that draw a declared
// field go through here: the settings panes and a service's connect form, which
// speak the same field vocabulary on the wire.
// One declared field, rendered from its kind — the settings panes and a
// service's connect form both draw through here.
interface SettingFieldProps {
label: string
help?: string
// The wire's field kind: str | int | bool | enum | secret | cron. ``bool`` is
// not drawn here — a toggle is a row, not a labelled control, so the panes
// pull those out and render them as switches.
// str | int | bool | enum | secret | cron. ``bool`` is not drawn here: a
// toggle is a row, not a labelled control, so the panes pull those out.
type: string
choices?: string[] | null
multiline?: boolean
// For a secret: whether one is already stored. The value itself never leaves
// the backend, so the box shows set-ness and takes a replacement.
// Whether a secret is already stored; the value itself never leaves the server.
secretSet?: boolean | null
value: string
onChange: (next: string) => void
Expand Down Expand Up @@ -43,8 +40,7 @@ function FieldControl({
disabled,
}: ControlProps) {
if (type === 'enum') {
// An enum without its choice set is a broken declaration, not a text field —
// say so rather than drawing a box that silently accepts anything.
// A broken declaration, not a text field: say so rather than accept anything.
if (!choices?.length) {
return <span className="set-field-error">{label} declares no choices</span>
}
Expand Down
4 changes: 1 addition & 3 deletions frontend/src/components/StatusGlyph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ interface Props {
size?: number
}

// A run that is still going anywhere pulses: it is moving, or it is waiting on
// you. The rule lives here rather than on a ``pulse`` prop, so every board
// reads the same state the same way.
// Here rather than on a prop, so every board reads the same state the same way.
const PULSING: RunState[] = ['scheduled', 'running', 'parked']

export function StatusGlyph({ state, size = 10 }: Props) {
Expand Down
8 changes: 2 additions & 6 deletions frontend/src/runtime/druks-ui.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,8 @@ import { describe, expect, it } from 'vitest'

import * as ui from './druks-ui'

// The lent frontend surface, pinned by exact equality — the twin of
// backend/tests/test_author_surface.py. Every name here is public to every
// installed app, forever: an app built against it keeps importing it, and
// removing one rejects that app's entry.js at link time.
//
// Red means stop and ask. Never edit the list to match the code.
// The twin of backend/tests/test_author_surface.py. Red means stop and ask —
// never edit the list to match the code.
const LENT = [
'Button',
'CancelRun',
Expand Down
15 changes: 3 additions & 12 deletions frontend/src/runtime/druks-ui.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,13 @@
// Import-map target for the bare specifier '@druks/ui' — the shell components an
// installed dist app borrows, so an app draws the shell's chrome instead of
// retyping it (see vite.config.ts). Bundled extensions import the same specifier
// through resolve.alias, which keeps the lent surface exercised by the shell's
// own build and typecheck.
//
// This export list IS the contract. druks-ui.test.ts pins it by exact equality:
// a name added here is public to every installed app, forever. Everything here
// must render without a provider — an app mounts it outside the shell's tree.
// Import-map target for '@druks/ui' (see vite.config.ts). This export list is
// the contract: a name added here is public to every installed app, forever, and
// must render without a provider.
export { Page } from '../components/Page'
export { PageHeader } from '../components/PageHeader'
export { SectionHead } from '../components/Common'
export { EmptyState } from '../components/EmptyState'
export { StatusGlyph } from '../components/StatusGlyph'
export { RelTime } from '../components/RelTime'
export { Button, Field, Select, TextInput } from '../components/Control'
// A run's operator actions. Gates are a platform pillar, so an app that ships a
// frontend has to be able to draw one — it loses the generic subject page that
// would otherwise carry it.
export { CancelRun, InAppReview, RetryRun } from '../components/RunControls'

export type { InputRequest, RunState } from '../api/types'
5 changes: 2 additions & 3 deletions frontend/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,8 @@ body[data-palette="mono"] {
--accent-violet: var(--text);
}

/* The interactive accent every control reads. Declared on ``body``, not
``:root``: the palettes retint --bucket-run on body[data-palette], and a
:root declaration would resolve against the base hue and skip them. */
/* On ``body``, not ``:root``: the palettes retint --bucket-run on
body[data-palette], which a :root declaration would resolve past. */
body { --accent: var(--bucket-run); }

* { box-sizing: border-box; }
Expand Down
12 changes: 4 additions & 8 deletions frontend/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,8 @@ const repoDist = fileURLToPath(
// document. In a build the shims are extra entries — hashed like any asset,
// with their shared code in the common chunks — and the import map (regenerated
// into index.html per build) carries the hashed names. In dev the Vite server
// serves the shim sources directly.
//
// Values carry their extension: the React shims are hand-written .js, while
// @druks/ui is .ts because it re-exports the shell's .tsx components.
// serves the shim sources directly. Values carry their extension: the React
// shims are .js, @druks/ui is .ts because it re-exports .tsx.
const SHARED_MODULES: Record<string, string> = {
react: 'react.js',
'react-dom': 'react-dom.js',
Expand All @@ -30,10 +28,8 @@ const shimUrl = (file: string) => new URL(`./src/runtime/${file}`, import.meta.u
// The rollup entry name for a shim, and what transformIndexHtml matches on.
const shimEntry = (file: string) => `runtime-${file.replace(/\.[jt]s$/, '')}`

// Bundled extensions import '@druks/ui' exactly as an installed app does, so a
// breaking change to the lent surface reddens the shell's own build, typecheck
// and tests before it can reach an app. Exported because vitest.config.ts needs
// the same alias and this is where it is decided.
// Bundled extensions import '@druks/ui' as an installed app does, so breaking
// the lent surface reddens the shell's own build first. Exported for vitest.
export const shellAlias = {
'@druks/ui': fileURLToPath(shimUrl(SHARED_MODULES['@druks/ui']!)),
}
Expand Down