Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/routes/+layout.marko
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ html lang="en"

prefetch-links

body
body spellcheck="false"
div#root
app-header
main
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
export default [
{
name: "counter",
content: `/**
* This component leverages Marko's controllable pattern!
*
* To hoist the value out when using it, you can optionally
* bind a value like this:
* \`\`\`
* <let/myCount=0>
* <counter value:=myCount/>
* \`\`\`
*/

<let/count:=input.value>
<button onClick() { count++ }>
\${count}
</button>
`,
},
{
name: "let-localstorage",
content: `/**
* This can be used just like any other \`<let>\` tag, except
* the value is synced over \`localStorage\` so it stays
* constant after a page reload!
*
* Just add a \`key=\` attribute:
* \`\`\`
* <let-localstorage/count=0 key="my-count"/>
* \`\`\`
*/

<let/internalValue=input.value>

<script>
const existingValue = localStorage.getItem(input.key);
if (existingValue) {
internalValue = JSON.parse(existingValue);
}
window.addEventListener(
"local-storage-update",
({ detail }) => {
if (detail.key === input.key) {
internalValue = JSON.parse(detail.value ?? "null");
}
},
{
signal: $signal,
},
);
</script>

<return=internalValue valueChange(newValue) {
const stringified = JSON.stringify(newValue);
localStorage.setItem(input.key, stringified);
window.dispatchEvent(
new CustomEvent("local-storage-update", {
detail: {
key: input.key,
value: stringified,
},
}),
);
}>
`,
},
{
name: "pointer-down",
content: `/**
* Uses reactive scripts and $signal to wire up document-wide
* event listeners with automatic clean-up
*
* Example usage:
* \`\`\`
* <pointer-down/clicking/>
* <if=clicking>Clicking!</if>
* \`\`\`
*/

<let/down=false>

<script>
document.addEventListener("pointerdown", () => {
down = true;
}, { signal: $signal });
document.addEventListener("pointerup", () => {
down = false;
}, { signal: $signal });
</script>

<return=down>
`,
},
];
107 changes: 77 additions & 30 deletions src/routes/playground/tags/playground/tags/editor/tags/tabs/tabs.marko
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import type { File } from "app/util/workspace";
import * as styles from "./tabs.style.module.scss";
import EXAMPLE_TEMPLATES from "./examples";
export interface Input {
files: File[];
filesChange: (files: File[]) => void;
Expand All @@ -10,53 +11,53 @@ export interface Input {

let/tabs:=input.files
let/selected:=input.selected
let/editingIndex=-1

div class=styles.tabs
div class=styles.files
for|tab, i| of=tabs
const/isSelected=selected === i
const/isEditable=i !== 0
let/editing=false valueChange=(isEditable ? undefined : () => {})
const/isEditing=editingIndex === i

div
,aria-selected=isSelected && "true"
,tabindex=0
,role="button"
,onClick() {
if (isSelected) {
editing = true;
} else {
if (isSelected && !isEditing) {
editingIndex = i;
} else if (!isEditing) {
selected = i;
}
}
,onKeyPress(e, el) {
,onKeyDown(e, el) {
if (e.target === el && (e.key === " " || e.key === "Enter")) {
if (isSelected) {
editing = true;
} else {
if (isSelected && !isEditing) {
editingIndex = i;
} else if (!isEditing) {
selected = i;
}
}
}
,class=styles.tab
if=editing
if=isEditing
let/length=(tab.path.length)
input/$input
,value=tab.path
,size=length
,onInput() {
length = $input().value.length;
,onInput(_, el) {
length = el.value.length;
}
,onBlur() {
tabs = tabs.toSpliced(i, 1, { ...tab, path: $input().value });
editing = false;
,onBlur(_, el) {
tabs = tabs.toSpliced(i, 1, { ...tab, path: el.value });
editingIndex = -1;
}
,onKeyPress(e) {
if (e.key === "Enter") {
$input().blur();
editing = false;
document.querySelector<HTMLElement>(".cm-content")?.focus();
} else if (e.key === "Escape") {
editing = false;
editingIndex = -1;
}
}
script --
Expand All @@ -67,6 +68,10 @@ div class=styles.tabs
if=isEditable
button
,title="Delete file"
,onMouseDown(e) {
// prevent button from stealing focus, so it doesn't move out underneath the mouse
e.preventDefault();
}
,onClick(e) {
e.stopPropagation();
if (selected >= i) {
Expand All @@ -77,21 +82,63 @@ div class=styles.tabs
,class=styles.close
-- ×

id/popoverId

button
,title="Add file"
,onClick() {
let filename = prompt(
"What filename (extension optional)?",
tabs.some((tab) => tab.path === "Tag.marko")
? `Tag${tabs.length}`
: "Tag",
);
,popovertarget=popoverId
,class=styles.add
,onClick(_, btn) {
const { top, right } = btn.getBoundingClientRect();
$popover().style.top = `${top}px`;
$popover().style.right = `${innerWidth - right}px`;
}
fa-icon=faPlus

if (filename) {
if (!filename.includes(".")) filename += ".marko";
selected = tabs.length;
tabs = [...tabs, { path: filename, content: "" }];
div/$popover
,id=popoverId
,class=styles.popover
,popover
,role="menu"
,onFocusOut(e) {
if (!$popover().contains(e.relatedTarget as Node)) {
$popover().hidePopover();
}
}
,class=styles.add
fa-icon=faPlus

button
,role="menuitem"
,autofocus
,onClick() {
const newIndex = tabs.length;
let path = "Tag.marko";
for (let i = 1; tabs.some((tab) => tab.path === path); i++) {
path = "Tag" + i + ".marko";
}
tabs = [
...tabs,
{
path,
content: "",
},
];
selected = newIndex;
editingIndex = newIndex;
}
,class=styles.templateButton
-- New Blank File

for|{ name, content }| of=EXAMPLE_TEMPLATES
button
,role="menuitem"
,onClick() {
selected = tabs.length;
let path = name + ".marko";
for (let i = 1; tabs.some((tab) => tab.path === path); i++) {
path = name + "-" + i + ".marko";
}
tabs = [...tabs, { path, content }];
document.querySelector<HTMLElement>(".cm-content")?.focus();
}
,class=styles.templateButton
-- ${`<${name}>`}
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,9 @@
color: var(--color-blue);
background-color: var(--color-gray-dim);
align-items: center;
border: 1px solid transparent;

&:hover {
background-color: var(--color-gray-alt-dim);
// color: var(--color-gray-dim);
}

svg {
Expand All @@ -56,3 +55,34 @@
}
}
}

.popover {
inset: unset;
flex-direction: column;
max-width: 95vw;
background-color: var(--color-gray-alt-dim);
border: 1px solid var(--color-gray-alt);
border-radius: 0.25rem;
&:popover-open {
display: flex;
}
}

.templateButton {
padding: 0.6rem 0.75rem;
background-color: transparent;
border: none;
text-align: right;

&:hover,
&:focus-visible {
background-color: var(--color-gray-dim);
&:first-child {
background-color: var(--color-blue-dim);
}
}

&:first-child {
font-weight: bold;
}
}