@wealthx/shadcn 1.5.1 → 1.5.2

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.
Files changed (36) hide show
  1. package/.turbo/turbo-build.log +118 -118
  2. package/CHANGELOG.md +6 -0
  3. package/dist/chunk-G2EWIP2N.mjs +960 -0
  4. package/dist/{chunk-MHHA7QGO.mjs → chunk-ODO6BUOF.mjs} +1 -1
  5. package/dist/chunk-PX4M67XQ.mjs +301 -0
  6. package/dist/{chunk-FYUSF5KO.mjs → chunk-QRVEI6J3.mjs} +1 -1
  7. package/dist/{chunk-42NEC57Y.mjs → chunk-RAKBWNQH.mjs} +272 -3
  8. package/dist/components/ui/{contact-alert-dialog.js → contact-alert-dialog/index.js} +1029 -593
  9. package/dist/components/ui/contact-alert-dialog/index.mjs +31 -0
  10. package/dist/components/ui/file-preview-dialog.js +407 -100
  11. package/dist/components/ui/file-preview-dialog.mjs +3 -1
  12. package/dist/components/ui/kanban-column.js +408 -113
  13. package/dist/components/ui/kanban-column.mjs +3 -2
  14. package/dist/components/ui/opportunity-card.js +383 -88
  15. package/dist/components/ui/opportunity-card.mjs +2 -1
  16. package/dist/components/ui/pipeline-board.js +424 -129
  17. package/dist/components/ui/pipeline-board.mjs +4 -3
  18. package/dist/index.js +3081 -2282
  19. package/dist/index.mjs +39 -35
  20. package/dist/styles.css +1 -1
  21. package/package.json +5 -4
  22. package/src/components/index.tsx +3 -2
  23. package/src/components/ui/contact-alert-dialog/builder-ui.tsx +556 -0
  24. package/src/components/ui/contact-alert-dialog/config.ts +262 -0
  25. package/src/components/ui/contact-alert-dialog/contact-alert-dialog.tsx +214 -0
  26. package/src/components/ui/contact-alert-dialog/index.tsx +15 -0
  27. package/src/components/ui/contact-alert-dialog/types.ts +61 -0
  28. package/src/components/ui/contact-alert-dialog/utils.ts +93 -0
  29. package/src/components/ui/file-preview-dialog.tsx +299 -99
  30. package/src/components/ui/opportunity-card.tsx +328 -1
  31. package/src/styles/styles-css.ts +1 -1
  32. package/tsup.config.ts +1 -1
  33. package/dist/chunk-5WMFKQZ6.mjs +0 -180
  34. package/dist/chunk-Y24TXIFJ.mjs +0 -518
  35. package/dist/components/ui/contact-alert-dialog.mjs +0 -27
  36. package/src/components/ui/contact-alert-dialog.tsx +0 -710
