@besaitech/ng-design-system 0.0.59 → 0.0.60
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 +44 -0
- package/fesm2022/besaitech-ng-design-system.mjs +119 -24
- package/fesm2022/besaitech-ng-design-system.mjs.map +1 -1
- package/index.d.ts +69 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -203,6 +203,50 @@ Import names are the `Sl…` classes; the markup selectors are `sds-…`.
|
|
|
203
203
|
| `SdsTopbar` | `<sds-topbar>` | `appName`, `user`, `(logout)`; `[sdsLogo]` slot |
|
|
204
204
|
| `SdsSidebar` + `SdsNavGroup` + `SdsNavItem` | `<sds-sidebar>` … | Collapsible nav rail; collapse state via root-provided `SidebarStore`; items render `routerLink` or `<button>` |
|
|
205
205
|
|
|
206
|
+
## Theme & cross-app preferences
|
|
207
|
+
|
|
208
|
+
`SdsThemeService` holds the person's **mode** — `'system' | 'light' | 'dark'`, persisted to `localStorage`
|
|
209
|
+
under `sds-theme` — and resolves it to the painted **theme** (`light`/`dark`); `system` follows the OS live.
|
|
210
|
+
`toggle()` / `setTheme()` set an explicit theme, `setMode('system')` goes back to following the OS.
|
|
211
|
+
|
|
212
|
+
Apps of one product can carry the person's language and theme in the URL when they link to each other, so the
|
|
213
|
+
page that opens matches. Sending side:
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
import { appendUiPrefs } from '@besaitech/ng-design-system';
|
|
217
|
+
|
|
218
|
+
appendUiPrefs('https://auth.example/continue#token=abc', { lang: translate.getCurrentLang(), theme: theme.mode() });
|
|
219
|
+
// → https://auth.example/continue?lang=fr&theme=dark#token=abc
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Never put them inside a nested redirect a server matches exactly (`return_to`, `redirect_uri`) — append them to
|
|
223
|
+
the outer URL. Receiving side: an inline script in `<head>` of `index.html` applies, persists and strips them
|
|
224
|
+
**before first paint** (no flash, and no import of this library at the root). `LANG_KEY` is the app's own
|
|
225
|
+
language storage key; keep `LANGS` in step with the app's languages. `readUiPrefs` / `stripUiPrefs` are the same
|
|
226
|
+
logic for code and tests.
|
|
227
|
+
|
|
228
|
+
```html
|
|
229
|
+
<script>
|
|
230
|
+
(function () {
|
|
231
|
+
var LANG_KEY = 'app.lang', LANGS = ['en', 'fr', 'nl'], THEME_KEY = 'sds-theme', MODES = ['system', 'light', 'dark'];
|
|
232
|
+
var url = new URL(location.href), q = url.searchParams, lang = q.get('lang'), mode = q.get('theme');
|
|
233
|
+
try {
|
|
234
|
+
if (LANGS.indexOf(lang) >= 0) localStorage.setItem(LANG_KEY, lang);
|
|
235
|
+
if (MODES.indexOf(mode) >= 0) localStorage.setItem(THEME_KEY, mode);
|
|
236
|
+
} catch (e) {}
|
|
237
|
+
try { lang = localStorage.getItem(LANG_KEY) || lang; mode = localStorage.getItem(THEME_KEY) || mode; } catch (e) {}
|
|
238
|
+
if (LANGS.indexOf(lang) >= 0) document.documentElement.lang = lang;
|
|
239
|
+
var dark = mode === 'dark' || (mode !== 'light' && matchMedia('(prefers-color-scheme: dark)').matches);
|
|
240
|
+
document.documentElement.classList.toggle('dark', dark);
|
|
241
|
+
document.documentElement.style.colorScheme = dark ? 'dark' : 'light';
|
|
242
|
+
if (q.has('lang') || q.has('theme')) {
|
|
243
|
+
q.delete('lang'); q.delete('theme');
|
|
244
|
+
history.replaceState(history.state, '', url.pathname + url.search + url.hash);
|
|
245
|
+
}
|
|
246
|
+
})();
|
|
247
|
+
</script>
|
|
248
|
+
```
|
|
249
|
+
|
|
206
250
|
## Labels & languages
|
|
207
251
|
|
|
208
252
|
The library ships **no translations of its own**. Every visible or spoken string — placeholders,
|
|
@@ -15837,7 +15837,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
15837
15837
|
}]
|
|
15838
15838
|
}], propDecorators: { closeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeLabel", required: false }] }] } });
|
|
15839
15839
|
|
|
15840
|
-
|
|
15840
|
+
/** The localStorage key the mode is persisted under. Also read by the pre-paint boot script (see README). */
|
|
15841
|
+
const SDS_THEME_STORAGE_KEY = 'sds-theme';
|
|
15842
|
+
/** `true` for the three values {@link SdsThemeMode} accepts — for validating stored or URL-supplied input. */
|
|
15843
|
+
function isSdsThemeMode(value) {
|
|
15844
|
+
return value === 'system' || value === 'light' || value === 'dark';
|
|
15845
|
+
}
|
|
15841
15846
|
/**
|
|
15842
15847
|
* Light/dark theme state for the whole app. It owns exactly one thing: the presence of the `.dark`
|
|
15843
15848
|
* class on the document root, which flips every design token defined under `.dark` in the theme CSS
|
|
@@ -15848,53 +15853,80 @@ const STORAGE_KEY = 'sds-theme';
|
|
|
15848
15853
|
*
|
|
15849
15854
|
* ```ts
|
|
15850
15855
|
* private readonly theme = inject(SdsThemeService);
|
|
15851
|
-
* this.theme.toggle(); // flip
|
|
15856
|
+
* this.theme.toggle(); // flip (to an explicit light/dark)
|
|
15857
|
+
* this.theme.setMode('system'); // follow the OS again
|
|
15852
15858
|
* this.theme.isDark(); // read (a signal)
|
|
15859
|
+
* this.theme.mode(); // 'system' | 'light' | 'dark'
|
|
15853
15860
|
* ```
|
|
15854
15861
|
*
|
|
15855
|
-
*
|
|
15856
|
-
*
|
|
15857
|
-
*
|
|
15858
|
-
* safe under SSR / prerender (it simply no-ops without a DOM).
|
|
15862
|
+
* Two layers: {@link mode} is the choice (persisted to `localStorage` under `sds-theme`), {@link theme} is
|
|
15863
|
+
* what that resolves to. With no stored choice the mode is `system`, which tracks the OS setting live —
|
|
15864
|
+
* a first-time visitor in dark at the OS level lands in dark, and follows the OS if it flips. Every
|
|
15865
|
+
* browser access is guarded, so the service is safe under SSR / prerender (it simply no-ops without a DOM).
|
|
15859
15866
|
*/
|
|
15860
15867
|
class SdsThemeService {
|
|
15861
15868
|
document = inject(DOCUMENT);
|
|
15862
|
-
|
|
15869
|
+
/** The person's choice. */
|
|
15870
|
+
mode = signal(this.initialMode(), ...(ngDevMode ? [{ debugName: "mode" }] : []));
|
|
15871
|
+
/** The OS preference, kept live by a `matchMedia` listener. */
|
|
15872
|
+
systemDark = signal(this.queryDark()?.matches ?? false, ...(ngDevMode ? [{ debugName: "systemDark" }] : []));
|
|
15873
|
+
/** The theme painted: the explicit choice, or the OS one under `system`. */
|
|
15874
|
+
theme = computed(() => {
|
|
15875
|
+
const mode = this.mode();
|
|
15876
|
+
return mode === 'system' ? (this.systemDark() ? 'dark' : 'light') : mode;
|
|
15877
|
+
}, ...(ngDevMode ? [{ debugName: "theme" }] : []));
|
|
15863
15878
|
isDark = computed(() => this.theme() === 'dark', ...(ngDevMode ? [{ debugName: "isDark" }] : []));
|
|
15864
15879
|
constructor() {
|
|
15865
|
-
// Keep the DOM class and the stored
|
|
15866
|
-
//
|
|
15867
|
-
effect(() => this.apply(this.theme()));
|
|
15880
|
+
// Keep the DOM class and the stored choice in lockstep with the signals — one effect, so any caller
|
|
15881
|
+
// (toggle, setTheme, setMode, or the OS flipping under `system`) applies uniformly.
|
|
15882
|
+
effect(() => this.apply(this.theme(), this.mode()));
|
|
15883
|
+
const query = this.queryDark();
|
|
15884
|
+
if (query) {
|
|
15885
|
+
const onChange = (event) => this.systemDark.set(event.matches);
|
|
15886
|
+
query.addEventListener?.('change', onChange);
|
|
15887
|
+
inject(DestroyRef).onDestroy(() => query.removeEventListener?.('change', onChange));
|
|
15888
|
+
}
|
|
15868
15889
|
}
|
|
15869
|
-
/** Flip between light and dark. */
|
|
15890
|
+
/** Flip between light and dark (an explicit choice, leaving `system`). */
|
|
15870
15891
|
toggle() {
|
|
15871
|
-
this.
|
|
15892
|
+
this.mode.set(this.theme() === 'light' ? 'dark' : 'light');
|
|
15872
15893
|
}
|
|
15873
15894
|
/** Set an explicit theme. */
|
|
15874
15895
|
setTheme(theme) {
|
|
15875
|
-
this.
|
|
15896
|
+
this.mode.set(theme);
|
|
15876
15897
|
}
|
|
15877
|
-
|
|
15898
|
+
/** Set the choice, `system` included. */
|
|
15899
|
+
setMode(mode) {
|
|
15900
|
+
this.mode.set(mode);
|
|
15901
|
+
}
|
|
15902
|
+
initialMode() {
|
|
15878
15903
|
try {
|
|
15879
|
-
const stored = this.readStorage()?.getItem(
|
|
15880
|
-
if (stored
|
|
15904
|
+
const stored = this.readStorage()?.getItem(SDS_THEME_STORAGE_KEY);
|
|
15905
|
+
if (isSdsThemeMode(stored))
|
|
15881
15906
|
return stored;
|
|
15882
|
-
const win = this.document.defaultView;
|
|
15883
|
-
if (win?.matchMedia('(prefers-color-scheme: dark)').matches)
|
|
15884
|
-
return 'dark';
|
|
15885
15907
|
}
|
|
15886
15908
|
catch {
|
|
15887
|
-
/* no storage
|
|
15909
|
+
/* no storage (SSR, privacy mode) — fall through to following the OS */
|
|
15910
|
+
}
|
|
15911
|
+
return 'system';
|
|
15912
|
+
}
|
|
15913
|
+
queryDark() {
|
|
15914
|
+
try {
|
|
15915
|
+
return this.document.defaultView?.matchMedia?.('(prefers-color-scheme: dark)') ?? null;
|
|
15916
|
+
}
|
|
15917
|
+
catch {
|
|
15918
|
+
return null;
|
|
15888
15919
|
}
|
|
15889
|
-
return 'light';
|
|
15890
15920
|
}
|
|
15891
|
-
apply(theme) {
|
|
15921
|
+
apply(theme, mode) {
|
|
15892
15922
|
const root = this.document.documentElement;
|
|
15893
15923
|
root.classList.toggle('dark', theme === 'dark');
|
|
15894
15924
|
// Let the UA paint form controls / scrollbars to match.
|
|
15895
15925
|
root.style.colorScheme = theme;
|
|
15896
15926
|
try {
|
|
15897
|
-
|
|
15927
|
+
// The CHOICE is stored, not the resolved theme — `system` must stay `system`, or the first visit
|
|
15928
|
+
// would freeze whatever the OS happened to be set to.
|
|
15929
|
+
this.readStorage()?.setItem(SDS_THEME_STORAGE_KEY, mode);
|
|
15898
15930
|
}
|
|
15899
15931
|
catch {
|
|
15900
15932
|
/* storage unavailable — the class is still applied; only persistence is skipped */
|
|
@@ -15984,6 +16016,69 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
15984
16016
|
}]
|
|
15985
16017
|
}], propDecorators: { size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], darkLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "darkLabel", required: false }] }], lightLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "lightLabel", required: false }] }] } });
|
|
15986
16018
|
|
|
16019
|
+
const SDS_UI_PREF_LANG_PARAM = 'lang';
|
|
16020
|
+
const SDS_UI_PREF_THEME_PARAM = 'theme';
|
|
16021
|
+
const ABSOLUTE = /^[a-z][a-z0-9+.-]*:/i;
|
|
16022
|
+
const PLACEHOLDER_ORIGIN = 'http://sds.invalid';
|
|
16023
|
+
/**
|
|
16024
|
+
* `url` with `lang` / `theme` set on its query (overwriting any present), the fragment kept. Accepts an
|
|
16025
|
+
* absolute URL or a root-relative path (returned root-relative). Invalid themes are dropped, not sent.
|
|
16026
|
+
*
|
|
16027
|
+
* ```ts
|
|
16028
|
+
* appendUiPrefs('https://auth.example/continue#token=abc', { lang: 'fr', theme: 'dark' })
|
|
16029
|
+
* // → 'https://auth.example/continue?lang=fr&theme=dark#token=abc'
|
|
16030
|
+
* ```
|
|
16031
|
+
*/
|
|
16032
|
+
function appendUiPrefs(url, prefs) {
|
|
16033
|
+
const absolute = ABSOLUTE.test(url);
|
|
16034
|
+
let parsed;
|
|
16035
|
+
try {
|
|
16036
|
+
parsed = new URL(url, PLACEHOLDER_ORIGIN);
|
|
16037
|
+
}
|
|
16038
|
+
catch {
|
|
16039
|
+
return url;
|
|
16040
|
+
}
|
|
16041
|
+
const lang = prefs.lang?.trim();
|
|
16042
|
+
if (lang) {
|
|
16043
|
+
parsed.searchParams.set(SDS_UI_PREF_LANG_PARAM, lang);
|
|
16044
|
+
}
|
|
16045
|
+
if (isSdsThemeMode(prefs.theme)) {
|
|
16046
|
+
parsed.searchParams.set(SDS_UI_PREF_THEME_PARAM, prefs.theme);
|
|
16047
|
+
}
|
|
16048
|
+
return absolute ? parsed.toString() : parsed.pathname + parsed.search + parsed.hash;
|
|
16049
|
+
}
|
|
16050
|
+
/**
|
|
16051
|
+
* The prefs carried by a query string (`location.search`), validated: `lang` only if it is one of
|
|
16052
|
+
* `supportedLangs`, `theme` only if it is a {@link SdsThemeMode}. Anything else reads as absent.
|
|
16053
|
+
*/
|
|
16054
|
+
function readUiPrefs(search, supportedLangs) {
|
|
16055
|
+
const params = new URLSearchParams(search);
|
|
16056
|
+
const lang = params.get(SDS_UI_PREF_LANG_PARAM);
|
|
16057
|
+
const theme = params.get(SDS_UI_PREF_THEME_PARAM);
|
|
16058
|
+
return {
|
|
16059
|
+
lang: lang && supportedLangs.includes(lang) ? lang : null,
|
|
16060
|
+
theme: isSdsThemeMode(theme) ? theme : null,
|
|
16061
|
+
};
|
|
16062
|
+
}
|
|
16063
|
+
/** `url` without the `lang` / `theme` params (everything else, fragment included, untouched). */
|
|
16064
|
+
function stripUiPrefs(url) {
|
|
16065
|
+
const absolute = ABSOLUTE.test(url);
|
|
16066
|
+
let parsed;
|
|
16067
|
+
try {
|
|
16068
|
+
parsed = new URL(url, PLACEHOLDER_ORIGIN);
|
|
16069
|
+
}
|
|
16070
|
+
catch {
|
|
16071
|
+
return url;
|
|
16072
|
+
}
|
|
16073
|
+
if (!parsed.searchParams.has(SDS_UI_PREF_LANG_PARAM) &&
|
|
16074
|
+
!parsed.searchParams.has(SDS_UI_PREF_THEME_PARAM)) {
|
|
16075
|
+
return url;
|
|
16076
|
+
}
|
|
16077
|
+
parsed.searchParams.delete(SDS_UI_PREF_LANG_PARAM);
|
|
16078
|
+
parsed.searchParams.delete(SDS_UI_PREF_THEME_PARAM);
|
|
16079
|
+
return absolute ? parsed.toString() : parsed.pathname + parsed.search + parsed.hash;
|
|
16080
|
+
}
|
|
16081
|
+
|
|
15987
16082
|
/**
|
|
15988
16083
|
* Shared collapse state for the sidebar and its nav items.
|
|
15989
16084
|
*
|
|
@@ -18322,5 +18417,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
18322
18417
|
* Generated bundle index. Do not edit.
|
|
18323
18418
|
*/
|
|
18324
18419
|
|
|
18325
|
-
export { SDS_ALLERGENS, SDS_ALLERGEN_ICONS, SDS_ALLERGEN_LABELS_EN, SDS_ICONS, SDS_PHONE_COUNTRIES, SdsAccordion, SdsAccordionContent, SdsAccordionHeader, SdsAccordionPanel, SdsAccordionStore, SdsAddressAutocomplete, SdsAddressExtra, SdsAllergenIcon, SdsAvatar, SdsBadge, SdsBulkBar, SdsButton, SdsCalendar, SdsCard, SdsCheckbox, SdsChecklistRow, SdsChip, SdsChipGroup, SdsDatePicker, SdsFileDrop, SdsIcon, SdsImageCropper, SdsInput, SdsInputNumber, SdsLanguageSwitcher, SdsLocalizedInput, SdsMenu, SdsMenuItem, SdsMenuTrigger, SdsMultiSelect, SdsNavGroup, SdsNavItem, SdsOtpInput, SdsPaginator, SdsPhoneInput, SdsRadio, SdsScore, SdsSearchCommand, SdsSegmented, SdsSelect, SdsSelectEmpty, SdsSelectFooter, SdsSelectOption, SdsShell, SdsSidebar, SdsSortableHandle, SdsSortableItem, SdsSortableList, SdsStatusTile, SdsTab, SdsTabList, SdsTabPanel, SdsTabPanels, SdsTable, SdsTableCell, SdsTableEmpty, SdsTableHeader, SdsTableRowExpansion, SdsTabs, SdsTabsStore, SdsTextarea, SdsThemeService, SdsThemeToggle, SdsTimeInput, SdsToastHost, SdsToastService, SdsToggleSwitch, SdsTopbar, SdsTriStateChip, SdsUserMenu, SidebarStore, addDays, addMonths, addMonthsKeepDay, formatDdMmYyyy, formatHhMm, isOutOfRange, isSameDay, isoDay, pad2, sdsFileMatchesAccept, sdsFormatBytes, sdsFormatNational, sdsMatchDialCode, sdsNormalizeSignificant, startOfDay, startOfMonth };
|
|
18420
|
+
export { SDS_ALLERGENS, SDS_ALLERGEN_ICONS, SDS_ALLERGEN_LABELS_EN, SDS_ICONS, SDS_PHONE_COUNTRIES, SDS_THEME_STORAGE_KEY, SDS_UI_PREF_LANG_PARAM, SDS_UI_PREF_THEME_PARAM, SdsAccordion, SdsAccordionContent, SdsAccordionHeader, SdsAccordionPanel, SdsAccordionStore, SdsAddressAutocomplete, SdsAddressExtra, SdsAllergenIcon, SdsAvatar, SdsBadge, SdsBulkBar, SdsButton, SdsCalendar, SdsCard, SdsCheckbox, SdsChecklistRow, SdsChip, SdsChipGroup, SdsDatePicker, SdsFileDrop, SdsIcon, SdsImageCropper, SdsInput, SdsInputNumber, SdsLanguageSwitcher, SdsLocalizedInput, SdsMenu, SdsMenuItem, SdsMenuTrigger, SdsMultiSelect, SdsNavGroup, SdsNavItem, SdsOtpInput, SdsPaginator, SdsPhoneInput, SdsRadio, SdsScore, SdsSearchCommand, SdsSegmented, SdsSelect, SdsSelectEmpty, SdsSelectFooter, SdsSelectOption, SdsShell, SdsSidebar, SdsSortableHandle, SdsSortableItem, SdsSortableList, SdsStatusTile, SdsTab, SdsTabList, SdsTabPanel, SdsTabPanels, SdsTable, SdsTableCell, SdsTableEmpty, SdsTableHeader, SdsTableRowExpansion, SdsTabs, SdsTabsStore, SdsTextarea, SdsThemeService, SdsThemeToggle, SdsTimeInput, SdsToastHost, SdsToastService, SdsToggleSwitch, SdsTopbar, SdsTriStateChip, SdsUserMenu, SidebarStore, addDays, addMonths, addMonthsKeepDay, appendUiPrefs, formatDdMmYyyy, formatHhMm, isOutOfRange, isSameDay, isSdsThemeMode, isoDay, pad2, readUiPrefs, sdsFileMatchesAccept, sdsFormatBytes, sdsFormatNational, sdsMatchDialCode, sdsNormalizeSignificant, startOfDay, startOfMonth, stripUiPrefs };
|
|
18326
18421
|
//# sourceMappingURL=besaitech-ng-design-system.mjs.map
|