mautourco-components 0.2.161 → 0.2.162

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.
@@ -3,7 +3,10 @@ interface SelectedValueProps {
3
3
  value: string;
4
4
  className?: string;
5
5
  iconSize?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
6
+ /** Chip size - controls padding and gap (Figma: chip/spacing/lg = 16px, 4px, 4px) */
6
7
  size?: 'xs' | 'sm' | 'md' | 'lg';
8
+ /** Text size - defaults to same as size. Use 'md' for Figma body-md (16px, leading-24) */
9
+ textSize?: 'xs' | 'sm' | 'md' | 'lg';
7
10
  variant?: 'filled' | 'text';
8
11
  color?: 'accent' | 'neutral';
9
12
  onRemove?: () => void;
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import Icon from '../Icon/Icon';
3
3
  import { Text } from '../Typography/Typography';
4
4
  var SelectedValue = function (_a) {
5
- var value = _a.value, _b = _a.className, className = _b === void 0 ? '' : _b, _c = _a.iconSize, iconSize = _c === void 0 ? 'xs' : _c, _d = _a.size, size = _d === void 0 ? 'md' : _d, _e = _a.variant, variant = _e === void 0 ? 'filled' : _e, _f = _a.color, color = _f === void 0 ? 'accent' : _f, onRemove = _a.onRemove, onSelect = _a.onSelect;
5
+ var value = _a.value, _b = _a.className, className = _b === void 0 ? '' : _b, _c = _a.iconSize, iconSize = _c === void 0 ? 'xs' : _c, _d = _a.size, size = _d === void 0 ? 'md' : _d, textSize = _a.textSize, _e = _a.variant, variant = _e === void 0 ? 'filled' : _e, _f = _a.color, color = _f === void 0 ? 'accent' : _f, onRemove = _a.onRemove, onSelect = _a.onSelect;
6
6
  var handleRemove = function (event) {
7
7
  event.stopPropagation();
8
8
  if (onRemove) {
@@ -10,9 +10,7 @@ var SelectedValue = function (_a) {
10
10
  }
11
11
  };
12
12
  var classes = "selected-value selected-value--".concat(size, " selected-value--").concat(color, " selected-value--").concat(variant, " ").concat(className, " ").concat(onSelect ? 'cursor-pointer' : '').trim();
13
- var getTextSize = function () {
14
- return size; // Utilise directement la taille passée en prop
15
- };
16
- return (_jsxs("div", { className: classes, onClick: onSelect, children: [_jsx(Text, { size: getTextSize(), variant: "medium", leading: "4", className: "selected-value__text", children: value }), variant !== 'text' && onRemove && (_jsx("button", { type: "button", className: "selected-value__remove", onClick: handleRemove, "aria-label": "Remove ".concat(value), children: _jsx(Icon, { name: "close", size: iconSize, className: "selected-value__remove-icon" }) }))] }));
13
+ var effectiveTextSize = textSize !== null && textSize !== void 0 ? textSize : size;
14
+ return (_jsxs("div", { className: classes, onClick: onSelect, children: [_jsx(Text, { size: effectiveTextSize, variant: "medium", leading: "6", className: "selected-value__text", children: value }), variant !== 'text' && onRemove && (_jsx("button", { type: "button", className: "selected-value__remove", onClick: handleRemove, "aria-label": "Remove ".concat(value), children: _jsx(Icon, { name: "close", size: iconSize, className: "selected-value__remove-icon" }) }))] }));
17
15
  };
18
16
  export default SelectedValue;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Do not edit directly, this file was auto-generated.
3
+ */
4
+
5
+ /* ===== DialogSaveQuotation - Tokens from Figma ===== */
6
+ /* Uses CSS variables from tokens.css */
7
+
8
+ .dialog-save-quotation {
9
+ display: flex;
10
+ flex-direction: column;
11
+ gap: var(--spacing-gap-gap-6, 24px);
12
+ }
13
+
14
+ /* Selected quotes section - tags + select all */
15
+ .dialog-save-quotation__quotes-section {
16
+ display: flex;
17
+ padding: var(--spacing-gap-gap-4, 16px) 0 var(--spacing-gap-gap-2, 8px) 0;
18
+ justify-content: space-between;
19
+ align-items: flex-start;
20
+ gap: var(--spacing-gap-gap-4, 16px);
21
+ flex-wrap: wrap;
22
+ }
23
+
24
+ .dialog-save-quotation__quotes-list {
25
+ display: flex;
26
+ flex-wrap: wrap;
27
+ gap: var(--spacing-gap-gap-2, 8px);
28
+ flex: 1;
29
+ min-width: 0;
30
+ }
31
+
32
+ .dialog-save-quotation__select-all {
33
+ flex-shrink: 0;
34
+ }
35
+
36
+ /* All quotations section header */
37
+ .dialog-save-quotation__section-title {
38
+ font-family:
39
+ var(--font-font-family-body), 'Satoshi', 'Inter', 'Segoe UI', system-ui, sans-serif;
40
+ font-size: var(--font-size-text-base, 16px);
41
+ font-weight: var(--font-weight-font-bold, 700);
42
+ line-height: var(--font-leading-leading-md, 24px);
43
+ color: var(--color-text-default);
44
+ margin: 0;
45
+ }
46
+
47
+ /* Form fields grid */
48
+ .dialog-save-quotation__form {
49
+ display: flex;
50
+ flex-direction: column;
51
+ gap: var(--spacing-gap-gap-6, 24px);
52
+ }
53
+
54
+ .dialog-save-quotation__form-row {
55
+ display: grid;
56
+ grid-template-columns: repeat(3, 1fr);
57
+ gap: var(--spacing-gap-gap-4, 16px);
58
+ }
59
+
60
+ .dialog-save-quotation__form-row--single {
61
+ grid-template-columns: 1fr;
62
+ }
63
+
64
+ @media (max-width: 768px) {
65
+ .dialog-save-quotation__form-row {
66
+ grid-template-columns: 1fr;
67
+ }
68
+ }
69
+
70
+ /* Form field */
71
+ .dialog-save-quotation__field {
72
+ display: flex;
73
+ flex-direction: column;
74
+ gap: var(--spacing-gap-gap-2, 8px);
75
+ }
76
+
77
+ .dialog-save-quotation__label {
78
+ font-family:
79
+ var(--font-font-family-body), 'Satoshi', 'Inter', 'Segoe UI', system-ui, sans-serif;
80
+ font-size: var(--font-size-text-sm, 14px);
81
+ font-weight: var(--font-weight-font-medium, 500);
82
+ line-height: var(--font-leading-leading-sm, 20px);
83
+ color: var(--color-text-default);
84
+ }
85
+
86
+ .dialog-save-quotation__label--required::after {
87
+ content: ' *';
88
+ color: var(--color-text-state-error);
89
+ }
90
+
91
+ /* Footer - buttons full width, side by side */
92
+ .dialog-save-quotation__footer {
93
+ display: flex;
94
+ flex-direction: row;
95
+ align-items: stretch;
96
+ gap: var(--spacing-gap-gap-4, 16px);
97
+ padding: var(--spacing-gap-gap-2, 8px) 0 var(--spacing-gap-gap-4, 16px) 0;
98
+ width: 100%;
99
+ }
100
+
101
+ .dialog-save-quotation__footer > * {
102
+ flex: 1;
103
+ min-width: 0;
104
+ }
@@ -0,0 +1,36 @@
1
+ import { SaveQuotationSchema } from './save-quotation-schema';
2
+ import './DialogSaveQuotation.css';
3
+ export interface SaveQuotationQuote {
4
+ id: string;
5
+ name: string;
6
+ }
7
+ export interface DialogSaveQuotationProps {
8
+ /** Whether the dialog is open */
9
+ open: boolean;
10
+ /** Callback to control dialog open state */
11
+ setOpen: (open: boolean) => void;
12
+ /** List of quotations to save (displayed as removable chips) */
13
+ quotations: SaveQuotationQuote[];
14
+ /** Callback when form is submitted */
15
+ onSubmit: (data: {
16
+ selectedQuotationIds: string[];
17
+ formData: SaveQuotationSchema;
18
+ }) => void;
19
+ /** Optional default values for the form */
20
+ defaultValues?: Partial<SaveQuotationSchema>;
21
+ }
22
+ /**
23
+ * DialogSaveQuotation - Modal to save quotations with agent and quotation details.
24
+ * Based on Figma "Save quotation" design with tokens.
25
+ *
26
+ * @example
27
+ * ```tsx
28
+ * <DialogSaveQuotation
29
+ * open={open}
30
+ * setOpen={setOpen}
31
+ * quotations={[{ id: '1', name: 'Quote 1' }]}
32
+ * onSubmit={(data) => console.log(data)}
33
+ * />
34
+ * ```
35
+ */
36
+ export declare function DialogSaveQuotation(props: DialogSaveQuotationProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,97 @@
1
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
2
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
3
+ if (ar || !(i in from)) {
4
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
5
+ ar[i] = from[i];
6
+ }
7
+ }
8
+ return to.concat(ar || Array.prototype.slice.call(from));
9
+ };
10
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
+ import { useEffect, useMemo, useState } from 'react';
12
+ import Button from '../../atoms/Button/Button';
13
+ import Checkbox from '../../atoms/Checkbox/Checkbox';
14
+ import Input from '../../atoms/Inputs/Input/Input';
15
+ import SelectedValue from '../../atoms/SelectedValue/SelectedValue';
16
+ import { DialogBookingConfirm } from '../DialogBookingConfirm';
17
+ import { saveQuotationSchema, } from './save-quotation-schema';
18
+ import './DialogSaveQuotation.css';
19
+ /**
20
+ * DialogSaveQuotation - Modal to save quotations with agent and quotation details.
21
+ * Based on Figma "Save quotation" design with tokens.
22
+ *
23
+ * @example
24
+ * ```tsx
25
+ * <DialogSaveQuotation
26
+ * open={open}
27
+ * setOpen={setOpen}
28
+ * quotations={[{ id: '1', name: 'Quote 1' }]}
29
+ * onSubmit={(data) => console.log(data)}
30
+ * />
31
+ * ```
32
+ */
33
+ export function DialogSaveQuotation(props) {
34
+ var _a, _b, _c, _d;
35
+ var open = props.open, setOpen = props.setOpen, quotations = props.quotations, onSubmit = props.onSubmit, defaultValues = props.defaultValues;
36
+ var _e = useState(quotations), remainingQuotations = _e[0], setRemainingQuotations = _e[1];
37
+ var _f = useState([]), selectedQuotationIds = _f[0], setSelectedQuotationIds = _f[1];
38
+ var _g = useState(false), isSelectAll = _g[0], setIsSelectAll = _g[1];
39
+ var _h = useState((_a = defaultValues === null || defaultValues === void 0 ? void 0 : defaultValues.agentForename) !== null && _a !== void 0 ? _a : ''), agentForename = _h[0], setAgentForename = _h[1];
40
+ var _j = useState((_b = defaultValues === null || defaultValues === void 0 ? void 0 : defaultValues.agentLastname) !== null && _b !== void 0 ? _b : ''), agentLastname = _j[0], setAgentLastname = _j[1];
41
+ var _k = useState((_c = defaultValues === null || defaultValues === void 0 ? void 0 : defaultValues.agency) !== null && _c !== void 0 ? _c : ''), agency = _k[0], setAgency = _k[1];
42
+ var _l = useState((_d = defaultValues === null || defaultValues === void 0 ? void 0 : defaultValues.quotationName) !== null && _d !== void 0 ? _d : ''), quotationName = _l[0], setQuotationName = _l[1];
43
+ var remainingQuotationsFiltered = useMemo(function () {
44
+ return remainingQuotations.filter(function (q) { return selectedQuotationIds.includes(q.id); });
45
+ }, [remainingQuotations, selectedQuotationIds]);
46
+ useEffect(function () {
47
+ setRemainingQuotations(quotations);
48
+ setSelectedQuotationIds(quotations.map(function (q) { return q.id; }));
49
+ }, [quotations]);
50
+ useEffect(function () {
51
+ setIsSelectAll(remainingQuotations.length > 0 &&
52
+ selectedQuotationIds.length === remainingQuotations.length);
53
+ }, [selectedQuotationIds, remainingQuotations]);
54
+ var handleRemoveQuotation = function (id) {
55
+ if (remainingQuotations.length > 1) {
56
+ setRemainingQuotations(function (prev) { return prev.filter(function (q) { return q.id !== id; }); });
57
+ setSelectedQuotationIds(function (prev) { return prev.filter(function (quoteId) { return quoteId !== id; }); });
58
+ }
59
+ };
60
+ var handleSelectQuotation = function (id) {
61
+ if (remainingQuotations.length > 1) {
62
+ setSelectedQuotationIds(function (prev) {
63
+ if (prev.includes(id)) {
64
+ return prev.length > 1 ? prev.filter(function (q) { return q !== id; }) : prev;
65
+ }
66
+ return __spreadArray(__spreadArray([], prev, true), [id], false);
67
+ });
68
+ }
69
+ };
70
+ var handleSelectAll = function () {
71
+ if (isSelectAll) {
72
+ setSelectedQuotationIds(remainingQuotations.length > 1 ? [remainingQuotations[0].id] : []);
73
+ }
74
+ else {
75
+ setSelectedQuotationIds(remainingQuotations.map(function (q) { return q.id; }));
76
+ }
77
+ setIsSelectAll(!isSelectAll);
78
+ };
79
+ var handleSubmit = function () {
80
+ var formData = {
81
+ agentForename: agentForename,
82
+ agentLastname: agentLastname,
83
+ agency: agency,
84
+ quotationName: quotationName,
85
+ };
86
+ var result = saveQuotationSchema.safeParse(formData);
87
+ if (!result.success) {
88
+ return;
89
+ }
90
+ onSubmit({
91
+ selectedQuotationIds: remainingQuotationsFiltered.map(function (q) { return q.id; }),
92
+ formData: result.data,
93
+ });
94
+ setOpen(false);
95
+ };
96
+ return (_jsx(DialogBookingConfirm, { open: open, setOpen: setOpen, title: "Save quotation", className: "!max-w-[800px]", closeOnOverlayClick: true, children: _jsxs("div", { className: "dialog-save-quotation", children: [_jsxs("div", { className: "dialog-save-quotation__quotes-section", children: [_jsx("div", { className: "dialog-save-quotation__quotes-list", children: remainingQuotations.map(function (quote) { return (_jsx(SelectedValue, { value: quote.name, color: selectedQuotationIds.includes(quote.id) ? 'accent' : 'neutral', onSelect: function () { return handleSelectQuotation(quote.id); }, onRemove: function () { return handleRemoveQuotation(quote.id); }, size: "lg", textSize: "md" }, quote.id)); }) }), _jsx("div", { className: "dialog-save-quotation__select-all", children: _jsx(Checkbox, { label: "Select all quotations", checked: isSelectAll || remainingQuotations.length === 1, disabled: remainingQuotations.length === 1, onChange: handleSelectAll, labelPosition: "leading" }) })] }), _jsx("h3", { className: "dialog-save-quotation__section-title", children: "All quotations" }), _jsxs("div", { className: "dialog-save-quotation__form", children: [_jsxs("div", { className: "dialog-save-quotation__form-row", children: [_jsxs("div", { className: "dialog-save-quotation__field", children: [_jsx("label", { htmlFor: "agent-forename", className: "dialog-save-quotation__label dialog-save-quotation__label--required", children: "Agent Forename" }), _jsx(Input, { id: "agent-forename", value: agentForename, onChange: function (e) { return setAgentForename(e.target.value); }, placeholder: "Mautourco" })] }), _jsxs("div", { className: "dialog-save-quotation__field", children: [_jsx("label", { htmlFor: "agent-lastname", className: "dialog-save-quotation__label dialog-save-quotation__label--required", children: "Agent Lastname" }), _jsx(Input, { id: "agent-lastname", value: agentLastname, onChange: function (e) { return setAgentLastname(e.target.value); }, placeholder: "B2B" })] }), _jsxs("div", { className: "dialog-save-quotation__field", children: [_jsx("label", { htmlFor: "agency", className: "dialog-save-quotation__label", children: "Agency" }), _jsx(Input, { id: "agency", value: agency, onChange: function (e) { return setAgency(e.target.value); }, placeholder: "Mautourco Ltd" })] })] }), _jsx("div", { className: "dialog-save-quotation__form-row dialog-save-quotation__form-row--single", children: _jsxs("div", { className: "dialog-save-quotation__field", children: [_jsx("label", { htmlFor: "quotation-name", className: "dialog-save-quotation__label dialog-save-quotation__label--required", children: "Quotation name" }), _jsx(Input, { id: "quotation-name", value: quotationName, onChange: function (e) { return setQuotationName(e.target.value); }, placeholder: "Insert client's name" })] }) })] }), _jsxs("div", { className: "dialog-save-quotation__footer", children: [_jsx(Button, { variant: "outline-secondary", size: "sm", onClick: function () { return setOpen(false); }, children: "Cancel" }), _jsx(Button, { variant: "secondary", size: "sm", onClick: handleSubmit, children: "Save quotation" })] })] }) }));
97
+ }
@@ -0,0 +1,2 @@
1
+ export { DialogSaveQuotation, type DialogSaveQuotationProps, type SaveQuotationQuote, } from './DialogSaveQuotation';
2
+ export type { SaveQuotationSchema } from './save-quotation-schema';
@@ -0,0 +1 @@
1
+ export { DialogSaveQuotation, } from './DialogSaveQuotation';
@@ -0,0 +1,8 @@
1
+ import z from 'zod';
2
+ export declare const saveQuotationSchema: z.ZodObject<{
3
+ agentForename: z.ZodString;
4
+ agentLastname: z.ZodString;
5
+ agency: z.ZodOptional<z.ZodString>;
6
+ quotationName: z.ZodString;
7
+ }, z.core.$strip>;
8
+ export type SaveQuotationSchema = z.infer<typeof saveQuotationSchema>;
@@ -0,0 +1,7 @@
1
+ import z from 'zod';
2
+ export var saveQuotationSchema = z.object({
3
+ agentForename: z.string().min(1, 'Agent forename is required'),
4
+ agentLastname: z.string().min(1, 'Agent lastname is required'),
5
+ agency: z.string().optional(),
6
+ quotationName: z.string().min(1, 'Quotation name is required'),
7
+ });
package/dist/index.d.ts CHANGED
@@ -65,6 +65,7 @@ export * from './components/organisms/DialogDeleteConfirm';
65
65
  export { DialogDeleteConfirm } from './components/organisms/DialogDeleteConfirm/DialogDeleteConfirm';
66
66
  export { DialogQuoteRename } from './components/organisms/DialogQuoteRename/DialogQuoteRename';
67
67
  export * from './components/organisms/DialogSendingMail';
68
+ export * from './components/organisms/DialogSaveQuotation';
68
69
  export { default as Docket } from './components/organisms/Docket/Docket';
69
70
  export { DocketAccordion, type DocketAccordionProps, } from './components/organisms/DocketAccordion';
70
71
  export { Footer } from './components/organisms/Footer/Footer';
package/dist/index.js CHANGED
@@ -67,6 +67,7 @@ export * from './components/organisms/DialogDeleteConfirm';
67
67
  export { DialogDeleteConfirm } from './components/organisms/DialogDeleteConfirm/DialogDeleteConfirm';
68
68
  export { DialogQuoteRename } from './components/organisms/DialogQuoteRename/DialogQuoteRename';
69
69
  export * from './components/organisms/DialogSendingMail';
70
+ export * from './components/organisms/DialogSaveQuotation';
70
71
  export { default as Docket } from './components/organisms/Docket/Docket';
71
72
  export { DocketAccordion, } from './components/organisms/DocketAccordion';
72
73
  export { Footer } from './components/organisms/Footer/Footer';
@@ -4,19 +4,21 @@
4
4
 
5
5
  /* SelectedValue Component Styles */
6
6
 
7
+ /* Base uses sm as default; tokens: chip-spacing-*-* from Figma */
8
+
7
9
  .selected-value {
8
10
  display: inline-flex;
9
11
  align-items: center;
10
- gap: var(--chip-spacing-gap, 0.5rem);
11
- padding: var(--chip-spacing-sm-padding-y, 0.375rem) var(--chip-spacing-sm-padding-x);
12
- border-radius: var(--chip-border-radius-pill, 1.375rem);
12
+ gap: var(--chip-spacing-sm-gap, 4px);
13
+ padding: var(--chip-spacing-sm-padding-y, 4px) var(--chip-spacing-sm-padding-x, 8px);
14
+ border-radius: var(--chip-border-radius-pill, 9999px);
13
15
  /* Typography is now handled by the Text component */
14
16
  cursor: default;
15
17
  -webkit-user-select: none;
16
18
  user-select: none;
17
19
  max-width: 200px;
18
20
  width: auto;
19
- opacity: var(--opacity-opacity-100);
21
+ opacity: var(--opacity-opacity-100, 1);
20
22
  /* Variant: text-only (no background, no padding) */
21
23
  .selected-value--text {
22
24
  background: transparent;
@@ -63,32 +65,36 @@
63
65
  /* Size variants */
64
66
 
65
67
  .selected-value--xs {
66
- padding: var(--chip-spacing-xs-padding-y, 0.25rem)
67
- var(--chip-spacing-xs-padding-x, 0.5rem);
68
- gap: var(--chip-spacing-xs-gap, 0.25rem);
68
+ padding: var(--chip-spacing-xs-padding-y, 2px)
69
+ var(--chip-spacing-xs-padding-x, 6px);
70
+ gap: var(--chip-spacing-xs-gap, 2px);
69
71
  }
70
72
 
71
73
  .selected-value--sm {
72
- padding: var(--chip-spacing-sm-padding-y, 0.375rem)
73
- var(--chip-spacing-sm-padding-x, 0.75rem);
74
- gap: var(--chip-spacing-sm-gap, 0.375rem);
74
+ padding: var(--chip-spacing-sm-padding-y, 4px)
75
+ var(--chip-spacing-sm-padding-x, 8px);
76
+ gap: var(--chip-spacing-sm-gap, 4px);
75
77
  }
76
78
 
77
79
  .selected-value--md {
78
- padding: var(--chip-spacing-md-padding-y, 0.5rem)
79
- var(--chip-spacing-md-padding-x, 0.875rem);
80
- gap: var(--chip-spacing-md-gap, 0.5rem);
80
+ padding: var(--chip-spacing-md-padding-y, 4px)
81
+ var(--chip-spacing-md-padding-x, 12px);
82
+ gap: var(--chip-spacing-md-gap, 4px);
81
83
  }
82
84
 
85
+ /* Figma chip/lg: padding-x 16px, padding-y 4px, gap 4px */
86
+
83
87
  .selected-value--lg {
84
- padding: var(--chip-spacing-lg-padding-y, 0.625rem)
85
- var(--chip-spacing-lg-padding-x, 1rem);
86
- gap: var(--chip-spacing-lg-gap, 0.625rem);
88
+ padding: var(--chip-spacing-lg-padding-y, 4px)
89
+ var(--chip-spacing-lg-padding-x, 16px);
90
+ gap: var(--chip-spacing-lg-gap, 4px);
87
91
  }
88
92
 
93
+ /* Figma chip/color/accent/filled: bg #0f7173, fg #ffffff */
94
+
89
95
  .selected-value--accent {
90
- background: var(--color-atoll-green-800);
91
- color: var(--color-white);
96
+ background: var(--chip-color-accent-filled-background, var(--color-atoll-green-800));
97
+ color: var(--chip-color-accent-filled-foreground, var(--color-white));
92
98
  }
93
99
 
94
100
  .selected-value--neutral {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mautourco-components",
3
- "version": "0.2.161",
3
+ "version": "0.2.162",
4
4
  "private": false,
5
5
  "description": "Bibliothèque de composants Mautourco pour le redesign",
6
6
  "main": "dist/index.js",
@@ -6,7 +6,10 @@ interface SelectedValueProps {
6
6
  value: string;
7
7
  className?: string;
8
8
  iconSize?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
9
+ /** Chip size - controls padding and gap (Figma: chip/spacing/lg = 16px, 4px, 4px) */
9
10
  size?: 'xs' | 'sm' | 'md' | 'lg';
11
+ /** Text size - defaults to same as size. Use 'md' for Figma body-md (16px, leading-24) */
12
+ textSize?: 'xs' | 'sm' | 'md' | 'lg';
10
13
  variant?: 'filled' | 'text';
11
14
  color?: 'accent' | 'neutral';
12
15
  onRemove?: () => void;
@@ -18,6 +21,7 @@ const SelectedValue: React.FC<SelectedValueProps> = ({
18
21
  className = '',
19
22
  iconSize = 'xs',
20
23
  size = 'md',
24
+ textSize,
21
25
  variant = 'filled',
22
26
  color = 'accent',
23
27
  onRemove,
@@ -33,16 +37,14 @@ const SelectedValue: React.FC<SelectedValueProps> = ({
33
37
  const classes =
34
38
  `selected-value selected-value--${size} selected-value--${color} selected-value--${variant} ${className} ${onSelect ? 'cursor-pointer' : ''}`.trim();
35
39
 
36
- const getTextSize = () => {
37
- return size; // Utilise directement la taille passée en prop
38
- };
40
+ const effectiveTextSize = textSize ?? size;
39
41
 
40
42
  return (
41
43
  <div className={classes} onClick={onSelect}>
42
44
  <Text
43
- size={getTextSize() as any}
45
+ size={effectiveTextSize}
44
46
  variant="medium"
45
- leading="4"
47
+ leading="6"
46
48
  className="selected-value__text">
47
49
  {value}
48
50
  </Text>
@@ -0,0 +1,100 @@
1
+ /* ===== DialogSaveQuotation - Tokens from Figma ===== */
2
+ /* Uses CSS variables from tokens.css */
3
+
4
+ .dialog-save-quotation {
5
+ display: flex;
6
+ flex-direction: column;
7
+ gap: var(--spacing-gap-gap-6, 24px);
8
+ }
9
+
10
+ /* Selected quotes section - tags + select all */
11
+ .dialog-save-quotation__quotes-section {
12
+ display: flex;
13
+ padding: var(--spacing-gap-gap-4, 16px) 0 var(--spacing-gap-gap-2, 8px) 0;
14
+ justify-content: space-between;
15
+ align-items: flex-start;
16
+ gap: var(--spacing-gap-gap-4, 16px);
17
+ flex-wrap: wrap;
18
+ }
19
+
20
+ .dialog-save-quotation__quotes-list {
21
+ display: flex;
22
+ flex-wrap: wrap;
23
+ gap: var(--spacing-gap-gap-2, 8px);
24
+ flex: 1;
25
+ min-width: 0;
26
+ }
27
+
28
+ .dialog-save-quotation__select-all {
29
+ flex-shrink: 0;
30
+ }
31
+
32
+ /* All quotations section header */
33
+ .dialog-save-quotation__section-title {
34
+ font-family:
35
+ var(--font-font-family-body), 'Satoshi', 'Inter', 'Segoe UI', system-ui, sans-serif;
36
+ font-size: var(--font-size-text-base, 16px);
37
+ font-weight: var(--font-weight-font-bold, 700);
38
+ line-height: var(--font-leading-leading-md, 24px);
39
+ color: var(--color-text-default);
40
+ margin: 0;
41
+ }
42
+
43
+ /* Form fields grid */
44
+ .dialog-save-quotation__form {
45
+ display: flex;
46
+ flex-direction: column;
47
+ gap: var(--spacing-gap-gap-6, 24px);
48
+ }
49
+
50
+ .dialog-save-quotation__form-row {
51
+ display: grid;
52
+ grid-template-columns: repeat(3, 1fr);
53
+ gap: var(--spacing-gap-gap-4, 16px);
54
+ }
55
+
56
+ .dialog-save-quotation__form-row--single {
57
+ grid-template-columns: 1fr;
58
+ }
59
+
60
+ @media (max-width: 768px) {
61
+ .dialog-save-quotation__form-row {
62
+ grid-template-columns: 1fr;
63
+ }
64
+ }
65
+
66
+ /* Form field */
67
+ .dialog-save-quotation__field {
68
+ display: flex;
69
+ flex-direction: column;
70
+ gap: var(--spacing-gap-gap-2, 8px);
71
+ }
72
+
73
+ .dialog-save-quotation__label {
74
+ font-family:
75
+ var(--font-font-family-body), 'Satoshi', 'Inter', 'Segoe UI', system-ui, sans-serif;
76
+ font-size: var(--font-size-text-sm, 14px);
77
+ font-weight: var(--font-weight-font-medium, 500);
78
+ line-height: var(--font-leading-leading-sm, 20px);
79
+ color: var(--color-text-default);
80
+ }
81
+
82
+ .dialog-save-quotation__label--required::after {
83
+ content: ' *';
84
+ color: var(--color-text-state-error);
85
+ }
86
+
87
+ /* Footer - buttons full width, side by side */
88
+ .dialog-save-quotation__footer {
89
+ display: flex;
90
+ flex-direction: row;
91
+ align-items: stretch;
92
+ gap: var(--spacing-gap-gap-4, 16px);
93
+ padding: var(--spacing-gap-gap-2, 8px) 0 var(--spacing-gap-gap-4, 16px) 0;
94
+ width: 100%;
95
+ }
96
+
97
+ .dialog-save-quotation__footer > * {
98
+ flex: 1;
99
+ min-width: 0;
100
+ }
@@ -0,0 +1,249 @@
1
+ import { useEffect, useMemo, useState } from 'react';
2
+ import Button from '../../atoms/Button/Button';
3
+ import Checkbox from '../../atoms/Checkbox/Checkbox';
4
+ import Input from '../../atoms/Inputs/Input/Input';
5
+ import SelectedValue from '../../atoms/SelectedValue/SelectedValue';
6
+ import { DialogBookingConfirm } from '../DialogBookingConfirm';
7
+ import {
8
+ saveQuotationSchema,
9
+ SaveQuotationSchema,
10
+ } from './save-quotation-schema';
11
+ import './DialogSaveQuotation.css';
12
+
13
+ export interface SaveQuotationQuote {
14
+ id: string;
15
+ name: string;
16
+ }
17
+
18
+ export interface DialogSaveQuotationProps {
19
+ /** Whether the dialog is open */
20
+ open: boolean;
21
+ /** Callback to control dialog open state */
22
+ setOpen: (open: boolean) => void;
23
+ /** List of quotations to save (displayed as removable chips) */
24
+ quotations: SaveQuotationQuote[];
25
+ /** Callback when form is submitted */
26
+ onSubmit: (data: {
27
+ selectedQuotationIds: string[];
28
+ formData: SaveQuotationSchema;
29
+ }) => void;
30
+ /** Optional default values for the form */
31
+ defaultValues?: Partial<SaveQuotationSchema>;
32
+ }
33
+
34
+ /**
35
+ * DialogSaveQuotation - Modal to save quotations with agent and quotation details.
36
+ * Based on Figma "Save quotation" design with tokens.
37
+ *
38
+ * @example
39
+ * ```tsx
40
+ * <DialogSaveQuotation
41
+ * open={open}
42
+ * setOpen={setOpen}
43
+ * quotations={[{ id: '1', name: 'Quote 1' }]}
44
+ * onSubmit={(data) => console.log(data)}
45
+ * />
46
+ * ```
47
+ */
48
+ export function DialogSaveQuotation(props: DialogSaveQuotationProps) {
49
+ const {
50
+ open,
51
+ setOpen,
52
+ quotations,
53
+ onSubmit,
54
+ defaultValues,
55
+ } = props;
56
+
57
+ const [remainingQuotations, setRemainingQuotations] =
58
+ useState<SaveQuotationQuote[]>(quotations);
59
+ const [selectedQuotationIds, setSelectedQuotationIds] = useState<string[]>([]);
60
+ const [isSelectAll, setIsSelectAll] = useState(false);
61
+
62
+ const [agentForename, setAgentForename] = useState(
63
+ defaultValues?.agentForename ?? ''
64
+ );
65
+ const [agentLastname, setAgentLastname] = useState(
66
+ defaultValues?.agentLastname ?? ''
67
+ );
68
+ const [agency, setAgency] = useState(defaultValues?.agency ?? '');
69
+ const [quotationName, setQuotationName] = useState(
70
+ defaultValues?.quotationName ?? ''
71
+ );
72
+
73
+ const remainingQuotationsFiltered = useMemo(
74
+ () =>
75
+ remainingQuotations.filter((q) => selectedQuotationIds.includes(q.id)),
76
+ [remainingQuotations, selectedQuotationIds]
77
+ );
78
+
79
+ useEffect(() => {
80
+ setRemainingQuotations(quotations);
81
+ setSelectedQuotationIds(quotations.map((q) => q.id));
82
+ }, [quotations]);
83
+
84
+ useEffect(() => {
85
+ setIsSelectAll(
86
+ remainingQuotations.length > 0 &&
87
+ selectedQuotationIds.length === remainingQuotations.length
88
+ );
89
+ }, [selectedQuotationIds, remainingQuotations]);
90
+
91
+ const handleRemoveQuotation = (id: string) => {
92
+ if (remainingQuotations.length > 1) {
93
+ setRemainingQuotations((prev) => prev.filter((q) => q.id !== id));
94
+ setSelectedQuotationIds((prev) => prev.filter((quoteId) => quoteId !== id));
95
+ }
96
+ };
97
+
98
+ const handleSelectQuotation = (id: string) => {
99
+ if (remainingQuotations.length > 1) {
100
+ setSelectedQuotationIds((prev) => {
101
+ if (prev.includes(id)) {
102
+ return prev.length > 1 ? prev.filter((q) => q !== id) : prev;
103
+ }
104
+ return [...prev, id];
105
+ });
106
+ }
107
+ };
108
+
109
+ const handleSelectAll = () => {
110
+ if (isSelectAll) {
111
+ setSelectedQuotationIds(remainingQuotations.length > 1 ? [remainingQuotations[0].id] : []);
112
+ } else {
113
+ setSelectedQuotationIds(remainingQuotations.map((q) => q.id));
114
+ }
115
+ setIsSelectAll(!isSelectAll);
116
+ };
117
+
118
+ const handleSubmit = () => {
119
+ const formData: SaveQuotationSchema = {
120
+ agentForename,
121
+ agentLastname,
122
+ agency,
123
+ quotationName,
124
+ };
125
+
126
+ const result = saveQuotationSchema.safeParse(formData);
127
+ if (!result.success) {
128
+ return;
129
+ }
130
+
131
+ onSubmit({
132
+ selectedQuotationIds: remainingQuotationsFiltered.map((q) => q.id),
133
+ formData: result.data,
134
+ });
135
+ setOpen(false);
136
+ };
137
+
138
+ return (
139
+ <DialogBookingConfirm
140
+ open={open}
141
+ setOpen={setOpen}
142
+ title="Save quotation"
143
+ className="!max-w-[800px]"
144
+ closeOnOverlayClick>
145
+ <div className="dialog-save-quotation">
146
+ {/* Quotes section: chips + select all */}
147
+ <div className="dialog-save-quotation__quotes-section">
148
+ <div className="dialog-save-quotation__quotes-list">
149
+ {remainingQuotations.map((quote) => (
150
+ <SelectedValue
151
+ key={quote.id}
152
+ value={quote.name}
153
+ color={selectedQuotationIds.includes(quote.id) ? 'accent' : 'neutral'}
154
+ onSelect={() => handleSelectQuotation(quote.id)}
155
+ onRemove={() => handleRemoveQuotation(quote.id)}
156
+ size="lg"
157
+ textSize="md"
158
+ />
159
+ ))}
160
+ </div>
161
+ <div className="dialog-save-quotation__select-all">
162
+ <Checkbox
163
+ label="Select all quotations"
164
+ checked={isSelectAll || remainingQuotations.length === 1}
165
+ disabled={remainingQuotations.length === 1}
166
+ onChange={handleSelectAll}
167
+ labelPosition="leading"
168
+ />
169
+ </div>
170
+ </div>
171
+
172
+ {/* All quotations form */}
173
+ <h3 className="dialog-save-quotation__section-title">All quotations</h3>
174
+
175
+ <div className="dialog-save-quotation__form">
176
+ <div className="dialog-save-quotation__form-row">
177
+ <div className="dialog-save-quotation__field">
178
+ <label
179
+ htmlFor="agent-forename"
180
+ className="dialog-save-quotation__label dialog-save-quotation__label--required">
181
+ Agent Forename
182
+ </label>
183
+ <Input
184
+ id="agent-forename"
185
+ value={agentForename}
186
+ onChange={(e) => setAgentForename(e.target.value)}
187
+ placeholder="Mautourco"
188
+ />
189
+ </div>
190
+ <div className="dialog-save-quotation__field">
191
+ <label
192
+ htmlFor="agent-lastname"
193
+ className="dialog-save-quotation__label dialog-save-quotation__label--required">
194
+ Agent Lastname
195
+ </label>
196
+ <Input
197
+ id="agent-lastname"
198
+ value={agentLastname}
199
+ onChange={(e) => setAgentLastname(e.target.value)}
200
+ placeholder="B2B"
201
+ />
202
+ </div>
203
+ <div className="dialog-save-quotation__field">
204
+ <label
205
+ htmlFor="agency"
206
+ className="dialog-save-quotation__label">
207
+ Agency
208
+ </label>
209
+ <Input
210
+ id="agency"
211
+ value={agency}
212
+ onChange={(e) => setAgency(e.target.value)}
213
+ placeholder="Mautourco Ltd"
214
+ />
215
+ </div>
216
+ </div>
217
+
218
+ <div className="dialog-save-quotation__form-row dialog-save-quotation__form-row--single">
219
+ <div className="dialog-save-quotation__field">
220
+ <label
221
+ htmlFor="quotation-name"
222
+ className="dialog-save-quotation__label dialog-save-quotation__label--required">
223
+ Quotation name
224
+ </label>
225
+ <Input
226
+ id="quotation-name"
227
+ value={quotationName}
228
+ onChange={(e) => setQuotationName(e.target.value)}
229
+ placeholder="Insert client's name"
230
+ />
231
+ </div>
232
+ </div>
233
+ </div>
234
+
235
+ <div className="dialog-save-quotation__footer">
236
+ <Button
237
+ variant="outline-secondary"
238
+ size="sm"
239
+ onClick={() => setOpen(false)}>
240
+ Cancel
241
+ </Button>
242
+ <Button variant="secondary" size="sm" onClick={handleSubmit}>
243
+ Save quotation
244
+ </Button>
245
+ </div>
246
+ </div>
247
+ </DialogBookingConfirm>
248
+ );
249
+ }
@@ -0,0 +1,6 @@
1
+ export {
2
+ DialogSaveQuotation,
3
+ type DialogSaveQuotationProps,
4
+ type SaveQuotationQuote,
5
+ } from './DialogSaveQuotation';
6
+ export type { SaveQuotationSchema } from './save-quotation-schema';
@@ -0,0 +1,10 @@
1
+ import z from 'zod';
2
+
3
+ export const saveQuotationSchema = z.object({
4
+ agentForename: z.string().min(1, 'Agent forename is required'),
5
+ agentLastname: z.string().min(1, 'Agent lastname is required'),
6
+ agency: z.string().optional(),
7
+ quotationName: z.string().min(1, 'Quotation name is required'),
8
+ });
9
+
10
+ export type SaveQuotationSchema = z.infer<typeof saveQuotationSchema>;
@@ -1,16 +1,17 @@
1
1
  /* SelectedValue Component Styles */
2
+ /* Base uses sm as default; tokens: chip-spacing-*-* from Figma */
2
3
  .selected-value {
3
4
  display: inline-flex;
4
5
  align-items: center;
5
- gap: var(--chip-spacing-gap, 0.5rem);
6
- padding: var(--chip-spacing-sm-padding-y, 0.375rem) var(--chip-spacing-sm-padding-x);
7
- border-radius: var(--chip-border-radius-pill, 1.375rem);
6
+ gap: var(--chip-spacing-sm-gap, 4px);
7
+ padding: var(--chip-spacing-sm-padding-y, 4px) var(--chip-spacing-sm-padding-x, 8px);
8
+ border-radius: var(--chip-border-radius-pill, 9999px);
8
9
  /* Typography is now handled by the Text component */
9
10
  cursor: default;
10
11
  user-select: none;
11
12
  max-width: 200px;
12
13
  width: auto;
13
- opacity: var(--opacity-opacity-100);
14
+ opacity: var(--opacity-opacity-100, 1);
14
15
  /* Variant: text-only (no background, no padding) */
15
16
  .selected-value--text {
16
17
  background: transparent;
@@ -56,32 +57,34 @@
56
57
 
57
58
  /* Size variants */
58
59
  .selected-value--xs {
59
- padding: var(--chip-spacing-xs-padding-y, 0.25rem)
60
- var(--chip-spacing-xs-padding-x, 0.5rem);
61
- gap: var(--chip-spacing-xs-gap, 0.25rem);
60
+ padding: var(--chip-spacing-xs-padding-y, 2px)
61
+ var(--chip-spacing-xs-padding-x, 6px);
62
+ gap: var(--chip-spacing-xs-gap, 2px);
62
63
  }
63
64
 
64
65
  .selected-value--sm {
65
- padding: var(--chip-spacing-sm-padding-y, 0.375rem)
66
- var(--chip-spacing-sm-padding-x, 0.75rem);
67
- gap: var(--chip-spacing-sm-gap, 0.375rem);
66
+ padding: var(--chip-spacing-sm-padding-y, 4px)
67
+ var(--chip-spacing-sm-padding-x, 8px);
68
+ gap: var(--chip-spacing-sm-gap, 4px);
68
69
  }
69
70
 
70
71
  .selected-value--md {
71
- padding: var(--chip-spacing-md-padding-y, 0.5rem)
72
- var(--chip-spacing-md-padding-x, 0.875rem);
73
- gap: var(--chip-spacing-md-gap, 0.5rem);
72
+ padding: var(--chip-spacing-md-padding-y, 4px)
73
+ var(--chip-spacing-md-padding-x, 12px);
74
+ gap: var(--chip-spacing-md-gap, 4px);
74
75
  }
75
76
 
77
+ /* Figma chip/lg: padding-x 16px, padding-y 4px, gap 4px */
76
78
  .selected-value--lg {
77
- padding: var(--chip-spacing-lg-padding-y, 0.625rem)
78
- var(--chip-spacing-lg-padding-x, 1rem);
79
- gap: var(--chip-spacing-lg-gap, 0.625rem);
79
+ padding: var(--chip-spacing-lg-padding-y, 4px)
80
+ var(--chip-spacing-lg-padding-x, 16px);
81
+ gap: var(--chip-spacing-lg-gap, 4px);
80
82
  }
81
83
 
84
+ /* Figma chip/color/accent/filled: bg #0f7173, fg #ffffff */
82
85
  .selected-value--accent {
83
- background: var(--color-atoll-green-800);
84
- color: var(--color-white);
86
+ background: var(--chip-color-accent-filled-background, var(--color-atoll-green-800));
87
+ color: var(--chip-color-accent-filled-foreground, var(--color-white));
85
88
  }
86
89
 
87
90
  .selected-value--neutral {