forked from cory/tildefriends
		
	Compare commits
	
		
			10 Commits
		
	
	
		
			main
			...
			user_setti
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 58dbf42a3a | |||
| a1f221879b | |||
| 2a928dcafc | |||
| 5474c5a101 | |||
| 4b7261fa20 | |||
| 4992ff3a2d | |||
| 88ee0aa6f0 | |||
| 392206c19e | |||
| f9e95e5733 | |||
| 1444c945de | 
| @@ -1,5 +0,0 @@ | ||||
| { | ||||
| 	"type": "tildefriends-app", | ||||
| 	"emoji": "🪪", | ||||
| 	"previous": "&kgukkyDk1RxgfzgMH6H/0QeDPIuwPZypLuAFax21ljk=.sha256" | ||||
| } | ||||
| @@ -1,93 +0,0 @@ | ||||
| import * as tfrpc from '/tfrpc.js'; | ||||
|  | ||||
| tfrpc.register(async function get_private_key(id) { | ||||
| 	return bip39Words(await ssb.getPrivateKey(id)); | ||||
| }); | ||||
| tfrpc.register(async function create_id(id) { | ||||
| 	return await ssb.createIdentity(); | ||||
| }); | ||||
| tfrpc.register(async function add_id(id) { | ||||
| 	return await ssb.addIdentity(bip39Bytes(id)); | ||||
| }); | ||||
| tfrpc.register(async function delete_id(id) { | ||||
| 	return await ssb.deleteIdentity(id); | ||||
| }); | ||||
| tfrpc.register(async function reload() { | ||||
| 	await main(); | ||||
| }); | ||||
|  | ||||
| async function main() { | ||||
| 	let ids = await ssb.getIdentities(); | ||||
| 	await app.setDocument( | ||||
| 		`<body style="color: #fff"> | ||||
| 		<script>const handler = {};</script> | ||||
| 		<script type="module"> | ||||
| 			import * as tfrpc from '/static/tfrpc.js'; | ||||
| 			handler.export_id = async function export_id(event) { | ||||
| 				let id = event.srcElement.dataset.id; | ||||
| 				let element = document.createElement('textarea'); | ||||
| 				element.value = await tfrpc.rpc.get_private_key(id); | ||||
| 				element.style = 'width: 100%; read-only: true'; | ||||
| 				element.readOnly = true; | ||||
| 				event.srcElement.parentElement.appendChild(element); | ||||
| 				event.srcElement.onclick = event => handler.hide_id(event, element); | ||||
| 			} | ||||
| 			handler.add_id = async function add_id(event) { | ||||
| 				let id = document.getElementById('add_id').value; | ||||
| 				try { | ||||
| 					let new_id = await tfrpc.rpc.add_id(id); | ||||
| 					alert('Successfully imported: ' + new_id); | ||||
| 					await tfrpc.rpc.reload(); | ||||
| 				} catch (e) { | ||||
| 					alert('Error importing identity: ' + e); | ||||
| 				} | ||||
| 			} | ||||
| 			handler.create_id = async function create_id(event) { | ||||
| 				try { | ||||
| 					let id = await tfrpc.rpc.create_id(); | ||||
| 					alert('Successfully created: ' + id); | ||||
| 					await tfrpc.rpc.reload(); | ||||
| 				} catch (e) { | ||||
| 					alert('Error creating identity: ' + e); | ||||
| 				} | ||||
| 			} | ||||
| 			handler.hide_id = function hide_id(event, element) { | ||||
| 				element.parentNode.removeChild(element); | ||||
| 				event.srcElement.onclick = handler.export_id; | ||||
| 			} | ||||
| 			handler.delete_id = async function delete_id(event) { | ||||
| 				let id = event.srcElement.dataset.id; | ||||
| 				try { | ||||
| 					if (prompt('Are you sure you want to delete "' + id + '"?  It cannot be recovered without the exported phrase.\\n\\nEnter the word "DELETE" to confirm you wish to delete it.') === 'DELETE') { | ||||
| 						if (await tfrpc.rpc.delete_id(id)) { | ||||
| 							alert('Successfully deleted ID: ' + id); | ||||
| 						} | ||||
| 						await tfrpc.rpc.reload(); | ||||
| 					} | ||||
| 				} catch (e) { | ||||
| 					alert('Error deleting ID: ' + e); | ||||
| 				} | ||||
| 			} | ||||
| 		</script> | ||||
| 		<h1>SSB Identity Management</h1> | ||||
| 		<h2>Create a new identity</h2> | ||||
| 		<button id="create_id" onclick="handler.create_id()">Create Identity</button> | ||||
| 		<h2>Import an SSB Identity from 12 BIP39 English Words</h2> | ||||
| 		<textarea id="add_id" style="width: 100%" rows="4"></textarea><button id="add" onclick="handler.add_id(event)">Import Identity</button> | ||||
| 		<h2>Identities</h2> | ||||
| 		<ul>` + | ||||
| 			ids | ||||
| 				.map( | ||||
| 					(id) => `<li> | ||||
| 			<button onclick="handler.export_id(event)" data-id="${id}">Export Identity</button> | ||||
| 			<button onclick="handler.delete_id(event)" data-id="${id}">Delete Identity</button> | ||||
| 			${id} | ||||
| 		</li>` | ||||
| 				) | ||||
| 				.join('\n') + | ||||
| 			`	</ul> | ||||
| 	</body>` | ||||
| 	); | ||||
| } | ||||
|  | ||||
| main(); | ||||
							
								
								
									
										1
									
								
								apps/user_settings.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								apps/user_settings.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| {"type": "tildefriends-app", "emoji": "⚙️"} | ||||
							
								
								
									
										60
									
								
								apps/user_settings/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								apps/user_settings/app.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| import * as tfrpc from '/tfrpc.js'; | ||||
