@cntyclub/ui-react 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cntyclub/ui-react",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "React component library for the Country Club UI Kit — Base UI primitives styled with the Country Club design system (Tailwind CSS v4)",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -28,33 +28,6 @@
28
28
  "dist"
29
29
  ],
30
30
  "comment-publishConfig": "In-repo (docs/storybook/vue) consume src/ via the exports above. When PUBLISHED to a registry, npm/pnpm swap in these fields so external installs get the compiled, self-contained dist/ (tsup bundles every dependency except react/react-dom) — no transpilePackages, no heavy transitive dep tree.",
31
- "publishConfig": {
32
- "main": "./dist/index.js",
33
- "module": "./dist/index.js",
34
- "types": "./dist/index.d.ts",
35
- "exports": {
36
- ".": {
37
- "types": "./dist/index.d.ts",
38
- "import": "./dist/index.js",
39
- "default": "./dist/index.js"
40
- },
41
- "./form": {
42
- "types": "./dist/form.d.ts",
43
- "import": "./dist/form.js",
44
- "default": "./dist/form.js"
45
- },
46
- "./styles.css": "./src/styles.css",
47
- "./package.json": "./package.json"
48
- },
49
- "access": "public"
50
- },
51
- "scripts": {
52
- "build": "tsup",
53
- "dev": "tsup --watch",
54
- "typecheck": "tsc --noEmit",
55
- "clean": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\"",
56
- "prepublishOnly": "tsup"
57
- },
58
31
  "dependencies": {
59
32
  "@base-ui/react": "1.1.0",
60
33
  "@headlessui/react": "^2.2.9",
@@ -91,12 +64,18 @@
91
64
  "react-dom": "^19.0.0"
92
65
  },
93
66
  "devDependencies": {
94
- "@countryclub/typescript-config": "workspace:*",
95
67
  "@types/react": "^19.1.0",
96
68
  "@types/react-dom": "^19.1.0",
97
69
  "react": "^19.1.0",
98
70
  "react-dom": "^19.1.0",
99
71
  "tsup": "^8.4.0",
100
- "typescript": "^5.8.3"
72
+ "typescript": "^5.8.3",
73
+ "@countryclub/typescript-config": "0.1.0"
74
+ },
75
+ "scripts": {
76
+ "build": "tsup",
77
+ "dev": "tsup --watch",
78
+ "typecheck": "tsc --noEmit",
79
+ "clean": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\""
101
80
  }
