panelui-native 0.35.2 → 0.36.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 (39) hide show
  1. package/lib/module/components/input/index.js +135 -32
  2. package/lib/module/components/input/index.js.map +1 -1
  3. package/lib/module/components/otp-input/index.js +4 -1
  4. package/lib/module/components/otp-input/index.js.map +1 -1
  5. package/lib/module/components/panelside/index.js +1 -1
  6. package/lib/module/components/panelside/index.js.map +1 -1
  7. package/lib/module/components/textarea/index.js +3 -1
  8. package/lib/module/components/textarea/index.js.map +1 -1
  9. package/lib/module/components/time-picker/index.js +801 -0
  10. package/lib/module/components/time-picker/index.js.map +1 -0
  11. package/lib/module/icons/index.js +59 -0
  12. package/lib/module/icons/index.js.map +1 -1
  13. package/lib/module/index.js +3 -1
  14. package/lib/module/index.js.map +1 -1
  15. package/lib/module/utils/time.js +180 -0
  16. package/lib/module/utils/time.js.map +1 -0
  17. package/lib/typescript/src/components/input/index.d.ts +92 -0
  18. package/lib/typescript/src/components/input/index.d.ts.map +1 -1
  19. package/lib/typescript/src/components/otp-input/index.d.ts.map +1 -1
  20. package/lib/typescript/src/components/textarea/index.d.ts.map +1 -1
  21. package/lib/typescript/src/components/time-picker/index.d.ts +104 -0
  22. package/lib/typescript/src/components/time-picker/index.d.ts.map +1 -0
  23. package/lib/typescript/src/icons/index.d.ts +4 -0
  24. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  25. package/lib/typescript/src/index.d.ts +3 -1
  26. package/lib/typescript/src/index.d.ts.map +1 -1
  27. package/lib/typescript/src/utils/time.d.ts +88 -0
  28. package/lib/typescript/src/utils/time.d.ts.map +1 -0
  29. package/package.json +1 -1
  30. package/src/components/input/index.tsx +171 -29
  31. package/src/components/otp-input/index.tsx +4 -1
  32. package/src/components/panelside/index.tsx +1 -1
  33. package/src/components/textarea/index.tsx +3 -1
  34. package/src/components/time-picker/index.tsx +971 -0
  35. package/src/icons/index.tsx +47 -0
  36. package/src/index.ts +25 -0
  37. package/src/theme/use-theme.ts +1 -1
  38. package/src/utils/time.ts +192 -0
  39. package/theme.css +32 -0
@@ -133,6 +133,53 @@ export function CalendarIcon({ size = 16, color, ...props }: IconProps) {
133
133
  );
134
134
  }
135
135
 
