@marimo-team/islands 0.23.16-dev76 → 0.23.16-dev8

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 (64) hide show
  1. package/dist/{ConnectedDataExplorerComponent-CJaUI-S-.js → ConnectedDataExplorerComponent-f9vLjQiF.js} +13 -13
  2. package/dist/{ErrorBoundary-DXlxsrTF.js → ErrorBoundary-DhLqwSWA.js} +3 -3
  3. package/dist/{any-language-editor-P9tjkj4n.js → any-language-editor-DifE-fl7.js} +5 -5
  4. package/dist/{button-BcSml68y.js → button-BSm8IXRU.js} +2 -4
  5. package/dist/{capabilities-BYLFYTYv.js → capabilities-C6OQ5YBO.js} +1 -1
  6. package/dist/{chat-ui-BdSDiVjk.js → chat-ui-JeekqoMH.js} +16 -16
  7. package/dist/{check-Cs-KV5XY.js → check-DP7U0H0G.js} +1 -1
  8. package/dist/{code-visibility-BQmXTtK5.js → code-visibility-z1wi6InA.js} +734 -711
  9. package/dist/{copy-Cs3yt6Vt.js → copy-DNq8q3zw.js} +2 -2
  10. package/dist/{dist-B96tMAst.js → dist-DIlxLFXG.js} +2 -2
  11. package/dist/{error-banner-DR7o3CbX.js → error-banner-B_GQfxxD.js} +2 -2
  12. package/dist/{esm-k8PIvuZW.js → esm-DzZVaNH4.js} +1 -1
  13. package/dist/{extends-Cq2JYHKy.js → extends-w2_S44bV.js} +4 -4
  14. package/dist/{formats-BFhkZJwd.js → formats-CO8cn5_K.js} +3 -3
  15. package/dist/{glide-data-editor--CaN-f27.js → glide-data-editor-B9K6-MJe.js} +9 -9
  16. package/dist/{html-to-image-DDENewl2.js → html-to-image-Bif4utQZ.js} +15 -15
  17. package/dist/{input-DStQIuqD.js → input-C5K4VBvB.js} +7 -7
  18. package/dist/{label-DIk0QoFe.js → label-DjoYwqmj.js} +2 -2
  19. package/dist/{loader-CTHCI-qe.js → loader-DX-IDS7n.js} +1 -1
  20. package/dist/main.js +1000 -1010
  21. package/dist/{mermaid-CdSvixW2.js → mermaid-hv6IckVw.js} +5 -5
  22. package/dist/{process-output-2QPcgBQg.js → process-output-TAzy-GUk.js} +3 -3
  23. package/dist/{reveal-component-B-AxUPCM.js → reveal-component-kTeHzMBu.js} +692 -618
  24. package/dist/{spec-CnaxwDqy.js → spec-CQKL_D3i.js} +4 -4
  25. package/dist/{strings-BYAhZnQi.js → strings-DIy9NAmg.js} +4 -4
  26. package/dist/style.css +1 -1
  27. package/dist/{swiper-component-DE94711u.js → swiper-component-CEdAYjb7.js} +2 -2
  28. package/dist/{toDate-B3IRfHhw.js → toDate-CmR_xR3P.js} +3 -3
  29. package/dist/{tooltip-B-qh7UPB.js → tooltip-f-qR0iUx.js} +3 -3
  30. package/dist/{types-DQl7jVYK.js → types-DicH9Oi0.js} +1 -1
  31. package/dist/{useAsyncData-CvO8WUy3.js → useAsyncData-CYOQb1EW.js} +1 -1
  32. package/dist/{useDateFormatter-BSoGim-G.js → useDateFormatter-Bt2qr_XS.js} +2 -2
  33. package/dist/{useDeepCompareMemoize-J3MKRdA4.js → useDeepCompareMemoize-CSHQeQEI.js} +1 -1
  34. package/dist/{useIframeCapabilities-POcFXHbw.js → useIframeCapabilities-kA09ykNq.js} +1 -1
  35. package/dist/{useLifecycle-DqPxHgAN.js → useLifecycle-CMJGqBaY.js} +3 -3
  36. package/dist/{useTheme-XMq9mNf_.js → useTheme-DYfby65G.js} +2 -2
  37. package/dist/{vega-component-CrFoZT1S.js → vega-component-DphdxaJ_.js} +10 -10
  38. package/dist/{zod-D0yOqoGe.js → zod-BUcyXSdR.js} +1 -1
  39. package/package.json +1 -1
  40. package/src/components/data-table/column-formatting/feature.ts +2 -1
  41. package/src/components/editor/file-tree/__tests__/file-render-mode.test.ts +27 -0
  42. package/src/components/editor/file-tree/__tests__/file-viewer.test.tsx +150 -0
  43. package/src/components/editor/file-tree/file-preview-metadata.tsx +51 -0
  44. package/src/components/editor/file-tree/file-render-mode.ts +32 -0
  45. package/src/components/editor/file-tree/file-viewer.tsx +76 -50
  46. package/src/components/editor/output/CalloutOutput.tsx +17 -3
  47. package/src/components/slides/__tests__/slide-scroll-hint.test.tsx +198 -0
  48. package/src/components/slides/reveal-component.tsx +3 -2
  49. package/src/components/slides/slide-scroll-hint.tsx +161 -0
  50. package/src/components/ui/input.tsx +1 -1
  51. package/src/core/codemirror/keymaps/__tests__/keymaps.test.ts +31 -2
  52. package/src/core/codemirror/keymaps/keymaps.ts +12 -0
  53. package/src/core/hotkeys/hotkeys.ts +0 -2
  54. package/src/core/i18n/__tests__/locale-provider.test.tsx +54 -118
  55. package/src/core/i18n/__tests__/locale.test.ts +113 -0
  56. package/src/core/i18n/locale-provider.tsx +1 -19
  57. package/src/core/i18n/locale.ts +71 -0
  58. package/src/core/network/types.ts +3 -1
  59. package/src/css/admonition.css +52 -71
  60. package/src/plugins/impl/FormPlugin.tsx +1 -1
  61. package/src/plugins/impl/__tests__/FormPlugin.test.tsx +162 -0
  62. package/src/plugins/layout/CalloutPlugin.tsx +8 -1
  63. package/src/stories/callout.stories.tsx +12 -0
  64. package/src/components/editor/output/CalloutOutput.styles.ts +0 -22
