cursedbelt 5.67.0 → 5.69.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/dist/looks.css +41 -0
- package/dist/react/canvas-hud/CanvasToolbar.d.ts +67 -0
- package/dist/react/canvas-hud/CanvasToolbar.d.ts.map +1 -0
- package/dist/react/canvas-hud/CanvasToolbar.js +84 -0
- package/dist/react/canvas-hud/CanvasToolbar.js.map +1 -0
- package/dist/react/canvas-hud/index.d.ts +2 -0
- package/dist/react/canvas-hud/index.d.ts.map +1 -1
- package/dist/react/canvas-hud/index.js +1 -0
- package/dist/react/canvas-hud/index.js.map +1 -1
- package/dist/react/components/ActionBar.d.ts.map +1 -1
- package/dist/react/components/ActionBar.js +9 -1
- package/dist/react/components/ActionBar.js.map +1 -1
- package/dist/react/components/DataTable/DataTable.d.ts.map +1 -1
- package/dist/react/components/DataTable/DataTable.js +3 -3
- package/dist/react/components/DataTable/DataTable.js.map +1 -1
- package/dist/react/components/EntityCard.d.ts +35 -0
- package/dist/react/components/EntityCard.d.ts.map +1 -0
- package/dist/react/components/EntityCard.js +18 -0
- package/dist/react/components/EntityCard.js.map +1 -0
- package/dist/react/components/Grid.d.ts.map +1 -1
- package/dist/react/components/Grid.js +4 -2
- package/dist/react/components/Grid.js.map +1 -1
- package/dist/react/components/ScrollArea.d.ts +1 -1
- package/dist/react/components/ScrollArea.js +1 -1
- package/dist/react/components/SecretInput.d.ts.map +1 -1
- package/dist/react/components/SecretInput.js +8 -2
- package/dist/react/components/SecretInput.js.map +1 -1
- package/dist/react/components/SegmentedControl.d.ts.map +1 -1
- package/dist/react/components/SegmentedControl.js +5 -1
- package/dist/react/components/SegmentedControl.js.map +1 -1
- package/dist/react/hooks/useKeyboardOpen.d.ts +13 -0
- package/dist/react/hooks/useKeyboardOpen.d.ts.map +1 -0
- package/dist/react/hooks/useKeyboardOpen.js +30 -0
- package/dist/react/hooks/useKeyboardOpen.js.map +1 -0
- package/dist/react/index.d.ts +3 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +3 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/look/AppearanceMenu.d.ts +29 -0
- package/dist/react/look/AppearanceMenu.d.ts.map +1 -0
- package/dist/react/look/AppearanceMenu.js +39 -0
- package/dist/react/look/AppearanceMenu.js.map +1 -0
- package/dist/react/look/AppearancePanel.d.ts +18 -0
- package/dist/react/look/AppearancePanel.d.ts.map +1 -0
- package/dist/react/look/AppearancePanel.js +20 -0
- package/dist/react/look/AppearancePanel.js.map +1 -0
- package/dist/react/look/LookPicker.d.ts +6 -1
- package/dist/react/look/LookPicker.d.ts.map +1 -1
- package/dist/react/look/LookPicker.js +2 -2
- package/dist/react/look/LookPicker.js.map +1 -1
- package/dist/react/media/denseGallery.css +2 -0
- package/dist/styles-areas/canvas-hud.css +6 -4
- package/dist/styles-areas/core.css +45 -44
- package/dist/styles-areas/fields.css +2 -2
- package/dist/styles-areas/filter-rail.css +5 -5
- package/dist/styles-areas/nav.css +6 -6
- package/dist/styles-areas/notifications.css +3 -3
- package/dist/styles-areas/place-bar.css +2 -2
- package/dist/styles-utilities.css +43 -8
- package/dist/testing/appleDevices.d.ts +68 -0
- package/dist/testing/appleDevices.d.ts.map +1 -0
- package/dist/testing/appleDevices.js +65 -0
- package/dist/testing/appleDevices.js.map +1 -0
- package/dist/testing/appleWalk.d.ts +72 -0
- package/dist/testing/appleWalk.d.ts.map +1 -0
- package/dist/testing/appleWalk.js +209 -0
- package/dist/testing/appleWalk.js.map +1 -0
- package/dist/testing/designAudit.d.ts +1 -1
- package/dist/testing/designAudit.d.ts.map +1 -1
- package/dist/testing/designAudit.js +12 -0
- package/dist/testing/designAudit.js.map +1 -1
- package/dist/testing/designBaseline.d.ts +14 -0
- package/dist/testing/designBaseline.d.ts.map +1 -1
- package/dist/testing/designBaseline.js +26 -28
- package/dist/testing/designBaseline.js.map +1 -1
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +9 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/lookPrePaint.d.ts +19 -0
- package/dist/testing/lookPrePaint.d.ts.map +1 -0
- package/dist/testing/lookPrePaint.js +56 -0
- package/dist/testing/lookPrePaint.js.map +1 -0
- package/dist/testing/phoneRules.d.ts +98 -0
- package/dist/testing/phoneRules.d.ts.map +1 -0
- package/dist/testing/phoneRules.js +282 -0
- package/dist/testing/phoneRules.js.map +1 -0
- package/dist/testing/safariDriver.d.ts +103 -0
- package/dist/testing/safariDriver.d.ts.map +1 -0
- package/dist/testing/safariDriver.js +288 -0
- package/dist/testing/safariDriver.js.map +1 -0
- package/dist/testing/safariRules.d.ts +57 -0
- package/dist/testing/safariRules.d.ts.map +1 -0
- package/dist/testing/safariRules.js +191 -0
- package/dist/testing/safariRules.js.map +1 -0
- package/package.json +2 -1
- package/scripts/styleAreas.ts +1 -0
- package/src/fonts/inter-OFL.txt +93 -0
- package/src/fonts/inter-latin-wght-normal.woff2 +0 -0
- package/src/looks.css +41 -0
- package/src/looks.spec.ts +59 -1
- package/src/react/canvas-hud/CanvasToolbar.tsx +177 -0
- package/src/react/canvas-hud/index.ts +2 -0
- package/src/react/components/ActionBar.spec.tsx +49 -1
- package/src/react/components/ActionBar.tsx +10 -2
- package/src/react/components/DataTable/DataTable.tsx +6 -1
- package/src/react/components/EntityCard.tsx +76 -0
- package/src/react/components/Grid.tsx +4 -2
- package/src/react/components/ScrollArea.tsx +1 -1
- package/src/react/components/SecretInput.spec.tsx +14 -0
- package/src/react/components/SecretInput.tsx +13 -1
- package/src/react/components/SegmentedControl.spec.tsx +1 -1
- package/src/react/components/SegmentedControl.tsx +5 -1
- package/src/react/components/tooltipWarm.spec.tsx +15 -0
- package/src/react/hooks/useKeyboardOpen.ts +29 -0
- package/src/react/index.ts +3 -0
- package/src/react/look/AppearanceMenu.spec.tsx +90 -0
- package/src/react/look/AppearanceMenu.tsx +64 -0
- package/src/react/look/AppearancePanel.tsx +54 -0
- package/src/react/look/LookPicker.tsx +14 -2
- package/src/react/media/denseGallery.css +2 -0
- package/src/styles-areas/canvas-hud.css +6 -4
- package/src/styles-areas/core.css +45 -44
- package/src/styles-areas/fields.css +2 -2
- package/src/styles-areas/filter-rail.css +5 -5
- package/src/styles-areas/nav.css +6 -6
- package/src/styles-areas/notifications.css +3 -3
- package/src/styles-areas/place-bar.css +2 -2
- package/src/styles-utilities.css +43 -8
- package/src/testing/appleDevices.spec.ts +36 -0
- package/src/testing/appleDevices.ts +97 -0
- package/src/testing/appleWalk.ts +232 -0
- package/src/testing/designAudit.ts +12 -0
- package/src/testing/designBaseline.spec.ts +9 -0
- package/src/testing/designBaseline.ts +26 -34
- package/src/testing/index.ts +18 -0
- package/src/testing/lookPrePaint.spec.ts +43 -0
- package/src/testing/lookPrePaint.ts +60 -0
- package/src/testing/phoneRules.spec.ts +75 -0
- package/src/testing/phoneRules.ts +332 -0
- package/src/testing/safariDriver.ts +331 -0
- package/src/testing/safariRules.spec.ts +92 -0
- package/src/testing/safariRules.ts +195 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter) Inter-Italic[opsz,wght].ttf: Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter)
|
|
2
|
+
|
|
3
|
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
4
|
+
This license is copied below, and is also available with a FAQ at:
|
|
5
|
+
http://scripts.sil.org/OFL
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
-----------------------------------------------------------
|
|
9
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
10
|
+
-----------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
PREAMBLE
|
|
13
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
14
|
+
development of collaborative font projects, to support the font creation
|
|
15
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
16
|
+
open framework in which fonts may be shared and improved in partnership
|
|
17
|
+
with others.
|
|
18
|
+
|
|
19
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
20
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
21
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
22
|
+
redistributed and/or sold with any software provided that any reserved
|
|
23
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
24
|
+
however, cannot be released under any other type of license. The
|
|
25
|
+
requirement for fonts to remain under this license does not apply
|
|
26
|
+
to any document created using the fonts or their derivatives.
|
|
27
|
+
|
|
28
|
+
DEFINITIONS
|
|
29
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
30
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
31
|
+
include source files, build scripts and documentation.
|
|
32
|
+
|
|
33
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
34
|
+
copyright statement(s).
|
|
35
|
+
|
|
36
|
+
"Original Version" refers to the collection of Font Software components as
|
|
37
|
+
distributed by the Copyright Holder(s).
|
|
38
|
+
|
|
39
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
40
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
41
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
42
|
+
new environment.
|
|
43
|
+
|
|
44
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
45
|
+
writer or other person who contributed to the Font Software.
|
|
46
|
+
|
|
47
|
+
PERMISSION & CONDITIONS
|
|
48
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
49
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
50
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
51
|
+
Software, subject to the following conditions:
|
|
52
|
+
|
|
53
|
+
1) Neither the Font Software nor any of its individual components,
|
|
54
|
+
in Original or Modified Versions, may be sold by itself.
|
|
55
|
+
|
|
56
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
57
|
+
redistributed and/or sold with any software, provided that each copy
|
|
58
|
+
contains the above copyright notice and this license. These can be
|
|
59
|
+
included either as stand-alone text files, human-readable headers or
|
|
60
|
+
in the appropriate machine-readable metadata fields within text or
|
|
61
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
62
|
+
|
|
63
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
64
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
65
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
66
|
+
presented to the users.
|
|
67
|
+
|
|
68
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
69
|
+
Software shall not be used to promote, endorse or advertise any
|
|
70
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
71
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
72
|
+
permission.
|
|
73
|
+
|
|
74
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
75
|
+
must be distributed entirely under this license, and must not be
|
|
76
|
+
distributed under any other license. The requirement for fonts to
|
|
77
|
+
remain under this license does not apply to any document created
|
|
78
|
+
using the Font Software.
|
|
79
|
+
|
|
80
|
+
TERMINATION
|
|
81
|
+
This license becomes null and void if any of the above conditions are
|
|
82
|
+
not met.
|
|
83
|
+
|
|
84
|
+
DISCLAIMER
|
|
85
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
86
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
87
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
88
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
89
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
90
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
91
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
92
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
93
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
Binary file
|
package/src/looks.css
CHANGED
|
@@ -235,3 +235,44 @@
|
|
|
235
235
|
--field-placeholder: var(--muted-foreground);
|
|
236
236
|
--field-ring: var(--ring);
|
|
237
237
|
}
|
|
238
|
+
|
|
239
|
+
/* ─────────────────────────────── cb-font-inter ─────────────────────────────── */
|
|
240
|
+
/*
|
|
241
|
+
* A TYPE scope, not a look: it changes the face and nothing else, so it composes with any look
|
|
242
|
+
* (`<html class="cb-obsidian cb-font-inter">`). The belt's stack is the system font — SF on a Mac,
|
|
243
|
+
* which is what Linear falls back to — but Windows and Android fall to Segoe and Roboto, so the
|
|
244
|
+
* same app read as two products depending on the OS (forge task 2915). An app opts in by putting
|
|
245
|
+
* the class on <html> beside its look (in index.html, so the first paint asks for the face).
|
|
246
|
+
*
|
|
247
|
+
* Self-hosted: the file ships INSIDE the belt (`src/fonts/`, with its OFL licence), copied from
|
|
248
|
+
* `@fontsource-variable/inter` (a devDependency — looks.spec holds the two byte-identical), and the
|
|
249
|
+
* relative url below is rebased by the app's Tailwind and copied into its own assets, so it is
|
|
250
|
+
* served from the app's origin — never a Google Fonts link: no third-party request, nothing for a
|
|
251
|
+
* CSP to refuse. Not a runtime dependency on purpose: a bare package url resolves from the APP's
|
|
252
|
+
* node_modules, which a library preview (tools/lib-preview.ts) never installs into, and which a
|
|
253
|
+
* nested install would miss. LATIN ONLY, one variable file for
|
|
254
|
+
* every weight: the fleet's copy is English, and the rest of the alphabet falls through to the
|
|
255
|
+
* system stack behind it. `font-display: swap` paints in the system face at once and swaps when the
|
|
256
|
+
* file lands; the metrics of the two are close enough that a TopBar does not move (the app's
|
|
257
|
+
* browser gate reads the layout shift). cv11 is the single-storey a, ss01 the open digits — the
|
|
258
|
+
* pair Linear sets.
|
|
259
|
+
*/
|
|
260
|
+
@font-face {
|
|
261
|
+
font-family: 'Inter Variable';
|
|
262
|
+
font-style: normal;
|
|
263
|
+
font-display: swap;
|
|
264
|
+
font-weight: 100 900;
|
|
265
|
+
src: url('./fonts/inter-latin-wght-normal.woff2') format('woff2-variations');
|
|
266
|
+
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
|
|
267
|
+
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.cb-font-inter {
|
|
271
|
+
--font-sans: 'Inter Variable', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue',
|
|
272
|
+
'Noto Sans', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
|
273
|
+
/* Tailwind's preflight reads these two on <html>; re-run them here, where --font-sans changed. */
|
|
274
|
+
--default-font-family: var(--font-sans);
|
|
275
|
+
--default-font-feature-settings: 'cv11', 'ss01';
|
|
276
|
+
font-family: var(--font-sans);
|
|
277
|
+
font-feature-settings: 'cv11', 'ss01';
|
|
278
|
+
}
|
package/src/looks.spec.ts
CHANGED
|
@@ -69,7 +69,8 @@ describe('looks.css', () => {
|
|
|
69
69
|
}
|
|
70
70
|
expect(declarationsOf(LOOKS_CSS, '.cb-aurora')).toBeNull();
|
|
71
71
|
const plain = LOOKS_CSS.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
72
|
-
|
|
72
|
+
// The type scopes (`cb-font-*`) change the face and no surface, so they are not looks.
|
|
73
|
+
const scopes = new Set([...plain.matchAll(/\.cb-([a-z-]+)/g)].map((m) => m[1]).filter((s) => !s?.startsWith('font-')));
|
|
73
74
|
expect([...scopes].sort()).toEqual(SHIPPED.map((l) => l.id).sort());
|
|
74
75
|
});
|
|
75
76
|
|
|
@@ -126,3 +127,60 @@ describe('looks.css', () => {
|
|
|
126
127
|
expect(new Set(LOOKS.map((l) => l.id)).size).toBe(LOOKS.length);
|
|
127
128
|
});
|
|
128
129
|
});
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* `cb-font-inter` (forge task 2915): the one face the belt self-hosts. The woff2 ships in
|
|
133
|
+
* `src/fonts/` and its `url()` is relative, so the app's Tailwind rebases it and the app's bundler
|
|
134
|
+
* copies it; the copy is held to the fontsource devDependency it came from, so a bump of that
|
|
135
|
+
* package without re-copying the file is red here rather than a stale face in every app.
|
|
136
|
+
*/
|
|
137
|
+
describe('cb-font-inter', () => {
|
|
138
|
+
const faces = [...LOOKS_CSS.replace(/\/\*[\s\S]*?\*\//g, '').matchAll(/@font-face\s*\{([^}]*)\}/g)].map((m) => m[1] ?? '');
|
|
139
|
+
|
|
140
|
+
/** Every face's `src: url(…)`, as written. */
|
|
141
|
+
const urls = (css: string[]): string[] => css.flatMap((face) => [...face.matchAll(/url\('([^']+)'\)/g)].map((m) => m[1] as string));
|
|
142
|
+
|
|
143
|
+
/** Where a face's url lands on disk, read the way the app's bundler reads it: relative to looks.css. */
|
|
144
|
+
const onDisk = (url: string): string => join(ROOT, 'src', url);
|
|
145
|
+
|
|
146
|
+
test('is one latin, variable, swap face, shipped inside the package beside its licence', async () => {
|
|
147
|
+
expect(faces).toHaveLength(1);
|
|
148
|
+
const face = faces[0] as string;
|
|
149
|
+
expect(face).toMatch(/font-family:\s*'Inter Variable'/);
|
|
150
|
+
expect(face).toMatch(/font-display:\s*swap/);
|
|
151
|
+
expect(face).toMatch(/font-weight:\s*100 900/);
|
|
152
|
+
expect(urls(faces)).toEqual(['./fonts/inter-latin-wght-normal.woff2']);
|
|
153
|
+
// Bun.file, not node:fs: this package's test preload virtualizes node:fs.
|
|
154
|
+
expect(await Bun.file(onDisk('./fonts/inter-latin-wght-normal.woff2')).exists()).toBe(true);
|
|
155
|
+
expect(await Bun.file(onDisk('./fonts/inter-OFL.txt')).text()).toContain('SIL Open Font License');
|
|
156
|
+
const pkg = (await Bun.file(join(ROOT, 'package.json')).json()) as { files: string[]; dependencies?: Record<string, string> };
|
|
157
|
+
expect(pkg.files).toContain('src');
|
|
158
|
+
expect(pkg.dependencies?.['@fontsource-variable/inter']).toBeUndefined();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('the shipped file is byte-for-byte the one the fontsource devDependency ships', async () => {
|
|
162
|
+
const shipped = new Uint8Array(await Bun.file(onDisk('./fonts/inter-latin-wght-normal.woff2')).arrayBuffer());
|
|
163
|
+
const source = Bun.resolveSync('@fontsource-variable/inter/files/inter-latin-wght-normal.woff2', ROOT);
|
|
164
|
+
const upstream = new Uint8Array(await Bun.file(source).arrayBuffer());
|
|
165
|
+
expect(shipped.length).toBeGreaterThan(10_000);
|
|
166
|
+
expect(Bun.hash(shipped)).toBe(Bun.hash(upstream));
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('🔴 the failure path: a face pointing at a file the package does not ship is caught', async () => {
|
|
170
|
+
const stale = faces.map((face) => face.replace('inter-latin-wght-normal', 'inter-latin-variable-normal'));
|
|
171
|
+
for (const url of urls(stale)) expect(await Bun.file(onDisk(url)).exists()).toBe(false);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('re-binds the sans stack with Inter first, and the cv11 + ss01 features', () => {
|
|
175
|
+
const decls = declarationsOf(LOOKS_CSS, '.cb-font-inter');
|
|
176
|
+
expect(decls?.get('--font-sans')).toStartWith("'Inter Variable',");
|
|
177
|
+
expect(decls?.get('--default-font-family')).toBe('var(--font-sans)');
|
|
178
|
+
expect(decls?.get('--default-font-feature-settings')).toBe("'cv11', 'ss01'");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('is a type scope, not a look: no surface token, and no look the picker offers', () => {
|
|
182
|
+
const decls = declarationsOf(LOOKS_CSS, '.cb-font-inter');
|
|
183
|
+
expect(decls?.has('--background')).toBe(false);
|
|
184
|
+
expect(LOOKS.some((look) => lookClass(look.id) === 'cb-font-inter')).toBe(false);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type { KeyboardEvent, MouseEvent, ReactNode } from 'react';
|
|
2
|
+
import { ScrollArea } from '../components/ScrollArea.js';
|
|
3
|
+
import { cn } from '../lib/cn.js';
|
|
4
|
+
|
|
5
|
+
export interface CanvasToolbarProps {
|
|
6
|
+
/** The left half: what the canvas shows (a reach, a layer switch). Hidden while the bar is
|
|
7
|
+
* under 768px wide when `compactFilters` is given — five stops beside the tools would scroll out of a thumb's sight. */
|
|
8
|
+
filters?: ReactNode;
|
|
9
|
+
/** The same choice as a phone wants it, usually the OS picker (`NativeSelect`): shown only while
|
|
10
|
+
* the bar is under 768px wide, on row two beside the legend. */
|
|
11
|
+
compactFilters?: ReactNode;
|
|
12
|
+
/** A key or chip strip (houses, layers, tags): ONE line that scrolls inside itself, its ends
|
|
13
|
+
* fading where there is more. Beside the filters at every width; never under 10rem. */
|
|
14
|
+
legend?: ReactNode;
|
|
15
|
+
/** Names the legend strip for a screen reader. */
|
|
16
|
+
legendLabel?: string;
|
|
17
|
+
/** The right half: the find box and the views. Never shrinks — the legend does. */
|
|
18
|
+
tools?: ReactNode;
|
|
19
|
+
/**
|
|
20
|
+
* Where the keyboard goes after a control in the bar acts: a POINTER click on a button, tab or
|
|
21
|
+
* radio, or Escape from one. Default: the `[role="application"]` beside the bar (the canvas the
|
|
22
|
+
* bar sits above). `false` keeps focus on the control, the browser's default.
|
|
23
|
+
*/
|
|
24
|
+
returnFocus?: (() => void) | false;
|
|
25
|
+
'aria-label'?: string;
|
|
26
|
+
className?: string;
|
|
27
|
+
/** Rides on the bar's root, for an app's own selectors (`data-view`, …). */
|
|
28
|
+
[data: `data-${string}`]: string | undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Whether a POINTER click landed on a control that should hand the keyboard back to the canvas
|
|
33
|
+
* once it has acted — a filter stop, a chip, a HUD button. A field keeps focus (it is typed into);
|
|
34
|
+
* a click the keyboard made (`detail` 0 — Enter or Space on a focused button) is a reader moving
|
|
35
|
+
* by Tab, whose focus stays where they put it.
|
|
36
|
+
*
|
|
37
|
+
* 🔴 The failure this exists for (family, 2026-10-06): the reach stops are a tab list, and a click
|
|
38
|
+
* leaves focus on the stop. Every arrow after that went to the stop — ← and → re-rooted and
|
|
39
|
+
* re-packed the tree — while the reader believed they were walking the family. Figma's rule: a
|
|
40
|
+
* toolbar click acts and the canvas keeps the keyboard.
|
|
41
|
+
*/
|
|
42
|
+
export function controlReleasesFocus(event: Pick<MouseEvent, 'target' | 'currentTarget' | 'detail'>): boolean {
|
|
43
|
+
if (event.detail === 0) return false;
|
|
44
|
+
const target = event.target;
|
|
45
|
+
if (!(target instanceof Element) || target === event.currentTarget) return false;
|
|
46
|
+
if (target.closest("input, select, textarea, [contenteditable='true'], [role='combobox'], [role='listbox'], [role='menu']")) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return target.closest("button, [role='tab'], [role='radio']") !== null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A key on a field is the field's whole language — Escape there is not the way back to the canvas. */
|
|
53
|
+
export const isTextControl = (target: EventTarget | null): boolean =>
|
|
54
|
+
target instanceof Element && target.closest("input, select, textarea, [contenteditable='true'], [role='combobox']") !== null;
|
|
55
|
+
|
|
56
|
+
/** The canvas drawn under a bar — the `application` in the box the bar shares with it. */
|
|
57
|
+
const canvasBeside = (bar: Element): HTMLElement | null =>
|
|
58
|
+
bar.parentElement?.querySelector<HTMLElement>("[role='application']") ?? null;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The toolbar a canvas wears ABOVE itself, in flow — filters left, find and views right, a legend
|
|
62
|
+
* strip that scrolls inside itself, the OS picker in place of the filters below 768px.
|
|
63
|
+
*
|
|
64
|
+
* 🔴 ONE BAR, IN FLOW — never five overlays over the drawing. family's tree floated its reach
|
|
65
|
+
* buttons, house legend, find box and view switch over the canvas's top edge until 2026-10-06, and
|
|
66
|
+
* their collisions were fought width by width while cards were drawn under them at every width. A
|
|
67
|
+
* bar above the canvas, like Figma's and Linear's, cannot collide with anything by construction:
|
|
68
|
+
* the drawing starts where the bar ends. Built from family's `TreeBar` (forge task 2917) so
|
|
69
|
+
* patterns' and desk's boards never hand-roll a second one. Not `ListToolbar`: that one is a
|
|
70
|
+
* search box and a chip filter above a list; a canvas's bar carries its own reach, a legend of
|
|
71
|
+
* anything, and gives the keyboard back to the drawing.
|
|
72
|
+
*
|
|
73
|
+
* Narrow (the BAR under 768px — a container query, so a split pane counts): row one is the
|
|
74
|
+
* `tools`, right-aligned; row two is what the canvas shows — `compactFilters` (or `filters`) and the
|
|
75
|
+
* legend strip beside it. A phone's find button and four views do not leave a picker room beside
|
|
76
|
+
* them (family at 390: 300px of tools in a 366px row), and the picker and the legend are the same
|
|
77
|
+
* question — who is drawn — so they share a row. Wide: one line when the filters, a
|
|
78
|
+
* 10rem legend and the tools fit; when they do not (a 900px window), the tools ride down to row two
|
|
79
|
+
* as a group, right-aligned, rather than the filters being cut — the stops never shrink on a wide
|
|
80
|
+
* bar. Two rows at most at every width: the legend scrolls rather than wrapping.
|
|
81
|
+
*
|
|
82
|
+
* The bar hands the keyboard back to the canvas after a control acts (`returnFocus`), because a
|
|
83
|
+
* bar in flow sits OUTSIDE the canvas, so the canvas's own hand-back never sees its clicks.
|
|
84
|
+
*/
|
|
85
|
+
export function CanvasToolbar({
|
|
86
|
+
filters,
|
|
87
|
+
compactFilters,
|
|
88
|
+
legend,
|
|
89
|
+
legendLabel,
|
|
90
|
+
tools,
|
|
91
|
+
returnFocus,
|
|
92
|
+
'aria-label': ariaLabel,
|
|
93
|
+
className,
|
|
94
|
+
...data
|
|
95
|
+
}: CanvasToolbarProps) {
|
|
96
|
+
const giveBack = (bar: Element) => {
|
|
97
|
+
if (returnFocus === false) return false;
|
|
98
|
+
if (returnFocus) {
|
|
99
|
+
returnFocus();
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
const canvas = canvasBeside(bar);
|
|
103
|
+
canvas?.focus({ preventScroll: true });
|
|
104
|
+
return canvas !== null;
|
|
105
|
+
};
|
|
106
|
+
const onClick = (event: MouseEvent<HTMLDivElement>) => {
|
|
107
|
+
if (controlReleasesFocus(event)) giveBack(event.currentTarget);
|
|
108
|
+
};
|
|
109
|
+
const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
|
110
|
+
if (event.key !== 'Escape' || event.target === event.currentTarget || isTextControl(event.target)) return;
|
|
111
|
+
if (giveBack(event.currentTarget)) event.preventDefault();
|
|
112
|
+
};
|
|
113
|
+
return (
|
|
114
|
+
// The handlers delegate — the bar's buttons, tabs and fields are the controls; these only hand
|
|
115
|
+
// the keyboard back to the canvas after one of them acts.
|
|
116
|
+
// biome-ignore lint/a11y/useSemanticElements: a <fieldset> is a FORM's grouping (its legend, its min-width quirk); this is a toolbar's layout box, and `group` only names it. No `toolbar` role: it promises arrow-key roving, which would take the arrows from the find box (ListToolbar's reasoning).
|
|
117
|
+
<div
|
|
118
|
+
{...data}
|
|
119
|
+
data-slot='canvas-toolbar'
|
|
120
|
+
role='group'
|
|
121
|
+
aria-label={ariaLabel}
|
|
122
|
+
onClick={onClick}
|
|
123
|
+
onKeyDown={onKeyDown}
|
|
124
|
+
// `@container`: the bar answers to ITS width, not the window's — a canvas in a split pane
|
|
125
|
+
// or a demo column is a phone-width bar on a desk-width screen.
|
|
126
|
+
className={cn('@container border-b border-border bg-card', className)}
|
|
127
|
+
>
|
|
128
|
+
<div className='flex flex-wrap items-center gap-x-3 gap-y-2 px-3 py-2'>
|
|
129
|
+
{filters ? (
|
|
130
|
+
<div
|
|
131
|
+
data-slot='canvas-toolbar-filters'
|
|
132
|
+
className={cn(
|
|
133
|
+
'order-2 min-w-0 items-center gap-2 @3xl:order-none @3xl:shrink-0',
|
|
134
|
+
compactFilters ? 'hidden @3xl:flex' : 'flex',
|
|
135
|
+
)}
|
|
136
|
+
>
|
|
137
|
+
{filters}
|
|
138
|
+
</div>
|
|
139
|
+
) : null}
|
|
140
|
+
{compactFilters ? (
|
|
141
|
+
<div
|
|
142
|
+
data-slot='canvas-toolbar-compact-filters'
|
|
143
|
+
className='order-2 flex min-w-26 max-w-48 flex-1 items-center @3xl:hidden [&>*]:w-full [&_select]:w-full'
|
|
144
|
+
>
|
|
145
|
+
{compactFilters}
|
|
146
|
+
</div>
|
|
147
|
+
) : null}
|
|
148
|
+
{legend ? (
|
|
149
|
+
<ScrollArea
|
|
150
|
+
axis='x'
|
|
151
|
+
space='none'
|
|
152
|
+
safeArea={false}
|
|
153
|
+
fade
|
|
154
|
+
role='group'
|
|
155
|
+
aria-label={legendLabel}
|
|
156
|
+
data-slot='canvas-toolbar-legend'
|
|
157
|
+
className={cn(
|
|
158
|
+
'order-3 flex min-w-40 basis-0 items-center gap-1 px-0.5 @3xl:order-none',
|
|
159
|
+
// The strip's own scrollbar is hidden; its fading ends are the "there is more" cue.
|
|
160
|
+
'[scrollbar-width:none] [&::-webkit-scrollbar]:hidden',
|
|
161
|
+
)}
|
|
162
|
+
>
|
|
163
|
+
{legend}
|
|
164
|
+
</ScrollArea>
|
|
165
|
+
) : null}
|
|
166
|
+
{tools ? (
|
|
167
|
+
<div
|
|
168
|
+
data-slot='canvas-toolbar-tools'
|
|
169
|
+
className='order-1 ml-auto flex flex-none basis-full items-center justify-end gap-2 @3xl:order-none @3xl:basis-auto'
|
|
170
|
+
>
|
|
171
|
+
{tools}
|
|
172
|
+
</div>
|
|
173
|
+
) : null}
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
@@ -7,6 +7,8 @@ export type { CanvasNodeMenuItem, CanvasNodeMenuProps, CanvasNodeMenuSection } f
|
|
|
7
7
|
export { CanvasNodeMenu } from './CanvasNodeMenu.js';
|
|
8
8
|
export type { CanvasSearchFieldProps } from './CanvasSearchField.js';
|
|
9
9
|
export { CanvasSearchField } from './CanvasSearchField.js';
|
|
10
|
+
export type { CanvasToolbarProps } from './CanvasToolbar.js';
|
|
11
|
+
export { CanvasToolbar, controlReleasesFocus, isTextControl } from './CanvasToolbar.js';
|
|
10
12
|
export type { CanvasZoomHintProps, HudButtonProps, HudZoomControlsProps } from './HudControls.js';
|
|
11
13
|
export { CanvasZoomHint, HudButton, HudZoomControls } from './HudControls.js';
|
|
12
14
|
export type { CanvasWheelMode, CanvasWheelOptions } from './useCanvasWheel.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
2
|
-
import { cleanup, render } from '@testing-library/react';
|
|
2
|
+
import { act, cleanup, render } from '@testing-library/react';
|
|
3
3
|
import { ActionBar, FooterSlot } from './ActionBar.js';
|
|
4
4
|
import { Button } from './Button.js';
|
|
5
5
|
|
|
@@ -291,3 +291,51 @@ describe('FooterSlot — the footer is ordered by variant, not by the children (
|
|
|
291
291
|
expect(labels(slot)).toEqual(['Back', 'Skip']);
|
|
292
292
|
});
|
|
293
293
|
});
|
|
294
|
+
|
|
295
|
+
describe('ActionBar — mobile, with the keyboard up', () => {
|
|
296
|
+
const realVV = Object.getOwnPropertyDescriptor(window, 'visualViewport');
|
|
297
|
+
beforeEach(() => stubMatchMedia(true));
|
|
298
|
+
afterEach(() => {
|
|
299
|
+
if (realVV) Object.defineProperty(window, 'visualViewport', realVV);
|
|
300
|
+
else Reflect.deleteProperty(window, 'visualViewport');
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
/** iOS's keyboard, stood in: the visible viewport loses `px` from the bottom. */
|
|
304
|
+
function standInKeyboard() {
|
|
305
|
+
const vv = new EventTarget() as EventTarget & { height: number; offsetTop: number };
|
|
306
|
+
vv.height = window.innerHeight;
|
|
307
|
+
vv.offsetTop = 0;
|
|
308
|
+
Object.defineProperty(window, 'visualViewport', { configurable: true, get: () => vv });
|
|
309
|
+
return {
|
|
310
|
+
raise(px: number) {
|
|
311
|
+
vv.height = window.innerHeight - px;
|
|
312
|
+
vv.dispatchEvent(new Event('resize'));
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
test('🔴 steps aside while the keyboard is up, and is back when it goes (LAY-15)', async () => {
|
|
318
|
+
const keyboard = standInKeyboard();
|
|
319
|
+
const { container } = render(
|
|
320
|
+
<ActionBar
|
|
321
|
+
destructive={<Button tooltip='Delete' variant='destructive'>Delete</Button>}
|
|
322
|
+
primary={<Button tooltip='Save' variant='primary'>Save</Button>}
|
|
323
|
+
secondary={<Button tooltip='Cancel' variant='secondary'>Cancel</Button>}
|
|
324
|
+
/>,
|
|
325
|
+
);
|
|
326
|
+
const bar = () => container.querySelector('[data-cb-action-bar]') as HTMLElement;
|
|
327
|
+
expect(bar().hidden).toBe(false);
|
|
328
|
+
await act(() => keyboard.raise(336));
|
|
329
|
+
expect(bar().hidden).toBe(true);
|
|
330
|
+
expect(bar().getAttribute('data-cb-keyboard')).toBe('aside');
|
|
331
|
+
await act(() => keyboard.raise(0));
|
|
332
|
+
expect(bar().hidden).toBe(false);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("the URL bar collapsing (under 120px) is not a keyboard", async () => {
|
|
336
|
+
const keyboard = standInKeyboard();
|
|
337
|
+
const { container } = render(<ActionBar primary={<Button tooltip='Save' variant='primary'>Save</Button>} />);
|
|
338
|
+
await act(() => keyboard.raise(60));
|
|
339
|
+
expect((container.querySelector('[data-cb-action-bar]') as HTMLElement).hidden).toBe(false);
|
|
340
|
+
});
|
|
341
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Children, Fragment, isValidElement, type ReactNode } from 'react';
|
|
2
|
+
import { useKeyboardOpen } from '../hooks/useKeyboardOpen.js';
|
|
2
3
|
import { useMediaQuery } from '../hooks/useMediaQuery.js';
|
|
3
4
|
import { cn } from '../lib/cn.js';
|
|
4
5
|
import { devWarnOnce } from '../lib/devWarn.js';
|
|
@@ -84,6 +85,7 @@ export function ActionBar({
|
|
|
84
85
|
className,
|
|
85
86
|
}: ActionBarProps) {
|
|
86
87
|
const isMobile = useMediaQuery(MOBILE_QUERY);
|
|
88
|
+
const keyboardOpen = useKeyboardOpen();
|
|
87
89
|
if (hasPrimaryChild(children)) {
|
|
88
90
|
devWarnOnce(
|
|
89
91
|
'ActionBar:primary-child',
|
|
@@ -92,6 +94,12 @@ export function ActionBar({
|
|
|
92
94
|
);
|
|
93
95
|
}
|
|
94
96
|
|
|
97
|
+
if (isMobile && keyboardOpen) {
|
|
98
|
+
// 🔴 Aside while the keyboard is up. Lifted by `--keyboard-inset` the bar sat IN the strip the
|
|
99
|
+
// keyboard leaves, over the line being typed on (family's editor, three rows deep, Apple pass
|
|
100
|
+
// 2026-10-06 — LAY-15). Nobody saves mid-keystroke: the keyboard goes, the bar is back.
|
|
101
|
+
return <div data-cb-action-bar='mobile' data-cb-keyboard='aside' hidden />;
|
|
102
|
+
}
|
|
95
103
|
if (isMobile) {
|
|
96
104
|
return (
|
|
97
105
|
<div
|
|
@@ -109,8 +117,8 @@ export function ActionBar({
|
|
|
109
117
|
)}
|
|
110
118
|
{/* `*`, not `button`: a Download is `<Button asChild><a download>`, and a link primary
|
|
111
119
|
sat half-width under a phone's full-width Cancel (desk's Export, forge task 2750). */}
|
|
112
|
-
{primary && <div className='[&>*]:w-full'>{primary}</div>}
|
|
113
|
-
{secondary && <div className='[&>*]:w-full'>{secondary}</div>}
|
|
120
|
+
{primary && <div className='[&>*]:w-full [&>*>button]:w-full'>{primary}</div>}
|
|
121
|
+
{secondary && <div className='[&>*]:w-full [&>*>button]:w-full'>{secondary}</div>}
|
|
114
122
|
</div>
|
|
115
123
|
);
|
|
116
124
|
}
|
|
@@ -2234,6 +2234,10 @@ export function DataTable<TRow extends Record<string, unknown>>({
|
|
|
2234
2234
|
{selectionShown ? (
|
|
2235
2235
|
<th
|
|
2236
2236
|
scope='col'
|
|
2237
|
+
// The seam an app's own cell padding leaves alone: this cell is exactly the 44px
|
|
2238
|
+
// the checkbox's label needs on a touchscreen (music's 6px pushed the label under
|
|
2239
|
+
// the next cell and cut a finger's reach to 37px — the Apple pass, 2026-10-06).
|
|
2240
|
+
data-cb-selection-cell=''
|
|
2237
2241
|
className={cn(
|
|
2238
2242
|
'w-11 border-b border-border px-0 text-center',
|
|
2239
2243
|
stickyHeader && 'sticky top-0 z-10 bg-card/95 backdrop-blur-sm',
|
|
@@ -2408,6 +2412,7 @@ export function DataTable<TRow extends Record<string, unknown>>({
|
|
|
2408
2412
|
} as CSSProperties)
|
|
2409
2413
|
: undefined
|
|
2410
2414
|
}
|
|
2415
|
+
data-cb-selection-cell=''
|
|
2411
2416
|
className={cn(
|
|
2412
2417
|
'w-11 cursor-pointer px-0 text-center align-middle',
|
|
2413
2418
|
stickyFirstColumn && 'sticky left-0 z-10 bg-[var(--cb-frozen-bg)]',
|
|
@@ -2473,7 +2478,7 @@ export function DataTable<TRow extends Record<string, unknown>>({
|
|
|
2473
2478
|
<tfoot data-cb-table-footer=''>
|
|
2474
2479
|
<tr>
|
|
2475
2480
|
{footerHeaders.filter((h) => h.column.columnDef.meta?.leading === true).map((h) => footerCell(h, -1))}
|
|
2476
|
-
{selectionShown ? <td className='w-11 border-t border-border px-0' /> : null}
|
|
2481
|
+
{selectionShown ? <td data-cb-selection-cell='' className='w-11 border-t border-border px-0' /> : null}
|
|
2477
2482
|
{footerHeaders.filter((h) => h.column.columnDef.meta?.leading !== true).map(footerCell)}
|
|
2478
2483
|
{hasActions ? <td className='border-t border-border px-2' /> : null}
|
|
2479
2484
|
</tr>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { cn } from '../lib/cn.js';
|
|
3
|
+
import { CardTapTarget } from './CardTapTarget.js';
|
|
4
|
+
|
|
5
|
+
export interface EntityCardProps {
|
|
6
|
+
/** The face: an `Avatar`, a portrait, a cover thumbnail. Drawn at its own size, never squeezed. */
|
|
7
|
+
media: ReactNode;
|
|
8
|
+
/** The name — one line, ellipsed. */
|
|
9
|
+
name: ReactNode;
|
|
10
|
+
/** One line under the name (dates, a role, a count) — muted, tabular figures, ellipsed. */
|
|
11
|
+
line?: ReactNode;
|
|
12
|
+
/** Fired on tap/click and on Enter/Space — the whole card is the target. */
|
|
13
|
+
onClick: () => void;
|
|
14
|
+
/** What a tap does, for a screen reader ("Open Ada Lovelace") — the name alone says what, not why. */
|
|
15
|
+
'aria-label'?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Skip `onClick` when the click only completes a text selection inside the card. Default
|
|
18
|
+
* `false`: a face, a name and one line hold nothing worth selecting, and a press that the
|
|
19
|
+
* selection check swallows reads as a dead card on a phone.
|
|
20
|
+
*/
|
|
21
|
+
selectionSafe?: boolean;
|
|
22
|
+
className?: string;
|
|
23
|
+
/** Rides on the card's root, for an app's own selectors (`data-ended`, `data-self`, …). */
|
|
24
|
+
[data: `data-${string}`]: string | undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A person, a title, a game — a face, a name, one line, the whole card one tap target that dips
|
|
29
|
+
* on press. Built from family's close-family `KinCard` (forge task 2918), on `CardTapTarget`, so
|
|
30
|
+
* the tap floor (`TAP_TARGET_HEIGHT`), the press dip (`PRESS_SURFACE`) and the iPhone-safe
|
|
31
|
+
* `div[role=button]` come from the one place they are kept.
|
|
32
|
+
*
|
|
33
|
+
* The card is a row: the face left, the text beside it. It fills its grid cell, and its text
|
|
34
|
+
* truncates rather than wrapping, so every card in a grid row is the same height whatever the
|
|
35
|
+
* name. `relative isolate` keeps anything the face positions from painting over the next card and
|
|
36
|
+
* eating its taps (`CardTapTarget`'s other half).
|
|
37
|
+
*/
|
|
38
|
+
export function EntityCard({
|
|
39
|
+
media,
|
|
40
|
+
name,
|
|
41
|
+
line,
|
|
42
|
+
onClick,
|
|
43
|
+
'aria-label': ariaLabel,
|
|
44
|
+
selectionSafe = false,
|
|
45
|
+
className,
|
|
46
|
+
...data
|
|
47
|
+
}: EntityCardProps) {
|
|
48
|
+
return (
|
|
49
|
+
<CardTapTarget
|
|
50
|
+
{...data}
|
|
51
|
+
onClick={onClick}
|
|
52
|
+
aria-label={ariaLabel}
|
|
53
|
+
selectionSafe={selectionSafe}
|
|
54
|
+
className={cn(
|
|
55
|
+
'relative isolate flex min-h-14 min-w-0 items-center gap-3 rounded-lg border border-border bg-card px-3 py-2',
|
|
56
|
+
'hover:border-border-strong hover:bg-card-raised',
|
|
57
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background',
|
|
58
|
+
className,
|
|
59
|
+
)}
|
|
60
|
+
>
|
|
61
|
+
<span data-slot='entity-card-media' className='flex flex-none items-center'>
|
|
62
|
+
{media}
|
|
63
|
+
</span>
|
|
64
|
+
<span className='flex min-w-0 flex-col gap-0.5'>
|
|
65
|
+
<span data-slot='entity-card-name' className='truncate text-sm leading-5 font-medium text-foreground'>
|
|
66
|
+
{name}
|
|
67
|
+
</span>
|
|
68
|
+
{line ? (
|
|
69
|
+
<span data-slot='entity-card-line' className='truncate text-xs leading-4 text-muted-foreground tabular-nums'>
|
|
70
|
+
{line}
|
|
71
|
+
</span>
|
|
72
|
+
) : null}
|
|
73
|
+
</span>
|
|
74
|
+
</CardTapTarget>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -23,8 +23,10 @@ export interface GridProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
23
23
|
// grid is pulled out by half a gutter on both sides, so content still aligns with the
|
|
24
24
|
// edges, and the wrapper clips what hangs over — which is exactly the first column of
|
|
25
25
|
// every row's border, wherever the row wraps. The clip margin keeps a focus ring at the
|
|
26
|
-
// edge visible (it is far less than the half gutter the border sits out at).
|
|
27
|
-
|
|
26
|
+
// edge visible (it is far less than the half gutter the border sits out at). Under `@supports`:
|
|
27
|
+
// Safari has no `overflow-clip-margin` (27.0.1, read by the shipped-Safari check, FND-18) and
|
|
28
|
+
// the ring is clipped there — an enhancement, said so, rather than a declaration it drops.
|
|
29
|
+
const DIVIDED_WRAPPER = 'overflow-x-clip supports-[overflow-clip-margin:1px]:[overflow-clip-margin:calc(var(--spacing)*1)]';
|
|
28
30
|
// The extra 1px on the left is the clipped rule's own width: without it the first
|
|
29
31
|
// column's content would sit a pixel in from the edge every other block lines up on.
|
|
30
32
|
const DIVIDED_GRID =
|
|
@@ -106,7 +106,7 @@ const REGION_HEIGHT: Record<ScrollAreaHeight, string> = {
|
|
|
106
106
|
*
|
|
107
107
|
* Rules:
|
|
108
108
|
* - NEVER write `overflow-y-auto` directly. Use `ScrollArea`.
|
|
109
|
-
* - NEVER use `
|
|
109
|
+
* - NEVER use a `100dvh` calc height to compensate for layout — `dvh` does NOT
|
|
110
110
|
* shrink for the keyboard. Height comes from the shell's flex column
|
|
111
111
|
* (`min-h-0` + `flex-1`), not the component.
|
|
112
112
|
* - Modals/drawers/panels use this same `ScrollArea` for their scrollable body.
|
|
@@ -65,3 +65,17 @@ describe('SecretInput', () => {
|
|
|
65
65
|
expect(copyButton(container)).toBeDefined();
|
|
66
66
|
});
|
|
67
67
|
});
|
|
68
|
+
|
|
69
|
+
describe('SecretInput — the frame is the field to a finger (LAY-13, the Apple pass)', () => {
|
|
70
|
+
test('a tap on the frame itself focuses the input; one on a button inside does not steal focus', () => {
|
|
71
|
+
const { container } = render(<SecretInput value='hunter2' onChange={() => {}} label='Password' />);
|
|
72
|
+
const frame = container.querySelector('[data-cb-field-control]') as HTMLElement;
|
|
73
|
+
const input = container.querySelector('input') as HTMLInputElement;
|
|
74
|
+
expect(frame).toBeTruthy();
|
|
75
|
+
fireEvent.pointerDown(frame);
|
|
76
|
+
expect(document.activeElement).toBe(input);
|
|
77
|
+
input.blur();
|
|
78
|
+
fireEvent.pointerDown(copyButton(container) as HTMLElement);
|
|
79
|
+
expect(document.activeElement).not.toBe(input);
|
|
80
|
+
});
|
|
81
|
+
});
|