@3sln/deck 0.0.7 → 0.0.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@3sln/deck",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "A Vite plugin for building scalable, zero-config, Markdown-based component playgrounds and documentation sites.",
5
5
  "type": "module",
6
6
  "author": "Ray Stubbs",
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "@3sln/bones": "^0.0.4",
40
- "@3sln/dodo": "^0.0.4",
40
+ "@3sln/dodo": "^0.0.5",
41
41
  "@3sln/ngin": "^0.0.1",
42
42
  "fs-extra": "^11.3.2",
43
43
  "glob": "^10.3.10",
package/src/deck-demo.js CHANGED
@@ -8,6 +8,33 @@ import {stylesheet as highlightStylesheet, highlight} from './highlight.js';
8
8
 
9
9
  const {reconcile, h, div, button, pre, code, span, label, input, p} = dodo;
10
10
 
11
+ function getLanguageFromPath(path) {
12
+ if (!path) return 'plaintext';
13
+ const extension = path.split('.').pop().toLowerCase();
14
+ switch (extension) {
15
+ case 'js':
16
+ case 'mjs':
17
+ return 'javascript';
18
+ case 'cljs':
19
+ case 'clj':
20
+ case 'cljd':
21
+ return 'clojure';
22
+ case 'css':
23
+ return 'css';
24
+ case 'html':
25
+ case 'xml':
26
+ return 'xml';
27
+ case 'md':
28
+ return 'markdown';
29
+ case 'json':
30
+ return 'json';
31
+ case 'sh':
32
+ return 'bash';
33
+ default:
34
+ return 'plaintext';
35
+ }
36
+ }
37
+
11
38
  const {ObservableSubject, watch, zip, map, dedup} = reactiveFactory({dodo});
12
39
  const {withContainerSize} = resizeFactory({dodo});
13
40
 
@@ -59,7 +86,7 @@ const propertiesStyle = css`
59
86
  }
60
87
  `;
61
88
 
