Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
509b8a6e01 | ||
|
|
4ede0f58d2 |
@@ -28,6 +28,17 @@ module.exports = {
|
|||||||
'no-console': 'off',
|
'no-console': 'off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// The stories harness is dev-only tooling, not user-facing UI.
|
||||||
|
files: ['app/javascript/stories/**/*.{js,vue}'],
|
||||||
|
rules: {
|
||||||
|
'vue/no-bare-strings-in-template': 'off',
|
||||||
|
'@intlify/vue-i18n/no-raw-text': 'off',
|
||||||
|
// `group` is declared on <Story> only to accept Histoire's API.
|
||||||
|
'vue/no-unused-properties': 'off',
|
||||||
|
'no-console': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
plugins: ['html', 'prettier'],
|
plugins: ['html', 'prettier'],
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
|
|||||||
+1
-1
@@ -100,7 +100,7 @@ CLAUDE.local.md
|
|||||||
|
|
||||||
# Histoire deployment
|
# Histoire deployment
|
||||||
.netlify
|
.netlify
|
||||||
.histoire
|
.stories
|
||||||
.pnpm-store/*
|
.pnpm-store/*
|
||||||
local/
|
local/
|
||||||
Procfile.worktree
|
Procfile.worktree
|
||||||
|
|||||||
+26
-1
@@ -59,13 +59,38 @@ const articles = [
|
|||||||
views: 400,
|
views: 400,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const categories = [
|
||||||
|
{ id: 1, name: 'Getting started', icon: '🚀' },
|
||||||
|
{ id: 2, name: 'Marketing', icon: '⚡️' },
|
||||||
|
{ id: 3, name: 'Development', icon: '🛠️' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const allowedLocales = [{ code: 'en', name: 'English' }];
|
||||||
|
|
||||||
|
const portalName = 'Chatwoot Help Center';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
currentPage: 1,
|
||||||
|
allArticlesCount: articles.length,
|
||||||
|
articlesCount: articles.length,
|
||||||
|
mineArticlesCount: 2,
|
||||||
|
draftArticlesCount: 1,
|
||||||
|
archivedArticlesCount: 2,
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Story title="Pages/HelpCenter/ArticlesPage" :layout="{ type: 'single' }">
|
<Story title="Pages/HelpCenter/ArticlesPage" :layout="{ type: 'single' }">
|
||||||
<Variant title="All Articles">
|
<Variant title="All Articles">
|
||||||
<div class="w-full min-h-screen bg-n-background">
|
<div class="w-full min-h-screen bg-n-background">
|
||||||
<ArticlesPage :articles="articles" />
|
<ArticlesPage
|
||||||
|
:articles="articles"
|
||||||
|
:categories="categories"
|
||||||
|
:allowed-locales="allowedLocales"
|
||||||
|
:portal-name="portalName"
|
||||||
|
:meta="meta"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Variant>
|
</Variant>
|
||||||
</Story>
|
</Story>
|
||||||
|
|||||||
+31
-175
@@ -3,206 +3,62 @@ import CategoriesPage from './CategoriesPage.vue';
|
|||||||
|
|
||||||
const categories = [
|
const categories = [
|
||||||
{
|
{
|
||||||
id: 'getting-started',
|
id: 1,
|
||||||
title: '🚀 Getting started',
|
name: 'Getting started',
|
||||||
|
icon: '🚀',
|
||||||
|
slug: 'getting-started',
|
||||||
description:
|
description:
|
||||||
'Learn how to use Chatwoot effectively and make the most of its features to enhance customer support and engagement.',
|
'Learn how to use Chatwoot effectively and make the most of its features to enhance customer support and engagement.',
|
||||||
articlesCount: '2',
|
meta: { articles_count: 12 },
|
||||||
articles: [
|
|
||||||
{
|
|
||||||
variant: 'Draft article',
|
|
||||||
title:
|
|
||||||
"How to get an SSL certificate for your Help Center's custom domain",
|
|
||||||
status: 'draft',
|
|
||||||
updatedAt: '2 days ago',
|
|
||||||
author: 'Michael',
|
|
||||||
category: '⚡️ Marketing',
|
|
||||||
views: 3400,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
variant: 'Published article',
|
|
||||||
title: 'Setting up your first Help Center portal',
|
|
||||||
status: '',
|
|
||||||
updatedAt: '1 week ago',
|
|
||||||
author: 'John',
|
|
||||||
category: '🛠️ Development',
|
|
||||||
views: 400,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'marketing',
|
id: 2,
|
||||||
title: 'Marketing',
|
name: 'Marketing',
|
||||||
|
icon: '⚡️',
|
||||||
|
slug: 'marketing',
|
||||||
description:
|
description:
|
||||||
'Learn how to use Chatwoot effectively and make the most of its features to enhance customer support.',
|
'Learn how to use Chatwoot effectively and make the most of its features to enhance customer support.',
|
||||||
articlesCount: '4',
|
meta: { articles_count: 4 },
|
||||||
articles: [
|
|
||||||
{
|
|
||||||
variant: 'Draft article',
|
|
||||||
title:
|
|
||||||
"How to get an SSL certificate for your Help Center's custom domain",
|
|
||||||
status: 'draft',
|
|
||||||
updatedAt: '2 days ago',
|
|
||||||
author: 'Michael',
|
|
||||||
category: '⚡️ Marketing',
|
|
||||||
views: 3400,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
variant: 'Published article',
|
|
||||||
title: 'Setting up your first Help Center portal',
|
|
||||||
status: '',
|
|
||||||
updatedAt: '1 week ago',
|
|
||||||
author: 'John',
|
|
||||||
category: '🛠️ Development',
|
|
||||||
views: 400,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
variant: 'Archived article',
|
|
||||||
title: 'Best practices for organizing your Help Center content',
|
|
||||||
status: 'archived',
|
|
||||||
updatedAt: '3 days ago',
|
|
||||||
author: 'Fernando',
|
|
||||||
category: '💰 Finance',
|
|
||||||
views: 400,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
variant: 'Published article',
|
|
||||||
title: 'Customizing the appearance of your Help Center',
|
|
||||||
status: '',
|
|
||||||
updatedAt: '5 days ago',
|
|
||||||
author: 'Jane',
|
|
||||||
category: '💰 Finance',
|
|
||||||
views: 400,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'development',
|
id: 3,
|
||||||
title: 'Development',
|
name: 'Development',
|
||||||
|
icon: '🛠️',
|
||||||
|
slug: 'development',
|
||||||
description: '',
|
description: '',
|
||||||
articlesCount: '5',
|
meta: { articles_count: 5 },
|
||||||
articles: [
|
|
||||||
{
|
|
||||||
variant: 'Draft article',
|
|
||||||
title:
|
|
||||||
"How to get an SSL certificate for your Help Center's custom domain",
|
|
||||||
status: 'draft',
|
|
||||||
updatedAt: '2 days ago',
|
|
||||||
author: 'Michael',
|
|
||||||
category: '⚡️ Marketing',
|
|
||||||
views: 3400,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
variant: 'Published article',
|
|
||||||
title: 'Setting up your first Help Center portal',
|
|
||||||
status: '',
|
|
||||||
updatedAt: '1 week ago',
|
|
||||||
author: 'John',
|
|
||||||
category: '🛠️ Development',
|
|
||||||
views: 400,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
variant: 'Archived article',
|
|
||||||
title: 'Best practices for organizing your Help Center content',
|
|
||||||
status: 'archived',
|
|
||||||
updatedAt: '3 days ago',
|
|
||||||
author: 'Fernando',
|
|
||||||
category: '💰 Finance',
|
|
||||||
views: 400,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
variant: 'Archived article',
|
|
||||||
title: 'Best practices for organizing your Help Center content',
|
|
||||||
status: 'archived',
|
|
||||||
updatedAt: '3 days ago',
|
|
||||||
author: 'Fernando',
|
|
||||||
category: '💰 Finance',
|
|
||||||
views: 400,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
variant: 'Published article',
|
|
||||||
title: 'Customizing the appearance of your Help Center',
|
|
||||||
status: '',
|
|
||||||
updatedAt: '5 days ago',
|
|
||||||
author: 'Jane',
|
|
||||||
category: '💰 Finance',
|
|
||||||
views: 400,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'roadmap',
|
id: 4,
|
||||||
title: '🛣️ Roadmap',
|
name: 'Roadmap',
|
||||||
|
icon: '🛣️',
|
||||||
|
slug: 'roadmap',
|
||||||
description:
|
description:
|
||||||
'Learn how to use Chatwoot effectively and make the most of its features to enhance customer support and engagement.',
|
'Learn how to use Chatwoot effectively and make the most of its features to enhance customer support and engagement.',
|
||||||
articlesCount: '3',
|
meta: { articles_count: 3 },
|
||||||
articles: [
|
|
||||||
{
|
|
||||||
variant: 'Draft article',
|
|
||||||
title:
|
|
||||||
"How to get an SSL certificate for your Help Center's custom domain",
|
|
||||||
status: 'draft',
|
|
||||||
updatedAt: '2 days ago',
|
|
||||||
author: 'Michael',
|
|
||||||
category: '⚡️ Marketing',
|
|
||||||
views: 3400,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
variant: 'Published article',
|
|
||||||
title: 'Setting up your first Help Center portal',
|
|
||||||
status: '',
|
|
||||||
updatedAt: '1 week ago',
|
|
||||||
author: 'John',
|
|
||||||
category: '🛠️ Development',
|
|
||||||
views: 400,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
variant: 'Published article',
|
|
||||||
title: 'Setting up your first Help Center portal',
|
|
||||||
status: '',
|
|
||||||
updatedAt: '1 week ago',
|
|
||||||
author: 'John',
|
|
||||||
category: '🛠️ Development',
|
|
||||||
views: 400,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'finance',
|
id: 5,
|
||||||
title: '💰 Finance',
|
name: 'Finance',
|
||||||
|
icon: '💰',
|
||||||
|
slug: 'finance',
|
||||||
description:
|
description:
|
||||||
'Learn how to use Chatwoot effectively and make the most of its features to enhance customer support and engagement.',
|
'Learn how to use Chatwoot effectively and make the most of its features to enhance customer support and engagement.',
|
||||||
articlesCount: '2',
|
meta: { articles_count: 2 },
|
||||||
articles: [
|
|
||||||
{
|
|
||||||
variant: 'Draft article',
|
|
||||||
title:
|
|
||||||
"How to get an SSL certificate for your Help Center's custom domain",
|
|
||||||
status: 'draft',
|
|
||||||
updatedAt: '2 days ago',
|
|
||||||
author: 'Michael',
|
|
||||||
category: '⚡️ Marketing',
|
|
||||||
views: 3400,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
variant: 'Published article',
|
|
||||||
title: 'Setting up your first Help Center portal',
|
|
||||||
status: '',
|
|
||||||
updatedAt: '1 week ago',
|
|
||||||
author: 'John',
|
|
||||||
category: '🛠️ Development',
|
|
||||||
views: 400,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const allowedLocales = [{ code: 'en', name: 'English' }];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Story title="Pages/HelpCenter/CategoryPage" :layout="{ type: 'single' }">
|
<Story title="Pages/HelpCenter/CategoryPage" :layout="{ type: 'single' }">
|
||||||
<Variant title="All Categories">
|
<Variant title="All Categories">
|
||||||
<div class="w-full min-h-screen bg-n-background">
|
<div class="w-full min-h-screen bg-n-background">
|
||||||
<CategoriesPage :categories="categories" />
|
<CategoriesPage
|
||||||
|
:categories="categories"
|
||||||
|
:allowed-locales="allowedLocales"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Variant>
|
</Variant>
|
||||||
</Story>
|
</Story>
|
||||||
|
|||||||
+17
-1
@@ -1,12 +1,28 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import PortalSettings from './PortalSettings.vue';
|
import PortalSettings from './PortalSettings.vue';
|
||||||
|
|
||||||
|
// `slug` matches the route the stories harness seeds (portals/chatwoot), so
|
||||||
|
// PortalSettings resolves an active portal and renders the settings forms.
|
||||||
|
const portals = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Chatwoot Help Center',
|
||||||
|
slug: 'chatwoot',
|
||||||
|
header_text: 'How can we help you today?',
|
||||||
|
page_title: 'Chatwoot Help Center',
|
||||||
|
homepage_link: 'https://www.chatwoot.com',
|
||||||
|
custom_domain: '',
|
||||||
|
config: {},
|
||||||
|
ssl_settings: {},
|
||||||
|
},
|
||||||
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Story title="Pages/HelpCenter/PortalSettings" :layout="{ type: 'single' }">
|
<Story title="Pages/HelpCenter/PortalSettings" :layout="{ type: 'single' }">
|
||||||
<Variant title="Default">
|
<Variant title="Default">
|
||||||
<div class="w-[1000px] min-h-screen bg-n-background">
|
<div class="w-[1000px] min-h-screen bg-n-background">
|
||||||
<PortalSettings />
|
<PortalSettings :portals="portals" :is-fetching="false" />
|
||||||
</div>
|
</div>
|
||||||
</Variant>
|
</Variant>
|
||||||
</Story>
|
</Story>
|
||||||
|
|||||||
@@ -2,44 +2,51 @@
|
|||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import Copilot from './Copilot.vue';
|
import Copilot from './Copilot.vue';
|
||||||
|
|
||||||
const supportAgent = {
|
|
||||||
available_name: 'Pranav Raj',
|
|
||||||
avatar_url:
|
|
||||||
'https://app.chatwoot.com/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBd3FodGc9PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--d218a325af0ef45061eefd352f8efb9ac84275e8/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lKYW5CbFp3WTZCa1ZVT2hOeVpYTnBlbVZmZEc5ZlptbHNiRnNIYVFINk1BPT0iLCJleHAiOm51bGwsInB1ciI6InZhcmlhdGlvbiJ9fQ==--533c3ad7218e24c4b0e8f8959dc1953ce1d279b9/1707423736896.jpeg',
|
|
||||||
};
|
|
||||||
|
|
||||||
const messages = ref([
|
const messages = ref([
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
role: 'user',
|
message_type: 'user',
|
||||||
content: 'Hi there! How can I help you today?',
|
message: { content: 'Hi there! How can I help you today?' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
role: 'assistant',
|
message_type: 'assistant_thinking',
|
||||||
content:
|
message: {
|
||||||
"Hello! I'm the AI assistant. I'll be helping the support team today.",
|
content: 'Analyzing the conversation',
|
||||||
|
reasoning: 'Breaking down the request into actionable steps',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
message_type: 'assistant_thinking',
|
||||||
|
message: {
|
||||||
|
content: 'Searching the knowledge base',
|
||||||
|
reasoning: 'Looking for relevant articles and past replies',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
message_type: 'assistant',
|
||||||
|
message: {
|
||||||
|
content:
|
||||||
|
"Hello! I'm the AI assistant. I'll be helping the support team today.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isCaptainTyping = ref(false);
|
|
||||||
|
|
||||||
const sendMessage = message => {
|
const sendMessage = message => {
|
||||||
// Add user message
|
|
||||||
messages.value.push({
|
messages.value.push({
|
||||||
id: messages.value.length + 1,
|
id: messages.value.length + 1,
|
||||||
role: 'user',
|
message_type: 'user',
|
||||||
content: message,
|
message: { content: message },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Simulate AI response
|
// Simulate AI response
|
||||||
isCaptainTyping.value = true;
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
isCaptainTyping.value = false;
|
|
||||||
messages.value.push({
|
messages.value.push({
|
||||||
id: messages.value.length + 1,
|
id: messages.value.length + 1,
|
||||||
role: 'assistant',
|
message_type: 'assistant',
|
||||||
content: 'This is a simulated AI response.',
|
message: { content: 'This is a simulated AI response.' },
|
||||||
});
|
});
|
||||||
}, 2000);
|
}, 2000);
|
||||||
};
|
};
|
||||||
@@ -51,9 +58,8 @@ const sendMessage = message => {
|
|||||||
:layout="{ type: 'grid', width: '400px', height: '800px' }"
|
:layout="{ type: 'grid', width: '400px', height: '800px' }"
|
||||||
>
|
>
|
||||||
<Copilot
|
<Copilot
|
||||||
:support-agent="supportAgent"
|
|
||||||
:messages="messages"
|
:messages="messages"
|
||||||
:is-captain-typing="isCaptainTyping"
|
conversation-inbox-type="Channel::WebWidget"
|
||||||
@send-message="sendMessage"
|
@send-message="sendMessage"
|
||||||
/>
|
/>
|
||||||
</Story>
|
</Story>
|
||||||
|
|||||||
@@ -4,18 +4,24 @@ import CopilotThinkingGroup from './CopilotThinkingGroup.vue';
|
|||||||
const messages = [
|
const messages = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
content: 'Analyzing the user query',
|
message: {
|
||||||
reasoning: 'Breaking down the request into actionable steps',
|
content: 'Analyzing the user query',
|
||||||
|
reasoning: 'Breaking down the request into actionable steps',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
content: 'Searching codebase',
|
message: {
|
||||||
reasoning: 'Looking for relevant files and functions',
|
content: 'Searching codebase',
|
||||||
|
reasoning: 'Looking for relevant files and functions',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
content: 'Generating response',
|
message: {
|
||||||
reasoning: 'Composing a helpful and accurate answer',
|
content: 'Generating response',
|
||||||
|
reasoning: 'Composing a helpful and accurate answer',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
@import 'dashboard/assets/scss/app';
|
|
||||||
|
|
||||||
*,
|
|
||||||
::before,
|
|
||||||
::after {
|
|
||||||
--_histoire-color-primary-50: 235 245 255;
|
|
||||||
--_histoire-color-primary-100: 194 225 255;
|
|
||||||
--_histoire-color-primary-200: 153 206 255;
|
|
||||||
--_histoire-color-primary-300: 112 186 255;
|
|
||||||
--_histoire-color-primary-400: 71 166 255;
|
|
||||||
--_histoire-color-primary-500: 31 147 255;
|
|
||||||
--_histoire-color-primary-600: 25 118 204;
|
|
||||||
--_histoire-color-primary-700: 19 88 153;
|
|
||||||
--_histoire-color-primary-800: 12 59 102;
|
|
||||||
--_histoire-color-primary-900: 6 29 51;
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
font-family: 'Inter', sans-serif;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
@import 'dashboard/assets/scss/app';
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed, defineAsyncComponent } from 'vue';
|
||||||
|
import { stories, buildTree } from './registry';
|
||||||
|
import TreeNode from './TreeNode.vue';
|
||||||
|
import StoryFrame from './StoryFrame.vue';
|
||||||
|
|
||||||
|
const query = ref('');
|
||||||
|
const filteredStories = computed(() => {
|
||||||
|
const term = query.value.trim().toLowerCase();
|
||||||
|
if (!term) return stories;
|
||||||
|
return stories.filter(story => story.title.toLowerCase().includes(term));
|
||||||
|
});
|
||||||
|
const tree = computed(() => buildTree(filteredStories.value));
|
||||||
|
const isSearching = computed(() => query.value.trim().length > 0);
|
||||||
|
|
||||||
|
function pathFromHash() {
|
||||||
|
const hash = decodeURIComponent(window.location.hash.replace(/^#/, ''));
|
||||||
|
return stories.find(story => story.path === hash)?.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedPath = ref(pathFromHash() || stories[0]?.path || '');
|
||||||
|
const current = computed(() =>
|
||||||
|
stories.find(story => story.path === selectedPath.value)
|
||||||
|
);
|
||||||
|
const StoryComponent = computed(() =>
|
||||||
|
current.value ? defineAsyncComponent(current.value.loader) : null
|
||||||
|
);
|
||||||
|
|
||||||
|
function select(path) {
|
||||||
|
selectedPath.value = path;
|
||||||
|
window.location.hash = encodeURIComponent(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDark = ref(false);
|
||||||
|
function toggleDark() {
|
||||||
|
isDark.value = !isDark.value;
|
||||||
|
document.documentElement.classList.toggle('dark', isDark.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scoped to the preview canvas only (see the <main :dir> below), so the
|
||||||
|
// sidebar stays LTR while stories can be inspected in RTL.
|
||||||
|
const isRtl = ref(false);
|
||||||
|
function toggleDir() {
|
||||||
|
isRtl.value = !isRtl.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remount the active story on every hot update so a fixed file clears the
|
||||||
|
// error boundary (otherwise a caught error stays latched until a full reload).
|
||||||
|
const hmrTick = ref(0);
|
||||||
|
if (import.meta.hot) {
|
||||||
|
import.meta.hot.on('vite:afterUpdate', () => {
|
||||||
|
hmrTick.value += 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex w-screen h-screen overflow-hidden bg-n-background text-n-slate-12"
|
||||||
|
>
|
||||||
|
<aside
|
||||||
|
dir="ltr"
|
||||||
|
class="flex flex-col border-r w-72 shrink-0 border-n-weak bg-n-solid-1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between h-12 gap-2 px-4 border-b shrink-0 border-n-weak"
|
||||||
|
>
|
||||||
|
<span class="text-sm font-semibold text-n-slate-12">
|
||||||
|
@chatwoot/design
|
||||||
|
</span>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="px-2 py-1 text-xs font-medium uppercase rounded-md text-n-slate-11 hover:bg-n-alpha-1"
|
||||||
|
@click="toggleDir"
|
||||||
|
>
|
||||||
|
{{ isRtl ? 'RTL' : 'LTR' }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="px-2 py-1 text-sm rounded-md text-n-slate-11 hover:bg-n-alpha-1"
|
||||||
|
@click="toggleDark"
|
||||||
|
>
|
||||||
|
{{ isDark ? 'Light' : 'Dark' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-3 border-b shrink-0 border-n-weak">
|
||||||
|
<input
|
||||||
|
v-model="query"
|
||||||
|
type="search"
|
||||||
|
placeholder="Search stories"
|
||||||
|
class="w-full px-3 py-1.5 text-sm rounded-md border outline-none border-n-weak bg-n-background text-n-slate-12 placeholder:text-n-slate-10 focus:border-n-brand"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<nav class="flex-1 p-2 overflow-y-auto">
|
||||||
|
<TreeNode
|
||||||
|
:nodes="tree"
|
||||||
|
:selected-path="selectedPath"
|
||||||
|
:force-expand="isSearching"
|
||||||
|
@select="select"
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
v-if="!filteredStories.length"
|
||||||
|
class="px-2 py-4 text-sm text-n-slate-10"
|
||||||
|
>
|
||||||
|
No stories match "{{ query }}".
|
||||||
|
</p>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
<main :dir="isRtl ? 'rtl' : 'ltr'" class="flex-1 min-w-0 overflow-hidden">
|
||||||
|
<StoryFrame
|
||||||
|
v-if="StoryComponent"
|
||||||
|
:key="`${selectedPath}:${hmrTick}`"
|
||||||
|
:component="StoryComponent"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="flex items-center justify-center h-full text-n-slate-10"
|
||||||
|
>
|
||||||
|
Select a story to preview.
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, provide } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
title: { type: String, required: true },
|
||||||
|
layout: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({ type: 'grid', width: '80%' }),
|
||||||
|
},
|
||||||
|
// Accepted for API compatibility with Histoire; not used by the harness.
|
||||||
|
group: { type: String, default: '' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const layout = computed(() => ({ type: 'grid', ...props.layout }));
|
||||||
|
provide('storyLayout', layout);
|
||||||
|
|
||||||
|
const name = computed(() => props.title.split('/').pop());
|
||||||
|
const containerClass = computed(() =>
|
||||||
|
layout.value.type === 'single'
|
||||||
|
? 'flex flex-col gap-6'
|
||||||
|
: 'flex flex-wrap items-start gap-6'
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-full min-h-0 bg-n-background">
|
||||||
|
<header
|
||||||
|
class="flex flex-col gap-0.5 px-6 py-4 border-b border-n-weak shrink-0"
|
||||||
|
>
|
||||||
|
<h1 class="text-lg font-semibold text-n-slate-12">{{ name }}</h1>
|
||||||
|
<p class="text-xs text-n-slate-10">{{ title }}</p>
|
||||||
|
</header>
|
||||||
|
<div class="flex-1 min-h-0 overflow-auto p-6">
|
||||||
|
<div :class="containerClass">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, onErrorCaptured } from 'vue';
|
||||||
|
|
||||||
|
// Per-story error boundary. Keyed by the story path in App.vue, so it remounts
|
||||||
|
// (and resets) on navigation. Containing the error here keeps one broken story
|
||||||
|
// from tearing down the whole harness, mirroring Histoire's per-story isolation.
|
||||||
|
defineProps({
|
||||||
|
component: { type: [Object, Function], default: null },
|
||||||
|
});
|
||||||
|
|
||||||
|
const error = ref(null);
|
||||||
|
onErrorCaptured(err => {
|
||||||
|
error.value = err;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="error" class="flex flex-col h-full gap-3 p-6 overflow-auto">
|
||||||
|
<p class="text-sm font-semibold text-n-ruby-11">
|
||||||
|
This story failed to render.
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
class="p-3 font-mono text-xs whitespace-pre-wrap rounded-md text-n-slate-11 bg-n-alpha-1"
|
||||||
|
>
|
||||||
|
{{ error.stack || error.message }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<component :is="component" v-else />
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, inject } from 'vue';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
title: { type: String, default: '' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const layout = inject('storyLayout', null);
|
||||||
|
|
||||||
|
// Normalizes layout sizes: numbers and bare numeric strings -> px,
|
||||||
|
// everything else (e.g. '100%', '800px') passes through unchanged.
|
||||||
|
function normalizeSize(value) {
|
||||||
|
if (value === undefined || value === null || value === '') return undefined;
|
||||||
|
if (typeof value === 'number') return `${value}px`;
|
||||||
|
return /^\d+$/.test(value) ? `${value}px` : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cellStyle = computed(() => {
|
||||||
|
const current = layout?.value ?? {};
|
||||||
|
return {
|
||||||
|
width: normalizeSize(current.width),
|
||||||
|
height: normalizeSize(current.height),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section
|
||||||
|
class="flex flex-col overflow-hidden rounded-lg border border-n-weak bg-n-solid-1"
|
||||||
|
:style="cellStyle"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="px-3 py-2 text-sm font-medium border-b text-n-slate-12 border-n-weak bg-n-alpha-1"
|
||||||
|
>
|
||||||
|
{{ title }}
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 p-4 bg-n-background">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>@chatwoot/design</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { createApp } from 'vue';
|
||||||
|
import App from './App.vue';
|
||||||
|
import Story from './Story.vue';
|
||||||
|
import Variant from './Variant.vue';
|
||||||
|
import { setupApp } from './setup';
|
||||||
|
|
||||||
|
const app = createApp(App);
|
||||||
|
|
||||||
|
// Story files reference <Story> and <Variant> as globals (same as Histoire).
|
||||||
|
app.component('Story', Story);
|
||||||
|
app.component('Variant', Variant);
|
||||||
|
|
||||||
|
setupApp(app);
|
||||||
|
|
||||||
|
app.mount('#app');
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
// Lazy component loaders for every *.story.vue file under app/javascript.
|
||||||
|
const storyModules = import.meta.glob('../**/*.story.vue');
|
||||||
|
|
||||||
|
// Raw sources, eagerly loaded, so we can read each story's title without
|
||||||
|
// instantiating the (potentially heavy) component just to build the sidebar.
|
||||||
|
const storySources = import.meta.glob('../**/*.story.vue', {
|
||||||
|
query: '?raw',
|
||||||
|
import: 'default',
|
||||||
|
eager: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
function parseTitle(source) {
|
||||||
|
const match = source.match(/<Story\b[^>]*?\btitle\s*=\s*"([^"]+)"/);
|
||||||
|
return match ? match[1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const stories = Object.keys(storyModules)
|
||||||
|
.map(filePath => ({
|
||||||
|
path: filePath,
|
||||||
|
title: parseTitle(storySources[filePath] || ''),
|
||||||
|
loader: storyModules[filePath],
|
||||||
|
}))
|
||||||
|
.filter(story => story.title)
|
||||||
|
.sort((a, b) => a.title.localeCompare(b.title));
|
||||||
|
|
||||||
|
function insert(nodes, parts, story) {
|
||||||
|
const [head, ...rest] = parts;
|
||||||
|
if (rest.length === 0) {
|
||||||
|
nodes.push({ type: 'story', name: head, path: story.path });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let group = nodes.find(node => node.type === 'group' && node.name === head);
|
||||||
|
if (!group) {
|
||||||
|
group = { type: 'group', name: head, children: [] };
|
||||||
|
nodes.push(group);
|
||||||
|
}
|
||||||
|
insert(group.children, rest, story);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sorts each level so groups come before individual stories, both alphabetically.
|
||||||
|
function sortNodes(nodes) {
|
||||||
|
nodes.sort((a, b) => {
|
||||||
|
if (a.type !== b.type) return a.type === 'group' ? -1 : 1;
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
nodes.forEach(node => {
|
||||||
|
if (node.type === 'group') sortNodes(node.children);
|
||||||
|
});
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builds a nested tree from the slash-delimited story titles
|
||||||
|
// (e.g. "Components/Button" -> group "Components" > story "Button").
|
||||||
|
export function buildTree(items) {
|
||||||
|
const roots = [];
|
||||||
|
items.forEach(story => {
|
||||||
|
const parts = story.title.split('/').map(part => part.trim());
|
||||||
|
insert(roots, parts, story);
|
||||||
|
});
|
||||||
|
return sortNodes(roots);
|
||||||
|
}
|
||||||
@@ -1,14 +1,33 @@
|
|||||||
import './design-system/histoire.scss';
|
import '../design-system/stories.scss';
|
||||||
import { defineSetupVue3 } from '@histoire/plugin-vue';
|
|
||||||
import dashboardI18n from 'dashboard/i18n';
|
|
||||||
import widgetI18n from 'widget/i18n';
|
|
||||||
import { createI18n } from 'vue-i18n';
|
import { createI18n } from 'vue-i18n';
|
||||||
|
import { createRouter, createMemoryHistory } from 'vue-router';
|
||||||
import { vResizeObserver } from '@vueuse/components';
|
import { vResizeObserver } from '@vueuse/components';
|
||||||
import store from 'dashboard/store';
|
|
||||||
import FloatingVue from 'floating-vue';
|
import FloatingVue from 'floating-vue';
|
||||||
import VueDOMPurifyHTML from 'vue-dompurify-html';
|
import VueDOMPurifyHTML from 'vue-dompurify-html';
|
||||||
import { domPurifyConfig } from 'shared/helpers/HTMLSanitizer.js';
|
|
||||||
import { directive as onClickaway } from 'vue3-click-away';
|
import { directive as onClickaway } from 'vue3-click-away';
|
||||||
|
import dashboardI18n from 'dashboard/i18n';
|
||||||
|
import widgetI18n from 'widget/i18n';
|
||||||
|
import store from 'dashboard/store';
|
||||||
|
import { domPurifyConfig } from 'shared/helpers/HTMLSanitizer.js';
|
||||||
|
|
||||||
|
// A throwaway router so composables like useRoute()/useAccount() resolve a real
|
||||||
|
// route object instead of throwing. We seed realistic Help Center params
|
||||||
|
// (accountId/portalSlug/locale) so page-level stories that key off the route
|
||||||
|
// render instead of bailing out. Story selection itself is hash-based and
|
||||||
|
// independent of this router.
|
||||||
|
const noop = { render: () => null };
|
||||||
|
const router = createRouter({
|
||||||
|
history: createMemoryHistory(),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/app/accounts/:accountId/portals/:portalSlug/:locale?',
|
||||||
|
name: 'stories',
|
||||||
|
component: noop,
|
||||||
|
},
|
||||||
|
{ path: '/:pathMatch(.*)*', name: 'fallback', component: noop },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
router.replace('/app/accounts/1/portals/chatwoot/en');
|
||||||
|
|
||||||
function mergeMessages(...sources) {
|
function mergeMessages(...sources) {
|
||||||
return sources.reduce((acc, src) => {
|
return sources.reduce((acc, src) => {
|
||||||
@@ -36,9 +55,12 @@ const i18n = createI18n({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setupVue3 = defineSetupVue3(({ app }) => {
|
// Registers the same global plugins/directives the dashboard components expect
|
||||||
|
// at runtime, so stories render identically to the real app.
|
||||||
|
export function setupApp(app) {
|
||||||
app.use(store);
|
app.use(store);
|
||||||
app.use(i18n);
|
app.use(i18n);
|
||||||
|
app.use(router);
|
||||||
app.use(FloatingVue, {
|
app.use(FloatingVue, {
|
||||||
instantMove: true,
|
instantMove: true,
|
||||||
arrowOverflow: false,
|
arrowOverflow: false,
|
||||||
@@ -48,4 +70,4 @@ export const setupVue3 = defineSetupVue3(({ app }) => {
|
|||||||
app.directive('resize', vResizeObserver);
|
app.directive('resize', vResizeObserver);
|
||||||
app.use(VueDOMPurifyHTML, domPurifyConfig);
|
app.use(VueDOMPurifyHTML, domPurifyConfig);
|
||||||
app.directive('on-clickaway', onClickaway);
|
app.directive('on-clickaway', onClickaway);
|
||||||
});
|
}
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import { defineConfig } from 'histoire';
|
|
||||||
import { HstVue } from '@histoire/plugin-vue';
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
setupFile: './histoire.setup.ts',
|
|
||||||
plugins: [HstVue()],
|
|
||||||
collectMaxThreads: 4,
|
|
||||||
vite: {
|
|
||||||
server: {
|
|
||||||
port: 6179,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
viteIgnorePlugins: ['vite-plugin-ruby'],
|
|
||||||
theme: {
|
|
||||||
darkClass: 'dark',
|
|
||||||
title: '@chatwoot/design',
|
|
||||||
logo: {
|
|
||||||
square: './design-system/images/logo-thumbnail.svg',
|
|
||||||
light: './design-system/images/logo.png',
|
|
||||||
dark: './design-system/images/logo-dark.png',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultStoryProps: {
|
|
||||||
icon: 'carbon:cube',
|
|
||||||
iconColor: '#1F93FF',
|
|
||||||
layout: {
|
|
||||||
type: 'grid',
|
|
||||||
width: '80%',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
tree: {
|
|
||||||
groups: [
|
|
||||||
{
|
|
||||||
id: 'top',
|
|
||||||
title: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'components',
|
|
||||||
title: 'Components',
|
|
||||||
include: () => true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
+3
-5
@@ -15,9 +15,9 @@
|
|||||||
"build:sdk": "vite build --config vite.lib.config.ts",
|
"build:sdk": "vite build --config vite.lib.config.ts",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"size": "size-limit",
|
"size": "size-limit",
|
||||||
"story:dev": "histoire dev",
|
"story:dev": "vite --config vite.stories.config.ts",
|
||||||
"story:build": "histoire build",
|
"story:build": "vite build --config vite.stories.config.ts",
|
||||||
"story:preview": "histoire preview",
|
"story:preview": "vite preview --config vite.stories.config.ts",
|
||||||
"sync:i18n": "bin/sync_i18n_file_change"
|
"sync:i18n": "bin/sync_i18n_file_change"
|
||||||
},
|
},
|
||||||
"size-limit": [
|
"size-limit": [
|
||||||
@@ -114,7 +114,6 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@egoist/tailwindcss-icons": "^1.9.2",
|
"@egoist/tailwindcss-icons": "^1.9.2",
|
||||||
"@histoire/plugin-vue": "0.17.15",
|
|
||||||
"@iconify-json/logos": "^1.2.10",
|
"@iconify-json/logos": "^1.2.10",
|
||||||
"@iconify-json/lucide": "^1.2.82",
|
"@iconify-json/lucide": "^1.2.82",
|
||||||
"@iconify-json/ph": "^1.2.2",
|
"@iconify-json/ph": "^1.2.2",
|
||||||
@@ -136,7 +135,6 @@
|
|||||||
"eslint-plugin-vitest-globals": "^1.5.0",
|
"eslint-plugin-vitest-globals": "^1.5.0",
|
||||||
"eslint-plugin-vue": "^9.28.0",
|
"eslint-plugin-vue": "^9.28.0",
|
||||||
"fake-indexeddb": "^6.0.0",
|
"fake-indexeddb": "^6.0.0",
|
||||||
"histoire": "0.17.15",
|
|
||||||
"husky": "^7.0.0",
|
"husky": "^7.0.0",
|
||||||
"jsdom": "^27.2.0",
|
"jsdom": "^27.2.0",
|
||||||
"lint-staged": "^16.2.7",
|
"lint-staged": "^16.2.7",
|
||||||
|
|||||||
Generated
+24
-1064
File diff suppressed because it is too large
Load Diff
@@ -34,6 +34,7 @@ const tailwindConfig = {
|
|||||||
'./app/javascript/dashboard/composables/**/*.js',
|
'./app/javascript/dashboard/composables/**/*.js',
|
||||||
'./app/javascript/dashboard/components-next/**/*.js',
|
'./app/javascript/dashboard/components-next/**/*.js',
|
||||||
'./app/javascript/dashboard/routes/dashboard/**/**/*.js',
|
'./app/javascript/dashboard/routes/dashboard/**/**/*.js',
|
||||||
|
'./app/javascript/stories/**/*.{js,vue}',
|
||||||
'./app/views/**/*.erb',
|
'./app/views/**/*.erb',
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import vue from '@vitejs/plugin-vue';
|
||||||
|
import yaml from '@rollup/plugin-yaml';
|
||||||
|
import { aliases, vueOptions } from './vite.shared';
|
||||||
|
|
||||||
|
// Standalone Vite app that renders the component stories (replaces Histoire).
|
||||||
|
// It reuses the dashboard aliases/plugins but drops vite-plugin-ruby, which is
|
||||||
|
// specific to the Rails asset pipeline and not needed here.
|
||||||
|
export default defineConfig({
|
||||||
|
root: path.resolve('app/javascript/stories'),
|
||||||
|
plugins: [vue(vueOptions), yaml()],
|
||||||
|
css: {
|
||||||
|
preprocessorOptions: {
|
||||||
|
scss: {
|
||||||
|
api: 'modern-compiler',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resolve: { alias: aliases },
|
||||||
|
server: {
|
||||||
|
port: 6179,
|
||||||
|
fs: { allow: [path.resolve('.')] },
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: path.resolve('.stories'),
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user