Latest type and dep improvements

This commit is contained in:
Kuba Orlik 2024-10-07 17:43:01 +02:00
parent 8f86a99116
commit 887829420e
120 changed files with 3914 additions and 7400 deletions

1
.gitignore vendored
View File

@ -45,3 +45,4 @@ src/back/routes/urls.ts
src/includes.css
.eslint-result.json
lint-report.json
/src/style-entrypoints/

View File

@ -16,7 +16,7 @@ npm install
```
npx sealgen make-env
docker-compose up -d db mailcatcher
docker-compose up -d db mailcatcher meilisearch
npm run watch
```
@ -41,3 +41,15 @@ And then
```
npm run test
```
The tests don't run webhint by default - it's delegated to run on Jenkins.
If Jenkins complains about a webhint issue and you'd like to reproduce that on your local machine, run
```
npm run full-test
```
The tests run in pararell mode by default, to speed things up. Because of that, the
`.only` syntax doesn't work. To run tests without pararell mode, run `npm run full-test
--serial` - you can then use the `.only` syntax to run a particular test

View File

@ -3,7 +3,7 @@ services:
db:
image: mongo:4.4-bionic
ports:
- "127.0.0.1:${PORT:-2074}7:27017"
- "127.0.0.1:${PORT:-2076}7:27017"
test:
image: sealious-app:latest
build:

View File

@ -1,4 +1,4 @@
FROM node:18-bullseye-slim
FROM node:21-bullseye-slim
ENV HOME=/opt/sealious-app
@ -11,7 +11,9 @@ RUN apt install -y git
RUN apt install -y tmux
# playwright deps
RUN apt-get update&& apt-get install -y --no-install-recommends libasound2 libatk-bridge2.0-0 libatk1.0-0 libatspi2.0-0 libcairo2 libcups2 libdbus-1-3 libdrm2 libgbm1 libglib2.0-0 libnspr4 libnss3 libpango-1.0-0 libwayland-client0 libx11-6 libxcb1 libxcomposite1 libxdamage1 libxext6 libxfixes3 libxkbcommon0 libxrandr2 xvfb fonts-noto-color-emoji fonts-unifont libfontconfig1 libfreetype6 xfonts-cyrillic xfonts-scalable fonts-liberation fonts-ipafont-gothic fonts-wqy-zenhei fonts-tlwg-loma-otf fonts-freefont-ttf libcairo-gobject2 libdbus-glib-1-2 libgdk-pixbuf-2.0-0 libgtk-3-0 libharfbuzz0b libpangocairo-1.0-0 libx11-xcb1 libxcb-shm0 libxcursor1 libxi6 libxrender1 libxtst6 gstreamer1.0-libav gstreamer1.0-plugins-bad gstreamer1.0-plugins-base gstreamer1.0-plugins-good libegl1 libenchant-2-2 libepoxy0 libevdev2 libgles2 libglx0 libgstreamer-gl1.0-0 libgstreamer-plugins-base1.0-0 libgstreamer1.0-0 libgudev-1.0-0 libharfbuzz-icu0 libhyphen0 libicu67 libjpeg62-turbo liblcms2-2 libmanette-0.2-0 libnotify4 libopengl0 libopenjp2-7 libopus0 libpng16-16 libproxy1v5 libsecret-1-0 libsoup2.4-1 libwayland-egl1 libwayland-server0 libwebp6 libwebpdemux2 libwoff1 libxml2 libxslt1.1 libatomic1 libevent-2.1-7
RUN apt-get update&& apt-get install -y --no-install-recommends libasound2 libatk-bridge2.0-0 libatk1.0-0 libatspi2.0-0 libcairo2 libcups2 libdbus-1-3 libdrm2 libgbm1 libglib2.0-0 libnspr4 libnss3 libpango-1.0-0 libwayland-client0 libx11-6 libxcb1 libxcomposite1 libxdamage1 libxext6 libxfixes3 libxkbcommon0 libxrandr2 xvfb fonts-noto-color-emoji fonts-unifont libfontconfig1 libfreetype6 xfonts-cyrillic xfonts-scalable fonts-liberation fonts-ipafont-gothic fonts-wqy-zenhei fonts-tlwg-loma-otf fonts-freefont-ttf libcairo-gobject2 libdbus-glib-1-2 libgdk-pixbuf-2.0-0 libgtk-3-0 libharfbuzz0b libpangocairo-1.0-0 libx11-xcb1 libxcb-shm0 libxcursor1 libxi6 libxrender1 libxtst6 gstreamer1.0-libav gstreamer1.0-plugins-bad gstreamer1.0-plugins-base gstreamer1.0-plugins-good libegl1 libenchant-2-2 libepoxy0 libevdev2 libgles2 libglx0 libgstreamer-gl1.0-0 libgstreamer-plugins-base1.0-0 libgstreamer1.0-0 libgudev-1.0-0 libharfbuzz-icu0 libhyphen0 libicu67 libjpeg62-turbo liblcms2-2 libmanette-0.2-0 libnotify4 libopengl0 libopenjp2-7 libopus0 libpng16-16 libproxy1v5 libsecret-1-0 libsoup2.4-1 libwayland-egl1 libwayland-server0 libwebp6 libwebpdemux2 libwoff1 libxml2 libxslt1.1 libatomic1 libevent-2.1-7 python
RUN apt install wget
RUN chmod +x /tini
ENTRYPOINT ["/tini", "--"]

View File

@ -19,6 +19,5 @@ mkdir -p docker_node_modules
docker compose build
docker compose up -d db
./npm.sh --no-TTY ci && ./npm.sh --no-TTY run build
rm -f log.html

View File

@ -17,11 +17,14 @@ export SEALIOUS_BASE_URL
node -e 'console.log(JSON.stringify(JSON.parse(fs.readFileSync("./.eslint-result.json")).map(fileResult => fileResult.messages.map(message=>({...message, path: fileResult.filePath.replace("/opt/sealious-app/", "")}))).flat().map(message=>({name: message.ruleId, code: message.ruleId, severity: message.severity==1 ? "warning" : "error", path: message.path, line: message.line, char: message.column, description: message.message}))))' \
>lint-report.json
docker-compose up -d meilisearch mailcatcher
CONTAINER_ID=$(docker compose run -d --user="$UID" \
-e "SEALIOUS_MONGO_PORT=27017" \
-e "SEALIOUS_MONGO_HOST=db" \
-e "SEALIOUS_PORT=8080" \
-e "SEALIOUS_BASE_URL=$SEALIOUS_BASE_URL" \
-e "MEILISEARCH_HOST=http://meilisearch:7700" \
-e "SEALIOUS_MAILCATCHER_HOST=mailcatcher" \
-e "SEALIOUS_MAILCATCHER_API_PORT=1080" \
-e "SEALIOUS_MAILCATCHER_SMTP_PORT=1025" \

View File

@ -4,9 +4,12 @@ SEALIOUS_PORT="${PORT}0"
SEALIOUS_BASE_URL=$(cat .base_url)
export SEALIOUS_BASE_URL
./npm.sh --no-TTY ci
./npm.sh --no-TTY --user="$UID" run build:front
docker compose up -d mailcatcher
docker compose up -d meilisearch
docker compose run --user="$UID" \
-e "SEALIOUS_MONGO_PORT=27017" \
@ -14,6 +17,7 @@ docker compose run --user="$UID" \
-e "SEALIOUS_PORT=$SEALIOUS_PORT" \
-e "SEALIOUS_BASE_URL=$SEALIOUS_BASE_URL" \
-e "SEALIOUS_MAILER=mailcatcher" \
-e "MEILISEARCH_HOST=http://meilisearch:7700" \
-p "${SEALIOUS_PORT}:${SEALIOUS_PORT}" \
-d \
test \

View File

@ -1,3 +1,5 @@
{
"delay": "500"
"delay": "200",
"ext": "js",
"ignore": ["public"]
}

5601
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -37,31 +37,72 @@
"npm run start-watch"
]
},
"sealgen": {
"styleDirs": [
"node_modules/@sealcode/jdd-editor/assets"
],
"controllerDirs": [
"node_modules/@sealcode/jdd-editor/src/controllers"
],
"copyToPublic": [
{
"from": "node_modules/@sealcode/jdd-editor/assets/icons",
"to": "dist/jdd-page/icons"
},
{
"from": "node_modules/@sealcode/simplemde/dist/simplemde.min.css",
"to": "dist/simplemde.min.css"
},
{
"from": "node_modules/@sealcode/simplemde/dist/simplemde.min.js",
"to": "dist/simplemde.min.js"
},
{
"from": "node_modules/@sealcode/sealcodemirror/lib/codemirror.js",
"to": "dist/codemirror.js"
},
{
"from": "node_modules/@sealcode/sealcodemirror/lib/codemirror.css",
"to": "dist/codemirror.css"
},
{
"from": "node_modules/@sealcode/sealcodemirror/mode/javascript/javascript.js",
"to": "dist/codemirror-javascript-mode.js"
}
]
},
"author": "",
"license": "ISC",
"dependencies": {
"@babel/core": "^7.12.10",
"@hotwired/turbo": "^8.0.2",
"@koa/router": "^12.0.1",
"@sealcode/crud-ui": "^0.0.17",
"@sealcode/file-manager": "^1.0.2",
"@sealcode/jdd": "^0.5.1",
"@sealcode/sealgen": "^0.15.47",
"@sealcode/jdd": "^0.6.5",
"@sealcode/jdd-editor": "^0.1.5",
"@sealcode/sealgen": "^0.17.5",
"@sealcode/simplemde": "^1.12.0-beta12",
"@sealcode/ts-predicates": "^0.6.2",
"@types/kill-port": "^2.0.0",
"@types/leaflet": "^1.9.8",
"@types/turndown": "^5.0.4",
"@types/object-hash": "^3.0.6",
"@types/simplemde": "^1.11.11",
"dotenv": "^16.4.5",
"escape-goat": "^4.0.0",
"get-port": "^7.0.0",
"js-convert-case": "^4.2.0",
"koa-responsive-image-router": "^0.2.29",
"koa-responsive-image-router": "^0.2.37",
"locreq": "^3.0.0",
"meilisearch": "^0.41.0",
"multiple-scripts-tmux": "^1.0.4",
"nodemon": "^3.0.1",
"object-path": "^0.11.8",
"qs": "^6.12.0",
"sealious": "^0.19.9",
"sealious": "^0.19.36",
"slug": "^9.1.0",
"stimulus": "^3.2.2",
"tempstream": "^0.4.1",
"tempstream": "^0.4.5",
"throttle-debounce": "^5.0.2",
"turndown": "^7.2.0"
},
@ -71,12 +112,15 @@
"@types/koa__router": "^12.0.4",
"@types/node": "^20.8.4",
"@types/object-path": "^0.11.4",
"@types/slug": "^5.0.9",
"@types/tedious": "^4.0.7",
"@types/throttle-debounce": "^5.0.2",
"@types/turndown": "^5.0.4",
"@types/uuid": "^9.0.8",
"@typescript-eslint/eslint-plugin": "7.4",
"@typescript-eslint/parser": "7.4",
"axios": "^1.6.2",
"chalk": "^5.3.0",
"eslint": "8.57",
"eslint-config-prettier": "^7.2.0",
"eslint-plugin-prettier": "^3.3.1",
@ -87,7 +131,7 @@
"prettier": "^2.2.1",
"ts-loader": "^8.0.14",
"ts-node": "^10.9.2",
"typescript": "^4.7"
"typescript": "^5.6"
},
"engines": {
"node": ">=21.0.0"

View File

@ -1,4 +1,6 @@
import _locreq from "locreq";
import type { Context } from "koa";
import type { FlatTemplatable } from "tempstream";
import type { Context as SealiousContext } from "sealious";
import { App, LoggerMailer, SMTPMailer } from "sealious";
import type { LoggerLevel } from "sealious/@types/src/app/logger.js";
@ -27,6 +29,25 @@ declare module "koa" {
}
}
declare module "@sealcode/sealgen" {
interface HTMLOptions {
preserveScroll?: boolean;
morphing?: boolean;
navbar?: (ctx: Context) => FlatTemplatable;
autoRefreshCSS?: boolean;
disableCopyEvent?: boolean;
language?: string;
showBanner?: boolean;
canHaveStreams?: boolean;
animateTitle?: boolean;
bodyClasses?: string[];
showFooter: boolean;
showBottomNavbar: boolean;
loadHamburgerMenu: boolean;
loadSearchModal: boolean;
}
}
export default class TheApp extends App {
FileManager = TheFileManager;
config = {

View File

@ -1,29 +1,32 @@
// DO NOT EDIT! This file is generated automaticaly with 'npm run generate-collections'
import { App } from "sealious";
import _Globals from "./globals.js";
import _GroupsToUsers from "./groups-to-users.js";
import _Groups from "./groups.js";
import _Page from "./page.js";
import _PasswordResetIntents from "./password-reset-intents.js";
import _Secrets from "./secrets.js";
import _Tasks from "./tasks.js";
import _Redirects from "./redirects.js";
import _UserRoles from "./user-roles.js";
import _Users from "./users.js";
export const Globals = new _Globals();
export const GroupsToUsers = new _GroupsToUsers();
export const Groups = new _Groups();
export const Page = new _Page();
export const PasswordResetIntents = new _PasswordResetIntents();
export const Secrets = new _Secrets();
export const Tasks = new _Tasks();
export const Redirects = new _Redirects();
export const UserRoles = new _UserRoles();
export const Users = new _Users();
export const collections = {
...App.BaseCollections,
globals: Globals,
"groups-to-users": GroupsToUsers,
groups: Groups,
page: Page,
"password-reset-intents": PasswordResetIntents,
secrets: Secrets,
tasks: Tasks,
redirects: Redirects,
"user-roles": UserRoles,
users: Users,
};

View File

@ -0,0 +1,15 @@
import { Collection, FieldTypes, Policies } from "sealious";
import { Roles } from "../policy-types/roles.js";
export default class Globals extends Collection {
fields = {
key: new FieldTypes.Text(),
value: new FieldTypes.Text(),
};
defaultPolicy = new Roles(["admin"]);
policies = {
show: new Policies.Public(),
delete: new Policies.Noone(),
create: new Policies.Super(),
};
}

View File

@ -0,0 +1,16 @@
import { Collection, FieldTypes, Policies } from "sealious";
import { CRUDRoles } from "../policy-types/roles.js";
export default class Page extends Collection {
fields = {
url: new FieldTypes.Text().setRequired(true),
content: new FieldTypes.JDD().setRequired(true),
domain: new FieldTypes.Text(),
title: new FieldTypes.Text(),
heading: new FieldTypes.Text(),
description: new FieldTypes.Text(),
imageForMetadata: new FieldTypes.Image().setRequired(true),
hideNavigation: new FieldTypes.Boolean(),
};
policies = CRUDRoles(["page"], { show: new Policies.Public() });
}

View File

@ -0,0 +1,12 @@
import { Collection, FieldTypes, Policies } from "sealious";
import { CRUDRoles, Roles } from "../policy-types/roles.js";
export default class Redirects extends Collection {
fields = {
from_url: new FieldTypes.Text().setRequired(true),
to_url: new FieldTypes.Text().setRequired(true),
type: new FieldTypes.Enum(["301", "302"]).setRequired(true),
};
policies = CRUDRoles(["quick-filters"], { show: new Policies.Public() });
}

View File

@ -1,10 +0,0 @@
import { Collection, FieldTypes } from "sealious";
import { Roles } from "../policy-types/roles.js";
/* For testing the Roles policy */
export default class Secrets extends Collection {
fields = {
content: new FieldTypes.Text(),
};
defaultPolicy = new Roles(["admin"]);
}

View File

@ -1,21 +0,0 @@
import { Collection, FieldTypes, Policies } from "sealious";
export default class Tasks extends Collection {
fields = {
title: new FieldTypes.Text(),
done: new (class extends FieldTypes.Boolean {
hasDefaultValue = () => true;
async getDefaultValue() {
return false;
}
})(),
};
policies = {
create: new Policies.Public(),
show: new Policies.Owner(),
list: new Policies.Owner(),
};
defaultPolicy = new Policies.Public();
}

View File

@ -21,6 +21,7 @@ describe("user-roles", () => {
);
},
(e) => {
console.error(e);
assert.equal(
e?.response.data.data.field_messages.role?.message,
"Missing value for field 'role'."

View File

@ -5,17 +5,18 @@ import { Roles } from "../policy-types/roles.js";
export default class UserRoles extends Collection {
name = "user-roles";
fields = {
role: new FieldTypes.Enum((app: App) =>
app.ConfigManager.get("roles")
).setRequired(true),
role: new FieldTypes.Text(),
user: new FieldTypes.SingleReference("users"),
};
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
policies = {
create: new Roles(["admin"]),
delete: new Policies.Public(),
show: new Policies.UserReferencedInField("user"),
delete: new Roles(["admin"]),
show: new Policies.Or([
new Policies.UserReferencedInField("user"),
new Roles(["admin"]),
]),
edit: new Policies.Noone(),
} as { [policy: string]: Policy }; // this `as` statement allows the policies to be overwritten;
@ -27,7 +28,7 @@ export default class UserRoles extends Collection {
throw new Error("roles undefined");
}
for (const action of <const>["create", "delete"]) {
const policy = roles.getPolicy(action);
const policy = roles?.getPolicy(action);
if (policy instanceof Policies.Public) {
app.Logger.warn(
"USER POLICY",

View File

@ -3,6 +3,7 @@ import { App, Collections, FieldTypes, Policies } from "sealious";
import assert from "assert";
import TheApp from "../app.js";
import ADMIN_CREDENTIALS from "../default-admin-credentials.js";
import { Roles } from "../policy-types/roles.js";
export default class Users extends Collections.users {
fields = {
@ -14,18 +15,20 @@ export default class Users extends Collections.users {
}),
};
defaultPolicy = new Policies.Themselves();
defaultPolicy = new Policies.Or([new Policies.Themselves(), new Roles(["admin"])]);
policies = {
create: new Roles(["admin"]),
show: new Policies.Or([new Policies.Themselves(), new Roles(["admin"])]),
};
async init(app: App, name: string) {
assert(app instanceof TheApp);
await super.init(app, name);
app.on("started", async () => {
const username = ADMIN_CREDENTIALS.username;
const users = await app.collections.users
.suList()
.filter({ username })
.fetch();
if (users.empty) {
const all_users = await app.collections.users.suList().fetch();
if (all_users.empty) {
app.Logger.warn(
"ADMIN",
`Creating an admin account for ${app.manifest.admin_email}`
@ -42,15 +45,13 @@ export default class Users extends Collections.users {
}
public static async getRoles(ctx: Context) {
if (!ctx.app.collections["user-roles"]) {
throw new Error("user-roles undefined");
}
const rolesEntries = await ctx.app.collections["user-roles"]
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const rolesEntries = await (ctx.app as TheApp).collections["user-roles"]
.list(ctx)
.filter({ user: ctx.user_id || "" })
.fetch();
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return rolesEntries.items.map((item) => item.get("role") as string);
return rolesEntries.items.map((item) => item.get("role"));
}
}

View File

@ -2,6 +2,10 @@ import _locreq from "locreq";
import { module_dirname } from "./utils/module_dirname.js";
const locreq = _locreq(module_dirname(import.meta.url));
import dotenv from "dotenv";
dotenv.config();
export const SEALIOUS_SANITY = Boolean(process.env.SEALIOUS_SANITY);
export const PORT = process.env.SEALIOUS_PORT
? parseInt(process.env.SEALIOUS_PORT)
@ -18,13 +22,14 @@ export const MAILCATCHER_SMTP_PORT = parseInt(
export const MAILCATCHER_API_PORT = parseInt(
process.env.SEALIOUS_MAILCATCHER_API_PORT || "1082"
);
export const MAILER = process.env.SEALIOUS_MAILER;
export const MAILER = process.env.SEALIOUS_MAILER || "mailcatcher";
export const DEFAULT_HTML_LANG = "pl";
export const IMAGE_CACHE_FS_DIR =
process.env.IMAGE_CACHE_FS_DIR || locreq.resolve("cache/images");
export const SMARTCROP_CACHE_FS_DIR =
process.env.IMAGE_CACHE_FS_DIR || locreq.resolve("cache/smartcrop");
export const UPLOADS_FS_DIR =
process.env.UPLOADS_FS_DIR || locreq.resolve("uploaded_files");
export const MEILISEARCH_MASTER_KEY = process.env.MEILISEARCH_MASTER_KEY || "qwerty";
export const MEILISEARCH_HOST = process.env.MEILISEARCH_HOST || "http://localhost:1098";

86
src/back/defaultHead.tsx Normal file
View File

@ -0,0 +1,86 @@
import { tempstream } from "tempstream";
import type { Readable } from "stream";
import type { Context } from "koa";
import type { HTMLOptions } from "@sealcode/sealgen";
import { htmlEscape } from "escape-goat";
export const start_timestamp = Date.now();
export const animation_script_head = /* HTML */ `<script type="text/javascript">
message = document.title + " • ";
window.title_animation_active = false;
window.animatePageTitle = function () {
if (window.title_animation_active) {
return;
}
window.title_animation_active = true;
function step() {
message = message.substr(1) + message.substr(0, 1);
document.title = message;
}
setInterval(step, 100);
};
</script>`;
export function defaultHead({
ctx,
title,
htmlOptions,
metaImage,
canonicalPath,
css_clumps = [],
description = "",
}: {
ctx: Context;
title: string | Promise<string | Readable>;
htmlOptions: Partial<HTMLOptions>;
metaImage?: string;
canonicalPath?: string;
css_clumps: string[];
description: string;
}): JSX.Element | Readable {
const origin = ctx.URL.origin;
return tempstream/* HTML */ `<title>${title}</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="${htmlEscape(description)}" />
<link
rel="preload"
href="/dist/fonts/Poppins-400-1.woff2"
as="font"
type="font/woff2"
/>
<link
rel="preload"
href="/dist/fonts/Poppins-400-2.woff2"
as="font"
type="font/woff2"
/>
<link rel="preload" href="/Exotc350DmBdEU-subset.ttf" as="font" type="font/ttf" />
<script defer src="/dist/bundle.js?v=${start_timestamp}"></script>
${metaImage ? `<meta property="og:image" content="${metaImage}" />` : ""}
${[
"default",
"page",
...(ctx.url.includes("/dowodzenie/") ? ["admin"] : []),
...css_clumps,
].map(
(clump_name) => /* HTML*/ `<link
href="/dist/${clump_name}.entrypoint.css?v=${start_timestamp}${
htmlOptions.autoRefreshCSS ? `?${Math.random()}${Math.random()}` : ""
}"
rel="stylesheet"
type="text/css"
/>`
)}
<link href="/dist/fonts/fonts.css" rel="stylesheet" type="text/css" />
${canonicalPath
? `<link rel="canonical" href="${origin}${canonicalPath}" />`
: ""}
${htmlOptions.morphing
? `<meta name="turbo-refresh-method" content="morph" />`
: ""}
${htmlOptions.preserveScroll
? `<meta name="turbo-refresh-scroll" content="preserve">`
: ""} `;
}

View File

@ -13,10 +13,8 @@ export default async function PasswordResetTemplate(
if (!matching_users.items.length) {
throw new Errors.NotFound("No user with that email");
}
if (!matching_users.items[0]) {
throw new Error("matching_users.items[0] undefined");
}
const username = matching_users.items[0].get("username");
const username = matching_users.items[0]?.get("username");
return EmailTemplates.Simple(app, {
subject: app.i18n("password_reset_email_subject", [app.manifest.name]),

View File

@ -1,112 +0,0 @@
import { Controller } from "stimulus";
const APP_DOWN_ERROR_MESSAGE = "App is currently down";
const sleep = (time: number) =>
new Promise((resolve) => {
setTimeout(resolve, time);
});
async function get_status(): Promise<{ started_at: number; status: string }> {
const r = await fetch("/status.json");
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return (await r.json()) as { started_at: number; status: string };
}
async function wait_for_run_id_to_change() {
let first_timestamp: number;
try {
const { started_at } = await get_status();
first_timestamp = started_at;
} catch (e) {
await wait_for_app_to_be_stable();
return;
}
if (!first_timestamp) {
throw new Error(APP_DOWN_ERROR_MESSAGE);
}
// eslint-disable-next-line no-constant-condition
while (true) {
// eslint-disable-next-line no-await-in-loop
const { started_at } = await get_status().catch(() => ({
started_at: first_timestamp,
}));
if (started_at !== first_timestamp) {
return;
}
// eslint-disable-next-line no-await-in-loop
await sleep(100);
}
}
async function wait_for_app_to_be_stable(n = 3) {
// eslint-disable-next-line no-console
console.log("Waiting for app to be stable....");
let counter = 0;
// eslint-disable-next-line no-constant-condition
while (true) {
// eslint-disable-next-line no-await-in-loop
const { status } = await get_status().catch(() => ({
status: "down",
}));
if (status == "running") {
// eslint-disable-next-line no-console
console.log(counter);
counter++;
} else {
counter = 0;
}
if (counter == n) {
return;
}
// eslint-disable-next-line no-await-in-loop
await sleep(100);
}
}
async function wait_for_app_restart() {
try {
await wait_for_run_id_to_change();
} catch (e) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (e.message !== APP_DOWN_ERROR_MESSAGE) {
throw e;
}
}
await wait_for_app_to_be_stable();
}
export default class RefreshOnTSChanges extends Controller {
socket: WebSocket;
async connect() {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const { port, watch } = (await fetch("/dist/notifier.json").then((r) =>
r.json()
)) as { port: number; watch: boolean };
if (!watch) {
// eslint-disable-next-line no-console
console.warn(
"Not running auto refresh on watch because the build process is not running in watch mode"
);
return;
}
const socket = new WebSocket(`ws://localhost:${port}`);
socket.onmessage = async (message) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const data = message.data as unknown;
if (typeof data === "string" && data.endsWith("-ts")) {
document.documentElement.classList.add("restarting");
await wait_for_app_restart();
document.documentElement.dispatchEvent(new Event("ts-rebuilt"));
document.documentElement.classList.remove("restarting");
}
};
}
async disconnect() {
this.socket.close();
}
}

