├── .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 | 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 |
17 | Label 18 |
19 | 20 |
21 |
22 |
23 | ID 24 |
25 | 26 |
27 |
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 |
39 |
40 |