@svadmin/ui 0.63.0 → 0.65.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ActivityFeed.svelte +135 -0
- package/dist/components/ActivityFeed.svelte.d.ts +21 -0
- package/dist/components/ApprovalActionCard.svelte +248 -0
- package/dist/components/ApprovalActionCard.svelte.d.ts +22 -0
- package/dist/components/AutoTable.svelte +8 -0
- package/dist/components/AutoTable.svelte.d.ts +7 -1
- package/dist/components/CanvasAnnotation.svelte +389 -0
- package/dist/components/CanvasAnnotation.svelte.d.ts +17 -0
- package/dist/components/Cascader.svelte +205 -0
- package/dist/components/Cascader.svelte.d.ts +21 -0
- package/dist/components/ColumnHeaderFilter.svelte +178 -0
- package/dist/components/ColumnHeaderFilter.svelte.d.ts +12 -0
- package/dist/components/ColumnSettings.svelte +198 -0
- package/dist/components/ColumnSettings.svelte.d.ts +19 -0
- package/dist/components/DecisionTable.svelte +240 -0
- package/dist/components/DecisionTable.svelte.d.ts +22 -0
- package/dist/components/DraggableRowTable.svelte +117 -0
- package/dist/components/DraggableRowTable.svelte.d.ts +15 -0
- package/dist/components/DrawerForm.svelte +110 -26
- package/dist/components/DrawerForm.svelte.d.ts +20 -8
- package/dist/components/DynamicFormList.svelte +214 -0
- package/dist/components/DynamicFormList.svelte.d.ts +62 -0
- package/dist/components/EditableTable.svelte +195 -0
- package/dist/components/EditableTable.svelte.d.ts +25 -0
- package/dist/components/FieldRenderer.svelte +32 -0
- package/dist/components/FilterBuilder.svelte +310 -0
- package/dist/components/FilterBuilder.svelte.d.ts +23 -0
- package/dist/components/GanttChart.svelte +144 -0
- package/dist/components/GanttChart.svelte.d.ts +20 -0
- package/dist/components/ImageCropper.svelte +122 -0
- package/dist/components/ImageCropper.svelte.d.ts +12 -0
- package/dist/components/ImportWizard.svelte +336 -0
- package/dist/components/ImportWizard.svelte.d.ts +12 -0
- package/dist/components/JsonSchemaForm.svelte +149 -0
- package/dist/components/JsonSchemaForm.svelte.d.ts +10 -0
- package/dist/components/KanbanBoard.svelte +212 -0
- package/dist/components/KanbanBoard.svelte.d.ts +30 -0
- package/dist/components/MasterDetailView.svelte +134 -0
- package/dist/components/MasterDetailView.svelte.d.ts +16 -0
- package/dist/components/MediaLibraryModal.svelte +195 -0
- package/dist/components/MediaLibraryModal.svelte.d.ts +22 -0
- package/dist/components/MentionsInput.svelte +174 -0
- package/dist/components/MentionsInput.svelte.d.ts +21 -0
- package/dist/components/ModalForm.svelte +113 -28
- package/dist/components/ModalForm.svelte.d.ts +20 -8
- package/dist/components/MultiTabKeepAlive.svelte +143 -0
- package/dist/components/MultiTabKeepAlive.svelte.d.ts +21 -0
- package/dist/components/OfflineSyncBanner.svelte +172 -0
- package/dist/components/OfflineSyncBanner.svelte.d.ts +19 -0
- package/dist/components/PdfDocumentViewer.svelte +255 -0
- package/dist/components/PdfDocumentViewer.svelte.d.ts +22 -0
- package/dist/components/PivotTable.svelte +140 -0
- package/dist/components/PivotTable.svelte.d.ts +16 -0
- package/dist/components/PresenceAvatarGroup.svelte +89 -0
- package/dist/components/PresenceAvatarGroup.svelte.d.ts +15 -0
- package/dist/components/PrintableBill.svelte +149 -0
- package/dist/components/PrintableBill.svelte.d.ts +24 -0
- package/dist/components/SensitiveDataMask.svelte +125 -0
- package/dist/components/SensitiveDataMask.svelte.d.ts +13 -0
- package/dist/components/SignaturePad.svelte +264 -0
- package/dist/components/SignaturePad.svelte.d.ts +17 -0
- package/dist/components/SplitPaneLayout.svelte +113 -0
- package/dist/components/SplitPaneLayout.svelte.d.ts +13 -0
- package/dist/components/SpreadsheetView.svelte +363 -0
- package/dist/components/SpreadsheetView.svelte.d.ts +18 -0
- package/dist/components/StepForm.svelte +180 -0
- package/dist/components/StepForm.svelte.d.ts +23 -0
- package/dist/components/TableSummary.svelte +110 -0
- package/dist/components/TableSummary.svelte.d.ts +20 -0
- package/dist/components/Transfer.svelte +352 -0
- package/dist/components/Transfer.svelte.d.ts +28 -0
- package/dist/components/TreeSelect.svelte +307 -0
- package/dist/components/TreeSelect.svelte.d.ts +22 -0
- package/dist/components/TreeTable.svelte +237 -0
- package/dist/components/TreeTable.svelte.d.ts +28 -0
- package/dist/components/UnsavedChangesPrompt.svelte +44 -0
- package/dist/components/UnsavedChangesPrompt.svelte.d.ts +7 -0
- package/dist/components/VersionDiffViewer.svelte +196 -0
- package/dist/components/VersionDiffViewer.svelte.d.ts +12 -0
- package/dist/components/VirtualTable.svelte +86 -54
- package/dist/components/VirtualTable.svelte.d.ts +14 -21
- package/dist/components/Watermark.svelte +166 -0
- package/dist/components/Watermark.svelte.d.ts +26 -0
- package/dist/components/ui/navigation-menu/index.d.ts +11 -0
- package/dist/components/ui/navigation-menu/index.js +13 -0
- package/dist/components/ui/navigation-menu/navigation-menu-content.svelte +21 -0
- package/dist/components/ui/navigation-menu/navigation-menu-content.svelte.d.ts +4 -0
- package/dist/components/ui/navigation-menu/navigation-menu-indicator.svelte +22 -0
- package/dist/components/ui/navigation-menu/navigation-menu-indicator.svelte.d.ts +4 -0
- package/dist/components/ui/navigation-menu/navigation-menu-item.svelte +17 -0
- package/dist/components/ui/navigation-menu/navigation-menu-item.svelte.d.ts +4 -0
- package/dist/components/ui/navigation-menu/navigation-menu-link.svelte +20 -0
- package/dist/components/ui/navigation-menu/navigation-menu-link.svelte.d.ts +4 -0
- package/dist/components/ui/navigation-menu/navigation-menu-list.svelte +17 -0
- package/dist/components/ui/navigation-menu/navigation-menu-list.svelte.d.ts +4 -0
- package/dist/components/ui/navigation-menu/navigation-menu-trigger.svelte +28 -0
- package/dist/components/ui/navigation-menu/navigation-menu-trigger.svelte.d.ts +4 -0
- package/dist/components/ui/navigation-menu/navigation-menu-viewport.svelte +22 -0
- package/dist/components/ui/navigation-menu/navigation-menu-viewport.svelte.d.ts +4 -0
- package/dist/components/ui/navigation-menu/navigation-menu.svelte +24 -0
- package/dist/components/ui/navigation-menu/navigation-menu.svelte.d.ts +4 -0
- package/dist/index.d.ts +68 -3
- package/dist/index.js +42 -3
- package/package.json +2 -2
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../utils.js';
|
|
3
|
+
|
|
4
|
+
export type AggregationFn = 'sum' | 'count' | 'avg' | 'min' | 'max';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
data: Record<string, unknown>[];
|
|
8
|
+
rowField: string;
|
|
9
|
+
rowLabel?: string;
|
|
10
|
+
columnField: string;
|
|
11
|
+
columnLabel?: string;
|
|
12
|
+
valueField: string;
|
|
13
|
+
valueLabel?: string;
|
|
14
|
+
aggregator?: AggregationFn;
|
|
15
|
+
formatValue?: (val: number) => string;
|
|
16
|
+
class?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
data = [],
|
|
21
|
+
rowField,
|
|
22
|
+
rowLabel = rowField,
|
|
23
|
+
columnField,
|
|
24
|
+
columnLabel = columnField,
|
|
25
|
+
valueField,
|
|
26
|
+
valueLabel = valueField,
|
|
27
|
+
aggregator = 'sum',
|
|
28
|
+
formatValue = (v: number) => v.toLocaleString(undefined, { maximumFractionDigits: 2 }),
|
|
29
|
+
class: className = '',
|
|
30
|
+
}: Props = $props();
|
|
31
|
+
|
|
32
|
+
const distinctRowValues = $derived(
|
|
33
|
+
Array.from(new Set(data.map((d) => String(d[rowField] ?? '—')))).sort()
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const distinctColValues = $derived(
|
|
37
|
+
Array.from(new Set(data.map((d) => String(d[columnField] ?? '—')))).sort()
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
function computeAgg(vals: number[]): number {
|
|
41
|
+
if (vals.length === 0) return 0;
|
|
42
|
+
if (aggregator === 'count') return vals.length;
|
|
43
|
+
if (aggregator === 'sum') return vals.reduce((a, b) => a + b, 0);
|
|
44
|
+
if (aggregator === 'avg') return vals.reduce((a, b) => a + b, 0) / vals.length;
|
|
45
|
+
if (aggregator === 'min') return Math.min(...vals);
|
|
46
|
+
if (aggregator === 'max') return Math.max(...vals);
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const pivotMatrix = $derived(() => {
|
|
51
|
+
const map = new Map<string, number[]>();
|
|
52
|
+
for (const item of data) {
|
|
53
|
+
const r = String(item[rowField] ?? '—');
|
|
54
|
+
const c = String(item[columnField] ?? '—');
|
|
55
|
+
const key = `${r}:::${c}`;
|
|
56
|
+
const num = Number(item[valueField]) || 0;
|
|
57
|
+
const existing = map.get(key);
|
|
58
|
+
if (existing) {
|
|
59
|
+
existing.push(num);
|
|
60
|
+
} else {
|
|
61
|
+
map.set(key, [num]);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return map;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
function getCellValue(r: string, c: string): number {
|
|
68
|
+
const key = `${r}:::${c}`;
|
|
69
|
+
const vals = pivotMatrix().get(key) ?? [];
|
|
70
|
+
return computeAgg(vals);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getRowTotal(r: string): number {
|
|
74
|
+
const vals = data
|
|
75
|
+
.filter((d) => String(d[rowField] ?? '—') === r)
|
|
76
|
+
.map((d) => Number(d[valueField]) || 0);
|
|
77
|
+
return computeAgg(vals);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getColTotal(c: string): number {
|
|
81
|
+
const vals = data
|
|
82
|
+
.filter((d) => String(d[columnField] ?? '—') === c)
|
|
83
|
+
.map((d) => Number(d[valueField]) || 0);
|
|
84
|
+
return computeAgg(vals);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const grandTotal = $derived(
|
|
88
|
+
computeAgg(data.map((d) => Number(d[valueField]) || 0))
|
|
89
|
+
);
|
|
90
|
+
</script>
|
|
91
|
+
|
|
92
|
+
<div class={cn('rounded-xl border border-border bg-card p-4 shadow-xs text-xs space-y-3', className)}>
|
|
93
|
+
<div class="flex items-center justify-between gap-2 pb-2 border-b border-border/60">
|
|
94
|
+
<div class="font-semibold text-foreground">
|
|
95
|
+
Pivot Analysis <span class="text-muted-foreground font-normal">({rowLabel} × {columnLabel})</span>
|
|
96
|
+
</div>
|
|
97
|
+
<div class="text-muted-foreground">
|
|
98
|
+
Aggregator: <strong class="text-primary uppercase">{aggregator}</strong> ({valueLabel})
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div class="overflow-x-auto rounded-lg border border-border/60">
|
|
103
|
+
<table class="w-full text-right border-collapse">
|
|
104
|
+
<thead class="bg-muted/40 font-semibold text-muted-foreground border-b border-border/60">
|
|
105
|
+
<tr>
|
|
106
|
+
<th class="px-3 py-2.5 text-left bg-muted/60">{rowLabel} \ {columnLabel}</th>
|
|
107
|
+
{#each distinctColValues as col (col)}
|
|
108
|
+
<th class="px-3 py-2.5 font-semibold text-foreground">{col}</th>
|
|
109
|
+
{/each}
|
|
110
|
+
<th class="px-3 py-2.5 bg-muted/60 font-semibold text-foreground">Total</th>
|
|
111
|
+
</tr>
|
|
112
|
+
</thead>
|
|
113
|
+
<tbody class="divide-y divide-border/40 font-mono">
|
|
114
|
+
{#each distinctRowValues as row (row)}
|
|
115
|
+
<tr class="hover:bg-muted/20 transition-colors">
|
|
116
|
+
<td class="px-3 py-2 text-left font-medium text-foreground bg-muted/20 font-sans">{row}</td>
|
|
117
|
+
{#each distinctColValues as col (col)}
|
|
118
|
+
{@const val = getCellValue(row, col)}
|
|
119
|
+
<td class={cn('px-3 py-2', val === 0 ? 'text-muted-foreground/40' : 'text-foreground')}>
|
|
120
|
+
{formatValue(val)}
|
|
121
|
+
</td>
|
|
122
|
+
{/each}
|
|
123
|
+
<td class="px-3 py-2 font-semibold text-foreground bg-muted/20">
|
|
124
|
+
{formatValue(getRowTotal(row))}
|
|
125
|
+
</td>
|
|
126
|
+
</tr>
|
|
127
|
+
{/each}
|
|
128
|
+
</tbody>
|
|
129
|
+
<tfoot class="bg-muted/60 font-semibold font-mono border-t-2 border-border text-foreground">
|
|
130
|
+
<tr>
|
|
131
|
+
<td class="px-3 py-2.5 text-left font-sans">Grand Total</td>
|
|
132
|
+
{#each distinctColValues as col (col)}
|
|
133
|
+
<td class="px-3 py-2.5">{formatValue(getColTotal(col))}</td>
|
|
134
|
+
{/each}
|
|
135
|
+
<td class="px-3 py-2.5 text-primary">{formatValue(grandTotal)}</td>
|
|
136
|
+
</tr>
|
|
137
|
+
</tfoot>
|
|
138
|
+
</table>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type AggregationFn = 'sum' | 'count' | 'avg' | 'min' | 'max';
|
|
2
|
+
interface Props {
|
|
3
|
+
data: Record<string, unknown>[];
|
|
4
|
+
rowField: string;
|
|
5
|
+
rowLabel?: string;
|
|
6
|
+
columnField: string;
|
|
7
|
+
columnLabel?: string;
|
|
8
|
+
valueField: string;
|
|
9
|
+
valueLabel?: string;
|
|
10
|
+
aggregator?: AggregationFn;
|
|
11
|
+
formatValue?: (val: number) => string;
|
|
12
|
+
class?: string;
|
|
13
|
+
}
|
|
14
|
+
declare const PivotTable: import("svelte").Component<Props, {}, "">;
|
|
15
|
+
type PivotTable = ReturnType<typeof PivotTable>;
|
|
16
|
+
export default PivotTable;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Users } from '@lucide/svelte';
|
|
3
|
+
import { cn } from '../utils.js';
|
|
4
|
+
|
|
5
|
+
export interface PresenceUser {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
avatar?: string;
|
|
9
|
+
status?: 'online' | 'idle' | 'editing';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
users?: PresenceUser[];
|
|
14
|
+
maxVisible?: number;
|
|
15
|
+
label?: string;
|
|
16
|
+
class?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
users = [],
|
|
21
|
+
maxVisible = 4,
|
|
22
|
+
label = 'Viewing now',
|
|
23
|
+
class: className = '',
|
|
24
|
+
}: Props = $props();
|
|
25
|
+
|
|
26
|
+
const visibleUsers = $derived(users.slice(0, maxVisible));
|
|
27
|
+
const hiddenCount = $derived(Math.max(0, users.length - maxVisible));
|
|
28
|
+
|
|
29
|
+
function getInitials(name: string): string {
|
|
30
|
+
return name
|
|
31
|
+
.split(' ')
|
|
32
|
+
.map((n) => n[0])
|
|
33
|
+
.join('')
|
|
34
|
+
.toUpperCase()
|
|
35
|
+
.slice(0, 2);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getStatusIndicator(status?: PresenceUser['status']) {
|
|
39
|
+
switch (status) {
|
|
40
|
+
case 'editing':
|
|
41
|
+
return 'bg-warning ring-warning/30 animate-pulse';
|
|
42
|
+
case 'idle':
|
|
43
|
+
return 'bg-muted-foreground ring-muted-foreground/30';
|
|
44
|
+
default:
|
|
45
|
+
return 'bg-success ring-success/30';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<div class={cn('inline-flex items-center gap-2 text-xs select-none', className)}>
|
|
51
|
+
{#if label}
|
|
52
|
+
<span class="text-muted-foreground flex items-center gap-1">
|
|
53
|
+
<Users class="h-3.5 w-3.5" />
|
|
54
|
+
{label}:
|
|
55
|
+
</span>
|
|
56
|
+
{/if}
|
|
57
|
+
|
|
58
|
+
<div class="flex items-center -space-x-2 overflow-hidden py-1">
|
|
59
|
+
{#each visibleUsers as user (user.id)}
|
|
60
|
+
<div
|
|
61
|
+
class="relative inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-full border-2 border-background bg-muted text-[11px] font-semibold text-foreground shadow-xs transition-transform hover:z-10 hover:scale-110"
|
|
62
|
+
title="{user.name} ({user.status ?? 'online'})"
|
|
63
|
+
>
|
|
64
|
+
{#if user.avatar}
|
|
65
|
+
<img src={user.avatar} alt={user.name} class="h-full w-full rounded-full object-cover" />
|
|
66
|
+
{:else}
|
|
67
|
+
<span>{getInitials(user.name)}</span>
|
|
68
|
+
{/if}
|
|
69
|
+
|
|
70
|
+
<!-- Status Dot Indicator -->
|
|
71
|
+
<span
|
|
72
|
+
class={cn(
|
|
73
|
+
'absolute bottom-0 right-0 h-2 w-2 rounded-full ring-2 ring-background',
|
|
74
|
+
getStatusIndicator(user.status)
|
|
75
|
+
)}
|
|
76
|
+
></span>
|
|
77
|
+
</div>
|
|
78
|
+
{/each}
|
|
79
|
+
|
|
80
|
+
{#if hiddenCount > 0}
|
|
81
|
+
<div
|
|
82
|
+
class="relative inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-full border-2 border-background bg-secondary text-[10px] font-medium text-secondary-foreground shadow-xs"
|
|
83
|
+
title="{hiddenCount} more users"
|
|
84
|
+
>
|
|
85
|
+
+{hiddenCount}
|
|
86
|
+
</div>
|
|
87
|
+
{/if}
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface PresenceUser {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
avatar?: string;
|
|
5
|
+
status?: 'online' | 'idle' | 'editing';
|
|
6
|
+
}
|
|
7
|
+
interface Props {
|
|
8
|
+
users?: PresenceUser[];
|
|
9
|
+
maxVisible?: number;
|
|
10
|
+
label?: string;
|
|
11
|
+
class?: string;
|
|
12
|
+
}
|
|
13
|
+
declare const PresenceAvatarGroup: import("svelte").Component<Props, {}, "">;
|
|
14
|
+
type PresenceAvatarGroup = ReturnType<typeof PresenceAvatarGroup>;
|
|
15
|
+
export default PresenceAvatarGroup;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button } from './ui/button/index.js';
|
|
3
|
+
import { Printer } from '@lucide/svelte';
|
|
4
|
+
import { cn } from '../utils.js';
|
|
5
|
+
|
|
6
|
+
export interface BillItem {
|
|
7
|
+
name: string;
|
|
8
|
+
spec?: string;
|
|
9
|
+
quantity: number;
|
|
10
|
+
unitPrice: number;
|
|
11
|
+
total?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
title?: string;
|
|
16
|
+
billNumber: string;
|
|
17
|
+
date: string;
|
|
18
|
+
companyName?: string;
|
|
19
|
+
companyAddress?: string;
|
|
20
|
+
customerName: string;
|
|
21
|
+
customerAddress?: string;
|
|
22
|
+
items?: BillItem[];
|
|
23
|
+
notes?: string;
|
|
24
|
+
issuer?: string;
|
|
25
|
+
onprint?: () => void;
|
|
26
|
+
class?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let {
|
|
30
|
+
title = 'INVOICE / RECEIPT',
|
|
31
|
+
billNumber,
|
|
32
|
+
date,
|
|
33
|
+
companyName = 'Enterprise Admin Co., Ltd.',
|
|
34
|
+
companyAddress,
|
|
35
|
+
customerName,
|
|
36
|
+
customerAddress,
|
|
37
|
+
items = [],
|
|
38
|
+
notes,
|
|
39
|
+
issuer,
|
|
40
|
+
onprint,
|
|
41
|
+
class: className = '',
|
|
42
|
+
}: Props = $props();
|
|
43
|
+
|
|
44
|
+
const subtotal = $derived(
|
|
45
|
+
items.reduce((acc, item) => acc + (item.total ?? item.quantity * item.unitPrice), 0)
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
function handlePrint() {
|
|
49
|
+
if (onprint) {
|
|
50
|
+
onprint();
|
|
51
|
+
} else if (typeof window !== 'undefined') {
|
|
52
|
+
window.print();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<div class={cn('space-y-4 text-xs', className)}>
|
|
58
|
+
<!-- Print Trigger Bar (Hidden during actual print) -->
|
|
59
|
+
<div class="flex items-center justify-between p-3 rounded-xl border border-border bg-card shadow-xs print:hidden">
|
|
60
|
+
<div class="text-muted-foreground">
|
|
61
|
+
Document Ready: <strong class="text-foreground">{billNumber}</strong>
|
|
62
|
+
</div>
|
|
63
|
+
<Button size="sm" class="h-8 text-xs gap-1.5" onclick={handlePrint}>
|
|
64
|
+
<Printer class="h-3.5 w-3.5" />
|
|
65
|
+
Print Document
|
|
66
|
+
</Button>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<!-- Printable A4 Paper Container -->
|
|
70
|
+
<div class="rounded-xl border border-border bg-card p-8 shadow-xs max-w-3xl mx-auto space-y-6 print:border-none print:shadow-none print:p-0 print:m-0">
|
|
71
|
+
<!-- Header -->
|
|
72
|
+
<div class="flex items-start justify-between border-b-2 border-primary/40 pb-4">
|
|
73
|
+
<div>
|
|
74
|
+
<h2 class="text-xl font-bold text-foreground tracking-tight">{title}</h2>
|
|
75
|
+
<p class="text-muted-foreground font-mono mt-0.5">No: {billNumber}</p>
|
|
76
|
+
</div>
|
|
77
|
+
<div class="text-right space-y-0.5">
|
|
78
|
+
<h3 class="font-semibold text-foreground">{companyName}</h3>
|
|
79
|
+
{#if companyAddress}
|
|
80
|
+
<p class="text-muted-foreground text-[11px]">{companyAddress}</p>
|
|
81
|
+
{/if}
|
|
82
|
+
<p class="text-muted-foreground text-[11px]">Date: {date}</p>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<!-- Customer Info -->
|
|
87
|
+
<div class="p-3 rounded-lg bg-muted/20 border border-border/40 space-y-1">
|
|
88
|
+
<span class="text-muted-foreground font-semibold uppercase text-[10px] block">Bill To</span>
|
|
89
|
+
<div class="font-semibold text-foreground">{customerName}</div>
|
|
90
|
+
{#if customerAddress}
|
|
91
|
+
<div class="text-muted-foreground text-[11px]">{customerAddress}</div>
|
|
92
|
+
{/if}
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<!-- Items Table -->
|
|
96
|
+
<table class="w-full text-left border-collapse border border-border/60">
|
|
97
|
+
<thead class="bg-muted/40 font-semibold text-foreground border-b border-border/60">
|
|
98
|
+
<tr>
|
|
99
|
+
<th class="p-2 border-r border-border/40 w-12 text-center">#</th>
|
|
100
|
+
<th class="p-2 border-r border-border/40">Item Description</th>
|
|
101
|
+
<th class="p-2 border-r border-border/40 text-center w-20">Qty</th>
|
|
102
|
+
<th class="p-2 border-r border-border/40 text-right w-24">Unit Price</th>
|
|
103
|
+
<th class="p-2 text-right w-28">Amount</th>
|
|
104
|
+
</tr>
|
|
105
|
+
</thead>
|
|
106
|
+
<tbody class="divide-y divide-border/40">
|
|
107
|
+
{#each items as item, index (index)}
|
|
108
|
+
{@const rowTotal = item.total ?? item.quantity * item.unitPrice}
|
|
109
|
+
<tr>
|
|
110
|
+
<td class="p-2 border-r border-border/40 text-center text-muted-foreground">{index + 1}</td>
|
|
111
|
+
<td class="p-2 border-r border-border/40 font-medium text-foreground">
|
|
112
|
+
{item.name}
|
|
113
|
+
{#if item.spec}
|
|
114
|
+
<span class="block text-[11px] text-muted-foreground">{item.spec}</span>
|
|
115
|
+
{/if}
|
|
116
|
+
</td>
|
|
117
|
+
<td class="p-2 border-r border-border/40 text-center tabular-nums">{item.quantity}</td>
|
|
118
|
+
<td class="p-2 border-r border-border/40 text-right tabular-nums">¥{item.unitPrice.toFixed(2)}</td>
|
|
119
|
+
<td class="p-2 text-right tabular-nums font-semibold text-foreground">¥{rowTotal.toFixed(2)}</td>
|
|
120
|
+
</tr>
|
|
121
|
+
{/each}
|
|
122
|
+
</tbody>
|
|
123
|
+
<tfoot class="border-t-2 border-border font-semibold text-foreground bg-muted/20">
|
|
124
|
+
<tr>
|
|
125
|
+
<td colspan="4" class="p-2.5 text-right border-r border-border/40">Total Summary:</td>
|
|
126
|
+
<td class="p-2.5 text-right text-sm tabular-nums text-primary font-bold">¥{subtotal.toFixed(2)}</td>
|
|
127
|
+
</tr>
|
|
128
|
+
</tfoot>
|
|
129
|
+
</table>
|
|
130
|
+
|
|
131
|
+
<!-- Footer Notes & Signatures -->
|
|
132
|
+
<div class="flex items-end justify-between pt-4 border-t border-border/40 text-xs">
|
|
133
|
+
<div class="space-y-1 max-w-sm">
|
|
134
|
+
{#if notes}
|
|
135
|
+
<span class="font-semibold text-muted-foreground block text-[11px]">Notes:</span>
|
|
136
|
+
<p class="text-muted-foreground">{notes}</p>
|
|
137
|
+
{/if}
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<div class="text-right space-y-4">
|
|
141
|
+
{#if issuer}
|
|
142
|
+
<p class="text-muted-foreground">Issued By: <strong class="text-foreground">{issuer}</strong></p>
|
|
143
|
+
{/if}
|
|
144
|
+
<div class="w-40 border-b border-foreground/40 pt-6"></div>
|
|
145
|
+
<p class="text-[10px] text-muted-foreground text-center">Authorized Signature & Stamp</p>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface BillItem {
|
|
2
|
+
name: string;
|
|
3
|
+
spec?: string;
|
|
4
|
+
quantity: number;
|
|
5
|
+
unitPrice: number;
|
|
6
|
+
total?: number;
|
|
7
|
+
}
|
|
8
|
+
interface Props {
|
|
9
|
+
title?: string;
|
|
10
|
+
billNumber: string;
|
|
11
|
+
date: string;
|
|
12
|
+
companyName?: string;
|
|
13
|
+
companyAddress?: string;
|
|
14
|
+
customerName: string;
|
|
15
|
+
customerAddress?: string;
|
|
16
|
+
items?: BillItem[];
|
|
17
|
+
notes?: string;
|
|
18
|
+
issuer?: string;
|
|
19
|
+
onprint?: () => void;
|
|
20
|
+
class?: string;
|
|
21
|
+
}
|
|
22
|
+
declare const PrintableBill: import("svelte").Component<Props, {}, "">;
|
|
23
|
+
type PrintableBill = ReturnType<typeof PrintableBill>;
|
|
24
|
+
export default PrintableBill;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { untrack } from 'svelte';
|
|
3
|
+
import { Eye, EyeOff, Copy, Check } from '@lucide/svelte';
|
|
4
|
+
import { Button } from './ui/button/index.js';
|
|
5
|
+
import { cn } from '../utils.js';
|
|
6
|
+
|
|
7
|
+
export type MaskType = 'phone' | 'id-card' | 'email' | 'bank-card' | 'secret' | 'custom';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
value?: string | number | null;
|
|
11
|
+
type?: MaskType;
|
|
12
|
+
maskPattern?: (val: string) => string;
|
|
13
|
+
allowUnmask?: boolean;
|
|
14
|
+
defaultMasked?: boolean;
|
|
15
|
+
onUnmask?: () => void | Promise<void>;
|
|
16
|
+
class?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
value = '',
|
|
21
|
+
type = 'phone',
|
|
22
|
+
maskPattern,
|
|
23
|
+
allowUnmask = true,
|
|
24
|
+
defaultMasked = true,
|
|
25
|
+
onUnmask,
|
|
26
|
+
class: className = '',
|
|
27
|
+
}: Props = $props();
|
|
28
|
+
|
|
29
|
+
let isMasked = $state(untrack(() => defaultMasked));
|
|
30
|
+
let isCopied = $state(false);
|
|
31
|
+
|
|
32
|
+
const rawString = $derived(String(value ?? ''));
|
|
33
|
+
|
|
34
|
+
function maskValue(val: string, maskType: MaskType): string {
|
|
35
|
+
if (!val) return '—';
|
|
36
|
+
if (maskPattern) return maskPattern(val);
|
|
37
|
+
|
|
38
|
+
switch (maskType) {
|
|
39
|
+
case 'phone':
|
|
40
|
+
// e.g. 13812345678 -> 138****5678
|
|
41
|
+
return val.length >= 7
|
|
42
|
+
? `${val.slice(0, 3)}****${val.slice(-4)}`
|
|
43
|
+
: '****';
|
|
44
|
+
case 'id-card':
|
|
45
|
+
// e.g. 110101199001011234 -> 110101********1234
|
|
46
|
+
return val.length >= 10
|
|
47
|
+
? `${val.slice(0, 6)}********${val.slice(-4)}`
|
|
48
|
+
: '********';
|
|
49
|
+
case 'email': {
|
|
50
|
+
const parts = val.split('@');
|
|
51
|
+
if (parts.length === 2) {
|
|
52
|
+
const name = parts[0];
|
|
53
|
+
const maskedName = name.length > 2 ? `${name[0]}***${name.slice(-1)}` : `${name[0]}***`;
|
|
54
|
+
return `${maskedName}@${parts[1]}`;
|
|
55
|
+
}
|
|
56
|
+
return '***@***.***';
|
|
57
|
+
}
|
|
58
|
+
case 'bank-card':
|
|
59
|
+
return val.length >= 8
|
|
60
|
+
? `${val.slice(0, 4)} **** **** ${val.slice(-4)}`
|
|
61
|
+
: '**** ****';
|
|
62
|
+
case 'secret':
|
|
63
|
+
return val.length >= 8
|
|
64
|
+
? `${val.slice(0, 3)}••••••••••••${val.slice(-4)}`
|
|
65
|
+
: '••••••••';
|
|
66
|
+
default:
|
|
67
|
+
return '••••••••';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const displayedText = $derived(isMasked ? maskValue(rawString, type) : rawString);
|
|
72
|
+
|
|
73
|
+
async function toggleMask() {
|
|
74
|
+
if (isMasked && onUnmask) {
|
|
75
|
+
await onUnmask();
|
|
76
|
+
}
|
|
77
|
+
isMasked = !isMasked;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function handleCopy() {
|
|
81
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard) {
|
|
82
|
+
await navigator.clipboard.writeText(rawString);
|
|
83
|
+
isCopied = true;
|
|
84
|
+
setTimeout(() => {
|
|
85
|
+
isCopied = false;
|
|
86
|
+
}, 1500);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
</script>
|
|
90
|
+
|
|
91
|
+
<div class={cn('inline-flex items-center gap-1.5 font-mono text-xs text-foreground', className)}>
|
|
92
|
+
<span class="truncate select-all">{displayedText}</span>
|
|
93
|
+
|
|
94
|
+
{#if allowUnmask && rawString}
|
|
95
|
+
<Button
|
|
96
|
+
variant="ghost"
|
|
97
|
+
size="icon"
|
|
98
|
+
class="h-5 w-5 text-muted-foreground hover:text-foreground"
|
|
99
|
+
onclick={toggleMask}
|
|
100
|
+
aria-label={isMasked ? 'Reveal sensitive data' : 'Mask sensitive data'}
|
|
101
|
+
>
|
|
102
|
+
{#if isMasked}
|
|
103
|
+
<Eye class="h-3 w-3" />
|
|
104
|
+
{:else}
|
|
105
|
+
<EyeOff class="h-3 w-3" />
|
|
106
|
+
{/if}
|
|
107
|
+
</Button>
|
|
108
|
+
{/if}
|
|
109
|
+
|
|
110
|
+
{#if rawString}
|
|
111
|
+
<Button
|
|
112
|
+
variant="ghost"
|
|
113
|
+
size="icon"
|
|
114
|
+
class="h-5 w-5 text-muted-foreground hover:text-foreground"
|
|
115
|
+
onclick={handleCopy}
|
|
116
|
+
aria-label="Copy to clipboard"
|
|
117
|
+
>
|
|
118
|
+
{#if isCopied}
|
|
119
|
+
<Check class="h-3 w-3 text-success" />
|
|
120
|
+
{:else}
|
|
121
|
+
<Copy class="h-3 w-3" />
|
|
122
|
+
{/if}
|
|
123
|
+
</Button>
|
|
124
|
+
{/if}
|
|
125
|
+
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type MaskType = 'phone' | 'id-card' | 'email' | 'bank-card' | 'secret' | 'custom';
|
|
2
|
+
interface Props {
|
|
3
|
+
value?: string | number | null;
|
|
4
|
+
type?: MaskType;
|
|
5
|
+
maskPattern?: (val: string) => string;
|
|
6
|
+
allowUnmask?: boolean;
|
|
7
|
+
defaultMasked?: boolean;
|
|
8
|
+
onUnmask?: () => void | Promise<void>;
|
|
9
|
+
class?: string;
|
|
10
|
+
}
|
|
11
|
+
declare const SensitiveDataMask: import("svelte").Component<Props, {}, "">;
|
|
12
|
+
type SensitiveDataMask = ReturnType<typeof SensitiveDataMask>;
|
|
13
|
+
export default SensitiveDataMask;
|