Skip to content

Latest commit

 

History

History
586 lines (528 loc) · 14.7 KB

website.md

File metadata and controls

586 lines (528 loc) · 14.7 KB

Website

notebook.json

{
  "bundleFiles": [
    ["codemirror-bundle.md", "codemirror-bundle.js"]
  ],
  "importFiles": [
    ["loader.md", "builder.js"],
    ["forms.md", "button-group.js"],
    ["code-edit.md", "code-edit.js"],
    ["menu.md", "dropdown.js"]
  ],
  "dataFiles": [
    ["font.woff2.md", "font.woff2"]
  ]
}

overview.md

# Overview

## Play

- Explore Ristretto, a web app with example data and components that work with each type of data
- Examine the iframe sandbox container which controls access to outside resources
- Upload data and interact with it using the included components
- Customize the components and build your own

## Build

- Set up playgrounds with tabbed, gist-style, and directory tree layouts
- Make API requests, function calls, and database queries with testing
- Write notebooks containing prose with embedded components
- Create chat interfaces to use with a chatbot, other people, or an AI

## Embed

- Integrate a tiny web component that wraps an iframe which loads content from Markdown
- Use different form factors, including tiny view that opens to full overlay window
- Have it load minimal data for viewing, and more data for editing
- Integrate services using Message Channels
- Save your edits using your own backend stack

notebook-code.js

export class NotebookCode extends HTMLElement {
  constructor() {
    super()
    this.attachShadow({mode: 'open'})
    const toolbar = document.createElement('div')
    toolbar.classList.add('toolbar')
    const selectContainer = document.createElement('div')
    const iconContainer = document.createElement('div')
    const closeBtn = document.createElement(
      'button'
    )
    closeBtn.innerHTML = this.icons.close
    closeBtn.addEventListener('click', () => {
      this.onHide()
    })
    iconContainer.append(closeBtn)
    iconContainer.classList.add('icon-container')
    toolbar.append(selectContainer, iconContainer)
    const editorContainer = document.createElement('div')
    editorContainer.classList.add('editor-container')
    this.editor = document.createElement('m-editor-code-edit')
    this.editor.fileType = 'md'
    editorContainer.append(this.editor)
    this.shadowRoot.append(toolbar, editorContainer)
  }

  connectedCallback() {
    const style = document.createElement('style')
    style.textContent = `
      :host {
        display: grid;
        grid-template-rows: auto 1fr;
      }
      .toolbar {
        background: #111;
        color: #e7e7e7;
        display: grid;
        grid-template-columns: 1fr auto;
        padding: 3px;
      }
      .icon-container {
        display: flex;
      }
      .icon-container button {
        background: inherit;
        color: inherit;
        border: none;
      }
      .icon-container svg {
        height: 20px;
        width: 20px;
      }
      .editor-container {
        display: flex;
        flex-direction: column;
        align-items: flex;
        overflow-y: scroll;
      }
      .editor-container m-editor-code-edit {
        flex-grow: 1;
      }
    `
    this.shadowRoot.append(style)
  }

  set value(value) {
    this.editor.value = value
  }

  get value() {
    return this.editor.value
  }

  icons = {
    close: `
      <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24">
        <path d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z"/>
      </svg>
    `,
  }
}

toolbar.js

export class Toolbar extends HTMLElement {
  constructor() {
    super()
    this.attachShadow({mode: 'open'})
    const selectContainer = document.createElement('div')
    const iconContainer = document.createElement('div')
    const codeBtn = document.createElement(
      'button'
    )
    codeBtn.innerHTML = this.icons.code
    codeBtn.addEventListener('click', () => {
      this.onShowNotebookCode()
    })
    iconContainer.append(codeBtn)
    iconContainer.classList.add('icon-container')
    this.shadowRoot.append(selectContainer, iconContainer)
  }

  connectedCallback() {
    const style = document.createElement('style')
    style.textContent = `
      :host {
        background: #111;
        color: #e7e7e7;
        display: grid;
        grid-template-columns: 1fr auto;
        padding: 3px;
      }
      .icon-container {
        display: flex;
      }
      .icon-container button {
        background: inherit;
        color: inherit;
        border: none;
      }
      .icon-container svg {
        height: 20px;
        width: 20px;
      }
    `
    this.shadowRoot.append(style)
  }

  icons = {
    code: `
      <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24">
        <path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6l6 6zm5.2 0l4.6-4.6l-4.6-4.6L16 6l6 6l-6 6z" />
      </svg>
    `,
  }
}

app-view.js

import {Builder} from '/loader/builder.js'

export class AppView extends HTMLElement {
  constructor() {
    super()
    this.attachShadow({mode: 'open'})
    this.depsConfig = {bundleFiles: [], importFiles: []}
    this.loaded = false
    this.toolbar = document.createElement('m-toolbar')
    this.toolbar.onShowNotebookCode = () => {
      this.showNotebookCode()
    }
    this.viewFrame = document.createElement('iframe')
    this.viewFrame.sandbox = 'allow-scripts'
    this.renderView()
    this.shadowRoot.append(this.toolbar, this.viewFrame)
    this.shadowRoot.addEventListener('code-input', (e) => {
      this.handleInput()
    })
    this.shadowRoot.addEventListener('input', (e) => {
      this.handleInput()
    })
  }

