@budibase/frontend-core 2.5.6-alpha.5 → 2.5.6-alpha.7

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 (63) hide show
  1. package/package.json +6 -4
  2. package/src/components/grid/cells/AttachmentCell.svelte +153 -0
  3. package/src/components/grid/cells/BooleanCell.svelte +44 -0
  4. package/src/components/grid/cells/DataCell.svelte +86 -0
  5. package/src/components/grid/cells/DateCell.svelte +77 -0
  6. package/src/components/grid/cells/FormulaCell.svelte +5 -0
  7. package/src/components/grid/cells/GridCell.svelte +159 -0
  8. package/src/components/grid/cells/HeaderCell.svelte +237 -0
  9. package/src/components/grid/cells/JSONCell.svelte +36 -0
  10. package/src/components/grid/cells/LongFormCell.svelte +117 -0
  11. package/src/components/grid/cells/MultiSelectCell.svelte +7 -0
  12. package/src/components/grid/cells/NumberCell.svelte +7 -0
  13. package/src/components/grid/cells/OptionsCell.svelte +239 -0
  14. package/src/components/grid/cells/RelationshipCell.svelte +503 -0
  15. package/src/components/grid/cells/TextCell.svelte +110 -0
  16. package/src/components/grid/controls/AddColumnButton.svelte +16 -0
  17. package/src/components/grid/controls/AddRowButton.svelte +18 -0
  18. package/src/components/grid/controls/BetaButton.svelte +46 -0
  19. package/src/components/grid/controls/DeleteButton.svelte +72 -0
  20. package/src/components/grid/controls/HideColumnsButton.svelte +91 -0
  21. package/src/components/grid/controls/RowHeightButton.svelte +70 -0
  22. package/src/components/grid/controls/SortButton.svelte +107 -0
  23. package/src/components/grid/index.js +1 -0
  24. package/src/components/grid/layout/Avatar.svelte +24 -0
  25. package/src/components/grid/layout/Grid.svelte +250 -0
  26. package/src/components/grid/layout/GridBody.svelte +38 -0
  27. package/src/components/grid/layout/GridRow.svelte +57 -0
  28. package/src/components/grid/layout/GridScrollWrapper.svelte +84 -0
  29. package/src/components/grid/layout/HeaderRow.svelte +30 -0
  30. package/src/components/grid/layout/NewRowTop.svelte +247 -0
  31. package/src/components/grid/layout/RowCount.svelte +9 -0
  32. package/src/components/grid/layout/StickyColumn.svelte +245 -0
  33. package/src/components/grid/layout/UserAvatars.svelte +20 -0
  34. package/src/components/grid/lib/constants.js +11 -0
  35. package/src/components/grid/lib/events.js +29 -0
  36. package/src/components/grid/lib/renderers.js +29 -0
  37. package/src/components/grid/lib/utils.js +26 -0
  38. package/src/components/grid/lib/websocket.js +63 -0
  39. package/src/components/grid/overlays/KeyboardManager.svelte +180 -0
  40. package/src/components/grid/overlays/MenuOverlay.svelte +95 -0
  41. package/src/components/grid/overlays/ReorderOverlay.svelte +63 -0
  42. package/src/components/grid/overlays/ResizeOverlay.svelte +70 -0
  43. package/src/components/grid/overlays/ScrollOverlay.svelte +121 -0
  44. package/src/components/grid/stores/bounds.js +16 -0
  45. package/src/components/grid/stores/clipboard.js +34 -0
  46. package/src/components/grid/stores/columns.js +206 -0
  47. package/src/components/grid/stores/index.js +48 -0
  48. package/src/components/grid/stores/menu.js +49 -0
  49. package/src/components/grid/stores/pagination.js +24 -0
  50. package/src/components/grid/stores/reorder.js +155 -0
  51. package/src/components/grid/stores/resize.js +124 -0
  52. package/src/components/grid/stores/rows.js +462 -0
  53. package/src/components/grid/stores/scroll.js +201 -0
  54. package/src/components/grid/stores/ui.js +168 -0
  55. package/src/components/grid/stores/users.js +115 -0
  56. package/src/components/grid/stores/validation.js +45 -0
  57. package/src/components/grid/stores/viewport.js +141 -0
  58. package/src/components/index.js +1 -0
  59. package/src/fetch/DataFetch.js +27 -21
  60. package/src/fetch/QueryFetch.js +1 -1
  61. package/src/themes/midnight.css +3 -0
  62. package/src/themes/nord.css +3 -0
  63. package/src/utils/utils.js +20 -0
