@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.
Files changed (52) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/biome.json +1 -1
  3. package/package.json +60 -48
  4. package/src/components/ui/badge.tsx +2 -0
  5. package/src/components/ui/button.tsx +2 -0
  6. package/src/components/ui/custom/__tests__/settings-dialog-shell.test.tsx +48 -4
  7. package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +33 -0
  8. package/src/components/ui/custom/__tests__/workspace-select-reveal.test.tsx +88 -0
  9. package/src/components/ui/custom/animated-slot-text.tsx +20 -0
  10. package/src/components/ui/custom/common-footer.tsx +262 -237
  11. package/src/components/ui/custom/language-dropdown-item.tsx +3 -10
  12. package/src/components/ui/custom/language-toggle.test.tsx +30 -0
  13. package/src/components/ui/custom/language-toggle.tsx +11 -10
  14. package/src/components/ui/custom/locale-preference.ts +32 -0
  15. package/src/components/ui/custom/settings/appearance-settings.tsx +5 -13
  16. package/src/components/ui/custom/settings-dialog-shell.tsx +40 -9
  17. package/src/components/ui/custom/structure.tsx +12 -0
  18. package/src/components/ui/custom/system-language-dropdown-item.tsx +3 -6
  19. package/src/components/ui/custom/workspace-access/adapters.test.ts +10 -1
  20. package/src/components/ui/custom/workspace-access/adapters.ts +36 -4
  21. package/src/components/ui/custom/workspace-access/member-filter-utils.test.ts +14 -0
  22. package/src/components/ui/custom/workspace-access/member-filter-utils.ts +8 -0
  23. package/src/components/ui/custom/workspace-access/types.ts +20 -0
  24. package/src/components/ui/custom/workspace-access/workspace-access-context.test.tsx +111 -0
  25. package/src/components/ui/custom/workspace-access/workspace-access-default-role-card.tsx +15 -5
  26. package/src/components/ui/custom/workspace-access/workspace-access-invite-dialog.tsx +155 -48
  27. package/src/components/ui/custom/workspace-access/workspace-access-member-profile-dialog.tsx +92 -0
  28. package/src/components/ui/custom/workspace-access/workspace-access-member-row.tsx +171 -76
  29. package/src/components/ui/custom/workspace-access/workspace-access-members.tsx +11 -2
  30. package/src/components/ui/custom/workspace-access/workspace-access-page-header.tsx +11 -11
  31. package/src/components/ui/custom/workspace-access/workspace-access-page.tsx +180 -22
  32. package/src/components/ui/custom/workspace-access/workspace-access-people-filters.tsx +29 -15
  33. package/src/components/ui/custom/workspace-access/workspace-access-permission-checklist.tsx +56 -10
  34. package/src/components/ui/custom/workspace-access/workspace-access-permission-preview.test.ts +33 -0
  35. package/src/components/ui/custom/workspace-access/workspace-access-permission-preview.tsx +24 -1
  36. package/src/components/ui/custom/workspace-access/workspace-access-responsive.test.ts +64 -0
  37. package/src/components/ui/custom/workspace-access/workspace-access-role-editor-dialog.tsx +28 -16
  38. package/src/components/ui/custom/workspace-access/workspace-access-roles.tsx +35 -11
  39. package/src/components/ui/custom/workspace-access/workspace-access-tabs-toolbar.tsx +23 -11
  40. package/src/components/ui/custom/workspace-select-helpers.ts +5 -3
  41. package/src/components/ui/custom/workspace-select-reveal.tsx +46 -0
  42. package/src/components/ui/custom/workspace-select.tsx +13 -5
  43. package/src/components/ui/finance/shared/charts/monthly-total-chart-client.tsx +1 -1
  44. package/src/components/ui/finance/shared/charts/monthly-total-chart.tsx +1 -1
  45. package/src/components/ui/storefront/cart-summary.tsx +12 -2
  46. package/src/components/ui/storefront/storefront-surface.test.tsx +21 -0
  47. package/src/components/ui/storefront/storefront-surface.tsx +6 -0
  48. package/src/components/ui/text-editor/__tests__/collaboration-binding.test.tsx +161 -0
  49. package/src/components/ui/text-editor/editor.tsx +267 -235
  50. package/src/globals.css +166 -0
  51. package/src/hooks/__tests__/use-workspace-identity-mutation.test.tsx +181 -0
  52. 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
+ });