create-easel-theme 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +100 -0
- package/bin/create-easel-theme.mjs +9 -0
- package/dist/cli.js +604 -0
- package/dist/templates/js/alpine/snippets/counter.liquid +9 -0
- package/dist/templates/js/alpine/src/main.js +19 -0
- package/dist/templates/js/base/_gitignore +5 -0
- package/dist/templates/js/base/assets/easel.svg +4 -0
- package/dist/templates/js/base/config/settings_data.json +12 -0
- package/dist/templates/js/base/config/settings_schema.json +27 -0
- package/dist/templates/js/base/layout/theme.liquid +19 -0
- package/dist/templates/js/base/locales/en.default.json +13 -0
- package/dist/templates/js/base/locales/en.default.schema.json +21 -0
- package/dist/templates/js/base/sections/hero.liquid +30 -0
- package/dist/templates/js/base/sections/main-404.liquid +11 -0
- package/dist/templates/js/base/snippets/counter.liquid +9 -0
- package/dist/templates/js/base/src/main.js +21 -0
- package/dist/templates/js/base/src/sections.js +44 -0
- package/dist/templates/js/base/src/style.css +61 -0
- package/dist/templates/js/base/templates/404.json +9 -0
- package/dist/templates/js/base/templates/index.json +9 -0
- package/dist/templates/js/react/src/main.jsx +23 -0
- package/dist/templates/js/vue/src/Counter.vue +18 -0
- package/dist/templates/js/vue/src/main.js +13 -0
- package/dist/templates/ts/alpine/snippets/counter.liquid +9 -0
- package/dist/templates/ts/alpine/src/main.ts +22 -0
- package/dist/templates/ts/base/_gitignore +5 -0
- package/dist/templates/ts/base/assets/easel.svg +4 -0
- package/dist/templates/ts/base/config/settings_data.json +12 -0
- package/dist/templates/ts/base/config/settings_schema.json +27 -0
- package/dist/templates/ts/base/layout/theme.liquid +19 -0
- package/dist/templates/ts/base/locales/en.default.json +13 -0
- package/dist/templates/ts/base/locales/en.default.schema.json +21 -0
- package/dist/templates/ts/base/sections/hero.liquid +30 -0
- package/dist/templates/ts/base/sections/main-404.liquid +11 -0
- package/dist/templates/ts/base/snippets/counter.liquid +9 -0
- package/dist/templates/ts/base/src/main.ts +29 -0
- package/dist/templates/ts/base/src/sections.ts +47 -0
- package/dist/templates/ts/base/src/style.css +61 -0
- package/dist/templates/ts/base/templates/404.json +9 -0
- package/dist/templates/ts/base/templates/index.json +9 -0
- package/dist/templates/ts/react/src/main.tsx +40 -0
- package/dist/templates/ts/vue/src/Counter.vue +18 -0
- package/dist/templates/ts/vue/src/main.ts +14 -0
- package/package.json +43 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="{{ request.locale.iso_code }}">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<link rel="canonical" href="{{ canonical_url | escape }}">
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="{{ 'easel.svg' | asset_url }}">
|
|
8
|
+
<title>{{ page_title | escape }}{% unless page_title contains shop.name %} — {{ shop.name | escape }}{% endunless %}</title>
|
|
9
|
+
<style>
|
|
10
|
+
:root { --theme-background: {{ settings.background }}; --theme-foreground: {{ settings.foreground }}; }
|
|
11
|
+
</style>
|
|
12
|
+
{% render 'easel-assets' %}
|
|
13
|
+
{{ content_for_header }}
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<a class="skip-link" href="#MainContent">{{ 'accessibility.skip_to_content' | t }}</a>
|
|
17
|
+
<main id="MainContent" tabindex="-1">{{ content_for_layout }}</main>
|
|
18
|
+
</body>
|
|
19
|
+
</html>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"general": {
|
|
3
|
+
"not_found_name": "404 page"
|
|
4
|
+
},
|
|
5
|
+
"sections": {
|
|
6
|
+
"hero": {
|
|
7
|
+
"name": "Hero",
|
|
8
|
+
"heading": "Heading",
|
|
9
|
+
"default_heading": "__THEME_NAME__",
|
|
10
|
+
"text": "Text",
|
|
11
|
+
"default_text": "<p>Build the storefront experience here.</p>"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"settings": {
|
|
15
|
+
"colors": {
|
|
16
|
+
"name": "Colors",
|
|
17
|
+
"background": "Background",
|
|
18
|
+
"foreground": "Foreground"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<section class="hero">
|
|
2
|
+
<h1>{{ section.settings.heading | escape }}</h1>
|
|
3
|
+
<div>{{ section.settings.text }}</div>
|
|
4
|
+
{% render 'counter' %}
|
|
5
|
+
</section>
|
|
6
|
+
|
|
7
|
+
{% schema %}
|
|
8
|
+
{
|
|
9
|
+
"name": "t:sections.hero.name",
|
|
10
|
+
"settings": [
|
|
11
|
+
{
|
|
12
|
+
"type": "text",
|
|
13
|
+
"id": "heading",
|
|
14
|
+
"label": "t:sections.hero.heading",
|
|
15
|
+
"default": "t:sections.hero.default_heading"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"type": "richtext",
|
|
19
|
+
"id": "text",
|
|
20
|
+
"label": "t:sections.hero.text",
|
|
21
|
+
"default": "t:sections.hero.default_text"
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"presets": [
|
|
25
|
+
{
|
|
26
|
+
"name": "t:sections.hero.name"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
{% endschema %}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{% doc %}
|
|
2
|
+
Counter labels are provided by Liquid so client-side components share the theme's translations.
|
|
3
|
+
{% enddoc %}
|
|
4
|
+
|
|
5
|
+
<div data-easel-counter data-decrease="{{ 'general.counter.decrease' | t | escape }}" data-increase="{{ 'general.counter.increase' | t | escape }}">
|
|
6
|
+
<button type="button" data-decrease-button aria-label="{{ 'general.counter.decrease' | t | escape }}" disabled>−</button>
|
|
7
|
+
<output>0</output>
|
|
8
|
+
<button type="button" data-increase-button aria-label="{{ 'general.counter.increase' | t | escape }}" disabled>+</button>
|
|
9
|
+
</div>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { mountSections } from './sections';
|
|
2
|
+
const dispose = mountSections('[data-easel-counter]', (element) => {
|
|
3
|
+
const decrease = element.querySelector('[data-decrease-button]');
|
|
4
|
+
const increase = element.querySelector('[data-increase-button]');
|
|
5
|
+
const output = element.querySelector('output');
|
|
6
|
+
if (!decrease || !increase || !output)
|
|
7
|
+
return () => { };
|
|
8
|
+
let count = 0;
|
|
9
|
+
const controller = new AbortController();
|
|
10
|
+
decrease.disabled = false;
|
|
11
|
+
increase.disabled = false;
|
|
12
|
+
decrease.addEventListener('click', () => {
|
|
13
|
+
output.value = String(--count);
|
|
14
|
+
}, { signal: controller.signal });
|
|
15
|
+
increase.addEventListener('click', () => {
|
|
16
|
+
output.value = String(++count);
|
|
17
|
+
}, { signal: controller.signal });
|
|
18
|
+
return () => controller.abort();
|
|
19
|
+
});
|
|
20
|
+
if (import.meta.hot)
|
|
21
|
+
import.meta.hot.dispose(dispose);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export function mountSections(selector, initialize) {
|
|
2
|
+
const cleanups = new Map();
|
|
3
|
+
function mount(root) {
|
|
4
|
+
const elements = [...root.querySelectorAll(selector)];
|
|
5
|
+
if (root instanceof HTMLElement && root.matches(selector))
|
|
6
|
+
elements.unshift(root);
|
|
7
|
+
for (const element of elements) {
|
|
8
|
+
if (!(element instanceof HTMLElement) || cleanups.has(element))
|
|
9
|
+
continue;
|
|
10
|
+
const fallback = element.innerHTML;
|
|
11
|
+
const dispose = initialize(element);
|
|
12
|
+
cleanups.set(element, () => {
|
|
13
|
+
dispose();
|
|
14
|
+
element.innerHTML = fallback;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function unmount(root) {
|
|
19
|
+
for (const [element, dispose] of cleanups) {
|
|
20
|
+
if (!root.contains(element))
|
|
21
|
+
continue;
|
|
22
|
+
dispose();
|
|
23
|
+
cleanups.delete(element);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function handleSectionLoad(event) {
|
|
27
|
+
if (event.target instanceof HTMLElement)
|
|
28
|
+
mount(event.target);
|
|
29
|
+
}
|
|
30
|
+
function handleSectionUnload(event) {
|
|
31
|
+
if (event.target instanceof HTMLElement)
|
|
32
|
+
unmount(event.target);
|
|
33
|
+
}
|
|
34
|
+
mount(document);
|
|
35
|
+
document.addEventListener('shopify:section:load', handleSectionLoad);
|
|
36
|
+
document.addEventListener('shopify:section:unload', handleSectionUnload);
|
|
37
|
+
return () => {
|
|
38
|
+
document.removeEventListener('shopify:section:load', handleSectionLoad);
|
|
39
|
+
document.removeEventListener('shopify:section:unload', handleSectionUnload);
|
|
40
|
+
for (const dispose of cleanups.values())
|
|
41
|
+
dispose();
|
|
42
|
+
cleanups.clear();
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color: var(--theme-foreground);
|
|
3
|
+
background: var(--theme-background);
|
|
4
|
+
font-family: system-ui, sans-serif;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
* {
|
|
8
|
+
box-sizing: border-box;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
body {
|
|
12
|
+
margin: 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
button {
|
|
16
|
+
min-width: 2.75rem;
|
|
17
|
+
min-height: 2.75rem;
|
|
18
|
+
border: 1px solid currentColor;
|
|
19
|
+
background: transparent;
|
|
20
|
+
color: inherit;
|
|
21
|
+
font: inherit;
|
|
22
|
+
cursor: pointer;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
button:disabled {
|
|
26
|
+
cursor: default;
|
|
27
|
+
opacity: 0.5;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
:focus-visible {
|
|
31
|
+
outline: 2px solid currentColor;
|
|
32
|
+
outline-offset: 4px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.counter,
|
|
36
|
+
[data-easel-counter] {
|
|
37
|
+
display: flex;
|
|
38
|
+
align-items: center;
|
|
39
|
+
gap: 1rem;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
output {
|
|
43
|
+
min-width: 2ch;
|
|
44
|
+
text-align: center;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.skip-link {
|
|
48
|
+
position: absolute;
|
|
49
|
+
transform: translateY(-200%);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.skip-link:focus {
|
|
53
|
+
transform: translateY(0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.hero,
|
|
57
|
+
.not-found {
|
|
58
|
+
margin-inline: auto;
|
|
59
|
+
max-width: 64rem;
|
|
60
|
+
padding: 4rem 1.5rem;
|
|
61
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import '@vitejs/plugin-react/preamble';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { createRoot } from 'react-dom/client';
|
|
4
|
+
import { mountSections } from './sections';
|
|
5
|
+
function Counter({ decrease, increase }) {
|
|
6
|
+
const [count, setCount] = useState(0);
|
|
7
|
+
return (<div className="counter">
|
|
8
|
+
<button type="button" aria-label={decrease} onClick={() => setCount((value) => value - 1)}>
|
|
9
|
+
−
|
|
10
|
+
</button>
|
|
11
|
+
<output>{count}</output>
|
|
12
|
+
<button type="button" aria-label={increase} onClick={() => setCount((value) => value + 1)}>
|
|
13
|
+
+
|
|
14
|
+
</button>
|
|
15
|
+
</div>);
|
|
16
|
+
}
|
|
17
|
+
const dispose = mountSections('[data-easel-counter]', (element) => {
|
|
18
|
+
const root = createRoot(element);
|
|
19
|
+
root.render(<Counter decrease={element.dataset.decrease ?? ''} increase={element.dataset.increase ?? ''}/>);
|
|
20
|
+
return () => root.unmount();
|
|
21
|
+
});
|
|
22
|
+
if (import.meta.hot)
|
|
23
|
+
import.meta.hot.dispose(dispose);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import {ref} from 'vue';
|
|
3
|
+
|
|
4
|
+
defineProps({
|
|
5
|
+
decrease: {type: String, required: true},
|
|
6
|
+
increase: {type: String, required: true},
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const count = ref(0);
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<div class="counter">
|
|
14
|
+
<button type="button" :aria-label="decrease" @click="count -= 1">−</button>
|
|
15
|
+
<output>{{ count }}</output>
|
|
16
|
+
<button type="button" :aria-label="increase" @click="count += 1">+</button>
|
|
17
|
+
</div>
|
|
18
|
+
</template>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createApp } from 'vue';
|
|
2
|
+
import Counter from './Counter.vue';
|
|
3
|
+
import { mountSections } from './sections';
|
|
4
|
+
const dispose = mountSections('[data-easel-counter]', (element) => {
|
|
5
|
+
const app = createApp(Counter, {
|
|
6
|
+
decrease: element.dataset.decrease ?? '',
|
|
7
|
+
increase: element.dataset.increase ?? '',
|
|
8
|
+
});
|
|
9
|
+
app.mount(element);
|
|
10
|
+
return () => app.unmount();
|
|
11
|
+
});
|
|
12
|
+
if (import.meta.hot)
|
|
13
|
+
import.meta.hot.dispose(dispose);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{% doc %}
|
|
2
|
+
Disabled fallback buttons become interactive when Alpine initializes this section.
|
|
3
|
+
{% enddoc %}
|
|
4
|
+
|
|
5
|
+
<div class="counter" x-data="easelCounter">
|
|
6
|
+
<button type="button" aria-label="{{ 'general.counter.decrease' | t | escape }}" disabled :disabled="false" @click="count -= 1">−</button>
|
|
7
|
+
<output x-text="count">0</output>
|
|
8
|
+
<button type="button" aria-label="{{ 'general.counter.increase' | t | escape }}" disabled :disabled="false" @click="count += 1">+</button>
|
|
9
|
+
</div>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import Alpine from 'alpinejs';
|
|
2
|
+
|
|
3
|
+
Alpine.data('easelCounter', () => ({count: 0}));
|
|
4
|
+
Alpine.start();
|
|
5
|
+
|
|
6
|
+
function handleSectionLoad(event: Event): void {
|
|
7
|
+
if (event.target instanceof HTMLElement) Alpine.initTree(event.target);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function handleSectionUnload(event: Event): void {
|
|
11
|
+
if (event.target instanceof HTMLElement) Alpine.destroyTree(event.target);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
document.addEventListener('shopify:section:load', handleSectionLoad);
|
|
15
|
+
document.addEventListener('shopify:section:unload', handleSectionUnload);
|
|
16
|
+
|
|
17
|
+
if (import.meta.hot) {
|
|
18
|
+
import.meta.hot.dispose(() => {
|
|
19
|
+
document.removeEventListener('shopify:section:load', handleSectionLoad);
|
|
20
|
+
document.removeEventListener('shopify:section:unload', handleSectionUnload);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
|
2
|
+
<rect width="64" height="64" rx="16" fill="#171717"/>
|
|
3
|
+
<path d="M32 9v8M23 41l-6 14m24-14 6 14M14 41h36M20 17h24v18H20z" fill="none" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"name": "theme_info",
|
|
4
|
+
"theme_name": "__THEME_NAME__",
|
|
5
|
+
"theme_version": "0.1.0",
|
|
6
|
+
"theme_author": "Billy Noyes",
|
|
7
|
+
"theme_documentation_url": "https://github.com/BillyNoyes/Easel",
|
|
8
|
+
"theme_support_url": "https://github.com/BillyNoyes/Easel/issues"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"name": "t:settings.colors.name",
|
|
12
|
+
"settings": [
|
|
13
|
+
{
|
|
14
|
+
"type": "color",
|
|
15
|
+
"id": "background",
|
|
16
|
+
"label": "t:settings.colors.background",
|
|
17
|
+
"default": "#f5f2eb"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"type": "color",
|
|
21
|
+
"id": "foreground",
|
|
22
|
+
"label": "t:settings.colors.foreground",
|
|
23
|
+
"default": "#171714"
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="{{ request.locale.iso_code }}">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<link rel="canonical" href="{{ canonical_url | escape }}">
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="{{ 'easel.svg' | asset_url }}">
|
|
8
|
+
<title>{{ page_title | escape }}{% unless page_title contains shop.name %} — {{ shop.name | escape }}{% endunless %}</title>
|
|
9
|
+
<style>
|
|
10
|
+
:root { --theme-background: {{ settings.background }}; --theme-foreground: {{ settings.foreground }}; }
|
|
11
|
+
</style>
|
|
12
|
+
{% render 'easel-assets' %}
|
|
13
|
+
{{ content_for_header }}
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<a class="skip-link" href="#MainContent">{{ 'accessibility.skip_to_content' | t }}</a>
|
|
17
|
+
<main id="MainContent" tabindex="-1">{{ content_for_layout }}</main>
|
|
18
|
+
</body>
|
|
19
|
+
</html>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"general": {
|
|
3
|
+
"not_found_name": "404 page"
|
|
4
|
+
},
|
|
5
|
+
"sections": {
|
|
6
|
+
"hero": {
|
|
7
|
+
"name": "Hero",
|
|
8
|
+
"heading": "Heading",
|
|
9
|
+
"default_heading": "__THEME_NAME__",
|
|
10
|
+
"text": "Text",
|
|
11
|
+
"default_text": "<p>Build the storefront experience here.</p>"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"settings": {
|
|
15
|
+
"colors": {
|
|
16
|
+
"name": "Colors",
|
|
17
|
+
"background": "Background",
|
|
18
|
+
"foreground": "Foreground"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<section class="hero">
|
|
2
|
+
<h1>{{ section.settings.heading | escape }}</h1>
|
|
3
|
+
<div>{{ section.settings.text }}</div>
|
|
4
|
+
{% render 'counter' %}
|
|
5
|
+
</section>
|
|
6
|
+
|
|
7
|
+
{% schema %}
|
|
8
|
+
{
|
|
9
|
+
"name": "t:sections.hero.name",
|
|
10
|
+
"settings": [
|
|
11
|
+
{
|
|
12
|
+
"type": "text",
|
|
13
|
+
"id": "heading",
|
|
14
|
+
"label": "t:sections.hero.heading",
|
|
15
|
+
"default": "t:sections.hero.default_heading"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"type": "richtext",
|
|
19
|
+
"id": "text",
|
|
20
|
+
"label": "t:sections.hero.text",
|
|
21
|
+
"default": "t:sections.hero.default_text"
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"presets": [
|
|
25
|
+
{
|
|
26
|
+
"name": "t:sections.hero.name"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
{% endschema %}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{% doc %}
|
|
2
|
+
Counter labels are provided by Liquid so client-side components share the theme's translations.
|
|
3
|
+
{% enddoc %}
|
|
4
|
+
|
|
5
|
+
<div data-easel-counter data-decrease="{{ 'general.counter.decrease' | t | escape }}" data-increase="{{ 'general.counter.increase' | t | escape }}">
|
|
6
|
+
<button type="button" data-decrease-button aria-label="{{ 'general.counter.decrease' | t | escape }}" disabled>−</button>
|
|
7
|
+
<output>0</output>
|
|
8
|
+
<button type="button" data-increase-button aria-label="{{ 'general.counter.increase' | t | escape }}" disabled>+</button>
|
|
9
|
+
</div>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {mountSections} from './sections';
|
|
2
|
+
|
|
3
|
+
const dispose = mountSections('[data-easel-counter]', (element) => {
|
|
4
|
+
const decrease = element.querySelector<HTMLButtonElement>('[data-decrease-button]');
|
|
5
|
+
const increase = element.querySelector<HTMLButtonElement>('[data-increase-button]');
|
|
6
|
+
const output = element.querySelector('output');
|
|
7
|
+
if (!decrease || !increase || !output) return () => {};
|
|
8
|
+
let count = 0;
|
|
9
|
+
const controller = new AbortController();
|
|
10
|
+
decrease.disabled = false;
|
|
11
|
+
increase.disabled = false;
|
|
12
|
+
decrease.addEventListener(
|
|
13
|
+
'click',
|
|
14
|
+
() => {
|
|
15
|
+
output.value = String(--count);
|
|
16
|
+
},
|
|
17
|
+
{signal: controller.signal},
|
|
18
|
+
);
|
|
19
|
+
increase.addEventListener(
|
|
20
|
+
'click',
|
|
21
|
+
() => {
|
|
22
|
+
output.value = String(++count);
|
|
23
|
+
},
|
|
24
|
+
{signal: controller.signal},
|
|
25
|
+
);
|
|
26
|
+
return () => controller.abort();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (import.meta.hot) import.meta.hot.dispose(dispose);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export function mountSections(
|
|
2
|
+
selector: string,
|
|
3
|
+
initialize: (element: HTMLElement) => () => void,
|
|
4
|
+
): () => void {
|
|
5
|
+
const cleanups = new Map<HTMLElement, () => void>();
|
|
6
|
+
|
|
7
|
+
function mount(root: ParentNode): void {
|
|
8
|
+
const elements = [...root.querySelectorAll(selector)];
|
|
9
|
+
if (root instanceof HTMLElement && root.matches(selector)) elements.unshift(root);
|
|
10
|
+
for (const element of elements) {
|
|
11
|
+
if (!(element instanceof HTMLElement) || cleanups.has(element)) continue;
|
|
12
|
+
const fallback = element.innerHTML;
|
|
13
|
+
const dispose = initialize(element);
|
|
14
|
+
cleanups.set(element, () => {
|
|
15
|
+
dispose();
|
|
16
|
+
element.innerHTML = fallback;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function unmount(root: ParentNode): void {
|
|
22
|
+
for (const [element, dispose] of cleanups) {
|
|
23
|
+
if (!root.contains(element)) continue;
|
|
24
|
+
dispose();
|
|
25
|
+
cleanups.delete(element);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function handleSectionLoad(event: Event): void {
|
|
30
|
+
if (event.target instanceof HTMLElement) mount(event.target);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function handleSectionUnload(event: Event): void {
|
|
34
|
+
if (event.target instanceof HTMLElement) unmount(event.target);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
mount(document);
|
|
38
|
+
document.addEventListener('shopify:section:load', handleSectionLoad);
|
|
39
|
+
document.addEventListener('shopify:section:unload', handleSectionUnload);
|
|
40
|
+
|
|
41
|
+
return () => {
|
|
42
|
+
document.removeEventListener('shopify:section:load', handleSectionLoad);
|
|
43
|
+
document.removeEventListener('shopify:section:unload', handleSectionUnload);
|
|
44
|
+
for (const dispose of cleanups.values()) dispose();
|
|
45
|
+
cleanups.clear();
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color: var(--theme-foreground);
|
|
3
|
+
background: var(--theme-background);
|
|
4
|
+
font-family: system-ui, sans-serif;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
* {
|
|
8
|
+
box-sizing: border-box;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
body {
|
|
12
|
+
margin: 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
button {
|
|
16
|
+
min-width: 2.75rem;
|
|
17
|
+
min-height: 2.75rem;
|
|
18
|
+
border: 1px solid currentColor;
|
|
19
|
+
background: transparent;
|
|
20
|
+
color: inherit;
|
|
21
|
+
font: inherit;
|
|
22
|
+
cursor: pointer;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
button:disabled {
|
|
26
|
+
cursor: default;
|
|
27
|
+
opacity: 0.5;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
:focus-visible {
|
|
31
|
+
outline: 2px solid currentColor;
|
|
32
|
+
outline-offset: 4px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.counter,
|
|
36
|
+
[data-easel-counter] {
|
|
37
|
+
display: flex;
|
|
38
|
+
align-items: center;
|
|
39
|
+
gap: 1rem;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
output {
|
|
43
|
+
min-width: 2ch;
|
|
44
|
+
text-align: center;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.skip-link {
|
|
48
|
+
position: absolute;
|
|
49
|
+
transform: translateY(-200%);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.skip-link:focus {
|
|
53
|
+
transform: translateY(0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.hero,
|
|
57
|
+
.not-found {
|
|
58
|
+
margin-inline: auto;
|
|
59
|
+
max-width: 64rem;
|
|
60
|
+
padding: 4rem 1.5rem;
|
|
61
|
+
}
|