├── .gitmodules ├── .gitignore ├── js ├── components │ ├── ShipComponent.coffee │ ├── ImagepanelComponent.coffee │ ├── CodeMirror.coffee │ ├── LoadComponent.coffee │ ├── ModuleComponent.coffee │ ├── KidsComponent.coffee │ ├── TreeComponent.coffee │ ├── Components.coffee │ ├── ScriptComponent.coffee │ ├── PanelComponent.coffee │ ├── DpadComponent.coffee │ ├── EmailComponent.coffee │ ├── SibsComponent.coffee │ ├── SearchComponent.coffee │ ├── Reactify.coffee │ ├── PostComponent.coffee │ ├── TocComponent.coffee │ ├── CommentsComponent.coffee │ ├── PlanComponent.coffee │ ├── Async.coffee │ ├── ListComponent.coffee │ ├── BodyComponent.coffee │ └── NavComponent.coffee ├── dispatcher │ └── Dispatcher.coffee ├── main.coffee ├── persistence │ └── TreePersistence.coffee ├── utils │ ├── util.coffee │ └── scroll.coffee ├── actions │ └── TreeActions.coffee └── stores │ └── TreeStore.coffee ├── css ├── _kids.scss ├── main.scss ├── _sections.scss ├── _comments.scss ├── _post.scss ├── _util.scss ├── _menu.scss ├── _body-plan.scss ├── _list.scss ├── _body.scss ├── _nav-urbit.scss ├── _body-urbit.scss └── _nav.scss ├── package.json └── readme.md /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "bootstrap"] 2 | path = css/bootstrap 3 | url = https://github.com/urbit/bootstrap 4 | branch = urbit-module 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules/ 3 | .sass-cache 4 | # OS 5 | .DS_Store 6 | # compiled files 7 | main.css 8 | main.css.map 9 | main.js 10 | /desk 11 | -------------------------------------------------------------------------------- /js/components/ShipComponent.coffee: -------------------------------------------------------------------------------- 1 | recl = React.createClass 2 | {div} = React.DOM 3 | 4 | module.exports = recl 5 | render: -> 6 | attr = 7 | "data-alias":"~"+window.tree.util.shortShip(@props.ship) 8 | className:'ship' 9 | (div attr,"~",@props.ship) 10 | -------------------------------------------------------------------------------- /css/_kids.scss: -------------------------------------------------------------------------------- 1 | .kids.runes { 2 | h1 { padding-top: 3rem; } 3 | 4 | h2 { 5 | font-size: 1.5rem; 6 | margin-bottom: 1rem; 7 | } 8 | 9 | > div { margin-top: 6rem; } 10 | 11 | > hr { display: none; } 12 | 13 | > div p:first-of-type { font-weight: 500; } 14 | } 15 | -------------------------------------------------------------------------------- /js/dispatcher/Dispatcher.coffee: -------------------------------------------------------------------------------- 1 | module.exports = _.extend new Flux.Dispatcher(), { 2 | handleServerAction: (action) -> 3 | @dispatch 4 | source: 'server' 5 | action: action 6 | 7 | handleViewAction: (action) -> 8 | @dispatch 9 | source: 'view' 10 | action: action 11 | } -------------------------------------------------------------------------------- /js/components/ImagepanelComponent.coffee: -------------------------------------------------------------------------------- 1 | recl = React.createClass 2 | name = (displayName,component)-> _.extend component, {displayName} 3 | {div} = React.DOM 4 | 5 | module.exports = name "ImagePanel", ({src})-> 6 | div({ 7 | className:"image-container", 8 | style:{backgroundImage:"url('#{src}')"} 9 | }) 10 | -------------------------------------------------------------------------------- /js/components/CodeMirror.coffee: -------------------------------------------------------------------------------- 1 | recl = React.createClass 2 | {div,textarea} = React.DOM 3 | 4 | module.exports = recl 5 | render: -> div {}, textarea ref:'ed', value:@props.value 6 | componentDidMount: -> 7 | CodeMirror.fromTextArea ReactDOM.findDOMNode(@refs.ed), 8 | readOnly:true 9 | lineNumbers:true 10 | -------------------------------------------------------------------------------- /css/main.scss: -------------------------------------------------------------------------------- 1 | @import "./bootstrap/scss/mixins/breakpoints.scss"; 2 | @import "./bootstrap/scss/mixins/grid.scss"; 3 | @import "./bootstrap/scss/mixins/urbit.scss"; 4 | @import "./bootstrap/scss/_custom.scss"; 5 | 6 | @import "util"; 7 | @import "nav"; 8 | @import "nav-urbit"; 9 | @import "body"; 10 | @import "body-urbit"; 11 | @import "body-plan"; 12 | @import "menu"; 13 | @import "list"; 14 | @import "kids"; 15 | @import "post"; 16 | @import "sections"; 17 | @import "comments"; 18 | -------------------------------------------------------------------------------- /js/components/LoadComponent.coffee: -------------------------------------------------------------------------------- 1 | recl = React.createClass 2 | {span,div} = React.DOM 3 | 4 | module.exports = recl 5 | displayName: "Load" 6 | getInitialState: -> {anim: 0} 7 | 8 | componentDidMount: -> @interval = setInterval @setAnim, 100 9 | 10 | componentWillUnmount: -> clearInterval @interval 11 | 12 | setAnim: -> 13 | anim = @state.anim+1 14 | if anim > 3 then anim = 0 15 | @setState {anim} 16 | 17 | render: -> (span {className:"loading state-#{@state.anim}"}, '') 18 | -------------------------------------------------------------------------------- /js/components/ModuleComponent.coffee: -------------------------------------------------------------------------------- 1 | recl = React.createClass 2 | {div} = React.DOM 3 | 4 | TreeActions = require '../actions/TreeActions.coffee' 5 | 6 | module.exports = recl 7 | displayName:"Module" 8 | 9 | componentDidMount: -> 10 | setTimeout => TreeActions.setNav 11 | title:@props["nav:title"] 12 | dpad:false if @props["nav:no-dpad"]? 13 | sibs:false if @props["nav:no-sibs"]? 14 | subnav:@props["nav:subnav"] 15 | , 0 # XX dispatch while dispatching 16 | 17 | componentWillUnmount: -> 18 | # reset tree store state 19 | setTimeout (-> TreeActions.clearNav()), 0 20 | 21 | render: -> (div {className:"module"}, @props.children) 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tree", 3 | "version": "0.0.0", 4 | "description": "urbit tree browser", 5 | "main": "main.js", 6 | "dependencies": { 7 | "classnames": "^2.1.3", 8 | "coffeeify": "~1.0.0", 9 | "watchify": "^3.7.0", 10 | "node-sass": "^4.7.2" 11 | }, 12 | "browserify": { 13 | "transform": [ 14 | "coffeeify" 15 | ] 16 | }, 17 | "scripts": { 18 | "watch": "sh build/watch.sh" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/urbit/tree" 23 | }, 24 | "contributors": [ 25 | { 26 | "name": "galen wolfe-pauly", 27 | "email": "galen@tlon.io" 28 | }, 29 | { 30 | "name": "anton dyudin", 31 | "email": "anton@tlon.io" 32 | } 33 | ], 34 | "license": "MIT" 35 | } 36 | -------------------------------------------------------------------------------- /js/components/KidsComponent.coffee: -------------------------------------------------------------------------------- 1 | clas = require 'classnames' 2 | 3 | util = require '../utils/util.coffee' 4 | 5 | reactify = require './Reactify.coffee' 6 | query = require './Async.coffee' 7 | 8 | recl = React.createClass 9 | {div,a,ul,li,hr} = React.DOM 10 | 11 | module.exports = query {kids: {name:'t', bump:'t', body:'r', meta:'j', path:'t'}}, recl 12 | displayName: "Kids" 13 | render: -> 14 | kids = util.sortKids @props.kids, @props.sortBy 15 | 16 | kidsClas = clas 17 | kids:true 18 | @props.className 19 | 20 | kidClas = clas 21 | "col-md-4":(@props.grid is 'true') 22 | 23 | div {className:kidsClas,key:"kids"}, 24 | for elem in kids 25 | body = reactify elem.body, null, {basePath:elem.path} 26 | [(div {key:elem.name,id:elem.name,className:kidClas}, body), (hr {})] 27 | -------------------------------------------------------------------------------- /js/components/TreeComponent.coffee: -------------------------------------------------------------------------------- 1 | # top level tree component should get rendered to document.body 2 | # and only render two components, head and nav 3 | # each one can determine whether or not it's a container. 4 | 5 | query = require './Async.coffee' 6 | 7 | clas = require 'classnames' 8 | 9 | recf = React.createFactory 10 | recl = React.createClass 11 | 12 | head = recf require './NavComponent.coffee' 13 | body = recf require './BodyComponent.coffee' 14 | 15 | {div} = React.DOM 16 | 17 | module.exports = query { 18 | body:'r' 19 | name:'t' 20 | path:'t' 21 | meta:'j' 22 | sein:'t' 23 | }, (recl 24 | displayName: "Tree" 25 | 26 | render: -> 27 | treeClas = clas 28 | container: @props.meta.container isnt 'false' 29 | 30 | (div {className:treeClas},[ 31 | (head {key:'head-container'}, "") 32 | (body {key:'body-container'}, "") 33 | ]) 34 | ) 35 | -------------------------------------------------------------------------------- /js/components/Components.coffee: -------------------------------------------------------------------------------- 1 | recl = React.createClass 2 | {div} = React.DOM 3 | 4 | module.exports = 5 | codemirror: require './CodeMirror.coffee' 6 | search: require './SearchComponent.coffee' 7 | list: require './ListComponent.coffee' 8 | kids: require './KidsComponent.coffee' 9 | toc: require './TocComponent.coffee' 10 | email: require './EmailComponent.coffee' 11 | module: require './ModuleComponent.coffee' 12 | script: require './ScriptComponent.coffee' 13 | plan: require './PlanComponent.coffee' 14 | panel: require './PanelComponent.coffee' 15 | post: require './PostComponent.coffee' 16 | imagepanel: require './ImagepanelComponent.coffee' 17 | load: require './LoadComponent.coffee' 18 | ship: require './ShipComponent.coffee' 19 | lost: recl render: -> (div {}, "") 20 | -------------------------------------------------------------------------------- /js/components/ScriptComponent.coffee: -------------------------------------------------------------------------------- 1 | recl = React.createClass 2 | rele = React.createElement 3 | 4 | TreeActions = require '../actions/TreeActions.coffee' 5 | 6 | waitingScripts = null # null = none waiting, [] = one in flight, [...] = blocked 7 | appendNext = -> 8 | unless waitingScripts? 9 | return 10 | if waitingScripts.length is 0 11 | waitingScripts = null 12 | else 13 | document.body.appendChild waitingScripts.shift() 14 | 15 | # Script eval shim 16 | module.exports = recl 17 | displayName:"Script" 18 | componentDidMount: -> 19 | s = document.createElement 'script' 20 | _.assign s, @props 21 | TreeActions.registerScriptElement s 22 | s.onload = appendNext 23 | @js = s 24 | if waitingScripts? 25 | waitingScripts.push s 26 | else 27 | waitingScripts = [s] 28 | appendNext() 29 | 30 | componentWillUnmount: -> 31 | if @js.parentNode == document.body 32 | document.body.removeChild @js 33 | 34 | render: -> rele "script", @props 35 | -------------------------------------------------------------------------------- /css/_sections.scss: -------------------------------------------------------------------------------- 1 | @import "./bootstrap/scss/_custom.scss"; 2 | 3 | .sections { 4 | h1 { 5 | font-size: 2rem; 6 | // color: $blue; 7 | } 8 | 9 | h1:first-of-type { padding-bottom: 1rem; } 10 | 11 | .list h1 { padding-bottom: 0; } 12 | 13 | li h1 { font-size: 1.2rem; } 14 | 15 | ul { 16 | list-style-type: none; 17 | font-weight: 500; 18 | padding-left: 0; 19 | } 20 | 21 | ul li, 22 | ul li a, 23 | ul li h1 span { 24 | color: $gray; 25 | margin-bottom: 1rem; 26 | line-height: 1.5rem; 27 | } 28 | 29 | .kids > div { 30 | display: inline-block; 31 | vertical-align: top; 32 | margin-bottom: 3rem; 33 | float: none; 34 | } 35 | 36 | hr { display: none; } 37 | } 38 | 39 | .lead-offset { margin-left: 4rem; } 40 | 41 | @include media-breakpoint-down(md) { 42 | .lead-offset { margin-left: 4rem; } 43 | } 44 | 45 | @include media-breakpoint-down(sm) { 46 | .lead-offset { margin-left: 0; } 47 | 48 | .sections { 49 | h1 { font-size: 1.6rem;} 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /js/components/PanelComponent.coffee: -------------------------------------------------------------------------------- 1 | recl = React.createClass 2 | rele = React.createElement 3 | {nav,ul,li,a} = React.DOM 4 | 5 | module.exports = recl 6 | getInitialState: -> loaded:urb.ship? 7 | componentDidMount: -> urb.init => @setState {'loaded'} 8 | render: -> 9 | if !urb.user? or urb.user isnt urb.ship 10 | (nav {className:"navbar panel"}, [ 11 | (ul {className:"nav navbar-nav"},[ 12 | (li {className:'nav-item pull-right'}, 13 | (a {href:"/~~"}, "Log in")) 14 | ]) 15 | ]) 16 | 17 | else 18 | (nav {className:"navbar panel"}, [ 19 | (ul {className:"nav navbar-nav"},[ 20 | (li {className:"nav-item"}, 21 | (a {href:"/~~/talk"}, "Talk")) 22 | (li {className:"nav-item"}, 23 | (a {href:"/~~/dojo"}, "Dojo")) 24 | (li {className:"nav-item"}, 25 | (a {href:"/~~/static"}, "Static")) 26 | (li {className:'nav-item pull-right'}, 27 | (a {href:"/~/away"}, "Log out")) 28 | ]) 29 | ]) 30 | -------------------------------------------------------------------------------- /js/components/DpadComponent.coffee: -------------------------------------------------------------------------------- 1 | util = require '../utils/util.coffee' 2 | 3 | recl = React.createClass 4 | {div,a} = React.DOM 5 | 6 | Arrow = (name,path)-> 7 | href = util.basepath path 8 | (a {href,key:"#{name}",className:"#{name}"},"") 9 | 10 | module.exports = Dpad = ({sein,curr,kids,meta})-> 11 | arrowUp = 12 | if sein 13 | if meta.navuptwo 14 | Arrow "up", sein.replace /\/[^\/]*$/, "" # strip last path element 15 | else 16 | Arrow "up", sein 17 | 18 | arrowSibs = ( 19 | keys = util.getKeys kids, meta.navsort 20 | if keys.length > 1 21 | index = keys.indexOf(curr) 22 | prev = index-1 23 | next = index+1 24 | if prev < 0 then prev = keys.length-1 25 | if next is keys.length then next = 0 26 | prev = keys[prev] 27 | next = keys[next] 28 | if sein 29 | if sein is "/" then sein = "" 30 | div {}, 31 | if prev then Arrow "prev", "#{sein}/#{prev}" 32 | if next then Arrow "next", "#{sein}/#{next}" 33 | ) 34 | 35 | (div {className:'dpad',key:'dpad'}, arrowUp, arrowSibs) 36 | -------------------------------------------------------------------------------- /js/main.coffee: -------------------------------------------------------------------------------- 1 | rend = ReactDOM.render 2 | 3 | $ -> 4 | util = require './utils/util.coffee' 5 | window.tree.util = util 6 | require './utils/scroll.coffee' 7 | 8 | if document.location.pathname.substr(-1) isnt "/" 9 | history.replaceState {}, "",document.location.pathname+"/"+ 10 | document.location.search+ 11 | document.location.hash 12 | 13 | # we load modules that may need to send actions up, so we attach 14 | # the actions to window here. 15 | window.tree.actions = require './actions/TreeActions.coffee' 16 | 17 | # reactify has virtual components which themselves need to call 18 | # reactify. to do this, we register the components after the fact 19 | window.tree.actions.addVirtual require './components/Components.coffee' 20 | 21 | frag = util.fragpath window.location.pathname.replace /\.[^\/]*$/,'' 22 | window.tree.actions.setCurr frag, true 23 | window.tree.actions.loadPath frag,window.tree.data 24 | if window.tree.sein? 25 | window.tree.actions.loadSein frag,window.tree.sein 26 | window.urb.ondataupdate = (dep)-> 27 | for dat of window.urb.datadeps 28 | window.urb.dewasp(dat) 29 | window.tree.actions.clearData() 30 | 31 | main = React.createFactory require './components/TreeComponent.coffee' 32 | rend (main {}, ""),document.getElementById('tree') 33 | -------------------------------------------------------------------------------- /js/persistence/TreePersistence.coffee: -------------------------------------------------------------------------------- 1 | util = require '../utils/util.coffee' 2 | dedup = {} # XX wrong layer 3 | 4 | pending = {} 5 | waspWait = [] 6 | 7 | module.exports = 8 | refresh: -> dedup = {} 9 | get: (path,query="no-query",cb) -> 10 | url = "#{util.basepath(path)}.tree-json?q=#{@encode query}" 11 | return if dedup[url] 12 | dedup[url] = true 13 | pending[url] = true 14 | $.get url, {}, (data,status,xhr) -> # XX on error 15 | delete pending[url] 16 | if urb.wasp? 17 | dep = urb.getXHRWasp(xhr) 18 | urb.sources[dep] = url # debugging info 19 | waspWait.push dep 20 | if _.isEmpty pending 21 | waspWait.map urb.waspData 22 | waspWait = [] 23 | if cb then cb null,data 24 | 25 | put: (data,mark,appl,cb)-> 26 | appl ?= /[a-z]*/.exec(mark)[0] 27 | urb.init -> urb.send data, {mark,appl}, cb 28 | 29 | waspElem: (a)-> 30 | if urb.wasp? 31 | urb.waspElem a 32 | 33 | encode: (obj)-> 34 | delim = (n)-> Array(n+1).join('_') || '.' 35 | _encode = (obj)-> 36 | if typeof obj isnt 'object' 37 | return [0,obj] 38 | dep = 0 39 | sub = for k,v of obj 40 | [_dep,res] = _encode v 41 | dep = _dep if _dep > dep 42 | k+(delim _dep)+res if res? 43 | dep++ 44 | [dep, sub.join delim dep] 45 | (_encode obj)[1] 46 | -------------------------------------------------------------------------------- /js/components/EmailComponent.coffee: -------------------------------------------------------------------------------- 1 | recl = React.createClass 2 | {div,p,button,input} = React.DOM 3 | 4 | module.exports = recl 5 | displayName: "email" 6 | 7 | getInitialState: -> {submit:false,email:""} 8 | 9 | onClick: -> @submit() 10 | onChange: (e) -> 11 | email = e.target.value 12 | @setState {email:e.target.value} 13 | valid = (email.indexOf('@') != -1 && 14 | email.indexOf('.') != -1 && 15 | email.length > 7 && 16 | email.split(".")[1].length > 1 && 17 | email.split("@")[0].length > 0 && 18 | email.split("@")[1].length > 4) 19 | @$email.toggleClass 'valid',valid 20 | @$email.removeClass 'error' 21 | if e.keyCode is 13 22 | if valid is true 23 | @submit() 24 | e.stopPropagation() 25 | e.preventDefault() 26 | return false 27 | else 28 | @$email.addClass 'error' 29 | 30 | submit: -> 31 | $.post @props.dataPath,{email:@$email.val()},() => 32 | @setState {submit:true} 33 | 34 | componentDidMount: -> @$email = $('input.email') 35 | 36 | render: -> 37 | if @state.submit is false 38 | submit = @props.submit ? "Sign up" 39 | cont = [ 40 | (input {key:"field",className:"email",placeholder:"your@email.com",@onChange,value:@state.email}) 41 | (button {key:"submit",className:"submit btn",@onClick}, submit) 42 | ] 43 | else 44 | cont = [(div {className:"submitted"},"Got it. Thanks!")] 45 | (p {className:"email",id:"sign-up"}, cont) 46 | -------------------------------------------------------------------------------- /js/components/SibsComponent.coffee: -------------------------------------------------------------------------------- 1 | util = require '../utils/util.coffee' 2 | clas = require 'classnames' 3 | reactify = require './Reactify.coffee' 4 | query = require './Async.coffee' 5 | 6 | recl = React.createClass 7 | {ul,li,a} = React.DOM 8 | 9 | module.exports = query { 10 | kids: 11 | head:'r' 12 | meta:'j' 13 | name:'t' 14 | path:'t' 15 | bump:'t' 16 | }, recl 17 | displayName:"Siblings" 18 | toText: (elem)-> reactify.walk elem, 19 | ()->'' 20 | (s)->s 21 | ({c})->(c ? []).join '' 22 | render: -> 23 | kids = util.sortKids @props.kids, @props.meta.navsort 24 | 25 | navClas = 26 | nav: true 27 | 'col-md-12': (@props.meta.navmode is 'navbar') 28 | if @props.className then navClas[@props.className] = true 29 | navClas = clas navClas 30 | 31 | ul {className:navClas}, kids.map ({head,meta={},name,path}) => 32 | selected = name is @props.curr 33 | if @props.meta.navselect 34 | selected = name is @props.meta.navselect 35 | href = util.basepath path 36 | head = meta.title 37 | head ?= @toText head 38 | head ||= name 39 | className = clas 40 | "nav-item": true 41 | selected: selected 42 | if meta.sibsclass 43 | className += " "+clas(meta.sibsclass.split(",")) 44 | (li {className,key:name}, 45 | (a {className:"nav-link",href,onClick:@props.closeNav}, head)) 46 | -------------------------------------------------------------------------------- /js/components/SearchComponent.coffee: -------------------------------------------------------------------------------- 1 | query = require './Async.coffee' 2 | reactify = require './Reactify.coffee' 3 | 4 | recl = React.createClass 5 | {a,div,input} = React.DOM 6 | 7 | 8 | module.exports = query {name:'t', kids: sect:'j'}, recl 9 | hash:null 10 | displayName: "Search" 11 | getInitialState: -> search: 'wut' 12 | onKeyUp: (e)-> @setState search: e.target.value 13 | wrap: (elem,dir,path)-> 14 | path = path[...-1] if path[-1...] is "/" 15 | href = @props.name+"/"+dir+path 16 | if elem?.ga?.id 17 | {gn,ga,c} = elem 18 | ga = _.clone ga 19 | href += "#"+ga.id 20 | delete ga.id 21 | elem = {gn,ga,c} 22 | {gn:'div', c:[{gn:'a', ga:{href}, c:[elem]}]} 23 | 24 | render: -> div {}, 25 | input {@onKeyUp,ref:'inp',defaultValue:'wut'} 26 | _(@props.kids) 27 | .map(({sect},dir)=> @wrap h,dir,path for h in heds for path,heds of sect) 28 | .flatten() 29 | .flatten() 30 | .map(@highlight) 31 | .filter() 32 | .take(50) 33 | .map(reactify) 34 | .value() 35 | 36 | highlight: (e)-> 37 | return e unless @state.search 38 | got = false 39 | res = reactify.walk e, 40 | ()-> null 41 | (s)=> 42 | m = s.split @state.search 43 | return [s] unless m[1]? 44 | lit = gn:'span',c:[@state.search],ga:style:background: '#ff6' 45 | got = true 46 | [m[0], _.flatten([lit,s] for s in m[1..])...] 47 | ({gn,ga,c})->{gn,ga,c:_.flatten c} 48 | res if got 49 | -------------------------------------------------------------------------------- /css/_comments.scss: -------------------------------------------------------------------------------- 1 | .add-comment { 2 | width: 100%; 3 | border-top: 3px solid $gray-light; 4 | margin-top: 8rem; 5 | 6 | .btn { 7 | padding: .6rem .3rem .4rem .3rem; 8 | text-transform: none; 9 | } 10 | 11 | .ship { 12 | display: inline-block; 13 | margin-top: 2rem; 14 | background-color: transparent; 15 | padding: 0; 16 | } 17 | 18 | textarea { 19 | width: 100%; 20 | display: block; 21 | height: 12rem; 22 | background-color: $gray-lightest; 23 | border-bottom: 0; 24 | margin-bottom: .6rem; 25 | margin-top: .6rem; 26 | padding: .3rem; 27 | } 28 | } 29 | 30 | .add-comment, 31 | .comments { 32 | .ship { 33 | font-family: 'scp'; 34 | font-size: 1rem; 35 | font-weight: 400; 36 | } 37 | } 38 | 39 | .comments { 40 | margin-top: 4rem; 41 | padding-top: 1rem; 42 | border-top: 3px solid $gray-lighter; 43 | 44 | .comment { 45 | margin-top: 2rem; 46 | 47 | & > span { 48 | font-family: 'scp'; 49 | font-size: .8rem; 50 | color: $gray-light; 51 | } 52 | 53 | h2 { 54 | padding-top: 0; 55 | font-size: 1rem; 56 | font-weight: 500; 57 | 58 | code { 59 | background-color: transparent; 60 | padding: 0; 61 | } 62 | } 63 | 64 | p { 65 | width: 66% 66 | } 67 | 68 | &.loading { 69 | color: $gray-light 70 | } 71 | 72 | &.loading > span { 73 | color: $gray-lighter 74 | } 75 | } 76 | } 77 | 78 | @include media-breakpoint-down(sm) { 79 | .add-comment textarea, 80 | .comments .comment p { width: 100% } 81 | } 82 | -------------------------------------------------------------------------------- /js/components/Reactify.coffee: -------------------------------------------------------------------------------- 1 | recl = React.createClass 2 | rele = React.createElement 3 | {div,span} = React.DOM 4 | load = React.createFactory require './LoadComponent.coffee' 5 | 6 | TreeStore = require '../stores/TreeStore.coffee' 7 | 8 | name = (displayName,component)-> _.extend component, {displayName} 9 | 10 | walk = (root,_nil,_str,_comp)-> 11 | # manx: {fork: ["string", {gn:"string" ga:{dict:"string"} c:{list:"manx"}}]} 12 | _walk = (elem,key)-> switch 13 | when !elem? then _nil() 14 | when typeof elem == "string" then _str elem 15 | when elem.gn? 16 | {gn,ga,c} = elem 17 | c = (c?.map _walk) ? [] 18 | _comp.call elem, {gn,ga,c}, key 19 | else throw "Bad react-json #{JSON.stringify elem}" 20 | _walk root 21 | 22 | DynamicVirtual = recl 23 | displayName: "DynamicVirtual" 24 | getInitialState: -> @stateFromStore() 25 | stateFromStore: -> components: TreeStore.getVirtualComponents() 26 | 27 | _onChangeStore: -> if @isMounted() then @setState @stateFromStore() 28 | componentDidMount: -> TreeStore.addChangeListener @_onChangeStore 29 | componentWillUnmount: -> TreeStore.removeChangeListener @_onChangeStore 30 | 31 | render: -> (Virtual _.extend {}, @props, components: @state.components) 32 | 33 | Virtual = name "Virtual", ({manx,components,basePath})-> 34 | walk manx, 35 | ()-> (load {},"") 36 | (str)-> str 37 | ({gn,ga,c},key)-> 38 | props = {key} 39 | if ga?.style 40 | try 41 | ga.style = eval "(#{ga.style})" 42 | catch e 43 | ga.style = ga.style 44 | if components[gn] 45 | props.basePath = basePath 46 | rele (components[gn] ? gn), 47 | (_.extend props, ga), 48 | c if c.length 49 | 50 | reactify = (manx,key,{basePath,components}={})-> 51 | if components? 52 | rele Virtual, {manx,key,basePath,components} 53 | else rele DynamicVirtual, {manx,key,basePath} 54 | module.exports = _.extend reactify, {walk,Virtual} 55 | -------------------------------------------------------------------------------- /css/_post.scss: -------------------------------------------------------------------------------- 1 | #body.post { 2 | .date { 3 | font-family: 'scp'; 4 | color: $gray-light; 5 | font-size: .7rem; 6 | font-weight: 200; 7 | } 8 | 9 | .date, 10 | p.preview, 11 | h3.author { margin-bottom: .6rem; } 12 | 13 | h3.author { 14 | padding-top: 0; 15 | font-size: 1rem; 16 | } 17 | 18 | h3.author:before { 19 | content: "—"; 20 | margin-right: .6rem; 21 | } 22 | 23 | img { 24 | max-width: 100%; 25 | } 26 | } 27 | 28 | .urbit .post, 29 | .urbit.post { 30 | h1.title, 31 | p.preview { font-weight: 500; } 32 | 33 | h1.title { 34 | font-size: 2rem; 35 | padding-bottom: 0; 36 | } 37 | 38 | h1.title, 39 | img { 40 | width: 600px; 41 | margin-bottom: 2rem; 42 | } 43 | 44 | img { border: 12px solid #000; } 45 | 46 | img.full-width { 47 | width: 100%; 48 | border: 0; 49 | } 50 | 51 | img.inline { 52 | margin-bottom: 0; 53 | } 54 | 55 | p.preview { 56 | margin-bottom: .6rem; 57 | max-width: 32rem; 58 | } 59 | h3.author { 60 | line-height: 1rem; 61 | margin-bottom: 1rem; 62 | } 63 | } 64 | 65 | @include media-breakpoint-down(sm) { 66 | .urbit .post h1.title, 67 | .urbit .post img { 68 | max-width: 100%; 69 | } 70 | } 71 | 72 | .urbit.post .person { 73 | margin-bottom: 2rem; 74 | 75 | &:first-of-type { margin-top: 3rem; } 76 | &:last-of-type { margin-bottom: 3rem; } 77 | 78 | & > p, 79 | img { display: inline-block; } 80 | 81 | & > p { width: 60%; } 82 | 83 | img { 84 | width: 30%; 85 | margin: 0 2rem 0 0; 86 | border: 6px solid #555; 87 | } 88 | } 89 | 90 | .urbit .post h1.title { 91 | line-height: 3rem; 92 | } 93 | 94 | .urbit .post img { max-width: 32rem; } 95 | 96 | @include media-breakpoint-down(sm) { 97 | .urbit .post img { 98 | max-width: 100%; 99 | border-width: 6px; 100 | } 101 | } 102 | 103 | .urbit.post { 104 | .preview { display:none; } 105 | } 106 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # `tree` 2 | 3 | `tree` is a single page app for browsing files in `%clay` and loading modular [urbit](http://github.com/urbit/urbit) apps, like `talk`. 4 | 5 | `tree` is a fairly straightforward [flux](https://facebook.github.io/flux/) app. Because pages load inside of a [react](https://facebook.github.io/react/) environment you can use JSX to invoke components found in `components/` in this repo. 6 | 7 | `tree` ships as compiled `main.js` and `main.css` on your urbit (in `/home/web/tree`). If you want to make changes or develop on top, you'll need these source files. 8 | 9 | # Developing 10 | 11 | The `desk/` folder in this repo mirrors a desk on an urbit `planet`. Source files live outside of this folder, we compile them in using watchify / sass and then copy the `/desk` onto the desk we're using for development on a planet. 12 | 13 | Our sass depends on bootstrap mixins, so the urbit fork of bootstrap is included as a submodule. 14 | 15 | First: 16 | 17 | ``` 18 | git submodule init 19 | git submodule update --remote 20 | ``` 21 | 22 | Then: 23 | 24 | ``` 25 | npm install 26 | npm run watch 27 | ``` 28 | 29 | ## Deploy 30 | 31 | Simple: 32 | 33 | `cp -r desk/ [$desk_mountpoint]/` 34 | 35 | If you have urbit installed in `~/urbit` with a planet called `sampel-sipnym` and have mounted the `home` desk: 36 | 37 | `cp -r desk/ ~/urbit/sampel-sipnym/home/` 38 | 39 | Then use the `?dbg.nopack=true` query string to test: 40 | 41 | `http://localhost:8080/some/page?dbg.nopack=true` 42 | 43 | Your Urbit links to the concatenated JS / CSS by default. This query string loads the scripts directly. See below for more information. 44 | 45 | # Contributing 46 | 47 | If you have a patch you'd like to contribute: 48 | 49 | - Test your changes using the above instructions 50 | - Fork this repo 51 | - Send us a pull request 52 | 53 | # Distribution 54 | 55 | Compiled `main.js` and `main.css` get periodically shipped to the [urbit core](http://github.com/urbit/urbit). Each time these compiled files are moved to urbit core their commit message should contain the sha-1 of the commit from this repo. 56 | -------------------------------------------------------------------------------- /js/components/PostComponent.coffee: -------------------------------------------------------------------------------- 1 | clas = require 'classnames' 2 | 3 | load = require './LoadComponent.coffee' 4 | query = require './Async.coffee' 5 | reactify = require './Reactify.coffee' 6 | 7 | TreeActions = require '../actions/TreeActions.coffee' 8 | 9 | util = require '../utils/util.coffee' 10 | 11 | Ship = require './ShipComponent.coffee' 12 | 13 | recl = React.createClass 14 | rele = React.createElement 15 | {div,p,h2,img,a,form,textarea,input,code} = React.DOM 16 | 17 | DEFER_USER = no 18 | 19 | module.exports = query {comt:'j', path:'t', spur:'t'}, recl 20 | displayName: "Post" 21 | getInitialState: -> 22 | loading:null 23 | value:"" 24 | user: urb.user ? "" 25 | 26 | componentDidMount: -> 27 | unless DEFER_USER 28 | urb.init => @setState user:urb.user 29 | 30 | componentDidUpdate: (_props)-> 31 | if urb.user and not @state.user 32 | @setState user: urb.user ? "" 33 | if @props.comt.length > _props.comt.length 34 | @setState loading:null 35 | 36 | onSubmit: (e) -> 37 | @setState loading:true 38 | title = @refs.in.title.value 39 | comment = @refs.in.comment.value 40 | path = @props.path or "/" # XX deal with root path 41 | TreeActions.addPost path,@props.spur,title,comment 42 | e.preventDefault() 43 | 44 | onChange: (e) -> @setState {value:e.target.value} 45 | 46 | render: -> 47 | titleInput = input { 48 | disabled: if @state.loading then "true" 49 | type: "text" 50 | name: "title" 51 | placeholder: "Title" 52 | } 53 | 54 | bodyTextArea = textarea { 55 | disabled: if @state.loading then "true" 56 | type:"text" 57 | name:"comment" 58 | value:@state.value 59 | @onChange 60 | } 61 | 62 | postButton = input { 63 | disabled: if @state.loading then "true" 64 | type:"submit" 65 | value:"Post" 66 | className:"btn btn-primary" 67 | } 68 | 69 | (div {}, 70 | (div {className:"add-post"}, 71 | (form {ref:"in",@onSubmit}, 72 | (rele Ship,{ship:@state.user}) 73 | titleInput 74 | bodyTextArea 75 | postButton 76 | ) 77 | ) 78 | ) 79 | -------------------------------------------------------------------------------- /css/_util.scss: -------------------------------------------------------------------------------- 1 | .col-md-10.body { padding-left: 0; } 2 | 3 | @include media-breakpoint-down(sm) { 4 | .col-md-10.body { 5 | margin-top: $grid-gutter-width/2; 6 | padding-left: $grid-gutter-width/2; 7 | } 8 | } 9 | 10 | .label { 11 | font-size: inherit; 12 | font-weight: 500; 13 | line-height: inherit; 14 | } 15 | 16 | img.logo { 17 | height: 2rem; 18 | width: 2rem; 19 | } 20 | 21 | img.logo.first { 22 | margin-bottom: 2rem; 23 | } 24 | 25 | div.logo { 26 | width: 4rem; 27 | height: 4rem; 28 | background-color: $brand-primary; 29 | display: inline-block; 30 | margin-right: 1rem; 31 | border-radius: 50%; 32 | vertical-align: middle; 33 | margin-top: -.8rem; 34 | } 35 | 36 | div.logo:before { 37 | content: "~"; 38 | color: #FFFFFF; 39 | font-size: 4rem; 40 | vertical-align: middle; 41 | line-height: 3rem; 42 | margin-top: .2rem; 43 | text-align: center; 44 | width: 2rem; 45 | display: inline-block; 46 | font-weight: 200; 47 | } 48 | 49 | div.logo.inverse:before { 50 | color: #fff; 51 | } 52 | 53 | .lead .logo.inverse { 54 | margin-top: -1.4rem; 55 | } 56 | 57 | .short { 58 | width: 9 / $grid-columns * 100%; 59 | } 60 | 61 | .meta-data { 62 | padding: 1rem; 63 | background-color: $gray-lightest; 64 | font-family: 'scp'; 65 | max-width: 12rem; 66 | margin-bottom: 2rem; 67 | 68 | h2, 69 | h3 { 70 | padding: 0; 71 | margin: 0; 72 | font-size: 1rem; 73 | line-height: 2rem; 74 | } 75 | } 76 | 77 | .link-next { 78 | margin-top: 4rem; 79 | 80 | a { 81 | padding: .6rem; 82 | border: 2px solid; 83 | text-decoration: none; 84 | font-weight: 500; 85 | margin-top: 4rem; 86 | } 87 | 88 | a:hover { 89 | background-color: #000; 90 | color: #fff; 91 | } 92 | } 93 | 94 | .loading:before { 95 | font-family: 'scp'; 96 | background-color: #000; 97 | color: #fff; 98 | padding: 0; 99 | margin: 0; 100 | width: 1.6rem; 101 | height: 1.6rem; 102 | text-align: center; 103 | font-size: .8rem; 104 | line-height: 1.7rem; 105 | display: block; 106 | font-weight: 600; 107 | z-index: 3; 108 | } 109 | .loading.state-0:before { content: "\25D3"; } 110 | .loading.state-1:before { content: "\25D1"; } 111 | .loading.state-2:before { content: "\25D2"; } 112 | .loading.state-3:before { content: "\25D0"; } 113 | -------------------------------------------------------------------------------- /js/components/TocComponent.coffee: -------------------------------------------------------------------------------- 1 | query = require './Async.coffee' 2 | reactify = require './Reactify.coffee' 3 | 4 | recl = React.createClass 5 | {div} = React.DOM 6 | 7 | 8 | module.exports = query {body:'r'}, recl 9 | hash:null 10 | displayName: "TableOfContents" 11 | 12 | _click: (id)-> 13 | -> if id then document.location.hash = id 14 | 15 | componentDidMount: -> 16 | @int = setInterval @checkHash,100 17 | @st = $(window).scrollTop() 18 | # $(window).on 'scroll',@checkScroll 19 | @$headers = $('#toc').children('h1,h2,h3,h4').filter('[id]') 20 | 21 | checkScroll: -> 22 | st = $(window).scrollTop() 23 | if Math.abs(@st-st) > 10 24 | hash = null 25 | @st = st 26 | for v in @$headers 27 | continue if v.tagName is undefined 28 | $h = $ v 29 | hst = $h.offset().top-$h.outerHeight(true)+10 30 | if hst < st 31 | hash = $h.attr('id') 32 | if hst > st and hash isnt @hash and hash isnt null 33 | @hash = "#"+hash 34 | document.location.hash = hash 35 | break 36 | 37 | checkHash: -> 38 | if document.location.hash?.length > 0 and document.location.hash isnt @hash 39 | hash = document.location.hash.slice(1) 40 | for v in @$headers 41 | $h = $ v 42 | if hash is $h.attr('id') 43 | @hash = document.location.hash 44 | offset = $h.offset().top - $h.outerHeight(true) 45 | setTimeout -> $(window).scrollTop offset 46 | , 10 47 | break 48 | 49 | componentWillUnmount: -> 50 | clearInterval @int 51 | 52 | collectHeader: ({gn,ga,c}) -> 53 | if @props.match then comp = (gn is @props.match) else 54 | comp = (gn and gn[0] is 'h' and parseInt(gn[1]) isnt NaN) 55 | if comp 56 | ga = _.clone ga 57 | ga.onClick = @_click ga.id 58 | delete ga.id 59 | {gn,ga,c} 60 | 61 | parseHeaders: -> 62 | if @props.body.c 63 | for v in @props.body.c 64 | if v.gn is 'div' and v.ga?.id is "toc" 65 | contents = [ 66 | {gn:"h1", ga:{className:"t"}, c:["Table of contents"]}, 67 | (_.filter v.c.map @collectHeader)... 68 | ] 69 | if @props.noHeader then contents.shift() 70 | return { 71 | gn:"div" 72 | ga:{className:"toc"} 73 | c:contents 74 | } 75 | 76 | render: -> reactify @parseHeaders() 77 | -------------------------------------------------------------------------------- /js/utils/util.coffee: -------------------------------------------------------------------------------- 1 | _basepath = window.urb.util.basepath("/") 2 | _basepath += 3 | (window.location.pathname.replace window.tree._basepath, "").split("/")[0] 4 | 5 | module.exports = 6 | components: 7 | ship: require '../components/ShipComponent.coffee' 8 | 9 | basepath: (path) -> 10 | prefix = _basepath 11 | if prefix is "/" then prefix = "" 12 | if path[0] isnt "/" then path = "/"+path 13 | _path = prefix + path 14 | if _path.slice(-1) is "/" and _path.length > 1 15 | _path = _path.slice(0,-1) 16 | _path 17 | 18 | fragpath: (path) -> 19 | path.replace(/\/$/,'') 20 | .replace(_basepath,"") 21 | 22 | shortShip: (ship= urb.user ? "")-> 23 | if ship.length <= 13 24 | ship 25 | else if ship.length == 27 26 | ship[14...20] + "^" + ship[-6...] 27 | else 28 | ship[0...6] + "_" + ship[-6...] # s/(.{6}).*(.{6})/\1_\2/ 29 | 30 | dateFromAtom: (date)-> 31 | [yer,mon,day,__,hor,min,sec] = # ~y.m.d..h.m.s 32 | date.slice(1).split "." 33 | if day? 34 | d = new Date() 35 | d.setYear yer 36 | d.setMonth mon-1 37 | d.setDate day 38 | if hor? 39 | d.setHours hor 40 | d.setMinutes min 41 | d.setSeconds sec 42 | return d 43 | 44 | getKeys: (kids,sortBy) -> _.map (@sortKids kids,sortBy), 'name' 45 | sortKids: (kids,sortBy=null)-> # kids: {name:'t', bump:'t', meta:'j'} 46 | kids = _.filter(kids,({meta})-> !(meta?.hide)) 47 | switch sortBy 48 | when 'bump' 49 | _.sortBy(kids, 50 | ({bump,meta,name})=> @dateFromAtom bump || meta?.date || name 51 | ).reverse() 52 | # 53 | when 'date' 54 | _kids = [] 55 | for k,v of kids 56 | if not v.meta?.date? # XX throw? 57 | return _.sortBy(kids,'name') 58 | date = @dateFromAtom v.meta.date 59 | unless date? # XX throw 60 | return _.sortBy(kids,'name') 61 | _k = Number(new Date(date)) 62 | _kids[_k] = v 63 | for k in _.keys(_kids).sort().reverse() 64 | _kids[k] 65 | # 66 | when null 67 | _kids = [] 68 | for k,v of kids 69 | if not v.meta?.sort? # XX throw if inconsistent? 70 | return _.sortBy(kids,'name') 71 | _kids[Number(v.meta.sort)] = v 72 | for k in _.keys(_kids).sort() 73 | _kids[k] 74 | # 75 | else throw new Error "Unknown sort: #{sortBy}" 76 | -------------------------------------------------------------------------------- /js/actions/TreeActions.coffee: -------------------------------------------------------------------------------- 1 | TreeDispatcher = require '../dispatcher/Dispatcher.coffee' 2 | TreePersistence = require '../persistence/TreePersistence.coffee' 3 | 4 | _initialLoad = true # XX right place? 5 | _initialLoadDedup = {} 6 | 7 | module.exports = 8 | loadPath: (path,data) -> 9 | TreeDispatcher.handleServerAction {path,data,type:"loadPath"} 10 | 11 | loadSein: (path,data) -> 12 | TreeDispatcher.handleServerAction {path,data,type:"loadSein"} 13 | 14 | clearData: () -> 15 | _initialLoad = false 16 | _initialLoadDedup = {} 17 | TreePersistence.refresh() # XX right place? 18 | TreeDispatcher.handleServerAction {type:"clearData"} 19 | 20 | sendQuery: (path,query) -> 21 | return unless query? 22 | if _initialLoad 23 | key = path+(JSON.stringify query) 24 | unless _initialLoadDedup[key] 25 | _initialLoadDedup[key] = true 26 | console.warn "Requesting data during initial page load", (JSON.stringify path), query 27 | if path.slice(-1) is "/" then path = path.slice(0,-1) 28 | TreePersistence.get path,query,(err,res) => 29 | if err? then throw err 30 | @loadPath path,res 31 | 32 | registerComponent: (name,comp) -> @addVirtual "#{name}": comp 33 | registerScriptElement: (elem)-> TreePersistence.waspElem elem 34 | 35 | addVirtual: (components) -> 36 | TreeDispatcher.handleViewAction {type:"addVirtual", components} 37 | 38 | addComment: (pax,sup,txt)-> 39 | TreePersistence.put {pax,sup,txt}, "fora-comment", "fora", (err,res)=> 40 | if !err? 41 | @clearData() 42 | 43 | addPost: (pax,sup,hed,txt)-> 44 | TreePersistence.put {pax,sup,hed,txt}, "fora-post", "fora", (err,res)=> 45 | if !err? 46 | @clearData() 47 | history.pushState {},"",".." 48 | @setCurr pax 49 | 50 | setPlanInfo: ({who,loc})-> 51 | TreePersistence.put {who,loc}, "write-plan-info", "hood" 52 | 53 | setCurr: (path,init=false) -> 54 | _initialLoad &= init 55 | TreeDispatcher.handleViewAction 56 | type:"setCurr" 57 | path:path 58 | 59 | setNav: ({title,dpad,sibs,subnav}) -> 60 | TreeDispatcher.handleViewAction { 61 | title 62 | dpad 63 | sibs 64 | subnav 65 | type:"setNav" 66 | } 67 | 68 | toggleNav: -> TreeDispatcher.handleViewAction {type:"toggleNav"} 69 | closeNav: -> TreeDispatcher.handleViewAction {type:"closeNav"} 70 | 71 | clearNav: -> 72 | TreeDispatcher.handleViewAction {type:"clearNav"} 73 | -------------------------------------------------------------------------------- /css/_menu.scss: -------------------------------------------------------------------------------- 1 | // submenu 2 | 3 | @keyframes menu-open { 4 | 0% { visibility:hidden; } 5 | 1% { visibility: visible; } 6 | 100% { visibility: visible; } 7 | } 8 | 9 | @keyframes menu-close { 10 | 0% { visibility: visible; } 11 | 1% { visibility: visible; } 12 | 100% { visibility:hidden; } 13 | } 14 | 15 | .menu { 16 | padding-left: 0; 17 | padding-right: 0; 18 | position: fixed; 19 | overflow: hidden; 20 | z-index:101; 21 | margin-top: -3rem; 22 | 23 | .contents { 24 | padding-left: $grid-gutter-width/2; 25 | padding-right: $grid-gutter-width/2; 26 | padding-bottom: 6rem; 27 | position: relative; 28 | left: -100%; 29 | @include transition(left,.3s,ease-in-out); 30 | } 31 | 32 | .close { margin-top: -2rem; } 33 | .close:hover { opacity: 1; } 34 | 35 | h2 { 36 | font-size: 1rem; 37 | padding-top: 3rem; 38 | } 39 | h2:first-of-type { padding-top: 0; } 40 | 41 | label.sum { 42 | font-family: 'scp'; 43 | margin-left: $grid-gutter-width/2; 44 | font-size: 1.2rem; 45 | } 46 | } 47 | 48 | .menu.depth-1 .contents { 49 | background-color: $gray-lightest; 50 | border-left: 2px solid $gray-light; 51 | } 52 | 53 | .menu.depth-2 .contents { 54 | background-color: $gray-lighter; 55 | border-left: 2px solid $gray-light; 56 | } 57 | 58 | .menu.depth-2 { 59 | max-height: 100%; 60 | overflow-y: scroll; 61 | } 62 | 63 | .menu.closed { 64 | animation-name: menu-close; 65 | animation-duration:.3s; 66 | animation-fill-mode: forwards; 67 | } 68 | .menu.open { 69 | animation-name: menu-open; 70 | animation-duration: .3s; 71 | animation-fill-mode: forwards; 72 | } 73 | .menu.open .contents { left: 0; } 74 | 75 | @include media-breakpoint-down(sm) { 76 | .menu { 77 | height: 100%; 78 | padding-left: 0; 79 | padding-right: $grid-gutter-width*1.5; 80 | border-left: 0; 81 | 82 | .contents { 83 | left: inherit; 84 | top: -100%; 85 | padding-top: $grid-gutter-width/2; 86 | padding-bottom: $grid-gutter-width*1.5; 87 | @include transition(top,.3s,ease-in-out) 88 | } 89 | 90 | &.depth-1 .contents, 91 | &.depth-2 .contents { 92 | border-left: 0; 93 | border-bottom: 2px solid $gray-light; 94 | } 95 | 96 | .close { 97 | margin-top: 0; 98 | } 99 | } 100 | 101 | .menu.open .contents { 102 | top: 3rem; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /js/utils/scroll.coffee: -------------------------------------------------------------------------------- 1 | TreeActions = require '../actions/TreeActions.coffee' 2 | 3 | scroll = 4 | w: null # width 5 | $d: null # container 6 | $n: null # nav 7 | nh: null # nav height cached 8 | cs: null # current scroll 9 | ls: null # last scroll 10 | cwh: null # current window height 11 | lwh: null # last window height 12 | 13 | track: -> 14 | @w = $(window).width() 15 | @$n = $('#head') 16 | @$d = $('#head .ctrl') 17 | @nh = $('#head .ctrl').outerHeight(true) 18 | 19 | clearNav: -> @$n.removeClass 'm-up m-down m-fixed' 20 | 21 | resize: -> if @w > 1170 then @clearNav() 22 | 23 | scroll: -> 24 | unless @$n? and @$d? 25 | return 26 | @cs = $(window).scrollTop() 27 | @cwh = window.innerHeight 28 | 29 | if @w > 767 then @clearNav() 30 | if @w < 767 31 | dy = @ls-@cs 32 | 33 | @$d.removeClass 'focus' 34 | 35 | if @cs <= 0 36 | @$n.removeClass 'm-up' 37 | @$n.addClass 'm-down m-fixed' 38 | return 39 | 40 | # scrolling up 41 | if dy > 0 42 | if not @$n.hasClass 'm-down' 43 | @$n.removeClass('m-up').addClass 'm-down' 44 | ct = @$n.offset().top 45 | top = @cs-@nh 46 | if @cs > ct and @cs < ct+@nh then top = ct 47 | # if top < 0 then top = 0 48 | @$n.offset top:top 49 | # set fixed when at top 50 | if @$n.hasClass('m-down') and 51 | not @$n.hasClass('m-fixed') and 52 | @$n.offset().top >= @cs 53 | @$n.addClass 'm-fixed' 54 | @$n.attr {style:''} 55 | 56 | # scrolling down 57 | if dy < 0 58 | if @cwh == @lwh 59 | if not @$n.hasClass 'm-up' 60 | @$n.removeClass('m-down m-fixed').addClass 'm-up' 61 | TreeActions.closeNav() 62 | $('.menu.open').removeClass 'open' 63 | top = if @cs < 0 then 0 else @cs 64 | ct = @$n.offset().top 65 | if top > ct and top < ct+@nh then top = ct 66 | @$n.offset top:top 67 | # close when gone if open 68 | if @$n.hasClass('m-up') and 69 | @$d.hasClass('open') 70 | if @cs > @$n.offset().top + @$n.height() 71 | TreeActions.closeNav() 72 | 73 | @ls = @cs 74 | @lwh = @cwh 75 | 76 | 77 | init: -> 78 | setInterval @track.bind(@),200 79 | 80 | @ls = $(window).scrollTop() 81 | @cs = $(window).scrollTop() 82 | 83 | $(window).on 'resize', @resize.bind @ 84 | $(window).on 'scroll', @scroll.bind @ 85 | 86 | scroll.init() 87 | 88 | module.exports = scroll 89 | -------------------------------------------------------------------------------- /css/_body-plan.scss: -------------------------------------------------------------------------------- 1 | @include media-breakpoint-down(sm) { 2 | #body.plan { padding-top: 0; } 3 | } 4 | 5 | .body .plan { 6 | .above { 7 | background-color: #000; 8 | 9 | .mono, 10 | h6 { font-size: 1.2rem; } 11 | 12 | h6 { padding-top: 0; } 13 | 14 | .mono { font-weight: 200; } 15 | 16 | & > .container { 17 | padding-top: 6rem; 18 | padding-bottom: 6rem; 19 | } 20 | 21 | .home { 22 | border-radius: 50%; 23 | height: 2.4rem; 24 | width: 2.4rem; 25 | border: 4px solid #fff; 26 | display: inline-block; 27 | float: left; 28 | margin-left: -4.4rem; 29 | margin-top: 1rem; 30 | } 31 | 32 | .edit { 33 | background-color: transparent; 34 | border: 0; 35 | padding: 0; 36 | font-weight: 500; 37 | border-bottom: 3px solid #fff; 38 | line-height: 1rem; 39 | padding-top: 2rem; 40 | } 41 | 42 | .grid .tr .td { font-family: 'scp'; } 43 | .grid .tr .td:first-child { font-family: 'bau'; } 44 | 45 | input { 46 | background-color: $gray-dark; 47 | border: none; 48 | } 49 | 50 | .panel a { 51 | text-decoration: none; 52 | border-bottom: 2px solid $gray; 53 | font-weight: 500; 54 | color: $gray; 55 | } 56 | 57 | .panel a:hover { 58 | border-color: #fff; 59 | color: #fff; 60 | } 61 | } 62 | 63 | .panel.stack { 64 | padding-top: 0; 65 | padding-bottom: 0; 66 | } 67 | 68 | .plan.stack { 69 | padding-top: 4rem; 70 | } 71 | 72 | .pull-right { 73 | float: right; 74 | } 75 | 76 | .above, 77 | .above a { color: #fff; } 78 | 79 | .service { 80 | white-space: nowrap; 81 | } 82 | 83 | .service:before { 84 | content:" "; 85 | display: inline-block; 86 | border-radius: 50%; 87 | height: .5rem; 88 | width: .5rem; 89 | background-color: $green; 90 | margin-right: .6rem; 91 | } 92 | 93 | .grid { 94 | .td { 95 | display: table-cell; 96 | } 97 | .tr .td:first-child { 98 | min-width: 9rem; 99 | margin-right: 2rem; 100 | letter-spacing: 1px; 101 | color: $gray-light; 102 | } 103 | } 104 | } 105 | 106 | .stream { 107 | .mini-module { margin-top: 3rem; } 108 | h6 { 109 | color: $gray; 110 | font-size: 1rem; 111 | margin-bottom: 2rem; 112 | } 113 | } 114 | 115 | @include media-breakpoint-down(sm) { 116 | .grid .td { display: block; } 117 | .tr .td:first-child { margin-top: 1rem; } 118 | } 119 | -------------------------------------------------------------------------------- /css/_list.scss: -------------------------------------------------------------------------------- 1 | .list { 2 | h1, 3 | h1:first-of-type { 4 | font-size: inherit; 5 | font-weight: inherit; 6 | padding-bottom: 0; 7 | margin-bottom: 0; 8 | line-height: 2rem; 9 | } 10 | h1.error { 11 | font-size: 1.6rem; 12 | font-weight: 500; 13 | } 14 | } 15 | 16 | .list.spaced li { 17 | margin-bottom: 2rem; 18 | } 19 | 20 | .list.children { 21 | h1 { 22 | font-weight: 500; 23 | font-size: 1.4rem; 24 | } 25 | } 26 | 27 | .list.post .date { 28 | font-family: 'scp'; 29 | font-size: .7rem; 30 | margin-bottom: .6rem; 31 | } 32 | 33 | .list.post .author { 34 | font-size: 1rem; 35 | padding-top: 0; 36 | } 37 | 38 | .list.post .author:before { 39 | content: "—"; 40 | margin-right: .6rem; 41 | } 42 | 43 | .list.p.code { 44 | background-color: transparent; 45 | } 46 | 47 | 48 | .body[data-path*='/docs'] .list, 49 | .body[data-path^='/work'] .list { 50 | list-style-type: none; 51 | padding-left: 0; 52 | 53 | a, 54 | h1 { 55 | color: $black; 56 | font-weight: 500; 57 | height: 2rem; 58 | display: inline; 59 | text-decoration: none; 60 | } 61 | 62 | p a { 63 | font-weight: 400; 64 | text-decoration: underline; 65 | } 66 | 67 | h1 { border-bottom: 2px solid #000; } 68 | } 69 | 70 | .body[data-path^='/work'] .list.main { 71 | a, 72 | h1 { 73 | font-size: 2rem; 74 | line-height: 4rem; 75 | height: 4rem; 76 | } 77 | 78 | h1 { border-width: 4px; } 79 | } 80 | 81 | .body[data-path*='/docs'] .list.runes { 82 | a h1, 83 | a div, 84 | a p { 85 | display: inline-block; 86 | } 87 | 88 | a p { font-weight: 300; } 89 | 90 | a { text-decoration: none; } 91 | 92 | a, 93 | a h1 code { color: #000; } 94 | 95 | a h1 { 96 | display: block; 97 | margin-bottom: .3rem; 98 | height: auto; 99 | border-bottom: none; 100 | } 101 | 102 | a h1 code { 103 | background-color: transparent; 104 | font-weight: 500; 105 | font-size: 1.2rem; 106 | padding: 0; 107 | border-bottom: 2px solid #000; 108 | } 109 | 110 | a code { background-color: $gray-lighter; } 111 | 112 | a code { 113 | padding: .3rem; 114 | } 115 | 116 | // a:after { 117 | // content: "▶"; 118 | // width: 1rem; 119 | // font-size: .6rem; 120 | // vertical-align: middle; 121 | // padding: .3rem; 122 | // padding-top: .4rem; 123 | // line-height: 1rem; 124 | // margin-left: 1rem; 125 | // background-color: $gray-light; 126 | // color: #fff; 127 | // } 128 | 129 | // a:hover:after { 130 | // background-color: $black; 131 | // } 132 | } 133 | 134 | .urbit ul.blog { 135 | list-style-type: none; 136 | padding-left: 0; 137 | 138 | li { margin-bottom: 12rem; } 139 | 140 | .continue { 141 | font-weight: 500; 142 | text-decoration: none; 143 | border-bottom: 3px solid #000; 144 | } 145 | } 146 | 147 | @include media-breakpoint-down(sm) { 148 | .urbit ul.blog li { margin-bottom: 6rem; } 149 | } 150 | -------------------------------------------------------------------------------- /js/components/CommentsComponent.coffee: -------------------------------------------------------------------------------- 1 | clas = require 'classnames' 2 | 3 | load = require './LoadComponent.coffee' 4 | query = require './Async.coffee' 5 | reactify = require './Reactify.coffee' 6 | 7 | TreeActions = require '../actions/TreeActions.coffee' 8 | 9 | util = require '../utils/util.coffee' 10 | 11 | Ship = require './ShipComponent.coffee' 12 | 13 | recl = React.createClass 14 | rele = React.createElement 15 | {div,p,h2,img,a,form,textarea,input,code} = React.DOM 16 | 17 | DEFER_USER = yes 18 | 19 | Comment = ({time,user,body,loading=false}) -> 20 | (div {className:(clas "comment", {loading})}, 21 | "#{window.urb.util.toDate(new Date(time))}", 22 | (h2 {}, (rele Ship, ship:user)) 23 | (reactify body,"comt",{components:{}}) 24 | ) 25 | 26 | module.exports = query {comt:'j', path:'t', spur:'t', meta:'j'}, recl 27 | displayName: "Comments" 28 | getInitialState: -> 29 | loading:null 30 | value:"" 31 | user: urb.user ? "" 32 | 33 | componentDidMount: -> 34 | unless DEFER_USER 35 | urb.init => @setState user:urb.user 36 | 37 | componentDidUpdate: (_props)-> 38 | if urb.user and not @state.user 39 | @setState user: urb.user ? "" 40 | if @props.comt.length > _props.comt.length 41 | @setState loading:null 42 | 43 | onSubmit: (e) -> 44 | {value} = @refs.in.comment 45 | TreeActions.addComment @props.path, @props.spur, value 46 | @setState 47 | value:"" 48 | loading:{'loading', body:{gn:'p',c:[value]}, time:Date.now()} 49 | e.preventDefault() 50 | 51 | onChange: (e) -> @setState {value:e.target.value} 52 | 53 | render: -> 54 | _attr = {} 55 | if @state.loading is true then _attr.disabled = "true" 56 | textareaAttr = _.create _attr, { 57 | type:"text" 58 | name:"comment" 59 | value:@state.value 60 | @onChange 61 | } 62 | inputAttr = _.create _attr, { 63 | type:"submit" 64 | value:"Add comment" 65 | className:"btn btn-primary" 66 | } 67 | 68 | addComment = 69 | (div {key:'add-comment',className:"add-comment"}, 70 | (form {ref:"in",@onSubmit}, 71 | (rele Ship,{ship:@state.user}) 72 | (textarea textareaAttr) 73 | (input inputAttr) 74 | ) 75 | ) 76 | 77 | comments = @props.comt.map (props,key)-> 78 | rele Comment, _.extend {key}, props 79 | 80 | comments.unshift (if @state.loading? 81 | rele Comment, _.extend {key:'loading'}, @state.loading, user: @state.user 82 | ) 83 | 84 | if "reverse" in (@props.meta.comments?.split(" ") ? []) 85 | comments = comments.reverse() 86 | (div {}, [ 87 | (div {key:'comments',className:"comments"}, comments) 88 | addComment 89 | ]) 90 | else 91 | (div {}, [ 92 | addComment 93 | (div {key:'comments',className:"comments"}, comments) 94 | ]) 95 | -------------------------------------------------------------------------------- /js/components/PlanComponent.coffee: -------------------------------------------------------------------------------- 1 | load = require './LoadComponent.coffee' 2 | query = require './Async.coffee' 3 | 4 | TreeActions = require '../actions/TreeActions.coffee' 5 | 6 | recl = React.createClass 7 | rele = React.createElement 8 | {div,textarea,button,input,a,h6,code,span} = React.DOM 9 | 10 | {table,tbody,tr,td} = React.DOM # XX flexbox? 11 | Grid = (props,rows...)-> # Grid [[1,2],null,[3,4],[5,6]] 12 | _td = (x)-> (div {className:"td"}, x) 13 | _tr = (x)-> if x? then (div {className:"tr"}, x.map(_td)...) 14 | (div props, rows.map(_tr)...) 15 | 16 | module.exports = query { 17 | plan:'j' 18 | beak:'t' 19 | path:'t' 20 | }, recl 21 | displayName: "Plan" 22 | getInitialState: -> edit:no, plan:@props.plan, focus: null, loaded:urb.ship? 23 | componentDidMount: -> urb.init => @setState {'loaded'} 24 | componentWillReceiveProps: (props)-> 25 | if _.isEqual @props.plan, @state.plan 26 | @setState plan: props.plan 27 | 28 | refInput: (ref)-> 29 | (node)=> 30 | @[ref] = node 31 | if ref is @state.focus 32 | node?.focus() 33 | 34 | saveInfo: -> 35 | plan = {who:@who.value,loc:@loc.value,acc:@props.plan?.acc} 36 | unless _.isEqual plan, @state.plan 37 | TreeActions.setPlanInfo plan 38 | @setState {plan} 39 | @setState edit:no, focus:null 40 | 41 | render: -> 42 | unless @state.loaded 43 | return (div {className:"plan"}, "Loading authentication info") 44 | {beak,path} = @props 45 | {acc,loc,who} = @state.plan ? {} 46 | issuedBy = 47 | if urb.sein isnt urb.ship 48 | "~"+urb.sein 49 | else "self" 50 | 51 | if urb.user isnt urb.ship 52 | editButton = null 53 | editable = (ref,val,placeholder)-> val ? placeholder 54 | else if @state.edit 55 | editButton = button {className:'edit', onClick:=> @saveInfo()}, "Save" 56 | editable = (ref,val,placeholder)=> 57 | input { 58 | placeholder 59 | defaultValue:val 60 | ref: @refInput ref 61 | onKeyDown: ({keyCode})=> @saveInfo() if keyCode is 13 62 | } 63 | else 64 | editButton = button {className:'edit', onClick:=> @setState edit:yes}, "Edit" 65 | editable = (ref,val,placeholder)=> 66 | span {onClick:=> @setState edit:yes, focus:ref}, 67 | val ? placeholder 68 | unless @props.plan?[ref] is @state.plan?[ref] 69 | rele load, {} 70 | 71 | (div {className:"plan"}, 72 | (div {className:"home"}, "") 73 | (div {className:"mono"}, "~"+urb.ship) 74 | (h6 {}, editable 'who', who, "Sun Tzu") if who? or @state.edit 75 | (Grid {className:"grid"}, 76 | ["Location:", (editable 'loc', loc, "Unknown")] 77 | ["Issued by:", (a {href:"//"+urb.sein+".urbit.org"}, issuedBy)], 78 | ["Immutable link:", (a {href:beak+"/web"+path}, beak)], 79 | ["Connected to:", div {}, 80 | for key,{usr,url} of acc 81 | (div {key, className:'service'}, 82 | if !url? then key+"/"+usr 83 | else a {href:url}, key+"/"+usr 84 | ) 85 | ] unless _.isEmpty acc 86 | ) 87 | editButton 88 | ) 89 | -------------------------------------------------------------------------------- /css/_body.scss: -------------------------------------------------------------------------------- 1 | #tree > div > .container { 2 | padding-top: 3rem; 3 | padding-bottom: 6rem; 4 | } 5 | 6 | @include media-breakpoint-down(sm) { 7 | #tree > div > .container { padding-top: 0; } 8 | #body { padding-top: 4rem; } 9 | } 10 | 11 | .lead h1:first-of-type { 12 | padding-bottom: 0; 13 | } 14 | 15 | .flush { 16 | padding-top: 0; 17 | } 18 | 19 | .h-arrow { 20 | width: 100%; 21 | height: 9rem; 22 | margin-bottom: 3rem; 23 | 24 | h1, 25 | img { float: left; } 26 | 27 | img { 28 | height: 6rem; 29 | margin: 1rem 0 0 1rem; 30 | } 31 | 32 | h1 { 33 | color: $brand-primary; 34 | display: inline-block; 35 | line-height: 4rem; 36 | } 37 | 38 | h1 code { 39 | background-color: transparent; 40 | color: $brand-primary; 41 | } 42 | 43 | } 44 | 45 | .footer { 46 | margin-top: 6rem; 47 | font-weight: 500; 48 | color: $gray-light; 49 | 50 | a { 51 | color: $gray; 52 | text-decoration: none; 53 | border-bottom: 2px solid $gray; 54 | margin-left: .6rem; 55 | } 56 | } 57 | 58 | ol { 59 | counter-reset:li; 60 | list-style: none; 61 | padding-left: 2rem; 62 | } 63 | 64 | ol > li { margin-bottom: 2rem; } 65 | .top ol { padding-left: 0; } 66 | 67 | ol > li:before { 68 | content:counter(li) "."; 69 | counter-increment: li; 70 | text-align: left; 71 | font-weight: 500; 72 | float: left; 73 | margin-right: 1rem; 74 | margin-left: -2rem; 75 | } 76 | 77 | .body[data-path*='/docs'], 78 | .body[data-path^='/work'] { 79 | h1 code { 80 | background-color: transparent; 81 | color: $brand-primary; 82 | padding: 0; 83 | font-size: 100%; 84 | font-weight: 500; 85 | } 86 | 87 | .head { 88 | margin-bottom: 4rem; 89 | padding-left: 0; 90 | } 91 | 92 | p.label { 93 | font-size: .8rem; 94 | padding: 0; 95 | display: block; 96 | text-align: left; 97 | margin-bottom: .6rem; 98 | 99 | .type { 100 | background-color: $gray-light; 101 | color: #fff; 102 | padding: .4rem; 103 | margin-right: .3rem; 104 | } 105 | code { 106 | background-color: transparent; 107 | font-size: 100%; 108 | font-weight: 400; 109 | } 110 | } 111 | 112 | .book { 113 | h2 { color: $gray-light; } 114 | 115 | h2 code { 116 | color: $brand-primary; 117 | background-color: transparent; 118 | } 119 | 120 | h2 a { color: $brand-primary; } 121 | 122 | hr { margin-bottom: 3rem; } 123 | 124 | img { 125 | max-width: 100%; 126 | } 127 | } 128 | } 129 | 130 | .body[data-path^='/work'] { 131 | h2 { color: $brand-primary; } 132 | } 133 | 134 | .more { 135 | border-bottom: .1rem solid $brand-primary; 136 | } 137 | 138 | .more::after { 139 | content: "→"; 140 | font-family: "scp"; 141 | font-weight: 600; 142 | padding-left: .3rem; 143 | } 144 | 145 | // .body[data-path^='/docs/hoon/twig/'] { h1 { color: $gray-light; } } 146 | 147 | // .body[data-path^='/docs/hoon/library/'] { 148 | // h3 { font-size: 1.5rem; } 149 | // h2 { font-size: 1rem; } 150 | // .toc { 151 | // h3 { 152 | // padding-top: 0; 153 | // margin-bottom: 0; 154 | // } 155 | // } 156 | // } 157 | -------------------------------------------------------------------------------- /js/components/Async.coffee: -------------------------------------------------------------------------------- 1 | util = require '../utils/util.coffee' 2 | _load = require './LoadComponent.coffee' 3 | 4 | TreeStore = require '../stores/TreeStore.coffee' 5 | TreeActions = require '../actions/TreeActions.coffee' 6 | 7 | recl = React.createClass 8 | {div,span,code} = React.DOM 9 | 10 | fragsrc = (src, basePath)-> 11 | if src? 12 | basePath = util.basepath basePath 13 | if basePath.slice(-1) isnt "/" then basePath += "/" 14 | base = new URL basePath, document.location 15 | {pathname} = new URL src, base 16 | util.fragpath(pathname) 17 | 18 | module.exports = (queries, Child, load=_load)-> recl 19 | displayName: "Async" 20 | 21 | getInitialState: -> @stateFromStore() 22 | _onChangeStore: -> 23 | if @isMounted() then @setState @stateFromStore() 24 | 25 | getPath: -> 26 | path = @props.dataPath 27 | base = @props.basePath ? TreeStore.getCurr() 28 | path ?= (fragsrc @props.src, base) ? base 29 | if path.slice(-1) is "/" 30 | path.slice 0,-1 31 | else path 32 | 33 | stateFromStore: -> 34 | path = @getPath() 35 | fresh = TreeStore.fulfill path, queries 36 | unless @state? and path is @state.path 37 | got = fresh 38 | else 39 | got = @mergeWith @state.got, fresh 40 | {path,fresh,got, queries} 41 | 42 | mergeWith: (have={},fresh={},_queries=queries)-> 43 | got = {} 44 | for k of _queries when k isnt 'kids' 45 | got[k] = fresh[k] ? have[k] 46 | if _queries.kids? 47 | if not fresh.kids? 48 | got.kids = have.kids 49 | else 50 | got.kids = {} 51 | for k,kid of fresh.kids 52 | got.kids[k] = 53 | @mergeWith have.kids?[k], kid, _queries.kids 54 | got 55 | 56 | componentDidMount: -> 57 | TreeStore.addChangeListener @_onChangeStore 58 | @checkPath() 59 | 60 | componentWillUnmount: -> 61 | TreeStore.removeChangeListener @_onChangeStore 62 | 63 | componentDidUpdate: (_props,_state) -> 64 | if _props isnt @props 65 | @setState @stateFromStore() 66 | @checkPath() 67 | 68 | checkPath: -> TreeActions.sendQuery @getPath(), @filterFreshQueries() 69 | 70 | filterFreshQueries: -> @filterWith @state.fresh, queries 71 | filterQueries: -> @filterWith @state.got, queries 72 | filterWith: (have,_queries)-> 73 | return _queries unless have? 74 | request = {} 75 | for k of _queries when k isnt 'kids' 76 | request[k] = _queries[k] unless have[k] isnt undefined 77 | if _queries.kids? 78 | if not have.kids? 79 | request.kids = _queries.kids 80 | else 81 | request.kids = {} 82 | for k,kid of have.kids 83 | _.merge request.kids, @filterWith kid, _queries.kids 84 | if _.isEmpty request.kids 85 | delete request.kids 86 | request unless _.isEmpty request 87 | 88 | scrollHash: -> @getHashElement()?.scrollIntoView() 89 | getHashElement: -> 90 | {hash} = document.location 91 | if hash then document.getElementById hash[1..] 92 | 93 | render: -> div {}, 94 | if @filterQueries()? 95 | React.createElement load, @props 96 | else 97 | if not @getHashElement() # onmount? 98 | setTimeout @scrollHash,0 99 | React.createElement Child, 100 | (_.extend {}, @props, @state.got), 101 | @props.children 102 | -------------------------------------------------------------------------------- /css/_nav-urbit.scss: -------------------------------------------------------------------------------- 1 | // urbit.org navbar 2 | 3 | .urbit.navbar.ctrl { 4 | margin-top: 1rem; 5 | 6 | .icon .home { 7 | border-color: #000; 8 | background-color: #000; 9 | width: 3rem; 10 | height: 3rem; 11 | margin: 0 4rem 0 0; 12 | } 13 | 14 | .icon .home:before { 15 | content: "~"; 16 | color: #fff; 17 | font-size: 2.6rem; 18 | line-height: 2.6rem; 19 | text-align: center; 20 | width: 2.4rem; 21 | display: inline-block; 22 | } 23 | 24 | ul.nav { 25 | li { 26 | height: 3rem; 27 | vertical-align: middle; 28 | min-width: 0; 29 | margin-right: 2rem; 30 | } 31 | 32 | li a { 33 | text-decoration: none; 34 | color: $black; 35 | font-weight: 500; 36 | font-size: 1rem; 37 | border-bottom: 3px solid $black; 38 | margin-top: 1rem; 39 | } 40 | 41 | .btn { 42 | border: 3px solid $brand-primary; 43 | margin-top: -3px; 44 | height: 3rem; 45 | padding: 0rem; 46 | 47 | a { 48 | font-size: 1rem; 49 | color: $brand-primary; 50 | letter-spacing: 0; 51 | } 52 | } 53 | 54 | .btn.selected, 55 | .btn:hover { 56 | background-color: $brand-primary; 57 | a { color: #fff; } 58 | } 59 | 60 | li a:hover, 61 | li.selected a { 62 | color: $gray; 63 | border-color: $gray; 64 | } 65 | } 66 | 67 | .subnav { 68 | ul.nav { 69 | height: 1.5rem; 70 | 71 | li { height: 1.5rem; } 72 | li a { line-height: 1.5rem; } 73 | 74 | .btn { 75 | position:relative; 76 | height: 3rem; 77 | a{ line-height: 3rem; } 78 | } 79 | } 80 | } 81 | } 82 | 83 | .urbit.home.navbar.ctrl { 84 | .icon .home { 85 | opacity: 0; 86 | border-color: #fff; 87 | background-color: #fff; 88 | } 89 | 90 | .icon .home:before { 91 | color: $gray-light; 92 | } 93 | 94 | ul li a { 95 | color: #fff; 96 | border-color: #fff; 97 | } 98 | } 99 | 100 | @include media-breakpoint-down(sm) { 101 | .urbit.home.navbar.ctrl { 102 | .icon .home { display: none; } 103 | } 104 | 105 | .urbit.navbar.ctrl { 106 | height: auto; 107 | margin: 0; 108 | position: absolute; 109 | border: none; 110 | 111 | .icon { 112 | margin-top: 1rem; 113 | margin-bottom: .5rem; 114 | 115 | .home { 116 | width: 18px; 117 | height: 18px; 118 | margin: 0 1rem 0 0; 119 | } 120 | 121 | .home:before { 122 | font-size: 1rem; 123 | line-height: .7rem; 124 | width: .6rem; 125 | } 126 | 127 | .navbar-toggler { margin-left: 0; } 128 | 129 | .navbar-toggler, 130 | .open .navbar-toggler { opacity: 1; } 131 | } 132 | 133 | ul.nav, 134 | .subnav ul.nav { 135 | overflow: visible; 136 | background-color: #fff; 137 | height: auto; 138 | margin-left: -1rem; 139 | padding-left: 1rem; 140 | padding-top: .8rem; 141 | float: left; 142 | 143 | li { 144 | display: block; 145 | height: 1.5rem; 146 | a { 147 | line-height: 1.5rem; 148 | margin-top: 0; 149 | color: #000; 150 | } 151 | } 152 | 153 | li.btn { 154 | height: 1.5rem; 155 | border: 0; 156 | a { 157 | color: $green; 158 | line-height: 1.5rem; 159 | } 160 | } 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /js/components/ListComponent.coffee: -------------------------------------------------------------------------------- 1 | clas = require 'classnames' 2 | 3 | reactify = require './Reactify.coffee' 4 | query = require './Async.coffee' 5 | 6 | util = require '../utils/util.coffee' 7 | 8 | recl = React.createClass 9 | {div,pre,span,a,ul,li,h1} = React.DOM 10 | 11 | module.exports = query { 12 | path:'t' 13 | kids: 14 | snip:'r' 15 | head:'r' 16 | meta:'j' 17 | bump:'t' 18 | name:'t' 19 | }, recl 20 | displayName: "List" 21 | 22 | render: -> 23 | k = clas 24 | list: true 25 | @props.dataType 26 | default: @props['data-source'] is 'default' 27 | @props.className 28 | kids = @renderList (util.sortKids @props.kids, @props.sortBy) 29 | unless kids.length is 0 and @props.is404? 30 | return (ul {className:k}, kids) 31 | 32 | div {className:k}, 33 | h1 {className:'red inverse block error'}, 'Error: Empty path' 34 | div {}, 35 | pre {}, @props.path 36 | span {}, 'is either empty or does not exist.' 37 | 38 | renderList: (elems)-> 39 | for elem in elems 40 | item = elem.name 41 | meta = elem.meta ? {} 42 | path = @props.path+"/"+item 43 | if meta.hide? then continue 44 | href = util.basepath path 45 | if @props.linkToFragments? then href="#"+item 46 | if @props.childIsFragment? then href=(util.basepath @props.path)+"#"+item 47 | if meta.link then href = meta.link 48 | parts = [] 49 | title = null 50 | 51 | if meta.title 52 | if @props.dataType is 'post' 53 | title = 54 | gn: 'a' 55 | ga: {href} 56 | c: [ 57 | gn: 'h1' 58 | ga: {className:'title'} 59 | c: [meta.title] 60 | ] 61 | else 62 | title = 63 | gn: 'h1' 64 | ga: {className:'title'} 65 | c: [meta.title] 66 | if not title && elem.head.c.length > 0 67 | title = elem.head 68 | if not title 69 | title = 70 | gn: 'h1' 71 | ga: {className:'title'} 72 | c: [item] 73 | 74 | unless @props.titlesOnly # date 75 | _date = meta.date 76 | if not _date or _date.length is 0 then _date = "" 77 | date = 78 | gn: 'div' 79 | ga: {className: 'date'} 80 | c: [_date] 81 | parts.push date 82 | 83 | parts.push title 84 | 85 | unless @props.titlesOnly # metadata 86 | if @props.dataType is 'post' 87 | if meta.image # image 88 | image = 89 | gn: 'a' 90 | ga: {href} 91 | c: [ 92 | gn: 'img' 93 | ga: 94 | src: meta.image 95 | ] 96 | parts.push image 97 | if @props.dataPreview # preview 98 | if not meta.preview 99 | parts.push (elem.snip.c.slice 0,2)... 100 | else 101 | if meta.preview 102 | preview = 103 | gn: 'p' 104 | ga: {className:'preview'} 105 | c: [meta.preview] 106 | else 107 | preview = elem.snip 108 | parts.push preview 109 | if @props.dataType is 'post' 110 | if meta.author # author 111 | author = 112 | gn: 'h3' 113 | ga: {className:'author'} 114 | c: [meta.author] 115 | parts.push author 116 | cont = 117 | gn: 'a' 118 | ga: {className:'continue',href} 119 | c: ['Read more'] 120 | parts.push cont 121 | linked = true 122 | 123 | node = reactify {gn:'div',c:parts} 124 | if not linked? 125 | node = (a { 126 | href 127 | className:(clas preview:@props.dataPreview?) 128 | },node) 129 | 130 | li {key:item},node 131 | -------------------------------------------------------------------------------- /js/stores/TreeStore.coffee: -------------------------------------------------------------------------------- 1 | {EventEmitter} = require('events').EventEmitter 2 | 3 | MessageDispatcher = require '../dispatcher/Dispatcher.coffee' 4 | clog = console.log.bind(console) 5 | 6 | _virt = {} 7 | _tree = {} 8 | _data = {} 9 | _curr = "" 10 | _nav = {} 11 | 12 | QUERIES = { 13 | body:'r' 14 | head:'r' 15 | snip:'r' 16 | sect:'j' 17 | meta:'j' 18 | comt:'j' 19 | plan:'j' 20 | beak:'t' 21 | spur:'t' 22 | bump:'t' 23 | } 24 | 25 | TreeStore = _.extend (new EventEmitter).setMaxListeners(50), { 26 | addChangeListener: (cb) -> @on 'change', cb 27 | 28 | removeChangeListener: (cb) -> @removeListener "change", cb 29 | 30 | emitChange: -> @emit 'change' 31 | 32 | pathToArr: (_path) -> _path.split "/" 33 | 34 | fulfill: (path,query) -> 35 | if path is "/" then path = "" 36 | @fulfillAt (@getTree path.split '/'),path,query 37 | fulfillAt: (tree,path,query)-> 38 | data = @fulfillLocal path, query 39 | have = _data[path] 40 | if have? 41 | for k,t of query when QUERIES[k] 42 | if t isnt QUERIES[k] then throw TypeError "Wrong query type: #{k}, '#{t}'" 43 | data[k] = have[k] 44 | 45 | if query.kids 46 | if have?.kids is false 47 | data.kids = {} 48 | else for k,sub of tree 49 | data.kids ?= {} 50 | data.kids[k] = @fulfillAt sub, path+"/"+k, query.kids 51 | data unless _.isEmpty data 52 | 53 | fulfillLocal: (path, query)-> 54 | data = {} 55 | if query.path then data.path = path 56 | if query.name then data.name = path.split("/").pop() 57 | if query.sein then data.sein = @getPare path 58 | if query.next then data.next = @getNext path 59 | if query.prev then data.prev = @getPrev path 60 | data 61 | 62 | setCurr: ({path}) -> _curr = path 63 | getCurr: -> _curr 64 | 65 | addVirtual: ({components}) -> _.extend _virt, components 66 | getVirtualComponents: -> _virt 67 | 68 | clearData: -> _data = {}; _tree = {} 69 | 70 | loadSein: ({path,data}) -> 71 | sein = @getPare path 72 | if sein? 73 | @loadPath {path:sein,data} 74 | 75 | loadPath: ({path,data}) -> 76 | @loadValues (@getTree (path.split '/'),true), path, data 77 | 78 | loadValues: (tree,path,data) -> 79 | old = _data[path] ? {} 80 | for k of data when QUERIES[k] 81 | old[k] = data[k] 82 | 83 | for k,v of data.kids 84 | tree[k] ?= {} 85 | _path = path 86 | if _path is "/" 87 | _path = "" 88 | @loadValues tree[k], _path+"/"+k, v 89 | 90 | if data.kids && _.isEmpty data.kids 91 | old.kids = false 92 | 93 | _data[path] = old 94 | 95 | getSiblings: (path=_curr)-> 96 | curr = path.split("/") 97 | curr.pop() 98 | if curr.length isnt 0 99 | @getTree curr 100 | else 101 | {} 102 | 103 | getTree: (_path,make=false) -> 104 | tree = _tree 105 | for sub in _path 106 | unless sub then continue # discard empty path elements 107 | if not tree[sub]? 108 | if not make then return null 109 | tree[sub] = {} 110 | tree = tree[sub] 111 | tree 112 | 113 | getPrev: (path=_curr)-> 114 | sibs = _.keys(@getSiblings path).sort() 115 | if sibs.length < 2 116 | null 117 | else 118 | par = path.split "/" 119 | key = par.pop() 120 | ind = sibs.indexOf key 121 | win = if ind-1 >= 0 then sibs[ind-1] else sibs[sibs.length-1] 122 | par.push win 123 | par.join "/" 124 | 125 | getNext: (path=_curr)-> 126 | sibs = _.keys(@getSiblings path).sort() 127 | if sibs.length < 2 128 | null 129 | else 130 | par = path.split "/" 131 | key = par.pop() 132 | ind = sibs.indexOf key 133 | win = if ind+1 < sibs.length then sibs[ind+1] else sibs[0] 134 | par.push win 135 | par.join "/" 136 | 137 | getPare: (path=_curr)-> 138 | _path = @pathToArr path 139 | if _path.length > 1 140 | _path.pop() 141 | _path = _path.join "/" 142 | if _path is "" then _path = "/" 143 | _path 144 | else 145 | null 146 | 147 | setNav: ({title,dpad,sibs,subnav}) -> 148 | nav = { 149 | title 150 | dpad 151 | sibs 152 | subnav 153 | open:(if _nav.open then _nav.open else false) 154 | } 155 | _nav = nav 156 | getNav: -> _nav 157 | toggleNav: -> _nav.open = !_nav.open 158 | closeNav: -> _nav.open = false 159 | clearNav: -> 160 | _nav = 161 | title:null 162 | dpad:null 163 | sibs:null 164 | subnav:null 165 | open:false 166 | } 167 | 168 | TreeStore.dispatchToken = MessageDispatcher.register (p) -> 169 | a = p.action 170 | 171 | if TreeStore[a.type] 172 | TreeStore[a.type] a 173 | TreeStore.emitChange() 174 | 175 | module.exports = TreeStore 176 | -------------------------------------------------------------------------------- /js/components/BodyComponent.coffee: -------------------------------------------------------------------------------- 1 | clas = require 'classnames' 2 | 3 | load = require './LoadComponent.coffee' 4 | query = require './Async.coffee' 5 | reactify = require './Reactify.coffee' 6 | 7 | TreeActions = require '../actions/TreeActions.coffee' 8 | TreeStore = require '../stores/TreeStore.coffee' 9 | 10 | Comments = require './CommentsComponent.coffee' 11 | # Plan = require './PlanComponent.coffee' 12 | 13 | util = require '../utils/util.coffee' 14 | 15 | name = (displayName,component)-> _.extend component, {displayName} 16 | recl = React.createClass 17 | rele = React.createElement 18 | {div,h1,h3,p,img,a,input} = React.DOM 19 | 20 | # named = (x,f)-> f.displayName = x; f 21 | 22 | extras = 23 | spam: name "Spam", -> 24 | if document.location.hostname isnt 'urbit.org' 25 | return (div {}) 26 | (div {className:'spam'}, 27 | (a {href:"http://urbit.org#sign-up"}, "Sign up") 28 | " for our newsletter." 29 | ) 30 | 31 | logo: name "Logo", ({color})-> 32 | if color is "white" or color is "black" # else? 33 | src = "//media.urbit.org/logo/logo-#{color}-100x100.png" 34 | (a {href:"http://urbit.org",style:{border:"none"}}, 35 | (img {src,className:"logo first"}) 36 | ) 37 | 38 | date: name "Date", ({date})-> (div {className:'date'}, date) 39 | 40 | title: name "Title", ({title})-> (h1 {className:'title'}, title) 41 | 42 | image: name "Image", ({image})-> (img {src:image}) 43 | 44 | preview: name "Preview", ({preview})-> (p {className:'preview'}, preview) 45 | 46 | author: name "Author", ({author})-> (h3 {className:'author'}, author) 47 | 48 | # plan: Plan 49 | 50 | 51 | next: query { 52 | path:'t' 53 | kids: 54 | name:'t' 55 | head:'r' 56 | meta:'j' 57 | bump:'t' 58 | }, name "Next", ({curr,meta,path,kids})-> 59 | if kids[curr]?.meta?.next 60 | keys = util.getKeys kids, meta.navsort 61 | if keys.length > 1 62 | index = keys.indexOf(curr) 63 | next = index+1 64 | if next is keys.length then next = 0 65 | next = keys[next] 66 | next = kids[next] 67 | 68 | if next 69 | return (div {className:"link-next"}, 70 | (a {href:"#{path}/#{next.name}"}, "Next: #{next.meta.title}") 71 | ) 72 | return (div {},"") 73 | 74 | comments: Comments 75 | 76 | footer: name "Footer", ({container})-> 77 | containerClas = clas 78 | footer: true 79 | container: (container is 'false') 80 | footerClas = clas 81 | 'col-md-12': (container is 'false') 82 | (div {className:containerClas,key:'footer-container'}, [ 83 | (div {className:footerClas,key:'footer-inner'}, [ 84 | "This page was made by Urbit. Feedback: " 85 | (a {href:"mailto:urbit@urbit.org"}, "urbit@urbit.org") 86 | " " 87 | (a {href:"https://twitter.com/urbit"}, "@urbit") 88 | ]) 89 | ]) 90 | 91 | module.exports = query { 92 | body:'r' 93 | name:'t' 94 | path:'t' 95 | meta:'j' 96 | sein:'t' 97 | }, (recl 98 | displayName: "Body" 99 | stateFromStore: -> {curr:TreeStore.getCurr()} 100 | getInitialState: -> @stateFromStore() 101 | _onChangeStore: -> if @isMounted() then @setState @stateFromStore() 102 | componentDidMount: -> TreeStore.addChangeListener @_onChangeStore 103 | 104 | render: -> 105 | extra = (name,props={})=> 106 | if @props.meta[name]? 107 | if (_.keys props).length is 0 108 | props[name] = @props.meta[name] 109 | props.key = name 110 | React.createElement extras[name], props 111 | 112 | innerClas = {body:true} 113 | if @props.meta.anchor isnt 'none' and @props.meta.navmode isnt 'navbar' 114 | innerClas['col-md-9'] = true 115 | innerClas['col-md-offset-3'] = true 116 | if @props.meta.navmode is 'navbar' and @props.meta.container isnt 'false' 117 | innerClas['col-md-9'] = true 118 | innerClas['col-md-offset-1'] = true 119 | innerClas = clas innerClas 120 | 121 | bodyClas = clas (@props.meta.layout?.split ',') 122 | if @props.meta.type && bodyClas.indexOf(@props.meta.type) is -1 123 | bodyClas += " #{@props.meta.type}" 124 | 125 | parts = [ 126 | extra 'spam' 127 | extra 'logo', color: @props.meta.logo 128 | # extra 'plan' 129 | reactify @props.body, 'body' 130 | extra 'next', {dataPath:@props.sein,curr:@props.name,meta:@props.meta} 131 | extra 'comments' 132 | extra 'footer', {container:@props.meta.container} 133 | ] 134 | 135 | if @props.meta.type is "post" 136 | parts.splice( 137 | 1 138 | 0 139 | extra 'date' 140 | extra 'title' 141 | extra 'image' 142 | extra 'preview' 143 | extra 'author' 144 | ) 145 | 146 | div {dataPath:@state.curr,key:@state.curr},[ 147 | div {className:innerClas,'data-path':@props.path,key:'body-inner'},[ 148 | (div { 149 | key:"body"+@props.path 150 | id: 'body' 151 | className: bodyClas 152 | }, parts 153 | ) 154 | ] 155 | ] 156 | ), (recl 157 | render: -> 158 | (div {id:'body', className:"col-md-offset-3 col-md-9"}, rele(load)) 159 | ) 160 | -------------------------------------------------------------------------------- /css/_body-urbit.scss: -------------------------------------------------------------------------------- 1 | .body .urbit { 2 | padding-bottom: 9rem; 3 | 4 | .logo { 5 | background-color: transparent; 6 | border: .3rem solid #fff; 7 | } 8 | .logo:before { 9 | line-height: 3.4rem; 10 | } 11 | 12 | h1 { 13 | color: $brand-primary; 14 | line-height: 4rem; 15 | } 16 | 17 | a.green:hover { 18 | color: $green; 19 | } 20 | 21 | .container.stack { 22 | margin-bottom: 3rem; 23 | 24 | p { 25 | font-size: 1.2rem; 26 | line-height: 2rem; 27 | } 28 | } 29 | 30 | .container.stack.six { 31 | margin-top: 12rem; 32 | margin-bottom: 6rem; 33 | } 34 | 35 | .btn.black { 36 | text-transform: none; 37 | text-decoration: none; 38 | padding: 0; 39 | letter-spacing: 0; 40 | margin: 0 1rem 1rem 0; 41 | font-size: 1.2rem; 42 | border: 0; 43 | border-bottom: 3px solid #000; 44 | } 45 | 46 | .front { 47 | padding-bottom: 4rem; 48 | 49 | h1 { padding-top: 0; } 50 | 51 | h1, 52 | h1 a { 53 | font-size: 4rem; 54 | line-height: 6rem; 55 | } 56 | } 57 | 58 | .image-fs { 59 | height: 44rem; 60 | margin-bottom: 3rem; 61 | 62 | .text-container, 63 | .image-container { 64 | position: absolute; 65 | } 66 | 67 | .text-container { 68 | display:table; 69 | height: 44rem; 70 | width: 100%; 71 | z-index: 1; 72 | 73 | .text { 74 | display:table-cell; 75 | vertical-align: middle; 76 | } 77 | 78 | .rect { 79 | margin-left: auto; 80 | margin-right: auto; 81 | 82 | h1 { 83 | padding-bottom: 1rem; 84 | text-align: left; 85 | } 86 | } 87 | 88 | @each $break,$max-width in $container-max-widths { 89 | @include media-breakpoint-up($break) { 90 | .rect { 91 | width: $max-width*(10/$grid-columns); 92 | } 93 | } 94 | } 95 | 96 | .rect { padding: 0 30px 0 30px; } 97 | 98 | // @include media-breakpoint-down(sm) { 99 | // .rect { padding: 0 30px 0 30px; } 100 | // } 101 | 102 | .rect.no-header { 103 | font-weight: 500; 104 | color: #fff; 105 | font-size: 1.4rem; 106 | 107 | p.email { font-size: 1rem; } 108 | } 109 | } 110 | 111 | .image-container { 112 | z-index: 0; 113 | height: 44rem; 114 | width: 100%; 115 | overflow: hidden; 116 | 117 | background-repeat: no-repeat; 118 | background-position: center center; 119 | -webkit-background-size: cover; 120 | -moz-background-size: cover; 121 | -o-background-size: cover; 122 | background-size: cover; 123 | 124 | 125 | // img { 126 | // position: relative; 127 | // left: 50%; 128 | // margin-left: -50%; 129 | // top: 50%; 130 | // margin-top: -50%; 131 | // min-height: 44rem; 132 | // min-width: 100%; 133 | // } 134 | } 135 | 136 | h1 { 137 | text-align: center; 138 | color: #fff; 139 | } 140 | } 141 | 142 | .image-fs.first { 143 | margin-top: -9rem; 144 | 145 | h1 { padding-bottom: 0; } 146 | } 147 | 148 | .slide { 149 | margin-bottom: 12rem; 150 | position: relative; 151 | 152 | h1 { padding-bottom: 1rem; } 153 | 154 | .pair { 155 | display: table; 156 | 157 | .text, 158 | .image { 159 | display: table-cell; 160 | vertical-align: middle; 161 | } 162 | 163 | .image.right { padding-left: 5%; } 164 | .image.left { padding-right: 5%; } 165 | 166 | .text { 167 | width: 75%; 168 | } 169 | .image { width: 20%; } 170 | .image img { width: 100%; } 171 | 172 | p:last-child { 173 | margin-bottom: 0; 174 | } 175 | } 176 | } 177 | 178 | .end { 179 | padding: 1rem; 180 | background-color: $green; 181 | color: white; 182 | text-decoration: none; 183 | font-size: 2rem; 184 | font-weight: 500; 185 | } 186 | 187 | p.last { margin-bottom: 2rem; } 188 | 189 | input.email { 190 | font-weight: 500; 191 | font-family: 'scp'; 192 | background-color: $gray-lighter; 193 | color: #000; 194 | font-size: 1rem; 195 | margin-right: 1rem; 196 | border: 3px solid $gray-lighter; 197 | padding: .375rem 1rem; 198 | margin: 0 1rem 0 0; 199 | line-height: 1.6rem; 200 | vertical-align: top; 201 | margin-bottom: .6rem; 202 | min-width: 16rem; 203 | } 204 | 205 | input.email::-moz-placeholder { 206 | color: $gray; 207 | font-weight: 400; 208 | } 209 | input.email::-webkit-input-placeholder { 210 | color: $gray; 211 | font-weight: 400; 212 | } 213 | 214 | button.submit { 215 | text-transform: none; 216 | text-decoration: none; 217 | background-color: #fff; 218 | color: #000; 219 | border: 3px solid #000; 220 | font-weight: 500; 221 | letter-spacing: 0; 222 | } 223 | 224 | .image-fs input.email { background-color: #fff } 225 | .image-fs input.email,.image-fs button.submit { border-color: #fff; } 226 | 227 | .last a { 228 | display:block; 229 | font-weight: 500; 230 | color: $gray; 231 | line-height: 2rem; 232 | } 233 | 234 | .last h2 { 235 | padding-top: 0; 236 | padding-bottom:1rem; 237 | margin-bottom: 0; 238 | } 239 | } 240 | 241 | @include media-breakpoint-down(sm) { 242 | .body .urbit { 243 | .slide .pair { 244 | &, 245 | .text, 246 | .image { 247 | display: block; 248 | width: 100%; 249 | } 250 | 251 | .image { text-align: center; } 252 | .image.left { margin-bottom: 2rem; } 253 | .image.right { margin-top: 2rem; } 254 | .image img { max-width: 12rem; } 255 | } 256 | 257 | .last .col-md-4 { margin-top: 2rem; } 258 | .last .col-md-4.col-md-offset-1 { margin-top: 0; } 259 | } 260 | } 261 | 262 | .body .talk-stream { 263 | .audience { display: none; } 264 | 265 | .speech, .message { margin-left: 0; } 266 | 267 | .length { color: $gray; } 268 | 269 | .input[contenteditable=true]:empty:before { 270 | content: 'say hello, or ask a question'; 271 | color: $gray-light; 272 | } 273 | 274 | .grams { margin: 2rem 0; } 275 | 276 | .grams .meta { 277 | .time, .path { display: none; } 278 | } 279 | 280 | .grams .gram { 281 | label { background-color: #000; } 282 | 283 | // .speech { font-size: 1.6rem; } 284 | 285 | .meta h2 { 286 | margin-bottom: 0; 287 | vertical-align: middle; 288 | } 289 | 290 | 291 | .meta label:before { font-weight: 200; } 292 | 293 | .time { display: none; } 294 | 295 | .iden, 296 | .path { 297 | max-width: 32rem; 298 | white-space: nowrap; 299 | overflow: hidden; 300 | vertical-align: middle; 301 | } 302 | } 303 | } 304 | 305 | .ship { 306 | color:#fff; 307 | padding-right: 2rem; 308 | max-width: 12rem; 309 | overflow: hidden; 310 | white-space: nowrap; 311 | } 312 | 313 | .ship span { 314 | display: none; 315 | } 316 | 317 | .ship:before { 318 | content: attr(data-alias); 319 | color: #000; 320 | } 321 | 322 | .ship:hover { 323 | color: #000; 324 | max-width: none; 325 | overflow: visible; 326 | white-space: normal; 327 | &:before { display: none; } 328 | } 329 | 330 | .ship:hover span { 331 | display: inline; 332 | } 333 | 334 | @include media-breakpoint-down(sm) { 335 | .iden, 336 | .path { max-width: 12rem; } 337 | } 338 | -------------------------------------------------------------------------------- /js/components/NavComponent.coffee: -------------------------------------------------------------------------------- 1 | clas = require 'classnames' 2 | 3 | BodyComponent = React.createFactory require './BodyComponent.coffee' 4 | query = require './Async.coffee' 5 | reactify = require './Reactify.coffee' 6 | 7 | TreeStore = require '../stores/TreeStore.coffee' 8 | TreeActions = require '../actions/TreeActions.coffee' 9 | 10 | Sibs = React.createFactory require './SibsComponent.coffee' 11 | Dpad = React.createFactory require './DpadComponent.coffee' 12 | 13 | util = require '../utils/util.coffee' 14 | 15 | recl = React.createClass 16 | rend = ReactDOM.render 17 | {div,a,ul,li,button} = React.DOM 18 | 19 | Nav = React.createFactory query { 20 | path:'t' 21 | kids: 22 | name:'t' 23 | head:'r' 24 | meta:'j' 25 | }, (recl 26 | displayName: "Links" 27 | stateFromStore: -> TreeStore.getNav() 28 | getInitialState: -> @stateFromStore() 29 | _onChangeStore: -> if @isMounted() then @setState @stateFromStore() 30 | componentDidMount: -> TreeStore.addChangeListener @_onChangeStore 31 | componentWillUnmount: -> TreeStore.removeChangeListener @_onChangeStore 32 | 33 | onClick: -> @toggleFocus() 34 | onMouseOver: -> @toggleFocus true 35 | onMouseOut: -> @toggleFocus false 36 | onTouchStart: -> @ts = Number Date.now() 37 | onTouchEnd: -> dt = @ts - Number Date.now() 38 | 39 | _home: -> 40 | @props.goTo if @props.meta.navhome then @props.meta.navhome else "/" 41 | 42 | toggleFocus: (state) -> $(ReactDOM.findDOMNode(@)).toggleClass 'focus',state 43 | toggleNav: -> TreeActions.toggleNav() 44 | closeNav: -> TreeActions.closeNav() 45 | 46 | render: -> 47 | attr = { 48 | @onMouseOver 49 | @onMouseOut 50 | @onClick 51 | @onTouchStart 52 | @onTouchEnd 53 | 'data-path':@props.dataPath 54 | } 55 | 56 | if _.keys(window).indexOf("ontouchstart") isnt -1 57 | delete attr.onMouseOver 58 | delete attr.onMouseOut 59 | 60 | linksClas = clas 61 | links: true 62 | subnav: (@props.meta.navsub?) 63 | 64 | navClas = 65 | navbar: (@props.meta.navmode is 'navbar') 66 | ctrl: true 67 | open: (@state.open is true) 68 | if @props.meta.layout 69 | for v in @props.meta.layout.split "," 70 | navClas[v.trim()] = true 71 | navClas = clas navClas 72 | iconClass = clas 73 | icon: true 74 | 'col-md-1':(@props.meta.navmode is 'navbar') 75 | 76 | attr = _.extend attr,{className:navClas,key:"nav"} 77 | 78 | title = if @state.title then @state.title else "" 79 | dpad = if @state.dpad isnt false and @props.meta?.navdpad isnt "false" 80 | (Dpad @props,"") 81 | else "" 82 | sibs = if @state.sibs isnt false and @props.meta?.navsibs isnt "false" 83 | (Sibs _.merge(_.clone(@props),{@closeNav}), "") 84 | else "" 85 | 86 | itemsClass = clas 87 | items: true 88 | 'col-md-11':(@props.meta.navmode is 'navbar') 89 | 90 | if @props.meta.navsub 91 | subprops = _.cloneDeep @props 92 | subprops.dataPath = subprops.meta.navsub 93 | delete subprops.meta.navselect 94 | subprops.className = 'subnav' 95 | sub = Sibs _.merge(subprops,{@toggleNav}), "" 96 | 97 | toggleClas = clas 98 | 'navbar-toggler':true 99 | show:@state.subnav? 100 | 101 | div attr, 102 | div {className:linksClas,key:"links"}, 103 | div {className:iconClass}, 104 | (div {className:'home',onClick:@_home}, "") 105 | (div {className:'app'}, title) 106 | dpad 107 | (button { 108 | className:toggleClas 109 | type:'button' 110 | onClick:@toggleNav}, "☰") 111 | (div {className:itemsClass}, 112 | sibs 113 | sub 114 | ) 115 | ), recl 116 | displayName: "Links_loading" 117 | _home: -> @props.goTo "/" 118 | render: -> 119 | div { 120 | className:"ctrl loading", 121 | "data-path":@props.dataPath, 122 | key:"nav-loading" 123 | }, 124 | div {className:'links'}, 125 | div {className:'icon'}, 126 | (div {className:'home',onClick:@_home}, "") 127 | ul {className:"nav"}, 128 | li {className:"nav-item selected"}, 129 | a {className:"nav-link"}, @props.curr 130 | 131 | module.exports = query { 132 | sein:'t' 133 | path:'t' 134 | name:'t' 135 | meta:'j' 136 | },(recl 137 | displayName: "Nav" 138 | stateFromStore: -> TreeStore.getNav() 139 | getInitialState: -> _.extend @stateFromStore(),{url: window.location.pathname} 140 | _onChangeStore: -> if @isMounted() then @setState @stateFromStore() 141 | 142 | componentWillUnmount: -> 143 | clearInterval @interval 144 | $('body').off 'click', 'a' 145 | TreeStore.removeChangeListener @_onChangeStore 146 | 147 | componentDidUpdate: -> 148 | @setTitle() 149 | @checkRedirect() 150 | 151 | componentDidMount: -> 152 | @setTitle() 153 | 154 | window.onpopstate = @pullPath 155 | 156 | TreeStore.addChangeListener @_onChangeStore 157 | 158 | _this = @ 159 | $('body').on 'click', 'a', (e) -> 160 | # allow cmd+click 161 | if e.shiftKey or e.ctrlKey or e.metaKey then return true 162 | href = $(@).attr('href') 163 | if href?[0] is "#" then return true 164 | if href and not /^https?:\/\//i.test(href) 165 | url = new URL @.href 166 | if not /http/.test url.protocol # mailto: bitcoin: etc. 167 | return 168 | e.preventDefault() 169 | {basepath} = urb.util 170 | if basepath("",url.pathname) isnt basepath("",document.location.pathname) 171 | document.location = @.href 172 | return 173 | if url.pathname.substr(-1) isnt "/" 174 | url.pathname += "/" 175 | _this.goTo url.pathname+url.search+url.hash 176 | @checkRedirect() 177 | 178 | checkRedirect: -> 179 | if @props.meta.redirect 180 | setTimeout (=> (@goTo @props.meta.redirect)), 0 181 | 182 | setTitle: -> 183 | title = $('#body h1').first().text() || @props.name 184 | title = @props.meta.title if @props.meta?.title 185 | path = @props.path 186 | path = "/" if path is "" 187 | document.title = "#{title} - #{path}" 188 | 189 | pullPath: -> 190 | l = document.location 191 | path = l.pathname+l.search+l.hash 192 | @setPath path,false 193 | 194 | setPath: (path,hist) -> 195 | if hist isnt false 196 | history.pushState {},"",path 197 | next = util.fragpath path.split('#')[0] 198 | if next isnt @props.path 199 | TreeActions.setCurr next 200 | 201 | reset: -> 202 | $("html,body").animate {scrollTop:0} 203 | # $('#nav').attr 'style','' 204 | # $('#nav').removeClass 'scrolling m-up' 205 | # $('#nav').addClass 'm-down m-fixed' 206 | 207 | goTo: (path) -> 208 | @reset() 209 | @setPath path 210 | 211 | render: -> 212 | return (div {}, "") if @props.meta.anchor is 'none' 213 | 214 | navClas = clas 215 | container: @props.meta.container is 'false' 216 | 217 | kidsPath = @props.sein 218 | kidsPath = @props.meta.navpath if @props.meta.navpath 219 | 220 | kids = [(Nav { 221 | curr:@props.name 222 | dataPath:kidsPath 223 | meta:@props.meta 224 | sein:@props.sein 225 | goTo:@goTo 226 | key:"nav" 227 | }, "div")] 228 | 229 | if @state.subnav 230 | kids.push reactify { 231 | gn:@state.subnav 232 | ga:{open:@state.open,toggle:TreeActions.toggleNav} 233 | c:[] 234 | }, "subnav" 235 | 236 | div {id:'head', className:navClas}, kids 237 | ) 238 | -------------------------------------------------------------------------------- /css/_nav.scss: -------------------------------------------------------------------------------- 1 | // nav 2 | 3 | 4 | @each $break,$max-width in $container-max-widths { 5 | @if $break != 'sm' { 6 | @include media-breakpoint-up($break) { 7 | .menu, 8 | .ctrl { 9 | width: $max-width*(3/$grid-columns); 10 | max-width: $max-width*(3/$grid-columns); 11 | } 12 | .ctrl.open, 13 | .ctrl:hover { 14 | max-width: $max-width*(6/$grid-columns); 15 | min-width: $max-width*(3/$grid-columns); 16 | width: auto; 17 | } 18 | 19 | .menu.depth-1 { margin-left: $max-width*(3/$grid-columns); } 20 | .menu.depth-2 { margin-left: $max-width*(6/$grid-columns); } 21 | } 22 | } 23 | } 24 | 25 | #head .loading { display: none; } 26 | 27 | @include media-breakpoint-down(xs) { 28 | #head { 29 | top: 0; 30 | z-index: 10; 31 | width: calc(100% - 1.875rem); 32 | position: absolute; 33 | transform: translateZ(0); 34 | -webkit-transform: translateZ(0); 35 | } 36 | #head.m-down, 37 | #head.m-up { position: absolute; } 38 | #head.m-down.m-fixed { position: fixed; } 39 | } 40 | @include media-breakpoint-down(xs) { .nav.container { left: 0; } } 41 | 42 | .menu .contents { padding-top: 3rem; } 43 | 44 | .ctrl { 45 | padding-left: 0; // no outside gutters. 46 | padding-right: 0; 47 | background-color: #fff; 48 | transition: max-width .2s ease-in-out; 49 | position: fixed; 50 | height: 100%; 51 | z-index:100; 52 | 53 | ul.nav { 54 | margin-top: 2rem; 55 | overflow-x: hidden; 56 | overflow-y: scroll; 57 | height: 100%; 58 | 59 | li { 60 | width: 100%; 61 | overflow: hidden; 62 | white-space: nowrap; 63 | margin-bottom: .6rem; 64 | } 65 | } 66 | 67 | ul.nav:hover { 68 | overflow-x:visible; 69 | } 70 | 71 | a.nav-link { 72 | letter-spacing: 1px; 73 | text-decoration: none; 74 | border-bottom: 2px solid; 75 | line-height: 1rem; 76 | } 77 | .selected a.nav-link { font-weight: 500; } 78 | } 79 | 80 | .ctrl.navbar { 81 | position: relative; 82 | margin-bottom: 3rem; 83 | padding: 0; 84 | background-color: transparent; 85 | width: auto; 86 | 87 | &, 88 | .items { height: 3rem; } 89 | 90 | .icon, 91 | ul.nav, 92 | li { 93 | display: inline-block; 94 | padding: 0; 95 | } 96 | 97 | ul.nav { 98 | margin-top: 0; 99 | overflow: visible; 100 | 101 | li { 102 | width: auto; 103 | display: inline-block; 104 | min-width: 2/$grid-columns*100%; 105 | } 106 | 107 | .right { float: right; } 108 | .btn { 109 | margin: 0; 110 | padding: 0; 111 | border: 0; 112 | text-transform: none; 113 | } 114 | } 115 | } 116 | 117 | .ctrl.navbar { 118 | max-width: none; 119 | } 120 | 121 | .ctrl.navbar.open, 122 | .ctrl.navbar:hover { 123 | min-width: none; 124 | } 125 | 126 | @include media-breakpoint-down(md) { 127 | ul.nav { line-height: 1.3rem; } 128 | a.nav-link { font-size: .8rem; } 129 | } 130 | 131 | @include media-breakpoint-down(sm) { 132 | .ctrl { 133 | position: relative; 134 | max-height: 3rem; 135 | max-width: 100%; 136 | padding-top: 1rem; 137 | background-color: #fff; 138 | overflow: hidden; 139 | border-bottom: 2px solid $gray-light; 140 | @include transition(max-height,.2s,ease-in-out) 141 | 142 | ul.nav { 143 | line-height: 2rem; 144 | margin: 0 0 2rem $grid-gutter-width/2; 145 | } 146 | } 147 | 148 | .ctrl.open, 149 | .ctrl:hover { 150 | width: inherit; 151 | max-width: inherit; 152 | } 153 | 154 | .ctrl.open { 155 | height: auto; 156 | max-height: 12rem; 157 | min-height: 3rem; 158 | } 159 | 160 | a.nav-link { 161 | font-size: 1rem; 162 | line-height: 1rem; 163 | text-decoration: none; 164 | border-bottom: 2px solid; 165 | } 166 | 167 | .ctrl.open { 168 | ul.nav { 169 | max-height: 9rem; 170 | overflow-y: scroll; 171 | } 172 | } 173 | } 174 | 175 | .navbar-toggler { 176 | font-size: 24px; 177 | transition: opacity .2s, transform .3s, margin-left .2s; 178 | -webkit-transition: opacity .2s, transform .3s, margin-left .2s; 179 | } 180 | 181 | .open .navbar-toggler { 182 | transform: rotate(.25turn); 183 | opacity: .2; 184 | margin-left: -.2rem; 185 | } 186 | 187 | .items { height: 80%; } 188 | 189 | @include media-breakpoint-up(md) { 190 | .ctrl .navbar-toggler { display: none; } 191 | .ctrl .navbar-toggler.show { 192 | display: block; 193 | padding: 0; 194 | } 195 | } 196 | 197 | @include media-breakpoint-down(sm) { 198 | .navbar-toggler { 199 | padding: 0; 200 | margin-left: 2rem; 201 | vertical-align: top; 202 | line-height: 18px; 203 | } 204 | .open .navbar-toggler { margin-left: 2rem; } 205 | } 206 | 207 | .icon { 208 | div, a { display: inline-block; } 209 | a { margin-right: .6rem; } 210 | 211 | .home { 212 | margin: 0 $grid-gutter-width/2 $grid-gutter-width/2 0; 213 | @include circle(24px,transparent); 214 | border: 6px solid $gray-light; 215 | cursor: pointer; 216 | } 217 | 218 | .home:hover { border-color: $gray; } 219 | 220 | .dpad { display: block; } 221 | 222 | .up { @include arrow(top,18px,#000); } 223 | .prev { @include arrow(left,18px,#000); } 224 | .next { @include arrow(right,18px,#000); margin-right: 0; } 225 | } 226 | 227 | @include media-breakpoint-down(md) { 228 | .icon { 229 | .home { @include circle(18px,transparent); border-width: 4px;} 230 | .up { @include arrow(top,12px,#000); margin-left: 1px; } 231 | .prev { @include arrow(left,12px,#000); margin-right: .6rem } 232 | .next { @include arrow(right,12px,#000); } 233 | } 234 | } 235 | 236 | @include media-breakpoint-down(sm) { 237 | .icon { 238 | margin-left: $grid-gutter-width/2; 239 | height: 2rem; 240 | 241 | .dpad { display: inline; } 242 | .home { margin-top: 0; margin-bottom: 0; } 243 | } 244 | } 245 | 246 | // doc execption 247 | // XX should go somewhere else? 248 | 249 | [data-path*='/docs'] .selected .nav-link, 250 | [data-path^='/work'] .selected .nav-link { color: $gray; } 251 | 252 | [data-path*='/docs'] .nav-link, 253 | [data-path^='/work'] .nav-link { font-weight: 500; } 254 | 255 | [data-path*='/docs'] .home, 256 | [data-path^='/work'] .home { 257 | background-color: $gray; 258 | border-color: $gray; 259 | } 260 | 261 | [data-path*='/docs'] .home:hover, 262 | [data-path^='/work'] .home:hover { 263 | background-color: $gray-light; 264 | border-color: $gray-light; 265 | opacity: .6; 266 | } 267 | 268 | [data-path*='/docs'] .home:before, 269 | [data-path^='/work'] .home:before { 270 | content: "~"; 271 | color: #fff; 272 | line-height: .8rem; 273 | font-size: 1.4rem; 274 | } 275 | 276 | @include media-breakpoint-down(md) { 277 | [data-path*='/docs'] .home:before, 278 | [data-path^='/work'] .home:before { 279 | line-height: 0.7rem; 280 | font-size: 1rem; 281 | margin-left: .06rem; 282 | } 283 | } 284 | 285 | @include media-breakpoint-down(sm) { 286 | [data-path*='/docs'] .home:before, 287 | [data-path^='/work'] .home:before { 288 | line-height: .7rem; 289 | margin-left: .05rem; 290 | font-size: 1rem; 291 | } 292 | } 293 | 294 | // [data-path^='/docs/hoon/twig'] .nav-link { font-family: 'scp'; } 295 | // [data-path^='/docs/hoon/twig'] .nav-link, 296 | // [data-path^='/docs/hoon/twig'] .ctrl .selected a.nav-link { 297 | // font-weight: 600; 298 | // } 299 | // 300 | 301 | // menu 302 | 303 | .app { 304 | vertical-align: top; 305 | font-size: 1.6rem; 306 | font-weight: 500; 307 | line-height: 1.6rem; 308 | margin-top: 0; 309 | } 310 | 311 | @include media-breakpoint-down(md) { 312 | .app { 313 | font-size: 1.3rem; 314 | line-height: 1.3rem; 315 | } 316 | } 317 | 318 | @include media-breakpoint-down(sm) { 319 | .app { 320 | margin-top: .1rem; 321 | line-height: 1rem; 322 | } 323 | } 324 | 325 | .context { 326 | margin-top: 1rem; 327 | 328 | .name { 329 | font-weight: 300; 330 | font-size: 1.2rem; 331 | font-family: 'scp'; 332 | } 333 | } 334 | 335 | @include media-breakpoint-down(md) { 336 | .context .name { 337 | font-size: 1rem; 338 | } 339 | } 340 | 341 | @include media-breakpoint-down(sm) { 342 | .context { 343 | line-height: 1rem; 344 | margin-bottom: 1rem; 345 | } 346 | } 347 | --------------------------------------------------------------------------------