shutro-erp 0.3.2 → 0.3.3
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/account_portal/.kosh/bindings.js +1 -1
- package/dist/account_portal/.kosh/schema.d.ts +29 -1
- package/dist/accounting/.kosh/bindings.js +1 -1
- package/dist/accounting/.kosh/schema.d.ts +29 -1
- package/dist/assistant/index.js +1 -1
- package/dist/contacts/.kosh/bindings.js +1 -1
- package/dist/contacts/.kosh/schema.d.ts +29 -1
- package/dist/l10n_bd/.kosh/bindings.js +1 -1
- package/dist/l10n_bd/.kosh/schema.d.ts +29 -1
- package/dist/l10n_in/.kosh/bindings.js +1 -1
- package/dist/l10n_in/.kosh/schema.d.ts +29 -4
- package/dist/l10n_in/module.d.ts +0 -62
- package/dist/l10n_in/module.js +2 -4
- package/dist/payment/.kosh/bindings.js +1 -1
- package/dist/payment/.kosh/schema.d.ts +30 -2
- package/dist/payment/bkash.d.ts +28 -0
- package/dist/payment/bkash.js +169 -0
- package/dist/payment/index.d.ts +4 -0
- package/dist/payment/index.js +80 -24
- package/dist/payment/module.d.ts +7 -4
- package/dist/payment/module.js +3 -0
- package/dist/payment/money.d.ts +7 -0
- package/dist/payment/money.js +37 -0
- package/dist/payment/providers.d.ts +13 -2
- package/dist/payment/razorpay.d.ts +12 -0
- package/dist/payment/razorpay.js +149 -0
- package/dist/payment/sslcommerz.js +1 -1
- package/dist/payment/stripe.d.ts +15 -0
- package/dist/payment/stripe.js +182 -0
- package/dist/permissions/admin.d.ts +3 -0
- package/dist/permissions/admin.js +108 -10
- package/dist/permissions/index.d.ts +1 -0
- package/dist/permissions/index.js +3 -0
- package/dist/permissions/roles.d.ts +2 -0
- package/dist/permissions/roles.js +236 -0
- package/dist/portal/.kosh/bindings.js +1 -1
- package/dist/portal/.kosh/schema.d.ts +29 -1
- package/dist/product/.kosh/bindings.js +1 -1
- package/dist/product/.kosh/schema.d.ts +29 -1
- package/dist/project/.kosh/bindings.js +1 -1
- package/dist/project/.kosh/schema.d.ts +29 -1
- package/dist/project_portal/.kosh/bindings.js +1 -1
- package/dist/project_portal/.kosh/schema.d.ts +29 -1
- package/dist/purchase/.kosh/bindings.js +1 -1
- package/dist/purchase/.kosh/schema.d.ts +29 -1
- package/dist/purchase_account/.kosh/bindings.js +1 -1
- package/dist/purchase_account/.kosh/schema.d.ts +29 -1
- package/dist/purchase_portal/.kosh/bindings.js +1 -1
- package/dist/purchase_portal/.kosh/schema.d.ts +29 -1
- package/dist/sale_account/.kosh/bindings.js +1 -1
- package/dist/sale_account/.kosh/schema.d.ts +29 -1
- package/dist/sale_portal/.kosh/bindings.js +1 -1
- package/dist/sale_portal/.kosh/schema.d.ts +29 -1
- package/dist/sale_project/.kosh/bindings.js +1 -1
- package/dist/sale_project/.kosh/schema.d.ts +29 -1
- package/dist/sales/.kosh/bindings.js +1 -1
- package/dist/sales/.kosh/schema.d.ts +29 -1
- package/dist/server/auth/base.js +22 -0
- package/dist/server/auth/routes.d.ts +3 -0
- package/dist/server/auth/routes.js +29 -0
- package/dist/server/auth/service.d.ts +52 -0
- package/dist/server/auth/service.js +168 -3
- package/dist/server/identity.d.ts +2 -0
- package/dist/server/index.d.ts +1 -1
- package/dist/server/server.js +11 -3
- package/dist/settings/company.d.ts +34 -0
- package/dist/settings/company.js +187 -0
- package/dist/settings/index.d.ts +10 -10
- package/dist/settings/index.js +159 -30
- package/dist/settings/people.d.ts +16 -0
- package/dist/settings/people.js +505 -0
- package/dist/settings/security.d.ts +13 -0
- package/dist/settings/security.js +158 -0
- package/dist/settings/web/AuditPage.d.ts +8 -0
- package/dist/settings/web/AuditPage.js +74 -0
- package/dist/settings/web/CompaniesPage.d.ts +8 -0
- package/dist/settings/web/CompaniesPage.js +52 -0
- package/dist/settings/web/CompanyPage.d.ts +9 -0
- package/dist/settings/web/CompanyPage.js +94 -0
- package/dist/settings/web/OverviewPage.d.ts +9 -0
- package/dist/settings/web/OverviewPage.js +66 -0
- package/dist/settings/web/PeoplePage.d.ts +9 -0
- package/dist/settings/web/PeoplePage.js +163 -0
- package/dist/settings/web/ProfilePage.d.ts +14 -0
- package/dist/settings/web/ProfilePage.js +194 -0
- package/dist/settings/web/RolesEditor.d.ts +35 -0
- package/dist/settings/web/RolesEditor.js +49 -0
- package/dist/settings/web/RolesPage.d.ts +9 -0
- package/dist/settings/web/RolesPage.js +296 -0
- package/dist/settings/web/SecurityPage.d.ts +8 -0
- package/dist/settings/web/SecurityPage.js +81 -0
- package/dist/settings/web/index.d.ts +9 -0
- package/dist/settings/web/index.js +26 -1
- package/dist/settings/web/messages.js +566 -0
- package/dist/ui/api.d.ts +4 -0
- package/dist/ui/app.d.ts +5 -0
- package/dist/ui/app.js +108 -31
- package/dist/ui/components/primitives.d.ts +9 -1
- package/dist/ui/components/primitives.js +12 -4
- package/dist/ui/context.d.ts +10 -1
- package/dist/ui/context.js +60 -2
- package/dist/ui/i18n/messages.d.ts +46 -0
- package/dist/ui/i18n/messages.js +92 -0
- package/dist/ui/index.d.ts +2 -1
- package/dist/ui/index.js +2 -1
- package/dist/ui/lib/accent.d.ts +13 -0
- package/dist/ui/lib/accent.js +36 -0
- package/dist/ui/shell/Activate.d.ts +6 -0
- package/dist/ui/shell/Activate.js +39 -0
- package/dist/ui/shell/CommandPalette.d.ts +7 -1
- package/dist/ui/shell/CommandPalette.js +16 -2
- package/dist/ui/shell/Sidebar.d.ts +27 -0
- package/dist/ui/shell/Sidebar.js +84 -0
- package/dist/ui/shell/TwoFactorSetup.d.ts +5 -0
- package/dist/ui/shell/TwoFactorSetup.js +32 -0
- package/dist/ui/shell/shortcuts.d.ts +3 -0
- package/dist/ui/shell/shortcuts.js +13 -4
- package/dist/ui/styles.css +70 -28
- package/dist/ui/views/DateField.d.ts +10 -0
- package/dist/ui/views/DateField.js +129 -0
- package/dist/ui/views/FormView.js +161 -21
- package/dist/ui/views/ListView.d.ts +2 -0
- package/dist/ui/views/ListView.js +8 -3
- package/dist/ui/views/StatusChip.d.ts +10 -0
- package/dist/ui/views/StatusChip.js +22 -0
- package/dist/ui/views/widgets.d.ts +3 -0
- package/dist/ui/views/widgets.js +26 -8
- package/dist/views/types.d.ts +7 -0
- package/dist/views/validate.js +4 -0
- package/package.json +2 -2
package/dist/ui/i18n/messages.js
CHANGED
|
@@ -12,6 +12,14 @@ export const en = {
|
|
|
12
12
|
unknownPage: 'The page "{{page}}" is not part of this app. Register it with registry.registerPage.',
|
|
13
13
|
},
|
|
14
14
|
auth: {
|
|
15
|
+
activateTitle: 'Choose your password',
|
|
16
|
+
activateIntro: 'You were invited to Shutro ERP. Choose a password to start.',
|
|
17
|
+
activateNoToken: 'This link is incomplete: open the link from your invitation.',
|
|
18
|
+
newPassword: 'Password',
|
|
19
|
+
repeatPassword: 'Repeat the password',
|
|
20
|
+
passwordRule: 'At least 12 characters.',
|
|
21
|
+
mismatch: 'The passwords are not the same.',
|
|
22
|
+
activate: 'Save and sign in',
|
|
15
23
|
signIn: 'Sign in',
|
|
16
24
|
login: 'Login',
|
|
17
25
|
password: 'Password',
|
|
@@ -21,11 +29,32 @@ export const en = {
|
|
|
21
29
|
signOut: 'Sign out',
|
|
22
30
|
invalid: 'The login or password is wrong.',
|
|
23
31
|
adminNeeds2fa: 'Administrator rights apply after you verify a second factor.',
|
|
32
|
+
setupTitle: 'Set up two-factor sign-in',
|
|
33
|
+
setupIntro: 'This company requires a second step when you sign in: a code from an authenticator app on your phone. Set it up to continue.',
|
|
34
|
+
setupStart: 'Set it up',
|
|
35
|
+
setupStep1: 'Add this account to your authenticator app.',
|
|
36
|
+
setupOpenApp: 'Open in the app',
|
|
37
|
+
setupStep2: 'Or type this key:',
|
|
38
|
+
setupStep3: 'Enter the 6-digit code it shows.',
|
|
39
|
+
setupTurnOn: 'Turn on and continue',
|
|
40
|
+
setupCodes: 'Keep these recovery codes somewhere safe: each signs you in once if you lose your phone.',
|
|
41
|
+
setupContinue: 'Continue',
|
|
24
42
|
},
|
|
25
43
|
shell: {
|
|
26
44
|
apps: 'Apps',
|
|
27
45
|
menu: 'Main menu',
|
|
28
46
|
theme: 'Theme',
|
|
47
|
+
density: 'Density',
|
|
48
|
+
home: 'Home',
|
|
49
|
+
appNamed: '{{app}} app',
|
|
50
|
+
switchCompany: 'Company: {{name}} (switch)',
|
|
51
|
+
sidebar: 'Sidebar',
|
|
52
|
+
collapseSidebar: 'Collapse the sidebar',
|
|
53
|
+
expandSidebar: 'Expand the sidebar',
|
|
54
|
+
openMenu: 'Open the menu',
|
|
55
|
+
closeMenu: 'Close the menu',
|
|
56
|
+
menuHint: 'Apps and their sections',
|
|
57
|
+
location: 'You are here',
|
|
29
58
|
light: 'Light',
|
|
30
59
|
dark: 'Dark',
|
|
31
60
|
system: 'System',
|
|
@@ -62,6 +91,20 @@ export const en = {
|
|
|
62
91
|
exportFailed: 'The export failed: {{reason}}',
|
|
63
92
|
columns: 'Columns',
|
|
64
93
|
density: 'Density',
|
|
94
|
+
statusOf: '{{field}}: {{state}}',
|
|
95
|
+
nextState: 'Next',
|
|
96
|
+
recordActions: 'Record actions',
|
|
97
|
+
summary: 'Summary',
|
|
98
|
+
details: 'Details',
|
|
99
|
+
recordPanel: 'About this record',
|
|
100
|
+
showPanel: 'Show the panel',
|
|
101
|
+
hidePanel: 'Hide the panel',
|
|
102
|
+
previousMonth: 'Previous month',
|
|
103
|
+
nextMonth: 'Next month',
|
|
104
|
+
pickDate: 'Pick a date',
|
|
105
|
+
datePlaceholder: 'Date',
|
|
106
|
+
today: 'Today',
|
|
107
|
+
clear: 'Clear',
|
|
65
108
|
proposed: 'Proposed',
|
|
66
109
|
design: { addField: 'Add field', addColumn: 'Add column' },
|
|
67
110
|
compact: 'Compact',
|
|
@@ -191,6 +234,8 @@ export const en = {
|
|
|
191
234
|
},
|
|
192
235
|
toast: { region: 'Notifications', close: 'Close' },
|
|
193
236
|
palette: {
|
|
237
|
+
switchCompany: 'Switch to {{name}}',
|
|
238
|
+
profile: 'Your profile',
|
|
194
239
|
button: 'Search or jump to…',
|
|
195
240
|
title: 'Command palette',
|
|
196
241
|
description: 'Search records, menus and commands. Use the arrow keys and Enter.',
|
|
@@ -209,6 +254,7 @@ export const en = {
|
|
|
209
254
|
shortcuts: {
|
|
210
255
|
title: 'Keyboard shortcuts',
|
|
211
256
|
palette: 'Search or jump anywhere',
|
|
257
|
+
sidebar: 'Collapse or expand the sidebar',
|
|
212
258
|
search: 'Focus the search box',
|
|
213
259
|
new: 'New record',
|
|
214
260
|
views: 'Switch view',
|
|
@@ -247,6 +293,14 @@ export const bn = {
|
|
|
247
293
|
unknownPage: '"{{page}}" পৃষ্ঠাটি এই অ্যাপে নেই। registry.registerPage দিয়ে নিবন্ধন করুন।',
|
|
248
294
|
},
|
|
249
295
|
auth: {
|
|
296
|
+
activateTitle: 'আপনার পাসওয়ার্ড বেছে নিন',
|
|
297
|
+
activateIntro: 'আপনাকে Shutro ERP-এ আমন্ত্রণ জানানো হয়েছে। শুরু করতে একটি পাসওয়ার্ড বেছে নিন।',
|
|
298
|
+
activateNoToken: 'লিংকটি অসম্পূর্ণ: আমন্ত্রণের লিংকটি খুলুন।',
|
|
299
|
+
newPassword: 'পাসওয়ার্ড',
|
|
300
|
+
repeatPassword: 'পাসওয়ার্ড আবার লিখুন',
|
|
301
|
+
passwordRule: 'কমপক্ষে ১২ অক্ষর।',
|
|
302
|
+
mismatch: 'পাসওয়ার্ড দুটি মেলেনি।',
|
|
303
|
+
activate: 'সংরক্ষণ করে সাইন ইন করুন',
|
|
250
304
|
signIn: 'সাইন ইন',
|
|
251
305
|
login: 'লগইন',
|
|
252
306
|
password: 'পাসওয়ার্ড',
|
|
@@ -256,11 +310,32 @@ export const bn = {
|
|
|
256
310
|
signOut: 'সাইন আউট',
|
|
257
311
|
invalid: 'লগইন বা পাসওয়ার্ড ভুল।',
|
|
258
312
|
adminNeeds2fa: 'দ্বিতীয় ধাপ যাচাইয়ের পর অ্যাডমিনিস্ট্রেটরের অধিকার কার্যকর হবে।',
|
|
313
|
+
setupTitle: 'দুই-ধাপের সাইন-ইন চালু করুন',
|
|
314
|
+
setupIntro: 'এই প্রতিষ্ঠানে সাইন-ইনের সময় একটি দ্বিতীয় ধাপ লাগে: ফোনের অথেনটিকেটর অ্যাপের একটি কোড। চালিয়ে যেতে এটি চালু করুন।',
|
|
315
|
+
setupStart: 'চালু করুন',
|
|
316
|
+
setupStep1: 'অ্যাকাউন্টটি আপনার অথেনটিকেটর অ্যাপে যোগ করুন।',
|
|
317
|
+
setupOpenApp: 'অ্যাপে খুলুন',
|
|
318
|
+
setupStep2: 'অথবা এই কী লিখুন:',
|
|
319
|
+
setupStep3: 'অ্যাপে দেখানো ৬-সংখ্যার কোড দিন।',
|
|
320
|
+
setupTurnOn: 'চালু করে এগিয়ে যান',
|
|
321
|
+
setupCodes: 'এই রিকভারি কোডগুলো নিরাপদে রাখুন: ফোন হারালে প্রতিটি একবার সাইন-ইন করায়।',
|
|
322
|
+
setupContinue: 'এগিয়ে যান',
|
|
259
323
|
},
|
|
260
324
|
shell: {
|
|
261
325
|
apps: 'অ্যাপস',
|
|
262
326
|
menu: 'প্রধান মেনু',
|
|
263
327
|
theme: 'থিম',
|
|
328
|
+
density: 'ঘনত্ব',
|
|
329
|
+
home: 'হোম',
|
|
330
|
+
appNamed: '{{app}} অ্যাপ',
|
|
331
|
+
switchCompany: 'প্রতিষ্ঠান: {{name}} (বদলান)',
|
|
332
|
+
sidebar: 'সাইডবার',
|
|
333
|
+
collapseSidebar: 'সাইডবার ছোট করুন',
|
|
334
|
+
expandSidebar: 'সাইডবার বড় করুন',
|
|
335
|
+
openMenu: 'মেনু খুলুন',
|
|
336
|
+
closeMenu: 'মেনু বন্ধ করুন',
|
|
337
|
+
menuHint: 'অ্যাপ ও তাদের অংশ',
|
|
338
|
+
location: 'আপনি এখানে আছেন',
|
|
264
339
|
light: 'লাইট',
|
|
265
340
|
dark: 'ডার্ক',
|
|
266
341
|
system: 'সিস্টেম',
|
|
@@ -297,6 +372,20 @@ export const bn = {
|
|
|
297
372
|
exportFailed: 'এক্সপোর্ট ব্যর্থ: {{reason}}',
|
|
298
373
|
columns: 'কলাম',
|
|
299
374
|
density: 'ঘনত্ব',
|
|
375
|
+
statusOf: '{{field}}: {{state}}',
|
|
376
|
+
nextState: 'পরবর্তী',
|
|
377
|
+
recordActions: 'রেকর্ডের কাজ',
|
|
378
|
+
summary: 'সারাংশ',
|
|
379
|
+
details: 'বিস্তারিত',
|
|
380
|
+
recordPanel: 'এই রেকর্ড সম্পর্কে',
|
|
381
|
+
showPanel: 'প্যানেল দেখান',
|
|
382
|
+
hidePanel: 'প্যানেল লুকান',
|
|
383
|
+
previousMonth: 'আগের মাস',
|
|
384
|
+
nextMonth: 'পরের মাস',
|
|
385
|
+
pickDate: 'তারিখ বাছুন',
|
|
386
|
+
datePlaceholder: 'তারিখ',
|
|
387
|
+
today: 'আজ',
|
|
388
|
+
clear: 'মুছে ফেলুন',
|
|
300
389
|
proposed: 'প্রস্তাবিত',
|
|
301
390
|
design: { addField: 'ফিল্ড যোগ করুন', addColumn: 'কলাম যোগ করুন' },
|
|
302
391
|
compact: 'সংক্ষিপ্ত',
|
|
@@ -423,6 +512,8 @@ export const bn = {
|
|
|
423
512
|
},
|
|
424
513
|
toast: { region: 'বিজ্ঞপ্তি', close: 'বন্ধ করুন' },
|
|
425
514
|
palette: {
|
|
515
|
+
switchCompany: '{{name}}-এ যান',
|
|
516
|
+
profile: 'আপনার প্রোফাইল',
|
|
426
517
|
button: 'খুঁজুন বা যান…',
|
|
427
518
|
title: 'কমান্ড প্যালেট',
|
|
428
519
|
description: 'রেকর্ড, মেনু ও কমান্ড খুঁজুন। তীর কী ও এন্টার ব্যবহার করুন।',
|
|
@@ -441,6 +532,7 @@ export const bn = {
|
|
|
441
532
|
shortcuts: {
|
|
442
533
|
title: 'কীবোর্ড শর্টকাট',
|
|
443
534
|
palette: 'যেকোনো জায়গায় খুঁজুন বা যান',
|
|
535
|
+
sidebar: 'সাইডবার ছোট বা বড় করুন',
|
|
444
536
|
search: 'সার্চ বক্সে যান',
|
|
445
537
|
new: 'নতুন রেকর্ড',
|
|
446
538
|
views: 'ভিউ বদলান',
|
package/dist/ui/index.d.ts
CHANGED
|
@@ -8,13 +8,14 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { Registry } from './registry.js';
|
|
10
10
|
export { ApiError, type ApiOptions, type AppEntry, type CurrentUser, createKoshApi, type KoshApi, type ListQuery, type MenuNode, NetworkError, type Problem, type Row, type ServedView, type Versioned, type ViewField, } from './api.js';
|
|
11
|
-
export { KoshApp, type KoshAppProps } from './app.js';
|
|
11
|
+
export { KoshApp, type KoshAppProps, useLanguage } from './app.js';
|
|
12
12
|
export { AppIcon } from './components/AppIcon.js';
|
|
13
13
|
export * from './components/primitives.js';
|
|
14
14
|
export { type KoshContextValue, KoshProvider, type ThemeChoice, useKosh, useTheme, } from './context.js';
|
|
15
15
|
export { type DesignContextValue, DesignProvider, type DesignSpot, useDesign, } from './design.js';
|
|
16
16
|
export { addMessages, createI18n, directionOf, NAMESPACE } from './i18n/index.js';
|
|
17
17
|
export { LANGUAGES } from './i18n/messages.js';
|
|
18
|
+
export { accentVariables, hexToOklch } from './lib/accent.js';
|
|
18
19
|
export { cn } from './lib/cn.js';
|
|
19
20
|
export { bcp47, createFormatter, type Formatter } from './lib/format.js';
|
|
20
21
|
export { type LiveRecord, useLiveModel, useLiveRecord } from './live.js';
|
package/dist/ui/index.js
CHANGED
|
@@ -12,13 +12,14 @@ import { KanbanView } from './views/KanbanView.js';
|
|
|
12
12
|
import { ListView } from './views/ListView.js';
|
|
13
13
|
import { registerDefaultWidgets } from './views/widgets.js';
|
|
14
14
|
export { ApiError, createKoshApi, NetworkError, } from './api.js';
|
|
15
|
-
export { KoshApp } from './app.js';
|
|
15
|
+
export { KoshApp, useLanguage } from './app.js';
|
|
16
16
|
export { AppIcon } from './components/AppIcon.js';
|
|
17
17
|
export * from './components/primitives.js';
|
|
18
18
|
export { KoshProvider, useKosh, useTheme, } from './context.js';
|
|
19
19
|
export { DesignProvider, useDesign, } from './design.js';
|
|
20
20
|
export { addMessages, createI18n, directionOf, NAMESPACE } from './i18n/index.js';
|
|
21
21
|
export { LANGUAGES } from './i18n/messages.js';
|
|
22
|
+
export { accentVariables, hexToOklch } from './lib/accent.js';
|
|
22
23
|
export { cn } from './lib/cn.js';
|
|
23
24
|
export { bcp47, createFormatter } from './lib/format.js';
|
|
24
25
|
export { useLiveModel, useLiveRecord } from './live.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A company's brand colour as the back-office's accent (RFC 0018 §4.5). Only its hue and chroma are used: the
|
|
3
|
+
* themes fix the accent's lightness (light: dark enough for white text; dark: light enough for dark text), so any
|
|
4
|
+
* brand colour keeps text on buttons and links readable (WCAG AA) in both.
|
|
5
|
+
*/
|
|
6
|
+
/** OKLCH of an sRGB hex colour (`#4f46e5` → `{ l, c, h }`), or null when it is not one. */
|
|
7
|
+
export declare function hexToOklch(hex: string): {
|
|
8
|
+
l: number;
|
|
9
|
+
c: number;
|
|
10
|
+
h: number;
|
|
11
|
+
} | null;
|
|
12
|
+
/** The CSS variables a brand colour sets (`--accent-h`, `--accent-c`), or none for the default accent. */
|
|
13
|
+
export declare function accentVariables(hex: string | null | undefined): Record<string, string>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A company's brand colour as the back-office's accent (RFC 0018 §4.5). Only its hue and chroma are used: the
|
|
3
|
+
* themes fix the accent's lightness (light: dark enough for white text; dark: light enough for dark text), so any
|
|
4
|
+
* brand colour keeps text on buttons and links readable (WCAG AA) in both.
|
|
5
|
+
*/
|
|
6
|
+
/** OKLCH of an sRGB hex colour (`#4f46e5` → `{ l, c, h }`), or null when it is not one. */
|
|
7
|
+
export function hexToOklch(hex) {
|
|
8
|
+
const m = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex);
|
|
9
|
+
if (m === null)
|
|
10
|
+
return null;
|
|
11
|
+
const linear = (v) => {
|
|
12
|
+
const x = Number.parseInt(v, 16) / 255;
|
|
13
|
+
return x <= 0.04045 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4;
|
|
14
|
+
};
|
|
15
|
+
const [r, g, b] = [linear(m[1]), linear(m[2]), linear(m[3])];
|
|
16
|
+
const l_ = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
|
|
17
|
+
const m_ = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
|
|
18
|
+
const s_ = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
|
|
19
|
+
const L = 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_;
|
|
20
|
+
const A = 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_;
|
|
21
|
+
const B = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_;
|
|
22
|
+
const c = Math.sqrt(A * A + B * B);
|
|
23
|
+
const h = (Math.atan2(B, A) * 180) / Math.PI;
|
|
24
|
+
return { l: L, c, h: h < 0 ? h + 360 : h };
|
|
25
|
+
}
|
|
26
|
+
/** The CSS variables a brand colour sets (`--accent-h`, `--accent-c`), or none for the default accent. */
|
|
27
|
+
export function accentVariables(hex) {
|
|
28
|
+
const color = hex === null || hex === undefined ? null : hexToOklch(hex);
|
|
29
|
+
if (color === null)
|
|
30
|
+
return {};
|
|
31
|
+
// Chroma is capped (very saturated colours glare as large surfaces); greys stay grey.
|
|
32
|
+
return {
|
|
33
|
+
'--accent-h': color.h.toFixed(1),
|
|
34
|
+
'--accent-c': Math.min(color.c, 0.2).toFixed(3),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The invitation page (RFC 0019 §3.2): an invited person chooses their password (at least 12 characters) and is
|
|
4
|
+
* signed in. A used, replaced or expired link says so.
|
|
5
|
+
*/
|
|
6
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
7
|
+
import { useId, useState } from 'react';
|
|
8
|
+
import { useTranslation } from 'react-i18next';
|
|
9
|
+
import { ApiError } from '../api.js';
|
|
10
|
+
import { Alert, Button, Card, Input, Label } from '../components/primitives.js';
|
|
11
|
+
export function Activate({ api, onSignedIn, }) {
|
|
12
|
+
const { t } = useTranslation();
|
|
13
|
+
const queryClient = useQueryClient();
|
|
14
|
+
const id = useId();
|
|
15
|
+
const token = new URLSearchParams(window.location.search).get('token') ?? '';
|
|
16
|
+
const [password, setPassword] = useState('');
|
|
17
|
+
const [repeat, setRepeat] = useState('');
|
|
18
|
+
const activate = useMutation({
|
|
19
|
+
mutationFn: async () => {
|
|
20
|
+
const { login } = await api.call('POST', '/api/v1/auth/activate', {
|
|
21
|
+
token,
|
|
22
|
+
password,
|
|
23
|
+
});
|
|
24
|
+
await api.login(login, password);
|
|
25
|
+
},
|
|
26
|
+
onSuccess: async () => {
|
|
27
|
+
await queryClient.invalidateQueries();
|
|
28
|
+
onSignedIn();
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
const mismatch = repeat !== '' && repeat !== password;
|
|
32
|
+
return (_jsx("main", { className: "grid min-h-screen place-items-center bg-background p-4", children: _jsxs(Card, { className: "w-full max-w-sm p-6", children: [_jsx("h1", { className: "mb-1 text-xl font-semibold", children: t('auth.activateTitle') }), _jsx("p", { className: "mb-5 text-sm text-muted-foreground", children: t('auth.activateIntro') }), token === '' ? (_jsx(Alert, { children: t('auth.activateNoToken') })) : (_jsxs("form", { className: "grid gap-4", onSubmit: (e) => {
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
if (!mismatch && password.length >= 12)
|
|
35
|
+
activate.mutate();
|
|
36
|
+
}, children: [activate.error === null ? null : (_jsx(Alert, { children: activate.error instanceof ApiError
|
|
37
|
+
? activate.error.message
|
|
38
|
+
: String(activate.error) })), _jsxs("div", { className: "grid gap-1", children: [_jsx(Label, { htmlFor: `${id}-password`, children: t('auth.newPassword') }), _jsx(Input, { id: `${id}-password`, type: "password", autoComplete: "new-password", minLength: 12, required: true, value: password, onChange: (e) => setPassword(e.target.value) }), _jsx("p", { className: "text-xs text-muted-foreground", children: t('auth.passwordRule') })] }), _jsxs("div", { className: "grid gap-1", children: [_jsx(Label, { htmlFor: `${id}-repeat`, children: t('auth.repeatPassword') }), _jsx(Input, { id: `${id}-repeat`, type: "password", autoComplete: "new-password", required: true, value: repeat, "aria-invalid": mismatch ? true : undefined, onChange: (e) => setRepeat(e.target.value) }), mismatch ? _jsx("p", { className: "text-xs text-danger", children: t('auth.mismatch') }) : null] }), _jsx(Button, { type: "submit", variant: "primary", busy: activate.isPending, disabled: password.length < 12 || mismatch, children: t('auth.activate') })] }))] }) }));
|
|
39
|
+
}
|
|
@@ -3,9 +3,15 @@ import type { KoshApi } from '../api.js';
|
|
|
3
3
|
/** Opens the palette from anywhere in the shell (the home page's search box). */
|
|
4
4
|
export declare const PaletteContext: Context<() => void>;
|
|
5
5
|
export declare function usePalette(): () => void;
|
|
6
|
-
export declare function CommandPalette({ open, onOpenChange, api, onShortcuts, }: {
|
|
6
|
+
export declare function CommandPalette({ open, onOpenChange, api, onShortcuts, companies, onSwitchCompany, }: {
|
|
7
7
|
readonly open: boolean;
|
|
8
8
|
readonly onOpenChange: (open: boolean) => void;
|
|
9
9
|
readonly api: KoshApi;
|
|
10
10
|
readonly onShortcuts: () => void;
|
|
11
|
+
/** The user's other companies, offered as "Switch to …" (RFC 0019 §3.4). */
|
|
12
|
+
readonly companies?: readonly {
|
|
13
|
+
readonly id: number;
|
|
14
|
+
readonly name: string;
|
|
15
|
+
}[];
|
|
16
|
+
readonly onSwitchCompany?: (id: number) => void;
|
|
11
17
|
}): ReactElement;
|
|
@@ -7,7 +7,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
7
7
|
*/
|
|
8
8
|
import { useQuery } from '@tanstack/react-query';
|
|
9
9
|
import { useNavigate } from '@tanstack/react-router';
|
|
10
|
-
import { ArrowRight, FileText, History, Home, Keyboard, Monitor, Moon, Plus, Search, Sun, } from 'lucide-react';
|
|
10
|
+
import { ArrowRight, Building2, FileText, History, Home, Keyboard, Monitor, Moon, Plus, Search, Sun, UserRound, } from 'lucide-react';
|
|
11
11
|
import { Dialog as RDialog } from 'radix-ui';
|
|
12
12
|
import { createContext, useContext, useEffect, useId, useMemo, useRef, useState, } from 'react';
|
|
13
13
|
import { useTranslation } from 'react-i18next';
|
|
@@ -57,7 +57,7 @@ function score(text, query) {
|
|
|
57
57
|
return 2;
|
|
58
58
|
return t.includes(q) ? 1 : 0;
|
|
59
59
|
}
|
|
60
|
-
export function CommandPalette({ open, onOpenChange, api, onShortcuts, }) {
|
|
60
|
+
export function CommandPalette({ open, onOpenChange, api, onShortcuts, companies = [], onSwitchCompany, }) {
|
|
61
61
|
const { t } = useTranslation();
|
|
62
62
|
const { uid, setTheme } = useKosh();
|
|
63
63
|
const navigate = useNavigate();
|
|
@@ -199,6 +199,18 @@ export function CommandPalette({ open, onOpenChange, api, onShortcuts, }) {
|
|
|
199
199
|
icon: _jsx(Monitor, { className: "size-4", "aria-hidden": true }),
|
|
200
200
|
run: () => setTheme('system'),
|
|
201
201
|
},
|
|
202
|
+
...companies.map((c) => ({
|
|
203
|
+
id: `cmd:company:${c.id}`,
|
|
204
|
+
label: t('palette.switchCompany', { name: c.name }),
|
|
205
|
+
icon: _jsx(Building2, { className: "size-4", "aria-hidden": true }),
|
|
206
|
+
run: () => onSwitchCompany?.(c.id),
|
|
207
|
+
})),
|
|
208
|
+
{
|
|
209
|
+
id: 'cmd:profile',
|
|
210
|
+
label: t('palette.profile'),
|
|
211
|
+
icon: _jsx(UserRound, { className: "size-4", "aria-hidden": true }),
|
|
212
|
+
run: () => void navigate({ to: '/me/$page', params: { page: 'settings.Profile' } }),
|
|
213
|
+
},
|
|
202
214
|
{
|
|
203
215
|
id: 'cmd:shortcuts',
|
|
204
216
|
label: t('palette.shortcuts'),
|
|
@@ -230,6 +242,8 @@ export function CommandPalette({ open, onOpenChange, api, onShortcuts, }) {
|
|
|
230
242
|
setTheme,
|
|
231
243
|
t,
|
|
232
244
|
onShortcuts,
|
|
245
|
+
companies,
|
|
246
|
+
onSwitchCompany,
|
|
233
247
|
]);
|
|
234
248
|
useEffect(() => {
|
|
235
249
|
if (active >= items.length)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode } from 'react';
|
|
2
|
+
import type { AppEntry, KoshApi } from '../api.js';
|
|
3
|
+
/** Whether the sidebar is collapsed to its rail, remembered per browser. */
|
|
4
|
+
export declare function useSidebarCollapsed(): [boolean, (collapsed: boolean) => void];
|
|
5
|
+
export declare function Sidebar({ api, apps, current, company, collapsed, onCollapse, onSearch, top, bottom, onNavigate, }: {
|
|
6
|
+
readonly api: KoshApi;
|
|
7
|
+
readonly apps: readonly AppEntry[];
|
|
8
|
+
/** The open app (from the URL). */
|
|
9
|
+
readonly current: string | undefined;
|
|
10
|
+
readonly company: number;
|
|
11
|
+
readonly collapsed: boolean;
|
|
12
|
+
/** Absent in the drawer (it closes instead). */
|
|
13
|
+
readonly onCollapse?: ((collapsed: boolean) => void) | undefined;
|
|
14
|
+
readonly onSearch: () => void;
|
|
15
|
+
/** The company block. */
|
|
16
|
+
readonly top: ReactNode;
|
|
17
|
+
/** The user menu. */
|
|
18
|
+
readonly bottom: ReactNode;
|
|
19
|
+
/** After following a link (the drawer closes). */
|
|
20
|
+
readonly onNavigate?: (() => void) | undefined;
|
|
21
|
+
}): ReactElement;
|
|
22
|
+
/** The sidebar as a drawer on small screens. */
|
|
23
|
+
export declare function SidebarDrawer({ open, onOpenChange, children, }: {
|
|
24
|
+
readonly open: boolean;
|
|
25
|
+
readonly onOpenChange: (open: boolean) => void;
|
|
26
|
+
readonly children: ReactNode;
|
|
27
|
+
}): ReactElement;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The Workspace sidebar (RFC 0018 §4.1): the company at the top, search, Home, then the user's apps — each a button
|
|
4
|
+
* that opens its sections (the current app opens itself; one at a time) — and the user at the bottom. It collapses
|
|
5
|
+
* to an icon rail (`Ctrl+B`, remembered per browser) and becomes a drawer on small screens. The server decides which
|
|
6
|
+
* apps and menus the user sees; the sidebar shows them.
|
|
7
|
+
*/
|
|
8
|
+
import { useQuery } from '@tanstack/react-query';
|
|
9
|
+
import { Link, useNavigate } from '@tanstack/react-router';
|
|
10
|
+
import { ChevronDown, House, PanelLeftClose, PanelLeftOpen, Search, X } from 'lucide-react';
|
|
11
|
+
import { Dialog } from 'radix-ui';
|
|
12
|
+
import { useEffect, useState } from 'react';
|
|
13
|
+
import { useTranslation } from 'react-i18next';
|
|
14
|
+
import { AppIcon } from '../components/AppIcon.js';
|
|
15
|
+
import { Button } from '../components/primitives.js';
|
|
16
|
+
import { cn } from '../lib/cn.js';
|
|
17
|
+
const SIDEBAR_KEY = 'kosh.sidebar';
|
|
18
|
+
/** Whether the sidebar is collapsed to its rail, remembered per browser. */
|
|
19
|
+
export function useSidebarCollapsed() {
|
|
20
|
+
const [collapsed, setCollapsed] = useState(() => {
|
|
21
|
+
try {
|
|
22
|
+
return localStorage.getItem(SIDEBAR_KEY) === 'collapsed';
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
const set = (next) => {
|
|
29
|
+
setCollapsed(next);
|
|
30
|
+
try {
|
|
31
|
+
if (next)
|
|
32
|
+
localStorage.setItem(SIDEBAR_KEY, 'collapsed');
|
|
33
|
+
else
|
|
34
|
+
localStorage.removeItem(SIDEBAR_KEY);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// storage unavailable: the choice lasts for this page
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
return [collapsed, set];
|
|
41
|
+
}
|
|
42
|
+
/** An app's entries as the sidebar lists them: its top level, with groups (Reporting, Configuration) kept. */
|
|
43
|
+
function entriesOf(menus) {
|
|
44
|
+
return menus.flatMap((root) => (root.action === undefined ? [...root.children] : [root]));
|
|
45
|
+
}
|
|
46
|
+
function SectionLink({ app, node, onNavigate, }) {
|
|
47
|
+
if (node.action === undefined)
|
|
48
|
+
return null;
|
|
49
|
+
return (_jsx(Link, { to: "/$app/a/$action", params: { app, action: node.action }, onClick: onNavigate, className: "block truncate rounded-md px-2 py-1.5 text-sm text-muted-foreground hover:bg-muted hover:text-foreground", activeProps: {
|
|
50
|
+
'aria-current': 'page',
|
|
51
|
+
className: 'bg-accent font-medium text-accent-foreground hover:bg-accent',
|
|
52
|
+
}, children: node.label }));
|
|
53
|
+
}
|
|
54
|
+
function AppSections({ api, app, company, onNavigate, }) {
|
|
55
|
+
const menus = useQuery({
|
|
56
|
+
queryKey: ['menus', app, company],
|
|
57
|
+
queryFn: () => api.menus(app),
|
|
58
|
+
});
|
|
59
|
+
return (_jsx("ul", { className: "ms-4 grid gap-0.5 border-s ps-2 pt-1 pb-2", children: entriesOf(menus.data ?? []).map((node) => node.children.length > 0 ? (_jsxs("li", { className: "pt-1", children: [_jsx("p", { className: "px-2 pb-0.5 text-xs font-medium tracking-wide text-muted-foreground uppercase", children: node.label }), _jsx("ul", { className: "grid gap-0.5", children: node.children.map((child) => (_jsx("li", { children: _jsx(SectionLink, { app: app, node: child, onNavigate: onNavigate }) }, child.id))) })] }, node.id)) : (_jsx("li", { children: _jsx(SectionLink, { app: app, node: node, onNavigate: onNavigate }) }, node.id))) }));
|
|
60
|
+
}
|
|
61
|
+
export function Sidebar({ api, apps, current, company, collapsed, onCollapse, onSearch, top, bottom, onNavigate, }) {
|
|
62
|
+
const { t } = useTranslation();
|
|
63
|
+
const navigate = useNavigate();
|
|
64
|
+
const [open, setOpen] = useState(current);
|
|
65
|
+
useEffect(() => setOpen(current), [current]);
|
|
66
|
+
const item = 'flex w-full items-center gap-2.5 rounded-md px-2 py-1.5 text-sm hover:bg-muted focus-visible:outline-2';
|
|
67
|
+
return (_jsxs("div", { className: "flex h-full flex-col gap-1 p-2", children: [_jsx("div", { className: cn('pb-1', collapsed && 'flex justify-center'), children: top }), _jsxs("button", { type: "button", className: cn(item, 'text-muted-foreground', collapsed && 'justify-center'), onClick: onSearch, "aria-keyshortcuts": "Control+K Meta+K", "aria-label": collapsed ? t('palette.button') : undefined, title: collapsed ? t('palette.button') : undefined, children: [_jsx(Search, { className: "size-4 shrink-0", "aria-hidden": true }), collapsed ? null : (_jsxs(_Fragment, { children: [_jsx("span", { className: "flex-1 text-start", children: t('palette.button') }), _jsx("kbd", { className: "rounded border bg-surface px-1 text-[0.65rem]", children: "Ctrl K" })] }))] }), _jsxs(Link, { to: "/", onClick: onNavigate, className: cn(item, collapsed && 'justify-center'), activeOptions: { exact: true }, activeProps: { 'aria-current': 'page', className: 'bg-muted font-medium' }, "aria-label": collapsed ? t('shell.home') : undefined, title: collapsed ? t('shell.home') : undefined, children: [_jsx(House, { className: "size-4 shrink-0", "aria-hidden": true }), collapsed ? null : t('shell.home')] }), _jsx("nav", { "aria-label": t('shell.menu'), className: "-mx-2 mt-2 flex-1 overflow-y-auto border-t px-2 pt-2", children: _jsx("ul", { className: "grid gap-0.5", children: apps.map((a) => {
|
|
68
|
+
const expanded = !collapsed && open === a.code;
|
|
69
|
+
return (_jsxs("li", { children: [_jsxs("button", { type: "button", "aria-expanded": collapsed ? undefined : expanded, "aria-label": t('shell.appNamed', { app: a.label }), title: collapsed ? a.label : undefined, className: cn(item, current === a.code && 'font-medium', collapsed && 'justify-center'), onClick: () => {
|
|
70
|
+
if (collapsed || a.code !== current) {
|
|
71
|
+
setOpen(a.code);
|
|
72
|
+
void navigate({ to: '/$app', params: { app: a.code } });
|
|
73
|
+
onNavigate?.();
|
|
74
|
+
}
|
|
75
|
+
else
|
|
76
|
+
setOpen(expanded ? undefined : a.code);
|
|
77
|
+
}, children: [_jsx(AppIcon, { name: a.icon ?? a.code, className: "size-6 rounded-md [&_svg]:size-3.5" }), collapsed ? null : (_jsxs(_Fragment, { children: [_jsx("span", { className: "flex-1 truncate text-start", children: a.label }), _jsx(ChevronDown, { className: cn('size-3.5 text-muted-foreground transition-transform', !expanded && '-rotate-90 rtl:rotate-90'), "aria-hidden": true })] }))] }), expanded ? (_jsx(AppSections, { api: api, app: a.code, company: company, onNavigate: onNavigate })) : null] }, a.code));
|
|
78
|
+
}) }) }), _jsxs("div", { className: cn('flex items-center gap-1 border-t pt-2', collapsed && 'flex-col'), children: [_jsx("div", { className: "min-w-0 flex-1", children: bottom }), onCollapse === undefined ? null : (_jsx(Button, { variant: "ghost", size: "icon", "aria-label": collapsed ? t('shell.expandSidebar') : t('shell.collapseSidebar'), title: `${collapsed ? t('shell.expandSidebar') : t('shell.collapseSidebar')} (Ctrl B)`, "aria-keyshortcuts": "Control+B Meta+B", onClick: () => onCollapse(!collapsed), children: collapsed ? (_jsx(PanelLeftOpen, { className: "rtl:rotate-180", "aria-hidden": true })) : (_jsx(PanelLeftClose, { className: "rtl:rotate-180", "aria-hidden": true })) }))] })] }));
|
|
79
|
+
}
|
|
80
|
+
/** The sidebar as a drawer on small screens. */
|
|
81
|
+
export function SidebarDrawer({ open, onOpenChange, children, }) {
|
|
82
|
+
const { t } = useTranslation();
|
|
83
|
+
return (_jsx(Dialog.Root, { open: open, onOpenChange: onOpenChange, children: _jsxs(Dialog.Portal, { children: [_jsx(Dialog.Overlay, { className: "fixed inset-0 z-40 bg-black/40" }), _jsxs(Dialog.Content, { className: "fixed inset-y-0 start-0 z-50 w-72 max-w-[85vw] border-e bg-surface shadow-xl outline-none", children: [_jsx(Dialog.Title, { className: "sr-only", children: t('shell.menu') }), _jsx(Dialog.Description, { className: "sr-only", children: t('shell.menuHint') }), _jsx(Dialog.Close, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "absolute end-2 top-2 z-10", "aria-label": t('shell.closeMenu'), children: _jsx(X, { "aria-hidden": true }) }) }), children] })] }) }));
|
|
84
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Two-factor sign-in required of everyone (RFC 0019 §3.5): until the person sets it up, the server allows nothing
|
|
4
|
+
* else, so the shell shows this page instead — the key for the authenticator app, a code to turn it on, the recovery
|
|
5
|
+
* codes once — then opens the back-office.
|
|
6
|
+
*/
|
|
7
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
8
|
+
import { useId, useState } from 'react';
|
|
9
|
+
import { useTranslation } from 'react-i18next';
|
|
10
|
+
import { ApiError } from '../api.js';
|
|
11
|
+
import { Alert, Button, Card, Input, Label } from '../components/primitives.js';
|
|
12
|
+
export function TwoFactorSetup({ api }) {
|
|
13
|
+
const { t } = useTranslation();
|
|
14
|
+
const id = useId();
|
|
15
|
+
const queryClient = useQueryClient();
|
|
16
|
+
const [code, setCode] = useState('');
|
|
17
|
+
const [codes, setCodes] = useState(null);
|
|
18
|
+
const start = useMutation({
|
|
19
|
+
mutationFn: () => api.call('POST', '/api/v1/auth/2fa/setup'),
|
|
20
|
+
});
|
|
21
|
+
const enable = useMutation({
|
|
22
|
+
mutationFn: () => api.call('POST', '/api/v1/auth/2fa/enable', { code }),
|
|
23
|
+
onSuccess: (data) => setCodes(data.recoveryCodes),
|
|
24
|
+
});
|
|
25
|
+
const error = start.error ?? enable.error;
|
|
26
|
+
const setup = start.data;
|
|
27
|
+
return (_jsx("main", { className: "grid min-h-screen place-items-center bg-background p-4", children: _jsxs(Card, { className: "grid w-full max-w-md gap-4 p-6", children: [_jsxs("div", { children: [_jsx("h1", { className: "mb-1 text-xl font-semibold", children: t('auth.setupTitle') }), _jsx("p", { className: "text-sm text-muted-foreground", children: t('auth.setupIntro') })] }), error === null ? null : (_jsx(Alert, { children: error instanceof ApiError ? error.message : String(error) })), codes !== null ? (_jsxs(_Fragment, { children: [_jsxs(Alert, { tone: "info", children: [_jsx("p", { className: "font-medium", children: t('auth.setupCodes') }), _jsx("ul", { className: "mt-2 grid grid-cols-2 gap-1 font-mono text-sm", children: codes.map((c) => (_jsx("li", { children: c }, c))) })] }), _jsx(Button, { variant: "primary", onClick: () => void queryClient.invalidateQueries(), children: t('auth.setupContinue') })] })) : setup === undefined ? (_jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsx(Button, { variant: "primary", busy: start.isPending, onClick: () => start.mutate(), children: t('auth.setupStart') }), _jsx(Button, { variant: "ghost", onClick: () => void api.logout().then(() => window.location.assign('/login')), children: t('auth.signOut') })] })) : (_jsxs("form", { className: "grid gap-4", onSubmit: (e) => {
|
|
28
|
+
e.preventDefault();
|
|
29
|
+
if (code.length >= 6)
|
|
30
|
+
enable.mutate();
|
|
31
|
+
}, children: [_jsxs("ol", { className: "grid list-decimal gap-2 ps-5 text-sm", children: [_jsxs("li", { children: [t('auth.setupStep1'), ' ', _jsx("a", { className: "text-primary underline", href: setup.otpauthUri, children: t('auth.setupOpenApp') })] }), _jsxs("li", { children: [t('auth.setupStep2'), _jsx("code", { "data-testid": "two-factor-key", className: "ms-2 select-all break-all rounded bg-muted px-2 py-1 font-mono text-xs", children: setup.secret })] }), _jsx("li", { children: t('auth.setupStep3') })] }), _jsxs("div", { className: "grid gap-1.5", children: [_jsx(Label, { htmlFor: `${id}-code`, children: t('auth.code') }), _jsx(Input, { id: `${id}-code`, inputMode: "numeric", autoComplete: "one-time-code", className: "w-40 font-mono", value: code, onChange: (e) => setCode(e.target.value.trim()) })] }), _jsx(Button, { type: "submit", variant: "primary", busy: enable.isPending, disabled: code.length < 6, children: t('auth.setupTurnOn') })] }))] }) }));
|
|
32
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* | Keys | Does | Marked with |
|
|
6
6
|
* | --------------- | ----------------------------------- | ----------------------- |
|
|
7
7
|
* | Ctrl/⌘ + K | command palette | (the shell) |
|
|
8
|
+
* | Ctrl/⌘ + B | collapse / expand the sidebar | (the shell) |
|
|
8
9
|
* | ? | this list | (the shell) |
|
|
9
10
|
* | / | focus the search box | `data-kosh-search` |
|
|
10
11
|
* | N | new record | `data-kosh-new` |
|
|
@@ -21,6 +22,8 @@ import { type ReactElement } from 'react';
|
|
|
21
22
|
export declare function useShortcuts(handlers: {
|
|
22
23
|
readonly palette: () => void;
|
|
23
24
|
readonly help: () => void;
|
|
25
|
+
/** Collapses or expands the sidebar (RFC 0018). */
|
|
26
|
+
readonly sidebar?: () => void;
|
|
24
27
|
}): void;
|
|
25
28
|
/** The cheat sheet (`?`). */
|
|
26
29
|
export declare function ShortcutsHelp({ open, onOpenChange, }: {
|
|
@@ -6,6 +6,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
6
6
|
* | Keys | Does | Marked with |
|
|
7
7
|
* | --------------- | ----------------------------------- | ----------------------- |
|
|
8
8
|
* | Ctrl/⌘ + K | command palette | (the shell) |
|
|
9
|
+
* | Ctrl/⌘ + B | collapse / expand the sidebar | (the shell) |
|
|
9
10
|
* | ? | this list | (the shell) |
|
|
10
11
|
* | / | focus the search box | `data-kosh-search` |
|
|
11
12
|
* | N | new record | `data-kosh-new` |
|
|
@@ -48,6 +49,11 @@ export function useShortcuts(handlers) {
|
|
|
48
49
|
handlers.palette();
|
|
49
50
|
return;
|
|
50
51
|
}
|
|
52
|
+
if (mod && key === 'b' && handlers.sidebar !== undefined && !typing(e.target)) {
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
handlers.sidebar();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
51
57
|
if (mod && key === 's') {
|
|
52
58
|
// On a form, Ctrl+S saves (or does nothing when there is nothing to save): never the browser's dialog.
|
|
53
59
|
if (click('[data-kosh-save]') || document.querySelector('[data-kosh-form]') !== null)
|
|
@@ -68,8 +74,10 @@ export function useShortcuts(handlers) {
|
|
|
68
74
|
}
|
|
69
75
|
if (mod || e.altKey || typing(e.target))
|
|
70
76
|
return;
|
|
71
|
-
|
|
72
|
-
|
|
77
|
+
// Dialogs, popovers, menus and lists own their keys (Escape closes them, not the page). Checked while the key
|
|
78
|
+
// is captured, before they handle it and close.
|
|
79
|
+
if (document.querySelector('[role="dialog"], [role="menu"], [role="listbox"], [data-radix-popper-content-wrapper]') !== null)
|
|
80
|
+
return;
|
|
73
81
|
if (e.key === '?') {
|
|
74
82
|
e.preventDefault();
|
|
75
83
|
handlers.help();
|
|
@@ -90,8 +98,8 @@ export function useShortcuts(handlers) {
|
|
|
90
98
|
e.preventDefault();
|
|
91
99
|
}
|
|
92
100
|
};
|
|
93
|
-
window.addEventListener('keydown', onKey);
|
|
94
|
-
return () => window.removeEventListener('keydown', onKey);
|
|
101
|
+
window.addEventListener('keydown', onKey, true);
|
|
102
|
+
return () => window.removeEventListener('keydown', onKey, true);
|
|
95
103
|
}, [handlers]);
|
|
96
104
|
}
|
|
97
105
|
const isMac = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform);
|
|
@@ -101,6 +109,7 @@ export function ShortcutsHelp({ open, onOpenChange, }) {
|
|
|
101
109
|
const mod = isMac ? '⌘' : 'Ctrl';
|
|
102
110
|
const rows = [
|
|
103
111
|
['palette', [mod, 'K']],
|
|
112
|
+
['sidebar', [mod, 'B']],
|
|
104
113
|
['search', ['/']],
|
|
105
114
|
['new', ['N']],
|
|
106
115
|
['views', ['Alt', '1…9']],
|