Solid Memo Studio is a second app beside Solid Memo, in the same page,
at https://solid-memo.com/#/studio. Users manage their decks there,
and creators write decks and courses and publish them.
To manage, it edits and groups an instance's decks in a table, and a deck's cards one by one or at once, with their wrong options, schedules and answers. It shows a deck's schedule to come and everything wrong with a deck or the instance, upgrades copies of releases, exports and imports decks as files, and draws the instance's data as a graph.
To create, it keeps a creator's drafts of releases in the instance: decks and courses, with their metadata and provenance. A draft is checked by every rule a release is held to, previewed, tried, compared with the release it follows, and published in the pod or saved as a file for the library. A learner adds a published release in Solid Memo from its link (deck-library.md).
The Studio is its own package, @solid-memo/studio in
apps/studio/, with its own screens, tests and
router, on the same layers as Solid Memo's app
(boundaries.md):
- Its entry point is
StudioWorkspace, the signed-in Studio. Solid Memo's page (apps/web/src/App.tsx) shows it for the hashes under#/studio. - Around it is the page's one
AppShellfrom@solid-memo/ui: the session restore, the landing page and its login, the connected Pod, the masthead, the language, the theme and the footer. At the Studio's routes the shell names the Studio (STUDIO, anAppIdentityinui): its name, its tagline, its home (#/studio, the wordmark's link) and a link back to Solid Memo. - Its screens are in
apps/studio/src/ui/, containers over use cases and screens over props, as inui. They reuseui's components, such asInstancePickerContainer,BreadcrumbsandReaderText. - Its text is in its own message files,
apps/studio/src/i18n/, understudio.*, in each of Solid Memo's languages (i18n.md).ui's files keep only what the page says before the Studio's code is there: its name, its tagline, the link back to Solid Memo, the links that open it (studio.open), and that it's opening or couldn't be.
Both apps share one session and one login (authentication.md), and link to each other by hash. A login set off in the Studio, or a reload of one of its routes, comes back to the Studio: the session gateway puts back the hash the redirect drops. They share the use cases, their cached queries, and the browser's storage: the guest's pod, the language and theme chosen on this device, and the update journal. While a tab moves a guest's study into a pod, the others refuse to write to it (guest-mode.md).
The Studio's code is a chunk of its own, fetched the first time a
Studio route opens (loadStudio, in
apps/web/src/main.tsx). Its messages
come the same way: English in its chunk, and each other language in a
chunk of its own. The page adds them to its catalogs, and fetches the
one for its language before it shows the Studio (addCatalogs, in
apps/web/src/App.tsx). Meanwhile, under the masthead, the page says
the Studio is opening, and if the chunk or its messages can't be
fetched (offline, or after a new deploy), it says so.
The Studio is a workspace for authors, not a column to read
(shell/StudioShell.tsx).
It takes the whole window, and the window doesn't scroll:
- The header is one row: the sidebar's button, the masthead (the Studio's name, who is signed in, Log out), the open instance, the palette's button (below), the theme and language lists, and the link back to Solid Memo. Who is signed in is cut short (to no less than 12rem), and left out on a narrow screen (48rem or less), where the header is two rows: the button, the name and Log out, then the rest.
- The open instance, for a user with several, is a menu of the
others, each at its decks, and All instances
(
shell/InstanceSwitcher.tsx). - The sidebar, beside the main content while an instance is open
(
shell/StudioSidebar.tsx, a navigation named Studio), links to the instance's sections: Decks (Home), Groups, Drafts, Releases, Graph, Health, Library copies, Import and export, and Name and catalog.- The section shown is marked (
aria-current). Beside Health is the number of problems the instance's check found, once a screen has run it (the sidebar runs none). - Under the sections are the instance's decks, each at its cards, and its drafts, each at its overview with its version, the one shown marked, from the screens' own queries. A filter keeps those whose name holds its text, case and accents aside.
- Above the decks are those opened last on any of their screens (Recently opened, five at most, the last first), less those pinned. Each has a pin (Pin name, pressed while pinned). Pinned ones are listed at the sidebar's top (Pinned), in the order pinned.
- Its button, or Ctrl+B, hides it; the device remembers. Hidden with the focus in it, it gives the focus to its button, as the aside and the dock do.
- On a narrow screen it's a drawer over the page, opened by its button and closed by a link followed in it, a press outside it, Escape, or its own Close.
- The section shown is marked (
- The main content scrolls on its own: the trail, the tabs of the
deck or draft shown, then the screen. A table's header row stays in
sight. A table too wide scrolls the main content sideways, or, on a
narrow screen, its own box. What has the focus in a table is
scrolled clear of the parts that stick over it (its header row, its
first column and the bar of the rows selected)
(
shell/stickyClearance.ts). - The aside, right of the main content, is a complementary region named for what it shows (the table below: Home's deck, a library copy's upgrade), such as a course draft's outline. Its button at the header's end, its Hide, or Ctrl+\ hides it, and its splitter, on its left edge, sets its width; the device remembers both. The aside is at most the window less 35rem, the main content keeping 20rem beside the sidebar's room. On a screen 64rem wide or less it's a drawer at the page's right, as the sidebar's is. Escape in a dialog of it (a question of the outline's) closes only the dialog.
- A card beside its deck's cards: on a card's route, wider than 64rem, the main content is the deck's workbench and the aside, named Card, is the card inspector. Its Close, or Ctrl+, goes to the deck's cards alone, a Back stop. Its splitter is wider than the outline's. On a screen 64rem wide or less the card's route shows only the inspector.
- The dock, under the main content and the aside, is a region named Panel (below), hidden until its button in the status bar, or Ctrl+J, shows it. The device remembers that, and its height, which its splitter on its top edge sets. Hidden from its Hide or with Ctrl+J, it gives the focus to Panel.
- The status bar, at the foot, beside the site's footer, shows:
- the open instance;
- on a draft's screens, "Saving the draft…", "Draft saved", or "Last change not saved" when its write failed or the draft refused it (below);
- on a draft's screens, Ready to publish, a link to its Release tab, while no error blocks it, or Released once it is (below);
- the count of the problems of the deck or draft shown, a button that opens the dock at them: "5 problems, 3 block publishing", or "3 problems block publishing" when all do (or the check shown is for another policy);
- the dock's button (Panel), the density switch, and the keys (? Keys).
Tabs. A deck's screens have the deck's tabs, under the trail
(shell/ContextTabs.tsx,
a navigation named Deck): Cards (a card in the inspector among them),
Schedule, Health, About, and Export, which opens import and export with
the deck checked. A draft's screens have the draft's tabs (named
Draft):
- Overview (its chapters and steps under it);
- Cards (its questions under them);
- Check, which counts the problems the release check finds ("Check 3", heard as "Check 3 problems");
- Preview, Trial, Changes (only for a draft that follows a release) and Release.
The tab shown is marked aria-current="page", and on a page under a
tab (a card, a chapter, a step or a question) that tab is marked
aria-current="true"; both look bold and underlined. The card
inspector's tabs and the release check's policies are tabs of the same
kind. The sidebar and the tabs are links, each a Back stop; hiding or
sizing a pane, or opening a drawer, isn't.
What each screen has around it comes from its route alone
(shell/workspaceLayout.ts:
sectionOf, deckOf, contextTabsOf, asideOf):
| Screens | The sidebar marks | Tabs | The aside | The dock's problems |
|---|---|---|---|---|
| the instances | no sidebar: no instance is open | none | none | none |
| Home | Decks | none (its views are its own) | the deck at the table's cursor (Deck) | none |
| the library copies | Library copies | none | the upgrade of the copy at the table's cursor (Upgrade) | none |
| groups, drafts, releases, the instance's health, import and export, name and catalog | that section | none | none | none |
| the graph | Graph | none | the node selected (Node) | none |
| a deck's cards, schedule, health and about screen | the deck | the deck's | none | what the deck's health found |
| a card | the deck | the deck's, Cards marked as the card's | on a wide screen, the card (Card) | what the deck's health found |
| a draft's overview | the draft | the draft's | none | its release check's |
| a draft's other screens: its cards, check, preview, trial, changes and release | the draft | the draft's | a course's outline (Outline) | its release check's |
| a chapter, a step, a question | the draft | the draft's, Overview or Cards marked as theirs | a course's outline, the item edited marked | its release check's |
What is saved, and when. A draft's changes are saved as they're made (How edits are saved, below). Elsewhere a button, a menu's item or a drag changes the pod at once, asking first when something is lost. A form waits for its Save: a card's content, its wrong options, a deck's or the instance's about screen, a cell edited in place. A card's content or a cell not saved is asked about (Save, Discard or Stay) before anything leaves it. The layout is kept on the device, never in the pod.
Its look is calm: headings in the body's type, sections flat with a thin border, no tilt, stamp or pop, and Solid Memo's colors, theme and instance colors among them (theme.md).
Density. Rows and controls are about 28px high on every screen,
touch too: above the 24px minimum target of WCAG 2.2
(glossary). The status bar's
Comfortable makes them 44px; Compact goes back. The choice is kept on
the device
(shell/prefs.ts, key
solid-memo:studioLayout.v1; storage that's missing or refuses gives
compact), and changes no history entry. The same key keeps:
- whether the sidebar, the aside and the dock are shown;
- the aside's width (the outline's, and a card's beside its deck's cards) and the dock's height, each kept within its bounds;
- the columns each table hides.
Each instance's own state is under another key
(solid-memo:studioInstances.v1, by the instance's URL): the views
saved of Home and of the workbench (Saved views, below), the decks and
drafts pinned, and those opened last. Storage that refuses keeps them
for the page's life only.
Keys. The Studio has one listener of keys, in its shell
(shell/useShortcuts.ts).
Its keymap is data
(shell/shortcuts.ts), each
key of one scope: anywhere in the Studio, or in a table (useRowKeys,
shell/useRowKeys.ts). The
key help lists them by their scope:
| Keys | Where | What they do |
|---|---|---|
| Ctrl+K, Cmd+K | anywhere, a field too | open the palette (Quick open and commands, below), as Search or run… does |
? |
anywhere | list every key, as ? Keys does |
/ |
anywhere | go to the screen's search or filter: Home's, the workbench's, a draft's cards', the releases', the graph's |
| F6, Shift+F6 | anywhere, a field too | go to the next region, or the one before: the header, the sidebar, the main content, the aside, the dock (each while it's shown), the status bar |
g d, g g, g r, g p, g h, g l, g t |
with an instance open | go to its decks (Home), groups, drafts, releases, health, library copies, or import and export |
g i |
anywhere | go to the instances |
| Ctrl+B | with an instance open | show or hide the sidebar; on a narrow screen, open or close the drawer |
| Ctrl+\ | Home, the library copies, the graph, a course draft's screens but its overview; a card beside its deck's cards | show or hide the aside, or its drawer on a screen not wide; close the card |
| Ctrl+J | with an instance open | show or hide the dock |
| F8, Shift+F8 | a deck's or a draft's screens, a field too | go to the field of the next problem, or the one before (The dock, below) |
], [ |
a deck's or a draft's screens | go to the next tab, or the one before, round from either end |
1 … 9 |
a deck's or a draft's screens, but the trial | go to the tab of that number; in the trial they're the learner's, a grade or an option |
j, k, x, Enter |
a table: the card workbench (a card beside it too), Home, the library copies, a draft's cards, the releases | the next row (shown in the aside, where there is one), the one before, select it, open it (Card workbench, below) |
| Shift+J, Shift+K, Escape | a table whose rows are selected | select the row and the next, or the one before; clear the selection |
. |
a table whose rows are selected | open the first menu of the selection's bar, or go to its first button (below) |
| F2 | a table with cells to edit: Home, the card workbench | edit the cell the focus is in, or the row's first that can be edited (Editing in a cell, below) |
| Ctrl+Enter, Cmd+Enter | a text of several lines | submit its form, as Enter does a text of one line (ui's LangTextField); the key help leaves it out |
| Escape | a drawer, a dialog, the palette, a cell edited in place | close it, the cell unchanged |
Single-key shortcuts are a character alone: ?, /, the g
chords, [, ], 1 … 9, and a table's j, k, x, ., Shift+J
and Shift+K. The key help can turn them off (WCAG 2.1.4, for speech
input), and the device remembers it (characterKeys, Density, above).
The keys with Ctrl, the F keys, Enter, Escape and the buttons still
work. A character typed with AltGr, or with Option on a Mac ([ is
AltGr+8 on a Swedish keyboard), is the character. On Windows, Ctrl+
can't be told from a \ typed with AltGr (Ctrl+Alt there); the
aside's button or the palette shows or hides the aside instead.
A pair such as g d is pressed one key after the other, the second
within 1.5 seconds; a key held down counts once. A g chord, and a
tab's key, is a Back stop, except to the page already shown. A field
being typed in keeps its keys, Ctrl+K, F6, Shift+F6, F8 and Shift+F8
aside.
A key goes first to what has the focus (a menu's arrows, say), then to
the screen's own keys (useScreenKeys), then to the Studio's. While a
dialog is open, keys are the dialog's, wherever the focus is, but
Ctrl+K (Cmd+K) closes the dialog and opens the palette. While a menu is
open (Columns, say), every key is the menu's.
A field or button with a key, the one / goes to among them, says so to
screen readers (aria-keyshortcuts). The key help is a modal dialog,
the browser's own
(shell/Dialog.tsx); Escape
or Close closes it, and the focus goes back where it was. A saved view
renamed keeps the focus; one removed gives it to + Save view.
A splitter
(shell/Splitter.tsx) is a
separator, as screen readers know one, that takes the focus. Its value
is the pane's size in pixels, between bounds that scale with the text
size over the browser's default 16px (theme.md).
Left and Right (Up and Down, for the dock's) move it 16 pixels, Home
and End to either end; the pointer drags it
(shell/splitter.ts).
Filters and the selection. A table's filters (the workbench's,
Home's, a draft's cards') are one row over it, wrapping only on a
narrow screen, each label beside its field, and the counts at the
row's end. Rows selected in a table (the workbench, Home, the library
copies, a draft's cards) bring up a bar under it
(shell/BulkBar.tsx), a
group named for them ("Selected cards") at the foot of the main
content. It stays in sight as the table scrolls, and it's there only
while some are selected. It shows what can be done with them all at
once (a few buttons, and menus of the rest), and Clear selection.
A status line, there throughout, says how many are selected.
Questions and forms in a dialog. Whatever loses something asks
first, in the Studio's own dialog, not the browser's
(shell/ConfirmDialog.tsx,
useConfirm): a heading, the question, the action (it has the focus)
and Cancel; Escape or Close does nothing either. Deleting cards, decks,
a draft or a part of it, forgetting progress, restarting a course, and
removing data from the pod on the health screen ask this way.
The components the Studio shares with Solid Memo (removing a card,
deleting a wrong option, deleting an instance) ask through ui's
AskFirst (askFirst.ts): the
Studio gives it useConfirm, and Solid Memo the browser's confirm.
Deleting a group, or a deck from the groups' tree, still asks in the
browser's dialog: the focus afterward comes from the button the
question leaves it on. A bulk edit that needs more input (a language,
a day, a deck, a group) asks in a dialog too.
Editing in a cell. Some cells are edited where they are
(shell/EditableCell.tsx):
on Home a deck's direction and pace, on the workbench a card's front
and back.
- Such a cell is a button named for what it does ("New per day of Kanji N5: 20 (instance)"), outlined when pointed at.
- A click, Enter on it or F2 on its row opens a field in the cell; a card's front, whose cell is its link, opens only by F2.
- Enter, or Save, saves it as the bulk edits do, and the cell has the focus again. Escape, or Cancel, leaves it unchanged.
- A value refused stays in the field, marked invalid, and the screen says why.
- A value changed and not saved stays as the focus goes elsewhere, and is asked about before it's left, as a card's edits are; one unchanged closes as the focus leaves it. Saved from that question, it's saved onto the row as it is then (a deck's pace saved from another cell since is kept); discarded, the edit is gone.
- Enter and Escape that an input method takes, to end or drop a syllable it composes (Korean, say), are the input method's.
- A side's field is marked with the side's language (an Arabic side reads right to left, in its font).
- The names of the button and the field are put together from the
page's text with
aria-labelledby, so the deck's or card's name and the cell's text keep their languages ("Back of كتاب: Book"). - Nothing is saved unasked.
Saved views. Beside a table's own views (Home's and the
workbench's), + Save view saves the table's query under a name,
asked in a dialog
(shell/SavedViews.tsx).
Each is a tab, pressed while the table is in its query, which a press
puts it in (no Back stop). Its menu renames it or removes it, once the
user confirms. A view saved or renamed under a name taken replaces that
view. The device keeps them for each instance, the workbench's for
every deck. A view keeps the query as its URL has it (q=kanji&show=due;
shell/views.ts), the
workbench's without its page.
The dock
(shell/BottomDock.tsx)
has two tabs, a tablist (Left and Right go from one to the other):
-
Problems, counted in its tab and in the status bar, on a deck's or a draft's screens (
WorkspaceProblems.tsx).- A draft's are its release check's, the shapes
left out: for a pod, or, on the check, for the policy it shows.
The query, shared with the overview and the check screen, runs once
a version of the draft. They're listed as the check lists them
(
ProblemList): by severity, then by what they're in, each a link to its field. - A deck's are what its health found. The check of its data is one line, a link to the deck's health. Each unstated language, group of cards that say the same, and Markdown finding links to its field. The dock, shown at a deck's problems, checks it if neither a Home badge nor its health screen has.
- Off a deck's or a draft's screens the tab says where problems are listed.
- A draft's are its release check's, the shapes
left out: for a pod, or, on the check, for the policy it shows.
The query, shared with the overview and the check screen, runs once
a version of the draft. They're listed as the check lists them
(
-
Activity lists what the screens said they did since the page was opened, the last first, each after its time, 50 at most:
- the draft saved (or its last change not), and a draft made or deleted;
- Home's bulk changes and the workbench's bulk edits;
- undo, moves and copies, and a card saved in the inspector;
- an export or an import, and a release published or saved as a file.
A line said twice in a row is counted on the last, at the time it was last said ("Draft saved ×5",
addActivity). A screen tells its status line throughActivityLog(useActivity, inshell/activity.ts), but not what it shows as it opens.
F8 goes to the field of the next problem in the Problems list, Shift+F8
to the one before, as a link does, round from either end. It counts
from the problem gone to last while its page is shown, so two problems
in one field are each gone to, else from the page shown
(stepProblem in
shell/workspaceLayout.ts).
Its stylesheet is studio.css,
in the Studio's chunk, over the site's style.css, which holds none of
the Studio's rules. Every rule holds under
<html data-surface="studio">, which the shell sets while it's shown and
removes after;
the density is data-density beside it. The screens' rules come first,
their scope in :where(), so the workspace's rules after them win. A
test (studio.test.ts) checks
the rules through the cascade, and that Solid Memo's screens are left
as they were.
The palette
(shell/CommandPalette.tsx,
a dialog named Quick open) opens with Ctrl+K (Cmd+K on a Mac), or with
Search or run… in the header. It finds, as the user types:
- the commands of the screen shown, and the Studio's own (show the keys, show or hide the sidebar, the aside or the dock);
- the tabs of the deck or draft shown, each with its number;
- the open instance's sections, each with its
gchord (Graph and Name and catalog have none), and All instances; - the user's instances (at their decks), the open instance's decks (at their cards) and drafts (those that can be read, at their overview);
- the cards of the deck shown, in the inspector, once something is typed.
It lists what the workspace's cache held when it opened, reading
nothing from the pod. The entries and their rank come from
shell/commandIndex.ts:
- An entry matches when its name holds the letters typed in their order, case and accents aside ("kn5" finds Kanji N5).
- A letter at the start of a word, or right after the one before, counts for more, and a shorter name a little. The best 50 come first, each group under its heading where its best is.
- With nothing typed, the decks and drafts pinned come first (Pinned),
then the entries used last (Recently used, eight at most, those
pinned aside, the last first), then the rest but the cards. The
device keeps those used last by group and target, or command
(
shell/prefs.ts, keysolid-memo:studioRecent.v1). - Each entry shows the keys that do the same, if any.
The field is a combobox over the list: the focus stays in the field,
and the entry picked is its aria-activedescendant. Down and Up pick
the next entry or the one before, round from either end, and Enter, or
a click, chooses it. Escape, or Ctrl+K (Cmd+K) again, closes the
palette, and the focus goes back where it was. An entry that goes
somewhere is a Back stop, except to the page already shown; a command
runs once the palette has closed.
A screen adds the commands it can run at the moment
(useScreenCommands, in
shell/useScreenCommands.ts);
beside a card, Close the card stands for the aside's. A screen's are:
| Screen | Command | What it does |
|---|---|---|
| Drafts | New draft | goes to the new draft's form, while a draft can be made |
| Health, an instance's or a deck's | Check again | checks again, while it isn't checking |
| Import and export | Export n decks | exports the decks checked, as the form's button does, while some are and none is being exported |
The Studio has its own hash router
(src/ui/router.ts), on the same
core as Solid Memo's (routerCore.ts in ui,
routing.md). Its hashes all start with #/studio
(STUDIO_PATH, isStudioHash in ui's router); Solid Memo's are the
others.
| Hash | Screen |
|---|---|
#/studio/instances |
the instance picker. Its link for a new instance goes to Solid Memo's storage picker. |
#/studio?instance=…[&q=…&sort=…&order=desc&show=…&deck=…] |
Home (below). q filters it, sort names the column it's sorted by, and order=desc sorts it the other way. show is the view: due, courses, copies or attention; every deck without it. deck is the deck at the cursor. |
#/studio/groups?instance=… |
Groups (below). |
#/studio/cards?deck=…[&q&field&lang&state&has&sort&order&page&size] |
the card workbench (below). The deck names the instance: its catalog's folder. |
#/studio/card?deck=…&card=…[&tab=distractors|schedule|history&field=…][&q&in&lang&state&has&sort&order&page&size] |
the card inspector (below): its content (the default tab, left out of the URL), or with tab its wrong options, review state or answers. field opens it at one field: a text of the content (front, backNote…), or a wrong option by its id. The rest is the query of the workbench beside the card, but in for its field; without it, the deck's cards as listed. |
#/studio/schedule?deck=… |
a deck's schedule (below). |
#/studio/about?deck=… |
a deck's about screen (below). |
#/studio/instance?instance=… |
the instance's name and its catalog (below). |
#/studio/health?instance=…[&deck=…] |
the instance's health, or one deck's (below). |
#/studio/library?instance=… |
the library copies (below). |
#/studio/transfer?instance=…[&deck=…&deck=…] |
import and export, each deck checked (below). |
#/studio/drafts?instance=… |
the instance's drafts (below). |
#/studio/draft?draft=…[&field=…] |
a draft's overview (below). draft is the draft's release document, which names the instance. |
#/studio/chapter?draft=…&chapter=…[&field=…] |
a chapter of a course draft, by its id (below). |
#/studio/step?draft=…&step=…[&field=…] |
a step of a course draft, by its id (below). |
#/studio/question?draft=…&card=…[&field=…] |
a card of a draft, by its id: a course's question, or a deck's card (below). |
#/studio/draft-cards?draft=…[&filter=…&lang=…&sort=…&order=desc&page=…] |
a draft's cards (below): filter is unasked, askedTwice, fewDistractors or retired, lang a language, sort the column (front, back, place, distractors or id; by id without it), and order=desc the other way. |
#/studio/check?draft=…[&policy=library] |
a draft's release check (below), for a pod, or with policy=library for the Solid Memo library. |
#/studio/preview?draft=… |
a draft's listing preview (below). |
#/studio/trial?draft=…[&chapter=…&part=review] |
a draft's trial (below): a course's page, with chapter (an id) that chapter's steps, and with part=review its final review; a deck's study. |
#/studio/diff?draft=… |
a draft's release diff (below). |
#/studio/release?draft=…[&field=…] |
the release's metadata and provenance (below), then its publishing (below). |
#/studio/releases?instance=… |
the releases the instance published (below). |
#/studio/graph?instance=…[&node=…] |
the graph (below). node is the node selected, by its IRI. |
#/studio/ and its query count as #/studio. Anything else under
#/studio, #/studio among them, is the default route: the only
instance's Home, or else the picker. An unknown instance falls back to
the picker, an unknown deck to its instance's Home, an unknown card
(one removed, say) to its deck's cards, and a chapter, step or card a
draft doesn't have to the draft's overview. A node the graph doesn't
have counts as none. Like Solid Memo's fallbacks, these replace the
history entry.
field opens a draft's screen at one of its fields, where the release
check links (a field the screen doesn't have is left out):
- the overview's
title,descriptionoroutline; - a chapter's
title,description,stepsorreview; - a step's
theoryorquestions; - a question's text (
front,backNote…), its wrong options (distractors) or one of them (distractor:<id>); - the release screen's
versionNotes,languages,themes,license,publisher,authors,making,sourcesorchecks.
These replace the history entry: Home's query and deck beside it, the workbench's query, the inspector's tab, the decks checked to export, a draft's cards' view, the graph's node selected, and the card beside the deck's cards. Opening a card from the list, or a deck from Home or a card from a draft's cards with Enter, is a Back stop.
Solid Memo links to Home as #/studio?instance=… ("Open in Studio",
in its instance bar, each deck's actions menu and the deck page;
routing.md). A deck's link opens its instance's Home too.
The trail starts Instances › Decks, then goes on by screen (crumbsOf,
in shell/titles.ts, from
the route and the deck, card or draft it names):
| Screen | The trail goes on |
|---|---|
| Groups | › Groups |
| the workbench | › Cards of deck |
| the inspector | › Cards of deck › card |
| a deck's schedule | › Cards of deck › Schedule |
| a deck's about screen | › About deck |
| the instance's screen | › Name and catalog |
| the instance's health | › Health |
| a deck's health | › Health › deck |
| the library copies | › Library copies |
| import and export | › Import and export |
| the drafts | › Drafts |
| the releases | › Releases |
| the graph | › Graph |
| a draft's overview | › Drafts › draft |
| a chapter | › Drafts › draft › chapter |
| a step | › Drafts › draft › chapter › Step 2 |
| a draft's cards | › Drafts › draft › Cards |
| a question | › Drafts › draft › Cards › card |
| a draft's check | › Drafts › draft › Release check |
| a draft's preview | › Drafts › draft › Listing preview |
| a draft's trial | › Drafts › draft › Trial |
| a draft's comparison | › Drafts › draft › Changes |
| a draft's release metadata | › Drafts › draft › Release |
The document title is the trail's last step and "Solid Memo Studio".
After a move, the screen's heading takes the focus (useScreenFocus).
The link back to Solid Memo, in the header, the landing page and the
Pod connection screen (headerLink in STUDIO), goes to the open
instance's decks when there is one. The Studio's links into Solid Memo
(a deck's page, its preferences, a new instance) are Solid Memo's own
hashes.
The Studio's old address, /studio/, is a small page
(apps/web/public/studio/index.html)
that sends /studio/#/x on to ../#/studio/x. Its script runs under a
Content Security Policy that allows only it, by its hash. A test
(studioRedirect.test.ts)
checks both.
Home (DeckTableContainer)
is a table with a row per deck. Its columns are:
- the deck's name, a link to its about screen,
and its badges:
- Library for a copy of a library deck (it has
prov:wasDerivedFrom); - From <host> for a copy of a release added from a link, which no index lists (deck-library.md);
- Course for a copy of a course, wherever it came from;
- Invalid data when the instance's check finds a problem in its entry or its documents (validation.md);
- Unreadable when its cards couldn't be read;
- Library for a copy of a library deck (it has
- the groups it's in, outermost first, or "Top level";
- its direction, and its pace: new cards and reviews per day. A limit the deck doesn't set is the instance's, marked "(instance)";
- how many cards are due today and how many are new, as Solid Memo counts them (from the study digest when it's fresh);
- when it last changed (
dcterms:modified, else when it was made); - its cards, retired ones aside, a link to its cards in the workbench.
Beside a deck's name are its problems (health), "3
problems", a link to the deck's health, and for a copy with a newer
release out, "Release 2 out", a link to the
library copies. Both are looked up only once the row
is on the screen (IntersectionObserver): one read of the library's
index for every copy (listLibraryUpdates), and for a copy from a link
its creator's catalog. A deck with no problem gets no badge, nor does a
copy that's up to date or whose newer releases can't be known.
With no decks yet, Home says to make or import them in Solid Memo.
The table starts in the order the user arranged the decks. A column's
name sorts by it, then the other way, then back to that order; the
sorted column says so (aria-sort). A figure not known yet sorts last,
either way. The filter keeps the decks whose name, in any language, or
whose groups' names hold its text, case and accents aside. The domain
does both (deckTable.ts).
Views. Tabs above the table (a group named Views, the one shown pressed) each keep the decks of one kind, and count them:
- All;
- Due today: cards due today, and those still counted, a loader meanwhile;
- Courses: copies of a course;
- Library copies: copies of a release, from the library or a link;
- Needs attention: invalid data, or cards that couldn't be read.
Each is a preset of Home's query
(shell/views.ts): it sets
the view (show), and keeps the filter's text and the sort. No view
keeps decks by their problems or a newer release. After them come the
views the user saved, and + Save view (Layout).
Editing a deck in its row. A deck that may be changed has its direction, new cards per day and reviews per day edited in their cells (Editing in a cell, under Layout): a choice of directions, or a whole number. One left empty follows the instance's pace again. Each is saved as the bulk edits save it, for that deck alone ("Set the pace of 1 deck.").
The table is a DataTable (Card workbench, below): j and k
move its cursor, x selects a deck, and Enter opens the deck's
about screen. Its Columns menu shows or hides each column but the
deck's name, as the device keeps. Every column shows until hidden. The
workbench and a draft's cards have the menu too, each table its own.
The deck beside the table. The deck at the table's cursor is in the
aside, named Deck
(DeckPeekContainer):
its groups, its cards, today's due and new cards, when it last changed,
its health, and links to its tabs, from the table's own queries. A
deck the instance doesn't
have shows none; before the cursor has moved, the aside says how to
show one.
Decks are selected by their checkbox, or all those shown at once. The selection's bar offers Edit (a menu of the first three below, each asking how in a dialog), Export and Delete. A selected deck the filter hides stays selected, but is left alone until it's shown again. The selected decks shown can be:
- moved into a group, or to the top level, at its end, in their order in the table (not when a newer version arranged the groups);
- given a pace: a limit left empty follows the instance's preferences again;
- given a direction;
- exported: Export opens import and export with them checked;
- deleted, with their cards, once the user confirms a question that names each deck (a ConfirmDialog).
Each is one write of catalog.ttl, made in turn with the other writes
of the catalog, and the table is read afresh after it
(data-model.md). A deck set aside can't be
selected, nor can any deck until the data check is done
(below). While the arrangement is set aside, no deck
is moved into a group.
The workbench
(CardWorkbenchContainer)
lists a deck's cards in a table, a row each. The columns are the front
(a link to the card), the back, when it's due, its interval and its
ease, its lapses (below), when it was added, and
its id. A retired card says so. A card studied both ways shows the
direction that needs the most work: the earliest due day, the shortest
interval and the lowest ease. A copy of a course says its cards are
the course's questions.
The URL holds the query, which the domain applies (cardQuery.ts):
q: text to find, case and accents aside. A card in Markdown is searched as its plain text (markdown.md).field: where to look: the front, the back, the notes, the labels or the wrong options. Without it, all of them and the pictures' descriptions.lang: only text in this language (svfindssv-fitoo), orunstatedfor a side whose language isn't stated; withq, the text is looked for in that language only. The filter offers the cards' languages, and the URL's own.state:live(in use) orretired, or, for a card in use, where its schedule is.newhas a direction never reviewed, andlearningwas answered wrong last time.younghas an interval under 21 days andmatureone of 21 or more (MATURE_INTERVAL_DAYS).dueis due by today's study day, andleechhas 4 lapses or more (LEECH_LAPSES). A card studied both ways is in a state when either direction is.has: apicture,distractors(wrong options),markdownornotes.sortandorder=desc: byid,created,front,back,due,interval,easeorlapses, a column's name cycling as on Home. A side sorts by the text the table shows, as plain text: the deck's card language first when it has one (i18n.md), in the order of the UI's language. Case and accents count only where that language makes a letter of one (in Swedish,äafterz). Numbers sort by value. A card without the value (one never studied) comes last, either way.pageandsize: 10, 50 (the default) or 200 cards a page, with the same pager as Solid Memo's Browser. A change of the search or a filter goes back to the first page.
Views. Tabs above the filters (a group named Views) put the cards in one state: All (none), Due, New and Leeches, the one the query is in pressed, or none when its state is another. A view keeps the rest of the query, from the first page. After them come the views the user saved (Layout).
Editing a card in its row. A plain text card (not in Markdown) has
its front and back edited in their cells (Editing in a cell, under
Layout), a side only while its text is in one stated language. It's a
bulk edit of that card alone (setText in
cardBulk.ts), with Undo
(below). A side left empty is refused; one unchanged
writes nothing. Other cards are edited in the
inspector.
The table is the Studio's DataTable
(shell/DataTable.tsx).
Its first column (the checkboxes and the fronts) stays in sight as it
scrolls. Cards are selected by their checkbox, or a page's at once; a
click with Shift held selects, or not, every card from the one checked
last. The selection stays while the view changes. The keys are listed
under the selection's bar:
- j and k move the cursor between the rows (the focus goes to the row's front);
- x selects the row;
- Shift+J and Shift+K select on from where the selection was last made;
- Escape clears the selection;
- Enter opens the card;
- F2 edits the row's front, or the cell the focus is in;
.opens the bar's first menu.
The cursor's row is marked, as is each row selected. j and k
work wherever the focus is, from the cursor's row, or the first.
x, Enter and the Shift keys act on the row the focus is in, or
the cursor's with the focus on the page's body; Enter on a row's link
or button is the link's or the button's. The search and the filters
keep the keys while focused. The selection, the sort, the cursor and
the columns shown are worked out in
shell/tableModel.ts.
Home, the library copies, a draft's cards, the releases and a card's answers are DataTables too, each with the keys and the marks it has use for. The workbench's Columns menu shows or hides each column but the first (Home). A row that can't be selected now (a deck set aside, a copy with nothing to upgrade) has its checkbox disabled, or none, and a range or a page's checkbox passes over it.
The lapses column counts each card's lapses, in either direction, in the
answer log: reviews graded below 3, but
not a first answer, an answer the same day after another, or a course
answer (statistics.md). The
row of filters ends with how many cards the query keeps and the month
of the deck's first answer in the log ("since March 2025"). The
workbench reads the whole log once (loadAnswerLog). Meanwhile the
cards show without their lapses, and a query that needs them
(state=leech, sort=lapses) waits. When it
can't be read, the line says so, and such a query shows why.
A card opens in the card inspector, on a screen wider than 64rem beside the workbench, which keeps its query, selection and Undo. The workbench reads the cards and review states with Solid Memo's queries, and the instance's preferences for today's study day. A test keeps a query over 5,000 cards linear: timed on the same machine, eight times the cards take less than four times as long as an eighth of them eight times over.
The selected cards the query keeps, on any page, are edited from the
selection's bar
(CardBulkActions),
which groups the edits:
- Edit: Write in Markdown, Write as plain text, State language, Find and replace;
- Schedule: Set due date, Forget progress;
- Move or copy: Move to deck…, Copy to deck…;
- then Retire, Restore and Delete.
A selected card the query hides stays selected, but is left alone. While the deck may not be changed, the bar has only Clear selection. The edits are:
- Retire and Restore. A retired card keeps its review state.
- Write in Markdown and Write as plain text, which is written
as
sm:plainText(markdown.md). - State language of the sides whose language isn't stated yet, both or one of them, the text unchanged.
- Find and replace
(
FindReplaceDialog): text as written (Markdown as its source), never a pattern. It looks in the fields checked (the front, the back, the notes, the labels, the wrong options and their notes), in one language or all, matching case and whole words when asked. A preview lists every text changed, before and after, and every card left as it is, and why. Nothing's written until the user confirms. - Delete, with every SM-2 and FSRS-6 state of each card, read or not, once the user confirms. Another scheduler's state stays (data-model.md). Undone, the cards' SM-2 states come back, and FSRS-6 estimates its own from them at the next review.
- Set due date and Forget progress, of the cards' review states in every direction (below), a card not studied yet staying new. These have no Undo, and a forget asks first.
- Move to deck… and Copy to deck…, to another deck of the instance (below), with no Undo.
The domain plans each edit
(cardBulk.ts, planCardEdit). A
card it would change must pass the card editor's validation
(validateCardContent), else it's left as it is, as is a card the
edit doesn't change, and one whose side or wrong option a replace
would leave empty. A note or a label left empty is removed, as in the
card editor. Text whose language isn't stated can't be changed before
its language is.
The use case editCards plans the edit again on the cards as they are
now, and writes it in one If-Match PUT of the cards document
(data-model.md). Then it writes the
review states of deleted cards in one write of the reviews document,
then the deck's schedule in the digest. When the cards document
changed meanwhile, or was made since it was read as absent, it reads
it and plans again, three times in all. Once that plan isn't the one
the user previewed, it stops with "changed elsewhere", writing nothing. The
deck's catalog entry is untouched. The end-to-end tests hold this
against real servers, a change made by another app during the edit
included
(cardEdits.integration.test.ts).
After an edit, the status line says what it did, and Undo undoes
it: undoCardEdit writes the plan's inverse the same way, the deleted
cards' review states included, only while the cards are as the edit
left them. The plan is kept in the page only, so Undo goes with the
next edit, another deck, or leaving the page.
No edit takes a backup first: Solid Memo keeps none (migrations.md). Undo can't bring back triples another app put on a deleted card or its wrong options, or a second state of a card that was never read.
The card inspector
(CardInspectorContainer)
shows one card of a deck, named in its heading, with a link to its page
in Solid Memo. On a screen wider than 64rem it's in the aside, beside
the deck's cards (Layout); below that it's the main content
alone. The card's row is marked (aria-current) and is the cursor's.
A card opened from the list takes the focus to its heading; closed,
the focus goes back to its row.
Previous and Next at its heading's end, or k and j, show the card before it in the list, or the one after, each at its page of the list, in the same tab, with no Back stop. At either end, or for a card the query doesn't keep, there is none. A card opened from elsewhere (the palette, a deck's schedule, a link) puts the list at its page, replacing the history entry.
Its four tabs are links, the one shown marked (aria-current):
- Content: Solid Memo's card editor (
CardContainerinui): the card as it's studied (small, its faces side by side), its sides, pictures, notes, labels and Markdown, Save, and Remove, which goes back to the deck's cards. Each save, and each change of the wrong options, is told to the dock's activity. - Wrong options, with their number
(
DistractorFieldsinui): each with its text, its note on why it's wrong, its id and how often a learner chose it (from the card's answers), to edit, retire, restore or delete. "Add a wrong option" writes a new one, in the back's language to start with. Each change is saved as it's made, as one write of the card (updateCard) naming each option withsm:distractorandschema:suggestedAnsweralike, and the status line says so. An option being written stays open until it's saved. - Schedule: the card's review state in each direction the deck
studies, and in one it no longer does while a state is kept there
(
CardScheduleContainer).- SM-2's state: when the card is due, its interval, ease and right answers in a row, and when it was first and last reviewed.
- An FSRS-6 part follows when the instance's preferences name FSRS-6,
or the card has an FSRS-6 state that way. It shows its due day, its
stability (in days), its difficulty (1 to 10, to a tenth) and the
chance of recalling it now (
recallNowinfsrs.ts, at whole study days since the last review). A line says which of the two sets the due days. - While FSRS-6 sets them but the state is missing or stale (srs.md), it says the state is estimated from SM-2's at the next review, SM-2's due day counting until then.
- An FSRS-6 state with no SM-2 state beside it isn't shown. A direction never studied says the card is new.
- Each state can be given a due day, starting at the one that sets it, or forgotten, once the user confirms (below).
- History: the card's answers in its deck, newest first
(
CardHistoryScreen).- Each shows when it was given, which way, its grade, how (recalled, or chosen among options in a course) and, for a wrong choice, the wrong option chosen, by its id once it's no longer on the card.
- A column's name sorts the answers by it (but the wrong option's), then the other way, then newest first again. Beside the deck's cards, j and k stay the cards' table's.
- A line counts the answers and the card's lapses, as the cards' table counts them (statistics.md). An answer below grade 3 is marked forgotten, but only a scored review is a lapse.
- The use case
cardAnswersreads the deck's answers from the answer log (loadAnswerLog), so a card moved in from another deck starts its history again (below). Answers given before a library upgrade count, named by the card's id.
The domain makes each change (distractors.ts):
addDistractorcreates one only once it has text, undernextDistractorId:<card>-d<n>,none more than the highest such id the card or its release has used. An id the release published is never used again; one deleted before it was published may be.editDistractorchanges its text and note, keeping its id and whether it's retired.retireDistractorandrestoreDistractormark it retired (owl:deprecated true, kept but never offered), or in use again.deleteDistractorremoves it, once the user confirms, but only one the release the deck came from never published; one it did (a learner's history may name it) is refused, to be retired instead. While the release is read, every one counts as published.distractorIssueslists an option in use that isn't in exactly the back's languages, and fewer than two in use, as warnings in a pod's deck, never refusals.
In a copy of a library deck or a course, a card still as the release has it warns that an edit detaches it. A newer release then no longer updates it, but may retire it. A card already changed says so (migrations.md).
Solid Memo's own card page saves the wrong options with the card. Left as they were, they follow the card as it's read afresh (a change in the inspector meanwhile included), and its save doesn't state them, so the card keeps its own.
While the deck is set aside, or the data check is under way, every tab is shown but nothing in it can be changed (below).
Edits not saved. The content tab's card editor says while it holds
changes not saved. Leaving them by any link, key, Close, another tab,
Back and Forward, or a hash typed in, first asks, in a dialog
(shell/ConfirmDialog.tsx).
The answers are
(shell/unsavedEdits.tsx):
- Save saves them, then goes on, unless the save failed or was refused, which the editor shows;
- Discard goes on without them, and a cell that stays on the page shows them no longer;
- Stay, Escape or Close goes nowhere.
A table's cell edited and not saved is asked about the same way, beside a card's edits; Save saves them all, and goes on only once each is saved.
Back or Forward is undone before the Studio's router hears of it, by
as many entries the other way (the router numbers them,
historyPosition in
routerCore.ts). It's done again
once the question is answered so, the entries after it kept for
Forward. A hash typed in is undone as Back is. A card's edits are
never saved unasked. Leaving the app (a
reload, closing the tab) is asked by the browser (beforeunload). The
edits are kept as the window crosses 64rem (keep, in CardScreen),
until another card or tab is shown.
Opened at a field (field, as the health screen links to
one), the inspector puts the focus there: on the text, or on the wrong
option's Edit button (data-arrival, as useScreenFocus in ui
takes it). Another tab opens at none. It reads the cards and review
states with the workbench's and Solid Memo's queries.
The domain makes both changes (reviewStateEdits.ts):
resetStatesforgets cards, so the scheduler sees them as new: one direction in the inspector, every direction of the selected cards in the workbench. Every SM-2 and FSRS-6 state that way goes, read or not; another scheduler's stays.rescheduleStatesets a state's due day, keeping its interval, ease and repetitions. It drops the state's snapshot for resetting the study day (previous, srs.md), so that reset can't bring back a state from before the new due day. A day that's no date is refused. The card's FSRS-6 state, when it has one, is set due that day too, its snapshot dropped, so the day holds whichever scheduler sets the due days, now or after a switch.
The states are those Solid Memo reads
(data-model.md), joined to their card
by sm:reviewOf and sm:reviewDirection, the fragment rule only where
a state leaves them out, so a state another app named its own way is
edited in place. Only sm:sm2 and sm:fsrs6 states are touched.
Neither touches the answer log. The use cases resetCards and
rescheduleCards write the change in one write of the reviews
document (none when no card has a state), then the deck's schedule in
the digest. The screens then read the review states afresh and drop
the deck's study queue. The end-to-end
tests hold both against real servers
(reviewStateEdits.integration.test.ts).
Move to deck… and Copy to deck… open a form
(TransferCardsDialog)
that offers the instance's other decks and Keep their progress,
checked to start with, and says what becomes of the cards' history. A
moved card leaves the selection.
A deck whose cards or reviews document is one of this deck's isn't offered, as another app may point two decks at one document. Its cards are this deck's, so a move would remove them from both. The use case refuses such a transfer before reading anything, and the domain plans none. Two addresses name one document when a server may read them as one: written another way a request or a server resolves, such as a character encoded that needs no encoding, or with a user name and password in it.
The domain plans the transfer
(cardTransfer.ts,
planCardTransfer):
- A card keeps its id when the target's cards document has no subject
of it or its wrong options' ids. Else it gets the first free id of
<id>-2,<id>-3and so on, its wrong options renumbered after it (<new id>-d1,-d2…). The status line says how many cards got a new id. - A card keeps its content, its creation time and whether it's retired, and becomes the target deck's.
- Triples another app put on the card or its wrong options aren't copied, and a move removes them from the source with the card (data-model.md).
- With Keep their progress, the card's review states go with it,
under its id in the target, their snapshot for resetting the study
day included; without, the card starts as new there.
- A state is found by the card its
sm:reviewOfnames, whatever its subject is called. - It's written in the target naming the card there (
sm:reviewOfinto the target's cards document), under the subject the fragment rule names, or a new#review-<uuid>when another's is there. - Its FSRS-6 state goes with its SM-2 state the same way; where the card has none, a state the target held at that prompt goes. One with no SM-2 state beside it isn't carried, only removed.
- A state is found by the card its
- A card written in the target names its wrong options with
sm:distractorandschema:suggestedAnsweralike, each typedsm:Distractorandschema:Answer. - A move then removes the cards from the source, with every SM-2 and FSRS-6 state they have, read or not. A copy leaves the source as it is. Another scheduler's states are never moved, copied or removed; they stay in the source, naming the card there.
- Past answers keep naming the source, so a moved card's history starts again in the target (the answer log).
The use case transferCards writes the target first: its cards
document in one write, made only if it's still as read (If-Match),
then its reviews document. For a move, it then writes the source's
reviews document, and last its cards document, If-Match too. A
document changed meanwhile, or made since it was read as absent, has
the whole transfer planned again, three times in all, then it stops
with "changed elsewhere". Then the decks' schedules in the digest are
brought up to date, and both decks and the instance's counts read
afresh.
A move stopped half way (the target written, the source not, or only its review states) leaves the cards in both decks, never in neither, and making it again finishes it. The plan names the same ids each time, and a card the target holds already under its id, saying the same, isn't written again (its state there is kept). Two cards of the source saying the same are never both found at one id. The source's states go before its cards. The end-to-end tests hold this against real servers, and include a move stopped before the source's cards were written (cardTransfer.integration.test.ts).
A deck's schedule
(DeckInsightContainer)
shows its study to come. The domain computes it
(scheduleInsight.ts,
cardHistory.ts), and the use
case deckInsight reads what it needs, writing nothing.
- Tiles: the reviews today and in the next 7 days, the cards studied (each way), and the leeches.
- Reviews to come, the next 30 days (
forecastOf).- A day counts what falls due on it, and today what is due by today.
- The reviews a day are capped by the deck's pace, else the
instance's (
deckPreferences), today's less what was reviewed today, the rest waiting for the next day. - The due days come from the deck's schedule in the
digest (
sm:dueOnDay) while it's fresh (freshSchedule: the same versions, direction and day boundary); else from the review states, and the digest is brought up to date.
- Intervals and eases, as bars, with tables: how many cards, each way, have each interval (1 day, 2–3, 4–7, up to a year and more) and each ease (from 1.3, in steps of 0.2). Only cards in use count, in the directions the deck studies. The intervals are those of the scheduler that sets the due days. The ease is SM-2's, kept on every review, as the chart's note says.
- Lapses and leeches. A lapse is a review graded below 3, in the
deck's answers in the answer log
(
lapseIndex, byisLapse).- A card's first answer isn't a lapse, nor is an answer the same day after another to the same prompt, nor a course answer (statistics.md).
- The screen names the month of the deck's first answer in the log.
- A leech is a card in use with 4 lapses or more (
LEECH_LAPSES,leechesOf), the most forgotten first. Each links to its card's history, and a link shows them all in the workbench (state=leech&sort=lapses&order=desc). - A test keeps
lapseIndexlinear over a year of answers: timed on the same machine, eight times the answers take less than four times as long as an eighth of them eight times over.
The charts are the statistics' own (BarChart and StatTile in ui).
A bar pointed at, tapped or reached with the arrow keys says what it
holds, and each chart has its figures as a table.
The about screen
(DeckAboutContainer)
shows a deck's metadata and how it's studied, with a link to its page in
Solid Memo. Each part has its own form and status line:
- Name: the deck's name in every language it has, renamed as in
Solid Memo's deck preferences (
renameDeck). - About this deck: its description, topics and keywords, with
Solid Memo's own form (
DeckAboutSectioninui,describeDeck). - Authors and license: an author a line,
NameorName <email>, and a license chosen from those offered (KNOWN_LICENSESin license.ts, each named aslicenseLabelnames it), a license another app wrote among them. The domain tidies and checks them (deckProvenance.ts): two authors that would share one agent node are refused, the form staying. An email with,,;,?,#,%or&in it isn't read as one: that author stays a name alone. Nothing here says anyone reviewed the deck.setDeckProvenancewrites again from a fresh read on a 412, three times in all. - Study: its direction and its pace, a limit left empty following
the instance's (
setDeckPace, thensetDeckDirectionwhen that changed). - Languages: its card sides whose language isn't stated, set as in
Solid Memo (
DeckLanguagesSectioninui). - Course progress, for a copy of a course: its chapters in order,
each done, open or locked. A chapter done can be marked not done, and
the course restarted, once the user confirms. A chapter completed
that the release no longer has is listed by its id, to mark not done
too. Answers and review states stay (
setCompletedChapters, courses.md).
After a save the decks are read afresh, and the deck's study queue is dropped.
The instance's screen
(InstanceAboutContainer)
shows where the instance is, links to its study preferences in Solid
Memo, and has two forms:
- Name:
renameInstancewrites it everywhere it's kept: the meta document, the catalog and the type index registrations (data-model.md), and the instances are read afresh. An empty name is refused. - Catalog: its description, required, and its license, from the
same list as a deck's (
describeCatalog). Its publisher, the pod's owner, is shown, never edited. An instance without a catalog (one not updated yet) says so instead. The form starts again from each read of the catalog (a rename can change its description).
Both forms are held until the data check is done, and while the catalog or a group is set aside (below).
The meta document and catalog writes pass the shape check; the type index renames are If-Match writes only. Each write is made again from a fresh read on a 412, three times in all. The end-to-end tests hold these edits against real servers (metadata.integration.test.ts).
The health screen
(HealthContainer) shows
everything wrong with a deck, or with the instance, for every user.
"Check again" reads it all afresh.
A deck's health is the use case checkDeck
(deckHealth.ts): a line counts
the problems, then come four parts:
- Data check: the shape check of the deck's
subjects in the catalog (its entry, its distribution and its
authors' agent nodes), and of its cards and reviews documents
(
deckReport).- It runs as the check run when an instance is opened: a document still at the version the digest says conformed isn't checked again.
- Each result says what is wrong, in the shape's own words. It links
to a card's field (
cardSpotsOf, by the result's predicate), a wrong option, a review state, or the deck's about screen (for its entry and its distribution). The cards and their wrong options are indexed once for the deck's cards, so each link is a lookup, not a search through them. - Solid Memo's own repairs of these are offered, and a subject no repair covers can be removed once the user confirms.
- Languages: the card sides that state no language, of the cards
the user may settle (
unstatedSides, which, like deckLanguages.ts, leaves out a card still as its release has it). Each links to its side, and a link goes to the about screen, which states them all at once. When the release can't be read, it says those aren't known. - Cards that say the same (
duplicateCardsOf): cards in use with the same front and back, as plain text, in the same languages, and the same pictures. White space aside, case counts. The cards are grouped in one pass, so a deck with thousands of cards alike takes no longer per card than a small one. - Markdown: what the markdown package's check (
markdownProblems) finds in the cards written in Markdown, field by field, by the card editor's hints (markdown.md), passed in by the screen (deckTextCheckinui).
Above them, on a deck's health and the instance's, a deck whose
documents are unclaimed (they don't say they're the deck's, and are
outside the instance's own folder,
data-model.md) is
named with its documents' addresses (UnclaimedDecksNotice). "These
are this deck's documents" asks once more, then marks them as the
deck's (claimDeckDocuments), and the checks are read afresh. Until
then nothing in those documents changes.
The badge on Home counts every violation (warnings aside),
side to settle, group of cards that say the same, and Markdown finding
(healthProblemCount).
While the deck is held (set aside, or the instance blocked: below), every place is named, not linked, and a line says why. The repairs and removals are never held; a problem no repair covers is then removed here, or corrected in the pod by other means.
What the deck's check finds (as its screen opens, after a repair, or
on "Check again") takes the place of what the instance's check found
of that deck (withDeckReport), in the same place in its list. That
result is what holds it. A deck mended is let go without a reload or a
check of the whole instance. A check overtaken by another
(one still running when a repair is made) is dropped.
The instance's health is the check run when it's opened
(checkInstance, the same query as Home's), with its repairs, then
every deck with its badge, a link to its own health, as is a result
about a deck. The decks are indexed once for those links
(deckOfSubjectsIn), so each is a lookup, not a search through them.
A badge checks its deck once, and what it finds holds its
deck too. A repair or a removal here reads every check of the instance
afresh (their queries are under its key); on a deck's health, only the
deck's, which is read afresh each time its screen opens. The end-to-end
tests hold checkDeck against real servers, the documents it checks
again and a removal that clears it included
(deckHealth.integration.test.ts).
The library copies screen
(LibraryCopiesContainer)
lists the instance's decks copied from a release (they have
prov:wasDerivedFrom), from the library or added from a link. A row
shows the release it came from, the newest one out, and what upgrading
would change. The screen links to Solid Memo's deck library.
The use case listLibraryUpdates reads the instance's decks and the
library's index once
(library.ts, libraryCopiesOf). A
copy of the current release, or of one the index lists as no older, is
up to date (offersNewerRelease, as Solid Memo's offer tells it). A
copy whose deck the index no longer lists says so. A release the index
doesn't list is an unknown release.
For each copy with a newer release, the screen asks what upgrading
would do (planLibraryUpgrade, the plan Solid Memo's deck page offers;
migrations.md), with the
copy and its series as found, reading nothing again. It says what the
upgrade does in Solid Memo's sentence, or that it has nothing for the
copy.
The table is a DataTable. The copy at the cursor is
in the aside (named Upgrade), with the rest of its plan: what history is
kept, the new releases' notes, and its cards. Those are added, changed,
retired, brought back, removed, left as the user has them, or given what
the release says they're about (about: every card of a release that
changes only that). That copy isn't in the URL: on
another screen and back, none is shown until the cursor moves.
The copies that can be upgraded are selected by their checkbox, or all
at once; a copy set aside can't be, nor any until the data check is
done (below). Update 2 decks (as many as are
selected), in the
selection's bar, upgrades them one after another with Solid Memo's own
upgrade (applyLibraryUpgrade), on the plan the user saw: a deck
changed since is refused.
- While it runs, the screen names the deck, "2 of 3", and its steps.
- Then a line for each deck says the release it's at now, or the step it failed at and whether the deck changed. One cut off part-way can be studied as it is, and updating it again finishes it (migrations.md).
- A failure is that deck's alone; the next is upgraded anyway.
- Everything an upgrade touched is read afresh, a failed deck changed in part among it, and a failed deck is planned again.
- Each upgrade reads the index once more, to plan again.
The end-to-end tests hold listLibraryUpdates and a batch of two
upgrades against real servers
(deckUpgrade.integration.test.ts).
A copy whose series the index doesn't list is looked up as a copy of a release added from a link (deck-library.md). Its release is read, and its row says "From <host>".
- Its creator's catalog, found in the folders above the release and
read as anyone reads it, lists the releases its instance published
(
sm:publishedRelease); only links at an address a release may have are read. The newest of the copy's series is its upgrade, planned and applied as a library copy's. The newer release's own list of releases in between is kept to such addresses the same way. - Each release a catalog links is read once for each copy, and none that the copy's own release lists. A copy's lookup reads them for itself, its own release among them, under one release's limits together; past them, the row says it's not known, as below.
- Before an upgrade is planned, the newer release and those in between
are checked against the library's shapes; one that breaks them is
refused (
releaseNotConforming). The upgrade is planned from the same reads that were checked, its notes and the releases in between included. Those reads share one release's limits, and none of them is kept after the plan. - No public catalog of its creator may list the release: an instance's catalog is private unless its owner shares it. Then, or when the releases it links are larger together than that, the row says "Not known: no public catalog of its creator lists it, or its releases are larger than Solid Memo reads".
- A copy whose release can't be read either shows as no longer in the library.
Import and export
(TransferContainer)
saves decks as files and makes decks of files: Turtle or JSON-LD, one
deck to a file (data-model.md).
Export lists the instance's decks, as they're arranged, those in
the URL checked. The user picks the format and whether their progress
goes too: the cards' review states and a course's completed chapters.
Export saves each deck as a file of its own, named after its title
(capitals.ttl), one after another, the screen naming the deck it's
at. Export writes nothing to the pod. The use case exportDeckFile
reads the deck's documents (DeckArchive.exportDeck) and hands the
text to the browser to save (FileExchange.save).
Browsers ask before a page's second download, and save none of the rest on a no. The app can't tell, so after several decks the screen says how many it handed the browser, and that the browser may ask.
Import asks for a file (openDeckFile, FileExchange.open): a
.ttl, .jsonld or .json file, its format by its name, else by its
text. The screen then says what the file holds: its deck, its cards,
its progress, how many parts are brought up from an older format, and
how many can't be read and are left out. A
file in a newer format than the app reads is refused (as
deckFileTooNew, not writtenByNewerApp, since nothing is saved
over), as is one without exactly one deck, or one that doesn't parse.
Import into the instance (importDeckFile) makes a new deck of
it, with its progress when the file has some and the user leaves it
checked. Its cards document goes first, only if none is at its URL
yet, then its review states, then its entry in the catalog, so the
deck shows only once it's whole. A cards document left behind by an
import cut off makes the deck take a fresh id, once. Then the deck's
schedule in the digest is brought up to date, the screen links to the
deck's cards, and the instance's decks are read afresh.
The deck is written as any deck Solid Memo writes: wrong options named
with sm:distractor and schema:suggestedAnswer, states naming their
card in the new deck. The instance's type-index registrations of its
decks, cards and review states cover it (data-model.md). While the data
check is under way, or the catalog is set aside, the import is held
(below); a file can still be read.
Each write passes the shape check. Turtle and JSON-LD go through the
Solid client library, behind client/resources.ts, and files through the browser's own Blob and
file input: no new library
(boundaries.md). Unit
tests take a deck with Markdown, pictures, wrong options (one retired),
text in two languages, a retired card and review states through both
formats and back
(solidDeckArchive.test.ts),
and the end-to-end tests against real servers
(deckFiles.integration.test.ts).
A creator writes a deck or a course as a draft of a release, in the
instance, before publishing it. The drafts screen
(DraftsContainer) lists
the instance's drafts, each with its kind (deck or course), the version
it drafts and whether it's being written, was released, or can't be
read. One is deleted, with its documents, once the user confirms. A
draft is private, in the private type index alone
(data-model.md).
Each draft that can be read links to its overview, where a draft made opens, the activity saying so. A draft of a deck copied from a release stays on this screen instead (below).
A new draft starts from
- a blank deck or a blank course, by its name in a language the user states: version 1 of a series of its own, with its Turtle distribution and the data theme EDUC. A course is studied front to back.
- a deck of the instance (
deckToDraftin deckToDraft.ts).- The draft takes every card under its id, its retired ones and wrong options with it, its authors as the release's agents, the first as its publisher, its themes and EDUC.
- The learner's own is left behind: where its documents are, its study caps, its card language, its place among the groups, a course's progress.
- A deck copied from a release names it (
prov:wasDerivedFrom), which is no source of the new release. The screen says the deck is based on it, and offers to start the next version of that release instead, which keeps what its learners studied.
- the next version of a release, by its address, in the library or
in a pod (
nextVersionDraftin releaseVersion.ts).- The draft is the release whole, as version N + 1 in its series, with no release time or notes yet. What it published can only be retired, never deleted or its id given to another, and its activities are kept.
- A series and publisher the release names in another document (a library's index) are read from it, as no one. The draft describes them: the series by what an index says of one, a publisher by its class and name only. A document that can't be read gives nothing, and the release check says what the draft lacks.
- a release saved as a file, Turtle or JSON-LD, as it is
(
releaseToDraftin releaseToDraft.ts).
Either way, the draft doesn't keep a statement that one of its
documents is dcterms:isPartOf anything, which to a deck would say the
document is that deck's
(data-model.md).
A release is made a draft only if its version is 1, 2, …, small
enough that it and the next one are safe integers in JavaScript (a
draft's address names its version). Every id in it must have letters
A to Z, digits, ., _ and -, starting with a letter or digit
(unsupportedIdOf). A chapter's document is named
after its id. Else it's refused (releaseIdUnsupported). Every card,
chapter, step and wrong option must be a fragment of the release
(<release>#id), as the draft holds only those as its parts. One
anywhere else is shown by no editor and checked by no rule, but would
be published with the draft, so such a release is refused too
(releaseForeignPart).
Of a release's other statements, the draft keeps those of the release,
of its own subjects, and of what they name, at any depth: a source, a
license, a blank node. A statement of what none of these leads to is
dropped, with nothing said. So is one of, or naming, an address in the
new draft's folder, its own release document's too, before the
release's subjects are moved there. Read back from the pod, that
address would be one of the draft's subjects, with statements no editor
shows. Copying a draft drops them the same way. One that is kept can
still be of the address the draft is later published at, or of a
fragment of it. No editor shows it, so publishing there refuses the draft
(releaseForeignStatement, Publishing a release).
In the pod, another folder is the way out; for the library, whose
address is fixed, a new draft that doesn't state it.
A draft
whose documents (perhaps written by another app) name a subject by
another id isn't opened, and no change gives a draft one
(draftIdUnsupported). Either is refused with nothing written, as is a
draft at an address no draft has (draftPlaceOf).
A draft is named after its title, or its release's name, unlike the instance's other drafts of that version. Only a draft's place taken meanwhile (its release document made there first) gives it the next name. Any other failure, such as the catalog's link refused on 412 a few times, takes back what was written and is thrown, so no draft is left that the catalog doesn't link.
The use cases are listReleaseDrafts, createReleaseDraft,
copyReleaseDraft (a guest's draft, as the guest's study is added to
an instance, guest-mode.md),
getReleaseDraft, editReleaseDraft and deleteReleaseDraft
(releaseDrafts.ts),
over the ReleaseDraftRepository port.
- When a document changed elsewhere meanwhile, the changes are made
again on the draft as it is now. If they're then refused because
part of them was written before (a chapter they add is there), what
they changed is kept where nothing else changed it (
mergedDraft). - What a release before a draft published is read from that release
(
dcat:prev), once. While it can't be read, everything the draft has counts as published. - A release that isn't in the library is read as no one, so its server learns nothing of who opens the draft. One in the user's own pod (a storage of theirs, or an instance they registered) is read as the user, since a release the pod wouldn't share is its owner's alone. A redirect from there isn't followed, as none is with the user's credentials (authentication.md).
- Making or deleting a draft is held until the data check is done, and while the catalog is set aside (below).
- A draft made with a deleted draft's name has its URL, so what the screens read of a draft is forgotten when one is deleted or made.
A draft changes by DraftChanges
(releaseDraft.ts,
UseCases.editReleaseDraft), covering:
- the release's metadata and its agents;
- its chapters and steps: added, edited, moved, retired, restored or deleted, the chapters and steps in use numbered 0, 1, … again;
- cards and their wrong options, and where a question is asked;
- its sources, and the checks it had.
A change is refused, writing nothing, for:
- deleting what an earlier release published (it's retired instead);
- giving an id that's another subject's or was an earlier release's;
- asking a card from a second place;
- a course not studied front to back;
- rewriting how an earlier release was made, or what an activity carried from one states (removing an agent it names, say).
A check informs, and a source is used by, the draft's own activity
that generated the release. A next version, generated only by
activities carried over, is given one when it first needs it,
#revision-<N>. A check is recorded only as a machine's or an AI's,
in words that say it was no human review ("Scope: …; an AI check, not
a human review."). The release screen makes these changes
(below). A creator names the
ids, the way courses.md says.
A draft is written in six screens: its overview, a chapter, a step, a
question, its cards, and its release metadata and provenance. They
share the draft as one query, read once (getReleaseDraft), and change
it by DraftChanges. Four more read it: its release check, its
listing preview, its trial, and its comparison with the release it
follows.
Every edit is made at once on the screen, then written in the pod
(editReleaseDraft), by the editor's hook
(draftEditor.ts,
useDraftEditor):
- Optimistic. The draft on screen has the change made. A change the draft refuses (an id taken, say) isn't made, and the screen says why.
- Debounced per document. Text is written once its document
(
changedDocumentsin draftLayout.ts) has been still for 0.8 seconds (DEBOUNCE_MS). Changes typed in that time are written together. A change of several documents (a move, say) is written at once. What waits is the draft's, not an editor's: the screen's editor and the outline share it. Leaving the screen, or an edit that isn't typing, such as a move in the outline, writes what waits first. No write's draft is put on the screen while typing waits that it doesn't hold. - Queued per draft. The writes of a draft wait for one another, in one mutation scope, whatever their documents and screens. Each reads the draft after the writes before it, and only the last one's draft is put on the screen.
- Said. A status line, and the status bar on any of the draft's screens (above), says "Saving…" while a change waits or is written, then "All changes saved.". A write that fails, or that the draft as it now is refuses, says why; once the writes after it are done, the draft is read afresh from the pod.
A draft released is frozen, its next version started from the drafts, its release screen or the releases (below). Nothing is changed either while the instance's data check is under way, or blocks the instance (below).
The overview
(DraftOverviewContainer)
names the draft, its kind and version, and edits its title and
description. For a course, it shows the outline, adds a chapter (its
title, and its id), and lists the chapters retired, to restore. It
counts the draft's cards, with a link to their table (a deck's cards
are added here), and the problems the release check finds (for a pod,
the shapes left out).
The outline (DraftOutline,
a list named Outline) is a course's chapters in their order, each with
its steps ("Step 1.2"), each step with the questions that check it, in
the order of their ids, each linked to its editor. Chapters and steps
are arranged as the deck list's groups and decks are, with the same
code in ui:
- Drag (
deckTree/useDragReorder.ts): a chapter among the chapters, a step anywhere in a chapter, or into a chapter by its header. The outline's rules (OUTLINE_RULES,DropRulesindeckTree/dropZones.ts) keep a chapter at the top and a step in a chapter, and make no new group of two. - The row's menu (
MoveItems): up, down, and, for a step, to another chapter. It retires the chapter or step too, or deletes one no release published, once the user confirms.
Each move is a moveChapter or moveStep change (outlineMove in
draftOutline.ts). A
drop the draft no longer has a place for (it changed under the drag)
comes to nothing, and says so.
A chapter, step or question the release check finds problems in has a badge beside it ("2 problems"), a link to the check.
On a course draft's other screens the outline is in the aside
(OutlineAside,
above), where it navigates and arranges too. The one the
page shown edits is marked (aria-current="page"; currentOutlineKey
in
shell/workspaceLayout.ts),
and each badge links to where its first problem is fixed, as the
check's links do (targetRoute). The overview keeps the outline in its
main content, with its new chapter and its retired ones, and has no
aside.
A chapter's screen
(ChapterEditorContainer)
edits its title, always plain text, and its description, with "Format
with Markdown" (sm:textFormat of the chapter). A preview shows it as
the course's screen does, beside the field on a screen 80rem wide or
more, else under it. The screen orders the chapter's steps, up or
down, adds one, and restores one retired. It lists the questions asked
only in the chapter's final review, and adds one.
A step's screen
(StepEditorContainer)
edits its theory, in each language it has, with "Format with Markdown"
(sm:textFormat of the step). In Markdown, each text is hinted at
where it wouldn't show as meant, by the rules of prose (PROSE,
markdown.md). A preview, beside it on a screen 80rem
wide or more, else under it, shows the theory as the course player
does (ProseField in ui, with the player's DataProse). The screen
lists the questions that check the step, and adds one: its front and
back, its id, and the question it's asked before, if not after the
last.
A question's screen
(QuestionEditorContainer)
says where the card is asked: a step, a chapter's final review, or
nowhere yet; choosing another moves it there (moveQuestion). Its
content is Solid Memo's card editor (CardContentFields), saved by
Save; its wrong options are the inspector's (DistractorFields, each
change saved as it's made: distractorChanges). A preview asks
the card as the course does (CourseQuestion, with MultipleChoice),
as typed, to answer and ask again: beside the form and in sight as it
scrolls on a screen 80rem wide or more, else under it.
A draft's cards
(DraftCardsContainer)
are a table, a row each, by id: its front (a link to its editor), its
back, where it's asked, its wrong options in use, and its id. The URL
holds the view (above).
- The filters keep the cards asked nowhere (in a course), asked from two places, with fewer than two wrong options in use, or retired.
- The language keeps those with text in it on their front or back.
- A column's name sorts by it, then the other way, then by id again, from the first page. A page holds 50.
The table is a DataTable. Retire 2 cards or
Restore 1 card, in the selection's bar, retires or restores those
selected the filter keeps, in one edit. Nothing is selected while the
draft may not be changed. Enter opens a card's editor. A new card
is added under the table, asked nowhere yet.
The id assistant (courseIds.ts) suggests each new subject's id, the way courses.md names them:
ch-<title>for a chapter;<chapter>-<n>for a step;q-<topic>-<n><letter>for a step's question, sorting after its last;q-<topic>-r<nn>for a review question.
The user may write another. The field says at once when an id is none
a subject can have, or one the draft or a release before it has. A new
question's id sorts after the step's last; past z, it's the last's
id with a number after it.
A new question may instead be asked before one already asked ("Ask
it", questionIdBefore). Its id then sorts between that one and the
one before it: a free letter when there is one, else an id from
idBetween. Before the first, the place's own name (q-<topic>-<n>,
q-<topic>-r00) is the lower end. When no id sorts there, the form
says so, and the user writes one. An id the user writes is asked where
it sorts, and the form says when that isn't where "Ask it" says.
idBetween finds a free id that sorts between two others, by code
unit: a number after the first when one fits, else one made code unit
by code unit. A seeded property test checks that it keeps the order
and never gives an id taken.
Nothing an earlier release published is deleted, a wrong option among it: its Delete is held, and the screen offers to retire it instead.
The release check
(ReleaseCheckContainer)
runs every rule a release is held to on the draft, as the release it
becomes: the rules npm run library:check runs on decks/
(deck-library.md), from the domain's
(release/). The use case is
checkReleaseDraft(draft, markdownCheck, policy). It finds:
- Rules (
ruleProblemsin releaseCheck.ts):- the course's (
courseProblems), among them cards, chapters, steps or wrong options the draft doesn't hold as its parts (strayParts): ones outside the release, or whose records can't be read. No editor shows them, but the release would publish them, so a draft that holds one can't be published; - what a release needs that a draft may lack (
readinessProblems): a title and a description with English text, a publisher, a version, a series, a distribution, the theme EDUC, and each chapter's and step's fields the draft shapes relax; - the policy's curation (
curationProblems); - how the release says it was made (
provenanceProblems, below).
- the course's (
- Library (the library policy only): the draft's name, version and
metadata against its place in the library's live index
(
readLibraryIndex,libraryProblems). - Drops: against the release it follows, when there is one
(
continuityProblems): nothing dropped, no id given to another kind of subject, and its version the next. - Markdown: its text formats, its text in Markdown, and how a
step's theory is split into chunks: none empty, and as many in each
language (
markdownProblems, markdown.md). The screen passes the markdown package's check in (releaseMarkdownCheckinui, itsproblemsandchunks). It keeps the problems of every text it has checked, by field rule, and the chunks of every theory, so a changed draft's check reads only the text that changed. It keeps them for one draft, the last one checked: another draft's check starts anew. - Shapes, when asked: the release as it's assembled, moved to its
address (
mapIris). Each subject is checked against its library shape (LibraryDeckV6for the release), and the whole against DCAT-AP and SKOS with the reference data (ShapeValidator.validateRelease).- For the library, its index is beside it, with the release's series in it (described from the release when the index doesn't list it yet).
- In a pod nothing else is read: a publisher or series the release doesn't describe fails DCAT-AP's class check (validation.md).
- A button runs them on the draft as it is then; once it changes, or they fail, they offer to check again.
The library's index, or the release the draft follows, that can't be
read is a problem of its own (libraryUnread, previousUnread), and
the rest of the check stands. Such a check isn't kept: the screen
checks again when it's opened again, when its window is focused, or by
its button, as after a check that failed.
The draft checked is the editor's, changes not yet written included, each version once; the release a draft follows is read once.
The policy is a pod's by default, its data's rules only.
policy=library adds the Solid Memo library's curation (repoPolicy)
and its place among the library's decks. The curation asks:
- a title, description and keywords in every language the app speaks;
- each picture on a card described in English and the card's other languages;
- the theme EDUC;
- the library's contact in place of an email, as
npm run library:checkasks of a new version (deck-library.md).
Neither policy names a series, author or host: a library is its index, and a policy is data.
Problems are codes, never text
(problems.ts), worded in
the reader's language (studio.problem.<code>,
problemText.ts). They're listed
by severity, then by what they're in, in two lists, Errors and
Warnings. Each subject links to its editor, and each problem to its
field there (problemTarget), focused; a wrong option's problem opens
its question at it, and counts under it in the outline's badges.
Not every problem can be fixed in the Studio yet, so a new draft's problem count may not reach zero:
- No screen has a field yet for the version, the series (
inSeries,isVersionOf) or the distribution. A blank draft and a deck's have them all, so only a draft of a release that lacks one has this problem. It links to the overview. A problem about the release's license, contact, publisher, languages, themes, version notes or authors, a source, an agent or an activity links to the release screen, at its part. - The library policy places the draft at
decks/<draft name>/v<N>.ttland reports each link that differs from that place, so a draft that follows a release in a pod has alinkMismatchfor that release, its series and its publisher. It reports a draft that states no license too (noLicense, a library rule: deck-library.md), linked to the release screen's license field. - A version the library's index lists already shows up as a
versionGap, with that version twice.
The listing preview
(ListingPreviewContainer)
shows the draft as learners see it in the library: its row in the
library's list (LibraryDeckRow) and its own page
(LibraryDeckScreen). They're over a library deck built from the
draft alone (draftLibraryDeck in
draftListing.ts).
Both are inert: nothing in them to follow or press.
The trial (TrialContainer),
the draft's Trial tab, plays the draft as learners play the release it
becomes: a course with Solid Memo's own course screens (its page, a
chapter's steps, its final review), a deck with Solid Memo's study.
It's played in a sandbox, set up by openTrial(draft, instanceUrl)
with startTrial, both in
trial.ts:
- A pod of its own. Each trial has a new pod, kept in memory, at
https://trial.solid-memo.invalid/(TRIAL_ORIGINin release/trial.ts), which never resolves.createTrialUseCases(trialUseCases.ts) wires the page's own Solid adapters over it (routedFetch). Other requests go to the page's pods through a fence that refuses every method but GET and HEAD (readOnlyFetch). - Only what a trial plays. The trial composes only the slices of
the use cases it supports (
TrialUseCasesintrial.ts): its instance, its preferences, the course or the deck it plays, the library's upgrade and study.- It has no login, guest, drafts or statistics.
- It keeps none of the device's language, theme, palette or text size
(a "none" of its own,
NO_DEVICE). - A mistake in the draft is reported nowhere (
reportPlacehas the trial's own answer).
- The draft as a library. The trial's library is the draft alone,
as the editor keeps it, made afresh at each read
(
createDraftDeckLibraryin draftDeckLibrary.ts), its cards and a course's outline read as a release's are, its metadata the listing preview's (draftLibraryDeck). - The same shapes. Its writes are checked by the page's validator,
over the trial's pod (
over), the shapes loaded once. - Set up as a guest's. The trial's pod is started as a guest's is
(a profile, then an instance with its meta document and its catalog),
with new preferences but for the user's answer scale and the hour the
day rolls over. Then the draft is started as a course
(
startCourse), or imported as a deck.
A draft that can't be played is refused, with what blocks it
(trialProblems), each a link to its field: the course's rules, and
what the player needs of a release (its title, and each chapter's and
step's fields). The release's publisher,
version, series, distribution and theme don't block it.
The course's screens link to the trial's routes, by CourseLinks in
ui (courseLinks.ts), which
is Solid Memo's course routes unless a provider says otherwise. Each
trial has its own query client too.
Its controls (TrialControls):
- Open a chapter, as a learner who completed every chapter before it (the course restarted, then each one before it completed), the answers given kept.
- Answer all right, or all wrong: every question of the chapter in view, or of the one to take next (its steps', then its review's), a wrong answer its first wrong option. A chapter answered right is completed. For a deck, every card to study now.
- Days ahead: the trial's clock (
Clockinui) moved ahead by a number of days. - Start over: a new pod and a new query client, for the same draft.
- Reload the draft: the draft read afresh from the pod, then a new trial of it.
After each control, what is shown is read again. Leaving the trial's screen, or reloading the page, forgets it.
The release diff
(ReleaseDiffContainer),
the draft's Changes tab, compares a draft with the release it follows
(its dcat:prev). A draft of a first version follows none, and the
screen (still at its address) says so. The use case is
diffReleaseDraft(draft)
(releaseDrafts.ts),
which reads the release once and fails when it can't be read. It
shows:
- Rules of the series: the check's drops (
continuityProblems). - What it says of itself: which of its title, description,
keywords, themes, study direction, license, authors, languages and
sources changed, and whether it became a course or a deck
(
releaseDiffin releaseDiff.ts). - Its content: each chapter, step, card and wrong option the draft
adds, changes, retires or brings back, matched by id, a link each to
its editor, and how many of each kind are as they were.
- A card compares as an upgrade compares it (
sameContentin libraryUpgrade.ts), and by what it's about (itsdcterms:subject, in any order:sameSubjects), its wrong options listed on their own. - A card a release states in a format before card format 7 (a
format-4 card compiled from linked data, say) is read with what
it's about too, kept in the card's record. It's compared, and
written once, as card format 7 writes it (
draftFromQuadsin releaseDraftMapper.ts). - A subject retired and changed says both.
- A card compares as an upgrade compares it (
- What learners get (
simulateLearnerUpgrade): Solid Memo's own upgrade plan (planLibraryUpgrade) for a learner's copy of the release, as an import makes it, the learner having changed nothing.- A course's learner has reached every question and completed every chapter.
- The sentence is Solid Memo's update offer's, but a plan that changes only retired cards says "updates only retired cards".
- Then comes the progress learners would lose: the history of the cards the draft drops, and, of a course, the completion of the chapters it drops, found by id, whatever the draft's version. A draft that retires what it no longer uses loses nothing.
- A draft whose version doesn't come after the release's offers learners nothing.
The learner's copy is built in memory from the release and the draft
(draftLibraryContent in
draftListing.ts),
reading and writing nothing. Each version of the draft is compared once,
as the editor keeps it, changes not yet written included.
The release screen
(ReleaseMetadataContainer),
the draft's Release tab, edits the release's metadata beyond its
listing, who made it and how, and what it was made from, written as
the library's releases are
(deck-library.md). Each change is a
DraftChange, saved as the other screens save theirs
(above). Its parts:
- About this version: the version notes (
adms:versionNotes, one text), the languages (dcterms:language), the themes (dcat:theme), the license, the contact and the publisher.- The languages are those of the EU's table (
EU_LANGUAGES, generated from external.ttl: vocab.md), each named in the reader's language; one outside it is shown by its IRI, to keep or leave out. - The themes are in groups: the EU's data theme EDUC; Solid Memo's
optional topics (
TOPICS, from topics.ttl), named in the reader's language, each narrower one under its broader one; and any other, by its IRI, to keep or leave out. - The license is one of
KNOWN_LICENSES(license.ts), or the one it has.setLicensetypes itdcterms:LicenseDocument, as DCAT-AP asks; the license it replaces loses that type, unless a source still names it. - The contact is a web page where readers ask about the release or
report a mistake in it, such as an issue tracker (
setContact): the release's onedcat:contactPoint,#contact, avcard:Kindwith thatvcard:hasURL(deck-library.md). It's saved once it's an http(s) address, and the contact it replaces goes with what it stated. Use the library's contact fills inLIBRARY_CONTACT(curationRules.ts). The app reads it to say where a mistake is reported (deck-library.md). - The publisher is one of the release's agents, or the one it names elsewhere.
- The languages are those of the EU's table (
- Authors: the release's own agents (
foaf:Agent,#<name>), itsdcterms:creators, each with a name and an optional email (releaseMetadata.ts). A release in a pod may name an email; one for the library names none. Removing an author removes its agent too, unless it publishes the release or a statement names it. Authors another document describes are kept as they are. Each author is found among the agents by its id, which the draft's agents are indexed by once, not looked through for each author. A test keeps the time this takes growing in step with the number of authors, not with its square. - How it was made: the attribution, and notes.
- The attribution is "Compiled by <the authors' names>", with or
without "with the help of AI", in English and Swedish ("Sammanställd
av …"): two
rdfs:comments of the release's own making (setAttribution). It needs an author to name, and is written again whenever the authors change. - Only a comment worded exactly so, for the authors the release names, is read as the attribution: a note starting "Compiled by hand…" stays a note.
- Nothing here can say that anyone reviewed the release.
- The notes are the making's other comments: each paragraph one
comment, in its language (
setMakingNotes). - A release that names no making is given one,
#compilation, when it first needs it; a next version gets its own#revision-<N>.
- The attribution is "Compiled by <the authors' names>", with or
without "with the help of AI", in English and Swedish ("Sammanställd
av …"): two
- Sources: each by its address, shown as text and never opened,
with its title, its creator (as text), its license, and the evidence
for that license (a comment that quotes or describes it).
- Each says whether the release is derived from it
(
prov:wasDerivedFrom) and whether this version's making used it (prov:used), at least one, else the change is refused (unusedSource). Only one an earlier version's making used may be neither; it says so and can't be removed. - A release that names no making is given
#compilationfor a source it used. What else the draft states of a source is kept. - Its license is an
http:orhttps:address, not a name such as "CC BY 4.0" (sourceLicenseValid); one it states already is kept. - Removing a source takes away this version's mentions of it; what it states stays while an earlier version's making uses it.
- Deleting an activity of this version takes with it what the draft states of a source only that activity used.
- Only the source being edited has a form.
- A new source is an
http:orhttps:address the release doesn't name yet.
- Each says whether the release is derived from it
(
- Checks: each check this version had, by a machine or by AI, with
what it looked at, its scope, its outcome, the day it ended and its
language. It's recorded in words that say it was no human review
(
checkActivityTriples). An activity written otherwise can be deleted, not edited (readCheckActivityreads only what the Studio writes). Those carried from earlier versions are listed apart, and never changed.
The release check warns of what a reader of how the release was made
would miss (provenanceProblems in
provenanceRules.ts),
each a warning that links to the release screen:
- A source that doesn't describe itself (
sourceUndescribed): it states no title, no creator, or neither a license nor a comment with the evidence for one. - A source used, not derived from (
usedNotDerived): this version's own making used it, but the release doesn't say it's derived from it. An earlier version's making isn't held to this. - A count that disagrees (
countDisagrees): a comment of the release's own making states how many chapters, steps, sources (or documents) or cards it has, in English or Swedish, and the release has another number. Chapters and steps count only in a course, and cards only as a total ("466 cards in all"). An earlier version's making isn't held to this.
The library's command reads a release alone, and can't tell which of
its making is an earlier version's, so it doesn't run these rules; the
Studio does (quadsToReleaseModel marks no making carried).
Below a draft's metadata, the release screen
(ReleasePublishContainer)
releases it, two ways.
Publish to my Pod (publishRelease in
releasePublishing.ts):
- Where. In the instance's
releases/, or another folder whose address the user types, at<folder><name>/v<N>.ttl(releaseUrlInin releasePlace.ts): the draft's name and version, as the library names its releases. The screen shows the address before anything is written. - The user's word. The user confirms that the release becomes public, readable by anyone with its address and never changed or taken back here. Publishing waits for that, for the changes above to be saved, and for the catalog to be writable (below).
- Checked. The screen counts the errors the release check finds
for a pod, with a link to it (
publishErrorsin releaseCheck.ts). Publishing checks the draft again, the shapes too, as the release at the address it's published at. An error stops it (releaseHasErrors), with nothing written; a warning doesn't. - Written once. The draft is made one Turtle document at the
address (
assemble, at library deck format 6, data-model.md), written only where nothing is (If-None-Match: *). An address taken is refused (releaseTaken). So is a draft that states anything of the release at that address, or a subject of it, itself (releaseForeignStatement): no editor shows it, and it would add to the release's own parts. So is a draft whose documents name one of its own subjects otherwise than the draft writes it (the address ofcards.ttl, or another of its documents' fragment) (draftAliasedSubject). Another app can write that, and a draft made from a release before such statements were dropped can hold it. So is a draft whose documents state one of its subjects otherwise than the Studio reads it (draftHiddenValue). That's a second value where a field holds one, or a second text in one language. It's also a value of the wrong kind or not written as one of its kind, or one outside a field's list. No editor or check would see the difference, so the Studio doesn't open that draft either. A subject in a format newer than the Studio's is refused too, with the message to reload the page (writtenByNewerApp). Each value a shape owns is published as the value the check saw. Download .ttl refuses these the same way, at the library's address. A copy of the draft leaves out the statements of a subject named otherwise, and is refused, like the draft, for a value stated otherwise. - Public. Then only that document is made readable by everyone, as
its server's access control says it: its own ACL on a server with Web
Access Control, its ACR on one with Access Control Policies
(
ReleasePublisher, boundaries.md).- This is the only place the app changes who may read something.
- Only the release's own access control, beside it on its server
(
<release>.aclor<release>.acr), is ever written, whatever a server's answers name instead. - Nothing of another origin is asked, nothing else is written, and a write follows no redirect.
- Listed. The catalog links it (
sm:publishedRelease). - Frozen. The draft names its release (
sm:releasedAs), and changes no more. Ansm:releasedAsthat isn't a published release's address is ignored, so such a draft reads as not released, asdata-model.mdsays. - One version. What is checked, written and marked released is the
draft at the version read (If-Match). A draft changed elsewhere
meanwhile is
changedElsewhere: before the release is written, nothing is written; after, the release stays and the draft isn't marked. - Finished again. Publishing cut short after the release is written
(the catalog, the draft or the network failed) is finished by
publishing again, at the same address. A document there that states
what this release does, its time of issue aside, is made public,
listed and its draft frozen, as if newly written. A document that
states something else is
releaseTaken, as is a release another of the instance's drafts names as its own, even when this draft would state the same.
A pod that has no access control the app can write, refuses the write,
or names another access control than the release's own (the folder's,
say) keeps the release written and listed, readable by its owner
alone. The draft is released anyway. The screen says so ("Only you can
read it"), with Make it public, which tries the access again
(makeReleasePublic; publicAccessRefused when it still fails). It
asks first, in a ConfirmDialog that names the release and says the
Studio can't make it private again, both on the release screen and in
the releases screen's table.
Make it public acts only on a release that meets all of these:
- the instance's catalog lists it;
- its document is a release (its own subject an
sm:Deck); - it's in the instance, or in one of the user's storages that the instance is in too.
So a link in the catalog, which anyone may have written, opens nothing
else to everyone, nor a release of the user's own that an instance in
someone else's pod lists. The releases screen lists only links at an
http(s) address named as a release is, …/<name>/v<N>.ttl, as a
request sends it
(data-model.md).
Whether a release is public is asked without credentials
(isReleasePublic).
A guest's draft offers only the download.
A released draft shows where its release is, a link to copy, whether anyone can read it, and Start the next version, which makes and opens a draft of version N + 1 (above). Deleting a released draft leaves the release.
Download .ttl (downloadRelease) saves the draft as a release
file, <name>-v<N>.ttl, to propose it for the library, at the address
the library would publish it at (libraryReleaseUrl, by the library's
index). Nothing in the pod changes, and nothing's checked: the
library's own check does that. The screen lists the steps of a
pull request, in a copy of the repository:
- Put the file in
decks/asdecks/<name>/v<N>.ttl. npm run format:turtle && npm run library.npm run library:check -- --base main.npm run check && npm run crosscheck.- Open a pull request with the new file and the index.
It links to the draft's release check for the library first (above).
The releases screen
(ReleasesContainer,
listPublishedReleases) lists the releases the instance published, as
its catalog links them. Each shows:
- its address, named by it ("capitals, version 2") when it's named as releases are;
- a link to copy;
- who can read it (everyone, or only its owner, with Make it public);
- Start the next version, which makes a draft of it and opens it;
- a link to the draft it was published from, while the instance has it.
It changes no release. The table is a DataTable,
a column's name sorting it, then the other way, then as the catalog
lists them, and Enter opening the release at the cursor. A filter
keeps the releases whose name or address holds its text, case and
accents aside.
The sidebar and the drafts screen link to it ("Published releases"
there). A learner adds a release in Solid Memo by the link copied here
(deck-library.md).
The graph
(graph/GraphContainer.tsx,
readInstanceGraph) draws what Solid Memo keeps in the instance: every
folder, document and resource is a node, and every link between them an
edge.
- It reads the documents the instance check reads (validation.md): the instance's meta, preferences and catalog, each deck's cards and reviews in it, the digest, the answer log's months, its drafts' documents and the releases published in it.
- It reads the user's WebID profile too, to find their storages, which tell the leaves in their pod apart (none, when it can't).
- While it reads, it says how many documents it has read of how many.
- As anywhere in the app, it doesn't read a document the catalog names
outside the instance, one whose address climbs out of it (
..,%2e%2e), or a folder (data-model.md), nor a deck the instance doesn't keep where it keeps decks. - It doesn't read or draw a document whose path in the instance is longer than 2,048 characters or more than 32 parts deep. That's far past any address Solid Memo makes, and each folder on the way would be a node of its own.
- A document that can't be read (refused, say, or not Turtle) is drawn as such, and the rest as read.
Its nodes, each drawn in a shape of its own, its color only helping:
| Node | What it is | Drawn as |
|---|---|---|
| folder | the instance's folder, or a folder in it that holds a document read | a rounded square |
| document | a document read; one that wasn't there, or couldn't be read, too | a square, dashed when it wasn't there or couldn't be read |
| resource | a subject a document defines: the catalog, a group, a deck, its distribution and its authors, a card, a wrong option, a review state, an answer, a receipt, schedule or month summary of the digest, a draft, a chapter, a step, a release | a dot |
| vocabulary term | a class (sm:Card), a concept (sm:frontToBack), a license, a topic or theme, a media type, an EU authority table: one node for each, however many link to it |
a small square |
| "in your Pod" | an address in one of the user's storages that isn't Solid Memo data: the WebID profile on many servers, a release published to another folder | a diamond |
| "outside your Pod" | any other address: a library release a copy came from, a picture, a source, a mailto: address |
a hollow dashed ring |
| found in no document | an address in the instance a link names and no document read defines: an answer to a card since removed, say | a crossed ring |
The last four are leaves, and a leaf is never fetched, in the pod or out of it (privacy.md). Which folder holds a document, and which document defines a subject, comes from their addresses, with no request. A link between documents is drawn as the statements say.
Edges. They're in three kinds, each in a color at 3:1 or more on the background: links green, a folder's or a document's own gray, and those to a vocabulary term amber, paler where bundled. A folder's or a document's own edges start at its square, those to a term end at a small square, and the table of edges names each.
- Edges are solid. The node selected's own are wider and dashed, its links within its column of the band (a card's wrong options, review state and answers) bowed aside, each a curve of its own.
- Of the edges to vocabulary terms, only the node selected's are drawn, and those of the node pointed at or gone to by a key, unless it's a term. The aside and the table of edges list them all.
- A term selected has all of its own drawn in one path, solid and a pixel wide, under the nodes, the paler the more it has past 300.
- An edge to a term goes up the layout's tree from its node to the document, or to the blocks of one as wide as half the drawing, and on to the term.
- The node pointed at is ringed, dashed, as the one gone to is.
Filters. Above the graph is a row of filters:
- Find a node, by its name or its address, among the nodes the filters show, beyond the neighbors too: ten at most, Enter selecting the first, the focus staying in the search.
- Cards, Wrong options, Review states, Answers and Leaves ("in your Pod", "outside your Pod", and found in no document), each shown or hidden, all shown at first.
- Neighbors only: the node selected and those an edge joins it to (the node selected even when its group is hidden).
- Show as a table.
- Last, Vocabulary terms, hidden at first. Until it's checked, the search finds no term and the tables list none; checked, the terms take their places round the rim (Drawing it, below).
Then come how many nodes and edges are shown of all, and how many edges go to terms while they're shown. The filters are kept for the page's life, for each instance.
Names. A node is named by its address, never by its text. The
instance's folder is named by the instance's name, a folder by its own
name (decks/), and a document by its path in the instance. A
vocabulary term is named in short (sm:Card), a blank node by its
label, and any other by its fragment, else the last part of its path.
Two folders named the same are told apart by their parent folders
(capitals/v1/, verbs/v1/), else by their paths in the instance. Two
nodes of documents named the same are told apart by the file
(korean-verbs#card-7), else by its path in the instance
(decks/capitals#sweden, reviews/capitals#sweden), with no
extension. Each is drawn cut at 24 characters, in the middle, for its
end to show.
- The nodes in sight are named once a move or a zoom settles with the part in sight 800 units wide or less, holding 400 nodes at most; else only the folders and documents in sight.
- The node selected and its neighbors are named first, at any zoom, then folders and documents, then decks, then the rest. A name that would overlap or touch one already drawn is left out.
- A name's width is counted from its characters, never measured, and it's ringed in the background.
Drawing it. The layout is a radial tree, its links bundled along
it (hierarchical edge bundling), in
graph/layout.ts, plain
arithmetic with no library. Every node hangs in one tree: under what
holds it (a folder its documents, a document its resources), or under
what it's about when it says so, by the first of these it has:
- a document part of a deck;
- a review state, an answer, a receipt or a schedule by what it's of;
- a cards or reviews document by its deck;
- a wrong option by its card;
- a distribution or a deck by what lists it.
A leaf hangs by the first node that links to it. A choice that would make a cycle falls back, at the first node of the cycle (by address) that can, to what holds it, else to the first node that links to it. If none can, that first node goes under the middle. Where the cycle is broken doesn't depend on nodes unrelated to it. No node hangs more than 16 steps from the middle: one that a chain of links (each part of the next, say) would hang deeper hangs beside its parent instead. So no edge's path is longer for a long chain, and laying it out takes no longer than for as many nodes in a short one.
The instance's folder is in the middle.
- The cards, each with its review states, answers and wrong options beside it, fill a band of rows round the outside, a column at a time, each deck's in a slice as wide as they need.
- The folders and documents are on rings inside it, each in the middle of the slice of what it holds; one holding half the drawing or more is near the middle.
- Those that hold next to nothing once their resources hang elsewhere (the meta, the preferences, the answer log's months, the digest, the folders whose documents went with their decks) have a ring of their own about the middle.
- The vocabulary terms are round a rim outside the band, each as near as there is room for to what links to it.
An edge of the tree is drawn along it, a curve a step.
- Those to a document's cards meet in the same few curves, through a routing point for each column and each block of six columns. A document of more than twelve blocks gathers them under rings of fewer further in, for a few trunks to leave it.
- Any other edge is bundled: up the tree from each end to its block or hub, and between those as a curve bent toward the tree, shared by all links between the same two. It never runs through the middle, a hub as wide as half the drawing, or a hub it would turn back at.
- A curve of 8 links or more is two pixels wide, of 64 or more four.
- The bundled ones are under the tree's, the paler the further in, in steps of 0.04, and the more of them there are past 5,000; those of the node selected, pointed at or gone to are over them in full.
The same graph is laid out the same way every time, every node at a point of its own. The layout runs in steps, 4,000 edges' paths a step, in batches of up to 12 ms, the page drawing between them, under a bar that says how far it is (Placing the nodes). A layout done is kept for the page's life, so coming back to the same graph draws it at once. It's kept with the whole graph it was made of, every node's id and kind and every edge. It's used only for a graph the same in all of these; any other graph, however like it, is laid out afresh. A node the drawing has no position for is left out, with its edges, rather than drawn nowhere. An instance of the library's ten largest decks with a hundred answers each (about 14,500 nodes and 50,000 edges) is laid out and routed in about a tenth of a second.
It's drawn in SVG: dragging moves it, the wheel zooms about the pointer, as Zoom in, Zoom out and Whole graph do. Either changes one transform and one CSS property for the edges' width, and draws no node again.
- A click on a node selects it, anywhere in its shape, a hollow one's inside too. A click on the background (an edge, or a name, which the pointer goes through) lets go of the node selected. Either changes the address, with no Back stop.
- A press that moves 3 pixels is a drag: the click that ends it, and a zoom, let go of nothing. On a screen not wide, the press has already closed the aside's drawer.
- The nodes, their rings and their names stay the same size on the screen at any zoom; zooming in spreads them apart. As a move or a zoom settles (after 120 ms), one CSS property scales every shape about its own middle, and the names are drawn again; during a wheel zoom they lag behind it.
- The drawing resized (the window, say) is measured again once it has been still as long.
- Selecting a node looks its edges up, never searching all of them.
- Past 20,000 nodes or 60,000 edges shown, it isn't drawn until asked: the screen says how many there would be, and offers Draw anyway, the filters and the tables. No node is ever left out unsaid.
Keys. The drawing is one tab stop, as a chart is (statistics.md):
| Keys | What they do |
|---|---|
| Arrows | go to the nearest node that way (within 45 degrees of it); the first, from none |
| Home, End | go to the first node, or the last |
| Enter | select the node, and open it in the aside; on a screen not wide, its drawer opens, with the focus in it |
| Escape | let go of the node gone to |
+ (=), -, 0 |
zoom in, out, or to the whole graph |
The line under the drawing names the node pointed at or gone to ("catalog, resource"). A node gone to by a key is read out in a status line, and brought into sight.
The aside (Node) shows the node selected:
- its address, its kind, the document that defines it, and its classes;
- the values the documents state of it, each with its language tag as
BCP 47 writes it (
@en,@svor@en-GB, though the data is read in lower case) or its datatype (xsd:date). The document a value is stated in follows when not its own; - its links from it and to it. Each is a button that selects the node at its other end and moves the focus to that node's heading.
Its classes, its values and each list of links show 50 at first and all on asking, the focus moving to the first new one, so a node with a great many of them still opens at once.
A leaf says why it wasn't read; a document that wasn't there, or couldn't be read, says so. A node selected opens the aside's drawer on a screen not wide. The aside shares the screen's query.
Show as a table shows the nodes and the edges the filters show as
two DataTables, 100 rows a page. A node row has
its name and address, its kind, the document that defines it and its
classes; an edge row, where it's from, its link and where it goes. A
name, or Enter on its row, selects its node.
Every Studio screen holds to the check Solid Memo runs when an
instance is opened, and to the user's invalid data policy
(validation.md). useDataCheck in ui reads both
with Solid Memo's workspace's queries, once and again after a repair
(health):
- Until the check is done, nothing can be changed, under any policy but "only warn". The screens are shown, and say so.
- A deck set aside (under "set invalid data aside", one whose entry or documents have invalid data) is read-only in every screen, a line saying why, with a link to its health, where it's repaired. It's left out of every bulk action: Home and the library copies don't let it be selected, and moving or copying cards doesn't offer it.
- The arrangement set aside (the catalog or a deck group invalid): Groups can't be rearranged, and Home moves no deck into a group. The instance's name and catalog, an import, making or deleting a draft, publishing a release and starting a next version are held.
- Under "block the instance", invalid data holds every deck and the instance, as a set-aside deck is held.
- What another app wrote only warns: it sets nothing aside.
- A draft is held while the check is under way, or blocks the instance; drafts aren't set aside one by one.
A held form is a disabled fieldset (ReadOnlyScope): links still
lead on, and what is typed in it stays.
Groups (GroupsContainer)
is Solid Memo's deck list without the study: the same screen
(DeckListScreen) and the same edits, kept by useDeckTreeEditor in
ui, which Solid Memo's DeckListContainer uses too. Decks and groups
are dragged, moved from their menus, grouped, renamed and deleted there.
What isn't arranging (a deck's page, its preferences, a course, a new
deck, the library) opens in Solid Memo. As in Solid Memo, a deck set
aside says so, and nothing is rearranged until the data check is done,
nor while the arrangement is set aside. Unlike in Solid Memo, a deck
held (above) can't be renamed or deleted
(deckHeld), a line saying why, with a link to the instance's health.
- The Studio has no page or build of its own. Solid Memo's Vite build
(apps/web/vite.config.ts) bundles it,
through the dynamic
import("@solid-memo/studio")inapps/web/src/main.tsx, as a chunk of its own. - Solid Memo's build test (build.test.ts) checks that nothing the page loads up front holds any of the Studio, its stylesheet included (above), and checks the whole bundle, the Studio's chunk included, for HTML sinks (markdown.md).
npm run buildandnpm run devbuild and serve the Studio with the site:npm run devserves it athttp://localhost:5173/#/studio.