Skip to content
SheetForge

SheetForge Web

SheetForge ships a companion web app at web.sheetforge.workers.dev. It brings authoring, validation and sheet-reflection to the browser — no Unity, no install.

The same core, not a re-implementation

The web app compiles the exact same C# core sources — the ones the headless .NET test harness runs — to a .NET browser-wasm assembly. There is no second copy of the parser or validator, so the two can never drift apart: a rule fixed once is fixed in both places.

Two things follow directly from that design.

  • Your compiled plugin DLLs load unmodified, and they light up the same slots here as in the editor (below).
  • The sheet-syntax rules on this site hold identically in the browser. Markers, the type system, @overlap, @style, @enum, @loc and reference integrity all behave the same way, because it is the same code answering. That includes IntId@Tab and LocRef@Tab parity — see Sheet Syntax.

One plugin, twelve slots, two hosts

A plugin does not have a "web subset". Assembly — instantiation, ordering, isolation and the compatibility gate — is a single pure Core function that both hosts call. Only discovery differs: Unity indexes the project's types, while the browser scans the assembly you uploaded.

So the twelve slots a plugin can fill are the same on both sides:

SlotIn the browser
Enums · cell parsers · wrapper typesCells parse, validate and round-trip through your own notation
Domain validatorsYour rules appear in Problems beside the core ones
Edge contributorsLinks buried in your notation are drawn on the canvas and counted in the reference index
Structural markersYour @marker rows are accepted and validated
Sheet templatesYour templates appear in Create sheet
Canvas augmentersVirtual nodes, extra edges, layer and display hints
Code registriesKeys that live in code stop being drawn as broken references
Colour presetsMapped onto the app's CSS variables
Declarative studio UIYour actions, panels, column badges and cell-editor hints, rendered in React
UI stringsYour labels follow the user's language through the same overlay t() consults
Pipeline observersNotified at the end of an import cycle, exactly as in the editor

The declarative surfaces are the reason a plugin's authoring extensions exist here at all. A contract that handed back a UIToolkit element could never load in a browser, so the shell is described as data — an id, a label key, a placement, a tone — and each host draws it with its own widgets.

Where a pack needs something the vocabulary cannot say, it registers an editor-only rich panel under the same id. The editor draws that one, the browser draws the described one, and the extension is never simply missing.

Two rules hold at render time. Text arriving from a plugin is escaped, and an action whose situation changed since it was drawn answers with an honest no-op instead of acting on a stale id.

A plugin assembly may also declare which generation of the plugin format it was built for. If it falls outside what this host reads, the whole assembly is refused with a readable reason rather than half-loaded. It is the same gate, reading the same declaration, on the market path, the sideload path and a local file. See Plugin Authoring.

What stays in Unity

Codegen and baking remain a Unity-only responsibility. The browser cannot emit .cs files or write ScriptableObjects, and it does not pretend to. The same honesty covers localization: a localization sheet is fully authored and validated here, but synchronizing Unity Localization StringTables is the editor's job — the browser has no Unity project to write tables into. The same goes for anything that needs the project itself: the asset picker, drag-and-drop from the Project window and the Addressables registrations they stage exist in the editor only; the <Type> of AssetRef@Group<Type> is parsed and shown in the browser but resolved — and type-checked — only where the project's types are loaded; and the editor's asset thumbnails, enlarged previews and audio playback have no web counterpart, because the browser has no project assets to read — an asset cell in the web grid is its address text.

The web output is the reflected sheet — the validated, round-tripped source — not baked assets. You author and validate in the browser, then run an import in Unity when you want strongly-typed classes and baked SOs. The sheet is the single source of truth either way, so the two surfaces meet at the sheet.

What the web app does

Everything is round-tripped through a single JSON boundary contract to the WebAssembly core, so the browser UI never re-derives a rule the core already owns.