View File

@ -1,47 +0,0 @@
import { Controller } from "stimulus";
function make_new_link() {
const new_link = document.createElement("link");
new_link.rel = "stylesheet";
new_link.href = `/dist/main.css?${Math.random()}+${Math.random()}`;
new_link.type = "text/css";
return new_link;
}
function getStyles() {
return Array.from(document.querySelectorAll("head link")).filter(
(e: HTMLLinkElement) => new URL(e.href).pathname == "/dist/main.css"
);
}
function cleanup_css() {
getStyles()
.slice(0, -1)
.forEach((style) => {
style.parentElement.removeChild(style);
});
}
export default class RefreshStyles extends Controller {
socket: WebSocket;
async connect() {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { port } = await fetch("/dist/notifier.json").then((r) => r.json());
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
this.socket = new WebSocket(`ws://localhost:${port as number}`);
this.socket.onmessage = async (message) => {
if (message.data === "css") {
const new_link = make_new_link();
new_link.onload = cleanup_css;
document.querySelector("head").appendChild(new_link);
}
};
document.documentElement.addEventListener("turbo:morph", cleanup_css);
}
async disconnect() {
this.socket.close();
document.documentElement.removeEventListener("turbo:morph", cleanup_css);
}
}

View File

@ -1,71 +0,0 @@
import type { FlatTemplatable, Templatable } from "tempstream";
import { tempstreamAsync } from "tempstream";
import type { Readable } from "stream";
import type { BaseContext } from "koa";
import { toKebabCase } from "js-convert-case";
import { DEFAULT_HTML_LANG } from "./config.js";
import { default_navbar } from "./routes/common/navbar.js";
export const defaultHead = (
ctx: BaseContext,
title: string,
options: HTMLOptions
) => /* HTML */ `<title>${title} · ${ctx.$app.manifest.name}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script async src="/dist/bundle.js"></script>
<link
href="/dist/main.css${options.autoRefreshCSS
? `?${Math.random()}${Math.random()}`
: ""}"
rel="stylesheet"
type="text/css"
/>
<link href="/dist/fonts/fonts.css" rel="stylesheet" type="text/css" />
${options.morphing ? `<meta name="turbo-refresh-method" content="morph" />` : ""}
${options.preserveScroll
? `<meta name="turbo-refresh-scroll" content="preserve">`
: ""}`;
export type HTMLOptions = {
preserveScroll?: boolean;
morphing?: boolean;
navbar?: (ctx: BaseContext) => FlatTemplatable;
autoRefreshCSS?: boolean;
disableCopyEvent?: boolean;
language?: string;
bodyClasses?: string[];
};
export default function html(
ctx: BaseContext,
title: string,
body: Templatable,
htmlOptions: HTMLOptions = {},
makeHead: (
ctx: BaseContext,
title: string,
options: HTMLOptions
) => Templatable = defaultHead
): Readable {
ctx.set("content-type", "text/html;charset=utf-8");
const controllers: string[] = [];
if (htmlOptions.autoRefreshCSS) {
controllers.push("refresh-styles");
controllers.push("refresh-on-ts-changes");
}
return tempstreamAsync/* HTML */ ` <!DOCTYPE html>
<html
lang="${htmlOptions.language || DEFAULT_HTML_LANG}"
class="title--${toKebabCase(title)}"
>
<head>
${makeHead(ctx, title, htmlOptions)}
</head>
<body data-controller="${controllers.join(" ")}">
${(htmlOptions?.navbar || default_navbar)(ctx)} ${body}
${htmlOptions.disableCopyEvent
? /* HTML */ "<script>document.addEventListener('copy', (e) => e.preventDefault());</script>"
: ""}
</body>
</html>`;
}

71
src/back/html.tsx Normal file
View File

@ -0,0 +1,71 @@
import type { HTMLArgs, HTMLOptions } from "@sealcode/sealgen";
import { tempstreamAsync } from "tempstream";
import type { Readable } from "stream";
import { toKebabCase } from "js-convert-case";
import { DEFAULT_HTML_LANG } from "./config.js";
import { default_navbar } from "./routes/common/navbar.js";
import { defaultHead } from "./defaultHead.js";
const default_html_options: Partial<HTMLOptions> = {
showFooter: true,
showBottomNavbar: true,
loadHamburgerMenu: true,
loadSearchModal: true,
};
export default function html({
ctx,
title,
body,
htmlOptions = {},
makeHead = defaultHead,
metaImage,
canonicalPath,
css_clumps = [],
description,
hideNavigation = false,
}: HTMLArgs): Readable {
htmlOptions = { ...default_html_options, ...htmlOptions };
ctx.set(
"content-type",
htmlOptions.canHaveStreams &&
ctx.get("accept").includes("text/vnd.turbo-stream.html")
? "text/vnd.turbo-stream.html"
: "text/html;charset=utf-8"
);
const controllers: string[] = [];
if (htmlOptions.autoRefreshCSS) {
controllers.push("refresh-styles");
controllers.push("refresh-on-ts-changes");
}
return tempstreamAsync/* HTML */ ` <!DOCTYPE html>
<html
lang="${htmlOptions.language || DEFAULT_HTML_LANG}"
class="title--${typeof title == "string" ? toKebabCase(title) : ""}"
>
<head>
<meta
name="google-site-verification"
content="Tf9a888qUgbgQJS4u1F6A8H7ZI_DBJvAeduluSI0T_g"
/>
${makeHead({
ctx,
title: title,
htmlOptions: { ...htmlOptions },
metaImage,
canonicalPath,
css_clumps,
description,
})}
</head>
<body
data-controller="${controllers.join(" ")}"
class="${(htmlOptions?.bodyClasses || []).join(" ")}"
>
${!hideNavigation ? (htmlOptions?.navbar || default_navbar)(ctx) : ""}
${body}
</body>
</html>`;
}

View File

@ -9,4 +9,6 @@ export const imageRouter = new KoaResponsiveImageRouter({
cacheManagerResolutionThreshold: 50,
imageStoragePath: IMAGE_CACHE_FS_DIR,
smartCropStoragePath: SMARTCROP_CACHE_FS_DIR,
formatsForLossy: ["webp"],
formatsForLossless: ["webp"],
});

View File

