Use Tailwind apply for Obsidian-style settings

This commit is contained in:
shi.changliang 2026-06-04 15:10:06 +08:00
parent 80680adeb2
commit bda621be02
6 changed files with 263 additions and 265 deletions

View file

@ -20,14 +20,15 @@
"author": "Chaly",
"license": "MIT",
"devDependencies": {
"@tailwindcss/postcss": "4.3.0",
"@types/node": "^24.10.1",
"obsidian": "^1.8.7",
"postcss": "^8.5.15",
"typescript": "^6.0.3",
"vite": "^8.0.16",
"vitest": "^4.1.8"
},
"dependencies": {
"@tailwindcss/vite": "^4.3.0",
"@vitejs/plugin-vue": "^6.0.7",
"sass": "^1.100.0",
"tailwindcss": "^4.3.0",

View file

@ -8,9 +8,6 @@ importers:
.:
dependencies:
'@tailwindcss/vite':
specifier: ^4.3.0
version: 4.3.0(vite@8.0.16(@types/node@24.12.4)(jiti@2.7.0)(sass@1.100.0))
'@vitejs/plugin-vue':
specifier: ^6.0.7
version: 6.0.7(vite@8.0.16(@types/node@24.12.4)(jiti@2.7.0)(sass@1.100.0))(vue@3.5.35(typescript@6.0.3))
@ -24,12 +21,18 @@ importers:
specifier: ^3.5.35
version: 3.5.35(typescript@6.0.3)
devDependencies:
'@tailwindcss/postcss':
specifier: 4.3.0
version: 4.3.0
'@types/node':
specifier: ^24.10.1
version: 24.12.4
obsidian:
specifier: ^1.8.7
version: 1.13.0(@codemirror/state@6.5.0)(@codemirror/view@6.38.6)
postcss:
specifier: ^8.5.15
version: 8.5.15
typescript:
specifier: ^6.0.3
version: 6.0.3
@ -42,6 +45,10 @@ importers:
packages:
'@alloc/quick-lru@5.2.0':
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
'@babel/helper-string-parser@7.29.7':
resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
engines: {node: '>=6.9.0'}
@ -380,10 +387,8 @@ packages:
resolution: {integrity: sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==}
engines: {node: '>= 20'}
'@tailwindcss/vite@4.3.0':
resolution: {integrity: sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==}
peerDependencies:
vite: ^5.2.0 || ^6 || ^7 || ^8
'@tailwindcss/postcss@4.3.0':
resolution: {integrity: sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w==}
'@tybys/wasm-util@0.10.2':
resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==}
@ -825,6 +830,8 @@ packages:
snapshots:
'@alloc/quick-lru@5.2.0': {}
'@babel/helper-string-parser@7.29.7': {}
'@babel/helper-validator-identifier@7.29.7': {}
@ -1070,12 +1077,13 @@ snapshots:
'@tailwindcss/oxide-win32-arm64-msvc': 4.3.0
'@tailwindcss/oxide-win32-x64-msvc': 4.3.0
'@tailwindcss/vite@4.3.0(vite@8.0.16(@types/node@24.12.4)(jiti@2.7.0)(sass@1.100.0))':
'@tailwindcss/postcss@4.3.0':
dependencies:
'@alloc/quick-lru': 5.2.0
'@tailwindcss/node': 4.3.0
'@tailwindcss/oxide': 4.3.0
postcss: 8.5.15
tailwindcss: 4.3.0
vite: 8.0.16(@types/node@24.12.4)(jiti@2.7.0)(sass@1.100.0)
'@tybys/wasm-util@0.10.2':
dependencies:

6
postcss.config.mjs Normal file
View file

@ -0,0 +1,6 @@
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};

View file

