@bunnix/components 0.10.3 → 0.11.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.
Files changed (149) hide show
  1. package/@types/index.d.ts +179 -15
  2. package/README.md +41 -4
  3. package/package.json +3 -8
  4. package/src/core/buttons.css +1 -0
  5. package/src/core/core.css +16 -2
  6. package/src/core/dialog.css +3 -1
  7. package/src/core/dialog.mjs +101 -16
  8. package/src/core/input.css +202 -0
  9. package/src/core/inputs.mjs +702 -23
  10. package/src/core/layout.mjs +1 -2
  11. package/src/core/media.css +36 -1
  12. package/src/core/media.mjs +13 -13
  13. package/src/core/menu.css +10 -29
  14. package/src/core/menu.mjs +159 -70
  15. package/src/core/outline.mjs +100 -0
  16. package/src/core/sidebar.mjs +189 -68
  17. package/src/core/sliderUtils.mjs +51 -0
  18. package/src/core/table.css +23 -0
  19. package/src/core/table.mjs +35 -20
  20. package/src/core/textareaUtils.mjs +31 -0
  21. package/src/core/utils.mjs +105 -0
  22. package/src/font-face/Framework7Icons-Regular.woff2 +0 -0
  23. package/src/index.mjs +3 -1
  24. package/src/icons/add-circle.svg +0 -1
  25. package/src/icons/add.svg +0 -1
  26. package/src/icons/alt.svg +0 -1
  27. package/src/icons/archive.svg +0 -1
  28. package/src/icons/arrow-down.svg +0 -1
  29. package/src/icons/arrow-left.svg +0 -1
  30. package/src/icons/arrow-right.svg +0 -1
  31. package/src/icons/arrow-up.svg +0 -1
  32. package/src/icons/at.svg +0 -1
  33. package/src/icons/attestation.svg +0 -1
  34. package/src/icons/battery-25.svg +0 -1
  35. package/src/icons/bell.svg +0 -3
  36. package/src/icons/bookmark.svg +0 -1
  37. package/src/icons/bot.svg +0 -1
  38. package/src/icons/bubble.svg +0 -1
  39. package/src/icons/building.svg +0 -3
  40. package/src/icons/button.svg +0 -1
  41. package/src/icons/calculate.svg +0 -1
  42. package/src/icons/calendar.svg +0 -1
  43. package/src/icons/captions-bubble.svg +0 -1
  44. package/src/icons/cart.svg +0 -1
  45. package/src/icons/chart.svg +0 -1
  46. package/src/icons/check.svg +0 -1
  47. package/src/icons/chevron-down.svg +0 -1
  48. package/src/icons/chevron-left.svg +0 -1
  49. package/src/icons/chevron-right.svg +0 -1
  50. package/src/icons/clip.svg +0 -1
  51. package/src/icons/clock.svg +0 -3
  52. package/src/icons/close-circle.svg +0 -3
  53. package/src/icons/close.svg +0 -1
  54. package/src/icons/cloud-download.svg +0 -1
  55. package/src/icons/cloud-upload.svg +0 -1
  56. package/src/icons/cloud.svg +0 -1
  57. package/src/icons/columns-layout.svg +0 -1
  58. package/src/icons/command.svg +0 -1
  59. package/src/icons/cube.svg +0 -1
  60. package/src/icons/delete.svg +0 -3
  61. package/src/icons/dollar.svg +0 -3
  62. package/src/icons/download.svg +0 -1
  63. package/src/icons/draw.svg +0 -1
  64. package/src/icons/duplicate.svg +0 -3
  65. package/src/icons/ear.svg +0 -1
  66. package/src/icons/edit.svg +0 -1
  67. package/src/icons/exclamation-mark.svg +0 -1
  68. package/src/icons/eye-open.svg +0 -1
  69. package/src/icons/eye.svg +0 -1
  70. package/src/icons/file-html.svg +0 -1
  71. package/src/icons/file.svg +0 -3
  72. package/src/icons/finger.svg +0 -1
  73. package/src/icons/flag.svg +0 -1
  74. package/src/icons/folder.svg +0 -1
  75. package/src/icons/function.svg +0 -1
  76. package/src/icons/gear.svg +0 -1
  77. package/src/icons/gift.svg +0 -1
  78. package/src/icons/globe.svg +0 -3
  79. package/src/icons/grid.svg +0 -1
  80. package/src/icons/hammer.svg +0 -1
  81. package/src/icons/hand.svg +0 -1
  82. package/src/icons/hare.svg +0 -1
  83. package/src/icons/heart.svg +0 -3
  84. package/src/icons/home.svg +0 -3
  85. package/src/icons/image.svg +0 -1
  86. package/src/icons/inbox.svg +0 -3
  87. package/src/icons/info.svg +0 -1
  88. package/src/icons/key.svg +0 -1
  89. package/src/icons/lamp.svg +0 -1
  90. package/src/icons/link.svg +0 -1
  91. package/src/icons/location.svg +0 -1
  92. package/src/icons/locker.svg +0 -1
  93. package/src/icons/login.svg +0 -1
  94. package/src/icons/logout.svg +0 -3
  95. package/src/icons/mail.svg +0 -3
  96. package/src/icons/map.svg +0 -3
  97. package/src/icons/markup.svg +0 -1
  98. package/src/icons/merge.svg +0 -1
  99. package/src/icons/more-horizontal.svg +0 -5
  100. package/src/icons/more-vertical.svg +0 -5
  101. package/src/icons/mouse.svg +0 -1
  102. package/src/icons/music-mic.svg +0 -1
  103. package/src/icons/paintbrush.svg +0 -1
  104. package/src/icons/palette.svg +0 -1
  105. package/src/icons/password.svg +0 -1
  106. package/src/icons/pencil.svg +0 -1
  107. package/src/icons/people.svg +0 -3
  108. package/src/icons/percent.svg +0 -1
  109. package/src/icons/person-add.svg +0 -1
  110. package/src/icons/person-remove.svg +0 -1
  111. package/src/icons/person.svg +0 -4
  112. package/src/icons/phone.svg +0 -1
  113. package/src/icons/pin.svg +0 -1
  114. package/src/icons/question-circle.svg +0 -3
  115. package/src/icons/remove-circle.svg +0 -1
  116. package/src/icons/return-arrow.svg +0 -1
  117. package/src/icons/save.svg +0 -1
  118. package/src/icons/search.svg +0 -1
  119. package/src/icons/sections.svg +0 -1
  120. package/src/icons/send.svg +0 -1
  121. package/src/icons/share.svg +0 -1
  122. package/src/icons/shine.svg +0 -1
  123. package/src/icons/sliders.svg +0 -1
  124. package/src/icons/star.svg +0 -3
  125. package/src/icons/staroflife.svg +0 -1
  126. package/src/icons/storage.svg +0 -1
  127. package/src/icons/success-circle.svg +0 -3
  128. package/src/icons/swap.svg +0 -1
  129. package/src/icons/switch.svg +0 -1
  130. package/src/icons/sync.svg +0 -3
  131. package/src/icons/table.svg +0 -3
  132. package/src/icons/tag.svg +0 -3
  133. package/src/icons/terminal.svg +0 -1
  134. package/src/icons/text.svg +0 -1
  135. package/src/icons/thumb-down.svg +0 -1
  136. package/src/icons/thumb-up.svg +0 -1
  137. package/src/icons/timer.svg +0 -3
  138. package/src/icons/toggle.svg +0 -1
  139. package/src/icons/trash.svg +0 -1
  140. package/src/icons/tv-music.svg +0 -1
  141. package/src/icons/update-page.svg +0 -1
  142. package/src/icons/upload.svg +0 -1
  143. package/src/icons/video.svg +0 -1
  144. package/src/icons/wallet.svg +0 -1
  145. package/src/icons/wand-stars.svg +0 -1
  146. package/src/icons/waveform.svg +0 -1
  147. package/src/icons/window.svg +0 -1
  148. package/src/utils/iconRegistry.generated.mjs +0 -187
  149. package/src/utils/iconRegistry.mjs +0 -34
