Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 14 additions & 13 deletions src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,12 @@ body {
-moz-osx-font-smoothing: grayscale;
}

#root { width: 100%; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
/* height:100% (cascading html→body→#root), NOT 100vh: `vh` ignores the
`html { zoom: 1.2 }` above, so 100vh renders 1.2× too tall (≈120vh) and the
whole shell overflows the viewport — pushing the results pane's lower rows
below the fold with no page scroll (html is overflow:hidden). % heights scale
with zoom correctly. */
#root { width: 100%; height: 100%; display: flex; flex-direction: column; overflow: hidden; }

/* ------------ login ------------ */
.login-screen {
Expand Down Expand Up @@ -179,6 +184,7 @@ body {
cursor: pointer;
display: flex; align-items: center; justify-content: center;
border-radius: 5px;
text-decoration: none; /* also used as an <a> (GitHub source link) */
}
.hd-btn:hover { background: var(--bg-hover); color: var(--fg); }
.hd-btn.text { width: auto; padding: 0 10px; font-size: 11.5px; font-family: inherit; border: 1px solid var(--border); }
Expand Down Expand Up @@ -255,15 +261,6 @@ body {
}
.saved-row:hover .del { display: inline-flex; }
.saved-row .del:hover { color: var(--fg); background: var(--bg-hover); }
.list-head {
display: flex; justify-content: flex-end;
padding: 5px 8px; border-bottom: 1px solid var(--border-faint);
}
.clear-btn {
border: none; background: transparent; color: var(--fg-faint);
font-size: 10.5px; cursor: pointer; padding: 3px 7px; border-radius: 4px;
}
.clear-btn:hover { color: var(--error-fg); background: var(--bg-hover); }
.history-row {
position: relative;
padding: 8px 10px; cursor: pointer; user-select: none;
Expand Down Expand Up @@ -355,19 +352,23 @@ body {
.result-view-tab.active { color: var(--fg); background: var(--bg-hover); border-color: var(--border); }

/* ------------ json view ------------ */
/* height:100% (not flex:1) — .res-body is a relatively-positioned block, not a
flex container, so flex had no effect and the pane grew to content height with
no vertical scroll (only horizontal worked). A bounded height + overflow:auto
scrolls both axes, matching .res-table-wrap. */
.json-view {
flex: 1; overflow: auto; padding: 12px 14px;
height: 100%; overflow: auto; padding: 12px 14px;
font-family: var(--mono); font-size: 11.5px;
color: var(--fg); white-space: pre;
background: var(--bg);
}
.raw-text-view {
flex: 1; overflow: auto; padding: 10px 12px;
height: 100%; overflow: auto; padding: 10px 12px;
font-family: var(--mono); font-size: 11.5px;
color: var(--fg); white-space: pre;
background: var(--bg);
}
/* Focusable (tabindex) so a click scopes ⌘A to the pane's text; no focus ring. */
/* tabindex makes the pane keyboard-scrollable; suppress the focus ring. */
.raw-text-view:focus, .json-view:focus { outline: none; }

/* ------------ chart placeholder ------------ */
Expand Down
6 changes: 5 additions & 1 deletion src/ui/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,11 @@ export function renderApp(app, helpers) {
h('button', { class: 'hd-btn', title: 'Keyboard shortcuts (?)', onclick: () => app.actions.openShortcuts() }, Icon.shortcuts()),
app.dom.themeBtn,
h('div', { class: 'user-email', title: app.email() }, app.email()),
h('button', { class: 'hd-btn text', title: 'Sign out', onclick: () => app.signOut() }, 'Sign out'));
h('button', { class: 'hd-btn text', title: 'Log out', onclick: () => app.signOut() }, 'Log Out'),
h('a', {
class: 'hd-btn', href: 'https://github.com/Altinity/altinity-sql-browser',
target: '_blank', rel: 'noopener noreferrer', title: 'View source on GitHub',
}, Icon.github()));

app.dom.schemaSearchInput = h('input', {
type: 'text', placeholder: 'Search tables, columns…',
Expand Down
8 changes: 5 additions & 3 deletions src/ui/icons.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@ export function svg(d, w = 12, hgt = 12, opts = {}) {
return el;
}

/** Single-path filled icon. */
export function svgFilled(d, w = 12, hgt = 12) {
/** Single-path filled icon. `vbW`/`vbH` default to the display size, but can
* differ when the path is authored in a different coordinate space. */
export function svgFilled(d, w = 12, hgt = 12, vbW = w, vbH = hgt) {
const el = document.createElementNS(NS, 'svg');
el.setAttribute('width', w);
el.setAttribute('height', hgt);
el.setAttribute('viewBox', `0 0 ${w} ${hgt}`);
el.setAttribute('viewBox', `0 0 ${vbW} ${vbH}`);
el.setAttribute('fill', 'currentColor');
const path = document.createElementNS(NS, 'path');
path.setAttribute('d', d);
Expand Down Expand Up @@ -78,4 +79,5 @@ export const Icon = {
json: () => svg('M4 1.5C2.5 1.5 2.5 3 2.5 4S2.5 5 1.5 6c1 1 1 2 1 2s0 1.5 1.5 1.5M8 1.5c1.5 0 1.5 1.5 1.5 2.5s0 1 1 2c-1 1-1 2-1 2s0 1.5-1.5 1.5', 12, 12),
table2: () => iconEl('<rect x="1.5" y="2" width="9" height="8" rx=".5"/><path d="M1.5 4.5h9M1.5 7h9M4.5 4.5v5"/>', 12, 12),
shortcuts: () => iconEl('<rect x="1.5" y="3" width="9" height="6" rx="1"/><path d="M3.5 5h.01M6 5h.01M8.5 5h.01M3.5 7h5"/>', 12, 12, 1.3),
github: () => svgFilled('M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12', 15, 15, 24, 24),
};
7 changes: 1 addition & 6 deletions src/ui/saved-history.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { h } from './dom.js';
import { Icon } from './icons.js';
import { timeAgo } from '../core/format.js';
import { deleteSaved, deleteHistory, clearHistory } from '../state.js';
import { deleteSaved, deleteHistory } from '../state.js';

export function renderSavedHistory(app) {
const tabsRow = app.dom.savedTabsRow;
Expand Down Expand Up @@ -53,11 +53,6 @@ function renderHistory(app, list) {
list.appendChild(h('div', { class: 'saved-empty' }, 'No history yet.'));
return;
}
list.appendChild(h('div', { class: 'list-head' },
h('button', {
class: 'clear-btn', title: 'Clear all history',
onclick: () => { clearHistory(state, app.saveJSON); renderSavedHistory(app); },
}, 'Clear history')));
for (const ent of state.history) {
list.appendChild(h('div', { class: 'history-row', onclick: () => app.actions.loadIntoNewTab('From history', ent.sql) },
h('button', {
Expand Down
12 changes: 8 additions & 4 deletions src/ui/shortcuts.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,15 @@ export function handleKeydown(e, app) {
return 'toggleSaved';
}
if (mod && e.key.toLowerCase() === 'a') {
// Inside a raw result pane (TSV / JSON output), select just that text so it
// can be copied — not the whole page. Elsewhere (e.g. the editor textarea)
// fall through to the browser's native select-all.
// When a raw result pane (TSV / JSON output) is on screen and the user isn't
// typing, ⌘/Ctrl+A selects just that text so it can be copied — not the whole
// page. Keyed off "not editing + pane present" rather than pane focus, because
// macOS WebKit doesn't focus a tabindex <div> on click (so e.target stays
// <body>). A focused editor/input keeps the native select-all (whole query).
const t = e.target;
const box = t && t.closest && t.closest('.raw-text-view, .json-view');
if (t && (t.tagName === 'TEXTAREA' || t.tagName === 'INPUT' || t.isContentEditable)) return null;
const doc = (t && t.ownerDocument) || document;
const box = doc.querySelector('.raw-text-view, .json-view');
if (!box) return null;
e.preventDefault();
box.ownerDocument.defaultView.getSelection().selectAllChildren(box);
Expand Down
9 changes: 9 additions & 0 deletions tests/unit/app.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,15 @@ describe('renderApp shell', () => {
expect(e.sessionStorage.getItem('oauth_id_token')).toBeNull();
expect(app.root.querySelector('.login-screen')).not.toBeNull();
});
it('header has a Log Out button and a GitHub source link', () => {
const { app } = rendered();
expect(app.root.querySelector('.hd-btn.text').textContent).toContain('Log Out');
const gh = app.root.querySelector('a.hd-btn[href*="github.com"]');
expect(gh).not.toBeNull();
expect(gh.getAttribute('target')).toBe('_blank');
expect(gh.getAttribute('rel')).toContain('noopener');
expect(gh.querySelector('svg')).not.toBeNull();
});
it('setTokens clears the one-shot pkce verifier and csrf state', () => {
const e = env({ sessionStorage: memSession({ oauth_verifier: 'v', oauth_state: 's' }) });
const app = createApp(e);
Expand Down
6 changes: 6 additions & 0 deletions tests/unit/icons.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ describe('svgFilled', () => {
const el = svgFilled('M0 0z', 16, 16);
expect(el.getAttribute('fill')).toBe('currentColor');
expect(el.getAttribute('width')).toBe('16');
expect(el.getAttribute('viewBox')).toBe('0 0 16 16');
});
it('honours an explicit viewBox distinct from the display size', () => {
const el = svgFilled('M0 0z', 15, 15, 24, 24);
expect(el.getAttribute('width')).toBe('15');
expect(el.getAttribute('viewBox')).toBe('0 0 24 24');
});
});

Expand Down
11 changes: 0 additions & 11 deletions tests/unit/saved-history.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,17 +70,6 @@ describe('renderSavedHistory', () => {
expect(app.dom.savedList.querySelectorAll('.history-row')).toHaveLength(1);
});

it('history: clear button empties all history', () => {
const app = makeApp();
app.state.sidePanel = 'history';
app.state.history = [{ id: 'h1', sql: 'SELECT 1', ts: Date.now(), rows: 3, ms: 4 }];
renderSavedHistory(app);
click(app.dom.savedList.querySelector('.clear-btn'));
expect(app.state.history).toEqual([]);
expect(app.saveJSON).toHaveBeenCalled();
expect(app.dom.savedList.textContent).toContain('No history yet.');
});

it('switching panels persists the choice', () => {
const app = makeApp();
app.state.sidePanel = 'saved';
Expand Down
16 changes: 11 additions & 5 deletions tests/unit/shortcuts.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,26 +90,32 @@ describe('handleKeydown', () => {
expect(handleKeydown(ev({ key: '?', target: null }), makeApp())).toBe('shortcuts');
});

it('⌘A inside a raw result pane selects just that text', () => {
it('⌘A selects a raw result pane even when it is not focused (macOS body target)', () => {
const app = makeApp();
const box = document.createElement('div');
box.className = 'raw-text-view';
box.textContent = 'a\tb\nc\td';
document.body.appendChild(box);
const e = ev({ metaKey: true, key: 'a', target: box });
// target is <body> (pane not focused — the macOS WebKit case), pane on screen
const e = ev({ metaKey: true, key: 'a', target: document.body });
expect(handleKeydown(e, app)).toBe('selectAll');
expect(e.preventDefault).toHaveBeenCalled();
expect(box.ownerDocument.defaultView.getSelection().toString()).toBe('a\tb\nc\td');
});

it('⌘A elsewhere falls through to the native select-all', () => {
it('⌘A while editing keeps the native select-all (editor / inputs)', () => {
const app = makeApp();
// editor textarea (no raw-pane ancestor) → not handled
document.body.appendChild(document.createElement('div')).className = 'raw-text-view';
const ta = document.createElement('textarea');
const e = ev({ metaKey: true, key: 'A', target: ta });
expect(handleKeydown(e, app)).toBeNull();
expect(e.preventDefault).not.toHaveBeenCalled();
// no target at all
expect(handleKeydown(ev({ metaKey: true, key: 'a', target: { tagName: 'INPUT' } }), app)).toBeNull();
expect(handleKeydown(ev({ metaKey: true, key: 'a', target: { isContentEditable: true } }), app)).toBeNull();
});

it('⌘A with no raw pane on screen falls through to native select-all', () => {
const app = makeApp();
expect(handleKeydown(ev({ metaKey: true, key: 'a', target: null }), app)).toBeNull();
});
});
Loading