├── .ddev ├── commands │ └── host │ │ └── vite-visualizer.sh └── config.yaml ├── .editorconfig ├── .env.example.dev ├── .env.example.production ├── .env.example.staging ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── ARCHITECTURE.md ├── README.md ├── bootstrap.php ├── composer-scripts ├── ScriptHelpers.php └── post-create-project.php ├── composer.json ├── composer.lock ├── config ├── app.php ├── general.php ├── htmlpurifier │ └── Default.json ├── imager-x-aws-serverless-transformer.php ├── imager-x-transforms.php ├── imager-x.php ├── project │ ├── ckeditor │ │ └── configs │ │ │ ├── b7e66782-af96-4012-9e17-914134073ced.yaml │ │ │ └── d39724b0-dd22-4c74-aa0b-87a022555cf2.yaml │ ├── entryTypes │ │ ├── callToAction--534ccdd8-7abd-4300-a102-71885b21c126.yaml │ │ ├── cardGrid--7ced3e9d-9b54-47f6-8103-e5c3a4fbaf9c.yaml │ │ ├── codeBlock--cbf3aeac-588c-4aeb-9573-6f1920206abf.yaml │ │ ├── globalCode--25362218-e66c-494e-9f2c-63e8cbc6ce0a.yaml │ │ ├── heading--e445a3b9-5827-422a-8328-6b10449fa891.yaml │ │ ├── home--0e7762a5-2553-4c0d-b8af-9b43b59524bf.yaml │ │ ├── iconCard--3f1dc0c5-f091-469a-858a-4cbd3596bab7.yaml │ │ ├── imageCard--7d418368-91eb-4c64-8f5b-94c62cd62359.yaml │ │ ├── pageHero--27f19bae-64f8-47b8-9fc9-7552e11e2656.yaml │ │ ├── richText--896b8f2a-ed0c-42c9-b5fd-ba8668fc6784.yaml │ │ └── textWithMedia--3bcf7ddb-71e7-4115-868c-815b3611b34c.yaml │ ├── fields │ │ ├── basicLink--69fe4c18-21c9-4a3d-8fce-9988d3f741e4.yaml │ │ ├── button--2ea51656-9b0c-4736-a6fe-c95dc805dc60.yaml │ │ ├── cardGrid--5e84b0b7-6851-47ed-962e-76498ac9476f.yaml │ │ ├── code--7a701601-ee6c-4d56-8bd0-efa7719f6a67.yaml │ │ ├── codeBlocks--9c7dc3c3-cb8c-4167-880d-ab9426eef873.yaml │ │ ├── codePosition--f7d30e72-f872-487a-aba1-c8e95268a220.yaml │ │ ├── description--a99edea2-f8ae-44e5-a12f-89cb9aa66738.yaml │ │ ├── heading--4382f55f-01a1-4b67-84c8-ebbc89b238d2.yaml │ │ ├── icon--0011a64a-91b6-4aa3-98e6-be048394a2cf.yaml │ │ ├── image--570fb481-ad97-46db-8bad-f7b0fd90e831.yaml │ │ ├── mediaType--ac3b0590-d417-429e-aa1f-96be3c79a5b0.yaml │ │ ├── pageBlocks--7bd21dd3-6b12-4163-8432-030ea6a3f8d3.yaml │ │ ├── richText--cd489d3c-f914-475c-a270-ab926e4e7485.yaml │ │ ├── richTextSimple--c52a9170-652b-4d92-a226-69894b1822ed.yaml │ │ ├── side--d7f9c488-b14d-48c2-b6fc-d616b7a9f49f.yaml │ │ └── videoUrl--55cc26b6-8b49-47de-bbe7-5908da6f4545.yaml │ ├── navigation │ │ └── navs │ │ │ ├── footerNavigation--101b4db6-8914-45eb-a661-180b15f70894.yaml │ │ │ └── primaryNavigation--b3d7753a-c7ad-4f33-aca8-df63277e9b42.yaml │ ├── project.yaml │ ├── sections │ │ ├── globalCode--b4bfeb63-5bc4-4368-b4c9-408f7bac68fc.yaml │ │ └── home--969acdd6-6362-41b1-b782-6bbd43a8a6b4.yaml │ ├── siteGroups │ │ └── 805d8826-faed-4186-9b88-f509eb9b07e6.yaml │ ├── sites │ │ └── default--35b563a0-4662-40b9-b885-a8450a2868d9.yaml │ └── volumes │ │ └── assets--853413e4-e02c-487e-81a5-04e58eb18683.yaml ├── redirects.php ├── routes.php ├── tailwind │ ├── buttons.js │ ├── dialog.js │ ├── forms.js │ └── rich-text.js └── vite.php ├── craft ├── docs ├── block-call-to-action-admin.png ├── block-call-to-action-frontend.png ├── block-card-grid-admin.png ├── block-card-grid-frontend.png ├── block-heading-admin.png ├── block-heading-frontend.png ├── block-page-hero-admin.png ├── block-page-hero-frontend.png ├── block-rich-text-admin.png ├── block-rich-text-frontend.png ├── block-text-with-media-image-admin.png ├── block-text-with-media-image-frontend.png ├── block-text-with-media-video-admin.png ├── block-text-with-media-video-frontend.png ├── building-with-site-starter.md ├── project-logo.png ├── release-qa-1.png ├── release-qa-2.png └── site-starter-ecosystem.excalidraw.png ├── ecs.php ├── eslint.config.js ├── lint-staged.config.js ├── modules └── todo.php ├── package-lock.json ├── package.json ├── phpstan.neon ├── postcss.config.js ├── src ├── css │ └── app.css ├── fonts │ ├── .gitkeep │ ├── sourcesanspro-bold-webfont.woff2 │ ├── sourcesanspro-light-webfont.woff2 │ ├── sourcesanspro-regular-webfont.woff2 │ └── sourcesanspro-semibold-webfont.woff2 ├── icons │ ├── arrow-left.svg │ ├── arrow-right.svg │ ├── chevron-down.svg │ ├── clock.svg │ ├── close.svg │ ├── error.svg │ ├── logo-mark.svg │ └── logo-primary.svg └── js │ ├── alpine │ └── nav.js │ └── app.js ├── storage ├── .gitignore └── config-deltas │ └── .gitignore ├── templates ├── .gitkeep ├── _blocks │ ├── _callToAction.twig │ ├── _cardGrid.twig │ ├── _heading.twig │ ├── _pageHero.twig │ ├── _richText.twig │ ├── _textWithMedia.twig │ └── index.twig ├── _components │ ├── accordion.twig │ ├── alert-banner.twig │ ├── button.twig │ ├── call-to-action.twig │ ├── card-icon.twig │ ├── card-image.twig │ ├── dialog.twig │ ├── field-group.twig │ ├── field-select.twig │ ├── field-text-input.twig │ ├── field-textarea.twig │ ├── icon.twig │ ├── image-caption.twig │ ├── image.twig │ ├── page-hero.twig │ ├── pagination.twig │ ├── tabs.twig │ ├── tag.twig │ └── video.twig ├── _elements │ └── home.twig ├── _layouts │ └── base.twig ├── _partials │ ├── footer.twig │ ├── global-code-blocks.twig │ ├── header.twig │ ├── nav-desktop.twig │ └── nav-mobile.twig └── parts-kit │ ├── accordion │ └── default.twig │ ├── alert-banner │ └── default.twig │ ├── button │ └── default.twig │ ├── call-to-action │ └── default.twig │ ├── card │ ├── card-with-icon.twig │ └── card-with-image.twig │ ├── dialog │ ├── default.twig │ └── digalog-triggers-dialog.twig │ ├── forms │ ├── select.twig │ ├── text-input.twig │ └── textarea.twig │ ├── icons │ └── default.twig │ ├── image-caption │ └── default.twig │ ├── image │ └── default.twig │ ├── page-hero │ └── default.twig │ ├── paginaton │ └── default.twig │ ├── tabs │ └── default.twig │ ├── tag │ └── default.twig │ └── video │ └── default.twig ├── vite.config.js └── web ├── .htaccess ├── cpresources └── .gitignore ├── index.php └── web.config /.ddev/commands/host/vite-visualizer.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## Description: Analyze and visualize the Vite bundle with the current project 4 | ## OSTypes: darwin 5 | ## Usage: vite-visualizer 6 | ## Example: "ddev vite-visualizer" 7 | ## See: https://www.npmjs.com/package/vite-bundle-visualizer 8 | 9 | ddev exec npx vite-bundle-visualizer -o vite-bundle-visualizer.html 10 | 11 | open "${DDEV_APPROOT}/vite-bundle-visualizer.html" -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://EditorConfig.org 2 | 3 | root = true 4 | 5 | # Unix-style newlines with a newline ending every file 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | 14 | [*.csv] 15 | insert_final_newline = false 16 | 17 | [*.{php,py}] 18 | indent_size = 4 19 | 20 | # https://www.markdownguide.org/basic-syntax/#line-breaks 21 | [*.md] 22 | trim_trailing_whitespace = false 23 | -------------------------------------------------------------------------------- /.env.example.dev: -------------------------------------------------------------------------------- 1 | # Read about configuration, here: 2 | # https://craftcms.com/docs/5.x/configure.html 3 | 4 | # The application ID used to to uniquely store session and cache data, mutex locks, and more 5 | CRAFT_APP_ID= 6 | 7 | # The environment Craft is currently running in (dev, staging, production, etc.) 8 | CRAFT_ENVIRONMENT=dev 9 | 10 | # Email 11 | SYSTEM_EMAIL_ADDRESS=craft@viget.com 12 | SYSTEM_EMAIL_SENDER="Craft CMS" 13 | SYSTEM_EMAIL_HOSTNAME= 14 | SYSTEM_EMAIL_PORT= 15 | SYSTEM_EMAIL_USERNAME= 16 | SYSTEM_EMAIL_PASSWORD= 17 | 18 | # Database connection settings 19 | CRAFT_DB_DRIVER=mysql 20 | CRAFT_DB_SERVER=127.0.0.1 21 | CRAFT_DB_PORT=3306 22 | CRAFT_DB_DATABASE= 23 | CRAFT_DB_USER=root 24 | CRAFT_DB_PASSWORD= 25 | CRAFT_DB_SCHEMA=public 26 | CRAFT_DB_TABLE_PREFIX= 27 | 28 | # General settings 29 | CRAFT_SECURITY_KEY= 30 | CRAFT_DEV_MODE=true 31 | CRAFT_ALLOW_ADMIN_CHANGES=true 32 | CRAFT_DISALLOW_ROBOTS=true 33 | 34 | # Amazon S3 35 | S3_BASE_URL= 36 | S3_SUBFOLDER= 37 | S3_BUCKET_REGION= 38 | S3_BUCKET= 39 | S3_ACCESS_KEY_ID= 40 | S3_SECRET_ACCESS_KEY= 41 | S3_CLOUDFRONT_DISTRIBUTION_ID= 42 | 43 | # AWS Serverless 44 | IMAGERX_SERVERLESS_DISTRIBUTION_URL= 45 | IMAGERX_SERVERLESS_DISTRIBUTION_SIGNATURE= 46 | -------------------------------------------------------------------------------- /.env.example.production: -------------------------------------------------------------------------------- 1 | # Read about configuration, here: 2 | # https://craftcms.com/docs/5.x/configure.html 3 | 4 | # The application ID used to to uniquely store session and cache data, mutex locks, and more 5 | CRAFT_APP_ID= 6 | 7 | # The environment Craft is currently running in (dev, staging, production, etc.) 8 | CRAFT_ENVIRONMENT=production 9 | 10 | # Email 11 | SYSTEM_EMAIL_FROM=craft@viget.com 12 | SYSTEM_EMAIL_HOSTNAME= 13 | SYSTEM_EMAIL_PORT= 14 | SYSTEM_EMAIL_USERNAME= 15 | SYSTEM_EMAIL_PASSWORD= 16 | 17 | # Database connection settings 18 | CRAFT_DB_DRIVER=mysql 19 | CRAFT_DB_SERVER=127.0.0.1 20 | CRAFT_DB_PORT=3306 21 | CRAFT_DB_DATABASE= 22 | CRAFT_DB_USER=root 23 | CRAFT_DB_PASSWORD= 24 | CRAFT_DB_SCHEMA=public 25 | CRAFT_DB_TABLE_PREFIX= 26 | 27 | # General settings 28 | CRAFT_SECURITY_KEY= 29 | CRAFT_DEV_MODE=false 30 | CRAFT_ALLOW_ADMIN_CHANGES=false 31 | CRAFT_DISALLOW_ROBOTS=false 32 | -------------------------------------------------------------------------------- /.env.example.staging: -------------------------------------------------------------------------------- 1 | # Read about configuration, here: 2 | # https://craftcms.com/docs/5.x/configure.html 3 | 4 | # The application ID used to to uniquely store session and cache data, mutex locks, and more 5 | CRAFT_APP_ID= 6 | 7 | # The environment Craft is currently running in (dev, staging, production, etc.) 8 | CRAFT_ENVIRONMENT=staging 9 | 10 | # Email 11 | SYSTEM_EMAIL_FROM=craft@viget.com 12 | SYSTEM_EMAIL_HOSTNAME= 13 | SYSTEM_EMAIL_PORT= 14 | SYSTEM_EMAIL_USERNAME= 15 | SYSTEM_EMAIL_PASSWORD= 16 | 17 | # Database connection settings 18 | CRAFT_DB_DRIVER=mysql 19 | CRAFT_DB_SERVER=127.0.0.1 20 | CRAFT_DB_PORT=3306 21 | CRAFT_DB_DATABASE= 22 | CRAFT_DB_USER=root 23 | CRAFT_DB_PASSWORD= 24 | CRAFT_DB_SCHEMA=public 25 | CRAFT_DB_TABLE_PREFIX= 26 | 27 | # General settings 28 | CRAFT_SECURITY_KEY= 29 | CRAFT_DEV_MODE=false 30 | CRAFT_ALLOW_ADMIN_CHANGES=false 31 | CRAFT_DISALLOW_ROBOTS=true 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.env 2 | /.idea 3 | /vendor 4 | .DS_Store 5 | node_modules 6 | /web/dist 7 | /web/assets 8 | /web/imager 9 | /.vite 10 | php-cs-fixer.cache 11 | # Ignore generated visualization files 12 | vite-bundle-visualizer.html 13 | 14 | # BEGIN-STARTER-ONLY 15 | config/license.key 16 | # END-STARTER-ONLY 17 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | ddev exec npx lint-staged 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | composer.lock 4 | composer.json 5 | vendor 6 | config/project 7 | storage 8 | web 9 | package-lock.json 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "trailingComma": "all" 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "EditorConfig.EditorConfig", 4 | "bradlc.vscode-tailwindcss", 5 | "DEVSENSE.phptools-vscode", 6 | "biati.ddev-manager", 7 | "xdebug.php-debug", 8 | "moetelo.twiggy" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Listen for Xdebug", 5 | "type": "php", 6 | "request": "launch", 7 | "hostname": "0.0.0.0", 8 | "port": 9003, 9 | "pathMappings": { 10 | "/var/www/html": "${workspaceFolder}" 11 | }, 12 | "preLaunchTask": "DDEV: Enable Xdebug", 13 | "postDebugTask": "DDEV: Disable Xdebug" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "intelephense.environment.phpVersion": "8.2", 3 | "php.version": "8.2", 4 | "twiggy.framework": "craft" 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "DDEV: Enable Xdebug", 6 | "type": "shell", 7 | "command": "ddev xdebug on", 8 | "presentation": { 9 | "reveal": "silent", 10 | "close": true 11 | } 12 | }, 13 | { 14 | "label": "DDEV: Disable Xdebug", 15 | "type": "shell", 16 | "command": "ddev xdebug off", 17 | "presentation": { 18 | "reveal": "silent", 19 | "close": true 20 | } 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /ARCHITECTURE.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | 3 | ## Goals 4 | 5 | - As simple as possible 6 | - Easy to contribute 7 | - Use PHP/Composer as much as possible for setup scripts 8 | - Easy to test changes 9 | - Make it easy to `ddev start` this repo and test your plugin and build tool changes. 10 | - It should feel like you're working on a typical Craft project 11 | - Cross-platform (Mac, Windows, Linux) 12 | - We can achieve this by using PHP/Composer and running scripts from within Docker containers. 13 | 14 | ## Overview 15 | 16 | This starter is a Composer "project" that can be installed using `composer create-project`. 17 | 18 | composer.json has a `post-create-project-cmd` [script](https://getcomposer.org/doc/articles/scripts.md) that will run after the project is created. 19 | 20 | We use this hook to start the installation process (modify and delete files, edit config, etc). 21 | 22 | Some aspects of the installation are single line bash scripts. However, more complex actions are handled by PHP files in the [install-scripts](/install-scripts) directory 23 | -------------------------------------------------------------------------------- /bootstrap.php: -------------------------------------------------------------------------------- 1 | safeLoad(); 18 | } 19 | -------------------------------------------------------------------------------- /composer-scripts/ScriptHelpers.php: -------------------------------------------------------------------------------- 1 | true, 16 | ]); 17 | 18 | Console::output("Great! We'll use the name: $projectName"); 19 | 20 | $suggestedProjectSlug = StringHelper::toKebabCase($projectName); 21 | 22 | $projectSlugPrompt = Console::prompt("Customize the project slug? This controls the DDEV URL, etc.", [ 23 | 'default' => $suggestedProjectSlug, 24 | ]); 25 | 26 | $projectSlug = !empty(trim($projectSlugPrompt)) ? StringHelper::toKebabCase($projectSlugPrompt) : $suggestedProjectSlug; 27 | 28 | Console::output("Great! We'll use $projectSlug"); 29 | 30 | /** 31 | * Update DDEV config 32 | */ 33 | 34 | ScriptHelpers::replaceFileText( 35 | filePath: "$cwd/.ddev/config.yaml", 36 | pattern: "/name:\s+viget-craft-starter/", 37 | replacement: "name: $projectSlug", 38 | ); 39 | 40 | /** 41 | * Update package.json 42 | */ 43 | 44 | ScriptHelpers::replaceFileText( 45 | filePath: "$cwd/package.json", 46 | pattern: "/\"name\": \"viget-craft-starter\"/", 47 | replacement: "\"name\": \"$projectSlug\"", 48 | ); 49 | 50 | ScriptHelpers::replaceFileText( 51 | filePath: "$cwd/package-lock.json", 52 | pattern: "/\"name\": \"viget-craft-starter\"/", 53 | replacement: "\"name\": \"$projectSlug\"", 54 | ); 55 | 56 | /** 57 | * Update project config 58 | */ 59 | 60 | // Replace "Viget Craft Starter" site name in every file in the project config directory. 61 | ScriptHelpers::replaceFileTextInDirectory( 62 | directoryPath: "$cwd/config/project/", 63 | pattern: "/Viget Craft Starter/", 64 | replacement: "$projectName", 65 | ); 66 | 67 | // Replace plugin license keys. 68 | // These are regenerated when viewing the Control Panel 69 | ScriptHelpers::replaceFileText( 70 | filePath: "$cwd/config/project/project.yaml", 71 | pattern: "/ licenseKey: REPLACE[\r\n|\r|\n]/", // Make sure to remove new line too 72 | replacement: "", 73 | ); 74 | 75 | /** 76 | * .gitignore 77 | */ 78 | 79 | ScriptHelpers::replaceFileText( 80 | filePath: "$cwd/.gitignore", 81 | pattern: "/# BEGIN-STARTER-ONLY\X*# END-STARTER-ONLY/m", 82 | replacement: '', 83 | ); 84 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "viget/craft-site-starter", 3 | "type": "project", 4 | "license": "0BSD", 5 | "minimum-stability": "dev", 6 | "prefer-stable": true, 7 | "require": { 8 | "craftcms/aws-s3": "2.2.2", 9 | "craftcms/ckeditor": "4.9.0", 10 | "craftcms/cms": "5.7.7", 11 | "mmikkel/cp-field-inspect": "2.0.4", 12 | "nystudio107/craft-emptycoalesce": "5.0.0", 13 | "nystudio107/craft-retour": "5.0.10", 14 | "nystudio107/craft-seomatic": "5.1.13", 15 | "nystudio107/craft-vite": "5.0.1", 16 | "spacecatninja/imager-x": "5.1.3", 17 | "spacecatninja/imager-x-aws-serverless-transformer": "3.0.2", 18 | "spacecatninja/imager-x-power-pack": "1.0.5", 19 | "verbb/expanded-singles": "3.0.2", 20 | "verbb/navigation": "3.0.8", 21 | "viget/craft-classnames": "3.0.0", 22 | "viget/craft-parts-kit": "dev-main", 23 | "vlucas/phpdotenv": "^5.4.0" 24 | }, 25 | "require-dev": { 26 | "craftcms/ecs": "dev-main", 27 | "craftcms/generator": "^2.0.0", 28 | "craftcms/phpstan": "dev-main", 29 | "nystudio107/craft-autocomplete": "^1.12", 30 | "yiisoft/yii2-shell": "^2.0.3" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "modules\\": "modules/" 35 | } 36 | }, 37 | "config": { 38 | "allow-plugins": { 39 | "craftcms/plugin-installer": true, 40 | "yiisoft/yii2-composer": true 41 | }, 42 | "sort-packages": true, 43 | "optimize-autoloader": true, 44 | "platform": { 45 | "php": "8.2" 46 | } 47 | }, 48 | "scripts": { 49 | "post-create-project-cmd": [ 50 | "@php -r \"file_exists('.env') || copy('.env.example.dev', '.env');\"", 51 | "@php composer-scripts/post-create-project.php", 52 | "echo 'Cleaning composer.json'", 53 | "@composer config --unset scripts.post-create-project-cmd", 54 | "@composer config --unset name", 55 | "@composer config --unset license", 56 | "@composer config --unset type", 57 | "@composer update --ignore-platform-reqs", 58 | "@composer dump-autoload -o", 59 | "rm -rf composer-scripts" 60 | ], 61 | "post-root-package-install": [ 62 | "@php -r \"file_exists('.env') || copy('.env.example.dev', '.env');\"" 63 | ], 64 | "ecs-check": "ecs check --ansi", 65 | "ecs-fix": "ecs check --ansi --fix", 66 | "phpstan": "phpstan --memory-limit=1G" 67 | }, 68 | "repositories": [ 69 | { 70 | "type": "composer", 71 | "url": "https://composer.craftcms.com", 72 | "canonical": false 73 | }, 74 | { 75 | "type": "github", 76 | "url": "https://github.com/vigetlabs/craft-parts-kit.git" 77 | } 78 | ] 79 | } 80 | -------------------------------------------------------------------------------- /config/app.php: -------------------------------------------------------------------------------- 1 | App::env('CRAFT_APP_ID') ?: 'CraftCMS', 27 | ]; 28 | -------------------------------------------------------------------------------- /config/general.php: -------------------------------------------------------------------------------- 1 | defaultWeekStartDay(1) 17 | // Prevent generated URLs from including "index.php" 18 | ->omitScriptNameInUrls() 19 | // Preload Single entries as Twig variables 20 | ->preloadSingles() 21 | // Prevent user enumeration attacks 22 | ->preventUserEnumeration() 23 | // Set the @webroot alias so the clear-caches command knows where to find CP resources 24 | ->aliases([ 25 | '@webroot' => dirname(__DIR__) . '/web', 26 | ]) 27 | ; 28 | -------------------------------------------------------------------------------- /config/htmlpurifier/Default.json: -------------------------------------------------------------------------------- 1 | { 2 | "Attr.AllowedFrameTargets": ["_blank"], 3 | "Attr.EnableID": true, 4 | "HTML.AllowedComments": ["pagebreak"], 5 | "HTML.SafeIframe": true, 6 | "URI.SafeIframeRegexp": "%^(https?:)?//(www.youtube.com/|player.vimeo.com/)%" 7 | } 8 | -------------------------------------------------------------------------------- /config/imager-x-aws-serverless-transformer.php: -------------------------------------------------------------------------------- 1 | App::env('IMAGERX_SERVERLESS_DISTRIBUTION_URL'), 7 | 'signatureKey' => App::env('IMAGERX_SERVERLESS_DISTRIBUTION_SIGNATURE'), 8 | ]; 9 | -------------------------------------------------------------------------------- /config/imager-x-transforms.php: -------------------------------------------------------------------------------- 1 | [ 21 | 'displayName' => 'Example Named Transform', 22 | 'transforms' => [ 23 | ['width' => 300], 24 | ['width' => 700], 25 | ], 26 | 'defaults' => [ 27 | 'ratio' => 9 / 16, 28 | 'format' => 'webp', 29 | ], 30 | ], 31 | ]; 32 | -------------------------------------------------------------------------------- /config/imager-x.php: -------------------------------------------------------------------------------- 1 | 'awsserverless', 7 | 'removeMetadata' => true, 8 | 'cacheRemoteFiles' => true, 9 | 'allowUpscale' => false, 10 | 'preserveColorProfiles' => true, 11 | 'transformAnimatedGifs' => false, 12 | 'transformSvgs' => false, 13 | 'fillTransforms' => true, 14 | 'imagerUrl' => App::env('S3_BASE_URL') . '/' . App::env('S3_SUBFOLDER') . '/transforms/', 15 | 'storages' => ['aws'], 16 | 'storageConfig' => [ 17 | 'aws' => [ 18 | 'accessKey' => App::env('S3_ACCESS_KEY_ID'), 19 | 'secretAccessKey' => App::env('S3_SECRET_ACCESS_KEY'), 20 | 'region' => App::env('S3_BUCKET_REGION'), 21 | 'bucket' => App::env('S3_BUCKET'), 22 | 'folder' => App::env('S3_SUBFOLDER') . '/transforms', 23 | 'requestHeaders' => [], 24 | 'storageType' => 'standard', 25 | 'public' => false, 26 | 'cloudfrontInvalidateEnabled' => true, 27 | 'cloudfrontDistributionId' => App::env('S3_CLOUDFRONT_DISTRIBUTION_ID'), 28 | ], 29 | ], 30 | ]; 31 | -------------------------------------------------------------------------------- /config/project/ckeditor/configs/b7e66782-af96-4012-9e17-914134073ced.yaml: -------------------------------------------------------------------------------- 1 | headingLevels: 2 | - 1 3 | - 2 4 | - 3 5 | - 4 6 | - 5 7 | - 6 8 | name: Simple 9 | toolbar: 10 | - bold 11 | - italic 12 | - underline 13 | - link 14 | -------------------------------------------------------------------------------- /config/project/ckeditor/configs/d39724b0-dd22-4c74-aa0b-87a022555cf2.yaml: -------------------------------------------------------------------------------- 1 | css: "/* Hides style buttons that don't apply to selection */\r\n.ck-style-grid__button.ck-disabled { display: none!important; }\r\n\r\n.ck.ck-content .rich-text-size-xlarge {\r\n font-size: 32px;\r\n line-height: 1.2;\r\n}\r\n\r\n.ck.ck-content .rich-text-size-large {\r\n font-size: 24px;\r\n line-height: 1.2;\r\n}\r\n\r\n.ck.ck-content .rich-text-size-medium {\r\n font-size: 18px;\r\n line-height: 1.2;\r\n}\r\n\r\n.ck.ck-content .rich-text-size-small {\r\n font-size: 12px;\r\n line-height: 1.2;\r\n}" 2 | headingLevels: false 3 | js: "/**\r\n * Takes a given definition and creates that definition for the tags defined in elements\r\n */\r\nfunction defintionsForElements(definition, elements = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p']) {\r\n return elements.map((element) => ({\r\n ...definition,\r\n element,\r\n name: `${definition.name} - ${element}`\r\n }))\r\n}\r\n\r\ndebugger\r\n\r\nreturn {\r\n list: {\r\n properties: {\r\n styles: false,\r\n },\r\n },\r\n fontSize: {\r\n options: [\r\n 'a',\r\n 'b',\r\n 'c',\r\n 'd',\r\n 'e',\r\n ]\r\n },\r\n style: {\r\n definitions: [\r\n ...defintionsForElements({\r\n name: 'Heading XXL',\r\n classes: [\r\n 'richtext-heading-xxl',\r\n ],\r\n }),\r\n ...defintionsForElements({\r\n name: 'Heading XL',\r\n classes: [\r\n 'richtext-heading-xl',\r\n ],\r\n }),\r\n ...defintionsForElements({\r\n name: 'Heading XL',\r\n classes: [\r\n 'richtext-heading-xl',\r\n ],\r\n })\r\n ],\r\n },\r\n}" 4 | name: Advanced 5 | toolbar: 6 | - undo 7 | - redo 8 | - style 9 | - fontSize 10 | - '|' 11 | - bold 12 | - italic 13 | - underline 14 | - link 15 | - blockQuote 16 | - '|' 17 | - bulletedList 18 | - numberedList 19 | - outdent 20 | - indent 21 | - '|' 22 | - removeFormat 23 | -------------------------------------------------------------------------------- /config/project/entryTypes/callToAction--534ccdd8-7abd-4300-a102-71885b21c126.yaml: -------------------------------------------------------------------------------- 1 | color: null 2 | fieldLayouts: 3 | 1c969fb9-3597-4082-8e64-34c1c9d9a30e: 4 | tabs: 5 | - 6 | elementCondition: null 7 | elements: 8 | - 9 | autocapitalize: true 10 | autocomplete: false 11 | autocorrect: true 12 | class: null 13 | dateAdded: '2024-10-01T00:05:01+00:00' 14 | disabled: false 15 | elementCondition: null 16 | id: null 17 | includeInCards: false 18 | inputType: null 19 | instructions: null 20 | label: null 21 | max: null 22 | min: null 23 | name: null 24 | orientation: null 25 | placeholder: null 26 | providesThumbs: false 27 | readonly: false 28 | required: true 29 | size: null 30 | step: null 31 | tip: null 32 | title: null 33 | type: craft\fieldlayoutelements\entries\EntryTitleField 34 | uid: eee9050d-ab79-46d2-87ee-878855f72e52 35 | userCondition: null 36 | warning: null 37 | width: 100 38 | - 39 | dateAdded: '2024-10-01T13:11:53+00:00' 40 | elementCondition: null 41 | fieldUid: c52a9170-652b-4d92-a226-69894b1822ed # Rich Text: Simple 42 | handle: null 43 | includeInCards: false 44 | instructions: null 45 | label: Description 46 | providesThumbs: false 47 | required: false 48 | tip: null 49 | type: craft\fieldlayoutelements\CustomField 50 | uid: ba1291b5-b2a2-4ca5-b843-6373de152e83 51 | userCondition: null 52 | warning: null 53 | width: 100 54 | - 55 | dateAdded: '2024-10-01T00:12:31+00:00' 56 | elementCondition: null 57 | type: craft\fieldlayoutelements\HorizontalRule 58 | uid: 484dde25-62c7-4238-aea8-6b652747f0e5 59 | userCondition: null 60 | - 61 | dateAdded: '2025-01-03T17:45:49+00:00' 62 | elementCondition: null 63 | fieldUid: 2ea51656-9b0c-4736-a6fe-c95dc805dc60 # Button 64 | handle: null 65 | includeInCards: false 66 | instructions: null 67 | label: null 68 | providesThumbs: false 69 | required: false 70 | tip: null 71 | type: craft\fieldlayoutelements\CustomField 72 | uid: 765e759a-f61a-4cfa-b03c-9906bd4bdfe6 73 | userCondition: null 74 | warning: null 75 | width: 100 76 | name: Content 77 | uid: 8f12bd58-cd2c-4be0-9371-9e66d9828ff5 78 | userCondition: null 79 | handle: callToAction 80 | hasTitleField: true 81 | icon: diagram-next 82 | name: 'Call To Action' 83 | showSlugField: true 84 | showStatusField: true 85 | slugTranslationKeyFormat: null 86 | slugTranslationMethod: site 87 | titleFormat: null 88 | titleTranslationKeyFormat: null 89 | titleTranslationMethod: site 90 | -------------------------------------------------------------------------------- /config/project/entryTypes/cardGrid--7ced3e9d-9b54-47f6-8103-e5c3a4fbaf9c.yaml: -------------------------------------------------------------------------------- 1 | color: null 2 | fieldLayouts: 3 | 20f64bfb-c1fa-4afa-9631-7660c7cdeb37: 4 | tabs: 5 | - 6 | elementCondition: null 7 | elements: 8 | - 9 | dateAdded: '2024-10-02T23:56:36+00:00' 10 | elementCondition: null 11 | fieldUid: 4382f55f-01a1-4b67-84c8-ebbc89b238d2 # Heading 12 | handle: null 13 | includeInCards: false 14 | instructions: null 15 | label: null 16 | providesThumbs: false 17 | required: false 18 | tip: null 19 | type: craft\fieldlayoutelements\CustomField 20 | uid: dbdfb37c-efcb-4c7f-a367-c9bf274977ed 21 | userCondition: null 22 | warning: null 23 | width: 100 24 | - 25 | dateAdded: '2024-09-30T23:33:26+00:00' 26 | elementCondition: null 27 | fieldUid: 5e84b0b7-6851-47ed-962e-76498ac9476f # Card Grid 28 | handle: null 29 | includeInCards: false 30 | instructions: null 31 | label: null 32 | providesThumbs: false 33 | required: false 34 | tip: null 35 | type: craft\fieldlayoutelements\CustomField 36 | uid: 34ece3df-7fa2-439f-985c-2cd74abfd1e3 37 | userCondition: null 38 | warning: null 39 | width: 100 40 | name: Content 41 | uid: be2ae502-0ddb-4540-a73b-eecc45b4753a 42 | userCondition: null 43 | handle: cardGrid 44 | hasTitleField: false 45 | icon: grid 46 | name: 'Card Grid' 47 | showSlugField: false 48 | showStatusField: true 49 | slugTranslationKeyFormat: null 50 | slugTranslationMethod: site 51 | titleFormat: null 52 | titleTranslationKeyFormat: null 53 | titleTranslationMethod: site 54 | -------------------------------------------------------------------------------- /config/project/entryTypes/codeBlock--cbf3aeac-588c-4aeb-9573-6f1920206abf.yaml: -------------------------------------------------------------------------------- 1 | color: null 2 | fieldLayouts: 3 | e399cfbc-ad49-4346-af55-44c91bafc80c: 4 | tabs: 5 | - 6 | elementCondition: null 7 | elements: 8 | - 9 | dateAdded: '2025-03-10T16:53:04+00:00' 10 | elementCondition: null 11 | fieldUid: f7d30e72-f872-487a-aba1-c8e95268a220 # Code Position 12 | handle: null 13 | includeInCards: false 14 | instructions: null 15 | label: null 16 | providesThumbs: false 17 | required: false 18 | tip: null 19 | type: craft\fieldlayoutelements\CustomField 20 | uid: 0945a6dc-8201-4deb-bbad-dc2af0e0dd9b 21 | userCondition: null 22 | warning: null 23 | width: 100 24 | - 25 | dateAdded: '2025-03-10T17:28:48+00:00' 26 | elementCondition: null 27 | fieldUid: a99edea2-f8ae-44e5-a12f-89cb9aa66738 # Description 28 | handle: null 29 | includeInCards: false 30 | instructions: null 31 | label: null 32 | providesThumbs: false 33 | required: false 34 | tip: "Add a brief description of your code block. \r\n\r\nThis will never appear on the front end of the site. " 35 | type: craft\fieldlayoutelements\CustomField 36 | uid: 1b062515-de1a-4a90-b9e7-368032fe47e7 37 | userCondition: null 38 | warning: null 39 | width: 100 40 | - 41 | dateAdded: '2025-03-10T16:44:51+00:00' 42 | elementCondition: null 43 | fieldUid: 7a701601-ee6c-4d56-8bd0-efa7719f6a67 # Code 44 | handle: null 45 | includeInCards: false 46 | instructions: null 47 | label: null 48 | providesThumbs: false 49 | required: false 50 | tip: null 51 | type: craft\fieldlayoutelements\CustomField 52 | uid: 9848de68-0b9e-4a27-a38c-34a7f68eeaba 53 | userCondition: null 54 | warning: 'Use Caution: Improperly formatted code can break pages.' 55 | width: 100 56 | name: Content 57 | uid: 39937348-1f3d-4fa6-aaf3-cffe7a6c2a13 58 | userCondition: null 59 | handle: codeBlock 60 | hasTitleField: false 61 | icon: code 62 | name: 'Code Block' 63 | showSlugField: false 64 | showStatusField: false 65 | slugTranslationKeyFormat: null 66 | slugTranslationMethod: site 67 | titleFormat: null 68 | titleTranslationKeyFormat: null 69 | titleTranslationMethod: site 70 | -------------------------------------------------------------------------------- /config/project/entryTypes/globalCode--25362218-e66c-494e-9f2c-63e8cbc6ce0a.yaml: -------------------------------------------------------------------------------- 1 | color: null 2 | fieldLayouts: 3 | f275bbb7-5f45-46ad-8feb-16f35f683456: 4 | tabs: 5 | - 6 | elementCondition: null 7 | elements: 8 | - 9 | dateAdded: '2025-03-10T17:00:59+00:00' 10 | dismissible: false 11 | elementCondition: null 12 | style: warning 13 | tip: "**Use Caution:** Improperly formatted code can break pages. \r\n\r\nBe sure to wrap any JavaScript in opening and closing tags. \r\n\r\n " 14 | type: craft\fieldlayoutelements\Tip 15 | uid: 6dae3c5a-5842-433f-93ff-8f5696852098 16 | userCondition: null 17 | - 18 | content: "## Instructions\r\n\r\n**Code Positions**\r\n\r\n- **Start of ** - Code is loaded at the beginning of the tag. Only use when absolutely required.\r\n- **End of ** - Code is loaded at the end of the .\r\n- **Start of ** - Code is loaded at the beginning of the tag. \r\n- **End of ** - Code is loaded at the bottom of the tag after all other scripts, styles and markup has loaded. \r\nThis option has the lowest impact on page speed. " 19 | dateAdded: '2025-03-10T17:02:50+00:00' 20 | displayInPane: true 21 | elementCondition: null 22 | type: craft\fieldlayoutelements\Markdown 23 | uid: c4baee17-adfa-4390-a757-7697c5e4752a 24 | userCondition: null 25 | width: 100 26 | - 27 | dateAdded: '2025-03-10T16:49:09+00:00' 28 | elementCondition: null 29 | fieldUid: 9c7dc3c3-cb8c-4167-880d-ab9426eef873 # Code Blocks 30 | handle: null 31 | includeInCards: false 32 | instructions: null 33 | label: null 34 | providesThumbs: false 35 | required: false 36 | tip: null 37 | type: craft\fieldlayoutelements\CustomField 38 | uid: c37c8413-d135-4e9e-8d12-7ebca4b5662f 39 | userCondition: null 40 | warning: null 41 | width: 100 42 | name: Content 43 | uid: 0cc45244-3937-457a-899e-6f786bb6b0a4 44 | userCondition: null 45 | handle: globalCode 46 | hasTitleField: false 47 | icon: code 48 | name: 'Global Code' 49 | showSlugField: false 50 | showStatusField: false 51 | slugTranslationKeyFormat: null 52 | slugTranslationMethod: site 53 | titleFormat: null 54 | titleTranslationKeyFormat: null 55 | titleTranslationMethod: site 56 | -------------------------------------------------------------------------------- /config/project/entryTypes/heading--e445a3b9-5827-422a-8328-6b10449fa891.yaml: -------------------------------------------------------------------------------- 1 | color: null 2 | fieldLayouts: 3 | 506cf507-9ade-49bb-a2d1-82b7a13493c8: 4 | tabs: 5 | - 6 | elementCondition: null 7 | elements: 8 | - 9 | autocapitalize: true 10 | autocomplete: false 11 | autocorrect: true 12 | class: null 13 | dateAdded: '2024-10-02T21:54:11+00:00' 14 | disabled: false 15 | elementCondition: null 16 | id: null 17 | includeInCards: false 18 | inputType: null 19 | instructions: null 20 | label: null 21 | max: null 22 | min: null 23 | name: null 24 | orientation: null 25 | placeholder: null 26 | providesThumbs: false 27 | readonly: false 28 | requirable: false 29 | size: null 30 | step: null 31 | tip: null 32 | title: null 33 | type: craft\fieldlayoutelements\entries\EntryTitleField 34 | uid: 96628d9f-2772-4b53-b6fc-7a4108ed43f3 35 | userCondition: null 36 | warning: null 37 | width: 100 38 | - 39 | dateAdded: '2024-10-02T22:19:47+00:00' 40 | elementCondition: null 41 | fieldUid: 4382f55f-01a1-4b67-84c8-ebbc89b238d2 # Heading 42 | handle: null 43 | includeInCards: false 44 | instructions: null 45 | label: null 46 | providesThumbs: false 47 | required: true 48 | tip: null 49 | type: craft\fieldlayoutelements\CustomField 50 | uid: 7f551dd8-de08-4ab1-bd9a-cb6c3a17915e 51 | userCondition: null 52 | warning: null 53 | width: 100 54 | name: Content 55 | uid: 5455a7fa-e8af-4d57-ab81-16c5aabd6f30 56 | userCondition: null 57 | handle: heading 58 | hasTitleField: false 59 | icon: text 60 | name: Heading 61 | showSlugField: false 62 | showStatusField: true 63 | slugTranslationKeyFormat: null 64 | slugTranslationMethod: site 65 | titleFormat: '' 66 | titleTranslationKeyFormat: null 67 | titleTranslationMethod: site 68 | -------------------------------------------------------------------------------- /config/project/entryTypes/home--0e7762a5-2553-4c0d-b8af-9b43b59524bf.yaml: -------------------------------------------------------------------------------- 1 | color: null 2 | fieldLayouts: 3 | 1ba4cf8b-7fa5-48a1-bedc-228107cc529a: 4 | tabs: 5 | - 6 | elementCondition: null 7 | elements: 8 | - 9 | autocapitalize: true 10 | autocomplete: false 11 | autocorrect: true 12 | class: null 13 | dateAdded: '2024-09-30T22:56:28+00:00' 14 | disabled: false 15 | elementCondition: null 16 | id: null 17 | includeInCards: false 18 | inputType: null 19 | instructions: null 20 | label: null 21 | max: null 22 | min: null 23 | name: null 24 | orientation: null 25 | placeholder: null 26 | providesThumbs: false 27 | readonly: false 28 | requirable: false 29 | size: null 30 | step: null 31 | tip: null 32 | title: null 33 | type: craft\fieldlayoutelements\entries\EntryTitleField 34 | uid: ea0cb902-1247-4433-bd52-2cccebaa964c 35 | userCondition: null 36 | warning: null 37 | width: 100 38 | - 39 | dateAdded: '2024-09-30T23:01:15+00:00' 40 | elementCondition: null 41 | fieldUid: 7bd21dd3-6b12-4163-8432-030ea6a3f8d3 # Page Blocks 42 | handle: null 43 | includeInCards: false 44 | instructions: null 45 | label: null 46 | providesThumbs: false 47 | required: false 48 | tip: null 49 | type: craft\fieldlayoutelements\CustomField 50 | uid: c2b7fef9-1532-475d-88ab-66770a63c552 51 | userCondition: null 52 | warning: null 53 | width: 100 54 | name: Content 55 | uid: a5f66f1f-782a-4b31-978c-9c9f79c9af26 56 | userCondition: null 57 | handle: home 58 | hasTitleField: false 59 | icon: house 60 | name: Home 61 | showSlugField: false 62 | showStatusField: true 63 | slugTranslationKeyFormat: null 64 | slugTranslationMethod: site 65 | titleFormat: Home 66 | titleTranslationKeyFormat: null 67 | titleTranslationMethod: site 68 | -------------------------------------------------------------------------------- /config/project/entryTypes/iconCard--3f1dc0c5-f091-469a-858a-4cbd3596bab7.yaml: -------------------------------------------------------------------------------- 1 | color: null 2 | fieldLayouts: 3 | 57d6dbf5-4050-46e9-b1d8-2990c925c04c: 4 | cardView: 5 | - 'layoutElement:93e23b5a-05ee-4ba1-9ae8-b4a7471b69e1' 6 | - 'layoutElement:e44a04d0-1ede-4985-9d10-8e2c13dfc7d9' 7 | tabs: 8 | - 9 | elementCondition: null 10 | elements: 11 | - 12 | dateAdded: '2024-09-30T23:33:06+00:00' 13 | elementCondition: null 14 | fieldUid: 0011a64a-91b6-4aa3-98e6-be048394a2cf # Icon 15 | handle: null 16 | includeInCards: true 17 | instructions: null 18 | label: null 19 | providesThumbs: false 20 | required: false 21 | tip: null 22 | type: craft\fieldlayoutelements\CustomField 23 | uid: 93e23b5a-05ee-4ba1-9ae8-b4a7471b69e1 24 | userCondition: null 25 | warning: null 26 | width: 100 27 | - 28 | dateAdded: '2024-09-30T23:33:06+00:00' 29 | elementCondition: null 30 | fieldUid: 69fe4c18-21c9-4a3d-8fce-9988d3f741e4 # Link 31 | handle: null 32 | includeInCards: false 33 | instructions: null 34 | label: null 35 | providesThumbs: false 36 | required: false 37 | tip: null 38 | type: craft\fieldlayoutelements\CustomField 39 | uid: 7a0ed038-606f-41eb-a3a3-c5783a9e4df4 40 | userCondition: null 41 | warning: null 42 | width: 100 43 | - 44 | autocapitalize: true 45 | autocomplete: false 46 | autocorrect: true 47 | class: null 48 | dateAdded: '2024-09-30T23:29:31+00:00' 49 | disabled: false 50 | elementCondition: null 51 | id: null 52 | includeInCards: false 53 | inputType: null 54 | instructions: null 55 | label: null 56 | max: null 57 | min: null 58 | name: null 59 | orientation: null 60 | placeholder: null 61 | providesThumbs: false 62 | readonly: false 63 | required: true 64 | size: null 65 | step: null 66 | tip: null 67 | title: null 68 | type: craft\fieldlayoutelements\entries\EntryTitleField 69 | uid: 2a49c922-8c52-4df2-8069-e87e89e2f03e 70 | userCondition: null 71 | warning: null 72 | width: 100 73 | - 74 | dateAdded: '2024-09-30T23:33:06+00:00' 75 | elementCondition: null 76 | fieldUid: a99edea2-f8ae-44e5-a12f-89cb9aa66738 # Description 77 | handle: null 78 | includeInCards: true 79 | instructions: null 80 | label: null 81 | providesThumbs: false 82 | required: false 83 | tip: null 84 | type: craft\fieldlayoutelements\CustomField 85 | uid: e44a04d0-1ede-4985-9d10-8e2c13dfc7d9 86 | userCondition: null 87 | warning: null 88 | width: 100 89 | name: Content 90 | uid: 3eca7200-b91a-4369-98b2-54b2d5970c79 91 | userCondition: null 92 | handle: iconCard 93 | hasTitleField: true 94 | icon: icons 95 | name: 'Icon Card' 96 | showSlugField: false 97 | showStatusField: false 98 | slugTranslationKeyFormat: null 99 | slugTranslationMethod: site 100 | titleFormat: null 101 | titleTranslationKeyFormat: null 102 | titleTranslationMethod: site 103 | -------------------------------------------------------------------------------- /config/project/entryTypes/imageCard--7d418368-91eb-4c64-8f5b-94c62cd62359.yaml: -------------------------------------------------------------------------------- 1 | color: null 2 | fieldLayouts: 3 | 35c4a5cf-04f0-400e-a3bd-107187ab73ee: 4 | cardView: 5 | - 'layoutElement:137a7e8f-bd2a-46c9-972b-365f2ea8a1b7' 6 | - 'layoutElement:14a7db31-c0d3-47d5-a03a-24b0bba82130' 7 | tabs: 8 | - 9 | elementCondition: null 10 | elements: 11 | - 12 | dateAdded: '2024-09-30T23:29:23+00:00' 13 | elementCondition: null 14 | fieldUid: 570fb481-ad97-46db-8bad-f7b0fd90e831 # Image 15 | handle: null 16 | includeInCards: true 17 | instructions: null 18 | label: null 19 | providesThumbs: false 20 | required: false 21 | tip: null 22 | type: craft\fieldlayoutelements\CustomField 23 | uid: 137a7e8f-bd2a-46c9-972b-365f2ea8a1b7 24 | userCondition: null 25 | warning: null 26 | width: 100 27 | - 28 | dateAdded: '2024-09-30T23:29:23+00:00' 29 | elementCondition: null 30 | fieldUid: 69fe4c18-21c9-4a3d-8fce-9988d3f741e4 # Link 31 | handle: null 32 | includeInCards: false 33 | instructions: null 34 | label: null 35 | providesThumbs: false 36 | required: false 37 | tip: null 38 | type: craft\fieldlayoutelements\CustomField 39 | uid: c76db590-6bba-4a59-9353-adbf9eeb9cf6 40 | userCondition: null 41 | warning: null 42 | width: 100 43 | - 44 | autocapitalize: true 45 | autocomplete: false 46 | autocorrect: true 47 | class: null 48 | dateAdded: '2024-09-30T23:24:56+00:00' 49 | disabled: false 50 | elementCondition: null 51 | id: null 52 | includeInCards: false 53 | inputType: null 54 | instructions: null 55 | label: null 56 | max: null 57 | min: null 58 | name: null 59 | orientation: null 60 | placeholder: null 61 | providesThumbs: false 62 | readonly: false 63 | required: true 64 | size: null 65 | step: null 66 | tip: null 67 | title: null 68 | type: craft\fieldlayoutelements\entries\EntryTitleField 69 | uid: 860a8146-8741-437e-95e0-b4e2bf886d5f 70 | userCondition: null 71 | warning: null 72 | width: 100 73 | - 74 | dateAdded: '2024-09-30T23:29:23+00:00' 75 | elementCondition: null 76 | fieldUid: a99edea2-f8ae-44e5-a12f-89cb9aa66738 # Description 77 | handle: null 78 | includeInCards: true 79 | instructions: null 80 | label: null 81 | providesThumbs: false 82 | required: false 83 | tip: null 84 | type: craft\fieldlayoutelements\CustomField 85 | uid: 14a7db31-c0d3-47d5-a03a-24b0bba82130 86 | userCondition: null 87 | warning: null 88 | width: 100 89 | name: Content 90 | uid: 2d299e0f-b490-4172-874c-983cf1b2cd4f 91 | userCondition: null 92 | handle: imageCard 93 | hasTitleField: true 94 | icon: image-polaroid 95 | name: 'Image Card' 96 | showSlugField: false 97 | showStatusField: false 98 | slugTranslationKeyFormat: null 99 | slugTranslationMethod: site 100 | titleFormat: null 101 | titleTranslationKeyFormat: null 102 | titleTranslationMethod: site 103 | -------------------------------------------------------------------------------- /config/project/entryTypes/pageHero--27f19bae-64f8-47b8-9fc9-7552e11e2656.yaml: -------------------------------------------------------------------------------- 1 | color: null 2 | fieldLayouts: 3 | 4fea0032-240b-4dbe-b1b6-a467326f5323: 4 | tabs: 5 | - 6 | elementCondition: null 7 | elements: 8 | - 9 | autocapitalize: true 10 | autocomplete: false 11 | autocorrect: true 12 | class: null 13 | dateAdded: '2024-10-01T13:27:07+00:00' 14 | disabled: false 15 | elementCondition: null 16 | id: null 17 | includeInCards: false 18 | inputType: null 19 | instructions: null 20 | label: null 21 | max: null 22 | min: null 23 | name: null 24 | orientation: null 25 | placeholder: null 26 | providesThumbs: false 27 | readonly: false 28 | requirable: false 29 | size: null 30 | step: null 31 | tip: null 32 | title: null 33 | type: craft\fieldlayoutelements\entries\EntryTitleField 34 | uid: 77b77d44-0b65-428f-ba8f-f078760a3ff8 35 | userCondition: null 36 | warning: null 37 | width: 100 38 | - 39 | dateAdded: '2024-10-01T13:31:02+00:00' 40 | elementCondition: null 41 | fieldUid: c52a9170-652b-4d92-a226-69894b1822ed # Rich Text: Simple 42 | handle: null 43 | includeInCards: false 44 | instructions: null 45 | label: Description 46 | providesThumbs: false 47 | required: false 48 | tip: null 49 | type: craft\fieldlayoutelements\CustomField 50 | uid: dcf0db81-9675-40bc-ae62-5f69274da598 51 | userCondition: null 52 | warning: null 53 | width: 100 54 | - 55 | dateAdded: '2024-10-01T13:31:02+00:00' 56 | elementCondition: null 57 | fieldUid: 570fb481-ad97-46db-8bad-f7b0fd90e831 # Image 58 | handle: null 59 | includeInCards: false 60 | instructions: null 61 | label: null 62 | providesThumbs: false 63 | required: false 64 | tip: null 65 | type: craft\fieldlayoutelements\CustomField 66 | uid: 252b484d-2307-4e74-b167-a3c63074ae10 67 | userCondition: null 68 | warning: null 69 | width: 100 70 | name: Content 71 | uid: 232fbbf7-98ae-45f0-992d-e4597db12aad 72 | userCondition: null 73 | handle: pageHero 74 | hasTitleField: true 75 | icon: subtitles 76 | name: 'Page Hero' 77 | showSlugField: false 78 | showStatusField: true 79 | slugTranslationKeyFormat: null 80 | slugTranslationMethod: site 81 | titleFormat: '' 82 | titleTranslationKeyFormat: null 83 | titleTranslationMethod: site 84 | -------------------------------------------------------------------------------- /config/project/entryTypes/richText--896b8f2a-ed0c-42c9-b5fd-ba8668fc6784.yaml: -------------------------------------------------------------------------------- 1 | color: null 2 | fieldLayouts: 3 | e33f8044-cf16-4605-bc3d-09679348a498: 4 | tabs: 5 | - 6 | elementCondition: null 7 | elements: 8 | - 9 | autocapitalize: true 10 | autocomplete: false 11 | autocorrect: true 12 | class: null 13 | dateAdded: '2024-09-30T22:58:38+00:00' 14 | disabled: false 15 | elementCondition: null 16 | id: null 17 | includeInCards: false 18 | inputType: null 19 | instructions: null 20 | label: null 21 | max: null 22 | min: null 23 | name: null 24 | orientation: null 25 | placeholder: null 26 | providesThumbs: false 27 | readonly: false 28 | requirable: false 29 | size: null 30 | step: null 31 | tip: null 32 | title: null 33 | type: craft\fieldlayoutelements\entries\EntryTitleField 34 | uid: 205c8050-85f3-478e-b411-c7e6073f9b3e 35 | userCondition: null 36 | warning: null 37 | width: 100 38 | - 39 | dateAdded: '2024-09-30T23:04:32+00:00' 40 | elementCondition: null 41 | fieldUid: cd489d3c-f914-475c-a270-ab926e4e7485 # Rich Text 42 | handle: null 43 | includeInCards: false 44 | instructions: null 45 | label: null 46 | providesThumbs: false 47 | required: false 48 | tip: null 49 | type: craft\fieldlayoutelements\CustomField 50 | uid: 0fc08c7a-1dfa-40d0-b234-5540eae14b50 51 | userCondition: null 52 | warning: null 53 | width: 100 54 | name: Content 55 | uid: b1d8412f-75ff-4e81-896b-ef3b9e203f18 56 | userCondition: null 57 | handle: richText 58 | hasTitleField: false 59 | icon: font-case 60 | name: 'Rich Text' 61 | showSlugField: false 62 | showStatusField: true 63 | slugTranslationKeyFormat: null 64 | slugTranslationMethod: site 65 | titleFormat: '' 66 | titleTranslationKeyFormat: null 67 | titleTranslationMethod: site 68 | -------------------------------------------------------------------------------- /config/project/fields/basicLink--69fe4c18-21c9-4a3d-8fce-9988d3f741e4.yaml: -------------------------------------------------------------------------------- 1 | columnSuffix: null 2 | handle: basicLink 3 | instructions: null 4 | name: Link 5 | searchable: false 6 | settings: 7 | maxLength: 255 8 | showLabelField: false 9 | showTargetField: true 10 | typeSettings: 11 | __assoc__: 12 | - 13 | - url 14 | - 15 | __assoc__: 16 | - 17 | - allowRootRelativeUrls 18 | - '1' 19 | - 20 | - allowAnchors 21 | - '1' 22 | - 23 | - asset 24 | - 25 | __assoc__: 26 | - 27 | - sources 28 | - '*' 29 | - 30 | - allowedKinds 31 | - '*' 32 | - 33 | - showUnpermittedVolumes 34 | - '' 35 | - 36 | - showUnpermittedFiles 37 | - '' 38 | - 39 | - entry 40 | - 41 | __assoc__: 42 | - 43 | - sources 44 | - '*' 45 | types: 46 | - url 47 | - asset 48 | - email 49 | - entry 50 | - tel 51 | translationKeyFormat: null 52 | translationMethod: none 53 | type: craft\fields\Link 54 | -------------------------------------------------------------------------------- /config/project/fields/button--2ea51656-9b0c-4736-a6fe-c95dc805dc60.yaml: -------------------------------------------------------------------------------- 1 | columnSuffix: null 2 | handle: button 3 | instructions: null 4 | name: Button 5 | searchable: false 6 | settings: 7 | maxLength: 255 8 | showLabelField: true 9 | showTargetField: true 10 | typeSettings: 11 | __assoc__: 12 | - 13 | - entry 14 | - 15 | __assoc__: 16 | - 17 | - sources 18 | - '*' 19 | - 20 | - url 21 | - 22 | __assoc__: 23 | - 24 | - allowRootRelativeUrls 25 | - '' 26 | - 27 | - allowAnchors 28 | - '' 29 | - 30 | - asset 31 | - 32 | __assoc__: 33 | - 34 | - sources 35 | - '*' 36 | - 37 | - allowedKinds 38 | - '*' 39 | - 40 | - showUnpermittedVolumes 41 | - '' 42 | - 43 | - showUnpermittedFiles 44 | - '' 45 | types: 46 | - entry 47 | - url 48 | - asset 49 | - email 50 | - tel 51 | translationKeyFormat: null 52 | translationMethod: none 53 | type: craft\fields\Link 54 | -------------------------------------------------------------------------------- /config/project/fields/cardGrid--5e84b0b7-6851-47ed-962e-76498ac9476f.yaml: -------------------------------------------------------------------------------- 1 | columnSuffix: null 2 | handle: cardGrid 3 | instructions: null 4 | name: 'Card Grid' 5 | searchable: false 6 | settings: 7 | createButtonLabel: 'New card' 8 | defaultIndexViewMode: cards 9 | entryTypes: 10 | - 7d418368-91eb-4c64-8f5b-94c62cd62359 # Image Card 11 | - 3f1dc0c5-f091-469a-858a-4cbd3596bab7 # Icon Card 12 | includeTableView: false 13 | maxEntries: null 14 | minEntries: null 15 | pageSize: 50 16 | propagationKeyFormat: null 17 | propagationMethod: all 18 | showCardsInGrid: true 19 | viewMode: cards 20 | translationKeyFormat: null 21 | translationMethod: site 22 | type: craft\fields\Matrix 23 | -------------------------------------------------------------------------------- /config/project/fields/code--7a701601-ee6c-4d56-8bd0-efa7719f6a67.yaml: -------------------------------------------------------------------------------- 1 | columnSuffix: null 2 | handle: code 3 | instructions: null 4 | name: Code 5 | searchable: false 6 | settings: 7 | byteLimit: null 8 | charLimit: null 9 | code: true 10 | initialRows: 4 11 | multiline: true 12 | placeholder: null 13 | uiMode: normal 14 | translationKeyFormat: null 15 | translationMethod: none 16 | type: craft\fields\PlainText 17 | -------------------------------------------------------------------------------- /config/project/fields/codeBlocks--9c7dc3c3-cb8c-4167-880d-ab9426eef873.yaml: -------------------------------------------------------------------------------- 1 | columnSuffix: null 2 | handle: codeBlocks 3 | instructions: null 4 | name: 'Code Blocks' 5 | searchable: false 6 | settings: 7 | createButtonLabel: 'Add Code Block' 8 | defaultIndexViewMode: cards 9 | entryTypes: 10 | - 11 | __assoc__: 12 | - 13 | - uid 14 | - cbf3aeac-588c-4aeb-9573-6f1920206abf # Code Block 15 | includeTableView: false 16 | maxEntries: null 17 | minEntries: null 18 | pageSize: null 19 | propagationKeyFormat: null 20 | propagationMethod: all 21 | showCardsInGrid: false 22 | viewMode: blocks 23 | translationKeyFormat: null 24 | translationMethod: site 25 | type: craft\fields\Matrix 26 | -------------------------------------------------------------------------------- /config/project/fields/codePosition--f7d30e72-f872-487a-aba1-c8e95268a220.yaml: -------------------------------------------------------------------------------- 1 | columnSuffix: null 2 | handle: codePosition 3 | instructions: null 4 | name: 'Code Position' 5 | searchable: false 6 | settings: 7 | customOptions: false 8 | options: 9 | - 10 | __assoc__: 11 | - 12 | - label 13 | - 'Start of ' 14 | - 15 | - value 16 | - headTop 17 | - 18 | - default 19 | - '' 20 | - 21 | __assoc__: 22 | - 23 | - label 24 | - 'End of ' 25 | - 26 | - value 27 | - headBottom 28 | - 29 | - default 30 | - '1' 31 | - 32 | __assoc__: 33 | - 34 | - label 35 | - 'Start of ' 36 | - 37 | - value 38 | - bodyTop 39 | - 40 | - default 41 | - '' 42 | - 43 | __assoc__: 44 | - 45 | - label 46 | - 'End of ' 47 | - 48 | - value 49 | - bodyBottom 50 | - 51 | - default 52 | - '' 53 | translationKeyFormat: null 54 | translationMethod: none 55 | type: craft\fields\Dropdown 56 | -------------------------------------------------------------------------------- /config/project/fields/description--a99edea2-f8ae-44e5-a12f-89cb9aa66738.yaml: -------------------------------------------------------------------------------- 1 | columnSuffix: null 2 | handle: description 3 | instructions: null 4 | name: Description 5 | searchable: false 6 | settings: 7 | byteLimit: null 8 | charLimit: null 9 | code: false 10 | initialRows: 2 11 | multiline: true 12 | placeholder: null 13 | uiMode: normal 14 | translationKeyFormat: null 15 | translationMethod: none 16 | type: craft\fields\PlainText 17 | -------------------------------------------------------------------------------- /config/project/fields/heading--4382f55f-01a1-4b67-84c8-ebbc89b238d2.yaml: -------------------------------------------------------------------------------- 1 | columnSuffix: null 2 | handle: heading 3 | instructions: null 4 | name: Heading 5 | searchable: false 6 | settings: 7 | byteLimit: null 8 | charLimit: null 9 | code: false 10 | initialRows: 1 11 | multiline: true 12 | placeholder: null 13 | uiMode: normal 14 | translationKeyFormat: null 15 | translationMethod: none 16 | type: craft\fields\PlainText 17 | -------------------------------------------------------------------------------- /config/project/fields/icon--0011a64a-91b6-4aa3-98e6-be048394a2cf.yaml: -------------------------------------------------------------------------------- 1 | columnSuffix: null 2 | handle: icon 3 | instructions: null 4 | name: Icon 5 | searchable: false 6 | settings: 7 | options: 8 | - 9 | __assoc__: 10 | - 11 | - label 12 | - None 13 | - 14 | - value 15 | - '' 16 | - 17 | - default 18 | - '1' 19 | - 20 | __assoc__: 21 | - 22 | - label 23 | - 'Arrow Left' 24 | - 25 | - value 26 | - arrow-left 27 | - 28 | - default 29 | - '' 30 | - 31 | __assoc__: 32 | - 33 | - label 34 | - 'Arrow Right' 35 | - 36 | - value 37 | - arrowRight 38 | - 39 | - default 40 | - '' 41 | - 42 | __assoc__: 43 | - 44 | - label 45 | - Clock 46 | - 47 | - value 48 | - clock 49 | - 50 | - default 51 | - '' 52 | - 53 | __assoc__: 54 | - 55 | - label 56 | - Close 57 | - 58 | - value 59 | - close 60 | - 61 | - default 62 | - '' 63 | translationKeyFormat: null 64 | translationMethod: none 65 | type: craft\fields\Dropdown 66 | -------------------------------------------------------------------------------- /config/project/fields/image--570fb481-ad97-46db-8bad-f7b0fd90e831.yaml: -------------------------------------------------------------------------------- 1 | columnSuffix: null 2 | handle: image 3 | instructions: null 4 | name: Image 5 | searchable: false 6 | settings: 7 | allowSelfRelations: false 8 | allowSubfolders: false 9 | allowUploads: true 10 | allowedKinds: 11 | - image 12 | branchLimit: null 13 | defaultUploadLocationSource: 'volume:853413e4-e02c-487e-81a5-04e58eb18683' # Assets 14 | defaultUploadLocationSubpath: null 15 | maintainHierarchy: false 16 | maxRelations: 1 17 | minRelations: null 18 | previewMode: full 19 | restrictFiles: true 20 | restrictLocation: false 21 | restrictedDefaultUploadSubpath: null 22 | restrictedLocationSource: 'volume:853413e4-e02c-487e-81a5-04e58eb18683' # Assets 23 | restrictedLocationSubpath: null 24 | selectionLabel: 'Add image' 25 | showCardsInGrid: false 26 | showSiteMenu: false 27 | showUnpermittedFiles: false 28 | showUnpermittedVolumes: false 29 | sources: '*' 30 | targetSiteId: null 31 | validateRelatedElements: false 32 | viewMode: list 33 | translationKeyFormat: null 34 | translationMethod: none 35 | type: craft\fields\Assets 36 | -------------------------------------------------------------------------------- /config/project/fields/mediaType--ac3b0590-d417-429e-aa1f-96be3c79a5b0.yaml: -------------------------------------------------------------------------------- 1 | columnSuffix: null 2 | handle: mediaType 3 | instructions: null 4 | name: 'Media Type' 5 | searchable: false 6 | settings: 7 | options: 8 | - 9 | __assoc__: 10 | - 11 | - label 12 | - Image 13 | - 14 | - value 15 | - image 16 | - 17 | - default 18 | - '1' 19 | - 20 | __assoc__: 21 | - 22 | - label 23 | - Video 24 | - 25 | - value 26 | - video 27 | - 28 | - default 29 | - '' 30 | translationKeyFormat: null 31 | translationMethod: none 32 | type: craft\fields\RadioButtons 33 | -------------------------------------------------------------------------------- /config/project/fields/pageBlocks--7bd21dd3-6b12-4163-8432-030ea6a3f8d3.yaml: -------------------------------------------------------------------------------- 1 | columnSuffix: null 2 | handle: pageBlocks 3 | instructions: null 4 | name: 'Page Blocks' 5 | searchable: false 6 | settings: 7 | createButtonLabel: 'New Block' 8 | defaultIndexViewMode: cards 9 | entryTypes: 10 | - 27f19bae-64f8-47b8-9fc9-7552e11e2656 # Page Hero 11 | - e445a3b9-5827-422a-8328-6b10449fa891 # Heading 12 | - 896b8f2a-ed0c-42c9-b5fd-ba8668fc6784 # Rich Text 13 | - 3bcf7ddb-71e7-4115-868c-815b3611b34c # Text With Media 14 | - 7ced3e9d-9b54-47f6-8103-e5c3a4fbaf9c # Card Grid 15 | - 534ccdd8-7abd-4300-a102-71885b21c126 # Call To Action 16 | includeTableView: false 17 | maxEntries: null 18 | minEntries: null 19 | pageSize: null 20 | propagationKeyFormat: null 21 | propagationMethod: all 22 | showCardsInGrid: false 23 | viewMode: blocks 24 | translationKeyFormat: null 25 | translationMethod: site 26 | type: craft\fields\Matrix 27 | -------------------------------------------------------------------------------- /config/project/fields/richText--cd489d3c-f914-475c-a270-ab926e4e7485.yaml: -------------------------------------------------------------------------------- 1 | columnSuffix: null 2 | handle: richText 3 | instructions: null 4 | name: 'Rich Text' 5 | searchable: false 6 | settings: 7 | availableTransforms: '' 8 | availableVolumes: '*' 9 | ckeConfig: d39724b0-dd22-4c74-aa0b-87a022555cf2 # Advanced 10 | createButtonLabel: null 11 | defaultTransform: null 12 | enableSourceEditingForNonAdmins: false 13 | purifierConfig: null 14 | purifyHtml: true 15 | showUnpermittedFiles: false 16 | showUnpermittedVolumes: false 17 | showWordCount: false 18 | wordLimit: null 19 | translationKeyFormat: null 20 | translationMethod: none 21 | type: craft\ckeditor\Field 22 | -------------------------------------------------------------------------------- /config/project/fields/richTextSimple--c52a9170-652b-4d92-a226-69894b1822ed.yaml: -------------------------------------------------------------------------------- 1 | columnSuffix: null 2 | handle: richTextSimple 3 | instructions: null 4 | name: 'Rich Text: Simple' 5 | searchable: false 6 | settings: 7 | availableTransforms: '' 8 | availableVolumes: '*' 9 | ckeConfig: b7e66782-af96-4012-9e17-914134073ced # Simple 10 | createButtonLabel: null 11 | defaultTransform: null 12 | enableSourceEditingForNonAdmins: false 13 | purifierConfig: null 14 | purifyHtml: true 15 | showUnpermittedFiles: false 16 | showUnpermittedVolumes: false 17 | showWordCount: false 18 | wordLimit: null 19 | translationKeyFormat: null 20 | translationMethod: none 21 | type: craft\ckeditor\Field 22 | -------------------------------------------------------------------------------- /config/project/fields/side--d7f9c488-b14d-48c2-b6fc-d616b7a9f49f.yaml: -------------------------------------------------------------------------------- 1 | columnSuffix: null 2 | handle: side 3 | instructions: null 4 | name: 'Show Media On' 5 | searchable: false 6 | settings: 7 | options: 8 | - 9 | __assoc__: 10 | - 11 | - label 12 | - Left 13 | - 14 | - value 15 | - left 16 | - 17 | - default 18 | - '1' 19 | - 20 | __assoc__: 21 | - 22 | - label 23 | - Right 24 | - 25 | - value 26 | - right 27 | - 28 | - default 29 | - '' 30 | translationKeyFormat: null 31 | translationMethod: none 32 | type: craft\fields\RadioButtons 33 | -------------------------------------------------------------------------------- /config/project/fields/videoUrl--55cc26b6-8b49-47de-bbe7-5908da6f4545.yaml: -------------------------------------------------------------------------------- 1 | columnSuffix: null 2 | handle: videoUrl 3 | instructions: null 4 | name: 'Video Url' 5 | searchable: false 6 | settings: 7 | byteLimit: null 8 | charLimit: null 9 | code: false 10 | initialRows: 4 11 | multiline: false 12 | placeholder: null 13 | uiMode: normal 14 | translationKeyFormat: null 15 | translationMethod: none 16 | type: craft\fields\PlainText 17 | -------------------------------------------------------------------------------- /config/project/project.yaml: -------------------------------------------------------------------------------- 1 | dateModified: 1748040902 2 | elementSources: 3 | craft\elements\Entry: 4 | - 5 | defaultSort: 6 | - postDate 7 | - desc 8 | defaultViewMode: '' 9 | disabled: false 10 | key: '*' 11 | tableAttributes: 12 | - status 13 | - section 14 | - postDate 15 | - expiryDate 16 | - link 17 | type: native 18 | - 19 | heading: Singles 20 | type: heading 21 | - 22 | key: 'single:969acdd6-6362-41b1-b782-6bbd43a8a6b4' # Home 23 | type: native 24 | - 25 | heading: Globals 26 | type: heading 27 | - 28 | defaultSort: 29 | - id 30 | - asc 31 | defaultViewMode: '' 32 | disabled: false 33 | key: 'single:b4bfeb63-5bc4-4368-b4c9-408f7bac68fc' # Global Code 34 | tableAttributes: 35 | - status 36 | - postDate 37 | - expiryDate 38 | - link 39 | type: native 40 | email: 41 | fromEmail: $SYSTEM_EMAIL_FROM 42 | fromName: 'Viget Craft Starter' 43 | replyToEmail: null 44 | template: null 45 | transportSettings: 46 | host: $SYSTEM_EMAIL_HOSTNAME 47 | password: $SYSTEM_EMAIL_PASSWORD 48 | port: $SYSTEM_EMAIL_PORT 49 | useAuthentication: '1' 50 | username: $SYSTEM_EMAIL_USERNAME 51 | transportType: craft\mail\transportadapters\Smtp 52 | fs: 53 | s3: 54 | hasUrls: true 55 | name: S3 56 | settings: 57 | addSubfolderToRootUrl: true 58 | autoFocalPoint: false 59 | bucket: $S3_BUCKET 60 | bucketSelectionMode: manual 61 | cfDistributionId: $S3_CLOUDFRONT_DISTRIBUTION_ID 62 | cfPrefix: $S3_SUBFOLDER 63 | expires: '30 days' 64 | keyId: $S3_ACCESS_KEY_ID 65 | makeUploadsPublic: false 66 | region: $S3_BUCKET_REGION 67 | secret: $S3_SECRET_ACCESS_KEY 68 | storageClass: '' 69 | subfolder: $S3_SUBFOLDER 70 | type: craft\awss3\Fs 71 | url: $S3_BASE_URL 72 | meta: 73 | __names__: 74 | 0e7762a5-2553-4c0d-b8af-9b43b59524bf: Home # Home 75 | 2ea51656-9b0c-4736-a6fe-c95dc805dc60: Button # Button 76 | 3bcf7ddb-71e7-4115-868c-815b3611b34c: 'Text With Media' # Text With Media 77 | 3f1dc0c5-f091-469a-858a-4cbd3596bab7: 'Icon Card' # Icon Card 78 | 5e84b0b7-6851-47ed-962e-76498ac9476f: 'Card Grid' # Card Grid 79 | 7a701601-ee6c-4d56-8bd0-efa7719f6a67: Code # Code 80 | 7bd21dd3-6b12-4163-8432-030ea6a3f8d3: 'Page Blocks' # Page Blocks 81 | 7ced3e9d-9b54-47f6-8103-e5c3a4fbaf9c: 'Card Grid' # Card Grid 82 | 7d418368-91eb-4c64-8f5b-94c62cd62359: 'Image Card' # Image Card 83 | 9c7dc3c3-cb8c-4167-880d-ab9426eef873: 'Code Blocks' # Code Blocks 84 | 0011a64a-91b6-4aa3-98e6-be048394a2cf: Icon # Icon 85 | 27f19bae-64f8-47b8-9fc9-7552e11e2656: 'Page Hero' # Page Hero 86 | 35b563a0-4662-40b9-b885-a8450a2868d9: 'Viget Craft Starter' # Viget Craft Starter 87 | 55cc26b6-8b49-47de-bbe7-5908da6f4545: 'Video Url' # Video Url 88 | 69fe4c18-21c9-4a3d-8fce-9988d3f741e4: Link # Link 89 | 101b4db6-8914-45eb-a661-180b15f70894: 'Footer Navigation' # Footer Navigation 90 | 534ccdd8-7abd-4300-a102-71885b21c126: 'Call To Action' # Call To Action 91 | 570fb481-ad97-46db-8bad-f7b0fd90e831: Image # Image 92 | 805d8826-faed-4186-9b88-f509eb9b07e6: 'Viget Craft Starter' # Viget Craft Starter 93 | 896b8f2a-ed0c-42c9-b5fd-ba8668fc6784: 'Rich Text' # Rich Text 94 | 969acdd6-6362-41b1-b782-6bbd43a8a6b4: Home # Home 95 | 4382f55f-01a1-4b67-84c8-ebbc89b238d2: Heading # Heading 96 | 853413e4-e02c-487e-81a5-04e58eb18683: Assets # Assets 97 | 25362218-e66c-494e-9f2c-63e8cbc6ce0a: 'Global Code' # Global Code 98 | a99edea2-f8ae-44e5-a12f-89cb9aa66738: Description # Description 99 | ac3b0590-d417-429e-aa1f-96be3c79a5b0: 'Media Type' # Media Type 100 | b3d7753a-c7ad-4f33-aca8-df63277e9b42: 'Primary Navigation' # Primary Navigation 101 | b4bfeb63-5bc4-4368-b4c9-408f7bac68fc: 'Global Code' # Global Code 102 | b7e66782-af96-4012-9e17-914134073ced: Simple # Simple 103 | c52a9170-652b-4d92-a226-69894b1822ed: 'Rich Text: Simple' # Rich Text: Simple 104 | cbf3aeac-588c-4aeb-9573-6f1920206abf: 'Code Block' # Code Block 105 | cd489d3c-f914-475c-a270-ab926e4e7485: 'Rich Text' # Rich Text 106 | d7f9c488-b14d-48c2-b6fc-d616b7a9f49f: 'Show Media On' # Show Media On 107 | d39724b0-dd22-4c74-aa0b-87a022555cf2: Advanced # Advanced 108 | e445a3b9-5827-422a-8328-6b10449fa891: Heading # Heading 109 | f7d30e72-f872-487a-aba1-c8e95268a220: 'Code Position' # Code Position 110 | plugins: 111 | aws-s3: 112 | edition: standard 113 | enabled: true 114 | schemaVersion: '2.0' 115 | ckeditor: 116 | edition: standard 117 | enabled: true 118 | schemaVersion: 3.0.0.0 119 | classnames: 120 | edition: standard 121 | enabled: true 122 | schemaVersion: 3.0.0 123 | cp-field-inspect: 124 | edition: standard 125 | enabled: true 126 | schemaVersion: 1.0.0 127 | empty-coalesce: 128 | edition: standard 129 | enabled: true 130 | schemaVersion: 1.0.0 131 | expanded-singles: 132 | edition: standard 133 | enabled: true 134 | schemaVersion: 1.0.0 135 | settings: 136 | expandSingles: true 137 | redirectToEntry: true 138 | imager-x: 139 | edition: pro 140 | enabled: true 141 | licenseKey: REPLACE 142 | schemaVersion: 4.0.0 143 | imager-x-aws-serverless-transformer: 144 | edition: standard 145 | enabled: true 146 | schemaVersion: 2.0.0 147 | imager-x-power-pack: 148 | edition: standard 149 | enabled: true 150 | schemaVersion: 1.0.0 151 | navigation: 152 | edition: standard 153 | enabled: true 154 | licenseKey: REPLACE 155 | schemaVersion: 2.1.0 156 | retour: 157 | edition: standard 158 | enabled: true 159 | licenseKey: REPLACE 160 | schemaVersion: 3.0.12 161 | seomatic: 162 | edition: standard 163 | enabled: true 164 | licenseKey: REPLACE 165 | schemaVersion: 3.0.13 166 | viget-parts-kit: 167 | edition: standard 168 | enabled: true 169 | schemaVersion: 1.0.0 170 | vite: 171 | edition: standard 172 | enabled: true 173 | schemaVersion: 1.0.0 174 | system: 175 | edition: solo 176 | live: true 177 | name: 'Viget Craft Starter' 178 | schemaVersion: 5.7.0.3 179 | timeZone: America/Los_Angeles 180 | users: 181 | allowPublicRegistration: false 182 | defaultGroup: null 183 | photoSubpath: null 184 | photoVolumeUid: null 185 | require2fa: false 186 | requireEmailVerification: true 187 | -------------------------------------------------------------------------------- /config/project/sections/globalCode--b4bfeb63-5bc4-4368-b4c9-408f7bac68fc.yaml: -------------------------------------------------------------------------------- 1 | defaultPlacement: end 2 | enableVersioning: true 3 | entryTypes: 4 | - 5 | uid: 25362218-e66c-494e-9f2c-63e8cbc6ce0a # Global Code 6 | handle: globalCode 7 | maxAuthors: 1 8 | name: 'Global Code' 9 | propagationMethod: all 10 | siteSettings: 11 | 35b563a0-4662-40b9-b885-a8450a2868d9: # Viget Craft Starter 12 | enabledByDefault: true 13 | hasUrls: false 14 | template: null 15 | uriFormat: null 16 | type: single 17 | -------------------------------------------------------------------------------- /config/project/sections/home--969acdd6-6362-41b1-b782-6bbd43a8a6b4.yaml: -------------------------------------------------------------------------------- 1 | defaultPlacement: end 2 | enableVersioning: true 3 | entryTypes: 4 | - 0e7762a5-2553-4c0d-b8af-9b43b59524bf # Home 5 | handle: home 6 | maxAuthors: 1 7 | name: Home 8 | previewTargets: 9 | - 10 | __assoc__: 11 | - 12 | - label 13 | - 'Primary entry page' 14 | - 15 | - urlFormat 16 | - '{url}' 17 | - 18 | - refresh 19 | - '1' 20 | propagationMethod: all 21 | siteSettings: 22 | 35b563a0-4662-40b9-b885-a8450a2868d9: # Viget Craft Starter 23 | enabledByDefault: true 24 | hasUrls: true 25 | template: _elements/home.twig 26 | uriFormat: __home__ 27 | type: single 28 | -------------------------------------------------------------------------------- /config/project/siteGroups/805d8826-faed-4186-9b88-f509eb9b07e6.yaml: -------------------------------------------------------------------------------- 1 | name: 'Viget Craft Starter' 2 | -------------------------------------------------------------------------------- /config/project/sites/default--35b563a0-4662-40b9-b885-a8450a2868d9.yaml: -------------------------------------------------------------------------------- 1 | baseUrl: $PRIMARY_SITE_URL 2 | enabled: true 3 | handle: default 4 | hasUrls: true 5 | language: en-US 6 | name: 'Viget Craft Starter' 7 | primary: true 8 | siteGroup: 805d8826-faed-4186-9b88-f509eb9b07e6 # Viget Craft Starter 9 | sortOrder: 1 10 | -------------------------------------------------------------------------------- /config/project/volumes/assets--853413e4-e02c-487e-81a5-04e58eb18683.yaml: -------------------------------------------------------------------------------- 1 | altTranslationKeyFormat: null 2 | altTranslationMethod: none 3 | fieldLayouts: 4 | a7f496e4-142f-49a4-b241-2cb6ce1a1f30: 5 | tabs: 6 | - 7 | elementCondition: null 8 | elements: 9 | - 10 | autocapitalize: true 11 | autocomplete: false 12 | autocorrect: true 13 | class: null 14 | dateAdded: '2024-09-25T21:25:44+00:00' 15 | disabled: false 16 | elementCondition: null 17 | id: null 18 | includeInCards: false 19 | inputType: null 20 | instructions: null 21 | label: null 22 | max: null 23 | min: null 24 | name: null 25 | orientation: null 26 | placeholder: null 27 | providesThumbs: false 28 | readonly: false 29 | requirable: false 30 | size: null 31 | step: null 32 | tip: null 33 | title: null 34 | type: craft\fieldlayoutelements\assets\AssetTitleField 35 | uid: 46777601-180e-43d4-8853-3ab2dcd42057 36 | userCondition: null 37 | warning: null 38 | width: 100 39 | - 40 | attribute: alt 41 | class: null 42 | cols: null 43 | dateAdded: '2024-09-25T21:27:17+00:00' 44 | disabled: false 45 | elementCondition: null 46 | id: null 47 | includeInCards: false 48 | instructions: null 49 | label: null 50 | name: null 51 | orientation: null 52 | placeholder: null 53 | providesThumbs: false 54 | readonly: false 55 | requirable: true 56 | required: false 57 | rows: null 58 | tip: null 59 | title: null 60 | type: craft\fieldlayoutelements\assets\AltField 61 | uid: d508c2f5-a04e-4614-b005-f848c95bfaa5 62 | userCondition: null 63 | warning: null 64 | width: 100 65 | name: Content 66 | uid: c5d6deb9-e383-44bf-aa0f-a8de30dfca6f 67 | userCondition: null 68 | fs: s3 69 | handle: assets 70 | name: Assets 71 | sortOrder: 1 72 | subpath: '' 73 | titleTranslationKeyFormat: null 74 | titleTranslationMethod: site 75 | transformFs: '' 76 | transformSubpath: transforms 77 | -------------------------------------------------------------------------------- /config/redirects.php: -------------------------------------------------------------------------------- 1 | { 4 | // Base Styles 5 | const base = { 6 | // core 7 | '@apply inline-flex items-center justify-center rounded font-bold transition text-base gap-12 cursor-pointer no-underline': 8 | {}, 9 | // focus 10 | '@apply focus:outline-none focus-visible:ring-4': {}, 11 | // disabled 12 | '@apply disabled:opacity-20 disabled:pointer-events-none': {}, 13 | // icon 14 | '& svg': { 15 | '@apply size-20': {}, 16 | }, 17 | // text exempt 18 | '&:not(.btn-text)': { 19 | '@apply px-20 min-h-40': {}, 20 | }, 21 | } 22 | 23 | const buttons = { 24 | // Button Variants 25 | '.btn-contained': { 26 | ...base, 27 | [`@apply 28 | bg-sky-600 text-white 29 | hover:bg-sky-700 30 | active:bg-sky-800 31 | focus-visible:bg-sky-700 focus-visible:ring-sky-600/50 32 | dark:bg-sky-200 dark:text-sky-800 33 | dark:hover:bg-sky-50 34 | dark:active:bg-sky-100 35 | dark:focus-visible:bg-sky-50 dark:focus-visible:ring-white/50`]: {}, 36 | }, 37 | 38 | '.btn-outlined': { 39 | ...base, 40 | [`@apply 41 | border border-sky-600 bg-transparent text-sky-600 42 | hover:bg-sky-100 hover:border-sky-700 hover:text-sky-700 43 | active:bg-sky-200/80 active:text-sky-800 44 | focus-visible:bg-sky-100 focus-visible:border-sky-700 focus-visible:ring-sky-600/50 45 | dark:border-white dark:text-white 46 | dark:hover:bg-white/25 47 | dark:active:bg-white/30 48 | dark:focus-visible:bg-sky-100/30 dark:focus-visible:ring-white/50`]: {}, 49 | }, 50 | 51 | '.btn-subtle': { 52 | ...base, 53 | [`@apply 54 | bg-transparent bg-transparent text-sky-600 55 | hover:bg-sky-100 hover:text-sky-700 56 | active:bg-sky-200/80 active:text-sky-800 57 | focus-visible:bg-sky-100 focus-visible:border-sky-700 focus-visible:ring-sky-600/50 58 | dark:text-white 59 | dark:hover:bg-white/25 60 | dark:active:bg-white/30 61 | dark:focus-visible:bg-sky-100/30 dark:focus-visible:ring-white/75`]: {}, 62 | }, 63 | 64 | '.btn-text': { 65 | ...base, 66 | [`@apply 67 | bg-transparent text-sky-600 68 | hover:text-sky-700 hover:underline 69 | active:text-sky-800 70 | focus-visible:bg-sky-100 focus-visible:ring-sky-600/50 71 | dark:border-white dark:text-white 72 | dark:hover:text-white/90 73 | dark:active:text-white/80 74 | dark:focus-visible:bg-sky-100/30 dark:focus-visible:ring-white/75`]: 75 | {}, 76 | }, 77 | 78 | // Button Sizes 79 | '.btn-sm': { 80 | '@apply text-sm gap-8': {}, 81 | '& svg': { 82 | '@apply size-16': {}, 83 | }, 84 | '&:not(.btn-text)': { 85 | '@apply px-16 min-h-32': {}, 86 | }, 87 | }, 88 | '.btn-lg': { 89 | '@apply text-xl gap-12': {}, 90 | '& svg': { 91 | '@apply size-24': {}, 92 | }, 93 | '&:not(.btn-text)': { 94 | '@apply px-32 min-h-56': {}, 95 | }, 96 | }, 97 | 98 | // Button Icon/Sizes 99 | '.btn-icon': { 100 | fontSize: '0 !important', 101 | lineHeight: '0 !important', 102 | '@apply !p-0 !gap-0 aspect-square justify-center items-center': {}, 103 | }, 104 | } 105 | 106 | addComponents(buttons) 107 | }) 108 | -------------------------------------------------------------------------------- /config/tailwind/dialog.js: -------------------------------------------------------------------------------- 1 | import plugin from 'tailwindcss/plugin' 2 | 3 | export default plugin(({ addComponents }) => { 4 | addComponents({ 5 | '.dialog': { 6 | '@apply invisible opacity-0 fixed inset-24 overflow-hidden z-50 open:visible m-auto bg-white p-24 text-black dark:border dark:border-gray-500 dark:bg-gray-900 dark:text-white rounded': 7 | {}, 8 | '&[open]': { 9 | animation: 10 | 'dialog-in var(--dialog-transition-duration, 300ms) cubic-bezier(0.2, 0, 0.13, 1) forwards', 11 | }, 12 | '&::backdrop': { 13 | animation: 14 | 'dialog-backdrop-in var(--dialog-transition-duration, 300ms) cubic-bezier(0.2, 0, 0.13, 1) forwards', 15 | '@apply bg-black/80 backdrop-blur-sm': {}, 16 | }, 17 | '&[data-dialog-status="closing"]': { 18 | animation: 19 | 'dialog-out var(--dialog-transition-duration, 300ms) cubic-bezier(0.2, 0, 0.13, 1) forwards', 20 | 21 | '&::backdrop': { 22 | animation: 23 | 'dialog-backdrop-out var(--dialog-transition-duration, 300ms) cubic-bezier(0.2, 0, 0.13, 1) forwards', 24 | }, 25 | }, 26 | }, 27 | // Animation Keyframes 28 | '@keyframes dialog-in': { 29 | '0%': { transform: 'translateY(2rem)', opacity: '0' }, 30 | '100%': { transform: 'translateY(0px)', opacity: '1' }, 31 | }, 32 | '@keyframes dialog-out': { 33 | '0%': { transform: 'scale(1)', opacity: '1' }, 34 | '100%': { transform: 'scale(0.9)', opacity: '0' }, 35 | }, 36 | '@keyframes dialog-backdrop-in': { 37 | '0%': { opacity: '0' }, 38 | '100%': { opacity: '1' }, 39 | }, 40 | '@keyframes dialog-backdrop-out': { 41 | '0%': { opacity: '1' }, 42 | '100%': { opacity: '0' }, 43 | }, 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /config/tailwind/forms.js: -------------------------------------------------------------------------------- 1 | import plugin from 'tailwindcss/plugin' 2 | 3 | export default plugin(({ addComponents }) => { 4 | // Base styles 5 | const inputs = { 6 | '.field-group': { 7 | '@apply flex flex-col gap-8': {}, 8 | // label container 9 | '& label': { 10 | '@apply flex flex-col [&:has(.sr-only:last-child)]:sr-only': {}, 11 | }, 12 | // label 13 | '& .field-label': { 14 | '@apply text-gray-900 dark:text-white': {}, 15 | }, 16 | // sublabel & instructions 17 | '& .field-sublabel, & .field-instructions': { 18 | '@apply text-sm text-gray-700 dark:text-gray-300': {}, 19 | }, 20 | // core field styles 21 | '& input[type="text"], & input[type="password"], & input[type="email"], & input[type="number"], & input[type="tel"], & input[type="url"], & select, & textarea': 22 | { 23 | // base styles 24 | '@apply rounded-sm border-0 bg-white ring-1 outline-none ring-black/50 px-8 dark:bg-white/10 dark:text-white dark:ring-white/50': 25 | {}, 26 | // placeholder styles 27 | '@apply placeholder:text-black/40 dark:placeholder:text-white/50': {}, 28 | // hover styles 29 | '@apply hover:ring-black/75 dark:hover:ring-white/75': {}, 30 | // focus styles 31 | '@apply focus:ring-blue-500 focus:ring-2 dark:focus:ring-white/75': 32 | {}, 33 | // disabled styles 34 | '@apply disabled:bg-gray-500/10 disabled:ring-gray-500/20 disabled:text-black/75 disabled:cursor-not-allowed dark:disabled:ring-white/20 dark:disabled:bg-white/25 dark:disabled:text-white/50 dark:disabled:placeholder:text-white/35': 35 | {}, 36 | // Exclude textarea from fix height 37 | '&:not(textarea)': { 38 | '@apply h-40': {}, 39 | }, 40 | '&:is(input[type="checkbox"])': { 41 | '@apply size-20 border-0 text-blue-500 checked:bg-blue-500 min-h-0': 42 | {}, 43 | }, 44 | '&:is([aria-invalid="true"])': { 45 | '@apply ring-red-600 hover:ring-red-700 focus:ring-red-600 dark:ring-red-300 dark:hover:ring-red-400 dark:focus:ring-red-300': 46 | {}, 47 | }, 48 | }, 49 | // field specific styles 50 | '& textarea': { 51 | '@apply py-8': {}, 52 | }, 53 | '& select': { 54 | backgroundImage: `url("data:image/svg+xml, %3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23000000' d='M12 5.83 15.17 9l1.41-1.41L12 3 7.41 7.59 8.83 9 12 5.83zm0 12.34L8.83 15l-1.41 1.41L12 21l4.59-4.59L15.17 15 12 18.17z'/%3E%3C/svg%3E")`, 55 | backgroundPosition: 'right .5rem center', 56 | backgroundSize: '1rem', 57 | backgroundRepeat: 'no-repeat', 58 | '@apply appearance-none pr-32': {}, 59 | 60 | '.dark &': { 61 | backgroundImage: `url("data:image/svg+xml, %3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23ffffff' d='M12 5.83 15.17 9l1.41-1.41L12 3 7.41 7.59 8.83 9 12 5.83zm0 12.34L8.83 15l-1.41 1.41L12 21l4.59-4.59L15.17 15 12 18.17z'/%3E%3C/svg%3E")`, 62 | }, 63 | '& option': { 64 | '@apply text-black': {}, 65 | }, 66 | }, 67 | // required field styles 68 | '&[data-required] label .field-label': { 69 | '&::after': { 70 | '@apply content-["*"] text-red-600 align-super text-xs ml-2 font-medium': 71 | {}, 72 | }, 73 | '.dark &::after': { 74 | '@apply text-red-300': {}, 75 | }, 76 | }, 77 | '&[data-required="long"] label .field-label::after': { 78 | '@apply content-["*Required"]': {}, 79 | }, 80 | // error message styles 81 | '.field-errors': { 82 | '@apply relative text-red-600 text-sm font-medium flex gap-10 flex-col pl-20 list-none dark:text-red-300': 83 | {}, 84 | }, 85 | }, 86 | } 87 | 88 | addComponents(inputs) 89 | }) 90 | -------------------------------------------------------------------------------- /config/tailwind/rich-text.js: -------------------------------------------------------------------------------- 1 | import plugin from 'tailwindcss/plugin' 2 | 3 | export default plugin(({ addComponents }) => { 4 | const richText = { 5 | '.rich-text': { 6 | '& strong, & b': { 7 | '@apply font-semibold': {}, 8 | }, 9 | '& p': { 10 | '@apply mb-16': {}, 11 | }, 12 | '& a': { 13 | '@apply text-sky-600 underline': {}, 14 | }, 15 | '& ol, & ul': { 16 | '@apply mt-20 mb-20 pl-20': {}, 17 | }, 18 | '& ol': { 19 | '@apply list-decimal': {}, 20 | }, 21 | '& ul': { 22 | '@apply list-disc': {}, 23 | }, 24 | '& li, & li > ol, & li > ul': { 25 | '@apply mt-8 mb-8': {}, 26 | }, 27 | '& blockquote': { 28 | '@apply italic text-lg border-l-2 border-sky-600 pl-16': {}, 29 | }, 30 | // Separate rich text classes let us adjust sizes without migrating existing content 31 | '& .rich-text-size-xlarge': { 32 | '@apply text-2xl': {}, 33 | }, 34 | '& .rich-text-size-large': { 35 | '@apply text-xl': {}, 36 | }, 37 | '& .rich-text-size-medium': { 38 | '@apply text-lg': {}, 39 | }, 40 | '& .rich-text-size-small': { 41 | '@apply text-sm': {}, 42 | }, 43 | }, 44 | } 45 | 46 | addComponents(richText) 47 | }) 48 | -------------------------------------------------------------------------------- /config/vite.php: -------------------------------------------------------------------------------- 1 | preg_replace('/:\d+$/', '', $host) . ':' . $httpsPort, 14 | 'serverPublic' => '/dist/', 15 | 'useDevServer' => App::env('CRAFT_ENVIRONMENT') === 'dev', 16 | 'manifestPath' => '@webroot/dist/.vite/manifest.json', 17 | ]; 18 | -------------------------------------------------------------------------------- /craft: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | run(); 14 | exit($exitCode); 15 | -------------------------------------------------------------------------------- /docs/block-call-to-action-admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigetlabs/craft-site-starter/9d8de11625fe21cb7ce5373a61b0749fc21e57a7/docs/block-call-to-action-admin.png -------------------------------------------------------------------------------- /docs/block-call-to-action-frontend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigetlabs/craft-site-starter/9d8de11625fe21cb7ce5373a61b0749fc21e57a7/docs/block-call-to-action-frontend.png -------------------------------------------------------------------------------- /docs/block-card-grid-admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigetlabs/craft-site-starter/9d8de11625fe21cb7ce5373a61b0749fc21e57a7/docs/block-card-grid-admin.png -------------------------------------------------------------------------------- /docs/block-card-grid-frontend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigetlabs/craft-site-starter/9d8de11625fe21cb7ce5373a61b0749fc21e57a7/docs/block-card-grid-frontend.png -------------------------------------------------------------------------------- /docs/block-heading-admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigetlabs/craft-site-starter/9d8de11625fe21cb7ce5373a61b0749fc21e57a7/docs/block-heading-admin.png -------------------------------------------------------------------------------- /docs/block-heading-frontend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigetlabs/craft-site-starter/9d8de11625fe21cb7ce5373a61b0749fc21e57a7/docs/block-heading-frontend.png -------------------------------------------------------------------------------- /docs/block-page-hero-admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigetlabs/craft-site-starter/9d8de11625fe21cb7ce5373a61b0749fc21e57a7/docs/block-page-hero-admin.png -------------------------------------------------------------------------------- /docs/block-page-hero-frontend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigetlabs/craft-site-starter/9d8de11625fe21cb7ce5373a61b0749fc21e57a7/docs/block-page-hero-frontend.png -------------------------------------------------------------------------------- /docs/block-rich-text-admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigetlabs/craft-site-starter/9d8de11625fe21cb7ce5373a61b0749fc21e57a7/docs/block-rich-text-admin.png -------------------------------------------------------------------------------- /docs/block-rich-text-frontend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigetlabs/craft-site-starter/9d8de11625fe21cb7ce5373a61b0749fc21e57a7/docs/block-rich-text-frontend.png -------------------------------------------------------------------------------- /docs/block-text-with-media-image-admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigetlabs/craft-site-starter/9d8de11625fe21cb7ce5373a61b0749fc21e57a7/docs/block-text-with-media-image-admin.png -------------------------------------------------------------------------------- /docs/block-text-with-media-image-frontend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigetlabs/craft-site-starter/9d8de11625fe21cb7ce5373a61b0749fc21e57a7/docs/block-text-with-media-image-frontend.png -------------------------------------------------------------------------------- /docs/block-text-with-media-video-admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigetlabs/craft-site-starter/9d8de11625fe21cb7ce5373a61b0749fc21e57a7/docs/block-text-with-media-video-admin.png -------------------------------------------------------------------------------- /docs/block-text-with-media-video-frontend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigetlabs/craft-site-starter/9d8de11625fe21cb7ce5373a61b0749fc21e57a7/docs/block-text-with-media-video-frontend.png -------------------------------------------------------------------------------- /docs/building-with-site-starter.md: -------------------------------------------------------------------------------- 1 | # Building with Craft Site Starter 2 | 3 | ## Overview 4 | 5 | ![alt text](site-starter-ecosystem.excalidraw.png) 6 | 7 | - Lives withing the broader ecosystem of Blueprint and WordPress Site Starter. 8 | - Blueprint is the common design and naming system across both Site Starter projects. 9 | - We aren't trying to have 100% parity with WordPress Site Starter. 10 | - Craft Site Starter leans into the strengths of Craft CMS and the Craft ecosystem. 11 | - Craft is more of a "blank slate" compared to WordPress. 12 | - However, the starter contains Blueprint Components, a feature rich & fully accessible Header and Footer, and the beginnings of a simple Matrix Field based block editor. 13 | 14 | ## For Design & UX 15 | 16 | ### Blueprint 17 | 18 | The Craft Site Starter's components library is based on Blueprint. 19 | 20 | [🔗 Blueprint Github repo](https://github.com/vigetlabs/blueprint) 21 | 22 | [🔗 Live Blueprint docs](https://staging.blueprint.viget.com/) 23 | 24 | - A component library that serves as a guide for both design and development. 25 | - It's meant to be customized, modified and adapted for projects. 26 | - Just because a component exists in Blueprint doesn't mean it's the only way to do something. However, there is added cost the further we venture outside the boundaries of Blueprint. 27 | - One of the biggest benefits of Blueprint is a shared example to use for design and development conversations. 28 | - Exampes: 29 | - "Instead of button style 'Outlined" we're namning it 'Secondary" and changing it in the following ways." 30 | - "We modified the card styles for this project, but it's still about 80% the same as the Blueprint card styles." 31 | - "We're heavily customizing the hero styles for this project, but components X, Y, and Z aren't changing at all." 32 | 33 | > See a common design pattern that's missing from Blueprint? [🔗 Open an issue](https://github.com/vigetlabs/blueprint/issues/new) 34 | 35 | ### Header, Navigation, & Footer 36 | 37 | There is an example of a fully accessible header and navigation built for the Craft Site Starter. 38 | 39 | Style & layout can be modified a good bit without breaking the core interactivity and accessibility. 40 | 41 | ### Block Editor 42 | 43 | The block editor uses Craft's Matrix field and contains the following Blueprint blocks: 44 | 45 | #### Page Hero 46 | 47 | | Admin | Frontend | 48 | | -------------------------------------- | ----------------------------------------- | 49 | | ![alt text](block-page-hero-admin.png) | ![alt text](block-page-hero-frontend.png) | 50 | 51 | #### Call to Action 52 | 53 | | Admin | Frontend | 54 | | ------------------------------------------- | ---------------------------------------------- | 55 | | ![alt text](block-call-to-action-admin.png) | ![alt text](block-call-to-action-frontend.png) | 56 | 57 | #### Card Grid 58 | 59 | | Admin | Frontend | 60 | | -------------------------------------- | ----------------------------------------- | 61 | | ![alt text](block-card-grid-admin.png) | ![alt text](block-card-grid-frontend.png) | 62 | 63 | #### Heading 64 | 65 | | Admin | Frontend | 66 | | ------------------------------------ | --------------------------------------- | 67 | | ![alt text](block-heading-admin.png) | ![alt text](block-heading-frontend.png) | 68 | 69 | #### Rich Text 70 | 71 | | Admin | Frontend | 72 | | -------------------------------------- | ----------------------------------------- | 73 | | ![alt text](block-rich-text-admin.png) | ![alt text](block-rich-text-frontend.png) | 74 | 75 | #### Text with Media (Image) 76 | 77 | | Admin | Frontend | 78 | | -------------------------------------------------- | ----------------------------------------------------- | 79 | | ![alt text](block-text-with-media-image-admin.png) | ![alt text](block-text-with-media-image-frontend.png) | 80 | 81 | #### Text with Media (Video) 82 | 83 | | Admin | Frontend | 84 | | -------------------------------------------------- | ----------------------------------------------------- | 85 | | ![alt text](block-text-with-media-video-admin.png) | ![alt text](block-text-with-media-video-frontend.png) | 86 | 87 | - Unlike WordPress, the entire site won't be built with a "Gutenberg" like editor. 88 | - For some projects, budget can be reduced by planning for the majority of pages using the Matrix field block editor. 89 | - However, a large project would likely take a hybrid approach and use the block editor for some sections and Craft's typical custom field setup for others. 90 | 91 | ## For Developers (Platform & UI) 92 | 93 | - All components and Tailwind plugins are meant to be modified directly. 94 | - There is a pre-configured parts kit for building and testing components in isolation. 95 | - These components provide a solid starting point and consistent structure for creating new components. 96 | - Craft comes with commonly used plugins pre-installed and configured. 97 | - Build tooling such as Vite, Tailwind and Alpine.js are pre-configured. 98 | - Code quality and formatting tools such as Prettier, PHPStan and PHPECS run on every commit. 99 | -------------------------------------------------------------------------------- /docs/project-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigetlabs/craft-site-starter/9d8de11625fe21cb7ce5373a61b0749fc21e57a7/docs/project-logo.png -------------------------------------------------------------------------------- /docs/release-qa-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigetlabs/craft-site-starter/9d8de11625fe21cb7ce5373a61b0749fc21e57a7/docs/release-qa-1.png -------------------------------------------------------------------------------- /docs/release-qa-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigetlabs/craft-site-starter/9d8de11625fe21cb7ce5373a61b0749fc21e57a7/docs/release-qa-2.png -------------------------------------------------------------------------------- /docs/site-starter-ecosystem.excalidraw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigetlabs/craft-site-starter/9d8de11625fe21cb7ce5373a61b0749fc21e57a7/docs/site-starter-ecosystem.excalidraw.png -------------------------------------------------------------------------------- /ecs.php: -------------------------------------------------------------------------------- 1 | parallel(); 10 | $ecsConfig->paths([ 11 | __DIR__ . '/modules', 12 | __DIR__ . '/install-scripts', 13 | __DIR__ . '/bootstrap.php', 14 | __DIR__ . '/web/index.php', 15 | __FILE__, 16 | ]); 17 | 18 | // TODO waiting on this PR or similar https://github.com/craftcms/ecs/pull/4 19 | $ecsConfig->sets([SetList::CRAFT_CMS_4]); // for Craft 4 projects 20 | }; 21 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import eslintConfigPrettier from 'eslint-config-prettier' 2 | import js from '@eslint/js' 3 | import globals from 'globals' 4 | 5 | export default [ 6 | js.configs.recommended, 7 | eslintConfigPrettier, 8 | 9 | { 10 | rules: { 11 | 'no-console': ['error', { allow: ['warn', 'error'] }], 12 | }, 13 | languageOptions: { 14 | globals: { 15 | ...globals.browser, 16 | }, 17 | }, 18 | }, 19 | ] 20 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This runs within the DDEV container 3 | * We have access to the appropriate PHP & Composer versions 4 | */ 5 | export default { 6 | '**/*.php': [ 7 | './vendor/bin/ecs check --ansi --fix', 8 | './vendor/bin/phpstan analyse --memory-limit=1G', 9 | ], 10 | '*.{js,jsx,ts,tsx}': ['eslint --fix', 'prettier --ignore-unknown --write'], 11 | // Negate the JS files above so there isn't a race condition 12 | // This runs prettier on every type of file other than the extensions listed below 13 | // https://github.com/lint-staged/lint-staged?tab=readme-ov-file#task-concurrency 14 | '!(*.js|*.jsx|*.ts|*.tsx)': 'prettier --ignore-unknown --write', 15 | } 16 | -------------------------------------------------------------------------------- /modules/todo.php: -------------------------------------------------------------------------------- 1 | ", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "lint": "eslint --fix src", 11 | "format": "prettier --write .", 12 | "prepare": "husky" 13 | }, 14 | "devDependencies": { 15 | "@eslint/js": "^9.21.0", 16 | "@tailwindcss/postcss": "^4.0.0", 17 | "cssnano": "^7.0.6", 18 | "eslint-config-prettier": "^10.0.2", 19 | "globals": "^16.0.0", 20 | "husky": "^9.1.7", 21 | "lint-staged": "^15.4.3", 22 | "postcss": "^8.5.3", 23 | "postcss-pxtorem": "^6.1.0", 24 | "prettier": "^3.5.2", 25 | "tailwindcss": "^4.0.0", 26 | "vite": "^6.3.5", 27 | "vite-plugin-static-copy": "^2.2.0" 28 | }, 29 | "dependencies": { 30 | "@alpinejs/collapse": "^3.14.1", 31 | "@alpinejs/focus": "^3.14.1", 32 | "alpinejs": "^3.14.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - vendor/craftcms/phpstan/phpstan.neon 3 | 4 | parameters: 5 | level: 5 6 | paths: 7 | - composer-scripts 8 | - modules 9 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | import process from 'process' 2 | 3 | export default { 4 | plugins: { 5 | '@tailwindcss/postcss': {}, 6 | 'postcss-pxtorem': { 7 | rootValue: 16, 8 | unitPrecision: 5, 9 | propList: ['*'], 10 | selectorBlackList: [], 11 | replace: true, 12 | mediaQuery: true, 13 | minPixelValue: 1, 14 | exclude: /node_modules/i, 15 | }, 16 | ...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {}), 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /src/css/app.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | 3 | /* Import plugins */ 4 | @plugin '../../config/tailwind/buttons.js'; 5 | @plugin '../../config/tailwind/dialog.js'; 6 | @plugin '../../config/tailwind/rich-text.js'; 7 | @plugin '../../config/tailwind/forms.js'; 8 | 9 | /* Configure Tailwind */ 10 | @theme { 11 | /* Custom Spacing */ 12 | --spacing-*: initial; /* Resets all spacing config */ 13 | --spacing-0: 0px; 14 | --spacing-1: 1px; 15 | --spacing-2: 2px; 16 | --spacing-4: 4px; 17 | --spacing-6: 6px; 18 | --spacing-8: 8px; 19 | --spacing-10: 10px; 20 | --spacing-12: 12px; 21 | --spacing-16: 16px; 22 | --spacing-20: 20px; 23 | --spacing-24: 24px; 24 | --spacing-28: 28px; 25 | --spacing-32: 32px; 26 | --spacing-36: 36px; 27 | --spacing-40: 40px; 28 | --spacing-44: 44px; 29 | --spacing-48: 48px; 30 | --spacing-52: 52px; 31 | --spacing-56: 56px; 32 | --spacing-60: 60px; 33 | --spacing-64: 64px; 34 | --spacing-80: 80px; 35 | --spacing-96: 96px; 36 | --spacing-112: 112px; 37 | --spacing-128: 128px; 38 | 39 | /* Add named z-index values */ 40 | --z-index-header: 100; 41 | --z-index-max: 9999; 42 | } 43 | 44 | /* Use selector based dark mode */ 45 | @custom-variant dark (&:where(.dark, .dark *)); 46 | 47 | /* Override the default Tailwind container class */ 48 | @utility container { 49 | /* Override the different max-widths at different breakpoints */ 50 | @apply max-w-[1240px]; 51 | /** Apply padding to the left and right and automatically center the container */ 52 | @apply mx-auto px-16 sm:px-32; 53 | } 54 | 55 | /* Base styles */ 56 | @layer base { 57 | @font-face { 58 | font-display: swap; 59 | font-family: 'Source Sans Pro'; 60 | font-style: normal; 61 | font-weight: 300; 62 | src: url('../fonts/sourcesanspro-light-webfont.woff2') format('woff2'); 63 | } 64 | 65 | @font-face { 66 | font-display: swap; 67 | font-family: 'Source Sans Pro'; 68 | font-style: normal; 69 | font-weight: 400; 70 | src: url('../fonts/sourcesanspro-regular-webfont.woff2') format('woff2'); 71 | } 72 | 73 | @font-face { 74 | font-display: swap; 75 | font-family: 'Source Sans Pro'; 76 | font-style: normal; 77 | font-weight: 600; 78 | src: url('../fonts/sourcesanspro-semibold-webfont.woff2') format('woff2'); 79 | } 80 | 81 | @font-face { 82 | font-display: swap; 83 | font-family: 'Source Sans Pro'; 84 | font-style: normal; 85 | font-weight: 700; 86 | src: url('../fonts/sourcesanspro-bold-webfont.woff2') format('woff2'); 87 | } 88 | 89 | body { 90 | font-family: 'Source Sans Pro', sans-serif; 91 | } 92 | 93 | strong { 94 | font-weight: 700; 95 | } 96 | 97 | [x-cloak] { 98 | display: none !important; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/fonts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigetlabs/craft-site-starter/9d8de11625fe21cb7ce5373a61b0749fc21e57a7/src/fonts/.gitkeep -------------------------------------------------------------------------------- /src/fonts/sourcesanspro-bold-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigetlabs/craft-site-starter/9d8de11625fe21cb7ce5373a61b0749fc21e57a7/src/fonts/sourcesanspro-bold-webfont.woff2 -------------------------------------------------------------------------------- /src/fonts/sourcesanspro-light-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigetlabs/craft-site-starter/9d8de11625fe21cb7ce5373a61b0749fc21e57a7/src/fonts/sourcesanspro-light-webfont.woff2 -------------------------------------------------------------------------------- /src/fonts/sourcesanspro-regular-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigetlabs/craft-site-starter/9d8de11625fe21cb7ce5373a61b0749fc21e57a7/src/fonts/sourcesanspro-regular-webfont.woff2 -------------------------------------------------------------------------------- /src/fonts/sourcesanspro-semibold-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigetlabs/craft-site-starter/9d8de11625fe21cb7ce5373a61b0749fc21e57a7/src/fonts/sourcesanspro-semibold-webfont.woff2 -------------------------------------------------------------------------------- /src/icons/arrow-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/arrow-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/chevron-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icons/clock.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/error.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/icons/logo-mark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/js/alpine/nav.js: -------------------------------------------------------------------------------- 1 | export default () => ({ 2 | /** @type {string[]} */ 3 | stack: [], 4 | popStack(popTimes = 1, focus = true) { 5 | const idToFocus = this.stack.at(-1 * popTimes) 6 | 7 | if (idToFocus && focus) { 8 | const elementToFocus = document.querySelector( 9 | `[aria-controls="${idToFocus}"]`, 10 | ) 11 | this.$focus.focus(elementToFocus) 12 | } 13 | 14 | this.stack = this.stack.slice(0, popTimes * -1) 15 | }, 16 | pushStack(id) { 17 | this.stack.push(id) 18 | 19 | const elementToFocus = document.getElementById(id) 20 | this.$focus.within(elementToFocus).first() 21 | }, 22 | resetStack() { 23 | this.stack = [] 24 | }, 25 | toggle(subnav) { 26 | if (!subnav) { 27 | this.resetStack() 28 | return 29 | } 30 | 31 | const level = parseInt(subnav.dataset.level, 10) 32 | const currentLevel = this.stack.length 33 | const shouldPush = !this.isInStack(subnav) 34 | 35 | if (level <= currentLevel) { 36 | // Remove everything down to the level you clicked. 37 | this.popStack(currentLevel - level + 1) 38 | } 39 | // If you clicked a toggle outside of the current stack, push it to the stack 40 | if (shouldPush) { 41 | this.pushStack(subnav.id) 42 | } 43 | }, 44 | shouldTrap(el) { 45 | // You could modify this to only trap at a mobile sized match media 46 | return this.isInStack(el) 47 | }, 48 | isInStack(el) { 49 | return this.stack.includes(el.id) 50 | }, 51 | /** 52 | * @param {KeyboardEvent} event 53 | */ 54 | handleEscape(event) { 55 | if (this.stack.length === 0) { 56 | return 57 | } 58 | // Prevent the event from bubbling up if we have nav to pop 59 | event.stopPropagation() 60 | 61 | this.popStack(1) 62 | }, 63 | handleTab() { 64 | const activeSubnavId = this.stack.at(-1) 65 | 66 | if (!activeSubnavId) { 67 | return 68 | } 69 | 70 | const activeSubnav = document.getElementById(activeSubnavId) 71 | 72 | if (!activeSubnav) { 73 | return 74 | } 75 | 76 | this.$nextTick(() => { 77 | const activeElement = document.activeElement 78 | 79 | // If document.activeElement is within activeSubnav, do nothing 80 | if (!activeElement || activeSubnav.contains(activeElement)) { 81 | return 82 | } 83 | 84 | // If document.activeElement is not within $root, reset the stack 85 | if (!this.$root.contains(activeElement)) { 86 | this.resetStack() 87 | return 88 | } 89 | 90 | // Focused toggle 91 | const idOfFocusedSubnav = activeElement.getAttribute('aria-controls') 92 | const focusedSubnavLevel = idOfFocusedSubnav 93 | ? document.getElementById(idOfFocusedSubnav)?.dataset.level 94 | : null 95 | 96 | // Edge case, we're still in the nav component, but somehow what we're focused on doesn't have subnav or level 97 | if (!focusedSubnavLevel) { 98 | this.popStack(1, false) 99 | return 100 | } 101 | 102 | // Pop everything down to the level of the focused subnav 103 | const currentLevel = this.stack.length 104 | this.popStack(currentLevel - parseInt(focusedSubnavLevel, 10) + 1, false) 105 | }) 106 | }, 107 | }) 108 | -------------------------------------------------------------------------------- /src/js/app.js: -------------------------------------------------------------------------------- 1 | import '../css/app.css' 2 | 3 | import Alpine from 'alpinejs' 4 | import focus from '@alpinejs/focus' 5 | import collapse from '@alpinejs/collapse' 6 | import nav from './alpine/nav' 7 | 8 | Alpine.plugin(focus) 9 | Alpine.plugin(collapse) 10 | 11 | // @ts-ignore 12 | window.Alpine = Alpine 13 | 14 | Alpine.data('nav', nav) 15 | Alpine.start() 16 | -------------------------------------------------------------------------------- /storage/.gitignore: -------------------------------------------------------------------------------- 1 | backups 2 | composer-backups 3 | config-backups 4 | logs 5 | runtime 6 | -------------------------------------------------------------------------------- /storage/config-deltas/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /templates/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vigetlabs/craft-site-starter/9d8de11625fe21cb7ce5373a61b0749fc21e57a7/templates/.gitkeep -------------------------------------------------------------------------------- /templates/_blocks/_callToAction.twig: -------------------------------------------------------------------------------- 1 | {# @var \craft\elements\Entry|null block #} 2 | {% from '_components/call-to-action' import CallToAction %} 3 | 4 | {% set block = block ?? null %} 5 | 6 | {{ CallToAction({ 7 | title: block.title, 8 | text: block.richTextSimple, 9 | href: block.button.value, 10 | buttonText: block.button.label ?? null, 11 | newTab: block.button.target ?? null, 12 | }) }} 13 | -------------------------------------------------------------------------------- /templates/_blocks/_cardGrid.twig: -------------------------------------------------------------------------------- 1 | {# @var \craft\elements\Entry block #} 2 | {% from '_components/card-image' import CardImage %} 3 | {% from '_components/card-icon' import CardIcon %} 4 | 5 | {% set block = block ?? null %} 6 | 7 | {% set cardHeadingLevel = block.heading is not empty ? 3 : 2 %} 8 | 9 |
10 | {% if block.heading %} 11 |

