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
59 changes: 55 additions & 4 deletions Classes/Controller/FrontendStudioModuleController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Andersundsehr\FrontendStudio\Controller;

use Andersundsehr\FrontendStudio\Service\ComponentMetadataProvider;
use Andersundsehr\FrontendStudio\Service\ComponentDocumentation;
use Andersundsehr\FrontendStudio\Service\ComponentPreviewRenderer;
use Andersundsehr\FrontendStudio\Service\FluidUsageSnippetRenderer;
use Andersundsehr\FrontendStudio\Service\HtmlSourceHighlighter;
Expand Down Expand Up @@ -48,11 +49,21 @@ public function __construct(
private Typo3Version $typo3Version,
private AssetCollector $assetCollector,
private FluidTemplateAnalyzer $fluidTemplateAnalyzer,
private ?ComponentDocumentation $documentation = null,
) {
}

public function handleRequest(ServerRequestInterface $request): ResponseInterface
{
$identifier = $request->getQueryParams()['component'] ?? null;
if (is_string($identifier) && $identifier !== '') {
$moduleTemplate = $this->moduleTemplateFactory->create($request);
$moduleTemplate->setTitle($identifier . ' — Docs');
$moduleTemplate->getDocHeaderComponent()->disable();
$moduleTemplate->assignMultiple($this->getOverviewAssignments($identifier, $request));
return $moduleTemplate->renderResponse('FrontendStudio/Overview');
}

return $this->renderVariantViewResponse($request);
}

Expand All @@ -67,6 +78,46 @@ public function templateAction(ServerRequestInterface $request): ResponseInterfa
return new JsonResponse($this->fluidTemplateAnalyzer->analyze($metadata?->template));
}

/** @return array<string, mixed> */
private function getOverviewAssignments(string $identifier, ServerRequestInterface $request): array
{
$component = $this->componentMetadataProvider->getComponentMetadataForIdentifier($identifier);
$variants = $component?->fixture->variants ?? [];
$firstIdentifier = $variants !== [] ? $identifier . ':' . $variants[0]->name : '';
$assignments = $this->getVariantAssignments($firstIdentifier, $request, false);
$previews = [];
foreach ($variants as $index => $variant) {
$variantIdentifier = $identifier . ':' . $variant->name;
$context = $this->getPreviewContext($variantIdentifier, $request);
$previews[] = [
'identifier' => $variantIdentifier,
'name' => $variant->name,
'first' => $index === 0,
'index' => $index,
'previewUri' => $context['componentPreviewUri'],
'variantUri' => (string)$this->uriBuilder->buildUriFromRoute('admin_frontendstudio', [
'componentVariant' => $variantIdentifier,
'site' => $context['selectedSiteIdentifier'],
'language' => $context['selectedLanguageHreflang'],
]),
];
}

$markdown = '';
$documentationError = '';
if ($component !== null && $this->documentation !== null) {
try {
$markdown = $this->documentation->read($identifier)['markdown'];
} catch (Throwable $throwable) {
$documentationError = 'Documentation could not be loaded: ' . $throwable->getMessage();
}
}

return [...$assignments, 'overviewComponent' => $component, 'overviewVariants' => $previews,
'documentationMarkdown' => $markdown, 'documentationError' => $documentationError,
'showContentNavigationToggle' => $this->typo3Version->getMajorVersion() >= 14];
}