@@ -8,20 +8,9 @@ import {
8
8
  defaultUserConfig,
9
9
  parseUserConfig,
10
10
  } from "@/core/config/config-schema";
11
+ import { FALLBACK_LOCALE } from "../locale";
11
12
  import { LocaleProvider } from "../locale-provider";
12
13
 
13
- // Mock navigator.language with a getter
14
- let mockNavigatorLanguage: string | undefined;
15
-
16
- Object.defineProperty(window, "navigator", {
17
- value: {
18
- get language() {
19
- return mockNavigatorLanguage;
20
- },
21
- },
22
- writable: true,
23
- });
24
-
25
14
  // Mock react-aria-components I18nProvider
26
15
  vi.mock("react-aria-components", () => ({
27
16
  I18nProvider: ({
@@ -37,143 +26,90 @@ vi.mock("react-aria-components", () => ({
37
26
  ),
38
27
  }));
39
28
 
29
+ function renderWithLocale(configLocale: string | null | undefined) {
30
+ const store = createStore();
31
+ store.set(
32
+ userConfigAtom,
33
+ parseUserConfig({ display: { locale: configLocale } }),
34
+ );
35
+ return render(
36
+ <Provider store={store}>
37
+ <LocaleProvider>
38
+ <div>Test content</div>
39
+ </LocaleProvider>
40
+ </Provider>,
41
+ );
42
+ }
43
+
40
44
  describe("LocaleProvider", () => {
41
45
  beforeEach(() => {
42
- // Reset the mock before each test
43
- mockNavigatorLanguage = undefined;
46
+ vi.stubGlobal("navigator", { language: undefined as string | undefined });
44
47
  });
45
48
 
46
49
  afterEach(() => {
47
50
  cleanup();
48
- // Clear all mocks after each test
49
- mockNavigatorLanguage = undefined;
51
+ vi.unstubAllGlobals();
50
52
  vi.clearAllMocks();
51
53
  });
52
54
 
53
- it("should render I18nProvider without locale when locale is null", () => {
54
- const store = createStore();
55
- const config = parseUserConfig({ display: { locale: null } });
56
- store.set(userConfigAtom, config);
57
-
58
- const { getByTestId } = render(
59
- <Provider store={store}>
60
- <LocaleProvider>
61
- <div>Test content</div>
62
- </LocaleProvider>
63
- </Provider>,
64
- );
65
-
66
- const i18nProvider = getByTestId("i18n-provider");
67
- expect(i18nProvider).toBeInTheDocument();
68
- expect(i18nProvider.dataset.locale).toBe(undefined);
69
- expect(i18nProvider).toHaveTextContent("Test content");
55
+ it("passes a valid configured locale through unchanged", () => {
56
+ const { getByTestId } = renderWithLocale("es-ES");
57
+ expect(getByTestId("i18n-provider").dataset.locale).toBe("es-ES");
70
58
  });
71
59
 
72
- it("should render I18nProvider without locale when locale is undefined", () => {
73
- const store = createStore();
74
- const config = parseUserConfig({ display: { locale: undefined } });
75
- store.set(userConfigAtom, config);
76
-
77
- const { getByTestId } = render(
78
- <Provider store={store}>
79
- <LocaleProvider>
80
- <div>Test content</div>
81
- </LocaleProvider>
82
- </Provider>,
83
- );
84
-
85
- const i18nProvider = getByTestId("i18n-provider");
86
- expect(i18nProvider).toBeInTheDocument();
87
- expect(i18nProvider.dataset.locale).toBe(undefined);
88
- expect(i18nProvider).toHaveTextContent("Test content");
60
+ it("prefers the browser locale when config is unset", () => {
61
+ vi.stubGlobal("navigator", { language: "de-DE" });
62
+ const { getByTestId } = renderWithLocale(null);
63
+ expect(getByTestId("i18n-provider").dataset.locale).toBe("de-DE");
89
64
  });
90
65
 
91
- it("should render I18nProvider with locale when locale is provided", () => {
92
- const store = createStore();
93
- const testLocale = "es-ES";
94
- const config = parseUserConfig({ display: { locale: testLocale } });
95
- store.set(userConfigAtom, config);
96
-
97
- const { getByTestId } = render(
98
- <Provider store={store}>
99
- <LocaleProvider>
100
- <div>Test content</div>
101
- </LocaleProvider>
102
- </Provider>,
103
- );
66
+ it.each([null, undefined])(
67
+ "falls back to %s config + unusable browser locale",
68
+ (configLocale) => {
69
+ const { getByTestId } = renderWithLocale(configLocale);
70
+ expect(getByTestId("i18n-provider").dataset.locale).toBe(FALLBACK_LOCALE);
71
+ },
72
+ );
104
73
 
105
- const i18nProvider = getByTestId("i18n-provider");
106
- expect(i18nProvider).toBeInTheDocument();
107
- expect(i18nProvider.dataset.locale).toBe(testLocale);
108
- expect(i18nProvider).toHaveTextContent("Test content");
74
+ it("sanitizes a POSIX-tagged browser locale (issue #9938)", () => {
75
+ vi.stubGlobal("navigator", { language: "en-US@posix" });
76
+ const { getByTestId } = renderWithLocale(null);
77
+ expect(getByTestId("i18n-provider").dataset.locale).toBe("en-US");
109
78
  });
110
79
 
111
- it("should render I18nProvider with different locale values", () => {
112
- const testCases = ["en-US", "fr-FR", "de-DE", "ja-JP"];
113
-
114
- testCases.forEach((locale) => {
115
- const store = createStore();
116
- const config = parseUserConfig({ display: { locale } });
117
- store.set(userConfigAtom, config);
118
-
119
- const { getByTestId } = render(
120
- <Provider store={store}>
121
- <LocaleProvider>
122
- <div>Test content for {locale}</div>
123
- </LocaleProvider>
124
- </Provider>,
125
- );
126
-
127
- const i18nProvider = getByTestId("i18n-provider");
128
- expect(i18nProvider.dataset.locale).toBe(locale);
129
- expect(i18nProvider).toHaveTextContent(`Test content for ${locale}`);
130
-
131
- // Clean up after each iteration
132
- cleanup();
133
- });
80
+ it("falls back to the browser locale when the configured locale is unusable", () => {
81
+ vi.stubGlobal("navigator", { language: "de-DE" });
82
+ const { getByTestId } = renderWithLocale("@@@");
83
+ expect(getByTestId("i18n-provider").dataset.locale).toBe("de-DE");
134
84
  });
135
85
 
136
- it("should render children correctly", () => {
86
+ it("resolves a concrete locale for the default config", () => {
87
+ vi.stubGlobal("navigator", { language: "fr-FR" });
137
88
  const store = createStore();
138
- const config = parseUserConfig({ display: { locale: "en-US" } });
139
- store.set(userConfigAtom, config);
140
-
141
- const { getByText, getByRole } = render(
89
+ store.set(userConfigAtom, defaultUserConfig());
90
+ const { getByTestId } = render(
142
91
  <Provider store={store}>
143
92
  <LocaleProvider>
144
- <div>
145
- <h1>Test Heading</h1>
146
- <p>Test paragraph</p>
147
- <button type="button">Test Button</button>
148
- </div>
93
+ <div>Default config test</div>
149
94
  </LocaleProvider>
150
95
  </Provider>,
151
96
  );
152
-
153
- expect(getByText("Test Heading")).toBeInTheDocument();
154
- expect(getByText("Test paragraph")).toBeInTheDocument();
155
- expect(getByRole("button", { name: "Test Button" })).toBeInTheDocument();
97
+ expect(getByTestId("i18n-provider").dataset.locale).toBe("fr-FR");
156
98
  });
157
99
 
158
- it("should auto-detect locale when no locale is set in config", () => {
159
- mockNavigatorLanguage = "de-DE";
160
-
100
+ it("renders children", () => {
161
101
  const store = createStore();
162
- const config = defaultUserConfig();
163
- store.set(userConfigAtom, config);
164
-
165
- const { getByTestId } = render(
102
+ store.set(
103
+ userConfigAtom,
104
+ parseUserConfig({ display: { locale: "en-US" } }),
105
+ );
106
+ const { getByRole } = render(
166
107
  <Provider store={store}>
167
108
  <LocaleProvider>
168
- <div>Test content</div>
109
+ <button type="button">Test Button</button>
169
110
  </LocaleProvider>
170
111
  </Provider>,
171
112
  );
172
-
173
- const i18nProvider = getByTestId("i18n-provider");
174
- expect(i18nProvider).toBeInTheDocument();
175
- // When no locale is specified in config, it should use navigator.language
176
- expect(i18nProvider.dataset.locale).toBe("de-DE");
177
- expect(i18nProvider).toHaveTextContent("Test content");
113
+ expect(getByRole("button", { name: "Test Button" })).toBeInTheDocument();
178
114
  });
179
115
  });
@@ -0,0 +1,113 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { afterEach, describe, expect, it, vi } from "vitest";
4
+ import {
5
+ browserLocale,
6
+ FALLBACK_LOCALE,
7
+ isValidLocale,
8
+ normalizeLocale,
9
+ safeLocale,
10
+ } from "../locale";
11
+
12
+ describe("isValidLocale", () => {
13
+ it("accepts valid BCP 47 tags", () => {
14
+ expect(isValidLocale("en-US")).toBe(true);
15
+ expect(isValidLocale("de-DE")).toBe(true);
16
+ expect(isValidLocale("en")).toBe(true);
17
+ });
18
+
19
+ it("rejects empty and modifier-tainted tags", () => {
20
+ expect(isValidLocale("")).toBe(false);
21
+ expect(isValidLocale("en-US@posix")).toBe(false);
22
+ expect(isValidLocale("en_US.UTF-8")).toBe(false);
23
+ });
24
+ });
25
+
26
+ describe("normalizeLocale", () => {
27
+ it("keeps valid tags unchanged, including script + region", () => {
28
+ expect(normalizeLocale("de-DE")).toBe("de-DE");
29
+ expect(normalizeLocale("zh-Hant-TW")).toBe("zh-Hant-TW");
30
+ });
31
+
32
+ it("strips POSIX @modifiers (issue #9938)", () => {
33
+ expect(normalizeLocale("en-US@posix")).toBe("en-US");
34
+ expect(normalizeLocale("de-DE@euro")).toBe("de-DE");
35
+ // ICU-style unicode extension keywords are also dropped.
36
+ expect(normalizeLocale("de@collation=phonebook")).toBe("de");
37
+ });
38
+
39
+ it("strips charset suffixes and maps underscores", () => {
40
+ expect(normalizeLocale("en_US.UTF-8")).toBe("en-US");
41
+ expect(normalizeLocale("en_US")).toBe("en-US");
42
+ });
43
+
44
+ it("falls back to the language subtag when the full tag is invalid", () => {
45
+ // `-toolongsubtag` / `-oed` are structurally invalid, so `Intl.Locale`
46
+ // rejects the whole tag but accepts the leading `en`.
47
+ expect(normalizeLocale("en-toolongsubtag")).toBe("en");
48
+ expect(normalizeLocale("en-GB-oed")).toBe("en");
49
+ });
50
+
51
+ it("returns null for empty or unrecoverable tags", () => {
52
+ expect(normalizeLocale(null)).toBeNull();
53
+ expect(normalizeLocale(undefined)).toBeNull();
54
+ expect(normalizeLocale("")).toBeNull();
55
+ expect(normalizeLocale("@@@")).toBeNull();
56
+ // POSIX `C` locale: single-letter tag is not a valid language subtag.
57
+ expect(normalizeLocale("C")).toBeNull();
58
+ });
59
+ });
60
+
61
+ describe("browserLocale", () => {
62
+ afterEach(() => {
63
+ vi.unstubAllGlobals();
64
+ });
65
+
66
+ it("normalizes navigator.language", () => {
67
+ vi.stubGlobal("navigator", { language: "en-US@posix" });
68
+ expect(browserLocale()).toBe("en-US");
69
+ });
70
+
71
+ it("falls back when navigator.language is unusable", () => {
72
+ vi.stubGlobal("navigator", { language: "@@@" });
73
+ expect(browserLocale()).toBe(FALLBACK_LOCALE);
74
+ });
75
+
76
+ it("falls back when navigator is unavailable", () => {
77
+ vi.stubGlobal("navigator", undefined);
78
+ expect(browserLocale()).toBe(FALLBACK_LOCALE);
79
+ });
80
+ });
81
+
82
+ describe("safeLocale", () => {
83
+ afterEach(() => {
84
+ vi.unstubAllGlobals();
85
+ });
86
+
87
+ it("prefers a valid configured locale", () => {
88
+ vi.stubGlobal("navigator", { language: "de-DE" });
89
+ expect(safeLocale("fr-FR")).toBe("fr-FR");
90
+ });
91
+
92
+ it("normalizes configured locales before use", () => {
93
+ vi.stubGlobal("navigator", { language: "de-DE" });
94
+ expect(safeLocale("en_US")).toBe("en-US");
95
+ expect(safeLocale("en-US@posix")).toBe("en-US");
96
+ });
97
+
98
+ it("falls back to the browser locale when config is unusable", () => {
99
+ vi.stubGlobal("navigator", { language: "de-DE" });
100
+ expect(safeLocale(null)).toBe("de-DE");
101
+ expect(safeLocale("@@@")).toBe("de-DE");
102
+ });
103
+
104
+ it("normalizes the browser locale on the fallback path", () => {
105
+ vi.stubGlobal("navigator", { language: "en-US@posix" });
106
+ expect(safeLocale(null)).toBe("en-US");
107
+ });
108
+
109
+ it("falls back to FALLBACK_LOCALE when nothing is usable", () => {
110
+ vi.stubGlobal("navigator", { language: "@@@" });
111
+ expect(safeLocale(null)).toBe(FALLBACK_LOCALE);
112
+ });
113
+ });
@@ -4,6 +4,7 @@ import { useAtomValue } from "jotai";
4
4
  import type { ReactNode } from "react";
5
5
  import { I18nProvider } from "react-aria-components";
6
6
  import { localeAtom } from "@/core/config/config";
7
+ import { safeLocale } from "./locale";
7
8
 
8
9
  interface LocaleProviderProps {
9
10
  children: ReactNode;
@@ -14,22 +15,3 @@ export const LocaleProvider = ({ children }: LocaleProviderProps) => {
14
15
 
15
16
  return <I18nProvider locale={safeLocale(locale)}>{children}</I18nProvider>;
16
17
  };
17
-
18
- function safeLocale(locale: string | null | undefined) {
19
- if (!locale) {
20
- return navigator.language;
21
- }
22
- if (isValidLocale(locale)) {
23
- return locale;
24
- }
25
- return navigator.language;
26
- }
27
-
28
- function isValidLocale(locale: string) {
29
- try {
30
- new Intl.NumberFormat(locale);
31
- return true;
32
- } catch {
33
- return false;
34
- }
35
- }
@@ -0,0 +1,71 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ /**
4
+ * Locale used when neither the configured locale nor the browser locale can be
5
+ * resolved to a valid BCP 47 tag.
6
+ */
7
+ export const FALLBACK_LOCALE = "en-US";
8
+
9
+ /**
10
+ * Whether a tag is accepted by `Intl` (and therefore by react-aria's
11
+ * `I18nProvider`, which constructs `Intl.Locale` internally).
12
+ */
13
+ export function isValidLocale(locale: string): boolean {
14
+ if (!locale) {
15
+ return false;
16
+ }
17
+ try {
18
+ new Intl.Locale(locale);
19
+ return true;
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Coerce a browser or config locale tag into a valid
27
+ * [BCP 47](https://www.rfc-editor.org/info/rfc5646) tag, or `null` if nothing
28
+ * usable can be recovered.
29
+ *
30
+ * Some environments report POSIX-style tags that `Intl` rejects with
31
+ * `RangeError: Incorrect locale information provided`, crashing anything that
32
+ * constructs an `Intl.*` formatter (see issue #9938). Known offenders:
33
+ * - POSIX modifiers, e.g. Chromium under Playwright reporting `en-US@posix`
34
+ * - charset suffixes, e.g. `en_US.UTF-8`
35
+ * - underscore separators, e.g. `en_US`
36
+ */
37
+ export function normalizeLocale(tag: string | null | undefined): string | null {
38
+ if (!tag) {
39
+ return null;
40
+ }
41
+
42
+ const candidate = tag.split("@")[0].split(".")[0].trim().replaceAll("_", "-");
43
+
44
+ if (isValidLocale(candidate)) {
45
+ return candidate;
46
+ }
47
+
48
+ // Fall back to the language subtag alone (e.g. `en` from a mangled `en-XX`).
49
+ const language = candidate.split("-")[0];
50
+ if (isValidLocale(language)) {
51
+ return language;
52
+ }
53
+
54
+ return null;
55
+ }
56
+
57
+ /** Resolve the browser's locale to a safe BCP 47 tag.
58
+ * See https://github.com/marimo-team/marimo/issues/9938 */
59
+ export function browserLocale(): string {
60
+ const language =
61
+ typeof navigator === "undefined" ? undefined : navigator.language;
62
+ return normalizeLocale(language) ?? FALLBACK_LOCALE;
63
+ }
64
+
65
+ /**
66
+ * Resolve a configured locale to a safe BCP 47 tag, preferring the config and
67
+ * falling back to the browser locale, then to {@link FALLBACK_LOCALE}.
68
+ */
69
+ export function safeLocale(locale: string | null | undefined): string {
70
+ return normalizeLocale(locale) ?? browserLocale();
71
+ }
@@ -191,7 +191,9 @@ export interface EditRequests {
191
191
  request: FileMoveRequest,
192
192
  ) => Promise<FileMoveResponse>;
193
193
  sendUpdateFile: (request: FileUpdateRequest) => Promise<FileUpdateResponse>;
194
- sendFileDetails: (request: { path: string }) => Promise<FileDetailsResponse>;
194
+ sendFileDetails: (
195
+ request: FileDetailsRequest,
196
+ ) => Promise<FileDetailsResponse>;
195
197
  // Homepage requests
196
198
  openTutorial: (request: OpenTutorialRequest) => Promise<MarimoFile>;
197
199
  getRecentFiles: () => Promise<RecentFilesResponse>;
@@ -1,32 +1,53 @@
1
1
  @reference "../css/globals.css";
2
2
 
3
+ /*
4
+ * Flat, GitHub-style admonitions.
5
+ *
6
+ * Icons are lucide (https://lucide.dev/icons/) data URIs applied as masks so
7
+ * they render in currentColor.
8
+ */
9
+
3
10
  .admonition {
4
- --admonition-bg: var(--blue-2);
5
- --admonition-border: var(--blue-8);
6
11
  --admonition-heading-color: var(--blue-11);
12
+ --admonition-icon: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-info"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>');
7
13
 
8
14
  display: flex;
9
- position: relative;
10
15
  flex-direction: column;
11
- padding: 1rem;
12
- padding-left: 3rem;
13
- border-left: 4px solid;
16
+ padding: 0.5rem 1rem;
17
+ border-left: 0.25rem solid var(--admonition-heading-color);
14
18
  margin-bottom: 1rem;
15
- background-color: var(--admonition-bg);
16
- border-color: var(--admonition-border);
17
19
 
18
- &::before {
19
- position: absolute;
20
- margin-top: 0.35rem;
21
- margin-left: -2rem;
20
+ /* Tighten the gap between the title and body (md.css gives paragraphs a
21
+ 1rem block margin; flex children don't collapse margins). */
22
+ & > .paragraph,
23
+ & > p {
24
+ margin-block: 0.375rem;
25
+ }
26
+
27
+ /* The paragraph right after the title hugs it, matching callouts, whose
28
+ body is nested in a wrapper and gets the md.css :first-child treatment. */
29
+ & > .admonition-title + .paragraph,
30
+ & > .admonition-title + p {
31
+ margin-block-start: 0;
22
32
  }
23
33
  }
24
34
 
25
35
  .admonition-title {
26
- font-weight: bold;
36
+ display: block;
37
+ margin-bottom: 0.25rem;
38
+ font-weight: 600;
27
39
  color: var(--admonition-heading-color);
28
40
 
29
- @apply tracking-wide;
41
+ &::before {
42
+ content: "";
43
+ display: inline-block;
44
+ width: 1rem;
45
+ height: 1rem;
46
+ margin-right: 0.5rem;
47
+ vertical-align: -0.125rem;
48
+ background-color: currentcolor;
49
+ mask: var(--admonition-icon) no-repeat center / contain;
50
+ }
30
51
 
31
52
  > .paragraph {
32
53
  margin: 0;
@@ -38,90 +59,50 @@
38
59
  }
39
60
  }
40
61
 
41
- /* Icons from https://lucide.dev/icons/ */
42
-
43
62
  .admonition.info,
44
63
  .admonition.note {
45
- --admonition-bg: var(--blue-2);
46
- --admonition-border: var(--blue-8);
47
64
  --admonition-heading-color: var(--blue-11);
48
-
49
- &::before {
50
- content: light-dark(
51
- url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" opacity="60%" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="admonition-icon lucide lucide-info"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>'),
52
- url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" opacity="60%" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="admonition-icon lucide lucide-info"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>')
53
- );
54
- }
65
+ --admonition-icon: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-info"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>');
55
66
  }
56
67
 
57
68
  .admonition.danger,
58
69
  .admonition.caution,
59
70
  .admonition.error {
60
- --admonition-bg: var(--red-2);
61
- --admonition-border: var(--red-8);
62
71
  --admonition-heading-color: var(--red-11);
63
-
64
- &::before {
65
- content: light-dark(
66
- url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" opacity="60%" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-octagon-alert"><polygon points="7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2"/><line x1="12" x2="12" y1="8" y2="12"/><line x1="12" x2="12.01" y1="16" y2="16"/></svg>'),
67
- url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" opacity="60%" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-octagon-alert"><polygon points="7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2"/><line x1="12" x2="12" y1="8" y2="12"/><line x1="12" x2="12.01" y1="16" y2="16"/></svg>')
68
- );
69
- }
72
+ --admonition-icon: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-octagon-alert"><polygon points="7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2"/><line x1="12" x2="12" y1="8" y2="12"/><line x1="12" x2="12.01" y1="16" y2="16"/></svg>');
70
73
  }
71
74
 
72
75
  .admonition.attention,
73
76
  .admonition.warning {
74
- --admonition-bg: var(--yellow-2);
75
- --admonition-border: var(--yellow-8);
76
77
  --admonition-heading-color: var(--yellow-11);
77
-
78
- &::before {
79
- content: light-dark(
80
- url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" opacity="60%" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-triangle-alert"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>'),
81
- url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" opacity="60%" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-triangle-alert"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>')
82
- );
83
- }
78
+ --admonition-icon: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-triangle-alert"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>');
84
79
  }
85
80
 
86
81
  .admonition.hint {
87
- --admonition-bg: var(--gray-2);
88
- --admonition-border: var(--gray-8);
89
82
  --admonition-heading-color: var(--gray-11);
90
-
91
- .dark & {
92
- --admonition-bg: var(--gray-3);
93
- }
94
-
95
- &::before {
96
- content: light-dark(
97
- url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" opacity="60%" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-info"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>'),
98
- url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" opacity="60%" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-info"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>')
99
- );
100
- }
83
+ --admonition-icon: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-info"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>');
101
84
  }
102
85
 
103
86
  .admonition.important {
104
- --admonition-bg: var(--cyan-2);
105
- --admonition-border: var(--cyan-8);
106
87
  --admonition-heading-color: var(--cyan-11);
88
+ --admonition-icon: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-square-warning"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/><path d="M12 7v2"/><path d="M12 13h.01"/></svg>');
89
+ }
107
90
 
108
- &::before {
109
- content: light-dark(
110
- url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" opacity="60%" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-square-warning"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/><path d="M12 7v2"/><path d="M12 13h.01"/></svg>'),
111
- url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" opacity="60%" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-square-warning"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/><path d="M12 7v2"/><path d="M12 13h.01"/></svg>')
112
- );
113
- }
91
+ .admonition.success {
92
+ --admonition-heading-color: var(--grass-11);
93
+ --admonition-icon: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-check"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>');
114
94
  }
115
95
 
116
96
  .admonition.tip {
117
- --admonition-bg: var(--grass-2);
118
- --admonition-border: var(--grass-8);
119
97
  --admonition-heading-color: var(--grass-11);
98
+ --admonition-icon: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-lightbulb"><path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/></svg>');
99
+ }
120
100
 
121
- &::before {
122
- content: light-dark(
123
- url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" opacity="60%" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-lightbulb"><path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/></svg>'),
124
- url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" opacity="60%" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-lightbulb"><path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/></svg>')
125
- );
101
+ /* Quiet, icon-less variant used by neutral callouts. */
102
+ .admonition.neutral {
103
+ --admonition-heading-color: var(--gray-11);
104
+
105
+ .admonition-title::before {
106
+ content: none;
126
107
  }
127
108
  }
@@ -148,7 +148,7 @@ export const FormWrapper = <T,>({
148
148
  if (evt.key === "Enter" && (evt.ctrlKey || evt.metaKey)) {
149
149
  evt.preventDefault();
150
150
  evt.stopPropagation();
151
- setValue(newValue);
151
+ formDiv.current?.requestSubmit();
152
152
  }
153
153
  }}
154
154
  >