fluix 0.1.22

Rust UI components for GPUI
Documentation
<script lang="ts">
	import { base } from '$app/paths';
	
	const components = [
		{ id: 'button', name: 'Button', demo: 'button_demo' },
		{ id: 'icon', name: 'Icon', demo: 'icon_demo' },
		{ id: 'textinput', name: 'TextInput', demo: 'text_input_demo' },
		{ id: 'checkbox', name: 'Checkbox', demo: 'checkbox_demo' },
		{ id: 'select', name: 'Select', demo: 'select_demo' },
		{ id: 'combobox', name: 'Combobox', demo: 'combobox_demo' }
	];
	
	let selectedComponent = $state('button');
	
	const componentDescriptions: Record<string, string> = {
		button: 'Interactive button component with multiple variants and sizes.',
		icon: '22 built-in icons with customizable sizes and colors.',
		textinput: 'Text input field with validation and event handling.',
		checkbox: 'Checkbox component for boolean selections.',
		select: 'Dropdown selection component with single and multiple selection modes.',
		combobox: 'Combobox component with search and selection capabilities.'
	};
	
	function getScreenshotPath(componentId: string): string {
		return `${base}/screenshots/components/${componentId}-demo.png`;
	}
	
	function getScreenshotPaths(componentId: string): string[] {
		if (componentId === 'icon') {
			return [
				`${base}/screenshots/components/icon-demo.png`,
				`${base}/screenshots/components/icon-demo2.png`
			];
		}
		if (componentId === 'textinput') {
			return [
				`${base}/screenshots/components/textinput-demo.png`,
				`${base}/screenshots/components/textinput-demo2.png`
			];
		}
		if (componentId === 'select') {
			return [
				`${base}/screenshots/components/select-demo.png`,
				`${base}/screenshots/components/select-demo2.png`,
				`${base}/screenshots/components/select-demo3.png`
			];
		}
		return [getScreenshotPath(componentId)];
	}
</script>

