@frame-ui-ng/foundation 0.4.2-beta.0 → 0.5.1-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
@@ -6,16 +6,16 @@ Documentation: https://frame-ui.com
6
6
 
7
7
  Current scope:
8
8
 
9
- - CSS-variable token contract
9
+ - CSS variables
10
10
  - Angular theme switching via `data-theme` or a shared `.dark` class
11
11
  - class merge helpers for future slot-based primitives
12
12
  - Vitest unit tests
13
13
 
14
14
  No primitives or complex components are included here.
15
15
 
16
- ## Token Contract
16
+ ## Tokens
17
17
 
18
- The foundation token contract is intentionally semantic and small.
18
+ The foundation tokens are intentionally semantic and small.
19
19
 
20
20
  ### Color tokens
21
21
 
@@ -29,6 +29,14 @@ The foundation token contract is intentionally semantic and small.
29
29
  - `--frame-primary-foreground`: readable text on `primary`
30
30
  - `--frame-accent`: secondary emphasis or selection highlight
31
31
  - `--frame-accent-foreground`: readable text on `accent`
32
+ - `--frame-destructive`: destructive or error emphasis
33
+ - `--frame-destructive-foreground`: readable text on `destructive`
34
+ - `--frame-success`: success emphasis
35
+ - `--frame-success-foreground`: readable text on `success`
36
+ - `--frame-warning`: warning emphasis
37
+ - `--frame-warning-foreground`: readable text on `warning`
38
+ - `--frame-info`: informational emphasis
39
+ - `--frame-info-foreground`: readable text on `info`
32
40
  - `--frame-border`: default border color
33
41
  - `--frame-input`: input field chrome
34
42
  - `--frame-ring`: focus ring color
@@ -38,13 +46,26 @@ The foundation token contract is intentionally semantic and small.
38
46
  - `--frame-radius-sm`
39
47
  - `--frame-radius-md`
40
48
  - `--frame-radius-lg`
49
+ - `--frame-radius-full`
50
+
51
+ The default FrameUI theme keeps `sm`, `md`, and `lg` at `0px` for the technical square look.
52
+ Override these tokens in the host app when a rounder theme is wanted.
41
53
 
42
54
  ### Elevation tokens
43
55
 
44
56
  - `--frame-shadow-sm`
45
57
  - `--frame-shadow-md`
58
+ - `--frame-shadow-lg`
59
+
60
+ Use `shadow` in `provideFrameUI` when the whole app should be flatter or more raised:
61
+
62
+ ```ts
63
+ provideFrameUI({
64
+ shadow: 'flat', // 'flat' | 'default' | 'raised'
65
+ });
66
+ ```
46
67
 
47
- ## Contract Rules
68
+ ## Token Rules
48
69
 
49
70
  - Tokens must describe purpose, not implementation.
50
71
  - Components should consume semantic tokens, never hardcoded theme colors.
@@ -87,49 +108,70 @@ Brand, product, or campaign differences should be handled by the host app's toke
87
108
 
88
109
  ## Theme Ownership
89
110
 
90
- The foundation layer should expose a token contract, not force itself to be the only dark mode owner.
111
+ The foundation layer should expose CSS tokens, not force itself to be the only dark mode owner.
91
112
 
92
- There are two recommended ownership models:
113
+ There are two questions:
93
114
 
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
115
+ - Who controls light/dark: FrameUI or the app?
116
+ - Which root selector is used: `data-theme` or `.dark`?
96
117
 
97
- ### Library-managed with `data-theme`
118
+ ### FrameUI controls `data-theme`
98
119
 
99
120
  This is the current default:
100
121
 
101
122
  ```ts
102
123
  provideFrameUI({
103
124
  defaultTheme: 'light',
125
+ theme: {
126
+ controlledBy: 'frame',
127
+ using: 'data-theme',
128
+ },
104
129
  });
105
130
  ```
106
131
 
107
- ### Library-managed with Tailwind's `.dark` class
132
+ `ThemeService.setTheme('dark')` writes `html[data-theme="dark"]`.
108
133
 
109
- If you want the FrameUI service to be the single source of truth, but Tailwind utilities should respond too, switch to class strategy:
134
+ ### FrameUI controls `.dark`
135
+
136
+ Use this when FrameUI should control the theme, but the app expects a shared `.dark` selector:
110
137
 
111
138
  ```ts
112
139
  provideFrameUI({
113
- strategy: 'class',
114
- className: 'dark',
140
+ theme: {
141
+ controlledBy: 'frame',
142
+ using: 'class',
143
+ },
115
144
  });
116
145
  ```
117
146
 
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.
147
+ `ThemeService.setTheme('dark')` adds `.dark` to the root element, so both FrameUI tokens and Tailwind `dark:` utilities can react to the same switch.
119
148
 
120
- ### Externally managed by Tailwind or another app shell
149
+ ### App controls `.dark`
121
150
 
122
- If the host app already owns dark mode, let the FrameUI observe instead of write:
151
+ Use this when Tailwind, an app shell, or another design layer already owns dark mode:
123
152
 
124
153
  ```ts
125
154
  provideFrameUI({
126
- strategy: 'class',
127
- mode: 'observe',
128
- className: 'dark',
155
+ theme: {
156
+ controlledBy: 'app',
157
+ using: 'class',
158
+ },
129
159
  });
130
160
  ```
131
161
 
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.
162
+ In this setup, FrameUI does not write light/dark state. It reads `html.dark` and keeps `ThemeService.theme()` in sync. Calling `ThemeService.setTheme()` throws because the app owns the switch.
163
+
164
+ ### Bootstrap
165
+
166
+ Bootstrap apps usually use `data-bs-theme`. FrameUI does not need to observe that attribute for visual theming. Map FrameUI tokens to Bootstrap variables in CSS instead:
167
+
168
+ ```css
169
+ :root {
170
+ --frame-background: var(--bs-body-bg);
171
+ --frame-foreground: var(--bs-body-color);
172
+ --frame-border: var(--bs-border-color);
173
+ }
174
+ ```
133
175
 
134
176
  ## Global Appearance Options
135
177
 
@@ -36,14 +36,18 @@ function withClassOverrides(slots, overrides) {
36
36
  }
37
37
 
38
38
  const DEFAULT_CONFIG = {
39
- attribute: 'data-theme',
40
- className: 'dark',
41
39
  defaultTheme: 'light',
40
+ density: null,
42
41
  disableCornerHandles: false,
43
- mode: 'managed',
44
- strategy: 'attribute',
42
+ shadow: null,
43
+ theme: {
44
+ controlledBy: 'frame',
45
+ using: 'data-theme',
46
+ },
45
47
  };
46
48
  const CORNER_HANDLES_ATTRIBUTE = 'data-frame-corner-handles';
49
+ const DENSITY_ATTRIBUTE = 'data-density';
50
+ const SHADOW_ATTRIBUTE = 'data-shadow';
47
51
  const FRAME_UI_CONFIG = new InjectionToken('FRAME_UI_CONFIG', {
48
52
  factory: () => DEFAULT_CONFIG,
49
53
  });
@@ -66,12 +70,14 @@ function provideFrameUI(options = {}) {
66
70
  function createFrameUIConfig(options = {}) {
67
71
  const defaultTheme = options.defaultTheme ?? DEFAULT_CONFIG.defaultTheme;
68
72
  return {
69
- attribute: options.attribute ?? DEFAULT_CONFIG.attribute,
70
- className: options.className ?? DEFAULT_CONFIG.className,
71
73
  defaultTheme,
74
+ density: options.density ?? DEFAULT_CONFIG.density,
72
75
  disableCornerHandles: options.disableCornerHandles ?? DEFAULT_CONFIG.disableCornerHandles,
73
- mode: options.mode ?? DEFAULT_CONFIG.mode,
74
- strategy: options.strategy ?? DEFAULT_CONFIG.strategy,
76
+ shadow: options.shadow ?? DEFAULT_CONFIG.shadow,
77
+ theme: {
78
+ controlledBy: options.theme?.controlledBy ?? DEFAULT_CONFIG.theme.controlledBy,
79
+ using: options.theme?.using ?? DEFAULT_CONFIG.theme.using,
80
+ },
75
81
  };
76
82
  }
77
83
  class ThemeService {
@@ -83,7 +89,9 @@ class ThemeService {
83
89
  isDark = computed(() => this.activeTheme() === 'dark', ...(ngDevMode ? [{ debugName: "isDark" }] : /* istanbul ignore next */ []));
84
90
  constructor() {
85
91
  this.applyCornerHandlesPreference();
86
- if (this.config.mode === 'observe') {
92
+ this.applyDensityPreference();
93
+ this.applyShadowPreference();
94
+ if (this.config.theme.controlledBy === 'app') {
87
95
  this.syncFromDom();
88
96
  this.observeThemeChanges();
89
97
  return;
@@ -94,8 +102,8 @@ class ThemeService {
94
102
  if (!isFrameUITheme(name)) {
95
103
  throw new Error(`Unknown theme "${name}".`);
96
104
  }
97
- if (this.config.mode === 'observe') {
98
- throw new Error('ThemeService is configured to observe external theme state and cannot set the theme.');
105
+ if (this.config.theme.controlledBy === 'app') {
106
+ throw new Error('ThemeService is configured to read app-controlled theme state and cannot set the theme.');
99
107
  }
100
108
  this.activeTheme.set(name);
101
109
  this.applyTheme(name);
@@ -114,11 +122,11 @@ class ThemeService {
114
122
  if (!root) {
115
123
  return;
116
124
  }
117
- if (this.config.strategy === 'class') {
118
- root.classList.toggle(this.config.className, name === 'dark');
125
+ if (this.config.theme.using === 'class') {
126
+ root.classList.toggle('dark', name === 'dark');
119
127
  return;
120
128
  }
121
- root.setAttribute(this.config.attribute, name);
129
+ root.setAttribute('data-theme', name);
122
130
  }
123
131
  applyCornerHandlesPreference() {
124
132
  const root = this.document?.documentElement;
@@ -131,12 +139,26 @@ class ThemeService {
131
139
  }
132
140
  root.removeAttribute(CORNER_HANDLES_ATTRIBUTE);
133
141
  }
142
+ applyDensityPreference() {
143
+ const root = this.document?.documentElement;
144
+ if (!root || !this.config.density) {
145
+ return;
146
+ }
147
+ root.setAttribute(DENSITY_ATTRIBUTE, this.config.density);
148
+ }
149
+ applyShadowPreference() {
150
+ const root = this.document?.documentElement;
151
+ if (!root || !this.config.shadow) {
152
+ return;
153
+ }
154
+ root.setAttribute(SHADOW_ATTRIBUTE, this.config.shadow);
155
+ }
134
156
  observeThemeChanges() {
135
157
  const root = this.document?.documentElement;
136
158
  if (!root || typeof MutationObserver === 'undefined') {
137
159
  return;
138
160
  }
139
- const attributeFilter = this.config.strategy === 'class' ? ['class'] : [this.config.attribute];
161
+ const attributeFilter = this.config.theme.using === 'class' ? ['class'] : ['data-theme'];
140
162
  this.observer = new MutationObserver(() => {
141
163
  this.syncFromDom();
142
164
  });
@@ -153,10 +175,10 @@ class ThemeService {
153
175
  this.activeTheme.set(this.readThemeFromDom(root));
154
176
  }
155
177
  readThemeFromDom(root) {
156
- if (this.config.strategy === 'class') {
157
- return root.classList.contains(this.config.className) ? 'dark' : 'light';
178
+ if (this.config.theme.using === 'class') {
179
+ return root.classList.contains('dark') ? 'dark' : 'light';
158
180
  }
159
- const theme = root.getAttribute(this.config.attribute);
181
+ const theme = root.getAttribute('data-theme');
160
182
  return isFrameUITheme(theme) ? theme : this.config.defaultTheme;
161
183
  }
162
184
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: ThemeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
@@ -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';\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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"}
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 FrameUIThemeController = 'frame' | 'app';\r\nexport type FrameUIThemeSelector = 'data-theme' | 'class';\r\nexport type FrameUITheme = 'light' | 'dark';\r\nexport type FrameUIDensity = 'compact' | 'default' | 'comfortable';\r\nexport type FrameUIShadow = 'flat' | 'default' | 'raised';\r\n\r\nexport interface FrameUIThemeConfig {\r\n controlledBy: FrameUIThemeController;\r\n using: FrameUIThemeSelector;\r\n}\r\n\r\nexport interface FrameUIConfig {\r\n defaultTheme: FrameUITheme;\r\n density: FrameUIDensity | null;\r\n disableCornerHandles: boolean;\r\n shadow: FrameUIShadow | null;\r\n theme: FrameUIThemeConfig;\r\n}\r\n\r\nconst DEFAULT_CONFIG: FrameUIConfig = {\r\n defaultTheme: 'light',\r\n density: null,\r\n disableCornerHandles: false,\r\n shadow: null,\r\n theme: {\r\n controlledBy: 'frame',\r\n using: 'data-theme',\r\n },\r\n};\r\n\r\nconst CORNER_HANDLES_ATTRIBUTE = 'data-frame-corner-handles';\r\nconst DENSITY_ATTRIBUTE = 'data-density';\r\nconst SHADOW_ATTRIBUTE = 'data-shadow';\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 defaultTheme?: FrameUITheme;\r\n density?: FrameUIDensity;\r\n disableCornerHandles?: boolean;\r\n shadow?: FrameUIShadow;\r\n theme?: Partial<FrameUIThemeConfig>;\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 defaultTheme,\r\n density: options.density ?? DEFAULT_CONFIG.density,\r\n disableCornerHandles:\r\n options.disableCornerHandles ?? DEFAULT_CONFIG.disableCornerHandles,\r\n shadow: options.shadow ?? DEFAULT_CONFIG.shadow,\r\n theme: {\r\n controlledBy:\r\n options.theme?.controlledBy ?? DEFAULT_CONFIG.theme.controlledBy,\r\n using: options.theme?.using ?? DEFAULT_CONFIG.theme.using,\r\n },\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 this.applyDensityPreference();\r\n this.applyShadowPreference();\r\n\r\n if (this.config.theme.controlledBy === 'app') {\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.theme.controlledBy === 'app') {\r\n throw new Error(\r\n 'ThemeService is configured to read app-controlled 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.theme.using === 'class') {\r\n root.classList.toggle('dark', name === 'dark');\r\n return;\r\n }\r\n\r\n root.setAttribute('data-theme', 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 applyDensityPreference(): void {\r\n const root = this.document?.documentElement;\r\n\r\n if (!root || !this.config.density) {\r\n return;\r\n }\r\n\r\n root.setAttribute(DENSITY_ATTRIBUTE, this.config.density);\r\n }\r\n\r\n private applyShadowPreference(): void {\r\n const root = this.document?.documentElement;\r\n\r\n if (!root || !this.config.shadow) {\r\n return;\r\n }\r\n\r\n root.setAttribute(SHADOW_ATTRIBUTE, this.config.shadow);\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.theme.using === 'class' ? ['class'] : ['data-theme'];\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.theme.using === 'class') {\r\n return root.classList.contains('dark') ? 'dark' : 'light';\r\n }\r\n\r\n const theme = root.getAttribute('data-theme');\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.4.2-beta.0",
3
+ "version": "0.5.1-beta.0",
4
4
  "description": "Foundation utilities, tokens, and providers for FrameUI.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,5 +1,4 @@
1
- :root,
2
- [data-theme='light'] {
1
+ :where(:root, [data-theme='light']) {
3
2
  /* Theme metadata */
4
3
  color-scheme: light;
5
4
 
@@ -21,6 +20,12 @@
21
20
  --frame-accent-foreground: oklch(0.205 0 0);
22
21
  --frame-destructive: oklch(55.205% 0.18114 25.384);
23
22
  --frame-destructive-foreground: oklch(100% 0.00011 271.152);
23
+ --frame-success: oklch(0.62 0.18 149);
24
+ --frame-success-foreground: oklch(0.985 0 0);
25
+ --frame-warning: oklch(0.68 0.16 65);
26
+ --frame-warning-foreground: oklch(0.145 0 0);
27
+ --frame-info: oklch(0.58 0.19 255);
28
+ --frame-info-foreground: oklch(0.985 0 0);
24
29
 
25
30
  /* Structure and focus */
26
31
  --frame-border: oklch(0.87 0.006 247);
@@ -29,7 +34,7 @@
29
34
  --frame-ring: #d71920;
30
35
  --frame-frame-line: color-mix(in srgb, var(--frame-border-strong) 48%, transparent);
31
36
  --frame-frame-line-muted: color-mix(in srgb, var(--frame-border) 54%, transparent);
32
- --frame-frame-accent: #d71920;
37
+ --frame-frame-accent: var(--frame-ring);
33
38
  --frame-frame-tick-size: 0.375rem;
34
39
  --frame-frame-tick-inset: 0;
35
40
 
@@ -40,21 +45,52 @@
40
45
  --frame-spacing-lg: 1rem;
41
46
  --frame-spacing-xl: 1.25rem;
42
47
 
48
+ /* Density */
49
+ --frame-density-gap-xs: 0.125rem;
50
+ --frame-density-gap-sm: 0.25rem;
51
+ --frame-density-gap-md: 0.5rem;
52
+ --frame-density-gap-lg: 0.75rem;
53
+ --frame-density-gap-xl: 1rem;
54
+ --frame-density-control-height-sm: 2rem;
55
+ --frame-density-control-height-md: 2.25rem;
56
+ --frame-density-control-height-lg: 2.5rem;
57
+ --frame-density-control-height-xl: 2.875rem;
58
+ --frame-density-control-padding-x-sm: 0.75rem;
59
+ --frame-density-control-padding-x-md: 0.875rem;
60
+ --frame-density-control-padding-x-lg: 1rem;
61
+ --frame-density-inline-height: 1.375rem;
62
+ --frame-density-inline-padding-block: 0.1875rem;
63
+ --frame-density-inline-padding-inline: 0.625rem;
64
+ --frame-density-item-height: 2rem;
65
+ --frame-density-item-padding: 0.375rem 0.5rem;
66
+ --frame-density-file-padding-block: 0.375rem;
67
+ --frame-density-overlay-padding-block: 0.375rem;
68
+ --frame-density-overlay-padding-inline: 0.625rem;
69
+ --frame-density-shortcut-padding-block: 0.0625rem;
70
+ --frame-density-shortcut-padding-inline: 0.3125rem;
71
+ --frame-density-panel-padding-sm: 0.25rem;
72
+ --frame-density-panel-padding-md: 1rem;
73
+ --frame-density-panel-padding-lg: 1.5rem;
74
+ --frame-density-table-cell-padding-block: 0.75rem;
75
+ --frame-density-table-cell-padding-inline: 1rem;
76
+ --frame-density-table-cell-padding-block-sm: 0.5rem;
77
+ --frame-density-table-cell-padding-inline-sm: 0.75rem;
78
+ --frame-density-table-cell-padding-block-lg: 1rem;
79
+ --frame-density-table-cell-padding-inline-lg: 1.125rem;
80
+
43
81
  /* Shape */
44
- --frame-radius-xs: 0.125rem;
45
- --frame-radius-sm: 0.375rem;
46
- --frame-radius-md: 0.5rem;
47
- --frame-radius-lg: 0.75rem;
82
+ --frame-radius-sm: 0px;
83
+ --frame-radius-md: 0px;
84
+ --frame-radius-lg: 0px;
48
85
  --frame-radius-full: 9999px;
49
86
 
50
87
  /* Elevation */
51
88
  --frame-shadow-sm: none;
52
89
  --frame-shadow-md: 0 10px 15px -3px rgb(0 0 0 / 0.1);
53
- --frame-shadow-frame: none;
90
+ --frame-shadow-lg: 0 24px 80px rgb(0 0 0 / 0.18), 0 8px 24px rgb(0 0 0 / 0.12);
54
91
  }
55
92
 
56
- [data-theme='dark'],
57
- .dark {
93
+ :where([data-theme='dark'], .dark) {
58
94
  /* Theme metadata */
59
95
  color-scheme: dark;
60
96
 
@@ -74,6 +110,14 @@
74
110
  --frame-primary-foreground: oklch(0.205 0 0);
75
111
  --frame-accent: oklch(0.269 0 0);
76
112
  --frame-accent-foreground: oklch(0.985 0 0);
113
+ --frame-destructive: oklch(0.704 0.191 22.216);
114
+ --frame-destructive-foreground: oklch(0.985 0 0);
115
+ --frame-success: oklch(0.72 0.17 149);
116
+ --frame-success-foreground: oklch(0.145 0 0);
117
+ --frame-warning: oklch(0.78 0.16 75);
118
+ --frame-warning-foreground: oklch(0.145 0 0);
119
+ --frame-info: oklch(0.72 0.15 255);
120
+ --frame-info-foreground: oklch(0.145 0 0);
77
121
 
78
122
  /* Structure and focus */
79
123
  --frame-border: oklch(1 0 0 / 0.08);
@@ -82,10 +126,107 @@
82
126
  --frame-ring: #ff4a4f;
83
127
  --frame-frame-line: color-mix(in srgb, var(--frame-border-strong) 54%, transparent);
84
128
  --frame-frame-line-muted: color-mix(in srgb, var(--frame-border) 62%, transparent);
85
- --frame-frame-accent: #ff4a4f;
129
+ --frame-frame-accent: var(--frame-ring);
86
130
 
87
131
  /* Elevation */
88
132
  --frame-shadow-sm: none;
89
133
  --frame-shadow-md: 0 10px 15px -3px rgb(0 0 0 / 0.35);
90
- --frame-shadow-frame: none;
134
+ --frame-shadow-lg: 0 24px 80px rgb(0 0 0 / 0.38), 0 8px 24px rgb(0 0 0 / 0.24);
135
+ }
136
+
137
+ :where([data-shadow='flat']) {
138
+ --frame-shadow-sm: none;
139
+ --frame-shadow-md: none;
140
+ --frame-shadow-lg: none;
141
+ }
142
+
143
+ :where([data-shadow='default']) {
144
+ --frame-shadow-sm: none;
145
+ --frame-shadow-md: 0 10px 15px -3px rgb(0 0 0 / 0.1);
146
+ --frame-shadow-lg: 0 24px 80px rgb(0 0 0 / 0.18), 0 8px 24px rgb(0 0 0 / 0.12);
147
+ }
148
+
149
+ :where([data-theme='dark'], .dark):where([data-shadow='default']) {
150
+ --frame-shadow-md: 0 10px 15px -3px rgb(0 0 0 / 0.35);
151
+ --frame-shadow-lg: 0 24px 80px rgb(0 0 0 / 0.38), 0 8px 24px rgb(0 0 0 / 0.24);
152
+ }
153
+
154
+ :where([data-shadow='raised']) {
155
+ --frame-shadow-sm: 0 2px 8px rgb(0 0 0 / 0.1);
156
+ --frame-shadow-md: 0 16px 32px -12px rgb(0 0 0 / 0.22);
157
+ --frame-shadow-lg: 0 32px 90px rgb(0 0 0 / 0.24), 0 12px 32px rgb(0 0 0 / 0.16);
158
+ }
159
+
160
+ :where([data-theme='dark'], .dark):where([data-shadow='raised']) {
161
+ --frame-shadow-sm: 0 2px 8px rgb(0 0 0 / 0.32);
162
+ --frame-shadow-md: 0 16px 32px -12px rgb(0 0 0 / 0.52);
163
+ --frame-shadow-lg: 0 32px 90px rgb(0 0 0 / 0.58), 0 12px 32px rgb(0 0 0 / 0.36);
164
+ }
165
+
166
+ :where([data-density='compact']) {
167
+ --frame-density-gap-xs: 0.0625rem;
168
+ --frame-density-gap-sm: 0.1875rem;
169
+ --frame-density-gap-md: 0.375rem;
170
+ --frame-density-gap-lg: 0.5rem;
171
+ --frame-density-gap-xl: 0.75rem;
172
+ --frame-density-control-height-sm: 1.75rem;
173
+ --frame-density-control-height-md: 2rem;
174
+ --frame-density-control-height-lg: 2.25rem;
175
+ --frame-density-control-height-xl: 2.5rem;
176
+ --frame-density-control-padding-x-sm: 0.5rem;
177
+ --frame-density-control-padding-x-md: 0.75rem;
178
+ --frame-density-control-padding-x-lg: 0.875rem;
179
+ --frame-density-inline-height: 1.25rem;
180
+ --frame-density-inline-padding-block: 0.125rem;
181
+ --frame-density-inline-padding-inline: 0.5rem;
182
+ --frame-density-item-height: 1.75rem;
183
+ --frame-density-item-padding: 0.25rem 0.5rem;
184
+ --frame-density-file-padding-block: 0.25rem;
185
+ --frame-density-overlay-padding-block: 0.25rem;
186
+ --frame-density-overlay-padding-inline: 0.5rem;
187
+ --frame-density-shortcut-padding-block: 0.0625rem;
188
+ --frame-density-shortcut-padding-inline: 0.25rem;
189
+ --frame-density-panel-padding-sm: 0.1875rem;
190
+ --frame-density-panel-padding-md: 0.75rem;
191
+ --frame-density-panel-padding-lg: 1rem;
192
+ --frame-density-table-cell-padding-block: 0.5rem;
193
+ --frame-density-table-cell-padding-inline: 0.75rem;
194
+ --frame-density-table-cell-padding-block-sm: 0.375rem;
195
+ --frame-density-table-cell-padding-inline-sm: 0.5rem;
196
+ --frame-density-table-cell-padding-block-lg: 0.75rem;
197
+ --frame-density-table-cell-padding-inline-lg: 0.875rem;
198
+ }
199
+
200
+ :where([data-density='comfortable']) {
201
+ --frame-density-gap-xs: 0.1875rem;
202
+ --frame-density-gap-sm: 0.375rem;
203
+ --frame-density-gap-md: 0.625rem;
204
+ --frame-density-gap-lg: 0.875rem;
205
+ --frame-density-gap-xl: 1.25rem;
206
+ --frame-density-control-height-sm: 2.25rem;
207
+ --frame-density-control-height-md: 2.5rem;
208
+ --frame-density-control-height-lg: 2.75rem;
209
+ --frame-density-control-height-xl: 3.125rem;
210
+ --frame-density-control-padding-x-sm: 0.875rem;
211
+ --frame-density-control-padding-x-md: 1rem;
212
+ --frame-density-control-padding-x-lg: 1.125rem;
213
+ --frame-density-inline-height: 1.5rem;
214
+ --frame-density-inline-padding-block: 0.25rem;
215
+ --frame-density-inline-padding-inline: 0.75rem;
216
+ --frame-density-item-height: 2.25rem;
217
+ --frame-density-item-padding: 0.5rem 0.625rem;
218
+ --frame-density-file-padding-block: 0.5rem;
219
+ --frame-density-overlay-padding-block: 0.5rem;
220
+ --frame-density-overlay-padding-inline: 0.75rem;
221
+ --frame-density-shortcut-padding-block: 0.125rem;
222
+ --frame-density-shortcut-padding-inline: 0.375rem;
223
+ --frame-density-panel-padding-sm: 0.375rem;
224
+ --frame-density-panel-padding-md: 1.25rem;
225
+ --frame-density-panel-padding-lg: 1.75rem;
226
+ --frame-density-table-cell-padding-block: 1rem;
227
+ --frame-density-table-cell-padding-inline: 1.125rem;
228
+ --frame-density-table-cell-padding-block-sm: 0.625rem;
229
+ --frame-density-table-cell-padding-inline-sm: 0.875rem;
230
+ --frame-density-table-cell-padding-block-lg: 1.125rem;
231
+ --frame-density-table-cell-padding-inline-lg: 1.25rem;
91
232
  }
@@ -8,25 +8,29 @@ type SlotClassOverrides<TSlot extends string> = Partial<Record<TSlot, ClassValue
8
8
  declare function cx(...values: readonly ClassValue[]): string;
9
9
  declare function withClassOverrides<TSlot extends string>(slots: SlotClasses<TSlot>, overrides?: SlotClassOverrides<TSlot>): Record<TSlot, string>;
10
10
 
11
- type ThemeBindingStrategy = 'attribute' | 'class';
12
- type ThemeSyncMode = 'managed' | 'observe';
11
+ type FrameUIThemeController = 'frame' | 'app';
12
+ type FrameUIThemeSelector = 'data-theme' | 'class';
13
13
  type FrameUITheme = 'light' | 'dark';
14
+ type FrameUIDensity = 'compact' | 'default' | 'comfortable';
15
+ type FrameUIShadow = 'flat' | 'default' | 'raised';
16
+ interface FrameUIThemeConfig {
17
+ controlledBy: FrameUIThemeController;
18
+ using: FrameUIThemeSelector;
19
+ }
14
20
  interface FrameUIConfig {
15
- attribute: string;
16
- className: string;
17
21
  defaultTheme: FrameUITheme;
22
+ density: FrameUIDensity | null;
18
23
  disableCornerHandles: boolean;
19
- mode: ThemeSyncMode;
20
- strategy: ThemeBindingStrategy;
24
+ shadow: FrameUIShadow | null;
25
+ theme: FrameUIThemeConfig;
21
26
  }
22
27
  declare const FRAME_UI_CONFIG: InjectionToken<FrameUIConfig>;
23
28
  interface FrameUIOptions {
24
- attribute?: string;
25
- className?: string;
26
29
  defaultTheme?: FrameUITheme;
30
+ density?: FrameUIDensity;
27
31
  disableCornerHandles?: boolean;
28
- mode?: ThemeSyncMode;
29
- strategy?: ThemeBindingStrategy;
32
+ shadow?: FrameUIShadow;
33
+ theme?: Partial<FrameUIThemeConfig>;
30
34
  }
31
35
  declare function provideFrameUI(options?: FrameUIOptions): EnvironmentProviders;
32
36
  declare function createFrameUIConfig(options?: FrameUIOptions): FrameUIConfig;
@@ -43,6 +47,8 @@ declare class ThemeService implements OnDestroy {
43
47
  ngOnDestroy(): void;
44
48
  private applyTheme;
45
49
  private applyCornerHandlesPreference;
50
+ private applyDensityPreference;
51
+ private applyShadowPreference;
46
52
  private observeThemeChanges;
47
53
  private syncFromDom;
48
54
  private readThemeFromDom;
@@ -51,4 +57,4 @@ declare class ThemeService implements OnDestroy {
51
57
  }
52
58
 
53
59
  export { FRAME_UI_CONFIG, ThemeService, createFrameUIConfig, cx, provideFrameUI, withClassOverrides };
54
- export type { ClassDictionary, ClassValue, FrameUIConfig, FrameUIOptions, FrameUITheme, SlotClassOverrides, SlotClasses, ThemeBindingStrategy, ThemeSyncMode };
60
+ export type { ClassDictionary, ClassValue, FrameUIConfig, FrameUIDensity, FrameUIOptions, FrameUIShadow, FrameUITheme, FrameUIThemeConfig, FrameUIThemeController, FrameUIThemeSelector, SlotClassOverrides, SlotClasses };