@makolabs/ripple 1.9.2 → 1.11.0

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.
@@ -14,8 +14,12 @@
14
14
  name = '',
15
15
  showFlags = true,
16
16
  label = '',
17
+ variant = undefined,
18
+ collapsed = false,
17
19
  appearance = 'surface',
18
20
  orientation = 'horizontal',
21
+ labelLayout = undefined,
22
+ labelClass = undefined,
19
23
  color = Color.PRIMARY,
20
24
  size = Size.BASE,
21
25
  compact = false,
@@ -25,6 +29,24 @@
25
29
  testId
26
30
  }: MarketSelectorProps = $props();
27
31
 
32
+ const resolvedAppearance = $derived.by(() => {
33
+ if (variant === 'default') return 'clarkDefault';
34
+ if (variant === 'sidebar') return 'clarkSidebar';
35
+ return appearance;
36
+ });
37
+
38
+ const resolvedLabelLayout = $derived(labelLayout ?? (variant !== undefined ? 'inline' : 'above'));
39
+
40
+ const resolvedOrientation = $derived(
41
+ variant === 'sidebar' && collapsed ? 'vertical' : orientation
42
+ );
43
+
44
+ const resolvedCompact = $derived(variant === 'sidebar' && collapsed ? true : compact);
45
+
46
+ const resolvedLabelClass = $derived(
47
+ labelClass ?? (variant === 'sidebar' && collapsed && label ? 'sr-only' : undefined)
48
+ );
49
+
28
50
  const options = $derived.by((): SegmentedOption[] => {
29
51
  return markets.map((code) => ({
30
52
  value: code,
@@ -55,11 +77,13 @@
55
77
  {options}
56
78
  {name}
57
79
  {label}
58
- {appearance}
59
- {orientation}
80
+ appearance={resolvedAppearance}
81
+ orientation={resolvedOrientation}
82
+ labelLayout={resolvedLabelLayout}
83
+ labelClass={resolvedLabelClass}
60
84
  {color}
61
85
  {size}
62
- {compact}
86
+ compact={resolvedCompact}
63
87
  {disabled}
64
88
  {testId}
65
89
  onchange={handleChange}
@@ -3,7 +3,7 @@
3
3
  import { cn } from '../helper/cls.js';
4
4
  import { buildTestId } from '../helper/testid.js';
5
5
  import { Color, Size } from '../variants.js';
6
- import { segmentedTrack, segmentClasses } from './segmented-control.js';
6
+ import { segmentedLabelClasses, segmentedTrack, segmentClasses } from './segmented-control.js';
7
7
  import type { SegmentedControlProps, VariantColors, VariantSizes } from '../index.js';
8
8
 
9
9
  const groupId = `seg-${Math.random().toString(36).slice(2, 11)}`;
@@ -19,6 +19,8 @@
19
19
  color = Color.PRIMARY,
20
20
  size = Size.BASE,
21
21
  compact = false,
22
+ labelLayout = 'above',
23
+ labelClass = '',
22
24
  disabled = false,
23
25
  errors = [],
24
26
  onchange = undefined,
@@ -27,13 +29,30 @@
27
29
 
28
30
  const hasError = $derived(errors && errors.length > 0);
29
31
 
32
+ const clarkCollapsed = $derived(
33
+ compact && appearance === 'clarkSidebar' && orientation === 'vertical'
34
+ );
35
+
30
36
  const trackClass = $derived(
31
37
  cn(
32
- segmentedTrack({ appearance, orientation }),
33
- orientation === 'vertical' && compact && 'items-stretch'
38
+ segmentedTrack({
39
+ appearance,
40
+ orientation,
41
+ clarkCollapsed
42
+ }),
43
+ orientation === 'vertical' && compact && appearance !== 'clarkSidebar' && 'items-stretch'
34
44
  )
35
45
  );
36
46
 
47
+ const rootClass = $derived(
48
+ cn(
49
+ labelLayout === 'inline' ? 'flex flex-row items-center gap-2' : 'flex flex-col gap-2',
50
+ className
51
+ )
52
+ );
53
+
54
+ const labelTextClass = $derived(cn(segmentedLabelClasses(appearance, !!hasError), labelClass));
55
+
37
56
  async function handleSelect(optionValue: string, optionDisabled: boolean | undefined) {
38
57
  if (disabled || optionDisabled) return;
39
58
  if (optionValue === value) return;
@@ -91,20 +110,19 @@
91
110
  }
92
111
  }
93
112
  }
113
+
114
+ const isClark = $derived(appearance === 'clarkDefault' || appearance === 'clarkSidebar');
94
115
  </script>
95
116
 
96
- <div class={cn('flex flex-col gap-2', className)}>
97
- {#if hasError}
98
- <div class="text-danger-500 text-sm">{errors[0]}</div>
99
- {/if}
117
+ <div class={rootClass}>
100
118
  {#if label}
101
- <span
102
- id="{groupId}-label"
103
- class={cn('text-sm font-medium', hasError ? 'text-danger-500' : 'text-default-700')}
104
- >
119
+ <span id="{groupId}-label" class={labelTextClass}>
105
120
  {label}
106
121
  </span>
107
122
  {/if}
123
+ {#if hasError}
124
+ <div class="text-danger-500 text-sm">{errors[0]}</div>
125
+ {/if}
108
126
 
109
127
  <div
110
128
  class={trackClass}
@@ -134,7 +152,8 @@
134
152
  size: size as VariantSizes,
135
153
  isFirst,
136
154
  isLast,
137
- orientation
155
+ orientation,
156
+ clarkCollapsed
138
157
  })}
139
158
  data-testid={buildTestId('segmented', 'item', testId, index)}
140
159
  onclick={() => handleSelect(option.value, option.disabled)}
@@ -143,7 +162,13 @@
143
162
  {#if option.prefix}
144
163
  <span class="text-base leading-none" aria-hidden="true">{option.prefix}</span>
145
164
  {/if}
146
- <span class:text-sm={size === Size.XS || size === Size.SM} class:sr-only={compact}>
165
+ <span
166
+ class={cn(
167
+ !compact && (size === Size.XS || size === Size.SM) && !isClark && 'text-sm',
168
+ isClark && !compact && 'text-xs font-medium',
169
+ compact && 'sr-only'
170
+ )}
171
+ >
147
172
  {option.label}
148
173
  </span>
149
174
  </button>
@@ -180,13 +180,20 @@ export type SegmentedControlProps = {
180
180
  name?: string;
181
181
  label?: string;
182
182
  class?: ClassValue;
183
- /** Light track (e.g. page toolbar) vs dark track (e.g. sidebar) */
184
- appearance?: 'surface' | 'inverted';
183
+ /**
184
+ * - `surface` / `inverted`: generic Ripple styling (`color` applies).
185
+ * - `clarkDefault` / `clarkSidebar`: match Clark AR market bar (fixed blue selection, inline-friendly).
186
+ */
187
+ appearance?: 'surface' | 'inverted' | 'clarkDefault' | 'clarkSidebar';
185
188
  orientation?: 'horizontal' | 'vertical';
186
189
  color?: VariantColors;
187
190
  size?: VariantSizes;
188
- /** Hide label text; keep prefix only (label is `sr-only` for screen readers) */
191
+ /** Hide segment label text; prefix only (`sr-only`); with Clark sidebar + vertical = collapsed column */
189
192
  compact?: boolean;
193
+ /** Label above the track vs beside the track (Clark uses `inline`) */
194
+ labelLayout?: 'above' | 'inline';
195
+ /** Optional class on the visible label (e.g. `sr-only` when collapsed sidebar) */
196
+ labelClass?: ClassValue;
190
197
  disabled?: boolean;
191
198
  errors?: string[];
192
199
  onchange?: (value: string) => void;
@@ -1,6 +1,8 @@
1
1
  import type { ClassValue } from 'tailwind-variants';
2
2
  import type { CountryCode } from './country-data.js';
3
3
  import type { VariantColors, VariantSizes } from '../../index.js';
4
+ /** Clark AR toolbar (`default`) vs dark sidebar (`sidebar`) — maps to SegmentedControl Clark appearances. */
5
+ export type MarketSelectorVariant = 'default' | 'sidebar';
4
6
  export type MarketSelectorProps = {
5
7
  /**
6
8
  * Non-empty ordered list of ISO 3166-1 alpha-2 codes to show (e.g. `[Market.DE, Market.GB, Market.CH]`).
@@ -13,8 +15,21 @@ export type MarketSelectorProps = {
13
15
  defaultMarket?: CountryCode;
14
16
  showFlags?: boolean;
15
17
  label?: string;
16
- appearance?: 'surface' | 'inverted';
18
+ /**
19
+ * Clark AR–style market bar. When set, overrides `appearance` to `clarkDefault` / `clarkSidebar`
20
+ * and defaults `labelLayout` to `inline` (override with `labelLayout`).
21
+ */
22
+ variant?: MarketSelectorVariant;
23
+ /**
24
+ * With `variant="sidebar"` only: vertical icon column and `sr-only` segment labels (Clark collapsed sidebar).
25
+ */
26
+ collapsed?: boolean;
27
+ appearance?: 'surface' | 'inverted' | 'clarkDefault' | 'clarkSidebar';
17
28
  orientation?: 'horizontal' | 'vertical';
29
+ /** Label above vs beside the track (Clark uses `inline` via `variant`). */
30
+ labelLayout?: 'above' | 'inline';
31
+ /** Extra classes on the visible label (e.g. override default `sr-only` when collapsed). */
32
+ labelClass?: ClassValue;
18
33
  color?: VariantColors;
19
34
  size?: VariantSizes;
20
35
  compact?: boolean;
@@ -1,39 +1,72 @@
1
1
  import type { VariantColors, VariantSizes } from '../index.js';
2
+ /** @see SegmentedControlProps['appearance'] */
3
+ export type SegmentAppearance = 'surface' | 'inverted' | 'clarkDefault' | 'clarkSidebar';
2
4
  export declare const segmentedTrack: import("tailwind-variants").TVReturnType<{
3
5
  appearance: {
4
6
  surface: string;
5
7
  inverted: string;
8
+ /** Clark (light toolbar): flush segments, gray border, white track */
9
+ clarkDefault: string;
10
+ /** Clark (dark sidebar): translucent track */
11
+ clarkSidebar: string;
6
12
  };
7
13
  orientation: {
8
14
  horizontal: string;
9
15
  vertical: string;
10
16
  };
11
- }, undefined, "inline-flex overflow-hidden rounded-lg border p-0.5", {
17
+ /** Collapsed sidebar column layout (Clark) */
18
+ clarkCollapsed: {
19
+ true: string;
20
+ false: string;
21
+ };
22
+ }, undefined, "inline-flex overflow-hidden rounded-lg border", {
12
23
  appearance: {
13
24
  surface: string;
14
25
  inverted: string;
26
+ /** Clark (light toolbar): flush segments, gray border, white track */
27
+ clarkDefault: string;
28
+ /** Clark (dark sidebar): translucent track */
29
+ clarkSidebar: string;
15
30
  };
16
31
  orientation: {
17
32
  horizontal: string;
18
33
  vertical: string;
19
34
  };
35
+ /** Collapsed sidebar column layout (Clark) */
36
+ clarkCollapsed: {
37
+ true: string;
38
+ false: string;
39
+ };
20
40
  }, undefined, import("tailwind-variants").TVReturnType<{
21
41
  appearance: {
22
42
  surface: string;
23
43
  inverted: string;
44
+ /** Clark (light toolbar): flush segments, gray border, white track */
45
+ clarkDefault: string;
46
+ /** Clark (dark sidebar): translucent track */
47
+ clarkSidebar: string;
24
48
  };
25
49
  orientation: {
26
50
  horizontal: string;
27
51
  vertical: string;
28
52
  };
29
- }, undefined, "inline-flex overflow-hidden rounded-lg border p-0.5", unknown, unknown, undefined>>;
53
+ /** Collapsed sidebar column layout (Clark) */
54
+ clarkCollapsed: {
55
+ true: string;
56
+ false: string;
57
+ };
58
+ }, undefined, "inline-flex overflow-hidden rounded-lg border", unknown, unknown, undefined>>;
30
59
  export declare function segmentClasses(args: {
31
60
  selected: boolean;
32
61
  disabled: boolean;
33
- appearance: 'surface' | 'inverted';
62
+ appearance: SegmentAppearance;
34
63
  color: VariantColors;
35
64
  size: VariantSizes;
36
65
  isFirst: boolean;
37
66
  isLast: boolean;
38
67
  orientation: 'horizontal' | 'vertical';
68
+ /** Clark sidebar collapsed: stacked segments, smaller padding */
69
+ clarkCollapsed?: boolean;
39
70
  }): string;
71
+ /** Label next to / above the track (Clark uses muted gray or default-300 in sidebar). */
72
+ export declare function segmentedLabelClasses(appearance: SegmentAppearance, hasError: boolean): string;
@@ -2,20 +2,37 @@ import { tv } from 'tailwind-variants';
2
2
  import { cn } from '../helper/cls.js';
3
3
  import { Color, Size } from '../variants.js';
4
4
  export const segmentedTrack = tv({
5
- base: 'inline-flex overflow-hidden rounded-lg border p-0.5',
5
+ base: 'inline-flex overflow-hidden rounded-lg border',
6
6
  variants: {
7
7
  appearance: {
8
- surface: 'border-default-200 bg-default-50',
9
- inverted: 'border-white/20 bg-white/5'
8
+ surface: 'border-default-200 bg-default-50 p-0.5',
9
+ inverted: 'border-white/20 bg-white/5 p-0.5',
10
+ /** Clark (light toolbar): flush segments, gray border, white track */
11
+ clarkDefault: 'items-center border-gray-300 bg-white p-0',
12
+ /** Clark (dark sidebar): translucent track */
13
+ clarkSidebar: 'items-center border-white/20 bg-white/5 p-0'
10
14
  },
11
15
  orientation: {
12
16
  horizontal: 'flex-row',
13
17
  vertical: 'w-full flex-col'
18
+ },
19
+ /** Collapsed sidebar column layout (Clark) */
20
+ clarkCollapsed: {
21
+ true: 'gap-1',
22
+ false: ''
14
23
  }
15
24
  },
25
+ compoundVariants: [
26
+ {
27
+ appearance: ['clarkDefault', 'clarkSidebar'],
28
+ orientation: 'vertical',
29
+ class: 'items-center'
30
+ }
31
+ ],
16
32
  defaultVariants: {
17
33
  appearance: 'surface',
18
- orientation: 'horizontal'
34
+ orientation: 'horizontal',
35
+ clarkCollapsed: false
19
36
  }
20
37
  });
21
38
  const selectedByColor = {
@@ -35,8 +52,31 @@ const segmentSize = {
35
52
  [Size.XL]: 'gap-2 px-4 py-2.5 text-lg',
36
53
  [Size.XXL]: 'gap-2 px-5 py-3 text-xl'
37
54
  };
55
+ function isClark(appearance) {
56
+ return appearance === 'clarkDefault' || appearance === 'clarkSidebar';
57
+ }
38
58
  export function segmentClasses(args) {
39
- const { selected, disabled, appearance, color, size, isFirst, isLast, orientation } = args;
59
+ const { selected, disabled, appearance, color, size, isFirst, isLast, orientation, clarkCollapsed } = args;
60
+ if (isClark(appearance)) {
61
+ const isSidebar = appearance === 'clarkSidebar';
62
+ const rounding = clarkCollapsed && isSidebar
63
+ ? 'rounded-md'
64
+ : orientation === 'horizontal'
65
+ ? cn(isFirst && 'rounded-l-lg', isLast && 'rounded-r-lg')
66
+ : cn(isFirst && 'rounded-t-lg', isLast && 'rounded-b-lg');
67
+ const clarkPad = clarkCollapsed && isSidebar ? 'justify-center px-2 py-1.5' : 'gap-1.5 px-3 py-1.5';
68
+ const clarkBase = cn('group relative flex cursor-pointer items-center transition-all duration-150', clarkPad, 'focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:outline-none', isSidebar ? 'focus-visible:ring-offset-0' : 'focus-visible:ring-offset-2', rounding, disabled && 'cursor-not-allowed opacity-50');
69
+ if (disabled) {
70
+ return cn(clarkBase, 'text-default-400');
71
+ }
72
+ if (selected) {
73
+ return cn(clarkBase, 'bg-blue-600 text-white');
74
+ }
75
+ if (appearance === 'clarkDefault') {
76
+ return cn(clarkBase, 'bg-white text-gray-600 hover:bg-gray-50');
77
+ }
78
+ return cn(clarkBase, 'text-default-200 hover:bg-white/10');
79
+ }
40
80
  const rounding = orientation === 'horizontal'
41
81
  ? cn(isFirst && 'rounded-l-md', isLast && 'rounded-r-md')
42
82
  : cn(isFirst && 'rounded-t-md', isLast && 'rounded-b-md');
@@ -48,7 +88,20 @@ export function segmentClasses(args) {
48
88
  return cn(base, selectedByColor[color]);
49
89
  }
50
90
  if (appearance === 'surface') {
51
- return cn(base, 'text-default-700 hover:bg-white/90', 'bg-transparent');
91
+ return cn(base, 'bg-transparent text-default-700 hover:bg-white/90');
92
+ }
93
+ return cn(base, 'bg-transparent text-default-200 hover:bg-white/10');
94
+ }
95
+ /** Label next to / above the track (Clark uses muted gray or default-300 in sidebar). */
96
+ export function segmentedLabelClasses(appearance, hasError) {
97
+ if (hasError) {
98
+ return 'text-sm font-medium text-danger-500';
99
+ }
100
+ if (appearance === 'clarkDefault') {
101
+ return 'text-sm text-gray-500';
102
+ }
103
+ if (appearance === 'clarkSidebar') {
104
+ return 'text-xs text-default-300';
52
105
  }
53
- return cn(base, 'text-default-200 hover:bg-white/10', 'bg-transparent');
106
+ return cn('text-sm font-medium', 'text-default-700');
54
107
  }
package/dist/index.d.ts CHANGED
@@ -34,7 +34,7 @@ export type { CountryCode } from './forms/market/country-data.js';
34
34
  export type { MarketCode } from './forms/market/market.js';
35
35
  export { ALL_COUNTRY_CODES, COUNTRY_NAMES } from './forms/market/country-data.js';
36
36
  export { Market } from './forms/market/market.js';
37
- export type { MarketSelectorProps } from './forms/market/market-selector-types.js';
37
+ export type { MarketSelectorProps, MarketSelectorVariant } from './forms/market/market-selector-types.js';
38
38
  export { countryCodeToFlagEmoji } from './forms/market/flag-emoji.js';
39
39
  export type { ProgressSegment, ProgressProps } from './elements/progress/progress-types.js';
40
40
  export type { AccordionProps } from './elements/accordion/accordion-types.js';
@@ -49,7 +49,8 @@ export { isRouteActive } from './helper/nav.svelte.js';
49
49
  export { default as Button } from './button/Button.svelte';
50
50
  export { default as Modal } from './modal/Modal.svelte';
51
51
  export { default as Pipeline } from './pipeline/Pipeline.svelte';
52
- export type { PipelineStage } from './pipeline/Pipeline.svelte';
52
+ export { pipelineVariants } from './pipeline/pipeline.js';
53
+ export type { PipelineStage, PipelineStageColor, PipelineStageEvent, PipelineStageClickEvent, PipelineStagePointerEvent, PipelineProps } from './pipeline/pipeline-types.js';
53
54
  export { default as Drawer } from './drawer/Drawer.svelte';
54
55
  export { default as PageHeader } from './header/PageHeader.svelte';
55
56
  export { default as Breadcrumbs } from './header/Breadcrumbs.svelte';
@@ -106,7 +107,8 @@ export { metricCard } from './layout/card/metric-card.js';
106
107
  export { rankedCard } from './layout/card/ranked-card.js';
107
108
  export { activityList } from './layout/activity-list/activity-list.js';
108
109
  export { slider } from './forms/slider.js';
109
- export { segmentedTrack, segmentClasses } from './forms/segmented-control.js';
110
+ export type { SegmentAppearance } from './forms/segmented-control.js';
111
+ export { segmentedTrack, segmentClasses, segmentedLabelClasses } from './forms/segmented-control.js';
110
112
  export { CompactFilters } from './filters/index.js';
111
113
  export * from './file-browser/index.js';
112
114
  export * from './adapters/storage/index.js';
package/dist/index.js CHANGED
@@ -26,6 +26,7 @@ export { default as Button } from './button/Button.svelte';
26
26
  export { default as Modal } from './modal/Modal.svelte';
27
27
  // Pipeline
28
28
  export { default as Pipeline } from './pipeline/Pipeline.svelte';
29
+ export { pipelineVariants } from './pipeline/pipeline.js';
29
30
  // Drawer
30
31
  export { default as Drawer } from './drawer/Drawer.svelte';
31
32
  // Header
@@ -103,7 +104,7 @@ export { metricCard } from './layout/card/metric-card.js';
103
104
  export { rankedCard } from './layout/card/ranked-card.js';
104
105
  export { activityList } from './layout/activity-list/activity-list.js';
105
106
  export { slider } from './forms/slider.js';
106
- export { segmentedTrack, segmentClasses } from './forms/segmented-control.js';
107
+ export { segmentedTrack, segmentClasses, segmentedLabelClasses } from './forms/segmented-control.js';
107
108
  // ============================================================================
108
109
  // Re-export filters
109
110
  // ============================================================================
@@ -1,174 +1,123 @@
1
1
  <script lang="ts">
2
- import { tv } from 'tailwind-variants';
3
- import type { Snippet } from 'svelte';
2
+ import { cn } from '../helper/cls.js';
3
+ import { pipelineVariants } from './pipeline.js';
4
+ import type { PipelineProps, PipelineStage } from '../index.js';
4
5
 
5
- export type PipelineStage = {
6
- label: string;
7
- count?: number | string;
8
- color?: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
9
- active?: boolean;
10
- };
11
-
12
- interface Props {
13
- stages: PipelineStage[];
14
- class?: string;
15
- size?: 'sm' | 'base' | 'lg';
16
- equalWidth?: boolean;
17
- children?: Snippet<[PipelineStage, number]>;
18
- beneathChildren?: Snippet<[PipelineStage, number]>;
19
- }
6
+ const CHEVRON_WIDTH = 20;
20
7
 
21
8
  let {
22
9
  stages = [],
23
10
  class: className = '',
24
11
  size = 'base',
25
12
  equalWidth = true,
13
+ selectedClass,
14
+ unselectedClass,
15
+ disabledClass,
16
+ onstageclick,
17
+ onstagehover,
18
+ onstageleave,
26
19
  children,
27
20
  beneathChildren
28
- }: Props = $props();
29
-
30
- const pipeline = tv({
31
- slots: {
32
- container: 'flex items-center gap-0 w-full',
33
- stage: 'relative transition-all duration-300',
34
- borderLayer: 'absolute inset-0 transition-all duration-300',
35
- innerLayer: 'relative flex items-center justify-center bg-white transition-all duration-300',
36
- content: 'text-center flex flex-col items-center justify-center',
37
- label: 'font-medium text-default-600 leading-tight break-words',
38
- count: 'font-bold text-default-900'
39
- },
40
- variants: {
41
- size: {
42
- sm: {
43
- stage: 'min-w-[130px]',
44
- borderLayer: 'h-20',
45
- innerLayer: 'h-20',
46
- label: 'text-[11px] max-w-[100px]',
47
- count: 'text-xl mt-0.5',
48
- content: 'px-4'
49
- },
50
- base: {
51
- stage: 'min-w-[150px]',
52
- borderLayer: 'h-24',
53
- innerLayer: 'h-24',
54
- label: 'text-xs max-w-[120px]',
55
- count: 'text-2xl mt-1',
56
- content: 'px-5'
57
- },
58
- lg: {
59
- stage: 'min-w-[180px]',
60
- borderLayer: 'h-28',
61
- innerLayer: 'h-28',
62
- label: 'text-sm max-w-[150px]',
63
- count: 'text-3xl mt-1',
64
- content: 'px-6'
65
- }
66
- },
67
- color: {
68
- default: {
69
- borderLayer: 'bg-default-200'
70
- },
71
- primary: {
72
- borderLayer: 'bg-blue-500',
73
- label: 'text-default-700',
74
- count: 'text-default-900'
75
- },
76
- success: {
77
- borderLayer: 'bg-green-500',
78
- label: 'text-default-700',
79
- count: 'text-default-900'
80
- },
81
- warning: {
82
- borderLayer: 'bg-amber-500',
83
- label: 'text-default-700',
84
- count: 'text-default-900'
85
- },
86
- danger: {
87
- borderLayer: 'bg-rose-500',
88
- label: 'text-default-700',
89
- count: 'text-default-900'
90
- },
91
- info: {
92
- borderLayer: 'bg-purple-500',
93
- label: 'text-default-700',
94
- count: 'text-default-900'
95
- }
96
- },
97
- active: {
98
- true: {
99
- borderLayer: 'ring-2 ring-blue-500 ring-offset-2'
100
- }
101
- },
102
- equalWidth: {
103
- true: {
104
- stage: 'flex-1'
105
- },
106
- false: {
107
- stage: ''
108
- }
109
- }
110
- }
111
- });
21
+ }: PipelineProps = $props();
112
22
 
113
- const styles = $derived(pipeline({ size, equalWidth }));
23
+ const containerStyles = $derived(pipelineVariants({ size, equalWidth }));
114
24
 
115
25
  function getStageStyles(stage: PipelineStage) {
116
- return pipeline({
26
+ return pipelineVariants({
117
27
  size,
118
- color: stage.color || 'default',
28
+ color: stage.color ?? 'default',
119
29
  active: stage.active,
120
- equalWidth
30
+ equalWidth,
31
+ interactive: !!onstageclick && !stage.disabled,
32
+ disabled: stage.disabled
121
33
  });
122
34
  }
35
+
36
+ function borderClassesFor(stage: PipelineStage, base: string): string {
37
+ return cn(base, stage.active ? selectedClass : unselectedClass);
38
+ }
39
+
40
+ function borderClipPath(isFirst: boolean): string {
41
+ const leftTip = isFirst ? '0' : `${CHEVRON_WIDTH}px`;
42
+ return `polygon(0 0%, calc(100% - ${CHEVRON_WIDTH}px) 0%, 100% 50%, calc(100% - ${CHEVRON_WIDTH}px) 100%, 0 100%, ${leftTip} 50%)`;
43
+ }
44
+
45
+ function innerClipPath(isFirst: boolean): string {
46
+ const leftTip = isFirst ? '2px' : `${CHEVRON_WIDTH + 2}px`;
47
+ return `polygon(2px 2px, calc(100% - ${CHEVRON_WIDTH}px - 2px) 2px, calc(100% - 2px) 50%, calc(100% - ${CHEVRON_WIDTH}px - 2px) calc(100% - 2px), 2px calc(100% - 2px), ${leftTip} 50%)`;
48
+ }
49
+
50
+ function handleStageClick(stage: PipelineStage, index: number, event: MouseEvent) {
51
+ if (stage.disabled || !onstageclick) return;
52
+ onstageclick({ stage, index, event });
53
+ }
54
+
55
+ function handleStageMouseEnter(stage: PipelineStage, index: number, event: MouseEvent) {
56
+ if (stage.disabled || !onstagehover) return;
57
+ onstagehover({ stage, index, event });
58
+ }
59
+
60
+ function handleStageMouseLeave(stage: PipelineStage, index: number, event: MouseEvent) {
61
+ if (stage.disabled || !onstageleave) return;
62
+ onstageleave({ stage, index, event });
63
+ }
123
64
  </script>
124
65
 
125
- <div class="{styles.container()} {className}">
66
+ {#snippet stageContent(stage: PipelineStage, index: number, s: ReturnType<typeof getStageStyles>)}
67
+ <div class={s.content()}>
68
+ {#if children}
69
+ {@render children(stage, index)}
70
+ {:else}
71
+ <span class={s.label()}>{stage.label}</span>
72
+ {#if stage.count !== undefined}
73
+ <span class={s.count()}>{stage.count}</span>
74
+ {/if}
75
+ {/if}
76
+ </div>
77
+ {/snippet}
78
+
79
+ <div class={cn(containerStyles.container(), className)}>
126
80
  {#each stages as stage, index (stage.label + index)}
81
+ {@const s = getStageStyles(stage)}
127
82
  {@const isFirst = index === 0}
128
- {@const chevronWidth = 20}
83
+ {@const renderAsButton = !!onstageclick}
84
+ {@const stageStyle = isFirst
85
+ ? `z-index: ${stages.length - index}`
86
+ : `margin-left: -${CHEVRON_WIDTH}px; z-index: ${stages.length - index}`}
129
87
 
130
88
  <div
131
- class="flex flex-col {getStageStyles(stage).stage()}"
132
- style={!isFirst
133
- ? `margin-left: -${chevronWidth}px; z-index: ${stages.length - index}`
134
- : `z-index: ${stages.length - index}`}
89
+ class={cn('flex flex-col', s.stage(), stage.disabled ? disabledClass : undefined)}
90
+ style={stageStyle}
91
+ data-pipeline-stage=""
92
+ role={onstagehover || onstageleave ? 'group' : undefined}
93
+ aria-label={onstagehover || onstageleave ? stage.label : undefined}
94
+ aria-disabled={stage.disabled ? 'true' : undefined}
95
+ onmouseenter={onstagehover ? (e) => handleStageMouseEnter(stage, index, e) : undefined}
96
+ onmouseleave={onstageleave ? (e) => handleStageMouseLeave(stage, index, e) : undefined}
135
97
  >
136
- <!-- BACKGROUND LAYER (Border color - larger) -->
137
98
  <div
138
- class={getStageStyles(stage).borderLayer()}
139
- style="clip-path: polygon({isFirst
140
- ? '0'
141
- : '0'} 0%, calc(100% - {chevronWidth}px) 0%, 100% 50%, calc(100% - {chevronWidth}px) 100%, {isFirst
142
- ? '0'
143
- : '0'} 100%, {isFirst ? '0' : `${chevronWidth}px`} 50%);"
99
+ class={borderClassesFor(stage, s.borderLayer())}
100
+ data-pipeline-border=""
101
+ style="clip-path: {borderClipPath(isFirst)};"
144
102
  ></div>
145
103
 
146
- <!-- FOREGROUND LAYER (White - smaller, creates border effect) -->
147
- <div
148
- class={getStageStyles(stage).innerLayer()}
149
- style="clip-path: polygon({isFirst
150
- ? '2px'
151
- : '2px'} 2px, calc(100% - {chevronWidth}px - 2px) 2px, calc(100% - 2px) 50%, calc(100% - {chevronWidth}px - 2px) calc(100% - 2px), {isFirst
152
- ? '2px'
153
- : '2px'} calc(100% - 2px), {isFirst ? '2px' : `${chevronWidth + 2}px`} 50%);"
154
- >
155
- <div class={getStageStyles(stage).content()}>
156
- {#if children}
157
- {@render children(stage, index)}
158
- {:else}
159
- <span class={getStageStyles(stage).label()}>
160
- {stage.label}
161
- </span>
162
- {#if stage.count !== undefined}
163
- <span class={getStageStyles(stage).count()}>
164
- {stage.count}
165
- </span>
166
- {/if}
167
- {/if}
104
+ {#if renderAsButton}
105
+ <button
106
+ type="button"
107
+ class={s.innerLayer()}
108
+ style="clip-path: {innerClipPath(isFirst)};"
109
+ disabled={stage.disabled}
110
+ aria-disabled={stage.disabled ? 'true' : undefined}
111
+ onclick={(e) => handleStageClick(stage, index, e)}
112
+ >
113
+ {@render stageContent(stage, index, s)}
114
+ </button>
115
+ {:else}
116
+ <div class={s.innerLayer()} style="clip-path: {innerClipPath(isFirst)};">
117
+ {@render stageContent(stage, index, s)}
168
118
  </div>
169
- </div>
119
+ {/if}
170
120
 
171
- <!-- Beneath Children - Rendered directly below this stage -->
172
121
  {#if beneathChildren}
173
122
  <div class="mt-4">
174
123
  {@render beneathChildren(stage, index)}
@@ -1,18 +1,4 @@
1
- import type { Snippet } from 'svelte';
2
- export type PipelineStage = {
3
- label: string;
4
- count?: number | string;
5
- color?: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
6
- active?: boolean;
7
- };
8
- interface Props {
9
- stages: PipelineStage[];
10
- class?: string;
11
- size?: 'sm' | 'base' | 'lg';
12
- equalWidth?: boolean;
13
- children?: Snippet<[PipelineStage, number]>;
14
- beneathChildren?: Snippet<[PipelineStage, number]>;
15
- }
16
- declare const Pipeline: import("svelte").Component<Props, {}, "">;
1
+ import type { PipelineProps } from '../index.js';
2
+ declare const Pipeline: import("svelte").Component<PipelineProps, {}, "">;
17
3
  type Pipeline = ReturnType<typeof Pipeline>;
18
4
  export default Pipeline;
@@ -0,0 +1,31 @@
1
+ import type { ClassValue } from 'tailwind-variants';
2
+ import type { Snippet } from 'svelte';
3
+ export type PipelineStageColor = 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
4
+ export type PipelineStage = {
5
+ label: string;
6
+ count?: number | string;
7
+ color?: PipelineStageColor;
8
+ active?: boolean;
9
+ disabled?: boolean;
10
+ };
11
+ export type PipelineStageEvent<E extends Event = Event> = {
12
+ stage: PipelineStage;
13
+ index: number;
14
+ event: E;
15
+ };
16
+ export type PipelineStageClickEvent = PipelineStageEvent<MouseEvent>;
17
+ export type PipelineStagePointerEvent = PipelineStageEvent<MouseEvent>;
18
+ export type PipelineProps = {
19
+ stages: PipelineStage[];
20
+ class?: ClassValue;
21
+ size?: 'sm' | 'base' | 'lg';
22
+ equalWidth?: boolean;
23
+ children?: Snippet<[PipelineStage, number]>;
24
+ beneathChildren?: Snippet<[PipelineStage, number]>;
25
+ selectedClass?: ClassValue;
26
+ unselectedClass?: ClassValue;
27
+ disabledClass?: ClassValue;
28
+ onstageclick?: (e: PipelineStageClickEvent) => void;
29
+ onstagehover?: (e: PipelineStagePointerEvent) => void;
30
+ onstageleave?: (e: PipelineStagePointerEvent) => void;
31
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,256 @@
1
+ export declare const pipelineVariants: import("tailwind-variants").TVReturnType<{
2
+ size: {
3
+ sm: {
4
+ stage: string;
5
+ borderLayer: string;
6
+ innerLayer: string;
7
+ label: string;
8
+ count: string;
9
+ content: string;
10
+ };
11
+ base: {
12
+ stage: string;
13
+ borderLayer: string;
14
+ innerLayer: string;
15
+ label: string;
16
+ count: string;
17
+ content: string;
18
+ };
19
+ lg: {
20
+ stage: string;
21
+ borderLayer: string;
22
+ innerLayer: string;
23
+ label: string;
24
+ count: string;
25
+ content: string;
26
+ };
27
+ };
28
+ color: {
29
+ default: {
30
+ borderLayer: string;
31
+ };
32
+ primary: {
33
+ borderLayer: string;
34
+ label: string;
35
+ count: string;
36
+ };
37
+ success: {
38
+ borderLayer: string;
39
+ label: string;
40
+ count: string;
41
+ };
42
+ warning: {
43
+ borderLayer: string;
44
+ label: string;
45
+ count: string;
46
+ };
47
+ danger: {
48
+ borderLayer: string;
49
+ label: string;
50
+ count: string;
51
+ };
52
+ info: {
53
+ borderLayer: string;
54
+ label: string;
55
+ count: string;
56
+ };
57
+ };
58
+ active: {
59
+ true: {
60
+ borderLayer: string;
61
+ };
62
+ };
63
+ equalWidth: {
64
+ true: {
65
+ stage: string;
66
+ };
67
+ };
68
+ interactive: {
69
+ true: {
70
+ innerLayer: string;
71
+ };
72
+ };
73
+ disabled: {
74
+ true: {
75
+ stage: string;
76
+ };
77
+ };
78
+ }, {
79
+ container: string;
80
+ stage: string;
81
+ borderLayer: string;
82
+ innerLayer: string;
83
+ content: string;
84
+ label: string;
85
+ count: string;
86
+ }, undefined, {
87
+ size: {
88
+ sm: {
89
+ stage: string;
90
+ borderLayer: string;
91
+ innerLayer: string;
92
+ label: string;
93
+ count: string;
94
+ content: string;
95
+ };
96
+ base: {
97
+ stage: string;
98
+ borderLayer: string;
99
+ innerLayer: string;
100
+ label: string;
101
+ count: string;
102
+ content: string;
103
+ };
104
+ lg: {
105
+ stage: string;
106
+ borderLayer: string;
107
+ innerLayer: string;
108
+ label: string;
109
+ count: string;
110
+ content: string;
111
+ };
112
+ };
113
+ color: {
114
+ default: {
115
+ borderLayer: string;
116
+ };
117
+ primary: {
118
+ borderLayer: string;
119
+ label: string;
120
+ count: string;
121
+ };
122
+ success: {
123
+ borderLayer: string;
124
+ label: string;
125
+ count: string;
126
+ };
127
+ warning: {
128
+ borderLayer: string;
129
+ label: string;
130
+ count: string;
131
+ };
132
+ danger: {
133
+ borderLayer: string;
134
+ label: string;
135
+ count: string;
136
+ };
137
+ info: {
138
+ borderLayer: string;
139
+ label: string;
140
+ count: string;
141
+ };
142
+ };
143
+ active: {
144
+ true: {
145
+ borderLayer: string;
146
+ };
147
+ };
148
+ equalWidth: {
149
+ true: {
150
+ stage: string;
151
+ };
152
+ };
153
+ interactive: {
154
+ true: {
155
+ innerLayer: string;
156
+ };
157
+ };
158
+ disabled: {
159
+ true: {
160
+ stage: string;
161
+ };
162
+ };
163
+ }, {
164
+ container: string;
165
+ stage: string;
166
+ borderLayer: string;
167
+ innerLayer: string;
168
+ content: string;
169
+ label: string;
170
+ count: string;
171
+ }, import("tailwind-variants").TVReturnType<{
172
+ size: {
173
+ sm: {
174
+ stage: string;
175
+ borderLayer: string;
176
+ innerLayer: string;
177
+ label: string;
178
+ count: string;
179
+ content: string;
180
+ };
181
+ base: {
182
+ stage: string;
183
+ borderLayer: string;
184
+ innerLayer: string;
185
+ label: string;
186
+ count: string;
187
+ content: string;
188
+ };
189
+ lg: {
190
+ stage: string;
191
+ borderLayer: string;
192
+ innerLayer: string;
193
+ label: string;
194
+ count: string;
195
+ content: string;
196
+ };
197
+ };
198
+ color: {
199
+ default: {
200
+ borderLayer: string;
201
+ };
202
+ primary: {
203
+ borderLayer: string;
204
+ label: string;
205
+ count: string;
206
+ };
207
+ success: {
208
+ borderLayer: string;
209
+ label: string;
210
+ count: string;
211
+ };
212
+ warning: {
213
+ borderLayer: string;
214
+ label: string;
215
+ count: string;
216
+ };
217
+ danger: {
218
+ borderLayer: string;
219
+ label: string;
220
+ count: string;
221
+ };
222
+ info: {
223
+ borderLayer: string;
224
+ label: string;
225
+ count: string;
226
+ };
227
+ };
228
+ active: {
229
+ true: {
230
+ borderLayer: string;
231
+ };
232
+ };
233
+ equalWidth: {
234
+ true: {
235
+ stage: string;
236
+ };
237
+ };
238
+ interactive: {
239
+ true: {
240
+ innerLayer: string;
241
+ };
242
+ };
243
+ disabled: {
244
+ true: {
245
+ stage: string;
246
+ };
247
+ };
248
+ }, {
249
+ container: string;
250
+ stage: string;
251
+ borderLayer: string;
252
+ innerLayer: string;
253
+ content: string;
254
+ label: string;
255
+ count: string;
256
+ }, undefined, unknown, unknown, undefined>>;
@@ -0,0 +1,91 @@
1
+ import { tv } from 'tailwind-variants';
2
+ export const pipelineVariants = tv({
3
+ slots: {
4
+ container: 'flex items-center gap-0 w-full',
5
+ stage: 'relative transition-all duration-300',
6
+ borderLayer: 'absolute inset-0 transition-all duration-300',
7
+ innerLayer: 'relative flex items-center justify-center bg-white transition-all duration-300 focus:outline-none',
8
+ content: 'text-center flex flex-col items-center justify-center',
9
+ label: 'font-medium text-default-600 leading-tight break-words',
10
+ count: 'font-bold text-default-900'
11
+ },
12
+ variants: {
13
+ size: {
14
+ sm: {
15
+ stage: 'min-w-[130px]',
16
+ borderLayer: 'h-20',
17
+ innerLayer: 'h-20',
18
+ label: 'text-[11px] max-w-[100px]',
19
+ count: 'text-xl mt-0.5',
20
+ content: 'px-4'
21
+ },
22
+ base: {
23
+ stage: 'min-w-[150px]',
24
+ borderLayer: 'h-24',
25
+ innerLayer: 'h-24',
26
+ label: 'text-xs max-w-[120px]',
27
+ count: 'text-2xl mt-1',
28
+ content: 'px-5'
29
+ },
30
+ lg: {
31
+ stage: 'min-w-[180px]',
32
+ borderLayer: 'h-28',
33
+ innerLayer: 'h-28',
34
+ label: 'text-sm max-w-[150px]',
35
+ count: 'text-3xl mt-1',
36
+ content: 'px-6'
37
+ }
38
+ },
39
+ color: {
40
+ default: { borderLayer: 'bg-default-200' },
41
+ primary: {
42
+ borderLayer: 'bg-blue-500',
43
+ label: 'text-default-700',
44
+ count: 'text-default-900'
45
+ },
46
+ success: {
47
+ borderLayer: 'bg-green-500',
48
+ label: 'text-default-700',
49
+ count: 'text-default-900'
50
+ },
51
+ warning: {
52
+ borderLayer: 'bg-amber-500',
53
+ label: 'text-default-700',
54
+ count: 'text-default-900'
55
+ },
56
+ danger: {
57
+ borderLayer: 'bg-rose-500',
58
+ label: 'text-default-700',
59
+ count: 'text-default-900'
60
+ },
61
+ info: {
62
+ borderLayer: 'bg-purple-500',
63
+ label: 'text-default-700',
64
+ count: 'text-default-900'
65
+ }
66
+ },
67
+ active: {
68
+ true: {
69
+ borderLayer: 'ring-2 ring-blue-500 ring-offset-2'
70
+ }
71
+ },
72
+ equalWidth: {
73
+ true: { stage: 'flex-1' }
74
+ },
75
+ interactive: {
76
+ true: {
77
+ innerLayer: 'cursor-pointer focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
78
+ }
79
+ },
80
+ disabled: {
81
+ true: {
82
+ stage: 'opacity-50 cursor-not-allowed'
83
+ }
84
+ }
85
+ },
86
+ defaultVariants: {
87
+ size: 'base',
88
+ color: 'default',
89
+ equalWidth: true
90
+ }
91
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makolabs/ripple",
3
- "version": "1.9.2",
3
+ "version": "1.11.0",
4
4
  "description": "Simple Svelte 5 powered component library ✨",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {