@3sln/deck 0.0.7 → 0.0.8

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.8",
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
@@ -59,7 +59,7 @@ const propertiesStyle = css`
59
59
  }
60
60
  `;
61
61
 
62
- function getEngine(rootNode, key, src) {
62
+ function getEngine(rootNode, key, src, canonicalSrc) {
63
63
  if (!rootNodeCaches.has(rootNode)) {
64
64
  rootNodeCaches.set(rootNode, new Map());
65
65
  }
@@ -72,7 +72,7 @@ function getEngine(rootNode, key, src) {
72
72
  return entry.engine;
73
73
  }
74
74
 
75
- const {engine, abortController} = createEngine(src);
75
+ const {engine, abortController} = createEngine(src, canonicalSrc);
76
76
  const entry = {
77
77
  engine,
78
78
  refCount: 1,
@@ -129,7 +129,7 @@ function propertyControl(engine, name) {
129
129
  });
130
130
  }
131
131
 
132
- function createEngine(src) {
132
+ function createEngine(src, canonicalSrc) {
133
133
  const abortController = new AbortController();
134
134
  const sourceCode$ = new ObservableSubject('Loading...');
135
135
 
@@ -210,35 +210,35 @@ function createEngine(src) {
210
210
  engine.dispatch(new UpsertProperty(name, options));
211
211
  return engine.query(new PropertyValue(name));
212
212
  },
213
- setActivePanel: name => {
214
- engine.dispatch(new ActivatePanel(name));
215
- },
216
213
  get signal() {
217
214
  return abortController.signal;
218
215
  },
219
216
  };
220
217
 
221
218
  (async () => {
222
- if (!src) {
223
- return;
224
- }
219
+ const esmSrc = src;
220
+ const textSrc = canonicalSrc || src;
221
+ if (!esmSrc || !textSrc) return;
225
222
 
226
223
  try {
227
224
  if (HOT) {
228
- const m = await import(/* @vite-ignore */ `/@deck-dev-hmr/${encodeURIComponent(src)}`);
229
- const sub = m.moduleText$.subscribe(text => {
225
+ const esm = await import(/* @vite-ignore */ `/@deck-dev-esm/${encodeURIComponent(esmSrc)}`);
226
+ const txt = await import(/* @vite-ignore */ `/@deck-dev-src/${encodeURIComponent(textSrc)}.js`);
227
+
228
+ const sub = txt.moduleText$.subscribe(text => {
230
229
  sourceCode$.next(text);
231
230
  });
232
231
  abortController.signal.addEventListener('abort', () => {
233
232
  sub.unsubscribe();
234
233
  });
235
- m.default(driver);
234
+ esm.default(driver);
236
235
  } else {
237
- const url = new URL(src, location.href);
238
- const m = await import(/* @vite-ignore */ url.href);
236
+ const esmUrl = new URL(esmSrc, location.href);
237
+ const textUrl = new URL(textSrc, location.href);
238
+ const m = await import(/* @vite-ignore */ esmUrl.href);
239
239
  m.default(driver);
240
240
 
241
- const text = await fetch(url).then(r => r.text());
241
+ const text = await fetch(textUrl).then(r => r.text());
242
242
  sourceCode$.next(text);
243
243
  }
244
244
  } catch (err) {
@@ -736,7 +736,7 @@ class DeckDemo extends HTMLElement {
736
736
  }
737
737
 
738
738
  this.#id = this.id;
739
- this.#engine = getEngine(this.getRootNode(), this.id, this.getAttribute('src'));
739
+ this.#engine = getEngine(this.getRootNode(), this.id, this.getAttribute('src'), this.getAttribute('canonical-src'));
740
740
  this.#render();
741
741
  }
742
742
 
package/src/highlight.js CHANGED
@@ -1,6 +1,7 @@
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';
5
6
  import githubStyle from 'highlight.js/styles/github.css?inline';
6
7
  import githubDarkStyle from 'highlight.js/styles/github-dark.css?inline';
@@ -9,6 +10,7 @@ import {css as createSheet} from '@3sln/bones/style';
9
10
  hljs.registerLanguage('javascript', javascript);
10
11
  hljs.registerLanguage('xml', xml); // For HTML
11
12
  hljs.registerLanguage('css', css);
13
+ hljs.registerLanguage('clojure', clojure);
12
14
 
13
15
  export const stylesheet = createSheet`
14
16
  /* 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,16 @@ 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
+ console.log('q', q);
325
+ engine.dispatch(new SelectCard(c));
326
+ engine.dispatch(new SetSearchQuery(q));
327
+ };
328
+
329
+ history.onPopState(syncNav);
330
+ syncNav();
331
+
314
332
  // Handle HMR
315
333
  if (import.meta.hot) {
316
334
  import.meta.hot.on('deck:card-changed', ({path}) => {
@@ -323,4 +341,4 @@ export async function renderDeck({target, initialCardsData, pinnedCardPaths}) {
323
341
  }
324
342
 
325
343
  reconcile(target, [app(engine)]);
326
- }
344
+ }
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');