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", | 		"style.css", | ||||||
| 		"tfrpc.js", | 		"tfrpc.js", | ||||||
| 		"w3.css", | 		"w3.css", | ||||||
|  | 		"tildefriends-v1.css" | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	const char* k_map[][2] = { | 	const char* k_map[][2] = { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user