@danielito1996/compose-svelted 0.0.1

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.
Files changed (86) hide show
  1. package/.vscode/extensions.json +3 -0
  2. package/README.md +274 -0
  3. package/docs/assets/components/button/button.png +0 -0
  4. package/docs/assets/components/surface/surface_simple.png +0 -0
  5. package/docs/assets/components/text/text.png +0 -0
  6. package/docs/assets/components/textfield/textfield_simple.png +0 -0
  7. package/docs/assets/svelted.png +0 -0
  8. package/docs/assets/svelted.svg +41 -0
  9. package/docs/getting_started.md +116 -0
  10. package/docs/index.md +106 -0
  11. package/index.html +14 -0
  12. package/package.json +49 -0
  13. package/public/vite.svg +1 -0
  14. package/screenshots/Captura de pantalla 2025-12-20 022710.png +0 -0
  15. package/screenshots/capturas.txt +1 -0
  16. package/src/App.svelte +39 -0
  17. package/src/app.css +23 -0
  18. package/src/assets/img/hav3m.png +0 -0
  19. package/src/assets/img/vessel.jpg +0 -0
  20. package/src/assets/raw/boat.svg +15 -0
  21. package/src/assets/raw/cash.svg +39 -0
  22. package/src/assets/raw/police.json +1 -0
  23. package/src/assets/raw/svelte.svg +1 -0
  24. package/src/lib/Counter.svelte +10 -0
  25. package/src/lib/components/AppRoot.svelte +15 -0
  26. package/src/lib/components/ContentScale.ts +12 -0
  27. package/src/lib/components/Icon.svelte +47 -0
  28. package/src/lib/components/Image.svelte +31 -0
  29. package/src/lib/components/Spacer.svelte +11 -0
  30. package/src/lib/components/Surface.svelte +19 -0
  31. package/src/lib/components/Text.svelte +23 -0
  32. package/src/lib/components/TonalButton.svelte +34 -0
  33. package/src/lib/components/buttons/Button.svelte +34 -0
  34. package/src/lib/components/buttons/ButtonWithIcon.svelte +0 -0
  35. package/src/lib/components/buttons/IconButton.svelte +0 -0
  36. package/src/lib/components/buttons/OutlinedButton.svelte +0 -0
  37. package/src/lib/components/buttons/OutlinedButtonWithIcon.svelte +0 -0
  38. package/src/lib/components/buttons/OutlinedIconButton.svelte +0 -0
  39. package/src/lib/components/buttons/TextButton.svelte +0 -0
  40. package/src/lib/components/cards/Card.svelte +0 -0
  41. package/src/lib/components/cards/OutlinedCard.svelte +0 -0
  42. package/src/lib/components/layouts/Alignment.ts +37 -0
  43. package/src/lib/components/layouts/Arrangement.ts +66 -0
  44. package/src/lib/components/layouts/Box.svelte +25 -0
  45. package/src/lib/components/layouts/Column.svelte +23 -0
  46. package/src/lib/components/layouts/LazyColumn.svelte +0 -0
  47. package/src/lib/components/layouts/LazyRow.svelte +0 -0
  48. package/src/lib/components/layouts/Row.svelte +23 -0
  49. package/src/lib/components/layouts/Scafold.svelte +0 -0
  50. package/src/lib/components/menus/DropdownMenu.svelte +0 -0
  51. package/src/lib/components/menus/DropdownMenuItem.svelte +0 -0
  52. package/src/lib/components/textFields/BaseTextField.svelte +130 -0
  53. package/src/lib/components/textFields/OutlinedTextField.svelte +52 -0
  54. package/src/lib/components/textFields/TextField.svelte +36 -0
  55. package/src/lib/components/textFields/TextFieldColors.ts +11 -0
  56. package/src/lib/components/textFields/TextFieldDefaults.ts +48 -0
  57. package/src/lib/core/helpers/painterResource.ts +26 -0
  58. package/src/lib/core/modifier/Modifier.ts +259 -0
  59. package/src/lib/core/modifier/ModifierImpl.ts +275 -0
  60. package/src/lib/core/shapes/RoundedCornerShape.ts +53 -0
  61. package/src/lib/core/shapes/Shape.ts +3 -0
  62. package/src/lib/core/theme/ColorScheme.ts +25 -0
  63. package/src/lib/core/theme/ComposeTheme.svelte +22 -0
  64. package/src/lib/core/theme/TextStyle.ts +25 -0
  65. package/src/lib/core/theme/colors.ts +21 -0
  66. package/src/lib/core/theme/cssVars.ts +32 -0
  67. package/src/lib/core/theme/defaults/darkColors.ts +17 -0
  68. package/src/lib/core/theme/defaults/defaultTheme.ts +35 -0
  69. package/src/lib/core/theme/defaults/lightColors.ts +17 -0
  70. package/src/lib/core/theme/defaults/typography.ts +128 -0
  71. package/src/lib/core/theme/elevation.ts +7 -0
  72. package/src/lib/core/theme/getCurrentColor.ts +10 -0
  73. package/src/lib/core/theme/resolve.ts +29 -0
  74. package/src/lib/core/theme/shapes.ts +7 -0
  75. package/src/lib/core/theme/spacing.ts +7 -0
  76. package/src/lib/core/theme/store.ts +26 -0
  77. package/src/lib/core/theme/systemTheme.ts +20 -0
  78. package/src/lib/core/theme/theme.ts +15 -0
  79. package/src/lib/core/theme/typography.ts +29 -0
  80. package/src/lib/index.ts +42 -0
  81. package/src/main.ts +9 -0
  82. package/svelte.config.js +8 -0
  83. package/tsconfig.app.json +21 -0
  84. package/tsconfig.json +7 -0
  85. package/tsconfig.node.json +26 -0
  86. package/vite.config.ts +11 -0
