Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 87940481d6 | |||
| 66fbbf8abe | |||
| 8de950c402 | |||
| 1a14629404 |
67
.playground/pages/composant/site/siteSelector.vue
Normal file
67
.playground/pages/composant/site/siteSelector.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="grid grid-cols-1 items-start gap-6">
|
||||
<div class="rounded-lg border p-4">
|
||||
<h2 class="mb-4 text-xl font-bold">Simple (3 sites) + event change</h2>
|
||||
<MalioSiteSelector v-model="simpleValue" :sites="sites" @change="onSiteChange" />
|
||||
<p class="mt-3 text-sm text-gray-600">Site sélectionné : <code>{{ simpleValue }}</code></p>
|
||||
<p class="mt-1 text-sm text-gray-600">Dernier event <code>change</code> : <code>{{ lastChange }}</code></p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border p-4">
|
||||
<h2 class="mb-4 text-xl font-bold">Deux sites</h2>
|
||||
<MalioSiteSelector v-model="twoValue" :sites="sitesTwo" />
|
||||
<p class="mt-3 text-sm text-gray-600">Site sélectionné : <code>{{ twoValue }}</code></p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border p-4">
|
||||
<h2 class="mb-4 text-xl font-bold">Cinq sites (largeur proportionnelle)</h2>
|
||||
<MalioSiteSelector v-model="fiveValue" :sites="sitesFive" />
|
||||
<p class="mt-3 text-sm text-gray-600">Site sélectionné : <code>{{ fiveValue }}</code></p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border p-4">
|
||||
<h2 class="mb-4 text-xl font-bold">Non contrôlé (sans v-model)</h2>
|
||||
<MalioSiteSelector :sites="sites" />
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border p-4">
|
||||
<h2 class="mb-4 text-xl font-bold">Largeur contrainte</h2>
|
||||
<div class="w-[480px]">
|
||||
<MalioSiteSelector v-model="constrainedValue" :sites="sites" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const sites = [
|
||||
{ id: 'chatellerault', name: 'Châtellerault', color: '#0055ff' },
|
||||
{ id: 'saint-jean', name: 'Saint-Jean', color: '#16a34a' },
|
||||
{ id: 'pommevic', name: 'Pommevic', color: '#dc2626' },
|
||||
]
|
||||
|
||||
const sitesTwo = [
|
||||
{ id: 'nord', name: 'Usine Nord', color: '#7c3aed' },
|
||||
{ id: 'sud', name: 'Usine Sud', color: '#ea580c' },
|
||||
]
|
||||
|
||||
const sitesFive = [
|
||||
{ id: 's1', name: 'Site 1', color: '#0ea5e9' },
|
||||
{ id: 's2', name: 'Site 2', color: '#14b8a6' },
|
||||
{ id: 's3', name: 'Site 3', color: '#f59e0b' },
|
||||
{ id: 's4', name: 'Site 4', color: '#ec4899' },
|
||||
{ id: 's5', name: 'Site 5', color: '#6366f1' },
|
||||
]
|
||||
|
||||
const simpleValue = ref('chatellerault')
|
||||
const twoValue = ref('nord')
|
||||
const fiveValue = ref('s3')
|
||||
const constrainedValue = ref('saint-jean')
|
||||
const lastChange = ref<string>('—')
|
||||
|
||||
function onSiteChange(site: { id: string; name: string; color: string }) {
|
||||
lastChange.value = JSON.stringify(site)
|
||||
}
|
||||
</script>
|
||||
@@ -25,7 +25,10 @@ Liste des évolutions de la librairie Malio layer UI
|
||||
* [#MUI-2] Faire un MCP pour la librairie de composant
|
||||
* [#MUI-15] Création d'un composant drawer
|
||||
* [#MUI-22] Création d'un composant datatable
|
||||
* [#MUI-27] Création d'un composant sélection de site
|
||||
|
||||
### Changed
|
||||
|
||||
### Fixed
|
||||
* Hauteur des boutons de pagination du datatable alignée sur le select (40px)
|
||||
* Distribution de `tailwind.config.ts` aux projets consommateurs avec paths `content` absolus
|
||||
|
||||
@@ -35,6 +35,6 @@
|
||||
--m-site-yellow: 243 203 0; /* #F3CB00 - Jaune Saint-Jean */
|
||||
--m-site-green: 116 191 4; /* #74BF04 - Vert Pommevic */
|
||||
|
||||
--m-radius: 8px;
|
||||
--m-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
variant="tertiary"
|
||||
label="Prev"
|
||||
:disabled="page <= 1"
|
||||
button-class="h-8 w-auto min-w-0 px-3 text-sm"
|
||||
button-class="h-10 w-auto min-w-0 px-3 text-sm"
|
||||
aria-label="Page précédente"
|
||||
data-test="prev-button"
|
||||
@click="goToPage(page - 1)"
|
||||
@@ -95,7 +95,7 @@
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="h-8 min-w-[2rem] rounded px-2 text-sm transition-colors"
|
||||
class="h-10 min-w-[2.5rem] rounded px-2 text-sm transition-colors"
|
||||
:class="p === page
|
||||
? 'bg-m-btn-primary text-white font-semibold'
|
||||
: 'text-m-text hover:bg-m-bg'"
|
||||
@@ -111,7 +111,7 @@
|
||||
variant="tertiary"
|
||||
label="Next"
|
||||
:disabled="page >= totalPages"
|
||||
button-class="h-8 w-auto min-w-0 px-3 text-sm"
|
||||
button-class="h-10 w-auto min-w-0 px-3 text-sm"
|
||||
aria-label="Page suivante"
|
||||
data-test="next-button"
|
||||
@click="goToPage(page + 1)"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="relative mt-4 w-full"
|
||||
class="relative w-full"
|
||||
>
|
||||
<textarea
|
||||
:id="inputId"
|
||||
|
||||
154
app/components/malio/site/SiteSelector.test.ts
Normal file
154
app/components/malio/site/SiteSelector.test.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import {describe, expect, it} from 'vitest'
|
||||
import {mount} from '@vue/test-utils'
|
||||
import type {DefineComponent} from 'vue'
|
||||
import SiteSelector from './SiteSelector.vue'
|
||||
|
||||
type Site = {
|
||||
id: string
|
||||
name: string
|
||||
color: string
|
||||
}
|
||||
|
||||
type SiteSelectorProps = {
|
||||
sites: Site[]
|
||||
modelValue?: string
|
||||
id?: string
|
||||
groupClass?: string
|
||||
tileClass?: string
|
||||
labelClass?: string
|
||||
}
|
||||
|
||||
const SiteSelectorForTest = SiteSelector as DefineComponent<SiteSelectorProps>
|
||||
|
||||
const sites: Site[] = [
|
||||
{id: 'chatellerault', name: 'Châtellerault', color: '#2563eb'},
|
||||
{id: 'saint-jean', name: 'Saint-Jean', color: '#16a34a'},
|
||||
{id: 'pommevic', name: 'Pommevic', color: '#dc2626'},
|
||||
]
|
||||
|
||||
function mountComponent(props: SiteSelectorProps) {
|
||||
return mount(SiteSelectorForTest, {props})
|
||||
}
|
||||
|
||||
describe('MalioSiteSelector', () => {
|
||||
it('renders one tile per site with the site name', () => {
|
||||
const wrapper = mountComponent({sites})
|
||||
const tiles = wrapper.findAll('[role="radio"]')
|
||||
expect(tiles).toHaveLength(3)
|
||||
expect(tiles[0]!.text()).toBe('Châtellerault')
|
||||
expect(tiles[1]!.text()).toBe('Saint-Jean')
|
||||
expect(tiles[2]!.text()).toBe('Pommevic')
|
||||
})
|
||||
|
||||
it('has role="radiogroup" on the wrapper', () => {
|
||||
const wrapper = mountComponent({sites})
|
||||
expect(wrapper.find('[role="radiogroup"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('selects the first site by default in uncontrolled mode', () => {
|
||||
const wrapper = mountComponent({sites})
|
||||
const tiles = wrapper.findAll('[role="radio"]')
|
||||
expect(tiles[0]!.attributes('aria-checked')).toBe('true')
|
||||
expect(tiles[1]!.attributes('aria-checked')).toBe('false')
|
||||
expect(tiles[2]!.attributes('aria-checked')).toBe('false')
|
||||
})
|
||||
|
||||
it('paints all tiles with the selected site color', () => {
|
||||
const wrapper = mountComponent({sites, modelValue: 'saint-jean'})
|
||||
const tiles = wrapper.findAll('[role="radio"]')
|
||||
for (const tile of tiles) {
|
||||
expect(tile.attributes('style')).toContain('background-color: rgb(22, 163, 74)')
|
||||
}
|
||||
})
|
||||
|
||||
it('applies opacity 1 on the selected tile and 0.4 on the others', () => {
|
||||
const wrapper = mountComponent({sites, modelValue: 'chatellerault'})
|
||||
const tiles = wrapper.findAll('[role="radio"]')
|
||||
expect(tiles[0]!.attributes('style')).toContain('opacity: 1')
|
||||
expect(tiles[1]!.attributes('style')).toContain('opacity: 0.4')
|
||||
expect(tiles[2]!.attributes('style')).toContain('opacity: 0.4')
|
||||
})
|
||||
|
||||
it('updates the shared color when the selection changes', async () => {
|
||||
const wrapper = mountComponent({sites})
|
||||
let tiles = wrapper.findAll('[role="radio"]')
|
||||
expect(tiles[0]!.attributes('style')).toContain('background-color: rgb(37, 99, 235)')
|
||||
|
||||
await tiles[2]!.trigger('click')
|
||||
tiles = wrapper.findAll('[role="radio"]')
|
||||
for (const tile of tiles) {
|
||||
expect(tile.attributes('style')).toContain('background-color: rgb(220, 38, 38)')
|
||||
}
|
||||
})
|
||||
|
||||
it('emits update:modelValue with the clicked site id', async () => {
|
||||
const wrapper = mountComponent({sites, modelValue: 'chatellerault'})
|
||||
const tiles = wrapper.findAll('[role="radio"]')
|
||||
|
||||
await tiles[1]!.trigger('click')
|
||||
|
||||
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['saint-jean'])
|
||||
})
|
||||
|
||||
it('emits change with the full selected site object', async () => {
|
||||
const wrapper = mountComponent({sites, modelValue: 'chatellerault'})
|
||||
const tiles = wrapper.findAll('[role="radio"]')
|
||||
|
||||
await tiles[2]!.trigger('click')
|
||||
|
||||
expect(wrapper.emitted('change')?.[0]).toEqual([
|
||||
{id: 'pommevic', name: 'Pommevic', color: '#dc2626'},
|
||||
])
|
||||
})
|
||||
|
||||
it('respects modelValue in controlled mode', () => {
|
||||
const wrapper = mountComponent({sites, modelValue: 'pommevic'})
|
||||
const tiles = wrapper.findAll('[role="radio"]')
|
||||
expect(tiles[0]!.attributes('aria-checked')).toBe('false')
|
||||
expect(tiles[1]!.attributes('aria-checked')).toBe('false')
|
||||
expect(tiles[2]!.attributes('aria-checked')).toBe('true')
|
||||
})
|
||||
|
||||
it('switches selection on click in uncontrolled mode', async () => {
|
||||
const wrapper = mountComponent({sites})
|
||||
const tiles = wrapper.findAll('[role="radio"]')
|
||||
|
||||
await tiles[1]!.trigger('click')
|
||||
|
||||
expect(tiles[0]!.attributes('aria-checked')).toBe('false')
|
||||
expect(tiles[1]!.attributes('aria-checked')).toBe('true')
|
||||
expect(tiles[2]!.attributes('aria-checked')).toBe('false')
|
||||
})
|
||||
|
||||
it('sets roving tabindex (active = 0, others = -1)', () => {
|
||||
const wrapper = mountComponent({sites, modelValue: 'saint-jean'})
|
||||
const tiles = wrapper.findAll('[role="radio"]')
|
||||
expect(tiles[0]!.attributes('tabindex')).toBe('-1')
|
||||
expect(tiles[1]!.attributes('tabindex')).toBe('0')
|
||||
expect(tiles[2]!.attributes('tabindex')).toBe('-1')
|
||||
})
|
||||
|
||||
it('merges groupClass, tileClass and labelClass via twMerge', () => {
|
||||
const wrapper = mountComponent({
|
||||
sites,
|
||||
groupClass: 'rounded-none bg-black',
|
||||
tileClass: 'py-10',
|
||||
labelClass: 'text-xs',
|
||||
})
|
||||
const group = wrapper.find('[role="radiogroup"]')
|
||||
expect(group.classes()).toContain('rounded-none')
|
||||
expect(group.classes()).toContain('bg-black')
|
||||
|
||||
const tile = wrapper.find('[role="radio"]')
|
||||
expect(tile.classes()).toContain('py-10')
|
||||
expect(tile.classes()).not.toContain('py-4')
|
||||
|
||||
const label = tile.find('span')
|
||||
expect(label.classes()).toContain('text-xs')
|
||||
})
|
||||
|
||||
it('uses a custom id when provided', () => {
|
||||
const wrapper = mountComponent({sites, id: 'my-selector'})
|
||||
expect(wrapper.find('[role="radiogroup"]').attributes('id')).toBe('my-selector')
|
||||
})
|
||||
})
|
||||
104
app/components/malio/site/SiteSelector.vue
Normal file
104
app/components/malio/site/SiteSelector.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<div
|
||||
v-bind="$attrs"
|
||||
:id="componentId"
|
||||
role="radiogroup"
|
||||
:class="mergedGroupClass"
|
||||
>
|
||||
<button
|
||||
v-for="site in sites"
|
||||
:key="site.id"
|
||||
type="button"
|
||||
role="radio"
|
||||
:aria-checked="activeId === site.id"
|
||||
:tabindex="activeId === site.id ? 0 : -1"
|
||||
:style="{
|
||||
backgroundColor: activeColor,
|
||||
opacity: activeId === site.id ? 1 : 0.4,
|
||||
}"
|
||||
:class="mergedTileClass"
|
||||
@click="select(site.id)"
|
||||
>
|
||||
<span :class="mergedLabelClass">{{ site.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, ref, useId} from 'vue'
|
||||
import {twMerge} from 'tailwind-merge'
|
||||
|
||||
defineOptions({name: 'MalioSiteSelector', inheritAttrs: false})
|
||||
|
||||
type Site = {
|
||||
id: string
|
||||
name: string
|
||||
color: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
sites: Site[]
|
||||
modelValue?: string
|
||||
id?: string
|
||||
groupClass?: string
|
||||
tileClass?: string
|
||||
labelClass?: string
|
||||
}>(), {
|
||||
modelValue: undefined,
|
||||
id: '',
|
||||
groupClass: '',
|
||||
tileClass: '',
|
||||
labelClass: '',
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string): void
|
||||
(e: 'change', site: Site): void
|
||||
}>()
|
||||
|
||||
const generatedId = useId()
|
||||
const componentId = computed(() => props.id || `malio-site-selector-${generatedId}`)
|
||||
|
||||
const isControlled = computed(() => props.modelValue !== undefined)
|
||||
const localValue = ref(props.sites.length > 0 ? props.sites[0]!.id : '')
|
||||
|
||||
const activeId = computed(() =>
|
||||
isControlled.value ? props.modelValue! : localValue.value,
|
||||
)
|
||||
|
||||
const activeColor = computed(() =>
|
||||
props.sites.find((s) => s.id === activeId.value)?.color ?? '',
|
||||
)
|
||||
|
||||
const mergedGroupClass = computed(() =>
|
||||
twMerge(
|
||||
'flex w-full',
|
||||
props.groupClass,
|
||||
),
|
||||
)
|
||||
|
||||
const mergedTileClass = computed(() =>
|
||||
twMerge(
|
||||
'flex-1 cursor-pointer px-6 py-4 text-center transition-opacity focus:outline-none',
|
||||
props.tileClass,
|
||||
),
|
||||
)
|
||||
|
||||
const mergedLabelClass = computed(() =>
|
||||
twMerge(
|
||||
'text-white font-bold uppercase tracking-wide',
|
||||
props.labelClass,
|
||||
),
|
||||
)
|
||||
|
||||
function select(id: string) {
|
||||
const site = props.sites.find((s) => s.id === id)
|
||||
if (!site) return
|
||||
|
||||
if (!isControlled.value) {
|
||||
localValue.value = id
|
||||
}
|
||||
emit('update:modelValue', id)
|
||||
emit('change', site)
|
||||
}
|
||||
</script>
|
||||
116
app/story/site/siteSelector.story.vue
Normal file
116
app/story/site/siteSelector.story.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<Story title="Site/Selector">
|
||||
<div class="grid grid-cols-1 gap-6">
|
||||
<div class="rounded-lg border p-4">
|
||||
<h2 class="mb-4 text-xl font-bold">Trois sites</h2>
|
||||
<MalioSiteSelector v-model="threeValue" :sites="sites" />
|
||||
<p class="mt-3 text-sm text-gray-600">Site sélectionné : <code>{{ threeValue }}</code></p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border p-4">
|
||||
<h2 class="mb-4 text-xl font-bold">Deux sites</h2>
|
||||
<MalioSiteSelector v-model="twoValue" :sites="sitesTwo" />
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border p-4">
|
||||
<h2 class="mb-4 text-xl font-bold">Cinq sites</h2>
|
||||
<MalioSiteSelector v-model="fiveValue" :sites="sitesFive" />
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border p-4">
|
||||
<h2 class="mb-4 text-xl font-bold">Non contrôlé</h2>
|
||||
<MalioSiteSelector :sites="sites" />
|
||||
</div>
|
||||
</div>
|
||||
</Story>
|
||||
</template>
|
||||
|
||||
<docs lang="md">
|
||||
# MalioSiteSelector
|
||||
|
||||
Sélecteur horizontal pour choisir **un site** (usine ou lieu) parmi une liste. Les tuiles occupent une largeur proportionnelle du conteneur. La couleur du site sélectionné est appliquée à toutes les tuiles ; la tuile active est opaque (opacité 1), les autres sont atténuées (opacité 0.4).
|
||||
|
||||
---
|
||||
|
||||
## Props détaillées
|
||||
|
||||
### sites
|
||||
|
||||
- Type : `Array<{ id: string; name: string; color: string }>`
|
||||
- Requis : oui
|
||||
- Description : Liste des sites à afficher. `color` est un hex (ex : `'#0055ff'`). La couleur du site actuellement sélectionné est appliquée à toutes les tuiles.
|
||||
|
||||
### modelValue
|
||||
|
||||
- Type : `string`
|
||||
- Description : `id` du site sélectionné (v-model). Sans `v-model`, le premier site est sélectionné par défaut (mode non contrôlé).
|
||||
|
||||
### id
|
||||
|
||||
- Type : `string`
|
||||
- Description : Identifiant HTML du conteneur. Auto-généré si absent.
|
||||
|
||||
### groupClass / tileClass / labelClass
|
||||
|
||||
- Type : `string`
|
||||
- Description : Classes Tailwind additionnelles fusionnées via `twMerge` sur, respectivement, le conteneur `<div role="radiogroup">`, chaque tuile et le libellé.
|
||||
|
||||
---
|
||||
|
||||
## Comportement
|
||||
|
||||
- **Toujours un site sélectionné.** Re-cliquer sur la tuile active ne la désélectionne pas.
|
||||
- **Couleur partagée.** Le `background-color` de toutes les tuiles suit la couleur du site sélectionné. Changer de site met à jour instantanément la couleur de la bande.
|
||||
- **Pas de gestion d'overflow** : les tuiles se répartissent proportionnellement sur toute la largeur disponible.
|
||||
|
||||
---
|
||||
|
||||
## Accessibilité
|
||||
|
||||
- `role="radiogroup"` sur le conteneur.
|
||||
- `role="radio"` avec `aria-checked` sur chaque tuile.
|
||||
- Roving `tabindex` : la tuile active est focusable (`tabindex="0"`), les autres sont exclues du tab order (`tabindex="-1"`).
|
||||
- Activation par Enter/Space via l'élément `<button>`.
|
||||
|
||||
---
|
||||
|
||||
## Events
|
||||
|
||||
### update:modelValue
|
||||
|
||||
- Émis au clic sur une tuile.
|
||||
- Retourne l'`id` (`string`) du site sélectionné.
|
||||
|
||||
### change
|
||||
|
||||
- Émis au clic sur une tuile, en complément de `update:modelValue`.
|
||||
- Retourne l'objet `Site` complet (`{ id, name, color }`) — utile pour déclencher des actions (appel API, filtrage…) sans avoir à relire le tableau `sites` côté consommateur.
|
||||
</docs>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import MalioSiteSelector from '../../components/malio/site/SiteSelector.vue'
|
||||
|
||||
const sites = [
|
||||
{ id: 'chatellerault', name: 'Châtellerault', color: '#0055ff' },
|
||||
{ id: 'saint-jean', name: 'Saint-Jean', color: '#16a34a' },
|
||||
{ id: 'pommevic', name: 'Pommevic', color: '#dc2626' },
|
||||
]
|
||||
|
||||
const sitesTwo = [
|
||||
{ id: 'nord', name: 'Usine Nord', color: '#7c3aed' },
|
||||
{ id: 'sud', name: 'Usine Sud', color: '#ea580c' },
|
||||
]
|
||||
|
||||
const sitesFive = [
|
||||
{ id: 's1', name: 'Site 1', color: '#0ea5e9' },
|
||||
{ id: 's2', name: 'Site 2', color: '#14b8a6' },
|
||||
{ id: 's3', name: 'Site 3', color: '#f59e0b' },
|
||||
{ id: 's4', name: 'Site 4', color: '#ec4899' },
|
||||
{ id: 's5', name: 'Site 5', color: '#6366f1' },
|
||||
]
|
||||
|
||||
const threeValue = ref('chatellerault')
|
||||
const twoValue = ref('nord')
|
||||
const fiveValue = ref('s3')
|
||||
</script>
|
||||
@@ -6,6 +6,7 @@
|
||||
"files": [
|
||||
"app/**",
|
||||
"nuxt.config.ts",
|
||||
"tailwind.config.ts",
|
||||
"README.md",
|
||||
"COMPONENTS.md"
|
||||
],
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import type {Config} from 'tailwindcss'
|
||||
import {fileURLToPath} from 'node:url'
|
||||
import {dirname, join} from 'node:path'
|
||||
|
||||
const dir = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
export default {
|
||||
content: [
|
||||
'./app/**/*.{vue,js,ts}',
|
||||
'./app/**/*.story.{vue,js,ts}',
|
||||
'./.playground/**/*.{vue,js,ts}',
|
||||
'./histoire.setup.ts',
|
||||
'./histoire.config.ts',
|
||||
join(dir, 'app/**/*.{vue,js,ts}'),
|
||||
join(dir, 'app/**/*.story.{vue,js,ts}'),
|
||||
join(dir, '.playground/**/*.{vue,js,ts}'),
|
||||
join(dir, 'histoire.setup.ts'),
|
||||
join(dir, 'histoire.config.ts'),
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
|
||||
Reference in New Issue
Block a user