diff options
| author | Petri Hienonen <petri.hienonen@gmail.com> | 2025-12-14 11:22:29 +0200 |
|---|---|---|
| committer | Petri Hienonen <petri.hienonen@gmail.com> | 2025-12-14 11:22:29 +0200 |
| commit | 91802c21d446175a02c820bcd243920a0791441b (patch) | |
| tree | 959652e39710c735be6e3ecea1827aabe07f474b | |
| download | page-91802c21d446175a02c820bcd243920a0791441b.tar.zst | |
Intial commit
| -rw-r--r-- | biome.json | 100 | ||||
| -rw-r--r-- | flake.lock | 27 | ||||
| -rw-r--r-- | flake.nix | 28 | ||||
| -rw-r--r-- | index.html | 264 | ||||
| -rw-r--r-- | main.js | 972 | ||||
| -rw-r--r-- | style.css | 1022 |
6 files changed, 2413 insertions, 0 deletions
diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..7463a70 --- /dev/null +++ b/biome.json @@ -0,0 +1,100 @@ +{ + "assist": { + "actions": { + "source": { + "organizeImports": "on", + "recommended": true, + "useSortedAttributes": "on", + "useSortedKeys": "on" + } + }, + "enabled": true + }, + "files": { + "ignoreUnknown": true, + "includes": ["index.html", "style.css", "main.js", "biome.json"] + }, + "formatter": { + "indentStyle": "tab", + "lineWidth": 100 + }, + "html": { + "experimentalFullSupportEnabled": true, + "formatter": { + "enabled": true, + "indentScriptAndStyle": true, + "lineWidth": 100, + "whitespaceSensitivity": "ignore" + }, + "linter": { "enabled": true } + }, + "linter": { + "enabled": true, + "rules": { + "complexity": { + "noExcessiveCognitiveComplexity": { + "level": "warn", + "options": { + "maxAllowedComplexity": 50 + } + } + }, + "correctness": { + "noUnusedVariables": { + "fix": "none", + "level": "warn" + } + }, + "nursery": { + "recommended": true + }, + "recommended": true, + "style": { + "useNamingConvention": { + "level": "error", + "options": { + "conventions": [ + { + "formats": ["PascalCase"], + "selector": { + "kind": "typeParameter" + } + }, + { + "formats": ["camelCase"], + "selector": { + "kind": "let" + } + }, + { + "formats": ["camelCase"], + "selector": { + "kind": "classMember" + } + }, + { + "formats": ["camelCase"], + "selector": { + "kind": "classProperty" + } + }, + { + "formats": ["camelCase"], + "selector": { + "kind": "function" + } + }, + { + "formats": ["camelCase", "PascalCase"], + "selector": { + "kind": "objectLiteralMember" + } + } + ], + "strictCase": false + } + } + } + } + } +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..7d39aeb --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1765311797, + "narHash": "sha256-mSD5Ob7a+T2RNjvPvOA1dkJHGVrNVl8ZOrAwBjKBDQo=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "09eb77e94fa25202af8f3e81ddc7353d9970ac1b", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-25.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..2cf8f59 --- /dev/null +++ b/flake.nix @@ -0,0 +1,28 @@ +{ + description = "Page"; + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11"; + }; + + outputs = + { self, nixpkgs }: + let + supportedSystems = [ + "x86_64-linux" + "aarch64-linux" + ]; + forAllSystems = nixpkgs.lib.genAttrs supportedSystems; + nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; }); + in + { + devShells = forAllSystems ( + system: + let + pkgs = nixpkgsFor.${system}; + in + { + default = pkgs.mkShell { buildInputs = with pkgs; [ biome ]; }; + } + ); + }; +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..5573e60 --- /dev/null +++ b/index.html @@ -0,0 +1,264 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>SEARCH</title> + <!-- FAVICON --> + <link + rel="icon" + href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><rect width=%22100%22 height=%22100%22 fill=%22black%22/><text x=%2250%22 y=%2250%22 fill=%22white%22 font-family=%22monospace%22 font-size=%2280%22 text-anchor=%22middle%22 dy=%22.35em%22>%3E</text></svg>" + > + <link rel="stylesheet" href="style.css"> + </head> + <body> + <!-- HERO SEARCH --> + <section class="hero" id="hero"> + <div class="logo-large">SEARCH</div> + <div class="tagline">Universal Search Engine</div> + + <div class="search-container"> + <div class="search-box"> + <div class="search-prefix">→</div> + <input + type="text" + id="search-input" + placeholder="SEARCH, $TICKER, !N NEWS..." + autofocus + spellcheck="false" + > + <div class="search-actions"> + <button class="action-btn" id="voice-btn" title="VOICE">◉</button> + <button class="action-btn" id="clear-btn" title="CLEAR">×</button> + </div> + </div> + </div> + + <div class="shortcuts"> + <span class="shortcut" onclick="insertBang('!n')">!N NEWS</span> + <span class="shortcut" onclick="searchFor('$TSLA')">$TSLA</span> + <span class="shortcut" onclick="insertBang('!w')">!W WIKI</span> + <span class="shortcut" onclick="insertBang('!g')">!G GITHUB</span> + <span class="shortcut" onclick="insertBang('!r')">!R REDDIT</span> + <span class="shortcut" onclick="toolUUID()">UUID</span> + <span class="shortcut" onclick="toolTimer()">TIMER</span> + </div> + </section> + + <!-- STATS --> + <div class="stats-bar" id="stats-bar"> + <div class="stat-item"> + <span>RESULTS</span> + <span class="stat-value" id="stat-results">0</span> + </div> + <div class="stat-item"> + <span>SOURCES</span> + <span class="stat-value" id="stat-sources">0</span> + </div> + <div class="stat-item"> + <span>LATENCY</span> + <span class="stat-value" id="stat-time">0MS</span> + </div> + </div> + + <!-- MAIN --> + <main class="main" id="main-content" style="display:none;"> + <!-- LEFT SIDEBAR --> + <aside class="sidebar-left"> + <!-- SOURCES --> + <div class="widget" id="widget-sources"> + <div class="widget-header" onclick="toggleWidget('widget-sources')"> + <span class="widget-title">Sources</span> + <span class="widget-toggle">+</span> + </div> + <div class="widget-body"> + <div class="sources-list" id="sources-list"></div> + </div> + </div> + + <!-- TOOLS --> + <div class="widget" id="widget-tools"> + <div class="widget-header" onclick="toggleWidget('widget-tools')"> + <span class="widget-title">Tools</span> + <span class="widget-toggle">+</span> + </div> + <div class="widget-body"> + <div class="tools-grid"> + <button class="tool-btn" onclick="toolUUID()"> + <span class="icon">◈</span>UUID + </button> + <button class="tool-btn" onclick="toolTimer()"> + <span class="icon">◷</span>TIMER + </button> + <button class="tool-btn" onclick="toolBase64()"> + <span class="icon">◐</span>BASE64 + </button> + <button class="tool-btn" onclick="toolJSON()"> + <span class="icon">{ }</span>JSON + </button> + <button class="tool-btn" onclick="toolHash()"> + <span class="icon">#</span>HASH + </button> + <button class="tool-btn" onclick="toolColor()"> + <span class="icon">◼</span>COLOR + </button> + <button class="tool-btn" onclick="toolPassword()"> + <span class="icon">※</span>PASSWORD + </button> + <button class="tool-btn" onclick="toolLoremIpsum()"> + <span class="icon">¶</span>LOREM + </button> + </div> + </div> + </div> + + <!-- BANGS --> + <div class="widget" id="widget-bangs"> + <div class="widget-header" onclick="toggleWidget('widget-bangs')"> + <span class="widget-title">Bangs</span> + <span class="widget-toggle">+</span> + </div> + <div class="widget-body"> + <div class="bangs-grid"> + <span class="bang" onclick="insertBang('!n')">!N NEWS</span> + <span class="bang" onclick="insertBang('!w')">!W WIKI</span> + <span class="bang" onclick="insertBang('!g')">!G GITHUB</span> + <span class="bang" onclick="insertBang('!r')">!R REDDIT</span> + <span class="bang" onclick="insertBang('!hn')">!HN NEWS</span> + <span class="bang" onclick="insertBang('!so')">!SO STACK</span> + <span class="bang" onclick="insertBang('!npm')">!NPM</span> + <span class="bang" onclick="insertBang('!d')">!D DICT</span> + <span class="bang" onclick="insertBang('!b')">!B BOOKS</span> + <span class="bang" onclick="insertBang('$')">$ STOCK</span> + </div> + </div> + </div> + + <!-- HISTORY --> + <div class="widget" id="widget-history"> + <div class="widget-header" onclick="toggleWidget('widget-history')"> + <span class="widget-title">History</span> + <span class="widget-toggle">+</span> + </div> + <div class="widget-body"> + <div id="history-list"> + <div style="color:var(--gray);font-size:11px;">NO HISTORY</div> + </div> + </div> + </div> + </aside> + + <!-- RESULTS --> + <section class="results-section"> + <div class="results-header"> + <span id="results-label">READY</span> + <span id="results-info"></span> + </div> + <div class="results-container" id="results"></div> + </section> + + <!-- RIGHT SIDEBAR --> + <aside class="sidebar-right"> + <!-- CALCULATOR --> + <div class="widget" id="widget-calc"> + <div class="widget-header" onclick="toggleWidget('widget-calc')"> + <span class="widget-title">Calculator</span> + <span class="widget-toggle">+</span> + </div> + <div class="widget-body"> + <div class="calc-display"> + <div class="calc-expr" id="calc-expr"></div> + <div class="calc-result" id="calc-result">0</div> + </div> + <div class="calc-grid"> + <button class="calc-btn" data-v="C">C</button> + <button class="calc-btn" data-v="(">(</button> + <button class="calc-btn" data-v=")">)</button> + <button class="calc-btn op" data-v="/">÷</button> + <button class="calc-btn" data-v="7">7</button> + <button class="calc-btn" data-v="8">8</button> + <button class="calc-btn" data-v="9">9</button> + <button class="calc-btn op" data-v="*">×</button> + <button class="calc-btn" data-v="4">4</button> + <button class="calc-btn" data-v="5">5</button> + <button class="calc-btn" data-v="6">6</button> + <button class="calc-btn op" data-v="-">−</button> + <button class="calc-btn" data-v="1">1</button> + <button class="calc-btn" data-v="2">2</button> + <button class="calc-btn" data-v="3">3</button> + <button class="calc-btn op" data-v="+">+</button> + <button class="calc-btn" data-v="0">0</button> + <button class="calc-btn" data-v=".">.</button> + <button class="calc-btn" data-v="^">^</button> + <button class="calc-btn op" data-v="=">=</button> + </div> + </div> + </div> + + <!-- WORLD CLOCKS --> + <div class="widget" id="widget-clocks"> + <div class="widget-header" onclick="toggleWidget('widget-clocks')"> + <span class="widget-title">World Time</span> + <span class="widget-toggle">+</span> + </div> + <div class="widget-body"> + <div class="clocks-grid" id="world-clocks"></div> + </div> + </div> + + <!-- STOCKS WATCHLIST (EDITABLE) --> + <div class="widget" id="widget-watchlist"> + <div class="widget-header" onclick="toggleWidget('widget-watchlist')"> + <span class="widget-title">Watchlist</span> + <span class="widget-toggle">+</span> + </div> + <div class="widget-body"> + <div class="watchlist-input-row"> + <input type="text" id="watchlist-input" placeholder="ADD TICKER..."> + <button id="watchlist-add-btn">+</button> + </div> + <div class="sources-list" id="watchlist-container"> + <!-- Items rendered via JS --> + </div> + </div> + </div> + + <!-- KEYBOARD --> + <div class="widget" id="widget-keyboard"> + <div class="widget-header" onclick="toggleWidget('widget-keyboard')"> + <span class="widget-title">Shortcuts</span> + <span class="widget-toggle">+</span> + </div> + <div class="widget-body" style="font-size:11px;color:var(--gray);line-height:2;"> + <div> + <span style="color:var(--fg)">/</span>Focus search + </div> + <div> + <span style="color:var(--fg)">ESC</span>Clear + </div> + <div> + <span style="color:var(--fg)">$XXX</span>Stock ticker + </div> + <div> + <span style="color:var(--fg)">=</span>Calculator + </div> + <div> + <span style="color:var(--fg)">!bang</span>Direct search + </div> + </div> + </div> + </aside> + </main> + + <footer class="footer"> + <button class="theme-btn" onclick="toggleTheme()">◐ Theme</button> + <span>NO TRACKING</span> + <span id="clock">00:00:00</span> + </footer> + + <!-- TOAST --> + <div class="toast-container" id="toast-container"></div> + + <script defer src="main.js"></script> + </body> +</html> @@ -0,0 +1,972 @@ +// ═══════════════════════════════════════════════════════════ +// SOURCES +// ═══════════════════════════════════════════════════════════ +const SOURCES = { + dictionary: { + bang: "!d", + count: 0, + enabled: true, + fetch: async (q) => { + const r = await fetch( + `https://api.dictionaryapi.dev/api/v2/entries/en/${encodeURIComponent(q)}`, + ); + if (!r.ok) return []; + const d = await r.json(); + return d.slice(0, 3).flatMap((e) => + e.meanings.slice(0, 2).map((m) => ({ + meta: { phonetic: e.phonetic }, + snippet: m.definitions[0]?.definition || "", + source: "dictionary", + title: `${e.word} [${m.partOfSpeech}]`, + url: e.sourceUrls?.[0] || "#", + })), + ); + }, + name: "DICTIONARY", + }, + github: { + bang: "!g", + count: 0, + enabled: true, + fetch: async (q) => { + const r = await fetch( + `https://api.github.com/search/repositories?q=${encodeURIComponent(q)}&per_page=8&sort=stars`, + ); + const d = await r.json(); + return (d.items || []).map((i) => ({ + meta: { forks: i.forks_count, lang: i.language, stars: i.stargazers_count }, + snippet: i.description || "NO DESCRIPTION", + source: "github", + title: i.full_name, + url: i.html_url, + })); + }, + name: "GITHUB", + }, + hackernews: { + bang: "!hn", + count: 0, + enabled: true, + fetch: async (q) => { + const r = await fetch( + `https://hn.algolia.com/api/v1/search?query=${encodeURIComponent(q)}&hitsPerPage=8`, + ); + const d = await r.json(); + return (d.hits || []) + .filter((h) => h.title) + .map((h) => ({ + meta: { comments: h.num_comments, points: h.points }, + snippet: `${h.points || 0} POINTS // ${h.author}`, + source: "hackernews", + title: h.title, + url: h.url || `https://news.ycombinator.com/item?id=${h.objectID}`, + })); + }, + name: "HACKERNEWS", + }, + news: { + bang: "!n", + count: 0, + enabled: true, + fetch: async (q) => { + // Combine HN, Reddit News, and Wiki Current Events logic + const p1 = fetch( + `https://hn.algolia.com/api/v1/search_by_date?query=${encodeURIComponent(q)}&tags=story&hitsPerPage=6`, + ) + .then((r) => r.json()) + .catch(() => ({ hits: [] })); + const p2 = fetch( + `https://www.reddit.com/r/worldnews+news+technology/search.json?q=${encodeURIComponent(q)}&restrict_sr=1&sort=new&limit=6`, + ) + .then((r) => r.json()) + .catch(() => ({ data: { children: [] } })); + + const [hn, rd] = await Promise.all([p1, p2]); + + const hnRes = (hn.hits || []).map((h) => ({ + meta: { date: h.created_at_i, lang: "HN" }, + snippet: `HACKERNEWS // ${h.points || 0} PTS // ${timeAgo(h.created_at_i * 1000)}`, + source: "news", + title: h.title, + url: h.url || `https://news.ycombinator.com/item?id=${h.objectID}`, + })); + + const rdRes = (rd.data?.children || []).map((c) => ({ + meta: { date: c.data.created_utc, lang: "RD" }, + snippet: `REDDIT r/${c.data.subreddit.toUpperCase()} // ${c.data.score} UPVOTES`, + source: "news", + title: c.data.title, + url: `https://reddit.com${c.data.permalink}`, + })); + + // Interleave/Sort by recency + return [...hnRes, ...rdRes].sort((a, b) => b.meta.date - a.meta.date); + }, + name: "AGGREGATE NEWS", + }, + npm: { + bang: "!npm", + count: 0, + enabled: true, + fetch: async (q) => { + const r = await fetch( + `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(q)}&size=8`, + ); + const d = await r.json(); + return (d.objects || []).map((o) => ({ + meta: { version: o.package.version }, + snippet: o.package.description || "NO DESCRIPTION", + source: "npm", + title: o.package.name, + url: `https://npmjs.com/package/${o.package.name}`, + })); + }, + name: "NPM", + }, + openlibrary: { + bang: "!b", + count: 0, + enabled: true, + fetch: async (q) => { + const r = await fetch( + `https://openlibrary.org/search.json?q=${encodeURIComponent(q)}&limit=6`, + ); + const d = await r.json(); + return (d.docs || []).map((b) => ({ + meta: { year: b.first_publish_year }, + snippet: `BY ${(b.author_name || ["UNKNOWN"]).join(", ").toUpperCase()}`, + source: "openlibrary", + title: b.title, + url: `https://openlibrary.org${b.key}`, + })); + }, + name: "OPENLIBRARY", + }, + reddit: { + bang: "!r", + count: 0, + enabled: true, + fetch: async (q) => { + const r = await fetch( + `https://www.reddit.com/search.json?q=${encodeURIComponent(q)}&limit=8`, + ); + const d = await r.json(); + return (d.data?.children || []).map((c) => ({ + meta: { comments: c.data.num_comments, score: c.data.score }, + snippet: c.data.selftext?.substring(0, 200) || `r/${c.data.subreddit}`, + source: "reddit", + title: c.data.title, + url: `https://reddit.com${c.data.permalink}`, + })); + }, + name: "REDDIT", + }, + stackoverflow: { + bang: "!so", + count: 0, + enabled: true, + fetch: async (q) => { + const r = await fetch( + `https://api.stackexchange.com/2.3/search?order=desc&sort=relevance&intitle=${encodeURIComponent(q)}&site=stackoverflow&pagesize=8`, + ); + const d = await r.json(); + return (d.items || []).map((i) => ({ + meta: { answers: i.answer_count, score: i.score }, + snippet: `${i.answer_count} ANSWERS // ${i.view_count} VIEWS`, + source: "stackoverflow", + title: decodeHTML(i.title), + url: i.link, + })); + }, + name: "STACKOVERFLOW", + }, + wikipedia: { + bang: "!w", + count: 0, + enabled: true, + fetch: async (q) => { + const r = await fetch( + `https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(q)}&format=json&origin=*&srlimit=8`, + ); + const d = await r.json(); + return (d.query?.search || []).map((i) => ({ + meta: { words: i.wordcount }, + snippet: i.snippet.replace(/<[^>]*>/g, ""), + source: "wikipedia", + title: i.title, + url: `https://en.wikipedia.org/wiki/${encodeURIComponent(i.title)}`, + })); + }, + instant: async (q) => { + const s = await fetch( + `https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(q)}&format=json&origin=*&srlimit=1`, + ); + const sd = await s.json(); + if (!sd.query?.search?.length) return null; + const title = sd.query.search[0].title; + const r = await fetch( + `https://en.wikipedia.org/w/api.php?action=query&prop=extracts&exintro&explaintext&titles=${encodeURIComponent(title)}&format=json&origin=*`, + ); + const d = await r.json(); + const page = Object.values(d.query.pages)[0]; + return { + content: page.extract, + source: "WIKIPEDIA", + title: page.title, + url: `https://en.wikipedia.org/wiki/${encodeURIComponent(title)}`, + }; + }, + name: "WIKIPEDIA", + }, +}; + +const STOCK_EXCHANGES = { + AAPL: "NASDAQ", + ADA: "CRYPTO", + AMD: "NASDAQ", + AMZN: "NASDAQ", + BAC: "NYSE", + BTC: "CRYPTO", + DIS: "NYSE", + DOGE: "CRYPTO", + ETH: "CRYPTO", + EURUSD: "FX", + GBPUSD: "FX", + GOOG: "NASDAQ", + GOOGL: "NASDAQ", + INTC: "NASDAQ", + JNJ: "NYSE", + JPM: "NYSE", + KO: "NYSE", + META: "NASDAQ", + MSFT: "NASDAQ", + NFLX: "NASDAQ", + NVDA: "NASDAQ", + PYPL: "NASDAQ", + QQQ: "NASDAQ", + SOL: "CRYPTO", + SPY: "AMEX", + TSLA: "NASDAQ", + USDJPY: "FX", + V: "NYSE", + WMT: "NYSE", + XOM: "NYSE", + XRP: "CRYPTO", +}; + +function getSymbol(ticker) { + const t = ticker.toUpperCase(); + const ex = STOCK_EXCHANGES[t]; + if (ex === "CRYPTO") return `BINANCE:${t}USDT`; + if (ex === "FX") return `FX:${t}`; + if (ex) return `${ex}:${t}`; + return `NASDAQ:${t}`; +} + +// ═══════════════════════════════════════════════════════════ +// STATE +// ═══════════════════════════════════════════════════════════ +const state = { + calcExpr: "", + calcResult: "0", + hasSearched: false, + history: JSON.parse(localStorage.getItem("sh") || "[]"), + query: "", + results: [], + watchlist: JSON.parse(localStorage.getItem("sw") || '["TSLA", "AAPL", "BTC", "ETH"]'), +}; + +const $ = (id) => document.getElementById(id); +const searchInput = $("search-input"); +const resultsEl = $("results"); +const heroEl = $("hero"); +const mainEl = $("main-content"); +const statsBar = $("stats-bar"); + +// ═══════════════════════════════════════════════════════════ +// INIT +// ═══════════════════════════════════════════════════════════ +function init() { + renderSources(); + renderHistory(); + renderWatchlist(); + initTheme(); + updateClocks(); + setInterval(updateClocks, 1000); + setupCalc(); + setupListeners(); + setupWatchlistListeners(); +} + +function renderSources() { + $("sources-list").innerHTML = Object.entries(SOURCES) + .map( + ([k, s]) => ` + <div class="source-item ${s.enabled ? "active" : ""}" data-source="${k}" onclick="toggleSource('${k}')"> + ${s.name} <span class="source-count" id="c-${k}">${s.count}</span> + </div> + `, + ) + .join(""); +} + +function toggleSource(k) { + SOURCES[k].enabled = !SOURCES[k].enabled; + renderSources(); + if (state.query) search(state.query); +} + +function toggleWidget(id) { + $(id).classList.toggle("open"); +} + +// ═══════════════════════════════════════════════════════════ +// THEME +// ═══════════════════════════════════════════════════════════ +function initTheme() { + const t = localStorage.getItem("theme"); + if (t === "light") document.body.classList.add("light-mode"); +} + +function toggleTheme() { + document.body.classList.toggle("light-mode"); + const isLight = document.body.classList.contains("light-mode"); + localStorage.setItem("theme", isLight ? "light" : "dark"); +} + +// ═══════════════════════════════════════════════════════════ +// WATCHLIST +// ═══════════════════════════════════════════════════════════ +function renderWatchlist() { + const c = $("watchlist-container"); + if (!state.watchlist.length) { + c.innerHTML = '<div style="color:var(--gray);font-size:11px;padding:10px;">EMPTY</div>'; + return; + } + c.innerHTML = state.watchlist + .map((t) => { + const ex = STOCK_EXCHANGES[t] || "STOCK"; + return ` + <div class="watchlist-item-row" onclick="searchFor('$${t}')"> + <span>${t} <span style="color:var(--gray)">${ex}</span></span> + <span class="watchlist-del-btn" onclick="removeTicker(event, '${t}')">×</span> + </div>`; + }) + .join(""); +} + +function addTicker() { + const val = $("watchlist-input").value.toUpperCase().trim(); + if (val && !state.watchlist.includes(val)) { + state.watchlist.push(val); + localStorage.setItem("sw", JSON.stringify(state.watchlist)); + renderWatchlist(); + $("watchlist-input").value = ""; + } +} + +window.removeTicker = (e, t) => { + e.stopPropagation(); // Prevent search trigger + state.watchlist = state.watchlist.filter((i) => i !== t); + localStorage.setItem("sw", JSON.stringify(state.watchlist)); + renderWatchlist(); +}; + +function setupWatchlistListeners() { + $("watchlist-add-btn").onclick = addTicker; + $("watchlist-input").addEventListener("keydown", (e) => { + if (e.key === "Enter") addTicker(); + }); +} + +// ═══════════════════════════════════════════════════════════ +// SEARCH +// ═══════════════════════════════════════════════════════════ +async function search(query) { + if (!query.trim()) return goHome(); + + // Show results view + if (!state.hasSearched) { + state.hasSearched = true; + heroEl.classList.add("compact"); + mainEl.style.display = "grid"; + statsBar.classList.add("visible"); + } + + // Stock ticker + const stockMatch = query.match(/^\$([A-Za-z]{1,5})$/); + if (stockMatch) { + showStockWidget(stockMatch[1].toUpperCase()); + addHistory(query); + return; + } + + // Bangs + const bangMatch = query.match(/^!(\w+)\s*(.*)/); + if (bangMatch) { + const bang = "!" + bangMatch[1]; + const q = bangMatch[2]; + const src = Object.entries(SOURCES).find(([_, s]) => s.bang === bang); + if (src && q) { + Object.keys(SOURCES).forEach((k) => (SOURCES[k].enabled = k === src[0])); + renderSources(); + query = q; + } + } + + // Calc + if (query.startsWith("=")) { + showCalcResult(query.slice(1)); + return; + } + + state.query = query; + showLoading(); + addHistory(query); + + const start = performance.now(); + const enabled = Object.entries(SOURCES).filter(([_, s]) => s.enabled); + + fetchInstant(query); + + const promises = enabled.map(async ([k, s]) => { + try { + const r = await s.fetch(query); + SOURCES[k].count = r.length; + $(`c-${k}`).textContent = r.length; + return r; + } catch (e) { + return []; + } + }); + + const all = await Promise.all(promises); + state.results = all.flat(); + const elapsed = Math.round(performance.now() - start); + + $("stat-results").textContent = state.results.length; + $("stat-sources").textContent = enabled.length; + $("stat-time").textContent = elapsed + "MS"; + $("results-label").textContent = `${state.results.length} RESULTS`; + $("results-info").textContent = `${elapsed}MS // ${enabled.length} SOURCES`; + + renderResults(); +} + +function goHome() { + state.hasSearched = false; + heroEl.classList.remove("compact"); + mainEl.style.display = "none"; + statsBar.classList.remove("visible"); + searchInput.value = ""; +} + +// ═══════════════════════════════════════════════════════════ +// STOCK WIDGET +// ═══════════════════════════════════════════════════════════ +function showStockWidget(ticker) { + const symbol = getSymbol(ticker); + const widgetId = "tv_" + Math.random().toString(36).substr(2, 9); + const isLight = document.body.classList.contains("light-mode"); + + resultsEl.innerHTML = ` + <div class="stock-widget"> + <div class="stock-header"> + <div class="stock-info"> + <h2>${ticker}</h2> + <div class="ticker">${symbol}</div> + </div> + <div class="stock-actions-top"> + <button class="stock-btn" onclick="window.open('https://www.tradingview.com/symbols/${symbol}/','_blank')">TRADINGVIEW ↗</button> + <button class="stock-btn" onclick="copy('${symbol}')">COPY</button> + </div> + </div> + <div class="tradingview-widget-container" id="${widgetId}"></div> + </div> + `; + + $("results-label").textContent = `STOCK: ${ticker}`; + $("results-info").textContent = symbol; + + loadTradingView(widgetId, symbol, isLight); +} + +function loadTradingView(containerId, symbol, isLight) { + if (!window.TradingView) { + const s = document.createElement("script"); + s.src = "https://s3.tradingview.com/tv.js"; + s.onload = () => createChart(containerId, symbol, isLight); + document.head.appendChild(s); + } else { + createChart(containerId, symbol, isLight); + } +} + +function createChart(containerId, symbol, isLight) { + new TradingView.widget({ + allow_symbol_change: true, + backgroundColor: isLight ? "rgba(244, 244, 245, 1)" : "rgba(0,0,0,1)", + container_id: containerId, + enable_publishing: false, + gridColor: isLight ? "rgba(220, 220, 220, 1)" : "rgba(30,30,30,1)", + height: 500, + interval: "D", + locale: "en", + studies: ["Volume@tv-basicstudies"], + style: "1", + symbol: symbol, + theme: isLight ? "light" : "dark", + timezone: "Etc/UTC", + toolbar_bg: isLight ? "#f4f4f5" : "#000", + width: "100%", + }); +} + +async function fetchInstant(query) { + if (/^[a-zA-Z]+$/.test(query) && SOURCES.dictionary.enabled) { + try { + const r = await fetch( + `https://api.dictionaryapi.dev/api/v2/entries/en/${encodeURIComponent(query)}`, + ); + if (r.ok) { + const d = await r.json(); + if (d[0]) { + const meanings = d[0].meanings + .slice(0, 3) + .map((m) => `[${m.partOfSpeech.toUpperCase()}] ${m.definitions[0]?.definition}`) + .join("\n\n"); + showInstant({ + content: meanings, + source: "DICTIONARY", + title: d[0].word.toUpperCase() + (d[0].phonetic ? ` ${d[0].phonetic}` : ""), + }); + return; + } + } + } catch {} + } + if (SOURCES.wikipedia.enabled) { + const w = await SOURCES.wikipedia.instant?.(query); + if (w) showInstant(w); + } +} + +function showInstant(data) { + const el = document.createElement("div"); + el.className = "instant-box"; + el.innerHTML = ` + <div class="instant-label">INSTANT ANSWER</div> + <div class="instant-title">${esc(data.title)}</div> + <div class="instant-content">${esc(data.content) + .split("\n") + .map((p) => `<p>${p}</p>`) + .join("")}</div> + <div class="instant-meta"> + <span>SOURCE: ${esc(data.source)}</span> + ${data.url ? `<a href="${data.url}" target="_blank">VIEW FULL →</a>` : ""} + </div> + `; + resultsEl.innerHTML = ""; + resultsEl.appendChild(el); +} + +function renderResults() { + if (state.results.length === 0) { + resultsEl.innerHTML = `<div class="empty-state"><div class="empty-title">NO RESULTS</div><div class="empty-text">TRY DIFFERENT KEYWORDS OR ENABLE MORE SOURCES</div></div>`; + return; + } + + const instant = resultsEl.querySelector(".instant-box"); + resultsEl.innerHTML = ""; + if (instant) resultsEl.appendChild(instant); + + state.results.forEach((r, i) => { + const el = document.createElement("div"); + el.className = "result"; + el.innerHTML = ` + <div class="result-index">${String(i + 1).padStart(2, "0")}</div> + <div class="result-body"> + <div class="result-source">${r.source.toUpperCase()}</div> + <div class="result-title" onclick="window.open('${esc(r.url)}','_blank')">${esc(r.title)}</div> + <div class="result-url">${esc(r.url)}</div> + <div class="result-snippet">${highlight(esc(r.snippet), state.query)}</div> + ${ + r.meta + ? `<div class="result-meta"> + ${r.meta.stars !== undefined ? `<span>★ ${fmt(r.meta.stars)}</span>` : ""} + ${r.meta.forks !== undefined ? `<span>⑂ ${fmt(r.meta.forks)}</span>` : ""} + ${r.meta.score !== undefined ? `<span>↑ ${fmt(r.meta.score)}</span>` : ""} + ${r.meta.comments !== undefined ? `<span>◨ ${fmt(r.meta.comments)}</span>` : ""} + ${r.meta.words !== undefined ? `<span>◎ ${fmt(r.meta.words)}W</span>` : ""} + ${r.meta.lang ? `<span>${r.meta.lang.toUpperCase()}</span>` : ""} + ${r.meta.version ? `<span>V${r.meta.version}</span>` : ""} + </div>` + : "" + } + </div> + `; + resultsEl.appendChild(el); + }); +} + +function showLoading() { + resultsEl.innerHTML = Array(4) + .fill( + `<div class="loading-row"><span class="loading-text">FETCHING</span><div class="loading-bar"></div></div>`, + ) + .join(""); +} + +function showCalcResult(expr) { + try { + const result = Function('"use strict";return (' + expr.replace(/\^/g, "**") + ")")(); + resultsEl.innerHTML = `<div class="instant-box"><div class="instant-label">CALCULATOR</div><div class="instant-title" style="font-variant-numeric:tabular-nums">${esc(expr)} = ${result}</div></div>`; + } catch { + toast("INVALID EXPRESSION"); + } +} + +// ═══════════════════════════════════════════════════════════ +// CALCULATOR +// ═══════════════════════════════════════════════════════════ +function setupCalc() { + document.querySelectorAll(".calc-btn").forEach((btn) => { + btn.onclick = () => { + const v = btn.dataset.v; + if (v === "C") { + state.calcExpr = ""; + state.calcResult = "0"; + } else if (v === "=") { + try { + state.calcResult = Function( + '"use strict";return (' + state.calcExpr.replace(/\^/g, "**") + ")", + )(); + } catch { + state.calcResult = "ERR"; + } + } else { + state.calcExpr += v; + } + $("calc-expr").textContent = state.calcExpr; + $("calc-result").textContent = state.calcResult; + }; + }); +} + +// ═══════════════════════════════════════════════════════════ +// TOOLS +// ═══════════════════════════════════════════════════════════ +function toolUUID() { + const uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0; + return (c === "x" ? r : (r & 0x3) | 0x8).toString(16); + }); + copy(uuid); + toast("UUID: " + uuid.substring(0, 13) + "..."); +} + +let timerInterval, + timerSec = 0; +function toolTimer() { + if (!state.hasSearched) { + state.hasSearched = true; + heroEl.classList.add("compact"); + mainEl.style.display = "grid"; + statsBar.classList.add("visible"); + } + resultsEl.innerHTML = `<div class="tool-modal"><div class="instant-label">TIMER</div><div class="instant-title" id="timer-display" style="font-variant-numeric:tabular-nums">00:00:00</div><div class="tool-btn-row" style="margin-top:20px"><button class="tool-action" onclick="startTimer()">START</button><button class="tool-action" onclick="stopTimer()">STOP</button><button class="tool-action" onclick="resetTimer()">RESET</button></div></div>`; + $("results-label").textContent = "TIMER"; +} +window.startTimer = () => { + if (timerInterval) return; + timerInterval = setInterval(() => { + timerSec++; + const h = String(Math.floor(timerSec / 3600)).padStart(2, "0"); + const m = String(Math.floor((timerSec % 3600) / 60)).padStart(2, "0"); + const s = String(timerSec % 60).padStart(2, "0"); + $("timer-display").textContent = `${h}:${m}:${s}`; + }, 1000); +}; +window.stopTimer = () => { + clearInterval(timerInterval); + timerInterval = null; +}; +window.resetTimer = () => { + stopTimer(); + timerSec = 0; + $("timer-display").textContent = "00:00:00"; +}; + +function toolBase64() { + if (!state.hasSearched) { + state.hasSearched = true; + heroEl.classList.add("compact"); + mainEl.style.display = "grid"; + statsBar.classList.add("visible"); + } + resultsEl.innerHTML = `<div class="tool-modal"><div class="instant-label">BASE64</div><textarea id="b64-input" placeholder="ENTER TEXT..."></textarea><div class="tool-btn-row"><button class="tool-action" onclick="$('b64-output').textContent=btoa($('b64-input').value)">ENCODE</button><button class="tool-action" onclick="try{$('b64-output').textContent=atob($('b64-input').value)}catch{$('b64-output').textContent='INVALID'}">DECODE</button></div><div class="tool-output" id="b64-output">OUTPUT</div></div>`; + $("results-label").textContent = "BASE64"; +} + +function toolJSON() { + if (!state.hasSearched) { + state.hasSearched = true; + heroEl.classList.add("compact"); + mainEl.style.display = "grid"; + statsBar.classList.add("visible"); + } + resultsEl.innerHTML = `<div class="tool-modal"><div class="instant-label">JSON</div><textarea id="json-input" placeholder="PASTE JSON..."></textarea><div class="tool-btn-row"><button class="tool-action" onclick="formatJSON()">FORMAT</button><button class="tool-action" onclick="minifyJSON()">MINIFY</button></div><pre class="tool-output" id="json-output" style="white-space:pre-wrap;max-height:300px;overflow:auto">OUTPUT</pre></div>`; + $("results-label").textContent = "JSON"; +} +window.formatJSON = () => { + try { + $("json-output").textContent = JSON.stringify(JSON.parse($("json-input").value), null, 2); + } catch (e) { + $("json-output").textContent = "INVALID: " + e.message; + } +}; +window.minifyJSON = () => { + try { + $("json-output").textContent = JSON.stringify(JSON.parse($("json-input").value)); + } catch (e) { + $("json-output").textContent = "INVALID: " + e.message; + } +}; + +function toolHash() { + if (!state.hasSearched) { + state.hasSearched = true; + heroEl.classList.add("compact"); + mainEl.style.display = "grid"; + statsBar.classList.add("visible"); + } + resultsEl.innerHTML = `<div class="tool-modal"><div class="instant-label">SHA-256 HASH</div><textarea id="hash-input" placeholder="ENTER TEXT..."></textarea><div class="tool-btn-row"><button class="tool-action" onclick="genHash()">GENERATE</button></div><div class="tool-output" id="hash-output">OUTPUT</div></div>`; + $("results-label").textContent = "HASH"; +} +window.genHash = async () => { + const data = new TextEncoder().encode($("hash-input").value); + const hash = await crypto.subtle.digest("SHA-256", data); + $("hash-output").textContent = Array.from(new Uint8Array(hash)) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); +}; + +function toolColor() { + if (!state.hasSearched) { + state.hasSearched = true; + heroEl.classList.add("compact"); + mainEl.style.display = "grid"; + statsBar.classList.add("visible"); + } + resultsEl.innerHTML = `<div class="tool-modal"><div class="instant-label">COLOR</div><input type="color" id="color-input" value="#ffffff" style="width:100%;height:100px;border:3px solid var(--fg);background:var(--bg);cursor:crosshair;"><div class="tool-output" id="color-output" style="margin-top:20px"></div></div>`; + const updateColor = () => { + const hex = $("color-input").value; + const r = parseInt(hex.slice(1, 3), 16); + const g = parseInt(hex.slice(3, 5), 16); + const b = parseInt(hex.slice(5, 7), 16); + $("color-output").innerHTML = + `HEX: ${hex}<br>RGB: ${r}, ${g}, ${b}<br>HSL: ${rgbToHsl(r, g, b)}`; + }; + $("color-input").oninput = updateColor; + updateColor(); + $("results-label").textContent = "COLOR"; +} +function rgbToHsl(r, g, b) { + r /= 255; + g /= 255; + b /= 255; + const max = Math.max(r, g, b), + min = Math.min(r, g, b); + let h, + s, + l = (max + min) / 2; + if (max === min) { + h = s = 0; + } else { + const d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch (max) { + case r: + h = ((g - b) / d + (g < b ? 6 : 0)) / 6; + break; + case g: + h = ((b - r) / d + 2) / 6; + break; + case b: + h = ((r - g) / d + 4) / 6; + break; + } + } + return `${Math.round(h * 360)}°, ${Math.round(s * 100)}%, ${Math.round(l * 100)}%`; +} + +function toolPassword() { + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*"; + let pass = ""; + for (let i = 0; i < 20; i++) pass += chars[Math.floor(Math.random() * chars.length)]; + copy(pass); + toast("PASSWORD: " + pass.substring(0, 10) + "..."); +} + +function toolLoremIpsum() { + const lorem = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris."; + copy(lorem); + toast("LOREM IPSUM COPIED"); +} + +// ═══════════════════════════════════════════════════════════ +// HISTORY +// ═══════════════════════════════════════════════════════════ +function addHistory(q) { + state.history = state.history.filter((h) => h.q !== q); + state.history.unshift({ q, t: Date.now() }); + state.history = state.history.slice(0, 20); + localStorage.setItem("sh", JSON.stringify(state.history)); + renderHistory(); +} + +function renderHistory() { + const list = $("history-list"); + if (!state.history.length) { + list.innerHTML = '<div style="color:var(--gray);font-size:11px;">NO HISTORY</div>'; + return; + } + list.innerHTML = state.history + .slice(0, 6) + .map( + (h) => + `<div class="history-item" onclick="searchFor('${esc(h.q)}')"><span>${esc(h.q)}</span><span class="history-time">${timeAgo(h.t)}</span></div>`, + ) + .join(""); +} + +// ═══════════════════════════════════════════════════════════ +// CLOCKS +// ═══════════════════════════════════════════════════════════ +function updateClocks() { + const now = new Date(); + $("clock").textContent = now.toLocaleTimeString("en-GB"); + const zones = [ + { label: "LOCAL", tz: Intl.DateTimeFormat().resolvedOptions().timeZone }, + { label: "UTC", tz: "UTC" }, + { label: "NYC", tz: "America/New_York" }, + { label: "TOKYO", tz: "Asia/Tokyo" }, + ]; + $("world-clocks").innerHTML = zones + .map( + (z) => + `<div class="clock"><div class="clock-time">${new Date().toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit", timeZone: z.tz })}</div><div class="clock-label">${z.label}</div></div>`, + ) + .join(""); +} + +// ═══════════════════════════════════════════════════════════ +// LISTENERS +// ═══════════════════════════════════════════════════════════ +function setupListeners() { + let debounce; + searchInput.addEventListener("input", (e) => { + clearTimeout(debounce); + debounce = setTimeout(() => search(e.target.value), 300); + }); + searchInput.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + clearTimeout(debounce); + search(searchInput.value); + } + if (e.key === "Escape") goHome(); + }); + document.addEventListener("keydown", (e) => { + if ( + e.key === "/" && + document.activeElement !== searchInput && + document.activeElement !== $("watchlist-input") + ) { + e.preventDefault(); + searchInput.focus(); + } + if (e.key === "Escape") goHome(); + }); + $("clear-btn").onclick = goHome; + $("voice-btn").onclick = voiceSearch; +} + +function voiceSearch() { + if (!("webkitSpeechRecognition" in window || "SpeechRecognition" in window)) { + toast("VOICE NOT SUPPORTED"); + return; + } + const Sr = window.SpeechRecognition || window.webkitSpeechRecognition; + const r = new Sr(); + r.lang = "en-US"; + $("voice-btn").style.background = "#fff"; + $("voice-btn").style.color = "#000"; + r.onresult = (e) => { + searchInput.value = e.results[0][0].transcript; + search(searchInput.value); + }; + r.onend = () => { + $("voice-btn").style.background = ""; + $("voice-btn").style.color = ""; + }; + r.start(); +} + +// ═══════════════════════════════════════════════════════════ +// UTILITIES +// ═══════════════════════════════════════════════════════════ +function esc(s) { + if (!s) return ""; + return s.replace( + /[&<>"']/g, + (m) => ({ "'": "'", '"': """, "&": "&", "<": "<", ">": ">" })[m], + ); +} +function decodeHTML(s) { + const t = document.createElement("textarea"); + t.innerHTML = s; + return t.value; +} +function highlight(text, query) { + if (!query) return text; + const words = query.split(/\s+/).filter((w) => w.length > 2); + let result = text; + words.forEach((w) => { + result = result.replace(new RegExp(`(${w})`, "gi"), "<mark>$1</mark>"); + }); + return result; +} +function fmt(n) { + if (n >= 1000000) return (n / 1000000).toFixed(1) + "M"; + if (n >= 1000) return (n / 1000).toFixed(1) + "K"; + return n?.toString() || "0"; +} +function timeAgo(ts) { + const s = Math.floor((Date.now() - ts) / 1000); + if (s < 60) return "NOW"; + if (s < 3600) return Math.floor(s / 60) + "M"; + if (s < 86400) return Math.floor(s / 3600) + "H"; + return Math.floor(s / 86400) + "D"; +} +function copy(text) { + navigator.clipboard.writeText(text); + toast("COPIED"); +} +function toast(msg) { + const t = document.createElement("div"); + t.className = "toast"; + t.textContent = msg; + $("toast-container").appendChild(t); + setTimeout(() => t.remove(), 3000); +} +function searchFor(q) { + searchInput.value = q; + search(q); +} +function insertBang(bang) { + searchInput.value = bang + " "; + searchInput.focus(); +} + +init(); diff --git a/style.css b/style.css new file mode 100644 index 0000000..6f3f24e --- /dev/null +++ b/style.css @@ -0,0 +1,1022 @@ +@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600;700&display=swap"); + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + /* DARK MODE (DEFAULT) */ + --bg: #000000; + --fg: #ffffff; + --gray: #666666; + --panel: #0a0a0a; + --border-color: #ffffff; + --border-thin-color: #333333; + --input-ph: #333333; + --hover-bg: #111111; +} + +body.light-mode { + /* LIGHT MODE (INVERTED) */ + --bg: #f4f4f5; + --fg: #09090b; + --gray: #52525b; + --panel: #e4e4e7; + --border-color: #09090b; + --border-thin-color: #a1a1aa; + --input-ph: #a1a1aa; + --hover-bg: #d4d4d8; +} + +/* Variables used in CSS */ +:root { + --border: 3px solid var(--border-color); + --border-thin: 1px solid var(--border-thin-color); +} + +::selection { + background: var(--fg); + color: var(--bg); +} + +html { + font-size: 14px; +} + +body { + font-family: "IBM Plex Mono", monospace; + background: var(--bg); + color: var(--fg); + min-height: 100vh; + cursor: crosshair; + transition: + background 0.2s, + color 0.2s; +} + +a { + color: inherit; + text-decoration: none; +} + +/* HERO SEARCH */ +.hero { + min-height: 50vh; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 40px 20px; + border-bottom: var(--border); + position: relative; +} + +.hero.compact { + min-height: auto; + padding: 20px; + position: sticky; + top: 0; + background: var(--bg); + z-index: 1000; +} + +.logo-large { + font-size: 72px; + font-weight: 700; + letter-spacing: -4px; + margin-bottom: 10px; + display: flex; + align-items: center; + gap: 10px; +} + +.hero.compact .logo-large { + font-size: 24px; + letter-spacing: -1px; + margin-bottom: 0; + position: absolute; + left: 30px; + top: 50%; + transform: translateY(-50%); +} + +.logo-large::before { + content: ">"; + animation: blink 1s infinite; +} + +@keyframes blink { + 0%, + 49% { + opacity: 1; + } + 50%, + 100% { + opacity: 0; + } +} + +.tagline { + font-size: 12px; + color: var(--gray); + letter-spacing: 4px; + text-transform: uppercase; + margin-bottom: 50px; +} + +.hero.compact .tagline { + display: none; +} + +.search-container { + width: 100%; + max-width: 800px; +} + +.search-box { + display: flex; + border: var(--border); + background: var(--bg); +} + +.search-prefix { + padding: 25px 25px; + font-size: 28px; + font-weight: 700; + border-right: var(--border); + background: var(--fg); + color: var(--bg); +} + +#search-input { + flex: 1; + padding: 25px 30px; + font-size: 24px; + font-family: inherit; + font-weight: 600; + background: transparent; + color: var(--fg); + border: none; + outline: none; + letter-spacing: -0.5px; +} + +#search-input::placeholder { + color: var(--input-ph); +} + +.search-actions { + display: flex; +} + +.action-btn { + width: 80px; + background: transparent; + border: none; + border-left: var(--border); + color: var(--fg); + font-size: 20px; + cursor: pointer; + transition: all 0.1s; + font-family: inherit; +} + +.action-btn:hover { + background: var(--fg); + color: var(--bg); +} + +.hero.compact .search-container { + max-width: 600px; +} + +.hero.compact .search-box { + border-width: 2px; +} + +.hero.compact .search-prefix { + padding: 15px 20px; + font-size: 20px; + border-width: 2px; +} + +.hero.compact #search-input { + padding: 15px 20px; + font-size: 18px; +} + +.hero.compact .action-btn { + width: 60px; + border-width: 2px; +} + +/* SHORTCUTS */ +.shortcuts { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 10px; + margin-top: 30px; +} + +.hero.compact .shortcuts { + display: none; +} + +.shortcut { + padding: 12px 20px; + border: 2px solid var(--border-thin-color); + font-size: 11px; + letter-spacing: 2px; + cursor: pointer; + transition: all 0.1s; + text-transform: uppercase; +} + +.shortcut:hover { + border-color: var(--fg); + background: var(--fg); + color: var(--bg); +} + +/* STATS BAR */ +.stats-bar { + display: none; + justify-content: center; + gap: 40px; + padding: 15px; + font-size: 11px; + letter-spacing: 2px; + color: var(--gray); + border-bottom: var(--border-thin); +} + +.stats-bar.visible { + display: flex; +} + +.stat-item { + display: flex; + gap: 10px; +} + +.stat-value { + color: var(--fg); + font-weight: 700; +} + +/* MAIN LAYOUT */ +.main { + display: grid; + grid-template-columns: 300px 1fr 300px; + min-height: 50vh; +} + +@media (max-width: 1200px) { + .main { + grid-template-columns: 1fr; + } + .sidebar-left, + .sidebar-right { + border: none !important; + border-bottom: var(--border) !important; + } +} + +/* SIDEBARS */ +.sidebar-left, +.sidebar-right { + border-right: var(--border); +} + +.sidebar-right { + border-right: none; + border-left: var(--border); +} + +/* WIDGET */ +.widget { + border-bottom: var(--border-thin); +} + +.widget-header { + padding: 15px 20px; + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + transition: background 0.1s; +} + +.widget-header:hover { + background: var(--hover-bg); +} + +.widget-title { + font-size: 10px; + text-transform: uppercase; + letter-spacing: 3px; + color: var(--gray); +} + +.widget-toggle { + font-size: 16px; + color: var(--gray); + transition: transform 0.2s; +} + +.widget.open .widget-toggle { + transform: rotate(45deg); +} + +.widget-body { + display: none; + padding: 20px; + border-top: var(--border-thin); +} + +.widget.open .widget-body { + display: block; +} + +/* TOOLS GRID */ +.tools-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 8px; +} + +.tool-btn { + padding: 15px 10px; + border: 2px solid var(--border-thin-color); + background: transparent; + color: var(--gray); + font-family: inherit; + font-size: 9px; + text-transform: uppercase; + letter-spacing: 1px; + cursor: pointer; + transition: all 0.1s; + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; +} + +.tool-btn:hover { + background: var(--fg); + color: var(--bg); + border-color: var(--fg); +} + +.tool-btn .icon { + font-size: 20px; +} + +/* BANGS */ +.bangs-grid { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.bang { + padding: 6px 10px; + border: 1px solid var(--border-thin-color); + font-size: 10px; + cursor: pointer; + transition: all 0.1s; +} + +.bang:hover { + background: var(--fg); + color: var(--bg); +} + +/* CALCULATOR */ +.calc-display { + background: var(--hover-bg); + padding: 15px; + margin-bottom: 15px; + border: 1px solid var(--border-thin-color); +} + +.calc-expr { + font-size: 12px; + color: var(--gray); + min-height: 16px; +} + +.calc-result { + font-size: 28px; + font-weight: 700; +} + +.calc-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 4px; +} + +.calc-btn { + padding: 12px; + border: 1px solid var(--border-thin-color); + background: transparent; + color: var(--fg); + font-family: inherit; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: all 0.1s; +} + +.calc-btn:hover { + background: var(--fg); + color: var(--bg); +} + +.calc-btn.op { + background: var(--hover-bg); +} + +/* CLOCKS */ +.clocks-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 10px; +} + +.clock { + padding: 12px; + border: 1px solid var(--border-thin-color); + text-align: center; +} + +.clock-time { + font-size: 16px; + font-weight: 700; + font-variant-numeric: tabular-nums; +} + +.clock-label { + font-size: 8px; + text-transform: uppercase; + letter-spacing: 2px; + color: var(--gray); + margin-top: 4px; +} + +/* HISTORY */ +.history-item { + padding: 10px 0; + border-bottom: 1px solid var(--border-thin-color); + display: flex; + justify-content: space-between; + align-items: center; + font-size: 12px; + cursor: pointer; + color: var(--gray); +} + +.history-item:hover { + color: var(--fg); +} + +.history-item:last-child { + border-bottom: none; +} + +.history-time { + font-size: 9px; +} + +/* SOURCES LIST */ +.sources-list { + display: flex; + flex-direction: column; + gap: 4px; +} + +.source-item { + padding: 10px 12px; + border: 1px solid var(--border-thin-color); + display: flex; + justify-content: space-between; + align-items: center; + font-size: 10px; + letter-spacing: 1px; + cursor: pointer; + transition: all 0.1s; +} + +.source-item:hover { + border-color: var(--fg); +} + +.source-item.active { + background: var(--fg); + color: var(--bg); +} + +.source-count { + font-weight: 700; +} + +/* WATCHLIST (Editable) */ +.watchlist-input-row { + display: flex; + margin-bottom: 10px; + border: 1px solid var(--border-thin-color); +} + +#watchlist-input { + flex: 1; + background: transparent; + border: none; + color: var(--fg); + font-family: inherit; + font-size: 10px; + padding: 8px; + outline: none; + text-transform: uppercase; +} + +#watchlist-add-btn { + padding: 0 10px; + background: var(--hover-bg); + border: none; + border-left: 1px solid var(--border-thin-color); + color: var(--fg); + cursor: pointer; + font-size: 14px; +} + +#watchlist-add-btn:hover { + background: var(--fg); + color: var(--bg); +} + +.watchlist-item-row { + padding: 10px 12px; + border: 1px solid var(--border-thin-color); + display: flex; + justify-content: space-between; + align-items: center; + font-size: 10px; + letter-spacing: 1px; + cursor: pointer; + transition: all 0.1s; + margin-bottom: 4px; +} + +.watchlist-item-row:hover { + border-color: var(--fg); +} + +.watchlist-del-btn { + color: var(--gray); + padding: 0 5px; + cursor: pointer; + font-size: 14px; +} + +.watchlist-del-btn:hover { + color: var(--fg); + font-weight: bold; +} + +/* RESULTS */ +.results-section { + min-height: 400px; +} + +.results-header { + display: flex; + justify-content: space-between; + padding: 15px 25px; + border-bottom: var(--border-thin); + font-size: 10px; + text-transform: uppercase; + letter-spacing: 2px; + color: var(--gray); +} + +.results-container { + display: flex; + flex-direction: column; +} + +/* STOCK WIDGET */ +.stock-widget { + border-bottom: var(--border); + background: var(--panel); +} + +.stock-header { + padding: 30px; + border-bottom: var(--border-thin); + display: flex; + justify-content: space-between; + align-items: center; +} + +.stock-info h2 { + font-size: 32px; + font-weight: 700; + letter-spacing: -1px; +} + +.stock-info .ticker { + font-size: 12px; + color: var(--gray); + letter-spacing: 2px; + margin-top: 5px; +} + +.stock-actions-top { + display: flex; + gap: 10px; +} + +.stock-btn { + padding: 10px 20px; + border: 2px solid var(--border-thin-color); + background: transparent; + color: var(--fg); + font-family: inherit; + font-size: 10px; + letter-spacing: 1px; + cursor: pointer; + transition: all 0.1s; +} + +.stock-btn:hover { + background: var(--fg); + color: var(--bg); + border-color: var(--fg); +} + +.tradingview-widget-container { + height: 500px; +} + +/* RESULT CARD */ +.result { + border-bottom: var(--border-thin); + display: grid; + grid-template-columns: 50px 1fr; + transition: background 0.1s; +} + +.result:hover { + background: var(--hover-bg); +} + +.result-index { + padding: 20px 15px; + border-right: var(--border-thin); + font-size: 11px; + color: var(--gray); + text-align: center; + font-weight: 700; +} + +.result-body { + padding: 20px 25px; +} + +.result-source { + font-size: 9px; + text-transform: uppercase; + letter-spacing: 2px; + color: var(--gray); + margin-bottom: 8px; +} + +.result-title { + font-size: 16px; + font-weight: 600; + letter-spacing: -0.5px; + margin-bottom: 8px; + line-height: 1.4; + cursor: pointer; +} + +.result-title:hover { + text-decoration: underline; + text-underline-offset: 3px; +} + +.result-url { + font-size: 11px; + color: var(--gray); + margin-bottom: 10px; + word-break: break-all; +} + +.result-snippet { + font-size: 12px; + line-height: 1.7; + color: var(--gray); +} + +.result-snippet mark { + background: var(--fg); + color: var(--bg); + padding: 0 2px; +} + +.result-meta { + display: flex; + gap: 15px; + margin-top: 12px; + font-size: 10px; + color: var(--gray); + text-transform: uppercase; + letter-spacing: 1px; +} + +/* INSTANT ANSWER */ +.instant-box { + border-bottom: var(--border); + padding: 40px; + background: var(--panel); +} + +.instant-label { + font-size: 9px; + text-transform: uppercase; + letter-spacing: 3px; + color: var(--gray); + margin-bottom: 20px; + display: flex; + align-items: center; + gap: 10px; +} + +.instant-label::before { + content: "■"; + animation: blink 0.5s infinite; +} + +.instant-title { + font-size: 28px; + font-weight: 700; + letter-spacing: -1px; + margin-bottom: 20px; + line-height: 1.2; +} + +.instant-content { + font-size: 14px; + line-height: 1.8; + color: var(--gray); + max-width: 700px; +} + +.instant-content p { + margin-bottom: 12px; +} + +.instant-meta { + margin-top: 25px; + padding-top: 20px; + border-top: var(--border-thin); + display: flex; + gap: 25px; + font-size: 10px; + text-transform: uppercase; + letter-spacing: 1px; + color: var(--gray); +} + +.instant-meta a { + border-bottom: 1px solid var(--gray); +} + +.instant-meta a:hover { + border-color: var(--fg); + color: var(--fg); +} + +/* EMPTY STATE */ +.empty-state { + padding: 60px 30px; + text-align: center; +} + +.empty-title { + font-size: 18px; + font-weight: 700; + margin-bottom: 15px; + color: var(--gray); +} + +.empty-text { + color: var(--gray); + font-size: 12px; + line-height: 1.8; +} + +/* LOADING */ +.loading-row { + display: flex; + padding: 25px; + border-bottom: var(--border-thin); + gap: 20px; + align-items: center; +} + +.loading-bar { + height: 2px; + background: var(--hover-bg); + flex: 1; + position: relative; + overflow: hidden; +} + +.loading-bar::after { + content: ""; + position: absolute; + left: -50%; + width: 50%; + height: 100%; + background: var(--fg); + animation: loading 1s infinite; +} + +@keyframes loading { + to { + left: 150%; + } +} + +.loading-text { + font-size: 10px; + text-transform: uppercase; + letter-spacing: 2px; + color: var(--gray); +} + +/* TOOL MODAL */ +.tool-modal { + padding: 30px; + background: var(--panel); +} + +.tool-modal textarea { + width: 100%; + padding: 15px; + background: var(--bg); + border: 2px solid var(--border-thin-color); + color: var(--fg); + font-family: inherit; + font-size: 13px; + resize: vertical; + min-height: 100px; + margin: 15px 0; +} + +.tool-modal textarea:focus { + outline: none; + border-color: var(--fg); +} + +.tool-btn-row { + display: flex; + gap: 10px; +} + +.tool-action { + padding: 12px 25px; + border: 2px solid var(--border-thin-color); + background: transparent; + color: var(--fg); + font-family: inherit; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 2px; + cursor: pointer; +} + +.tool-action:hover { + background: var(--fg); + color: var(--bg); +} + +.tool-output { + margin-top: 20px; + padding: 20px; + background: var(--bg); + border: 2px solid var(--border-thin-color); + font-size: 12px; + word-break: break-all; +} + +/* TOAST */ +.toast-container { + position: fixed; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + z-index: 3000; + display: flex; + flex-direction: column; + gap: 10px; +} + +.toast { + padding: 15px 30px; + border: 3px solid var(--fg); + background: var(--bg); + font-size: 12px; + letter-spacing: 2px; + animation: toastIn 0.2s ease; +} + +@keyframes toastIn { + from { + transform: translateY(20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +/* FOOTER */ +.footer { + border-top: var(--border); + padding: 20px 30px; + display: flex; + justify-content: center; + align-items: center; + gap: 40px; + font-size: 10px; + color: var(--gray); + text-transform: uppercase; + letter-spacing: 2px; +} + +.theme-btn { + background: transparent; + border: 1px solid var(--gray); + color: var(--gray); + padding: 5px 10px; + cursor: pointer; + font-family: inherit; + text-transform: uppercase; + letter-spacing: 1px; + font-size: 9px; +} + +.theme-btn:hover { + border-color: var(--fg); + color: var(--fg); +} + +/* SCROLLBAR */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: var(--bg); +} + +::-webkit-scrollbar-thumb { + background: #222; +} + +body.light-mode ::-webkit-scrollbar-thumb { + background: #ccc; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--gray); +} + +/* RESPONSIVE */ +@media (max-width: 768px) { + .logo-large { + font-size: 48px; + } + .hero.compact .logo-large { + display: none; + } + #search-input { + font-size: 18px; + padding: 20px; + } + .search-prefix { + padding: 20px; + font-size: 20px; + } + .main { + grid-template-columns: 1fr; + } +} |
