Skip to content

v1.0.0: merge into a single enhancedForm, add draft persistence and autoSubmit#1

Merged
notnotjake merged 16 commits into
mainfrom
feat/rewrite
Jul 7, 2026
Merged

v1.0.0: merge into a single enhancedForm, add draft persistence and autoSubmit#1
notnotjake merged 16 commits into
mainfrom
feat/rewrite

Conversation

@notnotjake

@notnotjake notnotjake commented Jul 7, 2026

Copy link
Copy Markdown
Member

Summary

Rewrites the package around a single enhancedForm export and adds the two features planned for this release: per-field draft persistence and auto-submit. Prepared as the v1.0.0 release (version bumped, changelog stamped — tag v1.0.0 after merge to publish).

The merge (breaking)

createValidation + createEnhancedFormenhancedForm. The two were coupled in practice — one took the other as an option required for correctness, both built the same typed field proxy, and both demanded their own template spreads. Now there is one object, validation is always wired into the submission lifecycle (the manual updateIssues()/validateAll() calls in enhance handlers are gone), and the failure mode of forgetting the validation option no longer exists.

Migration:

  • createValidation(remote) + createEnhancedForm(remote, { validation })enhancedForm(remote, options)
  • {...valid.formHandler}{...form.handlers}
  • {...valid.fields.x.handlers}{...form.fields.x.validate}
  • resetOnSuccess: falsepreventResetOnSuccess: true (opt-in polarity)
  • reset() now resets the form element as well as the state; resetState() is the old state-only behavior

Draft persistence

Spread form.fields.<path>.persist on an input and its value survives reloads: debounced writes to web storage, restore on mount (through kit's field.set() so as() spreads stay in sync, marked dirty in validation), discarded on successful submit / form reset / expiry / discardPersisted(). The storage key defaults to the remote form's action id — a deterministic hash of the remote file path + export name, verified against the kit source, so it's stable across builds and self-invalidates on rename. Options: key, storage: 'local' | 'session', maxAgeMs.

autoSubmit

autoSubmit: true | { debounceMs } (default 600ms) submits via requestSubmit() once input settles — a real submit event, so preflight, submit-attempt issue display, and enhance callbacks run as for a button press. Built in: unchanged-data dirty check, mid-submission coalescing, immediate flush on change. Mutually exclusive with preventResetOnSuccess at the type level (auto-submitting forms never reset).

Testing

New vitest + jsdom suite (43 tests): state machine flows and races, autoSubmit debounce/dirty-check/coalescing, persist restore/expiry/discard, validation issue layers and validators. CI's test job runs it after the type checks (scripts/ci/test.sh now calls the vitest script — bun test can't compile runes or provide a DOM). Compile-time type tests extended for the new conditional surface.

Reviewable commit-by-commit: merge → persist → autoSubmit → tests → docs → release prep. Each commit type-checks; lint/test/build CI scripts all pass at HEAD.

Not verified interactively: persist restore and autoSubmit timing in a real browser — the example app (examples/contact-form) now has .persist on all fields for a quick manual check.

🤖 Generated with Claude Code


Summary by cubic

Consolidates @opensky/remotes around a single enhancedForm export and adds per‑field draft persistence and auto‑submit for a simpler, faster form experience. Polishes v1.0.0 with stability fixes for auto‑submit and persistence, safer draft restore, updated docs noting the svelte >=5.29 requirement, and a three‑demo example app with live code viewing.

  • New Features

    • One enhancedForm(remote, options) with a clear submission state machine and lifecycle callbacks.
    • Draft persistence via form.fields.<path>.persist (debounced writes, restore on mount; options: key, storage, maxAgeMs). Correctly persists checkbox groups as selected values and restores typed values for validators and fields.value().
    • Auto-submit with autoSubmit: true | { debounceMs } (dirty check, in-flight coalescing, change flush). Auto-submit forms never reset on success.
    • Inline validation via .validate field spreads and per-field validators; no manual updateIssues()/validateAll() calls.
    • Tests, docs, and examples: Vitest + jsdom suite; example app expanded to three demos (validation, auto-save, draft persistence) with an in-app code viewer and a raw-remote-source Vite plugin. The code viewer watches real .remote.ts files for live updates; the profile demo compares current data against the last saved values for its editing indicator and now merges submitted values over the saved snapshot to keep a stable shape (issues also outrank stale results).
    • Reliability and polish: cancel pending draft writes on discard(), cancel auto-submit debounce/queue on reset and flush after thrown submits, capture the dirty-check snapshot at submit time, guard draft restore (fields null check, array rejection, and Object.hasOwn), forward updates() calls with no args, export ValidationHandlers, and bump svelte peer to ^5.29.0 (READMEs note svelte >=5.29).
  • Migration

    • Replace createValidation(remote) + createEnhancedForm(remote, { validation }) with enhancedForm(remote, options).
    • Replace {...valid.formHandler} with {...form.handlers}.
    • Replace {...valid.fields.x.handlers} with {...form.fields.x.validate}.
    • resetOnSuccess: falsepreventResetOnSuccess: true. reset() now resets the form element and state; use resetState() for state-only.
    • autoSubmit cannot be used with preventResetOnSuccess (mutually exclusive).