@@ -1,710 +0,0 @@
1
- import * as React from "react";
2
- import { PlusIcon, Trash2Icon } from "lucide-react";
3
- import {
4
- Dialog,
5
- DialogContent,
6
- DialogHeader,
7
- DialogTitle,
8
- DialogFooter,
9
- } from "@/components/ui/dialog";
10
- import { Button } from "@/components/ui/button";
11
- import { Input } from "@/components/ui/input";
12
- import {
13
- InputGroup,
14
- InputGroupAddon,
15
- InputGroupInput,
16
- InputGroupText,
17
- } from "@/components/ui/input-group";
18
- import { Checkbox } from "@/components/ui/checkbox";
19
- import { Label } from "@/components/ui/label";
20
- import { Field, FieldLabel } from "@/components/ui/field";
21
- import {
22
- Select,
23
- SelectContent,
24
- SelectItem,
25
- SelectTrigger,
26
- SelectValue,
27
- } from "@/components/ui/select";
28
- import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
29
- import { cn } from "@/lib/utils";
30
-
31
- // ---------------------------------------------------------------------------
32
- // Types
33
- // ---------------------------------------------------------------------------
34
-
35
- export type AlertQueryCombinator = "AND" | "OR";
36
-
37
- export type AlertQueryOperator =
38
- | "equal"
39
- | "not_equal"
40
- | "less"
41
- | "less_or_equal"
42
- | "greater"
43
- | "greater_or_equal"
44
- | "between";
45
-
46
- export type AlertQueryFieldType = "number" | "boolean";
47
-
48
- export type ContactAlertSeverity = "INSIGHT" | "WATCH" | "NEED_ACTION";
49
-
50
- export interface AlertQueryField {
51
- key: string;
52
- label: string;
53
- type: AlertQueryFieldType;
54
- /** "dollar" = prefix "$", "percent" = suffix "%" */
55
- unit?: "dollar" | "percent";
56
- /** Allowed operators (defaults to all numeric operators). */
57
- operators?: AlertQueryOperator[];
58
- }
59
-
60
- export interface AlertQueryRule {
61
- id: string;
62
- field: string;
63
- operator: AlertQueryOperator;
64
- value: string;
65
- /** Second value used by the "between" operator. */
66
- value2?: string;
67
- }
68
-
69
- export interface AlertQueryGroup {
70
- rules: AlertQueryRule[];
71
- /**
72
- * Combinator connecting rule[i] to rule[i+1].
73
- * Length is always rules.length - 1.
74
- */
75
- logicalOps: AlertQueryCombinator[];
76
- }
77
-
78
- export interface ContactAlertQueryBuilderProps {
79
- value: AlertQueryGroup;
80
- onChange: (value: AlertQueryGroup) => void;
81
- fields: AlertQueryField[];
82
- className?: string;
83
- }
84
-
85
- export interface ContactAlertDialogProps {
86
- open: boolean;
87
- onOpenChange: (open: boolean) => void;
88
- mode?: "create" | "edit";
89
- initialName?: string;
90
- initialSeverity?: ContactAlertSeverity;
91
- initialQuery?: AlertQueryGroup;
92
- /** Show "Share across company" checkbox for company admins. */
93
- isCompanyAdmin?: boolean;
94
- initialShareAcrossCompany?: boolean;
95
- onSave: (data: {
96
- name: string;
97
- severity: ContactAlertSeverity;
98
- query: AlertQueryGroup;
99
- shareAcrossCompany: boolean;
100
- }) => void;
101
- isLoading?: boolean;
102
- className?: string;
103
- }
104
-
105
- // ---------------------------------------------------------------------------
106
- // Constants
107
- // ---------------------------------------------------------------------------
108
-
109
- export const ALERT_QUERY_FIELDS: AlertQueryField[] = [
110
- {
111
- key: "userMetric.max_loan_amount",
112
- label: "Borrowing Capacity",
113
- type: "number",
114
- unit: "dollar",
115
- operators: [
116
- "equal",
117
- "less",
118
- "less_or_equal",
119
- "greater",
120
- "greater_or_equal",
121
- "between",
122
- ],
123
- },
124
- {
125
- key: "userMetric.debt_outstanding",
126
- label: "Outstanding Debt",
127
- type: "number",
128
- unit: "dollar",
129
- operators: [
130
- "equal",
131
- "less",
132
- "less_or_equal",
133
- "greater",
134
- "greater_or_equal",
135
- ],
136
- },
137
- {
138
- key: "userMetric.lvr",
139
- label: "Current LVR",
140
- type: "number",
141
- unit: "percent",
142
- operators: [
143
- "equal",
144
- "less",
145
- "less_or_equal",
146
- "greater",
147
- "greater_or_equal",
148
- ],
149
- },
150
- {
151
- key: "userMetric.has_met_buying_goal",
152
- label: "Has Met Buying Goal",
153
- type: "boolean",
154
- },
155
- {
156
- key: "userMetric.excess_monthly_surplus",
157
- label: "Excess Monthly Surplus",
158
- type: "number",
159
- unit: "dollar",
160
- operators: [
161
- "equal",
162
- "less",
163
- "less_or_equal",
164
- "greater",
165
- "greater_or_equal",
166
- ],
167
- },
168
- {
169
- key: "userMetric.equity",
170
- label: "Equity Amount",
171
- type: "number",
172
- unit: "dollar",
173
- operators: [
174
- "equal",
175
- "less",
176
- "less_or_equal",
177
- "greater",
178
- "greater_or_equal",
179
- ],
180
- },
181
- {
182
- key: "userMetric.max_debt_interest_rate",
183
- label: "Max Debt Interest Rate",
184
- type: "number",
185
- unit: "percent",
186
- operators: [
187
- "equal",
188
- "less",
189
- "less_or_equal",
190
- "greater",
191
- "greater_or_equal",
192
- ],
193
- },
194
- {
195
- key: "userMetric.min_debt_interest_rate",
196
- label: "Min Debt Interest Rate",
197
- type: "number",
198
- unit: "percent",
199
- operators: [
200
- "equal",
201
- "less",
202
- "less_or_equal",
203
- "greater",
204
- "greater_or_equal",
205
- ],
206
- },
207
- ];
208
-
209
- const ALL_NUMERIC_OPERATORS: AlertQueryOperator[] = [
210
- "equal",
211
- "not_equal",
212
- "less",
213
- "less_or_equal",
214
- "greater",
215
- "greater_or_equal",
216
- "between",
217
- ];
218
-
219
- const BOOLEAN_OPERATORS: AlertQueryOperator[] = ["equal"];
220
-
221
- const OPERATOR_LABELS: Record<AlertQueryOperator, string> = {
222
- equal: "=",
223
- not_equal: "≠",
224
- less: "<",
225
- less_or_equal: "≤",
226
- greater: ">",
227
- greater_or_equal: "≥",
228
- between: "between",
229
- };
230
-
231
- const SEVERITY_LABELS: Record<ContactAlertSeverity, string> = {
232
- INSIGHT: "Insight",
233
- WATCH: "Watch",
234
- NEED_ACTION: "Need Action",
235
- };
236
-
237
- const EMPTY_RULE = (): AlertQueryRule => ({
238
- id: Math.random().toString(36).slice(2),
239
- field: ALERT_QUERY_FIELDS[0].key,
240
- operator: "greater_or_equal",
241
- value: "",
242
- value2: "",
243
- });
244
-
245
- // ---------------------------------------------------------------------------
246
- // Helpers
247
- // ---------------------------------------------------------------------------
248
-
249
- /** Longest label in an array of strings. */
250
- function longestOf(labels: string[]): string {
251
- return labels.reduce((a, b) => (a.length >= b.length ? a : b), "");
252
- }
253
-
254
- /** Operators available across all fields — used to lock the operator-select width. */
255
- function allOperatorLabels(fields: AlertQueryField[]): string[] {
256
- const ops = new Set<AlertQueryOperator>();
257
- for (const f of fields) {
258
- const fieldOps =
259
- f.type === "boolean"
260
- ? BOOLEAN_OPERATORS
261
- : (f.operators ?? ALL_NUMERIC_OPERATORS);
262
- fieldOps.forEach((op) => ops.add(op));
263
- }
264
- return [...ops].map((op) => OPERATOR_LABELS[op]);
265
- }
266
-
267
- /** Add thousand separators to a raw digit string. */
268
- function formatWithCommas(raw: string): string {
269
- if (!raw) return "";
270
- const [int, dec] = raw.split(".");
271
- const formatted = int.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
272
- return dec !== undefined ? `${formatted}.${dec}` : formatted;
273
- }
274
-
275
- /** Strip thousand separators; keep digits and at most one decimal point. */
276
- function parseCommas(display: string): string {
277
- return display.replace(/[^\d.]/g, "").replace(/(\..*)\./g, "$1");
278
- }
279
-
280
- // ---------------------------------------------------------------------------
281
- // SelectAutoWidth
282
- // ---------------------------------------------------------------------------
283
-
284
- /**
285
- * Locks a Select trigger to the width of its longest option.
286
- *
287
- * Ghost block + absolute overlay pattern:
288
- * - An invisible <span> with longestLabel sizes the container (no circular CSS dependency).
289
- * - The trigger sits in an absolute overlay that fills the container.
290
- *
291
- * Ghost padding mirrors trigger chrome:
292
- * left = px-3 (12 px)
293
- * right = gap-2 (8 px) + chevron size-4 (16 px) + px-3 (12 px) = 36 px → pr-10 + 4 px buffer
294
- * text-body-medium matches the SelectTrigger font (1 rem / 16 px).
295
- * h-8 matches trigger size="sm".
296
- */
297
- function SelectAutoWidth({
298
- longestLabel,
299
- children,
300
- }: {
301
- longestLabel: string;
302
- children: React.ReactNode;
303
- }) {
304
- return (
305
- <div className="relative inline-block shrink-0">
306
- <span
307
- aria-hidden
308
- className="invisible block h-8 whitespace-nowrap pl-3 pr-10 text-body-medium"
309
- >
310
- {longestLabel}
311
- </span>
312
- <div className="absolute inset-0">{children}</div>
313
- </div>
314
- );
315
- }
316
-
317
- // ---------------------------------------------------------------------------
318
- // ValueInput
319
- // ---------------------------------------------------------------------------
320
-
321
- function ValueInput({
322
- value,
323
- onChange,
324
- unit,
325
- placeholder = "0",
326
- }: {
327
- value: string;
328
- onChange: (v: string) => void;
329
- unit?: "dollar" | "percent";
330
- placeholder?: string;
331
- }) {
332
- return (
333
- <InputGroup className="w-36">
334
- {unit === "dollar" && (
335
- <InputGroupAddon align="inline-start">
336
- <InputGroupText>$</InputGroupText>
337
- </InputGroupAddon>
338
- )}
339
- <InputGroupInput
340
- type="text"
341
- inputMode="numeric"
342
- value={formatWithCommas(value)}
343
- onChange={(e) => onChange(parseCommas(e.target.value))}
344
- placeholder={placeholder}
345
- />
346
- {unit === "percent" && (
347
- <InputGroupAddon align="inline-end">
348
- <InputGroupText>%</InputGroupText>
349
- </InputGroupAddon>
350
- )}
351
- </InputGroup>
352
- );
353
- }
354
-
355
- // ---------------------------------------------------------------------------
356
- // QueryRuleRow
357
- // ---------------------------------------------------------------------------
358
-
359
- function QueryRuleRow({
360
- rule,
361
- fields,
362
- longestFieldLabel,
363
- longestOperatorLabel,
364
- onUpdate,
365
- onRemove,
366
- removable,
367
- }: {
368
- rule: AlertQueryRule;
369
- fields: AlertQueryField[];
370
- longestFieldLabel: string;
371
- longestOperatorLabel: string;
372
- onUpdate: (updated: AlertQueryRule) => void;
373
- onRemove: () => void;
374
- removable: boolean;
375
- }) {
376
- const fieldDef = fields.find((f) => f.key === rule.field) ?? fields[0];
377
- const isBooleanField = fieldDef.type === "boolean";
378
- const availableOperators = isBooleanField
379
- ? BOOLEAN_OPERATORS
380
- : (fieldDef.operators ?? ALL_NUMERIC_OPERATORS);
381
-
382
- const update = (patch: Partial<AlertQueryRule>) =>
383
- onUpdate({ ...rule, ...patch });
384
-
385
- function handleFieldChange(key: string) {
386
- const newField = fields.find((f) => f.key === key);
387
- const newOps =
388
- newField?.type === "boolean"
389
- ? BOOLEAN_OPERATORS
390
- : (newField?.operators ?? ALL_NUMERIC_OPERATORS);
391
- const validOp = newOps.includes(rule.operator) ? rule.operator : newOps[0];
392
- update({ field: key, operator: validOp, value: "", value2: "" });
393
- }
394
-
395
- return (
396
- <div className="flex flex-wrap items-center gap-1.5 border border-border bg-muted/20 p-2">
397
- {/* Field */}
398
- <SelectAutoWidth longestLabel={longestFieldLabel}>
399
- <Select value={rule.field} onValueChange={handleFieldChange}>
400
- <SelectTrigger size="sm" className="w-full">
401
- <SelectValue>
402
- {fields.find((f) => f.key === rule.field)?.label ?? rule.field}
403
- </SelectValue>
404
- </SelectTrigger>
405
- <SelectContent>
406
- {fields.map((f) => (
407
- <SelectItem key={f.key} value={f.key}>
408
- {f.label}
409
- </SelectItem>
410
- ))}
411
- </SelectContent>
412
- </Select>
413
- </SelectAutoWidth>
414
-
415
- {/* Operator */}
416
- <SelectAutoWidth longestLabel={longestOperatorLabel}>
417
- <Select
418
- value={rule.operator}
419
- onValueChange={(v) => update({ operator: v as AlertQueryOperator })}
420
- >
421
- <SelectTrigger size="sm" className="w-full">
422
- <SelectValue>{OPERATOR_LABELS[rule.operator]}</SelectValue>
423
- </SelectTrigger>
424
- <SelectContent>
425
- {availableOperators.map((op) => (
426
- <SelectItem key={op} value={op}>
427
- {OPERATOR_LABELS[op]}
428
- </SelectItem>
429
- ))}
430
- </SelectContent>
431
- </Select>
432
- </SelectAutoWidth>
433
-
434
- {/* Value */}
435
- {isBooleanField ? (
436
- <Select
437
- value={rule.value || "true"}
438
- onValueChange={(v) => update({ value: v })}
439
- >
440
- <SelectTrigger size="sm" className="w-36">
441
- <SelectValue>
442
- {(rule.value || "true") === "false" ? "No" : "Yes"}
443
- </SelectValue>
444
- </SelectTrigger>
445
- <SelectContent>
446
- <SelectItem value="true">Yes</SelectItem>
447
- <SelectItem value="false">No</SelectItem>
448
- </SelectContent>
449
- </Select>
450
- ) : (
451
- <>
452
- <ValueInput
453
- value={rule.value}
454
- onChange={(v) => update({ value: v })}
455
- unit={fieldDef.unit}
456
- />
457
- {rule.operator === "between" && (
458
- <>
459
- <span className="text-xs text-muted-foreground">and</span>
460
- <ValueInput
461
- value={rule.value2 ?? ""}
462
- onChange={(v) => update({ value2: v })}
463
- unit={fieldDef.unit}
464
- />
465
- </>
466
- )}
467
- </>
468
- )}
469
-
470
- {/* Remove */}
471
- <Button
472
- type="button"
473
- variant="ghost"
474
- size="sm"
475
- onClick={onRemove}
476
- disabled={!removable}
477
- aria-label="Remove rule"
478
- className="ml-auto"
479
- >
480
- <Trash2Icon />
481
- </Button>
482
- </div>
483
- );
484
- }
485
-
486
- // ---------------------------------------------------------------------------
487
- // ContactAlertQueryBuilder
488
- // ---------------------------------------------------------------------------
489
-
490
- export function ContactAlertQueryBuilder({
491
- value,
492
- onChange,
493
- fields,
494
- className,
495
- }: ContactAlertQueryBuilderProps) {
496
- const longestFieldLabel = React.useMemo(
497
- () => longestOf(fields.map((f) => f.label)),
498
- [fields],
499
- );
500
-
501
- const longestOperatorLabel = React.useMemo(
502
- () => longestOf(allOperatorLabels(fields)),
503
- [fields],
504
- );
505
-
506
- function updateRule(index: number, updated: AlertQueryRule) {
507
- onChange({
508
- ...value,
509
- rules: value.rules.map((r, i) => (i === index ? updated : r)),
510
- });
511
- }
512
-
513
- function removeRule(index: number) {
514
- if (value.rules.length <= 1) return;
515
- const rules = value.rules.filter((_, i) => i !== index);
516
- const opIdx = Math.min(index, value.logicalOps.length - 1);
517
- const logicalOps = value.logicalOps.filter((_, i) => i !== opIdx);
518
- onChange({ rules, logicalOps });
519
- }
520
-
521
- function addRule() {
522
- onChange({
523
- rules: [...value.rules, EMPTY_RULE()],
524
- logicalOps: [...value.logicalOps, "AND"],
525
- });
526
- }
527
-
528
- function setLogicalOp(index: number, op: AlertQueryCombinator) {
529
- onChange({
530
- ...value,
531
- logicalOps: value.logicalOps.map((o, i) => (i === index ? op : o)),
532
- });
533
- }
534
-
535
- return (
536
- <div className={cn("flex flex-col gap-1.5", className)}>
537
- {value.rules.map((rule, idx) => (
538
- <React.Fragment key={rule.id}>
539
- <QueryRuleRow
540
- rule={rule}
541
- fields={fields}
542
- longestFieldLabel={longestFieldLabel}
543
- longestOperatorLabel={longestOperatorLabel}
544
- onUpdate={(updated) => updateRule(idx, updated)}
545
- onRemove={() => removeRule(idx)}
546
- removable={value.rules.length > 1}
547
- />
548
- {idx < value.rules.length - 1 && (
549
- <div className="flex items-center gap-2 pl-1">
550
- <ToggleGroup
551
- type="single"
552
- variant="outline"
553
- size="sm"
554
- value={value.logicalOps[idx] ?? "AND"}
555
- onValueChange={(v) =>
556
- v && setLogicalOp(idx, v as AlertQueryCombinator)
557
- }
558
- >
559
- <ToggleGroupItem value="AND">AND</ToggleGroupItem>
560
- <ToggleGroupItem value="OR">OR</ToggleGroupItem>
561
- </ToggleGroup>
562
- </div>
563
- )}
564
- </React.Fragment>
565
- ))}
566
-
567
- <Button
568
- type="button"
569
- variant="ghost"
570
- size="sm"
571
- onClick={addRule}
572
- className="w-fit"
573
- >
574
- <PlusIcon />
575
- Add rule
576
- </Button>
577
- </div>
578
- );
579
- }
580
-
581
- // ---------------------------------------------------------------------------
582
- // ContactAlertDialog
583
- // ---------------------------------------------------------------------------
584
-
585
- export function ContactAlertDialog({
586
- open,
587
- onOpenChange,
588
- mode = "create",
589
- initialName = "",
590
- initialSeverity = "NEED_ACTION",
591
- initialQuery,
592
- isCompanyAdmin = false,
593
- initialShareAcrossCompany = false,
594
- onSave,
595
- isLoading = false,
596
- className,
597
- }: ContactAlertDialogProps) {
598
- const [name, setName] = React.useState(initialName);
599
- const [severity, setSeverity] =
600
- React.useState<ContactAlertSeverity>(initialSeverity);
601
- const [query, setQuery] = React.useState<AlertQueryGroup>(
602
- initialQuery ?? { rules: [EMPTY_RULE()], logicalOps: [] },
603
- );
604
- const [shareAcrossCompany, setShareAcrossCompany] = React.useState(
605
- initialShareAcrossCompany,
606
- );
607
-
608
- React.useEffect(() => {
609
- if (open) {
610
- setName(initialName);
611
- setSeverity(initialSeverity);
612
- setQuery(initialQuery ?? { rules: [EMPTY_RULE()], logicalOps: [] });
613
- setShareAcrossCompany(initialShareAcrossCompany);
614
- }
615
- }, [open]);
616
-
617
- const hasValidRule = query.rules.some((r) => {
618
- const fieldDef = ALERT_QUERY_FIELDS.find((f) => f.key === r.field);
619
- return fieldDef?.type === "boolean" || r.value !== "";
620
- });
621
- const canSave = name.trim().length > 0 && hasValidRule && !isLoading;
622
-
623
- function handleSave() {
624
- if (!canSave) return;
625
- onSave({ name: name.trim(), severity, query, shareAcrossCompany });
626
- }
627
-
628
- return (
629
- <Dialog open={open} onOpenChange={isLoading ? undefined : onOpenChange}>
630
- <DialogContent size="lg" className={className}>
631
- <DialogHeader>
632
- <DialogTitle>
633
- {mode === "edit" ? "Update Alert" : "Create Alert"}
634
- </DialogTitle>
635
- </DialogHeader>
636
-
637
- {/* Severity */}
638
- <div className="flex flex-col gap-1.5">
639
- <p className="text-sm font-medium text-foreground">
640
- Alert trigger severity
641
- </p>
642
- <ToggleGroup
643
- type="single"
644
- variant="outline"
645
- size="sm"
646
- value={severity}
647
- onValueChange={(v) => v && setSeverity(v as ContactAlertSeverity)}
648
- >
649
- {(
650
- ["NEED_ACTION", "WATCH", "INSIGHT"] as ContactAlertSeverity[]
651
- ).map((s) => (
652
- <ToggleGroupItem key={s} value={s}>
653
- {SEVERITY_LABELS[s]}
654
- </ToggleGroupItem>
655
- ))}
656
- </ToggleGroup>
657
- </div>
658
-
659
- {/* Name */}
660
- <Field>
661
- <FieldLabel>Alert name</FieldLabel>
662
- <Input
663
- value={name}
664
- onChange={(e) => setName(e.target.value)}
665
- placeholder="e.g. High equity opportunity"
666
- />
667
- </Field>
668
-
669
- {/* Share across company */}
670
- {isCompanyAdmin && (
671
- <div className="flex items-center gap-2">
672
- <Checkbox
673
- id="alert-share"
674
- checked={shareAcrossCompany}
675
- onCheckedChange={(v) => setShareAcrossCompany(!!v)}
676
- />
677
- <Label htmlFor="alert-share" className="cursor-pointer font-normal">
678
- Share across company
679
- </Label>
680
- </div>
681
- )}
682
-
683
- {/* Query builder */}
684
- <div className="flex flex-col gap-1.5">
685
- <p className="text-sm font-medium text-foreground">
686
- Filter conditions
687
- </p>
688
- <ContactAlertQueryBuilder
689
- value={query}
690
- onChange={setQuery}
691
- fields={ALERT_QUERY_FIELDS}
692
- />
693
- </div>
694
-
695
- <DialogFooter>
696
- <Button
697
- variant="outline"
698
- onClick={() => onOpenChange(false)}
699
- disabled={isLoading}
700
- >
701
- Cancel
702
- </Button>
703
- <Button disabled={!canSave} onClick={handleSave}>
704
- {mode === "edit" ? "Update Alert" : "Create Alert"}
705
- </Button>
706
- </DialogFooter>
707
- </DialogContent>
708
- </Dialog>
709
- );
710
- }