Tear-off tab
Drag a tab past the threshold to fire a tear-off event; the consumer decides whether to spawn a DetachedPanel or a new window.
Basic usage
Standard tabs plus a drag gesture. When the vertical drag distance exceeds tearThreshold (default 60px), tear-off(id, item, x, y) fires.
The component itself does not create windows or panels — the consumer decides the behavior.
背景
<script setup lang="ts">
import { ref } from 'vue';
import { CfTearOffTabs } from '@chufix-design/vue';
const tabs = ref([
{ id: 'orders', title: 'orders.ts', closable: true, modified: true },
{ id: 'login', title: 'login.ts', closable: true },
{ id: 'readme', title: 'README.md', closable: true },
]);
const active = ref('orders');
</script>
<template>
<div style="height: 240px; border: 1px solid var(--line-1); border-radius: var(--r-6); overflow: hidden;">
<CfTearOffTabs
:tabs="tabs"
:model-value="active"
@update:model-value="(v) => active = v"
@tear-off="(id) => alert(`Torn off: ${id}\n(消费方在此 spawn DetachedPanel 或新窗口)`)"
@close="(id) => tabs = tabs.filter(t => t.id !== id)"
>
<template #content-orders>
<pre style="margin: 0; padding: 12px 16px; font-family: var(--font-mono); font-size: 12px;">// orders.ts</pre>
</template>
<template #content-login>
<pre style="margin: 0; padding: 12px 16px; font-family: var(--font-mono); font-size: 12px;">// login.ts</pre>
</template>
<template #content-readme>
<pre style="margin: 0; padding: 12px 16px; font-family: var(--font-mono); font-size: 12px;"># README</pre>
</template>
</CfTearOffTabs>
</div>
</template> <script setup>
import { ref } from 'vue';
import { CfTearOffTabs } from '@chufix-design/vue';
const tabs = ref([
{ id: 'orders', title: 'orders.ts', closable, modified: true },
{ id: 'login', title: 'login.ts', closable: true },
{ id: 'readme', title: 'README.md', closable: true },
]);
const active = ref('orders');
</script>
<template>
<div style="height: 240px; border: 1px solid var(--line-1); border-radius: var(--r-6); overflow: hidden;">
<CfTearOffTabs
:tabs="tabs"
:model-value="active"
@update:model-value="(v) => active = v"
@tear-off="(id) => alert(`Torn off: ${id}\n(消费方在此 spawn DetachedPanel 或新窗口)`)"
@close="(id) => tabs = tabs.filter(t => t.id !== id)"
>
<template #content-orders>
<pre style="margin: 0; padding: 12px 16px; font-family: var(--font-mono); font-size: 12px;">// orders.ts</pre>
</template>
<template #content-login>
<pre style="margin: 0; padding: 12px 16px; font-family: var(--font-mono); font-size: 12px;">// login.ts</pre>
</template>
<template #content-readme>
<pre style="margin: 0; padding: 12px 16px; font-family: var(--font-mono); font-size: 12px;"># README</pre>
</template>
</CfTearOffTabs>
</div>
</template> import { useState } from 'react';
import { CfTearOffTabs } from '@chufix-design/react';
export default function Demo() {
const tabs = [
{ id: 'orders', title: 'orders.ts', closable: true, modified: true },
{ id: 'login', title: 'login.ts', closable: true },
{ id: 'readme', title: 'README.md', closable: true },
];
const [active, setActive] = useState('orders');
return (
<>
<CfTearOffTabs
tabs={tabs}
value={active}
onChange={setActive}
onTearOff={(id) => spawnPanel(id)}
onClose={(id) => removeTab(id)}
slots={{ 'content-orders': <pre>orders.ts</pre> }}
/>
</>
);
} import { useState } from 'react';
import { CfTearOffTabs } from '@chufix-design/react';
export default function Demo() {
const tabs = [
{ id: 'orders', title: 'orders.ts', closable, modified: true },
{ id: 'login', title: 'login.ts', closable: true },
{ id: 'readme', title: 'README.md', closable: true },
];
const [active, setActive] = useState('orders');
return (
<>
<CfTearOffTabs
tabs={tabs}
value={active}
onChange={setActive}
onTearOff={(id) => spawnPanel(id)}
onClose={(id) => removeTab(id)}
slots={{ 'content-orders': <pre>orders.ts</pre> }}
/>
</>
);
} Tear-off into a DetachedPanel
Spawn the torn-off tab into a DetachedPanel immediately; reattach it back when the panel is closed.
背景
<script setup lang="ts">
import { ref } from 'vue';
import { CfTearOffTabs, CfDetachedPanel } from '@chufix-design/vue';
const tabs = ref([
{ id: 'orders', title: 'orders.ts', closable: true, modified: true },
{ id: 'login', title: 'login.ts', closable: true },
]);
const active = ref('orders');
const detachedId = ref<string | null>(null);
const detachedX = ref(0);
const detachedY = ref(0);
function tearOff(id: string, _: any, x: number, y: number) {
detachedId.value = id;
detachedX.value = Math.max(40, x - 100);
detachedY.value = Math.max(40, y - 20);
tabs.value = tabs.value.filter((t) => t.id !== id);
}
function reattach() {
if (detachedId.value) {
tabs.value = [
...tabs.value,
{ id: detachedId.value, title: `${detachedId.value}.ts`, closable: true },
];
detachedId.value = null;
}
}
</script>
<template>
<div style="height: 200px; border: 1px solid var(--line-1); border-radius: var(--r-6); overflow: hidden;">
<CfTearOffTabs
:tabs="tabs"
:model-value="active"
@update:model-value="(v) => active = v"
@tear-off="tearOff"
@close="(id) => tabs = tabs.filter((t) => t.id !== id)"
>
<template #content-orders>
<pre style="margin: 0; padding: 12px; font-family: var(--font-mono); font-size: 12px;">// orders.ts
拖动这个 tab 向下可触发 tear-off →</pre>
</template>
<template #content-login>
<pre style="margin: 0; padding: 12px; font-family: var(--font-mono); font-size: 12px;">// login.ts</pre>
</template>
</CfTearOffTabs>
</div>
<CfDetachedPanel
:open="detachedId !== null"
:x="detachedX"
:y="detachedY"
:title="`${detachedId ?? ''}.ts`"
:width="280"
:height="160"
@update:open="(v) => { if (!v) reattach(); }"
>
<pre style="margin: 0; padding: 0; font-family: var(--font-mono); font-size: 12px;">// 已被分离的 {{ detachedId }}
关闭面板会重新挂回 tab 栏。</pre>
</CfDetachedPanel>
</template> <script setup>
import { ref } from 'vue';
import { CfTearOffTabs, CfDetachedPanel } from '@chufix-design/vue';
const tabs = ref([
{ id: 'orders', title: 'orders.ts', closable, modified: true },
{ id: 'login', title: 'login.ts', closable: true },
]);
const active = ref('orders');
const detachedId = ref<string | null>(null);
const detachedX = ref(0);
const detachedY = ref(0);
function tearOff(id, _, x, y) {
detachedId.value = id;
detachedX.value = Math.max(40, x - 100);
detachedY.value = Math.max(40, y - 20);
tabs.value = tabs.value.filter((t) => t.id !== id);
}
function reattach() {
if (detachedId.value) {
tabs.value = [
...tabs.value,
{ id: detachedId.value, title: `${detachedId.value}.ts`, closable: true },
];
detachedId.value = null;
}
}
</script>
<template>
<div style="height: 200px; border: 1px solid var(--line-1); border-radius: var(--r-6); overflow: hidden;">
<CfTearOffTabs
:tabs="tabs"
:model-value="active"
@update:model-value="(v) => active = v"
@tear-off="tearOff"
@close="(id) => tabs = tabs.filter((t) => t.id !== id)"
>
<template #content-orders>
<pre style="margin: 0; padding: 12px; font-family: var(--font-mono); font-size: 12px;">// orders.ts
拖动这个 tab 向下可触发 tear-off →</pre>
</template>
<template #content-login>
<pre style="margin: 0; padding: 12px; font-family: var(--font-mono); font-size: 12px;">// login.ts</pre>
</template>
</CfTearOffTabs>
</div>
<CfDetachedPanel
:open="detachedId !== null"
:x="detachedX"
:y="detachedY"
:title="`${detachedId ?? ''}.ts`"
:width="280"
:height="160"
@update:open="(v) => { if (!v) reattach(); }"
>
<pre style="margin: 0; padding: 0; font-family: var(--font-mono); font-size: 12px;">// 已被分离的 {{ detachedId }}
关闭面板会重新挂回 tab 栏。</pre>
</CfDetachedPanel>
</template> import { useState } from 'react';
import { CfTearOffTabs } from '@chufix-design/react';
export default function Demo() {
const tabs = ref([
{ id: 'orders', title: 'orders.ts', closable: true, modified: true },
{ id: 'login', title: 'login.ts', closable: true },
]);
const [active, setActive] = useState('orders');
const detachedId = ref<string | null>(null);
const [detachedX, setDetachedX] = useState(0);
const [detachedY, setDetachedY] = useState(0);
function tearOff(id: string, _: any, x: number, y: number) {
detachedId.value = id;
detachedX.value = Math.max(40, x - 100);
detachedY.value = Math.max(40, y - 20);
tabs.value = tabs.value.filter((t) => t.id !== id);
}
function reattach() {
if (detachedId.value) {
tabs.value = [
...tabs.value,
{ id: detachedId.value, title: `${detachedId.value}.ts`, closable: true },
];
detachedId.value = null;
}
}
return (
<>
<div style={{ height: 200, border: "1px solid var(--line-1)", borderRadius: "var(--r-6)", overflow: "hidden" }}>
<CfTearOffTabs tabs={tabs} modelValue={active} onModelValueChange={(v) => setActive(v)}
onTearOff={tearOff}
onClose={(id) => tabs = tabs.filter((t) => t.id !== id)}
>
<pre style={{ margin: 0, padding: 12, fontFamily: "var(--font-mono)", fontSize: 12 }}>// orders.ts
拖动这个 tab 向下可触发 tear-off →</pre>
</>
);
} import { useState } from 'react';
import { CfTearOffTabs } from '@chufix-design/react';
export default function Demo() {
const tabs = ref([
{ id: 'orders', title: 'orders.ts', closable, modified: true },
{ id: 'login', title: 'login.ts', closable: true },
]);
const [active, setActive] = useState('orders');
const detachedId = ref<string | null>(null);
const [detachedX, setDetachedX] = useState(0);
const [detachedY, setDetachedY] = useState(0);
function tearOff(id, _, x, y) {
detachedId.value = id;
detachedX.value = Math.max(40, x - 100);
detachedY.value = Math.max(40, y - 20);
tabs.value = tabs.value.filter((t) => t.id !== id);
}
function reattach() {
if (detachedId.value) {
tabs.value = [
...tabs.value,
{ id: detachedId.value, title: `${detachedId.value}.ts`, closable: true },
];
detachedId.value = null;
}
}
return (
<>
<div style={{ height: 200, border: "1px solid var(--line-1)", borderRadius: "var(--r-6)", overflow: "hidden" }}>
<CfTearOffTabs tabs={tabs} modelValue={active} onModelValueChange={(v) => setActive(v)}
onTearOff={tearOff}
onClose={(id) => tabs = tabs.filter((t) => t.id !== id)}
>
<pre style={{ margin: 0, padding: 12, fontFamily: "var(--font-mono)", fontSize: 12 }}>// orders.ts
拖动这个 tab 向下可触发 tear-off →</pre>
</>
);
} API
| Prop | Type | Default | Description |
|---|---|---|---|
tabs | TearOffTabItem[] | — | { id, title, contentKey?, modified?, closable? } |
modelValue / value | string | first tab | Active tab id |
tearThreshold | number | 60 | Vertical drag distance that triggers tear-off |
Events: update:modelValue / tear-off(id, item, x, y) / close(id, item).
反馈与讨论
Tear-off tab · Discussion