aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPetri Hienonen <petri.hienonen@gmail.com>2025-12-14 11:22:29 +0200
committerPetri Hienonen <petri.hienonen@gmail.com>2025-12-14 11:22:29 +0200
commit91802c21d446175a02c820bcd243920a0791441b (patch)
tree959652e39710c735be6e3ecea1827aabe07f474b
downloadpage-91802c21d446175a02c820bcd243920a0791441b.tar.zst
Intial commit
-rw-r--r--biome.json100
-rw-r--r--flake.lock27
-rw-r--r--flake.nix28
-rw-r--r--index.html264
-rw-r--r--main.js972
-rw-r--r--style.css1022
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>
diff --git a/main.js b/main.js
new file mode 100644
index 0000000..8bba38e
--- /dev/null
+++ b/main.js
@@ -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) => ({ "'": "&#39;", '"': "&quot;", "&": "&amp;", "<": "&lt;", ">": "&gt;" })[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;
+ }
+}