folder management + cleanup
This commit is contained in:
		
							parent
							
								
									e959809a0e
								
							
						
					
					
						commit
						a97ed1746d
					
				
					 3 changed files with 273 additions and 147 deletions
				
			
		|  | @ -1,14 +1,14 @@ | ||||||
| import NDK, { | import NDK, { | ||||||
| 	NDKEvent, |   NDKEvent, | ||||||
| 	type NDKEventId, |   type NDKEventId, | ||||||
| 	NDKKind, |   NDKKind, | ||||||
| 	NDKPrivateKeySigner, |   NDKPrivateKeySigner, | ||||||
| 	type NDKUser |   type NDKUser | ||||||
| } from '@nostr-dev-kit/ndk'; | } from '@nostr-dev-kit/ndk'; | ||||||
| import { | import { | ||||||
| 	dateFormat as dateFormatStore, |   dateFormat as dateFormatStore, | ||||||
| 	ndk as ndkStore, |   ndk as ndkStore, | ||||||
| 	timeFormat as timeFormatStore |   timeFormat as timeFormatStore | ||||||
| } from './stores.svelte'; | } from './stores.svelte'; | ||||||
| import { generateSecretKey } from 'nostr-tools'; | import { generateSecretKey } from 'nostr-tools'; | ||||||
| import { Letter } from '$lib/letter'; | import { Letter } from '$lib/letter'; | ||||||
|  | @ -23,195 +23,202 @@ dateFormatStore.subscribe((d: string) => (dateFormat = d)); | ||||||
| timeFormatStore.subscribe((t: string) => (timeFormat = t)); | timeFormatStore.subscribe((t: string) => (timeFormat = t)); | ||||||
| 
 | 
 | ||||||
