Commit 7dcd09e
feat: read and edit the hints of a converted legacy question
QTI has no hint element, so the conversion carries a legacy question's hints in the
item's <qti-catalog-info> — dormant content the delivery engine never renders on its
own — as cards tagged with a Kolibri support value. Studio already writes them there on
read and reads them back out on publish; the editor in between did neither, so opening a
converted question and saving it dropped every hint it had.
Hints belong to the item rather than to any one interaction, so they come out of
parseItem beside `interactions` rather than inside them, and useQtiItem holds them next
to identifier and title — a ref the card mutates and the rawData computed reads. Cards
are matched on their support value rather than the catalog they sit in, the same way the
publish-side derivation does: a catalog id is a name, the support value is the contract.
assembleItemXml writes the catalog after the item body, which is where the schema wants
it — the one mistake this shape can make and still look right. A hint with nothing
written in it is left out, and an item whose hints are all empty carries no catalog at
all rather than an empty one, since a catalog has to hold at least one card. Two backend
tests pin the result: the document this editor emits is schema-valid, and moving the
catalog ahead of the body is not; and derive_perseus_item still recovers the hints from
it, which is what makes editing a converted question safe to publish.
Only a question that arrived with hints offers the section. That is a scope limit, not a
technical one: assembleItemXml writes the catalog for whatever hints it is handed, and
publishing derives legacy hints from the cards' support value without asking where the item
came from, so hints authored here would survive too — except on the shapes Perseus cannot
express, ordering and free response, where the QTI package would keep them and the derived
Perseus item would not. Whether to offer them everywhere waits on that. The gate is read
once from the parsed item rather than from the live list, so removing the last hint does not
take the section away while the author is still working in it.
A hint is the only thing an author can change on a question with nothing to answer, and
that turned out to be enough to lose the question. Such an item has a body and no
interaction, so no interaction editor mounts and the card held no body of its own;
reassembling from that wrote an empty <qti-item-body/> over the question's text. The body
is read from the item now and seeded whether or not an interaction was found.
HintsSection keeps the shape and styling of the HintsEditor it replaces: bordered cards
that open one at a time into a rich-text editor, and the same move/delete actions and
dashed add button, now drawn with the components the interaction editors already share.
Its header is labelled like the "Question" and "Answers" fields beside it and tints on
hover the way a flat button does, so it reads as one of the editor's own sections rather
than something bolted underneath. A card is only inviting where clicking it does
something: in view mode the reader gets no pointer and no hover tint, because there is
nothing to open.
Its heading joins the ones already in the card: the field labels the interaction
editors put above the question and the answers become headings at the same level,
so a question reads as a section with named parts rather than a run of bold text.
They set their own margins, since a heading brings the browser stylesheet's.
The header is a disclosure, built like the one the community-library modal uses: a button
that says whether what it controls is showing, inside a heading, so the hints sit under
the question in the document outline. The controlled element stays in the document and is
hidden rather than removed, so aria-controls always resolves, while what it holds is
mounted only while open — a rich-text editor per hint per question is not worth paying
for unseen.
Theme values are bound in the style block rather than assembled in JavaScript, so the
component needs no instance handle: the focus ring is :focus-visible, which distinguishes
keyboard from pointer focus the same way $coreOutline does, without asking the theme
plugin.
Hints are supplementary to the question, so a closed card keeps them out of the way until
the reader asks for answers.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>1 parent bcfe223 commit 7dcd09e
18 files changed
Lines changed: 1303 additions & 32 deletions
Lines changed: 160 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
0 commit comments