basefn 1.7.0 → 1.9.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "basefn",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/brnrdog/basefn.git"
package/src/Basefn.res CHANGED
@@ -62,6 +62,7 @@ type hoverCardAlign = Basefn__HoverCard.align
62
62
  type alertDialogVariant = Basefn__AlertDialog.variant
63
63
  type contextMenuItem = Basefn__ContextMenu.menuItem
64
64
  type contextMenuContent = Basefn__ContextMenu.menuContent
65
+ type spotlightItem = Basefn__Spotlight.spotlightItem
65
66
  type gridColumns = Basefn__Grid.columns
66
67
  type gridRows = Basefn__Grid.rows
67
68
  type gridAutoFlow = Basefn__Grid.autoFlow
@@ -71,6 +72,9 @@ type gridJustifyContent = Basefn__Grid.justifyContent
71
72
  type gridAlignContent = Basefn__Grid.alignContent
72
73
  type gridItemColumnSpan = Basefn__Grid.Item.columnSpan
73
74
  type gridItemRowSpan = Basefn__Grid.Item.rowSpan
75
+ type breakpoint = Basefn__Responsive.breakpoint
76
+ type currentBreakpoint = Basefn__Responsive.currentBreakpoint
77
+ type responsiveValue<'a> = Basefn__Responsive.responsiveValue<'a>
74
78
 
75
79
  // Form Components
