Initial commit

This commit is contained in:
KirbyCMS Deploy
2026-04-15 11:50:31 +00:00
commit 0a8c107828
1114 changed files with 203788 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 2
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_style = space
+18
View File
@@ -0,0 +1,18 @@
KIRBY_DEBUG=false
REQUIRES_LOGIN=false
KIRBY_CACHE=false
APP_URL=http://localhost:3000
DREAMFORM_SECRET=changeme
PLAUSIBLE_SHARED_LINK=
# MailerSend
KIRBY_MAIL_FROM=baukasten@moeritz.io
KIRBY_MAIL_HOST=smtp.mailersend.net
KIRBY_MAIL_PORT=587
KIRBY_MAIL_USER=
KIRBY_MAIL_PASS=
# Only neccessary for dev environments
VITE_DEV_PORT=3000
KIRBY_DEV_HOSTNAME=127.0.0.1
KIRBY_DEV_PORT=3001
+1
View File
@@ -0,0 +1 @@
github: tobimori
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021-2023 Tobias Möritz
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+168
View File
@@ -0,0 +1,168 @@
![Kirby 4 Baukasten Banner by Johann Schopplich](/.github/banner.png)
# Kirby Baukasten
An opinionated, supercharged version of the Kirby Plainkit used internally at Love & Kindness for our clients' sites, with preconfigured tooling and plugins.
> [!NOTE]
> While Kirby Baukasten is open source & used in production as base for my own projects, it's not properly versioned, and I'm not offering support for it. Instead, it should serve as a reference or guide for implementing certain best practices in your own starterkit.
## Requirements
- PHP 8.2+ with composer
- Node.js 20+ with pnpm
## Usage
##### Install Composer & Node dependencies with `composer install` and `pnpm install`.
```
composer install && pnpm install
```
##### Running the scaffold command with Kirby CLI
```
kirby baukasten:scaffold
```
> NOTE: If you don't have the Kirby CLI installed, you will need to run `composer global require getkirby/cli` first.
##### Start the dev server.
```
pnpm run dev
```
## Best Practices/Features
### Styling ([Tailwind CSS](https://tailwindcss.com/))
Styling is done with [Tailwind CSS](https://tailwindcss.com/) directly in Kirby templates or snippets.
The only pre-made change to the default theme involves switching from the Mobile-first approach to a Desktop-first approach. I do think that this is still the go-to approach for most projects.
#### What does this mean?
- Don’t use `sm:` to target all non-mobile devices
- Use unprefixed utilities to target desktop, and override them at smaller breakpoints
### TypeScript / interactivity ([Stimulus](https://stimulus.hotwired.dev/))
I try to avoid using TypeScript as much as possible, but some things are impossible to solve with just HTML + CSS, which is why I'm following a strict Progressive Enhancement policy when needed.
Since Kirby v4, I switched to [Stimulus](https://stimulus.hotwired.dev/) as framework supporting this approach. It's integrated with server-side templates exactly how I need it to be and very similiar to my own, previously used, "micro framework".
More information can be found in [src ▸ controllers ▸ README.md](src/controllers/README.md).
### Block system & the `$block->attr()` helper
Most of our pages are build in a page-builder-like fashion utilizing the Kirby Blocks field. To make it easier to work with, I've implemented a helper that allows you to deploy a block with a set of base attributes.
```php
<section <?= $block->attr(['class' => 'container']) ?>>
// <section
// class="container"
// id="id-from-fields"
// data-next-block="footer"
// data-prev-block="navigation"
// >
```
This function is part of the [`tobimori/kirby-spielzeug`](https://github.com/tobimori/kirby-spielzeug) plugin, which contains a encapsulated set of helpers & tools I use for my projects and serves as the independent foundation for Baukasten.
### View Transitions ([Taxi.js](https://taxi.js.org/))
When working on fancy sites that use a lot of animations, I use [Taxi.js](https://taxi.js.org/) to go the extra-mile & handle view transitions. It's a very lightweight library that has a nice API and is easy to use.
If you don't want to use Taxi:
- remove the `@unseenco/taxi` JS dependency
- delete the `src/transitions` & `src/renderers` folder
- remove the `data-taxi` & `data-taxi-view` attributes from `layout.php`
- remove the related code from `src/index.ts`
### Code Typing
This template tries to make Kirby development more accessible by adding PHP code
typing and auto completion. Sadly, this doesn't work straight out of the box.
#### Controllers
For controllers & other PHP files, we can add type declarations by importing the classes using
PHP’s `use`:
```php
<?php // site/controllers/article.php
use Kirby\Cms\App;
use Kirby\Cms\Page;
use Kirby\Cms\Site;
return function (Site $site, App $kirby, Page $page) {
[…]
}
```
#### Templates
Templates will receive variables defined by Kirby (like the `$page` and `$kirby`
objects), and any other variable you return in a controller. Unfortunately, we can't
declare them in PHP directly, so we need to
[use the PHPDoc @var tag](https://github.com/php-fig/fig-standards/blob/2668020622d9d9eaf11d403bc1d26664dfc3ef8e/proposed/phpdoc-tags.md#517-var).
```php
<?php // site/templates/article.php
/** @var Kirby\Cms\Page $page */
<h1><?= $page->title() ?></h1>
```
As PHPDoc comments aren't a native PHP feature, this won't affect how our code
runs, although all IDEs and most code editors (like VS Code) should support
them.
#### Page Models
If we're using a
[Page Model](https://getkirby.com/docs/guide/templates/page-models) to expand
Kirby's default page object, we can use it in our templates in the same way.
```php
<?php // site/models/article.php
class ArticlePage extends Kirby\Cms\Page {
public function getArticleBody(): string {
if ($this->content()->body()->isNotEmpty()) {
return $this->content()->body()->markdown();
}
return '';
}
}
```
```php
<?php // site/templates/article.php
/** @var ArticlePage $page */
<h1><?= $page->title() ?></h1>
<?= $page->getArticleBody() ?>
```
For classes reference, check out the
[Kirby reference](https://getkirby.com/docs/reference/objects).
#### Auto completion in VS Code
For excellent PHP support in VS Code, we use
[PHP Intelephense](https://marketplace.visualstudio.com/items?itemName=bmewburn.vscode-intelephense-client).
Follow the Quick Start instructions. Other IDEs like PhpStorm may support this
out-of-the-box.
## License
[MIT License](.github/LICENSE) © 2021-2024 [Tobias Möritz](https://github.com/tobimori)
Thanks to [Johann](https://github.com/johannschopplich) for the cute banner gecko!
Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

+63
View File
@@ -0,0 +1,63 @@
# System files
# ------------
Icon
.DS_Store
# Temporary files
# ---------------
/media/*
!/media/.gitkeep
/public/backups
# Lock files
# ---------------
hot
# Editors
# ---------------
*.sublime-project
*.sublime-workspace
/.idea
/.vscode/*
!/.vscode/extensions.json
!/.vscode/settings.json
# Vendor files
# ---------------
/vendor
/kirby
node_modules
# Build assets
# ---------------
/public/media
/public/assets/dev
/public/dist
/assets/sprite.svg
# Kirby
# ---------------
# Runtime data
/data/runtime/sessions/*
!/data/runtime/sessions/.gitkeep
/data/runtime/logs/*
!/data/runtime/logs/.gitkeep
/data/runtime/cache/*
!/data/runtime/cache/.gitkeep
# Storage
/data/storage/accounts/*
!/data/storage/accounts/.gitkeep
/data/storage/content/*
!/data/storage/content/.gitkeep
/data/storage/.license
/data/storage/retour/*
/site/plugins/*
!/site/plugins/extended
site/plugins/extended/index.dev.mjs
# Env
# ---------------
.env
+1
View File
@@ -0,0 +1 @@
22
+1
View File
@@ -0,0 +1 @@
!*.yml
+6
View File
@@ -0,0 +1,6 @@
{
"semi": false,
"trailingComma": "none",
"useTabs": true,
"tabWidth": 2
}
+14
View File
@@ -0,0 +1,14 @@
{
"recommendations": [
"esbenp.prettier-vscode",
"eamodio.gitlens",
"kisstkondoros.vscode-gutter-preview",
"github.vscode-pull-request-github",
"bmewburn.vscode-intelephense-client",
"editorconfig.editorconfig",
"bradlc.vscode-tailwindcss",
"emeraldwalk.runonsave",
"yoavbls.pretty-ts-errors",
"biomejs.biome"
]
}
+44
View File
@@ -0,0 +1,44 @@
{
"editor.formatOnSave": true,
"intelephense.telemetry.enabled": false,
// Default formatters for each language
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[javascript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[json]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[css]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[tailwindcss]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[php]": {
"editor.defaultFormatter": "bmewburn.vscode-intelephense-client"
},
// YAML schema association
"yaml.schemas": {
"https://json.schemastore.org/yamllint.json": "**/*.yml"
},
// Tailwind CSS experimental settings for custom class detection
"tailwindCSS.experimental.classRegex": ["([a-zA-Z0-9\\-:]+)"],
// Run specific command on save for PHP files
"emeraldwalk.runonsave": {
"commands": [
{
"match": "\\.php$",
"cmd": "pnpm env-cmd --use-shell \"rustywind --write '${file}' --vite-css \\$APP_URL/src/styles/index.css \""
}
]
}
}
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16">
<path fill="currentcolor"
d="M1.2426 8.2426 8 15l6.7574-6.7574a4.2428 4.2428 0 0 0 1.2426-3v-.1903C16 2.8143 14.1857 1 11.9477 1a4.0523 4.0523 0 0 0-3.1644 1.5209L8 3.5l-.7833-.9791A4.0524 4.0524 0 0 0 4.0523 1C1.8143 1 0 2.8143 0 5.0523v.1903c0 1.1253.447 2.2044 1.2426 3Z" />
</svg>

After

Width:  |  Height:  |  Size: 360 B

+24
View File
@@ -0,0 +1,24 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"organizeImports": {
"enabled": true
},
"files": {
"ignore": ["./src/styles/*.css"]
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"formatter": {
"lineWidth": 120
},
"javascript": {
"formatter": {
"semicolons": "asNeeded",
"trailingCommas": "none"
}
}
}
+42
View File
@@ -0,0 +1,42 @@
{
"@schema": "https://getcomposer.org/schema.json",
"name": "tobimori/kirby-baukasten",
"description": "An opinionated, supercharged version of the Kirby CMS Plainkit",
"type": "project",
"keywords": [
"kirby",
"cms",
"starterkit"
],
"homepage": "https://moeritz.io",
"authors": [
{
"name": "Tobias Möritz",
"email": "tobias@moeritz.io",
"homepage": "https://moeritz.io"
}
],
"minimum-stability": "beta",
"require": {
"php": ">=8.2.0",
"getkirby/cms": "^5",
"bnomei/kirby3-dotenv": "^2.0",
"genxbe/kirby3-ray": "^2.0.1",
"tobimori/kirby-seo": "dev-main",
"lukaskleinschmidt/kirby-laravel-vite": "^2.1.1",
"tobimori/kirby-spielzeug": "^2.1.0",
"distantnative/retour-for-kirby": "^5.0",
"getkirby/cli": "^1.2",
"bnomei/kirby3-janitor": "^4.0",
"tobimori/kirby-tailwind-merge": "^3.0",
"tobimori/kirby-thumbhash": "^1.1",
"johannschopplich/kirby-plausible": "^0.5.0"
},
"config": {
"optimize-autoloader": true,
"allow-plugins": {
"getkirby/composer-installer": true,
"php-http/discovery": true
}
}
}
+3758
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -0,0 +1,4 @@
# data
This folder contains all data stored or generated by Kirby.
There are categorized in two subfolders: `runtime` and `storage`.
@@ -0,0 +1,3 @@
# data > runtime
This folder contains all data generated automatically by Kirby at runtime that doesn't necessarily need to be persisted.
View File
@@ -0,0 +1,3 @@
# data > storage
This folder contains all data generated through user-input in the Kirby panel, such as the sites' content or accounts.
+53
View File
@@ -0,0 +1,53 @@
{
"private": true,
"type": "module",
"scripts": {
"dev": "pnpm run \"/dev:/\"",
"dev:kirby": "env-cmd --use-shell \"PHP_CLI_SERVER_WORKERS=4 php -S \\$KIRBY_DEV_HOSTNAME:\\$KIRBY_DEV_PORT -t public kirby/router.php\"",
"dev:vite": "vite",
"dev:plugin": "cd site/plugins/extended && kirbyup serve src/index.js",
"build": "pnpm run \"/build:/\"",
"build:site": "vite build",
"build:plugin": "cd site/plugins/extended && kirbyup src/index.js"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@rollup/plugin-inject": "^5.0.5",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/vite": "4.0.3",
"@total-typescript/ts-reset": "^0.6.1",
"@types/node": "^22.13.0",
"autoprefixer": "^10.4.20",
"browserslist": "^4.24.4",
"env-cmd": "^10.1.0",
"kirbyup": "^3.2.0",
"laravel-vite-plugin": "^1.2.0",
"lightningcss": "^1.29.1",
"postcss": "^8.5.1",
"prettier": "^3.4.2",
"tailwindcss": "^4.0.3",
"typescript": "^5.7.3",
"vite": "^6.0.11",
"vite-svg-sprite-wrapper": "^1.4.1",
"vite-tsconfig-paths": "^5.1.4"
},
"dependencies": {
"@hotwired/stimulus": "^3.2.2",
"@unseenco/taxi": "^1.8.0",
"css-clamper": "^0.2.0",
"htmx.org": "^1.9.12",
"idiomorph": "^0.4.0",
"stimulus-typescript": "^0.1.3",
"stimulus-use": "^0.52.3",
"unlazy": "^0.12.1"
},
"browserslist": [
"last 2 versions",
">= 0.4%",
"not dead",
"Firefox ESR",
"not op_mini all",
"not and_uc > 0"
],
"packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0"
}
+3815
View File
File diff suppressed because it is too large Load Diff
+28
View File
@@ -0,0 +1,28 @@
<?php
use Kirby\Cms\App;
define("KIRBY_HELPER_ATTR", false); // attr is set by tailwind-merge
define("KIRBY_HELPER_DUMP", false); // dump is set by ray
$base = dirname(__DIR__);
require $base . '/kirby/bootstrap.php';
require $base . '/vendor/autoload.php';
$kirby = new App([
'roots' => [
'index' => __DIR__,
'base' => $base,
'site' => $base . '/site',
'data' => $data = $base . '/data',
'content' => $data . '/storage/content',
'accounts' => $data . '/storage/accounts',
'license' => $data . '/storage/.license',
'cache' => $data . '/runtime/cache',
'logs' => $data . '/runtime/logs',
'sessions' => $data . '/runtime/sessions',
]
]);
echo $kirby->render();
@@ -0,0 +1,23 @@
<svg viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<style>
.logo {
fill: #000000;
}
@media (prefers-color-scheme: dark) {
.logo {
fill: #ffffff;
}
}
</style>
<path
d="m20.428 0 15.429 8.998v18.004L20.429 36 5 27.002V8.998L20.428 0ZM6.714 10.023v15.954l13.714 8 13.715-8V10.023l-13.715-8-13.714 8Z"
class="logo" />
<path
d="M27.286 18.002 23 20.221v.351h4.286v3.428H13.572v-3.428h4.286v-.375l-4.286-2.197v-4.285l6.857 3.683 6.857-3.683"
class="logo" />
<rect x="16" y="26" width="24" height="14" rx="3" fill="#FAD6D6" />
<path
d="M24.41 33.45c0 .98-.1 2.04-.83 2.79-.47.49-1.18.76-1.99.76h-2.76v-7.09h2.76c.81 0 1.52.27 1.99.76.73.75.83 1.8.83 2.78Zm-1.31 0c0-.65-.01-1.48-.51-1.98-.24-.24-.59-.4-1-.4h-1.47v4.77h1.47c.41 0 .76-.16 1-.4.5-.5.51-1.34.51-1.99ZM30.716 37h-5.05v-7.09h5.05v1.17h-3.76v1.65h3.31v1.16h-3.31v1.94h3.76V37Zm6.5-7.09L34.885 37h-1.42l-2.35-7.09h1.37l1.72 5.41 1.71-5.41h1.3Z"
fill="#801919" />
</svg>

After

Width:  |  Height:  |  Size: 1023 B

@@ -0,0 +1,23 @@
<svg viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<style>
.logo {
fill: #000000;
}
@media (prefers-color-scheme: dark) {
.logo {
fill: #ffffff;
}
}
</style>
<path
d="m20.428 0 15.429 8.998v18.004L20.429 36 5 27.002V8.998L20.428 0ZM6.714 10.023v15.954l13.714 8 13.715-8V10.023l-13.715-8-13.714 8Z"
class="logo" />
<path
d="M27.286 18.002 23 20.221v.351h4.286v3.428H13.572v-3.428h4.286v-.375l-4.286-2.197v-4.285l6.857 3.683 6.857-3.683"
class="logo" />
<rect x="15" y="26" width="25" height="14" rx="3" fill="#CEE7CE" />
<path
d="M22.41 37h-4.58v-7.09h1.29v5.87h3.29V37Zm2.228 0h-1.31v-7.09h1.31V37Zm6.929-7.09L29.237 37h-1.42l-2.35-7.09h1.37l1.72 5.41 1.71-5.41h1.3ZM37.45 37H32.4v-7.09h5.05v1.17h-3.76v1.65H37v1.16h-3.31v1.94h3.76V37Z"
fill="#2E5C2E" />
</svg>

After

Width:  |  Height:  |  Size: 862 B

@@ -0,0 +1,23 @@
<svg viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<style>
.logo {
fill: #000000;
}
@media (prefers-color-scheme: dark) {
.logo {
fill: #ffffff;
}
}
</style>
<path
d="m20.428 0 15.429 8.998v18.004L20.429 36 5 27.002V8.998L20.428 0ZM6.714 10.023v15.954l13.714 8 13.715-8V10.023l-13.715-8-13.714 8Z"
class="logo" />
<path
d="M27.286 18.002 23 20.221v.351h4.286v3.428H13.572v-3.428h4.286v-.375l-4.286-2.197v-4.285l6.857 3.683 6.857-3.683"
class="logo" />
<rect x="15" y="26" width="25" height="14" rx="3" fill="#CEE7CE" />
<path
d="M22.41 37h-4.58v-7.09h1.29v5.87h3.29V37Zm2.228 0h-1.31v-7.09h1.31V37Zm6.929-7.09L29.237 37h-1.42l-2.35-7.09h1.37l1.72 5.41 1.71-5.41h1.3ZM37.45 37H32.4v-7.09h5.05v1.17h-3.76v1.65H37v1.16h-3.31v1.94h3.76V37Z"
fill="#2E5C2E" />
</svg>

After

Width:  |  Height:  |  Size: 862 B

@@ -0,0 +1,15 @@
preview: fields
wysiwyg: true
tabs:
content:
label: tab.content.label
fields:
advanced:
label: tab.advanced.label
fields:
blockId:
label: field.blockId.label
type: slug
before: "#"
icon: false
@@ -0,0 +1,11 @@
extends: blocks/_base
title: example
icon: title
label: "{{ text }}"
tabs:
content:
fields:
text:
label: text
type: text
@@ -0,0 +1,4 @@
label: field.blocks.label
type: blocks
fieldsets:
- example
@@ -0,0 +1,2 @@
extends: fields/link
label: field.button.label
@@ -0,0 +1,10 @@
label: field.image.label
type: files
uploads:
parent: site.find('page://images')
template: image
multiple: false
image:
cover: false
info: "{{ file.alt }}"
query: site.find('page://images').images.filterBy('template', 'image')
@@ -0,0 +1,34 @@
label: field.link.label
type: object
empty: field.link.empty
columns:
label:
width: 1/2
link:
width: 1/2
fields:
label:
label: field.link.label.label
type: text
width: 1/3
link:
label: field.link.link.label
type: link
width: 2/3
options:
- url
- page
- file
- email
- tel
newTab:
label: field.link.newTab.label
type: toggle
width: 1/3
default: false
anchor:
label: field.link.anchor.label
type: slug
icon: false
width: 2/3
before: "#"
@@ -0,0 +1,12 @@
extends: fields/writer
inline: false
nodes:
- paragraph
- heading
- bulletList
- orderedList
headings:
- 2
- 3
- 4
- 5
@@ -0,0 +1,7 @@
label: field.text.label
type: writer
marks:
- bold
- link
- email
inline: true
@@ -0,0 +1,4 @@
title: file.file.title
fields:
hidden: true
@@ -0,0 +1,10 @@
title: file.image.title
accept:
extension: jpg, jpeg, png, svg
type: image
fields:
alt:
label: file.image.alt.label
type: text
help: file.image.alt.help
@@ -0,0 +1,4 @@
extends: pages/home
title: page.error.title
image:
icon: cancel
@@ -0,0 +1,16 @@
extends: pages/images
title: page.files.titles
image:
icon: file-word
sections:
files:
label: section.files.label
type: files
template: file
layout: table
limit: 60
uploads:
template: file
search: true
sortBy: filename desc
@@ -0,0 +1,25 @@
extends: pages/layout
title: page.home.title
image:
icon: home
options:
# access
# changeSlug
# changeStatus
# changeTemplate
# changeTitle
# create
delete: false
duplicate: false
# list
move: false
# preview
# sort
# update
tabs:
content:
columns:
sidebar: false
@@ -0,0 +1,40 @@
title: page.images.title
image:
icon: file-image
back: "transparent"
color: "var(--color-gray-600)"
query: false
options:
# access
changeSlug: false
changeStatus: false
# changeTemplate
changeTitle: false
# create
delete: false
duplicate: false
# list
move: false
preview: false
# sort
update: false
status:
draft: false
unlisted: true
listed: false
sections:
files:
label: section.images.label
type: files
layout: cardlets
template: image
uploads:
template: image
search: true
sortBy: filename desc
info: '{{ file.alt.isNotEmpty ? file.filename : "" }}'
text: "{{ file.alt.or(file.filename) }}"
limit: 100
@@ -0,0 +1,39 @@
title: page.layout.title
image:
icon: grid
back: "var(--theme-accent)"
color: "var(--theme-primary)"
options:
# access
# changeSlug
# changeStatus
# changeTemplate
# changeTitle
# create
# delete
# duplicate
# list
# move
# preview
# sort
# update
tabs:
content:
label: tab.content.label
icon: text
columns:
main:
width: 3/4
type: fields
fields:
blocks: fields/blocks
sidebar:
width: 1/4
sections:
children:
label: section.children.label
type: pages
template: layout
seo: seo/page
@@ -0,0 +1,22 @@
title: site.title
tabs:
pages:
label: tab.pages.label
icon: page
columns:
- width: 2/3
sections:
pages:
label: field.pages.label
type: pages
info: "/{{ page.slug }}"
create:
- layout
templates:
- home
- error
- layout
image: icon
navigation: tabs/navigation
seo: seo/site
@@ -0,0 +1,26 @@
icon: bars
label: tab.navigation.label
columns:
- width: 1/4
fields:
_navigation:
label: field.navigation.label
type: headline
- width: 3/4
sections:
placeholder: &placeholder
headline: Placeholder
type: info
text: " "
- width: 1
fields:
_line1: &line
type: line
- width: 1/4
fields:
_footer:
label: field.footer.label
type: headline
- width: 3/4
sections:
placeholder: *placeholder
@@ -0,0 +1,7 @@
title: user.admin.title
description: user.admin.description
fields:
favorites:
label: user.favorites.label
type: pages
@@ -0,0 +1,10 @@
extends: users/admin
title: user.editor.title
description: user.editor.description
permissions:
access:
system: false
languages: false
backups: false
users: false
@@ -0,0 +1,80 @@
<?php
use Kirby\Cms\Pages;
use Kirby\Http\Url;
use tobimori\Spielzeug\Menu;
require_once dirname(__DIR__) . '/plugins/kirby3-dotenv/global.php';
loadenv([
'dir' => realpath(dirname(__DIR__, 2)),
'file' => '.env',
]);
return [
'debug' => json_decode(env('KIRBY_DEBUG')),
'routes' => require __DIR__ . '/routes.php',
'thumbs' => require __DIR__ . '/thumbs.php',
'yaml.handler' => 'symfony',
'date.handler' => 'intl',
'url' => env("APP_URL"),
'tobimori.seo' => require __DIR__ . '/seo.php',
'tobimori.dreamform' => require __DIR__ . '/dreamform.php',
'tobimori.icon-field' => [
// use generated sprite, requires pnpm run build
// to generate (dev sprite doesn't work since it's not in the public folder)
'folder' => '',
'sprite' => fn() => Url::path(vite()->useHotFile('.never')->asset('assets/sprite.svg'))
],
/** Email */
'email' => require __DIR__ . '/email.php',
'auth' => [
'methods' => ['password', 'password-reset'],
'challenge' => [
'email' => [
'from' => env("KIRBY_MAIL_FROM"),
'subject' => 'Login-Code'
]
]
],
/** Caching */
'cache' => [
'pages' => [
'active' => json_decode(env('KIRBY_CACHE')),
]
],
/** Build Env / Vite / etc. */
'bnomei.dotenv.dir' => fn() => realpath(kirby()->roots()->base()),
'lukaskleinschmidt.kirby-laravel-vite.buildDirectory' => 'dist',
/** Panel */
'johannschopplich.plausible.sharedLink' => env('PLAUSIBLE_SHARED_LINK'),
'distantnative' => [
'retour' => [
'config' => __DIR__ . './../../data/storage/retour/config.yml',
'database' => __DIR__ . './../../data/storage/retour/log.sqlite'
]
],
'panel' => [
'menu' => fn() => array_merge(
[
'site' => Menu::site(),
],
Menu::favorites(kirby()->user()?->favorites()->toPages() ?? new Pages([])),
[
'-',
'images' => Menu::page(null, 'file-image', page('page://images')),
'files' => Menu::page(null, 'file-word', page('page://files')),
'-',
'users',
'plausible',
'retour',
]
)
],
'ready' => fn() => [
'panel' => [
'favicon' => option('debug') ? 'static/panel/favicon-dev.svg' : 'static/panel/favicon-live.svg',
'css' => vite('src/styles/panel.css'),
],
]
];
@@ -0,0 +1,12 @@
<?php
return [
'multiStep' => false,
'mode' => 'htmx',
'useDataAttributes' => true,
'precognition' => true,
'secret' => env('DREAMFORM_SECRET'),
'guards' => [
'available' => ['honeypot']
],
];
@@ -0,0 +1,13 @@
<?php
return [
'transport' => [
'type' => 'smtp',
'host' => env("KIRBY_MAIL_HOST"),
'port' => json_decode(env("KIRBY_MAIL_PORT")),
'security' => true,
'auth' => 'tls',
'username' => env("KIRBY_MAIL_USER"),
'password' => env("KIRBY_MAIL_PASS")
]
];
@@ -0,0 +1,3 @@
<?php
return [];
+16
View File
@@ -0,0 +1,16 @@
<?php
return [
'robots' => [
'pageSettings' => false
],
'canonicalBase' => env("APP_URL"),
'files' => [
'parent' => 'site.find("page://images")',
'template' => 'image'
],
'socialMedia' => [
'twitter' => false,
'youtube' => false
],
];
@@ -0,0 +1,9 @@
<?php
return [
'srcsets' => [
'small' => [300, 500, 800],
'default' => [400, 800, 1000, 1200],
'large' => [800, 1400, 2000, 2600],
]
];
@@ -0,0 +1,15 @@
<?php
use Kirby\Data\Yaml;
return [
'code' => 'de',
'default' => true,
'direction' => 'ltr',
'locale' => [
'LC_ALL' => 'de_DE'
],
'name' => 'Deutsch',
'translations' => Yaml::read(__DIR__ . '/../translations/de.yml'),
'url' => ''
];
@@ -0,0 +1,15 @@
<?php
use Kirby\Data\Yaml;
return [
'code' => 'en',
'default' => false,
'direction' => 'ltr',
'locale' => [
'LC_ALL' => 'en_US'
],
'name' => 'English',
'translations' => Yaml::read(__DIR__ . '/../translations/en.yml'),
'url' => '/en'
];
@@ -0,0 +1,23 @@
<?php
use Kirby\Cms\Page;
use Kirby\Content\Field;
class FilesPage extends Page
{
public function metaDefaults()
{
return [
'robotsIndex' => false
];
}
/**
* Override the page title to be static
* to the template name
*/
public function title(): Field
{
return new Field($this, 'title', t("page.{$this->intendedTemplate()->name()}.title"));
}
}
@@ -0,0 +1,5 @@
<?php
class ImagesPage extends FilesPage
{
}
@@ -0,0 +1,5 @@
<?php
class VideosPage extends FilesPage
{
}
@@ -0,0 +1,13 @@
<?php
use Kirby\Cms\App;
App::plugin('project/extended', []);
// helpers
if (!function_exists('icon')) {
function icon(string $type, array|string|null $class = null, array $attr = []): string
{
return snippet('icon', compact('type', 'class', 'attr'), true);
}
}
@@ -0,0 +1 @@
panel.plugin("project/extended", {})
@@ -0,0 +1,5 @@
<?php
/** @var Kirby\Cms\Site $site */ ?>
<footer></footer>
@@ -0,0 +1,5 @@
<?php
/** @var Kirby\Cms\Site $site */ ?>
<header></header>
@@ -0,0 +1,7 @@
<?php
/** @var Kirby\Cms\Site $site */ ?>
<a class="text-white bg-black border-white outline-none sr-only focus-visible:fixed focus-visible:top-4 focus-visible:left-4 focus-visible:py-2.5 focus-visible:px-4 focus-visible:not-sr-only" href="#main">
<?= t('aria.skipNavigation') ?>
</a>
@@ -0,0 +1,12 @@
<?php
use Kirby\Toolkit\A;
if (isset($type)) : ?>
<svg <?= attr(A::merge($attr ?? [], [
'aria-hidden' => true,
'class' => cls($class ?? 'icon')
])) ?>>
<use xlink:href="<?= vite()->asset('assets/sprite.svg') ?>#<?= $type ?>"></use>
</svg>
<?php endif ?>
@@ -0,0 +1,41 @@
<?php
/** @var Kirby\Cms\Page $page
* @var Kirby\Cms\Site $site
* @var Kirby\Cms\App $kirby */
if (json_decode(env('REQUIRES_LOGIN')) && !$kirby->user()) {
go('/panel');
} ?>
<!DOCTYPE html>
<html lang="<?= $kirby->language()?->code() ?>">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<?php snippet('seo/head') ?>
<link rel="icon" href="/favicon.svg" />
<link rel="mask-icon" href="/favicon.svg" color="#000000" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<meta name="theme-color" content="#000000">
<?= vite([
'src/styles/index.css',
'src/index.ts'
]) ?>
</head>
<body class="flex flex-col min-h-screen antialiased overflow-x-clip">
<?php snippet('core/skip-nav') ?>
<?php snippet('core/nav') ?>
<main class="flex-grow">
<div id="main"></div>
<?= $slot ?>
</main>
<?php snippet('core/footer') ?>
<?php snippet('seo/schemas') ?>
</body>
</html>
@@ -0,0 +1,45 @@
<?php
/** @var Kirby\Cms\Page $page
* @var Kirby\Cms\Site $site
* @var Kirby\Cms\App $kirby */
if (json_decode(env('REQUIRES_LOGIN')) && !$kirby->user()) {
go('/panel');
} ?>
<!DOCTYPE html>
<html lang="<?= $kirby->language()?->code() ?>">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<?php snippet('seo/head') ?>
<link rel="icon" href="/favicon.svg" />
<link rel="mask-icon" href="/favicon.svg" color="#000000" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<meta name="theme-color" content="#000000">
<?= vite([
'src/styles/index.css',
'src/index.ts'
]) ?>
</head>
<body class="min-h-screen antialiased overflow-x-clip">
<?php snippet('core/skip-nav') ?>
<?php snippet('core/nav') ?>
<div data-taxi>
<div class="flex flex-col" data-taxi-view>
<main class="flex-grow">
<div id="main"></div>
<?= $slot ?>
</main>
<?php snippet('core/footer') ?>
</div>
</div>
<?php snippet('seo/schemas') ?>
</body>
</html>
@@ -0,0 +1,18 @@
<?php
/**
* @var Kirby\Cms\StructureObject|Kirby\Content\Content $link
*/ ?>
<?php if ($link->link()->isNotEmpty()) : ?>
<a <?= attr([
'class' => $class ?? '',
'href' => $link->link()->toUrl(),
'target' => $link->newTab()->toBool() ? '_blank' : null,
'rel' => $link->newTab()->toBool() ? 'noopener' : null,
'aria-current' => $link->link()->linkType() === 'page' && $link->link()->toPage()?->isActive() ? 'page' : null,
...($attr ?? [])
]) ?>>
<?= $slot ?? $link->label()->or($link->link()->linkTitle()) ?>
</a>
<?php endif ?>
@@ -0,0 +1,97 @@
<?php
/**
* @var Kirby\Cms\File|Kirby\Filesystem\Asset $image The image to display
* @var string|null $alt An optional alt text, defaults to the file's alt text
* @var int|null $ratio Specify a ratio for the image crop
*
* @var array|null $formats Which formats to render (avif, jpeg, etc.)
* @var string|null $preset Which srcset preset to use
* @var string|null $sizes The sizes attribute for the <img> element
*
* @var string|array|null $class Additional classes for the <picture> element
* @var string|array|null $imgClass Additional classes for the <img> element
* @var array|null $attr Additional attributes for the <picture> element
*
* @var bool|null $clientBlur Whether to use client-side blurhash/thumbhash, defaults to true
* @var bool|null $lazy Whether to use lazy loading, defaults to true
* */
use Kirby\Toolkit\A;
$focus ??= is_a($image, 'Kirby\Cms\File') ? $image?->focus() ?? 'center' : 'center';
$ratio ??= null;
$preset ??= 'default';
$clientBlur ??= true;
$attr ??= [];
$formats ??= ['webp', $image?->extension()];
$lazy ??= true;
if (is_a($image, 'Kirby\Cms\File') || is_a($image, 'Kirby\Filesystem\Asset')) : ?>
<picture <?= attr([
'class' => ['block', $class ?? ''],
'style' => '--ratio: ' . ($ratio ?? round($image->ratio(), 2)) . ';',
...$attr
]) ?>>
<?php if ($image->extension() == 'svg') : ?>
<?= svg($image) ?>
<?php else : ?>
<?php foreach ($formats as $format) :
$widths = option("thumbs.srcsets.{$preset}");
$srcset = [];
$median = $ratio ? $image->crop(median($widths), floor(median($widths) / $ratio)) : $image->resize(median($widths)); ?>
<?php
// Generate srcset array
foreach ($widths as $width) {
$srcset[$width . 'w'] = [
'width' => $width,
'height' => $ratio ? floor($width / $ratio) : null,
'crop' => $ratio ? true : false,
'format' => $format
];
} ?>
<?php if ($lazy) : ?>
<source <?= attr([
'type' => "image/{$format}",
'data-srcset' => $image->srcset($srcset),
'data-sizes' => $sizes ?? 'auto',
]) ?>>
<?php else : ?>
<source <?= attr([
'type' => "image/{$format}",
'srcset' => $image->srcset($srcset),
'sizes' => $sizes ?? '100vw'
]) ?>>
<?php endif; ?>
<?php endforeach; ?>
<img <?= attr([
'data-thumbhash' => $clientBlur ? $image->th() : null,
'src' => !$clientBlur ? $image->thUri() : null,
'data-src' => $median->url(),
'width' => $image->width(),
'height' => $ratio ? floor($image->width() / $ratio) : $image->height(),
'alt' => $alt ?? (is_a($image, 'Kirby\Cms\File') ? $image->alt() : null),
'loading' => $lazy ? "lazy" : null,
'data-sizes' => $sizes ?? 'auto',
'class' => cls(['size-full object-cover', $imgClass ?? ' ']),
'style' => A::join([
"aspect-ratio: " . ($ratio ?? $image->ratio()),
"object-position: {$focus}"
], '; '),
]) ?>>
<?php endif ?>
</picture>
<?php
// Dummy element that will be rendered when specified image is not an image
else : ?>
<picture <?= attr(['class' => ['block', $class ?? ''], ...$attr]) ?>></picture>
<?php endif ?>
@@ -0,0 +1,2 @@
<?php $kirby->response()->code(404); // set status code
echo $site->errorPage()->render(); // render 404 if unmatched template
@@ -0,0 +1,3 @@
<?php
require __DIR__ . '/layout.php';
@@ -0,0 +1,3 @@
<?php
require __DIR__ . '/layout.php';
@@ -0,0 +1,11 @@
<?php
/**
* @var Kirby\Cms\App $kirby
* @var Kirby\Cms\Page $page
* @var Kirby\Cms\Site $site
*/
snippet('layout', slots: true); ?>
<?= $page->blocks()->toBlocks() ?>
<?php endsnippet() ?>
@@ -0,0 +1,69 @@
# ARIA
aria.skipNavigation: Navigation überspringen
# Social Media
twitter: Twitter
facebook: Facebook
instagram: Instagram
linkedin: LinkedIn
youtube: YouTube
vimeo: Vimeo
# Roles
user.admin.title: Administrator
user.admin.description: Hat Zugriff auf alle Inhalte und administrative Tools.
user.editor.title: Editor
user.editor.description: Kann alle Inhalte bearbeiten, aber keine administrativen Tools nutzen. (z.B. Nutzerverwaltung)
# Tabs
tab.content.label: Inhalt
tab.advanced.label: Erweitert
tab.pages.label: Seiten
tab.navigation.label: Navigation & Footer
# Files
file.image.title: Bild
file.image.alt.label: Alternativer Text/Bildbeschreibung
file.image.alt.help: Der alternative Text soll Nutzern mit Seheinschränkung & Suchmaschinen-Crawlern helfen, den Inhalt des Bildes zu verstehen.
file.video.title: Video
file.file.title: Dokument
# Pages
page.layout.title: Layout-Seite
page.error.title: Fehlerseite
page.home.title: Startseite
page.images.title: Bilder
page.videos.title: Videos
page.files.title: Dokumente
# Fields
field.blocks.label: Inhalt
field.image.label: Bild
field.link.label: Link
field.link.label.label: Beschriftung
field.link.link.label: Link
field.link.newTab.label: In neuem Tab öffnen?
field.link.empty: Kein Link angegeben
field.link.anchor.label: Anker (Block-ID)
field.button.label: Button
field.blockId.label: Eindeutige ID (Anker)
field.pages.label: Seiten
field.text.label: Text
field.navigation.label: Navigation
field.footer.label: Footer
# Blocks
block.example.title: Block
# Sections
section.images.label: Alle Bilder
section.videos.label: Alle Videos
section.files.label: Alle Dokumente
section.children.label: Unterseiten
# Other
site.title: Dashboard
@@ -0,0 +1,69 @@
# ARIA
aria.skipNavigation: Skip navigation
# Social Media
twitter: Twitter
facebook: Facebook
instagram: Instagram
linkedin: LinkedIn
youtube: YouTube
vimeo: Vimeo
# Roles
user.admin.title: Administrator
user.admin.description: Has access to all content and administrative tools.
user.editor.title: Editor
user.editor.description: Can edit all content but cannot use administrative tools (e.g., user management).
# Tabs
tab.content.label: Content
tab.advanced.label: Advanced
tab.pages.label: Pages
tab.navigation.label: Navigation & Footer
# Files
file.image.title: Image
file.image.alt.label: Alternative Text/Image Description
file.image.alt.help: The alternative text helps visually impaired users & search engine crawlers understand the content of the image.
file.video.title: Video
file.file.title: Document
# Pages
page.layout.title: Layout Page
page.error.title: Error Page
page.home.title: Home Page
page.images.title: Images
page.videos.title: Videos
page.files.title: Documents
# Fields
field.blocks.label: Content
field.image.label: Image
field.link.label: Link
field.link.label.label: Label
field.link.link.label: Link
field.link.newTab.label: Open in new tab?
field.link.empty: No link provided
field.link.anchor.label: Anchor (Block ID)
field.button.label: Button
field.blockId.label: Unique ID (Anchor)
field.pages.label: Pages
field.text.label: Text
field.navigation.label: Navigation
field.footer.label: Footer
# Blocks
block.example.title: Block
# Sections
section.images.label: All Images
section.videos.label: All Videos
section.files.label: All Documents
section.children.label: Subpages
# Other
site.title: Dashboard
@@ -0,0 +1,20 @@
# src > controllers
This folder contains controller files for [https://stimulus.hotwired.dev/](Stimulus) framework. They will be autoloaded by a script included in index.ts.
The naming conventions are different from the Stimulus default. Please do not append `*_controller.ts` at the end of the filename. Instead:
- Use hyphens to separate words
- Just use your desired controller name as the filename
→ `hello-world.ts`, not `hello_world_controller.ts`
## Lazy loading
When importing large libraries, it is recommended to lazy load them. This can be done by appending `.lazy` to the filename. Thanks to ES Modules, the file will then be loaded only when the controller is used for the first time.
→ `hello-world.lazy.ts`
## TypeScript
We use [stimulus-typescript](https://github.com/ajaishankar/stimulus-typescript) for strongly typed Stimulus controllers.
@@ -0,0 +1,8 @@
import { Controller } from "@hotwired/stimulus"
import { Typed } from "stimulus-typescript"
export default class extends Typed(Controller, {}) {
connect() {
console.log("Hello world!")
}
}
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+66
View File
@@ -0,0 +1,66 @@
import "htmx.org"
import "idiomorph/dist/idiomorph-ext.js"
import { Application, AttributeObserver } from "@hotwired/stimulus"
import type { ControllerConstructor } from "@hotwired/stimulus"
import { lazyLoad } from "unlazy"
import.meta.glob(["../assets/**"]) // Import all assets for copying them to dist
console.log(
"%cMade with Kirby and ❤️ by Love & Kindness GmbH",
"font-size: 12px; font-weight: bold; color: #fff; background-color: #000; padding: 8px 12px; margin: 4px 0; border-radius: 4px;"
)
declare global {
interface Window {
Stimulus: Application
}
}
// Register Stimulus & controllers
window.Stimulus = Application.start()
// Register all eager controllers in the controllers folder
for (const [key, controller] of Object.entries(
import.meta.glob(["./controllers/*.ts", "!./controllers/*.lazy.ts"], { eager: true })
)) {
window.Stimulus.register(key.slice(14, -3), (controller as { default: ControllerConstructor }).default)
}
// Register observer & lazy controllers
const lazyControllers = Object.entries(import.meta.glob("./controllers/*.lazy.ts")).reduce(
(prev, [key, controller]) => {
prev.set(key.slice(14, -8), controller as () => Promise<{ default: ControllerConstructor }>)
return prev
},
new Map<string, () => Promise<{ default: ControllerConstructor }>>()
)
const loadController = async (element: HTMLElement) => {
// data-controller attribute can contain multiple controllers
const controllerNames = element
.getAttribute(window.Stimulus.schema.controllerAttribute ?? "data-controller")
?.split(/\s+/)
for (const name of controllerNames ?? []) {
// If the controller is not registered yet, register it
if (!window.Stimulus.router.modules.some((module) => module.definition.identifier === name)) {
const controllerDefinition = (await lazyControllers.get(name)?.())?.default
if (controllerDefinition) window.Stimulus.register(name, controllerDefinition)
}
}
}
const controllerObserver = new AttributeObserver(window.Stimulus.element, window.Stimulus.schema.controllerAttribute, {
elementMatchedAttribute: loadController,
elementAttributeValueChanged: loadController
})
controllerObserver.start()
if (import.meta.env.DEV) {
window.Stimulus.debug = true
}
lazyLoad()
@@ -0,0 +1,81 @@
import "htmx.org"
import "idiomorph/dist/idiomorph-ext.js"
import { Core } from "@unseenco/taxi"
import { Application, AttributeObserver } from "@hotwired/stimulus"
import type { ControllerConstructor } from "@hotwired/stimulus"
import type { Transition, Renderer } from "@unseenco/taxi"
import.meta.glob(["../assets/**"]) // Import all assets for copying them to dist
declare global {
interface Window {
Stimulus: Application
Taxi: Core
}
}
// Register Stimulus & controllers
window.Stimulus = Application.start()
// Register all eager controllers in the controllers folder
for (const [key, controller] of Object.entries(
import.meta.glob(["./controllers/*.ts", "!./controllers/*.lazy.ts"], { eager: true })
)) {
window.Stimulus.register(key.slice(14, -3), (controller as { default: ControllerConstructor }).default)
}
// Register observer & lazy controllers
const lazyControllers = Object.entries(import.meta.glob("./controllers/*.lazy.ts")).reduce(
(prev, [key, controller]) => {
prev.set(key.slice(14, -8), controller as () => Promise<{ default: ControllerConstructor }>)
return prev
},
new Map<string, () => Promise<{ default: ControllerConstructor }>>()
)
const loadController = async (element: HTMLElement) => {
// data-controller attribute can contain multiple controllers
const controllerNames = element
.getAttribute(window.Stimulus.schema.controllerAttribute ?? "data-controller")
?.split(/\s+/)
for (const name of controllerNames ?? []) {
// If the controller is not registered yet, register it
if (!window.Stimulus.router.modules.some((module) => module.definition.identifier === name)) {
const controllerDefinition = (await lazyControllers.get(name)?.())?.default
if (controllerDefinition) window.Stimulus.register(name, controllerDefinition)
}
}
}
const controllerObserver = new AttributeObserver(window.Stimulus.element, window.Stimulus.schema.controllerAttribute, {
elementMatchedAttribute: loadController,
elementAttributeValueChanged: loadController
})
controllerObserver.start()
if (import.meta.env.DEV) {
window.Stimulus.debug = true
}
// Install Taxi
window.Taxi = new Core({
renderers: Object.entries(import.meta.glob("./renderers/*.ts", { eager: true })).reduce(
(acc, [key, transition]) => {
acc[key.slice(12, -3)] = (transition as { default: typeof Renderer }).default
return acc
},
{} as Record<string, typeof Renderer>
),
transitions: Object.entries(import.meta.glob("./transitions/*.ts", { eager: true })).reduce(
(acc, [key, transition]) => {
acc[key.slice(14, -3)] = (transition as { default: typeof Transition }).default
return acc
},
{} as Record<string, typeof Transition>
),
allowInterruption: true,
removeOldContent: false
})
@@ -0,0 +1,3 @@
# src > renderers
This folder contains renderers for use with [https://taxi.js.org/renderers/](Taxi.js).
@@ -0,0 +1,13 @@
import { Renderer } from "@unseenco/taxi"
import { lazyLoad } from "unlazy"
export default class extends Renderer {
initialLoad() {
lazyLoad()
}
onEnterCompleted() {
this.remove()
lazyLoad()
}
}
+2
View File
@@ -0,0 +1,2 @@
// Do not add any other lines of code to this file!
import "@total-typescript/ts-reset"
+15
View File
@@ -0,0 +1,15 @@
@font-face {
font-family: 'Inter';
src: url('/assets/fonts/inter/normal.woff2') format('woff2');
font-weight: 100 900;
font-display: swap;
font-style: normal;
}
@font-face {
font-family: 'Inter';
src: url('/assets/fonts/inter/italic.woff2') format('woff2');
font-weight: 100 900;
font-display: swap;
font-style: italic;
}
+39
View File
@@ -0,0 +1,39 @@
@import './fonts.css';
@import 'tailwindcss';
@plugin "@tailwindcss/forms";
@source "../../site/**/*.php";
@theme {
/* Fonts */
--font-family-*: initial;
--font-family-sans: 'Inter', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
/* Reset Breakpoints to fix sorting */
--breakpoint-*: initial;
/* Colors */
--color-*: initial;
/* neutrals */
--color-black: #000;
--color-white: #fff;
}
@layer base {
svg {
@apply w-full h-full;
}
/* Fix Safari issue related to <summary> / <details> arrow */
details > summary.list-none::-webkit-details-marker,
details > summary.list-none::marker {
display: none;
}
/* https://defensivecss.dev/tip/flexbox-min-content-size/ */
* {
min-height: 0;
min-width: 0;
}
}
+76
View File
@@ -0,0 +1,76 @@
@import './fonts.css';
:root {
@apply antialiased;
}
/* Panel Max Width */
@media (min-width: 60rem) {
.k-panel-main {
max-width: 112rem;
margin-inline: auto;
padding-inline-start: calc(var(--main-start, 0) + var(--main-padding-inline));
}
}
/* Custom login form styling */
.k-login-view {
position: relative;
.k-login-dialog {
overflow: visible;
&::before {
--height: 1.8rem;
--width: 2.7rem;
bottom: calc(100% + 2rem);
display: block;
height: var(--height);
width: var(--width);
margin: 0 auto;
left: 0.25rem;
position: absolute;
content: var(--logo);
}
}
&::before {
content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 172 20'%3E%3Cpath fill='%23000' d='M3.48 3.14V18H1.3V3.14h2.18Zm2.265 9.74c0-3.16 2.16-5.34 5.26-5.34 3.12 0 5.28 2.18 5.28 5.34 0 3.16-2.16 5.32-5.28 5.32-3.1 0-5.26-2.16-5.26-5.32Zm2.24-.02c0 2.1 1.2 3.46 3.02 3.46 1.84 0 3.04-1.36 3.04-3.46 0-2.12-1.2-3.46-3.04-3.46-1.82 0-3.02 1.34-3.02 3.46ZM19.16 7.74l3.06 7.94 3.02-7.94h2.36L23.4 18h-2.44l-4.1-10.26h2.3Zm19.127 4.92v.82h-7.84c.04 1.92 1.3 2.92 2.9 2.92 1.2 0 2.2-.56 2.62-1.68h2.22c-.54 2.08-2.38 3.48-4.88 3.48-3.06 0-5.08-2.12-5.08-5.34 0-3.2 2.02-5.32 5.06-5.32 2.98 0 5 2.04 5 5.12Zm-5-3.38c-1.46 0-2.66.9-2.82 2.68h5.6c-.12-1.78-1.3-2.68-2.78-2.68Zm46.859 6.8h.98V18h-1.16c-1.38 0-2.2-.32-3-1.24-1.14 1-2.58 1.44-4.1 1.44-2.74 0-4.9-1.42-4.9-3.9 0-1.34.62-2.68 2.82-3.9l.34-.18-.22-.26c-1.16-1.3-1.48-2.22-1.48-3.12 0-1.98 1.58-3.28 3.92-3.28 2.3 0 3.8 1.26 3.8 3.1 0 1.38-.86 2.48-2.14 3.32l-.88.58 2.94 3.26c.5-.88.84-1.96.88-3.32h1.92c0 1.86-.56 3.54-1.48 4.8l.02.02c.5.54.94.76 1.74.76Zm-6.86-10.84c-1.08 0-1.76.66-1.76 1.58 0 .7.42 1.3 1.12 2.1l.34.38.74-.46c.9-.56 1.36-1.32 1.36-2.04 0-.94-.72-1.56-1.8-1.56Zm-3.06 8.92c0 1.5 1.26 2.24 2.74 2.24.94 0 1.98-.3 2.8-.98l-3.5-3.92-.3.18c-1.38.84-1.74 1.66-1.74 2.48ZM87.966 18V3.14h2.18v9.16l4.48-4.56h2.72l-4.38 4.42 4.42 5.84h-2.76l-3.22-4.3-1.2 1.22V18h-2.24ZM98.784 5.84V3.56h2.42v2.28h-2.42Zm.12 12.16V7.76h2.18V18h-2.18Zm10.184-10.44c2.58 0 4.1 1.7 4.1 4.32V18h-2.2v-5.66c0-1.92-.98-2.86-2.48-2.86-1.7 0-2.62 1.2-2.62 3.02V18h-2.18V7.76h1.98l.16 1.38c.58-.98 1.68-1.58 3.24-1.58Zm14.593-4.42h2.2V18h-2l-.18-1.56c-.62 1.04-1.96 1.76-3.54 1.76-2.82 0-4.86-1.96-4.86-5.3 0-3.44 2.14-5.36 4.86-5.36 1.6 0 2.9.68 3.52 1.72V3.14Zm-3.08 13.12c1.72 0 3.04-1.24 3.04-3.36 0-2.1-1.32-3.42-3.04-3.4-1.8 0-3.04 1.32-3.04 3.4 0 2.12 1.26 3.36 3.04 3.36Zm13.253-8.7c2.58 0 4.1 1.7 4.1 4.32V18h-2.2v-5.66c0-1.92-.98-2.86-2.48-2.86-1.7 0-2.62 1.2-2.62 3.02V18h-2.18V7.76h1.98l.16 1.38c.58-.98 1.68-1.58 3.24-1.58Zm16.333 5.1v.82h-7.84c.04 1.92 1.3 2.92 2.9 2.92 1.2 0 2.2-.56 2.62-1.68h2.22c-.54 2.08-2.38 3.48-4.88 3.48-3.06 0-5.08-2.12-5.08-5.34 0-3.2 2.02-5.32 5.06-5.32 2.98 0 5 2.04 5 5.12Zm-5-3.38c-1.46 0-2.66.9-2.82 2.68h5.6c-.12-1.78-1.3-2.68-2.78-2.68Zm6.592 5.62h2.18c.02 1.06 1 1.68 2.34 1.68 1.34 0 2.14-.5 2.14-1.4 0-.78-.4-1.2-1.56-1.38l-2.06-.36c-2-.34-2.78-1.38-2.78-2.84 0-1.84 1.82-3.04 4.14-3.04 2.5 0 4.32 1.2 4.34 3.24h-2.16c-.02-1.04-.92-1.62-2.18-1.62-1.2 0-1.96.5-1.96 1.34 0 .74.52 1.08 1.66 1.28l2.02.36c1.9.34 2.78 1.28 2.78 2.82 0 2.12-1.96 3.22-4.34 3.22-2.7 0-4.52-1.3-4.56-3.3Zm10.215 0h2.18c.02 1.06 1 1.68 2.34 1.68 1.34 0 2.14-.5 2.14-1.4 0-.78-.4-1.2-1.56-1.38l-2.06-.36c-2-.34-2.78-1.38-2.78-2.84 0-1.84 1.82-3.04 4.14-3.04 2.5 0 4.32 1.2 4.34 3.24h-2.16c-.02-1.04-.92-1.62-2.18-1.62-1.2 0-1.96.5-1.96 1.34 0 .74.52 1.08 1.66 1.28l2.02.36c1.9.34 2.78 1.28 2.78 2.82 0 2.12-1.96 3.22-4.34 3.22-2.7 0-4.52-1.3-4.56-3.3Z'/%3E%3C/svg%3E");
position: absolute;
opacity: 0.5;
bottom: 2.5rem;
height: 1.125rem;
width: 8.95rem;
}
}
/** Custom styles for plausible embed */
@media (min-width: 60rem) {
.k-panel-main:has(iframe[plausible-embed]) {
max-width: 104rem;
}
}
.k-header:has(+ iframe[plausible-embed]) {
display: none;
}
/** No page header for files */
:where([data-template="images"], [data-template="files"]) .k-page-view-header {
display: none;
}
/* Hide pages in Link field that we don't want to link to */
.k-page-browser .k-page-tree li:has(.k-icon-frame > .k-icon:is([data-type="survey"], [data-type="file-image"], [data-type="file-word"])) {
display: none;
}
.k-file-browser-tree ul li ul li:has(.k-icon-frame > .k-icon:not([data-type="file-image"], [data-type="file-word"])) {
display: none;
}
/* No padding in Block selector plugin */
.k-block-selector-button-preview {
padding: 0;
}
@@ -0,0 +1,3 @@
# src > transitions
This folder contains transitions for use with [https://taxi.js.org/transitions/](Taxi.js).
@@ -0,0 +1,13 @@
import { Transition } from "@unseenco/taxi"
export default class extends Transition {
onEnter({ done }: { done: CallableFunction }) {
window.scrollTo({
top: 0,
left: 0,
behavior: "instant"
})
done()
}
}
@@ -0,0 +1,41 @@
/* main breakpoints definition */
export const breakpoints = {
xxl: 96,
xl: 72,
lg: 62,
md: 44,
sm: 29.5
} as const
export const remToPx = (rem: number) => {
return rem * Number.parseFloat(getComputedStyle(document.documentElement).fontSize)
}
export const breakpointsString = Object.fromEntries(
Object.entries(breakpoints).map(([key, value]) => [key, `${value}rem` as `${number}rem`])
) as { [key in keyof typeof breakpoints]: `${number}rem` }
export const getBreakpointValue = <T>(values: { [K in keyof typeof breakpoints | number | "_"]?: T }) => {
const viewportWidth = window.innerWidth
// find the closest breakpoint
const closestBreakpoint = Object.entries(breakpoints).find(
([, breakpointValue]) => viewportWidth >= remToPx(breakpointValue)
)
if (closestBreakpoint && Object.hasOwn(values, closestBreakpoint[0])) {
return values[closestBreakpoint[0] as keyof typeof breakpoints]
}
// nothing found yet, check if values has a rem value
const remEntry = Object.entries(values).find(([key]) => !Number.isNaN(Number(key)))
if (remEntry) {
const [remValue, value] = remEntry
if (viewportWidth >= remToPx(Number.parseFloat(remValue))) {
return value
}
}
// return fallback
return values._
}
+29
View File
@@ -0,0 +1,29 @@
{
"compilerOptions": {
"target": "ES2023",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
/* Paths */
"baseUrl": "./src",
"paths": {
"@/*": ["./*"]
}
},
"include": ["src"]
}
+66
View File
@@ -0,0 +1,66 @@
import { resolve } from "node:path"
import { defineConfig, loadEnv } from "vite"
import laravel from "laravel-vite-plugin"
import tsconfigPaths from "vite-tsconfig-paths"
import svgSprite from "vite-svg-sprite-wrapper"
import tailwind from "@tailwindcss/vite"
import inject from "@rollup/plugin-inject"
import browserslist from "browserslist"
import { browserslistToTargets } from "lightningcss"
import { browserslist as browserslistConfig } from "./package.json"
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), "")
return {
base: mode === "development" ? "/" : "/dist/",
build: {
outDir: resolve(__dirname, "public/dist"),
emptyOutDir: true,
manifest: "manifest.json",
cssMinify: "lightningcss"
},
plugins: [
// this plugin is necessary for our HTMX extensions to correctly register
inject({
htmx: "htmx.org"
}),
svgSprite({
sprite: {
shape: {
transform: [
{
svgo: {
plugins: [{ name: "preset-default" }, "removeXMLNS"]
}
}
]
}
},
icons: "assets/icons/*.svg",
outputDir: "assets/"
}),
laravel({
input: ["src/index.ts", "src/styles/index.css", "src/styles/panel.css"],
refresh: ["site/{layouts,snippets,templates}/**/*"]
}),
tsconfigPaths(),
tailwind()
],
css: {
transformer: "lightningcss",
lightningcss: {
targets: browserslistToTargets(browserslist(browserslistConfig))
}
},
server: {
origin: env.APP_URL,
port: Number(env.VITE_DEV_PORT || 3000),
proxy: {
// we proxy anything except the folders our vite dev assets are in
"^(?!/src|/node_modules|/@vite|/@react-refresh|/assets).*$": `http://${env.KIRBY_DEV_HOSTNAME}:${env.KIRBY_DEV_PORT}`
}
}
}
})