pict-docuserve 1.4.0 → 1.4.2

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": "pict-docuserve",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "description": "Pict Documentation Server - A single-page documentation viewer built on Pict",
5
5
  "main": "source/Pict-Application-Docuserve.js",
6
6
  "bin": {
@@ -14,9 +14,7 @@
14
14
  "scripts": {
15
15
  "start": "node source/cli/Docuserve-CLI-Run.js serve",
16
16
  "brand": "node node_modules/pict-section-theme/bin/pict-section-theme-brand.js --manifest ../../../Retold-Modules-Manifest.json --module pict-docuserve",
17
- "prebuild": "npm run brand",
18
17
  "build": "npx quack build && npx quack copy",
19
- "prebuild-docs": "npm run brand",
20
18
  "build-docs": "npx quack build && npx quack copy && node source/cli/Docuserve-CLI-Run.js inject ./docs && node example_applications/build-examples.js stage-docs",
21
19
  "serve-docs": "node source/cli/Docuserve-CLI-Run.js serve ./docs",
22
20
  "serve-examples": "node example_applications/build-examples.js",
@@ -34,16 +32,16 @@
34
32
  "pict-application": "^1.0.34",
35
33
  "pict-provider": "^1.0.13",
36
34
  "pict-section-code": "^1.0.11",
37
- "pict-section-content": "^1.0.4",
35
+ "pict-section-content": "^1.0.5",
38
36
  "pict-section-histogram": "^1.0.1",
39
- "pict-section-modal": "^1.1.3",
37
+ "pict-section-modal": "^1.1.4",
40
38
  "pict-section-theme": "^1.0.6",
41
39
  "pict-service-commandlineutility": "^1.0.19",
42
40
  "pict-view": "^1.0.68"
43
41
  },
44
42
  "devDependencies": {
45
- "pict-docuserve": "^1.3.3",
46
- "quackage": "^1.2.4"
43
+ "pict-docuserve": "^1.4.1",
44
+ "quackage": "^1.2.5"
47
45
  },
48
46
  "copyFilesSettings": {
49
47
  "whenFileExists": "overwrite"
@@ -23,6 +23,7 @@ const libViewContent = require('./views/PictView-Docuserve-Content.js');
23
23
  const libViewSearch = require('./views/PictView-Docuserve-Search.js');
24
24
  const libViewDemo = require('./views/PictView-Docuserve-Demo.js');
25
25
  const libViewFablePlayground = require('./views/PictView-Docuserve-Fable-Playground.js');
26
+ const libViewSectionPlayground = require('./views/PictView-Docuserve-Section-Playground.js');
26
27
 
27
28
  class DocuserveApplication extends libPictApplication
28
29
  {
@@ -55,6 +56,7 @@ class DocuserveApplication extends libPictApplication
55
56
  this.pict.addView('Docuserve-Search', libViewSearch.default_configuration, libViewSearch);
56
57
  this.pict.addView('Docuserve-Demo', libViewDemo.default_configuration, libViewDemo);
57
58
  this.pict.addView('Docuserve-Fable-Playground', libViewFablePlayground.default_configuration, libViewFablePlayground);
59
+ this.pict.addView('Docuserve-Section-Playground', libViewSectionPlayground.default_configuration, libViewSectionPlayground);
58
60
 
59
61
  // Theme-Section is added later from onAfterInitializeAsync, after
60
62
  // the brand has been resolved. Docuserve is a viewer — the docs
@@ -288,10 +290,23 @@ class DocuserveApplication extends libPictApplication
288
290
  return;
289
291
  }
290
292
 
291
- // #/playground/<kind> — live editor + sandbox for trying Fable
292
- // (eventually Pict too).
293
+ // #/playground/<kind> — live editor + sandbox. Three shapes:
294
+ // #/playground/fable — Fable JS REPL (bottom drawer)
295
+ // #/playground/section — section playground for the
296
+ // current module (standalone-mode
297
+ // docs site)
298
+ // #/playground/section/<group>/<module> — section playground for a
299
+ // specific module (catalog-mode
300
+ // docs site serving multiple)
293
301
  if (tmpParts[0] === 'playground' && tmpParts.length >= 2)
294
302
  {
303
+ if (tmpParts[1] === 'section')
304
+ {
305
+ let tmpGroup = (tmpParts.length >= 4) ? tmpParts[2] : (this.pict.AppData.Docuserve.CurrentGroup || '');
306
+ let tmpModule = (tmpParts.length >= 4) ? tmpParts[3] : (this.pict.AppData.Docuserve.CurrentModule || '');
307
+ this.navigateToSectionPlayground(tmpGroup, tmpModule);
308
+ return;
309
+ }
295
310
  this.navigateToPlayground(tmpParts[1]);
296
311
  return;
297
312
  }
@@ -374,6 +389,55 @@ class DocuserveApplication extends libPictApplication
374
389
  }
375
390
  }