  connectedCallback() {
    const style = document.createElement('style')
    const globalStyle = document.createElement('style')
    globalStyle.textContent = `
      body {
        margin: 0;
        padding: 0;
      }
      html, body {
        margin: 0;
        padding: 0;
      }
      html {
        box-sizing: border-box;
      }
      *, *:before, *:after {
        box-sizing: inherit;
      }
    `
    document.head.append(globalStyle)
    style.textContent = `
      :host {
        display: grid;
        grid-template-rows: auto 1fr;
        grid-template-columns: 1fr;
        height: 100vh;
      }
      iframe {
        width: 100%;
        height: 100%;
        padding: 0;
        border: none;
      }
      .notebook-code {
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
      }
    `
    this.shadowRoot.append(style)
    this.renderView()
  }

  pages = {
    '/': 'pages/index.html',
  }

  renderPage(path = '/', output = false) {
    const pageFile = this.pages[path]
    const data = {
      'layout.html': null,
      'style.css': null,
      'app-redirect-dialog.js': null,
      [pageFile]: null,
    }
    for (const block of readBlocksWithNames(__source)) {
      if (block.name in data) {
        data[block.name] = __source.slice(...block.contentRange)
      }
    }
    let page = data['layout.html']
    page = page.replace(
      '/* {{style}} */',
      data['style.css'].replace(
        /(,)(\).*\{\{font\}\})/,
        (match, p1, p2) => {
          return p1 + Macchiato.data['font.woff2/font.woff2'].replaceAll(/\s+/g, '') + p2
        }
      )
    )
    if (path === '/' && output) {
      page = page.replace('// {{script}}', data['app-redirect-dialog.js'])
    }
    page = page.replace('<!-- {{content}} -->', data[pageFile])
    return page
  }

  renderView() {
    const viewFrame = document.createElement('iframe')
    viewFrame.sandbox = 'allow-scripts'
    viewFrame.srcdoc = this.renderPage()
    this.shadowRoot.appendChild(viewFrame)
    this.viewFrame.remove()
    this.viewFrame = viewFrame
  }

  fence(text, info = '') {
    const matches = Array.from(text.matchAll(new RegExp('^\\s*(`+)', 'gm')))
    const maxCount = matches.map(m => m[1].length).toSorted((a, b) => a - b).at(-1) ?? 0
    const quotes = '`'.repeat(Math.max(maxCount + 1, 3))
    return `\n${quotes}${info}\n${text}\n${quotes}\n`
  }

  async buildNotebook() {
    const pages = Object.entries(this.pages).map(([path, file]) => (
      `\`${file.replace('pages/', '')}\`\n${this.fence(this.renderPage(path, true), 'html')}`
    ))
    return '# Output\n\n' + pages.join('\n') + '\n\n'
  }

  async showNotebookCode() {
    const value = await this.buildNotebook()
    if (!this.notebookCodeEl) {
      this.notebookCodeEl = document.createElement('m-notebook-code')
      this.notebookCodeEl.classList.add('notebook-code')
      this.notebookCodeEl.initialValue = value
      this.notebookCodeEl.value = value
      this.notebookCodeEl.onHide = () => {
        this.hideNotebookCode()
      }
      this.shadowRoot.append(this.notebookCodeEl)
    }
  }

  async hideNotebookCode() {
    if (this.notebookCodeEl) {
      if (this.notebookCodeEl.value.trim() !== this.notebookCodeEl.initialValue.trim()) {
        
      }
      this.notebookCodeEl.remove()
      this.notebookCodeEl = undefined
    }
  }
}

app.js

import { ButtonGroup } from "/forms/button-group.js"
import { Dropdown } from "/menu/dropdown.js"
import { CodeEdit } from "/code-edit/code-edit.js"
import { NotebookCode } from "/notebook-code.js"
import { Toolbar } from "/toolbar.js"
import { AppView } from "/app-view.js"

customElements.define('m-forms-button-group', ButtonGroup)
customElements.define('m-menu-dropdown', Dropdown)
customElements.define('m-editor-code-edit', CodeEdit)
customElements.define('m-notebook-code', NotebookCode)
customElements.define('m-toolbar', Toolbar)
customElements.define('m-app-view', AppView)

class App {
  async run() {
    document.body.appendChild(
      document.createElement(
        'm-app-view'
      )
    )
  }
}

new App().run()

layout.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <link rel="icon" type="image/png" href="/favicon.png" />
<style>
/* {{style}} */
</style>
<script type="module">
// {{script}}
</script>
</head>
<body>
  <main>
    <!-- {{content}} -->
  </main>
  <aside>
    <nav aria-label="primary">
      <ul>
        <li><a href="/">Home</a></li>
      </ul>
    </nav>
  </aside>
  <app-redirect-dialog></app-redirect-dialog>
