@jschofield/scroll-explain 0.2.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/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # @jschofield/scroll-explain
2
+
3
+ A scroll-driven code walkthrough web component built with [Lit](https://lit.dev/). Creates a two-column layout where prose explanations on the left drive line highlighting in a sticky code block on the right. Pairs with [`@jschofield/code-highlight`](https://www.npmjs.com/package/@jschofield/code-highlight).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @jschofield/scroll-explain @jschofield/code-highlight
9
+ ```
10
+
11
+ ### Peer dependencies
12
+
13
+ This component requires the following peer dependency:
14
+
15
+ ```bash
16
+ npm install lit
17
+ ```
18
+
19
+ | Peer | Version |
20
+ |---|---|
21
+ | `lit` | `^3.0.0` |
22
+
23
+ ## Usage
24
+
25
+ ```html
26
+ <script type="module">
27
+ import '@jschofield/scroll-explain';
28
+ import '@jschofield/code-highlight';
29
+ </script>
30
+
31
+ <scroll-explain>
32
+ <div slot="prose">
33
+ <p>Here's how this function works:</p>
34
+
35
+ <explain-section lines="1-3">
36
+ <p>First, we set up the variables...</p>
37
+ </explain-section>
38
+
39
+ <explain-section lines="5-8">
40
+ <p>Then we process each item in the loop...</p>
41
+ </explain-section>
42
+
43
+ <explain-section lines="10-12">
44
+ <p>Finally, we return the result.</p>
45
+ </explain-section>
46
+ </div>
47
+
48
+ <code-highlight lang="javascript" slot="code" line-numbers>
49
+ <template>
50
+ <script>
51
+ const items = getData();
52
+ const results = [];
53
+ let count = 0;
54
+
55
+ for (const item of items) {
56
+ const processed = transform(item);
57
+ results.push(processed);
58
+ count++;
59
+ }
60
+
61
+ console.log(`Processed ${count} items`);
62
+ return results;
63
+ </script>
64
+ </template>
65
+ </code-highlight>
66
+ </scroll-explain>
67
+ ```
68
+
69
+ ### Without a bundler
70
+
71
+ If you're not using a bundler, provide peer dependencies via an import map:
72
+
73
+ ```html
74
+ <script type="importmap">
75
+ {
76
+ "imports": {
77
+ "lit": "https://esm.run/lit",
78
+ "lit/": "https://esm.run/lit/"
79
+ }
80
+ }
81
+ </script>
82
+ <script type="module" src="https://esm.run/@jschofield/scroll-explain"></script>
83
+ <script type="module" src="https://esm.run/@jschofield/code-highlight"></script>
84
+ ```
85
+
86
+ ## Slots
87
+
88
+ | Slot | Content |
89
+ |---|---|
90
+ | `prose` | Container with text and `<explain-section>` elements |
91
+ | `code` | A `<code-highlight>` element with `line-numbers` |
92
+
93
+ ## `<explain-section>` attributes
94
+
95
+ | Attribute | Type | Default | Description |
96
+ |---|---|---|---|
97
+ | `lines` | `string` | `""` | Line spec to highlight when this section is visible (e.g., `"1-3,5,7-9"`) |
98
+
99
+ ## How it works
100
+
101
+ As the user scrolls, an IntersectionObserver detects which `<explain-section>` is visible and tells the `<code-highlight>` element to activate the corresponding lines. The code block smoothly scrolls to keep the active lines centered.
102
+
103
+ ## Responsive behavior
104
+
105
+ - **Desktop (>900px):** Two-column grid. Code sticks to the right side.
106
+ - **Mobile (<900px):** Code sticks to the top of the viewport. Prose scrolls below.
107
+
108
+ ## License
109
+
110
+ MIT
@@ -0,0 +1,81 @@
1
+ import{LitElement as e,css as t,html as n}from"lit";import{customElement as r,property as i}from"lit/decorators.js";function a(e,t,n,r){var i=arguments.length,a=i<3?t:r===null?r=Object.getOwnPropertyDescriptor(t,n):r,o;if(typeof Reflect==`object`&&typeof Reflect.decorate==`function`)a=Reflect.decorate(e,t,n,r);else for(var s=e.length-1;s>=0;s--)(o=e[s])&&(a=(i<3?o(a):i>3?o(t,n,a):o(t,n))||a);return i>3&&a&&Object.defineProperty(t,n,a),a}var o=class extends e{constructor(...e){super(...e),this._lastActiveLines=``,this._codeBlock=null,this._sections=[],this._sectionObserver=null,this._activeSections=new Set,this._onResize=()=>{this._setupObserver()},this._scrollAnimation=null}firstUpdated(){this._codeBlock=this.querySelector(`code-highlight`),this._sections=Array.from(this.querySelectorAll(`explain-section`)),!(!this._codeBlock||this._sections.length===0)&&(this._setupObserver(),window.addEventListener(`resize`,this._onResize,{passive:!0}))}_setupObserver(){this._sectionObserver?.disconnect(),this._activeSections.clear();let e=window.matchMedia(`(max-width: 900px)`).matches?`-65% 0px -10% 0px`:`-40% 0px -35% 0px`;this._sectionObserver=new IntersectionObserver(e=>{for(let t of e){let e=t.target;t.isIntersecting?this._activeSections.add(e):this._activeSections.delete(e)}for(let e of this._sections)if(this._activeSections.has(e)){this._setActive(e);return}this._activeSections.size===0&&(this._lastActiveLines=``,this._codeBlock.clearActiveLines())},{rootMargin:e,threshold:0});for(let e of this._sections)this._sectionObserver.observe(e)}_setActive(e){let t=e.lines;!t||t===this._lastActiveLines||(this._lastActiveLines=t,this._codeBlock.setActiveLines(t),this._scrollToActiveLine())}async _scrollToActiveLine(){let e=this._codeBlock;await e.updateComplete;let t=e.getFirstActiveLine();if(!t)return;let n=e.getScrollContainer();if(!n)return;let r=n.getBoundingClientRect(),i=t.getBoundingClientRect(),a=Math.max(0,i.top-r.top+n.scrollTop-n.clientHeight/2+i.height/2);this._smoothScroll(n,a,1e3)}_smoothScroll(e,t,n){this._scrollAnimation&&cancelAnimationFrame(this._scrollAnimation);let r=e.scrollTop,i=t-r;if(Math.abs(i)<1)return;let a=performance.now(),o=t=>{let s=t-a,c=Math.min(s/n,1);e.scrollTop=r+i*(1-(1-c)**3),c<1?this._scrollAnimation=requestAnimationFrame(o):this._scrollAnimation=null};this._scrollAnimation=requestAnimationFrame(o)}disconnectedCallback(){super.disconnectedCallback(),this._sectionObserver?.disconnect(),window.removeEventListener(`resize`,this._onResize)}render(){return n`
2
+ <div class="container">
3
+ <div class="prose">
4
+ <slot name="prose"></slot>
5
+ </div>
6
+ <div class="code">
7
+ <slot name="code"></slot>
8
+ </div>
9
+ </div>
10
+ `}static{this.styles=t`
11
+ :host {
12
+ display: block;
13
+ }
14
+
15
+ *,
16
+ *::before,
17
+ *::after {
18
+ box-sizing: border-box;
19
+ }
20
+
21
+ .container {
22
+ display: grid;
23
+ grid-template-columns: 1fr 1fr;
24
+ gap: 2rem;
25
+ align-items: start;
26
+ }
27
+
28
+ .prose {
29
+ grid-column: 1;
30
+ /* Scroll padding so code sticks before first section activates,
31
+ and last section stays active before code unsticks */
32
+ padding-block: 20vh 40vh;
33
+ }
34
+
35
+ .code {
36
+ grid-column: 2;
37
+ position: sticky;
38
+ top: 10vh;
39
+ }
40
+
41
+ ::slotted([slot="code"]) {
42
+ --code-max-height: 80vh;
43
+ --code-padding-bottom: 50vh;
44
+ }
45
+
46
+ @media (max-width: 900px) {
47
+ .container {
48
+ display: flex;
49
+ flex-direction: column-reverse;
50
+ gap: 0;
51
+ }
52
+
53
+ .prose {
54
+ padding-block: 10vh 20vh;
55
+ padding-inline: 1rem;
56
+ }
57
+
58
+ ::slotted([slot="code"]) {
59
+ --code-max-height: 35vh;
60
+ --code-padding-bottom: 30vh;
61
+ --code-border-radius: 0;
62
+ }
63
+
64
+ .code {
65
+ position: sticky;
66
+ top: 0;
67
+ z-index: 1;
68
+ width: 100%;
69
+ }
70
+ }
71
+ `}};o=a([r(`scroll-explain`)],o);var s=class extends e{constructor(...e){super(...e),this.lines=``}render(){return n`<slot></slot>`}static{this.styles=t`
72
+ :host {
73
+ display: block;
74
+ min-height: 40vh;
75
+ padding: 1rem 0;
76
+ }
77
+
78
+ :host(:first-of-type) {
79
+ padding-top: 0;
80
+ }
81
+ `}};a([i()],s.prototype,`lines`,void 0),s=a([r(`explain-section`)],s);
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@jschofield/scroll-explain",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "main": "./dist/assets/index.js",
6
+ "exports": {
7
+ ".": "./dist/assets/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "description": "Scroll-driven explanation web component built with Lit, uses code-highlight",
13
+ "author": "Jim Schofield",
14
+ "license": "MIT",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/JimSchofield/jschof.dev",
18
+ "directory": "component/scroll-explain"
19
+ },
20
+ "dependencies": {},
21
+ "peerDependencies": {
22
+ "lit": "^3.0.0"
23
+ },
24
+ "devDependencies": {
25
+ "typescript": "^6.0.2",
26
+ "vite": "^8.0.3"
27
+ },
28
+ "scripts": {
29
+ "dev": "vite",
30
+ "build": "tsc && vite build",
31
+ "preview": "vite preview"
32
+ }
33
+ }