main f6d8405

text-formatting-toolbar.svelte RawHistoryFolder

1<script lang="ts">
2	import {
3		RETRO_FONTS,
4		DEFAULT_TEXT_STYLE,
5		type TextStyle,
6		type FontFamily
7	} from '../types/text-formatting';
8	import ColorPicker256 from './color-picker-256.svelte';
9	import { SMILEYS } from '$lib/aim/smileys';
10
11	// Props
12	let {
13		style = $bindable(DEFAULT_TEXT_STYLE),
14		compact = true,
15		showFontSelector = false,
16		onSmiley
17	} = $props<{
18		style?: TextStyle;
19		compact?: boolean;
20		showFontSelector?: boolean;
21		/** When set, shows the AIM smiley picker and inserts the chosen code */
22		onSmiley?: (code: string) => void;
23	}>();
24
25	let smileyOpen = $state(false);
26
27	// State
28	let currentColor = $state(style.color || '#000000');
29	let currentGradient = $state(style.gradient);
30
31	// Toggle functions
32	function toggleBold() {
33		style = { ...style, bold: !style.bold };
34	}
35
36	function toggleItalic() {
37		style = { ...style, italic: !style.italic };
38	}
39
40	function toggleUnderline() {
41		style = { ...style, underline: !style.underline };
42	}
43
44	function handleColorChange(color: string) {
45		currentColor = color;
46		style = { ...style, color, gradient: undefined };
47	}
48
49	function handleGradientChange(gradient: string[]) {
50		currentGradient = gradient;
51		style = { ...style, gradient, color: gradient[0] };
52	}
53
54	function selectFont(fontFamily: FontFamily) {
55		style = { ...style, fontFamily };
56	}
57
58	// Keyboard shortcuts handler
59	function handleKeydown(event: KeyboardEvent) {
60		if (event.ctrlKey || event.metaKey) {
61			switch (event.key.toLowerCase()) {
62				case 'b':
63					event.preventDefault();
64					toggleBold();
65					break;
66				case 'i':
67					event.preventDefault();
68					toggleItalic();
69					break;
70				case 'u':
71					event.preventDefault();
72					toggleUnderline();
73					break;
74			}
75		}
76	}
77
78	// Update internal state when style prop changes
79	$effect(() => {
80		currentColor = style.color || '#000000';
81		currentGradient = style.gradient;
82	});
83</script>
84
85<svelte:window onkeydown={handleKeydown} />
86
87<div class="text-formatting-toolbar sunken-panel" class:compact>
88	<!-- Font selector (if enabled) -->
89	{#if showFontSelector}
90		<select
91			value={style.fontFamily}
92			onchange={(e) => selectFont((e.target as HTMLSelectElement).value as FontFamily)}
93			title="Font Family"
94		>
95			{#each Object.entries(RETRO_FONTS) as [key, font] (key)}
96				<option value={key}>{font.shortName}</option>
97			{/each}
98		</select>
99	{/if}
100
101	<!-- Size slider -->
102	<div class="size-control">
103		<input
104			type="range"
105			min="8"
106			max="18"
107			bind:value={style.fontSize}
108			class="size-slider"
109			title="Font Size: {style.fontSize}px"
110		/>
111		<span class="size-display">{style.fontSize}</span>
112	</div>
113
114	<!-- Style toggle buttons -->
115	<div class="style-toggles">
116		<button
117			onclick={toggleBold}
118			title="Bold (Ctrl+B)"
119			style="width: 20px; height: 20px; padding: 0; font-size: 10px;"
120		>
121			<strong>B</strong>
122		</button>
123
124		<button
125			onclick={toggleItalic}
126			title="Italic (Ctrl+I)"
127			style="width: 20px; height: 20px; padding: 0; font-size: 10px;"
128		>
129			<em>I</em>
130		</button>
131
132		<button
133			onclick={toggleUnderline}
134			title="Underline (Ctrl+U)"
135			style="width: 20px; height: 20px; padding: 0; font-size: 10px;"
136		>
137			<u>U</u>
138		</button>
139	</div>
140
141	{#if onSmiley}
142		<div class="smiley-control">
143			<button
144				type="button"
145				title="Smileys"
146				aria-haspopup="true"
147				aria-expanded={smileyOpen}
148				onclick={() => (smileyOpen = !smileyOpen)}
149				class="smiley-trigger"
150			>
151				<img src="/smileys/01.gif" alt="🙂" width="15" height="15" />
152			</button>
153			{#if smileyOpen}
154				<div class="smiley-picker" role="menu">
155					{#each SMILEYS as smiley (smiley.file)}
156						<button
157							type="button"
158							role="menuitem"
159							title="{smiley.label}  {smiley.codes[0]}"
160							onclick={() => {
161								smileyOpen = false;
162								onSmiley(smiley.codes[0]);
163							}}
164						>
165							<img src="/smileys/{smiley.file}.gif" alt={smiley.emoji} width="19" height="19" />
166						</button>
167					{/each}
168				</div>
169			{/if}
170		</div>
171	{/if}
172
173	<!-- Color controls -->
174	<div class="color-controls">
175		<ColorPicker256
176			bind:selectedColor={currentColor}
177			bind:selectedGradient={currentGradient}
178			onColorChange={handleColorChange}
179			onGradientChange={handleGradientChange}
180		/>
181	</div>
182</div>
183
184<style>
185	.text-formatting-toolbar {
186		display: flex;
187		align-items: center;
188		gap: 4px;
189		padding: 2px;
190		font-size: 11px;
191		position: relative;
192		flex-wrap: wrap;
193	}
194
195	.size-control {
196		display: flex;
197		align-items: center;
198		gap: 2px;
199	}
200
201	.size-slider {
202		width: 50px;
203		height: 12px;
204	}
205
206	.size-display {
207		font-size: 9px;
208		color: #666;
209		min-width: 18px;
210		text-align: center;
211	}
212
213	.style-toggles {
214		display: flex;
215		gap: 1px;
216	}
217
218	/* xp.css gives every button min-width: 75px; keep toolbar buttons square */
219	.style-toggles button,
220	.color-controls :global(.color-picker-container > button) {
221		width: 22px !important;
222		height: 20px !important;
223		min-width: 0;
224		min-height: 0;
225		padding: 0 !important;
226		display: flex;
227		align-items: center;
228		justify-content: center;
229	}
230
231	.smiley-control {
232		position: relative;
233		display: flex;
234	}
235	.smiley-trigger {
236		width: 24px;
237		height: 20px;
238		min-width: 0;
239		min-height: 0;
240		padding: 0;
241		display: flex;
242		align-items: center;
243		justify-content: center;
244		line-height: 0;
245	}
246	.smiley-control img {
247		display: block;
248		image-rendering: pixelated;
249	}
250	.smiley-picker {
251		position: absolute;
252		bottom: calc(100% + 2px);
253		left: 0;
254		z-index: 60;
255		display: grid;
256		grid-template-columns: repeat(4, 25px);
257		gap: 1px;
258		padding: 3px;
259		background: #fff;
260		border: 1px solid #7f9db9;
261		box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
262	}
263	.smiley-picker button {
264		width: 25px;
265		height: 25px;
266		min-width: 0;
267		min-height: 0;
268		padding: 0;
269		background: transparent;
270		box-shadow: none;
271		border: 1px solid transparent;
272		display: flex;
273		align-items: center;
274		justify-content: center;
275	}
276	.smiley-picker button:hover {
277		border-color: #316ac5;
278		background: #e8eefb;
279	}
280	.color-controls {
281		display: flex;
282		gap: 1px;
283	}
284
285	/* Fix font dropdown text alignment and overflow */
286	select {
287		height: 20px;
288		line-height: 1;
289		vertical-align: middle;
290		padding: 1px 21px 1px 4px;
291		font-size: 11px;
292		max-width: 120px;
293		overflow: hidden;
294		text-overflow: ellipsis;
295		white-space: nowrap;
296	}
297
298	select option {
299		padding: 2px 4px;
300		line-height: normal;
301		font-family: inherit;
302	}
303</style>

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 3

Tags 8

↑↓ moveenter openesc close