@frame-ui-ng/foundation 0.2.0-beta.0 → 0.4.0-beta.0

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 CHANGED
@@ -1,15 +1,15 @@
1
1
  # Foundation
2
2
 
3
- `@frame-ui-ng/foundation` is the stable base layer for FrameUI.
4
-
5
- Documentation: https://frame-ui.com
3
+ `@frame-ui-ng/foundation` is the stable base layer for FrameUI.
6
4
 
7
- Current scope:
8
-
9
- - CSS-variable token contract
10
- - Angular theme switching via `data-theme` or a shared `.dark` class
11
- - class merge helpers for future slot-based primitives
12
- - Vitest unit tests
5
+ Documentation: https://frame-ui.com
6
+
7
+ Current scope:
8
+
9
+ - CSS-variable token contract
10
+ - Angular theme switching via `data-theme` or a shared `.dark` class
11
+ - class merge helpers for future slot-based primitives
12
+ - Vitest unit tests
13
13
 
14
14
  No primitives or complex components are included here.
15
15
 
@@ -79,70 +79,80 @@ Bad token naming:
79
79
  --frame-card-border-hover
80
80
  ```
81
81
 
82
- ## Light And Dark
83
-
84
- The foundation layer only models `light` and `dark`.
85
-
86
- Brand, product, or campaign differences should be handled by the host app's tokens or by scoped CSS-variable overrides. They should not become additional registered theme names.
87
-
88
- ## Theme Ownership
89
-
90
- The foundation layer should expose a token contract, not force itself to be the only dark mode owner.
91
-
92
- There are two recommended ownership models:
93
-
94
- - library-managed: the FrameUI writes the active theme to the root element
95
- - externally managed: another system such as Tailwind owns the root selector and the FrameUI follows it
96
-
97
- ### Library-managed with `data-theme`
98
-
99
- This is the current default:
100
-
101
- ```ts
102
- provideFrameUI({
103
- defaultTheme: 'light',
104
- });
105
- ```
106
-
107
- ### Library-managed with Tailwind's `.dark` class
108
-
109
- If you want the FrameUI service to be the single source of truth, but Tailwind utilities should respond too, switch to class strategy:
110
-
111
- ```ts
112
- provideFrameUI({
113
- strategy: 'class',
114
- className: 'dark',
115
- });
116
- ```
117
-
118
- `ThemeService.setTheme('dark')` now adds `.dark` to the root element, so both the FrameUI tokens and Tailwind `dark:` utilities react to the same switch.
119
-
120
- ### Externally managed by Tailwind or another app shell
121
-
122
- If the host app already owns dark mode, let the FrameUI observe instead of write:
123
-
124
- ```ts
125
- provideFrameUI({
126
- strategy: 'class',
127
- mode: 'observe',
128
- className: 'dark',
129
- });
130
- ```
131
-
132
- In this mode the library does not write to the DOM. It reads the current root class and keeps `ThemeService.theme()` in sync with that external source of truth.
133
-
134
- Use scoped overrides for local brand moments:
135
-
136
- ```css
137
- .marketing-hero {
138
- --frame-primary: oklch(0.69 0.19 38);
139
- --frame-primary-foreground: oklch(0.99 0.01 95);
140
- --frame-radius-lg: 1rem;
141
- }
142
- ```
143
-
144
- ## Commands
145
-
146
- ```bash
147
- npm install @frame-ui-ng/foundation
148
- ```
82
+ ## Light And Dark
83
+
84
+ The foundation layer only models `light` and `dark`.
85
+
86
+ Brand, product, or campaign differences should be handled by the host app's tokens or by scoped CSS-variable overrides. They should not become additional registered theme names.
87
+
88
+ ## Theme Ownership
89
+
90
+ The foundation layer should expose a token contract, not force itself to be the only dark mode owner.
91
+
92
+ There are two recommended ownership models:
93
+
94
+ - library-managed: the FrameUI writes the active theme to the root element
95
+ - externally managed: another system such as Tailwind owns the root selector and the FrameUI follows it
96
+
97
+ ### Library-managed with `data-theme`
98
+
99
+ This is the current default:
100
+
101
+ ```ts
102
+ provideFrameUI({
103
+ defaultTheme: 'light',
104
+ });
105
+ ```
106
+
107
+ ### Library-managed with Tailwind's `.dark` class
108
+
109
+ If you want the FrameUI service to be the single source of truth, but Tailwind utilities should respond too, switch to class strategy:
110
+
111
+ ```ts
112
+ provideFrameUI({
113
+ strategy: 'class',
114
+ className: 'dark',
115
+ });
116
+ ```
117
+
118
+ `ThemeService.setTheme('dark')` now adds `.dark` to the root element, so both the FrameUI tokens and Tailwind `dark:` utilities react to the same switch.
119
+
120
+ ### Externally managed by Tailwind or another app shell
121
+
122
+ If the host app already owns dark mode, let the FrameUI observe instead of write:
123
+
124
+ ```ts
125
+ provideFrameUI({
126
+ strategy: 'class',
127
+ mode: 'observe',
128
+ className: 'dark',
129
+ });
130
+ ```
131
+
132
+ In this mode the library does not write to the DOM. It reads the current root class and keeps `ThemeService.theme()` in sync with that external source of truth.
133
+
134
+ ## Global Appearance Options
135
+
136
+ FrameUI components include small blueprint-style corner handles by default. Disable them for the whole app from the foundation provider:
137
+
138
+ ```ts
139
+ provideFrameUI({
140
+ disableCornerHandles: true,
141
+ });
142
+ ```
143
+
144
+ Use scoped overrides for local brand moments:
145
+
146
+ ```css
147
+ .marketing-hero {
148
+ --frame-primary: oklch(0.69 0.19 38);
149
+ --frame-primary-foreground: oklch(0.99 0.01 95);
150
+ --frame-radius-lg: 1rem;
151
+ }
152
+ ```
153
+
154
+ ## Commands
155
+
156
+ ```bash
157
+ npm install @frame-ui-ng/foundation
158
+ ```
@@ -1,6 +1,6 @@
1
1
  import { DOCUMENT } from '@angular/common';
2
2
  import * as i0 from '@angular/core';
3
- import { InjectionToken, makeEnvironmentProviders, inject, signal, computed, Injectable } from '@angular/core';
3
+ import { InjectionToken, makeEnvironmentProviders, ENVIRONMENT_INITIALIZER, inject, signal, computed, Injectable } from '@angular/core';
4
4
 
5
5
  function cx(...values) {
6
6
  const classNames = [];
@@ -39,9 +39,11 @@ const DEFAULT_CONFIG = {
39
39
  attribute: 'data-theme',
40
40
  className: 'dark',
41
41
  defaultTheme: 'light',
42
+ disableCornerHandles: false,
42
43
  mode: 'managed',
43
44
  strategy: 'attribute',
44
45
  };
46
+ const CORNER_HANDLES_ATTRIBUTE = 'data-frame-corner-handles';
45
47
  const FRAME_UI_CONFIG = new InjectionToken('FRAME_UI_CONFIG', {
46
48
  factory: () => DEFAULT_CONFIG,
47
49
  });
@@ -52,6 +54,13 @@ function provideFrameUI(options = {}) {
52
54
  useValue: createFrameUIConfig(options),
53
55
  },
54
56
  ThemeService,
57
+ {
58
+ provide: ENVIRONMENT_INITIALIZER,
59
+ multi: true,
60
+ useValue: () => {
61
+ inject(ThemeService);
62
+ },
63
+ },
55
64
  ]);
56
65
  }
57
66
  function createFrameUIConfig(options = {}) {
@@ -60,6 +69,7 @@ function createFrameUIConfig(options = {}) {
60
69
  attribute: options.attribute ?? DEFAULT_CONFIG.attribute,
61
70
  className: options.className ?? DEFAULT_CONFIG.className,
62
71
  defaultTheme,
72
+ disableCornerHandles: options.disableCornerHandles ?? DEFAULT_CONFIG.disableCornerHandles,
63
73
  mode: options.mode ?? DEFAULT_CONFIG.mode,
64
74
  strategy: options.strategy ?? DEFAULT_CONFIG.strategy,
65
75
  };
@@ -72,6 +82,7 @@ class ThemeService {
72
82
  theme = this.activeTheme.asReadonly();
73
83
  isDark = computed(() => this.activeTheme() === 'dark', ...(ngDevMode ? [{ debugName: "isDark" }] : /* istanbul ignore next */ []));
74
84
  constructor() {
85
+ this.applyCornerHandlesPreference();
75
86
  if (this.config.mode === 'observe') {
76
87
  this.syncFromDom();
77
88
  this.observeThemeChanges();
@@ -109,6 +120,17 @@ class ThemeService {
109
120
  }
110
121
  root.setAttribute(this.config.attribute, name);
111
122
  }
123
+ applyCornerHandlesPreference() {
124
+ const root = this.document?.documentElement;
125
+ if (!root) {
126
+ return;
127
+ }
128
+ if (this.config.disableCornerHandles) {
129
+ root.setAttribute(CORNER_HANDLES_ATTRIBUTE, 'false');
130
+ return;
131
+ }
132
+ root.removeAttribute(CORNER_HANDLES_ATTRIBUTE);
133
+ }
112
134
  observeThemeChanges() {
113
135
  const root = this.document?.documentElement;
114
136
  if (!root || typeof MutationObserver === 'undefined') {
@@ -1 +1 @@
1
- {"version":3,"file":"frame-ui-ng-foundation.mjs","sources":["../../../projects/foundation/src/lib/class-names.ts","../../../projects/foundation/src/lib/frame-ui.ts","../../../projects/foundation/src/frame-ui-ng-foundation.ts"],"sourcesContent":["export type ClassDictionary = Readonly<Record<string, boolean | null | undefined>>;\nexport type ClassValue =\n | ClassDictionary\n | ClassValue[]\n | false\n | null\n | string\n | undefined;\n\nexport type SlotClasses<TSlot extends string> = Readonly<Record<TSlot, string>>;\nexport type SlotClassOverrides<TSlot extends string> = Partial<Record<TSlot, ClassValue>>;\n\nexport function cx(...values: readonly ClassValue[]): string {\n const classNames: string[] = [];\n\n for (const value of values) {\n if (!value) {\n continue;\n }\n\n if (typeof value === 'string') {\n classNames.push(value);\n continue;\n }\n\n if (Array.isArray(value)) {\n const nested = cx(...value);\n\n if (nested) {\n classNames.push(nested);\n }\n\n continue;\n }\n\n for (const [className, enabled] of Object.entries(value)) {\n if (enabled) {\n classNames.push(className);\n }\n }\n }\n\n return classNames.join(' ');\n}\n\nexport function withClassOverrides<TSlot extends string>(\n slots: SlotClasses<TSlot>,\n overrides?: SlotClassOverrides<TSlot>,\n): Record<TSlot, string> {\n const mergedSlots = {} as Record<TSlot, string>;\n\n for (const slot of Object.keys(slots) as TSlot[]) {\n mergedSlots[slot] = cx(slots[slot], overrides?.[slot]);\n }\n\n return mergedSlots;\n}\n","import { DOCUMENT } from '@angular/common';\nimport {\n EnvironmentProviders,\n Injectable,\n InjectionToken,\n OnDestroy,\n Signal,\n computed,\n inject,\n makeEnvironmentProviders,\n signal,\n} from '@angular/core';\n\nexport type ThemeBindingStrategy = 'attribute' | 'class';\nexport type ThemeSyncMode = 'managed' | 'observe';\nexport type FrameUITheme = 'light' | 'dark';\n\nexport interface FrameUIConfig {\n attribute: string;\n className: string;\n defaultTheme: FrameUITheme;\n mode: ThemeSyncMode;\n strategy: ThemeBindingStrategy;\n}\n\nconst DEFAULT_CONFIG: FrameUIConfig = {\n attribute: 'data-theme',\n className: 'dark',\n defaultTheme: 'light',\n mode: 'managed',\n strategy: 'attribute',\n};\n\nexport const FRAME_UI_CONFIG = new InjectionToken<FrameUIConfig>(\n 'FRAME_UI_CONFIG',\n {\n factory: () => DEFAULT_CONFIG,\n },\n);\n\nexport interface FrameUIOptions {\n attribute?: string;\n className?: string;\n defaultTheme?: FrameUITheme;\n mode?: ThemeSyncMode;\n strategy?: ThemeBindingStrategy;\n}\n\nexport function provideFrameUI(\n options: FrameUIOptions = {},\n): EnvironmentProviders {\n return makeEnvironmentProviders([\n {\n provide: FRAME_UI_CONFIG,\n useValue: createFrameUIConfig(options),\n },\n ThemeService,\n ]);\n}\n\nexport function createFrameUIConfig(\n options: FrameUIOptions = {},\n): FrameUIConfig {\n const defaultTheme = options.defaultTheme ?? DEFAULT_CONFIG.defaultTheme;\n\n return {\n attribute: options.attribute ?? DEFAULT_CONFIG.attribute,\n className: options.className ?? DEFAULT_CONFIG.className,\n defaultTheme,\n mode: options.mode ?? DEFAULT_CONFIG.mode,\n strategy: options.strategy ?? DEFAULT_CONFIG.strategy,\n };\n}\n\n@Injectable()\nexport class ThemeService implements OnDestroy {\n private readonly document = inject(DOCUMENT);\n private readonly config = inject(FRAME_UI_CONFIG);\n private readonly activeTheme = signal(this.config.defaultTheme);\n private observer: MutationObserver | null = null;\n\n readonly theme: Signal<FrameUITheme> = this.activeTheme.asReadonly();\n\n readonly isDark = computed(() => this.activeTheme() === 'dark');\n\n constructor() {\n if (this.config.mode === 'observe') {\n this.syncFromDom();\n this.observeThemeChanges();\n return;\n }\n\n this.applyTheme(this.activeTheme());\n }\n\n setTheme(name: FrameUITheme): void {\n if (!isFrameUITheme(name)) {\n throw new Error(`Unknown theme \"${name}\".`);\n }\n\n if (this.config.mode === 'observe') {\n throw new Error(\n 'ThemeService is configured to observe external theme state and cannot set the theme.',\n );\n }\n\n this.activeTheme.set(name);\n this.applyTheme(name);\n }\n\n toggleTheme(): FrameUITheme {\n const nextTheme = this.activeTheme() === 'dark' ? 'light' : 'dark';\n\n this.setTheme(nextTheme);\n\n return nextTheme;\n }\n\n ngOnDestroy(): void {\n this.observer?.disconnect();\n this.observer = null;\n }\n\n private applyTheme(name: FrameUITheme): void {\n const root = this.document?.documentElement;\n\n if (!root) {\n return;\n }\n\n if (this.config.strategy === 'class') {\n root.classList.toggle(this.config.className, name === 'dark');\n return;\n }\n\n root.setAttribute(this.config.attribute, name);\n }\n\n private observeThemeChanges(): void {\n const root = this.document?.documentElement;\n\n if (!root || typeof MutationObserver === 'undefined') {\n return;\n }\n\n const attributeFilter =\n this.config.strategy === 'class' ? ['class'] : [this.config.attribute];\n\n this.observer = new MutationObserver(() => {\n this.syncFromDom();\n });\n this.observer.observe(root, {\n attributeFilter,\n attributes: true,\n });\n }\n\n private syncFromDom(): void {\n const root = this.document?.documentElement;\n\n if (!root) {\n return;\n }\n\n this.activeTheme.set(this.readThemeFromDom(root));\n }\n\n private readThemeFromDom(root: HTMLElement): FrameUITheme {\n if (this.config.strategy === 'class') {\n return root.classList.contains(this.config.className) ? 'dark' : 'light';\n }\n\n const theme = root.getAttribute(this.config.attribute);\n\n return isFrameUITheme(theme) ? theme : this.config.defaultTheme;\n }\n}\n\nfunction isFrameUITheme(value: unknown): value is FrameUITheme {\n return value === 'light' || value === 'dark';\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"frame-ui-ng-foundation.mjs","sources":["../../../projects/foundation/src/lib/class-names.ts","../../../projects/foundation/src/lib/frame-ui.ts","../../../projects/foundation/src/frame-ui-ng-foundation.ts"],"sourcesContent":["export type ClassDictionary = Readonly<Record<string, boolean | null | undefined>>;\nexport type ClassValue =\n | ClassDictionary\n | ClassValue[]\n | false\n | null\n | string\n | undefined;\n\nexport type SlotClasses<TSlot extends string> = Readonly<Record<TSlot, string>>;\nexport type SlotClassOverrides<TSlot extends string> = Partial<Record<TSlot, ClassValue>>;\n\nexport function cx(...values: readonly ClassValue[]): string {\n const classNames: string[] = [];\n\n for (const value of values) {\n if (!value) {\n continue;\n }\n\n if (typeof value === 'string') {\n classNames.push(value);\n continue;\n }\n\n if (Array.isArray(value)) {\n const nested = cx(...value);\n\n if (nested) {\n classNames.push(nested);\n }\n\n continue;\n }\n\n for (const [className, enabled] of Object.entries(value)) {\n if (enabled) {\n classNames.push(className);\n }\n }\n }\n\n return classNames.join(' ');\n}\n\nexport function withClassOverrides<TSlot extends string>(\n slots: SlotClasses<TSlot>,\n overrides?: SlotClassOverrides<TSlot>,\n): Record<TSlot, string> {\n const mergedSlots = {} as Record<TSlot, string>;\n\n for (const slot of Object.keys(slots) as TSlot[]) {\n mergedSlots[slot] = cx(slots[slot], overrides?.[slot]);\n }\n\n return mergedSlots;\n}\n","import { DOCUMENT } from '@angular/common';\r\nimport {\r\n EnvironmentProviders,\r\n ENVIRONMENT_INITIALIZER,\r\n Injectable,\r\n InjectionToken,\r\n OnDestroy,\r\n Signal,\r\n computed,\r\n inject,\r\n makeEnvironmentProviders,\r\n signal,\r\n} from '@angular/core';\r\n\r\nexport type ThemeBindingStrategy = 'attribute' | 'class';\r\nexport type ThemeSyncMode = 'managed' | 'observe';\r\nexport type FrameUITheme = 'light' | 'dark';\r\n\r\nexport interface FrameUIConfig {\r\n attribute: string;\r\n className: string;\r\n defaultTheme: FrameUITheme;\r\n disableCornerHandles: boolean;\r\n mode: ThemeSyncMode;\r\n strategy: ThemeBindingStrategy;\r\n}\r\n\r\nconst DEFAULT_CONFIG: FrameUIConfig = {\r\n attribute: 'data-theme',\r\n className: 'dark',\r\n defaultTheme: 'light',\r\n disableCornerHandles: false,\r\n mode: 'managed',\r\n strategy: 'attribute',\r\n};\r\n\r\nconst CORNER_HANDLES_ATTRIBUTE = 'data-frame-corner-handles';\r\n\r\nexport const FRAME_UI_CONFIG = new InjectionToken<FrameUIConfig>(\r\n 'FRAME_UI_CONFIG',\r\n {\r\n factory: () => DEFAULT_CONFIG,\r\n },\r\n);\r\n\r\nexport interface FrameUIOptions {\r\n attribute?: string;\r\n className?: string;\r\n defaultTheme?: FrameUITheme;\r\n disableCornerHandles?: boolean;\r\n mode?: ThemeSyncMode;\r\n strategy?: ThemeBindingStrategy;\r\n}\r\n\r\nexport function provideFrameUI(\r\n options: FrameUIOptions = {},\r\n): EnvironmentProviders {\r\n return makeEnvironmentProviders([\r\n {\r\n provide: FRAME_UI_CONFIG,\r\n useValue: createFrameUIConfig(options),\r\n },\r\n ThemeService,\r\n {\r\n provide: ENVIRONMENT_INITIALIZER,\r\n multi: true,\r\n useValue: () => {\r\n inject(ThemeService);\r\n },\r\n },\r\n ]);\r\n}\r\n\r\nexport function createFrameUIConfig(\r\n options: FrameUIOptions = {},\r\n): FrameUIConfig {\r\n const defaultTheme = options.defaultTheme ?? DEFAULT_CONFIG.defaultTheme;\r\n\r\n return {\r\n attribute: options.attribute ?? DEFAULT_CONFIG.attribute,\r\n className: options.className ?? DEFAULT_CONFIG.className,\r\n defaultTheme,\r\n disableCornerHandles:\r\n options.disableCornerHandles ?? DEFAULT_CONFIG.disableCornerHandles,\r\n mode: options.mode ?? DEFAULT_CONFIG.mode,\r\n strategy: options.strategy ?? DEFAULT_CONFIG.strategy,\r\n };\r\n}\r\n\r\n@Injectable()\r\nexport class ThemeService implements OnDestroy {\r\n private readonly document = inject(DOCUMENT);\r\n private readonly config = inject(FRAME_UI_CONFIG);\r\n private readonly activeTheme = signal(this.config.defaultTheme);\r\n private observer: MutationObserver | null = null;\r\n\r\n readonly theme: Signal<FrameUITheme> = this.activeTheme.asReadonly();\r\n\r\n readonly isDark = computed(() => this.activeTheme() === 'dark');\r\n\r\n constructor() {\r\n this.applyCornerHandlesPreference();\r\n\r\n if (this.config.mode === 'observe') {\r\n this.syncFromDom();\r\n this.observeThemeChanges();\r\n return;\r\n }\r\n\r\n this.applyTheme(this.activeTheme());\r\n }\r\n\r\n setTheme(name: FrameUITheme): void {\r\n if (!isFrameUITheme(name)) {\r\n throw new Error(`Unknown theme \"${name}\".`);\r\n }\r\n\r\n if (this.config.mode === 'observe') {\r\n throw new Error(\r\n 'ThemeService is configured to observe external theme state and cannot set the theme.',\r\n );\r\n }\r\n\r\n this.activeTheme.set(name);\r\n this.applyTheme(name);\r\n }\r\n\r\n toggleTheme(): FrameUITheme {\r\n const nextTheme = this.activeTheme() === 'dark' ? 'light' : 'dark';\r\n\r\n this.setTheme(nextTheme);\r\n\r\n return nextTheme;\r\n }\r\n\r\n ngOnDestroy(): void {\r\n this.observer?.disconnect();\r\n this.observer = null;\r\n }\r\n\r\n private applyTheme(name: FrameUITheme): void {\r\n const root = this.document?.documentElement;\r\n\r\n if (!root) {\r\n return;\r\n }\r\n\r\n if (this.config.strategy === 'class') {\r\n root.classList.toggle(this.config.className, name === 'dark');\r\n return;\r\n }\r\n\r\n root.setAttribute(this.config.attribute, name);\r\n }\r\n\r\n private applyCornerHandlesPreference(): void {\r\n const root = this.document?.documentElement;\r\n\r\n if (!root) {\r\n return;\r\n }\r\n\r\n if (this.config.disableCornerHandles) {\r\n root.setAttribute(CORNER_HANDLES_ATTRIBUTE, 'false');\r\n return;\r\n }\r\n\r\n root.removeAttribute(CORNER_HANDLES_ATTRIBUTE);\r\n }\r\n\r\n private observeThemeChanges(): void {\r\n const root = this.document?.documentElement;\r\n\r\n if (!root || typeof MutationObserver === 'undefined') {\r\n return;\r\n }\r\n\r\n const attributeFilter =\r\n this.config.strategy === 'class' ? ['class'] : [this.config.attribute];\r\n\r\n this.observer = new MutationObserver(() => {\r\n this.syncFromDom();\r\n });\r\n this.observer.observe(root, {\r\n attributeFilter,\r\n attributes: true,\r\n });\r\n }\r\n\r\n private syncFromDom(): void {\r\n const root = this.document?.documentElement;\r\n\r\n if (!root) {\r\n return;\r\n }\r\n\r\n this.activeTheme.set(this.readThemeFromDom(root));\r\n }\r\n\r\n private readThemeFromDom(root: HTMLElement): FrameUITheme {\r\n if (this.config.strategy === 'class') {\r\n return root.classList.contains(this.config.className) ? 'dark' : 'light';\r\n }\r\n\r\n const theme = root.getAttribute(this.config.attribute);\r\n\r\n return isFrameUITheme(theme) ? theme : this.config.defaultTheme;\r\n }\r\n}\r\n\r\nfunction isFrameUITheme(value: unknown): value is FrameUITheme {\r\n return value === 'light' || value === 'dark';\r\n}\r\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frame-ui-ng/foundation",
3
- "version": "0.2.0-beta.0",
3
+ "version": "0.4.0-beta.0",
4
4
  "description": "Foundation utilities, tokens, and providers for FrameUI.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -15,6 +15,7 @@ interface FrameUIConfig {
15
15
  attribute: string;
16
16
  className: string;
17
17
  defaultTheme: FrameUITheme;
18
+ disableCornerHandles: boolean;
18
19
  mode: ThemeSyncMode;
19
20
  strategy: ThemeBindingStrategy;
20
21
  }
@@ -23,6 +24,7 @@ interface FrameUIOptions {
23
24
  attribute?: string;
24
25
  className?: string;
25
26
  defaultTheme?: FrameUITheme;
27
+ disableCornerHandles?: boolean;
26
28
  mode?: ThemeSyncMode;
27
29
  strategy?: ThemeBindingStrategy;
28
30
  }
@@ -40,6 +42,7 @@ declare class ThemeService implements OnDestroy {
40
42
  toggleTheme(): FrameUITheme;
41
43
  ngOnDestroy(): void;
42
44
  private applyTheme;
45
+ private applyCornerHandlesPreference;
43
46
  private observeThemeChanges;
44
47
  private syncFromDom;
45
48
  private readThemeFromDom;