@twentyfirstmedia/ui-core 0.2.1 → 0.3.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 (146) hide show
  1. package/dist/components/admin.d.ts.map +1 -1
  2. package/dist/components/admin.js +95 -71
  3. package/dist/components/admin.js.map +1 -1
  4. package/dist/components/agents.d.ts.map +1 -1
  5. package/dist/components/agents.js +49 -35
  6. package/dist/components/agents.js.map +1 -1
  7. package/dist/components/app-chrome.d.ts.map +1 -1
  8. package/dist/components/app-chrome.js +6 -2
  9. package/dist/components/app-chrome.js.map +1 -1
  10. package/dist/components/approvals.d.ts.map +1 -1
  11. package/dist/components/approvals.js +22 -18
  12. package/dist/components/approvals.js.map +1 -1
  13. package/dist/components/building-blocks.d.ts.map +1 -1
  14. package/dist/components/building-blocks.js +6 -2
  15. package/dist/components/building-blocks.js.map +1 -1
  16. package/dist/components/charts.d.ts.map +1 -1
  17. package/dist/components/charts.js +20 -10
  18. package/dist/components/charts.js.map +1 -1
  19. package/dist/components/chat.d.ts.map +1 -1
  20. package/dist/components/chat.js +31 -19
  21. package/dist/components/chat.js.map +1 -1
  22. package/dist/components/commerce.d.ts +2 -2
  23. package/dist/components/commerce.d.ts.map +1 -1
  24. package/dist/components/commerce.js +45 -31
  25. package/dist/components/commerce.js.map +1 -1
  26. package/dist/components/data-display.d.ts +5 -9
  27. package/dist/components/data-display.d.ts.map +1 -1
  28. package/dist/components/data-display.js +34 -35
  29. package/dist/components/data-display.js.map +1 -1
  30. package/dist/components/data-table.d.ts.map +1 -1
  31. package/dist/components/data-table.js +15 -10
  32. package/dist/components/data-table.js.map +1 -1
  33. package/dist/components/developer.d.ts.map +1 -1
  34. package/dist/components/developer.js +20 -11
  35. package/dist/components/developer.js.map +1 -1
  36. package/dist/components/feedback.d.ts.map +1 -1
  37. package/dist/components/feedback.js +12 -6
  38. package/dist/components/feedback.js.map +1 -1
  39. package/dist/components/form.d.ts +2 -1
  40. package/dist/components/form.d.ts.map +1 -1
  41. package/dist/components/form.js +12 -9
  42. package/dist/components/form.js.map +1 -1
  43. package/dist/components/forms.d.ts +2 -1
  44. package/dist/components/forms.d.ts.map +1 -1
  45. package/dist/components/forms.js +92 -42
  46. package/dist/components/forms.js.map +1 -1
  47. package/dist/components/foundation.d.ts +5 -2
  48. package/dist/components/foundation.d.ts.map +1 -1
  49. package/dist/components/foundation.js +15 -12
  50. package/dist/components/foundation.js.map +1 -1
  51. package/dist/components/inputs.d.ts.map +1 -1
  52. package/dist/components/inputs.js +3 -1
  53. package/dist/components/inputs.js.map +1 -1
  54. package/dist/components/mail.d.ts.map +1 -1
  55. package/dist/components/mail.js +15 -10
  56. package/dist/components/mail.js.map +1 -1
  57. package/dist/components/media.d.ts.map +1 -1
  58. package/dist/components/media.js +39 -18
  59. package/dist/components/media.js.map +1 -1
  60. package/dist/components/navigation.d.ts.map +1 -1
  61. package/dist/components/navigation.js +21 -9
  62. package/dist/components/navigation.js.map +1 -1
  63. package/dist/components/operations.d.ts.map +1 -1
  64. package/dist/components/operations.js +37 -27
  65. package/dist/components/operations.js.map +1 -1
  66. package/dist/components/previews.d.ts.map +1 -1
  67. package/dist/components/previews.js +14 -10
  68. package/dist/components/previews.js.map +1 -1
  69. package/dist/components/primitives.d.ts.map +1 -1
  70. package/dist/components/primitives.js +4 -1
  71. package/dist/components/primitives.js.map +1 -1
  72. package/dist/components/real-estate.d.ts.map +1 -1
  73. package/dist/components/real-estate.js +43 -28
  74. package/dist/components/real-estate.js.map +1 -1
  75. package/dist/components/rich-text-editor.d.ts.map +1 -1
  76. package/dist/components/rich-text-editor.js +18 -15
  77. package/dist/components/rich-text-editor.js.map +1 -1
  78. package/dist/components/select-menu.d.ts.map +1 -1
  79. package/dist/components/select-menu.js +4 -1
  80. package/dist/components/select-menu.js.map +1 -1
  81. package/dist/components/shell.d.ts.map +1 -1
  82. package/dist/components/shell.js +8 -3
  83. package/dist/components/shell.js.map +1 -1
  84. package/dist/components/site.d.ts.map +1 -1
  85. package/dist/components/site.js +17 -13
  86. package/dist/components/site.js.map +1 -1
  87. package/dist/components/types.d.ts +102 -3
  88. package/dist/components/types.d.ts.map +1 -1
  89. package/dist/components/workspace.d.ts.map +1 -1
  90. package/dist/components/workspace.js +50 -34
  91. package/dist/components/workspace.js.map +1 -1
  92. package/dist/context.d.ts +39 -0
  93. package/dist/context.d.ts.map +1 -0
  94. package/dist/context.js +76 -0
  95. package/dist/context.js.map +1 -0
  96. package/dist/format.d.ts +33 -6
  97. package/dist/format.d.ts.map +1 -1
  98. package/dist/format.js +75 -10
  99. package/dist/format.js.map +1 -1
  100. package/dist/index.d.ts +3 -2
  101. package/dist/index.d.ts.map +1 -1
  102. package/dist/index.js +3 -2
  103. package/dist/index.js.map +1 -1
  104. package/dist/provider.d.ts +18 -11
  105. package/dist/provider.d.ts.map +1 -1
  106. package/dist/provider.js +11 -18
  107. package/dist/provider.js.map +1 -1
  108. package/dist/strings.d.ts +503 -0
  109. package/dist/strings.d.ts.map +1 -0
  110. package/dist/strings.js +537 -0
  111. package/dist/strings.js.map +1 -0
  112. package/package.json +1 -1
  113. package/src/components/admin.tsx +117 -93
  114. package/src/components/agents.tsx +55 -41
  115. package/src/components/app-chrome.tsx +6 -2
  116. package/src/components/approvals.tsx +26 -22
  117. package/src/components/building-blocks.tsx +6 -2
  118. package/src/components/charts.tsx +21 -11
  119. package/src/components/chat.tsx +45 -35
  120. package/src/components/commerce.tsx +61 -53
  121. package/src/components/data-display.tsx +38 -45
  122. package/src/components/data-table.tsx +20 -15
  123. package/src/components/developer.tsx +26 -15
  124. package/src/components/feedback.tsx +19 -7
  125. package/src/components/form.tsx +17 -9
  126. package/src/components/forms.tsx +131 -53
  127. package/src/components/foundation.tsx +23 -36
  128. package/src/components/inputs.tsx +4 -2
  129. package/src/components/mail.tsx +16 -11
  130. package/src/components/media.tsx +64 -21
  131. package/src/components/navigation.tsx +25 -13
  132. package/src/components/operations.tsx +40 -32
  133. package/src/components/previews.tsx +17 -15
  134. package/src/components/primitives.tsx +8 -5
  135. package/src/components/real-estate.tsx +55 -35
  136. package/src/components/rich-text-editor.tsx +20 -17
  137. package/src/components/select-menu.tsx +5 -2
  138. package/src/components/shell.tsx +8 -3
  139. package/src/components/site.tsx +18 -14
  140. package/src/components/types.ts +111 -3
  141. package/src/components/workspace.tsx +79 -60
  142. package/src/context.ts +112 -0
  143. package/src/format.ts +108 -15
  144. package/src/index.ts +43 -0
  145. package/src/provider.tsx +36 -40
  146. package/src/strings.ts +1500 -0
