Skip to content

Commit

Permalink
Merge pull request #1851 from NullVoxPopuli/share-menu
Browse files Browse the repository at this point in the history
Share menu
  • Loading branch information
NullVoxPopuli authored Nov 3, 2024
2 parents afcd05d + 34e2dd5 commit 741ea16
Show file tree
Hide file tree
Showing 8 changed files with 370 additions and 4 deletions.
2 changes: 2 additions & 0 deletions apps/repl/.template-lintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ module.exports = {
'no-implicit-this': 'off',
// false negatives due to being defined in js-scope
'no-curly-component-invocation': 'off',
// Don't care
'no-forbidden-elements': 'off',
},
},
],
Expand Down
2 changes: 2 additions & 0 deletions apps/repl/app/components/limber/header.gts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { FormatButtons } from 'limber/components/limber/layout/controls/format-b
import { ExternalLink } from 'limber-ui';

import DemoSelect from './demo-select';
import { Share } from './share';

<template>
<header
Expand All @@ -27,6 +28,7 @@ import DemoSelect from './demo-select';
{{#if (notInIframe)}}
<FormatButtons />
{{/if}}
<Share />
</div>

<nav class="text-white flex gap-2 items-baseline">
Expand Down
178 changes: 178 additions & 0 deletions apps/repl/app/components/limber/share.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/* focus */
button[data-share-button],
dialog.preem input {
--tw-ring-color: var(--ember-brand);

&:focus-visible, &:focus {
outline: 2px solid transparent;
outsilen-offset: 2px;
border-color: transparent;
}
&:focus {
--tw-ring-offset-shadow:
var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow:
var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow:
var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
}

dialog.preem {
.error {
border: 2px solid #622;
border-radius: 0.25rem;
padding: 0.25rem;
background: #fee;
}
}


button[data-share-button] {
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
transition-property: all;
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
padding: 0.25rem 0.5rem;
/**
* TODO: can this be nested automatically?
*/
border-radius: 0.25rem;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
}


.preem__tip {
--tw-bg-opacity: 1;
padding: 1rem;
background: white;
background: rgb(240 249 255 / var(--tw-bg-opacity));
color: rgb(50, 60, 100);
position: relative;
border-radius: 0.25rem;

.preem__tip__bulb {
position: absolute;
left: 1rem;
top: 1rem;
text-shadow: 1px 2px 1px rgba(0,0,0,0.2);
font-size: 1.5rem;
}
.preem__tip__text {
padding-left: 2rem;
padding-right: 1.5rem;
}
}
dialog.preem {
border-radius: 0.25rem;
animation: var(--animation-slide-in-up), var(--animation-fade-in);
animation-timing-function: var(--ease-out-5);
animation-duration: 0.2s;
}
dialog.preem::backdrop {
backdrop-filter: blur(1px);
}
dialog.preem header {
display: flex;
justify-content: space-between;
padding: 1rem;
align-items: center;
border-bottom: 1px solid #333;
}
dialog.preem h2 {
margin: 0 !important;
}

dialog.preem main {
padding: 2rem;
padding-bottom: 1rem;
max-width: 500px;
display: grid;
gap: 1rem;

.field {
display: grid;
gap: 0.5rem;
input {
width: 100%;
}
.field-input {
display: flex;
gap: 0.5rem;
}
}
input {
padding: 0.25rem 0.5rem;
border: 1px solid #333;
border-radius: 0.25rem;
color: #444;
}
}

dialog.preem form {
}
dialog.preem .inline-mini-form {
display: grid;
align-items: end;
grid-auto-flow: column;
grid-template-columns: 1fr min-content;

[type="submit"] {
margin-left: 0.5rem;
}
}

dialog.preem footer,
dialog.preem form {
button {
color: white;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
background: var(--code-bg);
border: 1px solid var(--horizon-border);

&.cancel {
color: black;
background: #eee;
border-color: #aaa;
}
}

button:focus {
outline: 2px solid transparent;
outline-offset: 2px;
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width)
var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width))
var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}

button:focus-visible {
outline: 2px solid transparent;
outline-offset: 2px;
}

button:hover {
opacity: 0.9;
}
}

dialog.preem footer {
padding: 1rem 2rem;

.right {
display: grid;
justify-content: end;
}

.buttons {
display: flex;
gap: 1rem;
}
}

179 changes: 179 additions & 0 deletions apps/repl/app/components/limber/share.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import './share.css';

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { array, fn } from '@ember/helper';
import { on } from '@ember/modifier';
import { service } from '@ember/service';

import FaIcon from '@fortawesome/ember-fontawesome/components/fa-icon';
// @ts-expect-error womp types
import { focusTrap } from 'ember-focus-trap';
import { Modal } from 'ember-primitives/components/dialog';

