@widgetic/design-system 0.5.15 → 0.5.16
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/README.md +167 -0
- package/dist/components/CopyableHeader.svelte +25 -11
- package/dist/components/CopyableHeader.svelte.d.ts +4 -0
- package/dist/components/EmptyState.svelte +1 -1
- package/dist/components/PasswordInput.svelte +2 -2
- package/dist/components/WScrollArea.svelte +2 -2
- package/dist/components/ic/browser/BrowserInputController.svelte +2 -2
- package/dist/components/index.d.ts +1 -0
- package/dist/components/modals/ActionModal.svelte +3 -0
- package/dist/components/modals/ActionModal.svelte.d.ts +1 -0
- package/dist/components/modals/BaseModal.svelte +4 -2
- package/dist/components/modals/BaseModal.svelte.d.ts +1 -0
- package/dist/components/modals/NewItemDialog.svelte +48 -30
- package/dist/components/modals/NewItemDialog.svelte.d.ts +3 -0
- package/dist/components/modals/Portal.svelte +9 -1
- package/dist/components/modals/README.md +34 -1
- package/dist/components/modals/RenameDialog.svelte +29 -9
- package/dist/components/modals/RenameDialog.svelte.d.ts +3 -0
- package/dist/components/modals/messages.d.ts +39 -0
- package/dist/components/modals/messages.js +10 -0
- package/dist/components/navigation/Breadcrumbs.svelte +30 -17
- package/dist/components/navigation/Breadcrumbs.svelte.d.ts +4 -0
- package/dist/components/navigation/types.d.ts +8 -0
- package/dist/components/pdf/PDFViewer.svelte +17 -8
- package/dist/components/pdf/PDFViewer.svelte.d.ts +5 -0
- package/dist/components/search-input/SearchInput.svelte +3 -3
- package/dist/components/shad-editor/colorpicker.svelte +4 -1
- package/dist/components/shad-editor/custom/Extentions/ImageExtention.d.ts +20 -1
- package/dist/components/shad-editor/custom/Extentions/ImageExtention.js +4 -1
- package/dist/components/shad-editor/custom/code-extended.svelte +4 -2
- package/dist/components/shad-editor/custom/image-extended-component.svelte +54 -32
- package/dist/components/shad-editor/editor.css +5 -5
- package/dist/components/shad-editor/i18n/context.d.ts +6 -0
- package/dist/components/shad-editor/i18n/context.js +11 -0
- package/dist/components/shad-editor/i18n/index.d.ts +8 -0
- package/dist/components/shad-editor/i18n/index.js +11 -0
- package/dist/components/shad-editor/i18n/messages.d.ts +109 -0
- package/dist/components/shad-editor/i18n/messages.js +116 -0
- package/dist/components/shad-editor/icons/block-quote.svelte +3 -1
- package/dist/components/shad-editor/icons/bold.svelte +3 -1
- package/dist/components/shad-editor/icons/buttle-list.svelte +3 -1
- package/dist/components/shad-editor/icons/code.svelte +3 -1
- package/dist/components/shad-editor/icons/highlighter.svelte +5 -3
- package/dist/components/shad-editor/icons/image.svelte +7 -5
- package/dist/components/shad-editor/icons/italic.svelte +3 -1
- package/dist/components/shad-editor/icons/link.svelte +8 -6
- package/dist/components/shad-editor/icons/ordered-list.svelte +3 -1
- package/dist/components/shad-editor/icons/quickcolor.svelte +24 -22
- package/dist/components/shad-editor/icons/redo.svelte +3 -1
- package/dist/components/shad-editor/icons/search-replace.svelte +16 -14
- package/dist/components/shad-editor/icons/strikethrough.svelte +3 -1
- package/dist/components/shad-editor/icons/subscript.svelte +3 -1
- package/dist/components/shad-editor/icons/superscript.svelte +3 -1
- package/dist/components/shad-editor/icons/table.svelte +21 -19
- package/dist/components/shad-editor/icons/task-list.svelte +3 -1
- package/dist/components/shad-editor/icons/text.svelte +13 -11
- package/dist/components/shad-editor/icons/textalign.svelte +11 -9
- package/dist/components/shad-editor/icons/textcolor.svelte +5 -3
- package/dist/components/shad-editor/icons/underline.svelte +3 -1
- package/dist/components/shad-editor/icons/undo.svelte +3 -1
- package/dist/components/shad-editor/index.d.ts +1 -0
- package/dist/components/shad-editor/index.js +2 -0
- package/dist/components/shad-editor/shad-editor.svelte +32 -3
- package/dist/components/shad-editor/shad-editor.svelte.d.ts +12 -0
- package/dist/components/textDirection.d.ts +91 -0
- package/dist/components/textDirection.js +99 -0
- package/dist/components/toast/ToasterModal.svelte +49 -14
- package/dist/components/toast/ToasterModal.svelte.d.ts +9 -2
- package/dist/components/toast/toasterRoots.d.ts +23 -0
- package/dist/components/toast/toasterRoots.js +36 -0
- package/dist/components/ui/alert/index.d.ts +2 -2
- package/dist/components/ui/alert/index.js +1 -1
- package/dist/components/ui/breadcrumb/breadcrumb-separator.svelte +1 -1
- package/dist/components/ui/calendar/calendar-next-button.svelte +21 -4
- package/dist/components/ui/calendar/calendar-next-button.svelte.d.ts +9 -2
- package/dist/components/ui/calendar/calendar-prev-button.svelte +21 -4
- package/dist/components/ui/calendar/calendar-prev-button.svelte.d.ts +9 -2
- package/dist/components/ui/calendar/calendar.svelte +18 -6
- package/dist/components/ui/calendar/calendar.svelte.d.ts +6 -1
- package/dist/components/ui/command/command-input.svelte +1 -1
- package/dist/components/ui/command/command-shortcut.svelte +1 -1
- package/dist/components/ui/dialog/dialog-content.svelte +5 -2
- package/dist/components/ui/dialog/dialog-content.svelte.d.ts +1 -0
- package/dist/components/ui/dialog/dialog-header.svelte +1 -1
- package/dist/components/ui/drawer/drawer-header.svelte +1 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +2 -2
- package/dist/components/ui/dropdown-menu/dropdown-menu-content.svelte +6 -0
- package/dist/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte +1 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu-item.svelte +1 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu-label.svelte +1 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte +2 -2
- package/dist/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte +1 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte +7 -1
- package/dist/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +2 -2
- package/dist/components/ui/hover-card/hover-card-content.svelte +6 -0
- package/dist/components/ui/input-otp/input-otp-slot.svelte +1 -1
- package/dist/components/ui/popover/popover-content.svelte +6 -0
- package/dist/components/ui/progress/progress.svelte +2 -2
- package/dist/components/ui/range-calendar/range-calendar-cell.svelte +1 -1
- package/dist/components/ui/range-calendar/range-calendar-next-button.svelte +21 -4
- package/dist/components/ui/range-calendar/range-calendar-next-button.svelte.d.ts +9 -2
- package/dist/components/ui/range-calendar/range-calendar-prev-button.svelte +21 -4
- package/dist/components/ui/range-calendar/range-calendar-prev-button.svelte.d.ts +9 -2
- package/dist/components/ui/range-calendar/range-calendar.svelte +18 -6
- package/dist/components/ui/range-calendar/range-calendar.svelte.d.ts +7 -2
- package/dist/components/ui/scroll-area/scroll-area-scrollbar.svelte +1 -1
- package/dist/components/ui/scroll-area/scroll-area.svelte +6 -1
- package/dist/components/ui/select/select-content.svelte +6 -0
- package/dist/components/ui/select/select-group-heading.svelte +1 -1
- package/dist/components/ui/select/select-item.svelte +2 -2
- package/dist/components/ui/slider/slider.svelte +6 -0
- package/dist/components/ui/switch/switch.svelte +1 -1
- package/dist/components/ui/table/table-cell.svelte +1 -1
- package/dist/components/ui/table/table-head.svelte +1 -1
- package/dist/components/ui/tooltip/tooltip-content.svelte +6 -0
- package/dist/i18n/audit/catalogUsage.d.ts +36 -0
- package/dist/i18n/audit/catalogUsage.js +58 -0
- package/dist/i18n/audit/designSystemWords.d.ts +47 -0
- package/dist/i18n/audit/designSystemWords.js +137 -0
- package/dist/i18n/audit/hardCodedText.d.ts +40 -0
- package/dist/i18n/audit/hardCodedText.js +27 -0
- package/dist/i18n/audit/index.d.ts +19 -0
- package/dist/i18n/audit/index.js +19 -0
- package/dist/i18n/audit/messageCalls.d.ts +34 -0
- package/dist/i18n/audit/messageCalls.js +89 -0
- package/dist/i18n/audit/syntaxTree.d.ts +33 -0
- package/dist/i18n/audit/syntaxTree.js +90 -0
- package/dist/i18n/audit/visibleText.d.ts +57 -0
- package/dist/i18n/audit/visibleText.js +252 -0
- package/dist/i18n/createI18n.d.ts +63 -0
- package/dist/i18n/createI18n.js +72 -0
- package/dist/i18n/direction.d.ts +27 -0
- package/dist/i18n/direction.js +89 -0
- package/dist/i18n/index.d.ts +17 -0
- package/dist/i18n/index.js +16 -0
- package/dist/i18n/localeDate.d.ts +34 -0
- package/dist/i18n/localeDate.js +104 -0
- package/dist/i18n/splitMessages.d.ts +21 -0
- package/dist/i18n/splitMessages.js +33 -0
- package/dist/i18n/translator.d.ts +61 -0
- package/dist/i18n/translator.js +337 -0
- package/dist/rtl/audit/auditPhysicalPlacement.d.ts +36 -0
- package/dist/rtl/audit/auditPhysicalPlacement.js +58 -0
- package/dist/rtl/audit/index.d.ts +14 -0
- package/dist/rtl/audit/index.js +14 -0
- package/dist/rtl/audit/physicalPlacement.d.ts +64 -0
- package/dist/rtl/audit/physicalPlacement.js +444 -0
- package/dist/shad-editor/index.d.ts +1 -1
- package/dist/shad-editor/index.js +1 -1
- package/dist/styles/global.css +4 -4
- package/dist/utils/fillMessage.d.ts +8 -0
- package/dist/utils/fillMessage.js +10 -0
- package/dist/utils/itemName.d.ts +17 -0
- package/dist/utils/itemName.js +41 -0
- package/package.json +18 -1
package/README.md
CHANGED
|
@@ -172,6 +172,173 @@ The global styles load the necessary `@font-face` rules. You can apply typograph
|
|
|
172
172
|
<span class="subtext">Some smaller text.</span>
|
|
173
173
|
```
|
|
174
174
|
|
|
175
|
+
### Words of a package (`@widgetic/design-system/i18n`)
|
|
176
|
+
|
|
177
|
+
A package that has words of its own (the File Browser, the editor, the canvas, the chat, the widget creator) keeps them in one catalog of English messages and lets the page that embeds it change them and choose the language. This is the translator and the Svelte context they all share, so every package reads a message, a plural and a number the same way.
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
// my-package/src/lib/i18n.ts: the catalog, once
|
|
181
|
+
import { createI18n } from '@widgetic/design-system/i18n';
|
|
182
|
+
|
|
183
|
+
export const MY_MESSAGES = {
|
|
184
|
+
'upload.clear': 'Clear',
|
|
185
|
+
'toast.deleted': '{count, plural, one {Deleted # file} other {Deleted # files}}'
|
|
186
|
+
} as const;
|
|
187
|
+
|
|
188
|
+
export const { provideI18n, useI18n, translateEnglish } = createI18n({ catalog: MY_MESSAGES, label: 'MyPackage' });
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
```svelte
|
|
192
|
+
<!-- the root component of the package: the page's language, set once and again whenever the props change -->
|
|
193
|
+
<script lang="ts">
|
|
194
|
+
import { provideI18n } from './i18n';
|
|
195
|
+
export let locale: string | undefined = undefined;
|
|
196
|
+
export let messages: Record<string, string> | undefined = undefined;
|
|
197
|
+
const language = provideI18n({ locale, messages });
|
|
198
|
+
$: language.update({ locale, messages });
|
|
199
|
+
</script>
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
```svelte
|
|
203
|
+
<!-- any part below it -->
|
|
204
|
+
<script lang="ts">
|
|
205
|
+
import { useI18n } from './i18n';
|
|
206
|
+
const { t } = useI18n();
|
|
207
|
+
</script>
|
|
208
|
+
<button>{$t('upload.clear')}</button>
|
|
209
|
+
<p>{$t('toast.deleted', { count: 3 })}</p>
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
- A message is text with `{name}` where a value goes and `{count, plural, =0 {…} one {…} other {…}}` for plurals (`#` is the number, written the way the locale writes numbers). `\{`, `\}`, `\#` and `\\` write the character itself.
|
|
213
|
+
- `messages` from the page replace the English text of any key of the catalog; a key it leaves out keeps its English. A key that does not exist, a value that is not text, a template that does not read, or one that asks for a value the message is never given is said once in the console, as `[MyPackage] …` with the nearest real key, and the English stays: a mistake of the page never stops the package.
|
|
214
|
+
- `locale` is a BCP 47 tag (`de`, `pt-BR`): it sets the plural rules and how numbers, dates and lists are written. A tag the browser does not know is said once and English is used.
|
|
215
|
+
- For a language written right to left (`ar`, `he`, `fa`, `ur` …) the translator puts every text value it inserts (a `{name}`) between two invisible marks, the Unicode isolates U+2068 and U+2069, so that the words around a name cannot turn the name around: a file name that starts with a digit, or has a hyphen, would be put in the wrong order otherwise. Numbers, the `#` of a plural and empty values get none, and a left to right language gets none. A screen reader does not say the marks; a test that compares what a screen says in such a language has to leave them out. `createTranslator` takes `isolateValues: true | false` to decide for itself; left out, it follows `locale`.
|
|
216
|
+
- `findMessageProblems(MY_MESSAGES, messages)` lists the same problems without showing anything (for a test or a build step of the page); `createTranslator` is the translator on its own, for code that has no component around it.
|
|
217
|
+
- Each package makes its own with `createI18n`, so two packages on one page (the File Browser inside the editor) each have their own words and never hear each other's.
|
|
218
|
+
- A package with others inside it (an editor that has the File Browser and a rich text editor) takes one flat `messages` from the page and gives each inner package its own keys: `const { slices, unclaimed } = splitMessages(messages, { editor: EDITOR_MESSAGES, fileBrowser: FILE_BROWSER_MESSAGES })`. `slices.editor` holds the entries whose key is in that catalog, and `unclaimed` the entries whose key is in none (a misspelt key): check those with `findMessageProblems` against the catalogs put together, so the page is told once and by the nearest real key.
|
|
219
|
+
|
|
220
|
+
#### Keeping every word in the catalog (`@widgetic/design-system/i18n/audit`)
|
|
221
|
+
|
|
222
|
+
For a package's tests only: it reads the source with the Svelte parser and runs in Node, so it must never be imported by code that runs in a page. Three audits run over a package's own source, so a new English sentence cannot slip into a component without somebody noticing.
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
// my-package/tests/i18n.test.ts (node:test; the same calls work in vitest)
|
|
226
|
+
import assert from 'node:assert/strict';
|
|
227
|
+
import { test } from 'node:test';
|
|
228
|
+
import { auditCatalogUsage, auditDesignSystemWords, auditHardCodedText } from '@widgetic/design-system/i18n/audit';
|
|
229
|
+
import { MY_MESSAGES } from '../src/lib/i18n.ts';
|
|
230
|
+
|
|
231
|
+
const folder = new URL('../src/lib', import.meta.url).pathname;
|
|
232
|
+
const catalogFile = new URL('../src/lib/i18n.ts', import.meta.url).pathname;
|
|
233
|
+
|
|
234
|
+
test('no sentence is written into a component', () => {
|
|
235
|
+
const report = auditHardCodedText({
|
|
236
|
+
folder,
|
|
237
|
+
exclude: [catalogFile],
|
|
238
|
+
allowed: [{ file: 'Specimen.svelte', text: 'aA', reason: 'A type specimen, not a word.' }]
|
|
239
|
+
});
|
|
240
|
+
assert.deepEqual(report.loose, []); // one line for each text that is not in the catalog
|
|
241
|
+
assert.deepEqual(report.unusedAllowances, []); // an allowance whose text is gone: take it off
|
|
242
|
+
assert.deepEqual(report.allowancesWithoutReason, []); // a reason is at least twelve characters
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('the code and the catalog agree', () => {
|
|
246
|
+
const report = auditCatalogUsage({ folder, catalog: MY_MESSAGES, exclude: [catalogFile] });
|
|
247
|
+
assert.deepEqual(report.unknownKeys, []); // asked for, but not in the catalog
|
|
248
|
+
assert.deepEqual(report.wrongValues, []); // a value the message needs and is not given, or one it never uses
|
|
249
|
+
assert.deepEqual(report.unusedKeys, []); // in the catalog, and nothing asks for it
|
|
250
|
+
assert.deepEqual(report.messageProblems, []); // a message that does not parse
|
|
251
|
+
assert.deepEqual(report.oddKeys, []); // not `group.name` in camelCase
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test('every design system component that shows words is given all of them', () => {
|
|
255
|
+
const report = auditDesignSystemWords({
|
|
256
|
+
folder,
|
|
257
|
+
componentsWithWords: { ActionModal: ['closeLabel'], Breadcrumbs: ['backLabel', 'rootLabel'] },
|
|
258
|
+
withoutWords: ['Button', 'Tooltip']
|
|
259
|
+
});
|
|
260
|
+
assert.deepEqual(report.missing, []); // a component used without one of its words
|
|
261
|
+
assert.deepEqual(report.unknown, []); // a component the package imports that is in neither list
|
|
262
|
+
assert.deepEqual(report.stale, []); // a name in a list that the package no longer imports
|
|
263
|
+
});
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
- What counts as a word: text in the markup, the attributes that carry words (`title`, `aria-label`, `alt`, `placeholder`, `label`, …), the properties and variables with a word's name (`label`, `message`, `errorMessage`, …) and the strings a listed function returns. Pass `textAttributes`, `textProperties`, `textFunctions` and `messageLookups` to add what a package calls things; they add to the defaults, they never replace them. Pass `messageKeys` (the keys of the catalog) when a table of keys sits in a property that is also a word's name (`{ text: 'chat.contextType.text' }`): a string that is exactly a key is the name of a message, not words.
|
|
267
|
+
- A text a visitor never reads as a word (a CSS unit, a type specimen, English meant for the host page's code) goes into `allowed` with the reason; the audit fails when the text is gone from its file, so the list never outlives the code.
|
|
268
|
+
- A message is asked for with `$t('group.name')`, `t(…)`, `translate(…)` or `translateEnglish(…)`; a package that has its own function says where the key and the values are with `lookups: { messageForVisitor: { key: 2, values: null } }`. A list of descriptors that carry a key writes it as `messageKey` (and the values as `messageValues`), and the audit checks it like a call. A key built in code (`` $t(`filter.${kind}`) ``) is checked as a prefix: at least one key of the catalog must start with `filter.`.
|
|
269
|
+
|
|
270
|
+
### Right to left (`@widgetic/design-system/i18n` and `components/textDirection`)
|
|
271
|
+
|
|
272
|
+
A screen that can be read from right to left says so once, and the components below it follow. Nothing changes for a screen that says nothing: a component with no provider above it is left to right, as every component here has always been.
|
|
273
|
+
|
|
274
|
+
- `resolveTextDirection(locale)` answers `'rtl'` or `'ltr'` for a language tag. It reads the script the language is written in (`ar` and `ar-EG` are Arabic, `he` is Hebrew, `pa-PK` is Arabic script but `pa-IN` is Gurmukhi, `ar-Latn` is Latin and so left to right), and a short list of languages is only the fallback for a browser without `Intl.Locale`. A blank, missing or unreadable tag is `'ltr'`, and it never throws. `resolveLanguageTag(locale)` gives the `lang` of the same tag in its canonical spelling (`pt_BR` becomes `pt-BR`, a blank one is `en`, one that cannot be read is `undefined`: leave `lang` off).
|
|
275
|
+
- The root component of a screen puts the direction in the context, from a store that follows its page's language, and a component that wraps a library reads it:
|
|
276
|
+
|
|
277
|
+
```svelte
|
|
278
|
+
<!-- the root component of the package -->
|
|
279
|
+
<script lang="ts">
|
|
280
|
+
import { writable } from 'svelte/store';
|
|
281
|
+
import { provideTextDirection, readTextDirection, type TextDirection } from '@widgetic/design-system/components/textDirection';
|
|
282
|
+
|
|
283
|
+
export let dir: TextDirection = 'ltr';
|
|
284
|
+
const textDirection = writable<TextDirection>(readTextDirection(dir)); // anything but 'rtl' is left to right
|
|
285
|
+
provideTextDirection(textDirection);
|
|
286
|
+
$: textDirection.set(readTextDirection(dir));
|
|
287
|
+
</script>
|
|
288
|
+
|
|
289
|
+
<div {dir}>…</div>
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
- Why a context and not only the `dir` attribute: the scroll area and the menus of `bits-ui` write `dir="ltr"` on their own element unless they are given the direction, and that one attribute turns a whole screen back from the inside. The scroll area, tooltip, popover, select, dropdown menu, hover card, breadcrumbs and toaster of this package read the context, so a screen that provides it turns around as a whole, and the toaster opens in the bottom corner where a line starts.
|
|
293
|
+
- `dirForUserText(direction)` is the `dir` for text a visitor wrote (the name of a file): `'auto'` on a right to left screen, so a Latin name is not put in the wrong order in a row of Arabic ones, and `undefined` on a left to right one.
|
|
294
|
+
- `shareTextDirection(givenDirection)` is for the root of a package that takes a `dir` prop which may be left out (`givenDirection` is a store that holds the prop): a `dir` that is given is the direction of that screen whatever the screen above it says, one that is left out follows the screen above it (and is left to right when there is none), and everything below reads the answer from the context. It returns the store of the answer, for the component's own decisions (a position worked out in pixels, an arrow key). `useTextDirection()` is what a component reads it with (left to right when nothing above it provides one) and `useOptionalTextDirection()` tells "left to right because the screen says so" from "nobody said anything".
|
|
295
|
+
- `sideAtStart(direction)` and `sideAtEnd(direction)` give the `side` (`'left'` or `'right'`) of a pop-up that opens at the start or the end of the line: the library names the sides of a pop-up by places on the screen, which do not turn around.
|
|
296
|
+
- Write placement in logical terms: `ms-`/`me-`, `ps-`/`pe-`, `start-`/`end-`, `text-start`/`text-end`, `border-s`/`border-e`, `rounded-s`/`rounded-e`/`rounded-ss`, `float-start` in Tailwind; `margin-inline-start`, `inset-inline-end`, `border-start-start-radius` in CSS. Mirror an arrow that means "forward" with `rtl:-scale-x-100`.
|
|
297
|
+
|
|
298
|
+
#### Keeping it so (`@widgetic/design-system/rtl/audit`)
|
|
299
|
+
|
|
300
|
+
For a package's tests only (it reads files with `node:fs`, so it must never be imported by code that runs in a page). `auditPhysicalPlacement` reads every `.svelte`, `.ts`, `.js` and `.css` file under a folder (not `node_modules`, `dist`, `.d.ts` files or tests) and reports each class, style and CSS rule that puts something on a physical side, with the logical one to write instead:
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
// my-package/tests/rtl.test.ts
|
|
304
|
+
import { auditPhysicalPlacement } from '@widgetic/design-system/rtl/audit';
|
|
305
|
+
|
|
306
|
+
const report = auditPhysicalPlacement({
|
|
307
|
+
folder: new URL('../src', import.meta.url).pathname,
|
|
308
|
+
skipped: [{ path: 'lib/board/', reason: 'A drawing board: its left and right are the picture, not the language.' }]
|
|
309
|
+
});
|
|
310
|
+
expect(report.filesRead).toBeGreaterThan(0); // it looked at something
|
|
311
|
+
expect(report.lines).toEqual([]); // one line for each physical side
|
|
312
|
+
expect(report.unusedSkips).toEqual([]); // a part that was named and is gone: take it off
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
A physical side that is meant (a slider's track, a pixel offset that is the same in both directions) says why with the note `rtl-ok` on the same line or the line above.
|
|
316
|
+
|
|
317
|
+
What the audit reads besides classes and properties: a **shorthand that names the sides by their place** (`margin`, `padding`, `inset`, `scroll-margin`, `scroll-padding`, `border-width`, `border-color`, `border-style` and `border-radius`: `padding: 4px 6px 4px 0` is top, right, bottom, left) is reported when its right and left (or its corners) differ, with the logical property to write (`padding-inline: 0 6px`); one that is the same at both ends is not. A comment hides a line only where it is a comment: a `/*` inside a quoted attribute, an image path or a regular expression does not hide what follows it.
|
|
318
|
+
|
|
319
|
+
What it cannot see (found by looking at a screen in Arabic, not by the audit, so look at one): a box that is put in place **in pixels** from the left (a dock, a pop-up, a divider) has to be mirrored where it is placed; an element that says `position: relative` itself is moved from where it sits in the flow, which on a right to left screen is its right edge, so a box placed with `left:` says `position: absolute` inline; and `dir="auto"` on a field with **nothing in it yet** reads left to right (there is no first letter), so write it only once there is a value and let an empty field take the direction of the screen.
|
|
320
|
+
|
|
321
|
+
### Rich text editor (`@widgetic/design-system/shad-editor`)
|
|
322
|
+
|
|
323
|
+
`ShadEditor` is a TipTap editor with a toolbar. It shows English until the page gives it words in another language:
|
|
324
|
+
|
|
325
|
+
```svelte
|
|
326
|
+
<script lang="ts">
|
|
327
|
+
import { ShadEditor, checkShadEditorMessages } from '@widgetic/design-system/shad-editor';
|
|
328
|
+
|
|
329
|
+
let html = $state('<p>Hello</p>');
|
|
330
|
+
const messages = { 'shadEditor.toolbar.bold': 'Fett (⌘B)', 'shadEditor.table.insert': 'Tabelle einfügen' };
|
|
331
|
+
console.log(checkShadEditorMessages(messages)); // [] when every key exists and every message reads
|
|
332
|
+
</script>
|
|
333
|
+
|
|
334
|
+
<ShadEditor bind:content={html} locale="de" {messages} />
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
- `SHAD_EDITOR_MESSAGES` is the catalog, 85 messages: the tooltip of every toolbar button (the keyboard shortcut is part of the message, so a page that wants `Ctrl` for its visitors writes it there), the menus of text styles and alignment, the link, image, colour and highlight pop-ups, the table menu, the names of the quick colours, search and replace, and the bar over an image in the text. A key the page leaves out keeps its English.
|
|
338
|
+
- `checkShadEditorMessages(messages)` lists the keys that do not exist (with the nearest real one), the values that are not text and the templates that do not read, without showing anything.
|
|
339
|
+
- A change of `locale` or `messages` reaches the toolbar, the menus and the images that are already in the text, without making the editor again.
|
|
340
|
+
- Two words are not the editor's to change: the button of the file chooser in the image pop-up is the browser's own, and the colour picker names its window `color picker` in English.
|
|
341
|
+
|
|
175
342
|
## Troubleshooting
|
|
176
343
|
|
|
177
344
|
### File Case Sensitivity Issues (macOS/Windows Teams)
|
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
* helpUrl="/embed"
|
|
18
18
|
* />
|
|
19
19
|
* ```
|
|
20
|
+
*
|
|
21
|
+
* The words of the pop-up that says whether the copy worked are props too (`copiedTitle`, `copiedDescription`,
|
|
22
|
+
* `copyFailedTitle`, `copyFailedDescription`), so a page in another language can give them. A page that shows its own pop-up
|
|
23
|
+
* on the `copy` and `copyError` events passes `showInternalToast={false}` instead.
|
|
20
24
|
*/
|
|
21
25
|
-->
|
|
22
26
|
<script lang="ts">
|
|
@@ -33,6 +37,10 @@ const dispatch = createEventDispatcher();
|
|
|
33
37
|
// import { toast, Button, Tooltip, Portal } from '@widgetic/design-system/components';
|
|
34
38
|
import { toast, Button, Tooltip, Portal } from './';
|
|
35
39
|
import { IconCopy, IconHelp } from '@tabler/icons-svelte';
|
|
40
|
+
import { sideAtEnd, sideAtStart, useTextDirection } from './textDirection.js';
|
|
41
|
+
|
|
42
|
+
// The pop-ups open on a side of the screen, and the sides at the start and at the end of the line trade places on a right to left one.
|
|
43
|
+
const textDirection = useTextDirection();
|
|
36
44
|
|
|
37
45
|
/**
|
|
38
46
|
* COMPONENT PROPS
|
|
@@ -48,6 +56,12 @@ export let showHelp = true;
|
|
|
48
56
|
export let monospaceContent = true;
|
|
49
57
|
export let showStandaloneCopyButton = false;
|
|
50
58
|
export let standaloneCopyButtonText = '';
|
|
59
|
+
/** The pop-up after a copy that worked: its title and the line under it. */
|
|
60
|
+
export let copiedTitle = 'Copied to clipboard';
|
|
61
|
+
export let copiedDescription = 'Content copied successfully.';
|
|
62
|
+
/** The pop-up after a copy that failed: its title and the line under it. */
|
|
63
|
+
export let copyFailedTitle = 'Copy failed';
|
|
64
|
+
export let copyFailedDescription = 'Please try manual selection';
|
|
51
65
|
export let showButtonText = true;
|
|
52
66
|
export let buttonClass = '';
|
|
53
67
|
export let showInternalToast = true;
|
|
@@ -127,15 +141,15 @@ async function handleCopyButtonClick(e: MouseEvent) {
|
|
|
127
141
|
try {
|
|
128
142
|
await navigator.clipboard.writeText(content);
|
|
129
143
|
if (showInternalToast) {
|
|
130
|
-
showToast('success',
|
|
131
|
-
description:
|
|
144
|
+
showToast('success', copiedTitle, {
|
|
145
|
+
description: copiedDescription,
|
|
132
146
|
});
|
|
133
147
|
}
|
|
134
148
|
dispatch('copy', { content });
|
|
135
149
|
} catch (err) {
|
|
136
150
|
if (showInternalToast) {
|
|
137
|
-
showToast('error',
|
|
138
|
-
description:
|
|
151
|
+
showToast('error', copyFailedTitle, {
|
|
152
|
+
description: copyFailedDescription,
|
|
139
153
|
});
|
|
140
154
|
}
|
|
141
155
|
dispatch('copyError', { error: err });
|
|
@@ -217,7 +231,7 @@ function showToast(type: 'success' | 'error', message: string, options = {}) {
|
|
|
217
231
|
{#if showHelpTooltip}
|
|
218
232
|
<Portal target="editor-tooltips">
|
|
219
233
|
<Tooltip.Content
|
|
220
|
-
side=
|
|
234
|
+
side={sideAtEnd($textDirection)}
|
|
221
235
|
sideOffset={5}
|
|
222
236
|
class="bg-[hsl(var(--dark))] text-white z-200000 overflow-hidden rounded-md px-2 py-1.5 shadow-md widgetic-colors widgetic-radius"
|
|
223
237
|
>
|
|
@@ -247,7 +261,7 @@ function showToast(type: 'success' | 'error', message: string, options = {}) {
|
|
|
247
261
|
>
|
|
248
262
|
<IconCopy class="w-5 h-5" style="width: 20px !important; height: 20px !important;" />
|
|
249
263
|
{#if showButtonText}
|
|
250
|
-
<span class="
|
|
264
|
+
<span class="ms-2">{standaloneCopyButtonText}</span>
|
|
251
265
|
{/if}
|
|
252
266
|
</Button>
|
|
253
267
|
</Tooltip.Trigger>
|
|
@@ -255,7 +269,7 @@ function showToast(type: 'success' | 'error', message: string, options = {}) {
|
|
|
255
269
|
{#if showCopyTooltip}
|
|
256
270
|
<Portal target="editor-tooltips">
|
|
257
271
|
<Tooltip.Content
|
|
258
|
-
side=
|
|
272
|
+
side={sideAtStart($textDirection)}
|
|
259
273
|
sideOffset={5}
|
|
260
274
|
class="bg-[hsl(var(--dark))] text-white z-200000 overflow-hidden rounded-md px-3 py-1.5 shadow-md widgetic-colors widgetic-radius"
|
|
261
275
|
>
|
|
@@ -272,7 +286,7 @@ function showToast(type: 'success' | 'error', message: string, options = {}) {
|
|
|
272
286
|
<Button
|
|
273
287
|
variant="ghost"
|
|
274
288
|
size="icon"
|
|
275
|
-
class="h-10 w-10 {headerBackgroundColor ? headerBackgroundColor : 'hover:bg-white'} active:scale-95 transition-transform duration-150
|
|
289
|
+
class="h-10 w-10 {headerBackgroundColor ? headerBackgroundColor : 'hover:bg-white'} active:scale-95 transition-transform duration-150 me-[-4px]"
|
|
276
290
|
onclick={handleCopyButtonClick}
|
|
277
291
|
onmouseenter={handleCopyMouseEnter}
|
|
278
292
|
onmousemove={handleMouseMove}
|
|
@@ -293,7 +307,7 @@ function showToast(type: 'success' | 'error', message: string, options = {}) {
|
|
|
293
307
|
{#if showCopyTooltip}
|
|
294
308
|
<Portal target="editor-tooltips">
|
|
295
309
|
<Tooltip.Content
|
|
296
|
-
side=
|
|
310
|
+
side={sideAtStart($textDirection)}
|
|
297
311
|
sideOffset={5}
|
|
298
312
|
class="bg-[hsl(var(--dark))] text-white z-200000 overflow-hidden rounded-md px-3 py-1.5 shadow-md widgetic-colors widgetic-radius"
|
|
299
313
|
>
|
|
@@ -312,7 +326,7 @@ function showToast(type: 'success' | 'error', message: string, options = {}) {
|
|
|
312
326
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
313
327
|
<div
|
|
314
328
|
bind:this={contentElement}
|
|
315
|
-
class="copyable-content-area text-[14px] w-full text-
|
|
329
|
+
class="copyable-content-area text-[14px] w-full text-start bg-white relative {monospaceContent ? 'font-mono' : ''}"
|
|
316
330
|
style="
|
|
317
331
|
padding: 16px;
|
|
318
332
|
min-height: 80px;
|
|
@@ -396,7 +410,7 @@ function showToast(type: 'success' | 'error', message: string, options = {}) {
|
|
|
396
410
|
:global(.copyable-header-toast) {
|
|
397
411
|
position: fixed !important;
|
|
398
412
|
bottom: 20px !important;
|
|
399
|
-
|
|
413
|
+
inset-inline-end: 20px !important;
|
|
400
414
|
top: auto !important;
|
|
401
415
|
display: flex !important;
|
|
402
416
|
align-items: center !important;
|
|
@@ -24,6 +24,10 @@ declare const CopyableHeader: $$__sveltets_2_IsomorphicComponent<{
|
|
|
24
24
|
monospaceContent?: boolean;
|
|
25
25
|
showStandaloneCopyButton?: boolean;
|
|
26
26
|
standaloneCopyButtonText?: string;
|
|
27
|
+
/** The pop-up after a copy that worked: its title and the line under it. */ copiedTitle?: string;
|
|
28
|
+
copiedDescription?: string;
|
|
29
|
+
/** The pop-up after a copy that failed: its title and the line under it. */ copyFailedTitle?: string;
|
|
30
|
+
copyFailedDescription?: string;
|
|
27
31
|
showButtonText?: boolean;
|
|
28
32
|
buttonClass?: string;
|
|
29
33
|
showInternalToast?: boolean;
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
<div class="relative widgetic-colors">
|
|
24
24
|
<div class="relative">
|
|
25
25
|
<Input
|
|
26
|
-
class="
|
|
26
|
+
class="pe-[32px]"
|
|
27
27
|
bind:value
|
|
28
28
|
{name}
|
|
29
29
|
type={showPassword ? 'text' : 'password'}
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
<!-- Toggle Password Visibility -->
|
|
37
37
|
<button
|
|
38
38
|
type="button"
|
|
39
|
-
class="absolute top-1/2 -translate-y-1/2
|
|
39
|
+
class="absolute top-1/2 -translate-y-1/2 end-3 cursor-pointer text-[hsl(var(--sage-green))] hover:text-[hsl(var(--dark))]"
|
|
40
40
|
tabindex="-1"
|
|
41
41
|
on:click={() => showPassword = !showPassword}
|
|
42
42
|
>
|
|
@@ -26,7 +26,7 @@ export let element: HTMLElement | null = null;
|
|
|
26
26
|
<ScrollArea bind:ref={element}
|
|
27
27
|
class={(className ? className + ' ' : '') + "scroll-area-container w-full h-full " +
|
|
28
28
|
"widgetic-radius widgetic-colors select-text " +
|
|
29
|
-
"**:!data-scroll-area-viewport:
|
|
29
|
+
"**:!data-scroll-area-viewport:pe-(--scrollbar-width)"}
|
|
30
30
|
style="{style} {scrollOutside ? `width: calc(100% + ${scrollbarWidth}px) !important;` : ''}
|
|
31
31
|
--scrollbar-width: {scrollbarWidth}px; --scrollbar-padding: {scrollbarWidth/4}px; "
|
|
32
32
|
{orientation}
|
|
@@ -38,7 +38,7 @@ export let element: HTMLElement | null = null;
|
|
|
38
38
|
"[&>div]:bg-[hsl(var(--grey1))] hover:[&>div]:bg-[hsl(var(--grey2))] " +
|
|
39
39
|
"[&>div]:transition-colors [&>div]:duration-250"}
|
|
40
40
|
>
|
|
41
|
-
<div class="actual-viewport {viewPortClass ? viewPortClass + ' ' : ''} absolute inset-0 z-[-1]" style="margin-
|
|
41
|
+
<div class="actual-viewport {viewPortClass ? viewPortClass + ' ' : ''} absolute inset-0 z-[-1]" style="margin-inline-end: {scrollbarWidth}px;">
|
|
42
42
|
<!-- <div class="viewport-inner absolute border border-red-500" style="inset: {contentPadding}px;"></div> -->
|
|
43
43
|
</div>
|
|
44
44
|
<slot/>
|
|
@@ -285,10 +285,10 @@
|
|
|
285
285
|
</span>
|
|
286
286
|
</Tooltip.Trigger>
|
|
287
287
|
<Tooltip.Content
|
|
288
|
-
side=
|
|
288
|
+
side={/* rtl-ok: the help text is put where the pointer is, by the style below */ 'right'}
|
|
289
289
|
sideOffset={5}
|
|
290
290
|
class="bg-gray-900 text-white z-99999 overflow-hidden rounded-md px-3 py-1.5 text-sm shadow-md"
|
|
291
|
-
style="position: fixed; left: {mouseX + 20}px; top: {mouseY}px; z-index: 999999;"
|
|
291
|
+
style="position: fixed; left: {mouseX + 20}px; top: {mouseY}px; z-index: 999999; /* rtl-ok: the help text follows the pointer, measured from the left edge of the page */"
|
|
292
292
|
>
|
|
293
293
|
<p class="text-sm">{header.helpText}</p>
|
|
294
294
|
</Tooltip.Content>
|
|
@@ -9,6 +9,7 @@ export { default as BrowserInputController } from './ic/browser/BrowserInputCont
|
|
|
9
9
|
export { default as ActionModal } from './modals/ActionModal.svelte';
|
|
10
10
|
export { default as BaseModal } from './modals/BaseModal.svelte';
|
|
11
11
|
export { default as NewItemDialog } from './modals/NewItemDialog.svelte';
|
|
12
|
+
export type { NewItemDialogMessages, RenameDialogMessages } from './modals/messages';
|
|
12
13
|
export { default as Portal } from './modals/Portal.svelte';
|
|
13
14
|
export { default as RenameDialog } from './modals/RenameDialog.svelte';
|
|
14
15
|
export { Breadcrumbs, type BreadcrumbsProps } from './navigation';
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
export let confirmLabel: string = 'Confirm';
|
|
24
24
|
export let onConfirm: () => Promise<any> = async () => {};
|
|
25
25
|
export let cancelLabel: string = 'Cancel';
|
|
26
|
+
// The words a screen reader says for the close button (see BaseModal)
|
|
27
|
+
export let closeLabel: string = 'Close modal';
|
|
26
28
|
export let onCancel: () => void = () => {};
|
|
27
29
|
export let width: string = '410px';
|
|
28
30
|
export let height: string = '168px';
|
|
@@ -122,6 +124,7 @@
|
|
|
122
124
|
width={width}
|
|
123
125
|
height={height}
|
|
124
126
|
titlePadding="small"
|
|
127
|
+
{closeLabel}
|
|
125
128
|
{modalType}
|
|
126
129
|
{modalData}
|
|
127
130
|
on:restore={handleRestore}
|
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
export let enableSpaceKey = false;
|
|
34
34
|
export let isSelected = false;
|
|
35
35
|
export let showScroll = false;
|
|
36
|
+
// The words a screen reader says for the close button. A page that shows the modal in another language passes its own.
|
|
37
|
+
export let closeLabel: string = 'Close modal';
|
|
36
38
|
|
|
37
39
|
// children prop(svelte 5) to replace the traditional slot(svelte 4)
|
|
38
40
|
export let children: any;
|
|
@@ -286,13 +288,13 @@
|
|
|
286
288
|
></div>
|
|
287
289
|
|
|
288
290
|
<!-- Close button -->
|
|
289
|
-
<div class="modal-close-bt-container absolute top-4
|
|
291
|
+
<div class="modal-close-bt-container absolute top-4 end-4 z-1004" transition:fade={{ duration: transitionDuration, easing }}>
|
|
290
292
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
|
291
293
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
|
292
294
|
<div
|
|
293
295
|
class="close-button widgetic-colors p-0 w-10 h-10 bg-[hsl(var(--dark))] hover:bg-[hsl(var(--grey6))] transition-colors duration-200 rounded-full flex items-center justify-center cursor-pointer"
|
|
294
296
|
on:click|stopPropagation|preventDefault={handleClose}
|
|
295
|
-
aria-label=
|
|
297
|
+
aria-label={closeLabel}
|
|
296
298
|
role="button"
|
|
297
299
|
tabindex="0"
|
|
298
300
|
>
|
|
@@ -10,23 +10,12 @@
|
|
|
10
10
|
// Import BaseModal as a default import using relative path, similar to RenameDialog
|
|
11
11
|
import BaseModal from './BaseModal.svelte';
|
|
12
12
|
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function getNameValidationError(name: string, itemType: string = 'name'): string | null {
|
|
22
|
-
if (!name || typeof name !== 'string') return `${itemType} is required`;
|
|
23
|
-
const trimmed = name.trim();
|
|
24
|
-
if (trimmed.length === 0) return `${itemType} is required`;
|
|
25
|
-
if (!isValidName(name)) {
|
|
26
|
-
return `${itemType} can only contain letters, numbers, and spaces`;
|
|
27
|
-
}
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
13
|
+
// Words of the dialog and the helper that puts values into them
|
|
14
|
+
import type { NewItemDialogMessages } from './messages';
|
|
15
|
+
import { fillMessage } from '../../utils/fillMessage';
|
|
16
|
+
|
|
17
|
+
// What a name may be made of: letters, numbers and white space of any script (see utils/itemName)
|
|
18
|
+
import { findItemNameProblem, isValidItemName, keepAllowedNameCharacters } from '../../utils/itemName';
|
|
30
19
|
|
|
31
20
|
// Props
|
|
32
21
|
let className = '';
|
|
@@ -43,6 +32,10 @@
|
|
|
43
32
|
export let currentPathNames: string[] = [];
|
|
44
33
|
export let confirmLabel = "Create";
|
|
45
34
|
export let cancelLabel = "Cancel";
|
|
35
|
+
// The words a screen reader says for the close button (see BaseModal)
|
|
36
|
+
export let closeLabel = "Close modal";
|
|
37
|
+
// The words of the validation errors; a key that is left out keeps its English text (see ./messages)
|
|
38
|
+
export let messages: Partial<NewItemDialogMessages> = {};
|
|
46
39
|
export let modalType: string = "newItem";
|
|
47
40
|
export let modalWidth = "410px";
|
|
48
41
|
export let modalHeight = "168px";
|
|
@@ -58,6 +51,19 @@
|
|
|
58
51
|
// Compute title based on itemType if not provided
|
|
59
52
|
$: displayTitle = title || `New ${itemType.charAt(0).toUpperCase() + itemType.slice(1)}`;
|
|
60
53
|
|
|
54
|
+
// The words of the validation errors: the page's own, or English built from the item type ("Folder name is required")
|
|
55
|
+
$: itemTypeCapitalized = itemType.charAt(0).toUpperCase() + itemType.slice(1);
|
|
56
|
+
$: text = {
|
|
57
|
+
invalidTitle: messages.invalidTitle ?? `Invalid ${itemType} name`,
|
|
58
|
+
required: messages.required ?? `${itemTypeCapitalized} name is required`,
|
|
59
|
+
invalidCharacters:
|
|
60
|
+
messages.invalidCharacters ?? `${itemTypeCapitalized} name can only contain letters, numbers, and spaces`,
|
|
61
|
+
tooLong: fillMessage(
|
|
62
|
+
messages.tooLong ?? `${itemTypeCapitalized} name cannot exceed {max} characters`,
|
|
63
|
+
{ max: maxLength }
|
|
64
|
+
)
|
|
65
|
+
};
|
|
66
|
+
|
|
61
67
|
$: if (isOpen) {
|
|
62
68
|
itemName = '';
|
|
63
69
|
hasAttemptedSubmit = false;
|
|
@@ -66,15 +72,15 @@
|
|
|
66
72
|
|
|
67
73
|
$: {
|
|
68
74
|
const trimmedName = itemName.trim();
|
|
69
|
-
isValid = trimmedName.length > 0 && itemName.length <= maxLength &&
|
|
75
|
+
isValid = trimmedName.length > 0 && itemName.length <= maxLength && isValidItemName(trimmedName);
|
|
70
76
|
|
|
71
77
|
// Update real-time error message
|
|
72
78
|
if (itemName.length > 0) {
|
|
73
79
|
if (itemName.length > maxLength) {
|
|
74
|
-
currentError =
|
|
80
|
+
currentError = text.tooLong;
|
|
75
81
|
} else {
|
|
76
|
-
const
|
|
77
|
-
currentError =
|
|
82
|
+
const nameProblem = findItemNameProblem(trimmedName);
|
|
83
|
+
currentError = nameProblem ? text[nameProblem] : '';
|
|
78
84
|
}
|
|
79
85
|
} else {
|
|
80
86
|
currentError = '';
|
|
@@ -85,18 +91,18 @@
|
|
|
85
91
|
hasAttemptedSubmit = true;
|
|
86
92
|
|
|
87
93
|
// Validate name format
|
|
88
|
-
const
|
|
89
|
-
if (
|
|
90
|
-
toast.error(
|
|
91
|
-
description:
|
|
94
|
+
const nameProblem = findItemNameProblem(itemName);
|
|
95
|
+
if (nameProblem) {
|
|
96
|
+
toast.error(text.invalidTitle, {
|
|
97
|
+
description: text[nameProblem]
|
|
92
98
|
});
|
|
93
99
|
return;
|
|
94
100
|
}
|
|
95
101
|
|
|
96
102
|
if (itemName.length > maxLength) {
|
|
97
103
|
// Show error toast for name too long
|
|
98
|
-
toast.error(
|
|
99
|
-
description:
|
|
104
|
+
toast.error(text.invalidTitle, {
|
|
105
|
+
description: text.tooLong
|
|
100
106
|
});
|
|
101
107
|
return;
|
|
102
108
|
}
|
|
@@ -106,6 +112,12 @@
|
|
|
106
112
|
handleClose();
|
|
107
113
|
}
|
|
108
114
|
|
|
115
|
+
// Only allow letters, numbers, and spaces (of any script): anything else typed or pasted is dropped
|
|
116
|
+
function keepAllowedCharacters(inputElement: HTMLInputElement) {
|
|
117
|
+
inputElement.value = keepAllowedNameCharacters(inputElement.value);
|
|
118
|
+
itemName = inputElement.value;
|
|
119
|
+
}
|
|
120
|
+
|
|
109
121
|
function handleClose() {
|
|
110
122
|
isOpen = false;
|
|
111
123
|
itemName = '';
|
|
@@ -131,6 +143,7 @@
|
|
|
131
143
|
{modalType}
|
|
132
144
|
modalData={combinedModalData}
|
|
133
145
|
title={showTitle ? displayTitle : undefined}
|
|
146
|
+
{closeLabel}
|
|
134
147
|
{target}
|
|
135
148
|
on:restore={(event: CustomEvent<any>) => {
|
|
136
149
|
if (event.detail) {
|
|
@@ -158,10 +171,15 @@
|
|
|
158
171
|
}}
|
|
159
172
|
oninput={(e) => {
|
|
160
173
|
const target = e.target as HTMLInputElement;
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
174
|
+
// An input method (Japanese, Chinese, Korean...) is still building a word: changing the field now would
|
|
175
|
+
// cancel the word. It is checked when the word is committed, in oncompositionend.
|
|
176
|
+
if ('isComposing' in e && e.isComposing) {
|
|
177
|
+
itemName = target.value;
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
keepAllowedCharacters(target);
|
|
164
181
|
}}
|
|
182
|
+
oncompositionend={(e) => keepAllowedCharacters(e.target as HTMLInputElement)}
|
|
165
183
|
onfocus={(e) => {
|
|
166
184
|
const target = e.target as HTMLInputElement;
|
|
167
185
|
target.placeholder = "";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { NewItemDialogMessages } from './messages';
|
|
1
2
|
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
2
3
|
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
3
4
|
$$bindings?: Bindings;
|
|
@@ -24,6 +25,8 @@ declare const NewItemDialog: $$__sveltets_2_IsomorphicComponent<{
|
|
|
24
25
|
currentPathNames?: string[];
|
|
25
26
|
confirmLabel?: string;
|
|
26
27
|
cancelLabel?: string;
|
|
28
|
+
closeLabel?: string;
|
|
29
|
+
messages?: Partial<NewItemDialogMessages>;
|
|
27
30
|
modalType?: string;
|
|
28
31
|
modalWidth?: string;
|
|
29
32
|
modalHeight?: string;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<!-- Portal component for mounting elements outside their normal DOM hierarchy -->
|
|
2
2
|
<script lang="ts">
|
|
3
3
|
import { onMount, onDestroy } from 'svelte';
|
|
4
|
+
import { readable, type Readable } from 'svelte/store';
|
|
5
|
+
import { useOptionalTextDirection, type TextDirection } from '../textDirection.js';
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* Component props
|
|
@@ -26,6 +28,11 @@
|
|
|
26
28
|
// show logs
|
|
27
29
|
let showLogs = false;
|
|
28
30
|
|
|
31
|
+
// The content leaves the element that carries the direction of its screen, and a pop-up in a layer of the page would read
|
|
32
|
+
// the direction of the page instead. When a screen above gave one, the portal says it again on its own element (and says
|
|
33
|
+
// nothing on a page whose screens never did, so the page's own `dir` stays).
|
|
34
|
+
const screenDirection: Readable<TextDirection | undefined> = useOptionalTextDirection() ?? readable<TextDirection | undefined>(undefined);
|
|
35
|
+
|
|
29
36
|
onMount(() => {
|
|
30
37
|
// if(showLogs) console.log('DS Portal target:', target);
|
|
31
38
|
// If target is an HTML element, use it directly
|
|
@@ -85,6 +92,7 @@
|
|
|
85
92
|
<!-- Portal Container -->
|
|
86
93
|
<div bind:this={portal}
|
|
87
94
|
class="{className ? `${className} ` : ''}portal-contents{clickThrough ? ' click-through' : ''}"
|
|
95
|
+
dir={$screenDirection}
|
|
88
96
|
style={style}
|
|
89
97
|
bind:clientWidth
|
|
90
98
|
bind:clientHeight
|
|
@@ -97,7 +105,7 @@
|
|
|
97
105
|
.portal-contents {
|
|
98
106
|
position: absolute;
|
|
99
107
|
top: 0;
|
|
100
|
-
|
|
108
|
+
inset-inline-start: 0;
|
|
101
109
|
width: 100%;
|
|
102
110
|
height: 100%;
|
|
103
111
|
z-index: 99999999;
|