package/src/format.ts CHANGED
@@ -1,15 +1,16 @@
1
1
  /**
2
- * Numbers, money, percentages and dates, formatted with Intl in English (en-US). Money defaults
3
- * to euros; calendars start on Monday.
2
+ * Numbers, money, percentages and dates, formatted with Intl. English (en-US) by default; every
3
+ * helper takes an optional BCP 47 locale as its last argument, and components use the locale of
4
+ * the nearest `UIProvider` (see `useFormat`). Money defaults to euros; calendars start on Monday.
4
5
  */
5
6
  export const LOCALE = "en-US";
6
7
  export const CURRENCY = "EUR";
7
8
  /** First day of the week in calendars: 0 Sunday … 6 Saturday. */
8
9
  export const WEEK_START = 1;
9
10
 
10
- export function formatNumber(v: number, o?: Intl.NumberFormatOptions): string {
11
+ export function formatNumber(v: number, o?: Intl.NumberFormatOptions, locale?: string): string {
11
12
  try {
12
- return new Intl.NumberFormat(LOCALE, o).format(v);
13
+ return new Intl.NumberFormat(locale || LOCALE, o).format(v);
13
14
  } catch {
14
15
  return String(v);
15
16
  }
@@ -19,29 +20,121 @@ export function formatMoney(
19
20
  v: number,
20
21
  currency?: string | null,
21
22
  o?: Intl.NumberFormatOptions,
23
+ locale?: string,
22
24
  ): string {
23
- return formatNumber(v, {
24
- style: "currency",
25
- currency: currency || CURRENCY,
26
- minimumFractionDigits: v % 1 ? 2 : 0,
27
- maximumFractionDigits: 2,
28
- ...o,
29
- });
25
+ return formatNumber(
26
+ v,
27
+ {
28
+ style: "currency",
29
+ currency: currency || CURRENCY,
30
+ minimumFractionDigits: v % 1 ? 2 : 0,
31
+ maximumFractionDigits: 2,
32
+ ...o,
33
+ },
34
+ locale,
35
+ );
30
36
  }
31
37
 
32
38
  /** A share in percent: `19` → "19%". */
33
- export function formatPercent(v: number, o?: Intl.NumberFormatOptions): string {
34
- return formatNumber(v / 100, { style: "percent", maximumFractionDigits: 0, ...o });
39
+ export function formatPercent(v: number, o?: Intl.NumberFormatOptions, locale?: string): string {
40
+ return formatNumber(v / 100, { style: "percent", maximumFractionDigits: 0, ...o }, locale);
35
41
  }
36
42
 
37
- export function formatDate(input: Date | string | number, o?: Intl.DateTimeFormatOptions): string {
43
+ export function formatDate(
44
+ input: Date | string | number,
45
+ o?: Intl.DateTimeFormatOptions,
46
+ locale?: string,
47
+ ): string {
38
48
  const d = input instanceof Date ? input : new Date(input);
39
49
  try {
40
50
  return new Intl.DateTimeFormat(
41
- LOCALE,
51
+ locale || LOCALE,
42
52
  o || { day: "2-digit", month: "2-digit", year: "numeric" },
43
53
  ).format(d);
44
54
  } catch {
45
55
  return d.toDateString();
46
56
  }
47
57
  }
58
+
59
+ /** The group and decimal separators of a locale: `en-US` → `{ group: ",", decimal: "." }`. */
60
+ export function numberSeparators(locale?: string): { group: string; decimal: string } {
61
+ let group = ",",
62
+ decimal = ".";
63
+ try {
64
+ for (const part of new Intl.NumberFormat(locale || LOCALE).formatToParts(12345.6)) {
65
+ if (part.type === "group") group = part.value;
66
+ else if (part.type === "decimal") decimal = part.value;
67
+ }
68
+ } catch {
69
+ // keep the English separators
70
+ }
71
+ return { group, decimal };
72
+ }
73
+
74
+ const escape = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
75
+
76
+ /** A number inside a formatted string, as `parseFormatted` finds it. */
77
+ export interface FormattedNumber {
78
+ /** Text before the number ("€", "+"). */
79
+ prefix: string;
80
+ value: number;
81
+ /** Digits after the decimal separator. */
82
+ decimals: number;
83
+ /** Whether the number used group separators ("1,305"). */
84
+ grouped: boolean;
85
+ /** Text after the number (" €/m²", "%"). */
86
+ suffix: string;
87
+ }
88
+
89
+ /**
90
+ * Finds the first number in a string formatted for `locale` and returns it with the text around
91
+ * it: `parseFormatted("13,05 €/m²", "de-DE")` → `{ prefix: "", value: 13.05, decimals: 2, … }`.
92
+ * Group separators are only taken between digits; a space or no-break space counts as a group
93
+ * separator for locales that group with one.
94
+ */
95
+ export function parseFormatted(input: string | number, locale?: string): FormattedNumber | null {
96
+ if (typeof input === "number") {
97
+ if (!Number.isFinite(input)) return null;
98
+ const s = String(input);
99
+ if (/e/i.test(s)) return null;
100
+ return {
101
+ prefix: "",
102
+ value: input,
103
+ decimals: (s.split(".")[1] || "").length,
104
+ grouped: false,
105
+ suffix: "",
106
+ };
107
+ }
108
+ const { group, decimal } = numberSeparators(locale);
109
+ // narrow and regular no-break spaces both appear as group separators (fr, de-CH …)
110
+ const groups = /^\s$/.test(group) ? "[\\s\\u00a0\\u202f]" : escape(group);
111
+ const re = new RegExp(
112
+ `^([^0-9-]*)(-?[0-9](?:[0-9]|${groups}(?=[0-9]))*)(?:${escape(decimal)}([0-9]+))?(.*)$`,
113
+ "s",
114
+ );
115
+ const m = re.exec(input);
116
+ if (!m) return null;
117
+ const int = (m[2] as string).replace(new RegExp(groups, "g"), "");
118
+ const frac = m[3] ?? "";
119
+ return {
120
+ prefix: m[1] ?? "",
121
+ value: parseFloat(frac ? `${int}.${frac}` : int),
122
+ decimals: frac.length,
123
+ grouped: int.length !== (m[2] as string).length,
124
+ suffix: m[4] ?? "",
125
+ };
126
+ }
127
+
128
+ /** Writes a number back in the shape `parseFormatted` found it in (same decimals, grouping, text). */
129
+ export function formatLike(v: number, shape: FormattedNumber, locale?: string): string {
130
+ const s = formatNumber(
131
+ v,
132
+ {
133
+ minimumFractionDigits: shape.decimals,
134
+ maximumFractionDigits: shape.decimals,
135
+ useGrouping: shape.grouped,
136
+ },
137
+ locale,
138
+ );
139
+ return shape.prefix + s + shape.suffix;
140
+ }
package/src/index.ts CHANGED
@@ -4,13 +4,56 @@ export {
4
4
  formatPercent,
5
5
  formatMoney,
6
6
  formatDate,
7
+ formatLike,
8
+ parseFormatted,
9
+ numberSeparators,
7
10
  LOCALE,
8
11
  CURRENCY,
9
12
  WEEK_START,
13
+ type FormattedNumber,
10
14
  } from "./format.js";
15
+ export {
16
+ DEFAULT_STRINGS,
17
+ SHARED_STRINGS,
18
+ ADMIN_STRINGS,
19
+ AGENTS_STRINGS,
20
+ APP_CHROME_STRINGS,
21
+ APPROVALS_STRINGS,
22
+ BUILDING_BLOCKS_STRINGS,
23
+ CHARTS_STRINGS,
24
+ CHAT_STRINGS,
25
+ COMMERCE_STRINGS,
26
+ DATA_DISPLAY_STRINGS,
27
+ DATA_TABLE_STRINGS,
28
+ DEVELOPER_STRINGS,
29
+ FEEDBACK_STRINGS,
30
+ FORM_STRINGS,
31
+ FORMS_STRINGS,
32
+ MAIL_STRINGS,
33
+ MEDIA_STRINGS,
34
+ NAVIGATION_STRINGS,
35
+ OPERATIONS_STRINGS,
36
+ PREVIEWS_STRINGS,
37
+ PRIMITIVES_STRINGS,
38
+ REAL_ESTATE_STRINGS,
39
+ RICH_TEXT_EDITOR_STRINGS,
40
+ SELECT_MENU_STRINGS,
41
+ SHELL_STRINGS,
42
+ SITE_STRINGS,
43
+ WORKSPACE_STRINGS,
44
+ mergeStrings,
45
+ mergeMessages,
46
+ type UIStrings,
47
+ type UIMessages,
48
+ } from "./strings.js";
11
49
  export {
12
50
  UIProvider,
13
51
  useUI,
52
+ useUIStrings,
53
+ useStrings,
54
+ useFormat,
55
+ formattersFor,
56
+ type Formatters,
14
57
  type UIProviderProps,
15
58
  type UIContextValue,
16
59
  type Kit,
package/src/provider.tsx CHANGED
@@ -1,15 +1,18 @@
1
1
  "use client";
2
- import {
3
- createContext,
4
- useContext,
5
- useEffect,
6
- useMemo,
7
- useRef,
8
- useState,
9
- type CSSProperties,
10
- type ReactNode,
11
- } from "react";
2
+ import { useEffect, useMemo, useRef, useState, type CSSProperties, type ReactNode } from "react";
3
+ import { UIContext, useUI, type UIContextValue } from "./context.js";
4
+ import { mergeMessages, type UIMessages } from "./strings.js";
12
5
  import { themeStyle, type ThemeInput, type ThemeValues } from "./theme.js";
6
+
7
+ export {
8
+ useUI,
9
+ useUIStrings,
10
+ useStrings,
11
+ useFormat,
12
+ formattersFor,
13
+ type UIContextValue,
14
+ type Formatters,
15
+ } from "./context.js";
13
16
  export type Kit = "halo" | "pro";
14
17
  export type Theme = "light" | "dark" | "system";
15
18
  export type Density = "compact" | "comfortable";
@@ -53,39 +56,23 @@ export interface UIProviderProps {
53
56
  * reduced-motion setting always turns motion off.
54
57
  */
55
58
  motion?: Motion;
59
+ /**
60
+ * BCP 47 locale for the numbers, money, percentages and dates components show (`"de-DE"`).
61
+ * Default `"en-US"`; a nested provider keeps its parent's locale unless it sets its own.
62
+ */
63
+ locale?: string;
64
+ /**
65
+ * Your own text for any built-in string: accessible names, button texts, placeholders, status
66
+ * words (`{ close: "Schließen", selectedCount: (n) => \`${n} ausgewählt\` }`). Partial: what you
67
+ * leave out stays English (or what an outer provider set). Props such as `title` or `label` on a
68
+ * component still win. See `DEFAULT_STRINGS` for every key.
69
+ */
70
+ messages?: UIMessages;
56
71
  className?: string;
57
72
  style?: CSSProperties;
58
73
  children?: ReactNode;
59
74
  }
60
75
 
61
- export interface UIContextValue {
62
- kit: Kit | undefined;
63
- theme: Theme | undefined;
64
- density: Density | undefined;
65
- surface: Surface | undefined;
66
- motion: Motion | undefined;
67
- /** Element portals render into (null: document.body). */
68
- portalRoot: HTMLElement | null;
69
- }
70
-
71
- const UIContext = createContext<UIContextValue>({
72
- kit: undefined,
73
- theme: undefined,
74
- density: undefined,
75
- surface: undefined,
76
- motion: undefined,
77
- portalRoot: null,
78
- });
79
-
80
- /** Current kit, theme, density and portal target. */
81
- export function useUI(): UIContextValue {
82
- return useContext(UIContext);
83
- }
84
-
85
- /**
86
- * Sets kit, theme and density for everything inside (they only change attributes, so switching
87
- * never re-mounts the subtree).
88
- */
89
76
  /** True while the colour scheme the page shows is dark (theme prop, kit default, system). */
90
77
  function useDarkMode(theme: Theme | undefined, kit: Kit | undefined, enabled: boolean): boolean {
91
78
  const [systemDark, setSystemDark] = useState(false);
@@ -106,6 +93,10 @@ function useDarkMode(theme: Theme | undefined, kit: Kit | undefined, enabled: bo
106
93
  const isThemeInput = (t: ThemeValues | ThemeInput): t is ThemeInput =>
107
94
  "light" in t || "dark" in t || "radius" in t;
108
95
 
96
+ /**
97
+ * Sets kit, theme, density, locale and strings for everything inside (switching never re-mounts
98
+ * the subtree: kit, theme and density only change attributes, locale and strings re-render).
99
+ */
109
100
  export function UIProvider(p: UIProviderProps) {
110
101
  const parent = useUI();
111
102
  const scope = p.scope ?? "document";
@@ -115,6 +106,11 @@ export function UIProvider(p: UIProviderProps) {
115
106
  const density = p.density ?? parent.density;
116
107
  const surface = p.surface ?? parent.surface;
117
108
  const motion = p.motion ?? parent.motion;
109
+ const locale = p.locale || parent.locale;
110
+ const messages = useMemo(
111
+ () => (p.messages ? mergeMessages(parent.messages, p.messages) : parent.messages),
112
+ [parent.messages, p.messages],
113
+ );
118
114
  const perMode = !!p.tokens && isThemeInput(p.tokens);
119
115
  const dark = useDarkMode(theme, kit, perMode);
120
116
  const vars = useMemo(() => {
@@ -155,8 +151,8 @@ export function UIProvider(p: UIProviderProps) {
155
151
 
156
152
  const [portalRoot, setPortalRoot] = useState<HTMLElement | null>(null);
157
153
  const ctx = useMemo<UIContextValue>(
158
- () => ({ kit, theme, density, surface, motion, portalRoot }),
159
- [kit, theme, density, surface, motion, portalRoot],
154
+ () => ({ kit, theme, density, surface, motion, locale, messages, portalRoot }),
155
+ [kit, theme, density, surface, motion, locale, messages, portalRoot],
160
156
  );
161
157
  // Derived colours (glass, hover shades, Pro's scale) are worked out where data-theme sits. A
162
158
  // subtree with its own tokens needs data-theme too, or they would keep the outer values.