@budibase/bbui 3.42.0 → 3.43.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,7 +1,7 @@
1
1
  {
2
2
  "name": "@budibase/bbui",
3
3
  "description": "A UI solution used in the different Budibase projects.",
4
- "version": "v3.42.0",
4
+ "version": "v3.43.0",
5
5
  "license": "MPL-2.0",
6
6
  "module": "dist/bbui.mjs",
7
7
  "exports": {
@@ -75,9 +75,9 @@
75
75
  "@spectrum-css/underlay": "2.0.9",
76
76
  "@spectrum-css/vars": "3.0.1",
77
77
  "atrament": "^4.3.0",
78
- "date-fns": "^4.1.0",
79
78
  "dayjs": "^1.10.8",
80
79
  "easymde": "^2.16.1",
80
+ "nanoid": "6.0.1",
81
81
  "sanitize-html": "^2.13.0",
82
82
  "svelte-portal": "^2.2.1"
83
83
  },
@@ -108,5 +108,5 @@
108
108
  }
109
109
  }
110
110
  },
111
- "gitHead": "180c04c6852aa7acf0402d53dc909eab764d0251"
111
+ "gitHead": "221c199a898fffd9c8803d5327cc3089cc02b7f3"
112
112
  }
@@ -66,7 +66,6 @@
66
66
  </script>
67
67
 
68
68
  <script lang="ts">
69
- import { generate } from "shortid"
70
69
  import { createEventDispatcher, onDestroy, setContext } from "svelte"
71
70
  import type { TransitionConfig } from "svelte/transition"
72
71
  import Portal from "svelte-portal"
@@ -79,6 +78,7 @@
79
78
  overlayStack,
80
79
  BASE_Z_INDEX,
81
80
  } from "../Modal/overlayStack"
81
+ import { generateId } from "../utils/ids"
82
82
 
