@olwiba/ui 0.1.9 → 0.1.11

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": "@olwiba/ui",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -18,7 +18,7 @@ export interface FlowBracketProps {
18
18
  * Defaults to 22.
19
19
  */
20
20
  extent?: number;
21
- /** Show open-chevron arrowhead at the top (destination) end. Default: true. */
21
+ /** Show open-chevron arrowhead at the destination end. Default: true. */
22
22
  arrow?: boolean;
23
23
  /** CSS color for both bracket lines and animated dots. Defaults to muted slate. */
24
24
  color?: string;
@@ -46,16 +46,14 @@ export interface FlowBracketProps {
46
46
  children: React.ReactNode;
47
47
  }
48
48
 
49
- const MARKER_L = 'fb-ml';
50
- const MARKER_R = 'fb-mr';
51
-
52
49
  /**
53
50
  * Wraps children in a relative container and draws square bracket lines on the
54
51
  * left and/or right outside edges — connecting the top of the first child down
55
52
  * to the bottom of the last child (e.g. docs → SYNC direction).
56
53
  *
57
- * Bracket lines use `overflow: visible` on the SVG to draw into the parent's padding.
58
- * Ensure the parent section/container has enough horizontal padding for `extent`.
54
+ * Arrowheads are rendered as absolutely-positioned SVGs (not SVG markers) so
55
+ * they render at a consistent pixel size regardless of the bracket SVG's aspect
56
+ * ratio (which spans the full section height with preserveAspectRatio="none").
59
57
  */
