瀏覽代碼

refac: collection styling

Timothy J. Baek 6 月之前
父節點
當前提交
03282da45c

+ 2 - 14
src/lib/components/common/Drawer.svelte

@@ -6,23 +6,11 @@
 	const dispatch = createEventDispatcher();
 
 	export let show = false;
-	export let size = 'md';
+	export let className = '';
 
 	let modalElement = null;
 	let mounted = false;
 
-	const sizeToWidth = (size) => {
-		if (size === 'xs') {
-			return 'w-[16rem]';
-		} else if (size === 'sm') {
-			return 'w-[30rem]';
-		} else if (size === 'md') {
-			return 'w-[48rem]';
-		} else {
-			return 'w-[56rem]';
-		}
-	};
-
 	const handleKeyDown = (event: KeyboardEvent) => {
 		if (event.key === 'Escape' && isTopModal()) {
 			console.log('Escape');
@@ -76,7 +64,7 @@
 	}}
 >
 	<div
-		class=" mt-auto max-w-full w-full bg-gray-50 dark:bg-gray-900 max-h-[100dvh] overflow-y-auto scrollbar-hidden"
+		class=" mt-auto max-w-full w-full bg-gray-50 dark:bg-gray-900 dark:text-gray-100 {className} max-h-[100dvh] overflow-y-auto scrollbar-hidden"
 		on:mousedown={(e) => {
 			e.stopPropagation();
 		}}

+ 7 - 3
src/lib/components/common/Modal.svelte

@@ -6,11 +6,15 @@
 
 	export let show = true;
 	export let size = 'md';
+	export let className = 'bg-gray-50 dark:bg-gray-900  rounded-2xl';
 
 	let modalElement = null;
 	let mounted = false;
 
 	const sizeToWidth = (size) => {
+		if (size === 'full') {
+			return 'w-full';
+		}
 		if (size === 'xs') {
 			return 'w-[16rem]';
 		} else if (size === 'sm') {
@@ -68,9 +72,9 @@
 		}}
 	>
 		<div