|  | ||||
| tfrpc.register(async function getIdentities() { | ||||
| 	return ssb.getIdentities(); | ||||
| }); | ||||
| tfrpc.register(async function createID(id) { | ||||
| 	return await ssb.createIdentity(); | ||||
| }); | ||||
| tfrpc.register(async function getPrivateKey(id) { | ||||
| 	return bip39Words(await ssb.getPrivateKey(id)); | ||||
| }); | ||||
| tfrpc.register(async function addID(id) { | ||||
| 	return await ssb.addIdentity(bip39Bytes(id)); | ||||
| }); | ||||
| tfrpc.register(async function deleteID(id) { | ||||
| 	return await ssb.deleteIdentity(id); | ||||
| }); | ||||
| tfrpc.register(async function getThemes() { | ||||
| 	// TODO | ||||
| 	return ['solarized', 'gruvbox', 'light']; | ||||
| }); | ||||
| tfrpc.register(async function getTheme() { | ||||
| 	// TODO | ||||
| 	return 'gruvbox'; | ||||
| }); | ||||
| tfrpc.register(async function setTheme() { | ||||
| 	// TODO | ||||
| 	console.warn('setTheme called - not implemented'); | ||||
| 	return null; | ||||
| }); | ||||
| tfrpc.register(async function reload() { | ||||
| 	await main(); | ||||
| }); | ||||
|  | ||||
| async function main() { | ||||
| 	// Get body.html | ||||
| 	const body = utf8Decode(await getFile('body.html')); | ||||
|  | ||||
| 	// Build the document | ||||
| 	const document = ` | ||||
| 	<!DOCTYPE html> | ||||
| 	<html> | ||||
| 		<head> | ||||
| 			<link rel="stylesheet" href="/static/tildefriends-v1.css"/> | ||||
| 			<script src="tf-theme-picker.js" type="module"></script> | ||||
| 			<script src="tf-password-form.js" type="module"></script> | ||||
| 			<script src="tf-delete-account-btn.js" type="module"></script> | ||||
| 			<script src="tf-identity-manager.js" type="module"></script> | ||||
| 		</head> | ||||
|  | ||||
| 		<body class="flex-column"> | ||||
| 			${body} | ||||
| 		</body> | ||||
| 	</html>`; | ||||
|  | ||||
| 	// Send it to the browser | ||||
| 	app.setDocument(document); | ||||
| } | ||||
|  | ||||
| main(); | ||||
							
								
								
									
										20
									
								
								apps/user_settings/body.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								apps/user_settings/body.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| <h1>Your settings</h1> | ||||
