ConusAI vendor v0.1.0-dev.20261027+9b1131fb
ci / gate (push) Waiting to run

This commit is contained in:
ConusAI vendor v0.1.0-dev.20261027+9b1131fb
2026-10-27 18:05:17 +00:00
commit 3102273ec7
538 changed files with 80203 additions and 0 deletions
+162
View File
@@ -0,0 +1,162 @@
"use client"
// A note's attachments: the list with a remove button on each, and the upload.
//
// An attachment is two steps, both the API's own: the browser sends the file to the files module
// (the kit's `uploadFile`: `files.file.beginUpload`, one `files.file.putPart` per piece, and
// `files.file.completeUpload`, which answer before anything that follows an upload has run), and
// then links the stored file to the note (`notes.attachment.add`). A file uploaded and never linked
// is an ordinary file of the project.
import { downloadHref, type ProjectSlugs, uploadFile, useTransport } from "@conusai-kit/ui"
import { type FormEvent, type ReactElement, useId, useState } from "react"
export interface AttachmentRow {
readonly attachmentId: string
readonly fileId: string
readonly filename: string
readonly contentType: string
/** Already formatted: a size is a message of the module, not arithmetic of the component. */
readonly size: string
}
export interface AttachmentLabels {
readonly title: string
readonly count: string
readonly empty: string
readonly file: string
readonly type: string
readonly sizeHeading: string
readonly actions: string
/** `Remove {filename}`: the button's accessible name, with the placeholder still in it. */
readonly remove: string
/** The button's visible text, which its accessible name contains. */
readonly removeShort: string
/** `Download {filename}`: a same-origin, project-scoped file stream. */
readonly download: string
readonly off: string
readonly uploadLabel: string
readonly uploadHint: string
readonly uploadSubmit: string
readonly uploadBusy: string
readonly uploadDone: string
readonly failed: string
}
type State = { kind: "idle" | "busy" | "done" } | { kind: "failed"; detail: string }
export function Attachments({
slugs,
noteId,
rows,
enabled,
accept,
labels,
}: {
slugs: ProjectSlugs
noteId: string
rows: readonly AttachmentRow[]
enabled: boolean
accept: readonly string[]
labels: AttachmentLabels
}): ReactElement {
const transport = useTransport()
const { call, describe } = transport
const inputId = useId()
const hintId = useId()
const [state, setState] = useState<State>({ kind: "idle" })
const attach = async (event: FormEvent<HTMLFormElement>): Promise<void> => {
event.preventDefault()
const form = event.currentTarget
const file = new FormData(form).get("file")
if (!(file instanceof File) || file.size === 0) return
setState({ kind: "busy" })
try {
const stored = await uploadFile(transport, slugs, file)
await call("notes.attachment.add", { ...slugs, noteId, fileId: stored.fileId })
form.reset()
setState({ kind: "done" })
window.location.reload()
} catch (error) {
setState({ kind: "failed", detail: describe(error) })
}
}
const remove = async (attachmentId: string): Promise<void> => {
setState({ kind: "busy" })
try {
await call("notes.attachment.remove", { ...slugs, noteId, attachmentId })
window.location.reload()
} catch (error) {
setState({ kind: "failed", detail: describe(error) })
}
}
return (
<section aria-labelledby={`${inputId}-heading`}>
<h2 id={`${inputId}-heading`}>{labels.title}</h2>
<p>{labels.count}</p>
{rows.length === 0 ? (
<p>{labels.empty}</p>
) : (
<table>
<caption>{labels.title}</caption>
<thead>
<tr>
<th scope="col">{labels.file}</th>
<th scope="col">{labels.type}</th>
<th scope="col">{labels.sizeHeading}</th>
<th scope="col">{labels.actions}</th>
</tr>
</thead>
<tbody>
{rows.map((row) => (
<tr key={row.attachmentId}>
<th scope="row">{row.filename}</th>
<td>{row.contentType}</td>
<td>{row.size}</td>
<td>
<a
href={downloadHref("files.file.download", { ...slugs, fileId: row.fileId })}
aria-label={labels.download.replace("{filename}", row.filename)}
>
{labels.download.replace("{filename}", row.filename)}
</a>{" "}
<button
type="button"
disabled={state.kind === "busy"}
aria-label={labels.remove.replace("{filename}", row.filename)}
onClick={() => void remove(row.attachmentId)}
>
{labels.removeShort}
</button>
</td>
</tr>
))}
</tbody>
</table>
)}
{enabled ? (
<form onSubmit={attach} aria-busy={state.kind === "busy"}>
<div className="kit-field">
<label htmlFor={inputId}>{labels.uploadLabel}</label>
<input id={inputId} name="file" type="file" accept={accept.join(",")} required aria-describedby={hintId} />
<p id={hintId}>{labels.uploadHint}</p>
</div>
<button type="submit" disabled={state.kind === "busy"}>
{state.kind === "busy" ? labels.uploadBusy : labels.uploadSubmit}
</button>
<p role="status">{state.kind === "done" ? labels.uploadDone : ""}</p>
</form>
) : (
<p role="status">{labels.off}</p>
)}
{state.kind === "failed" ? (
<p className="app-error" role="alert">
{labels.failed.replace("{detail}", state.detail)}
</p>
) : null}
</section>
)
}
+69
View File
@@ -0,0 +1,69 @@
"use client"
// Deleting a note, in two steps: the button asks, and a second press does it. The attached files are
// not deleted with the note (they are the project's); the confirmation says so.
import { type ProjectSlugs, useTransport } from "@conusai-kit/ui"
import { type ReactElement, useState } from "react"
export interface DeleteNoteLabels {
readonly button: string
readonly confirm: string
readonly yes: string
readonly no: string
readonly failed: string
}
export function DeleteNote({
slugs,
noteId,
listHref,
labels,
}: {
slugs: ProjectSlugs
noteId: string
/** Where to go once the note is gone. */
listHref: string
labels: DeleteNoteLabels
}): ReactElement {
const { call, describe } = useTransport()
const [state, setState] = useState<{ kind: "idle" | "asking" | "busy" } | { kind: "failed"; detail: string }>({
kind: "idle",
})
const remove = async (): Promise<void> => {
setState({ kind: "busy" })
try {
await call("notes.note.delete", { ...slugs, noteId })
window.location.assign(listHref)
} catch (error) {
setState({ kind: "failed", detail: describe(error) })
}
}
return (
<div>
{state.kind === "asking" || state.kind === "busy" ? (
<fieldset>
<legend>{labels.button}</legend>
<p>{labels.confirm}</p>
<button type="button" disabled={state.kind === "busy"} onClick={() => void remove()}>
{labels.yes}
</button>
<button type="button" disabled={state.kind === "busy"} onClick={() => setState({ kind: "idle" })}>
{labels.no}
</button>
</fieldset>
) : (
<button type="button" onClick={() => setState({ kind: "asking" })}>
{labels.button}
</button>
)}
{state.kind === "failed" ? (
<p className="app-error" role="alert">
{labels.failed.replace("{detail}", state.detail)}
</p>
) : null}
</div>
)
}
+17
View File
@@ -0,0 +1,17 @@
// The notes module's pages and components. `notesPage` is what the web shell mounts; the rest is
// exported for an app that composes its own page from the parts.
export { type AttachmentLabels, type AttachmentRow, Attachments } from "./attachments.js"
export { DeleteNote, type DeleteNoteLabels } from "./delete-note.js"
export {
attachmentLabels,
createFormLabels,
deleteLabels,
editFormLabels,
listLabels,
sizeLabel,
type Translate,
} from "./labels.js"
export { NoteForm, type NoteFormLabels, type NoteFormTarget } from "./note-form.js"
export { NoteList, type NoteListLabels } from "./note-list.js"
export { type NotesPage, notesPage } from "./pages.js"
+86
View File
@@ -0,0 +1,86 @@
// The labels each component takes, built from a message lookup. Every key this module's pages use is
// named here and nowhere else, so `__tests__/i18n.test.ts` can run these builders against the
// catalogues and fail on a key either language lacks -- the page itself holds no string.
//
// `t` takes a key relative to the module (`form.create`); the page binds it to
// `i18n.message("notes." + key)`. The one message that is not this module's own, the app's
// `app.failed`, is passed in already looked up.
import type { AttachmentLabels } from "./attachments.js"
import type { DeleteNoteLabels } from "./delete-note.js"
import type { NoteFormLabels } from "./note-form.js"
import type { NoteListLabels } from "./note-list.js"
export type Translate = (key: string, values?: Record<string, string | number>) => string
export function createFormLabels(t: Translate, failed: string): NoteFormLabels {
return {
heading: t("form.createHeading"),
title: t("form.titleLabel"),
body: t("form.bodyLabel"),
submit: t("form.create"),
busy: t("form.creating"),
saved: t("form.saved"),
failed,
}
}
export function editFormLabels(t: Translate, failed: string): NoteFormLabels {
return {
heading: t("form.editHeading"),
title: t("form.titleLabel"),
body: t("form.bodyLabel"),
submit: t("form.save"),
busy: t("form.saving"),
saved: t("form.saved"),
failed,
}
}
export function listLabels(t: Translate): NoteListLabels {
return {
caption: t("list.caption"),
empty: t("list.empty"),
note: t("list.note"),
attachments: t("list.attachments"),
updated: t("list.updated"),
// The `{title}` placeholder is the component's to fill, so it is passed through untouched.
open: t("list.open", { title: "{title}" }),
}
}
export function attachmentLabels(t: Translate, failed: string, count: number): AttachmentLabels {
return {
title: t("attachments.title"),
count: t("attachments.count", { count }),
empty: t("attachments.empty"),
file: t("attachments.file"),
type: t("attachments.type"),
sizeHeading: t("attachments.size"),
actions: t("attachments.actions"),
download: t("attachments.download", { filename: "{filename}" }),
remove: t("attachments.remove", { filename: "{filename}" }),
removeShort: t("attachments.removeShort"),
off: t("attachments.off"),
uploadLabel: t("upload.label"),
uploadHint: t("upload.hint"),
uploadSubmit: t("upload.submit"),
uploadBusy: t("upload.busy"),
uploadDone: t("upload.done"),
failed,
}
}
export function deleteLabels(t: Translate, failed: string): DeleteNoteLabels {
return {
button: t("delete.button"),
confirm: t("delete.confirm"),
yes: t("delete.yes"),
no: t("delete.no"),
failed,
}
}
/** A size as the message of the module: whole kilobytes, at least one. */
export const sizeLabel = (t: Translate, bytes: number): string =>
t("attachments.sizeKb", { size: Math.max(1, Math.ceil(bytes / 1024)) })
+112
View File
@@ -0,0 +1,112 @@
"use client"
// Creates a note, or saves a change to one. The same form for both: a note being edited carries the
// version it was read at, so a second editor's save is a 409 shown here and never a silent overwrite.
import { emitAppEvent, type ProjectSlugs, useTransport } from "@conusai-kit/ui"
import { type FormEvent, type ReactElement, useId, useState } from "react"
export interface NoteFormLabels {
readonly heading: string
readonly title: string
readonly body: string
readonly submit: string
readonly busy: string
readonly saved: string
/** The app's `app.failed` message, with `{detail}` still in it. */
readonly failed: string
}
export type NoteFormTarget =
| { readonly mode: "create"; readonly listHref: string }
| {
readonly mode: "edit"
readonly noteId: string
readonly version: number
readonly title: string
readonly body: string
}
export function NoteForm({
slugs,
target,
labels,
}: {
slugs: ProjectSlugs
target: NoteFormTarget
labels: NoteFormLabels
}): ReactElement {
const { call, describe } = useTransport()
const titleId = useId()
const bodyId = useId()
const [state, setState] = useState<{ kind: "idle" | "busy" | "saved" } | { kind: "failed"; detail: string }>({
kind: "idle",
})
const submit = async (event: FormEvent<HTMLFormElement>): Promise<void> => {
event.preventDefault()
const form = new FormData(event.currentTarget)
const title = String(form.get("title") ?? "").trim()
const body = String(form.get("body") ?? "")
if (title.length === 0) return
setState({ kind: "busy" })
try {
if (target.mode === "create") {
const created = (await call("notes.note.create", { ...slugs, title, body })) as { noteId: string }
// For whatever listens in the browser (an in-app guide's checklist task); it waits for a
// listener that asked to store the event first, because the next line leaves the page.
await emitAppEvent("note_created")
window.location.assign(`${target.listHref}/${created.noteId}`)
return
}
await call("notes.note.update", {
...slugs,
noteId: target.noteId,
title,
body,
expectedVersion: target.version,
})
setState({ kind: "saved" })
// The page is rendered on the server: a reload shows the new version, which the next save needs.
window.location.reload()
} catch (error) {
setState({ kind: "failed", detail: describe(error) })
}
}
return (
<form onSubmit={submit} aria-busy={state.kind === "busy"}>
<h2>{labels.heading}</h2>
<div className="kit-field" data-guide="notes.note-title">
<label htmlFor={titleId}>{labels.title}</label>
<input
id={titleId}
name="title"
type="text"
required
maxLength={200}
defaultValue={target.mode === "edit" ? target.title : ""}
/>
</div>
<div className="kit-field">
<label htmlFor={bodyId}>{labels.body}</label>
<textarea
id={bodyId}
name="body"
rows={6}
maxLength={20000}
defaultValue={target.mode === "edit" ? target.body : ""}
/>
</div>
<button type="submit" disabled={state.kind === "busy"}>
{state.kind === "busy" ? labels.busy : labels.submit}
</button>
<p role="status">{state.kind === "saved" ? labels.saved : ""}</p>
{state.kind === "failed" ? (
<p className="app-error" role="alert">
{labels.failed.replace("{detail}", state.detail)}
</p>
) : null}
</form>
)
}
+56
View File
@@ -0,0 +1,56 @@
// The project's notes, one row each. A server component: it holds no state, and every string is passed in.
import type { ReactElement } from "react"
import type { NoteView } from "../contract/index.js"
export interface NoteListLabels {
readonly caption: string
readonly empty: string
readonly note: string
readonly attachments: string
readonly updated: string
/** `Open {title}`: the link's accessible name, so a list of links is not a list of the same word. */
readonly open: string
}
export function NoteList({
notes,
hrefFor,
labels,
formatDate,
}: {
notes: readonly NoteView[]
hrefFor: (noteId: string) => string
labels: NoteListLabels
formatDate: (iso: string) => string
}): ReactElement {
if (notes.length === 0) {
return <p data-guide="notes.note-list">{labels.empty}</p>
}
return (
// guide: The notes of this project
<table data-guide="notes.note-list">
<caption>{labels.caption}</caption>
<thead>
<tr>
<th scope="col">{labels.note}</th>
<th scope="col">{labels.attachments}</th>
<th scope="col">{labels.updated}</th>
</tr>
</thead>
<tbody>
{notes.map((note) => (
<tr key={note.noteId}>
<th scope="row">
<a href={hrefFor(note.noteId)} aria-label={labels.open.replace("{title}", note.title)}>
{note.title}
</a>
</th>
<td>{note.attachmentCount}</td>
<td>{formatDate(note.updatedAt)}</td>
</tr>
))}
</tbody>
</table>
)
}
+83
View File
@@ -0,0 +1,83 @@
// The notes module's pages, as the one server component the web shell mounts under
// `/<org>/<workspace>/<project>/notes` (apps/web's module mount reads it from `modules/index.ts`):
//
// /notes the list and the form that adds a note
// /notes/<noteId> one note: edit it, attach files, delete it
//
// Every read is a procedure through the same caller the API uses (spec §6.2: one API path); every
// write is a client component that calls a procedure through the shell's transport.
import { type Container, notFound, type TransportCaller } from "@conusai-kit/core"
import type { I18n } from "@conusai-kit/i18n"
import type { ProjectPage } from "@conusai-kit/ui"
import type { ReactElement } from "react"
import { ATTACHMENT_CONTENT_TYPES, noteGet, noteList } from "../contract/index.js"
import { FeedbackButton } from "../guides/feedback-button.js" // kit:guides
import { feedbackLabels } from "../guides/feedback-labels.js" // kit:guides
import { Attachments } from "./attachments.js"
import { DeleteNote } from "./delete-note.js"
import { attachmentLabels, createFormLabels, deleteLabels, editFormLabels, listLabels, sizeLabel } from "./labels.js"
import { NoteForm } from "./note-form.js"
import { NoteList } from "./note-list.js"
export type NotesPage = ProjectPage<TransportCaller, I18n, Container>
export const notesPage: NotesPage = async ({ slugs, segments, caller, i18n }): Promise<ReactElement> => {
const t = (key: string, values?: Record<string, string | number>) => i18n.message(`notes.${key}`, values)
const failed = i18n.message("app.failed")
const listHref = `/${slugs.org}/${slugs.workspace}/${slugs.project}/notes`
if (segments.length === 0) {
const { notes } = await caller.call(noteList, slugs)
return (
<>
<h1>{t("title")}</h1>
<p>{t("intro")}</p>
<FeedbackButton labels={feedbackLabels(t)} /* kit:guides */ />
{/* guide: The form that adds a note */}
<div data-guide="notes.new-note">
<NoteForm slugs={slugs} target={{ mode: "create", listHref }} labels={createFormLabels(t, failed)} />
</div>
<NoteList
notes={notes}
hrefFor={(noteId) => `${listHref}/${noteId}`}
labels={listLabels(t)}
formatDate={(iso) => i18n.date(new Date(iso))}
/>
</>
)
}
const [noteId, ...rest] = segments
if (noteId === undefined || rest.length > 0) throw notFound("no such page")
const { note, attachments, attachmentsEnabled } = await caller.call(noteGet, { ...slugs, noteId })
return (
<>
<p>
<a href={listHref}>{t("detail.back")}</a>
</p>
<h1>{note.title}</h1>
<p>{t("detail.changed", { date: i18n.date(new Date(note.updatedAt)) })}</p>
<NoteForm
slugs={slugs}
target={{ mode: "edit", noteId: note.noteId, version: note.version, title: note.title, body: note.body }}
labels={editFormLabels(t, failed)}
/>
<Attachments
slugs={slugs}
noteId={note.noteId}
rows={attachments.map((attachment) => ({
attachmentId: attachment.attachmentId,
fileId: attachment.fileId,
filename: attachment.filename,
contentType: attachment.contentType,
size: sizeLabel(t, attachment.size),
}))}
enabled={attachmentsEnabled}
accept={ATTACHMENT_CONTENT_TYPES}
labels={attachmentLabels(t, failed, attachments.length)}
/>
<DeleteNote slugs={slugs} noteId={note.noteId} listHref={listHref} labels={deleteLabels(t, failed)} />
</>
)
}