Preview Updated 2026-05-10

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.

背景
// orders.ts
src/App.vue
<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.

背景
// orders.ts
拖动这个 tab 向下可触发 tear-off →
src/App.vue
<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

PropTypeDefaultDescription
tabsTearOffTabItem[]{ id, title, contentKey?, modified?, closable? }
modelValue / valuestringfirst tabActive tab id
tearThresholdnumber60Vertical drag distance that triggers tear-off

Events: update:modelValue / tear-off(id, item, x, y) / close(id, item).

反馈与讨论

Tear-off tab · Discussion

0
0 / 600
一键发送
正在加载评论...