18 Commits

Author SHA1 Message Date
code d9c4b64d9c Add AGENTS.md and CLAUDE.md for agent onboarding
Documents project structure, Docker-only build/run commands, and
conventions so any coding agent (Claude Code, DeepSeek, etc.) can
orient quickly. CLAUDE.md imports AGENTS.md as the single source of
truth.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 00:59:47 +00:00
nick 27c15f747a ignore claude dir 2026-07-09 17:29:08 -07:00
nick d5871ed8e7 minor updates 2026-06-16 19:00:30 -07:00
nick ad6e07c76d fixed css and changed timezone 2026-05-04 11:30:14 -07:00
nick 06310d9eb9 Updated readme and composer 2026-05-03 13:27:30 -07:00
nick f679d0b20e safety fix 2026-02-07 17:26:26 -08:00
nick 9feccf9eaa tabs and draft working 2026-01-26 21:55:22 -08:00
nick 14ec6b7e7a fixed matomo to be in the master twig template. 2026-01-22 15:40:46 -08:00
nick 42a828a778 edit page template 2026-01-12 19:00:03 -08:00
nick 81c239f043 added matomo 2026-01-09 12:41:55 -08:00
nick 0d91b61bd0 control panel head changes 2025-12-22 18:32:35 -08:00
nick fd9a71c4d6 added page to the front end 2025-12-16 05:35:56 -08:00
nick 71413283c8 added humans and robots 2025-12-15 09:54:57 -08:00
nick a32956b4a2 added coming soon page 2025-12-15 09:28:09 -08:00
nick 7bf3dc6610 added coming soon page 2025-12-15 09:27:56 -08:00
nick 7b064eb6da morning changes 2025-12-15 06:14:07 -08:00
nick 934e134941 ready to test new layout 2025-12-14 03:45:07 -08:00
nick 6f410b5d0e added logs to docker 2025-12-07 23:10:32 -08:00
58 changed files with 875 additions and 88 deletions
+1
View File
@@ -0,0 +1 @@
.claude/
+59
View File
@@ -0,0 +1,59 @@
# NovaconiumPHP
A lightweight PHP 8.1+ MVC-ish web framework/CMS toolkit (router + controllers + Twig views + Sass styling), authored by Nick Yeoman (4lt.ca) and distributed via Composer as `4lt/novaconium`. The canonical repo is hosted on a self-hosted Gitea instance (`git.4lt.ca`), not GitHub — do not assume `gh` CLI or GitHub Actions apply here.
## Directory map
- `src/` — core framework classes, namespace `Novaconium\` (PSR-4 → `src/`): `novaconium.php` (bootstrap), `Router.php`, `Database.php`, `Session.php`, `Redirect.php`, `Post.php`, `Logger.php`, `MessageHandler.php`, `functions.php`, `twig.php`, and `src/Services/` (`Auth.php`, `TagManager.php`).
- `controllers/` — framework-level default controllers (dashboard, pages, messages, settings, auth/, sitemap, 404, init, create_admin, etc.).
- `config/routes.php` — framework-level default route definitions, merged with app-level `App/routes.php` at runtime.
- `views/` — Twig views mirroring controller names (`controllers/dashboard.php``views/dashboard.html.twig`).
- `twig/` — shared/partial Twig templates (layout pieces: `master.html.twig`, `nav.html.twig`, `head.html.twig`, `foot.html.twig`, `cp/` control-panel partials, `javascript/`).
- `sass/` — framework's default Sass source (indented Sass, not SCSS), 7-1-ish structure: `abstracts/`, `base/`, `framework/`, `controlPanel/`, `coming-soon/`; has its own `Dockerfile` for compiling.
- `skeleton/` — scaffold copied into a new consumer project on install: `.env`, `docker-compose.yml`, `novaconium/App/{config.php,routes.php,controllers,views,templates}`, `novaconium/public/{index.php,.htaccess,css,js}`, `novaconium/sass/project.sass`.
- `docs/` — one short Markdown file per topic (see below).
- `_assets/` — static assets (e.g. logo used in README).
## Build / run (Docker-only — no host tooling assumed)
**Agents (Claude Code, opencode, etc.) run inside containers and have no access to Docker.** Do not attempt to run any `docker`/`docker compose` command below — they will fail. These commands are documented for a human maintainer to run on their own host. If you need to verify a change, do so by reading/reasoning about the code, not by invoking Docker.
**Composer** (per `docs/Composer.md`):
```
docker run --rm --interactive --tty --volume $PWD:/app composer:latest require 4lt/novaconium
docker run --rm --interactive --tty --volume $PWD:/app composer:latest update
```
**Sass build** (per `docs/Sass.md`), using `sass/Dockerfile`:
```
cd sass && docker build -t sass-container .
docker run --rm -v $(pwd):/usr/src/app -w /usr/src/app sass-container sass novaconium/sass/project.sass novaconium/public/css/novaconium.css
```
Compressed variant: add `--style=compressed`, e.g.
```
docker run --rm -v $(pwd):/usr/src/app -w /usr/src/app sass-container --style=compressed sass/novaconium.sass skeleton/novaconium/public/css/novaconium.css
```
**Dev stack**: `docker compose up -d` from `skeleton/docker-compose.yml` — services: `4lights/corxn:8.5.3` (Apache+PHP), `redis`, `mariadb`, `phpmyadmin`.
**Testing a cloned copy without reinstalling via Composer**: see `docs/Dev-Fake_autoload.md` for the fake-autoload dev trick.
## No test suite / lint / CI
There is no `phpunit.xml`, no lint config (no PHPCS, `.editorconfig`, ESLint), and no CI pipeline in this repo. Don't hunt for `npm test`/`phpunit`/lint commands — none exist.
## Conventions
- PSR-4 autoloading: `Novaconium\``src/`.
- Controllers and views are name-paired by default (e.g. `controllers/dashboard.php``views/dashboard.html.twig`), but this is only a convention — a controller can render any view, they are not required to match.
- Sass partials are prefixed `_` (e.g. `_forms.sass`), aggregated per-folder via `index.sass`.
- App-level config (`App/config.php`) is a multi-dimensional PHP array: database credentials, `base_url`, `secure_key`, `logfile`, `loglevel`.
- Versioning strategy is semantic-versioning (declared in `composer.json` `extra.versioning`).
## Git workflow
Single `master` branch (no `main`), tracking a self-hosted Gitea remote. Solo-maintainer, trunk-based, direct commits to `master` with short informal messages — no conventional-commits enforcement, no PR-based tooling assumptions.
## Further reading
See `docs/*.md` for per-topic detail: `404.md`, `Composer.md`, `ConfigurationFile.md`, `Dev-Fake_autoload.md`, `Logs.md`, `Messages.md`, `Post.md`, `Redirect.md`, `Sass.md`, `Session.md`, `StyleSheets-sass.md`, `Twig-Views.md`, `docker.md`.
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+14 -7
View File
@@ -6,13 +6,20 @@ NovaconiumPHP is a high-performance PHP framework designed with inspiration from
Pronounced: Noh-vah-koh-nee-um
Packagist: https://packagist.org/packages/4lt/novaconium
Master Repo: https://git.4lt.ca/4lt/novaconium
* Packagist: https://packagist.org/packages/4lt/novaconium
* Master Repo: https://git.4lt.ca/4lt/novaconium
## Getting Started
Novaconium is heavly influenced by docker, but you can use composer outside of docker.
You can [learn more about how novaconium works with composer](https://git.4lt.ca/4lt/novaconium/src/branch/master/docs/Install-Composer-On-Debian.md).
Novaconium is designed to be developed primarily using Docker. Instead of relying on tools installed directly on your system, common development tasks are executed inside containers:
* Composer runs inside a Docker container rather than on your host machine
* Apache / PHP are served from containers instead of your local environment
* Sass compilation is also handled within a container
As long as you have Docker installed, you can use the full development environment without installing additional dependencies on your system.
You can [learn more about how novaconium works with composer](https://git.4lt.ca/4lt/novaconium/src/branch/master/docs/Composer.md).
```bash
PROJECTNAME=novaproject;
@@ -24,9 +31,9 @@ docker run --rm --interactive --tty --volume ./novaconium/:/app composer:latest
cp -R novaconium/vendor/4lt/novaconium/skeleton/. .;
# Edit .env
# pwgen -cnsB1v 12 root password
# pwgen -cnsB1v 12 mysql user password (need in both config and env)
# pwgen -cnsB1v 64 framework key (need in config)
# pwgen -cnsB1v 12 # root password
# pwgen -cnsB1v 12 # mysql user password (need in both config and env)
# pwgen -cnsB1v 64 # framework key (need in config)
# Edit novaconium/App/config.php
docker compose up -d
+6 -3
View File
@@ -8,11 +8,14 @@ $framework_routes = [
],
'/novaconium/login' => [
'post' => 'NOVACONIUM/authenticate',
'get' => 'NOVACONIUM/login'
'get' => 'NOVACONIUM/auth/login'
],
'/novaconium/dashboard' => [
'get' => 'NOVACONIUM/dashboard'
],
'/novaconium/settings' => [
'get' => 'NOVACONIUM/settings'
],
'/novaconium/pages' => [
'get' => 'NOVACONIUM/pages'
],
@@ -38,8 +41,8 @@ $framework_routes = [
'post' => 'NOVACONIUM/message_save'
],
'/novaconium/logout' => [
'post' => 'NOVACONIUM/logout',
'get' => 'NOVACONIUM/logout'
'post' => 'NOVACONIUM/auth/logout',
'get' => 'NOVACONIUM/auth/logout'
],
'/novaconium/sitemap.xml' => [
'get' => 'NOVACONIUM/sitemap'
+11
View File
@@ -0,0 +1,11 @@
<?php
$data = array_merge($data, [
'title' => 'Novaconium Login Page',
'pageclass' => 'novaconium'
]);
// Don't come here if logged in
if ($session->get('username')) {
$redirect->url('/novaconium/dashboard');
makeitso();
}
view('@novacore/auth/login');
+9
View File
@@ -0,0 +1,9 @@
<?php
$data = array_merge($data, [
'title' => 'Coming Soon',
'heading' => 'Coming Soon',
'countdown' => true,
'launch_date' => '2026-01-01T00:00:00'
]);
view('@novacore/coming-soon', $data);
+2 -1
View File
@@ -1,7 +1,8 @@
<?php
$data = array_merge($data, [
'title' => 'Novaconium Dashboard Page',
'pageclass' => 'novaconium'
'pageclass' => 'novaconium',
'pageid' => 'controlPanel'
]);
if ( empty($session->get('username'))) {
+1
View File
@@ -3,6 +3,7 @@
$data = array_merge($data, [
'title' => 'Novaconium Edit Page',
'pageclass' => 'novaconium',
'pageid' => 'controlPanel',
'editor' => 'ace'
]);
+1
View File
@@ -7,6 +7,7 @@ $data = [
'empty_users' => false,
'show_login' => false,
'token' => $session->get('token'),
'pageclass' => 'novaconium',
'title' => 'Novaconium Admin'
];
-9
View File
@@ -1,9 +0,0 @@
<?php
$data['title'] = 'Novaconium Login Page';
// Don't come here if logged in
if ($session->get('username')) {
$redirect->url('/novaconium/dashboard');
makeitso();
}
view('@novacore/login');
+2 -1
View File
@@ -2,7 +2,8 @@
$data = array_merge($data, [
'title' => 'Novaconium Messages',
'pageclass' => 'novaconium'
'pageclass' => 'novaconium',
'pageid' => 'controlPanel'
]);
if ( empty($session->get('username'))) {
+2 -1
View File
@@ -2,7 +2,8 @@
$data = array_merge($data, [
'title' => 'Novaconium Pages',
'pageclass' => 'novaconium'
'pageclass' => 'novaconium',
'pageid' => 'controlPanel'
]);
if ( empty($session->get('username'))) {
+15
View File
@@ -0,0 +1,15 @@
<?php
$data = array_merge($data, [
'title' => 'Novaconium Settings',
'pageclass' => 'novaconium',
'pageid' => 'controlPanel'
]);
if ( empty($session->get('username'))) {
$redirect->url('/novaconium/login');
$messages->error('You are not loggedin');
makeitso();
}
view('@novacore/settings', $data);
+1 -1
View File
@@ -8,7 +8,7 @@ Update novaconium with composer in docker: ```docker run --rm --interactive --tt
## Install Composer natively on Debian
Assuming you have nala installed:
Assuming you have nala installed (otherwise use apt-get):
```bash
sudo nala install curl php-cli php-mbstring git unzip
+10
View File
@@ -20,4 +20,14 @@ Compressed:
# Build Novaconium (compressed)
docker run --rm -v "$(pwd):/usr/src/app" -w /usr/src/app sass-container --style=compressed sass/novaconium.sass skeleton/novaconium/public/css/novaconium.css
```
Dev:
```bash
docker run --rm \
-v "$(pwd)/sass:/usr/src/sass" \
-v "/home/nick/tmp/novaproject/novaconium/public/css:/usr/src/css" \
-w /usr/src \
sass-container \
sass sass/novaconium.sass css/novaconium.css --no-source-map --style=compressed
```
+94
View File
@@ -0,0 +1,94 @@
body#coming-soon
display: flex
justify-content: center
align-items: center
height: 100%
padding: 1rem
box-sizing: border-box
margin: 0
text-align: center
color: #eee
font-family: 'Segoe UI', Roboto, sans-serif
background-size: cover
background-repeat: no-repeat
background-position: center center
background-attachment: fixed
.container
max-width: 600px
background-color: rgba(30, 30, 30, 0.89)
padding: 2rem
border-radius: 8px
box-shadow: 0 0 20px rgba(0,0,0,0.5)
h1
font-size: 3rem
margin-bottom: 1rem
animation: pulse 1.5s infinite
p
font-size: 1.2rem
opacity: 0.85
margin-bottom: 1.5rem
.countdown
font-size: 2rem
font-weight: bold
margin: 1rem 0 2rem 0
form.newsletter
display: flex
flex-direction: column
gap: 0.5rem
margin-top: 1rem
input[type="email"]
padding: 0.5rem
font-size: 1rem
border: 1px solid #666
border-radius: 4px
background: rgba(255,255,255,0.05)
color: #eee
button
padding: 0.5rem
font-size: 1rem
border: 1px solid #1e90ff
border-radius: 4px
background: #1e90ff
color: #fff
cursor: pointer
transition: background 0.2s
&:hover
background: #1565c0
.social-icons
margin-top: 2rem
display: flex
justify-content: center
gap: 1rem
a
display: inline-flex
align-items: center
justify-content: center
width: 40px
height: 40px
font-size: 1.5rem
color: #eee
text-decoration: none
transition: color 0.2s
&:hover
color: #1e90ff
i
font-style: normal
@keyframes pulse
0%, 100%
opacity: 0.8
50%
opacity: 1
+4
View File
@@ -0,0 +1,4 @@
body#controlPanel
footer
padding: 1rem
+8
View File
@@ -0,0 +1,8 @@
body#controlPanel
header
padding: 0
h1#biglogo
padding: 10px 0 0 0
margin: 0
+11
View File
@@ -0,0 +1,11 @@
body#controlPanel
div#panel
main#content
flex: 1
padding: 20px 40px
min-width: 0
background-color: #0b0b0bd6
border-left: 2px solid #104910
+22
View File
@@ -0,0 +1,22 @@
@use '../abstracts' as *
body#controlPanel
div#panel
div#cp-menu
background-color: #000
width: 360px
padding: 0
white-space: nowrap
ul#cp-nav
list-style: none
margin: 0
padding: 20px 0 0 0
li a
display: block
border-bottom: 1px solid $border-light
padding: 4px 10px
&:hover
background-color: #222
+5
View File
@@ -0,0 +1,5 @@
body#controlPanel
div#panel
display: flex
flex-direction: row
+10
View File
@@ -0,0 +1,10 @@
@forward 'header';
@forward 'panel';
@forward 'menu';
@forward 'main';
@forward 'footer';
body#controlPanel
h1
font-family: "VT323", "Courier New", "SF Mono", "Fira Code", Consolas, monospace
font-size: 48px
+1 -1
View File
@@ -3,7 +3,7 @@
@use '../abstracts' as *
@use 'sass:color' // For color.adjust()non-deprecated color tweaks
body.novaconium
body#controlPanel
// Simplified tab styling: Square borders, no rounds
.tab-container
margin: space('lg') auto
+1 -1
View File
@@ -74,7 +74,7 @@ body.novaconium
display: inline-flex
align-items: center
background: color.adjust($accent-light, $alpha: -0.2)
color: $accent-light
color: #fff
padding: space('xs') space('sm')
border-radius: 4px
font-size: 12px
+2
View File
@@ -2,3 +2,5 @@
@use 'abstracts' as *
@use 'base' as *
@use 'framework' as *
@use 'controlPanel' as *
@use 'coming-soon' as *
+2 -8
View File
@@ -1,14 +1,13 @@
# Sample Docker Compose
services:
corxn:
image: 4lights/corxn:6.0.0
image: 4lights/corxn:8.5.3
ports:
- "8000:80"
volumes:
- "/etc/timezone:/etc/timezone:ro"
- "/etc/localtime:/etc/localtime:ro"
- ./novaconium:/data
- ./data/logs:/var/log/apache2 # Optional Logs
- "./logs:/data/logs"
restart: unless-stopped
networks:
- internal
@@ -28,8 +27,6 @@ services:
MYSQL_USER: novaconium
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
volumes:
- "/etc/timezone:/etc/timezone:ro"
- "/etc/localtime:/etc/localtime:ro"
- ./data/db:/var/lib/mysql
networks:
- internal
@@ -48,9 +45,6 @@ services:
- PMA_USER=root
- PMA_PASSWORD=${MYSQL_ROOT_PASSWORD}
- UPLOAD_LIMIT=200M
volumes:
- "/etc/timezone:/etc/timezone:ro"
- "/etc/localtime:/etc/localtime:ro"
networks:
proxy:
+3 -1
View File
@@ -10,6 +10,8 @@ $config = [
'base_url' => 'http://localhost:8000',
'secure_key' => '', //64 alphanumeric characters
'logfile' => '/logs/novaconium.log',
'loglevel' => 'ERROR', // 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'NONE'
'loglevel' => 'ERROR', // 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'NONE',
'matomo_url' => 'matomo.4lt.ca',
'matomo_id' => '0',
'fonts' => 'https://fonts.googleapis.com/css2?family=VT323:wght@400&family=Fira+Code:wght@400;500&display=swap&family=Material+Icons:wght@400;500&display=swap'
];
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
header('Content-Type: text/plain; charset=UTF-8');
header('Cache-Control: public, max-age=604800');
echo <<<TXT
/*
* This humans.txt was generated by the framework.
* You may edit or remove it without affecting your site.
*/
/* FRAMEWORK */
Built With: Novaconium PHP
repo: https://git.4lt.ca/4lt/novaconium
Human: Nick Yeoman
url: https://www.nickyeoman.com/
Occupation: Linux Systems Administrator / Software Framework Author
Location: Canada
/* SITE */
Built with: Novaconium PHP
Runs on: CORXN Container
Optimized for: Humans
/* META */
There are four lights.
TXT;
@@ -1,2 +1,7 @@
<?php
$data = array_merge($data, [
'title' => 'Welcome to Novaconium Index Page',
'pageclass' => 'novaconium'
]);
view('index');
@@ -0,0 +1,33 @@
<?php
$slug = $router->parameters['slug'];
$query=<<<EOSQL
SELECT
id,
title,
heading,
description,
keywords,
author,
body,
created,
updated
FROM pages
WHERE slug = '$slug'
EOSQL;
//$db->debugGetRow($query);
$data = $db->getRow($query);
if(!$data) {
http_response_code('404');
header("Content-Type: text/html");
view('404');
exit;
}
$data = array_merge($data, [
'menuActive' => 'blog',
'pageid' => 'page',
'permissionsGroup' => $session->get('group')
]);
view('page', $data);
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
// Send proper headers
header('Content-Type: text/plain; charset=UTF-8');
header('Cache-Control: public, max-age=604800');
// Use $config['base_url'] as-is
$baseUrl = $config['base_url'];
echo <<<TXT
# robots.txt for sites powered by Novaconium framework
User-agent: *
Disallow: /novaconium/
Sitemap: {$baseUrl}/sitemap.xml
TXT;
+9
View File
@@ -2,5 +2,14 @@
$routes = [
'/' => [
'get' => 'index'
],
'/robots.txt' => [
'get' => 'robots'
],
'/humans.txt' => [
'get' => 'humans'
],
'/page/{slug}' => [
'get' => 'page'
]
];
@@ -0,0 +1,11 @@
{% extends '@novaconium/master.html.twig' %}
{% block content %}
<h1>{{ title }}</h1>
<div class="page-meta">
{% if updated is not empty %}
<span class="page-updated">Last Updated: {{ updated | date("M\\. jS Y \\a\\t g:ia") }}</span>
{% endif %}
</div>
{{ body | raw}}
{% endblock %}
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
{"version":3,"sourceRoot":"","sources":["../../../../sass/abstracts/_variables.sass","../../../../sass/base/_reset.sass","../../../../sass/base/_background.sass","../../../../sass/framework/_main.sass","../../../../sass/framework/_ui.sass","../../../../sass/framework/_forms.sass","../../../../sass/framework/_login_form.sass","../../../../sass/framework/_logo.sass","../../../../sass/framework/_tabs.sass","../../../../sass/framework/_edit_page.sass","../../../../sass/framework/_tooltip.sass","../../../../sass/framework/_ace.sass","../../../../sass/framework/_tags.sass"],"names":[],"mappings":"CAoEA,MACE,0BACA,8BACA,+BACA,mCACA,iCACA,oCACA,qCACA,qCACA,kCACA,yCACA,8CC1EF,EACE,sBAEF,KACE,eACA,gBACA,uBACA,iBDWQ,aCVR,MDYW,KCVb,KACE,SACA,UACA,YDgBW,kCCfX,iBDIQ,aCHR,MDKW,KCJX,iBAGF,kBACE,gBACA,gBACA,gBACA,qBAEF,GACE,iBAEF,GACE,eAEF,GACE,kBAEF,GACE,iBAEF,GACE,kBAEF,GACE,eAGF,EACE,eAEF,MACE,kBAEF,SACE,gBAEF,KACE,kBAGF,EACE,MDnCa,eCoCb,qBACA,sCACA,iCAEA,gBACE,oBDzCW,eC0CX,aAGJ,MACE,eACA,mBAEF,GACE,oBAGF,WACE,aACA,iBACA,qCACA,kCACA,MD7DW,KCgEb,cACE,YDvDU,uDCwDV,iBACA,oCACA,qBACA,kBAEF,IACE,aACA,YACA,cACA,oCACA,kBACA,SACE,gBACA,UAGJ,MACE,WACA,yBACA,aAEF,MACE,cACA,gBACA,2CAEF,GACE,gBAGF,6BACE,oBACA,kBACA,oCACA,MDpGW,KCqGX,oCACA,kBACA,aAEA,qDACE,aACA,aDxGW,eCyGX,wCAEJ,OACE,eACA,qCAEA,gBACE,WACA,mBAGJ,IACE,eACA,YACA,kBAEF,OACE,aAGF,GACE,YACA,WACA,oCACA,aAGF,gBACE,iCACA,mBAGF,aACE,KACE,2BACA,uBAEJ,YACE,8BACA,MDnJW,KCsJb,kBACE,8BACA,MDxJW,KEtBb,gBACI,sBACA,0uKCEF,iCACE,aACA,UACA,uBACA,uBACA,mBAGF,wBACE,aACA,cACA,aACA,SACA,iBHGM,aGFN,gCAEF,2BACE,YACA,cACA,gBACA,UACA,oBACA,iBHNM,aGON,gCAEF,4BACE,6CAEA,uCACE,mBAEJ,2BACE,cACA,mBACA,qBACA,MHjBS,KGmBT,kEACE,oCACA,MHlBS,eGmBT,kBAGJ,0BACE,iCACE,sBACA,mBAEF,iDACE,WACA,gBACA,cAEF,yBACE,gBCtDJ,qBACE,2EACA,kBACA,gCACA,MJcS,KIbT,qBACA,kBACA,sCACA,sCAGF,uBACE,kBAGF,+EACE,mBACA,YACA,eACA,kBACA,iBAEF,0BACE,oCACA,qBACA,4BAEF,2BACE,sCACA,uBACA,8BAEF,0BACE,gCACA,MJdS,iBIeT,kCACA,gBACA,mBAGF,6BACE,WACA,sCAEA,gEACE,sCACA,gBAEF,gCACE,qCACA,MJ/BS,iBIgCT,gBClDF,uDACE,mBAEA,0EACE,WACA,gBAEJ,2DACE,kBAGF,gOAIE,gCACA,sCACA,WACA,cACA,kBACA,2EAEA,wPACE,aLHO,eKIP,2CACA,aAEJ,gEACE,aLRS,eKWX,+DACE,iBLZS,eKaT,mBACA,YACA,mBACA,kBACA,eACA,2EAEA,0IACE,sBACA,sCAGJ,6CACE,ML3BO,iBK6BT,6CACE,ML7BS,eK+BT,mDACE,MLlCO,iBKmCP,0BCzDR,uBAEE,UAIA,+HAGE,WACA,gBACA,aACA,mBACA,sBACA,eACA,sCACA,kBACA,WNEM,aMDN,MNGS,6CMCT,uUACA,4BACA,gCACA,qBACA,kBAEF,4CACE,iVACA,4BACA,gCACA,qBACA,kBAEF,2CACE,WNZW,eMaX,aNbW,eMcX,eACA,0BAEA,iDACE,WNlBS,eMmBT,uCAGF,kDACE,aACA,kBCnDJ,yBACE,kBACA,qBACA,eACA,gBACA,gBAEA,iCACE,WACA,kBACA,MACA,OACA,QACA,SACA,gJACA,0BACA,WACA,oBAGF,gCACE,WACA,kBACA,MACA,OACA,QACA,WACA,qEACA,YACA,kCAEF,+BACE,cACA,kCACA,iBACA,gBACA,WACA,oBACA,gBACA,yBAGA,yFACA,mDAEF,8BACE,cACA,kCACA,iBACA,gBACA,WACA,qBACA,iBACA,0EACA,mDAIA,qCACE,8BAEN,gBACE,GACE,SACF,KACE,UAEJ,kBACE,QACE,uBACF,IACE,+BACF,IACE,gCACF,IACE,8BACF,IACE,gCCvEJ,+BACE,mBACA,WRcM,aQZR,yBACE,aACA,iBRWQ,eQVR,6CAEF,4BACE,mBACA,WRMQ,eQLR,sCACA,mBACA,eACA,eACA,gBACA,kBACA,WACA,YRSQ,uDQPR,kCACE,iCAEF,wCACE,iBAEF,mCACE,iBRZI,aQaJ,uCACA,MRTS,eQUT,iBAEJ,6BACE,aACA,eACA,iBRpBM,aQqBN,MRnBS,KQoBT,YAEA,oCACE,cC5CN,iBACI,gBAEA,wBACI,WACA,iBACA,aAEJ,qBACI,gBACA,oBCRN,yBACE,kBACA,qBACA,YACA,eACA,MViBW,eUhBX,mBACA,QAEA,sCACE,kBACA,YACA,iBVMM,eULN,MVMO,KULP,gBACA,gBACA,cACA,eACA,aACA,UACA,uBACA,eACA,oCACA,oBAEA,6CACE,WACA,kBACA,SACA,UACA,iBACA,mBACA,sEAEJ,4CACE,mBACA,UAGJ,8BACE,kBACA,aC1CJ,kBACE,WACA,iBAEF,YACE,aACA,gCACA,YXuBU,uDWpBV,wBACE,qCACA,kCACA,iDAEF,0BACE,mCAEF,8CACE,oCAGF,6CACE,yCCtBF,+BACE,kBACA,SACA,OACA,QACA,iBACA,gBACA,WZWQ,eYVR,sCACA,gBACA,0BACA,gBACA,SACA,UACA,WACA,UACA,kBACA,uBAEA,mDACE,UACA,mBAEF,kCACE,qBACA,eACA,MZPO,KYQP,eACA,sCAEA,mFAEE,8BACA,MZXO,eYaT,6CACE,mBAEN,4BACE,kBAGF,4BACE,aACA,eACA,WACA,mBACA,sCACA,eACA,WZ/BQ,eYgCR,gBACA,cAEA,kCACE,YACA,yBACA,MZrCO,KYsCP,YZ5BM,uDY6BN,eACA,OACA,gBACA,aAEA,+CACE,MZ3CK,iBY6CX,0BACE,oBACA,mBACA,8BACA,MZhDW,eYiDX,qBACA,kBACA,eACA,gBACA,gBACA,uBACA,mBAEA,sCACE,gBACA,YACA,cACA,eACA,mBACA,eACA,UACA,cAEA,4CACE,WAWN,2EACE","file":"novaconium.css"}
+1 -13
View File
@@ -1,22 +1,10 @@
<?php
/**
* Novaconium Framework Entry Point
*
* This is the main entry point for the Novaconium framework.
* It sets up the environment, loads necessary components,
* and runs the application.
*
* @package Novaconium
* @author Nick Yeoman <dev@4lt.ca>
*/
// Enable error reporting for development environments
// error_reporting(E_ALL);
// ini_set('display_errors', 1);
// Define the base path where the website is running from
define('BASEPATH', dirname(__DIR__, 1));
define('BASEPATH', dirname(__DIR__));
// Load Composer's autoload file to handle class autoloading
require BASEPATH . '/vendor/autoload.php';
+185
View File
@@ -0,0 +1,185 @@
// /js/tabs.js
// Tab switching
function switchTab(tabId, button) {
const contents = document.querySelectorAll('.tab-content');
contents.forEach(content => content.classList.remove('active'));
const buttons = document.querySelectorAll('.tab-button');
buttons.forEach(b => b.classList.remove('active'));
document.getElementById(tabId).classList.add('active');
button.classList.add('active');
if (tabId === 'content6') {
setTimeout(initTags, 0);
}
}
// Tags Init (with custom dropdown autocomplete)
let tagsListeners = [];
function initTags() {
const tagsInput = document.getElementById('tags-input');
const tagsField = document.getElementById('tags');
const hiddenTags = document.getElementById('tags_json');
const dropdown = document.getElementById('tags-dropdown');
if (!tagsInput || !tagsField || !hiddenTags || !dropdown) {
console.warn('Tags elements missing');
return;
}
// Remove old listeners
tagsListeners.forEach(ls => ls());
tagsListeners = [];
let tags = [];
let existingTags = [];
try {
tags = JSON.parse(tagsInput.dataset.tags || '[]');
existingTags = JSON.parse(tagsInput.dataset.existingTags || '[]');
} catch (e) {
console.warn('JSON error:', e);
}
let selectedIndex = -1;
function renderTags() {
tagsInput.innerHTML = '';
tags.forEach((tag, index) => {
const chip = document.createElement('span');
chip.className = 'tag-chip';
chip.innerHTML = `${tag} <button type="button" class="tag-remove">&times;</button>`;
chip.querySelector('.tag-remove').onclick = () => {
tags.splice(index, 1);
renderTags();
hiddenTags.value = JSON.stringify(tags);
};
tagsInput.appendChild(chip);
});
tagsInput.appendChild(tagsField);
tagsInput.appendChild(dropdown);
hiddenTags.value = JSON.stringify(tags);
}
function updateDropdown() {
const value = tagsField.value.toLowerCase().trim();
dropdown.innerHTML = '';
dropdown.setAttribute('aria-expanded', value ? 'true' : 'false');
selectedIndex = -1;
if (!value) return;
const matches = existingTags
.filter(tag => tag.toLowerCase().startsWith(value) && !tags.includes(tag.toLowerCase()))
.slice(0, 10);
matches.forEach((tag, index) => {
const li = document.createElement('li');
li.textContent = tag;
li.setAttribute('role', 'option');
li.onclick = () => selectTag(tag);
li.onmouseover = () => { selectedIndex = index; updateHighlight(); };
dropdown.appendChild(li);
});
if (matches.length) dropdown.parentElement.classList.add('has-dropdown');
else dropdown.parentElement.classList.remove('has-dropdown');
}
function updateHighlight() {
dropdown.querySelectorAll('li').forEach((li, index) => {
li.classList.toggle('selected', index === selectedIndex);
});
}
function selectTag(tag) {
addTag(tag, true);
tagsField.value = '';
dropdown.setAttribute('aria-expanded', 'false');
}
function addTag(inputValue, refocus = false) {
const tag = inputValue.trim().toLowerCase().replace(/[^\w-]/g, '');
if (tag && tag.length > 0 && tag.length <= 50 && !tags.includes(tag)) {
tags.push(tag);
renderTags();
if (refocus) tagsField.focus();
}
tagsField.value = '';
updateDropdown();
}
const keydownListener = (e) => {
if (dropdown.getAttribute('aria-expanded') === 'true') {
if (e.key === 'ArrowDown') {
e.preventDefault();
selectedIndex = Math.min(selectedIndex + 1, dropdown.querySelectorAll('li').length - 1);
updateHighlight();
dropdown.querySelector(`li:nth-child(${selectedIndex + 1})`)?.scrollIntoView({ block: 'nearest' });
} else if (e.key === 'ArrowUp') {
e.preventDefault();
selectedIndex = Math.max(selectedIndex - 1, 0);
updateHighlight();
dropdown.querySelector(`li:nth-child(${selectedIndex + 1})`)?.scrollIntoView({ block: 'nearest' });
} else if ((e.key === 'Enter' || e.key === 'Tab') && selectedIndex >= 0) {
e.preventDefault();
const selected = dropdown.querySelector(`li:nth-child(${selectedIndex + 1})`)?.textContent;
if (selected) selectTag(selected);
} else if (e.key === 'Escape') {
e.preventDefault();
tagsField.blur();
}
}
if (!dropdown.getAttribute('aria-expanded') === 'true' || selectedIndex < 0) {
if (e.key === 'Enter' || e.key === ',') {
e.preventDefault();
addTag(tagsField.value, true);
} else if (e.key === 'Tab') {
e.preventDefault();
if (tagsField.value.trim()) addTag(tagsField.value, true);
}
}
};
tagsField.addEventListener('keydown', keydownListener);
tagsListeners.push(() => tagsField.removeEventListener('keydown', keydownListener));
const inputListener = () => updateDropdown();
tagsField.addEventListener('input', inputListener);
tagsListeners.push(() => tagsField.removeEventListener('input', inputListener));
const blurListener = () => {
setTimeout(() => dropdown.setAttribute('aria-expanded', 'false'), 150);
if (tagsField.value.trim()) addTag(tagsField.value);
};
tagsField.addEventListener('blur', blurListener);
tagsListeners.push(() => tagsField.removeEventListener('blur', blurListener));
renderTags();
}
// Init on load and observe tab changes
document.addEventListener('DOMContentLoaded', () => {
const tagsTab = document.getElementById('content6');
if (tagsTab?.classList.contains('active')) {
initTags();
}
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
const target = mutation.target;
if (target.id === 'content6' && target.classList.contains('active')) {
initTags();
}
}
});
});
observer.observe(document.body, { subtree: true, attributes: true });
});
// Export switchTab for external use if needed
window.switchTab = switchTab;
+2 -2
View File
@@ -56,7 +56,7 @@ class Router {
}
private function getRequestType() {
// is the requewst a get or post?
// is the request a get or post?
if (empty($_POST)) {
return 'get';
} else {
@@ -75,7 +75,7 @@ class Router {
foreach ($this->routes as $key => $value) {
// Check if key contains a curly bracket, if not continue. We already checked above.
if (!strpos($key, '{')) continue;
if (strpos($key, '{') === false) continue;
// Remove everything after the curly bracket, from key
$keyPath = substr($key, 0, strpos($key, '{'));
+13 -7
View File
@@ -1,5 +1,16 @@
<?php
use Novaconium\Logger;
use Novaconium\Session;
use Novaconium\MessageHandler;
use Novaconium\Database;
use Novaconium\Post;
use Novaconium\Redirect;
use Novaconium\Router;
$db = null;
$post = null;
// --- Load Config ---
if (file_exists(\BASEPATH . '/App/config.php')) {
require_once \BASEPATH . '/App/config.php';
@@ -11,43 +22,38 @@ require_once \FRAMEWORKPATH . '/src/functions.php';
require_once \FRAMEWORKPATH . '/src/twig.php';
// --- Logging ---
use Novaconium\Logger;
$log = new Logger(\BASEPATH . $config['logfile'], $config['loglevel']);
// --- Twig Data Array ---
$data = [];
$data['fonts'] = $config['fonts'] ?? [];
$data['matomo_url'] = $config['matomo_url'] ?? '';
$data['matomo_id'] = $config['matomo_id'] ?? '0';
// --- Session ---
use Novaconium\Session;
$session = new Session();
$data['token'] = $session->get('token');
$data['username'] = $session->get('username');
// --- Messages ---
use Novaconium\MessageHandler;
$messages = new MessageHandler($session->flash('messages'));
foreach (['error', 'notice'] as $key) {
$data[$key] = $messages->showMessages($key);
}
// --- Database ---
use Novaconium\Database;
if (!empty($config['database']['host'])) {
$db = new Database($config['database']);
}
// --- POST Wrapper ---
use Novaconium\Post;
if (!empty($_POST)) {
$post = new Post($_POST);
}
// --- Redirect Handler ---
use Novaconium\Redirect;
$redirect = new Redirect();
// --- Router ---
use Novaconium\Router;
$router = new Router();
require_once $router->controllerPath;
+71
View File
@@ -0,0 +1,71 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ title | default('Coming Soon') }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/css/novaconium.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
body {
background: url("{{ bg_image | default('https://w.wallhaven.cc/full/5y/wallhaven-5yymk9.jpg') }}") no-repeat center center fixed;
background-size: cover;
}
</style>
</head>
<body id="coming-soon">
<div class="container">
<h1>{{ heading | default('Coming Soon') }}</h1>
{% block content %}{% endblock %}
{% if countdown %}
<div class="countdown" id="countdown"></div>
{% endif %}
{# TODO: listmonk #}
<form class="newsletter" action="#" method="post">
<label for="email">Subscribe to our newsletter:</label>
<input type="email" id="email" name="email" placeholder="Your email address" required>
<button type="submit">Subscribe</button>
</form>
{# Social media icons using Font Awesome #}
<div class="social-icons">
{% block social %}{% endblock %}
</div>
</div>
{% if countdown %}
<script>
{% if countdown %}
{% set default_launch = "now"|date_modify("+30 days")|date("Y-m-d\TH:i:s") %}
const launchDate = new Date('{{ launch_date | default(default_launch) }}').getTime();
const countdownEl = document.getElementById('countdown');
function updateCountdown() {
const now = new Date().getTime();
const distance = launchDate - now;
if (distance <= 0) {
countdownEl.innerText = 'Launched!';
return;
}
const days = Math.floor(distance / (1000*60*60*24));
const hours = Math.floor((distance % (1000*60*60*24)) / (1000*60*60));
const mins = Math.floor((distance % (1000*60*60)) / (1000*60));
const secs = Math.floor((distance % (1000*60)) / 1000);
countdownEl.innerText = `${days}d ${hours}h ${mins}m ${secs}s`;
}
updateCountdown();
setInterval(updateCountdown, 1000);
{% endif %}
</script>
{% endif %}
</body>
</html>
+44
View File
@@ -0,0 +1,44 @@
<!doctype html>
<html class="no-js" lang="en">
<head>
{% include ['@novaconium/cp/head.html.twig'] %}
</head>
<body id="{{ pageid | default('pageid') }}" class="{{ pageclass | default('pageclass') }}" >
{# Page Header #}
<header>
<h1 id="biglogo"><span class="main">Novaconium</span></h1>
</header>
<!-- Main Content Of The Page -->
<div id="panel" class="container">
{% include ['@novaconium/cp/menu.html.twig'] %}
<main id="content">
{% block content %}{% endblock %}
</main>
</div>
{# Page Footer #}
<footer>
<div class="copyright">&copy; {{ 'now' | date('Y') }} Novaconium</div>
</footer>
{% if editor == 'ace' %}
{% include '@novaconium/javascript/page-edit.html.twig' %}
{% include '@novaconium/javascript/ace.html.twig' %}
{% endif %}
{% if debug is not empty %}
<div id="debug">
<h2>Debugging Information</h2>
{{ debug|raw }}
</div>
{% endif %}
</body></html>
+60
View File
@@ -0,0 +1,60 @@
{# =============================================================================
<HEAD>
=============================================================================
#}
<meta charset="utf-8">
<title>{{ title | default('Welcome To Novaconium') }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
{# SEO & METADATA #}
<meta name="generator" content="Novaconium" />
<meta name="description" content="{{ description | default('No description given') }}">
<meta name="keywords" content="{{ keywords | default('website') }}">
<meta name="author" content="{{ author | default('anonymous') }}">
{# DARK MODE & THEME HINTS #}
<meta name="color-scheme" content="dark">
<meta name="theme-color" content="#000000">
{# OPEN GRAPH (OG) FOR SOCIAL SHARING #}
<meta property="og:title" content="{{ title | default('Welcome') }}">
<meta property="og:type" content="website">
<meta property="og:url" content="{{ app_url | default(request.scheme ~ '://' ~ request.host) }}">
<meta property="og:image" content="{{ og_image | default('/icon.png') }}">
{# PWA & FAVICONS #}
<link rel="manifest" href="site.webmanifest">
<link rel="apple-touch-icon" href="/icon.png">
<link rel="icon" type="image/x-icon" href="/favicon.ico">
{# GOOGLE FONTS (CDN VIA PRECONNECT) #}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="{{ fonts | default('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&family=Roboto+Mono&family=Source+Code+Pro&family=Lato&family=Poppins&family=Material+Icons&family=Material+Icons+Outlined&family=VT323:wght@400&family=Fira+Code:wght@400;500&display=swap') }}"
rel="stylesheet"
>
{% if editor == 'ace' %}
<!-- ACE Editor -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/ace/1.35.2/ace.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.35.2/ace.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.35.2/mode-html.min.js"></script> {# HTML syntax #}
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.35.2/theme-tomorrow_night.min.js"></script> {# Dark theme #}
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.35.2/ext-language_tools.min.js"></script> {# Autocomplete #}
<!-- END ACE Editor -->
{% endif %}
{# highlight.js #}
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/ir-black.min.css">
<script src="/js/tabs.js"></script>
{# STYLESHEET #}
<link rel="stylesheet" href="/css/novaconium.css">
+9
View File
@@ -0,0 +1,9 @@
<div id="cp-menu">
<ul id="cp-nav">
<li><a href="/novaconium/dashboard">Dashboard</a></li>
<li><a href="/novaconium/pages">Pages</a></li>
<li><a href="/novaconium/messages">Messages</a></li>
<li><a href="/novaconium/settings">Settings</a></li>
<li><a href="/novaconium/logout">Logout</a></li>
</ul>
</div>
+1 -7
View File
@@ -1,11 +1,5 @@
<!--
What goes very last on the page.
right before the /body
like javascript
or analytics
such as javascript
-->
{% include '@novaconium/javascript/page-edit.html.twig' %}
{% if editor == 'ace' %}
{% include '@novaconium/javascript/ace.html.twig' %}
{% endif %}
+1 -3
View File
@@ -1,3 +1 @@
<!--
What goes in the footer html tag
-->
<div class="copyright">&copy; {{ 'now' | date('Y') }} Novaconium</div>
+1 -16
View File
@@ -39,22 +39,7 @@
>
{# STYLESHEET #}
{% if pageclass == "novaconium" %}
<link rel="stylesheet" href="/css/novaconium.css">
{% else %}
<link rel="stylesheet" href="/css/main.css">
{% endif %}
{% if editor == 'ace' %}
<!-- ACE Editor -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/ace/1.35.2/ace.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.35.2/ace.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.35.2/mode-html.min.js"></script> {# HTML syntax #}
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.35.2/theme-tomorrow_night.min.js"></script> {# Dark theme #}
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.35.2/ext-language_tools.min.js"></script> {# Autocomplete #}
<!-- END ACE Editor -->
{% endif %}
<link rel="stylesheet" href="/css/novaconium.css">
{# highlight.js #}
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
+28
View File
@@ -60,4 +60,32 @@
{% endif %}
{% include ['@override/foot.html.twig', '@novaconium/foot.html.twig'] %}
{% if matomo_id > 0 %}
<!-- Matomo -->
<script>
var _paq = window._paq = window._paq || [];
{% if is_404 %}
_paq.push([
'setDocumentTitle',
'404 / Not Found - ' + document.location.pathname
]);
{% endif %}
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u = "//{{ matomo_url|trim('/') }}/";
_paq.push(['setTrackerUrl', u + 'matomo.php']);
_paq.push(['setSiteId', {{ matomo_id }}]);
var d = document, g = d.createElement('script'), s = d.getElementsByTagName('script')[0];
g.async = true;
g.src = u + 'matomo.js';
s.parentNode.insertBefore(g, s);
})();
</script>
<!-- End Matomo Code -->
{% endif %}
</body></html>
+1
View File
@@ -1,3 +1,4 @@
{% set is_404 = status_code is defined and status_code == 404 %}
{% extends '@novaconium/master.html.twig' %}
{% block content %}
+32
View File
@@ -0,0 +1,32 @@
{% extends '@novaconium/coming-soon/index.html.twig' %}
{% set bg_image = "https://i.4lt.ca/4lt/waterBubbles.webp" %}
{% set default_launch = "now"|date_modify("+30 days")|date("Y-m-d\TH:i:s") %}
{% block content %}
<p>Our website is under construction. Were working hard to bring you a better experience.</p>
<p>Stay tuned for updates!</p>
{% endblock %}
{% block social %}
<a href="mailto:you@example.com" title="Email">
<i class="fa-solid fa-envelope"></i>
</a>
<a href="tel:+1234567890" title="Phone">
<i class="fa-solid fa-phone"></i>
</a>
<a href="#" title="Twitter">
<i class="fab fa-twitter"></i>
</a>
<a href="#" title="Facebook">
<i class="fab fa-facebook-f"></i>
</a>
<a href="#" title="Instagram">
<i class="fab fa-instagram"></i>
</a>
{% endblock %}
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends '@novaconium/master.html.twig' %}
{% extends '@novaconium/cp/control-panel.html.twig' %}
{% block content %}
<h1>{{title}}</h1>
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends '@novaconium/master.html.twig' %}
{% extends '@novaconium/cp/control-panel.html.twig' %}
{% block content %}
<h2>Edit Page - {{ title }}</h2>
+13
View File
@@ -13,4 +13,17 @@
<textarea id="body" name="body" rows="10" style="display: none;">{{ rows.body|default('')|e('html') }}</textarea>
<div id="body-editor" class="ace-editor"></div> {# Ace mounts here #}
</div>
</div>
<div class="form-group">
<label for="draft">
<input
type="checkbox"
id="draft"
name="draft"
value="1"
{% if rows.draft|default(false) %}checked{% endif %}
>
Save as draft
</label>
</div>
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends '@novaconium/master.html.twig' %}
{% extends '@novaconium/cp/control-panel.html.twig' %}
{% block content %}
<h1>{{title}}</h1>
+1 -1
View File
@@ -1,4 +1,4 @@
{% extends '@novaconium/master.html.twig' %}
{% extends '@novaconium/cp/control-panel.html.twig' %}
{% block content %}
<h1>{{title}}</h1>
+6
View File
@@ -0,0 +1,6 @@
{% extends '@novaconium/cp/control-panel.html.twig' %}
{% block content %}
<h1>{{title}}</h1>
<p>Settings will go here.</p>
{% endblock %}