@@ -0,0 +1,36 @@
1
+ <script lang="ts">
2
+ import BaseTextField from "./BaseTextField.svelte";
3
+ import { TextFieldDefaults } from "./TextFieldDefaults";
4
+ import type { TextFieldColors } from "./TextFieldColors";
5
+ import { Modifier } from "../../core/modifier/Modifier";
6
+ import { RoundedCornerShape } from "../../core/shapes/RoundedCornerShape";
7
+ import type { Shape } from "../../core/shapes/Shape";
8
+ import type { TextStyleToken } from "../../core/theme/TextStyle";
9
+
10
+ export let value = "";
11
+ export let onValueChange = (v: string) => {};
12
+ export let label = "";
13
+ export let placeholder = "";
14
+ export let singleLine = true;
15
+ export let textStyle: TextStyleToken = "bodyLarge";
16
+ export let modifier: Modifier = Modifier.empty();
17
+ export let shape: Shape = RoundedCornerShape(12);
18
+
19
+ // 🔒 FilledTextField SIEMPRE usa filledColors por defecto
20
+ export let colors: TextFieldColors = TextFieldDefaults.filledColors();
21
+ </script>
22
+
23
+ <BaseTextField
24
+ value={value}
25
+ onValueChange={onValueChange}
26
+ label={label}
27
+ placeholder={placeholder}
28
+ singleLine={singleLine}
29
+ textStyle={textStyle}
30
+ modifier={modifier}
31
+ shape={shape}
32
+ colors={colors}
33
+ >
34
+ <slot name="leadingIcon" slot="leadingIcon" />
35
+ <slot name="trailingIcon" slot="trailingIcon" />
36
+ </BaseTextField>
@@ -0,0 +1,11 @@
1
+ export interface TextFieldColors {
2
+ container: string;
3
+ label: string;
4
+ placeholder: string;
5
+ text: string;
6
+ cursor: string;
7
+ indicatorFocused: string;
8
+ indicatorUnfocused: string;
9
+ borderFocused?: string;
10
+ borderUnfocused?: string;
11
+ }
@@ -0,0 +1,48 @@
1
+ import { resolveColor } from "../../core/theme/resolve";
2
+ import type { TextFieldColors } from "./TextFieldColors";
3
+
4
+ export const TextFieldDefaults = {
5
+ /**
6
+ * FilledTextField (Material default)
7
+ */
8
+ filledColors(): TextFieldColors {
9
+ return {
10
+ // Container
11
+ container: resolveColor("surfaceVariant"),
12
+
13
+ // Content
14
+ label: resolveColor("onSurfaceVariant"),
15
+ placeholder: resolveColor("onSurfaceVariant"),
16
+ text: resolveColor("onSurface"),
17
+ cursor: resolveColor("primary"),
18
+
19
+ // Indicator (bottom line)
20
+ indicatorFocused: resolveColor("primary"),
21
+ indicatorUnfocused: resolveColor("onSurfaceVariant"),
22
+ };
23
+ },
24
+
25
+ /**
26
+ * OutlinedTextField
27
+ */
28
+ outlinedColors(): TextFieldColors {
29
+ return {
30
+ // Container
31
+ container: "transparent",
32
+
33
+ // Content
34
+ label: resolveColor("onSurfaceVariant"),
35
+ placeholder: resolveColor("onSurfaceVariant"),
36
+ text: resolveColor("onSurface"),
37
+ cursor: resolveColor("primary"),
38
+
39
+ // Indicator (used only if needed)
40
+ indicatorFocused: resolveColor("primary"),
41
+ indicatorUnfocused: resolveColor("outline"),
42
+
43
+ // Border (outlined specific)
44
+ borderFocused: resolveColor("primary"),
45
+ borderUnfocused: resolveColor("outline"),
46
+ };
47
+ }
48
+ };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * painterResource - como en Jetpack Compose
3
+ * Carga un recurso local desde src/lib/assets/
4
+ * Soporta SVG (?raw para string) e imágenes (?url para URL)
5
+ */
6
+ export const Res = {
7
+ raw(fileName: string): string {
8
+ return "raw/"+fileName;
9
+ },
10
+
11
+ image(fileName: string): string {
12
+ return "img/"+fileName;
13
+ },
14
+
15
+ values(fileName: string): string {
16
+ return "values/"+fileName;
17
+ },
18
+
19
+ fonts(fileName: string): string {
20
+ return "fonts/"+fileName;
21
+ }
22
+ } as const;
23
+
24
+ export function painterResource(resourceName: string): string {
25
+ return "/src/assets/"+resourceName;
26
+ }
@@ -0,0 +1,259 @@
1
+ import { ModifierImpl } from "./ModifierImpl";
2
+ import type {BoxAlignment} from "../../components/layouts/Alignment";
3
+ import type {Shape} from "../shapes/Shape";
4
+ import type {ColorToken} from "../theme/ColorScheme";
5
+
6
+ /**
7
+ * Modifier
8
+ *
9
+ * Modifier is an immutable, chainable object used to decorate or augment
10
+ * a UI component.
11
+ *
12
+ * Inspired by Jetpack Compose Modifier.
13
+ *
14
+ * Usage:
15
+ * ```
16
+ * Modifier
17
+ * .padding(16)
18
+ * .fillMaxWidth()
19
+ * .background(ColorScheme.Surface)
20
+ * ```
21
+ */
22
+ export const Modifier = {
23
+ /**
24
+ * Returns an empty Modifier with no effects.
25
+ *
26
+ * Useful as a default value or starting point.
27
+ */
28
+ empty(): ModifierImpl {
29
+ return new ModifierImpl();
30
+ },
31
+
32
+ /**
33
+ * Adds padding around the content.
34
+ *
35
+ * Can be used with:
36
+ * - a single number (uniform padding)
37
+ * - an object with directional values
38
+ *
39
+ * Examples:
40
+ * ```
41
+ * Modifier.padding(16)
42
+ * Modifier.padding({ top: 8, bottom: 8 })
43
+ * ```
44
+ */
45
+ padding(
46
+ valueOrParams: number | { top?: number; bottom?: number; start?: number; end?: number } = 0,
47
+ unit: string = 'px'
48
+ ): ModifierImpl {
49
+ return new ModifierImpl().padding(valueOrParams,unit);
50
+ },
51
+
52
+
53
+ /**
54
+ * Adds horizontal padding (left and right).
55
+ */
56
+ paddingHorizontal(value: number): ModifierImpl {
57
+ return new ModifierImpl().paddingHorizontal(value);
58
+ },
59
+
60
+ /**
61
+ * Adds a border around the component.
62
+ *
63
+ * Optionally accepts a Shape to match rounded corners.
64
+ *
65
+ * Examples:
66
+ * ```
67
+ * Modifier.border(1, ColorScheme.Outline)
68
+ * Modifier.border(2, "#FF0000", RoundedCornerShape(12))
69
+ * ```
70
+ */
71
+ border(width: number, color: string, shape?: Shape): ModifierImpl {
72
+ return new ModifierImpl().border(width, color, shape);
73
+ },
74
+
75
+ /**
76
+ * Marks the component as clickable.
77
+ *
78
+ * This modifier applies interaction semantics such as:
79
+ * - pointer cursor
80
+ * - user-select disabling
81
+ *
82
+ * Note:
83
+ * The click handler must still be attached at the component level.
84
+ *
85
+ * Example:
86
+ * ```
87
+ * <Box
88
+ * on:click={onClick}
89
+ * modifier={Modifier.clickable(onClick)}
90
+ * />
91
+ * ```
92
+ */
93
+ clickable(onClick: () => void): ModifierImpl {
94
+ return new ModifierImpl().clickable(onClick);
95
+ },
96
+
97
+ /**
98
+ * Offsets the component visually without affecting its layout.
99
+ *
100
+ * Equivalent to Jetpack Compose Modifier.offset.
101
+ *
102
+ * Note:
103
+ * This uses CSS transform and does not affect surrounding layout.
104
+ */
105
+ offset(x: number, y: number): ModifierImpl {
106
+ return new ModifierImpl().offset(x, y);
107
+ },
108
+
109
+ /**
110
+ * Enables vertical scrolling for the component.
111
+ *
112
+ * Useful for Column or large content containers.
113
+ */
114
+ verticalScroll(enabled: boolean = true): ModifierImpl {
115
+ return new ModifierImpl().verticalScroll(enabled);
116
+ },
117
+
118
+ /**
119
+ * Enables horizontal scrolling for the component.
120
+ */
121
+ horizontalScroll(enabled: boolean = true): ModifierImpl {
122
+ return new ModifierImpl().horizontalScroll(enabled);
123
+ },
124
+
125
+ /**
126
+ * Adds vertical padding (top and bottom).
127
+ */
128
+ paddingVertical(value: number): ModifierImpl {
129
+ return new ModifierImpl().paddingVertical(value);
130
+ },
131
+
132
+ /**
133
+ * Aligns the component inside a Box.
134
+ *
135
+ * ⚠️ This modifier is intended to be used only inside Box layouts.
136
+ *
137
+ * Example:
138
+ * ```
139
+ * Modifier.align(Alignment.Center)
140
+ * ```
141
+ */
142
+ align(alignment: BoxAlignment): ModifierImpl {
143
+ return new ModifierImpl().align(alignment);
144
+ },
145
+
146
+ /**
147
+ * Makes the component fill the maximum available width.
148
+ */
149
+ fillMaxWidth(): ModifierImpl {
150
+ return new ModifierImpl().fillMaxWidth();
151
+ },
152
+
153
+ /**
154
+ * Makes the component fill the maximum available height.
155
+ */
156
+ fillMaxHeight(): ModifierImpl {
157
+ return new ModifierImpl().fillMaxHeight();
158
+ },
159
+
160
+ /**
161
+ * Makes the component fill both width and height.
162
+ */
163
+ fillMaxSize(): ModifierImpl {
164
+ return new ModifierImpl().fillMaxSize();
165
+ },
166
+
167
+ /**
168
+ * Sets a fixed height for the component.
169
+ *
170
+ * Accepts either a number (px by default) or a CSS size string.
171
+ */
172
+ height(value: number | string, unit = 'px'): ModifierImpl {
173
+ return new ModifierImpl().height(value, unit);
174
+ },
175
+
176
+ /**
177
+ * Sets a fixed width for the component.
178
+ *
179
+ * Accepts either a number (px by default) or a CSS size string.
180
+ */
181
+ width(value: number | string, unit = 'px'): ModifierImpl {
182
+ return new ModifierImpl().width(value, unit);
183
+ },
184
+
185
+ /**
186
+ * Applies a background color to the component.
187
+ *
188
+ * Accepts either:
189
+ * - A CSS color string (e.g. "#2A2A2A", "transparent")
190
+ * - A Compose color token (e.g. ColorScheme.Surface)
191
+ *
192
+ * Examples:
193
+ * ```
194
+ * Modifier.background(ColorScheme.Surface)
195
+ * Modifier.background("#121212")
196
+ * ```
197
+ */
198
+ background(color: ColorToken | string): ModifierImpl {
199
+ return new ModifierImpl().background(color);
200
+ },
201
+
202
+ /**
203
+ * Assigns a proportional weight to the component inside
204
+ * a Row or Column.
205
+ *
206
+ * Similar to flex-grow.
207
+ *
208
+ * Example:
209
+ * ```
210
+ * Modifier.weight(1)
211
+ * ```
212
+ */
213
+ weight(weight: number, fill: boolean = true): ModifierImpl {
214
+ return new ModifierImpl().weight(weight, fill);
215
+ },
216
+
217
+ /**
218
+ * Assigns weight without forcing fill behavior.
219
+ */
220
+ weightNoFill(weight: number): ModifierImpl {
221
+ return this.weight(weight, false);
222
+ },
223
+
224
+ /**
225
+ * Adds top margin to the component.
226
+ */
227
+ marginTop(value: number, unit = 'px'): ModifierImpl {
228
+ return new ModifierImpl().marginTop(value, unit);
229
+ },
230
+
231
+ /**
232
+ * Clips the component using the provided Shape.
233
+ *
234
+ * Example:
235
+ * ```
236
+ * Modifier.clip(RoundedCornerShape(16))
237
+ * ```
238
+ */
239
+ clip(shape: Shape): ModifierImpl {
240
+ return new ModifierImpl().clip(shape);
241
+ },
242
+
243
+ /**
244
+ * Sets both width and height to the same value.
245
+ *
246
+ * Useful for icons and square components.
247
+ *
248
+ * Example:
249
+ * ```
250
+ * Modifier.size(24)
251
+ * ```
252
+ */
253
+ size(value: number | string, unit: string = "px"): ModifierImpl {
254
+ return new ModifierImpl().size(value, unit);
255
+ }
256
+ } as const;
257
+
258
+ // Tipo público
259
+ export type Modifier = ModifierImpl;
@@ -0,0 +1,275 @@
1
+ import type {BoxAlignment} from "../../components/layouts/Alignment";
2
+ import type {Shape} from "../shapes/Shape";
3
+ import type {ColorToken} from "../theme/ColorScheme";
4
+ import {resolveColor} from "../theme/resolve";
5
+
6
+ export type ModifierEntry = {
7
+ className?: string;
8
+ style?: string;
9
+ };
10
+
11
+ export class ModifierImpl {
12
+ private readonly entries: ModifierEntry[];
13
+
14
+ constructor(entries: ModifierEntry[] = []) {
15
+ this.entries = entries;
16
+ }
17
+
18
+ then(other: ModifierImpl): ModifierImpl {
19
+ return new ModifierImpl([...this.entries, ...other.entries]);
20
+ }
21
+
22
+ paddingHorizontal(value: number): ModifierImpl {
23
+ return this.then(
24
+ new ModifierImpl([
25
+ { style: `padding-left:${value}px;padding-right:${value}px;` },
26
+ ])
27
+ );
28
+ }
29
+
30
+ verticalScroll(enabled: boolean = true): ModifierImpl {
31
+ return this.then(
32
+ new ModifierImpl([{
33
+ style: enabled
34
+ ? `overflow-y:auto;overflow-x:hidden;-webkit-overflow-scrolling:touch;`
35
+ : ''
36
+ }])
37
+ );
38
+ }
39
+
40
+ horizontalScroll(enabled: boolean = true): ModifierImpl {
41
+ return this.then(
42
+ new ModifierImpl([{
43
+ style: enabled
44
+ ? `overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;white-space:nowrap;`
45
+ : ''
46
+ }])
47
+ );
48
+ }
49
+
50
+ paddingVertical(value: number): ModifierImpl {
51
+ return this.then(
52
+ new ModifierImpl([
53
+ { style: `padding-top:${value}px;padding-bottom:${value}px;` },
54
+ ])
55
+ );
56
+ }
57
+
58
+ fillMaxWidth(): ModifierImpl {
59
+ return this.then(
60
+ new ModifierImpl([{ style: `width:100%;` }])
61
+ );
62
+ }
63
+
64
+ fillMaxHeight(): ModifierImpl {
65
+ return this.then(
66
+ new ModifierImpl([{ style: `height:100%;` }])
67
+ );
68
+ }
69
+
70
+ fillMaxSize(): ModifierImpl {
71
+ return this.then(
72
+ new ModifierImpl([{ style: `width:100%;height:100%;` }])
73
+ );
74
+ }
75
+
76
+ background(color: ColorToken | string): ModifierImpl {
77
+ let resolved: string;
78
+
79
+ if (
80
+ typeof color === "string" &&
81
+ (
82
+ color.startsWith("#") ||
83
+ color.startsWith("rgb") ||
84
+ color.startsWith("hsl") ||
85
+ color === "transparent" ||
86
+ color === "currentColor"
87
+ )
88
+ ) {
89
+ // Color CSS directo
90
+ resolved = color;
91
+ } else {
92
+ // Token de ComposeTheme
93
+ resolved = resolveColor(color as ColorToken);
94
+ }
95
+
96
+ return this.then(
97
+ new ModifierImpl([
98
+ { style: `background:${resolved};` }
99
+ ])
100
+ );
101
+ }
102
+
103
+ weight(weight: number, fill: boolean = true): ModifierImpl {
104
+ if (weight <= 0) {
105
+ console.warn("Modifier.weight() debe ser > 0");
106
+ return this;
107
+ }
108
+
109
+ const styleParts = [
110
+ `flex-grow: ${weight};`,
111
+ `flex-shrink: ${fill ? 1 : 0};`,
112
+ `flex-basis: 0%;` // importante para que el peso funcione bien
113
+ ];
114
+
115
+ return this.then(new ModifierImpl([{ style: styleParts.join(" ") }]));
116
+ }
117
+
118
+ align(alignment: BoxAlignment): ModifierImpl {
119
+ const parts = alignment.split(' ');
120
+ const horizontal = parts[0]; // flex-start, center, flex-end
121
+ const vertical = parts[1] || parts[0]; // para casos simples como "center"
122
+
123
+ let style = 'position: absolute;';
124
+
125
+ // Vertical
126
+ if (vertical === 'flex-start') {
127
+ style += 'top: 0;';
128
+ } else if (vertical === 'flex-end') {
129
+ style += 'bottom: 0;';
130
+ } else if (vertical === 'center') {
131
+ style += 'top: 50%; transform: translateY(-50%);';
132
+ }
133
+
134
+ // Horizontal
135
+ if (horizontal === 'flex-start') {
136
+ style += 'left: 0;';
137
+ } else if (horizontal === 'flex-end') {
138
+ style += 'right: 0;';
139
+ } else if (horizontal === 'center') {
140
+ style += 'left: 50%;';
141
+ // Combinar transform si ya hay translateY
142
+ if (style.includes('translateY')) {
143
+ style = style.replace('translateY(-50%)', 'translate(-50%, -50%)');
144
+ } else {
145
+ style += 'transform: translateX(-50%);';
146
+ }
147
+ }
148
+
149
+ return this.then(new ModifierImpl([{ style }]));
150
+ }
151
+
152
+ padding(valueOrParams: number | { top?: number; bottom?: number; start?: number; end?: number } = 0, unit: string = 'px'): ModifierImpl {
153
+ let style = '';
154
+
155
+ if (typeof valueOrParams === 'number') {
156
+ // Padding uniforme
157
+ style = `padding:${valueOrParams}${unit};`;
158
+ } else {
159
+ // Padding direccional
160
+ const { top = 0, bottom = 0, start = 0, end = 0 } = valueOrParams;
161
+ style = `
162
+ padding-top:${top}${unit};
163
+ padding-bottom:${bottom}${unit};
164
+ padding-left:${start}${unit};
165
+ //padding-right:${end}${unit};
166
+ `.trim();
167
+ }
168
+
169
+ return this.then(new ModifierImpl([{ style }]));
170
+ }
171
+
172
+ width(value: number | string, unit = 'px'): ModifierImpl {
173
+ const size = typeof value === 'number' ? `${value}${unit}` : value;
174
+ return this.then(new ModifierImpl([{ style: `width:${size};` }]));
175
+ }
176
+
177
+ height(value: number | string, unit = 'px'): ModifierImpl {
178
+ const size = typeof value === 'number' ? `${value}${unit}` : value;
179
+ return this.then(new ModifierImpl([{ style: `height:${size};` }]));
180
+ }
181
+
182
+ marginTop(value: number, unit = 'px'): ModifierImpl {
183
+ return this.then(new ModifierImpl([{ style: `margin-top:${value}${unit};` }]));
184
+ }
185
+
186
+ clip(shape: Shape): ModifierImpl {
187
+ return this.then(
188
+ new ModifierImpl([
189
+ {
190
+ style: `
191
+ border-radius:${shape.toCssBorderRadius()};
192
+ overflow:hidden;
193
+ `
194
+ }
195
+ ])
196
+ );
197
+ }
198
+
199
+ size(value: number | string, unit: string = "px"): ModifierImpl {
200
+ if (value === null || value === undefined) {
201
+ return this;
202
+ }
203
+
204
+ let resolved: string;
205
+
206
+ if (typeof value === "number") {
207
+ if (isNaN(value)) return this;
208
+ resolved = `${value}${unit}`;
209
+ } else {
210
+ if (value.trim() === "") return this;
211
+ resolved = value;
212
+ }
213
+
214
+ return this.then(
215
+ new ModifierImpl([
216
+ {
217
+ style: `width:${resolved};height:${resolved};`
218
+ }
219
+ ])
220
+ );
221
+ }
222
+
223
+ offset(x: number, y: number): ModifierImpl {
224
+ if (isNaN(x) || isNaN(y)) return this;
225
+
226
+ return this.then(
227
+ new ModifierImpl([
228
+ {
229
+ style: `transform: translate(${x}px, ${y}px);`
230
+ }
231
+ ])
232
+ );
233
+ }
234
+
235
+ clickable(onClick: () => void): ModifierImpl {
236
+ return this.then(
237
+ new ModifierImpl([
238
+ {
239
+ className: "compose-clickable",
240
+ style: `
241
+ cursor: pointer;
242
+ user-select: none;
243
+ `
244
+ }
245
+ ])
246
+ );
247
+ }
248
+
249
+ border(width: number, color: string, shape?: Shape): ModifierImpl {
250
+ if (width <= 0) return this;
251
+
252
+ const radius = shape ? shape.toCssBorderRadius() : undefined;
253
+
254
+ return this.then(
255
+ new ModifierImpl([
256
+ {
257
+ style: `
258
+ border:${width}px solid ${color};
259
+ ${radius ? `border-radius:${radius};` : ""}
260
+ `
261
+ }
262
+ ])
263
+ );
264
+ }
265
+
266
+ // ---- consumo interno ----
267
+
268
+ toStyle(): string {
269
+ return this.entries.map(e => e.style ?? "").join("");
270
+ }
271
+
272
+ toClass(): string {
273
+ return this.entries.map(e => e.className ?? "").join(" ");
274
+ }
275
+ }
@@ -0,0 +1,53 @@
1
+ import type { Shape } from "./Shape";
2
+
3
+ type CornerSize = number | string;
4
+
5
+ type RoundedCornerParams =
6
+ | CornerSize
7
+ | {
8
+ topStart?: CornerSize;
9
+ topEnd?: CornerSize;
10
+ bottomEnd?: CornerSize;
11
+ bottomStart?: CornerSize;
12
+ };
13
+
14
+ function toCss(value?: CornerSize): string {
15
+ if (value === undefined) return "0px";
16
+ return typeof value === "number" ? `${value}px` : value;
17
+ }
18
+
19
+ class RoundedCornerShapeImpl implements Shape {
20
+ constructor(private readonly params: RoundedCornerParams) {}
21
+
22
+ toCssBorderRadius(): string {
23
+ // Caso: un solo valor → todas las esquinas
24
+ if (typeof this.params === "number" || typeof this.params === "string") {
25
+ const v = toCss(this.params);
26
+ return `${v} ${v} ${v} ${v}`;
27
+ }
28
+
29
+ const {
30
+ topStart = 0,
31
+ topEnd = 0,
32
+ bottomEnd = 0,
33
+ bottomStart = 0,
34
+ } = this.params;
35
+
36
+ // CSS: top-left, top-right, bottom-right, bottom-left
37
+ return `
38
+ ${toCss(topStart)}
39
+ ${toCss(topEnd)}
40
+ ${toCss(bottomEnd)}
41
+ ${toCss(bottomStart)}
42
+ `.trim();
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Compose-like factory
48
+ */
49
+ export function RoundedCornerShape(
50
+ params: RoundedCornerParams
51
+ ): Shape {
52
+ return new RoundedCornerShapeImpl(params);
53
+ }