AreaWhat you get
Authoring sessionImport, projection, and a Problems panel fed by the same import validator — pre-flight diagnostics, not a separate check. Import takes TSV and CSV files and .xlsx workbooks — every sheet in a workbook arrives as its own tab, read by the same reader the editor uses (compiled to WebAssembly), and when something had to be interpreted — a formula's cached value, a date as yyyy-MM-dd text, formatting not read — the import dialog says so in a "How this workbook was read" note.
Table editingPer-type cell widgets, undo, column pinning, sort, search, @style grouping and colour — plus the spreadsheet column set the editor has: drag the header handle to set a column's width (double-click to fit; saved per sheet in this browser), hide and show columns with the letters and a boundary mark kept honest, per-column value filters that combine with search, and clear-column-data as one undo step. + Row stages a row at once with a suggested key — integer id columns pre-filled — ready to rename in the key cell, and a sheet with no key column disables it with the reason. Enum and optional-boolean cells open the same searchable picker reference cells use — and only one picker window is ever open: opening another closes the last, and clicking elsewhere or Esc closes it. Row numbers and column letters select (Ctrl toggles, Shift spans), the header and body highlight the selection, the selected cell draws an outline that follows the keyboard, and a selection drags to a new position with an insertion line — a non-adjacent selection arrives gathered at the drop point in the order it was selected, as one undo step — while the right-click menus apply width, fit, hide, move, delete and clear-data to every selected column (moves and delete to every selected row), single-target items standing apart in their own section. Enum definition sheets have the same column-width axis as data sheets — drag, double-click to fit, exact width, fit to data, hide. Past 200 rows the grid renders only the visible window (plus overscan), with spacers holding the true scroll height — the same threshold the editor table uses, so both feel the same on a large sheet.
Visual valuesColor, AnimationCurve and Gradient cells show a preview that fills the cell — a swatch, a curve polyline, a gradient bar — and open a full editor in a popover: a colour editor with an HSV square, hue and alpha sliders and a hex field (the browser's native colour input has no alpha, so it is not used); a curve editor with a zoomable grid, draggable keys and tangent handles, tangent modes (Free, Auto, Linear, Constant, ClampedAuto — right-click a key or use the select), broken and weighted toggles, numeric fields, pre/post wrap and the Linear / Ease In-Out / Constant presets; a gradient editor with draggable colour and alpha keys (up to 8 each), a mode select and a colour-space select. Every change is round-tripped through the WebAssembly core — parsing, tangent recomputation and sampling are C#, not JavaScript — and the committed text is the same canonical form the editor writes, byte for byte. Changes commit the moment you make them, exactly like the editor's native fields: the cell, the canvas and Problems follow along while the editor is open, one editing session coalesces into one undo step however many changes it makes, and reopening the editor starts a new step. There is no Apply — the one button is Close (Esc closes too, and in the colour editor Esc also restores the value from before the session); reverting is Ctrl+Z. Opening and closing without changing anything stages nothing. An optional column shows when empty and offers Clear (default) in a right-click menu; a List<> of the three is a chip list whose add / remove / reorder / edit gestures are each one undo step. The preview strip sits inside the standard row height, as in the editor's table. The canvas value rows use the same components.
Structure editingAdd / remove / move columns, the four-field column form, tab rename (mutual swaps and cycles included; xlsx tabs are blocked), sheet create / delete, and an active-settings dropdown — each staged as one undo step. Enum definition sheets are authored the same way — define a new enum, rename (with every referring @type rewritten in the same batch), delete, reorder columns, edit the underlying type and description — under the same rules as the editor, because it is the same code ruling.
Cross-sheet searchThe sidebar's All entry searches every sheet's ids and field values by partial match, listing sheet · key · field · value, 50 rows to a page — the same matching, ordering and page size as the editor. It reads the values the session shows right now, staged edits included, so a value you just changed is found at once. Double-click a result — or select it and press Enter — and that sheet opens with the matched cell selected; since the search reads the session itself, every result can be jumped to.
LocalizationLocalization sheets are ordinary sheets here — authoring, validation and per-locale coverage included. A data sheet's LocRef cell shows the entry's source-locale text inline and opens the same key picker other references have; typing into an empty cell mints the key, its source text and the reference as one undo step, exactly as the editor does. The locale lens toggles visible locale columns (display-only), and the translation workbook exports chosen locales to xlsx and re-imports the returned file as a locale-columns-only merge. Synchronizing Unity Localization StringTables stays in the editor.
Record canvasA record's materials and consumers are cards you connect by dragging wires — the canvas reuses the same pure layout, wiring and edit-chain code as the editor, so the pixel math lives in C#, not in a JS graph library. Cards show the same projection as the table: a column staged in this batch already stands on the card marked as pending, a staged rename shows the new name over the original value, and a staged delete removes the row here too.
OutputTSV / CSV download per sheet, an .xlsx workbook download carrying every chosen sheet in one file (a sheet name the workbook rules cannot hold is adjusted and reported, never silently renamed), and Google Sheets read/write, with the sheet's data-validation dropdowns refreshed automatically after every push (see below).

Made identical, not merely similar

The web UI matches the in-editor Data Studio on purpose, down to the shared logic.

  • Shared answers. The header predicates (pending / edited), the diff modal, and the projection-composition order are shared C#, so both surfaces give the same answer to the same question.
  • Shared sentences. Where the web mirrors a Studio screen it uses the Studio's own string keys, and the values are generated from the editor's language tables at build time rather than re-typed. Adding a sentence that both surfaces show is one key in Lang*.cs; drift is structurally impossible rather than merely discouraged. Two things are deliberately kept separate, and a build-time guard enforces the boundary:
    • strings that are genuinely web-only stay in the web catalog;
    • diagnostics and reports are rendered by the WebAssembly core, not re-worded in the UI.
  • Shared look. The graph and sheet are shown together in a resizable split panel that mirrors the editor's canvas-clamp behaviour, and every colour comes from the editor's .aw-root token set as the single source of truth — no hardcoded palette.
  • Shared component kit. The UI is built on an in-house aw kit whose class names, tokens and pop-up placement match the editor's USS and the canonical HTML mockup one-to-one. One mockup now feeds two ports — the web app and the UIToolkit editor — instead of the web drifting on its own. web/Docs/ui-parity.md records how the web follows the studio when it changes.
  • Shared cell editors. Which widget a cell gets is decided by the core's cell-editor hint — a pack's registration first, then the built-in table (BuiltinCellEditorHints) — so a plugin type that declares the ColorPicker, CurveEditor or GradientEditor archetype opens the same editors here that it opens in Unity, and the two hosts cannot pick different widgets for the same type name.
  • Canvas parity. Drag-and-drop reached full parity with the editor: pan on middle-drag or Alt-drag, empty-space cascade menus, wire and node inspectors, picked-wire ghosting, and terminus delete with a confirm dialog. The zoom range is the editor's (25–200%), and the card metrics come from the core rather than a second copy in JavaScript.
  • Shared type list. The @type dropdown offers what the parser registry actually knows, grouped exactly as the editor groups them: the built-in scalars (Color, AnimationCurve and Gradient included), RecordId / IntId keys, RecordId@Tab and IntId@Tab references, Enum<…>, AssetRef@Group, and any type or wrapper a loaded plugin registered. A plugin that registers a new type therefore appears in the browser without a web change. (Before the WebAssembly core finishes booting, the very first paint falls back to the four built-in scalars.)

When to use which

  • Use the browser for quick authoring, validation and sheet edits from anywhere, and for teammates who do not open Unity.
  • Use Unity when you need generated C# classes and baked ScriptableObjects — the import, codegen and bake steps.

Both write to the same sheet, so a change made in one is picked up by the other on the next read.