Written for commit 5062c29. Summary will update on new commits.

Review in cubic

The two helpers were coupled in practice: createEnhancedForm took the
validation instance as an option required for correctness, both built the
same typed field proxy, and both required their own spreads in templates.
enhancedForm merges them into one object with validation always wired into
the submission lifecycle, so the manual updateIssues()/validateAll() calls
in enhance handlers (and the silent miswiring when the validation option
was forgotten) no longer exist.

- validation.svelte.ts becomes an internal core (createValidationCore);
  the field proxy and form-level handlers are assembled by enhancedForm
- Field spread renamed: fields.path.handlers -> fields.path.validate
- Form-level spread renamed: formHandler -> handlers
- resetOnSuccess (default true) replaced by the opt-in
  preventResetOnSuccess (default false)
- reset() now resets the form element as well as the submission state;
  resetState() keeps the old state-only behavior
- updateIssues() is internal; validateAll(), clearAllIssues(), allIssues,
  allKnownIssues, and formIssues remain public
Spread form.fields.<path>.persist onto an input and its value is saved to
web storage as the user types (debounced 300ms, flushed on change) and
restored when the input mounts — so a reload no longer loses what was
typed. Restored values go through kit's field.set() so as() spreads stay
in sync, and restored fields are marked dirty in validation.

Spreading .persist is the whole opt-in. The persist creation option only
overrides defaults: key (defaults to the remote form's action id, a
deterministic hash of the remote file path + export name that
self-invalidates when the function moves), storage ('local' default or
'session'), and maxAgeMs (expiry checked at restore; default none).

Drafts are discarded on successful submission, on form reset, on expiry,
or via the new discardPersisted(). File inputs are skipped. The attachment
spread is memoized per field path so template re-evaluations don't detach
and re-restore the field.
autoSubmit: boolean | { debounceMs?: number } (default 600ms). Once input
settles, the form submits itself via element.requestSubmit() — a real
submit event, so preflight validation, the submit-attempt issue display,
and the enhance pipeline all run exactly as they would for a button press.
The listener rides in the form-level handlers spread as an attachment.

Built-in behaviors rather than options:
- Dirty check: data identical to the last submission is never re-submitted
- In-flight coalescing: a debounce firing mid-submission waits for it to
  settle, then submits once more only if the data changed since
- Commit flush: a change event (text blur, select/checkbox pick) submits
  immediately instead of waiting out the debounce
- A debounce still pending at teardown is dropped (persistence holds the
  draft)

Auto-submitting forms never reset after success, so preventResetOnSuccess
is not accepted alongside autoSubmit — the options are mutually exclusive
at the type level.
43 tests across three suites:
- form.test.ts: the submission state machine (success/issues/error flows,
  default reset vs preventResetOnSuccess, cancel, delayed/timeout timer
  progression, the superseded-submission race, reset vs resetState) and
  autoSubmit behavior (debounce window, custom debounceMs, change flush,
  the unchanged-data dirty check, mid-submission coalescing, dropped
  debounce at teardown, no reset after auto-save)
- persist.test.ts: restore into element/kit state/dirty tracking, expiry,
  debounced writes with change flush, checkbox round-trip, shared drafts,
  sessionStorage selection, discard paths, memoized attachment identity