| async function waitForNDK() { | async function waitForNDK() { | ||||||
| 	if (ndk) return; |   if (ndk) return; | ||||||
| 	await new Promise((resolve) => setTimeout(resolve, 1000)); |   await new Promise((resolve) => setTimeout(resolve, 1000)); | ||||||
| 	await waitForNDK(); |   await waitForNDK(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function randomTimeUpTo2DaysInThePast() { | export function randomTimeUpTo2DaysInThePast() { | ||||||
| 	const now = Date.now(); |   const now = Date.now(); | ||||||
| 	const twoDaysAgo = now - 2 * 24 * 60 * 60 * 1000 - 3600 * 1000; // 1 hour buffer in case of clock skew
 |   const twoDaysAgo = now - 2 * 24 * 60 * 60 * 1000 - 3600 * 1000; // 1 hour buffer in case of clock skew
 | ||||||
| 	return Math.floor((Math.floor(Math.random() * (now - twoDaysAgo)) + twoDaysAgo) / 1000); |   return Math.floor((Math.floor(Math.random() * (now - twoDaysAgo)) + twoDaysAgo) / 1000); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function decryptSealedMessage(message: NDKEvent): Promise<NDKEvent> { | export async function decryptSealedMessage(message: NDKEvent): Promise<NDKEvent> { | ||||||
| 	await waitForNDK(); |   await waitForNDK(); | ||||||
| 	const sealedMessage = JSON.parse(await ndk.signer!.nip44Decrypt(message.author, message.content)); |   const sealedMessage = JSON.parse(await ndk.signer!.nip44Decrypt(message.author, message.content)); | ||||||
| 	const author = ndk.getUser({ pubkey: sealedMessage.pubkey }); |   const author = ndk.getUser({ pubkey: sealedMessage.pubkey }); | ||||||
| 	const msg = JSON.parse(await ndk.signer!.nip44Decrypt(author, sealedMessage.content)); |   const msg = JSON.parse(await ndk.signer!.nip44Decrypt(author, sealedMessage.content)); | ||||||
| 	const event = new NDKEvent(ndk, msg); |   const event = new NDKEvent(ndk, msg); | ||||||
| 	if (event.pubkey === '') event.pubkey = author.pubkey; |   if (event.pubkey === '') event.pubkey = author.pubkey; | ||||||
| 	return event; |   return event; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function decryptSealedMessageIntoReadableType( | export async function decryptSealedMessageIntoReadableType( | ||||||
| 	encryptedMessage: NDKEvent |   encryptedMessage: NDKEvent | ||||||
| ): Promise<Letter | undefined> { | ): Promise<Letter | undefined> { | ||||||
| 	await waitForNDK(); |   await waitForNDK(); | ||||||
| 	let rawDecrypted = await decryptSealedMessage(encryptedMessage); |   let rawDecrypted = await decryptSealedMessage(encryptedMessage); | ||||||
| 	switch (rawDecrypted.kind) { |   switch (rawDecrypted.kind) { | ||||||
| 		case NDKKind.Article: |     case NDKKind.Article: | ||||||
| 			return getLetterFromDecryptedMessage(rawDecrypted, encryptedMessage); |       return getLetterFromDecryptedMessage(rawDecrypted, encryptedMessage); | ||||||
| 	} |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function moveMessageToFolder(id: NDKEventId, folder: NDKEventId | string) { | export async function moveMessageToFolder(id: NDKEventId, folder: NDKEventId | string) { | ||||||
| 	if (folder === 'sent') throw new Error('Cannot move message to sent folder'); |   if (folder === 'sent') throw new Error('Cannot move message to sent folder'); | ||||||
| 	await waitForNDK(); |   await waitForNDK(); | ||||||
| 	const user = await ndk.signer!.user(); |   const user = await ndk.signer!.user(); | ||||||
| 	const rawMessage = new NDKEvent(); |   const rawMessage = new NDKEvent(); | ||||||
| 	rawMessage.author = user; |   rawMessage.author = user; | ||||||
| 	rawMessage.created_at = Math.ceil(Date.now() / 1000); |   rawMessage.created_at = Math.ceil(Date.now() / 1000); | ||||||
| 	rawMessage.kind = NDKKind.Label; |   rawMessage.kind = NDKKind.Label; | ||||||
| 	rawMessage.content = ''; |   rawMessage.content = ''; | ||||||
| 	rawMessage.tags.push(['label-type', 'letter-to-folder-mapping']); |   rawMessage.tags.push(['label-type', 'letter-to-folder-mapping']); | ||||||
| 	rawMessage.tags.push(['message', id]); |   rawMessage.tags.push(['message', id]); | ||||||
| 	rawMessage.tags.push(['folder', folder]); |   rawMessage.tags.push(['folder', folder]); | ||||||
| 	return encryptEventForRecipient(rawMessage, user); |   return encryptEventForRecipient(rawMessage, user); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function createFolder(name: string, icon: string) { | export async function createFolder(name: string, icon: string) { | ||||||
| 	await waitForNDK(); |   await waitForNDK(); | ||||||
| 	const allFolders = await FolderLabel.getAll(ndk); |   const allFolders = await FolderLabel.getAll(ndk); | ||||||
| 	const newFolder = { |   const newFolder = FolderLabel.fromJSON({ | ||||||
| 		id: crypto.randomUUID(), |     id: crypto.randomUUID(), | ||||||
| 		name: name, |     name: name, | ||||||
| 		icon: icon |     icon: icon | ||||||
| 	}; |   }); | ||||||
| 	allFolders.push(newFolder); |   allFolders.push(newFolder); | ||||||
| 	await FolderLabel.save(ndk, allFolders); |   await FolderLabel.save(ndk, allFolders); | ||||||
| 	return newFolder; |   return newFolder; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function deleteFolder(id: string) { | ||||||
|  |   await waitForNDK(); | ||||||
|  |   const allFolders = await FolderLabel.getAll(ndk); | ||||||
|  |   allFolders.splice(allFolders.findIndex(f => f.id === id), 1); | ||||||
|  |   await FolderLabel.save(ndk, allFolders); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function createSealedLetter( | export async function createSealedLetter( | ||||||
| 	from: NDKUser, |   from: NDKUser, | ||||||
| 	to: NDKUser, |   to: NDKUser, | ||||||
| 	subject: string, |   subject: string, | ||||||
| 	content: string, |   content: string, | ||||||
| 	replyTo?: string, |   replyTo?: string, | ||||||
| 	stamp?: string |   stamp?: string | ||||||
| ) { | ) { | ||||||
| 	await waitForNDK(); |   await waitForNDK(); | ||||||
| 	const rawMessage = new NDKEvent(); |   const rawMessage = new NDKEvent(); | ||||||
| 	rawMessage.author = from; |   rawMessage.author = from; | ||||||
| 	rawMessage.created_at = Math.ceil(Date.now() / 1000); |   rawMessage.created_at = Math.ceil(Date.now() / 1000); | ||||||
| 	rawMessage.kind = NDKKind.Article; |   rawMessage.kind = NDKKind.Article; | ||||||
| 	rawMessage.content = content; |   rawMessage.content = content; | ||||||
| 	rawMessage.tags.push(['subject', subject]); |   rawMessage.tags.push(['subject', subject]); | ||||||
| 	if (typeof replyTo !== 'undefined' && replyTo) rawMessage.tags.push(['e', replyTo, 'reply']); |   if (typeof replyTo !== 'undefined' && replyTo) rawMessage.tags.push(['e', replyTo, 'reply']); | ||||||
| 	if (stamp) { |   if (stamp) { | ||||||
| 		const encryptedStamp = await ndk.signer!.nip44Encrypt(to, stamp); |     const encryptedStamp = await ndk.signer!.nip44Encrypt(to, stamp); | ||||||
| 		rawMessage.tags.push(['stamp', encryptedStamp]); |     rawMessage.tags.push(['stamp', encryptedStamp]); | ||||||
| 	} |   } | ||||||
| 	rawMessage.tags.push(['p', to.pubkey]); |   rawMessage.tags.push(['p', to.pubkey]); | ||||||
| 	return encryptEventForRecipient(rawMessage, to); |   return encryptEventForRecipient(rawMessage, to); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function createCarbonCopyLetter( | export async function createCarbonCopyLetter( | ||||||
| 	sender: NDKUser, |   sender: NDKUser, | ||||||
| 	recipients: NDKUser[], |   recipients: NDKUser[], | ||||||
| 	subject: string, |   subject: string, | ||||||
| 	content: string, |   content: string, | ||||||
| 	replyTo?: string |   replyTo?: string | ||||||
| ) { | ) { | ||||||
| 	await waitForNDK(); |   await waitForNDK(); | ||||||
| 	const rawMessage = new NDKEvent(); |   const rawMessage = new NDKEvent(); | ||||||
| 	rawMessage.author = sender; |   rawMessage.author = sender; | ||||||
| 	rawMessage.created_at = Math.ceil(Date.now() / 1000); |   rawMessage.created_at = Math.ceil(Date.now() / 1000); | ||||||
| 	rawMessage.kind = NDKKind.Article; |   rawMessage.kind = NDKKind.Article; | ||||||
| 	rawMessage.content = content; |   rawMessage.content = content; | ||||||
| 	rawMessage.tags.push(['subject', subject]); |   rawMessage.tags.push(['subject', subject]); | ||||||
| 	if (typeof replyTo !== 'undefined' && replyTo) rawMessage.tags.push(['e', replyTo, 'reply']); |   if (typeof replyTo !== 'undefined' && replyTo) rawMessage.tags.push(['e', replyTo, 'reply']); | ||||||
| 	for (const recipient of recipients) rawMessage.tags.push(['p', recipient.pubkey]); |   for (const recipient of recipients) rawMessage.tags.push(['p', recipient.pubkey]); | ||||||
| 	return encryptEventForRecipient(rawMessage, sender); |   return encryptEventForRecipient(rawMessage, sender); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function encryptEventForRecipient( | export async function encryptEventForRecipient( | ||||||
| 	event: NDKEvent, |   event: NDKEvent, | ||||||
| 	recipient: NDKUser |   recipient: NDKUser | ||||||
| ): Promise<NDKEvent> { | ): Promise<NDKEvent> { | ||||||
| 	await waitForNDK(); |   await waitForNDK(); | ||||||
| 	let randomKey = generateSecretKey(); |   let randomKey = generateSecretKey(); | ||||||
| 	const randomKeySinger = new NDKPrivateKeySigner(randomKey); |   const randomKeySinger = new NDKPrivateKeySigner(randomKey); | ||||||
| 	const seal = new NDKEvent(); |   const seal = new NDKEvent(); | ||||||
| 	seal.pubkey = recipient.pubkey; |   seal.pubkey = recipient.pubkey; | ||||||
| 	seal.kind = 13; |   seal.kind = 13; | ||||||
| 	seal.content = await ndk.signer!.nip44Encrypt(recipient, JSON.stringify(event)); |   seal.content = await ndk.signer!.nip44Encrypt(recipient, JSON.stringify(event)); | ||||||
| 	seal.created_at = randomTimeUpTo2DaysInThePast(); |   seal.created_at = randomTimeUpTo2DaysInThePast(); | ||||||
| 	await seal.sign(ndk.signer); |   await seal.sign(ndk.signer); | ||||||
| 	const giftWrap = new NDKEvent(); |   const giftWrap = new NDKEvent(); | ||||||
| 	giftWrap.kind = 1059; |   giftWrap.kind = 1059; | ||||||
| 	giftWrap.created_at = randomTimeUpTo2DaysInThePast(); |   giftWrap.created_at = randomTimeUpTo2DaysInThePast(); | ||||||
| 	giftWrap.content = await randomKeySinger.nip44Encrypt(recipient, JSON.stringify(seal)); |   giftWrap.content = await randomKeySinger.nip44Encrypt(recipient, JSON.stringify(seal)); | ||||||
| 	giftWrap.tags.push(['p', recipient.pubkey]); |   giftWrap.tags.push(['p', recipient.pubkey]); | ||||||
| 	await giftWrap.sign(randomKeySinger); |   await giftWrap.sign(randomKeySinger); | ||||||
| 	giftWrap.ndk = ndk; |   giftWrap.ndk = ndk; | ||||||
| 	return giftWrap; |   return giftWrap; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function isValidNip05(nip05: string): boolean { | export function isValidNip05(nip05: string): boolean { | ||||||
| 	let parts = nip05.split('@'); |   let parts = nip05.split('@'); | ||||||
| 	if (parts.length !== 2) return false; |   if (parts.length !== 2) return false; | ||||||
| 	let domain = parts[1]; |   let domain = parts[1]; | ||||||
| 	return domain.includes('.'); |   return domain.includes('.'); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| let letterCache: { | let letterCache: { | ||||||
| 	[id: string]: Letter; |   [id: string]: Letter; | ||||||
| } = $state({}); | } = $state({}); | ||||||
| 
 | 
 | ||||||
| export async function getLetterFromDecryptedMessage( | export async function getLetterFromDecryptedMessage( | ||||||
| 	msg: NDKEvent, |   msg: NDKEvent, | ||||||
| 	encryptedMessage: NDKEvent |   encryptedMessage: NDKEvent | ||||||
| ): Promise<Letter | undefined> { | ): Promise<Letter | undefined> { | ||||||
| 	if (letterCache[encryptedMessage.id]) return letterCache[encryptedMessage.id]; |   if (letterCache[encryptedMessage.id]) return letterCache[encryptedMessage.id]; | ||||||
| 	await waitForNDK(); |   await waitForNDK(); | ||||||
| 	if (msg.kind != NDKKind.Article) return; |   if (msg.kind != NDKKind.Article) return; | ||||||
| 	letterCache[encryptedMessage.id] = await Letter.fromDecryptedMessage(msg, encryptedMessage, ndk); |   letterCache[encryptedMessage.id] = await Letter.fromDecryptedMessage(msg, encryptedMessage, ndk); | ||||||
| 	return letterCache[encryptedMessage.id]; |   return letterCache[encryptedMessage.id]; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function getReadableDate(date: Date): string { | export function getReadableDate(date: Date): string { | ||||||
| 	const map = { |   const map = { | ||||||
| 		y: date.getFullYear(), |     y: date.getFullYear(), | ||||||
| 		m: String(date.getMonth() + 1).padStart(2, '0'), |     m: String(date.getMonth() + 1).padStart(2, '0'), | ||||||
| 		d: String(date.getDate()).padStart(2, '0') |     d: String(date.getDate()).padStart(2, '0') | ||||||
| 	}; |   }; | ||||||
| 	return dateFormat.replace(/[ymd]/g, (char) => map[char]); |   return dateFormat.replace(/[ymd]/g, (char) => map[char]); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function getReadableTime(date: Date) { | export function getReadableTime(date: Date) { | ||||||
| 	let hours = date.getHours(); |   let hours = date.getHours(); | ||||||
| 	const minutes = String(date.getMinutes()).padStart(2, '0'); |   const minutes = String(date.getMinutes()).padStart(2, '0'); | ||||||
| 	const seconds = String(date.getSeconds()).padStart(2, '0'); |   const seconds = String(date.getSeconds()).padStart(2, '0'); | ||||||
| 
 | 
 | ||||||
| 	const use12Hour = timeFormat.includes('a'); |   const use12Hour = timeFormat.includes('a'); | ||||||
| 	let period = ''; |   let period = ''; | ||||||
| 
 | 
 | ||||||
| 	if (use12Hour) { |   if (use12Hour) { | ||||||
| 		period = hours >= 12 ? 'PM' : 'AM'; |     period = hours >= 12 ? 'PM' : 'AM'; | ||||||
| 		hours = hours % 12; |     hours = hours % 12; | ||||||
| 		hours = hours ? hours : 12; |     hours = hours ? hours : 12; | ||||||
| 	} |   } | ||||||
| 
 | 
 | ||||||
| 	const map = { |   const map = { | ||||||
| 		h: String(hours).padStart(2, '0'), |     h: String(hours).padStart(2, '0'), | ||||||
| 		m: minutes, |     m: minutes, | ||||||
| 		s: seconds, |     s: seconds, | ||||||
| 		a: period |     a: period | ||||||
| 	}; |   }; | ||||||
| 
 | 
 | ||||||
| 	return timeFormat.replace(/[hmsa]/g, (char) => map[char]); |   return timeFormat.replace(/[hmsa]/g, (char) => map[char]); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function appendToBody(node: HTMLElement) { | export function appendToBody(node: HTMLElement) { | ||||||
| 	document.body.appendChild(node); |   document.body.appendChild(node); | ||||||
| 
 | 
 | ||||||
| 	return { |   return { | ||||||
| 		destroy() { |     destroy() { | ||||||
| 			if (node.parentNode) { |       if (node.parentNode) { | ||||||
| 				node.parentNode.removeChild(node); |         node.parentNode.removeChild(node); | ||||||
| 			} |       } | ||||||
| 		} |     } | ||||||
| 	}; |   }; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -15,6 +15,7 @@ | ||||||
| 	import Select from '../../components/Select.svelte'; | 	import Select from '../../components/Select.svelte'; | ||||||
| 	import SettingsLine from './SettingsLine.svelte'; | 	import SettingsLine from './SettingsLine.svelte'; | ||||||
| 	import SubscriptionSettings from './SubscriptionSettings.svelte'; | 	import SubscriptionSettings from './SubscriptionSettings.svelte'; | ||||||
|  | 	import FolderManagement from './FolderManagement.svelte'; | ||||||
| 
 | 
 | ||||||
| 	onMount(async () => { | 	onMount(async () => { | ||||||
| 		$pageTitle = 'Settings'; | 		$pageTitle = 'Settings'; | ||||||
|  | @ -30,6 +31,10 @@ | ||||||
| 	<SubscriptionSettings /> | 	<SubscriptionSettings /> | ||||||
| </SettingsLine> | </SettingsLine> | ||||||
| 
 | 
 | ||||||
|  | <SettingsLine title="Folder Management"> | ||||||
|  | 	<FolderManagement /> | ||||||
|  | </SettingsLine> | ||||||
|  | 
 | ||||||
| <SettingsLine title="Sorting and Grouping"> | <SettingsLine title="Sorting and Grouping"> | ||||||
| 	<Checkbox bind:checked={$groupByStamps} label="Group by stamps" /> | 	<Checkbox bind:checked={$groupByStamps} label="Group by stamps" /> | ||||||
| 	<Select | 	<Select | ||||||
|  |  | ||||||
							
								
								
									
										114
									
								
								src/routes/settings/FolderManagement.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								src/routes/settings/FolderManagement.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,114 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  | 	import { FolderLabel } from '$lib/folderLabel'; | ||||||
|  | 	import { ndk } from '$lib/stores.svelte'; | ||||||
|  | 	import { createFolder, deleteFolder } from '$lib/utils.svelte'; | ||||||
|  | 	import { onMount } from 'svelte'; | ||||||
|  | 	import Icon from '@iconify/svelte'; | ||||||
|  | 	import IconButton from '../../components/IconButton.svelte'; | ||||||
|  | 
 | ||||||
|  | 	let loading = $state(true); | ||||||
|  | 	let folders = $state<FolderLabel[]>([]); | ||||||
|  | 	let newFolderName = $state(''); | ||||||
|  | 
 | ||||||
|  | 	async function loadFolders() { | ||||||
|  | 		loading = true; | ||||||
|  | 		folders = await FolderLabel.getAll($ndk); | ||||||
|  | 		loading = false; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	async function handleDelete(folderId: string) { | ||||||
|  | 		loading = true; | ||||||
|  | 		await deleteFolder(folderId); | ||||||
|  | 		await loadFolders(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	async function handleAdd() { | ||||||
|  | 		if (!newFolderName.trim()) return; | ||||||
|  | 		loading = true; | ||||||
|  | 		await createFolder(newFolderName, ''); | ||||||
|  | 		newFolderName = ''; | ||||||
|  | 		await loadFolders(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	onMount(loadFolders); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | {#if loading} | ||||||
|  | 	<div class="loading"> | ||||||
|  | 		<Icon icon="eos-icons:loading" width="5em" /> | ||||||
|  | 	</div> | ||||||
|  | {:else} | ||||||
|  | 	<table> | ||||||
|  | 		<tbody> | ||||||
|  | 			{#each folders as folder} | ||||||
|  | 				<tr> | ||||||
|  | 					<td class="icon-cell"> | ||||||
|  | 						<Icon icon={folder.icon} width="1.5em" /> | ||||||
|  | 					</td> | ||||||
|  | 					<td class="name-cell">{folder.name}</td> | ||||||
|  | 					<td class="action-cell"> | ||||||
|  | 						<button onclick={() => handleDelete(folder.id)}> | ||||||
|  | 							<Icon icon="icon-park-twotone:delete" width="1.5em" /> | ||||||
|  | 						</button> | ||||||
|  | 					</td> | ||||||
|  | 				</tr> | ||||||
|  | 			{/each} | ||||||
|  | 			<tr class="add-folder"> | ||||||
|  | 				<td class="input-container"> | ||||||
|  | 					<input type="text" bind:value={newFolderName} placeholder="New folder name" /> | ||||||
|  | 				</td> | ||||||
|  | 				<td class="button-cell"> | ||||||
|  | 					<IconButton icon="material-symbols:add" text="Add" on:click={handleAdd} /> | ||||||
|  | 				</td> | ||||||
|  | 			</tr> | ||||||
|  | 		</tbody> | ||||||
|  | 	</table> | ||||||
|  | {/if} | ||||||
|  | 
 | ||||||
|  | <style> | ||||||
|  | 	.loading { | ||||||
|  | 		display: flex; | ||||||
|  | 		justify-content: center; | ||||||
|  | 		align-items: center; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	table { | ||||||
|  | 		width: 100%; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	tr { | ||||||
|  | 		display: flex; | ||||||
|  | 		align-items: center; | ||||||
|  | 		gap: 1rem; | ||||||
|  | 		width: 100%; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	.icon-cell { | ||||||
|  | 		display: flex; | ||||||
|  | 		width: 1.5em; | ||||||
|  | 		height: 1.5em; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	.name-cell { | ||||||
|  | 		flex: 1; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	.action-cell { | ||||||
|  | 		display: flex; | ||||||
|  | 		justify-content: flex-end; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	.add-folder { | ||||||
|  | 		margin-top: 1rem; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	.input-container { | ||||||
|  | 		flex: 1; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	.button-cell { | ||||||
|  | 		display: flex; | ||||||
|  | 		justify-content: flex-end; | ||||||
|  | 		padding-left: 1rem; | ||||||
|  | 	} | ||||||
|  | </style> | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue