re-implement sky (and window wrapper) as iframes composer

This commit is contained in:
Will Hanlen 2024-07-24 00:24:15 -05:00
parent f34bfe5b76
commit 177999824e
9 changed files with 2125 additions and 54 deletions

View File

@ -1561,6 +1561,8 @@
(emit (do-card #/[p/our.bowl]/sky %make %sky ~ ~))
=. run
(emit (do-card #/[p/our.bowl]/srv/hawk %make %hawk-eyre ~ ~))
=. run
(emit (do-card #/[p/our.bowl]/srv/tree %make %tree-eyre ~ ~))
=. run
(emit (do-card #/[p/our.bowl]/srv/sky %make %sky-eyre ~ ~))
run

View File

@ -3,6 +3,8 @@
/- feather-icons
/* date-now
/* a-i-r
/* s-k-y
/* wi-nd
/* feather
/* reset
/* hawk-icon
@ -20,27 +22,12 @@
?: menu.sky m
m(a.g [[%closed ""] a.g.m])
^- manx
;a-i-r.wf.hf.relative
;s-k-y.wf.hf.relative
=our (scow %p our.bowl)
=style "opacity: var(--sky-opacity); padding: var(--sky-outer-gap);"
=id "air"
=hawks "{<open.sky>}"
=morph-retain "closed"
=hx-on-hawks-moved hawks-moved-js
=hx-on-htmx-after-request
"""
let verb = event.detail.requestConfig.verb;
let url = new URL(event.detail.xhr.responseURL);
let pams = new URLSearchParams(url.search);
if (verb === 'get' &&
url.pathname.startsWith('/neo/hawk/~') &&
!pams.has('no-save')
) \{
$('.nav-refresher').emit('refresh');
}
"""
;* p:(spin (scag open.sky hawks.sky) 0 ha-wk)
;+ menu-btn
;+ nav
==
::
++ hawks-moved-js
@ -135,15 +122,12 @@
?: =(pith /) ""
(en-tape:pith:neo pith)
=/ idt `tape`(zing (scan +:(scow %da id) (most dot (star ;~(less dot prn)))))
;div.wf.hf.fc.jc.ac.f2.s3.spinner
;wi-nd
:: =sandbox "allow-scripts allow-forms allow-same-origin"
=slot "s{<a>}"
=id "hawk-{idt}"
=hx-get "/neo/hawk{ext}?slot={<a>}&hawk-id={<id>}&no-save"
=morph-retain "slot"
=hx-trigger "load"
=hx-target "this"
=hx-swap "morph"
;+ loading.feather-icons
:: =here "/neo/hawk{ext}?slot={<a>}&hawk-id={<id>}&no-save"
=here ext
;
==
++ nav
;nav.wf.hf.p2.fc.g2.sky-nav
@ -187,16 +171,15 @@
});
$(this).emit('hawks-moved');
'''
:: =/ maximize-hawk-js
:: %- trip
:: '''
:: let air = $('a-i-r');
:: let num = parseInt(air.attr('hawks'));
:: air.attr('hawks', num+1);
:: let toMax = '#hawk-' + this.getAttribute('hawk');
:: $(toMax).attr('slot', 's-1');
:: $(this).emit('hawks-moved');
:: '''
=/ maximize-hawk-js
%- trip
'''
let air = $('a-i-r');
let num = parseInt(air.attr('hawks'));
air.attr('hawks', num+1);
let toMax = '#hawk-' + this.getAttribute('hawk');
$(toMax).attr('slot', 's-1');
'''
;div
=id "hawk-tab-{idt}"
=hx-ext "ignore:html-enc"
@ -204,11 +187,15 @@
;button
=class "loader p2 tl br1 hover grow {color}"
=hx-post "/neo/hawk/{<our.bowl>}/sky?stud=sky-diff&head=maximize&slot={<a>}"
:: XX optimistically render
::=hx-on-htmx-after-request maximize-hawk-js
=hx-on-htmx-before-send maximize-hawk-js
=hx-on-htmx-after-request
"""
$(this).emit('hawks-moved');
"""
=hawk idt
=hx-target "find .loading"
=hx-swap "outerHTML"
:: =hx-swap "outerHTML"
=hx-swap "none"
;span.loaded: {(en-tape:pith:neo pith)}
;span.loading
;+ loading.feather-icons
@ -446,14 +433,19 @@
;script: {(trip htmx-response-targets)}
;script: {(trip htmx-idiomorph)}
;script: {htmx-extensions}
:: ;link
:: =rel "stylesheet"
:: =href "https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.15.1/cdn/themes/light.css"
:: ;
:: ==
:: ;script
:: =type "module"
:: =src "https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.15.1/cdn/shoelace.js"
:: ;
:: ==
;link
=href "https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200"
=rel "stylesheet"
=href "https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.15.1/cdn/themes/light.css"
;
==
;script
=type "module"
=src "https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.15.1/cdn/shoelace.js"
;
==
;meta
@ -484,6 +476,14 @@
::==
;style: {(trip reset)}
;style: {(trip feather)}
;script
;+ ;/
"""
const sharedStyles = new CSSStyleSheet();
sharedStyles.replaceSync(`{(trip reset)}\0a{(trip feather)}`);
document.adoptedStyleSheets = [sharedStyles];
"""
==
;script
;+ ;/ %- trip
'''
@ -493,7 +493,11 @@
}
};
jQuery.fn.log = function (msg) {
if (msg) {
console.log(msg, this);
} else {
console.log(this);
}
return this;
};
jQuery.fn.emit = function (name) {
@ -505,6 +509,15 @@
);
return this;
};
jQuery.fn.poke = function (name) {
(this[0]).dispatchEvent(
new Event(
name,
{ bubbles: false, cancelable: true, composed: true }
)
);
return this;
};
function urbitTimestamp() {
let now = new Date();
let year = now.getFullYear();
@ -515,9 +528,60 @@
let sec = String(now.getSeconds()).padStart(2, '0');
return `~${year}.${month}.${date}..${hour}.${min}.${sec}`;
}
function urbitOur() {
return document.body.getAttribute('our');
}
function hawkAtSlot(slot) {
return document.querySelector(`.hawk[slot='${slot}']`);
}
function hawkDrag(event) {
let fromHawk = $(event.target).closest('.hawk').get(0);
event.dataTransfer.setData("text", fromHawk.slot);
}
function hawkDragEnter(event) {
event.preventDefault();
$('a-i-r').find('.drag-overlay').addClass('hidden');
$(event.target).closest('.hawk').children('.drag-overlay').removeClass('hidden');
}
function hawkDragAllow(event) {
event.preventDefault();
}
function hawkDragLeave(event) {
event.preventDefault();
}
function hawkDrop(event) {
event.preventDefault();
$('a-i-r').find('.drag-overlay').addClass('hidden');
let fromSlot = event.dataTransfer.getData("text");
let fromHawk = $('a-i-r').children(`.hawk[slot='${fromSlot}']`).get(0);
let toHawk = $(event.target).closest('.hawk').get(0);
let toSlot = toHawk.slot;
if (toSlot != fromSlot) {
toHawk.setAttribute('slot', fromSlot);
fromHawk.setAttribute('slot', toSlot);
const formData = new URLSearchParams();
formData.append('from', fromSlot.slice(1));
formData.append('to', toSlot.slice(1));
fetch(`/neo/hawk/${urbitOur()}/sky?stud=sky-diff&head=swap-hawks`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: formData.toString(),
})
$('a-i-r').emit('hawks-moved');
}
}
window.addEventListener('message', function(event) {
let wid = event.data.wid;
let path = event.data.path;
document.querySelector(`[wid='${wid}']`)?.setAttribute('here', path);
});
'''
==
;script: {(trip a-i-r)}
;script: {(trip s-k-y)}
;script: {(trip wi-nd)}
;script: {(trip date-now)}
;+ favicon
;+ manifest
@ -528,6 +592,7 @@
=hx-boost "true"
=hx-history "false"
=hx-replace-url "/neo/sky"
=our (scow %p our.bowl)
=style
"""
background-color: var(--b1);

View File

@ -14,7 +14,7 @@ class extends HTMLElement {
box-sizing: border-box;
}
::slotted(*) {
overflow: auto;
/* overflow: auto; */
}
:host {
display: grid;

View File

@ -704,3 +704,6 @@ button {
user-select: none;
cursor: pointer;
}
.grabber {
cursor: grab;
}

View File

@ -0,0 +1,521 @@
customElements.define('s-k-y',
class extends HTMLElement {
static get observedAttributes() {
//
return ["our", "closed", "hawks"];
}
constructor() {
//
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.adoptedStyleSheets = [sharedStyles];
this.shadowRoot.innerHTML = `
<style>
@import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200');
.mso,
.material-symbols-outlined {
font-family: 'Material Symbols Outlined';
font-weight: normal;
font-style: normal;
font-size: 1em;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
direction: ltr;
-webkit-font-feature-settings: 'liga';
-webkit-font-smoothing: antialiased;
font-variation-settings:
'FILL' 0,
'wght' 400,
'GRAD' 0,
'opsz' 24;
}
* {
box-sizing: border-box;
}
::slotted(*) {
/* overflow: auto; */
}
:host {
display: grid;
width: 100%;
height: 100%;
max-height: 100%;
overflow: hidden;
margin: 0;
grid-template-columns: 230px auto;
grid-template-rows: auto 1fr;
grid-template-areas:
"tray main"
"nav main";
}
:host(.closed) {
grid-template-columns: 50px auto;
grid-template-rows: auto 1fr;
grid-template-areas:
"tray main"
"tray main";
}
:host(.closed) #nav {
display: none;
}
#nav {
grid-area: nav;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
gap: 12px;
overflow: auto;
}
:host(.closed) #tray {
display: flex;
}
#tray {
grid-area: tray;
display: none;
padding-bottom: 15px;
}
/*
* grid display
*
*/
main {
display: grid;
grid-area: main;
overflow: hidden;
padding-left: var(--sky-inner-gap);
}
#s0, #s1, #s2, #s3 {
overflow: auto;
}
#button {
grid-area: btn;
height: fit-content;
}
#s0 {
grid-area: s0;
}
#s1 {
grid-area: s1;
}
#s2 {
grid-area: s2;
}
#s3 {
grid-area: s3;
}
main.open-0 {
grid-template-columns: 1fr;
grid-template-rows: 1fr;
grid-template-areas:
".";
}
main.open-0 #s0,
main.open-0 #s1,
main.open-0 #s2,
main.open-0 #s3 {
display: none;
}
main.open-1 {
grid-template-columns: 1fr;
grid-template-rows: 1fr;
grid-template-areas:
"s0";
}
main.open-1 #s0 {
display: block;
}
main.open-1 #s1,
main.open-1 #s2,
main.open-1 #s3 {
display: none;
}
main.open-2 {
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr;
grid-template-areas:
"s0 s1";
}
main.open-2 #s0,
main.open-2 #s1 {
display: block;
}
main.open-2 #s2,
main.open-2 #s3 {
display: none;
}
main.open-3 {
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
grid-template-areas:
"s0 s1"
"s0 s2";
}
main.open-3 #s0,
main.open-3 #s1,
main.open-3 #s2 {
display: block;
}
main.open-3 #s3 {
display: none;
}
main.open-4 {
grid-template-columns: 2fr 1fr 1fr;
grid-template-rows: 1fr 1fr;
grid-template-areas:
"s0 s1 s1"
"s0 s2 s3";
}
main.open-4 #s0,
main.open-4 #s1,
main.open-4 #s2,
main.open-4 #s3 {
display: block;
}
/*
* gaps
*
*/
main.open-1 #s0 {
padding-right: 0;
}
main.open-2 #s0,
main.open-3 #s0,
main.open-4 #s0 {
padding-right: var(--sky-inner-gap);
}
main.open-1 #s1,
main.open-2 #s1 {
padding-bottom: 0;
}
main.open-3 #s1,
main.open-4 #s1 {
padding-bottom: var(--sky-inner-gap);
}
main.open-1 #s2,
main.open-2 #s2,
main.open-3 #s2 {
padding-right: 0;
}
main.open-4 #s2 {
padding-right: var(--sky-inner-gap);
}
/*
* mobile
*
*/
@media (max-width: 900px) {
:host {
grid-template-columns: auto;
grid-template-rows: 1fr auto;
grid-template-areas:
"main"
"tray";
padding: 0 !important;
}
:host(.closed) {
grid-template-columns: auto;
grid-template-rows: 1fr auto;
grid-template-areas:
"nav"
"tray";
}
:host(.closed) main {
display: none;
}
:host(:not(.closed)) main {
display: grid;
grid-template-columns: auto;
grid-template-rows: auto;
grid-template-areas:
"s0";
}
:host(.closed) #tray,
:host(:not(.closed)) #tray {
display: flex;
flex-direction: row;
padding: 6px 6px 10px 6px;
}
#tray .hideable {
display: none;
}
#nav {
padding: 8px;
}
#nav .hideable {
display: none;
}
main {
padding: 0;
}
main #s0 {
display: block;
padding: 0;
padding-right: 0 !important;
}
main #s1 {
display: none !important;
}
main #s2 {
display: none !important;
}
main #s3 {
display: none !important;
}
:host(.closed) #nav {
display: flex;
}
:host(:not(.closed)) #nav {
display: none;
}
}
</style>
<div id="tray" class="fc g2 js af">
<button
class="br1 p1 b2 hover fc js ac grow"
onclick="this.getRootNode().host.dispatchEvent(new CustomEvent('sky-open', {bubbles:true, composed: true}))"
>
<span class="p1 s-1 bold">~</span>
</button>
<button class="p2 br1 bd1 b2 hover o7 hideable toggled">1</button>
<button class="p2 br1 bd1 b2 hover o7 hideable">2</button>
<button class="p2 br1 bd1 b2 hover o7 hideable">3</button>
<button class="p2 br1 bd1 b3 hover f3"><span class="mso">settings</span></button>
<button class="p2 br1 bd1 b3 hover f3"><span class="mso">notifications</span></button>
</div>
<nav id="nav" class="fc g8" style="padding-bottom: 15px;">
<button
class="br1 p1 b2 hover fc jc ac hideable"
onclick="this.getRootNode().host.dispatchEvent(new CustomEvent('sky-open', {bubbles:true, composed: true}))"
>
<span class="p1 s-1 bold">~</span>
</button>
<div class="fc g3 grow scroll-y">
<button
onclick="this.getRootNode().host.dispatchEvent(new CustomEvent('new-window'))"
class="wfc p2 br1 bd1 b2 hover fr g3 ac"
>
<span class="mso">add</span>
<span class="f3">new window</span>
</button>
<div id="tabs" class="fc g2"></div>
</div>
<footer class="fc g2">
<select class="fr p2 br1 bd1 b0">
<option>workspace 1</option>
<option>workspace 2</option>
<option>workspace 3</option>
</select>
<button
onclick="this.getRootNode().host.dispatchEvent(new CustomEvent('open-settings'))"
class="p2 br1 bd1 b3 hover fr g3 ac f3 hideable"
>
<span class="mso">settings</span>
settings
</button>
<button
onclick="this.getRootNode().host.dispatchEvent(new CustomEvent('open-settings'))"
class="p2 br1 bd1 b3 hover fr g3 ac f3 hideable"
>
<span class="mso">notifications</span>
notifications
</button>
</footer>
</nav>
<main>
<slot name="s0" id="s0"></slot>
<slot name="s1" id="s1"></slot>
<slot name="s2" id="s2"></slot>
<slot name="s3" id="s3"></slot>
</main>
<slot id="default" style="display: none;"></slot>
`
}
get hawks() {
return parseInt(this.getAttribute("hawks") || "0");
}
get our() {
return this.getAttribute('our');
}
get windows() {
let slots = $(this).children('wi-nd[slot]').get().toSorted((a, b) => {
let aSlot = parseFloat(a.getAttribute('slot').slice(1));
let bSlot = parseFloat(b.getAttribute('slot').slice(1));
if (aSlot > bSlot) return 1;
if (aSlot < bSlot) return -1;
return 0;
});
let noslots = $(this).children('wi-nd:not([slot])').get();
return [...slots, ...noslots];
}
qs(sel) {
return this.shadowRoot.querySelector(sel);
}
gid(id) {
return this.shadowRoot.getElementById(id);
}
connectedCallback() {
$(this).off();
$(this).on("sky-open", (e) => {
this.toggleAttribute("closed");
})
$(this).on('fix-slots', () => {
this.fixSlots();
})
$(this).on('new-window', () => {
let wind = document.createElement('wi-nd');
$(wind).attr('here', `/${this.our}/home`);
$(wind).attr('slot', `s-1`);
$(this).append(wind);
this.growFlock();
this.fixSlots();
})
$(this).on('close-window', (e) => {
let wind = $(e.target);
if (wind.attr('slot') != undefined) {
this.shrinkFlock();
}
wind.remove();
this.fixSlots();
this.renderTabs();
})
$(this).on('minimize-window', (e) => {
let wind = $(e.target);
if (wind.attr('slot') != undefined) {
wind.removeAttr('slot');
this.shrinkFlock();
}
this.fixSlots();
this.renderTabs();
})
$(this).on('maximize-window', (e) => {
let wind = $(e.target);
if (!wind.attr('slot')) {
this.growFlock();
}
wind.attr('slot', 's-1');
this.fixSlots();
this.renderTabs();
})
$(this).on('drag-start', (e) => {
$(this.windows).attr('dragging', '');
$(this.gid('nav')).addClass('active');
})
$(this).on('drag-end', (e) => {
$(this.windows).removeAttr('dragging');
})
$(this).on('here-moved', () => {
this.renderTabs();
})
$(this.gid('s0')).off();
$(this.gid('s0')).on('slotchange', (e) => {
this.renderTabs();
});
$(this.gid('s1')).off();
$(this.gid('s1')).on('slotchange', () => {
this.renderTabs();
});
$(this.gid('s2')).off();
$(this.gid('s2')).on('slotchange', () => {
this.renderTabs();
});
$(this.gid('s3')).off();
$(this.gid('s3')).on('slotchange', () => {
this.renderTabs();
});
$(this.gid('default')).off();
$(this.gid('default')).on('slotchange', () => {
this.renderTabs();
});
$(this.gid('nav')).off();
$(this.gid('nav')).on('dragover', (e) => {
e.preventDefault();
})
$(this.gid('nav')).on('drop', (e) => {
e.preventDefault();
let wid = e.originalEvent.dataTransfer.getData('text/plain');
let wind = $(`[wid='${wid}']`);
wind.poke('minimize');
});
}
attributeChangedCallback(name, oldValue, newValue) {
//
if (name === "closed") {
this.classList.toggle("closed");
} else if (name === "hawks") {
this.qs("main").className = `open-${this.hawks}`;
}
}
renderIcon(name) {
let s = document.createElement('span');
s.className = 'mso';
s.textContent = name;
return s;
}
renderTabs() {
let tabs = $(this.gid('tabs'));
tabs.children().remove();
let hawks = this.hawks;
let that = this;
$(this.windows).each(function(i) {
let wind = this;
let tab = document.createElement('div');
$(tab).addClass('b2 br1 fr ac js g1 bd1');
if (i < hawks) {
$(tab).addClass('toggled');
}
let max = document.createElement('button');
$(max).text($(wind).attr('here'));
$(max).addClass('b2 hover br1 bd0 p2 grow tl');
max.style = 'overflow: hidden; white-space: nowrap; text-overflow: ellipsis; text-align: left;';
$(max).on('click', () => {
$(wind).emit('maximize-window');
});
let min = document.createElement('button');
$(min).append(that.renderIcon('minimize'));
$(min).addClass('b2 hover br1 bd0 p1 f3');
$(min).on('click', () => {
$(wind).emit('minimize-window');
});
if (i >= hawks) {
$(min).hide();
}
let close = document.createElement('button');
$(close).append(that.renderIcon('close'));
$(close).addClass('b2 hover br1 bd0 p1 f3');
$(close).on('click', () => {
$(wind).emit('close-window');
});
$(tab).append(max);
$(tab).append(min);
$(tab).append(close);
tabs.append(tab);
})
}
fixSlots() {
let slotted = $(this.windows).filter('[slot]').get().slice(0, 3);
$(this.windows).removeAttr('slot');
slotted.forEach((s, i) => {
s.setAttribute('slot', `s${i}`);
})
}
growFlock() {
$(this).attr('hawks', Math.min(3, this.hawks + 1));
}
shrinkFlock() {
$(this).attr('hawks', Math.max(0, this.hawks - 1));
}
});

View File

@ -0,0 +1,285 @@
customElements.define('wi-nd',
class extends HTMLElement {
static get observedAttributes() {
//
return [
"wid",
"here",
"searching", // boolean. true is user is using the search bar in the header
"tabs", // currently unused. soon be space-separated list of iframe prefixes for each renderer
"current", // currently boolean. soon interpret to match prefix(tab)
"dragging",
];
}
constructor() {
//
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.adoptedStyleSheets = [sharedStyles];
this.shadowRoot.innerHTML = `
<style>
@import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200');
.mso,
.material-symbols-outlined {
font-family: 'Material Symbols Outlined';
font-weight: normal;
font-style: normal;
font-size: 1em;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
direction: ltr;
-webkit-font-feature-settings: 'liga';
-webkit-font-smoothing: antialiased;
font-variation-settings:
'FILL' 0,
'wght' 400,
'GRAD' 0,
'opsz' 24;
}
:host {
position: relative;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
overflow: hidden;
border-radius: 3px;
}
#drag-overlay {
background: blue;
opacity: 0%;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 50;
}
:host(.dragging) #drag-overlay {
opacity: 14%;
}
@media (max-width: 900px) {
#axns {
display: none;
}
}
</style>
<div hidden id="drag-overlay"></div>
<header class="b2 p1 fr ac js g2">
<button class="p-1 s-1 b3 br1 hover" id="tree-toggle"><span class="mso">sort</span></button>
<div id="breadcrumbs" class="grow fr g1 af js"></div>
<form id="searchbar" class="grow fr hidden">
<input id="input-here" class="f2 grow b0 br1 p-1 s-1"/>
</form>
<div id="axns" class="fr g2">
<button
class="p1 s-1 b2 hover br1"
onclick="this.getRootNode().host.dispatchEvent(new CustomEvent('minimize'))"
>
<span class="mso">minimize</span>
</button>
<button
class="p1 s-1 b2 hover br1"
onclick="this.getRootNode().host.dispatchEvent(new CustomEvent('close'))"
>
<span class="mso">close</span>
</button>
<div
class="p1 s-1 b2 grabber f3"
draggable="true"
id="dragger"
>
<span class="mso">drag_indicator</span>
</div>
</div>
</header>
<div id="tabs" class="fc grow">
</div>
`
}
connectedCallback() {
$(this.gid('searchbar')).off();
$(this.gid('searchbar')).on('submit', (e) => {
e.preventDefault();
this.setAttribute('here', $(this.gid('input-here')).val());
});
$(this.gid('input-here')).off();
$(this.gid('input-here')).on('focusout', (e) => {
$(this).removeAttr('searching');
});
$(this.gid('input-here')).on('blur', (e) => {
$(this).removeAttr('searching');
});
$(this.gid('tree-toggle')).off();
$(this.gid('tree-toggle')).on('click', (e) => {
this.toggleAttribute('current');
});
$(this.gid('dragger')).off();
$(this.gid('dragger')).on('dragstart', (e) => {
$(this).emit('drag-start');
e.originalEvent.dataTransfer.setData('text/plain', this.getAttribute('wid'));
})
$(this.gid('dragger')).on('dragend', () => {
$(this).emit('drag-end');
})
$(this).off();
$(this).on('close', () => {
$(this).emit('close-window');
})
$(this).on('minimize', () => {
$(this).emit('minimize-window');
})
$(this).on('dragenter', (e) => {
$(this).addClass('dragging');
})
$(this).on('dragover', (e) => {
e.preventDefault();
})
$(this).on('dragleave', (e) => {
$(this).removeClass('dragging');
})
$(this).on('drop', (e) => {
e.preventDefault();
$(this).removeClass('dragging');
$(this).emit('drag-end');
let wid = e.originalEvent.dataTransfer.getData('text/plain');
let wind = $(`[wid='${wid}']`);
let newSlot = parseInt(this.getAttribute('slot').slice(1));
let oldSlot = parseInt(wind.attr('slot')?.slice(1));
if (!isNaN(oldSlot) && oldSlot < newSlot) {
newSlot = newSlot + 0.5;
} else {
newSlot = newSlot - 0.5;
}
wind.attr('slot', `s${newSlot}`);
$(this).emit('fix-slots');
})
this.setAttribute('wid', `${Date.now()}`);
}
attributeChangedCallback(name, oldValue, newValue) {
//
if (name === "here") {
if (oldValue !== newValue) {
let tabs = $(this.gid('tabs'));
tabs.children().remove();
let hawk = this.createIframe('/neo/hawk', newValue, $(this).attr('current') != undefined);
let tree = this.createIframe('/neo/tree', newValue, $(this).attr('current') == undefined);
tabs.append(hawk);
tabs.append(tree);
this.buildBreadcrumbs();
$(this.gid('input-here')).val(newValue);
$(this).emit('here-moved');
}
} else if (name === "searching") {
if (newValue === null) {
$(this.gid('breadcrumbs')).removeClass('hidden');
$(this.gid('searchbar')).addClass('hidden');
} else {
$(this.gid('breadcrumbs')).addClass('hidden');
$(this.gid('searchbar')).removeClass('hidden');
this.gid('input-here').focus();
}
} else if (name === "current") {
if (newValue === null) {
$(this.gid('tabs')).children(`[prefix='/neo/hawk']`).show()
$(this.gid('tabs')).children(`[prefix='/neo/tree']`).hide();
$(this.gid('tree-toggle')).removeClass('toggled');
} else {
$(this.gid('tabs')).children(`[prefix='/neo/hawk']`).hide()
$(this.gid('tabs')).children(`[prefix='/neo/tree']`).show();
$(this.gid('tree-toggle')).addClass('toggled');
}
} else if (name === "dragging") {
if (newValue === null) {
$(this).removeClass('dragging');
$(this.gid('drag-overlay')).hide();
} else {
$(this.gid('drag-overlay')).show();
}
}
}
qs(sel) {
return this.shadowRoot.querySelector(sel);
}
gid(id) {
return this.shadowRoot.getElementById(id);
}
get path() {
let here = this.getAttribute("here") || "/";
return here.slice(1).split("/").filter(s => !!s.trim().length);
}
createIframe(prefix, here, hidden) {
let el = document.createElement('iframe');
el.setAttribute('prefix', prefix);
el.setAttribute('lazy', '');
el.setAttribute('src', prefix+here);
el.setAttribute('style', 'width: 100%; flex-grow: 1; border: none; background: var(--b0);');
if (hidden) {
el.hidden = true;
}
el.addEventListener('load', () => {
this.registerServiceWorker(el, prefix);
});
return el;
}
registerServiceWorker(iframe, prefix) {
const iframeWindow = iframe.contentWindow;
const iframeDoc = iframeWindow.document;
let wid = this.getAttribute('wid');
let pre = prefix.length;
const inlineScript = iframeDoc.createElement('script');
inlineScript.textContent = `
function notifySky() {
window.parent.postMessage({wid: '${wid}', path: window.location.pathname.slice(${pre})}, '*');
}
window.addEventListener('beforeunload', function (e) {
notifySky();
});
window.addEventListener('htmx:beforeHistorySave', function (e) {
notifySky();
});
window.addEventListener('htmx:beforeRequest', function (e) {
notifySky();
});
window.addEventListener('htmx:afterSwap', function (e) {
notifySky();
});
`;
iframeDoc.body.appendChild(inlineScript);
}
buildBreadcrumbs() {
let breadcrumbs = $(this.gid('breadcrumbs'));
breadcrumbs.children().remove();
//
this.path.forEach((p, i) => {
let chevron = $(document.createElement('span'));
chevron.addClass('s-2 f4 o6 fc ac jc');
chevron.text('');
breadcrumbs.append(chevron);
//
let crumb = $(document.createElement('button'));
crumb.addClass((i === 0 ? 'p-1' : 'p1') + ' b2 hover br1 s-1 f2');
crumb.text(i === 0 ? "/" : this.path[i]);
crumb.on('click', () => {
$(this).attr('here', "/"+this.path.slice(0, i+1).join("/"));
});
breadcrumbs.append(crumb);
})
let spacer = $(document.createElement('button'));
spacer.addClass('grow b2 br1 hover')
spacer.on('click', () => {
$(this).attr('searching', '');
});
breadcrumbs.append(spacer);
}
});

View File

@ -1,6 +1,7 @@
/@ htmx-type=htmx
/- feather-icons
/- serv=sky-server
/- sky-wrapper
/> htmx
/< node
/< http-request
@ -59,7 +60,7 @@
bowl
404
['content-type' 'text/html']~
~(lift hawk pit stub stub & meta)
~(lift hawk pit stub stub & meta bowl)
==
=/ here p.u.src
^- (list card:neo)
@ -77,7 +78,7 @@
bowl
200
['content-type' 'text/html']~
~(lift hawk here.bol stub stub & meta)
~(lift hawk here.bol stub stub & meta bowl)
==
=/ root=idea:neo u.reet
=/ bol *bowl:neo
@ -105,13 +106,14 @@
==
=; c
?: (~(has by pam.purl) 'no-save') c
[(sky-move-tab bol slot) c]
c
:: [(sky-move-tab bol slot) c]
%: eyre-cards
eyre-id
bowl
200
['content-type' 'text/html']~
~(lift hawk here.bol main (raw-view bol(kids q.u.src)) has-conversion meta)
~(lift hawk here.bol main (raw-view bol(kids q.u.src)) has-conversion meta bowl)
==
::
%'POST'
@ -342,7 +344,7 @@
==
==
++ hawk
|_ [here=pith main=manx raw=manx has-app=? meta=[@da @ud]]
|_ [here=pith main=manx raw=manx has-app=? meta=[@da @ud] =bowl:neo]
++ our-tape
=/ f (snag 0 here)
?@(f (trip f) (scow f))
@ -351,7 +353,7 @@
++ slot-tag
:: XX oh boy this is hacky.
:: working with slots in ssr is tough
?: =(slot 999) "s-1"
?: =(slot 999) "s0"
"s{<slot>}"
++ idt `tape`(zing (scan +:(scow %da id) (most dot (star ;~(less dot prn)))))
++ vals
@ -362,7 +364,9 @@
'}'
++ lift
^- manx
;div.hawk.fc.wf.hf.br1
%-
~(lift sky-wrapper bowl)
;div.hawk.fc.wf.hf.br1.iframe-switch
=id "hawk-{idt}"
=hawk-id (scow %da id)
=slot slot-tag
@ -371,7 +375,7 @@
=hx-target "closest .hawk"
=hx-target-x "closest .rendered"
=hx-target-404 "this"
;+ header
:: ;+ header
;div
=class "raw wf hf b0 scroll-y scroll-x {(trip ?:(has-app 'hidden' ''))}"
;+ raw
@ -538,4 +542,713 @@
==
==
--
++ feather
'''
/** feather.css
* ~2024.4.6
*
* styling resets
* and
* utility classes
*
**/
:root {
/* --font: 'Urbit Sans';
--font-mono: 'Monaco';
--mono-scale: 0.8;
--letter-spacing: 0.024em;
--line-height: 1.4;
*/
--0in: calc(0 * var(--1in));
--1in: 4px;
--font-size: calc(4 * var(--1in));
--2in: calc(2 * var(--1in));
--3in: calc(3 * var(--1in));
--4in: calc(4 * var(--1in));
--5in: calc(5 * var(--1in));
--6in: calc(6 * var(--1in));
--7in: calc(7 * var(--1in));
--8in: calc(8 * var(--1in));
--9in: calc(9 * var(--1in));
--10in: calc(10 * var(--1in));
--11in: calc(11 * var(--1in));
--12in: calc(12 * var(--1in));
--13in: calc(13 * var(--1in));
--14in: calc(14 * var(--1in));
--15in: calc(15 * var(--1in));
--16in: calc(16 * var(--1in));
--17in: calc(17 * var(--1in));
--18in: calc(18 * var(--1in));
--19in: calc(19 * var(--1in));
--20in: calc(20 * var(--1in));
--21in: calc(21 * var(--1in));
--22in: calc(22 * var(--1in));
--23in: calc(23 * var(--1in));
--24in: calc(24 * var(--1in));
--25in: calc(25 * var(--1in));
--26in: calc(26 * var(--1in));
--27in: calc(27 * var(--1in));
--28in: calc(28 * var(--1in));
--29in: calc(29 * var(--1in));
--30in: calc(30 * var(--1in));
--31in: calc(31 * var(--1in));
--32in: calc(32 * var(--1in));
--33in: calc(33 * var(--1in));
--34in: calc(34 * var(--1in));
--35in: calc(35 * var(--1in));
--36in: calc(36 * var(--1in));
--37in: calc(37 * var(--1in));
--38in: calc(38 * var(--1in));
--39in: calc(39 * var(--1in));
--40in: calc(40 * var(--1in));
/* --light-b-3: #dd5522;
--light-b-2: #ddaa33;
--light-b-1: #55dd33;
--light-b0: #dddddd;
--light-b1: #cccccc;
--light-b2: #bbbbbb;
--light-b3: #aaaaaa;
--light-b4: #999999;
--light-f-3: #993311;
--light-f-2: #aaaa22;
--light-f-1: #339911;
--light-f0: #111111;
--light-f1: #333333;
--light-f2: #444444;
--light-f3: #555555;
--light-f4: #777777;
--dark-b-3: #551111;
--dark-b-2: #555511;
--dark-b-1: #225511;
--dark-b0: #222222;
--dark-b1: #333333;
--dark-b2: #444444;
--dark-b3: #555555;
--dark-b4: #666666;
--dark-f-3: #ee7755;
--dark-f-2: #ccbb33;
--dark-f-1: #55cc33;
--dark-f0: #eeeeee;
--dark-f1: #cccccc;
--dark-f2: #bbbbbb;
--dark-f3: #aaaaaa;
--dark-f4: #888888;
*/
--b-3: var(--light-b-3);
--b-2: var(--light-b-2);
--b-1: var(--light-b-1);
--b0: var(--light-b0);
--b1: var(--light-b1);
--b2: var(--light-b2);
--b3: var(--light-b3);
--b4: var(--light-b4);
--f-3: var(--light-f-3);
--f-2: var(--light-f-2);
--f-1: var(--light-f-1);
--f0: var(--light-f0);
--f1: var(--light-f1);
--f2: var(--light-f2);
--f3: var(--light-f3);
--f4: var(--light-f4);
}
@media (prefers-color-scheme: dark) {
:root {
--b-3: var(--dark-b-3);
--b-2: var(--dark-b-2);
--b-1: var(--dark-b-1);
--b0: var(--dark-b0);
--b1: var(--dark-b1);
--b2: var(--dark-b2);
--b3: var(--dark-b3);
--b4: var(--dark-b4);
--f-3: var(--dark-f-3);
--f-2: var(--dark-f-2);
--f-1: var(--dark-f-1);
--f0: var(--dark-f0);
--f1: var(--dark-f1);
--f2: var(--dark-f2);
--f3: var(--dark-f3);
--f4: var(--dark-f4);
}
}
* {
font-size: var(--font-size);
}
/*
@media (max-width: 900px) {
:root {
--font-size: calc(1.3 * var(--font-size));
}
}
*/
body {
font-family: var(--font);
letter-spacing: var(--letter-spacing);
line-height: 1;
height: 100%;
}
button {
border: inherit;
background: inherit;
color: inherit;
}
.break {
word-break: break-word;
}
.action {
touch-action: manipulation;
}
.hidden,
.folded {
display: none !important;
}
.jc {
justify-content: center;
}
.jb {
justify-content: space-between;
}
.ja {
justify-content: space-around;
}
.js {
justify-content: start;
}
.je {
justify-content: end;
}
.js {
justify-content: start;
}
.as {
align-items: start;
}
.af {
align-items: stretch;
}
.ae {
align-items: end;
}
.ac {
align-items: center;
}
.wfc {
width: fit-content;
}
.wf {
width: 100%;
}
.mw-page {
max-width: 650px;
}
.hf {
height: 100%;
}
.hfc {
height: fit-content;
}
.mono {
font-family: var(--font-mono), monospace;
font-size: calc(1em * var(--mono-scale));
}
.italic {
font-style: italic;
}
.underline {
text-decoration: underline;
}
.bold {
font-weight: bold;
}
.strike {
text-decoration: line-through;
}
.pre {
white-space: pre;
}
.pre-line {
white-space: pre-line;
}
.tl {
text-align: left;
}
.tc {
text-align: center;
}
.tr {
text-align: right;
}
.block {
display: block;
}
.inline {
display: inline-block;
}
.fc {
display: flex;
flex-direction: column;
}
.fcr {
display: flex;
flex-direction: column-reverse;
}
.fr {
display: flex;
flex-direction: row;
}
.frw {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.basis-full {
flex-basis: 100%;
}
.basis-half {
flex-basis: 50%;
flex-shrink: 0;
}
.basis-none {
flex-basis: 0%;
flex-shrink: 1;
}
.shrink-0 {
flex-shrink: 0;
}
.relative {
position: relative;
}
.absolute {
position: absolute;
}
.fixed {
position: fixed;
}
.sticky {
position: sticky;
}
.z-2 {
z-index: -20;
}
.z-1 {
z-index: -10;
}
.z0 {
z-index: 0;
}
.z1 {
z-index: 10;
}
.z2 {
z-index: 20;
}
.grow {
flex-grow: 1;
}
.g0 {
gap: 0;
}
.g1 {
gap: 4px;
}
.g2 {
gap: 8px;
}
.g3 {
gap: 12px;
}
.g4 {
gap: 16px;
}
.g5 {
gap: 20px;
}
.g6 {
gap: 24px;
}
.g7 {
gap: 32px;
}
.g8 {
gap: 40px;
}
.p-8 {
padding: 32px 64px;
}
.p-7 {
padding: 28px 56px;
}
.p-6 {
padding: 24px 48px;
}
.p-5 {
padding: 20px 40px;
}
.p-4 {
padding: 16px 32px;
}
.p-3 {
padding: 12px 24px;
}
.p-2 {
padding: 8px 16px;
}
.p-1 {
padding: 4px 8px;
}
.p0 {
padding: 0;
}
.p1 {
padding: 4px;
}
.p2 {
padding: 8px;
}
.p3 {
padding: 12px;
}
.p4 {
padding: 16px;
}
.p5 {
padding: 24px;
}
.p6 {
padding: 30px;
}
.p7 {
padding: 34px;
}
.p8 {
padding: 38px;
}
.p-page {
padding-top: 30px;
padding-bottom: 200px;
padding-left: 12px;
padding-right: 12px;
}
.ma {
margin: auto;
}
.mt1 {
margin-top: 1rem;
}
.mt2 {
margin-top: 2rem;
}
.mt3 {
margin-top: 3rem;
}
.m0 {
margin: 0;
}
.o0 {
opacity: 0%;
}
.o1 {
opacity: 10%;
}
.o2 {
opacity: 20%;
}
.o3 {
opacity: 30%;
}
.o4 {
opacity: 40%;
}
.o5 {
opacity: 50%;
}
.o6 {
opacity: 60%;
}
.o7 {
opacity: 70%;
}
.o8 {
opacity: 80%;
}
.o9 {
opacity: 90%;
}
.o10 {
opacity: 100%;
}
.scroll-y {
overflow-y: auto;
}
.scroll-x {
overflow-x: auto;
}
.scroll-hidden {
overflow: hidden;
}
.loader {
position: relative;
}
.loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
}
.loader .loading {
opacity: 0;
transition: opacity 300ms;
}
.htmx-request .loader .loading,
.loader.htmx-request .loading {
opacity: 1;
}
.loader .loaded {
opacity: 1;
transition: opacity 300ms;
}
.htmx-request .loader .loaded,
.loader.htmx-request .loaded {
opacity: 0;
}
.f-3 {
color: var(--f-3);
}
.f-2 {
color: var(--f-2);
}
.f-1 {
color: var(--f-1);
}
.f0 {
color: var(--f0);
}
.f1 {
color: var(--f1);
}
.f2 {
color: var(--f2);
}
.f3 {
color: var(--f3);
}
.f4 {
color: var(--f4);
}
.b-3 {
background-color: var(--b-3);
}
.b-2 {
background-color: var(--b-2);
}
.b-1 {
background-color: var(--b-1);
}
.b0 {
background-color: var(--b0);
}
.b1 {
background-color: var(--b1);
}
.b2 {
background-color: var(--b2);
}
.b3 {
background-color: var(--b3);
}
.b4 {
background-color: var(--b4);
}
.s-2 {
font-size: 0.7rem;
}
.s-1 {
font-size: 0.85rem;
}
.s0 {
font-size: 1rem;
}
.s1 {
font-size: 1.15rem;
}
.s2 {
font-size: 1.3rem;
}
.s3 {
font-size: 1.45rem;
}
.s4 {
font-size: 1.6rem;
}
.s5 {
font-size: 2rem;
}
.s6 {
font-size: 2.4rem;
}
.bd0 {
border: none;
}
.bd1 {
border: 0.8px solid var(--b3);
}
.bd2 {
border: 0.8px solid var(--f3);
}
.bd3 {
border: 2px solid var(--f1);
}
/* deprecated */
.border {
border: 0.8px solid var(--f3);
}
.br1 {
border-radius: 3px;
}
.br2 {
border-radius: 6px;
}
.br3 {
border-radius: 12px;
}
.hover:hover {
filter: invert(20%);
}
.toggled {
filter: invert(100%);
}
.hover.toggled:hover {
filter: invert(100%);
}
.active,
.selected {
filter: invert(10%);
}
.hover.active:hover,
.hover.selected:hover {
filter: invert(25%);
}
.numbered > *:before {
content: counter(line);
display: inline-block;
/* border-right: 1px solid var(--f3); */
/* padding: 0 .5em; */
/* margin-right: .5em; */
color: var(--f3);
/* min-width: 34px; */
text-align: right;
}
.numbered > * {
display: block;
counter-increment: line;
}
.prose h1 {
font-size: 1.45rem;
margin: 1rem 0;
}
.prose h2 {
font-size: 1.3rem;
margin: 1rem 0;
}
.prose h3 {
font-size: 1.15rem;
margin: 1rem 0;
}
.prose h1, .prose h2, .prose h3 {
font-weight: bold;
}
.prose p {
margin-bottom: 1rem;
line-height: var(--line-height);
}
.prose img {
max-width: 100%;
display: block;
max-height: 350px;
}
.prose details {
margin-bottom: 1rem;
}
.prose a {
text-decoration: underline;
}
.prose blockquote {
margin-left: 10px;
border-left: 2px solid var(--b3);
padding: 4px;
padding-left: 12px;
color: var(--f2);
}
.prose pre {
font-family: var(--font-mono);
font-size: calc(1em * var(--mono-scale));
overflow-x: auto;
width: 100%;
display: block;
padding: 8px;
}
.prose code {
font-family: var(--font-mono);
font-size: calc(1em * var(--mono-scale));
color: var(--f2);
}
.prose ul,
.prose ol {
padding-left: 29px;
line-height: calc(calc(1 + var(--line-height)) / 2);
margin-bottom: 0.6rem;
}
.prose ul p,
.prose ol p {
margin-bottom: 0;
line-height: var(--line-height);
}
.prose ul ul,
.prose ol ul,
.prose ul ol,
.prose ol ol {
margin-bottom: 0;
}
.prose summary {
user-select: none;
cursor: pointer;
}
'''
--

View File

@ -0,0 +1,57 @@
/@ eyre-reqs
/- serv=server
^- kook:neo
|%
++ state pro/%sig
++ poke (sy %eyre-task ~)
++ kids
:+ ~ %y
%- ~(gas by *lads:neo)
~
++ deps
%- ~(gas by *band:neo)
~
::
++ form
|_ [=bowl:neo =aeon:neo =pail:neo]
++ poke
|= [=stud:neo vax=vase]
^- (quip card:neo pail:neo)
?+ stud ~|(bad-stud/stud !!)
%eyre-task
=+ !<(=task:eyre:neo vax)
=/ [eyre-id=@ta req=inbound-request:eyre] task
:_ sig/!>(~)
=/ doc
%- en-xml:html
;html
;body
;style
;+ ;/ %- trip
'''
body {
color: red;
}
'''
==
;h1: tree stub: {(trip url.request.req)}
==
==
=/ head=sign:eyre:neo [eyre-id %head [200 ~]]
=/ data=sign:eyre:neo [eyre-id %data `(as-octt:mimes:html doc)]
=/ done=sign:eyre:neo [eyre-id %done ~]
:~ [#/[p/our.bowl]/$/eyre %poke eyre-sign/!>(head)]
[#/[p/our.bowl]/$/eyre %poke eyre-sign/!>(data)]
[#/[p/our.bowl]/$/eyre %poke eyre-sign/!>(done)]
==
==
++ init
|= pal=(unit pail:neo)
:_ sig/!>(~)
=/ =pith:neo #/[p/our.bowl]/$/eyre
=/ =binding:eyre [~ ~[%neo %tree]]
=/ =req:eyre:neo [%connect binding here.bowl]
:~ [pith %poke eyre-req/!>(req)]
==
--
--

View File

@ -0,0 +1,425 @@
/- feather-icons
/* date-now
/* a-i-r
/* feather
/* reset
/* hawk-icon
/* jquery
/* htmx-js
/* htmx-response-targets
/* htmx-idiomorph
|_ =bowl:neo
++ hawks-moved-js
:: js to run whenever the order or number of hawks changed.
:: it will:
:: - trigger a refresh of the nav
:: - loop through any slotted hawks and reslot them starting from 0
%- trip
'''
let air = $(this);
let num = parseInt(air.attr('hawks'));
let hawks = air.children('[slot]').filter('.hawk').get();
hawks.sort((a, b) => {
return (a.getAttribute('slot') < b.getAttribute('slot')) ? -1 : 1;
}).forEach((h, i) => {
h.setAttribute('slot', `s${i}`);
});
air.find('.nav-refresher').emit('refresh');
'''
++ map-to-css-tape
|= m=(map @t @t)
^- tape
%- zing
%+ turn ~(tap by m)
|= [key=@t val=@t]
"""
--{(trip key)}: {(trip val)};
"""
++ eye
;div#eye.fixed.hidden
=style "bottom: 30px; left: 30px;"
=morph-no-swap ""
;script
;+ ;/ %- trip
'''
function handleKey(e) {
let focused = document.activeElement;
let textarea = ['TEXTAREA'].includes(focused.nodeName)
let textinput = ['text', 'number', 'email', 'password'].includes(focused.type);
if (textarea || textinput) {
if (e.key === 'Escape') {
document.activeElement.blur();
}
return;
}
else if (e.key === 'Escape') {
closeEye();
document.activeElement.blur();
}
else if (e.key === ' ') {
e.preventDefault();
if (window.eye.open) {
closeEye();
} else {
openEye();
}
} else if (e.key === 'Escape') {
e.preventDefault();
document.activeElement.blur();
} else if (!e.ctrlKey && !e.metaKey && !e.altKey) {
if (window.eye?.open) {
e.preventDefault();
let area = window.eye?.spots?.filter(s => s[0][0] === e.key);
if (area.length === 1) {
let btn = area[0][1];
btn.click();
btn.focus();
closeEye();
}
else if (!!area.length) {
let news = area.map(c => (c[0].length < 2) ? c : [c[0].slice(1), c[1]]);
window.eye.spots = news;
closeEye(true);
openEye();
} else {
closeEye();
}
}
}
}
function openEye() {
window.eye.open = true;
document.getElementById('eye').classList.remove('hidden');
buildGazeSpots();
}
function closeEye(keep) {
window.eye.open = false;
if (!keep) {
window.eye.spots = null;
}
document.getElementById('eye').classList.add('hidden');
document.querySelectorAll('.gaze').forEach(g => g.remove());
}
function buildGazeSpots() {
let buttons = window.eye?.spots?.map(s => s[1]) ||
document.querySelectorAll(
'a, button, summary, [role="button"], input, textarea, .clickable'
);
let chars = ['a', 's', 'd', 'f', 'k', 'm', 'n', 'r', 't', 'y', 'u', 'i', 'c', 'v', 'b'];
buttons.forEach((b, i) => {
let d = b.getBoundingClientRect();
if (d.right > 0 && d.right > 0) {
let t = document.createElement('div');
var lent = Math.floor((i / chars.length) + 1);
var word = ''
while (lent > 0) {
let ch = chars[i % chars.length];
word = `${word}${ch}`;
lent = lent - 1;
}
t.textContent = word.slice(-1);
t.className = 'b-1 br2 p1 s0 bold fixed gaze z2'
t.style = `top: ${Math.max(0, d.top - 10)}px; left: ${Math.max(0, d.left - 10)}px;`
document.getElementById('eye')?.parentNode.appendChild(t);
window.eye.spots = [[word, b], ...(window.eye?.spots || [])]
}
})
}
window.addEventListener('keydown', handleKey);
'''
==
==
++ icon-url
^~
%- trip
%^ cat
3
'data:image/png;base64,'
%- ~(en base64:mimes:html & |)
(as-octs:mimes:html hawk-icon)
++ favicon
^~
=; m m(a.g [[%href icon-url] a.g.m])
^- manx
;link
=rel "icon"
=type "image/png"
;
==
++ manifest-url
^~
%- trip
%^ cat
3
'data:application/json;utf-8,'
%- en:json:html
%- pairs:enjs:format
:~
['name' s+'sky']
['description' s+'an urbit namespace viewer']
['display' s+'standalone']
['background_color' s+'black']
:+ 'icons' %a
:~
%- pairs:enjs:format
:~
['src' s+(crip icon-url)]
['sizes' s+'196x196']
['type' s+'image/png']
==
==
==
++ manifest
^~
=; m m(a.g [[%href manifest-url] a.g.m])
^- manx
;link
=rel "manifest"
;
==
++ htmx-extensions
:: htmx extension which encodes the request
:: as the serialized HTML of the calling element
::
:: XX usage of this should be optional.
:: requests should default to form-encoded.
%- trip
'''
htmx.defineExtension('html-enc', {
onEvent: function (name, evt) {
if (name === "htmx:configRequest") {
evt.detail.headers['Content-Type'] = "text/html";
}
},
encodeParameters : function(xhr, parameters, elt) {
xhr.overrideMimeType('text/html');
let xmls = new XMLSerializer();
return (xmls.serializeToString(elt));
}
});
Idiomorph.defaults.ignoreActive = true;
Idiomorph.defaults.callbacks.beforeAttributeUpdated = (name, node, type) => {
if (node.hasAttribute('morph-retain')) {
let ribs = node.getAttribute('morph-retain').split(',').map(t => t.trim());
if (ribs.includes(name)) {
return false;
}
}
}
Idiomorph.defaults.callbacks.beforeNodeMorphed = (oldNode, newNode) => {
if (oldNode?.nodeName !== "#text") {
if (oldNode.hasAttribute('morph-no-swap') && oldNode.id === newNode.id) {
return false;
}
else if (
newNode.hasAttribute('morph-if-class') &&
!oldNode.classList.contains(newNode.getAttribute('morph-if-class'))
) {
return false;
}
}
}
'''
::
++ lift
|= in=manx
^- manx
;html
;head
;meta(charset "UTF-8");
;title: hawk
;script: {(trip jquery)}
;script: {(trip htmx-js)}
;script: {(trip htmx-response-targets)}
;script: {(trip htmx-idiomorph)}
;script: {htmx-extensions}
:: ;link
:: =rel "stylesheet"
:: =href "https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.15.1/cdn/themes/light.css"
:: ;
:: ==
:: ;script
:: =type "module"
:: =src "https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.15.1/cdn/shoelace.js"
:: ;
:: ==
;meta
=name "viewport"
=content
"""
width=device-width,
initial-scale=1.0,
maximum-scale=1.0
"""
;
==
;meta
=name "htmx-config"
=content (trip '{"ignoreTitle":"true"}')
;
==
::;style
:: ;+ ;/ %- trip
:: '''
:: @font-face {
:: font-family: 'Urbit Sans';
:: src: url("https://media.urbit.org/fonts/UrbitSans/UrbitSansVFWeb-Regular.woff2") format("woff2");
:: font-style: normal;
:: font-weight: 100 700;
:: }
:: '''
::==
;style: {(trip reset)}
;style: {(trip feather)}
;script
;+ ;/ %- trip
'''
window.log = function() {
if (this.console) {
console.log(Array.prototype.slice.call(arguments));
}
};
jQuery.fn.log = function (msg) {
console.log(this);
return this;
};
jQuery.fn.emit = function (name) {
(this[0]).dispatchEvent(
new Event(
name,
{ bubbles: true, cancelable: true, composed: true }
)
);
return this;
};
function urbitTimestamp() {
let now = new Date();
let year = now.getFullYear();
let month = now.getMonth() + 1;
let date = now.getDate();
let hour = String(now.getHours()).padStart(2, '0');
let min = String(now.getMinutes()).padStart(2, '0');
let sec = String(now.getSeconds()).padStart(2, '0');
return `~${year}.${month}.${date}..${hour}.${min}.${sec}`;
}
function urbitOur() {
return document.body.getAttribute('our');
}
function hawkAtSlot(slot) {
return document.querySelector(`.hawk[slot='${slot}']`);
}
function hawkDrag(event) {
let fromHawk = $(event.target).closest('.hawk').get(0);
event.dataTransfer.setData("text", fromHawk.slot);
}
function hawkDragEnter(event) {
event.preventDefault();
$('a-i-r').find('.drag-overlay').addClass('hidden');
$(event.target).closest('.hawk').children('.drag-overlay').removeClass('hidden');
}
function hawkDragAllow(event) {
event.preventDefault();
}
function hawkDragLeave(event) {
event.preventDefault();
}
function hawkDrop(event) {
event.preventDefault();
$('a-i-r').find('.drag-overlay').addClass('hidden');
let fromSlot = event.dataTransfer.getData("text");
let fromHawk = $('a-i-r').children(`.hawk[slot='${fromSlot}']`).get(0);
let toHawk = $(event.target).closest('.hawk').get(0);
let toSlot = toHawk.slot;
if (toSlot != fromSlot) {
toHawk.setAttribute('slot', fromSlot);
fromHawk.setAttribute('slot', toSlot);
const formData = new URLSearchParams();
formData.append('from', fromSlot.slice(1));
formData.append('to', toSlot.slice(1));
fetch(`/neo/hawk/${urbitOur()}/sky?stud=sky-diff&head=swap-hawks`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: formData.toString(),
})
$('a-i-r').emit('hawks-moved');
}
}
'''
==
;script: {(trip a-i-r)}
;script: {(trip date-now)}
;+ favicon
;+ manifest
==
;body
=hx-ext "html-enc,response-targets,morph"
=hx-swap "outerHTML"
=hx-boost "true"
=hx-history "false"
=our (scow %p our.bowl)
=style
"""
background-color: var(--b1);
background-image: var(--sky-bg-url);
background-size: var(--sky-bg-size);
background-repeat: var(--sky-bg-repeat);
"""
;+ in
;+ eye
;+ stub-styling
==
==
++ stub-styling
;style
;+ ;/ %- trip
'''
html {
--line-height: 1.4;
--sky-outer-gap: 8px;
--dark-b-1: #225511;
--dark-b3: #555555;
--light-b-1: #55dd33;
--letter-spacing: 0.024em;
--dark-f-2: #ccbb33;
--light-b1: #cccccc;
--light-b0: #dddddd;
--light-b3: #aaaaaa;
--light-b-3: #dd5522;
--dark-b4: #666666;
--light-f2: #444444;
--dark-b-3: #551111;
--dark-f4: #888888;
--dark-f-3: #ee7755;
--light-b-2: #ddaa33;
--sky-opacity: 0.88;
--dark-b0: #222222;
--light-b2: #bbbbbb;
--light-f-3: #993311;
--light-f1: #333333;
--dark-f-1: #55cc33;
--dark-b1: #333333;
--dark-f1: #cccccc;
--light-f3: #555555;
--light-b4: #999999;
--sky-bg-url: ;
--dark-f3: #aaaaaa;
--light-f0: #111111;
--sky-bg-size: contain;
--light-f4: #777777;
--1in: 4px;
--font-mono: monospace;
--dark-f0: #eeeeee;
--sky-inner-gap: 8px;
--dark-b2: #444444;
--dark-b-2: #555511;
--font: Urbit Sans, sans-serif;
--mono-scale: 0.8;
--light-f-2: #aaaa22;
--dark-f2: #bbbbbb;
--light-f-1: #339911;
}
'''
==
--