@shipfox/react-ui 0.22.0 → 0.23.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.
Files changed (54) hide show
  1. package/dist/components/dashboard/context/dashboard-context.d.ts +12 -8
  2. package/dist/components/dashboard/context/dashboard-context.js +42 -7
  3. package/dist/components/dashboard/context/index.d.ts +2 -2
  4. package/dist/components/dashboard/context/index.js +1 -1
  5. package/dist/components/dashboard/context/types.d.ts +9 -7
  6. package/dist/components/dashboard/index.d.ts +2 -4
  7. package/dist/components/dashboard/index.js +1 -2
  8. package/dist/components/dashboard/pages/analytics-page.js +2 -9
  9. package/dist/components/dashboard/pages/jobs-page.js +2 -4
  10. package/dist/components/dashboard/toolbar/filter-button.d.ts +6 -10
  11. package/dist/components/dashboard/toolbar/filter-button.js +109 -76
  12. package/dist/components/dashboard/toolbar/page-toolbar.d.ts +7 -19
  13. package/dist/components/dashboard/toolbar/page-toolbar.js +11 -99
  14. package/dist/components/dashboard/toolbar/toolbar-actions.js +3 -3
  15. package/dist/components/index.d.ts +1 -0
  16. package/dist/components/index.js +1 -0
  17. package/dist/components/interval-selector/hooks/index.d.ts +4 -0
  18. package/dist/components/interval-selector/hooks/index.js +5 -0
  19. package/dist/components/interval-selector/hooks/use-interval-selector-input.d.ts +30 -0
  20. package/dist/components/interval-selector/hooks/use-interval-selector-input.js +125 -0
  21. package/dist/components/interval-selector/hooks/use-interval-selector-navigation.d.ts +21 -0
  22. package/dist/components/interval-selector/hooks/use-interval-selector-navigation.js +58 -0
  23. package/dist/components/interval-selector/hooks/use-interval-selector.d.ts +25 -0
  24. package/dist/components/interval-selector/hooks/use-interval-selector.js +75 -0
  25. package/dist/components/interval-selector/index.d.ts +3 -0
  26. package/dist/components/interval-selector/index.js +4 -0
  27. package/dist/components/interval-selector/interval-selector-calendar.d.ts +7 -0
  28. package/dist/components/interval-selector/interval-selector-calendar.js +47 -0
  29. package/dist/components/interval-selector/interval-selector-input.d.ts +8 -0
  30. package/dist/components/interval-selector/interval-selector-input.js +34 -0
  31. package/dist/components/interval-selector/interval-selector-suggestions.d.ts +11 -0
  32. package/dist/components/interval-selector/interval-selector-suggestions.js +107 -0
  33. package/dist/components/interval-selector/interval-selector.d.ts +12 -0
  34. package/dist/components/interval-selector/interval-selector.js +56 -0
  35. package/dist/components/interval-selector/interval-selector.stories.js +232 -0
  36. package/dist/components/interval-selector/types.d.ts +19 -0
  37. package/dist/components/interval-selector/types.js +3 -0
  38. package/dist/components/interval-selector/utils/constants.d.ts +24 -0
  39. package/dist/components/interval-selector/utils/constants.js +129 -0
  40. package/dist/components/interval-selector/utils/format.d.ts +16 -0
  41. package/dist/components/interval-selector/utils/format.js +23 -0
  42. package/dist/components/interval-selector/utils/index.d.ts +3 -0
  43. package/dist/components/interval-selector/utils/index.js +4 -0
  44. package/dist/components/popover/popover.d.ts +3 -1
  45. package/dist/components/popover/popover.js +2 -1
  46. package/dist/styles.css +1 -1
  47. package/dist/utils/date.js +130 -22
  48. package/dist/utils/format/date.d.ts +1 -0
  49. package/dist/utils/format/date.js +11 -4
  50. package/package.json +3 -2
  51. package/dist/components/dashboard/filters/expression-filter-bar.d.ts +0 -42
  52. package/dist/components/dashboard/filters/expression-filter-bar.js +0 -80
  53. package/dist/components/dashboard/filters/index.d.ts +0 -6
  54. package/dist/components/dashboard/filters/index.js +0 -5
