@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 +110 -0
- package/dist/assets/index.js +81 -0
- package/package.json +33 -0
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
|
+
}
|