import { shortenUrl } from 'limber/utils/editor-text';

import { FlatButton } from './help';

import type { TOC } from '@ember/component/template-only';
import type RouterService from '@ember/routing/router-service';

const { Boolean } = globalThis;

export class Share extends Component {
<template>
<Modal as |m|>
<button data-share-button type="button" {{on "click" m.open}}>
Share
<FaIcon @icon="share-from-square" @prefix="fas" />
</button>
<m.Dialog class="preem" {{focusTrap isActive=m.isOpen}}>
<header><h2>Share</h2>

<FlatButton {{on "click" m.close}} aria-label="close this share modal">
<FaIcon @size="xs" @icon="xmark" class="aspect-square" />
</FlatButton>
</header>
<form {{on "submit" this.handleSubmit}}>
<main>
{{#if this.error}}
<div class="error">{{this.error}}</div>
{{/if}}
<div class="inline-mini-form">
<ReadonlyField
@label="shortened URL"
@value={{this.shortUrl}}
@copyable={{Boolean this.shortUrl}}
placeholder="Click 'Create'"
/>

{{#unless this.shortUrl}}
<button type="submit">Create</button>
{{/unless}}
</div>
<Tip>
<KeyCombo @keys={{array "Ctrl" "S"}} @mac={{array "Command" "S"}} />
will copy a shortened URL to your clipboard.</Tip>
</main>

<footer>
<div class="right">
<div class="buttons">
<button type="button" class="cancel" {{on "click" m.close}}>Close</button>
{{#unless this.shortUrl}}
<button type="submit">Create Link</button>
{{/unless}}
</div>
</div>
</footer>
</form>
</m.Dialog>
</Modal>
</template>

@service declare router: RouterService;

@tracked shortUrl: string | undefined;
@tracked error: string | undefined;

toClipboard = async () => {
let url = location.origin + this.router.currentURL;

try {
url = await shortenUrl(url);
} catch (e) {
console.error(`Could not shorten the URL`);
console.error(e);
throw e;
}

await navigator.clipboard.writeText(url);
};

handleSubmit = async (event: SubmitEvent) => {
event.preventDefault();
this.error = undefined;

let href = window.location.href;

if (!href.includes('glimdown.com')) {
if (href.includes('localhost')) {
this.error = "You're on localhost, silly, here is a fake error with fake URL";
this.shortUrl = 'https://share.glimdown.com/something';
} else {
this.error = 'This is only supported on glimdown.com';
}

return;
}

try {
await this.toClipboard();
} catch {
// TODO: Toast message
}
};
}

async function writeToClipboard(text: string) {
await navigator.clipboard.writeText(text);
}

// "with copy" / @copyable={{true}}?
const ReadonlyField: TOC<{
Element: HTMLInputElement;
Args: {
label: string;
value: string | undefined;
copyable?: boolean | undefined;
};
}> = <template>
<span class="field">
<label for="share-copy">{{@label}}</label>
<span class="field-input">
<input value={{@value}} name="share-copy" readonly ...attributes />
{{#if @copyable}}
<button type="button" {{on "click" (fn writeToClipboard @value)}}>Copy</button>
{{/if}}
</span>
</span>
</template>;

const isLast = (collection: unknown[], index: number) => index === collection.length - 1;
const isNotLast = (collection: unknown[], index: number) => !isLast(collection, index);
const isMac = navigator.userAgent.indexOf('Mac OS') >= 0;
const getKeys = (keys: string[], mac: string[]) => (isMac ? mac ?? keys : keys);

const KeyCombo: TOC<{
Args: {
keys: string[];
mac: string[];
};
}> = <template>
<span class="preem__key-combination">
{{#let (getKeys @keys @mac) as |keys|}}
{{#each keys as |key i|}}
<Key>{{key}}</Key>
{{#if (isNotLast @keys i)}}
<span class="preem__key-combination__separator">+</span>
{{/if}}
{{/each}}
{{/let}}
</span>
</template>;

const Key: TOC<{
Blocks: { default?: [] };
}> = <template>
<span class="preem-key">{{yield}}</span>
</template>;

const Tip: TOC<{
Blocks: {
default: [];
};
}> = <template>
<div class="preem__tip"><span class="preem__tip__bulb">💡</span><p
class="preem__tip__text"
>{{yield}}</p></div>
</template>;
2 changes: 1 addition & 1 deletion apps/repl/app/utils/editor-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const DEBOUNCE_MS = 300;
const queueWaiter = buildWaiter('FileURIComponent::queue');
const queueTokens: unknown[] = [];

async function shortenUrl(url: string) {
export async function shortenUrl(url: string) {
let response = await fetch(`https://api.nvp.gg/v1/links`, {
method: 'POST',
headers: {
Expand Down
Loading

0 comments on commit 741ea16

Please sign in to comment.