@adecore/ui 0.14.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/LICENSE +21 -0
- package/README.md +83 -0
- package/dist/AccentSwatches.d.ts +15 -0
- package/dist/AccentSwatches.js +21 -0
- package/dist/Banner.d.ts +12 -0
- package/dist/Banner.js +18 -0
- package/dist/Button.d.ts +9 -0
- package/dist/Button.js +28 -0
- package/dist/ButtonGroup.d.ts +3 -0
- package/dist/ButtonGroup.js +11 -0
- package/dist/Checkbox.d.ts +11 -0
- package/dist/Checkbox.js +9 -0
- package/dist/ChoiceCards.d.ts +23 -0
- package/dist/ChoiceCards.js +21 -0
- package/dist/CloseButton.d.ts +15 -0
- package/dist/CloseButton.js +11 -0
- package/dist/ColorSwatch.d.ts +14 -0
- package/dist/ColorSwatch.js +28 -0
- package/dist/ColumnResizeHandle.d.ts +9 -0
- package/dist/ColumnResizeHandle.js +12 -0
- package/dist/DisabledReason.d.ts +6 -0
- package/dist/DisabledReason.js +8 -0
- package/dist/DockShell.d.ts +9 -0
- package/dist/DockShell.js +49 -0
- package/dist/EmptyState.d.ts +16 -0
- package/dist/EmptyState.js +11 -0
- package/dist/ErrorBoundary.d.ts +24 -0
- package/dist/ErrorBoundary.js +47 -0
- package/dist/Field.d.ts +17 -0
- package/dist/Field.js +48 -0
- package/dist/FileIcon.d.ts +8 -0
- package/dist/FileIcon.js +15 -0
- package/dist/Icon.d.ts +9 -0
- package/dist/Icon.js +12 -0
- package/dist/IconButton.d.ts +23 -0
- package/dist/IconButton.js +25 -0
- package/dist/IconPicker.d.ts +17 -0
- package/dist/IconPicker.js +93 -0
- package/dist/Input.d.ts +14 -0
- package/dist/Input.js +25 -0
- package/dist/Kbd.d.ts +23 -0
- package/dist/Kbd.js +20 -0
- package/dist/ListRow.d.ts +10 -0
- package/dist/ListRow.js +20 -0
- package/dist/Meter.d.ts +12 -0
- package/dist/Meter.js +7 -0
- package/dist/PanelEmpty.d.ts +16 -0
- package/dist/PanelEmpty.js +7 -0
- package/dist/PanelHeader.d.ts +6 -0
- package/dist/PanelHeader.js +20 -0
- package/dist/Pill.d.ts +27 -0
- package/dist/Pill.js +25 -0
- package/dist/ProjectSwitcher.d.ts +30 -0
- package/dist/ProjectSwitcher.js +18 -0
- package/dist/PromptDialog.d.ts +40 -0
- package/dist/PromptDialog.js +47 -0
- package/dist/SectionLabel.d.ts +3 -0
- package/dist/SectionLabel.js +16 -0
- package/dist/SegmentBar.d.ts +18 -0
- package/dist/SegmentBar.js +44 -0
- package/dist/Segmented.d.ts +17 -0
- package/dist/Segmented.js +7 -0
- package/dist/Select.d.ts +27 -0
- package/dist/Select.js +34 -0
- package/dist/Separator.d.ts +7 -0
- package/dist/Separator.js +8 -0
- package/dist/ShortcutHints.d.ts +1 -0
- package/dist/ShortcutHints.js +94 -0
- package/dist/Skeleton.d.ts +6 -0
- package/dist/Skeleton.js +6 -0
- package/dist/SlidingColumn.d.ts +19 -0
- package/dist/SlidingColumn.js +41 -0
- package/dist/Spinner.d.ts +6 -0
- package/dist/Spinner.js +7 -0
- package/dist/Stepper.d.ts +14 -0
- package/dist/Stepper.js +18 -0
- package/dist/Surface.d.ts +3 -0
- package/dist/Surface.js +16 -0
- package/dist/Switch.d.ts +10 -0
- package/dist/Switch.js +7 -0
- package/dist/TextMenu.d.ts +5 -0
- package/dist/TextMenu.js +31 -0
- package/dist/Tile.d.ts +11 -0
- package/dist/Tile.js +10 -0
- package/dist/Toasts.d.ts +9 -0
- package/dist/Toasts.js +58 -0
- package/dist/Tooltip.d.ts +16 -0
- package/dist/Tooltip.js +21 -0
- package/dist/UIProvider.d.ts +9 -0
- package/dist/UIProvider.js +26 -0
- package/dist/Waveform.d.ts +16 -0
- package/dist/Waveform.js +123 -0
- package/dist/Wipe.d.ts +12 -0
- package/dist/Wipe.js +47 -0
- package/dist/ZoomControls.d.ts +30 -0
- package/dist/ZoomControls.js +16 -0
- package/dist/class-name.d.ts +2 -0
- package/dist/class-name.js +3 -0
- package/dist/clipboard.d.ts +2 -0
- package/dist/clipboard.js +16 -0
- package/dist/context-menu/index.parts.d.ts +1 -0
- package/dist/context-menu/index.parts.js +2 -0
- package/dist/dialog/index.parts.d.ts +1 -0
- package/dist/dialog/index.parts.js +1 -0
- package/dist/dialog/parts.d.ts +36 -0
- package/dist/dialog/parts.js +55 -0
- package/dist/dialog-layer.d.ts +1 -0
- package/dist/dialog-layer.js +27 -0
- package/dist/error-boundary.d.ts +4 -0
- package/dist/error-boundary.js +34 -0
- package/dist/error-message.d.ts +1 -0
- package/dist/error-message.js +4 -0
- package/dist/field-context.d.ts +7 -0
- package/dist/field-context.js +4 -0
- package/dist/file-icon.d.ts +4 -0
- package/dist/file-icon.js +46 -0
- package/dist/floating.d.ts +5 -0
- package/dist/floating.js +6 -0
- package/dist/format/datetime.d.ts +11 -0
- package/dist/format/datetime.js +50 -0
- package/dist/format/duration.d.ts +5 -0
- package/dist/format/duration.js +76 -0
- package/dist/format/index.d.ts +6 -0
- package/dist/format/index.js +7 -0
- package/dist/format/locale.d.ts +17 -0
- package/dist/format/locale.js +105 -0
- package/dist/format/number.d.ts +8 -0
- package/dist/format/number.js +72 -0
- package/dist/format/regions.d.ts +6 -0
- package/dist/format/regions.js +15 -0
- package/dist/format/time-zone.d.ts +1 -0
- package/dist/format/time-zone.js +2 -0
- package/dist/icon-button-size.d.ts +3 -0
- package/dist/icon-button-size.js +3 -0
- package/dist/icon-picker.d.ts +23 -0
- package/dist/icon-picker.js +80 -0
- package/dist/index.d.ts +74 -0
- package/dist/index.js +74 -0
- package/dist/lazy.d.ts +12 -0
- package/dist/lazy.js +104 -0
- package/dist/locales/en.json +59 -0
- package/dist/locales/nl.json +59 -0
- package/dist/locales.d.ts +4 -0
- package/dist/locales.js +17 -0
- package/dist/menu/index.parts.d.ts +1 -0
- package/dist/menu/index.parts.js +1 -0
- package/dist/menu/parts.d.ts +67 -0
- package/dist/menu/parts.js +113 -0
- package/dist/merge-refs.d.ts +2 -0
- package/dist/merge-refs.js +11 -0
- package/dist/modality.d.ts +1 -0
- package/dist/modality.js +29 -0
- package/dist/platform.d.ts +2 -0
- package/dist/platform.js +9 -0
- package/dist/popover/index.parts.d.ts +1 -0
- package/dist/popover/index.parts.js +1 -0
- package/dist/popover/parts.d.ts +26 -0
- package/dist/popover/parts.js +28 -0
- package/dist/prefetch.d.ts +21 -0
- package/dist/prefetch.js +99 -0
- package/dist/preview-card/index.parts.d.ts +1 -0
- package/dist/preview-card/index.parts.js +1 -0
- package/dist/project-switcher-row.d.ts +6 -0
- package/dist/project-switcher-row.js +16 -0
- package/dist/selection.d.ts +2 -0
- package/dist/selection.js +22 -0
- package/dist/seti-icons.d.ts +5 -0
- package/dist/seti-icons.js +445 -0
- package/dist/settings/ConfirmDialog.d.ts +9 -0
- package/dist/settings/ConfirmDialog.js +9 -0
- package/dist/settings/DetailHeader.d.ts +10 -0
- package/dist/settings/DetailHeader.js +6 -0
- package/dist/settings/MasterDetail.d.ts +24 -0
- package/dist/settings/MasterDetail.js +16 -0
- package/dist/settings/SettingsDialog.d.ts +46 -0
- package/dist/settings/SettingsDialog.js +96 -0
- package/dist/settings/SettingsRow.d.ts +22 -0
- package/dist/settings/SettingsRow.js +30 -0
- package/dist/settings/SettingsSection.d.ts +14 -0
- package/dist/settings/SettingsSection.js +8 -0
- package/dist/settings/index.d.ts +7 -0
- package/dist/settings/index.js +8 -0
- package/dist/settings/target.d.ts +7 -0
- package/dist/settings/target.js +3 -0
- package/dist/shortcut-hints.d.ts +38 -0
- package/dist/shortcut-hints.js +81 -0
- package/dist/shortcut.d.ts +32 -0
- package/dist/shortcut.js +137 -0
- package/dist/testing/dedupe.d.ts +1 -0
- package/dist/testing/dedupe.js +73 -0
- package/dist/testing/fake-source.d.ts +8 -0
- package/dist/testing/fake-source.js +15 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +2 -0
- package/dist/theme.css +562 -0
- package/dist/toast-store.d.ts +38 -0
- package/dist/toast-store.js +61 -0
- package/dist/useAsyncAction.d.ts +8 -0
- package/dist/useAsyncAction.js +23 -0
- package/dist/useColumnResize.d.ts +14 -0
- package/dist/useColumnResize.js +49 -0
- package/dist/useContentSize.d.ts +5 -0
- package/dist/useContentSize.js +21 -0
- package/dist/useMeasuredWidth.d.ts +1 -0
- package/dist/useMeasuredWidth.js +6 -0
- package/dist/useNow.d.ts +3 -0
- package/dist/useNow.js +41 -0
- package/dist/wipe-split.d.ts +7 -0
- package/dist/wipe-split.js +20 -0
- package/dist/zoom.d.ts +3 -0
- package/dist/zoom.js +6 -0
- package/package.json +92 -0
- package/src/AccentSwatches.tsx +73 -0
- package/src/Banner.tsx +43 -0
- package/src/Button.tsx +55 -0
- package/src/ButtonGroup.tsx +14 -0
- package/src/Checkbox.tsx +44 -0
- package/src/ChoiceCards.tsx +108 -0
- package/src/CloseButton.tsx +21 -0
- package/src/ColorSwatch.tsx +41 -0
- package/src/ColumnResizeHandle.tsx +23 -0
- package/src/DisabledReason.tsx +23 -0
- package/src/DockShell.tsx +76 -0
- package/src/EmptyState.tsx +48 -0
- package/src/ErrorBoundary.tsx +116 -0
- package/src/Field.tsx +116 -0
- package/src/FileIcon.tsx +27 -0
- package/src/Icon.tsx +22 -0
- package/src/IconButton.tsx +84 -0
- package/src/IconPicker.tsx +257 -0
- package/src/Input.tsx +78 -0
- package/src/Kbd.tsx +67 -0
- package/src/ListRow.tsx +26 -0
- package/src/Meter.tsx +41 -0
- package/src/PanelEmpty.tsx +37 -0
- package/src/PanelHeader.tsx +31 -0
- package/src/Pill.tsx +61 -0
- package/src/ProjectSwitcher.tsx +127 -0
- package/src/PromptDialog.tsx +168 -0
- package/src/SectionLabel.tsx +20 -0
- package/src/SegmentBar.tsx +128 -0
- package/src/Segmented.tsx +51 -0
- package/src/Select.tsx +135 -0
- package/src/Separator.tsx +15 -0
- package/src/ShortcutHints.tsx +118 -0
- package/src/Skeleton.tsx +13 -0
- package/src/SlidingColumn.tsx +91 -0
- package/src/Spinner.tsx +32 -0
- package/src/Stepper.tsx +67 -0
- package/src/Surface.tsx +20 -0
- package/src/Switch.tsx +32 -0
- package/src/TextMenu.tsx +59 -0
- package/src/Tile.tsx +49 -0
- package/src/Toasts.tsx +151 -0
- package/src/Tooltip.tsx +70 -0
- package/src/UIProvider.tsx +41 -0
- package/src/Waveform.tsx +199 -0
- package/src/Wipe.tsx +99 -0
- package/src/ZoomControls.tsx +99 -0
- package/src/class-name.ts +8 -0
- package/src/clipboard.ts +17 -0
- package/src/context-menu/index.parts.ts +21 -0
- package/src/dialog/index.parts.ts +10 -0
- package/src/dialog/parts.tsx +112 -0
- package/src/dialog-layer.ts +31 -0
- package/src/error-boundary.ts +40 -0
- package/src/error-message.ts +5 -0
- package/src/field-context.ts +12 -0
- package/src/file-icon.ts +52 -0
- package/src/floating.ts +9 -0
- package/src/format/datetime.ts +63 -0
- package/src/format/duration.ts +83 -0
- package/src/format/index.ts +20 -0
- package/src/format/locale.ts +153 -0
- package/src/format/number.ts +87 -0
- package/src/format/regions.ts +20 -0
- package/src/format/time-zone.ts +2 -0
- package/src/icon-button-size.ts +6 -0
- package/src/icon-picker.ts +112 -0
- package/src/index.ts +103 -0
- package/src/lazy.tsx +124 -0
- package/src/locales/en.json +59 -0
- package/src/locales/nl.json +59 -0
- package/src/locales.ts +21 -0
- package/src/menu/index.parts.ts +20 -0
- package/src/menu/parts.tsx +296 -0
- package/src/merge-refs.ts +14 -0
- package/src/modality.ts +37 -0
- package/src/platform.ts +10 -0
- package/src/popover/index.parts.ts +8 -0
- package/src/popover/parts.tsx +119 -0
- package/src/prefetch.ts +113 -0
- package/src/preview-card/index.parts.ts +1 -0
- package/src/project-switcher-row.tsx +51 -0
- package/src/selection.ts +24 -0
- package/src/seti-icons.ts +451 -0
- package/src/settings/ConfirmDialog.tsx +30 -0
- package/src/settings/DetailHeader.tsx +25 -0
- package/src/settings/MasterDetail.tsx +67 -0
- package/src/settings/SettingsDialog.tsx +342 -0
- package/src/settings/SettingsRow.tsx +89 -0
- package/src/settings/SettingsSection.tsx +46 -0
- package/src/settings/index.ts +16 -0
- package/src/settings/target.ts +12 -0
- package/src/shortcut-hints.ts +117 -0
- package/src/shortcut.ts +170 -0
- package/src/testing/dedupe.ts +93 -0
- package/src/testing/fake-source.ts +21 -0
- package/src/testing/index.ts +3 -0
- package/src/theme.css +562 -0
- package/src/toast-store.ts +120 -0
- package/src/useAsyncAction.ts +32 -0
- package/src/useColumnResize.ts +75 -0
- package/src/useContentSize.ts +27 -0
- package/src/useMeasuredWidth.ts +7 -0
- package/src/useNow.ts +49 -0
- package/src/wipe-split.ts +24 -0
- package/src/zoom.ts +9 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { Glob, plugin } from 'bun';
|
|
4
|
+
/*
|
|
5
|
+
* A preload for `bun test` in an app that links a checkout of this library: what `resolve.dedupe`
|
|
6
|
+
* does for Vite. The checkout has node_modules of its own, and a second React, i18next or Base UI
|
|
7
|
+
* breaks every hook, every word and every compound part. Bun has no dedupe and its resolve hook never
|
|
8
|
+
* sees a package import, so every file of these packages in the checkout loads as a stand-in for the
|
|
9
|
+
* same file in the app's copy. Does nothing for a package the checkout has no copy of, which is every
|
|
10
|
+
* package once the library is installed from the registry.
|
|
11
|
+
*/
|
|
12
|
+
const SHARED = ['react', 'react-dom', 'i18next', 'react-i18next', '@base-ui-components/react'];
|
|
13
|
+
const NAME = '@adecore/ui';
|
|
14
|
+
/* The package root, from `src/testing` or `dist/testing` alike. */
|
|
15
|
+
const library = realpathSync(join(import.meta.dir, '..', '..'));
|
|
16
|
+
const manifestOf = (folder) => {
|
|
17
|
+
const path = join(folder, 'package.json');
|
|
18
|
+
return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : null;
|
|
19
|
+
};
|
|
20
|
+
const dependsOnLibrary = (manifest) => manifest !== null && [manifest.dependencies, manifest.devDependencies, manifest.peerDependencies].some((deps) => deps !== undefined && NAME in deps);
|
|
21
|
+
/*
|
|
22
|
+
* Where the app's copies are found: the folder the tests run in, then each workspace of it that
|
|
23
|
+
* depends on the library. With an isolated install the root of a monorepo has no React of its own.
|
|
24
|
+
*/
|
|
25
|
+
const appFolders = () => {
|
|
26
|
+
const root = process.cwd();
|
|
27
|
+
const workspaces = manifestOf(root)?.workspaces;
|
|
28
|
+
const patterns = (Array.isArray(workspaces) ? workspaces : workspaces?.packages) ?? [];
|
|
29
|
+
const members = patterns
|
|
30
|
+
.flatMap((pattern) => [...new Glob(`${pattern}/package.json`).scanSync({ cwd: root, onlyFiles: true })])
|
|
31
|
+
.map((path) => join(root, dirname(path)))
|
|
32
|
+
.filter((folder) => dependsOnLibrary(manifestOf(folder)));
|
|
33
|
+
return [root, ...members];
|
|
34
|
+
};
|
|
35
|
+
/* The copy of a package that `from` resolves to, walking up the node_modules folders as Node does. */
|
|
36
|
+
const copyFrom = (from, name) => {
|
|
37
|
+
for (let folder = from;; folder = dirname(folder)) {
|
|
38
|
+
const candidate = join(folder, 'node_modules', name);
|
|
39
|
+
if (existsSync(candidate)) {
|
|
40
|
+
return realpathSync(candidate);
|
|
41
|
+
}
|
|
42
|
+
if (dirname(folder) === folder) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const folders = appFolders();
|
|
48
|
+
const copies = SHARED.flatMap((name) => {
|
|
49
|
+
const theirs = join(library, 'node_modules', name);
|
|
50
|
+
if (!existsSync(theirs)) {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
const real = realpathSync(theirs);
|
|
54
|
+
const ours = folders.map((folder) => copyFrom(folder, name)).find((copy) => copy !== null && !copy.startsWith(`${library}/`));
|
|
55
|
+
return ours === undefined || ours === real ? [] : [{ theirs: `${real}/`, ours: `${ours}/` }];
|
|
56
|
+
});
|
|
57
|
+
const escape = (text) => text.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
|
|
58
|
+
// Bun refuses `export default` beside `export *`, so the stand-in names every export of the real file.
|
|
59
|
+
const standIn = (path) => {
|
|
60
|
+
const names = Object.keys(require(path)).filter((name) => name !== 'default' && /^[A-Za-z_$][\w$]*$/.test(name));
|
|
61
|
+
return [`import * as real from ${JSON.stringify(path)};`, 'export default real.default;', `export const { ${names.join(', ')} } = real;`].join('\n');
|
|
62
|
+
};
|
|
63
|
+
if (copies.length > 0) {
|
|
64
|
+
plugin({
|
|
65
|
+
name: 'adecore-ui-dedupe',
|
|
66
|
+
setup(build) {
|
|
67
|
+
build.onLoad({ filter: new RegExp(`^(${copies.map((copy) => escape(copy.theirs)).join('|')})`) }, (args) => {
|
|
68
|
+
const copy = copies.find((candidate) => args.path.startsWith(candidate.theirs));
|
|
69
|
+
return { contents: standIn(copy.ours + args.path.slice(copy.theirs.length)), loader: 'js' };
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { FORMAT_LANGUAGE } from '../format/regions.js';
|
|
2
|
+
/* A source a test sets by hand: English, the region of the language, and no system locale of a shell. */
|
|
3
|
+
export const fakeFormatSource = () => {
|
|
4
|
+
let language = 'en';
|
|
5
|
+
let region = FORMAT_LANGUAGE;
|
|
6
|
+
return {
|
|
7
|
+
language: () => language,
|
|
8
|
+
region: () => region,
|
|
9
|
+
subscribe: () => () => { },
|
|
10
|
+
set: (settings) => {
|
|
11
|
+
language = settings.language ?? language;
|
|
12
|
+
region = settings.region ?? region;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { fakeFormatSource, type FakeFormatSource } from './fake-source.ts';
|
package/dist/theme.css
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
/* The tokens, type scale and shared rules of @adecore/ui. An app imports this right after Tailwind and adds its own tokens after it. */
|
|
2
|
+
|
|
3
|
+
/* dark: follows the app theme, not the OS. */
|
|
4
|
+
@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
|
|
5
|
+
|
|
6
|
+
/* Semantic tokens. Components only ever reference these, never a raw color. */
|
|
7
|
+
:root, [data-theme="light"] {
|
|
8
|
+
color-scheme: light;
|
|
9
|
+
--bg: #f4f4f5;
|
|
10
|
+
--surface: #ffffff;
|
|
11
|
+
--surface-raised: #ffffff;
|
|
12
|
+
--surface-sunken: #ececef;
|
|
13
|
+
/* A bar that is not the one in focus: between the sunken page behind it and a focused bar, so it
|
|
14
|
+
still reads as a bar. The toolbar of a cell in the grid that does not have the keyboard. */
|
|
15
|
+
--surface-idle: color-mix(in srgb, var(--surface) 50%, var(--surface-sunken));
|
|
16
|
+
/* The step under the pointer: one shade off the ground a row or a button sits on, and lighter
|
|
17
|
+
than the pressed step, so hover reads as a lift and pressed as the heavier state. A white
|
|
18
|
+
popup cannot go lighter, so in this theme the step is the softest gray there is. */
|
|
19
|
+
--surface-hover: #f3f3f5;
|
|
20
|
+
/* The pressed step of a toggle. It cannot be `--surface-raised`, which equals `--surface` in the
|
|
21
|
+
light theme, and it has to sit a shade past `--surface-hover` so pressed outranks hover. */
|
|
22
|
+
--surface-active: #dcdce2;
|
|
23
|
+
/* Borders carry an alpha rather than a gray of their own, so a line takes the color of whatever
|
|
24
|
+
it is drawn over instead of fighting it. A gray hairline over a tinted surface reads as a
|
|
25
|
+
smudge; the same line in black at a low alpha stays a line. Everything with a surface of its
|
|
26
|
+
own clips its background to the padding box (see `@layer base`), or the border would blend
|
|
27
|
+
with that surface and never with the ground behind it. */
|
|
28
|
+
--border: rgb(0 0 0 / 0.11);
|
|
29
|
+
--border-strong: rgb(0 0 0 / 0.2);
|
|
30
|
+
/* A hairline that stays out of the way: on a sunken field a full `--border` draws a box. */
|
|
31
|
+
--border-soft: rgb(0 0 0 / 0.06);
|
|
32
|
+
--text: #18181b;
|
|
33
|
+
--text-muted: #6f6f78;
|
|
34
|
+
--text-faint: #a1a1aa;
|
|
35
|
+
/* A neutral default. An app sets its own accent, in both channels, in a rule after this file. */
|
|
36
|
+
--accent: #155dfc;
|
|
37
|
+
/* The same accent as channels, for the tokens that carry it with an alpha. */
|
|
38
|
+
--accent-rgb: 21 93 252;
|
|
39
|
+
--accent-soft: color-mix(in srgb, var(--accent) 16%, var(--surface));
|
|
40
|
+
--accent-text: #ffffff;
|
|
41
|
+
--selection: rgb(var(--accent-rgb) / 0.55);
|
|
42
|
+
--float-shadow:
|
|
43
|
+
0 1px 2px -1px rgb(0 0 0 / 0.08),
|
|
44
|
+
0 3px 6px -2px rgb(0 0 0 / 0.07),
|
|
45
|
+
0 8px 16px -8px rgb(0 0 0 / 0.1),
|
|
46
|
+
0 16px 28px -14px rgb(0 0 0 / 0.1);
|
|
47
|
+
/* The small lift of a thumb or a picked segment off the track it sits in: close to the surface
|
|
48
|
+
rather than a haze around it. Not named `--shadow-*`, since the theme maps that name onto this. */
|
|
49
|
+
--raised-shadow:
|
|
50
|
+
0 1px 2px rgb(0 0 0 / 0.05),
|
|
51
|
+
0 2px 4px -1px rgb(0 0 0 / 0.05),
|
|
52
|
+
0 6px 12px -6px rgb(0 0 0 / 0.08);
|
|
53
|
+
--status-running: #2563eb;
|
|
54
|
+
--status-needs-you: #d97706;
|
|
55
|
+
--status-idle: #16a34a;
|
|
56
|
+
--status-error: #dc2626;
|
|
57
|
+
--positive: #15803d;
|
|
58
|
+
--positive-text: #ffffff;
|
|
59
|
+
/* A control drawn over a picture rather than over the interface, the handle of a wipe, so it
|
|
60
|
+
stays the same in both themes. */
|
|
61
|
+
--media-handle: #ffffff;
|
|
62
|
+
--media-handle-text: #18181b;
|
|
63
|
+
--media-handle-edge: 0 0 0 1px rgb(0 0 0 / 0.3);
|
|
64
|
+
--media-handle-shadow: 0 2px 12px rgb(0 0 0 / 0.35);
|
|
65
|
+
/* The file type palette. A file icon is the one thing in the interface that is not tinted by the
|
|
66
|
+
theme: TypeScript is blue and Vue is green wherever the file shows up, which is what makes a
|
|
67
|
+
row readable before the name is. The sprite of the file icons reads these, inside a tree's
|
|
68
|
+
shadow root as well as beside it, so both stay the same color. */
|
|
69
|
+
--file-icon-gray: #84848a;
|
|
70
|
+
--file-icon-red: #d52c36;
|
|
71
|
+
--file-icon-vermilion: #ff8c5b;
|
|
72
|
+
--file-icon-orange: #d47628;
|
|
73
|
+
--file-icon-yellow: #d5a910;
|
|
74
|
+
--file-icon-green: #199f43;
|
|
75
|
+
--file-icon-teal: #17a5af;
|
|
76
|
+
--file-icon-cyan: #1ca1c7;
|
|
77
|
+
--file-icon-blue: #1a85d4;
|
|
78
|
+
--file-icon-indigo: #693acf;
|
|
79
|
+
--file-icon-purple: #a631be;
|
|
80
|
+
--file-icon-pink: #d32a61;
|
|
81
|
+
--file-icon-mauve: #594c5b;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* The theme attribute sits on `<html>`, the element the light rule above lands on too, so a token
|
|
85
|
+
derived there with `var()` resolves against these values, and a value that does not change
|
|
86
|
+
between themes is not repeated here. */
|
|
87
|
+
[data-theme="dark"] {
|
|
88
|
+
color-scheme: dark;
|
|
89
|
+
--bg: #0d0d10;
|
|
90
|
+
--surface: #131316;
|
|
91
|
+
--surface-raised: #18181c;
|
|
92
|
+
--surface-sunken: #08080a;
|
|
93
|
+
--surface-hover: #202024;
|
|
94
|
+
--surface-active: #28282e;
|
|
95
|
+
/* White at a low alpha, which is what gives a dark surface its lifted edge. */
|
|
96
|
+
--border: rgb(255 255 255 / 0.07);
|
|
97
|
+
--border-strong: rgb(255 255 255 / 0.13);
|
|
98
|
+
--border-soft: rgb(255 255 255 / 0.03);
|
|
99
|
+
--text: #ececf1;
|
|
100
|
+
--text-muted: #9a9aa6;
|
|
101
|
+
--text-faint: #5f5f6b;
|
|
102
|
+
--selection: rgb(var(--accent-rgb) / 0.6);
|
|
103
|
+
--float-shadow:
|
|
104
|
+
0 1px 2px -1px rgb(0 0 0 / 0.3),
|
|
105
|
+
0 2px 4px rgb(0 0 0 / 0.24),
|
|
106
|
+
0 4px 8px rgb(0 0 0 / 0.18),
|
|
107
|
+
0 6px 12px rgb(0 0 0 / 0.1);
|
|
108
|
+
/* Tighter than in the light theme: the compositor rounds the faint tail of a shadow on a layer
|
|
109
|
+
of its own a gray level darker, so a wide blur ends in a flat ring that reads as a band. */
|
|
110
|
+
--raised-shadow:
|
|
111
|
+
0 1px 2px rgb(0 0 0 / 0.3),
|
|
112
|
+
0 2px 4px rgb(0 0 0 / 0.2),
|
|
113
|
+
0 4px 8px rgb(0 0 0 / 0.12);
|
|
114
|
+
--status-running: #60a5fa;
|
|
115
|
+
--status-needs-you: #fbbf24;
|
|
116
|
+
--status-idle: #4ade80;
|
|
117
|
+
--status-error: #ef4444;
|
|
118
|
+
--positive: #4ade80;
|
|
119
|
+
/* Dark on light green: the dark theme's positive is a foreground color, so text sits on it the
|
|
120
|
+
other way round from the light theme, where the same token is a fill deep enough for white. */
|
|
121
|
+
--positive-text: #052e16;
|
|
122
|
+
--file-icon-gray: #adadb1;
|
|
123
|
+
--file-icon-red: #ff6762;
|
|
124
|
+
--file-icon-vermilion: #d5512f;
|
|
125
|
+
--file-icon-orange: #ffa359;
|
|
126
|
+
--file-icon-yellow: #ffd452;
|
|
127
|
+
--file-icon-green: #5ecc71;
|
|
128
|
+
--file-icon-teal: #64d1db;
|
|
129
|
+
--file-icon-cyan: #68cdf2;
|
|
130
|
+
--file-icon-blue: #69b1ff;
|
|
131
|
+
--file-icon-indigo: #9d6afb;
|
|
132
|
+
--file-icon-purple: #d568ea;
|
|
133
|
+
--file-icon-pink: #ff678d;
|
|
134
|
+
--file-icon-mauve: #79697b;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* Who floats over whom. Whole numbers in one place, so a new popup cannot guess. */
|
|
138
|
+
:root {
|
|
139
|
+
--z-dialog-backdrop: 80;
|
|
140
|
+
--z-dialog: 90;
|
|
141
|
+
--z-popup: 100;
|
|
142
|
+
--z-tooltip: 110;
|
|
143
|
+
--z-shortcut-hints: 120;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* One type scale for the whole interface, in rem against the font size the app sets on `<html>`
|
|
147
|
+
(15px reads well in a desktop app), so text and the rem-based layout scale together when it moves.
|
|
148
|
+
Code keeps its own absolute size, next to a terminal's, so it never scales twice.
|
|
149
|
+
`xs` is the size most of the interface is written in, so it is where reading has to work: the
|
|
150
|
+
sizes above it are a step apart rather than a leap.
|
|
151
|
+
Every rem is rounded to a whole pixel, since no interface size in the range lands a quarter rem
|
|
152
|
+
on one. The spacing unit is rounded rather than each multiple, so `p-2` is twice a whole pixel. */
|
|
153
|
+
@theme {
|
|
154
|
+
/* No default palette: a component reaches only the semantic colors below. */
|
|
155
|
+
--color-*: initial;
|
|
156
|
+
--spacing: round(0.25rem, 1px);
|
|
157
|
+
--text-xs: round(0.875rem, 1px);
|
|
158
|
+
--text-xs--line-height: round(1.25rem, 1px);
|
|
159
|
+
--text-sm: round(0.9375rem, 1px);
|
|
160
|
+
--text-sm--line-height: round(1.375rem, 1px);
|
|
161
|
+
--text-base: round(1rem, 1px);
|
|
162
|
+
--text-base--line-height: round(1.5rem, 1px);
|
|
163
|
+
--text-lg: round(1.125rem, 1px);
|
|
164
|
+
--text-lg--line-height: round(1.5rem, 1px);
|
|
165
|
+
--text-4xl: round(2.25rem, 1px);
|
|
166
|
+
--text-4xl--line-height: round(2.5rem, 1px);
|
|
167
|
+
--text-code: 13px;
|
|
168
|
+
--text-code--line-height: 20px;
|
|
169
|
+
--radius-xs: round(0.125rem, 1px);
|
|
170
|
+
--radius-sm: round(0.25rem, 1px);
|
|
171
|
+
--radius-md: round(0.375rem, 1px);
|
|
172
|
+
--radius-lg: round(0.5rem, 1px);
|
|
173
|
+
--radius-xl: round(0.75rem, 1px);
|
|
174
|
+
--radius-2xl: round(1rem, 1px);
|
|
175
|
+
--font-sans: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", sans-serif;
|
|
176
|
+
/* Not inline: an app that lets a person pick a monospace font overrides this variable on
|
|
177
|
+
`<html>`, and `font-mono` has to follow it. */
|
|
178
|
+
--font-mono: ui-monospace, "JetBrains Mono", "SF Mono", Menlo, monospace;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* A line height relative to the font would put a line between two pixels. Inline, so the `em`
|
|
182
|
+
resolves against the element that carries the utility. */
|
|
183
|
+
@theme inline {
|
|
184
|
+
--leading-tight: round(1.25em, 1px);
|
|
185
|
+
--leading-snug: round(1.375em, 1px);
|
|
186
|
+
--leading-normal: round(1.5em, 1px);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
@theme inline {
|
|
190
|
+
--color-bg: var(--bg);
|
|
191
|
+
--color-surface: var(--surface);
|
|
192
|
+
--color-surface-raised: var(--surface-raised);
|
|
193
|
+
--color-surface-sunken: var(--surface-sunken);
|
|
194
|
+
--color-surface-idle: var(--surface-idle);
|
|
195
|
+
--color-surface-hover: var(--surface-hover);
|
|
196
|
+
--color-surface-active: var(--surface-active);
|
|
197
|
+
--color-border: var(--border);
|
|
198
|
+
--color-border-strong: var(--border-strong);
|
|
199
|
+
--color-border-soft: var(--border-soft);
|
|
200
|
+
--color-text: var(--text);
|
|
201
|
+
--color-text-muted: var(--text-muted);
|
|
202
|
+
--color-text-faint: var(--text-faint);
|
|
203
|
+
--color-accent: var(--accent);
|
|
204
|
+
--color-accent-soft: var(--accent-soft);
|
|
205
|
+
--color-accent-text: var(--accent-text);
|
|
206
|
+
--color-status-running: var(--status-running);
|
|
207
|
+
--color-status-needs-you: var(--status-needs-you);
|
|
208
|
+
--color-status-idle: var(--status-idle);
|
|
209
|
+
--color-status-error: var(--status-error);
|
|
210
|
+
--color-positive: var(--positive);
|
|
211
|
+
--color-positive-text: var(--positive-text);
|
|
212
|
+
--color-media-handle: var(--media-handle);
|
|
213
|
+
--color-media-handle-text: var(--media-handle-text);
|
|
214
|
+
--shadow-float: var(--float-shadow);
|
|
215
|
+
--shadow-raised: var(--raised-shadow);
|
|
216
|
+
--shadow-media-handle-edge: var(--media-handle-edge);
|
|
217
|
+
--shadow-media-handle: var(--media-handle-shadow);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
@layer base {
|
|
221
|
+
/*
|
|
222
|
+
* A border with an alpha has to mix with what is behind the element, not with the element's own
|
|
223
|
+
* background: by default a background runs on under the border and a node's white surface would
|
|
224
|
+
* wash its line out over the canvas. Utilities sit in a later layer, so `bg-clip-text` still wins.
|
|
225
|
+
*/
|
|
226
|
+
*, ::before, ::after {
|
|
227
|
+
background-clip: padding-box;
|
|
228
|
+
}
|
|
229
|
+
body {
|
|
230
|
+
font-family: var(--font-sans);
|
|
231
|
+
font-size: var(--text-sm);
|
|
232
|
+
color: var(--text);
|
|
233
|
+
background: var(--bg);
|
|
234
|
+
-webkit-font-smoothing: antialiased;
|
|
235
|
+
-moz-osx-font-smoothing: grayscale;
|
|
236
|
+
user-select: none;
|
|
237
|
+
/* The longhand, because the `text-wrap` shorthand also resets the wrap mode and would undo `whitespace-nowrap`. */
|
|
238
|
+
text-wrap-style: pretty;
|
|
239
|
+
}
|
|
240
|
+
::selection {
|
|
241
|
+
background: var(--selection);
|
|
242
|
+
color: var(--accent-text);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/* The rules a utility cannot write: Base UI state selectors shared by many items, pseudo-elements,
|
|
247
|
+
keyframes. Inside Tailwind's `components` layer, so a utility on the same element still wins. */
|
|
248
|
+
@layer components {
|
|
249
|
+
/* Every icon button in the app, with the pressed state a `data-` or `aria-` attribute puts on it. */
|
|
250
|
+
.icon-btn {
|
|
251
|
+
display: inline-flex;
|
|
252
|
+
align-items: center;
|
|
253
|
+
justify-content: center;
|
|
254
|
+
width: 32px;
|
|
255
|
+
height: 32px;
|
|
256
|
+
/* An icon button is a square in every row it stands in: a bar that runs out of room truncates
|
|
257
|
+
its text or folds its actions away, it never squeezes a button into a sliver. */
|
|
258
|
+
flex-shrink: 0;
|
|
259
|
+
border-radius: 8px;
|
|
260
|
+
color: var(--text-muted);
|
|
261
|
+
/* Only the press animates. A fade on hover lags behind the pointer, which reads as the
|
|
262
|
+
button answering late. */
|
|
263
|
+
transition: scale 120ms ease;
|
|
264
|
+
}
|
|
265
|
+
/* The size decides the radius, and the icon inside: 16 in the default of 32, 14 in 28, 12 below. */
|
|
266
|
+
.icon-btn-sm { width: 28px; height: 28px; border-radius: 6px; }
|
|
267
|
+
.icon-btn-xs { width: 24px; height: 24px; border-radius: 6px; }
|
|
268
|
+
.icon-btn-2xs { width: 20px; height: 20px; border-radius: 4px; }
|
|
269
|
+
.icon-btn:hover { background: var(--surface-hover); color: var(--text); }
|
|
270
|
+
.icon-btn:active { scale: 0.96; }
|
|
271
|
+
/* Pressed, not important: a toggle that is on reads as a filled gray key. The accent stays for
|
|
272
|
+
the focus ring, the primary button, canvas selection and agent status. */
|
|
273
|
+
.icon-btn[data-active="true"], .icon-btn[aria-pressed="true"] { background: var(--surface-active); color: var(--text); }
|
|
274
|
+
/* A button with nothing to do says so and stays quiet under the pointer. `aria-disabled` is the
|
|
275
|
+
one that keeps its focus and its tooltip. */
|
|
276
|
+
.icon-btn:disabled, .icon-btn[aria-disabled="true"] { color: var(--text-faint); cursor: default; }
|
|
277
|
+
.icon-btn:disabled:hover, .icon-btn[aria-disabled="true"]:hover { background: transparent; color: var(--text-faint); }
|
|
278
|
+
.icon-btn:disabled:active, .icon-btn[aria-disabled="true"]:active { scale: 1; }
|
|
279
|
+
/* Every text input in the app: one border and one focus ring, at the one height or the compact
|
|
280
|
+
one, so a field in a dialog, in settings and next to a chat answer cannot drift apart. */
|
|
281
|
+
.field {
|
|
282
|
+
height: 32px;
|
|
283
|
+
width: 100%;
|
|
284
|
+
padding: 0 10px;
|
|
285
|
+
border-radius: 8px;
|
|
286
|
+
border: 1px solid var(--border);
|
|
287
|
+
background: var(--surface);
|
|
288
|
+
color: var(--text);
|
|
289
|
+
font-size: var(--text-sm);
|
|
290
|
+
outline: none;
|
|
291
|
+
}
|
|
292
|
+
.field::placeholder { color: var(--text-faint); }
|
|
293
|
+
.field:focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; }
|
|
294
|
+
/* A field with an icon is a box around a bare input: the box wears the outline the input would. */
|
|
295
|
+
.field:has(> input:focus-visible) { outline: 2px solid var(--accent); outline-offset: -1px; }
|
|
296
|
+
/* A value shown in a field but not typed into, such as a chosen folder beside the button that picks it. */
|
|
297
|
+
input.field[readonly], .field > input[readonly] { color: var(--text-muted); text-overflow: ellipsis; }
|
|
298
|
+
/* At the height of a small button: a row of tools, and a name typed in place of the label it renames. */
|
|
299
|
+
.field-sm {
|
|
300
|
+
height: 28px;
|
|
301
|
+
padding: 0 8px;
|
|
302
|
+
border-radius: 6px;
|
|
303
|
+
font-size: var(--text-xs);
|
|
304
|
+
}
|
|
305
|
+
/* Keyboard focus is visible on every control; pointer focus stays quiet. `.focus-ring` is the
|
|
306
|
+
same outline for something that takes focus without being a button, such as a row in a list. */
|
|
307
|
+
.icon-btn:focus-visible, .menu-item:focus-visible, button:focus-visible, .focus-ring:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
308
|
+
.menu-item:focus-visible { outline-offset: -2px; }
|
|
309
|
+
/* A card that holds a field, the composer's or a prompt's, wears the field's outline on its own edge. */
|
|
310
|
+
.focus-ring-within:has(:focus-visible) { outline: 2px solid var(--accent); outline-offset: -1px; }
|
|
311
|
+
/* Base UI moves DOM focus to the item the pointer hovers, and a browser keeps `:focus-visible`
|
|
312
|
+
on for scripted focus as long as its own keyboard flag is up, so after any keystroke the ring
|
|
313
|
+
would follow the mouse through every menu. The last real input event decides instead
|
|
314
|
+
(`startInputModality`); hover keeps the background it already has. The role reaches an item
|
|
315
|
+
that is no row, such as an icon button or a swatch inside a menu. */
|
|
316
|
+
:root[data-modality='pointer'] :is(.menu-item, [role^='menuitem']):focus-visible { outline: none; }
|
|
317
|
+
/* Base UI's open and close transitions are `data-` states on the popup itself, and every menu,
|
|
318
|
+
picker, dialog and tooltip in the app shares them, along with the surface they float on. */
|
|
319
|
+
.menu-popup, .picker-popup, .dialog-popup, .tooltip-popup {
|
|
320
|
+
background: var(--surface-raised);
|
|
321
|
+
border: 1px solid var(--border);
|
|
322
|
+
box-shadow: var(--float-shadow);
|
|
323
|
+
transition: opacity 120ms ease, scale 120ms ease;
|
|
324
|
+
}
|
|
325
|
+
.menu-popup {
|
|
326
|
+
min-width: 200px;
|
|
327
|
+
padding: 4px;
|
|
328
|
+
border-radius: 10px;
|
|
329
|
+
outline: none;
|
|
330
|
+
/* The positioner says how much room is left before the viewport edge; a long menu scrolls
|
|
331
|
+
inside that, with some air so it never sits against the window's edge. */
|
|
332
|
+
max-height: calc(var(--available-height) - 16px);
|
|
333
|
+
overflow-y: auto;
|
|
334
|
+
overscroll-behavior: contain;
|
|
335
|
+
transform-origin: var(--transform-origin);
|
|
336
|
+
}
|
|
337
|
+
.menu-popup[data-starting-style], .menu-popup[data-ending-style] { opacity: 0; scale: 0.96; }
|
|
338
|
+
/* A menu row, with the states Base UI puts on it while it is highlighted, disabled or the
|
|
339
|
+
trigger of a submenu that is open. */
|
|
340
|
+
.menu-item {
|
|
341
|
+
display: flex;
|
|
342
|
+
align-items: center;
|
|
343
|
+
gap: 10px;
|
|
344
|
+
padding: 6px 10px;
|
|
345
|
+
border-radius: 6px;
|
|
346
|
+
font-size: var(--text-sm);
|
|
347
|
+
/* The type scale, not the 1.5 the reset inherits, so a row is a whole number of pixels tall
|
|
348
|
+
and a glyph next to a two-line label lands on a whole pixel too. */
|
|
349
|
+
line-height: var(--text-sm--line-height);
|
|
350
|
+
color: var(--text);
|
|
351
|
+
cursor: default;
|
|
352
|
+
outline: none;
|
|
353
|
+
}
|
|
354
|
+
/* Base UI highlights a row for the pointer and for the arrow keys alike. The keyboard row is a
|
|
355
|
+
mark that has to be findable, the pointer's is the lift under the cursor. */
|
|
356
|
+
.menu-item[data-highlighted] { background: var(--surface-active); }
|
|
357
|
+
:root[data-modality='pointer'] .menu-item[data-highlighted] { background: var(--surface-hover); }
|
|
358
|
+
.menu-item[data-disabled] { opacity: .5; }
|
|
359
|
+
/* Two lines against each other divide nothing, and so does a line at the top or the bottom of a
|
|
360
|
+
menu. A menu is composed out of rows that each decide whether they are there, so the last word
|
|
361
|
+
on this is here rather than in every list that draws one. */
|
|
362
|
+
.menu-popup > [role='separator'] + [role='separator'],
|
|
363
|
+
.menu-popup > [role='separator']:first-child,
|
|
364
|
+
.menu-popup > [role='separator']:last-child { display: none; }
|
|
365
|
+
/* A list the arrow keys walk that is not a Base UI menu: the palette, the model picker, the
|
|
366
|
+
composer's own menus. Its cursor carries the same two marks a menu row does. */
|
|
367
|
+
.cursor-row[data-active='true'] { background: var(--surface-active); color: var(--text); }
|
|
368
|
+
:root[data-modality='pointer'] .cursor-row[data-active='true'] { background: var(--surface-hover); }
|
|
369
|
+
.menu-item[data-popup-open] { background: var(--surface-active); }
|
|
370
|
+
.project-switcher-trigger {
|
|
371
|
+
display: flex;
|
|
372
|
+
height: 32px;
|
|
373
|
+
min-width: 0;
|
|
374
|
+
align-items: center;
|
|
375
|
+
gap: 8px;
|
|
376
|
+
border-radius: 6px;
|
|
377
|
+
padding: 0 8px;
|
|
378
|
+
text-align: left;
|
|
379
|
+
}
|
|
380
|
+
.project-switcher-trigger:hover { background: var(--surface-hover); }
|
|
381
|
+
.project-switcher-trigger[data-popup-open] { background: var(--surface-active); }
|
|
382
|
+
.project-switcher-trigger:disabled { opacity: .5; pointer-events: none; }
|
|
383
|
+
/* A row with buttons at its end: the items touch, 1px apart, so only the outer corners stay round,
|
|
384
|
+
and the whole row lights up under the pointer while the item beneath it lights up more. */
|
|
385
|
+
.menu-row { display: flex; min-width: 0; align-items: stretch; gap: 1px; }
|
|
386
|
+
.menu-row > .menu-item:first-child { flex: 1 1 0%; min-width: 0; }
|
|
387
|
+
/* By the items around it, not by :last-child: an open submenu puts its focus guards in the row. */
|
|
388
|
+
.menu-row > .menu-item:has(~ .menu-item) { border-top-right-radius: 0; border-bottom-right-radius: 0; }
|
|
389
|
+
.menu-row > .menu-item ~ .menu-item { border-top-left-radius: 0; border-bottom-left-radius: 0; }
|
|
390
|
+
.menu-row-action {
|
|
391
|
+
flex-shrink: 0;
|
|
392
|
+
width: calc(var(--text-sm--line-height) + 12px);
|
|
393
|
+
padding: 0;
|
|
394
|
+
justify-content: center;
|
|
395
|
+
gap: 0;
|
|
396
|
+
}
|
|
397
|
+
:root[data-modality='pointer'] .menu-row:hover > .menu-item {
|
|
398
|
+
background: color-mix(in srgb, var(--surface-hover) 55%, transparent);
|
|
399
|
+
}
|
|
400
|
+
:root[data-modality='pointer'] .menu-row > .menu-item[data-highlighted] {
|
|
401
|
+
background: var(--surface-hover);
|
|
402
|
+
}
|
|
403
|
+
.menu-row:has(.menu-row-action[data-popup-open]) > .menu-item {
|
|
404
|
+
background: var(--surface-hover);
|
|
405
|
+
}
|
|
406
|
+
/* A shortcut in a row: pushed to the right, in the interface font, wherever a row carries one. */
|
|
407
|
+
.menu-item kbd { margin-left: auto; font-family: var(--font-sans); font-size: var(--text-xs); color: var(--text-faint); }
|
|
408
|
+
/* A menu with a search field on top: the field sits flush against the edge, so the padding is the list's. */
|
|
409
|
+
.picker-popup {
|
|
410
|
+
width: 320px;
|
|
411
|
+
border-radius: 10px;
|
|
412
|
+
outline: none;
|
|
413
|
+
overflow: hidden;
|
|
414
|
+
transform-origin: var(--transform-origin);
|
|
415
|
+
}
|
|
416
|
+
.picker-popup[data-starting-style], .picker-popup[data-ending-style] { opacity: 0; scale: 0.96; }
|
|
417
|
+
/* One stack for everything that floats: a dialog, then the menus and selects a dialog can host,
|
|
418
|
+
then the tooltips over those. A popup under its own dialog is the bug this ordering prevents;
|
|
419
|
+
a component reaches the middle step with `z-(--z-popup)`. */
|
|
420
|
+
.popup-positioner { z-index: var(--z-popup); }
|
|
421
|
+
/* A press on a window's drag region goes to the system and never reaches the page, so a popup
|
|
422
|
+
that closes on a press outside it would stay open over a custom title bar. While one is open
|
|
423
|
+
nothing drags, the way a native title bar takes that first press to close a menu. */
|
|
424
|
+
:root:has(.popup-positioner[data-open]) * { -webkit-app-region: no-drag; }
|
|
425
|
+
.dialog-backdrop { position: fixed; inset: 0; z-index: var(--z-dialog-backdrop); background: color-mix(in srgb, var(--bg) 55%, transparent); }
|
|
426
|
+
.dialog-backdrop[data-starting-style], .dialog-backdrop[data-ending-style] { opacity: 0; }
|
|
427
|
+
.dialog-popup {
|
|
428
|
+
position: fixed;
|
|
429
|
+
left: 50%;
|
|
430
|
+
top: 50%;
|
|
431
|
+
z-index: var(--z-dialog);
|
|
432
|
+
/* The individual `translate` property, not `transform`, because the spec applies translate
|
|
433
|
+
before scale: a `-50%` inside `transform` would be scaled along with the popup and leave
|
|
434
|
+
it drifting sideways while it grows. */
|
|
435
|
+
translate: -50% -50%;
|
|
436
|
+
max-width: calc(100vw - 32px);
|
|
437
|
+
max-height: calc(100dvh - 32px);
|
|
438
|
+
border-radius: 14px;
|
|
439
|
+
outline: none;
|
|
440
|
+
overflow: hidden;
|
|
441
|
+
}
|
|
442
|
+
.dialog-popup[data-starting-style], .dialog-popup[data-ending-style] { opacity: 0; scale: 0.97; }
|
|
443
|
+
/* A dialog opened from inside another dialog. Base UI leaves the nested backdrop out because the
|
|
444
|
+
one of the dialog underneath is still on screen, but that one lies under its own popup, so the
|
|
445
|
+
dialog you opened this from keeps taking clicks. `forceRender` on the backdrop plus these two
|
|
446
|
+
steps put the dim over it; both stay under `--z-popup`, so a menu in the nested dialog wins. */
|
|
447
|
+
.dialog-backdrop-nested { z-index: calc(var(--z-dialog) + 1); }
|
|
448
|
+
.dialog-popup-nested { z-index: calc(var(--z-dialog) + 2); }
|
|
449
|
+
/* A lightbox is the picture and nothing else, so the popup takes the size the picture settles at
|
|
450
|
+
instead of filling the window. The margin keeps the window's own title bar, and the traffic
|
|
451
|
+
lights on it, in reach above a picture that is as large as it is allowed to get. */
|
|
452
|
+
.lightbox-frame {
|
|
453
|
+
--lightbox-margin: 64px;
|
|
454
|
+
--lightbox-header: 45px;
|
|
455
|
+
}
|
|
456
|
+
.lightbox-frame img {
|
|
457
|
+
max-width: calc(100vw - 2 * var(--lightbox-margin));
|
|
458
|
+
max-height: calc(100dvh - 2 * var(--lightbox-margin) - var(--lightbox-header));
|
|
459
|
+
}
|
|
460
|
+
/* A picture holds the eye better against a heavier dim than a dialog of controls needs. */
|
|
461
|
+
.lightbox-backdrop { background: color-mix(in srgb, var(--bg) 80%, transparent); }
|
|
462
|
+
/* Tooltips. The positioner animates its position, so a tooltip slides to the next button
|
|
463
|
+
instead of blinking; the popup fades and scales from its anchor side. Both drop the motion
|
|
464
|
+
while Base UI says the tooltip is instant. */
|
|
465
|
+
/* As wide as its label up to the viewport's max width, so a short label near the edge of the
|
|
466
|
+
window does not wrap to the room left of it before the positioner moves it back in. */
|
|
467
|
+
.tooltip-positioner {
|
|
468
|
+
width: max-content;
|
|
469
|
+
z-index: var(--z-tooltip);
|
|
470
|
+
pointer-events: none;
|
|
471
|
+
transition: top 150ms ease, left 150ms ease, right 150ms ease, bottom 150ms ease;
|
|
472
|
+
}
|
|
473
|
+
.tooltip-positioner[data-instant] { transition: none; }
|
|
474
|
+
.tooltip-popup {
|
|
475
|
+
display: flex;
|
|
476
|
+
border-radius: 7px;
|
|
477
|
+
color: var(--text);
|
|
478
|
+
font-size: var(--text-xs);
|
|
479
|
+
line-height: var(--text-xs--line-height);
|
|
480
|
+
transform-origin: var(--transform-origin);
|
|
481
|
+
}
|
|
482
|
+
.tooltip-popup[data-starting-style], .tooltip-popup[data-ending-style] { opacity: 0; scale: 0.98; }
|
|
483
|
+
.tooltip-popup[data-instant] { transition: none; }
|
|
484
|
+
|
|
485
|
+
/* A file icon outside the tree. Its glyph takes the hue the sprite gives the file type, and a
|
|
486
|
+
type without one keeps the muted text it would have in the tree. */
|
|
487
|
+
.file-icon { flex-shrink: 0; vertical-align: middle; fill: currentColor; color: var(--text-muted); }
|
|
488
|
+
|
|
489
|
+
/* A strip that scrolls sideways fades out at the edge that still hides something, and only there,
|
|
490
|
+
so a strip that fits keeps its first and last item sharp. The edges are set by whoever scrolls. */
|
|
491
|
+
.scroll-fade-x {
|
|
492
|
+
--fade-start: 0px;
|
|
493
|
+
--fade-end: 0px;
|
|
494
|
+
mask-image: linear-gradient(to right, transparent, #000 var(--fade-start), #000 calc(100% - var(--fade-end)), transparent);
|
|
495
|
+
}
|
|
496
|
+
.scroll-fade-x[data-fade-start] { --fade-start: 24px; }
|
|
497
|
+
.scroll-fade-x[data-fade-end] { --fade-end: 24px; }
|
|
498
|
+
/* The same for a column under a fixed header: its top fades once something has scrolled under it. */
|
|
499
|
+
.scroll-fade-top {
|
|
500
|
+
--fade-start: 0px;
|
|
501
|
+
mask-image: linear-gradient(to bottom, transparent, #000 var(--fade-start));
|
|
502
|
+
}
|
|
503
|
+
.scroll-fade-top[data-fade-start] { --fade-start: 24px; }
|
|
504
|
+
|
|
505
|
+
/* A toast's countdown ring; `TimerRing` sets duration and delay from the toast's own timer. */
|
|
506
|
+
.toast-timer-arc { animation: toast-timer linear forwards; }
|
|
507
|
+
@keyframes toast-timer {
|
|
508
|
+
from { stroke-dashoffset: 0; }
|
|
509
|
+
to { stroke-dashoffset: 1; }
|
|
510
|
+
}
|
|
511
|
+
@media (prefers-reduced-motion: reduce) {
|
|
512
|
+
.toast-timer-arc { animation: none; }
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/* A `Spinner`: Leap from loading-dev (https://github.com/jakubkrehel/loading), MIT, Copyright (c) 2026 Jakub Krehel.
|
|
516
|
+
Each dot turns half a circle over the other two, then slides back one gap at a time; the three
|
|
517
|
+
are a third of a turn apart, so at every moment one dot is in the air. */
|
|
518
|
+
.spinner {
|
|
519
|
+
--spinner-duration: 1800ms;
|
|
520
|
+
--spinner-dot: round(calc(var(--spinner-size) * 0.22), 1px);
|
|
521
|
+
--spinner-gap: round(down, calc((var(--spinner-size) - var(--spinner-dot)) / 2), 1px);
|
|
522
|
+
position: relative;
|
|
523
|
+
display: inline-block;
|
|
524
|
+
flex-shrink: 0;
|
|
525
|
+
width: var(--spinner-size);
|
|
526
|
+
height: var(--spinner-size);
|
|
527
|
+
vertical-align: middle;
|
|
528
|
+
}
|
|
529
|
+
.spinner-hop {
|
|
530
|
+
position: absolute;
|
|
531
|
+
top: round(calc((var(--spinner-size) - var(--spinner-dot)) / 2), 1px);
|
|
532
|
+
left: calc(var(--spinner-size) - var(--spinner-dot) - var(--spinner-gap) * 2);
|
|
533
|
+
width: calc(var(--spinner-gap) * 2 + var(--spinner-dot));
|
|
534
|
+
height: var(--spinner-dot);
|
|
535
|
+
animation: spinner-hop var(--spinner-duration) ease-in-out infinite;
|
|
536
|
+
animation-delay: calc(var(--spinner-duration) * (var(--spinner-step) - 3) / 3);
|
|
537
|
+
}
|
|
538
|
+
.spinner-dot {
|
|
539
|
+
position: absolute;
|
|
540
|
+
inset: 0 auto 0 0;
|
|
541
|
+
width: var(--spinner-dot);
|
|
542
|
+
border-radius: 50%;
|
|
543
|
+
background: currentColor;
|
|
544
|
+
}
|
|
545
|
+
@keyframes spinner-hop {
|
|
546
|
+
0% { transform: translateX(0) rotate(0); }
|
|
547
|
+
33.33% { transform: translateX(0) rotate(180deg); }
|
|
548
|
+
66.66% { transform: translateX(calc(var(--spinner-gap) * -1)) rotate(180deg); }
|
|
549
|
+
100% { transform: translateX(calc(var(--spinner-gap) * -2)) rotate(180deg); }
|
|
550
|
+
}
|
|
551
|
+
@media (prefers-reduced-motion: reduce) {
|
|
552
|
+
.spinner-hop {
|
|
553
|
+
transform: translateX(calc(var(--spinner-step) * var(--spinner-gap)));
|
|
554
|
+
animation: none;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/* A `SlidingColumn` opening and closing. A drag or a width restored on load lands at once. */
|
|
559
|
+
.sliding-column { transition: width 200ms cubic-bezier(0, 0, 0.2, 1); }
|
|
560
|
+
.sliding-column[data-resizing],
|
|
561
|
+
.sliding-column[data-instant] { transition: none; }
|
|
562
|
+
}
|