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
48 changes: 48 additions & 0 deletions src/components/CodeBlock.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
import { Code } from 'astro:components';

interface Props {
code: string;
lang: string;
wrap?: boolean;
inline?: boolean;
/** Shiki theme for light mode */
lightTheme?: string;
/** Shiki theme for dark mode */
darkTheme?: string;
}

const {
code,
lang,
wrap = false,
inline = false,
lightTheme = 'min-light',
darkTheme = 'night-owl',
} = Astro.props as Props;
---

<div
class="relative group"
data-code={code}
>
<!-- Light theme for light mode, hidden in dark -->
<Code
code={code}
lang={lang}
wrap={wrap}
inline={inline}
theme={lightTheme}
class="text-sm block dark:hidden"
/>

<!-- Dark theme for dark mode, hidden in light -->
<Code
code={code}
lang={lang}
wrap={wrap}
inline={inline}
theme={darkTheme}
class="hidden dark:block text-sm"
/>
</div>
141 changes: 141 additions & 0 deletions src/controllers/copy_to_clipboard_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { Controller } from "@hotwired/stimulus";

// Connects to data-controller="copy-to-clipboard"
export default class extends Controller {
static targets = ["source", "button"]
static values = {
code: String,
successMessage: { type: String, default: "Copied!" },
successDuration: { type: Number, default: 2000 }
}

connect() {
// Create and inject the copy button
console.log("Copy to Clipboard Controller connected");
this.createCopyButton();
}

createCopyButton() {
// Find the CodeBlock container with class "relative group"
const codeBlockContainer = this.element.querySelector('.relative.group');

if (!codeBlockContainer) {
console.error('Could not find code block container');
return;
}

// Create the copy button element
const button = document.createElement("button");
button.setAttribute("data-copy-to-clipboard-target", "button");
button.setAttribute("data-action", "click->copy_to_clipboard#copy");
button.className = "absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 bg-slate-200 dark:bg-slate-700 hover:bg-slate-300 dark:hover:bg-slate-600 text-slate-700 dark:text-slate-300 p-2 rounded text-xs font-medium flex items-center gap-1 z-10";

// Create clipboard icon SVG
const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
icon.setAttribute("class", "w-4 h-4");
icon.setAttribute("fill", "none");
icon.setAttribute("stroke", "currentColor");
icon.setAttribute("viewBox", "0 0 24 24");

const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("stroke-linecap", "round");
path.setAttribute("stroke-linejoin", "round");
path.setAttribute("stroke-width", "2");
path.setAttribute("d", "M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z");

icon.appendChild(path);

// Create text span
const textSpan = document.createElement("span");
textSpan.className = "copy-text";

// Create success span (hidden initially)
const successSpan = document.createElement("span");
successSpan.textContent = this.successMessageValue;
successSpan.className = "success-text hidden";

// Assemble button
button.appendChild(icon);
button.appendChild(textSpan);
button.appendChild(successSpan);

// Add button to the code block container (which has the group hover effect)
codeBlockContainer.appendChild(button);
}

async copy(event) {
event.preventDefault();

const button = event.currentTarget;
const copyText = button.querySelector('.copy-text');
const successText = button.querySelector('.success-text');

try {
// Get the code to copy - either from data attribute or from the code block content
const codeToCopy = this.codeValue || this.getCodeFromElement();

await navigator.clipboard.writeText(codeToCopy);

// Show success state
copyText.classList.add('hidden');
successText.classList.remove('hidden');

// Reset after specified duration
setTimeout(() => {
copyText.classList.remove('hidden');
successText.classList.add('hidden');
}, this.successDurationValue);

} catch (err) {
console.error('Failed to copy code:', err);

// Fallback for older browsers
this.fallbackCopy(this.codeValue || this.getCodeFromElement());
}
}

getCodeFromElement() {
// Try to get code from various possible sources
const codeElement = this.element.querySelector('code');
const preElement = this.element.querySelector('pre');

if (codeElement) {
return codeElement.textContent;
} else if (preElement) {
return preElement.textContent;
} else {
return this.element.textContent;
}
}

fallbackCopy(text) {
// Fallback method for older browsers
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();

try {
document.execCommand('copy');
const button = this.element.querySelector('[data-copy-to-clipboard-target="button"]');
const copyText = button.querySelector('.copy-text');
const successText = button.querySelector('.success-text');

copyText.classList.add('hidden');
successText.classList.remove('hidden');

setTimeout(() => {
copyText.classList.remove('hidden');
successText.classList.add('hidden');
}, this.successDurationValue);
} catch (err) {
console.error('Fallback copy failed:', err);
} finally {
document.body.removeChild(textArea);
}
}
}
56 changes: 37 additions & 19 deletions src/pages/index.astro
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
---
import "../styles/global.css";
import Main from "../layouts/main.astro";
import CodeBlock from "../components/CodeBlock.astro";

