diff --git a/README.md b/README.md
index 9765d37..f754e38 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,36 @@
+
+
+
# Log Highlighter
[](https://github.com/al-af/LogHighlighter/actions/workflows/ci.yml)
A static, no-build browser tool for highlighting and filtering raw logs. Paste log text, define keyword groups (each gets a distinct pastel color), and the output panel highlights matches. Embedded JSON and Apple `NSDictionary` payloads inside log lines are detected and pretty-printed inline. Android `logcat` lines (threadtime, time, and brief formats) are detected and colored by severity (V/D/I/W/E/F/A), so iOS and Android logs both render usefully.
+## Quickstart
+
+**Try it instantly** on GitHub Pages: .
+
+**Use it in 30 seconds:**
+
+1. Pick a starter preset from the **Presets** dropdown — iOS SDK, Android SDK, React Native bridge, or OkHttp. The groups load immediately.
+2. Paste any log into the **Input** panel.
+3. Matching lines stay; non-matching lines collapse into `···` separators. Use the **Full** toggle to see everything.
+4. Press n / Shift+N to jump between matches. The counter next to **Output** shows your position.
+5. Click ? Guide in the header for the full feature tour.
+
+**Make it yours:**
+
+- Define your own keyword groups in the **Keyword Groups** panel (comma-separated, one chip per keyword).
+- Click + New preset to save a setup with a name. Pick it from the dropdown anytime to reload.
+- Click Share to copy a URL with your groups encoded — send it to a teammate. **Log content is never included.**
+
+**For teams:**
+
+- Share the GitHub Pages URL with your team — no install, no backend.
+- Use Share to standardize keyword setups across the team.
+- Click the moon / sun icon to switch between light and dark themes.
+
## Structure
```
diff --git a/assets/banner.png b/assets/banner.png
new file mode 100644
index 0000000..88deb91
Binary files /dev/null and b/assets/banner.png differ
diff --git a/assets/icon.png b/assets/icon.png
new file mode 100644
index 0000000..8edbc58
Binary files /dev/null and b/assets/icon.png differ
diff --git a/index.html b/index.html
index dea451c..5643a71 100644
--- a/index.html
+++ b/index.html
@@ -3,6 +3,8 @@
+ New preset — opens an empty editor where you give the preset a name and define its keyword groups (one group per line, comma-separated keywords).
Picking a preset from the dropdown applies it immediately, replacing your current groups.
× — delete the selected preset (with confirmation).
+
Four starter presets ship by default: iOS SDK, Android SDK, React Native bridge, OkHttp. Delete the ones you don't use — they won't come back on reload.
@@ -60,13 +61,18 @@ const HTML = `
F Fatal — dark red, bold
A Assert — purple tint
-
iOS-style log lines without a logcat level are unaffected.
+
The same severity scheme applies to Apple os_log output: Debug maps to Verbose, Info stays Info, Default/Notice become Debug-blue, Error stays Error, Fault stays Fatal.
+
Plain NSLog-style lines without an explicit level marker pass through uncolored.
Tips
Keep input under ~500 KB for smooth rendering.
State auto-saves to localStorage — refresh-safe.
+
Press n to jump to the next match in the output, Shift+N for the previous. The match count is shown next to the Output heading.
+
Line numbers reflect the original log line, not the filtered position — handy for sharing references like "see line 4823".
+
Click the moon / sun icon in the header to switch between light and dark themes. Your choice is remembered.
+
iOS os_log / log show output is auto-colored by severity (Debug, Info, Default/Notice, Error, Fault) using the same scheme as Android logcat.
Press Esc to close this guide.
diff --git a/src/lineNav.js b/src/lineNav.js
new file mode 100644
index 0000000..3b602de
--- /dev/null
+++ b/src/lineNav.js
@@ -0,0 +1,70 @@
+// Tracks the set of elements inside #output and exposes
+// next/prev navigation plus a count to render in the header slot.
+
+let currentIdx = -1;
+
+function getMarks() {
+ return Array.from(document.querySelectorAll('#output mark'));
+}
+
+function clearCurrent(marks) {
+ for (const m of marks) m.classList.remove('mark-current');
+}
+
+function applyCurrent(marks) {
+ if (currentIdx < 0 || currentIdx >= marks.length) return;
+ const el = marks[currentIdx];
+ el.classList.add('mark-current');
+ if (typeof el.scrollIntoView === 'function') {
+ el.scrollIntoView({ block: 'center', behavior: 'smooth' });
+ }
+}
+
+function renderCounter(total) {
+ const el = document.getElementById('matchCount');
+ if (!el) return;
+ if (total === 0) { el.hidden = true; el.textContent = ''; return; }
+ el.hidden = false;
+ el.textContent = currentIdx >= 0
+ ? `— ${currentIdx + 1} of ${total} matches`
+ : `— ${total} matches`;
+}
+
+export function refreshAfterRender() {
+ currentIdx = -1;
+ const marks = getMarks();
+ clearCurrent(marks);
+ renderCounter(marks.length);
+}
+
+function step(delta) {
+ const marks = getMarks();
+ if (marks.length === 0) return;
+ clearCurrent(marks);
+ currentIdx = currentIdx === -1
+ ? (delta > 0 ? 0 : marks.length - 1)
+ : (currentIdx + delta + marks.length) % marks.length;
+ applyCurrent(marks);
+ renderCounter(marks.length);
+}
+
+function isTypingTarget(el) {
+ if (!el) return false;
+ const tag = el.tagName;
+ return tag === 'INPUT' || tag === 'TEXTAREA' || el.isContentEditable;
+}
+
+let keyboardAttached = false;
+
+function onKeydown(e) {
+ if (isTypingTarget(e.target)) return;
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
+ if (e.key === 'n' && !e.shiftKey) { e.preventDefault(); step(1); }
+ else if (e.key === 'N' || (e.key === 'n' && e.shiftKey)) { e.preventDefault(); step(-1); }
+}
+
+export function attachKeyboard() {
+ if (keyboardAttached) return;
+ keyboardAttached = true;
+ document.addEventListener('keydown', onKeydown);
+}
diff --git a/src/main.js b/src/main.js
index 94417f6..5a6933b 100644
--- a/src/main.js
+++ b/src/main.js
@@ -7,6 +7,9 @@ import { listPresets, savePreset, loadPreset, deletePreset, onChange as onPreset
import { encodeGroups, consumeHash } from './share.js';
import { openGuide } from './guide.js';
import { openPresetEditor } from './presetEditor.js';
+import { seedStarterPresets } from './starterPresets.js';
+import { attachKeyboard } from './lineNav.js';
+import { applyInitialTheme, toggleTheme, syncToggleButton } from './theme.js';
const STATE_KEY = 'loghl:state';
@@ -193,6 +196,9 @@ document.getElementById('copyShareLink').addEventListener('click', copyShareLink
document.getElementById('openGuide').addEventListener('click', openGuide);
+const themeBtn = document.getElementById('toggleTheme');
+themeBtn.addEventListener('click', () => syncToggleButton(themeBtn, toggleTheme()));
+
window.addEventListener('hashchange', () => {
const r = consumeHash();
if (r.ok) {
@@ -209,9 +215,12 @@ window.addEventListener('hashchange', () => {
// ── initial render ──────────────────────────────────────────────────────────
+syncToggleButton(themeBtn, applyInitialTheme());
+seedStarterPresets();
renderPresetSelect();
syncModeButtons();
syncShareButton();
syncPresetSelectionButtons();
renderGroups();
renderOutput();
+attachKeyboard();
diff --git a/src/oslog.js b/src/oslog.js
new file mode 100644
index 0000000..429518b
--- /dev/null
+++ b/src/oslog.js
@@ -0,0 +1,27 @@
+// Detects Apple `os_log` / `log show` / `log stream` lines and maps their
+// level keywords to the same severity letters used by logcat so the
+// existing .lc-* CSS classes light up correctly.
+
+// Matches `log show --style syslog` and `log stream` output:
+// 2026-05-27 10:08:42.123456+0300 0x1a2b3c Default 0x0 1234 0 MyApp: hello
+const OSLOG_SYSLOG = /^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d+[+-]\d{4}\s+0x[\da-fA-F]+\s+(Debug|Info|Notice|Default|Error|Fault)\b/;
+
+// Matches `log show` "compact" / table output where the level is the
+// second whitespace-separated column right after the timestamp.
+// 2026-05-27 10:08:42.123456+0300 Default com.appsflyer.sdk ...
+const OSLOG_TABLE = /^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d+[+-]\d{4}\s+(Debug|Info|Notice|Default|Error|Fault)\b/;
+
+const LEVEL_MAP = {
+ Debug: 'V',
+ Info: 'I',
+ Notice: 'D', // legacy alias for Default in Apple's unified logging
+ Default: 'D',
+ Error: 'E',
+ Fault: 'F',
+};
+
+export function parseOSLog(line) {
+ const m = OSLOG_SYSLOG.exec(line) || OSLOG_TABLE.exec(line);
+ if (!m) return null;
+ return { level: LEVEL_MAP[m[1]] };
+}
diff --git a/src/output.js b/src/output.js
index 479fbc6..5c44f58 100644
--- a/src/output.js
+++ b/src/output.js
@@ -1,12 +1,14 @@
import { state } from './state.js';
import { detectPayload } from './payload.js';
import { parseLogcat } from './logcat.js';
+import { parseOSLog } from './oslog.js';
+import { refreshAfterRender } from './lineNav.js';
import { escapeHTML, highlight } from './highlight.js';
-function lineDiv(line, innerHTML) {
- const lc = parseLogcat(line);
- const cls = lc ? ` class="lc lc-${lc.level}"` : '';
- return `