Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,41 @@ All notable changes to Docara are recorded here. The format follows

## [Unreleased]

## [3.14.2] - 2026-11-11

### Fixed

- **An example frame takes the size ladder of its own width.** The page
sent every one of its design tokens to the frame, so a frame narrower
than the page kept the page's desktop sizes (40 px controls, 16 px text)
while its own width queries, density among them, read the phone side.
Tokens declared under a width query, tokens built from them and the
breakout width the Framework measures are now left to the frame's own
stylesheets: a 760 px frame on a 1440 px page draws 36 px controls and
14 px text. Colours and the other page tokens still reach the frame, and
a token the page sets on its root (the reader's UI radius) is sent as
written, so the frame resolves it against its own sizes.
- **An example frame settles when a toast is fixed to its bottom edge.** A
block or popover fixed to the frame's bottom moved down as far as the
frame grew, so the frame chased it (56 and 62 px about 40 times a second
on a toast placement example) and clipped the toast's top. Such a block
now counts with its own height and its gap to that edge: the frame grows
once and shows the whole toast.
- **An open menu in an example frame no longer pushes the page down.** A
date picker, menu or dialog that needs more room than the example grew
the frame and moved everything below it (118 to 496 px on a date and time
example), then moved it back on close. The frame now keeps its own height
in the page and shows the menu's room over what follows, as a menu does
on a page, with 12 px to spare below the menu instead of 8.
- **An open list no longer stretches its example frame without end.** The
frame added the rows a list scrolls inside its own popup, and a list
capped to the frame's height then got taller as the frame grew: a country
input with its list open reached a 9815 px frame. The frame now reaches
the menu's drawn box only.
- **The Framework preloader stays off in example frames.** The page turns
it off in its preference script, which a frame does not get, so a slow
load covered a finished example with the preloader.

## [3.14.1] - 2026-11-10

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.14.1
3.14.2
430 changes: 215 additions & 215 deletions resources/component-catalog/source-metadata.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions resources/portable/declarative-shell.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 44 additions & 7 deletions resources/portable/declarative-shell.js
Original file line number Diff line number Diff line change
Expand Up @@ -489,27 +489,44 @@
return content===null?null:portableExampleStyle(content,resolved.href,codepoints,faces);
});
}
function collectExampleTokenNames(rules,names){
/* The frame gets the page's design tokens, except those that follow the viewport width: the Framework switches its size ladder (text, control heights, spacing, containers) with width media queries, and a frame narrower than the page must take the ladder of its own width. A token declared under a width query, or declared from such a token, is left to the frame's own stylesheets; a token the page sets on its root element itself (a reader setting such as the UI radius) is sent as written, so the frame resolves it against its own ladder. */
function collectExampleTokenNames(rules,names,declared,widthScoped,inWidth){
Array.from(rules||[]).forEach(function(rule){
var scoped=inWidth||Boolean(rule.media&&/width/i.test(rule.media.mediaText||''));
if(rule.style){
for(var index=0;index<rule.style.length;index++){
var name=rule.style[index];
if(typeof name==='string'&&name.indexOf('--sf-')===0&&/^--sf-[a-z0-9_\\/.-]+$/.test(name)){names.add(name)}
if(typeof name==='string'&&name.indexOf('--sf-')===0&&/^--sf-[a-z0-9_\\/.-]+$/.test(name)){
names.add(name);
if(scoped)widthScoped.add(name);
if(!declared[name])declared[name]=[];
declared[name].push(rule.style.getPropertyValue(name));
}
}
}
try{if(rule.cssRules){collectExampleTokenNames(rule.cssRules,names)}}catch(error){}
try{if(rule.cssRules){collectExampleTokenNames(rule.cssRules,names,declared,widthScoped,scoped)}}catch(error){}
});
}
function exampleDesignTokens(){
var computed=getComputedStyle(document.documentElement),names=new Set(['--sf-text--size','--sf-text--height']);
var root=document.documentElement,computed=getComputedStyle(root),names=new Set(['--sf-text--size','--sf-text--height']),declared=Object.create(null),widthScoped=new Set();
for(var index=0;index<computed.length;index++){
var name=computed[index];
if(typeof name==='string'&&name.indexOf('--sf-')===0&&/^--sf-[a-z0-9_\\/.-]+$/.test(name)){names.add(name)}
}
Array.from(document.styleSheets).forEach(function(sheet){try{collectExampleTokenNames(sheet.cssRules,names)}catch(error){}});
Array.from(document.styleSheets).forEach(function(sheet){try{collectExampleTokenNames(sheet.cssRules,names,declared,widthScoped,Boolean(sheet.media&&/width/i.test(sheet.media.mediaText||'')))}catch(error){}});
var changed=true;
while(changed){
changed=false;
Object.keys(declared).forEach(function(name){
if(widthScoped.has(name))return;
if(declared[name].some(function(value){var reference=/var\(\s*(--sf-[a-z0-9_\\/.-]+)/gi,match;while((match=reference.exec(value))!==null){if(widthScoped.has(match[1]))return true}return false})){widthScoped.add(name);changed=true}
});
}
var tokens=Object.create(null);
Array.from(names).sort().slice(0,4096).forEach(function(name){
var value=computed.getPropertyValue(name).trim();
/* A breakout or viewport width the Framework measures on the page is the page's own; the frame measures its own. */
if(/breakout|viewport/.test(name))return;
var own=root.style.getPropertyValue(name).trim(),value=own||(widthScoped.has(name)?'':computed.getPropertyValue(name).trim());
if(!value||value.length>512||/url\s*\(|@import|expression\s*\(/i.test(value))return;
tokens[name]=value;
});
Expand Down Expand Up @@ -638,7 +655,27 @@
var frame=docaraExampleFrames.find(function(candidate){return candidate.contentWindow===event.source})||null;
var height=Number(event.data.height);
if(!frame||!Number.isFinite(height))return;
frame.style.blockSize=Math.max(32,Math.ceil(height))+'px';
var size=Math.max(32,Math.ceil(height)),flow=Number(event.data.flow);
var boxes=Array.isArray(event.data.overlays)?event.data.overlays.filter(function(box){return Array.isArray(box)&&box.length===4&&box.every(Number.isFinite)}):[];
var extra=Number.isFinite(flow)&&boxes.length?Math.max(0,size-Math.max(32,Math.ceil(flow))):0;
var preview=frame.closest('.docara-example-preview');
frame.style.blockSize=size+'px';
/* An open menu's room hangs below the frame's flow height over what follows, as a menu does on a page: the page keeps its place, and only the frame's flow box and the menus stay visible and clickable. */
if(extra>0){
var width=frame.clientWidth,flowSize=size-extra;
var path='M0 0H'+width+'V'+flowSize+'H0Z'+boxes.map(function(box){return 'M'+box[0]+' '+box[1]+'H'+box[2]+'V'+box[3]+'H'+box[0]+'Z'}).join('');
frame.style.marginBlockEnd=(-extra)+'px';
frame.style.position='relative';
frame.style.zIndex='5';
frame.style.clipPath="path('"+path+"')";
if(preview)preview.setAttribute('data-docara-example-overlay','');
}else{
frame.style.marginBlockEnd='';
frame.style.position='';
frame.style.zIndex='';
frame.style.clipPath='';
if(preview)preview.removeAttribute('data-docara-example-overlay');
}
});
docaraExampleFrames.forEach(function(frame){
frame.addEventListener('load',function(){requestExampleHeight(frame)});
Expand Down
31 changes: 23 additions & 8 deletions src/PortableSite/PortableMarkdownRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -1527,6 +1527,8 @@ function applyEnvironment(data){
document.head.appendChild(script);
currentBoot.push(item.key);
});
/* The page turns the Framework preloader off in its own preference script, which a frame does not get: without it a slow load covers the finished example with the preloader. */
window.SF_BOOT_CONFIG=window.SF_BOOT_CONFIG||{};window.SF_BOOT_CONFIG.preloader={enabled:false};
/* The page's boot state turns the Loader's theme off (Docara resolves the page theme itself); an example with its own theme keeps the Loader's theme API, which then works on this frame alone. */
if(ownTheme&&window.SF_BOOT_CONFIG&&window.SF_BOOT_CONFIG.theme===false){delete window.SF_BOOT_CONFIG.theme}
var scriptsLoaded=loadFrameworkScripts(Array.isArray(data.scripts)?data.scripts:[]);
Expand Down Expand Up @@ -1561,22 +1563,34 @@ function measure(){
});
}
/* Offscreen sandboxed frames get no animation frames, so timers measure directly to size them before they scroll into view. */
var layerPlaces=typeof WeakMap==='function'?new WeakMap():{get:function(){},set:function(){}};
/* Typed OM tells a layer whose top is auto and bottom is set; elsewhere a layer that moves by exactly the frame's growth is taken as pinned. */
/* A notification area is capped at the frame's height and squeezes its messages in a short frame; without the cap it shows the height it needs. The cap comes back before anything paints, and the observer drops the change. */
function naturalBlockSize(layer){var value=layer.style.getPropertyValue('max-block-size'),priority=layer.style.getPropertyPriority('max-block-size'),size;layer.style.setProperty('max-block-size','none','important');size=Math.max(layer.getBoundingClientRect().height,layer.scrollHeight);if(value)layer.style.setProperty('max-block-size',value,priority);else layer.style.removeProperty('max-block-size');if(bodyObserver)bodyObserver.takeRecords();return size}
function bottomPinned(layer){try{var map=layer.computedStyleMap();return map.get('position').toString()==='fixed'&&map.get('top').toString()==='auto'&&map.get('bottom').toString()!=='auto'}catch(error){return false}}
function measureNow(){
var style=getComputedStyle(body),rect=body.getBoundingClientRect();
var marginTop=parseFloat(style.marginTop)||0,marginBottom=parseFloat(style.marginBottom)||0;
var contentTop=rect.top-marginTop,contentBottom=rect.bottom+marginBottom;
var contentTop=rect.top-marginTop,contentBottom=rect.bottom+marginBottom,pinnedExtent=0;
Array.from(body.children).forEach(function(child){
/* An open popover or dialog is measured with the top layer below. */
var open=false;try{open=child.matches(':popover-open,dialog[open]')}catch(error){}if(open)return;
var childRect=child.getBoundingClientRect(),childStyle=getComputedStyle(child);
/* A block fixed to the frame's bottom edge, such as a toast container, moves down as far as the frame grows: it counts with its own height and its gap to that edge, and an empty one not at all. */
if(childStyle.position==='fixed'&&bottomPinned(child)){if(childRect.height>0)pinnedExtent=Math.max(pinnedExtent,Math.max(0,innerHeight-childRect.bottom)+naturalBlockSize(child));return}
contentTop=Math.min(contentTop,childRect.top-(parseFloat(childStyle.marginTop)||0));
contentBottom=Math.max(contentBottom,childRect.bottom+(parseFloat(childStyle.marginBottom)||0));
});
contentBottom=Math.max(contentBottom,contentTop+pinnedExtent);
/* An open menu or dialog sits in the top layer, which a frame clips at its own height, and a positioned menu shrinks to the room it has. The frame grows to the layer's full height, so the menu gets its natural size: the part a scrolling list hides counts too. */
var layers=[];try{layers=Array.from(document.querySelectorAll(':popover-open,dialog[open]'))}catch(error){}
layers.forEach(function(layer){var layerRect=layer.getBoundingClientRect(),hidden=Math.max(0,layer.scrollHeight-layer.clientHeight);Array.prototype.forEach.call(layer.querySelectorAll('*'),function(part){var overflow=getComputedStyle(part).overflowY;if(overflow==='auto'||overflow==='scroll')hidden=Math.max(hidden,part.scrollHeight-part.clientHeight)});contentBottom=Math.max(contentBottom,layerRect.bottom+hidden+8)});
var height=Math.ceil(Math.max(0,contentBottom-contentTop));
if(height===lastHeight)return;
lastHeight=height;
parent.postMessage({type:'docara:example-height',height:height},'*');
var layers=[],flowBottom=contentBottom,overlays=[];try{layers=Array.from(document.querySelectorAll(':popover-open,dialog[open]'))}catch(error){}
/* A layer pinned to the frame's bottom edge, such as a notification area, moves down as far as the frame grows: it needs its own height and its gap to that edge, not room below its bottom, or the frame would chase it. */
layers.forEach(function(layer){var layerRect=layer.getBoundingClientRect();var seen=layerPlaces.get(layer),viewport=innerHeight;if(!seen){seen={pinned:bottomPinned(layer)};layerPlaces.set(layer,seen)}else if(seen.viewport!==viewport&&Math.abs((layerRect.top-seen.top)-(viewport-seen.viewport))<1)seen.pinned=true;seen.viewport=viewport;seen.top=layerRect.top;if(seen.pinned){contentBottom=Math.max(contentBottom,contentTop+Math.max(0,viewport-layerRect.bottom)+naturalBlockSize(layer)+12);return}var reach=layerRect.bottom+12;if(reach>contentBottom)contentBottom=reach;overlays.push([Math.floor(layerRect.left-12),Math.floor(layerRect.top-contentTop-12),Math.ceil(layerRect.right+12),Math.ceil(reach-contentTop)])});
var height=Math.ceil(Math.max(0,contentBottom-contentTop)),flow=Math.ceil(Math.max(0,flowBottom-contentTop)),report=height+'/'+flow+'/'+overlays.join(';');
if(report===lastHeight)return;
lastHeight=report;
/* The page lays the frame out at its flow height and shows an open menu's room over what follows, so the menu does not push the page down. */
parent.postMessage({type:'docara:example-height',height:height,flow:flow,overlays:overlays},'*');
}
function measureSettled(){
measure();
Expand Down Expand Up @@ -1617,7 +1631,8 @@ function scheduleIconReport(){if(iconScan===null)iconScan=setTimeout(reportIcons
});
if(typeof ResizeObserver==='function')new ResizeObserver(measure).observe(body);
if(typeof ResizeObserver==='function')new ResizeObserver(measure).observe(document.documentElement);
if(typeof MutationObserver==='function')new MutationObserver(measure).observe(body,{childList:true,subtree:true,attributes:true,characterData:true});
var bodyObserver=null;
if(typeof MutationObserver==='function'){bodyObserver=new MutationObserver(measure);bodyObserver.observe(body,{childList:true,subtree:true,attributes:true,characterData:true})}
if(document.fonts&&document.fonts.ready)document.fonts.ready.then(measureSettled);
measureSettled();
/* A fresh frame document holds nothing yet; this asks the shell for the whole environment. */
Expand Down
7 changes: 4 additions & 3 deletions tests/PortableDocumentationSiteTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -213,16 +213,17 @@ public function real_documentation_build_matches_the_exact_product_matrix_and_st
self::assertStringContainsString('localizeCodeCopy', $shellJs);
self::assertStringContainsString('showCopyState(false);', $shellJs);
self::assertStringContainsString("event.data.type!=='docara:example-height'", $shellJs);
self::assertStringContainsString("frame.style.blockSize=Math.max(32,Math.ceil(height))+'px'", $shellJs);
self::assertStringContainsString("frame.style.blockSize=size+'px'", $shellJs);
self::assertStringContainsString("frame.style.clipPath=\"path('\"+path+\"')\"", $shellJs);
self::assertStringContainsString('link[data-docara-framework-asset][rel="stylesheet"]', $shellJs);
self::assertStringContainsString('return exampleEnvironment(frame).then(function(environment)', $shellJs);
self::assertStringContainsString('script[data-docara-framework-asset="simai.framework.boot"]:not([src])', $shellJs);
self::assertStringContainsString('script[data-docara-framework-asset="docara.framework.storage.compatibility"]:not([src])', $shellJs);
self::assertStringContainsString('script[data-docara-framework-asset="simai.framework.preloaded"]:not([src])', $shellJs);
self::assertStringNotContainsString("!=='simai.framework.core.js'", $shellJs);
self::assertStringContainsString('function exampleDesignTokens()', $shellJs);
self::assertStringContainsString('function collectExampleTokenNames(rules,names)', $shellJs);
self::assertStringContainsString('collectExampleTokenNames(sheet.cssRules,names)', $shellJs);
self::assertStringContainsString('function collectExampleTokenNames(rules,names,declared,widthScoped,inWidth)', $shellJs);
self::assertStringContainsString('collectExampleTokenNames(sheet.cssRules,names,declared,widthScoped,', $shellJs);
self::assertStringContainsString('designTokens:exampleDesignTokens()', $shellJs);
self::assertStringContainsString('rootFontSize:getComputedStyle(document.documentElement).fontSize', $shellJs);
self::assertStringContainsString("addEventListener('resize',function()", $shellJs);
Expand Down
2 changes: 1 addition & 1 deletion tests/Unit/DocumentationContractTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ public function beginner_path_ends_with_a_verified_http_preview_contract(): void
public function portable_installation_surfaces_use_the_stable_package_after_release(): void
{
self::assertSame(
'3.14.1',
'3.14.2',
trim((string) file_get_contents($this->repositoryRoot() . '/VERSION')),
);

Expand Down
Loading
Loading