From 1fb40d636b2a7c477a5868eca5824751ea0157c5 Mon Sep 17 00:00:00 2001 From: benStre Date: Thu, 28 Mar 2024 23:29:24 +0100 Subject: [PATCH 01/19] add editable template function, edit mode session flags --- src/app/frontend-manager.ts | 6 ++- src/html/editable-template.ts | 91 +++++++++++++++++++++++++++++++++++ src/html/render.ts | 42 ++++++++++++---- src/html/template-strings.ts | 2 +- src/html/template.ts | 10 +++- src/routing/context.ts | 45 +++++++++++++++++ src/server/server.ts | 1 + src/style/base.css | 22 +++++++++ 8 files changed, 206 insertions(+), 13 deletions(-) create mode 100644 src/html/editable-template.ts diff --git a/src/app/frontend-manager.ts b/src/app/frontend-manager.ts index cf5f5eab7..d1efe2b60 100644 --- a/src/app/frontend-manager.ts +++ b/src/app/frontend-manager.ts @@ -859,7 +859,10 @@ if (!window.location.origin.endsWith(".unyt.app")) { // convert content to valid HTML string if (content instanceof Element || content instanceof DocumentFragment) { - const requiredPointers = new Set() + const requiredPointers = new Set(); + + context = context instanceof Function ? context() : context; + const html = getOuterHTML( content as Element, { @@ -868,6 +871,7 @@ if (!window.location.origin.endsWith(".unyt.app")) { injectStandaloneComponents:render_method!=RenderMethod.STATIC, allowIgnoreDatexFunctions:(render_method==RenderMethod.HYBRID||render_method==RenderMethod.PREVIEW), lang, + editMode: !!context?.getSessionFlag("editMode"), requiredPointers } ); diff --git a/src/html/editable-template.ts b/src/html/editable-template.ts new file mode 100644 index 000000000..c36aef8f9 --- /dev/null +++ b/src/html/editable-template.ts @@ -0,0 +1,91 @@ +import { getCallerFile, getCallerInfo } from "datex-core-legacy/utils/caller_metadata.ts"; +import { createTemplateGenerator, jsxInputGenerator } from "./template.ts"; +import { Class } from "datex-core-legacy/utils/global_types.ts"; +import { cache_path } from "datex-core-legacy/runtime/cache_path.ts"; +import { UIX } from "../../uix.ts"; +import { getOuterHTML } from "./render.ts"; +import { Path } from "datex-core-legacy/utils/path.ts"; +import { app } from "../app/app.ts"; + +export function editableTemplate< + Options extends Record = Record +>( + propsInfo: {[key in keyof Options]: string}, + initialContent?: jsxInputGenerator, Options, never, false, false> +): jsxInputGenerator&((cl: Class, context: ClassDecoratorContext)=>any) { + const info = getCallerInfo()?.[0]; + if (!info) throw new Error("Cannot get caller info"); + + const module = info.file + "_" + info.row; + + // store initial template + if (!hasStoredTemplate(module)) { + const generator = initialContent ? createTemplateGenerator(initialContent, module) : null; + const template = generator ? generateTemplateFromGenerator(generator, propsInfo) : ""; + updateTemplate(module, template); + } + + return (...args) => getTemplateFunction(module, propsInfo)(...args); +} + +type PropsDefinition = Record; + +const cachedTemplateGenerators = new Map(); + +function generateTemplateFromGenerator(generator: jsxInputGenerator, propsDefinition: PropsDefinition) { + const placeholderProps = Object.fromEntries(Object.entries(propsDefinition).map(([key]) => [key, `{{${key}}}`])); + const rendered = (generator as any)(placeholderProps, placeholderProps); + const [_, html] = getOuterHTML(rendered, { + plainHTML: true, + }); + return html; +} + +function hasStoredTemplate(modulePath: string) { + const templatePath = getTemplatePath(modulePath) + return templatePath.fs_exists; +} + +function getStoredTemplate(modulePath: string) { + const templatePath = getTemplatePath(modulePath) + if (templatePath.fs_exists) { + return Deno.readTextFileSync(templatePath.normal_pathname); + } + else return null +} + +export function updateTemplate(modulePath: string, content: string) { + const templatePath = getTemplatePath(modulePath); + Deno.writeTextFileSync(templatePath.normal_pathname, content); + if (cachedTemplateGenerators.has(modulePath)) updateTemplateFunction(modulePath, cachedTemplateGenerators.get(modulePath)![1]); +} + +function getTemplateFunction(modulePath: string, propsInfo: PropsDefinition) { + if (!cachedTemplateGenerators.has(modulePath)) { + updateTemplateFunction(modulePath, propsInfo); + } + return cachedTemplateGenerators.get(modulePath)![0]; +} + +function updateTemplateFunction(modulePath: string, propsInfo: PropsDefinition) { + const template = getStoredTemplate(modulePath); + if (!template) throw new Error("Template not found"); + const propsDestructoringCode = `const {${ + Object.keys(propsInfo).join(", ") + }} = props`; + const generator = new Function('props', `${propsDestructoringCode}; const _genEl = HTML\`${template.replace(/\{\{([^}]*)\}\}/g, '\${$1}')}\`; _genEl.setAttribute("data-edit-location", "${modulePath}"); _genEl.setAttribute("data-edit-props", '${JSON.stringify(propsInfo)}'); return _genEl;`); + + cachedTemplateGenerators.set(modulePath, [generator, propsInfo]); +} + + +function getTemplatePath(modulePath: string|URL) { + const modulePathObj = new Path(modulePath) + const modulePathName = modulePathObj.isWeb() ? modulePathObj.toString() : modulePathObj.getAsRelativeFrom(app.base_url) + const normalizedPath = modulePathName.replace(/[^a-zA-Z0-9]/g, "_").replace(/^_*/, ""); + const templateDir = UIX.cacheDir.getChildPath("templates").asDir(); + + if (!templateDir.fs_exists) Deno.mkdirSync(templateDir.normal_pathname, {recursive: true}); + + return templateDir.getChildPath(normalizedPath); +} \ No newline at end of file diff --git a/src/html/render.ts b/src/html/render.ts index bc337315c..fd802cde1 100644 --- a/src/html/render.ts +++ b/src/html/render.ts @@ -34,6 +34,8 @@ type _renderOptions = { lang?:string, allowIgnoreDatexFunctions?: boolean, requiredPointers?: Set, + plainHTML?: boolean, // if true, only plain html is returned, attributes like uix-ptr are not rendered + editMode?: boolean, // if true, editable content is enabled for editable elements forceParentLive?: boolean // set by child nodes to indicate parent should be marked as hydratable } @@ -88,7 +90,7 @@ function loadInitScript(component:(Element|ShadowRoot) & {getStandaloneInit?:()= } export function getInnerHTML(el:Element|ShadowRoot, opts?:_renderOptions, collectedStylesheets?:string[], standaloneContext = false) { - if (!opts?.includeShadowRoots) return el.innerHTML; + if (!opts?.includeShadowRoots && !opts?.plainHTML) return el.innerHTML; let html = ""; @@ -214,6 +216,10 @@ function _getOuterHTML(el:Node, opts?:_renderOptions, collectedStylesheets?:stri for (let i = 0; i < el.attributes.length; i++) { const attrib = el.attributes[i]; + + if (opts?.plainHTML && attrib.name == "uix-ptr") continue; + if (!opts?.editMode && (attrib.name == "data-edit-location" || attrib.name == "data-edit-props")) continue; + let val:string; const transformMap = opts?.lang && (el as any)[DOMUtils.ATTR_DX_VALUES]?.get(attrib.name)?.transformMap; @@ -224,6 +230,12 @@ function _getOuterHTML(el:Node, opts?:_renderOptions, collectedStylesheets?:stri val = attrib.value; } + // is a module identifier -> resolve - only for specific attributes: + // * src + if (attrib.name == "src" && val.match(/^[a-zA-Z0-9_]/)) { + val = import.meta.resolve(val); + } + // relative web path (@...) if (val.startsWith("file://")) val = convertToWebPath(val); // blob -> data url @@ -243,14 +255,22 @@ function _getOuterHTML(el:Node, opts?:_renderOptions, collectedStylesheets?:stri else if (el.checked !== false) attrs.push(`value="${domUtils.escapeHtml(el.checked?.toString()??"")}"`) } - // hydratable - if (forceLive || isLiveNode(el)) { - if (opts?.requiredPointers) opts.requiredPointers.add(el); - attrs.push("uix-dry"); + if (!opts?.plainHTML) { + // hydratable + if (forceLive || isLiveNode(el)) { + if (opts?.requiredPointers) opts.requiredPointers.add(el); + attrs.push("uix-dry"); + } + + // just static + else attrs.push("uix-static"); + } + + // enable edit mode (TODO:) + if (opts?.editMode && el.hasAttribute("data-edit-location")) { + attrs.push("contenteditable"); } - - // just static - else attrs.push("uix-static"); + // inject event listeners if (dataPtr && opts?._injectedJsData && ((el)[DOMUtils.EVENT_LISTENERS] || (el)[DOMUtils.ATTR_BINDINGS])) { @@ -365,8 +385,8 @@ function _getOuterHTML(el:Node, opts?:_renderOptions, collectedStylesheets?:stri opts?.forms?.pop() } - if (selfClosingTags.has(tag)) return `<${tag} ${attrs.join(" ")}/>`; - else return `<${tag} ${attrs.join(" ")}>${inner}` + if (selfClosingTags.has(tag)) return `<${tag}${attrs.length ? ' ' + attrs.join(" ") : ''}/>`; + else return `<${tag}${attrs.length ? ' ' + attrs.join(" ") : ''}>${inner}` // const outer = el.cloneNode().outerHTML; // const start = outer.replace(/<\/[A-Za-z0-9-_ ]+>$/, ''); // const end = outer.match(/<\/[A-Za-z0-9-_ ]+>$/)?.[0] ?? ""; // might not have an end tag @@ -495,6 +515,8 @@ export function getOuterHTML(el:Element|DocumentFragment, opts?:{ injectStandaloneComponents?: boolean, allowIgnoreDatexFunctions?: boolean, lang?:string, + plainHTML?: boolean, + editMode?: boolean requiredPointers?: Set }): [header_script:string, html_content:string] { diff --git a/src/html/template-strings.ts b/src/html/template-strings.ts index dda3587b5..9d33179f0 100644 --- a/src/html/template-strings.ts +++ b/src/html/template-strings.ts @@ -1,4 +1,4 @@ -import { getHTMLGenerator } from "../uix-dom/html-template-strings/html_template_strings.ts"; +import { getHTMLGenerator } from "../uix-dom/html-template-strings/html-template-strings.ts"; import { domContext, domUtils } from "../app/dom-context.ts"; import { jsx } from "../jsx-runtime/jsx.ts"; diff --git a/src/html/template.ts b/src/html/template.ts index e604c1405..53c3a5ff5 100644 --- a/src/html/template.ts +++ b/src/html/template.ts @@ -165,8 +165,16 @@ export function template():jsxInputGenerator, export function template(templateOrGenerator?:JSX.Element|jsxInputGenerator, any, any, any>) { - let generator:any; const module = getCallerFile(); + return createTemplateGenerator(templateOrGenerator, module); +} + +export function createTemplateGenerator(templateOrGenerator?:JSX.Element|jsxInputGenerator, any, any, any>, module?: string):jsxInputGenerator, never>&((cl: Class, context: ClassDecoratorContext)=>any) { + + let generator: Function; + + module ??= getCallerFile(); + if (typeof templateOrGenerator == "function") generator = function(propsOrClass:any, context?:any) { // decorator if (Component.isPrototypeOf(propsOrClass)) { diff --git a/src/routing/context.ts b/src/routing/context.ts index 8ab2474c4..ad9dd417c 100644 --- a/src/routing/context.ts +++ b/src/routing/context.ts @@ -44,6 +44,8 @@ export class Context > { + static #sessionFlags = new WeakMap>() + request: Request requestData: RequestData = { address: null @@ -137,6 +139,49 @@ export class Context if (endpoint == BROADCAST) throw new Error("Cannot get private data for UIX context, no session found"); } + /** + * Enables edit mode for the current session. + * Allows the user to edit editableTemplate content directly on the website. + */ + enableEditMode() { + this.setSessionFlag("editMode"); + } + /** + * Disables edit mode for the current session. + */ + disableEditMode() { + this.removeSessionFlag("editMode"); + } + + /** + * Returns the value of a session flag if it is set. + * @param key session flag key + */ + getSessionFlag(key: string) { + if (!this.endpoint) throw new Error("Cannot set session flag without endpoint"); + return Context.#sessionFlags.get(this.endpoint)?.get(key); + } + + /** + * Sets a session flag for the current session. + * Important: Don't set the flag to false, use removeSessionFlag instead. + * @param key session flag key + * @param value optional session flag value, defaults: true + */ + setSessionFlag(key: string, value: any = true) { + if (!this.endpoint) throw new Error("Cannot set session flag without endpoint"); + if (!Context.#sessionFlags.has(this.endpoint)) Context.#sessionFlags.set(this.endpoint, new Map()); + Context.#sessionFlags.get(this.endpoint)!.set(key, value); + } + + /** + * Removes a session flag for the current session. + * @param key session flag key + */ + removeSessionFlag(key: string) { + if (!this.endpoint) throw new Error("Cannot delete session flag without endpoint"); + Context.#sessionFlags.get(this.endpoint)?.delete(key); + } [Symbol.dispose]() { console.log("disposing context", this.#disposeCallbacks); diff --git a/src/server/server.ts b/src/server/server.ts index 9b169dc1d..772772fde 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -443,6 +443,7 @@ export class Server { const datexEndpointNew = getCookie("datex-endpoint-new", requestEvent.request.headers, port); const uixSessionCookie = getCookie("uix-session", requestEvent.request.headers, port); + // has datex-endpoint if (datexEndpointCookie) { diff --git a/src/style/base.css b/src/style/base.css index b92aaeab9..8b258a780 100644 --- a/src/style/base.css +++ b/src/style/base.css @@ -10,6 +10,28 @@ light-root, uix-fragment, uix-placeholder, frontend-slot { display: contents; } +uix-editable { + display: inline-block; +} + +uix-editable[contenteditable]:focus { + outline: 3px dashed var(--text); + outline-offset: 24px; + position: relative; + + &::after { + content: attr(data-placeholder); + width: 100vw; + height: 1000vh; + position: absolute; + left: calc(100% + 27px); + top: -500vh; + backdrop-filter: grayscale(100%); + /* background-color: red; */ + z-index: 100000; + } +} + frontend-slot > * { all: inherit; display: revert; From da77d84a57a4447aa5444ff0a0ab79873d14bebb Mon Sep 17 00:00:00 2001 From: benStre Date: Thu, 28 Mar 2024 23:30:05 +0100 Subject: [PATCH 02/19] update uix-dom --- src/uix-dom | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uix-dom b/src/uix-dom index ad6b6af5d..1b331c60c 160000 --- a/src/uix-dom +++ b/src/uix-dom @@ -1 +1 @@ -Subproject commit ad6b6af5d5afcfbdb7a3926be0ab94e3aa831838 +Subproject commit 1b331c60c4841666fc7e8d22f27bdf61a0691c1f From 487094ec8bb853a18664a3a97588ef9494bbb2cc Mon Sep 17 00:00:00 2001 From: benStre Date: Fri, 29 Mar 2024 16:32:40 +0100 Subject: [PATCH 03/19] fix theme setters --- uix.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/uix.ts b/uix.ts index 23b996abb..474b0d072 100644 --- a/uix.ts +++ b/uix.ts @@ -41,15 +41,25 @@ export const UIX = { }) // redefine UIX.Theme.theme/mode getters to enable smart transforms / effects + + const themeSetter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(themeManager), "theme")!.set!.bind(themeManager); + const modeSetter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(themeManager), "mode")!.set!.bind(themeManager); + Object.defineProperty(themeManager, "theme", { get() { return theme.val; + }, + set(val: string) { + themeSetter(val); } }) Object.defineProperty(themeManager, "mode", { get() { return mode.val; + }, + set(val: "dark"|"light") { + modeSetter(val); } }) From 043406888069676d2589f620f49ce555ef98cbea Mon Sep 17 00:00:00 2001 From: benStre Date: Thu, 4 Apr 2024 21:01:14 +0200 Subject: [PATCH 04/19] fix clear for local development --- src/runners/run-local.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/runners/run-local.ts b/src/runners/run-local.ts index 6353abd43..585a3dbb2 100644 --- a/src/runners/run-local.ts +++ b/src/runners/run-local.ts @@ -130,7 +130,8 @@ export async function runLocal(params: runParams, root_path: URL, options: norma args.splice(args.indexOf("--clear"), 1); isClearingState = true; } - else { + // only set clear indicator when running in deployed environment + else if(Deno.env.has("UIX_HOST_ENDPOINT")) { Deno.mkdirSync(cache_path, {recursive: true}); Deno.writeTextFileSync(clearIndicatorPath.normal_pathname, ""); } From 65f4221d805c56e7c5b687ad2338ad79266933ae Mon Sep 17 00:00:00 2001 From: benStre Date: Sun, 7 Apr 2024 19:57:08 +0200 Subject: [PATCH 05/19] update run local --- src/runners/run-local.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runners/run-local.ts b/src/runners/run-local.ts index 585a3dbb2..7f76a174f 100644 --- a/src/runners/run-local.ts +++ b/src/runners/run-local.ts @@ -131,7 +131,7 @@ export async function runLocal(params: runParams, root_path: URL, options: norma isClearingState = true; } // only set clear indicator when running in deployed environment - else if(Deno.env.has("UIX_HOST_ENDPOINT")) { + else if (Deno.env.has("UIX_HOST_ENDPOINT")) { Deno.mkdirSync(cache_path, {recursive: true}); Deno.writeTextFileSync(clearIndicatorPath.normal_pathname, ""); } From c21678410df68846156c95152c194f6642bb2e56 Mon Sep 17 00:00:00 2001 From: benStre Date: Sun, 7 Apr 2024 20:15:26 +0200 Subject: [PATCH 06/19] refactor css scoping using nested root selector --- src/utils/css-scoping.ts | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/src/utils/css-scoping.ts b/src/utils/css-scoping.ts index 9741a3e0a..a21bf2a0c 100644 --- a/src/utils/css-scoping.ts +++ b/src/utils/css-scoping.ts @@ -1,30 +1,18 @@ /** - * Add a scope selector everywhere in a CSS document + * Wrap a CSS document with a custom scope selector * @param css css document * @param scope scope selector * @returns */ export function addCSSScopeSelector(css: string, scope: string) { - const scopedCSS = css.replace(/^[^@\n]+{[^}]*}/gm, (part) => { - if (part.match(/^(to|from|\d+%|[\s,\n])+{[^}]*}$/)) return part; // is inside @keyframe (e.g. "50% {}""), ignore - else { - // for each selectors (e.g. ':host, a#b'), add the scope, e.g. 'scope, scope a#b' - return part.replace(/^[^@\n]+(?={)/, (s) => { - const selectors = s.split(/, */g).map(selector => - (selector.includes(":host") || selector.includes(":root")) ? - selector - .trimEnd() - .replace(/\:(host|root)\s*\:host-context/g, ':host-context') - .replace(/\:(host|root)(?![-\w])/g, scope) : - scope + ' ' + selector.trimEnd() - ); - return selectors.join(", ") + ' ' - }) - } - }); - return scopedCSS; + return `${scope} {\n${ + ' ' + + css + .replaceAll(':host', '&') + .replaceAll('\n', '\n ') + }\n}`; } export function addCSSScope(css: string) { - return "@scope {\n" + css + "\n}"; + return addCSSScopeSelector(css, "@scope") } \ No newline at end of file From bf38ca0b793b93f21d3cdf53303fd9bdd68f9fdd Mon Sep 17 00:00:00 2001 From: benStre Date: Tue, 9 Apr 2024 22:27:25 +0200 Subject: [PATCH 07/19] add todo --- src/utils/css-scoping.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/css-scoping.ts b/src/utils/css-scoping.ts index a21bf2a0c..5c5cc1521 100644 --- a/src/utils/css-scoping.ts +++ b/src/utils/css-scoping.ts @@ -5,6 +5,7 @@ * @returns */ export function addCSSScopeSelector(css: string, scope: string) { + // TODO: not working with @keyframes return `${scope} {\n${ ' ' + css From b56da76bdc95f64b7f31a18d782c1af110c336eb Mon Sep 17 00:00:00 2001 From: benStre Date: Tue, 9 Apr 2024 22:27:53 +0200 Subject: [PATCH 08/19] disable new scopingg --- src/utils/css-scoping.ts | 50 ++++++++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/src/utils/css-scoping.ts b/src/utils/css-scoping.ts index 5c5cc1521..91c1de2a6 100644 --- a/src/utils/css-scoping.ts +++ b/src/utils/css-scoping.ts @@ -1,19 +1,51 @@ +// TODO: +// /** +// * Wrap a CSS document with a custom scope selector +// * @param css css document +// * @param scope scope selector +// * @returns +// */ +// export function addCSSScopeSelector(css: string, scope: string) { +// // TODO: not working with @keyframes +// return `${scope} {\n${ +// ' ' + +// css +// .replaceAll(':host', '&') +// .replaceAll('\n', '\n ') +// }\n}`; +// } + +// export function addCSSScope(css: string) { +// return addCSSScopeSelector(css, "@scope") +// } + /** - * Wrap a CSS document with a custom scope selector + * Add a scope selector everywhere in a CSS document * @param css css document * @param scope scope selector * @returns */ export function addCSSScopeSelector(css: string, scope: string) { - // TODO: not working with @keyframes - return `${scope} {\n${ - ' ' + - css - .replaceAll(':host', '&') - .replaceAll('\n', '\n ') - }\n}`; + const scopedCSS = css.replace(/^[^@\n]+{[^}]*}/gm, (part) => { + if (part.match(/^(to|from|\d+%|[\s,\n])+{[^}]*}$/)) return part; // is inside @keyframe (e.g. "50% {}""), ignore + else { + // for each selectors (e.g. ':host, a#b'), add the scope, e.g. 'scope, scope a#b' + return part.replace(/^[^@\n]+(?={)/, (s) => { + const selectors = s.split(/, */g).map(selector => + (selector.includes(":host") || selector.includes(":root")) ? + selector + .trimEnd() + .replace(/\:(host|root)\s*\:host-context/g, ':host-context') + .replace(/\:(host|root)(?![-\w])/g, scope) : + scope + ' ' + selector.trimEnd() + ); + return selectors.join(", ") + ' ' + }) + } + }); + return scopedCSS; } export function addCSSScope(css: string) { - return addCSSScopeSelector(css, "@scope") + return "@scope {\n" + css + "\n}"; } \ No newline at end of file From 6dff5f0c07bcdd41ab7a4d5e7d0d81c387610f94 Mon Sep 17 00:00:00 2001 From: benStre Date: Thu, 18 Apr 2024 19:48:22 +0200 Subject: [PATCH 09/19] remove $ in docs --- docs/manual/01 Getting Started.md | 8 ++++---- docs/manual/08 Configuration.md | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/manual/01 Getting Started.md b/docs/manual/01 Getting Started.md index 8424b1ef3..a1b37e18b 100644 --- a/docs/manual/01 Getting Started.md +++ b/docs/manual/01 Getting Started.md @@ -56,14 +56,14 @@ $ brew install uix Now, you can install UIX with `deno install`: ```bash -$ deno install --import-map https://cdn.unyt.org/uix/importmap.json -Aq -n uix https://cdn.unyt.org/uix/run.ts +deno install --import-map https://cdn.unyt.org/uix/importmap.json -Aq -n uix https://cdn.unyt.org/uix/run.ts ``` ## Creating a new UIX project You can create a new UIX project by running ```bash -$ uix --init +uix --init ``` This creates a new base project (https://github.com/unyt-org/uix-base-project.git) in the current directory @@ -79,7 +79,7 @@ To run your UIX app, make sure the [app.dx](./08%20Configuration.md#the-app-dx-f Execute the `uix` command in the root directory of your application (where the `app.dx` is located) to initialize and run the project. ```bash -$ uix +uix ``` You can pass the following args to the UIX command line utility: @@ -107,7 +107,7 @@ You can pass the following args to the UIX command line utility: To run your UIX project without installing the UIX CLI, you can alternatively run the following command in the project root directory: ```bash -$ deno run -A --import-map https://cdn.unyt.org/importmap.json https://cdn.unyt.org/uix/run.ts +deno run -A --import-map https://cdn.unyt.org/importmap.json https://cdn.unyt.org/uix/run.ts ``` ## Architecture of a UIX Project diff --git a/docs/manual/08 Configuration.md b/docs/manual/08 Configuration.md index 179681b7f..d5258f678 100644 --- a/docs/manual/08 Configuration.md +++ b/docs/manual/08 Configuration.md @@ -80,7 +80,7 @@ UIX apps can be run in different **stages**. The names of the stages are not pre To run a UIX app in a specific stage, use the `--stage` options: ```bash -$ uix --stage production +uix --stage production ``` Per default, running a UIX app in a different stage does not have any noticable effect. From 471f358a35a881ca59dc4c344dd05021c5a8652b Mon Sep 17 00:00:00 2001 From: Jonas Strehle Date: Fri, 19 Apr 2024 08:10:03 +0200 Subject: [PATCH 10/19] Fix #119 --- docs/manual/11 Style and Themes.md | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/docs/manual/11 Style and Themes.md b/docs/manual/11 Style and Themes.md index 2df6c1533..14c0bc1f2 100644 --- a/docs/manual/11 Style and Themes.md +++ b/docs/manual/11 Style and Themes.md @@ -161,10 +161,24 @@ but you can override the current mode: UIX.Theme.setMode('dark'); ``` -### Observing mode changes +### Observing theme and mode changes -Changes between dark and light mode can be handled with `UIX.Theme.onModeChange`: +UIX provides the reactive properties `UIX.Theme.$.theme` and `UIX.Theme.$.mode` to observe and handle theme and mode changes. +These properties can be used in JSX to display different content or change styling for different themes and light or dark mode. + +DATEX `effects` and `always` statements get triggered on change: ```ts -UIX.Theme.onModeChange(mode => console.log("mode changed to", mode);) +effect(() => console.log(`Mode changed to ${UIX.Theme.$.mode}`)); +``` + + +```tsx +
+ { always(() => + val(UIX.Theme.$.mode) == "dark" ? + "Dark mode" : + "Light mode" + ) } +
``` From b906d11002c5ac2ee11d8c40b73a4b5d37fe686a Mon Sep 17 00:00:00 2001 From: Jonas Strehle Date: Mon, 22 Apr 2024 17:59:00 +0200 Subject: [PATCH 11/19] update getting started intro --- docs/manual/01 Getting Started.md | 36 +++++++++++++++++++------------ 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/docs/manual/01 Getting Started.md b/docs/manual/01 Getting Started.md index a1b37e18b..3e0c5de91 100644 --- a/docs/manual/01 Getting Started.md +++ b/docs/manual/01 Getting Started.md @@ -1,18 +1,26 @@ # Getting Started with UIX -UIX is an open-source full-stack framework for developing reactive web apps with *restorable and shared state*. -UIX apps run on a [Deno](https://docs.deno.com/runtime/manual) backend and use state-of-the-art web technologies. +## What is UIX? -The [DATEX JavaScript Library](https://docs.unyt.org/manual/datex/introduction) acts as the backbone of UIX, providing useful functionality such as *reactivity and cross-device data exchange*. -In contrast to frameworks like React, UIX provides *direct wiring* to the DOM for reactivity and does not need a virtual DOM. +UIX is a state-of-the-art TypeScript framework for developing web apps. +With UIX, you can write frontend and backend code in a single [Deno](https://docs.deno.com/runtime/manual) project. +UIX abstracts away the complexity of communicating between servers and clients - there +is no need to think about APIs, serialization, or data storage. +Everything is connected. -**Our core principles** - * Full compatibility with web standards - * Full compatibility with [DATEX](https://github.com/unyt-org/datex-specification) and unyt.org Supranet principles - * Both backend and frontend code is written as ES6 TypeScript modules - * No JavaScript bundlers +The [DATEX JavaScript Library](https://docs.unyt.org/manual/datex/introduction) acts as the backbone of UIX, providing useful functionality such as *reactivity, restorable state and cross-device data exchange*. + +UIX works out of the box with TypeScript and JSX and does not require any additional tooling or build steps. + +UIX encourages the use of standard web APIs wherever possible, and provides a simple +and intuitive abstraction layer for more advanced features. + +> [!NOTE] +> The [UIX Guide](./17%20Guide.md) explains the core features of UIX and how to use them in your project. + + +## Main Features -**Main features** * [Cross-network reactivity](02%20Cross-Realm%20Imports.md#Reactivity) * [Server side rendering with partial hydration](07%20Rendering%20Methods.md) * [Hybrid backend/frontend routing](05%20Entrypoints%20and%20Routing.md) @@ -30,13 +38,10 @@ This is why UIX ships with integrated features such as: * [Automated deployment](./13%20Deployment.md) * [Testing library](https://github.com/unyt-org/unyt-tests/) -### CLI Installation +## Installation To install UIX, you first need to install [Deno](https://docs.deno.com/runtime/manual/getting_started/installation). -> [!WARNING] -> UIX is only supported for Deno versions > 1.40.0 -