package/@types/index.d.ts CHANGED
@@ -16,6 +16,10 @@ export type StateLike<T = any> = {
16
16
 
17
17
  export interface LayoutProps extends BaseProps {
18
18
  gap?: number | string;
19
+ fontSize?: number | string;
20
+ overflow?: string;
21
+ overflowX?: string;
22
+ overflowY?: string;
19
23
  width?: number | string;
20
24
  height?: number | string;
21
25
  minWidth?: number | string;
@@ -24,8 +28,24 @@ export interface LayoutProps extends BaseProps {
24
28
  maxHeight?: number | string;
25
29
  fillWidth?: boolean;
26
30
  fillHeight?: boolean;
31
+ zIndex?: number | string;
32
+ border?: "none" | "primary" | "secondary" | "tertiary" | "transparent";
33
+ bgColor?: "primary" | "primary-dimmed" | "secondary" | "success" | "success-dimmed" | "warning" | "warning-dimmed" | "danger" | string;
34
+ margin?: number | string;
27
35
  marginX?: number | string;
28
36
  marginY?: number | string;
37
+ marginTop?: number | string;
38
+ marginBottom?: number | string;
39
+ marginLeft?: number | string;
40
+ marginRight?: number | string;
41
+ padding?: number | string;
42
+ paddingX?: number | string;
43
+ paddingY?: number | string;
44
+ paddingTop?: number | string;
45
+ paddingBottom?: number | string;
46
+ paddingLeft?: number | string;
47
+ paddingRight?: number | string;
48
+ borderRadius?: number | string;
29
49
  }
30
50
 
31
51
  export interface GridColumn {
@@ -71,7 +91,7 @@ export interface ButtonProps extends LayoutProps {
71
91
  variant?: "primary" | "secondary" | "tertiary" | "danger" | string;
72
92
  disabled?: boolean | StateLike<boolean>;
73
93
  outline?: boolean;
74
- padding?: boolean;
94
+ padding?: number | string | boolean;
75
95
  click?: (event?: any) => void;
76
96
  }
77
97
 
@@ -85,6 +105,7 @@ export interface LinkButtonProps extends LayoutProps {
85
105
 
86
106
  export interface TextInputProps extends LayoutProps {
87
107
  value?: string | number | Date | StateLike<any>;
108
+ focused?: boolean | StateLike<boolean>;
88
109
  placeholder?: string;
89
110
  type?: string;
90
111
  label?: string;
@@ -93,6 +114,62 @@ export interface TextInputProps extends LayoutProps {
93
114
  input?: (event?: any) => void;
94
115
  }
95
116
 
117
+ export interface TextAreaProps extends LayoutProps {
118
+ value?: string | number | StateLike<any>;
119
+ focused?: boolean | StateLike<boolean>;
120
+ placeholder?: string;
121
+ label?: string;
122
+ outline?: boolean;
123
+ disabled?: boolean;
124
+ minLines?: number;
125
+ maxLines?: number;
126
+ input?: (event?: any) => void;
127
+ }
128
+
129
+ export type MenuAnchor =
130
+ | "bottom-left"
131
+ | "bottom-right"
132
+ | "top-left"
133
+ | "top-right"
134
+ | "bottomLeft"
135
+ | "bottomRight"
136
+ | "topLeft"
137
+ | "topRight";
138
+
139
+ export interface MenuItem {
140
+ key?: string;
141
+ text?: string;
142
+ icon?: string;
143
+ action?: (() => void) | null;
144
+ divider?: boolean;
145
+ }
146
+
147
+ export interface PickerProps extends LayoutProps {
148
+ value?: string | StateLike<string>;
149
+ options?: MenuItem[] | StateLike<MenuItem[]>;
150
+ label?: string;
151
+ outline?: boolean;
152
+ disabled?: boolean | StateLike<boolean>;
153
+ anchor?: MenuAnchor;
154
+ input?: (event?: any) => void;
155
+ }
156
+
157
+ export interface SegmentedPickerItem {
158
+ key: string;
159
+ text: string;
160
+ icon?: string;
161
+ }
162
+
163
+ export interface SegmentedPickerProps extends LayoutProps {
164
+ value?: string | StateLike<string>;
165
+ items?: SegmentedPickerItem[] | StateLike<SegmentedPickerItem[]>;
166
+ label?: string;
167
+ outline?: boolean;
168
+ disabled?: boolean;
169
+ input?: (event?: any) => void;
170
+ change?: (event?: any) => void;
171
+ }
172
+
96
173
  export interface SelectOption {
97
174
  key?: string;
98
175
  content?: any;
@@ -100,7 +177,7 @@ export interface SelectOption {
100
177
 
101
178
  export interface SelectProps extends LayoutProps {
102
179
  value?: string | StateLike<string>;
103
- options?: SelectOption[];
180
+ options?: SelectOption[] | StateLike<SelectOption[]>;
104
181
  label?: string;
105
182
  outline?: boolean;
106
183
  disabled?: boolean;
@@ -117,6 +194,33 @@ export interface CheckBoxProps extends LayoutProps {
117
194
  input?: (event?: any) => void;
118
195
  }
119
196
 
197
+ export interface SwitchProps extends LayoutProps {
198
+ checked?: boolean | StateLike<boolean>;
199
+ value?: boolean | StateLike<boolean>;
200
+ label?: string;
201
+ outline?: boolean;
202
+ disabled?: boolean;
203
+ change?: (event?: any) => void;
204
+ input?: (event?: any) => void;
205
+ }
206
+
207
+ export interface SliderStep {
208
+ value: number;
209
+ label?: string;
210
+ }
211
+
212
+ export interface SliderProps extends LayoutProps {
213
+ value?: number | StateLike<number>;
214
+ min?: number | StateLike<number>;
215
+ max?: number | StateLike<number>;
216
+ step?: number | StateLike<number>;
217
+ steps?: SliderStep[] | StateLike<SliderStep[]>;
218
+ label?: string;
219
+ outline?: boolean;
220
+ disabled?: boolean;
221
+ input?: (event?: any) => void;
222
+ }
223
+
120
224
  export interface TableHeader {
121
225
  content?: any;
122
226
  key?: string;
@@ -124,20 +228,33 @@ export interface TableHeader {
124
228
  }
125
229
 
126
230
  export interface TableProps extends LayoutProps {
127
- headers?: TableHeader[];
128
- rows?: Array<Record<string, any>>;
231
+ headers?: TableHeader[] | StateLike<TableHeader[]>;
232
+ rows?: Array<Record<string, any>> | StateLike<Array<Record<string, any>>>;
233
+ type?: "regular" | "alternate-rows";
234
+ hideHeaders?: boolean;
235
+ renderCell?: (
236
+ record: Record<string, any>,
237
+ rowIndex: number,
238
+ field: string,
239
+ ) => BunnixChild;
129
240
  }
130
241
 
131
- export interface DialogConfirmation {
242
+ export interface DialogAction {
132
243
  text?: string;
133
244
  variant?: string;
134
245
  action?: (() => void) | null;
135
246
  }
136
247
 
248
+ export interface DialogConfirmation extends DialogAction {}
249
+
137
250
  export interface ShowDialogOptions {
138
- title?: string;
251
+ title?: string | BunnixChild;
139
252
  contents?: any[] | any;
140
- confirmation?: DialogConfirmation;
253
+ padding?: number | string;
254
+ width?: number | string;
255
+ height?: number | string;
256
+ secondary?: DialogAction;
257
+ confirmation?: DialogAction;
141
258
  }
142
259
 
143
260
  export interface ProgressBarProps extends LayoutProps {
@@ -145,7 +262,53 @@ export interface ProgressBarProps extends LayoutProps {
145
262
  color?: "primary" | "primary-dimmed" | "secondary" | "tertiary" | "success" | "warning" | "danger" | "error" | "link" | string;
146
263
  }
147
264
 
148
- export type Component<P = BaseProps> = (props?: P, ...children: BunnixChildren[]) => any;
265
+ export interface OutlineProps extends LayoutProps {
266
+ /** Always-visible trigger content — accepts any Bunnix node (text, icon, row, etc.) */
267
+ anchor?: BunnixChild;
268
+ /** Collapsible content shown when expanded — accepts any Bunnix node */
269
+ details?: BunnixChild;
270
+ /** Whether to render the chevron toggle icon (default: true) */
271
+ showChevron?: boolean;
272
+ /** Controlled open/closed state — accepts a static boolean (initial value) or StateLike<boolean> for two-way binding */
273
+ open?: boolean | StateLike<boolean>;
274
+ }
275
+
276
+ export interface MenuProps extends LayoutProps {
277
+ items?: MenuItem[] | StateLike<MenuItem[]>;
278
+ trigger?:
279
+ | BunnixChild
280
+ | ((state: { isOpen: boolean; toggle: () => void }) => BunnixChild);
281
+ anchor?: MenuAnchor;
282
+ }
283
+
284
+ /** Sidebar navigation item configuration */
285
+ export interface SidebarItem {
286
+ /** Unique identifier for the sidebar item */
287
+ key: string;
288
+ /** Display text for the item */
289
+ text: string;
290
+ /** Optional official Framework7 icon name */
291
+ icon?: string;
292
+ /** If true, renders as a section header instead of a clickable item */
293
+ isHeader?: boolean;
294
+ /** Optional nested child items rendered below this item when expanded */
295
+ children?: SidebarItem[];
296
+ /** Initial expanded state for items that have nested children */
297
+ expanded?: boolean;
298
+ }
299
+
300
+ /** Props for the Sidebar component */
301
+ export interface SidebarProps extends LayoutProps {
302
+ /** Array of sidebar items or a state object containing items. Supports dynamic updates via StateLike */
303
+ items?: SidebarItem[] | StateLike<SidebarItem[]>;
304
+ /** Currently selected item key, null for no selection, or a state object. Updates on item click */
305
+ selection?: string | null | StateLike<string | null>;
306
+ }
307
+
308
+ export type Component<P = BaseProps> = {
309
+ (...children: BunnixChildren[]): any;
310
+ (props: P, ...children: BunnixChildren[]): any;
311
+ };
149
312
 
150
313
  export const Column: Component<LayoutProps>;
151
314
  export const Row: Component<LayoutProps>;
@@ -164,14 +327,20 @@ export const Button: Component<ButtonProps>;
164
327
  export const LinkButton: Component<LinkButtonProps>;
165
328
 
166
329
  export const TextInput: Component<TextInputProps>;
330
+ export const TextArea: Component<TextAreaProps>;
167
331
  export const Select: Component<SelectProps>;
168
332
  export const CheckBox: Component<CheckBoxProps>;
333
+ export const Switch: Component<SwitchProps>;
334
+ export const SegmentedPicker: Component<SegmentedPickerProps>;
335
+ export const Slider: Component<SliderProps>;
169
336
 
170
337
  export const Table: Component<TableProps>;
171
338
  export const Code: Component<BaseProps & { html?: string; language?: string }>;
172
339
 
173
- export const Sidebar: Component<BaseProps>;
174
- export const Menu: Component<BaseProps>;
340
+ export const Sidebar: Component<SidebarProps>;
341
+ export const Picker: Component<PickerProps>;
342
+ export const Menu: Component<MenuProps>;
343
+ export const Outline: Component<OutlineProps>;
175
344
 
176
345
  export function useDialog(): {
177
346
  Dialog: Component<BaseProps>;
@@ -184,8 +353,3 @@ declare module "@bunnix/components/styles.css" {
184
353
  const content: string;
185
354
  export default content;
186
355
  }
187
-
188
- declare module "@bunnix/components/styles" {
189
- const content: string;
190
- export default content;
191
- }
package/README.md CHANGED
@@ -28,6 +28,10 @@ import {
28
28
  Text,
29
29
  Button,
30
30
  TextInput,
31
+ TextArea,
32
+ Switch,
33
+ Slider,
34
+ Picker,
31
35
  ProgressBar,
32
36
  } from "@bunnix/components";
33
37
 
@@ -40,19 +44,52 @@ Column(
40
44
  Button({ variant: "tertiary" }, "Cancel"),
41
45
  ),
42
46
  TextInput({ label: "Name", placeholder: "Type here" }),
47
+ TextArea({
48
+ label: "Notes",
49
+ minLines: 3,
50
+ maxLines: 6,
51
+ placeholder: "Write more...",
52
+ }),
53
+ Switch({ checked: false, label: "Enable sync" }),
54
+ Picker({
55
+ value: "calendar",
56
+ options: [
57
+ { key: "calendar", text: "Calendar", icon: "calendar" },
58
+ { key: "messages", text: "Messages", icon: "chat_bubble_2" },
59
+ ],
60
+ }),
61
+ Slider({ min: 0, max: 100, step: 5, value: 50 }),
43
62
  ProgressBar({ value: 65, color: "success" }),
44
63
  );
45
64
  ```
46
65
 
66
+ ## Layout Border Prop
67
+
68
+ Layout primitives accept a resolved `border` prop:
69
+
70
+ `"none" | "primary" | "secondary" | "tertiary" | "transparent"`
71
+
72
+ ```js
73
+ Column(
74
+ { gap: 8 },
75
+ Column({ border: "primary", padding: "regular", radius: "regular" }, "Primary"),
76
+ Column({ border: "secondary", padding: "regular", radius: "regular" }, "Secondary"),
77
+ Column({ border: "tertiary", padding: "regular", radius: "regular" }, "Tertiary"),
78
+ Column({ border: "transparent", padding: "regular", radius: "regular" }, "Transparent"),
79
+ );
80
+ ```
81
+
82
+ These border tokens automatically adapt to light and dark color schemes, with `secondary` and `tertiary` rendered as softer tones of the primary border.
83
+
47
84
  ## Exported API
48
85
 
49
86
  - Layout: `Column`, `Row`, `Spacer`, `Grid`
50
87
  - Typography: `Heading`, `Text`
51
88
  - Media: `Media`, `Icon`, `Spinner`, `Avatar`
52
89
  - Buttons: `Button`, `LinkButton`
53
- - Inputs: `TextInput`, `Select`, `CheckBox`
90
+ - Inputs: `TextInput`, `TextArea`, `Select`, `CheckBox`, `Switch`, `Slider`
54
91
  - Data display: `Table`, `Code`
55
- - Navigation: `Sidebar`, `Menu`
92
+ - Navigation: `Sidebar`, `Picker`, `Menu`
56
93
  - Feedback: `useDialog`, `ProgressBar`
57
94
 
58
95
  ## ProgressBar Colors
@@ -78,9 +115,9 @@ Primary consumer stylesheet:
78
115
 
79
116
  - `@bunnix/components/styles.css`
80
117
 
81
- Optional core styles (advanced use):
118
+ Canonical public stylesheet:
82
119
 
83
- - `@bunnix/components/src/core/core.css`
120
+ - `@bunnix/components/styles.css`
84
121
  - `@bunnix/components/src/core/input.css`
85
122
  - `@bunnix/components/src/core/table.css`
86
123
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bunnix/components",
3
- "version": "0.10.3",
3
+ "version": "0.11.0",
4
4
  "description": "Bunnix components: a set of bunnix ready components for modern web apps.",
5
5
  "keywords": [
6
6
  "bunnix",
@@ -29,9 +29,7 @@
29
29
  "types": "./@types/index.d.ts",
30
30
  "default": "./src/index.mjs"
31
31
  },
32
- "./styles": "./src/core/core.css",
33
- "./core.css": "./src/core/core.css",
34
- "./icons/*": "./src/icons/*"
32
+ "./styles.css": "./src/core/core.css"
35
33
  },
36
34
  "sideEffects": [
37
35
  "./src/core/*.css"
@@ -41,10 +39,7 @@
41
39
  "@types"
42
40
  ],
43
41
  "scripts": {
44
- "test": "node --test",
45
- "sanitize:icons": "node scripts/sanitizeSvgs.mjs",
46
- "prebuild": "npm run sanitize:icons && node scripts/generateIconRegistry.mjs",
47
- "build:icons": "npm run sanitize:icons && node scripts/generateIconRegistry.mjs"
42
+ "test": "node --test"
48
43
  },
49
44
  "peerDependencies": {
50
45
  "@bunnix/core": "^0.9.4"
@@ -14,6 +14,7 @@
14
14
  -webkit-user-select: none;
15
15
  min-height: 32px;
16
16
  font-size: 1rem;
17
+ font-weight: var(--font-weight-heavy);
17
18
  }
18
19
 
19
20
  .button:disabled {
package/src/core/core.css CHANGED
@@ -25,6 +25,8 @@
25
25
  --color-bg-secondary: light-dark(#EDEDED, #1A1A1A);
26
26
 
27
27
  --color-border-primary: light-dark(#D2D2D2, #3D3D3D);
28
+ --color-border-secondary: light-dark(#E0E0E0, #2E2E2E);
29
+ --color-border-tertiary: light-dark(#ECECEC, #242424);
28
30
  --color-outline: light-dark(#000, #fff);
29
31
  --color-outline-dimmed: light-dark(#444, #555);
30
32
 
@@ -398,12 +400,14 @@ body {
398
400
  }
399
401
 
400
402
  /* Layout positioning */
401
- .sticky-top {
403
+ .sticky-top,
404
+ [sticky="top"] {
402
405
  position: sticky;
403
406
  top: 0;
404
407
  }
405
408
 
406
- .sticky-bottom {
409
+ .sticky-bottom,
410
+ [sticky="bottom"] {
407
411
  position: sticky;
408
412
  bottom: 0;
409
413
  }
@@ -492,6 +496,16 @@ body {
492
496
  border: 1px solid var(--color-border-primary);
493
497
  }
494
498
 
499
+ .border-secondary,
500
+ [border="secondary"] {
501
+ border: 1px solid var(--color-border-secondary);
502
+ }
503
+
504
+ .border-tertiary,
505
+ [border="tertiary"] {
506
+ border: 1px solid var(--color-border-tertiary);
507
+ }
508
+
495
509
  .border-transparent,
496
510
  [border="transparent"] {
497
511
  border: 1px solid transparent;
@@ -2,7 +2,9 @@
2
2
  .dialog {
3
3
  border: 1px solid var(--color-border-primary);
4
4
  border-radius: var(--radius-lg);
5
- padding: var(--padding-sm);
5
+ padding: 0;
6
+ background-color: var(--color-bg-primary);
7
+ overflow: hidden;
6
8
  }
7
9
 
8
10
  .dialog::backdrop {
@@ -23,6 +23,14 @@ import { Icon } from "./media.mjs";
23
23
 
24
24
  const { dialog } = Bunnix;
25
25
 
26
+ const resolveDialogSize = (value) =>
27
+ typeof value === "number" ? `${value}px` : value;
28
+
29
+ const renderDialogTitle = (title) =>
30
+ typeof title === "string"
31
+ ? Heading({ h3: true, flexShrink: 0 }, title)
32
+ : title;
33
+
26
34
  /**
27
35
  * Creates a dialog controller and render component.
28
36
  *
@@ -35,6 +43,10 @@ export const useDialog = () => {
35
43
  const dialogState = useState({
36
44
  title: "",
37
45
  contents: [],
46
+ padding: "regular",
47
+ width: null,
48
+ height: null,
49
+ secondary: null,
38
50
  confirmation: { text: "OK", variant: "primary" },
39
51
  });
40
52
  const dialogRef = useRef(null);
@@ -50,28 +62,84 @@ export const useDialog = () => {
50
62
  dialogRef.current?.close();
51
63
  };
52
64
 
53
- return dialog(
54
- {
55
- ref: dialogRef,
56
- cancel: closeDialog,
57
- class: `dialog`,
58
- },
59
- Show(
60
- dialogState,
61
- (state) => (
65
+ return Show(
66
+ dialogState,
67
+ (state) => (
68
+ dialog(
69
+ {
70
+ ref: dialogRef,
71
+ cancel: closeDialog,
72
+ class: `dialog`,
73
+ style: {
74
+ ...(state.width ? { width: resolveDialogSize(state.width) } : {}),
75
+ ...(state.height ? { height: resolveDialogSize(state.height) } : {}),
76
+ },
77
+ },
62
78
  Column(
63
- { class: "padding-md", gap: 12, minWidth: 420, maxWidth: 580 },
79
+ {
80
+ gap: 12,
81
+ minWidth: state.width ? undefined : 420,
82
+ maxWidth: state.width ? undefined : 580,
83
+ maxHeight: state.height ? undefined : "calc(100vh - 32px)",
84
+ ...(state.width ? { fillWidth: true } : {}),
85
+ ...(state.height ? { fillHeight: true } : {}),
86
+ minHeight: 0,
87
+ overflow: "hidden",
88
+ },
64
89
  Row(
65
- { gap: 24, alignItems: "center" },
66
- Heading({ h3: true, flexShrink: 0 }, state.title),
90
+ {
91
+ gap: 24,
92
+ alignItems: "center",
93
+ paddingX: state.padding,
94
+ paddingTop: state.padding,
95
+ flexShrink: 0,
96
+ },
97
+ renderDialogTitle(state.title),
67
98
  Spacer(),
68
99
  Button(
69
100
  { variant: "tertiary", click: closeDialog },
70
- Icon({ name: "close", color: "secondary", size: 20 }),
101
+ Icon({ name: "xmark", color: "secondary", size: 20 }),
102
+ ),
103
+ ),
104
+ state.contents.length > 0 && Column(
105
+ {
106
+ flexGrow: 1,
107
+ minHeight: 0,
108
+ },
109
+ Column(
110
+ {
111
+ gap: 12,
112
+ paddingX: state.padding,
113
+ fillHeight: true,
114
+ minHeight: 0,
115
+ style: {
116
+ overflowY: "auto",
117
+ },
118
+ },
119
+ ...state.contents,
71
120
  ),
72
121
  ),
73
- state.contents.length > 0 && Column(...state.contents),
74
122
  Row(
123
+ {
124
+ paddingX: state.padding,
125
+ paddingBottom: state.padding,
126
+ bgColor: "primary",
127
+ flexShrink: 0,
128
+ },
129
+ ...(state.secondary ? [
130
+ Button(
131
+ {
132
+ minWidth: 80,
133
+ variant: state.secondary.variant,
134
+ click: () => {
135
+ let action = state.secondary.action;
136
+ (action) && action();
137
+ closeDialog();
138
+ }
139
+ },
140
+ state.secondary.text
141
+ ),
142
+ ] : []),
75
143
  Spacer(),
76
144
  Button(
77
145
  {
@@ -86,7 +154,7 @@ export const useDialog = () => {
86
154
  state.confirmation.text
87
155
  )
88
156
  ),
89
- )
157
+ ),
90
158
  )
91
159
  )
92
160
  );
@@ -95,8 +163,15 @@ export const useDialog = () => {
95
163
  * Opens dialog with provided content and confirmation behavior.
96
164
  *
97
165
  * @param {Object} [config] - Dialog config
98
- * @param {string} [config.title] - Dialog title text
166
+ * @param {string|*} [config.title] - Dialog title text or custom node
99
167
  * @param {Array|*} [config.contents=[]] - Renderable content block(s)
168
+ * @param {number|string} [config.padding="regular"] - Shared dialog section padding
169
+ * @param {number|string} [config.width] - Fixed dialog width
170
+ * @param {number|string} [config.height] - Fixed dialog height
171
+ * @param {Object} [config.secondary] - Optional secondary button config
172
+ * @param {string} [config.secondary.text="Cancel"] - Secondary label
173
+ * @param {string} [config.secondary.variant="secondary"] - Button variant
174
+ * @param {Function|null} [config.secondary.action=null] - Action run before close
100
175
  * @param {Object} [config.confirmation] - Confirmation button config
101
176
  * @param {string} [config.confirmation.text=\"OK\"] - Confirmation label
102
177
  * @param {string} [config.confirmation.variant=\"primary\"] - Button variant
@@ -106,14 +181,24 @@ export const useDialog = () => {
106
181
  showDialog: ({
107
182
  title,
108
183
  contents = [],
184
+ padding = "regular",
185
+ width = null,
186
+ height = null,
187
+ secondary,
109
188
  confirmation,
110
189
  } = {}) => {
190
+ const defaultSecondary = { text: "Cancel", variant: "secondary", action: null };
111
191
  const defaultConfirmation = { text: "OK", variant: "primary", action: null };
192
+ const mergedSecondary = secondary ? { ...defaultSecondary, ...secondary } : null;
112
193
  const mergedConfirmation = { ...defaultConfirmation, ...confirmation };
113
194
 
114
195
  dialogState.set({
115
196
  title,
116
197
  contents: Array.isArray(contents) ? contents : [contents],
198
+ padding,
199
+ width,
200
+ height,
201
+ secondary: mergedSecondary,
117
202
  confirmation: mergedConfirmation,
118
203
  });
119
204
  dialogRef.current?.showModal();