60
58
  export function FlowBracket({
61
59
  anchorTop = 8,
@@ -80,17 +78,15 @@ export function FlowBracket({
80
78
  const cl = colorLeft ?? color;
81
79
  const cr = colorRight ?? color;
82
80
 
83
- // Left bracket: bottom → top (arrowhead at top, pointing into genesis)
81
+ // Left bracket: bottom → top (arrowhead at top, pointing right into genesis)
84
82
  const leftD = `M 0,${anchorBottom} L ${L},${anchorBottom} L ${L},${anchorTop} L 0,${anchorTop}`;
85
- // Right bracket: reversed = top → bottom (arrowhead at bottom, pointing into sync)
83
+ // Right bracket: reversed = top → bottom (arrowhead at bottom, pointing left into sync)
86
84
  const rightD = reverseRight
87
85
  ? `M 1000,${anchorTop} L ${R},${anchorTop} L ${R},${anchorBottom} L 1000,${anchorBottom}`
88
86
  : `M 1000,${anchorBottom} L ${R},${anchorBottom} L ${R},${anchorTop} L 1000,${anchorTop}`;
89
87
 
90
88
  // CSS dot: extent as % of container width
91
89
  const ep = `${(extent / 10).toFixed(1)}%`;
92
- // Dots travel bottom → top (sync → genesis/docs direction), fade in/out at card edges
93
- // fb-drr: reversed — top → bottom (docs → sync direction) used when reverseRight=true
94
90
  const dotKeyframes = animate ? `
95
91
  @keyframes fb-dl{
96
92
  0%{top:${anchorBottom}%;left:0;opacity:0}
@@ -127,6 +123,25 @@ export function FlowBracket({
127
123
  opacity: 0,
128
124
  };
129
125
 
126
+ // Shared arrowhead polyline props — same chevron shape as ecosystem connector markers
127
+ const chevronProps = {
128
+ fill: 'none',
129
+ strokeWidth: '1.5',
130
+ strokeLinecap: 'round' as const,
131
+ strokeLinejoin: 'round' as const,
132
+ };
133
+
134
+ // Arrowhead SVG style — positioned in CSS space so it renders at a fixed pixel size
135
+ // regardless of the bracket SVG's non-uniform scale (preserveAspectRatio="none")
136
+ const arrowBase: React.CSSProperties = {
137
+ position: 'absolute',
138
+ width: 10,
139
+ height: 8,
140
+ overflow: 'visible',
141
+ pointerEvents: 'none',
142
+ transform: 'translate(-50%, -50%)',
143
+ };
144
+
130
145
  return (
131
146
  <div className={className} style={{ position: 'relative', ...style }}>
132
147
  {animate && <style>{dotKeyframes}</style>}
@@ -137,24 +152,42 @@ export function FlowBracket({
137
152
  className="pointer-events-none absolute inset-0"
138
153
  style={{ width: '100%', height: '100%', overflow: 'visible' }}
139
154
  >
140
- <defs>
141
- <marker id={MARKER_L} markerWidth="10" markerHeight="8" refX="8" refY="4" orient="auto">
142
- <polyline points="1,1 8,4 1,7" fill="none" stroke={cl} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
143
- </marker>
144
- <marker id={MARKER_R} markerWidth="10" markerHeight="8" refX="8" refY="4" orient="auto">
145
- <polyline points="1,1 8,4 1,7" fill="none" stroke={cr} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
146
- </marker>
147
- </defs>
148
- {left && (
149
- <path d={leftD} fill="none" stroke={cl} strokeWidth={1.5} strokeDasharray="5,4"
150
- vectorEffect="non-scaling-stroke" {...(arrow ? { markerEnd: `url(#${MARKER_L})` } : {})} />
155
+ {left && (
156
+ <path d={leftD} fill="none" style={{ stroke: cl }} strokeWidth={1.5} strokeDasharray="5,4"
157
+ vectorEffect="non-scaling-stroke" />
151
158
  )}
152
159
  {right && (
153
- <path d={rightD} fill="none" stroke={cr} strokeWidth={1.5} strokeDasharray="5,4"
154
- vectorEffect="non-scaling-stroke" {...(arrow ? { markerEnd: `url(#${MARKER_R})` } : {})} />
160
+ <path d={rightD} fill="none" style={{ stroke: cr }} strokeWidth={1.5} strokeDasharray="5,4"
161
+ vectorEffect="non-scaling-stroke" />
155
162
  )}
156
163
  </svg>
157
164
 
165
+ {/* Left bracket arrowhead — tip points right (→) into the genesis/docs tier */}
166
+ {arrow && left && (
167
+ <svg
168
+ aria-hidden
169
+ viewBox="0 0 10 8"
170
+ style={{ ...arrowBase, top: `${anchorTop}%`, left: 0 }}
171
+ >
172
+ <polyline points="1,1 8,4 1,7" style={{ stroke: cl }} {...chevronProps} />
173
+ </svg>
174
+ )}
175
+
176
+ {/* Right bracket arrowhead — tip points left (←) into the destination tier */}
177
+ {arrow && right && (
178
+ <svg
179
+ aria-hidden
180
+ viewBox="0 0 10 8"
181
+ style={{
182
+ ...arrowBase,
183
+ top: reverseRight ? `${anchorBottom}%` : `${anchorTop}%`,
184
+ left: '100%',
185
+ }}
186
+ >
187
+ <polyline points="9,1 2,4 9,7" style={{ stroke: cr }} {...chevronProps} />
188
+ </svg>
189
+ )}
190
+
158
191
  {children}
159
192
 
160
193
  {/* Dots placed after children so card stacking contexts (z-index ≥ 1) render above */}
@@ -18,7 +18,11 @@ export interface PricingCardProps extends React.HTMLAttributes<HTMLDivElement> {
18
18
  cta: string;
19
19
  highlighted?: boolean;
20
20
  disabled?: boolean;
21
+ /** Disables only the CTA button, leaving the rest of the card interactive. */
22
+ ctaDisabled?: boolean;
21
23
  badge?: React.ReactNode;
24
+ /** Rendered directly below the CTA button (e.g. a "Get notified" link). */
25
+ footer?: React.ReactNode;
22
26
  onSelect?: () => void;
23
27
  }
24
28
 
@@ -31,7 +35,9 @@ export function PricingCard({
31
35
  cta,
32
36
  highlighted = false,
33
37
  disabled = false,
38
+ ctaDisabled = false,
34
39
  badge,
40
+ footer,
35
41
  onSelect,
36
42
  className,
37
43
  ...props
@@ -72,11 +78,13 @@ export function PricingCard({
72
78
  variant={highlighted ? 'default' : 'outline'}
73
79
  className="mt-6 w-full"
74
80
  onClick={onSelect}
75
- disabled={disabled}
81
+ disabled={disabled || ctaDisabled}
76
82
  >
77
83
  {cta}
78
84
  </Button>
79
85
 
86
+ {footer && <div className="mt-3 text-center text-sm">{footer}</div>}
87
+
80
88
  <Separator className="my-6" />
81
89
 
82
90
  <ul className="space-y-3 text-sm">
@@ -15,6 +15,8 @@ export interface PricingPlan {
15
15
  cta: string;
16
16
  highlighted?: boolean;
17
17
  disabled?: boolean;
18
+ /** Disables only the CTA button, leaving the rest of the card interactive. */
19
+ ctaDisabled?: boolean;
18
20
  features: PricingFeature[];
19
21
  /** Overrides computed price display (e.g. "$?" for community/reach-out tiers). */
20
22
  priceDisplay?: string;
@@ -33,6 +35,8 @@ export interface PricingSectionProps {
33
35
  footnote?: string;
34
36
  mode?: 'subscription' | 'one-time';
35
37
  foundingDeadline?: string;
38
+ /** Rendered below each plan's CTA button (e.g. a "Get notified" link). */
39
+ renderPlanFooter?: (plan: PricingPlan) => React.ReactNode;
36
40
  }
37
41
 
38
42
  const defaultRenderLink: AppShellRenderLink = ({ href, children, className }) => (
@@ -50,6 +54,7 @@ export function PricingSection({
50
54
  footnote,
51
55
  mode = 'subscription',
52
56
  foundingDeadline,
57
+ renderPlanFooter,
53
58
  }: PricingSectionProps) {
54
59
  const [annual, setAnnual] = React.useState(false);
55
60
  const isOneTime = mode === 'one-time';
@@ -139,7 +144,9 @@ export function PricingSection({
139
144
  cta={plan.cta}
140
145
  highlighted={plan.highlighted}
141
146
  disabled={plan.disabled}
147
+ ctaDisabled={plan.ctaDisabled}
142
148
  badge={badge}
149
+ footer={renderPlanFooter?.(plan)}
143
150
  />
144
151
  );
145
152
  })}