├── Resources ├── Public │ ├── Css │ │ ├── .gitkeep │ │ ├── Iframe.css │ │ └── DemoComponents.css │ ├── Javascript │ │ └── .gitkeep │ ├── Build │ │ ├── .vite │ │ │ └── manifest.json │ │ └── JavaScript │ │ │ └── Chunks │ │ │ └── index-CTDsMjWk.js │ └── Icons │ │ └── Extension.svg └── Private │ ├── DemoComponents │ ├── Atom │ │ ├── Image │ │ │ ├── Image.fixture.json │ │ │ └── Image.html │ │ └── Button │ │ │ ├── Button.fixture.json │ │ │ └── Button.html │ └── Molecule │ │ └── Teaser │ │ ├── Teaser.html │ │ └── Teaser.fixture.json │ ├── Components │ ├── Atom │ │ ├── StyleguideScrollTop │ │ │ ├── StyleguideScrollTop.html │ │ │ ├── StyleguideScrollTop.js │ │ │ └── StyleguideScrollTop.scss │ │ ├── StyleguideRefreshIFrame │ │ │ ├── StyleguideRefreshIFrame.html │ │ │ ├── StyleguideRefreshIFrame.js │ │ │ └── StyleguideRefreshIFrame.scss │ │ ├── StyleguideSelect │ │ │ ├── StyleguideSelect.html │ │ │ ├── StyleguideSelect.js │ │ │ └── StyleguideSelect.scss │ │ ├── LanguageNavigation │ │ │ ├── LanguageNavigation.html │ │ │ ├── LanguageNavigation.js │ │ │ └── LanguageNavigation.scss │ │ └── ViewportNavigation │ │ │ ├── ViewportNavigation.html │ │ │ ├── ViewportNavigation.scss │ │ │ └── ViewportNavigation.js │ ├── Molecule │ │ └── EditFixtures │ │ │ ├── EditFixtures.js │ │ │ ├── EditFixtures.scss │ │ │ └── EditFixtures.html │ └── Organism │ │ └── StyleguideToolbar │ │ ├── StyleguideToolbar.js │ │ ├── StyleguideToolbar.scss │ │ └── StyleguideToolbar.html │ ├── Scss │ ├── StyleguideShow.scss │ ├── _Variables.scss │ ├── Styleguide.scss │ ├── StyleguideHeader.scss │ └── StyleguideList.scss │ ├── Javascript │ ├── Iframe.js │ ├── Utils.js │ ├── Polyfills.js │ └── Styleguide.js │ ├── Templates │ └── Styleguide │ │ ├── Component.html │ │ ├── List.html │ │ └── Show.html │ ├── Layouts │ ├── Styleguide.html │ └── Iframe.html │ └── Partials │ └── Ruler.html ├── .browserslistrc ├── .yarn └── install-state.gz ├── Classes ├── Exception │ ├── InvalidAssetException.php │ └── RequiredComponentArgumentException.php ├── Event │ ├── PreProcessComponentViewEvent.php │ ├── AfterConfigurationLoadedEvent.php │ └── PostProcessComponentViewEvent.php ├── Domain │ ├── Model │ │ ├── ComponentFixture.php │ │ ├── ComponentLocation.php │ │ ├── ComponentName.php │ │ ├── Package.php │ │ └── Component.php │ └── Repository │ │ ├── ComponentNameRepository.php │ │ ├── PackageRepository.php │ │ └── ComponentRepository.php ├── Factory │ └── Component │ │ ├── ComponentFactoryInterface.php │ │ └── ComponentFactory.php ├── ViewHelpers │ ├── Format │ │ └── MarkdownViewHelper.php │ ├── Uri │ │ └── StyleguideViewHelper.php │ └── Component │ │ └── ExampleViewHelper.php ├── EventListener │ ├── AssetCollectorExtensionInjector.php │ └── AssetCollectorInjector.php ├── Service │ ├── ComponentDownloadService.php │ └── StyleguideConfigurationManager.php ├── Middleware │ └── StyleguideRouter.php └── Controller │ └── StyleguideController.php ├── ext_conf_template.txt ├── .gitignore ├── ext_localconf.php ├── Configuration ├── RequestMiddlewares.php ├── Services.yaml └── Yaml │ └── FluidStyleguide.yaml ├── vitesse.config.json ├── package.json ├── conventional.config.json ├── Build ├── FunctionalTestsBootstrap.php ├── FunctionalTests.xml ├── UnitTests.xml ├── UnitTestsBootstrap.php └── Scripts │ └── runTests.sh ├── ext_emconf.php ├── webpack.config.js ├── composer.json ├── README.md └── Documentation ├── BuildingComponents.md └── ConfigurationReference.md /Resources/Public/Css/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Resources/Public/Javascript/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | # https://github.com/browserslist/browserslist 2 | 3 | last 2 version 4 | not dead 5 | -------------------------------------------------------------------------------- /.yarn/install-state.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sitegeist/fluid-styleguide/HEAD/.yarn/install-state.gz -------------------------------------------------------------------------------- /Resources/Private/DemoComponents/Atom/Image/Image.fixture.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": { 3 | "image": { 4 | "height": 500, 5 | "width": 500 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Classes/Exception/InvalidAssetException.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
 