</body>
</html>

style.css

body {
  background: #2d1d0e;
  display: flex;
  flex-direction: row-reverse;
  align-items: stretch;
  margin: 0;
  padding: 0;
  color: #eee;
  min-height: 100vh;
  box-sizing: border-box;
  font-family: sans-serif;
}
*, *::before, *::after {
  box-sizing: inherit;
}
main {
  flex-grow: 1;
}
aside {
  width: 280px;
  background-color: #57391b;
}
nav ul {
  list-style-type: none;
  padding: 0;
}
nav {
  padding: 20px;
  font-size: 24px;
}
nav a {
  color: inherit;
  text-decoration: none;
}
nav a:hover {
  text-decoration: underline;
  text-underline-offset: 3px;
}
@font-face {
  font-family: 'Mohave';
  font-display: block;
  src: url(data:application/octet-stream;base64,) format(woff2) /* {{font}} */;
}
h1 {
  color: #fff596;
  text-align: center;
  font-size: 48px;
  letter-spacing: 2px;
  font-family: Mohave;
}

app-redirect-dialog.js

export class AppRedirectDialog extends HTMLElement {
  constructor() {
    super()
    this.attachShadow({mode: 'open'})
    this.dontShowAgain = false
    this.dialogEl = document.createElement('dialog')
    const title = document.createElement('h1')
    title.innerText = 'Notebook Data Found'
    const text1 = document.createElement('p')
    text1.innerText = (
      'The previous site at this domain, a side by side markdown editing notebook, ' +
      'has been replaced with a new page about the macchiato.dev project.'
    )
    const text2 = document.createElement('p')
    text2.innerText = 'The notebook has moved to:'
    const link = document.createElement('a')
    link.setAttribute('href', 'https://notebook.macchiato.dev/')
    link.setAttribute('target', '_blank')
    link.innerText = 'https://notebook.macchiato.dev/'
    const linkRow = document.createElement('div')
    linkRow.append(link)
    linkRow.classList.add('link')
    const text3 = document.createElement('p')
    text3.innerText = "Any data stored in the browser's local storage area related to the notebook may be found there."
    const dontShowCheckRow = document.createElement('div')
    const checkLabel = document.createElement('label')
    const checkInput = document.createElement('input')
    checkInput.setAttribute('type', 'checkbox')
    const checkTextEl = document.createElement('span')
    checkTextEl.innerText = "Don't show this dialog again"
    checkLabel.append(checkInput, checkTextEl)
    checkInput.checked = this.dontShowAgain
    checkInput.addEventListener('input', ({target: {checked}}) => {
      this.dontShowAgain = checked
    })
    dontShowCheckRow.append(checkLabel)
    const btn = document.createElement('button')
    btn.innerText = 'Close'
    btn.addEventListener('click', () => {
      if (this.dontShowAgain) {
        this.dispatchEvent(new CustomEvent('dont-show-again', {bubbles: true}))
      }
      this.dialogEl.close()
    })
    const btnRow = document.createElement('div')
    btnRow.classList.add('btn-row')
    btnRow.append(btn)
    this.dialogEl.append(title, text1, text2, linkRow, text3, dontShowCheckRow, btnRow)
    this.shadowRoot.append(this.dialogEl)
  }

  connectedCallback() {
    const style = document.createElement('style')
    style.textContent = `
      dialog {
        margin-top: 5vh;
        margin-left: auto;
        margin-right: auto;
        background: #ccc;
        border: 5px solid #222;
        border-radius: 5px;
        color: #111;
        max-height: 60vh;
        max-width: 85vh;
        overflow: auto;
      }
      a {
        color: #115;
        font-size: 140%;
      }
      .btn-row {
        display: flex;
        justify-content: center;
      }
    `
    this.shadowRoot.appendChild(style)
  }

  run(data) {
    if (data.notebookPresent && !data.showNotebookMessageIsFalse) {
      this.dialogEl.showModal()
    }
  }
}

customElements.define('app-redirect-dialog', AppRedirectDialog)

document.addEventListener('DOMContentLoaded', () => {
  const notebookPresent = !!localStorage.getItem('rco/settings/color-scheme')
  if (!notebookPresent) {
    localStorage.setItem('rco/showNotebookMessage', 'false')
  }
  const showNotebookMessageIsFalse = localStorage.getItem('rco/showNotebookMessage') === 'false'
  const dialog = document.querySelector('app-redirect-dialog')
  dialog.addEventListener('dont-show-again', () => {
    localStorage.setItem('rco/showNotebookMessage', 'false')
  })
  dialog.run({notebookPresent, showNotebookMessageIsFalse})
})

pages/index.html

<h1>macchiato.dev</h1>
<p></p>

License

Icon svg in icons: google material-design-icons, Apache 2.0

Other content: Apache 2.0