@ojiepermana/angular-theme 22.0.35 → 22.0.41
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 +54 -22
- package/fesm2022/ojiepermana-angular-theme-layout-wrapper.mjs +74 -20
- package/fesm2022/ojiepermana-angular-theme-layout.mjs +14 -2
- package/fesm2022/ojiepermana-angular-theme-page.mjs +251 -101
- package/fesm2022/ojiepermana-angular-theme-styles.mjs +375 -53
- package/layout/README.md +21 -19
- package/package.json +3 -3
- package/page/README.md +53 -15
- package/styles/README.md +18 -3
- package/styles/css/{seasonal/base → base}/tailwind.css +9 -5
- package/styles/css/{seasonal/base → base}/theme.css +27 -52
- package/styles/css/{seasonal/base → base}/tokens.css +62 -44
- package/styles/css/color/amber.css +52 -0
- package/styles/css/color/blue.css +52 -0
- package/styles/css/color/brand.css +16 -0
- package/styles/css/color/cyan.css +52 -0
- package/styles/css/color/emerald.css +52 -0
- package/styles/css/color/fuchsia.css +52 -0
- package/styles/css/color/green.css +52 -0
- package/styles/css/color/index.css +19 -0
- package/styles/css/color/indigo.css +52 -0
- package/styles/css/color/lime.css +52 -0
- package/styles/css/color/orange.css +52 -0
- package/styles/css/color/pink.css +52 -0
- package/styles/css/color/purple.css +52 -0
- package/styles/css/color/red.css +52 -0
- package/styles/css/color/rose.css +52 -0
- package/styles/css/color/sky.css +52 -0
- package/styles/css/color/teal.css +52 -0
- package/styles/css/color/violet.css +52 -0
- package/styles/css/color/yellow.css +52 -0
- package/styles/css/index.css +15 -6
- package/styles/css/neutral/gray.css +36 -0
- package/styles/css/neutral/index.css +11 -0
- package/styles/css/neutral/mauve.css +36 -0
- package/styles/css/neutral/mist.css +36 -0
- package/styles/css/neutral/neutral.css +36 -0
- package/styles/css/neutral/olive.css +36 -0
- package/styles/css/neutral/slate.css +36 -0
- package/styles/css/neutral/stone.css +36 -0
- package/styles/css/neutral/taupe.css +36 -0
- package/styles/css/neutral/zinc.css +36 -0
- package/styles/css/radius/index.css +29 -0
- package/styles/css/space/index.css +24 -0
- package/types/ojiepermana-angular-theme-layout-wrapper.d.ts +43 -10
- package/types/ojiepermana-angular-theme-layout.d.ts +1 -0
- package/types/ojiepermana-angular-theme-page.d.ts +88 -36
- package/types/ojiepermana-angular-theme-styles.d.ts +169 -37
- package/styles/css/seasonal/ied/package.css +0 -4
- package/styles/css/seasonal/ied/theme.css +0 -78
- package/styles/css/seasonal/imlek/components.css +0 -87
- package/styles/css/seasonal/imlek/package.css +0 -6
- package/styles/css/seasonal/imlek/tailwind.css +0 -144
- package/styles/css/seasonal/imlek/theme.css +0 -95
- package/styles/css/seasonal/imlek/tokens.css +0 -152
- package/styles/css/seasonal/index.css +0 -6
- package/styles/css/seasonal/natal/package.css +0 -4
- package/styles/css/seasonal/natal/theme.css +0 -78
- package/styles/css/seasonal/new-year/package.css +0 -4
- package/styles/css/seasonal/new-year/theme.css +0 -78
- package/styles/css/seasonal/ramadhan/package.css +0 -4
- package/styles/css/seasonal/ramadhan/theme.css +0 -78
- /package/styles/css/{seasonal/base → base}/components.css +0 -0
- /package/styles/css/{seasonal/base → base}/package.css +0 -0
|
@@ -7,25 +7,103 @@ const THEME_OPTIONS = new InjectionToken('THEME_OPTIONS');
|
|
|
7
7
|
|
|
8
8
|
const THEME_MODES = ['light', 'dark', 'system'];
|
|
9
9
|
const RESOLVED_THEME_MODES = ['light', 'dark'];
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* FluxUI-style accent palettes (axis `theme-color`). `base` = core theme (no
|
|
12
|
+
* override); `brand` = the consumer's `--brand` color. Others re-tint the full
|
|
13
|
+
* palette. Order mirrors the FluxUI accent picker (base…rose) + `brand`.
|
|
14
|
+
*/
|
|
15
|
+
const THEME_COLORS = [
|
|
16
|
+
'base',
|
|
17
|
+
'red',
|
|
18
|
+
'orange',
|
|
19
|
+
'amber',
|
|
20
|
+
'yellow',
|
|
21
|
+
'lime',
|
|
22
|
+
'green',
|
|
23
|
+
'emerald',
|
|
24
|
+
'teal',
|
|
25
|
+
'cyan',
|
|
26
|
+
'sky',
|
|
27
|
+
'blue',
|
|
28
|
+
'indigo',
|
|
29
|
+
'violet',
|
|
30
|
+
'purple',
|
|
31
|
+
'fuchsia',
|
|
32
|
+
'pink',
|
|
33
|
+
'rose',
|
|
34
|
+
'brand',
|
|
35
|
+
];
|
|
36
|
+
/**
|
|
37
|
+
* Neutral (gray) families (axis `theme-neutral`). `base` = core theme neutrals.
|
|
38
|
+
* Overrides only the gray family, composing with any accent.
|
|
39
|
+
*/
|
|
40
|
+
const THEME_NEUTRALS = [
|
|
41
|
+
'base',
|
|
42
|
+
'slate',
|
|
43
|
+
'gray',
|
|
44
|
+
'zinc',
|
|
45
|
+
'neutral',
|
|
46
|
+
'stone',
|
|
47
|
+
'mauve',
|
|
48
|
+
'olive',
|
|
49
|
+
'mist',
|
|
50
|
+
'taupe',
|
|
51
|
+
];
|
|
52
|
+
/**
|
|
53
|
+
* Corner radius presets (axis `theme-radius`). Each drives the single
|
|
54
|
+
* `--radius-base` knob; the whole `--radius-*` scale + `rounded-*` utilities
|
|
55
|
+
* follow. `md` = the 0.625rem default; `full` is the pill extreme.
|
|
56
|
+
*/
|
|
57
|
+
const THEME_RADII = ['none', 'sm', 'md', 'lg', 'xl', 'full'];
|
|
58
|
+
/**
|
|
59
|
+
* Spacing density presets (axis `theme-space`). Each drives the single
|
|
60
|
+
* `--spacing-base` knob; every `p-*`/`m-*`/`gap-*`/`w-*`/`h-*` utility follows.
|
|
61
|
+
* `normal` = the 0.25rem default (no-op baseline).
|
|
62
|
+
*/
|
|
63
|
+
const THEME_SPACES = ['compact', 'normal', 'relaxed', 'spacious'];
|
|
11
64
|
const DEFAULT_THEME_MODE = 'system';
|
|
12
|
-
const
|
|
65
|
+
const DEFAULT_THEME_COLOR = 'base';
|
|
66
|
+
const DEFAULT_THEME_NEUTRAL = 'base';
|
|
67
|
+
const DEFAULT_THEME_RADIUS = 'md';
|
|
68
|
+
const DEFAULT_THEME_SPACE = 'normal';
|
|
13
69
|
const THEME_MODE_STORAGE_KEY = 'theme-mode';
|
|
14
|
-
const
|
|
70
|
+
const THEME_COLOR_STORAGE_KEY = 'theme-color';
|
|
71
|
+
const THEME_NEUTRAL_STORAGE_KEY = 'theme-neutral';
|
|
72
|
+
const THEME_BRAND_STORAGE_KEY = 'theme-brand';
|
|
73
|
+
const THEME_RADIUS_STORAGE_KEY = 'theme-radius';
|
|
74
|
+
const THEME_SPACE_STORAGE_KEY = 'theme-space';
|
|
15
75
|
function isThemeMode(value) {
|
|
16
76
|
return typeof value === 'string' && THEME_MODES.includes(value);
|
|
17
77
|
}
|
|
18
78
|
function isResolvedThemeMode(value) {
|
|
19
79
|
return typeof value === 'string' && RESOLVED_THEME_MODES.includes(value);
|
|
20
80
|
}
|
|
21
|
-
function
|
|
22
|
-
return typeof value === 'string' &&
|
|
81
|
+
function isThemeColor(value) {
|
|
82
|
+
return typeof value === 'string' && THEME_COLORS.includes(value);
|
|
83
|
+
}
|
|
84
|
+
function isThemeNeutral(value) {
|
|
85
|
+
return typeof value === 'string' && THEME_NEUTRALS.includes(value);
|
|
86
|
+
}
|
|
87
|
+
function isThemeRadius(value) {
|
|
88
|
+
return typeof value === 'string' && THEME_RADII.includes(value);
|
|
89
|
+
}
|
|
90
|
+
function isThemeSpace(value) {
|
|
91
|
+
return typeof value === 'string' && THEME_SPACES.includes(value);
|
|
23
92
|
}
|
|
24
93
|
function normalizeThemeMode(value) {
|
|
25
94
|
return isThemeMode(value) ? value : DEFAULT_THEME_MODE;
|
|
26
95
|
}
|
|
27
|
-
function
|
|
28
|
-
return
|
|
96
|
+
function normalizeThemeColor(value) {
|
|
97
|
+
return isThemeColor(value) ? value : DEFAULT_THEME_COLOR;
|
|
98
|
+
}
|
|
99
|
+
function normalizeThemeNeutral(value) {
|
|
100
|
+
return isThemeNeutral(value) ? value : DEFAULT_THEME_NEUTRAL;
|
|
101
|
+
}
|
|
102
|
+
function normalizeThemeRadius(value) {
|
|
103
|
+
return isThemeRadius(value) ? value : DEFAULT_THEME_RADIUS;
|
|
104
|
+
}
|
|
105
|
+
function normalizeThemeSpace(value) {
|
|
106
|
+
return isThemeSpace(value) ? value : DEFAULT_THEME_SPACE;
|
|
29
107
|
}
|
|
30
108
|
|
|
31
109
|
class ThemeModeService {
|
|
@@ -127,67 +205,170 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImpor
|
|
|
127
205
|
type: Service
|
|
128
206
|
}], ctorParameters: () => [] });
|
|
129
207
|
|
|
130
|
-
|
|
208
|
+
const DEFAULT_BRAND_FOREGROUND = '0 0% 100%';
|
|
209
|
+
/**
|
|
210
|
+
* Consumer-configurable brand color. Writes `--brand` / `--brand-foreground` as
|
|
211
|
+
* inline custom properties on `<html>` so `bg-brand` / `text-brand-foreground`
|
|
212
|
+
* and the `theme-color='brand'` accent preset resolve to it.
|
|
213
|
+
*
|
|
214
|
+
* Default comes from `provideUiTheme({ brand })`; `setBrand()` overrides at
|
|
215
|
+
* runtime and persists. When unset, `--brand` falls back to `var(--primary)`.
|
|
216
|
+
*/
|
|
217
|
+
class ThemeBrandService {
|
|
131
218
|
documentRef = inject(DOCUMENT, { optional: true });
|
|
132
219
|
options = inject(THEME_OPTIONS, { optional: true });
|
|
133
|
-
|
|
134
|
-
...(ngDevMode ? [{ debugName: "
|
|
135
|
-
|
|
220
|
+
brandState = signal(null, /* @ts-ignore */
|
|
221
|
+
...(ngDevMode ? [{ debugName: "brandState" }] : /* istanbul ignore next */ []));
|
|
222
|
+
brand = this.brandState.asReadonly();
|
|
136
223
|
constructor() {
|
|
137
224
|
this.ensureDefaults();
|
|
138
225
|
effect(() => {
|
|
139
|
-
const
|
|
140
|
-
this.
|
|
141
|
-
this.
|
|
142
|
-
this.ensureSeasonStylesheet(season);
|
|
226
|
+
const brand = this.brandState();
|
|
227
|
+
this.persist(brand);
|
|
228
|
+
this.applyBrand(brand);
|
|
143
229
|
});
|
|
144
230
|
}
|
|
145
|
-
|
|
146
|
-
|
|
231
|
+
/** Set the brand color. `color`/`foreground` are HSL triplets (e.g. `'221 83% 53%'`). */
|
|
232
|
+
setBrand(color, foreground = DEFAULT_BRAND_FOREGROUND) {
|
|
233
|
+
this.brandState.set({ color, foreground });
|
|
234
|
+
}
|
|
235
|
+
/** Clear the consumer brand → `--brand` falls back to `var(--primary)`. */
|
|
236
|
+
clearBrand() {
|
|
237
|
+
this.brandState.set(null);
|
|
147
238
|
}
|
|
148
239
|
ensureDefaults() {
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
240
|
+
const stored = this.readStoredBrand();
|
|
241
|
+
if (stored) {
|
|
242
|
+
this.brandState.set(stored);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const fromOptions = this.normalizeOption(this.options?.brand);
|
|
246
|
+
if (fromOptions) {
|
|
247
|
+
this.brandState.set(fromOptions);
|
|
248
|
+
}
|
|
152
249
|
}
|
|
153
|
-
|
|
154
|
-
|
|
250
|
+
normalizeOption(brand) {
|
|
251
|
+
if (!brand) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
if (typeof brand === 'string') {
|
|
255
|
+
return { color: brand, foreground: DEFAULT_BRAND_FOREGROUND };
|
|
256
|
+
}
|
|
257
|
+
return { color: brand.color, foreground: brand.foreground ?? DEFAULT_BRAND_FOREGROUND };
|
|
155
258
|
}
|
|
156
|
-
|
|
259
|
+
applyBrand(brand) {
|
|
157
260
|
const root = this.documentRef?.documentElement;
|
|
158
261
|
if (!root) {
|
|
159
262
|
return;
|
|
160
263
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
* Lazily loads the active season's stylesheet via `<link>` when the app
|
|
165
|
-
* configured `seasonalCssUrl`, so only base CSS ships in the main bundle.
|
|
166
|
-
*/
|
|
167
|
-
ensureSeasonStylesheet(season) {
|
|
168
|
-
const doc = this.documentRef;
|
|
169
|
-
const resolveUrl = this.options?.seasonalCssUrl;
|
|
170
|
-
if (!doc || !resolveUrl) {
|
|
264
|
+
if (!brand) {
|
|
265
|
+
root.style.removeProperty('--brand');
|
|
266
|
+
root.style.removeProperty('--brand-foreground');
|
|
171
267
|
return;
|
|
172
268
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
return;
|
|
269
|
+
root.style.setProperty('--brand', brand.color);
|
|
270
|
+
root.style.setProperty('--brand-foreground', brand.foreground);
|
|
271
|
+
}
|
|
272
|
+
storage() {
|
|
273
|
+
try {
|
|
274
|
+
return this.documentRef?.defaultView?.localStorage ?? null;
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return null;
|
|
179
278
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
279
|
+
}
|
|
280
|
+
readStoredBrand() {
|
|
281
|
+
try {
|
|
282
|
+
const raw = this.storage()?.getItem(THEME_BRAND_STORAGE_KEY);
|
|
283
|
+
if (!raw) {
|
|
284
|
+
return null;
|
|
183
285
|
}
|
|
286
|
+
const parsed = JSON.parse(raw);
|
|
287
|
+
if (typeof parsed?.color === 'string') {
|
|
288
|
+
return {
|
|
289
|
+
color: parsed.color,
|
|
290
|
+
foreground: typeof parsed.foreground === 'string' ? parsed.foreground : DEFAULT_BRAND_FOREGROUND,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
persist(brand) {
|
|
300
|
+
try {
|
|
301
|
+
const storage = this.storage();
|
|
302
|
+
if (!storage) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (!brand) {
|
|
306
|
+
storage.removeItem(THEME_BRAND_STORAGE_KEY);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
storage.setItem(THEME_BRAND_STORAGE_KEY, JSON.stringify(brand));
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
184
312
|
return;
|
|
185
313
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
314
|
+
}
|
|
315
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: ThemeBrandService, deps: [], target: i0.ɵɵFactoryTarget.Service });
|
|
316
|
+
static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.0.3", ngImport: i0, type: ThemeBrandService });
|
|
317
|
+
}
|
|
318
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: ThemeBrandService, decorators: [{
|
|
319
|
+
type: Service
|
|
320
|
+
}], ctorParameters: () => [] });
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Drives the two FluxUI-style color axes:
|
|
324
|
+
* - accent → `<html theme-color>` (full tinted palette per color)
|
|
325
|
+
* - neutral → `<html theme-neutral>` (gray family only)
|
|
326
|
+
*
|
|
327
|
+
* Both persist to localStorage and seed from `provideUiTheme({ color, neutral })`
|
|
328
|
+
* (a persisted choice wins over the configured default). `base` on either axis
|
|
329
|
+
* means "no override" — the core base theme applies.
|
|
330
|
+
*/
|
|
331
|
+
class ThemeColorService {
|
|
332
|
+
documentRef = inject(DOCUMENT, { optional: true });
|
|
333
|
+
options = inject(THEME_OPTIONS, { optional: true });
|
|
334
|
+
colorState = signal(DEFAULT_THEME_COLOR, /* @ts-ignore */
|
|
335
|
+
...(ngDevMode ? [{ debugName: "colorState" }] : /* istanbul ignore next */ []));
|
|
336
|
+
neutralState = signal(DEFAULT_THEME_NEUTRAL, /* @ts-ignore */
|
|
337
|
+
...(ngDevMode ? [{ debugName: "neutralState" }] : /* istanbul ignore next */ []));
|
|
338
|
+
color = this.colorState.asReadonly();
|
|
339
|
+
neutral = this.neutralState.asReadonly();
|
|
340
|
+
constructor() {
|
|
341
|
+
this.ensureDefaults();
|
|
342
|
+
effect(() => {
|
|
343
|
+
const color = this.colorState();
|
|
344
|
+
this.persist(THEME_COLOR_STORAGE_KEY, color);
|
|
345
|
+
this.applyAttribute('theme-color', color);
|
|
346
|
+
});
|
|
347
|
+
effect(() => {
|
|
348
|
+
const neutral = this.neutralState();
|
|
349
|
+
this.persist(THEME_NEUTRAL_STORAGE_KEY, neutral);
|
|
350
|
+
this.applyAttribute('theme-neutral', neutral);
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
setColor(color) {
|
|
354
|
+
this.colorState.set(color);
|
|
355
|
+
}
|
|
356
|
+
setNeutral(neutral) {
|
|
357
|
+
this.neutralState.set(neutral);
|
|
358
|
+
}
|
|
359
|
+
ensureDefaults() {
|
|
360
|
+
// A configured brand with no explicit accent defaults the accent to 'brand'
|
|
361
|
+
// so the consumer's brand color becomes the primary (FluxUI-style).
|
|
362
|
+
const configuredColor = this.options?.color ?? (this.options?.brand ? 'brand' : undefined);
|
|
363
|
+
const defaultColor = normalizeThemeColor(configuredColor);
|
|
364
|
+
const storedColor = this.readStorage(THEME_COLOR_STORAGE_KEY) ?? defaultColor;
|
|
365
|
+
this.colorState.set(normalizeThemeColor(storedColor));
|
|
366
|
+
const defaultNeutral = normalizeThemeNeutral(this.options?.neutral);
|
|
367
|
+
const storedNeutral = this.readStorage(THEME_NEUTRAL_STORAGE_KEY) ?? defaultNeutral;
|
|
368
|
+
this.neutralState.set(normalizeThemeNeutral(storedNeutral));
|
|
369
|
+
}
|
|
370
|
+
applyAttribute(attribute, value) {
|
|
371
|
+
this.documentRef?.documentElement?.setAttribute(attribute, value);
|
|
191
372
|
}
|
|
192
373
|
storage() {
|
|
193
374
|
try {
|
|
@@ -205,7 +386,7 @@ class ThemeSeasonService {
|
|
|
205
386
|
return null;
|
|
206
387
|
}
|
|
207
388
|
}
|
|
208
|
-
|
|
389
|
+
persist(key, value) {
|
|
209
390
|
try {
|
|
210
391
|
this.storage()?.setItem(key, value);
|
|
211
392
|
}
|
|
@@ -213,10 +394,148 @@ class ThemeSeasonService {
|
|
|
213
394
|
return;
|
|
214
395
|
}
|
|
215
396
|
}
|
|
216
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type:
|
|
217
|
-
static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.0.3", ngImport: i0, type:
|
|
397
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: ThemeColorService, deps: [], target: i0.ɵɵFactoryTarget.Service });
|
|
398
|
+
static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.0.3", ngImport: i0, type: ThemeColorService });
|
|
399
|
+
}
|
|
400
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: ThemeColorService, decorators: [{
|
|
401
|
+
type: Service
|
|
402
|
+
}], ctorParameters: () => [] });
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Corner radius axis. Writes `<html theme-radius>`; each preset CSS sets the
|
|
406
|
+
* single `--radius-base` knob so the whole `--radius-*` scale + `rounded-*`
|
|
407
|
+
* utilities rescale. Persists to localStorage and seeds from
|
|
408
|
+
* `provideUiTheme({ radius })` (a persisted choice wins over the default).
|
|
409
|
+
*/
|
|
410
|
+
class ThemeRadiusService {
|
|
411
|
+
documentRef = inject(DOCUMENT, { optional: true });
|
|
412
|
+
options = inject(THEME_OPTIONS, { optional: true });
|
|
413
|
+
radiusState = signal(DEFAULT_THEME_RADIUS, /* @ts-ignore */
|
|
414
|
+
...(ngDevMode ? [{ debugName: "radiusState" }] : /* istanbul ignore next */ []));
|
|
415
|
+
radius = this.radiusState.asReadonly();
|
|
416
|
+
radiusPx = computed(() => {
|
|
417
|
+
switch (this.radius()) {
|
|
418
|
+
case 'none':
|
|
419
|
+
return 0;
|
|
420
|
+
case 'sm':
|
|
421
|
+
return 6;
|
|
422
|
+
case 'md':
|
|
423
|
+
return 10;
|
|
424
|
+
case 'lg':
|
|
425
|
+
return 14;
|
|
426
|
+
case 'xl':
|
|
427
|
+
return 20;
|
|
428
|
+
case 'full':
|
|
429
|
+
return 9999;
|
|
430
|
+
default:
|
|
431
|
+
return 10;
|
|
432
|
+
}
|
|
433
|
+
}, /* @ts-ignore */
|
|
434
|
+
...(ngDevMode ? [{ debugName: "radiusPx" }] : /* istanbul ignore next */ []));
|
|
435
|
+
constructor() {
|
|
436
|
+
this.ensureDefaults();
|
|
437
|
+
effect(() => {
|
|
438
|
+
const radius = this.radiusState();
|
|
439
|
+
this.persist(radius);
|
|
440
|
+
this.documentRef?.documentElement?.setAttribute('theme-radius', radius);
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
setRadius(radius) {
|
|
444
|
+
this.radiusState.set(radius);
|
|
445
|
+
}
|
|
446
|
+
ensureDefaults() {
|
|
447
|
+
const fallback = normalizeThemeRadius(this.options?.radius);
|
|
448
|
+
const stored = this.readStorage() ?? fallback;
|
|
449
|
+
this.radiusState.set(normalizeThemeRadius(stored));
|
|
450
|
+
}
|
|
451
|
+
storage() {
|
|
452
|
+
try {
|
|
453
|
+
return this.documentRef?.defaultView?.localStorage ?? null;
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
readStorage() {
|
|
460
|
+
try {
|
|
461
|
+
return this.storage()?.getItem(THEME_RADIUS_STORAGE_KEY) ?? null;
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
persist(radius) {
|
|
468
|
+
try {
|
|
469
|
+
this.storage()?.setItem(THEME_RADIUS_STORAGE_KEY, radius);
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: ThemeRadiusService, deps: [], target: i0.ɵɵFactoryTarget.Service });
|
|
476
|
+
static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.0.3", ngImport: i0, type: ThemeRadiusService });
|
|
477
|
+
}
|
|
478
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: ThemeRadiusService, decorators: [{
|
|
479
|
+
type: Service
|
|
480
|
+
}], ctorParameters: () => [] });
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Spacing density axis. Writes `<html theme-space>`; each preset CSS sets the
|
|
484
|
+
* single `--spacing-base` knob, which Tailwind's `--spacing` derives from, so
|
|
485
|
+
* every `p-*`/`m-*`/`gap-*`/`w-*`/`h-*` utility rescales. Persists to
|
|
486
|
+
* localStorage and seeds from `provideUiTheme({ space })` (a persisted choice
|
|
487
|
+
* wins over the default).
|
|
488
|
+
*/
|
|
489
|
+
class ThemeSpaceService {
|
|
490
|
+
documentRef = inject(DOCUMENT, { optional: true });
|
|
491
|
+
options = inject(THEME_OPTIONS, { optional: true });
|
|
492
|
+
spaceState = signal(DEFAULT_THEME_SPACE, /* @ts-ignore */
|
|
493
|
+
...(ngDevMode ? [{ debugName: "spaceState" }] : /* istanbul ignore next */ []));
|
|
494
|
+
space = this.spaceState.asReadonly();
|
|
495
|
+
constructor() {
|
|
496
|
+
this.ensureDefaults();
|
|
497
|
+
effect(() => {
|
|
498
|
+
const space = this.spaceState();
|
|
499
|
+
this.persist(space);
|
|
500
|
+
this.documentRef?.documentElement?.setAttribute('theme-space', space);
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
setSpace(space) {
|
|
504
|
+
this.spaceState.set(space);
|
|
505
|
+
}
|
|
506
|
+
ensureDefaults() {
|
|
507
|
+
const fallback = normalizeThemeSpace(this.options?.space);
|
|
508
|
+
const stored = this.readStorage() ?? fallback;
|
|
509
|
+
this.spaceState.set(normalizeThemeSpace(stored));
|
|
510
|
+
}
|
|
511
|
+
storage() {
|
|
512
|
+
try {
|
|
513
|
+
return this.documentRef?.defaultView?.localStorage ?? null;
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
readStorage() {
|
|
520
|
+
try {
|
|
521
|
+
return this.storage()?.getItem(THEME_SPACE_STORAGE_KEY) ?? null;
|
|
522
|
+
}
|
|
523
|
+
catch {
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
persist(space) {
|
|
528
|
+
try {
|
|
529
|
+
this.storage()?.setItem(THEME_SPACE_STORAGE_KEY, space);
|
|
530
|
+
}
|
|
531
|
+
catch {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: ThemeSpaceService, deps: [], target: i0.ɵɵFactoryTarget.Service });
|
|
536
|
+
static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.0.3", ngImport: i0, type: ThemeSpaceService });
|
|
218
537
|
}
|
|
219
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type:
|
|
538
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: ThemeSpaceService, decorators: [{
|
|
220
539
|
type: Service
|
|
221
540
|
}], ctorParameters: () => [] });
|
|
222
541
|
|
|
@@ -235,7 +554,10 @@ function provideUiTheme(options = {}) {
|
|
|
235
554
|
inject(MaterialSymbolsService).ensureLoaded();
|
|
236
555
|
}
|
|
237
556
|
inject(ThemeModeService);
|
|
238
|
-
inject(
|
|
557
|
+
inject(ThemeColorService);
|
|
558
|
+
inject(ThemeBrandService);
|
|
559
|
+
inject(ThemeRadiusService);
|
|
560
|
+
inject(ThemeSpaceService);
|
|
239
561
|
}),
|
|
240
562
|
]);
|
|
241
563
|
}
|
|
@@ -244,4 +566,4 @@ function provideUiTheme(options = {}) {
|
|
|
244
566
|
* Generated bundle index. Do not edit.
|
|
245
567
|
*/
|
|
246
568
|
|
|
247
|
-
export { DEFAULT_THEME_MODE,
|
|
569
|
+
export { DEFAULT_THEME_COLOR, DEFAULT_THEME_MODE, DEFAULT_THEME_NEUTRAL, DEFAULT_THEME_RADIUS, DEFAULT_THEME_SPACE, RESOLVED_THEME_MODES, THEME_BRAND_STORAGE_KEY, THEME_COLORS, THEME_COLOR_STORAGE_KEY, THEME_MODES, THEME_MODE_STORAGE_KEY, THEME_NEUTRALS, THEME_NEUTRAL_STORAGE_KEY, THEME_OPTIONS, THEME_RADII, THEME_RADIUS_STORAGE_KEY, THEME_SPACES, THEME_SPACE_STORAGE_KEY, ThemeBrandService, ThemeColorService, ThemeModeService, ThemeRadiusService, ThemeSpaceService, isResolvedThemeMode, isThemeColor, isThemeMode, isThemeNeutral, isThemeRadius, isThemeSpace, normalizeThemeColor, normalizeThemeMode, normalizeThemeNeutral, normalizeThemeRadius, normalizeThemeSpace, provideUiTheme };
|
package/layout/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @ojiepermana/angular-theme/layout
|
|
2
2
|
|
|
3
|
-
Primitive layout projection untuk menyusun shell UI dengan satu root `Layout`, satu variant layout (`vertical`, `horizontal`, `empty`, atau `fluid`), slot `
|
|
3
|
+
Primitive layout projection untuk menyusun shell UI dengan satu root `Layout`, satu variant layout (`vertical`, `horizontal`, `empty`, atau `fluid`), slot `LayoutNavigation`, dan area `LayoutContent` yang menjadi scroll container utama.
|
|
4
4
|
|
|
5
5
|
README ini mendokumentasikan seluruh API publik yang diekspor oleh package agar consumer bisa memahami kontrak komponen, type, service, dan perilaku layout tanpa perlu membaca source code.
|
|
6
6
|
|
|
@@ -65,17 +65,17 @@ Struktur yang direkomendasikan adalah sebagai berikut.
|
|
|
65
65
|
|
|
66
66
|
```html
|
|
67
67
|
<Layout>
|
|
68
|
-
<LayoutVertical |
|
|
68
|
+
<LayoutVertical | LayoutHorizontal | LayoutEmpty | LayoutFluid>
|
|
69
69
|
<LayoutNavigation>...</LayoutNavigation>
|
|
70
70
|
<LayoutContent>...</LayoutContent>
|
|
71
|
-
</
|
|
71
|
+
</LayoutVertical | LayoutHorizontal | LayoutEmpty | LayoutFluid>
|
|
72
72
|
</Layout>
|
|
73
73
|
```
|
|
74
74
|
|
|
75
75
|
Aturan penggunaannya:
|
|
76
76
|
|
|
77
77
|
- Gunakan tepat satu variant layout di dalam `Layout`.
|
|
78
|
-
- Untuk layout `vertical` dan `horizontal`, urutan child yang umum adalah `
|
|
78
|
+
- Untuk layout `vertical` dan `horizontal`, urutan child yang umum adalah `LayoutNavigation` lalu `LayoutContent`.
|
|
79
79
|
- Untuk layout `empty`, biasanya cukup `LayoutContent`.
|
|
80
80
|
- Untuk layout `fluid`, child dapat berupa konten page tunggal yang ingin dipusatkan terhadap frame.
|
|
81
81
|
- `LayoutContent` adalah area yang memiliki `overflow-auto`, jadi konten utama sebaiknya ditempatkan di sana.
|
|
@@ -194,13 +194,14 @@ Selector: `Layout`
|
|
|
194
194
|
|
|
195
195
|
Inputs:
|
|
196
196
|
|
|
197
|
-
| Input | Type | Default | Deskripsi
|
|
198
|
-
| ------------------- | -------------------------- | -------- |
|
|
199
|
-
| `surface` | `LayoutSurface` | `'flat'` | Menentukan fallback background root layout. Jika local storage `layout-surface` berisi nilai valid, nilai storage yang dipakai.
|
|
200
|
-
| `appearance` | `LayoutAppearance or null` | `null` | API template untuk menentukan fallback appearance frame. Jika input ini kosong, primitive mencoba alias `layout-appearance`, lalu fallback efektif akhirnya `flat`.
|
|
201
|
-
| `layout-appearance` | `LayoutAppearance or null` | `null` | Alias simetris dengan `nav-appearance` pada navigation (kosakata sama `flat \| border-rail`). Memberi `appearance` yang sama ke shell & nav membuat keduanya seragam.
|
|
202
|
-
| `width` | `LayoutWidth` | `'full'` | Menentukan fallback padding outer dan perilaku container frame. Jika local storage `layout-width` berisi nilai valid, nilai storage yang dipakai.
|
|
203
|
-
| `
|
|
197
|
+
| Input | Type | Default | Deskripsi |
|
|
198
|
+
| ------------------- | -------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
199
|
+
| `surface` | `LayoutSurface` | `'flat'` | Menentukan fallback background root layout. Jika local storage `layout-surface` berisi nilai valid, nilai storage yang dipakai. |
|
|
200
|
+
| `appearance` | `LayoutAppearance or null` | `null` | API template untuk menentukan fallback appearance frame. Jika input ini kosong, primitive mencoba alias `layout-appearance`, lalu fallback efektif akhirnya `flat`. |
|
|
201
|
+
| `layout-appearance` | `LayoutAppearance or null` | `null` | Alias simetris dengan `nav-appearance` pada navigation (kosakata sama `flat \| border-rail`). Memberi `appearance` yang sama ke shell & nav membuat keduanya seragam. |
|
|
202
|
+
| `width` | `LayoutWidth` | `'full'` | Menentukan fallback padding outer dan perilaku container frame. Jika local storage `layout-width` berisi nilai valid, nilai storage yang dipakai. |
|
|
203
|
+
| `layout-type` | `LayoutType or null` | `null` | Override eksplisit type aktif (`vertical \| horizontal \| empty \| fluid`). Jika di-set, menjadi sumber kebenaran type dan menimpa variant `Layout*`. Jika `null`, variant yang menetapkan. |
|
|
204
|
+
| `class` | `string` | `''` | Menambahkan class pada host `Layout`. |
|
|
204
205
|
|
|
205
206
|
Behavior:
|
|
206
207
|
|
|
@@ -208,7 +209,7 @@ Behavior:
|
|
|
208
209
|
- Jika input manual kosong, primitive mendaftarkan default `surface`, `appearance`, dan `width` ke `LayoutService` hanya saat local storage belum memiliki nilai valid.
|
|
209
210
|
- Nilai visual final selalu dibaca kembali dari `LayoutService`, sehingga child primitive tetap membaca state yang sama selama instance aktif.
|
|
210
211
|
- Menambahkan atribut host `data-surface`, `data-layout-appearance`, `data-layout-width`, dan `data-layout-type`.
|
|
211
|
-
- Root
|
|
212
|
+
- Root menyediakan input opsional `layout-type`. Bila di-set, input ini menjadi sumber kebenaran type aktif dan menimpa variant layout yang dirender (override in-memory tanpa menulis `localStorage`). Bila `null`, type aktif dikendalikan oleh variant layout yang dirender, atau oleh consumer melalui `LayoutService.registerDefaults({ type })` sebelum template mengevaluasi `layout.type()`.
|
|
212
213
|
- Selalu merender frame border.
|
|
213
214
|
- Jika `appearance="border-rail"`, root menambah rail dekoratif di empat sudut frame, rail inset horizontal, dan rail vertikal sekunder di luar sisi kiri-kanan frame sehingga frame terlihat seperti memiliki double rail.
|
|
214
215
|
- Jika `width="container"`, frame dipusatkan mulai breakpoint `lg` dengan container behavior.
|
|
@@ -266,20 +267,21 @@ Behavior:
|
|
|
266
267
|
|
|
267
268
|
- Mengatur `LayoutService.type` menjadi `'empty'` hanya untuk state aktif dan tidak menulis `localStorage`.
|
|
268
269
|
- Menyediakan wrapper penuh untuk satu area konten.
|
|
269
|
-
- `
|
|
270
|
+
- `LayoutNavigation` akan tersembunyi jika tetap dirender di mode ini.
|
|
270
271
|
|
|
271
272
|
### `LayoutNavigationComponent`
|
|
272
273
|
|
|
273
274
|
Wrapper untuk slot navigasi.
|
|
274
275
|
|
|
275
|
-
Selector: `
|
|
276
|
+
Selector: `LayoutNavigation`
|
|
276
277
|
|
|
277
278
|
Inputs:
|
|
278
279
|
|
|
279
|
-
| Input
|
|
280
|
-
|
|
|
281
|
-
| `ariaLabel`
|
|
282
|
-
| `
|
|
280
|
+
| Input | Type | Default | Deskripsi |
|
|
281
|
+
| ------------ | ---------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
282
|
+
| `ariaLabel` | `string` | `'Layout navigation'` | Label aksesibilitas untuk landmark `navigation`. |
|
|
283
|
+
| `railOffset` | `string \| null` | `null` | Override offset rail vertikal nav (mode `vertical + border-rail`). Jika `null`, mengikuti preview rail offset dari nav yang diproyeksikan. |
|
|
284
|
+
| `class` | `string` | `''` | Menambahkan class pada host `LayoutNavigation`. |
|
|
283
285
|
|
|
284
286
|
Behavior:
|
|
285
287
|
|
|
@@ -293,7 +295,7 @@ Behavior:
|
|
|
293
295
|
Catatan penggunaan:
|
|
294
296
|
|
|
295
297
|
- Untuk mode `border-rail`, jangan tambahkan `border-r` manual pada konten nav bila yang diinginkan adalah rail bawaan primitive.
|
|
296
|
-
- Styling visual isi nav sebaiknya ditempatkan pada elemen child di dalam `
|
|
298
|
+
- Styling visual isi nav sebaiknya ditempatkan pada elemen child di dalam `LayoutNavigation`, bukan dengan mengandalkan border host.
|
|
297
299
|
|
|
298
300
|
### `LayoutContentComponent`
|
|
299
301
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ojiepermana/angular-theme",
|
|
3
|
-
"version": "22.0.
|
|
3
|
+
"version": "22.0.41",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/edsis/angular.git"
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
"@angular/common": ">=22.0.0",
|
|
14
14
|
"@angular/core": ">=22.0.0",
|
|
15
15
|
"@angular/router": ">=22.0.0",
|
|
16
|
-
"@ojiepermana/angular-navigation": "^22.0.
|
|
17
|
-
"@ojiepermana/angular-component": "^22.0.
|
|
16
|
+
"@ojiepermana/angular-navigation": "^22.0.41",
|
|
17
|
+
"@ojiepermana/angular-component": "^22.0.41",
|
|
18
18
|
"rxjs": ">=7.8.0"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|