presi-slides 1.0.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.
@@ -0,0 +1,10 @@
1
+ {
2
+ "creatorIdentifier" : "net.shinyfrog.bear",
3
+ "net.shinyfrog.bear" : {
4
+ "bear-note-unique-identifier" : "F0C44A09-49A8-4C1D-8255-7B75118A4153",
5
+ "version" : "2.0"
6
+ },
7
+ "transient" : false,
8
+ "type" : "net.daringfireball.markdown",
9
+ "version" : 2
10
+ }
@@ -0,0 +1,45 @@
1
+ ---
2
+ title: Sample Presi
3
+ ---
4
+
5
+ # Sample Presi
6
+
7
+ ---
8
+
9
+ # Slide One
10
+
11
+ - This is a simple slide
12
+ - It just has bullets
13
+ - It's boring, but it works.
14
+ - Time for more fun
15
+
16
+ ---
17
+
18
+ # Slide Two
19
+
20
+
21
+
22
+ ![](assets/image%206.png)
23
+
24
+ - This slide has a picture
25
+ - There are also bullets
26
+ - They should be side by side
27
+
28
+ ---
29
+
30
+ # Slide 3
31
+
32
+ ### Some Bullets
33
+
34
+ - Thing 1
35
+ - Thing 2
36
+ - Thing 3
37
+
38
+ ### A Table
39
+
40
+ | Row Number | Thing |
41
+ |------------|----------|
42
+ | 1 | Parsley |
43
+ | 2 | Sage |
44
+ | 3 | Rosemary |
45
+ | 4 | Thyme |
@@ -0,0 +1,3 @@
1
+ {
2
+ "layout": "presentation.njk"
3
+ }
package/src/index.js ADDED
@@ -0,0 +1,10 @@
1
+ import { define } from "@unbndl/html";
2
+ import { PresiElement, SlideElement, FooterElement, scaleSlides } from "./slides.js";
3
+
4
+ define({
5
+ "slide-presi": PresiElement,
6
+ "slide-view": SlideElement,
7
+ "slide-footer": FooterElement
8
+ });
9
+
10
+ scaleSlides();
@@ -0,0 +1,109 @@
1
+ import { css, html, shadow } from "@unbndl/html";
2
+
3
+ export class FooterElement extends HTMLElement {
4
+ static template = html`
5
+ <template>
6
+ <footer>
7
+ <fieldset name="overlay">
8
+ <label>
9
+ <input type="radio" name="overlay" value="pointer" checked>
10
+ <span>Select</span>
11
+ </label>
12
+ <label>
13
+ <input type="radio" name="overlay" value="brush">
14
+ <span>Brush</span>
15
+ </label>
16
+ <label>
17
+ <input type="radio" name="overlay" value="eraser">
18
+ <span>Eraser</span>
19
+ </label>
20
+ <label>
21
+ <input type="radio" name="overlay" value="hidden">
22
+ <span>Hide</span>
23
+ </label>
24
+ </fieldset>
25
+ <span>
26
+ <input name="slide" type="number" min="1" value="1">
27
+ /
28
+ <slot name="count">N</span>
29
+ </span>
30
+ </footer>
31
+ </template>
32
+ `;
33
+
34
+ static styles = css`
35
+ footer {
36
+ display: flex;
37
+ justify-content: space-between;
38
+ color: var(--color-text-inverted);
39
+ }
40
+ span {
41
+ background: rgb(0 0 0 / 0.8);
42
+ border-radius: var(--size-spacing-small);
43
+ padding: var(--size-spacing-small);
44
+ }
45
+ input,
46
+ select {
47
+ font: inherit;
48
+ color: inherit;
49
+ line-height: inherit;
50
+ border: none;
51
+ border-radius: 1px;
52
+ background: transparent;
53
+ }
54
+ input[type="number"] {
55
+ width: 3em;
56
+ }
57
+ `;
58
+
59
+ slideControl = null;
60
+ overlayControl = null;
61
+
62
+ constructor() {
63
+ super();
64
+
65
+ shadow(this)
66
+ .template(FooterElement.template)
67
+ .styles(FooterElement.styles)
68
+ .delegate('input[name="slide"]', {
69
+ input: (ev) =>
70
+ this.relay("slide:input", { slide: ev.target.value })
71
+ })
72
+ .delegate('input[name="overlay"]', {
73
+ change: (ev) =>
74
+ this.relay("slide:overlay", { overlay: ev.target.value })
75
+ });
76
+
77
+ this.slideControl = this.shadowRoot.querySelector(
78
+ 'input[name="slide"]'
79
+ );
80
+ this.overlayControl = this.shadowRoot.querySelector(
81
+ 'fieldset[name="overlay"]'
82
+ );
83
+
84
+ }
85
+
86
+ static observedAttributes = ["slide", "overlay", "color"];
87
+
88
+ attributeChangedCallback(name, _, newValue) {
89
+ switch (name) {
90
+ case "slide":
91
+ this.slideControl.value = newValue;
92
+ break;
93
+ case "overlay": {
94
+ const input = this.overlayControl.querySelector(`input[value="${newValue}"]`);
95
+ if (input) input.checked = true;
96
+ break;
97
+ }
98
+ }
99
+ }
100
+
101
+ relay(type, detail) {
102
+ const event = new CustomEvent(type, {
103
+ bubbles: true,
104
+ composed: true,
105
+ detail
106
+ });
107
+ this.dispatchEvent(event);
108
+ }
109
+ }
@@ -0,0 +1,120 @@
1
+ import { html } from "@unbndl/html";
2
+
3
+ const MIN_INTERSECTION = 0.4;
4
+ const MAX_INTERSECTION = 0.8;
5
+
6
+ export class PresiElement extends HTMLElement {
7
+ VIEW_ELEMENT = "slide-view";
8
+ FOOTER_ELEMENT = "slide-footer";
9
+
10
+ controls = null;
11
+ active = null;
12
+
13
+ observer = new IntersectionObserver(
14
+ (entries) => {
15
+ let maxIntersection = 0;
16
+ let maxElement = null;
17
+ entries.forEach((entry) => {
18
+ if (entry.isIntersecting) {
19
+ const element = entry.target;
20
+ //console.log("Intersection:", element, entry.intersectionRatio);
21
+ if (entry.intersectionRatio < MIN_INTERSECTION) {
22
+ element.removeAttribute("active");
23
+ } else {
24
+ if (entry.intersectionRatio > maxIntersection) {
25
+ maxIntersection = entry.intersectionRatio;
26
+ maxElement = element;
27
+ }
28
+ }
29
+ }
30
+ });
31
+
32
+ if (maxElement && maxElement !== this.active) {
33
+ this.setActiveSlide(maxElement);
34
+ // this.gotoSlide(maxElement);
35
+ }
36
+ },
37
+ {
38
+ threshold: [0, MIN_INTERSECTION, MAX_INTERSECTION, 1],
39
+ delay: 300
40
+ }
41
+ );
42
+
43
+ constructor() {
44
+ super();
45
+ this.addEventListener("slide:input", (ev) => {
46
+ const { slide } = ev.detail;
47
+ const element = this.slide(slide);
48
+ // console.log("Change Slide:", slide, element);
49
+ this.setActiveSlide(element);
50
+ this.jumpToSlide(element);
51
+ });
52
+ this.addEventListener("slide:overlay", (ev) => {
53
+ this.manageOverlay(ev.detail);
54
+ });
55
+ }
56
+
57
+ slide(n) {
58
+ const slides = Array.from(
59
+ this.querySelectorAll(this.VIEW_ELEMENT)
60
+ );
61
+ return slides[n - 1];
62
+ }
63
+
64
+ connectedCallback() {
65
+ const slides = Array.from(
66
+ this.querySelectorAll(this.VIEW_ELEMENT)
67
+ );
68
+ this.controls = this.querySelector(this.FOOTER_ELEMENT);
69
+ slides.forEach((target) => this.observer.observe(target));
70
+ }
71
+
72
+ gotoSlide(element) {
73
+ console.log("Going to slide", element);
74
+ if (element) {
75
+ element.scrollIntoView({
76
+ behavior: "smooth",
77
+ block: "center",
78
+ inline: "nearest"
79
+ });
80
+ }
81
+ }
82
+
83
+ jumpToSlide(element) {
84
+ if (element) {
85
+ element.scrollIntoView({
86
+ block: "center",
87
+ inline: "nearest"
88
+ });
89
+ }
90
+ }
91
+
92
+ setActiveSlide(element) {
93
+ //console.log("Activating slide", element);
94
+ this.active = element;
95
+ if (this.active) {
96
+ this.active.setAttribute("active", "active");
97
+ const slides = Array.from(
98
+ this.querySelectorAll(this.VIEW_ELEMENT)
99
+ );
100
+ const index = slides.findIndex(
101
+ (slide) => slide === element
102
+ );
103
+ if (this.controls) {
104
+ this.controls.setAttribute("slide", index + 1);
105
+ this.controls.setAttribute(
106
+ "overlay",
107
+ element.getAttribute("overlay") || "pointer"
108
+ );
109
+ }
110
+ }
111
+ }
112
+
113
+ manageOverlay(options) {
114
+ const { overlay } = options || {};
115
+ console.log("Overlay options", options);
116
+ if (this.active && overlay) {
117
+ this.active.setAttribute("overlay", overlay);
118
+ }
119
+ }
120
+ }
@@ -0,0 +1,142 @@
1
+ import { html, css, shadow } from "@unbndl/html";
2
+ import Konva from 'konva';
3
+
4
+
5
+ export class SlideElement extends HTMLElement {
6
+ static template = html`
7
+ <template>
8
+ <slot></slot>
9
+ <div id="overlay"></div>
10
+ </template>
11
+ `;
12
+
13
+ static styles = css`
14
+ :host {
15
+ --slide-pointer-events: none;
16
+ --slide-opacity: 1;
17
+ --slide-overlay-visibility: hidden;
18
+ /*position: relative;*/
19
+ }
20
+ @media screen {
21
+ :host {
22
+ --slide-opacity: 1; // make it 0.2 for fading effect on scroll;
23
+ --slide-overlay-visibility: visible;
24
+ }
25
+ :host([active]) {
26
+ --slide-opacity: 1;
27
+ }
28
+ :host([overlay]) {
29
+ cursor: pointer;
30
+ }
31
+ :host([overlay="hidden"]) {
32
+ --slide-overlay-visibility: hidden;
33
+ }
34
+ :host([overlay="brush"]) {
35
+ cursor: url("/includes/pencil.png") 0 100, crosshair;
36
+ --slide-pointer-events: auto;
37
+ }
38
+ :host([overlay="eraser"]) {
39
+ cursor: not-allowed;
40
+ --slide-pointer-events: auto;
41
+ }
42
+ #overlay {
43
+ position: absolute;
44
+ inset: 0;
45
+ pointer-events: var(--slide-pointer-events);
46
+ visibility: var(--slide-overlay-visibility);
47
+ }
48
+ ::slotted(*) {
49
+ opacity: var(--slide-opacity);
50
+ transition: opacity 1s;
51
+ }
52
+ }
53
+ `;
54
+
55
+ // initialize the Konva overlay
56
+ isPaint = false;
57
+ overlayMode = "pointer";
58
+ overlayColor = "red";
59
+ stage = null;
60
+
61
+ constructor() {
62
+ super();
63
+
64
+ shadow(this)
65
+ .template(SlideElement.template)
66
+ .styles(SlideElement.styles);
67
+ }
68
+
69
+ static observedAttributes = ["active", "overlay"];
70
+
71
+ attributeChangedCallback(name, _, newValue) {
72
+ switch (name) {
73
+ case "overlay":
74
+ this.overlayMode = newValue;
75
+ this.createOverlay();
76
+ break;
77
+ case "color":
78
+ this.overlayColor = newValue;
79
+ break;
80
+ }
81
+ }
82
+
83
+ disconnectedCallback() {
84
+ if (this.stage) delete this.stage;
85
+ }
86
+
87
+ createOverlay() {
88
+ if (this.stage) return;
89
+
90
+ const container = this.shadowRoot.getElementById("overlay");
91
+
92
+ console.log("Creating overlay if needed", this.stage);
93
+
94
+ this.stage = new Konva.Stage({
95
+ container,
96
+ width: container?.clientWidth,
97
+ height: container?.clientHeight
98
+ });
99
+
100
+ this.layer = new Konva.Layer();
101
+ this.stage.add(this.layer);
102
+
103
+ this.stage.on("mousedown touchstart", (e) => {
104
+ this.isPaint = true;
105
+ const pos = this.stage.getPointerPosition();
106
+ console.log("touchstart", pos);
107
+ this.lastLine = new Konva.Line({
108
+ stroke: this.overlayColor,
109
+ strokeWidth: this.overlayMode === "eraser" ? 10 : 4,
110
+ globalCompositeOperation:
111
+ this.overlayMode === "brush"
112
+ ? "source-over"
113
+ : "destination-out",
114
+ // round cap for smoother lines
115
+ lineCap: "round",
116
+ lineJoin: "round",
117
+ // add point twice, so we have some drawings even on a simple click
118
+ points: [pos.x, pos.y, pos.x, pos.y]
119
+ });
120
+ this.layer.add(this.lastLine);
121
+ });
122
+
123
+ this.stage.on("mouseup touchend", () => {
124
+ this.isPaint = false;
125
+ console.log("touchend");
126
+ });
127
+
128
+ // and core function - drawing
129
+ this.stage.on("mousemove touchmove", (e) => {
130
+ if (!this.isPaint) return;
131
+
132
+ // prevent scrolling on touch devices
133
+ e.evt.preventDefault();
134
+
135
+ const pos = this.stage.getPointerPosition();
136
+ const newPoints = this.lastLine
137
+ .points()
138
+ .concat([pos.x, pos.y]);
139
+ this.lastLine.points(newPoints);
140
+ });
141
+ }
142
+ }
package/src/slides.js ADDED
@@ -0,0 +1,18 @@
1
+ export * from "./slide-footer.js";
2
+ export * from "./slide-view.js";
3
+ export * from "./slide-presi.js";
4
+
5
+ export function scaleSlides() {
6
+ window.addEventListener("resize", computeSlideScaling);
7
+ computeSlideScaling();
8
+ }
9
+
10
+ function computeSlideScaling() {
11
+ const body = document.body;
12
+ const vw = document.documentElement.clientWidth;
13
+ const vh = document.documentElement.clientHeight;
14
+ body.classList.add("presentation-scaled");
15
+ body.style.setProperty("--viewport-width", vw.toString());
16
+ body.style.setProperty("--viewport-height", vh.toString());
17
+ console.log("Rescaled", vw, vh);
18
+ }
@@ -0,0 +1,75 @@
1
+ import path from "node:path";
2
+ import { JSDOM } from "jsdom";
3
+
4
+ const PRESI_COMPONENT = "slide-presi";
5
+ const FOOTER_COMPONENT = "slide-footer";
6
+ const SLIDE_COMPONENT = "slide-view";
7
+
8
+ // JSDOM cannot parse modern CSS stylesheets.
9
+ // Workaround from: https://github.com/jsdom/jsdom/issues/3236#issuecomment-1699777509
10
+
11
+ const originalConsoleError = console.error;
12
+ console.error = (message, ...optionalParams) => {
13
+ if (message.includes("Could not parse CSS stylesheet")) {
14
+ return;
15
+ }
16
+ originalConsoleError(message, ...optionalParams);
17
+ };
18
+
19
+ export default function (content) {
20
+ const { inputPath, outputPath } = this.page;
21
+ const inputDir = path.dirname(inputPath);
22
+ const inputFile = path.basename(inputPath);
23
+ const outputDir = path.dirname(outputPath);
24
+ const outputBase = path.dirname(outputDir);
25
+ const outputFile = path.basename(outputPath);
26
+
27
+ if (
28
+ inputPath &&
29
+ inputDir.endsWith(".textbundle") &&
30
+ inputFile === "text.md"
31
+ ) {
32
+ console.log("Processing slides in...", inputDir);
33
+
34
+ const DOM = new JSDOM(content);
35
+ const { document } = DOM.window;
36
+ const body = document.querySelector("body");
37
+ const children = Array.from(body.children);
38
+ const parts = children.reduce(
39
+ (acc, val) => {
40
+ let current = acc.length ? acc[acc.length - 1] : [];
41
+ let previous = acc;
42
+ if (val.tagName === "HR") {
43
+ current = [];
44
+ } else {
45
+ current = current.concat([val]);
46
+ previous = previous.slice(0, -1);
47
+ }
48
+ return previous.concat([current]);
49
+ },
50
+ [[]]
51
+ );
52
+ const count = parts.length;
53
+ const slides = parts.map((children, i) => {
54
+ const section = document.createElement(SLIDE_COMPONENT);
55
+ section.replaceChildren(...children);
56
+ return section;
57
+ });
58
+
59
+ const footer = document.createElement(FOOTER_COMPONENT);
60
+ const countSpan = document.createElement('span');
61
+ countSpan.textContent = count.toString();
62
+ countSpan.setAttribute("slot", "count");
63
+ footer.replaceChildren(countSpan);
64
+
65
+
66
+ const presi = document.createElement(PRESI_COMPONENT);
67
+ presi.replaceChildren(...slides, footer);
68
+
69
+ body.replaceChildren(presi);
70
+
71
+ return `<!doctype html>${document.documentElement.outerHTML}`;
72
+ }
73
+
74
+ return content;
75
+ }
package/vite.config.js ADDED
@@ -0,0 +1,9 @@
1
+ export default {
2
+ build: {
3
+ lib: {
4
+ entry: ['src/index.js'],
5
+ fileName: "presi-slides",
6
+ formats: ['es', 'cjs']
7
+ }
8
+ }
9
+ }