Initial commit
This commit is contained in:
59
extension/src/App.vue
Normal file
59
extension/src/App.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { cn } from './lib/utils'
|
||||
|
||||
const isLoading = ref(false)
|
||||
|
||||
const triggerMockLoading = () => {
|
||||
isLoading.value = true
|
||||
setTimeout(() => {
|
||||
isLoading.value = false
|
||||
}, 1000)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-[400px] h-[600px] bg-[#0A0A0A]/90 backdrop-blur-xl border border-white/10 text-[#E5E5E5] p-6 flex flex-col font-sans">
|
||||
|
||||
<!-- Title Area -->
|
||||
<div class="mb-6">
|
||||
<h1 class="text-xl font-medium tracking-tight bg-gradient-to-r from-violet-500 to-blue-500 bg-clip-text text-transparent inline-block">
|
||||
InsightReply
|
||||
</h1>
|
||||
<p class="text-xs text-zinc-400 mt-1">Social Insight Copilot</p>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<div class="flex-1 overflow-y-auto pr-2 space-y-4">
|
||||
|
||||
<!-- Example Heat Score Card -->
|
||||
<div class="bg-[#171717] rounded-xl p-4 border border-white/5 shadow-lg shadow-black/50">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="text-sm font-medium text-zinc-300">Current Tweet Heat</span>
|
||||
<span class="text-xs font-bold px-2 py-0.5 rounded-full bg-orange-500/20 text-orange-400 border border-orange-500/20">Hot</span>
|
||||
</div>
|
||||
<div class="flex items-end gap-2">
|
||||
<span class="text-3xl font-semibold tracking-tighter">87.5</span>
|
||||
<span class="text-xs text-zinc-500 mb-1">/ 100</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Button -->
|
||||
<button
|
||||
@click="triggerMockLoading"
|
||||
:disabled="isLoading"
|
||||
:class="cn(
|
||||
'w-full py-2.5 rounded-lg text-sm font-medium transition-all duration-200 ease-in-out',
|
||||
'flex items-center justify-center gap-2',
|
||||
isLoading
|
||||
? 'bg-[#171717] text-zinc-500 border border-white/5 cursor-not-allowed'
|
||||
: 'bg-brand-primary hover:bg-brand-primary/90 text-white shadow-lg shadow-brand-primary/20 hover:scale-[0.98]'
|
||||
)"
|
||||
>
|
||||
<span v-if="isLoading" class="animate-spin inline-block w-4 h-4 border-2 border-white/20 border-t-white rounded-full"></span>
|
||||
{{ isLoading ? 'Generating Insights...' : 'Generate Replies' }}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
16
extension/src/assets/tailwind.css
Normal file
16
extension/src/assets/tailwind.css
Normal file
@@ -0,0 +1,16 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-brand-primary: #8B5CF6;
|
||||
--color-brand-secondary: #3B82F6;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
@apply bg-[#0A0A0A] text-[#E5E5E5] antialiased;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: rgb(139 92 246 / 0.3);
|
||||
}
|
||||
}
|
||||
1
extension/src/assets/vue.svg
Normal file
1
extension/src/assets/vue.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 496 B |
39
extension/src/background/index.ts
Normal file
39
extension/src/background/index.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* InsightReply Background Script
|
||||
* 负责中转消息、管理 OAuth 以及与 Go 后端 API 通信
|
||||
*/
|
||||
|
||||
console.log('InsightReply Background Script Loaded');
|
||||
|
||||
const API_BASE = 'http://localhost:8080/api/v1';
|
||||
|
||||
chrome.runtime.onMessage.addListener((message: { type: string; payload?: any }, _sender: chrome.runtime.MessageSender, sendResponse: (response?: any) => void) => {
|
||||
if (message.type === 'SHOW_INSIGHT') {
|
||||
console.log('Received tweet data in background:', message.payload);
|
||||
}
|
||||
|
||||
if (message.type === 'GENERATE_REPLY') {
|
||||
const { tweetContent, strategy, identity } = message.payload;
|
||||
|
||||
fetch(`${API_BASE}/ai/generate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
tweet_content: tweetContent,
|
||||
strategy: strategy,
|
||||
identity: identity || 'Independent Developer / Founder'
|
||||
})
|
||||
})
|
||||
.then(resp => resp.json())
|
||||
.then(data => {
|
||||
sendResponse({ success: true, data: data.data });
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('API Error:', err);
|
||||
sendResponse({ success: false, error: err.message });
|
||||
});
|
||||
|
||||
return true; // Keep channel open for async response
|
||||
}
|
||||
return true;
|
||||
});
|
||||
41
extension/src/components/HelloWorld.vue
Normal file
41
extension/src/components/HelloWorld.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps<{ msg: string }>()
|
||||
|
||||
const count = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ msg }}</h1>
|
||||
|
||||
<div class="card">
|
||||
<button type="button" @click="count++">count is {{ count }}</button>
|
||||
<p>
|
||||
Edit
|
||||
<code>components/HelloWorld.vue</code> to test HMR
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Check out
|
||||
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
||||
>create-vue</a
|
||||
>, the official Vue + Vite starter
|
||||
</p>
|
||||
<p>
|
||||
Learn more about IDE Support for Vue in the
|
||||
<a
|
||||
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
|
||||
target="_blank"
|
||||
>Vue Docs Scaling up Guide</a
|
||||
>.
|
||||
</p>
|
||||
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
143
extension/src/content/Sidebar.vue
Normal file
143
extension/src/content/Sidebar.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
tweetData?: {
|
||||
author: string;
|
||||
content: string;
|
||||
stats: {
|
||||
replies: string;
|
||||
retweets: string;
|
||||
likes: string;
|
||||
}
|
||||
}
|
||||
}>()
|
||||
|
||||
const isVisible = ref(true)
|
||||
const selectedStrategy = ref('Insightful')
|
||||
const generatedReply = ref('')
|
||||
const isGenerating = ref(false)
|
||||
|
||||
const strategies = [
|
||||
{ id: 'Insightful', label: '认知升级型', icon: '🧠' },
|
||||
{ id: 'Humorous', label: '幽默风趣型', icon: '😄' },
|
||||
{ id: 'Professional', label: '专业严谨型', icon: '⚖️' },
|
||||
{ id: 'Supportive', label: '共鸣支持型', icon: '❤️' },
|
||||
{ id: 'Critical', label: '锐评批判型', icon: '🔥' }
|
||||
]
|
||||
|
||||
const generate = () => {
|
||||
if (!props.tweetData) return
|
||||
|
||||
isGenerating.value = true
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'GENERATE_REPLY',
|
||||
payload: {
|
||||
tweetContent: props.tweetData.content,
|
||||
strategy: selectedStrategy.value,
|
||||
identity: 'Independent Developer / Founder' // Could be dynamic later
|
||||
}
|
||||
}, (response) => {
|
||||
isGenerating.value = false
|
||||
if (response && response.success) {
|
||||
generatedReply.value = response.data.reply
|
||||
} else {
|
||||
generatedReply.value = 'Failed to generate reply. Please check your connection or API key.'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const copyToClipboard = () => {
|
||||
navigator.clipboard.writeText(generatedReply.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="isVisible" class="fixed right-4 top-20 w-[360px] max-h-[85vh] flex flex-col bg-[#0A0A0A]/90 backdrop-blur-2xl border border-white/10 rounded-2xl shadow-2xl text-[#E5E5E5] font-sans z-[9999] overflow-hidden">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="p-4 border-b border-white/5 flex justify-between items-center bg-white/5">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-2 h-2 rounded-full bg-brand-primary animate-pulse"></div>
|
||||
<span class="text-sm font-medium tracking-wide">InsightReply AI</span>
|
||||
</div>
|
||||
<button @click="isVisible = false" class="text-zinc-500 hover:text-white transition-colors">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="p-5 flex-1 overflow-y-auto space-y-6">
|
||||
|
||||
<!-- Tweet Context (Small Preview) -->
|
||||
<div v-if="tweetData" class="bg-white/5 rounded-xl p-3 border border-white/5">
|
||||
<div class="text-[10px] text-zinc-500 mb-1 font-mono uppercase">Context</div>
|
||||
<div class="text-xs text-zinc-400 line-clamp-2 italic">" {{ tweetData.content }} "</div>
|
||||
</div>
|
||||
|
||||
<!-- Strategy Selector -->
|
||||
<div class="space-y-3">
|
||||
<span class="text-xs font-semibold text-zinc-500 uppercase tracking-widest">Generation Strategy</span>
|
||||
<div class="grid grid-cols-1 gap-2">
|
||||
<button
|
||||
v-for="s in strategies"
|
||||
:key="s.id"
|
||||
@click="selectedStrategy = s.id"
|
||||
:class="[
|
||||
'flex items-center gap-3 p-3 rounded-xl border transition-all duration-200 text-sm group',
|
||||
selectedStrategy === s.id
|
||||
? 'bg-brand-primary/20 border-brand-primary text-white shadow-lg shadow-brand-primary/10'
|
||||
: 'bg-white/5 border-transparent hover:bg-white/10 text-zinc-400'
|
||||
]"
|
||||
>
|
||||
<span class="text-lg">{{ s.icon }}</span>
|
||||
<span class="flex-1 text-left">{{ s.label }}</span>
|
||||
<div v-if="selectedStrategy === s.id" class="w-1.5 h-1.5 rounded-full bg-brand-primary"></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Result Area -->
|
||||
<div v-if="generatedReply" class="space-y-3 animate-in fade-in slide-in-from-bottom-2 duration-500">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-xs font-semibold text-zinc-500 uppercase tracking-widest">Assistant Suggestion</span>
|
||||
<button @click="copyToClipboard" class="text-[10px] text-brand-primary hover:underline">Copy Result</button>
|
||||
</div>
|
||||
<div class="bg-[#171717] rounded-xl p-4 border border-white/10 text-sm leading-relaxed whitespace-pre-wrap">
|
||||
{{ generatedReply }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Footer Action -->
|
||||
<div class="p-4 bg-white/5 border-t border-white/5">
|
||||
<button
|
||||
@click="generate"
|
||||
:disabled="isGenerating"
|
||||
class="w-full py-3 bg-gradient-to-r from-violet-600 to-blue-600 hover:from-violet-500 hover:to-blue-500 disabled:from-zinc-800 disabled:to-zinc-800 disabled:text-zinc-500 disabled:cursor-not-allowed text-white rounded-xl text-sm font-semibold transition-all shadow-xl shadow-brand-primary/20 flex items-center justify-center gap-2"
|
||||
>
|
||||
<div v-if="isGenerating" class="w-4 h-4 border-2 border-white/20 border-t-white rounded-full animate-spin"></div>
|
||||
{{ isGenerating ? 'AI Thinking...' : 'Generate High-Quality Reply' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 局部样式保证不溢出 */
|
||||
::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
</style>
|
||||
107
extension/src/content/index.ts
Normal file
107
extension/src/content/index.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* InsightReply Content Script
|
||||
* 负责解析 X (Twitter) 页面 DOM,提取推文内容并注入交互按钮
|
||||
*/
|
||||
|
||||
console.log('InsightReply Content Script Loaded');
|
||||
|
||||
// 1. 定义推文数据结构
|
||||
interface TweetData {
|
||||
id: string;
|
||||
author: string;
|
||||
content: string;
|
||||
stats: {
|
||||
replies: string;
|
||||
retweets: string;
|
||||
likes: string;
|
||||
};
|
||||
}
|
||||
|
||||
// 2. 提取推文内容的逻辑
|
||||
const extractTweetData = (tweetElement: HTMLElement): TweetData | null => {
|
||||
try {
|
||||
// 根据 X 的 DOM 结构提取 (可能会随 Twitter 更新而变化)
|
||||
const textElement = tweetElement.querySelector('[data-testid="tweetText"]');
|
||||
const authorElement = tweetElement.querySelector('[data-testid="User-Name"]');
|
||||
const linkElement = tweetElement.querySelector('time')?.parentElement as HTMLAnchorElement;
|
||||
|
||||
// 互动数据提取
|
||||
const getStat = (testid: string) => {
|
||||
const el = tweetElement.querySelector(`[data-testid="${testid}"]`);
|
||||
return el?.getAttribute('aria-label') || '0';
|
||||
};
|
||||
|
||||
if (!textElement || !authorElement || !linkElement) return null;
|
||||
|
||||
const tweetId = linkElement.href.split('/').pop() || '';
|
||||
|
||||
return {
|
||||
id: tweetId,
|
||||
author: authorElement.textContent || 'Unknown',
|
||||
content: textElement.textContent || '',
|
||||
stats: {
|
||||
replies: getStat('reply'),
|
||||
retweets: getStat('retweet'),
|
||||
likes: getStat('like'),
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
console.error('Failed to extract tweet data:', e);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// 3. 注入“Insight”按钮
|
||||
const injectInsightButton = (tweetElement: HTMLElement) => {
|
||||
// 查找操作栏 (Actions bar)
|
||||
const actionBar = tweetElement.querySelector('[role="group"]');
|
||||
if (!actionBar || actionBar.querySelector('.insight-reply-btn')) return;
|
||||
|
||||
// 创建按钮
|
||||
const btnContainer = document.createElement('div');
|
||||
btnContainer.className = 'insight-reply-btn';
|
||||
btnContainer.style.display = 'flex';
|
||||
btnContainer.style.alignItems = 'center';
|
||||
btnContainer.style.marginLeft = '12px';
|
||||
btnContainer.style.cursor = 'pointer';
|
||||
|
||||
// 按钮内部图标 (简易版)
|
||||
btnContainer.innerHTML = `
|
||||
<div style="padding: 4px; border-radius: 9999px; transition: background 0.2s;" onmouseover="this.style.background='rgba(139, 92, 246, 0.1)'" onmouseout="this.style.background='transparent'">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor" style="color: #8B5CF6;">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
`;
|
||||
|
||||
btnContainer.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
const data = extractTweetData(tweetElement);
|
||||
console.log('Target Tweet Data:', data);
|
||||
|
||||
// 发送消息给插件侧边栏/Popup (后续实现)
|
||||
if (data) {
|
||||
chrome.runtime.sendMessage({ type: 'SHOW_INSIGHT', payload: data });
|
||||
}
|
||||
};
|
||||
|
||||
actionBar.appendChild(btnContainer);
|
||||
};
|
||||
|
||||
// 4. 定时或监听 DOM 变化进行扫描
|
||||
const scanTweets = () => {
|
||||
const tweets = document.querySelectorAll('article[data-testid="tweet"]');
|
||||
tweets.forEach((tweet) => {
|
||||
injectInsightButton(tweet as HTMLElement);
|
||||
});
|
||||
};
|
||||
|
||||
// 使用 MutationObserver 监听动态加载
|
||||
const observer = new MutationObserver(() => {
|
||||
scanTweets();
|
||||
});
|
||||
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
// 初始扫描
|
||||
scanTweets();
|
||||
51
extension/src/content/sidebar-mount.ts
Normal file
51
extension/src/content/sidebar-mount.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { createApp } from 'vue'
|
||||
import Sidebar from './Sidebar.vue'
|
||||
import '../assets/tailwind.css' // We might need to handle this specially for Shadow DOM
|
||||
|
||||
const MOUNT_ID = 'insight-reply-sidebar-root'
|
||||
|
||||
function initSidebar(tweetData?: any) {
|
||||
if (document.getElementById(MOUNT_ID)) return
|
||||
|
||||
// 1. Create Host Element
|
||||
const host = document.createElement('div')
|
||||
host.id = MOUNT_ID
|
||||
document.body.appendChild(host)
|
||||
|
||||
// 2. Create Shadow Root
|
||||
const shadowRoot = host.attachShadow({ mode: 'open' })
|
||||
|
||||
// 3. Create Container for Vue
|
||||
const container = document.createElement('div')
|
||||
container.id = 'app'
|
||||
shadowRoot.appendChild(container)
|
||||
|
||||
// 4. Inject Styles into Shadow DOM
|
||||
// Note: In development/build, we need to find the generated CSS and inject it.
|
||||
// CRXJS usually puts CSS in <link> tags in the head for content scripts.
|
||||
// For Shadow DOM, we need to move or clone them into the shadow root.
|
||||
const injectStyles = () => {
|
||||
const styles = document.querySelectorAll('style, link[rel="stylesheet"]')
|
||||
styles.forEach(style => {
|
||||
// Only clone styles that look like they belong to our extension
|
||||
// This is a heuristic, in a real build we'd use the asset URL
|
||||
shadowRoot.appendChild(style.cloneNode(true))
|
||||
})
|
||||
}
|
||||
|
||||
// Initial injection
|
||||
injectStyles()
|
||||
|
||||
// 5. Create Vue App
|
||||
const app = createApp(Sidebar, { tweetData })
|
||||
app.mount(container)
|
||||
|
||||
console.log('InsightReply Sidebar Mounted in Shadow DOM');
|
||||
}
|
||||
|
||||
// Listen for messages to show/hide or update data
|
||||
chrome.runtime.onMessage.addListener((message) => {
|
||||
if (message.type === 'SHOW_INSIGHT') {
|
||||
initSidebar(message.payload)
|
||||
}
|
||||
});
|
||||
6
extension/src/lib/utils.ts
Normal file
6
extension/src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
5
extension/src/main.ts
Normal file
5
extension/src/main.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { createApp } from 'vue'
|
||||
import './assets/tailwind.css'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
79
extension/src/style.css
Normal file
79
extension/src/style.css
Normal file
@@ -0,0 +1,79 @@
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
7
extension/src/vite-env.d.ts
vendored
Normal file
7
extension/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module "*.vue" {
|
||||
import type { DefineComponent } from "vue";
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
Reference in New Issue
Block a user