Develop - #373
Draft
namedgraph wants to merge 308 commits into
Draft
Develop#373namedgraph wants to merge 308 commits into
namedgraph wants to merge 308 commits into
Conversation
… the extractor entry becomes rdfax:extract-rdfa, the canonicalization passes cm:canonical, cm:normalize and cm:demote, and the standalone entry rdfae:main Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Tgf5DjbYiX3SPnCFSgCzd
…ar while an XHTML block is being edited Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Tgf5DjbYiX3SPnCFSgCzd
…, joining graph mode in the fullbleed treatment Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Tgf5DjbYiX3SPnCFSgCzd
…tes that consult it (bs2:Actions) drop their edit buttons in views Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Tgf5DjbYiX3SPnCFSgCzd
…orm dedup Four refactorings in one commit: Facet popovers (view.xsl, ldh-bridge.css, translations.rdf): a facet pill opens its popover immediately with an indeterminate loading sweep that the response replaces with the value list; opening one facet closes the others, and unclaimed clicks bubbling to body dismiss any open popover. The value list caps its height and scrolls, an empty facet says "No values" (new translations.rdf entry), and query errors render inside the popover instead of after it. RDF export (document.xsl): the signed-in-agent guard comes off bs2:MediaTypeList, so anonymous users get the serialization dropdown too. Map canvas (app.css, ldh-bridge.css): the mock-map skin (gradient landmass, pins, scale, attribution) leaves app.css; fullbleed graph/map canvases size from flex-basis 0 so they fill exactly the space between action bar and footer. Chart form dedup (document.xsl, client/block/chart.xsl): the rdf:RDF and srx:sparql bs2:Chart templates collapse into one shell over the new reusable bs2:ChartHeader mode - the chart-controls grid with variant category/series fields. The ReadMode block path (ldh:render-chart) applies the shell to a placeholder document instead of its drifted Bootstrap-era copy, which fixes the unstyled controls and the blank-faced save button, and ldh:RenderChartForm is deleted - when results arrive the header re-renders via bs2:ChartHeader, so SPARQL-backed charts now also get the correct name="ol" and hidden pu markup. Control ids/classes/names and all four bs2:Chart call sites are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WK1ssk2A67MYseeH3hWnhf
…block drag handle becomes a CSS gutter grip, diff decorations move to tokens, chart type options come from ac:Chart subclasses, grid cards prefer ac:image Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WK1ssk2A67MYseeH3hWnhf
… ldh:FormPreSubmit, the single point before parse-rdf-post reads the ol input; the xml-c14n JS lib, ldh:canonicalize-xml and the ldh:CanonicalizeXML re-pass are gone Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WK1ssk2A67MYseeH3hWnhf
…uards, dragenter owns a single moving drop marker, the move PATCH rides the promise chain with revert-on-failure, and ReadMode rows stop emitting drag handles Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0184W2B82P2wUBntUPie223L
…load guards compare via array:flatten instead of calling the JS includes method Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0184W2B82P2wUBntUPie223L
…/status/next, page count) rendered by bs2:PagerControls and re-rendered once the COUNT total arrives; labels come from translations.rdf and the Bootstrap ul.pager markup and CSS are gone Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Tgf5DjbYiX3SPnCFSgCzd
… on valid targets (uncancelled, the browser retargets the drag to the body and every release snaps back), the marker line's gap over the content-body resolves the drop to the marked block, the two no-op positions (the dragged block and its previous sibling) show no marker and refuse the drop, and the DnD payload types move to the vnd tree as application/vnd.atomgraph.* Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Tgf5DjbYiX3SPnCFSgCzd
…d', a bare div parses as the division operator keyword and Saxon warns at SEF compile Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Tgf5DjbYiX3SPnCFSgCzd
… new ldh:ViewFilter/ldh:ViewParallax transform templates, joining the ldh:View* idiom; the OFFSET deletion buried in the ldh:add-parallax-step mode moves to the View layer with its siblings Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Tgf5DjbYiX3SPnCFSgCzd
…he view toolbar's right zone (titled by a new 'links' translation), and the drawer content moves to the design-system .drow vocabulary - parallax rows via ldh:parallax-property-response, backlinks via a shared ldh:DrawerRow template replacing the ul.nav/xhtml:ListItem list, with the lazy-load toggle tracking the dgroup container in flex Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Tgf5DjbYiX3SPnCFSgCzd
…ve and the drawer trades fixed for absolute (retro-theme hover-lift transforms make the card a fixed-position containing block, so a fixed drawer re-anchored between card and viewport as hover toggled), inheriting the card's rounded right corners; the actionbar's right zone widens to mirror ab-left so ab-mid's end lands on the content column's right edge, and the mode selector's opener loses default button chrome Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0184W2B82P2wUBntUPie223L
…ointer rule keys on the pane's data-acl-modes carrying acl:Write, mirroring the ixsl:onclick click-to-edit guard in block.xsl, so agents without write access no longer see a click invitation with no action behind it Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0184W2B82P2wUBntUPie223L
…vocabulary: btn-primary/btn/btn-danger action buttons map onto in-primary/in-neutral/in-negative ap-solid - sz-md in block foots, modal footers and page-level actions, sz-sm in toolbars, legends, table rows and the editor dialogs (the form.xsl copies and rdfa-editor's own alike, ending their bare-accent-fill drift). Vestigial btn tokens drop where a component class already carries the look: the navbar icon buttons and avatar (ldh-icon-btn/ldh-avatar), dropdown-menu items now styled by the bridge's li > button rule, and the clickless sidebar nav-headers; add-value joins the tb icon-button idiom beside btn-add/btn-remove-property, the uncalled ShapeConstructor aligns with ConstructorListItem's it menu-item classes, and sign-up/login step up from sz-sm to sz-md to match the 36px header controls. ldh.css sheds the rules that existed to fight .btn chrome and the search form aligns at 36px against the icon-only sz-md submit; every CSR hook class stays, and the bridge's :where(.btn) fallback remains for Web-Client-emitted buttons only Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Tgf5DjbYiX3SPnCFSgCzd
…s each column's predicate to a SELECT variable via the tunneled $var-predicates map, stamping th.sortable with data-var-name and aria-sort on the active ORDER BY column; the onclick handler toggles direction on the active column (ldh:ViewOrderDirection) or switches the sort key (ldh:ViewOrder), keeping the toolbar's order-by select and direction toggle in agreement, and the header row pins under the view toolbar with a hover-surfaced sort arrow as the affordance. Toolbar sort controls stay out entirely when no variable is sortable; the view's own buttons (mode opener, inline Create, direction toggle) join the ldhc-btn port from the previous commit, and the map info window passes the ldh-block class so the design-system property-list CSS applies there too Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Tgf5DjbYiX3SPnCFSgCzd
…nts chosen by what a click does. Backlinks (jump-off navigation) move into a facet-pop-styled popover anchored to the tb-links button, which now works on every block type: ldh:BlockLinksPopover emits the .links-nav anchor in bs2:Header's actions, in the view toolbar's right zone, and - new - on XHTML content blocks, whose drawers had shipped with no opener; their popover anchors to the card's top-right corner and surfaces on card hover, independent of the acl:Write-gated hover controls. The tb-links onclick in block.xsl toggles the popover and lazy-loads the backlink rows on first open (the DESCRIBE moved out of navigation.xsl's nav-header handler), one open popover closes the others, and the body click-outside handler dismisses them alongside facet popovers; ldh:DrawerRow becomes ldh:LinkRow, and signup keeps its suppression via an ldh:BlockLinksPopover override.
Parallax - a query control that consumes the current result set - becomes the second row of the view's control header: a .parallax-nav row below the filter toolbar on the same surface and dashed hairline, hidden until bs2:ParallaxNav (now writing into the row's .pchips container instead of the drawer's right-nav) delivers arrow_forward property chips. Each pivot records its step (predicate + label, JSON in the block cache) and renders removable 'via <property>' chips beside the facet pills; clicking one rewinds the view by rebuilding the query from the cached initial select-string and replaying the steps before the removed one (ldh:ReplayParallaxSteps) - the undo the drawer never offered. The order-by direction toggle bakes both labels into one button with an empty sort-arrow span, so the existing class-flipping handlers drive the visible label and glyph from CSS alone, mirroring the column headers.
CSS follows: app.css gains .links-pop/.parallax-nav/.pchip/.parallax-step and drops the drawer, edge sensor and .ldh-block-row vestiges, with the tb-links look consolidated onto one .links-nav > .tb-links selector now that the button always sits in its anchor; ldh-bridge.css sheds its drawer rules; retro.css swaps .ldh-drawer for .links-pop in the glass/m3 floating-chrome surface lists; translations.rdf adds the 'via' label. Alongside: ldh:new-object() switches from ixsl:eval to ixsl:new('Object') and 3d-force-graph.xsl adopts it for all node/link/state objects, ldh:load-edited-resource stops replacing an existing per-URI contents object (which wiped the block-html snapshot the btn-edit handler stores for proxied external resources), and the action bar's sticky offset derives from a new --ldh-tabbar-height token instead of a hardcoded 98px.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0184W2B82P2wUBntUPie223L
…dh:CopyUriButton mode (the ldh:BlockLinksPopover pattern): bs2:Actions delegates to it, the view toolbar's right zone appends it after the links button, and XHTML content blocks anchor it beside their links popover at the card's top-right corner, hover-surfaced by the same CSS treatment and given the header actions' quiet 32px icon look in both new hosts. The onclick handler's URI lookup becomes placement-aware - the block header's title anchor (found via the ancestor axis, replacing the stale ../../h2 hop that matched neither the redesigned titles-group header nor youtube's flat override), the form legend's su/sb inputs, or the ancestor block's @about for the anchor-less toolbar and corner hosts. The action bar timestamp hides entirely when the document carries no dct:created/dct:modified castable to a date (the schedule-icon guard mirrors bs2:Timestamp's own filter, also covering documents absent from the results); and facet value lists rebuild their select-xml from the cached initial SELECT string instead of reading the transformed select-xml, so one facet's selection no longer constrains another facet's value list. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WK1ssk2A67MYseeH3hWnhf
…chart's Table branch passes Google's width '100%' draw option (guarded so an explicit $width param still wins), since the table package renders a content-sized HTML table where the corechart types default to the container - this also stretches the generated scroll region, moving the scrollbar to the block's right edge Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Tgf5DjbYiX3SPnCFSgCzd
…ldh:view-cache function - keyed by the host block's @about with the container's @id as the modal-hosted fallback (geo/latest/search/class instances) - retiring the seven priority-1 'BLOCK DELEGATION' wrapper templates that existed only to look the cache up by block URI and forward it via next-match; the facet, parallax and step handlers join the generic handlers' container/cache param pattern and pass $container as the context map's block, and ldh:refresh-view resolves through the same function. Fragment navigation waits for block hydration: ldh:RenderTab collects the content-block factories and, when a fragment is present, chains each promise through ldh:block-hydrated, which counts them down in LinkedDataHub.pending-scrolls and fires the scroll after the last block settles - unless the user has switched to another document's pane meanwhile; the failure twin reports the error like ldh:promise-failure and still counts down so a broken block cannot strand the scroll. The scroll itself moves into ldh:ScrollToFragment (shared with ldh:PushState, whose pane is already hydrated) and falls back from the @about lookup to plain @id for anchors authored inside XHTML content. Design-system touches: the parallax row's label slot trades the 'Related results' text for a lone arrow_forward glyph (the translation survives as a visually-hidden .ldhc-vh accessible name) and the pchips drop their per-pill arrows - the arrow marks the row of available pivots once, the applied steps' 'via' chips mark the taken ones - with the pchip padding going symmetric now that the leading icon is gone; the search modal form sheds the wrapper div that kept ldh.css's direct-child flex rules from reaching the input and iconic submit; and the header address bar spans the content column by pairing its start margin with a width that discounts it (a grid item's 100% resolves against the area, so the margin would otherwise push the bar under the header actions), lifting app.css's 720px cap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Tgf5DjbYiX3SPnCFSgCzd
… and action bar zone formulas derived the column edge from 100vw, which includes the scrollbar, while .content-body centers via margin auto in the layout width that excludes it - parking both half a scrollbar right of the blocks. The bars now recover the layout width from container units (.ldh-header and .ldh-actionbar--zoned become inline-size containers; 100cqw plus the sp-6 side padding pair reconstructs it, shared through --layout-w/--wing custom properties), and the fixed overlays - the design-system .modal plus the doc-edit/constructor/search/access modal widths, the toast region cap and the maximized 3D graph canvas - swap 100vw for 100%, which a fixed element already resolves against the scrollbar-less viewport that centers the column. The create dock keeps its 100vw base deliberately: its 50vw margin bleed and 100vw right padding are each half a scrollbar off in the same direction, so the errors cancel and the buttons already land on the column edge - now stated in the comment so a later sweep doesn't rebase one without the other. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Tgf5DjbYiX3SPnCFSgCzd
… .facet-pill .pred as the 10px lowercase-mono hint designed for the applied parallax chips' 'via' prefix, which shrank the facet dropdown pills' only label to hint size - the span now renders as the pill's regular label text, and the parallax-step chip keeps .pred for its prefix Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Tgf5DjbYiX3SPnCFSgCzd
…entity wraps each predicate's dt+dds in a <div> (HTML's dl grouping element), grouped adjacently on the dds' RDFa @Property URI - a dt keys off its own first dd, only the group's first dt is forwarded, and a plain dt identity template retires the outer label-based dedup (labels can collide across adjacent predicates, property URIs cannot). Blank-node values join the RDFa overrides so every dd carries the URI the grouping keys on: @Rdf:nodeID now emits property/resource="_:..." like its @Rdf:resource sibling instead of falling through to the attribute-less outer dd. ldh-bridge.css styles the wrapper as the unit - subgrid slots each group's cells into the dl's shared label/value tracks, the group carries the top border, the value dividers move to dd ~ dd in the value column, and the hover band rides the group's own background plus the sp-6 card-padding box-shadow copies - replacing the enumerated :has(+ dd + dd ...) ladders that capped the label highlight at four values and left it one row tall beside a multi-valued property's later values; the whole group now lights as one continuous full-height band. Also: the facet popover mirrors its open state as the container's is-open class (added on open, toggled with visibility, dropped on close) so the app.css :has() rule can elevate the host block above subsequent blocks; the view sort's numeric primary key regains the closing paren of number(), which was failing SEF compilation with XPST0003; and the graph/map tab-body min-height comment/rule from the previous session rides along in ldh-bridge.css. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Tgf5DjbYiX3SPnCFSgCzd
…splits into label band + hovered value. Sortable th get full-strength --fg-1 text so the non-sortable headers (focus-var and button columns), which keep the base th --fg-muted, visibly recede - previously the affordance was hover-only (cursor, background, surfacing arrow) and sortable/dead columns were indistinguishable until mouseover; the hover color and [aria-sort] color rules fold away as redundant, the active column stays marked by its persistent arrow. The read-mode dl group hover no longer lights the whole group as one band: the dt spans the group's value rows (grid-row 1 / span 99, surplus implicit tracks collapse to zero height) so its own background covers the full label column like the design's row-spanning .ldh-prop-group .label, div:hover > dt paints the label band and dd:hover paints only the value under the pointer - each with the sp-6 shadow copies filling their side's card padding and the shared column gap, meeting as one continuous highlight; the background transition moves from the group div to the dt/dd that now actually change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Tgf5DjbYiX3SPnCFSgCzd
…s one --surface-1 band with the property lists' fast background fade - the row-per-resource analog of the read-mode dt/dd hover - scoped to tbody so the sticky thead keeps its own --surface-2 sortable hover. No striping: the td border-bottom dividers already separate rows, and a resting --surface-1 stripe would spend the hover highlight's contrast. The sp-6 card-padding shadow bleed stays out - .container-results (overflow: auto) clips at the table edge, so the copies would never paint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Tgf5DjbYiX3SPnCFSgCzd
…numeric values: number() on a text column produced NaN for every row, and a NaN sort key freezes SaxonJS's comparison entirely - later keys are never consulted, so the string key stopped applying and the DESCRIBE response's arbitrary document order leaked through unchanged in both directions (reproduced with xslt3: NaN primary + string secondary returns identical unsorted output for ascending and descending, string key alone sorts correctly). Both the primary and the default-order-by key now cast via 'if ($key castable as xs:double) then xs:double($key) else ()' - numeric columns still compare numerically (5, 9, 10, 100), non-numeric and absent values tie on () and fall through to the string key; the secondary key's number(if ... else ()) wrapper was also NaN whenever no distinct default sort existed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WK1ssk2A67MYseeH3hWnhf
…ugh an ldh:sort-key() function that xsl:sort turns out to honour after all. The previous commit's numeric-then-lexical key pair sniffed castability per value, which cannot express a column whose values are numeric-looking strings: schema:identifier in the Northwind data is xsd:string, so sniffing would order it 1, 2, 3 while the server's SPARQL ORDER BY - which picked the page's members by OFFSET/LIMIT over the same condition - orders it 1, 10, 11. The wrapped DESCRIBE returns an unordered graph and the client only re-sorts the page it was given, so the two orderings have to agree or paging shows a coherent set in an incoherent order. ldh:sort-datatype() resolves the single rdf:datatype shared by a column's literals once over the whole result set (xsl:sort needs mutually comparable keys, so the type is a property of the column, not of the value), and ldh:sort-key() casts the lexical key to the XSD type that orders it: xs:integer and its twelve derived types exactly rather than promoted to double, xs:decimal, float/double, dateTime/dateTimeStamp, date, time, the two ordered duration subtypes, and boolean. Datatypes XPath does not order - strings and their subtypes, xs:anyURI, binaries, gregorians, QNames, and xs:duration itself, which is only partially ordered - return (), as do untyped columns, mixed-datatype columns and values that fail to cast; those tie on the typed key and are ordered by the lexical key that follows it. NaN is filtered back out of the numeric branch because 'NaN' is a valid xs:double lexical form and would reintroduce the freeze the previous commit fixed. The lexical coalesce becomes ldh:sort-key-lexical(), retiring the four inlined copies and the comment claiming SaxonJS drops a user function's return value in xsl:sort - a NaN key freezing the comparison explains the symptom that claim was built on. The default-order-by distinctness guard moves into $default-order-by-predicates, which is empty unless distinct, so the secondary keys collapse on their own. Verified against the live Northwind data: xsd:float schema:totalPrice sorts 4.8, 7.3, 8.5, 8.64, 12.5, 13.5, 14.0, 14.4 where lexical gives 12.5, 13.5, 14.0, 4.8, 7.3; plain-literal schema:legalName sorts alphabetically in both directions; and synthetic cases cover mixed-timezone dateTimes in true temporal order, zoned dates, dayTimeDuration PT90M < PT2H < P1D, booleans, absent and dirty values, and a NaN-poisoned float column that no longer freezes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WK1ssk2A67MYseeH3hWnhf
…iner takes the panel treatment - --bg-card ground, --border-default hairline, --r-lg corners and --shadow-lg lift - and the resource rendered inside it goes .ldh-block is-quiet, whose app.css rule drops the border and background. The info window borrows the modal's chrome (modal-header + modal-body) but its host is ol.Overlay's own wrapper rather than .modal, so nothing was painting the surface and the inner .ldh-block was left as the only bordered thing in an unstyled box; moving the chrome outwards and quieting the block keeps one card instead of a card nested in a panel. The container's existing overflow, already there as the scroll region, doubles as the clip that keeps the property-list hover's sp-6 bleed inside the rounded edge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WK1ssk2A67MYseeH3hWnhf
…ject is a literal or a resource but never both. The dialog asserts one statement, so its invariant half - the subject it is about and the predicate it asserts - is pinned, and a two-tab strip carries what merely elaborates it: the object, and the subject-side @about/@typeof overrides. The object tab leads, because the value is what the user came for. Three things were wrong with showing all seven controls at once. The rows rode .ldh-prop-group, whose 200px label track and 64px action track leave a 360-520px point-positioned card nothing for the control itself; they are stacked ac:FieldShell rows now, and the empty .row-actions cells that only existed to satisfy that grid are gone. The literal/resource exclusivity that rdfae:apply-annotation has always enforced was invisible, so Value, Resource, Datatype and Language all sat there looking equally live; a Text|Link segmented control makes the choice, and rdfae:form-values is overridden to read only the checked side - previously an IRI left in the resource input after switching back to Text still emitted @resource and voided the literal. And the <details> disclosure that LDH's override had flattened into two always-open fieldsets took the progressive disclosure with it, leaving populate-form setting @OPEN on an element that does not exist here. The one shape-dependent step of the shared populate-form moves into rdfae:reveal-fields, so a host that groups those fields differently overrides that alone and leaves the rest of the prefill in one place; the imported default still opens the <details> for the standalone editor. LDH's override selects the object kind, resets the strip to the object tab, and counts the subject-side overrides onto the tab - with no disclosure to open, a @typeof the user cannot see would otherwise be a silent regression. The [hidden] rules in ldh.css MUST keep the specificity of the flex rules above them: an id-scoped display:flex silently outranks both the UA sheet and core.css, and hiding stops working with the attribute still set. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019EptZ6w7rd883q8N4CMorR
…uilders drop to the layer that owns the markup A 4xx or 5xx rendered as an ordinary block: a card headed with the reason phrase, then Reason phrase / Status code / Status code / Title as four data rows. Nothing on that path knew the response was a failure. The only signal was the tab, which ldh:AddDataspaceTab marks with error=true(). Every other failure surface in the app - a block whose query failed, a modal form that could not save - already renders the design system's negative alert with a sentence explaining the status; the document body was the one host that never got one. The block header now carries it. The card, its property rows and its actions all stay: a failed response is a resource with an IRI and a description like any other, and the four rows ARE that description - the header is what changes, not the body. It becomes ac:InlineAlert va-negative holding the same h2 and type chip it held before, plus the sentence ac:http-error-key() picks for the status. The 403 that offers an access request keeps its own priority-2 header, so the Request access button is untouched. The three builders behind this move to Web-Client as ac:http-error-key, ac:error-alert and ac:error-detail. They were ldh:* in client/functions.xsl, a client-only module, and neither the status mapping nor the markup is an app concern - InlineAlert is the client layer's component and that layer renders error documents too. What stays here is ldh:error-alert, now a wrapper that resolves this app's own catalog keys and hands the resolved labels down, which leaves all ten call sites unchanged. technical-detail goes with the disclosure; the http-error-* sentences stay, because ldh:error-alert resolves them here. The disclosure itself moved kits. It was .ldh-block-detail in app.css, which Web-Client does not link, so a shared emitter would have shipped unstyled markup there - or a hand-copy into client.css, which is the drift the vendoring channel already has four recorded instances of. It is now .ac-disclosure in core.css, the one kit both layers vendor byte-identically, renamed because core is ac- throughout since 77ae481 and "block" is app vocabulary. The alert's URI link gains .ac-alert-uri for the same reason it left .ldh-code behind: a kit component that borrows the brand type scale to look right is how an ldh- class reached the client layer. Both rules were pushed to the design system project before being vendored back. app.css re-vendors whole, which closes a divergence rather than creating one: the hosted kit was ahead by the --ldh-menu-avail overflow rules on all three app panels, pushed upstream by 28ee741 without a re-vendor, and uploading the local copy would have silently reverted them. They arrive here as a result, so the rule now exists both in app.css and in ldh.css, where 28ee741 also put it - identical declarations, so nothing renders differently, but one of the two copies is now redundant. Measured on the live instance rather than assumed. Client-side: a proxied 404 (example.org), a proxied 403 as an anonymous agent and a 502 whose body is a Tomcat HTML page with no RDF at all, so it takes the synthesized-response branch in client.xsl. Server-side: a local 403, rendered by layout.xsl. One negative alert in each, the status sentence under the headline, and every property row still present. The 403-with-requestAccess header was exercised too - it is the pane behind the 502 - and still renders informative with its button. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BSRUJBNKKjdgJeP3qRZTPd
… them saying it
Opening a served stylesheet directly showed "â€"" where the source has "—" and
"·" where it has "·". The bytes on disk were never wrong - core.css is clean
single-encoded UTF-8, with no double-encoding signature anywhere in it. The
response was: Content-Type: text/css, and nothing more. A response that names
no charset is decoded with the client's fallback encoding, windows-1252 in a
browser tab, and every multi-byte character comes apart into its individual
bytes.
The app never showed it, which is why it survived this long. A stylesheet or
script loaded FROM a page inherits the referencing document's encoding when the
response names none, and every page here declares UTF-8 - so the rendered
application was correct while the files behind it were unreadable on their own.
ldh.css:29 is where that stops being cosmetic: `a.external::after { content:
"⤴" }` is a non-ASCII character in a VALUE, not a comment, and it only survives
because of the inherited encoding. Any consumer fetching that sheet without an
environment encoding to inherit gets a broken glyph on every external link.
web.xml already solved this - for one extension. The xsl mapping declares
text/xsl;charset=UTF-8 and has done for a long time; css, js and txt simply
never got the same entry, so the fix is four more of what was already there.
.map gets a mapping for a different reason: Tomcat has no default for it at
all, so source maps were served with no Content-Type line whatsoever, and JSON
is UTF-8 by definition (RFC 8259) so it takes the type without a charset. .rdf
is deliberately untouched - an XML document declares its own encoding, and a
header parameter beside it is a second source of truth that can disagree with
it (RFC 7303). Fonts are binary.
A new http-tests/static suite keeps it that way, and collects the static-asset
tests that were sitting in misc: cors-static.sh and gzip-sefjson.sh move in as
cors.sh and gzip.sh, which were the only two candidates - the one other mention
of /static/ in the suite is a comment about VCL rules inside a proxy
cache-poisoning test.
The two new tests are complementary, because the header and the bytes fail
independently. content-type-charset.sh asserts what the response claims, over
five files covering every text type plus .map; it prints expected and actual
for each and accumulates failures rather than stopping at the first, so one run
names every type that regressed. utf8-round-trip.sh asserts what it delivers -
a re-encoding build step would leave the label correct and the bytes doubled,
and nothing else in the suite would notice.
That second test was wrong first, in a way worth recording. It hunted for the
mojibake signature, "Â" and "â€", and PASSED on a deliberately corrupted body:
"·" degrades to "·" and "—" to "â€"", both of which those markers catch, but
"⤴" degrades to "⤴", which carries neither. What a character becomes after a
latin-1 round trip varies; that it must still be itself does not, so the
assertion is now that "—" and "⤴" are present, not that mojibake is absent.
Both tests were run against deliberately broken input to prove they fail: the
content-type test against an expectation without the charset, the round-trip
test against a body pushed through latin-1. Both exit 1 and say why. All seven
extensions this serves were probed on the live instance before and after.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BSRUJBNKKjdgJeP3qRZTPd
acl:mode() reaches window.LinkedDataHub['acl-modes'] through ixsl:contains(), which THROWS on a missing intermediate segment rather than returning false - and Saxon-JS discards a throw raised inside a match pattern, so the rule just does not match, with no console error. The bootstrap created every other LinkedDataHub sub-object but not that one, so between bootstrap and the first ldh:SetAclModes every acl:mode()-guarded PATTERN was dead: click-to-edit on XHTML content, and the two Write-gated drag rules. The same call in a template body threw visibly, which is why ~25 body call sites were fine and these three were not. Measured: LinkedDataHub undefined at click time and the editor never opened; populated, the same click opened it. Initializing it empty makes the guard evaluate to "no modes known yet", which fails closed. It does not make the modes arrive sooner. acl:mode() still reads a global that only a document response fills, while the pane's data-acl-modes - which the CSS mirror and the SSR stamp both read - is correct from first paint. Two sources of truth for one fact; collapsing them onto ldh:active-pane(), as lapp:base() and sd:endpoint() already do two functions away, is left as its own decision. rdfae:populate-form writes DOM properties, while the design system carries the same states as wrapper classes and only the onchange handlers bridge the two. Setting .value from script fires no change event, so on prefill the bridge never ran: a typed literal showed a live-looking language field that was inert, and a datatype outside the option list routed to a custom input that stayed hidden. Both now sync in reveal-fields, which already runs on every populate. The custom-datatype case asks the option list whether it has a match rather than restating the list, so the two cannot drift. The overlay rebuild moves ahead of populate. It is reached by id(), so a host that dropped it made populate a silent no-op over an empty sequence, and show-overlay - which rebuilds it - runs after; the dialog opened blank. Tabs and the object switch stay pointer-activated. The design system specifies a roving tabindex and arrow/Home/End for Tabs and LDH has never ported it; its ContentSwitcher binds no keys at all, so the switch markup is already a faithful port. Both decisions are now recorded where they apply rather than inferred from their absence. The radio role still promises arrow-key selection that neither the component nor this port provides - worth closing in the design system first, so the two do not fork. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019EptZ6w7rd883q8N4CMorR
…king one nothing requests. #383 landed the composed client stylesheet - a dataspace that imports a package runs a SEF composed from the platform's modules plus the package's, published under ./sef and named by the page in its bootstrap script. The harness landed in #381, one commit earlier, so its SEF check still compared static/com/atomgraph/linkeddatahub/xsl/client.xsl.sef.json: a file the browser no longer requests. "SEF served copy matches the working tree" went green while the page executed a build hours old, which cost a session - an xsl:message that never appeared and a working fix that looked broken were both that stale stylesheet. So read stylesheetLocation off the page and check whatever it names. For the stock path the byte comparison stands. For a composed one the artifact cannot be compared to anything local - it is a different stylesheet - so the check is freshness instead: ClientStylesheetService keys a composition on the digest of the stock SEF plus the import set, and digests the stock SEF once, AT STARTUP. An app left running after `make sef` therefore keeps publishing the key it started with. Every build stamps a new buildDateTime, so the digest and the key always move when the app is current, and a key whose file in ./sef predates the stock build is exactly the app that has not been restarted. It asks a CHILD document, never the root, and so runs after seeding: the root is served the stock stylesheet even on a dataspace whose children get the composed one, so probing it reports "stock matches" and re-lands the very false guarantee this removes. Verified in both directions - green as it stands, and failing with the composed key's timestamps once the stock SEF is touched newer. The remedy it prints now restarts linkeddatahub first, because Varnish alone cannot help when the app is publishing a stale key, and nginx last, because restarting Varnish changes the container IPs nginx resolved at startup. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019EptZ6w7rd883q8N4CMorR
A radiogroup is one tab stop whose arrows move between the radios and select as they go - that is the role's contract, not a refinement of it. The switch declared role=radiogroup with role=radio segments and bound nothing, so a screen reader announced a promise the control could not keep, and every segment was its own tab stop besides. The segments now carry a roving tabindex that travels with the selection, and Arrow/Home/End move and select, wrapping at both ends. Selection follows focus rather than waiting for Enter, because for radios that is the definition; the keys route through rdfae:set-object-kind so pointer and keyboard share one write path and cannot drift. Bound on the segment, where focus is, rather than the group. This goes beyond the design system's own ContentSwitcher, which binds no keys and has no roving tabindex (ui_kits/core/Status.jsx) - the port was faithful, and the gap was upstream. The component needs the same fix so the two do not fork; the comment at the markup now points at the contract rather than recording the absence. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019EptZ6w7rd883q8N4CMorR
…to hold a block. settled() returned as soon as the .ldh-block-row count was stable AND non-zero, and the server-rendered shell already carries those rows. On any page whose shell has blocks it therefore returned before window.onload -> location mapping -> SEF fetch -> transform had begun: in the CI trace goto() finished at 120067, the click landed 120078-120136, and "Asynchronous transform ... initialTemplate=main" was logged at 120255, with no network request and no xsl:message after the click at all. A gesture happens once, so the retrying expect() after it could never recover - the annotation spec and document-tree:122 failed every run for that reason, the first of them fixed one commit ago by gating that spec alone. The gate belongs to the helper every spec navigates through, so hydrated() moves into settle.mjs and goto() carries it. It stays strict for a spec that wants to assert hydration; settled() swallows its timeout, because a page that never hydrates is the spec's finding to report and the calibration probe runs against pages that may well be a 403. Saxon-JS binds every ixsl:on* listener in one pass at result delivery, AFTER the initial template completes - not at transform setup - so window.rdfaEditor being set inside that template does not by itself prove the listeners are live. Both happen in one synchronous task, which an outside poller cannot interleave: measured over six loads, the gate opened 0.1-0.2 ms after the click and mousemove listeners bound, never before. document-tree needs no retry around its mousemove. The count > 0 clause goes with it. Its only job was to stand in for the gate now ahead of it, and it is what made every block-less page wait out the full timeout: the three document-tree specs that passed did so by sitting in that loop for 22 s, which is also what hid the race from the suite. They now take 2 s. The annotation spec was also the only one missing the owner guard, so its every test ran against a 403 page in the anonymous project and burned the 120 s timeout three times over - half the run's 49 minutes, and growing as the spec grew. The guard is declared before the fixture hook: hooks run in declaration order, so the skip lands before the CLI builds a document nothing will open. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BSRUJBNKKjdgJeP3qRZTPd
Measured with a probe document carrying every literal kind, only 11 of 30 triples survived the round-trip from Turtle to RDFa and back: 19 lost, and 10 of those returned as different triples. rdfa-streaming-parser, pyRdfa and the platform's own RDFa2RDFXML-v3.xsl agreed on every one. Both representations of a document URL now extract the whole graph and nothing else - 30/30 and 26/26 on the probe and the root container, in text/html and application/xhtml+xml alike, and after Saxon-JS hydration. Most of the damage predates the design-system port: v5's xhtml:DefinitionDescription already emitted no @datatype and already nested the language pill inside the literal, so "Square"@en extracted as "Squareen"@en and 42^^xsd:integer as "42"@en - the datatype lost and a language the number never claimed added in its place. The value cells now take their attributes from Web-Client's ac:RDFaAttributes, which carries @content, so what a cell shows and what it asserts are free to differ: a date renders formatted and asserts its lexical form. Two subject-identity defects are the port's own. @typeof does not inherit a subject - on its own it mints a blank node - so the ldh:Object card and the typed-resource card, which had both carried @about beside it in v5, were typing nodes that are in no graph; the second nulled @about explicitly, with a comment saying the card carried it. Repeating the card's URI on the inner div is a no-op for the handlers that resolve the nearest ancestor @about, since it is the same string. A sweep over every @typeof emission in both trees found the remaining bare ones legitimate: the query block's container carries @about where it is created, a form's container names a resource that has no URI yet, and the four CSR modal panes are ephemeral UI that names no resource - giving those an @about would assert something false. The document's own description was rendered as chrome, which asserts nothing, so its type, title, timestamps, creator, owner, parent, primary topic and rdf:_N block membership were in no representation of the page at all - and no mode renders the whole graph, since ldh:ContentList walks only the rdf:_N members and ldh:BlockRow skips the content resources. ldh:DocumentMetadata carries every description in link and meta elements, which render nothing and take part in no layout. That also reaches rdf:_N without moving @about off the block rows, where the CSR handlers anchor. An XHTML block rendered its prose without saying it was rdf:value, so the one triple such a block exists to carry was the one missing. The attributes ride an element already present rather than a wrapper, because app.css styles that body by child combinator. Also: @about on the root element pins the RDFa default subject to the canonical document URL, which was otherwise the request URI and moved every head-level assertion to a different subject under ?mode= or ?version=; xml:lang beside lang so the two representations agree on every language-tagged literal; og:url and og:title move to @Property, their siblings' spelling and the protocol's own, where @name emitted no triple at all; and the XHTML 1.0 Strict doctype goes, which the writer overrode for both media types anyway and which does not admit @about, @Property or @typeof. The design system is untouched: no stylesheet changed, the vendored kits stay byte-identical to Web-Client's, and the root page's markup is identical once the RDFa attributes and carriers are stripped. The property list differs by one row - the empty-literal property regains the value cell it had been losing, in the same dd.ldh-prop-row anatomy as its siblings, which also restores its dt's title and makes the dl valid again. http-tests/rdfa pins the whole of it. literal-lang-attributes scopes to rendered elements, since the carriers assert the same statements without being something a reader hears, and gains the assertion that a tagged value's @content is the value alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPLdcTh9VikFVpAN3iTXf4
…ght an empty one measures. d72b7af traded yasqe.css's .CodeMirror { height: 300px } for grow-to-content, which is what let the editor fill its host on both axes, and in doing so removed both of the things that gave the field a floor: that 300px, and .ac-cf-gutter's max($rows, line count) rows, which the same commit hides whenever an editor mounts. The textarea's own rows="3" cannot stand in for either, since YASQE sets it display: none on the way in. So the field sized itself by its content with nothing underneath it, and a query with no text yet - Create > ASK, the constructor's first paint - rendered one line tall: measured on the running instance, a field of 904 by 30 around an editor of 902 by 28, with YASQE's share button (30 by 30, absolutely positioned at top: 5px right: 5px) overflowing the field's bottom border by 6px and sitting on the line being typed into. An existing query was never affected, because its own text held the field open; the report named the create form for that reason. The floor is stated in the editor's own terms rather than in pixels: fifteen line boxes of the line-height: 1.5em yasqe.css gives .CodeMirror - 13px there, so 19.5px a line - plus the 4px .CodeMirror-lines pads with above and below. It goes on .CodeMirror-scroll, which is where CodeMirror's own grow-to-content recipe puts a minimum, and it comes to 300.5px: within half a pixel of the height yasqe.css used to impose, now as a floor instead of a ceiling. The gutter needs a floor of its own because CodeMirror stamps .CodeMirror-gutters' height in JS from the height of the document, so the line-number column stood one line tall inside a fifteen-line field; min-height: 100% resolves against the scroller, and the overshoot the scroller's -30px margin buys is clipped by the field's own overflow: hidden. Measured with Playwright against the live instance rather than read off the code. Signed in with the owner certificate - bound to every dataspace origin, since the page dereferences its WebID across them - dismissed the first-time dialog, whose backdrop makes .ldh-create-dock display: none and the Create button unclickable until it goes, and created an ASK through the Create menu: the empty field measures 904 by 302.5 around an editor of 902 by 300.5, exactly 15 x 19.5 + 8; five lines typed leave it unchanged; twenty lines take it to 904 by 400, so the floor stops binding the moment the content passes it; and the ten-line #select-categories-query edit form on /categories/ measures 302.5 where it measured 205 before. The gutter column reaches the field's bottom edge in every one of those, and the share button sits 266.5px clear of it. Click-to-focus was probed at 10, 30, 50, 80 and 97 per cent of the empty field's height - every point hit-tests to .CodeMirror-scroll and focuses the editor, and typing after a click near the bottom puts the text on line 1, which is the property that matters for a field that is now mostly empty space. The rules were injected from the working tree over the served stylesheet to measure them, because the container bakes ldh.css into its image - the live instance keeps serving the old file until the next rebuild. Nothing goes upstream to the design system: its CodeField ships a plain textarea, so .CodeMirror stays an app-layer concern, exactly as .ldh-sparql-pane's rules already are. One thing left alone deliberately: sp.xsl's $rows is still 3, which is what the server-rendered textarea and its static gutter use until the client mounts YASQE, so that first paint is three rows and jumps to fifteen on hydration. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019EptZ6w7rd883q8N4CMorR
The class-instances, Geo and Latest dialogs left a 60px band of nothing between the pager and the dialog's foot. Measured on the running instance at 1440x900: a modal of 805 whose body was 676 with a scrollHeight of 676 - so the band was not slack in a stretched box but three page-flow paddings stacked. .ldh-block-body's own 24px bottom inset, the 16px gutter .content-body .ldh-block puts between stacked blocks, and the dialog body's 20px. The same doubling ran horizontally, putting content 44px in from the dialog's edge while its title sat at 20. The three dialogs now pass flush to ldh:Modal, the parameter the search dialog and the OL info window already use: the dialog body stops padding and the block body's own inset is the only one. The gutter is scoped away rather than cancelled per dialog - ldh:ShowModalForm mounts a backdrop INTO .content-body, so the one block a dialog holds was inheriting spacing meant to separate siblings it does not have. What remained was app.css capping a paged view's body at min(70vh, 620px). That cap is what pins the pager when nothing else constrains height; in a dialog the dialog is the constraint, and the cap only stopped the rows 620px short of the height already there. A flush dialog body now runs a column down to the view body, which drops the cap and takes the slack; the rows keep their own scroll box and the pager stays the column's last unshrinkable item. That rule belongs to the kit, not to the app: it composes .ac-modal-body.is-flush with .ldh-block-row and .ldh-block-body, all design-system classes, and the design system has a dialog-hosted view of its own in SearchModal.jsx. It went to the Claude Design project (ui_kits/app/app.css, beside the PAGED VIEW section it qualifies) and is vendored back here; the two copies differ only in the @import path. It selects the wrappers with :has() instead of naming them, because the kit cannot know what a consumer nests between its dialog and its view - which is also what covers the search dialog's .ldh-search-view without a second rule. Measured with Playwright against the running demo. class-instances and Latest: the band is 24px, the block's own bottom inset, at 1440x900 and at 1000x650, and the dialog reaches the viewport less the backdrop's 64px (836 of 900, 586 of 650), showing 10 rows of 20 with the rest scrolling inside .container-results. The search dialog, untouched in markup, went from an 812px dialog around a 620px view to 836 and 644, its search field and hint bar still pinned. Geo carries a map and no pager, so the column rule does not match it and its body scrolls exactly as before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F69Tr4pjrAaSvAkhra3cQp
The pencil on a committed object value swapped its chip for a search combobox with ixsl:replace-element and kept nothing: the label, the @data-for-class scope and the hidden input[@name='ou'] that carries the value all went with the span.ac-cb-committed the instruction destroyed. The only exits from that state wrote a new value - a mousedown on a panel item, or Enter on a highlighted one - so a pencil clicked by accident left the row empty with no way back short of Reset or Cancel on the whole form, which discards every other edit in it too. So the handler now clones the chip before the replace and parks the clone on the fresh div.ac-combobox, and mode="ldh:CancelCombobox" puts it back - the same stash-and-restore shape as block editing's block-html and ldh:CancelEditing. Two paths reach it. Focusout restores only when the box is empty: the input is a live RDF/POST field, so typed or pasted text is a value in flight, and clicking Save is itself a click-away - a blanket revert would restore the old value moments before submit. Escape restores unconditionally, which is the deliberate cancel and the way out of the typed case; it previously only closed the panel and left the box open. The focusout branch is guarded on exists(ancestor::body). Picking a panel item commits on mousedown, replacing div.ac-combobox; the browser's focus move then fires focusout on the input that replacement detached, and without the guard that late event would paint the stale snapshot over the value just picked. Neither li handler calls preventDefault, unlike rdfa-editor's typeahead. One stash site covers every pencil in the platform: add-type-combobox, add-class-combobox and add-property-combobox all next-match into this rule, since contains-token makes button[contains-token(@Class, 'add-type')] match nothing in 'cb-chip-btn add-combobox add-type-combobox'. Measured on the running demo rather than argued: pencil then click away restores the chip with its label, its title and the same object URI, in exactly one RDF/POST input; typing cond and clicking away keeps cond; Escape with text typed restores the chip; a picked item stands afterwards and is not overwritten; the rdf:type row takes the same route; a combobox that was the value's own initial state carries no snapshot and stays put, ixsl:get of the absent property yielding an empty sequence rather than an error. Saving a form whose chip is a restored clone submits schema:category <categories/2/#this> and leaves the document byte-identical modulo dct:modified, so the clone serializes as the original did. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BSRUJBNKKjdgJeP3qRZTPd
The whole-resource edit form had two exits, both buttons in its own action bar, so a form opened to read something and never edited had to be dismissed deliberately - while the XHTML block beside it has quit its editor on a click outside since the autosave landed. Making the press simply equivalent to Cancel is the reading this does not take. Cancel is a button the author aims at; outside the block is the rest of the page - the same gesture that scrolls, that dismisses a tooltip, that reaches for a breadcrumb and thinks better of it - so giving it Cancel's consequences hands an accidental gesture the power to discard a form's worth of typing, with no undo. Editing UIs that do anything on a click-outside commit (spreadsheets, Finder rename, Figma text) or ignore it (GitHub's comment editor, Slack's message edit); discarding is confined to single-value pickers that never committed in the first place, which is what the combobox chip already does one commit back. The XHTML block is not a counter-example either: its focusout saves a dirty region and cancels only a clean one, and it can afford to save because it has no Save button to compete with, while this form carries validation, If-Match and a minted version. So the press dismisses an untouched form and leaves an edited one, whose exits stay Save and Cancel. The body pointerdown rule - the one rule that must keep claiming that event - gains the walk over blocks whose form the press landed outside of, and defers the reading to ldh:DismissEditing, which compares the form against the baseline ldh:RenderRowForm stashed on it at activation and hands a match to the ldh:CancelEditing that Cancel itself uses. A modal mounts on the body, outside the block it was opened from, so a press inside one is not a press outside. The comparison is ldh:form-content: the same controls, through the same ldh:parse-rdf-post, that the submit path sends, so the dirty check and the request cannot disagree about what an edit is - and the property picker of the add row, which is not state the form would save, does not read as one. Two blocks opt out, each in the layer that owns the reason. An XHTML block's own focusout already handles the outside press, and letting the generic dismissal run too would cancel the block here and leave the focusout that follows cancelling a block whose snapshot is gone. A query block's text lives in YASQE until the form submits, so a query being written reads as an untouched form, and dismissing on that reading would discard it. The ordering is the part that had to be measured rather than reasoned. Reverting an abandoned combobox from inside the dismissal rewrites the wrapper under the focused input, and the blur the browser fires for that re-enters the combobox's focusout in the middle of the dismissal - two rules replacing nodes the other has already moved, which surfaces as SaxonJS failing a removeChild with "the node to be removed is no longer a child of this node". Focus therefore leaves the form first, while the DOM is still, and the combobox's own focusout does the revert; that focusout is now guarded whole, since the input a wrapper replacement detaches has no panel left to hide either. Measured on the running demo, with the document diffed before and after to prove it: an untouched form dismissed by an outside press restores read mode intact and issues no request; an edited one survives the press twice over with the edit still in it, and Cancel still discards it; a press inside the block changes nothing; a pencil clicked and left empty is reverted and the form dismissed in the same press. The combobox suite re-runs unchanged. An XHTML block still exits clean with no request - which is also what proves its canonicalBaseline survives the new form rule, since a lost baseline would have saved instead - and still autosaves dirty, verified by typing into one and undoing it the same way. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BSRUJBNKKjdgJeP3qRZTPd
The stored lexical form of an rdf:XMLLiteral is one rooted XHTML div, but the div and its content were serialized in two separate namespace contexts and joined as strings. ldh:canonical-content serialized the region's children on their own, and nothing in a bare sequence of XHTML elements carries the namespace those elements are in, so Saxon declared it on every top-level child; ldh:parse-rdf-post then concatenated a div around that string which declared it a second time. The first save of prose nobody had edited therefore rewrote the literal, adding an xmlns to its first child element - measured on the demo, where a block saved and immediately unsaved came back differing from the authored form by exactly that declaration, and where the round-trip could never be diffed clean again. So the div is built where the content is, as a node rather than as text around a finished string: inside it the children inherit the declaration and Saxon emits it once. Proved before the change rather than after, with a scratch stylesheet on SaxonJS - the same fragment yields two declarations serialized loose and one serialized inside its wrapper. The wrap ldh:parse-rdf-post was doing goes with it. It is a shim inherited from the server-side RDFPostCleanupInterceptor, applied to every ol input whose lt is rdf:XMLLiteral, and the RDFa editor control is its only reachable producer: every XMLLiteral in the data comes back from the store as rdf:parseType="Literal", which the parseType form control claims, so the generic typed-literal controls - the ones that pass @Rdf:datatype straight through - never see one. Should a lexical form ever reach one of those by hand, it is now stored as it was typed instead of being silently rooted. Measured on the running demo: a prose block typed into, autosaved by the outside press, undone and autosaved again leaves the document byte-identical to the snapshot taken before it, where it used to drift; the served Turtle carries one xmlns per stored literal across all four blocks in the document. The shared parse is exercised by every form submit, so the ordinary path was re-run too - a product form saved with no change round-trips products/3 unchanged - and the outside-press, combobox and XHTML-block suites all pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BSRUJBNKKjdgJeP3qRZTPd
… its namespaces are. misc/PATCH-settings-package-import.sh asserted that the object of the probe concept's skos:broader was in the rendered document while the package was not imported and absent while it was, and grepped the whole document for that URI. 688a975 made the second half untrue by design: ldh:DocumentMetadata applies ac:RDFaCarrier to every description in the graph, so each triple also gets a link or meta element that renders nothing, and the package's rule - one empty ac:PropertyEditor template for skos:narrower/broader/related/member - does not reach those and must not. What a document serves as RDF does not change because a package draws it differently; rdfa/graph-in-markup.sh pins exactly that. So the URI is in the markup in every phase and the grep matched in both directions. CI brackets it: the run for cf05cd5 was green, the one for 688a975 failed on this test alone, and every run since failed on the same one, with no "falling back to the stylesheet alone" anywhere in the job log and the ontology sibling still passing. The assertion is now which ELEMENT carries the marker, read with xmllint over the XHTML rendering as rdfa/graph-in-markup.sh already does - well-formed, so it can be counted rather than matched as text, and attribute order is a serialization detail nothing depends on. The rendered row goes 1, 0, 1 across import and removal; the carrier is asserted to be there in all three phases, so a package that ever did suppress a statement fails here rather than passing quietly. Measured in the browser rather than read off the code: Playwright drove the same fixture against the running stack, PATCHing ldh:import in and out, and counted [property=skos:broader][resource=marker] in the hydrated DOM - 1 div and 1 link before, 1 link alone while imported, 1 div and 1 link after removal, with the marker string present in the document in all three. The second half is what that run first turned up. A DOCTYPE in an application's ac:stylesheet silently disabled package composition for that application: XsltExecutableFilter parsed the stylesheet with SecureXML.newDocumentBuilderFactory(), which sets disallow-doctype-decl, so the parse threw, the filter logged and fell back to the stylesheet alone, and the application rendered with none of the package's rules - a declarative import doing nothing at all, with an ERROR in the log as its only trace. It does not bite here because the build inlines the entities, so every deployed stylesheet carries no DOCTYPE; it bites an application stylesheet that is authored rather than built, which is the ordinary case, since declaring namespaces as internal entities is the idiom every stylesheet in this repository is written in. The client half had already reached the conclusion and said so in a comment - ClientStylesheetService expands entities through SecureXML.newXMLReader(), "which tolerates a benign internal DOCTYPE; newDocumentBuilderFactory() forbids it outright" - so the server half now parses through the same reader into a DOMResult and the two paths accept the same stylesheets. Measured on the stylesheet that was failing: the old path throws DOCTYPE is disallowed, the new one parses, leaves no entity reference unexpanded and resolves &ldh; to its namespace URI. A DOCTYPE-free stylesheet still parses, and an external entity still expands to nothing - a probe declaring file:///etc/passwd serialises as an empty xsl:text, so the internal subset costs no XXE. SecureXML's javadoc claimed stylesheets never carry a DOCTYPE, which is the premise that led here, and now points at the reader that handles them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019EptZ6w7rd883q8N4CMorR
…he rest of it. content-language-by-reader.sh failed in CI with `line 36: echo: write error: Broken pipe` - not a wrong Content-Language, and nothing to do with the response at all. The assertion was `echo "$response" | grep -qE "^HTTP/[0-9.]+ 200"`, and the status line is the FIRST line of a rendered HTML page tens of KiB long, so grep -q matched it and closed the pipe immediately with almost all of the document still unwritten; under `set -o pipefail` the SIGPIPE'd echo failed the pipeline and `set -e` took the test with it. The suite has met this before - 24a3bb8 named it, and d0d8a58 and misc/PATCH-settings-package-import.sh each fixed their own instance with a here-string - but the fix went in per test rather than per pattern, so the sites with the widest window were left carrying it. This one is the widest there is: the match is on line one, so the window is the whole body. Every reader that can exit before its writer is done now takes a here-string. Which sites those are is decided by where the match can land rather than by the shape of the statement: language/content-language-by-reader.sh and language/content-language-media-type.sh match the status line of a full page - the worst case, and the one that failed; misc/cors-jaxrs.sh matches CORS headers at the top of a response that also carries a Turtle body; sparql-protocol/query/GET-ns-no-query.sh matches a class in an N-Triples graph that comes back unordered, so an early line ends the read with the rest of the graph unwritten. Left alone, and why: the great majority of `| grep -q` in the suite reads a curl invoked with -w "%{http_code}" -o /dev/null, which writes three bytes and cannot race; static/gzip.sh and the versioning and system probes read captures taken with -D or -I, so there is no body behind the headers they match; and the negated greps elsewhere read to EOF whenever they pass, which is the only run whose outcome a race could change. Those would report a broken pipe instead of their own message on a run that was failing anyway, which is a diagnostic cost rather than a false failure. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019EptZ6w7rd883q8N4CMorR
…ms and a function. xsl/rdfa-editor/ is a copy of RDFa-Editor/src, and a copy's contract is that diff -r comes out empty. Seven of its files had drifted - 229 lines - because the editor hardcoded its English and applied its own button classes, and the only places to change either were the copy itself, where the next cp reverts it, or a whole-template override here, which forks markup this platform would rather inherit. Both had happened: the toolbar and slash menu were localised in the copy, four dialogs were forked into client/form.xsl for the sake of a class name, and six lines of editor-generic work - a seam, a guard, two @title readouts - existed nowhere but the copy. The editor now carries the mechanisms instead (AtomGraph/RDFa-Editor#32): a translations.rdf catalog read through rdfae:translations() and rdfae:label(), rdfae:langs() asking navigator.languages, and presentational classes declared as parameters. All of it upstream, so all of it arrives by copying. What is left here is the part that is genuinely this platform's. One function points the editor's label lookup at LDH's own catalog, which already answered 110 of its 112 keys in both languages - block-ellipsis and failed-to-load-block-data are the two it did not. Four parameters dress the editor's buttons and helper text in the design system. The four dialog templates that used to do that by copying are deleted, and the overlay redesign, the ldh:Combobox typeahead and the block dialog stay, because those are behaviour rather than costume. Measured on the running instance rather than argued: in a Spanish browser the slash menu reads Párrafo, the find dialog Buscar, the drag handle Arrastre para reordenar; in an English one, Paragraph, Find, Drag to reorder. The dialogs arrive from the editor's own templates wearing ac-btn in-primary ap-solid sz-sm, and the annotation overlay still opens as LDH's redesign - tabs, and an object that is a literal or a resource but never both - because upstream's populate-form calls the rdfae:reveal-fields seam this platform overrides. diff -r against RDFa-Editor/src is empty across all 21 files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BSRUJBNKKjdgJeP3qRZTPd
A dialog hangs its actions outside the scrolling body as .ac-modal-foot, but a form's actions cannot go there: the form element has to wrap the fields and the buttons together, so the bar rides at the end of .ac-modal-body and scrolls away with the fields. Measured on the running instance at 1280x620, the document-edit dialog put its Save 50px below the body's bottom edge and the create-instance dialog put it 160px below, so the first thing either form asked of the reader was a scroll to find out what the buttons even were. The bar now sticks to the scrollport's bottom edge, which lands it where .ac-modal-foot sits. The insets beneath it go with it - the body's own and every wrapper's between the two - or the rows scrolling under the bar keep showing through the strip left below it, which is what the History dialog did. Those wrappers are selected with :has() rather than named, because the kit cannot know what a consumer nests between its dialog and its form: three depths exist already, body > form, body > .ldh-block > .ldh-block-body > form, and body > .ac-tabs > .ldh-panes > div > form. Dropping the inset is guarded on the bar closing the body. The add-data dialog parks a notice after its form, and that notice is the dialog's content rather than the form's: it keeps the inset and scrolls out from under the bar at the form's end. That is the one dialog of six still showing anything in the strip - span.ac-alert-text, 110px of it - and its body does not scroll, so nothing ever passes behind the bar there. A form that IS the dialog's body content carries no card frame of its own, so its bar spans the body's side inset and takes the dialog's bottom rounding; a form nested in a block keeps that card's frame and stays within it. Both band classes are named because both reach dialogs: ldh:FormFooter's $class picks between .ldh-form-bar and .ldh-block-foot per call site, and memento.xsl writes the latter by hand. None of the .ldh-block-foot dialogs sit in a .ldh-block, so those are one band under two names - a consolidation, not attempted here. Verified with Playwright against the running instance, the working tree's app.css routed past the varnish-cached copy, measuring the strip between the bar and the body's bottom edge and hit-testing its middle with elementFromPoint. History (13662px of overflow), document edit (51), create instance (160), app settings and generate containers all report a strip of 0. No dialogs fired. Pushed to the Claude Design project as the same change to ui_kits/app/app.css; the hosted file and this vendored copy differ only in the @import path, before and after. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BSRUJBNKKjdgJeP3qRZTPd
.ldh-form-bar and .ldh-block-foot were one band under two names. ldh:FormFooter's own comment called the three placement wrappers interchangeable, so which one a dialog got was whatever its call site had passed, and after ldh.css's override the two differed by padding-inline and flex-wrap and nothing else. The sticky dialog placement added in cc3795b had to name both to reach every dialog, which is what made the duplication worth paying off rather than working around. Six footers move to .ldh-form-bar: the two ldh:FormFooter call sites that passed the card class (Add data, Request access) and four hand-written bands (Generate containers, History, the ACL Allow form, the RDFa annotation overlay). None of them was inside a .ldh-block - the measured chains end at form > .ac-modal-body or, for Generate containers, form > div > .ldh-panes > .ac-tabs > .ac-modal-body - so the class that names a card's foot was styling forms that have no card. .ldh-block-foot is left to the three that do: the chart block's Save and the view and chart blocks' Create, which chart.xsl also reaches by class through an ancestor axis. The hand-written four gain the .fb-end cluster ldh:FormFooter emits, because the bar's right alignment is the cluster's auto margin rather than the row's justify-content. In the annotation overlay Remove stays outside that cluster: it is destructive and not a peer of Cancel/Annotate, and the auto margin parks it at the opposite edge on its own - which retires the #rdfa-editor-overlay .remove-action override, and the overlay's foot margin with it, now that the generic dialog placement supplies the same bleed. The .ldh-block-foot override drops to what actually differs from the kit, one line instead of seven. The rest was a restatement, except the padding: redeclaring sp-5 left a card's foot a step tighter than the .ldh-block-body directly above it, which is sp-6. The kit's own inset is the right one and the three card feet now take it. The dialog placement collapses to one class, and its bleed picks up the guard the inset already had. A bar with dialog content after it - the add-data notice - is the form's footer rather than the dialog's foot, so it keeps its side inset instead of spanning. Verified with Playwright against the running instance, app.css, ldh.css and the freshly compiled SEF all routed from the working tree past the varnish-cached copies (the composed SEF key is digested at startup, so linkeddatahub was restarted first). All six dialogs report .ldh-form-bar, position sticky, a 12px 24px band, .fb-end present and the last button 24px from the bar's right edge and hit-testing to itself. Strip between bar and body edge: 0 for History, document edit, create instance, app settings and generate containers, at scroll top and scroll end; 110px for Add data, which is the notice below its form and the reason for the guard. A synthetic card foot reads 12px 24px against its body's 0 24px 24px, and the overlay's bar puts Remove 24px from the left edge with the cluster 24px from the right. No dialogs fired. app.css went to the Claude Design project as before; ldh.css and the stylesheets are LDH's own, and the kit's .ldh-block-foot is untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BSRUJBNKKjdgJeP3qRZTPd
The four block/ sub-module includes sat at the bottom of client/block.xsl, under ~1280 lines of functions, so nothing at the top of the file said the module was a bundle. XSLT forces xsl:import to come first but lets xsl:include sit anywhere among the declarations; the convention is now that composition is one block at the top - imports, then includes, then every other declaration. The comment justifying include over import moves with the includes it explains. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F69Tr4pjrAaSvAkhra3cQp
… the vocabulary it speaks. https://packages.linkeddatahub.com/skos/#this becomes https://packages.linkeddatahub.com/editor/taxonomy/#this, and the bundled copy moves to com/linkeddatahub/packages/editor/taxonomy/ with layout.xsl renamed skos.xsl. The old name was the vocabulary, not the capability. What the package ships is a taxonomy editor: a concept tree beside the content, hierarchy views that read both assertion directions, and constructors and constraints for concepts, schemes and collections. SKOS is what it speaks, and skos.xsl is the part of the package that knows it - the same rule that puts vocabulary-typed templates in imports/<prefix>.xsl. editor/ opens a namespace for the editors that follow. Breaking, and safe to do here: 6.0.0 already states that packages installed with earlier releases were webapp-file mutations and must be re-declared with ldh:import. An import naming the old URI resolves to nothing and renders the application WITHOUT the package rather than failing, so the changelog says so explicitly. Nothing in Java changes. Package.getStylesheet() reads the ac:stylesheet triple and the composed SEF key digests the stock SEF plus the sorted package URIs, so both follow the new values on their own. What couples the URI to a file is the two Jena mapping configs, where PrefixGraphRepository resolves an exact lm:name first and otherwise the LONGEST matching lm:prefix at a / or # boundary. The nesting is therefore safe - .../editor/taxonomy/ beats the registry-root prefix - and the exact lm:name for skos.xsl has to stay, since without it the stylesheet URI would fall to the package prefix and resolve to package.ttl. ldt:ontology moves to the develop branch path, which is where the LinkedDataHub-Apps rename lands first and where that repo's own ac:stylesheet already points. The CLI listing is sorted by package URI, and editor/ sorts before foaf/ where skos/ sorted after, so listPackagesMarksTheImportedOnes needed its expected order flipped, not just its literals swapped. ui-tests rename skosPackage and UI_TESTS_SKOS_PACKAGE after the package rather than the vocabulary. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019EptZ6w7rd883q8N4CMorR
Re-vendors the kit's new long-form layer: h1-h6 bound to the .ldh-h* role of the same name, code/samp/kbd, pre, dl/dt/dd, dfn, mark, blockquote, hr, and the DataTable base moved from .ac-table onto `table`. The Google Fonts @import stays stripped here, as always, because this copy self-hosts through fonts.css. The reason it has to be the element rather than a class or a wrapper scope is that the same rdf:XMLLiteral renders in two places: the documentation site, under .docs-main, and this product, under the XHTML block. An element rule is the only form that works in both without either stylesheet having to know the other's wrapper — which is exactly what docs.css's own element layer had been standing in for, and why roughly 2,500 elements were undressed here while looking correct there. Two local rules go with it. The prose <pre> rule was the kit's rule written out by hand, so it is gone. The table pair stays, because flow spacing and topped-out cells are what a table in PROSE needs and an app surface does not, but it rekeys off the element now that the content carries no class. One emitter needed fixing rather than protecting: the first-time modal's subtitle was a bare <h2> inside .ldh-hero, and app.css dresses .ldh-hero h1 but not h2, so it would have jumped to 38px prose type. It takes the hero's own .lede idiom. Every other heading, dl and table emitter in the tree was checked and is already classed or scoped: the property lists build an intermediate xhtml: tree that ac:PropertyGroups projects into dl.ldh-prop-form > dt.label + dd.ldh-prop-row, so the bare dl/dd in resource.xsl, layout.xsl and imports/default.xsl never reach the DOM at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019EptZ6w7rd883q8N4CMorR
Re-vendors the kit's .ac-alert-body child-margin reset, and gives an alert inside an XHTML content block the flow spacing a block in running text needs. .ldh-block .ac-alert zeroes the margin for the block-failure alert, which fills its block and should not be inset — but it is a descendant selector, so it caught every alert an author wrote inside a content block too, and those sat flush against the paragraphs above and below. The prose rule outranks it rather than narrowing it, so the block rule keeps saying what it means. Mirrors docs.css, which does the same for the static site. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019EptZ6w7rd883q8N4CMorR
Two entries for work landed in this cycle: the element-level typography layer that dresses rdf:XMLLiteral content in the product as well as on the documentation site, and the alert inside an XHTML content block keeping its prose spacing instead of inheriting the flush margins the block-failure alert needs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019EptZ6w7rd883q8N4CMorR
Two changes to the band between a view block's title and its results, both spending less of the card on chrome. The parallax row becomes a disclosure, closed by default. The whole pivot set is still one tap away and still opens in place, with its size stated on the closed line — what the flat bar cost was the results, since a full pill row wrapping to a second on a narrow card stacked under the toolbar deeply enough to push the rows themselves below the fold. A pivot is onward navigation rather than an input to the query above it, so it is the row that yields. It is the core kit's Disclosure grammar (hidden marker, chevron rotating on [open]) on a native <details>, so no handler drives it; the slide is a ::details-content transition where the engine has one, and a snap where it does not. Create moves out of the toolbar into the card header, beside copy. The toolbar states what the query is showing — filters, sort, count, mode — and creating a resource is not one of those statements. The slot is emitted declaratively by ac:BlockActions for ldh:View, so SSR and CSR carry it alike, and it rides ahead of the icon cluster, which keeps copy and edit pinned to the card corner so their column holds down a document of mixed blocks. ldh:ViewContainer carries that element now rather than a generated id, which also drops a generate-id dependency from the chain. The kit files go back to the design system with it: PivotBar as the disclosure, and Block threading extraActions into the header slot BlockHeader already took but no block rendering its own header could fill. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BSRUJBNKKjdgJeP3qRZTPd
The kit had no responsive dimension at all: zero @media across core, controls, surfaces, overlays and colors_and_type, no breakpoint tokens, no container queries. What existed were four one-off breakpoints in the app layer sharing no scale between them. The axis is keyed on the CONTAINER rather than the viewport, because a content component is not one width. The same statement grid renders at the document's full width, inside a nested block, inside the 300px content aside and inside a 372px modal body — four widths at one viewport. Measured, the aside at a 1440px desktop gave the value column 36px, worse than the 26px it got on a 390px phone, and no media query can reach that. So the shell — header, tab strip, action bar, footer, rail — queries @media, everything in the content column queries @container, and the containers are declared on .ac-card, .ac-modal-body, .ldh-block, .ldh-nblock and .ldh-content-aside. Never .content-body: that is where ldh:ShowModalForm appends a fixed-position backdrop. Three stops — 520 for containers, 768 and 1024 for the shell — replacing 560, 640, 860 and 900. 520 is derived rather than picked: the statement grid spends 200px on its label and 64px on its row actions, so three columns need 200 + 240 of readable value + 64, and three labelled selects need about the same. Two unrelated components, one number, which is what makes it a scale. Where columns have nothing to align across them, a track floor beats a breakpoint: the footer columns and the chart controls take repeat(auto-fit, minmax(160px, 1fr)) and need no query at all. Two defects the axis surfaced rather than caused. The action bar's three zones are max-content clusters in an auto 1fr auto grid, and a grid track does not shrink below max-content, so below 640px they painted over each other and the breadcrumb was 0px wide; it now wraps into the two-tier shape the kit already describes. And a block's corner affordances are opacity 0 until a hover that a touch device never sends, leaving Backlinks, Copy URI and Edit invisible and still tappable across the block title — they join the flow where the device cannot hover. Measured on the Northwind dashboard at 390px: the page stops overflowing (scrollWidth 616 to 390, and html { overflow-x: clip } made those 226px unreachable rather than merely awkward) and the document is 10551px tall against 15025. Desktop tracks are unchanged. ui-tests/specs/responsive.spec.mjs guards it, including the clone-into-a-narrow-box assertion that a media-query-only implementation passes everything else and fails. The kit files go back to the design system with it, along with RESPONSIVE.md and a preview card showing one component at three container widths. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BSRUJBNKKjdgJeP3qRZTPd
…hat is. The block-reorder dragover rule matches every element inside a block row and carries priority="1", and it guarded its payload with an xsl:if that fell off the end for anything else - so a file drag reached it, nothing called preventDefault(), and the browser refused the drop. That is the whole content area of any container, since a container's graph carries rdf:_1 <#select-children> and every element in the blocks below it is inside a block row. Measured on the running instance: everything inside .ldh-block-row refused, everything outside accepted, which is why the bare pane looked like it worked. What hid it was the comment claiming the file rule had higher import precedence than block.xsl. client.xsl *includes* block.xsl, and xsl:include confers no precedence, so the two sit at the same level and priority decided the other way. The fallthrough was built in one direction only: client.xsl yielded to block drags, block.xsl never yielded back. Now every drag rule yields on a payload it does not own, which is what the payload marker was for. Three gestures, disjoint by type: Files, ...linkeddatahub.block, ...rdfa-editor.block. The drop itself moves off content elements onto one fixed overlay, raised on the first dragenter carrying files. Being topmost it takes every dragover, dragleave and drop that follows, so the enter/leave bookkeeping stays on a single element - no counter needed to survive crossing a child boundary - and no content rule sees a file drag at all. It is also the missing half of the feature: nothing on screen ever said a drop was possible, so a page that accepted one and a page that did not looked identical under the cursor. The overlay joins Menu, Popover and Modal in the core kit as the page-scale sibling of .ac-file-drop and .ldh-dropzone. The ReadMode condition goes with it. Dropping RDF POSTs triples into the document's graph, which is orthogonal to the layout rendering it; that test was avoiding the block DnD collision the payload marker now handles. Where the mode does belong is afterwards - a successful import lands in ReadMode, because reloading a ContentMode document into ContentMode redraws the same blocks and an import that worked looks like it did nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019EptZ6w7rd883q8N4CMorR
It shipped in fc479e8 unexecuted - the stack it targets was not running while the responsive work was verified, so every assertion had been checked by hand against a different instance instead. Its first CI run failed five of seven tests, all of them the spec's fault. Nothing in that run indicted the stylesheet: the two assertions that did exercise it, the header's address track and the tab strip's scrollport, passed at both 390 and 768. Three defects, and they rhyme - each one asserted about a page without checking what that page renders. The statement grid was measured on /ui-fixtures/item-01/, which renders no blocks and no property list at all; the fixture container renders several. calibration.spec.mjs prints the anatomy of both, so the run that failed also carried the answer. The overflow test visited two documents on one page. The second navigation aborts the first page's in-flight SPARQL, Saxon-JS raises that as an alert, and lib/console.mjs fails the test in teardown - after the assertion itself passed. Each document now gets its own test, and so its own page. Being a race, it had failed intermittently, which is the worse half: the tablet case passed on its third attempt. The chart assertion called test.skip() when .chart-controls was missing, so it went green while testing nothing - and what it was not testing was repeat(auto-fit, ...), half the responsive change. The controls were missing for a structural reason rather than a timing one: ldh:ResultSetChart is data until something puts it in the document's rdf:_N list, and only ldh:Object and ldh:XHTML may be values there, so a chart reaches the page wrapped in an Object exactly as the built-in children view is. The fixture created the chart and never made it content, so the element could not appear however long the spec waited. Seeded now, and the skip is an auto-retrying expect(): a missing component fails the build instead of quietly shrinking it. The grid assertions also stopped pinning querySelector(). The container holds several statement grids and document order says nothing about how wide any of them is - a grid inside a narrow block is meant to be collapsed at a desktop viewport. Desktop asserts that at least one is still three columns on the original tracks; the phone asserts the widest has collapsed. Verified by running it: 12 owner tests pass, none skipped, against a stack built from this working tree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BSRUJBNKKjdgJeP3qRZTPd
Three defects that scripting the screencast scenes turned up against the northwind demo, recorded there as measurements and left unfixed. A facet on either of the two biggest containers never returned. The value query appends a label lookup for the values it found, and that lookup walked an eight-alternative property path under GRAPH ?labelGraph. Jena evaluates a path whose graph is a variable as a generic path operator over every graph in the store - 36,877 label rows here - and then joins them against the left side. On /orders/ that is 24.9s, with no error and no failed request: the browser waits. Binding the predicate with VALUES instead leaves a plain triple pattern that uses the quad indexes, and the same lookup takes 0.055s for the same results. Both of the obvious fixes are worth recording as tried and wrong: FILTER(!isLiteral(?value)) inside the OPTIONAL still takes 26.1s, because the filter cannot be reached before the path it guards has been evaluated, and moving the aggregation into a sub-SELECT so the left side is 89 rows still takes 2.7s. The cost is the path evaluation, not the join size and not the literal-subject case that made the lookup pointless in the first place. Timed on the query the browser actually sends, by running ldh:bgp-value-counts itself under xslt3-he and handing the result back to SPARQLBuilder. In the page the five /orders/ facets answer in 131-248ms and /customers/ in 131, and a URI-valued facet still shows its labels. The x on a parallax step chip did nothing and raised an error. The chip carries facet-pill and contains a span.x, so it matched the facet pill's inline clear - which binds the div.facet its pill sits in as="element()", and a step chip lives in span.parallax-steps, with no facet ancestor. Removing a step is now a mode, ldh:RemoveParallaxStep, that the chip body's click and a higher-priority rule on the chip's x both apply: one path rather than two copies that can drift. Either one takes /products/ from the 8 rows of a Category pivot back to 77. A chart axis bound to a URI-valued variable printed the cell's escaped markup, one <a href="..."> per bar. The link is there because Saxon-JS cannot intercept Google Charts events, so a Table cell has to carry a real anchor - but allowHtml is a Table option, and every other chart type prints the cell as text, on the category axis, in the legend, in a tooltip. Only the Table gets the anchor now; the rest get the object's label, and the chart type reaches the data table as an argument. Two more URIs printed where a label belongs in the same render: the column label was the property URI, which is what the legend and the Table's own header show, and the axis titles were the raw $category and $series, which name columns rather than saying anything to a reader. The column label is ac:property-label() and the axis titles are read back off the data table's columns, so both paths - properties and variables - are titled by whatever that table already resolved. The labels for that axis have to come from somewhere. ldh:RenderViewResults leaves its object-metadata on the block cache beside the results, because the chart controls rebuild the data table from the cache on every change; without it a category axis degrades to the fragment of each URI and reads "this" ten times. Verified in the browser: Meat/Poultry, Seafood, Beverages against a Category axis and a Price legend, and the Table chart keeps its links. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BSRUJBNKKjdgJeP3qRZTPd
Asking the endpoint for a document rather than for results - Accept: text/html, what a browser sends - was answered 500, and had been since the document-tabs work landed in April. The shell binds its content pane as="element()" and fills it by applying ldh:TabPanel, which the tabs refactor introduced matching rdf:RDF only, as it did the ldh:DocumentBody beneath it. Before that refactor the body applied ldh:ContentBody directly, and that mode has always had an srx:sparql rule rendering the results table - so a result set went from rendering to matching no rule at all, and an empty sequence where an element is declared is not an unstyled page, it is XTTE0570 and a 500. Both matches widen to rdf:RDF | srx:sparql, the way every sibling in that file is already written, and ldh:DocumentMetadata gains the empty srx:sparql rule that stops the built-in template copying the bindings' text into the page - the counterpart of Web-Client's own empty srx:sparql rule for ac:Aside. Nothing else in either template needed touching: ac:ActionBar and its three zones, ac:MediaTypeList, the breadcrumbs and the footer all match result sets already. Fixing the server exposed the second half, which no response-reading test can see: the client painted an error block over the table about a second after it arrived. Saxon-JS re-fetches the document once it hydrates, and ldh:snapshot-params kept only ?version and ?timemap from the URL - so the re-fetch asked the endpoint for nothing, was told 400 "Query string not provided", and rendered that http:Response where the results had been. The function's comment already called these representation-selecting params, which is exactly what the SPARQL protocol's three are for an endpoint document, so it takes the name ldh:representation-params and carries query, default-graph-uri and named-graph-uri alongside the Memento roles. Keeping the query is not sufficient on its own: the request asked for application/rdf+xml unconditionally and a SELECT is answered 406, so a request carrying ?query= now accepts both shapes and the response's own media type decides how it renders - measured against the live endpoint, SELECT returns application/sparql-results+xml and CONSTRUCT application/rdf+xml, both 200. The success branch accepts either media type and dispatches on the document element rather than on /rdf:RDF; everything inside it guards itself, because a result set has no diff, no object metadata, no blocks and no canvas. The reason this shipped through 5.9.x with a green suite is in the coverage: all fifteen tracked tests under sparql-protocol/ ask for application/sparql-results+xml or application/n-triples. Not one asked for a representation that reaches the XSLT writer. GET-sparql-xhtml.sh does, asserting the pane, a header cell per projected variable and a row per solution against a query that binds its own values, so it holds on any dataset. The Playwright spec covers what it cannot: that the table is still there after the client's re-fetch, and that coming back to the page client-side rebuilds it with no server-rendered body to fall back on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BSRUJBNKKjdgJeP3qRZTPd
app.css (PAGED VIEW) caps a paged view's body at min(70vh, 620px), makes it a flex column, and hands the scroll box to its direct .ldh-grid-block / .ldh-list-block child. LDH never has that child: ldh:GridViewBlock and ldh:ListViewBlock both render one level down, inside .container-results, so those child rules have never matched here at all. The cap was survivable only because the wrapper is itself a scroll container - max-height: 80em, overflow: auto - and shrinks under it, which is exactly the arrangement the kit's own comment anticipates and names. Grid mode takes that scrollport away. The m3 card draws its border as an outside box-shadow and the grid sits flush in the wrapper, so any clip box cuts the card edges, and the rule above it opts out with max-height: none; overflow: visible. The cap never came off with it, and a flex item with overflow visible has a content-sized automatic minimum: it will not shrink and it does not scroll. There is no scrollbar and no clipping to notice - the rows simply leave the block. Measured on /territories/ at 1920x1080 before this: an 890px grid in a 620px body, 358px of cards outside it. What that looks like is a paint-order consequence rather than a second bug. .ldh-block-row is position: relative, so every content row paints after all in-flow content in tree order: the escaped cards went over the page footer, which is static, leaving its text visible only through the grid's gaps, while the next row's own background and text painted over the cards. That is how the report came in - a paragraph of prose sitting inside a card, with an editor block apparently drawn on top of a view. Uncapping the body when the results region holds a grid is what the opt-out already assumes: the page scrolls the grid, and the pager sits after it in flow. List and table are untouched, their wrapper still being a scrollport - verified, not assumed: the same probe reports a 358px spill in grid mode and -87px in both of the others before the change, and -87px in all three after, with the footer moving from 1052 to 1497. ui-tests/specs/view-overflow.spec.mjs guards it across the three modes that page. It addresses the body exactly as the kit rule does, so it needs no fixture-specific locator, and it asserts geometry rather than declarations: a max-height is one way to satisfy the invariant and not the only one. The results region ends inside the body that frames it; a region that does not scroll contains its own rows, read off the computed overflow rather than hardcoded per mode, since list and table legitimately let rows run past a scrollport; and the footer starts below the results, which is the half a fix that contained the rows but left the page's own height wrong would miss. List and table are the controls that say the cap itself is not what broke. Run against the pre-fix stylesheet the grid case fails at 565px outside a 620px body while both controls stay green; after, 6/6 pass and the full suite is 56 passed, 44 skipped, 0 failed. Nothing else moves. Web-Client has no .container-results, .ldh-block-body or .ldh-pager - the wrapper is this repo's, emitted by client/block/view.xsl - so there is no second copy to keep in step. The fix is in ldh.css, which is not part of the vendored kit, so the design system has nothing to take from this; its five stylesheets that changed since the hosted project's last recorded update were audited and are in sync, app.css and surfaces.css by full byte diff, the other three against the specific rules their pending commits added. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BSRUJBNKKjdgJeP3qRZTPd
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.