Files
chatwoot/app/javascript/stories/TreeNode.vue
T
Shivam Mishra 4ede0f58d2 feat: replace histoire with a minimal in-house stories harness
Histoire is unmaintained and blocks upgrading Vite and other dev
dependencies. Replace it with a small standalone Vite app that keeps the
same `<Story>`/`<Variant>` template API so story files need no changes.

- Add vite.stories.config.ts and app/javascript/stories/ (sidebar tree
  with collapsible groups, search, dark-mode and RTL/LTR toggles, and a
  per-story error boundary)
- Globally register Story/Variant; migrate the histoire setup (store,
  i18n, plugins/directives) and seed a memory router for route-aware
  components
- Repoint story:dev/build/preview scripts and drop the histoire deps
- Remove histoire.config.ts, histoire.setup.ts and histoire.scss
2026-06-03 14:45:51 +05:30

77 lines
2.4 KiB
Vue

<script setup>
import { reactive } from 'vue';
// Recursive sidebar node. Each instance owns the collapsed state of its own
// direct child groups, so nesting just works. Plain buttons/divs (no <ul>/<li>)
// avoid the list markers the dashboard stylesheet injects.
const props = defineProps({
nodes: { type: Array, required: true },
selectedPath: { type: String, default: '' },
forceExpand: { type: Boolean, default: false },
depth: { type: Number, default: 0 },
});
defineEmits(['select']);
// Default: top-level (L1) groups open, everything deeper collapsed.
// `overrides` holds the explicit open/closed state once the user toggles a group.
const overrides = reactive({});
const isOpen = name => {
if (props.forceExpand) return true;
if (name in overrides) return overrides[name];
return props.depth === 0;
};
const toggle = name => {
overrides[name] = !isOpen(name);
};
</script>
<template>
<div class="flex flex-col gap-px">
<template v-for="node in nodes" :key="node.name">
<div v-if="node.type === 'group'" class="flex flex-col">
<button
type="button"
class="flex items-center w-full gap-1.5 px-2 py-1 rounded-md group text-n-slate-11 hover:bg-n-alpha-1"
@click="toggle(node.name)"
>
<span
class="transition-transform i-lucide-chevron-right size-3.5 shrink-0 text-n-slate-10"
:class="{ 'rotate-90': isOpen(node.name) }"
/>
<span
class="text-xs font-semibold tracking-wide uppercase truncate text-n-slate-10"
>
{{ node.name }}
</span>
</button>
<div
v-show="isOpen(node.name)"
class="ml-2.5 pl-2 border-l border-n-weak"
>
<TreeNode
:nodes="node.children"
:selected-path="selectedPath"
:force-expand="forceExpand"
:depth="depth + 1"
@select="$emit('select', $event)"
/>
</div>
</div>
<button
v-else
type="button"
class="block w-full px-2 py-1 ml-1 text-sm text-left truncate transition-colors rounded-md"
:class="
node.path === selectedPath
? 'bg-n-brand/10 text-n-brand font-medium'
: 'text-n-slate-11 hover:bg-n-alpha-1 hover:text-n-slate-12'
"
@click="$emit('select', node.path)"
>
{{ node.name }}
</button>
</template>
</div>
</template>