@ojiepermana/angular-theme 22.0.35 → 22.0.36
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 +38 -19
- package/fesm2022/ojiepermana-angular-theme-layout-wrapper.mjs +10 -6
- package/fesm2022/ojiepermana-angular-theme-styles.mjs +356 -53
- package/package.json +3 -3
- package/styles/README.md +10 -3
- package/styles/css/{seasonal/base → base}/tailwind.css +9 -5
- package/styles/css/{seasonal/base → base}/theme.css +1 -6
- package/styles/css/{seasonal/base → base}/tokens.css +26 -16
- package/styles/css/color/amber.css +50 -0
- package/styles/css/color/blue.css +50 -0
- package/styles/css/color/brand.css +16 -0
- package/styles/css/color/cyan.css +50 -0
- package/styles/css/color/emerald.css +50 -0
- package/styles/css/color/fuchsia.css +50 -0
- package/styles/css/color/green.css +50 -0
- package/styles/css/color/index.css +19 -0
- package/styles/css/color/indigo.css +50 -0
- package/styles/css/color/lime.css +50 -0
- package/styles/css/color/orange.css +50 -0
- package/styles/css/color/pink.css +50 -0
- package/styles/css/color/purple.css +50 -0
- package/styles/css/color/red.css +50 -0
- package/styles/css/color/rose.css +50 -0
- package/styles/css/color/sky.css +50 -0
- package/styles/css/color/teal.css +50 -0
- package/styles/css/color/violet.css +50 -0
- package/styles/css/color/yellow.css +50 -0
- package/styles/css/index.css +15 -6
- package/styles/css/neutral/gray.css +34 -0
- package/styles/css/neutral/index.css +11 -0
- package/styles/css/neutral/mauve.css +34 -0
- package/styles/css/neutral/mist.css +34 -0
- package/styles/css/neutral/neutral.css +34 -0
- package/styles/css/neutral/olive.css +34 -0
- package/styles/css/neutral/slate.css +34 -0
- package/styles/css/neutral/stone.css +34 -0
- package/styles/css/neutral/taupe.css +34 -0
- package/styles/css/neutral/zinc.css +34 -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 +6 -6
- package/types/ojiepermana-angular-theme-styles.d.ts +168 -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
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @ojiepermana/angular-theme
|
|
2
2
|
|
|
3
3
|
Theme layer for the `@ojiepermana/angular` design system: a runtime provider
|
|
4
|
-
(mode /
|
|
5
|
-
styled against.
|
|
4
|
+
(mode / color / neutral / brand), design tokens, and the Tailwind v4 CSS that the
|
|
5
|
+
components are styled against.
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
bun add @ojiepermana/angular-theme
|
|
@@ -11,13 +11,13 @@ bun add @ojiepermana/angular-theme
|
|
|
11
11
|
|
|
12
12
|
## Entry points
|
|
13
13
|
|
|
14
|
-
| Import path
|
|
15
|
-
|
|
|
16
|
-
| `@ojiepermana/angular-theme/styles`
|
|
17
|
-
| `@ojiepermana/angular-theme/layout`
|
|
18
|
-
| `@ojiepermana/angular-theme/page`
|
|
19
|
-
| `@ojiepermana/angular-theme/theme.css`
|
|
20
|
-
| `@ojiepermana/angular-theme/styles/css
|
|
14
|
+
| Import path | Contents |
|
|
15
|
+
| ----------------------------------------- | ------------------------------------------------------- |
|
|
16
|
+
| `@ojiepermana/angular-theme/styles` | `provideUiTheme`, `ThemeModeService`, … |
|
|
17
|
+
| `@ojiepermana/angular-theme/layout` | Layout shell building blocks |
|
|
18
|
+
| `@ojiepermana/angular-theme/page` | Page-level scaffolding |
|
|
19
|
+
| `@ojiepermana/angular-theme/theme.css` | Base tokens + component styles (CSS) |
|
|
20
|
+
| `@ojiepermana/angular-theme/styles/css/*` | Raw CSS assets (color + neutral palettes, Tailwind map) |
|
|
21
21
|
|
|
22
22
|
## Tailwind v4 setup
|
|
23
23
|
|
|
@@ -26,9 +26,9 @@ stylesheet:
|
|
|
26
26
|
|
|
27
27
|
```css
|
|
28
28
|
/* styles.css */
|
|
29
|
-
@import '@ojiepermana/angular-theme/
|
|
29
|
+
@import '@ojiepermana/angular-theme/styles/css/index.css'; /* base + all color + neutral palettes */
|
|
30
30
|
@import 'tailwindcss';
|
|
31
|
-
@import '@ojiepermana/angular-theme/styles/css/
|
|
31
|
+
@import '@ojiepermana/angular-theme/styles/css/base/tailwind.css'; /* maps tokens → bg-primary, bg-brand, text-foreground, … */
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
Requires Tailwind CSS `^4.3.0`.
|
|
@@ -39,20 +39,39 @@ Requires Tailwind CSS `^4.3.0`.
|
|
|
39
39
|
import { provideUiTheme } from '@ojiepermana/angular-theme/styles';
|
|
40
40
|
|
|
41
41
|
export const appConfig = {
|
|
42
|
-
providers: [
|
|
42
|
+
providers: [
|
|
43
|
+
provideUiTheme({
|
|
44
|
+
mode: 'light',
|
|
45
|
+
color: 'base', // accent palette (base, red…rose, brand)
|
|
46
|
+
neutral: 'base', // gray family (base, slate, gray, zinc, …)
|
|
47
|
+
brand: { color: '221 83% 53%', foreground: '0 0% 100%' }, // consumer brand
|
|
48
|
+
}),
|
|
49
|
+
],
|
|
43
50
|
};
|
|
44
51
|
```
|
|
45
52
|
|
|
46
53
|
- `mode` — bootstraps `ThemeModeService` and persists the default mode.
|
|
47
|
-
- `
|
|
48
|
-
- `
|
|
49
|
-
|
|
54
|
+
- `color` — bootstraps `ThemeColorService`; initial accent palette (`<html theme-color>`).
|
|
55
|
+
- `neutral` — initial neutral family (`<html theme-neutral>`); composes with any accent.
|
|
56
|
+
- `brand` — bootstraps `ThemeBrandService`; sets `--brand` / `bg-brand` and the
|
|
57
|
+
`theme-color='brand'` accent preset. Settable at runtime via `setBrand()`.
|
|
50
58
|
|
|
51
|
-
|
|
59
|
+
A persisted choice (localStorage `theme-color` / `theme-neutral` / `theme-brand`)
|
|
60
|
+
always wins over the configured default.
|
|
52
61
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
62
|
+
### Color system (FluxUI-style)
|
|
63
|
+
|
|
64
|
+
Two independent axes switch at runtime via attribute selectors on `<html>`:
|
|
65
|
+
|
|
66
|
+
- **accent** (`theme-color`) — `base` (core), `red … rose`, and `brand`. Each
|
|
67
|
+
re-tints the full palette. `base` = no override.
|
|
68
|
+
- **neutral** (`theme-neutral`) — `base` (core), `slate`, `gray`, `zinc`, `neutral`,
|
|
69
|
+
`stone`, `mauve`, `olive`, `mist`, `taupe`. Overrides only the gray family and is
|
|
70
|
+
layered after accent so it wins the shared neutral tokens — letting you pair any
|
|
71
|
+
accent with any neutral.
|
|
72
|
+
|
|
73
|
+
`styles/css/index.css` bundles the core base theme plus every accent and neutral
|
|
74
|
+
palette, so switching needs no runtime CSS loading.
|
|
56
75
|
|
|
57
76
|
### Material Symbols icons (opt-in)
|
|
58
77
|
|
|
@@ -129,11 +129,13 @@ class LayoutNavSidebar {
|
|
|
129
129
|
...(ngDevMode ? [{ debugName: "brand" }] : /* istanbul ignore next */ []));
|
|
130
130
|
user = input.required(/* @ts-ignore */
|
|
131
131
|
...(ngDevMode ? [{ debugName: "user" }] : /* istanbul ignore next */ []));
|
|
132
|
-
/** Appearance dari shell (`[appearance]`);
|
|
132
|
+
/** Appearance dari shell (`[appearance]`); menentukan ketebalan border kanan pemisah konten. */
|
|
133
133
|
appearance = input('flat', /* @ts-ignore */
|
|
134
134
|
...(ngDevMode ? [{ debugName: "appearance" }] : /* istanbul ignore next */ []));
|
|
135
|
-
/**
|
|
136
|
-
shellClass = computed(() => this.appearance() === '
|
|
135
|
+
/** Border kanan pemisah; ketebalan ikut appearance: `border-rail` 1.5px, `flat` 1px. */
|
|
136
|
+
shellClass = computed(() => this.appearance() === 'border-rail'
|
|
137
|
+
? 'border-r-[1.5px] border-border'
|
|
138
|
+
: 'border-r border-border', /* @ts-ignore */
|
|
137
139
|
...(ngDevMode ? [{ debugName: "shellClass" }] : /* istanbul ignore next */ []));
|
|
138
140
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: LayoutNavSidebar, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
139
141
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.3", type: LayoutNavSidebar, isStandalone: true, selector: "LayoutNavSidebar", inputs: { brand: { classPropertyName: "brand", publicName: "brand", isSignal: true, isRequired: true, transformFunction: null }, user: { classPropertyName: "user", publicName: "user", isSignal: true, isRequired: true, transformFunction: null }, appearance: { classPropertyName: "appearance", publicName: "appearance", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "contents" }, ngImport: i0, template: `
|
|
@@ -181,11 +183,13 @@ class LayoutNavDockbar {
|
|
|
181
183
|
...(ngDevMode ? [{ debugName: "brand" }] : /* istanbul ignore next */ []));
|
|
182
184
|
user = input.required(/* @ts-ignore */
|
|
183
185
|
...(ngDevMode ? [{ debugName: "user" }] : /* istanbul ignore next */ []));
|
|
184
|
-
/** Appearance dari shell (`[appearance]`);
|
|
186
|
+
/** Appearance dari shell (`[appearance]`); menentukan ketebalan border kanan pemisah konten. */
|
|
185
187
|
appearance = input('flat', /* @ts-ignore */
|
|
186
188
|
...(ngDevMode ? [{ debugName: "appearance" }] : /* istanbul ignore next */ []));
|
|
187
|
-
/**
|
|
188
|
-
shellClass = computed(() => this.appearance() === '
|
|
189
|
+
/** Border kanan pemisah; ketebalan ikut appearance: `border-rail` 1.5px, `flat` 1px. */
|
|
190
|
+
shellClass = computed(() => this.appearance() === 'border-rail'
|
|
191
|
+
? 'border-r-[1.5px] border-border'
|
|
192
|
+
: 'border-r border-border', /* @ts-ignore */
|
|
189
193
|
...(ngDevMode ? [{ debugName: "shellClass" }] : /* istanbul ignore next */ []));
|
|
190
194
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: LayoutNavDockbar, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
191
195
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.3", type: LayoutNavDockbar, isStandalone: true, selector: "LayoutNavDockbar", inputs: { brand: { classPropertyName: "brand", publicName: "brand", isSignal: true, isRequired: true, transformFunction: null }, user: { classPropertyName: "user", publicName: "user", isSignal: true, isRequired: true, transformFunction: null }, appearance: { classPropertyName: "appearance", publicName: "appearance", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "contents" }, ngImport: i0, template: `
|
|
@@ -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;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
readStoredBrand() {
|
|
281
|
+
try {
|
|
282
|
+
const raw = this.storage()?.getItem(THEME_BRAND_STORAGE_KEY);
|
|
283
|
+
if (!raw) {
|
|
284
|
+
return null;
|
|
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;
|
|
179
294
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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;
|
|
183
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,129 @@ 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
|
+
constructor() {
|
|
417
|
+
this.ensureDefaults();
|
|
418
|
+
effect(() => {
|
|
419
|
+
const radius = this.radiusState();
|
|
420
|
+
this.persist(radius);
|
|
421
|
+
this.documentRef?.documentElement?.setAttribute('theme-radius', radius);
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
setRadius(radius) {
|
|
425
|
+
this.radiusState.set(radius);
|
|
426
|
+
}
|
|
427
|
+
ensureDefaults() {
|
|
428
|
+
const fallback = normalizeThemeRadius(this.options?.radius);
|
|
429
|
+
const stored = this.readStorage() ?? fallback;
|
|
430
|
+
this.radiusState.set(normalizeThemeRadius(stored));
|
|
431
|
+
}
|
|
432
|
+
storage() {
|
|
433
|
+
try {
|
|
434
|
+
return this.documentRef?.defaultView?.localStorage ?? null;
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
readStorage() {
|
|
441
|
+
try {
|
|
442
|
+
return this.storage()?.getItem(THEME_RADIUS_STORAGE_KEY) ?? null;
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
persist(radius) {
|
|
449
|
+
try {
|
|
450
|
+
this.storage()?.setItem(THEME_RADIUS_STORAGE_KEY, radius);
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: ThemeRadiusService, deps: [], target: i0.ɵɵFactoryTarget.Service });
|
|
457
|
+
static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.0.3", ngImport: i0, type: ThemeRadiusService });
|
|
458
|
+
}
|
|
459
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: ThemeRadiusService, decorators: [{
|
|
460
|
+
type: Service
|
|
461
|
+
}], ctorParameters: () => [] });
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Spacing density axis. Writes `<html theme-space>`; each preset CSS sets the
|
|
465
|
+
* single `--spacing-base` knob, which Tailwind's `--spacing` derives from, so
|
|
466
|
+
* every `p-*`/`m-*`/`gap-*`/`w-*`/`h-*` utility rescales. Persists to
|
|
467
|
+
* localStorage and seeds from `provideUiTheme({ space })` (a persisted choice
|
|
468
|
+
* wins over the default).
|
|
469
|
+
*/
|
|
470
|
+
class ThemeSpaceService {
|
|
471
|
+
documentRef = inject(DOCUMENT, { optional: true });
|
|
472
|
+
options = inject(THEME_OPTIONS, { optional: true });
|
|
473
|
+
spaceState = signal(DEFAULT_THEME_SPACE, /* @ts-ignore */
|
|
474
|
+
...(ngDevMode ? [{ debugName: "spaceState" }] : /* istanbul ignore next */ []));
|
|
475
|
+
space = this.spaceState.asReadonly();
|
|
476
|
+
constructor() {
|
|
477
|
+
this.ensureDefaults();
|
|
478
|
+
effect(() => {
|
|
479
|
+
const space = this.spaceState();
|
|
480
|
+
this.persist(space);
|
|
481
|
+
this.documentRef?.documentElement?.setAttribute('theme-space', space);
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
setSpace(space) {
|
|
485
|
+
this.spaceState.set(space);
|
|
486
|
+
}
|
|
487
|
+
ensureDefaults() {
|
|
488
|
+
const fallback = normalizeThemeSpace(this.options?.space);
|
|
489
|
+
const stored = this.readStorage() ?? fallback;
|
|
490
|
+
this.spaceState.set(normalizeThemeSpace(stored));
|
|
491
|
+
}
|
|
492
|
+
storage() {
|
|
493
|
+
try {
|
|
494
|
+
return this.documentRef?.defaultView?.localStorage ?? null;
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
readStorage() {
|
|
501
|
+
try {
|
|
502
|
+
return this.storage()?.getItem(THEME_SPACE_STORAGE_KEY) ?? null;
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
persist(space) {
|
|
509
|
+
try {
|
|
510
|
+
this.storage()?.setItem(THEME_SPACE_STORAGE_KEY, space);
|
|
511
|
+
}
|
|
512
|
+
catch {
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: ThemeSpaceService, deps: [], target: i0.ɵɵFactoryTarget.Service });
|
|
517
|
+
static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.0.3", ngImport: i0, type: ThemeSpaceService });
|
|
218
518
|
}
|
|
219
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type:
|
|
519
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: ThemeSpaceService, decorators: [{
|
|
220
520
|
type: Service
|
|
221
521
|
}], ctorParameters: () => [] });
|
|
222
522
|
|
|
@@ -235,7 +535,10 @@ function provideUiTheme(options = {}) {
|
|
|
235
535
|
inject(MaterialSymbolsService).ensureLoaded();
|
|
236
536
|
}
|
|
237
537
|
inject(ThemeModeService);
|
|
238
|
-
inject(
|
|
538
|
+
inject(ThemeColorService);
|
|
539
|
+
inject(ThemeBrandService);
|
|
540
|
+
inject(ThemeRadiusService);
|
|
541
|
+
inject(ThemeSpaceService);
|
|
239
542
|
}),
|
|
240
543
|
]);
|
|
241
544
|
}
|
|
@@ -244,4 +547,4 @@ function provideUiTheme(options = {}) {
|
|
|
244
547
|
* Generated bundle index. Do not edit.
|
|
245
548
|
*/
|
|
246
549
|
|
|
247
|
-
export { DEFAULT_THEME_MODE,
|
|
550
|
+
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ojiepermana/angular-theme",
|
|
3
|
-
"version": "22.0.
|
|
3
|
+
"version": "22.0.36",
|
|
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.36",
|
|
17
|
+
"@ojiepermana/angular-component": "^22.0.36",
|
|
18
18
|
"rxjs": ">=7.8.0"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
package/styles/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Theme
|
|
2
2
|
|
|
3
|
-
`@ojiepermana/angular-theme/styles` menyediakan provider untuk mode,
|
|
3
|
+
`@ojiepermana/angular-theme/styles` menyediakan provider untuk mode, warna (accent +
|
|
4
|
+
neutral, gaya FluxUI), brand color, dan preload aset tema global.
|
|
4
5
|
|
|
5
6
|
```ts
|
|
6
7
|
import { provideUiTheme } from '@ojiepermana/angular-theme/styles';
|
|
@@ -9,7 +10,9 @@ export const appConfig = {
|
|
|
9
10
|
providers: [
|
|
10
11
|
provideUiTheme({
|
|
11
12
|
mode: 'light',
|
|
12
|
-
|
|
13
|
+
color: 'base', // accent: base, red…rose, brand
|
|
14
|
+
neutral: 'base', // gray family: base, slate, gray, zinc, …
|
|
15
|
+
brand: '221 83% 53%', // warna brand consumer (HSL triplet)
|
|
13
16
|
}),
|
|
14
17
|
],
|
|
15
18
|
};
|
|
@@ -18,8 +21,12 @@ export const appConfig = {
|
|
|
18
21
|
## API Notes
|
|
19
22
|
|
|
20
23
|
- `mode` mem-bootstrap `ThemeModeService` dan default mode yang disimpan di storage.
|
|
21
|
-
- `
|
|
24
|
+
- `color` mem-bootstrap `ThemeColorService`; accent palette awal (`<html theme-color>`).
|
|
25
|
+
- `neutral` keluarga neutral awal (`<html theme-neutral>`); berkomposisi dengan accent.
|
|
26
|
+
- `brand` mem-bootstrap `ThemeBrandService`; set `--brand` / utility `bg-brand` dan preset
|
|
27
|
+
accent `theme-color='brand'`. Bisa diubah runtime via `setBrand()`, persist di storage.
|
|
22
28
|
- `icons.materialSymbols` mengontrol preload stylesheet Material Symbols pada bootstrap aplikasi.
|
|
29
|
+
- Pilihan yang dipersist (`theme-color` / `theme-neutral` / `theme-brand`) menang atas default.
|
|
23
30
|
|
|
24
31
|
## Default Behavior
|
|
25
32
|
|