102
- }
81
+ }
@@ -0,0 +1,81 @@
1
+ "use client";
2
+
3
+ import type * as React from "react";
4
+
5
+ import { cn } from "../../lib/utils/css";
6
+
7
+ interface PipelineStage {
8
+ /** The headline number for the stage. */
9
+ value: React.ReactNode;
10
+ /** The stage caption, e.g. "Target accounts". */
11
+ label: React.ReactNode;
12
+ /** Render the value in the brand/primary color (e.g. a final stage). */
13
+ accent?: boolean;
14
+ }
15
+
16
+ interface PipelineProps extends React.ComponentProps<"div"> {
17
+ /** Ordered funnel stages, left to right. */
18
+ stages: PipelineStage[];
19
+ /**
20
+ * Conversion markers shown after each stage (length = `stages.length - 1`).
21
+ * Typically a percentage string; any node works.
22
+ */
23
+ conversions?: React.ReactNode[];
24
+ }
25
+
26
+ /**
27
+ * A horizontal conversion funnel — a row of headline stages, each with the
28
+ * step-to-step conversion pill sitting right beside its number. The row wraps
29
+ * onto multiple lines when space is tight (e.g. beside a docked panel), so it
30
+ * stays readable without stretching the stages apart.
31
+ */
32
+ function Pipeline({ stages, conversions = [], className, ...props }: PipelineProps) {
33
+ return (
34
+ <div
35
+ className={cn("flex flex-wrap items-start gap-x-5 gap-y-4", className)}
36
+ data-slot="pipeline"
37
+ {...props}
38
+ >
39
+ {stages.map((stage, i) => {
40
+ const conv = i < stages.length - 1 ? conversions[i] : undefined;
41
+ return (
42
+ <div key={i} data-slot="pipeline-stage">
43
+ <div className="flex items-baseline gap-2">
44
+ <span
45
+ className={cn(
46
+ "font-sans font-semibold text-3xl text-foreground leading-none tabular-nums tracking-tight",
47
+ stage.accent && "text-primary",
48
+ )}
49
+ >
50
+ {stage.value}
51
+ </span>
52
+ {conv != null ? <PipelineConversion>{conv}</PipelineConversion> : null}
53
+ </div>
54
+ <div className="mt-1.5 text-muted-foreground text-xs leading-snug">
55
+ {stage.label}
56
+ </div>
57
+ </div>
58
+ );
59
+ })}
60
+ </div>
61
+ );
62
+ }
63
+
64
+ /** The small pill that carries a stage-to-stage conversion value. */
65
+ function PipelineConversion({
66
+ className,
67
+ ...props
68
+ }: React.ComponentProps<"span">) {
69
+ return (
70
+ <span
71
+ className={cn(
72
+ "inline-flex items-center rounded-full border border-border bg-secondary px-2 py-0.5 font-medium text-secondary-foreground text-xs tabular-nums",
73
+ className,
74
+ )}
75
+ data-slot="pipeline-conversion"
76
+ {...props}
77
+ />
78
+ );
79
+ }
80
+
81
+ export { Pipeline, PipelineConversion, type PipelineProps, type PipelineStage };
@@ -0,0 +1,68 @@
1
+ "use client";
2
+
3
+ import type * as React from "react";
4
+
5
+ import { ToggleGroup, ToggleGroupItem } from "./toggle-group";
6
+ import { cn } from "../../lib/utils/css";
7
+
8
+ interface SegmentedControlOption<T extends string> {
9
+ value: T;
10
+ label: React.ReactNode;
11
+ }
12
+
13
+ interface SegmentedControlProps<T extends string> {
14
+ /** The currently-selected option value. */
15
+ value: T;
16
+ /** Called with the newly-selected value (never with an empty selection). */
17
+ onValueChange: (value: T) => void;
18
+ /** The mutually-exclusive options, left to right. */
19
+ options: SegmentedControlOption<T>[];
20
+ size?: "sm" | "default" | "lg";
21
+ variant?: "default" | "outline";
22
+ className?: string;
23
+ }
24
+
25
+ /**
26
+ * A single-select segmented control — a compact row of mutually-exclusive
27
+ * options (e.g. a time-range switch: Past month / Past 3 months / Overall).
28
+ *
29
+ * A thin, typed wrapper over {@link ToggleGroup} that keeps exactly one option
30
+ * selected — the active segment can't be toggled off into an empty state — and
31
+ * hides Base UI's array-value plumbing. Pass `options`, `value`, and
32
+ * `onValueChange`; the value is a string union you control.
33
+ */
34
+ function SegmentedControl<T extends string>({
35
+ value,
36
+ onValueChange,
37
+ options,
38
+ size = "sm",
39
+ variant = "outline",
40
+ className,
41
+ }: SegmentedControlProps<T>) {
42
+ return (
43
+ <ToggleGroup
44
+ className={cn(className)}
45
+ data-slot="segmented-control"
46
+ value={[value]}
47
+ onValueChange={(groupValue) => {
48
+ const next = Array.isArray(groupValue) ? groupValue[0] : groupValue;
49
+ // Ignore the empty-array case so a segment always stays selected.
50
+ if (next != null) onValueChange(next as T);
51
+ }}
52
+ size={size}
53
+ variant={variant}
54
+ >
55
+ {options.map((option) => (
56
+ <ToggleGroupItem key={option.value} value={option.value}>
57
+ {option.label}
58
+ </ToggleGroupItem>
59
+ ))}
60
+ </ToggleGroup>
61
+ );
62
+ }
63
+
64
+ export {
65
+ SegmentedControl,
66
+ type SegmentedControlOption,
67
+ type SegmentedControlProps,
68
+ };
package/src/index.ts CHANGED
@@ -68,6 +68,8 @@ export * from "./components/ui/section-header";
68
68
  export * from "./components/ui/smooth-scroll";
69
69
  export * from "./components/ui/social-button";
70
70
  export * from "./components/ui/stepper";
71
+ export * from "./components/ui/pipeline";
72
+ export * from "./components/ui/segmented-control";
71
73
  export * from "./components/ui/select";
72
74
  export * from "./components/ui/separator";
73
75
  export * from "./components/ui/sheet";