@@ -43,33 +43,141 @@ const DURATION_SHORTCUTS_REVERSED = Object.fromEntries(Object.entries(DURATION_S
43
43
  value,
44
44
  key
45
45
  ]));
46
- const DURATION_SHORTCUT_REGEX = new RegExp(`^(\\d+)(${Object.keys(DURATION_SHORTCUTS_REVERSED).join('|')})$`);
46
+ const UNIT_NAMES = {
47
+ minutes: [
48
+ 'm',
49
+ 'min',
50
+ 'minute',
51
+ 'minutes'
52
+ ],
53
+ hours: [
54
+ 'h',
55
+ 'hr',
56
+ 'hour',
57
+ 'hours'
58
+ ],
59
+ days: [
60
+ 'd',
61
+ 'day',
62
+ 'days'
63
+ ],
64
+ weeks: [
65
+ 'w',
66
+ 'wk',
67
+ 'week',
68
+ 'weeks'
69
+ ],
70
+ months: [
71
+ 'mo',
72
+ 'mon',
73
+ 'month',
74
+ 'months'
75
+ ],
76
+ years: [
77
+ 'y',
78
+ 'yr',
79
+ 'year',
80
+ 'years'
81
+ ],
82
+ seconds: [
83
+ 's',
84
+ 'sec',
85
+ 'second',
86
+ 'seconds'
87
+ ]
88
+ };
89
+ const UNIT_NAME_TO_KEY = {};
90
+ for (const [key, names] of Object.entries(UNIT_NAMES)){
91
+ for (const name of names){
92
+ UNIT_NAME_TO_KEY[name.toLowerCase()] = key;
93
+ }
94
+ }
95
+ const SHORTCUT_PATTERN = Object.keys(DURATION_SHORTCUTS_REVERSED).join('|');
96
+ const FULL_NAME_PATTERN = Object.values(UNIT_NAMES).flat().sort((a, b)=>b.length - a.length).map((name)=>name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
97
+ const DURATION_SHORTCUT_REGEX = new RegExp(`^(\\d+)\\s*(${SHORTCUT_PATTERN})$`, 'i');
98
+ const DURATION_FULL_REGEX = new RegExp(`^(\\d+)\\s*(${FULL_NAME_PATTERN})\\s*$`, 'i');
47
99
  export function generateDurationShortcut(duration) {
48
- const keys = Object.keys(duration);
49
- if (keys.length !== 1) return '';
50
- const key = keys[0];
51
- const value = duration[key];
52
- return `${value}${DURATION_SHORTCUTS[key]}`;
100
+ for (const [key, shortcut] of Object.entries(DURATION_SHORTCUTS)){
101
+ const value = duration[key];
102
+ if (value) {
103
+ return `${value}${shortcut}`;
104
+ }
105
+ }
106
+ return '';
53
107
  }
54
108
  export function parseTextDurationShortcut(text) {
55
- const match = text.match(DURATION_SHORTCUT_REGEX);
56
- if (!match) return;
57
- const [_, value, shortcut] = match;
58
- const unit = DURATION_SHORTCUTS_REVERSED[shortcut];
59
- return {
60
- [unit]: Number.parseInt(value, 10)
61
- };
109
+ const trimmed = text.trim();
110
+ if (!trimmed) return undefined;
111
+ const shortcutMatch = trimmed.match(DURATION_SHORTCUT_REGEX);
112
+ if (shortcutMatch) {
113
+ const [_, value, shortcut] = shortcutMatch;
114
+ const unit = DURATION_SHORTCUTS_REVERSED[shortcut.toLowerCase()];
115
+ if (unit) {
116
+ return {
117
+ [unit]: Number.parseInt(value, 10)
118
+ };
119
+ }
120
+ }
121
+ const fullMatch = trimmed.match(DURATION_FULL_REGEX);
122
+ if (fullMatch) {
123
+ const [_, value, unitName] = fullMatch;
124
+ const normalizedUnitName = unitName.toLowerCase().trim();
125
+ const unit = UNIT_NAME_TO_KEY[normalizedUnitName];
126
+ if (unit) {
127
+ return {
128
+ [unit]: Number.parseInt(value, 10)
129
+ };
130
+ }
131
+ }
132
+ return undefined;
133
+ }
134
+ const DATE_SPLITTER_REGEX = /[-\u2013]/;
135
+ const YEAR_REGEX = /\d{4}/;
136
+ function hasYearInText(text) {
137
+ return YEAR_REGEX.test(text);
138
+ }
139
+ function parseDateString(dateText) {
140
+ const date = new Date(dateText);
141
+ return Number.isNaN(date.getTime()) ? null : date;
142
+ }
143
+ function assignYearsToDates(start, end, startHasYear, endHasYear, currentYear) {
144
+ if (!startHasYear && !endHasYear) {
145
+ start.setFullYear(currentYear);
146
+ end.setFullYear(currentYear);
147
+ } else if (!startHasYear) {
148
+ start.setFullYear(end.getFullYear());
149
+ } else if (!endHasYear) {
150
+ end.setFullYear(currentYear);
151
+ }
152
+ }
153
+ function fixInvalidInterval(start, end, startHasYear, endHasYear) {
154
+ if (end < start) {
155
+ if (startHasYear || endHasYear) return false;
156
+ const endYear = end.getFullYear();
157
+ start.setFullYear(endYear - 1);
158
+ }
159
+ return true;
62
160
  }
63
- const dateSplitterRefex = /[-\u2013]/;
64
161
  export function parseTextInterval(text) {
65
- const durationShortcut = parseTextDurationShortcut(text);
66
- if (durationShortcut) return intervalToNowFromDuration(durationShortcut);
67
- const textDates = text.split(dateSplitterRefex).map((token)=>token.trim());
68
- if (textDates.length !== 2) return;
69
- const start = new Date(textDates[0]);
70
- const end = new Date(textDates[1]);
71
- if (Number.isNaN(start.getTime())) return;
72
- if (Number.isNaN(end.getTime())) return;
162
+ const textDates = text.split(DATE_SPLITTER_REGEX).map((token)=>token.trim());
163
+ if (textDates.length !== 2) {
164
+ return undefined;
165
+ }
166
+ const [startText, endText] = textDates;
167
+ const startHasYear = hasYearInText(startText);
168
+ const endHasYear = hasYearInText(endText);
169
+ const start = parseDateString(startText);
170
+ const end = parseDateString(endText);
171
+ if (!start || !end) {
172
+ return undefined;
173
+ }
174
+ const now = new Date();
175
+ const currentYear = now.getFullYear();
176
+ assignYearsToDates(start, end, startHasYear, endHasYear, currentYear);
177
+ const isValid = fixInvalidInterval(start, end, startHasYear, endHasYear);
178
+ if (!isValid) {
179
+ return undefined;
180
+ }
73
181
  return {
74
182
  start,
75
183
  end
@@ -1,6 +1,7 @@
1
1
  import { type NormalizedInterval } from 'date-fns';
2
2
  interface DateTimeFormatOptions extends Intl.DateTimeFormatOptions {
3
3
  locale?: string;
4
+ forceShowTime?: boolean;
4
5
  }
5
6
  export declare function formatDateTime(date: Date, options?: DateTimeFormatOptions): string;
6
7
  export declare function formatDateTimeRelativeToInterval(date: Date, interval: NormalizedInterval, options?: DateTimeFormatOptions): string;
@@ -30,14 +30,21 @@ export function formatDateTimeRelativeToInterval(date, interval, options) {
30
30
  }
31
31
  export function formatDateTimeRange(interval, options) {
32
32
  const { start, end } = interval;
33
+ const { forceShowTime, ...formatOptions } = options || {};
33
34
  const areFullDays = isStartOfDay(start) && isEndOfDay(end);
35
+ const shouldShowTime = forceShowTime || !areFullDays;
34
36
  const formatter = getDateTimeFormatter({
35
37
  year: areCurrentYear(interval) ? undefined : defaultOptions.year,
36
- hour: areFullDays ? undefined : defaultOptions.hour,
37
- minute: areFullDays ? undefined : defaultOptions.minute,
38
- ...options
38
+ hour: shouldShowTime ? defaultOptions.hour : undefined,
39
+ minute: shouldShowTime ? defaultOptions.minute : undefined,
40
+ ...formatOptions
39
41
  });
40
- return formatter.formatRange(start, end);
42
+ if (areFullDays && !forceShowTime) {
43
+ return formatter.formatRange(start, end);
44
+ }
45
+ const startFormatted = formatter.format(start);
46
+ const endFormatted = formatter.format(end);
47
+ return `${startFormatted} – ${endFormatted}`;
41
48
  }
42
49
  export function formatTimeSeriesTick(date, { locale, ...options } = {}) {
43
50
  const tickOptions = isStartOfDay(date) ? {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/react-ui",
3
3
  "license": "MIT",
4
- "version": "0.22.0",
4
+ "version": "0.23.0",
5
5
  "private": false,
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -21,6 +21,7 @@
21
21
  "@radix-ui/react-checkbox": "^1.3.3",
22
22
  "@radix-ui/react-collapsible": "^1.1.12",
23
23
  "@radix-ui/react-dialog": "^1.1.15",
24
+ "@radix-ui/react-dismissable-layer": "^1.1.11",
24
25
  "@radix-ui/react-dropdown-menu": "^2.1.16",
25
26
  "@radix-ui/react-label": "^2.1.7",
26
27
  "@radix-ui/react-popover": "^1.1.15",
@@ -86,8 +87,8 @@
86
87
  "zod": "^4.1.12",
87
88
  "@shipfox/biome": "1.6.0",
88
89
  "@shipfox/swc": "1.2.2",
89
- "@shipfox/typescript": "1.1.2",
90
90
  "@shipfox/ts-config": "1.3.5",
91
+ "@shipfox/typescript": "1.1.2",
91
92
  "@shipfox/vite": "1.2.2",
92
93
  "@shipfox/vitest": "1.2.0"
93
94
  },
@@ -1,42 +0,0 @@
1
- /**
2
- * Expression Filter Bar Component
3
- *
4
- * A horizontal button group for filtering by resource type.
5
- */
6
- import type { ComponentProps } from 'react';
7
- import type { ResourceType } from '../context';
8
- export interface ResourceTypeOption {
9
- id: ResourceType;
10
- label: string;
11
- disabled?: boolean;
12
- }
13
- export interface ExpressionFilterBarProps extends Omit<ComponentProps<'div'>, 'children'> {
14
- /**
15
- * Available resource type options
16
- */
17
- options?: ResourceTypeOption[];
18
- /**
19
- * Currently selected resource type
20
- */
21
- value?: ResourceType;
22
- /**
23
- * Callback when resource type changes
24
- */
25
- onValueChange?: (value: ResourceType) => void;
26
- }
27
- /**
28
- * Expression Filter Bar
29
- *
30
- * Displays a horizontal button group for selecting resource types.
31
- * Integrates with the dashboard context for state management.
32
- *
33
- * @example
34
- * ```tsx
35
- * <ExpressionFilterBar
36
- * value="ci-pipeline"
37
- * onValueChange={setResourceType}
38
- * />
39
- * ```
40
- */
41
- export declare function ExpressionFilterBar({ options, value, onValueChange, className, ...props }: ExpressionFilterBarProps): import("react/jsx-runtime").JSX.Element;
42
- //# sourceMappingURL=expression-filter-bar.d.ts.map
@@ -1,80 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- /**
3
- * Expression Filter Bar Component
4
- *
5
- * A horizontal button group for filtering by resource type.
6
- */ import { Button } from '../../../components/button/index.js';
7
- import { cn } from '../../../utils/cn.js';
8
- /**
9
- * Default resource type options
10
- */ const DEFAULT_OPTIONS = [
11
- {
12
- id: 'ci-pipeline',
13
- label: 'CI Pipeline'
14
- },
15
- {
16
- id: 'ci-jobs',
17
- label: 'CI Jobs'
18
- },
19
- {
20
- id: 'ci-steps',
21
- label: 'CI Steps'
22
- },
23
- {
24
- id: 'runners',
25
- label: 'Runners',
26
- disabled: true
27
- },
28
- {
29
- id: 'suite',
30
- label: 'Suite',
31
- disabled: true
32
- },
33
- {
34
- id: 'cases',
35
- label: 'Cases',
36
- disabled: true
37
- }
38
- ];
39
- /**
40
- * Expression Filter Bar
41
- *
42
- * Displays a horizontal button group for selecting resource types.
43
- * Integrates with the dashboard context for state management.
44
- *
45
- * @example
46
- * ```tsx
47
- * <ExpressionFilterBar
48
- * value="ci-pipeline"
49
- * onValueChange={setResourceType}
50
- * />
51
- * ```
52
- */ export function ExpressionFilterBar({ options = DEFAULT_OPTIONS, value = 'ci-pipeline', onValueChange, className, ...props }) {
53
- return /*#__PURE__*/ _jsx("div", {
54
- className: cn(// Desktop: Normal flex layout
55
- 'md:flex md:gap-4 md:items-start', // Mobile: Swipeable with scroll-snap
56
- 'overflow-x-auto scrollbar-none', // Scroll snap for smooth swiping
57
- 'snap-x snap-mandatory', // Hide scrollbar but allow scrolling
58
- '[&::-webkit-scrollbar]:hidden', className),
59
- ...props,
60
- children: /*#__PURE__*/ _jsx("div", {
61
- className: "flex gap-4 items-start px-0",
62
- children: options.map((option)=>{
63
- const isActive = value === option.id;
64
- return /*#__PURE__*/ _jsx(Button, {
65
- variant: isActive ? 'secondary' : 'transparent',
66
- size: "md",
67
- disabled: option.disabled,
68
- onClick: ()=>!option.disabled && onValueChange?.(option.id),
69
- className: cn('flex items-center justify-center gap-8 px-10 py-6 rounded-6', 'text-sm font-medium leading-20 tracking-0', 'transition-colors', // Mobile: Prevent shrinking, snap alignment
70
- 'shrink-0 snap-start', // Active state
71
- isActive && 'shadow-none bg-background-button-neutral-pressed', // Inactive state
72
- !isActive && !option.disabled && 'bg-transparent text-foreground-neutral-subtle hover:text-foreground-neutral-base'),
73
- children: option.label
74
- }, option.id);
75
- })
76
- })
77
- });
78
- }
79
-
80
- //# sourceMappingURL=expression-filter-bar.js.map
@@ -1,6 +0,0 @@
1
- /**
2
- * Filter components exports
3
- */
4
- export type { ExpressionFilterBarProps, ResourceTypeOption } from './expression-filter-bar';
5
- export { ExpressionFilterBar } from './expression-filter-bar';
6
- //# sourceMappingURL=index.d.ts.map
@@ -1,5 +0,0 @@
1
- /**
2
- * Filter components exports
3
- */ export { ExpressionFilterBar } from './expression-filter-bar.js';
4
-
5
- //# sourceMappingURL=index.js.map