83
83
  interface DrawerEvents {
84
84
  drawerShow: string
@@ -93,7 +93,7 @@
93
93
  const spacing = 11
94
94
 
95
95
  let visible: boolean = false
96
- let drawerId: string = generate()
96
+ let drawerId: string = generateId()
97
97
  let depth: number = 0
98
98
  let stackIndex: number = -1
99
99
  let computedZIndex: number = BASE_Z_INDEX
@@ -6,7 +6,7 @@
6
6
  import { stringifyDate } from "../../../helpers"
7
7
  import Calendar from "./Calendar.svelte"
8
8
  import TimePicker from "./TimePicker.svelte"
9
- import type { Weekday } from "./utils"
9
+ import { getLocaleStartDayOfWeek, type Weekday } from "./utils"
10
10
 
11
11
  export let useKeyboardShortcuts = true
12
12
  export let ignoreTimezones = false
@@ -14,7 +14,8 @@
14
14
  export let timeOnly = false
15
15
  export let setTimeTo: string | undefined = undefined
16
16
  export let value: Dayjs | null | undefined = null
17
- export let startDayOfWeek: Weekday = "Monday"
17
+ const browserStartDayOfWeek = getLocaleStartDayOfWeek()
18
+ export let startDayOfWeek: Weekday | undefined = undefined
18
19
  export let calendarLabels = resolveTranslationGroup("calendar")
19
20
 
20
21
  const dispatch = createEventDispatcher<{ change: string | null }>()
@@ -22,6 +23,7 @@
22
23
 
23
24
  $: showCalendar = !timeOnly
24
25
  $: showTime = enableTime || timeOnly
26
+ $: resolvedStartDayOfWeek = startDayOfWeek ?? browserStartDayOfWeek
25
27
 
26
28
  const setToNow = () => {
27
29
  const now = dayjs().second(0).millisecond(0)
@@ -65,7 +67,7 @@
65
67
  {#if showCalendar}
66
68
  <Calendar
67
69
  {value}
68
- {startDayOfWeek}
70
+ startDayOfWeek={resolvedStartDayOfWeek}
69
71
  {calendarLabels}
70
72
  on:change={e => handleChange(e.detail)}
71
73
  bind:this={calendar}
@@ -0,0 +1,53 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest"
2
+ import { getLocaleStartDayOfWeek } from "./utils"
3
+
4
+ interface LocaleWithWeekInfo {
5
+ readonly weekInfo?: { firstDay?: number }
6
+ }
7
+
8
+ describe("getLocaleStartDayOfWeek", () => {
9
+ afterEach(() => {
10
+ vi.restoreAllMocks()
11
+ vi.unstubAllGlobals()
12
+ })
13
+
14
+ it("uses the locale's first day of the week", () => {
15
+ expect(getLocaleStartDayOfWeek(["en-US"])).toBe("Sunday")
16
+ expect(getLocaleStartDayOfWeek(["en-GB"])).toBe("Monday")
17
+ expect(getLocaleStartDayOfWeek(["ar-AF"])).toBe("Saturday")
18
+ expect(getLocaleStartDayOfWeek(["dv-MV"])).toBe("Friday")
19
+ })
20
+
21
+ it("normalizes locale separators", () => {
22
+ expect(getLocaleStartDayOfWeek(["en_US"])).toBe("Sunday")
23
+ })
24
+
25
+ it("uses the browser's preferred locales by default", () => {
26
+ vi.stubGlobal("navigator", {
27
+ language: "en-US",
28
+ languages: ["en-US"],
29
+ })
30
+
31
+ expect(getLocaleStartDayOfWeek()).toBe("Sunday")
32
+ })
33
+
34
+ it("uses the next valid locale", () => {
35
+ expect(getLocaleStartDayOfWeek(["invalid--locale", "en-US"])).toBe("Sunday")
36
+ })
37
+
38
+ it("uses region data when locale week information is unavailable", () => {
39
+ const localePrototype: Intl.Locale & LocaleWithWeekInfo =
40
+ Intl.Locale.prototype
41
+ vi.spyOn(localePrototype, "weekInfo", "get").mockReturnValue(undefined)
42
+
43
+ expect(getLocaleStartDayOfWeek(["en-US"])).toBe("Sunday")
44
+ expect(getLocaleStartDayOfWeek(["ar-AF"])).toBe("Saturday")
45
+ expect(getLocaleStartDayOfWeek(["dv-MV"])).toBe("Friday")
46
+ expect(getLocaleStartDayOfWeek(["en-GB"])).toBe("Monday")
47
+ })
48
+
49
+ it("falls back to Monday without supported locale metadata", () => {
50
+ expect(getLocaleStartDayOfWeek([])).toBe("Monday")
51
+ expect(getLocaleStartDayOfWeek(["invalid--locale"])).toBe("Monday")
52
+ })
53
+ })
@@ -1,12 +1,18 @@
1
- import type { Locale } from "date-fns"
2
- import * as dateFnsLocales from "date-fns/locale"
3
-
4
1
  interface Input {
5
2
  max: number
6
3
  pad: number
7
4
  fallback: string
8
5
  }
9
6
 
7
+ interface LocaleWeekInfo {
8
+ firstDay?: number
9
+ }
10
+
11
+ interface LocaleWithWeekInfo {
12
+ readonly weekInfo?: LocaleWeekInfo
13
+ getWeekInfo?: () => LocaleWeekInfo
14
+ }
15
+
10
16
  export type Weekday =
11
17
  | "Sunday"
12
18
  | "Monday"
@@ -27,6 +33,19 @@ const WEEKDAY_BY_INDEX: Weekday[] = [
27
33
  "Saturday",
28
34
  ]
29
35
 
36
+ // Fallback data for browsers without Intl.Locale week information support.
37
+ // Regions not listed use CLDR's global Monday default.
38
+ // Source: unicode-org/cldr-json/cldr-json/cldr-core/supplemental/weekData.json
39
+ const SUNDAY_START_REGIONS = new Set(
40
+ "AG AS BD BR BS BT BW BZ CA CO DM DO ET GT GU HK HN ID IL IN IS JM JP KE KH KR LA MH MM MO MT MX MZ NI NP PA PE PH PK PR PT SA SG SV TH TT TW UM US VE VI WS YE ZA ZW".split(
41
+ " "
42
+ )
43
+ )
44
+ const SATURDAY_START_REGIONS = new Set(
45
+ "AF BH DJ DZ EG IQ IR JO KW LY OM QA SD SY".split(" ")
46
+ )
47
+ const FRIDAY_START_REGIONS = new Set(["MV"])
48
+
30
49
  const normalizeLocaleCode = (code?: string | null) => {
31
50
  if (!code) {
32
51
  return null
@@ -34,52 +53,6 @@ const normalizeLocaleCode = (code?: string | null) => {
34
53
  return code.toLowerCase().replace(/_/g, "-")
35
54
  }
36
55
 
37
- const normalizeLocaleKey = (key?: string) => {
38
- if (!key) {
39
- return null
40
- }
41
- return key
42
- .replace(/_/g, "-")
43
- .replace(/([a-z])([A-Z])/g, "$1-$2")
44
- .toLowerCase()
45
- }
46
-
47
- const localeLookup = (() => {
48
- const lookup = new Map<string, Locale>()
49
-
50
- const register = (key: string, locale: Locale | undefined) => {
51
- if (!locale) {
52
- return
53
- }
54
- const codeCandidates = new Set<string>()
55
- const normalizedKey = normalizeLocaleKey(key)
56
- if (normalizedKey) {
57
- codeCandidates.add(normalizedKey)
58
- }
59
- const normalizedLocaleCode = normalizeLocaleCode(locale.code)
60
- if (normalizedLocaleCode) {
61
- codeCandidates.add(normalizedLocaleCode)
62
- }
63
- for (const candidate of codeCandidates) {
64
- if (!lookup.has(candidate)) {
65
- lookup.set(candidate, locale)
66
- }
67
- const base = candidate.split("-")[0]
68
- if (base && !lookup.has(base)) {
69
- lookup.set(base, locale)
70
- }
71
- }
72
- }
73
-
74
- for (const [key, locale] of Object.entries(dateFnsLocales)) {
75
- if (typeof locale === "object" && locale) {
76
- register(key, locale as Locale)
77
- }
78
- }
79
-
80
- return lookup
81
- })()
82
-
83
56
  const getNavigatorLocales = (): readonly string[] => {
84
57
  if (typeof navigator === "undefined") {
85
58
  return []
@@ -90,6 +63,34 @@ const getNavigatorLocales = (): readonly string[] => {
90
63
  return navigator.language ? [navigator.language] : []
91
64
  }
92
65
 
66
+ const getFallbackFirstDay = (locale: Intl.Locale): number => {
67
+ const region = locale.region ?? locale.maximize().region
68
+ if (region && SUNDAY_START_REGIONS.has(region)) {
69
+ return 7
70
+ }
71
+ if (region && SATURDAY_START_REGIONS.has(region)) {
72
+ return 6
73
+ }
74
+ if (region && FRIDAY_START_REGIONS.has(region)) {
75
+ return 5
76
+ }
77
+ return 1
78
+ }
79
+
80
+ const getLocaleFirstDay = (code: string): number | undefined => {
81
+ if (typeof Intl.Locale !== "function") {
82
+ return undefined
83
+ }
84
+
85
+ try {
86
+ const locale = new Intl.Locale(code) as Intl.Locale & LocaleWithWeekInfo
87
+ const weekInfo = locale.weekInfo ?? locale.getWeekInfo?.()
88
+ return weekInfo?.firstDay ?? getFallbackFirstDay(locale)
89
+ } catch {
90
+ return undefined
91
+ }
92
+ }
93
+
93
94
  export const getLocaleStartDayOfWeek = (
94
95
  locales: readonly string[] = getNavigatorLocales()
95
96
  ): Weekday => {
@@ -98,12 +99,9 @@ export const getLocaleStartDayOfWeek = (
98
99
  if (!normalized) {
99
100
  continue
100
101
  }
101
- const match =
102
- localeLookup.get(normalized) || localeLookup.get(normalized.split("-")[0])
103
- const weekStartsOn = match?.options?.weekStartsOn
104
- if (typeof weekStartsOn === "number") {
105
- const index = ((weekStartsOn % 7) + 7) % 7
106
- return WEEKDAY_BY_INDEX[index]
102
+ const firstDay = getLocaleFirstDay(normalized)
103
+ if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) {
104
+ return WEEKDAY_BY_INDEX[firstDay % 7]
107
105
  }
108
106
  }
109
107
  return DEFAULT_WEEKDAY
@@ -22,6 +22,7 @@
22
22
  undefined
23
23
  export let gallery: boolean = true
24
24
  export let fileTags: string[] = []
25
+ export let extensions: string = "*"
25
26
  export let maximum: number | undefined = undefined
26
27
  export let compact: boolean = false
27
28
  export let helpText: string | undefined = undefined
@@ -44,6 +45,7 @@
44
45
  {handleTooManyFiles}
45
46
  {gallery}
46
47
  {fileTags}
48
+ {extensions}
47
49
  {maximum}
48
50
  {compact}
49
51
  on:change={onChange}
@@ -21,7 +21,6 @@
21
21
  import Portal from "svelte-portal"
22
22
  import Context from "../context"
23
23
  import { ModalCancelFrom } from "../constants"
24
- import { generate } from "shortid"
25
24
  import {
26
25
  BASE_Z_INDEX,
27
26
  overlayStack,
@@ -29,6 +28,7 @@
29
28
  removeOverlay,
30
29
  isActiveOverlay,
31
30
  } from "./overlayStack"
31
+ import { generateId } from "../utils/ids"
32
32
 
33
33
  export let fixed: boolean = false
34
34
  export let inline: boolean = false
@@ -44,7 +44,7 @@
44
44
 
45
45
  // Ensure any popovers inside this modal are rendered inside this modal
46
46
  // Unique ids are required to ensure nested modals are parented correctly
47
- const uniqueId = generate()
47
+ const uniqueId = generateId()
48
48
  const modalId = uniqueId
49
49
  setContext(Context.PopoverRoot, `.spectrum-Modal-${uniqueId}`)
50
50
 
@@ -3,16 +3,19 @@
3
3
  import AbsTooltip from "../Tooltip/AbsTooltip.svelte"
4
4
  import ActionButton from "../ActionButton/ActionButton.svelte"
5
5
 
6
- export let leftIcon: string
6
+ export let leftIcon: string | undefined = undefined
7
7
  export let leftNotificationTooltip: string | undefined = undefined
8
8
  export let leftNotificationCount: number | undefined = undefined
9
9
  export let leftText: string
10
- export let rightIcon: string
10
+ export let rightIcon: string | undefined = undefined
11
11
  export let rightNotificationTooltip: string | undefined = undefined
12
12
  export let rightNotificationCount: number | undefined = undefined
13
13
  export let rightText: string
14
14
  export let selected: "left" | "right" = "left"
15
15
  export let disabled = false
16
+ export let leftDisabled = false
17
+ export let rightDisabled = false
18
+ export let size: "S" | "M" | "L" = "M"
16
19
 
17
20
  const dispatch = createEventDispatcher<{
18
21
  left: void
@@ -21,7 +24,7 @@
21
24
  </script>
22
25
 
23
26
  <div class="view-mode-toggle" class:disabled>
24
- <div class="group">
27
+ <div class="group size-{size}">
25
28
  <div class="wrapper">
26
29
  {#if leftNotificationTooltip && leftNotificationCount}
27
30
  <AbsTooltip text={leftNotificationTooltip}>
@@ -39,7 +42,8 @@
39
42
  <ActionButton
40
43
  icon={leftIcon}
41
44
  quiet
42
- {disabled}
45
+ disabled={disabled || leftDisabled}
46
+ {size}
43
47
  selected={selected === "left"}
44
48
  on:click={() => {
45
49
  selected = "left"
@@ -67,7 +71,8 @@
67
71
  <ActionButton
68
72
  icon={rightIcon}
69
73
  quiet
70
- {disabled}
74
+ disabled={disabled || rightDisabled}
75
+ {size}
71
76
  selected={selected === "right"}
72
77
  on:click={() => {
73
78
  selected = "right"
@@ -101,6 +106,16 @@
101
106
  .left :global(*) {
102
107
  border-radius: 10px 0 0 10px;
103
108
  }
109
+ /* A radius sized for M crops the label on a shorter button */
110
+ .group.size-S {
111
+ border-radius: 8px;
112
+ }
113
+ .size-S .right :global(*) {
114
+ border-radius: 0 6px 6px 0;
115
+ }
116
+ .size-S .left :global(*) {
117
+ border-radius: 6px 0 0 6px;
118
+ }
104
119
  .wrapper {
105
120
  position: relative;
106
121
  }
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@ import "./bbui.css"
2
2
 
3
3
  // Constants
4
4
  export * from "./constants"
5
+ export { generateId } from "./utils/ids"
5
6
 
6
7
  // Form components
7
8
  export { default as Checkbox } from "./Form/Checkbox.svelte"
@@ -122,5 +123,4 @@ export { createNotificationStore, notifications } from "./Stores/notifications"
122
123
 
123
124
  // Helpers
124
125
  export * as Helpers from "./helpers"
125
-
126
126
  export type * from "./types"
@@ -0,0 +1,13 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { generateId } from "./ids"
3
+
4
+ const ID_PATTERN = /^[0-9a-zA-Z_-]+$/
5
+
6
+ describe("generateId", () => {
7
+ it("uses the configured length and character set", () => {
8
+ const ids = Array.from({ length: 100 }, generateId)
9
+
10
+ expect(ids.every(id => id.length === 9)).toBe(true)
11
+ expect(ids.every(id => ID_PATTERN.test(id))).toBe(true)
12
+ })
13
+ })
@@ -0,0 +1,5 @@
1
+ import { nanoid } from "nanoid"
2
+
3
+ const ID_LENGTH = 9
4
+
5
+ export const generateId = (): string => nanoid(ID_LENGTH)