@ -38,145 +38,185 @@ const emit = defineEmits<{
<div class="obsync-status">MVP</div>
</header>
<section class="obsync-section">
<div class="obsync-section-copy">
<h3>对象存储</h3>
<p>填写 S3 兼容对象存储信息Cloudflare R2 Region 通常填 auto</p>
</div>
<div class="obsync-grid">
<label class="obsync-field obsync-field-wide">
<span>服务端点</span>
<section class="obsync-card">
<div class="obsync-row">
<div class="obsync-row-copy">
<h3>服务端点</h3>
<p>S3 兼容对象存储地址例如 Cloudflare R2 endpoint</p>
</div>
<div class="obsync-control obsync-control-wide">
<input
:value="props.settings.endpoint"
placeholder="https://xxx.r2.cloudflarestorage.com"
@input="emit('update', { endpoint: ($event.target as HTMLInputElement).value.trim() })"
/>
</label>
</div>
</div>
<label class="obsync-field">
<span>Bucket</span>
<div class="obsync-row">
<div class="obsync-row-copy">
<h3>Bucket</h3>
<p>用于保存同步文件的对象存储桶</p>
</div>
<div class="obsync-control">
<input
:value="props.settings.bucket"
placeholder="ideabase"
@input="emit('update', { bucket: ($event.target as HTMLInputElement).value.trim() })"
/>
</label>
</div>
</div>
<label class="obsync-field">
<span>Region</span>
<div class="obsync-row">
<div class="obsync-row-copy">
<h3>Region</h3>
<p>R2 通常填写 auto其他 S3 服务按实际区域填写</p>
</div>
<div class="obsync-control">
<input
:value="props.settings.region"
placeholder="auto"
@input="emit('update', { region: ($event.target as HTMLInputElement).value.trim() })"
/>
</label>
</div>
</div>
<label class="obsync-field">
<span>根路径</span>
<div class="obsync-row">
<div class="obsync-row-copy">
<h3>根路径</h3>
<p>同一个 bucket 内的插件数据前缀</p>
</div>
<div class="obsync-control">
<input
:value="props.settings.rootPrefix"
placeholder="obsync/v1"
@input="emit('update', { rootPrefix: ($event.target as HTMLInputElement).value.trim() || 'obsync/v1' })"
/>
</label>
</div>
</div>
<label class="obsync-field">
<span>Access Key ID</span>
<div class="obsync-row">
<div class="obsync-row-copy">
<h3>Access Key ID</h3>
<p>对象存储访问密钥 ID仅保存在本地</p>
</div>
<div class="obsync-control">
<input
:value="props.settings.accessKeyId"
autocomplete="off"
@input="emit('update', { accessKeyId: ($event.target as HTMLInputElement).value.trim() })"
/>
</label>
</div>
</div>
<label class="obsync-field obsync-field-wide">
<span>Secret Access Key</span>
<div class="obsync-row">
<div class="obsync-row-copy">
<h3>Secret Access Key</h3>
<p>不会包含在连接配置中</p>
</div>
<div class="obsync-control obsync-control-wide">
<input
type="password"
:value="props.settings.secretAccessKey"
autocomplete="off"
@input="emit('update', { secretAccessKey: ($event.target as HTMLInputElement).value })"
/>
</label>
</div>
</div>
</section>
<section class="obsync-section">
<div class="obsync-section-copy">
<h3>仓库身份</h3>
<p>多台设备要使用相同的账号标识和仓库标识才能同步到同一个远端仓库</p>
</div>
<div class="obsync-grid">
<label class="obsync-field">
<span>账号标识</span>
<section class="obsync-card">
<div class="obsync-row">
<div class="obsync-row-copy">
<h3>账号标识</h3>
<p>同一用户或团队在所有设备上保持一致</p>
</div>
<div class="obsync-control">
<input
:value="props.settings.accountKey"
@input="emit('update', { accountKey: ($event.target as HTMLInputElement).value })"
/>
</label>
</div>
</div>
<label class="obsync-field">
<span>仓库标识</span>
<div class="obsync-row">
<div class="obsync-row-copy">
<h3>仓库标识</h3>
<p>同一个 vault 在所有设备上保持一致</p>
</div>
<div class="obsync-control">
<input
:value="props.settings.vaultKey"
@input="emit('update', { vaultKey: ($event.target as HTMLInputElement).value })"
/>
</label>
</div>
</div>
<div class="obsync-readonly-row">
<div>
<span>Vault ID</span>
<code>{{ props.settings.vaultId }}</code>
<div class="obsync-row">
<div class="obsync-row-copy">
<h3>Vault ID</h3>
<p>根据账号标识和仓库标识自动计算</p>
</div>
<div class="obsync-readonly">
<code>{{ props.settings.vaultId }}</code>
<button type="button" @click="emit('copyVaultId')">复制</button>
</div>
<button type="button" @click="emit('copyVaultId')">复制</button>
</div>
</section>
<section class="obsync-section">
<div class="obsync-section-copy">
<h3>当前设备</h3>
<p>设备 ID 只保存在本地用于同步锁和冲突文件命名</p>
</div>
<label class="obsync-field">
<span>设备名称</span>
<section class="obsync-card">
<div class="obsync-row">
<div class="obsync-row-copy">
<h3>设备名称</h3>
<p>用于冲突文件名和界面展示</p>
</div>
<div class="obsync-control">
<input
:value="props.settings.deviceName"
@input="emit('update', { deviceName: ($event.target as HTMLInputElement).value.trim() || 'This device' })"
/>
</label>
</div>
</div>
<div class="obsync-readonly-row">
<div>
<span>设备 ID</span>
<div class="obsync-row">
<div class="obsync-row-copy">
<h3>设备 ID</h3>
<p>首次启用插件时随机生成</p>
</div>
<div class="obsync-readonly">
<code>{{ props.settings.deviceId }}</code>
</div>
</div>
<label class="obsync-toggle">
<input
type="checkbox"
:checked="props.settings.autoSync"
@change="emit('update', { autoSync: ($event.target as HTMLInputElement).checked })"
/>
<span>启用自动同步</span>
</label>
<div class="obsync-row">
<div class="obsync-row-copy">
<h3>自动同步</h3>
<p>启动恢复前台和文件变化时自动同步</p>
</div>
<label class="obsync-switch">
<input
type="checkbox"
:checked="props.settings.autoSync"
@change="emit('update', { autoSync: ($event.target as HTMLInputElement).checked })"
/>
<span></span>
</label>
</div>
</section>
<section class="obsync-section">
<div class="obsync-section-copy">
<h3>连接配置</h3>
<p>复制这段 JSON 到另一台设备导入这里不包含 Access Key Secret</p>
<section class="obsync-card">
<div class="obsync-row obsync-row-block">
<div class="obsync-row-copy">
<h3>连接配置</h3>
<p>复制到另一台设备导入这里不包含 Access Key Secret</p>
</div>
<pre>{{ JSON.stringify(props.connectionConfig, null, 2) }}</pre>
<button type="button" class="obsync-primary" @click="emit('copyConnectionConfig')">
复制连接配置
</button>
</div>
<pre>{{ JSON.stringify(props.connectionConfig, null, 2) }}</pre>
<button type="button" class="obsync-primary" @click="emit('copyConnectionConfig')">
复制连接配置
</button>
</section>
</main>
</template>

View file

@ -1,274 +1,219 @@
@reference "tailwindcss";
.obsync-settings-root {
container-type: inline-size;
}
.obsync-panel {
box-sizing: border-box;
width: 100%;
max-width: 760px;
padding: 0 0 24px;
@apply w-full max-w-3xl pb-6;
color: var(--text-normal);
}
.obsync-panel *,
.obsync-panel *::before,
.obsync-panel *::after {
box-sizing: border-box;
@apply box-border;
}
.obsync-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin-bottom: 16px;
padding-bottom: 14px;
border-bottom: 1px solid var(--background-modifier-border);
@apply mb-4 flex items-start justify-between gap-3;
}
.obsync-eyebrow,
.obsync-subtitle,
.obsync-section-copy p {
margin: 0;
.obsync-row-copy p {
@apply m-0;
color: var(--text-muted);
}
.obsync-eyebrow {
font-size: 12px;
font-weight: 600;
line-height: 1.4;
@apply text-xs font-semibold leading-5;
}
.obsync-title {
margin: 4px 0 0;
@apply m-0 mt-1 text-2xl font-bold leading-tight;
color: var(--text-normal);
font-size: 24px;
font-weight: 700;
line-height: 1.2;
}
.obsync-subtitle {
margin-top: 6px;
font-size: 14px;
line-height: 1.5;
@apply mt-1 text-sm leading-6;
}
.obsync-status {
flex: 0 0 auto;
@apply shrink-0 rounded-md px-2 py-1 text-xs leading-5;
border: 1px solid var(--background-modifier-border);
border-radius: 6px;
padding: 4px 8px;
color: var(--text-muted);
font-size: 12px;
line-height: 1.4;
}
.obsync-section {
margin-top: 12px;
border: 1px solid var(--background-modifier-border);
border-radius: 8px;
.obsync-card {
@apply mb-5 rounded-xl p-0;
background: var(--background-secondary);
padding: 14px;
}
.obsync-section-copy {
margin-bottom: 12px;
.obsync-row {
@apply flex items-center justify-between gap-5 px-5 py-4;
border-bottom: 1px solid var(--background-modifier-border);
}
.obsync-row:last-child {
@apply border-b-0;
}
.obsync-row-copy {
@apply min-w-0 flex-1;
h3 {
margin: 0;
@apply m-0 text-base font-semibold leading-6;
color: var(--text-normal);
font-size: 16px;
font-weight: 700;
line-height: 1.35;
}
p {
margin-top: 4px;
font-size: 13px;
line-height: 1.55;
@apply mt-0.5 text-sm leading-5;
}
}
.obsync-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 12px;
.obsync-control {
@apply w-60 shrink-0;
}
.obsync-field {
display: grid;
min-width: 0;
gap: 6px;
color: var(--text-muted);
font-size: 13px;
font-weight: 600;
span {
line-height: 1.35;
}
input {
width: 100%;
min-width: 0;
height: 36px;
border: 1px solid var(--background-modifier-border);
border-radius: 6px;
background: var(--background-primary);
color: var(--text-normal);
font-size: 14px;
line-height: 20px;
outline: none;
padding: 7px 10px;
transition: border-color 120ms ease, box-shadow 120ms ease;
}
input:focus {
border-color: var(--interactive-accent);
box-shadow: 0 0 0 2px var(--background-modifier-border-focus);
}
input::placeholder {
color: var(--text-faint);
}
.obsync-control-wide {
@apply w-80;
}
.obsync-field-wide {
grid-column: 1 / -1;
}
.obsync-readonly-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
min-width: 0;
margin-top: 12px;
border-radius: 6px;
.obsync-control input {
@apply h-9 w-full min-w-0 rounded-md px-3 py-2 text-sm outline-none transition;
background: var(--background-primary);
padding: 10px;
> div {
min-width: 0;
}
span {
display: block;
color: var(--text-muted);
font-size: 13px;
font-weight: 600;
line-height: 1.35;
}
code {
display: block;
margin-top: 4px;
color: var(--text-normal);
font-size: 12px;
line-height: 1.45;
overflow-wrap: anywhere;
white-space: normal;
}
border: 1px solid var(--background-modifier-border);
color: var(--text-normal);
}
.obsync-readonly-row button,
.obsync-control input:focus {
border-color: var(--interactive-accent);
box-shadow: 0 0 0 2px var(--background-modifier-border-focus);
}
.obsync-control input::placeholder {
color: var(--text-faint);
}
.obsync-readonly {
@apply flex min-w-0 shrink-0 items-center justify-end gap-3;
width: min(380px, 58%);
}
.obsync-readonly code {
@apply min-w-0 flex-1 break-all text-right text-xs leading-5;
color: var(--text-normal);
}
.obsync-readonly button,
.obsync-primary {
border: none;
border-radius: 6px;
@apply shrink-0 cursor-pointer rounded-md border-0 px-3 py-2 text-sm font-semibold leading-5 transition;
background: var(--interactive-accent);
color: var(--text-on-accent);
cursor: pointer;
flex: 0 0 auto;
font-size: 14px;
font-weight: 600;
line-height: 20px;
min-height: 34px;
padding: 7px 12px;
transition: opacity 120ms ease, transform 120ms ease;
}
.obsync-readonly-row button:hover,
.obsync-readonly button:hover,
.obsync-primary:hover {
opacity: 0.92;
@apply opacity-90;
}
.obsync-readonly-row button:active,
.obsync-readonly button:active,
.obsync-primary:active {
transform: translateY(1px);
@apply translate-y-px;
}
.obsync-toggle {
display: flex;
align-items: center;
gap: 8px;
margin-top: 12px;
color: var(--text-normal);
font-size: 14px;
line-height: 1.4;
.obsync-switch {
@apply relative block h-6 w-11 shrink-0 cursor-pointer;
input {
flex: 0 0 auto;
width: 16px;
height: 16px;
@apply sr-only;
}
span {
@apply absolute inset-0 rounded-full transition;
background: var(--background-modifier-border);
}
span::before {
@apply absolute left-1 top-1 h-4 w-4 rounded-full transition;
background: var(--background-primary);
content: "";
}
input:checked + span {
background: var(--interactive-accent);
}
input:checked + span::before {
@apply translate-x-5;
}
}
.obsync-section pre {
max-height: 240px;
margin: 0 0 12px;
overflow: auto;
border-radius: 6px;
background: var(--background-primary);
color: var(--text-normal);
font-size: 12px;
line-height: 1.5;
padding: 10px;
white-space: pre-wrap;
overflow-wrap: anywhere;
.obsync-row-block {
@apply block;
}
@container (max-width: 560px) {
.obsync-row-block .obsync-row-copy {
@apply mb-3;
}
.obsync-row-block pre {
@apply mb-3 max-h-60 overflow-auto rounded-md p-3 text-xs leading-5;
background: var(--background-primary);
color: var(--text-normal);
overflow-wrap: anywhere;
white-space: pre-wrap;
}
@container (max-width: 640px) {
.obsync-header {
display: block;
@apply block;
}
.obsync-status {
display: inline-block;
margin-top: 10px;
@apply mt-3 inline-block;
}
.obsync-grid {
grid-template-columns: 1fr;
.obsync-card {
@apply mb-4 rounded-lg;
}
.obsync-field-wide {
grid-column: auto;
.obsync-row {
@apply block px-4 py-4;
}
.obsync-readonly-row {
align-items: stretch;
flex-direction: column;
button {
width: 100%;
}
.obsync-row-copy {
@apply mb-2;
}
.obsync-control,
.obsync-control-wide,
.obsync-readonly {
@apply w-full;
}
.obsync-readonly {
@apply flex-col items-stretch gap-2;
}
.obsync-readonly code {
@apply text-left;
}
.obsync-readonly button,
.obsync-primary {
width: 100%;
@apply w-full;
}
.obsync-switch {
@apply mt-2;
}
}
@media (max-width: 560px) {
.obsync-panel {
padding-bottom: 16px;
}
.obsync-title {
font-size: 22px;
}
.obsync-section {
padding: 12px;
@apply text-xl;
}
}

View file

@ -1,5 +1,4 @@
import { builtinModules } from "node:module";
import tailwindcss from "@tailwindcss/vite";
import vue from "@vitejs/plugin-vue";
import { defineConfig } from "vite";
@ -22,7 +21,6 @@ const obsidianExternals = [
export default defineConfig({
plugins: [
vue(),
tailwindcss(),
],
build: {
lib: {