private function renderVariantViewResponse(ServerRequestInterface $request): ResponseInterface
{
$moduleTemplate = $this->moduleTemplateFactory->create($request);
Expand All @@ -89,11 +140,11 @@ private function renderVariantViewResponse(ServerRequestInterface $request): Res
/**
* @return array<string, mixed>
*/
private function getVariantAssignments(string $selectedVariantIdentifier, ServerRequestInterface $request): array
private function getVariantAssignments(string $selectedVariantIdentifier, ServerRequestInterface $request, bool $renderSources = true): array
{
$selectedComponentMetadata = $this->componentMetadataProvider->getComponentMetadataForVariantIdentifier($selectedVariantIdentifier);
$previewContext = $this->getPreviewContext($selectedVariantIdentifier, $request);
[$renderedHtmlSource, $renderedHtmlStatus] = $selectedComponentMetadata !== null
[$renderedHtmlSource, $renderedHtmlStatus] = $renderSources && $selectedComponentMetadata !== null
? $this->renderInitialHtmlSource($selectedVariantIdentifier, $request)
: ['', ''];

Expand All @@ -110,8 +161,8 @@ private function getVariantAssignments(string $selectedVariantIdentifier, Server
'variantActiveTab' => $this->getVariantActiveTab($GLOBALS['BE_USER']->uc ?? []),
'renderedHtmlSource' => $renderedHtmlSource,
'renderedHtmlStatus' => $renderedHtmlStatus,
'fluidTemplateAnalysis' => $this->fluidTemplateAnalyzer->analyze($selectedComponentMetadata?->template),
'fluidUsageSource' => $this->fluidUsageSnippetRenderer->render($selectedComponentMetadata),
'fluidTemplateAnalysis' => $this->fluidTemplateAnalyzer->analyze($renderSources ? $selectedComponentMetadata?->template : null),
'fluidUsageSource' => $renderSources ? $this->fluidUsageSnippetRenderer->render($selectedComponentMetadata) : [],
'componentChangeStreamUri' => Environment::getContext()->isDevelopment()
? (string)$this->uriBuilder->buildUriFromRoute('ajax_frontend_studio_component_change_stream')
: '',
Expand Down
7 changes: 2 additions & 5 deletions Classes/Middleware/ComponentPreviewMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -279,11 +279,8 @@ private function wrapHtml(string $title, string $body): string
body {
margin: 5px;

background: linear-gradient(45deg, rgba(0, 0, 0, 0.098) 25%, transparent 25%, transparent 75%, rgba(0, 0, 0, 0.098) 75%, rgba(0, 0, 0, 0.098) 0), linear-gradient(45deg, rgba(0, 0, 0, 0.098) 25%, transparent 25%, transparent 75%, rgba(0, 0, 0, 0.098) 75%, rgba(0, 0, 0, 0.098) 0), white;
background-repeat: repeat, repeat;
background-position: 0 0, 5px 5px;
background-clip: border-box, border-box;
background-size: 10px 10px, 10px 10px;
color-scheme: light dark;
background: repeating-conic-gradient(light-dark(#fff, #ddd) 0% 25%, light-dark(#fff, #fff) 0% 50%) 0 0 / 10px 10px;
transition: none;
transform: scaleX(1) scaleY(1) scaleZ(1);
}
Expand Down
18 changes: 14 additions & 4 deletions Classes/Service/ComponentMetadataProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,20 +63,30 @@ public function getComponentTemplateForIdentifier(string $identifier): ?Componen
: null;
}

public function getComponentMetadataForIdentifier(string $componentIdentifier): ?ComponentMetadata
{
return $this->getComponentMetadata($componentIdentifier, true);
}

public function getComponentMetadataForVariantIdentifier(string $variantIdentifier): ?ComponentMetadata
{
return $this->getComponentMetadata($variantIdentifier, false);
}

private function getComponentMetadata(string $variantIdentifier, bool $overview): ?ComponentMetadata
{
$variantIdentifier = trim($variantIdentifier);
if ($variantIdentifier === '') {
return null;
}

$identifierParts = explode(':', $variantIdentifier, 3);
if (count($identifierParts) !== 3) {
if (count($identifierParts) !== ($overview ? 2 : 3)) {
return null;
}

[$namespace, $componentName, $variantName] = $identifierParts;
if ($namespace === '' || $componentName === '' || $variantName === '') {
[$namespace, $componentName, $variantName] = [...$identifierParts, ''];
if ($namespace === '' || $componentName === '' || (!$overview && $variantName === '')) {
return null;
}

Expand Down Expand Up @@ -124,7 +134,7 @@ public function getComponentMetadataForVariantIdentifier(string $variantIdentifi
$fixture = $this->getFixtureMetadata($resolverDelegate, $componentName, $variantName);
if ($fixture->error !== null) {
$errors[] = 'Fixture file could not be loaded: ' . $fixture->error;
} elseif ($fixture->variants !== [] && $fixture->selectedVariant === null) {
} elseif (!$overview && $fixture->variants !== [] && $fixture->selectedVariant === null) {
$errors[] = 'Selected variant was not found in the fixture file.';
}

Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,25 @@ Document what another frontend developer needs to use the component:
- Available variants and when to choose each one.
- Accessibility requirements, expected interactions, and usage limitations.

### Component Docs overview

Selecting a component in the tree opens its Docs overview; selecting an individual variant still opens its inspector.
The overview shows the shared component Markdown, then the first variant in fixture declaration order with its existing Controls beneath it.
Outside Production, edit the documentation directly in the overview and save it with **Save**.
The same documentation is available in each variant's **Documentation** tab; Production displays the rendered text.
Other variants appear as separate previews with links to their inspectors.
Live controls affect only the first preview; the remaining examples keep their stored fixture values.
Long documentation starts collapsed with a fade and an Expand documentation button.
Short documentation remains fully visible; expanded documentation can be collapsed again.
Focusing the documentation expands it for editing.
The overview preserves the current site and language context without showing selectors.
Controls provide temporary live edits; fixture save and reset actions remain in the variant inspector.

A long first preview can be expanded or collapsed. Preview frames resize when content, images, fonts or layout change;
when measurement is unavailable, a scrolling frame and variant link remain available.
Lower previews load lazily. A component without fixtures displays an empty state without creating files.
Production retains temporary live controls while the existing write restrictions continue to apply.

## Component Fixtures

Frontend Studio creates and updates fixture files next to component templates.
Expand Down
55 changes: 29 additions & 26 deletions Resources/Private/Components/Variant/Controls/Controls.fluid.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
>
<f:argument name="selectedComponentMetadata" type="Andersundsehr\FrontendStudio\Dto\ComponentMetadata"/>
<f:argument name="variantActiveTab" type="string"/>
<f:argument name="showActions" type="bool" default="true"/>
<f:asset.css identifier="frontend-studio-variant-view" href="EXT:frontend_studio/Resources/Public/Css/Backend/variant-view.css"/>
<f:asset.module identifier="@andersundsehr/frontend-studio/backend/variant-controls.js"/>

Expand Down Expand Up @@ -47,32 +48,34 @@
</div>
</f:then>
<f:else>
<div class="frontend-studio-variant-values-actions">
<frontend.studio:variant.resetButton data="{frontend-studio-variant-reset: ''}"/>
<f:if condition="{selectedComponentMetadata.readOnly}">
<f:then>
<span class="text-body-secondary">Production: fixture files are read-only.</span>
</f:then>
<f:else>
<button
type="button"
class="btn btn-default btn-sm"
data-frontend-studio-variant-copy
>
<core:icon identifier="actions-edit-copy"/>
Save as new
</button>
<frontend.studio:variant.saveButton data="{frontend-studio-variant-save: ''}"/>
<span
class="frontend-studio-variant-save-state"
data-frontend-studio-variant-save-state
hidden
>
Unsaved changes
</span>
</f:else>
</f:if>
</div>
<f:if condition="{showActions}">
<div class="frontend-studio-variant-values-actions">
<frontend.studio:variant.resetButton data="{frontend-studio-variant-reset: ''}"/>
<f:if condition="{selectedComponentMetadata.readOnly}">
<f:then>
<span class="text-body-secondary">Production: fixture files are read-only.</span>
</f:then>
<f:else>
<button
type="button"
class="btn btn-default btn-sm"
data-frontend-studio-variant-copy
>
<core:icon identifier="actions-edit-copy"/>
Save as new
</button>
<frontend.studio:variant.saveButton data="{frontend-studio-variant-save: ''}"/>
<span
class="frontend-studio-variant-save-state"
data-frontend-studio-variant-save-state
hidden
>
Unsaved changes
</span>
</f:else>
</f:if>
</div>
</f:if>
<f:if condition="{selectedComponentMetadata.fixture.selectedVariant.values} || {selectedComponentMetadata.fixture.selectedVariant.slots}">
<f:then>
<div class="panel-group mb-0">
Expand Down
102 changes: 102 additions & 0 deletions Resources/Private/Templates/FrontendStudio/Overview.fluid.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
xmlns:frontend.studio="http://typo3.org/ns/Andersundsehr/FrontendStudio/Components" data-namespace-typo3-fluid="true">
<f:if condition="{showContentNavigationToggle}">
<f:asset.module identifier="@typo3/backend/viewport/content-navigation-toggle.js"/>
</f:if>
<f:asset.module identifier="@andersundsehr/frontend-studio/backend/component-overview.js"/>
<f:asset.script identifier="frontend-studio-component-tree-startup" src="EXT:frontend_studio/Resources/Public/JavaScript/Backend/component-tree-startup.js" type="module"/>
<f:asset.css identifier="frontend-studio-markdown" href="EXT:frontend_studio/Resources/Public/Css/Backend/component-markdown.css"/>
<f:asset.css identifier="frontend-studio-documentation" href="EXT:frontend_studio/Resources/Public/Css/Backend/component-documentation.css"/>
<f:asset.css identifier="frontend-studio-overview" href="EXT:frontend_studio/Resources/Public/Css/Backend/component-overview.css"/>
<div class="frontend-studio-overview" data-component-overview data-frontend-studio-variant-view
data-component-identifier="{overviewComponent.componentIdentifier}"
data-variant-identifier="{selectedVariantIdentifier}" data-preview-uri="{componentPreviewUri}"
data-component-change-stream-uri="{componentChangeStreamUri}"
data-selected-site-identifier="{selectedSiteIdentifier}" data-selected-language-hreflang="{selectedLanguageHreflang}">
<f:if condition="{overviewComponent}">
<f:then>
<header class="frontend-studio-overview-header">
<div class="d-flex align-items-center gap-2">
<f:if condition="{showContentNavigationToggle}">
<typo3-backend-content-navigation-toggle class="btn btn-sm btn-icon btn-default" action="expand" hidden></typo3-backend-content-navigation-toggle>
</f:if>
<h1>{overviewComponent.componentName}</h1>
</div>
<p class="frontend-studio-overview-identifier text-body-secondary"><code>{overviewComponent.componentIdentifier}</code></p>
</header>
<f:if condition="{documentationError}"><p role="alert">{documentationError}</p></f:if>
<f:if condition="{overviewComponent.readOnly} == false && {overviewComponent.template.documentationPath}">
<f:then>
<section class="frontend-studio-documentation" data-component-documentation
data-component-identifier="{overviewComponent.componentIdentifier}"
data-doc-uri="{f:be.uri(route: 'ajax_frontend_studio_component_documentation')}">
<div class="frontend-studio-variant-values-actions" data-doc-actions>
<frontend.studio:variant.resetButton data="{doc-reset: ''}"/>
<frontend.studio:variant.saveButton data="{doc-save: ''}"/>
<span class="frontend-studio-variant-save-state" data-doc-save-state hidden>Unsaved changes</span>
<div data-doc-editor-controls></div>
</div>
<div data-overview-description id="overview-description">
<div class="frontend-studio-markdown" data-overview-description-content data-doc-rich><pre data-overview-markdown>{documentationMarkdown}</pre></div>
</div>
<button type="button" class="btn btn-default btn-sm frontend-studio-overview-description-toggle"
data-overview-description-toggle aria-expanded="false" aria-controls="overview-description" hidden>Expand documentation</button>
<p class="small text-body-secondary" data-doc-file>
Documentation file: <code>{overviewComponent.template.documentationPath}</code>.
Documentation is shared by all variants of this component.
</p>
<p data-doc-status role="status" aria-live="polite"></p>
</section>
</f:then>
<f:else>
<f:if condition="{documentationMarkdown}">
<f:then>
<div data-overview-description id="overview-description">
<div class="frontend-studio-markdown" data-overview-description-content><pre data-overview-markdown>{documentationMarkdown}</pre></div>
</div>
<button type="button" class="btn btn-default btn-sm frontend-studio-overview-description-toggle"
data-overview-description-toggle aria-expanded="false" aria-controls="overview-description" hidden>Expand documentation</button>
</f:then>
<f:else><p class="text-body-secondary">No documentation yet.</p></f:else>
</f:if>
</f:else>
</f:if>
<f:if condition="{overviewComponent.errors}">
<ul class="alert alert-warning"><f:for each="{overviewComponent.errors}" as="error"><li>{error}</li></f:for></ul>
</f:if>
<f:if condition="{overviewVariants}">
<f:then>
<f:for each="{overviewVariants}" as="variant">
<section class="frontend-studio-overview-example" aria-labelledby="overview-variant-{variant.index}">
<h2 id="overview-variant-{variant.index}"><a href="{variant.variantUri}" data-overview-variant-link>{variant.name}</a></h2>
<f:if condition="{variant.previewUri}">
<f:then>
<div class="frontend-studio-overview-frame{f:if(condition: variant.first, then: ' is-first')}" data-overview-frame-container>
<iframe id="overview-frame-{variant.index}" src="{variant.previewUri}" title="{variant.name} component preview"
data-overview-frame data-variant-identifier="{variant.identifier}"
loading="{f:if(condition: variant.first, then: 'eager', else: 'lazy')}"
{f:if(condition: variant.first, then: 'data-frontend-studio-variant-frame')}></iframe>
</div>
<p data-overview-frame-status class="text-body-secondary" role="status"></p>
<f:if condition="{variant.first}">
<button type="button" class="btn btn-default btn-sm" data-overview-expand aria-expanded="false" aria-controls="overview-frame-{variant.index}" hidden>Expand preview</button>
</f:if>
</f:then>
<f:else><p>The rendered component preview is not available. Configure a preview site and language.</p></f:else>
</f:if>
<f:if condition="{variant.first}">
<div class="frontend-studio-overview-controls">
<h3>Controls</h3>
<frontend.studio:variant.controls selectedComponentMetadata="{selectedComponentMetadata}" variantActiveTab="values" showActions="{false}"/>
</div>
</f:if>
</section>
</f:for>
</f:then>
<f:else><p>No fixture variants yet. Use Create variant in the component tree to add an example.</p></f:else>
</f:if>
</f:then>
<f:else><h1>Component not found</h1><p>Select a registered component from the tree.</p></f:else>
</f:if>
</div>
</html>
Loading
Loading