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 +1 -1
- package/src/Basefn.res +12 -0
- package/src/Basefn.res.mjs +8 -0
- package/src/Basefn__Responsive.res +291 -0
- package/src/Basefn__Responsive.res.mjs +407 -0
- package/src/components/Basefn__Responsive.css +118 -0
- package/src/components/Basefn__Spotlight.css +207 -0
- package/src/components/Basefn__Spotlight.res +205 -0
- package/src/components/Basefn__Spotlight.res.mjs +233 -0
package/package.json
CHANGED
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
|
+
}
|
package/src/Basefn.res.mjs
CHANGED
|
@@ -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
|
+
}
|