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
8 changes: 7 additions & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@sveltejs/vite-plugin-svelte": "^7.2.0",
"@tailwindcss/vite": "^4.3.3",
"@types/node": "^26.1.2",
"@types/sortablejs": "^1.15.9",
"@vitest/coverage-v8": "4.1.10",
"file-type": "^22.0.1",
"oxlint": "^1.77.0",
Expand All @@ -46,6 +47,7 @@
"@tiptap/starter-kit": "^3.29.2",
"idb": "^8.0.3",
"modern-screenshot": "^4.7.0",
"sortablejs": "^1.15.7",
"svelte-awesome-color-picker": "^4.1.3",
"svelte-highlight": "^7.16.2",
"tippy.js": "^6.3.7",
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/Modal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@

export async function open() {
opened = true;
await tick();
if (onOpen) {
onOpen();
}
await tick();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IDK who did this change (might have been me for all i know) but is there a reason this is being rearranged?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not to my knowledge, not sure why. but i think it all still works now?

}

export async function close() {
Expand Down
239 changes: 178 additions & 61 deletions src/lib/components/modals/ColorGradientModal.svelte
Original file line number Diff line number Diff line change
@@ -1,13 +1,38 @@
<script lang="ts">
import Modal from "$lib/components/Modal.svelte";
import type { Editor } from "@tiptap/core";
import Sortable from "sortablejs";
import ColorPicker from "svelte-awesome-color-picker";
import { generateGradient } from "typescript-color-gradient";

import { tick } from "svelte";
import IconBack from "~icons/tabler/arrow-back-up";
import IconCircle from "~icons/tabler/circle-filled";
import IconRecent from "~icons/tabler/clock-hour-4";
import IconHandle from "~icons/tabler/grip-vertical";
import IconCustom from "~icons/tabler/plus";
import IconDelete from "~icons/tabler/trash";

let { gradientDialog = $bindable(), gradientSteps = $bindable(), editor } = $props();
let recentsPageOpen: boolean = $state(false);
let recentGradients: Array<string[]> = $state([]);
let gradientSteps: { id: number; color: string }[] = $state([{ id: 0, color: "#ffffff"}]);

interface Props {
gradientDialog: Modal;
editor: Editor | undefined;
}

function openRecentsPage() {
if (localStorage.getItem("recent_gradients")) {
recentGradients = JSON.parse(localStorage.getItem("recent_gradients")!);
} else {
recentGradients = [];
localStorage.setItem("recent_gradients", "[]");
}
recentsPageOpen = true;
}

let { gradientDialog = $bindable(), editor }: Props = $props();

function applyGradient(editor: Editor, gradientColors: string[]) {
const { from, to } = editor.state.selection;
Expand Down Expand Up @@ -54,71 +79,163 @@
chain.focus().setTextSelection({ from, to });

chain.run();

// Add to recents if necessary
let gradientHexes = gradientSteps.map((step) => step.color)

const alreadyAppeared = recentGradients.some(elem =>{
return JSON.stringify(gradientHexes) === JSON.stringify(elem);
});

if (alreadyAppeared) {
recentGradients = recentGradients.filter((grad) => JSON.stringify(grad) !== JSON.stringify(gradientHexes));
}

recentGradients.push(gradientHexes);

if (recentGradients.length > 10) {
recentGradients.shift();
}

localStorage.setItem("recent_gradients", JSON.stringify(recentGradients));
}

function opened() {
recentsPageOpen = false;

// Load recent gradients
if (localStorage.getItem("recent_gradients")) {
recentGradients = JSON.parse(localStorage.getItem("recent_gradients")!);
} else {
recentGradients = [];
localStorage.setItem("recent_gradients", "[]");
}

// Create sortable
Sortable.create(document.querySelector("#gradientsort")!, {
animation: 200,
// handle: ".handle",
onEnd: (evt: Sortable.SortableEvent): void => {
const { oldIndex, newIndex } = evt;
if (oldIndex === undefined || newIndex === undefined || oldIndex === newIndex) {
return;
}
const [movedItem]: { id: number; color: string }[] = gradientSteps.splice(oldIndex, 1);
gradientSteps.splice(newIndex, 0, movedItem);
gradientSteps = gradientSteps;
},
});
}
</script>

<Modal title="Color Gradient" bind:this={gradientDialog} key="G">
<div class="flex w-full flex-col space-y-2">
<p>Add colours to the gradient below:</p>
<div class="flex flex-col space-y-1">
{#each gradientSteps ?? [] as _, i}
<div class="flex w-full items-center rounded-md bg-zinc-900 p-2">
<div class="grow">
<ColorPicker
bind:hex={gradientSteps[i]}
position="responsive"
--cp-bg-color="#18181b"
--cp-text-color="white"
--cp-input-color="#0C0C0E"
--cp-button-hover-color="#18181b"
textInputModes={["hex"]}
isAlpha={false} />
</div>
{#if gradientSteps.length > 1}
<Modal title="Color Gradient" bind:this={gradientDialog} onOpen={opened} key="G">
<div class="flex w-full flex-col space-y-1">
{#if !recentsPageOpen}
<p>Add colours to the gradient below:</p>
<ul class="flex flex-col space-y-1" id="gradientsort">
{#each gradientSteps as g, i (g.id)}
<li class="flex w-full items-center rounded-md bg-zinc-900 p-2">
<IconHandle class="handle cursor-move text-zinc-500" />
<div class="grow">
<ColorPicker
bind:hex={gradientSteps[i].color}
position="responsive"
--cp-bg-color="#18181b"
--cp-text-color="white"
--cp-input-color="#0C0C0E"
--cp-button-hover-color="#18181b"
textInputModes={["hex"]}
isAlpha={false} />
</div>
{#if gradientSteps.length > 1}
<button
onclick={() => {
gradientSteps.splice(i, 1);
gradientSteps = gradientSteps;
}}
class="flex aspect-square h-9 w-fit items-center justify-center rounded-md bg-zinc-900 p-2 hover:bg-black/20">
<IconDelete />
</button>
{/if}
</li>
{/each}
</ul>
<div class="flex justify-between space-x-2">
<button
onclick={() => {
gradientSteps.push({ id: Date.now(), color: "#ffffff" });
gradientSteps = gradientSteps;
}}
class="aspect-square h-9 w-9 rounded-md bg-zinc-900 p-2 hover:bg-black/50">
<IconCustom class="m-auto" />
</button>
</div>
<p class="mt-2">Preview</p>
<div class="font-minecraft rounded-md bg-zinc-950 px-4 py-2 text-2xl">
<span
style="background: -webkit-linear-gradient(0, {gradientSteps.map((step) => step.color).join(
',',
)}); -webkit-background-clip: text; -webkit-text-fill-color: transparent;"
>{editor?.state.doc.textBetween(
editor.state.selection.from,
editor.state.selection.to,
" ",
) || "error! no text selected"}</span>
</div>

<p class="text-sm text-white/60">
<b>Note:</b> you need to select text in the editor before you try and apply a gradient.
</p>

<div class="mt-2 flex items-center justify-between">
<button
onclick={() => {
applyGradient(editor!, gradientSteps.map((step) => step.color));
gradientDialog.close();
}}
class="btn">
Apply Gradient
</button>

<button onclick={openRecentsPage} class="btn flex items-center space-x-1">
<IconRecent />
<span>Recent gradients</span>
</button>
</div>
{:else}
<div class="flex items-center">
<p class="grow">Your top 10 most recent gradients are shown below:</p>
</div>
<div class="grid grid-cols-1 gap-2 md:grid-cols-2">
{#each recentGradients.slice().reverse() ?? [] as gradient}
<div class="flex items-center space-x-1">
<div class="flex w-full items-center space-x-2 rounded-md bg-zinc-900 p-2">
<!-- <span>Colours: </span> -->
<div class="flex grow items-center space-x-1">
{#each gradient ?? [] as step}
<IconCircle class="text-xl" style={"color: " + step + ";"} />
{/each}
</div>
</div>
<button
onclick={() => {
gradientSteps.splice(i, 1);
gradientSteps = gradientSteps;
}}
class="flex aspect-square h-9 w-fit items-center justify-center rounded-md bg-zinc-900 p-2 hover:bg-black/20">
<IconDelete />
</button>
{/if}
</div>
{/each}
class="btn"
onclick={async () => {
// im 90% sure this solution is slow as hell and probably unstable too, but i hate this so much i dont care
gradientSteps = gradient.map((color, i) => ({ id: Date.now() - i, color }));
recentsPageOpen = false;
await tick();
opened();
}}>Use</button>
</div>
{/each}
</div>

<button
onclick={() => {
gradientSteps.push("#ffffff");
gradientSteps = gradientSteps;
}}
class="aspect-square h-9 w-9 rounded-md bg-zinc-900 p-2 hover:bg-black/50">
<IconCustom class="m-auto" />
onclick={() => (recentsPageOpen = false)}
class="mt-2 flex h-full w-fit cursor-pointer items-center space-x-2 rounded-md bg-zinc-900 p-2 hover:bg-black/50">
<IconBack />
<span>Back</span>
</button>
</div>
<p class="my-2">Preview</p>
<div class="font-minecraft bg-zinc-950 px-4 py-2 text-2xl">
<span
style="background: -webkit-linear-gradient(0, {gradientSteps.join(
',',
)}); -webkit-background-clip: text; -webkit-text-fill-color: transparent;"
>{editor?.state.doc.textBetween(
editor.state.selection.from,
editor.state.selection.to,
" ",
) || "error! no text selected"}</span>
</div>

<p class="text-sm text-white/60">
<b>Note:</b> you need to select text in the editor before you try and apply a gradient.
</p>

<button
onclick={() => {
applyGradient(editor, gradientSteps);
gradientDialog.close();
}}
class="btn">
Apply Gradient
</button>
{/if}
</div>
</Modal>
Loading