12 | {{ block.heading }} 13 |

14 | {% endif %} 15 |
16 | {% for card in block.cardGrid.eagerly().collect() %} 17 | {% switch card.type.handle %} 18 | {% case 'imageCard' %} 19 | {{ CardImage({ 20 | image: card.image.eagerly().one(), 21 | sizes: { 22 | default: '100vw', 23 | sm: '50vw', 24 | md: '33vw', 25 | lg: '25vw', 26 | }, 27 | href: card.basicLink, 28 | newTab: card.basicLink.target ?? null, 29 | title: card.title, 30 | description: card.description, 31 | headingLevel: cardHeadingLevel, 32 | }) }} 33 | {% case 'iconCard' %} 34 | {{ CardIcon({ 35 | href: card.basicLink, 36 | newTab: card.basicLink.target ?? null, 37 | icon: card.icon.value, 38 | title: card.title, 39 | description: card.description, 40 | headingLevel: cardHeadingLevel, 41 | }) }} 42 | {% endswitch %} 43 | {% endfor %} 44 |
45 |
46 | -------------------------------------------------------------------------------- /templates/_blocks/_heading.twig: -------------------------------------------------------------------------------- 1 | {# @var \craft\elements\Entry|null block #} 2 | {% set block = block ?? null %} 3 | 4 | {# @var bool hasPreviousBlocksWithH1 #} 5 | {% set hasPreviousBlocksWithH1 = hasPreviousBlocksWithH1 ?? false %} 6 | 7 | {% set element = hasPreviousBlocksWithH1 ? 'h2' : 'h1' %} 8 | 9 | {% tag element with { 10 | class: cx('container font-bold', { 11 | 'text-4xl': not hasPreviousBlocksWithH1, 12 | 'text-3xl': hasPreviousBlocksWithH1, 13 | }), 14 | } %} 15 | {{ block.heading }} 16 | {% endtag %} 17 | -------------------------------------------------------------------------------- /templates/_blocks/_pageHero.twig: -------------------------------------------------------------------------------- 1 | {# @var \craft\elements\Entry|null block #} 2 | {% from '_components/page-hero' import PageHero %} 3 | 4 | {% set block = block ?? null %} 5 | 6 | {# @var bool hasPreviousBlocksWithH1 #} 7 | {% set hasPreviousBlocksWithH1 = hasPreviousBlocksWithH1 ?? false %} 8 | 9 | {{ PageHero({ 10 | image: block.image.eagerly().one(), 11 | title: block.title, 12 | description: block.richTextSimple, 13 | headingLevel: hasPreviousBlocksWithH1 ? 2 : 1, 14 | }) }} 15 | -------------------------------------------------------------------------------- /templates/_blocks/_richText.twig: -------------------------------------------------------------------------------- 1 | {# @var \craft\elements\Entry block #} 2 | {% set block = block ?? null %} 3 | 4 |
5 | {{ block.richText }} 6 |
7 | -------------------------------------------------------------------------------- /templates/_blocks/_textWithMedia.twig: -------------------------------------------------------------------------------- 1 | {% from '_components/image' import Image %} 2 | {% from '_components/video' import Video %} 3 | 4 | {# @var \craft\elements\Entry block #} 5 | {% set block = block ?? null %} 6 | 7 | {% set textContent %} 8 |
9 | {% if block.heading %} 10 |

{{ block.heading }}

11 | {% endif %} 12 | 13 | {% if block.richText %} 14 |
15 | {{ block.richText }} 16 |
17 | {% endif %} 18 |
19 | {% endset %} 20 | 21 | {% set mediaContent %} 22 |
23 | {% switch block.mediaType %} 24 | {% case 'image' %} 25 | {{ Image({ 26 | image: block.image.eagerly().one(), 27 | sizes: { 28 | default: '100vw', 29 | sm: '50vw', 30 | }, 31 | }) }} 32 | {% case 'video' %} 33 | {{ Video({ 34 | source: block.videoUrl, 35 | }) }} 36 | {% endswitch %} 37 |
38 | {% endset %} 39 | 40 |
41 | {# Resolves any tab ordering issues #} 42 | {% switch block.side %} 43 | {% case 'left' %} 44 | {{ mediaContent }} 45 | {{ textContent }} 46 | {% case 'right' %} 47 | {{ textContent }} 48 | {{ mediaContent }} 49 | {% endswitch %} 50 |
51 | -------------------------------------------------------------------------------- /templates/_blocks/index.twig: -------------------------------------------------------------------------------- 1 | {# @var \Illuminate\Support\Collection<\craft\elements\Entry> blocks #} 2 | {% set blocks = blocks is defined ? blocks.collect() : collect([]) %} 3 | 4 | {% if blocks.count() %} 5 |
9 | {% for block in blocks %} 10 | {% set nextBlock = blocks.get(loop.index0 + 1) %} 11 | {% set previousBlocks = blocks.take(loop.index0) %} 12 | 13 | {# 14 | Check if there are previous blocks that have a heading level of 1. This is used 15 | to determine if the current block should use an h1 or h2 as its root heading level. 16 | #} 17 | {% set hasPreviousBlocksWithH1 = previousBlocks 18 | |map(block => block.type.handle) 19 | |intersect(['pageHero', 'heading']) 20 | |length 21 | %} 22 | 23 | {% set marginBottomConfig = { 24 | default: 'mb-64', 25 | 'heading + richText': 'mb-32', 26 | } %} 27 | 28 | {# 29 | Pull margins from the config. The order of the array is important. 30 | 31 | First we check the most unique case, the current block combined with the next block. 32 | 33 | Such as heading + richText. Those cases are typically where we deviate from the default margin. 34 | 35 | Next we check just the current block handle, which is useful for when we have a 36 | unique case for a specific block type. Since we first check for currentBlock + nextBlock, 37 | those cases will win out over the more generic ones. 38 | 39 | Lastly we fall back to the default margin. 40 | 41 | We use the `|filter` to remove any null values. and then `|first` 42 | to get the first matching config value. 43 | #} 44 | {% set marginBottom = [ 45 | nextBlock ? "#{block.type.handle} + #{nextBlock.type.handle}" : null, 46 | block.type.handle, 47 | 'default', 48 | ] 49 | |map(item => marginBottomConfig[item] ?? null) 50 | |filter 51 | |first 52 | %} 53 | 54 | {# Block wrapper #} 55 |
58 | {{ include("_blocks/_#{block.type.handle}.twig", { 59 | block, 60 | hasPreviousBlocksWithH1, 61 | }) }} 62 |
63 | {% endfor %} 64 |
65 | {% endif %} 66 | -------------------------------------------------------------------------------- /templates/_components/accordion.twig: -------------------------------------------------------------------------------- 1 | {% macro Accordion(props = {}) %} 2 | {# Props #} 3 | 4 | {# @var string|null title #} 5 | {% set title = props.title ?? null %} 6 | 7 | {# @var string|null content #} 8 | {% set content = props.content ?? null %} 9 | 10 | {# @var string|null name #} 11 | {% set name = props.name ?? null %} 12 | 13 | {# @var string|null class #} 14 | {% set class = props.class ?? null %} 15 | 16 | {# @var int headingLevel #} 17 | {% set headingLevel = props.headingLevel ?? 2 %} 18 | 19 | {# Computed values #} 20 | 21 | {% set headingTag = 'h' ~ headingLevel %} 22 | 23 |
29 | 32 | {{ tag(headingTag, { 33 | class: 'text-lg font-bold', 34 | text: title, 35 | }) }} 36 | 37 | {# Animated toggle icon #} 38 | 41 | 44 | 47 | 48 | 49 | 50 |
51 | {{ content }} 52 |
53 |
54 | {% endmacro %} 55 | -------------------------------------------------------------------------------- /templates/_components/alert-banner.twig: -------------------------------------------------------------------------------- 1 | {% macro AlertBanner(props = {}) %} 2 | {% from '_components/button' import Button %} 3 | {# Props #} 4 | 5 | {# @var string title #} 6 | {% set title = props.title ?? null %} 7 | 8 | {# @var string description #} 9 | {% set description = props.description ?? null %} 10 | 11 | {# @var bool dismissible #} 12 | {% set dismissible = props.dismissible ?? false %} 13 | 14 | {# @var string|null class #} 15 | {% set class = props.class ?? null %} 16 | 17 | 45 | {% endmacro %} 46 | -------------------------------------------------------------------------------- /templates/_components/button.twig: -------------------------------------------------------------------------------- 1 | {% macro Button(props = {}) %} 2 | {% from '_components/icon.twig' import Icon %} 3 | {# Props #} 4 | 5 | {# @var string|null as #} 6 | {% set as = props.as ?? null %} 7 | 8 | {# @var string variant #} 9 | {% set variant = props.variant ?? 'contained' %} 10 | 11 | {# @var string size #} 12 | {% set size = props.size ?? 'md' %} 13 | 14 | {# @var string|null title #} 15 | {% set title = props.title ?? null %} 16 | 17 | {# @var string|null icon The name of the icon to use #} 18 | {% set icon = props.icon ?? null %} 19 | 20 | {# @var bool iconLast #} 21 | {% set iconLast = props.iconLast ?? false %} 22 | 23 | {# @var boolean iconOnly #} 24 | {% set iconOnly = props.iconOnly ?? false %} 25 | 26 | {# @var string|null href #} 27 | {% set href = props.href ?? null %} 28 | 29 | {# @var boolean newTab #} 30 | {% set newTab = props.newTab ?? false %} 31 | 32 | {# @var string type #} 33 | {% set type = props.type ?? 'button' %} 34 | 35 | {# @var boolean disabled #} 36 | {% set disabled = props.disabled ?? false %} 37 | 38 | {# @var string|null class #} 39 | {% set class = props.class ?? null %} 40 | 41 | {# @var string|null text #} 42 | {% set text = props.text ?? null %} 43 | 44 | {# Determine element type #} 45 | {% set el = as ?? (href and not disabled ? 'a' : 'button') %} 46 | 47 | {# @var array attrs #} 48 | {% set attrs = props.attrs ?? {} %} 49 | 50 | {% tag el with attrs | merge({ 51 | href: not disabled ? href : null, 52 | target: newTab ? '_blank' : null, 53 | disabled: disabled ? true : null, 54 | type: el == 'button' ? type : null, 55 | title: title, 56 | class: classNames({ 57 | 'btn-contained': variant == 'contained', 58 | 'btn-outlined': variant == 'outlined', 59 | 'btn-subtle': variant == 'subtle', 60 | 'btn-text': variant == 'text', 61 | 'btn-icon': iconOnly, 62 | 'btn-sm': size == 'sm', 63 | 'btn-lg': size == 'lg', 64 | }, class), 65 | }) %} 66 | {{ icon ? Icon({ 67 | name: icon, 68 | size: size, 69 | class: iconLast ? 'order-last' : null, 70 | }) }} 71 | 72 | {{ text }} 73 | {% endtag %} 74 | {% endmacro %} 75 | -------------------------------------------------------------------------------- /templates/_components/call-to-action.twig: -------------------------------------------------------------------------------- 1 | {% macro CallToAction(props = {}) %} 2 | {% from '_components/button' import Button %} 3 | 4 | {# Props #} 5 | 6 | {# @var string|null title #} 7 | {% set title = props.title ?? null %} 8 | 9 | {# @var string|null text #} 10 | {% set text = props.text ?? null %} 11 | 12 | {# @var string|null href #} 13 | {% set href = props.href ?? null %} 14 | 15 | {# @var string|null buttonText #} 16 | {% set buttonText = props.buttonText ?? null %} 17 | 18 | {# @var boolean|null newTab #} 19 | {% set newTab = props.newTab ?? null %} 20 | 21 | {# @var string|null class #} 22 | {% set class = props.class ?? null %} 23 | 24 |
25 |
26 | {% if title %} 27 |

{{ title }}

28 | {% endif %} 29 | 30 | {% if text %} 31 |
{{ text }}
32 | {% endif %} 33 | 34 | {% if href and buttonText %} 35 | {{ Button({ 36 | href, 37 | newTab, 38 | text: buttonText, 39 | }) }} 40 | {% endif %} 41 |
42 |
43 | {% endmacro %} 44 | -------------------------------------------------------------------------------- /templates/_components/card-icon.twig: -------------------------------------------------------------------------------- 1 | {% macro CardIcon(props = {}) %} 2 | {% from '_components/icon.twig' import Icon %} 3 | 4 | {# Props #} 5 | 6 | {# @var string|null class #} 7 | {% set class = props.class ?? null %} 8 | 9 | {# @var string|null href #} 10 | {% set href = props.href ?? null %} 11 | 12 | {# @var string|null newTab #} 13 | {% set newTab = props.newTab ?? null %} 14 | 15 | {# @var string align #} 16 | {% set align = props.align ?? 'left' %} 17 | 18 | {# @var string|null icon #} 19 | {% set icon = props.icon ?? null %} 20 | 21 | {# @var int headingLevel #} 22 | {% set headingLevel = props.headingLevel ?? 3 %} 23 | 24 | {# @var string|null title #} 25 | {% set title = props.title ?? null %} 26 | 27 | {# @var string|null description #} 28 | {% set description = props.description ?? null %} 29 | 30 | {# Computed values #} 31 | {% set element = href ? 'a' : 'article' %} 32 | {% set headingElement = "h#{headingLevel}" %} 33 | 34 | {% tag element with { 35 | class: cx(class, 'flex flex-col gap-24', { 36 | 'items-start': align == 'left', 37 | 'items-center text-center': align == 'center', 38 | }), 39 | href: href, 40 | target: href and newTab ? '_blank' : null, 41 | } %} 42 | {% if icon %} 43 |
44 | {{ Icon({ 45 | name: icon, 46 | }) }} 47 |
48 | {% endif %} 49 | 50 | {% if title or description %} 51 |
52 | {% if title %} 53 | {% tag headingElement with { 54 | class: 'text-xl font-bold', 55 | } %} 56 | {{ title }} 57 | {% endtag %} 58 | {% endif %} 59 | {% if description %} 60 |

{{ description }}

61 | {% endif %} 62 |
63 | {% endif %} 64 | {% endtag %} 65 | {% endmacro %} 66 | -------------------------------------------------------------------------------- /templates/_components/card-image.twig: -------------------------------------------------------------------------------- 1 | {% macro CardImage(props = {}) %} 2 | {% from '_components/icon.twig' import Icon %} 3 | {% from '_components/image' import Image %} 4 | 5 | {# Props #} 6 | 7 | {# @var string|null class #} 8 | {% set class = props.class ?? null %} 9 | 10 | {# @var string|null href #} 11 | {% set href = props.href ?? null %} 12 | 13 | {# @var bool newTab #} 14 | {% set newTab = props.newTab ?? false %} 15 | 16 | {# @var craft\elements\Asset|null image #} 17 | {% set image = props.image ?? null %} 18 | 19 | {# @var float|null ratio #} 20 | {% set ratio = props.ratio ?? null %} 21 | 22 | {# @var array|null sizes #} 23 | {% set sizes = props.sizes ?? null %} 24 | 25 | {# @var string align #} 26 | {% set align = props.align ?? 'left' %} 27 | 28 | {# @var int headingLevel #} 29 | {% set headingLevel = props.headingLevel ?? 3 %} 30 | 31 | {# @var string|null title #} 32 | {% set title = props.title ?? null %} 33 | 34 | {# @var string|null description #} 35 | {% set description = props.description ?? null %} 36 | 37 | {# Computed values #} 38 | {% set element = href ? 'a' : 'article' %} 39 | {% set headingElement = "h#{headingLevel}" %} 40 | 41 | {% tag element with { 42 | class: cx(class, 'flex flex-col gap-24', { 43 | 'items-start': align == 'left', 44 | 'items-center text-center': align == 'center', 45 | }), 46 | href: href, 47 | target: href and newTab ? '_blank' : null, 48 | } %} 49 | {% if image %} 50 | {{ Image({ 51 | image: image, 52 | sizes: sizes, 53 | ratio: ratio, 54 | }) }} 55 | {% endif %} 56 | 57 | {% if title or description %} 58 |
59 | {% if title %} 60 | {% tag headingElement with { 61 | class: 'text-xl font-bold', 62 | } %} 63 | {{ title }} 64 | {% endtag %} 65 | {% endif %} 66 | {% if description %} 67 |

{{ description }}

68 | {% endif %} 69 |
70 | {% endif %} 71 | {% endtag %} 72 | {% endmacro %} 73 | -------------------------------------------------------------------------------- /templates/_components/dialog.twig: -------------------------------------------------------------------------------- 1 | {% macro Dialog(props = {}) %} 2 | {% from '_components/icon.twig' import Icon %} 3 | {% from '_components/button' import Button %} 4 | 5 | {# Props #} 6 | 7 | {# @var string id #} 8 | {% set id = props.id ?? null %} 9 | 10 | {# @var boolean hideClose #} 11 | {% set hideClose = props.hideClose ?? false %} 12 | 13 | {# @var array triggerProps #} 14 | {% set triggerProps = props.triggerProps ?? {} %} 15 | 16 | {# @var string|null class #} 17 | {% set class = props.class ?? null %} 18 | 19 | {# @var string|null content #} 20 | {% set content = props.content ?? null %} 21 | 22 |
58 | 65 | {% if not hideClose %} 66 | {{ Button({ 67 | icon: 'close', 68 | iconOnly: true, 69 | size: 'sm', 70 | title: 'Close', 71 | variant: 'subtle', 72 | class: 'absolute right-4 top-4 size-24 min-h-0! [&_svg]:size-12', 73 | attrs: { 74 | '@click': 'handleDialogClose()' 75 | } 76 | }) }} 77 | {% endif %} 78 | 79 | {{ content }} 80 | 81 | 82 |
83 | {% endmacro %} 84 | -------------------------------------------------------------------------------- /templates/_components/field-group.twig: -------------------------------------------------------------------------------- 1 | {# 2 | The field group component is a wrapper for our field components. 3 | It provides consistent styling for labels, sublabels, instructions, and error messages. 4 | 5 | The {% tag fieldElement %} tag is used to render the field element. It can be 6 | configured as an input, select, textarea, or other field element. 7 | 8 | The field attrs hash is used to pass additional attributes to the field element. 9 | #} 10 | 11 | {% macro FieldGroup(props = {}) %} 12 | {% from '_components/icon.twig' import Icon %} 13 | 14 | {# @var string id #} 15 | {% set id = props.id ?? ('input-' ~ random()) %} 16 | 17 | {# @var string|null class #} 18 | {% set class = props.class ?? null %} 19 | 20 | {# @var string fieldElement - Required #} 21 | {% set fieldElement = props.fieldElement %} 22 | 23 | {# @var array fieldAttrs - Additional attributes to pass to the field element #} 24 | {% set fieldAttrs = props.fieldAttrs ?? {} %} 25 | 26 | {# @var array|null fieldChildren An array of elements to render inside the field element, used for select dropdowns #} 27 | {% set fieldChildren = props.fieldChildren ?? [] %} 28 | 29 | {# @var string|null label #} 30 | {% set label = props.label ?? null %} 31 | 32 | {# @var bool labelHidden #} 33 | {% set labelHidden = props.labelHidden ?? false %} 34 | 35 | {# @var bool|string required long is an option in addition to true #} 36 | {% set required = props.required ?? false %} 37 | 38 | {# @var string|null subLabel #} 39 | {% set subLabel = props.subLabel ?? null %} 40 | 41 | {# @var string|null instructions #} 42 | {% set instructions = props.instructions ?? null %} 43 | 44 | {# @var string[] errorMessages #} 45 | {% set errorMessages = props.errorMessages ?? [] %} 46 | 47 | {# Generated vars #} 48 | {% set idErrors = id ~ '-errors' %} 49 | {% set idInstructions = id ~ '-instructions' %} 50 | {% set hasError = errorMessages|length %} 51 | {% set dataRequired = required ? (required is same as 'long' ? 'long' : true) : null %} 52 | {% set describedByElements = [ 53 | instructions ? idInstructions, 54 | hasError ? idErrors, 55 | ]|filter|join(' ') %} 56 | 57 |
61 | 76 | 77 | {% tag fieldElement with { 78 | ...fieldAttrs, 79 | id, 80 | 'aria-required': required ? 'true' : null, 81 | 'aria-describedby': describedByElements ? describedByElements : null, 82 | 'aria-invalid': hasError ? 'true' : null, 83 | required: required is same as false ? null : '', 84 | } -%} 85 | {% for child in fieldChildren %} 86 | {{ child }} 87 | {% endfor %} 88 | {%- endtag %} 89 | 90 | {% if instructions %} 91 | 92 | {{ instructions }} 93 | 94 | {% endif %} 95 | 96 | {% if hasError %} 97 | 108 | {% endif %} 109 |
110 | {% endmacro %} 111 | -------------------------------------------------------------------------------- /templates/_components/field-select.twig: -------------------------------------------------------------------------------- 1 | {% macro FieldSelect(props = {}) %} 2 | {% from '_components/field-group' import FieldGroup %} 3 | 4 | {# Props #} 5 | 6 | {# These props mirror the props in field-group.twig #} 7 | 8 | {# @var string|null id #} 9 | {% set id = props.id ?? null %} 10 | 11 | {# @var string|null class #} 12 | {% set class = props.class ?? null %} 13 | 14 | {# @var array fieldAttrs #} 15 | {% set fieldAttrs = props.fieldAttrs ?? {} %} 16 | 17 | {# @var string|null label #} 18 | {% set label = props.label ?? null %} 19 | 20 | {# @var bool labelHidden #} 21 | {% set labelHidden = props.labelHidden ?? false %} 22 | 23 | {# @var bool required #} 24 | {% set required = props.required ?? false %} 25 | 26 | {# @var string|null subLabel #} 27 | {% set subLabel = props.subLabel ?? null %} 28 | 29 | {# @var string|null instructions #} 30 | {% set instructions = props.instructions ?? null %} 31 | 32 | {# @var string[] errorMessages #} 33 | {% set errorMessages = props.errorMessages ?? [] %} 34 | 35 | {# These props are for the select element #} 36 | 37 | {# @var string|null name #} 38 | {% set name = props.name ?? null %} 39 | 40 | {# @var string|null placeholder Only used when a blank option is present #} 41 | {% set placeholder = props.placeholder ?? null %} 42 | 43 | {# @var bool useEmptyOption #} 44 | {% set useEmptyOption = props.useEmptyOption ?? false %} 45 | 46 | {# @var bool disabled #} 47 | {% set disabled = props.disabled ?? false %} 48 | 49 | {# @var array{label: string, value: string, selected: bool}[] options #} 50 | {% set options = props.options ?? [] %} 51 | 52 | {% macro option(item) %} 53 | 59 | {% endmacro %} 60 | 61 | {% if useEmptyOption %} 62 | {% set options = options|unshift({label: placeholder, value: '' }) %} 63 | {% endif %} 64 | 65 | {{ FieldGroup({ 66 | fieldElement: 'select', 67 | fieldAttrs: { 68 | ...fieldAttrs, 69 | name, 70 | disabled: disabled ? '' : null, 71 | }, 72 | id, 73 | class, 74 | fieldChildren: options|map(item => _self.option(item)), 75 | label, 76 | labelHidden, 77 | required, 78 | subLabel, 79 | instructions, 80 | errorMessages, 81 | }) }} 82 | {% endmacro %} 83 | -------------------------------------------------------------------------------- /templates/_components/field-text-input.twig: -------------------------------------------------------------------------------- 1 | {% macro FieldTextInput(props = {}) %} 2 | {% from '_components/field-group' import FieldGroup %} 3 | 4 | {# Props #} 5 | 6 | {# These props mirror the props in field-group.twig #} 7 | 8 | {# @var string|null id #} 9 | {% set id = props.id ?? null %} 10 | 11 | {# @var string|null class #} 12 | {% set class = props.class ?? null %} 13 | 14 | {# @var array fieldAttrs #} 15 | {% set fieldAttrs = props.fieldAttrs ?? {} %} 16 | 17 | {# @var string|null label #} 18 | {% set label = props.label ?? null %} 19 | 20 | {# @var bool labelHidden #} 21 | {% set labelHidden = props.labelHidden ?? false %} 22 | 23 | {# @var bool required #} 24 | {% set required = props.required ?? false %} 25 | 26 | {# @var string|null subLabel #} 27 | {% set subLabel = props.subLabel ?? null %} 28 | 29 | {# @var string|null instructions #} 30 | {% set instructions = props.instructions ?? null %} 31 | 32 | {# @var string[] errorMessages #} 33 | {% set errorMessages = props.errorMessages ?? [] %} 34 | 35 | {# These props are for the input element #} 36 | 37 | {# @var string|null name #} 38 | {% set name = props.name ?? null %} 39 | 40 | {# @var string|null placeholder #} 41 | {% set placeholder = props.placeholder ?? null %} 42 | 43 | {# @var bool disabled #} 44 | {% set disabled = props.disabled ?? false %} 45 | 46 | {# @var string|null type #} 47 | {% set type = props.type ?? 'text' %} 48 | 49 | {# Allowed types #} 50 | {% set allowedTypes = ['text', 'password', 'email', 'number', 'tel', 'url'] %} 51 | {% set validatedType = type in allowedTypes ? type : 'text' %} 52 | 53 | {{ FieldGroup({ 54 | fieldElement: 'input', 55 | fieldAttrs: { 56 | ...fieldAttrs, 57 | name, 58 | type: validatedType, 59 | placeholder, 60 | disabled: disabled ? '' : null, 61 | }, 62 | id, 63 | class, 64 | label, 65 | labelHidden, 66 | required, 67 | subLabel, 68 | instructions, 69 | errorMessages, 70 | }) }} 71 | {% endmacro %} 72 | -------------------------------------------------------------------------------- /templates/_components/field-textarea.twig: -------------------------------------------------------------------------------- 1 | {% macro FieldTextarea(props = {}) %} 2 | {% from '_components/field-group' import FieldGroup %} 3 | 4 | {# Props #} 5 | 6 | {# These props mirror the props in field-group.twig #} 7 | 8 | {# @var string|null id #} 9 | {% set id = props.id ?? null %} 10 | 11 | {# @var string|null class #} 12 | {% set class = props.class ?? null %} 13 | 14 | {# @var array fieldAttrs #} 15 | {% set fieldAttrs = props.fieldAttrs ?? {} %} 16 | 17 | {# @var string|null label #} 18 | {% set label = props.label ?? null %} 19 | 20 | {# @var bool labelHidden #} 21 | {% set labelHidden = props.labelHidden ?? false %} 22 | 23 | {# @var bool required #} 24 | {% set required = props.required ?? false %} 25 | 26 | {# @var string|null subLabel #} 27 | {% set subLabel = props.subLabel ?? null %} 28 | 29 | {# @var string|null instructions #} 30 | {% set instructions = props.instructions ?? null %} 31 | 32 | {# @var string[] errorMessages #} 33 | {% set errorMessages = props.errorMessages ?? [] %} 34 | 35 | {# These props are for the textarea element #} 36 | 37 | {# @var string|null name #} 38 | {% set name = props.name ?? null %} 39 | 40 | {# @var string|null placeholder #} 41 | {% set placeholder = props.placeholder ?? null %} 42 | 43 | {# @var bool disabled #} 44 | {% set disabled = props.disabled ?? false %} 45 | 46 | {# @var int rows #} 47 | {% set rows = props.rows ?? 4 %} 48 | 49 | {# @var bool resize #} 50 | {% set resize = props.resize ?? false %} 51 | 52 | {{ FieldGroup({ 53 | fieldElement: 'textarea', 54 | fieldAttrs: { 55 | ...fieldAttrs, 56 | class: cx( 57 | fieldAttrs.class ?? null, 58 | { 59 | 'resize-none': not resize, 60 | }, 61 | ), 62 | name, 63 | placeholder, 64 | disabled: disabled ? '' : null, 65 | rows, 66 | }, 67 | id, 68 | class, 69 | label, 70 | labelHidden, 71 | required, 72 | subLabel, 73 | instructions, 74 | errorMessages, 75 | }) }} 76 | {% endmacro %} 77 | -------------------------------------------------------------------------------- /templates/_components/icon.twig: -------------------------------------------------------------------------------- 1 | {# 2 | Inlines the SVG markup directly into the page. 3 | 4 | Provides icon size props so you don't have to remember to pass 5 | `.size-xyz` every time you want to use your icon system. 6 | 7 | By setting size to `none` you can add arbitrary size classes to the icon. 8 | #} 9 | {% macro Icon(props = {}) %} 10 | {# Props #} 11 | 12 | {# @var string|null name #} 13 | {% set name = props.name ?? null %} 14 | 15 | {# @var string size #} 16 | {% set size = props.size ?? 'md' %} 17 | 18 | {# @var string|null class #} 19 | {% set class = props.class ?? null %} 20 | 21 | {# Computed values #} 22 | {% set sizeConfig = { 23 | 'sm': 'size-16', 24 | 'md': 'size-24', 25 | 'lg': 'size-32', 26 | 'none': null, 27 | } %} 28 | 29 | {% set sizeClass = sizeConfig[size] ?? sizeConfig.md %} 30 | 31 | {{ svg('@webroot/dist/assets/icons/' ~ name ~ '.svg')|attr({ 32 | class: cx(sizeClass, class), 33 | }) }} 34 | {% endmacro %} 35 | -------------------------------------------------------------------------------- /templates/_components/image-caption.twig: -------------------------------------------------------------------------------- 1 | {% macro ImageCaption(props = {}) %} 2 | {% from '_components/image' import Image %} 3 | 4 | {# Props #} 5 | 6 | {# 7 | @var array imageProps 8 | 9 | Use the same props that are defined in the image component. 10 | 11 | Example: 12 | 13 | {{ include('_components/image-caption.twig', { 14 | imageProps: { 15 | image: craft.assets().filename('image.jpg').one(), 16 | class: 'my-custom-class', 17 | ratio: 1/1, 18 | sizes: { 19 | default: '100vw', 20 | sm: '50vw', 21 | md: '33vw', 22 | }, 23 | }, 24 | caption: 'Image caption', 25 | }) }} 26 | #} 27 | {% set imageProps = props.imageProps ?? {} %} 28 | 29 | {# @var string|null caption #} 30 | {% set caption = props.caption ?? null %} 31 | 32 | {# @var string|null class #} 33 | {% set class = props.class ?? null %} 34 | 35 |
36 | {{ Image(imageProps) }} 37 | {% if caption %} 38 |
{{ caption }}
39 | {% endif %} 40 |
41 | {% endmacro %} 42 | -------------------------------------------------------------------------------- /templates/_components/image.twig: -------------------------------------------------------------------------------- 1 | {% macro Image(props = {}) %} 2 | {# 3 | Example Usage: 4 | ``` 5 | {{ Image({ 6 | image: craft.assets.one(), 7 | ratio: 3/4, 8 | sizes: { 9 | default: '50vw', 10 | sm: '25vw', 11 | md: '33vw', 12 | } 13 | }) }} ``` 14 | 15 | Component Configuration: 16 | 17 | This component is coupled to several values in your Tailwind config. 18 | 19 | transformMaxWidth: 20 | The default for this prop should be double the width of your .container 21 | class (or whatever is used to define the max width of your content) 22 | 23 | screens: 24 | This config lets you use breakpoint names in your sizes prop instead of 25 | typing custom media queries for every image. 26 | 27 | You must manually keep in sync with Tailwind's screens config. 28 | @see https://tailwindcss.com/docs/responsive-design 29 | 30 | You may add any custom breakpoint names (such as `container` to apply a size when the container width is reached) 31 | #} 32 | {% set screens = { 33 | 'sm': '(min-width: 640px)', 34 | 'md': '(min-width: 768px)', 35 | 'lg': '(min-width: 1024px)', 36 | 'xl': '(min-width: 1280px)', 37 | '2xl': '(min-width: 1536px)', 38 | } %} 39 | 40 | {# Props #} 41 | 42 | {# @var \craft\elements\Asset|null image #} 43 | {% set image = props.image ?? null %} 44 | 45 | {# 46 | @var string|null class 47 | Pass an empty string to have no class applied to the image. 48 | #} 49 | {% set class = props.class ?? "w-full h-auto" %} 50 | 51 | {# 52 | @var string|null loading 53 | Can be 'lazy' or 'eager' 54 | #} 55 | {% set loading = props.loading ?? 'lazy' %} 56 | 57 | {# 58 | @var array sizes 59 | 60 | The 'sizes' prop is used to specify the size of the image at different viewport widths. 61 | It should be an associative array where: 62 | - Keys are either Tailwind breakpoint names (sm, md, lg, xl, 2xl) or custom media queries. 63 | - Values are CSS length values (e.g., px, em, rem, vw) representing the image width at that breakpoint. 64 | 65 | Example usage: 66 | sizes: { 67 | default: '100vw', 68 | sm: '50vw', 69 | md: '33vw', 70 | '(min-width: 1200px)': '400px' 71 | } 72 | If not provided, it defaults to full viewport width at all sizes. 73 | #} 74 | {% set sizes = props.sizes ?? { 75 | default: '100vw', 76 | } %} 77 | 78 | {# 79 | @var float|null ratio 80 | Calculated by dividing width by height. 81 | Example: 82 | { 83 | ratio: 16/9, 84 | } 85 | #} 86 | {% set ratio = props.ratio ?? null %} 87 | 88 | {# 89 | @var int|null transformMinWidth 90 | The smallest transform to use when using fillTransforms: auto 91 | #} 92 | {% set transformMinWidth = props.transformMinWidth ?? 420 %} 93 | 94 | {# 95 | @var int|null transformMaxWidth 96 | The largest transform to use when using fillTransforms: auto 97 | #} 98 | {% set transformMaxWidth = props.transformMaxWidth ?? 2480 %} 99 | 100 | {# 101 | @var int|null transformAutofillCount 102 | The number of transforms to auto-fill between transformMinWidth and transformMaxWidth 103 | #} 104 | {% set transformAutofillCount = props.transformAutofillCount ?? 3 %} 105 | 106 | {# 107 | @var array|string|null transform 108 | 109 | Leave null to use an auto-filled transform (recommended). 110 | 111 | Can optionally use a named transform. 112 | 113 | The auto-filled transform uses Imager X to pre-populate our transform with multiple 114 | transforms between the width of transformMinWidth and transformMaxWidth. 115 | 116 | See: 117 | - transformMinWidth 118 | - transformMaxWidth 119 | - transformAutofillCount 120 | - fillTransforms 121 | - https://www.spacecat.ninja/blog/power-pack-and-quick-transform-syntax 122 | #} 123 | {% set transform = props.transform ?? null %} 124 | 125 | {# Computed values #} 126 | 127 | {# This flag disables the imagerOverrides that set fillTransform: 'auto #} 128 | {% set useDefaultTransform = transform is null %} 129 | 130 | {# Use either our default transform, or the overridden version from our props #} 131 | {% set transform = useDefaultTransform ? [ 132 | transformMinWidth, 133 | transformMaxWidth, 134 | { 135 | ratio: ratio, 136 | format: 'webp', 137 | }, 138 | ] : transform %} 139 | 140 | {% set defaultSize = sizes.default ?? '100vw' %} 141 | 142 | {# 143 | When sizes is scalar, the single value will be used in the images sizes attribute.\ 144 | 145 | Otherwise, iterate through sizes object: 146 | 1. Remove the default key so we can add it to the end (step 3). 147 | 2. Create our pair of MQs + Unit values. First checks if the 148 | key matches our Tailwind screen config. If not, assumes that 149 | it is a manually defined media query. 150 | 3. Add the default size to the end. 151 | 4. Join the values with a comma. 152 | #} 153 | {% set sizes = sizes is scalar ? sizes : sizes 154 | |filter((value, key) => key != 'default') 155 | |map((value, key) => "#{screens[key] ?? key} #{value}") 156 | |merge([ defaultSize ]) 157 | |join(', ') %} 158 | 159 | {# 160 | Default settings applied to all transforms 161 | 162 | imagerOverrides: 163 | - Overides settings from config/imager-x.php 164 | 165 | See: 166 | - https://www.spacecat.ninja/blog/power-pack-and-quick-transform-syntax 167 | - https://github.com/spacecatninja/craft-imager-x-power-pack 168 | #} 169 | {{ image ? ppimg( 170 | image: image, 171 | transform: transform, 172 | params: { 173 | class, 174 | loading, 175 | sizes, 176 | imagerOverrides: useDefaultTransform ? { 177 | fillTransforms: 'auto', 178 | autoFillCount: transformAutofillCount, 179 | allowUpscale: false, 180 | } : null, 181 | }, 182 | ) }} 183 | {% endmacro %} 184 | -------------------------------------------------------------------------------- /templates/_components/page-hero.twig: -------------------------------------------------------------------------------- 1 | {% macro PageHero(props = {}) %} 2 | {% from '_components/image' import Image %} 3 | 4 | {# Props #} 5 | 6 | {# @var string|null title #} 7 | {% set title = props.title ?? null %} 8 | 9 | {# @var string|null description #} 10 | {% set description = props.description ?? null %} 11 | 12 | {# @var \craft\elements\Asset|null image #} 13 | {% set image = props.image ?? null %} 14 | 15 | {# @var int headingLevel #} 16 | {% set headingLevel = props.headingLevel ?? 1 %} 17 | 18 | {# @var string|null class #} 19 | {% set class = props.class ?? null %} 20 | 21 | {# Computed values #} 22 | {% set element = "h#{headingLevel}" %} 23 | 24 |
25 |
26 |
27 | {% if title %} 28 | {% tag element with { 29 | class: 'text-4xl font-bold', 30 | } %} 31 | {{ title }} 32 | {% endtag %} 33 | {% endif %} 34 | 35 | {% if description %} 36 |
{{ description }}
37 | {% endif %} 38 |
39 | 40 | {{ image ? Image({ 41 | class: 'lg:col-span-2', 42 | image, 43 | ratio: 4/3, 44 | sizes: { 45 | default: '100vw', 46 | sm: '50vw', 47 | } 48 | }) }} 49 |
50 |
51 | {% endmacro %} 52 | -------------------------------------------------------------------------------- /templates/_components/pagination.twig: -------------------------------------------------------------------------------- 1 | {% macro Pagination(props = {}) %} 2 | {% from '_components/button' import Button %} 3 | {# Props #} 4 | 5 | {# @var object pageInfo - Pass the pageInfo object from Craft's {% paginate %} tag to pre-fill all props #} 6 | {% set pageInfo = props.pageInfo ?? null %} 7 | 8 | {# 9 | @var int maxLinks 10 | Controls the number of page links to show. Includes the first and last page URLs. 11 | Odd numbers work best. 12 | 13 | maxLinks has no effect when using manual props 14 | #} 15 | {% set maxLinks = props.maxLinks ?? 5 %} 16 | 17 | {# @var string|null nextUrl #} 18 | {% set nextUrl = props.nextUrl ?? pageInfo.nextUrl ?? null %} 19 | 20 | {# @var string|null prevUrl #} 21 | {% set prevUrl = props.prevUrl ?? pageInfo.prevUrl ?? null %} 22 | 23 | {# @var int currentPage #} 24 | {% set currentPage = props.currentPage ?? pageInfo.currentPage ?? null %} 25 | 26 | {# @var int totalPages #} 27 | {% set totalPages = props.totalPages ?? pageInfo.totalPages ?? null %} 28 | 29 | {# @var string|null firstUrl #} 30 | {% set firstUrl = props.firstUrl ?? pageInfo.firstUrl ?? null %} 31 | 32 | {# @var string|null lastUrl #} 33 | {% set lastUrl = props.lastUrl ?? pageInfo.lastUrl ?? null %} 34 | 35 | {# @var array visibleUrls An array of visible page URLs indexed by page number #} 36 | {% set visibleUrls = props.visibleUrls ?? [] %} 37 | 38 | {# @var string size #} 39 | {% set size = props.size ?? 'md' %} 40 | 41 | {# @var string ariaLabel #} 42 | {% set ariaLabel = props.ariaLabel ?? 'pagination navigation' %} 43 | 44 | {# Computed values #} 45 | {% set needEllipsis = totalPages > maxLinks %} 46 | {% set elipsisThreshold = floor(maxLinks / 2) %} 47 | {% set hasStartEllipsis = needEllipsis and currentPage > maxLinks - elipsisThreshold %} 48 | {% set hasEndEllipsis = needEllipsis and currentPage < totalPages - elipsisThreshold %} 49 | 50 | {% if pageInfo %} 51 | {% if hasStartEllipsis %} 52 | {% set maxLinks = maxLinks - 1 %} 53 | {% endif %} 54 | 55 | {% if hasEndEllipsis or (currentPage < totalPages - (elipsisThreshold - 1)) %} 56 | {% set maxLinks = maxLinks - 1 %} 57 | {% endif %} 58 | 59 | {% set visibleUrls = pageInfo.getDynamicRangeUrls(maxLinks) %} 60 | {% endif %} 61 | 62 | 130 | {% endmacro %} 131 | 132 | {# Reused markup for pagination items #} 133 | {% macro _PaginationItem(props = {}) %} 134 | {% from '_components/button' import Button %} 135 | 136 | {# @var string|null url #} 137 | {% set url = props.url ?? null %} 138 | 139 | {# @var int|null page #} 140 | {% set page = props.page ?? null %} 141 | 142 | {# @var string|null size #} 143 | {% set size = props.size ?? null %} 144 | 145 | {# @var int|null currentPage #} 146 | {% set currentPage = props.currentPage ?? null %} 147 | 148 | {{ Button({ 149 | href: url, 150 | text: page, 151 | variant: page == currentPage ? 'contained' : 'subtle', 152 | size: size, 153 | class: cx('!p-0 aspect-square rounded-full'), 154 | attrs: { 155 | 'aria-current': page == currentPage ? 'page' : null, 156 | }, 157 | }) }} 158 | {% endmacro %} 159 | -------------------------------------------------------------------------------- /templates/_components/tabs.twig: -------------------------------------------------------------------------------- 1 | {% macro Tabs(props = {}) %} 2 | {% from '_components/button' import Button %} 3 | 4 | {# Props #} 5 | 6 | {# @var array{title: string, content: string}[] tabs #} 7 | {% set tabs = props.tabs ?? [] %} 8 | 9 | {# @var int activeIndex #} 10 | {% set activeIndex = props.activeIndex ?? 0 %} 11 | 12 | {# @var string|null class #} 13 | {% set class = props.class ?? null %} 14 | 15 | {# Computed values #} 16 | {% set id = 'tabs-' ~ random() %} 17 | 18 |
35 | 56 | 57 | {% for tab in tabs %} 58 |
68 | {{ tab.content }} 69 |
70 | {% endfor %} 71 | 72 | {{ Button({ 73 | text: 'Back to tabs list', 74 | size: 'sm', 75 | variant: 'text', 76 | class: 'sr-only focus:not-sr-only focus:mt-16 focus:z-max', 77 | attrs: { 78 | '@click': 'activeTab.focus()', 79 | }, 80 | }) }} 81 |
82 | {% endmacro %} 83 | 84 | -------------------------------------------------------------------------------- /templates/_components/tag.twig: -------------------------------------------------------------------------------- 1 | {% macro Tag(props = {}) %} 2 | {% from '_components/icon.twig' import Icon %} 3 | 4 | {# Props #} 5 | 6 | {# @var string|null as #} 7 | {% set as = props.as ?? null %} 8 | 9 | {# @var string variant #} 10 | {% set variant = props.variant ?? 'contained' %} 11 | 12 | {# @var string size #} 13 | {% set size = props.size ?? 'sm' %} 14 | 15 | {# @var boolean clickable #} 16 | {% set clickable = props.clickable ?? false %} 17 | 18 | {# @var string|null href #} 19 | {% set href = props.href ?? null %} 20 | 21 | {# @var string|null class #} 22 | {% set class = props.class ?? null %} 23 | 24 | {# @var array attrs #} 25 | {% set attrs = props.attrs ?? {} %} 26 | 27 | {# @var string|null text #} 28 | {% set text = props.text ?? null %} 29 | 30 | {# @var string|null icon #} 31 | {% set icon = props.icon ?? null %} 32 | 33 | {# Computed values #} 34 | 35 | {# Determine element type #} 36 | {% set el = as ?? (href ? 'a' : (clickable ? 'button' : 'span')) %} 37 | 38 | {% tag el with attrs|merge({ 39 | href: href, 40 | type: clickable and el == 'button' ? 'button' : null, 41 | class: cx('inline-flex items-center justify-center gap-4 rounded-xs text-center font-medium focus:outline-hidden focus-visible:ring-4', { 42 | 'bg-sky-600 text-white dark:bg-sky-200 dark:text-sky-800': variant == 'contained', 43 | 'no-underline hover:bg-sky-700 focus-visible:bg-sky-700 focus-visible:ring-sky-600/50 active:bg-sky-800 dark:hover:bg-sky-50 dark:focus-visible:bg-sky-50 dark:focus-visible:ring-white/50 dark:active:bg-sky-100': variant == 'contained' and (href or clickable), 44 | 'border border-sky-600 bg-transparent text-sky-600 dark:border-white dark:text-white': variant == 'outlined', 45 | 'hover:border-sky-700 hover:bg-sky-100 hover:text-sky-700 focus-visible:border-sky-700 focus-visible:bg-sky-100 focus-visible:ring-sky-600/50 active:bg-sky-200/80 active:text-sky-800 dark:hover:border-white dark:hover:bg-white/25 dark:hover:text-white dark:focus-visible:border-white dark:focus-visible:bg-white/25 dark:focus-visible:ring-white/50 dark:active:bg-white/30': variant == 'outlined' and (href or clickable), 46 | 'min-h-20 px-6 text-xs [&_svg]:size-12': size == 'sm', 47 | 'min-h-28 px-8 text-sm [&_svg]:size-16': size == 'md', 48 | 'min-h-36 px-10 text-base [&_svg]:size-20': size == 'lg', 49 | }, class), 50 | }) %} 51 | {{ icon ? Icon({ 52 | name: icon, 53 | size: 'none', 54 | class: '-my-4', 55 | }) }} 56 | {{ text }} 57 | {% endtag %} 58 | {% endmacro %} 59 | -------------------------------------------------------------------------------- /templates/_components/video.twig: -------------------------------------------------------------------------------- 1 | {# 2 | TODO possible improvements 3 | - rework to parse the video provider & id from any type of URL. 4 | - customizable thumbnail / play button 5 | #} 6 | 7 | {% macro Video(props = {}) %} 8 | {% from '_components/button' import Button %} 9 | 10 | {# Props #} 11 | 12 | {# 13 | @var string source - The source of the video. E.g. 'https://www.youtube.com/embed/lJIrF4YjHfQ' 14 | #} 15 | {% set source = props.source ?? '' %} 16 | 17 | {# 18 | @var array iframeAttrs - Additional attributes to pass to the iframe element. 19 | #} 20 | {% set iframeAttrs = props.iframeAttrs ?? {} %} 21 | 22 | {# 23 | @var string|null class - Additional classes for the wrapper div 24 | #} 25 | {% set class = props.class ?? null %} 26 | 27 | {# Computed values #} 28 | 29 | {# Generate a unique ID for the video element #} 30 | {% set id = 'video-' ~ random() %} 31 | 32 |
33 | {{ Button({ 34 | href: '#' ~ id, 35 | variant: 'text', 36 | class: 'sr-only focus-visible:not-sr-only', 37 | text: 'Skip past video' 38 | }) }} 39 | 40 | 48 |
49 | 50 |
51 | {% endmacro %} 52 | -------------------------------------------------------------------------------- /templates/_elements/home.twig: -------------------------------------------------------------------------------- 1 | {% extends '_layouts/base.twig' %} 2 | 3 | {% block content %} 4 | {{ include("_blocks/index.twig", { 5 | blocks: entry.pageBlocks, 6 | }) }} 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /templates/_layouts/base.twig: -------------------------------------------------------------------------------- 1 | {%- set codeBlocks = globalCode.codeBlocks.collect() -%} 2 | {%- set codeBlocksTemplate = '_partials/global-code-blocks.twig' -%} 3 | 4 | 5 | 6 | {# -- Dangerously added raw HTML -- #} 7 | {{ include(codeBlocksTemplate, { 8 | codeBlocks, 9 | codePosition: 'headTop' 10 | }, with_context = false) }} 11 | 12 | 13 | 14 | 15 | {% if seomatic is not defined -%} 16 | {{ siteName }} 17 | {%- endif %} 18 | 19 | {# Preload Webfonts #} 20 | 21 | 22 | 23 | 24 | 25 | {# Load our main CSS file to avoid FOUC in dev mode #} 26 | {% if craft.vite.devServerRunning() %} 27 | 28 | {% endif %} 29 | 30 | {{ craft.vite.script('src/js/app.js', false) }} 31 | 32 | {# Hide elements if we're in the parts-kit #} 33 | {% set isPartsKitUrl = craft.app.request.segments | first == 'parts-kit' %} 34 | 35 | {# -- Dangerously added raw HTML -- #} 36 | {{ include(codeBlocksTemplate, { 37 | codeBlocks, 38 | codePosition: 'headBottom' 39 | }, with_context = false) }} 40 | 41 | 42 | {# -- Dangerously added raw HTML -- #} 43 | {{ include(codeBlocksTemplate, { 44 | codeBlocks, 45 | codePosition: 'bodyTop' 46 | }, with_context = false) }} 47 | 48 | {% if not isPartsKitUrl %} 49 | {{ include('_partials/header.twig') }} 50 | {% endif %} 51 | 52 |
53 | {% block content %}{% endblock %} 54 |
55 | 56 | {% if not isPartsKitUrl %} 57 | {{ include('_partials/footer.twig') }} 58 | {% endif %} 59 | 60 | {# -- Dangerously added raw HTML -- #} 61 | {{ include(codeBlocksTemplate, { 62 | codeBlocks, 63 | codePosition: 'bodyBottom' 64 | }, with_context = false) }} 65 | 66 | 67 | -------------------------------------------------------------------------------- /templates/_partials/footer.twig: -------------------------------------------------------------------------------- 1 | {% from "_components/icon.twig" import Icon %} 2 | 3 | {% set footerNavigation = craft.navigation.nodes() 4 | .handle('footerNavigation') 5 | .level(1) 6 | .with(['children']) 7 | .collect() %} 8 | 9 | 72 | -------------------------------------------------------------------------------- /templates/_partials/global-code-blocks.twig: -------------------------------------------------------------------------------- 1 | {% apply spaceless %} 2 | {# @var \Illuminate\Support\Collection<\craft\elements\Entry> blocks #} 3 | {% set codeBlocks = codeBlocks ?? collect([]) %} 4 | 5 | {# @var string codePosition - Required #} 6 | {% set codePosition = codePosition ?? null %} 7 | 8 | {% set validPositions = ['headTop', 'headBottom', 'bodyTop', 'bodyBottom'] %} 9 | 10 | {% set blocksForPosition = codeBlocks|filter(block => (block.codePosition.value|default == codePosition)) %} 11 | 12 | {% if blocksForPosition|length and codePosition in validPositions %} 13 | {% for block in blocksForPosition %} 14 | {# -- Dangerously added raw HTML -- #} 15 | {{ block.code|raw }} 16 | {% endfor %} 17 | {% endif %} 18 | {% endapply %} 19 | -------------------------------------------------------------------------------- /templates/_partials/header.twig: -------------------------------------------------------------------------------- 1 | {% from "_components/icon.twig" import Icon %} 2 | {% from '_components/button' import Button %} 3 | 4 | {# The primary nav has a max depth of 3 levels. This simplifies our code since it doesn't have to be truly recursive #} 5 | {% set primaryNavigation = craft.navigation.nodes() 6 | .handle('primaryNavigation') 7 | .level(1) 8 | .with(['children.children']) 9 | .collect() %} 10 | 11 | {{ Button({ 12 | text: 'Skip to main content', 13 | href: '#content', 14 | class: 'sr-only focus:not-sr-only focus:absolute focus:top-16 focus:left-16 focus:z-50 focus:p-16 focus:z-max', 15 | }) }} 16 | 17 |
21 | {{ include('_partials/nav-desktop.twig', { 22 | nodes: primaryNavigation, 23 | }) }} 24 | 25 | {{ include('_partials/nav-mobile.twig', { 26 | nodes: primaryNavigation, 27 | }) }} 28 |
29 | -------------------------------------------------------------------------------- /templates/_partials/nav-desktop.twig: -------------------------------------------------------------------------------- 1 | {% from "_components/icon.twig" import Icon %} 2 | 3 | {# @var \Illuminate\Support\Collection nodes #} 4 | {% set nodes = nodes ?? collect([]) %} 5 | 6 | {% set totalNodes = nodes.count() %} 7 | 8 | {% macro navIem(props = {}) %} 9 | {% set node = props.node %} 10 | {% set textClass = props.textClass ?? null %} 11 | {% set verticalPaddingClass = props.verticalPaddingClass ?? 'py-16' %} 12 | {% set iconTransitionClass = props.iconTransitionClass ?? 'transition-transform group-aria-expanded:rotate-180' %} 13 | {% set hasChildren = props.hasChildren ?? false %} 14 | {% set element = hasChildren ? 'button' : 'a' %} 15 | 16 | {% tag element with { 17 | href: hasChildren ? null : node.url, 18 | class: cx(verticalPaddingClass, textClass, 'group flex w-full items-center justify-between gap-4 pl-8', { 19 | 'pr-16': not hasChildren, 20 | 'pr-8': hasChildren, 21 | }), 22 | ':aria-expanded': hasChildren ? "isInStack($refs.subnav)" : null, 23 | ':aria-controls': hasChildren ? "$id('subnav')" : null, 24 | '@click': "toggle($refs.subnav)" 25 | } %} 26 | {{ node.title }} 27 | 28 | {{ hasChildren ? Icon({ 29 | name: 'chevron-down', 30 | size: 'sm', 31 | class: iconTransitionClass, 32 | }) }} 33 | {% endtag %} 34 | {% endmacro %} 35 | 36 | 140 | -------------------------------------------------------------------------------- /templates/_partials/nav-mobile.twig: -------------------------------------------------------------------------------- 1 | {% from "_components/icon.twig" import Icon %} 2 | 3 | {# @var \Illuminate\Support\Collection nodes #} 4 | {% set nodes = nodes ?? collect([]) %} 5 | 6 | {% set totalNodes = nodes.count() %} 7 | 8 | {% macro navIem(props = {}) %} 9 | {% set node = props.node %} 10 | {% set textClass = props.textClass ?? null %} 11 | {% set iconTransitionClass = props.iconTransitionClass ?? 'transition-transform group-aria-expanded:rotate-180' %} 12 | {% set hasChildren = props.hasChildren ?? false %} 13 | {% set element = hasChildren ? 'button' : 'a' %} 14 | 15 | {% tag element with { 16 | href: hasChildren ? null : node.url, 17 | class: cx(textClass, 'text-gray-100 group py-16 flex w-full items-center justify-between gap-4 px-16'), 18 | ':aria-expanded': hasChildren ? "isInStack($refs.subnav)" : null, 19 | ':aria-controls': hasChildren ? "$id('subnav')" : null, 20 | '@click': "toggle($refs.subnav)" 21 | } %} 22 | {{ node.title }} 23 | 24 | {{ hasChildren ? Icon({ 25 | name: 'chevron-down', 26 | size: 'sm', 27 | class: iconTransitionClass, 28 | }) }} 29 | {% endtag %} 30 | {% endmacro %} 31 | 32 | 180 | -------------------------------------------------------------------------------- /templates/parts-kit/accordion/default.twig: -------------------------------------------------------------------------------- 1 | {% extends "viget-parts-kit/layout.twig" %} 2 | {% from '_components/accordion' import Accordion %} 3 | 4 | {% block main %} 5 | 6 |
7 | {% for i in 1..3 %} 8 | {{ Accordion({ 9 | title: 'Accordion Item ' ~ i, 10 | content: 'This is the content for accordion item ' ~ i ~ '.', 11 | }) }} 12 | {% endfor %} 13 |
14 | 15 |

Grouped Accordions

16 | 17 |
18 | {% for i in 1..3 %} 19 | {{ Accordion({ 20 | title: 'Accordion Item ' ~ i, 21 | content: 'This is the content for accordion item ' ~ i ~ '.', 22 | name: 'accordion-group', 23 | }) }} 24 | {% endfor %} 25 |
26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /templates/parts-kit/alert-banner/default.twig: -------------------------------------------------------------------------------- 1 | {% extends 'viget-parts-kit/layout.twig' %} 2 | 3 | {% from '_components/alert-banner' import AlertBanner %} 4 | 5 | {% block main %} 6 | {{ AlertBanner({ 7 | title: 'Dismissible Alert Banner', 8 | description: 'Cillum dolor nisi et sunt in in et ullamco eiusmod duis aute et fugiat excepteur. Sit irure consectetur anim do aliqua excepteur amet nulla magna enim proident incididunt ipsum.', 9 | dismissible: true, 10 | }) }} 11 | 12 |
13 | 14 | {{ AlertBanner({ 15 | title: 'Non-Dismissible Alert Banner', 16 | description: 'Cillum dolor nisi et sunt in in et ullamco eiusmod duis aute et fugiat excepteur. Sit irure consectetur anim do aliqua excepteur amet nulla magna enim proident incididunt ipsum.', 17 | dismissible: false, 18 | }) }} 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /templates/parts-kit/button/default.twig: -------------------------------------------------------------------------------- 1 | {% extends 'viget-parts-kit/layout.twig' %} 2 | 3 | {% from '_components/button' import Button %} 4 | 5 | {% set sizes = ['sm', 'md', 'lg'] %} 6 | 7 | {% block main %} 8 |
9 | {% for size in sizes %} 10 |
11 |

{{ size }}

12 | 13 | {{ Button({ 14 | variant: 'contained', 15 | text: 'Button', 16 | size, 17 | }) }} 18 | 19 | {{ Button({ 20 | variant: 'outlined', 21 | text: 'Button', 22 | size, 23 | }) }} 24 | 25 | {{ Button({ 26 | variant: 'subtle', 27 | text: 'Button', 28 | size, 29 | }) }} 30 | 31 | {{ Button({ 32 | variant: 'text', 33 | text: 'Button', 34 | size, 35 | }) }} 36 |
37 | {% endfor %} 38 |
39 | 40 |
41 | 42 |
43 |

With Icon

44 | {% for size in sizes %} 45 |
46 |

{{ size }}

47 | {{ Button({ 48 | variant: 'contained', 49 | text: 'Button', 50 | size, 51 | icon: 'arrow-right', 52 | }) }} 53 | 54 | {{ Button({ 55 | variant: 'contained', 56 | text: 'Button', 57 | size, 58 | icon: 'arrow-right', 59 | iconLast: true, 60 | }) }} 61 | 62 | {{ Button({ 63 | variant: 'contained', 64 | text: 'Button', 65 | size, 66 | icon: 'arrow-right', 67 | iconOnly: true, 68 | }) }} 69 | 70 | {{ Button({ 71 | variant: 'outlined', 72 | text: 'Button', 73 | size, 74 | icon: 'arrow-right', 75 | }) }} 76 | 77 | {{ Button({ 78 | variant: 'outlined', 79 | text: 'Button', 80 | size, 81 | icon: 'arrow-right', 82 | iconOnly: true, 83 | }) }} 84 | 85 | {{ Button({ 86 | variant: 'subtle', 87 | text: 'Button', 88 | size, 89 | icon: 'arrow-right', 90 | }) }} 91 | 92 | {{ Button({ 93 | variant: 'subtle', 94 | iconOnly: true, 95 | text: 'Button', 96 | size, 97 | icon: 'arrow-right', 98 | }) }} 99 | 100 | {{ Button({ 101 | variant: 'text', 102 | text: 'Button', 103 | size, 104 | icon: 'arrow-right', 105 | }) }} 106 | 107 | {{ Button({ 108 | variant: 'text', 109 | text: 'Button', 110 | size, 111 | icon: 'arrow-right', 112 | iconOnly: true, 113 | }) }} 114 |
115 | {% endfor %} 116 |
117 | {% endblock %} 118 | -------------------------------------------------------------------------------- /templates/parts-kit/call-to-action/default.twig: -------------------------------------------------------------------------------- 1 | {% extends 'viget-parts-kit/layout.twig' %} 2 | 3 | {% from '_components/call-to-action' import CallToAction %} 4 | 5 | {% block main %} 6 | {{ CallToAction({ 7 | title: 'Lorem ipsum dolor sit amet', 8 | text: 'Lorem ipsum dolor sit amet, consectetur adipiscing e lit. Nulla facilisi. Nulla facilisi. Nulla facilisi.', 9 | href: '#TEST', 10 | buttonText: 'Button', 11 | newTab: true, 12 | }) }} 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /templates/parts-kit/card/card-with-icon.twig: -------------------------------------------------------------------------------- 1 | {% extends 'viget-parts-kit/layout.twig' %} 2 | 3 | {% from '_components/card-icon' import CardIcon %} 4 | 5 | {% block main %} 6 |
7 | {% for i in 1..3 %} 8 | {{ CardIcon({ 9 | href: i == 2 ? '#TEST' : null, 10 | newTab: i == 2, 11 | icon: 'clock', 12 | title: 'Lorem ipsum dolor sit amet', 13 | description: 'Cillum dolor nisi et sunt in in et ullamco eiusmod duis aute et fugiat excepteur. Sit irure consectetur anim do aliqua excepteur amet nulla magna enim proident incididunt ipsum.', 14 | }) }} 15 | {% endfor %} 16 |
17 | 18 |
19 | {% for i in 1..3 %} 20 | {{ CardIcon({ 21 | href: i == 2 ? '#TEST' : null, 22 | newTab: i == 2, 23 | align: 'center', 24 | icon: 'clock', 25 | title: 'Lorem ipsum dolor sit amet', 26 | description: 'Cillum dolor nisi et sunt in in et ullamco eiusmod duis aute et fugiat excepteur. Sit irure consectetur anim do aliqua excepteur amet nulla magna enim proident incididunt ipsum.', 27 | }) }} 28 | {% endfor %} 29 |
30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /templates/parts-kit/card/card-with-image.twig: -------------------------------------------------------------------------------- 1 | {% extends 'viget-parts-kit/layout.twig' %} 2 | 3 | {% from '_components/card-image' import CardImage %} 4 | 5 | {% block main %} 6 |
7 | {% for i in 1..3 %} 8 | {{ CardImage({ 9 | image: craft.assets.one(), 10 | href: i == 2 ? '#TEST' : null, 11 | newTab: i == 2, 12 | sizes: { 13 | default: '33vw', 14 | }, 15 | title: 'Lorem ipsum dolor sit amet', 16 | description: 'Cillum dolor nisi et sunt in in et ullamco eiusmod duis aute et fugiat excepteur. Sit irure consectetur anim do aliqua excepteur amet nulla magna enim proident incididunt ipsum.', 17 | }) }} 18 | {% endfor %} 19 |
20 | 21 |
22 | {% for i in 1..3 %} 23 | {{ CardImage({ 24 | align: 'center', 25 | image: craft.assets.one(), 26 | href: i == 2 ? '#TEST' : null, 27 | newTab: i == 2, 28 | ratio: 1/1, 29 | sizes: { 30 | default: '33vw', 31 | }, 32 | title: 'Lorem ipsum dolor sit amet', 33 | description: 'Cillum dolor nisi et sunt in in et ullamco eiusmod duis aute et fugiat excepteur. Sit irure consectetur anim do aliqua excepteur amet nulla magna enim proident incididunt ipsum.', 34 | }) }} 35 | {% endfor %} 36 |
37 | {% endblock %} 38 | -------------------------------------------------------------------------------- /templates/parts-kit/dialog/default.twig: -------------------------------------------------------------------------------- 1 | {% extends "viget-parts-kit/layout.twig" %} 2 | 3 | {% from '_components/button' import Button %} 4 | {% from '_components/dialog' import Dialog %} 5 | 6 | {% block main %} 7 | 8 | {% set content %} 9 |

Hello World

10 |

Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.

11 | {% endset %} 12 | 13 | {# Dialogs are triggered by dispatching a custom event dialog:open with an id that matches the dialog's id #} 14 | {{ Button({ 15 | text: 'Open Dialog', 16 | attrs: { 17 | 'x-data': '', 18 | '@click': "$dispatch('dialog:open', { id: 'my-dialog' })" 19 | }, 20 | }) }} 21 | 22 | {{ Dialog({ 23 | content, 24 | id: 'my-dialog', 25 | hideClose: true, 26 | }) }} 27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /templates/parts-kit/dialog/digalog-triggers-dialog.twig: -------------------------------------------------------------------------------- 1 | {% extends "viget-parts-kit/layout.twig" %} 2 | 3 | {% from '_components/button' import Button %} 4 | {% from '_components/dialog' import Dialog %} 5 | 6 | {% block main %} 7 | 8 | {% set dialog1Content %} 9 |

Dialog 1

10 |

Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.

11 | 12 | {{ Button({ 13 | text: 'Open Dialog 2', 14 | attrs: { 15 | 'x-data': '', 16 | '@click': "$dispatch('dialog:open', { id: 'my-dialog-2' })" 17 | }, 18 | }) }} 19 | {% endset %} 20 | 21 | {% set dialog2Content %} 22 |

Dialog 2

23 |

Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.

24 | 25 | 26 | {% endset %} 27 | 28 | {# Dialogs are triggered by dispatching a custom event dialog:open with an id that matches the dialog's id #} 29 | {{ Button({ 30 | text: 'Open Dialog 1', 31 | attrs: { 32 | 'x-data': '', 33 | '@click': "$dispatch('dialog:open', { id: 'my-dialog' })" 34 | }, 35 | }) }} 36 | 37 | {{ Dialog({ 38 | content: dialog1Content, 39 | id: 'my-dialog', 40 | }) }} 41 | 42 | {{ Dialog({ 43 | content: dialog2Content, 44 | id: 'my-dialog-2', 45 | }) }} 46 | 47 | 48 | {% endblock %} 49 | -------------------------------------------------------------------------------- /templates/parts-kit/forms/select.twig: -------------------------------------------------------------------------------- 1 | {% extends 'viget-parts-kit/layout.twig' %} 2 | 3 | {% from '_components/field-select' import FieldSelect %} 4 | 5 | {% block main %} 6 | 7 | {% set options = [ 8 | { label: 'Option 1', value: 'option-1' }, 9 | { label: 'Option 2', value: 'option-2' }, 10 | { label: 'Option 3', value: 'option-3' }, 11 | ] %} 12 | 13 | {% set fields %} 14 |
15 | {{ FieldSelect({ 16 | id: 'custom-id', 17 | label: 'Select', 18 | subLabel: 'This is a sublabel', 19 | instructions: 'This is an instruction', 20 | options, 21 | name: 'select', 22 | }) }} 23 | 24 | {{ FieldSelect({ 25 | label: 'Selected options', 26 | options: [ 27 | { label: 'Option 1', value: 'option-1' }, 28 | { label: 'Option 2 (selected)', value: 'option-2', selected: true }, 29 | { label: 'Option 3', value: 'option-3' }, 30 | ], 31 | name: 'select-selected', 32 | }) }} 33 | 34 |
35 | {{ FieldSelect({ 36 | class: 'w-full', 37 | label: 'Select (required)', 38 | required: true, 39 | instructions: 'This is an instruction', 40 | useEmptyOption: true, 41 | placeholder: 'Choose an option', 42 | options, 43 | name: 'select-required', 44 | }) }} 45 | 46 | 47 |
48 | 49 | {{ FieldSelect({ 50 | label: 'Select (required long)', 51 | required: 'long', 52 | instructions: 'This is an instruction', 53 | options, 54 | name: 'select-required-long', 55 | }) }} 56 | 57 | {{ FieldSelect({ 58 | label: 'Select (required with error)', 59 | required: true, 60 | instructions: 'This is an instruction', 61 | errorMessages: ['This is an error message', 'This is another error message'], 62 | options, 63 | name: 'select-required-with-error', 64 | }) }} 65 | 66 | {{ FieldSelect({ 67 | label: 'Select (disabled)', 68 | disabled: true, 69 | options, 70 | name: 'select-disabled', 71 | }) }} 72 |
73 | {% endset %} 74 | 75 | {# default colors #} 76 | {{ fields }} 77 | 78 | {# dark background #} 79 |
80 | {% namespace 'dark' %} 81 | {{ fields }} 82 | {% endnamespace %} 83 |
84 | 85 | {% endblock %} 86 | -------------------------------------------------------------------------------- /templates/parts-kit/forms/text-input.twig: -------------------------------------------------------------------------------- 1 | {% extends 'viget-parts-kit/layout.twig' %} 2 | 3 | {% from '_components/field-text-input' import FieldTextInput %} 4 | 5 | {% block main %} 6 | 7 | {% set fields %} 8 |
9 | {{ FieldTextInput({ 10 | id: 'custom-id', 11 | label: 'Text Input', 12 | subLabel: 'This is a sublabel', 13 | instructions: 'This is an instruction', 14 | name: 'text-input', 15 | }) }} 16 | 17 | {{ FieldTextInput({ 18 | label: 'Text Input', 19 | placeholder: 'Placeholder (label hidden)', 20 | labelHidden: true, 21 | required: 'long', 22 | name: 'text-input-placeholder', 23 | }) }} 24 | 25 |
26 | {{ FieldTextInput({ 27 | class: 'w-full', 28 | label: 'Text Input (required)', 29 | required: true, 30 | instructions: 'This is an instruction', 31 | name: 'text-input-required', 32 | }) }} 33 | 34 | 35 |
36 | 37 | {{ FieldTextInput({ 38 | label: 'Text Input (required long)', 39 | required: 'long', 40 | instructions: 'This is an instruction', 41 | name: 'text-input-required-long', 42 | }) }} 43 | 44 | {{ FieldTextInput({ 45 | label: 'Text Input (required with error)', 46 | required: true, 47 | instructions: 'This is an instruction', 48 | errorMessages: ['This is an error message', 'This is another error message'], 49 | name: 'text-input-required-with-error', 50 | }) }} 51 | 52 | {{ FieldTextInput({ 53 | label: 'Text Input (disabled)', 54 | disabled: true, 55 | name: 'text-input-disabled', 56 | }) }} 57 | 58 | {% set inputTypes = ['text', 'password', 'email', 'number', 'tel', 'url'] %} 59 | {% for type in inputTypes %} 60 | {{ FieldTextInput({ 61 | label: 'Text Input (' ~ type ~ ')', 62 | type, 63 | name: 'text-input-' ~ type, 64 | }) }} 65 | {% endfor %} 66 |
67 | {% endset %} 68 | 69 | {# default colors #} 70 | {{ fields }} 71 | 72 | {# dark background #} 73 |
74 | {% namespace 'dark' %} 75 | {{ fields }} 76 | {% endnamespace %} 77 |
78 | 79 | {% endblock %} 80 | -------------------------------------------------------------------------------- /templates/parts-kit/forms/textarea.twig: -------------------------------------------------------------------------------- 1 | {% extends 'viget-parts-kit/layout.twig' %} 2 | 3 | {% from '_components/field-textarea' import FieldTextarea %} 4 | 5 | {% block main %} 6 | 7 | {% set fields %} 8 |
9 | {{ FieldTextarea({ 10 | id: 'custom-id', 11 | label: 'Text Area', 12 | subLabel: 'This is a sublabel', 13 | instructions: 'This is an instruction', 14 | name: 'textarea', 15 | }) }} 16 | 17 | {{ FieldTextarea({ 18 | label: 'Text Area', 19 | placeholder: 'Placeholder (label hidden)', 20 | labelHidden: true, 21 | required: 'long', 22 | name: 'textarea-placeholder', 23 | }) }} 24 | 25 |
26 | {{ FieldTextarea({ 27 | class: 'w-full', 28 | label: 'Text Area (required)', 29 | required: true, 30 | instructions: 'This is an instruction', 31 | name: 'textarea-required', 32 | }) }} 33 | 34 | 35 |
36 | 37 | {{ FieldTextarea({ 38 | label: 'Text Area (required long)', 39 | required: 'long', 40 | instructions: 'This is an instruction', 41 | name: 'textarea-required-long', 42 | }) }} 43 | 44 | {{ FieldTextarea({ 45 | label: 'Text Area (required with error)', 46 | required: true, 47 | instructions: 'This is an instruction', 48 | errorMessages: ['This is an error message', 'This is another error message'], 49 | name: 'textarea-required-with-error', 50 | }) }} 51 | 52 | {{ FieldTextarea({ 53 | label: 'Text Area (disabled)', 54 | disabled: true, 55 | name: 'textarea-disabled', 56 | }) }} 57 | 58 | {{ FieldTextarea({ 59 | label: 'Text Area (custom rows)', 60 | rows: 10, 61 | name: 'textarea-custom-rows', 62 | }) }} 63 | 64 | {{ FieldTextarea({ 65 | label: 'Text Area (resize)', 66 | resize: true, 67 | name: 'textarea-resize', 68 | }) }} 69 |
70 | {% endset %} 71 | 72 | {# default colors #} 73 | {{ fields }} 74 | 75 | {# dark background #} 76 |
77 | {% namespace 'dark' %} 78 | {{ fields }} 79 | {% endnamespace %} 80 |
81 | 82 | {% endblock %} 83 | -------------------------------------------------------------------------------- /templates/parts-kit/icons/default.twig: -------------------------------------------------------------------------------- 1 | {% extends 'viget-parts-kit/layout.twig' %} 2 | 3 | {% from '_components/icon.twig' import Icon %} 4 | 5 | {% set sizes = ['sm', 'md', 'lg', 'none'] %} 6 | 7 | {% set icons = ['arrow-left', 'arrow-right', 'clock', 'close'] %} 8 | 9 | {% block main %} 10 | 11 | {% for size in sizes %} 12 |
13 |

14 | {{ size }} 15 | {% if size == 'none' %} 16 | Custom size class 17 | {% endif %} 18 |

19 |
20 | {% for icon in icons %} 21 |
22 | {{ Icon({ 23 | name: icon, 24 | size: size, 25 | class: size == 'none' ? 'size-64' : null, 26 | }) }} 27 | {{ icon }} 28 |
29 | {% endfor %} 30 |
31 |
32 | {% endfor %} 33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /templates/parts-kit/image-caption/default.twig: -------------------------------------------------------------------------------- 1 | {% extends 'viget-parts-kit/layout.twig' %} 2 | 3 | {% from '_components/image-caption' import ImageCaption %} 4 | 5 | {# 6 | 7 | The image-caption component wraps the image component in a
tag. 8 | It also includes a
tag for the caption. 9 | 10 | Use the same props that are defined in the image component. 11 | 12 | #} 13 | 14 | {% block main %} 15 |
16 |

Image Caption

17 | 18 | {{ ImageCaption({ 19 | imageProps: { 20 | image: craft.assets().one(), 21 | ratio: 16/9, 22 | }, 23 | caption: 'Image caption', 24 | }) }} 25 |
26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /templates/parts-kit/image/default.twig: -------------------------------------------------------------------------------- 1 | {% extends 'viget-parts-kit/layout.twig' %} 2 | 3 | {% from '_components/image' import Image %} 4 | 5 | {# 6 | Overview: 7 | 8 | The Image component provides performant responsive images with minimal code. 9 | 10 | In most cases, you only need to think about the desired aspect ratio for the image 11 | and any sizes hints to help the browser determine the appropriate image size. 12 | 13 | The Image component will automatically auto-fill a range of image transforms 14 | to populate the srcset. 15 | 16 | Auto-filled transforms can be resource intensive. They work best with "Serverless 17 | Image Transforms", which is the recommended default for our Craft Site Starter. 18 | 19 | Serverless Image Transforms are generated by a lamda function only when the 20 | specific image URL is requested. This means that URLs in the srcset that are 21 | never requested are not generated. 22 | 23 | If you need to use named transforms, you can pass a string to the `transform` prop. 24 | 25 | Example: 26 | 27 | {{ Image({ 28 | image: craft.assets.one(), 29 | sizes: { 30 | default: '50vw', 31 | sm: '25vw', 32 | md: '33vw', 33 | }, 34 | ratio: 3/4, 35 | }) }} 36 | 37 | Output: 38 | 39 | 51 | #} 52 | 53 | {% block main %} 54 | 55 |
56 | 57 | {# 58 | Basic Example: 59 | Transforms and sizes are configured to work well with the .container class. 60 | #} 61 |

Basic Example

62 | {{ Image({ 63 | image: craft.assets.one(), 64 | ratio: 16/9, 65 | }) }} 66 | 67 | {# 68 | Custom Sizes: 69 | The keys of the sizes attribute correspond to the breakpoints in your Tailwind config. 70 | It uses a mobile first approach, default is the size used without a breakpoint. 71 | #} 72 |
73 |
74 |

Custom Sizes

75 | {{ Image({ 76 | image: craft.assets.one(), 77 | sizes: { 78 | default: '50vw', 79 | sm: '25vw', 80 | md: '33vw', 81 | }, 82 | }) }} 83 |
84 |
85 |

Custom Sizes and Aspect Ratio

86 | {{ Image({ 87 | image: craft.assets.one(), 88 | sizes: { 89 | default: '50vw', 90 | sm: '25vw', 91 | md: '33vw', 92 | }, 93 | ratio: 3/4, 94 | }) }} 95 |
96 |
97 | {# 98 | Named Transform: 99 | If you need to use a named transform, you can pass a string into the `transform` prop. 100 | #} 101 |

Named Transform

102 | {{ Image({ 103 | image: craft.assets.one(), 104 | transform: 'exampleNamedTransform', 105 | sizes: { 106 | default: '50vw', 107 | sm: '25vw', 108 | md: '33vw', 109 | }, 110 | }) }} 111 |
112 |
113 | {# 114 | Scalar Sizes 115 | This component supports custom transforms and scalar values for the sizes prop. 116 | #} 117 |

Scalar Sizes & Custom Transform

118 | {{ Image({ 119 | image: craft.assets.one(), 120 | class: 'size-40', 121 | transformMinWidth: 40, 122 | transformMaxWidth: 80, 123 | transformAutofillCount: 0, 124 | ratio: 1/1, 125 | sizes: '40px', 126 | }) }} 127 |
128 |
129 |
130 | 131 | {# 132 | Full Bleed Image 133 | 134 | The defaults for the image component are configured to work well 135 | with a .container class. 136 | 137 | If you need to use a full bleed image, you can override the ranges 138 | of the auto-filled transforms. 139 | 140 | Example Output: 141 | 142 | 153 | #} 154 | 155 |
156 |

Full Bleed Image

157 | {{ Image({ 158 | image: craft.assets.one(), 159 | ratio: 16/9, 160 | transformMinWidth: 800, 161 | transformMaxWidth: 3200, 162 | transformAutofillCount: 1, 163 | }) }} 164 |
165 | {% endblock %} 166 | -------------------------------------------------------------------------------- /templates/parts-kit/page-hero/default.twig: -------------------------------------------------------------------------------- 1 | {% extends 'viget-parts-kit/layout.twig' %} 2 | 3 | {% from '_components/page-hero' import PageHero %} 4 | 5 | {% block main %} 6 | {{ PageHero({ 7 | image: craft.assets.one(), 8 | title: 'Lorem ipsum dolor sit amet', 9 | description: 'Cillum dolor nisi et sunt in in et ullamco eiusmod duis aute et fugiat excepteur. Sit irure consectetur anim do aliqua excepteur amet nulla magna enim proident incididunt ipsum.', 10 | }) }} 11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /templates/parts-kit/paginaton/default.twig: -------------------------------------------------------------------------------- 1 | {% extends 'viget-parts-kit/layout.twig' %} 2 | 3 | {% from '_components/pagination' import Pagination %} 4 | 5 | {# 6 | The pagination component that integrates well with Craft's {% paginate %} by 7 | using the pageInfo object. 8 | 9 | https://craftcms.com/docs/5.x/development/element-queries.html#pagination 10 | 11 | You can manually use this component by passing props, however, calculating 12 | the number of links to show must be implemented manually. 13 | 14 | Example Usage: 15 | 16 | {% set blogQuery = craft.entries() 17 | .section('blog') 18 | .limit(1) 19 | .orderBy('postDate DESC') %} 20 | 21 | {% paginate blogQuery as pageInfo, posts %} 22 | 23 | {% for post in posts %} 24 |
25 |

{{ post.title }}

26 |
27 | {% endfor %} 28 | 29 | {{ Pagination({ 30 | pageInfo, 31 | maxLinks: 7, 32 | }) }} 33 | 34 | #} 35 | 36 | {% set props = { 37 | nextUrl: '?page=5', 38 | prevUrl: '?page=3', 39 | firstUrl: '?page=1', 40 | lastUrl: '?page=15', 41 | totalPages: 15, 42 | currentPage: 5, 43 | visibleUrls: { 44 | 4: '?page=4', 45 | 5: '?page=5', 46 | 6: '?page=6', 47 | }, 48 | } %} 49 | 50 | {% set examples %} 51 | {{ Pagination({ 52 | ...props, 53 | size: 'sm', 54 | }) }} 55 | 56 | {{ Pagination({ 57 | ...props, 58 | size: 'md', 59 | }) }} 60 | 61 | {{ Pagination({ 62 | ...props, 63 | size: 'lg', 64 | }) }} 65 | {% endset %} 66 | 67 | {% block main %} 68 |
69 | {{ examples }} 70 |
71 | 72 |
73 | {{ examples }} 74 |
75 | {% endblock %} 76 | -------------------------------------------------------------------------------- /templates/parts-kit/tabs/default.twig: -------------------------------------------------------------------------------- 1 | {% extends "viget-parts-kit/layout.twig" %} 2 | 3 | {% from '_components/tabs' import Tabs %} 4 | 5 | {% block main %} 6 |
7 | {{ Tabs({ 8 | tabs: [ 9 | { title: 'Tab 1', content: 'Content for Tab 1' }, 10 | { title: 'Tab 2', content: 'Content for Tab 2' }, 11 | ], 12 | }) }} 13 |
14 | 15 |
16 | {{ Tabs({ 17 | tabs: [ 18 | { title: 'Tab 1', content: 'Content for Tab 1' }, 19 | { title: 'Tab 2', content: 'Content for Tab 2' }, 20 | ], 21 | }) }} 22 |
23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /templates/parts-kit/tag/default.twig: -------------------------------------------------------------------------------- 1 | {% extends "viget-parts-kit/layout.twig" %} 2 | 3 | {% from '_components/tag' import Tag %} 4 | 5 | {% set variants = ['contained', 'outlined'] %} 6 | 7 | {% set sizes = ['sm', 'md', 'lg'] %} 8 | 9 | {% block main %} 10 | {% for variant in variants %} 11 |
12 | {% for size in sizes %} 13 | {{ Tag({ 14 | text: 'Tag ' ~ size ~ ' ' ~ variant, 15 | size: size, 16 | variant: variant, 17 | }) }} 18 | 19 | {{ Tag({ 20 | text: 'Tag ' ~ size ~ ' link', 21 | size: size, 22 | variant: variant, 23 | href: '#TEST', 24 | }) }} 25 | 26 | {{ Tag({ 27 | text: 'Tag ' ~ size ~ ' ' ~ variant, 28 | size: size, 29 | variant: variant, 30 | icon: 'clock', 31 | }) }} 32 | {% endfor %} 33 |
34 | {% endfor %} 35 | {% endblock %} 36 | -------------------------------------------------------------------------------- /templates/parts-kit/video/default.twig: -------------------------------------------------------------------------------- 1 | {% extends 'viget-parts-kit/layout.twig' %} 2 | 3 | {% from '_components/video' import Video %} 4 | 5 | {% block main %} 6 |
7 | 8 | {{ Video({ 9 | source: 'https://www.youtube.com/embed/lJIrF4YjHfQ', 10 | class: 'mb-24', 11 | }) }} 12 | 13 |

14 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos. Focusable Content After Video 15 |

16 |
17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import { viteStaticCopy } from 'vite-plugin-static-copy' 3 | import process from 'node:process' 4 | 5 | // Matches ddev web_extra_exposed_ports.https_port 6 | const HTTPS_PORT = 3000 7 | 8 | export default defineConfig(({ command }) => { 9 | return { 10 | base: command === 'serve' ? '' : '/dist/', 11 | build: { 12 | manifest: true, 13 | outDir: './web/dist/', 14 | rollupOptions: { 15 | input: { 16 | app: 'src/js/app.js', 17 | }, 18 | }, 19 | }, 20 | server: { 21 | host: '0.0.0.0', 22 | strictPort: true, 23 | // Matches ddev web_extra_exposed_ports.container_port 24 | port: 3000, 25 | // Strips custom ports from DDEV_PRIMARY_URL if present 26 | origin: `${process.env.DDEV_PRIMARY_URL?.replace(/:\d+$/, '')}:${HTTPS_PORT}`, 27 | allowedHosts: ['.ddev.site'], 28 | cors: { 29 | origin: /https?:\/\/([A-Za-z0-9\-.]+)?(\.ddev\.site)(?::\d+)?$/, 30 | }, 31 | }, 32 | plugins: [ 33 | viteStaticCopy({ 34 | targets: [{ src: 'src/icons/**/*', dest: './assets/icons' }], 35 | }), 36 | ], 37 | } 38 | }) 39 | -------------------------------------------------------------------------------- /web/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | RewriteEngine On 3 | 4 | # Send would-be 404 requests to Craft 5 | RewriteCond %{REQUEST_FILENAME} !-f 6 | RewriteCond %{REQUEST_FILENAME} !-d 7 | RewriteCond %{REQUEST_URI} !^/(favicon\.ico|apple-touch-icon.*\.png)$ [NC] 8 | RewriteRule (.+) index.php?p=$1 [QSA,L] 9 | 10 | -------------------------------------------------------------------------------- /web/cpresources/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /web/index.php: -------------------------------------------------------------------------------- 1 | run(); 13 | -------------------------------------------------------------------------------- /web/web.config: -------------------------------------------------------------------------------- 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 | 26 | 27 | 28 | --------------------------------------------------------------------------------