376
391
 
392
+ /**
393
+ * Open the section playground for `<group>/<module>`. Unlike the
394
+ * Fable playground (which lives in the bottom drawer), the section
395
+ * playground takes over the main content area — it needs full
396
+ * vertical space for its four editor tabs + iframe pane.
397
+ *
398
+ * @param {string} pGroup
399
+ * @param {string} pModule
400
+ */
401
+ navigateToSectionPlayground(pGroup, pModule)
402
+ {
403
+ this.pict.AppData.Docuserve.CurrentGroup = pGroup;
404
+ this.pict.AppData.Docuserve.CurrentModule = pModule;
405
+ this.pict.AppData.Docuserve.CurrentDemo = '';
406
+ this.pict.AppData.Docuserve.CurrentPath = 'section-playground';
407
+
408
+ // Hide the bottom playground drawer — the section playground
409
+ // is the whole show on this route.
410
+ let tmpLayout = this.pict.views['Docuserve-Layout'];
411
+ if (tmpLayout && typeof tmpLayout.setPlaygroundEnabled === 'function')
412
+ {
413
+ tmpLayout.setPlaygroundEnabled(false);
414
+ }
415
+
416
+ let tmpPlayground = this.pict.views['Docuserve-Section-Playground'];
417
+ if (!tmpPlayground || typeof tmpPlayground.openPlayground !== 'function')
418
+ {
419
+ let tmpContentView = this.pict.views['Docuserve-Content'];
420
+ if (tmpContentView && typeof tmpContentView.displayContent === 'function')
421
+ {
422
+ tmpContentView.render();
423
+ tmpContentView.displayContent('<h1>Section playground unavailable</h1><p>The Section Playground view is not registered.</p>');
424
+ }
425
+ return;
426
+ }
427
+ tmpPlayground.openPlayground(pGroup, pModule);
428
+
429
+ // Keep the sidebar in sync — show the module's nav + active item.
430
+ let tmpSidebarView = this.pict.views['Docuserve-Sidebar'];
431
+ if (tmpSidebarView)
432
+ {
433
+ tmpSidebarView.renderSidebarGroups();
434
+ if (typeof tmpSidebarView.renderModuleNav === 'function')
435
+ {
436
+ tmpSidebarView.renderModuleNav(pGroup, pModule);
437
+ }
438
+ }
439
+ }
440
+
377
441
  /**
378
442
  * Navigate to a hash route.
379
443
  *
@@ -433,12 +497,16 @@ class DocuserveApplication extends libPictApplication
433
497
  ? ('JS Playground: ' + tmpModule)
434
498
  : 'JS Playground');
435
499
  }
436
- // Load the module's _playground.json (cached after first load).
437
- // Stash the config in AppData so the playground view's require
438
- // shim and the per-run capture logic can consult it. We don't
439
- // gate the playground on the config landing — it's purely
440
- // informative right now since all fable modules are bundled.
441
- if (tmpEnabled && tmpDoc && typeof tmpDoc.loadPlaygroundConfig === 'function')
500
+ // Load the module's _playground.json. Two reasons to always do
501
+ // this (regardless of tmpEnabled):
502
+ // 1. The async `Kind` check inside isPlaygroundEnabled needs
503
+ // the cache populated. If the config declares Kind:
504
+ // "section", we have to re-call setPlaygroundEnabled(false)
505
+ // to hide the Fable drawer that the sync sidebar-based
506
+ // check just turned on.
507
+ // 2. Apps still consult AppData.Docuserve.Playground.Config
508
+ // (DOM-sandbox pane visibility etc.).
509
+ if (tmpDoc && typeof tmpDoc.loadPlaygroundConfig === 'function')
442
510
  {
443
511
  tmpDoc.loadPlaygroundConfig(tmpGroup, tmpModule).then((pConfig) =>
444
512
  {
@@ -447,6 +515,16 @@ class DocuserveApplication extends libPictApplication
447
515
  this.pict.AppData.Docuserve.Playground = {};
448
516
  }
449
517
  this.pict.AppData.Docuserve.Playground.Config = pConfig || null;
518
+
519
+ // Re-evaluate the drawer's enabled state now that the
520
+ // cache is populated — a Kind: "section" config flips
521
+ // it from true back to false.
522
+ if (typeof tmpDoc.isPlaygroundEnabled === 'function')
523
+ {
524
+ let tmpReEnabled = tmpDoc.isPlaygroundEnabled(tmpGroup, tmpModule);
525
+ tmpLayout.setPlaygroundEnabled(tmpReEnabled);
526
+ }
527
+
450
528
  // Config controls DOM-sandbox pane visibility; let the
451
529
  // playground view re-evaluate the moment it lands. No-op
452
530
  // if the view hasn't been mounted yet.
@@ -457,18 +535,6 @@ class DocuserveApplication extends libPictApplication
457
535
  }
458
536
  });
459
537
  }
460
- else
461
- {
462
- if (this.pict.AppData.Docuserve.Playground)
463
- {
464
- this.pict.AppData.Docuserve.Playground.Config = null;
465
- }
466
- let tmpPlaygroundView = this.pict.views['Docuserve-Fable-Playground'];
467
- if (tmpPlaygroundView && typeof tmpPlaygroundView._applySandboxMode === 'function')
468
- {
469
- tmpPlaygroundView._applySandboxMode();
470
- }
471
- }
472
538
  }
473
539
 
474
540
  /**
@@ -13,6 +13,7 @@ let _PictCLIProgram = new libCLIProgram(
13
13
  require('./commands/Docuserve-Command-Inject.js'),
14
14
  require('./commands/Docuserve-Command-PrepareLocal.js'),
15
15
  require('./commands/Docuserve-Command-StageExamples.js'),
16
+ require('./commands/Docuserve-Command-StagePlayground.js'),
16
17
  require('./commands/Docuserve-Command-CheckLinks.js')
17
18
  ]);
18
19
 
@@ -0,0 +1,279 @@
1
+ const libCommandLineCommand = require('pict-service-commandlineutility').ServiceCommandLineCommand;
2
+
3
+ const libFS = require('fs');
4
+ const libPath = require('path');
5
+
6
+ /**
7
+ * Recursively create a directory if it does not exist.
8
+ */
9
+ function ensureDir(pPath)
10
+ {
11
+ if (!libFS.existsSync(pPath))
12
+ {
13
+ libFS.mkdirSync(pPath, { recursive: true });
14
+ }
15
+ }
16
+
17
+ /**
18
+ * `pict-docuserve stage-playground <docs>` — read `<docs>/_playground.json`
19
+ * and copy every `Imports[]` entry with `Source: "local"` from its resolved
20
+ * source bundle into `<docs>/<Path>`.
21
+ *
22
+ * This is the per-module half of the section-playground story: each module's
23
+ * `_playground.json` declares which UMD bundles its iframe needs, this
24
+ * command stages them into the docs tree so the iframe can `<script src=...>`
25
+ * them relative to the docs root. Without it, the bundles either have to be
26
+ * hand-copied (the original section-form workflow) or loaded from the CDN —
27
+ * which doesn't work for un-published in-development versions.
28
+ *
29
+ * Resolution order for each Import's source bundle:
30
+ * 1. `<moduleRoot>/node_modules/<Name>/dist/<Name>.min.js` — the normal
31
+ * case for peer dependencies.
32
+ * 2. `<moduleRoot>/dist/<Name>.min.js` — when the Import IS the module
33
+ * being staged (e.g. pict-section-form staging itself).
34
+ * 3. Sibling monorepo checkout — walks up looking for a `modules/`
35
+ * directory and searches `modules/<group>/<Name>/dist/<Name>.min.js`.
36
+ * Lets in-monorepo dev workflows pick up a freshly-built bundle even
37
+ * when the umbrella `node_modules/` is stale.
38
+ *
39
+ * Clean no-op when `_playground.json` is absent — safe to wire into
40
+ * `quack prepare-docs` for every module.
41
+ */
42
+ class DocuserveCommandStagePlayground extends libCommandLineCommand
43
+ {
44
+ constructor(pFable, pManifest, pServiceHash)
45
+ {
46
+ super(pFable, pManifest, pServiceHash);
47
+
48
+ this.options.CommandKeyword = 'stage-playground';
49
+ this.options.Description = 'Stage local Imports referenced by docs/_playground.json into the docs playground runtime folder.';
50
+
51
+ this.options.CommandArguments.push({ Name: '[docs_folder]', Description: 'The documentation folder to stage into. Defaults to ./docs.' });
52
+ this.options.CommandOptions.push({ Name: '-m, --module_root [module_root]', Description: 'Root of the module being staged (where node_modules lives). Defaults to the docs folder\'s parent.', Default: '' });
53
+
54
+ this.options.Aliases.push('stage-playground-runtime');
55
+
56
+ this.addCommand();
57
+ }
58
+
59
+ onRun()
60
+ {
61
+ let tmpDocsFolder = libPath.resolve(this.ArgumentString || './docs');
62
+ let tmpModuleRoot = libPath.resolve(this.CommandOptions.module_root || libPath.dirname(tmpDocsFolder));
63
+
64
+ let tmpPlaygroundConfigPath = libPath.join(tmpDocsFolder, '_playground.json');
65
+ if (!libFS.existsSync(tmpPlaygroundConfigPath))
66
+ {
67
+ this.log.info(`No _playground.json at [${tmpPlaygroundConfigPath}]; nothing to stage.`);
68
+ return;
69
+ }
70
+
71
+ let tmpConfig;
72
+ try
73
+ {
74
+ tmpConfig = JSON.parse(libFS.readFileSync(tmpPlaygroundConfigPath, 'utf8'));
75
+ }
76
+ catch (pError)
77
+ {
78
+ this.log.error(`Failed to parse _playground.json [${tmpPlaygroundConfigPath}]: ${pError.message}`);
79
+ process.exit(1);
80
+ return;
81
+ }
82
+
83
+ let tmpImports = (tmpConfig && Array.isArray(tmpConfig.Imports)) ? tmpConfig.Imports : [];
84
+ let tmpStylesheets = (tmpConfig && Array.isArray(tmpConfig.Stylesheets)) ? tmpConfig.Stylesheets : [];
85
+
86
+ // Staging targets — every Import / Stylesheet with Source: "local".
87
+ // Each entry produces { kind, name, source, dest } once resolved.
88
+ let tmpJobs = [];
89
+ for (let i = 0; i < tmpImports.length; i++)
90
+ {
91
+ let tmpImp = tmpImports[i];
92
+ if (!tmpImp || tmpImp.Source !== 'local') { continue; }
93
+ tmpJobs.push({ kind: 'script', spec: tmpImp });
94
+ }
95
+ for (let i = 0; i < tmpStylesheets.length; i++)
96
+ {
97
+ let tmpSheet = tmpStylesheets[i];
98
+ if (!tmpSheet || tmpSheet.Source !== 'local') { continue; }
99
+ tmpJobs.push({ kind: 'stylesheet', spec: tmpSheet });
100
+ }
101
+
102
+ if (tmpJobs.length === 0)
103
+ {
104
+ this.log.info(`No local Imports or Stylesheets declared in _playground.json; nothing to stage.`);
105
+ return;
106
+ }
107
+
108
+ this.log.info(`Staging ${tmpJobs.length} playground runtime asset(s) from module root [${tmpModuleRoot}]...`);
109
+
110
+ let tmpCopied = 0;
111
+ let tmpFailed = 0;
112
+ for (let i = 0; i < tmpJobs.length; i++)
113
+ {
114
+ let tmpJob = tmpJobs[i];
115
+ let tmpKind = tmpJob.kind;
116
+ let tmpSpec = tmpJob.spec;
117
+ let tmpName = tmpSpec.Name;
118
+ let tmpExt = (tmpKind === 'stylesheet') ? 'css' : 'js';
119
+
120
+ if (!tmpName && !tmpSpec.Path)
121
+ {
122
+ this.log.warn(`${tmpKind} #${i} has neither Name nor Path; skipping.`);
123
+ tmpFailed++;
124
+ continue;
125
+ }
126
+
127
+ let tmpRelativePath = tmpSpec.Path || `playground/runtime/${tmpName}.min.${tmpExt}`;
128
+ let tmpDest = libPath.join(tmpDocsFolder, tmpRelativePath);
129
+
130
+ let tmpSource = this._resolveAssetSource(tmpKind, tmpName, tmpSpec.Path, tmpModuleRoot);
131
+ if (!tmpSource)
132
+ {
133
+ this.log.warn(`[${tmpName || tmpRelativePath}] No ${tmpKind} source found; skipping. Build it (npx quack build) and re-run prepare-docs.`);
134
+ tmpFailed++;
135
+ continue;
136
+ }
137
+
138
+ try
139
+ {
140
+ ensureDir(libPath.dirname(tmpDest));
141
+ libFS.copyFileSync(tmpSource, tmpDest);
142
+ this.log.info(`Staged ${tmpKind} ${tmpName || tmpRelativePath}: ${tmpSource} -> ${tmpDest}`);
143
+ tmpCopied++;
144
+ }
145
+ catch (pError)
146
+ {
147
+ this.log.error(`[${tmpName || tmpRelativePath}] Copy failed: ${pError.message}`);
148
+ tmpFailed++;
149
+ }
150
+ }
151
+
152
+ this.log.info(`Playground staging complete: ${tmpCopied} copied, ${tmpFailed} failed.`);
153
+ if (tmpFailed > 0 && tmpCopied === 0)
154
+ {
155
+ // Hard failure only when nothing landed — a partial copy is a
156
+ // warning so prepare-docs can keep going.
157
+ process.exit(1);
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Resolve the source asset (script bundle or stylesheet) for an Import
163
+ * or Stylesheet entry. Scripts look at `<pkg>/dist/<pkg>.min.js`;
164
+ * stylesheets look at `<pkg>/dist/<file>` for whatever Path was declared
165
+ * (since CSS filenames don't follow a single naming convention).
166
+ *
167
+ * @param {string} pKind - "script" or "stylesheet".
168
+ * @param {string} pName - Package name (may be empty for ad-hoc paths).
169
+ * @param {string} pSpecPath - Optional Path from the spec; for stylesheets
170
+ * we use its basename to find the source CSS.
171
+ * @param {string} pModuleRoot - Root of the module being staged.
172
+ * @returns {string|null} Absolute source path, or null if not found.
173
+ */
174
+ _resolveAssetSource(pKind, pName, pSpecPath, pModuleRoot)
175
+ {
176
+ if (pKind === 'script')
177
+ {
178
+ return this._resolveBundleSource(pName, pModuleRoot);
179
+ }
180
+ // Stylesheet — look under the package's dist (or root) for a matching
181
+ // CSS file. The spec's Path basename hints at what file to find.
182
+ let tmpBasename = pSpecPath ? libPath.basename(pSpecPath) : (pName ? pName + '.min.css' : null);
183
+ if (!tmpBasename) { return null; }
184
+
185
+ let tmpCandidates = [];
186
+ if (pName)
187
+ {
188
+ tmpCandidates.push(libPath.join(pModuleRoot, 'node_modules', pName, 'dist', tmpBasename));
189
+ tmpCandidates.push(libPath.join(pModuleRoot, 'node_modules', pName, tmpBasename));
190
+ }
191
+ // Also check the module root in case the CSS lives alongside source/
192
+ tmpCandidates.push(libPath.join(pModuleRoot, 'dist', tmpBasename));
193
+ tmpCandidates.push(libPath.join(pModuleRoot, tmpBasename));
194
+
195
+ for (let i = 0; i < tmpCandidates.length; i++)
196
+ {
197
+ if (libFS.existsSync(tmpCandidates[i]))
198
+ {
199
+ return tmpCandidates[i];
200
+ }
201
+ }
202
+ return null;
203
+ }
204
+
205
+ /**
206
+ * Resolve the source UMD bundle for an Import by Name.
207
+ *
208
+ * @param {string} pName - Package name (e.g. "pict", "pict-section-form").
209
+ * @param {string} pModuleRoot - Root of the module being staged.
210
+ * @returns {string|null} Absolute path to the bundle, or null if not found.
211
+ */
212
+ _resolveBundleSource(pName, pModuleRoot)
213
+ {
214
+ let tmpCandidates = [];
215
+
216
+ // 1. Peer dependency: <moduleRoot>/node_modules/<Name>/dist/<Name>.min.js
217
+ tmpCandidates.push(libPath.join(pModuleRoot, 'node_modules', pName, 'dist', `${pName}.min.js`));
218
+
219
+ // 2. Module IS the package being staged: <moduleRoot>/dist/<Name>.min.js
220
+ try
221
+ {
222
+ let tmpPkgPath = libPath.join(pModuleRoot, 'package.json');
223
+ if (libFS.existsSync(tmpPkgPath))
224
+ {
225
+ let tmpPkg = JSON.parse(libFS.readFileSync(tmpPkgPath, 'utf8'));
226
+ if (tmpPkg && tmpPkg.name === pName)
227
+ {
228
+ tmpCandidates.push(libPath.join(pModuleRoot, 'dist', `${pName}.min.js`));
229
+ }
230
+ }
231
+ }
232
+ catch (pError)
233
+ {
234
+ // Non-fatal — fall through to other candidates.
235
+ }
236
+
237
+ // 3. Sibling monorepo checkout — walk up until we find a `modules/`
238
+ // directory, then search modules/<group>/<Name>/dist/<Name>.min.js.
239
+ let tmpUp = pModuleRoot;
240
+ for (let i = 0; i < 5; i++)
241
+ {
242
+ let tmpModulesDir = libPath.join(tmpUp, 'modules');
243
+ if (libFS.existsSync(tmpModulesDir) && libFS.statSync(tmpModulesDir).isDirectory())
244
+ {
245
+ try
246
+ {
247
+ let tmpGroups = libFS.readdirSync(tmpModulesDir);
248
+ for (let g = 0; g < tmpGroups.length; g++)
249
+ {
250
+ let tmpCandidate = libPath.join(tmpModulesDir, tmpGroups[g], pName, 'dist', `${pName}.min.js`);
251
+ tmpCandidates.push(tmpCandidate);
252
+ }
253
+ }
254
+ catch (pError)
255
+ {
256
+ // readdirSync can fail on permission errors; skip silently.
257
+ }
258
+ break;
259
+ }
260
+ let tmpNext = libPath.dirname(tmpUp);
261
+ if (tmpNext === tmpUp)
262
+ {
263
+ break;
264
+ }
265
+ tmpUp = tmpNext;
266
+ }
267
+
268
+ for (let i = 0; i < tmpCandidates.length; i++)
269
+ {
270
+ if (libFS.existsSync(tmpCandidates[i]))
271
+ {
272
+ return tmpCandidates[i];
273
+ }
274
+ }
275
+ return null;
276
+ }
277
+ }
278
+
279
+ module.exports = DocuserveCommandStagePlayground;
@@ -1617,18 +1617,24 @@ class DocuserveDocumentationProvider extends libPictProvider
1617
1617
  }
1618
1618
 
1619
1619
  /**
1620
- * Decide whether the playground panel should be enabled for the
1621
- * given group/module.
1622
- *
1623
- * Opt-in signal: a module's `_sidebar.md` (baked into the catalog as
1624
- * `module.Sidebar`) contains an entry whose Path matches
1625
- * `playground.md` (case-insensitive, anywhere in the tree). When the
1626
- * sidebar contains that entry, code blocks in the module's docs get
1627
- * the "Try in Playground" button and the bottom drawer/tab strip
1628
- * becomes visible; otherwise the drawer is suppressed entirely.
1629
- *
1630
- * Falls back to the root SidebarGroups (standalone mode — docuserve
1631
- * serving a single module's docs directly without a catalog).
1620
+ * Decide whether the **Fable bottom-drawer** playground panel should
1621
+ * be enabled for the given group/module.
1622
+ *
1623
+ * Resolution order:
1624
+ * 1. If `_playground.json` is cached AND its `Kind` is `"section"`,
1625
+ * return false. Section-playground modules use the full-page
1626
+ * `#/playground/section` route and explicitly do NOT want the
1627
+ * Fable JS REPL drawer popping up on every doc page.
1628
+ * 2. Otherwise the opt-in signal is a `playground.md` entry in the
1629
+ * module's sidebar (catalog mode) or the root SidebarGroups
1630
+ * (standalone mode). Presence of that entry drawer + tab
1631
+ * strip + "Try in Playground" buttons on code blocks.
1632
+ *
1633
+ * The cache is populated by `loadPlaygroundConfig()` (called on each
1634
+ * navigation by `_syncPlaygroundVisibility`). On the first visit to
1635
+ * a module, the cache may not yet be populated — this method
1636
+ * conservatively falls through to the sidebar check, and the caller
1637
+ * re-invokes once the async config load resolves.
1632
1638
  *
1633
1639
  * @param {string} [pGroup] - Current group key (may be empty)
1634
1640
  * @param {string} [pModule] - Current module name (may be empty)
@@ -1636,6 +1642,20 @@ class DocuserveDocumentationProvider extends libPictProvider
1636
1642
  */
1637
1643
  isPlaygroundEnabled(pGroup, pModule)
1638
1644
  {
1645
+ // Check the _playground.json cache for an explicit Kind: 'section'
1646
+ // declaration — that module uses the full-page playground and
1647
+ // the Fable drawer must stay out of its way on every doc page,
1648
+ // not just on the playground route itself.
1649
+ let tmpCacheKey = (pGroup || '') + '/' + (pModule || '');
1650
+ if (Object.prototype.hasOwnProperty.call(this._PlaygroundConfigCache, tmpCacheKey))
1651
+ {
1652
+ let tmpConfig = this._PlaygroundConfigCache[tmpCacheKey];
1653
+ if (tmpConfig && tmpConfig.Kind === 'section')
1654
+ {
1655
+ return false;
1656
+ }
1657
+ }
1658
+
1639
1659
  // Per-module sidebar (catalog mode).
1640
1660
  if (pGroup && pModule)
1641
1661
  {