// TODO: Extract these into a config file
const installCode = `git clone https://github.com/josstei/JoStVIM.git
cd LuxVIM`;

const runInstallCode = `./install.sh`;

const launchCode = `Luxvim # For Vim
Lux # For Neovim`;
---

<Main
Expand Down Expand Up @@ -204,7 +214,9 @@ import Main from "../layouts/main.astro";
class="py-16 md:py-20
bg-gray-50 dark:bg-slate-900
dark:from-slate-800 dark:via-slate-700 dark:to-slate-600
dark:backdrop-blur-sm"
dark:backdrop-blur-sm

"
>
<div class="max-w-7xl mx-auto px-6">
<div class="text-center mb-16">
Expand Down Expand Up @@ -423,8 +435,8 @@ import Main from "../layouts/main.astro";
<section
id="installation"
class="py-20
bg-gradient-to-r
from-purple-200 via-indigo-200 to-white
bg-gray-50
dark:bg-gradient-to-r
dark:from-slate-900 dark:via-slate-800 dark:to-slate-700"
>
<div class="max-w-6xl mx-auto px-6">
Expand Down Expand Up @@ -458,13 +470,14 @@ import Main from "../layouts/main.astro";
<p class="text-slate-700 dark:text-slate-300 mb-3">
Download LuxVim to your local machine:
</p>
<div
class="bg-slate-800 text-gray-100 dark:bg-slate-700 dark:text-gray-200 rounded-lg p-4 font-mono text-sm overflow-x-auto shadow-lg"
<div
data-controller="copy_to_clipboard"
data-copy-to-clipboard-code-value={installCode}
>
<code class="text-green-400"
>git clone https://github.com/josstei/JoStVIM.git</code
><br />
<code class="text-blue-400">cd LuxVIM</code>
<CodeBlock
code={installCode}
lang="bash"
/>
</div>
</div>
</div>
Expand All @@ -484,10 +497,14 @@ import Main from "../layouts/main.astro";
<p class="text-slate-700 dark:text-slate-300 mb-3">
The install script handles everything automatically:
</p>
<div
class="bg-slate-800 text-gray-100 dark:bg-slate-700 dark:text-gray-200 rounded-lg p-4 font-mono text-sm shadow-lg"
<div
data-controller="copy_to_clipboard"
data-copy-to-clipboard-code-value={runInstallCode}
>
<code class="text-yellow-400">./install.sh</code>
<CodeBlock
code={runInstallCode}
lang="bash"
/>
</div>
</div>
</div>
Expand All @@ -507,13 +524,14 @@ import Main from "../layouts/main.astro";
<p class="text-slate-700 dark:text-slate-300 mb-3">
Start coding with your new setup:
</p>
<div
class="bg-slate-800 text-gray-100 dark:bg-slate-700 dark:text-gray-200 rounded-lg p-4 font-mono text-sm shadow-lg"
<div
data-controller="copy_to_clipboard"
data-copy-to-clipboard-code-value={launchCode}
>
<code class="text-cyan-400">Luxvim</code>
<span class="text-slate-500"># For Vim</span><br />
<code class="text-cyan-400">Lux</code>
<span class="text-slate-500"># For Neovim</span>
<CodeBlock
code={launchCode}
lang="bash"
/>
</div>
</div>
</div>
Expand Down Expand Up @@ -634,7 +652,7 @@ import Main from "../layouts/main.astro";
Star on GitHub
</a>
<a
href="/docs"
href={import.meta.env.BASE_URL + "/docs"}
class="inline-flex items-center px-8 py-4 text-lg font-semibold text-slate-600 dark:text-slate-200 bg-white/90 dark:bg-slate-700/80 backdrop-blur-sm border border-slate-300 dark:border-slate-600/50 rounded-xl hover:bg-white dark:hover:bg-slate-600/80 hover:text-slate-800 dark:hover:text-white transition-all duration-300 hover:scale-105 shadow-lg"
>
<svg
Expand Down
23 changes: 23 additions & 0 deletions src/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,26 @@ nav {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}

/* Shiki dual theme support */
.astro-code,
.astro-code span {
color: var(--shiki-light);
background-color: var(--shiki-light-bg);
font-style: var(--shiki-light-font-style);
font-weight: var(--shiki-light-font-weight);
text-decoration: var(--shiki-light-text-decoration);
}

html.dark .astro-code,
html.dark .astro-code span {
color: var(--shiki-dark);
background-color: var(--shiki-dark-bg);
font-style: var(--shiki-dark-font-style);
font-weight: var(--shiki-dark-font-weight);
text-decoration: var(--shiki-dark-text-decoration);
}

.astro-code {
@apply rounded-lg;
@apply p-4;
}