├── .env.example
├── .gitignore
├── .npmrc
├── .prettierignore
├── .prettierrc
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── jsconfig.json
├── package-lock.json
├── package.json
├── playwright.config.js
├── postcss.config.cjs
├── src
├── app.d.ts
├── app.html
├── index.test.js
├── lib
│ ├── Primo.svelte
│ ├── Sidebar.svelte
│ ├── Sidebar_Symbol.svelte
│ ├── app.d.ts
│ ├── compiler
│ │ ├── lib
│ │ │ ├── rollup-browser.js
│ │ │ └── svelte-compiler.min.js
│ │ ├── processors.js
│ │ └── workers
│ │ │ ├── postcss.worker.js
│ │ │ └── worker.js
│ ├── component.js
│ ├── components
│ │ ├── CodeEditor
│ │ │ ├── CodeMirror.svelte
│ │ │ ├── extensions.ts
│ │ │ ├── extensions
│ │ │ │ ├── autocomplete.js
│ │ │ │ └── inspector.ts
│ │ │ └── theme.ts
│ │ ├── Dropdown
│ │ │ └── Dropdown.svelte
│ │ ├── FieldItem.svelte
│ │ ├── GenericFields.svelte
│ │ ├── IconButton.svelte
│ │ ├── MenuPopup.svelte
│ │ ├── buttons
│ │ │ ├── Button.svelte
│ │ │ ├── IconButton.svelte
│ │ │ ├── MobileNavButton.svelte
│ │ │ ├── PrimaryButton.svelte
│ │ │ ├── PrimoButton.svelte
│ │ │ ├── SaveButton.svelte
│ │ │ └── index.js
│ │ ├── index.js
│ │ ├── inputs
│ │ │ ├── EditField.svelte
│ │ │ ├── SelectOne.svelte
│ │ │ ├── SubField.svelte
│ │ │ ├── TextInput.svelte
│ │ │ └── index.js
│ │ ├── misc
│ │ │ ├── Card.svelte
│ │ │ ├── CodePreview.svelte
│ │ │ ├── IconButton.svelte
│ │ │ ├── Preview.svelte
│ │ │ ├── Spinner.svelte
│ │ │ ├── Tabs.svelte
│ │ │ ├── index.js
│ │ │ └── misc.js
│ │ └── svg
│ │ │ └── PrimoLogo.svelte
│ ├── const.js
│ ├── converter.js
│ ├── database.js
│ ├── deploy.js
│ ├── field-types
│ │ ├── ColorPicker.svelte
│ │ ├── ContentField.svelte
│ │ ├── EmptyField.svelte
│ │ ├── GroupField.svelte
│ │ ├── IconPicker.svelte
│ │ ├── Image.svelte
│ │ ├── Information.svelte
│ │ ├── Link.svelte
│ │ ├── Markdown.svelte
│ │ ├── Number.svelte
│ │ ├── RepeaterField.svelte
│ │ ├── Select.svelte
│ │ ├── SelectField.svelte
│ │ ├── Switch.svelte
│ │ ├── URL.svelte
│ │ └── index.js
│ ├── index.d.ts
│ ├── index.js
│ ├── libraries
│ │ ├── emmet
│ │ │ └── plugin.js
│ │ ├── pluralize
│ │ │ └── index.js
│ │ ├── prettier.js
│ │ ├── prettier
│ │ │ └── prettier-svelte.js
│ │ └── svelte-undo.js
│ ├── stores
│ │ ├── actions.js
│ │ ├── app
│ │ │ ├── activePage.js
│ │ │ ├── fieldTypes.js
│ │ │ ├── index.js
│ │ │ ├── misc.js
│ │ │ ├── modal.js
│ │ │ └── modalTypes.js
│ │ ├── data
│ │ │ ├── index.js
│ │ │ ├── pages.js
│ │ │ ├── sections.js
│ │ │ ├── site.js
│ │ │ └── symbols.js
│ │ └── helpers.js
│ ├── supabase.js
│ ├── ui
│ │ ├── Card.svelte
│ │ ├── DropdownButton.svelte
│ │ ├── HSplitPane.svelte
│ │ ├── PrimaryButton.svelte
│ │ ├── Spinner.svelte
│ │ ├── TextInput.svelte
│ │ ├── index.js
│ │ ├── inputs
│ │ │ ├── Select.svelte
│ │ │ ├── Slider.svelte
│ │ │ ├── SplitButton.svelte
│ │ │ └── TextField.svelte
│ │ └── misc
│ │ │ └── Spinner.svelte
│ ├── utilities.js
│ ├── utils.js
│ └── views
│ │ ├── editor
│ │ ├── Layout
│ │ │ ├── BlockToolbar.svelte
│ │ │ ├── ComponentNode.svelte
│ │ │ ├── LockedOverlay.svelte
│ │ │ └── MarkdownButton.svelte
│ │ ├── LocaleSelector.svelte
│ │ ├── Page.svelte
│ │ ├── Toolbar.svelte
│ │ └── ToolbarButton.svelte
│ │ └── modal
│ │ ├── ComponentEditor
│ │ ├── ComponentEditor.svelte
│ │ ├── FullCodeEditor.svelte
│ │ └── HSplitPane.svelte
│ │ ├── ComponentLibrary
│ │ └── IFrame.svelte
│ │ ├── Deploy.js
│ │ ├── Deploy.svelte
│ │ ├── Dialog.svelte
│ │ ├── Dialogs
│ │ ├── Feedback.svelte
│ │ └── Image.svelte
│ │ ├── ModalContainer.svelte
│ │ ├── ModalHeader.svelte
│ │ ├── PageEditor
│ │ ├── FullCodeEditor.svelte
│ │ ├── HSplitPane.svelte
│ │ └── PageEditor.svelte
│ │ ├── SiteEditor
│ │ └── SiteEditor.svelte
│ │ ├── SitePages
│ │ ├── PageList
│ │ │ ├── Item.svelte
│ │ │ ├── PageForm.svelte
│ │ │ └── PageList.svelte
│ │ └── SitePages.svelte
│ │ ├── SymbolEditor
│ │ └── SymbolEditor.svelte
│ │ └── index.js
└── routes
│ ├── +layout.svelte
│ ├── +page.svelte
│ ├── [site]
│ ├── +layout.js
│ ├── +layout.svelte
│ ├── +page.svelte
│ └── [...page]
│ │ └── +page.svelte
│ └── reset.css
├── static
└── favicon.png
├── svelte.config.js
├── tailwind.config.js
├── tests
└── test.js
└── vite.config.js
/.env.example:
--------------------------------------------------------------------------------
1 | PUBLIC_SUPABASE_URL = ""
2 | PUBLIC_SUPABASE_PUBLIC_KEY = ""
3 | PRIVATE_SUPABASE_PRIVATE_KEY = ""
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /build
4 | /dist
5 | /.svelte-kit
6 | /package
7 | .env
8 | .env.*
9 | !.env.example
10 | vite.config.js.timestamp-*
11 | vite.config.ts.timestamp-*
12 | pnpm-lock.yaml
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict=true
2 | resolution-mode=highest
3 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /build
4 | /.svelte-kit
5 | /package
6 | .env
7 | .env.*
8 | !.env.example
9 |
10 | # Ignore files for PNPM, NPM and YARN
11 | pnpm-lock.yaml
12 | package-lock.json
13 | yarn.lock
14 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "singleQuote": true,
4 | "semi": false,
5 | "htmlWhitespaceSensitivity": "ignore",
6 | "useTabs": true,
7 | "trailingComma": "none",
8 | "printWidth": 100,
9 | "plugins": [
10 | "prettier-plugin-svelte"
11 | ],
12 | "pluginSearchDirs": [
13 | "."
14 | ],
15 | "overrides": [
16 | {
17 | "files": "*.svelte",
18 | "options": {
19 | "parser": "svelte"
20 | }
21 | }
22 | ]
23 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "i18n-ally.localesPaths": [
3 | "src/lib/languages"
4 | ]
5 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 mateomorris
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This repo contains the builder for Primo. If you're working on Primo, you'll need to clone both this and the main repo and link this one. Futher instructions coming soon.
2 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.svelte-kit/tsconfig.json",
3 | "compilerOptions": {
4 | "esModuleInterop": true,
5 | "forceConsistentCasingInFileNames": true,
6 | "resolveJsonModule": true,
7 | "skipLibCheck": true,
8 | "sourceMap": true,
9 | "moduleResolution": "NodeNext"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@primocms/builder",
3 | "version": "0.1.67",
4 | "scripts": {
5 | "dev": "vite dev",
6 | "build": "vite build && npm run package",
7 | "preview": "vite preview",
8 | "package": "svelte-kit sync && svelte-package && publint",
9 | "package-watch": "svelte-kit sync && svelte-package -w && publint",
10 | "prepublishOnly": "npm run package",
11 | "check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
12 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
13 | "test": "npm run test:integration && npm run test:unit",
14 | "test:integration": "playwright test",
15 | "test:unit": "vitest",
16 | "lint": "prettier --plugin-search-dir . --check .",
17 | "format": "prettier --plugin-search-dir . --write ."
18 | },
19 | "exports": {
20 | ".": {
21 | "types": "./dist/index.d.ts",
22 | "svelte": "./dist/index.js"
23 | }
24 | },
25 | "files": [
26 | "dist",
27 | "!dist/**/*.test.*",
28 | "!dist/**/*.spec.*"
29 | ],
30 | "peerDependencies": {
31 | "@codemirror/autocomplete": "^6.1.0",
32 | "@codemirror/commands": "^6.0.1",
33 | "@codemirror/lang-css": "^6.0.0",
34 | "@codemirror/lang-html": "^6.1.0",
35 | "@codemirror/lang-javascript": "^6.0.2",
36 | "@codemirror/language": "^6.2.1",
37 | "@codemirror/state": "^6.1.0",
38 | "@codemirror/view": "^6.1.2",
39 | "@fontsource/fira-code": "^5.0.5",
40 | "@iconify/svelte": "^2.2.1",
41 | "@lezer/highlight": "^1.0.0",
42 | "@replit/codemirror-lang-svelte": "^6.0.0",
43 | "@tiptap/core": "^2.0.0-beta.174",
44 | "@tiptap/extension-bubble-menu": "^2.0.0-beta.55",
45 | "@tiptap/extension-bullet-list": "^2.0.0-beta.26",
46 | "@tiptap/extension-floating-menu": "^2.0.0-beta.50",
47 | "@tiptap/extension-highlight": "^2.0.0-beta.33",
48 | "@tiptap/extension-link": "^2.0.0-beta.199",
49 | "@tiptap/starter-kit": "^2.0.0-beta.183",
50 | "autosize": "^5.0.1",
51 | "axios": "^0.26.0",
52 | "codemirror": "^6.0.1",
53 | "file-saver": "^2.0.5",
54 | "idb-keyval": "^6.1.0",
55 | "jszip": "^3.7.1",
56 | "lodash-es": "^4.17.21",
57 | "mousetrap": "^1.6.5",
58 | "nanoid": "^3.1.23",
59 | "pluralize": "^8.0.0",
60 | "prettier": "^2.4.1",
61 | "promise-worker": "^2.0.1",
62 | "showdown": "^2.1.0",
63 | "showdown-highlight": "^3.1.0",
64 | "svelte": "^3.59.2",
65 | "svelte-dnd-action": "^0.9.24",
66 | "svelte-toggle": "^3.1.0",
67 | "timeago.js": "^4.0.2",
68 | "uuid": "^9.0.0"
69 | },
70 | "devDependencies": {
71 | "@playwright/test": "^1.28.1",
72 | "@sveltejs/adapter-auto": "^2.0.0",
73 | "@sveltejs/kit": "^1.20.4",
74 | "@sveltejs/package": "^2.0.0",
75 | "autoprefixer": "^10.4.14",
76 | "postcss-nested": "^6.0.1",
77 | "prettier": "^2.8.0",
78 | "prettier-plugin-svelte": "^2.10.1",
79 | "publint": "^0.1.9",
80 | "svelte": "^4.0.5",
81 | "svelte-check": "^3.4.3",
82 | "svelte-json-tree": "^1.0.0",
83 | "svelte-preprocess": "^5.0.3",
84 | "tailwindcss": "^3.3.2",
85 | "tslib": "^2.4.1",
86 | "typescript": "^5.0.0",
87 | "vite": "^4.4.2",
88 | "vitest": "^0.32.2"
89 | },
90 | "svelte": "./dist/index.js",
91 | "types": "./dist/index.d.ts",
92 | "type": "module",
93 | "dependencies": {
94 | "@tiptap/extension-image": "^2.2.6",
95 | "install": "^0.13.0",
96 | "npm": "^10.5.1"
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/playwright.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('@playwright/test').PlaywrightTestConfig} */
2 | const config = {
3 | webServer: {
4 | command: 'npm run build && npm run preview',
5 | port: 4173
6 | },
7 | testDir: 'tests',
8 | testMatch: /(.+\.)?(test|spec)\.[jt]s/
9 | };
10 |
11 | export default config;
12 |
--------------------------------------------------------------------------------
/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | require('tailwindcss'),
4 | require("autoprefixer"),
5 | require("postcss-nested")
6 | ],
7 | };
--------------------------------------------------------------------------------
/src/app.d.ts:
--------------------------------------------------------------------------------
1 | // See https://kit.svelte.dev/docs/types#app
2 | // for information about these interfaces
3 | declare global {
4 | namespace App {
5 | // interface Error {}
6 | // interface Locals {}
7 | // interface PageData {}
8 | // interface Platform {}
9 | }
10 | }
11 |
12 | export {};
13 |
--------------------------------------------------------------------------------
/src/app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | %sveltekit.head%
9 |
10 |
11 |
12 | %sveltekit.body%
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/index.test.js:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest';
2 |
3 | describe('sum test', () => {
4 | it('adds 1 + 2 to equal 3', () => {
5 | expect(1 + 2).toBe(3);
6 | });
7 | });
8 |
--------------------------------------------------------------------------------
/src/lib/Primo.svelte:
--------------------------------------------------------------------------------
1 |
100 |
101 |
102 |
103 | {#if showing_sidebar}
104 |
105 | {:else}
106 |
107 |
108 |
109 | {/if}
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
133 |
134 |
135 |
208 |
--------------------------------------------------------------------------------
/src/lib/app.d.ts:
--------------------------------------------------------------------------------
1 | declare module '$lib'
--------------------------------------------------------------------------------
/src/lib/compiler/processors.js:
--------------------------------------------------------------------------------
1 | import {clone as _cloneDeep} from 'lodash-es'
2 | import PromiseWorker from 'promise-worker';
3 | import {get} from 'svelte/store'
4 | import {site} from '$lib/index.js'
5 | import {locale} from '$lib/index.js'
6 | import svelteWorker from './workers/worker?worker'
7 | import postCSSWorker from './workers/postcss.worker?worker'
8 |
9 | const cssPromiseWorker = new PromiseWorker(new postCSSWorker());
10 | const htmlPromiseWorker = new PromiseWorker(new svelteWorker());
11 |
12 | const componentsMap = new Map();
13 |
14 | export async function html({ component, buildStatic = true, format = 'esm'}) {
15 |
16 | // return {
17 | // error: 'none'
18 | // }
19 |
20 | let cacheKey
21 | if (!buildStatic) {
22 | cacheKey = JSON.stringify({
23 | component,
24 | format
25 | })
26 | if (componentsMap.has(cacheKey)) {
27 | const cached = componentsMap.get(cacheKey)
28 | return cached
29 | }
30 | }
31 |
32 | let res
33 | try {
34 | const has_js = Array.isArray(component) ? component.some(s => s.js) : !!component.js
35 | res = await htmlPromiseWorker.postMessage({
36 | component,
37 | hydrated: buildStatic && has_js,
38 | buildStatic,
39 | format,
40 | site: get(site),
41 | locale: get(locale)
42 | })
43 | } catch(e) {
44 | console.log('error', e)
45 | res = {
46 | error: e.toString()
47 | }
48 | }
49 |
50 | let final
51 |
52 | if (!res) {
53 | final = {
54 | html: 'could not render
'
55 | }
56 | res = {}
57 | } else if (res.error) {
58 | console.error(res.error)
59 | final = {
60 | error: escapeHtml(res.error)
61 | }
62 | function escapeHtml(unsafe) {
63 | return unsafe
64 | .replace(/&/g, "&")
65 | .replace(//g, ">")
67 | .replace(/"/g, """)
68 | .replace(/'/g, "'");
69 | }
70 | } else if (buildStatic) {
71 | const blob = new Blob([res.ssr], { type: 'text/javascript' });
72 | const url = URL.createObjectURL(blob);
73 | const {default:App} = await import(url/* @vite-ignore */)
74 | const rendered = App.render(component.data)
75 | final = {
76 | head: rendered.head,
77 | html: rendered.html,
78 | css: rendered.css.code,
79 | js: res.dom
80 | }
81 | } else {
82 | final = {
83 | js: res.dom
84 | }
85 | }
86 |
87 | if (!buildStatic) {
88 | componentsMap.set(cacheKey, final)
89 | }
90 |
91 | return final
92 | }
93 |
94 |
95 | const cssMap = new Map()
96 | export async function css(raw) {
97 | // return {
98 | // css: ''
99 | // }
100 | if (cssMap.has(raw)) {
101 | return ({ css: cssMap.get(raw) })
102 | }
103 | const processed = await cssPromiseWorker.postMessage({
104 | css: raw
105 | })
106 | if (processed.message) {
107 | return {
108 | error: processed.message
109 | }
110 | }
111 | cssMap.set(raw, processed)
112 | return {
113 | css: processed
114 | }
115 | }
--------------------------------------------------------------------------------
/src/lib/component.js:
--------------------------------------------------------------------------------
1 | const compilers = {}
2 |
3 | let checked = 0
4 |
5 | export const processors = {
6 | html: async (raw, data) => {
7 | return await new Promise((resolve) => {
8 | checkIfRegistered()
9 | async function checkIfRegistered() {
10 | const compiler = compilers['html']
11 | if (compiler) {
12 | const res = await compiler(raw)
13 | resolve(res)
14 | } else {
15 | checked++
16 | if (checked < 100) {
17 | setTimeout(checkIfRegistered, 100)
18 | }
19 | }
20 | }
21 | })
22 | },
23 | css: async (raw, data) => {
24 | return await new Promise((resolve) => {
25 | checkIfRegistered()
26 | async function checkIfRegistered() {
27 | const compiler = compilers['css']
28 | if (compiler) {
29 | const res = await compiler(raw)
30 | resolve(res)
31 | } else {
32 | checked++
33 | if (checked < 100) {
34 | setTimeout(checkIfRegistered, 100)
35 | }
36 | }
37 | }
38 | })
39 | },
40 | js: async (raw, options) => {
41 | const final = raw
42 | return final
43 | }
44 | }
45 |
46 | export function registerProcessors(fns) {
47 | for (const [lang, processor] of Object.entries(fns)) {
48 | compilers[lang] = processor
49 | // processors[lang] = processor
50 | }
51 | }
--------------------------------------------------------------------------------
/src/lib/components/CodeEditor/extensions.ts:
--------------------------------------------------------------------------------
1 | import { css } from "@codemirror/lang-css"
2 | import { javascript } from "@codemirror/lang-javascript"
3 | import { svelte } from "@replit/codemirror-lang-svelte";
4 |
5 | export function getLanguage(mode) {
6 | return {
7 | 'html': svelte(),
8 | 'css': css(),
9 | 'javascript': javascript()
10 | }[mode]
11 | }
--------------------------------------------------------------------------------
/src/lib/components/CodeEditor/extensions/autocomplete.js:
--------------------------------------------------------------------------------
1 | import {svelteLanguage} from '@replit/codemirror-lang-svelte'
2 | import { cssLanguage } from "@codemirror/lang-css"
3 | import { snippetCompletion } from '@codemirror/autocomplete'
4 | import _ from 'lodash-es';
5 |
6 | const Completion_Label = (value) => {
7 | if (Array.isArray(value)) {
8 | return `[ ${typeof(value[0])} ]`
9 | } else if (_.isObject(value)) {
10 | return '{ ' + Object.entries(value).map(([ key, value ]) => `${key}:${typeof(value)}`).join(', ') + ' }'
11 | } else {
12 | return typeof(value)
13 | }
14 | }
15 |
16 | function svelteCompletions(data) {
17 | const completions = [
18 | snippetCompletion('{#if ${true}}\n\t${Shown if true}\n{:else}\n\t${Shown if false}\n{/if', {
19 | label: "{#if}",
20 | type: "text",
21 | detail: "Conditionally render a block of content",
22 | }),
23 | snippetCompletion('{#each ${["one", "two"]} as ${item}}\n\t${\{item\\}}\n{/each', {
24 | label: "{#each}",
25 | type: "text",
26 | detail: "Loop over array or Repeater items"
27 | }),
28 | snippetCompletion('{#await ${promise}}\n\t${promise is pending}\n{:then ${value}}\n\t${promise was fullfilled}\n{:catch ${error}}\n\t${promise was rejected}\n{/await', {
29 | label: "{#await}",
30 | type: "text",
31 | detail: "Show content depending on the states of a Promise"
32 | }),
33 | snippetCompletion('{#key ${"value"}}\n\tthis will re-render when "value" changes\n{/key', {
34 | label: "{#key}",
35 | type: "text",
36 | detail: "Re-render a block when a value changes"
37 | }),
38 | snippetCompletion('{@html ${"content
"}', {
39 | label: "{@html}", type: "text", detail: "Render HTML from a Markdown field"
40 | }),
41 | snippetCompletion('{@debug ${variable}', {
42 | label: "{@debug}",
43 | type: "text",
44 | detail: "Log a variable's value"
45 | }),
46 | snippetCompletion('{@const ${variable = "foo"}', {
47 | label: "{@const}",
48 | type: "text",
49 | detail: "Define a local constant"
50 | }),
51 | ]
52 | return svelteLanguage.data.of({
53 | autocomplete: (context) => {
54 | const word = context.matchBefore(/\S*/)
55 |
56 | // Svelte blocks
57 | if ((word.text.includes('{#') || word.text.includes('{@'))) {
58 | const position = (word.text.indexOf('{#') !== - 1 ? word.text.indexOf('{#') : word.text.indexOf('{@'))
59 | return {
60 | from: word.from + position,
61 | options: completions
62 | }
63 | }
64 |
65 | // Field values
66 | if (word.text.includes('{')) {
67 | // matches child field values
68 | const position = word.text.indexOf('{')
69 |
70 | if (word.text.includes('.')) {
71 | const options = Object.entries(data).filter(([key, value]) => (_.isObject(value) && !Array.isArray(value))).map(([key, value]) => {
72 | const child_options = Object.entries(value).map(([child_key, child_value]) => ({
73 | label: `${key}.${child_key}`,
74 | type: 'variable',
75 | detail: Completion_Label(child_value)
76 | }))
77 | return child_options
78 | })
79 | return {
80 | from: word.from + position + 1,
81 | options: _.flattenDeep(options)
82 | }
83 | }
84 |
85 | // matches root-level fields
86 | return {
87 | from: word.from + position + 1, // offset for bracket
88 | options: [
89 | ...Object.entries(data).map(([key, value]) => ({
90 | label: key,
91 | type: 'variable',
92 | detail: Completion_Label(value)
93 | })),
94 | {
95 | label: '{#block}',
96 | apply: '#',
97 | type: 'text',
98 | detail: 'each, if, key, await',
99 | boost: -1
100 | },
101 | {
102 | label: '{@tag}',
103 | apply: '@',
104 | type: 'text',
105 | detail: 'html, const, debug',
106 | boost: -2
107 | }
108 | ]
109 | }
110 | }
111 | }
112 | })
113 | }
114 |
115 |
116 | function cssCompletions(list = []) {
117 | const variables = list.map(item => item.substring(0, item.length - 1))
118 | return cssLanguage.data.of({
119 | autocomplete: (context) => {
120 | const word = context.matchBefore(/\S*/)
121 | if (!word.text.startsWith('var(')) return null
122 | return {
123 | from: word.from,
124 | options: variables.map(item => ({
125 | label: `var(${item})`,
126 | type: "text",
127 | apply: `var(${item}`
128 | }))
129 | }
130 | }
131 | })
132 | }
133 |
134 | export function updateCompletions(Editor, variables, compartment) {
135 | Editor.dispatch({
136 | effects: compartment.reconfigure(cssCompletions(variables))
137 | })
138 | }
139 |
140 | export function extract_css_variables(css) {
141 | return css.match(/--\S*:/gm) || []
142 | }
143 |
144 | export {
145 | cssCompletions,
146 | svelteCompletions
147 | }
--------------------------------------------------------------------------------
/src/lib/components/CodeEditor/extensions/inspector.ts:
--------------------------------------------------------------------------------
1 | import { EditorView, Decoration } from '@codemirror/view';
2 | import type { DecorationSet } from '@codemirror/view'
3 | import { StateEffect, StateField } from '@codemirror/state';
4 |
5 | const addUnderline = StateEffect.define<{ from: number, to: number }>()
6 |
7 | const underlineField = StateField.define({
8 | create() {
9 | return Decoration.none
10 | },
11 | update(underlines, tr) {
12 | underlines = underlines.map(tr.changes)
13 | for (let e of tr.effects) if (e.is(addUnderline)) {
14 | underlines = underlines.update({
15 | add: [underlineMark.range(e.value.from, e.value.from)],
16 | filter: (f, t, value) => {
17 | if (value.spec.class === 'cm-highlight') return false
18 | else return true
19 | },
20 | })
21 | }
22 | return underlines
23 | },
24 | provide: f => EditorView.decorations.from(f)
25 | })
26 |
27 | const underlineMark = Decoration.line({ class: "cm-highlight" })
28 |
29 | const underlineTheme = EditorView.baseTheme({
30 | ".cm-highlight": { background: "#333" }
31 | })
32 |
33 | export default function highlight_active_line(Editor, loc) {
34 | if (!loc) return
35 | let activeLine
36 | for (let { from, to } of Editor.visibleRanges) {
37 | for (let pos = from; pos <= to;) {
38 | let line = Editor.state.doc.lineAt(pos)
39 | if (line.number === (loc.line + 1) && line.from !== line.to) {
40 | activeLine = line
41 | break;
42 | } else {
43 | pos = line.to + 1
44 | }
45 | }
46 | }
47 |
48 | if (activeLine) {
49 | highlightLine(Editor, activeLine);
50 | }
51 |
52 | function highlightLine(view: EditorView, line) {
53 | let effects: StateEffect[] = [addUnderline.of({ from: line.from, to: line.to })]
54 | if (!effects.length) return false
55 |
56 | if (!view.state.field(underlineField, false))
57 | effects.push(StateEffect.appendConfig.of([underlineField, underlineTheme]))
58 | view.dispatch({ effects })
59 | return true
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/lib/components/CodeEditor/theme.ts:
--------------------------------------------------------------------------------
1 | import { EditorView } from "@codemirror/view"
2 | import { HighlightStyle, syntaxHighlighting } from "@codemirror/language"
3 | import { tags as t } from "@lezer/highlight"
4 |
5 | // Using https://github.com/one-dark/vscode-one-dark-theme/ as reference for the colors
6 |
7 | const chalky = "#e5c07b",
8 | coral = "#e06c75",
9 | cyan = "#56b6c2",
10 | invalid = "#ffffff",
11 | ivory = "#abb2bf",
12 | stone = "#7d8799", // Brightened compared to original to increase contrast
13 | malibu = "#61afef",
14 | sage = "#98c379",
15 | whiskey = "#d19a66",
16 | violet = "#c678dd",
17 | darkBackground = "#21252b",
18 | highlightBackground = "#2c313a",
19 | background = "rgb(30,30,30)",
20 | tooltipBackground = "#353a42",
21 | selection = "#333",
22 | cursor = "white"
23 |
24 | /// The editor theme styles for One Dark.
25 | export const oneDarkTheme = EditorView.theme({
26 | "&": {
27 | color: ivory,
28 | backgroundColor: background
29 | },
30 |
31 | '.cm-line': {
32 | fontFamily: `'Fira Code'`,
33 | },
34 |
35 | ".cm-content": {
36 | caretColor: cursor
37 | },
38 |
39 | ".cm-cursor, .cm-dropCursor": { borderLeftColor: cursor },
40 | // ".cm-activeLine": { backgroundColor: highlightBackground },
41 |
42 | "&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": {
43 | backgroundColor: selection,
44 | },
45 |
46 | ".cm-panels": { backgroundColor: darkBackground, color: ivory },
47 | ".cm-panels.cm-panels-top": { borderBottom: "2px solid black" },
48 | ".cm-panels.cm-panels-bottom": { borderTop: "2px solid black" },
49 |
50 | ".cm-searchMatch": {
51 | backgroundColor: "#72a1ff59",
52 | outline: "1px solid #457dff"
53 | },
54 | ".cm-searchMatch.cm-searchMatch-selected": {
55 | backgroundColor: "#6199ff2f"
56 | },
57 |
58 | ".cm-selectionMatch": { backgroundColor: "#aafe661a" },
59 |
60 | "&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket": {
61 | backgroundColor: "#bad0f847",
62 | outline: "1px solid #515a6b"
63 | },
64 |
65 | ".cm-gutters": {
66 | backgroundColor: background,
67 | color: stone,
68 | border: "none"
69 | },
70 |
71 | ".cm-activeLineGutter": {
72 | backgroundColor: highlightBackground
73 | },
74 |
75 | ".cm-foldPlaceholder": {
76 | backgroundColor: "transparent",
77 | border: "none",
78 | color: "#ddd"
79 | },
80 |
81 | ".cm-tooltip": {
82 | border: "none",
83 | backgroundColor: tooltipBackground
84 | },
85 | ".cm-tooltip .cm-tooltip-arrow:before": {
86 | borderTopColor: "transparent",
87 | borderBottomColor: "transparent"
88 | },
89 | ".cm-tooltip .cm-tooltip-arrow:after": {
90 | borderTopColor: tooltipBackground,
91 | borderBottomColor: tooltipBackground
92 | },
93 | ".cm-tooltip-autocomplete": {
94 | "& > ul > li[aria-selected]": {
95 | backgroundColor: highlightBackground,
96 | color: ivory
97 | }
98 | }
99 | }, { dark: true })
100 |
101 | /// The highlighting style for code in the One Dark theme.
102 | export const oneDarkHighlightStyle = HighlightStyle.define([
103 | {
104 | tag: t.keyword,
105 | color: violet
106 | },
107 | {
108 | tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName],
109 | color: coral
110 | },
111 | {
112 | tag: [t.function(t.variableName), t.labelName],
113 | color: malibu
114 | },
115 | {
116 | tag: [t.color, t.constant(t.name), t.standard(t.name)],
117 | color: whiskey
118 | },
119 | {
120 | tag: [t.definition(t.name), t.separator],
121 | color: ivory
122 | },
123 | {
124 | tag: [t.typeName, t.className, t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace],
125 | color: chalky
126 | },
127 | {
128 | tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.link, t.special(t.string)],
129 | color: cyan
130 | },
131 | {
132 | tag: [t.meta, t.comment],
133 | color: stone
134 | },
135 | {
136 | tag: t.strong,
137 | fontWeight: "bold"
138 | },
139 | {
140 | tag: t.emphasis,
141 | fontStyle: "italic"
142 | },
143 | {
144 | tag: t.strikethrough,
145 | textDecoration: "line-through"
146 | },
147 | {
148 | tag: t.link,
149 | color: stone,
150 | textDecoration: "underline"
151 | },
152 | {
153 | tag: t.heading,
154 | fontWeight: "bold",
155 | color: coral
156 | },
157 | {
158 | tag: [t.atom, t.bool, t.special(t.variableName)],
159 | color: whiskey
160 | },
161 | {
162 | tag: [t.processingInstruction, t.string, t.inserted],
163 | color: sage
164 | },
165 | {
166 | tag: t.invalid,
167 | color: invalid
168 | },
169 | ])
170 |
171 | // workaround for introduced bug
172 | // https://discuss.codemirror.net/t/highlighting-that-seems-ignored-in-cm6/4320/17
173 | const fn0 = oneDarkHighlightStyle.style;
174 | // noinspection JSConstantReassignment
175 | oneDarkHighlightStyle.style = tags => fn0(tags || [])
176 |
177 | export const ThemeHighlighting = syntaxHighlighting(oneDarkHighlightStyle)
178 |
179 | /// Extension to enable the One Dark theme (both the editor theme and
180 | /// the highlight style).
181 | const oneDark: Extension = [oneDarkTheme, syntaxHighlighting(oneDarkHighlightStyle)]
182 | export default oneDark
--------------------------------------------------------------------------------
/src/lib/components/Dropdown/Dropdown.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
22 | {#if showingSelector}
23 |
24 |
25 |
26 | {#each options as item}
27 |
31 | {/each}
32 |
33 |
34 |
35 | {/if}
36 |
37 |
38 |
--------------------------------------------------------------------------------
/src/lib/components/IconButton.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
12 |
13 |
30 |
--------------------------------------------------------------------------------
/src/lib/components/MenuPopup.svelte:
--------------------------------------------------------------------------------
1 |
17 |
18 |
90 |
91 |
171 |
--------------------------------------------------------------------------------
/src/lib/components/buttons/Button.svelte:
--------------------------------------------------------------------------------
1 |
10 |
11 |
17 | {#if !loading}
18 | {label}
19 | {:else}
20 |
21 | {/if}
22 |
23 |
24 |
59 |
--------------------------------------------------------------------------------
/src/lib/components/buttons/IconButton.svelte:
--------------------------------------------------------------------------------
1 |
18 |
19 | {#if link}
20 |
27 | {#if position === 'left'}
28 |
29 | {#if iconClasses}
30 |
31 | {:else}{/if}
32 |
33 | {/if}
34 | {#if label}{label}{/if}
35 | {#if position === 'right'}
36 |
37 | {#if iconClasses}
38 |
39 | {:else}{/if}
40 |
41 | {/if}
42 |
43 | {:else}
44 |
66 | {/if}
67 |
68 |
94 |
--------------------------------------------------------------------------------
/src/lib/components/buttons/MobileNavButton.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
14 |
15 |
30 |
--------------------------------------------------------------------------------
/src/lib/components/buttons/PrimaryButton.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
29 |
30 |
72 |
--------------------------------------------------------------------------------
/src/lib/components/buttons/PrimoButton.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 | {#if $loadingSite}
9 |
10 | {:else}
11 |
12 | {/if}
13 |
14 |
15 |
46 |
--------------------------------------------------------------------------------
/src/lib/components/buttons/SaveButton.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 |
26 |
27 |
47 |
--------------------------------------------------------------------------------
/src/lib/components/buttons/index.js:
--------------------------------------------------------------------------------
1 | import IconButton from './IconButton.svelte'
2 | import Button from './Button.svelte'
3 | import PrimoButton from './PrimoButton.svelte'
4 | import MobileNavButton from './MobileNavButton.svelte'
5 | import PrimaryButton from './PrimaryButton.svelte'
6 | import SaveButton from './SaveButton.svelte'
7 |
8 | export {
9 | IconButton,
10 | Button,
11 | PrimoButton,
12 | MobileNavButton,
13 | PrimaryButton,
14 | SaveButton
15 | }
--------------------------------------------------------------------------------
/src/lib/components/index.js:
--------------------------------------------------------------------------------
1 | import * as buttons from './buttons'
2 | import * as inputs from './inputs'
3 | import * as misc from './misc'
4 |
5 | export {
6 | buttons,
7 | inputs,
8 | misc
9 | }
--------------------------------------------------------------------------------
/src/lib/components/inputs/EditField.svelte:
--------------------------------------------------------------------------------
1 |
48 |
49 |
50 |
58 |
59 |
69 |
70 |
71 |
75 | {#if minimal}
76 |
80 | {:else}
81 |
82 |
86 |
87 |
88 |
92 |
93 |
99 | {/if}
100 | {#if top_level && !minimal}
101 |
102 |
103 |
104 | {/if}
105 |
106 |
107 | dispatch('download'),
114 | // },
115 | {
116 | label: 'Move up',
117 | icon: 'material-symbols:arrow-circle-up-outline',
118 | on_click: () => dispatch('move', 'up')
119 | },
120 | {
121 | label: 'Move down',
122 | icon: 'material-symbols:arrow-circle-down-outline',
123 | on_click: () => dispatch('move', 'down')
124 | },
125 | {
126 | label: 'Duplicate',
127 | icon: 'bxs:duplicate',
128 | on_click: () => dispatch('duplicate')
129 | },
130 | {
131 | label: 'Delete',
132 | icon: 'ic:outline-delete',
133 | on_click: () => dispatch('delete')
134 | }
135 | ]}
136 | />
137 |
138 |
139 | {#if has_subfields}
140 |
141 |
142 |
143 | {/if}
144 |
145 |
146 |
254 |
--------------------------------------------------------------------------------
/src/lib/components/inputs/SelectOne.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
{label}
17 |
18 | {#each options as option}
19 |
23 | {/each}
24 |
25 |
26 |
27 |
55 |
--------------------------------------------------------------------------------
/src/lib/components/inputs/SubField.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 |
22 |
28 |
dispatch('delete')}
32 | {disabled} />
33 |
34 |
35 |
76 |
--------------------------------------------------------------------------------
/src/lib/components/inputs/TextInput.svelte:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 |
41 |
42 |
95 |
--------------------------------------------------------------------------------
/src/lib/components/inputs/index.js:
--------------------------------------------------------------------------------
1 | import EditField from './EditField.svelte'
2 | import SelectOne from './SelectOne.svelte'
3 | import TextInput from './TextInput.svelte'
4 | import SubField from './SubField.svelte'
5 |
6 | export {
7 | EditField,
8 | SelectOne,
9 | TextInput,
10 | SubField
11 | }
--------------------------------------------------------------------------------
/src/lib/components/misc/Card.svelte:
--------------------------------------------------------------------------------
1 |
26 |
27 |
28 | {#if title}
29 |
49 | {/if}
50 | {#if !hidden}
51 |
52 |
53 |
54 |
55 | {/if}
56 |
57 |
58 |
59 |
106 |
--------------------------------------------------------------------------------
/src/lib/components/misc/IconButton.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
--------------------------------------------------------------------------------
/src/lib/components/misc/Preview.svelte:
--------------------------------------------------------------------------------
1 |
37 |
38 |
48 |
49 |
50 |
51 |
82 |
--------------------------------------------------------------------------------
/src/lib/components/misc/Spinner.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
12 |
13 |
42 |
--------------------------------------------------------------------------------
/src/lib/components/misc/Tabs.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 | {#if tabs.length > 1}
14 |
15 | {#each tabs as tab, i}
16 |
26 | {/each}
27 |
28 | {/if}
29 |
30 |
51 |
--------------------------------------------------------------------------------
/src/lib/components/misc/index.js:
--------------------------------------------------------------------------------
1 | import IconButton from './IconButton.svelte'
2 | import Card from './Card.svelte'
3 | import Spinner from './Spinner.svelte'
4 | import Tabs from './Tabs.svelte'
5 | import CodePreview from './CodePreview.svelte'
6 |
7 | export {
8 | IconButton,
9 | Card,
10 | Spinner,
11 | Tabs,
12 | CodePreview
13 | }
--------------------------------------------------------------------------------
/src/lib/components/misc/misc.js:
--------------------------------------------------------------------------------
1 | export const iframePreview = (locale = 'en') => `
2 |
3 |
4 |
5 |
6 |
7 |
24 |
25 |
26 | {field.label}
27 | dispatch('input', field)}
31 | />
32 |
33 |
34 |
50 |
--------------------------------------------------------------------------------
/src/lib/field-types/ContentField.svelte:
--------------------------------------------------------------------------------
1 |
24 |
25 | {#if value}
26 |
47 | {:else}
48 |
66 | {/if}
67 |
68 |
104 |
--------------------------------------------------------------------------------
/src/lib/field-types/EmptyField.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 | The {field.type}
7 | field doesn't exist. Consider updating Primo.
8 |
9 |
10 |
17 |
--------------------------------------------------------------------------------
/src/lib/field-types/GroupField.svelte:
--------------------------------------------------------------------------------
1 |
45 |
46 |
47 | {#if level > 0}
48 |
52 | {/if}
53 | {#if !hidden}
54 |
55 | {#each subfieldsWithValues as subfield}
56 | {#if subfield.options.hidden === '__show' || subfield.options.hidden === selectedOption || !subfield.options.hidden}
57 |
58 |
64 |
65 | {/if}
66 | {/each}
67 |
68 | {/if}
69 |
70 |
71 |
115 |
--------------------------------------------------------------------------------
/src/lib/field-types/IconPicker.svelte:
--------------------------------------------------------------------------------
1 |
39 |
40 |
41 |
{field.label}
42 |
43 | {#if field.value}
44 |
45 |
46 |
47 | {/if}
48 |
56 |
57 | {#if searched}
58 |
59 | {#each icons as icon}
60 |
63 | {:else}
64 |
70 | No icons found
71 |
72 | {/each}
73 |
74 | {/if}
75 |
76 |
77 |
117 |
--------------------------------------------------------------------------------
/src/lib/field-types/Information.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
24 | {@html html}
25 |
26 |
27 |
62 |
--------------------------------------------------------------------------------
/src/lib/field-types/Link.svelte:
--------------------------------------------------------------------------------
1 |
72 |
73 |
74 |
{field.label}
75 |
122 |
123 |
124 |
125 |
172 |
--------------------------------------------------------------------------------
/src/lib/field-types/Markdown.svelte:
--------------------------------------------------------------------------------
1 |
49 |
50 |
61 |
62 |
90 |
--------------------------------------------------------------------------------
/src/lib/field-types/Number.svelte:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
12 |
18 |
--------------------------------------------------------------------------------
/src/lib/field-types/Select.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
28 |
29 |
30 |
57 |
--------------------------------------------------------------------------------
/src/lib/field-types/SelectField.svelte:
--------------------------------------------------------------------------------
1 |
41 |
42 |
43 | {#if options}
44 | {#each options as option, i}
45 |
80 | {/each}
81 | {/if}
82 |
109 |
110 |
111 |
182 |
--------------------------------------------------------------------------------
/src/lib/field-types/Switch.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
21 |
22 |
23 |
86 |
--------------------------------------------------------------------------------
/src/lib/field-types/URL.svelte:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
12 |
18 |
--------------------------------------------------------------------------------
/src/lib/field-types/index.js:
--------------------------------------------------------------------------------
1 | import Number from './Number.svelte'
2 | import Image from './Image.svelte'
3 | import Markdown from './Markdown.svelte'
4 | import Switch from './Switch.svelte'
5 | import URL from './URL.svelte'
6 | import Link from './Link.svelte'
7 | import Information from './Information.svelte'
8 | import Select from './Select.svelte'
9 | import RepeaterField from './RepeaterField.svelte'
10 | import GroupField from './GroupField.svelte'
11 | import ContentField from './ContentField.svelte'
12 | // import ColorPicker from './ColorPicker.svelte'
13 | import IconPicker from './IconPicker.svelte'
14 |
15 | export default [
16 | {
17 | id: 'repeater',
18 | label: 'Repeater',
19 | component: RepeaterField
20 | },
21 | {
22 | id: 'group',
23 | label: 'Group',
24 | component: GroupField
25 | },
26 | {
27 | id: 'text',
28 | label: 'Text',
29 | component: ContentField
30 | },
31 | {
32 | id: 'markdown',
33 | label: 'Markdown',
34 | component: Markdown
35 | },
36 | {
37 | id: 'image',
38 | label: 'Image',
39 | component: Image
40 | },
41 | {
42 | id: 'number',
43 | label: 'Number',
44 | component: Number
45 | },
46 | {
47 | id: 'switch',
48 | label: 'Switch',
49 | component: Switch
50 | },
51 | {
52 | id: 'url',
53 | label: 'URL',
54 | component: URL
55 | },
56 | {
57 | id: 'link',
58 | label: 'Link',
59 | component: Link
60 | },
61 | {
62 | id: 'select',
63 | label: 'Select',
64 | component: Select
65 | },
66 | {
67 | id: 'icon',
68 | label: 'Icon',
69 | component: IconPicker
70 | },
71 | {
72 | id: 'info',
73 | label: 'Info',
74 | component: Information
75 | }
76 | // {
77 | // id: 'color',
78 | // label: 'Color Picker',
79 | // component: ColorPicker
80 | // }
81 | ]
82 |
--------------------------------------------------------------------------------
/src/lib/index.d.ts:
--------------------------------------------------------------------------------
1 | export type Section = {
2 | id: string;
3 | content: Content;
4 | index: number;
5 | symbol: Symbol;
6 | page: string;
7 | }
8 |
9 | export type Symbol = {
10 | id: string;
11 | name: string;
12 | code: Code,
13 | fields: Array;
14 | content: Content;
15 | created_at?: string;
16 | site: string;
17 | }
18 |
19 | export type Block_Code = {
20 | html: string;
21 | css: string;
22 | js: string;
23 | }
24 |
25 | export type Page_Code = {
26 | html: {
27 | head: string;
28 | below: string;
29 | };
30 | css: string;
31 | js: string;
32 | }
33 |
34 | export type Content = {
35 | en: {
36 | [field_key?: string]: any; //
37 | };
38 | [locale: string]: {
39 | [field_key?: string]: any; //
40 | };
41 | }
42 |
43 | export type Field = {
44 | id: string,
45 | key: string,
46 | label: string,
47 | type: string,
48 | fields: Array,
49 | options: object,
50 | is_static: boolean,
51 | value?: any
52 | }
53 |
54 | export type Page = {
55 | id: string;
56 | name: string;
57 | url: string;
58 | code: Page_Code;
59 | fields: Array;
60 | content: Content;
61 | site: string;
62 | parent: string | null;
63 | created_at?: string;
64 | }
65 |
66 | export type Site = {
67 | id: string;
68 | name: string;
69 | url: string;
70 | code: Page_Code;
71 | fields: Array;
72 | content: Content;
73 | active_deployment?: object | null;
74 | created_at?: string;
75 | }
76 |
77 | export type User = {
78 | id: string;
79 | email: string;
80 | server_member: boolean;
81 | admin: boolean;
82 | role: 'DEV' | 'EDITOR';
83 | created_at: string;
84 | }
85 |
86 | export type Site_Data = {
87 | site: Site;
88 | pages: Array;
89 | sections: Array;
90 | symbols: Array;
91 | }
--------------------------------------------------------------------------------
/src/lib/index.js:
--------------------------------------------------------------------------------
1 | import Primo from './Primo.svelte'
2 |
3 | import TextInput from './ui/TextInput.svelte'
4 | import { saved, onMobile, userRole } from './stores/app/misc'
5 | import { site, content } from './stores/data/site'
6 | import activePage from './stores/app/activePage'
7 | import fieldTypes from './stores/app/fieldTypes'
8 | import modal from './stores/app/modal'
9 | import { locale, locked_blocks } from './stores/app/misc'
10 | import { processCode } from './utils'
11 | import { buildStaticPage, getPageData, get_content_with_static } from './stores/helpers'
12 | import { registerProcessors } from './component'
13 | import { Page, Site, languages } from './const'
14 | import PrimoFieldTypes from './field-types'
15 | import { validate_site_structure_v2 } from './converter'
16 | import PrimoPage from './views/editor/Page.svelte'
17 | import { database_subscribe, storage_subscribe, realtime_subscribe } from './database'
18 | import { deploy, deploy_subscribe } from './deploy'
19 |
20 | import * as utils from './utils'
21 | import * as components from './components'
22 |
23 | // export const components = {
24 | // TextInput
25 | // }
26 |
27 | const stores = {
28 | saved,
29 | onMobile,
30 | userRole
31 | }
32 |
33 | export {
34 | database_subscribe,
35 | storage_subscribe,
36 | realtime_subscribe,
37 | deploy_subscribe,
38 | deploy,
39 | locale,
40 | locked_blocks,
41 | site,
42 | content,
43 | activePage,
44 | modal,
45 | utils,
46 | components,
47 | Page,
48 | Site,
49 | fieldTypes,
50 | PrimoFieldTypes,
51 | stores,
52 | registerProcessors,
53 | processCode,
54 | buildStaticPage,
55 | validate_site_structure_v2,
56 | PrimoPage,
57 | languages,
58 | getPageData,
59 | get_content_with_static
60 | }
61 | export default Primo
62 |
--------------------------------------------------------------------------------
/src/lib/libraries/prettier.js:
--------------------------------------------------------------------------------
1 | const plugins = {
2 | html, css, babel
3 | }
4 |
5 | let prettier
6 | let html
7 | let css
8 | let babel
9 |
10 | export default async function format(code, { mode, position }) {
11 | if (!pretter) {
12 | prettier = await import('prettier').default
13 | html = await import('prettier/esm/parser-html').default;
14 | css = await import('prettier/esm/parser-postcss').default
15 | babel = await import('prettier/esm/parser-babel').default
16 | }
17 |
18 | let formatted
19 | try {
20 | if (mode === 'javascript') {
21 | mode = 'babel'
22 | }
23 |
24 | formatted = prettier.formatWithCursor(code, {
25 | parser: mode,
26 | bracketSameLine: true,
27 | cursorOffset: position,
28 | plugins: [
29 | plugins[mode]
30 | ]
31 | })
32 | } catch(e) {
33 | console.warn(e)
34 | }
35 |
36 | return formatted
37 | }
38 |
--------------------------------------------------------------------------------
/src/lib/libraries/svelte-undo.js:
--------------------------------------------------------------------------------
1 | import { writable } from 'svelte/store';
2 | import { cloneDeep } from 'lodash-es';
3 |
4 | export function createStack(current) {
5 | /** @type {T[]} */
6 | let stack = [current];
7 |
8 | let index = stack.length;
9 |
10 | const state = writable({
11 | first: true,
12 | last: true,
13 | current,
14 | });
15 |
16 | function update() {
17 | current = stack[index - 1];
18 |
19 | state.set({
20 | first: index === 1,
21 | last: index === stack.length,
22 | current,
23 | });
24 |
25 | return current;
26 | }
27 |
28 | return {
29 | set: value => {
30 | stack = [value];
31 | index = 1;
32 | return update();
33 | },
34 | /** @param {T | ((current: T) => T)} value */
35 | push: value => {
36 | stack.length = index;
37 | stack[index++] = cloneDeep(value);
38 | return update();
39 | },
40 | undo: () => {
41 | if (index > 1) index -= 1;
42 | return update();
43 | },
44 | redo: () => {
45 | if (index < stack.length) index += 1;
46 | return update();
47 | },
48 | subscribe: state.subscribe,
49 | };
50 | }
51 |
--------------------------------------------------------------------------------
/src/lib/stores/app/activePage.js:
--------------------------------------------------------------------------------
1 | import {writable,get,derived} from 'svelte/store'
2 | import {Page} from '../../const'
3 |
4 | export const id = writable('')
5 | export const name = writable('')
6 | export const url = writable('index')
7 | export const code = writable(Page().code)
8 | export const content = writable({ en: {} })
9 | export const fields = writable(Page().fields)
10 |
11 | export function set(val) {
12 | if (val.id) {
13 | id.set(val.id)
14 | }
15 | if (val.name) {
16 | name.set(val.name)
17 | }
18 | if (val.url) {
19 | url.set(val.url)
20 | }
21 | if (val.code) {
22 | code.set(val.code)}
23 | if (val.content) {
24 | content.set(val.content)}
25 | if (val.fields) {
26 | fields.set(val.fields)
27 | }
28 | }
29 |
30 | // conveniently get the entire site
31 | export default derived(
32 | [ id, name, url, code, content, fields ],
33 | ([id, name, url, code, content, fields]) => {
34 | return {
35 | id,
36 | name,
37 | url,
38 | code,
39 | content,
40 | fields
41 | }
42 | })
43 |
--------------------------------------------------------------------------------
/src/lib/stores/app/fieldTypes.js:
--------------------------------------------------------------------------------
1 | import {writable} from 'svelte/store'
2 | import buildInFieldTypes from '../../field-types'
3 |
4 | const fieldTypes = writable(buildInFieldTypes)
5 |
6 | export default {
7 | register: (userTypes) => {
8 | fieldTypes.update(types => [
9 | ...types,
10 | ...userTypes
11 | ])
12 | },
13 | set: fieldTypes.set,
14 | subscribe: fieldTypes.subscribe
15 | }
--------------------------------------------------------------------------------
/src/lib/stores/app/index.js:
--------------------------------------------------------------------------------
1 | import {showingIDE,userRole} from './misc.js'
2 | import modal from './modal.js'
3 | import fieldTypes from './fieldTypes.js'
4 |
5 | export {
6 | userRole,
7 | showingIDE,
8 | modal,
9 | fieldTypes
10 | }
--------------------------------------------------------------------------------
/src/lib/stores/app/misc.js:
--------------------------------------------------------------------------------
1 | import {writable} from 'svelte/store'
2 |
3 | export const saved = writable(true)
4 |
5 | export const showingIDE = writable(false)
6 |
7 | export const userRole = writable('DEV')
8 |
9 | export const showKeyHint = writable(false)
10 |
11 | export const loadingSite = writable(false)
12 |
13 | export const onMobile = !import.meta.env.SSR ? writable(/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) : writable(false)
14 |
15 | export const locale = writable('en')
16 |
17 | export const highlightedElement = writable(null)
18 |
19 | export const locked_blocks = writable([])
--------------------------------------------------------------------------------
/src/lib/stores/app/modal.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import {modalTypes as initialModalTypes} from './modalTypes'
4 | import {writable,get} from 'svelte/store'
5 | import Mousetrap from 'mousetrap'
6 |
7 | const initialState = {
8 | type: null,
9 | component: null,
10 | componentProps: {},
11 | header: {
12 | title: '',
13 | icon: null
14 | },
15 | footer: null,
16 | variants: '',
17 | disableClose: false,
18 | disabledBgClose: false,
19 | maxWidth: null,
20 | showSwitch: false,
21 | noPadding: false
22 | }
23 |
24 | const modalTypes = {
25 | ...initialModalTypes
26 | }
27 |
28 | const store = writable(initialState)
29 |
30 | const modal_startup = () => {
31 | Mousetrap.bind('backspace', (e) => {
32 | e.preventDefault()
33 | })
34 | }
35 | const modal_cleanup = () => {
36 | Mousetrap.unbind('backspace')
37 | }
38 |
39 | export default {
40 | show: (type, componentProps = {}, modalOptions = {}) => {
41 | const typeToShow = getModalType(type, componentProps, modalOptions)
42 | modal_startup()
43 | store.update(s => ({
44 | ...initialState,
45 | ...typeToShow,
46 | ...modalOptions,
47 | type,
48 | }))
49 | },
50 | hide: (nav = null) => {
51 | modal_cleanup()
52 | store.update(s => ({
53 | ...s,
54 | type: null,
55 | }))
56 | },
57 | register: (modal) => {
58 | if (Array.isArray(modal)) {
59 | modal.forEach(createModal)
60 | } else if (typeof modal === 'object') {
61 | createModal(modal)
62 | } else {
63 | console.error('Could not register modal an array or object')
64 | }
65 |
66 | function createModal(modal) {
67 | const { id, component, componentProps={}, options={} } = modal
68 | modalTypes[id] = {
69 | ...options,
70 | component,
71 | header: options.header,
72 | variants: options.width ? `${options.width}` : '',
73 | componentProps
74 | }
75 | }
76 | },
77 | subscribe: store.subscribe
78 | }
79 |
80 | function getModalType(type, componentProps = {}, modalOptions = {}) {
81 | return {
82 | componentProps,
83 | ...modalTypes[type],
84 | ...modalOptions
85 | } || console.error('Invalid modal type:', type)
86 | }
87 |
--------------------------------------------------------------------------------
/src/lib/stores/app/modalTypes.js:
--------------------------------------------------------------------------------
1 | import {Deploy,ComponentEditor,SymbolEditor,PageEditor,SiteEditor,SitePages,Dialog} from '../../views/modal'
2 |
3 | export const modalTypes = {
4 | 'DEPLOY' : {
5 | component: Deploy,
6 | header: {
7 | title: 'Deploy',
8 | icon: 'fas fa-cloud-upload-alt'
9 | }
10 | },
11 | 'COMPONENT_EDITOR' : {
12 | component: ComponentEditor,
13 | header: {
14 | title: 'Create Component',
15 | icon: 'fas fa-code'
16 | },
17 | },
18 | 'SYMBOL_EDITOR' : {
19 | component: SymbolEditor,
20 | header: {
21 | title: 'Create Symbol',
22 | icon: 'fas fa-code'
23 | },
24 | },
25 | 'PAGE_EDITOR' : {
26 | component: PageEditor,
27 | header: {
28 | title: 'Edit Page',
29 | icon: 'fas fa-code'
30 | },
31 | },
32 | 'SITE_EDITOR' : {
33 | component: SiteEditor,
34 | header: {
35 | title: 'Edit Page',
36 | icon: 'fas fa-code'
37 | },
38 | },
39 | 'SITE_PAGES' : {
40 | component: SitePages,
41 | header: {
42 | title: 'Pages',
43 | icon: 'fas fa-th-large'
44 | },
45 | },
46 | 'DIALOG' : {
47 | component: Dialog
48 | },
49 | }
--------------------------------------------------------------------------------
/src/lib/stores/data/index.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash-es'
2 | import { createStack } from '../../libraries/svelte-undo';
3 | import site from './site'
4 | import pages from './pages'
5 | import sections from './sections'
6 | import symbols from './symbols'
7 |
8 | export default {
9 | site,
10 | pages,
11 | sections,
12 | symbols
13 | }
14 |
15 | export let timeline = createStack({
16 | doing: () => {console.log('initial doing')},
17 | undoing: () => {console.log('initial undoing')}
18 | });
19 |
20 | /** @param {{ doing: () => Promise, undoing: () => Promise }} functions */
21 | export async function update_timeline({ doing, undoing }) {
22 | await doing()
23 | timeline.push({
24 | doing,
25 | undoing
26 | })
27 | }
--------------------------------------------------------------------------------
/src/lib/stores/data/pages.js:
--------------------------------------------------------------------------------
1 | import { writable, get } from 'svelte/store';
2 | import { page } from '$app/stores';
3 | import {browser} from '$app/environment';
4 |
5 | /** @type {import('svelte/store').Writable} */
6 | const pages = writable([])
7 |
8 | if (browser) {
9 | page.subscribe(({data}) => {
10 | if (data?.pages) {
11 | pages.set(data.pages)
12 | }
13 | })
14 | }
15 |
16 | export default pages
17 |
--------------------------------------------------------------------------------
/src/lib/stores/data/sections.js:
--------------------------------------------------------------------------------
1 | import { writable } from 'svelte/store';
2 |
3 | /** @type {import('svelte/store').Writable} */
4 | export default writable([])
--------------------------------------------------------------------------------
/src/lib/stores/data/site.js:
--------------------------------------------------------------------------------
1 | import { writable, derived } from 'svelte/store';
2 | import { Site } from '../../const';
3 |
4 | export const id = writable('default');
5 | export const url = writable('');
6 | export const name = writable('');
7 | export const fields = writable([]);
8 | export const code = writable(Site().code);
9 | export const content = writable(Site().content);
10 | export const active_deployment = writable(null);
11 |
12 | export function update(props) {
13 | if (props.id) {
14 | id.set(props.id);
15 | }
16 | if (props.url) {
17 | url.set(props.url);
18 | }
19 | if (props.name) {
20 | name.set(props.name);
21 | }
22 | if (props.code) {
23 | code.set(props.code);
24 | }
25 | if (props.fields) {
26 | fields.set(props.fields);
27 | }
28 | if (props.content) {
29 | content.set(props.content);
30 | }
31 | if (props.active_deployment) {
32 | active_deployment.set(props.active_deployment);
33 | }
34 | }
35 |
36 | // conveniently get the entire site
37 | /** @type {import('svelte/store').Readable} */
38 | export const site = derived([id, url, name, code, fields, content, active_deployment], ([id, url, name, code, fields, content, active_deployment]) => {
39 | return {
40 | id,
41 | url,
42 | name,
43 | code,
44 | fields,
45 | content,
46 | active_deployment
47 | };
48 | });
49 |
50 | export default site;
--------------------------------------------------------------------------------
/src/lib/stores/data/symbols.js:
--------------------------------------------------------------------------------
1 | import { writable, get } from 'svelte/store';
2 |
3 | /** @type {import('svelte/store').Writable} */
4 | export default writable([])
--------------------------------------------------------------------------------
/src/lib/stores/helpers.js:
--------------------------------------------------------------------------------
1 | import { find as _find, chain as _chain, flattenDeep as _flattenDeep } from 'lodash-es'
2 | import _ from 'lodash-es'
3 | import { get } from 'svelte/store'
4 | import { processors } from '../component.js'
5 | import { site as activeSite } from './data/site.js'
6 | import sections from './data/sections.js'
7 | import symbols from './data/symbols.js'
8 | import pages from './data/pages.js'
9 | import activePage from './app/activePage.js'
10 | import { locale } from './app/misc.js'
11 | import { processCSS, getEmptyValue } from '../utils.js'
12 |
13 | export function getSymbolUseInfo(symbolID) {
14 | const info = { pages: [], frequency: 0 }
15 | get(pages).forEach((page) => {
16 | // TODO: fix this
17 | // page.sections.forEach(section => {
18 | // if (section.symbolID === symbolID) {
19 | // info.frequency++
20 | // if (!info.pages.includes(page.id)) info.pages.push(page.name)
21 | // }
22 | // })
23 | })
24 | return info
25 | }
26 |
27 | export function getSymbol(symbolID) {
28 | return _find(get(symbols), ['id', symbolID])
29 | }
30 |
31 | /**
32 | * @param {{
33 | * page?: import('$lib').Page
34 | * site?: import('$lib').Site
35 | * page_sections?: import('$lib').Section[]
36 | * page_symbols?: import('$lib').Symbol[]
37 | * locale?: string
38 | * no_js?: boolean
39 | * }} details
40 | * @returns {Promise<{ html: string, js: string}>}
41 | * */
42 | export async function buildStaticPage({
43 | page = get(activePage),
44 | site = get(activeSite),
45 | page_sections = get(sections),
46 | page_symbols = get(symbols),
47 | locale = 'en',
48 | no_js = false
49 | }) {
50 | const hydratable_symbols_on_page = page_symbols.filter(
51 | (s) => s.code.js && page_sections.some((section) => section.symbol === s.id)
52 | )
53 |
54 | const component = await Promise.all([
55 | (async () => {
56 | const css = await processCSS(site.code.css + page.code.css)
57 | const data = getPageData({ page, site, loc: locale })
58 | return {
59 | html: `
60 |
61 | ${site.code.html.head}
62 | ${page.code.html.head}
63 |
64 | `,
65 | css: ``,
66 | js: ``,
67 | data
68 | }
69 | })(),
70 | ...page_sections
71 | .map(async (section) => {
72 | const symbol = page_symbols.find((symbol) => symbol.id === section.symbol)
73 | const { html, css: postcss, js } = symbol.code
74 | const data = get_content_with_static({
75 | component: section,
76 | symbol,
77 | loc: locale
78 | })
79 | const { css, error } = await processors.css(postcss || '')
80 | const section_id = section.id.split('-')[0]
81 | return {
82 | html: `
83 |
84 | ${html}
85 |
`,
86 | js,
87 | css,
88 | data
89 | }
90 | })
91 | .filter(Boolean), // remove options blocks
92 | (async () => {
93 | const data = getPageData({ page, site, loc: locale })
94 | return {
95 | html: site.code.html.below + page.code.html.below,
96 | css: ``,
97 | js: ``,
98 | data
99 | }
100 | })()
101 | ])
102 |
103 | const res = await processors.html({
104 | component,
105 | locale
106 | })
107 |
108 | const final = `\
109 |
110 |
111 |
112 |
113 | ${res.head}
114 |
115 |
116 |
117 | ${res.html}
118 | ${no_js ? `` : ``}
119 |
120 |
121 | `
122 |
123 | return {
124 | html: final,
125 | js: res.js
126 | }
127 |
128 | // fetch module to hydrate component, include hydration data
129 | function fetch_modules(symbols) {
130 | return symbols
131 | .map(
132 | (symbol) => `
133 | import('/_symbols/${symbol.id}.js')
134 | .then(({default:App}) => {
135 | ${page_sections
136 | .filter((section) => section.symbol === symbol.id)
137 | .map((section) => {
138 | const section_id = section.id.split('-')[0]
139 | const instance_content = get_content_with_static({
140 | component: section,
141 | symbol,
142 | loc: locale
143 | })
144 | return `
145 | new App({
146 | target: document.querySelector('#section-${section_id}'),
147 | hydrate: true,
148 | props: ${JSON.stringify(instance_content)}
149 | })
150 | `
151 | })
152 | .join('\n')}
153 | })
154 | .catch(e => console.error(e))
155 | `
156 | )
157 | .join('\n')
158 | }
159 | }
160 |
161 | // Include static content alongside the component's content
162 | /**
163 | * @param {{
164 | * component?: import('$lib').Section
165 | * symbol?: import('$lib').Symbol
166 | * loc?: string
167 | * }} details
168 | * @returns {import('$lib').Content}
169 | * */
170 | export function get_content_with_static({ component, symbol, loc }) {
171 | if (!symbol) return { en: {} }
172 | const content = _chain(symbol.fields)
173 | .map((field) => {
174 | const field_value = component.content?.[loc]?.[field.key]
175 | // if field is static, use value from symbol content
176 | if (field.is_static) {
177 | const symbol_value = symbol.content?.[loc]?.[field.key]
178 | return {
179 | key: field.key,
180 | value: symbol_value
181 | }
182 | } else if (field_value !== undefined) {
183 | return {
184 | key: field.key,
185 | value: field_value
186 | }
187 | } else {
188 | const default_content = symbol.content?.[loc]?.[field.key]
189 | return {
190 | key: field.key,
191 | value: default_content || getEmptyValue(field)
192 | }
193 | }
194 | })
195 | .keyBy('key')
196 | .mapValues('value')
197 | .value()
198 |
199 | return _.cloneDeep(content)
200 | }
201 |
202 | export function getPageData({ page = get(activePage), site = get(activeSite), loc = get(locale) }) {
203 | const page_content = page.content[loc]
204 | const site_content = site.content[loc]
205 | return {
206 | ...site_content,
207 | ...page_content
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/src/lib/supabase.js:
--------------------------------------------------------------------------------
1 | import { createClient } from '@supabase/supabase-js'
2 | import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_PUBLIC_KEY } from '$env/static/public';
3 |
4 | export const supabase = createClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_PUBLIC_KEY, {
5 | // auth: {
6 | // autoRefreshToken: true,
7 | // persistSession: true,
8 | // },
9 | realtime: {
10 | params: {
11 | eventsPerSecond: 10,
12 | },
13 | },
14 | })
15 |
--------------------------------------------------------------------------------
/src/lib/ui/Card.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 | {#if title}
11 |
12 | {#if title}
13 | {title}
14 | {:else}
15 |
16 | {/if}
17 |
18 | {/if}
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
47 |
--------------------------------------------------------------------------------
/src/lib/ui/DropdownButton.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 |
35 |
36 |
136 |
--------------------------------------------------------------------------------
/src/lib/ui/PrimaryButton.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 | {#if type === 'input'}
15 |
36 | {:else}
37 |
56 | {/if}
57 |
58 |
103 |
--------------------------------------------------------------------------------
/src/lib/ui/Spinner.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
12 |
13 |
42 |
--------------------------------------------------------------------------------
/src/lib/ui/TextInput.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
32 |
33 |
81 |
--------------------------------------------------------------------------------
/src/lib/ui/index.js:
--------------------------------------------------------------------------------
1 | import Card from './Card.svelte'
2 | import Slider from './inputs/Slider.svelte'
3 | import TextField from './inputs/TextField.svelte'
4 | import Select from './inputs/Select.svelte'
5 | import Spinner from './misc/Spinner.svelte'
6 |
7 | export {
8 | Card,
9 | Slider,
10 | TextField,
11 | Select,
12 | Spinner
13 | }
--------------------------------------------------------------------------------
/src/lib/ui/inputs/Select.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
30 |
31 |
84 |
--------------------------------------------------------------------------------
/src/lib/ui/inputs/Slider.svelte:
--------------------------------------------------------------------------------
1 |
10 |
11 |
19 |
20 |
29 |
--------------------------------------------------------------------------------
/src/lib/ui/inputs/SplitButton.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 | {#each buttons as button, i}
15 |
27 | {/each}
28 |
29 |
30 |
68 |
--------------------------------------------------------------------------------
/src/lib/ui/inputs/TextField.svelte:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 |
36 |
37 |
78 |
--------------------------------------------------------------------------------
/src/lib/ui/misc/Spinner.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
12 |
13 |
42 |
--------------------------------------------------------------------------------
/src/lib/utilities.js:
--------------------------------------------------------------------------------
1 | import { customAlphabet } from 'nanoid/non-secure'
2 | import _ from 'lodash-es'
3 |
4 | export function createUniqueID(length = 5) {
5 | const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', length)
6 | return nanoid()
7 | }
8 |
9 | // https://stackoverflow.com/a/21071454
10 | export function move(array, from, to) {
11 | if (to === from) return array
12 |
13 | var target = array[from]
14 | var increment = to < from ? -1 : 1
15 |
16 | for (var k = from; k != to; k += increment) {
17 | array[k] = array[k + increment]
18 | }
19 | array[to] = target
20 | return array
21 | }
22 |
23 | export function replaceDashWithUnderscore(str) {
24 | return str.replace(/-/g, '_').replace(/ /g, '_').toLowerCase()
25 | }
26 |
27 | export function validate_url(url) {
28 | return url
29 | .replace(/\s+/g, '-')
30 | .replace(/[^0-9a-z\-._]/gi, '')
31 | .toLowerCase()
32 | }
33 |
34 | export function content_editable(element, params) {
35 | let value = element.textContent
36 | element.contentEditable = true
37 | element.spellcheck = false
38 |
39 | element.onfocus = () => {
40 | const range = document.createRange()
41 | const sel = window.getSelection()
42 | range.setStart(element, 1)
43 | range.collapse(true)
44 |
45 | sel?.removeAllRanges()
46 | sel?.addRange(range)
47 | }
48 |
49 | if (params.autofocus) element.focus()
50 |
51 | element.onkeydown = (e) => {
52 | if (e.code === 'Enter') {
53 | e.preventDefault()
54 | e.target?.blur()
55 | params.on_submit(value)
56 | }
57 | }
58 |
59 | element.onkeyup = (e) => {
60 | value = e.target.textContent
61 | params.on_change(value)
62 | }
63 | }
64 |
65 | // detect whether the mouse is hovering outside of an element
66 | export function hovering_outside(event, element) {
67 | if (!element) return false
68 | const rect = element.getBoundingClientRect()
69 | const is_outside =
70 | event.x >= Math.floor(rect.right) ||
71 | event.y >= Math.floor(rect.bottom) ||
72 | event.x <= Math.floor(rect.left) ||
73 | event.y <= Math.floor(rect.top)
74 | return is_outside
75 | }
76 |
77 | export function swap_array_item_index(arr, from, to) {
78 | let new_array = _.cloneDeep(arr)
79 | new_array[from] = arr[to]
80 | new_array[to] = arr[from]
81 | return new_array
82 | }
83 |
84 | export function clickOutside(node) {
85 | const handleClick = (event) => {
86 | if (node && !node.contains(event.target) && !event.defaultPrevented) {
87 | node.dispatchEvent(new CustomEvent('click_outside', node))
88 | }
89 | }
90 |
91 | document.addEventListener('click', handleClick, true)
92 |
93 | return {
94 | destroy() {
95 | document.removeEventListener('click', handleClick, true)
96 | }
97 | }
98 | }
99 |
100 | export function click_to_copy(node, value = null) {
101 | navigator.clipboard.writeText(value || node.innerText)
102 | node.addEventListener('click', () => {
103 | node.style.opacity = '0.5'
104 | })
105 | }
106 |
--------------------------------------------------------------------------------
/src/lib/utils.js:
--------------------------------------------------------------------------------
1 | import _, { chain as _chain, capitalize as _capitalize } from "lodash-es";
2 | import { processors } from './component'
3 |
4 | const componentsCache = new Map();
5 | export async function processCode({ component, buildStatic = true, format = 'esm', locale = 'en', hydrated = true, ignoreCachedData = false }) {
6 | let css = ''
7 | if (component.css) {
8 | css = await processCSS(component.css || '')
9 | }
10 |
11 | const cacheKey = ignoreCachedData ? JSON.stringify({
12 | component: Array.isArray(component) ? component.map(c => ({ html: c.html, css: c.css, head: c.head })) : {
13 | head: component.head,
14 | html: component.html,
15 | css: component.css
16 | },
17 | }) : JSON.stringify({
18 | component,
19 | format,
20 | buildStatic,
21 | hydrated
22 | })
23 |
24 | if (componentsCache.has(cacheKey)) {
25 | return componentsCache.get(cacheKey)
26 | }
27 |
28 | const res = await processors.html({
29 | component: {
30 | ...component,
31 | css
32 | }, buildStatic, format, locale, hydrated
33 | })
34 |
35 | componentsCache.set(cacheKey, res)
36 |
37 | return res
38 | }
39 |
40 | const cssCache = new Map();
41 | export async function processCSS(raw) {
42 | if (cssCache.has(raw)) {
43 | return cssCache.get(raw)
44 | }
45 |
46 | const res = await processors.css(raw) || {}
47 | if (!res) {
48 | return ''
49 | } else if (res.error) {
50 | console.log('CSS Error:', res.error)
51 | return raw
52 | } else if (res.css) {
53 | cssCache.set(raw, res.css)
54 | return res.css
55 | }
56 | }
57 |
58 | // Lets us debounce from reactive statements
59 | export function createDebouncer(time) {
60 | return _.debounce((val) => {
61 | const [fn, arg] = val;
62 | fn(arg);
63 | }, time);
64 | }
65 |
66 | export function wrapInStyleTags(css, id) {
67 | return ``;
68 | }
69 |
70 | export function getEmptyValue(field) {
71 | if (field.default) return field.default
72 | if (field.type === 'repeater') return []
73 | else if (field.type === 'group') return getGroupValue(field)
74 | else if (field.type === 'image') return {
75 | url: '',
76 | src: '',
77 | alt: '',
78 | size: null
79 | }
80 | else if (field.type === 'text') return ''
81 | else if (field.type === 'markdown') return { html: '', markdown: '' }
82 | else if (field.type === 'link') return {
83 | label: '',
84 | url: ''
85 | }
86 | else if (field.type === 'url') return ''
87 | else if (field.type === 'select') return ''
88 | else if (field.type === 'switch') return true
89 | else {
90 | console.warn('No placeholder set for field type', field.type)
91 | return ''
92 | }
93 |
94 | function getGroupValue(field) {
95 | return _chain(field.fields).keyBy('key').mapValues((field) => getEmptyValue(field)).value()
96 | }
97 | }
98 |
99 | let converter, showdown, showdown_highlight
100 | export async function convert_html_to_markdown(html) {
101 | if (converter) {
102 | return converter.makeMarkdown(html)
103 | } else {
104 | const modules = await Promise.all([import('showdown'), import('showdown-highlight')])
105 | showdown = modules[0].default
106 | showdown_highlight = modules[1].default
107 | converter = new showdown.Converter({
108 | extensions: [showdown_highlight()]
109 | })
110 | return converter.makeMarkdown(html)
111 | }
112 | }
113 |
114 | export async function convert_markdown_to_html(markdown) {
115 | if (converter) {
116 | return converter.makeHtml(markdown)
117 | } else {
118 | const modules = await Promise.all([import('showdown'), import('showdown-highlight')])
119 | showdown = modules[0].default
120 | showdown_highlight = modules[1].default
121 | converter = new showdown.Converter({
122 | extensions: [showdown_highlight()]
123 | })
124 | return converter.makeHtml(markdown)
125 | }
126 | }
--------------------------------------------------------------------------------
/src/lib/views/editor/Layout/BlockToolbar.svelte:
--------------------------------------------------------------------------------
1 |
22 |
23 |
24 |
25 |
64 |
65 |
153 |
--------------------------------------------------------------------------------
/src/lib/views/editor/Layout/LockedOverlay.svelte:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 | Being edited by {locked.user}
11 |
12 |
13 |
27 |
--------------------------------------------------------------------------------
/src/lib/views/editor/Layout/MarkdownButton.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
11 |
12 |
37 |
--------------------------------------------------------------------------------
/src/lib/views/editor/ToolbarButton.svelte:
--------------------------------------------------------------------------------
1 |
28 |
29 |
74 |
75 |
176 |
--------------------------------------------------------------------------------
/src/lib/views/modal/ComponentLibrary/IFrame.svelte:
--------------------------------------------------------------------------------
1 |
55 |
56 |
57 |
58 |
59 | {#if !iframeLoaded}
60 |
61 |
62 |
63 | {/if}
64 |
69 | {#if componentCode}
70 |
82 |
83 |
84 |
126 |
--------------------------------------------------------------------------------
/src/lib/views/modal/Deploy.js:
--------------------------------------------------------------------------------
1 | import { get } from 'svelte/store'
2 | import pages from '$lib/stores/data/pages'
3 | import symbols from '$lib/stores/data/symbols'
4 | // import beautify from 'js-beautify' // remove for now to reduce bundle size, dynamically import later if wanted
5 | import { dataChanged } from '$lib/database.js'
6 | import { deploy } from '$lib/deploy'
7 | import { buildStaticPage } from '$lib/stores/helpers'
8 | import { processCode } from '$lib/utils'
9 | import _ from 'lodash-es'
10 | import { page } from '$app/stores'
11 | import { site } from '$lib/stores/data/site'
12 |
13 | /**
14 | * @param {{
15 | * repo_name: string,
16 | * provider: 'github' | 'gitlab'
17 | * }} params
18 | * @param {boolean} create_new
19 | * @returns {Promise}
20 | */
21 | export async function push_site({ repo_name, provider }, create_new = false) {
22 | const site_bundle = await build_site_bundle({
23 | pages: get(pages),
24 | symbols: get(symbols)
25 | })
26 | if (!site_bundle) {
27 | return null
28 | }
29 |
30 | const files = site_bundle.map((file) => ({
31 | file: file.path,
32 | data: file.content,
33 | size: new Blob([file.content], { type: 'text/plain' }).size / 1024
34 | }))
35 |
36 | return await deploy({ files, site_id: get(site).id, repo_name, provider }, create_new)
37 | }
38 |
39 | export async function build_site_bundle({ pages, symbols }) {
40 | let site_bundle
41 |
42 | let all_sections = []
43 | let all_pages = []
44 | try {
45 | const page_files = await Promise.all(
46 | pages.map((page) => {
47 | return Promise.all(
48 | Object.keys(page.content).map((language) => {
49 | return build_page_tree(page, language)
50 | })
51 | )
52 | })
53 | )
54 | const symbol_files = await Promise.all(
55 | symbols.filter((s) => s.code.js).map((symbol) => build_symbol_tree(symbol))
56 | )
57 | site_bundle = build_site_tree([...symbol_files, ...page_files.flat()])
58 | } catch (e) {
59 | alert(e.message)
60 | }
61 |
62 | return site_bundle
63 |
64 | async function build_symbol_tree(symbol) {
65 | const res = await processCode({
66 | component: {
67 | html: symbol.code.html,
68 | css: symbol.code.css,
69 | js: symbol.code.js,
70 | data: symbol.content['en']
71 | }
72 | })
73 | if (res.error) {
74 | throw Error('Error processing symbol ' + symbol.name)
75 | }
76 | const date = new Intl.DateTimeFormat('en-US', {
77 | year: 'numeric',
78 | month: 'long',
79 | day: 'numeric'
80 | }).format(new Date())
81 | return {
82 | path: '_symbols/' + symbol.id + '.js',
83 | content: `// ${symbol.name} - Updated ${date}\n` + res.js
84 | }
85 | }
86 |
87 | async function build_page_tree(page, language) {
88 | const sections = await dataChanged({
89 | table: 'sections',
90 | action: 'select',
91 | match: { page: page.id },
92 | order: ['index', { ascending: true }]
93 | })
94 |
95 | const { html } = await buildStaticPage({
96 | page,
97 | page_sections: sections,
98 | page_symbols: symbols.filter((symbol) =>
99 | sections.find((section) => section.symbol === symbol.id)
100 | ),
101 | locale: language
102 | })
103 | // const formattedHTML = await beautify.html(html)
104 |
105 | let parent_urls = []
106 | const parent = pages.find((p) => p.id === page.parent)
107 |
108 | if (parent) {
109 | let no_more_parents = false
110 | let grandparent = parent
111 | parent_urls.push(parent.url)
112 | while (!no_more_parents) {
113 | grandparent = pages.find((p) => p.id === grandparent.parent)
114 | if (!grandparent) {
115 | no_more_parents = true
116 | } else {
117 | parent_urls.unshift(grandparent.url)
118 | }
119 | }
120 | }
121 |
122 | let path
123 | let full_url = page.url
124 | if (page.url === 'index' || page.url === '404') {
125 | path = `${page.url}.html`
126 | } else if (parent) {
127 | path = `${parent_urls.join('/')}/${page.url}/index.html`
128 | full_url = `${parent_urls.join('/')}/${page.url}`
129 | } else {
130 | path = `${page.url}/index.html`
131 | }
132 |
133 | // add language prefix
134 | if (language !== 'en') {
135 | path = `${language}/${path}`
136 | full_url = `${language}/${full_url}`
137 | } else {
138 | // only add en sections and pages to primo.json
139 | all_sections = [...all_sections, ...sections]
140 | all_pages = [...all_pages, page]
141 | }
142 |
143 | const page_tree = [
144 | {
145 | path,
146 | content: html
147 | }
148 | ]
149 |
150 | return page_tree
151 | }
152 |
153 | async function build_site_tree(pages) {
154 | const site = get(page).data.site
155 | const symbols = get(page).data.symbols
156 | const json = JSON.stringify({
157 | site: {
158 | id: site.id,
159 | name: site.name,
160 | url: site.url,
161 | code: site.code,
162 | fields: site.fields,
163 | content: site.content
164 | },
165 | pages: all_pages.map((p) => ({
166 | id: p.id,
167 | url: p.url,
168 | name: p.name,
169 | code: p.code,
170 | fields: p.fields,
171 | content: p.content,
172 | site: p.site,
173 | parent: p.parent
174 | })),
175 | sections: all_sections.map((s) => ({
176 | id: s.id,
177 | content: s.content,
178 | page: s.page,
179 | site: s.site,
180 | symbol: s.symbol,
181 | index: s.index
182 | })),
183 | symbols: symbols.map((s) => ({
184 | id: s.id,
185 | name: s.name,
186 | code: s.code,
187 | fields: s.fields,
188 | content: s.content,
189 | site: s.site
190 | })),
191 | version: 2
192 | })
193 |
194 | return [
195 | ..._.flattenDeep(pages),
196 | {
197 | path: `primo.json`,
198 | content: json
199 | },
200 | {
201 | path: 'edit/index.html',
202 | content: `
203 |
204 |
205 |
208 |
209 |
210 | Edit site
211 |
212 |
213 | redirecting to Primo server
214 |
215 |
216 | `
217 | },
218 | {
219 | path: 'robots.txt',
220 | content: `User-agent: *`
221 | }
222 | ]
223 | }
224 | }
225 |
--------------------------------------------------------------------------------
/src/lib/views/modal/Dialog.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 | {#if !options.disableClose}
17 |
18 |
35 |
36 | {/if}
37 |
38 | {#if component === 'IMAGE'}
39 |
onSubmit(detail)} {...props} />
40 | {:else if component === 'LINK'}
41 |
48 | {:else if component === 'FEEDBACK'}
49 |
50 | {:else if typeof component !== 'string'}
51 |
52 | {/if}
53 |
54 |
55 |
56 |
82 |
--------------------------------------------------------------------------------
/src/lib/views/modal/Dialogs/Feedback.svelte:
--------------------------------------------------------------------------------
1 |
21 |
22 | {#if !submitted}
23 |
24 | Feedback
25 | Let us know if something broke, if there's something you want to do that you can't do (yet), or even if you're just enjoying primo.
26 |
52 |
53 | {:else if submitted && successful}
54 |
55 | Feedback Submitted Successfully
56 | Thanks for your message! If you provided your email address, we'll try to follow up with you as soon as possible.
57 |
58 | {:else if submitted && !successful}
59 |
60 | Unsuccessful
61 | For some reason we weren't able to submit your feedback. Please let us know by emailing contact@primo.af
62 |
63 | {/if}
64 |
65 |
--------------------------------------------------------------------------------
/src/lib/views/modal/Dialogs/Image.svelte:
--------------------------------------------------------------------------------
1 |
70 |
71 |
72 |
73 | {#if loading}
74 |
75 |
76 |
77 | {:else}
78 |
79 | {#if value.size}
80 |
81 | {value.size}KB
82 |
83 | {/if}
84 | {#if value.url}
85 |

86 | {/if}
87 |
94 |
95 | {/if}
96 |
120 |
121 |
122 |
123 |
124 |
239 |
--------------------------------------------------------------------------------
/src/lib/views/modal/ModalContainer.svelte:
--------------------------------------------------------------------------------
1 |
22 |
23 | {#if visible}
24 |
29 |
30 |
{} : () => modal.hide()}
34 | />
35 |
36 |
37 |
38 |
39 | {#if $modal.footer}
40 |
41 | {/if}
42 |
43 |
44 | {/if}
45 |
46 |
103 |
--------------------------------------------------------------------------------
/src/lib/views/modal/ModalHeader.svelte:
--------------------------------------------------------------------------------
1 |
25 |
26 |
82 |
83 |
208 |
--------------------------------------------------------------------------------
/src/lib/views/modal/SitePages/PageList/Item.svelte:
--------------------------------------------------------------------------------
1 |
50 |
51 |
52 |
53 | {#if editing_page}
54 |
55 |
edit_page({ name: val }),
60 | on_submit: () => (editing_page = false)
61 | }}
62 | >
63 | {page.name}
64 |
65 | {#if page.url !== 'index'}
66 |
67 |
/
68 |
{
72 | edit_page({ url: validate_url(val) })
73 | },
74 | on_submit: () => (editing_page = false)
75 | }}
76 | >
77 | {page.url}
78 |
79 |
80 | {/if}
81 |
82 | {:else}
83 |
modal.hide()}>
84 | {page.name}
85 | /{page.url !== 'index' ? page.url : ''}
86 |
87 | {/if}
88 | {#if has_children}
89 |
97 | {/if}
98 |
99 |
100 |
108 | {#if page.url !== 'index'}
109 |
112 |
115 | {/if}
116 |
117 |
118 |
119 | {#if creating_page}
120 |
{
123 | creating_page = false
124 | dispatch('create', page)
125 | }}
126 | />
127 | {/if}
128 |
129 | {#if showing_children && has_children}
130 |
131 | {#each children as subpage}
132 | {@const subchildren = $pages.filter((p) => p.parent === subpage.id)}
133 | -
134 |
143 |
144 | {/each}
145 |
146 | {/if}
147 |
148 |
231 |
--------------------------------------------------------------------------------
/src/lib/views/modal/SitePages/PageList/PageForm.svelte:
--------------------------------------------------------------------------------
1 |
34 |
35 |
72 |
73 |
100 |
--------------------------------------------------------------------------------
/src/lib/views/modal/SitePages/PageList/PageList.svelte:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 | {#each $pages.filter((p) => !p.parent) as page, i}
22 | {@const children = $pages.filter((p) => p.parent === page.id)}
23 | -
24 |
- create_page(page)}
29 | on:delete={({ detail: page }) => delete_page(page.id)}
30 | />
31 |
32 | {/each}
33 | {#if creating_page}
34 | -
35 | {
37 | creating_page = false
38 | create_page(new_page)
39 | }}
40 | />
41 |
42 | {/if}
43 |
44 | {#if !creating_page}
45 | (creating_page = true)}
47 | label="Create Page"
48 | icon="akar-icons:plus"
49 | />
50 | {/if}
51 |
52 |
78 |
--------------------------------------------------------------------------------
/src/lib/views/modal/SitePages/SitePages.svelte:
--------------------------------------------------------------------------------
1 |
5 |
6 | `}
8 | title="Pages"
9 | />
10 |
11 |
12 |
13 |
14 |
15 |
22 |
--------------------------------------------------------------------------------
/src/lib/views/modal/index.js:
--------------------------------------------------------------------------------
1 | import Deploy from './Deploy.svelte'
2 | import ComponentEditor from './ComponentEditor/ComponentEditor.svelte'
3 | import SymbolEditor from './SymbolEditor/SymbolEditor.svelte'
4 | import PageEditor from './PageEditor/PageEditor.svelte'
5 | import SiteEditor from './SiteEditor/SiteEditor.svelte'
6 | import SitePages from './SitePages/SitePages.svelte'
7 | import Dialog from './Dialog.svelte'
8 |
9 | export {
10 | Deploy,
11 | ComponentEditor,
12 | SymbolEditor,
13 | PageEditor,
14 | SiteEditor,
15 | SitePages,
16 | Dialog
17 | }
18 |
--------------------------------------------------------------------------------
/src/routes/+layout.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 |
--------------------------------------------------------------------------------
/src/routes/+page.svelte:
--------------------------------------------------------------------------------
1 | Minimal
2 | Nonprofit
3 |
--------------------------------------------------------------------------------
/src/routes/[site]/+layout.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 |
3 | /** @type {import('@sveltejs/kit').Load} */
4 | export async function load(event) {
5 | const [ parent_url = 'index', child_url ] = event.params['page']?.split('/') ?? []
6 | const page_url = child_url ?? parent_url
7 | const {data} = await axios.get(`https://raw.githubusercontent.com/mateomorris/${event.params.site}/main/primo.json`)
8 | const {site, pages, sections, symbols} = data
9 | const page = pages.find(page => page.url === (page_url ?? 'index'))
10 |
11 | return {
12 | user: {
13 | role: 'DEV'
14 | },
15 | site,
16 | page,
17 | pages,
18 | sections: sections.filter(s => s.page === page.id),
19 | symbols,
20 | config: {
21 | github_token: {}
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/src/routes/[site]/+layout.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/routes/[site]/+page.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
13 |
--------------------------------------------------------------------------------
/src/routes/[site]/[...page]/+page.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
13 |
--------------------------------------------------------------------------------
/src/routes/reset.css:
--------------------------------------------------------------------------------
1 | .primo-reset {
2 | @tailwind base;
3 | direction: ltr;
4 | color-scheme: dark;
5 | }
6 |
7 | button,
8 | a {
9 | cursor: pointer;
10 | }
11 |
12 | body {
13 | margin: 0;
14 | }
15 |
16 | .primo-input {
17 | appearance: none;
18 | border: 0;
19 | background-color: transparent;
20 | font-size: inherit;
21 | background: var(--color-white);
22 | padding: 0.5rem 0.75rem;
23 | width: 100%;
24 |
25 | /* &:focus {
26 | box-shadow: 0 0 0 1px var(--color-primored);
27 | border: 0;
28 | }
29 |
30 | &:placeholder {
31 | color: var(--color-gray-5);
32 | } */
33 | }
34 |
35 | .primo-modal {
36 | color: var(--color-gray-1);
37 | /* background: var(--color-gray-9); */
38 | padding: 1rem;
39 | border-radius: var(--primo-border-radius);
40 | margin: 0 auto;
41 | width: 100vw;
42 | }
43 |
44 | .primo-heading-xl {
45 | margin-bottom: 0.5rem;
46 | font-size: 1.25rem;
47 | line-height: 1.75rem;
48 | font-weight: 700;
49 | }
50 |
51 | .primo-heading-lg {
52 | margin-bottom: 0.25rem;
53 | font-size: 1.1rem;
54 | line-height: 1.5rem;
55 | font-weight: 700;
56 | }
57 |
58 | .sr-only {
59 | position: absolute;
60 | width: 1px;
61 | height: 1px;
62 | padding: 0;
63 | margin: -1px;
64 | overflow: hidden;
65 | clip: rect(0, 0, 0, 0);
66 | white-space: nowrap;
67 | border-width: 0;
68 | }
69 |
70 |
--------------------------------------------------------------------------------
/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/palacms/builder/c0952ca1b331d25abd19eeb419c440321e697b08/static/favicon.png
--------------------------------------------------------------------------------
/svelte.config.js:
--------------------------------------------------------------------------------
1 | import adapter from '@sveltejs/adapter-auto';
2 | import preprocess from 'svelte-preprocess';
3 |
4 | /** @type {import('@sveltejs/kit').Config} */
5 | const config = {
6 | preprocess: preprocess({
7 | postcss: true
8 | }),
9 | kit: {
10 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
11 | // If your environment is not supported or you settled on a specific environment, switch out the adapter.
12 | // See https://kit.svelte.dev/docs/adapters for more information about adapters.
13 | adapter: adapter()
14 | },
15 | vitePlugin: {
16 | inspector: true,
17 | },
18 | };
19 |
20 | export default config;
21 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | export default {}
--------------------------------------------------------------------------------
/tests/test.js:
--------------------------------------------------------------------------------
1 | import { expect, test } from '@playwright/test';
2 |
3 | test('Navigates to root-level page', async ({ page }) => {
4 | await page.goto('http://localhost:5173/theme-nonprofit');
5 | await page.waitForSelector('.spinner-container', { state: 'detached' });
6 | await page.click('button#toolbar--pages');
7 | await page.getByRole('link', { name: 'About /about' }).click();
8 | await page.getByRole('navigation', { name: 'toolbar' }).getByText('About')
9 | await page.waitForTimeout(100);
10 | await expect(page.url()).toBe('http://localhost:5173/theme-nonprofit/about');
11 | });
12 |
13 | test('Navigates to child-level page', async ({ page }) => {
14 | await page.goto('http://localhost:5173/theme-nonprofit');
15 | await page.waitForSelector('.spinner-container', { state: 'detached' });
16 | await page.click('button#toolbar--pages');
17 | await page.getByRole('link', { name: 'Blog Post /blog-post' }).click();
18 | await page.getByRole('navigation', { name: 'toolbar' }).getByText('Blog Post')
19 | await page.waitForTimeout(100);
20 | await expect(page.url()).toBe('http://localhost:5173/theme-nonprofit/blog/blog-post');
21 | });
22 |
23 | test('Edits Site CSS', async ({ page }) => {
24 | await page.goto('http://localhost:5173/theme-minimal');
25 | await page.waitForSelector('.spinner-container', { state: 'detached' });
26 | await page.getByRole('button', { name: 'Site' }).click();
27 | await page.getByRole('button', { name: 'Toggle code mode' }).click();
28 | await page.getByText('rem').first().click();
29 | await page.getByText('1', { exact: true }).nth(2).click();
30 | await page.getByText('rem').first().click();
31 | await page.getByText('@import url("https://unpkg.com/@primo-app/primo@1.3.64/reset.css");#page { font-').fill('\n\n@import url("https://unpkg.com/@primo-app/primo@1.3.64/reset.css");\n\n\n#page {\n font-family: system-ui, sans-serif;\n color: var(--color);\n line-height: 1.6; \n font-size: 5rem;\n background: var(--background);\n}\n\n\n.section-container {\n max-width: var(--max-width, 1000px);\n margin: 0 auto;\n padding: 3rem var(--padding, 1rem); \n}\n\n\n.heading {\n font-size: 3rem;\n line-height: 1;\n font-weight: 700;\n margin: 0;\n}\n\n\n.button {\n color: white;\n background: var(--color-accent);\n border-radius: 5px;\n padding: 8px 20px;\n transition: var(--transition);\n\n\n &:hover {\n box-shadow: 0 0 10px 5px rgba(0, 0, 0, 0.1);\n } \n\n\n &.inverted {');
32 | await page.getByRole('button', { name: 'Save' }).click();
33 | await page.waitForTimeout(2000);
34 |
35 | const fontSize = await page.$eval('#page', (element) => {
36 | return window.getComputedStyle(element).getPropertyValue('font-size');
37 | });
38 |
39 | const expectedFontSizePixels = 80; // 5rem equals 80px for most browsers by default
40 | expect(parseFloat(fontSize)).toBe(expectedFontSizePixels);
41 | });
42 |
43 | test('Creates a new page', async ({ page }) => {
44 | await page.goto('http://localhost:5173/theme-nonprofit');
45 | await page.waitForSelector('.spinner-container', { state: 'detached' });
46 | await page.click('button#toolbar--pages');
47 | await page.getByRole('button', { name: 'Create Page' }).click();
48 | await page.getByPlaceholder('About Us', { exact: true }).fill('blankpage');
49 | await page.getByRole('combobox', { name: 'Create from' }).selectOption('null');
50 | await page.getByRole('listitem').filter({ hasText: 'Page Name Page URL Create from Blank───────AboutMissionHome PageTeamBlogBlog Pos' }).getByRole('button').click();
51 | const detailsElement = await page.$('.details:has-text("blankpage /blankpage")');
52 | expect(detailsElement).not.toBeNull();
53 | });
54 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { sveltekit } from '@sveltejs/kit/vite';
2 | import { defineConfig } from 'vitest/config';
3 |
4 | /** @type {import('vite').UserConfig} */
5 | export default defineConfig({
6 | plugins: [sveltekit()],
7 | test: {
8 | include: ['src/**/*.{test,spec}.{js,ts}']
9 | }
10 | });
11 |
--------------------------------------------------------------------------------