5 |
6 |
7 | 8 | -------------------------------------------------------------------------------- /Resources/Private/Scss/StyleguideShow.scss: -------------------------------------------------------------------------------- 1 | .fluidStyleguideShow { 2 | position: fixed; 3 | top: 55px; 4 | left: 0; 5 | right: 0; 6 | bottom: 16px; 7 | 8 | .fluidStyleguideComponent { 9 | height: 100%; 10 | width: 100%; 11 | border: 0 none; 12 | transition: width .6s; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ext_localconf.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 |
7 | 8 | -------------------------------------------------------------------------------- /Resources/Private/Components/Atom/StyleguideRefreshIFrame/StyleguideRefreshIFrame.js: -------------------------------------------------------------------------------- 1 | const button = document.getElementById('styleguideRefreshIframe_button') 2 | const iframe = document.getElementById('componentIframe') 3 | 4 | if (button != null) { 5 | button.addEventListener('click', function () { 6 | iframe.contentWindow.location.reload(true) 7 | }) 8 | } 9 | 10 | -------------------------------------------------------------------------------- /Resources/Private/DemoComponents/Atom/Button/Button.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Resources/Private/Javascript/Iframe.js: -------------------------------------------------------------------------------- 1 | import '@iframe-resizer/child'; 2 | 3 | // close select if clicked outside 4 | document.addEventListener('click', () => { 5 | const selectOpened = window.top.document.querySelector('.styleguideSelectOpened'); 6 | if (selectOpened) { 7 | window.top.document.querySelector('.styleguideSelectOpened').classList.remove('styleguideSelectOpened'); 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /Resources/Public/Css/Iframe.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: var(--styleguide-iframe-background) !important; 3 | } 4 | 5 | #fluidStyleguideRuler { 6 | position: absolute; 7 | z-index: 1; 8 | width: 100%; 9 | height: 100%; 10 | top: 0; 11 | } 12 | 13 | #fluidStyleguideRulerComponentWrap { 14 | position: relative; 15 | z-index: 2; 16 | } 17 | 18 | .fluidStyleguideComponentSpacing { 19 | padding: 24px; 20 | } 21 | -------------------------------------------------------------------------------- /Configuration/RequestMiddlewares.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'sitegeist/fluid-styleguide/router' => [ 6 | 'target' => \Sitegeist\FluidStyleguide\Middleware\StyleguideRouter::class, 7 | 'after' => [ 8 | 'typo3/cms-frontend/site' 9 | ], 10 | 'before' => [ 11 | 'typo3/cms-frontend/authentication' 12 | ] 13 | ], 14 | ], 15 | ]; 16 | -------------------------------------------------------------------------------- /Resources/Private/Templates/Styleguide/Component.html: -------------------------------------------------------------------------------- 1 | {namespace fsv=Sitegeist\FluidStyleguide\ViewHelpers} 2 | 3 | 4 | 5 | 13 | 14 | -------------------------------------------------------------------------------- /Resources/Private/Components/Atom/StyleguideSelect/StyleguideSelect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | {content -> f:format.raw()} 6 |
7 |
8 |
9 | -------------------------------------------------------------------------------- /Resources/Private/Javascript/Utils.js: -------------------------------------------------------------------------------- 1 | const find = (selector, scope = document) => scope.querySelector(selector) 2 | const findAll = (selector, scope = document) => [].slice.call(scope.querySelectorAll(selector)) 3 | 4 | const register = (name, component) => { 5 | document.addEventListener('DOMContentLoaded', () => { 6 | findAll(`[data-component=${name}]`).forEach(el => component(el)) 7 | }) 8 | } 9 | 10 | export { 11 | find, 12 | findAll, 13 | register 14 | } 15 | -------------------------------------------------------------------------------- /Resources/Private/Javascript/Polyfills.js: -------------------------------------------------------------------------------- 1 | if (window.Element && !Element.prototype.closest) { 2 | Element.prototype.closest = function(s) { 3 | var matches = (this.document || this.ownerDocument).querySelectorAll(s), 4 | i, 5 | el = this; 6 | do { 7 | i = matches.length; 8 | while (--i >= 0 && matches.item(i) !== el) {}; 9 | } while ((i < 0) && (el = el.parentElement)); 10 | return el; 11 | }; 12 | } 13 | 14 | -------------------------------------------------------------------------------- /Resources/Private/DemoComponents/Atom/Image/Image.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {image.alternative} 13 | 14 | 15 | -------------------------------------------------------------------------------- /vitesse.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionPath": "/", 3 | "build": { 4 | "inputFiles": [ 5 | "./Resources/Private/Scss/Styleguide.scss", 6 | "./Resources/Private/Javascript/Styleguide.js", 7 | "./Resources/Private/Javascript/Iframe.js" 8 | ], 9 | "outputPath": "./Resources/Public/Build/", 10 | "outputFilePattern": "[name].min.js" 11 | }, 12 | "emptyOutDir": false, 13 | "excludeTailwind": true, 14 | "modulePreload": false 15 | } 16 | -------------------------------------------------------------------------------- /Resources/Private/Components/Atom/StyleguideRefreshIFrame/StyleguideRefreshIFrame.scss: -------------------------------------------------------------------------------- 1 | .styleguideRefreshIFrame { 2 | 3 | button { 4 | background-color: $dark-grey-0; 5 | color: $white; 6 | cursor: pointer; 7 | font-size: 14px; 8 | padding: 5px 10px; 9 | border: 1px solid white; 10 | 11 | &:hover { 12 | background-color: $highlight; 13 | } 14 | 15 | &:focus { 16 | outline: none; 17 | } 18 | 19 | } 20 | } 21 | 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fluidstyleguide", 3 | "version": "3.0.0", 4 | "main": "index.js", 5 | "author": "sitegeist media solutions GmbH ", 6 | "license": "GPL-3.0-or-later", 7 | "scripts": { 8 | "build": "vitesse", 9 | "watch": "vitesse --watch" 10 | }, 11 | "dependencies": { 12 | "@iframe-resizer/child": "^5.5.7", 13 | "@iframe-resizer/parent": "^5.5.7" 14 | }, 15 | "devDependencies": { 16 | "@sitegeist/vitesse": "^3.0" 17 | }, 18 | "packageManager": "yarn@4.10.3" 19 | } 20 | -------------------------------------------------------------------------------- /Classes/Event/PreProcessComponentViewEvent.php: -------------------------------------------------------------------------------- 1 | { 4 | 5 | window.addEventListener('scroll', () => { 6 | if(window.scrollY > 200) { 7 | el.classList.add('active'); 8 | } else { 9 | el.classList.remove('active'); 10 | } 11 | }); 12 | 13 | find('div',el).addEventListener('click', () => { 14 | window.scrollTo({ 15 | top: 0, 16 | behavior: 'smooth' 17 | }); 18 | 19 | }); 20 | } 21 | 22 | register('StyleguideScrollTop', StyleguideScrollTop) 23 | 24 | -------------------------------------------------------------------------------- /Resources/Private/Components/Molecule/EditFixtures/EditFixtures.js: -------------------------------------------------------------------------------- 1 | import {register, findAll, find} from '../../../Javascript/Utils'; 2 | 3 | const EditFixtures = el => { 4 | 5 | const form = find('form', el); 6 | const inputs = findAll('.editFixturesInput', el); 7 | const checkboxes = findAll('.editFixturesCheckbox', el); 8 | 9 | inputs.forEach((input) => { 10 | input.addEventListener('input', ()=>{ 11 | form.submit(); 12 | 13 | }) 14 | }); 15 | 16 | checkboxes.forEach((checkbox) => { 17 | checkbox.addEventListener('change', ()=>{ 18 | form.submit(); 19 | }) 20 | }); 21 | 22 | }; 23 | 24 | register('EditFixtures', EditFixtures); 25 | -------------------------------------------------------------------------------- /conventional.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "minify": true, 3 | "autoprefixer": true, 4 | "sourceMaps": true, 5 | "browserUpdateNote": false, 6 | "sass": { 7 | "inputFiles": { 8 | "Styleguide": "./Resources/Private/Scss/Main.scss" 9 | }, 10 | "outputPath": "./Resources/Public/Css/", 11 | "outputFilePattern": "[name].min.css" 12 | }, 13 | "js": { 14 | "inputFiles": { 15 | "Styleguide": "./Resources/Private/Javascript/Styleguide.js", 16 | "Iframe": "./Resources/Private/Javascript/Iframe.js" 17 | }, 18 | "outputPath": "./Resources/Public/Javascript/", 19 | "outputFilePattern": "[name].min.js" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Resources/Private/Javascript/Styleguide.js: -------------------------------------------------------------------------------- 1 | import iframeResize from '@iframe-resizer/parent'; 2 | 3 | import '../Components/Atom/StyleguideRefreshIFrame/StyleguideRefreshIFrame'; 4 | import '../Components/Atom/StyleguideSelect/StyleguideSelect'; 5 | import '../Components/Organism/StyleguideToolbar/StyleguideToolbar'; 6 | import '../Components/Atom/StyleguideScrollTop/StyleguideScrollTop'; 7 | import '../Components/Atom/ViewportNavigation/ViewportNavigation'; 8 | import '../Components/Atom/LanguageNavigation/LanguageNavigation'; 9 | import '../Components/Molecule/EditFixtures/EditFixtures'; 10 | 11 | iframeResize( 12 | { heightCalculationMethod: 'taggedElement', warningTimeout: 0, license: 'GPLv3' }, 13 | '.iframeResize' 14 | ); 15 | -------------------------------------------------------------------------------- /Classes/Domain/Model/ComponentFixture.php: -------------------------------------------------------------------------------- 1 | filePath; 18 | } 19 | 20 | public function getName(): string 21 | { 22 | return $this->name; 23 | } 24 | 25 | public function getData(): array 26 | { 27 | return $this->data; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Classes/Factory/Component/ComponentFactoryInterface.php: -------------------------------------------------------------------------------- 1 | configuration; 20 | } 21 | 22 | public function setConfiguration(array $configuration): void 23 | { 24 | $this->configuration = $configuration; 25 | } 26 | 27 | public function getSite(): SiteInterface 28 | { 29 | return $this->site; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Build/FunctionalTestsBootstrap.php: -------------------------------------------------------------------------------- 1 | defineOriginalRootPath(); 18 | $testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/tests'); 19 | $testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/transient'); 20 | }); 21 | -------------------------------------------------------------------------------- /Classes/Factory/Component/ComponentFactory.php: -------------------------------------------------------------------------------- 1 | text($this->renderChildren()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Resources/Private/Scss/Styleguide.scss: -------------------------------------------------------------------------------- 1 | @import 'Variables'; 2 | @import 'StyleguideList'; 3 | @import 'StyleguideShow'; 4 | @import 'StyleguideHeader'; 5 | // Component CSS 6 | 7 | @import '../Components/Atom/StyleguideRefreshIFrame/StyleguideRefreshIFrame'; 8 | @import '../Components/Atom/StyleguideSelect/StyleguideSelect'; 9 | @import '../Components/Atom/StyleguideScrollTop/StyleguideScrollTop'; 10 | @import '../Components/Atom/ViewportNavigation/ViewportNavigation'; 11 | @import '../Components/Atom/LanguageNavigation/LanguageNavigation'; 12 | @import '../Components/Molecule/EditFixtures/EditFixtures'; 13 | @import '../Components/Organism/StyleguideToolbar/StyleguideToolbar'; 14 | 15 | body { 16 | margin: 0; 17 | } 18 | 19 | .breakPoint { 20 | background-color: $dark-grey-4 !important; 21 | } 22 | 23 | .boxMargin { 24 | margin-bottom: 40px; 25 | } 26 | 27 | pre { 28 | margin: 0 !important; 29 | } 30 | -------------------------------------------------------------------------------- /Classes/Domain/Repository/ComponentNameRepository.php: -------------------------------------------------------------------------------- 1 | packageRepository->findForComponentIdentifier($componentIdentifier); 18 | if (!$package) { 19 | return null; 20 | } 21 | 22 | return new ComponentName( 23 | $package->extractComponentName($componentIdentifier), 24 | $package 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Resources/Public/Build/.vite/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "Resources/Private/Javascript/Iframe.js": { 3 | "file": "JavaScript/Iframe.min.js", 4 | "name": "Iframe", 5 | "src": "Resources/Private/Javascript/Iframe.js", 6 | "isEntry": true, 7 | "imports": [ 8 | "_index-CTDsMjWk.js" 9 | ] 10 | }, 11 | "Resources/Private/Javascript/Styleguide.js": { 12 | "file": "JavaScript/Styleguide.min2.js", 13 | "name": "Styleguide", 14 | "src": "Resources/Private/Javascript/Styleguide.js", 15 | "isEntry": true, 16 | "imports": [ 17 | "_index-CTDsMjWk.js" 18 | ] 19 | }, 20 | "Resources/Private/Scss/Styleguide.scss": { 21 | "file": "Css/Styleguide.min.css", 22 | "src": "Resources/Private/Scss/Styleguide.scss", 23 | "isEntry": true, 24 | "names": [ 25 | "Styleguide.css" 26 | ] 27 | }, 28 | "_index-CTDsMjWk.js": { 29 | "file": "JavaScript/Chunks/index-CTDsMjWk.js", 30 | "name": "index" 31 | } 32 | } -------------------------------------------------------------------------------- /ext_emconf.php: -------------------------------------------------------------------------------- 1 | 'Fluid Styleguide', 4 | 'description' => 'Living styleguide for Fluid Components', 5 | 'category' => 'fe', 6 | 'author' => 'Ulrich Mathes, Simon Praetorius', 7 | 'author_email' => 'mathes@sitegeist.de, moin@praetorius.me', 8 | 'author_company' => 'sitegeist media solutions GmbH', 9 | 'state' => 'stable', 10 | 'version' => '', 11 | 'constraints' => [ 12 | 'depends' => [ 13 | 'typo3' => '12.4.0-13.4.99', 14 | 'fluid_components' => '3.0.0-3.99.99', 15 | 'php' => '8.2.0-8.3.99' 16 | ], 17 | 'conflicts' => [ 18 | ], 19 | 'suggests' => [ 20 | ], 21 | ], 22 | 'autoload' => [ 23 | 'classmap' => [ 24 | 'Resources/Private/Php/Parsedown.php' 25 | ], 26 | 'psr-4' => [ 27 | 'Sitegeist\\FluidStyleguide\\' => 'Classes' 28 | ] 29 | ], 30 | ]; 31 | -------------------------------------------------------------------------------- /Build/FunctionalTests.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | ../Tests/Functional/ 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Build/UnitTests.xml: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 | 20 | ../Tests/Unit/ 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Classes/Domain/Model/ComponentLocation.php: -------------------------------------------------------------------------------- 1 | fileName = basename($filePath); 14 | $this->directory = dirname($filePath); 15 | } 16 | 17 | public function getFileName(): string 18 | { 19 | return $this->fileName; 20 | } 21 | 22 | public function getFilePath(): string 23 | { 24 | return $this->filePath; 25 | } 26 | 27 | public function getDirectory(): string 28 | { 29 | return $this->directory; 30 | } 31 | 32 | public function generatePathToFile(string $fileName): string 33 | { 34 | return $this->directory . DIRECTORY_SEPARATOR 35 | . ltrim($fileName, DIRECTORY_SEPARATOR); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Resources/Private/DemoComponents/Molecule/Teaser/Teaser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |

{title}

16 | 17 |

{content}

18 |
19 | {actions -> f:format.raw()} 20 |
21 |
22 |
23 |
24 | -------------------------------------------------------------------------------- /Resources/Private/Components/Atom/LanguageNavigation/LanguageNavigation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 14 | 19 |
20 |
21 |
22 | -------------------------------------------------------------------------------- /Configuration/Services.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | _defaults: 3 | autowire: true 4 | autoconfigure: true 5 | public: false 6 | 7 | Sitegeist\FluidStyleguide\: 8 | resource: '../Classes/*' 9 | exclude: '../Classes/Domain/Model/*' 10 | 11 | 12 | Sitegeist\FluidStyleguide\EventListener\AssetCollectorExtensionInjector: 13 | autowire: false 14 | tags: 15 | - 16 | name: event.listener 17 | method: 'injectJsAndCssFromAssetCollectorExtension' 18 | event: Sitegeist\FluidStyleguide\Event\PostProcessComponentViewEvent 19 | 20 | Sitegeist\FluidStyleguide\EventListener\AssetCollectorInjector: 21 | autowire: false 22 | tags: 23 | - 24 | name: event.listener 25 | method: 'injectJsAndCssFromAssetCollector' 26 | event: Sitegeist\FluidStyleguide\Event\PostProcessComponentViewEvent 27 | 28 | Sitegeist\FluidStyleguide\Controller\StyleguideController: 29 | public: true 30 | 31 | Sitegeist\FluidStyleguide\Service\StyleguideConfigurationManager: 32 | public: true 33 | -------------------------------------------------------------------------------- /Resources/Private/Components/Atom/ViewportNavigation/ViewportNavigation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 14 | 19 |
20 |
21 |
22 | -------------------------------------------------------------------------------- /Classes/Domain/Model/ComponentName.php: -------------------------------------------------------------------------------- 1 | name = trim($name, '\\'); 15 | } 16 | 17 | public function getName(): string 18 | { 19 | return $this->name; 20 | } 21 | 22 | public function getIdentifier(): string 23 | { 24 | return $this->package->getNamespace() . '\\' . $this->name; 25 | } 26 | 27 | public function getDisplayName(): string 28 | { 29 | return str_replace('\\', '/', $this->name); 30 | } 31 | 32 | public function getSimpleName(): string 33 | { 34 | return array_pop(explode('\\', $this->name)); 35 | } 36 | 37 | public function getTagName(): string 38 | { 39 | $tagName = implode('.', array_map('lcfirst', explode('\\', $this->name))); 40 | return $this->package->getAlias() . ':' . $tagName; 41 | } 42 | 43 | public function getPackage(): Package 44 | { 45 | return $this->package; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Resources/Public/Icons/Extension.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Resources/Private/Components/Atom/StyleguideScrollTop/StyleguideScrollTop.scss: -------------------------------------------------------------------------------- 1 | .styleguideScrollTop { 2 | position: fixed; 3 | width: 100%; 4 | bottom: 0; 5 | 6 | div { 7 | display: none; 8 | } 9 | 10 | &.active { 11 | div { 12 | cursor: pointer; 13 | margin: auto; 14 | width: 50px; 15 | height: 50px; 16 | border: 2px solid $dark-grey-0; 17 | background: $dark-grey-0; 18 | display: block; 19 | 20 | &::before { 21 | font-family: $verdana-font; 22 | content: '›'; 23 | speak: none; 24 | font-style: normal; 25 | font-weight: normal; 26 | font-variant: normal; 27 | text-transform: none; 28 | line-height: 1; 29 | font-size: 34px; 30 | padding-left: 0; 31 | padding-top: 0; 32 | color: $highlight; 33 | display: block; 34 | transform: rotate(-90deg); 35 | position: relative; 36 | top: -7px; 37 | left: -2px; 38 | } 39 | } 40 | } 41 | 42 | 43 | 44 | 45 | } 46 | -------------------------------------------------------------------------------- /Resources/Private/DemoComponents/Molecule/Teaser/Teaser.fixture.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": { 3 | "title": "Fluid Components", 4 | "content": "Fluid Components for TYPO3 enables frontend developers to implement consistent web designs with reusable components", 5 | "image": { 6 | "height": 200, 7 | "width": 500, 8 | "alternative": "My alt text", 9 | "title": "My image title" 10 | }, 11 | "link": { 12 | "uri": "https://fluidcomponents.sitegeist.de", 13 | "target": "_blank" 14 | }, 15 | "actions": "Primary ButtonSecondary Button" 16 | }, 17 | "dark": { 18 | "title": "Fluid Components", 19 | "content": "Fluid Components for TYPO3 enables frontend developers to implement consistent web designs with reusable components", 20 | "image": { 21 | "height": 200, 22 | "width": 500, 23 | "alternative": "My alt text", 24 | "title": "My image title" 25 | }, 26 | "link": { 27 | "uri": "https://fluidcomponents.sitegeist.de", 28 | "target": "_blank" 29 | }, 30 | "theme": "dark" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Resources/Private/Components/Atom/LanguageNavigation/LanguageNavigation.js: -------------------------------------------------------------------------------- 1 | import {register, findAll, find} from '../../../Javascript/Utils'; 2 | 3 | const LanguageNavigation = el => { 4 | 5 | const iframes = findAll('.fluidStyleguideComponent'); 6 | const languageNavItem = findAll('.languageItem' , el); 7 | const dropdownLabel = find('.dropdownLabel', el); 8 | // const listView = el.dataset.listview; 9 | 10 | languageNavItem.forEach((item) => { 11 | const languageValue = item.dataset.language; 12 | item.addEventListener('click', ()=>{ 13 | find('.active',el).classList.remove('active'); 14 | item.classList.add('active'); 15 | 16 | iframes.forEach((iframe) => { 17 | let url = new URL(iframe.src); 18 | url.searchParams.set('language', languageValue); 19 | iframe.src = url.toString(); 20 | }) 21 | 22 | dropdownLabel.textContent = item.textContent; 23 | languageNavItem.forEach((inner) => { 24 | inner.style.display = 'block'; 25 | }); 26 | item.style.display = 'none'; 27 | 28 | document.body.classList.remove('breakPoint'); 29 | }); 30 | }); 31 | 32 | }; 33 | 34 | register('LanguageNavigation', LanguageNavigation); 35 | -------------------------------------------------------------------------------- /Classes/EventListener/AssetCollectorExtensionInjector.php: -------------------------------------------------------------------------------- 1 | getExternalCssFiles() as $cssFile) { 23 | $event->addHeaderData( 24 | '' 25 | ); 26 | } 27 | 28 | // Add inline css 29 | $event->addHeaderData($assetCollector->buildInlineCssTag()); 30 | 31 | // Add javascript files 32 | $event->addFooterData($assetCollector->buildJavaScriptIncludes()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Resources/Private/Components/Atom/LanguageNavigation/LanguageNavigation.scss: -------------------------------------------------------------------------------- 1 | .languageNavigation { 2 | display: flex; 3 | color: $white; 4 | height: 100%; 5 | align-items: center; 6 | font-size: 13px; 7 | list-style: none; 8 | 9 | &:hover .dropdownContent { 10 | display: block; 11 | } 12 | 13 | .dropdownLabel { 14 | color: $highlight; 15 | padding: 16px 0; 16 | border: 0 none; 17 | cursor: pointer; 18 | margin: 0 16px; 19 | } 20 | 21 | .dropdownContent { 22 | position: absolute; 23 | background-color: $dark-grey-0; 24 | padding: 16px; 25 | min-width: 82px; 26 | z-index: 1; 27 | top: 43px; 28 | display: none; 29 | padding-top: 0; 30 | 31 | &:hover { 32 | display: block; 33 | } 34 | 35 | li { 36 | color: $white; 37 | margin-top: 16px; 38 | text-decoration: none; 39 | cursor: pointer; 40 | list-style: none; 41 | 42 | &:last-child { 43 | margin-bottom: 0; 44 | } 45 | 46 | &.rulerOption { 47 | &.activeRulerOption { 48 | color: $highlight; 49 | } 50 | } 51 | 52 | &:hover { 53 | color: $highlight; 54 | } 55 | } 56 | } 57 | 58 | } 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /Resources/Private/Components/Atom/ViewportNavigation/ViewportNavigation.scss: -------------------------------------------------------------------------------- 1 | .viewportNavigation { 2 | display: flex; 3 | color: $white; 4 | height: 100%; 5 | align-items: center; 6 | font-size: 13px; 7 | list-style: none; 8 | 9 | &:hover .dropdownContent { 10 | display: block; 11 | } 12 | 13 | .dropdownLabel { 14 | color: $highlight; 15 | padding: 16px 0; 16 | border: 0 none; 17 | cursor: pointer; 18 | margin: 0 16px; 19 | } 20 | 21 | .dropdownContent { 22 | position: absolute; 23 | background-color: $dark-grey-0; 24 | padding: 16px; 25 | min-width: 82px; 26 | z-index: 1; 27 | top: 43px; 28 | display: none; 29 | padding-top: 0; 30 | 31 | &:hover { 32 | display: block; 33 | } 34 | 35 | li { 36 | color: $white; 37 | margin-top: 16px; 38 | text-decoration: none; 39 | cursor: pointer; 40 | list-style: none; 41 | 42 | &:last-child { 43 | margin-bottom: 0; 44 | } 45 | 46 | &.rulerOption { 47 | &.activeRulerOption { 48 | color: $highlight; 49 | } 50 | } 51 | 52 | &:hover { 53 | color: $highlight; 54 | } 55 | } 56 | } 57 | 58 | } 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /Resources/Private/Components/Organism/StyleguideToolbar/StyleguideToolbar.js: -------------------------------------------------------------------------------- 1 | import {register, findAll, find} from '../../../Javascript/Utils'; 2 | 3 | const StyleguideToolbar = el => { 4 | 5 | const toolbarOpener = find('.toolbarOpener', el); 6 | const tabOpeners = findAll('.tabOpener', el); 7 | const tabContents = findAll('.tabContent', el); 8 | 9 | 10 | el.style.bottom = `-${el.clientHeight-16}px`; 11 | 12 | toolbarOpener.addEventListener('click', ()=>{ 13 | 14 | if(!el.classList.toggle('open')) { 15 | tabContents.forEach((content, contentIndex) => { 16 | if (!contentIndex) { 17 | content.classList.add('active'); 18 | } else { 19 | content.classList.remove('active'); 20 | } 21 | }); 22 | tabOpeners.forEach((opener, openerIndex) => { 23 | if (!openerIndex) { 24 | opener.classList.add('active'); 25 | } else { 26 | opener.classList.remove('active'); 27 | } 28 | }); 29 | } 30 | }) 31 | 32 | tabOpeners.forEach((opener, openerIndex) => { 33 | opener.addEventListener('click', ()=>{ 34 | find('.tabOpener.active').classList.remove('active'); 35 | find('.tabContent.active').classList.remove('active'); 36 | tabContents.forEach((content, contentIndex) => { 37 | if (openerIndex === contentIndex) { 38 | opener.classList.add('active'); 39 | content.classList.add('active'); 40 | } 41 | }); 42 | }) 43 | }); 44 | }; 45 | 46 | register('StyleguideToolbar', StyleguideToolbar); 47 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin') 3 | 4 | module.exports = { 5 | entry: { 6 | 'Styleguide': './Resources/Private/Javascript/Styleguide.js', 7 | 'Iframe': './Resources/Private/Javascript/Iframe.js' 8 | }, 9 | output: { 10 | filename: '[name].min.js', 11 | path: path.resolve('Resources', 'Public', 'Javascript') 12 | }, 13 | // stats: 'minimal', 14 | mode: 'development', 15 | devtool: 'source-map', 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.js$/, 20 | exclude: /(node_modules)/, 21 | use: { 22 | loader: 'babel-loader', 23 | options: { 24 | "presets": [ 25 | [ 26 | "@babel/preset-env", 27 | { 28 | "modules": false, 29 | "corejs": "3.6.5", 30 | "useBuiltIns": "usage" 31 | } 32 | ] 33 | ], 34 | "plugins": [ 35 | "@babel/plugin-transform-runtime", 36 | "@babel/plugin-proposal-class-properties" 37 | ] 38 | } 39 | } 40 | } 41 | ] 42 | }, 43 | optimization: { 44 | minimizer: [ 45 | new UglifyJsPlugin({ 46 | cache: true, 47 | parallel: true, 48 | sourceMap: true 49 | }) 50 | ] 51 | }, 52 | watchOptions: { 53 | ignored: /node_modules/ 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Resources/Public/Css/DemoComponents.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | color: #111; 5 | } 6 | 7 | img { 8 | vertical-align: middle; 9 | } 10 | 11 | /* Button component */ 12 | .sitegeistAtomButton { 13 | padding: 16px 24px; 14 | border: 2px #e6a300 solid; 15 | text-transform: uppercase; 16 | cursor: pointer; 17 | font-size: inherit; 18 | margin-right: 8px; 19 | } 20 | 21 | .sitegeistAtomButton:active, 22 | .sitegeistAtomButton:hover, 23 | .sitegeistAtomButton:focus { 24 | background-color: #e6a300; 25 | color: #FFF; 26 | outline: none; 27 | } 28 | 29 | .sitegeistAtomButton--secondary { 30 | border-color: #545454; 31 | } 32 | 33 | .sitegeistAtomButton--secondary:active, 34 | .sitegeistAtomButton--secondary:hover, 35 | .sitegeistAtomButton--secondary:focus { 36 | background-color: #545454; 37 | color: #FFF; 38 | } 39 | 40 | /* Teaser component */ 41 | .sitegeistMoleculeTeaser { 42 | display: block; 43 | width: 500px; 44 | text-decoration: none; 45 | } 46 | 47 | .sitegeistMoleculeTeaser:active .sitegeistMoleculeTeaser_title, 48 | .sitegeistMoleculeTeaser:hover .sitegeistMoleculeTeaser_title, 49 | .sitegeistMoleculeTeaser:focus .sitegeistMoleculeTeaser_title { 50 | text-decoration: underline; 51 | } 52 | 53 | .sitegeistMoleculeTeaser--light { 54 | border: 2px #545454 solid; 55 | background: #f5f5f5; 56 | color: #111; 57 | } 58 | 59 | .sitegeistMoleculeTeaser--dark { 60 | border: 2px #545454 solid; 61 | background: #545454; 62 | color: #FFF; 63 | } 64 | 65 | .sitegeistMoleculeTeaser_title { 66 | font-size: 24px; 67 | margin-bottom: 16px; 68 | } 69 | 70 | .sitegeistMoleculeTeaser_content { 71 | padding: 16px; 72 | } 73 | 74 | .sitegeistMoleculeTeaser_content :first-child { 75 | margin-top: 0; 76 | } 77 | 78 | .sitegeistMoleculeTeaser_content :last-child { 79 | margin-bottom: 0; 80 | } 81 | -------------------------------------------------------------------------------- /Resources/Private/Layouts/Styleguide.html: -------------------------------------------------------------------------------- 1 | {namespace fsc=Sitegeist\FluidStyleguide\Components}{namespace fsv=Sitegeist\FluidStyleguide\ViewHelpers} 2 | 3 | 4 | 5 | 6 | {f:if(condition: activeComponent, then: '{activeComponent.name.name} - ')} 7 | {styleguideConfiguration.brandingTitle} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 | 24 | 36 | 37 |
38 | 39 | 40 |
41 | 42 | 43 |
44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/Uri/StyleguideViewHelper.php: -------------------------------------------------------------------------------- 1 | registerArgument('action', 'string', 'Action name', true); 17 | $this->registerArgument('arguments', 'array', 'Action arguments', false, []); 18 | $this->registerArgument('section', 'string', 'the anchor to be added to the URI', false, ''); 19 | $this->registerArgument('relative', 'bool', 'generate a relative path', false, true); 20 | } 21 | 22 | /** 23 | * Renders markdown code in fluid templates 24 | */ 25 | public function render(): UriInterface 26 | { 27 | $prefix = GeneralUtility::makeInstance(ExtensionConfiguration::class) 28 | ->get('fluid_styleguide', 'uriPrefix'); 29 | $prefix = rtrim((string) $prefix, '/') . '/'; 30 | $baseUrl = static::getCurrentSite()->getBase(); 31 | // reset scheme and host to return a relative path 32 | if ($this->arguments['relative'] === true) { 33 | return $baseUrl 34 | ->withScheme('') 35 | ->withHost('') 36 | ->withPath($prefix . $this->arguments['action']) 37 | ->withQuery(http_build_query($this->arguments['arguments'])) 38 | ->withFragment($this->arguments['section']); 39 | } 40 | return $baseUrl 41 | ->withPath($prefix . $this->arguments['action']) 42 | ->withQuery(http_build_query($this->arguments['arguments'])) 43 | ->withFragment($this->arguments['section']) 44 | ->withPort(GeneralUtility::getIndpEnv('TYPO3_PORT') ?: null); 45 | } 46 | 47 | /** 48 | * Returns the current Site object to create urls 49 | */ 50 | protected static function getCurrentSite(): SiteInterface 51 | { 52 | return $GLOBALS['TYPO3_CURRENT_SITE']; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Resources/Private/Scss/StyleguideHeader.scss: -------------------------------------------------------------------------------- 1 | .styleguideHeader { 2 | display: flex; 3 | position: fixed; 4 | z-index: 99999; 5 | top: 0; 6 | left: 0; 7 | right: 0; 8 | align-items: center; 9 | background: $dark-grey-0; 10 | font-family: $styleguide-font; 11 | 12 | .componentNavigation .styleguideSelectSelected { 13 | position: relative; 14 | 15 | &::before { 16 | position: absolute; 17 | content: '›'; 18 | top: 15px; 19 | width: 0; 20 | height: 0; 21 | speak: none; 22 | font-style: normal; 23 | font-weight: normal; 24 | font-variant: normal; 25 | text-transform: none; 26 | line-height: 1; 27 | font-size: 24px; 28 | right: 24px; 29 | font-family: $verdana-font; 30 | color: $highlight; 31 | } 32 | } 33 | 34 | .examplesNavigation { 35 | width: 50%; 36 | margin-left: -7px; 37 | 38 | svg { 39 | width: 26px; 40 | height: 26px; 41 | margin-left: -24px; 42 | fill: $highlight; 43 | display: none; 44 | } 45 | } 46 | 47 | .backLink { 48 | font-size: 13px; 49 | color: $white; 50 | position: relative; 51 | padding-left: 50px; 52 | text-decoration: none; 53 | height: 100%; 54 | display: flex; 55 | align-items: center; 56 | border-left: 1px solid $highlight; 57 | margin-left: 13px; 58 | 59 | 60 | &:hover { 61 | color: $highlight; 62 | } 63 | 64 | &::before { 65 | font-family: $verdana-font; 66 | position: absolute; 67 | content: '‹'; 68 | top: 16px; 69 | width: 0; 70 | height: 0; 71 | speak: none; 72 | font-style: normal; 73 | font-weight: normal; 74 | font-variant: normal; 75 | text-transform: none; 76 | line-height: 1; 77 | font-size: 24px; 78 | left: 23px; 79 | color: $highlight; 80 | } 81 | } 82 | 83 | .actions { 84 | margin: 0 24px 0 auto; 85 | display: flex; 86 | align-items: center; 87 | height: 56px; 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /Classes/Event/PostProcessComponentViewEvent.php: -------------------------------------------------------------------------------- 1 | part 12 | * 13 | * @var string[] 14 | */ 15 | private $headerData = []; 16 | 17 | /** 18 | * Markup that should be added to the end of the part 19 | * 20 | * @var string[] 21 | */ 22 | private $footerData = []; 23 | 24 | public function __construct( 25 | private readonly Component $component, // Component that has been rendered 26 | private readonly string $fixtureName, // Name of the component fixture that has been used 27 | private readonly array $formData, // Form data that has been entered by the user in the styleguide 28 | private string $renderedView // Rendered component html that will be displayed in the iframe 29 | ) { 30 | } 31 | 32 | public function getComponent(): Component 33 | { 34 | return $this->component; 35 | } 36 | 37 | public function getFixtureName(): string 38 | { 39 | return $this->fixtureName; 40 | } 41 | 42 | public function getFormData(): array 43 | { 44 | return $this->formData; 45 | } 46 | 47 | public function getRenderedView(): string 48 | { 49 | return $this->renderedView; 50 | } 51 | 52 | public function setRenderedView(string $markup): void 53 | { 54 | $this->renderedView = $markup; 55 | } 56 | 57 | public function getHeaderData(): array 58 | { 59 | return $this->headerData; 60 | } 61 | 62 | public function setHeaderData(array $headerData): void 63 | { 64 | $this->headerData = $headerData; 65 | } 66 | 67 | public function addHeaderData(string $headerData): void 68 | { 69 | $this->headerData[] = $headerData; 70 | } 71 | 72 | public function getFooterData(): array 73 | { 74 | return $this->footerData; 75 | } 76 | 77 | public function setFooterData(array $footerData): void 78 | { 79 | $this->footerData = $footerData; 80 | } 81 | 82 | public function addFooterData(string $footerData): void 83 | { 84 | $this->footerData[] = $footerData; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Resources/Private/Components/Molecule/EditFixtures/EditFixtures.scss: -------------------------------------------------------------------------------- 1 | .editFixtures { 2 | 3 | .editFixturesFixtureFormItem { 4 | 5 | margin-bottom: 24px; 6 | 7 | &:last-child { 8 | margin-bottom: 0; 9 | } 10 | } 11 | 12 | .editFixturesInput { 13 | color: $dark-grey-0; 14 | font-size: 14px; 15 | font-style: normal; 16 | font-weight: 400; 17 | letter-spacing: 0; 18 | line-height: 24px; 19 | border: 2px $dark-grey-0 solid; 20 | padding: 10px; 21 | background: $white; 22 | width: 300px; 23 | 24 | &:focus { 25 | color: $dark-grey-0; 26 | border-color: $dark-grey-0; 27 | outline: 0; 28 | } 29 | } 30 | 31 | .editFixturesLabelContainer { 32 | margin-bottom: 6px; 33 | 34 | label { 35 | margin-bottom: 0; 36 | display: block; 37 | } 38 | 39 | .editFixturesLabelDescription { 40 | font-size: 10px; 41 | } 42 | } 43 | 44 | .editFixturesCheckboxContainer { 45 | display: block; 46 | position: relative; 47 | padding-left: 35px; 48 | margin-bottom: 12px; 49 | cursor: pointer; 50 | font-size: 22px; 51 | user-select: none; 52 | height: 25px; 53 | width: 24px; 54 | } 55 | 56 | .editFixturesCheckboxContainer input { 57 | position: absolute; 58 | opacity: 0; 59 | cursor: pointer; 60 | height: 0; 61 | width: 0; 62 | } 63 | 64 | .editFixturesCheckboxCheckmark { 65 | position: absolute; 66 | left: 0; 67 | height: 24px; 68 | width: 24px; 69 | background-color: $white; 70 | top: 3px; 71 | } 72 | 73 | .editFixturesCheckboxContainer input:checked ~ .editFixturesCheckboxCheckmark { 74 | background-color: $white; 75 | } 76 | 77 | .editFixturesCheckboxCheckmark::after { 78 | content: ''; 79 | position: absolute; 80 | display: none; 81 | } 82 | 83 | .editFixturesCheckboxContainer input:checked ~ .editFixturesCheckboxCheckmark::after { 84 | display: block; 85 | } 86 | 87 | .editFixturesCheckboxContainer .editFixturesCheckboxCheckmark::after { 88 | left: 8px; 89 | top: 3px; 90 | width: 5px; 91 | height: 13px; 92 | border: solid $highlight; 93 | border-width: 0 3px 3px 0; 94 | transform: rotate(45deg); 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /Resources/Public/Build/JavaScript/Chunks/index-CTDsMjWk.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * @module auto-console-group v1.2.11 3 | * 4 | * @description Automagically group console logs in the browser console. 5 | * 6 | * @author David J. Bradshaw 7 | * @see {@link https://github.com/davidjbradshaw/auto-console-group#readme} 8 | * @license MIT 9 | * 10 | * @copyright (c) 2025, David J. Bradshaw. All rights reserved. 11 | */const y="font-weight: normal;",G="font-weight: bold;",L="font-style: italic;",T=y+L,z="color: #135CD2;",H="color: #A9C7FB;",P="color: #1F1F1F;",R="color: #E3E3E3;",a="default",U="error",E="log",I=Object.freeze({assert:!0,error:!0,warn:!0}),v={expand:!1,defaultEvent:void 0,event:void 0,label:"AutoConsoleGroup",showTime:!0},J={profile:0,profileEnd:0,timeStamp:0,trace:0},K=o=>{const e=o.event||o.defaultEvent;return e?`${e}`:""},u=Object.assign(console);function N(){const o=new Date,e=(l,d)=>o[l]().toString().padStart(d,"0"),c=e("getHours",2),i=e("getMinutes",2),r=e("getSeconds",2),s=e("getMilliseconds",3);return`@ ${c}:${i}:${r}.${s}`}const{fromEntries:Q,keys:V}=Object,W=o=>[o,u[o]],X=o=>e=>[e,function(c){o[e]=c}],w=(o,e)=>Q(V(o).map(e));function Y(o={}){const e={},c={},i=[],r={...v,expand:!o.collapsed||v.expanded,...o};let s="";function l(){i.length=0,s=""}function d(){delete r.event,l()}const M=()=>i.some(([t])=>t in I),k=()=>M()?!0:!!r.expand,x=()=>r.showTime?s:"";function g(){if(i.length===0){d();return}u[k()?"group":"groupCollapsed"](`%c${r.label}%c ${K(r)} %c${x()}`,y,G,T);for(const[t,...n]of i)u.assert(t in u,`Unknown console method: ${t}`),t in u&&u[t](...n);u.groupEnd(),d()}function p(){s===""&&(s=N())}function C(t){p(),r.event=t}function F(){p(),queueMicrotask(()=>queueMicrotask(g))}function f(t,...n){i.length===0&&F(),i.push([t,...n])}const O=t=>(...n)=>{let m;try{m=t(...n)}catch(h){if(!Error.prototype.isPrototypeOf(h))throw h;f(U,h),g()}return m};function S(t,...n){t!==!0&&f("assert",t,...n)}function j(t=a){c[t]?c[t]+=1:c[t]=1,f(E,`${t}: ${c[t]}`)}function A(t=a){delete c[t]}function q(t=a){p(),e[t]=performance.now()}function $(t=a,...n){if(!e[t]){f("timeLog",t,...n);return}const m=performance.now()-e[t];f(E,`${t}: ${m} ms`,...n)}function B(t=a){$(t),delete e[t]}const D=t=>[t,(...n)=>f(t,...n)];return{...w(r,X(r)),...w(console,D),...w(J,W),assert:S,count:j,countReset:A,endAutoGroup:g,errorBoundary:O,event:C,purge:l,time:q,timeEnd:B,timeLog:$,touch:p}}const b=typeof window>"u"||typeof window.matchMedia!="function"?!1:window.matchMedia("(prefers-color-scheme: dark)").matches,Z=b?H:z,_=b?R:P;export{G as H,Y as J,L as S,y as T,Z as Y,_ as Z}; 12 | -------------------------------------------------------------------------------- /Classes/Domain/Model/Package.php: -------------------------------------------------------------------------------- 1 | namespace = trim($namespace, '\\'); 23 | } 24 | 25 | public function getNamespace(): string 26 | { 27 | return $this->namespace; 28 | } 29 | 30 | public function getAlias(): string 31 | { 32 | return $this->alias; 33 | } 34 | 35 | public function getPath(): string 36 | { 37 | return $this->path; 38 | } 39 | 40 | public function getExtension(): ?PackageInterface 41 | { 42 | if ($this->extension) { 43 | return $this->extension; 44 | } 45 | 46 | $activeExtensions = GeneralUtility::makeInstance(PackageManager::class)->getActivePackages(); 47 | foreach ($activeExtensions as $extension) { 48 | if (str_starts_with($this->getPath(), (string) $extension->getPackagePath())) { 49 | $this->extension = $extension; 50 | return $this->extension; 51 | } 52 | } 53 | 54 | return null; 55 | } 56 | 57 | /** 58 | * Returns the specificity (= depth) of the PHP namespace 59 | */ 60 | public function getSpecificity(): int 61 | { 62 | return substr_count($this->namespace, '\\'); 63 | } 64 | 65 | /** 66 | * Checks if the specified component is part of this component package 67 | */ 68 | public function isResponsibleForComponent(string $componentIdentifier): bool 69 | { 70 | $componentIdentifier = trim($componentIdentifier, '\\'); 71 | return str_starts_with($componentIdentifier, $this->namespace); 72 | } 73 | 74 | public function extractComponentName(string $componentIdentifier): ?string 75 | { 76 | $componentIdentifier = trim($componentIdentifier, '\\'); 77 | 78 | if (!$this->isResponsibleForComponent($componentIdentifier)) { 79 | return null; 80 | } 81 | 82 | return substr($componentIdentifier, strlen($this->namespace) + 1); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Resources/Private/Components/Atom/ViewportNavigation/ViewportNavigation.js: -------------------------------------------------------------------------------- 1 | import {register, findAll, find} from '../../../Javascript/Utils'; 2 | 3 | const ViewportNavigation = el => { 4 | 5 | const iframes = findAll('.fluidStyleguideComponent'); 6 | const viewPortNavItem = findAll('.viewportItem' , el); 7 | const rulerOption = find('.rulerOption' , el); 8 | const dropdownLabel = find('.dropdownLabel', el); 9 | const listView = el.dataset.listview; 10 | 11 | viewPortNavItem.forEach((item) => { 12 | const viewPortValue = item.dataset.viewport; 13 | item.addEventListener('click', ()=>{ 14 | 15 | find('.active',el).classList.remove('active'); 16 | item.classList.add('active'); 17 | 18 | iframes.forEach((iframe) => { 19 | iframe.style.width = viewPortValue; 20 | if(viewPortValue !== '100%' && listView) { 21 | iframe.parentElement.classList.add('breakPoint'); 22 | } 23 | }) 24 | 25 | dropdownLabel.textContent = item.textContent; 26 | viewPortNavItem.forEach((inner) => { 27 | inner.style.display = 'block'; 28 | }); 29 | item.style.display = 'none'; 30 | 31 | document.body.classList.remove('breakPoint'); 32 | 33 | if(viewPortValue !== '100%' && !listView) { 34 | document.body.classList.add('breakPoint'); 35 | /* 36 | const ruler = iframe.contentWindow.document.getElementById('fluidStyleguideRuler'); 37 | rulerOption.classList.add('activeRulerOption'); 38 | find('span', rulerOption).textContent = rulerOption.dataset.labelon 39 | ruler.classList.add('show') 40 | */ 41 | } 42 | }); 43 | }); 44 | 45 | /* 46 | rulerOption.addEventListener('click', (e)=>{ 47 | 48 | const ruler = iframe.contentWindow.document.getElementById('fluidStyleguideRuler'); 49 | rulerOption.classList.add('pressed'); 50 | if(ruler.classList.contains('show')) { 51 | find('span',e.currentTarget).textContent = e.currentTarget.dataset.labeloff 52 | ruler.classList.remove('show') 53 | e.currentTarget.classList.remove('activeRulerOption'); 54 | } else { 55 | find('span',e.currentTarget).textContent = e.currentTarget.dataset.labelon 56 | ruler.classList.add('show') 57 | e.currentTarget.classList.add('activeRulerOption'); 58 | } 59 | }); 60 | */ 61 | 62 | }; 63 | 64 | register('ViewportNavigation', ViewportNavigation); 65 | -------------------------------------------------------------------------------- /Classes/Domain/Repository/PackageRepository.php: -------------------------------------------------------------------------------- 1 | getViewHelperResolver()->getNamespaces(); 26 | $componentNamespaces = $this->componentLoader->getNamespaces(); 27 | $packages = []; 28 | foreach ($componentNamespaces as $namespace => $path) { 29 | $matchingNamespaceAlias = '???'; 30 | foreach ($fluidNamespaces as $namespaceAlias => $namespaceCandidates) { 31 | if (in_array($namespace, $namespaceCandidates)) { 32 | $matchingNamespaceAlias = $namespaceAlias; 33 | break; 34 | } 35 | } 36 | 37 | $packages[] = new Package( 38 | $namespace, 39 | $matchingNamespaceAlias, 40 | $path 41 | ); 42 | } 43 | 44 | return $packages; 45 | } 46 | 47 | /** 48 | * Finds the component package the specified component belongs to 49 | */ 50 | public function findForComponentIdentifier(string $componentIdentifier): ?Package 51 | { 52 | $componentPackage = null; 53 | foreach ($this->findAll() as $package) { 54 | if (!$package->isResponsibleForComponent($componentIdentifier)) { 55 | continue; 56 | } 57 | 58 | // Prefer packages with higher namespace specificity 59 | if (isset($componentPackage) && 60 | $componentPackage->getSpecificity() >= $package->getSpecificity() 61 | ) { 62 | continue; 63 | } 64 | 65 | $componentPackage = $package; 66 | } 67 | 68 | return $componentPackage; 69 | } 70 | 71 | protected function getViewHelperResolver(): ViewHelperResolver 72 | { 73 | return $this->container->get(ViewHelperResolverFactoryInterface::class)->create(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sitegeist/fluid-styleguide", 3 | "description": "Living styleguide for Fluid Components", 4 | "type": "typo3-cms-extension", 5 | "homepage": "https://github.com/sitegeist/fluid-styleguide", 6 | "license": ["GPL-3.0-or-later"], 7 | "keywords": ["typo3", "typo3-extension", "fluid", "typo3-fluid", "components", "html", "template", "styleguide", "living-styleguide"], 8 | "authors": [ 9 | { 10 | "name": "Ulrich Mathes", 11 | "email": "mathes@sitegeist.de" 12 | }, 13 | { 14 | "name": "Simon Praetorius", 15 | "email": "moin@praetorius.me" 16 | } 17 | ], 18 | "support": { 19 | "issues": "https://github.com/sitegeist/fluid-styleguide/issues" 20 | }, 21 | "require": { 22 | "php": "^8.2", 23 | "typo3/cms-core": "^13.3 || ^12.4", 24 | "sitegeist/fluid-components": "^3.0 || dev-master", 25 | "erusev/parsedown": "^1.7.4", 26 | "colinodell/json5": "^2.1" 27 | }, 28 | "require-dev": { 29 | "typo3/testing-framework": "^8.2", 30 | "squizlabs/php_codesniffer": "^3.0", 31 | "editorconfig-checker/editorconfig-checker": "^10.0" 32 | }, 33 | "suggest": { 34 | "sitegeist/fluid-components-linter": "Checks fluid components for code quality problems via CLI and in the styleguide" 35 | }, 36 | "autoload": { 37 | "psr-4": { 38 | "Sitegeist\\FluidStyleguide\\": "Classes/" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "SMS\\FluidComponents\\Tests\\": "Tests/" 44 | } 45 | }, 46 | "config": { 47 | "vendor-dir": ".Build/vendor", 48 | "bin-dir": ".Build/bin", 49 | "allow-plugins": { 50 | "typo3/class-alias-loader": true, 51 | "typo3/cms-composer-installers": true 52 | } 53 | }, 54 | "extra": { 55 | "typo3/cms": { 56 | "cms-package-dir": "{$vendor-dir}/typo3/cms", 57 | "app-dir": ".Build", 58 | "web-dir": ".Build/Web", 59 | "extension-key": "fluid_styleguide" 60 | } 61 | }, 62 | "scripts": { 63 | "prepare-release": [ 64 | "sed -i'' -e \"s/'version' => ''/'version' => '$(echo ${GITHUB_REF#refs/tags/} | sed 's/v//')'/\" ext_emconf.php", 65 | "rm -r .github .ecrc .editorconfig .gitattributes Tests Build" 66 | ], 67 | "lint": [ 68 | "@lint:editorconfig", 69 | "@lint:frontend" 70 | ], 71 | "lint:editorconfig": [ 72 | "ec" 73 | ], 74 | "lint:frontend": [ 75 | "yarn lint" 76 | ] 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Resources/Private/Templates/Styleguide/List.html: -------------------------------------------------------------------------------- 1 | {namespace fsc=Sitegeist\FluidStyleguide\Components} 2 | {namespace fsv=Sitegeist\FluidStyleguide\ViewHelpers} 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 | 19 | 20 | 21 |
22 |
23 |

{styleguideConfiguration.brandingTitle}

24 | {styleguideConfiguration.brandingIntro} 25 |
26 | 27 |
28 | 29 |
30 |

Package: {namespace}

31 |
32 | 33 | 37 |
38 |
39 |

{component.name.displayName}

40 |
41 | 42 |
43 | 44 | Show 45 | 46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | 54 | 55 | 56 |
57 | 58 | -------------------------------------------------------------------------------- /Classes/Domain/Repository/ComponentRepository.php: -------------------------------------------------------------------------------- 1 | packageRepository->findAll(); 29 | 30 | $components = []; 31 | foreach ($packages as $package) { 32 | $detectedComponents = $this->componentLoader->findComponentsInNamespace( 33 | $package->getNamespace() 34 | ); 35 | 36 | foreach ($detectedComponents as $componentIdentifier => $componentFilePath) { 37 | $component = $this->componentFactory->build( 38 | new ComponentName($package->extractComponentName($componentIdentifier), $package), 39 | new ComponentLocation($componentFilePath) 40 | ); 41 | 42 | if ($component->hasFixtures()) { 43 | $components[] = $component; 44 | } 45 | } 46 | } 47 | 48 | return $components; 49 | } 50 | 51 | /** 52 | * Returns the component record for the specified component identifier 53 | */ 54 | public function findWithFixturesByIdentifier(string $identifier): ?Component 55 | { 56 | $identifier = trim($identifier, '\\'); 57 | $componentName = $this->componentNameRepository->findByComponentIdentifier($identifier); 58 | if (!$componentName) { 59 | return null; 60 | } 61 | 62 | $componentFile = $this->componentLoader->findComponent($identifier); 63 | if (!$componentFile) { 64 | return null; 65 | } 66 | 67 | $component = $this->componentFactory->build($componentName, new ComponentLocation($componentFile)); 68 | if (!$component->hasFixtures()) { 69 | return null; 70 | } 71 | 72 | return $component; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Classes/Service/ComponentDownloadService.php: -------------------------------------------------------------------------------- 1 | getLocation()->getDirectory(); 16 | $realFileName = $component->getName()->getSimpleName() .'.zip'; 17 | $componentPath = PathUtility::sanitizeTrailingSeparator($componentPath); 18 | $temporaryPath = Environment::getVarPath() . '/transient/'; 19 | if (!@is_dir($temporaryPath)) { 20 | GeneralUtility::mkdir($temporaryPath); 21 | } 22 | $fileName = $temporaryPath . 'component_' 23 | . md5($component->getName()->getIdentifier()) 24 | . '_' . bin2hex(random_bytes(16)) . '.zip'; 25 | $temporaryPath = Environment::getVarPath() . '/transient/'; 26 | if (!@is_dir($temporaryPath)) { 27 | GeneralUtility::mkdir($temporaryPath); 28 | } 29 | $zip = new \ZipArchive(); 30 | $zip->open($fileName, \ZipArchive::CREATE); 31 | // Get all the files of the extension, but exclude the ones specified in the excludePattern 32 | $files = GeneralUtility::getAllFilesAndFoldersInPath( 33 | [], // No files pre-added 34 | $componentPath, // Start from here 35 | '', // Do not filter files by extension 36 | true, // Include subdirectories 37 | PHP_INT_MAX, // Recursion level 38 | '' // Files and directories to exclude. 39 | ); 40 | // Make paths relative to extension root directory. 41 | $files = GeneralUtility::removePrefixPathFromList($files, $componentPath); 42 | // Remove the one empty path that is the extension dir itself. 43 | $files = array_filter($files); 44 | foreach ($files as $file) { 45 | $fullPath = $componentPath . $file; 46 | // Distinguish between files and directories, as creation of the archive 47 | // fails on Windows when trying to add a directory with "addFile". 48 | if (is_dir($fullPath)) { 49 | $zip->addEmptyDir($file); 50 | } else { 51 | $zip->addFile($fullPath, $file); 52 | } 53 | } 54 | $zip->close(); 55 | 56 | if (file_exists($fileName)) { 57 | header('Content-Type: application/zip'); 58 | header('Content-Disposition: attachment; filename="' . basename($realFileName) . '"'); 59 | header('Content-Length: ' . filesize($fileName)); 60 | flush(); 61 | readfile($fileName); 62 | unlink($fileName); 63 | exit; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Resources/Private/Layouts/Iframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {component.name.displayName} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
39 | 40 |
41 |
42 | 43 | 44 | 45 |
46 | 47 |
48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /Classes/EventListener/AssetCollectorInjector.php: -------------------------------------------------------------------------------- 1 | getStyleSheets() as $assetData) { 19 | $assetData['attributes']['href'] = $this->getAbsoluteWebPath($assetData['source']); 20 | $assetData['attributes']['rel'] = $assetData['attributes']['rel'] ?? 'stylesheet'; 21 | $assetData['attributes']['type'] = $assetData['attributes']['type'] ?? 'text/css'; 22 | $event->addHeaderData( 23 | '' 24 | ); 25 | } 26 | 27 | // Add inline css to head 28 | foreach ($assetCollector->getInlineStyleSheets() as $assetData) { 29 | $event->addHeaderData( 30 | '' 32 | ); 33 | } 34 | 35 | // Add script tags to head or body 36 | foreach ($assetCollector->getJavaScripts() as $assetData) { 37 | $assetData['attributes']['src'] = $this->getAbsoluteWebPath($assetData['source']); 38 | $scriptTag = ''; 39 | if ($assetData['options']['priority'] ?? false) { 40 | $event->addHeaderData($scriptTag); 41 | } else { 42 | $event->addFooterData($scriptTag); 43 | } 44 | } 45 | 46 | // Add inline javascript to head or body 47 | foreach ($assetCollector->getInlineJavaScripts() as $assetData) { 48 | $scriptTag = ''; 50 | if ($assetData['options']['priority']) { 51 | $event->addHeaderData($scriptTag); 52 | } else { 53 | $event->addFooterData($scriptTag); 54 | } 55 | } 56 | } 57 | 58 | private function getAbsoluteWebPath(string $file): string 59 | { 60 | if (strpos($file, '://') !== false || strpos($file, '//') === 0) { 61 | return $file; 62 | } 63 | $file = PathUtility::getAbsoluteWebPath(GeneralUtility::getFileAbsFileName($file)); 64 | return GeneralUtility::createVersionNumberedFilename($file); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Configuration/Yaml/FluidStyleguide.yaml: -------------------------------------------------------------------------------- 1 | FluidStyleguide: 2 | Features: 3 | # Enable/Disable markdown documentation rendering 4 | Documentation: true 5 | 6 | # Enable/Disable live editing of component fixture 7 | Editor: true 8 | 9 | # Enable/Disable zip download of component folder 10 | ZipDownload: false 11 | 12 | # Enable/Disable breakpoint switcher 13 | ResponsiveBreakpoints: true 14 | 15 | # Enable/Disable rulers 16 | Ruler: false 17 | 18 | # Escapes string input from the editor. This prevents Cross-Site-Scripting 19 | # but leads to differing component output when using the editor. 20 | EscapeInputFromEditor: true 21 | 22 | # Show demo components in styleguide even if other components exist 23 | DemoComponents: false 24 | 25 | # Enable/Disable support for multiple languages 26 | Languages: false 27 | 28 | # Show code quality tab in component detail view 29 | # uses fluid-components-linter to provide hints to potential problems 30 | CodeQuality: true 31 | 32 | # Markup that will be wrapped around the component output in the styleguide 33 | # This can be overwritten per component fixture by specifying 34 | # "styleguideComponentContext" in the fixture data 35 | ComponentContext: '
|
' 36 | 37 | ComponentAssets: 38 | Global: 39 | Css: 40 | - EXT:fluid_styleguide/Resources/Public/Css/Iframe.css 41 | Javascript: 42 | - 43 | file: EXT:fluid_styleguide/Resources/Public/Build/JavaScript/Iframe.min.js 44 | type: module 45 | Packages: 46 | 'Sitegeist\FluidStyleguide\DemoComponents': 47 | Css: 48 | - EXT:fluid_styleguide/Resources/Public/Css/DemoComponents.css 49 | # 'Vendor\MyExtension\Components': 50 | # Css: 51 | # - EXT:my_extension/Resources/Public/Css/Components.css 52 | # Javascript: 53 | # - EXT:my_extension/Resources/Public/Javascript/Components.js 54 | 55 | ResponsiveBreakpoints: 56 | Desktop: '100%' 57 | Tablet: '768px' 58 | Mobile: '375px' 59 | 60 | Branding: 61 | # Title: 'My Styleguide' 62 | # IntroFile: 'EXT:my_extension/Documentation/FluidStyleguide.md' 63 | IframeBackground: '#FFF' 64 | HighlightColor: '#00d8e6' 65 | FontFamily: "'Open Sans', Helvetica, FreeSans, Arial, sans-serif" 66 | 67 | Languages: 68 | default: 69 | identifier: default 70 | twoLetterIsoCode: en 71 | locale: en_US.UTF-8 72 | hreflang: en 73 | direction: ltr 74 | label: English 75 | 76 | Fluid: 77 | TemplateRootPaths: 78 | - EXT:fluid_styleguide/Resources/Private/Templates 79 | PartialRootPaths: 80 | - EXT:fluid_styleguide/Resources/Private/Partials 81 | LayoutRootPaths: 82 | - EXT:fluid_styleguide/Resources/Private/Layouts 83 | -------------------------------------------------------------------------------- /Resources/Private/Components/Molecule/EditFixtures/EditFixtures.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 |
23 | 24 | 25 | ({item.description}) 26 | 27 |
28 | 29 |
30 | 31 |
32 | 33 | 34 | ({item.description}) 35 | 36 |
37 | 38 |
39 | 40 |
41 |
{item.name}
42 | 43 | ({item.description}) 44 | 45 |
46 | 51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | -------------------------------------------------------------------------------- /Resources/Private/Components/Atom/StyleguideSelect/StyleguideSelect.js: -------------------------------------------------------------------------------- 1 | import {register, find, findAll} from '../../../Javascript/Utils'; 2 | import {StyleguideSelectHelper} from './StyleguideSelectHelper'; 3 | 4 | const StyleguideSelect = el => { 5 | 6 | if (el) { 7 | 8 | // options 9 | const autoSuggest = el.dataset.autosuggest ? el.dataset.autosuggest : false 10 | const submitFormOnSelect = el.dataset.submitformonselect ? el.dataset.submitformonselect : false 11 | 12 | const classPrefix = 'styleguideSelect'; 13 | const nativeSelectElement = el.getElementsByTagName('select')[0]; 14 | const selectLabel = StyleguideSelectHelper.buildFormCustomSelectLabel(nativeSelectElement, autoSuggest); 15 | const select = StyleguideSelectHelper.buildFormCustomSelect(nativeSelectElement, submitFormOnSelect); 16 | const fakeOptions = findAll('div', select); 17 | const autoSuggestInput = find('input', selectLabel); 18 | 19 | // append custom select and custom select label 20 | el.appendChild(selectLabel); 21 | el.appendChild(select); 22 | 23 | 24 | let preSelected; 25 | // handle auto suggest if set in options 26 | if (autoSuggest && autoSuggestInput) { 27 | 28 | autoSuggestInput.addEventListener('input', (e) => { 29 | StyleguideSelectHelper.handleAutoSuggestInput(fakeOptions, select, e.currentTarget); 30 | }) 31 | 32 | autoSuggestInput.addEventListener('focus', (e) => { 33 | e.currentTarget.value = ''; 34 | }) 35 | 36 | autoSuggestInput.addEventListener('blur', (e) => { 37 | StyleguideSelectHelper.handleAutoSuggestBlur(fakeOptions, select, e.currentTarget) 38 | }) 39 | 40 | preSelected = findAll('div', select).find(h => (h.innerHTML === find('input', selectLabel).value.trim())); 41 | 42 | } else { 43 | // preselect option 44 | preSelected = findAll('div', select).find(h => (h.innerHTML === selectLabel.textContent)) 45 | } 46 | 47 | if (typeof preSelected !== 'undefined') { 48 | preSelected.classList.add(`${classPrefix}EqualSelected`); 49 | } 50 | 51 | // apply click event to custom select label 52 | selectLabel.addEventListener('click', (e) => { 53 | e.stopPropagation(); 54 | e.preventDefault(); 55 | 56 | if (autoSuggest && autoSuggestInput) { 57 | autoSuggestInput.focus(); 58 | } 59 | 60 | StyleguideSelectHelper.toggleSelect(select); 61 | 62 | }); 63 | 64 | // apply keyboard events to custom select 65 | document.addEventListener('keydown', event => { 66 | StyleguideSelectHelper.handleSelectNavigationKeypressDown(select, event); 67 | StyleguideSelectHelper.handleSelectNavigationKeypressUp(select, event); 68 | StyleguideSelectHelper.handleSelectNavigationKeypressEnter(select, event); 69 | StyleguideSelectHelper.handleSelectNavigationKeypressEscape(select, event); 70 | }); 71 | 72 | // simulate select focus 73 | 74 | nativeSelectElement.addEventListener('focus', event => { 75 | StyleguideSelectHelper.simulateSelectFocus(select, event); 76 | }) 77 | 78 | nativeSelectElement.addEventListener('blur', event => { 79 | StyleguideSelectHelper.simulateSelectBlur(select, event); 80 | }) 81 | 82 | } 83 | }; 84 | 85 | register('StyleguideSelect', StyleguideSelect); 86 | -------------------------------------------------------------------------------- /Resources/Private/Scss/StyleguideList.scss: -------------------------------------------------------------------------------- 1 | .fluidStyleguideList { 2 | margin-top: 120px; 3 | max-width: 1200px; 4 | margin-left: auto; 5 | margin-right: auto; 6 | 7 | strong { 8 | font-weight: 700; 9 | } 10 | 11 | .fluidStyleguideTitle { 12 | font-size: 40px; 13 | margin-bottom: 16px; 14 | font-family: $styleguide-font; 15 | color: $dark-grey-0; 16 | } 17 | 18 | .fluidStyleguideIntro { 19 | padding-bottom: 64px; 20 | font-family: $styleguide-font; 21 | line-height: 1.7; 22 | 23 | a { 24 | color: inherit; 25 | } 26 | 27 | p { 28 | max-width: 60em; 29 | } 30 | 31 | &::after { 32 | content: ""; 33 | position: relative; 34 | display: block; 35 | bottom: -30px; 36 | left: 0; 37 | right: 0; 38 | height: 3px; 39 | border: #000 solid; 40 | border-width: 1px 0; 41 | } 42 | } 43 | 44 | .fluidStyleguidePackageName { 45 | margin-bottom: 40px; 46 | font-size: 32px; 47 | font-family: $styleguide-font; 48 | color: $dark-grey-0; 49 | } 50 | 51 | .fluidStyleguideComponentName { 52 | font-size: 24px; 53 | font-family: $styleguide-font; 54 | color: $dark-grey-0; 55 | } 56 | 57 | .fluidStyleguidePackageItem { 58 | margin-bottom: 60px; 59 | padding-bottom: 60px; 60 | } 61 | 62 | .fluidStyleguideComponent { 63 | display: block; 64 | width: 100%; 65 | height: 200px; 66 | border: 0; 67 | transition: width .6s; 68 | } 69 | 70 | .fluidStyleguideListIframeWrapper { 71 | margin-bottom: 10px; 72 | } 73 | 74 | .breakPoint { 75 | background-image: repeating-linear-gradient(45deg, $white 0%, $white 2.2%, $light-grey-2 2.2%, $light-grey-3 2.1%, $white 2.5%); 76 | } 77 | 78 | .fluidStyleguideComponentVariants { 79 | &:last-child { 80 | .fluidStyleguideComponentExample { 81 | &:last-child { 82 | border-bottom: 0; 83 | margin-bottom: 0; 84 | padding-bottom: 0; 85 | } 86 | } 87 | } 88 | } 89 | 90 | .fluidStyleguideComponentExample { 91 | margin-bottom: 60px; 92 | border-bottom: 1px solid $dark-grey-0; 93 | padding-bottom: 60px; 94 | } 95 | 96 | .fluidStyleguideBtn { 97 | background: $dark-grey-0; 98 | color: $white; 99 | padding: 16px 53px 18px 30px; 100 | text-align: center; 101 | display: inline-block; 102 | font-size: 14px; 103 | position: relative; 104 | font-family: $styleguide-font; 105 | 106 | &:hover { 107 | color: $highlight; 108 | } 109 | 110 | span { 111 | position: relative; 112 | 113 | &::after { 114 | position: absolute; 115 | font-family: $verdana-font; 116 | content: '›'; 117 | top: -4px; 118 | width: 0; 119 | height: 0; 120 | speak: none; 121 | font-style: normal; 122 | font-weight: normal; 123 | font-variant: normal; 124 | text-transform: none; 125 | line-height: 1; 126 | font-size: 24px; 127 | right: -15px; 128 | color: $highlight; 129 | } 130 | } 131 | 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /Build/UnitTestsBootstrap.php: -------------------------------------------------------------------------------- 1 | getWebRoot(), '/')); 27 | } 28 | if (!getenv('TYPO3_PATH_WEB')) { 29 | putenv('TYPO3_PATH_WEB=' . rtrim($testbase->getWebRoot(), '/')); 30 | } 31 | 32 | $testbase->defineSitePath(); 33 | 34 | $requestType = \TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::REQUESTTYPE_BE | \TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::REQUESTTYPE_CLI; 35 | \TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::run(0, $requestType); 36 | 37 | $testbase->createDirectory(\TYPO3\CMS\Core\Core\Environment::getPublicPath() . '/typo3conf/ext'); 38 | $testbase->createDirectory(\TYPO3\CMS\Core\Core\Environment::getPublicPath() . '/typo3temp/assets'); 39 | $testbase->createDirectory(\TYPO3\CMS\Core\Core\Environment::getPublicPath() . '/typo3temp/var/tests'); 40 | $testbase->createDirectory(\TYPO3\CMS\Core\Core\Environment::getPublicPath() . '/typo3temp/var/transient'); 41 | 42 | // Retrieve an instance of class loader and inject to core bootstrap 43 | $classLoader = require $testbase->getPackagesPath() . '/autoload.php'; 44 | \TYPO3\CMS\Core\Core\Bootstrap::initializeClassLoader($classLoader); 45 | 46 | // Initialize default TYPO3_CONF_VARS 47 | $configurationManager = new \TYPO3\CMS\Core\Configuration\ConfigurationManager(); 48 | $GLOBALS['TYPO3_CONF_VARS'] = $configurationManager->getDefaultConfiguration(); 49 | 50 | $cache = new \TYPO3\CMS\Core\Cache\Frontend\PhpFrontend( 51 | 'core', 52 | new \TYPO3\CMS\Core\Cache\Backend\NullBackend('production', []) 53 | ); 54 | 55 | // Set all packages to active 56 | if (interface_exists(\TYPO3\CMS\Core\Package\Cache\PackageCacheInterface::class)) { 57 | $packageManager = \TYPO3\CMS\Core\Core\Bootstrap::createPackageManager( 58 | \TYPO3\CMS\Core\Package\UnitTestPackageManager::class, 59 | \TYPO3\CMS\Core\Core\Bootstrap::createPackageCache($cache) 60 | ); 61 | } else { 62 | // v10 compatibility layer 63 | $packageManager = \TYPO3\CMS\Core\Core\Bootstrap::createPackageManager( 64 | \TYPO3\CMS\Core\Package\UnitTestPackageManager::class, 65 | $cache 66 | ); 67 | } 68 | 69 | \TYPO3\CMS\Core\Utility\GeneralUtility::setSingletonInstance(\TYPO3\CMS\Core\Package\PackageManager::class, $packageManager); 70 | \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::setPackageManager($packageManager); 71 | 72 | $testbase->dumpClassLoadingInformation(); 73 | 74 | \TYPO3\CMS\Core\Utility\GeneralUtility::purgeInstances(); 75 | }); 76 | -------------------------------------------------------------------------------- /Resources/Private/Templates/Styleguide/Show.html: -------------------------------------------------------------------------------- 1 | {namespace fsc=Sitegeist\FluidStyleguide\Components} 2 | {namespace fsv=Sitegeist\FluidStyleguide\ViewHelpers} 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 23 | 24 |
25 | 26 | 27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | Overview 36 |
37 |
38 | 39 | 40 | 44 | 45 |
46 | 47 |
48 | 49 | 60 |
61 | -------------------------------------------------------------------------------- /Resources/Private/Components/Atom/StyleguideSelect/StyleguideSelect.scss: -------------------------------------------------------------------------------- 1 | .styleguideSelect { 2 | font-family: $styleguide-font; 3 | width: 100%; 4 | 5 | datalist { 6 | display: block; 7 | } 8 | 9 | select { 10 | position: absolute; 11 | left: -999999px; 12 | } 13 | 14 | &.hasIcon { 15 | .styleguideSelectSelected { 16 | &::after { 17 | font-family: 'fluid-styleguide'; 18 | position: absolute; 19 | content: '›'; 20 | top: 9px; 21 | right: 56px; 22 | width: 0; 23 | height: 0; 24 | speak: none; 25 | font-style: normal; 26 | font-weight: normal; 27 | font-variant: normal; 28 | text-transform: none; 29 | line-height: 1; 30 | font-size: 38px; 31 | color: $highlight; 32 | } 33 | 34 | &.styleguideSelectArrowActive::after { 35 | content: '\e911'; 36 | } 37 | } 38 | } 39 | 40 | &Focus { 41 | border-color: $white; 42 | color: $white; 43 | } 44 | 45 | &Items div, &Selected { 46 | display: flex; 47 | align-items: center; 48 | height: 55px; 49 | cursor: pointer; 50 | padding: 0 24px; 51 | font-size: 13px; 52 | color: $white; 53 | border-bottom: 1px solid $grey; 54 | font-family: $styleguide-font; 55 | 56 | &.styleguideSelectEqualSelected { 57 | background-color: $dark-grey-0; 58 | color: $highlight; 59 | border-color: $dark-grey-3; 60 | } 61 | 62 | &.styleguideSelectArrowActive { 63 | color: $white; 64 | } 65 | } 66 | 67 | 68 | &Selected { 69 | background: $dark-grey-0; 70 | color: $white; 71 | cursor: pointer; 72 | border-color: transparent; 73 | 74 | &:hover { 75 | color: $highlight; 76 | } 77 | } 78 | 79 | input[type='text'] { 80 | padding: 0; 81 | font-size: 13px; 82 | color: $white; 83 | font-family: $styleguide-font; 84 | outline: none; 85 | width: 100%; 86 | background-color: transparent; 87 | border: 0 none; 88 | cursor: pointer; 89 | 90 | &:hover { 91 | color: $highlight; 92 | } 93 | 94 | &:focus { 95 | color: $highlight; 96 | } 97 | 98 | &::placeholder { 99 | color: $light-grey; 100 | font-size: 13px; 101 | } 102 | } 103 | 104 | &Items { 105 | cursor: pointer; 106 | transition: transform .15s ease-out; 107 | transform: scaleY(0); 108 | height: 0; 109 | transform-origin: top; 110 | overflow: auto; 111 | position: absolute; 112 | background-color: $dark-grey-1; 113 | left: 0; 114 | right: 0; 115 | z-index: 9999; 116 | max-height: 617px !important; 117 | 118 | &.selectPositionedTop { 119 | border-bottom: 0; 120 | } 121 | 122 | &::-webkit-scrollbar { 123 | width: 8px; 124 | } 125 | 126 | &::-webkit-scrollbar-thumb { 127 | background-color: $highlight; 128 | } 129 | 130 | &.styleguideSelectOpened { 131 | transition: transform .15s ease-out; 132 | height: auto; 133 | transform: scaleY(1); 134 | transform-origin: top; 135 | border-top: 1px solid $dark-grey-3; 136 | } 137 | } 138 | 139 | &Hide { 140 | display: none; 141 | } 142 | 143 | &Items div:hover, 144 | &Active { 145 | color: $highlight !important; 146 | transition: color .25s ease; 147 | } 148 | 149 | &.orientationBottom { 150 | .styleguideSelectItems { 151 | transform-origin: bottom; 152 | } 153 | } 154 | 155 | &InputLabel { 156 | position: absolute; 157 | left: -99999px; 158 | top: -99999px; 159 | padding: 0 16px; 160 | } 161 | 162 | } 163 | 164 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fluid Styleguide – Living Styleguide for TYPO3 2 | 3 | Fluid Styleguide is a design collaboration tool for TYPO3 projects. It supports frontend developers in creating reusable 4 | components and encourages effective communication across all project stakeholders. 5 | 6 | **[Try the live demo](https://fluidcomponents.sitegeist.de/fluid-styleguide/list)** 7 | 8 | ## Target Groups 9 | 10 | Fluid Styleguide can be a useful tool for all project stakeholders: 11 | 12 | * **designers and frontend developers** can improve their development and QA workflows 13 | * **frontend, backend and integration** discuss and coordinate data structures and interfaces between the stacks 14 | * **project managers and product owners** see the current state of the project's components 15 | * **clients** get more transparency of the project status 16 | 17 | ## Features 18 | 19 | * visualization of project components 20 | * isolated development of components 21 | * responsive testing 22 | * integrated component documentation 23 | * support for multiple languages 24 | * support for frontend assets provided by TYPO3's asset collector 25 | * shows code quality problems based on [fluid-components-linter](https://github.com/sitegeist/fluid-components-linter) 26 | * basic skinning to fit the project's brand colors 27 | * zip download 28 | * easy and flexible configuration via [yaml file](./Documentation/ConfigurationReference.md) 29 | * live editing of example data [BETA] 30 | 31 | ## Getting Started 32 | 33 | Just follow these simple steps to get started with the styleguide: 34 | 35 | 1. Install Fluid Styleguide 36 | 37 | via composer: 38 | 39 | composer require sitegeist/fluid-styleguide 40 | 41 | or download the extension from TER: 42 | 43 | [TER: fluid_styleguide](https://extensions.typo3.org/extension/fluid_styleguide/) 44 | 45 | 2. Test Fluid Styleguide with demo components 46 | 47 | Just open the page `/fluid-styleguide/` in your TYPO3 installation: 48 | 49 | https://my-domain.tld/fluid-styleguide/ 50 | 51 | To add your own components to the styleguide, just follow these additional steps: 52 | 53 | 3. Configure Fluid Components 54 | 55 | Make sure to define the component namespace in your **ext_localconf.php**: 56 | 57 | ```php 58 | $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['fluid_components']['namespaces']['VENDOR\\MyExtension\\Components'] = 59 | \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath('my_extension', 'Resources/Private/Components'); 60 | ``` 61 | 62 | Use your own vendor name for `VENDOR`, extension name for `MyExtension`, and extension key for `my_extension`. 63 | 64 | 4. Configure your frontend assets 65 | 66 | Create a styleguide configuration file in your extension or sitepackage. 67 | 68 | **Configuration/Yaml/FluidStyleguide.yaml:** 69 | 70 | ```yaml 71 | FluidStyleguide: 72 | ComponentAssets: 73 | Packages: 74 | 'Vendor\MyExtension\Components': 75 | Css: 76 | - 'EXT:my_extension/Resources/Public/Css/Main.min.css' 77 | Javascript: 78 | - 'EXT:my_extension/Resources/Public/Javascript/Main.min.js' 79 | ``` 80 | 81 | Use your own vendor name for `VENDOR`, extension name for `MyExtension`, and extension key for `my_extension`. 82 | Adjust the paths to the assets according to your directory structure. 83 | 84 | 5. Start [building your own components](./Documentation/BuildingComponents.md) using Fluid Components and fixture files 85 | 86 | If you have any questions, need support or want to discuss components in TYPO3, feel free to join [#ext-fluid_components](https://typo3.slack.com/archives/ext-fluid_components). 87 | 88 | ## Documentation 89 | 90 | * [Building Components with Fluid Styleguide](./Documentation/BuildingComponents.md) 91 | * [Styleguide Configuration Reference](./Documentation/ConfigurationReference.md) 92 | 93 | ## License 94 | 95 | This project is licensed under the GPL-3.0-or-later license. 96 | See the LICENSE file for details. 97 | 98 | The project includes third-party dependencies that are distributed under their respective open-source licenses: 99 | 100 | @sitegeist/vitesse — MIT License 101 | iframe-resizer — GPL-3.0 License 102 | 103 | All bundled assets remain subject to the licenses of their original authors. 104 | -------------------------------------------------------------------------------- /Documentation/BuildingComponents.md: -------------------------------------------------------------------------------- 1 | # Building Components with Fluid Styleguide 2 | 3 | Components must meet the following requirements for them to show up in the styleguide correctly: 4 | 5 | 1. The **component namespace must be registered** for Fluid Components (as shown in [Getting Started](../README.md)) 6 | 2. The component folder must contain a **fixture file** which at least contains the `default` fixture (see below). 7 | 3. To load your **frontend assets**, you need to specify them in the FluidStyleguide.yaml configuration file 8 | (as shown in [Getting Started](../README.md)) 9 | 10 | ## Example Component 11 | 12 | For illustration purposes we want to add the following component to the styleguide: 13 | 14 | **Molecule/Teaser/Teaser.html:** 15 | 16 | ```xml 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |

{title}

27 | 28 |

{content}

29 |
30 | 31 | {actions -> f:format.raw()} 32 | 33 | 34 | 35 | 36 |
37 |
38 |
39 | ``` 40 | 41 | For further instructions on how to build components, please refer to the [documentation of Fluid Components](https://github.com/sitegeist/fluid-components). 42 | 43 | ## Adding fixtures to your component 44 | 45 | Each component in the styleguide needs a fixture file which contains example values for all of the component's required arguments. 46 | A fixture file must at least contain a `default` fixture, but it may define additional fixtures that can then be selected 47 | in the styleguide interface. 48 | 49 | A fixture can be created in `.php`, `.json`, [`.json5`](https://json5.org/), `.yml` or `.yaml` files. You should create only one fixture file per 50 | component. The styleguide takes the first fixture file and ignores eventually existing files in other formats in the following order: 51 | 52 | 1. `.php` 53 | 2. `.json` 54 | 3. `.json5` 55 | 4. `.yml` 56 | 5. `.yaml` 57 | 58 | Support for `json5` only works if you install the fluid-styleguide with composer (not by TER!). 59 | 60 | **Molecule/Teaser/Teaser.fixture.php:** 61 | 62 | ```php 63 | return [ 64 | 'default' => [ 65 | 'title' => 'TYPO3', 66 | 'link' => 'https://typo3.org', 67 | ], 68 | 'dark' => [ 69 | 'title' => 'TYPO3', 70 | 'link' => 'https://typo3.org', 71 | 'theme' => 'dark', 72 | ], 73 | ]; 74 | ``` 75 | 76 | or **Molecule/Teaser/Teaser.fixture.json:** 77 | 78 | ```json 79 | { 80 | "default": { 81 | "title": "TYPO3", 82 | "link": "https://typo3.org" 83 | }, 84 | "dark": { 85 | "title": "TYPO3", 86 | "link": "https://typo3.org", 87 | "theme": "dark" 88 | } 89 | } 90 | ``` 91 | 92 | or **Molecule/Teaser/Teaser.fixture.json5:** 93 | 94 | ```json5 95 | { 96 | // can contain comments 97 | "default": { 98 | "title": "TYPO3 \ 99 | can contain multiline strings", 100 | "link": "https://typo3.org" 101 | }, 102 | "dark": { 103 | "title": 'TYPO3 with single quotes', 104 | "link": "https://typo3.org", 105 | "theme": "dark" 106 | } 107 | } 108 | ``` 109 | 110 | or **Molecule/Teaser/Teaser.fixture.yaml:** 111 | 112 | ```yaml 113 | default: 114 | title: | 115 | TYPO3 116 | Multiline 117 | link: https://typo3.org 118 | dark: 119 | title: TYPO3 120 | link: https://typo3.org 121 | theme: dark 122 | ``` 123 | 124 | File naming scheme: *{ComponentName}.fixture.[php|json|[json5](https://json5.org/)|yml|yaml]* 125 | 126 | You can define the content of a component if the component supports it: 127 | 128 | ```json 129 | { 130 | "default": { 131 | "content": "The professional, flexible Content Management System" 132 | } 133 | } 134 | ``` 135 | 136 | You can use fluid in your fixture data to nest components (as long as the Fluid namespace [is defined globally](https://docs.typo3.org/c/typo3/cms-core/master/en-us/Changelog/8.5/Feature-78415-GlobalFluidViewHelperNamespacesMovedToTYPO3Configuration.html)): 137 | 138 | ```json 139 | { 140 | "default": { 141 | "actions": "Primary ButtonSecondary Button" 142 | } 143 | } 144 | ``` 145 | 146 | You can use [data structures with argument converters](https://github.com/sitegeist/fluid-components/blob/master/Documentation/DataStructures.md) to define placeholder images and advanced links: 147 | 148 | ```json 149 | { 150 | "default": { 151 | "image": { 152 | "height": 300, 153 | "width": 500, 154 | "alternative": "My alt text", 155 | "title": "My image title" 156 | }, 157 | "link": { 158 | "uri": "https://typo3.org", 159 | "target": "_blank" 160 | } 161 | } 162 | } 163 | ``` 164 | 165 | You can provide a label for each fixture: 166 | 167 | ```json 168 | "onDarkBackground": { 169 | "styleguideFixtureLabel": "On dark background", 170 | }, 171 | ``` 172 | 173 | You can override the default [component context](./ConfigurationReference.md) for each fixture: 174 | 175 | ```json 176 | { 177 | "onDarkBackground": { 178 | "styleguideComponentContext": "
|
" 179 | } 180 | } 181 | ``` 182 | 183 | ## Adding documentation to your component 184 | 185 | If you want to add further documentation to your component, just place a markdown file that is named like your component 186 | inside your component folder. Fluid Styleguide will pick up the documentation automatically and render it in the DOC tab. 187 | 188 | **Molecule/Teaser/Teaser.md:** 189 | 190 | ```markdown 191 | ## Teaser Component 192 | 193 | This is a generic teaser components. It supports both a light and a dark styling. [...] 194 | ``` 195 | 196 | File naming scheme: *{ComponentName}.md* 197 | -------------------------------------------------------------------------------- /Classes/Domain/Model/Component.php: -------------------------------------------------------------------------------- 1 | name; 29 | } 30 | 31 | public function getLocation(): ComponentLocation 32 | { 33 | return $this->location; 34 | } 35 | 36 | public function getFixtureFile(): string 37 | { 38 | $fixtureFilesToSearch = [ 39 | '.fixture.php', 40 | '.fixture.json', 41 | '.fixture.yml', 42 | '.fixture.yaml' 43 | ]; 44 | if (Environment::isComposerMode()) { 45 | $fixtureFilesToSearch[] = '.fixture.json5'; 46 | } 47 | 48 | foreach ($fixtureFilesToSearch as $fixtureFile) { 49 | $path = $this->location->generatePathToFile($this->name->getSimpleName() . $fixtureFile); 50 | if (file_exists($path)) { 51 | return $path; 52 | } 53 | } 54 | // fallback to .json 55 | return $this->location->generatePathToFile($this->name->getSimpleName() . '.fixture.json'); 56 | } 57 | 58 | public function hasFixtures(): bool 59 | { 60 | return file_exists($this->getFixtureFile()); 61 | } 62 | 63 | public function getFixtures(): array 64 | { 65 | if (isset($this->fixtures)) { 66 | return $this->fixtures; 67 | } 68 | 69 | if (!$this->hasFixtures()) { 70 | return $this->fixtures; 71 | } 72 | 73 | $fixtureFile = $this->getFixtureFile(); 74 | $fileParts = pathinfo((string) $fixtureFile); 75 | switch ($fileParts['extension']) { 76 | case 'json': 77 | $fixtures = \json_decode(file_get_contents($fixtureFile), true) ?? []; 78 | break; 79 | case 'json5': 80 | if (function_exists('json5_decode')) { 81 | $fixtures = \json5_decode(file_get_contents($fixtureFile), true) ?? []; 82 | } else { 83 | $fixtures = []; 84 | } 85 | break; 86 | case 'yaml': 87 | case 'yml': 88 | $loader = GeneralUtility::makeInstance(YamlFileLoader::class); 89 | $fixtures = $loader->load($fixtureFile) ?? []; 90 | break; 91 | case 'php': 92 | $fixtures = require $fixtureFile; 93 | break; 94 | default: 95 | throw new \Exception('Fixture format unknown', 1582196195); 96 | } 97 | if (!is_array($fixtures)) { 98 | throw new \InvalidArgumentException('Fixtures must be of type array', 1738326135); 99 | } 100 | if (!isset($fixtures['default'])) { 101 | $fixtures['default'] = []; 102 | } 103 | foreach ($fixtures as $fixtureName => $fixtureData) { 104 | $this->fixtures[$fixtureName] = new ComponentFixture( 105 | $fixtureFile, 106 | $fixtureName, 107 | $fixtureData 108 | ); 109 | } 110 | 111 | return $this->fixtures; 112 | } 113 | 114 | public function getFixture(string $name): ?ComponentFixture 115 | { 116 | return $this->getFixtures()[$name] ?? null; 117 | } 118 | 119 | public function getDocumentationFile(): string 120 | { 121 | return $this->location->generatePathToFile($this->name->getSimpleName() . '.md'); 122 | } 123 | 124 | public function hasDocumentation(): bool 125 | { 126 | return file_exists($this->getDocumentationFile()); 127 | } 128 | 129 | public function getDocumentation(): string 130 | { 131 | if (!isset($this->documentation)) { 132 | if ($this->hasDocumentation()) { 133 | $this->documentation = file_get_contents($this->getDocumentationFile()); 134 | } else { 135 | $this->documentation = ''; 136 | } 137 | } 138 | 139 | return $this->documentation; 140 | } 141 | 142 | public function getArguments(): array 143 | { 144 | if (!isset($this->arguments)) { 145 | $this->arguments = $this->getComponentRenderer()->prepareArguments(); 146 | } 147 | 148 | return $this->arguments; 149 | } 150 | 151 | public function getDefaultValues(): array 152 | { 153 | return array_reduce($this->getArguments(), function ($defaults, $argument) { 154 | if (!$argument->isRequired()) { 155 | $defaults[$argument->getName()] = $argument->getDefaultValue(); 156 | } 157 | return $defaults; 158 | }, []); 159 | } 160 | 161 | public function getCodeQualityConfiguration(): ?string 162 | { 163 | $configurationFileLocations = [ 164 | $this->getLocation()->getDirectory(), 165 | $this->getName()->getPackage()->getPath(), 166 | rtrim($this->getName()->getPackage()->getExtension()->getPackagePath(), DIRECTORY_SEPARATOR) 167 | ]; 168 | foreach ($configurationFileLocations as $configurationFileLocation) { 169 | $configurationFile = $configurationFileLocation . DIRECTORY_SEPARATOR . '.fclint.json'; 170 | if (file_exists($configurationFile)) { 171 | return $configurationFile; 172 | } 173 | } 174 | return null; 175 | } 176 | 177 | protected function getComponentRenderer(): ComponentRenderer 178 | { 179 | $componentRenderer = GeneralUtility::makeInstance(ComponentRenderer::class); 180 | $componentRenderer->setComponentNamespace($this->name->getIdentifier()); 181 | return $componentRenderer; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /Documentation/ConfigurationReference.md: -------------------------------------------------------------------------------- 1 | # Fluid Styleguide Configuration Reference 2 | 3 | Fluid Styleguide can be configured in various ways by creating a YAML configuration file in your extension or sitepackage: 4 | 5 | **Configuration/Yaml/FluidStyleguide.yaml** 6 | 7 | Each extension can add its own configuration to the styleguide, all available files will be picked up automatically and merged with the [default configuration file](../Configuration/Yaml/FluidStyleguide.yaml). 8 | 9 | ## Adding frontend assets 10 | 11 | Each component package (= a folder containing multiple components in a directory structure) usually brings its own frontend assets 12 | that define the design (CSS) and behavior (JavaScript) of the components. 13 | 14 | Assets can be defined per component package: 15 | 16 | ```yaml 17 | FluidStyleguide: 18 | ComponentAssets: 19 | Packages: 20 | 'Vendor\MyExtension\Components': 21 | Css: 22 | - EXT:my_extension/Resources/Public/Css/Components.css 23 | Javascript: 24 | - EXT:my_extension/Resources/Public/Javascript/Components.js 25 | ``` 26 | 27 | Assets can also be added globally: 28 | 29 | ```yaml 30 | FluidStyleguide: 31 | ComponentAssets: 32 | Global: 33 | Css: 34 | - EXT:my_extension/Resources/Public/Css/Global.css 35 | Javascript: 36 | - EXT:my_extension/Resources/Public/Javascript/Global.min.js 37 | ``` 38 | 39 | For JavaScript assets, you can define if they should be added to the `` or to the 40 | bottom of the page (which is the default): 41 | 42 | ```yaml 43 | FluidStyleguide: 44 | ComponentAssets: 45 | Global: 46 | Javascript: 47 | - 48 | file: EXT:my_extension/Resources/Public/Javascript/Global.min.js 49 | position: head 50 | ``` 51 | 52 | ## Modifying the component context 53 | 54 | While most components can function without a specific context around them, for 55 | some components their context is quite important. For example, a button component 56 | could have a special styling when used on dark backgrounds. In this case the styleguide 57 | should use a dark background as well when this variant of the button is displayed. 58 | 59 | By default, Fluid Styleguide uses the following component context, which adds 60 | a 24px space around each component: 61 | 62 | ```yaml 63 | FluidStyleguide: 64 | # Markup that will be wrapped around the component output in the styleguide 65 | # This can be overwritten per component fixture by specifying 66 | # "styleguideComponentContext" in the fixture data 67 | ComponentContext: '
|
' 68 | ``` 69 | 70 | Fluid markup is supported in the component context, which means that you can wrap 71 | your components in other components in the styleguide. Every pipe character within 72 | the specified context markup will be replaced with the component markup. 73 | 74 | This context can be modified either globally in your FluidStyleguide.yaml or 75 | individually for each variant of a component in the appropriate fixture file: 76 | 77 | Button.fixture.json: 78 | 79 | ```json 80 | { 81 | "default": { 82 | ... 83 | }, 84 | "onDarkBackground": { 85 | ... 86 | "styleguideComponentContext": "
|
" 87 | }, 88 | "inTwoColumnGrid": { 89 | ... 90 | "styleguideComponentContext": "
|
|
" 91 | } 92 | } 93 | ``` 94 | 95 | Instead of inline html, you can also specify a path to a file which contains the context markup: 96 | 97 | ```yaml 98 | FluidStyleguide: 99 | ComponentContext: 'EXT:my_extension/Resources/Private/Components/ComponentContext.html' 100 | ``` 101 | 102 | or per component: 103 | 104 | ```json 105 | { 106 | "default": { 107 | ... 108 | }, 109 | "onDarkBackground": { 110 | ... 111 | "styleguideComponentContext": "EXT:my_extension/Resources/Private/Components/DarkComponentContext.html" 112 | } 113 | } 114 | ``` 115 | 116 | ## Enabling and disabling styleguide features 117 | 118 | Specific features of the styleguide can be enabled and disabled: 119 | 120 | ```yaml 121 | FluidStyleguide: 122 | Features: 123 | # Enable/Disable markdown documentation rendering 124 | Documentation: true 125 | 126 | # Enable/Disable live editing of component fixture 127 | Editor: true 128 | 129 | # Enable/Disable zip download of component folder 130 | ZipDownload: false 131 | 132 | # Enable/Disable breakpoint switcher 133 | ResponsiveBreakpoints: true 134 | 135 | # Enable/Disable rulers 136 | Ruler: false 137 | 138 | # Escapes string input from the editor. This prevents Cross-Site-Scripting 139 | # but leads to differing component output when using the editor. 140 | EscapeInputFromEditor: true 141 | 142 | # Show demo components in styleguide even if other components exist 143 | DemoComponents: false 144 | 145 | # Enable/Disable support for multiple languages 146 | Languages: false 147 | 148 | # Show code quality tab in component detail view 149 | # uses fluid-components-linter to provide hints to potential problems 150 | CodeQuality: true 151 | ``` 152 | 153 | ## Intro text and branding 154 | 155 | Some of the colors and styles the styleguide uses can be customized to match the customer's branding: 156 | 157 | ```yaml 158 | FluidStyleguide: 159 | Branding: 160 | IframeBackground: '#FFF' 161 | HighlightColor: '#00d8e6' 162 | FontFamily: "'Open Sans', Helvetica, FreeSans, Arial, sans-serif" 163 | ``` 164 | 165 | You can define both a title and an intro text (in a separate markdown file) for the styleguide that will both appear above the component listing: 166 | 167 | ```yaml 168 | FluidStyleguide: 169 | Branding: 170 | Title: 'Customer Styleguide' 171 | IntroFile: 'EXT:my_extension/Documentation/FluidStyleguide.md' 172 | ``` 173 | 174 | ## Specifying responsive breakpoints for testing 175 | 176 | The default responsive breakpoints can be altered or extended. 177 | 178 | ```yaml 179 | FluidStyleguide: 180 | ResponsiveBreakpoints: 181 | Desktop: '100%' 182 | Tablet: '800px' 183 | Mobile: '400px' 184 | ``` 185 | 186 | ## Check for code quality problems 187 | 188 | If [fluid-components-linter](https://github.com/sitegeist/fluid-components-linter) is installed in 189 | your project and the code quality feature is enabled, a code quality tab will appear in the styleguide 190 | detail view. It lists all code quality problems of the current component. 191 | 192 | The default configuration of the linter will be used, however the styleguide checks the following 193 | locations for a `.fclint.json` file: 194 | 195 | * component directory (e. g. `EXT:my_extension/Resources/Private/Components/MyComponent/.fclint.json`) 196 | * component package directory (e. g. `EXT:my_extension/Resources/Private/Components/.fclint.json`) 197 | * extension directory (e. g. `EXT:my_extension/.fclint.json`) 198 | 199 | ## Support for multiple languages 200 | 201 | The styleguide has basic support for multiple languages. First, you need to enable the feature 202 | flag. Then you can specify the languages that should be available in the styleguide. The `default` 203 | language is predefined in the [default configuration file](./Configuration/Yaml/FluidStyleguide.yaml). 204 | 205 | ```yaml 206 | FluidStyleguide: 207 | Features: 208 | Languages: true 209 | 210 | Languages: 211 | de: 212 | identifier: de 213 | twoLetterIsoCode: de 214 | locale: de_DE.UTF-8 215 | hreflang: de 216 | direction: ltr 217 | label: Deutsch 218 | ``` 219 | -------------------------------------------------------------------------------- /Resources/Private/Components/Organism/StyleguideToolbar/StyleguideToolbar.scss: -------------------------------------------------------------------------------- 1 | .styleguideToolbar { 2 | position: fixed; 3 | width: 100%; 4 | color: $white; 5 | font-size: 13px; 6 | z-index: 3; 7 | font-family: $styleguide-font; 8 | transition: bottom .25s ease 0s; 9 | background-color: $dark-grey-0; 10 | 11 | .toolbarTop { 12 | height: 16px; 13 | display: flex; 14 | align-items: center; 15 | justify-content: center; 16 | } 17 | 18 | .toolbarBot { 19 | position: relative; 20 | } 21 | 22 | .toolbarOpener { 23 | cursor: pointer; 24 | position: relative; 25 | top: 0; 26 | border-radius: 10%; 27 | width: 50px; 28 | height: 54px; 29 | z-index: -2; 30 | background: $dark-grey-0; 31 | 32 | &::before { 33 | font-family: $verdana-font; 34 | content: '›'; 35 | width: 0; 36 | height: 0; 37 | speak: none; 38 | font-style: normal; 39 | font-weight: normal; 40 | font-variant: normal; 41 | text-transform: none; 42 | line-height: 1; 43 | font-size: 24px; 44 | color: $highlight; 45 | left: 12px; 46 | top: 19px; 47 | transform: rotate(-90deg); 48 | position: absolute; 49 | } 50 | 51 | &:hover { 52 | color: $highlight; 53 | } 54 | } 55 | 56 | .toolbarRuler { 57 | cursor: pointer; 58 | 59 | svg { 60 | margin-left: 15px; 61 | position: relative; 62 | top: -3px; 63 | } 64 | 65 | &:hover { 66 | color: $highlight; 67 | } 68 | } 69 | 70 | &.open { 71 | bottom: 0 !important; 72 | 73 | .toolbarOpener { 74 | &::before { 75 | left: 38px; 76 | top: 8px; 77 | transform: rotate(90deg); 78 | } 79 | } 80 | 81 | .toolbarTabs { 82 | display: block; 83 | } 84 | 85 | .toolbarBot { 86 | top: -16px; 87 | } 88 | } 89 | 90 | .toolbarTabs { 91 | position: relative; 92 | 93 | .tabNav { 94 | display: flex; 95 | 96 | .tabOpener { 97 | height: 54px; 98 | background: $dark-grey-1; 99 | cursor: pointer; 100 | width: 150px; 101 | display: flex; 102 | justify-content: center; 103 | align-items: center; 104 | color: $white; 105 | border-right: 2px solid $dark-grey-0; 106 | transition: background-color .25s ease; 107 | position: relative; 108 | overflow: hidden; 109 | 110 | svg { 111 | width: 28px; 112 | height: 28px; 113 | fill: $white; 114 | } 115 | 116 | &.active { 117 | background: $dark-grey-0; 118 | color: $highlight; 119 | 120 | svg { 121 | fill: $highlight; 122 | } 123 | } 124 | 125 | &.betaSign { 126 | 127 | &::after { 128 | content: 'Beta'; 129 | position: absolute; 130 | transform: rotate(42deg); 131 | right: -33px; 132 | top: -16px; 133 | font-size: 10px; 134 | background: $red; 135 | padding: 1px; 136 | color: $white; 137 | width: 48px; 138 | padding-left: 100px; 139 | } 140 | } 141 | 142 | &:hover { 143 | svg { 144 | fill: $highlight; 145 | } 146 | } 147 | } 148 | } 149 | 150 | .tabContent { 151 | display: none; 152 | padding: 32px 32px 16px; 153 | font-family: SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; 154 | font-size: 1em; 155 | color: $white; 156 | max-height: 200px; 157 | overflow-y: scroll; 158 | 159 | a { 160 | color: $highlight; 161 | } 162 | 163 | > :first-child { 164 | margin-top: 0; 165 | } 166 | 167 | &::-webkit-scrollbar { 168 | width: 8px; 169 | } 170 | 171 | &::-webkit-scrollbar-thumb { 172 | background-color: $highlight; 173 | } 174 | 175 | &.active { 176 | display: block; 177 | } 178 | 179 | .downloadZip { 180 | color: $white; 181 | text-decoration: underline; 182 | 183 | &:hover { 184 | color: $highlight; 185 | } 186 | } 187 | } 188 | 189 | } 190 | 191 | .componentArguments { 192 | overflow: auto; 193 | 194 | table { 195 | border-spacing: 0; 196 | min-width: 100%; 197 | } 198 | 199 | th, 200 | td { 201 | padding: 12px 8px; 202 | border-bottom: 1px $white solid; 203 | } 204 | 205 | th { 206 | text-align: left; 207 | font-weight: bold; 208 | } 209 | 210 | .componentArgumentsName { 211 | font-weight: bold; 212 | white-space: nowrap; 213 | } 214 | 215 | .componentArgumentsType { 216 | white-space: nowrap; 217 | } 218 | 219 | .componentArgumentsRequired { 220 | padding-left: 0; 221 | padding-right: 0; 222 | text-align: center; 223 | } 224 | 225 | .componentArgumentsDefault, 226 | .componentArgumentsDescription { 227 | font-size: 0.85em; 228 | word-wrap: break-word; 229 | } 230 | } 231 | 232 | .componentDocumentation { 233 | line-height: 1.8; 234 | max-width: 70em; 235 | 236 | pre, code { 237 | background: $dark-grey-1; 238 | } 239 | 240 | pre { 241 | padding: 16px; 242 | } 243 | 244 | code { 245 | padding: 0.3em 0.5em; 246 | border-radius: 3px; 247 | } 248 | 249 | pre > code { 250 | padding: 0; 251 | border-radius: 0; 252 | } 253 | } 254 | 255 | .qualityIssues { 256 | list-style: none; 257 | padding: 0; 258 | border-bottom: $white solid; 259 | border-width: 1px 1px 0; 260 | border-spacing: 0; 261 | min-width: 100%; 262 | 263 | th, td { 264 | padding: 12px 8px; 265 | border-bottom: 1px $white solid; 266 | } 267 | 268 | th { 269 | text-align: left; 270 | font-weight: bold; 271 | } 272 | 273 | .qualityIssueSeverity { 274 | text-align: center; 275 | padding: 12px 4px; 276 | 277 | span { 278 | padding: .4em .8em; 279 | } 280 | } 281 | 282 | .qualityIssueMessage { 283 | width: 100%; 284 | } 285 | 286 | .qualityIssue { 287 | &--major, 288 | &--critical, 289 | &--blocker { 290 | .qualityIssueSeverity span { 291 | background: $issue-major; 292 | font-weight: bold; 293 | } 294 | } 295 | 296 | &--minor .qualityIssueSeverity span { 297 | background: $issue-minor; 298 | font-weight: bold; 299 | } 300 | } 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /Classes/Middleware/StyleguideRouter.php: -------------------------------------------------------------------------------- 1 | getAttribute('site', null); 52 | 53 | // Extract url prefix from styleguide configuration 54 | $prefix = $this->extensionConfiguration->get('fluid_styleguide', 'uriPrefix'); 55 | $prefixWithoutSlash = rtrim($prefix, '/'); 56 | $prefix = $prefixWithoutSlash . '/'; 57 | 58 | // Check if fluid styleguide should be rendered 59 | if (!str_starts_with((string) $request->getUri()->getPath(), $prefixWithoutSlash)) { 60 | return $handler->handle($request); 61 | } 62 | 63 | // Correct calls without trailing slash in request url 64 | if (!str_starts_with((string) $request->getUri()->getPath(), $prefix)) { 65 | return new RedirectResponse( 66 | $request->getUri()->withPath($prefix . static::DEFAULT_ACTION) 67 | ); 68 | } 69 | 70 | // Extract routing information from URI 71 | $path = substr((string) $request->getUri()->getPath(), strlen($prefix)); 72 | $pathSegments = explode('/', $path); 73 | $actionName = array_shift($pathSegments) ?? ''; 74 | $actionName = preg_replace('#[^a-z]#i', '', $actionName); 75 | 76 | // Redirect to default action 77 | if ($actionName === '') { 78 | return new RedirectResponse( 79 | $request->getUri()->withPath($prefix . static::DEFAULT_ACTION) 80 | ); 81 | } 82 | 83 | // Create controller 84 | $controller = $this->container->get(StyleguideController::class); 85 | 86 | // Validate controller action 87 | $actionMethod = $actionName . 'Action'; 88 | if (!method_exists($controller, $actionMethod)) { 89 | throw new \Exception( 90 | 'Invalid styleguide action name: ' . $actionName, 91 | 1566584663 92 | ); 93 | } 94 | 95 | // Build simple TSFE object for basic typolink support in styleguide 96 | $GLOBALS['TSFE'] = GeneralUtility::makeInstance( 97 | TypoScriptFrontendController::class, 98 | $this->context, 99 | $GLOBALS['TYPO3_CURRENT_SITE'], 100 | $request->getAttribute('language', $site->getDefaultLanguage()), 101 | new PageArguments(0, '0', []), 102 | $this->frontendUserAuthentication 103 | ); 104 | 105 | // Call action 106 | $actionArguments = array_replace( 107 | $request->getQueryParams() ?? [], 108 | $request->getParsedBody() ?? [] 109 | ); 110 | 111 | // Initialize language handling 112 | $styleguideConfigurationManager = $this->container->get(StyleguideConfigurationManager::class); 113 | if ($styleguideConfigurationManager->isFeatureEnabled('Languages')) { 114 | // Determine language based on GET parameter 115 | $styleguideLanguage = $styleguideConfigurationManager->getLanguage( 116 | $actionArguments['language'] ?? 'default' 117 | ); 118 | 119 | if ($styleguideLanguage) { 120 | // Replace language in request 121 | $request = $request->withAttribute('language', new SiteLanguage( 122 | 0, 123 | $styleguideLanguage['locale'], 124 | $request->getAttribute('site')->getBase(), 125 | [ 126 | 'title' => $styleguideLanguage['label'], 127 | 'typo3Language' => $styleguideLanguage['identifier'], 128 | 'hreflang' => $styleguideLanguage['hreflang'], 129 | 'direction' => $styleguideLanguage['direction'], 130 | 'twoLetterIsoCode' => $styleguideLanguage['twoLetterIsoCode'] 131 | ] 132 | )); 133 | } 134 | } 135 | 136 | $request = $request->withAttribute('frontend.controller', $GLOBALS['TSFE']); 137 | $GLOBALS['TYPO3_REQUEST'] = $request; 138 | 139 | $extbaseAttribute = new ExtbaseRequestParameters(); 140 | $extbaseAttribute->setControllerExtensionName('fluidStyleguide'); 141 | $extbaseAttribute->setControllerName('Styleguide'); 142 | $extbaseAttribute->setControllerActionName($actionName); 143 | $request = new Request($request 144 | ->withAttribute('extbase', $extbaseAttribute) 145 | ->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE) 146 | ->withAttribute('frontend.controller', $GLOBALS['TSFE'])); 147 | 148 | 149 | $plainFrontendTypoScript = new FrontendTypoScript(new RootNode(), [], [], []); 150 | if ((new Typo3Version())->getMajorVersion() >= 13) { 151 | $plainFrontendTypoScript->setConfigArray([]); 152 | $plainFrontendTypoScript->setSetupArray([]); 153 | } 154 | 155 | $request = $request->withAttribute('frontend.typoscript', $plainFrontendTypoScript); 156 | 157 | // Create view 158 | if (GeneralUtility::makeInstance(Typo3Version::class)->getMajorVersion() < 13) { 159 | $view = $this->container->get(StandaloneView::class); 160 | $view->setTemplateRootPaths($styleguideConfigurationManager->getTemplateRootPaths()); 161 | $view->setPartialRootPaths($styleguideConfigurationManager->getPartialRootPaths()); 162 | $view->setLayoutRootPaths($styleguideConfigurationManager->getLayoutRootPaths()); 163 | $view->setRequest($request); 164 | } else { 165 | $view = $this->viewFactory->create(new ViewFactoryData( 166 | templateRootPaths: $styleguideConfigurationManager->getTemplateRootPaths(), 167 | partialRootPaths: $styleguideConfigurationManager->getPartialRootPaths(), 168 | layoutRootPaths: $styleguideConfigurationManager->getLayoutRootPaths(), 169 | templatePathAndFilename: null, 170 | request: $request, 171 | )); 172 | } 173 | 174 | $controller->setRequest($request); 175 | 176 | // set the global, since some ViewHelper still fallback to $GLOBALS['TYPO3_REQUEST'] 177 | $GLOBALS['TYPO3_REQUEST'] = $request; 178 | $controller->initializeView($view); 179 | 180 | // Call controller action 181 | $response = $this->callControllerAction($controller, $actionMethod, $actionArguments); 182 | 183 | // Normalize response 184 | if (!$response instanceof ResponseInterface) { 185 | if (!isset($response)) { 186 | $response = $view->render(); 187 | } 188 | $response = new HtmlResponse((string)$response); 189 | } 190 | 191 | return $response; 192 | } 193 | 194 | protected function callControllerAction( 195 | object $controller, 196 | string $actionMethod, 197 | array $actionArguments 198 | ) { 199 | return $controller->$actionMethod($actionArguments); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /Resources/Private/Components/Organism/StyleguideToolbar/StyleguideToolbar.html: -------------------------------------------------------------------------------- 1 | {namespace fsc=Sitegeist\FluidStyleguide\Components} 2 | {namespace fsv=Sitegeist\FluidStyleguide\ViewHelpers} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 |
19 |
20 |
21 |
22 | 23 |
24 |
25 | 26 |
27 | 28 |
29 | DOCUMENTATION 30 |
31 |
32 | 33 |
34 | CODE QUALITY 35 |
36 |
37 |
38 | FLUID 39 |
40 |
41 | HTML 42 |
43 | 44 |
45 | ZIP 46 |
47 |
48 | 49 |
50 | EDIT 51 |
52 |
53 |
54 | 55 |
56 | 57 |
58 |

{componentData.name.displayName}

59 | 60 |
61 |

API definition

62 |
63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 89 | 90 | 91 | 92 | 93 |
NameTypeRequiredDefaultDescription
{argument.name}{argument.type}{f:if(condition: argument.required, then: '✔︎')} 78 | 79 | 80 | 81 | {f:if(condition: argument.defaultValue, then: 'true', else: 'false')} 82 | 83 | 84 | {argument.defaultValue} 85 | 86 | 87 | 88 | {argument.description}
94 |
95 |
96 |
97 | 98 | 99 |
100 | {componentData.documentation} 101 |
102 |
103 |
104 |
105 | 106 | 107 |
108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 |
SeverityIssue
{issue.severity}{issue.message}
122 |
123 | 124 | Move along, everything is fine! 125 | 126 |
127 |
128 |
129 | 130 |
131 |
132 |
133 | 134 |
135 | 136 | 142 | 143 |
144 | 145 | 146 | 151 | 152 | 153 | 154 |
155 | 156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 | -------------------------------------------------------------------------------- /Classes/Service/StyleguideConfigurationManager.php: -------------------------------------------------------------------------------- 1 | yamlFileLoader = $yamlFileLoader; 29 | $this->packageManager = $packageManager; 30 | $this->loadConfiguration(); 31 | } 32 | 33 | public function loadConfiguration(): void 34 | { 35 | $this->mergedConfiguration = $this->yamlFileLoader->load($this->defaultConfigurationFile)['FluidStyleguide']; 36 | 37 | // Merge default configuration with custom configuration 38 | $activeExtensions = $this->packageManager->getActivePackages(); 39 | foreach ($activeExtensions as $package) { 40 | // Skip default configuration 41 | if ($package->getPackageKey() === 'fluid_styleguide') { 42 | continue; 43 | } 44 | 45 | $packageConfiguration = $package->getPackagePath() . 'Configuration/Yaml/FluidStyleguide.yaml'; 46 | if (file_exists($packageConfiguration)) { 47 | ArrayUtility::mergeRecursiveWithOverrule( 48 | $this->mergedConfiguration, 49 | $this->yamlFileLoader->load($packageConfiguration)['FluidStyleguide'] ?? [] 50 | ); 51 | } 52 | } 53 | 54 | $this->mergedConfiguration = $this->eventDispatcher 55 | ->dispatch(new AfterConfigurationLoadedEvent($this->mergedConfiguration, self::getCurrentSite())) 56 | ->getConfiguration(); 57 | 58 | // Sanitize component assets 59 | $this->mergedConfiguration['ComponentAssets']['Global']['Css'] = $this->sanitizeComponentAssets( 60 | $this->mergedConfiguration['ComponentAssets']['Global']['Css'] ?? [] 61 | ); 62 | $this->mergedConfiguration['ComponentAssets']['Global']['Javascript'] = $this->sanitizeComponentAssets( 63 | $this->mergedConfiguration['ComponentAssets']['Global']['Javascript'] ?? [] 64 | ); 65 | foreach ($this->mergedConfiguration['ComponentAssets']['Packages'] as &$assets) { 66 | $assets['Css'] = $this->sanitizeComponentAssets($assets['Css'] ?? []); 67 | $assets['Javascript'] = $this->sanitizeComponentAssets($assets['Javascript'] ?? []); 68 | } 69 | 70 | $this->mergedConfiguration['ResponsiveBreakpoints'] = array_filter($this->mergedConfiguration['ResponsiveBreakpoints']); 71 | $this->mergedConfiguration['Languages'] = array_filter($this->mergedConfiguration['Languages']); 72 | } 73 | 74 | public function getFeatures(): array 75 | { 76 | return $this->mergedConfiguration['Features']; 77 | } 78 | 79 | public function isFeatureEnabled(string $feature): bool 80 | { 81 | return !empty($this->mergedConfiguration['Features'][$feature]); 82 | } 83 | 84 | public function getComponentContext(): string 85 | { 86 | return $this->mergedConfiguration['ComponentContext'] ?? '|'; 87 | } 88 | 89 | public function getGlobalCss(): array 90 | { 91 | return $this->mergedConfiguration['ComponentAssets']['Global']['Css'] ?? []; 92 | } 93 | 94 | public function getGlobalJavascript(): array 95 | { 96 | return $this->mergedConfiguration['ComponentAssets']['Global']['Javascript'] ?? []; 97 | } 98 | 99 | public function getCssForPackage(Package $package): array 100 | { 101 | return $this->mergedConfiguration['ComponentAssets']['Packages'][$package->getNamespace()]['Css'] ?? []; 102 | } 103 | 104 | public function getJavascriptForPackage(Package $package): array 105 | { 106 | return $this->mergedConfiguration['ComponentAssets']['Packages'][$package->getNamespace()]['Javascript'] ?? []; 107 | } 108 | 109 | public function getStyleguideCss(): Uri 110 | { 111 | return $this->generateAssetUrl('EXT:fluid_styleguide/Resources/Public/Build/Css/Styleguide.min.css'); 112 | } 113 | 114 | public function getStyleguideJavascript(): Uri 115 | { 116 | return $this->generateAssetUrl('EXT:fluid_styleguide/Resources/Public/Build/JavaScript/Styleguide.min2.js'); 117 | } 118 | 119 | public function getResponsiveBreakpoints(): array 120 | { 121 | return $this->mergedConfiguration['ResponsiveBreakpoints'] ?? []; 122 | } 123 | 124 | public function getLanguages(): array 125 | { 126 | return $this->mergedConfiguration['Languages'] ?? []; 127 | } 128 | 129 | public function getLanguage($languageKey): ?array 130 | { 131 | $languageMatch = array_filter( 132 | $this->getLanguages(), 133 | fn($language) => $language['identifier'] === $languageKey 134 | ); 135 | return reset($languageMatch) ?: null; 136 | } 137 | 138 | public function getTemplateRootPaths(): array 139 | { 140 | return $this->mergedConfiguration['Fluid']['TemplateRootPaths'] ?? []; 141 | } 142 | 143 | public function getPartialRootPaths(): array 144 | { 145 | return $this->mergedConfiguration['Fluid']['PartialRootPaths'] ?? []; 146 | } 147 | 148 | public function getLayoutRootPaths(): array 149 | { 150 | return $this->mergedConfiguration['Fluid']['LayoutRootPaths'] ?? []; 151 | } 152 | 153 | public function getBrandingHighlightColor(): string 154 | { 155 | return $this->mergedConfiguration['Branding']['HighlightColor'] ?? ''; 156 | } 157 | 158 | public function getBrandingFontFamily(): string 159 | { 160 | return $this->mergedConfiguration['Branding']['FontFamily'] ?? ''; 161 | } 162 | 163 | public function getBrandingIframeBackground(): string 164 | { 165 | return $this->mergedConfiguration['Branding']['IframeBackground'] ?? ''; 166 | } 167 | 168 | public function getBrandingCss(): string 169 | { 170 | $variables = array_filter([ 171 | '--styleguide-highlight-color' => $this->getBrandingHighlightColor(), 172 | '--styleguide-font-family' => $this->getBrandingFontFamily(), 173 | '--styleguide-iframe-background' => $this->getBrandingIframeBackground() 174 | ]); 175 | 176 | return ':root {' . array_reduce( 177 | array_keys($variables), 178 | fn($css, $variable) => $css . $variable . ':' . $variables[$variable] . ';', 179 | '' 180 | ) . '}'; 181 | } 182 | 183 | public function getBrandingTitle(): string 184 | { 185 | return $this->mergedConfiguration['Branding']['Title'] ?? $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename']; 186 | } 187 | 188 | public function getBrandingIntro(): string 189 | { 190 | if ($this->mergedConfiguration['Branding']['IntroFile'] ?? '') { 191 | $introFile = GeneralUtility::getFileAbsFileName( 192 | $this->mergedConfiguration['Branding']['IntroFile'] 193 | ); 194 | return ($introFile && file_exists($introFile)) ? (string) file_get_contents($introFile) : ''; 195 | } 196 | 197 | return ''; 198 | } 199 | 200 | protected function sanitizeComponentAssets($assets) 201 | { 202 | if (is_string($assets)) { 203 | $assets = [$assets]; 204 | } elseif (!is_array($assets)) { 205 | return []; 206 | } 207 | 208 | foreach ($assets as $key => &$asset) { 209 | // Support both strings and arrays with additional asset information 210 | if (is_array($asset) && $asset['file']) { 211 | $file =& $asset['file']; 212 | } else { 213 | $file =& $asset; 214 | } 215 | 216 | if (!static::isRemoteUri($file)) { 217 | try { 218 | $file = $this->generateAssetUrl($file); 219 | } catch (InvalidAssetException) { 220 | unset($assets[$key]); 221 | } 222 | } 223 | } 224 | return $assets; 225 | } 226 | 227 | /** 228 | * Generates an asset (js/css) url without throwing away any url prefixes 229 | */ 230 | protected function generateAssetUrl(string $path): Uri 231 | { 232 | $path = GeneralUtility::getFileAbsFileName($path); 233 | if (!$path) { 234 | throw new InvalidAssetException(sprintf('Asset not found: %s', $path), 1608723092); 235 | } 236 | 237 | $baseUrl = static::getCurrentSite()->getBase(); 238 | $modified = filemtime($path); 239 | return $baseUrl 240 | ->withPath( 241 | $baseUrl->getPath() . 242 | PathUtility::getAbsoluteWebPath($path, false) 243 | ) 244 | ->withQuery('?' . $modified) 245 | ->withPort((int) GeneralUtility::getIndpEnv('TYPO3_PORT') ?: null); 246 | } 247 | 248 | /** 249 | * Checks if the provided uri is a valid remote uri 250 | */ 251 | protected static function isRemoteUri(string $uri): bool 252 | { 253 | $scheme = parse_url($uri, PHP_URL_SCHEME); 254 | return ($scheme && in_array(strtolower($scheme), ['http', 'https'])); 255 | } 256 | 257 | /** 258 | * Returns the current Site object to create urls 259 | */ 260 | protected static function getCurrentSite(): SiteInterface 261 | { 262 | return $GLOBALS['TYPO3_CURRENT_SITE']; 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /Resources/Private/Partials/Ruler.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 35 | 50 | 51 | 52 | 53 | 100 54 | 200 55 | 300 56 | 400 57 | 500 58 | 600 59 | 700 60 | 800 61 | 900 62 | 1000 63 | 1100 64 | 1200 65 | 1300 66 | 1400 67 | 1500 68 | 1600 69 | 1700 70 | 1800 71 | 1900 72 | 2000 73 | 2100 74 | 2200 75 | 2300 76 | 2400 77 | 2500 78 | 79 | 80 | 100 81 | 200 82 | 300 83 | 400 84 | 500 85 | 600 86 | 700 87 | 800 88 | 900 89 | 1000 90 | 1100 91 | 1200 92 | 1300 93 | 1400 94 | 95 | 96 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /Build/Scripts/runTests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # 4 | # TYPO3 core test runner based on docker and docker-compose. 5 | # 6 | 7 | # Function to write a .env file in Build/testing-docker/local 8 | # This is read by docker-compose and vars defined here are 9 | # used in Build/testing-docker/local/docker-compose.yml 10 | setUpDockerComposeDotEnv() { 11 | # Delete possibly existing local .env file if exists 12 | [ -e .env ] && rm .env 13 | # Set up a new .env file for docker-compose 14 | echo "COMPOSE_PROJECT_NAME=local" >> .env 15 | # To prevent access rights of files created by the testing, the docker image later 16 | # runs with the same user that is currently executing the script. docker-compose can't 17 | # use $UID directly itself since it is a shell variable and not an env variable, so 18 | # we have to set it explicitly here. 19 | echo "HOST_UID=`id -u`" >> .env 20 | # Your local home directory for composer and npm caching 21 | echo "HOST_HOME=${HOME}" >> .env 22 | # Your local user 23 | echo "ROOT_DIR"=${ROOT_DIR} >> .env 24 | echo "HOST_USER=${USER}" >> .env 25 | echo "TEST_FILE=${TEST_FILE}" >> .env 26 | echo "PHP_XDEBUG_ON=${PHP_XDEBUG_ON}" >> .env 27 | echo "PHP_XDEBUG_PORT=${PHP_XDEBUG_PORT}" >> .env 28 | echo "PHP_VERSION=${PHP_VERSION}" >> .env 29 | echo "TYPO3_VERSION=${TYPO3_VERSION}" >> .env 30 | echo "DOCKER_PHP_IMAGE=${DOCKER_PHP_IMAGE}" >> .env 31 | echo "EXTRA_TEST_OPTIONS=${EXTRA_TEST_OPTIONS}" >> .env 32 | echo "SCRIPT_VERBOSE=${SCRIPT_VERBOSE}" >> .env 33 | } 34 | 35 | # Load help text into $HELP 36 | read -r -d '' HELP < 46 | Specifies which test suite to run 47 | - composerInstall: "composer install" 48 | - composerInstallMax: "composer update", with no platform.php config. 49 | - composerInstallMin: "composer update --prefer-lowest", with platform.php set to PHP version x.x.0. 50 | - composerValidate: "composer validate" 51 | - lintPhp: PHP linting 52 | - lintEditorconfig: Editorconfig linting 53 | - unit (default): PHP unit tests 54 | - functional: functional tests 55 | 56 | -d 57 | Only with -s functional 58 | Specifies on which DBMS tests are performed 59 | - mariadb (default): use mariadb 60 | - mssql: use mssql microsoft sql server 61 | - postgres: use postgres 62 | - sqlite: use sqlite 63 | 64 | -p <8.2|8.3> 65 | Specifies the PHP minor version to be used 66 | - 8.2 (default): use PHP 8.2 67 | - 8.3: use PHP 8.3 68 | 69 | -t <12|13> 70 | Specifies the TYPO3 version to be used 71 | - 12 (default): use TYPO3 12 72 | - 13: use TYPO3 13 73 | 74 | -e "" 75 | Only with -s functional|unit 76 | Additional options to send to phpunit tests. 77 | For phpunit, options starting with "--" must be added after options starting with "-". 78 | Example -e "-v --filter canRetrieveValueWithGP" to enable verbose output AND filter tests 79 | named "canRetrieveValueWithGP" 80 | 81 | -x 82 | Only with -s unit 83 | Send information to host instance for test or system under test break points. This is especially 84 | useful if a local PhpStorm instance is listening on default xdebug port 9003. A different port 85 | can be selected with -y 86 | 87 | -y 88 | Send xdebug information to a different port than default 9003 if an IDE like PhpStorm 89 | is not listening on default port. 90 | 91 | -u 92 | Update existing typo3gmbh/phpXY:latest docker images. Maintenance call to docker pull latest 93 | versions of the main php images. The images are updated once in a while and only the youngest 94 | ones are supported by core testing. Use this if weird test errors occur. Also removes obsolete 95 | image versions of typo3gmbh/phpXY. 96 | 97 | -v 98 | Enable verbose script output. Shows variables and docker commands. 99 | 100 | -h 101 | Show this help. 102 | 103 | Examples: 104 | # Run unit tests using PHP 8.2 TYPO3 12 105 | ./Build/Scripts/runTests.sh 106 | 107 | # Run unit tests using PHP 8.3 TYPO3 13 108 | ./Build/Scripts/runTests.sh -p 8.3 -t 13 109 | EOF 110 | 111 | # Test if docker compose exists, else exit out with error 112 | if ! type "docker" > /dev/null; then 113 | echo "This script relies on docker and docker compose. Please install" >&2 114 | exit 1 115 | fi 116 | 117 | # Go to the directory this script is located, so everything else is relative 118 | # to this dir, no matter from where this script is called. 119 | THIS_SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" 120 | cd "$THIS_SCRIPT_DIR" || exit 1 121 | 122 | # Go to directory that contains the local docker-compose.yml file 123 | cd ../testing-docker || exit 1 124 | 125 | # Option defaults 126 | if ! command -v realpath &> /dev/null; then 127 | echo "Consider installing realpath for properly resolving symlinks" >&2 128 | ROOT_DIR="${PWD}/../../" 129 | else 130 | ROOT_DIR=`realpath ${PWD}/../../` 131 | fi 132 | TEST_SUITE="unit" 133 | DBMS="mariadb" 134 | PHP_VERSION="8.2" 135 | TYPO3_VERSION="12" 136 | PHP_XDEBUG_ON=0 137 | PHP_XDEBUG_PORT=9003 138 | EXTRA_TEST_OPTIONS="" 139 | SCRIPT_VERBOSE=0 140 | 141 | # Option parsing 142 | # Reset in case getopts has been used previously in the shell 143 | OPTIND=1 144 | # Array for invalid options 145 | INVALID_OPTIONS=(); 146 | # Simple option parsing based on getopts (! not getopt) 147 | while getopts ":s:d:p:e:t:xy:huv" OPT; do 148 | case ${OPT} in 149 | s) 150 | TEST_SUITE=${OPTARG} 151 | ;; 152 | d) 153 | DBMS=${OPTARG} 154 | ;; 155 | p) 156 | PHP_VERSION=${OPTARG} 157 | ;; 158 | e) 159 | EXTRA_TEST_OPTIONS=${OPTARG} 160 | ;; 161 | x) 162 | PHP_XDEBUG_ON=1 163 | ;; 164 | y) 165 | PHP_XDEBUG_PORT=${OPTARG} 166 | ;; 167 | h) 168 | echo "${HELP}" 169 | exit 0 170 | ;; 171 | u) 172 | TEST_SUITE=update 173 | ;; 174 | v) 175 | SCRIPT_VERBOSE=1 176 | ;; 177 | t) 178 | TYPO3_VERSION=${OPTARG} 179 | ;; 180 | \?) 181 | INVALID_OPTIONS+=(${OPTARG}) 182 | ;; 183 | :) 184 | INVALID_OPTIONS+=(${OPTARG}) 185 | ;; 186 | esac 187 | done 188 | 189 | # Exit on invalid options 190 | if [ ${#INVALID_OPTIONS[@]} -ne 0 ]; then 191 | echo "Invalid option(s):" >&2 192 | for I in "${INVALID_OPTIONS[@]}"; do 193 | echo "-"${I} >&2 194 | done 195 | echo >&2 196 | echo "${HELP}" >&2 197 | exit 1 198 | fi 199 | 200 | # Move "7.4" to "php74", the latter is the docker container name 201 | DOCKER_PHP_IMAGE=`echo "php${PHP_VERSION}" | sed -e 's/\.//'` 202 | 203 | # Set $1 to first mass argument, this is the optional test file or test directory to execute 204 | shift $((OPTIND - 1)) 205 | if [ -n "${1}" ]; then 206 | TEST_FILE="Web/typo3conf/ext/fluid_styleguide/${1}" 207 | fi 208 | 209 | if [ ${SCRIPT_VERBOSE} -eq 1 ]; then 210 | set -x 211 | fi 212 | 213 | # Suite execution 214 | case ${TEST_SUITE} in 215 | composerInstall) 216 | setUpDockerComposeDotEnv 217 | docker compose run composer_install 218 | SUITE_EXIT_CODE=$? 219 | docker compose down 220 | ;; 221 | composerInstallMax) 222 | setUpDockerComposeDotEnv 223 | docker compose run composer_install_max 224 | SUITE_EXIT_CODE=$? 225 | docker compose down 226 | ;; 227 | composerInstallMin) 228 | setUpDockerComposeDotEnv 229 | docker compose run composer_install_min 230 | SUITE_EXIT_CODE=$? 231 | docker compose down 232 | ;; 233 | composerValidate) 234 | setUpDockerComposeDotEnv 235 | docker compose run composer_validate 236 | SUITE_EXIT_CODE=$? 237 | docker compose down 238 | ;; 239 | functional) 240 | setUpDockerComposeDotEnv 241 | case ${DBMS} in 242 | mariadb) 243 | docker compose run functional_mariadb10 244 | SUITE_EXIT_CODE=$? 245 | ;; 246 | mssql) 247 | docker compose run functional_mssql2019latest 248 | SUITE_EXIT_CODE=$? 249 | ;; 250 | postgres) 251 | docker compose run functional_postgres10 252 | SUITE_EXIT_CODE=$? 253 | ;; 254 | sqlite) 255 | # sqlite has a tmpfs as .Build/Web/typo3temp/var/tests/functional-sqlite-dbs/ 256 | # Since docker is executed as root (yay!), the path to this dir is owned by 257 | # root if docker creates it. Thank you, docker. We create the path beforehand 258 | # to avoid permission issues. 259 | mkdir -p ${ROOT_DIR}/.Build/Web/typo3temp/var/tests/functional-sqlite-dbs/ 260 | docker compose run functional_sqlite 261 | SUITE_EXIT_CODE=$? 262 | ;; 263 | *) 264 | echo "Invalid -d option argument ${DBMS}" >&2 265 | echo >&2 266 | echo "${HELP}" >&2 267 | exit 1 268 | esac 269 | docker compose down 270 | ;; 271 | lintPhp) 272 | setUpDockerComposeDotEnv 273 | docker compose run lint_php 274 | SUITE_EXIT_CODE=$? 275 | docker compose down 276 | ;; 277 | lintEditorconfig) 278 | setUpDockerComposeDotEnv 279 | docker compose run lint_editorconfig 280 | SUITE_EXIT_CODE=$? 281 | docker compose down 282 | ;; 283 | unit) 284 | setUpDockerComposeDotEnv 285 | docker compose run unit 286 | SUITE_EXIT_CODE=$? 287 | docker compose down 288 | ;; 289 | update) 290 | # pull typo3/core-testing-*:latest versions of those ones that exist locally 291 | docker images typo3/core-testing-*:latest --format "{{.Repository}}:latest" | xargs -I {} docker pull {} 292 | # remove "dangling" typo3/core-testing-* images (those tagged as ) 293 | docker images typo3/core-testing-* --filter "dangling=true" --format "{{.ID}}" | xargs -I {} docker rmi {} 294 | ;; 295 | *) 296 | echo "Invalid -s option argument ${TEST_SUITE}" >&2 297 | echo >&2 298 | echo "${HELP}" >&2 299 | exit 1 300 | esac 301 | 302 | exit $SUITE_EXIT_CODE 303 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/Component/ExampleViewHelper.php: -------------------------------------------------------------------------------- 1 | registerArgument('component', Component::class, 'Component that should be rendered', true); 23 | $this->registerArgument('fixtureName', 'string', 'Name of the fixture that should be used in the example'); 24 | $this->registerArgument('fixtureData', 'array', 'Additional dynamic fixture data that should be used in the example'); 25 | $this->registerArgument('context', 'string', 'The context (html markup) in which the component should be displayed in the styleguide', false, '|'); 26 | $this->registerArgument( 27 | 'applyContextFromFixture', 28 | 'bool', 29 | 'Component context from fixture data (styleguideComponentContext property) will overrule context specified in ViewHelper call', 30 | false, 31 | false 32 | ); 33 | $this->registerArgument('execute', 'bool', 'Set to true if the component example should be executed', false, false); 34 | $this->registerArgument('handleExceptions', 'bool', 'Handle exceptions that occur during execution of the example', false, false); 35 | } 36 | 37 | /** 38 | * Renders fluid example code for the specified component 39 | */ 40 | public function render(): string 41 | { 42 | if (!isset($this->arguments['fixtureName']) && !isset($this->arguments['fixtureData'])) { 43 | throw new \InvalidArgumentException(sprintf( 44 | 'A fixture name or fixture data has to be specified to render the component example of %s.', 45 | $this->arguments['component']->getName()->getIdentifier() 46 | ), 1566377563); 47 | } 48 | $fixtureData = $this->arguments['fixtureData'] ?? []; 49 | $componentContext = $this->arguments['context']; 50 | if (isset($this->arguments['fixtureName'])) { 51 | $componentFixture = $this->arguments['component']->getFixture($this->arguments['fixtureName']); 52 | if (!$componentFixture) { 53 | throw new \InvalidArgumentException(sprintf( 54 | 'Invalid fixture name "%s" specified for component %s.', 55 | $this->arguments['fixtureName'], 56 | $this->arguments['component']->getName()->getIdentifier() 57 | ), 1566377564); 58 | } 59 | 60 | // Merge static fixture data with manually edited data 61 | $fixtureData = array_replace($componentFixture->getData(), $fixtureData); 62 | 63 | // Overrule component context if specified in fixture data 64 | if ($this->arguments['applyContextFromFixture'] && isset($fixtureData['styleguideComponentContext'])) { 65 | $componentContext = $fixtureData['styleguideComponentContext']; 66 | } 67 | unset($fixtureData['styleguideComponentContext']); 68 | } 69 | if ($this->arguments['execute']) { 70 | try { 71 | // Parse fluid code in fixtures 72 | $fixtureData = self::renderFluidInExampleData($fixtureData, $this->renderingContext); 73 | 74 | $componentMarkup = self::renderComponent( 75 | $this->arguments['component'], 76 | $fixtureData, 77 | $this->renderingContext 78 | ); 79 | 80 | $componentWithContext = self::applyComponentContext( 81 | $componentMarkup, 82 | $componentContext, 83 | $this->renderingContext, 84 | array_replace( 85 | $this->arguments['component']->getDefaultValues(), 86 | $fixtureData 87 | ) 88 | ); 89 | } catch (\Exception $e) { 90 | if ($this->arguments['handleExceptions']) { 91 | return sprintf( 92 | 'Exception: %s (#%d %s)', 93 | $e->getMessage(), 94 | $e->getCode(), 95 | $e::class 96 | ); 97 | } else { 98 | throw $e; 99 | } 100 | } 101 | } else { 102 | $componentMarkup = static::renderComponentTag( 103 | $this->arguments['component']->getName(), 104 | $fixtureData 105 | ); 106 | 107 | $componentWithContext = self::applyComponentContext( 108 | $componentMarkup, 109 | $componentContext 110 | ); 111 | } 112 | return $componentWithContext; 113 | } 114 | 115 | /** 116 | * Calls a component with the supplied example data 117 | */ 118 | public static function renderComponent( 119 | Component $component, 120 | array $data, 121 | RenderingContextInterface $renderingContext 122 | ): string { 123 | // Check if all required arguments were supplied to the component 124 | foreach ($component->getArguments() as $expectedArgument) { 125 | if ($expectedArgument->isRequired() && !isset($data[$expectedArgument->getName()])) { 126 | throw new RequiredComponentArgumentException(sprintf( 127 | 'Required argument "%s" was not supplied for component %s.', 128 | $expectedArgument->getName(), 129 | $component->getName()->getIdentifier() 130 | ), 1566636254); 131 | } 132 | } 133 | 134 | return ComponentRenderer::renderComponent( 135 | $data, 136 | fn() => '', 137 | $renderingContext, 138 | $component->getName()->getIdentifier() 139 | ); 140 | } 141 | 142 | /** 143 | * Renders inline fluid code in a fixture array that will be provided as example data to a component 144 | */ 145 | public static function renderFluidInExampleData(array $data, RenderingContextInterface $renderingContext): array 146 | { 147 | return array_map(function ($value) use ($renderingContext) { 148 | if (is_string($value)) { 149 | return $renderingContext->getTemplateParser()->parse($value)->render($renderingContext); 150 | } else { 151 | return $value; 152 | } 153 | }, $data); 154 | } 155 | 156 | /** 157 | * Renders fluid code of a component call 158 | */ 159 | public static function renderComponentTag(ComponentName $componentName, array $data): string 160 | { 161 | $fluidComponent = new TagBuilder($componentName->getTagName()); 162 | $data = array_map([static::class, 'encodeFluidVariable'], $data); 163 | 164 | if (isset($data['content'])) { 165 | $fluidComponent->setContent($data['content']); 166 | unset($data['content']); 167 | } 168 | 169 | $fluidComponent->addAttributes($data, false); 170 | 171 | return $fluidComponent->render(); 172 | } 173 | 174 | /** 175 | * Wraps component markup in the specified component context (HTML markup) 176 | * The component markup will replace all pipe characters (|) in the context string 177 | * Optionally, a renderingContext and template data can be provided, in which case 178 | * the context markup will be treated as fluid markup 179 | */ 180 | public static function applyComponentContext( 181 | string $componentMarkup, 182 | string $context, 183 | RenderingContextInterface $renderingContext = null, 184 | array $data = [] 185 | ): string { 186 | // Check if the context should be fetched from a file 187 | $context = self::checkObtainComponentContextFromFile($context); 188 | 189 | if (isset($renderingContext)) { 190 | // Use unique value as component markup marker 191 | $marker = '###COMPONENT_MARKUP_' . mt_rand() . '###'; 192 | $context = str_replace('|', $marker, $context); 193 | 194 | // Parse fluid tags in context string 195 | $originalVariableContainer = $renderingContext->getVariableProvider(); 196 | $renderingContext->setVariableProvider(new StandardVariableProvider($data)); 197 | $context = $renderingContext->getTemplateParser()->parse($context)->render($renderingContext); 198 | $renderingContext->setVariableProvider($originalVariableContainer); 199 | 200 | // Wrap component markup 201 | return str_replace($marker, $componentMarkup, $context); 202 | } else { 203 | return str_replace('|', $componentMarkup, $context); 204 | } 205 | } 206 | 207 | /** 208 | * Checks if the provided component context is a file path and returns its contents; 209 | * falls back to the specified context string. 210 | */ 211 | protected static function checkObtainComponentContextFromFile(string $context): string 212 | { 213 | // Probably not a file path 214 | if (str_contains($context, '|')) { 215 | return $context; 216 | } 217 | 218 | // Check if the value is a valid file 219 | $path = GeneralUtility::getFileAbsFileName($context); 220 | if (!file_exists($path)) { 221 | return $context; 222 | } 223 | 224 | return file_get_contents($path); 225 | } 226 | 227 | /** 228 | * Encodes a fluid variable for use in component/viewhelper call 229 | */ 230 | public static function encodeFluidVariable(mixed $input, bool $isRoot = true): string 231 | { 232 | if (is_array($input)) { 233 | $fluidArray = []; 234 | foreach ($input as $key => $value) { 235 | $fluidArray[] = (string) $key . ': ' . static::encodeFluidVariable($value, false); 236 | } 237 | return '{' . implode(', ', $fluidArray) . '}'; 238 | } 239 | 240 | if (is_string($input) && !$isRoot) { 241 | return "'" . addcslashes($input, "'") . "'"; 242 | } 243 | 244 | if (is_bool($input)) { 245 | return ($input) ? 'TRUE' : 'FALSE'; 246 | } 247 | 248 | if ($input instanceof \UnitEnum) { 249 | return $input::class . '::' . $input->name; 250 | } 251 | 252 | return (string) $input; 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /Classes/Controller/StyleguideController.php: -------------------------------------------------------------------------------- 1 | componentRepository->findWithFixtures(); 41 | $componentPackages = $this->groupComponentsByPackage($allComponents); 42 | 43 | $this->view->assignMultiple([ 44 | 'navigation' => $allComponents, 45 | 'packages' => $componentPackages 46 | ]); 47 | 48 | return new HtmlResponse($this->view->render('Styleguide/List')); 49 | } 50 | 51 | public function showAction(array $arguments = []): ResponseInterface 52 | { 53 | $component = $arguments['component'] ?? ''; 54 | $fixture = $arguments['fixture'] ?? 'default'; 55 | 56 | // Sanitize user input 57 | $component = $this->sanitizeComponentIdentifier($component); 58 | $fixture = $this->sanitizeFixtureName($fixture); 59 | 60 | // Check if component exists 61 | $component = $this->componentRepository->findWithFixturesByIdentifier($component); 62 | if (!$component) { 63 | return new Response('Component not found', 404); 64 | } 65 | 66 | if ($this->styleguideConfigurationManager->isFeatureEnabled('CodeQuality') && class_exists(CodeQualityService::class)) { 67 | $showQualityIssues = true; 68 | 69 | // Initialize code quality service 70 | $configurationService = new ConfigurationService; 71 | $configuration = $configurationService->getFinalConfiguration(false, $component->getCodeQualityConfiguration() ?? false); 72 | $registeredChecks = $configurationService->getRegisteredChecks(); 73 | $codeQualityService = new CodeQualityService($configuration, $registeredChecks); 74 | 75 | // Get code quality issues for component 76 | $qualityIssues = $codeQualityService->validateComponent( 77 | $component->getLocation()->getFilePath() 78 | ); 79 | } else { 80 | $showQualityIssues = false; 81 | $qualityIssues = []; 82 | } 83 | 84 | $this->view->assignMultiple([ 85 | 'navigation' => $this->componentRepository->findWithFixtures(), 86 | 'activeComponent' => $component, 87 | 'activeFixture' => $fixture, 88 | 'showQualityIssues' => $showQualityIssues, 89 | 'qualityIssues' => $qualityIssues 90 | ]); 91 | 92 | return new HtmlResponse($this->view->render('Styleguide/Show')); 93 | } 94 | 95 | /** 96 | * Shows a rendered example of a component. This will be shown inside of the iframe 97 | * 98 | * @return void 99 | */ 100 | public function componentAction(array $arguments = []) 101 | { 102 | $component = $arguments['component'] ?? ''; 103 | $fixture = $arguments['fixture'] ?? 'default'; 104 | $formData = $arguments['formData'] ?? []; 105 | 106 | // Sanitize user input 107 | $component = $this->sanitizeComponentIdentifier($component); 108 | $fixture = $this->sanitizeFixtureName($fixture); 109 | if (!$this->styleguideConfigurationManager->isFeatureEnabled('Editor')) { 110 | $formData = []; 111 | } else { 112 | $formData = $this->sanitizeFormData($formData); 113 | } 114 | 115 | // Check if component exists 116 | $component = $this->componentRepository->findWithFixturesByIdentifier($component); 117 | if (!$component) { 118 | return new Response('Component not found', 404); 119 | } 120 | 121 | $package = $component->getName()->getPackage(); 122 | 123 | $this->view->assignMultiple([ 124 | 'component' => $component, 125 | 'componentCss' => $this->styleguideConfigurationManager->getCssForPackage($package), 126 | 'componentJavascript' => $this->styleguideConfigurationManager->getJavascriptForPackage($package), 127 | 'fixtureName' => $fixture, 128 | 'fixtureData' => $formData 129 | ]); 130 | 131 | $eventDispatcher = $this->container->get(EventDispatcher::class); 132 | 133 | $eventDispatcher->dispatch(new PreProcessComponentViewEvent($component, $fixture, $formData, $this->view)); 134 | 135 | $renderedView = $this->view->render('Styleguide/Component'); 136 | 137 | $event = new PostProcessComponentViewEvent($component, $fixture, $formData, $renderedView); 138 | $event = $eventDispatcher->dispatch($event); 139 | 140 | $renderedView = $event->getRenderedView(); 141 | $renderedView = str_replace('', implode('', $event->getHeaderData()), $renderedView); 142 | $renderedView = str_replace('', implode('', $event->getFooterData()), $renderedView); 143 | 144 | return $renderedView; 145 | } 146 | 147 | /** 148 | * Provides a zip download of a component folder 149 | */ 150 | public function downloadComponentZipAction(array $arguments = []) 151 | { 152 | $component = $arguments['component'] ?? ''; 153 | 154 | // Sanitize user input 155 | if (!$this->styleguideConfigurationManager->isFeatureEnabled('ZipDownload')) { 156 | return new Response('Zip download is not available', 403); 157 | } 158 | $component = $this->sanitizeComponentIdentifier($component); 159 | 160 | // Check if component exists 161 | $component = $this->componentRepository->findWithFixturesByIdentifier($component); 162 | if (!$component) { 163 | return new Response('Component not found', 404); 164 | } 165 | 166 | return $this->componentDownloadService->downloadZip($component); 167 | } 168 | 169 | protected function groupComponentsByPackage(array $components): array 170 | { 171 | $componentPackages = []; 172 | foreach ($components as $component) { 173 | $packageNamespace = $component->getName()->getPackage()->getNamespace(); 174 | if (!isset($componentPackages[$packageNamespace])) { 175 | $componentPackages[$packageNamespace] = []; 176 | } 177 | 178 | $componentPackages[$packageNamespace][] = $component; 179 | } 180 | return $componentPackages; 181 | } 182 | 183 | public function initializeView(StandaloneView|FluidViewAdapter $view): void 184 | { 185 | $this->view = $view; 186 | 187 | $this->view->assignMultiple([ 188 | 'styleguideConfiguration' => $this->styleguideConfigurationManager, 189 | 'styleguideLanguage' => $this->request->getAttribute('language'), 190 | 'sitename' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?? '', 191 | 'baseUri' => $this->request->getAttribute('site')->getBase() 192 | ]); 193 | 194 | $this->registerDemoComponents(); 195 | } 196 | 197 | public function setRequest(ServerRequestInterface $request): void 198 | { 199 | $this->request = $request; 200 | } 201 | 202 | /** 203 | * Makes sure that no malicious user input will be passed to a component 204 | */ 205 | protected function sanitizeFormData(array $formData): array 206 | { 207 | foreach ($formData as $key => &$value) { 208 | // Throw away any input other than string 209 | if (!is_string($value)) { 210 | unset($formData[$key]); 211 | continue; 212 | } 213 | 214 | // Convert to integer 215 | if (MathUtility::canBeInterpretedAsInteger($value)) { 216 | $value = (int)$value; 217 | // Convert to float 218 | } elseif (MathUtility::canBeInterpretedAsFloat($value)) { 219 | $value = (float)$value; 220 | // Convert to boolean 221 | } elseif (mb_strtoupper($value) === 'TRUE' || mb_strtoupper($value) === 'FALSE') { 222 | $value = (mb_strtoupper($value) === 'TRUE'); 223 | // Escape string if necessary 224 | } elseif ($this->styleguideConfigurationManager->isFeatureEnabled('EscapeInputFromEditor')) { 225 | $value = htmlspecialchars($value); 226 | } 227 | } 228 | 229 | return $formData; 230 | } 231 | 232 | /** 233 | * Make sure that the component identifier doesn't include any malicious characters 234 | */ 235 | protected function sanitizeComponentIdentifier(string $componentIdentifier): string 236 | { 237 | return trim((string) preg_replace('#[^a-z0-9_\\\\]#i', '', $componentIdentifier), '\\'); 238 | } 239 | 240 | /** 241 | * Make sure that the fixture name doesn't include any malicious characters 242 | */ 243 | protected function sanitizeFixtureName(string $fixtureName): string 244 | { 245 | return preg_replace('#[^a-z0-9_]#i', '', $fixtureName); 246 | } 247 | 248 | protected function registerDemoComponents(): void 249 | { 250 | $componentLoader = $this->container->get(ComponentLoader::class); 251 | if (count($componentLoader->getNamespaces()) === 1 || 252 | $this->styleguideConfigurationManager->isFeatureEnabled('DemoComponents') 253 | ) { 254 | $demoNamespace = 'Sitegeist\\FluidStyleguide\\DemoComponents'; 255 | $componentLoader->addNamespace( 256 | $demoNamespace, 257 | ExtensionManagementUtility::extPath( 258 | 'fluid_styleguide', 259 | 'Resources/Private/DemoComponents' 260 | ) 261 | ); 262 | $this->view->getRenderingContext()->getViewHelperResolver()->addNamespace( 263 | 'demo', 264 | $demoNamespace 265 | ); 266 | $GLOBALS['TYPO3_CONF_VARS']['SYS']['fluid']['namespaces']['demo'] = [$demoNamespace]; 267 | } 268 | } 269 | } 270 | --------------------------------------------------------------------------------