- validation.test.ts: issue-layer merging and clearing, the allIssues
  tree, validator lifecycle, dirty-gated debounced blur validation

Vitest is used rather than bun test because the runes in .svelte.ts
modules need the Svelte compiler and the persist/autoSubmit tests need a
DOM. jsdom ships no web storage, so a minimal in-memory implementation is
installed in the test setup. CI's test job runs the suite after the type
checks.
Rewrite the READMEs around the single enhancedForm export: creation
options, the three template spreads (handlers on the form, validate and
persist per field), draft persistence, autoSubmit, and the submission
state/callbacks. The old wiring caveats (passing validation to
createEnhancedForm, calling updateIssues/validateAll manually in enhance
handlers) are gone since that class of miswiring no longer exists.

The contact-form example now uses enhancedForm and demonstrates draft
persistence on all three fields — type into the form, reload, and the
values come back. The changelog records the merge and the new features,
with migration notes for each breaking change.
The API has settled: one enhancedForm export covering submission state,
validation, draft persistence, and auto-submit, locked in by compile-time
type tests and a runtime suite. Stamp the changelog and bump the package
version for the 1.0.0 tag.
The section opened with the new features, burying the fact that the
entire public API changed. Open with a breaking-rewrite notice and a
0.1.0 -> 1.0.0 migration table, and list the BREAKING entries before
the additions.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 issues found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/remotes/src/lib/form.svelte.ts">

<violation number="1" location="packages/remotes/src/lib/form.svelte.ts:8">
P1: Consumers on allowed Svelte 5.0–5.28 installs can fail at import time because this file now depends on `svelte/attachments`, which is only available since Svelte 5.29. The Svelte peer range should be raised to match the new API requirement.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

RemoteQueryUpdate
} from '@sveltejs/kit'
import type { Attachment } from 'svelte/attachments'
import { createAttachmentKey } from 'svelte/attachments'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Consumers on allowed Svelte 5.0–5.28 installs can fail at import time because this file now depends on svelte/attachments, which is only available since Svelte 5.29. The Svelte peer range should be raised to match the new API requirement.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/remotes/src/lib/form.svelte.ts, line 8:

<comment>Consumers on allowed Svelte 5.0–5.28 installs can fail at import time because this file now depends on `svelte/attachments`, which is only available since Svelte 5.29. The Svelte peer range should be raised to match the new API requirement.</comment>