136
+ /** A clock face reading ten past ten — the hour, a time field, a schedule. */
137
+ export function ClockIcon({ size = 16, color, ...props }: IconProps) {
138
+ const resolved = useResolvedColor(color, '#737373');
139
+ return (
140
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
141
+ <Circle
142
+ cx={12}
143
+ cy={12}
144
+ r={9}
145
+ stroke={resolved}
146
+ strokeWidth={2}
147
+ strokeLinecap="round"
148
+ strokeLinejoin="round"
149
+ />
150
+ <Path
151
+ d="M12 7v5l3.5 2"
152
+ stroke={resolved}
153
+ strokeWidth={2}
154
+ strokeLinecap="round"
155
+ strokeLinejoin="round"
156
+ />
157
+ </Svg>
158
+ );
159
+ }
160
+
161
+ /** A keyboard — swapping a gesture-driven control for typed entry. */
162
+ export function KeyboardIcon({ size = 16, color, ...props }: IconProps) {
163
+ const resolved = useResolvedColor(color, '#737373');
164
+ return (
165
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
166
+ <Path
167
+ d="M4 5h16a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1Z"
168
+ stroke={resolved}
169
+ strokeWidth={2}
170
+ strokeLinejoin="round"
171
+ />
172
+ <Path
173
+ d="M7 9h.01M11 9h.01M15 9h.01M7 12h.01M11 12h.01M15 12h.01M8 15h8"
174
+ stroke={resolved}
175
+ strokeWidth={2}
176
+ strokeLinecap="round"
177
+ strokeLinejoin="round"
178
+ />
179
+ </Svg>
180
+ );
181
+ }
182
+
136
183
  export function BookmarkIcon({
137
184
  size = 16,
138
185
  color,
package/src/index.ts CHANGED
@@ -630,6 +630,12 @@ export {
630
630
  type SwipeHandle,
631
631
  } from './components/swipe';
632
632
  export { Switch, type SwitchProps } from './components/switch';
633
+ export {
634
+ TimePicker,
635
+ type TimePickerProps,
636
+ type TimePickerLayout,
637
+ type TimePickerPresentation,
638
+ } from './components/time-picker';
633
639
  export {
634
640
  Timeline,
635
641
  type TimelineProps,
@@ -694,6 +700,7 @@ export {
694
700
  ChevronRightIcon,
695
701
  ChevronUpIcon,
696
702
  CircleIcon,
703
+ ClockIcon,
697
704
  CompassIcon,
698
705
  CopyIcon,
699
706
  CrosshairIcon,
@@ -707,6 +714,7 @@ export {
707
714
  IconColorProvider,
708
715
  InfoIcon,
709
716
  ImageIcon,
717
+ KeyboardIcon,
710
718
  LinkIcon,
711
719
  ListChecksIcon,
712
720
  LockIcon,
@@ -749,3 +757,20 @@ export * from './hooks';
749
757
 
750
758
  // Utils
751
759
  export { cn } from './utils/cn';
760
+ export {
761
+ clampTime,
762
+ compareTime,
763
+ displayHour,
764
+ formatTime,
765
+ isSameTime,
766
+ isTimeInRange,
767
+ meridiemLabels,
768
+ minutesToTime,
769
+ roundToStep,
770
+ timeFromDate,
771
+ timeToDate,
772
+ timeToMinutes,
773
+ timesOfDay,
774
+ type HourCycle,
775
+ type TimeValue,
776
+ } from './utils/time';
@@ -37,7 +37,7 @@ export interface PanelThemeFamily {
37
37
  *
38
38
  * ```js
39
39
  * withUniwindConfig(config, {
40
- * cssEntryFile: './global.css',
40
+ * cssEntryFile: './src/global.css',
41
41
  * extraThemes: ['moon', 'moon-dark', 'grass', 'grass-dark'],
42
42
  * });
43
43
  * ```
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Time-of-day helpers for TimePicker.
3
+ *
4
+ * A time here is `{ hour, minute }` on a 24-hour clock, not a `Date`. A `Date`
5
+ * cannot hold a time without also holding a day, so every caller with only a
6
+ * time has to invent a date to carry it and strip it off again afterwards —
7
+ * and the two conversions are where the daylight-saving bugs live. The pair of
8
+ * numbers is the whole value, and `timeFromDate` / `timeToDate` are here for
9
+ * the boundaries where a `Date` is genuinely what you have.
10
+ *
11
+ * Three rules hold throughout:
12
+ *
13
+ * - **`hour` is always 0–23**, whatever the picker is displaying. A 12-hour
14
+ * face is a rendering choice; storing 7pm as `{ hour: 7 }` with a separate
15
+ * meridiem flag means every comparison has to know about the flag.
16
+ * - **Minutes wrap into hours, and hours wrap into the day.** Stepping forward
17
+ * from 23:45 gives 00:00, not 24:00, so arithmetic can never produce a time
18
+ * that does not exist.
19
+ * - **Formatting goes through `Intl`, never through string concatenation.**
20
+ * Whether the meridiem is "PM", "pm" or absent entirely, and whether it
21
+ * leads or trails, belongs to the locale.
22
+ */
23
+
24
+ /** A time of day. `hour` is 0–23 regardless of how it is displayed. */
25
+ export interface TimeValue {
26
+ hour: number;
27
+ minute: number;
28
+ }
29
+
30
+ /** Which face a time is written on. `24` drops the meridiem entirely. */
31
+ export type HourCycle = 12 | 24;
32
+
33
+ const MINUTES_PER_DAY = 24 * 60;
34
+
35
+ /** Minutes since midnight — the form every comparison here works in. */
36
+ export function timeToMinutes(value: TimeValue): number {
37
+ return value.hour * 60 + value.minute;
38
+ }
39
+
40
+ /** The inverse, wrapping into the day so the result is always a real time. */
41
+ export function minutesToTime(minutes: number): TimeValue {
42
+ const wrapped = ((minutes % MINUTES_PER_DAY) + MINUTES_PER_DAY) % MINUTES_PER_DAY;
43
+ return { hour: Math.floor(wrapped / 60), minute: wrapped % 60 };
44
+ }
45
+
46
+ /** Negative if `a` is earlier, positive if later, `0` if the same time. */
47
+ export function compareTime(a: TimeValue, b: TimeValue): number {
48
+ return timeToMinutes(a) - timeToMinutes(b);
49
+ }
50
+
51
+ export function isSameTime(
52
+ a: TimeValue | null | undefined,
53
+ b: TimeValue | null | undefined
54
+ ): boolean {
55
+ if (!a || !b) return false;
56
+ return a.hour === b.hour && a.minute === b.minute;
57
+ }
58
+
59
+ /**
60
+ * Holds a time inside a span, both ends inclusive and either one optional.
61
+ *
62
+ * Clamping rather than wrapping: a value outside the span is a value the
63
+ * caller has been refused, and the nearest allowed time is the useful answer.
64
+ */
65
+ export function clampTime(value: TimeValue, min?: TimeValue, max?: TimeValue): TimeValue {
66
+ let minutes = timeToMinutes(value);
67
+ if (min) minutes = Math.max(minutes, timeToMinutes(min));
68
+ if (max) minutes = Math.min(minutes, timeToMinutes(max));
69
+ return minutesToTime(minutes);
70
+ }
71
+
72
+ export function isTimeInRange(value: TimeValue, min?: TimeValue, max?: TimeValue): boolean {
73
+ const minutes = timeToMinutes(value);
74
+ if (min && minutes < timeToMinutes(min)) return false;
75
+ if (max && minutes > timeToMinutes(max)) return false;
76
+ return true;
77
+ }
78
+
79
+ /**
80
+ * Rounds a time to the nearest multiple of `step` minutes.
81
+ *
82
+ * Rounding, not flooring: at a 30-minute step, 7:29 is a finger that stopped
83
+ * just short of half past rather than someone asking for seven o'clock.
84
+ */
85
+ export function roundToStep(value: TimeValue, step: number): TimeValue {
86
+ if (step <= 1) return minutesToTime(timeToMinutes(value));
87
+ return minutesToTime(Math.round(timeToMinutes(value) / step) * step);
88
+ }
89
+
90
+ /** Every time in the day at `step`-minute intervals, from midnight. */
91
+ export function timesOfDay(step: number): TimeValue[] {
92
+ const safe = Math.max(1, Math.floor(step));
93
+ const out: TimeValue[] = [];
94
+ for (let minutes = 0; minutes < MINUTES_PER_DAY; minutes += safe) {
95
+ out.push(minutesToTime(minutes));
96
+ }
97
+ return out;
98
+ }
99
+
100
+ /** The hour as it is written on the face: 1–12, or 0–23. */
101
+ export function displayHour(hour: number, hourCycle: HourCycle): number {
102
+ if (hourCycle === 24) return hour;
103
+ const twelve = hour % 12;
104
+ return twelve === 0 ? 12 : twelve;
105
+ }
106
+
107
+ /** Rebuilds a 0–23 hour from what the face shows and which half of the day. */
108
+ export function hourFromDisplay(
109
+ displayed: number,
110
+ meridiem: 'am' | 'pm',
111
+ hourCycle: HourCycle
112
+ ): number {
113
+ if (hourCycle === 24) return displayed;
114
+ const base = displayed % 12;
115
+ return meridiem === 'pm' ? base + 12 : base;
116
+ }
117
+
118
+ export function meridiemOf(hour: number): 'am' | 'pm' {
119
+ return hour < 12 ? 'am' : 'pm';
120
+ }
121
+
122
+ /**
123
+ * The meridiem labels in the caller's locale, as `[am, pm]`.
124
+ *
125
+ * Read out of `Intl` rather than hardcoded, because "AM" is not universal —
126
+ * and taken from the *parts* rather than from the formatted string, since
127
+ * pulling it back out of "7:00 PM" means knowing where the locale puts it.
128
+ */
129
+ export function meridiemLabels(locale?: string): [string, string] {
130
+ const format = (hour: number) => {
131
+ try {
132
+ const parts = new Intl.DateTimeFormat(locale, {
133
+ hour: 'numeric',
134
+ hour12: true,
135
+ }).formatToParts(new Date(2020, 0, 1, hour));
136
+ return parts.find((part) => part.type === 'dayPeriod')?.value;
137
+ } catch {
138
+ return undefined;
139
+ }
140
+ };
141
+ return [format(9) ?? 'AM', format(21) ?? 'PM'];
142
+ }
143
+
144
+ /** Two digits, so a column of minutes is a column and not a ragged edge. */
145
+ export function padTwo(value: number): string {
146
+ return value < 10 ? `0${value}` : String(value);
147
+ }
148
+
149
+ export interface FormatTimeOptions {
150
+ hourCycle?: HourCycle;
151
+ locale?: string;
152
+ }
153
+
154
+ /**
155
+ * One time as one line of text, in the caller's locale.
156
+ *
157
+ * Falls back to `H:MM` shapes if `Intl` is unavailable or throws on the tag —
158
+ * a picker that renders nothing is worse than one that renders plainly.
159
+ */
160
+ export function formatTime(
161
+ value: TimeValue,
162
+ { hourCycle = 12, locale }: FormatTimeOptions = {}
163
+ ): string {
164
+ try {
165
+ return new Intl.DateTimeFormat(locale, {
166
+ hour: 'numeric',
167
+ minute: '2-digit',
168
+ hour12: hourCycle === 12,
169
+ }).format(new Date(2020, 0, 1, value.hour, value.minute));
170
+ } catch {
171
+ if (hourCycle === 24) return `${padTwo(value.hour)}:${padTwo(value.minute)}`;
172
+ const [am, pm] = meridiemLabels(locale);
173
+ const suffix = value.hour < 12 ? am : pm;
174
+ return `${displayHour(value.hour, 12)}:${padTwo(value.minute)} ${suffix}`;
175
+ }
176
+ }
177
+
178
+ /** The time part of a `Date`, for callers whose value arrives as one. */
179
+ export function timeFromDate(date: Date): TimeValue {
180
+ return { hour: date.getHours(), minute: date.getMinutes() };
181
+ }
182
+
183
+ /**
184
+ * A time put back onto a day. Defaults to today, and seconds are zeroed —
185
+ * a picker with no seconds column has no opinion about them, and carrying the
186
+ * ones that happened to be on the clock makes two equal times unequal.
187
+ */
188
+ export function timeToDate(value: TimeValue, day: Date = new Date()): Date {
189
+ const copy = new Date(day);
190
+ copy.setHours(value.hour, value.minute, 0, 0);
191
+ return copy;
192
+ }
package/theme.css CHANGED
@@ -145,10 +145,42 @@
145
145
  --radius-xl: unset;
146
146
  --radius-2xl: unset;
147
147
  --radius-3xl: unset;
148
+
149
+ /*
150
+ * The monospace family. Declared because Tailwind's default is a *stack* —
151
+ * eight families separated by commas — and React Native takes fontFamily as
152
+ * one name. The whole list arrives at the platform as a single family that
153
+ * does not exist, so `font-mono` silently draws in the system font and code
154
+ * stops looking like code. One real name per platform is the fix.
155
+ *
156
+ * The four weight names are deliberately *not* declared here, and that is
157
+ * the mechanism by which an app changes the font. Tailwind resolves the
158
+ * `font-*` utility against `--font-*` before `--font-weight-*`, so leaving
159
+ * `--font-normal` and friends undefined is what keeps `font-semibold`
160
+ * meaning a weight. An app that defines them flips every one of those
161
+ * classes to a font family, which is the right trade on native: a weight
162
+ * there is a separately registered face, not a variation of one family.
163
+ * Declaring them here — even as `unset` — would take the weights away from
164
+ * every app that never asked for a custom font.
165
+ */
166
+ --font-mono: unset;
148
167
  }
149
168
 
150
169
  @layer theme {
151
170
  :root {
171
+ /*
172
+ * Not themed — a platform's monospace face is a property of the platform,
173
+ * not of the palette. `Menlo` ships with iOS; `monospace` is Android's
174
+ * alias for its own. Neither exists on the other, which is why this cannot
175
+ * be one value.
176
+ */
177
+ @variant ios {
178
+ --font-mono: Menlo;
179
+ }
180
+ @variant android {
181
+ --font-mono: monospace;
182
+ }
183
+
152
184
  @variant light {
153
185
  --color-background: #ffffff;
154
186
  /* neutral-800 */