@tuturuuu/ui 0.7.0 → 0.9.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/CHANGELOG.md +88 -0
- package/biome.json +1 -1
- package/package.json +75 -73
- package/src/components/ui/accordion.tsx +1 -1
- package/src/components/ui/breadcrumb.tsx +1 -1
- package/src/components/ui/calendar-app/calendar-page-shell.tsx +4 -0
- package/src/components/ui/calendar-app/components/calendar-connections-settings-content.tsx +239 -33
- package/src/components/ui/calendar-app/components/load-smart-scheduling-tasks.tsx +143 -0
- package/src/components/ui/calendar-app/components/priority-view.tsx +10 -3
- package/src/components/ui/calendar-app/components/tasks-sidebar.tsx +4 -116
- package/src/components/ui/calendar-app/components/use-calendar-connections-manager.ts +67 -2
- package/src/components/ui/calendar.tsx +1 -1
- package/src/components/ui/carousel.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-external-thread-panel.test.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-external-thread-panel.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-operations-panel.test.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-operations-panel.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-setup-panel.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-sidebar.test.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-sidebar.tsx +2 -2
- package/src/components/ui/chat/chat-agent-details-utils.test.ts +1 -1
- package/src/components/ui/chat/chat-agent-details-utils.tsx +1 -1
- package/src/components/ui/chat/chat-agent-details-zalo-personal-panel.tsx +2 -2
- package/src/components/ui/checkbox.tsx +1 -1
- package/src/components/ui/color-picker.tsx +1 -1
- package/src/components/ui/command.tsx +1 -1
- package/src/components/ui/context-menu.tsx +5 -1
- package/src/components/ui/currency-input.test.tsx +43 -0
- package/src/components/ui/currency-input.tsx +1 -1
- package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +3 -0
- package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +19 -0
- package/src/components/ui/custom/combobox.test.tsx +195 -0
- package/src/components/ui/custom/combobox.tsx +273 -156
- package/src/components/ui/custom/education/modules/youtube/delete-link-button.tsx +5 -13
- package/src/components/ui/custom/facebook-mockup/facebook-mockup.tsx +7 -1
- package/src/components/ui/custom/facebook-mockup/form.tsx +1 -1
- package/src/components/ui/custom/facebook-mockup/image-upload-field.tsx +1 -1
- package/src/components/ui/custom/facebook-mockup/preview.tsx +1 -1
- package/src/components/ui/custom/settings-dialog-shell.tsx +2 -1
- package/src/components/ui/custom/theme-toggle.tsx +1 -1
- package/src/components/ui/custom/workspace-access/workspace-access-default-role-card.tsx +60 -35
- package/src/components/ui/custom/workspace-access/workspace-access-member-row.tsx +176 -167
- package/src/components/ui/custom/workspace-access/workspace-access-members.tsx +16 -10
- package/src/components/ui/custom/workspace-access/workspace-access-page-header.tsx +75 -36
- package/src/components/ui/custom/workspace-access/workspace-access-page.tsx +39 -42
- package/src/components/ui/custom/workspace-access/workspace-access-people-filters.tsx +1 -1
- package/src/components/ui/custom/workspace-access/workspace-access-roles.tsx +113 -91
- package/src/components/ui/custom/workspace-access/workspace-access-tabs-toolbar.tsx +73 -32
- package/src/components/ui/custom/workspace-select.tsx +8 -3
- package/src/components/ui/dialog.test.tsx +52 -0
- package/src/components/ui/dialog.tsx +6 -2
- package/src/components/ui/dropdown-menu.tsx +5 -1
- package/src/components/ui/finance/debts/debt-loan-form.tsx +12 -5
- package/src/components/ui/finance/debts/debt-loan-summary.tsx +3 -2
- package/src/components/ui/finance/debts/debts-page.test.tsx +54 -5
- package/src/components/ui/finance/debts/debts-page.tsx +15 -2
- package/src/components/ui/finance/invoices/components/subscription-group-selector.tsx +3 -5
- package/src/components/ui/finance/invoices/new-invoice-page.test.tsx +25 -5
- package/src/components/ui/finance/invoices/new-invoice-page.tsx +7 -2
- package/src/components/ui/finance/invoices/standard-invoice.tsx +4 -2
- package/src/components/ui/finance/invoices/subscription-invoice.tsx +4 -2
- package/src/components/ui/finance/invoices/utils.ts +3 -1
- package/src/components/ui/finance/transactions/form-content-dialog.tsx +3 -0
- package/src/components/ui/finance/transactions/form-types.ts +3 -0
- package/src/components/ui/finance/transactions/form.tsx +2 -0
- package/src/components/ui/finance/transactions/infinite-transactions-list.tsx +2 -0
- package/src/components/ui/finance/transactions/period-charts/category-breakdown-dialog.tsx +1 -1
- package/src/components/ui/finance/transactions/transaction-card.tsx +21 -9
- package/src/components/ui/finance/transactions/transaction-edit-dialog.tsx +1 -4
- package/src/components/ui/finance/transactions/transactions-create-summary.tsx +3 -0
- package/src/components/ui/finance/transactions/transactions-page.tsx +4 -1
- package/src/components/ui/finance/wallets/form.test.tsx +51 -3
- package/src/components/ui/finance/wallets/form.tsx +15 -4
- package/src/components/ui/finance/wallets/walletId/wallet-details-actions.tsx +4 -0
- package/src/components/ui/finance/wallets/walletId/wallet-details-page.tsx +4 -2
- package/src/components/ui/finance/wallets/wallets-data-table.tsx +1 -0
- package/src/components/ui/finance/wallets/wallets-page.tsx +5 -2
- package/src/components/ui/input-otp.tsx +1 -1
- package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +28 -39
- package/src/components/ui/legacy/calendar/calendar-cell.tsx +2 -0
- package/src/components/ui/legacy/calendar/calendar-content.tsx +10 -6
- package/src/components/ui/legacy/calendar/calendar-header.tsx +23 -3
- package/src/components/ui/legacy/calendar/calendar-loading-skeleton.tsx +135 -0
- package/src/components/ui/legacy/calendar/calendar-matrix.tsx +175 -237
- package/src/components/ui/legacy/calendar/event-card.test.tsx +177 -0
- package/src/components/ui/legacy/calendar/event-card.tsx +220 -131
- package/src/components/ui/legacy/calendar/event-modal.tsx +17 -17
- package/src/components/ui/legacy/calendar/event-provider-display.tsx +69 -0
- package/src/components/ui/legacy/calendar/smart-calendar.test.tsx +86 -4
- package/src/components/ui/legacy/calendar/smart-calendar.tsx +32 -2
- package/src/components/ui/legacy/meet/create-plan-dialog.tsx +19 -10
- package/src/components/ui/money-input.test.tsx +64 -0
- package/src/components/ui/money-input.tsx +63 -0
- package/src/components/ui/navigation-menu.tsx +1 -1
- package/src/components/ui/pagination.tsx +1 -1
- package/src/components/ui/radio-group.tsx +1 -1
- package/src/components/ui/select.tsx +5 -1
- package/src/components/ui/sheet.tsx +1 -1
- package/src/components/ui/sidebar.tsx +1 -1
- package/src/components/ui/storefront/cart-popover.tsx +61 -0
- package/src/components/ui/storefront/cart-summary-parts.tsx +290 -0
- package/src/components/ui/storefront/cart-summary.tsx +104 -80
- package/src/components/ui/storefront/checkout-overlay.tsx +26 -0
- package/src/components/ui/storefront/hero-panel.tsx +2 -8
- package/src/components/ui/storefront/image-panel.tsx +6 -0
- package/src/components/ui/storefront/index.ts +11 -0
- package/src/components/ui/storefront/listing-card.tsx +84 -22
- package/src/components/ui/storefront/merch-sections.tsx +70 -0
- package/src/components/ui/storefront/product-detail.tsx +289 -0
- package/src/components/ui/storefront/product-dialog.tsx +72 -0
- package/src/components/ui/storefront/storefront-surface.test.tsx +221 -3
- package/src/components/ui/storefront/storefront-surface.tsx +288 -153
- package/src/components/ui/storefront/types.ts +27 -1
- package/src/components/ui/storefront/utils.ts +117 -27
- package/src/components/ui/text-editor/__tests__/content-migration.test.ts +32 -0
- package/src/components/ui/text-editor/__tests__/extensions.test.ts +123 -0
- package/src/components/ui/text-editor/__tests__/image-extension.test.ts +69 -1
- package/src/components/ui/text-editor/__tests__/video-extension.test.ts +47 -0
- package/src/components/ui/text-editor/background-color-extension.ts +62 -0
- package/src/components/ui/text-editor/color-controls.tsx +284 -0
- package/src/components/ui/text-editor/content-migration.ts +41 -18
- package/src/components/ui/text-editor/editor.tsx +69 -14
- package/src/components/ui/text-editor/extensions.ts +9 -3
- package/src/components/ui/text-editor/highlight-extension.ts +22 -0
- package/src/components/ui/text-editor/image-extension.ts +40 -18
- package/src/components/ui/text-editor/tool-bar.tsx +9 -16
- package/src/components/ui/text-editor/video-extension.ts +11 -2
- package/src/components/ui/toast.tsx +1 -1
- package/src/components/ui/tu-do/boards/__tests__/board-share-dialog.test.tsx +270 -0
- package/src/components/ui/tu-do/boards/__tests__/workspace-projects-client-page.test.tsx +70 -1
- package/src/components/ui/tu-do/boards/board-public-link-section.tsx +231 -0
- package/src/components/ui/tu-do/boards/board-share-dialog.tsx +222 -109
- package/src/components/ui/tu-do/boards/boardId/board-column-external-retry.test.tsx +127 -0
- package/src/components/ui/tu-do/boards/boardId/board-column.tsx +113 -46
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-clear-delete.ts +2 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-move.ts +5 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/bulk/bulk-mutations-updates.ts +3 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/data/kanban-deadline-query.ts +50 -2
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/__tests__/column-reorder.test.ts +17 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/column-reorder.ts +4 -1
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-cache.ts +51 -9
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-drag-order.ts +2 -8
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/task-sort-key.ts +47 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.test.ts +63 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/dnd/use-kanban-dnd.ts +127 -38
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/__tests__/kanban-planner-island.test.tsx +380 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/kanban-planner-dialog.tsx +204 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-digest-panel.tsx +61 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-item-strip.tsx +54 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-plan-toolbar.tsx +251 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-scope-badge.tsx +27 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-section.tsx +58 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-share-dialog.tsx +238 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-target-controls.tsx +143 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/planner-utils.ts +65 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/planner/use-kanban-planner-state.ts +234 -0
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.test.tsx +410 -4
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-columns.tsx +106 -14
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-deadline-panels.tsx +443 -19
- package/src/components/ui/tu-do/boards/boardId/kanban/rendering/kanban-skeleton.tsx +94 -32
- package/src/components/ui/tu-do/boards/boardId/kanban.tsx +213 -106
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.test.tsx +186 -0
- package/src/components/ui/tu-do/boards/boardId/task-board-server-page.tsx +59 -2
- package/src/components/ui/tu-do/boards/boardId/task-card/measured-task-card.tsx +3 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card-comparator.ts +3 -0
- package/src/components/ui/tu-do/boards/boardId/task-card/task-card.tsx +191 -28
- package/src/components/ui/tu-do/boards/boardId/task-filter.test.tsx +152 -0
- package/src/components/ui/tu-do/boards/boardId/task-filter.tsx +555 -545
- package/src/components/ui/tu-do/boards/boardId/task-list.tsx +7 -0
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-display.ts +9 -0
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-grid.tsx +8 -16
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-task-row.tsx +5 -25
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.test.ts +36 -1
- package/src/components/ui/tu-do/boards/boardId/timeline/timeline-utils.ts +51 -2
- package/src/components/ui/tu-do/boards/share-section.tsx +100 -0
- package/src/components/ui/tu-do/boards/workspace-projects-client-page.tsx +13 -3
- package/src/components/ui/tu-do/drafts/draft-convert-dialog.tsx +10 -12
- package/src/components/ui/tu-do/drafts/drafts-page.tsx +33 -16
- package/src/components/ui/tu-do/initiatives/task-initiatives-client.tsx +56 -88
- package/src/components/ui/tu-do/my-tasks/my-tasks-content.tsx +26 -2
- package/src/components/ui/tu-do/my-tasks/use-my-tasks-state.ts +55 -8
- package/src/components/ui/tu-do/notes/note-edit-dialog.tsx +1 -4
- package/src/components/ui/tu-do/shared/__tests__/board-client.test.tsx +25 -0
- package/src/components/ui/tu-do/shared/__tests__/board-header.test.tsx +341 -38
- package/src/components/ui/tu-do/shared/__tests__/board-switcher.test.tsx +253 -0
- package/src/components/ui/tu-do/shared/__tests__/board-views.test.tsx +237 -3
- package/src/components/ui/tu-do/shared/__tests__/task-board-loading-state.test.tsx +17 -0
- package/src/components/ui/tu-do/shared/__tests__/task-legacy-route-recovery.test.tsx +16 -0
- package/src/components/ui/tu-do/shared/board-client.tsx +2 -7
- package/src/components/ui/tu-do/shared/board-config-storage.ts +7 -1
- package/src/components/ui/tu-do/shared/board-header.tsx +465 -937
- package/src/components/ui/tu-do/shared/board-layout-settings.tsx +165 -136
- package/src/components/ui/tu-do/shared/board-switcher.tsx +209 -217
- package/src/components/ui/tu-do/shared/board-views.tsx +596 -82
- package/src/components/ui/tu-do/shared/cursor-overlay-multi-wrapper.tsx +53 -12
- package/src/components/ui/tu-do/shared/list-view.tsx +227 -1
- package/src/components/ui/tu-do/shared/recycle-bin-panel.tsx +142 -94
- package/src/components/ui/tu-do/shared/special-task-list-pins.ts +51 -0
- package/src/components/ui/tu-do/shared/task-board-loading-state.tsx +28 -0
- package/src/components/ui/tu-do/shared/task-dialog-presentation.test.ts +53 -0
- package/src/components/ui/tu-do/shared/task-dialog-presentation.ts +19 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.test.tsx +57 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/compact-task-create-popover.tsx +136 -111
- package/src/components/ui/tu-do/shared/task-edit-dialog/components/task-description-editor.tsx +3 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/field-diff-viewer.tsx +3 -2
- package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.test.tsx +91 -0
- package/src/components/ui/tu-do/shared/task-edit-dialog/selective-revert-panel.tsx +123 -78
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-activity-section.tsx +7 -1
- package/src/components/ui/tu-do/shared/task-edit-dialog/task-snapshot-dialog.tsx +8 -3
- package/src/components/ui/tu-do/shared/task-edit-dialog.tsx +44 -15
- package/src/components/ui/tu-do/shared/task-legacy-route-recovery.tsx +2 -9
- package/src/declarations.d.ts +1 -0
- package/src/hooks/__tests__/use-calendar-readonly.test.tsx +322 -2
- package/src/hooks/__tests__/use-calendar-sync.test.tsx +446 -0
- package/src/hooks/__tests__/useBoardRealtime.test.tsx +2 -2
- package/src/hooks/__tests__/useCursorTracking.test.tsx +212 -0
- package/src/hooks/use-calendar-sync.tsx +247 -243
- package/src/hooks/use-calendar.tsx +323 -138
- package/src/hooks/use-task-actions.ts +24 -0
- package/src/hooks/use-user-workspace-config.ts +75 -0
- package/src/hooks/use-workspace-currency.ts +8 -3
- package/src/hooks/useBoardRealtime.ts +6 -3
- package/src/hooks/useBoardRealtime.types.ts +11 -0
- package/src/hooks/useBoardRealtimeEventHandler.ts +11 -0
- package/src/hooks/useCursorTracking.ts +91 -27
- package/src/hooks/useTaskUserRealtime.ts +5 -3
|
@@ -1,52 +1,58 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
InventoryListingVariant,
|
|
2
3
|
InventoryStorefront,
|
|
3
4
|
InventoryStorefrontListing,
|
|
4
5
|
} from '@tuturuuu/internal-api/inventory';
|
|
6
|
+
import { formatMoneyFromMinor } from '@tuturuuu/utils/money';
|
|
5
7
|
import type { CSSProperties } from 'react';
|
|
6
8
|
|
|
9
|
+
// The storefront now ships a single, unified design language. The merchant
|
|
10
|
+
// preset fields (cornerStyle/surfaceStyle/themePreset/layoutStyle) are retained
|
|
11
|
+
// in the data model for backwards compatibility, but every value resolves to the
|
|
12
|
+
// same refined look so the experience is consistent across all storefronts.
|
|
13
|
+
|
|
14
|
+
/** One soft, modern corner radius for every surface. */
|
|
15
|
+
export const STOREFRONT_RADIUS = 'rounded-2xl';
|
|
16
|
+
|
|
7
17
|
export const storefrontRadiusClasses: Record<
|
|
8
18
|
InventoryStorefront['cornerStyle'],
|
|
9
19
|
string
|
|
10
20
|
> = {
|
|
11
|
-
compact:
|
|
12
|
-
rounded:
|
|
13
|
-
soft:
|
|
21
|
+
compact: STOREFRONT_RADIUS,
|
|
22
|
+
rounded: STOREFRONT_RADIUS,
|
|
23
|
+
soft: STOREFRONT_RADIUS,
|
|
14
24
|
};
|
|
15
25
|
|
|
26
|
+
/** One elevated card surface for every storefront. */
|
|
27
|
+
export const STOREFRONT_SURFACE =
|
|
28
|
+
'border border-border/60 bg-card shadow-sm shadow-foreground/5';
|
|
29
|
+
|
|
16
30
|
export const storefrontSurfaceClasses: Record<
|
|
17
31
|
InventoryStorefront['surfaceStyle'],
|
|
18
32
|
string
|
|
19
33
|
> = {
|
|
20
|
-
glass:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
solid: 'border border-border bg-card shadow-sm shadow-foreground/5',
|
|
34
|
+
glass: STOREFRONT_SURFACE,
|
|
35
|
+
soft: STOREFRONT_SURFACE,
|
|
36
|
+
solid: STOREFRONT_SURFACE,
|
|
24
37
|
};
|
|
25
38
|
|
|
26
|
-
/**
|
|
27
|
-
* Theme presets change the storefront's typographic personality so the choice
|
|
28
|
-
* is actually visible to shoppers. Applied at the surface root and inherited by
|
|
29
|
-
* headings/body inside.
|
|
30
|
-
*/
|
|
39
|
+
/** Unified typography for the whole storefront. */
|
|
31
40
|
export const storefrontThemeClasses: Record<
|
|
32
41
|
InventoryStorefront['themePreset'],
|
|
33
42
|
string
|
|
34
43
|
> = {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
// Refined boutique look: airy, wide-tracked uppercase headings.
|
|
39
|
-
boutique:
|
|
40
|
-
'font-sans [&_h1]:uppercase [&_h1]:tracking-[0.12em] [&_h2]:tracking-wide',
|
|
41
|
-
// Dense, scannable product-catalog density.
|
|
42
|
-
catalog: 'font-sans text-[0.95rem] [&_h1]:tracking-tight',
|
|
43
|
-
// Clean default.
|
|
44
|
+
editorial: 'font-sans',
|
|
45
|
+
boutique: 'font-sans',
|
|
46
|
+
catalog: 'font-sans',
|
|
44
47
|
minimal: 'font-sans',
|
|
45
48
|
};
|
|
46
49
|
|
|
47
50
|
export type StorefrontAccentStyle = CSSProperties & {
|
|
48
51
|
'--storefront-accent'?: string;
|
|
52
|
+
'--storefront-accent-border'?: string;
|
|
49
53
|
'--storefront-accent-foreground'?: string;
|
|
54
|
+
'--storefront-accent-soft'?: string;
|
|
55
|
+
'--storefront-accent-text'?: string;
|
|
50
56
|
};
|
|
51
57
|
|
|
52
58
|
export function sanitizeStorefrontAccentColor(value?: string | null) {
|
|
@@ -65,6 +71,19 @@ export function sanitizeStorefrontAccentColor(value?: string | null) {
|
|
|
65
71
|
return null;
|
|
66
72
|
}
|
|
67
73
|
|
|
74
|
+
export function getSafeStorefrontHttpUrl(value?: string | null) {
|
|
75
|
+
const normalized = value?.trim();
|
|
76
|
+
if (!normalized) return null;
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const url = new URL(normalized);
|
|
80
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') return null;
|
|
81
|
+
return url.toString();
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
68
87
|
export function getStorefrontListingLimit(listing: InventoryStorefrontListing) {
|
|
69
88
|
const available =
|
|
70
89
|
typeof listing.availableQuantity === 'number'
|
|
@@ -74,12 +93,80 @@ export function getStorefrontListingLimit(listing: InventoryStorefrontListing) {
|
|
|
74
93
|
return Math.max(0, Math.min(listing.maxPerOrder, available));
|
|
75
94
|
}
|
|
76
95
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
96
|
+
/** Active, selectable variants for a listing, in display order. */
|
|
97
|
+
export function getStorefrontListingVariants(
|
|
98
|
+
listing: InventoryStorefrontListing
|
|
99
|
+
): InventoryListingVariant[] {
|
|
100
|
+
return (listing.variants ?? []).filter(
|
|
101
|
+
(variant) => variant.status === 'active'
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function listingHasVariants(listing: InventoryStorefrontListing) {
|
|
106
|
+
return getStorefrontListingVariants(listing).length > 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Per-order limit for a specific variant, capped by the listing's maxPerOrder. */
|
|
110
|
+
export function getStorefrontVariantLimit(
|
|
111
|
+
listing: InventoryStorefrontListing,
|
|
112
|
+
variant: InventoryListingVariant
|
|
113
|
+
) {
|
|
114
|
+
const available =
|
|
115
|
+
typeof variant.availableQuantity === 'number'
|
|
116
|
+
? variant.availableQuantity
|
|
117
|
+
: Number.POSITIVE_INFINITY;
|
|
118
|
+
|
|
119
|
+
return Math.max(0, Math.min(listing.maxPerOrder, available));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Resolves the price to charge for a cart line: the variant's resolved price
|
|
124
|
+
* when a variant is selected, otherwise the listing price. Both are minor units.
|
|
125
|
+
*/
|
|
126
|
+
export function getStorefrontLinePrice(
|
|
127
|
+
listing: InventoryStorefrontListing,
|
|
128
|
+
variant?: InventoryListingVariant | null
|
|
129
|
+
) {
|
|
130
|
+
return variant ? variant.price : listing.price;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Lowest active-variant price, for a "from {price}" label on variant listings. */
|
|
134
|
+
export function getStorefrontListingFromPrice(
|
|
135
|
+
listing: InventoryStorefrontListing
|
|
136
|
+
) {
|
|
137
|
+
const variants = getStorefrontListingVariants(listing);
|
|
138
|
+
if (variants.length === 0) return listing.price;
|
|
139
|
+
return variants.reduce(
|
|
140
|
+
(min, variant) => Math.min(min, variant.price),
|
|
141
|
+
Number.POSITIVE_INFINITY
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Stable identity for a cart line so listing+variant combos stay distinct. */
|
|
146
|
+
export function storefrontCartLineKey(
|
|
147
|
+
listingId: string,
|
|
148
|
+
variantId?: string | null
|
|
149
|
+
) {
|
|
150
|
+
return `${listingId}::${variantId ?? ''}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Composes a human label for the selected variant from its option values. */
|
|
154
|
+
export function getStorefrontVariantLabel(
|
|
155
|
+
variant: InventoryListingVariant
|
|
156
|
+
): string | null {
|
|
157
|
+
if (variant.title) return variant.title;
|
|
158
|
+
const labels = variant.optionValues.map((value) => value.label);
|
|
159
|
+
if (labels.length > 0) return labels.join(' / ');
|
|
160
|
+
return variant.sku ?? null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Format a storefront price. Listing/bundle prices and cart totals are stored
|
|
165
|
+
* in integer minor units (cents for USD), so convert through the shared money
|
|
166
|
+
* helper, which also applies the currency's correct decimal precision.
|
|
167
|
+
*/
|
|
168
|
+
export function formatStorefrontPrice(minorValue: number, currency: string) {
|
|
169
|
+
return formatMoneyFromMinor(minorValue, currency);
|
|
83
170
|
}
|
|
84
171
|
|
|
85
172
|
export function getListingInitials(title: string) {
|
|
@@ -98,7 +185,10 @@ export function getAccentStyle(
|
|
|
98
185
|
|
|
99
186
|
return {
|
|
100
187
|
'--storefront-accent': accentColor,
|
|
188
|
+
'--storefront-accent-border': `color-mix(in oklab, ${accentColor} 42%, var(--border))`,
|
|
101
189
|
'--storefront-accent-foreground': getAccentForeground(accentColor),
|
|
190
|
+
'--storefront-accent-soft': `color-mix(in oklab, ${accentColor} 14%, var(--background))`,
|
|
191
|
+
'--storefront-accent-text': `color-mix(in oklab, ${accentColor} 68%, var(--foreground))`,
|
|
102
192
|
};
|
|
103
193
|
}
|
|
104
194
|
|
|
@@ -16,6 +16,29 @@ describe('content-migration', () => {
|
|
|
16
16
|
expect(migrateInlineImagesToBlock(content)).toEqual(content);
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
+
it('should return malformed non-array content unchanged', () => {
|
|
20
|
+
const content = {
|
|
21
|
+
type: 'doc',
|
|
22
|
+
content: {},
|
|
23
|
+
} as unknown as JSONContent;
|
|
24
|
+
|
|
25
|
+
expect(migrateInlineImagesToBlock(content)).toEqual(content);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should ignore malformed nested non-array content', () => {
|
|
29
|
+
const content = {
|
|
30
|
+
type: 'doc',
|
|
31
|
+
content: [
|
|
32
|
+
{
|
|
33
|
+
type: 'paragraph',
|
|
34
|
+
content: {},
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
} as unknown as JSONContent;
|
|
38
|
+
|
|
39
|
+
expect(migrateInlineImagesToBlock(content)).toEqual(content);
|
|
40
|
+
});
|
|
41
|
+
|
|
19
42
|
it('should pass through content with no images', () => {
|
|
20
43
|
const content: JSONContent = {
|
|
21
44
|
type: 'doc',
|
|
@@ -662,6 +685,15 @@ describe('content-migration', () => {
|
|
|
662
685
|
expect(needsMigration(content)).toBe(false);
|
|
663
686
|
});
|
|
664
687
|
|
|
688
|
+
it('should return false for malformed non-array content', () => {
|
|
689
|
+
const content = {
|
|
690
|
+
type: 'doc',
|
|
691
|
+
content: {},
|
|
692
|
+
} as unknown as JSONContent;
|
|
693
|
+
|
|
694
|
+
expect(needsMigration(content)).toBe(false);
|
|
695
|
+
});
|
|
696
|
+
|
|
665
697
|
it('should return false for content with no images', () => {
|
|
666
698
|
const content: JSONContent = {
|
|
667
699
|
type: 'doc',
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { generateHTML, generateJSON } from '@tiptap/core';
|
|
1
2
|
import type SupabaseProvider from '@tuturuuu/ui/hooks/supabase-provider';
|
|
2
3
|
import { describe, expect, it } from 'vitest';
|
|
3
4
|
import * as Y from 'yjs';
|
|
@@ -34,6 +35,128 @@ describe('text editor extensions', () => {
|
|
|
34
35
|
expect(names).toContain('collaborationCaret');
|
|
35
36
|
});
|
|
36
37
|
|
|
38
|
+
it('registers theme-aware highlight, text color, and background color extensions', () => {
|
|
39
|
+
const names = extensionNames({});
|
|
40
|
+
|
|
41
|
+
expect(names).toContain('highlight');
|
|
42
|
+
expect(names).toContain('textStyle');
|
|
43
|
+
expect(names).toContain('color');
|
|
44
|
+
expect(names).toContain('backgroundColor');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('round-trips theme-aware highlight, text color, and background color marks', () => {
|
|
48
|
+
const extensions = getEditorExtensions();
|
|
49
|
+
const content = {
|
|
50
|
+
type: 'doc',
|
|
51
|
+
content: [
|
|
52
|
+
{
|
|
53
|
+
type: 'paragraph',
|
|
54
|
+
attrs: { textAlign: null },
|
|
55
|
+
content: [
|
|
56
|
+
{
|
|
57
|
+
type: 'text',
|
|
58
|
+
marks: [
|
|
59
|
+
{
|
|
60
|
+
type: 'highlight',
|
|
61
|
+
attrs: {
|
|
62
|
+
color: 'var(--calendar-bg-yellow)',
|
|
63
|
+
textColor: 'var(--yellow)',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
text: 'Highlighted',
|
|
68
|
+
},
|
|
69
|
+
{ type: 'text', text: ' ' },
|
|
70
|
+
{
|
|
71
|
+
type: 'text',
|
|
72
|
+
marks: [
|
|
73
|
+
{
|
|
74
|
+
type: 'textStyle',
|
|
75
|
+
attrs: {
|
|
76
|
+
color: 'var(--blue)',
|
|
77
|
+
backgroundColor: 'var(--calendar-bg-blue)',
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
text: 'Colored',
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const html = generateHTML(content, extensions);
|
|
89
|
+
|
|
90
|
+
expect(html).toContain('var(--calendar-bg-yellow)');
|
|
91
|
+
expect(html).toContain('var(--yellow)');
|
|
92
|
+
expect(html).toContain('var(--calendar-bg-blue)');
|
|
93
|
+
expect(html).toContain('var(--blue)');
|
|
94
|
+
|
|
95
|
+
const parsed = generateJSON(html, extensions);
|
|
96
|
+
const paragraph = parsed.content?.[0];
|
|
97
|
+
const highlightedText = paragraph?.content?.[0];
|
|
98
|
+
const coloredText = paragraph?.content?.[2];
|
|
99
|
+
|
|
100
|
+
expect(highlightedText?.marks).toEqual(
|
|
101
|
+
expect.arrayContaining([
|
|
102
|
+
expect.objectContaining({
|
|
103
|
+
type: 'highlight',
|
|
104
|
+
attrs: expect.objectContaining({
|
|
105
|
+
color: 'var(--calendar-bg-yellow)',
|
|
106
|
+
textColor: 'var(--yellow)',
|
|
107
|
+
}),
|
|
108
|
+
}),
|
|
109
|
+
])
|
|
110
|
+
);
|
|
111
|
+
expect(coloredText?.marks).toEqual(
|
|
112
|
+
expect.arrayContaining([
|
|
113
|
+
expect.objectContaining({
|
|
114
|
+
type: 'textStyle',
|
|
115
|
+
attrs: expect.objectContaining({
|
|
116
|
+
color: 'var(--blue)',
|
|
117
|
+
backgroundColor: 'var(--calendar-bg-blue)',
|
|
118
|
+
}),
|
|
119
|
+
}),
|
|
120
|
+
])
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('keeps legacy and raw hex highlight content valid', () => {
|
|
125
|
+
const html = generateHTML(
|
|
126
|
+
{
|
|
127
|
+
type: 'doc',
|
|
128
|
+
content: [
|
|
129
|
+
{
|
|
130
|
+
type: 'paragraph',
|
|
131
|
+
attrs: { textAlign: null },
|
|
132
|
+
content: [
|
|
133
|
+
{
|
|
134
|
+
type: 'text',
|
|
135
|
+
marks: [{ type: 'highlight' }],
|
|
136
|
+
text: 'Legacy',
|
|
137
|
+
},
|
|
138
|
+
{ type: 'text', text: ' ' },
|
|
139
|
+
{
|
|
140
|
+
type: 'text',
|
|
141
|
+
marks: [
|
|
142
|
+
{
|
|
143
|
+
type: 'highlight',
|
|
144
|
+
attrs: { color: '#FFF59D' },
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
text: 'Hex',
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
getEditorExtensions()
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
expect(html).toContain('<mark');
|
|
157
|
+
expect(html).toContain('#FFF59D');
|
|
158
|
+
});
|
|
159
|
+
|
|
37
160
|
it('round-trips task mention workspace metadata through HTML attrs', () => {
|
|
38
161
|
const renderOutput = (Mention.config as any).renderHTML({
|
|
39
162
|
HTMLAttributes: {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type EditorState, NodeSelection, type Plugin } from '@tiptap/pm/state';
|
|
2
|
+
import { toast } from '@tuturuuu/ui/sonner';
|
|
2
3
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
4
|
import { __imageExtensionPrivate, CustomImage } from '../image-extension';
|
|
4
5
|
import { MAX_IMAGE_SIZE, MAX_VIDEO_SIZE } from '../media-utils';
|
|
@@ -16,7 +17,7 @@ vi.mock('../media-utils', async () => {
|
|
|
16
17
|
});
|
|
17
18
|
|
|
18
19
|
// Mock the sonner toast
|
|
19
|
-
vi.mock('
|
|
20
|
+
vi.mock('@tuturuuu/ui/sonner', () => ({
|
|
20
21
|
toast: {
|
|
21
22
|
error: vi.fn(),
|
|
22
23
|
success: vi.fn(),
|
|
@@ -113,6 +114,27 @@ describe('ImageExtension', () => {
|
|
|
113
114
|
|
|
114
115
|
expect(extension).toBeDefined();
|
|
115
116
|
});
|
|
117
|
+
|
|
118
|
+
it('should let a delegated getter clear the configured image upload handler', () => {
|
|
119
|
+
const staleUpload = vi.fn().mockResolvedValue('stale-url');
|
|
120
|
+
|
|
121
|
+
expect(
|
|
122
|
+
__imageExtensionPrivate.resolveUploadHandler({
|
|
123
|
+
configuredHandler: staleUpload,
|
|
124
|
+
delegatedGetter: () => undefined,
|
|
125
|
+
})
|
|
126
|
+
).toBeUndefined();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should fall back to the configured upload handler only without a delegated getter', () => {
|
|
130
|
+
const upload = vi.fn().mockResolvedValue('url');
|
|
131
|
+
|
|
132
|
+
expect(
|
|
133
|
+
__imageExtensionPrivate.resolveUploadHandler({
|
|
134
|
+
configuredHandler: upload,
|
|
135
|
+
})
|
|
136
|
+
).toBe(upload);
|
|
137
|
+
});
|
|
116
138
|
});
|
|
117
139
|
|
|
118
140
|
describe('size presets', () => {
|
|
@@ -300,6 +322,52 @@ describe('ImageExtension', () => {
|
|
|
300
322
|
);
|
|
301
323
|
expect(result).toBe(false);
|
|
302
324
|
});
|
|
325
|
+
|
|
326
|
+
it('should block pasted images when delegated upload permission is cleared', () => {
|
|
327
|
+
const staleUpload = vi.fn().mockResolvedValue('stale-url');
|
|
328
|
+
const extension = CustomImage({
|
|
329
|
+
onImageUpload: staleUpload,
|
|
330
|
+
getOnImageUpload: () => undefined,
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const plugins = extension.config.addProseMirrorPlugins();
|
|
334
|
+
const pastePlugin = plugins.find(
|
|
335
|
+
(p: Plugin) => p.props?.handleDOMEvents?.paste !== undefined
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
const imageFile = new File(['image'], 'image.png', {
|
|
339
|
+
type: 'image/png',
|
|
340
|
+
});
|
|
341
|
+
const mockView = {
|
|
342
|
+
state: { selection: { from: 0, to: 0 } },
|
|
343
|
+
dispatch: vi.fn(),
|
|
344
|
+
dom: document.createElement('div'),
|
|
345
|
+
};
|
|
346
|
+
const mockEvent = {
|
|
347
|
+
clipboardData: {
|
|
348
|
+
items: [
|
|
349
|
+
{
|
|
350
|
+
type: 'image/png',
|
|
351
|
+
getAsFile: () => imageFile,
|
|
352
|
+
},
|
|
353
|
+
],
|
|
354
|
+
},
|
|
355
|
+
preventDefault: vi.fn(),
|
|
356
|
+
} as unknown as ClipboardEvent;
|
|
357
|
+
|
|
358
|
+
const result = pastePlugin?.props?.handleDOMEvents?.paste(
|
|
359
|
+
mockView as any,
|
|
360
|
+
mockEvent
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
expect(result).toBe(true);
|
|
364
|
+
expect(mockEvent.preventDefault).toHaveBeenCalled();
|
|
365
|
+
expect(staleUpload).not.toHaveBeenCalled();
|
|
366
|
+
expect(toast.error).toHaveBeenCalledWith('Insufficient permissions', {
|
|
367
|
+
description:
|
|
368
|
+
'You do not have permission to upload images in this editor.',
|
|
369
|
+
});
|
|
370
|
+
});
|
|
303
371
|
});
|
|
304
372
|
|
|
305
373
|
describe('drop handler behavior', () => {
|
|
@@ -53,6 +53,16 @@ describe('VideoExtension', () => {
|
|
|
53
53
|
const extension = Video();
|
|
54
54
|
expect(extension).toBeDefined();
|
|
55
55
|
});
|
|
56
|
+
|
|
57
|
+
it('should accept a delegated upload getter', () => {
|
|
58
|
+
const mockUpload = vi.fn().mockResolvedValue('url');
|
|
59
|
+
const extension = Video({
|
|
60
|
+
onVideoUpload: vi.fn().mockResolvedValue('stale-url'),
|
|
61
|
+
getOnVideoUpload: () => mockUpload,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(extension).toBeDefined();
|
|
65
|
+
});
|
|
56
66
|
});
|
|
57
67
|
|
|
58
68
|
describe('node configuration', () => {
|
|
@@ -251,6 +261,43 @@ describe('VideoExtension', () => {
|
|
|
251
261
|
expect(result).toBe(false);
|
|
252
262
|
});
|
|
253
263
|
|
|
264
|
+
it('should return false when delegated video upload permission is cleared', () => {
|
|
265
|
+
const staleUpload = vi.fn().mockResolvedValue('stale-url');
|
|
266
|
+
const extension = Video({
|
|
267
|
+
onVideoUpload: staleUpload,
|
|
268
|
+
getOnVideoUpload: () => undefined,
|
|
269
|
+
});
|
|
270
|
+
const plugins = (
|
|
271
|
+
extension.config as any
|
|
272
|
+
).addProseMirrorPlugins() as any[];
|
|
273
|
+
const pastePlugin = plugins[1];
|
|
274
|
+
|
|
275
|
+
const mockView = {
|
|
276
|
+
state: { selection: { from: 0, to: 0 }, tr: {} },
|
|
277
|
+
dispatch: vi.fn(),
|
|
278
|
+
};
|
|
279
|
+
const mockEvent = {
|
|
280
|
+
clipboardData: {
|
|
281
|
+
items: [
|
|
282
|
+
{
|
|
283
|
+
type: 'video/mp4',
|
|
284
|
+
getAsFile: () => new File(['video'], 'video.mp4'),
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
},
|
|
288
|
+
preventDefault: vi.fn(),
|
|
289
|
+
} as unknown as ClipboardEvent;
|
|
290
|
+
|
|
291
|
+
const result = pastePlugin.props?.handleDOMEvents?.paste(
|
|
292
|
+
mockView as any,
|
|
293
|
+
mockEvent
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
expect(result).toBe(false);
|
|
297
|
+
expect(mockEvent.preventDefault).not.toHaveBeenCalled();
|
|
298
|
+
expect(staleUpload).not.toHaveBeenCalled();
|
|
299
|
+
});
|
|
300
|
+
|
|
254
301
|
it('should return false when clipboard has no items', () => {
|
|
255
302
|
const mockUpload = vi.fn().mockResolvedValue('url');
|
|
256
303
|
const extension = Video({ onVideoUpload: mockUpload });
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Extension } from '@tiptap/core';
|
|
2
|
+
|
|
3
|
+
export interface BackgroundColorOptions {
|
|
4
|
+
types: string[];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
declare module '@tiptap/core' {
|
|
8
|
+
interface Commands<ReturnType> {
|
|
9
|
+
backgroundColor: {
|
|
10
|
+
setBackgroundColor: (color: string) => ReturnType;
|
|
11
|
+
unsetBackgroundColor: () => ReturnType;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const BackgroundColor = Extension.create<BackgroundColorOptions>({
|
|
17
|
+
name: 'backgroundColor',
|
|
18
|
+
|
|
19
|
+
addOptions() {
|
|
20
|
+
return {
|
|
21
|
+
types: ['textStyle'],
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
addGlobalAttributes() {
|
|
26
|
+
return [
|
|
27
|
+
{
|
|
28
|
+
types: this.options.types,
|
|
29
|
+
attributes: {
|
|
30
|
+
backgroundColor: {
|
|
31
|
+
default: null,
|
|
32
|
+
parseHTML: (element) =>
|
|
33
|
+
element.style.getPropertyValue('background-color') || null,
|
|
34
|
+
renderHTML: (attributes) => {
|
|
35
|
+
if (!attributes.backgroundColor) return {};
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
style: `background-color: ${attributes.backgroundColor}`,
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
addCommands() {
|
|
48
|
+
return {
|
|
49
|
+
setBackgroundColor:
|
|
50
|
+
(color) =>
|
|
51
|
+
({ chain }) =>
|
|
52
|
+
chain().setMark('textStyle', { backgroundColor: color }).run(),
|
|
53
|
+
unsetBackgroundColor:
|
|
54
|
+
() =>
|
|
55
|
+
({ chain }) =>
|
|
56
|
+
chain()
|
|
57
|
+
.setMark('textStyle', { backgroundColor: null })
|
|
58
|
+
.removeEmptyTextStyle()
|
|
59
|
+
.run(),
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
});
|