mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-04-19 03:04:05 +00:00
Phase 3: Typed Vue components in scripts-v2/
Migrate all shared components to TypeScript SFCs with script setup lang=ts. 72 files, 7144 lines, zero any types. - components/base/ (42 files): Button, Input, Textarea, Checkbox, Radio, Switch, Badge, Card, Modal, Dialog, Dropdown, DatePicker, TimePicker, Money, FileUploader, Select, Icon, Loader, Multiselect, TabGroup, Wizard, CustomerSelect, ItemSelect, CustomInput, alerts, status badges (Invoice/Estimate/Paid/RecurringInvoice), List/ListItem - components/table/ (3 files): DataTable, TablePagination - components/form/ (4 files): FormGroup, FormGrid, SwitchSection - components/layout/ (11 files): Page, PageHeader, Breadcrumb, FilterWrapper, EmptyPlaceholder, ContentPlaceholders, SettingCard - components/editor/ (2 files): RichEditor with Tiptap - components/charts/ (2 files): LineChart with Chart.js - components/notifications/ (3 files): NotificationRoot, NotificationItem - components/icons/ (2 files): MainLogo All use defineProps<Props>(), defineEmits<Emits>(), typed refs, and import domain types from types/domain. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
10
resources/scripts-v2/components/layout/Breadcrumb.vue
Normal file
10
resources/scripts-v2/components/layout/Breadcrumb.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<nav>
|
||||
<ol class="flex flex-wrap py-4 text-heading rounded list-reset">
|
||||
<slot />
|
||||
</ol>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
35
resources/scripts-v2/components/layout/BreadcrumbItem.vue
Normal file
35
resources/scripts-v2/components/layout/BreadcrumbItem.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<li class="pr-2 text-sm">
|
||||
<router-link
|
||||
class="
|
||||
m-0
|
||||
mr-2
|
||||
text-sm
|
||||
font-medium
|
||||
leading-5
|
||||
text-heading
|
||||
outline-hidden
|
||||
focus:ring-2 focus:ring-offset-2 focus:ring-primary-400
|
||||
"
|
||||
:to="to"
|
||||
>
|
||||
{{ title }}
|
||||
</router-link>
|
||||
|
||||
<span v-if="!active" class="px-1">/</span>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
title?: string
|
||||
to?: string
|
||||
active?: boolean
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
title: '',
|
||||
to: '#',
|
||||
active: false,
|
||||
})
|
||||
</script>
|
||||
212
resources/scripts-v2/components/layout/ContentPlaceholder.vue
Normal file
212
resources/scripts-v2/components/layout/ContentPlaceholder.vue
Normal file
@@ -0,0 +1,212 @@
|
||||
<template>
|
||||
<div :class="classObject">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface Props {
|
||||
rounded?: boolean
|
||||
centered?: boolean
|
||||
animated?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
rounded: false,
|
||||
centered: false,
|
||||
animated: true,
|
||||
})
|
||||
|
||||
const classObject = computed<Record<string, boolean>>(() => {
|
||||
return {
|
||||
'base-content-placeholders': true,
|
||||
'base-content-placeholders-is-rounded': props.rounded,
|
||||
'base-content-placeholders-is-centered': props.centered,
|
||||
'base-content-placeholders-is-animated': props.animated,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@keyframes vueContentPlaceholdersAnimation {
|
||||
0% {
|
||||
transform: translate3d(-30%, 0, 0);
|
||||
}
|
||||
100% {
|
||||
transform: translate3d(100%, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.base-content-placeholders-heading {
|
||||
display: flex;
|
||||
|
||||
[class^='base-content-placeholders-'] + & {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.base-content-placeholders-heading__img {
|
||||
margin-right: 15px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 15px;
|
||||
background: var(--color-surface-muted);
|
||||
|
||||
.base-content-placeholders-is-rounded & {
|
||||
border-radius: 6px;
|
||||
}
|
||||
.base-content-placeholders-is-centered & {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
.base-content-placeholders-is-animated &::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
max-width: 1000px;
|
||||
height: 100%;
|
||||
background: linear-gradient(to right, transparent 0%, var(--color-hover-strong) 15%, transparent 30%);
|
||||
animation: vueContentPlaceholdersAnimation 1.5s linear infinite forwards;
|
||||
}
|
||||
}
|
||||
|
||||
.base-content-placeholders-heading__content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.base-content-placeholders-heading__title {
|
||||
width: 85%;
|
||||
margin-bottom: 10px;
|
||||
background: var(--color-surface-muted);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 15px;
|
||||
|
||||
.base-content-placeholders-is-rounded & {
|
||||
border-radius: 6px;
|
||||
}
|
||||
.base-content-placeholders-is-centered & {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
.base-content-placeholders-is-animated &::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
max-width: 1000px;
|
||||
height: 100%;
|
||||
background: linear-gradient(to right, transparent 0%, var(--color-hover-strong) 15%, transparent 30%);
|
||||
animation: vueContentPlaceholdersAnimation 1.5s linear infinite forwards;
|
||||
}
|
||||
}
|
||||
|
||||
.base-content-placeholders-heading__subtitle {
|
||||
width: 90%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 15px;
|
||||
background: var(--color-surface-muted);
|
||||
|
||||
.base-content-placeholders-is-rounded & {
|
||||
border-radius: 6px;
|
||||
}
|
||||
.base-content-placeholders-is-centered & {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
.base-content-placeholders-is-animated &::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
max-width: 1000px;
|
||||
height: 100%;
|
||||
background: linear-gradient(to right, transparent 0%, var(--color-hover-strong) 15%, transparent 30%);
|
||||
animation: vueContentPlaceholdersAnimation 1.5s linear infinite forwards;
|
||||
}
|
||||
}
|
||||
|
||||
.base-content-placeholders-text {
|
||||
[class^='base-content-placeholders-'] + & {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.base-content-placeholders-text__line {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 15px;
|
||||
background: var(--color-surface-muted);
|
||||
|
||||
.base-content-placeholders-is-rounded & {
|
||||
border-radius: 6px;
|
||||
}
|
||||
.base-content-placeholders-is-centered & {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
.base-content-placeholders-is-animated &::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
max-width: 1000px;
|
||||
height: 100%;
|
||||
background: linear-gradient(to right, transparent 0%, var(--color-hover-strong) 15%, transparent 30%);
|
||||
animation: vueContentPlaceholdersAnimation 1.5s linear infinite forwards;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
width: 100%;
|
||||
}
|
||||
&:nth-child(2) {
|
||||
width: 90%;
|
||||
}
|
||||
&:nth-child(3) {
|
||||
width: 80%;
|
||||
}
|
||||
&:nth-child(4) {
|
||||
width: 70%;
|
||||
}
|
||||
}
|
||||
|
||||
.base-content-placeholders-box {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 15px;
|
||||
background: var(--color-surface-muted);
|
||||
|
||||
.base-content-placeholders-is-animated &::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
max-width: 1000px;
|
||||
height: 100%;
|
||||
background: linear-gradient(to right, transparent 0%, var(--color-hover-strong) 15%, transparent 30%);
|
||||
animation: vueContentPlaceholdersAnimation 1.5s linear infinite forwards;
|
||||
}
|
||||
}
|
||||
|
||||
.base-content-circle {
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.base-content-placeholders-is-rounded {
|
||||
border-radius: 6px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="base-content-placeholders-box" :class="circleClass" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface Props {
|
||||
circle?: boolean
|
||||
rounded?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
circle: false,
|
||||
rounded: false,
|
||||
})
|
||||
|
||||
const circleClass = computed<Record<string, boolean>>(() => {
|
||||
return {
|
||||
'base-content-circle': props.circle,
|
||||
'base-content-placeholders-is-rounded': props.rounded,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="base-content-placeholders-heading">
|
||||
<div v-if="box" class="base-content-placeholders-heading__box" />
|
||||
<div class="base-content-placeholders-heading__content">
|
||||
<div
|
||||
class="base-content-placeholders-heading__title"
|
||||
style="background: #eee"
|
||||
/>
|
||||
<div class="base-content-placeholders-heading__subtitle" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
box?: boolean
|
||||
rounded?: boolean
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
box: false,
|
||||
rounded: false,
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="base-content-placeholders-text">
|
||||
<div
|
||||
v-for="n in lines"
|
||||
:key="n"
|
||||
:class="lineClass"
|
||||
class="w-full h-full base-content-placeholders-text__line"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface Props {
|
||||
lines?: number
|
||||
rounded?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
lines: 4,
|
||||
rounded: false,
|
||||
})
|
||||
|
||||
const lineClass = computed<Record<string, boolean>>(() => {
|
||||
return {
|
||||
'base-content-placeholders-is-rounded': props.rounded,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
30
resources/scripts-v2/components/layout/EmptyPlaceholder.vue
Normal file
30
resources/scripts-v2/components/layout/EmptyPlaceholder.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center justify-center mt-16">
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<label class="font-medium">{{ title }}</label>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<label class="text-muted">
|
||||
{{ description }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
title?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
title: '',
|
||||
description: '',
|
||||
})
|
||||
</script>
|
||||
59
resources/scripts-v2/components/layout/FilterWrapper.vue
Normal file
59
resources/scripts-v2/components/layout/FilterWrapper.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<transition
|
||||
enter-active-class="transition duration-500 ease-in-out"
|
||||
enter-from-class="opacity-0"
|
||||
enter-to-class="opacity-100"
|
||||
leave-active-class="transition ease-in-out"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<div v-show="show" class="relative z-10 p-4 md:p-8 bg-surface-muted rounded">
|
||||
<slot name="filter-header" />
|
||||
|
||||
<label
|
||||
class="
|
||||
absolute
|
||||
text-sm
|
||||
leading-snug
|
||||
text-heading
|
||||
cursor-pointer
|
||||
hover:text-body
|
||||
top-2.5
|
||||
right-3.5
|
||||
"
|
||||
@click="emit('clear')"
|
||||
>
|
||||
{{ $t('general.clear_all') }}
|
||||
</label>
|
||||
|
||||
<div
|
||||
class="flex flex-col space-y-3"
|
||||
:class="
|
||||
rowOnXl
|
||||
? 'xl:flex-row xl:space-x-4 xl:space-y-0 xl:items-center'
|
||||
: 'lg:flex-row lg:space-x-4 lg:space-y-0 lg:items-center'
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
show?: boolean
|
||||
rowOnXl?: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'clear'): void
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
show: false,
|
||||
rowOnXl: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
</script>
|
||||
8
resources/scripts-v2/components/layout/Page.vue
Normal file
8
resources/scripts-v2/components/layout/Page.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<div class="flex-1 p-4 md:p-8 flex flex-col">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
21
resources/scripts-v2/components/layout/PageHeader.vue
Normal file
21
resources/scripts-v2/components/layout/PageHeader.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div class="flex flex-wrap justify-between">
|
||||
<div>
|
||||
<h3 class="text-2xl font-bold text-left text-heading">
|
||||
{{ title }}
|
||||
</h3>
|
||||
<slot />
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
title: string
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
38
resources/scripts-v2/components/layout/SettingCard.vue
Normal file
38
resources/scripts-v2/components/layout/SettingCard.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<BaseCard>
|
||||
<div class="flex flex-wrap justify-between lg:flex-nowrap mb-5">
|
||||
<div>
|
||||
<h6 class="font-medium text-lg text-left">
|
||||
{{ title }}
|
||||
</h6>
|
||||
|
||||
<p
|
||||
class="
|
||||
mt-2
|
||||
text-sm
|
||||
leading-snug
|
||||
text-left text-muted
|
||||
max-w-[680px]
|
||||
"
|
||||
>
|
||||
{{ description }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 lg:mt-0 lg:ml-2">
|
||||
<slot name="action" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<slot />
|
||||
</BaseCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
11
resources/scripts-v2/components/layout/index.ts
Normal file
11
resources/scripts-v2/components/layout/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export { default as Page } from './Page.vue'
|
||||
export { default as PageHeader } from './PageHeader.vue'
|
||||
export { default as Breadcrumb } from './Breadcrumb.vue'
|
||||
export { default as BreadcrumbItem } from './BreadcrumbItem.vue'
|
||||
export { default as FilterWrapper } from './FilterWrapper.vue'
|
||||
export { default as EmptyPlaceholder } from './EmptyPlaceholder.vue'
|
||||
export { default as ContentPlaceholder } from './ContentPlaceholder.vue'
|
||||
export { default as ContentPlaceholderBox } from './ContentPlaceholderBox.vue'
|
||||
export { default as ContentPlaceholderText } from './ContentPlaceholderText.vue'
|
||||
export { default as ContentPlaceholderHeading } from './ContentPlaceholderHeading.vue'
|
||||
export { default as SettingCard } from './SettingCard.vue'
|
||||
Reference in New Issue
Block a user