@tuturuuu/ui 0.19.1 → 0.21.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/CHANGELOG.md +113 -0
- package/biome.json +1 -1
- package/package.json +60 -48
- package/src/components/ui/badge.tsx +2 -0
- package/src/components/ui/button.tsx +2 -0
- package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +48 -4
- package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +33 -0
- package/src/components/ui/custom/__tests__/workspace-select-reveal.test.tsx +88 -0
- package/src/components/ui/custom/animated-slot-text.tsx +20 -0
- package/src/components/ui/custom/common-footer.tsx +262 -237
- package/src/components/ui/custom/language-dropdown-item.tsx +3 -10
- package/src/components/ui/custom/language-toggle.test.tsx +30 -0
- package/src/components/ui/custom/language-toggle.tsx +11 -10
- package/src/components/ui/custom/locale-preference.ts +32 -0
- package/src/components/ui/custom/settings/appearance-settings.tsx +5 -13
- package/src/components/ui/custom/settings-dialog-shell.tsx +40 -9
- package/src/components/ui/custom/structure.tsx +12 -0
- package/src/components/ui/custom/system-language-dropdown-item.tsx +3 -6
- package/src/components/ui/custom/workspace-access/adapters.test.ts +10 -1
- package/src/components/ui/custom/workspace-access/adapters.ts +36 -4
- package/src/components/ui/custom/workspace-access/member-filter-utils.test.ts +14 -0
- package/src/components/ui/custom/workspace-access/member-filter-utils.ts +8 -0
- package/src/components/ui/custom/workspace-access/types.ts +20 -0
- package/src/components/ui/custom/workspace-access/workspace-access-context.test.tsx +111 -0
- package/src/components/ui/custom/workspace-access/workspace-access-default-role-card.tsx +15 -5
- package/src/components/ui/custom/workspace-access/workspace-access-invite-dialog.tsx +155 -48
- package/src/components/ui/custom/workspace-access/workspace-access-member-profile-dialog.tsx +92 -0
- package/src/components/ui/custom/workspace-access/workspace-access-member-row.tsx +171 -76
- package/src/components/ui/custom/workspace-access/workspace-access-members.tsx +11 -2
- package/src/components/ui/custom/workspace-access/workspace-access-page-header.tsx +11 -11
- package/src/components/ui/custom/workspace-access/workspace-access-page.tsx +180 -22
- package/src/components/ui/custom/workspace-access/workspace-access-people-filters.tsx +29 -15
- package/src/components/ui/custom/workspace-access/workspace-access-permission-checklist.tsx +56 -10
- package/src/components/ui/custom/workspace-access/workspace-access-permission-preview.test.ts +33 -0
- package/src/components/ui/custom/workspace-access/workspace-access-permission-preview.tsx +24 -1
- package/src/components/ui/custom/workspace-access/workspace-access-responsive.test.ts +64 -0
- package/src/components/ui/custom/workspace-access/workspace-access-role-editor-dialog.tsx +28 -16
- package/src/components/ui/custom/workspace-access/workspace-access-roles.tsx +35 -11
- package/src/components/ui/custom/workspace-access/workspace-access-tabs-toolbar.tsx +23 -11
- package/src/components/ui/custom/workspace-select-helpers.ts +5 -3
- package/src/components/ui/custom/workspace-select-reveal.tsx +46 -0
- package/src/components/ui/custom/workspace-select.tsx +13 -5
- package/src/components/ui/finance/shared/charts/monthly-total-chart-client.tsx +1 -1
- package/src/components/ui/finance/shared/charts/monthly-total-chart.tsx +1 -1
- package/src/components/ui/storefront/cart-summary.tsx +12 -2
- package/src/components/ui/storefront/storefront-surface.test.tsx +21 -0
- package/src/components/ui/storefront/storefront-surface.tsx +6 -0
- package/src/components/ui/text-editor/__tests__/collaboration-binding.test.tsx +161 -0
- package/src/components/ui/text-editor/editor.tsx +267 -235
- package/src/globals.css +166 -0
- package/src/hooks/__tests__/use-workspace-identity-mutation.test.tsx +181 -0
- package/src/hooks/use-workspace-identity-mutation.ts +136 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { render } from '@testing-library/react';
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import * as Y from 'yjs';
|
|
4
|
+
import { RichTextEditor } from '../editor';
|
|
5
|
+
|
|
6
|
+
const useEditorMock = vi.hoisted(() => vi.fn((..._args: unknown[]) => null));
|
|
7
|
+
|
|
8
|
+
vi.mock('@tiptap/react', async (importOriginal) => ({
|
|
9
|
+
...(await importOriginal<typeof import('@tiptap/react')>()),
|
|
10
|
+
EditorContent: () => null,
|
|
11
|
+
useEditor: (...args: unknown[]) => useEditorMock(...args),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock('../tool-bar', () => ({
|
|
15
|
+
FixedToolbar: ({ className }: { className?: string }) => (
|
|
16
|
+
<div className={className} data-testid="fixed-toolbar" />
|
|
17
|
+
),
|
|
18
|
+
ToolBar: () => null,
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
type UseEditorCall = [
|
|
22
|
+
{ extensions: Array<{ name: string }> },
|
|
23
|
+
unknown[] | undefined,
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function lastCall(): UseEditorCall {
|
|
27
|
+
const calls = useEditorMock.mock.calls as unknown as UseEditorCall[];
|
|
28
|
+
const call = calls.at(-1);
|
|
29
|
+
if (!call) throw new Error('useEditor was never called');
|
|
30
|
+
return call;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function extensionNames() {
|
|
34
|
+
return lastCall()[0].extensions.map((extension) => extension.name);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('RichTextEditor collaboration binding', () => {
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
useEditorMock.mockClear();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('omits the collaboration extension while collaboration is off', () => {
|
|
43
|
+
render(
|
|
44
|
+
<RichTextEditor
|
|
45
|
+
allowCollaboration={false}
|
|
46
|
+
content={null}
|
|
47
|
+
yjsDoc={undefined}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
expect(extensionNames()).not.toContain('collaboration');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Regression: `Editor.setOptions({ extensions })` does not rebuild the
|
|
55
|
+
// extension manager, so the only way to attach the Collaboration extension
|
|
56
|
+
// after mount is to recreate the editor. A task dialog opened from a deep link
|
|
57
|
+
// mounts before the task hydrates — i.e. with collaboration off — and used to
|
|
58
|
+
// stay non-collaborative forever, leaving the description permanently empty.
|
|
59
|
+
it('rebuilds the editor when collaboration turns on after mount', () => {
|
|
60
|
+
const doc = new Y.Doc();
|
|
61
|
+
const { rerender } = render(
|
|
62
|
+
<RichTextEditor allowCollaboration={false} content={null} yjsDoc={doc} />
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const initialDeps = lastCall()[1];
|
|
66
|
+
expect(extensionNames()).not.toContain('collaboration');
|
|
67
|
+
|
|
68
|
+
rerender(<RichTextEditor allowCollaboration content={null} yjsDoc={doc} />);
|
|
69
|
+
|
|
70
|
+
const collaborativeCall = lastCall();
|
|
71
|
+
expect(collaborativeCall[0].extensions.map((e) => e.name)).toContain(
|
|
72
|
+
'collaboration'
|
|
73
|
+
);
|
|
74
|
+
// A changed dependency array is what makes `useEditor` throw away the
|
|
75
|
+
// non-collaborative instance and build a new one.
|
|
76
|
+
expect(collaborativeCall[1]).not.toEqual(initialDeps);
|
|
77
|
+
expect(collaborativeCall[1]).toContain(doc);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('keeps a stable dependency array across unrelated re-renders', () => {
|
|
81
|
+
const doc = new Y.Doc();
|
|
82
|
+
const { rerender } = render(
|
|
83
|
+
<RichTextEditor allowCollaboration content={null} yjsDoc={doc} />
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const initialDeps = lastCall()[1];
|
|
87
|
+
|
|
88
|
+
// `collaborationUser` is a fresh object on every render in the task dialog;
|
|
89
|
+
// it must not be part of the dependency array or the editor would be
|
|
90
|
+
// rebuilt (and the caret reset) on every render.
|
|
91
|
+
rerender(
|
|
92
|
+
<RichTextEditor
|
|
93
|
+
allowCollaboration
|
|
94
|
+
collaborationUser={{ color: '#fff', name: 'Ada' }}
|
|
95
|
+
content={null}
|
|
96
|
+
yjsDoc={doc}
|
|
97
|
+
/>
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
expect(lastCall()[1]).toEqual(initialDeps);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// The provider only drives CollaborationCaret (cosmetic remote cursors).
|
|
104
|
+
// Rebuilding a live editor for it would tear down the ProseMirror view and
|
|
105
|
+
// re-run the Yjs binding over the whole document mid-session — expensive and
|
|
106
|
+
// user-visible on a large document, for no content benefit.
|
|
107
|
+
it('does not rebuild the editor when only the provider arrives', () => {
|
|
108
|
+
const doc = new Y.Doc();
|
|
109
|
+
const provider = { awareness: { setLocalStateField: () => {} } };
|
|
110
|
+
const { rerender } = render(
|
|
111
|
+
<RichTextEditor allowCollaboration content={null} yjsDoc={doc} />
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const initialDeps = lastCall()[1];
|
|
115
|
+
|
|
116
|
+
rerender(
|
|
117
|
+
<RichTextEditor
|
|
118
|
+
allowCollaboration
|
|
119
|
+
collaborationUser={{ color: '#fff', name: 'Ada' }}
|
|
120
|
+
content={null}
|
|
121
|
+
yjsDoc={doc}
|
|
122
|
+
yjsProvider={provider as never}
|
|
123
|
+
/>
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
expect(lastCall()[1]).toEqual(initialDeps);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('RichTextEditor toolbar reveal', () => {
|
|
131
|
+
beforeEach(() => {
|
|
132
|
+
useEditorMock.mockClear();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// The toolbar must reveal on focus *within* the editor wrapper, not on editor
|
|
136
|
+
// focus alone: clicking a toolbar button moves focus out of the text, so a
|
|
137
|
+
// stricter rule would hide the toolbar on mousedown and swallow the click.
|
|
138
|
+
it('hides the toolbar until focus lands inside the editor', () => {
|
|
139
|
+
const { container } = render(
|
|
140
|
+
<RichTextEditor content={null} revealToolbarOnFocus />
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const toolbar = container.querySelector('[data-testid="fixed-toolbar"]');
|
|
144
|
+
expect(toolbar?.className).toContain('opacity-0');
|
|
145
|
+
expect(toolbar?.className).toContain('group-focus-within:opacity-100');
|
|
146
|
+
// Inert while hidden, so it cannot show hover states or tooltips; the
|
|
147
|
+
// wrapper turns a press in that strip into editor focus instead.
|
|
148
|
+
expect(toolbar?.className).toContain('pointer-events-none');
|
|
149
|
+
expect(toolbar?.className).toContain(
|
|
150
|
+
'group-focus-within:pointer-events-auto'
|
|
151
|
+
);
|
|
152
|
+
expect(container.firstElementChild?.className).toContain('group');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('leaves the toolbar visible by default', () => {
|
|
156
|
+
const { container } = render(<RichTextEditor content={null} />);
|
|
157
|
+
|
|
158
|
+
const toolbar = container.querySelector('[data-testid="fixed-toolbar"]');
|
|
159
|
+
expect(toolbar?.className ?? '').not.toContain('opacity-0');
|
|
160
|
+
});
|
|
161
|
+
});
|