<div class="components-layout">
	<!-- Sidebar Navigation -->
	<aside class="sidebar">
		<h2>Components</h2>
		<nav class="component-nav">
			{#each components as component}
				<button 
					class="nav-item"
					class:active={selectedComponent === component.id}
					onclick={() => selectedComponent = component.id}
				>
					{component.name}
				</button>
			{/each}
		</nav>
	</aside>
	
	<!-- Main Content -->
	<main class="content">
		<div class="content-header">
			<h1>{components.find(c => c.id === selectedComponent)?.name}</h1>
			<p class="component-description">
				{componentDescriptions[selectedComponent]}
			</p>
		</div>
		
		<div class="demo-content">
			{#if selectedComponent === 'icon' || selectedComponent === 'textinput' || selectedComponent === 'select'}
				<!-- Components with multiple screenshots -->
				{#each getScreenshotPaths(selectedComponent) as screenshotPath, index}
					<div class="screenshot-container">
						<img 
							src={screenshotPath}
							alt="{components.find(c => c.id === selectedComponent)?.name} Demo {index + 1}"
							class="component-screenshot"
							onerror={(e) => {
								const img = e.target as HTMLImageElement;
								img.style.display = 'none';
								const placeholder = img.nextElementSibling as HTMLElement;
								if (placeholder) placeholder.style.display = 'flex';
							}}
						/>
						<div class="screenshot-placeholder" style="display: none;">
							<div class="placeholder-content">
								<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
									<rect x="3" y="3" width="18" height="18" rx="2" stroke-dasharray="4 4"/>
									<path d="M8 12h8M12 8v8"/>
								</svg>
								<h3>截图未找到</h3>
								<p class="hint">截图保存位置:<code>websites/static/screenshots/components/{screenshotPath.split('/').pop()}</code></p>
							</div>
						</div>
					</div>
				{/each}
			{:else}
				<!-- Other components with single screenshot -->
				<div class="screenshot-container">
					<img 
						src={getScreenshotPath(selectedComponent)}
						alt="{components.find(c => c.id === selectedComponent)?.name} Demo"
						class="component-screenshot"
						onerror={(e) => {
							const img = e.target as HTMLImageElement;
							img.style.display = 'none';
							const placeholder = img.nextElementSibling as HTMLElement;
							if (placeholder) placeholder.style.display = 'flex';
						}}
					/>
					<div class="screenshot-placeholder" style="display: none;">
						<div class="placeholder-content">
							<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
								<rect x="3" y="3" width="18" height="18" rx="2" stroke-dasharray="4 4"/>
								<path d="M8 12h8M12 8v8"/>
							</svg>
							<h3>截图未找到</h3>
							<p>请运行以下命令并截图:</p>
							<code class="code-block">
								cargo run --example {components.find(c => c.id === selectedComponent)?.demo}
							</code>
							<p class="hint">截图保存位置:<code>websites/static/screenshots/components/{selectedComponent}-demo.png</code></p>
						</div>
					</div>
				</div>
			{/if}
		</div>
	</main>
</div>

<style>
	.components-layout {
		display: flex;
		gap: 2rem;
		min-height: calc(100vh - 4rem);
	}

	/* Sidebar */
	.sidebar {
		width: 280px;
		flex-shrink: 0;
		padding: 1.5rem;
		border-right: 1px solid var(--border);
		background: var(--card);
		position: sticky;
		top: 4rem;
		height: fit-content;
		max-height: calc(100vh - 4rem);
		overflow-y: auto;
	}

	.sidebar h2 {
		font-size: 1.125rem;
		font-weight: 600;
		margin-bottom: 1rem;
		color: var(--foreground);
	}

	.component-nav {
		display: flex;
		flex-direction: column;
		gap: 0.25rem;
	}

	.nav-item {
		display: flex;
		align-items: center;
		padding: 0.75rem 1rem;
		border: none;
		background: transparent;
		color: var(--foreground);
		font-size: 0.875rem;
		cursor: pointer;
		border-radius: 0.375rem;
		transition: background-color 0.2s;
		text-align: left;
		font-weight: 500;
	}

	.nav-item:hover {
		background: var(--secondary);
	}

	.nav-item.active {
		background: var(--primary);
		color: var(--primary-foreground);
	}

	/* Content */
	.content {
		flex: 1;
		padding: 2rem;
		max-width: 1400px;
	}

	.content-header h1 {
		font-size: 2rem;
		font-weight: 700;
		margin-bottom: 0.5rem;
		color: var(--primary);
	}

	.component-description {
		color: var(--muted-foreground);
		margin-bottom: 2rem;
		font-size: 1rem;
	}

	.demo-content {
		display: flex;
		flex-direction: column;
		gap: 2rem;
	}

	.screenshot-container {
		position: relative;
		width: 100%;
		border: 1px solid var(--border);
		border-radius: 0.5rem;
		background: var(--card);
		overflow: hidden;
		min-height: 400px;
		display: flex;
		align-items: center;
		justify-content: center;
	}

	.component-screenshot {
		width: 100%;
		height: auto;
		display: block;
		max-width: 100%;
	}

	.screenshot-placeholder {
		position: absolute;
		top: 0;
		left: 0;
		right: 0;
		bottom: 0;
		display: flex;
		align-items: center;
		justify-content: center;
		background: var(--background);
	}

	.placeholder-content {
		text-align: center;
		padding: 2rem;
		max-width: 500px;
	}

	.placeholder-content svg {
		color: var(--muted-foreground);
		margin-bottom: 1rem;
	}

	.placeholder-content h3 {
		font-size: 1.125rem;
		font-weight: 600;
		margin-bottom: 0.5rem;
		color: var(--foreground);
	}

	.placeholder-content p {
		color: var(--muted-foreground);
		margin-bottom: 1rem;
		font-size: 0.875rem;
	}

	.code-block {
		display: block;
		padding: 1rem;
		background: var(--secondary);
		border-radius: 0.375rem;
		border: 1px solid var(--border);
		font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
		font-size: 0.875rem;
		color: var(--foreground);
		overflow-x: auto;
		margin: 0.75rem 0;
	}

	code {
		background: var(--secondary);
		padding: 0.125rem 0.375rem;
		border-radius: 0.25rem;
		font-size: 0.875rem;
		color: var(--foreground);
		font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
	}

	.hint {
		color: var(--muted-foreground);
		font-size: 0.8125rem;
		margin-top: 0.5rem;
	}

	/* Responsive */
	@media (max-width: 768px) {
		.components-layout {
			flex-direction: column;
		}

		.sidebar {
			width: 100%;
			position: relative;
			top: 0;
			border-right: none;
			border-bottom: 1px solid var(--border);
		}

		.content {
			padding: 1.5rem 1rem;
		}

		.screenshot-container {
			min-height: 300px;
		}
	}
</style>