├── .datignore ├── .gitignore ├── .well-known └── dat ├── assets ├── designs │ ├── jacinto.png │ ├── starter-kit.png │ └── vacant.png ├── fonts │ ├── Inter-UI-Bold.woff │ ├── Inter-UI-Bold.woff2 │ ├── Inter-UI-Italic.woff │ ├── Inter-UI-Italic.woff2 │ ├── Inter-UI-Regular.woff │ ├── Inter-UI-Regular.woff2 │ ├── fontawesome-webfont.woff │ └── fontawesome-webfont.woff2 ├── meta.jpg └── settings.svg ├── blueprints ├── fields.json ├── guide.json ├── guides.json ├── home.json ├── issue.json └── issues.json ├── bundles ├── 0.1.0 │ └── .gitkeep ├── bootstrap │ ├── bootstrap.css │ └── bootstrap.js └── content.json ├── content ├── community │ └── index.txt ├── docs │ ├── fields │ │ └── index.txt │ └── index.txt ├── faq │ └── index.txt ├── guides │ ├── 01-creating-your-first-site │ │ ├── image.svg │ │ └── index.txt │ ├── 02-customizing-your-site │ │ ├── image.svg │ │ └── index.txt │ ├── 03-creating-pages │ │ ├── image.svg │ │ └── index.txt │ ├── 04-how-to-leave-enoki │ │ ├── image.svg │ │ └── index.txt │ ├── 05-starting-from-scratch │ │ ├── image.svg │ │ └── index.txt │ ├── drafts.txt │ └── index.txt ├── index.txt ├── issues │ ├── cleanup-codebase │ │ └── index.txt │ ├── designs │ │ └── index.txt │ ├── fields │ │ └── index.txt │ ├── files │ │ └── index.txt │ ├── hub │ │ └── index.txt │ ├── index.txt │ ├── inline-editing │ │ └── index.txt │ ├── library │ │ └── index.txt │ ├── page-functionality │ │ └── index.txt │ ├── panel-layouts │ │ └── index.txt │ ├── saving │ │ └── index.txt │ └── sites │ │ └── index.txt └── log │ └── index.txt ├── favicon.ico ├── favicon.png ├── index.html ├── readme.md └── source ├── blueprints ├── default.json ├── page-header.json └── sites-create.json ├── components ├── actionbar.js ├── breadcrumbs.js ├── field.js ├── format.js ├── guide-thumbnail.js ├── header.js ├── modal.js ├── publish.js ├── sidebar.js ├── split.js └── uploader.js ├── containers ├── fields.js ├── page-fields.js ├── page-header.js ├── page-new.js ├── wrapper-hub.js └── wrapper-site.js ├── design ├── custom.js ├── guides.css ├── index.js ├── options.js ├── simplecolorpicker.css ├── simplemde.css └── utilities.js ├── fields ├── checkbox.js ├── color.js ├── date.js ├── dropdown.js ├── files.js ├── index.js ├── pages.js ├── range.js ├── tags.js ├── text.js └── textarea.js ├── index.js ├── lib ├── file.js └── page.js ├── package.json ├── plugins ├── designs.js ├── docs.js ├── hub.js ├── interface.js ├── panel.js ├── scroll.js └── sites.js └── views ├── changes.js ├── default.js ├── docs.js ├── file-new.js ├── file.js ├── files-all.js ├── guide.js ├── guides.js ├── hub.js ├── log.js ├── network.js ├── notfound.js ├── page-new.js ├── pages-all.js ├── settings.js ├── sites-create.js └── sites.js /.datignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dat 3 | .git 4 | source/node_modules 5 | source/node_modules/** 6 | package-lock.json -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dat 3 | 4 | node_modules 5 | bundle.js 6 | bundle.css 7 | dat.json 8 | package-lock.json 9 | -------------------------------------------------------------------------------- /.well-known/dat: -------------------------------------------------------------------------------- 1 | dat://53f1ef8d157ded803f14f6906fc3e34acabb0651ef422791b39c2d3a0700dd20 2 | TTL=3600 -------------------------------------------------------------------------------- /assets/designs/jacinto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enokidotsite/panel/12b6d563f0c23ba9f42f9fb1a23e59cf7ea2c0fb/assets/designs/jacinto.png -------------------------------------------------------------------------------- /assets/designs/starter-kit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enokidotsite/panel/12b6d563f0c23ba9f42f9fb1a23e59cf7ea2c0fb/assets/designs/starter-kit.png -------------------------------------------------------------------------------- /assets/designs/vacant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enokidotsite/panel/12b6d563f0c23ba9f42f9fb1a23e59cf7ea2c0fb/assets/designs/vacant.png -------------------------------------------------------------------------------- /assets/fonts/Inter-UI-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enokidotsite/panel/12b6d563f0c23ba9f42f9fb1a23e59cf7ea2c0fb/assets/fonts/Inter-UI-Bold.woff -------------------------------------------------------------------------------- /assets/fonts/Inter-UI-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enokidotsite/panel/12b6d563f0c23ba9f42f9fb1a23e59cf7ea2c0fb/assets/fonts/Inter-UI-Bold.woff2 -------------------------------------------------------------------------------- /assets/fonts/Inter-UI-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enokidotsite/panel/12b6d563f0c23ba9f42f9fb1a23e59cf7ea2c0fb/assets/fonts/Inter-UI-Italic.woff -------------------------------------------------------------------------------- /assets/fonts/Inter-UI-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enokidotsite/panel/12b6d563f0c23ba9f42f9fb1a23e59cf7ea2c0fb/assets/fonts/Inter-UI-Italic.woff2 -------------------------------------------------------------------------------- /assets/fonts/Inter-UI-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enokidotsite/panel/12b6d563f0c23ba9f42f9fb1a23e59cf7ea2c0fb/assets/fonts/Inter-UI-Regular.woff -------------------------------------------------------------------------------- /assets/fonts/Inter-UI-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enokidotsite/panel/12b6d563f0c23ba9f42f9fb1a23e59cf7ea2c0fb/assets/fonts/Inter-UI-Regular.woff2 -------------------------------------------------------------------------------- /assets/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enokidotsite/panel/12b6d563f0c23ba9f42f9fb1a23e59cf7ea2c0fb/assets/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /assets/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enokidotsite/panel/12b6d563f0c23ba9f42f9fb1a23e59cf7ea2c0fb/assets/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /assets/meta.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enokidotsite/panel/12b6d563f0c23ba9f42f9fb1a23e59cf7ea2c0fb/assets/meta.jpg -------------------------------------------------------------------------------- /assets/settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 41 | 42 | -------------------------------------------------------------------------------- /blueprints/fields.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Fields", 3 | "pages": false, 4 | "fields": { 5 | "title": { 6 | "label": "Title", 7 | "type": "text" 8 | }, 9 | "text": { 10 | "label": "Text", 11 | "type": "textarea" 12 | }, 13 | "checkbox": { 14 | "label": "Checkbox", 15 | "text": "Descriptive text", 16 | "type": "checkbox", 17 | "width": "1/2" 18 | }, 19 | "color": { 20 | "label": "Color", 21 | "text": "Descriptive text", 22 | "type": "color", 23 | "width": "1/2" 24 | }, 25 | "tags": { 26 | "label": "Tags", 27 | "type": "tags" 28 | }, 29 | "range": { 30 | "label": "Range", 31 | "text": "Descriptive text", 32 | "type": "range", 33 | "start": 20 34 | }, 35 | "dropdown": { 36 | "label": "Dropdown", 37 | "type": "dropdown", 38 | "options": { 39 | "one": { 40 | "title": "One" 41 | }, 42 | "two": { 43 | "title": "Two" 44 | } 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /blueprints/guide.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Guide", 3 | "pages": false, 4 | "fields": { 5 | "title": { 6 | "label": "Title", 7 | "type": "text" 8 | }, 9 | "tags": { 10 | "label": "Tags", 11 | "type": "tags", 12 | "width": "1/2" 13 | }, 14 | "featured": { 15 | "label": "Featured", 16 | "type": "checkbox", 17 | "text": "Featured guide?", 18 | "width": "1/2" 19 | }, 20 | "background": { 21 | "label": "Background Color", 22 | "type": "color", 23 | "width": "1/2" 24 | }, 25 | "color": { 26 | "label": "Inverted", 27 | "true": "Is inverted", 28 | "false": "Is not inverted", 29 | "type": "checkbox", 30 | "width": "1/2" 31 | }, 32 | "excerpt": { 33 | "label": "Excerpt", 34 | "type": "textarea" 35 | }, 36 | "text": { 37 | "label": "Text", 38 | "type": "textarea" 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /blueprints/guides.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Guides", 3 | "pages": { 4 | "view": "guide" 5 | }, 6 | "layout": { 7 | "all": { 8 | "fields": true, 9 | "width": "1/1" 10 | } 11 | }, 12 | "fields": { 13 | "pages": { 14 | "label": "Pages", 15 | "type": "pages" 16 | }, 17 | "title": { 18 | "label": "Title", 19 | "type": "text" 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /blueprints/home.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Home", 3 | "layout": { 4 | "all": { 5 | "fields": ["pages"], 6 | "width": "1/1" 7 | } 8 | }, 9 | "fields": { 10 | "pages": { 11 | "label": "Pages", 12 | "type": "pages" 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /blueprints/issue.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Issue", 3 | "pages": false, 4 | "fields": { 5 | "title": { 6 | "label": "Title", 7 | "type": "text" 8 | }, 9 | "date": { 10 | "label": "Date", 11 | "type": "date", 12 | "default": "today", 13 | "width": "1/2" 14 | }, 15 | "tags": { 16 | "label": "Tags", 17 | "type": "tags", 18 | "width": "1/2" 19 | }, 20 | "text": { 21 | "label": "Text", 22 | "type": "textarea" 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /blueprints/issues.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Tasks", 3 | "pages": { 4 | "view": "issue" 5 | }, 6 | "files": false, 7 | "layout": { 8 | "pages": { 9 | "sticky": true, 10 | "width": "1/2", 11 | "fields": ["pages"] 12 | }, 13 | "all": { 14 | "width": "1/2", 15 | "fields": true 16 | } 17 | }, 18 | "fields": { 19 | "pages": { 20 | "label": "Pages", 21 | "type": "pages", 22 | "sort": "alphabetical", 23 | "limit": 20 24 | }, 25 | "title": { 26 | "label": "Title", 27 | "type": "text" 28 | }, 29 | "text": { 30 | "label": "Text", 31 | "type": "textarea" 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /bundles/0.1.0/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enokidotsite/panel/12b6d563f0c23ba9f42f9fb1a23e59cf7ea2c0fb/bundles/0.1.0/.gitkeep -------------------------------------------------------------------------------- /bundles/bootstrap/bootstrap.css: -------------------------------------------------------------------------------- 1 | .bootstrap-load { 2 | box-sizing: border-box; 3 | border-radius: 50%; 4 | width: 30px; 5 | height: 30px; 6 | position: fixed; 7 | bottom: 20px; 8 | left: 20px; 9 | text-indent: -9999em; 10 | border-top: 3px solid #1a1a1a; 11 | border-right: 3px solid #1a1a1a; 12 | border-bottom: 3px solid #1a1a1a; 13 | border-left: 3px solid #fff; 14 | animation: bootstrap_load 1s infinite linear; 15 | } 16 | 17 | @keyframes bootstrap_load { 18 | 0% { transform: rotate(0deg) } 19 | 100% { transform: rotate(360deg) } 20 | } 21 | -------------------------------------------------------------------------------- /bundles/bootstrap/bootstrap.js: -------------------------------------------------------------------------------- 1 | // start 2 | init() 3 | 4 | // TODO: panel versioning 5 | function init () { 6 | // var archiveActive = window.localStorage.getItem('active') 7 | // var localVersion = window.localStorage.getItem('version-' + archiveActive) 8 | var elHead = document.querySelector('head') 9 | var elScript = document.createElement('script') 10 | var elLink = document.createElement('link') 11 | // var version = localVersion 12 | // ? JSON.parse(localVersion) 13 | // : { selected: '0.1.0' } 14 | var version = { selected: '0.1.0' } 15 | var ran = Math.floor(Math.random() * 10000000) 16 | 17 | elScript.setAttribute('src', '/bundles/' + version.selected + '/bundle.js?' + ran) 18 | elLink.setAttribute('href', '/bundles/' + version.selected + '/bundle.css?' + ran) 19 | elLink.setAttribute('rel', 'stylesheet') 20 | 21 | elHead.appendChild(elScript) 22 | elHead.appendChild(elLink) 23 | } 24 | -------------------------------------------------------------------------------- /content/community/index.txt: -------------------------------------------------------------------------------- 1 | title: Community 2 | ---- 3 | view: community 4 | ---- 5 | text: Coming soon -------------------------------------------------------------------------------- /content/docs/fields/index.txt: -------------------------------------------------------------------------------- 1 | title: Fields 2 | ---- 3 | view: fields 4 | ---- 5 | text: An overview of the available fields. 6 | ---- 7 | tags: 8 | - technopastoral 9 | - extra-statecraft 10 | 11 | ---- 12 | checkbox: true 13 | ---- 14 | color: #fffb00 15 | ---- 16 | range: 70 -------------------------------------------------------------------------------- /content/docs/index.txt: -------------------------------------------------------------------------------- 1 | title: Docs 2 | ---- 3 | view: docs 4 | ---- 5 | text: ## Alpha Release 6 | 7 | You’re here early—everything is extremely unstable. Consider this a modestly functional sketch. A starting point with clear limitations, leaning on existing and familiar convention to form a foundation for spanning the gap between here and there. 8 | 9 | How do we save the future? With another CMS, of course. 10 | 11 | Hoping you find this immediately useful as a basic tool for creating experimental personal sites in the short term. 12 | 13 | If you have any questions please [see the FAQ](/#hub/faq), or feel free to send an email. 14 | 15 | ## Features 16 | 17 | - **no database**: there is only a filesystem 18 | - **data ownership**: your data remains with you 19 | - **offline accessible**: manage your site without a connection 20 | - **hackable**: create editable copies of the panel and sites to customize as you see fit 21 | - **extendable**: easily define custom fieldsets with blueprints 22 | - **free**: publish to the web for free without any intermediaries 23 | - **fun**: uses [Choo](https://choo.io) as a front-end framework 24 | 25 | ## Philosophy 26 | 27 | You are your data, your data is you. You should own your tools, your tools should not own you. Culture wants to be free. Platforms are dead. Universal knowledge for everyone. 28 | 29 | ## Getting Started 30 | 31 | Enoki is an ultralight set of tools for publishing on the decentralized web. Simply open [Beaker Browser](https://beakerbrowser.com) and navigate to [https://panel.enoki.site](https://panel.enoki.site). 32 | 33 | ## Sites 34 | 35 | Creating a site is as simple as navigating to **Sites** → **Create a New Site**. Choose a design, enter some basic information, and authorize Beaker to make an editable copy of the design. Your site is now loaded into Enoki and will remain accessible in the **Sites** area. 36 | 37 | Say you manually created an Archive with Beaker, or copied a site some other way. Simply click **Load an Existing Site** and select your Archive. Note that you must be the owner of the Archive, and it must contain a `/content` directory. 38 | 39 | ## Editor 40 | 41 | The **Editor** is where you make edits to your Site’s content. 42 | 43 | ### Pages 44 | 45 | To create a new page navigate to **Editor** → **Pages** → **Create**. When creating a page you’ll see a few options. 46 | 47 | - **Title** is self explanatory. 48 | - **Pathname** is the `/what-you-see-in-the-url`. 49 | - **View** defines how the content is displayed on your Site, and what fields you see in the Panel. 50 | 51 | ### Coming soon 52 | 53 | - Change page location after creation 54 | - Adjust view after creation 55 | 56 | ## Files 57 | 58 | Files are pretty dumb at the moment. 59 | 60 | ## Blueprints 61 | 62 | When viewing Pages and Files in the Panel you’re presented with a set of fields representing the content. What fields should appear are defined by creating **Blueprints**. 63 | 64 | For now, take a look at a blueprint associated with one of the Designs. It’s pretty self-explanatory. 65 | 66 | ## Customization and Development 67 | 68 | This version of Enoki is highly experimental! Certain things are going to change quite quickly, so please tread lightly. 69 | 70 | ### Front-end 71 | 72 | Enoki uses [Choo](https://choo.io) as a front-end framework. It’s like React, but fun. Copy the Panel or any Design, then navigate to `/source` and run `npm install` and `npm start`. Working to document this better, but should give you a starting point. 73 | 74 | ### Fields 75 | 76 | Fields are constructed using [`nanocomponent`](https://github.com/choojs/nanocomponent). For now, take a look at a simple field such as `text`. For a more advanced example which depends on a 3rd party library, look at `textarea`. 77 | 78 | ### Idiosyncrasies 79 | 80 | - Shit is messy right now, please watch your step. 81 | - When creating a View, be sure to add it to `/views/index.js`. 82 | - Also be sure to create a Blueprint so the correct fields appear in the Panel. -------------------------------------------------------------------------------- /content/faq/index.txt: -------------------------------------------------------------------------------- 1 | title: FAQ 2 | ---- 3 | view: faq 4 | ---- 5 | text: ## Who is this for? 6 | 7 | Currently, for people who probably spend most of their time making websites to learn more about the peer-to-peer web. In the future? Hmm… 8 | 9 | ## What about access over HTTP? 10 | 11 | Yeah, this is important. For now, Enoki uses Beaker’s web API to read your site’s Dat archive into one big javascript object. This object loaded into Choo’s state. 12 | 13 | One way to do this is to write `state.content` to a JSON file saving from the Panel. Then, depending upon dat/http use the web API or request the JSON file. Actually, this happens now, it’s just not documented as there are interesting things which could be done by reading multiple archives into a site, and writing to static JSON prevents that from happening. 14 | 15 | Anyway, with that static JSON in place you can use a service like [Hashbase](https://hashbase.io) to persistently sync your app. Just copy the `dat://` url of your site to Hashbase. Alternatively, install [`dathttpd`](https://github.com/beakerbrowser/dathttpd) on your own server. 16 | 17 | ## What inspired this 18 | 19 | - Kirby 20 | - Teenage Engineering 21 | - etc 22 | 23 | ## This looks like a wireframe 24 | 25 | Yeah, it sort of is a functional wireframe—a representation of the expected basics for a CMS today. Consider it a starting point meant to progressively evolve into an interface for peer-to-peer publishing as we collectively develop the language. 26 | 27 | ## My Sites sometime dissapear 28 | 29 | This is because sites are currently using localstorage, which associates data with domain. If you load a site at `https://panel.enoki.site`, it will not appear in the Sites section when at `dat://panel.enoki.site`. This will be resolved in the future. 30 | 31 | ## Shouldn’t this output static HTML for pre-rendered routes? 32 | 33 | Yeah, this would be great. Found a few difficult questions about how to do this without cluttering up the content directory with a bunch of `index.html` files. 34 | 35 | ## How does pagination work? 36 | 37 | Enoki currently reads your entire `/content` directory into one big object. For smaller sites, this is fine. For larger sites, that can become an issue. For now, just manually fake paginate by converting an object of content into an array, then using array methods. 38 | 39 | ## What about image resizing? 40 | 41 | This will probably be handled with Canvas in the future. Nothing yet. 42 | 43 | ## I want _______________ 44 | 45 | Yeah, chances are this has been thought about. Honestly, there are so many solid and mature CMS solutions that can output static sites. Just use one of those and publish the contents inside a Dat Archive if your’re looking for all the classic stuff. This project is looking to expand upon much more than just making sites. -------------------------------------------------------------------------------- /content/guides/01-creating-your-first-site/image.svg: -------------------------------------------------------------------------------- 1 | Artboard -------------------------------------------------------------------------------- /content/guides/01-creating-your-first-site/index.txt: -------------------------------------------------------------------------------- 1 | title: Creating Your First Site (or not!) 2 | ---- 3 | view: guide 4 | ---- 5 | text: Enoki is currently an *experimental* CMS (content management system) for the peer-to-peer web. It’s intended for those familiar with making websites to gain exposure to the unique affordances of the peer-to-peer web. 6 | 7 | 1. Platforms should be free, and not treat attention as a resource to be extracted from users and sold to advertisers. 8 | 2. Users should own their data, and grant permission to platforms to read and write locally *by default*. Interfaces are simply *views* for data which exist with the user. 9 | 3. Data should be shared openly; the tools should be fungible. Anyone can create applications with instant access to all public data, avoiding limitations implimented by proprietary APIs. 10 | 4. Our tools should be ultralight and understandable, not heavy and complex! 11 | 12 | ## Creating a Site 13 | 14 | 1. Navigate to **Sites** and click *Create a New Site* 15 | 2. Enter your site’s Title, and a quick Description 16 | 3. Browse the design, and select one which looks right for you 17 | 4. Authorize Beaker to make an **editable copy** the design 18 | 5. Begin editing your content! 19 | 20 | You might be asking, what is an **editable copy**? Simply put, it’s creating your own unique copy of the site. Unlike other platforms, your site’s files exist on your computer! This means *you* own and control your site. There is no risk of your site dissapearing if Enoki ceases to exist. 21 | 22 | This is a *super critical thing*. Platforms today are designed to make you dependant upon them as a way to sell advertising. By giving you control over your own content and data, you are not bound to Enoki! 23 | 24 | ## Managing your Content 25 | 26 | You can manage your content in a few different ways. 27 | 28 | 1. With the Enoki Panel 29 | 2. By editing the files directly using whatever tool you’d like 30 | 3. Using any application which can read your content 31 | 32 | Unlike platforms, your data is human readable. Enoki just creates folders and text files! You can edit your site with any application which can edit text files. Not only that, you can easily use other applications (or create your own) to manage your site. 33 | 34 | This is possible because you own your content and data! 35 | 36 | ## Accessing Your Site 37 | 38 | You’ll notice that URLs look funny in Beaker. Remember torrents? You’ve probably downloaded music or movies this way. Beaker Browser is an experiment built on the entire internet working like that. You don’t have to pay any one for your site to appear online! This is *truely free*, and not dependant upon advertising. 39 | 40 | This is called peer-to-peer networking. When using a tool like SquareSpace, your site exists on a server they own. Anyone visiting your site must connect through SquareSpace. Now, imagine them instead connected directly to you, and not only that, but any one else who happens to be visiting your site. Crazy, right?! 41 | 42 | Because there is no central provider, like SquareSpace, someone (either you or someone else) must be providing your site for others to access it. Fortunately, you can simply enter your site’s `dat` URL somewhere like [Hashbase](https://hashbase.com) which ensure your site will always be accessible! Not only that, you can also access your site over `http` in a normal browser like Chrome or Firefox! 43 | 44 | You might be thinking, *Shit! This makes a lot of sense!* Yeah, it does! If not, don’t worry—it’s still early and the tools will get there. 45 | 46 | ## Cutomizing Your Site 47 | 48 | Enoki builds *real websites*. Not only do you get all the files for the site, you also get the entire source. You can take this source and choose to never use Enoki again, if you’d like. Remember; you are learning how to *actually program*, and not just use proprietary tools. 49 | ---- 50 | excerpt: Ok, you’ve stumbled across Enoki. What are you getting yourself into? Let’s look at how is this different from existing platforms, and expand upon both the pros and cons. Is Enoki right for you? Maybe, but hopefully you can take the knowledge with you regardless of it’s a match. 51 | ---- 52 | color: true 53 | ---- 54 | featured: true 55 | ---- 56 | tags: 57 | - beginner 58 | 59 | ---- 60 | background: #045bc1 -------------------------------------------------------------------------------- /content/guides/02-customizing-your-site/image.svg: -------------------------------------------------------------------------------- 1 | Artboard -------------------------------------------------------------------------------- /content/guides/02-customizing-your-site/index.txt: -------------------------------------------------------------------------------- 1 | title: Customizing Your Sites 2 | ---- 3 | view: guide 4 | ---- 5 | excerpt: Unlike other platforms, which provide interfaces to enter data into centralized databases and render that data with proprietary source, Enoki generates actual websites! When creating a site, you are given the actual source code for the site, providing infinite customization possibility. 6 | ---- 7 | text: Unlike other platforms, which provide interfaces to enter data into centralized databases and render that data with proprietary source, Enoki generates actual websites! When creating a site, you are given the actual source code for the site, providing infinite customization possibility. 8 | 9 | Not a developer? Don’t worry, creating lessons and guides on how to start learning is one of the top priorities. In the meantime please feel free to poke around! 10 | 11 | ## Installation 12 | 13 | 1. Open the `source` directory of your site in terminal 14 | 2. Run `npm install` 15 | 3. Run `npm start` to watch files during development 16 | 4. Run `npm build` to bundle for production 17 | 18 | ## Development and Dependencies 19 | 20 | Because there is no difference between server and client on the peer-to-peer web, everything is client-side. In practice, this feels like the conveniences of an API like Firebase’s, but native to the browser and data is saved locally to static files. 21 | 22 | Enoki has some preferences in default tooling, but everything is swappable. The most notable is [Choo](https://choo.io), the cutest little front-end framework around. Think of it as React or Vue, but without the fluff. The main critisism of Choo is once you try it you wish everything else was like it :) 23 | 24 | Instead of using Webpack, we opt for Browserify to bundle builds. Eventually you won’t need to bundle anything, but for now we find Browserify to be the most focused and calm. If you don’t feel, free to swap for whatever you’d like and use an Enoki Adapter to load the content object. 25 | 26 | And of course, each design has it’s own Git repository, which you can use to pull in updates of the core design as they are made. Feel free to open issues and make pull requests, too! 27 | 28 | ## Resources 29 | 30 | Because Enoki sites are real sites, the entire history of books, videos and tutorials as they relate to the internet at large are relevant in customizing your Enoki site. 31 | 32 | ### Choo 33 | 34 | A great place to start is the [Choo](https://choo.io) homepage. There you’ll find a great overview of the core principles Choo represents. There is also the Choo handbook, which guides you through creating an app. The line between websites and apps is increasingly being blurred; play around with it. 35 | 36 | ### The peer-to-peer web 37 | 38 | The community around Enoki has helped create the Peer-to-Peer Web event series. These are days dedicated to faciliating converations and workshops about peer-to-peer web, focusing on the importance of data ownership, archival, and accessibility. 39 | 40 | Instead of focusing on this through the lens of technology, anyone doing anything involving creative thinking is invited to learn how the peer-to-peer web can be incorperated into their practice. 41 | 42 | Documentation is available through the [web site](https://peer-to-peer-web.com) with more events planned for the future. 43 | ---- 44 | color: true 45 | ---- 46 | featured: 47 | ---- 48 | tags: 49 | - beginner 50 | 51 | ---- 52 | background: #00917b -------------------------------------------------------------------------------- /content/guides/03-creating-pages/image.svg: -------------------------------------------------------------------------------- 1 | Artboard -------------------------------------------------------------------------------- /content/guides/03-creating-pages/index.txt: -------------------------------------------------------------------------------- 1 | title: Creating Custom Pages 2 | ---- 3 | view: guide 4 | ---- 5 | tags: 6 | - beginner 7 | 8 | ---- 9 | color: true 10 | ---- 11 | featured: false 12 | ---- 13 | excerpt: Creating websites can be fun! Updating websites can be a major drag. Like, a really major drag. Enoki is interested in trying to solve these issues around *updating websites* as a foundation for creating fresh and exciting ways of *creating websites*. 14 | ---- 15 | text: Creating websites can be fun! Updating websites can be a major drag. Like, a really major drag. Enoki is interested in solving these issues around *updating websites* as a foundation for exploring exciting new ways of *creating websites*. There are a few primary interfaces for managing websites these days. 16 | 17 | 1. Through a maze of fields for data entry. 18 | 2. With a WYSIWYG (what you see is what you get) 19 | 3. Some sort of broken thing between those two. 20 | 21 | If you’ve ever used one of these WYSIWYG solutions to create a client site, you’ll know the dangers of handing over the keys. 22 | 23 | Let’s take that third option and make it *significantly* less broken, and get there by first implimenting the pro functionality you’d expect in a (capital letters) CMS solution centered around fieldsets. 24 | 25 | Of course, this is all a little boring now, but intended to create a truly robust foundation for building the next generation of interfaces for connecting and sharing both online *and offline*—and have fun while doing it. 26 | 27 | ## Views 28 | 29 | When creating a new Page in the Panel you select a *view*. For instance, there could be `blog` or `about` views. When visiting your site, this let’s you say, “I’d like my about page to be formatted a certain way.” Inside the Panel, this let’s you say, “I’d like to see certain fields to let me easily input exactly the type of content I want on my about page.” 30 | 31 | This sort of specificity ensures your page doesn’t get super messy when updating content, as each type of content is defined seperately. This same cleanliness clearly implies a limitation in the *mesiness*, arguably the most critical part of any creative endevour. For now, this mesiness takes place during the creative process of determining what the site *should be*, not in when you are trying to simply update it. These lines are blurry, but for now let’s go with that. 32 | 33 | ## Pages 34 | 35 | The interface of the *editor* in the Panel is always the same; a set of fields representing the data associated with that page. We define what sort of data is associated with a page by creating a *blueprint* located in `/blueprints`. 36 | 37 | There are all sorts of options associated with blueprints, but these guides are being written hastily as there is a lot to do, and this will be expanded on in the future! 38 | 39 | For now, just look around the example designs `/blueprints` directories to see what’s up. As an example, here is the default blueprint: 40 | 41 | ``` 42 | { 43 | "title": "Default", 44 | "fields": { 45 | "title": { 46 | "label": "Title", 47 | "type": "text" 48 | }, 49 | "text": { 50 | "label": "Text", 51 | "type": "textarea" 52 | } 53 | } 54 | } 55 | ``` 56 | 57 | ## Getting messy 58 | 59 | As mentioned above, this somewhat strict field-based composition is a near-term strategy for creating a truly flexible future for Enoki. Certain sites feature inline-editable interface only visible to the site’s owner. This is endearingly called `self-mutation`, and will be rolling out in surprising ways across various designs soon! 60 | ---- 61 | background: #f21006 -------------------------------------------------------------------------------- /content/guides/04-how-to-leave-enoki/image.svg: -------------------------------------------------------------------------------- 1 | Artboard -------------------------------------------------------------------------------- /content/guides/04-how-to-leave-enoki/index.txt: -------------------------------------------------------------------------------- 1 | title: How to Leave Enoki 2 | ---- 3 | view: guide 4 | ---- 5 | tags: 6 | - beginner 7 | 8 | ---- 9 | color: false 10 | ---- 11 | featured: false 12 | ---- 13 | excerpt: Technology moves fast. Enoki knows it won’t always be the best tool for you. Rest easy knowing that you own your data and content. It exists on your computer, and not only on a server owned by a platform. 14 | ---- 15 | text: Technology moves fast. Enoki won’t always be the best tool for you. Rest easy knowing that you own your data and content. It exists on your computer, and not only on a server owned by a platform. 16 | 17 | There is no option for exporting your content from Enoki because it already exists with you; there is nothing to export from. 18 | 19 | Simply, the Enoki Panel creates files in folders on your local machine instead of jumbled data in a database somewhere. Not only this, the methods for reading and writing these files have been abstracted out into discreet open source javascript modules ready for developers to integrate into any other open tool. 20 | 21 | Those modules are: 22 | 23 | - [**Smarkt**](https://github.com/jondashkyle/smarkt) for reading `.txt` files with `keys: and values` seperated by `----`. 24 | - [**Hypha**](https://github.com/jondashkyle/hypha) for creating flat JSON out of those files and the folders they’re contained within. 25 | - [**Enoki**](https://github.com/jondashkyle/enoki) for reading and writing Enoki formatted sites. 26 | 27 | It’s hoped that these modules will be useful to anyone who wants to create their own tools! 28 | ---- 29 | background: #fbc200 -------------------------------------------------------------------------------- /content/guides/05-starting-from-scratch/image.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /content/guides/05-starting-from-scratch/index.txt: -------------------------------------------------------------------------------- 1 | title: Starting From Scratch 2 | ---- 3 | view: guide 4 | ---- 5 | background: #5a2da8 6 | ---- 7 | visible: false 8 | ---- 9 | color: true 10 | ---- 11 | excerpt: Nam elementum augue eu lacus sagittis, eu gravida ligula porta. Vivamus et tincidunt ipsum. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Aenean molestie mollis arcu nec sodales. Suspendisse ac lacus turpis. Etiam vulputate ligula at erat laoreet, vel fermentum tortor convallis. Fusce nec accumsan purus. Sed ac mi pharetra, vulputate augue nec, pharetra nibh. 12 | ---- 13 | text: Interdum et malesuada fames ac ante ipsum primis in faucibus. Nulla congue volutpat lectus. Etiam vehicula convallis eros. Quisque semper venenatis risus. Morbi fermentum, est id commodo venenatis, dui ante pulvinar arcu, a tempus enim arcu id augue. Sed convallis ac velit sed interdum. Nam nec ante rhoncus sapien cursus dignissim eu vel arcu. 14 | 15 | Aliquam quis magna felis. Sed ac magna dolor. Nunc nunc augue, tempus at eros tristique, convallis eleifend quam. Nam eget elementum quam. Nunc consequat, risus sit amet tincidunt malesuada, est nibh condimentum leo, quis laoreet velit tortor et lorem. Proin vitae sem urna. Proin pharetra justo mi, et volutpat urna hendrerit ac. Fusce volutpat ex ut nunc hendrerit, a placerat justo sollicitudin. Vestibulum sagittis justo ut nisi venenatis tincidunt porttitor nec dolor. Maecenas dui felis, efficitur a nibh vitae, tristique accumsan augue. 16 | 17 | ## Suspendisse varius 18 | 19 | Nisl in sem lobortis, quis consectetur dui pharetra. In a dui vel dui scelerisque egestas. Curabitur rutrum enim metus, eget facilisis sem sagittis eget. Praesent interdum sem ullamcorper elementum ultrices. Nulla sit amet maximus dui, quis venenatis neque. Fusce dignissim porttitor efficitur. Nulla malesuada eleifend elementum. Nam malesuada consequat ex id ultricies. 20 | 21 | Sed posuere consectetur ipsum, id dapibus orci fringilla at. Aliquam placerat et eros sed efficitur. Ut sapien diam, auctor sed lectus nec, sodales molestie turpis. Sed volutpat odio vel purus ultricies luctus. Fusce placerat, turpis non cursus rhoncus, massa eros lobortis diam, et malesuada mi purus bibendum ante. Vestibulum at tellus nulla. Praesent dolor mi, mollis sed felis ac, mollis porta magna. Morbi interdum imperdiet ex et ultrices. Curabitur luctus pharetra nunc, eget consequat risus vehicula at. Aenean finibus augue orci, ut scelerisque urna fringilla quis. Phasellus rutrum luctus enim eu tincidunt. Phasellus sagittis turpis eget ligula dignissim sodales. Nulla rutrum, neque sed eleifend efficitur, tellus elit fringilla augue, ut elementum ex magna sit amet est. Etiam eget est sed purus lobortis eleifend eu non dolor. -------------------------------------------------------------------------------- /content/guides/drafts.txt: -------------------------------------------------------------------------------- 1 | # Creating Your First Site (or not!) 2 | 3 | ## Overview 4 | 5 | - What is Enoki? 6 | - Create a new site 7 | - Enter your title and description 8 | - Select a design (more coming soon) 9 | - Click “Fork” 10 | - Explain what forking is and data ownership 11 | - Talk about content editing 12 | - Lead into customization 13 | 14 | # Customizing Your Sites 15 | 16 | - How Enoki exposes the real tools to you 17 | - What tooling looks like today 18 | - Introduction to Choo -------------------------------------------------------------------------------- /content/guides/index.txt: -------------------------------------------------------------------------------- 1 | title: Guides 2 | ---- 3 | view: guides 4 | ---- 5 | text: 6 | 7 | Working on this :) -------------------------------------------------------------------------------- /content/index.txt: -------------------------------------------------------------------------------- 1 | title: Hub 2 | ---- 3 | view: home 4 | ---- 5 | text: -------------------------------------------------------------------------------- /content/issues/cleanup-codebase/index.txt: -------------------------------------------------------------------------------- 1 | title: Cleanup codebase 2 | ---- 3 | view: issue 4 | ---- 5 | text: Gotta refactor a little bit, it’s getting messy in here. 6 | 7 | ## Structure 8 | 9 | - [ ] Try to make the DOM as representational as possible. 10 | - [ ] Support imports/require/window 11 | - [ ] Create a bundle.js which exposes over window 12 | - [ ] Create a super simple demo using no build system 13 | 14 | ## Styles 15 | 16 | - [ ] Switch from gr8 for layout to CSS grid/variables. 17 | - [ ] Create `.copy` styles for dark/light 18 | - [ ] Simple `sheetify` css-in-js for most things. 19 | ---- 20 | tags: 21 | - todo 22 | ---- 23 | date: 2018-03-05 24 | ---- 25 | visible: true -------------------------------------------------------------------------------- /content/issues/designs/index.txt: -------------------------------------------------------------------------------- 1 | title: Designs 2 | ---- 3 | view: issue 4 | ---- 5 | tags: 6 | - 1.0.0 7 | - todo 8 | 9 | ---- 10 | text: Create some fresh designs. 11 | 12 | ## Todo 13 | 14 | - [ ] Update dependencies before release 15 | - [ ] Load design info from an array of `dat://` urls 16 | - [ ] Readme / info 17 | - [ ] Compare package.json of the site to the design 18 | 19 | ## Things to keep in mind 20 | 21 | - Super simple document / plain CSS 22 | 23 | ## Streamer 24 | 25 | - [ ] A design for streams 26 | - [ ] Vlog style? 27 | 28 | ## Ripple 29 | 30 | - SVG warping sort of thing, text input to outlined shapes 31 | ---- 32 | date: 2018-03-05 33 | ---- 34 | visible: true -------------------------------------------------------------------------------- /content/issues/fields/index.txt: -------------------------------------------------------------------------------- 1 | title: Fields 2 | ---- 3 | view: issue 4 | ---- 5 | text: ## Cleanup 6 | 7 | - [ ] Create `form` and `field` components 8 | - [ ] Pass full state, emit, and options to `form` 9 | - [ ] Use the `enoki/page` api for most things 10 | - [ ] Autoscroll textarea when at bottom 11 | - [ ] Add html5 validation 12 | 13 | ## New fields 14 | 15 | - [ ] Universal `list` which can display pages or files based on option. Include delete, etc. 16 | - [ ] Radio selector 17 | - [ ] Structured data 18 | - [ ] Font (copy font files to site?) 19 | ---- 20 | tags: 21 | - 1.0.0 22 | - todo 23 | 24 | ---- 25 | date: 2018-03-05 26 | ---- 27 | visible: true -------------------------------------------------------------------------------- /content/issues/files/index.txt: -------------------------------------------------------------------------------- 1 | title: Files 2 | ---- 3 | view: issue 4 | ---- 5 | tags: 6 | - todo 7 | - 1.0.1 8 | ---- 9 | text: ## Todo 10 | 11 | - [ ] Meta data support (api/panel) 12 | - [x] Video preview 13 | - [x] Audio preview 14 | - [ ] Image zooming 15 | ---- 16 | date: 2018-03-05 17 | ---- 18 | visible: true -------------------------------------------------------------------------------- /content/issues/hub/index.txt: -------------------------------------------------------------------------------- 1 | title: Hub 2 | ---- 3 | view: issue 4 | ---- 5 | tags: 6 | - todo 7 | 8 | ---- 9 | text: The hub contains the Network, Guides, Docs, and Log. 10 | 11 | - [ ] Sticky header (scroll) 12 | - [ ] Network coming soon splash 13 | ---- 14 | date: 2018-03-05 15 | ---- 16 | visible: true -------------------------------------------------------------------------------- /content/issues/index.txt: -------------------------------------------------------------------------------- 1 | title: Issues 2 | ---- 3 | view: issues 4 | ---- 5 | text: ## Contributing 6 | 7 | Feel free to fork the panel repository, create whatever changes you’d like by following the pattern here (or using the panel itself to manage issues), then creating a pull request. Wanting to consolidate things outside of Github Issues to make things offline accessibile. -------------------------------------------------------------------------------- /content/issues/inline-editing/index.txt: -------------------------------------------------------------------------------- 1 | title: Inline editing 2 | ---- 3 | view: issue 4 | ---- 5 | tags: 6 | - idea 7 | 8 | ---- 9 | text: Explore implimentation of inline-editing in a design. 10 | 11 | - Web API `isOwner` to enable `contenteditable` 12 | ---- 13 | date: 2018-03-05 14 | ---- 15 | visible: true -------------------------------------------------------------------------------- /content/issues/library/index.txt: -------------------------------------------------------------------------------- 1 | title: Library 2 | ---- 3 | view: issue 4 | ---- 5 | text: Need to clean up some of the boundaries between things. 6 | 7 | - [ ] Localization support 8 | 9 | ---- 10 | tags: 11 | - todo 12 | 13 | ---- 14 | date: 2018-03-05 15 | ---- 16 | visible: true -------------------------------------------------------------------------------- /content/issues/page-functionality/index.txt: -------------------------------------------------------------------------------- 1 | title: Page functionality 2 | ---- 3 | view: issue 4 | ---- 5 | text: Need to improve some management issues with Pages. 6 | 7 | - [ ] If no `view` key is available, fallback to parent page blueprint’s setting 8 | - [x] Field for `hidden` toggle 9 | - [x] Field for adjusting `view` in Settings. 10 | - [x] Field for adjusting `pathname` in Settings. 11 | ---- 12 | tags: 13 | - todo 14 | 15 | ---- 16 | date: 2018-03-05 17 | ---- 18 | visible: true -------------------------------------------------------------------------------- /content/issues/panel-layouts/index.txt: -------------------------------------------------------------------------------- 1 | title: Panel layouts 2 | ---- 3 | view: issue 4 | ---- 5 | text: Fields will be both **input** and **display**. An example of an input field is `text` and `textarea`. An an example of a display field is `pages` or `files` in the existing sidebar. 6 | 7 | Nothing is needed to make display fields possible, however it should be possible to not only place custom fields in the sidebar, but be able to define custom columns an layouts from within blueprints. 8 | 9 | For instance: 10 | 11 | ``` 12 | { 13 | "title": "hi", 14 | "layout": { 15 | "navigation": { 16 | "fields": ['pages', 'files'], 17 | "width": "1/3" 18 | }, 19 | "fieldset": { 20 | "fields": ['title', 'text'], 21 | "width": "2/3" 22 | } 23 | }, 24 | "fields": { 25 | "pages": { 26 | "type": "pages" 27 | }, 28 | "files": { 29 | "type": "files" 30 | }, 31 | "title": { 32 | "label": "Title", 33 | "type": "text" 34 | }, 35 | "text": { 36 | "label": "Text", 37 | "type": "textarea" 38 | } 39 | } 40 | } 41 | ``` 42 | 43 | ## Features 44 | 45 | - `sticky` option for columns to replicate sidebar functionality 46 | ---- 47 | tags: 48 | - todo 49 | 50 | ---- 51 | date: 2018-03-05 52 | ---- 53 | visible: false -------------------------------------------------------------------------------- /content/issues/saving/index.txt: -------------------------------------------------------------------------------- 1 | title: Saving 2 | ---- 3 | view: issue 4 | ---- 5 | tags: 6 | - 0.1.1 7 | - todo 8 | 9 | ---- 10 | text: ### Quick ones 11 | 12 | - [ ] Local storage for changes? 13 | - [ ] Saving title change saves to index.html, too 14 | - [ ] Meta tags in index.html 15 | - [ ] How to speed things up (reload all state) 16 | ---- 17 | date: 2018-03-05 18 | ---- 19 | visible: true -------------------------------------------------------------------------------- /content/issues/sites/index.txt: -------------------------------------------------------------------------------- 1 | title: Sites 2 | ---- 3 | view: issue 4 | ---- 5 | tags: 6 | - todo 7 | - 1.0.1 8 | 9 | ---- 10 | text: ## Todo 11 | 12 | - [x] Settings → Remove 13 | - [ ] Settings → HTTP fallback 14 | - [ ] Reordering 15 | - [ ] Persist across domains (ditch localstorage?) 16 | - [ ] Store references to designs inside of `/content` instead of the `designs.js` plugin 17 | 18 | ## Ideas 19 | 20 | - Some sort of p2p stats, such as how many peers, etc… 21 | - A simple way of deploying to a service like Hashbase 22 | ---- 23 | date: 2018-03-05 24 | ---- 25 | visible: true -------------------------------------------------------------------------------- /content/log/index.txt: -------------------------------------------------------------------------------- 1 | title: Log 2 | ---- 3 | view: log 4 | ---- 5 | text: ## 0.1.0 6 | 7 | This is the first release of Enoki to run in Beaker following the Alpha period. Too many changes to list, but a quick summarization follows. 8 | 9 | ### Fields 10 | 11 | - Added display fields (pages/files) 12 | - New checkbox field 13 | - New color field 14 | - New range field 15 | - Simplified how data is passed to fields from fieldsets 16 | 17 | ### Interface 18 | 19 | - Added `layouts` to blueprints, enabling positioning of fields 20 | - Added support for selecting a design when creating a site 21 | - Created the **Guides** section in Hub 22 | - Modularized navigational elements into composable wrappers 23 | 24 | ### Designs 25 | 26 | - Cleaned up the existing Enoki Starterkit design 27 | - Created the new **System** design, the most basic functional design 28 | 29 | ### Functionality 30 | 31 | - Created the `smarkt` module for reading files 32 | - Created the `hypha` modules for reading folders 33 | - Updated the `enoki` module with these new dependencies 34 | - Added async/promises to the API for easy use with Beaker’s Web Api 35 | - Implimented early HTTP support with `content.json` fallback -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enokidotsite/panel/12b6d563f0c23ba9f42f9fb1a23e59cf7ea2c0fb/favicon.ico -------------------------------------------------------------------------------- /favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enokidotsite/panel/12b6d563f0c23ba9f42f9fb1a23e59cf7ea2c0fb/favicon.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Enoki Panel 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |

Enoki Panel (beta)

2 | 3 | ![](assets/meta.jpg) 4 | 5 | The Enoki Panel is an ultralight set of tools for creating websites and applications. It’s early in development and runs exclusively in the experimental [Beaker Browser](https://beakerbrowser.com) as some of the primary concerns the project engages are those of data ownership, archival, and platform mutability. 6 | 7 | Consider this a modestly functional sketch. A starting point with clear limitations, leaning on existing and familiar convention to form a foundation for spanning the gap between here and there. 8 | 9 | ## Features 10 | 11 | - **nodb**: only static files and folders 12 | - **extensible**: easily define custom fieldsets and create your own fields 13 | - **simple**: built entirely on [choo](https://choo.io), the cutest front-end framework 14 | - **offline**: create and edit your sites offline, sync when reconnecting 15 | 16 | ## Development 17 | 18 | - Clone this repository 19 | - Open Beaker Browser and create a new site 20 | - Change the folder to the repository 21 | - **`cd source`** open the source directory 22 | - **`npm install`** install dependencies 23 | - **`npm start`** watch for changes 24 | - **`npm build`** bundle for production 25 | 26 | You now have a fully standalone instance of the Enoki Panel free modify however you’d like. I’d suggest if you’d like to receive updates to create a git repository and branch for your changes. Add this repository as a source and merge in changes every once in a while. 27 | 28 | ## Guides 29 | 30 | For help getting started, managing your sites, and customization, open the Enoki Panel and navigate to **Hub** → **Guides**. -------------------------------------------------------------------------------- /source/blueprints/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Default", 3 | "layout": { 4 | "one": { 5 | "fields": ["pages", "files"], 6 | "sticky": true, 7 | "width": "1/3" 8 | }, 9 | "two": { 10 | "fields": true, 11 | "width": "2/3" 12 | } 13 | }, 14 | "fields": { 15 | "pages": { 16 | "label": "Pages", 17 | "type": "pages" 18 | }, 19 | "files": { 20 | "label": "Files", 21 | "type": "files" 22 | }, 23 | "title": { 24 | "label": "Title", 25 | "type": "text" 26 | }, 27 | "text": { 28 | "label": "Text", 29 | "type": "textarea" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /source/blueprints/page-header.json: -------------------------------------------------------------------------------- 1 | { 2 | "layout": false, 3 | "fields": { 4 | "name": { 5 | "label": "Pathname", 6 | "type": "text", 7 | "width": "1/4" 8 | }, 9 | "view": { 10 | "label": "View", 11 | "type": "dropdown", 12 | "width": "1/4", 13 | "options": { 14 | "one": "Hey there", 15 | "two": "Sick job" 16 | } 17 | }, 18 | "visible": { 19 | "label": "Visible", 20 | "true": "Is visible", 21 | "false": "Is not visible", 22 | "type": "checkbox", 23 | "width": "1/4" 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /source/blueprints/sites-create.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Create Site", 3 | "layout": false, 4 | "fields": { 5 | "title": { 6 | "label": "Title", 7 | "type": "text", 8 | "required": true 9 | }, 10 | "description": { 11 | "label": "Description", 12 | "type": "textarea", 13 | "toolbar": false, 14 | "required": true 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /source/components/actionbar.js: -------------------------------------------------------------------------------- 1 | var html = require('choo/html') 2 | 3 | module.exports = ActionBar 4 | 5 | function ActionBar (props) { 6 | props = props || { } 7 | var saveText = props.saveText || 'Save Changes' 8 | var cancelText = props.cancelText || 'Cancel' 9 | var disabled = (props.disabled === undefined) ? false : props.disabled 10 | var disabledClass = props.disabled ? 'pen dn' : 'x xjc' 11 | 12 | return html` 13 |
14 | ${props.handleCancel ? cancel() : ''} 15 | ${save()} 16 |
17 | ` 18 | 19 | function save () { 20 | return html` 21 |
22 | 26 |
27 | ` 28 | } 29 | 30 | function cancel () { 31 | return html` 32 |
33 |
${cancelText}
37 |
38 | ` 39 | } 40 | } -------------------------------------------------------------------------------- /source/components/breadcrumbs.js: -------------------------------------------------------------------------------- 1 | var queryString = require('query-string') 2 | var objectKeys = require('object-keys') 3 | var html = require('choo/html') 4 | 5 | var methodsFile = require('../lib/file') 6 | 7 | module.exports = container 8 | 9 | function container (props) { 10 | return html` 11 |
12 |
13 |
14 | home 15 | 18 |
19 |
20 |
21 | ` 22 | } 23 | 24 | function breadcrumbs (props) { 25 | props = props || { } 26 | var page = props.page || { } 27 | var path = page.url || '' 28 | var search = queryString.parse(location.search) 29 | 30 | var searchPaths = objectKeys(search) 31 | .reduce(function (result, key) { 32 | if (key !== 'file') return result 33 | if (key === 'file' && search[key] === 'new') return result 34 | var value = methodsFile.decodeFilename(search[key]) 35 | result.push({ 36 | path: '', 37 | el: html`${value}` 38 | }) 39 | return result 40 | }, [ ]) 41 | 42 | var pagePaths = path 43 | .split('/') 44 | .filter(str => str) 45 | .reduce(function (result, path) { 46 | var href = result.map(crumb => crumb.path).join('/') + '/' + path 47 | result.push({ 48 | path: path, 49 | el: html`${path}` 50 | }) 51 | return result 52 | }, [{ path: '', el: ''}]) 53 | 54 | return pagePaths 55 | .concat(searchPaths) 56 | .reverse() 57 | .reduce(function (arr, crumb) { 58 | arr.push(crumb.el) 59 | return arr 60 | }, [ ]) 61 | } -------------------------------------------------------------------------------- /source/components/field.js: -------------------------------------------------------------------------------- 1 | var html = require('choo/html') 2 | 3 | var fields = require('../fields') 4 | var cache = { } 5 | 6 | module.exports = Field 7 | 8 | function Field (props, emit) { 9 | props = props || { } 10 | 11 | props.content = props.content || { } 12 | props.fields = props.fields || { } 13 | props.field = props.field || { } 14 | props.query = props.query || { } 15 | props.page = props.page || { } 16 | props.site = props.site || { } 17 | 18 | var type = props.field.type.toLowerCase() 19 | 20 | // grab the input, or fallback to text 21 | var input = (typeof fields[type] === 'function') 22 | ? fields[type] 23 | : fields.text 24 | 25 | // field properties 26 | var width = getWidth(props.field.width) 27 | 28 | // public 29 | return html` 30 |
31 | ${wrapper(props, emit)} 32 |
33 | ` 34 | 35 | function label () { 36 | return html` 37 |
38 | ${props.field.label || props.field.key} 39 |
40 | ` 41 | } 42 | 43 | // wrap the field in a cache for nanocomponent 44 | function wrapper (props, emit) { 45 | if (!cache[props.field.id]) cache[props.field.id] = new input() 46 | var hasLabel = cache[props.field.id].label !== false && props.field.label !== false 47 | 48 | return [ 49 | hasLabel ? label() : '', 50 | cache[props.field.id].render(props, emit) 51 | ] 52 | } 53 | } 54 | 55 | function getWidth (width) { 56 | var widths = { 57 | false: '', 58 | auto: 'xx', 59 | '1/2': 'c12', 60 | '1/2': 'c6', 61 | '1/3': 'c4', 62 | '1/4': 'c3', 63 | '2/3': 'c8', 64 | '3/4': 'c10' 65 | } 66 | 67 | if (typeof width === 'undefined') return 'c12' 68 | var setting = widths[width.toString()] 69 | return typeof setting === 'undefined' ? 'c12' : setting 70 | } 71 | -------------------------------------------------------------------------------- /source/components/format.js: -------------------------------------------------------------------------------- 1 | var taskLists = require('markdown-it-task-lists') 2 | var MarkdownIt = require('markdown-it') 3 | var raw = require('choo/html/raw') 4 | var md = new MarkdownIt() 5 | .use(taskLists) 6 | 7 | module.exports = format 8 | 9 | function format (str) { 10 | return raw(md.render(str || '')) 11 | } 12 | -------------------------------------------------------------------------------- /source/components/guide-thumbnail.js: -------------------------------------------------------------------------------- 1 | var html = require('choo/html') 2 | 3 | module.exports = guideThumbnail 4 | 5 | function guideThumbnail (props) { 6 | return html` 7 | 12 |
13 |
14 |

15 | ${props.title} 16 |

17 |
18 |

${props.excerpt}

19 |
20 |
21 |
22 | ` 23 | 24 | function renderImage () { 25 | return html` 26 |
29 | >
30 | ` 31 | } 32 | } -------------------------------------------------------------------------------- /source/components/header.js: -------------------------------------------------------------------------------- 1 | var queryString = require('query-string') 2 | var objectKeys = require('object-keys') 3 | var html = require('choo/html') 4 | var xtend = require('xtend') 5 | 6 | var Breadcrumbs = require('./breadcrumbs') 7 | 8 | var links = { 9 | hub: { 10 | name: 'hub', 11 | title: 'Hub', 12 | icon: 'home' 13 | }, 14 | sites: { 15 | name: 'sites', 16 | title: 'Sites', 17 | icon: 'clone' 18 | }, 19 | editor: { 20 | name: 'editor', 21 | title: 'Editor', 22 | icon: 'i-cursor' 23 | } 24 | } 25 | 26 | module.exports = header 27 | 28 | function header (state, emit) { 29 | var search = queryString.parse(location.search) 30 | 31 | var linksState = { 32 | editor: { 33 | active: typeof search.url !== 'undefined' && state.sites.active, 34 | url: '?url=' + state.ui.history.editor 35 | }, 36 | sites: { 37 | active: typeof search.sites !== 'undefined', 38 | url: '?sites=' + state.ui.history.sites 39 | }, 40 | hub: { 41 | active: state.route.indexOf('hub') >= 0, 42 | url: '/#hub/' + state.ui.history.hub 43 | } 44 | } 45 | 46 | // lame fallback 47 | if ( 48 | !linksState.editor.active && 49 | !linksState.sites.active && 50 | !linksState.hub.active 51 | ) { 52 | if (state.sites.active) linksState.editor.active = true 53 | else linksState.sites.active = true 54 | } 55 | 56 | // non p2p 57 | if (!state.sites.p2p) return 58 | 59 | return html` 60 | 75 | ` 76 | 77 | function renderLink (props) { 78 | var activeClass = props.active ? 'bgc-bg fc-fg' : 'fc-bg25 bgc-bg90 fch-bg' 79 | return html` 80 |
81 | 85 | 86 | 87 | 88 | ${props.name === 'editor' ? renderChanges() : ''} 89 |
90 | ` 91 | } 92 | 93 | function renderChanges () { 94 | var changes = objectKeys(state.enoki.changes) 95 | var isActive = changes.length > 0 && linksState.editor.active 96 | var urlChanges = unescape(queryString.stringify( 97 | xtend({ changes: 'all' }, state.query) 98 | )) 99 | 100 | return html` 101 |
102 | ${changes.length || 1} 106 |
107 | ` 108 | } 109 | } -------------------------------------------------------------------------------- /source/components/modal.js: -------------------------------------------------------------------------------- 1 | var Nanocomponent = require('nanocomponent') 2 | var html = require('choo/html') 3 | 4 | module.exports = Modal 5 | 6 | function Modal () { 7 | if (!(this instanceof Modal)) return new Modal() 8 | this.handleContainerClick = this.handleContainerClick.bind(this) 9 | Nanocomponent.call(this) 10 | } 11 | 12 | Modal.prototype = Object.create(Nanocomponent.prototype) 13 | 14 | Modal.prototype.createElement = function (props) { 15 | this.content = props.content 16 | this.handleContainerClick = props.handleContainerClick || this.handleContainerClick 17 | this.className = props.className || '' 18 | 19 | return html` 20 | 36 | ` 37 | } 38 | 39 | Modal.prototype.update = function (state) { 40 | return true 41 | } 42 | 43 | Modal.prototype.handleContainerClick = function(event) { 44 | 45 | } -------------------------------------------------------------------------------- /source/components/publish.js: -------------------------------------------------------------------------------- 1 | var html = require('choo/html') 2 | var Nanocomponent = require('nanocomponent') 3 | 4 | module.exports = function wrapper () { 5 | if (!(this instanceof Publish)) return new Publish() 6 | } 7 | 8 | class Publish extends Nanocomponent { 9 | constructor () { 10 | super() 11 | } 12 | 13 | createElement (props) { 14 | return html` 15 |
16 | Publish 17 |
18 | ` 19 | } 20 | 21 | update () { 22 | return true 23 | } 24 | } -------------------------------------------------------------------------------- /source/components/sidebar.js: -------------------------------------------------------------------------------- 1 | var objectValues = require('object-values') 2 | var queryString = require('query-string') 3 | var html = require('choo/html') 4 | var xtend = require('xtend') 5 | 6 | var Uploader = require('../components/uploader') 7 | var methodsFile = require('../lib/file') 8 | var uploader = Uploader() 9 | 10 | module.exports = sidebar 11 | 12 | function sidebar (props, emit) { 13 | props = props || { } 14 | props.pagesActive = props.pagesActive === true 15 | props.filesActive = props.filesActive === true 16 | props.uploadActive = props.uploadActive === true 17 | 18 | var pagePages = objectValues(props.page.pages || { }).map(function (pagePage) { 19 | return props.content[pagePage.url] 20 | }) 21 | 22 | var pageFiles = objectValues(props.page.files || { }).map(function (pageFile) { 23 | var data = xtend(pageFile, { }) 24 | data.urlPanel = queryString.stringify(xtend({ 25 | file: methodsFile.encodeFilename(pageFile.filename) 26 | }, props.query)) 27 | data.urlPanel = unescape(data.urlPanel) 28 | return data 29 | }) 30 | 31 | return html` 32 | 38 | ` 39 | 40 | function elPage () { 41 | return html` 42 | 66 | 67 | ` 68 | } 69 | 70 | function elPages () { 71 | var urlPageNew = unescape(queryString.stringify(xtend({ page: 'new' }, props.query))) 72 | var urlPagesAll = unescape(queryString.stringify(xtend({ pages: 'all' }, props.query))) 73 | return html` 74 | 88 | ` 89 | } 90 | 91 | function elFiles () { 92 | var urlFileNew = unescape(queryString.stringify(xtend({ file: 'new' }, props.query))) 93 | var urlFilesAll = unescape(queryString.stringify(xtend({ files: 'all' }, props.query))) 94 | return html` 95 | 113 | 114 | ` 115 | } 116 | 117 | function elUploadContainer () { 118 | return html` 119 |
123 | ${uploader.render({ 124 | text: 'Drag and drop here to add file', 125 | handleFiles: props.handleFilesUpload, 126 | handleDragEnter: function (event) { 127 | var el = event.target.parentNode.parentNode.parentNode 128 | el.classList.remove('bgc-bg', 'fc-fg') 129 | el.classList.add('bgc-fg', 'fc-bg') 130 | }, 131 | handleDragLeave: function (event) { 132 | var el = event.target.parentNode.parentNode.parentNode 133 | el.classList.add('bgc-bg', 'fc-fg') 134 | el.classList.remove('bgc-fg', 'fc-bg') 135 | } 136 | }, emit)} 137 |
138 | ` 139 | } 140 | 141 | function handleFilesAdd (event) { 142 | uploader.open() 143 | event.preventDefault() 144 | } 145 | } 146 | 147 | function elsChildren (children) { 148 | children = children || [ ] 149 | 150 | if (children.length <= 0) { 151 | return html` 152 |
  • 153 | No sub-pages 154 |
  • 155 | ` 156 | } 157 | 158 | return children 159 | .slice(0, 6) 160 | .map(function (child) { 161 | if (!child.url) return 162 | return html` 163 |
  • 164 | ${child.title || child.name} 169 |
  • 170 | ` 171 | 172 | function handleDragStart (event) { 173 | event.dataTransfer.setData('text/plain', `[${child.title}](${child.url})`) 174 | } 175 | }) 176 | } 177 | 178 | function elsFiles (files) { 179 | files = files || [ ] 180 | 181 | // Hide if there is nothing 182 | if (files.length <= 0) return html` 183 |
  • 184 | No files 185 |
  • 186 | ` 187 | 188 | return files 189 | .slice(0, 6) 190 | .map(function (child) { 191 | if (!child.url) return 192 | return html` 193 |
  • 194 | ${child.filename} 199 |
  • 200 | ` 201 | 202 | function handleDragStart (event) { 203 | event.dataTransfer.setData('text/plain', '![](' +child.source + ')') 204 | } 205 | }) 206 | } -------------------------------------------------------------------------------- /source/components/split.js: -------------------------------------------------------------------------------- 1 | var html = require('choo/html') 2 | 3 | module.exports = Split 4 | 5 | function Split (left, right) { 6 | return html` 7 |
    8 |
    9 | ${left} 10 |
    11 |
    12 | ${right} 13 |
    14 |
    15 | ` 16 | } 17 | -------------------------------------------------------------------------------- /source/components/uploader.js: -------------------------------------------------------------------------------- 1 | var Nanocomponent = require('nanocomponent') 2 | var objectValues = require('object-values') 3 | var html = require('choo/html') 4 | 5 | module.exports = function Wrapper () { 6 | if (!(this instanceof Uploader)) return new Uploader() 7 | } 8 | 9 | class Uploader extends Nanocomponent { 10 | constructor () { 11 | super() 12 | this.state = { 13 | 14 | } 15 | 16 | this.open = this.open.bind(this) 17 | this.handleChange = this.handleChange.bind(this) 18 | this.handleDragEnter = this.handleDragEnter.bind(this) 19 | this.handleDragLeave = this.handleDragLeave.bind(this) 20 | } 21 | 22 | createElement (props) { 23 | this.props = props || { } 24 | this.text = this.props.text || 'Drag and drop here' 25 | this.active = this.props.active || false 26 | 27 | return html` 28 |
    29 |
    30 | 39 |
    40 | ${this.text} 41 |
    42 |
    43 |
    44 | ` 45 | } 46 | 47 | handleChange (event) { 48 | var self = this 49 | var files = event.srcElement.files 50 | 51 | // if there are files and we can upload, go for it 52 | if (files && this.props.upload !== false) { 53 | if (self.props.handleFiles) { 54 | self.props.handleFiles('upload', { 55 | files: files 56 | }) 57 | } 58 | } 59 | 60 | // little callback handler 61 | if (this.props.handleChange) { 62 | this.props.handleChange('change', { 63 | files: files ? files : { } 64 | }) 65 | } 66 | } 67 | 68 | handleDragEnter (event) { 69 | if (this.props.handleDragEnter) { 70 | this.props.handleDragEnter(event) 71 | } 72 | } 73 | 74 | handleDragLeave (event) { 75 | if (this.props.handleDragLeave) { 76 | this.props.handleDragLeave(event) 77 | } 78 | } 79 | 80 | open () { 81 | var input = this.element.querySelector('input') 82 | if (input) input.click() 83 | } 84 | 85 | update (props) { 86 | return true 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /source/containers/fields.js: -------------------------------------------------------------------------------- 1 | var queryString = require('query-string') 2 | var objectKeys = require('object-keys') 3 | var Page = require('enoki/page') 4 | var html = require('choo/html') 5 | var assert = require('assert') 6 | var xtend = require('xtend') 7 | 8 | var blueprintDefault = require('../blueprints/default.json') 9 | var Field = require('../components/field') 10 | var methodsPage = require('../lib/page') 11 | 12 | /** 13 | * todo 14 | * - create `fields` component for array of fields 15 | * - create `page-fields` container for layout logic 16 | */ 17 | 18 | module.exports = Fields 19 | 20 | function Fields (state, emit, props) { 21 | assert.equal(typeof state, 'object', 'fields: typeof arg1 "state" must be type object') 22 | assert.equal(typeof state.content, 'object', 'fields: typeof arg1 "state.content" must be type object') 23 | assert.equal(typeof state.query, 'object', 'fields: typeof arg1 "state.query" must be type object') 24 | assert.equal(typeof state.site, 'object', 'fields: typeof arg1 "state.site" must be type object') 25 | 26 | props = props || { } 27 | var search = queryString.parse(location.search) 28 | var page = Page(xtend(state, { href: search.url })) 29 | var active = page() 30 | 31 | var blueprint = props.blueprint || methodsPage.getBlueprint(state, emit, page) 32 | var changes = methodsPage.getChanges(state, emit, active) 33 | var value = props.value || active.value() 34 | 35 | // if no layout return unwrapped fields 36 | if (blueprint.layout === false) { 37 | return objectKeys(blueprint.fields || { }).map(createField) 38 | } 39 | 40 | // field and layout fallback 41 | blueprint.fields = blueprint.fields || blueprintDefault.fields 42 | blueprint.layout = blueprint.layout || blueprintDefault.layout 43 | 44 | // fields wrapped in layout 45 | return objectKeys(blueprint.layout).map(function (key) { 46 | var column = blueprint.layout[key] 47 | var widths = { '1/1': 'c12', '1/2': 'c6', '1/3': 'c4', '2/3': 'c8' } 48 | var width = widths[column.width || '1/2'] 49 | var fields = getFields() 50 | 51 | return html` 52 |
    53 |
    54 | ${fields} 55 |
    56 |
    57 | ` 58 | 59 | function getFields () { 60 | // show all unsorted fields 61 | if (column.fields === true) { 62 | return objectKeys(blueprint.fields) 63 | .filter(function (key) { 64 | // does the field not appear in a column? 65 | return objectKeys(blueprint.layout) 66 | .reduce(function (result, active) { 67 | var fields = blueprint.layout[active].fields 68 | if (result && typeof fields === 'object') { 69 | result = fields.indexOf(key) < 0 70 | } 71 | return result 72 | }, true) 73 | }) 74 | .map(createField) 75 | // custom fields 76 | } else if (typeof column === 'object') { 77 | return column.fields.map(createField) 78 | } 79 | } 80 | }) 81 | 82 | function createField (key) { 83 | var fieldProps = blueprint.fields[key] 84 | var defaultProps = blueprintDefault.fields[key] 85 | 86 | if (!fieldProps) { 87 | if (!defaultProps || blueprint[key] === false) { 88 | return // nothing to see here 89 | } else { 90 | fieldProps = defaultProps 91 | } 92 | } 93 | 94 | return Field({ 95 | content: state.content, 96 | events: state.events, 97 | query: state.query, 98 | page: mergeDraftAndState(), 99 | site: state.site, 100 | field: mergeFieldDraftAndState(), 101 | oninput: oninput 102 | }, emit) 103 | 104 | function mergeDraftAndState () { 105 | return xtend(value, changes) 106 | } 107 | 108 | function mergeFieldDraftAndState () { 109 | return xtend(fieldProps, { 110 | id: [value.url, value.view, key].filter(str => str).join(':'), 111 | key: key, 112 | value: (changes && changes[key] !== undefined) 113 | ? changes[key] 114 | : value[key] 115 | }) 116 | } 117 | 118 | function oninput (data) { 119 | if (typeof data !== 'object') return 120 | if (typeof data.value === 'undefined') return 121 | props.oninput(key, data.value) 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /source/containers/page-fields.js: -------------------------------------------------------------------------------- 1 | var objectKeys = require('object-keys') 2 | var xtend = require('xtend') 3 | var html = require('choo/html') 4 | 5 | var blueprintDefault = require('../blueprints/default.json') 6 | 7 | module.exports = pageFields 8 | 9 | function pageFields (state, emit) { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /source/containers/page-header.js: -------------------------------------------------------------------------------- 1 | var queryString = require('query-string') 2 | var raw = require('choo/html/raw') 3 | var html = require('choo/html') 4 | var xtend = require('xtend') 5 | var path = require('path') 6 | 7 | var blueprintSettings = require('../blueprints/page-header.json') 8 | var blueprintDefault = require('../blueprints/default') 9 | var methodsPage = require('../lib/page') 10 | var fields = require('./fields') 11 | 12 | module.exports = pageHeader 13 | 14 | function pageHeader (state, emit) { 15 | var search = queryString.parse(location.search) 16 | var blueprint = getParentBlueprint() 17 | var draftPage = getDraftPage() 18 | var views = methodsPage.getViews({ 19 | blueprints: state.site.blueprints, 20 | blueprint: blueprint 21 | }) 22 | 23 | // views 24 | if (views) { 25 | blueprintSettings.fields.view.options = views 26 | } 27 | 28 | return html` 29 |
    30 |
    31 |
    32 | 37 |
    38 | 41 | ${elMeta()} 42 |
    43 | ${search.settings && state.page.url && state.page.url ? PageSettings() : ''} 44 |
    45 | ` 46 | 47 | function PageSettings () { 48 | return html` 49 |
    50 |
    51 |
    52 |
    53 | ${fields(state, emit, { 54 | oninput: handleFieldUpdate, 55 | blueprint: blueprintSettings, 56 | values: state.page, 57 | })} 58 |
    59 |
    60 | Delete 61 |
    62 | Delete Page 66 |
    67 |
    68 | ` 69 | } 70 | 71 | function elMeta () { 72 | var settingsUrl = search.settings ? unescape(queryString.stringify({ url: state.page.url })) : unescape(queryString.stringify(xtend(state.query, { settings: 'active' }))) 73 | var settingsClass = search.settings ? 'fc-fg' : 'fc-bg25 fch-fg' 74 | return html` 75 |
    76 |
    77 | Settings 78 |
    79 |
    80 | Open 85 |
    86 |
    87 | ` 88 | } 89 | 90 | function getDraftPage () { 91 | return state.enoki && state.page && state.enoki.changes[state.page.url] 92 | } 93 | 94 | function getParentBlueprint () { 95 | if (!state.page || !state.site.loaded) return { } 96 | try { 97 | var parent = path.join(state.page.url, '../').replace(/\/$/, '') 98 | var parentState = state.content[parent] 99 | 100 | return ( 101 | state.site.blueprints[parentState.view] || 102 | state.site.blueprints.default || 103 | blueprintDefault 104 | ) 105 | } catch (err) { 106 | return blueprintDefault 107 | } 108 | } 109 | 110 | function handleFieldUpdate (key, data) { 111 | emit(state.events.ENOKI_UPDATE, { 112 | path: state.page.path, 113 | url: state.page.url, 114 | data: { [key]: data } 115 | }) 116 | } 117 | 118 | function handleRemovePage () { 119 | emit(state.events.ENOKI_REMOVE, { 120 | confirm: true, 121 | title: state.page.title, 122 | path: state.page.path, 123 | url: state.page.url 124 | }) 125 | } 126 | } -------------------------------------------------------------------------------- /source/containers/page-new.js: -------------------------------------------------------------------------------- 1 | var Nanocomponent = require('nanocomponent') 2 | var html = require('choo/html') 3 | 4 | var methodsFile = require('../lib/file') 5 | var fields = require('../fields') 6 | 7 | var Title = new fields.text() 8 | var Uri = new fields.text() 9 | var View = new fields.dropdown() 10 | 11 | module.exports = wrapper 12 | 13 | function wrapper () { 14 | if (!(this instanceof PageNew)) return new PageNew() 15 | } 16 | 17 | class PageNew extends Nanocomponent { 18 | constructor () { 19 | super() 20 | this.id = 'pageAdd' 21 | this.customUri = false 22 | 23 | this.state = { 24 | 25 | } 26 | 27 | this.handleCancel = this.handleCancel.bind(this) 28 | this.handleTitle = this.handleTitle.bind(this) 29 | this.handleView = this.handleView.bind(this) 30 | this.handleSave = this.handleSave.bind(this) 31 | this.handleUri = this.handleUri.bind(this) 32 | } 33 | 34 | createElement (state, emit) { 35 | this.key = state.key 36 | this.views = state.views || { } 37 | this.value = state.value || { } 38 | this.value.view = state.view || 'default' 39 | this.emit = emit 40 | 41 | return html` 42 |
    43 |
    44 |
    45 | ${this.elTitle()} 46 | ${this.elUri()} 47 | ${this.elView()} 48 |
    49 | ${this.elActions()} 50 |
    51 |
    52 | ` 53 | } 54 | 55 | load (element) { 56 | var title = element.querySelector('[name="title"]') 57 | if (title && title.focus) title.focus() 58 | } 59 | 60 | unload () { 61 | this.customUri = false 62 | } 63 | 64 | elTitle () { 65 | var titleProps = { 66 | oninput: this.handleTitle, 67 | field: { 68 | id: 'pageAdd', 69 | key: 'title', 70 | value: this.value.title || '' 71 | } 72 | } 73 | return html` 74 |
    75 |
    76 | Title 77 |
    78 | ${Title.render(titleProps, this.emit)} 79 |
    80 | ` 81 | } 82 | 83 | elView () { 84 | var viewProps = { 85 | oninput: this.handleView, 86 | field: { 87 | key: 'dropdown', 88 | options: this.views, 89 | value: this.value.view 90 | } 91 | } 92 | 93 | return html` 94 |
    95 |
    96 | View 97 |
    98 | ${View.render(viewProps, this.emit)} 99 |
    100 | ` 101 | } 102 | 103 | elUri () { 104 | var uriProps = { 105 | field: { id: 'pageAdd', key: 'uri', value: this.value.uri || '' }, 106 | oninput: this.handleUri 107 | } 108 | return html` 109 |
    110 |
    111 | Pathname 112 |
    113 | ${Uri.render(uriProps, this.emit)} 114 |
    115 | ` 116 | } 117 | 118 | elActions () { 119 | return html` 120 |
    121 |
    122 | 127 |
    128 |
    129 | 134 |
    135 |
    136 | ` 137 | } 138 | 139 | handleTitle (data) { 140 | this.value.title = data.value 141 | if (!this.customUri) { 142 | var el = this.element.querySelector('input[name="uri"]') 143 | var value = methodsFile.sanitizeName(data.value) 144 | this.value.uri = value 145 | if (el) el.value = value 146 | } 147 | } 148 | 149 | handleUri (data) { 150 | var el = this.element.querySelector('input[name="uri"]') 151 | this.value.uri = methodsFile.sanitizeName(data.value) 152 | this.customUri = true 153 | if (el) el.value = this.value.uri 154 | } 155 | 156 | handleView (data) { 157 | this.value.view = data.value 158 | } 159 | 160 | handleSave (event) { 161 | this.emit({ key: this.key, event: 'save', value: this.value }) 162 | if (event) event.preventDefault() 163 | } 164 | 165 | handleCancel (event) { 166 | this.emit({ event: 'cancel' }) 167 | if (event) event.preventDefault() 168 | } 169 | 170 | update (props) { 171 | return props.views !== this.views 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /source/containers/wrapper-hub.js: -------------------------------------------------------------------------------- 1 | var html = require('choo/html') 2 | 3 | var Header = require('../components/header') 4 | var designOptions = require('../design/options') 5 | 6 | module.exports = wrapper 7 | 8 | function wrapper (view) { 9 | return function (state, emit) { 10 | 11 | // extend state 12 | var href = state.href.replace('/hub/', '') 13 | state.page = state.docs.content['/' + href] || { } 14 | 15 | if (state.ui.history.hub !== href) { 16 | emit(state.events.UI_HISTORY, { 17 | route: 'hub', 18 | path: href 19 | }) 20 | } 21 | 22 | return [ 23 | Header(state, emit), 24 | renderNavigation(state, emit), 25 | renderContent(), 26 | renderFooter(state, emit) 27 | ] 28 | 29 | function renderContent () { 30 | // async load content 31 | if (!state.docs.loaded) { 32 | emit(state.events.DOCS_LOAD) 33 | return html` 34 |
    35 |
    36 |
    37 | ` 38 | } else { 39 | return view(state, emit) 40 | } 41 | } 42 | } 43 | } 44 | 45 | function renderNavigation (state, emit) { 46 | var hrefActive = state.href.replace('/hub/', '') 47 | var links = ['guides', 'docs', 'log'] 48 | var highlight = state.page.background || designOptions.colors.fg 49 | 50 | return html` 51 | 66 | ` 67 | 68 | function renderLink (href) { 69 | var hrefPage = state.docs.content['/' + href] || { } 70 | var active = hrefActive.indexOf(href) >= 0 71 | var colorClass = active ? 'fc-fg' : 'fc-bg25 fch-fg' 72 | return html` 73 | 76 | ` 77 | } 78 | } 79 | 80 | function renderFooter (state, emit) { 81 | var hrefActive = state.href.replace('/hub/', '') 82 | var links = ['guides', 'docs', 'log'] 83 | 84 | return html` 85 |
    86 |
    87 | ${links.map(renderLink)} 88 |
    89 |
    90 |
    enoki
    91 |
    v${state.ui.version}
    92 |
    93 |
    94 | ` 95 | 96 | function renderLink (href) { 97 | var hrefPage = state.docs.content['/' + href] || { } 98 | var active = hrefActive.indexOf(href) >= 0 99 | var colorClass = active ? 'fc-fg' : 'fc-bg25 fch-fg' 100 | return html` 101 |
    102 | ${hrefPage.title} 103 |
    104 | ` 105 | } 106 | } 107 | 108 | function handleFocus (event) { 109 | event.target.value = 'Coming soon' 110 | } 111 | -------------------------------------------------------------------------------- /source/containers/wrapper-site.js: -------------------------------------------------------------------------------- 1 | var queryString = require('query-string') 2 | var raw = require('choo/html/raw') 3 | var html = require('choo/html') 4 | var ok = require('object-keys') 5 | var xtend = require('xtend') 6 | var path = require('path') 7 | 8 | module.exports = wrapper 9 | 10 | function wrapper (view) { 11 | return function (state, emit) { 12 | var href = state.query.url || '/' 13 | var page = state.content[href] || { } 14 | 15 | return html` 16 | 17 | ${view(xtend(state, { page: page }), emit)} 18 | ${state.enoki.loading ? loading() : ''} 19 | 20 | ` 21 | } 22 | } 23 | 24 | 25 | function loading () { 26 | return html` 27 |
    28 |
    29 |
    30 | ` 31 | } 32 | -------------------------------------------------------------------------------- /source/design/custom.js: -------------------------------------------------------------------------------- 1 | var options = require('./options') 2 | 3 | module.exports = [ 4 | typography(), 5 | copy(), 6 | media(), 7 | inputs(), 8 | extensions(), 9 | loader() 10 | ].join(' ') 11 | 12 | function typography () { 13 | return ` 14 | html { 15 | -webkit-font-smoothing: antialiased; 16 | -moz-osx-font-smoothing: grayscale; 17 | box-sizing: border-box; 18 | font-size: 62.50%; 19 | height: auto; 20 | } 21 | 22 | body { line-height: 2rem; } 23 | 24 | a { 25 | color: ${options.colors.fg}; 26 | text-decoration: none; 27 | } 28 | 29 | .truncate { 30 | white-space: nowrap; 31 | overflow: hidden; 32 | text-overflow: ellipsis; 33 | } 34 | 35 | .drtl { direction: rtl } 36 | .dltr { direction: ltr } 37 | 38 | ::-moz-selection { background: rgba(127, 127, 127, 0.5) } 39 | ::selection { background: rgba(127, 127, 127, 0.5) } 40 | 41 | .nav-button { 42 | transition: 250ms ease-out background, 250ms ease-out color, 250ms ease-out border; 43 | } 44 | 45 | .nav-tooltip { 46 | background: rgba(80, 80, 80, 0.9); 47 | color: ${options.colors.bg}; 48 | border-radius: 3px; 49 | position: absolute; 50 | font-weight: 500; 51 | left: 100%; 52 | top: 50%; 53 | pointer-events: none; 54 | margin-top: -1.5rem; 55 | margin-left: 0.25rem; 56 | height: 3rem; 57 | line-height: 3rem; 58 | padding: 0 1rem; 59 | font-size: ${options.fontSize['0-8']}rem; 60 | text-transform: uppercase; 61 | opacity: 0; 62 | transform: translateX(-0.5rem); 63 | transition: transform 100ms ease-out, opacity 100ms ease-out; 64 | } 65 | 66 | .nav-tooltip:before { 67 | content: ''; 68 | display: block; 69 | position: absolute; 70 | top: 50%; 71 | left: 0; 72 | margin-left: -0.5rem; 73 | margin-top: -0.55rem; 74 | width: 0; 75 | height: 0; 76 | border-top: 0.5rem solid transparent; 77 | border-bottom: 0.5rem solid transparent; 78 | border-right: 0.5rem solid rgba(80, 80, 80, 0.9); 79 | } 80 | 81 | .nav-tooltip-c:hover .nav-tooltip { 82 | opacity: 1; 83 | transform: translateX(0); 84 | } 85 | 86 | @font-face { 87 | font-family: 'Inter UI'; 88 | font-style: normal; 89 | font-weight: 400; 90 | src: url("/assets/fonts/Inter-UI-Regular.woff2?v=2.4") format("woff2"), 91 | url("/assets/fonts/Inter-UI-Regular.woff?v=2.4") format("woff"); 92 | } 93 | 94 | @font-face { 95 | font-family: 'Inter UI'; 96 | font-style: italic; 97 | font-weight: 400; 98 | src: url("/assets/fonts/Inter-UI-Italic.woff2?v=2.4") format("woff2"), 99 | url("/assets/fonts/Inter-UI-Italic.woff?v=2.4") format("woff"); 100 | } 101 | 102 | @font-face { 103 | font-family: 'Inter UI'; 104 | font-style: normal; 105 | font-weight: 700; 106 | src: url("/assets/fonts/Inter-UI-Bold.woff2?v=2.4") format("woff2"), 107 | url("/assets/fonts/Inter-UI-Bold.woff?v=2.4") format("woff"); 108 | } 109 | ` 110 | } 111 | 112 | function copy () { 113 | return ` 114 | .copy { 115 | line-height: 1.5; 116 | max-width: 60rem; 117 | width: 100%; 118 | } 119 | 120 | .copy h2 { 121 | font-size: ${options.fontSize['2']}rem; 122 | font-weight: 600; 123 | line-height: 1.25; 124 | } 125 | 126 | .copy h3 { 127 | font-size: ${options.fontSize['2'] * 0.75}rem; 128 | } 129 | 130 | .copy pre { 131 | background: ${options.colors.bg5}; 132 | border-radius: 3px; 133 | padding: 2rem; 134 | } 135 | 136 | .copy code { 137 | font-family: ${options.typography.mono}; 138 | background: ${options.colors.bg5}; 139 | border-radius: 3px; 140 | padding: 0.2rem; 141 | } 142 | 143 | .bgc-fg .copy code { 144 | background: ${options.colors.bg90}; 145 | } 146 | 147 | .fc-bg25 .copy a { 148 | color: ${options.colors.bg25}; 149 | } 150 | 151 | .fc-bg25 .copy h2, 152 | .fc-bg25 .copy h3 { 153 | color: ${options.colors.bg}; 154 | } 155 | 156 | .fc-bg25 .copy ol li:before { 157 | color: ${options.colors.bg70}; 158 | } 159 | 160 | .fc-bg70 .copy a { 161 | color: ${options.colors.bg70}; 162 | } 163 | 164 | .fc-bg70 .copy h2, 165 | .fc-bg70 .copy h3 { 166 | color: ${options.colors.fg}; 167 | } 168 | 169 | .fc-bg70 .copy ol li:before { 170 | color: ${options.colors.bg70}; 171 | } 172 | 173 | .copy > *, 174 | .editor-preview-side > *, 175 | .editor-preview > * { 176 | margin-top: 2rem; margin-bottom: 2rem; 177 | } 178 | 179 | .copy > h2:not(:first-child) { margin-top: 4rem; } 180 | 181 | .copy img, 182 | .editor-preview-side img, 183 | .editor-preview img { 184 | max-width: 100%; 185 | } 186 | 187 | .copy a, 188 | .editor-preview-side a, 189 | .editor-preview a { 190 | text-decoration: underline; 191 | } 192 | 193 | .copy ul li { 194 | list-style: disc; 195 | padding-left: 0rem; 196 | margin-left: 2rem; 197 | } 198 | 199 | .copy ol li { 200 | position: relative; 201 | padding-left: 3rem; 202 | margin-left: 0; 203 | } 204 | 205 | .copy ol li:before { 206 | position: absolute; 207 | left: 0; 208 | font-family: ${options.typography.mono}; 209 | } 210 | 211 | .copy ol li:nth-child(1):before { content: '1' } 212 | .copy ol li:nth-child(2):before { content: '2' } 213 | .copy ol li:nth-child(3):before { content: '3' } 214 | .copy ol li:nth-child(4):before { content: '4' } 215 | .copy ol li:nth-child(5):before { content: '5' } 216 | .copy ol li:nth-child(6):before { content: '6' } 217 | 218 | .copy input { 219 | margin: 0; 220 | line-height: 1; 221 | height: ${options.fontSize['1']}rem; 222 | } 223 | 224 | .copy-small > * { 225 | margin-top: 1rem; 226 | margin-bottom: 1rem; 227 | } 228 | 229 | .copy-small h2 { 230 | font-size: ${options.fontSize['1']}rem; 231 | font-weight: 600; 232 | line-height: 1.25; 233 | } 234 | 235 | .copy-small h3 { 236 | font-size: ${options.fontSize['0-8']}rem; 237 | } 238 | 239 | .copy > h2:not(:first-child) { 240 | margin-top: 2rem; 241 | } 242 | 243 | .copy > *:first-child, 244 | .editor-preview-side > *:first-child, 245 | .editor-preview > *:first-child { 246 | margin-top: 0 247 | } 248 | 249 | .copy > *:last-child, 250 | .editor-preview-side > *:last-child, 251 | .editor-preview > *:last-child { 252 | margin-bottom: 0 253 | } 254 | 255 | .copy-small ul li { 256 | list-style: disc; 257 | padding-left: 0; 258 | margin-left: 2rem; 259 | } 260 | 261 | .copy-small ul li.task-list-item { 262 | list-style: none; 263 | padding-left: 0; 264 | margin-left: 0; 265 | } 266 | 267 | .copy-small ul li.task-list-item input { 268 | width: 2rem; 269 | } 270 | 271 | .bgc-fg .copy-small code { 272 | background: ${options.colors.bg80}; 273 | } 274 | ` 275 | } 276 | 277 | function media () { 278 | return ` 279 | .ofc { 280 | object-fit: contain; 281 | height: 100%; 282 | width: 100%; 283 | padding: 10vmin; 284 | position: absolute; 285 | top: 0; 286 | left: 0; 287 | right: 0; 288 | bottom: 0; 289 | } 290 | 291 | .file-preview { 292 | max-height: calc(100vh - 6rem); 293 | margin: -3rem -3rem -3rem 2rem; 294 | width: calc(100% + 2rem); 295 | background-image: url('data:image/svg+xml;utf8,'); 296 | background-repeat: repeat; 297 | } 298 | 299 | @media (max-width: 767px) { 300 | .file-preview { 301 | height: 100vh; 302 | width: 100vw; 303 | margin: 0 -3rem 4rem -3rem; 304 | } 305 | 306 | .action-gradient { width: 100% } 307 | } 308 | ` 309 | } 310 | 311 | function inputs () { 312 | return ` 313 | .select { 314 | position: relative; 315 | width: 100%; 316 | } 317 | 318 | .select select { 319 | cursor: pointer; 320 | -moz-appearance: none; 321 | -webkit-appearance: none; 322 | background: ${options.colors.bg}; 323 | border: 1px solid ${options.colors.bg10}; 324 | line-height: 4rem; 325 | padding: 0 3.5rem 0 1.5rem; 326 | border-radius: 2rem; 327 | font-family: ${options.typography.sans}; 328 | line-height: 4rem; 329 | font-size: ${options.fontSize['1']}rem; 330 | font-weight: 400; 331 | outline: 0; 332 | width: 100%; 333 | } 334 | 335 | .select:before { 336 | content: '↓'; 337 | position: absolute; 338 | font-size: ${options.fontSize['1']}rem; 339 | top: 0; 340 | right: 0; 341 | padding: 1.2rem 1.5rem 0.8rem; 342 | pointer-events: none; 343 | font-family: ${options.typography.mono}; 344 | } 345 | 346 | input[type=date]::-webkit-inner-spin-button { 347 | -webkit-appearance: none; 348 | display: none; 349 | } 350 | 351 | .input { 352 | background: ${options.colors.bg}; 353 | border: 1px solid ${options.colors.bg10}; 354 | border-radius: 2rem; 355 | font-family: ${options.typography.sans}; 356 | line-height: 2rem; 357 | font-size: ${options.fontSize['1']}rem; 358 | font-weight: 400; 359 | outline: 0; 360 | width: 100%; 361 | } 362 | 363 | .input.dark { 364 | background: ${options.colors.fg}; 365 | border: 1px solid ${options.colors.bg90}; 366 | color: ${options.colors.bg}; 367 | } 368 | 369 | [tabindex] { outline: 0 } 370 | .input.lh1-5 { line-height: 1.5 } 371 | .input-disabled { color: #999 } 372 | textarea { min-height: 10rem } 373 | 374 | input { height: 4rem; line-height: 4rem; } 375 | button { outline: 0 } 376 | button:focus { outline: 0 } 377 | 378 | .button-large { 379 | user-select: none; 380 | line-height: 6rem; 381 | padding: 0 4rem; 382 | border-radius: 3rem; 383 | display: block; 384 | cursor: pointer; 385 | color: ${options.colors.bg}; 386 | font-size: ${options.fontSize['1']}rem; 387 | text-align: center; 388 | white-space: nowrap; 389 | font-weight: 600; 390 | transition: background 150ms ease-out, transform 150ms ease-out; 391 | } 392 | 393 | .tfyh { 394 | transition: background 150ms ease-out, color 150ms ease-out, transform 150ms ease-out; 395 | } 396 | 397 | .tfyh:hover, 398 | .button-large:hover { 399 | transform: translateY(-0.2rem) ; 400 | } 401 | 402 | .tfyh:active, 403 | .button-large:active { 404 | transform: translateY(0.1rem) ; 405 | transition: transform 50ms ease-out; 406 | } 407 | 408 | .button-medium { 409 | user-select: none; 410 | line-height: 4rem; 411 | height: 4rem; 412 | padding: 0 2rem; 413 | border-radius: 2rem; 414 | display: block; 415 | cursor: pointer; 416 | font-size: ${options.fontSize['1']}rem; 417 | white-space: nowrap; 418 | transition: background 150ms ease-out, color 150ms ease-out, border 150ms ease-out, transform 150ms ease-out; 419 | } 420 | 421 | .button-medium:hover { 422 | transform: translateY(-0.1rem) ; 423 | } 424 | 425 | .button-medium:active { 426 | transform: translateY(0) ; 427 | transition: transform 50ms ease-out; 428 | } 429 | 430 | .button-medium.b2-currentColor { 431 | line-height: 3.7rem; 432 | } 433 | 434 | .button-inline { 435 | display: inline-block; 436 | vertical-align: center; 437 | user-select: none; 438 | border: 1px solid ${options.colors.yellow}; 439 | color: ${options.colors.yellow}; 440 | margin: 0 0 0 0.5rem; 441 | padding: 0 1rem; 442 | line-height: 1.8rem; 443 | height: 2rem; 444 | border-radius: 1rem; 445 | text-transform: uppercase; 446 | white-space: nowrap; 447 | -webkit-user-select: none; 448 | -moz-user-select: none; 449 | -ms-user-select: none; 450 | user-select: none; 451 | cursor: pointer; 452 | position: relative; 453 | z-index: 2; 454 | transition: background 150ms ease-out, color 150ms ease-out, border 150ms ease-out, transform 150ms ease-out; 455 | } 456 | 457 | .button-inline:hover { 458 | border: 1px solid ${options.colors.fg}; 459 | color: ${options.colors.fg}; 460 | transform: translateY(-0.1rem); 461 | } 462 | 463 | .button-inline:active { 464 | transition: background 50ms ease-out, color 50ms ease-out, border 50ms ease-out, transform 50ms ease-out; 465 | transform: translateY(0); 466 | } 467 | 468 | .indicator { 469 | color: ${options.colors.bg}; 470 | font-size: ${options.fontSize['0-8']}rem; 471 | font-weight: 600; 472 | text-align: center; 473 | display: block; 474 | cursor: pointer; 475 | font-family: ${options.typography.mono}; 476 | border-radius: 1rem; 477 | line-height: 2rem; 478 | height: 2rem; 479 | width: 2rem; 480 | } 481 | 482 | .indicator-medium { 483 | font-size: 1.8rem; 484 | height: 4rem; 485 | width: 4rem; 486 | line-height: 4rem; 487 | border-radius: 2rem; 488 | } 489 | 490 | .design-thumbnail { 491 | transition: box-shadow 150ms ease-out; 492 | box-shadow: 0 0 2rem ${options.colors.bg5}; 493 | } 494 | 495 | .design-focused { 496 | 497 | } 498 | ` 499 | } 500 | 501 | function extensions () { 502 | return ` 503 | .psst { position: sticky; position: -webkit-sticky; } 504 | .br1 { border-radius: 3px } 505 | .br2 { border-radius: 2rem } 506 | 507 | .t0-75 { top: 0.75rem } 508 | 509 | .tom { transition: opacity 150ms ease-out } 510 | .tfcm { transition: color 150ms ease-out } 511 | .tbgcm { transition: background 150ms ease-out } 512 | 513 | .external:after { 514 | content: '→'; 515 | display: inline-block; 516 | transform: translateY(0.1rem) rotate(-45deg); 517 | margin-left: 0.5rem; 518 | } 519 | 520 | .action-gradient:before { 521 | content: ''; 522 | display: block; 523 | background: linear-gradient(rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.95) 25%); 524 | background: -webkit-linear-gradient(rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.95) 25%); 525 | background: -moz-linear-gradient(rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.95) 25%); 526 | position: absolute; 527 | pointer-events: none; 528 | bottom: 0; 529 | left: 0; 530 | right: 0; 531 | height: 7rem; 532 | } 533 | 534 | .myc1 > * { position: relative; } 535 | 536 | .myc1 > *:before { 537 | content: ''; 538 | position: absolute; 539 | top: 0; 540 | left: 0; 541 | right: 0; 542 | background: ${options.colors.bg10}; 543 | height: 1px; 544 | } 545 | 546 | .myc1 > *:last-child:after { 547 | content: ''; 548 | position: absolute; 549 | bottom: -1px; 550 | left: 0; 551 | right: 0; 552 | background: ${options.colors.bg10}; 553 | height: 1px; 554 | } 555 | 556 | .breadcrumbs { 557 | display: flex; 558 | width: 100%; 559 | line-height: 4rem; 560 | } 561 | 562 | a.breadcrumb, 563 | .breadcrumbs > a { 564 | display: block; 565 | position: relative; 566 | color: ${options.colors.bg25}; 567 | } 568 | 569 | a.breadcrumb, 570 | a.breadcrumb:hover, 571 | .breadcrumbs > a:first-child, 572 | .breadcrumbs > a:hover { 573 | color: ${options.colors.fg}; 574 | } 575 | 576 | .breadcrumb:before, 577 | .breadcrumbs > a:not(:first-child):before { 578 | background: ${options.colors.bg15}; 579 | content: ''; 580 | display: block; 581 | height: 3rem; 582 | width: 1px; 583 | position: absolute; 584 | top: 0.5rem; 585 | right: 0; 586 | transform: rotate(15deg); 587 | } 588 | 589 | summary { 590 | position: relative; 591 | padding-left: 2rem; 592 | } 593 | 594 | summary:before { 595 | color: ${options.colors.bg25}; 596 | font-family: ${options.typography.mono}; 597 | content: '→'; 598 | display: block; 599 | position: absolute; 600 | top: 0; 601 | left: 0; 602 | padding: 1rem 0; 603 | transition: 150ms ease-out transform; 604 | } 605 | 606 | summary:hover:before { 607 | transform: rotate(45deg); 608 | } 609 | 610 | details[open] summary:before { 611 | transform: rotate(90deg); 612 | } 613 | 614 | ::-webkit-input-placeholder { color: ${options.colors.bg25}; } 615 | ::-moz-placeholder { color: ${options.colors.bg25}; } 616 | :-ms-input-placeholder { color: ${options.colors.bg25}; } 617 | :-moz-placeholder { color: ${options.colors.bg25}; } 618 | ` 619 | } 620 | 621 | function loader () { 622 | return ` 623 | .loader { 624 | border-radius: 50%; 625 | width: 3rem; 626 | height: 3rem; 627 | } 628 | 629 | .loader { 630 | margin: 2rem; 631 | font-size: 3rem; 632 | position: relative; 633 | text-indent: -9999em; 634 | border-top: 3px solid ${options.colors.bg}; 635 | border-right: 3px solid ${options.colors.bg}; 636 | border-bottom: 3px solid ${options.colors.bg}; 637 | border-left: 3px solid ${options.colors.fg}; 638 | animation: load 1s infinite linear; 639 | } 640 | 641 | @keyframes load { 642 | 0% { transform: rotate(0deg) } 643 | 100% { transform: rotate(360deg) } 644 | } 645 | ` 646 | } -------------------------------------------------------------------------------- /source/design/guides.css: -------------------------------------------------------------------------------- 1 | .guides-grid { 2 | display: grid; 3 | grid-template-columns: repeat(auto-fill, minmax(30rem, 1fr)); 4 | grid-auto-rows: auto; 5 | grid-column-gap: 2px; 6 | grid-row-gap: 2px; 7 | padding: 2px; 8 | } 9 | 10 | .grid-column { 11 | grid-column-end: span 1; 12 | width: 100%; 13 | } 14 | 15 | .guides-grid h2 { font-size: 2.5rem } 16 | 17 | .guides-grid .copy { 18 | font-size: 1.4rem; 19 | height: 9.75rem; 20 | max-width: 40rem; 21 | } 22 | 23 | .guides-grid .guide-border { 24 | opacity: 0; 25 | pointer-events: none; 26 | position: absolute; 27 | top: 0; 28 | left: 0; 29 | right: 0; 30 | bottom: 0; 31 | transition: opacity 150ms ease-out; 32 | border: 2px solid var(--active); 33 | z-index: 3; 34 | } 35 | 36 | .guides-grid a:hover .guide-border { 37 | opacity: 1; 38 | } 39 | 40 | .guides-grid a .guide-meta { 41 | transition: 250ms ease-out transform; 42 | overflow: hidden; 43 | margin-bottom: -1rem; 44 | } 45 | 46 | .guides-grid a:hover .guide-meta { 47 | transform: translateY(-0.3rem); 48 | } 49 | 50 | .guides-grid a:active .guide-meta { 51 | transform: translateY(0rem); 52 | transition: 50ms ease-out transform; 53 | } 54 | 55 | @media (max-width: 38rem) { 56 | .grid-featured { 57 | grid-column-end: span 1; 58 | } 59 | 60 | .guides-grid h2 { font-size: 3.6rem } 61 | .guides-grid .copy { font-size: 1.8rem } 62 | } 63 | -------------------------------------------------------------------------------- /source/design/index.js: -------------------------------------------------------------------------------- 1 | var css = require('sheetify') 2 | 3 | css('nanoreset') 4 | 5 | css('./simplecolorpicker.css') 6 | css('./simplemde.css') 7 | 8 | css('./utilities.js') 9 | css('./custom.js') 10 | css('./guides.css') 11 | -------------------------------------------------------------------------------- /source/design/options.js: -------------------------------------------------------------------------------- 1 | exports.colors = { 2 | bg: '#fff', 3 | 'bg2-5': '#f7f7f7', 4 | bg5: '#eee', 5 | bg10: '#ddd', 6 | bg15: '#ccc', 7 | bg25: '#999', 8 | bg70: '#666', 9 | bg80: '#444', 10 | bg90: '#333', 11 | fg: '#1a1a1a', 12 | red: '#F21307', 13 | blue: '#005FD2', 14 | green: '#00907A', 15 | yellow: '#F8BE15', 16 | inherit: 'inherit', 17 | currentColor: 'currentColor' 18 | } 19 | 20 | exports.typography = { 21 | sans: '-apple-system, BlinkMacSystemFont, "Inter UI", "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif', 22 | mono: '"SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace' 23 | } 24 | 25 | exports.fontSize = { 26 | '0-8': 1.4, 27 | '1': 1.7, 28 | '2': 3.4, 29 | '3': 6.8 30 | } 31 | 32 | exports.spacing = [0, 0.5, 1, 1.5, 2, 3, 4, 5, 6] -------------------------------------------------------------------------------- /source/design/simplecolorpicker.css: -------------------------------------------------------------------------------- 1 | .Scp { 2 | -webkit-user-select: none; 3 | -moz-user-select: none; 4 | -ms-user-select: none; 5 | user-select: none; 6 | position: relative; 7 | background: #000; 8 | border-radius: 2px; 9 | padding: 2px!important; 10 | display: flex; 11 | position: absolute; 12 | top: calc(100% + 1rem); 13 | right: 0; 14 | z-index: 3; 15 | } 16 | 17 | .Scp:before { 18 | content: ''; 19 | display: block; 20 | position: absolute; 21 | top: -1rem; 22 | right: 2.25rem; 23 | width: 0; 24 | height: 0; 25 | border-style: solid; 26 | border-width: 0 1rem 1rem 1rem; 27 | border-color: transparent transparent #000 transparent; 28 | } 29 | 30 | .Scp-saturation { 31 | position: relative; 32 | height: 100%; 33 | cursor: crosshair; 34 | background: linear-gradient(to right, #fff, #f00); 35 | } 36 | 37 | .Scp-brightness { 38 | width: 100%; 39 | height: 100%; 40 | background: linear-gradient(rgba(255,255,255,0), #000); 41 | } 42 | .Scp-sbSelector { 43 | border: 2px solid #fff; 44 | position: absolute; 45 | width: 14px; 46 | height: 14px; 47 | background: #fff; 48 | border-radius: 10px; 49 | top: -7px; 50 | left: -7px; 51 | box-sizing: border-box; 52 | z-index: 10; 53 | } 54 | .Scp-hue { 55 | width: 19px; 56 | margin-left: 2px; 57 | height: 100%; 58 | position: relative; 59 | background: linear-gradient(#f00 0%, #f0f 17%, #00f 34%, #0ff 50%, #0f0 67%, #ff0 84%, #f00 100%); 60 | cursor: ns-resize; 61 | } 62 | .Scp-hSelector { 63 | position: absolute; 64 | background: #fff; 65 | border-bottom: 1px solid #000; 66 | right: -3px; 67 | width: 10px; 68 | height: 2px; 69 | } 70 | -------------------------------------------------------------------------------- /source/design/utilities.js: -------------------------------------------------------------------------------- 1 | var gr8 = require('gr8') 2 | var options = require('./options') 3 | var utils = [ ] 4 | 5 | utils.push({ 6 | prop: 'font-family', 7 | join: '-', 8 | vals: options.typography 9 | }) 10 | 11 | utils.push({ 12 | prop: { bgc: 'background-color' }, 13 | join: '-', 14 | vals: options.colors 15 | }) 16 | 17 | utils.push({ 18 | prop: { bgch: 'background-color' }, 19 | tail: ':hover', 20 | join: '-', 21 | vals: options.colors 22 | }) 23 | 24 | utils.push({ 25 | prop: { fc: 'color' }, 26 | join: '-', 27 | vals: options.colors 28 | }) 29 | 30 | utils.push({ 31 | prop: { fch: 'color' }, 32 | join: '-', 33 | tail: ':hover', 34 | vals: options.colors 35 | }) 36 | 37 | utils.push({ 38 | prop: { ophc: 'opacity' }, 39 | tail: ':hover .oph', 40 | vals: [0, 25, 50, 75, 100], 41 | transform: function (val) { 42 | return val / 100 43 | } 44 | }) 45 | 46 | utils.push({ 47 | prop: { oph: 'opacity' }, 48 | tail: ':hover', 49 | vals: [0, 25, 50, 75, 100], 50 | transform: function (val) { 51 | return val / 100 52 | } 53 | }) 54 | 55 | var borderWeights = [0, 1, 2] 56 | var borders = {} 57 | borderWeights.forEach(border => { 58 | Object.keys(options.colors).forEach(key => { 59 | borders[border + '-' + key] = `${border}px solid ${options.colors[key]}` 60 | }) 61 | }) 62 | 63 | utils.push({ 64 | prop: [ 65 | 'border', 66 | 'border-top', 67 | 'border-right', 68 | 'border-bottom', 69 | 'border-left' 70 | ], 71 | vals: borders 72 | }) 73 | 74 | utils.push({ 75 | prop: 'font-weight', 76 | vals: ['normal', { b: 600 }] 77 | }) 78 | 79 | module.exports = gr8({ 80 | lineHeight: [1, 1.25, 1.5], 81 | fontSize: options.fontSize, 82 | spacing: options.spacing, 83 | breakpointSelector: 'class', 84 | utils: utils 85 | }) 86 | -------------------------------------------------------------------------------- /source/fields/checkbox.js: -------------------------------------------------------------------------------- 1 | var Nanocomponent = require('nanocomponent') 2 | var html = require('choo/html') 3 | var css = require('sheetify') 4 | var xtend = require('xtend') 5 | 6 | var style = css` 7 | :host label { 8 | border-radius: 1.75rem; 9 | background: #ddd; 10 | margin: 0.5rem; 11 | height: 3rem; 12 | width: 3rem; 13 | } 14 | 15 | :host input:checked+label { 16 | background: #000; 17 | } 18 | ` 19 | 20 | module.exports = class Checkbox extends Nanocomponent { 21 | constructor () { 22 | super() 23 | this.state = { 24 | id: '', 25 | text: '', 26 | true: '', 27 | false: '', 28 | value: false 29 | } 30 | 31 | this.onChange = this.onChange.bind(this) 32 | } 33 | 34 | createElement (props, emit) { 35 | this.state = xtend(this.state, props.field) 36 | this.state.value = props.field.value || false 37 | this.oninput = props.oninput 38 | 39 | return html` 40 |
    41 |
    42 |
    43 | ${this.getText()} 44 |
    45 | 52 | 53 |
    54 |
    55 | ` 56 | } 57 | 58 | getText () { 59 | if (!this.state.true || !this.state.false) return this.state.text 60 | return this.state.value === true ? this.state.true : this.state.false 61 | } 62 | 63 | onChange (event) { 64 | var value = !this.state.value 65 | this.oninput({ value: value }) 66 | } 67 | 68 | update (props) { 69 | return true 70 | } 71 | } -------------------------------------------------------------------------------- /source/fields/color.js: -------------------------------------------------------------------------------- 1 | var Nanocomponent = require('nanocomponent') 2 | var ColorPicker = require('simple-color-picker') 3 | var html = require('choo/html') 4 | var css = require('sheetify') 5 | var xtend = require('xtend') 6 | 7 | var style = css` 8 | :host label { 9 | border-radius: 1.75rem; 10 | box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1); 11 | position: absolute; 12 | top: 0; 13 | right: 0; 14 | margin: 0.5rem; 15 | height: 3rem; 16 | width: 6rem; 17 | } 18 | ` 19 | 20 | module.exports = class Color extends Nanocomponent { 21 | constructor () { 22 | super() 23 | this.state = { 24 | id: '', 25 | value: '#ffffff' 26 | } 27 | 28 | this.onInput = this.onInput.bind(this) 29 | this.onFocus = this.onFocus.bind(this) 30 | this.onBlur = this.onBlur.bind(this) 31 | } 32 | 33 | load (element) { 34 | var self = this 35 | 36 | // skip if we have a color picker 37 | if (this.colorPicker) return 38 | 39 | this.colorPicker = new ColorPicker({ 40 | color: this.state.value.toLowerCase(), 41 | el: element, 42 | width: 200, 43 | height: 200 44 | }) 45 | 46 | this.colorPicker.onChange(function (data) { 47 | self.onInput({ target: { value: data.toLowerCase() }}) 48 | }) 49 | 50 | this.colorPicker.$el.style.display = 'none' 51 | } 52 | 53 | unload (element) { 54 | 55 | } 56 | 57 | createElement (props, emit) { 58 | this.state = xtend(this.state, props.field) 59 | this.state.value = this.state.value || '#ffffff' 60 | this.oninput = props.oninput 61 | 62 | return html` 63 |
    64 | 73 | 77 | ${this.colorPicker ? this.colorPicker.$el : ''} 78 |
    79 | ` 80 | } 81 | 82 | onInput (event) { 83 | var value = event.target.value.toLowerCase() 84 | if (this.state.value !== value) { 85 | this.oninput({ value: value }) 86 | } 87 | } 88 | 89 | onFocus (event) { 90 | this.colorPicker.$el.style.display = 'flex' 91 | window.addEventListener('click', this.onBlur, false) 92 | } 93 | 94 | onBlur (event) { 95 | if (!this.element) return 96 | var isTargetChild = this.element.contains(event.target) 97 | if (!isTargetChild) { 98 | this.colorPicker.$el.style.display = 'none' 99 | } 100 | } 101 | 102 | update (props, emit) { 103 | var shouldUpdate = this.state.value !== props.field.value 104 | var value = props.field.value || '#ffffff' 105 | 106 | if (this.colorPicker) { 107 | this.colorPicker.setColor(value.toLowerCase()) 108 | } 109 | 110 | return shouldUpdate 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /source/fields/date.js: -------------------------------------------------------------------------------- 1 | var Nanocomponent = require('nanocomponent') 2 | var html = require('choo/html') 3 | var xtend = require('xtend') 4 | 5 | module.exports = class Text extends Nanocomponent { 6 | constructor () { 7 | super() 8 | this.state = { 9 | value: '' 10 | } 11 | } 12 | 13 | load (element) { 14 | // override 15 | if (this.state.override === true) { 16 | this.oninput({ value: getNow() }) 17 | return 18 | } 19 | 20 | // default 21 | if (!this.state.value && this.state.default === 'today') { 22 | this.oninput({ value: getNow() }) 23 | return 24 | } 25 | } 26 | 27 | createElement (props, emit) { 28 | this.state = xtend(this.state, props.field) 29 | this.state.value = this.state.value 30 | this.oninput = props.oninput 31 | 32 | return html` 33 |
    34 | 42 |
    43 | ` 44 | 45 | function onInput (event) { 46 | props.oninput({ value: event.target.value }) 47 | } 48 | } 49 | 50 | update (props) { 51 | return props.field.value !== this.state.value 52 | } 53 | } 54 | 55 | function getNow () { 56 | var date = new Date() 57 | date.setMinutes(date.getMinutes() - date.getTimezoneOffset()) 58 | return date.toISOString().substring(0, 10) 59 | } 60 | -------------------------------------------------------------------------------- /source/fields/dropdown.js: -------------------------------------------------------------------------------- 1 | var Nanocomponent = require('nanocomponent') 2 | var objectValues = require('object-values') 3 | var objectKeys = require('object-keys') 4 | var html = require('choo/html') 5 | var xtend = require('xtend') 6 | var path = require('path') 7 | 8 | module.exports = class Dropdown extends Nanocomponent { 9 | constructor () { 10 | super() 11 | this.state = { 12 | options: { }, 13 | value: '' 14 | } 15 | } 16 | 17 | load (element) { 18 | if (!this.state.value && this.state.default) { 19 | this.oninput({ value: this.state.default }) 20 | } 21 | } 22 | 23 | createElement (props, emit) { 24 | var self = this 25 | this.state = xtend(this.state, props.field) 26 | this.oninput = props.oninput 27 | 28 | return html` 29 |
    30 |
    31 | 37 |
    38 |
    39 | ` 40 | 41 | function options () { 42 | return objectKeys(self.state.options).map(function (option) { 43 | return html` 44 | 50 | ` 51 | }) 52 | } 53 | 54 | function onInput (event) { 55 | props.oninput({ value: event.target.value }) 56 | } 57 | } 58 | 59 | update (props) { 60 | var shouldUpdate = false 61 | 62 | // new value 63 | if (props.field.value !== this.state.value) { 64 | shouldUpdate = true 65 | } 66 | 67 | // new options 68 | if ( 69 | objectKeys(props.field.options).length !== 70 | objectKeys(this.state.options).length 71 | ) { 72 | shouldUpdate = true 73 | } 74 | 75 | // if (props.field.value && props.field.value !== this.state.value) { 76 | // this.state.value = props.value 77 | // } 78 | 79 | return shouldUpdate 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /source/fields/files.js: -------------------------------------------------------------------------------- 1 | var Nanocomponent = require('nanocomponent') 2 | var objectValues = require('object-values') 3 | var queryString = require('query-string') 4 | var html = require('choo/html') 5 | var xtend = require('xtend') 6 | 7 | var Uploader = require('../components/uploader') 8 | var methodsFile = require('../lib/file') 9 | 10 | module.exports = class Files extends Nanocomponent { 11 | constructor () { 12 | super() 13 | this.label = false 14 | this.state = { 15 | limit: 6, 16 | value: '' 17 | } 18 | 19 | this.handleFilesAdd = this.handleFilesAdd.bind(this) 20 | } 21 | 22 | load () { 23 | this.uploader = new Uploader() 24 | this.rerender() 25 | } 26 | 27 | createElement (props, emit) { 28 | var self = this 29 | this.state = xtend(this.state, props.field) 30 | this.state.value = this.state.value || '' 31 | 32 | var urlFileNew = unescape(queryString.stringify(xtend({ file: 'new' }, props.query))) 33 | var urlFilesAll = unescape(queryString.stringify(xtend({ files: 'all' }, props.query))) 34 | var pageFiles = objectValues(props.page.files || { }).map(function (pageFile) { 35 | var data = xtend(pageFile, { }) 36 | data.urlPanel = queryString.stringify(xtend({ 37 | file: methodsFile.encodeFilename(pageFile.filename) 38 | }, props.query)) 39 | data.urlPanel = unescape(data.urlPanel) 40 | return data 41 | }) 42 | 43 | return html` 44 | 62 | 63 | ` 64 | 65 | function elUploadContainer () { 66 | if (!self.uploader) return 67 | return html` 68 |
    72 | ${self.uploader.render({ 73 | text: 'Drag and drop here to add file', 74 | handleFiles: handleFilesUpload, 75 | handleDragEnter: function (event) { 76 | var el = event.target.parentNode.parentNode.parentNode 77 | el.classList.remove('bgc-bg', 'fc-fg') 78 | el.classList.add('bgc-fg', 'fc-bg') 79 | }, 80 | handleDragLeave: function (event) { 81 | var el = event.target.parentNode.parentNode.parentNode 82 | el.classList.add('bgc-bg', 'fc-fg') 83 | el.classList.remove('bgc-fg', 'fc-bg') 84 | } 85 | }, emit)} 86 |
    87 | ` 88 | } 89 | 90 | function handleFilesUpload (event, data) { 91 | emit(props.events.ENOKI_FILES_ADD, { 92 | path: props.page.path, 93 | url: props.page.url, 94 | files: data.files 95 | }) 96 | } 97 | 98 | function onInput (event) { 99 | emit({ value: event.target.value }) 100 | } 101 | } 102 | 103 | elsFiles (files) { 104 | files = files || [ ] 105 | 106 | // Hide if there is nothing 107 | if (files.length <= 0) return html` 108 |
  • 109 | No files 110 |
  • 111 | ` 112 | 113 | return files 114 | .slice(0, this.state.limit) 115 | .map(function (child) { 116 | if (!child.url) return 117 | return html` 118 |
  • 119 | ${child.filename} 124 |
  • 125 | ` 126 | 127 | function handleDragStart (event) { 128 | event.dataTransfer.setData('text/plain', '![](' +child.source + ')') 129 | } 130 | }) 131 | } 132 | 133 | update (props) { 134 | return true 135 | } 136 | 137 | handleFilesAdd (event) { 138 | this.uploader.open() 139 | event.preventDefault() 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /source/fields/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | checkbox: require('./checkbox'), 3 | color: require('./color'), 4 | date: require('./date'), 5 | dropdown: require('./dropdown'), 6 | files: require('./files'), 7 | pages: require('./pages'), 8 | range: require('./range'), 9 | tags: require('./tags'), 10 | text: require('./text'), 11 | textarea: require('./textarea') 12 | } 13 | -------------------------------------------------------------------------------- /source/fields/pages.js: -------------------------------------------------------------------------------- 1 | var Nanocomponent = require('nanocomponent') 2 | var objectValues = require('object-values') 3 | var queryString = require('query-string') 4 | var html = require('choo/html') 5 | var xtend = require('xtend') 6 | 7 | module.exports = class Pages extends Nanocomponent { 8 | constructor () { 9 | super() 10 | this.label = false 11 | this.state = { 12 | pathnames: false, 13 | label: 'Pages', 14 | limit: 6, 15 | value: '' 16 | } 17 | } 18 | 19 | createElement (props, emit) { 20 | var self = this 21 | this.state = xtend(this.state, props.field) 22 | this.state.value = this.state.value || '' 23 | this.state.sort = props.field.sort || props.page.sort 24 | 25 | var urlPageNew = unescape(queryString.stringify(xtend({ page: 'new' }, props.query))) 26 | var urlPagesAll = unescape(queryString.stringify(xtend({ pages: 'all' }, props.query))) 27 | 28 | var pages = objectValues(props.page.pages || { }) 29 | .map(function (page) { 30 | return props.content[page.url] 31 | }) 32 | 33 | // custom sort 34 | if (typeof this.state.sort === 'string') { 35 | pages = getPagesSort(pages, this.state.sort) 36 | } 37 | 38 | return html` 39 | 53 | ` 54 | } 55 | 56 | update (props) { 57 | return true 58 | } 59 | 60 | elsChildren (children) { 61 | var self = this 62 | children = children || [ ] 63 | 64 | if (children.length <= 0) { 65 | return html` 66 |
  • 67 | No sub-pages 68 |
  • 69 | ` 70 | } 71 | 72 | return children 73 | .slice(0, this.state.limit) 74 | .map(function (child) { 75 | if (!child.url) return 76 | return html` 77 |
  • 78 | 82 | ${child.title || child.name} 83 | ${renderMeta()} 84 | 85 | ${self.state.delete ? renderDelete() : ''} 86 |
  • 87 | ` 88 | 89 | function renderMeta () { 90 | return html` 91 |
    92 | ${self.state.pathnames ? renderName() : ''} 93 |
    94 | ` 95 | } 96 | 97 | function renderName () { 98 | return html`/${child.name}` 99 | } 100 | 101 | function renderDelete () { 102 | return html` 103 |
    104 | D 105 |
    106 | ` 107 | } 108 | }) 109 | } 110 | } 111 | 112 | function getPagesSort (pages, sort) { 113 | switch (sort) { 114 | case 'alphabetical': 115 | return pages.sort(function (a, b) { 116 | return (a.title || a.name).localeCompare(b.title || b.name) 117 | }) 118 | case 'reverse-alphabetical': 119 | return pages.sort(function (a, b) { 120 | return (b.title || b.name).localeCompare(a.title || a.name) 121 | }) 122 | case 'reverse-chronological': 123 | return pages.sort(function (a, b) { 124 | if (a.date && b.date) return new Date(b.date) - new Date(a.date) 125 | }) 126 | case 'chronological': 127 | return pages.sort(function (a, b) { 128 | if (a.date && b.date) return new Date(a.date) - new Date(b.date) 129 | }) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /source/fields/range.js: -------------------------------------------------------------------------------- 1 | var Nanocomponent = require('nanocomponent') 2 | var html = require('choo/html') 3 | var xtend = require('xtend') 4 | var css = require('sheetify') 5 | 6 | var style = css` 7 | :host .value { 8 | background: linear-gradient(to right, #ddd calc(var(--value)*1%), #fff 0%) 9 | } 10 | 11 | :host input[type="range"] { 12 | cursor: ew-resize; 13 | } 14 | 15 | :host input[type=number]::-webkit-inner-spin-button, 16 | :host input[type=number]::-webkit-outer-spin-button { 17 | -webkit-appearance: none; 18 | margin: 0; /* Removes leftover margin */ 19 | } 20 | 21 | :host input[type="number"] { 22 | -moz-appearance: textfield; 23 | } 24 | ` 25 | 26 | module.exports = class Range extends Nanocomponent { 27 | constructor () { 28 | super() 29 | this.state = { 30 | min: 0, 31 | max: 100, 32 | value: 0, 33 | focused: false 34 | } 35 | 36 | this.onInput = this.onInput.bind(this) 37 | this.onFocus = this.onFocus.bind(this) 38 | this.onBlur = this.onBlur.bind(this) 39 | } 40 | 41 | createElement (props, emit) { 42 | this.state = xtend(this.state, props.field) 43 | this.oninput = props.oninput 44 | this.emit = emit 45 | 46 | if (this.state.focused) { 47 | this.state.value = props.field.value 48 | } else { 49 | this.state.value = props.field.value || 0 50 | } 51 | 52 | return html` 53 |
    54 |
    55 |
    56 |
    57 | ${this.state.text} 58 |
    59 | 68 |
    72 |
    73 | 84 |
    85 |
    86 | ` 87 | 88 | } 89 | 90 | onInput (event) { 91 | var value = event.target.value 92 | if (isNaN(value)) value = min 93 | if (value > this.state.max) value = this.state.max 94 | if (value < this.state.min) value = this.state.min 95 | this.oninput({ value: event.target.value }) 96 | } 97 | 98 | onFocus (event) { 99 | this.state.focused = true 100 | } 101 | 102 | onBlur (event) { 103 | this.state.focused = true 104 | } 105 | 106 | update (props) { 107 | return true 108 | } 109 | } -------------------------------------------------------------------------------- /source/fields/tags.js: -------------------------------------------------------------------------------- 1 | var Nanocomponent = require('nanocomponent') 2 | var tagsInput = require('tags-input') 3 | var html = require('choo/html') 4 | var css = require('sheetify') 5 | var xtend = require('xtend') 6 | 7 | var style = css` 8 | :host { 9 | display: block; 10 | padding: 0.2rem 0.2rem; 11 | background: #fff; 12 | border: 1px solid #ddd; 13 | width: 100%; 14 | border-radius: 2rem; 15 | min-height: 4rem; 16 | cursor: text; 17 | } 18 | 19 | :host .tag { 20 | display: inline-block; 21 | background: #eee; 22 | color: #000; 23 | height: 3rem; 24 | line-height: 3rem; 25 | padding: 0.5rem 1rem; 26 | margin: 0.2rem 0.2rem; 27 | border-radius: 2rem; 28 | font: inherit; 29 | -webkit-user-select: none; 30 | -moz-user-select: none; 31 | -ms-user-select: none; 32 | user-select: none; 33 | cursor: pointer; 34 | vertical-align: middle; 35 | } 36 | 37 | :host .tag.selected { 38 | background-color: #000; 39 | border-color: #000; 40 | color: #fff 41 | } 42 | 43 | :host .tag.dupe { 44 | -webkit-transform: scale3d(1.2, 1.2, 1.2); 45 | transform: scale3d(1.2, 1.2, 1.2); 46 | background-color: #FCC; 47 | border-color: #700 48 | } 49 | 50 | :host input { 51 | -webkit-appearance: none!important; 52 | -moz-appearance: none!important; 53 | appearance: none!important; 54 | display: inline-block!important; 55 | padding: 0.5rem 1rem !important; 56 | margin: 0.2rem 0.2rem !important; 57 | background: none!important; 58 | border: none!important; 59 | height: 2.8rem !important; 60 | box-shadow: none!important; 61 | line-height: 2.8rem!important; 62 | font: inherit!important; 63 | font-size: 1.8rem!important; 64 | outline: 0!important; 65 | vertical-align: middle; 66 | } 67 | 68 | :host .selected~input { 69 | opacity: .3; 70 | } 71 | ` 72 | 73 | module.exports = class Tags extends Nanocomponent { 74 | constructor () { 75 | super() 76 | this.state = { 77 | value: '', 78 | valueStart: '' 79 | } 80 | } 81 | 82 | createElement (props, emit) { 83 | var self = this 84 | this.state = xtend(this.state, props.field) 85 | this.state.value = this.state.value || '' 86 | this.oninput = props.oninput 87 | if (!this.state.valueStart) this.state.valueStart = this.state.value 88 | 89 | return html` 90 |
    91 | 98 |
    99 | ` 100 | 101 | function onChange (event) { 102 | var value = event.target.value.split(',') 103 | if (!arraysEqual(self.state.value, value)) { 104 | props.oninput({ value: value }) 105 | } 106 | } 107 | } 108 | 109 | update (props) { 110 | var value = props.field.value || '' 111 | if (value !== this.state.value) { 112 | var el = this.element.querySelector('.tags-input') 113 | this.state.value = value 114 | this.element.querySelector('input').value = value 115 | 116 | // reset 117 | if (this.state.value === this.state.valueStart) { 118 | this.element.removeChild(el) 119 | tagsInput(this.element.querySelector('input')) 120 | } 121 | } 122 | 123 | return false 124 | } 125 | 126 | load (props) { 127 | tagsInput(this.element.querySelector('input')) 128 | } 129 | } 130 | 131 | function arraysEqual (a, b) { 132 | if (a === b) return true 133 | if (a == null || b == null) return false 134 | if (a.length != b.length) return false 135 | 136 | for (var i = 0; i < a.length; ++i) { 137 | if (a[i] !== b[i]) return false 138 | } 139 | 140 | return true 141 | } 142 | -------------------------------------------------------------------------------- /source/fields/text.js: -------------------------------------------------------------------------------- 1 | var Nanocomponent = require('nanocomponent') 2 | var html = require('choo/html') 3 | var xtend = require('xtend') 4 | 5 | module.exports = class Text extends Nanocomponent { 6 | constructor () { 7 | super() 8 | this.state = { 9 | value: '' 10 | } 11 | } 12 | 13 | createElement (props, emit) { 14 | this.state = xtend(this.state, props.field) 15 | this.state.value = this.state.value || '' 16 | this.oninput = props.oninput 17 | 18 | return html` 19 |
    20 | 28 |
    29 | ` 30 | 31 | function onInput (event) { 32 | props.oninput({ value: event.target.value }) 33 | } 34 | } 35 | 36 | update (props) { 37 | return true 38 | } 39 | } -------------------------------------------------------------------------------- /source/fields/textarea.js: -------------------------------------------------------------------------------- 1 | var Nanocomponent = require('nanocomponent') 2 | var SimpleMDE = require('simplemde') 3 | var html = require('choo/html') 4 | var xtend = require('xtend') 5 | 6 | var toolbarDefaults = { 7 | condensed: [ 8 | 'bold', 'italic', 'heading', '|', 9 | 'quote', 'unordered-list','|', 10 | 'link', 'image' 11 | ], 12 | full: [ 13 | 'bold', 'italic', 'heading', '|', 14 | 'quote', 'unordered-list', 'ordered-list', '|', 15 | 'link', 'image', '|', 16 | 'preview' 17 | ] 18 | } 19 | 20 | module.exports = class Textarea extends Nanocomponent { 21 | constructor () { 22 | super() 23 | this.state = { 24 | id: '', 25 | key: '', 26 | value: '', 27 | valueStart: undefined, 28 | toolbar: '' 29 | } 30 | 31 | this.label = false 32 | this.toolbar = { } 33 | } 34 | 35 | createElement (props, emit) { 36 | this.state = xtend(this.state, props.field) 37 | this.state.value = this.state.value || '' 38 | this.toolbar = getToolbar(this.state.toolbar) 39 | this.oninput = props.oninput 40 | this.emit = emit 41 | 42 | return html` 43 |
    44 |
    45 |
    46 |
    47 |
    48 | ${this.state.label || this.state.key} 49 |
    50 |
    51 |
    52 |
    53 |
    54 | 58 |
    59 |
    60 | ` 61 | 62 | function onInput (event) { 63 | props.oninput({ value: event.target.value }) 64 | } 65 | } 66 | 67 | update (props) { 68 | var value = props.field.value || '' 69 | 70 | // reset 71 | if (this.state.valueStart === undefined) { 72 | this.state.valueStart = this.state.value 73 | } 74 | 75 | // update 76 | if (value !== this.state.value) { 77 | this.state.value = value 78 | this.element.querySelector('textarea').value = this.state.value 79 | } 80 | 81 | // cancel 82 | if (value === this.state.valueStart) { 83 | this.simplemde.value(value) 84 | } 85 | 86 | return false 87 | } 88 | 89 | load (element) { 90 | var self = this 91 | 92 | this.simplemde = new SimpleMDE({ 93 | autoDownloadFontAwesome: false, 94 | element: element.querySelector('textarea'), 95 | forceSync: true, 96 | spellChecker: false, 97 | status: false, 98 | toolbar: self.toolbar 99 | }) 100 | 101 | // set default vlue 102 | this.simplemde.value(this.state.value) 103 | var elToolbar = element.querySelector('.editor-toolbar') 104 | if (elToolbar) { 105 | element.querySelector('[data-editor-toolbar]').appendChild(elToolbar) 106 | } 107 | 108 | // send state up 109 | this.simplemde.codemirror.on('change', function () { 110 | var value = self.simplemde.value() || '' 111 | if (self.state.value !== value) { 112 | self.oninput({ value: value }) 113 | } 114 | }, false) 115 | } 116 | 117 | unload () { 118 | // this.simplemde.toTextArea() 119 | // this.simplemde = null 120 | } 121 | } 122 | 123 | function getToolbar (option) { 124 | var preset = toolbarDefaults[option] 125 | 126 | if (option === false) return false 127 | if (preset) return preset 128 | 129 | if (typeof option === 'object') { 130 | return option.map(function (opt) { 131 | if (opt === '') return '|' 132 | return opt 133 | }) 134 | } 135 | 136 | return toolbarDefaults.full 137 | } -------------------------------------------------------------------------------- /source/index.js: -------------------------------------------------------------------------------- 1 | var wrapper = require('./containers/wrapper-site') 2 | var choo = require('choo') 3 | require('./design') 4 | 5 | // create app 6 | var app = choo() 7 | 8 | // external 9 | app.use(require('enoki/choo-panel')()) 10 | 11 | // plugins 12 | app.use(require('./plugins/interface')) 13 | app.use(require('./plugins/designs')) 14 | app.use(require('./plugins/scroll')) 15 | app.use(require('./plugins/sites')) 16 | app.use(require('./plugins/docs')) 17 | app.use(require('./plugins/hub')) 18 | 19 | // routes 20 | app.route('*', wrapper(require('./views/default'))) 21 | app.route('#hub', wrapper(require('./views/network'))) 22 | app.route('#hub/:page', wrapper(require('./views/hub'))) 23 | app.route('#hub/guides', wrapper(require('./views/guides'))) 24 | app.route('#hub/guides/:page', wrapper(require('./views/guide'))) 25 | app.route('#hub/log', wrapper(require('./views/log'))) 26 | 27 | // init 28 | if (!module.parent) app.mount('body') 29 | else module.exports = app 30 | -------------------------------------------------------------------------------- /source/lib/file.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | getBlueprint, 3 | encodeFilename, 4 | decodeFilename, 5 | sanitizeName 6 | } 7 | 8 | function getBlueprint (state) { 9 | try { 10 | return state.site.blueprints[state.page.view].files || { } 11 | } catch (err) { 12 | return { } 13 | } 14 | } 15 | 16 | function encodeFilename (str) { 17 | return str.replace(/\.([^\.]*)$/, '-$1') 18 | } 19 | 20 | function decodeFilename (str) { 21 | return str.replace(/-([^\-]*)$/, '.$1') 22 | } 23 | 24 | function sanitizeName (str) { 25 | return str 26 | .replace(/\s+/g, '-') 27 | .replace(/[.,\/#!@?$%\^&\*;:{}=\_`~()]/g, '') 28 | .toLowerCase() 29 | } -------------------------------------------------------------------------------- /source/lib/page.js: -------------------------------------------------------------------------------- 1 | var objectKeys = require('object-keys') 2 | var Page = require('enoki/page') 3 | 4 | var blueprintDefault = require('../blueprints/default') 5 | 6 | module.exports = { 7 | getChanges: getChanges, 8 | getBlueprint: getBlueprint, 9 | getViews: getViews 10 | } 11 | 12 | function getChanges (state, emit, page) { 13 | try { 14 | page = page || Page(state) 15 | return state.enoki.changes[page.value('url')] 16 | } catch (err) { 17 | return { } 18 | } 19 | } 20 | 21 | function getBlueprint (state, emit, page) { 22 | try { 23 | page = page || Page(state) 24 | return ( 25 | state.site.blueprints[page().value('view')] || 26 | state.site.blueprints.default || 27 | blueprintDefault 28 | ) 29 | } catch (err) { 30 | return blueprintDefault 31 | } 32 | } 33 | 34 | function getViews (props) { 35 | props = props || { } 36 | if (!props.blueprint) return console.warn('must define blueprint') 37 | if (!props.blueprints) return console.warn('must define all blueprints') 38 | 39 | var blueprint = props.blueprint 40 | var blueprints = props.blueprints 41 | 42 | if (blueprint.pages && typeof blueprint.pages === 'object') { 43 | // if pages are disabled 44 | if (blueprint.pages.view === false) return false 45 | 46 | // presets 47 | if (typeof blueprint.pages.view === 'object') { 48 | return blueprint.pages.view.reduce(function (result, key) { 49 | result[key] = blueprints[key] 50 | return result 51 | }, { }) 52 | } else { 53 | // if just a string 54 | return { 55 | [blueprint.pages.view]: blueprints[blueprint.pages.view] 56 | } 57 | } 58 | } else { 59 | return objectKeys(blueprints).reduce(function (result, key) { 60 | result[key] = blueprints[key] 61 | return result 62 | }, { }) 63 | } 64 | 65 | return { } 66 | } -------------------------------------------------------------------------------- /source/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "enoki-panel", 3 | "version": "1.0.0", 4 | "description": "A little javascript static site generator", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "watchify index.js -o ../bundles/0.1.0/bundle.js -t brfs -t sheetify -p [ css-extract -o ../bundles/0.1.0/bundle.css ] index.js", 8 | "build": "browserify index.js -o ../bundles/0.1.0/bundle.js -t brfs -t yo-yoify -t [ sheetify -u sheetify-cssnext ] -g es2040 -p [ css-extract -o ../bundles/0.1.0/bundle.css ]", 9 | "publish": "dat sync ../ --no-ignoreHidden" 10 | }, 11 | "keywords": [], 12 | "author": "Jon-Kyle (http://jon-kyle.com)", 13 | "license": "MIT", 14 | "dependencies": { 15 | "choo": "^6.6.1", 16 | "choo-tts": "^1.0.0", 17 | "enoki": "^2.0.5", 18 | "gr8": "^3.1.4", 19 | "markdown-it": "^8.4.0", 20 | "markdown-it-task-lists": "^2.1.0", 21 | "nanocomponent": "^6.5.0", 22 | "nanoreset": "^1.2.0", 23 | "object-keys": "^1.0.11", 24 | "object-values": "^1.0.0", 25 | "query-string": "^5.0.0", 26 | "simple-color-picker": "^0.1.2", 27 | "simplemde": "^1.11.2", 28 | "tags-input": "^1.1.1", 29 | "xhr": "^2.4.0", 30 | "xtend": "^4.0.1", 31 | "yo-yoify": "^3.7.3" 32 | }, 33 | "devDependencies": { 34 | "brfs": "^1.4.3", 35 | "browserify": "^15.2.0", 36 | "css-extract": "^1.2.0", 37 | "es2040": "^1.2.6", 38 | "sheetify": "^7.0.0", 39 | "sheetify-cssnext": "^1.0.7", 40 | "tinyify": "^2.4.0", 41 | "watchify": "^3.9.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /source/plugins/designs.js: -------------------------------------------------------------------------------- 1 | module.exports = designs 2 | 3 | function designs (state, emitter) { 4 | state.designs = { 5 | loaded: false, 6 | public: { 7 | jacinto: { 8 | title: 'Jacinto', 9 | thumbnail: '/assets/designs/jacinto.png', 10 | url: 'dat://6bd019f189b1b674ddf238f81741b8c86520addfaf368a87058eeb207cd477c5' 11 | }, 12 | vacant: { 13 | title: 'Vacant', 14 | thumbnail: '/assets/designs/vacant.png', 15 | url: 'dat://68a206a64a3f30dda8720625592b8b07d02e2ad3a6116ce3b6b05230e7bb1566' 16 | }, 17 | starterkit: { 18 | title: 'Starter Kit', 19 | thumbnail: '/assets/designs/starter-kit.png', 20 | url: 'dat://57cb1b649045ab34d762e25a16fc08dbe8ea2006d4373e10719899d2ae7c6ff5' 21 | } 22 | } 23 | } 24 | 25 | state.events.DESIGNS_SELECT = 'designs:select' 26 | 27 | emitter.on(state.events.DESIGNS_SELECT, onSelect) 28 | 29 | function onSelect (data) { 30 | console.log(data) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /source/plugins/docs.js: -------------------------------------------------------------------------------- 1 | var Enoki = require('enoki') 2 | var enoki = new Enoki() 3 | 4 | module.exports = docs 5 | 6 | function docs (state, emitter, app) { 7 | state.docs = { 8 | loaded: false, 9 | content: { }, 10 | site: { } 11 | } 12 | 13 | state.events.DOCS_LOAD = 'docs:load' 14 | 15 | emitter.on(state.events.DOCS_LOAD, onLoad) 16 | 17 | async function onLoad () { 18 | await enoki.load() 19 | state.docs.content = await enoki.readContent() 20 | state.docs.loaded = true 21 | emitter.emit(state.events.RENDER) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /source/plugins/hub.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | 3 | module.exports = hub 4 | 5 | function hub (state, emitter) { 6 | state.hub = { 7 | active: 'sites' 8 | } 9 | 10 | state.events.HUB_ACTIVE = 'state:hub' 11 | 12 | emitter.on(state.events.HUB_ACTIVE, handleHubActive) 13 | 14 | function handleHubActive (data) { 15 | assert(typeof data ==='object', 'arg1 must be type object') 16 | state.hub.active = data.active 17 | if (data.render === true) emitter.emit(state.events.RENDER) 18 | } 19 | } -------------------------------------------------------------------------------- /source/plugins/interface.js: -------------------------------------------------------------------------------- 1 | var draggableCount = 0 2 | var renderTimeout 3 | var dragTimeout 4 | 5 | var _package = require('../package.json') 6 | 7 | module.exports = ui 8 | 9 | async function ui (state, emitter) { 10 | state.ui = { 11 | history: getHistoryDefaults(), 12 | version: _package.version, 13 | dragActive: false 14 | } 15 | 16 | state.events.UI_HISTORY = 'ui:history' 17 | state.events.UI_HISTORY_RESET = 'ui:history:reset' 18 | 19 | emitter.on(state.events.UI_HISTORY_RESET, handleHistoryReset) 20 | emitter.on(state.events.UI_HISTORY, handleHistory) 21 | // emitter.on(state.events.DOMCONTENTLOADED, handleLoad) 22 | 23 | function handleHistory (data) { 24 | if (!data.route || !data.path) return 25 | state.ui.history[data.route] = data.path 26 | } 27 | 28 | function handleHistoryReset () { 29 | state.history = getHistoryDefaults() 30 | } 31 | 32 | function handleLoad (data) { 33 | document.body.addEventListener('dragenter', handleDrag, false) 34 | window.addEventListener('dragend', handleDragEnd, false) 35 | window.addEventListener('drag', handleDrag, false) 36 | window.addEventListener('drop', handleDragEnd, false) 37 | } 38 | 39 | function handleDrag () { 40 | if (!state.ui.dragActive) { 41 | clearTimeout(dragTimeout) 42 | dragTimeout = setTimeout(handleDragEnd, 500) 43 | state.ui.dragActive = true 44 | emitter.emit(state.events.RENDER) 45 | } 46 | } 47 | 48 | function handleDragEnd (event) { 49 | if (event) event.preventDefault() 50 | state.ui.dragActive = false 51 | clearTimeout(dragTimeout) 52 | renderTimeout = setTimeout(function () { 53 | if (!state.ui.dragActive) emitter.emit(state.events.RENDER) 54 | }, 100) 55 | } 56 | } 57 | 58 | function getHistoryDefaults () { 59 | return { 60 | hub: 'guides', 61 | sites: 'all', 62 | editor: '/' 63 | } 64 | } -------------------------------------------------------------------------------- /source/plugins/panel.js: -------------------------------------------------------------------------------- 1 | var queryString = require('query-string') 2 | var objectKeys = require('object-keys') 3 | var html = require('choo/html') 4 | var assert = require('assert') 5 | var smarkt = require('smarkt') 6 | var xtend = require('xtend') 7 | var path = require('path') 8 | var xhr = require('xhr') 9 | 10 | var reservedKeys = ['files', 'pages', 'url', 'name', 'path'] 11 | 12 | module.exports = panel 13 | 14 | function panel () { 15 | return async function (state, emitter) { 16 | var archive 17 | 18 | state.content = { } 19 | 20 | state.site = { 21 | loaded: false, 22 | blueprints: { }, 23 | config: { }, 24 | info: { } 25 | } 26 | 27 | state.enoki = { 28 | changes: { }, 29 | loading: false 30 | } 31 | 32 | state.events.ENOKI_FILES_ADD = 'enoki:files:add' 33 | state.events.ENOKI_SITE_LOAD = 'enoki:site:load' 34 | state.events.ENOKI_PAGE_ADD = 'enoki:page:add' 35 | state.events.ENOKI_LOADING = 'enoki:loading' 36 | state.events.ENOKI_UPDATED = 'enoki:updated' 37 | state.events.ENOKI_CANCEL = 'enoki:cancel' 38 | state.events.ENOKI_UPDATE = 'enoki:update' 39 | state.events.ENOKI_REMOVE = 'enoki:remove' 40 | state.events.ENOKI_MOVE = 'enoki:move' 41 | state.events.ENOKI_SAVE = 'enoki:save' 42 | 43 | emitter.on(state.events.ENOKI_FILES_ADD, handleFilesAdd) 44 | emitter.on(state.events.ENOKI_SITE_LOAD, handleSiteLoad) 45 | emitter.on(state.events.ENOKI_PAGE_ADD, handlePageAdd) 46 | emitter.on(state.events.ENOKI_LOADING, handleLoading) 47 | emitter.on(state.events.ENOKI_UPDATE, handleUpdate) 48 | emitter.on(state.events.ENOKI_CANCEL, handleCancel) 49 | emitter.on(state.events.ENOKI_REMOVE, handleRemove) 50 | emitter.on(state.events.ENOKI_SAVE, handleSave) 51 | 52 | function handleUpdate (data) { 53 | assert.equal(typeof data, 'object', 'enoki: data must be type object') 54 | assert.equal(typeof data.url, 'string', 'enoki: data.url must be type string') 55 | assert.equal(typeof data.data, 'object', 'enoki: data.data must be type string') 56 | 57 | // get the changes and merge them 58 | var changes = state.enoki.changes[data.url] 59 | state.enoki.changes[data.url] = xtend(changes, data.data) 60 | 61 | // trigger updated and render 62 | emitter.emit(state.events.ENOKI_UPDATED) 63 | if (data.render !== false) emitter.emit(state.events.RENDER) 64 | } 65 | 66 | async function handleSave (data) { 67 | assert.equal(typeof data, 'object', 'enoki: arg1 must be type object') 68 | assert.equal(typeof data.path, 'string', 'enoki: arg1.path must be type string') 69 | assert.equal(typeof data.url, 'string', 'enoki: arg1.url must be type string') 70 | assert.equal(typeof data.data, 'object', 'enoki: arg1.data must be type object') 71 | 72 | emitter.emit(state.events.ENOKI_LOADING, { loading: true }) 73 | emitter.emit(state.events.RENDER) 74 | 75 | try { 76 | var contentPage = state.content[data.url] 77 | var content = xtend(state.content[data.url], data.data) 78 | var file = data.file || state.site.config.file || 'index.txt' 79 | var pathFile = path.join(data.path, file) 80 | 81 | // delete reserved keys 82 | reservedKeys.forEach(key => delete content[key]) 83 | 84 | // create the file and save 85 | await archive.writeFile(pathFile, smarkt.stringify(content)) 86 | await archive.commit() 87 | 88 | // add to state and remove from changes 89 | state.content[data.url] = xtend(state.content[data.url], state.enoki.changes[data.url]) 90 | delete state.enoki.changes[data.url] 91 | 92 | // refresh and create the content json 93 | emitter.once(state.events.SITE_REFRESHED, writeContentJson) 94 | emitter.emit(state.events.SITE_REFRESH) 95 | } catch (err) { 96 | alert(err.message) 97 | console.warn(err) 98 | } 99 | 100 | emitter.emit(state.events.ENOKI_LOADING, { loading: false }) 101 | emitter.emit(state.events.RENDER) 102 | } 103 | 104 | function handleCancel (data) { 105 | assert.equal(typeof data, 'object', 'enoki: data must be type object') 106 | assert.equal(typeof data.url, 'string', 'enoki: data.url must be type string') 107 | 108 | // discard our changes 109 | delete state.enoki.changes[data.url] 110 | emitter.emit(state.events.RENDER) 111 | } 112 | 113 | function handleLoading (data) { 114 | if (data && data.loading !== undefined) { 115 | state.enoki.loading = data.loading 116 | } else { 117 | state.enoki.loading = false 118 | } 119 | 120 | if (data.render !== false) emitter.emit(state.events.RENDER) 121 | } 122 | 123 | async function handlePageAdd (data) { 124 | assert.equal(typeof data, 'object', 'enoki: data must be type object') 125 | assert.equal(typeof data.path, 'string', 'enoki: data.path must be type string') 126 | assert.equal(typeof data.url, 'string', 'enoki: data.url must be type string') 127 | assert.equal(typeof data.title, 'string', 'enoki: data.title must be type string') 128 | assert.equal(typeof data.view, 'string', 'enoki: data.view must be type string') 129 | 130 | emitter.emit(state.events.ENOKI_LOADING, { loading: true }) 131 | 132 | try { 133 | var content = { title: data.title, view: data.view } 134 | var file = data.file || state.site.config.file 135 | var pathFile = path.join(data.path, file) 136 | 137 | // create the directory and file 138 | await archive.mkdir(data.path) 139 | await archive.writeFile(pathFile, smarkt.stringify(content)) 140 | emitter.emit(state.events.SITE_REFRESH) 141 | } catch (err) { 142 | alert(err.message) 143 | console.warn(err) 144 | } 145 | 146 | emitter.emit(state.events.ENOKI_LOADING, { loading: false, render: false }) 147 | if (data.redirect !== false) emitter.emit(state.events.REPLACESTATE, '?url=' + data.url) 148 | } 149 | 150 | async function handleRemove (data) { 151 | assert.equal(typeof data, 'object', 'enoki: data must be type object') 152 | assert.equal(typeof data.url, 'string', 'enoki: data.url must be type string') 153 | assert.equal(typeof data.path, 'string', 'enoki: data.path must be type string') 154 | 155 | if (data.confirm) { 156 | return window.confirm(`Are you sure you want to delete ${data.title || data.path}?`) 157 | } 158 | 159 | emitter.emit(state.events.ENOKI_LOADING, { loading: true }) 160 | 161 | try { 162 | var isFile = path.extname(data.path) 163 | if (isFile) { 164 | await archive.unlink(data.path) 165 | try { await archive.unlink(data.path + '.txt') } catch (err) { } 166 | } else { 167 | await archive.rmdir(data.path, { recursive: true }) 168 | } 169 | 170 | emitter.emit(state.events.SITE_REFRESH) 171 | 172 | if (data.redirect !== false) { 173 | emitter.emit( 174 | state.events.REPLACESTATE, 175 | '?url=' + path.join(data.url, '../').replace(/\/$/, '') 176 | ) 177 | } 178 | } catch (err) { 179 | alert(err.message) 180 | console.warn(err) 181 | } 182 | 183 | emitter.emit(state.events.ENOKI_LOADING, { loading: false }) 184 | } 185 | 186 | async function handleFilesAdd (data) { 187 | assert.equal(typeof data, 'object', 'enoki: data must be type object') 188 | assert.equal(typeof data.url, 'string', 'enoki: data.url must be type string') 189 | assert.equal(typeof data.path, 'string', 'enoki: data.path must be type string') 190 | assert.equal(typeof data.files, 'object', 'enoki: data.files must be type object') 191 | 192 | emitter.emit(state.events.ENOKI_LOADING, { loading: true }) 193 | await Promise.all(objectKeys(data.files).map(saveFile)) 194 | emitter.emit(state.events.ENOKI_LOADING, { loading: false }) 195 | 196 | async function saveFile (key) { 197 | try { 198 | var file = data.files[key] 199 | var filePath = path.join(data.path, file.name) 200 | var fileEncoded = await getBase64(file) 201 | var encoder = typeof fileEncoded === 'string' ? 'base64' : 'binary' 202 | return archive.writeFile(filePath, fileEncoded, encoder) 203 | } catch (err) { 204 | alert(err.message) 205 | console.warn(err) 206 | } 207 | } 208 | } 209 | 210 | function handleSiteLoad (data) { 211 | if (data.content) state.content = data.content 212 | if (data.site) state.site = data.site 213 | if (data.archive) archive = data.archive 214 | if (data.render !== false) emitter.emit(state.events.RENDER) 215 | } 216 | 217 | async function writeContentJson () { 218 | try { await archive.readdir('/bundles') } 219 | catch (err) { await archive.mkdir('/bundles') } 220 | 221 | try { 222 | await archive.writeFile( 223 | '/bundles/content.json', 224 | JSON.stringify(state.content, { }, 2) 225 | ) 226 | await archive.commit() 227 | } catch (err) { 228 | console.warn(err) 229 | } 230 | } 231 | } 232 | } 233 | 234 | function getBase64 (file) { 235 | return new Promise(function (resolve, reject) { 236 | var reader = new FileReader() 237 | reader.readAsDataURL(file) 238 | reader.onload = function () { 239 | resolve(reader.result.split(',')[1]) 240 | } 241 | reader.onerror = function (error) { 242 | reject(error) 243 | } 244 | }) 245 | } 246 | -------------------------------------------------------------------------------- /source/plugins/scroll.js: -------------------------------------------------------------------------------- 1 | module.exports = plugin 2 | 3 | function plugin (state, emitter) { 4 | emitter.on(state.events.NAVIGATE, function () { 5 | window.scrollTo(0, 0) 6 | }) 7 | } -------------------------------------------------------------------------------- /source/plugins/sites.js: -------------------------------------------------------------------------------- 1 | var Enoki = require('enoki') 2 | var objectKeys = require('object-keys') 3 | var assert = require('assert') 4 | var xtend = require('xtend') 5 | 6 | module.exports = sites 7 | 8 | function sites (state, emitter, app) { 9 | var enoki = new Enoki() 10 | var storage 11 | 12 | // state 13 | state.sites = { 14 | loaded: false, 15 | p2p: typeof DatArchive !== 'undefined', 16 | archives: { }, 17 | create: getCreateDefaults(), 18 | active: '', 19 | error: '' 20 | } 21 | 22 | // events 23 | state.events.SITE_REFRESHED = 'site:refreshed' 24 | state.events.SITES_LOADED = 'sites:loaded' 25 | state.events.SITE_CREATOR = 'site:creator' 26 | state.events.SITE_REFRESH = 'site:refresh' 27 | state.events.SITE_CREATE = 'site:create' 28 | state.events.SITES_RESET = 'sites:reset' 29 | state.events.SITE_LOADED = 'site:loaded' 30 | state.events.SITE_REMOVE = 'site:remove' 31 | state.events.SITE_LOAD = 'site:load' 32 | state.events.SITE_ADD = 'site:add' 33 | 34 | // listeners 35 | emitter.on(state.events.DOMCONTENTLOADED, handleSetup) 36 | emitter.on(state.events.SITE_CREATOR, handleCreator) 37 | emitter.on(state.events.SITE_REFRESH, handleRefresh) 38 | emitter.on(state.events.SITE_CREATE, handleCreate) 39 | emitter.on(state.events.SITE_REMOVE, handleRemove) 40 | emitter.on(state.events.SITES_RESET, handleReset) 41 | emitter.on(state.events.SITE_LOAD, handleLoad) 42 | emitter.on(state.events.SITE_ADD, handleAdd) 43 | 44 | async function handleSetup () { 45 | var archives = window.localStorage.getItem('archives') 46 | storage = window.localStorage 47 | state.sites.archives = archives ? JSON.parse(archives) : { } 48 | state.sites.active = window.localStorage.getItem('active') || '' 49 | 50 | if (state.sites.active) { 51 | emitter.emit(state.events.SITE_LOAD, { url: state.sites.active }) 52 | } else { 53 | state.sites.loaded = true 54 | emitter.emit(state.events.RENDER) 55 | } 56 | } 57 | 58 | function handleCreator (data) { 59 | assert.equal(typeof data, 'object', 'enoki: data must be type object') 60 | var changes = state.sites.create 61 | state.sites.create = xtend(changes, data.data) 62 | emitter.emit(state.events.RENDER) 63 | } 64 | 65 | async function handleCreate (data) { 66 | try { 67 | // download the source archive 68 | var archiveSource = await new DatArchive(state.sites.create.url) 69 | emitter.emit(state.events.ENOKI_LOADING, { loading: true, render: true }) 70 | await archiveSource.download('/') 71 | 72 | // fork it 73 | var archiveCreate = await DatArchive.fork( 74 | state.sites.create.url, 75 | state.sites.create 76 | ) 77 | 78 | // one time commit those changes 79 | emitter.once(state.events.SITE_LOADED, async function () { 80 | var archiveNew = await archiveCreate.getInfo() 81 | 82 | // update the title 83 | emitter.emit(state.events.ENOKI_SAVE, { 84 | path: state.content['/'].path, 85 | url: '/', 86 | data: { title: archiveNew.title } 87 | }) 88 | 89 | // reset sites history 90 | emitter.emit(state.events.UI_HISTORY, { 91 | route: 'sites', 92 | path: 'all' 93 | }) 94 | 95 | // reset creator 96 | state.sites.create = getCreateDefaults() 97 | }) 98 | 99 | // reset defaults commit to loading 100 | emitter.emit(state.events.SITE_LOAD, { 101 | url: archiveCreate.url, 102 | redirect: true 103 | }) 104 | } catch (err) { 105 | emitter.emit(state.events.ENOKI_LOADING, { loading: false, render: true }) 106 | throw err 107 | } 108 | } 109 | 110 | async function handleAdd () { 111 | try { 112 | var archive = await DatArchive.selectArchive({ 113 | title: 'Choose a Site or Content', 114 | buttonLabel: 'Add this archive', 115 | filters: { isOwner: true } 116 | }) 117 | emitter.emit(state.events.SITE_LOAD, { url: archive.url, redirect: true }) 118 | } catch (err) { 119 | state.sites.error = err.message 120 | emitter.emit(state.events.RENDER) 121 | throw err 122 | } 123 | } 124 | 125 | async function handleLoad (props) { 126 | emitter.emit(state.events.ENOKI_LOADING, { loading: true, render: true }) 127 | 128 | try { 129 | await enoki.load(props.url) 130 | var archives = await enoki.getArchives() 131 | var content = await enoki.readContent() 132 | var site = await enoki.readSite() 133 | var info = await archives.site.getInfo() 134 | 135 | if (!info.isOwner) throw new Error('You must be the owner of the site') 136 | 137 | state.sites.archives[info.url] = info 138 | state.sites.active = info.url 139 | storage.setItem('archives', JSON.stringify(state.sites.archives)) 140 | storage.setItem('active', info.url) 141 | 142 | emitter.emit(state.events.ENOKI_SITE_LOAD, { 143 | archive: archives.content, 144 | content: content, 145 | site: site, 146 | render: false 147 | }) 148 | 149 | emitter.emit(state.events.UI_HISTORY, { 150 | route: 'editor', 151 | path: '/' 152 | }) 153 | 154 | emitter.emit(state.events.ENOKI_LOADING, { loading: false }) 155 | emitter.emit(state.events.SITE_LOADED) 156 | 157 | if (props.redirect === true) { 158 | emitter.emit(state.events.PUSHSTATE, '/?url=/') 159 | } else if (props.render !== false) { 160 | state.sites.loaded = true 161 | emitter.emit(state.events.RENDER) 162 | } 163 | } catch (err) { 164 | var archiveInfo = state.sites.archives[props.url] 165 | 166 | if (typeof archiveInfo === 'object') { 167 | archiveInfo.error = err.message 168 | } 169 | 170 | state.sites.error = err.message 171 | state.sites.loaded = true 172 | 173 | emitter.emit(state.events.ENOKI_LOADING, { loading: false }) 174 | emitter.emit(state.events.RENDER) 175 | 176 | alert(err.message) 177 | console.warn(err) 178 | } 179 | } 180 | 181 | async function handleRefresh (props) { 182 | await handleLoad({ url: state.sites.active }) 183 | emitter.emit(state.events.SITE_REFRESHED) 184 | emitter.emit(state.events.RENDER) 185 | } 186 | 187 | function handleRemove (props) { 188 | delete state.sites.archives[props.url] 189 | if (props.url === state.sites.active) state.sites.active = '' 190 | storage.setItem('active', state.sites.active) 191 | storage.setItem('archives', JSON.stringify(state.sites.archives)) 192 | if (props.render !== false) emitter.emit(state.events.RENDER) 193 | } 194 | 195 | function handleReset () { 196 | state.sites.active = '' 197 | state.sites.archives = { } 198 | storage.setItem('active', state.sites.active) 199 | storage.setItem('archives', JSON.stringify(state.sites.archives)) 200 | emitter.emit(state.events.RENDER) 201 | } 202 | } 203 | 204 | function getCreateDefaults() { 205 | return { 206 | title: '', 207 | description: '', 208 | url: 'dat://57cb1b649045ab34d762e25a16fc08dbe8ea2006d4373e10719899d2ae7c6ff5' 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /source/views/changes.js: -------------------------------------------------------------------------------- 1 | var queryString = require('query-string') 2 | var objectKeys = require('object-keys') 3 | var html = require('choo/html') 4 | 5 | var Modal = require('../components/modal') 6 | var modal = Modal() 7 | 8 | module.exports = changes 9 | 10 | function changes (state, emit) { 11 | var activeChanges = objectKeys(state.enoki.changes) 12 | .map(function (key) { 13 | return { 14 | state: state.content[key], 15 | changes: state.enoki.changes[key] 16 | } 17 | }) 18 | 19 | var content = html` 20 |
    21 |
    22 |
    Title
    23 |
    24 | 27 |
    28 | ` 29 | 30 | return modal.render({ 31 | content: content, 32 | className: 'c6', 33 | handleContainerClick: function (event) { 34 | emit(state.events.REPLACESTATE, '?url=' + state.page.url) 35 | } 36 | }) 37 | } 38 | 39 | function renderRouteChanges (props) { 40 | if (!props.state || !props.state.url) return 41 | return html` 42 |
  • 43 | 44 |
    45 | ${props.state.title} 46 |
    47 |
    48 |
    49 | ${objectKeys(props.changes).length} 50 |
    51 |
    52 |
    53 |
  • 54 | ` 55 | } 56 | -------------------------------------------------------------------------------- /source/views/default.js: -------------------------------------------------------------------------------- 1 | var queryString = require('query-string') 2 | var objectKeys = require('object-keys') 3 | var html = require('choo/html') 4 | var xtend = require('xtend') 5 | 6 | // containers 7 | var PageHeader = require('../containers/page-header') 8 | var Fields = require('../containers/fields') 9 | 10 | // components 11 | var Breadcrumbs = require('../components/breadcrumbs') 12 | var Header = require('../components/header') 13 | var ActionBar = require('../components/actionbar') 14 | var Publish = require('../components/publish') 15 | var Split = require('../components/split') 16 | 17 | // views 18 | var FilesAll = require('./files-all') 19 | var PagesAll = require('./pages-all') 20 | var FileNew = require('./file-new') 21 | var PageNew = require('./page-new') 22 | var Changes = require('./changes') 23 | var Sites = require('./sites') 24 | var File = require('./file') 25 | 26 | // methods 27 | var methodsFile = require('../lib/file') 28 | var methodsPage = require('../lib/page') 29 | 30 | // misc 31 | 32 | module.exports = view 33 | 34 | function view (state, emit) { 35 | var search = queryString.parse(location.search) 36 | var changes = state.enoki.changes[search.url] 37 | 38 | return [ 39 | Header(state, emit), 40 | content() 41 | ] 42 | 43 | // TODO: clean this up 44 | function content () { 45 | // non p2p 46 | if (!state.sites.p2p && state.sites.loaded) { 47 | return nonDat(state, emit) 48 | } 49 | 50 | // sites 51 | if (search.sites || !state.sites.active) { 52 | return Sites(state, emit) 53 | } 54 | 55 | // changes 56 | if (search.changes) { 57 | return [ 58 | PageHeader(state, emit), 59 | Page(), 60 | Breadcrumbs(state, emit), 61 | Changes(state, emit) 62 | ] 63 | } 64 | 65 | // files 66 | if (search.file === 'new') { 67 | return [ 68 | PageHeader(state, emit), 69 | Page(), 70 | Breadcrumbs(state, emit), 71 | FileNew(state, emit) 72 | ] 73 | } 74 | 75 | // single file 76 | if (search.file) return [ 77 | File(state, emit), 78 | Breadcrumbs(state, emit) 79 | ] 80 | 81 | // pages 82 | if (search.pages === 'all') { 83 | return [ 84 | PageHeader(state, emit), 85 | Breadcrumbs(state, emit), 86 | PagesAll(state, emit) 87 | ] 88 | } 89 | 90 | // create page 91 | if (search.page === 'new') { 92 | return [ 93 | PageHeader(state, emit), 94 | Page(), 95 | Breadcrumbs(state, emit), 96 | PageNew(state, emit) 97 | ] 98 | } 99 | 100 | // all files 101 | if (search.files === 'all') { 102 | return [ 103 | PageHeader(state, emit), 104 | FilesAll(state, emit), 105 | Breadcrumbs(state, emit) 106 | ] 107 | } 108 | 109 | // store route history 110 | if (state.ui.history.editor !== state.query.url || '/') { 111 | emit(state.events.UI_HISTORY, { 112 | route: 'editor', 113 | path: state.query.url || '/' 114 | }) 115 | } 116 | 117 | return [ 118 | PageHeader(state, emit), 119 | Page(), 120 | Breadcrumbs(state, emit) 121 | ] 122 | } 123 | 124 | function Page () { 125 | return html` 126 |
    127 |
    128 | ${Fields(state, emit, { 129 | oninput: handleFieldUpdate 130 | })} 131 |
    132 | ${ActionBar({ 133 | disabled: changes === undefined, 134 | saveLarge: true, 135 | handleCancel: handleCancelPage 136 | })} 137 |
    138 |
    139 |
    140 | ` 141 | } 142 | 143 | function handleFieldUpdate (key, data) { 144 | emit(state.events.ENOKI_UPDATE, { 145 | url: state.page.url, 146 | data: { [key]: data } 147 | }) 148 | } 149 | 150 | function handleSavePage (event) { 151 | if (!changes) return 152 | if (event) event.preventDefault() 153 | 154 | emit(state.events.ENOKI_SAVE, { 155 | file: state.page.file, 156 | path: state.page.path, 157 | url: state.page.url, 158 | data: xtend(state.page, changes) 159 | }) 160 | } 161 | 162 | function handleCancelPage () { 163 | emit(state.events.ENOKI_CANCEL, { 164 | url: state.page.url 165 | }) 166 | } 167 | } 168 | 169 | function nonDat (state, emit) { 170 | return html` 171 |
    172 |
    173 |
    174 | Please open Enoki in Beaker, an
    experimental peer-to-peer browser 175 |
    176 | 183 |
    184 | Want to edit your site offline? Navigate to the dat:// url and “Add to Library”, or customize to your liking by forking. 185 |
    186 |
    187 | 190 |
    191 |
    192 | Thanks for your patience; this flow will be improving soon :) 193 |
    194 |
    195 |
    196 | ` 197 | } 198 | -------------------------------------------------------------------------------- /source/views/docs.js: -------------------------------------------------------------------------------- 1 | var html = require('choo/html') 2 | 3 | module.exports = NotFound 4 | 5 | function NotFound (state, emit) { 6 | // load docs 7 | if (!state.docs.loaded) { 8 | emit(state.events.DOCS_LOAD) 9 | return '' 10 | } 11 | 12 | var page = state.docs.content['/'] || { } 13 | 14 | return html` 15 |
    16 |
    ${page.title}
    17 | ${page.text} 18 |
    19 | ` 20 | } 21 | -------------------------------------------------------------------------------- /source/views/file-new.js: -------------------------------------------------------------------------------- 1 | var objectValues = require('object-values') 2 | var html = require('choo/html') 3 | 4 | var Modal = require('../components/modal') 5 | var Uploader = require('../components/uploader') 6 | 7 | var uploader = Uploader() 8 | var modal = Modal() 9 | 10 | module.exports = FileNew 11 | 12 | function FileNew (state, emit) { 13 | return modal.render({ 14 | content: content(), 15 | handleContainerClick: function () { 16 | emit(state.events.REPLACESTATE, '?panel=active') 17 | } 18 | }) 19 | 20 | function content () { 21 | return html` 22 |
    26 | ${uploader.render({ 27 | redirect: true, 28 | upload: true, 29 | text: 'Drag and drop here, or click to select files', 30 | handleFile: handleUploadFile, 31 | handleDragEnter: handleDragEnter, 32 | handleDragLeave: handleDragLeave 33 | })} 34 |
    35 | ` 36 | } 37 | 38 | function handleUploadFile (event, data) { 39 | emit(state.events.ENOKI_FILE_ADD, { 40 | filename: data.name, 41 | path: state.page.url, 42 | result: data.result 43 | }) 44 | } 45 | 46 | function handleDragEnter (event) { 47 | var el = event.target.parentNode 48 | el.classList.remove('bgwhite', 'tcblack') 49 | el.classList.add('bgblack', 'tcwhite') 50 | } 51 | 52 | function handleDragLeave (event) { 53 | var el = event.target.parentNode 54 | el.classList.add('bgwhite', 'tcblack') 55 | el.classList.remove('bgblack', 'tcwhite') 56 | } 57 | } -------------------------------------------------------------------------------- /source/views/file.js: -------------------------------------------------------------------------------- 1 | var queryString = require('query-string') 2 | var objectKeys = require('object-keys') 3 | var html = require('choo/html') 4 | var path = require('path') 5 | 6 | var methodsFile = require('../lib/file') 7 | 8 | var ActionBar = require('../components/actionbar') 9 | var Split = require('../components/split') 10 | var Fields = require('../containers/fields') 11 | 12 | module.exports = File 13 | 14 | function File (state, emit) { 15 | var search = queryString.parse(location.search) 16 | var filename = methodsFile.decodeFilename(search.file) 17 | var file = state.page.files ? state.page.files[filename] : false 18 | if (!file) return notFound() 19 | var blueprint = methodsFile.getBlueprint(state) 20 | var draftFile = state.enoki.changes[file.url] 21 | 22 | // blueprint layout fix 23 | blueprint.layout = false 24 | 25 | // display in columns 26 | return Split( 27 | [sidebar(), actionbarWrapper()], 28 | content() 29 | ) 30 | 31 | function content () { 32 | return html` 33 |
    37 | ${file.type === 'image' ? image() : ''} 38 | ${file.type === 'video' ? video() : ''} 39 | ${file.type === 'audio' ? audio() : ''} 40 |
    41 | ` 42 | } 43 | 44 | function sidebar () { 45 | return html` 46 | 69 | ` 70 | } 71 | 72 | function actionbarWrapper () { 73 | return html` 74 |
    75 |
    76 | ${ActionBar({ 77 | disabled: draftFile === undefined, 78 | handleSave: handleSave, 79 | handleCancel: handleCancel 80 | })} 81 |
    82 |
    83 | ` 84 | } 85 | 86 | function image () { 87 | return html`` 88 | } 89 | 90 | function audio () { 91 | return html` 92 |
    93 |
    98 | ` 99 | } 100 | 101 | function video () { 102 | return html` 103 |
    104 |
    106 | ` 107 | } 108 | 109 | function notFound () { 110 | return html` 111 |
    112 | ${filename} is not found 113 |
    114 | ` 115 | } 116 | 117 | function handleFieldUpdate (event, data) { 118 | emit(state.events.ENOKI_UPDATE, { 119 | url: file.url, 120 | data: { [event]: data } 121 | }) 122 | } 123 | 124 | function handleSave () { 125 | alert('Image meta saving coming soon') 126 | // emit(state.events.ENOKI_SAVE, { 127 | // file: file.filename + '.txt', 128 | // path: state.page.path, 129 | // url: file.url, 130 | // data: objectKeys(blueprint.fields).reduce(function (result, field) { 131 | // result[field] = draftFile[field] === undefined ? file[field] : draftFile[field] 132 | // return result 133 | // }, { }) 134 | // }) 135 | } 136 | 137 | function handleCancel () { 138 | emit(state.events.ENOKI_CANCEL, { 139 | path: file.path, 140 | url: file.url 141 | }) 142 | } 143 | 144 | function handleRemove () { 145 | emit(state.events.ENOKI_REMOVE, { 146 | path: file.path, 147 | url: file.url 148 | }) 149 | } 150 | } -------------------------------------------------------------------------------- /source/views/files-all.js: -------------------------------------------------------------------------------- 1 | var objectValues = require('object-values') 2 | var queryString = require('query-string') 3 | var html = require('choo/html') 4 | var xtend = require('xtend') 5 | 6 | var Uploader = require('../components/uploader') 7 | var methodsFile = require('../lib/file') 8 | var uploader = Uploader() 9 | 10 | module.exports = FilesAll 11 | 12 | function FilesAll (state, emit) { 13 | var pageFiles = objectValues(state.page.files || { }).map(function (pageFile) { 14 | var data = xtend(pageFile, { }) 15 | data.urlPanel = queryString.stringify(xtend({ 16 | file: methodsFile.encodeFilename(pageFile.filename) 17 | }, state.query)) 18 | data.urlPanel = unescape(data.urlPanel) 19 | return data 20 | }) 21 | 22 | return html` 23 |
    24 |
    25 |
    26 |
    27 | Files 28 |
    29 |
    30 | Upload 35 |
    36 |
    37 | ${handleFilesUpload ? elUploadContainer() : ''} 38 |
      39 | ${elsFiles(pageFiles)} 40 |
    41 |
    42 | 43 | ` 44 | 45 | function elUploadContainer () { 46 | return html` 47 |
    51 | ${uploader.render({ 52 | text: 'Drag and drop here to add files', 53 | handleFiles: handleFilesUpload, 54 | handleDragEnter: function (event) { 55 | var el = event.target.parentNode.parentNode.parentNode 56 | el.classList.remove('bgwhite', 'tcblack') 57 | el.classList.add('bgblack', 'tcwhite') 58 | }, 59 | handleDragLeave: function (event) { 60 | var el = event.target.parentNode.parentNode.parentNode 61 | el.classList.add('bgwhite', 'tcblack') 62 | el.classList.remove('bgblack', 'tcwhite') 63 | } 64 | }, emit)} 65 |
    66 | ` 67 | } 68 | 69 | function handleFilesAdd (event) { 70 | uploader.open() 71 | event.preventDefault() 72 | } 73 | 74 | function handleFilesUpload (event, data) { 75 | emit(state.events.ENOKI_FILES_ADD, { 76 | path: state.page.path, 77 | url: state.page.url, 78 | files: data.files 79 | }) 80 | } 81 | } 82 | 83 | function elsFiles (files) { 84 | files = files || [ ] 85 | 86 | // Hide if there is nothing 87 | if (files.length <= 0) return html` 88 |
  • 89 | No files 90 |
  • 91 | ` 92 | 93 | return files.map(function (child) { 94 | return html` 95 |
  • 96 | ${child.filename} 100 |
  • 101 | ` 102 | }) 103 | } -------------------------------------------------------------------------------- /source/views/guide.js: -------------------------------------------------------------------------------- 1 | var objectKeys = require('object-keys') 2 | var html = require('choo/html') 3 | var css = require('sheetify') 4 | var xtend = require('xtend') 5 | var path = require('path') 6 | 7 | var guideThumbnail = require('../components/guide-thumbnail') 8 | var wrapper = require('../containers/wrapper-hub') 9 | var format = require('../components/format') 10 | 11 | var styles = css` 12 | :host { 13 | margin-top: -1px; 14 | } 15 | 16 | :host .guides-grid { 17 | grid-template-columns: repeat(2, 1fr); 18 | } 19 | 20 | @media (max-width: 38rem) { 21 | :host .guides-grid { 22 | grid-template-columns: 1fr; 23 | } 24 | } 25 | ` 26 | 27 | module.exports = wrapper(view) 28 | 29 | function view (state, emit) { 30 | var tags = state.page.tags || [ ] 31 | var parent = state.docs.content[path.resolve(state.page.url, '../')] 32 | if (!parent) return 33 | var pages = objectKeys(parent.pages) 34 | var pageIndex = pages.indexOf(state.page.name) 35 | var pagePrev = parent.pages[pages[mod(pageIndex - 1, pages.length)]] 36 | var pageNext = parent.pages[pages[mod(pageIndex + 1, pages.length)]] 37 | 38 | if (pagePrev) pagePrev = state.docs.content[pagePrev.url] 39 | if (pageNext) pageNext = state.docs.content[pageNext.url] 40 | 41 | return html` 42 |
    43 |
    50 |
    51 |

    ${state.page.title}

    52 |
    53 | ${tags.map(function (tag) { 54 | return html`${tag}` 55 | })} 56 |
    57 |
    58 |
    59 |
    60 |
    61 | ${format(state.page.text)} 62 |
    63 |
    64 |
    65 | ${renderGuide({ title: 'Previous Guide', page: pagePrev })} 66 | ${renderGuide({ title: 'Next Guide', page: pageNext })} 67 |
    68 |
    69 | ` 70 | 71 | function renderGuide (props) { 72 | return html` 73 |
    74 | ${guideThumbnail(xtend(props.page, { featured: false }))} 75 |
    76 | ` 77 | } 78 | 79 | function renderImage () { 80 | return html` 81 |
    85 | ` 86 | } 87 | } 88 | 89 | function mod (num, mod) { 90 | var remain = num % mod 91 | return Math.floor(remain >= 0 ? remain : remain + mod) 92 | } -------------------------------------------------------------------------------- /source/views/guides.js: -------------------------------------------------------------------------------- 1 | var objectValues = require('object-values') 2 | var html = require('choo/html') 3 | var css = require('sheetify') 4 | 5 | var guideThumbnail = require('../components/guide-thumbnail') 6 | var wrapper = require('../containers/wrapper-hub') 7 | 8 | module.exports = wrapper(view) 9 | 10 | function view (state, emit) { 11 | var guides = objectValues(state.page.pages) 12 | .map(page => state.docs.content[page.url]) 13 | .filter(page => (page && page.visible !== false)) 14 | 15 | return html` 16 |
    17 |
    18 | ${guides.map(guideThumbnail)} 19 |
    20 |
    21 | ` 22 | } 23 | -------------------------------------------------------------------------------- /source/views/hub.js: -------------------------------------------------------------------------------- 1 | var html = require('choo/html') 2 | var wrapper = require('../containers/wrapper-hub') 3 | var format = require('../components/format') 4 | 5 | module.exports = wrapper(hub) 6 | 7 | function hub (state, emit) { 8 | return html` 9 |
    10 |
    11 |
    12 | ${format(state.page.text)} 13 |
    14 |
    15 |
    16 | ` 17 | } 18 | -------------------------------------------------------------------------------- /source/views/log.js: -------------------------------------------------------------------------------- 1 | var objectValues = require('object-values') 2 | var html = require('choo/html') 3 | var css = require('sheetify') 4 | 5 | var wrapper = require('../containers/wrapper-hub') 6 | var format = require('../components/format') 7 | 8 | var styles = css` 9 | :host { 10 | padding: 2rem; 11 | display: grid; 12 | grid-template-columns: repeat(2, minmax(30rem, 1fr)); 13 | grid-column-gap: 1rem; 14 | grid-row-gap: 1rem; 15 | } 16 | 17 | :host > div { 18 | grid-column-end: span 1; 19 | padding: 2rem; 20 | } 21 | 22 | :host summary { outline: 0 } 23 | 24 | :host summary::-webkit-details-marker { 25 | display: none; 26 | } 27 | ` 28 | 29 | module.exports = wrapper(view) 30 | 31 | function view (state, emit) { 32 | var issues = state.docs.content['/issues'] || { } 33 | var log = state.docs.content['/log'] 34 | var issuesActive = objectValues(issues.pages || { }) 35 | .map(page => state.docs.content[page.url]) 36 | .filter(page => page.visible === true) 37 | 38 | return html` 39 |
    40 |
    41 |
    42 | ${format(log.text)} 43 |
    44 |
    45 |
    46 |

    Issues

    47 |
      48 | ${issuesActive.map(renderIssue)} 49 |
    50 |
    51 |
    52 | ` 53 | } 54 | 55 | function renderIssue (props) { 56 | return html` 57 |
  • 58 |
    59 | 60 |
    61 | ${props.title} 62 |
    63 |
    64 | ${props.tags.map(function (tag) { 65 | return html`${tag}` 66 | })} 67 |
    68 |
    69 |
    70 |
    71 | ${format(props.text)} 72 |
    73 |
    74 |
    75 |
  • 76 | ` 77 | } 78 | -------------------------------------------------------------------------------- /source/views/network.js: -------------------------------------------------------------------------------- 1 | var html = require('choo/html') 2 | var wrapper = require('../containers/wrapper-hub') 3 | var format = require('../components/format') 4 | 5 | module.exports = wrapper(network) 6 | 7 | function network (state, emit) { 8 | return html` 9 |
    10 |
    11 |
    Network
    12 |
    Coming Soon
    13 |
    14 |
    15 | ` 16 | } 17 | -------------------------------------------------------------------------------- /source/views/notfound.js: -------------------------------------------------------------------------------- 1 | var html = require('choo/html') 2 | 3 | module.exports = NotFound 4 | 5 | function NotFound (state, emit) { 6 | return html` 7 |
    8 | Not Found 9 |
    10 | ` 11 | } 12 | -------------------------------------------------------------------------------- /source/views/page-new.js: -------------------------------------------------------------------------------- 1 | var html = require('choo/html') 2 | var objectKeys = require('object-keys') 3 | var path = require('path') 4 | 5 | var Modal = require('../components/modal') 6 | var PageNew = require('../containers/page-new') 7 | var methodsPage = require('../lib/page') 8 | 9 | var modal = Modal() 10 | var pageNew = PageNew() 11 | 12 | module.exports = PageNewView 13 | 14 | function PageNewView (state, emit) { 15 | var blueprint = getBlueprint() 16 | var views = methodsPage.getViews({ 17 | blueprint: blueprint, 18 | blueprints: state.site.blueprints 19 | }) 20 | 21 | var content = pageNew.render({ 22 | key: 'add', 23 | view: views.default ? 'default' : objectKeys(views)[0], 24 | views: views 25 | }, handleView) 26 | 27 | return modal.render({ 28 | content: content, 29 | className: 'c6', 30 | handleContainerClick: function (event) { 31 | emit(state.events.REPLACESTATE, '?url=' + state.page.url) 32 | } 33 | }) 34 | 35 | function handleView (data) { 36 | switch (data.event) { 37 | case 'save': 38 | if (!data.value.title || !data.value.uri || !data.value.view) { 39 | return alert('Missing data') 40 | } 41 | emit(state.events.ENOKI_PAGE_ADD, { 42 | title: data.value.title, 43 | view: data.value.view || 'default', 44 | path: path.join(state.page.path, data.value.uri), 45 | url: path.join(state.page.url, data.value.uri) 46 | }) 47 | break 48 | case 'cancel': return emit(state.events.REPLACESTATE, '?url=' + state.page.url) 49 | } 50 | } 51 | 52 | function getBlueprint () { 53 | if (!state.page || !state.site.loaded) { 54 | return { } 55 | } else { 56 | return ( 57 | state.site.blueprints[state.page.view] || 58 | state.site.blueprints.default 59 | ) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /source/views/pages-all.js: -------------------------------------------------------------------------------- 1 | var html = require('choo/html') 2 | var objectValues = require('object-values') 3 | var queryString = require('query-string') 4 | var xtend = require('xtend') 5 | 6 | var methodsFile = require('../lib/file') 7 | 8 | module.exports = pagesAll 9 | 10 | function pagesAll (state, emit) { 11 | var urlPageNew = unescape(queryString.stringify(xtend(state.query, { page: 'new', pages: undefined }))) 12 | var pagePages = objectValues(state.page.pages || { }).map(function (pagePage) { 13 | return state.content[pagePage.url] 14 | }) 15 | 16 | return html` 17 |
    18 |
    19 |
    20 | Pages 21 |
    22 |
    23 | Create 24 |
    25 |
    26 |
    29 | 30 | ` 31 | } 32 | 33 | function elsChildren (children) { 34 | children = children || [ ] 35 | 36 | if (children.length <= 0) { 37 | return html` 38 |
  • 39 | No sub-pages 40 |
  • 41 | ` 42 | } 43 | 44 | return children.map(function (child) { 45 | if (!child.url) return 46 | return html` 47 |
  • 48 | ${child.title || child.name} 52 |
  • 53 | ` 54 | }) 55 | } -------------------------------------------------------------------------------- /source/views/settings.js: -------------------------------------------------------------------------------- 1 | var html = require('choo/html') -------------------------------------------------------------------------------- /source/views/sites-create.js: -------------------------------------------------------------------------------- 1 | var objectValues = require('object-values') 2 | var html = require('choo/html') 3 | var xtend = require('xtend') 4 | 5 | var Fields = require('../containers/fields') 6 | var blueprint = require('../blueprints/sites-create.json') 7 | 8 | var subTitle = { 9 | designs: 'Select a starting point', 10 | meta: 'Enter the details' 11 | } 12 | 13 | module.exports = SitesCreate 14 | 15 | function SitesCreate (state, emit) { 16 | var designs = objectValues(state.designs.public) 17 | var section = state.query.sites 18 | var selected = designs.filter(function (design) { 19 | return state.sites.create.url === design.url 20 | })[0] 21 | 22 | function renderContainer (content) { 23 | 24 | } 25 | 26 | return html` 27 |
    28 |
    29 |
    30 | Select a starting point 31 |
    32 |
    33 |
    34 | Cancel 38 |
    39 |
    40 |
    41 | ${renderContent()} 42 |
    43 | ` 44 | 45 | function renderContent () { 46 | // designs 47 | if (state.query.sites === 'designs') { 48 | return [ 49 | renderDesigns(), 50 | renderButtonMeta() 51 | ] 52 | } 53 | 54 | // meta 55 | if (state.query.sites === 'meta') { 56 | return [ 57 | renderFields(), 58 | renderButtonGenerate() 59 | ] 60 | } 61 | } 62 | 63 | function renderFields () { 64 | return html` 65 |
    66 |
    67 | ${Fields({ 68 | blueprint: blueprint, 69 | draft: { }, 70 | page: state.sites.create, 71 | values: state.sites.create, 72 | oninput: handleFieldUpdate 73 | }, emit)} 74 |
    75 |
    76 | ` 77 | } 78 | 79 | function renderDesigns () { 80 | return html` 81 |
    82 | ${designs.map(function (design) { 83 | var props = xtend({ 84 | active: state.sites.create.url === design.url, 85 | handleSelect: handleDesignSelect 86 | }, design) 87 | return renderDesign(props) 88 | })} 89 |
    90 | ` 91 | } 92 | 93 | function renderButtonMeta () { 94 | return html` 95 |
    96 |
    97 | 102 |
    103 |
    104 | ` 105 | } 106 | 107 | function renderButtonMetaOld () { 108 | return html` 109 |
    110 | 116 |
    117 | ` 118 | } 119 | 120 | function renderButtonGenerate () { 121 | return html` 122 |
    123 |
    124 | 129 |
    130 |
    131 | ` 132 | } 133 | 134 | function handleFieldUpdate (event, data) { 135 | emit(state.events.SITE_CREATOR, { 136 | path: 'sites-create', 137 | data: { [event]: data } 138 | }) 139 | } 140 | 141 | function handleDesignSelect (data) { 142 | emit(state.events.SITE_CREATOR, { 143 | path: 'sites-create', 144 | data: { url: data.url } 145 | }) 146 | } 147 | 148 | function handleCreate (event) { 149 | event.preventDefault() 150 | emit(state.events.SITE_CREATE, { 151 | url: state.sites.create.url 152 | }) 153 | } 154 | } 155 | 156 | function renderDesign (props) { 157 | return html` 158 |
    159 |
    160 |
    161 |
    165 |
    166 | 167 |
    168 |
    169 |
    170 |
    171 | ${props.title} 172 |
    173 |
    174 | Preview 175 |
    176 |
    177 |
    178 |
    179 | ` 180 | 181 | function handleSelect () { 182 | if (typeof props.handleSelect === 'function' && props.url) { 183 | props.handleSelect({ url: props.url }) 184 | } 185 | } 186 | } -------------------------------------------------------------------------------- /source/views/sites.js: -------------------------------------------------------------------------------- 1 | var objectValues = require('object-values') 2 | var assert = require('assert') 3 | var html = require('choo/html') 4 | var xtend = require('xtend') 5 | 6 | var RenderSiteCreate = require('./sites-create') 7 | 8 | module.exports = view 9 | 10 | function view (state, emit) { 11 | var sites = objectValues(state.sites.archives) 12 | 13 | // store route history 14 | if (state.ui.history.sites !== state.query.sites) { 15 | emit(state.events.UI_HISTORY, { 16 | route: 'sites', 17 | path: state.query.sites 18 | }) 19 | } 20 | 21 | // not loaded 22 | if (!state.sites.loaded) return 23 | 24 | // create 25 | if (state.query.sites === 'designs' || state.query.sites === 'meta') { 26 | return RenderSiteCreate(state, emit) 27 | } 28 | 29 | // all 30 | if (sites.length === 0 || state.query.sites === 'empty') { 31 | return renderEmpty(state, emit) 32 | // empty 33 | } else { 34 | return renderSites({ 35 | selected: state.site.info ? state.site.info.url : '', 36 | handleRemove: handleRemove, 37 | handleLoad: handleLoad, 38 | handleAdd: handleAdd, 39 | active: state.query.sites, 40 | sites: sites 41 | }) 42 | } 43 | 44 | function handleAdd () { 45 | emit(state.events.SITE_ADD) 46 | } 47 | 48 | function handleLoad (props) { 49 | emit(state.events.SITE_LOAD, props) 50 | } 51 | 52 | function handleRemove (props) { 53 | emit(state.events.SITE_REMOVE, props) 54 | } 55 | } 56 | 57 | function renderSites (props) { 58 | return html` 59 |
    60 |
    61 |
    62 | Sites 63 |
    64 |
    65 |
    66 |
    Load an Existing Site
    70 |
    71 |
    72 | Create a New Site 76 |
    77 |
    78 |
    79 | ${props.sites.map(function (site) { 80 | return renderSite(xtend(site, { 81 | selected: props.selected === site.url, 82 | active: props.active === site.url, 83 | handleLoad: props.handleLoad, 84 | handleRemove: props.handleRemove 85 | })) 86 | })} 87 |
    88 | ` 89 | } 90 | 91 | function renderEmpty (state, emit) { 92 | return html` 93 |
    94 |
    95 |
    96 | enoki 97 |
    98 |
    99 | 103 |
    104 |
    105 |
    106 |
    107 | 113 |
    114 | Want some help? 115 |
    116 |
    117 |
    118 |
    119 |
    120 |
    121 | ` 122 | 123 | function handleTts () { 124 | emit('tts:set-voice', 'Samantha') 125 | emit('tts:speak', { 126 | id: 1, 127 | text: 'Enoki is a publishing tool for the decentralized web', 128 | rate: 0.65, 129 | pitch: 0.1 130 | }) 131 | console.log(state) 132 | } 133 | 134 | function handleAdd () { 135 | emit(state.events.SITE_ADD) 136 | } 137 | } 138 | 139 | function renderSite (props) { 140 | var settingsUrl = props.active ? '?sites=all' : ('?sites=' + props.url) 141 | var settingsClass = props.active ? 'fc-fg' : 'fc-bg25 fch-fg' 142 | 143 | return html` 144 |
    145 |
    146 |
    147 |
    148 |
    ${props.title}
    149 |
    150 |
    151 | Settings 152 |
    153 |
    154 | Open 155 |
    156 |
    157 | 161 |
    162 |
    163 | ${props.error ? renderError() : ''} 164 | ${props.active ? renderSettings() : ''} 165 |
    166 |
    167 |
    168 |
    169 |
    170 | ` 171 | 172 | function renderError () { 173 | return html`
    ${props.error}
    ` 174 | } 175 | 176 | function renderSettings () { 177 | return html` 178 |
    179 |
    180 |
    181 |
    182 |
    183 |
    184 | Additional settings and p2p stats coming soon 185 |
    186 |
    187 | 188 |
    189 |
    190 |
    191 | ` 192 | } 193 | 194 | function handleSiteClick () { 195 | if (typeof props.handleLoad === 'function') { 196 | props.handleLoad({ url: props.url, redirect: true }) 197 | } 198 | } 199 | 200 | function handleRemove () { 201 | if (typeof props.handleRemove === 'function') { 202 | props.handleRemove({ url: props.url, render: true }) 203 | } 204 | } 205 | } --------------------------------------------------------------------------------