62
- function getEngine(rootNode, key, src) {
89
+ function getEngine(rootNode, key, src, canonicalSrc) {
63
90
  if (!rootNodeCaches.has(rootNode)) {
64
91
  rootNodeCaches.set(rootNode, new Map());
65
92
  }
@@ -72,7 +99,7 @@ function getEngine(rootNode, key, src) {
72
99
  return entry.engine;
73
100
  }
74
101
 
75
- const {engine, abortController} = createEngine(src);
102
+ const {engine, abortController} = createEngine(src, canonicalSrc);
76
103
  const entry = {
77
104
  engine,
78
105
  refCount: 1,
@@ -129,9 +156,11 @@ function propertyControl(engine, name) {
129
156
  });
130
157
  }
131
158
 
132
- function createEngine(src) {
159
+ function createEngine(src, canonicalSrc) {
133
160
  const abortController = new AbortController();
134
161
  const sourceCode$ = new ObservableSubject('Loading...');
162
+ const textSrc = canonicalSrc || src;
163
+ const lang = getLanguageFromPath(textSrc);
135
164
 
136
165
  const demoState = new DemoState({
137
166
  activePanelIds: {},
@@ -160,7 +189,7 @@ function createEngine(src) {
160
189
  reconcile(container, [
161
190
  watch(sourceCode$, text =>
162
191
  pre(
163
- code({className: 'language-javascript'}, text).on({
192
+ code({className: `language-${lang}`}, text).on({
164
193
  $update: el => {
165
194
  delete el.dataset.highlighted;
166
195
  highlight(el);
@@ -210,35 +239,35 @@ function createEngine(src) {
210
239
  engine.dispatch(new UpsertProperty(name, options));
211
240
  return engine.query(new PropertyValue(name));
212
241
  },
213
- setActivePanel: name => {
214
- engine.dispatch(new ActivatePanel(name));
215
- },
216
242
  get signal() {
217
243
  return abortController.signal;
218
244
  },
219
245
  };
220
246
 
221
247
  (async () => {
222
- if (!src) {
223
- return;
224
- }
248
+ const esmSrc = src;
249
+ const textSrc = canonicalSrc || src;
250
+ if (!esmSrc || !textSrc) return;
225
251
 
226
252
  try {
227
253
  if (HOT) {
228
- const m = await import(/* @vite-ignore */ `/@deck-dev-hmr/${encodeURIComponent(src)}`);
229
- const sub = m.moduleText$.subscribe(text => {
254
+ const esm = await import(/* @vite-ignore */ `/@deck-dev-esm/${encodeURIComponent(esmSrc)}`);
255
+ const txt = await import(/* @vite-ignore */ `/@deck-dev-src/${encodeURIComponent(textSrc)}.js`);
256
+
257
+ const sub = txt.moduleText$.subscribe(text => {
230
258
  sourceCode$.next(text);
231
259
  });
232
260
  abortController.signal.addEventListener('abort', () => {
233
261
  sub.unsubscribe();
234
262
  });
235
- m.default(driver);
263
+ esm.default(driver);
236
264
  } else {
237
- const url = new URL(src, location.href);
238
- const m = await import(/* @vite-ignore */ url.href);
265
+ const esmUrl = new URL(esmSrc, location.href);
266
+ const textUrl = new URL(textSrc, location.href);
267
+ const m = await import(/* @vite-ignore */ esmUrl.href);
239
268
  m.default(driver);
240
269
 
241
- const text = await fetch(url).then(r => r.text());
270
+ const text = await fetch(textUrl).then(r => r.text());
242
271
  sourceCode$.next(text);
243
272
  }
244
273
  } catch (err) {
@@ -736,7 +765,7 @@ class DeckDemo extends HTMLElement {
736
765
  }
737
766
 
738
767
  this.#id = this.id;
739
- this.#engine = getEngine(this.getRootNode(), this.id, this.getAttribute('src'));
768
+ this.#engine = getEngine(this.getRootNode(), this.id, this.getAttribute('src'), this.getAttribute('canonical-src'));
740
769
  this.#render();
741
770
  }
742
771
 
package/src/highlight.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import hljs from 'highlight.js/lib/core';
2
2
  import javascript from 'highlight.js/lib/languages/javascript';
3
3
  import xml from 'highlight.js/lib/languages/xml';
4
+ import clojure from 'highlight.js/lib/languages/clojure';
4
5
  import css from 'highlight.js/lib/languages/css';
6
+ import json from 'highlight.js/lib/languages/json';
5
7
  import githubStyle from 'highlight.js/styles/github.css?inline';
6
8
  import githubDarkStyle from 'highlight.js/styles/github-dark.css?inline';
7
9
  import {css as createSheet} from '@3sln/bones/style';
@@ -9,6 +11,8 @@ import {css as createSheet} from '@3sln/bones/style';
9
11
  hljs.registerLanguage('javascript', javascript);
10
12
  hljs.registerLanguage('xml', xml); // For HTML
11
13
  hljs.registerLanguage('css', css);
14
+ hljs.registerLanguage('clojure', clojure);
15
+ hljs.registerLanguage('json', json);
12
16
 
13
17
  export const stylesheet = createSheet`
14
18
  /* Light Theme */
package/src/history.js ADDED
@@ -0,0 +1,45 @@
1
+ const history = window.history;
2
+
3
+ // Function to get the current query parameters as an object
4
+ export function getQueryParams() {
5
+ const params = new URLSearchParams(window.location.search);
6
+ return {
7
+ q: params.get('q') ?? '',
8
+ c: params.get('c') ?? null,
9
+ };
10
+ }
11
+
12
+ // Function to update the URL with new state
13
+ // Uses replaceState to avoid polluting the history for rapid changes
14
+ export function replaceState(params) {
15
+ const url = new URL(window.location);
16
+ if (params.hasOwnProperty('q')) {
17
+ if (params.q) url.searchParams.set('q', params.q);
18
+ else url.searchParams.delete('q');
19
+ }
20
+ if (params.hasOwnProperty('c')) {
21
+ if (params.c) url.searchParams.set('c', params.c);
22
+ else url.searchParams.delete('c');
23
+ }
24
+ history.replaceState({}, '', url.toString());
25
+ }
26
+
27
+ // Function to push a new state to the history
28
+ // Use this for significant changes, like selecting a new card
29
+ export function pushState(params) {
30
+ const url = new URL(window.location);
31
+ if (params.hasOwnProperty('q')) {
32
+ if (params.q) url.searchParams.set('q', params.q);
33
+ else url.searchParams.delete('q');
34
+ }
35
+ if (params.hasOwnProperty('c')) {
36
+ if (params.c) url.searchParams.set('c', params.c);
37
+ else url.searchParams.delete('c');
38
+ }
39
+ history.pushState({}, '', url.toString());
40
+ }
41
+
42
+ // Wrapper for popstate event
43
+ export function onPopState(callback) {
44
+ window.addEventListener('popstate', callback);
45
+ }
package/src/main.js CHANGED
@@ -5,6 +5,7 @@ import {Engine, Provider} from '@3sln/ngin';
5
5
  import * as db from './db.js';
6
6
  import { ThrottledFetcher } from './fetcher.js';
7
7
  import {highlight, stylesheet as highlightStylesheet} from './highlight.js';
8
+ import * as history from './history.js';
8
9
  import {
9
10
  uiStateProvider,
10
11
  FilteredCards,
@@ -16,6 +17,7 @@ import {
16
17
  RemoveCard,
17
18
  PruneCards,
18
19
  SetPinnedCards,
20
+ SearchQuery
19
21
  } from './state.js';
20
22
  import './deck-demo.js';
21
23
 
@@ -43,27 +45,32 @@ const closeIcon = () =>
43
45
  );
44
46
 
45
47
  const searchBar = alias(engine => {
46
- return div(
47
- {className: 'search-bar'},
48
- input({
49
- type: 'search',
50
- placeholder: 'Search cards...',
51
- $styling: {
52
- width: '100%',
53
- padding: '0.75em 1em',
54
- 'font-size': '1.1em',
55
- border: '1px solid var(--input-border)',
56
- 'background-color': 'var(--input-bg)',
57
- color: 'var(--text-color)',
58
- 'border-radius': '2em',
59
- outline: 'none',
60
- transition: 'box-shadow 0.2s',
61
- },
62
- }).on({
63
- focus: e => (e.target.style.boxShadow = '0 0 5px rgba(81, 203, 238, 1)'),
64
- blur: e => (e.target.style.boxShadow = 'none'),
65
- input: e => engine.dispatch(new SetSearchQuery(e.target.value)),
66
- }),
48
+ const query$ = engine.query(new SearchQuery());
49
+
50
+ return watch(query$, query =>
51
+ div(
52
+ {className: 'search-bar'},
53
+ input({
54
+ type: 'search',
55
+ placeholder: 'Search cards...',
56
+ value: query,
57
+ $styling: {
58
+ width: '100%',
59
+ padding: '0.75em 1em',
60
+ 'font-size': '1.1em',
61
+ border: '1px solid var(--input-border)',
62
+ 'background-color': 'var(--input-bg)',
63
+ color: 'var(--text-color)',
64
+ 'border-radius': '2em',
65
+ outline: 'none',
66
+ transition: 'box-shadow 0.2s',
67
+ },
68
+ }).on({
69
+ focus: e => (e.target.style.boxShadow = '0 0 5px rgba(81, 203, 238, 1)'),
70
+ blur: e => (e.target.style.boxShadow = 'none'),
71
+ input: e => engine.dispatch(new SetSearchQuery(e.target.value)),
72
+ }),
73
+ ),
67
74
  );
68
75
  });
69
76
 
@@ -301,6 +308,7 @@ export async function renderDeck({target, initialCardsData, pinnedCardPaths}) {
301
308
  providers: {
302
309
  state: uiStateProvider(),
303
310
  fetcher: Provider.fromSingleton(new ThrottledFetcher()),
311
+ history: Provider.fromSingleton(history),
304
312
  },
305
313
  });
306
314
 
@@ -311,6 +319,15 @@ export async function renderDeck({target, initialCardsData, pinnedCardPaths}) {
311
319
  });
312
320
  engine.dispatch(new PruneCards(initialCardsData.map(c => c.path)));
313
321
 
322
+ const syncNav = () => {
323
+ const {q, c} = history.getQueryParams();
324
+ engine.dispatch(new SelectCard(c));
325
+ engine.dispatch(new SetSearchQuery(q));
326
+ };
327
+
328
+ history.onPopState(syncNav);
329
+ syncNav();
330
+
314
331
  // Handle HMR
315
332
  if (import.meta.hot) {
316
333
  import.meta.hot.on('deck:card-changed', ({path}) => {
@@ -323,4 +340,4 @@ export async function renderDeck({target, initialCardsData, pinnedCardPaths}) {
323
340
  }
324
341
 
325
342
  reconcile(target, [app(engine)]);
326
- }
343
+ }
package/src/state.js CHANGED
@@ -186,31 +186,57 @@ export class RemoveCard extends Action {
186
186
  }
187
187
 
188
188
  export class SetSearchQuery extends Action {
189
- static deps = ['state'];
189
+ static deps = ['state', 'history'];
190
190
  constructor(query) {
191
191
  super();
192
192
  this.query = query;
193
193
  }
194
- execute({state}) {
194
+ execute({state, history}) {
195
+ if (this.query !== state.state$.value.query) {
196
+ history.replaceState({q: this.query});
197
+ }
195
198
  state.update(s => ({...s, query: this.query}));
196
199
  }
197
200
  }
198
201
 
199
202
  export class SelectCard extends Action {
200
- static deps = ['state'];
203
+ static deps = ['state', 'history'];
201
204
  constructor(cardPath) {
202
205
  super();
203
206
  this.cardPath = cardPath;
204
207
  }
205
- async execute({state}) {
206
- await db.touchCard(this.cardPath);
207
- state.update(s => ({...s, selectedCardPath: this.cardPath}));
208
+ async execute({state, history}) {
209
+ if (!this.cardPath) {
210
+ if (state.state$.value.selectedCardPath !== null) {
211
+ history.pushState({c: null});
212
+ state.update(s => ({...s, selectedCardPath: null}));
213
+ }
214
+ return;
215
+ }
216
+
217
+ const card = await db.getCard(this.cardPath);
218
+
219
+ if (card) {
220
+ if (this.cardPath !== state.state$.value.selectedCardPath) {
221
+ history.pushState({c: this.cardPath});
222
+ }
223
+ await db.touchCard(this.cardPath);
224
+ state.update(s => ({...s, selectedCardPath: this.cardPath}));
225
+ } else {
226
+ if (state.state$.value.selectedCardPath !== null) {
227
+ history.pushState({c: null});
228
+ }
229
+ state.update(s => ({...s, selectedCardPath: null}));
230
+ }
208
231
  }
209
232
  }
210
233
 
211
234
  export class ClearSelection extends Action {
212
- static deps = ['state'];
213
- execute({state}) {
235
+ static deps = ['state', 'history'];
236
+ execute({state, history}) {
237
+ if (state.state$.value.selectedCardPath !== null) {
238
+ history.pushState({c: null});
239
+ }
214
240
  state.update(s => ({...s, selectedCardPath: null}));
215
241
  }
216
242
  }
package/vite-plugin.js CHANGED
@@ -9,7 +9,7 @@ export default function deckPlugin() {
9
9
  name: 'vite-plugin-deck',
10
10
 
11
11
  resolveId(id) {
12
- if (id.startsWith('/@deck-dev-hmr/')) {
12
+ if (id.startsWith('/@deck-dev-esm/') || id.startsWith('/@deck-dev-src/')) {
13
13
  return id;
14
14
  }
15
15
  return null;
@@ -29,12 +29,10 @@ export default function deckPlugin() {
29
29
  },
30
30
 
31
31
  load(id) {
32
- if (id.startsWith('/@deck-dev-hmr/')) {
33
- const realPath = decodeURIComponent(id.slice('/@deck-dev-hmr/'.length));
34
- const rawPath = realPath + '?raw';
32
+ if (id.startsWith('/@deck-dev-esm/')) {
33
+ const realPath = decodeURIComponent(id.slice('/@deck-dev-esm/'.length));
35
34
  return `
36
35
  import realDefault from '${realPath}';
37
- import moduleText from '${rawPath}';
38
36
 
39
37
  let lastArgs;
40
38
  let abortController = new AbortController();
@@ -43,6 +41,36 @@ export default function deckPlugin() {
43
41
  lastArgs = import.meta.hot.data.lastArgs;
44
42
  }
45
43
 
44
+ export default (...args) => {
45
+ lastArgs = args;
46
+ const thisContext = { signal: abortController.signal };
47
+ realDefault.call(thisContext, ...args);
48
+ };
49
+
50
+ if (import.meta.hot) {
51
+ import.meta.hot.dispose(data => {
52
+ data.lastArgs = lastArgs;
53
+ abortController.abort();
54
+ });
55
+
56
+ import.meta.hot.accept(newModule => {
57
+ if (newModule && newModule.default && lastArgs) {
58
+ newModule.default(...lastArgs);
59
+ }
60
+ });
61
+ }
62
+ `;
63
+ }
64
+
65
+ if (id.startsWith('/@deck-dev-src/')) {
66
+ let realPath = decodeURIComponent(id.slice('/@deck-dev-src/'.length));
67
+ if (realPath.endsWith('.js')) {
68
+ realPath = realPath.slice(0, -3);
69
+ }
70
+ const rawPath = realPath + '?raw';
71
+ return `
72
+ import moduleText from '${rawPath}';
73
+
46
74
  const textObservers = import.meta.hot?.data.textObservers ?? [];
47
75
  export const moduleText$ = {
48
76
  subscribe: observer => {
@@ -58,24 +86,12 @@ export default function deckPlugin() {
58
86
  }
59
87
  };
60
88
 
61
- export default (...args) => {
62
- lastArgs = args;
63
- const thisContext = { signal: abortController.signal };
64
- realDefault.call(thisContext, ...args);
65
- };
66
-
67
89
  if (import.meta.hot) {
90
+ import.meta.hot.accept();
68
91
  import.meta.hot.dispose(data => {
69
- data.lastArgs = lastArgs;
70
92
  data.textObservers = textObservers;
71
- abortController.abort();
72
93
  });
73
94
 
74
- import.meta.hot.accept(newModule => {
75
- if (newModule && newModule.default && lastArgs) {
76
- newModule.default(...lastArgs);
77
- }
78
- });
79
95
  import.meta.hot.on('deck-raw-update', ({urls, text}) => {
80
96
  if (!urls.includes('${rawPath}')) {
81
97
  return;
@@ -88,6 +104,7 @@ export default function deckPlugin() {
88
104
  }
89
105
  `;
90
106
  }
107
+
91
108
  return null;
92
109
  },
93
110
 
@@ -106,7 +123,7 @@ export default function deckPlugin() {
106
123
  server.watcher.on('all', (eventName, eventPath) => {
107
124
  const projPath = path.relative(resolvedConfig.root, eventPath);
108
125
  const webPath = '/' + projPath.replace(/\\/g, '/');
109
- const files = getProjectFiles(resolvedConfig.root, devConfig).map(p => `/${p}`);
126
+ const files = getCardFiles(resolvedConfig.root, devConfig).map(p => `/${p}`);
110
127
  if (!files.includes(webPath)) return;
111
128
 
112
129
  switch (eventName) {
@@ -121,7 +138,7 @@ export default function deckPlugin() {
121
138
  });
122
139
 
123
140
  server.middlewares.use(async (req, res, next) => {
124
- if (req.url.endsWith('/')) {
141
+ if (new URL(req.url, "https://localhost").pathname === '/') {
125
142
  const cardPaths = getCardFiles(resolvedConfig.root, devConfig).map(p => `/${p}`);
126
143
  const initialCardsData = await Promise.all(cardPaths.map(async (p) => {
127
144
  const content = await fs.readFile(path.join(resolvedConfig.root, p), 'utf-8');