create-portfoliable 1.0.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/bin/create-portfoliable.mjs +425 -0
- package/cli/portfoliable.mjs +836 -0
- package/index.html +18 -0
- package/package.json +102 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.png +0 -0
- package/scripts/check-updates.mjs +31 -0
- package/scripts/ensure-valence-index-css.mjs +245 -0
- package/scripts/generate-password-hash.mjs +25 -0
- package/scripts/release-orchestrator.mjs +363 -0
- package/scripts/scaffold-case.mjs +460 -0
- package/scripts/scaffold-consumer.mjs +87 -0
- package/scripts/smoke-homeview.mjs +133 -0
- package/scripts/smoke-initializer.mjs +129 -0
- package/scripts/smoke-packed.mjs +184 -0
- package/scripts/switch-valence-source.mjs +162 -0
- package/scripts/sync-template-shared.mjs +136 -0
- package/scripts/terminal-ui.mjs +274 -0
- package/scripts/verify-integration.mjs +72 -0
- package/src/App.js +10015 -0
- package/src/header-visibility.contract.js +31 -0
- package/src/header-visibility.contract.test.js +41 -0
- package/src/main.js +12 -0
- package/src/parser/markdown.js +855 -0
- package/src/style.css +88 -0
- package/templates/README.md +242 -0
- package/templates/configs/i18n/i18n.config.js +230 -0
- package/templates/configs/i18n/i18n.labels.js +188 -0
- package/templates/configs/portfoliable.a11y.config.js +262 -0
- package/templates/configs/portfoliable.design.config.js +651 -0
- package/templates/gitignore +9 -0
- package/templates/index.html +20 -0
- package/templates/public/api/.htaccess +4 -0
- package/templates/public/api/README.md +27 -0
- package/templates/public/api/password.config.example.json +16 -0
- package/templates/public/api/password.config.json +16 -0
- package/templates/public/api/unlock-case.php +56 -0
- package/templates/public/favicon.ico +0 -0
- package/templates/public/favicon.png +0 -0
- package/templates/public/portfoliable-logo.svg +3 -0
- package/templates/scripts/add-language.mjs +253 -0
- package/templates/scripts/convert-video-to-mp4.mjs +207 -0
- package/templates/scripts/convert-wav-to-mp3.mjs +184 -0
- package/templates/scripts/delete-case.mjs +320 -0
- package/templates/scripts/delete-language.mjs +195 -0
- package/templates/scripts/generate-password-hash.mjs +98 -0
- package/templates/scripts/i18n-config-utils.mjs +123 -0
- package/templates/scripts/scaffold-case.mjs +444 -0
- package/templates/scripts/sync-locales.mjs +1048 -0
- package/templates/scripts/validate-content.mjs +257 -0
- package/templates/scripts/validate-protection.mjs +187 -0
- package/templates/src/cases/index.js +188 -0
- package/templates/src/content/about/ABOUTME.md +202 -0
- package/templates/src/content/cases/template-case/case.md +479 -0
- package/templates/src/content/cases/template-case/cover.png +0 -0
- package/templates/src/content/cases/template-case/he-cover.png +0 -0
- package/templates/src/content/cases/template-case/template-audio.mp3 +0 -0
- package/templates/src/content/cases/template-case/template-video.mp4 +0 -0
- package/templates/src/content/thumbnail-options.generated.json +1044 -0
- package/templates/src/main.js +23 -0
- package/templates/src/parser/markdown.js +805 -0
- package/vite.config.js +111 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
"en": {
|
|
3
|
+
// SEO Meta
|
|
4
|
+
"meta_home_title": "Portfoliable",
|
|
5
|
+
"meta_home_description": "Your cases, your stories.",
|
|
6
|
+
|
|
7
|
+
// Home View
|
|
8
|
+
"h1_title": "Welcome to Portfoliable",
|
|
9
|
+
"footer_text": "Made with Portfoliable",
|
|
10
|
+
"nav_home": "Home",
|
|
11
|
+
"about_title": "About",
|
|
12
|
+
"view_about_aria_label": "About view",
|
|
13
|
+
|
|
14
|
+
// Case View and Actions
|
|
15
|
+
"view_case_aria_label": "Case view",
|
|
16
|
+
"protected_case": "Protected Case",
|
|
17
|
+
"enter_passcode": "Enter the passcode to unlock project details.",
|
|
18
|
+
"unlock": "Unlock",
|
|
19
|
+
"case_unlocked": "Case unlocked.",
|
|
20
|
+
"incorrect_passcode": "Incorrect passcode.",
|
|
21
|
+
|
|
22
|
+
// Player View
|
|
23
|
+
"player_video_title": "Case video",
|
|
24
|
+
"player_label_play": "Play",
|
|
25
|
+
"player_label_pause": "Pause",
|
|
26
|
+
"player_label_cc_on": "Captions on",
|
|
27
|
+
"player_label_cc_off": "Captions off",
|
|
28
|
+
"player_label_mute": "Mute",
|
|
29
|
+
"player_label_unmute": "Unmute",
|
|
30
|
+
"player_label_speed": "Playback speed",
|
|
31
|
+
|
|
32
|
+
// Audio Player
|
|
33
|
+
"player_audio_reader": "Audio reader",
|
|
34
|
+
"player_audio_play": "Play audio",
|
|
35
|
+
"player_audio_pause": "Pause audio",
|
|
36
|
+
"player_audio_mute": "Mute audio",
|
|
37
|
+
"player_audio_unmute": "Unmute audio",
|
|
38
|
+
"player_audio_speed": "Audio speed",
|
|
39
|
+
"player_audio_hide": "Hide player",
|
|
40
|
+
"player_audio_show": "Show player",
|
|
41
|
+
"player_audio_autoscroll_on": "Autoscroll on",
|
|
42
|
+
"player_audio_autoscroll_off": "Autoscroll off",
|
|
43
|
+
"player_audio_volume": "Volume",
|
|
44
|
+
"player_audio_volume_level": "Volume level",
|
|
45
|
+
"player_audio_position": "Playback position",
|
|
46
|
+
|
|
47
|
+
// Search and Navigator
|
|
48
|
+
"btn_return": "Return",
|
|
49
|
+
"search_cases_placeholder": "Search cases...",
|
|
50
|
+
"search_cases_tooltip_open": "Open search",
|
|
51
|
+
"search_cases_tooltip_close": "Close search",
|
|
52
|
+
"search_main_view_title": "Home",
|
|
53
|
+
"search_main_view_snippet": "Explore featured case studies.",
|
|
54
|
+
"search_case_studies_label": "Case Studies",
|
|
55
|
+
"case_nav_prev": "Previous case",
|
|
56
|
+
"case_nav_next": "Next case",
|
|
57
|
+
"case_nav_prev_locked": "Previous case is locked",
|
|
58
|
+
"case_nav_next_locked": "Next case is locked",
|
|
59
|
+
|
|
60
|
+
// Breadcrumb Menus
|
|
61
|
+
"breadcrumb_menu_case_header": "Case menu",
|
|
62
|
+
"breadcrumb_menu_video_header": "Video menu",
|
|
63
|
+
|
|
64
|
+
// Accessibility Modal
|
|
65
|
+
"popup_a11y_title": "Accessibility",
|
|
66
|
+
"a11y_cat_typography": "Typography",
|
|
67
|
+
"a11y_size": "Text size",
|
|
68
|
+
"a11y_dyslexia": "Dyslexia font",
|
|
69
|
+
"a11y_cat_visuals": "Visuals",
|
|
70
|
+
"a11y_dark": "Dark mode",
|
|
71
|
+
"a11y_contrast": "High contrast",
|
|
72
|
+
"a11y_motion": "No motion",
|
|
73
|
+
"a11y_tab": "TAB",
|
|
74
|
+
|
|
75
|
+
// Language Modal
|
|
76
|
+
"popup_lang_title": "Language",
|
|
77
|
+
"lang_cat": "",
|
|
78
|
+
|
|
79
|
+
// Sharing
|
|
80
|
+
"share_text": "Check out this case study.",
|
|
81
|
+
|
|
82
|
+
// Toast
|
|
83
|
+
"resume_reading": "Resume:",
|
|
84
|
+
"toast_label_close": "Close",
|
|
85
|
+
"toast_label_never_show": "Never show",
|
|
86
|
+
|
|
87
|
+
// Diagnostics and Errors
|
|
88
|
+
"mermaid_error": "Error rendering diagram.",
|
|
89
|
+
|
|
90
|
+
// Other
|
|
91
|
+
"gallery_pill_pitch": "Pitch",
|
|
92
|
+
"gallery_pill_repo": "Repository",
|
|
93
|
+
"gallery_pill_demo": "Demo",
|
|
94
|
+
},
|
|
95
|
+
"he": {
|
|
96
|
+
// SEO Meta
|
|
97
|
+
"meta_home_title": "פורטפוליאבל",
|
|
98
|
+
"meta_home_description": "הקייסים שלך, הסיפורים שלך.",
|
|
99
|
+
|
|
100
|
+
// Home View
|
|
101
|
+
"h1_title": "ברוכים הבאים ל-Portfoliable",
|
|
102
|
+
"footer_text": "נבנה עם Portfoliable",
|
|
103
|
+
"nav_home": "בית",
|
|
104
|
+
"about_title": "אודות",
|
|
105
|
+
"view_about_aria_label": "תצוגת אודות",
|
|
106
|
+
|
|
107
|
+
// Case View and Actions
|
|
108
|
+
"view_case_aria_label": "תצוגת קייס",
|
|
109
|
+
"protected_case": "קייס מוגן",
|
|
110
|
+
"enter_passcode": "הזינו קוד גישה כדי לפתוח את פרטי הפרויקט.",
|
|
111
|
+
"unlock": "פתיחה",
|
|
112
|
+
"case_unlocked": "הקייס נפתח.",
|
|
113
|
+
"incorrect_passcode": "קוד הגישה שגוי.",
|
|
114
|
+
|
|
115
|
+
// Player View
|
|
116
|
+
"player_video_title": "וידאו קייס",
|
|
117
|
+
"player_label_play": "ניגון",
|
|
118
|
+
"player_label_pause": "השהיה",
|
|
119
|
+
"player_label_cc_on": "כתוביות פועלות",
|
|
120
|
+
"player_label_cc_off": "כתוביות כבויות",
|
|
121
|
+
"player_label_mute": "השתקה",
|
|
122
|
+
"player_label_unmute": "ביטול השתקה",
|
|
123
|
+
"player_label_speed": "מהירות ניגון",
|
|
124
|
+
|
|
125
|
+
// Audio Player
|
|
126
|
+
"player_audio_reader": "קורא שמע",
|
|
127
|
+
"player_audio_play": "נגן שמע",
|
|
128
|
+
"player_audio_pause": "השהה שמע",
|
|
129
|
+
"player_audio_mute": "השתק שמע",
|
|
130
|
+
"player_audio_unmute": "בטל השתקת שמע",
|
|
131
|
+
"player_audio_speed": "מהירות שמע",
|
|
132
|
+
"player_audio_hide": "הסתר נגן",
|
|
133
|
+
"player_audio_show": "הצג נגן",
|
|
134
|
+
"player_audio_autoscroll_on": "גלילה אוטומטית פעילה",
|
|
135
|
+
"player_audio_autoscroll_off": "גלילה אוטומטית כבויה",
|
|
136
|
+
"player_audio_volume": "עוצמה",
|
|
137
|
+
"player_audio_volume_level": "רמת עוצמה",
|
|
138
|
+
"player_audio_position": "מיקום ניגון",
|
|
139
|
+
|
|
140
|
+
// Search and Navigator
|
|
141
|
+
"btn_return": "חזרה",
|
|
142
|
+
"search_cases_placeholder": "חיפוש קייסים...",
|
|
143
|
+
"search_cases_tooltip_open": "פתח חיפוש",
|
|
144
|
+
"search_cases_tooltip_close": "סגור חיפוש",
|
|
145
|
+
"search_main_view_title": "בית",
|
|
146
|
+
"search_main_view_snippet": "גלו קייסים נבחרים.",
|
|
147
|
+
"search_case_studies_label": "קייסים",
|
|
148
|
+
"case_nav_prev": "הקייס הקודם",
|
|
149
|
+
"case_nav_next": "הקייס הבא",
|
|
150
|
+
"case_nav_prev_locked": "הקייס הקודם נעול",
|
|
151
|
+
"case_nav_next_locked": "הקייס הבא נעול",
|
|
152
|
+
|
|
153
|
+
// Breadcrumb Menus
|
|
154
|
+
"breadcrumb_menu_case_header": "תפריט קייס",
|
|
155
|
+
"breadcrumb_menu_video_header": "תפריט וידאו",
|
|
156
|
+
|
|
157
|
+
// Accessibility Modal
|
|
158
|
+
"popup_a11y_title": "נגישות",
|
|
159
|
+
"a11y_cat_typography": "טיפוגרפיה",
|
|
160
|
+
"a11y_size": "גודל טקסט",
|
|
161
|
+
"a11y_dyslexia": "גופן דיסלקציה",
|
|
162
|
+
"a11y_cat_visuals": "ויזואליות",
|
|
163
|
+
"a11y_dark": "מצב כהה",
|
|
164
|
+
"a11y_contrast": "ניגודיות גבוהה",
|
|
165
|
+
"a11y_motion": "ללא תנועה",
|
|
166
|
+
"a11y_tab": "TAB",
|
|
167
|
+
|
|
168
|
+
// Language Modal
|
|
169
|
+
"popup_lang_title": "שפה",
|
|
170
|
+
"lang_cat": "",
|
|
171
|
+
|
|
172
|
+
// Sharing
|
|
173
|
+
"share_text": "בדקו את הקייס הזה.",
|
|
174
|
+
|
|
175
|
+
// Toast
|
|
176
|
+
"resume_reading": "המשך:",
|
|
177
|
+
"toast_label_close": "סגור",
|
|
178
|
+
"toast_label_never_show": "אל תציג שוב",
|
|
179
|
+
|
|
180
|
+
// Diagnostics and Errors
|
|
181
|
+
"mermaid_error": "שגיאה ברינדור דיאגרמה.",
|
|
182
|
+
|
|
183
|
+
// Other
|
|
184
|
+
"gallery_pill_pitch": "פיץ'",
|
|
185
|
+
"gallery_pill_repo": "מאגר",
|
|
186
|
+
"gallery_pill_demo": "דמו",
|
|
187
|
+
}
|
|
188
|
+
};
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// File: configs/portfoliable.a11y.config.js
|
|
2
|
+
// Purpose: Define keyboard shortcuts and a11y-related shortcut actions.
|
|
3
|
+
// Author: Lio Schimanko
|
|
4
|
+
|
|
5
|
+
// MARK: MATCH ORDER
|
|
6
|
+
// Ensures keyboard event matching checks modifiers in a stable order.
|
|
7
|
+
const MODIFIER_ORDER = ['metaKey', 'ctrlKey', 'altKey', 'shiftKey'];
|
|
8
|
+
|
|
9
|
+
// MARK: SHORTCUT CATALOG
|
|
10
|
+
// Declares all keyboard shortcuts consumed by the runtime shell.
|
|
11
|
+
export const keyboardShortcuts = [
|
|
12
|
+
{
|
|
13
|
+
id: 'about',
|
|
14
|
+
action: 'toggleAboutView',
|
|
15
|
+
code: 'KeyI',
|
|
16
|
+
altKey: true,
|
|
17
|
+
labels: {
|
|
18
|
+
mac: '⌥ I',
|
|
19
|
+
windows: 'Alt I',
|
|
20
|
+
linux: 'Alt I'
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'language',
|
|
25
|
+
action: 'toggleLanguageModal',
|
|
26
|
+
code: 'KeyL',
|
|
27
|
+
altKey: true,
|
|
28
|
+
labels: {
|
|
29
|
+
mac: '⌥ L',
|
|
30
|
+
windows: 'Alt L',
|
|
31
|
+
linux: 'Alt L'
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 'accessibility',
|
|
36
|
+
action: 'toggleA11yModal',
|
|
37
|
+
code: 'KeyA',
|
|
38
|
+
altKey: true,
|
|
39
|
+
labels: {
|
|
40
|
+
mac: '⌥ A',
|
|
41
|
+
windows: 'Alt A',
|
|
42
|
+
linux: 'Alt A'
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'text-size',
|
|
47
|
+
action: 'toggleA11yState',
|
|
48
|
+
stateKey: 'largeText',
|
|
49
|
+
code: 'KeyT',
|
|
50
|
+
altKey: true,
|
|
51
|
+
labels: {
|
|
52
|
+
mac: '⌥ T',
|
|
53
|
+
windows: 'Alt T',
|
|
54
|
+
linux: 'Alt T'
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'dark-mode',
|
|
59
|
+
action: 'toggleA11yState',
|
|
60
|
+
stateKey: 'darkMode',
|
|
61
|
+
code: 'KeyD',
|
|
62
|
+
altKey: true,
|
|
63
|
+
labels: {
|
|
64
|
+
mac: '⌥ D',
|
|
65
|
+
windows: 'Alt D',
|
|
66
|
+
linux: 'Alt D'
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: 'high-contrast',
|
|
71
|
+
action: 'toggleA11yState',
|
|
72
|
+
stateKey: 'highContrast',
|
|
73
|
+
code: 'KeyC',
|
|
74
|
+
altKey: true,
|
|
75
|
+
labels: {
|
|
76
|
+
mac: '⌥ C',
|
|
77
|
+
windows: 'Alt C',
|
|
78
|
+
linux: 'Alt C'
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: 'reduce-motion',
|
|
83
|
+
action: 'toggleA11yState',
|
|
84
|
+
stateKey: 'reduceMotion',
|
|
85
|
+
code: 'KeyM',
|
|
86
|
+
altKey: true,
|
|
87
|
+
labels: {
|
|
88
|
+
mac: '⌥ M',
|
|
89
|
+
windows: 'Alt M',
|
|
90
|
+
linux: 'Alt M'
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: 'tab-navigation',
|
|
95
|
+
action: 'toggleA11yState',
|
|
96
|
+
stateKey: 'tabNav',
|
|
97
|
+
code: 'KeyF',
|
|
98
|
+
altKey: true,
|
|
99
|
+
labels: {
|
|
100
|
+
mac: '⌥ F',
|
|
101
|
+
windows: 'Alt F',
|
|
102
|
+
linux: 'Alt F'
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: 'dyslexia-font',
|
|
107
|
+
action: 'toggleA11yState',
|
|
108
|
+
stateKey: 'dyslexiaFont',
|
|
109
|
+
code: 'KeyY',
|
|
110
|
+
altKey: true,
|
|
111
|
+
labels: {
|
|
112
|
+
mac: '⌥ Y',
|
|
113
|
+
windows: 'Alt Y',
|
|
114
|
+
linux: 'Alt Y'
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: 'case-search-open',
|
|
119
|
+
action: 'openCaseSearch',
|
|
120
|
+
code: 'KeyS',
|
|
121
|
+
ctrlKey: true,
|
|
122
|
+
labels: {
|
|
123
|
+
mac: '⌃ S',
|
|
124
|
+
windows: 'Ctrl S',
|
|
125
|
+
linux: 'Ctrl S'
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
id: 'case-search-close',
|
|
130
|
+
action: 'closeCaseSearch',
|
|
131
|
+
code: 'KeyX',
|
|
132
|
+
ctrlKey: true,
|
|
133
|
+
labels: {
|
|
134
|
+
mac: '⌃ X',
|
|
135
|
+
windows: 'Ctrl X',
|
|
136
|
+
linux: 'Ctrl X'
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: 'open-player',
|
|
141
|
+
action: 'openPlayer',
|
|
142
|
+
code: 'Enter',
|
|
143
|
+
labels: {
|
|
144
|
+
mac: 'Return',
|
|
145
|
+
windows: 'Enter',
|
|
146
|
+
linux: 'Enter'
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: 'navigate-back',
|
|
151
|
+
action: 'navigateBack',
|
|
152
|
+
code: 'Backspace',
|
|
153
|
+
labels: {
|
|
154
|
+
mac: 'Delete',
|
|
155
|
+
windows: 'Backspace',
|
|
156
|
+
linux: 'Backspace'
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
// MARK: I18N SHORTCUT LABEL BINDINGS
|
|
162
|
+
// Maps translation keys to shortcut IDs used for runtime kbd markup.
|
|
163
|
+
export const shortcutLabelBindings = {
|
|
164
|
+
btn_return: { shortcutId: 'navigate-back' },
|
|
165
|
+
btn_about: { shortcutId: 'about' },
|
|
166
|
+
btn_lang: { shortcutId: 'language' },
|
|
167
|
+
btn_a11y: { shortcutId: 'accessibility' },
|
|
168
|
+
a11y_size: { shortcutId: 'text-size', kbdClass: 'a11y-kbd' },
|
|
169
|
+
a11y_dark: { shortcutId: 'dark-mode', kbdClass: 'a11y-kbd' },
|
|
170
|
+
a11y_contrast: { shortcutId: 'high-contrast', kbdClass: 'a11y-kbd' },
|
|
171
|
+
a11y_motion: { shortcutId: 'reduce-motion', kbdClass: 'a11y-kbd' },
|
|
172
|
+
a11y_tab: { shortcutId: 'tab-navigation', kbdClass: 'a11y-kbd' },
|
|
173
|
+
a11y_dyslexia: { shortcutId: 'dyslexia-font', kbdClass: 'a11y-kbd' }
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// MARK: PLATFORM DETECTION
|
|
177
|
+
// Normalizes the host platform so shortcut labels can use mac/windows/linux variants.
|
|
178
|
+
function detectPlatform(platformHint) {
|
|
179
|
+
const raw = String(platformHint || globalThis?.navigator?.platform || '').toLowerCase();
|
|
180
|
+
if (raw.includes('mac')) return 'mac';
|
|
181
|
+
if (raw.includes('win')) return 'windows';
|
|
182
|
+
return 'linux';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// MARK: PUBLIC SHORTCUT LOOKUPS
|
|
186
|
+
// Resolves a shortcut object by its ID.
|
|
187
|
+
export function getShortcutById(shortcutId) {
|
|
188
|
+
return keyboardShortcuts.find((shortcut) => shortcut.id === shortcutId) || null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Returns a display-friendly shortcut string for the current platform.
|
|
192
|
+
export function getShortcutDisplay(shortcutId, platformHint) {
|
|
193
|
+
const shortcut = getShortcutById(shortcutId);
|
|
194
|
+
if (!shortcut) return '';
|
|
195
|
+
|
|
196
|
+
const platform = detectPlatform(platformHint);
|
|
197
|
+
return shortcut.labels?.[platform] || shortcut.labels?.linux || '';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Returns contextual tooltip parts split into modifier label and key label.
|
|
201
|
+
export function getShortcutTooltipParts(shortcutId) {
|
|
202
|
+
const shortcut = getShortcutById(shortcutId);
|
|
203
|
+
if (!shortcut) {
|
|
204
|
+
return {
|
|
205
|
+
kbdLabel: '',
|
|
206
|
+
kbdKey: '',
|
|
207
|
+
showPlus: false
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const platform = detectPlatform();
|
|
212
|
+
|
|
213
|
+
const KEY_LABEL_BY_CODE = {
|
|
214
|
+
Enter: {
|
|
215
|
+
mac: 'Return',
|
|
216
|
+
windows: 'Enter',
|
|
217
|
+
linux: 'Enter'
|
|
218
|
+
},
|
|
219
|
+
Backspace: {
|
|
220
|
+
mac: 'Delete',
|
|
221
|
+
windows: 'Backspace',
|
|
222
|
+
linux: 'Backspace'
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const explicitKeyLabel = KEY_LABEL_BY_CODE[shortcut.code]?.[platform];
|
|
227
|
+
|
|
228
|
+
const keyLabel = String(explicitKeyLabel || shortcut.code || '')
|
|
229
|
+
.replace(/^Key/, '')
|
|
230
|
+
.replace(/^Digit/, '');
|
|
231
|
+
|
|
232
|
+
const modifierLabels = [];
|
|
233
|
+
if (shortcut.ctrlKey) modifierLabels.push(platform === 'mac' ? '⌃' : 'Ctrl');
|
|
234
|
+
if (shortcut.altKey) modifierLabels.push('Alt');
|
|
235
|
+
if (shortcut.shiftKey) modifierLabels.push('Shift');
|
|
236
|
+
if (shortcut.metaKey) modifierLabels.push('Cmd');
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
kbdLabel: modifierLabels.join(' + '),
|
|
240
|
+
kbdKey: keyLabel,
|
|
241
|
+
showPlus: modifierLabels.length > 0
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// MARK: EVENT MATCHING
|
|
246
|
+
// Returns true only when code and all modifier expectations match.
|
|
247
|
+
function doesShortcutMatchEvent(shortcut, event) {
|
|
248
|
+
if (!shortcut || !event) return false;
|
|
249
|
+
if (event.code !== shortcut.code) return false;
|
|
250
|
+
|
|
251
|
+
return MODIFIER_ORDER.every((modifierName) => {
|
|
252
|
+
const expected = Boolean(shortcut[modifierName]);
|
|
253
|
+
const actual = Boolean(event[modifierName]);
|
|
254
|
+
return expected === actual;
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Finds the first configured shortcut matching a KeyboardEvent.
|
|
259
|
+
export function matchShortcutEvent(event) {
|
|
260
|
+
if (!event) return null;
|
|
261
|
+
return keyboardShortcuts.find((shortcut) => doesShortcutMatchEvent(shortcut, event)) || null;
|
|
262
|
+
}
|