Develop - #373
Draft
namedgraph wants to merge 314 commits into
Draft
Develop#373namedgraph wants to merge 314 commits into
namedgraph wants to merge 314 commits into
Conversation
… 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
….ol-overlay-container .ldh-block-head drops its side padding and keeps only the sp-3 gap under the title. The head's own inset is written for a card that wraps it in .ldh-block-body, whose ldh-bridge.css rule zeroes those sides; the default resource template emits head + property list with no body wrapper, so nothing cancelled them here and the title sat sp-6 right of the labels underneath while the head's top padding stacked on the container's. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WK1ssk2A67MYseeH3hWnhf
…type-family(), which reduces a literal's datatype to the type that carries its ordering and value space: the thirteen integer types collapse onto 'integer', float and double onto 'double', dateTimeStamp onto 'dateTime', and everything XPath does not order - xs:string and its subtypes, xs:anyURI, the binaries, the gregorians, QNames - onto 'string' along with untyped values, which are plain literals and so strings under RDF 1.1. ldh:sort-key() switches on the family instead of restating the URI lists, leaving the cast per branch as the only thing it still says itself. The function lands in imports/default.xsl, the property/value layer both chains already import - layout.xsl for SSR and client.xsl for CSR - so a consumer outside the client tree can reach it, and it stays free of ixsl so the SSR chain still compiles. Verified behaviour-identical across the twelve-case matrix: integers, floats, a NaN-poisoned float column, mixed-timezone dateTimes, zoned dates, numeric-looking strings, plain literals, booleans, absent and dirty values, a mixed-datatype column, resource values and dayTimeDurations all sort exactly as before in both directions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WK1ssk2A67MYseeH3hWnhf
…eTime branch that had been written out at every site. dct:created and dct:modified carry either granularity and xs:date and xs:dateTime are not mutually comparable, so each site normalised to xs:dateTime by appending 'T00:00:00' to the lexical form - which puts the timezone before the time part on a zoned date and yields '2024-01-05ZT00:00:00', not a valid xs:dateTime. That is FORG0001 rather than a misordering: a zoned xsd:date in either property took out the whole transform, and the guards let it through because such a value is castable as xs:date. Casting through xs:date() instead keeps the timezone and cannot fail once the value is known to cast. Three sites collapse onto the function: the bs2:Timestamp sort in resource.xsl, the ldh:ListRowTimestamp sort in view.xsl together with the format-date call below it that repeated the same branch to get its xs:date back, and the action bar's schedule-icon guard in document.xsl, whose filter now reads as the exists() it always meant. The function lives beside ldh:datatype-family() in imports/default.xsl and stays free of ixsl, so the SSR chain that reaches resource.xsl and document.xsl through layout.xsl still compiles. Checked across plain and zoned dates, dateTimes with and without a timezone, and non-dates: the first four normalise as expected and feed format-date, the rest return (). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WK1ssk2A67MYseeH3hWnhf
…ypes. Both enumerated only xsd:integer, decimal, double and float as chartable numbers and xsd:dateTime alone as a datetime, so a column of xsd:long - 18 of them in the Northwind data - typed as a string and its values serialised as quoted strings, charting as categories rather than a numeric axis. The same held for xsd:int, short, byte, the unsigned family, the bounded integer types and xsd:dateTimeStamp, all of which restrict a type the converters already handle and are exactly as chartable as it.
The widened list lands in both the column-type classification and the cell-value pattern in each file: they have to move together, since a column that types as 'number' while its cells still emit strings is worse than one that types as a string throughout. Each converter keeps its own copy - they carry no imports and no ldh: dependency, and stay compilable on their own. Verified by running both over the affected types: xsd:long and xsd:dateTimeStamp now yield {"type":"number"} with 42 rather than "42" and {"type":"datetime"} with Date(2024, 2, 11, 14, 47, 0, 0), a mixed long/unsignedShort column still types as a number since every member is now in the list, and output over the previously supported types is byte-identical to before.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WK1ssk2A67MYseeH3hWnhf
…e the source happens to carry. The object templates - foaf, schema and dbpedia-owl in Web-Client's imports server-side, the consolidated copy in client.xsl for CSR - emit a bare <a><img></a> with nothing bounding it, so the view's table mode put a 200px headshot in one row and a photo taller than the viewport in the next, and the same markup reaches property-list dd values and the map info window. Grid mode was the only one already normalised, fitting each card's image to a 4/3 frame with object-fit: cover, and list mode renders a type icon instead. A --thumb-sm/md/lg scale in colors_and_type.css carries the caps and ldh-bridge.css spends them on the three unbounded surfaces: table.table td at 48px, the property-list dd at 160px, the block head at 240px, each picking up the card language's radius. Only max-height and max-width are set, so every source stays proportional and images already smaller than the cap are left alone - a fixed box would have to crop or letterbox, and a depiction is as often a logo, a map or a scan as it is a headshot. The map info window needs no rule of its own, since its overlay hosts a .ldh-block property list and both rules outrank the .ol-overlay-container img catch-all in ldh.css on specificity and on load order. The rdfa-editor builds tables without the .table class, so author-written content keeps its images at natural size. The block head drops width: 100% for max-width: 100% on the way. Its grid track is a fixed minmax(140px, 220px) whatever the image does, so the title alignment that rule exists to protect comes from the track rather than from the image filling it, and stretching bought nothing but an upscaled logo. The height had to be bound there regardless or a portrait source grew the header past the property list it heads. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Tgf5DjbYiX3SPnCFSgCzd
…d-A stops falling back mid-word. All three families were vendored as the Google Fonts latin subset alone, and each @font-face repeated that subset's unicode-range - U+0000-00FF plus a handful of named codepoints, nothing between U+0100 and U+017F. The files match the declaration: 225 codepoints in geist.woff2, none above U+2212. Every Lithuanian diacritic therefore missed Geist and was substituted per glyph from the next family in --font-sans, which is system-ui, so a name like Balčytis or Širinskienė rendered as Geist spliced with SF Pro at different widths and weights inside one word. The quotes in „Vardan Lietuvos" were unaffected, being in the latin subset's U+2000-206F. Each family now declares the latin and latin-ext faces as a pair the way Google Fonts serves them - same family name, disjoint unicode-range, separate file - which keeps the extended subset off the wire for pages that never reach past U+00FF. The two Geist files carry the same wght 100-900 axis as their latin siblings and Instrument Serif stays static at 400, so the weight declarations remain honest. Coverage is verified across the affected languages, Lithuanian along with Polish, Czech, Turkish, Hungarian, Latvian and Estonian: every character resolves within the family for all four faces. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Tgf5DjbYiX3SPnCFSgCzd
…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
view-overflow ran in both projects and failed nine times in CI - three modes, each retried twice - waiting 20s apiece for a paged view that was never going to appear. The anonymous context got the 403 error page, whose anatomy shares no selector with the block being measured. The obvious reading, and the one every sibling spec's skip already states, is that the fixture container is owner-owned. It is not. Nothing in a fresh dataspace is readable without a certificate: the anonymous probe of the ROOT is a 403 too, before any fixture exists, because the shipped Public access authorization in platform/datasets/admin.trig carries acl:mode and acl:agentClass but no acl:accessTo or acl:accessToClass - it grants nothing until something fills it in. The suite never does. http-tests does, via admin/acl/make-public.sh, which is why this passes on a dev stack that has run the HTTP suite and fails on CI's virgin one. The workflow even waits for anonymous 403 at the root as its readiness signal. So the reason is not ownership and not the fixture. Run against a public instance the spec passes in both projects, which is the proof it was never owner-specific: what it asserts is that a paged view's rows stay inside the block that frames them, and that geometry is the same whoever is reading. The authorization axis would measure it twice. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every other input this suite depends on is seeded: 25 items, the query, the view, the chart, the taxonomy package. Authorization was the one it took from whatever state the instance happened to be in - which is why view-overflow reproduced in CI and not locally, and why nobody noticed for as long as the anonymous project asserted nothing. Of its 50 tests, 47 skipped and the three that ran were calibration probes, which assert nothing and passed against a 403 error page. Four pieces, in the order they depend on each other. A control document, ui-fixtures-private, that no authorization ever targets. Its anonymous 403 is what proves no blanket grant is in force. It is seeded because the check cannot be made against an assumption, and it is a SIBLING of the fixture container rather than a child: document-tree counts that container's children against itemCount, and a 26th child fails it - which it duly did. The baseline check runs after seeding, because LDH answers 403 for a document that does not exist as readily as for one that is refused, so probing before there is anything to probe proves nothing. Verified both ways: it passes on a virgin instance, and with a scoped grant constructed on the control it refuses to start the run. The grants themselves, scoped the way http-tests writes them - ldh admin create authorization, one document at a time, slugged so teardown can delete them, never the global make-public, which is an INSERT into the shipped authorization with no reverse in the CLI. What they had to contain was measured, not guessed: a document-scoped grant alone does NOT produce a working anonymous page. It yields four 403s and a Saxon-JS alert, because the chrome walks the ancestors and queries the endpoint. Adding the ancestors cleared most of it; the last 403 was a bare POST /sparql, which is what make-public's own #sparql-post authorization exists for. Hence two authorizations, kept apart so each scope is legible: the documents, and the endpoint at read and append. The endpoint grant is the floor, and it is worth stating plainly: /sparql enforces no per-graph ACL. With it in place an anonymous SELECT reads every graph in the dataspace, including documents whose HTTP representation is 403 - confirmed against a sentinel. That is the platform's behaviour and make-public grants the same thing, but it means the control proves only that no blanket DOCUMENT grant is in force, and the comment says so rather than claiming more. A cache purge after seeding. The negative control - delete the grant, expect the spec to fail - passed instead, because Varnish was still serving the HTML variant it had cached while the grant existed, and the suite reuses identical URLs every run. http-tests bans the whole cache before every test; seeding happens once here, so once is enough. Best-effort, so a dev pointed at a stack they do not run locally still gets a suite. And the spec that justifies all of it. The claim is not that an anonymous agent is refused - that is the default, and denial says nothing about rendering - but that one who IS allowed to read still gets none of the controls that write. Selectors measured by diffing both contexts against the same granted document, and asserted as presence rather than visibility, since every owner affordance is a menu item in a closed dropdown. The owner half of it reads the control document, which is the half the preflight cannot perform: it holds no certificate, so it cannot tell a locked document from a missing one. The anonymous project goes from nothing asserted to two tests. The 21 further skips that exist only because nothing could be granted before are left for their own commits. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Opening any document on an instance nobody had made public raised a modal alert carrying raw Saxon text - "Required cardinality of first argument of ac:document-uri() is exactly one; supplied value is empty". On the root document too, so it was the first thing an unauthorized visitor ever saw. ldh:TreeChildrenPlaceholder appends an li.tree-loading carrying no anchor at all, to be replaced when the children response lands. When that response is not a 200, ldh:tree-children-response emits a message and nothing else: it neither replaces the placeholder nor stops the chain, and ldh:tree-children-continue invokes the continuation regardless. ldh:doctree-descend-after-load then ran its predicate across a list whose only row had no @href, and ac:document-uri() of an empty sequence is a cardinality error against a param declared as item(). It escaped to ldh:promise-failure, which alerts whatever is not an ldh:HTTPError. A step predicate is the whole fix: a row with no anchor is not a child to descend into. Both sites take it, the one that raised and the one two branches up that is shielded only by its xsl:when. ldh:tree-node-uri was made empty-tolerant for exactly this and says so in its comment - "the rows this is asked about include the loading placeholder, which carries no anchor at all ... which a required cardinality turned into a runtime error mid-walk" - but substituting it here would be worse than the bug: it returns empty, and starts-with($uri, '') is true, so the placeholder would be SELECTED as the next node and fail a frame later on the same absent @href. Not an authorization bug, though that is how it was found. Reverting the fix fails the new spec for the owner as readily as for the anonymous reader: what matters is that the children query failed, not who asked it. Anonymity is merely the reliable way to be refused. Left alone deliberately: the placeholder is still never replaced on failure, so a refused children query spins "Loading..." forever. That is a UI concern and its own change, and the spec asserts nothing about the placeholder so fixing it will not break this. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
sparql-results and document-tree skipped the anonymous project for reasons that had stopped being true. "The endpoint grants acl:Read to authenticated agents; an anonymous request is a 401 and says nothing about rendering" - it is a 403, and the refusal is not the endpoint's own ACL but the dataspace granting nothing to anyone without a certificate. "The fixture container is owner-owned; these specs are not about authorization" - the second half is right and the first is not, since ownership is not what hides it. Both were true enough when the only anonymous state available was "refused everything". Now that the suite states the access it needs, neither spec has anything to skip for: they read a document and assert what is rendered, which is a claim about rendering in both projects. Four more anonymous tests, and nothing in either spec changed but the removal. The anonymous project now passes thirteen tests. It passed none before this week, which is what let a spec that forgot to skip fail in CI and nowhere else. concept-tree and concept-hierarchy still skip, and their reasons are sound as written - the taxonomy fixtures are granted to nobody. Reconsidered when they are. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…written down. "The taxonomy fixtures are owner-owned" was not true of anything. Nothing in a dataspace is readable without a certificate until an authorization says so, and a taxonomy document is a dh:Item created by ldh create item, indistinguishable from the fixture items the suite now grants. Granting these ten documents would make all fifteen tests run. The reason not to is the division of labour, which nothing had stated. WHETHER a refusal happens belongs to http-tests, where admin/acl/ covers the modes, the classes, the groups, the owner, and make-public against the real HTTP API; asserting it again through a browser is the same claim in a runner an order of magnitude slower. WHAT a refusal does to the client belongs here, because it is invisible to a suite that reads responses - a 403 can be correct at the HTTP layer while Saxon-JS raises an alert over it, which is precisely the defect fixed two commits ago, and tree-children-failure asserts it directly by injecting the refusal in both projects. Between those two, the concept tree renders from the hierarchy queries alone and the hierarchy blocks render what the query returns, identically for whoever may run it. A granted taxonomy would buy fifteen re-measurements of the same markup. The specs that DO differ by agent already run in both projects: affordances in anonymous-affordances, refusal in tree-children-failure. Comment and reason strings only; both specs still pass fifteen and skip fifteen, unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… not the one under the pointer. Dropping a block resolved the target document with ldh:base-uri(.), where the context item is whatever element the pointer was released over. A block that embeds another resource stamps @data-base-uri on its injected subtree, so that walk returned the EMBEDDED document whenever the release landed inside one - and on a dashboard that is most of the drop area: the northwind root carries eight such elements across five distinct bases, one of them the LDH ontology itself. The move then rewrote rdf:_N on that resource and addressed the PATCH to it, which for an external URI means going out through the Linked Data proxy (?uri=). Measured: PATCH /?uri=https%3A%2F%2Fw3id.org%2Fatomgraph%2Flinkeddatahub with the ontology as ?doc in the VALUES row, answered 502, the optimistic move reverted, and the reader got "Could not move block". Reordering was broken on any document containing an embedded block, which is what a content document usually is. $block is already constrained to a child of .content-body, so it carries no such stamp and names the document whose sequence is being reordered. Resolving from it is also what the surrounding code means - $document-body and $source-block are both resolved from $block for the same reason. Only a real drag reaches it. Dispatching the events on a chosen element lands outside the embedded subtree, so the synthetic regression check passed against the broken code; this was found by driving the drag through Chromium's own drag-and-drop processing model instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019EptZ6w7rd883q8N4CMorR
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.