-			class=" m-auto rounded-2xl max-w-full {sizeToWidth(
-				size
-			)} mx-2 bg-gray-50 dark:bg-gray-900 shadow-3xl max-h-[100dvh] overflow-y-auto scrollbar-hidden"
+			class=" m-auto max-w-full {sizeToWidth(size)} {size !== 'full'
+				? 'mx-2'
+				: ''} shadow-3xl max-h-[100dvh] overflow-y-auto scrollbar-hidden {className}"
 			in:flyAndScale
 			on:mousedown={(e) => {
 				e.stopPropagation();

+ 255 - 130
src/lib/components/workspace/Knowledge/Collection.svelte

@@ -2,6 +2,7 @@
 	import Fuse from 'fuse.js';
 	import { toast } from 'svelte-sonner';
 	import { v4 as uuidv4 } from 'uuid';
+	import { PaneGroup, Pane, PaneResizer } from 'paneforge';
 
 	import { onMount, getContext, onDestroy, tick } from 'svelte';
 	const i18n = getContext('i18n');
@@ -34,8 +35,17 @@
 
 	import SyncConfirmDialog from '../../common/ConfirmDialog.svelte';
 	import RichTextInput from '$lib/components/common/RichTextInput.svelte';
+	import EllipsisVertical from '$lib/components/icons/EllipsisVertical.svelte';
+	import Drawer from '$lib/components/common/Drawer.svelte';
+	import ChevronLeft from '$lib/components/icons/ChevronLeft.svelte';
+	import MenuLines from '$lib/components/icons/MenuLines.svelte';
+
 	let largeScreen = true;
 
+	let pane;
+	let showSidepanel = true;
+	let minSize = 0;
+
 	type Knowledge = {
 		id: string;
 		name: string;
@@ -458,6 +468,30 @@
 		mediaQuery.addEventListener('change', handleMediaQuery);
 		handleMediaQuery(mediaQuery);
 
+		// Select the container element you want to observe
+		const container = document.getElementById('collection-container');
+
+		// initialize the minSize based on the container width
+		minSize = !largeScreen ? 100 : Math.floor((300 / container.clientWidth) * 100);
+
+		// Create a new ResizeObserver instance
+		const resizeObserver = new ResizeObserver((entries) => {
+			for (let entry of entries) {
+				const width = entry.contentRect.width;
+				// calculate the percentage of 300
+				const percentage = (300 / width) * 100;
+				// set the minSize to the percentage, must be an integer
+				minSize = !largeScreen ? 100 : Math.floor(percentage);
+			}
+		});
+
+		// Start observing the container's size changes
+		resizeObserver.observe(container);
+
+		if (pane) {
+			pane.expand();
+		}
+
 		id = $page.params.id;
 
 		const res = await getKnowledgeById(localStorage.token, id).catch((e) => {
@@ -551,157 +585,248 @@
 	}}
 />
 
-<div class="flex flex-col w-full h-full max-h-[100dvh]">
+<div class="flex flex-col w-full h-full max-h-[100dvh]" id="collection-container">
 	{#if id && knowledge}
 		<div class="flex flex-row flex-1 h-full max-h-full pb-2.5">
-			<div
-				class=" {largeScreen
-					? 'flex-shrink-0'
-					: 'flex-1'} flex py-2.5 w-80 rounded-2xl border border-gray-50 dark:border-gray-850"
-			>
-				<div class=" flex flex-col w-full space-x-2 rounded-lg h-full">
-					<div class="w-full h-full flex flex-col">
-						<div class=" px-3">
-							<div class="flex">
-								<div class=" self-center ml-1 mr-3">
-									<svg
-										xmlns="http://www.w3.org/2000/svg"
-										viewBox="0 0 20 20"
-										fill="currentColor"
-										class="w-4 h-4"
-									>
-										<path
-											fill-rule="evenodd"
-											d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
-											clip-rule="evenodd"
+			<PaneGroup direction="horizontal">
+				<Pane
+					bind:pane
+					defaultSize={minSize}
+					collapsible={true}
+					maxSize={50}
+					{minSize}
+					class="h-full"
+					onExpand={() => {
+						showSidepanel = true;
+					}}
+					onCollapse={() => {
+						showSidepanel = false;
+					}}
+				>
+					<div
+						class="{largeScreen ? 'flex-shrink-0' : 'flex-1'}
+						flex
+						py-2
+						rounded-2xl
+						border
+						border-gray-50
+						h-full
+						dark:border-gray-850"
+					>
+						<div class=" flex flex-col w-full space-x-2 rounded-lg h-full">
+							<div class="w-full h-full flex flex-col">
+								<div class=" px-3">
+									<div class="flex py-1">
+										<div class=" self-center ml-1 mr-3">
+											<svg
+												xmlns="http://www.w3.org/2000/svg"
+												viewBox="0 0 20 20"
+												fill="currentColor"
+												class="w-4 h-4"
+											>
+												<path
+													fill-rule="evenodd"
+													d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
+													clip-rule="evenodd"
+												/>
+											</svg>
+										</div>
+										<input
+											class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
+											bind:value={query}
+											placeholder={$i18n.t('Search Collection')}
+											on:focus={() => {
+												selectedFileId = null;
+											}}
 										/>
-									</svg>
-								</div>
-								<input
-									class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
-									bind:value={query}
-									placeholder={$i18n.t('Search Collection')}
-									on:focus={() => {
-										selectedFileId = null;
-									}}
-								/>
-
-								<div>
-									<AddContentMenu
-										on:upload={(e) => {
-											if (e.detail.type === 'directory') {
-												uploadDirectoryHandler();
-											} else if (e.detail.type === 'text') {
-												showAddTextContentModal = true;
-											} else {
-												document.getElementById('files-input').click();
-											}
-										}}
-										on:sync={(e) => {
-											showSyncConfirmModal = true;
-										}}
-									/>
-								</div>
-							</div>
-
-							<hr class=" mt-2 mb-1 border-gray-50 dark:border-gray-850" />
-						</div>
-
-						{#if filteredItems.length > 0}
-							<div class=" flex overflow-y-auto h-full w-full scrollbar-hidden text-xs">
-								<Files
-									files={filteredItems}
-									{selectedFileId}
-									on:click={(e) => {
-										selectedFileId = selectedFileId === e.detail ? null : e.detail;
-									}}
-									on:delete={(e) => {
-										console.log(e.detail);
-
-										selectedFileId = null;
-										deleteFileHandler(e.detail);
-									}}
-								/>
-							</div>
-						{:else}
-							<div class="m-auto text-gray-500 text-xs">{$i18n.t('No content found')}</div>
-						{/if}
-					</div>
-				</div>
-			</div>
 
-			{#if largeScreen}
-				<div class="flex-1 flex justify-start h-full max-h-full pl-3">
-					{#if selectedFile}
-						<div class=" flex flex-col w-full h-full max-h-full">
-							<div class="flex-shrink-0 mb-2 flex items-center">
-								<div class=" flex-1 text-xl line-clamp-1">
-									{selectedFile?.meta?.name}
+										<div>
+											<AddContentMenu
+												on:upload={(e) => {
+													if (e.detail.type === 'directory') {
+														uploadDirectoryHandler();
+													} else if (e.detail.type === 'text') {
+														showAddTextContentModal = true;
+													} else {
+														document.getElementById('files-input').click();
+													}
+												}}
+												on:sync={(e) => {
+													showSyncConfirmModal = true;
+												}}
+											/>
+										</div>
+									</div>
 								</div>
 
-								<div>
-									<button
-										class="self-center w-fit text-sm py-1 px-2.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-lg"
-										on:click={() => {
-											updateFileContentHandler();
-										}}
-									>
-										{$i18n.t('Save')}
-									</button>
-								</div>
+								{#if filteredItems.length > 0}
+									<div class=" flex overflow-y-auto h-full w-full scrollbar-hidden text-xs">
+										<Files
+											files={filteredItems}
+											{selectedFileId}
+											on:click={(e) => {
+												selectedFileId = selectedFileId === e.detail ? null : e.detail;
+											}}
+											on:delete={(e) => {
+												console.log(e.detail);
+
+												selectedFileId = null;
+												deleteFileHandler(e.detail);
+											}}
+										/>
+									</div>
+								{:else}
+									<div class="m-auto text-gray-500 text-xs">{$i18n.t('No content found')}</div>
+								{/if}
 							</div>
+						</div>
+					</div>
+				</Pane>
 
-							<div
-								class=" flex-1 w-full h-full max-h-full py-2.5 px-3.5 rounded-xl text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none overflow-y-auto scrollbar-hidden"
-							>
-								{#key selectedFile.id}
-									<RichTextInput
-										className="input-prose-sm"
-										bind:value={selectedFile.data.content}
-										placeholder={$i18n.t('Add content here')}
-									/>
-								{/key}
-							</div>
+				{#if largeScreen}
+					<PaneResizer class="relative flex w-2 items-center justify-center bg-background group">
+						<div class="z-10 flex h-7 w-5 items-center justify-center rounded-sm">
+							<EllipsisVertical className="size-4 invisible group-hover:visible" />
 						</div>
-					{:else}
-						<div class="m-auto pb-32">
-							<div>
-								<div class=" flex w-full mt-1 mb-3.5">
-									<div class="flex-1">
-										<div class="flex items-center justify-between w-full px-0.5 mb-1">
-											<div class="w-full">
-												<input
-													type="text"
-													class="text-center w-full font-medium text-3xl font-primary bg-transparent outline-none"
-													bind:value={knowledge.name}
-													on:input={() => {
-														changeDebounceHandler();
+					</PaneResizer>
+					<Pane>
+						<div class="flex-1 flex justify-start h-full max-h-full">
+							{#if selectedFile}
+								<div class=" flex flex-col w-full h-full max-h-full ml-2">
+									<div class="flex-shrink-0 mb-2 flex items-center">
+										{#if !showSidepanel}
+											<div class="-translate-x-2">
+												<button
+													class="w-full text-left text-sm p-1.5 rounded-lg dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-gray-850"
+													on:click={() => {
+														pane.expand();
 													}}
-												/>
+												>
+													<ChevronLeft strokeWidth="2.5" />
+												</button>
 											</div>
+										{/if}
+
+										<div class=" flex-1 text-2xl font-medium line-clamp-1">
+											{selectedFile?.meta?.name}
 										</div>
 
-										<div class="flex w-full px-1">
-											<input
-												type="text"
-												class="text-center w-full text-gray-500 bg-transparent outline-none"
-												bind:value={knowledge.description}
-												on:input={() => {
-													changeDebounceHandler();
+										<div>
+											<button
+												class="self-center w-fit text-sm py-1 px-2.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-lg"
+												on:click={() => {
+													updateFileContentHandler();
 												}}
+											>
+												{$i18n.t('Save')}
+											</button>
+										</div>
+									</div>
+
+									<div
+										class=" flex-1 w-full h-full max-h-full text-sm bg-transparent outline-none overflow-y-auto scrollbar-hidden"
+									>
+										{#key selectedFile.id}
+											<RichTextInput
+												className="input-prose-sm"
+												bind:value={selectedFile.data.content}
+												placeholder={$i18n.t('Add content here')}
 											/>
+										{/key}
+									</div>
+								</div>
+							{:else}
+								<div class="m-auto pb-32">
+									<div>
+										<div class=" flex w-full mt-1 mb-3.5">
+											<div class="flex-1">
+												<div class="flex items-center justify-between w-full px-0.5 mb-1">
+													<div class="w-full">
+														<input
+															type="text"
+															class="text-center w-full font-medium text-3xl font-primary bg-transparent outline-none"
+															bind:value={knowledge.name}
+															on:input={() => {
+																changeDebounceHandler();
+															}}
+														/>
+													</div>
+												</div>
+
+												<div class="flex w-full px-1">
+													<input
+														type="text"
+														class="text-center w-full text-gray-500 bg-transparent outline-none"
+														bind:value={knowledge.description}
+														on:input={() => {
+															changeDebounceHandler();
+														}}
+													/>
+												</div>
+											</div>
 										</div>
 									</div>
+
+									<div class=" mt-2 text-center text-sm text-gray-200 dark:text-gray-700 w-full">
+										{$i18n.t('Select a file to view or drag and drop a file to upload')}
+									</div>
+								</div>
+							{/if}
+						</div>
+					</Pane>
+				{:else if !largeScreen && selectedFileId !== null}
+					<Drawer
+						className="h-full"
+						show={selectedFileId !== null}
+						on:close={() => {
+							selectedFileId = null;
+						}}
+					>
+						<div class="flex flex-col justify-start h-full max-h-full p-2">
+							<div class=" flex flex-col w-full h-full max-h-full">
+								<div class="flex-shrink-0 mb-2 flex items-center">
+									<div class="mr-2">
+										<button
+											class="w-full text-left text-sm p-1.5 rounded-lg dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-gray-850"
+											on:click={() => {
+												selectedFileId = null;
+											}}
+										>
+											<ChevronLeft strokeWidth="2.5" />
+										</button>
+									</div>
+									<div class=" flex-1 text-xl line-clamp-1">
+										{selectedFile?.meta?.name}
+									</div>
+
+									<div>
+										<button
+											class="self-center w-fit text-sm py-1 px-2.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-lg"
+											on:click={() => {
+												updateFileContentHandler();
+											}}
+										>
+											{$i18n.t('Save')}
+										</button>
+									</div>
 								</div>
-							</div>
 
-							<div class=" mt-2 text-center text-sm text-gray-200 dark:text-gray-700 w-full">
-								{$i18n.t('Select a file to view or drag and drop a file to upload')}
+								<div
+									class=" flex-1 w-full h-full max-h-full py-2.5 px-3.5 rounded-xl text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none overflow-y-auto scrollbar-hidden"
+								>
+									{#key selectedFile.id}
+										<RichTextInput
+											className="input-prose-sm"
+											bind:value={selectedFile.data.content}
+											placeholder={$i18n.t('Add content here')}
+										/>
+									{/key}
+								</div>
 							</div>
 						</div>
-					{/if}
-				</div>
-			{/if}
+					</Drawer>
+				{/if}
+			</PaneGroup>
 		</div>
 	{:else}
 		<Spinner />

+ 1 - 1
src/lib/components/workspace/Knowledge/Collection/AddContentMenu.svelte

@@ -29,7 +29,7 @@
 >
 	<Tooltip content={$i18n.t('Add Content')}>
 		<button
-			class=" px-2 py-2 rounded-xl border border-gray-50 dark:border-gray-600 dark:border-0 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 transition font-medium text-sm flex items-center space-x-1"
+			class=" p-1.5 rounded-xl hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition font-medium text-sm flex items-center space-x-1"
 			on:click={(e) => {
 				e.stopPropagation();
 				show = true;

+ 58 - 81
src/lib/components/workspace/Knowledge/Collection/AddTextContentModal.svelte

@@ -7,98 +7,75 @@
 	const dispatch = createEventDispatcher();
 
 	import Modal from '$lib/components/common/Modal.svelte';
+	import RichTextInput from '$lib/components/common/RichTextInput.svelte';
+	import XMark from '$lib/components/icons/XMark.svelte';
 	export let show = false;
 
-	let name = '';
+	let name = 'Untitled';
 	let content = '';
 </script>
 
-<Modal size="md" bind:show>
-	<div>
-		<div class=" flex justify-between dark:text-gray-300 px-5 pt-4">
-			<div class=" text-lg font-medium self-center">{$i18n.t('Add Content')}</div>
-			<button
-				class="self-center"
-				on:click={() => {
-					show = false;
-				}}
-			>
-				<svg
-					xmlns="http://www.w3.org/2000/svg"
-					viewBox="0 0 20 20"
-					fill="currentColor"
-					class="w-5 h-5"
-				>
-					<path
-						d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
-					/>
-				</svg>
-			</button>
-		</div>
-		<div class="flex flex-col md:flex-row w-full px-5 py-4 md:space-x-4 dark:text-gray-200">
-			<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
-				<form
-					class="flex flex-col w-full"
-					on:submit|preventDefault={() => {
-						if (name.trim() === '' || content.trim() === '') {
-							toast.error($i18n.t('Please fill in all fields.'));
-							name = '';
-							content = '';
-							return;
-						}
-
-						dispatch('submit', {
-							name,
-							content
-						});
-						show = false;
-						name = '';
-						content = '';
-					}}
-				>
-					<div class="mb-3 w-full">
-						<div class="w-full flex flex-col gap-2.5">
-							<div class="w-full">
-								<div class=" text-sm mb-2">{$i18n.t('Title')}</div>
-
-								<div class="w-full mt-1">
-									<input
-										class="w-full rounded-lg py-2 px-4 text-sm bg-white dark:text-gray-300 dark:bg-gray-850 outline-none"
-										type="text"
-										bind:value={name}
-										placeholder={`Name your content`}
-										required
-									/>
-								</div>
-							</div>
-
-							<div>
-								<div class="text-sm mb-2">{$i18n.t('Content')}</div>
+<Modal size="full" className="h-full bg-white dark:bg-gray-900" bind:show>
+	<div class="absolute top-0 right-0 p-5">
+		<button
+			class="self-center dark:text-white"
+			type="button"
+			on:click={() => {
+				show = false;
+			}}
+		>
+			<XMark className="size-4" />
+		</button>
+	</div>
+	<div class="flex flex-col md:flex-row w-full h-full md:space-x-4 dark:text-gray-200">
+		<form
+			class="flex flex-col w-full h-full"
+			on:submit|preventDefault={() => {
+				if (name.trim() === '' || content.trim() === '') {
+					toast.error($i18n.t('Please fill in all fields.'));
+					name = name.trim();
+					content = content.trim();
+					return;
+				}
 
-								<div class=" w-full mt-1">
-									<textarea
-										class="w-full resize-none rounded-lg py-2 px-4 text-sm bg-whites dark:text-gray-300 dark:bg-gray-850 outline-none"
-										rows="10"
-										bind:value={content}
-										placeholder={`Write your content here`}
-										required
-									/>
-								</div>
-							</div>
+				dispatch('submit', {
+					name,
+					content
+				});
+				show = false;
+				name = '';
+				content = '';
+			}}
+		>
+			<div class=" flex-1 w-full h-full flex justify-center overflow-auto px-5 py-4">
+				<div class=" max-w-3xl py-2 md:py-14 w-full flex flex-col gap-2">
+					<div class="flex-shrink-0 w-full flex justify-between items-center">
+						<div class="w-full">
+							<input
+								class="w-full text-3xl font-semibold rounded-lg bg-transparent outline-none"
+								type="text"
+								bind:value={name}
+								placeholder={$i18n.t('Title')}
+								required
+							/>
 						</div>
 					</div>
 
-					<div class="flex justify-end text-sm font-medium">
-						<button
-							class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
-							type="submit"
-						>
-							{$i18n.t('Add Content')}
-						</button>
+					<div class=" flex-1 w-full h-full">
+						<RichTextInput bind:value={content} placeholder={$i18n.t('Content')} />
 					</div>
-				</form>
+				</div>
+			</div>
+
+			<div class="flex justify-end text-sm font-medium flex-shrink-0 mt-1 py-3 px-3">
+				<button
+					class=" px-3.5 py-2 bg-black text-white dark:bg-white dark:text-black transition rounded-full"
+					type="submit"
+				>
+					{$i18n.t('Save')}
+				</button>
 			</div>
-		</div>
+		</form>
 	</div>
 </Modal>
 

+ 1 - 1
src/lib/components/workspace/Knowledge/Collection/Files.svelte

@@ -10,7 +10,7 @@
 
 <div class=" max-h-full flex flex-col w-full">
 	{#each files as file}
-		<div class="mt-2 px-2">
+		<div class="mt-1 px-2">
 			<FileItem
 				className="w-full"
 				colorClassName="{selectedFileId === file.id

+ 1 - 1
src/routes/(app)/workspace/+layout.svelte

@@ -110,7 +110,7 @@
 			</div>
 		</div>
 
-		<div class=" pb-1 px-5 flex-1 max-h-full overflow-y-auto">
+		<div class=" pb-1 px-[18px] flex-1 max-h-full overflow-y-auto">
 			<slot />
 		</div>
 	</div>