|  | ||||
| <div class="box flex-column"> | ||||
| 	<h2>Appearance</h2> | ||||
|  | ||||
| 	<tf-theme-picker></tf-theme-picker> | ||||
| </div> | ||||
|  | ||||
| <div class="box flex-column"> | ||||
| 	<h2>Danger Zone</h2> | ||||
|  | ||||
| 	<h3>Manage your identities</h3> | ||||
| 	<tf-identity-manager></tf-identity-manager> | ||||
|  | ||||
| 	<h3>Change my password</h3> | ||||
| 	<tf-password-form></tf-password-form> | ||||
|  | ||||
| 	<h3>Delete your account</h3> | ||||
| 	<tf-delete-account-btn></tf-delete-account-btn> | ||||
| </div> | ||||
							
								
								
									
										120
									
								
								apps/user_settings/lit-all.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								apps/user_settings/lit-all.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								apps/user_settings/lit-all.min.js.map
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								apps/user_settings/lit-all.min.js.map
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										36
									
								
								apps/user_settings/tf-delete-account-btn.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								apps/user_settings/tf-delete-account-btn.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| import {LitElement, html} from './lit-all.min.js'; | ||||
| import * as tfrpc from '/static/tfrpc.js'; | ||||
|  | ||||
| class TfDeleteAccountButtonElement extends LitElement { | ||||
| 	static get properties() { | ||||
| 		return {}; | ||||
| 	} | ||||
|  | ||||
| 	constructor() { | ||||
| 		super(); | ||||
| 	} | ||||
|  | ||||
| 	deleteAccount() { | ||||
| 		const res = confirm( | ||||
| 			'Are you really sure you want to delete your account ?' | ||||
| 		); | ||||
|  | ||||
| 		if (!res) return; | ||||
|  | ||||
| 		console.warn('TODO'); | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		return html` | ||||
| 			<link rel="stylesheet" href="/static/tildefriends-v1.css" /> | ||||
|  | ||||
| 			<span>This action is irreversible !</span> | ||||
|  | ||||
| 			<button class="red" @click=${this.deleteAccount}> | ||||
| 				[Not implemented] Delete my Tilde Friends account | ||||
| 			</button> | ||||
| 		`; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| customElements.define('tf-delete-account-btn', TfDeleteAccountButtonElement); | ||||
							
								
								
									
										118
									
								
								apps/user_settings/tf-identity-manager.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								apps/user_settings/tf-identity-manager.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,118 @@ | ||||
| import {LitElement, html} from './lit-all.min.js'; | ||||
| import * as tfrpc from '/static/tfrpc.js'; | ||||
|  | ||||
| class TfIdentityManagerElement extends LitElement { | ||||
| 	static get properties() { | ||||
| 		return { | ||||
| 			ids: {type: Array}, | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	constructor() { | ||||
| 		super(); | ||||
| 		this.ids = []; | ||||
| 		this.load(); | ||||
| 	} | ||||
|  | ||||
| 	async load() { | ||||
| 		this.ids = await tfrpc.rpc.getIdentities(); | ||||
| 	} | ||||
|  | ||||
| 	async createIdentity() { | ||||
| 		try { | ||||
| 			const id = await tfrpc.rpc.createID(); | ||||
| 			alert('Successfully created: ' + id); | ||||
| 			await tfrpc.rpc.reload(); | ||||
| 		} catch (err) { | ||||
| 			alert('Error creating identity: ' + err); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	async importIdentity() { | ||||
| 		const words = this.renderRoot?.querySelector('#import-id-textarea').value; | ||||
| 		if (!words) return; | ||||
|  | ||||
| 		try { | ||||
| 			const newID = await tfrpc.rpc.addID(words); | ||||
|  | ||||
| 			if (newID) alert('Successfully imported a new identity.'); | ||||
| 			else alert('This identity already exists or is invalid.'); | ||||
| 			await tfrpc.rpc.reload(); | ||||
| 		} catch (err) { | ||||
| 			alert('Error importing identity: ' + err); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	async exportIdentity(id) { | ||||
| 		alert( | ||||
| 			'Your private key is:\n' + | ||||
| 				(await tfrpc.rpc.getPrivateKey(id)) + | ||||
| 				'\nDo not share it with anyone!' | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	async deleteIdentity(id) { | ||||
| 		try { | ||||
| 			if ( | ||||
| 				prompt( | ||||
| 					'Are you sure you want to delete "' + | ||||
| 						id + | ||||
| 						'"?  It cannot be recovered without the exported phrase.\\n\\nEnter the word "DELETE" to confirm you wish to delete it.' | ||||
| 				) === 'DELETE' | ||||
| 			) { | ||||
| 				if (await tfrpc.rpc.deleteID(id)) { | ||||
| 					alert('Successfully deleted ID: ' + id); | ||||
| 				} | ||||
| 				await tfrpc.rpc.reload(); | ||||
| 			} | ||||
| 		} catch (e) { | ||||
| 			alert('Error deleting ID: ' + e); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		return html` <link rel="stylesheet" href="/static/tildefriends-v1.css" /> | ||||
| 			<style> | ||||
| 				.id-span { | ||||
| 					font-family: monospace; | ||||
| 					margin-left: 8px; | ||||
| 				} | ||||
| 			</style> | ||||
|  | ||||
| 			<h4>Create a new identity</h4> | ||||
| 			<button id="create-id" class="green" @click=${this.createIdentity}> | ||||
| 				Create Identity | ||||
| 			</button> | ||||
|  | ||||
| 			<h4>Import an SSB Identity from 12 BIP39 English Words</h4> | ||||
| 			<textarea id="import-id-textarea" style="width: 100%" rows="4"></textarea> | ||||
| 			<button class="green" @click=${this.importIdentity}> | ||||
| 				Import Identity | ||||
| 			</button> | ||||
|  | ||||
| 			<h4>Warning !</h4> | ||||
| 			<strong | ||||
| 				>Anybody that knows your private key can gain total access over your | ||||
| 				account.</strong | ||||
| 			> | ||||
| 			<br /><br /> | ||||
| 			Tilde Friends' contributors will never ask you for your private key ! | ||||
|  | ||||
| 			<ul> | ||||
| 				${this.ids.map( | ||||
| 					(id) => | ||||
| 						html` <li> | ||||
| 							<button class="blue" @click=${() => this.exportIdentity(id)}> | ||||
| 								Export Identity | ||||
| 							</button> | ||||
| 							<button class="red" @click=${() => this.deleteIdentity(id)}> | ||||
| 								Delete Identity | ||||
| 							</button> | ||||
| 							<span class="id-span">${id}</span> | ||||
| 						</li>` | ||||
| 				)} | ||||
| 			</ul>`; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| customElements.define('tf-identity-manager', TfIdentityManagerElement); | ||||
							
								
								
									
										82
									
								
								apps/user_settings/tf-password-form.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								apps/user_settings/tf-password-form.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | ||||
| import {LitElement, html} from './lit-all.min.js'; | ||||
| import * as tfrpc from '/static/tfrpc.js'; | ||||
|  | ||||
| class TfPasswordFormElement extends LitElement { | ||||
| 	static get properties() { | ||||
| 		return { | ||||
| 			//selected: {type: String}, | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	constructor() { | ||||
| 		super(); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Checks a password against different requirements | ||||
| 	 * @param {string} password the password to validate | ||||
| 	 * @returns | ||||
| 	 */ | ||||
| 	validatePassword(password) { | ||||
| 		// TODO(tasiaiso) | ||||
| 		return true; | ||||
| 	} | ||||
|  | ||||
| 	submitPassword() { | ||||
| 		const currentPwd = this.shadowRoot.getElementById('current').value; | ||||
| 		const newPwd = this.shadowRoot.getElementById('new').value; | ||||
| 		const repeatPwd = this.shadowRoot.getElementById('Repeat').value; | ||||
|  | ||||
| 		if (!(newPwd === repeatPwd)) { | ||||
| 			return; | ||||
| 		} | ||||
|  | ||||
| 		// TODO | ||||
| 		// tfrpc.changePassword() | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		return html` | ||||
| 			<link rel="stylesheet" href="/static/tildefriends-v1.css" /> | ||||
|  | ||||
| 			<style> | ||||
| 				.grid { | ||||
| 					display: grid; | ||||
| 					grid-template-columns: auto auto; | ||||
| 				} | ||||
| 			</style> | ||||
|  | ||||
| 			<div class="grid"> | ||||
| 				<label for="current">Current password:</label> | ||||
| 				<input | ||||
| 					type="password" | ||||
| 					id="current" | ||||
| 					name="current" | ||||
| 					autocomplete="current-password" | ||||
| 				/> | ||||
|  | ||||
| 				<label for="new">Enter new password:</label> | ||||
| 				<input | ||||
| 					type="password" | ||||
| 					id="new" | ||||
| 					name="new" | ||||
| 					autocomplete="new-password" | ||||
| 				/> | ||||
|  | ||||
| 				<label for="repeat">Repeat new password:</label> | ||||
| 				<input | ||||
| 					type="password" | ||||
| 					id="repeat" | ||||
| 					name="repeat" | ||||
| 					autocomplete="new-password" | ||||
| 				/> | ||||
| 			</div> | ||||
|  | ||||
| 			<button @click=${this.submitPassword} class="red"> | ||||
| 				[Not implemented] Change my password | ||||
| 			</button> | ||||
| 		`; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| customElements.define('tf-password-form', TfPasswordFormElement); | ||||
							
								
								
									
										51
									
								
								apps/user_settings/tf-theme-picker.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								apps/user_settings/tf-theme-picker.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| import {LitElement, html, nothing} from './lit-all.min.js'; | ||||
| import * as tfrpc from '/static/tfrpc.js'; | ||||
|  | ||||
| class TfThemePickerElement extends LitElement { | ||||
| 	static get properties() { | ||||
| 		return { | ||||
| 			selected: {type: String}, | ||||
| 			themes: {type: Array}, | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	constructor() { | ||||
| 		super(); | ||||
| 		this.load(); | ||||
| 	} | ||||
|  | ||||
| 	async load() { | ||||
| 		this.themes = await tfrpc.rpc.getThemes(); | ||||
| 		this.selected = await tfrpc.rpc.getTheme(); | ||||
|  | ||||
| 		let select = this.renderRoot?.querySelector('#theme-select'); | ||||
| 		select.value = this.selected; | ||||
| 	} | ||||
|  | ||||
| 	changed(event) { | ||||
| 		this.selected = event.srcElement.value; | ||||
| 		console.log('selected theme', this.selected); | ||||
| 		// TODO | ||||
| 	} | ||||
|  | ||||
| 	render() { | ||||
| 		return html` | ||||
| 			<link rel="stylesheet" href="/static/tildefriends-v1.css" /> | ||||
|  | ||||
| 			<label for="theme">[Not implemented] Choose your theme:</label> | ||||
|  | ||||
| 			<select | ||||
| 				name="theme" | ||||
| 				id="theme-select" | ||||
| 				?hidden=${!this.themes?.length} | ||||
| 				@change=${this.changed} | ||||
| 			> | ||||
| 				${(this.themes ?? []).map( | ||||
| 					(name) => html`<option value=${name}>${name}</option>` | ||||
| 				)} | ||||
| 			</select> | ||||
| 		`; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| customElements.define('tf-theme-picker', TfThemePickerElement); | ||||
							
								
								
									
										114
									
								
								core/tildefriends-v1.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								core/tildefriends-v1.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | ||||
| /* | ||||
|  * Tilde Friends core stylesheet | ||||
|  * | ||||
|  * This Software is an external library that is part of | ||||
|  * Tilde Friends and is shared under the MIT license. | ||||
|  * | ||||
|  * Inject this file in your app at tildefriends.css | ||||
|  * and use this tag to import it: | ||||
|  * <link rel="stylesheet" href="/static/tildefriends-v1.css"/> | ||||
|  * | ||||
|  * v1.0 / 2024 M03 21 | ||||
|  */ | ||||
|  | ||||
| body { | ||||
| 	color: white; | ||||
| 	font-family: sans-serif; | ||||
| } | ||||
|  | ||||
| button, | ||||
| .button, | ||||
| input[type='button'], | ||||
| input[type='submit'], | ||||
| select { | ||||
| 	border: none; | ||||
| 	border-radius: 8px; | ||||
| 	padding: 8px 12px; | ||||
| 	text-align: center; | ||||
| 	text-decoration: none; | ||||
| 	display: inline-block; | ||||
| 	margin: 4px; | ||||
|  | ||||
| 	&.red { | ||||
| 		background-color: #bd1e24; | ||||
| 		color: white; | ||||
| 	} | ||||
|  | ||||
| 	&.green { | ||||
| 		background-color: #18922d; | ||||
| 		color: white; | ||||
| 	} | ||||
|  | ||||
| 	&.blue { | ||||
| 		background-color: #0067a7; | ||||
| 		color: white; | ||||
| 	} | ||||
|  | ||||
| 	&.yellow { | ||||
| 		background-color: #ee9600; | ||||
| 		color: black; | ||||
| 	} | ||||
|  | ||||
| 	&:hover { | ||||
| 		filter: brightness(0.75); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| a:link { | ||||
| 	color: #268bd2; | ||||
| } | ||||
|  | ||||
| a:visited { | ||||
| 	color: #6c71c4; | ||||
| } | ||||
|  | ||||
| a:hover { | ||||
| 	color: #859900; | ||||
| } | ||||
|  | ||||
| a:active { | ||||
| 	color: #2aa198; | ||||
| } | ||||
|  | ||||
| table { | ||||
| 	border-collapse: collapse; | ||||
| 	width: 100%; | ||||
| } | ||||
|  | ||||
| td, | ||||
| th { | ||||
| 	border: 1px solid #ffffff40; | ||||
| 	text-align: left; | ||||
| 	padding: 8px; | ||||
| } | ||||
|  | ||||
| tr:nth-child(even) { | ||||
| 	background-color: #ffffff20; | ||||
| } | ||||
|  | ||||
| .flex { | ||||
| 	display: flex; | ||||
| } | ||||
|  | ||||
| .flex-column { | ||||
| 	display: flex; | ||||
| 	flex-direction: column; | ||||
| } | ||||
|  | ||||
| .flex-row { | ||||
| 	display: flex; | ||||
| 	flex-direction: row; | ||||
| } | ||||
|  | ||||
| .inline-flex-row { | ||||
| 	display: inline-flex; | ||||
| 	flex-direction: row; | ||||
| } | ||||
|  | ||||
| .box { | ||||
| 	background-color: #00000020; | ||||
| 	border: 1px solid grey; | ||||
| 	border-radius: 8px; | ||||
| 	padding: 16px; | ||||
| 	margin: 4px; | ||||
| } | ||||
| @@ -644,6 +644,7 @@ static void _httpd_endpoint_static(tf_http_request_t* request) | ||||
| 		"style.css", | ||||
| 		"tfrpc.js", | ||||
| 		"w3.css", | ||||
| 		"tildefriends-v1.css" | ||||
| 	}; | ||||
|  | ||||
| 	const char* k_map[][2] = { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user