@@ -0,0 +1,503 @@
1
+ <script context="module">
2
+ // We can create a module level cache for all relationship cells to avoid
3
+ // having to fetch the table definition one time for each cell
4
+ let primaryDisplayCache = {}
5
+
6
+ const getPrimaryDisplayForTableId = async (API, tableId) => {
7
+ if (primaryDisplayCache[tableId]) {
8
+ return primaryDisplayCache[tableId]
9
+ }
10
+ const definition = await API.fetchTableDefinition(tableId)
11
+ const primaryDisplay =
12
+ definition?.primaryDisplay || definition?.schema?.[0]?.name
13
+ primaryDisplayCache[tableId] = primaryDisplay
14
+ return primaryDisplay
15
+ }
16
+ </script>
17
+
18
+ <script>
19
+ import { getColor } from "../lib/utils"
20
+ import { onMount, getContext } from "svelte"
21
+ import { Icon, Input, ProgressCircle } from "@budibase/bbui"
22
+ import { debounce } from "../../../utils/utils"
23
+
24
+ export let value
25
+ export let api
26
+ export let readonly
27
+ export let focused
28
+ export let schema
29
+ export let onChange
30
+ export let invertX = false
31
+ export let invertY = false
32
+
33
+ const { API, dispatch } = getContext("grid")
34
+ const color = getColor(0)
35
+
36
+ let isOpen = false
37
+ let searchResults
38
+ let searchString
39
+ let lastSearchString
40
+ let primaryDisplay
41
+ let candidateIndex
42
+ let lastSearchId
43
+ let searching = false
44
+
45
+ $: oneRowOnly = schema?.relationshipType === "one-to-many"
46
+ $: editable = focused && !readonly
47
+ $: lookupMap = buildLookupMap(value, isOpen)
48
+ $: debouncedSearch(searchString)
49
+ $: {
50
+ if (!focused) {
51
+ close()
52
+ }
53
+ }
54
+
55
+ // Builds a lookup map to quickly check which rows are selected
56
+ const buildLookupMap = (value, isOpen) => {
57
+ let map = {}
58
+ if (!isOpen || !value?.length) {
59
+ return map
60
+ }
61
+ for (let i = 0; i < value.length; i++) {
62
+ map[value[i]._id] = true
63
+ }
64
+ return map
65
+ }
66
+
67
+ // Checks if a certain row is currently selected
68
+ const isRowSelected = row => {
69
+ if (!row?._id) {
70
+ return false
71
+ }
72
+ return lookupMap?.[row._id] === true
73
+ }
74
+
75
+ // Search for rows based on the search string
76
+ const search = async (searchString, force = false) => {
77
+ // Avoid update state at all if we've already handled the update and this is
78
+ // a wasted search due to svelte reactivity
79
+ if (!force && !searchString && !lastSearchString) {
80
+ return
81
+ }
82
+
83
+ // Reset state if this search is invalid
84
+ if (!schema?.tableId || !isOpen) {
85
+ lastSearchString = null
86
+ candidateIndex = null
87
+ searchResults = []
88
+ return
89
+ }
90
+
91
+ // Search for results, using IDs to track invocations and ensure we're
92
+ // handling the latest update
93
+ lastSearchId = Math.random()
94
+ searching = true
95
+ const thisSearchId = lastSearchId
96
+ const results = await API.searchTable({
97
+ paginate: false,
98
+ tableId: schema.tableId,
99
+ limit: 20,
100
+ query: {
101
+ string: {
102
+ [`1:${primaryDisplay}`]: searchString || "",
103
+ },
104
+ },
105
+ })
106
+ searching = false
107
+
108
+ // In case searching takes longer than our debounced update, abandon these
109
+ // results
110
+ if (thisSearchId !== lastSearchId) {
111
+ return
112
+ }
113
+
114
+ // Sort and process results
115
+ searchResults = sortRows(
116
+ results.rows?.map(row => ({
117
+ ...row,
118
+ primaryDisplay: row[primaryDisplay],
119
+ }))
120
+ )
121
+ candidateIndex = searchResults?.length ? 0 : null
122
+ lastSearchString = searchString
123
+ }
124
+
125
+ // Debounced version of searching
126
+ const debouncedSearch = debounce(search, 250)
127
+
128
+ // Alphabetically sorts rows by their primary display column
129
+ const sortRows = rows => {
130
+ if (!rows?.length) {
131
+ return []
132
+ }
133
+ return rows.slice().sort((a, b) => {
134
+ return a.primaryDisplay < b.primaryDisplay ? -1 : 1
135
+ })
136
+ }
137
+
138
+ const open = async () => {
139
+ isOpen = true
140
+
141
+ // Find the primary display for the related table
142
+ if (!primaryDisplay) {
143
+ searching = true
144
+ primaryDisplay = await getPrimaryDisplayForTableId(API, schema.tableId)
145
+ }
146
+
147
+ // Show initial list of results
148
+ await search(null, true)
149
+ }
150
+
151
+ const close = () => {
152
+ isOpen = false
153
+ searchResults = []
154
+ searchString = null
155
+ lastSearchString = null
156
+ candidateIndex = null
157
+ }
158
+
159
+ const onKeyDown = e => {
160
+ if (!isOpen) {
161
+ return false
162
+ }
163
+ if (e.key === "ArrowDown") {
164
+ // Select next result on down arrow
165
+ e.preventDefault()
166
+ if (candidateIndex == null) {
167
+ candidateIndex = 0
168
+ } else {
169
+ candidateIndex = Math.min(searchResults.length - 1, candidateIndex + 1)
170
+ }
171
+ } else if (e.key === "ArrowUp") {
172
+ // Select previous result on up array
173
+ e.preventDefault()
174
+ if (candidateIndex === 0) {
175
+ candidateIndex = null
176
+ } else if (candidateIndex != null) {
177
+ candidateIndex = Math.max(0, candidateIndex - 1)
178
+ }
179
+ } else if (e.key === "Enter") {
180
+ // Toggle the highlighted result on enter press
181
+ if (candidateIndex != null && searchResults[candidateIndex] != null) {
182
+ toggleRow(searchResults[candidateIndex])
183
+ }
184
+ }
185
+ return true
186
+ }
187
+
188
+ // Toggles whether a row is included in the relationship or not
189
+ const toggleRow = async row => {
190
+ if (value?.some(x => x._id === row._id)) {
191
+ // If the row is already included, remove it and update the candidate
192
+ // row to be the the same position if possible
193
+ if (oneRowOnly) {
194
+ await onChange([])
195
+ } else {
196
+ const newValue = value.filter(x => x._id !== row._id)
197
+ if (!newValue.length) {
198
+ candidateIndex = null
199
+ } else {
200
+ candidateIndex = Math.min(candidateIndex, newValue.length - 1)
201
+ }
202
+ await onChange(newValue)
203
+ }
204
+ } else {
205
+ // If we don't have this row, include it
206
+ if (oneRowOnly) {
207
+ await onChange([row])
208
+ } else {
209
+ await onChange(sortRows([...(value || []), row]))
210
+ }
211
+ candidateIndex = null
212
+ }
213
+ close()
214
+ }
215
+
216
+ const showRelationship = async id => {
217
+ const relatedRow = await API.fetchRow({
218
+ tableId: schema.tableId,
219
+ rowId: id,
220
+ })
221
+ dispatch("edit-row", relatedRow)
222
+ }
223
+
224
+ const readable = value => {
225
+ if (value == null) {
226
+ return ""
227
+ }
228
+ if (value instanceof Object) {
229
+ return JSON.stringify(value)
230
+ }
231
+ return value
232
+ }
233
+
234
+ onMount(() => {
235
+ api = {
236
+ focus: open,
237
+ blur: close,
238
+ onKeyDown,
239
+ }
240
+ })
241
+ </script>
242
+
243
+ <div class="wrapper" class:editable class:focused style="--color:{color};">
244
+ <div class="container">
245
+ <div class="values" on:wheel={e => (focused ? e.stopPropagation() : null)}>
246
+ {#each value || [] as relationship, idx}
247
+ {#if relationship.primaryDisplay}
248
+ <div class="badge">
249
+ <span
250
+ on:click={editable
251
+ ? () => showRelationship(relationship._id)
252
+ : null}
253
+ >
254
+ {readable(relationship.primaryDisplay)}
255
+ </span>
256
+ {#if editable}
257
+ <Icon
258
+ name="Close"
259
+ size="XS"
260
+ hoverable
261
+ on:click={() => toggleRow(relationship)}
262
+ />
263
+ {/if}
264
+ </div>
265
+ {/if}
266
+ {/each}
267
+ {#if editable}
268
+ <div class="add" on:click={open}>
269
+ <Icon name="Add" size="S" />
270
+ </div>
271
+ {/if}
272
+ </div>
273
+ {#if value?.length}
274
+ <div class="count">
275
+ {value?.length || 0}
276
+ </div>
277
+ {/if}
278
+ </div>
279
+
280
+ {#if isOpen}
281
+ <div class="dropdown" class:invertX class:invertY on:wheel|stopPropagation>
282
+ <div class="search">
283
+ <Input
284
+ autofocus
285
+ quiet
286
+ type="text"
287
+ bind:value={searchString}
288
+ placeholder={primaryDisplay ? `Search by ${primaryDisplay}` : null}
289
+ />
290
+ </div>
291
+ {#if searching}
292
+ <div class="searching">
293
+ <ProgressCircle size="S" />
294
+ </div>
295
+ {:else if searchResults?.length}
296
+ <div class="results">
297
+ {#each searchResults as row, idx}
298
+ <div
299
+ class="result"
300
+ on:click={() => toggleRow(row)}
301
+ class:candidate={idx === candidateIndex}
302
+ on:mouseenter={() => (candidateIndex = idx)}
303
+ >
304
+ <div class="badge">
305
+ <span>
306
+ {readable(row.primaryDisplay)}
307
+ </span>
308
+ </div>
309
+ {#if isRowSelected(row)}
310
+ <Icon
311
+ size="S"
312
+ name="Checkmark"
313
+ color="var(--spectrum-global-color-blue-400)"
314
+ />
315
+ {/if}
316
+ </div>
317
+ {/each}
318
+ </div>
319
+ {/if}
320
+ </div>
321
+ {/if}
322
+ </div>
323
+
324
+ <style>
325
+ .wrapper {
326
+ flex: 1 1 auto;
327
+ align-self: flex-start;
328
+ min-height: var(--row-height);
329
+ max-height: var(--row-height);
330
+ overflow: hidden;
331
+ --max-relationship-height: 120px;
332
+ }
333
+ .wrapper.focused {
334
+ position: absolute;
335
+ top: 0;
336
+ left: 0;
337
+ width: 100%;
338
+ background: var(--cell-background);
339
+ z-index: 1;
340
+ max-height: none;
341
+ overflow: visible;
342
+ }
343
+
344
+ .container {
345
+ min-height: var(--row-height);
346
+ overflow: hidden;
347
+ display: flex;
348
+ align-items: flex-start;
349
+ }
350
+ .focused .container {
351
+ overflow-y: auto;
352
+ border-radius: 2px;
353
+ max-height: var(--max-relationship-height);
354
+ }
355
+ .focused .container:after {
356
+ content: " ";
357
+ position: absolute;
358
+ top: 0;
359
+ left: 0;
360
+ height: 100%;
361
+ width: 100%;
362
+ border: 2px solid var(--cell-color);
363
+ pointer-events: none;
364
+ border-radius: 2px;
365
+ box-sizing: border-box;
366
+ }
367
+
368
+ .values {
369
+ display: flex;
370
+ flex-direction: row;
371
+ justify-content: flex-start;
372
+ align-items: flex-start;
373
+ flex: 1 1 auto;
374
+ grid-column-gap: var(--cell-spacing);
375
+ grid-row-gap: var(--cell-padding);
376
+ overflow: hidden;
377
+ padding: var(--cell-padding);
378
+ flex-wrap: wrap;
379
+ }
380
+ .count {
381
+ display: none;
382
+ position: absolute;
383
+ top: 0;
384
+ right: 0;
385
+ color: var(--spectrum-global-color-gray-500);
386
+ padding: var(--cell-padding) var(--cell-padding) var(--cell-padding) 20px;
387
+ background: linear-gradient(
388
+ to right,
389
+ transparent 0%,
390
+ var(--cell-background) 40%
391
+ );
392
+ }
393
+ .wrapper:hover:not(.focused) .count {
394
+ display: block;
395
+ }
396
+ .badge {
397
+ flex: 0 0 auto;
398
+ padding: 0 var(--cell-padding);
399
+ background: var(--color);
400
+ border-radius: var(--cell-padding);
401
+ user-select: none;
402
+ display: flex;
403
+ align-items: center;
404
+ gap: var(--cell-spacing);
405
+ height: 20px;
406
+ max-width: 100%;
407
+ }
408
+ .badge span {
409
+ overflow: hidden;
410
+ white-space: nowrap;
411
+ text-overflow: ellipsis;
412
+ }
413
+ .editable .values .badge span:hover {
414
+ cursor: pointer;
415
+ text-decoration: underline;
416
+ }
417
+
418
+ .add {
419
+ background: var(--spectrum-global-color-gray-200);
420
+ padding: 4px;
421
+ border-radius: 4px;
422
+ }
423
+ .add :global(.spectrum-Icon) {
424
+ width: 12px;
425
+ height: 12px;
426
+ }
427
+ .add:hover {
428
+ background: var(--spectrum-global-color-gray-300);
429
+ cursor: pointer;
430
+ }
431
+
432
+ .dropdown {
433
+ position: absolute;
434
+ top: 100%;
435
+ left: 0;
436
+ width: 100%;
437
+ max-height: calc(
438
+ var(--max-cell-render-height) + var(--row-height) -
439
+ var(--max-relationship-height)
440
+ );
441
+ background: var(--background);
442
+ border: var(--cell-border);
443
+ box-shadow: 0 0 20px -4px rgba(0, 0, 0, 0.15);
444
+ display: flex;
445
+ flex-direction: column;
446
+ align-items: stretch;
447
+ padding: 0 0 8px 0;
448
+ }
449
+ .dropdown.invertY {
450
+ transform: translateY(-100%);
451
+ top: -1px;
452
+ }
453
+ .dropdown.invertX {
454
+ left: auto;
455
+ right: 0;
456
+ }
457
+
458
+ .searching {
459
+ padding: var(--cell-padding);
460
+ display: flex;
461
+ justify-content: center;
462
+ }
463
+ .results {
464
+ overflow-y: auto;
465
+ overflow-x: hidden;
466
+ display: flex;
467
+ flex-direction: column;
468
+ align-items: stretch;
469
+ }
470
+ .result {
471
+ padding: 0 var(--cell-padding);
472
+ flex: 0 0 var(--default-row-height);
473
+ display: flex;
474
+ gap: var(--cell-spacing);
475
+ justify-content: space-between;
476
+ align-items: center;
477
+ }
478
+ .result.candidate {
479
+ background-color: var(--spectrum-global-color-gray-200);
480
+ cursor: pointer;
481
+ }
482
+ .result .badge {
483
+ max-width: calc(100% - 30px);
484
+ }
485
+
486
+ .search {
487
+ flex: 0 0 calc(var(--default-row-height) - 1px);
488
+ display: flex;
489
+ align-items: center;
490
+ margin: 4px var(--cell-padding);
491
+ width: calc(100% - 2 * var(--cell-padding));
492
+ }
493
+ .search :global(.spectrum-Textfield) {
494
+ min-width: 0;
495
+ width: 100%;
496
+ }
497
+ .search :global(.spectrum-Textfield-input) {
498
+ font-size: 13px;
499
+ }
500
+ .search :global(.spectrum-Form-item) {
501
+ flex: 1 1 auto;
502
+ }
503
+ </style>
@@ -0,0 +1,110 @@
1
+ <script>
2
+ import { onMount } from "svelte"
3
+
4
+ export let value
5
+ export let focused = false
6
+ export let onChange
7
+ export let type = "text"
8
+ export let readonly = false
9
+ export let api
10
+
11
+ let input
12
+ let active = false
13
+
14
+ $: editable = focused && !readonly
15
+
16
+ const handleChange = e => {
17
+ onChange(e.target.value)
18
+ }
19
+
20
+ const onKeyDown = e => {
21
+ if (!active) {
22
+ return false
23
+ }
24
+ if (e.key === "Enter") {
25
+ input?.blur()
26
+ const event = new KeyboardEvent("keydown", { key: "ArrowDown" })
27
+ document.dispatchEvent(event)
28
+ }
29
+ return true
30
+ }
31
+
32
+ onMount(() => {
33
+ api = {
34
+ focus: () => input?.focus(),
35
+ blur: () => input?.blur(),
36
+ onKeyDown,
37
+ }
38
+ })
39
+ </script>
40
+
41
+ {#if editable}
42
+ <input
43
+ bind:this={input}
44
+ on:focus={() => (active = true)}
45
+ on:blur={() => (active = false)}
46
+ {type}
47
+ value={value || ""}
48
+ on:change={handleChange}
49
+ spellcheck="false"
50
+ />
51
+ {:else}
52
+ <div class="text-cell" class:number={type === "number"}>
53
+ <div class="value">
54
+ {value || ""}
55
+ </div>
56
+ </div>
57
+ {/if}
58
+
59
+ <style>
60
+ .text-cell {
61
+ flex: 1 1 auto;
62
+ padding: var(--cell-padding);
63
+ align-self: stretch;
64
+ display: flex;
65
+ align-items: flex-start;
66
+ overflow: hidden;
67
+ }
68
+ .text-cell.number {
69
+ justify-content: flex-end;
70
+ }
71
+ .value {
72
+ display: -webkit-box;
73
+ -webkit-line-clamp: var(--content-lines);
74
+ -webkit-box-orient: vertical;
75
+ overflow: hidden;
76
+ line-height: 20px;
77
+ }
78
+ .number .value {
79
+ -webkit-line-clamp: 1;
80
+ }
81
+ input {
82
+ flex: 1 1 auto;
83
+ border: none;
84
+ padding: var(--cell-padding);
85
+ overflow: hidden;
86
+ text-overflow: ellipsis;
87
+ white-space: nowrap;
88
+ background: none;
89
+ font-size: var(--cell-font-size);
90
+ font-family: var(--font-sans);
91
+ color: inherit;
92
+ line-height: 20px;
93
+ }
94
+ input:focus {
95
+ outline: none;
96
+ }
97
+ input[type="number"] {
98
+ text-align: right;
99
+ }
100
+
101
+ /* Hide arrows for number fields */
102
+ input::-webkit-outer-spin-button,
103
+ input::-webkit-inner-spin-button {
104
+ -webkit-appearance: none;
105
+ margin: 0;
106
+ }
107
+ input[type="number"] {
108
+ -moz-appearance: textfield;
109
+ }
110
+ </style>
@@ -0,0 +1,16 @@
1
+ <script>
2
+ import { ActionButton } from "@budibase/bbui"
3
+ import { getContext } from "svelte"
4
+
5
+ const { config, dispatch } = getContext("grid")
6
+ </script>
7
+
8
+ <ActionButton
9
+ icon="TableColumnAddRight"
10
+ quiet
11
+ size="M"
12
+ on:click={() => dispatch("add-column")}
13
+ disabled={!$config.allowAddColumns}
14
+ >
15
+ Create column
16
+ </ActionButton>
@@ -0,0 +1,18 @@
1
+ <script>
2
+ import { ActionButton } from "@budibase/bbui"
3
+ import { getContext } from "svelte"
4
+
5
+ const { dispatch, columns, stickyColumn, config, loaded } = getContext("grid")
6
+ </script>
7
+
8
+ <ActionButton
9
+ icon="TableRowAddBottom"
10
+ quiet
11
+ size="M"
12
+ on:click={() => dispatch("add-row")}
13
+ disabled={!loaded ||
14
+ !$config.allowAddRows ||
15
+ (!$columns.length && !$stickyColumn)}
16
+ >
17
+ Create row
18
+ </ActionButton>
@@ -0,0 +1,46 @@
1
+ <script>
2
+ import { Button } from "@budibase/bbui"
3
+ </script>
4
+
5
+ <div class="beta-background" />
6
+ <div class="beta">
7
+ Enjoying the Grid?
8
+ <Button
9
+ size="M"
10
+ cta
11
+ on:click={() => window.open("https://t.maze.co/156382627", "_blank")}
12
+ >
13
+ Give Feedback
14
+ </Button>
15
+ </div>
16
+
17
+ <style>
18
+ .beta {
19
+ position: absolute;
20
+ bottom: 32px;
21
+ right: 32px;
22
+ display: flex;
23
+ flex-direction: row;
24
+ align-items: center;
25
+ gap: 16px;
26
+ }
27
+ .beta :global(.spectrum-Button) {
28
+ background: var(--spectrum-global-color-magenta-400);
29
+ border-color: var(--spectrum-global-color-magenta-400);
30
+ }
31
+ .beta-background {
32
+ z-index: 0;
33
+ pointer-events: none;
34
+ position: absolute;
35
+ bottom: -230px;
36
+ right: -105px;
37
+ width: 1400px;
38
+ height: 320px;
39
+ transform: rotate(-22deg);
40
+ background: linear-gradient(
41
+ to top,
42
+ var(--cell-background) 20%,
43
+ transparent
44
+ );
45
+ }
46
+ </style>