@ -3,9 +3,9 @@ import _locreq from "locreq";
import TheApp from "./app.js";
import { PORT, SEALIOUS_SANITY } from "./config.js";
import { mainRouter } from "./routes/index.js";
import { module_dirname } from "./utils/module_dirname.js";
import { waitForMeilisearch } from "./services/meilisearch.js";
const locreq = _locreq(module_dirname(import.meta.url));
const locreq = _locreq(new URL("./", import.meta.url).pathname);
export const the_app = new TheApp();
@ -13,14 +13,31 @@ void (async function () {
await kill(PORT);
await kill(PORT);
try {
await waitForMeilisearch(10);
} catch (e) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
if ((e as Error).message === "timeout") {
console.log("Meilisearch must be running before the app starts!"); // eslint-disable-line no-console
process.exit(1);
} else {
throw e;
}
}
try {
the_app.HTTPServer.addStaticRoute("/", locreq.resolve("public"));
await the_app.start();
mainRouter(the_app, the_app.HTTPServer.router);
// eslint-disable-next-line no-console
console.log("Population finished");
if (SEALIOUS_SANITY) {
console.error("Exiting with error code 0");
process.exit(0);
}
mainRouter(the_app.HTTPServer.router);
} catch (error) {
console.error(error);
if (SEALIOUS_SANITY) {

View File

@ -1,6 +1,7 @@
.autoscrolling-images {
display: flex;
justify-content: center;
margin: 2rem 0;
}
.autoscrolling-images-wrapper {
@ -17,6 +18,7 @@
}
.autoscrolling-images__title {
font-family: Exotc350DmBdEU;
font-size: 32px;
color: #0d4d69;
margin: 0;
@ -60,7 +62,13 @@
.autoscrolling-images__carousel-container {
position: relative;
max-width: 940px;
max-width: min(940px, calc(100dvw - 48px));
picture,
img {
max-height: 150px;
max-width: 288px;
}
}
.autoscrolling-images__imgs-carousel {
@ -71,6 +79,7 @@
width: 100%;
display: flex;
flex-flow: row nowrap;
transition: transform 200ms;
}
.autoscrolling-images__carousel-page {
@ -83,10 +92,28 @@
display: flex;
flex-wrap: wrap;
justify-content: space-evenly;
column-gap: 1rem;
max-width: 940px;
}
.autoscrolling-images__carousel-page--looping-head,
.autoscrolling-images__carousel-page--looping-tail {
display: none;
}
.autoscrolling-images__carousel-page--looping-tail {
/* background-color: red; */
position: absolute;
left: 100%;
}
.autoscrolling-images__carousel-page--looping-head {
/* background-color: green; */
position: absolute;
right: 100%;
}
.autoscrolling-images__img-wrapper {
width: 288px;
height: 150px;
@ -123,11 +150,8 @@
0% {
transform: translateX(0%);
}
50% {
transform: translateX(calc(-100% + 100cqw));
}
100% {
transform: translateX(0%);
transform: translateX(calc(-100%));
}
}
@ -172,6 +196,11 @@
animation-name: autoscrolling-images-infiniteScroll;
animation-duration: var(--animation-length);
animation-iteration-count: infinite;
animation-timing-function: ease-in-out;
animation-timing-function: linear;
}
.autoscrolling-images__carousel-page--looping-head,
.autoscrolling-images__carousel-page--looping-tail {
display: flex;
}
}

View File

@ -1,50 +1,156 @@
import { TempstreamJSX } from "tempstream";
import type { ComponentToHTMLArgs } from "@sealcode/jdd";
import type {
ComponentToHTMLArgs,
ExtractStructuredComponentArgumentsParsed,
JDDContext,
} from "@sealcode/jdd";
import { Component, ComponentArguments } from "@sealcode/jdd";
import arrow from "./autoscrolling-images-arrow.svg";
import type { FilePointer } from "@sealcode/file-manager";
import type { makeJDDContext } from "../../jdd-context.js";
import _locreq from "locreq";
const locreq = _locreq(new URL("./", import.meta.url).pathname);
const images = new ComponentArguments.List(
new ComponentArguments.Structured({
image: new ComponentArguments.Image().setExampleImages(
[
"logo1.png",
"logo2.png",
"logo3.png",
"logo4.png",
"logo5.png",
"logo6.png",
"logo7.png",
"logo8.png",
"logo9.png",
].map((name) => locreq.resolve("assets/" + name))
),
alt: new ComponentArguments.ShortText(),
})
);
images.getExampleCount = () => {
return Math.round(4 + Math.random() * 9);
};
const component_arguments = {
title: new ComponentArguments.ShortText(),
interval: new ComponentArguments.ShortText().setExampleValues(["5"]),
imagesPerPage: new ComponentArguments.ShortText().setExampleValues(["6"]),
images: new ComponentArguments.List(
new ComponentArguments.Structured({
image: new ComponentArguments.Image(),
alt: new ComponentArguments.ShortText(),
})
),
speed: new ComponentArguments.ShortText().setExampleValues(["50"]),
desktop_interval: new ComponentArguments.ShortText().setExampleValues(["5"]),
images,
} as const;
function renderimagePageArrows({
radioButtonIdPrefix,
pageIndex,
imagePages,
}: {
radioButtonIdPrefix: string;
pageIndex: number;
imagePages: Array<Array<Record<string, unknown>>>;
}) {
return (
<div class="autoscrolling-images__arrow-container">
<label
for={`${radioButtonIdPrefix}-autoscrolling-images__radio-${
pageIndex == 0 ? imagePages.length - 1 : pageIndex - 1
}`}
class="autoscrolling-images__arrow"
>
<img
class="autoscrolling-images__img-arrow-left"
src={arrow.url}
alt="←"
/>
</label>
<label
for={`${radioButtonIdPrefix}-autoscrolling-images__radio-${
pageIndex == imagePages.length - 1 ? 0 : pageIndex + 1
}`}
class="autoscrolling-images__arrow"
>
<img src={arrow.url} alt="→" />
</label>
</div>
);
}
export function renderImagePage({
page,
render_image,
mode = "regular",
}: {
page: Array<{ image: FilePointer | null; alt: string }>;
render_image: ReturnType<typeof makeJDDContext>["render_image"];
mode?: "regular" | "looping-tail" | "looping-head";
}): JSX.Element {
return (
<div
class={[
"autoscrolling-images__carousel-page",
`autoscrolling-images__carousel-page--${mode}`,
]}
>
{page.map((image) => (
<div class="autoscrolling-images__img-wrapper">
{render_image(image.image, {
container: {
width: 212,
height: 150,
objectFit: "contain",
},
alt: image.alt,
lossless: true,
thumbnailSize: 0,
})}
</div>
))}
</div>
);
}
export class AutoscrollingImages extends Component<typeof component_arguments> {
getArguments() {
return component_arguments;
}
getTitle(
_: JDDContext,
args: ExtractStructuredComponentArgumentsParsed<typeof component_arguments>
) {
return args.title || null;
}
toHTML({
args: { title, interval, imagesPerPage, images },
args: { title, imagesPerPage, images, speed, desktop_interval },
jdd_context: { render_image },
classes,
index,
}: ComponentToHTMLArgs<typeof component_arguments>) {
const imageNumberPerPage = parseInt(imagesPerPage);
const parsedImagesArray = [];
const imagePages: (typeof images)[] = [];
for (let i = 0; i < images.length; i += imageNumberPerPage) {
parsedImagesArray.push(images.slice(i, i + imageNumberPerPage));
imagePages.push(images.slice(i, i + imageNumberPerPage));
}
const radioButtonIdPrefix = "r" + Math.floor(100 + Math.random() * 900);
const radioButton_name = `autoscrolling-images__radio--${radioButtonIdPrefix}`;
const numberOfImages = images.length * 5;
const titleUpperCase = title.toUpperCase();
return (
<div
class="autoscrolling-images"
class={["autoscrolling-images", ...classes]}
style={`--jdd-index: ${index}`}
data-controller="autoscrolling-images"
data-autoscrolling-images-interval={interval}
data-autoscrolling-images-interval={desktop_interval}
>
<style>
{parsedImagesArray
{imagePages
.map(
(_, pageIndex) =>
`#${radioButtonIdPrefix}-autoscrolling-images__radio-${pageIndex}:checked ~ .autoscrolling-images__imgs-carousel > .autoscrolling-images__carousel {
@ -75,7 +181,7 @@ export class AutoscrollingImages extends Component<typeof component_arguments> {
</div>
<div class="autoscrolling-images__carousel-container">
{parsedImagesArray.map((_, pageIndex) => (
{imagePages.map((_, pageIndex) => (
<input
class="autoscrolling-images__radio"
type="radio"
@ -84,72 +190,62 @@ export class AutoscrollingImages extends Component<typeof component_arguments> {
value={pageIndex}
id={`${radioButtonIdPrefix}-autoscrolling-images__radio-${pageIndex}`}
checked={pageIndex === 0}
data-action="autoscrolling-images#handleRadiochange"
data-action="autoscrolling-images#handleRadioChange"
/>
))}
<div class="autoscrolling-images__arrow-carousel-container">
<div class="autoscrolling-images__arrow-carousel">
{parsedImagesArray.map((_, pageIndex) => (
<div class="autoscrolling-images__arrow-container">
<label
for={`${radioButtonIdPrefix}-autoscrolling-images__radio-${
pageIndex == 0
? parsedImagesArray.length - 1
: pageIndex - 1
}`}
class="autoscrolling-images__arrow"
>
<img
class="autoscrolling-images__img-arrow-left"
src={arrow.url}
alt="←"
/>
</label>
<label
for={`${radioButtonIdPrefix}-autoscrolling-images__radio-${
pageIndex == parsedImagesArray.length - 1
? 0
: pageIndex + 1
}`}
class="autoscrolling-images__arrow"
>
<img src={arrow.url} alt="→" />
</label>
</div>
))}
{imagePages.length > 1 ? (
<div class="autoscrolling-images__arrow-carousel-container">
<div class="autoscrolling-images__arrow-carousel">
{imagePages.map((_, pageIndex) =>
renderimagePageArrows({
pageIndex,
imagePages,
radioButtonIdPrefix,
})
)}
</div>
</div>
</div>
) : (
""
)}
<div class="autoscrolling-images__imgs-carousel">
<div
class="autoscrolling-images__carousel"
style={`--animation-length: ${numberOfImages}s`}
style={`--animation-length: ${
100 / parseInt(speed || "15")
}s`}
>
{parsedImagesArray.map((page) => (
<div class="autoscrolling-images__carousel-page">
{page.map((image) => (
<div class="autoscrolling-images__img-wrapper">
{render_image(image.image, {
container: {
width: 288,
height: 150,
objectFit: "contain",
},
alt: image.alt,
})}
</div>
))}
</div>
))}
{renderImagePage({
page: imagePages.at(-1)!,
render_image,
mode: "looping-head",
})}
{imagePages.map((page) =>
renderImagePage({ page, render_image })
)}
{/* for looping endless scroll*/}
{imagePages[0]
? renderImagePage({
page: imagePages[0],
render_image,
mode: "looping-tail",
})
: ""}
</div>
</div>
<div class="autoscrolling-images__dots-container">
{parsedImagesArray.map((_, pageIndex) => (
<label
for={`${radioButtonIdPrefix}-autoscrolling-images__radio-${pageIndex}`}
class="autoscrolling-images__dots"
></label>
))}
</div>
{imagePages.length > 1 ? (
<div class="autoscrolling-images__dots-container">
{imagePages.map((_, pageIndex) => (
<label
for={`${radioButtonIdPrefix}-autoscrolling-images__radio-${pageIndex}`}
class="autoscrolling-images__dots"
></label>
))}
</div>
) : (
""
)}
</div>
</div>
</div>

View File

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */
import { Controller } from "stimulus";
export default class AutoscrollingImages extends Controller {
@ -5,9 +6,10 @@ export default class AutoscrollingImages extends Controller {
interval_id: number;
getInterval(): number {
return parseInt(
this.element.getAttribute("data-autoscrolling-images-interval") || "5"
const result = parseInt(
this.element.getAttribute("data-autoscrolling-images-interval") || "3"
);
return result;
}
getRadioButtons(): Array<HTMLInputElement> {
@ -26,7 +28,7 @@ export default class AutoscrollingImages extends Controller {
let intervalTime: number;
const interval = this.getInterval();
if (!interval) {
if (interval) {
intervalTime = interval * 1000;
} else {
intervalTime = 5000;
@ -45,23 +47,20 @@ export default class AutoscrollingImages extends Controller {
next_slide() {
const carouselPages = this.element.querySelectorAll(
".autoscrolling-images__carousel-page"
".autoscrolling-images__carousel-page--regular"
);
const radioButtonIdPrefix = this.element
.querySelector(".autoscrolling-images-wrapper")
.getAttribute("data-carousel-id-prefix");
const nextIndex = (this.currentIndex + 1) % carouselPages.length;
const nextButton =
const nextButtonID =
radioButtonIdPrefix + "-autoscrolling-images__radio-" + nextIndex;
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
(document.getElementById(nextButton) as HTMLInputElement).checked = true;
this.currentIndex = nextIndex;
// this.handleRadioChange();
const nextButton = this.element.querySelector("#" + nextButtonID);
if (nextButton) {
(nextButton as HTMLInputElement).checked = true;
this.currentIndex = nextIndex;
this.handleRadioChange();
}
}
}

View File

@ -17,11 +17,11 @@
background: #eee;
color: #666;
font-size: 12px;
cursor: pointer;
transition: border 200ms, color 200ms;
label {
padding: 12px 15px;
cursor: pointer;
}
}
.tabs-menu-button:hover {
@ -30,13 +30,6 @@
}
}
.tabs-menu-radio:checked ~ .tabs-menu-button label {
border-bottom-color: #fff;
border-top-color: #b721ff;
background: #fff;
color: #222;
}
.tabs-menu-radio {
display: none;
}
@ -55,6 +48,11 @@
position: relative;
overflow: hidden;
a {
color: #000000;
text-decoration: none;
}
.tile-content {
position: absolute;
background-color: #c3c3c3;
@ -223,9 +221,13 @@
.tab-container {
.tiles-container {
grid-template-columns: 1fr;
max-width: 450px;
max-width: 700px;
margin: 0 auto;
.tile {
max-height: 450px;
}
.tile.horizontal {
grid-column: span 1;
grid-row: span 1;

View File

@ -1,6 +1,11 @@
import type { FlatTemplatable } from "tempstream";
import { TempstreamJSX } from "tempstream";
import type { ComponentToHTMLArgs, ExtractParsed, JDDContext } from "@sealcode/jdd";
import type {
ComponentToHTMLArgs,
ExtractParsed,
ExtractStructuredComponentArgumentsParsed,
JDDContext,
} from "@sealcode/jdd";
import { Component, ComponentArguments } from "@sealcode/jdd";
type ExtractArray<T> = T extends Array<infer X> ? X : never;
@ -58,8 +63,15 @@ export class DynamicGrid extends Component<typeof component_arguments> {
return component_arguments;
}
getTitle(
_: JDDContext,
args: ExtractStructuredComponentArgumentsParsed<typeof component_arguments>
) {
return args.heading || null;
}
public image_sizes = {
square: { width: 400, height: 400 },
square: { width: 700, height: 700 },
horizontal: { width: 824, height: 400 },
vertical: { width: 400, height: 824 },
};
@ -72,28 +84,33 @@ export class DynamicGrid extends Component<typeof component_arguments> {
) {
return (
<div class={["tile", tile.shape]}>
<div class="tile-content">
<div class="tile-content-wrapper">
<h3 class="tile-title">{tile.title}</h3>
<p class="tile-subtitle">{tile.subtitle}</p>
<a href={[tile.url]}>
<div class="tile-content">
<div class="tile-content-wrapper">
<h3 class="tile-title">{tile.title}</h3>
<p class="tile-subtitle">{tile.subtitle}</p>
</div>
<div class="spacer"></div>
</div>
<div class="spacer"></div>
</div>
{(["square", "horizontal", "vertical"] as const).map((shape) => (
<div class={["tile-image", `tile-image--${shape}`]}>
{jdd_context.render_image(tile.photo.image, {
sizesAttr: `${this.image_sizes[shape].width}px`,
alt: tile.photo.alt,
container: {
...this.image_sizes[shape],
objectFit: "cover",
},
crop: this.image_sizes[shape],
imgStyle: "display: none;",
})}
</div>
))}
{(["square", "horizontal", "vertical"] as const).map((shape) => (
<div class={["tile-image", `tile-image--${shape}`]}>
{!tile.photo
? ""
: jdd_context.render_image(tile.photo.image, {
sizesAttr:
"(max-width: 730px) 100vw, (max-width: 980px) 50vw, 810px",
alt: tile.photo.alt,
container: {
...this.image_sizes[shape],
objectFit: "cover",
},
crop: this.image_sizes[shape],
imgStyle: "display: none;",
})}
</div>
))}
</a>
</div>
);
}
@ -119,14 +136,24 @@ export class DynamicGrid extends Component<typeof component_arguments> {
);
}
toHTML({
args: { heading, tabs },
classes,
jdd_context,
}: ComponentToHTMLArgs<typeof component_arguments>): FlatTemplatable {
toHTML(
{
args: { heading, tabs },
jdd_context,
classes,
index,
}: ComponentToHTMLArgs<typeof component_arguments>,
tab_style_when_active = `border-bottom-color: #fff;
border-top-color: #b721ff;
background: #fff;
color: #222;`
): FlatTemplatable {
const { value: id } = generate_id.next();
return (
<div class={["dynamic-grid-component", ...classes]}>
<div
class={["dynamic-grid-component", ...classes]}
style={`--jdd-index: ${index}`}
>
<h2 class="dynamic-grid-title">{heading}</h2>
<ul class="tabs-menu">
{tabs.map((tab, index) => {
@ -137,10 +164,7 @@ export class DynamicGrid extends Component<typeof component_arguments> {
/* HTML */ `<style>
body:has(#input-${id}-${index}:checked)
li:has([for="input-${id}-${index}"]) {
border-bottom-color: #fff;
border-top-color: #b721ff;
background: #fff;
color: #222;
${tab_style_when_active}
}
</style>`
}

View File

@ -0,0 +1,79 @@
.horizontal-gallery {
--max-image-width: 600px;
.horizontal-gallery__top-bar {
display: flex;
gap: 16px;
width: 100%;
h2 {
font-size: 32px;
line-height: 48px;
font-family: Exotc350DmBdEU, sans-serif;
text-transform: uppercase;
text-align: center;
color: #0d4d69;
flex-grow: 1;
margin-top: 0;
@container (max-width: 600px) {
font-size: 28px;
line-height: 34px;
}
}
.horizontal-gallery__buttons,
&::before {
display: flex;
width: 92px;
content: " ";
align-items: center;
flex-shrink: 0;
@container (max-width: 600px) {
display: none !important;
}
}
@container (max-width: 600px) {
&::before {
/* repeated from above, as the above selector somehow does
not apply to the :before pseudo-element */
display: none !important;
}
}
button {
border: none;
background: none;
cursor: pointer;
}
}
.horizontal-scroller__element-container {
scrollbar-width: none;
&::-webkit-scrollbar {
width: 0px;
height: 0;
background: transparent; /* make scrollbar transparent */
}
}
.horizontal-scroller__element {
max-width: 100cqw;
align-items: center;
&,
picture,
img {
border-radius: 10px;
max-height: min(75vh, 700px);
width: auto !important;
max-width: 100cqw;
}
picture {
height: auto;
width: 100% !important;
background-image: none !important;
}
}
}

View File

@ -0,0 +1,83 @@
import type { FlatTemplatable } from "tempstream";
import { TempstreamJSX } from "tempstream";
import type {
ComponentToHTMLArgs,
ExtractStructuredComponentArgumentsParsed,
JDDContext,
} from "@sealcode/jdd";
import { Component, ComponentArguments, insert_nbsp } from "@sealcode/jdd";
import { horizontalScroller } from "../../routes/common/horizontal-scroller/horizontal-scroller.js";
const component_arguments = {
title: new ComponentArguments.ShortText(),
images: new ComponentArguments.List(
new ComponentArguments.Structured({
image: new ComponentArguments.Image(),
alt: new ComponentArguments.ShortText(),
})
),
} as const;
export class HorizontalGallery extends Component<typeof component_arguments> {
getArguments() {
return component_arguments;
}
getTitle(
_: JDDContext,
args: ExtractStructuredComponentArgumentsParsed<typeof component_arguments>
) {
return args.title || null;
}
toHTML({
args: { title, images },
classes,
jdd_context: { render_image },
index,
}: ComponentToHTMLArgs<typeof component_arguments>): FlatTemplatable {
return (
<div
class={["horizontal-gallery", ...classes]}
style={`--jdd-index: ${index}`}
>
{horizontalScroller({
elements: (images || []).map((image) =>
render_image(image.image, {
sizesAttr: "100vw",
container: { width: 800, height: 500, objectFit: "contain" },
alt: image.alt,
})
),
render: async ({ scroller, markers }) => (
<div>
<div class="horizontal-gallery__top-bar">
<h2>{insert_nbsp(title)}</h2>
<div class="horizontal-gallery__buttons">
<button
class="prev-button"
data-action="horizontal-scroller#scrollLeft"
type="button"
>
<span style="transform: rotate(180deg); margin-right: 16px;">
</span>
</button>
<button
class="next-button"
data-action="horizontal-scroller#scrollRight"
type="button"
>
<span></span>
</button>
</div>
</div>
{scroller}
{markers}
</div>
),
})}
</div>
);
}
}

View File

@ -1,6 +1,6 @@
.image-demo {
picture {
width: 200px;
height: 200px;
}
picture {
width: 200px;
height: 200px;
}
}

View File

@ -19,9 +19,11 @@ export class ImageDemo extends Component<typeof component_arguments> {
toHTML({
args: { image_with_alt, multiple_images },
jdd_context: { render_image },
classes,
index,
}: ComponentToHTMLArgs<typeof component_arguments>): FlatTemplatable {
return (
<div class="image-demo">
<div class={["image-demo", ...classes]} style={`--jdd-index: ${index}`}>
<h2>Image with alt text</h2>
{render_image(image_with_alt.image, {
container: { width: 200, height: 200 },

View File

@ -2,20 +2,12 @@ import { TempstreamJSX } from "tempstream";
import type { ComponentToHTMLArgs } from "@sealcode/jdd";
import { Component, ComponentArguments } from "@sealcode/jdd";
const coordinates = new ComponentArguments.ShortText();
coordinates.setExampleValues([
"52.39706859245613, 16.898251247012197",
"52.8, 16.6",
"52.5, 16.1",
"52.1, 16.35",
]);
const component_arguments = {
pins: new ComponentArguments.List(
new ComponentArguments.Structured({
title: new ComponentArguments.ShortText(),
address: new ComponentArguments.ShortText(),
coordinates,
coordinates: new ComponentArguments.ShortText(),
button: new ComponentArguments.Structured({
text: new ComponentArguments.ShortText(),
link: new ComponentArguments.ShortText(),
@ -48,10 +40,15 @@ export class MapWithPins extends Component<typeof component_arguments> {
];
}
toHTML({ args: { pins } }: ComponentToHTMLArgs<typeof component_arguments>) {
toHTML({
args: { pins },
classes,
index,
}: ComponentToHTMLArgs<typeof component_arguments>) {
return (
<div
class="map-with-pins"
class={["map-with-pins bleed-out", ...classes]}
style={`--jdd-index: ${index}`}
data-controller="map-with-pins"
data-map-with-pins-pins-value={JSON.stringify(pins)
.replaceAll("\n", "\\n")

View File

@ -1,3 +1,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/consistent-type-assertions */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Controller } from "stimulus";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -60,6 +64,7 @@ export default class MapWithPins extends Controller {
"loaded-https://unpkg.com/leaflet@1.9.4/dist/leaflet.js",
() => {
this.initiateMap();
(window as any).externalLinks();
}
);
}
@ -68,17 +73,23 @@ export default class MapWithPins extends Controller {
disconnect() {
this.map.remove();
this.initiated = false;
this.resizeObserver.disconnect();
this.resizeObserver?.disconnect();
}
initiateMap() {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
this.map = window.L.map(this.element as HTMLElement);
this.resizeObserver = new ResizeObserver(() => {
this.map.invalidateSize();
this.map = window.L.map(this.element as HTMLElement, {
dragging: "ontouchstart" in document.documentElement,
scrollWheelZoom: false,
});
this.resizeObserver.observe(this.element);
if (window.ResizeObserver) {
this.resizeObserver = new ResizeObserver(() => {
this.map.invalidateSize();
});
}
this.resizeObserver?.observe(this.element);
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
window.L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {

View File

@ -1,5 +1,9 @@
import { TempstreamJSX } from "tempstream";
import type { ComponentToHTMLArgs } from "@sealcode/jdd";
import type {
ComponentToHTMLArgs,
ExtractStructuredComponentArgumentsParsed,
JDDContext,
} from "@sealcode/jdd";
import { Component, ComponentArguments } from "@sealcode/jdd";
const component_arguments = {
@ -18,13 +22,21 @@ export class NiceBox extends Component<typeof component_arguments> {
return component_arguments;
}
getTitle(
_: JDDContext,
args: ExtractStructuredComponentArgumentsParsed<typeof component_arguments>
) {
return args.title || null;
}
async toHTML({
args: { title, content, images },
classes,
jdd_context: { render_markdown, render_image, language },
index,
}: ComponentToHTMLArgs<typeof component_arguments>): Promise<string> {
return (
<div class={["nice-box", ...classes]}>
<div class={["nice-box", ...classes]} style={`--jdd-index: ${index}`}>
<h2>{title}</h2>
<div>{render_markdown(language, content)}</div>
{images.map((image) =>

View File

@ -0,0 +1,9 @@
.image-jdd {
picture {
border-radius: 10px;
img {
width: auto;
border-radius: 10px;
}
}
}

View File

@ -0,0 +1,56 @@
import type { FlatTemplatable } from "tempstream";
import { TempstreamJSX } from "tempstream";
import type {
ComponentToHTMLArgs,
ExtractStructuredComponentArgumentsParsed,
JDDContext,
} from "@sealcode/jdd";
import { Component, ComponentArguments } from "@sealcode/jdd";
import _locreq from "locreq";
const locreq = _locreq(new URL("./", import.meta.url).pathname);
const component_arguments = {
image: new ComponentArguments.Structured({
image: new ComponentArguments.Image().setExampleImages(
["image1.jpg", "image2.jpg", "image3.jpg", "image5.jpg"].map((name) =>
locreq.resolve("assets/" + name)
)
),
alt: new ComponentArguments.ShortText(),
}),
} as const;
export class Photo extends Component<typeof component_arguments> {
getArguments() {
return component_arguments;
}
getTitle(
_: JDDContext,
args: ExtractStructuredComponentArgumentsParsed<typeof component_arguments>
) {
return args.image.alt || null;
}
getHeadings() {
return [];
}
toHTML({
args: { image },
classes,
jdd_context: { render_image },
index,
}: ComponentToHTMLArgs<typeof component_arguments>): FlatTemplatable {
return (
<div class={["image-jdd", ...classes]} style={`--jdd-index: ${index}`}>
{render_image(image.image, {
sizesAttr: "100vw",
alt: image.alt,
})}
</div>
);
}
}

View File

@ -6,13 +6,7 @@ const component_arguments = {
table: new ComponentArguments.Table(
new ComponentArguments.ShortText(),
new ComponentArguments.Structured({
color: new ComponentArguments.Enum(["red", "blue", "green", "aquamarine"]),
word: new ComponentArguments.ShortText().setExampleValues([
"apple",
"banana",
"pineapple",
"carrot",
]),
content: new ComponentArguments.ShortText().setExampleValues([""]),
})
),
} as const;
@ -22,9 +16,13 @@ export class Table extends Component<typeof component_arguments> {
return component_arguments;
}
toHTML({ args: { table } }: ComponentToHTMLArgs<typeof component_arguments>) {
toHTML({
args: { table },
classes,
index,
}: ComponentToHTMLArgs<typeof component_arguments>) {
return (
<div class="table">
<div class={["table", ...classes]} style={`--jdd-index: ${index}`}>
<table>
<tbody>
{table.rows.map((row) =>
@ -40,8 +38,8 @@ export class Table extends Component<typeof component_arguments> {
</tr>
) : (
<tr>
{row.cells.map(({ color, word }) => (
<td style={`color: ${color}`}>{word}</td>
{row.cells.map(({ content }) => (
<td style>{content}</td>
))}
</tr>
)

View File

@ -0,0 +1,6 @@
.tekst.markdown {
font-family: Poppins, sans-serif;
font-size: 16px;
line-height: 24px;
color: #0d4d69;
}

View File

@ -0,0 +1,47 @@
import type { FlatTemplatable } from "tempstream";
import { TempstreamJSX } from "tempstream";
import type {
ComponentToHTMLArgs,
ExtractStructuredComponentArgumentsParsed,
JDDContext,
} from "@sealcode/jdd";
import { Component, ComponentArguments } from "@sealcode/jdd";
const component_arguments = {
content: new ComponentArguments.Markdown(),
} as const;
export class Tekst extends Component<typeof component_arguments> {
getArguments() {
return component_arguments;
}
getTitle(
_: JDDContext,
args: ExtractStructuredComponentArgumentsParsed<typeof component_arguments>
) {
return args.content.slice(0, 20) + "..." || null;
}
toHTML({
args: { content },
classes,
jdd_context: { render_markdown, language },
index,
}: ComponentToHTMLArgs<typeof component_arguments>): FlatTemplatable {
return (
<div class={["tekst markdown", ...classes]} style={`--jdd-index: ${index}`}>
{render_markdown(language, content)}
</div>
);
}
getHeadings(
context: JDDContext,
args: ExtractStructuredComponentArgumentsParsed<{
readonly content: ComponentArguments.Markdown;
}>
) {
return this.getArguments().content.extractHeadings(args.content);
}
}

View File

@ -1,11 +1,11 @@
import type { BaseContext } from "koa";
import type { Context } from "koa";
import type { JDDContext } from "@sealcode/jdd";
import type { FilePointer } from "@sealcode/file-manager";
import { makeSimpleJDDContext } from "@sealcode/jdd";
import { TheFileManager } from "./file-manager.js";
import { imageRouter } from "./image-router.js";
export function makeJDDContext(ctx: BaseContext): JDDContext {
export function makeJDDContext(ctx: Context): JDDContext {
return {
language: "pl",
...makeSimpleJDDContext(TheFileManager),

5
src/back/main.admin.css Normal file
View File

@ -0,0 +1,5 @@
table td .list-actions {
display: flex;
flex-flow: row wrap;
gap: 8px;
}

View File

@ -1,3 +1,4 @@
import { LONG_TEST_TIMEOUT } from "../test_utils/webhint.js";
import { withProdApp } from "../test_utils/with-prod-app.js";
describe("roles", () => {
@ -16,9 +17,10 @@ describe("roles", () => {
email: "admin@example.com",
roles: [],
});
await app.collections["user-roles"].suCreate({
user: admin.id,
role: "admin",
});
}));
})).timeout(LONG_TEST_TIMEOUT);
});

View File

@ -1,4 +1,6 @@
import type { Context } from "sealious";
/* eslint-disable @typescript-eslint/consistent-type-assertions */
import type { ActionName, Collection, Context } from "sealious";
import { Policies } from "sealious";
import { Policy, QueryTypes } from "sealious";
export class Roles extends Policy {
@ -12,17 +14,19 @@ export class Roles extends Policy {
async countMatchingRoles(context: Context) {
const user_id = context.user_id;
context.app.Logger.debug2("ROLES", "Checking the roles for user", user_id);
if (!context.app.collections["user-roles"]) {
throw new Error(`context.app.collections["user-roles"] undefined`);
}
const user_roles = await context.app.collections["user-roles"]
.list(context)
.filter({ user: user_id })
.fetch();
const roles = user_roles.items.map(
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
(user_role) => user_role.get("role") as string
);
const roles = await context.cache("roles_for_this_user", async () => {
const { items: user_roles } = await (
context.app.collections["user-roles"] as Collection
)
.suList()
.filter({ user: user_id })
.fetch();
const roles = user_roles.map(
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
(user_role) => user_role.get("role") as string
);
return roles;
});
return this.allowed_roles.filter((allowed_role) => roles.includes(allowed_role))
.length;
@ -60,3 +64,27 @@ export class Roles extends Policy {
);
}
}
export function CRUDRoles(
collection_names: string[],
fallbacks: Partial<Record<ActionName, Policy>> = {}
) {
const result = Object.fromEntries(
(["show", "create", "edit", "delete"] as ActionName[]).map((action_name) => {
const crud_policy = new Roles([
"admin",
...collection_names.map(
(collection_name) => collection_name + "_" + action_name
),
]);
const fallback_policy = fallbacks[action_name];
return [
action_name,
fallback_policy
? new Policies.Or([crud_policy, fallback_policy])
: crud_policy,
];
})
);
// console.log(collection_names, result);
return result;
}

View File

@ -1,23 +1,23 @@
import { withProdApp } from "../test_utils/with-prod-app.js";
import { VERY_LONG_TEST_TIMEOUT, webhintURL } from "../test_utils/webhint.js";
import { TabsDemoURL } from "./urls.js";
import { HomeURL } from "./urls.js";
import { getBrowser } from "../test_utils/browser-creator.js";
import type { Browser, BrowserContext, Page } from "@playwright/test";
describe("TabsDemo webhint", () => {
describe("Home webhint", () => {
it("doesn't crash", async function () {
return withProdApp(async ({ base_url, rest_api }) => {
await rest_api.get(TabsDemoURL);
await webhintURL(base_url + TabsDemoURL);
await rest_api.get(HomeURL);
await webhintURL(base_url + HomeURL);
// alternatively you can use webhintHTML for faster but less precise scans
// or for scanning responses of requests that use some form of authorization:
// const response = await rest_api.get(TabsDemoURL);
// const response = await rest_api.get(HomeURL);
// await webhintHTML(response);
});
}).timeout(VERY_LONG_TEST_TIMEOUT);
});
describe("TabsDemo", () => {
describe("Home", () => {
let page: Page;
let browser: Browser;
let context: BrowserContext;
@ -34,7 +34,7 @@ describe("TabsDemo", () => {
it("works as expected", async function () {
return withProdApp(async ({ base_url }) => {
await page.goto(base_url + TabsDemoURL);
await page.goto(base_url + HomeURL);
});
}).timeout(VERY_LONG_TEST_TIMEOUT);
});

View File

@ -1,46 +0,0 @@
import type { Context } from "koa";
import { tempstream, TempstreamJSX } from "tempstream";
import { Page } from "@sealcode/sealgen";
import html, { defaultHead } from "../html.js";
import { registry } from "../jdd-components/components.js";
import { documentContainerFromParsed, render, renderEarlyAssets } from "@sealcode/jdd";
import { shuffle } from "../util.js";
import { makeJDDContext } from "../jdd-context.js";
export const actionName = "AllComponents";
export default new (class AllComponentsPage extends Page {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async canAccess(_: Context) {
return { canAccess: true, message: "" };
}
async render(ctx: Context) {
const components = registry.getAll();
const jdd_context = makeJDDContext(ctx);
const document = await Promise.all(
shuffle(Object.entries(components)).map(
async ([component_name, component]) => {
return {
component_name,
args: await component.getExampleValues(jdd_context),
};
}
)
);
return html(
ctx,
"AllComponents",
<div style="max-width: 1024px; margin: 0 auto; container-type: inline-size;">
{render(registry, documentContainerFromParsed(document), jdd_context)}
</div>,
{},
(...args) =>
tempstream`${defaultHead(...args)}${renderEarlyAssets(
registry,
documentContainerFromParsed(document),
jdd_context
)}`
);
}
})();

View File

@ -1,40 +0,0 @@
import { withProdApp } from "../test_utils/with-prod-app.js";
import { VERY_LONG_TEST_TIMEOUT, webhintURL } from "../test_utils/webhint.js";
import { AllComponentsURL } from "./urls.js";
import { getBrowser } from "../test_utils/browser-creator.js";
import type { Browser, BrowserContext, Page } from "@playwright/test";
describe("AllComponents webhint", () => {
it("doesn't crash", async function () {
return withProdApp(async ({ base_url, rest_api }) => {
await rest_api.get(AllComponentsURL);
await webhintURL(base_url + AllComponentsURL);
// alternatively you can use webhintHTML for faster but less precise scans
// or for scanning responses of requests that use some form of authorization:
// const response = await rest_api.get(AllComponentsURL);
// await webhintHTML(response);
});
}).timeout(VERY_LONG_TEST_TIMEOUT);
});
describe("AllComponents", () => {
let page: Page;
let browser: Browser;
let context: BrowserContext;
beforeEach(async () => {
browser = await getBrowser();
context = await browser.newContext();
page = await context.newPage();
});
afterEach(async () => {
await context.close();
});
it("works as expected", async function () {
return withProdApp(async ({ base_url }) => {
await page.goto(base_url + AllComponentsURL);
});
}).timeout(VERY_LONG_TEST_TIMEOUT);
});

View File

@ -63,7 +63,7 @@ export function fullscreenMenu({
}
<div>
{Object.keys(panels).map((panel_id) => (
<span>
<>
<input
type="radio"
name={`${menu_id}__active_panel`}
@ -79,7 +79,7 @@ export function fullscreenMenu({
${full_styles.panel_open}
}
</style>`}
</span>
</>
))}
</div>
<div class="fullscreen-menu__content">

View File

@ -1,7 +1,8 @@
import type { FlatTemplatable } from "tempstream";
import { tempstream, TempstreamJSX } from "tempstream";
import type { Stringifiable } from "tempstream/@types/stringify.js";
const make_id = function* () {
const make_id = (function* () {
let i = 0;
while (true) {
yield i++;
@ -9,7 +10,7 @@ const make_id = function* () {
i = 0;
}
}
};
})();
export async function horizontalScroller({
classes = [],
@ -17,13 +18,13 @@ export async function horizontalScroller({
render = async ({ scroller, markers }) => tempstream`${scroller}${markers}`,
}: {
classes?: string[];
elements: FlatTemplatable[];
elements: (Stringifiable | Promise<Stringifiable>)[];
render?: (options: {
scroller: FlatTemplatable;
markers: FlatTemplatable;
}) => Promise<FlatTemplatable>;
}) {
const id = make_id().next().value;
const id = make_id.next().value;
const scroller_id = `horizontal-scroller-${id}`;
const scroller = (
<div

View File

@ -1,16 +0,0 @@
import html from "../../html.js";
import type { BaseContext } from "koa";
import type { Readable } from "stream";
import { tempstream } from "tempstream";
export function MainView(ctx: BaseContext): Readable {
return html(
ctx,
"",
tempstream/* HTML */ `
<title>My Own ToDo App</title>
<h1>Sealious App</h1>
`
);
}

View File

@ -1,19 +1,13 @@
import type { BaseContext } from "koa";
import type { FlatTemplatable } from "tempstream";
import { SignUpURL, SignInURL, TodoURL, LogoutURL } from "../urls.js";
import { SignInURL, LogoutURL } from "../urls.js";
export async function default_navbar(ctx: BaseContext): Promise<FlatTemplatable> {
const isLoggedIn = !!ctx.$context.session_id;
const linkData = isLoggedIn
? [
{ text: "Logout", url: LogoutURL },
{ text: "To do app", url: TodoURL },
]
: [
{ text: "Sign in", url: SignInURL },
{ text: "Sign up", url: SignUpURL },
];
? [{ text: "Logout", url: LogoutURL }]
: [{ text: "Sign in", url: SignInURL }];
const linksHTML = linkData
.map((link) =>

View File

@ -24,7 +24,7 @@ export function tabs({
content: FlatTemplatable | Promise<FlatTemplatable>;
}[];
default_tab: string;
tab_bar?: FlatTemplatable;
tab_bar?: FlatTemplatable | Promise<FlatTemplatable>;
remember_tab?: boolean;
active_navbar_tab_style?: string;
}) {

View File

@ -1,55 +0,0 @@
import type { BaseContext } from "koa";
import type { CollectionItem } from "sealious";
import frame from "../../frame.js";
import type { Tasks } from "../../collections/collections.js";
export function Task(task: CollectionItem<typeof Tasks>) {
const title = task.get("title");
const title_string = title ? title : "";
return frame(
`task-${task.id}`,
/* HTML */ `<li class="task">
<input
type="checkbox"
data-controller="task"
data-action="task#toggle"
data-id="${task.id}"
${task.get("done") ? "checked" : ""}
/>
${title_string}
<form method="POST" action="/todo/">
<input class="delete-button" type="submit" value="Delete" />
<input
class="hidden-button"
type="hidden"
name="taskId"
value="${task.id}"
/>
<input
class="hidden-button"
type="hidden"
id="action"
name="action"
value="delete"
/>
</form>
</li>`
);
}
/*
<form method="DELETE" action="/todo/${task.id}">
<input class="delete-button" type="submit" value="Delete" />
</form>
*/
export async function TaskList(ctx: BaseContext) {
const { items: tasks } = await ctx.$app.collections.tasks.list(ctx.$context).fetch();
const tasksTemplate = tasks.map(Task).join("\n");
return `
<ul>
${tasksTemplate}
</ul>
`;
}

View File

@ -1,13 +0,0 @@
/* eslint-disable @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-unsafe-assignment */
import { Controller } from "stimulus";
export default class AutogrowTextarea extends Controller<HTMLTextAreaElement> {
connect() {
this.autogrow();
}
autogrow() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
(this.element.parentNode as any).dataset.replicatedValue = this.element.value;
}
}

View File

@ -1,165 +0,0 @@
/* eslint-disable @typescript-eslint/consistent-type-assertions */
/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */
import { Controller } from "stimulus";
export default class ComponentDebugger extends Controller {
declare gutterTarget: HTMLDivElement;
declare checkboxTarget: HTMLInputElement;
declare checkboxTargets: HTMLInputElement[];
declare previewTarget: HTMLDivElement;
declare componentBlockTargets: HTMLDivElement[];
declare componentBlockTargetDisconnected: (e: HTMLDivElement) => void;
static targets = ["gutter", "componentBlock", "checkbox", "preview"];
id: string;
main_form: HTMLFormElement;
is_resizing = false;
origin_x: number;
origin_width: number;
connect() {
this.main_form = document.querySelector("#component-debugger").closest("form");
document.documentElement.addEventListener("ts-rebuilt", () => {
this.main_form.requestSubmit();
});
this.main_form.addEventListener("turbo:submit-end", () => {
// this clears the values of file inputs, so they don't get unecessarily
// re-uploaded on future submissions - the file is alreade there on the server
this.main_form
.querySelectorAll("input[type=file]")
.forEach((input: HTMLInputElement) => (input.value = ""));
});
window.addEventListener("load", () => {
this.update_width_display();
});
document.addEventListener("turbo:render", () => this.update_width_display());
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const gutter = this.gutterTarget;
gutter.addEventListener("mousedown", (e) => {
this.is_resizing = true;
this.origin_x = e.clientX;
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const resizable = this.targets.find("preview") as HTMLSpanElement;
this.origin_width = resizable.getBoundingClientRect().width;
const handler = (e: MouseEvent) => this.resizeHandler(e);
document.addEventListener("mousemove", handler);
document.addEventListener("mouseup", () => {
document.removeEventListener("mousemove", handler);
});
e.preventDefault();
});
}
update_width_display() {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const preview = this.targets.find("preview") as HTMLSpanElement;
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const component_width_element = this.targets.find(
"component-width"
) as HTMLSpanElement;
const component_width = preview.offsetWidth;
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
component_width_element.innerHTML = `(width: ${component_width}px)`;
}
resizeHandler(e: MouseEvent) {
const width_offset = this.origin_x - e.clientX;
const new_width = Math.max(this.origin_width + width_offset, 1);
this.setPreviewWidth(new_width);
this.update_width_display();
}
setPreviewWidth(width: number) {
document
.getElementById("component-debugger")
.style.setProperty("--resizable-column-width", width.toString() + "px");
this.update_width_display();
}
handleWidthDropdown() {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const dropdown = this.targets.find("size-select") as HTMLSelectElement;
const value = dropdown.value;
this.setPreviewWidth(parseInt(value));
}
componentBlockTargetConnected(block_element: HTMLDivElement) {
const index = parseInt(block_element.getAttribute("data-component-index"));
block_element.addEventListener("focusin", () => {
this.scrollToComponentPreview(index);
});
}
previewTargetConnected(preview_element: HTMLDivElement) {
preview_element.addEventListener("click", ({ target }) => {
if (!(target instanceof HTMLElement)) {
return;
}
const closest = target.closest(".jdd-component");
if (!closest) {
return;
}
const index = parseInt(
Array.from(closest.classList)
.find((c) => c.startsWith("component-number-"))
?.replace("component-number-", "")
);
if (isNaN(index)) {
return;
}
this.focusComponentBlock(index);
});
}
focusComponentBlock(index: number) {
const block = this.componentBlockTargets[index];
if (!block) {
return;
}
this.checkboxTargets[index].checked = true;
block.scrollIntoView({ behavior: "smooth" });
(
block.querySelector(".component-preview-parameters input") as HTMLInputElement
)?.focus();
}
getIndex(block_element: HTMLDivElement) {
const index = parseInt(block_element.getAttribute("data-component-index"));
return index;
}
labelClicked(element: MouseEvent) {
const block_element = (element.target as HTMLDivElement).closest(
`[data-component-debugger-target="componentBlock"]`
) as HTMLDivElement;
const index = this.getIndex(block_element);
if (!this.checkboxTargets?.[index].checked) {
this.scrollToComponentPreview(index);
}
}
getPreviewElementForComponentIndex(index: number) {
const element = this.element.querySelector(
`.component-number-${index}`
) as HTMLDialogElement;
return element;
}
scrollToComponentPreview(index: number) {
const element = this.getPreviewElementForComponentIndex(index);
if (!element) {
return;
}
const preview_element = this.element.querySelector(".component-preview");
if (element.clientHeight > preview_element.clientHeight) {
preview_element.scrollTop = element.offsetTop - 44;
} else {
preview_element.scrollTop =
element.offsetTop -
(preview_element.clientHeight - element.clientHeight) / 2 -
44;
}
}
}

View File

@ -1,31 +0,0 @@
import type { Enum } from "@sealcode/jdd";
import { TempstreamJSX } from "tempstream";
import { printArgPath } from "./print-arg-path.js";
export function ComponentInputEnum<State, S extends string, T extends Enum<S>>({
arg_path,
arg,
value,
onchange,
}: {
state: State;
arg_path: string[];
arg: T;
value: string;
onchange?: string;
}) {
return (
<div>
<label>
{arg_path.at(-1) || ""}
<select name={`$${printArgPath(arg_path)}`} onchange={onchange}>
{arg.values.map((v: S) => (
<option value={v} selected={value == v}>
{v}
</option>
))}
</select>
</label>
</div>
);
}

View File

@ -1,72 +0,0 @@
import type { BaseContext } from "koa";
import { TempstreamJSX } from "tempstream";
import type { FilePointer } from "@sealcode/file-manager";
import type { Image } from "@sealcode/jdd";
import type { StatefulPage } from "@sealcode/sealgen";
import type { ComponentPreviewActions } from "./component-preview-actions.js";
import type { JDDPageState } from "./jdd-page.js";
import { makeJDDContext } from "../../jdd-context.js";
import { printArgPath } from "./print-arg-path.js";
import { htmlEscape } from "escape-goat";
export function ComponentInputImage<State extends JDDPageState>({
arg_path,
arg,
value,
ctx,
page,
state,
}: {
state: State;
arg_path: string[];
arg: Image;
value: FilePointer | null;
page: StatefulPage<JDDPageState, typeof ComponentPreviewActions>;
ctx: BaseContext;
}): JSX.Element {
const jdd_context = makeJDDContext(ctx);
return (
<div style="margin-bottom: 10px">
<label
style="display: flex; align-items: center; column-gap: 10px;"
data-controller="input-image-preview"
>
{arg_path.at(-1) || ""}
<div class="image-preview-container">
{value &&
jdd_context.render_image(value, {
container: { width: 40, height: 40, objectFit: "cover" },
crop: { width: 40, height: 40 },
style: "height: 40px; width: 40px;",
alt: "",
})}
</div>
<input
type="file"
name={`$${printArgPath(arg_path)}.new`}
value=""
autocomplete="off"
data-action="change->input-image-preview#handleChange"
multiple={arg.hasParent("list")}
/>
</label>
<div>
<input
type="hidden"
name={`$${printArgPath(arg_path)}.old`}
value={htmlEscape(value?.token || "")}
autocomplete="off"
/>
</div>
{page.makeActionButton(
state,
{
action: "remove_file",
label: "❌",
},
arg_path
)}
</div>
);
}

View File

@ -1,81 +0,0 @@
import type { BaseContext } from "koa";
import { TempstreamJSX } from "tempstream";
import type { ComponentArgument, List } from "@sealcode/jdd";
import type { JDDPageState } from "./jdd-page.js";
import type { StatefulPage } from "@sealcode/sealgen";
import { ComponentInput } from "./component-input.js";
import type { ComponentPreviewActions } from "./component-preview-actions.js";
export async function ComponentInputList<
State extends JDDPageState,
T extends ComponentArgument<unknown>
>({
state,
ctx,
arg_path,
arg,
value,
page,
}: {
state: State;
ctx: BaseContext;
arg_path: string[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
arg: List<T, any>;
value: T[];
page: StatefulPage<JDDPageState, typeof ComponentPreviewActions>;
}): Promise<string> {
if (!value) {
value = [];
}
return (
<fieldset>
<legend>{arg_path.at(-1)}</legend>
{value.map((value, i, all_values) => (
<div style="display: flex">
<ComponentInput
{...{
ctx,
state,
arg_path: [...arg_path, i.toString()],
arg: arg.item_type,
value,
page,
}}
/>
{page.makeActionButton(
state,
{
action: "move_array_item_down",
label: "↓",
disabled: i == all_values.length - 1,
},
arg_path,
i
)}
{page.makeActionButton(
state,
{ action: "move_array_item_up", label: "↑", disabled: i == 0 },
arg_path,
i
)}
{page.makeActionButton(
state,
{ action: "remove_array_item", label: "❌" },
arg_path,
i
)}
</div>
))}
{page.makeActionButton(
state,
{
action: "add_array_item",
label: "",
},
arg_path
)}
</fieldset>
);
}

View File

@ -1,39 +0,0 @@
import type { BaseContext } from "koa";
import type { SingleReference } from "@sealcode/jdd";
import { TempstreamJSX } from "tempstream";
import { makeJDDContext } from "../../jdd-context.js";
import { printArgPath } from "./print-arg-path.js";
export async function ComponentInputSingleReference<
State,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
T extends SingleReference<any>
>({
ctx,
arg_path,
arg,
value,
onchange,
}: {
ctx: BaseContext;
state: State;
arg_path: string[];
arg: T;
value: string;
onchange?: string;
}): Promise<string> {
return (
<div>
<label>
{arg_path.at(-1) || ""}
<select name={`$${printArgPath(arg_path)}`} onchange={onchange}>
{(await arg.getValues(makeJDDContext(ctx))).map((v) => (
<option value={v.value} selected={value == v.value}>
{v.label}
</option>
))}
</select>
</label>
</div>
);
}

View File

@ -1,48 +0,0 @@
import type { BaseContext } from "koa";
import type { ComponentArgument, Structured } from "@sealcode/jdd";
import type { StatefulPage } from "@sealcode/sealgen";
import { TempstreamJSX } from "tempstream";
import { ComponentInput } from "./component-input.js";
import type { JDDPageState } from "./jdd-page.js";
import type { ComponentPreviewActions } from "./component-preview-actions.js";
export function ComponentInputStructured<
T extends Structured<Record<string, ComponentArgument<unknown>>>
>({
state,
ctx,
arg_path,
arg,
value,
rerender_callback,
page,
}: {
state: JDDPageState;
ctx: BaseContext;
arg_path: string[];
arg: T;
value: Record<string, unknown>;
rerender_callback?: string;
page: StatefulPage<JDDPageState, typeof ComponentPreviewActions>;
}) {
return (
<fieldset>
<legend>{arg_path.at(-1)}</legend>
{Object.entries(arg.structure).map(([arg_name, arg]) => (
<div>
<ComponentInput
{...{
ctx,
state,
arg_path: [...arg_path, arg_name],
arg,
value: value[arg_name],
rerender_callback,
page,
}}
/>
</div>
))}
</fieldset>
);
}

View File

@ -1,225 +0,0 @@
import type { Table, TableData } from "@sealcode/jdd";
import { isTableHeader } from "@sealcode/jdd";
import type { StatefulPage } from "@sealcode/sealgen";
import type { BaseContext } from "koa";
import { TempstreamJSX } from "tempstream";
import { makeJDDContext } from "../../jdd-context.js";
import { ComponentInput } from "./component-input.js";
import type { ComponentPreviewActions } from "./component-preview-actions.js";
import type { JDDPageState } from "./jdd-page.js";
import add_column_right_icon from "./table-add-column-right.svg";
import add_row_below_icon from "./table-add-row-below.svg";
import add_column_header_icon from "./table-add-row-header-below.svg";
import delete_column_icon from "./table-delete-column.svg";
import delete_row_icon from "./table-delete-row.svg";
import move_column_right_icon from "./table-move-column-right.svg";
import move_row_down_icon from "./table-move-row-down.svg";
export async function ComponentInputTable<
State extends JDDPageState,
CellType,
HeaderType
>({
state,
arg_path,
ctx,
arg,
value,
page,
}: {
state: State;
ctx: BaseContext;
arg_path: string[];
arg: Table<CellType, HeaderType>;
value: TableData<CellType, HeaderType>;
page: StatefulPage<JDDPageState, typeof ComponentPreviewActions>;
}): Promise<string> {
if (!value) {
value = await arg.getEmptyValue(makeJDDContext(ctx));
}
return (
<fieldset>
<legend>{arg_path.at(-1)}</legend>
<div>
<table style="position: relative; /* necessary for sticky th*/">
<tbody>
<tr>
<td></td>
{[...Array(arg.getColumnsCount(value)).keys()].map(
(column_index) => (
<th class="sticky sticky--top subdued">
{page.makeActionButton(
state,
{
action: "remove_table_column",
label: "Remove column",
content: /* HTML */ `<img
width="20"
height="20"
src="${delete_column_icon.url}"
/>`,
},
arg_path,
column_index
)}
{column_index >= arg.getColumnsCount(value) - 1
? ""
: page.makeActionButton(
state,
{
action: "move_table_column_right",
label: "Move column to the right",
content: /* HTML */ `<img
width="20"
height="20"
src="${move_column_right_icon.url}"
/>`,
},
arg_path,
column_index
)}
</th>
)
)}
</tr>
{value.rows.map((row, row_index) => (
<tr>
<td class="sticky sticky--left subdued">
<div style="display: flex; flex-flow: column; row-gap: 5px;">
{page.makeActionButton(
state,
{
action: "remove_table_row",
label: "Remove row",
content: /* HTML */ `<img
width="20"
height="20"
src="${delete_row_icon.url}"
/>`,
},
arg_path,
row_index
)}
{page.makeActionButton(
state,
{
action: "move_table_row_down",
label: "Move this row down",
content: /* HTML */ `<img
width="20"
height="20"
src="${move_row_down_icon.url}"
/>`,
},
arg_path,
row_index
)}
</div>
</td>
{isTableHeader(row) ? (
<th colspan={arg.getColumnsCount(value).toString()}>
<ComponentInput
{...{
state,
ctx,
arg_path: [
...arg_path,
"rows",
row_index.toString(),
"header_content",
],
arg: arg.header_type,
value: row.header_content,
page,
}}
/>
</th>
) : (
row.cells.map((cell, cell_index) => (
<td>
<ComponentInput
{...{
ctx,
state,
arg_path: [
...arg_path,
"rows",
row_index.toString(),
"cells",
cell_index.toString(),
],
arg: arg.cell_type,
value: cell,
page,
}}
/>
</td>
))
)}
{row_index == 0 ? (
<td
class="subdued"
rowspan={value.rows.length.toString()}
>
{page.makeActionButton(
state,
{
action: "add_table_column",
label: "Add column",
content: /* HTML */ `<img
width="20"
height="20"
src="${add_column_right_icon.url}"
/>`,
},
arg_path
)}
</td>
) : (
""
)}
</tr>
))}
<tr>
<td
class="subdued"
colspan={(arg.getColumnsCount(value) + 1).toString()}
>
{page.makeActionButton(
state,
{
action: "add_table_row",
label: "Add table row",
content: /* HTML */ `<img
width="20"
height="20"
src="${add_row_below_icon.url}"
/>`,
},
arg_path,
arg.getColumnsCount(value)
)}
{page.makeActionButton(
state,
{
action: "add_table_row",
label: "Add table header",
content: /* HTML */ `<img
width="20"
height="20"
src="${add_column_header_icon.url}"
/>`,
},
arg_path,
arg.getColumnsCount(value),
"header"
)}
</td>
</tr>
</tbody>
</table>
</div>
</fieldset>
);
}

View File

@ -1,150 +0,0 @@
import { printArgPath } from "./print-arg-path.js";
import type { BaseContext } from "koa";
import type { ComponentArgument, TableData } from "@sealcode/jdd";
import { SingleReference } from "@sealcode/jdd";
import { ComponentArguments, Enum, Image, List, Structured, Table } from "@sealcode/jdd";
import { ComponentInputStructured } from "./component-input-structured.js";
import type { StatefulPage } from "@sealcode/sealgen";
import type { ComponentPreviewActions } from "./component-preview-actions.js";
import { ComponentInputList } from "./component-input-list.js";
import type { JDDPageState } from "./jdd-page.js";
import { ComponentInputEnum } from "./component-input-enum.js";
import { ComponentInputImage } from "./component-input-image.js";
import { ComponentInputTable } from "./component-input-table.js";
import { TempstreamJSX } from "tempstream";
import type { FilePointer } from "@sealcode/file-manager";
import { is, predicates } from "@sealcode/ts-predicates";
import { makeJDDContext } from "../../jdd-context.js";
import { ComponentInputSingleReference } from "./component-input-single-reference.js";
export const actionName = "Components";
const absoluteUrlPattern = "http(s?)(://)((www.)?)(([^.]+).)?([a-zA-z0-9-_]+)";
export async function ComponentInput<State extends JDDPageState, T>({
ctx,
state,
arg_path,
arg,
value,
page,
}: {
state: State;
ctx: BaseContext;
arg_path: string[];
arg: ComponentArgument<T>;
value: T;
page: StatefulPage<JDDPageState, typeof ComponentPreviewActions>;
}): Promise<string> {
if (value === undefined) {
value = await arg.getEmptyValue(makeJDDContext(ctx));
}
if (arg instanceof List) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return ComponentInputList({
ctx,
state,
arg_path,
arg,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
value: value as T[],
page,
});
}
const argType = arg.getTypeName();
const isUrlAbsolute =
arg instanceof ComponentArguments.URL && arg.urlType === "absolute";
const inputType = isUrlAbsolute ? "url" : "text";
if (arg instanceof Structured) {
return ComponentInputStructured({
ctx,
state,
arg_path,
arg,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
value: value as Record<string, unknown>,
page,
});
}
if (arg instanceof SingleReference) {
return ComponentInputSingleReference({
ctx,
state,
arg_path,
arg,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
value: value as string,
onchange: page.rerender(),
});
}
if (arg instanceof Enum) {
return ComponentInputEnum({
state,
arg_path,
arg,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
value: value as string,
onchange: page.rerender(),
});
}
if (arg instanceof Image) {
return ComponentInputImage({
ctx,
state,
arg_path,
arg,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
value: value as FilePointer,
page,
});
}
if (arg instanceof Table) {
return ComponentInputTable({
ctx,
state,
arg_path,
arg,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
value: value as TableData<unknown, unknown>,
page,
});
}
return (
<div>
<label>
{arg_path.at(-1) || ""}
{argType == "markdown" ? (
<div
class="grow-wrap"
data-replicated-value={is(value, predicates.string) ? value : ""}
>
{/* putting the content in the attribute to enable autogrow */}
<textarea
name={`$${printArgPath(arg_path)}`}
onblur={page.rerender()}
cols="40"
data-controller="autogrow-textarea submit-on-input paste-to-markdown"
data-action="autogrow-textarea#autogrow blur->autogrow-textarea#autogrow resize->autogrow-textarea#autogrow submit-on-input#sendValues focus->submit-on-input#makePermanent blur->submit-on-input#makeNotPermanent"
>
{is(value, predicates.string) ? value : ""}
</textarea>
</div>
) : (
<input
type={inputType}
name={`$${printArgPath(arg_path)}`}
value={is(value, predicates.string) ? value : ""}
size="40"
pattern={isUrlAbsolute ? absoluteUrlPattern : undefined}
/>
)}
</label>
</div>
);
}

View File

@ -1,428 +0,0 @@
/* eslint-disable @typescript-eslint/consistent-type-assertions */
import type { Registry, TableData } from "@sealcode/jdd";
import { List, Table } from "@sealcode/jdd";
import type { BaseContext } from "koa";
import { isTableData, isTableRegularRow } from "@sealcode/jdd";
import objectPath from "object-path";
import type { JDDPageState } from "./jdd-page.js";
import { registry } from "../../jdd-components/registry.js";
import { makeJDDContext } from "../../jdd-context.js";
function moveElement<T>(array: Array<T>, fromIndex: number, toIndex: number): Array<T> {
const element = array.splice(fromIndex, 1)[0];
if (!element) {
console.error("element is undefined");
return array;
}
array.splice(toIndex, 0, element);
return array;
}
export function getComponentData(
state: JDDPageState,
arg_path: string[],
registry: Registry
) {
const index_arg = arg_path[1];
if (!index_arg) {
throw new Error("Missing component index in arg path");
}
const component_index = parseInt(index_arg);
const component_args = state.components[component_index]?.args || {};
const component_name = state.components[component_index]?.component_name || "";
const component = registry.get(component_name);
const arg_path_within_component = arg_path.slice(3); // remove "components" and the index of the component and "args"
const [argument, , argument_value] = component?.getArgumentAtPath(
arg_path_within_component,
component_args
) || [null, null, null];
return {
component_index,
component_args,
component_name,
component,
argument,
argument_value,
arg_path_within_component,
};
}
export const ComponentPreviewActions = <const>{
add_array_item: async (
ctx: BaseContext,
state: JDDPageState,
_: Record<string, string>,
arg_path: string[]
) => {
const {
component_name,
component,
argument,
arg_path_within_component,
argument_value,
} = getComponentData(state, arg_path, registry);
if (!component) {
console.error("unknown component: ", component_name);
return state;
}
if (!argument) {
console.error(
"Didn't find a list argument at this path",
arg_path_within_component
);
return state;
}
if (!(argument instanceof List)) {
throw new Error(
`Expected argument in path ${arg_path.join(
"."
)} to be an instance of List`
);
}
objectPath.insert(
state,
arg_path,
await argument.item_type.getExampleValue(makeJDDContext(ctx)),
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
Array.isArray(argument_value) ? argument_value.length : 0
);
return state;
},
remove_array_item: (
_ctx: BaseContext,
state: JDDPageState,
_: Record<string, string>,
arg_path: string[],
index_to_remove: number
): JDDPageState => {
objectPath.del(state, [...arg_path, index_to_remove]);
return state;
},
move_array_item_up: async (
_ctx: BaseContext,
state: JDDPageState,
_inputs: Record<string, string>,
arg_path: string[],
element_index: number
): Promise<JDDPageState> => {
const array_values = objectPath.get(state, arg_path) as unknown[];
const curr = array_values[element_index];
const prev = array_values[element_index - 1];
if (!prev || !curr) {
throw new Error("No element at such index or cannot move it up");
}
[array_values[element_index - 1], array_values[element_index]] = [curr, prev];
return state;
},
move_array_item_down: async (
_ctx: BaseContext,
state: JDDPageState,
_inputs: Record<string, string>,
arg_path: string[],
element_index: number
): Promise<JDDPageState> => {
const array_values = objectPath.get(state, arg_path) as unknown[];
const curr = array_values[element_index];
const next = array_values[element_index + 1];
if (!next || !curr) {
throw new Error("No element at such index or cannot move it up");
}
[array_values[element_index], array_values[element_index + 1]] = [next, curr];
return state;
},
change_component: async (
ctx: BaseContext,
_state: JDDPageState,
inputs: Record<string, unknown>
): Promise<JDDPageState> => {
const component_name = inputs.component;
if (!component_name || typeof component_name !== "string") {
throw new Error(
"Missing input: 'component' for action change_component. It should contain the name of the new component type"
);
}
const component = registry.get(component_name);
if (!component) {
throw new Error(`Unknown or disallowed component name: ${component_name}`);
}
return {
components: [
{
component_name: component_name,
args: (await component?.getExampleValues(makeJDDContext(ctx))) || {},
},
],
};
},
randomize_args: async (
ctx: BaseContext,
state: JDDPageState,
_inputs: Record<string, string>,
component_index_str: string
): Promise<JDDPageState> => {
const { component_index, component } = getComponentData(
state,
["components", component_index_str],
registry
);
const component_data = state.components[component_index];
if (!component_data) {
throw new Error("Missing component data");
}
component_data.args =
(await component?.getExampleValues(makeJDDContext(ctx))) || {};
return {
...state,
};
},
add_table_row: async (
ctx: BaseContext,
state: JDDPageState,
_: Record<string, string>,
arg_path: string[],
columns: number,
type: "header" | "row" = "row"
) => {
const jdd_context = makeJDDContext(ctx);
const { component_args, argument } = getComponentData(state, arg_path, registry);
let row;
if (!argument) {
console.error("Unknown component at path", arg_path);
return state;
}
if (!(argument instanceof Table)) {
throw new Error(
`Expected argument at path ${arg_path.join(".")} to be of type Table`
);
}
if (type == "header") {
row = {
type: "header",
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
header_content: await argument.header_type.getExampleValue(jdd_context),
};
} else {
const cells = [];
for (let i = 0; i < columns; i++) {
// eslint-disable-next-line no-await-in-loop
cells.push(await argument.cell_type.getExampleValue(jdd_context));
}
row = { type: "row", cells };
}
objectPath.insert(
state,
[...arg_path, "rows"],
row,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
((objectPath.get(component_args, [...arg_path, "rows"]) as unknown[]) || [])
.length
);
return state;
},
add_table_column: async (
ctx: BaseContext,
state: JDDPageState,
_: Record<string, string>,
arg_path: string[]
) => {
const { argument } = getComponentData(state, arg_path, registry);
if (!argument) {
console.error("Unknown component at path", arg_path);
return state;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const tableData: TableData<unknown, unknown> = objectPath.get(state, arg_path);
if (!isTableData(tableData)) {
throw new Error("wrong table data");
}
// eslint-disable-next-line @typescript-eslint/no-for-in-array
for (const i in tableData.rows) {
const row = tableData.rows[i];
if (isTableRegularRow(row)) {
// eslint-disable-next-line no-await-in-loop
row.cells.push(await argument.getExampleValue(makeJDDContext(ctx)));
}
}
objectPath.set(state, arg_path, tableData);
return state;
},
remove_table_column: (
_ctx: BaseContext,
state: JDDPageState,
_: Record<string, string>,
arg_path: string[],
column_index_to_remove: number
): JDDPageState => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const tableData: TableData<unknown, unknown> = objectPath.get(state, arg_path);
if (!isTableData(tableData)) {
throw new Error("wrong table data");
}
// eslint-disable-next-line @typescript-eslint/no-for-in-array
for (const i in tableData.rows) {
const row = tableData.rows[i];
if (isTableRegularRow(row)) {
row.cells = row.cells.filter((_, i) => i != column_index_to_remove);
}
}
objectPath.set(state, arg_path, tableData);
return state;
},
remove_table_row: (
_ctx: BaseContext,
state: JDDPageState,
_: Record<string, string>,
arg_path: string[],
row_index: number
): JDDPageState => {
objectPath.del(state, [...arg_path, "rows", row_index]);
return state;
},
move_table_column_right: (
_ctx: BaseContext,
state: JDDPageState,
_: Record<string, string>,
arg_path: string[],
column_index: number
) => {
const { component_args } = getComponentData(state, arg_path, registry);
const last_path_element = arg_path.at(-1);
if (!last_path_element) {
throw new Error("arg path is empty");
}
const data = objectPath.get<unknown>(component_args, last_path_element, "");
if (!isTableData(data)) {
throw new Error("Expected arg value for a table to be properly shaped");
}
for (const row of data.rows) {
if (row.type == "row") {
moveElement(row.cells, column_index, column_index + 1);
}
}
objectPath.set(state, [...arg_path, "rows"], data.rows);
return state;
},
move_table_row_down: (
_ctx: BaseContext,
state: JDDPageState,
_: Record<string, string>,
arg_path: string[],
row_index: number
) => {
const { component_args } = getComponentData(state, arg_path, registry);
const last_path_element = arg_path.at(-1);
if (!last_path_element) {
throw new Error("arg path is empty");
}
const data = objectPath.get<unknown>(component_args, last_path_element, "");
if (!isTableData(data)) {
throw new Error("Expected arg value for a table to be properly shaped");
}
moveElement(data.rows, row_index, row_index + 1);
objectPath.set(state, [...arg_path, "rows"], data.rows);
return state;
},
change_size: (
_ctx: BaseContext,
state: JDDPageState,
inputs: Record<string, string>
) => {
return {
...state,
preview_size: inputs.size,
};
},
add_component: async (
ctx: BaseContext,
state: JDDPageState,
inputs: Record<string, string>
): Promise<JDDPageState> => {
const component_name = inputs.component;
if (!component_name) {
throw new Error("Missing component name");
}
const component = registry.get(component_name);
return {
...state,
components: [
...state.components,
{
component_name: component_name,
args: (await component?.getExampleValues(makeJDDContext(ctx))) || {},
},
],
};
},
remove_component: async (
_ctx: BaseContext,
state: JDDPageState,
_inputs: Record<string, string>,
component_index: number
): Promise<JDDPageState> => {
const newComponentState = [...state.components];
newComponentState.splice(component_index, 1);
return {
...state,
components: newComponentState,
};
},
move_component_up: async (
_ctx: BaseContext,
state: JDDPageState,
_inputs: Record<string, string>,
component_index: number
): Promise<JDDPageState> => {
const newComps = [...state.components];
const prev = newComps[component_index - 1];
const curr = newComps[component_index];
if (!prev || !curr) {
throw new Error("No component at such index or cannot move it up");
}
[newComps[component_index], newComps[component_index - 1]] = [prev, curr];
return { ...state, components: newComps };
},
move_component_down: async (
_ctx: BaseContext,
state: JDDPageState,
_inputs: Record<string, string>,
component_index: number
): Promise<JDDPageState> => {
const newComps = [...state.components];
const next = newComps[component_index + 1];
const curr = newComps[component_index];
if (!next || !curr) {
throw new Error("No component at such index or cannot move it up");
}
[newComps[component_index], newComps[component_index + 1]] = [next, curr];
return { ...state, components: newComps };
},
remove_file: async (
_ctx: BaseContext,
state: JDDPageState,
_: Record<string, string>,
arg_path: string[]
): Promise<JDDPageState> => {
objectPath.set(state, arg_path, null);
return state;
},
};

View File

@ -1,124 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/consistent-type-assertions */
/* eslint-disable @typescript-eslint/no-unused-vars */
import type Router from "@koa/router";
import type { JDDocumentContainer, RawJDDocument } from "@sealcode/jdd";
import {
documentContainerFromStorage,
documentToParsed,
documentToStorage,
} from "@sealcode/jdd";
import type { BaseContext } from "koa";
import type { Collection, CollectionItem, FieldNames } from "sealious";
import { TempstreamJSX } from "tempstream";
import { registry } from "../../jdd-components/registry.js";
import { makeJDDContext } from "../../jdd-context.js";
import JDDCreator from "./jdd-creator.js";
import type { JDDPageState } from "./jdd-page.js";
export const actionName = "ArticleContentEdit";
export abstract class EditJDDField<C extends Collection> extends JDDCreator {
async getID(ctx: BaseContext): Promise<string> {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const id = ctx.params["id"] as string;
if (!id) {
throw new Error("Missing URL parameter: " + "id");
}
return id;
}
abstract getCollection(ctx: BaseContext): C;
async getItem(ctx: BaseContext): Promise<CollectionItem<C>> {
const {
items: [item],
} = await this.getCollection(ctx)
.list(ctx.$context)
.ids([await this.getID(ctx)])
.fetch();
if (!item) {
throw new Error("Couldn't get item of id " + (await this.getID(ctx)));
}
return item;
}
abstract getJDDFieldName(): FieldNames<C["fields"]>;
mount(router: Router, path: string) {
super.mount(router, path);
router.post(path + "save/", async (ctx) => {
const { state } = await this.extractState(ctx);
const item = await this.getItem(ctx);
item.set(
this.getJDDFieldName(),
(
await documentToStorage(registry, makeJDDContext(ctx), {
value: state.components,
} as unknown as JDDocumentContainer<"parsed">)
).value as any
);
await item.save(ctx.$context);
ctx.type = "html";
ctx.status = 422;
if (!state.messages) {
state.messages = [];
}
state.messages.push("Saved!");
ctx.body = this.render(ctx, state);
});
}
async renderHeader(_ctx: BaseContext, _item: CollectionItem<C>) {
return <h1>Edit JDD</h1>;
}
async renderPreParameterButtons(ctx: BaseContext) {
const item = await this.getItem(ctx);
return <div>{this.renderHeader(ctx, item)}</div>;
}
renderParameterButtons(state: JDDPageState) {
{
/*The below button has to be here in order for it to be the default behavior */
}
return (
<div>
<input type="submit" value="Preview" />
<select name="component">
{Object.keys(this.getRegistryComponents()).map((cmp) => (
<option value={cmp}>{cmp}</option>
))}
</select>
{this.makeActionButton(state, {
action: "add_component",
label: "Add component",
})}
<input type="submit" formaction="./save/" value="zapisz" />
</div>
);
}
async getInitialState(ctx: BaseContext) {
const article = await this.getItem(ctx);
const parsed_document = await documentToParsed(
registry,
makeJDDContext(ctx),
documentContainerFromStorage(
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
(article.get(this.getJDDFieldName()) as RawJDDocument) || []
)
);
return {
components: parsed_document.value.map((e) => ({ ...e, open: true })),
};
}
// uncomment to create whitelist of allowed components
// getAllowedComponents() {
// return ["nice-box"];
// }
}

View File

@ -1,33 +0,0 @@
/* https://chriscoyier.net/2023/09/29/css-solves-auto-expanding-textareas-probably-eventually/ */
.grow-wrap {
/* easy way to plop the elements on top of each other and have them both sized based on the tallest one's height */
display: grid;
}
.grow-wrap::after {
/* Note the weird space! Needed to preventy jumpy behavior */
content: attr(data-replicated-value) " ";
/* This is how textarea text behaves */
white-space: pre-wrap;
/* Hidden from view, clicks, and screen readers */
visibility: hidden;
}
.grow-wrap > textarea {
/* You could leave this, but after a user resizes, then it ruins the auto sizing */
resize: none;
/* Firefox shows scrollbar on growth, you can hide like this. */
overflow: hidden;
}
.grow-wrap > textarea,
.grow-wrap::after {
/* Identical styling required!! */
border: 1px solid black;
padding: 0.5rem;
font: inherit;
/* Place on top of each other */
grid-area: 1 / 1 / 2 / 2;
}

View File

@ -1,24 +0,0 @@
import { Controller } from "stimulus";
export default class InputImagePreview extends Controller {
id: string;
handleChange() {
let img = this.element.querySelector("img");
if (!img) {
img = document.createElement("img");
img.setAttribute("style", "height: 40px; width: 40px");
this.element.querySelector(".image-preview-container").appendChild(img);
}
window.URL.revokeObjectURL(img.src);
const new_url = window.URL.createObjectURL(
this.element.querySelector("input").files[0]
);
img.src = new_url;
img.parentNode;
img.parentElement
.querySelectorAll("source")
.forEach((e) => e.parentNode.removeChild(e));
img.style.aspectRatio = "1";
}
}

View File

@ -1,134 +0,0 @@
import type { BaseContext } from "koa";
import { TempstreamJSX } from "tempstream";
import { ComponentPreviewActions } from "./component-preview-actions.js";
import type { JDDPageState } from "./jdd-page.js";
import JDDPage from "./jdd-page.js";
import move_row_down_icon from "./table-move-row-down.svg";
import move_row_up_icon from "./table-move-row-up.svg";
export default abstract class JDDCreator extends JDDPage {
actions = ComponentPreviewActions;
/**
* This method returns list of components allowed in JDD Editor instance.
* If list is empty it will allow all of the components in registry,
* if you overide this function you can decide on what components should
* available.
*/
getAllowedComponents(): string[] {
return [];
}
getRegistryComponents() {
const all_components = super.getRegistryComponents();
const allowed_components = this.getAllowedComponents();
if (allowed_components.length > 0) {
return Object.fromEntries(
Object.entries(all_components).filter(([name]) =>
allowed_components.includes(name)
)
);
}
return all_components;
}
renderParameterButtons(state: JDDPageState) {
{
/*The below button has to be here in order for it to be the default behavior */
}
return (
<div>
<input type="submit" value="Preview" />
<select name="component">
{Object.keys(this.getRegistryComponents()).map((cmp) => (
<option value={cmp}>{cmp}</option>
))}
</select>
{this.makeActionButton(state, {
action: "add_component",
label: "Add component",
})}
</div>
);
}
renderComponentBlock(
ctx: BaseContext,
state: JDDPageState,
component: {
component_name: string;
args: Record<string, unknown>;
},
component_index: number
) {
const checkbox_id = `component_${component_index}_open`;
return (
<div
class={[
"jdd-editor__component-block",
"jdd-editor__component-block--number-" + component_index,
]}
data-component-debugger-target="componentBlock"
data-component-index={component_index.toString()}
>
<summary class="jdd-editor__component-block__top_bar">
{this.makeActionButton(
state,
{ action: "remove_component", label: "❌" },
component_index
)}
{this.makeActionButton(
state,
{
action: "move_component_up",
label: "Move this row up",
content: /* HTML */ `<img
width="20"
height="20"
src="${move_row_up_icon.url}"
/>`,
},
component_index
)}
{this.makeActionButton(
state,
{
action: "move_component_down",
label: "Move this row down",
content: /* HTML */ `<img
width="20"
height="20"
src="${move_row_down_icon.url}"
/>`,
},
component_index
)}
<span>{component.component_name}</span>
<label
class="component-block__handle"
for={checkbox_id}
style="flex-grow: 1"
data-action="click->component-debugger#labelClicked"
>
<span class="jdd-editor__component-block__chevron"> &gt; </span>
</label>
</summary>
<input
type="checkbox"
class="component-collapse-toggle"
name={`$[components][${component_index}][open]`}
data-turbo-permanent
id={checkbox_id}
style="display:none"
data-component-debugger-target="checkbox"
/>
<div class="jdd-editor__component-block__inner">
{super.renderComponentBlock(ctx, state, component, component_index)}
</div>
</div>
);
}
}

View File

@ -1,327 +0,0 @@
import type { Component, RawJDDocument } from "@sealcode/jdd";
import { documentContainerFromParsed } from "@sealcode/jdd";
import { render, renderEarlyAssets } from "@sealcode/jdd";
import { StatefulPage } from "@sealcode/sealgen";
import { hasFieldOfType, hasShape, predicates } from "@sealcode/ts-predicates";
import type { BaseContext } from "koa";
import type { FlatTemplatable, Templatable } from "tempstream";
import { tempstream, TempstreamJSX } from "tempstream";
import html, { defaultHead } from "../../html.js";
import { registry } from "../../jdd-components/components.js";
import { makeJDDContext } from "../../jdd-context.js";
import { ComponentInput } from "./component-input.js";
import { ComponentPreviewActions } from "./component-preview-actions.js";
export const actionName = "Components";
export type JDDPageState = {
components: RawJDDocument;
preview_size?: string;
messages?: string[];
};
export default abstract class JDDPage extends StatefulPage<
JDDPageState,
typeof ComponentPreviewActions
> {
actions = ComponentPreviewActions;
previewSizes = ["320", "600", "800", "1024", "1300", "1920"];
getRegistryComponents() {
return registry.getAll();
}
async getInitialState(ctx: BaseContext) {
const all_components = Object.entries(this.getRegistryComponents());
const first_component = all_components[0];
if (!first_component) {
throw new Error("No defined components!");
}
const [component_name, component] = first_component;
const initial_state = {
components: [
{
component_name: component_name,
args: await component.getExampleValues(makeJDDContext(ctx)),
},
],
};
return initial_state;
}
wrapInLayout(
ctx: BaseContext,
content: Templatable,
state: JDDPageState
): Templatable {
return html(
ctx,
"Components",
content,
{
morphing: true,
preserveScroll: true,
autoRefreshCSS: true,
navbar: () => ``,
bodyClasses: ["jdd-editor"],
},
(...args) =>
tempstream`${defaultHead(...args)}${renderEarlyAssets(
registry,
documentContainerFromParsed(state.components),
makeJDDContext(ctx)
)}`
);
}
async preprocessOverrides(
_ctx: BaseContext,
state: JDDPageState,
overrides: Record<string, unknown>
) {
const jdd_context = makeJDDContext(_ctx);
if (
!hasFieldOfType(
"components",
overrides,
predicates.array(
predicates.shape({
args: predicates.object,
})
)
)
) {
return {};
}
for (const [component_index, { component_name }] of Object.entries(
state.components
)) {
const component = registry.get(component_name);
if (!component) {
throw new Error(`Unknown component: ${component_name}`);
}
const overrides_for_component = overrides.components[
parseInt(component_index)
] || { args: {} };
const promises = Object.entries(component.getArguments()).map(
async ([arg_name, arg]) => {
const value = overrides_for_component.args[arg_name];
if (value) {
const new_value = await arg.receivedToParsed(jdd_context, value);
overrides_for_component.args[arg_name] = new_value;
}
}
);
// eslint-disable-next-line no-await-in-loop
await Promise.all(promises);
}
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return overrides;
}
// eslint-disable-next-line no-unused-vars
abstract renderParameterButtons(_state: JDDPageState): FlatTemplatable;
renderComponentArgs<C extends Component>(
ctx: BaseContext,
state: JDDPageState,
component: C,
args: Record<string, unknown>,
index: number
) {
const jdd_context = makeJDDContext(ctx);
return (
<div
class="component-preview-parameters"
id={`component-preview-parameters--${index}`}
>
{Object.entries(component.getArguments()).map(async ([arg_name, arg]) => (
<ComponentInput
{...{
state,
arg_path: ["components", index.toString(), "args", arg_name],
ctx,
arg,
value:
args[arg_name] === undefined
? await arg.getExampleValue(jdd_context)
: args[arg_name],
onblur: this.rerender(),
page: this,
}}
/>
))}
</div>
);
}
renderComponentBlock(
ctx: BaseContext,
state: JDDPageState,
{
component_name,
args: component_args,
}: {
component_name: string;
args: Record<string, unknown>;
},
component_index: number
) {
const component = registry.get(component_name);
if (!component) {
return null;
}
return this.renderComponentArgs(
ctx,
state,
component,
component_args,
component_index
);
}
async serializeState(ctx: BaseContext, state: JDDPageState) {
const serialized_components = await Promise.all(
state.components.map(async ({ component_name, args }) => {
const component = registry.get(component_name);
const single_result = {
component_name,
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
args: component
? await component.convertParsedToStorage(
makeJDDContext(ctx),
args
)
: {},
};
return single_result;
})
);
const serialized_state = JSON.stringify({ components: serialized_components });
return serialized_state;
}
async deserializeState(ctx: BaseContext, state_string: string) {
const jdd_context = makeJDDContext(ctx);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const raw = JSON.parse(state_string);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
const components_storage = raw.components;
if (!Array.isArray(components_storage)) {
throw new Error(
"'components' key is not an array, got ${components_storage}"
);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const components_parsed = await Promise.all(
components_storage.map(async (entry) => {
if (
!hasShape(
{
component_name: predicates.string,
args: predicates.object,
},
entry
)
) {
throw new Error(
`Expected components[] items to be objects with 'component_name' and 'args' keys, got ${entry}`
);
}
const { component_name, args } = entry;
const component = registry.get(component_name);
if (!component) {
throw new Error("Unknown component: ${component_name}");
}
return {
component_name,
args: await component.convertStorageToParsed(jdd_context, args),
};
})
);
const result = { components: components_parsed };
return result;
}
renderPreParameterButtons(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_ctx: BaseContext,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_state: JDDPageState
): FlatTemplatable | Promise<FlatTemplatable> {
return "";
}
renderMessages(_ctx: BaseContext, state: JDDPageState) {
return (
<ul>
{(state.messages || []).map((e) => (
<li>{e}</li>
))}
</ul>
);
}
render(ctx: BaseContext, state: JDDPageState) {
return (
<div
class="two-column"
id="component-debugger"
style="--resizable-column-width: 50vw"
data-controller="component-debugger"
>
<div class="component-arguments" id="component-arguments">
{this.renderPreParameterButtons(ctx, state)}
{this.renderParameterButtons(state)}
{this.renderMessages(ctx, state)}
{state.components.map((component, component_index) =>
this.renderComponentBlock(ctx, state, component, component_index)
)}
<code style="max-height: 100px; display: block; overflow: hidden; font-size: 9px; color: #a8a8a8; padding: 1rem;">
{this.serializeState(ctx, state)}
</code>
</div>
<div
id="resize-gutter"
class="resize-gutter"
data-component-debugger-target="gutter"
></div>
<div
id="component-preview"
class="component-preview"
data-component-debugger-target="preview"
>
<div class="component-preview__header">
<span>Preview</span>
<span data-component-debugger-target="component-width"></span>
<select
name="size"
autocomplete="off"
class="component-preview-size-select"
data-component-debugger-target="size-select"
data-action="change->component-debugger#handleWidthDropdown"
>
{this.previewSizes.map((size) => (
<option
value={size}
selected={size === (state.preview_size || "800")}
>
{`${size} px`}
</option>
))}
</select>
<noscript>{this.makeActionButton(state, "change_size")}</noscript>
</div>
<div class="jdd-container">
{render(
registry,
documentContainerFromParsed(state.components),
makeJDDContext(ctx)
)}
</div>
</div>
</div>
);
}
}

View File

@ -1,23 +0,0 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unnecessary-type-assertion, @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-unsafe-assignment */
import { Controller } from "stimulus";
import TurndownService from "turndown";
export default class PasteToMarkdown extends Controller<HTMLTextAreaElement> {
connect() {
this.element.addEventListener("paste", (event) => {
if (event.clipboardData.types.includes("text/html")) {
const turndownService = new TurndownService({
headingStyle: "atx",
preformattedCode: true,
} as any);
event.preventDefault();
const html = (event.clipboardData.getData("text/html") as string)
.replaceAll("\n", " ")
// to get rid of some of the style metadata from libreoffice
.replace(/^<!doctype.*<body[^>]*>/i, "");
document.execCommand("insertText", false, turndownService.turndown(html));
return null;
}
});
}
}

View File

@ -1,3 +0,0 @@
export function printArgPath(path: string[]): string {
return path.map((e) => `[${e}]`).join("");
}

View File

@ -1,25 +0,0 @@
/* eslint-disable @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-unsafe-assignment */
import { Controller } from "stimulus";
import { throttle } from "throttle-debounce";
export default class SubmitOnInput extends Controller<HTMLTextAreaElement> {
sendValues: () => void;
connect() {
this.sendValues = throttle(
500,
() => {
this.element.closest("form").requestSubmit();
},
{ noTrailing: false }
);
}
makePermanent() {
// this prevents morphing from overwriting the input value with previous half-dane values - https://github.com/hotwired/turbo/issues/1199
this.element.setAttribute("data-turbo-permanent", "");
}
makeNotPermanent() {
this.element.removeAttribute("data-turbo-permanent");
}
}

View File

@ -1,50 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg
width="533.33331"
height="533.33331"
viewBox="0 0 15.999999 15.999999"
fill="none"
version="1.1"
id="svg2"
sodipodi:docname="table-add-column-right.svg"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<sodipodi:namedview
id="namedview2"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.86875"
inkscape:cx="265.89928"
inkscape:cy="266.47482"
inkscape:window-width="1536"
inkscape:window-height="928"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M 0,16 H 16 V 0 H 0 Z m 1.5,-1.5 v -2 h 3 v 2 z m 4.5,0 v -13 h 8.5 v 13 z M 4.5,1.5 v 2.51959 h -3 V 1.5 Z m -3,4.01959 h 3 V 7.5 h -3 z M 1.5,9 h 3 v 2 h -3 z"
fill="#1f2328"
id="path1" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M 9.5,8.5 V 11 H 11 V 8.5 h 2.5001 V 7 H 11 V 4.5 H 9.5 V 7 H 7 v 1.5 z"
fill="#1F2328"
id="path2"
style="fill:#009e00;fill-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1,50 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg
width="533.33331"
height="533.33331"
viewBox="0 0 15.999999 15.999999"
fill="none"
version="1.1"
id="svg2"
sodipodi:docname="table-add-row-below.svg"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<sodipodi:namedview
id="namedview2"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.86875"
inkscape:cx="265.89928"
inkscape:cy="266.47482"
inkscape:window-width="1536"
inkscape:window-height="928"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M 0,0 H 16 V 16 H 0 Z M 1.5,1.5 V 4 h 3 V 1.5 Z M 6,1.5 V 4 H 9.9999 L 10,1.5 Z m 5.5,0 -10e-5,2.5 H 14.5 V 1.5 Z m 3,4 h -13 v 9 h 13 z"
fill="#1f2328"
id="path1" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M 7.5,9.5 V 7 H 9 v 2.5 h 2.5001 V 11 H 9 v 2.5 H 7.5 V 11 H 5 V 9.5 Z"
fill="#1F2328"
id="path2"
style="fill:#009e00;fill-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1,70 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg
width="533.33331"
height="533.33331"
viewBox="0 0 15.999999 15.999999"
fill="none"
version="1.1"
id="svg2"
sodipodi:docname="table-add-row-header-below.svg"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<sodipodi:namedview
id="namedview2"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.86875"
inkscape:cx="265.89928"
inkscape:cy="266.47482"
inkscape:window-width="1536"
inkscape:window-height="928"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M 0,0 H 16 V 16 H 0 Z M 1.5,1.5 V 4 h 3 V 1.5 Z M 6,1.5 V 4 H 9.9999 L 10,1.5 Z m 5.5,0 -10e-5,2.5 H 14.5 V 1.5 Z m 3,4 h -13 v 9 h 13 z"
fill="#1f2328"
id="path1" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M 4.4849442,9.891467 V 7.692536 H 5.8043028 V 9.891467 H 8.003322 v 1.319359 H 5.8043028 v 2.198931 H 4.4849442 V 11.210826 H 2.286013 V 9.891467 Z"
fill="#1F2328"
id="path2"
style="fill:#009e00;fill-opacity:1;stroke-width:0.879573" />
<g
id="g1"
transform="translate(-3.2633543,-4)">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="m 12.442783,13.891467 v -2.198931 h 1.319359 v 2.198931 h 2.199019 v 1.319359 h -2.199019 v 2.198931 h -1.319359 v -2.198931 z"
fill="#1F2328"
id="path2-6"
style="fill:#009e00;fill-opacity:1;stroke-width:0.879573"
sodipodi:nodetypes="ccccccccccc" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M 16.758927,13.898931 V 11.7 h -1.319359 v 2.198931 h -2.199019 v 1.319359 h 2.199019 v 2.198931 h 1.319359 V 15.21829 Z"
fill="#1F2328"
id="path2-6-7"
style="fill:#009e00;fill-opacity:1;stroke-width:0.879573"
sodipodi:nodetypes="ccccccccccc" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -1,50 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg
width="566.59668"
height="533.33331"
viewBox="0 0 16.9979 15.999999"
fill="none"
version="1.1"
id="svg2"
sodipodi:docname="table-delete-column.svg"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<sodipodi:namedview
id="namedview2"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.86875"
inkscape:cx="265.89928"
inkscape:cy="266.47482"
inkscape:window-width="1536"
inkscape:window-height="928"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="m 16,12 v 4 H 0 V 0 H 16 V 4 H 14.5 V 1.5 H 11 v 13 h 3.5 V 12 Z M 1.5,12.5 v 2 h 3 v -2 z m 4.5,0 v 2 h 3.5 v -2 z M 9.5,4.01957 V 1.5 H 6 v 2.51958 z m -5,1e-5 V 1.5 h -3 v 2.51959 z m 0,1.5 -3,10e-6 V 7.5 h 3 z M 4.5,9 h -3 v 2 h 3 z M 6,9 v 2 H 9.5 V 9 Z M 6,7.5 H 9.5 V 5.51957 l -3.5,1e-5 z"
fill="#1f2328"
id="path1" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M 14.7176,9.0606 15.9373,10.2803 16.9979,9.2197 15.7782,8 16.9979,6.7803 15.9372,5.71967 14.7176,6.9393 13.4979,5.71968 12.4373,6.7803 13.6569,8 l -1.2196,1.2196 1.0606,1.0607 z"
fill="#1F2328"
id="path2"
style="fill:#ff0000;fill-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -1,50 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg
width="533.33331"
height="533.33667"
viewBox="0 0 15.999999 16.0001"
fill="none"
version="1.1"
id="svg2"
sodipodi:docname="table-delete-row.svg"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<sodipodi:namedview
id="namedview2"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.86875"
inkscape:cx="265.89928"
inkscape:cy="266.47482"
inkscape:window-width="1536"
inkscape:window-height="928"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M 16,0 H 0 V 16 H 4 V 14.5 H 1.5 V 9 h 13 v 5.5 H 12 V 16 h 4 z M 1.5,4 V 1.5 h 3 V 4 Z M 6,4 V 1.5 h 4 L 9.9999,4 Z M 11.4999,4 11.5,1.5 h 3 V 4 Z m 0,1.5 H 14.5 v 2 H 11.4998 Z M 4.5,7.5 h -3 v -2 h 3 z m 1.5,0 v -2 h 3.9999 l -10e-5,2 z"
fill="#1f2328"
id="path1" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M 8,14.7804 9.2197,16.0001 10.2803,14.9395 9.0606,13.7197 10.2802,12.5001 9.2196,11.4395 8,12.6591 6.7803,11.4395 5.71968,12.5001 6.9393,13.7197 5.71967,14.9394 6.7803,16.0001 Z"
fill="#1F2328"
id="path2"
style="fill:#ff0000;fill-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -1,48 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg
fill="#000000"
width="800px"
height="800px"
viewBox="0 0 14 14"
role="img"
focusable="false"
aria-hidden="true"
version="1.1"
id="svg2"
sodipodi:docname="gui-table-col-after-svgrepo-com.svg"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<sodipodi:namedview
id="namedview2"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.86875"
inkscape:cx="399.42446"
inkscape:cy="400"
inkscape:window-width="1536"
inkscape:window-height="928"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<path
d="M.52539062.00195313C.23546962.00195313 0 .17085937 0 .38085938V13.623047c0 .2101.23546962.378906.52539062.378906H5.3164062c.2900214 0 .5234375-.168806.5234376-.378906V.38085938c0-.21-.2334162-.37890626-.5234376-.37890626H.52539062zM1.2089844 1.0605469h3.4199218v3.2148437H1.2089844V1.0605469zm0 4.2832031h3.4199218v3.2148438H1.2089844V5.34375zm0 4.2832031h3.4199218V12.84375H1.2089844V9.6269531z"
id="path1" />
<path
fill="green"
d="M9.969294 11.0336q-.2639283 0-.4656736-.1915l-.3880946-.3882q-.1964467-.1966-.1964467-.4709 0-.2795.1964467-.4658l1.5159882-1.5217H6.988903q-.269027 0-.4370814-.194-.1682543-.1941-.1682543-.4684v-.6625q0-.2743.1682543-.4684.1680544-.1941.4370814-.1941h3.642611L9.1155258 4.4865q-.1964467-.1863-.1964467-.4658 0-.2795.1964467-.4658l.3880946-.3882q.1966467-.1966.4656736-.1966.274126 0 .470872.1966l3.368286 3.3693Q14 6.7172 14 7.0018q0 .2795-.191548.471l-3.368286 3.3693q-.201845.1915-.470872.1915z"
id="path2"
style="fill:#0086be;fill-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -1,48 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg
fill="#000000"
width="800"
height="800"
viewBox="0 0 14 14"
role="img"
focusable="false"
aria-hidden="true"
version="1.1"
id="svg2"
sodipodi:docname="table-move-row-up.svg"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<sodipodi:namedview
id="namedview2"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.86875"
inkscape:cx="399.42446"
inkscape:cy="400"
inkscape:window-width="1536"
inkscape:window-height="928"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<path
d="M 0,0.525391 C 0,0.23547 0.16890625,0 0.37890626,0 H 13.621094 C 13.831194,0 14,0.23547 14,0.525391 v 4.7910152 c 0,0.2900214 -0.168806,0.5234375 -0.378906,0.5234376 H 0.37890626 C 0.16890626,5.8398438 0,5.6064276 0,5.3164062 Z M 1.0585937,1.208984 V 4.6289062 H 4.2734374 V 1.208984 Z m 4.2832031,0 V 4.6289062 H 8.5566406 V 1.208984 Z m 4.2832031,0 V 4.6289062 H 12.841797 V 1.208984 Z"
id="path1" />
<path
fill="green"
d="m 11.031647,9.969294 q 0,-0.2639283 -0.1915,-0.4656736 l -0.3882,-0.3880946 q -0.1966,-0.1964467 -0.4709002,-0.1964467 -0.2795,0 -0.4658,0.1964467 l -1.5217,1.5159882 V 6.988903 q 0,-0.269027 -0.194,-0.4370814 -0.1941,-0.1682543 -0.4684,-0.1682543 h -0.6625 q -0.2743,0 -0.4684,0.1682543 -0.1941,0.1680544 -0.1941,0.4370814 v 3.642611 l -1.5216,-1.5159882 q -0.1863,-0.1964467 -0.4658,-0.1964467 -0.2795,0 -0.4658,0.1964467 l -0.3882,0.3880946 q -0.1966,0.1966467 -0.1966,0.4656736 0,0.274126 0.1966,0.470872 l 3.3693,3.368286 Q 6.7152468,14 6.9998468,14 q 0.2795,0 0.471,-0.191548 l 3.3693002,-3.368286 q 0.1915,-0.201845 0.1915,-0.470872 z"
id="path2"
style="fill:#0086be;fill-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -1,26 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg
fill="#000000"
width="800"
height="800"
viewBox="0 0 14 14"
role="img"
focusable="false"
aria-hidden="true"
version="1.1"
id="svg2"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<path
d="M 14,13.474609 C 14,13.76453 13.831094,14 13.621094,14 H 0.378906 C 0.168806,14 0,13.76453 0,13.474609 V 8.6835938 C 0,8.3935724 0.168806,8.1601563 0.378906,8.1601562 h 13.242188 c 0.21,0 0.378906,0.2334162 0.378906,0.5234376 z M 12.941406,12.791016 V 9.3710938 H 9.7265626 v 3.4199222 z m -4.2832028,0 V 9.3710938 H 5.4433594 v 3.4199222 z m -4.2832031,0 V 9.3710938 H 1.158203 v 3.4199222 z"
id="path1" />
<path
fill="green"
d="m 2.968353,4.030706 q 0,0.2639283 0.1915,0.4656736 l 0.3882,0.3880946 q 0.1966,0.1964467 0.4709002,0.1964467 0.2795,0 0.4658,-0.1964467 l 1.5217,-1.5159882 v 3.642611 q 0,0.269027 0.194,0.4370814 0.1941,0.1682543 0.4684,0.1682543 h 0.6625 q 0.2743,0 0.4684,-0.1682543 0.1941,-0.1680544 0.1941,-0.4370814 V 3.368486 l 1.5216,1.5159882 q 0.1863,0.1964467 0.4658,0.1964467 0.2794998,0 0.4657998,-0.1964467 l 0.3882,-0.3880946 q 0.1966,-0.1966467 0.1966,-0.4656736 0,-0.274126 -0.1966,-0.470872 L 7.4659532,0.191548 Q 7.2847532,0 7.0001532,0 q -0.2795,0 -0.471,0.191548 L 3.159853,3.559834 q -0.1915,0.201845 -0.1915,0.470872 z"
id="path2"
style="fill:#0086be;fill-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,93 +0,0 @@
.title--components {
body {
max-width: none;
}
.two-column {
display: grid;
grid-template-columns: 1fr 15px min-content;
}
.resize-gutter {
background-color: gray;
cursor: ew-resize;
height: 100%;
}
.resizable {
overflow-x: auto;
}
transition: transform 200ms, opacity 200ms;
&.restarting {
transform: scale(0.99);
opacity: 0.6;
}
.component-preview {
width: var(--resizable-column-width);
& > fieldset {
min-width: 0; /* default is min-content and that causes overflow*/
max-height: calc(100vh - 75px);
overflow-x: auto;
}
}
.component-arguments {
max-height: calc(100vh - 80px);
overflow-y: auto;
}
}
.component-preview-parameters {
fieldset {
background-color: #80808024;
table {
td,
th {
outline: 0.5px solid #0000006b;
&.subdued > * {
opacity: 50%;
}
&.subdued:hover > * {
opacity: 100%;
}
&.sticky {
position: sticky;
background-color: #ececec;
}
&.sticky--left {
left: 0;
box-shadow: 5px 0px 10px -4px #00000047;
}
&.sticky--top {
top: 0;
box-shadow: 0px 5px 10px -4px #00000047;
}
}
}
}
}
.component-preview {
* {
transition: all 150ms;
}
container-type: inline-size;
}
@media (scripting: none) {
body {
min-width: max-content;
}
}
.component-preview-size-select {
margin-left: 10px;
}

View File

@ -1,91 +1,10 @@
import { documentContainerFromParsed, renderEarlyAssets } from "@sealcode/jdd";
import type { BaseContext } from "koa";
import type { Templatable } from "tempstream";
import { tempstream, TempstreamJSX } from "tempstream";
import type { Stringifiable } from "tempstream/@types/stringify.js";
import html, { defaultHead } from "../html.js";
import { registry } from "../jdd-components/components.js";
import { makeJDDContext } from "../jdd-context.js";
import { ComponentPreviewActions } from "./component-preview/component-preview-actions.js";
import type { JDDPageState } from "./component-preview/jdd-page.js";
import JDDPage from "./component-preview/jdd-page.js";
import { JDDDebugger } from "@sealcode/jdd-editor";
export const actionName = "Components";
import html from "src/back/html.js";
import { registry } from "src/back/jdd-components/registry.js";
import { makeJDDContext } from "src/back/jdd-context.js";
import { defaultHead } from "src/back/defaultHead.js";
export default new (class JddcomponentDebuggerPage extends JDDPage {
renderParameterButtons(state: JDDPageState): Stringifiable {
const all_components = super.getRegistryComponents();
if (!state.components[0]) {
console.error("No components are present in state.components");
}
return (
<div>
<input type="submit" value="Preview" />
<select
name="component"
onchange={this.makeActionCallback("change_component")}
autocomplete="off"
>
{Object.entries(all_components).map(([name]) => (
<option
value={name}
selected={
state.components[0]
? name == state.components[0].component_name
: false
}
>
{name}
</option>
))}
</select>
<noscript>{this.makeActionButton(state, "change_component")}</noscript>
{this.makeActionButton(state, "randomize_args", "0")}
</div>
);
}
actions = ComponentPreviewActions;
export const actionName = "JDDDebugger";
async getInitialState(ctx: BaseContext) {
const component_constants = Object.entries(registry.getAll())[0];
if (!component_constants) {
throw new Error("Constant component_name or component is udefined");
}
const [component_name, component] = component_constants;
const initial_state = {
components: [
{
component_name: component_name,
args: await component.getExampleValues(makeJDDContext(ctx)),
},
],
};
return initial_state;
}
wrapInLayout(
ctx: BaseContext,
content: Templatable,
state: JDDPageState
): Templatable {
return html(
ctx,
"Components",
content,
{
morphing: true,
preserveScroll: true,
autoRefreshCSS: true,
navbar: () => ``,
},
(...args) =>
tempstream`${defaultHead(...args)}${renderEarlyAssets(
registry,
documentContainerFromParsed(state.components),
makeJDDContext(ctx)
)}`
);
}
containerSizes = ["320", "600", "800", "1024", "1300", "1920"];
})();
export default new JDDDebugger({ html, registry, makeJDDContext, defaultHead });

View File

@ -15,10 +15,10 @@
pointer-events: none;
opacity: 0.5;
}
&.has-next .next-button,
&.has-prev .prev-button {
pointer-events: all;
opacity: 1;
}
}
.horizontal-scroller.has-next .next-button,
.horizontal-scroller.has-prev .prev-button {
pointer-events: all;
opacity: 1;
}

View File

@ -13,55 +13,58 @@ export default new (class HorizontalScrollerDemoPage extends Page {
}
async render(ctx: Context) {
return html(
return html({
ctx,
"HorizontalScrollerDemo",
<div>
<h2>Always scrolls + has buttons</h2>
{horizontalScroller({
elements: [1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13].map((n) => (
<div class="bignum">{n}</div>
)),
render: async ({ scroller, markers }) => (
<div>
title: "HorizontalScrollerDemo",
description: "",
body: (
<div>
<h2>Always scrolls + has buttons</h2>
{horizontalScroller({
elements: [1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13].map((n) => (
<div class="bignum">{n}</div>
)),
render: async ({ scroller, markers }) => (
<div>
<button
class="prev-button"
type="button"
data-action="horizontal-scroller#scrollLeft"
>
{" "}
{" "}
</button>
<button
class="next-button"
type="button"
data-action="horizontal-scroller#scrollRight"
>
{" "}
{" "}
</button>
<div>
<button
class="prev-button"
type="button"
data-action="horizontal-scroller#scrollLeft"
>
{" "}
{" "}
</button>
<button
class="next-button"
type="button"
data-action="horizontal-scroller#scrollRight"
>
{" "}
{" "}
</button>
</div>
{scroller}
{markers}
</div>
{scroller}
{markers}
</div>
),
})}
<h2>Sometimes doesn't scroll, then dots are hidden</h2>
{
/* HTML */ `<style>
.horizontal-scroller--all-visible {
.horizontal-scroller__markers {
opacity: 0;
transition: 200ms;
),
})}
<h2>Sometimes doesn't scroll, then dots are hidden</h2>
{
/* HTML */ `<style>
.horizontal-scroller--all-visible {
.horizontal-scroller__markers {
opacity: 0;
transition: 200ms;
}
}
}
</style>`
}
{horizontalScroller({
elements: [1, 2, 3, 4].map((n) => <div class="bignum">{n}</div>),
})}
</div>
);
</style>`
}
{horizontalScroller({
elements: [1, 2, 3, 4].map((n) => <div class="bignum">{n}</div>),
})}
</div>
),
});
}
})();

View File

@ -19,42 +19,48 @@ export default new (class ShowFirstRowDemoPage extends Page {
}
async render(ctx: Context) {
return html(
return html({
ctx,
"ShowFirstRowDemo",
<div>
<h1>showFirstRow() demo</h1>
<h2> 9 elements (always overflows)</h2>
{showFirstRow({
items: [1, 2, 3, 4, 5, 6, 7, 8, 9].map((e) => (
<div style={makeBoxStyle()}>{e}</div>
)),
})}
title: "ShowFirstRowDemo",
description: "",
body: (
<div>
<h1>showFirstRow() demo</h1>
<h2> 9 elements (always overflows)</h2>
{showFirstRow({
items: [1, 2, 3, 4, 5, 6, 7, 8, 9].map((e) => (
<div style={makeBoxStyle()}>{e}</div>
)),
})}
<h2> 3 elements (overflow depends on screen width)</h2>
{showFirstRow({
items: [1, 2, 3].map((e) => <div style={makeBoxStyle()}>{e}</div>),
})}
<h2> 3 elements (overflow depends on screen width)</h2>
{showFirstRow({
items: [1, 2, 3].map((e) => (
<div style={makeBoxStyle()}>{e}</div>
)),
})}
<h2> 9 elements (always has two rows when collapsed)</h2>
{showFirstRow({
items: [1, 2, 3, 4, 5, 6, 7, 8, 9].map((e) => (
<div style={makeBoxStyle()}>{e}</div>
)),
how_many_rows: () => 2,
})}
<h2> 9 elements (always has two rows when collapsed)</h2>
{showFirstRow({
items: [1, 2, 3, 4, 5, 6, 7, 8, 9].map((e) => (
<div style={makeBoxStyle()}>{e}</div>
)),
how_many_rows: () => 2,
})}
<h2>
9 elements (4 rows in one column mode, two rows in two column mode,
one row otherwise)
</h2>
{showFirstRow({
items: [1, 2, 3, 4, 5, 6, 7, 8, 9].map((e) => (
<div style={makeBoxStyle()}>{e}</div>
)),
how_many_rows: (columns) => (columns == 1 ? 4 : columns == 2 ? 2 : 1),
})}
</div>
);
<h2>
9 elements (4 rows in one column mode, two rows in two column
mode, one row otherwise)
</h2>
{showFirstRow({
items: [1, 2, 3, 4, 5, 6, 7, 8, 9].map((e) => (
<div style={makeBoxStyle()}>{e}</div>
)),
how_many_rows: (columns) =>
columns == 1 ? 4 : columns == 2 ? 2 : 1,
})}
</div>
),
});
}
})();

View File

@ -13,25 +13,28 @@ export default new (class SortableDemoPage extends Page {
}
async render(ctx: Context) {
return html(
return html({
ctx,
"SortableDemo",
<div>
<h2>Short list</h2>
{sortable({
items: ["One", "Two", "Three", "Four", "Five"].map((e) => (
<div>{e}</div>
)),
})}
title: "SortableDemo",
description: "",
body: (
<div>
<h2>Short list</h2>
{sortable({
items: ["One", "Two", "Three", "Four", "Five"].map((e) => (
<div>{e}</div>
)),
})}
<h2>Long list</h2>
{sortable({
items: "a"
.repeat(100)
.split("")
.map((_, index) => <div>{index}</div>),
})}
</div>
);
<h2>Long list</h2>
{sortable({
items: "a"
.repeat(100)
.split("")
.map((_, index) => <div>{index}</div>),
})}
</div>
),
});
}
})();

View File

@ -13,76 +13,83 @@ export default new (class FullscreenMenuDemoPage extends Page {
}
async render(ctx: Context) {
return html(
return html({
ctx,
"FullscreenMenuDemo",
<div>
{
/* HTML */ `<style>
.fullscreen-menu__panel--panel_1 {
transform: translateX(-50vw); /* move that one to the left */
}
title: "FullscreenMenuDemo",
description: "",
body: (
<div>
{
/* HTML */ `<style>
.fullscreen-menu__panel--panel_1 {
transform: translateX(
-50vw
); /* move that one to the left */
}
.fullscreen-menu label {
cursor: pointer;
font-weight: bold;
}
.fullscreen-menu label {
cursor: pointer;
font-weight: bold;
}
.fullscreen-menu__panel {
font-size: 20px;
display: flex;
flex-flow: column;
row-gap: 20px;
}
</style>`
}
{fullscreenMenu({
id: "demo",
panels: {
panel_1: (
<div>
PANEL 1. Click{" "}
<label for="demo__activate--panel_2">here</label> to go to
panel 2<div>And here's some description</div>
Go to{" "}
<label for="demo__activate--panel_3">
Very tall panel 3
</label>
</div>
),
panel_2: (
<div>
PANEL 2
.fullscreen-menu__panel {
font-size: 20px;
display: flex;
flex-flow: column;
row-gap: 20px;
}
</style>`
}
{fullscreenMenu({
id: "demo",
panels: {
panel_1: (
<div>
<label for="demo__activate--panel_1">Go back</label>
PANEL 1. Click{" "}
<label for="demo__activate--panel_2">here</label> to
go to panel 2<div>And here's some description</div>
Go to{" "}
<label for="demo__activate--panel_3">
Very tall panel 3
</label>
</div>
</div>
),
panel_3: (
<div style="display: flex; flex-flow: column; row-gap: 20px;">
<label for="demo__activate--panel_1">Go back</label>
{[
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
30,
].map((n) => (
<div>{n}</div>
))}
</div>
),
},
default_panel: "panel_1",
})}
<div style="display: flex; flex-flow: column; row-gap: 10px;">
<label for="demo--visible">Toggle menu via checkbox label</label>
<button onclick="demo.showModal()" type="button">
Toggle menu with javascript
</button>
),
panel_2: (
<div>
PANEL 2
<div>
<label for="demo__activate--panel_1">
Go back
</label>
</div>
</div>
),
panel_3: (
<div style="display: flex; flex-flow: column; row-gap: 20px;">
<label for="demo__activate--panel_1">Go back</label>
{[
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
28, 29, 30,
].map((n) => (
<div>{n}</div>
))}
</div>
),
},
default_panel: "panel_1",
})}
<div style="display: flex; flex-flow: column; row-gap: 10px;">
<label for="demo--visible">Toggle menu via checkbox label</label>
<button onclick="demo.showModal()" type="button">
Toggle menu with javascript
</button>
</div>
<div style="height: 150vh">
This page is taaaaall - for testing reasons
</div>
</div>
<div style="height: 150vh">
This page is taaaaall - for testing reasons
</div>
</div>
);
),
});
}
})();

View File

@ -1,40 +0,0 @@
import { withProdApp } from "../test_utils/with-prod-app.js";
import { VERY_LONG_TEST_TIMEOUT, webhintURL } from "../test_utils/webhint.js";
import { FullscreenMenuDemoURL } from "./urls.js";
import { getBrowser } from "../test_utils/browser-creator.js";
import type { Browser, BrowserContext, Page } from "@playwright/test";
describe("FullscreenMenuDemo webhint", () => {
it("doesn't crash", async function () {
return withProdApp(async ({ base_url, rest_api }) => {
await rest_api.get(FullscreenMenuDemoURL);
await webhintURL(base_url + FullscreenMenuDemoURL);
// alternatively you can use webhintHTML for faster but less precise scans
// or for scanning responses of requests that use some form of authorization:
// const response = await rest_api.get(FullscreenMenuDemoURL);
// await webhintHTML(response);
});
}).timeout(VERY_LONG_TEST_TIMEOUT);
});
describe("FullscreenMenuDemo", () => {
let page: Page;
let browser: Browser;
let context: BrowserContext;
beforeEach(async () => {
browser = await getBrowser();
context = await browser.newContext();
page = await context.newPage();
});
afterEach(async () => {
await context.close();
});
it("works as expected", async function () {
return withProdApp(async ({ base_url }) => {
await page.goto(base_url + FullscreenMenuDemoURL);
});
}).timeout(VERY_LONG_TEST_TIMEOUT);
});

View File

@ -1,16 +0,0 @@
import { VERY_LONG_TEST_TIMEOUT, webhintURL } from "../test_utils/webhint.js";
import { withProdApp } from "../test_utils/with-prod-app.js";
import { HelloURL } from "./urls.js";
describe("Hello", () => {
it("doesn't crash", async function () {
return withProdApp(async ({ base_url, rest_api }) => {
await rest_api.get(HelloURL);
await webhintURL(base_url + HelloURL);
// alternatively you can use webhintHTML for faster but less precise scans
// or for scanning responses of requests that use some form of authorization:
// const response = await rest_api.get(HelloURL);
// await webhintHTML(response);
});
}).timeout(VERY_LONG_TEST_TIMEOUT);
});

View File

@ -1,17 +1,17 @@
import type { Context } from "koa";
import { tempstream } from "tempstream";
import { TempstreamJSX } from "tempstream";
import { Page } from "@sealcode/sealgen";
import html from "../html.js";
export const actionName = "Hello";
export const actionName = "Home";
export default new (class HelloPage extends Page {
export default new (class HomePage extends Page {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async canAccess(_: Context) {
return { canAccess: true, message: "" };
}
async render(ctx: Context) {
return html(ctx, "Hello", tempstream/* HTML */ `<div></div>`);
return html({ ctx: ctx, title: "Home", body: <div>Hello</div>, description: "" });
}
})();

View File

@ -1,9 +1,40 @@
import { VERY_LONG_TEST_TIMEOUT, webhintURL } from "../test_utils/webhint.js";
import { withProdApp } from "../test_utils/with-prod-app.js";
import { VERY_LONG_TEST_TIMEOUT, webhintURL } from "../test_utils/webhint.js";
import { HomeURL } from "./urls.js";
import { getBrowser } from "../test_utils/browser-creator.js";
import type { Browser, BrowserContext, Page } from "@playwright/test";
describe("homepage", function () {
it("passes webhint tests", () =>
withProdApp(async ({ base_url }) => {
await webhintURL(`${base_url}/`);
})).timeout(VERY_LONG_TEST_TIMEOUT);
describe("Home webhint", () => {
it("doesn't crash", async function () {
return withProdApp(async ({ base_url, rest_api }) => {
await rest_api.get(HomeURL);
await webhintURL(base_url + HomeURL);
// alternatively you can use webhintHTML for faster but less precise scans
// or for scanning responses of requests that use some form of authorization:
// const response = await rest_api.get(HomeURL);
// await webhintHTML(response);
});
}).timeout(VERY_LONG_TEST_TIMEOUT);
});
describe("Home", () => {
let page: Page;
let browser: Browser;
let context: BrowserContext;
beforeEach(async () => {
browser = await getBrowser();
context = await browser.newContext();
page = await context.newPage();
});
afterEach(async () => {
await context.close();
});
it("works as expected", async function () {
return withProdApp(async ({ base_url }) => {
await page.goto(base_url + HomeURL);
});
}).timeout(VERY_LONG_TEST_TIMEOUT);
});

View File

@ -1,14 +1,28 @@
import type Router from "@koa/router";
import { Middlewares } from "sealious";
import { imageRouter, RESPONSIVE_IMAGES_URL_PATH } from "../image-router.js";
import { MainView } from "./common/main-view.js";
import mountAutoRoutes from "./routes.js";
import { customUrlView } from "./middlewares/customUrlView.js";
import type TheApp from "../app.js";
import { Middlewares } from "sealious";
export const mainRouter = (router: Router): void => {
export const mainRouter = (app: TheApp, router: Router): void => {
const started_at = Date.now(); // necessary to detect aplication restarts
router.get("/", Middlewares.extractContext(), async (ctx) => {
ctx.body = MainView(ctx);
router.get("(.*)", async (ctx, next) => {
// custom redirects
if (ctx.body) return;
const {
items: [redirect],
} = await app.collections.redirects
.suList()
.filter({ from_url: ctx.url })
.fetch();
if (redirect) {
ctx.status = parseInt(redirect.get("type"));
ctx.redirect(redirect.get("to_url"));
return;
}
await next();
});
router.use(Middlewares.extractContext());
@ -18,5 +32,8 @@ export const mainRouter = (router: Router): void => {
router.use(RESPONSIVE_IMAGES_URL_PATH, imageRouter.getRoutes());
app.HTTPServer.koa.use(Middlewares.extractContext());
app.HTTPServer.koa.use(customUrlView(app));
mountAutoRoutes(router);
};

View File

@ -1,10 +0,0 @@
import JDDCreator from "./component-preview/jdd-creator.js";
export const actionName = "JDDPreview";
export default new (class JDDCreatePreviewPage extends JDDCreator {
// uncomment to create whitelist of allowed components
// getAllowedComponents() {
// return ["nice-box"];
// }
})();

View File

@ -1,41 +0,0 @@
import type { Browser, BrowserContext, Page } from "@playwright/test";
import ADMIN_CREDENTIALS from "../default-admin-credentials.js";
import { getBrowser } from "../test_utils/browser-creator.js";
import { LONG_TEST_TIMEOUT } from "../test_utils/webhint.js";
import { withProdApp } from "../test_utils/with-prod-app.js";
import { LogoutURL, SignInURL } from "./urls.js";
describe("Logout", () => {
let page: Page;
let browser: Browser;
let context: BrowserContext;
const username = ADMIN_CREDENTIALS.username;
const password = ADMIN_CREDENTIALS.password;
beforeEach(async () => {
browser = await getBrowser();
context = await browser.newContext();
page = await context.newPage();
});
afterEach(async () => {
await context.close();
});
describe("logout test", () => {
it("logout", async function () {
await withProdApp(async ({ base_url }) => {
await page.goto(base_url);
await page.getByRole("link", { name: "Sign in" }).click();
await page.getByPlaceholder("text").click();
await page.getByPlaceholder("text").fill(username);
await page.getByPlaceholder("text").press("Tab");
await page.getByPlaceholder("password").fill(password);
await page.getByPlaceholder("password").press("Enter");
await page.waitForSelector(`a[href="${LogoutURL}"]`);
await page.getByRole("link", { name: "Logout" }).click();
await page.waitForSelector(`a[href="${SignInURL}"]`);
});
}).timeout(LONG_TEST_TIMEOUT);
});
});

View File

@ -0,0 +1,81 @@
import type { Context, Next } from "koa";
import { TempstreamJSX } from "tempstream";
import { JDD } from "@sealcode/jdd";
import type TheApp from "../../app.js";
import { registry } from "../../jdd-components/components.js";
import { makeJDDContext } from "../../jdd-context.js";
import type { RawJDDocument } from "@sealcode/jdd";
import { imageRouter } from "../../image-router.js";
import type { FilePointer } from "@sealcode/file-manager";
import html from "../../html.js";
import { tempstream } from "tempstream";
import { defaultHead } from "../../defaultHead.js";
import { BASE_URL } from "../../config.js";
interface ContextState {
jddNames: string[];
}
/* eslint-disable @typescript-eslint/consistent-type-assertions */
export const customUrlView =
(app: TheApp) => async (ctx: Context & ContextState, next: Next) => {
try {
if (ctx.body) return;
const main_domain = new URL(BASE_URL).hostname;
const {
items: [page],
} = await app.collections.page
.list(ctx.$context)
.filter({
url: ctx.url.split("?")[0],
...(ctx.hostname !== main_domain
? { domain: ctx.hostname }
: { domain: "" }),
})
.fetch();
const pageContent = Array.isArray(page?.get("content"))
? (page?.get("content") as RawJDDocument)
: null;
if (page && pageContent) {
const jdd = await JDD.fromStorage(
registry,
makeJDDContext(ctx),
pageContent
);
const title = page.get("title");
const heading = page.get("heading");
const hideNavigation = page.get("hideNavigation") || false;
ctx.body = html({
ctx,
title: title || "Aspazja",
description: page.get("description") || "",
css_clumps: jdd.getAllCSSClumps(),
hideNavigation,
htmlOptions: {
showFooter: !hideNavigation,
},
body: (
<main class="custom-page jdd-outer-container">
{heading?.trim() ? <h1>{heading}</h1> : ""}
<div class="jdd-container">{jdd.render()}</div>
</main>
),
makeHead: (...args) =>
tempstream`${defaultHead(...args)}${jdd.renderEarlyAssets()}`,
metaImage: await imageRouter.singleImage(
await (page.get("imageForMetadata") as FilePointer).getPath(),
400,
"jpeg",
false
),
});
}
} catch (err) {
// eslint-disable-next-line no-console
console.log("error in custom url router middleware:", err);
} finally {
await next();
}
};

Some files were not shown because too many files have changed in this diff Show More