@twoux/ui 0.0.0-stage → 0.1.1
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/LICENSE.md +61 -0
- package/LICENSES/Apache-2.0.txt +202 -0
- package/NOTICE.md +64 -0
- package/README.md +211 -2
- package/SHADCN-DIFF.md +228 -0
- package/agent/AGENTS.md +358 -0
- package/agent/docs-md/accessibility.md +49 -0
- package/agent/docs-md/action-catalogue.md +81 -0
- package/agent/docs-md/actions.md +74 -0
- package/agent/docs-md/agent-activity-log.md +70 -0
- package/agent/docs-md/agent-permissions.md +82 -0
- package/agent/docs-md/agent-run.md +90 -0
- package/agent/docs-md/agents-on-behalf.md +74 -0
- package/agent/docs-md/ai-column.md +85 -0
- package/agent/docs-md/ai-origin-label.md +70 -0
- package/agent/docs-md/announcing-features.md +73 -0
- package/agent/docs-md/answer-actions.md +81 -0
- package/agent/docs-md/app-shell.md +114 -0
- package/agent/docs-md/artifact-card.md +89 -0
- package/agent/docs-md/ask-on-screen.md +72 -0
- package/agent/docs-md/assistant-page.md +100 -0
- package/agent/docs-md/assistant-panel.md +142 -0
- package/agent/docs-md/avatar.md +82 -0
- package/agent/docs-md/banner.md +106 -0
- package/agent/docs-md/board.md +163 -0
- package/agent/docs-md/breadcrumb.md +89 -0
- package/agent/docs-md/breakdown-table.md +97 -0
- package/agent/docs-md/bulk-action-bar.md +112 -0
- package/agent/docs-md/button.md +293 -0
- package/agent/docs-md/card.md +115 -0
- package/agent/docs-md/change-list.md +99 -0
- package/agent/docs-md/change-request.md +166 -0
- package/agent/docs-md/chart-area.md +87 -0
- package/agent/docs-md/chart-bar.md +100 -0
- package/agent/docs-md/chart-box-plot.md +88 -0
- package/agent/docs-md/chart-bullet.md +89 -0
- package/agent/docs-md/chart-candlestick.md +112 -0
- package/agent/docs-md/chart-card-line.md +120 -0
- package/agent/docs-md/chart-donut.md +90 -0
- package/agent/docs-md/chart-drill-down.md +72 -0
- package/agent/docs-md/chart-funnel.md +71 -0
- package/agent/docs-md/chart-heatmap.md +76 -0
- package/agent/docs-md/chart-histogram.md +81 -0
- package/agent/docs-md/chart-map.md +165 -0
- package/agent/docs-md/chart-mixed.md +88 -0
- package/agent/docs-md/chart-price.md +92 -0
- package/agent/docs-md/chart-radial-bars.md +109 -0
- package/agent/docs-md/chart-radial-lines.md +111 -0
- package/agent/docs-md/chart-sankey.md +104 -0
- package/agent/docs-md/chart-scatter.md +87 -0
- package/agent/docs-md/chart-share-bar.md +89 -0
- package/agent/docs-md/chart-treemap.md +98 -0
- package/agent/docs-md/chart-waterfall.md +93 -0
- package/agent/docs-md/checkbox.md +131 -0
- package/agent/docs-md/choosing-selection-control.md +71 -0
- package/agent/docs-md/citations-sources.md +79 -0
- package/agent/docs-md/citing-sources.md +63 -0
- package/agent/docs-md/clarify-before-acting.md +69 -0
- package/agent/docs-md/clarifying-question.md +92 -0
- package/agent/docs-md/code-block.md +174 -0
- package/agent/docs-md/colours.md +87 -0
- package/agent/docs-md/columns-menu.md +140 -0
- package/agent/docs-md/combobox.md +161 -0
- package/agent/docs-md/command-palette.md +126 -0
- package/agent/docs-md/confidence-note.md +87 -0
- package/agent/docs-md/configurable-dashboard.md +76 -0
- package/agent/docs-md/confirmation-dialog.md +93 -0
- package/agent/docs-md/connected-tools.md +80 -0
- package/agent/docs-md/connector-picker.md +127 -0
- package/agent/docs-md/consistency.md +53 -0
- package/agent/docs-md/content-density.md +61 -0
- package/agent/docs-md/conversation-history.md +78 -0
- package/agent/docs-md/create-record.md +80 -0
- package/agent/docs-md/csv-import.md +88 -0
- package/agent/docs-md/dashboard-items.md +78 -0
- package/agent/docs-md/data-export.md +61 -0
- package/agent/docs-md/data-grid.md +137 -0
- package/agent/docs-md/data-vis-colours.md +76 -0
- package/agent/docs-md/data-visualisation.md +76 -0
- package/agent/docs-md/date-picker.md +178 -0
- package/agent/docs-md/delete-record.md +75 -0
- package/agent/docs-md/dense-table.md +495 -0
- package/agent/docs-md/design-tokens.md +53 -0
- package/agent/docs-md/disabled-read-only.md +75 -0
- package/agent/docs-md/drag-and-drop.md +74 -0
- package/agent/docs-md/dropdown-menu.md +137 -0
- package/agent/docs-md/edit-record.md +73 -0
- package/agent/docs-md/empty-loading-error.md +78 -0
- package/agent/docs-md/empty-state.md +128 -0
- package/agent/docs-md/error-messages.md +75 -0
- package/agent/docs-md/error-state.md +123 -0
- package/agent/docs-md/export-menu.md +86 -0
- package/agent/docs-md/feedback-prompt.md +88 -0
- package/agent/docs-md/field-mapping.md +78 -0
- package/agent/docs-md/file-drop.md +110 -0
- package/agent/docs-md/filter-bar.md +138 -0
- package/agent/docs-md/filter-panel.md +103 -0
- package/agent/docs-md/filtering-search.md +79 -0
- package/agent/docs-md/follow-ups-starters.md +79 -0
- package/agent/docs-md/form-validation.md +95 -0
- package/agent/docs-md/generated-columns.md +65 -0
- package/agent/docs-md/generated-drafts-files.md +71 -0
- package/agent/docs-md/grouped-list.md +89 -0
- package/agent/docs-md/guide-cue.md +170 -0
- package/agent/docs-md/help-panel.md +103 -0
- package/agent/docs-md/help-system.md +74 -0
- package/agent/docs-md/iconography.md +49 -0
- package/agent/docs-md/inbox.md +108 -0
- package/agent/docs-md/inline-editing-validation.md +80 -0
- package/agent/docs-md/inline-input-in-answer.md +70 -0
- package/agent/docs-md/items-palette.md +116 -0
- package/agent/docs-md/keyboard-model.md +47 -0
- package/agent/docs-md/keyboard-navigation.md +68 -0
- package/agent/docs-md/keyboard-shortcut-hint.md +87 -0
- package/agent/docs-md/keyboard-shortcut-sheet.md +90 -0
- package/agent/docs-md/labelling-ai-output.md +66 -0
- package/agent/docs-md/layout.md +47 -0
- package/agent/docs-md/list-table.md +254 -0
- package/agent/docs-md/loading-skeleton.md +91 -0
- package/agent/docs-md/managing-agents.md +71 -0
- package/agent/docs-md/message-composer.md +105 -0
- package/agent/docs-md/migrating.md +49 -0
- package/agent/docs-md/motion.md +93 -0
- package/agent/docs-md/navigation.md +76 -0
- package/agent/docs-md/notification-channels.md +65 -0
- package/agent/docs-md/notifications-decision.md +100 -0
- package/agent/docs-md/onboarding-step.md +114 -0
- package/agent/docs-md/order-book.md +108 -0
- package/agent/docs-md/page-header.md +96 -0
- package/agent/docs-md/pagination.md +88 -0
- package/agent/docs-md/pane-layout.md +105 -0
- package/agent/docs-md/period-comparison.md +66 -0
- package/agent/docs-md/plan-card.md +120 -0
- package/agent/docs-md/popover-rules.md +88 -0
- package/agent/docs-md/prompt-editor.md +90 -0
- package/agent/docs-md/propose-apply.md +65 -0
- package/agent/docs-md/provider-logo.md +97 -0
- package/agent/docs-md/radio.md +83 -0
- package/agent/docs-md/ranked-list.md +102 -0
- package/agent/docs-md/reasoning-progress.md +72 -0
- package/agent/docs-md/record-detail.md +71 -0
- package/agent/docs-md/regenerating-answers.md +70 -0
- package/agent/docs-md/run-status-bar.md +94 -0
- package/agent/docs-md/saved-tasks-schedules.md +63 -0
- package/agent/docs-md/saved-views.md +149 -0
- package/agent/docs-md/saving-views.md +68 -0
- package/agent/docs-md/scheduler.md +258 -0
- package/agent/docs-md/secondary-panels.md +72 -0
- package/agent/docs-md/segmented-control.md +88 -0
- package/agent/docs-md/selection-bulk-actions.md +70 -0
- package/agent/docs-md/separator.md +75 -0
- package/agent/docs-md/setup-checklist.md +92 -0
- package/agent/docs-md/shape-elevation.md +55 -0
- package/agent/docs-md/sheet-grid.md +211 -0
- package/agent/docs-md/side-panel.md +134 -0
- package/agent/docs-md/sign-in.md +114 -0
- package/agent/docs-md/spacing.md +55 -0
- package/agent/docs-md/split-button.md +100 -0
- package/agent/docs-md/stat-compare.md +82 -0
- package/agent/docs-md/static-dashboard.md +75 -0
- package/agent/docs-md/status-badge.md +140 -0
- package/agent/docs-md/step-card.md +75 -0
- package/agent/docs-md/stepper.md +92 -0
- package/agent/docs-md/suggested-changes.md +91 -0
- package/agent/docs-md/switch.md +78 -0
- package/agent/docs-md/table-settings.md +81 -0
- package/agent/docs-md/tabs.md +85 -0
- package/agent/docs-md/tag-input.md +108 -0
- package/agent/docs-md/tag.md +79 -0
- package/agent/docs-md/task-builder.md +87 -0
- package/agent/docs-md/team-instructions.md +72 -0
- package/agent/docs-md/templates-gallery.md +85 -0
- package/agent/docs-md/text-input.md +160 -0
- package/agent/docs-md/thinking-steps.md +89 -0
- package/agent/docs-md/timeline.md +189 -0
- package/agent/docs-md/timestamp.md +84 -0
- package/agent/docs-md/timestamps.md +70 -0
- package/agent/docs-md/toast.md +117 -0
- package/agent/docs-md/tooltip.md +90 -0
- package/agent/docs-md/typography.md +62 -0
- package/agent/docs-md/undo-destructive.md +66 -0
- package/agent/docs-md/unsaved-changes.md +71 -0
- package/agent/docs-md/usage-meter.md +80 -0
- package/agent/docs-md/user-feedback.md +71 -0
- package/agent/docs-md/voice-and-tone.md +60 -0
- package/agent/docs-md/why-choice.md +93 -0
- package/agent/docs-md/workspace-onboarding.md +74 -0
- package/agent/docs-md/writing.md +48 -0
- package/agent/llms-full.txt +18369 -0
- package/agent/llms.txt +198 -0
- package/agent/skills/twoux-audit/SKILL.md +49 -0
- package/agent/skills/twoux-build/SKILL.md +39 -0
- package/agent/skills/twoux-design/SKILL.md +54 -0
- package/agent/skills/twoux-write/SKILL.md +32 -0
- package/bin/check.mjs +123 -0
- package/bin/docs.mjs +24 -0
- package/bin/guard.mjs +27 -0
- package/bin/init.mjs +113 -0
- package/bin/knowledge.mjs +87 -0
- package/bin/mcp.mjs +116 -0
- package/bin/theme.mjs +168 -0
- package/bin/twoux.mjs +25 -0
- package/package.json +80 -3
- package/registry/jobs.json +1028 -0
- package/registry/palette.json +770 -0
- package/registry/rules.json +913 -0
- package/src/components/agent-review.tsx +364 -0
- package/src/components/ai-basics.tsx +212 -0
- package/src/components/ai-work.tsx +304 -0
- package/src/components/ai.tsx +227 -0
- package/src/components/alert-dialog.tsx +203 -0
- package/src/components/alert.tsx +86 -0
- package/src/components/avatar.tsx +114 -0
- package/src/components/badge.tsx +59 -0
- package/src/components/board.tsx +397 -0
- package/src/components/breadcrumb.tsx +114 -0
- package/src/components/button-group.tsx +90 -0
- package/src/components/button.tsx +109 -0
- package/src/components/calendar.tsx +247 -0
- package/src/components/card.tsx +102 -0
- package/src/components/chart-map.tsx +336 -0
- package/src/components/charts-more.tsx +1331 -0
- package/src/components/charts-radial.tsx +328 -0
- package/src/components/charts.tsx +390 -0
- package/src/components/checkbox.tsx +40 -0
- package/src/components/code.tsx +139 -0
- package/src/components/collapsible.tsx +37 -0
- package/src/components/combobox.tsx +339 -0
- package/src/components/command-extras.tsx +11 -0
- package/src/components/command.tsx +196 -0
- package/src/components/data-grid.tsx +213 -0
- package/src/components/data-table.tsx +334 -0
- package/src/components/data-views.tsx +740 -0
- package/src/components/date-picker.tsx +686 -0
- package/src/components/dense-table.tsx +207 -0
- package/src/components/dialog.tsx +162 -0
- package/src/components/dropdown-menu.tsx +264 -0
- package/src/components/empty.tsx +114 -0
- package/src/components/feedback-prompt.tsx +80 -0
- package/src/components/field.tsx +254 -0
- package/src/components/fields.tsx +112 -0
- package/src/components/flow.tsx +840 -0
- package/src/components/guide-cue.tsx +273 -0
- package/src/components/inbox.tsx +113 -0
- package/src/components/input-group.tsx +176 -0
- package/src/components/input.tsx +26 -0
- package/src/components/kbd.tsx +34 -0
- package/src/components/label.tsx +27 -0
- package/src/components/labels.tsx +176 -0
- package/src/components/motion.tsx +136 -0
- package/src/components/onboarding.tsx +454 -0
- package/src/components/order-book.tsx +185 -0
- package/src/components/pane-layout.tsx +417 -0
- package/src/components/popover.tsx +96 -0
- package/src/components/provider-logos.data.ts +49 -0
- package/src/components/radio-group.tsx +52 -0
- package/src/components/scheduler.tsx +997 -0
- package/src/components/segmented-control.tsx +28 -0
- package/src/components/select.tsx +195 -0
- package/src/components/separator.tsx +31 -0
- package/src/components/sheet.tsx +148 -0
- package/src/components/shell.tsx +426 -0
- package/src/components/shortcut-sheet.tsx +83 -0
- package/src/components/skeleton.tsx +18 -0
- package/src/components/sonner.tsx +107 -0
- package/src/components/spinner.tsx +13 -0
- package/src/components/split-button.tsx +58 -0
- package/src/components/stepper.tsx +78 -0
- package/src/components/switch.tsx +43 -0
- package/src/components/table.tsx +126 -0
- package/src/components/tabs.tsx +118 -0
- package/src/components/tag-input.tsx +253 -0
- package/src/components/textarea.tsx +21 -0
- package/src/components/theme.tsx +80 -0
- package/src/components/timeline.tsx +408 -0
- package/src/components/toggle-group.tsx +87 -0
- package/src/components/toggle.tsx +54 -0
- package/src/components/tooltip.tsx +65 -0
- package/src/components/workspace-actions.tsx +294 -0
- package/src/components/workspace-import.tsx +465 -0
- package/src/components/workspace.tsx +1884 -0
- package/src/index.ts +80 -0
- package/src/lib/active-indicator.tsx +53 -0
- package/src/lib/chart-kit.tsx +72 -0
- package/src/lib/cn.ts +6 -0
- package/src/lib/shortcuts.tsx +61 -0
- package/src/lib/workspace.tsx +10 -0
- package/src/shadcn/alert-dialog.tsx +196 -0
- package/src/shadcn/alert.tsx +65 -0
- package/src/shadcn/avatar.tsx +108 -0
- package/src/shadcn/badge.tsx +47 -0
- package/src/shadcn/breadcrumb.tsx +108 -0
- package/src/shadcn/button-group.tsx +83 -0
- package/src/shadcn/button.tsx +63 -0
- package/src/shadcn/calendar.tsx +220 -0
- package/src/shadcn/card.tsx +91 -0
- package/src/shadcn/checkbox.tsx +31 -0
- package/src/shadcn/collapsible.tsx +33 -0
- package/src/shadcn/command.tsx +184 -0
- package/src/shadcn/dialog.tsx +158 -0
- package/src/shadcn/dropdown-menu.tsx +256 -0
- package/src/shadcn/empty.tsx +103 -0
- package/src/shadcn/field.tsx +248 -0
- package/src/shadcn/input-group.tsx +170 -0
- package/src/shadcn/input.tsx +20 -0
- package/src/shadcn/kbd.tsx +28 -0
- package/src/shadcn/label.tsx +23 -0
- package/src/shadcn/popover.tsx +88 -0
- package/src/shadcn/radio-group.tsx +44 -0
- package/src/shadcn/select.tsx +189 -0
- package/src/shadcn/separator.tsx +27 -0
- package/src/shadcn/sheet.tsx +142 -0
- package/src/shadcn/skeleton.tsx +13 -0
- package/src/shadcn/sonner.tsx +40 -0
- package/src/shadcn/switch.tsx +34 -0
- package/src/shadcn/table.tsx +115 -0
- package/src/shadcn/tabs.tsx +90 -0
- package/src/shadcn/textarea.tsx +17 -0
- package/src/shadcn/toggle-group.tsx +83 -0
- package/src/shadcn/toggle.tsx +46 -0
- package/src/shadcn/tooltip.tsx +56 -0
- package/src/styles/palette.css +127 -0
- package/src/styles/theme.css +35 -0
- package/src/styles/twoux.css +236 -0
- package/theme.json +14 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated from shadcn/ui new-york-v4 "tooltip" by scripts/tailor-shadcn.mjs. Do not edit by hand.
|
|
3
|
+
* TwoUX changes:
|
|
4
|
+
* - Short delay so moving across a toolbar does not flash labels
|
|
5
|
+
* - Keeps a gap from the trigger and stays inside the window
|
|
6
|
+
* - Compact padding
|
|
7
|
+
*/
|
|
8
|
+
"use client"
|
|
9
|
+
|
|
10
|
+
import * as React from "react"
|
|
11
|
+
import { cn } from "../lib/cn"
|
|
12
|
+
import { Tooltip as TooltipPrimitive } from "radix-ui"
|
|
13
|
+
|
|
14
|
+
function TooltipProvider({
|
|
15
|
+
delayDuration = 300,
|
|
16
|
+
...props
|
|
17
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
|
18
|
+
return (
|
|
19
|
+
<TooltipPrimitive.Provider
|
|
20
|
+
data-slot="tooltip-provider"
|
|
21
|
+
delayDuration={delayDuration}
|
|
22
|
+
{...props}
|
|
23
|
+
/>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function Tooltip({
|
|
28
|
+
...props
|
|
29
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
|
30
|
+
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function TooltipTrigger({
|
|
34
|
+
...props
|
|
35
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
|
36
|
+
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function TooltipContent({
|
|
40
|
+
className,
|
|
41
|
+
sideOffset = 4,
|
|
42
|
+
collisionPadding = 8,
|
|
43
|
+
children,
|
|
44
|
+
...props
|
|
45
|
+
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
|
46
|
+
return (
|
|
47
|
+
<TooltipPrimitive.Portal>
|
|
48
|
+
<TooltipPrimitive.Content
|
|
49
|
+
data-slot="tooltip-content"
|
|
50
|
+
sideOffset={sideOffset}
|
|
51
|
+
collisionPadding={collisionPadding}
|
|
52
|
+
className={cn(
|
|
53
|
+
"z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in rounded-sm bg-foreground px-2 py-1 text-sm text-balance text-background fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
|
54
|
+
className
|
|
55
|
+
)}
|
|
56
|
+
{...props}
|
|
57
|
+
>
|
|
58
|
+
{children}
|
|
59
|
+
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-xs bg-foreground fill-foreground" />
|
|
60
|
+
</TooltipPrimitive.Content>
|
|
61
|
+
</TooltipPrimitive.Portal>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TwoUX compositions for shaping a sheet.
|
|
3
|
+
* ActionCatalogue: the actions that add a column (look-ups, AI, formulas, exports), searchable and in tabs, each with a
|
|
4
|
+
* one-line description and its cost per row; choosing one opens a short set-up with the estimated total before adding.
|
|
5
|
+
* New columns test on a few rows by default; running on every row is a choice made with the total in view.
|
|
6
|
+
* TableSettings: duplicates, empty rows and sharing as a template, in the side panel, saved with Save. Closing with
|
|
7
|
+
* unsaved changes asks first; nothing asks when nothing changed.
|
|
8
|
+
*/
|
|
9
|
+
import { ArrowLeft, Check, Copy, Search } from "lucide-react"
|
|
10
|
+
import * as React from "react"
|
|
11
|
+
import { toast } from "sonner"
|
|
12
|
+
import { cn } from "../lib/cn"
|
|
13
|
+
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "./alert-dialog"
|
|
14
|
+
import { Button } from "./button"
|
|
15
|
+
import { CheckboxField, RadioField, TextField } from "./fields"
|
|
16
|
+
import { Input } from "./input"
|
|
17
|
+
import { Swap } from "./motion"
|
|
18
|
+
import { ProviderLogo } from "./onboarding"
|
|
19
|
+
import type { ProviderId } from "./provider-logos.data"
|
|
20
|
+
import { RadioGroup } from "./radio-group"
|
|
21
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select"
|
|
22
|
+
import { SidePanel, type PanelMode } from "./shell"
|
|
23
|
+
import { Tabs, TabsList, TabsTrigger } from "./tabs"
|
|
24
|
+
import { formatCredits } from "./workspace"
|
|
25
|
+
|
|
26
|
+
/* ---------------------------------------------------------------- action catalogue */
|
|
27
|
+
|
|
28
|
+
export type CatalogueCategory = "Enrichment" | "AI" | "Formulas" | "Exports"
|
|
29
|
+
export type CatalogueAction = {
|
|
30
|
+
id: string
|
|
31
|
+
name: string
|
|
32
|
+
/** One line: what the new column holds. */
|
|
33
|
+
description: string
|
|
34
|
+
category: CatalogueCategory
|
|
35
|
+
suggested?: boolean
|
|
36
|
+
/** The third-party service it uses; its logo replaces the icon. */
|
|
37
|
+
provider?: ProviderId
|
|
38
|
+
/** A lucide icon for actions that don't use another service. */
|
|
39
|
+
icon?: React.ReactNode
|
|
40
|
+
/** 0 for free. */
|
|
41
|
+
costPerRow: number
|
|
42
|
+
/** Inputs chosen before adding, such as which column holds the company domain. */
|
|
43
|
+
inputs?: { id: string; label: string; options: string[]; defaultValue?: string }[]
|
|
44
|
+
}
|
|
45
|
+
export type ActionRunChoice = "test" | "all" | "none"
|
|
46
|
+
export type ActionConfig = { action: CatalogueAction; columnName: string; inputs: Record<string, string>; run: ActionRunChoice }
|
|
47
|
+
|
|
48
|
+
const TABS = ["Suggested", "Enrichment", "AI", "Formulas", "Exports"] as const
|
|
49
|
+
const perRow = (c: number) => (c ? `≈ ${formatCredits(c)} / row` : "Free")
|
|
50
|
+
|
|
51
|
+
function ActionIcon({ action }: { action: CatalogueAction }) {
|
|
52
|
+
if (action.provider) return <ProviderLogo provider={action.provider} size="md" />
|
|
53
|
+
return <span aria-hidden="true" className="grid size-7 shrink-0 place-items-center rounded-xs border border-line bg-surface-2 text-ink-2 [&_svg]:size-3.5">{action.icon}</span>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function ActionCatalogue({ actions, rowCount, testRows = 10, onAdd, className }: { actions: CatalogueAction[]; /** Rows in the table now, for the estimate. */ rowCount: number; testRows?: number; onAdd: (config: ActionConfig) => void; className?: string }) {
|
|
57
|
+
const [tab, setTab] = React.useState<(typeof TABS)[number]>("Suggested")
|
|
58
|
+
const [query, setQuery] = React.useState("")
|
|
59
|
+
const [chosen, setChosen] = React.useState<CatalogueAction | null>(null)
|
|
60
|
+
const [name, setName] = React.useState("")
|
|
61
|
+
const [inputs, setInputs] = React.useState<Record<string, string>>({})
|
|
62
|
+
const [run, setRun] = React.useState<ActionRunChoice>("test")
|
|
63
|
+
const [nameError, setNameError] = React.useState("")
|
|
64
|
+
const listRef = React.useRef<HTMLDivElement>(null)
|
|
65
|
+
|
|
66
|
+
const q = query.trim().toLowerCase()
|
|
67
|
+
const inTab = (a: CatalogueAction, t: (typeof TABS)[number]) => (t === "Suggested" ? !!a.suggested : a.category === t)
|
|
68
|
+
const shown = q ? actions.filter((a) => `${a.name} ${a.description}`.toLowerCase().includes(q)) : actions.filter((a) => inTab(a, tab))
|
|
69
|
+
const n = (x: number) => x.toLocaleString("en-GB")
|
|
70
|
+
const tested = Math.min(testRows, rowCount)
|
|
71
|
+
|
|
72
|
+
const choose = (a: CatalogueAction) => {
|
|
73
|
+
setChosen(a); setName(a.name); setNameError(""); setRun(a.costPerRow ? "test" : "all")
|
|
74
|
+
setInputs(Object.fromEntries((a.inputs ?? []).map((i) => [i.id, i.defaultValue ?? i.options[0] ?? ""])))
|
|
75
|
+
}
|
|
76
|
+
const add = () => {
|
|
77
|
+
if (!chosen) return
|
|
78
|
+
if (!name.trim()) { setNameError("Enter a column name."); document.getElementById("action-column-name")?.focus(); return }
|
|
79
|
+
onAdd({ action: chosen, columnName: name.trim(), inputs, run })
|
|
80
|
+
setChosen(null)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (chosen) {
|
|
84
|
+
const total = chosen.costPerRow * rowCount
|
|
85
|
+
const runLabel = run === "test" ? `Add column and test on ${n(tested)} rows` : run === "all" ? `Add column and run on ${n(rowCount)} rows` : "Add column"
|
|
86
|
+
return (
|
|
87
|
+
<div data-slot="action-catalogue" className={cn("grid content-start gap-4", className)}>
|
|
88
|
+
<Button variant="ghost" size="sm" className="justify-self-start" onClick={() => { setChosen(null); window.setTimeout(() => listRef.current?.querySelector<HTMLElement>(`[data-action="${chosen.id}"]`)?.focus()) }}><ArrowLeft />All actions</Button>
|
|
89
|
+
<div className="flex items-start gap-3">
|
|
90
|
+
<ActionIcon action={chosen} />
|
|
91
|
+
<div className="grid min-w-0 gap-0.5">
|
|
92
|
+
<h3 className="text-base font-semibold text-ink">{chosen.name}</h3>
|
|
93
|
+
<p className="text-sm text-ink-2">{chosen.description}</p>
|
|
94
|
+
<p className="tabular text-sm text-ink-3">{perRow(chosen.costPerRow)}{chosen.provider ? ` · uses your connected account` : ""}</p>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
<TextField id="action-column-name" label="Column name" value={name} onChange={(e) => { setName(e.target.value); setNameError("") }} error={nameError} required />
|
|
98
|
+
{(chosen.inputs ?? []).map((i) => (
|
|
99
|
+
<div key={i.id} className="grid gap-2">
|
|
100
|
+
<label htmlFor={`action-input-${i.id}`} className="text-base font-medium text-ink">{i.label}</label>
|
|
101
|
+
<Select value={inputs[i.id]} onValueChange={(v) => setInputs((x) => ({ ...x, [i.id]: v }))}>
|
|
102
|
+
<SelectTrigger id={`action-input-${i.id}`} className="w-full"><SelectValue /></SelectTrigger>
|
|
103
|
+
<SelectContent>{i.options.map((o) => <SelectItem key={o} value={o}>{o}</SelectItem>)}</SelectContent>
|
|
104
|
+
</Select>
|
|
105
|
+
</div>
|
|
106
|
+
))}
|
|
107
|
+
<fieldset className="grid gap-2">
|
|
108
|
+
<legend className="mb-2 text-base font-medium text-ink">Run after adding</legend>
|
|
109
|
+
<RadioGroup value={run} onValueChange={(v) => setRun(v as ActionRunChoice)} className="gap-2">
|
|
110
|
+
{chosen.costPerRow > 0 && <RadioField id="run-test" value="test" label={`Test on the first ${n(tested)} rows`} hint={`About ${formatCredits(chosen.costPerRow * tested)}. Check the results, then run the rest.`} />}
|
|
111
|
+
<RadioField id="run-all" value="all" label={`Run on all ${n(rowCount)} rows`} hint={chosen.costPerRow ? `About ${formatCredits(total)}.` : "Free."} />
|
|
112
|
+
<RadioField id="run-none" value="none" label="Don't run yet" hint="The column is added empty. Run it from its menu." />
|
|
113
|
+
</RadioGroup>
|
|
114
|
+
</fieldset>
|
|
115
|
+
<div className="grid gap-0.5 rounded-sm bg-surface-2 px-3 py-2">
|
|
116
|
+
<p className="text-base font-medium text-ink">{chosen.costPerRow ? `About ${formatCredits(total)} for ${n(rowCount)} rows` : `Free for all ${n(rowCount)} rows`}</p>
|
|
117
|
+
<p className="tabular text-sm text-ink-2">{chosen.costPerRow ? `${formatCredits(chosen.costPerRow)} × ${n(rowCount)} rows. Failed and not found rows aren't charged. New rows cost the same when Auto-run is on.` : "Formulas and exports don't use credits."}</p>
|
|
118
|
+
</div>
|
|
119
|
+
<Button className="justify-self-end" onClick={add}>{runLabel}</Button>
|
|
120
|
+
</div>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<div data-slot="action-catalogue" ref={listRef} className={cn("grid content-start gap-3", className)}>
|
|
126
|
+
<div className="relative">
|
|
127
|
+
<Search className="pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2 text-ink-3" aria-hidden="true" />
|
|
128
|
+
<Input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search actions" aria-label="Search actions" className="pl-8" />
|
|
129
|
+
</div>
|
|
130
|
+
{!q && (
|
|
131
|
+
<Tabs value={tab} onValueChange={(v) => setTab(v as (typeof TABS)[number])}>
|
|
132
|
+
<TabsList aria-label="Action categories" className="w-full overflow-x-auto">
|
|
133
|
+
{TABS.map((t) => <TabsTrigger key={t} value={t} count={actions.filter((a) => inTab(a, t)).length}>{t}</TabsTrigger>)}
|
|
134
|
+
</TabsList>
|
|
135
|
+
</Tabs>
|
|
136
|
+
)}
|
|
137
|
+
{q && <p className="text-sm text-ink-2" aria-live="polite">{shown.length ? `${shown.length} ${shown.length === 1 ? "action matches" : "actions match"} in all categories` : ""}</p>}
|
|
138
|
+
{shown.length ? (
|
|
139
|
+
<ul className="grid gap-1">
|
|
140
|
+
{shown.map((a) => (
|
|
141
|
+
<li key={a.id}>
|
|
142
|
+
<button type="button" data-action={a.id} onClick={() => choose(a)} className="flex w-full items-start gap-3 rounded-sm px-2 py-2 text-left transition-colors duration-(--duration-fast) hover:bg-hover">
|
|
143
|
+
<ActionIcon action={a} />
|
|
144
|
+
<span className="grid min-w-0 flex-1 gap-0.5">
|
|
145
|
+
<span className="text-base font-medium text-ink">{a.name}</span>
|
|
146
|
+
<span className="text-sm text-ink-2">{a.description}</span>
|
|
147
|
+
</span>
|
|
148
|
+
<span className="tabular shrink-0 text-sm text-ink-3">{perRow(a.costPerRow)}</span>
|
|
149
|
+
</button>
|
|
150
|
+
</li>
|
|
151
|
+
))}
|
|
152
|
+
</ul>
|
|
153
|
+
) : (
|
|
154
|
+
<div className="grid justify-items-start gap-1 py-4">
|
|
155
|
+
<p className="text-base text-ink-2">{q ? `No actions match “${query.trim()}”.` : "No actions in this category yet."}</p>
|
|
156
|
+
{q && <Button variant="link" size="xs" onClick={() => setQuery("")}>Clear search</Button>}
|
|
157
|
+
</div>
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* ---------------------------------------------------------------- table settings */
|
|
164
|
+
|
|
165
|
+
export type TableSettingsValue = {
|
|
166
|
+
duplicates: "keep-oldest" | "keep-newest" | "allow"
|
|
167
|
+
/** Column id that identifies a duplicate. */
|
|
168
|
+
matchOn: string
|
|
169
|
+
deleteEmptyRows: boolean
|
|
170
|
+
template: { shared: boolean; audience: "workspace" | "link" }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function TableSettings({
|
|
174
|
+
open,
|
|
175
|
+
onOpenChange,
|
|
176
|
+
value,
|
|
177
|
+
onSave,
|
|
178
|
+
columns,
|
|
179
|
+
tableName,
|
|
180
|
+
workspaceName,
|
|
181
|
+
templateLink,
|
|
182
|
+
mode = "push",
|
|
183
|
+
}: {
|
|
184
|
+
open: boolean
|
|
185
|
+
onOpenChange: (open: boolean) => void
|
|
186
|
+
value: TableSettingsValue
|
|
187
|
+
/** May return a promise; Save shows Saving until it settles. */
|
|
188
|
+
onSave: (value: TableSettingsValue) => void | Promise<void>
|
|
189
|
+
/** Columns that can identify a duplicate. */
|
|
190
|
+
columns: { id: string; label: string }[]
|
|
191
|
+
tableName: string
|
|
192
|
+
workspaceName: string
|
|
193
|
+
templateLink: string
|
|
194
|
+
mode?: PanelMode
|
|
195
|
+
}) {
|
|
196
|
+
const [draft, setDraft] = React.useState(value)
|
|
197
|
+
const [saving, setSaving] = React.useState(false)
|
|
198
|
+
const [asking, setAsking] = React.useState(false)
|
|
199
|
+
const [copied, setCopied] = React.useState(false)
|
|
200
|
+
React.useEffect(() => { if (open) setDraft(value) }, [open, value])
|
|
201
|
+
const dirty = JSON.stringify(draft) !== JSON.stringify(value)
|
|
202
|
+
const changes = (Object.keys(value) as (keyof TableSettingsValue)[]).filter((k) => JSON.stringify(draft[k]) !== JSON.stringify(value[k])).length
|
|
203
|
+
|
|
204
|
+
React.useEffect(() => {
|
|
205
|
+
if (!open || !dirty) return
|
|
206
|
+
const warn = (e: BeforeUnloadEvent) => { e.preventDefault() }
|
|
207
|
+
window.addEventListener("beforeunload", warn)
|
|
208
|
+
return () => window.removeEventListener("beforeunload", warn)
|
|
209
|
+
}, [open, dirty])
|
|
210
|
+
|
|
211
|
+
const close = (o: boolean) => { if (!o && dirty) { setAsking(true); return } onOpenChange(o) }
|
|
212
|
+
const save = async () => {
|
|
213
|
+
setSaving(true)
|
|
214
|
+
try { await onSave(draft); toast.success("Table settings saved"); onOpenChange(false) } finally { setSaving(false) }
|
|
215
|
+
}
|
|
216
|
+
const match = columns.find((c) => c.id === draft.matchOn)?.label ?? "the key column"
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
<>
|
|
220
|
+
<SidePanel
|
|
221
|
+
open={open}
|
|
222
|
+
onOpenChange={close}
|
|
223
|
+
mode={mode}
|
|
224
|
+
label="Table settings"
|
|
225
|
+
title="Table settings"
|
|
226
|
+
meta={tableName}
|
|
227
|
+
footer={<><Button variant="outline" onClick={() => close(false)}>Cancel</Button><Button loading={saving} onClick={save} shortcut="⌘↵">Save</Button></>}
|
|
228
|
+
>
|
|
229
|
+
<form className="grid gap-6" onSubmit={(e) => { e.preventDefault(); void save() }} onKeyDown={(e) => { if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); void save() } }}>
|
|
230
|
+
<section aria-labelledby="ts-dupes" className="grid gap-3">
|
|
231
|
+
<div className="grid gap-0.5">
|
|
232
|
+
<h3 id="ts-dupes" className="text-base font-semibold text-ink">Duplicate rows</h3>
|
|
233
|
+
<p className="text-sm text-ink-2">Checked when rows are added, imported or arrive from an automation.</p>
|
|
234
|
+
</div>
|
|
235
|
+
<div className="grid gap-2">
|
|
236
|
+
<label htmlFor="ts-match" className="text-base font-medium text-ink">Rows are duplicates when they share</label>
|
|
237
|
+
<Select value={draft.matchOn} onValueChange={(v) => setDraft((d) => ({ ...d, matchOn: v }))}>
|
|
238
|
+
<SelectTrigger id="ts-match" className="w-full"><SelectValue /></SelectTrigger>
|
|
239
|
+
<SelectContent>{columns.map((c) => <SelectItem key={c.id} value={c.id}>{c.label}</SelectItem>)}</SelectContent>
|
|
240
|
+
</Select>
|
|
241
|
+
</div>
|
|
242
|
+
<RadioGroup value={draft.duplicates} onValueChange={(v) => setDraft((d) => ({ ...d, duplicates: v as TableSettingsValue["duplicates"] }))} aria-labelledby="ts-dupes" className="gap-3">
|
|
243
|
+
<RadioField id="ts-oldest" value="keep-oldest" label="Keep the oldest row" hint={`A new row with the same ${match} isn't added. Imports say how many were skipped. Nothing already in the table changes.`} />
|
|
244
|
+
<RadioField id="ts-newest" value="keep-newest" label="Keep the newest row" hint={`A new row with the same ${match} replaces the old one. The old row's values, including looked-up and AI cells, are overwritten, and computed columns run again for it at their usual cost.`} />
|
|
245
|
+
<RadioField id="ts-allow" value="allow" label="Keep both" hint="Duplicates are allowed. Every copy runs computed columns, so each one costs credits." />
|
|
246
|
+
</RadioGroup>
|
|
247
|
+
</section>
|
|
248
|
+
<section aria-labelledby="ts-empty" className="grid gap-3">
|
|
249
|
+
<h3 id="ts-empty" className="text-base font-semibold text-ink">Empty rows</h3>
|
|
250
|
+
<CheckboxField id="ts-delete-empty" label="Delete empty rows automatically" hint="Rows with no value in any column are moved to Deleted after 24 hours. You can restore them from Deleted for 30 days." checked={draft.deleteEmptyRows} onCheckedChange={(v) => setDraft((d) => ({ ...d, deleteEmptyRows: v === true }))} />
|
|
251
|
+
</section>
|
|
252
|
+
<section aria-labelledby="ts-template" className="grid gap-3">
|
|
253
|
+
<h3 id="ts-template" className="text-base font-semibold text-ink">Share as a template</h3>
|
|
254
|
+
<CheckboxField id="ts-share" label="Share this table as a template" hint="Shares the columns, formulas, prompts and these settings. Rows and looked-up values are never shared." checked={draft.template.shared} onCheckedChange={(v) => setDraft((d) => ({ ...d, template: { ...d.template, shared: v === true } }))} />
|
|
255
|
+
{draft.template.shared && (
|
|
256
|
+
<div className="grid gap-3 border-l-2 border-line pl-4">
|
|
257
|
+
<fieldset className="grid gap-2">
|
|
258
|
+
<legend className="mb-2 text-base font-medium text-ink">Who can use the template</legend>
|
|
259
|
+
<RadioGroup value={draft.template.audience} onValueChange={(v) => setDraft((d) => ({ ...d, template: { ...d.template, audience: v as "workspace" | "link" } }))} className="gap-2">
|
|
260
|
+
<RadioField id="ts-aud-ws" value="workspace" label={`People in ${workspaceName}`} hint="They find it in Templates." />
|
|
261
|
+
<RadioField id="ts-aud-link" value="link" label="Anyone with the link" hint="Including people outside the workspace. They get a copy, never this table." />
|
|
262
|
+
</RadioGroup>
|
|
263
|
+
</fieldset>
|
|
264
|
+
<div className="grid gap-2">
|
|
265
|
+
<label htmlFor="ts-link" className="text-base font-medium text-ink">Template link</label>
|
|
266
|
+
<div className="flex gap-2">
|
|
267
|
+
<Input id="ts-link" readOnly value={templateLink} onFocus={(e) => e.currentTarget.select()} className="font-mono" />
|
|
268
|
+
<Button type="button" variant="outline" onClick={() => { void navigator.clipboard?.writeText(templateLink); setCopied(true); window.setTimeout(() => setCopied(false), 1500) }}>
|
|
269
|
+
<Swap kind="icon" swapKey={copied ? "y" : "n"}>{copied ? <Check className="text-ok" /> : <Copy />}</Swap>
|
|
270
|
+
<Swap swapKey={copied ? "c" : "n"}>{copied ? "Copied" : "Copy"}</Swap>
|
|
271
|
+
</Button>
|
|
272
|
+
</div>
|
|
273
|
+
<p className="text-sm text-ink-3">The link works once you save.</p>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
)}
|
|
277
|
+
</section>
|
|
278
|
+
</form>
|
|
279
|
+
</SidePanel>
|
|
280
|
+
<AlertDialog open={asking} onOpenChange={setAsking}>
|
|
281
|
+
<AlertDialogContent>
|
|
282
|
+
<AlertDialogHeader>
|
|
283
|
+
<AlertDialogTitle>Discard changes?</AlertDialogTitle>
|
|
284
|
+
<AlertDialogDescription>You changed {changes} {changes === 1 ? "setting" : "settings"} in {tableName}. They haven't been saved.</AlertDialogDescription>
|
|
285
|
+
</AlertDialogHeader>
|
|
286
|
+
<AlertDialogFooter>
|
|
287
|
+
<AlertDialogCancel autoFocus>Keep editing</AlertDialogCancel>
|
|
288
|
+
<AlertDialogAction variant="destructive" onClick={() => { setAsking(false); setDraft(value); onOpenChange(false) }}>Discard changes</AlertDialogAction>
|
|
289
|
+
</AlertDialogFooter>
|
|
290
|
+
</AlertDialogContent>
|
|
291
|
+
</AlertDialog>
|
|
292
|
+
</>
|
|
293
|
+
)
|
|
294
|
+
}
|