76
80
  module Button = {
@@ -224,3 +228,11 @@ module AlertDialog = {
224
228
  module ContextMenu = {
225
229
  include Basefn__ContextMenu
226
230
  }
231
+ module Spotlight = {
232
+ include Basefn__Spotlight
233
+ }
234
+
235
+ // Responsive Utilities
236
+ module Responsive = {
237
+ include Basefn__Responsive
238
+ }
@@ -36,7 +36,9 @@ import * as Basefn__Accordion from "./components/Basefn__Accordion.res.mjs";
36
36
  import * as Basefn__AppLayout from "./components/Basefn__AppLayout.res.mjs";
37
37
  import * as Basefn__HoverCard from "./components/Basefn__HoverCard.res.mjs";
38
38
  import * as Basefn__Separator from "./components/Basefn__Separator.res.mjs";
39
+ import * as Basefn__Spotlight from "./components/Basefn__Spotlight.res.mjs";
39
40
  import * as Basefn__Breadcrumb from "./components/Basefn__Breadcrumb.res.mjs";
41
+ import * as Basefn__Responsive from "./Basefn__Responsive.res.mjs";
40
42
  import * as Basefn__ScrollArea from "./components/Basefn__ScrollArea.res.mjs";
41
43
  import * as Basefn__Typography from "./components/Basefn__Typography.res.mjs";
42
44
  import * as Basefn__AlertDialog from "./components/Basefn__AlertDialog.res.mjs";
@@ -139,6 +141,10 @@ let AlertDialog = Basefn__AlertDialog;
139
141
 
140
142
  let ContextMenu = Basefn__ContextMenu;
141
143
 
144
+ let Spotlight = Basefn__Spotlight;
145
+
146
+ let Responsive = Basefn__Responsive;
147
+
142
148
  export {
143
149
  Button,
144
150
  Input,
@@ -185,5 +191,7 @@ export {
185
191
  HoverCard,
186
192
  AlertDialog,
187
193
  ContextMenu,
194
+ Spotlight,
195
+ Responsive,
188
196
  }
189
197
  /* Not a pure module */
@@ -0,0 +1,291 @@
1
+ %%raw(`import './components/Basefn__Responsive.css'`)
2
+ open Xote
3
+
4
+ // ============================================================================
5
+ // Breakpoints
6
+ // ============================================================================
7
+
8
+ type breakpoint = Xs | Sm | Md | Lg | Xl | Xxl
9
+
10
+ let breakpointToPixels = (bp: breakpoint): int => {
11
+ switch bp {
12
+ | Xs => 480
13
+ | Sm => 640
14
+ | Md => 768
15
+ | Lg => 1024
16
+ | Xl => 1280
17
+ | Xxl => 1536
18
+ }
19
+ }
20
+
21
+ let breakpointToString = (bp: breakpoint): string => {
22
+ switch bp {
23
+ | Xs => "xs"
24
+ | Sm => "sm"
25
+ | Md => "md"
26
+ | Lg => "lg"
27
+ | Xl => "xl"
28
+ | Xxl => "xxl"
29
+ }
30
+ }
31
+
32
+ // ============================================================================
33
+ // Media query strings
34
+ // ============================================================================
35
+
36
+ let minWidth = (bp: breakpoint): string => {
37
+ let px = breakpointToPixels(bp)
38
+ `(min-width: ${Int.toString(px)}px)`
39
+ }
40
+
41
+ let maxWidth = (bp: breakpoint): string => {
42
+ let px = breakpointToPixels(bp) - 1
43
+ `(max-width: ${Int.toString(px)}px)`
44
+ }
45
+
46
+ let between = (lower: breakpoint, upper: breakpoint): string => {
47
+ let minPx = breakpointToPixels(lower)
48
+ let maxPx = breakpointToPixels(upper) - 1
49
+ `(min-width: ${Int.toString(minPx)}px) and (max-width: ${Int.toString(maxPx)}px)`
50
+ }
51
+
52
+ // Predefined media query strings
53
+ module Query = {
54
+ let xsUp = minWidth(Xs)
55
+ let smUp = minWidth(Sm)
56
+ let mdUp = minWidth(Md)
57
+ let lgUp = minWidth(Lg)
58
+ let xlUp = minWidth(Xl)
59
+ let xxlUp = minWidth(Xxl)
60
+
61
+ let xsDown = maxWidth(Xs)
62
+ let smDown = maxWidth(Sm)
63
+ let mdDown = maxWidth(Md)
64
+ let lgDown = maxWidth(Lg)
65
+ let xlDown = maxWidth(Xl)
66
+ let xxlDown = maxWidth(Xxl)
67
+
68
+ let xsOnly = maxWidth(Sm)
69
+ let smOnly = between(Sm, Md)
70
+ let mdOnly = between(Md, Lg)
71
+ let lgOnly = between(Lg, Xl)
72
+ let xlOnly = between(Xl, Xxl)
73
+ let xxlOnly = minWidth(Xxl)
74
+
75
+ let portrait = "(orientation: portrait)"
76
+ let landscape = "(orientation: landscape)"
77
+ let prefersReducedMotion = "(prefers-reduced-motion: reduce)"
78
+ let prefersDark = "(prefers-color-scheme: dark)"
79
+ let prefersLight = "(prefers-color-scheme: light)"
80
+ let highContrast = "(prefers-contrast: more)"
81
+ let touchDevice = "(hover: none) and (pointer: coarse)"
82
+ let finePointer = "(hover: hover) and (pointer: fine)"
83
+ let retina = "(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi)"
84
+ }
85
+
86
+ // ============================================================================
87
+ // matchMedia binding
88
+ // ============================================================================
89
+
90
+ type mediaQueryList
91
+
92
+ @val external matchMedia: string => mediaQueryList = "window.matchMedia"
93
+ @get external matches: mediaQueryList => bool = "matches"
94
+ @send external addListener: (mediaQueryList, mediaQueryList => unit) => unit = "addEventListener"
95
+ @send external removeListener: (mediaQueryList, mediaQueryList => unit) => unit = "removeEventListener"
96
+
97
+ let addChangeListener: (mediaQueryList, mediaQueryList => unit) => unit = %raw(`
98
+ function(mql, cb) { mql.addEventListener("change", cb) }
99
+ `)
100
+
101
+ let removeChangeListener: (mediaQueryList, mediaQueryList => unit) => unit = %raw(`
102
+ function(mql, cb) { mql.removeEventListener("change", cb) }
103
+ `)
104
+
105
+ // ============================================================================
106
+ // Media query matching utilities
107
+ // ============================================================================
108
+
109
+ let matchesQuery = (query: string): bool => {
110
+ matches(matchMedia(query))
111
+ }
112
+
113
+ let matchesBreakpointUp = (bp: breakpoint): bool => matchesQuery(minWidth(bp))
114
+ let matchesBreakpointDown = (bp: breakpoint): bool => matchesQuery(maxWidth(bp))
115
+
116
+ // ============================================================================
117
+ // Signal-based media query tracking
118
+ // ============================================================================
119
+
120
+ let makeMediaSignal = (query: string): Signal.t<bool> => {
121
+ let mql = matchMedia(query)
122
+ let signal = Signal.make(matches(mql))
123
+ let handler = (evt: mediaQueryList) => {
124
+ Signal.set(signal, matches(evt))
125
+ }
126
+ addChangeListener(mql, handler)
127
+ signal
128
+ }
129
+
130
+ let makeBreakpointSignal = (bp: breakpoint): Signal.t<bool> => {
131
+ makeMediaSignal(minWidth(bp))
132
+ }
133
+
134
+ // ============================================================================
135
+ // Predefined screen size signals (memoized singletons)
136
+ // ============================================================================
137
+
138
+ // Helper: create a memoized signal that initializes on first access
139
+ let _memo = (make: unit => Signal.t<bool>): (unit => Signal.t<bool>) => {
140
+ let cached: ref<option<Signal.t<bool>>> = ref(None)
141
+ () => {
142
+ switch cached.contents {
143
+ | Some(s) => s
144
+ | None => {
145
+ let s = make()
146
+ cached := Some(s)
147
+ s
148
+ }
149
+ }
150
+ }
151
+ }
152
+
153
+ // Exact breakpoint range signals
154
+ let isXsScreen = _memo(() => makeMediaSignal(Query.xsDown))
155
+ let isSmScreen = _memo(() => makeMediaSignal(Query.smOnly))
156
+ let isMdScreen = _memo(() => makeMediaSignal(Query.mdOnly))
157
+ let isLgScreen = _memo(() => makeMediaSignal(Query.lgOnly))
158
+ let isXlScreen = _memo(() => makeMediaSignal(Query.xlOnly))
159
+ let isXxlScreen = _memo(() => makeMediaSignal(Query.xxlOnly))
160
+
161
+ // "And up" signals
162
+ let isSmallAndUp = _memo(() => makeMediaSignal(Query.smUp))
163
+ let isMediumAndUp = _memo(() => makeMediaSignal(Query.mdUp))
164
+ let isLargeAndUp = _memo(() => makeMediaSignal(Query.lgUp))
165
+ let isExtraLargeAndUp = _memo(() => makeMediaSignal(Query.xlUp))
166
+
167
+ // Semantic device signals
168
+ let isMobile = _memo(() => makeMediaSignal(Query.smDown))
169
+ let isTablet = _memo(() => makeMediaSignal(Query.mdOnly))
170
+ let isDesktop = _memo(() => makeMediaSignal(Query.lgUp))
171
+
172
+ // Preference / capability signals
173
+ let isPortrait = _memo(() => makeMediaSignal(Query.portrait))
174
+ let isLandscape = _memo(() => makeMediaSignal(Query.landscape))
175
+ let prefersReducedMotion = _memo(() => makeMediaSignal(Query.prefersReducedMotion))
176
+ let prefersDarkMode = _memo(() => makeMediaSignal(Query.prefersDark))
177
+ let isTouchDevice = _memo(() => makeMediaSignal(Query.touchDevice))
178
+ let isRetina = _memo(() => makeMediaSignal(Query.retina))
179
+
180
+ // ============================================================================
181
+ // Current breakpoint tracking
182
+ // ============================================================================
183
+
184
+ type currentBreakpoint = ExtraSmall | Small | Medium | Large | ExtraLarge | ExtraExtraLarge
185
+
186
+ let currentBreakpointToString = (bp: currentBreakpoint): string => {
187
+ switch bp {
188
+ | ExtraSmall => "xs"
189
+ | Small => "sm"
190
+ | Medium => "md"
191
+ | Large => "lg"
192
+ | ExtraLarge => "xl"
193
+ | ExtraExtraLarge => "xxl"
194
+ }
195
+ }
196
+
197
+ let getCurrentBreakpoint = (): currentBreakpoint => {
198
+ if matchesQuery(Query.xxlUp) {
199
+ ExtraExtraLarge
200
+ } else if matchesQuery(Query.xlUp) {
201
+ ExtraLarge
202
+ } else if matchesQuery(Query.lgUp) {
203
+ Large
204
+ } else if matchesQuery(Query.mdUp) {
205
+ Medium
206
+ } else if matchesQuery(Query.smUp) {
207
+ Small
208
+ } else {
209
+ ExtraSmall
210
+ }
211
+ }
212
+
213
+ let makeCurrentBreakpointSignal = (): Signal.t<currentBreakpoint> => {
214
+ let signal = Signal.make(getCurrentBreakpoint())
215
+
216
+ let breakpoints = [Sm, Md, Lg, Xl, Xxl]
217
+ breakpoints->Array.forEach(bp => {
218
+ let mql = matchMedia(minWidth(bp))
219
+ let _handler = (_evt: mediaQueryList) => {
220
+ Signal.set(signal, getCurrentBreakpoint())
221
+ }
222
+ addChangeListener(mql, _handler)
223
+ })
224
+
225
+ signal
226
+ }
227
+
228
+ let currentBreakpoint: ref<option<Signal.t<currentBreakpoint>>> = ref(None)
229
+
230
+ let getCurrentBreakpointSignal = (): Signal.t<currentBreakpoint> => {
231
+ switch currentBreakpoint.contents {
232
+ | Some(s) => s
233
+ | None => {
234
+ let s = makeCurrentBreakpointSignal()
235
+ currentBreakpoint := Some(s)
236
+ s
237
+ }
238
+ }
239
+ }
240
+
241
+ // ============================================================================
242
+ // Responsive value helpers
243
+ // ============================================================================
244
+
245
+ type responsiveValue<'a> = {
246
+ xs?: 'a,
247
+ sm?: 'a,
248
+ md?: 'a,
249
+ lg?: 'a,
250
+ xl?: 'a,
251
+ xxl?: 'a,
252
+ }
253
+
254
+ let resolveResponsiveValue = (value: responsiveValue<'a>, fallback: 'a): 'a => {
255
+ let bp = getCurrentBreakpoint()
256
+ // Build ordered list from current breakpoint down, cascade to find first defined value
257
+ let ordered = switch bp {
258
+ | ExtraExtraLarge => [value.xxl, value.xl, value.lg, value.md, value.sm, value.xs]
259
+ | ExtraLarge => [value.xl, value.lg, value.md, value.sm, value.xs]
260
+ | Large => [value.lg, value.md, value.sm, value.xs]
261
+ | Medium => [value.md, value.sm, value.xs]
262
+ | Small => [value.sm, value.xs]
263
+ | ExtraSmall => [value.xs]
264
+ }
265
+ let rec find = (items: array<option<'a>>, idx: int): 'a => {
266
+ if idx >= Array.length(items) {
267
+ fallback
268
+ } else {
269
+ switch items->Array.getUnsafe(idx) {
270
+ | Some(v) => v
271
+ | None => find(items, idx + 1)
272
+ }
273
+ }
274
+ }
275
+ find(ordered, 0)
276
+ }
277
+
278
+ // ============================================================================
279
+ // Visibility helpers (CSS class-based)
280
+ // ============================================================================
281
+
282
+ module Visibility = {
283
+ let hiddenBelow = (bp: breakpoint): string =>
284
+ `basefn-hidden-below-${breakpointToString(bp)}`
285
+
286
+ let hiddenAbove = (bp: breakpoint): string =>
287
+ `basefn-hidden-above-${breakpointToString(bp)}`
288
+
289
+ let visibleOnly = (bp: breakpoint): string =>
290
+ `basefn-visible-${breakpointToString(bp)}-only`
291
+ }