@pipe0/react 1.0.37 → 1.0.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/components/compound/effect-catalog/category-filter.mjs +1 -1
- package/dist/components/compound/effect-catalog/category-filter.mjs.map +1 -1
- package/dist/components/compound/effect-catalog/search-filter.mjs +1 -1
- package/dist/components/compound/effect-catalog/search-filter.mjs.map +1 -1
- package/dist/components/compound/pipe-catalog/category-filter.mjs +1 -1
- package/dist/components/compound/pipe-catalog/category-filter.mjs.map +1 -1
- package/dist/components/compound/pipe-catalog/input-field-filter.mjs +1 -1
- package/dist/components/compound/pipe-catalog/input-field-filter.mjs.map +1 -1
- package/dist/components/compound/pipe-catalog/output-field-filter.mjs +1 -1
- package/dist/components/compound/pipe-catalog/output-field-filter.mjs.map +1 -1
- package/dist/components/compound/pipe-catalog/provider-filter.mjs +1 -1
- package/dist/components/compound/pipe-catalog/provider-filter.mjs.map +1 -1
- package/dist/components/compound/pipe-catalog/search-filter.mjs +1 -1
- package/dist/components/compound/pipe-catalog/search-filter.mjs.map +1 -1
- package/dist/components/compound/pipe-catalog/tag-filter.mjs +1 -1
- package/dist/components/compound/pipe-catalog/tag-filter.mjs.map +1 -1
- package/dist/components/compound/search-catalog/category-filter.mjs +1 -1
- package/dist/components/compound/search-catalog/category-filter.mjs.map +1 -1
- package/dist/components/compound/search-catalog/output-field-filter.mjs +1 -1
- package/dist/components/compound/search-catalog/output-field-filter.mjs.map +1 -1
- package/dist/components/compound/search-catalog/provider-filter.mjs +1 -1
- package/dist/components/compound/search-catalog/provider-filter.mjs.map +1 -1
- package/dist/components/compound/search-catalog/search-filter.mjs +1 -1
- package/dist/components/compound/search-catalog/search-filter.mjs.map +1 -1
- package/dist/components/compound/search-catalog/tag-filter.mjs +1 -1
- package/dist/components/compound/search-catalog/tag-filter.mjs.map +1 -1
- package/dist/components/defaults/adapters/json-extraction-input.mjs +1 -1
- package/dist/components/defaults/adapters/loose-object-input.mjs +1 -1
- package/dist/components/defaults/adapters/loose-object-input.mjs.map +1 -1
- package/dist/components/defaults/adapters/pipes-run-if-input.mjs +1 -1
- package/dist/components/defaults/adapters/prompt-input.mjs +1 -1
- package/dist/components/defaults/adapters/providers-input.mjs +1 -2
- package/dist/components/defaults/adapters/providers-input.mjs.map +1 -1
- package/dist/components/defaults/adapters/select-input.mjs +1 -1
- package/dist/components/defaults/adapters/select-input.mjs.map +1 -1
- package/dist/components/defaults/adapters/tagged-ordered-multi-create-input.mjs +2 -3
- package/dist/components/defaults/adapters/tagged-ordered-multi-create-input.mjs.map +1 -1
- package/dist/components/defaults/adapters/tagged-text-input.mjs +1 -1
- package/dist/components/defaults/adapters/tagged-text-input.mjs.map +1 -1
- package/dist/components/defaults/catalog/active-filter-pill.mjs +1 -1
- package/dist/components/defaults/catalog/active-filter-pill.mjs.map +1 -1
- package/dist/components/defaults/catalog/card-derived.d.mts.map +1 -1
- package/dist/components/defaults/catalog/card-derived.mjs +9 -6
- package/dist/components/defaults/catalog/card-derived.mjs.map +1 -1
- package/dist/components/defaults/catalog/category-section.d.mts +2 -2
- package/dist/components/defaults/catalog/category-section.d.mts.map +1 -1
- package/dist/components/defaults/catalog/category-section.mjs +10 -15
- package/dist/components/defaults/catalog/category-section.mjs.map +1 -1
- package/dist/components/defaults/catalog/provider-avatars.mjs +1 -1
- package/dist/components/defaults/catalog/provider-avatars.mjs.map +1 -1
- package/dist/components/defaults/layout/field-wrapper.mjs +2 -2
- package/dist/components/defaults/layout/field-wrapper.mjs.map +1 -1
- package/dist/components/defaults/layout/group.mjs +2 -1
- package/dist/components/defaults/layout/group.mjs.map +1 -1
- package/dist/components/defaults/layout/section.mjs +5 -8
- package/dist/components/defaults/layout/section.mjs.map +1 -1
- package/dist/components/internal/LiquidEditor/ChipEditPopover.mjs +1 -1
- package/dist/components/internal/LiquidEditor/ChipEditPopover.mjs.map +1 -1
- package/dist/components/internal/LiquidEditor/UnifiedReferencePicker.mjs +1 -1
- package/dist/components/internal/LiquidEditor/UnifiedReferencePicker.mjs.map +1 -1
- package/dist/components/internal/combobox/include-exclude-combobox.mjs +2 -3
- package/dist/components/internal/combobox/include-exclude-combobox.mjs.map +1 -1
- package/dist/components/internal/combobox/suggest-combobox.mjs +2 -3
- package/dist/components/internal/combobox/suggest-combobox.mjs.map +1 -1
- package/dist/components/internal/icons.d.mts +14 -0
- package/dist/components/internal/icons.d.mts.map +1 -0
- package/dist/components/internal/icons.mjs +137 -185
- package/dist/components/internal/icons.mjs.map +1 -1
- package/dist/components/ui/alert.d.mts +1 -1
- package/dist/components/ui/button.d.mts +1 -1
- package/dist/components/ui/combobox.mjs +4 -4
- package/dist/components/ui/combobox.mjs.map +1 -1
- package/dist/components/ui/select.mjs +5 -5
- package/dist/components/ui/select.mjs.map +1 -1
- package/dist/hooks/use-pipe-catalog-table.d.mts +8 -8
- package/dist/hooks/use-search-catalog-table.d.mts +6 -6
- package/dist/styles/pipe0-form.css +1 -1
- package/dist/utils/catalog-helpers.d.mts +4 -5
- package/dist/utils/catalog-helpers.d.mts.map +1 -1
- package/dist/utils/catalog-helpers.mjs +4 -4
- package/dist/utils/catalog-helpers.mjs.map +1 -1
- package/dist/widgets/avatar-group.d.mts +12 -3
- package/dist/widgets/avatar-group.d.mts.map +1 -1
- package/dist/widgets/avatar-group.mjs +173 -50
- package/dist/widgets/avatar-group.mjs.map +1 -1
- package/dist/widgets/field-type-badge.d.mts.map +1 -1
- package/dist/widgets/field-type-badge.mjs +1 -1
- package/dist/widgets/field-type-badge.mjs.map +1 -1
- package/dist/widgets/icon-glyph.mjs +2 -3
- package/dist/widgets/icon-glyph.mjs.map +1 -1
- package/dist/widgets/token-pricing-badge.mjs +1 -1
- package/dist/widgets/token-pricing-badge.mjs.map +1 -1
- package/package.json +4 -3
- package/dist/widgets/glyphs/linkedin.mjs +0 -32
- package/dist/widgets/glyphs/linkedin.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"field-wrapper.mjs","names":[],"sources":["../../../../src/components/defaults/layout/field-wrapper.tsx"],"sourcesContent":["import { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport {
|
|
1
|
+
{"version":3,"file":"field-wrapper.mjs","names":[],"sources":["../../../../src/components/defaults/layout/field-wrapper.tsx"],"sourcesContent":["import { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport { useFormAlert, useFormConfig } from \"../../../context/form-context.js\";\nimport { cn } from \"../../../lib/utils.js\";\nimport type { AnyFieldProps } from \"../../../types/field-props.js\";\nimport { HoverInfo } from \"../../hover-info.js\";\nimport { Asterisk } from \"../../internal/icons.js\";\nimport { RichText } from \"../../rich-text.js\";\n\nexport interface DefaultFieldWrapperState {\n field: AnyFieldProps;\n disabled: boolean;\n hasError: boolean;\n}\n\nexport interface DefaultFieldWrapperProps\n extends useRender.ComponentProps<\"div\", DefaultFieldWrapperState> {\n field: AnyFieldProps;\n}\n\nexport function DefaultFieldWrapper({\n field,\n children,\n className,\n render,\n ...props\n}: DefaultFieldWrapperProps) {\n const { classNames } = useFormConfig();\n const Alert = useFormAlert();\n const hasError = !!field.error;\n const isDisabled = field.disabled;\n\n // Boolean toggles render their label inline with the switch (settings-row\n // pattern), so the wrapper's label/description block is suppressed for them.\n const inlineLabel = field.kind === \"boolean_input\";\n\n const defaultContent = (\n <>\n {!inlineLabel && (field.label || field.description || field.info) && (\n <div className=\"pz:flex pz:flex-col pz:gap-1\">\n <div className=\"pz:flex pz:items-center pz:gap-1.5\">\n {field.label && (\n <label\n data-p0=\"label\"\n htmlFor={field.id}\n className={\n classNames?.label ??\n cn(\n \"pz:text-[13px] pz:leading-none pz:font-medium pz:select-none\",\n hasError && \"pz:text-destructive\",\n )\n }\n >\n <RichText>{field.label}</RichText>\n {field.meta.required && (\n <Asterisk\n aria-label=\"required\"\n className=\"pz:ml-0.5 pz:inline-block pz:size-3 pz:align-top pz:text-primary\"\n />\n )}\n </label>\n )}\n {field.info && (\n <HoverInfo>\n <RichText>{field.info}</RichText>\n </HoverInfo>\n )}\n </div>\n {field.description && (\n <p\n data-p0=\"description\"\n className={classNames?.description ?? \"pz:text-xs pz:text-muted-foreground\"}\n >\n <RichText>{field.description}</RichText>\n </p>\n )}\n </div>\n )}\n {isDisabled ? (\n <div className=\"pz:opacity-60 pz:pointer-events-none\">{children}</div>\n ) : (\n children\n )}\n {isDisabled && field.disabledReason && (\n <Alert variant=\"info\" data-p0=\"disabled-reason\" className={classNames?.disabledReason}>\n <RichText>{field.disabledReason}</RichText>\n </Alert>\n )}\n {hasError && (\n <span\n data-p0=\"error\"\n className={classNames?.error ?? \"pz:text-destructive pz:text-xs pz:font-medium\"}\n role=\"alert\"\n >\n <RichText>{field.error}</RichText>\n </span>\n )}\n </>\n );\n\n return useRender({\n defaultTagName: \"div\",\n render,\n state: { field, disabled: isDisabled, hasError },\n stateAttributesMapping: {\n field: () => null,\n },\n props: mergeProps<\"div\">(\n {\n className: cn(\n classNames?.field ?? \"pz:grid pz:gap-2\",\n isDisabled && classNames?.fieldDisabled,\n className,\n ),\n children: defaultContent,\n ...({\n \"data-p0\": \"field\",\n \"data-p0-field\": field.path,\n \"data-p0-kind\": field.kind,\n ...(isDisabled ? { \"data-p0-disabled\": \"true\", \"aria-disabled\": \"true\" } : {}),\n } as Record<string, string>),\n },\n props,\n ),\n });\n}\n"],"mappings":";;;;;;;;;;AAoBA,SAAgB,oBAAoB,EAClC,OACA,UACA,WACA,QACA,GAAG,SACwB;CAC3B,MAAM,EAAE,eAAe,eAAe;CACtC,MAAM,QAAQ,cAAc;CAC5B,MAAM,WAAW,CAAC,CAAC,MAAM;CACzB,MAAM,aAAa,MAAM;CAMzB,MAAM,iBACJ;EACG,EAJe,MAAM,SAAS,qBAIb,MAAM,SAAS,MAAM,eAAe,MAAM,SAC1D,qBAAC,OAAD;GAAK,WAAU;aAAf,CACE,qBAAC,OAAD;IAAK,WAAU;cAAf,CACG,MAAM,SACL,qBAAC,SAAD;KACE,WAAQ;KACR,SAAS,MAAM;KACf,WACE,YAAY,SACZ,GACE,gEACA,YAAY,sBACb;eARL,CAWE,oBAAC,UAAD,YAAW,MAAM,OAAiB,GACjC,MAAM,KAAK,YACV,oBAAC,UAAD;MACE,cAAW;MACX,WAAU;MACV,EAEE;QAET,MAAM,QACL,oBAAC,WAAD,YACE,oBAAC,UAAD,YAAW,MAAM,MAAgB,GACvB,EAEV;OACL,MAAM,eACL,oBAAC,KAAD;IACE,WAAQ;IACR,WAAW,YAAY,eAAe;cAEtC,oBAAC,UAAD,YAAW,MAAM,aAAuB;IACtC,EAEF;;EAEP,aACC,oBAAC,OAAD;GAAK,WAAU;GAAwC;GAAe,IAEtE;EAED,cAAc,MAAM,kBACnB,oBAAC,OAAD;GAAO,SAAQ;GAAO,WAAQ;GAAkB,WAAW,YAAY;aACrE,oBAAC,UAAD,YAAW,MAAM,gBAA0B;GACrC;EAET,YACC,oBAAC,QAAD;GACE,WAAQ;GACR,WAAW,YAAY,SAAS;GAChC,MAAK;aAEL,oBAAC,UAAD,YAAW,MAAM,OAAiB;GAC7B;EAER;AAGL,QAAO,UAAU;EACf,gBAAgB;EAChB;EACA,OAAO;GAAE;GAAO,UAAU;GAAY;GAAU;EAChD,wBAAwB,EACtB,aAAa,MACd;EACD,OAAO,WACL;GACE,WAAW,GACT,YAAY,SAAS,oBACrB,cAAc,YAAY,eAC1B,UACD;GACD,UAAU;GAER,WAAW;GACX,iBAAiB,MAAM;GACvB,gBAAgB,MAAM;GACtB,GAAI,aAAa;IAAE,oBAAoB;IAAQ,iBAAiB;IAAQ,GAAG,EAAE;GAEhF,EACD,MACD;EACF,CAAC"}
|
|
@@ -49,6 +49,7 @@ function DefaultGroup({ group, children, className, render, ...props }) {
|
|
|
49
49
|
countLabel && /* @__PURE__ */ jsx(Badge, {
|
|
50
50
|
variant: showDirty ? "default" : "secondary",
|
|
51
51
|
"data-p0": "group-badge",
|
|
52
|
+
className: showDirty ? "pz:h-[18px] pz:rounded-[5px] pz:bg-primary/[0.08] pz:px-1.5 pz:text-[11px] pz:text-primary pz:ring-1 pz:ring-primary/25 pz:ring-inset" : "pz:h-[18px] pz:rounded-[5px] pz:bg-muted pz:px-1.5 pz:text-[11px] pz:text-muted-foreground pz:ring-1 pz:ring-border pz:ring-inset",
|
|
52
53
|
children: countLabel
|
|
53
54
|
}),
|
|
54
55
|
hasErrors && /* @__PURE__ */ jsxs(Badge, {
|
|
@@ -95,7 +96,7 @@ function DefaultGroup({ group, children, className, render, ...props }) {
|
|
|
95
96
|
fields: () => null
|
|
96
97
|
},
|
|
97
98
|
props: mergeProps({
|
|
98
|
-
className: cn(classNames?.group ?? cn("pz:flex pz:flex-col pz:bg-card pz:border pz:border-border pz:rounded-
|
|
99
|
+
className: cn(classNames?.group ?? cn("pz:flex pz:flex-col pz:bg-card pz:border pz:border-border pz:rounded-xl pz:overflow-hidden", hasErrors && "pz:border-destructive"), className),
|
|
99
100
|
children: defaultContent,
|
|
100
101
|
"data-p0": "group",
|
|
101
102
|
"data-p0-group": group.key,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"group.mjs","names":["AccordionPrimitive"],"sources":["../../../../src/components/defaults/layout/group.tsx"],"sourcesContent":["import { Accordion as AccordionPrimitive } from \"@base-ui/react/accordion\";\nimport { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport { useState } from \"react\";\nimport { useFormConfig } from \"../../../context/form-context.js\";\nimport { cn } from \"../../../lib/utils.js\";\nimport type { AnyFieldProps } from \"../../../types/field-props.js\";\nimport type { FormGroupHandle } from \"../../../types/form-handle.js\";\nimport { IconGlyph } from \"../../../widgets/icon-glyph.js\";\nimport { HoverInfo } from \"../../hover-info.js\";\nimport { IconChevronDown } from \"../../internal/icons.js\";\nimport { Badge } from \"../../ui/badge.js\";\n\nexport interface DefaultGroupState {\n expanded: boolean;\n setExpanded: (expanded: boolean) => void;\n fields: ReadonlyArray<AnyFieldProps>;\n hasErrors: boolean;\n errorCount: number;\n setCount: number;\n dirtyCount: number;\n}\n\nexport interface DefaultGroupProps extends useRender.ComponentProps<\"div\", DefaultGroupState> {\n group: FormGroupHandle;\n}\n\nconst ITEM_VALUE = \"group\";\n\nexport function DefaultGroup({ group, children, className, render, ...props }: DefaultGroupProps) {\n const { classNames } = useFormConfig();\n // Track open state alongside the accordion so consumers reading\n // `state.expanded` (via render-prop) still get the right value.\n const [expanded, setExpanded] = useState(group.defaultExpand);\n const hasErrors = group.errorCount > 0;\n const showDirty = group.dirtyCount > 0;\n const countLabel = showDirty\n ? `${group.dirtyCount} modified`\n : group.setCount > 0\n ? `${group.setCount} set`\n : null;\n\n const defaultContent = (\n <AccordionPrimitive.Root\n value={expanded ? [ITEM_VALUE] : []}\n onValueChange={(next) => setExpanded(next.includes(ITEM_VALUE))}\n >\n <AccordionPrimitive.Item value={ITEM_VALUE}>\n <AccordionPrimitive.Header className=\"pz:flex\">\n <AccordionPrimitive.Trigger\n data-p0=\"group-header\"\n className={\n classNames?.groupHeader ??\n cn(\n \"pz:group/group-trigger\",\n \"pz:flex pz:items-center pz:justify-between pz:gap-4 pz:w-full\",\n \"pz:px-3.5 pz:py-2.5 pz:cursor-pointer pz:text-sm pz:font-medium pz:text-left\",\n \"pz:transition-colors pz:hover:bg-muted/60\",\n \"pz:aria-expanded:hover:bg-transparent\",\n \"pz:focus-visible:outline-none\",\n hasErrors && \"pz:text-destructive pz:bg-destructive/10\",\n )\n }\n >\n <span className=\"pz:flex pz:items-center pz:gap-2\">\n {group.iconKey && (\n <IconGlyph\n keyName={group.iconKey}\n size={14}\n className=\"pz:text-muted-foreground pz:shrink-0\"\n />\n )}\n {group.label ?? group.key}\n {group.description && (\n <span onClick={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()}>\n <HoverInfo>{group.description}</HoverInfo>\n </span>\n )}\n </span>\n <span className=\"pz:flex pz:items-center pz:gap-2\">\n {countLabel && (\n <Badge
|
|
1
|
+
{"version":3,"file":"group.mjs","names":["AccordionPrimitive"],"sources":["../../../../src/components/defaults/layout/group.tsx"],"sourcesContent":["import { Accordion as AccordionPrimitive } from \"@base-ui/react/accordion\";\nimport { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport { useState } from \"react\";\nimport { useFormConfig } from \"../../../context/form-context.js\";\nimport { cn } from \"../../../lib/utils.js\";\nimport type { AnyFieldProps } from \"../../../types/field-props.js\";\nimport type { FormGroupHandle } from \"../../../types/form-handle.js\";\nimport { IconGlyph } from \"../../../widgets/icon-glyph.js\";\nimport { HoverInfo } from \"../../hover-info.js\";\nimport { IconChevronDown } from \"../../internal/icons.js\";\nimport { Badge } from \"../../ui/badge.js\";\n\nexport interface DefaultGroupState {\n expanded: boolean;\n setExpanded: (expanded: boolean) => void;\n fields: ReadonlyArray<AnyFieldProps>;\n hasErrors: boolean;\n errorCount: number;\n setCount: number;\n dirtyCount: number;\n}\n\nexport interface DefaultGroupProps extends useRender.ComponentProps<\"div\", DefaultGroupState> {\n group: FormGroupHandle;\n}\n\nconst ITEM_VALUE = \"group\";\n\nexport function DefaultGroup({ group, children, className, render, ...props }: DefaultGroupProps) {\n const { classNames } = useFormConfig();\n // Track open state alongside the accordion so consumers reading\n // `state.expanded` (via render-prop) still get the right value.\n const [expanded, setExpanded] = useState(group.defaultExpand);\n const hasErrors = group.errorCount > 0;\n const showDirty = group.dirtyCount > 0;\n const countLabel = showDirty\n ? `${group.dirtyCount} modified`\n : group.setCount > 0\n ? `${group.setCount} set`\n : null;\n\n const defaultContent = (\n <AccordionPrimitive.Root\n value={expanded ? [ITEM_VALUE] : []}\n onValueChange={(next) => setExpanded(next.includes(ITEM_VALUE))}\n >\n <AccordionPrimitive.Item value={ITEM_VALUE}>\n <AccordionPrimitive.Header className=\"pz:flex\">\n <AccordionPrimitive.Trigger\n data-p0=\"group-header\"\n className={\n classNames?.groupHeader ??\n cn(\n \"pz:group/group-trigger\",\n \"pz:flex pz:items-center pz:justify-between pz:gap-4 pz:w-full\",\n \"pz:px-3.5 pz:py-2.5 pz:cursor-pointer pz:text-sm pz:font-medium pz:text-left\",\n \"pz:transition-colors pz:hover:bg-muted/60\",\n \"pz:aria-expanded:hover:bg-transparent\",\n \"pz:focus-visible:outline-none\",\n hasErrors && \"pz:text-destructive pz:bg-destructive/10\",\n )\n }\n >\n <span className=\"pz:flex pz:items-center pz:gap-2\">\n {group.iconKey && (\n <IconGlyph\n keyName={group.iconKey}\n size={14}\n className=\"pz:text-muted-foreground pz:shrink-0\"\n />\n )}\n {group.label ?? group.key}\n {group.description && (\n <span onClick={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()}>\n <HoverInfo>{group.description}</HoverInfo>\n </span>\n )}\n </span>\n <span className=\"pz:flex pz:items-center pz:gap-2\">\n {countLabel && (\n <Badge\n variant={showDirty ? \"default\" : \"secondary\"}\n data-p0=\"group-badge\"\n // One boxed chip for both states; \"modified\" only shifts to a\n // primary tint, so it reads as a change, not an alarm.\n className={\n showDirty\n ? \"pz:h-[18px] pz:rounded-[5px] pz:bg-primary/[0.08] pz:px-1.5 pz:text-[11px] pz:text-primary pz:ring-1 pz:ring-primary/25 pz:ring-inset\"\n : \"pz:h-[18px] pz:rounded-[5px] pz:bg-muted pz:px-1.5 pz:text-[11px] pz:text-muted-foreground pz:ring-1 pz:ring-border pz:ring-inset\"\n }\n >\n {countLabel}\n </Badge>\n )}\n {hasErrors && (\n <Badge variant=\"destructive\" data-p0=\"group-error-badge\">\n {group.errorCount} {group.errorCount === 1 ? \"error\" : \"errors\"}\n </Badge>\n )}\n <IconChevronDown\n width={16}\n height={16}\n className={cn(\n \"pz:text-muted-foreground pz:transition-transform pz:duration-150\",\n \"pz:group-aria-expanded/group-trigger:rotate-180\",\n )}\n />\n </span>\n </AccordionPrimitive.Trigger>\n </AccordionPrimitive.Header>\n <AccordionPrimitive.Panel\n data-p0=\"group-content\"\n // The library ships keyframes via `tw-animate-css` —\n // `animate-accordion-down` / `animate-accordion-up` plus the\n // `--accordion-panel-height` var that base-ui populates on the\n // panel give a smooth height transition without JS measuring.\n className=\"pz:overflow-hidden pz:border-t pz:border-border pz:data-open:animate-accordion-down pz:data-closed:animate-accordion-up\"\n >\n <div\n className={\n classNames?.groupContent ??\n cn(\n \"pz:flex pz:flex-col pz:gap-3 pz:px-3.5 pz:pb-2 pz:pt-2\",\n \"pz:h-(--accordion-panel-height) pz:data-ending-style:h-0 pz:data-starting-style:h-0\",\n )\n }\n >\n {children}\n </div>\n </AccordionPrimitive.Panel>\n </AccordionPrimitive.Item>\n </AccordionPrimitive.Root>\n );\n\n return useRender({\n defaultTagName: \"div\",\n render,\n state: {\n expanded,\n setExpanded,\n fields: group.fields,\n hasErrors,\n errorCount: group.errorCount,\n setCount: group.setCount,\n dirtyCount: group.dirtyCount,\n },\n stateAttributesMapping: {\n setExpanded: () => null,\n fields: () => null,\n },\n props: mergeProps<\"div\">(\n {\n className: cn(\n classNames?.group ??\n cn(\n \"pz:flex pz:flex-col pz:bg-card pz:border pz:border-border pz:rounded-xl pz:overflow-hidden\",\n hasErrors && \"pz:border-destructive\",\n ),\n className,\n ),\n children: defaultContent,\n ...({\n \"data-p0\": \"group\",\n \"data-p0-group\": group.key,\n \"data-p0-expanded\": String(expanded),\n } as Record<string, string>),\n },\n props,\n ),\n });\n}\n"],"mappings":";;;;;;;;;;;;;AA2BA,MAAM,aAAa;AAEnB,SAAgB,aAAa,EAAE,OAAO,UAAU,WAAW,QAAQ,GAAG,SAA4B;CAChG,MAAM,EAAE,eAAe,eAAe;CAGtC,MAAM,CAAC,UAAU,eAAe,SAAS,MAAM,cAAc;CAC7D,MAAM,YAAY,MAAM,aAAa;CACrC,MAAM,YAAY,MAAM,aAAa;CACrC,MAAM,aAAa,YACf,GAAG,MAAM,WAAW,aACpB,MAAM,WAAW,IACf,GAAG,MAAM,SAAS,QAClB;CAEN,MAAM,iBACJ,oBAACA,UAAmB,MAApB;EACE,OAAO,WAAW,CAAC,WAAW,GAAG,EAAE;EACnC,gBAAgB,SAAS,YAAY,KAAK,SAAS,WAAW,CAAC;YAE/D,qBAACA,UAAmB,MAApB;GAAyB,OAAO;aAAhC,CACE,oBAACA,UAAmB,QAApB;IAA2B,WAAU;cACnC,qBAACA,UAAmB,SAApB;KACE,WAAQ;KACR,WACE,YAAY,eACZ,GACE,0BACA,iEACA,gFACA,6CACA,yCACA,iCACA,aAAa,2CACd;eAZL,CAeE,qBAAC,QAAD;MAAM,WAAU;gBAAhB;OACG,MAAM,WACL,oBAAC,WAAD;QACE,SAAS,MAAM;QACf,MAAM;QACN,WAAU;QACV;OAEH,MAAM,SAAS,MAAM;OACrB,MAAM,eACL,oBAAC,QAAD;QAAM,UAAU,MAAM,EAAE,iBAAiB;QAAE,YAAY,MAAM,EAAE,iBAAiB;kBAC9E,oBAAC,WAAD,YAAY,MAAM,aAAwB;QACrC;OAEJ;SACP,qBAAC,QAAD;MAAM,WAAU;gBAAhB;OACG,cACC,oBAAC,OAAD;QACE,SAAS,YAAY,YAAY;QACjC,WAAQ;QAGR,WACE,YACI,0IACA;kBAGL;QACK;OAET,aACC,qBAAC,OAAD;QAAO,SAAQ;QAAc,WAAQ;kBAArC;SACG,MAAM;SAAW;SAAE,MAAM,eAAe,IAAI,UAAU;SACjD;;OAEV,oBAAC,iBAAD;QACE,OAAO;QACP,QAAQ;QACR,WAAW,GACT,oEACA,kDACD;QACD;OACG;QACoB;;IACH,GAC5B,oBAACA,UAAmB,OAApB;IACE,WAAQ;IAKR,WAAU;cAEV,oBAAC,OAAD;KACE,WACE,YAAY,gBACZ,GACE,0DACA,sFACD;KAGF;KACG;IACmB,EACH;;EACF;AAG5B,QAAO,UAAU;EACf,gBAAgB;EAChB;EACA,OAAO;GACL;GACA;GACA,QAAQ,MAAM;GACd;GACA,YAAY,MAAM;GAClB,UAAU,MAAM;GAChB,YAAY,MAAM;GACnB;EACD,wBAAwB;GACtB,mBAAmB;GACnB,cAAc;GACf;EACD,OAAO,WACL;GACE,WAAW,GACT,YAAY,SACV,GACE,8FACA,aAAa,wBACd,EACH,UACD;GACD,UAAU;GAER,WAAW;GACX,iBAAiB,MAAM;GACvB,oBAAoB,OAAO,SAAS;GAEvC,EACD,MACD;EACF,CAAC"}
|
|
@@ -8,16 +8,13 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
8
8
|
function DefaultSection({ section, children, className, render, hideLabel, ...props }) {
|
|
9
9
|
const { classNames } = useFormConfig();
|
|
10
10
|
const hasErrors = section.groups.some((g) => g.errorCount > 0);
|
|
11
|
-
const defaultContent = /* @__PURE__ */ jsxs(Fragment, { children: [!hideLabel && section.label && /* @__PURE__ */
|
|
11
|
+
const defaultContent = /* @__PURE__ */ jsxs(Fragment, { children: [!hideLabel && section.label && /* @__PURE__ */ jsx("div", {
|
|
12
12
|
"data-p0": "section-header",
|
|
13
|
-
className: classNames?.sectionLabel ?? "pz:flex pz:items-center pz:
|
|
14
|
-
children:
|
|
15
|
-
className: cn("pz:text-[
|
|
13
|
+
className: classNames?.sectionLabel ?? "pz:flex pz:items-center pz:px-0.5 pz:mt-3 pz:select-none",
|
|
14
|
+
children: /* @__PURE__ */ jsx("h2", {
|
|
15
|
+
className: cn("pz:text-[13px] pz:font-semibold pz:tracking-[-0.006em] pz:text-foreground pz:m-0 pz:whitespace-nowrap"),
|
|
16
16
|
children: section.label
|
|
17
|
-
})
|
|
18
|
-
"aria-hidden": true,
|
|
19
|
-
className: "pz:flex-1 pz:h-px pz:bg-border"
|
|
20
|
-
})]
|
|
17
|
+
})
|
|
21
18
|
}), children] });
|
|
22
19
|
return useRender({
|
|
23
20
|
defaultTagName: "div",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"section.mjs","names":[],"sources":["../../../../src/components/defaults/layout/section.tsx"],"sourcesContent":["import { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport { useFormConfig } from \"../../../context/form-context.js\";\nimport { cn } from \"../../../lib/utils.js\";\nimport type { FormGroupHandle, FormSectionHandle } from \"../../../types/form-handle.js\";\n\nexport interface DefaultSectionState {\n groups: ReadonlyArray<FormGroupHandle>;\n hasErrors: boolean;\n}\n\nexport interface DefaultSectionProps extends useRender.ComponentProps<\"div\", DefaultSectionState> {\n section: FormSectionHandle;\n /**\n * Suppress the section's own header. Set when several consecutive sections\n * share an umbrella label so the label only renders above the first\n * section in the cluster (the renderer in `pipe-form/content.tsx` and\n * `search-form/content.tsx` handles this automatically).\n */\n hideLabel?: boolean;\n}\n\nexport function DefaultSection({\n section,\n children,\n className,\n render,\n hideLabel,\n ...props\n}: DefaultSectionProps) {\n const { classNames } = useFormConfig();\n const hasErrors = section.groups.some((g) => g.errorCount > 0);\n\n // Section descriptions are intentionally not rendered — the umbrella label\n // alone is enough signal, and per-section descriptions add noise once\n // multiple sections sit under one umbrella. Field-level descriptions cover\n // the cases where copy is actually load-bearing.\n const defaultContent = (\n <>\n {!hideLabel && section.label && (\n <div\n data-p0=\"section-header\"\n className={\n classNames?.sectionLabel
|
|
1
|
+
{"version":3,"file":"section.mjs","names":[],"sources":["../../../../src/components/defaults/layout/section.tsx"],"sourcesContent":["import { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport { useFormConfig } from \"../../../context/form-context.js\";\nimport { cn } from \"../../../lib/utils.js\";\nimport type { FormGroupHandle, FormSectionHandle } from \"../../../types/form-handle.js\";\n\nexport interface DefaultSectionState {\n groups: ReadonlyArray<FormGroupHandle>;\n hasErrors: boolean;\n}\n\nexport interface DefaultSectionProps extends useRender.ComponentProps<\"div\", DefaultSectionState> {\n section: FormSectionHandle;\n /**\n * Suppress the section's own header. Set when several consecutive sections\n * share an umbrella label so the label only renders above the first\n * section in the cluster (the renderer in `pipe-form/content.tsx` and\n * `search-form/content.tsx` handles this automatically).\n */\n hideLabel?: boolean;\n}\n\nexport function DefaultSection({\n section,\n children,\n className,\n render,\n hideLabel,\n ...props\n}: DefaultSectionProps) {\n const { classNames } = useFormConfig();\n const hasErrors = section.groups.some((g) => g.errorCount > 0);\n\n // Section descriptions are intentionally not rendered — the umbrella label\n // alone is enough signal, and per-section descriptions add noise once\n // multiple sections sit under one umbrella. Field-level descriptions cover\n // the cases where copy is actually load-bearing.\n const defaultContent = (\n <>\n {!hideLabel && section.label && (\n <div\n data-p0=\"section-header\"\n className={\n classNames?.sectionLabel ?? \"pz:flex pz:items-center pz:px-0.5 pz:mt-3 pz:select-none\"\n }\n >\n <h2\n className={cn(\n // Sentence case and semibold: a heading you read, not a tag.\n // Space above it separates sections — no trailing rule.\n \"pz:text-[13px] pz:font-semibold pz:tracking-[-0.006em] pz:text-foreground pz:m-0 pz:whitespace-nowrap\",\n )}\n >\n {section.label}\n </h2>\n </div>\n )}\n {children}\n </>\n );\n\n return useRender({\n defaultTagName: \"div\",\n render,\n state: { groups: section.groups, hasErrors },\n stateAttributesMapping: {\n groups: () => null,\n },\n props: mergeProps<\"div\">(\n {\n className: cn(classNames?.section ?? \"pz:flex pz:flex-col pz:gap-2\", className),\n children: defaultContent,\n ...({\n \"data-p0\": \"section\",\n \"data-p0-section\": section.key,\n } as Record<string, string>),\n },\n props,\n ),\n });\n}\n"],"mappings":";;;;;;;AAsBA,SAAgB,eAAe,EAC7B,SACA,UACA,WACA,QACA,WACA,GAAG,SACmB;CACtB,MAAM,EAAE,eAAe,eAAe;CACtC,MAAM,YAAY,QAAQ,OAAO,MAAM,MAAM,EAAE,aAAa,EAAE;CAM9D,MAAM,iBACJ,4CACG,CAAC,aAAa,QAAQ,SACrB,oBAAC,OAAD;EACE,WAAQ;EACR,WACE,YAAY,gBAAgB;YAG9B,oBAAC,MAAD;GACE,WAAW,GAGT,wGACD;aAEA,QAAQ;GACN;EACD,GAEP,SACA;AAGL,QAAO,UAAU;EACf,gBAAgB;EAChB;EACA,OAAO;GAAE,QAAQ,QAAQ;GAAQ;GAAW;EAC5C,wBAAwB,EACtB,cAAc,MACf;EACD,OAAO,WACL;GACE,WAAW,GAAG,YAAY,WAAW,gCAAgC,UAAU;GAC/E,UAAU;GAER,WAAW;GACX,mBAAmB,QAAQ;GAE9B,EACD,MACD;EACF,CAAC"}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Button } from "../../ui/button.mjs";
|
|
2
|
+
import { Trash2 } from "../icons.mjs";
|
|
2
3
|
import { Input } from "../../ui/input.mjs";
|
|
3
4
|
import { Switch } from "../../ui/switch.mjs";
|
|
4
5
|
import { useFloatingElement } from "../../../hooks/use-floating-element.mjs";
|
|
5
6
|
import { useEffect, useId, useMemo, useRef, useState } from "react";
|
|
6
7
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
7
8
|
import { TYPES_TO_FORMATS } from "@pipe0/base";
|
|
8
|
-
import { Trash2 } from "lucide-react";
|
|
9
9
|
import { offset, shift } from "@floating-ui/react";
|
|
10
10
|
|
|
11
11
|
//#region src/components/internal/LiquidEditor/ChipEditPopover.tsx
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChipEditPopover.mjs","names":[],"sources":["../../../../src/components/internal/LiquidEditor/ChipEditPopover.tsx"],"sourcesContent":["import { offset, shift } from \"@floating-ui/react\";\nimport { TYPES_TO_FORMATS } from \"@pipe0/base\";\nimport type { Editor } from \"@tiptap/react\";\nimport { Trash2 } from \"lucide-react\";\nimport { useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport { useFloatingElement } from \"../../../hooks/use-floating-element.js\";\nimport { Button } from \"../../ui/button.js\";\nimport { Input } from \"../../ui/input.js\";\nimport { Switch } from \"../../ui/switch.js\";\nimport type { ChipKind } from \"../tag-chip-decoration.js\";\n\nconst SIMPLE_FIELD_RE = /^\\{\\{\\s*([a-zA-Z_][a-zA-Z0-9_.]*)\\s*(\\|\\s*default:\\s*\"\")?\\s*\\}\\}$/;\nconst SECRET_RE = /^\\{\\{\\s*SECRETS\\.([A-Z][A-Z0-9_]*)\\s*\\}\\}$/;\nconst CONSTANT_RE = /^\\{\\{\\s*CONSTANTS\\.([A-Z][A-Z0-9_]*)\\s*\\}\\}$/;\n// `{% output NAME, key: \"value\", key: true, ... %}` in any argument order. The\n// args are split out by OUTPUT_ARG_RE; BlockChipBody only edits the keys it\n// knows and falls back to the raw \"advanced\" view for anything else.\nconst OUTPUT_BLOCK_RE =\n /^\\{%\\s*output\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*((?:,\\s*[a-z_]+:\\s*(?:\"(?:[^\"\\\\]|\\\\.)*\"|true|false)\\s*)*)%\\}$/;\nconst OUTPUT_ARG_RE = /([a-z_]+):\\s*(\"(?:[^\"\\\\]|\\\\.)*\"|true|false)/g;\nconst OUTPUT_KEYS = new Set([\"type\", \"schema\", \"description\", \"format\", \"required\"]);\n\ntype OutputType = \"string\" | \"number\" | \"boolean\" | \"json\" | \"unknown\";\n\nfunction parseOutputBlock(raw: string) {\n const m = raw.match(OUTPUT_BLOCK_RE);\n if (!m) return null;\n const args: Record<string, string | boolean> = {};\n for (const [, key, value] of (m[2] ?? \"\").matchAll(OUTPUT_ARG_RE)) {\n if (!key || !value || !OUTPUT_KEYS.has(key)) return null;\n args[key] =\n value === \"true\" || value === \"false\"\n ? value === \"true\"\n : value.slice(1, -1).replace(/\\\\\"/g, '\"').replace(/\\\\\\\\/g, \"\\\\\");\n }\n const type = args.type;\n if (typeof type !== \"string\" || !(type in TYPES_TO_FORMATS)) return null;\n // A hand-written format the type does not take has no option to show; keep\n // the raw view rather than silently dropping it on save.\n const format = args.format;\n if (\n typeof format === \"string\" &&\n type !== \"json\" &&\n !(TYPES_TO_FORMATS[type as OutputType] as readonly string[]).includes(format)\n ) {\n return null;\n }\n return {\n name: m[1] ?? \"\",\n type: type as OutputType,\n schema: typeof args.schema === \"string\" ? args.schema : \"\",\n description: typeof args.description === \"string\" ? args.description : \"\",\n format: typeof args.format === \"string\" ? args.format : \"\",\n required: typeof args.required === \"boolean\" ? args.required : undefined,\n };\n}\n\nexport interface ChipEditTarget {\n kind: ChipKind;\n raw: string;\n from: number;\n to: number;\n rect: DOMRect;\n}\n\n/**\n * Contextual popover anchored to a chip's DOM rect. Different content per\n * chip kind:\n * - field: switch for `| default: \"\"` (simple cases only), remove.\n * - secret: publicId header, manage/remove buttons. Power users still\n * edit by hand.\n * - block (output declaration): name / type / format / description editors.\n *\n * On any mutation, the popover dispatches a single replaceWith transaction\n * over the chip's range, then closes itself. The Liquid serialization\n * stays identical to what would have been authored by hand.\n */\nexport function ChipEditPopover({\n editor,\n target,\n onClose,\n}: {\n editor: Editor;\n target: ChipEditTarget;\n onClose: () => void;\n}) {\n const { ref, style, isMounted, getFloatingProps } = useFloatingElement(true, target.rect, 1100, {\n placement: \"bottom-start\",\n middleware: [offset(6), shift()],\n onOpenChange(open) {\n if (!open) onClose();\n },\n dismissOptions: { outsidePress: true, outsidePressEvent: \"mousedown\" },\n });\n\n const popupRef = useRef<HTMLDivElement | null>(null);\n\n useEffect(() => {\n function onKey(e: KeyboardEvent) {\n if (e.key === \"Escape\") onClose();\n }\n window.addEventListener(\"keydown\", onKey);\n return () => window.removeEventListener(\"keydown\", onKey);\n }, [onClose]);\n\n function replaceWith(next: string) {\n editor.chain().focus().insertContentAt({ from: target.from, to: target.to }, next).run();\n onClose();\n }\n\n function remove() {\n editor.chain().focus().deleteRange({ from: target.from, to: target.to }).run();\n onClose();\n }\n\n if (!isMounted) return null;\n\n return (\n <div\n ref={(el) => {\n popupRef.current = el;\n ref(el);\n }}\n style={style}\n {...getFloatingProps()}\n onMouseDown={(e) => e.stopPropagation()}\n className=\"pz:flex pz:flex-col pz:gap-2 pz:rounded-md pz:border pz:border-input pz:bg-popover pz:p-3 pz:text-popover-foreground pz:shadow-md pz:min-w-72\"\n role=\"dialog\"\n aria-label={`Edit ${target.kind} chip`}\n >\n {target.kind === \"field\" && (\n <FieldChipBody raw={target.raw} replaceWith={replaceWith} remove={remove} />\n )}\n {target.kind === \"secret\" && <SecretChipBody raw={target.raw} remove={remove} />}\n {target.kind === \"constant\" && <ConstantChipBody raw={target.raw} remove={remove} />}\n {target.kind === \"block\" && (\n <BlockChipBody raw={target.raw} replaceWith={replaceWith} remove={remove} />\n )}\n </div>\n );\n}\n\nfunction FieldChipBody({\n raw,\n replaceWith,\n remove,\n}: {\n raw: string;\n replaceWith: (next: string) => void;\n remove: () => void;\n}) {\n const match = raw.match(SIMPLE_FIELD_RE);\n const name = match?.[1];\n const allowEmpty = !!match?.[2];\n const isSimple = !!match;\n\n if (!isSimple || !name) {\n return (\n <>\n <header className=\"pz:text-xs pz:font-medium pz:text-muted-foreground\">\n Field reference (advanced)\n </header>\n <p className=\"pz:text-xs pz:text-muted-foreground\">\n This chip uses a custom Liquid filter chain. Edit the raw text in the editor to change it.\n </p>\n <pre className=\"pz:overflow-x-auto pz:rounded-sm pz:bg-muted pz:px-2 pz:py-1 pz:text-xs\">\n {raw}\n </pre>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={remove}>\n <Trash2 className=\"pz:size-3.5 pz:mr-1\" />\n Remove\n </Button>\n </>\n );\n }\n\n function toggleAllowEmpty() {\n replaceWith(allowEmpty ? `{{ ${name} }}` : `{{ ${name} | default: \"\" }}`);\n }\n\n return (\n <>\n <header className=\"pz:flex pz:items-center pz:justify-between pz:gap-2\">\n <span className=\"pz:text-xs pz:font-medium pz:text-muted-foreground\">Field reference</span>\n <code className=\"pz:text-xs pz:text-sky-700\">{name}</code>\n </header>\n <div className=\"pz:flex pz:items-center pz:justify-between pz:gap-3 pz:text-sm\">\n <span className=\"pz:flex pz:flex-col\">\n <span>Allow empty</span>\n <span className=\"pz:text-xs pz:text-muted-foreground\">\n Inserts <code>| default: \"\"</code> so missing values render as empty.\n </span>\n </span>\n <Switch checked={allowEmpty} onCheckedChange={toggleAllowEmpty} />\n </div>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={remove}>\n <Trash2 className=\"pz:size-3.5 pz:mr-1\" />\n Remove\n </Button>\n </>\n );\n}\n\nfunction SecretChipBody({ raw, remove }: { raw: string; remove: () => void }) {\n const publicId = raw.match(SECRET_RE)?.[1];\n\n return (\n <>\n <header className=\"pz:flex pz:items-center pz:justify-between pz:gap-2\">\n <span className=\"pz:text-xs pz:font-medium pz:text-muted-foreground\">Secret reference</span>\n <code className=\"pz:text-xs pz:text-amber-700\">{publicId ?? \"(unknown)\"}</code>\n </header>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={remove}>\n <Trash2 className=\"pz:size-3.5 pz:mr-1\" />\n Remove\n </Button>\n </>\n );\n}\n\nfunction ConstantChipBody({ raw, remove }: { raw: string; remove: () => void }) {\n const publicId = raw.match(CONSTANT_RE)?.[1];\n\n return (\n <>\n <header className=\"pz:flex pz:items-center pz:justify-between pz:gap-2\">\n <span className=\"pz:text-xs pz:font-medium pz:text-muted-foreground\">\n Constant reference\n </span>\n <code className=\"pz:text-xs pz:text-emerald-700\">{publicId ?? \"(unknown)\"}</code>\n </header>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={remove}>\n <Trash2 className=\"pz:size-3.5 pz:mr-1\" />\n Remove\n </Button>\n </>\n );\n}\n\nfunction BlockChipBody({\n raw,\n replaceWith,\n remove,\n}: {\n raw: string;\n replaceWith: (next: string) => void;\n remove: () => void;\n}) {\n const initial = useMemo(() => parseOutputBlock(raw), [raw]);\n\n const [name, setName] = useState(initial?.name ?? \"\");\n const [type, setType] = useState<OutputType>(initial?.type ?? \"string\");\n const [schema, setSchema] = useState(initial?.schema ?? \"\");\n const [description, setDescription] = useState(initial?.description ?? \"\");\n const [format, setFormat] = useState(initial?.format ?? \"\");\n const formatOptions: readonly string[] = TYPES_TO_FORMATS[type];\n const idPrefix = useId();\n const nameId = `${idPrefix}-name`;\n const typeId = `${idPrefix}-type`;\n const schemaId = `${idPrefix}-schema`;\n const descId = `${idPrefix}-desc`;\n const formatId = `${idPrefix}-format`;\n\n if (!initial) {\n return (\n <>\n <header className=\"pz:text-xs pz:font-medium pz:text-muted-foreground\">\n Block (advanced)\n </header>\n <p className=\"pz:text-xs pz:text-muted-foreground\">\n This block uses a non-standard form. Edit the raw text in the editor.\n </p>\n <pre className=\"pz:overflow-x-auto pz:rounded-sm pz:bg-muted pz:px-2 pz:py-1 pz:text-xs\">\n {raw}\n </pre>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={remove}>\n <Trash2 className=\"pz:size-3.5 pz:mr-1\" />\n Remove\n </Button>\n </>\n );\n }\n\n function save() {\n if (!name.trim()) return;\n const escaped = description.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"');\n const parts = [`{% output ${name.trim()}`, `type: \"${type}\"`];\n if (type === \"json\" && schema.trim()) parts.push(`schema: \"${schema.trim()}\"`);\n if (format && formatOptions.includes(format)) parts.push(`format: \"${format}\"`);\n parts.push(`description: \"${escaped}\"`);\n if (initial?.required !== undefined) parts.push(`required: ${initial.required}`);\n replaceWith(`${parts.join(\", \")} %}`);\n }\n\n return (\n <>\n <header className=\"pz:text-xs pz:font-medium pz:text-muted-foreground\">\n Output declaration\n </header>\n <label htmlFor={nameId} className=\"pz:flex pz:flex-col pz:gap-1 pz:text-xs\">\n <span>Name</span>\n <Input\n id={nameId}\n value={name}\n onChange={(e) => setName(e.target.value)}\n placeholder=\"FIELD_NAME\"\n />\n </label>\n <label htmlFor={typeId} className=\"pz:flex pz:flex-col pz:gap-1 pz:text-xs\">\n <span>Type</span>\n <select\n id={typeId}\n value={type}\n onChange={(e) => {\n const next = e.target.value as OutputType;\n setType(next);\n // A format that the new type does not take would fail validation.\n if (!(TYPES_TO_FORMATS[next] as readonly string[]).includes(format)) setFormat(\"\");\n }}\n className=\"pz:rounded-sm pz:border pz:border-input pz:bg-transparent pz:px-2 pz:py-1 pz:text-sm\"\n >\n <option value=\"string\">string</option>\n <option value=\"number\">number</option>\n <option value=\"boolean\">boolean</option>\n <option value=\"json\">json</option>\n <option value=\"unknown\">unknown</option>\n </select>\n </label>\n {type === \"json\" && (\n <label htmlFor={schemaId} className=\"pz:flex pz:flex-col pz:gap-1 pz:text-xs\">\n <span>Schema</span>\n <Input\n id={schemaId}\n value={schema}\n onChange={(e) => setSchema(e.target.value)}\n placeholder=\"SCHEMA_NAME\"\n />\n </label>\n )}\n {type !== \"json\" && formatOptions.length > 0 && (\n <label htmlFor={formatId} className=\"pz:flex pz:flex-col pz:gap-1 pz:text-xs\">\n <span>Format</span>\n <select\n id={formatId}\n value={format}\n onChange={(e) => setFormat(e.target.value)}\n className=\"pz:rounded-sm pz:border pz:border-input pz:bg-transparent pz:px-2 pz:py-1 pz:text-sm\"\n >\n <option value=\"\">none</option>\n {formatOptions.map((f) => (\n <option key={f} value={f}>\n {f}\n </option>\n ))}\n </select>\n </label>\n )}\n <label htmlFor={descId} className=\"pz:flex pz:flex-col pz:gap-1 pz:text-xs\">\n <span>Description</span>\n <Input id={descId} value={description} onChange={(e) => setDescription(e.target.value)} />\n </label>\n <div className=\"pz:flex pz:items-center pz:justify-between pz:gap-2 pz:pt-1\">\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={remove}>\n <Trash2 className=\"pz:size-3.5 pz:mr-1\" />\n Remove\n </Button>\n <Button type=\"button\" size=\"sm\" onClick={save}>\n Save\n </Button>\n </div>\n </>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ChipEditPopover.mjs","names":[],"sources":["../../../../src/components/internal/LiquidEditor/ChipEditPopover.tsx"],"sourcesContent":["import { offset, shift } from \"@floating-ui/react\";\nimport { TYPES_TO_FORMATS } from \"@pipe0/base\";\nimport type { Editor } from \"@tiptap/react\";\nimport { useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport { useFloatingElement } from \"../../../hooks/use-floating-element.js\";\nimport { Button } from \"../../ui/button.js\";\nimport { Input } from \"../../ui/input.js\";\nimport { Switch } from \"../../ui/switch.js\";\nimport { Trash2 } from \"../icons.js\";\nimport type { ChipKind } from \"../tag-chip-decoration.js\";\n\nconst SIMPLE_FIELD_RE = /^\\{\\{\\s*([a-zA-Z_][a-zA-Z0-9_.]*)\\s*(\\|\\s*default:\\s*\"\")?\\s*\\}\\}$/;\nconst SECRET_RE = /^\\{\\{\\s*SECRETS\\.([A-Z][A-Z0-9_]*)\\s*\\}\\}$/;\nconst CONSTANT_RE = /^\\{\\{\\s*CONSTANTS\\.([A-Z][A-Z0-9_]*)\\s*\\}\\}$/;\n// `{% output NAME, key: \"value\", key: true, ... %}` in any argument order. The\n// args are split out by OUTPUT_ARG_RE; BlockChipBody only edits the keys it\n// knows and falls back to the raw \"advanced\" view for anything else.\nconst OUTPUT_BLOCK_RE =\n /^\\{%\\s*output\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*((?:,\\s*[a-z_]+:\\s*(?:\"(?:[^\"\\\\]|\\\\.)*\"|true|false)\\s*)*)%\\}$/;\nconst OUTPUT_ARG_RE = /([a-z_]+):\\s*(\"(?:[^\"\\\\]|\\\\.)*\"|true|false)/g;\nconst OUTPUT_KEYS = new Set([\"type\", \"schema\", \"description\", \"format\", \"required\"]);\n\ntype OutputType = \"string\" | \"number\" | \"boolean\" | \"json\" | \"unknown\";\n\nfunction parseOutputBlock(raw: string) {\n const m = raw.match(OUTPUT_BLOCK_RE);\n if (!m) return null;\n const args: Record<string, string | boolean> = {};\n for (const [, key, value] of (m[2] ?? \"\").matchAll(OUTPUT_ARG_RE)) {\n if (!key || !value || !OUTPUT_KEYS.has(key)) return null;\n args[key] =\n value === \"true\" || value === \"false\"\n ? value === \"true\"\n : value.slice(1, -1).replace(/\\\\\"/g, '\"').replace(/\\\\\\\\/g, \"\\\\\");\n }\n const type = args.type;\n if (typeof type !== \"string\" || !(type in TYPES_TO_FORMATS)) return null;\n // A hand-written format the type does not take has no option to show; keep\n // the raw view rather than silently dropping it on save.\n const format = args.format;\n if (\n typeof format === \"string\" &&\n type !== \"json\" &&\n !(TYPES_TO_FORMATS[type as OutputType] as readonly string[]).includes(format)\n ) {\n return null;\n }\n return {\n name: m[1] ?? \"\",\n type: type as OutputType,\n schema: typeof args.schema === \"string\" ? args.schema : \"\",\n description: typeof args.description === \"string\" ? args.description : \"\",\n format: typeof args.format === \"string\" ? args.format : \"\",\n required: typeof args.required === \"boolean\" ? args.required : undefined,\n };\n}\n\nexport interface ChipEditTarget {\n kind: ChipKind;\n raw: string;\n from: number;\n to: number;\n rect: DOMRect;\n}\n\n/**\n * Contextual popover anchored to a chip's DOM rect. Different content per\n * chip kind:\n * - field: switch for `| default: \"\"` (simple cases only), remove.\n * - secret: publicId header, manage/remove buttons. Power users still\n * edit by hand.\n * - block (output declaration): name / type / format / description editors.\n *\n * On any mutation, the popover dispatches a single replaceWith transaction\n * over the chip's range, then closes itself. The Liquid serialization\n * stays identical to what would have been authored by hand.\n */\nexport function ChipEditPopover({\n editor,\n target,\n onClose,\n}: {\n editor: Editor;\n target: ChipEditTarget;\n onClose: () => void;\n}) {\n const { ref, style, isMounted, getFloatingProps } = useFloatingElement(true, target.rect, 1100, {\n placement: \"bottom-start\",\n middleware: [offset(6), shift()],\n onOpenChange(open) {\n if (!open) onClose();\n },\n dismissOptions: { outsidePress: true, outsidePressEvent: \"mousedown\" },\n });\n\n const popupRef = useRef<HTMLDivElement | null>(null);\n\n useEffect(() => {\n function onKey(e: KeyboardEvent) {\n if (e.key === \"Escape\") onClose();\n }\n window.addEventListener(\"keydown\", onKey);\n return () => window.removeEventListener(\"keydown\", onKey);\n }, [onClose]);\n\n function replaceWith(next: string) {\n editor.chain().focus().insertContentAt({ from: target.from, to: target.to }, next).run();\n onClose();\n }\n\n function remove() {\n editor.chain().focus().deleteRange({ from: target.from, to: target.to }).run();\n onClose();\n }\n\n if (!isMounted) return null;\n\n return (\n <div\n ref={(el) => {\n popupRef.current = el;\n ref(el);\n }}\n style={style}\n {...getFloatingProps()}\n onMouseDown={(e) => e.stopPropagation()}\n className=\"pz:flex pz:flex-col pz:gap-2 pz:rounded-md pz:border pz:border-input pz:bg-popover pz:p-3 pz:text-popover-foreground pz:shadow-md pz:min-w-72\"\n role=\"dialog\"\n aria-label={`Edit ${target.kind} chip`}\n >\n {target.kind === \"field\" && (\n <FieldChipBody raw={target.raw} replaceWith={replaceWith} remove={remove} />\n )}\n {target.kind === \"secret\" && <SecretChipBody raw={target.raw} remove={remove} />}\n {target.kind === \"constant\" && <ConstantChipBody raw={target.raw} remove={remove} />}\n {target.kind === \"block\" && (\n <BlockChipBody raw={target.raw} replaceWith={replaceWith} remove={remove} />\n )}\n </div>\n );\n}\n\nfunction FieldChipBody({\n raw,\n replaceWith,\n remove,\n}: {\n raw: string;\n replaceWith: (next: string) => void;\n remove: () => void;\n}) {\n const match = raw.match(SIMPLE_FIELD_RE);\n const name = match?.[1];\n const allowEmpty = !!match?.[2];\n const isSimple = !!match;\n\n if (!isSimple || !name) {\n return (\n <>\n <header className=\"pz:text-xs pz:font-medium pz:text-muted-foreground\">\n Field reference (advanced)\n </header>\n <p className=\"pz:text-xs pz:text-muted-foreground\">\n This chip uses a custom Liquid filter chain. Edit the raw text in the editor to change it.\n </p>\n <pre className=\"pz:overflow-x-auto pz:rounded-sm pz:bg-muted pz:px-2 pz:py-1 pz:text-xs\">\n {raw}\n </pre>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={remove}>\n <Trash2 className=\"pz:size-3.5 pz:mr-1\" />\n Remove\n </Button>\n </>\n );\n }\n\n function toggleAllowEmpty() {\n replaceWith(allowEmpty ? `{{ ${name} }}` : `{{ ${name} | default: \"\" }}`);\n }\n\n return (\n <>\n <header className=\"pz:flex pz:items-center pz:justify-between pz:gap-2\">\n <span className=\"pz:text-xs pz:font-medium pz:text-muted-foreground\">Field reference</span>\n <code className=\"pz:text-xs pz:text-sky-700\">{name}</code>\n </header>\n <div className=\"pz:flex pz:items-center pz:justify-between pz:gap-3 pz:text-sm\">\n <span className=\"pz:flex pz:flex-col\">\n <span>Allow empty</span>\n <span className=\"pz:text-xs pz:text-muted-foreground\">\n Inserts <code>| default: \"\"</code> so missing values render as empty.\n </span>\n </span>\n <Switch checked={allowEmpty} onCheckedChange={toggleAllowEmpty} />\n </div>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={remove}>\n <Trash2 className=\"pz:size-3.5 pz:mr-1\" />\n Remove\n </Button>\n </>\n );\n}\n\nfunction SecretChipBody({ raw, remove }: { raw: string; remove: () => void }) {\n const publicId = raw.match(SECRET_RE)?.[1];\n\n return (\n <>\n <header className=\"pz:flex pz:items-center pz:justify-between pz:gap-2\">\n <span className=\"pz:text-xs pz:font-medium pz:text-muted-foreground\">Secret reference</span>\n <code className=\"pz:text-xs pz:text-amber-700\">{publicId ?? \"(unknown)\"}</code>\n </header>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={remove}>\n <Trash2 className=\"pz:size-3.5 pz:mr-1\" />\n Remove\n </Button>\n </>\n );\n}\n\nfunction ConstantChipBody({ raw, remove }: { raw: string; remove: () => void }) {\n const publicId = raw.match(CONSTANT_RE)?.[1];\n\n return (\n <>\n <header className=\"pz:flex pz:items-center pz:justify-between pz:gap-2\">\n <span className=\"pz:text-xs pz:font-medium pz:text-muted-foreground\">\n Constant reference\n </span>\n <code className=\"pz:text-xs pz:text-emerald-700\">{publicId ?? \"(unknown)\"}</code>\n </header>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={remove}>\n <Trash2 className=\"pz:size-3.5 pz:mr-1\" />\n Remove\n </Button>\n </>\n );\n}\n\nfunction BlockChipBody({\n raw,\n replaceWith,\n remove,\n}: {\n raw: string;\n replaceWith: (next: string) => void;\n remove: () => void;\n}) {\n const initial = useMemo(() => parseOutputBlock(raw), [raw]);\n\n const [name, setName] = useState(initial?.name ?? \"\");\n const [type, setType] = useState<OutputType>(initial?.type ?? \"string\");\n const [schema, setSchema] = useState(initial?.schema ?? \"\");\n const [description, setDescription] = useState(initial?.description ?? \"\");\n const [format, setFormat] = useState(initial?.format ?? \"\");\n const formatOptions: readonly string[] = TYPES_TO_FORMATS[type];\n const idPrefix = useId();\n const nameId = `${idPrefix}-name`;\n const typeId = `${idPrefix}-type`;\n const schemaId = `${idPrefix}-schema`;\n const descId = `${idPrefix}-desc`;\n const formatId = `${idPrefix}-format`;\n\n if (!initial) {\n return (\n <>\n <header className=\"pz:text-xs pz:font-medium pz:text-muted-foreground\">\n Block (advanced)\n </header>\n <p className=\"pz:text-xs pz:text-muted-foreground\">\n This block uses a non-standard form. Edit the raw text in the editor.\n </p>\n <pre className=\"pz:overflow-x-auto pz:rounded-sm pz:bg-muted pz:px-2 pz:py-1 pz:text-xs\">\n {raw}\n </pre>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={remove}>\n <Trash2 className=\"pz:size-3.5 pz:mr-1\" />\n Remove\n </Button>\n </>\n );\n }\n\n function save() {\n if (!name.trim()) return;\n const escaped = description.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"');\n const parts = [`{% output ${name.trim()}`, `type: \"${type}\"`];\n if (type === \"json\" && schema.trim()) parts.push(`schema: \"${schema.trim()}\"`);\n if (format && formatOptions.includes(format)) parts.push(`format: \"${format}\"`);\n parts.push(`description: \"${escaped}\"`);\n if (initial?.required !== undefined) parts.push(`required: ${initial.required}`);\n replaceWith(`${parts.join(\", \")} %}`);\n }\n\n return (\n <>\n <header className=\"pz:text-xs pz:font-medium pz:text-muted-foreground\">\n Output declaration\n </header>\n <label htmlFor={nameId} className=\"pz:flex pz:flex-col pz:gap-1 pz:text-xs\">\n <span>Name</span>\n <Input\n id={nameId}\n value={name}\n onChange={(e) => setName(e.target.value)}\n placeholder=\"FIELD_NAME\"\n />\n </label>\n <label htmlFor={typeId} className=\"pz:flex pz:flex-col pz:gap-1 pz:text-xs\">\n <span>Type</span>\n <select\n id={typeId}\n value={type}\n onChange={(e) => {\n const next = e.target.value as OutputType;\n setType(next);\n // A format that the new type does not take would fail validation.\n if (!(TYPES_TO_FORMATS[next] as readonly string[]).includes(format)) setFormat(\"\");\n }}\n className=\"pz:rounded-sm pz:border pz:border-input pz:bg-transparent pz:px-2 pz:py-1 pz:text-sm\"\n >\n <option value=\"string\">string</option>\n <option value=\"number\">number</option>\n <option value=\"boolean\">boolean</option>\n <option value=\"json\">json</option>\n <option value=\"unknown\">unknown</option>\n </select>\n </label>\n {type === \"json\" && (\n <label htmlFor={schemaId} className=\"pz:flex pz:flex-col pz:gap-1 pz:text-xs\">\n <span>Schema</span>\n <Input\n id={schemaId}\n value={schema}\n onChange={(e) => setSchema(e.target.value)}\n placeholder=\"SCHEMA_NAME\"\n />\n </label>\n )}\n {type !== \"json\" && formatOptions.length > 0 && (\n <label htmlFor={formatId} className=\"pz:flex pz:flex-col pz:gap-1 pz:text-xs\">\n <span>Format</span>\n <select\n id={formatId}\n value={format}\n onChange={(e) => setFormat(e.target.value)}\n className=\"pz:rounded-sm pz:border pz:border-input pz:bg-transparent pz:px-2 pz:py-1 pz:text-sm\"\n >\n <option value=\"\">none</option>\n {formatOptions.map((f) => (\n <option key={f} value={f}>\n {f}\n </option>\n ))}\n </select>\n </label>\n )}\n <label htmlFor={descId} className=\"pz:flex pz:flex-col pz:gap-1 pz:text-xs\">\n <span>Description</span>\n <Input id={descId} value={description} onChange={(e) => setDescription(e.target.value)} />\n </label>\n <div className=\"pz:flex pz:items-center pz:justify-between pz:gap-2 pz:pt-1\">\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={remove}>\n <Trash2 className=\"pz:size-3.5 pz:mr-1\" />\n Remove\n </Button>\n <Button type=\"button\" size=\"sm\" onClick={save}>\n Save\n </Button>\n </div>\n </>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UnifiedReferencePicker.mjs","names":[],"sources":["../../../../src/components/internal/LiquidEditor/UnifiedReferencePicker.tsx"],"sourcesContent":["import type { PipesFieldDefinitionWithName } from \"@pipe0/base\";\nimport { Hash, Key } from \"lucide-react\";\nimport type { ConstantSuggestion, SecretSuggestion } from \"../../../types/field-props.js\";\nimport type {\n SuggestionItem,\n SuggestionMenuRenderProps,\n} from \"../suggestion-menu/suggestion-menu-types.js\";\n\nexport const SECTION_CAP = 5;\n\nexport type ReferenceKind = \"field\" | \"secret\" | \"constant\";\n\nexport interface ReferenceContext {\n kind: ReferenceKind;\n}\n\nexport interface ReferenceSources {\n inputFields: PipesFieldDefinitionWithName[];\n secretSuggestions: SecretSuggestion[];\n constantSuggestions: ConstantSuggestion[];\n}\n\nconst PREFIX_RE = /^([fsc])\\/(.*)$/;\n\n/**\n * Parses a typed query into `(prefix, residual)`. Lets users filter\n * directly to a single source: `f/email`, `s/STRIPE`, `c/REGION`.\n * Without a prefix, all sources are searched.\n */\nexport function parseQuery(query: string): {\n prefix: ReferenceKind | null;\n residual: string;\n} {\n const m = query.match(PREFIX_RE);\n if (!m) return { prefix: null, residual: query };\n const map = { f: \"field\", s: \"secret\", c: \"constant\" } as const;\n return { prefix: map[m[1] as \"f\" | \"s\" | \"c\"], residual: m[2] };\n}\n\nfunction score(haystack: string, needle: string): number {\n if (!needle) return 1;\n const h = haystack.toLowerCase();\n const n = needle.toLowerCase();\n if (h === n) return 100;\n if (h.startsWith(n)) return 50;\n if (h.includes(n)) return 10;\n return 0;\n}\n\nfunction buildFieldItem(\n f: PipesFieldDefinitionWithName,\n insertField: (name: string) => string,\n): SuggestionItem<ReferenceContext> {\n return {\n title: f.resolvedName,\n subtext: f.type,\n keywords: [f.type],\n context: { kind: \"field\" },\n onSelect: ({ editor, range }) => {\n editor.chain().focus().insertContentAt(range, insertField(f.resolvedName)).run();\n },\n };\n}\n\nfunction buildSecretItem(s: SecretSuggestion): SuggestionItem<ReferenceContext> {\n return {\n title: s.label,\n subtext: `${s.mask} · ${s.ownershipLevel}${s.providerHint ? ` · ${s.providerHint}` : \"\"}`,\n keywords: [s.publicId, s.providerHint ?? \"\"],\n context: { kind: \"secret\" },\n onSelect: ({ editor, range }) => {\n editor.chain().focus().insertContentAt(range, `{{ SECRETS.${s.publicId} }}`).run();\n },\n };\n}\n\nfunction buildConstantItem(c: ConstantSuggestion): SuggestionItem<ReferenceContext> {\n return {\n title: c.label,\n // Flag multi-line constants — the reference is a single chip here but\n // expands to several lines at render time. A hint only; inserting one into\n // a single-line field is allowed.\n subtext: c.format === \"text\" ? `${c.ownershipLevel} · multi-line` : c.ownershipLevel,\n keywords: [c.publicId],\n context: { kind: \"constant\" },\n onSelect: ({ editor, range }) => {\n editor.chain().focus().insertContentAt(range, `{{ CONSTANTS.${c.publicId} }}`).run();\n },\n };\n}\n\n/**\n * Builds the suggestion list for a given query against the three sources.\n * Behavior:\n * - empty query → returns all items in source order (Fields, then\n * Secrets, then Constants). Header rows are rendered separately by\n * the picker UI — this function returns a flat list of items only.\n * - non-empty query → ranks across all sources with a simple\n * starts-with > includes scoring. Items with a zero score are\n * dropped.\n * - prefix syntax `f/`, `s/`, `c/` filters to a single source before\n * scoring with the residual.\n */\nexport function buildReferenceItems(\n query: string,\n sources: ReferenceSources,\n expectedFieldType: string | undefined,\n insertField: (name: string) => string,\n): SuggestionItem<ReferenceContext>[] {\n const { prefix, residual } = parseQuery(query);\n\n const fieldsAll = expectedFieldType\n ? sources.inputFields.filter((f) => f.type === expectedFieldType)\n : sources.inputFields;\n\n const fieldItems = fieldsAll.map((f) => buildFieldItem(f, insertField));\n const secretItems = sources.secretSuggestions.map(buildSecretItem);\n const constantItems = sources.constantSuggestions.map(buildConstantItem);\n\n const pool: SuggestionItem<ReferenceContext>[] =\n prefix === \"field\"\n ? fieldItems\n : prefix === \"secret\"\n ? secretItems\n : prefix === \"constant\"\n ? constantItems\n : [...fieldItems, ...secretItems, ...constantItems];\n\n if (!residual.trim()) return pool;\n\n return pool\n .map((item) => {\n const titleScore = score(item.title, residual);\n const keywordScore = (item.keywords ?? []).reduce(\n (best, kw) => Math.max(best, score(kw, residual)),\n 0,\n );\n const subtextScore = item.subtext ? score(item.subtext, residual) * 0.5 : 0;\n const total = Math.max(titleScore, keywordScore, subtextScore);\n return { item, total };\n })\n .filter(({ total }) => total > 0)\n .sort((a, b) => b.total - a.total)\n .map(({ item }) => item);\n}\n\nfunction KindIcon({ kind }: { kind: ReferenceKind }) {\n // Fields are the default category and don't need a marker — the section\n // header and the type column on the right convey the kind.\n if (kind === \"field\") return null;\n if (kind === \"secret\") return <Key className=\"pz:size-3.5 pz:text-amber-700\" aria-hidden />;\n return <Hash className=\"pz:size-3.5 pz:text-violet-700\" aria-hidden />;\n}\n\nfunction SectionHeader({ children }: { children: React.ReactNode }) {\n return (\n <div className=\"pz:px-2 pz:pt-1.5 pz:pb-0.5 pz:text-[10px] pz:font-medium pz:uppercase pz:tracking-wide pz:text-muted-foreground\">\n {children}\n </div>\n );\n}\n\nfunction ItemRow({\n item,\n active,\n onSelect,\n}: {\n item: SuggestionItem<ReferenceContext>;\n active: boolean;\n onSelect: (item: SuggestionItem<ReferenceContext>) => void;\n}) {\n return (\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={active}\n onClick={() => onSelect(item)}\n className={[\n \"pz:flex pz:w-full pz:items-center pz:gap-2 pz:rounded-sm pz:px-2 pz:py-1 pz:text-sm pz:text-left pz:cursor-pointer\",\n active\n ? \"pz:bg-accent pz:text-accent-foreground\"\n : \"pz:hover:bg-accent pz:hover:text-accent-foreground\",\n ].join(\" \")}\n >\n <KindIcon kind={item.context?.kind ?? \"field\"} />\n <span className=\"pz:truncate\">{item.title}</span>\n {item.subtext && (\n <span className=\"pz:ml-auto pz:shrink-0 pz:truncate pz:text-xs pz:text-muted-foreground\">\n {item.subtext}\n </span>\n )}\n </button>\n );\n}\n\n/**\n * Picker UI rendered inside the SuggestionMenu's children render-prop.\n *\n * - Empty query → sectioned view (Fields, Secrets, Constants).\n * - Non-empty query → flat ranked list.\n *\n * The selected-index pointer from `SuggestionMenuRenderProps` always maps\n * to the flat `items` order. Sectioned mode uses the same flat order but\n * inserts headers between the boundaries, so highlight tracking still\n * works without bespoke navigation.\n */\nexport interface OverflowCounts {\n field: number;\n secret: number;\n constant: number;\n}\n\nexport function UnifiedReferencePicker({\n items,\n selectedIndex,\n onSelect,\n query,\n hasSecretsResolver,\n hasConstantsResolver,\n inputFieldsCount,\n secretsPending,\n constantsPending,\n overflow,\n}: SuggestionMenuRenderProps<ReferenceContext> & {\n query: string;\n hasSecretsResolver: boolean;\n hasConstantsResolver: boolean;\n inputFieldsCount: number;\n secretsPending: boolean;\n constantsPending: boolean;\n overflow: OverflowCounts;\n}) {\n const { prefix, residual } = parseQuery(query);\n const isFlat = !!residual.trim() || prefix !== null;\n\n if (isFlat) {\n if (items.length === 0) {\n const isPendingForPrefix =\n (prefix === \"secret\" && secretsPending) ||\n (prefix === \"constant\" && constantsPending) ||\n (prefix === null && (secretsPending || constantsPending));\n return (\n <div className=\"pz:flex pz:flex-col pz:gap-0.5 pz:p-1 pz:min-w-72\" role=\"listbox\">\n {prefix && (\n <SectionHeader>\n {prefix === \"field\" ? \"Fields\" : prefix === \"secret\" ? \"Secrets\" : \"Constants\"}\n </SectionHeader>\n )}\n <div className=\"pz:px-2 pz:py-1.5 pz:text-sm pz:text-muted-foreground\">\n {isPendingForPrefix ? \"Searching…\" : describeEmpty(prefix)}\n </div>\n </div>\n );\n }\n return (\n <div className=\"pz:flex pz:flex-col pz:gap-0.5 pz:p-1 pz:min-w-72\" role=\"listbox\">\n {prefix && (\n <SectionHeader>\n {prefix === \"field\" ? \"Fields\" : prefix === \"secret\" ? \"Secrets\" : \"Constants\"}\n </SectionHeader>\n )}\n {items.map((item, index) => (\n <ItemRow\n key={`${item.context?.kind}-${item.title}`}\n item={item}\n active={selectedIndex === index}\n onSelect={onSelect}\n />\n ))}\n </div>\n );\n }\n\n // Sectioned view — always render Fields and Secrets headers in fixed\n // order, with empty-state hints when a section has no items. Constants\n // only appear when there are any to show.\n //\n // Items here are pre-capped per kind (LiquidEditor applies SECTION_CAP).\n // The selectedIndex maps to this capped flat order — no hidden rows.\n const fieldItems = items.filter((it) => it.context?.kind === \"field\");\n const secretItems = items.filter((it) => it.context?.kind === \"secret\");\n const constantItems = items.filter((it) => it.context?.kind === \"constant\");\n\n return (\n <div className=\"pz:flex pz:flex-col pz:gap-0.5 pz:p-1 pz:min-w-72\" role=\"listbox\">\n <Section\n label=\"Fields\"\n kind=\"field\"\n items={fieldItems}\n startIndex={0}\n selectedIndex={selectedIndex}\n onSelect={onSelect}\n emptyHint={\n inputFieldsCount === 0 ? \"No input fields available.\" : \"No matching input fields.\"\n }\n overflow={overflow.field}\n overflowHelper=\"Type to filter.\"\n />\n {hasSecretsResolver && (\n <Section\n label=\"Secrets\"\n kind=\"secret\"\n items={secretItems}\n startIndex={fieldItems.length}\n selectedIndex={selectedIndex}\n onSelect={onSelect}\n emptyHint={secretsPending ? \"Searching…\" : \"No matching secrets.\"}\n helperHint=\"Type /s/ to filter to secrets only.\"\n pending={secretsPending}\n overflow={overflow.secret}\n overflowHelper=\"Type to filter, or /s/ for secrets only.\"\n />\n )}\n {hasConstantsResolver && (\n <Section\n label=\"Constants\"\n kind=\"constant\"\n items={constantItems}\n startIndex={fieldItems.length + secretItems.length}\n selectedIndex={selectedIndex}\n onSelect={onSelect}\n emptyHint={constantsPending ? \"Searching…\" : \"No matching constants.\"}\n helperHint=\"Type /c/ to filter to constants only.\"\n pending={constantsPending}\n overflow={overflow.constant}\n overflowHelper=\"Type to filter, or /c/ for constants only.\"\n />\n )}\n </div>\n );\n}\n\nfunction Section({\n label,\n kind,\n items,\n startIndex,\n selectedIndex,\n onSelect,\n emptyHint,\n helperHint,\n pending,\n overflow = 0,\n overflowHelper,\n}: {\n label: string;\n kind: ReferenceKind;\n items: SuggestionItem<ReferenceContext>[];\n startIndex: number;\n selectedIndex?: number;\n onSelect: (item: SuggestionItem<ReferenceContext>) => void;\n emptyHint: string;\n helperHint?: string;\n pending?: boolean;\n overflow?: number;\n overflowHelper?: string;\n}) {\n return (\n <div className=\"pz:flex pz:flex-col\">\n <SectionHeader>\n {label}\n {items.length > 0 && (\n <span className=\"pz:ml-1 pz:normal-case pz:tracking-normal\">\n ({items.length}\n {overflow > 0 && ` of ${items.length + overflow}`})\n </span>\n )}\n {pending && (\n <span className=\"pz:ml-1 pz:normal-case pz:tracking-normal pz:opacity-70\">\n · Searching…\n </span>\n )}\n </SectionHeader>\n {items.length === 0 ? (\n <div className=\"pz:px-2 pz:py-1 pz:text-xs pz:text-muted-foreground\">\n {emptyHint}\n {helperHint && <span className=\"pz:block pz:opacity-70\">{helperHint}</span>}\n </div>\n ) : (\n <>\n {items.map((item, i) => (\n <ItemRow\n key={`${kind}-${item.title}`}\n item={item}\n active={selectedIndex === startIndex + i}\n onSelect={onSelect}\n />\n ))}\n {overflow > 0 && overflowHelper && (\n <div className=\"pz:px-2 pz:py-0.5 pz:text-[11px] pz:text-muted-foreground pz:opacity-70\">\n +{overflow} more — {overflowHelper}\n </div>\n )}\n </>\n )}\n </div>\n );\n}\n\nfunction describeEmpty(prefix: ReferenceKind | null): string {\n if (prefix === \"secret\") return \"No matching secrets.\";\n if (prefix === \"constant\") return \"No matching constants.\";\n if (prefix === \"field\") return \"No matching fields.\";\n return \"No matches.\";\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"UnifiedReferencePicker.mjs","names":[],"sources":["../../../../src/components/internal/LiquidEditor/UnifiedReferencePicker.tsx"],"sourcesContent":["import type { PipesFieldDefinitionWithName } from \"@pipe0/base\";\nimport type { ConstantSuggestion, SecretSuggestion } from \"../../../types/field-props.js\";\nimport { Hash, Key } from \"../icons.js\";\nimport type {\n SuggestionItem,\n SuggestionMenuRenderProps,\n} from \"../suggestion-menu/suggestion-menu-types.js\";\n\nexport const SECTION_CAP = 5;\n\nexport type ReferenceKind = \"field\" | \"secret\" | \"constant\";\n\nexport interface ReferenceContext {\n kind: ReferenceKind;\n}\n\nexport interface ReferenceSources {\n inputFields: PipesFieldDefinitionWithName[];\n secretSuggestions: SecretSuggestion[];\n constantSuggestions: ConstantSuggestion[];\n}\n\nconst PREFIX_RE = /^([fsc])\\/(.*)$/;\n\n/**\n * Parses a typed query into `(prefix, residual)`. Lets users filter\n * directly to a single source: `f/email`, `s/STRIPE`, `c/REGION`.\n * Without a prefix, all sources are searched.\n */\nexport function parseQuery(query: string): {\n prefix: ReferenceKind | null;\n residual: string;\n} {\n const m = query.match(PREFIX_RE);\n if (!m) return { prefix: null, residual: query };\n const map = { f: \"field\", s: \"secret\", c: \"constant\" } as const;\n return { prefix: map[m[1] as \"f\" | \"s\" | \"c\"], residual: m[2] };\n}\n\nfunction score(haystack: string, needle: string): number {\n if (!needle) return 1;\n const h = haystack.toLowerCase();\n const n = needle.toLowerCase();\n if (h === n) return 100;\n if (h.startsWith(n)) return 50;\n if (h.includes(n)) return 10;\n return 0;\n}\n\nfunction buildFieldItem(\n f: PipesFieldDefinitionWithName,\n insertField: (name: string) => string,\n): SuggestionItem<ReferenceContext> {\n return {\n title: f.resolvedName,\n subtext: f.type,\n keywords: [f.type],\n context: { kind: \"field\" },\n onSelect: ({ editor, range }) => {\n editor.chain().focus().insertContentAt(range, insertField(f.resolvedName)).run();\n },\n };\n}\n\nfunction buildSecretItem(s: SecretSuggestion): SuggestionItem<ReferenceContext> {\n return {\n title: s.label,\n subtext: `${s.mask} · ${s.ownershipLevel}${s.providerHint ? ` · ${s.providerHint}` : \"\"}`,\n keywords: [s.publicId, s.providerHint ?? \"\"],\n context: { kind: \"secret\" },\n onSelect: ({ editor, range }) => {\n editor.chain().focus().insertContentAt(range, `{{ SECRETS.${s.publicId} }}`).run();\n },\n };\n}\n\nfunction buildConstantItem(c: ConstantSuggestion): SuggestionItem<ReferenceContext> {\n return {\n title: c.label,\n // Flag multi-line constants — the reference is a single chip here but\n // expands to several lines at render time. A hint only; inserting one into\n // a single-line field is allowed.\n subtext: c.format === \"text\" ? `${c.ownershipLevel} · multi-line` : c.ownershipLevel,\n keywords: [c.publicId],\n context: { kind: \"constant\" },\n onSelect: ({ editor, range }) => {\n editor.chain().focus().insertContentAt(range, `{{ CONSTANTS.${c.publicId} }}`).run();\n },\n };\n}\n\n/**\n * Builds the suggestion list for a given query against the three sources.\n * Behavior:\n * - empty query → returns all items in source order (Fields, then\n * Secrets, then Constants). Header rows are rendered separately by\n * the picker UI — this function returns a flat list of items only.\n * - non-empty query → ranks across all sources with a simple\n * starts-with > includes scoring. Items with a zero score are\n * dropped.\n * - prefix syntax `f/`, `s/`, `c/` filters to a single source before\n * scoring with the residual.\n */\nexport function buildReferenceItems(\n query: string,\n sources: ReferenceSources,\n expectedFieldType: string | undefined,\n insertField: (name: string) => string,\n): SuggestionItem<ReferenceContext>[] {\n const { prefix, residual } = parseQuery(query);\n\n const fieldsAll = expectedFieldType\n ? sources.inputFields.filter((f) => f.type === expectedFieldType)\n : sources.inputFields;\n\n const fieldItems = fieldsAll.map((f) => buildFieldItem(f, insertField));\n const secretItems = sources.secretSuggestions.map(buildSecretItem);\n const constantItems = sources.constantSuggestions.map(buildConstantItem);\n\n const pool: SuggestionItem<ReferenceContext>[] =\n prefix === \"field\"\n ? fieldItems\n : prefix === \"secret\"\n ? secretItems\n : prefix === \"constant\"\n ? constantItems\n : [...fieldItems, ...secretItems, ...constantItems];\n\n if (!residual.trim()) return pool;\n\n return pool\n .map((item) => {\n const titleScore = score(item.title, residual);\n const keywordScore = (item.keywords ?? []).reduce(\n (best, kw) => Math.max(best, score(kw, residual)),\n 0,\n );\n const subtextScore = item.subtext ? score(item.subtext, residual) * 0.5 : 0;\n const total = Math.max(titleScore, keywordScore, subtextScore);\n return { item, total };\n })\n .filter(({ total }) => total > 0)\n .sort((a, b) => b.total - a.total)\n .map(({ item }) => item);\n}\n\nfunction KindIcon({ kind }: { kind: ReferenceKind }) {\n // Fields are the default category and don't need a marker — the section\n // header and the type column on the right convey the kind.\n if (kind === \"field\") return null;\n if (kind === \"secret\") return <Key className=\"pz:size-3.5 pz:text-amber-700\" aria-hidden />;\n return <Hash className=\"pz:size-3.5 pz:text-violet-700\" aria-hidden />;\n}\n\nfunction SectionHeader({ children }: { children: React.ReactNode }) {\n return (\n <div className=\"pz:px-2 pz:pt-1.5 pz:pb-0.5 pz:text-[10px] pz:font-medium pz:uppercase pz:tracking-wide pz:text-muted-foreground\">\n {children}\n </div>\n );\n}\n\nfunction ItemRow({\n item,\n active,\n onSelect,\n}: {\n item: SuggestionItem<ReferenceContext>;\n active: boolean;\n onSelect: (item: SuggestionItem<ReferenceContext>) => void;\n}) {\n return (\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={active}\n onClick={() => onSelect(item)}\n className={[\n \"pz:flex pz:w-full pz:items-center pz:gap-2 pz:rounded-sm pz:px-2 pz:py-1 pz:text-sm pz:text-left pz:cursor-pointer\",\n active\n ? \"pz:bg-accent pz:text-accent-foreground\"\n : \"pz:hover:bg-accent pz:hover:text-accent-foreground\",\n ].join(\" \")}\n >\n <KindIcon kind={item.context?.kind ?? \"field\"} />\n <span className=\"pz:truncate\">{item.title}</span>\n {item.subtext && (\n <span className=\"pz:ml-auto pz:shrink-0 pz:truncate pz:text-xs pz:text-muted-foreground\">\n {item.subtext}\n </span>\n )}\n </button>\n );\n}\n\n/**\n * Picker UI rendered inside the SuggestionMenu's children render-prop.\n *\n * - Empty query → sectioned view (Fields, Secrets, Constants).\n * - Non-empty query → flat ranked list.\n *\n * The selected-index pointer from `SuggestionMenuRenderProps` always maps\n * to the flat `items` order. Sectioned mode uses the same flat order but\n * inserts headers between the boundaries, so highlight tracking still\n * works without bespoke navigation.\n */\nexport interface OverflowCounts {\n field: number;\n secret: number;\n constant: number;\n}\n\nexport function UnifiedReferencePicker({\n items,\n selectedIndex,\n onSelect,\n query,\n hasSecretsResolver,\n hasConstantsResolver,\n inputFieldsCount,\n secretsPending,\n constantsPending,\n overflow,\n}: SuggestionMenuRenderProps<ReferenceContext> & {\n query: string;\n hasSecretsResolver: boolean;\n hasConstantsResolver: boolean;\n inputFieldsCount: number;\n secretsPending: boolean;\n constantsPending: boolean;\n overflow: OverflowCounts;\n}) {\n const { prefix, residual } = parseQuery(query);\n const isFlat = !!residual.trim() || prefix !== null;\n\n if (isFlat) {\n if (items.length === 0) {\n const isPendingForPrefix =\n (prefix === \"secret\" && secretsPending) ||\n (prefix === \"constant\" && constantsPending) ||\n (prefix === null && (secretsPending || constantsPending));\n return (\n <div className=\"pz:flex pz:flex-col pz:gap-0.5 pz:p-1 pz:min-w-72\" role=\"listbox\">\n {prefix && (\n <SectionHeader>\n {prefix === \"field\" ? \"Fields\" : prefix === \"secret\" ? \"Secrets\" : \"Constants\"}\n </SectionHeader>\n )}\n <div className=\"pz:px-2 pz:py-1.5 pz:text-sm pz:text-muted-foreground\">\n {isPendingForPrefix ? \"Searching…\" : describeEmpty(prefix)}\n </div>\n </div>\n );\n }\n return (\n <div className=\"pz:flex pz:flex-col pz:gap-0.5 pz:p-1 pz:min-w-72\" role=\"listbox\">\n {prefix && (\n <SectionHeader>\n {prefix === \"field\" ? \"Fields\" : prefix === \"secret\" ? \"Secrets\" : \"Constants\"}\n </SectionHeader>\n )}\n {items.map((item, index) => (\n <ItemRow\n key={`${item.context?.kind}-${item.title}`}\n item={item}\n active={selectedIndex === index}\n onSelect={onSelect}\n />\n ))}\n </div>\n );\n }\n\n // Sectioned view — always render Fields and Secrets headers in fixed\n // order, with empty-state hints when a section has no items. Constants\n // only appear when there are any to show.\n //\n // Items here are pre-capped per kind (LiquidEditor applies SECTION_CAP).\n // The selectedIndex maps to this capped flat order — no hidden rows.\n const fieldItems = items.filter((it) => it.context?.kind === \"field\");\n const secretItems = items.filter((it) => it.context?.kind === \"secret\");\n const constantItems = items.filter((it) => it.context?.kind === \"constant\");\n\n return (\n <div className=\"pz:flex pz:flex-col pz:gap-0.5 pz:p-1 pz:min-w-72\" role=\"listbox\">\n <Section\n label=\"Fields\"\n kind=\"field\"\n items={fieldItems}\n startIndex={0}\n selectedIndex={selectedIndex}\n onSelect={onSelect}\n emptyHint={\n inputFieldsCount === 0 ? \"No input fields available.\" : \"No matching input fields.\"\n }\n overflow={overflow.field}\n overflowHelper=\"Type to filter.\"\n />\n {hasSecretsResolver && (\n <Section\n label=\"Secrets\"\n kind=\"secret\"\n items={secretItems}\n startIndex={fieldItems.length}\n selectedIndex={selectedIndex}\n onSelect={onSelect}\n emptyHint={secretsPending ? \"Searching…\" : \"No matching secrets.\"}\n helperHint=\"Type /s/ to filter to secrets only.\"\n pending={secretsPending}\n overflow={overflow.secret}\n overflowHelper=\"Type to filter, or /s/ for secrets only.\"\n />\n )}\n {hasConstantsResolver && (\n <Section\n label=\"Constants\"\n kind=\"constant\"\n items={constantItems}\n startIndex={fieldItems.length + secretItems.length}\n selectedIndex={selectedIndex}\n onSelect={onSelect}\n emptyHint={constantsPending ? \"Searching…\" : \"No matching constants.\"}\n helperHint=\"Type /c/ to filter to constants only.\"\n pending={constantsPending}\n overflow={overflow.constant}\n overflowHelper=\"Type to filter, or /c/ for constants only.\"\n />\n )}\n </div>\n );\n}\n\nfunction Section({\n label,\n kind,\n items,\n startIndex,\n selectedIndex,\n onSelect,\n emptyHint,\n helperHint,\n pending,\n overflow = 0,\n overflowHelper,\n}: {\n label: string;\n kind: ReferenceKind;\n items: SuggestionItem<ReferenceContext>[];\n startIndex: number;\n selectedIndex?: number;\n onSelect: (item: SuggestionItem<ReferenceContext>) => void;\n emptyHint: string;\n helperHint?: string;\n pending?: boolean;\n overflow?: number;\n overflowHelper?: string;\n}) {\n return (\n <div className=\"pz:flex pz:flex-col\">\n <SectionHeader>\n {label}\n {items.length > 0 && (\n <span className=\"pz:ml-1 pz:normal-case pz:tracking-normal\">\n ({items.length}\n {overflow > 0 && ` of ${items.length + overflow}`})\n </span>\n )}\n {pending && (\n <span className=\"pz:ml-1 pz:normal-case pz:tracking-normal pz:opacity-70\">\n · Searching…\n </span>\n )}\n </SectionHeader>\n {items.length === 0 ? (\n <div className=\"pz:px-2 pz:py-1 pz:text-xs pz:text-muted-foreground\">\n {emptyHint}\n {helperHint && <span className=\"pz:block pz:opacity-70\">{helperHint}</span>}\n </div>\n ) : (\n <>\n {items.map((item, i) => (\n <ItemRow\n key={`${kind}-${item.title}`}\n item={item}\n active={selectedIndex === startIndex + i}\n onSelect={onSelect}\n />\n ))}\n {overflow > 0 && overflowHelper && (\n <div className=\"pz:px-2 pz:py-0.5 pz:text-[11px] pz:text-muted-foreground pz:opacity-70\">\n +{overflow} more — {overflowHelper}\n </div>\n )}\n </>\n )}\n </div>\n );\n}\n\nfunction describeEmpty(prefix: ReferenceKind | null): string {\n if (prefix === \"secret\") return \"No matching secrets.\";\n if (prefix === \"constant\") return \"No matching constants.\";\n if (prefix === \"field\") return \"No matching fields.\";\n return \"No matches.\";\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cn } from "../../../lib/utils.mjs";
|
|
2
2
|
import { Button } from "../../ui/button.mjs";
|
|
3
|
-
import { IconMinus, IconPlus } from "../icons.mjs";
|
|
3
|
+
import { IconMinus, IconPlus, X } from "../icons.mjs";
|
|
4
4
|
import { useDebouncedFn } from "../../../hooks/use-debounced-fn.mjs";
|
|
5
5
|
import { FieldTypeBadge } from "../../../widgets/field-type-badge.mjs";
|
|
6
6
|
import { WidgetStrip } from "../../../widgets/widget-strip.mjs";
|
|
@@ -8,7 +8,6 @@ import { Combobox, ComboboxChips, ComboboxChipsInput, ComboboxContent, ComboboxE
|
|
|
8
8
|
import { filterOptions } from "../multi-select-popover-trigger.mjs";
|
|
9
9
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
10
10
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
-
import { XIcon } from "lucide-react";
|
|
12
11
|
|
|
13
12
|
//#region src/components/internal/combobox/include-exclude-combobox.tsx
|
|
14
13
|
const includeChipClass = "pz:flex pz:h-[calc(--spacing(5.25))] pz:w-fit pz:items-center pz:gap-0 pz:rounded-sm pz:border pz:border-border pz:bg-muted pz:pl-0 pz:pr-0 pz:text-xs pz:font-medium pz:whitespace-nowrap pz:text-foreground pz:overflow-hidden";
|
|
@@ -246,7 +245,7 @@ function IncludeExcludeCombobox({ include, exclude, onChange, options, loadOptio
|
|
|
246
245
|
},
|
|
247
246
|
"aria-label": `Remove ${chip.label}`,
|
|
248
247
|
className: cn("pz:opacity-50 pz:hover:opacity-100", isInc ? void 0 : "pz:text-destructive pz:hover:bg-destructive/15"),
|
|
249
|
-
children: /* @__PURE__ */ jsx(
|
|
248
|
+
children: /* @__PURE__ */ jsx(X, { className: "pz:pointer-events-none" })
|
|
250
249
|
})
|
|
251
250
|
]
|
|
252
251
|
}, chip.value);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"include-exclude-combobox.mjs","names":[],"sources":["../../../../src/components/internal/combobox/include-exclude-combobox.tsx"],"sourcesContent":["import type { WidgetsByKind } from \"@pipe0/base\";\nimport { XIcon } from \"lucide-react\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { useDebouncedFn } from \"../../../hooks/use-debounced-fn.js\";\nimport { cn } from \"../../../lib/utils.js\";\nimport { FieldTypeBadge } from \"../../../widgets/field-type-badge.js\";\nimport { WidgetStrip } from \"../../../widgets/widget-strip.js\";\nimport { Button } from \"../../ui/button.js\";\nimport {\n Combobox,\n ComboboxChips,\n ComboboxChipsInput,\n ComboboxContent,\n ComboboxEmpty,\n ComboboxItem,\n ComboboxList,\n ComboboxTrigger,\n useComboboxAnchor,\n} from \"../../ui/combobox.js\";\nimport { IconMinus, IconPlus } from \"../icons.js\";\nimport { filterOptions, type PayloadOption } from \"../multi-select-popover-trigger.js\";\n\nexport type IncludeExcludeMode = \"include\" | \"exclude\";\n\nexport interface IncludeExcludeValue {\n include: string[];\n exclude: string[];\n}\n\nexport interface IncludeExcludeComboboxProps {\n include: string[];\n exclude: string[];\n /**\n * Atomic update for the entire `{ include, exclude }` value. We avoid\n * splitting include/exclude into separate add/remove callbacks because\n * flipping a chip needs to mutate both sides in one shot — otherwise the\n * stale closure in the field-handle wiring drops one of the updates.\n */\n onChange: (next: IncludeExcludeValue) => void;\n options?: PayloadOption[];\n loadOptions?: (query: string) => Promise<PayloadOption[]>;\n allowCreate?: boolean;\n disableSearch?: boolean;\n maxItems?: number;\n ariaInvalid?: boolean;\n disabled?: boolean;\n debounceMs?: number;\n emptyLabel?: string;\n placeholder?: string;\n}\n\ntype SelectableItem = PayloadOption & {\n __mode?: IncludeExcludeMode;\n __create?: boolean;\n};\n\nconst includeChipClass =\n \"pz:flex pz:h-[calc(--spacing(5.25))] pz:w-fit pz:items-center pz:gap-0 pz:rounded-sm pz:border pz:border-border pz:bg-muted pz:pl-0 pz:pr-0 pz:text-xs pz:font-medium pz:whitespace-nowrap pz:text-foreground pz:overflow-hidden\";\n\nconst excludeChipClass =\n \"pz:flex pz:h-[calc(--spacing(5.25))] pz:w-fit pz:items-center pz:gap-0 pz:rounded-sm pz:border pz:border-destructive/30 pz:bg-destructive/10 pz:pl-0 pz:pr-0 pz:text-xs pz:font-medium pz:whitespace-nowrap pz:text-destructive pz:overflow-hidden\";\n\nconst includeSignClass =\n \"pz:flex pz:h-full pz:w-5 pz:cursor-pointer pz:items-center pz:justify-center pz:border-r pz:border-border pz:bg-foreground/[0.04] pz:text-muted-foreground pz:hover:bg-foreground/[0.08] pz:hover:text-foreground\";\n\nconst excludeSignClass =\n \"pz:flex pz:h-full pz:w-5 pz:cursor-pointer pz:items-center pz:justify-center pz:border-r pz:border-destructive/30 pz:bg-destructive/[0.06] pz:text-destructive/80 pz:hover:bg-destructive/15 pz:hover:text-destructive\";\n\n/**\n * Unified include/exclude combobox.\n *\n * Renders a single chip input where each chip carries an inline `+` / `−`\n * sign toggle that flips include ↔ exclude in place. Free-text-create inputs\n * (`allowCreate=true`) show a \"Type to see suggestions\" hint until the user\n * types; select-style inputs show their options on focus. Pure-select mode\n * (`disableSearch=true`) shows the chevron trigger and renders the input\n * read-only.\n */\nexport function IncludeExcludeCombobox({\n include,\n exclude,\n onChange,\n options,\n loadOptions,\n allowCreate = false,\n disableSearch = false,\n maxItems,\n ariaInvalid,\n disabled,\n debounceMs = 300,\n emptyLabel,\n placeholder,\n}: IncludeExcludeComboboxProps) {\n const [input, setInput] = useState(\"\");\n const [debouncedQuery, setDebouncedQuery] = useState(\"\");\n const [asyncResults, setAsyncResults] = useState<PayloadOption[] | null>(null);\n const [loading, setLoading] = useState(false);\n const [open, setOpen] = useState(false);\n const cacheRef = useRef<Map<string, PayloadOption[]>>(new Map());\n const reqIdRef = useRef(0);\n\n const isAsync = typeof loadOptions === \"function\";\n\n const debouncedSetQuery = useDebouncedFn((q: string) => setDebouncedQuery(q), debounceMs);\n\n const effectiveQuery = input.trim();\n\n useEffect(() => {\n if (!isAsync || !loadOptions) return;\n if (!open && debouncedQuery === \"\") return;\n const q = debouncedQuery.trim();\n const cached = cacheRef.current.get(q);\n if (cached) {\n setAsyncResults(cached);\n setLoading(false);\n return;\n }\n const reqId = ++reqIdRef.current;\n setLoading(true);\n loadOptions(q)\n .then((opts) => {\n if (reqId !== reqIdRef.current) return;\n const kept = opts.filter((o) => !!o.value);\n cacheRef.current.set(q, kept);\n setAsyncResults(kept);\n setLoading(false);\n })\n .catch(() => {\n if (reqId !== reqIdRef.current) return;\n setAsyncResults([]);\n setLoading(false);\n });\n }, [debouncedQuery, isAsync, loadOptions, open]);\n\n const includeSet = useMemo(() => new Set(include), [include]);\n const excludeSet = useMemo(() => new Set(exclude), [exclude]);\n const totalSelected = include.length + exclude.length;\n const atMax = maxItems !== undefined && totalSelected >= maxItems;\n\n // Persist widgets seen for any value across loadOptions cycles. Async\n // results get replaced with each query, so without this cache an icon\n // shown when picking a value would disappear on the next refetch.\n const widgetCacheRef = useRef<Map<string, WidgetsByKind>>(new Map());\n for (const opt of options ?? []) {\n if (opt.widgets) widgetCacheRef.current.set(opt.value, opt.widgets);\n }\n for (const opt of asyncResults ?? []) {\n if (opt.widgets) widgetCacheRef.current.set(opt.value, opt.widgets);\n }\n\n // Track chip ordering locally so flipping include ↔ exclude keeps the chip\n // in its original visual position even though the field-handle data model\n // is two separate arrays. Mutated only in the action handlers below;\n // anything that arrives via an external `onChange` (e.g. form reset) is\n // appended at the end by the render-time reconciliation in `chips`.\n const [chipOrder, setChipOrder] = useState<string[]>(() => [...include, ...exclude]);\n\n const anchor = useComboboxAnchor();\n\n const filteredSuggestions = useMemo<PayloadOption[]>(() => {\n const source = isAsync ? (asyncResults ?? []) : (options ?? []);\n return isAsync ? source : filterOptions(source, effectiveQuery);\n }, [isAsync, asyncResults, options, effectiveQuery]);\n\n const showCreateRow =\n allowCreate &&\n effectiveQuery.length > 0 &&\n !includeSet.has(effectiveQuery) &&\n !excludeSet.has(effectiveQuery) &&\n !filteredSuggestions.some((o) => o.value.toLowerCase() === effectiveQuery.toLowerCase());\n\n const resolveLabel = (v: string): string => {\n const fromStatic = options?.find((o) => o.value === v)?.label;\n if (fromStatic) return fromStatic;\n const fromAsync = asyncResults?.find((o) => o.value === v)?.label;\n return fromAsync ?? v;\n };\n\n const resolveWidgets = (v: string): WidgetsByKind | undefined =>\n widgetCacheRef.current.get(v) ??\n options?.find((o) => o.value === v)?.widgets ??\n asyncResults?.find((o) => o.value === v)?.widgets;\n\n type Chip = {\n value: string;\n label: string;\n mode: IncludeExcludeMode;\n widgets?: WidgetsByKind;\n };\n\n // Order chips by the locally tracked `chipOrder`, with any not-yet-tracked\n // values appended at the end. Action handlers (`addAs`, `removeChip`) keep\n // `chipOrder` in sync; this append branch only kicks in for values that\n // arrived via an external `onChange` (e.g. a form reset).\n const chips = useMemo<Chip[]>(() => {\n const known = new Set(chipOrder);\n const orderedValues = chipOrder.filter((v) => includeSet.has(v) || excludeSet.has(v));\n for (const v of include)\n if (!known.has(v)) {\n orderedValues.push(v);\n known.add(v);\n }\n for (const v of exclude)\n if (!known.has(v)) {\n orderedValues.push(v);\n known.add(v);\n }\n return orderedValues.map((v) => ({\n value: v,\n label: resolveLabel(v),\n mode: includeSet.has(v) ? \"include\" : \"exclude\",\n widgets: resolveWidgets(v),\n }));\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [chipOrder, includeSet, excludeSet, options, asyncResults]);\n\n const items = useMemo<SelectableItem[]>(() => {\n const list: SelectableItem[] = [];\n if (showCreateRow) {\n list.push({\n value: effectiveQuery,\n label: `Add \"${effectiveQuery}\"`,\n __create: true,\n });\n }\n for (const o of filteredSuggestions) list.push(o);\n return list;\n }, [showCreateRow, effectiveQuery, filteredSuggestions]);\n\n const trackInsertion = (value: string) => {\n setChipOrder((prev) => (prev.includes(value) ? prev : [...prev, value]));\n };\n const trackRemoval = (value: string) => {\n setChipOrder((prev) => prev.filter((v) => v !== value));\n };\n\n const addAs = (value: string, mode: IncludeExcludeMode) => {\n if (!value) return;\n if (atMax && !includeSet.has(value) && !excludeSet.has(value)) return;\n if (mode === \"include\") {\n if (includeSet.has(value)) return;\n onChange({\n include: [...include, value],\n exclude: exclude.filter((v) => v !== value),\n });\n } else {\n if (excludeSet.has(value)) return;\n onChange({\n include: include.filter((v) => v !== value),\n exclude: [...exclude, value],\n });\n }\n trackInsertion(value);\n };\n\n // Flipping intentionally does NOT touch chipOrder — keeping the slot keeps\n // the chip in place visually as it changes mode.\n const flip = (value: string, currentMode: IncludeExcludeMode) => {\n if (currentMode === \"include\") {\n onChange({\n include: include.filter((v) => v !== value),\n exclude: [...exclude, value],\n });\n } else {\n onChange({\n include: [...include, value],\n exclude: exclude.filter((v) => v !== value),\n });\n }\n };\n\n const removeChip = (value: string, mode: IncludeExcludeMode) => {\n if (mode === \"include\") onChange({ include: include.filter((v) => v !== value), exclude });\n else onChange({ include, exclude: exclude.filter((v) => v !== value) });\n trackRemoval(value);\n };\n\n const clearInput = () => {\n setInput(\"\");\n if (isAsync) debouncedSetQuery(\"\");\n };\n\n return (\n <Combobox<SelectableItem, true>\n multiple\n items={items}\n filter={null}\n open={open}\n onOpenChange={setOpen}\n // We only use base-ui's value plumbing to capture pick events from list\n // rows; chips are rendered manually so we keep the include/exclude\n // grouping. Returning an empty array keeps base-ui from drawing chips.\n value={[] as SelectableItem[]}\n onValueChange={(next) => {\n if (!Array.isArray(next) || next.length === 0) return;\n const last = next[next.length - 1] as SelectableItem;\n const mode: IncludeExcludeMode = last.__mode ?? \"include\";\n addAs(last.value, mode);\n clearInput();\n }}\n isItemEqualToValue={(a, b) => (a as SelectableItem).value === (b as SelectableItem).value}\n itemToStringLabel={(item) => (item as SelectableItem).label}\n itemToStringValue={(item) => (item as SelectableItem).value}\n inputValue={input}\n onInputValueChange={(v) => {\n setInput(v);\n if (isAsync) debouncedSetQuery(v);\n }}\n disabled={disabled}\n >\n <ComboboxChips aria-invalid={ariaInvalid || undefined} ref={anchor}>\n {chips.map((chip) => {\n const isInc = chip.mode === \"include\";\n return (\n <span\n key={chip.value}\n className={cn(isInc ? includeChipClass : excludeChipClass)}\n data-mode={chip.mode}\n >\n <button\n type=\"button\"\n tabIndex={-1}\n onClick={(e) => {\n e.stopPropagation();\n flip(chip.value, chip.mode);\n }}\n aria-label={isInc ? \"Switch to exclude\" : \"Switch to include\"}\n title={isInc ? \"Switch to exclude\" : \"Switch to include\"}\n className={cn(isInc ? includeSignClass : excludeSignClass)}\n >\n {isInc ? <IconPlus width={10} height={10} /> : <IconMinus width={10} height={10} />}\n </button>\n <span className=\"pz:flex pz:items-center pz:gap-1 pz:px-1.5\">\n {chip.widgets && <WidgetStrip widgets={chip.widgets} size={14} />}\n {chip.label}\n </span>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n tabIndex={-1}\n onClick={(e) => {\n e.stopPropagation();\n removeChip(chip.value, chip.mode);\n }}\n aria-label={`Remove ${chip.label}`}\n className={cn(\n \"pz:opacity-50 pz:hover:opacity-100\",\n isInc ? undefined : \"pz:text-destructive pz:hover:bg-destructive/15\",\n )}\n >\n <XIcon className=\"pz:pointer-events-none\" />\n </Button>\n </span>\n );\n })}\n <ComboboxChipsInput\n placeholder={chips.length === 0 ? placeholder : undefined}\n readOnly={disableSearch}\n aria-invalid={ariaInvalid || undefined}\n onKeyDown={(e) => {\n if (e.key === \"Backspace\" && input.length === 0) {\n if (chips.length === 0) return;\n e.preventDefault();\n const last = chips[chips.length - 1];\n removeChip(last.value, last.mode);\n return;\n }\n if (e.key !== \"Enter\" || e.nativeEvent.isComposing) return;\n if (effectiveQuery.length === 0) return;\n // Prevent the Enter from bubbling up and submitting an enclosing form.\n e.preventDefault();\n e.stopPropagation();\n if (atMax) return;\n const exact = filteredSuggestions.find(\n (o) => o.value.toLowerCase() === effectiveQuery.toLowerCase(),\n );\n const toAdd = exact?.value ?? (allowCreate ? effectiveQuery : null);\n if (!toAdd) return;\n addAs(toAdd, \"include\");\n clearInput();\n }}\n />\n <ComboboxTrigger className=\"pz:ml-auto pz:bg-transparent pz:hover:bg-transparent\" />\n </ComboboxChips>\n\n <ComboboxContent anchor={anchor}>\n {loading && items.length > 0 && (\n // Refetch: prior items stay visible, dimmed, with a small corner\n // spinner so the layout doesn't jump while the new page loads.\n <span\n aria-label=\"Loading\"\n className=\"pz:absolute pz:right-2 pz:top-2 pz:z-10 pz:inline-block pz:h-3 pz:w-3 pz:animate-spin pz:rounded-full pz:border-2 pz:border-muted-foreground/30 pz:border-t-muted-foreground\"\n />\n )}\n {loading && items.length === 0 && (\n // Initial load: nothing to dim, so render a visible spinner in the\n // popover body to confirm options are being fetched.\n <div\n role=\"status\"\n aria-label=\"Loading\"\n className=\"pz:flex pz:items-center pz:justify-center pz:py-6\"\n >\n <span className=\"pz:inline-block pz:h-5 pz:w-5 pz:animate-spin pz:rounded-full pz:border-2 pz:border-muted-foreground/30 pz:border-t-muted-foreground\" />\n </div>\n )}\n <ComboboxList\n className={cn(\n loading &&\n items.length > 0 &&\n \"pz:opacity-50 pz:transition-opacity pz:pointer-events-none\",\n )}\n >\n {atMax && (\n <div className=\"pz:px-2 pz:py-1.5 pz:text-xs pz:text-muted-foreground\">\n Maximum {maxItems} item{maxItems === 1 ? \"\" : \"s\"} reached.\n </div>\n )}\n\n {items.map((item) => {\n const fieldType = !item.__create ? item.widgets?.field_type : undefined;\n const inIncluded = includeSet.has(item.value);\n const inExcluded = excludeSet.has(item.value);\n const includeDisabled = inIncluded || (atMax && !inExcluded);\n const excludeDisabled = inExcluded || (atMax && !inIncluded);\n return (\n <ComboboxItem\n key={`${item.__create ? \"__create__:\" : \"\"}${item.value}`}\n value={{ ...item, __mode: \"include\" } as SelectableItem}\n className={cn(\"pz:pr-1\", item.__create ? \"pz:italic\" : undefined)}\n disabled={includeDisabled && excludeDisabled && !item.__create}\n >\n {!item.__create && item.widgets && (\n <WidgetStrip\n widgets={\n // Field-type renders trailing; everything else leading.\n Object.fromEntries(\n Object.entries(item.widgets).filter(([k]) => k !== \"field_type\"),\n )\n }\n size={14}\n />\n )}\n <span className=\"pz:flex-1 pz:truncate\">{item.label}</span>\n {fieldType && <FieldTypeBadge type={fieldType.type} format={fieldType.format} />}\n <span className=\"pz:flex pz:items-center pz:pl-2\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n tabIndex={-1}\n disabled={excludeDisabled}\n onClick={(e) => {\n e.stopPropagation();\n e.preventDefault();\n if (excludeDisabled) return;\n addAs(item.value, \"exclude\");\n clearInput();\n }}\n aria-label={`Exclude ${item.label}`}\n title=\"Exclude\"\n className=\"pz:text-muted-foreground\"\n >\n <IconMinus width={10} height={10} />\n </Button>\n </span>\n </ComboboxItem>\n );\n })}\n\n {!loading && items.length === 0 && (\n <ComboboxEmpty>\n {emptyLabel ??\n (allowCreate\n ? options || loadOptions\n ? \"No matches\"\n : \"Type to add\"\n : \"No options\")}\n </ComboboxEmpty>\n )}\n </ComboboxList>\n </ComboboxContent>\n </Combobox>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"include-exclude-combobox.mjs","names":["XIcon"],"sources":["../../../../src/components/internal/combobox/include-exclude-combobox.tsx"],"sourcesContent":["import type { WidgetsByKind } from \"@pipe0/base\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { useDebouncedFn } from \"../../../hooks/use-debounced-fn.js\";\nimport { cn } from \"../../../lib/utils.js\";\nimport { FieldTypeBadge } from \"../../../widgets/field-type-badge.js\";\nimport { WidgetStrip } from \"../../../widgets/widget-strip.js\";\nimport { Button } from \"../../ui/button.js\";\nimport {\n Combobox,\n ComboboxChips,\n ComboboxChipsInput,\n ComboboxContent,\n ComboboxEmpty,\n ComboboxItem,\n ComboboxList,\n ComboboxTrigger,\n useComboboxAnchor,\n} from \"../../ui/combobox.js\";\nimport { IconMinus, IconPlus, X as XIcon } from \"../icons.js\";\nimport { filterOptions, type PayloadOption } from \"../multi-select-popover-trigger.js\";\n\nexport type IncludeExcludeMode = \"include\" | \"exclude\";\n\nexport interface IncludeExcludeValue {\n include: string[];\n exclude: string[];\n}\n\nexport interface IncludeExcludeComboboxProps {\n include: string[];\n exclude: string[];\n /**\n * Atomic update for the entire `{ include, exclude }` value. We avoid\n * splitting include/exclude into separate add/remove callbacks because\n * flipping a chip needs to mutate both sides in one shot — otherwise the\n * stale closure in the field-handle wiring drops one of the updates.\n */\n onChange: (next: IncludeExcludeValue) => void;\n options?: PayloadOption[];\n loadOptions?: (query: string) => Promise<PayloadOption[]>;\n allowCreate?: boolean;\n disableSearch?: boolean;\n maxItems?: number;\n ariaInvalid?: boolean;\n disabled?: boolean;\n debounceMs?: number;\n emptyLabel?: string;\n placeholder?: string;\n}\n\ntype SelectableItem = PayloadOption & {\n __mode?: IncludeExcludeMode;\n __create?: boolean;\n};\n\nconst includeChipClass =\n \"pz:flex pz:h-[calc(--spacing(5.25))] pz:w-fit pz:items-center pz:gap-0 pz:rounded-sm pz:border pz:border-border pz:bg-muted pz:pl-0 pz:pr-0 pz:text-xs pz:font-medium pz:whitespace-nowrap pz:text-foreground pz:overflow-hidden\";\n\nconst excludeChipClass =\n \"pz:flex pz:h-[calc(--spacing(5.25))] pz:w-fit pz:items-center pz:gap-0 pz:rounded-sm pz:border pz:border-destructive/30 pz:bg-destructive/10 pz:pl-0 pz:pr-0 pz:text-xs pz:font-medium pz:whitespace-nowrap pz:text-destructive pz:overflow-hidden\";\n\nconst includeSignClass =\n \"pz:flex pz:h-full pz:w-5 pz:cursor-pointer pz:items-center pz:justify-center pz:border-r pz:border-border pz:bg-foreground/[0.04] pz:text-muted-foreground pz:hover:bg-foreground/[0.08] pz:hover:text-foreground\";\n\nconst excludeSignClass =\n \"pz:flex pz:h-full pz:w-5 pz:cursor-pointer pz:items-center pz:justify-center pz:border-r pz:border-destructive/30 pz:bg-destructive/[0.06] pz:text-destructive/80 pz:hover:bg-destructive/15 pz:hover:text-destructive\";\n\n/**\n * Unified include/exclude combobox.\n *\n * Renders a single chip input where each chip carries an inline `+` / `−`\n * sign toggle that flips include ↔ exclude in place. Free-text-create inputs\n * (`allowCreate=true`) show a \"Type to see suggestions\" hint until the user\n * types; select-style inputs show their options on focus. Pure-select mode\n * (`disableSearch=true`) shows the chevron trigger and renders the input\n * read-only.\n */\nexport function IncludeExcludeCombobox({\n include,\n exclude,\n onChange,\n options,\n loadOptions,\n allowCreate = false,\n disableSearch = false,\n maxItems,\n ariaInvalid,\n disabled,\n debounceMs = 300,\n emptyLabel,\n placeholder,\n}: IncludeExcludeComboboxProps) {\n const [input, setInput] = useState(\"\");\n const [debouncedQuery, setDebouncedQuery] = useState(\"\");\n const [asyncResults, setAsyncResults] = useState<PayloadOption[] | null>(null);\n const [loading, setLoading] = useState(false);\n const [open, setOpen] = useState(false);\n const cacheRef = useRef<Map<string, PayloadOption[]>>(new Map());\n const reqIdRef = useRef(0);\n\n const isAsync = typeof loadOptions === \"function\";\n\n const debouncedSetQuery = useDebouncedFn((q: string) => setDebouncedQuery(q), debounceMs);\n\n const effectiveQuery = input.trim();\n\n useEffect(() => {\n if (!isAsync || !loadOptions) return;\n if (!open && debouncedQuery === \"\") return;\n const q = debouncedQuery.trim();\n const cached = cacheRef.current.get(q);\n if (cached) {\n setAsyncResults(cached);\n setLoading(false);\n return;\n }\n const reqId = ++reqIdRef.current;\n setLoading(true);\n loadOptions(q)\n .then((opts) => {\n if (reqId !== reqIdRef.current) return;\n const kept = opts.filter((o) => !!o.value);\n cacheRef.current.set(q, kept);\n setAsyncResults(kept);\n setLoading(false);\n })\n .catch(() => {\n if (reqId !== reqIdRef.current) return;\n setAsyncResults([]);\n setLoading(false);\n });\n }, [debouncedQuery, isAsync, loadOptions, open]);\n\n const includeSet = useMemo(() => new Set(include), [include]);\n const excludeSet = useMemo(() => new Set(exclude), [exclude]);\n const totalSelected = include.length + exclude.length;\n const atMax = maxItems !== undefined && totalSelected >= maxItems;\n\n // Persist widgets seen for any value across loadOptions cycles. Async\n // results get replaced with each query, so without this cache an icon\n // shown when picking a value would disappear on the next refetch.\n const widgetCacheRef = useRef<Map<string, WidgetsByKind>>(new Map());\n for (const opt of options ?? []) {\n if (opt.widgets) widgetCacheRef.current.set(opt.value, opt.widgets);\n }\n for (const opt of asyncResults ?? []) {\n if (opt.widgets) widgetCacheRef.current.set(opt.value, opt.widgets);\n }\n\n // Track chip ordering locally so flipping include ↔ exclude keeps the chip\n // in its original visual position even though the field-handle data model\n // is two separate arrays. Mutated only in the action handlers below;\n // anything that arrives via an external `onChange` (e.g. form reset) is\n // appended at the end by the render-time reconciliation in `chips`.\n const [chipOrder, setChipOrder] = useState<string[]>(() => [...include, ...exclude]);\n\n const anchor = useComboboxAnchor();\n\n const filteredSuggestions = useMemo<PayloadOption[]>(() => {\n const source = isAsync ? (asyncResults ?? []) : (options ?? []);\n return isAsync ? source : filterOptions(source, effectiveQuery);\n }, [isAsync, asyncResults, options, effectiveQuery]);\n\n const showCreateRow =\n allowCreate &&\n effectiveQuery.length > 0 &&\n !includeSet.has(effectiveQuery) &&\n !excludeSet.has(effectiveQuery) &&\n !filteredSuggestions.some((o) => o.value.toLowerCase() === effectiveQuery.toLowerCase());\n\n const resolveLabel = (v: string): string => {\n const fromStatic = options?.find((o) => o.value === v)?.label;\n if (fromStatic) return fromStatic;\n const fromAsync = asyncResults?.find((o) => o.value === v)?.label;\n return fromAsync ?? v;\n };\n\n const resolveWidgets = (v: string): WidgetsByKind | undefined =>\n widgetCacheRef.current.get(v) ??\n options?.find((o) => o.value === v)?.widgets ??\n asyncResults?.find((o) => o.value === v)?.widgets;\n\n type Chip = {\n value: string;\n label: string;\n mode: IncludeExcludeMode;\n widgets?: WidgetsByKind;\n };\n\n // Order chips by the locally tracked `chipOrder`, with any not-yet-tracked\n // values appended at the end. Action handlers (`addAs`, `removeChip`) keep\n // `chipOrder` in sync; this append branch only kicks in for values that\n // arrived via an external `onChange` (e.g. a form reset).\n const chips = useMemo<Chip[]>(() => {\n const known = new Set(chipOrder);\n const orderedValues = chipOrder.filter((v) => includeSet.has(v) || excludeSet.has(v));\n for (const v of include)\n if (!known.has(v)) {\n orderedValues.push(v);\n known.add(v);\n }\n for (const v of exclude)\n if (!known.has(v)) {\n orderedValues.push(v);\n known.add(v);\n }\n return orderedValues.map((v) => ({\n value: v,\n label: resolveLabel(v),\n mode: includeSet.has(v) ? \"include\" : \"exclude\",\n widgets: resolveWidgets(v),\n }));\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [chipOrder, includeSet, excludeSet, options, asyncResults]);\n\n const items = useMemo<SelectableItem[]>(() => {\n const list: SelectableItem[] = [];\n if (showCreateRow) {\n list.push({\n value: effectiveQuery,\n label: `Add \"${effectiveQuery}\"`,\n __create: true,\n });\n }\n for (const o of filteredSuggestions) list.push(o);\n return list;\n }, [showCreateRow, effectiveQuery, filteredSuggestions]);\n\n const trackInsertion = (value: string) => {\n setChipOrder((prev) => (prev.includes(value) ? prev : [...prev, value]));\n };\n const trackRemoval = (value: string) => {\n setChipOrder((prev) => prev.filter((v) => v !== value));\n };\n\n const addAs = (value: string, mode: IncludeExcludeMode) => {\n if (!value) return;\n if (atMax && !includeSet.has(value) && !excludeSet.has(value)) return;\n if (mode === \"include\") {\n if (includeSet.has(value)) return;\n onChange({\n include: [...include, value],\n exclude: exclude.filter((v) => v !== value),\n });\n } else {\n if (excludeSet.has(value)) return;\n onChange({\n include: include.filter((v) => v !== value),\n exclude: [...exclude, value],\n });\n }\n trackInsertion(value);\n };\n\n // Flipping intentionally does NOT touch chipOrder — keeping the slot keeps\n // the chip in place visually as it changes mode.\n const flip = (value: string, currentMode: IncludeExcludeMode) => {\n if (currentMode === \"include\") {\n onChange({\n include: include.filter((v) => v !== value),\n exclude: [...exclude, value],\n });\n } else {\n onChange({\n include: [...include, value],\n exclude: exclude.filter((v) => v !== value),\n });\n }\n };\n\n const removeChip = (value: string, mode: IncludeExcludeMode) => {\n if (mode === \"include\") onChange({ include: include.filter((v) => v !== value), exclude });\n else onChange({ include, exclude: exclude.filter((v) => v !== value) });\n trackRemoval(value);\n };\n\n const clearInput = () => {\n setInput(\"\");\n if (isAsync) debouncedSetQuery(\"\");\n };\n\n return (\n <Combobox<SelectableItem, true>\n multiple\n items={items}\n filter={null}\n open={open}\n onOpenChange={setOpen}\n // We only use base-ui's value plumbing to capture pick events from list\n // rows; chips are rendered manually so we keep the include/exclude\n // grouping. Returning an empty array keeps base-ui from drawing chips.\n value={[] as SelectableItem[]}\n onValueChange={(next) => {\n if (!Array.isArray(next) || next.length === 0) return;\n const last = next[next.length - 1] as SelectableItem;\n const mode: IncludeExcludeMode = last.__mode ?? \"include\";\n addAs(last.value, mode);\n clearInput();\n }}\n isItemEqualToValue={(a, b) => (a as SelectableItem).value === (b as SelectableItem).value}\n itemToStringLabel={(item) => (item as SelectableItem).label}\n itemToStringValue={(item) => (item as SelectableItem).value}\n inputValue={input}\n onInputValueChange={(v) => {\n setInput(v);\n if (isAsync) debouncedSetQuery(v);\n }}\n disabled={disabled}\n >\n <ComboboxChips aria-invalid={ariaInvalid || undefined} ref={anchor}>\n {chips.map((chip) => {\n const isInc = chip.mode === \"include\";\n return (\n <span\n key={chip.value}\n className={cn(isInc ? includeChipClass : excludeChipClass)}\n data-mode={chip.mode}\n >\n <button\n type=\"button\"\n tabIndex={-1}\n onClick={(e) => {\n e.stopPropagation();\n flip(chip.value, chip.mode);\n }}\n aria-label={isInc ? \"Switch to exclude\" : \"Switch to include\"}\n title={isInc ? \"Switch to exclude\" : \"Switch to include\"}\n className={cn(isInc ? includeSignClass : excludeSignClass)}\n >\n {isInc ? <IconPlus width={10} height={10} /> : <IconMinus width={10} height={10} />}\n </button>\n <span className=\"pz:flex pz:items-center pz:gap-1 pz:px-1.5\">\n {chip.widgets && <WidgetStrip widgets={chip.widgets} size={14} />}\n {chip.label}\n </span>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n tabIndex={-1}\n onClick={(e) => {\n e.stopPropagation();\n removeChip(chip.value, chip.mode);\n }}\n aria-label={`Remove ${chip.label}`}\n className={cn(\n \"pz:opacity-50 pz:hover:opacity-100\",\n isInc ? undefined : \"pz:text-destructive pz:hover:bg-destructive/15\",\n )}\n >\n <XIcon className=\"pz:pointer-events-none\" />\n </Button>\n </span>\n );\n })}\n <ComboboxChipsInput\n placeholder={chips.length === 0 ? placeholder : undefined}\n readOnly={disableSearch}\n aria-invalid={ariaInvalid || undefined}\n onKeyDown={(e) => {\n if (e.key === \"Backspace\" && input.length === 0) {\n if (chips.length === 0) return;\n e.preventDefault();\n const last = chips[chips.length - 1];\n removeChip(last.value, last.mode);\n return;\n }\n if (e.key !== \"Enter\" || e.nativeEvent.isComposing) return;\n if (effectiveQuery.length === 0) return;\n // Prevent the Enter from bubbling up and submitting an enclosing form.\n e.preventDefault();\n e.stopPropagation();\n if (atMax) return;\n const exact = filteredSuggestions.find(\n (o) => o.value.toLowerCase() === effectiveQuery.toLowerCase(),\n );\n const toAdd = exact?.value ?? (allowCreate ? effectiveQuery : null);\n if (!toAdd) return;\n addAs(toAdd, \"include\");\n clearInput();\n }}\n />\n <ComboboxTrigger className=\"pz:ml-auto pz:bg-transparent pz:hover:bg-transparent\" />\n </ComboboxChips>\n\n <ComboboxContent anchor={anchor}>\n {loading && items.length > 0 && (\n // Refetch: prior items stay visible, dimmed, with a small corner\n // spinner so the layout doesn't jump while the new page loads.\n <span\n aria-label=\"Loading\"\n className=\"pz:absolute pz:right-2 pz:top-2 pz:z-10 pz:inline-block pz:h-3 pz:w-3 pz:animate-spin pz:rounded-full pz:border-2 pz:border-muted-foreground/30 pz:border-t-muted-foreground\"\n />\n )}\n {loading && items.length === 0 && (\n // Initial load: nothing to dim, so render a visible spinner in the\n // popover body to confirm options are being fetched.\n <div\n role=\"status\"\n aria-label=\"Loading\"\n className=\"pz:flex pz:items-center pz:justify-center pz:py-6\"\n >\n <span className=\"pz:inline-block pz:h-5 pz:w-5 pz:animate-spin pz:rounded-full pz:border-2 pz:border-muted-foreground/30 pz:border-t-muted-foreground\" />\n </div>\n )}\n <ComboboxList\n className={cn(\n loading &&\n items.length > 0 &&\n \"pz:opacity-50 pz:transition-opacity pz:pointer-events-none\",\n )}\n >\n {atMax && (\n <div className=\"pz:px-2 pz:py-1.5 pz:text-xs pz:text-muted-foreground\">\n Maximum {maxItems} item{maxItems === 1 ? \"\" : \"s\"} reached.\n </div>\n )}\n\n {items.map((item) => {\n const fieldType = !item.__create ? item.widgets?.field_type : undefined;\n const inIncluded = includeSet.has(item.value);\n const inExcluded = excludeSet.has(item.value);\n const includeDisabled = inIncluded || (atMax && !inExcluded);\n const excludeDisabled = inExcluded || (atMax && !inIncluded);\n return (\n <ComboboxItem\n key={`${item.__create ? \"__create__:\" : \"\"}${item.value}`}\n value={{ ...item, __mode: \"include\" } as SelectableItem}\n className={cn(\"pz:pr-1\", item.__create ? \"pz:italic\" : undefined)}\n disabled={includeDisabled && excludeDisabled && !item.__create}\n >\n {!item.__create && item.widgets && (\n <WidgetStrip\n widgets={\n // Field-type renders trailing; everything else leading.\n Object.fromEntries(\n Object.entries(item.widgets).filter(([k]) => k !== \"field_type\"),\n )\n }\n size={14}\n />\n )}\n <span className=\"pz:flex-1 pz:truncate\">{item.label}</span>\n {fieldType && <FieldTypeBadge type={fieldType.type} format={fieldType.format} />}\n <span className=\"pz:flex pz:items-center pz:pl-2\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n tabIndex={-1}\n disabled={excludeDisabled}\n onClick={(e) => {\n e.stopPropagation();\n e.preventDefault();\n if (excludeDisabled) return;\n addAs(item.value, \"exclude\");\n clearInput();\n }}\n aria-label={`Exclude ${item.label}`}\n title=\"Exclude\"\n className=\"pz:text-muted-foreground\"\n >\n <IconMinus width={10} height={10} />\n </Button>\n </span>\n </ComboboxItem>\n );\n })}\n\n {!loading && items.length === 0 && (\n <ComboboxEmpty>\n {emptyLabel ??\n (allowCreate\n ? options || loadOptions\n ? \"No matches\"\n : \"Type to add\"\n : \"No options\")}\n </ComboboxEmpty>\n )}\n </ComboboxList>\n </ComboboxContent>\n </Combobox>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cn } from "../../../lib/utils.mjs";
|
|
2
2
|
import { Button } from "../../ui/button.mjs";
|
|
3
|
-
import { IconGripVertical } from "../icons.mjs";
|
|
3
|
+
import { IconGripVertical, X } from "../icons.mjs";
|
|
4
4
|
import { useDebouncedFn } from "../../../hooks/use-debounced-fn.mjs";
|
|
5
5
|
import { FieldTypeBadge } from "../../../widgets/field-type-badge.mjs";
|
|
6
6
|
import { WidgetStrip } from "../../../widgets/widget-strip.mjs";
|
|
@@ -8,7 +8,6 @@ import { Combobox, ComboboxChip, ComboboxChips, ComboboxChipsInput, ComboboxCont
|
|
|
8
8
|
import { filterOptions } from "../multi-select-popover-trigger.mjs";
|
|
9
9
|
import { useEffect, useId, useMemo, useRef, useState } from "react";
|
|
10
10
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
-
import { XIcon } from "lucide-react";
|
|
12
11
|
import { DndContext, KeyboardSensor, PointerSensor, closestCenter, useSensor, useSensors } from "@dnd-kit/core";
|
|
13
12
|
import { SortableContext, horizontalListSortingStrategy, useSortable } from "@dnd-kit/sortable";
|
|
14
13
|
import useSWR from "swr";
|
|
@@ -55,7 +54,7 @@ function SortableChip({ id, onRemove, children }) {
|
|
|
55
54
|
onRemove();
|
|
56
55
|
},
|
|
57
56
|
"aria-label": "Remove",
|
|
58
|
-
children: /* @__PURE__ */ jsx(
|
|
57
|
+
children: /* @__PURE__ */ jsx(X, { className: "pz:pointer-events-none" })
|
|
59
58
|
})
|
|
60
59
|
]
|
|
61
60
|
});
|