@neuromatch/course-kit 0.0.0-stage → 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/dist/assets/cells/cells.css +121 -0
- package/dist/assets/cells/page.css +71 -0
- package/dist/assets/cells/panel.mjs +27 -0
- package/dist/assets/cells/runtime-widget.mjs +4 -0
- package/dist/assets/feedback/widget.mjs +146 -0
- package/dist/assets/scrolly/engine.mjs +140 -0
- package/dist/assets/scrolly/runnable.css +67 -0
- package/dist/assets/scrolly/runnable.mjs +353 -0
- package/dist/assets/scrolly/scrolly.css +173 -0
- package/dist/build-info.json +4 -0
- package/dist/nma.mjs +8285 -0
- package/dist/nma.yml +9 -0
- package/package.json +38 -4
- package/README.md +0 -3
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// GENERATED by npm run build in course-kit/js. Do not edit.
|
|
2
|
+
var h="nma-no-fallback";var m=n=>n.includes(h)?n:`${n.replace(/\s*$/,"")}
|
|
3
|
+
# ${h}
|
|
4
|
+
`;var a=n=>`nma-cell-${n}`;function f(n){return{core:new URL("../thebe-core.min.js",n).href,lite:new URL("../thebe-lite.min.js",n).href}}var b="Starting Python\u2026 the first run takes about 10 seconds",p=()=>!!(window.thebeCore?.api&&window.thebeLite);async function x(n){if(p())return;if(n&&typeof document<"u"){let{core:t,lite:i}=f(n);for(let r of[t,i]){if(document.querySelector(`script[src="${r}"]`))continue;let o=document.createElement("script");o.src=r,document.head.appendChild(o)}}let e=Date.now()+3e4;for(;!p();){if(Date.now()>e)throw new Error("thebe did not load");await new Promise(t=>setTimeout(t,50))}}var c=class{constructor(e){this.path=e,this.cells=null,this.edits={},this.ran=new Set,this.ready=null,this.bundleUrl=null,this.kernel=null,this.queue=Promise.resolve(),this.listeners=new Set,this.outs=new Map,this.cellsReady=new Promise(t=>this._resolveCells=t)}setCells(e){this.cells=e,this._resolveCells(e)}subscribe(e){return this.listeners.add(e),()=>this.listeners.delete(e)}emit(e){for(let t of this.listeners)t(e)}register(e,t){return this.outs.set(e,t),this.ready&&this.ready.nb.getCellById(a(e))?.attachToDOM(t),()=>{this.outs.get(e)===t&&this.outs.delete(e)}}boot(){return this.kernel||=(async()=>{this.emit({kernel:"starting",message:b}),await x(this.bundleUrl);let e=window.thebeCore.api,t=e.makeEvents(),i=e.makeConfiguration({useJupyterLite:!0},t),r=e.makeServer(i);await r.connectToJupyterLiteServer();let o=e.makeRenderMimeRegistry(i.mathjax),l=await this.cellsReady,s=e.setupNotebookFromBlocks(l.map(u=>({id:a(u.index),source:u.code})),i,o),d=await r.startNewSession(o);s.attachSession(d),this.ready={api:e,config:i,server:r,rendermime:o,session:d,nb:s};for(let[u,y]of this.outs)s.getCellById(a(u))?.attachToDOM(y);return this.emit({kernel:"ready"}),this.ready})(),this.kernel.catch(()=>{this.kernel=null}),this.kernel}run(e,t){let i=this.queue.then(async()=>{let r=await this.boot();for(let l of this.cells){if(l.index>=e)break;if(this.ran.has(l.index))continue;let s=await this.exec(r,l.index,this.edits[l.index]??l.code);if(s.error)return{error:s.error,failedIndex:l.index}}let o=await this.exec(r,e,m(t));return{error:o.error,failedIndex:o.error?e:null}});return this.queue=i.catch(()=>{}),i}runAll(){let e=this.queue.then(async()=>{let t=await this.boot();for(let i of this.cells){let r=await this.exec(t,i.index,this.edits[i.index]??i.code);if(r.error)return{error:r.error,failedIndex:i.index}}return{error:null,failedIndex:null}});return this.queue=e.catch(()=>{}),e}async exec(e,t,i){let r=e.nb.getCellById(a(t));this.emit({index:t,state:"running"});let o=await r.execute(i);this.ran.add(t);let l=o==null?{ename:"InternalError"}:o.error?.[0]??null,s=l&&{...l,ename:String(l.ename??"Error").replace(/^<class '(.*)'>$/,"$1")};return this.emit({index:t,state:s?"error":"done",count:r.executionCount,error:s}),{error:s}}async restart(){let e=await this.kernel;await e.server.shutdownAllSessions();let t=await e.server.startNewSession(e.rendermime);e.nb.attachSession(t),e.session=t,this.ran.clear(),this.queue=Promise.resolve(),this.emit({kernel:"ready",restarted:!0})}async dispose(){if(!this.kernel)return;let e=await this.kernel.catch(()=>null);await e?.server.shutdownAllSessions(),e?.server.dispose()}};function w(){let n=location.pathname,e=window.NMA_CELLS||={};return e.runtime?.path!==n&&(e.runtime?.dispose(),e.runtime=new c(n)),e.runtime}function k(n){let e=n.querySelector('link[rel="stylesheet"]')??n.getRootNode().querySelector('link[rel="stylesheet"]');if(!e||document.head.querySelector("link[data-nma-cells]"))return;let t=document.createElement("link");t.rel="stylesheet",t.href=e.href,t.dataset.nmaCells="",document.head.appendChild(t)}function g(n){if((n.cells??[]).length===0||typeof document>"u")return;let t=document.getElementById("nma-run-all-bar");if(!t){t=document.createElement("div"),t.id="nma-run-all-bar",t.className="nma-run-all-bar",t.innerHTML='<button type="button" class="nma-run-all">\u25B6 Run all</button><span class="nma-run-all-status" role="status" aria-live="polite"></span>',document.body.appendChild(t);let r=t.querySelector(".nma-run-all"),o=t.querySelector(".nma-run-all-status"),l=s=>{t._running=s,r.disabled=s,r.textContent=s?"\u23F3 Running\u2026":"\u25B6 Run all"};r.addEventListener("click",async()=>{if(!t._running){l(!0),o.textContent="";try{let s=await t._rt.runAll();o.textContent=s.error?`\u2717 stopped at cell ${s.failedIndex+1}`:"\u2713 ran all cells"}catch(s){o.textContent=`\u2717 ${s?.message??s}`}finally{l(!1)}}})}t._rt=n,t._off?.();let i=t.querySelector(".nma-run-all-status");t._off=n.subscribe(r=>{t._running&&(r.kernel==="starting"?i.textContent="Starting\u2026":typeof r.index=="number"&&r.state==="running"&&(i.textContent=`Running cell ${r.index+1}\u2026`))})}var I={render({model:n,el:e}){k(e);let t=w();t.bundleUrl||=import.meta.url,t.setCells(n.get("cells")),g(t)}};export{I as default};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// Feedback widget for {nma-feedback} (anywidget ESM, loaded by the MyST theme
|
|
2
|
+
// in the browser, no kernel needed). feedback/plugin.mjs fills the model:
|
|
3
|
+
// section_id, url, project, user_key
|
|
4
|
+
//
|
|
5
|
+
// It does what vibecheck's DatatopsContentReviewContainer does in notebooks:
|
|
6
|
+
// 🙂 sends right away, 😐/🙁 ask for a comment first. The request body matches
|
|
7
|
+
// vibecheck's, so site and notebook feedback land in the same datatops project.
|
|
8
|
+
|
|
9
|
+
const CHOICES = [
|
|
10
|
+
{ name: 'happy', emoji: '🙂', color: '#aaffaa', label: 'Happy' },
|
|
11
|
+
{ name: 'medium', emoji: '😐', color: '#dddd77', label: 'Neutral' },
|
|
12
|
+
{ name: 'sad', emoji: '🙁', color: '#ffaaaa', label: 'Unhappy' },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const STYLE = `
|
|
16
|
+
.nma-fb { margin: 0.75rem 0; font-size: 0.95rem; }
|
|
17
|
+
.nma-fb-label { opacity: 0.75; margin: 0 0 0.35rem; }
|
|
18
|
+
.nma-fb-buttons { display: flex; gap: 0.5rem; }
|
|
19
|
+
.nma-fb-buttons button {
|
|
20
|
+
font-size: 1.6rem; line-height: 1; padding: 0.35rem 0.6rem; cursor: pointer;
|
|
21
|
+
border: 2px solid transparent; border-radius: 0.4rem; color: #000;
|
|
22
|
+
}
|
|
23
|
+
.nma-fb-buttons button[aria-pressed="true"] { border-color: #666; }
|
|
24
|
+
.nma-fb-buttons button:disabled { cursor: default; opacity: 0.6; }
|
|
25
|
+
.nma-fb-comment { display: flex; gap: 0.5rem; margin-top: 0.5rem; align-items: stretch; }
|
|
26
|
+
.nma-fb-comment[hidden] { display: none; }
|
|
27
|
+
.nma-fb-comment textarea {
|
|
28
|
+
flex: 1; min-height: 3rem; padding: 0.4rem; font: inherit; color: inherit;
|
|
29
|
+
background: transparent; border: 1px solid #888; border-radius: 0.3rem;
|
|
30
|
+
}
|
|
31
|
+
.nma-fb-comment button {
|
|
32
|
+
padding: 0 1rem; cursor: pointer; font: inherit; color: inherit;
|
|
33
|
+
background: transparent; border: 1px solid #888; border-radius: 0.3rem;
|
|
34
|
+
}
|
|
35
|
+
.nma-fb-status { margin: 0.4rem 0 0; }
|
|
36
|
+
.nma-fb-status.error { color: #c62828; }
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
/** vibecheck's timestamp format: ISO 8601 in UTC without the trailing Z. */
|
|
40
|
+
export function timestamp(date = new Date()) {
|
|
41
|
+
return date.toISOString().replace(/Z$/, '');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** POST one response to datatops. Resolves true on success, false otherwise. */
|
|
45
|
+
export async function send(settings, buttonName, feedback, fetchImpl = globalThis.fetch) {
|
|
46
|
+
const base = settings.url.replace(/\/+$/, '');
|
|
47
|
+
try {
|
|
48
|
+
const response = await fetchImpl(`${base}/api/v1/projects/${encodeURIComponent(settings.project)}`, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: { 'Content-Type': 'application/json', 'X-User-Key': settings.user_key },
|
|
51
|
+
body: JSON.stringify({
|
|
52
|
+
button_name: buttonName,
|
|
53
|
+
feedback,
|
|
54
|
+
section_id: settings.section_id,
|
|
55
|
+
timestamp_utc: timestamp(),
|
|
56
|
+
}),
|
|
57
|
+
});
|
|
58
|
+
if (!response.ok) return false;
|
|
59
|
+
const result = await response.json();
|
|
60
|
+
return result?.status === 'success';
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function element(tag, props = {}, children = []) {
|
|
67
|
+
const node = document.createElement(tag);
|
|
68
|
+
Object.assign(node, props);
|
|
69
|
+
node.append(...children);
|
|
70
|
+
return node;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function render({ model, el }) {
|
|
74
|
+
const settings = Object.fromEntries(
|
|
75
|
+
['section_id', 'url', 'project', 'user_key'].map((key) => [key, model.get(key)]),
|
|
76
|
+
);
|
|
77
|
+
let selected = null;
|
|
78
|
+
|
|
79
|
+
const buttons = CHOICES.map((choice) => {
|
|
80
|
+
const button = element('button', {
|
|
81
|
+
type: 'button',
|
|
82
|
+
textContent: choice.emoji,
|
|
83
|
+
title: choice.label,
|
|
84
|
+
});
|
|
85
|
+
button.style.background = choice.color;
|
|
86
|
+
button.setAttribute('aria-label', choice.label);
|
|
87
|
+
button.setAttribute('aria-pressed', 'false');
|
|
88
|
+
button.addEventListener('click', () => choose(choice.name));
|
|
89
|
+
return button;
|
|
90
|
+
});
|
|
91
|
+
const textarea = element('textarea', { placeholder: 'We want your feedback!', rows: 2 });
|
|
92
|
+
textarea.setAttribute('aria-label', 'Your feedback');
|
|
93
|
+
const submit = element('button', { type: 'button', textContent: 'Submit' });
|
|
94
|
+
const comment = element('div', { className: 'nma-fb-comment', hidden: true }, [textarea, submit]);
|
|
95
|
+
const status = element('p', { className: 'nma-fb-status' });
|
|
96
|
+
status.setAttribute('role', 'status');
|
|
97
|
+
|
|
98
|
+
const root = element('div', { className: 'nma-fb' }, [
|
|
99
|
+
element('style', { textContent: STYLE }),
|
|
100
|
+
element('p', { className: 'nma-fb-label', textContent: 'How did this section go?' }),
|
|
101
|
+
element('div', { className: 'nma-fb-buttons' }, buttons),
|
|
102
|
+
comment,
|
|
103
|
+
status,
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
function setBusy(busy) {
|
|
107
|
+
buttons.forEach((b) => (b.disabled = busy));
|
|
108
|
+
submit.disabled = busy;
|
|
109
|
+
textarea.disabled = busy;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function submitFeedback(buttonName, text) {
|
|
113
|
+
setBusy(true);
|
|
114
|
+
status.className = 'nma-fb-status';
|
|
115
|
+
status.textContent = 'Sending…';
|
|
116
|
+
if (await send(settings, buttonName, text)) {
|
|
117
|
+
comment.hidden = true;
|
|
118
|
+
buttons.forEach((b) => (b.disabled = true));
|
|
119
|
+
status.textContent = 'Thanks for your feedback!';
|
|
120
|
+
} else {
|
|
121
|
+
setBusy(false);
|
|
122
|
+
status.className = 'nma-fb-status error';
|
|
123
|
+
status.textContent = "Couldn't send feedback, please try again.";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function choose(name) {
|
|
128
|
+
selected = name;
|
|
129
|
+
CHOICES.forEach((choice, i) => buttons[i].setAttribute('aria-pressed', String(choice.name === name)));
|
|
130
|
+
if (name === 'happy') {
|
|
131
|
+
comment.hidden = true;
|
|
132
|
+
submitFeedback('happy', '');
|
|
133
|
+
} else {
|
|
134
|
+
comment.hidden = false;
|
|
135
|
+
status.textContent = '';
|
|
136
|
+
textarea.focus();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
submit.addEventListener('click', () => submitFeedback(selected, textarea.value));
|
|
141
|
+
|
|
142
|
+
el.replaceChildren(root);
|
|
143
|
+
return () => el.replaceChildren();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export default { render };
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Scrollytelling engine (one per scrolly page, emitted by scrolly.mjs).
|
|
2
|
+
//
|
|
3
|
+
// - Copies the page layout stylesheet (scrolly.css, attached to this widget
|
|
4
|
+
// so MyST bundles it) from the widget's shadow root into the document.
|
|
5
|
+
// - Tracks which step card is on screen and shows the matching stage view.
|
|
6
|
+
// - Draws the progress bar, section label and the "Enable Python" pill.
|
|
7
|
+
// - Plays muted videos in the active view and pauses the others.
|
|
8
|
+
// - Publishes the page's code cells on window.NMA_SCROLLY for runnable.mjs.
|
|
9
|
+
//
|
|
10
|
+
// Without JavaScript the page is a plain linear document.
|
|
11
|
+
|
|
12
|
+
const STYLE = `
|
|
13
|
+
.hud { position: absolute; top: 0; left: 0; right: 0; display: flex; align-items: center; gap: 16px; padding: 14px 28px; pointer-events: none; font-family: var(--nm-font-head, system-ui, sans-serif); }
|
|
14
|
+
.sec { font-weight: 600; font-size: 0.78rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--nm-mint, #5efbd7); min-width: 12ch; }
|
|
15
|
+
.bar { flex: 1; height: 3px; background: rgba(255,255,255,0.14); border-radius: 3px; overflow: hidden; }
|
|
16
|
+
.bar i { display: block; height: 100%; width: 0; background: linear-gradient(90deg, var(--nm-salmon, #fb7a7a), var(--nm-mint, #5efbd7)); transition: width 0.4s; }
|
|
17
|
+
.py[hidden] { display: none; }
|
|
18
|
+
.py { pointer-events: auto; flex: none; font: 600 0.74rem var(--nm-font-head, system-ui, sans-serif); letter-spacing: 0.04em; border: 1px solid var(--nm-mint, #5efbd7); border-radius: 999px; padding: 0.3rem 0.85rem; cursor: pointer; max-width: 34ch; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; background: var(--nm-mint, #5efbd7); color: var(--nm-indigo, #2a2952); }
|
|
19
|
+
.py[data-state="loading"] { background: transparent; color: var(--nm-mint, #5efbd7); cursor: progress; }
|
|
20
|
+
.py[data-state="ready"] { background: transparent; color: var(--nm-mint, #5efbd7); cursor: default; }
|
|
21
|
+
.py[data-state="error"] { background: var(--nm-salmon, #fb7a7a); border-color: var(--nm-salmon, #fb7a7a); }
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
// Section width (px) from which the stage sits beside the cards.
|
|
25
|
+
// Keep in sync with the @container breakpoint in scrolly.css.
|
|
26
|
+
const WIDE = 760;
|
|
27
|
+
|
|
28
|
+
const PY_LABELS = { idle: 'Enable Python ▸', ready: 'Python ready', error: 'Python failed. Retry' };
|
|
29
|
+
|
|
30
|
+
/** Copy the layout stylesheet link from the shadow root into <head> (once). */
|
|
31
|
+
function installPageStyles(el) {
|
|
32
|
+
const link = el.querySelector('link[rel="stylesheet"]');
|
|
33
|
+
if (!link || document.head.querySelector('link[data-nma-scrolly]')) return;
|
|
34
|
+
const copy = document.createElement('link');
|
|
35
|
+
copy.rel = 'stylesheet';
|
|
36
|
+
copy.href = link.href;
|
|
37
|
+
copy.dataset.nmaScrolly = '';
|
|
38
|
+
document.head.appendChild(copy);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function render({ model, el }) {
|
|
42
|
+
installPageStyles(el);
|
|
43
|
+
const host = el.getRootNode().host;
|
|
44
|
+
const container = host && host.closest('.scrolly');
|
|
45
|
+
if (!container) {
|
|
46
|
+
el.textContent = 'scrolly engine: no enclosing .scrolly section';
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
window.NMA_SCROLLY = {
|
|
50
|
+
...(window.NMA_SCROLLY || {}),
|
|
51
|
+
cells: model.get('cells') || [],
|
|
52
|
+
pyodideUrl: model.get('pyodideUrl'),
|
|
53
|
+
edits: (window.NMA_SCROLLY && window.NMA_SCROLLY.edits) || {},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const map = model.get('map') || [];
|
|
57
|
+
const hero = model.get('hero') || [];
|
|
58
|
+
const sections = model.get('sections') || [];
|
|
59
|
+
const steps = [...container.querySelectorAll(':scope > .scrolly-steps > .scrolly-step')];
|
|
60
|
+
const views = [...container.querySelectorAll(':scope > .scrolly-stage > .scrolly-view')];
|
|
61
|
+
|
|
62
|
+
const style = document.createElement('style');
|
|
63
|
+
style.textContent = STYLE;
|
|
64
|
+
const hud = document.createElement('div');
|
|
65
|
+
hud.className = 'hud';
|
|
66
|
+
hud.innerHTML = '<span class="sec"></span><div class="bar"><i></i></div><button type="button" class="py"></button>';
|
|
67
|
+
el.append(style, hud);
|
|
68
|
+
const secEl = hud.querySelector('.sec');
|
|
69
|
+
const barEl = hud.querySelector('.bar i');
|
|
70
|
+
const pyBtn = hud.querySelector('.py');
|
|
71
|
+
|
|
72
|
+
let active = -1;
|
|
73
|
+
function activate(i) {
|
|
74
|
+
if (i === active || i < 0) return;
|
|
75
|
+
active = i;
|
|
76
|
+
steps.forEach((s, k) => s.classList.toggle('is-active', k === i));
|
|
77
|
+
const v = map[i] ?? -1;
|
|
78
|
+
views.forEach((x, k) => {
|
|
79
|
+
const on = k === v;
|
|
80
|
+
x.classList.toggle('is-active', on);
|
|
81
|
+
x.querySelectorAll('video').forEach((video) => {
|
|
82
|
+
if (!on) video.pause();
|
|
83
|
+
else if (video.muted || video.autoplay) video.play().catch(() => {});
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
container.classList.toggle('is-hero-active', !!hero[i]);
|
|
87
|
+
secEl.textContent = sections[i] || '';
|
|
88
|
+
barEl.style.width = `${((i + 1) / steps.length) * 100}%`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let io;
|
|
92
|
+
let lastKey = '';
|
|
93
|
+
function observe() {
|
|
94
|
+
const H = window.innerHeight;
|
|
95
|
+
const wide = container.clientWidth >= WIDE;
|
|
96
|
+
const key = `${H}|${wide}`;
|
|
97
|
+
if (io && key === lastKey) return;
|
|
98
|
+
lastKey = key;
|
|
99
|
+
if (io) io.disconnect();
|
|
100
|
+
const top = parseFloat(getComputedStyle(container).getPropertyValue('--scrolly-top')) || 0;
|
|
101
|
+
const line = wide ? Math.round(H * 0.5) : Math.round(top + 0.55 * H + 48);
|
|
102
|
+
io = new IntersectionObserver(
|
|
103
|
+
(entries) => entries.forEach((e) => e.isIntersecting && activate(steps.indexOf(e.target.closest('.scrolly-step')))),
|
|
104
|
+
{ rootMargin: `-${line}px 0px -${Math.max(0, H - line - 1)}px 0px`, threshold: 0 },
|
|
105
|
+
);
|
|
106
|
+
steps.forEach((s) => io.observe(s.querySelector('.scrolly-card') || s));
|
|
107
|
+
}
|
|
108
|
+
window.addEventListener('resize', observe);
|
|
109
|
+
const ro = new ResizeObserver(observe); // the section's width changes with the menus, not just the window
|
|
110
|
+
ro.observe(container);
|
|
111
|
+
|
|
112
|
+
const showPy = (state, msg) => {
|
|
113
|
+
pyBtn.hidden = false;
|
|
114
|
+
pyBtn.dataset.state = state;
|
|
115
|
+
pyBtn.textContent = state === 'loading' ? msg : PY_LABELS[state] || PY_LABELS.idle;
|
|
116
|
+
pyBtn.title = state === 'error' ? msg : 'Runs Python in your browser (Pyodide). Downloaded once, then cached.';
|
|
117
|
+
};
|
|
118
|
+
const onPy = (e) => showPy(e.detail.state, e.detail.msg);
|
|
119
|
+
window.addEventListener('nma-py', onPy);
|
|
120
|
+
pyBtn.hidden = true; // shown once runnable.mjs creates the runtime
|
|
121
|
+
if (window.NMA_PY) showPy(window.NMA_PY.state, window.NMA_PY.msg);
|
|
122
|
+
pyBtn.addEventListener('click', async () => {
|
|
123
|
+
const py = window.NMA_PY;
|
|
124
|
+
if (py && (py.state === 'idle' || py.state === 'error')) py.ensure().catch(() => {});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
activate(0);
|
|
128
|
+
observe();
|
|
129
|
+
container.dataset.ready = 'true';
|
|
130
|
+
container._activate = activate; // test hook
|
|
131
|
+
|
|
132
|
+
return () => {
|
|
133
|
+
if (io) io.disconnect();
|
|
134
|
+
window.removeEventListener('resize', observe);
|
|
135
|
+
ro.disconnect();
|
|
136
|
+
window.removeEventListener('nma-py', onPy);
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export default { render };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/* Runnable Python panel (inside the widget's shadow DOM).
|
|
2
|
+
Colours come from the --nm-* variables set on .scrolly in scrolly.css. */
|
|
3
|
+
:host { display: block; width: 100%; }
|
|
4
|
+
* { box-sizing: border-box; }
|
|
5
|
+
[hidden] { display: none !important; }
|
|
6
|
+
|
|
7
|
+
.fl-codebox {
|
|
8
|
+
width: 100%;
|
|
9
|
+
background: #14131f;
|
|
10
|
+
border: 1px solid rgba(185, 184, 255, 0.28);
|
|
11
|
+
border-radius: 12px;
|
|
12
|
+
overflow: hidden;
|
|
13
|
+
box-shadow: 0 12px 34px rgba(8, 7, 30, 0.5);
|
|
14
|
+
color: #ecebff;
|
|
15
|
+
font-family: var(--nm-font-body, system-ui, sans-serif);
|
|
16
|
+
text-align: left;
|
|
17
|
+
}
|
|
18
|
+
.fl-codehead { display: flex; justify-content: space-between; padding: 0.45rem 1rem; background: #22214a; font: 600 0.72rem var(--nm-font-head, system-ui, sans-serif); letter-spacing: 0.1em; text-transform: uppercase; color: var(--nm-lilac, #b9b8ff); }
|
|
19
|
+
|
|
20
|
+
.fl-params { display: flex; flex-wrap: wrap; gap: 0.4rem 1.4rem; padding: 0.55rem 1rem; background: #1b1a36; border-bottom: 1px solid rgba(185, 184, 255, 0.2); }
|
|
21
|
+
.fl-params label { display: flex; align-items: center; gap: 0.5rem; font: 600 0.8rem ui-monospace, Consolas, monospace; color: var(--nm-lilac, #b9b8ff); }
|
|
22
|
+
.fl-params input[type=range] { width: 9rem; accent-color: var(--nm-mint, #5efbd7); }
|
|
23
|
+
.fl-params output { min-width: 4ch; color: var(--nm-mint, #5efbd7); text-align: right; }
|
|
24
|
+
.fl-params select { font: inherit; background: #14131f; color: var(--nm-mint, #5efbd7); border: 1px solid rgba(185, 184, 255, 0.4); border-radius: 6px; padding: 0.15rem 0.4rem; }
|
|
25
|
+
|
|
26
|
+
.fl-editor { position: relative; max-height: 46vh; overflow: auto; }
|
|
27
|
+
.fl-editor .fl-hl, .fl-editor .fl-ta {
|
|
28
|
+
margin: 0;
|
|
29
|
+
padding: 0.9rem 1.1rem;
|
|
30
|
+
font: 0.85rem/1.6 ui-monospace, "Cascadia Code", Consolas, monospace;
|
|
31
|
+
white-space: pre;
|
|
32
|
+
tab-size: 4;
|
|
33
|
+
}
|
|
34
|
+
.fl-editor .fl-hl { color: #ecebff; overflow: hidden; pointer-events: none; }
|
|
35
|
+
.fl-editor .fl-ta {
|
|
36
|
+
position: absolute; inset: 0; width: 100%; height: 100%;
|
|
37
|
+
color: transparent; caret-color: var(--nm-mint, #5efbd7); background: transparent; border: 0; outline: 0; resize: none; overflow: hidden;
|
|
38
|
+
}
|
|
39
|
+
.fl-editor .fl-ta::selection { background: rgba(94, 251, 215, 0.28); }
|
|
40
|
+
.fl-editor:focus-within { box-shadow: inset 0 0 0 2px rgba(94, 251, 215, 0.55); }
|
|
41
|
+
|
|
42
|
+
.fl-actions { display: flex; align-items: center; gap: 0.6rem; padding: 0.45rem 1rem; background: #1b1a36; border-top: 1px solid rgba(185, 184, 255, 0.2); }
|
|
43
|
+
.mode-output .fl-actions { border-top: 0; background: transparent; padding: 0.35rem 0.6rem; }
|
|
44
|
+
.fl-actions button { font: 600 0.78rem var(--nm-font-head, system-ui, sans-serif); border: 0; border-radius: 999px; padding: 0.3rem 0.95rem; cursor: pointer; }
|
|
45
|
+
.fl-run { background: var(--nm-mint, #5efbd7); color: var(--nm-indigo, #2a2952); }
|
|
46
|
+
.fl-run:disabled { opacity: 0.55; cursor: progress; }
|
|
47
|
+
.fl-reset, .fl-toggle { background: transparent; color: var(--nm-lilac, #b9b8ff); border: 1px solid rgba(185, 184, 255, 0.5) !important; }
|
|
48
|
+
.fl-status { font-size: 0.76rem; color: var(--nm-muted, #b4b3e6); }
|
|
49
|
+
|
|
50
|
+
.fl-out { border-top: 1px solid rgba(185, 184, 255, 0.2); background: #0e0d18; padding: 0.55rem 1.1rem 0.75rem; }
|
|
51
|
+
.mode-output .fl-out { border-top: 0; }
|
|
52
|
+
.fl-out pre { margin: 0.25rem 0 0; font: 0.84rem/1.55 ui-monospace, "Cascadia Code", Consolas, monospace; color: #c9f5ea; white-space: pre-wrap; max-height: 30vh; overflow: auto; }
|
|
53
|
+
.fl-figs img { display: block; max-width: 100%; max-height: 48vh; margin: 0.5rem auto 0; border-radius: 8px; background: #fff; }
|
|
54
|
+
.fl-err { color: var(--nm-salmon, #fb7a7a); font-size: 0.86rem; margin-top: 0.3rem; }
|
|
55
|
+
.fl-err details { margin-top: 0.25rem; color: var(--nm-muted, #b4b3e6); }
|
|
56
|
+
.fl-err pre { font: 0.74rem/1.5 ui-monospace, Consolas, monospace; color: #f5c1c1; }
|
|
57
|
+
.fl-note { margin: 0 0 0.3rem; font-size: 0.78rem; color: var(--nm-salmon, #fb7a7a); }
|
|
58
|
+
.fl-empty { margin: 0; font-size: 0.8rem; color: var(--nm-muted, #b4b3e6); font-style: italic; }
|
|
59
|
+
|
|
60
|
+
.tk-c { color: #8b8ac7; font-style: italic; }
|
|
61
|
+
.tk-s { color: var(--nm-mint, #5efbd7); font-style: normal; }
|
|
62
|
+
.tk-k { color: var(--nm-salmon, #fb7a7a); font-style: normal; }
|
|
63
|
+
.tk-n { color: var(--nm-lilac, #b9b8ff); font-style: normal; }
|
|
64
|
+
.tk-f { color: #7cc4ff; font-style: normal; }
|
|
65
|
+
.tk-b { font-style: normal; color: #fff; background: rgba(251, 122, 122, 0.35); border-radius: 4px; }
|
|
66
|
+
|
|
67
|
+
.is-collapsed .fl-editor { display: none; }
|