<file context>
@@ -0,0 +1,703 @@
+	RemoteQueryUpdate
+} from '@sveltejs/kit'
+import type { Attachment } from 'svelte/attachments'
+import { createAttachmentKey } from 'svelte/attachments'
+import { tick } from 'svelte'
+import {
</file context>

Comment thread README.md
Comment thread packages/remotes/src/lib/persist.svelte.ts Outdated
Comment thread packages/remotes/src/lib/persist.svelte.ts Outdated
Comment thread packages/remotes/src/lib/form.svelte.ts
Comment thread packages/remotes/src/lib/index.ts Outdated
A kit checkbox-array field (as('checkbox', value)) renders several
same-named checkboxes that all belong to one string[] field, so they all
share one draft key. Storing each element's checked flag as a bare boolean
meant the last change overwrote the group's state, and restore applied
that boolean to every box — check only 'music', reload, and every box in
the group came back checked. The corrupted restore also leaked into
validation through both of its inputs: field.set(true) put a boolean into
a string[] kit field (whose checked getter calls .includes on it), and
preflight validated the wrongly-checked DOM.

Group members are detected by kit's []-suffixed name. readValue now
collects the group's selected values through el.form and stores the array
kit itself uses; restore checks each box by membership.

Also coerce restored values before writing them into kit's field state,
using the same name-prefix encoding kit uses to coerce FormData strings
(n:count -> number, b:agree -> boolean), so validators and fields.value()
see correctly typed values mid-session.
- Raise the svelte peer dependency to ^5.29.0 — the package imports
  svelte/attachments, which was introduced in that release
- Forward updates() to kit even when called with zero arguments: kit
  treats submit().updates() with no args as taking control of updates
  (suppressing invalidateAll), so it must not be dropped
- Reject persisted drafts whose fields property is null instead of
  crashing field mount (typeof null === 'object' passed the shape guard)
- Use Object.hasOwn when restoring so inherited properties like toString
  can't be mistaken for saved fields
- Only clear shared auto-submit state at teardown when the cleanup still
  owns it (guard against a newer attachment racing an older cleanup)
- Split the README creation-options example into two enhancedForm calls
  so it compiles (preventResetOnSuccess and autoSubmit are mutually
  exclusive and were shown in one options object)
- Export ValidationHandlers — it is the type of form.fields.x.validate
- Cancel pending debounced draft writes on discard() via a write
  generation: a write scheduled just before a successful submission's
  discard no longer fires afterwards and resurrects the stale draft
  (input after the discard schedules at the new generation and persists
  normally)
- Cancel the auto-submit debounce and queued flag on form reset — the
  debounce could previously fire after a reset and submit the
  freshly-emptied form
- Flush a queued auto-submit on the onSubmit-threw path like every other
  settle path, and clear the queued flag in resetState() (the invalidated
  submission can never flush it)
- Capture the auto-submit dirty-check snapshot on the submit event — the
  moment kit captures its FormData — instead of when the enhance callback
  runs. Input arriving during an async preflight was recorded as
  submitted without being sent, so the next debounce compared equal and
  skipped genuinely unsaved data.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 9 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/remotes/src/lib/form.svelte.ts">

<violation number="1" location="packages/remotes/src/lib/form.svelte.ts:8">
P1: Consumers on allowed Svelte 5.0–5.28 installs can fail at import time because this file now depends on `svelte/attachments`, which is only available since Svelte 5.29. The Svelte peer range should be raised to match the new API requirement.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/remotes/src/lib/persist.svelte.ts
An array passes the typeof-object and null checks, but JSON.stringify
drops string-keyed properties on arrays — so a corrupt array draft would
silently swallow every subsequent write and never self-heal. Treat it
like any other malformed draft: remove it, and let the next write start
a fresh object draft.
- /contact — the existing validation + submission-state demo
- /profile — auto-save: autoSubmit with no save button, a live
  Saving/Saved status line driven by form state
- /application — draft persistence across text inputs, a select, a
  textarea, and a checkbox group, with maxAgeMs expiry and a
  discard-draft button

A shared layout adds a tab bar to switch between examples, and every
page ends with a 'View the code' panel showing its +page.svelte, remote
function, and schema via ?raw imports. Remote files can't be imported
with ?raw directly (kit's remote transform matches them by filename and
rejects the raw module's default export), so a small vite plugin serves
them under a ?raw-source query through a virtual id kit never matches.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 issues found across 17 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="examples/contact-form/src/routes/+layout.svelte">

<violation number="1" location="examples/contact-form/src/routes/+layout.svelte:7">
P2: These root-relative navigation links won't work correctly if the app is ever deployed with a `paths.base` subpath in SvelteKit. Root-relative `/contact`, `/profile`, and `/application` hrefs do not automatically include the configured base prefix, so they'd navigate to the domain root rather than the mounted path. The `class:active` comparison against `page.url.pathname` would also mismatch under a base path. Consider importing `base` from `$app/paths` and composing links as `${base}/contact`, or using the `resolve` helper from `$app/paths` so the example remains robust regardless of deployment path.</violation>
</file>

<file name="packages/remotes/src/lib/form.svelte.ts">

<violation number="1" location="packages/remotes/src/lib/form.svelte.ts:8">
P1: Consumers on allowed Svelte 5.0–5.28 installs can fail at import time because this file now depends on `svelte/attachments`, which is only available since Svelte 5.29. The Svelte peer range should be raised to match the new API requirement.</violation>
</file>

<file name="examples/contact-form/src/lib/CodeViewer.svelte">

<violation number="1" location="examples/contact-form/src/lib/CodeViewer.svelte:14">
P2: The tab UI uses partial ARIA tab roles without completing the required pattern. The `role="tablist"` and `role="tab"` attributes tell assistive technologies this is a tab widget, but the implementation is missing `role="tabpanel"` on the content area, `aria-controls`/`aria-labelledby` linking tabs to panels, roving `tabindex`, and arrow/Home/End keyboard handling. Screen reader users will hear "tab" but cannot navigate it with expected keyboard shortcuts. Consider either completing the ARIA tabs implementation or removing the tab roles and keeping this as a simple button group.</violation>
</file>

<file name="examples/contact-form/package.json">

<violation number="1" location="examples/contact-form/package.json:19">
P2: Adding `@types/node@^26.1.0` introduces two maintainability risks for a SvelteKit example app:
1. The types target Node 26, but CI (`release.yml`) runs on Node 24, so TypeScript may accept APIs that fail at runtime.
2. `tsconfig.json` does not specify `compilerOptions.types`, so Node globals like `process` and `Buffer` leak into all browser/client modules, masking client-side runtime errors.

Consider either downgrading `@types/node` to match the CI runtime, scoping it with `types: ["node"]` only where needed, or adding `@types/node` to a server-only tsconfig/project reference rather than the root example package.</violation>
</file>

<file name="examples/contact-form/src/routes/profile/+page.svelte">

<violation number="1" location="examples/contact-form/src/routes/profile/+page.svelte:41">
P1: The invalid fields and error messages lack ARIA state/relationship attributes, making the form inaccessible to screen-reader users. The inputs only receive a CSS `invalid` class, and the error `<p>` elements have no IDs or `aria-describedby` linkage. This pattern will be copied by consumers since it is example code for the library.

Adding `aria-invalid="true"` when a field has issues, and connecting the error paragraphs to the input via `aria-describedby` with generated IDs, ensures assistive technologies announce validation state and messages.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

{...profileForm.fields.displayName.as('text', 'Ada Lovelace')}
{...form.fields.displayName.validate}
autocomplete="nickname"
class:invalid={form.fields.displayName.issues}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The invalid fields and error messages lack ARIA state/relationship attributes, making the form inaccessible to screen-reader users. The inputs only receive a CSS invalid class, and the error <p> elements have no IDs or aria-describedby linkage. This pattern will be copied by consumers since it is example code for the library.

Adding aria-invalid="true" when a field has issues, and connecting the error paragraphs to the input via aria-describedby with generated IDs, ensures assistive technologies announce validation state and messages.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/contact-form/src/routes/profile/+page.svelte, line 41:

<comment>The invalid fields and error messages lack ARIA state/relationship attributes, making the form inaccessible to screen-reader users. The inputs only receive a CSS `invalid` class, and the error `<p>` elements have no IDs or `aria-describedby` linkage. This pattern will be copied by consumers since it is example code for the library.

Adding `aria-invalid="true"` when a field has issues, and connecting the error paragraphs to the input via `aria-describedby` with generated IDs, ensures assistive technologies announce validation state and messages.</comment>

<file context>
@@ -0,0 +1,152 @@
+				{...profileForm.fields.displayName.as('text', 'Ada Lovelace')}
+				{...form.fields.displayName.validate}
+				autocomplete="nickname"
+				class:invalid={form.fields.displayName.issues}
+			/>
+		</label>
</file context>

let { children } = $props()

const examples = [
{ href: '/contact', label: 'Contact form', hint: 'validation & submission state' },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: These root-relative navigation links won't work correctly if the app is ever deployed with a paths.base subpath in SvelteKit. Root-relative /contact, /profile, and /application hrefs do not automatically include the configured base prefix, so they'd navigate to the domain root rather than the mounted path. The class:active comparison against page.url.pathname would also mismatch under a base path. Consider importing base from $app/paths and composing links as ${base}/contact, or using the resolve helper from $app/paths so the example remains robust regardless of deployment path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/contact-form/src/routes/+layout.svelte, line 7:

<comment>These root-relative navigation links won't work correctly if the app is ever deployed with a `paths.base` subpath in SvelteKit. Root-relative `/contact`, `/profile`, and `/application` hrefs do not automatically include the configured base prefix, so they'd navigate to the domain root rather than the mounted path. The `class:active` comparison against `page.url.pathname` would also mismatch under a base path. Consider importing `base` from `$app/paths` and composing links as `${base}/contact`, or using the `resolve` helper from `$app/paths` so the example remains robust regardless of deployment path.</comment>

<file context>
@@ -0,0 +1,93 @@
+	let { children } = $props()
+
+	const examples = [
+		{ href: '/contact', label: 'Contact form', hint: 'validation & submission state' },
+		{ href: '/profile', label: 'Profile settings', hint: 'auto-save' },
+		{ href: '/application', label: 'Job application', hint: 'draft persistence' }
</file context>

</button>

{#if open}
<div class="tabs" role="tablist">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The tab UI uses partial ARIA tab roles without completing the required pattern. The role="tablist" and role="tab" attributes tell assistive technologies this is a tab widget, but the implementation is missing role="tabpanel" on the content area, aria-controls/aria-labelledby linking tabs to panels, roving tabindex, and arrow/Home/End keyboard handling. Screen reader users will hear "tab" but cannot navigate it with expected keyboard shortcuts. Consider either completing the ARIA tabs implementation or removing the tab roles and keeping this as a simple button group.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/contact-form/src/lib/CodeViewer.svelte, line 14:

<comment>The tab UI uses partial ARIA tab roles without completing the required pattern. The `role="tablist"` and `role="tab"` attributes tell assistive technologies this is a tab widget, but the implementation is missing `role="tabpanel"` on the content area, `aria-controls`/`aria-labelledby` linking tabs to panels, roving `tabindex`, and arrow/Home/End keyboard handling. Screen reader users will hear "tab" but cannot navigate it with expected keyboard shortcuts. Consider either completing the ARIA tabs implementation or removing the tab roles and keeping this as a simple button group.</comment>

<file context>
@@ -0,0 +1,86 @@
+	</button>
+
+	{#if open}
+		<div class="tabs" role="tablist">
+			{#each files as file, index (file.name)}
+				<button
</file context>

"@sveltejs/adapter-auto": "^6.1.0",
"@sveltejs/kit": "^2.69.1",
"@sveltejs/vite-plugin-svelte": "^7.1.2",
"@types/node": "^26.1.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Adding @types/node@^26.1.0 introduces two maintainability risks for a SvelteKit example app:

  1. The types target Node 26, but CI (release.yml) runs on Node 24, so TypeScript may accept APIs that fail at runtime.
  2. tsconfig.json does not specify compilerOptions.types, so Node globals like process and Buffer leak into all browser/client modules, masking client-side runtime errors.

Consider either downgrading @types/node to match the CI runtime, scoping it with types: ["node"] only where needed, or adding @types/node to a server-only tsconfig/project reference rather than the root example package.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/contact-form/package.json, line 19:

<comment>Adding `@types/node@^26.1.0` introduces two maintainability risks for a SvelteKit example app:
1. The types target Node 26, but CI (`release.yml`) runs on Node 24, so TypeScript may accept APIs that fail at runtime.
2. `tsconfig.json` does not specify `compilerOptions.types`, so Node globals like `process` and `Buffer` leak into all browser/client modules, masking client-side runtime errors.

Consider either downgrading `@types/node` to match the CI runtime, scoping it with `types: ["node"]` only where needed, or adding `@types/node` to a server-only tsconfig/project reference rather than the root example package.</comment>

<file context>
@@ -16,6 +16,7 @@
 		"@sveltejs/adapter-auto": "^6.1.0",
 		"@sveltejs/kit": "^2.69.1",
 		"@sveltejs/vite-plugin-svelte": "^7.1.2",
+		"@types/node": "^26.1.0",
 		"svelte": "^5.56.4",
 		"svelte-check": "^4.7.1",
</file context>

Comment thread examples/contact-form/vite.config.ts
Comment thread examples/contact-form/src/routes/profile/+page.svelte Outdated
…READMEs

- Watch the real .remote.ts path from the raw-source virtual module so
  edits invalidate the code viewer in dev
- Track editing state on the profile page: form.result stays true until
  the next submission begins, so the status line showed 'Saved' over
  unsaved edits while the auto-submit debounce settled; issues now also
  rank above the stale result
- Mention the svelte >=5.29 requirement alongside kit in both READMEs

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="examples/contact-form/src/routes/+layout.svelte">

<violation number="1" location="examples/contact-form/src/routes/+layout.svelte:7">
P2: These root-relative navigation links won't work correctly if the app is ever deployed with a `paths.base` subpath in SvelteKit. Root-relative `/contact`, `/profile`, and `/application` hrefs do not automatically include the configured base prefix, so they'd navigate to the domain root rather than the mounted path. The `class:active` comparison against `page.url.pathname` would also mismatch under a base path. Consider importing `base` from `$app/paths` and composing links as `${base}/contact`, or using the `resolve` helper from `$app/paths` so the example remains robust regardless of deployment path.</violation>
</file>

<file name="packages/remotes/src/lib/form.svelte.ts">

<violation number="1" location="packages/remotes/src/lib/form.svelte.ts:8">
P1: Consumers on allowed Svelte 5.0–5.28 installs can fail at import time because this file now depends on `svelte/attachments`, which is only available since Svelte 5.29. The Svelte peer range should be raised to match the new API requirement.</violation>
</file>

<file name="examples/contact-form/src/lib/CodeViewer.svelte">

<violation number="1" location="examples/contact-form/src/lib/CodeViewer.svelte:14">
P2: The tab UI uses partial ARIA tab roles without completing the required pattern. The `role="tablist"` and `role="tab"` attributes tell assistive technologies this is a tab widget, but the implementation is missing `role="tabpanel"` on the content area, `aria-controls`/`aria-labelledby` linking tabs to panels, roving `tabindex`, and arrow/Home/End keyboard handling. Screen reader users will hear "tab" but cannot navigate it with expected keyboard shortcuts. Consider either completing the ARIA tabs implementation or removing the tab roles and keeping this as a simple button group.</violation>
</file>

<file name="examples/contact-form/package.json">

<violation number="1" location="examples/contact-form/package.json:19">
P2: Adding `@types/node@^26.1.0` introduces two maintainability risks for a SvelteKit example app:
1. The types target Node 26, but CI (`release.yml`) runs on Node 24, so TypeScript may accept APIs that fail at runtime.
2. `tsconfig.json` does not specify `compilerOptions.types`, so Node globals like `process` and `Buffer` leak into all browser/client modules, masking client-side runtime errors.

Consider either downgrading `@types/node` to match the CI runtime, scoping it with `types: ["node"]` only where needed, or adding `@types/node` to a server-only tsconfig/project reference rather than the root example package.</violation>
</file>

<file name="examples/contact-form/src/routes/profile/+page.svelte">

<violation number="1" location="examples/contact-form/src/routes/profile/+page.svelte:24">
P2: The local `editing` flag can get permanently stuck in the wrong state, which will mislead users. It flips to `true` on every keystroke but is only cleared when a submission actually starts (`form.pending`). Because the example's auto-submit skips sending data that hasn't really changed, a user who types and then reverts before the debounce ends will never trigger `pending`, leaving `editing === true` and the status message stuck on "Unsaved changes — pausing will save them" indefinitely. Instead of relying only on `oninput` and `pending`, the component should tie `editing` more directly to whether the current field values differ from the last saved state (for example, by diffing against `form.result` data or by using a library-level dirty indicator if available).</violation>

<violation number="2" location="examples/contact-form/src/routes/profile/+page.svelte:41">
P1: The invalid fields and error messages lack ARIA state/relationship attributes, making the form inaccessible to screen-reader users. The inputs only receive a CSS `invalid` class, and the error `<p>` elements have no IDs or `aria-describedby` linkage. This pattern will be copied by consumers since it is example code for the library.

Adding `aria-invalid="true"` when a field has issues, and connecting the error paragraphs to the input via `aria-describedby` with generated IDs, ensures assistive technologies announce validation state and messages.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread examples/contact-form/src/routes/profile/+page.svelte
Typing and then reverting to the saved value before the debounce fired
left the keystroke-latched editing flag stuck on 'Unsaved changes' —
auto-submit correctly skips unchanged data, so no submission ever
cleared it. Derive the flag by comparing the form's current data against
the last saved values instead, updated from onReturn.
fields.value() types every property as optional; spreading over the
previous snapshot keeps the required shape.
@notnotjake
notnotjake merged commit c0c4daa into main Jul 7, 2026
4 checks passed
@notnotjake
notnotjake deleted the feat/rewrite branch July 8, 2026 01:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant