@esphome/compose-ui 0.2.0 → 0.3.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/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _esphome_compose from '@esphome/compose';
2
- import { EspComposeElement } from '@esphome/compose';
2
+ import { EspComposeElement, TriggerHandler } from '@esphome/compose';
3
3
 
4
4
  /**
5
5
  * Theme type definitions for the LVGL design system.
@@ -30,6 +30,17 @@ interface StatusColors {
30
30
  text: string;
31
31
  bgPressed: string;
32
32
  }
33
+ interface PartColors {
34
+ /** Primary fill / track color. */
35
+ bg: string;
36
+ /** Knob / handle color. */
37
+ knob: string;
38
+ }
39
+ interface ThemeParts {
40
+ slider: PartColors;
41
+ switch: PartColors;
42
+ arc: PartColors;
43
+ }
33
44
  interface ThemeColors {
34
45
  primary: StatusColors;
35
46
  secondary: StatusColors;
@@ -63,6 +74,13 @@ interface Theme {
63
74
  radii: Record<RadiusToken, number>;
64
75
  /** Component size scale. */
65
76
  sizes: Record<SizeToken, SizeDimensions>;
77
+ /**
78
+ * Widget part colors (slider indicator/knob, switch, arc).
79
+ *
80
+ * Optional — when omitted, `themeToStyleDefinitions()` derives sensible
81
+ * defaults from `colors.primary` and `colors.textPrimary`.
82
+ */
83
+ parts?: ThemeParts;
66
84
  }
67
85
 
68
86
  /** The theme context — defaults to `darkTheme` when no provider is present. */
@@ -130,6 +148,98 @@ declare function fontDefToLvgl(def: FontDef): string;
130
148
  /** Resolve a radius token or pass through a raw pixel value. */
131
149
  declare function resolveRadius(value: RadiusToken | number): number;
132
150
 
151
+ /**
152
+ * Theme → LVGL bridge.
153
+ *
154
+ * Converts the high-level `Theme` token system into:
155
+ * 1. `style_definitions` — named style bundles referenced by DS components
156
+ * 2. `theme` — static per-widget-type defaults for unthemed raw LVGL widgets
157
+ * 3. `createThemeSwitchActions()` — runtime theme switching via `lvgl.style.update`
158
+ */
159
+
160
+ interface StyleDefinition {
161
+ id: string;
162
+ [prop: string]: unknown;
163
+ }
164
+ /**
165
+ * Convert a `Theme` into an array of ESPHome `style_definitions` entries.
166
+ *
167
+ * Each entry is a plain object with snake_case keys matching the LVGL YAML
168
+ * schema. Colors remain as `#RRGGBB` strings — the serializer converts them
169
+ * to `0xRRGGBB` automatically.
170
+ */
171
+ declare function themeToStyleDefinitions(theme: Theme): StyleDefinition[];
172
+ /**
173
+ * Build the LVGL `theme:` block — static defaults applied to all widgets of
174
+ * a given type. This is the fallback for raw `<lvgl-*>` elements that don't
175
+ * use `styles:` references.
176
+ *
177
+ * NOTE: The `theme:` block is NOT runtime-updatable. Only `style_definitions`
178
+ * (via `lvgl.style.update`) support runtime switching.
179
+ */
180
+ declare function themeToLvglTheme(theme: Theme): Record<string, unknown>;
181
+ /**
182
+ * Build the props to spread on the `<lvgl>` element.
183
+ *
184
+ * ```tsx
185
+ * <lvgl displays={[ref]} {...createLvglThemeProps(darkTheme)}>
186
+ * ```
187
+ */
188
+ declare function createLvglThemeProps(theme: Theme): {
189
+ styleDefinitions: StyleDefinition[];
190
+ theme: Record<string, unknown>;
191
+ };
192
+ /**
193
+ * Apply a theme at runtime inside a trigger function.
194
+ *
195
+ * The ESPCompose compiler recognises `applyTheme()` calls inside trigger
196
+ * bodies and spreads the resulting `lvgl.style.update` actions into the
197
+ * action list.
198
+ *
199
+ * @example
200
+ * ```tsx
201
+ * <Button
202
+ * text="Dark mode"
203
+ * onPress={() => { applyTheme(darkTheme); }}
204
+ * />
205
+ * ```
206
+ *
207
+ * @espcomposeAction applyTheme
208
+ */
209
+ declare function applyTheme(theme: Theme): Array<Record<string, unknown>>;
210
+
211
+ /**
212
+ * LVGL style definition ID constants.
213
+ *
214
+ * Every design-system component references these IDs via the LVGL `styles:`
215
+ * prop instead of inlining visual values. The IDs correspond 1-to-1 with
216
+ * entries generated by `themeToStyleDefinitions()` in `bridge.ts`.
217
+ *
218
+ * Naming: `ds_{category}[_{variant}]`
219
+ * NOTE: ESPHome requires underscores in IDs — dashes are not allowed.
220
+ */
221
+
222
+ declare const STYLE_BG = "ds_bg";
223
+ declare const STYLE_SURFACE = "ds_surface";
224
+ declare const STYLE_SURFACE_ALT = "ds_surface_alt";
225
+ declare const STYLE_BORDER = "ds_border";
226
+ declare const STYLE_TEXT_PRIMARY = "ds_text_primary";
227
+ declare const STYLE_TEXT_SECONDARY = "ds_text_secondary";
228
+ declare const STYLE_TEXT_DISABLED = "ds_text_disabled";
229
+ /** Typography variant style IDs. */
230
+ declare const STYLE_TEXT_VARIANT: Record<TextVariant, string>;
231
+ type ButtonVariant$1 = 'solid' | 'outline';
232
+ /** Style ID for a status button container. */
233
+ declare function statusStyleId(status: StatusToken, variant: ButtonVariant$1): string;
234
+ /** Style ID for a status button's inner label text. */
235
+ declare function statusTextStyleId(status: StatusToken, variant: ButtonVariant$1): string;
236
+ declare const STYLE_SLIDER_INDICATOR = "ds_slider_indicator";
237
+ declare const STYLE_SLIDER_KNOB = "ds_slider_knob";
238
+ declare const STYLE_SWITCH_INDICATOR = "ds_switch_indicator";
239
+ declare const STYLE_SWITCH_KNOB = "ds_switch_knob";
240
+ declare const STYLE_ARC_INDICATOR = "ds_arc_indicator";
241
+ declare const STYLE_ARC_KNOB = "ds_arc_knob";
242
+
133
243
  /**
134
244
  * Intent constants for @esphome/compose-ui design system.
135
245
  *
@@ -151,7 +261,7 @@ interface ScreenProps {
151
261
  padding?: SpacingToken | number;
152
262
  /** Skip this page in the page list. */
153
263
  skip?: boolean;
154
- /** Background color override (hex). If omitted, uses theme background. */
264
+ /** Background color override (hex). If set, overrides the style definition. */
155
265
  bgColor?: string;
156
266
  /** Border width in pixels. Default: 0. */
157
267
  borderWidth?: number;
@@ -161,7 +271,7 @@ interface ScreenProps {
161
271
  /**
162
272
  * Screen — a top-level LVGL page container.
163
273
  *
164
- * Applies the active theme's background color by default.
274
+ * Applies the active theme's background color by default via style reference.
165
275
  *
166
276
  * @example
167
277
  * <Screen padding="lg">
@@ -389,7 +499,7 @@ interface TextProps {
389
499
  text?: string;
390
500
  /** Text alignment within the label. */
391
501
  align?: 'LEFT' | 'CENTER' | 'RIGHT' | 'AUTO';
392
- /** Text color (hex). If omitted, uses theme textPrimary. */
502
+ /** Text color (hex). If omitted, inherits from the variant style definition. */
393
503
  color?: string;
394
504
  /** Long text mode. */
395
505
  longMode?: 'WRAP' | 'DOT' | 'SCROLL' | 'SCROLL_CIRCULAR' | 'CLIP';
@@ -427,8 +537,8 @@ interface ButtonProps {
427
537
  width?: number | string;
428
538
  /** Height override. */
429
539
  height?: number | string;
430
- /** Press handler (ESPHome action). */
431
- onPress?: unknown;
540
+ /** Press handler (ESPHome trigger function). */
541
+ onPress?: TriggerHandler;
432
542
  }
433
543
  /**
434
544
  * Button — a styled, clickable button with a label.
@@ -445,7 +555,7 @@ interface CardProps {
445
555
  padding?: SpacingToken | number;
446
556
  /** Corner radius. Default: 'md'. */
447
557
  radius?: RadiusToken | number;
448
- /** Background color (hex). Default: theme surfaceAlt. */
558
+ /** Background color override (hex). If set, overrides the style definition. */
449
559
  bgColor?: string;
450
560
  /** Border color (hex). */
451
561
  borderColor?: string;
@@ -475,7 +585,9 @@ interface SliderFieldProps {
475
585
  /** Bound value (sensor or entity reference). */
476
586
  value?: unknown;
477
587
  /** Change handler (ESPHome action). */
478
- onChange?: unknown;
588
+ onChange?: TriggerHandler<{
589
+ x: number;
590
+ }>;
479
591
  /** Minimum value. Default: 0. */
480
592
  min?: number;
481
593
  /** Maximum value. Default: 100. */
@@ -493,18 +605,15 @@ interface SliderFieldProps {
493
605
  */
494
606
  declare const SliderField: _esphome_compose.IntentComponent<SliderFieldProps, readonly ["lvgl:widget"], readonly [], undefined, undefined>;
495
607
 
496
- /**
497
- * SwitchField — a label + switch in a row layout.
498
- *
499
- * Compiles to a container with a label and a switch widget.
500
- */
501
608
  interface SwitchFieldProps {
502
609
  /** Label text displayed next to the switch. */
503
610
  label: string;
504
611
  /** Bound value (sensor or entity reference). */
505
612
  value?: unknown;
506
613
  /** Change handler (ESPHome action). */
507
- onChange?: unknown;
614
+ onChange?: TriggerHandler<{
615
+ x: boolean;
616
+ }>;
508
617
  /** Width of the field container. */
509
618
  width?: number | string;
510
619
  }
@@ -524,7 +633,9 @@ interface DropdownFieldProps {
524
633
  /** Bound selection index. */
525
634
  value?: unknown;
526
635
  /** Change handler (ESPHome action). */
527
- onChange?: unknown;
636
+ onChange?: TriggerHandler<{
637
+ x: number;
638
+ }>;
528
639
  /** Gap between label and dropdown. Default: 'xs'. */
529
640
  gap?: SpacingToken | number;
530
641
  /** Width of the field container. */
@@ -538,4 +649,4 @@ interface DropdownFieldProps {
538
649
  */
539
650
  declare const DropdownField: _esphome_compose.IntentComponent<DropdownFieldProps, readonly ["lvgl:widget"], readonly [], undefined, undefined>;
540
651
 
541
- export { Button, COMPOSE_UI_INTENTS, Card, Col, DropdownField, type FontDef, Grid, GridItem, HStack, type RadiusToken, Row, Screen, type SizeDimensions, type SizeToken, SliderField, Space, type SpacingToken, type StatusColors, type StatusToken, SwitchField, Text, type TextVariant, type Theme, type ThemeColors, ThemeContext, ThemeProvider, type ThemeTypography, type TrackSize, VStack, darkTheme, fontDefToLvgl, lightTheme, resolveRadius, resolveSize, resolveSpacing, resolveStatus, resolveTypography, themeFromJSON, themeToJSON, useTheme };
652
+ export { Button, COMPOSE_UI_INTENTS, Card, Col, DropdownField, type FontDef, Grid, GridItem, HStack, type PartColors, type RadiusToken, Row, STYLE_ARC_INDICATOR, STYLE_ARC_KNOB, STYLE_BG, STYLE_BORDER, STYLE_SLIDER_INDICATOR, STYLE_SLIDER_KNOB, STYLE_SURFACE, STYLE_SURFACE_ALT, STYLE_SWITCH_INDICATOR, STYLE_SWITCH_KNOB, STYLE_TEXT_DISABLED, STYLE_TEXT_PRIMARY, STYLE_TEXT_SECONDARY, STYLE_TEXT_VARIANT, Screen, type SizeDimensions, type SizeToken, SliderField, Space, type SpacingToken, type StatusColors, type StatusToken, type StyleDefinition, SwitchField, Text, type TextVariant, type Theme, type ThemeColors, ThemeContext, type ThemeParts, ThemeProvider, type ThemeTypography, type TrackSize, VStack, applyTheme, createLvglThemeProps, darkTheme, fontDefToLvgl, lightTheme, resolveRadius, resolveSize, resolveSpacing, resolveStatus, resolveTypography, statusStyleId, statusTextStyleId, themeFromJSON, themeToJSON, themeToLvglTheme, themeToStyleDefinitions, useTheme };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _esphome_compose from '@esphome/compose';
2
- import { EspComposeElement } from '@esphome/compose';
2
+ import { EspComposeElement, TriggerHandler } from '@esphome/compose';
3
3
 
4
4
  /**
5
5
  * Theme type definitions for the LVGL design system.
@@ -30,6 +30,17 @@ interface StatusColors {
30
30
  text: string;
31
31
  bgPressed: string;
32
32
  }
33
+ interface PartColors {
34
+ /** Primary fill / track color. */
35
+ bg: string;
36
+ /** Knob / handle color. */
37
+ knob: string;
38
+ }
39
+ interface ThemeParts {
40
+ slider: PartColors;
41
+ switch: PartColors;
42
+ arc: PartColors;
43
+ }
33
44
  interface ThemeColors {
34
45
  primary: StatusColors;
35
46
  secondary: StatusColors;
@@ -63,6 +74,13 @@ interface Theme {
63
74
  radii: Record<RadiusToken, number>;
64
75
  /** Component size scale. */
65
76
  sizes: Record<SizeToken, SizeDimensions>;
77
+ /**
78
+ * Widget part colors (slider indicator/knob, switch, arc).
79
+ *
80
+ * Optional — when omitted, `themeToStyleDefinitions()` derives sensible
81
+ * defaults from `colors.primary` and `colors.textPrimary`.
82
+ */
83
+ parts?: ThemeParts;
66
84
  }
67
85
 
68
86
  /** The theme context — defaults to `darkTheme` when no provider is present. */
@@ -130,6 +148,98 @@ declare function fontDefToLvgl(def: FontDef): string;
130
148
  /** Resolve a radius token or pass through a raw pixel value. */
131
149
  declare function resolveRadius(value: RadiusToken | number): number;
132
150
 
151
+ /**
152
+ * Theme → LVGL bridge.
153
+ *
154
+ * Converts the high-level `Theme` token system into:
155
+ * 1. `style_definitions` — named style bundles referenced by DS components
156
+ * 2. `theme` — static per-widget-type defaults for unthemed raw LVGL widgets
157
+ * 3. `createThemeSwitchActions()` — runtime theme switching via `lvgl.style.update`
158
+ */
159
+
160
+ interface StyleDefinition {
161
+ id: string;
162
+ [prop: string]: unknown;
163
+ }
164
+ /**
165
+ * Convert a `Theme` into an array of ESPHome `style_definitions` entries.
166
+ *
167
+ * Each entry is a plain object with snake_case keys matching the LVGL YAML
168
+ * schema. Colors remain as `#RRGGBB` strings — the serializer converts them
169
+ * to `0xRRGGBB` automatically.
170
+ */
171
+ declare function themeToStyleDefinitions(theme: Theme): StyleDefinition[];
172
+ /**
173
+ * Build the LVGL `theme:` block — static defaults applied to all widgets of
174
+ * a given type. This is the fallback for raw `<lvgl-*>` elements that don't
175
+ * use `styles:` references.
176
+ *
177
+ * NOTE: The `theme:` block is NOT runtime-updatable. Only `style_definitions`
178
+ * (via `lvgl.style.update`) support runtime switching.
179
+ */
180
+ declare function themeToLvglTheme(theme: Theme): Record<string, unknown>;
181
+ /**
182
+ * Build the props to spread on the `<lvgl>` element.
183
+ *
184
+ * ```tsx
185
+ * <lvgl displays={[ref]} {...createLvglThemeProps(darkTheme)}>
186
+ * ```
187
+ */
188
+ declare function createLvglThemeProps(theme: Theme): {
189
+ styleDefinitions: StyleDefinition[];
190
+ theme: Record<string, unknown>;
191
+ };
192
+ /**
193
+ * Apply a theme at runtime inside a trigger function.
194
+ *
195
+ * The ESPCompose compiler recognises `applyTheme()` calls inside trigger
196
+ * bodies and spreads the resulting `lvgl.style.update` actions into the
197
+ * action list.
198
+ *
199
+ * @example
200
+ * ```tsx
201
+ * <Button
202
+ * text="Dark mode"
203
+ * onPress={() => { applyTheme(darkTheme); }}
204
+ * />
205
+ * ```
206
+ *
207
+ * @espcomposeAction applyTheme
208
+ */
209
+ declare function applyTheme(theme: Theme): Array<Record<string, unknown>>;
210
+
211
+ /**
212
+ * LVGL style definition ID constants.
213
+ *
214
+ * Every design-system component references these IDs via the LVGL `styles:`
215
+ * prop instead of inlining visual values. The IDs correspond 1-to-1 with
216
+ * entries generated by `themeToStyleDefinitions()` in `bridge.ts`.
217
+ *
218
+ * Naming: `ds_{category}[_{variant}]`
219
+ * NOTE: ESPHome requires underscores in IDs — dashes are not allowed.
220
+ */
221
+
222
+ declare const STYLE_BG = "ds_bg";
223
+ declare const STYLE_SURFACE = "ds_surface";
224
+ declare const STYLE_SURFACE_ALT = "ds_surface_alt";
225
+ declare const STYLE_BORDER = "ds_border";
226
+ declare const STYLE_TEXT_PRIMARY = "ds_text_primary";
227
+ declare const STYLE_TEXT_SECONDARY = "ds_text_secondary";
228
+ declare const STYLE_TEXT_DISABLED = "ds_text_disabled";
229
+ /** Typography variant style IDs. */
230
+ declare const STYLE_TEXT_VARIANT: Record<TextVariant, string>;
231
+ type ButtonVariant$1 = 'solid' | 'outline';
232
+ /** Style ID for a status button container. */
233
+ declare function statusStyleId(status: StatusToken, variant: ButtonVariant$1): string;
234
+ /** Style ID for a status button's inner label text. */
235
+ declare function statusTextStyleId(status: StatusToken, variant: ButtonVariant$1): string;
236
+ declare const STYLE_SLIDER_INDICATOR = "ds_slider_indicator";
237
+ declare const STYLE_SLIDER_KNOB = "ds_slider_knob";
238
+ declare const STYLE_SWITCH_INDICATOR = "ds_switch_indicator";
239
+ declare const STYLE_SWITCH_KNOB = "ds_switch_knob";
240
+ declare const STYLE_ARC_INDICATOR = "ds_arc_indicator";
241
+ declare const STYLE_ARC_KNOB = "ds_arc_knob";
242
+
133
243
  /**
134
244
  * Intent constants for @esphome/compose-ui design system.
135
245
  *
@@ -151,7 +261,7 @@ interface ScreenProps {
151
261
  padding?: SpacingToken | number;
152
262
  /** Skip this page in the page list. */
153
263
  skip?: boolean;
154
- /** Background color override (hex). If omitted, uses theme background. */
264
+ /** Background color override (hex). If set, overrides the style definition. */
155
265
  bgColor?: string;
156
266
  /** Border width in pixels. Default: 0. */
157
267
  borderWidth?: number;
@@ -161,7 +271,7 @@ interface ScreenProps {
161
271
  /**
162
272
  * Screen — a top-level LVGL page container.
163
273
  *
164
- * Applies the active theme's background color by default.
274
+ * Applies the active theme's background color by default via style reference.
165
275
  *
166
276
  * @example
167
277
  * <Screen padding="lg">
@@ -389,7 +499,7 @@ interface TextProps {
389
499
  text?: string;
390
500
  /** Text alignment within the label. */
391
501
  align?: 'LEFT' | 'CENTER' | 'RIGHT' | 'AUTO';
392
- /** Text color (hex). If omitted, uses theme textPrimary. */
502
+ /** Text color (hex). If omitted, inherits from the variant style definition. */
393
503
  color?: string;
394
504
  /** Long text mode. */
395
505
  longMode?: 'WRAP' | 'DOT' | 'SCROLL' | 'SCROLL_CIRCULAR' | 'CLIP';
@@ -427,8 +537,8 @@ interface ButtonProps {
427
537
  width?: number | string;
428
538
  /** Height override. */
429
539
  height?: number | string;
430
- /** Press handler (ESPHome action). */
431
- onPress?: unknown;
540
+ /** Press handler (ESPHome trigger function). */
541
+ onPress?: TriggerHandler;
432
542
  }
433
543
  /**
434
544
  * Button — a styled, clickable button with a label.
@@ -445,7 +555,7 @@ interface CardProps {
445
555
  padding?: SpacingToken | number;
446
556
  /** Corner radius. Default: 'md'. */
447
557
  radius?: RadiusToken | number;
448
- /** Background color (hex). Default: theme surfaceAlt. */
558
+ /** Background color override (hex). If set, overrides the style definition. */
449
559
  bgColor?: string;
450
560
  /** Border color (hex). */
451
561
  borderColor?: string;
@@ -475,7 +585,9 @@ interface SliderFieldProps {
475
585
  /** Bound value (sensor or entity reference). */
476
586
  value?: unknown;
477
587
  /** Change handler (ESPHome action). */
478
- onChange?: unknown;
588
+ onChange?: TriggerHandler<{
589
+ x: number;
590
+ }>;
479
591
  /** Minimum value. Default: 0. */
480
592
  min?: number;
481
593
  /** Maximum value. Default: 100. */
@@ -493,18 +605,15 @@ interface SliderFieldProps {
493
605
  */
494
606
  declare const SliderField: _esphome_compose.IntentComponent<SliderFieldProps, readonly ["lvgl:widget"], readonly [], undefined, undefined>;
495
607
 
496
- /**
497
- * SwitchField — a label + switch in a row layout.
498
- *
499
- * Compiles to a container with a label and a switch widget.
500
- */
501
608
  interface SwitchFieldProps {
502
609
  /** Label text displayed next to the switch. */
503
610
  label: string;
504
611
  /** Bound value (sensor or entity reference). */
505
612
  value?: unknown;
506
613
  /** Change handler (ESPHome action). */
507
- onChange?: unknown;
614
+ onChange?: TriggerHandler<{
615
+ x: boolean;
616
+ }>;
508
617
  /** Width of the field container. */
509
618
  width?: number | string;
510
619
  }
@@ -524,7 +633,9 @@ interface DropdownFieldProps {
524
633
  /** Bound selection index. */
525
634
  value?: unknown;
526
635
  /** Change handler (ESPHome action). */
527
- onChange?: unknown;
636
+ onChange?: TriggerHandler<{
637
+ x: number;
638
+ }>;
528
639
  /** Gap between label and dropdown. Default: 'xs'. */
529
640
  gap?: SpacingToken | number;
530
641
  /** Width of the field container. */
@@ -538,4 +649,4 @@ interface DropdownFieldProps {
538
649
  */
539
650
  declare const DropdownField: _esphome_compose.IntentComponent<DropdownFieldProps, readonly ["lvgl:widget"], readonly [], undefined, undefined>;
540
651
 
541
- export { Button, COMPOSE_UI_INTENTS, Card, Col, DropdownField, type FontDef, Grid, GridItem, HStack, type RadiusToken, Row, Screen, type SizeDimensions, type SizeToken, SliderField, Space, type SpacingToken, type StatusColors, type StatusToken, SwitchField, Text, type TextVariant, type Theme, type ThemeColors, ThemeContext, ThemeProvider, type ThemeTypography, type TrackSize, VStack, darkTheme, fontDefToLvgl, lightTheme, resolveRadius, resolveSize, resolveSpacing, resolveStatus, resolveTypography, themeFromJSON, themeToJSON, useTheme };
652
+ export { Button, COMPOSE_UI_INTENTS, Card, Col, DropdownField, type FontDef, Grid, GridItem, HStack, type PartColors, type RadiusToken, Row, STYLE_ARC_INDICATOR, STYLE_ARC_KNOB, STYLE_BG, STYLE_BORDER, STYLE_SLIDER_INDICATOR, STYLE_SLIDER_KNOB, STYLE_SURFACE, STYLE_SURFACE_ALT, STYLE_SWITCH_INDICATOR, STYLE_SWITCH_KNOB, STYLE_TEXT_DISABLED, STYLE_TEXT_PRIMARY, STYLE_TEXT_SECONDARY, STYLE_TEXT_VARIANT, Screen, type SizeDimensions, type SizeToken, SliderField, Space, type SpacingToken, type StatusColors, type StatusToken, type StyleDefinition, SwitchField, Text, type TextVariant, type Theme, type ThemeColors, ThemeContext, type ThemeParts, ThemeProvider, type ThemeTypography, type TrackSize, VStack, applyTheme, createLvglThemeProps, darkTheme, fontDefToLvgl, lightTheme, resolveRadius, resolveSize, resolveSpacing, resolveStatus, resolveTypography, statusStyleId, statusTextStyleId, themeFromJSON, themeToJSON, themeToLvglTheme, themeToStyleDefinitions, useTheme };
package/dist/index.js CHANGED
@@ -29,6 +29,20 @@ __export(index_exports, {
29
29
  GridItem: () => GridItem,
30
30
  HStack: () => HStack,
31
31
  Row: () => Row,
32
+ STYLE_ARC_INDICATOR: () => STYLE_ARC_INDICATOR,
33
+ STYLE_ARC_KNOB: () => STYLE_ARC_KNOB,
34
+ STYLE_BG: () => STYLE_BG,
35
+ STYLE_BORDER: () => STYLE_BORDER,
36
+ STYLE_SLIDER_INDICATOR: () => STYLE_SLIDER_INDICATOR,
37
+ STYLE_SLIDER_KNOB: () => STYLE_SLIDER_KNOB,
38
+ STYLE_SURFACE: () => STYLE_SURFACE,
39
+ STYLE_SURFACE_ALT: () => STYLE_SURFACE_ALT,
40
+ STYLE_SWITCH_INDICATOR: () => STYLE_SWITCH_INDICATOR,
41
+ STYLE_SWITCH_KNOB: () => STYLE_SWITCH_KNOB,
42
+ STYLE_TEXT_DISABLED: () => STYLE_TEXT_DISABLED,
43
+ STYLE_TEXT_PRIMARY: () => STYLE_TEXT_PRIMARY,
44
+ STYLE_TEXT_SECONDARY: () => STYLE_TEXT_SECONDARY,
45
+ STYLE_TEXT_VARIANT: () => STYLE_TEXT_VARIANT,
32
46
  Screen: () => Screen,
33
47
  SliderField: () => SliderField,
34
48
  Space: () => Space,
@@ -37,6 +51,8 @@ __export(index_exports, {
37
51
  ThemeContext: () => ThemeContext,
38
52
  ThemeProvider: () => ThemeProvider,
39
53
  VStack: () => VStack,
54
+ applyTheme: () => applyTheme,
55
+ createLvglThemeProps: () => createLvglThemeProps,
40
56
  darkTheme: () => darkTheme,
41
57
  fontDefToLvgl: () => fontDefToLvgl,
42
58
  lightTheme: () => lightTheme,
@@ -45,8 +61,12 @@ __export(index_exports, {
45
61
  resolveSpacing: () => resolveSpacing,
46
62
  resolveStatus: () => resolveStatus,
47
63
  resolveTypography: () => resolveTypography,
64
+ statusStyleId: () => statusStyleId,
65
+ statusTextStyleId: () => statusTextStyleId,
48
66
  themeFromJSON: () => themeFromJSON,
49
67
  themeToJSON: () => themeToJSON,
68
+ themeToLvglTheme: () => themeToLvglTheme,
69
+ themeToStyleDefinitions: () => themeToStyleDefinitions,
50
70
  useTheme: () => useTheme
51
71
  });
52
72
  module.exports = __toCommonJS(index_exports);
@@ -98,6 +118,11 @@ var darkTheme = {
98
118
  md: { height: 44, fontSize: 16, paddingX: 16, paddingY: 8 },
99
119
  lg: { height: 52, fontSize: 18, paddingX: 20, paddingY: 10 },
100
120
  xl: { height: 64, fontSize: 22, paddingX: 24, paddingY: 12 }
121
+ },
122
+ parts: {
123
+ slider: { bg: "#1E88E5", knob: "#E0E0E0" },
124
+ switch: { bg: "#1E88E5", knob: "#E0E0E0" },
125
+ arc: { bg: "#1E88E5", knob: "#E0E0E0" }
101
126
  }
102
127
  };
103
128
 
@@ -152,6 +177,11 @@ var lightTheme = {
152
177
  md: { height: 44, fontSize: 16, paddingX: 16, paddingY: 8 },
153
178
  lg: { height: 52, fontSize: 18, paddingX: 20, paddingY: 10 },
154
179
  xl: { height: 64, fontSize: 22, paddingX: 24, paddingY: 12 }
180
+ },
181
+ parts: {
182
+ slider: { bg: "#1565C0", knob: "#212121" },
183
+ switch: { bg: "#1565C0", knob: "#212121" },
184
+ arc: { bg: "#1565C0", knob: "#212121" }
155
185
  }
156
186
  };
157
187
 
@@ -185,6 +215,169 @@ function resolveRadius(value) {
185
215
  return useTheme().radii[value];
186
216
  }
187
217
 
218
+ // src/theme/style-ids.ts
219
+ var STYLE_BG = "ds_bg";
220
+ var STYLE_SURFACE = "ds_surface";
221
+ var STYLE_SURFACE_ALT = "ds_surface_alt";
222
+ var STYLE_BORDER = "ds_border";
223
+ var STYLE_TEXT_PRIMARY = "ds_text_primary";
224
+ var STYLE_TEXT_SECONDARY = "ds_text_secondary";
225
+ var STYLE_TEXT_DISABLED = "ds_text_disabled";
226
+ var STYLE_TEXT_VARIANT = {
227
+ title: "ds_text_title",
228
+ subtitle: "ds_text_subtitle",
229
+ body: "ds_text_body",
230
+ caption: "ds_text_caption"
231
+ };
232
+ function statusStyleId(status, variant) {
233
+ return `ds_status_${status}_${variant}`;
234
+ }
235
+ function statusTextStyleId(status, variant) {
236
+ return `ds_status_${status}_${variant}_text`;
237
+ }
238
+ var ALL_STATUSES = [
239
+ "primary",
240
+ "secondary",
241
+ "success",
242
+ "warning",
243
+ "danger"
244
+ ];
245
+ var STYLE_SLIDER_INDICATOR = "ds_slider_indicator";
246
+ var STYLE_SLIDER_KNOB = "ds_slider_knob";
247
+ var STYLE_SWITCH_INDICATOR = "ds_switch_indicator";
248
+ var STYLE_SWITCH_KNOB = "ds_switch_knob";
249
+ var STYLE_ARC_INDICATOR = "ds_arc_indicator";
250
+ var STYLE_ARC_KNOB = "ds_arc_knob";
251
+
252
+ // src/theme/bridge.ts
253
+ function resolveParts(theme) {
254
+ if (theme.parts) return theme.parts;
255
+ return {
256
+ slider: { bg: theme.colors.primary.bg, knob: theme.colors.textPrimary },
257
+ switch: { bg: theme.colors.primary.bg, knob: theme.colors.textPrimary },
258
+ arc: { bg: theme.colors.primary.bg, knob: theme.colors.textPrimary }
259
+ };
260
+ }
261
+ function themeToStyleDefinitions(theme) {
262
+ const defs = [];
263
+ const bodyFont = fontDefToLvgl(theme.typography.body);
264
+ defs.push({ id: STYLE_BG, bg_color: theme.colors.background });
265
+ defs.push({ id: STYLE_SURFACE, bg_color: theme.colors.surface });
266
+ defs.push({ id: STYLE_SURFACE_ALT, bg_color: theme.colors.surfaceAlt });
267
+ defs.push({ id: STYLE_BORDER, border_color: theme.colors.border });
268
+ defs.push({ id: STYLE_TEXT_PRIMARY, text_color: theme.colors.textPrimary, text_font: bodyFont });
269
+ defs.push({ id: STYLE_TEXT_SECONDARY, text_color: theme.colors.textSecondary, text_font: bodyFont });
270
+ defs.push({ id: STYLE_TEXT_DISABLED, text_color: theme.colors.textDisabled, text_font: bodyFont });
271
+ for (const variant of ["title", "subtitle", "body", "caption"]) {
272
+ const font = fontDefToLvgl(theme.typography[variant]);
273
+ defs.push({
274
+ id: STYLE_TEXT_VARIANT[variant],
275
+ text_color: theme.colors.textPrimary,
276
+ text_font: font
277
+ });
278
+ }
279
+ for (const status of ALL_STATUSES) {
280
+ const sc = theme.colors[status];
281
+ defs.push({
282
+ id: statusStyleId(status, "solid"),
283
+ bg_color: sc.bg
284
+ });
285
+ defs.push({
286
+ id: statusTextStyleId(status, "solid"),
287
+ text_color: sc.text
288
+ });
289
+ defs.push({
290
+ id: statusStyleId(status, "outline"),
291
+ bg_opa: "TRANSP",
292
+ border_color: sc.bg,
293
+ border_width: 2
294
+ });
295
+ defs.push({
296
+ id: statusTextStyleId(status, "outline"),
297
+ text_color: sc.bg
298
+ });
299
+ }
300
+ const parts = resolveParts(theme);
301
+ defs.push({ id: STYLE_SLIDER_INDICATOR, bg_color: parts.slider.bg });
302
+ defs.push({ id: STYLE_SLIDER_KNOB, bg_color: parts.slider.knob });
303
+ defs.push({ id: STYLE_SWITCH_INDICATOR, bg_color: parts.switch.bg });
304
+ defs.push({ id: STYLE_SWITCH_KNOB, bg_color: parts.switch.knob });
305
+ defs.push({ id: STYLE_ARC_INDICATOR, bg_color: parts.arc.bg });
306
+ defs.push({ id: STYLE_ARC_KNOB, bg_color: parts.arc.knob });
307
+ return defs;
308
+ }
309
+ function themeToLvglTheme(theme) {
310
+ const bodyFont = fontDefToLvgl(theme.typography.body);
311
+ const parts = resolveParts(theme);
312
+ return {
313
+ label: {
314
+ text_color: theme.colors.textPrimary,
315
+ text_font: bodyFont
316
+ },
317
+ button: {
318
+ bg_color: theme.colors.primary.bg,
319
+ border_width: 0,
320
+ pressed: {
321
+ bg_color: theme.colors.primary.bgPressed
322
+ }
323
+ },
324
+ obj: {
325
+ border_width: 0,
326
+ bg_opa: "TRANSP"
327
+ },
328
+ slider: {
329
+ bg_color: theme.colors.surfaceAlt,
330
+ indicator: {
331
+ bg_color: parts.slider.bg
332
+ },
333
+ knob: {
334
+ bg_color: parts.slider.knob
335
+ }
336
+ },
337
+ switch: {
338
+ bg_color: theme.colors.surfaceAlt,
339
+ indicator: {
340
+ bg_color: parts.switch.bg
341
+ },
342
+ knob: {
343
+ bg_color: parts.switch.knob
344
+ }
345
+ },
346
+ arc: {
347
+ arc_color: theme.colors.surfaceAlt,
348
+ indicator: {
349
+ arc_color: parts.arc.bg
350
+ },
351
+ knob: {
352
+ bg_color: parts.arc.knob
353
+ }
354
+ },
355
+ dropdown: {
356
+ text_color: theme.colors.textPrimary,
357
+ text_font: bodyFont,
358
+ bg_color: theme.colors.surface,
359
+ border_color: theme.colors.border,
360
+ border_width: 1
361
+ }
362
+ };
363
+ }
364
+ function createLvglThemeProps(theme) {
365
+ return {
366
+ styleDefinitions: themeToStyleDefinitions(theme),
367
+ theme: themeToLvglTheme(theme)
368
+ };
369
+ }
370
+ function createThemeSwitchActions(theme) {
371
+ const defs = themeToStyleDefinitions(theme);
372
+ return defs.map((def) => {
373
+ const { id, ...props } = def;
374
+ return { "lvgl.style.update": { id, ...props } };
375
+ });
376
+ }
377
+ function applyTheme(theme) {
378
+ return createThemeSwitchActions(theme);
379
+ }
380
+
188
381
  // src/intents.ts
189
382
  var COMPOSE_UI_INTENTS = {
190
383
  /** Col can only be placed inside Row. */
@@ -198,16 +391,16 @@ var import_compose2 = require("@esphome/compose");
198
391
  var Screen = (0, import_compose2.createIntentComponent)(
199
392
  (props) => {
200
393
  const padding = props.padding != null ? resolveSpacing(props.padding) : void 0;
201
- const theme = useTheme();
202
- const bgColor = props.bgColor ?? theme.colors.background;
203
394
  return {
204
395
  type: "lvgl-page",
205
396
  props: {
206
- bgColor,
397
+ styles: STYLE_BG,
398
+ ...props.bgColor != null ? { bgColor: props.bgColor } : {},
207
399
  borderWidth: props.borderWidth ?? 0,
208
400
  ...props.borderColor != null ? { borderColor: props.borderColor } : {},
209
401
  ...padding != null ? { padAll: padding } : {},
210
402
  ...props.skip != null ? { skip: props.skip } : {},
403
+ "x:custom": { scrollbar_mode: "OFF" },
211
404
  ...props.children ? { children: Array.isArray(props.children) ? props.children : [props.children] } : {}
212
405
  }
213
406
  };
@@ -239,8 +432,8 @@ function buildSpaceElement(props) {
239
432
  return {
240
433
  type: "lvgl-obj",
241
434
  props: {
242
- ...props.width != null ? { width: props.width } : {},
243
- ...props.height != null ? { height: props.height } : {},
435
+ width: props.width ?? "100%",
436
+ height: props.height ?? "SIZE_CONTENT",
244
437
  ...padding != null ? { padAll: padding } : {},
245
438
  ...props.bgColor != null ? { bgColor: props.bgColor } : {},
246
439
  ...props.bgOpa != null ? { bgOpa: props.bgOpa } : { bgOpa: "TRANSP" },
@@ -248,6 +441,7 @@ function buildSpaceElement(props) {
248
441
  borderWidth: props.borderWidth ?? 0,
249
442
  ...props.borderColor != null ? { borderColor: props.borderColor } : {},
250
443
  "x:custom": {
444
+ scrollbar_mode: "OFF",
251
445
  layout: buildFlexLayout(flow, gapKey, props)
252
446
  },
253
447
  ...props.children ? { children: Array.isArray(props.children) ? props.children : [props.children] } : {}
@@ -407,15 +601,13 @@ var import_compose6 = require("@esphome/compose");
407
601
  var Text = (0, import_compose6.createIntentComponent)(
408
602
  (props) => {
409
603
  const variant = props.variant ?? "body";
410
- const fontDef = resolveTypography(variant);
411
- const color = props.color ?? useTheme().colors.textPrimary;
412
604
  return {
413
605
  type: "lvgl-label",
414
606
  props: {
607
+ styles: STYLE_TEXT_VARIANT[variant],
415
608
  ...props.text != null ? { text: props.text } : {},
416
- textFont: fontDefToLvgl(fontDef),
417
609
  ...props.align != null ? { textAlign: props.align } : {},
418
- textColor: color,
610
+ ...props.color != null ? { textColor: props.color } : {},
419
611
  ...props.longMode != null ? { longMode: props.longMode } : {},
420
612
  ...props.x != null ? { x: props.x } : {},
421
613
  ...props.y != null ? { y: props.y } : {},
@@ -436,32 +628,25 @@ var Button = (0, import_compose7.createIntentComponent)(
436
628
  const status = props.status ?? "primary";
437
629
  const size = props.size ?? "md";
438
630
  const variant = props.variant ?? "solid";
439
- const colors = resolveStatus(status);
440
631
  const dims = resolveSize(size);
441
632
  const theme = useTheme();
442
- const isSolid = variant === "solid";
633
+ const sc = resolveStatus(status);
443
634
  const textFont = fontDefToLvgl({ fontFamily: theme.typography.body.fontFamily, fontSize: dims.fontSize });
635
+ const pressed = variant === "solid" ? { bgColor: sc.bgPressed } : { bgColor: sc.bg, bgOpa: "COVER" };
444
636
  const buttonProps = {
637
+ styles: statusStyleId(status, variant),
445
638
  width: props.width ?? dims.paddingX * 2 + 80,
446
639
  height: props.height ?? dims.height,
640
+ pressed,
447
641
  ...props.x != null ? { x: props.x } : {},
448
642
  ...props.y != null ? { y: props.y } : {},
449
- ...isSolid ? {
450
- bgColor: colors.bg,
451
- pressed: { bgColor: colors.bgPressed }
452
- } : {
453
- bgOpa: "TRANSP",
454
- borderColor: colors.bg,
455
- borderWidth: 2,
456
- pressed: { bgColor: colors.bg, bgOpa: "COVER" }
457
- },
458
643
  ...props.onPress != null ? { "x:custom": { on_press: props.onPress } } : {}
459
644
  };
460
645
  const label = {
461
646
  type: "lvgl-label",
462
647
  props: {
648
+ styles: statusTextStyleId(status, variant),
463
649
  text: props.text ?? "",
464
- textColor: isSolid ? colors.text : colors.bg,
465
650
  textFont,
466
651
  align: "CENTER"
467
652
  }
@@ -486,19 +671,20 @@ var Card = (0, import_compose8.createIntentComponent)(
486
671
  (props) => {
487
672
  const padding = resolveSpacing(props.padding ?? "md");
488
673
  const radius = resolveRadius(props.radius ?? "md");
489
- const theme = useTheme();
490
674
  const gap = props.gap != null ? resolveSpacing(props.gap) : void 0;
491
675
  return {
492
676
  type: "lvgl-obj",
493
677
  props: {
678
+ styles: STYLE_SURFACE_ALT,
494
679
  padAll: padding,
495
680
  radius,
496
- bgColor: props.bgColor ?? theme.colors.surfaceAlt,
681
+ ...props.bgColor != null ? { bgColor: props.bgColor } : {},
497
682
  ...props.borderColor != null ? { borderColor: props.borderColor } : {},
498
683
  borderWidth: props.borderWidth ?? 0,
499
- ...props.width != null ? { width: props.width } : {},
500
- ...props.height != null ? { height: props.height } : {},
684
+ width: props.width ?? "100%",
685
+ height: props.height ?? "SIZE_CONTENT",
501
686
  "x:custom": {
687
+ scrollbar_mode: "OFF",
502
688
  layout: {
503
689
  type: "flex",
504
690
  flex_flow: "COLUMN",
@@ -520,14 +706,12 @@ var Card = (0, import_compose8.createIntentComponent)(
520
706
  var import_compose9 = require("@esphome/compose");
521
707
  var SliderField = (0, import_compose9.createIntentComponent)(
522
708
  (props) => {
523
- const theme = useTheme();
524
709
  const gap = props.gap != null ? resolveSpacing(props.gap) : void 0;
525
710
  const label = {
526
711
  type: "lvgl-label",
527
712
  props: {
528
- text: props.label,
529
- textFont: fontDefToLvgl(theme.typography.body),
530
- textColor: theme.colors.textPrimary
713
+ styles: STYLE_TEXT_PRIMARY,
714
+ text: props.label
531
715
  }
532
716
  };
533
717
  const sliderProps = {
@@ -567,13 +751,11 @@ var SliderField = (0, import_compose9.createIntentComponent)(
567
751
  var import_compose10 = require("@esphome/compose");
568
752
  var SwitchField = (0, import_compose10.createIntentComponent)(
569
753
  (props) => {
570
- const theme = useTheme();
571
754
  const label = {
572
755
  type: "lvgl-label",
573
756
  props: {
574
- text: props.label,
575
- textFont: fontDefToLvgl(theme.typography.body),
576
- textColor: theme.colors.textPrimary
757
+ styles: STYLE_TEXT_PRIMARY,
758
+ text: props.label
577
759
  }
578
760
  };
579
761
  const switchProps = {
@@ -612,14 +794,12 @@ var SwitchField = (0, import_compose10.createIntentComponent)(
612
794
  var import_compose11 = require("@esphome/compose");
613
795
  var DropdownField = (0, import_compose11.createIntentComponent)(
614
796
  (props) => {
615
- const theme = useTheme();
616
797
  const gap = props.gap != null ? resolveSpacing(props.gap) : void 0;
617
798
  const label = {
618
799
  type: "lvgl-label",
619
800
  props: {
620
- text: props.label,
621
- textFont: fontDefToLvgl(theme.typography.body),
622
- textColor: theme.colors.textPrimary
801
+ styles: STYLE_TEXT_PRIMARY,
802
+ text: props.label
623
803
  }
624
804
  };
625
805
  const dropdownProps = {
@@ -664,6 +844,20 @@ var DropdownField = (0, import_compose11.createIntentComponent)(
664
844
  GridItem,
665
845
  HStack,
666
846
  Row,
847
+ STYLE_ARC_INDICATOR,
848
+ STYLE_ARC_KNOB,
849
+ STYLE_BG,
850
+ STYLE_BORDER,
851
+ STYLE_SLIDER_INDICATOR,
852
+ STYLE_SLIDER_KNOB,
853
+ STYLE_SURFACE,
854
+ STYLE_SURFACE_ALT,
855
+ STYLE_SWITCH_INDICATOR,
856
+ STYLE_SWITCH_KNOB,
857
+ STYLE_TEXT_DISABLED,
858
+ STYLE_TEXT_PRIMARY,
859
+ STYLE_TEXT_SECONDARY,
860
+ STYLE_TEXT_VARIANT,
667
861
  Screen,
668
862
  SliderField,
669
863
  Space,
@@ -672,6 +866,8 @@ var DropdownField = (0, import_compose11.createIntentComponent)(
672
866
  ThemeContext,
673
867
  ThemeProvider,
674
868
  VStack,
869
+ applyTheme,
870
+ createLvglThemeProps,
675
871
  darkTheme,
676
872
  fontDefToLvgl,
677
873
  lightTheme,
@@ -680,7 +876,11 @@ var DropdownField = (0, import_compose11.createIntentComponent)(
680
876
  resolveSpacing,
681
877
  resolveStatus,
682
878
  resolveTypography,
879
+ statusStyleId,
880
+ statusTextStyleId,
683
881
  themeFromJSON,
684
882
  themeToJSON,
883
+ themeToLvglTheme,
884
+ themeToStyleDefinitions,
685
885
  useTheme
686
886
  });
package/dist/index.mjs CHANGED
@@ -45,6 +45,11 @@ var darkTheme = {
45
45
  md: { height: 44, fontSize: 16, paddingX: 16, paddingY: 8 },
46
46
  lg: { height: 52, fontSize: 18, paddingX: 20, paddingY: 10 },
47
47
  xl: { height: 64, fontSize: 22, paddingX: 24, paddingY: 12 }
48
+ },
49
+ parts: {
50
+ slider: { bg: "#1E88E5", knob: "#E0E0E0" },
51
+ switch: { bg: "#1E88E5", knob: "#E0E0E0" },
52
+ arc: { bg: "#1E88E5", knob: "#E0E0E0" }
48
53
  }
49
54
  };
50
55
 
@@ -99,6 +104,11 @@ var lightTheme = {
99
104
  md: { height: 44, fontSize: 16, paddingX: 16, paddingY: 8 },
100
105
  lg: { height: 52, fontSize: 18, paddingX: 20, paddingY: 10 },
101
106
  xl: { height: 64, fontSize: 22, paddingX: 24, paddingY: 12 }
107
+ },
108
+ parts: {
109
+ slider: { bg: "#1565C0", knob: "#212121" },
110
+ switch: { bg: "#1565C0", knob: "#212121" },
111
+ arc: { bg: "#1565C0", knob: "#212121" }
102
112
  }
103
113
  };
104
114
 
@@ -132,6 +142,169 @@ function resolveRadius(value) {
132
142
  return useTheme().radii[value];
133
143
  }
134
144
 
145
+ // src/theme/style-ids.ts
146
+ var STYLE_BG = "ds_bg";
147
+ var STYLE_SURFACE = "ds_surface";
148
+ var STYLE_SURFACE_ALT = "ds_surface_alt";
149
+ var STYLE_BORDER = "ds_border";
150
+ var STYLE_TEXT_PRIMARY = "ds_text_primary";
151
+ var STYLE_TEXT_SECONDARY = "ds_text_secondary";
152
+ var STYLE_TEXT_DISABLED = "ds_text_disabled";
153
+ var STYLE_TEXT_VARIANT = {
154
+ title: "ds_text_title",
155
+ subtitle: "ds_text_subtitle",
156
+ body: "ds_text_body",
157
+ caption: "ds_text_caption"
158
+ };
159
+ function statusStyleId(status, variant) {
160
+ return `ds_status_${status}_${variant}`;
161
+ }
162
+ function statusTextStyleId(status, variant) {
163
+ return `ds_status_${status}_${variant}_text`;
164
+ }
165
+ var ALL_STATUSES = [
166
+ "primary",
167
+ "secondary",
168
+ "success",
169
+ "warning",
170
+ "danger"
171
+ ];
172
+ var STYLE_SLIDER_INDICATOR = "ds_slider_indicator";
173
+ var STYLE_SLIDER_KNOB = "ds_slider_knob";
174
+ var STYLE_SWITCH_INDICATOR = "ds_switch_indicator";
175
+ var STYLE_SWITCH_KNOB = "ds_switch_knob";
176
+ var STYLE_ARC_INDICATOR = "ds_arc_indicator";
177
+ var STYLE_ARC_KNOB = "ds_arc_knob";
178
+
179
+ // src/theme/bridge.ts
180
+ function resolveParts(theme) {
181
+ if (theme.parts) return theme.parts;
182
+ return {
183
+ slider: { bg: theme.colors.primary.bg, knob: theme.colors.textPrimary },
184
+ switch: { bg: theme.colors.primary.bg, knob: theme.colors.textPrimary },
185
+ arc: { bg: theme.colors.primary.bg, knob: theme.colors.textPrimary }
186
+ };
187
+ }
188
+ function themeToStyleDefinitions(theme) {
189
+ const defs = [];
190
+ const bodyFont = fontDefToLvgl(theme.typography.body);
191
+ defs.push({ id: STYLE_BG, bg_color: theme.colors.background });
192
+ defs.push({ id: STYLE_SURFACE, bg_color: theme.colors.surface });
193
+ defs.push({ id: STYLE_SURFACE_ALT, bg_color: theme.colors.surfaceAlt });
194
+ defs.push({ id: STYLE_BORDER, border_color: theme.colors.border });
195
+ defs.push({ id: STYLE_TEXT_PRIMARY, text_color: theme.colors.textPrimary, text_font: bodyFont });
196
+ defs.push({ id: STYLE_TEXT_SECONDARY, text_color: theme.colors.textSecondary, text_font: bodyFont });
197
+ defs.push({ id: STYLE_TEXT_DISABLED, text_color: theme.colors.textDisabled, text_font: bodyFont });
198
+ for (const variant of ["title", "subtitle", "body", "caption"]) {
199
+ const font = fontDefToLvgl(theme.typography[variant]);
200
+ defs.push({
201
+ id: STYLE_TEXT_VARIANT[variant],
202
+ text_color: theme.colors.textPrimary,
203
+ text_font: font
204
+ });
205
+ }
206
+ for (const status of ALL_STATUSES) {
207
+ const sc = theme.colors[status];
208
+ defs.push({
209
+ id: statusStyleId(status, "solid"),
210
+ bg_color: sc.bg
211
+ });
212
+ defs.push({
213
+ id: statusTextStyleId(status, "solid"),
214
+ text_color: sc.text
215
+ });
216
+ defs.push({
217
+ id: statusStyleId(status, "outline"),
218
+ bg_opa: "TRANSP",
219
+ border_color: sc.bg,
220
+ border_width: 2
221
+ });
222
+ defs.push({
223
+ id: statusTextStyleId(status, "outline"),
224
+ text_color: sc.bg
225
+ });
226
+ }
227
+ const parts = resolveParts(theme);
228
+ defs.push({ id: STYLE_SLIDER_INDICATOR, bg_color: parts.slider.bg });
229
+ defs.push({ id: STYLE_SLIDER_KNOB, bg_color: parts.slider.knob });
230
+ defs.push({ id: STYLE_SWITCH_INDICATOR, bg_color: parts.switch.bg });
231
+ defs.push({ id: STYLE_SWITCH_KNOB, bg_color: parts.switch.knob });
232
+ defs.push({ id: STYLE_ARC_INDICATOR, bg_color: parts.arc.bg });
233
+ defs.push({ id: STYLE_ARC_KNOB, bg_color: parts.arc.knob });
234
+ return defs;
235
+ }
236
+ function themeToLvglTheme(theme) {
237
+ const bodyFont = fontDefToLvgl(theme.typography.body);
238
+ const parts = resolveParts(theme);
239
+ return {
240
+ label: {
241
+ text_color: theme.colors.textPrimary,
242
+ text_font: bodyFont
243
+ },
244
+ button: {
245
+ bg_color: theme.colors.primary.bg,
246
+ border_width: 0,
247
+ pressed: {
248
+ bg_color: theme.colors.primary.bgPressed
249
+ }
250
+ },
251
+ obj: {
252
+ border_width: 0,
253
+ bg_opa: "TRANSP"
254
+ },
255
+ slider: {
256
+ bg_color: theme.colors.surfaceAlt,
257
+ indicator: {
258
+ bg_color: parts.slider.bg
259
+ },
260
+ knob: {
261
+ bg_color: parts.slider.knob
262
+ }
263
+ },
264
+ switch: {
265
+ bg_color: theme.colors.surfaceAlt,
266
+ indicator: {
267
+ bg_color: parts.switch.bg
268
+ },
269
+ knob: {
270
+ bg_color: parts.switch.knob
271
+ }
272
+ },
273
+ arc: {
274
+ arc_color: theme.colors.surfaceAlt,
275
+ indicator: {
276
+ arc_color: parts.arc.bg
277
+ },
278
+ knob: {
279
+ bg_color: parts.arc.knob
280
+ }
281
+ },
282
+ dropdown: {
283
+ text_color: theme.colors.textPrimary,
284
+ text_font: bodyFont,
285
+ bg_color: theme.colors.surface,
286
+ border_color: theme.colors.border,
287
+ border_width: 1
288
+ }
289
+ };
290
+ }
291
+ function createLvglThemeProps(theme) {
292
+ return {
293
+ styleDefinitions: themeToStyleDefinitions(theme),
294
+ theme: themeToLvglTheme(theme)
295
+ };
296
+ }
297
+ function createThemeSwitchActions(theme) {
298
+ const defs = themeToStyleDefinitions(theme);
299
+ return defs.map((def) => {
300
+ const { id, ...props } = def;
301
+ return { "lvgl.style.update": { id, ...props } };
302
+ });
303
+ }
304
+ function applyTheme(theme) {
305
+ return createThemeSwitchActions(theme);
306
+ }
307
+
135
308
  // src/intents.ts
136
309
  var COMPOSE_UI_INTENTS = {
137
310
  /** Col can only be placed inside Row. */
@@ -145,16 +318,16 @@ import { createIntentComponent, LVGL_INTENTS } from "@esphome/compose";
145
318
  var Screen = createIntentComponent(
146
319
  (props) => {
147
320
  const padding = props.padding != null ? resolveSpacing(props.padding) : void 0;
148
- const theme = useTheme();
149
- const bgColor = props.bgColor ?? theme.colors.background;
150
321
  return {
151
322
  type: "lvgl-page",
152
323
  props: {
153
- bgColor,
324
+ styles: STYLE_BG,
325
+ ...props.bgColor != null ? { bgColor: props.bgColor } : {},
154
326
  borderWidth: props.borderWidth ?? 0,
155
327
  ...props.borderColor != null ? { borderColor: props.borderColor } : {},
156
328
  ...padding != null ? { padAll: padding } : {},
157
329
  ...props.skip != null ? { skip: props.skip } : {},
330
+ "x:custom": { scrollbar_mode: "OFF" },
158
331
  ...props.children ? { children: Array.isArray(props.children) ? props.children : [props.children] } : {}
159
332
  }
160
333
  };
@@ -186,8 +359,8 @@ function buildSpaceElement(props) {
186
359
  return {
187
360
  type: "lvgl-obj",
188
361
  props: {
189
- ...props.width != null ? { width: props.width } : {},
190
- ...props.height != null ? { height: props.height } : {},
362
+ width: props.width ?? "100%",
363
+ height: props.height ?? "SIZE_CONTENT",
191
364
  ...padding != null ? { padAll: padding } : {},
192
365
  ...props.bgColor != null ? { bgColor: props.bgColor } : {},
193
366
  ...props.bgOpa != null ? { bgOpa: props.bgOpa } : { bgOpa: "TRANSP" },
@@ -195,6 +368,7 @@ function buildSpaceElement(props) {
195
368
  borderWidth: props.borderWidth ?? 0,
196
369
  ...props.borderColor != null ? { borderColor: props.borderColor } : {},
197
370
  "x:custom": {
371
+ scrollbar_mode: "OFF",
198
372
  layout: buildFlexLayout(flow, gapKey, props)
199
373
  },
200
374
  ...props.children ? { children: Array.isArray(props.children) ? props.children : [props.children] } : {}
@@ -354,15 +528,13 @@ import { createIntentComponent as createIntentComponent5, LVGL_INTENTS as LVGL_I
354
528
  var Text = createIntentComponent5(
355
529
  (props) => {
356
530
  const variant = props.variant ?? "body";
357
- const fontDef = resolveTypography(variant);
358
- const color = props.color ?? useTheme().colors.textPrimary;
359
531
  return {
360
532
  type: "lvgl-label",
361
533
  props: {
534
+ styles: STYLE_TEXT_VARIANT[variant],
362
535
  ...props.text != null ? { text: props.text } : {},
363
- textFont: fontDefToLvgl(fontDef),
364
536
  ...props.align != null ? { textAlign: props.align } : {},
365
- textColor: color,
537
+ ...props.color != null ? { textColor: props.color } : {},
366
538
  ...props.longMode != null ? { longMode: props.longMode } : {},
367
539
  ...props.x != null ? { x: props.x } : {},
368
540
  ...props.y != null ? { y: props.y } : {},
@@ -383,32 +555,25 @@ var Button = createIntentComponent6(
383
555
  const status = props.status ?? "primary";
384
556
  const size = props.size ?? "md";
385
557
  const variant = props.variant ?? "solid";
386
- const colors = resolveStatus(status);
387
558
  const dims = resolveSize(size);
388
559
  const theme = useTheme();
389
- const isSolid = variant === "solid";
560
+ const sc = resolveStatus(status);
390
561
  const textFont = fontDefToLvgl({ fontFamily: theme.typography.body.fontFamily, fontSize: dims.fontSize });
562
+ const pressed = variant === "solid" ? { bgColor: sc.bgPressed } : { bgColor: sc.bg, bgOpa: "COVER" };
391
563
  const buttonProps = {
564
+ styles: statusStyleId(status, variant),
392
565
  width: props.width ?? dims.paddingX * 2 + 80,
393
566
  height: props.height ?? dims.height,
567
+ pressed,
394
568
  ...props.x != null ? { x: props.x } : {},
395
569
  ...props.y != null ? { y: props.y } : {},
396
- ...isSolid ? {
397
- bgColor: colors.bg,
398
- pressed: { bgColor: colors.bgPressed }
399
- } : {
400
- bgOpa: "TRANSP",
401
- borderColor: colors.bg,
402
- borderWidth: 2,
403
- pressed: { bgColor: colors.bg, bgOpa: "COVER" }
404
- },
405
570
  ...props.onPress != null ? { "x:custom": { on_press: props.onPress } } : {}
406
571
  };
407
572
  const label = {
408
573
  type: "lvgl-label",
409
574
  props: {
575
+ styles: statusTextStyleId(status, variant),
410
576
  text: props.text ?? "",
411
- textColor: isSolid ? colors.text : colors.bg,
412
577
  textFont,
413
578
  align: "CENTER"
414
579
  }
@@ -433,19 +598,20 @@ var Card = createIntentComponent7(
433
598
  (props) => {
434
599
  const padding = resolveSpacing(props.padding ?? "md");
435
600
  const radius = resolveRadius(props.radius ?? "md");
436
- const theme = useTheme();
437
601
  const gap = props.gap != null ? resolveSpacing(props.gap) : void 0;
438
602
  return {
439
603
  type: "lvgl-obj",
440
604
  props: {
605
+ styles: STYLE_SURFACE_ALT,
441
606
  padAll: padding,
442
607
  radius,
443
- bgColor: props.bgColor ?? theme.colors.surfaceAlt,
608
+ ...props.bgColor != null ? { bgColor: props.bgColor } : {},
444
609
  ...props.borderColor != null ? { borderColor: props.borderColor } : {},
445
610
  borderWidth: props.borderWidth ?? 0,
446
- ...props.width != null ? { width: props.width } : {},
447
- ...props.height != null ? { height: props.height } : {},
611
+ width: props.width ?? "100%",
612
+ height: props.height ?? "SIZE_CONTENT",
448
613
  "x:custom": {
614
+ scrollbar_mode: "OFF",
449
615
  layout: {
450
616
  type: "flex",
451
617
  flex_flow: "COLUMN",
@@ -467,14 +633,12 @@ var Card = createIntentComponent7(
467
633
  import { createIntentComponent as createIntentComponent8, LVGL_INTENTS as LVGL_INTENTS8 } from "@esphome/compose";
468
634
  var SliderField = createIntentComponent8(
469
635
  (props) => {
470
- const theme = useTheme();
471
636
  const gap = props.gap != null ? resolveSpacing(props.gap) : void 0;
472
637
  const label = {
473
638
  type: "lvgl-label",
474
639
  props: {
475
- text: props.label,
476
- textFont: fontDefToLvgl(theme.typography.body),
477
- textColor: theme.colors.textPrimary
640
+ styles: STYLE_TEXT_PRIMARY,
641
+ text: props.label
478
642
  }
479
643
  };
480
644
  const sliderProps = {
@@ -514,13 +678,11 @@ var SliderField = createIntentComponent8(
514
678
  import { createIntentComponent as createIntentComponent9, LVGL_INTENTS as LVGL_INTENTS9 } from "@esphome/compose";
515
679
  var SwitchField = createIntentComponent9(
516
680
  (props) => {
517
- const theme = useTheme();
518
681
  const label = {
519
682
  type: "lvgl-label",
520
683
  props: {
521
- text: props.label,
522
- textFont: fontDefToLvgl(theme.typography.body),
523
- textColor: theme.colors.textPrimary
684
+ styles: STYLE_TEXT_PRIMARY,
685
+ text: props.label
524
686
  }
525
687
  };
526
688
  const switchProps = {
@@ -559,14 +721,12 @@ var SwitchField = createIntentComponent9(
559
721
  import { createIntentComponent as createIntentComponent10, LVGL_INTENTS as LVGL_INTENTS10 } from "@esphome/compose";
560
722
  var DropdownField = createIntentComponent10(
561
723
  (props) => {
562
- const theme = useTheme();
563
724
  const gap = props.gap != null ? resolveSpacing(props.gap) : void 0;
564
725
  const label = {
565
726
  type: "lvgl-label",
566
727
  props: {
567
- text: props.label,
568
- textFont: fontDefToLvgl(theme.typography.body),
569
- textColor: theme.colors.textPrimary
728
+ styles: STYLE_TEXT_PRIMARY,
729
+ text: props.label
570
730
  }
571
731
  };
572
732
  const dropdownProps = {
@@ -610,6 +770,20 @@ export {
610
770
  GridItem,
611
771
  HStack,
612
772
  Row,
773
+ STYLE_ARC_INDICATOR,
774
+ STYLE_ARC_KNOB,
775
+ STYLE_BG,
776
+ STYLE_BORDER,
777
+ STYLE_SLIDER_INDICATOR,
778
+ STYLE_SLIDER_KNOB,
779
+ STYLE_SURFACE,
780
+ STYLE_SURFACE_ALT,
781
+ STYLE_SWITCH_INDICATOR,
782
+ STYLE_SWITCH_KNOB,
783
+ STYLE_TEXT_DISABLED,
784
+ STYLE_TEXT_PRIMARY,
785
+ STYLE_TEXT_SECONDARY,
786
+ STYLE_TEXT_VARIANT,
613
787
  Screen,
614
788
  SliderField,
615
789
  Space,
@@ -618,6 +792,8 @@ export {
618
792
  ThemeContext,
619
793
  ThemeProvider,
620
794
  VStack,
795
+ applyTheme,
796
+ createLvglThemeProps,
621
797
  darkTheme,
622
798
  fontDefToLvgl,
623
799
  lightTheme,
@@ -626,7 +802,11 @@ export {
626
802
  resolveSpacing,
627
803
  resolveStatus,
628
804
  resolveTypography,
805
+ statusStyleId,
806
+ statusTextStyleId,
629
807
  themeFromJSON,
630
808
  themeToJSON,
809
+ themeToLvglTheme,
810
+ themeToStyleDefinitions,
631
811
  useTheme
632
812
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@esphome/compose-ui",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "LVGL Design System components for ESPHome Compose",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -35,13 +35,13 @@
35
35
  "access": "public"
36
36
  },
37
37
  "dependencies": {
38
- "@esphome/compose": "0.2.0"
38
+ "@esphome/compose": "0.3.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/node": "^22.0.0",
42
42
  "tsup": "^8.0.0",
43
- "tsx": "^4.0.0",
44
- "typescript": "^5.4.0",
43
+ "tsx": "^4.21.0",
44
+ "typescript": "^5.9.3",
45
45
  "rimraf": "^6.1.3",
46
46
  "vitest": "^2.0.0"
47
47
  },