jupyter-specta 0.1.7 → 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 CHANGED
@@ -14,8 +14,9 @@ Specta is a custom JupyterLite app for rendering notebooks and Jupyter‑support
14
14
 
15
15
  Render notebooks in:
16
16
 
17
- - **Dashboard mode** – structured panels for interactive widgets and outputs
18
- - **Article mode** – a minimal, blog-like reading experience
17
+ - **Dashboard mode** – structured panels for interactive widgets and outputs.
18
+ - **Article mode** – a minimal, blog-like reading experience.
19
+ - **Slides mode** – a fullscreen presentation mode.
19
20
 
20
21
  ### Clean Viewer for all Jupyter-supported file types
21
22
 
@@ -25,6 +26,12 @@ View any Jupyter-supported file using Specta's clean viewer with all Jupyter UI
25
26
 
26
27
  A `specta` preview can be launched directly from JupyterLab, letting users verify how their documents will look when published.
27
28
 
29
+ ## Try it online!
30
+
31
+ You can try it online by clicking on this badge:
32
+
33
+ [![Try on lite](https://jupyterlite.rtfd.io/en/latest/_static/badge.svg)](https://trungleduc.github.io/specta/specta/)
34
+
28
35
  ## Installation and Usage
29
36
 
30
37
  You can install `specta` using `pip` or `conda`
@@ -43,10 +50,77 @@ Once installed, you can build your JupyterLite app, a `specta` app will be inclu
43
50
  jupyter lite build
44
51
  ```
45
52
 
46
- Then serve the contents of the output directory (by default `./_output`) using any static file server. You can access the `specta` app at the `/specta/` path.
53
+ Then serve the contents of the output directory (by default `./_output`) using any static file server. You can access the `Specta` app at the `/specta/` path.
47
54
 
48
- ## Try it online!
55
+ ## Specta Configuration
49
56
 
50
- You can try it online by clicking on this badge:
57
+ ### Available layouts
51
58
 
52
- [![Try on lite](https://jupyterlite.rtfd.io/en/latest/_static/badge.svg)](https://trungleduc.github.io/specta/specta/)
59
+ Specta comes with three built-in layouts:
60
+
61
+ - `default`: The default layout, which renders the notebook as a dashboard.
62
+ - `article`: A minimal, blog-like reading experience.
63
+ - `slides`: A fullscreen presentation mode using [reveal.js](https://revealjs.com/).
64
+
65
+ ### Top-level configuration
66
+
67
+ Specta can be configured using the typicall JupyterLite configuration file: `jupyter-lite.json`. You can add a `spectaConfig` key to the `jupyter-config-data` section of this file to customize the Specta app.
68
+
69
+ The following options are available:
70
+
71
+ - `defaultLayout`: The default layout when opening a file.
72
+ - `hideTopbar`: Boolean flag to show or hide the top bar.
73
+ - `topBar`: Configuration for the top bar.
74
+ - `slidesTheme`: The theme for the slides layout. The list of available themes can be found [here](https://revealjs.com/themes/).
75
+
76
+ ```json
77
+ "topBar": {
78
+ "icon": "url to the icon file, it's shown on the left of the top bar",
79
+ "title": "Title on the left of the top bar",
80
+ "themeToggle": "Boolean flag to show/hide the theme selector",
81
+ "textColor": "Color of the text on the top bar",
82
+ "background": "Background color of the top bar"
83
+ },
84
+ ```
85
+
86
+ - `perFileConfig`: an object with key is the file path and value is the above configuration, it's used to have different layout/top bar config for each files, for example:
87
+
88
+ ```json
89
+ "perFileConfig": {
90
+ "blog.ipynb": {
91
+ "hideTopbar": false,
92
+ "defaultLayout": "article",
93
+ "topBar": {
94
+ "title": "My blog",
95
+ "themeToggle": false
96
+ }
97
+ },
98
+ "slides.ipynb": {
99
+ "hideTopbar": true,
100
+ "slidesTheme": "sky"
101
+ }
102
+ }
103
+ ```
104
+
105
+ ### Notebook metadata configuration
106
+
107
+ In addition to the global configuration, you can also configure the layout and top bar for each notebook by using the notebook metadata. You can use the `Specta App Config` of the `Property Inspector` panel of JupyterLab to edit the notebook metadata.
108
+
109
+ ![Metadata](./docs/images/specta-meta.jpg)
110
+
111
+ ### Notebook cell configuration
112
+
113
+ By default, when you open a notebook in Specta, all code cells are hidden, and placeholder skeletons are shown instead at the position of the cell. You can configure the visibility of each cell by using the Specta cell metadata toolbar.
114
+
115
+ ![Cell toolbar](./docs/images/specta-config.jpg)
116
+
117
+ By opening the `Property Inspector` panel of JupyterLab and selecting the `Specta Cell Config` section, you can change the visibility of each cell as follows:
118
+
119
+ - `Show cell source`: use this toggle to show or hide the cell source code. Default to `false`
120
+ - `Show output placeholder`: use this toggle to show or hide the output skeleton. Default to `true`
121
+
122
+ ### Slides layout configuration
123
+
124
+ For the slides layout, you can set the cells as a sub-slide for [vertical slide](https://revealjs.com/vertical-slides/) or [a fragment](https://revealjs.com/fragments/) using the Slide Type field in the `Common Tools` section of the `Property Inspector` panel.
125
+
126
+ ![Slide tool](./docs/images/slide-tool.png)
@@ -35,10 +35,6 @@ export class NotebookGridWidgetFactory extends ABCWidgetFactory {
35
35
  // Specta app, add topbar to layout
36
36
  topbar.id = 'specta-topbar-widget';
37
37
  this._shell.add(topbar, 'top');
38
- if (topbar.parent) {
39
- topbar.parent.node.style.boxShadow =
40
- 'rgba(0 0 0 / 20%) 0 2px 4px -1px, rgba(0 0 0 / 14%) 0 4px 5px 0, rgba(0 0 0 / 12%) 0 1px 10px 0';
41
- }
42
38
  }
43
39
  }
44
40
  else if (isSpecta) {
@@ -10,10 +10,11 @@ export declare class SpectaLayoutRegistry implements ISpectaLayoutRegistry {
10
10
  get selectedLayoutChanged(): ISignal<SpectaLayoutRegistry, {
11
11
  name: string;
12
12
  layout: ISpectaLayout;
13
+ oldLayout?: ISpectaLayout;
13
14
  }>;
14
15
  get(name: string): ISpectaLayout | undefined;
15
16
  getDefaultLayout(): ISpectaLayout;
16
- setSelectedLayout(name: string): void;
17
+ setSelectedLayout(name: string): Promise<void>;
17
18
  register(name: string, layout: ISpectaLayout): void;
18
19
  allLayouts(): string[];
19
20
  private _selectedLayout;
@@ -1,6 +1,7 @@
1
1
  import { Signal } from '@lumino/signaling';
2
2
  import { DefaultLayout } from './default';
3
3
  import { ArticleLayout } from './article';
4
+ import { SlidesLayout } from './slides';
4
5
  export class SpectaLayoutRegistry {
5
6
  constructor() {
6
7
  this._registry = new Map();
@@ -10,6 +11,7 @@ export class SpectaLayoutRegistry {
10
11
  this._registry = new Map();
11
12
  this._registry.set('default', defaultLayout);
12
13
  this._registry.set('article', new ArticleLayout());
14
+ this._registry.set('slides', new SlidesLayout());
13
15
  this._selectedLayout = {
14
16
  name: 'default',
15
17
  layout: defaultLayout
@@ -30,12 +32,13 @@ export class SpectaLayoutRegistry {
30
32
  getDefaultLayout() {
31
33
  return this._registry.get('default');
32
34
  }
33
- setSelectedLayout(name) {
35
+ async setSelectedLayout(name) {
34
36
  if (!this._registry.has(name)) {
35
37
  throw new Error(`Layout with name ${name} does not exist`);
36
38
  }
39
+ const oldLayout = this._selectedLayout.layout;
37
40
  this._selectedLayout = { name, layout: this._registry.get(name) };
38
- this._selectedLayoutChanged.emit(this._selectedLayout);
41
+ this._selectedLayoutChanged.emit(Object.assign(Object.assign({}, this._selectedLayout), { oldLayout }));
39
42
  }
40
43
  register(name, layout) {
41
44
  if (this._registry.has(name)) {
@@ -0,0 +1,14 @@
1
+ import { Panel } from '@lumino/widgets';
2
+ import { SpectaCellOutput } from '../specta_cell_output';
3
+ import * as nbformat from '@jupyterlab/nbformat';
4
+ import { ISpectaAppConfig, ISpectaLayout } from '../token';
5
+ export declare class SlidesLayout implements ISpectaLayout {
6
+ render(options: {
7
+ host: Panel;
8
+ items: SpectaCellOutput[];
9
+ notebook: nbformat.INotebookContent;
10
+ readyCallback: () => Promise<void>;
11
+ spectaConfig: ISpectaAppConfig;
12
+ }): Promise<void>;
13
+ private _deckMap;
14
+ }
@@ -0,0 +1,123 @@
1
+ import { Panel, Widget } from '@lumino/widgets';
2
+ import Reveal from 'reveal.js';
3
+ import { emitResizeEvent, setRevealTheme } from '../tool';
4
+ class HostPanel extends Panel {
5
+ constructor() {
6
+ super();
7
+ this.addClass('specta-slides-host-widget');
8
+ this.addClass('reveal');
9
+ this._outputs = new Panel();
10
+ this._outputs.addClass('slides');
11
+ this.addWidget(this._outputs);
12
+ }
13
+ addOutput(slideEl) {
14
+ const { type, widget } = slideEl;
15
+ const sectionWidget = new Widget({
16
+ node: document.createElement('section')
17
+ });
18
+ switch (type) {
19
+ case 'slide': {
20
+ const outputWidget = widget[0];
21
+ sectionWidget.node.appendChild(outputWidget.node);
22
+ outputWidget.parent = sectionWidget;
23
+ sectionWidget.processMessage = msg => {
24
+ outputWidget.processMessage(msg);
25
+ };
26
+ break;
27
+ }
28
+ case 'subslide': {
29
+ for (const outputWidget of widget) {
30
+ const subSection = document.createElement('section');
31
+ subSection.appendChild(outputWidget.node);
32
+ outputWidget.parent = sectionWidget;
33
+ sectionWidget.node.appendChild(subSection);
34
+ }
35
+ sectionWidget.processMessage = msg => {
36
+ widget.forEach(w => w.processMessage(msg));
37
+ };
38
+ break;
39
+ }
40
+ case 'fragment': {
41
+ for (const [idx, outputWidget] of widget.entries()) {
42
+ if (idx !== 0) {
43
+ outputWidget.addClass('fragment');
44
+ outputWidget.addClass('fade-in');
45
+ }
46
+ sectionWidget.node.appendChild(outputWidget.node);
47
+ outputWidget.parent = sectionWidget;
48
+ }
49
+ sectionWidget.processMessage = msg => {
50
+ widget.forEach(w => w.processMessage(msg));
51
+ };
52
+ break;
53
+ }
54
+ default:
55
+ break;
56
+ }
57
+ this._outputs.addWidget(sectionWidget);
58
+ }
59
+ }
60
+ export class SlidesLayout {
61
+ constructor() {
62
+ this._deckMap = new WeakMap();
63
+ }
64
+ async render(options) {
65
+ var _a, _b, _c;
66
+ const theme = options.spectaConfig.slidesTheme;
67
+ if (theme) {
68
+ setRevealTheme(theme);
69
+ }
70
+ const { host, items, readyCallback } = options;
71
+ const hostPanel = new HostPanel();
72
+ const elementList = [];
73
+ for (const el of items) {
74
+ const info = el.info;
75
+ const cellMeta = ((_b = (_a = info.cellModel) === null || _a === void 0 ? void 0 : _a.metadata) !== null && _b !== void 0 ? _b : {});
76
+ const slideType = (_c = cellMeta === null || cellMeta === void 0 ? void 0 : cellMeta.slideshow) === null || _c === void 0 ? void 0 : _c.slide_type;
77
+ if (!info.hidden) {
78
+ const lastEl = elementList[elementList.length - 1];
79
+ switch (slideType) {
80
+ case 'subslide': {
81
+ if ((lastEl === null || lastEl === void 0 ? void 0 : lastEl.type) === 'subslide') {
82
+ lastEl.widget.push(el);
83
+ }
84
+ else {
85
+ elementList.push({ type: 'subslide', widget: [el] });
86
+ }
87
+ break;
88
+ }
89
+ case 'fragment': {
90
+ if ((lastEl === null || lastEl === void 0 ? void 0 : lastEl.type) === 'fragment') {
91
+ lastEl.widget.push(el);
92
+ }
93
+ else {
94
+ elementList.push({ type: 'fragment', widget: [el] });
95
+ }
96
+ break;
97
+ }
98
+ case 'slide': {
99
+ elementList.push({ type: 'slide', widget: [el] });
100
+ break;
101
+ }
102
+ default: {
103
+ elementList.push({ type: 'slide', widget: [el] });
104
+ break;
105
+ }
106
+ }
107
+ }
108
+ }
109
+ for (const element of elementList) {
110
+ hostPanel.addOutput(element);
111
+ }
112
+ host.addWidget(hostPanel);
113
+ await readyCallback();
114
+ const deck = new Reveal(hostPanel.node, {
115
+ embedded: true
116
+ });
117
+ deck.initialize();
118
+ deck.on('slidetransitionend', event => {
119
+ emitResizeEvent();
120
+ });
121
+ this._deckMap.set(host.node, deck);
122
+ }
123
+ }
@@ -6,7 +6,6 @@ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
6
6
  import { ServiceManager } from '@jupyterlab/services';
7
7
  import { IExecuteReplyMsg } from '@jupyterlab/services/lib/kernel/messages';
8
8
  import { SpectaCellOutput } from './specta_cell_output';
9
- export declare const VIEW = "grid_default";
10
9
  export declare class AppModel {
11
10
  private options;
12
11
  constructor(options: AppModel.IOptions);
@@ -2,8 +2,7 @@ import { CodeCell, MarkdownCell, RawCell } from '@jupyterlab/cells';
2
2
  import { OutputAreaModel, SimplifiedOutputArea } from '@jupyterlab/outputarea';
3
3
  import { createNotebookContext, createNotebookPanel } from './create_notebook_panel';
4
4
  import { SpectaCellOutput } from './specta_cell_output';
5
- import { readCellConfig } from './tool';
6
- export const VIEW = 'grid_default';
5
+ import { emitResizeEvent, readCellConfig } from './tool';
7
6
  export class AppModel {
8
7
  constructor(options) {
9
8
  this.options = options;
@@ -148,6 +147,7 @@ export class AppModel {
148
147
  const source = cell.sharedModel.source;
149
148
  const rep = await SimplifiedOutputArea.execute(source, output, this._context.sessionContext);
150
149
  output.future.done.then(() => {
150
+ emitResizeEvent();
151
151
  outputWrapper.removePlaceholder();
152
152
  });
153
153
  return rep;
@@ -1,6 +1,6 @@
1
1
  import { PromiseDelegate } from '@lumino/coreutils';
2
2
  import { Panel, Widget } from '@lumino/widgets';
3
- import { hideAppLoadingIndicator, isSpectaApp } from './tool';
3
+ import { emitResizeEvent, hideAppLoadingIndicator, isSpectaApp } from './tool';
4
4
  export class AppWidget extends Panel {
5
5
  constructor(options) {
6
6
  super();
@@ -14,16 +14,14 @@ export class AppWidget extends Panel {
14
14
  this._layoutRegistry = options.layoutRegistry;
15
15
  this._host = new Panel();
16
16
  this._host.addClass('specta-output-host');
17
+ this.addClass('specta-app-widget');
17
18
  this.addWidget(this._host);
18
- this.node.style.overflow = 'auto';
19
19
  if (!isSpectaApp()) {
20
20
  // Not a specta app, add spinner
21
21
  this.addSpinner();
22
22
  }
23
23
  this._model.initialize().then(() => {
24
- this.render()
25
- .catch(console.error)
26
- .then(() => window.dispatchEvent(new Event('resize')));
24
+ this.render().catch(console.error).then(emitResizeEvent);
27
25
  });
28
26
  this._layoutRegistry.selectedLayoutChanged.connect(this._onSelectedLayoutChanged, this);
29
27
  }
@@ -85,7 +83,8 @@ export class AppWidget extends Panel {
85
83
  host: this._host,
86
84
  items: this._outputs,
87
85
  notebook: (_e = this._model.context) === null || _e === void 0 ? void 0 : _e.model.toJSON(),
88
- readyCallback
86
+ readyCallback,
87
+ spectaConfig: this._spectaAppConfig
89
88
  });
90
89
  }
91
90
  onCloseRequest(msg) {
@@ -94,16 +93,18 @@ export class AppWidget extends Panel {
94
93
  }
95
94
  _onSelectedLayoutChanged(sender, args) {
96
95
  var _a;
96
+ const { layout } = args;
97
97
  const currentEls = [...this._host.widgets];
98
98
  currentEls.forEach(el => {
99
99
  var _a;
100
100
  (_a = this._host.layout) === null || _a === void 0 ? void 0 : _a.removeWidget(el);
101
101
  });
102
- args.layout.render({
102
+ layout.render({
103
103
  host: this._host,
104
104
  items: this._outputs,
105
105
  notebook: (_a = this._model.context) === null || _a === void 0 ? void 0 : _a.model.toJSON(),
106
- readyCallback: async () => { }
106
+ readyCallback: async () => { },
107
+ spectaConfig: this._spectaAppConfig
107
108
  });
108
109
  }
109
110
  }
@@ -8,11 +8,17 @@ export class SpectaWidgetFactory {
8
8
  this._options = options;
9
9
  }
10
10
  async createNew(options) {
11
- var _a;
11
+ var _a, _b;
12
12
  const { context } = options;
13
13
  const rendermime = this._options.rendermime.clone({
14
14
  resolver: context.urlResolver
15
15
  });
16
+ const spectaConfig = readSpectaConfig({
17
+ nbMetadata: context.model.metadata,
18
+ nbPath: (_a = context.contentsModel) === null || _a === void 0 ? void 0 : _a.path
19
+ });
20
+ const defaultLayout = (_b = spectaConfig.defaultLayout) !== null && _b !== void 0 ? _b : 'default';
21
+ this._options.spectaLayoutRegistry.setSelectedLayout(defaultLayout);
16
22
  const model = new AppModel({
17
23
  context,
18
24
  manager: this._options.manager,
@@ -24,10 +30,7 @@ export class SpectaWidgetFactory {
24
30
  notebookConfig: StaticNotebook.defaultNotebookConfig,
25
31
  editorServices: this._options.editorServices
26
32
  });
27
- const spectaConfig = readSpectaConfig({
28
- nbMetadata: context.model.metadata,
29
- nbPath: (_a = context.contentsModel) === null || _a === void 0 ? void 0 : _a.path
30
- });
33
+ // Create the specta pane
31
34
  const panel = new AppWidget({
32
35
  id: UUID.uuid4(),
33
36
  label: '',
package/lib/token.d.ts CHANGED
@@ -14,6 +14,7 @@ export interface ISpectaLayout {
14
14
  items: SpectaCellOutput[];
15
15
  notebook: nbformat.INotebookContent;
16
16
  readyCallback: () => Promise<void>;
17
+ spectaConfig: ISpectaAppConfig;
17
18
  }): Promise<void>;
18
19
  }
19
20
  export interface ISpectaLayoutRegistry {
@@ -30,6 +31,7 @@ export interface ISpectaLayoutRegistry {
30
31
  selectedLayoutChanged: ISignal<ISpectaLayoutRegistry, {
31
32
  name: string;
32
33
  layout: ISpectaLayout;
34
+ oldLayout?: ISpectaLayout;
33
35
  }>;
34
36
  }
35
37
  export interface ITopbarConfig {
@@ -38,12 +40,15 @@ export interface ITopbarConfig {
38
40
  title?: string;
39
41
  icon?: string;
40
42
  kernelActivity?: boolean;
43
+ settingsButton?: boolean;
41
44
  themeToggle?: boolean;
45
+ layoutToggle?: boolean;
42
46
  }
43
47
  export interface ISpectaAppConfig {
44
48
  topBar?: ITopbarConfig;
45
49
  defaultLayout?: string;
46
50
  hideTopbar?: boolean;
51
+ slidesTheme?: string;
47
52
  }
48
53
  export interface ISpectaCellConfig {
49
54
  showSource?: boolean;
package/lib/tool.d.ts CHANGED
@@ -21,9 +21,14 @@ export declare function createFileBrowser(options: {
21
21
  defaultBrowser: IDefaultFileBrowser;
22
22
  }): any;
23
23
  export declare function hideAppLoadingIndicator(): void;
24
+ export declare function mergeObjects(...objects: Record<string, any>[]): Record<string, any>;
25
+ export declare function getSpectaAssetUrl(path: string): string;
24
26
  export declare function isSpectaApp(): boolean;
25
27
  export declare function readSpectaConfig({ nbMetadata, nbPath }: {
26
28
  nbMetadata?: INotebookMetadata;
27
29
  nbPath?: string | null;
28
30
  }): ISpectaAppConfig;
29
31
  export declare function readCellConfig(cell?: ICell): Required<ISpectaCellConfig>;
32
+ export declare function debounce<T extends (...args: any[]) => void>(fn: T, delay?: number): (...args: Parameters<T>) => void;
33
+ export declare const emitResizeEvent: () => void;
34
+ export declare function setRevealTheme(themeName: string): void;
package/lib/tool.js CHANGED
@@ -87,6 +87,22 @@ export function hideAppLoadingIndicator() {
87
87
  }, 1000);
88
88
  }
89
89
  }
90
+ export function mergeObjects(...objects) {
91
+ const result = {};
92
+ for (const obj of objects) {
93
+ for (const [key, value] of Object.entries(obj)) {
94
+ if (value !== null && value !== undefined) {
95
+ result[key] = value;
96
+ }
97
+ }
98
+ }
99
+ return result;
100
+ }
101
+ export function getSpectaAssetUrl(path) {
102
+ const labExtension = PageConfig.getOption('fullLabextensionsUrl');
103
+ const url = URLExt.join(labExtension, 'jupyter-specta', 'static', path);
104
+ return url;
105
+ }
90
106
  export function isSpectaApp() {
91
107
  return !!document.querySelector('meta[name="specta-config"]');
92
108
  }
@@ -96,13 +112,24 @@ export function readSpectaConfig({ nbMetadata, nbPath }) {
96
112
  if (!rawConfig || rawConfig.length === 0) {
97
113
  rawConfig = '{}';
98
114
  }
115
+ let pathWithoutDrive = nbPath;
116
+ const paths = nbPath === null || nbPath === void 0 ? void 0 : nbPath.split(':');
117
+ if (paths && paths.length > 1) {
118
+ pathWithoutDrive = paths[1];
119
+ }
99
120
  const _b = JSON.parse(rawConfig), { perFileConfig } = _b, globalConfig = __rest(_b, ["perFileConfig"]);
100
121
  let spectaConfig = Object.assign({}, (globalConfig !== null && globalConfig !== void 0 ? globalConfig : {}));
101
- if (perFileConfig && nbPath && perFileConfig[nbPath]) {
102
- spectaConfig = Object.assign(Object.assign({}, spectaConfig), perFileConfig[nbPath]);
122
+ if (perFileConfig && pathWithoutDrive && perFileConfig[pathWithoutDrive]) {
123
+ spectaConfig = Object.assign(Object.assign({}, spectaConfig), perFileConfig[pathWithoutDrive]);
103
124
  }
104
125
  const spectaMetadata = ((_a = nbMetadata === null || nbMetadata === void 0 ? void 0 : nbMetadata.specta) !== null && _a !== void 0 ? _a : {});
105
- return Object.assign(Object.assign({}, spectaConfig), spectaMetadata);
126
+ if (spectaMetadata.hideTopbar === 'Yes') {
127
+ spectaMetadata.hideTopbar = true;
128
+ }
129
+ else if (spectaMetadata.hideTopbar === 'No') {
130
+ spectaMetadata.hideTopbar = false;
131
+ }
132
+ return mergeObjects(spectaConfig, spectaMetadata);
106
133
  }
107
134
  export function readCellConfig(cell) {
108
135
  var _a, _b;
@@ -119,3 +146,27 @@ export function readCellConfig(cell) {
119
146
  }
120
147
  return spectaCellConfig;
121
148
  }
149
+ export function debounce(fn, delay = 100) {
150
+ let timeoutId;
151
+ return (...args) => {
152
+ clearTimeout(timeoutId);
153
+ timeoutId = setTimeout(() => {
154
+ fn(...args);
155
+ }, delay);
156
+ };
157
+ }
158
+ export const emitResizeEvent = debounce(() => {
159
+ window.dispatchEvent(new Event('resize'));
160
+ });
161
+ export function setRevealTheme(themeName) {
162
+ let themeLink = document.getElementById('reveal-theme');
163
+ if (!themeLink) {
164
+ // Create <link> tag if it doesn't exist
165
+ themeLink = document.createElement('link');
166
+ themeLink.rel = 'stylesheet';
167
+ themeLink.id = 'reveal-theme';
168
+ document.head.appendChild(themeLink);
169
+ }
170
+ // Set or update href to new theme
171
+ themeLink.href = getSpectaAssetUrl(`reveal.js/${themeName}.css`);
172
+ }
@@ -30,9 +30,5 @@ export const topbarPlugin = {
30
30
  widget.id = 'specta-topbar-widget';
31
31
  widget.addClass('specta-topbar-element');
32
32
  app.shell.add(widget, 'top');
33
- if (widget.parent) {
34
- widget.parent.node.style.boxShadow =
35
- 'rgba(0 0 0 / 20%) 0 2px 4px -1px, rgba(0 0 0 / 14%) 0 4px 5px 0, rgba(0 0 0 / 12%) 0 1px 10px 0';
36
- }
37
33
  }
38
34
  };
@@ -1,7 +1,8 @@
1
1
  import { IThemeManager } from '@jupyterlab/apputils';
2
2
  import React from 'react';
3
- import { ISpectaLayoutRegistry } from '../token';
3
+ import { ISpectaLayoutRegistry, ITopbarConfig } from '../token';
4
4
  export declare const SettingContent: (props: {
5
+ config?: ITopbarConfig;
5
6
  themeManager?: IThemeManager;
6
7
  layoutRegistry?: ISpectaLayoutRegistry;
7
8
  }) => React.JSX.Element;
@@ -1,7 +1,7 @@
1
1
  import React, { useState, useEffect, useCallback } from 'react';
2
2
  import { Divider } from '../components/divider';
3
3
  export const SettingContent = (props) => {
4
- var _a, _b, _c, _d, _e;
4
+ var _a, _b, _c, _d, _e, _f, _g;
5
5
  const { themeManager, layoutRegistry } = props;
6
6
  const [themeOptions, setThemeOptions] = useState([
7
7
  ...((_a = themeManager === null || themeManager === void 0 ? void 0 : themeManager.themes) !== null && _a !== void 0 ? _a : [])
@@ -54,7 +54,10 @@ export const SettingContent = (props) => {
54
54
  return (React.createElement("div", { style: { padding: '0 10px' } },
55
55
  React.createElement("p", { style: { marginTop: 0, marginBottom: '5px', fontSize: '1rem' } }, "SETTINGS"),
56
56
  React.createElement(Divider, null),
57
- layoutRegistry && (React.createElement("div", null,
57
+ (((_f = props.config) === null || _f === void 0 ? void 0 : _f.layoutToggle) !== undefined
58
+ ? props.config.layoutToggle
59
+ : true) &&
60
+ layoutRegistry && (React.createElement("div", null,
58
61
  React.createElement("label", { htmlFor: "" }, "Select layout"),
59
62
  React.createElement("div", { className: "jp-select-wrapper" },
60
63
  React.createElement("select", { className: " jp-mod-styled specta-topbar-theme", onChange: onLayoutChange, value: selectedLayout }, layoutOptions.map(el => {
@@ -62,7 +65,10 @@ export const SettingContent = (props) => {
62
65
  background: 'var(--jp-layout-color2)'
63
66
  } }, el.charAt(0).toUpperCase() + el.slice(1)));
64
67
  }))))),
65
- themeManager && (React.createElement("div", null,
68
+ (((_g = props.config) === null || _g === void 0 ? void 0 : _g.themeToggle) !== undefined
69
+ ? props.config.themeToggle
70
+ : true) &&
71
+ themeManager && (React.createElement("div", null,
66
72
  React.createElement("label", { htmlFor: "" }, "Select theme"),
67
73
  React.createElement("div", { className: "jp-select-wrapper" },
68
74
  React.createElement("select", { className: " jp-mod-styled specta-topbar-theme", onChange: onThemeChange, value: selectedTheme }, themeOptions.map(el => {
@@ -3,15 +3,16 @@ import { GearIcon } from '../components/icon/gear';
3
3
  import { IconButton } from '../components/iconButton';
4
4
  import { SettingContent } from './settingDialog';
5
5
  export function TopbarElement(props) {
6
- var _a, _b;
6
+ var _a, _b, _c;
7
7
  const config = React.useMemo(() => {
8
- var _a, _b, _c, _d, _e, _f, _g, _h;
8
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
9
9
  return {
10
10
  background: (_b = (_a = props.config) === null || _a === void 0 ? void 0 : _a.background) !== null && _b !== void 0 ? _b : 'var(--jp-layout-color2)',
11
11
  title: (_d = (_c = props.config) === null || _c === void 0 ? void 0 : _c.title) !== null && _d !== void 0 ? _d : 'Specta',
12
12
  themeToggle: Boolean((_e = props.config) === null || _e === void 0 ? void 0 : _e.themeToggle),
13
13
  kernelActivity: Boolean((_f = props.config) === null || _f === void 0 ? void 0 : _f.kernelActivity),
14
- textColor: (_h = (_g = props.config) === null || _g === void 0 ? void 0 : _g.textColor) !== null && _h !== void 0 ? _h : 'var(--jp-ui-font-color1)'
14
+ textColor: (_h = (_g = props.config) === null || _g === void 0 ? void 0 : _g.textColor) !== null && _h !== void 0 ? _h : 'var(--jp-ui-font-color1)',
15
+ icon: (_j = props.config) === null || _j === void 0 ? void 0 : _j.icon
15
16
  };
16
17
  }, [props.config]);
17
18
  const [open, setOpen] = useState(false);
@@ -31,11 +32,13 @@ export function TopbarElement(props) {
31
32
  }, []);
32
33
  return (React.createElement("div", { className: "specta-topbar", style: { background: (_a = config.background) !== null && _a !== void 0 ? _a : 'var(--jp-layout-color2)' } },
33
34
  React.createElement("div", { className: "specta-topbar-left" },
34
- React.createElement("div", null, config.icon && React.createElement("img", { style: { width: '50px' }, src: config.icon })),
35
+ React.createElement("div", { className: "specta-topbar-icon-container" }, config.icon && React.createElement("img", { style: { height: '100%' }, src: config.icon })),
35
36
  React.createElement("div", { className: "specta-topbar-title", style: { color: (_b = config.textColor) !== null && _b !== void 0 ? _b : 'var(--jp-ui-font-color1)' } }, config.title)),
36
- React.createElement("div", { className: "specta-topbar-right" },
37
- React.createElement(IconButton, { ref: buttonRef, onClick: () => setOpen(!open), icon: React.createElement(GearIcon, { fill: "var(--jp-ui-font-color2)", height: 24, width: 24 }) }),
37
+ (((_c = props.config) === null || _c === void 0 ? void 0 : _c.settingsButton) !== undefined
38
+ ? props.config.settingsButton
39
+ : true) && (React.createElement("div", { className: "specta-topbar-right" },
40
+ React.createElement(IconButton, { ref: buttonRef, onClick: () => setOpen(!open), icon: React.createElement(GearIcon, { fill: "var(--jp-ui-font-color2)", height: 23, width: 23 }) }),
38
41
  open && (React.createElement("div", { ref: dialogRef, className: "jp-Dialog-content specta-config-dialog" },
39
42
  React.createElement("div", { className: "specta-config-arrow" }),
40
- React.createElement(SettingContent, { themeManager: props.themeManager, layoutRegistry: props.layoutRegistry }))))));
43
+ React.createElement(SettingContent, { config: props.config, themeManager: props.themeManager })))))));
41
44
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jupyter-specta",
3
- "version": "0.1.7",
3
+ "version": "0.2.0",
4
4
  "description": "",
5
5
  "keywords": [],
6
6
  "homepage": "https://github.com/trungleduc/specta",
@@ -28,8 +28,8 @@
28
28
  "scripts": {
29
29
  "clean": "rimraf specta/labextension && rimraf app/specta/build && rimraf specta/app_static",
30
30
  "build:lib": "tsc",
31
- "build:labextension": "jupyter labextension build .",
32
- "build:labextension:dev": "jupyter labextension build --development True .",
31
+ "build:labextension": "jupyter labextension build . && npm run copy:reveal",
32
+ "build:labextension:dev": "jupyter labextension build --development True . && npm run copy:reveal",
33
33
  "build:lab": "npm run clean && npm run build:lib && npm run build:labextension",
34
34
  "build:app": "npm run clean && npm run build:lib && cd app && NODE_OPTIONS=--max-old-space-size=4096 NODE_ENV=production webpack --mode production",
35
35
  "build:app:dev": "npm run clean && npm run build:lib && cd app && webpack",
@@ -37,9 +37,13 @@
37
37
  "build:all:dev": "npm run build:app:dev && npm run build:labextension:dev",
38
38
  "build:demo": "cd demo && rm -rf .jupyterlite.doit.db _output && jupyter lite build .",
39
39
  "update:demo": "node script/build-dev.mjs",
40
+ "copy:reveal": "node script/copy-reveal.mjs",
40
41
  "lint": "npm run lint:prettier && npm run lint:eslint",
42
+ "lint:check": "npm run lint:prettier:check && npm run lint:eslint:check",
41
43
  "lint:prettier": "prettier --no-error-on-unmatched-pattern --write \"**/*{.ts,.tsx,.jsx,.css,.json,.md,.yml}\"",
42
- "lint:eslint": "eslint --ext .ts,.tsx .",
44
+ "lint:prettier:check": "prettier --no-error-on-unmatched-pattern --check \"**/*{.ts,.tsx,.jsx,.css,.json,.md,.yml}\"",
45
+ "lint:eslint": "eslint --ext .ts,.tsx . --fix",
46
+ "lint:eslint:check": "eslint --ext .ts,.tsx .",
43
47
  "install:extension": "npm run build:lib && npm run build:labextension:dev"
44
48
  },
45
49
  "overrides": {
@@ -106,9 +110,11 @@
106
110
  "@lumino/signaling": "^2.0.0",
107
111
  "@lumino/virtualdom": "^2.0.0",
108
112
  "@lumino/widgets": "^2.0.0",
113
+ "@types/reveal.js": "^5.2.0",
109
114
  "@voila-dashboards/voila": "^0.5.5",
110
115
  "react": "^18.3.0",
111
- "react-dom": "^18.3.0"
116
+ "react-dom": "^18.3.0",
117
+ "reveal.js": "^5.2.1"
112
118
  },
113
119
  "devDependencies": {
114
120
  "@jupyterlab/builder": "~4.4.2",
@@ -12,8 +12,34 @@
12
12
  "/specta/defaultLayout": {
13
13
  "title": "Page layout",
14
14
  "type": "string",
15
- "enum": ["default", "article"],
15
+ "enum": ["default", "article", "slides"],
16
16
  "default": "default"
17
+ },
18
+ "/specta/hideTopbar": {
19
+ "title": "Hide topbar",
20
+ "type": "string",
21
+ "enum": ["Yes", "No", null],
22
+ "default": null
23
+ },
24
+ "/specta/slidesTheme": {
25
+ "title": "Slides layout theme",
26
+ "type": "string",
27
+ "enum": [
28
+ null,
29
+ "beige",
30
+ "black",
31
+ "blood",
32
+ "dracula",
33
+ "league",
34
+ "moon",
35
+ "night",
36
+ "serif",
37
+ "simple",
38
+ "sky",
39
+ "solarized",
40
+ "white"
41
+ ],
42
+ "default": null
17
43
  }
18
44
  }
19
45
  },
@@ -25,6 +51,10 @@
25
51
  "/specta/defaultLayout": {
26
52
  "metadataLevel": "notebook",
27
53
  "writeDefault": false
54
+ },
55
+ "/specta/slidesTheme": {
56
+ "metadataLevel": "notebook",
57
+ "writeDefault": false
28
58
  }
29
59
  }
30
60
  }
package/style/base.css CHANGED
@@ -52,14 +52,24 @@
52
52
  left: calc(50% - 52px);
53
53
  color: var(--jp-ui-font-color1) !important;
54
54
  }
55
-
55
+ .specta-app-widget {
56
+ display: flex;
57
+ flex-grow: 1;
58
+ flex-direction: column;
59
+ }
56
60
  .jp-specta-notebook-panel {
57
61
  overflow: auto;
58
62
  padding: 0 5px 5px 5px;
63
+ display: flex;
64
+ flex-direction: column;
65
+ }
66
+
67
+ .specta-output-host {
68
+ flex-grow: 1;
59
69
  }
60
70
 
61
71
  #specta-top-panel {
62
- min-height: 40px;
72
+ min-height: 36px;
63
73
  display: flex;
64
74
  box-shadow: unset !important;
65
75
  z-index: 100;
@@ -74,12 +84,9 @@
74
84
  .specta-topbar {
75
85
  display: flex;
76
86
  flex-direction: row;
77
- height: 40px;
87
+ height: 36px;
78
88
  width: calc(100% + 20px);
79
- box-shadow:
80
- rgba(0 0 0 / 20%) 0 2px 4px -1px,
81
- rgba(0 0 0 / 14%) 0 4px 5px 0,
82
- rgba(0 0 0 / 12%) 0 1px 10px 0;
89
+ border-bottom: solid 0.5px var(--jp-border-color1);
83
90
  gap: 10px;
84
91
  align-items: center;
85
92
  justify-content: space-between;
@@ -91,15 +98,26 @@
91
98
  display: flex;
92
99
  align-items: center;
93
100
  gap: 0.5rem;
101
+ height: 80%;
102
+ }
103
+
104
+ .specta-topbar-icon-container {
105
+ height: 100%;
94
106
  }
95
107
 
96
108
  .specta-topbar-right {
97
109
  position: relative;
98
110
  }
99
111
 
112
+ .specta-topbar-right .jp-Dialog-content {
113
+ min-height: unset !important;
114
+ }
115
+
100
116
  .specta-topbar-title {
101
- line-height: 40px;
102
- font-size: 1.5rem;
117
+ line-height: 36px;
118
+ font-size: 1.25rem;
119
+ font-family: 'Quicksand', sans-serif;
120
+ font-weight: 500;
103
121
  }
104
122
 
105
123
  .specta-topbar-theme {
package/style/index.css CHANGED
@@ -1,3 +1,6 @@
1
1
  @import url('base.css');
2
2
  @import url('article.css');
3
+ @import url('slides.css');
3
4
  @import url('skeleton.css');
5
+ @import url('reveal.js/dist/reveal.css');
6
+ @import url('reveal.js/dist/theme/white.css');
package/style/index.js CHANGED
@@ -1,3 +1,6 @@
1
1
  import './base.css';
2
2
  import './article.css';
3
- import './skeleton.css';
3
+ import './slides.css';
4
+ import './skeleton.css';
5
+ import 'reveal.js/dist/reveal.css';
6
+ import 'reveal.js/dist/theme/white.css';
@@ -0,0 +1,30 @@
1
+ .specta-slides-host-widget {
2
+ --jp-content-font-family: 'Source Serif 4', Georgia, serif;
3
+ --jp-code-font-family:
4
+ 'Source Code Pro', Menlo, Monaco, 'Courier New', Courier, monospace;
5
+ --jp-code-font-size: 14px;
6
+ }
7
+
8
+ .specta-slides-host-widget.reveal {
9
+ .jp-Collapser-child,
10
+ .jp-collapseHeadingButton,
11
+ .jp-InputPrompt,
12
+ .jp-InputCollapser,
13
+ .jp-InternalAnchorLink {
14
+ display: none;
15
+ }
16
+
17
+ .jp-CodeMirrorEditor {
18
+ text-align: start !important;
19
+ }
20
+ h1:last-child,
21
+ h2:last-child,
22
+ h3:last-child,
23
+ h4:last-child,
24
+ h5:last-child,
25
+ h6:last-child {
26
+ margin-bottom: calc(
27
+ 0.5 * var(--jp-content-heading-margin-bottom)
28
+ ) !important;
29
+ }
30
+ }
package/style/style.css CHANGED
@@ -4,7 +4,7 @@
4
4
  background: var(--jp-layout-color1);
5
5
  }
6
6
 
7
- .specta-document-viewer #jp-main-content-panel jp-toolbar {
7
+ .specta-document-viewer #jp-main-content-panel jp-toolbar:first-of-type {
8
8
  display: none;
9
9
  }
10
10
 
@@ -30,4 +30,4 @@
30
30
  .specta-main-content-panel {
31
31
  padding-left: 5px;
32
32
  padding-right: 5px;
33
- }
33
+ }