diff --git a/import_map.json b/import_map.json
index b3263ef..93dd884 100644
--- a/import_map.json
+++ b/import_map.json
@@ -1,5 +1,5 @@
{
"imports": {
- "lume/": "https://deno.land/x/lume@v1.11.3/"
+ "lume/": "https://deno.land/x/lume@v1.11.4/"
}
}
diff --git a/src/404.tmpl.js b/src/404.tmpl.js
index c9f41c5..69ddfe6 100644
--- a/src/404.tmpl.js
+++ b/src/404.tmpl.js
@@ -13,7 +13,7 @@ export default function () {
url: url
},
slots: {
- hero: `
${search()}
diff --git a/src/_includes/partials/head.ts b/src/_includes/partials/head.ts
index 6a69ab2..b297fb2 100644
--- a/src/_includes/partials/head.ts
+++ b/src/_includes/partials/head.ts
@@ -39,6 +39,7 @@ export default function (metaInfo: MetaInfo) {
${metaInfo.title} — ${app.title}
+
diff --git a/src/main.js b/src/main.js
new file mode 100644
index 0000000..a56bc64
--- /dev/null
+++ b/src/main.js
@@ -0,0 +1,2 @@
+import './scripts/components/is-land.js'
+import './scripts/components/random-404.js'
diff --git a/src/scripts/components/is-land.js b/src/scripts/components/is-land.js
new file mode 100644
index 0000000..953f7d5
--- /dev/null
+++ b/src/scripts/components/is-land.js
@@ -0,0 +1,342 @@
+class Island extends HTMLElement {
+ static tagName = 'is-land';
+
+ static fallback = {
+ ':not(:defined)': (readyPromise, node, prefix) => {
+ // remove from document to prevent web component init
+ let cloned = document.createElement(prefix + node.localName);
+ for (let attr of node.getAttributeNames()) {
+ cloned.setAttribute(attr, node.getAttribute(attr));
+ }
+
+ let children = Array.from(node.childNodes);
+ for (let child of children) {
+ cloned.append(child); // Keep the *same* child nodes, clicking on a details->summary child should keep the state of that child
+ }
+ node.replaceWith(cloned);
+
+ return readyPromise.then(() => {
+ // restore children (not cloned)
+ for (let child of Array.from(cloned.childNodes)) {
+ node.append(child);
+ }
+ cloned.replaceWith(node);
+ });
+ },
+ };
+
+ static autoinit = {
+ 'petite-vue': function (library) {
+ library.createApp().mount(this);
+ },
+ vue: function (library) {
+ library.createApp().mount(this);
+ },
+ svelte: function (mod) {
+ new mod.default({ target: this });
+ },
+ 'svelte-ssr': function (mod) {
+ new mod.default({ target: this, hydrate: true });
+ },
+ preact: function (mod) {
+ mod.default(this);
+ },
+ };
+
+ constructor() {
+ super();
+
+ this.attrs = {
+ autoInitType: 'autoinit',
+ import: 'import',
+ scriptType: 'module/island',
+ template: 'data-island',
+ ready: 'ready',
+ };
+
+ this.conditionMap = {
+ visible: Conditions.visible,
+ idle: Conditions.idle,
+ interaction: Conditions.interaction,
+ media: Conditions.media,
+ 'save-data': Conditions.saveData,
+ };
+
+ // Internal promises
+ this.ready = new Promise((resolve, reject) => {
+ this.readyResolve = resolve;
+ this.readyReject = reject;
+ });
+ }
+
+ static getParents(el, selector) {
+ let nodes = [];
+ while (el) {
+ if (el.matches && el.matches(selector)) {
+ nodes.push(el);
+ }
+ el = el.parentNode;
+ }
+ return nodes;
+ }
+
+ static async ready(el) {
+ let parents = Island.getParents(el, Island.tagName);
+ let imports = await Promise.all(parents.map((el) => el.wait()));
+
+ // return innermost module import
+ if (imports.length) {
+ return imports[0];
+ }
+ }
+
+ async forceFallback() {
+ let prefix = Island.tagName + '--';
+ let promises = [];
+
+ if (window.Island) {
+ Object.assign(Island.fallback, window.Island.fallback);
+ }
+
+ for (let selector in Island.fallback) {
+ // Reverse here as a cheap way to get the deepest nodes first
+ let components = Array.from(this.querySelectorAll(selector)).reverse();
+
+ // with thanks to https://gist.github.com/cowboy/938767
+ for (let node of components) {
+ if (!node.isConnected || node.localName === Island.tagName) {
+ continue;
+ }
+
+ let readyPromise = Island.ready(node);
+ promises.push(Island.fallback[selector](readyPromise, node, prefix));
+ }
+ }
+
+ return promises;
+ }
+
+ wait() {
+ return this.ready;
+ }
+
+ getConditions() {
+ let map = {};
+ for (let key of Object.keys(this.conditionMap)) {
+ if (this.hasAttribute(`on:${key}`)) {
+ map[key] = this.getAttribute(`on:${key}`);
+ }
+ }
+
+ return map;
+ }
+
+ async connectedCallback() {
+ // Keep fallback content without initializing the components
+ // TODO improvement: only run this for not-eager components?
+ await this.forceFallback();
+
+ await this.hydrate();
+ }
+
+ getInitScripts() {
+ return this.querySelectorAll(
+ `:scope script[type="${this.attrs.scriptType}"]`
+ );
+ }
+
+ getTemplates() {
+ return this.querySelectorAll(`:scope template[${this.attrs.template}]`);
+ }
+
+ replaceTemplates(templates) {
+ // replace
with the live content
+ for (let node of templates) {
+ // get rid of the rest of the content on the island
+ if (node.getAttribute(this.attrs.template) === 'replace') {
+ let children = Array.from(this.childNodes);
+ for (let child of children) {
+ this.removeChild(child);
+ }
+ this.appendChild(node.content);
+ break;
+ } else {
+ node.replaceWith(node.content);
+ }
+ }
+ }
+
+ async hydrate() {
+ let conditions = [];
+ if (this.parentNode) {
+ // wait for all parents before hydrating
+ conditions.push(Island.ready(this.parentNode));
+ }
+ let attrs = this.getConditions();
+ for (let condition in attrs) {
+ if (this.conditionMap[condition]) {
+ conditions.push(this.conditionMap[condition](attrs[condition], this));
+ }
+ }
+ // Loading conditions must finish before dependencies are loaded
+ await Promise.all(conditions);
+
+ this.replaceTemplates(this.getTemplates());
+
+ let mod;
+ // [dependency="my-component-code.js"]
+ let importScript = this.getAttribute(this.attrs.import);
+ if (importScript) {
+ // we could resolve import maps here manually but you’d still have to use the full URL in your script’s import anyway
+ mod = await import(importScript);
+ }
+
+ // do nothing if has script[type="module/island"], will init manually in script via ready()
+ let initScripts = this.getInitScripts();
+
+ if (initScripts.length > 0) {
+ // activate