main f6d8405

text-formatting.ts RawHistoryFolder

1// Font and styling configuration for chat messages
2
3// Retro Windows fonts
4// Primary font is Pixelated MS Sans Serif from xp.css which works for all users
5// Secondary fonts use local system fonts with proper fallbacks
6export const RETRO_FONTS = {
7	pixelated: {
8		name: 'Pixelated MS Sans Serif',
9		shortName: 'MS Sans Serif',
10		stack: '"Pixelated MS Sans Serif", "Tahoma", Geneva, sans-serif'
11	},
12	tahoma: {
13		name: 'Tahoma',
14		shortName: 'Tahoma',
15		stack: 'Tahoma, "Pixelated MS Sans Serif", Geneva, sans-serif'
16	},
17	verdana: {
18		name: 'Verdana',
19		shortName: 'Verdana',
20		stack: 'Verdana, "Pixelated MS Sans Serif", Geneva, sans-serif'
21	},
22	comicSans: {
23		name: 'Comic Sans MS',
24		shortName: 'Comic Sans',
25		stack: '"Comic Sans MS", "Pixelated MS Sans Serif", cursive, sans-serif'
26	},
27	timesNewRoman: {
28		name: 'Times New Roman',
29		shortName: 'Times',
30		stack: '"Times New Roman", Times, serif'
31	},
32	arial: {
33		name: 'Arial',
34		shortName: 'Arial',
35		stack: 'Arial, "Pixelated MS Sans Serif", Helvetica, sans-serif'
36	},
37	courierNew: {
38		name: 'Courier New',
39		shortName: 'Courier',
40		stack: '"Courier New", "Perfect DOS VGA 437 Win", Courier, monospace'
41	},
42	dos: {
43		name: 'Perfect DOS VGA 437',
44		shortName: 'DOS VGA',
45		stack: '"Perfect DOS VGA 437 Win", "Courier New", Courier, monospace'
46	}
47} as const;
48
49export type FontFamily = keyof typeof RETRO_FONTS;
50
51// Generate 256-color web-safe palette ordered by hue
52export function generate256Colors(): string[] {
53	const colors: string[] = [];
54
55	// Start with grayscale colors
56	colors.push('#000000', '#333333', '#666666', '#999999', '#CCCCCC', '#FFFFFF');
57
58	// Then add colors in a more logical order by hue
59	// Red hues
60	colors.push('#330000', '#660000', '#990000', '#CC0000', '#FF0000');
61	colors.push('#FF3333', '#FF6666', '#FF9999', '#FFCCCC');
62
63	// Orange hues
64	colors.push('#663300', '#993300', '#CC3300', '#FF3300');
65	colors.push('#FF6600', '#FF9900', '#FFCC00', '#FFCC33', '#FFCC66', '#FFCC99');
66
67	// Yellow hues
68	colors.push('#666600', '#999900', '#CCCC00', '#FFFF00');
69	colors.push('#FFFF33', '#FFFF66', '#FFFF99', '#FFFFCC');
70
71	// Green hues
72	colors.push('#003300', '#006600', '#009900', '#00CC00', '#00FF00');
73	colors.push('#33FF33', '#66FF66', '#99FF99', '#CCFFCC');
74	colors.push('#336633', '#669966', '#99CC99');
75
76	// Cyan hues
77	colors.push('#006666', '#009999', '#00CCCC', '#00FFFF');
78	colors.push('#33FFFF', '#66FFFF', '#99FFFF', '#CCFFFF');
79	colors.push('#336666', '#669999', '#99CCCC');
80
81	// Blue hues
82	colors.push('#000033', '#000066', '#000099', '#0000CC', '#0000FF');
83	colors.push('#3333FF', '#6666FF', '#9999FF', '#CCCCFF');
84	colors.push('#003366', '#006699', '#0099CC', '#3399FF', '#66CCFF');
85
86	// Purple hues
87	colors.push('#330033', '#660066', '#990099', '#CC00CC', '#FF00FF');
88	colors.push('#FF33FF', '#FF66FF', '#FF99FF', '#FFCCFF');
89	colors.push('#663366', '#996699', '#CC99CC');
90
91	// Pink hues
92	colors.push('#660033', '#990066', '#CC0099', '#FF0099');
93	colors.push('#FF3399', '#FF66CC', '#FF99CC');
94
95	// Browns
96	colors.push('#663333', '#996633', '#CC9966', '#996666');
97
98	// Now fill in any missing combinations to get full 216 web-safe colors
99	const webSafeValues = ['00', '33', '66', '99', 'CC', 'FF'];
100	const existingColors = new Set(colors);
101
102	for (const r of webSafeValues) {
103		for (const g of webSafeValues) {
104			for (const b of webSafeValues) {
105				const color = `#${r}${g}${b}`.toUpperCase();
106				if (!existingColors.has(color)) {
107					colors.push(color);
108				}
109			}
110		}
111	}
112
113	return colors;
114}
115
116// Classic retro colors for backward compatibility
117export const CLASSIC_COLORS = {
118	black: '#000000',
119	darkBlue: '#000080',
120	green: '#008000',
121	teal: '#008080',
122	darkRed: '#800000',
123	purple: '#800080',
124	brown: '#808000',
125	silver: '#C0C0C0',
126	gray: '#808080',
127	blue: '#0000FF',
128	lime: '#00FF00',
129	aqua: '#00FFFF',
130	red: '#FF0000',
131	fuchsia: '#FF00FF',
132	yellow: '#FFFF00',
133	white: '#FFFFFF'
134} as const;
135
136export type ClassicColor = keyof typeof CLASSIC_COLORS;
137
138// Gradient presets inspired by AOL faders
139export const GRADIENT_PRESETS = {
140	rainbow: [
141		'#FF0000',
142		'#FF8000',
143		'#FFFF00',
144		'#80FF00',
145		'#00FF00',
146		'#00FF80',
147		'#00FFFF',
148		'#0080FF',
149		'#0000FF',
150		'#8000FF',
151		'#FF00FF',
152		'#FF0080'
153	],
154	fire: ['#FF0000', '#FF4000', '#FF8000', '#FFBF00', '#FFFF00'],
155	ocean: ['#000080', '#0040FF', '#0080FF', '#00BFFF', '#00FFFF'],
156	sunset: ['#FF4500', '#FF6347', '#FFD700', '#FFFF00'],
157	neon: ['#FF00FF', '#8000FF', '#0080FF', '#00FFFF'],
158	forest: ['#006400', '#228B22', '#32CD32', '#90EE90'],
159	royal: ['#4B0082', '#6A5ACD', '#9370DB', '#DDA0DD']
160} as const;
161
162export type GradientPreset = keyof typeof GRADIENT_PRESETS;
163
164// Text styling options
165export interface TextStyle {
166	fontFamily: FontFamily;
167	fontSize: number; // in pixels
168	color?: string; // hex color string
169	gradient?: string[]; // array of 1-3 hex colors for gradients
170	bold: boolean;
171	italic: boolean;
172	underline: boolean;
173	strikethrough: boolean;
174}
175
176// Default text style
177export const DEFAULT_TEXT_STYLE: TextStyle = {
178	fontFamily: 'pixelated',
179	fontSize: 14,
180	color: '#000000',
181	bold: false,
182	italic: false,
183	underline: false,
184	strikethrough: false
185};
186
187// User preferences for text formatting
188export interface UserTextPreferences {
189	defaultStyle: TextStyle;
190	allowFormatting: boolean;
191	maxMessageLength: number;
192}
193
194// Default user preferences
195export const DEFAULT_USER_PREFERENCES: UserTextPreferences = {
196	defaultStyle: DEFAULT_TEXT_STYLE,
197	allowFormatting: true,
198	maxMessageLength: 500
199};
200
201// Hex color validation
202export function isValidHexColor(color: string): boolean {
203	return /^#[0-9A-Fa-f]{6}$/.test(color);
204}
205
206// Formatting validation
207export function validateTextStyle(style: Partial<TextStyle>): TextStyle {
208	return {
209		fontFamily:
210			style.fontFamily && style.fontFamily in RETRO_FONTS
211				? style.fontFamily
212				: DEFAULT_TEXT_STYLE.fontFamily,
213		fontSize: Math.max(8, Math.min(18, style.fontSize || DEFAULT_TEXT_STYLE.fontSize)),
214		color: style.color && isValidHexColor(style.color) ? style.color : DEFAULT_TEXT_STYLE.color,
215		gradient:
216			style.gradient &&
217			Array.isArray(style.gradient) &&
218			style.gradient.length <= 3 &&
219			style.gradient.every(isValidHexColor)
220				? style.gradient
221				: undefined,
222		bold: Boolean(style.bold),
223		italic: Boolean(style.italic),
224		underline: Boolean(style.underline),
225		strikethrough: Boolean(style.strikethrough)
226	};
227}
228
229// Generate CSS style string from TextStyle
230export function generateCSSStyle(style: TextStyle): string {
231	const styles: string[] = [];
232
233	// Font family
234	styles.push(`font-family: ${RETRO_FONTS[style.fontFamily].stack}`);
235
236	// Font size
237	styles.push(`font-size: ${style.fontSize}px`);
238
239	// Color or gradient
240	if (style.gradient && style.gradient.length > 1) {
241		const gradientStops = style.gradient.join(', ');
242		styles.push(`background: linear-gradient(to right, ${gradientStops})`);
243		styles.push(`-webkit-background-clip: text`);
244		styles.push(`-webkit-text-fill-color: transparent`);
245		styles.push(`background-clip: text`);
246	} else if (style.color) {
247		styles.push(`color: ${style.color}`);
248	}
249
250	// Font weight
251	if (style.bold) {
252		// Use correct weight for Tahoma (200) vs standard bold (700)
253		if (style.fontFamily === 'tahoma') {
254			styles.push(`font-weight: 200`);
255		} else {
256			styles.push(`font-weight: 700`);
257		}
258	}
259
260	// Font style
261	if (style.italic) {
262		styles.push(`font-style: italic`);
263	}
264
265	// Text decoration
266	const decorations: string[] = [];
267	if (style.underline) decorations.push('underline');
268	if (style.strikethrough) decorations.push('line-through');
269	if (decorations.length > 0) {
270		styles.push(`text-decoration: ${decorations.join(' ')}`);
271	}
272
273	return styles.join('; ');
274}
275
276// Generate CSS style string for input fields (without font-family which should be handled by classes)
277export function generateInputCSSStyle(style: TextStyle): string {
278	const styles: string[] = [];
279
280	// Font size
281	styles.push(`font-size: ${style.fontSize}px`);
282
283	// Color handling for inputs
284	// IMPORTANT: Always set -webkit-text-fill-color to ensure proper color reset when switching from gradient to solid
285	if (style.gradient && style.gradient.length > 1) {
286		// Apply gradient background with text clipping for gradient effect
287		const gradientStops = style.gradient.join(', ');
288		styles.push(`background: linear-gradient(to right, ${gradientStops})`);
289		styles.push(`-webkit-background-clip: text`);
290		styles.push(`-webkit-text-fill-color: transparent`);
291		styles.push(`background-clip: text`);
292		styles.push(`letter-spacing: 0`);
293		styles.push(`word-spacing: 0`);
294	} else if (style.gradient && style.gradient.length === 1) {
295		// Single color from gradient array
296		const color = style.gradient[0];
297		styles.push(`color: ${color}`);
298		styles.push(`-webkit-text-fill-color: ${color}`);
299	} else if (style.color) {
300		styles.push(`color: ${style.color}`);
301		styles.push(`-webkit-text-fill-color: ${style.color}`);
302	} else {
303		// Fallback to black if no color is set
304		styles.push(`color: #000000`);
305		styles.push(`-webkit-text-fill-color: #000000`);
306	}
307
308	// Font weight
309	if (style.bold) {
310		// Use correct weight for Tahoma (200) vs standard bold (700)
311		if (style.fontFamily === 'tahoma') {
312			styles.push(`font-weight: 200`);
313		} else {
314			styles.push(`font-weight: 700`);
315		}
316	}
317
318	// Font style
319	if (style.italic) {
320		styles.push(`font-style: italic`);
321	}
322
323	// Text decoration
324	const decorations: string[] = [];
325	if (style.underline) decorations.push('underline');
326	if (style.strikethrough) decorations.push('line-through');
327	if (decorations.length > 0) {
328		styles.push(`text-decoration: ${decorations.join(' ')}`);
329	}
330
331	return styles.join('; ');
332}

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