@egose/shadcn-theme-ng-tw 0.1.0 → 0.2.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 (86) hide show
  1. package/README.md +1 -1
  2. package/accordion/README.md +405 -2
  3. package/alert/README.md +372 -2
  4. package/alert-dialog/README.md +471 -5
  5. package/aspect-ratio/README.md +272 -5
  6. package/autocomplete/README.md +502 -2
  7. package/avatar/README.md +357 -5
  8. package/badge/README.md +318 -2
  9. package/basic-alert/README.md +353 -2
  10. package/breadcrumb/README.md +406 -5
  11. package/button/README.md +482 -2
  12. package/button/fesm2022/button.mjs +85 -107
  13. package/button/types/button.d.ts +5 -8
  14. package/button-group/README.md +318 -5
  15. package/button-group/fesm2022/button-group.mjs +1 -1
  16. package/calendar/README.md +357 -2
  17. package/card/README.md +331 -5
  18. package/carousel/README.md +333 -5
  19. package/carousel/fesm2022/carousel.mjs +4 -1
  20. package/checkbox/README.md +320 -2
  21. package/collapsible/README.md +332 -5
  22. package/combobox/README.md +507 -5
  23. package/combobox/fesm2022/combobox.mjs +4 -1
  24. package/command/README.md +435 -5
  25. package/confirmation-dialog/README.md +301 -2
  26. package/context-menu/README.md +366 -5
  27. package/date-picker/README.md +465 -2
  28. package/date-picker/fesm2022/date-picker.mjs +2 -2
  29. package/dialog/README.md +448 -2
  30. package/drawer/README.md +395 -5
  31. package/dropdown-menu/README.md +417 -5
  32. package/empty/README.md +329 -5
  33. package/field/README.md +385 -5
  34. package/form-checkbox/README.md +312 -2
  35. package/form-date-picker/README.md +322 -2
  36. package/form-field/README.md +356 -2
  37. package/form-field-simple/README.md +340 -2
  38. package/form-searchable-multiselect/README.md +361 -2
  39. package/form-select/README.md +350 -2
  40. package/form-text-input/README.md +371 -2
  41. package/form-textarea/README.md +347 -2
  42. package/hover-card/README.md +256 -5
  43. package/icon/README.md +239 -2
  44. package/input/README.md +269 -2
  45. package/input-group/README.md +335 -5
  46. package/input-group/fesm2022/input-group.mjs +3 -3
  47. package/input-otp/README.md +375 -5
  48. package/item/README.md +385 -5
  49. package/item/fesm2022/item.mjs +3 -3
  50. package/kbd/README.md +291 -5
  51. package/label/README.md +272 -2
  52. package/layout-simple/README.md +193 -2
  53. package/layout-simple/fesm2022/layout-simple.mjs +877 -409
  54. package/layout-simple/types/layout-simple.d.ts +174 -137
  55. package/menu/README.md +417 -2
  56. package/menubar/README.md +343 -5
  57. package/native-select/README.md +323 -5
  58. package/navigation-menu/README.md +369 -5
  59. package/package.json +1 -1
  60. package/pagination/README.md +388 -5
  61. package/popover/README.md +331 -2
  62. package/progress/README.md +311 -5
  63. package/radio-group/README.md +364 -2
  64. package/radio-group/fesm2022/radio-group.mjs +5 -1
  65. package/resizable/README.md +269 -5
  66. package/scroll-area/README.md +233 -5
  67. package/searchable-multiselect/README.md +323 -2
  68. package/select/README.md +437 -2
  69. package/separator/README.md +222 -2
  70. package/sheet/README.md +311 -2
  71. package/sidebar/README.md +457 -5
  72. package/skeleton/README.md +217 -5
  73. package/slider/README.md +273 -5
  74. package/slider/fesm2022/slider.mjs +17 -13
  75. package/sonner/README.md +346 -2
  76. package/spinner/README.md +284 -2
  77. package/switch/README.md +310 -2
  78. package/table/README.md +423 -5
  79. package/tabs/README.md +411 -2
  80. package/tabs/fesm2022/tabs.mjs +12 -2
  81. package/textarea/README.md +282 -5
  82. package/toggle/README.md +270 -5
  83. package/toggle-group/README.md +340 -5
  84. package/tooltip/README.md +269 -2
  85. package/typography/README.md +271 -5
  86. package/utils/README.md +303 -2
@@ -1,3 +1,365 @@
1
- # Radio Group Subpath
1
+ # Radio Group (`@egose/shadcn-theme-ng/radio-group`)
2
2
 
3
- This project ships only as `@egose/shadcn-theme-ng/radio-group` or `@egose/shadcn-theme-ng-tw/radio-group`. See the [package README](../../README.md) for installation, compatibility, Tailwind variant, test, and release guidance. Do not publish this project directory independently.
3
+ A shadcn/ui-style **Radio Group** for Angular — a set of mutually exclusive options where exactly one can be selected. This is the Angular equivalent of shadcn/ui `RadioGroup` / `RadioGroupItem`.
4
+
5
+ The primitives come from **spartan-ng/brain** (`BrnRadioGroup`, `BrnRadio`, `BrnFieldControlDescribedBy`): keyboard navigation (arrow keys), roving tabindex, and form integration are handled by `BrnRadioGroup`/`BrnRadio`, while this package adds the shadcn look (grid layout, circular indicator, focus ring, error styling).
6
+
7
+ > **Ships as:** `@egose/shadcn-theme-ng/radio-group` and `@egose/shadcn-theme-ng-tw/radio-group` (the `tw:`-prefixed Tailwind variant — same API, class strings prefixed with `tw:`).
8
+ > See the [package README](../../README.md) for installation, peer dependencies, and Tailwind setup. Do not publish this project directory independently.
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ # Plain Tailwind (no prefix)
14
+ npm install @egose/shadcn-theme-ng
15
+
16
+ # Or the tw:-prefixed variant
17
+ npm install @egose/shadcn-theme-ng-tw
18
+ ```
19
+
20
+ Import from the subpath (not the package root):
21
+
22
+ ```ts
23
+ import { HlmRadioGroupImports } from '@egose/shadcn-theme-ng/radio-group';
24
+ // tw variant:
25
+ // import { HlmRadioGroupImports } from '@egose/shadcn-theme-ng-tw/radio-group';
26
+ ```
27
+
28
+ Peer dependencies (see [package README](../../README.md) for versions): `@angular/core`, `@angular/common`, `@spartan-ng/brain`. `@angular/cdk` is required transitively by `HlmRadio` (boolean coercion).
29
+
30
+ ## Imports
31
+
32
+ Real exported symbols (from `src/public-api.ts`):
33
+
34
+ | Symbol | Kind | Description |
35
+ | ---------------------- | ------------- | ----------------------------------------------------------------- |
36
+ | `HlmRadioGroup` | Directive | Group container; forwards `BrnRadioGroup` |
37
+ | `HlmRadio` | Component | Single radio item (`hlm-radio`), wraps `BrnRadio` |
38
+ | `HlmRadioIndicator` | Component | Circular visual indicator dot |
39
+ | `HlmRadioGroupImports` | `const` array | `[HlmRadioGroup, HlmRadio, HlmRadioIndicator]` standalone imports |
40
+ | `HlmRadioGroupModule` | `NgModule` | NgModule wrapper re-exporting the three above |
41
+
42
+ Standalone usage:
43
+
44
+ ```ts
45
+ import { Component } from '@angular/core';
46
+ import { HlmRadioGroupImports } from '@egose/shadcn-theme-ng/radio-group';
47
+
48
+ @Component({
49
+ selector: 'app-demo',
50
+ standalone: true,
51
+ imports: [HlmRadioGroupImports],
52
+ template: `...`,
53
+ })
54
+ export class DemoComponent {}
55
+ ```
56
+
57
+ NgModule usage:
58
+
59
+ ```ts
60
+ import { NgModule } from '@angular/core';
61
+ import { HlmRadioGroupModule } from '@egose/shadcn-theme-ng/radio-group';
62
+
63
+ @NgModule({ imports: [HlmRadioGroupModule] })
64
+ export class DemoModule {}
65
+ ```
66
+
67
+ You can also import the pieces individually (`import { HlmRadioGroup, HlmRadio, HlmRadioIndicator } from '...'`).
68
+
69
+ ## Anatomy / Structure
70
+
71
+ ```html
72
+ <!-- Attribute form on a div -->
73
+ <div hlmRadioGroup name="plan" [value]="plan()" (valueChange)="plan.set($event)">
74
+ <hlm-radio value="free" inputId="plan-free">
75
+ <hlm-radio-indicator />
76
+ Free
77
+ </hlm-radio>
78
+
79
+ <hlm-radio value="pro" inputId="plan-pro">
80
+ <hlm-radio-indicator />
81
+ Pro
82
+ </hlm-radio>
83
+ </div>
84
+
85
+ <!-- Element form also works -->
86
+ <hlm-radio-group name="plan">
87
+ <hlm-radio value="free"><hlm-radio-indicator />Free</hlm-radio>
88
+ </hlm-radio-group>
89
+ ```
90
+
91
+ Real selectors:
92
+
93
+ | Selector | Class | Notes |
94
+ | ------------------------------------ | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
95
+ | `[hlmRadioGroup]`, `hlm-radio-group` | `HlmRadioGroup` | Group host; `data-slot="radio-group"` |
96
+ | `hlm-radio` | `HlmRadio<T>` | Item host; `data-slot="radio-group-item"`; projects `[target],[indicator],hlm-radio-indicator` into the indicator slot, everything else as label content |
97
+ | `hlm-radio-indicator` | `HlmRadioIndicator` | Visual dot; `data-slot="radio-group-indicator"` |
98
+
99
+ ## API reference
100
+
101
+ ### HlmRadioGroup (directive)
102
+
103
+ Thin directive wrapper over `BrnRadioGroup` (plus `BrnFieldControlDescribedBy` for form-field `aria-describedby` wiring). Own inputs:
104
+
105
+ | Input | Type | Default | Description |
106
+ | --------------------- | ------------ | ------- | -------------------------------------------- |
107
+ | `class` (`userClass`) | `ClassValue` | `''` | Extra classes appended to `tw:grid tw:gap-3` |
108
+
109
+ Forwarded `BrnRadioGroup` host-directive bindings:
110
+
111
+ | Binding | Kind | Description |
112
+ | ------------- | ------ | ---------------------------------------- |
113
+ | `name` | input | Radio group name (native input grouping) |
114
+ | `value` | input | Currently selected value |
115
+ | `disabled` | input | Disables the whole group |
116
+ | `required` | input | Marks the group as required |
117
+ | `valueChange` | output | Emits the newly selected value |
118
+
119
+ The host also reflects form state as attributes: `aria-invalid`/`data-invalid` when the bound control is invalid, plus `data-dirty` and `data-touched`.
120
+
121
+ ### HlmRadio\<T\> (component)
122
+
123
+ | Input | Type | Default | Description |
124
+ | -------------------------------------- | --------------------- | ------------ | -------------------------------------------------------------------------------------------------- |
125
+ | `value` | `T` | **required** | The value this item represents |
126
+ | `inputId` | `string \| undefined` | `undefined` | `id` placed on the underlying `brn-radio` element; also used to find an associated `<label [for]>` |
127
+ | `aria-label` (`ariaLabel`) | `string \| undefined` | `undefined` | Accessible name when there is no visible label |
128
+ | `aria-labelledby` (`ariaLabelledby`) | `string \| undefined` | `undefined` | Id(s) of labelling element(s) |
129
+ | `aria-describedby` (`ariaDescribedby`) | `string \| undefined` | `undefined` | Id(s) of describing element(s) |
130
+ | `required` | `boolean` | `false` | Native required flag (boolean-coerced) |
131
+ | `disabled` | `boolean` | `false` | Disables this item (boolean-coerced); mirrors `data-disabled` onto an associated `<label>` |
132
+ | `class` (`userClass`) | `ClassValue` | `''` | Extra classes |
133
+
134
+ | Output | Type | Description |
135
+ | -------- | ------------------- | ---------------------------------------------- |
136
+ | `change` | `BrnRadioChange<T>` | Emitted when this item's checked state changes |
137
+
138
+ Label association detail: on init the component looks for `closest('label')`, falling back to `document.querySelector('label[for=inputId]')`, and mirrors `data-disabled="true"/"false"` onto that label so label styling follows the disabled state. IDs containing special characters (e.g. quotes/brackets) are matched safely via `htmlFor` comparison, not a CSS selector.
139
+
140
+ ### HlmRadioIndicator (component)
141
+
142
+ No inputs/outputs. Renders the styled outer circle; the inner dot fills via `group-data-[checked=true]` when the parent `brn-radio` reports checked. Always place it inside `hlm-radio` (it matches the `hlm-radio-indicator` content slot).
143
+
144
+ ## Examples
145
+
146
+ ### 1. Basic usage
147
+
148
+ ```ts
149
+ import { Component, signal } from '@angular/core';
150
+ import { HlmRadioGroupImports } from '@egose/shadcn-theme-ng/radio-group';
151
+
152
+ @Component({
153
+ selector: 'app-basic-radio',
154
+ standalone: true,
155
+ imports: [HlmRadioGroupImports],
156
+ template: `
157
+ <div hlmRadioGroup name="fruit" [value]="fruit()" (valueChange)="fruit.set($event)">
158
+ <hlm-radio value="apple" inputId="fruit-apple">
159
+ <hlm-radio-indicator />
160
+ Apple
161
+ </hlm-radio>
162
+ <hlm-radio value="banana" inputId="fruit-banana">
163
+ <hlm-radio-indicator />
164
+ Banana
165
+ </hlm-radio>
166
+ <hlm-radio value="orange" inputId="fruit-orange">
167
+ <hlm-radio-indicator />
168
+ Orange
169
+ </hlm-radio>
170
+ </div>
171
+ <p>Selected: {{ fruit() }}</p>
172
+ `,
173
+ })
174
+ export class BasicRadioComponent {
175
+ readonly fruit = signal('apple');
176
+ }
177
+ ```
178
+
179
+ ### 2. Element selector form + external labels
180
+
181
+ `hlm-radio-group` works as an element, and labels can live outside the item via `for`/`inputId`:
182
+
183
+ ```ts
184
+ import { Component, signal } from '@angular/core';
185
+ import { HlmRadioGroupImports } from '@egose/shadcn-theme-ng/radio-group';
186
+
187
+ @Component({
188
+ selector: 'app-labelled-radio',
189
+ standalone: true,
190
+ imports: [HlmRadioGroupImports],
191
+ template: `
192
+ <hlm-radio-group name="contact">
193
+ <label for="c-email">Email me</label>
194
+ <hlm-radio value="email" inputId="c-email"><hlm-radio-indicator /></hlm-radio>
195
+
196
+ <label for="c-sms">Text me</label>
197
+ <hlm-radio value="sms" inputId="c-sms"><hlm-radio-indicator /></hlm-radio>
198
+ </hlm-radio-group>
199
+ `,
200
+ })
201
+ export class LabelledRadioComponent {}
202
+ ```
203
+
204
+ Wrapping the item in a `<label>` also works — the item finds it with `closest('label')`:
205
+
206
+ ```html
207
+ <hlm-radio-group name="contact">
208
+ <label>
209
+ <hlm-radio value="email"><hlm-radio-indicator /></hlm-radio>
210
+ Email me
211
+ </label>
212
+ </hlm-radio-group>
213
+ ```
214
+
215
+ ### 3. Reactive forms
216
+
217
+ `BrnRadioGroup` is a `ControlValueAccessor`, so `formControlName`/`formControl` bind on the group host:
218
+
219
+ ```ts
220
+ import { Component } from '@angular/core';
221
+ import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
222
+ import { HlmRadioGroupImports } from '@egose/shadcn-theme-ng/radio-group';
223
+
224
+ @Component({
225
+ selector: 'app-reactive-radio',
226
+ standalone: true,
227
+ imports: [HlmRadioGroupImports, ReactiveFormsModule],
228
+ template: `
229
+ <form [formGroup]="form" (ngSubmit)="submit()">
230
+ <div hlmRadioGroup formControlName="plan">
231
+ <hlm-radio value="hobby"><hlm-radio-indicator />Hobby</hlm-radio>
232
+ <hlm-radio value="pro"><hlm-radio-indicator />Pro</hlm-radio>
233
+ <hlm-radio value="enterprise"><hlm-radio-indicator />Enterprise</hlm-radio>
234
+ </div>
235
+ @if (form.controls.plan.invalid && form.controls.plan.touched) {
236
+ <p class="tw:text-destructive tw:text-sm">Please pick a plan.</p>
237
+ }
238
+ <button type="submit">Continue</button>
239
+ </form>
240
+ `,
241
+ })
242
+ export class ReactiveRadioComponent {
243
+ readonly form = new FormGroup({
244
+ plan: new FormControl<string | null>(null, Validators.required),
245
+ });
246
+
247
+ submit(): void {
248
+ this.form.markAllAsTouched();
249
+ console.log(this.form.value);
250
+ }
251
+ }
252
+ ```
253
+
254
+ When the control is invalid + touched, the group automatically gets `data-invalid="true"` (and destructive text styling) via the forwarded control state — no manual class juggling needed.
255
+
256
+ ### 4. Disabled states (group vs item)
257
+
258
+ ```ts
259
+ import { Component, signal } from '@angular/core';
260
+ import { HlmRadioGroupImports } from '@egose/shadcn-theme-ng/radio-group';
261
+
262
+ @Component({
263
+ selector: 'app-disabled-radio',
264
+ standalone: true,
265
+ imports: [HlmRadioGroupImports],
266
+ template: `
267
+ <!-- Whole group disabled -->
268
+ <div hlmRadioGroup name="a" value="one" disabled>
269
+ <hlm-radio value="one"><hlm-radio-indicator />One</hlm-radio>
270
+ <hlm-radio value="two"><hlm-radio-indicator />Two</hlm-radio>
271
+ </div>
272
+
273
+ <!-- Single item disabled; its <label> gets data-disabled="true" -->
274
+ <div hlmRadioGroup name="b" [value]="choice()" (valueChange)="choice.set($event)">
275
+ <label for="b-one">One (soon unavailable)</label>
276
+ <hlm-radio value="one" inputId="b-one" disabled><hlm-radio-indicator /></hlm-radio>
277
+ <label for="b-two">Two</label>
278
+ <hlm-radio value="two" inputId="b-two"><hlm-radio-indicator /></hlm-radio>
279
+ </div>
280
+ `,
281
+ })
282
+ export class DisabledRadioComponent {
283
+ readonly choice = signal('two');
284
+ }
285
+ ```
286
+
287
+ ### 5. Per-item change events + typed values
288
+
289
+ `HlmRadio` is generic — `value` can be any type, and `change` emits `BrnRadioChange<T>`:
290
+
291
+ ```ts
292
+ import { Component, signal } from '@angular/core';
293
+ import { HlmRadioGroupImports } from '@egose/shadcn-theme-ng/radio-group';
294
+ import type { BrnRadioChange } from '@spartan-ng/brain/radio-group';
295
+
296
+ interface Tier {
297
+ id: string;
298
+ price: number;
299
+ }
300
+
301
+ @Component({
302
+ selector: 'app-typed-radio',
303
+ standalone: true,
304
+ imports: [HlmRadioGroupImports],
305
+ template: `
306
+ <div hlmRadioGroup name="tier" (valueChange)="onGroupChange($event)">
307
+ @for (tier of tiers; track tier.id) {
308
+ <hlm-radio [value]="tier" (change)="onItemChange($event)">
309
+ <hlm-radio-indicator />
310
+ {{ tier.id }} — ${{ tier.price }}/mo
311
+ </hlm-radio>
312
+ }
313
+ </div>
314
+ `,
315
+ })
316
+ export class TypedRadioComponent {
317
+ readonly tiers: Tier[] = [
318
+ { id: 'starter', price: 0 },
319
+ { id: 'growth', price: 29 },
320
+ ];
321
+
322
+ onGroupChange(value: Tier): void {
323
+ console.log('group selected:', value.id);
324
+ }
325
+
326
+ onItemChange(event: BrnRadioChange<Tier>): void {
327
+ console.log('item checked:', event.value.id, event.checked);
328
+ }
329
+ }
330
+ ```
331
+
332
+ ### 6. Custom indicator content (target slot)
333
+
334
+ Anything projected with `[target]` or `[indicator]` goes into the indicator slot instead of the default label position:
335
+
336
+ ```html
337
+ <div hlmRadioGroup name="layout">
338
+ <hlm-radio value="grid">
339
+ <span target class="tw:flex tw:items-center tw:gap-2">
340
+ <hlm-radio-indicator />
341
+ <strong>Grid</strong>
342
+ </span>
343
+ <span class="tw:text-muted-foreground tw:text-sm">Cards in a grid</span>
344
+ </hlm-radio>
345
+ </div>
346
+ ```
347
+
348
+ ## Accessibility notes
349
+
350
+ - The group uses the native `radiogroup` semantics from `BrnRadioGroup`, including arrow-key navigation and roving tabindex — keep all `hlm-radio` items inside one group container.
351
+ - Always provide `name` so assistive tech (and native form serialization) treats the items as one group.
352
+ - Prefer visible text content inside `hlm-radio`; use `aria-label`/`aria-labelledby` only when the item has no visible label.
353
+ - Disabled items expose `data-disabled` (and the native disabled state on the inner input) and are skipped in keyboard navigation.
354
+ - Invalid form state is announced via `aria-invalid="true"` on the group host.
355
+
356
+ ## Theming / CSS variables
357
+
358
+ Styling is class-driven (no component-specific CSS variables). Override via the `class` input on any of the three pieces; the indicator's checked dot keys off `group-data-[checked=true]`, and the group invalid state off `data-[invalid=true]`.
359
+
360
+ ## Related subpaths
361
+
362
+ - `@egose/shadcn-theme-ng/label` — labelling radio items and form rows
363
+ - `@egose/shadcn-theme-ng/field` / `form-field` — form rows, descriptions, and error text wired via `BrnFieldControlDescribedBy`
364
+ - `@egose/shadcn-theme-ng/checkbox` — multi-select counterpart
365
+ - `@egose/shadcn-theme-ng/form-field-simple` — lightweight wrapper for reactive-form controls
@@ -136,7 +136,11 @@ class HlmRadioGroup {
136
136
  _errorState = computed(() => this._brnRadioGroup.controlState?.()?.spartanInvalid, /* @ts-ignore */
137
137
  ...(ngDevMode ? [{ debugName: "_errorState" }] : /* istanbul ignore next */ []));
138
138
  constructor() {
139
- classes(() => ['tw:grid tw:gap-3', this.userClass(), this._errorState() ? 'tw:data-[invalid=true]:text-destructive' : '']);
139
+ classes(() => [
140
+ 'tw:grid tw:gap-3',
141
+ this.userClass(),
142
+ this._errorState() ? 'tw:data-[invalid=true]:text-destructive' : '',
143
+ ]);
140
144
  }
141
145
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmRadioGroup, deps: [], target: i0.ɵɵFactoryTarget.Directive });
142
146
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.3", type: HlmRadioGroup, isStandalone: true, selector: "[hlmRadioGroup],hlm-radio-group", inputs: { userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "data-slot": "radio-group" }, properties: { "attr.aria-invalid": "_ariaInvalid() ? \"true\" : null", "attr.data-invalid": "_ariaInvalid() ? \"true\" : null", "attr.data-dirty": "_dirty() ? \"true\" : null", "attr.data-touched": "_touched() ? \"true\" : null" } }, hostDirectives: [{ directive: i1.BrnRadioGroup, inputs: ["name", "name", "value", "value", "disabled", "disabled", "required", "required"], outputs: ["valueChange", "valueChange"] }, { directive: i2.BrnFieldControlDescribedBy }], ngImport: i0 });
@@ -1,11 +1,275 @@
1
- # Resizable
1
+ # Resizable (`@egose/shadcn-theme-ng/resizable`)
2
2
 
3
- This project was generated using [Angular CLI](https://github.com/angular/angular-cli).
3
+ A shadcn/ui-style **Resizable** panel layout — draggable split views (side-by-side or stacked) with collapsible panels. This is the Angular equivalent of shadcn/ui `ResizablePanelGroup` / `ResizablePanel` / `ResizableHandle`.
4
4
 
5
- ## Building
5
+ The behavior comes from **spartan-ng/brain** (`BrnResizableGroup`, `BrnResizablePanel`, `BrnResizableHandle`): pointer-drag resizing, panel size constraints, and layout events. This package is a thin styling/directive layer — a flex group container, constraint-forwarding panel directive with a `setSize` helper, and a handle with an optional grip pill.
6
6
 
7
- To build the library, run:
7
+ > **Ships as:** `@egose/shadcn-theme-ng/resizable` and `@egose/shadcn-theme-ng-tw/resizable` (the `tw:`-prefixed Tailwind variant — same API, class strings prefixed with `tw:`).
8
+ > See the [package README](../../README.md) for installation, peer dependencies, and Tailwind setup. Do not publish this project directory independently.
9
+
10
+ ## Installation
8
11
 
9
12
  ```bash
10
- ng build resizable
13
+ # Plain Tailwind (no prefix)
14
+ npm install @egose/shadcn-theme-ng
15
+
16
+ # Or the tw:-prefixed variant
17
+ npm install @egose/shadcn-theme-ng-tw
18
+ ```
19
+
20
+ ```ts
21
+ import { HlmResizableImports } from '@egose/shadcn-theme-ng/resizable';
22
+ // tw variant:
23
+ // import { HlmResizableImports } from '@egose/shadcn-theme-ng-tw/resizable';
24
+ ```
25
+
26
+ Peer dependencies (see [package README](../../README.md) for versions): `@angular/core`, `@angular/common`, `@spartan-ng/brain`.
27
+
28
+ ## Imports
29
+
30
+ Real exported symbols (from `src/public-api.ts`):
31
+
32
+ | Symbol | Kind | Description |
33
+ | --------------------- | ------------- | ------------------------------------------------------------------------------------- |
34
+ | `HlmResizableGroup` | Directive | Flex row/column container; forwards `BrnResizableGroup` |
35
+ | `HlmResizablePanel` | Directive | Size-constrained panel; forwards `BrnResizablePanel`, `exportAs: 'hlmResizablePanel'` |
36
+ | `HlmResizableHandle` | Component | Drag handle with optional grip pill; `exportAs: 'hlmResizableHandle'` |
37
+ | `HlmResizableImports` | `const` array | `[HlmResizableGroup, HlmResizablePanel, HlmResizableHandle]` standalone imports |
38
+ | `HlmResizableModule` | `NgModule` | NgModule wrapper re-exporting the three above |
39
+
40
+ Standalone usage:
41
+
42
+ ```ts
43
+ import { Component } from '@angular/core';
44
+ import { HlmResizableImports } from '@egose/shadcn-theme-ng/resizable';
45
+
46
+ @Component({
47
+ selector: 'app-demo',
48
+ standalone: true,
49
+ imports: [HlmResizableImports],
50
+ template: `...`,
51
+ })
52
+ export class DemoComponent {}
53
+ ```
54
+
55
+ NgModule usage:
56
+
57
+ ```ts
58
+ import { NgModule } from '@angular/core';
59
+ import { HlmResizableModule } from '@egose/shadcn-theme-ng/resizable';
60
+
61
+ @NgModule({ imports: [HlmResizableModule] })
62
+ export class DemoModule {}
63
+ ```
64
+
65
+ ## Anatomy / Structure
66
+
67
+ ```html
68
+ <div hlmResizableGroup direction="horizontal">
69
+ <div hlmResizablePanel defaultSize="25">Sidebar</div>
70
+
71
+ <hlm-resizable-handle withHandle />
72
+
73
+ <div hlmResizablePanel defaultSize="75">Main content</div>
74
+ </div>
75
+ ```
76
+
77
+ Real selectors:
78
+
79
+ | Selector | Class | Notes |
80
+ | -------------------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------ |
81
+ | `[hlmResizableGroup]`, `hlm-resizable-group` | `HlmResizableGroup` | `data-slot="resizable-group"`; row by default, column when the panel-group direction is vertical |
82
+ | `[hlmResizablePanel]`, `hlm-resizable-panel` | `HlmResizablePanel` | `data-slot="resizable-panel"` |
83
+ | `hlm-resizable-handle` | `HlmResizableHandle` | `data-slot="resizable-handle"`; renders a grip `<div>` only when `withHandle` is true |
84
+
85
+ ## API reference
86
+
87
+ ### HlmResizableGroup (directive)
88
+
89
+ Thin directive wrapper — no own inputs. Forwarded `BrnResizableGroup` bindings:
90
+
91
+ | Binding | Kind | Description |
92
+ | -------------- | ------ | --------------------------------------------- |
93
+ | `direction` | input | `'horizontal' \| 'vertical'` layout direction |
94
+ | `layout` | input | Controlled panel layout (sizes) |
95
+ | `dragStart` | output | Emitted when a drag resize starts |
96
+ | `dragEnd` | output | Emitted when a drag resize ends |
97
+ | `layoutChange` | output | Emitted when the panel layout changes |
98
+
99
+ ### HlmResizablePanel (directive)
100
+
101
+ Thin directive wrapper plus one helper method. No own inputs. Forwarded `BrnResizablePanel` inputs:
102
+
103
+ | Input | Description |
104
+ | ------------- | ------------------------------------------------ |
105
+ | `defaultSize` | Initial size (percentage of the group) |
106
+ | `id` | Panel id (used for persisted/controlled layouts) |
107
+ | `collapsible` | Whether the panel can collapse to zero |
108
+ | `minSize` | Minimum size (percentage) |
109
+ | `maxSize` | Maximum size (percentage) |
110
+
111
+ | Method | Signature | Description |
112
+ | --------- | ----------------------------- | --------------------------------------------------------------------------- |
113
+ | `setSize` | `setSize(size: number): void` | Programmatically resize the panel; delegates to `BrnResizablePanel.setSize` |
114
+
115
+ Because the directive is `exportAs: 'hlmResizablePanel'`, grab it with a template reference: `<div hlmResizablePanel #panel="hlmResizablePanel">` then call `panel.setSize(50)`.
116
+
117
+ ### HlmResizableHandle (component)
118
+
119
+ | Input (forwarded to `BrnResizableHandle`) | Description |
120
+ | ----------------------------------------- | ------------------------------------ |
121
+ | `withHandle` | Show the grip pill inside the handle |
122
+ | `disabled` | Disable dragging on this handle |
123
+
124
+ When `withHandle` is true the component renders a short rounded pill (`<div class="...">`) centered in the handle; otherwise the handle is just the thin hover/drag strip. Hover cursors adapt to direction (`ew-resize` horizontal, `ns-resize` vertical).
125
+
126
+ ## Examples
127
+
128
+ ### 1. Basic horizontal split
129
+
130
+ ```ts
131
+ import { Component } from '@angular/core';
132
+ import { HlmResizableImports } from '@egose/shadcn-theme-ng/resizable';
133
+
134
+ @Component({
135
+ selector: 'app-basic-resizable',
136
+ standalone: true,
137
+ imports: [HlmResizableImports],
138
+ template: `
139
+ <div hlmResizableGroup direction="horizontal" class="tw:h-64 tw:rounded-lg tw:border">
140
+ <div hlmResizablePanel defaultSize="30" class="tw:p-4">Nav</div>
141
+ <hlm-resizable-handle />
142
+ <div hlmResizablePanel defaultSize="70" class="tw:p-4">Content</div>
143
+ </div>
144
+ `,
145
+ })
146
+ export class BasicResizableComponent {}
147
+ ```
148
+
149
+ ### 2. Handle with grip pill
150
+
151
+ ```html
152
+ <div hlmResizableGroup direction="horizontal" class="tw:h-64 tw:rounded-lg tw:border">
153
+ <div hlmResizablePanel defaultSize="25" class="tw:p-4">Files</div>
154
+
155
+ <hlm-resizable-handle withHandle />
156
+
157
+ <div hlmResizablePanel defaultSize="75" class="tw:p-4">Editor</div>
158
+ </div>
159
+ ```
160
+
161
+ ### 3. Vertical stacking
162
+
163
+ ```ts
164
+ import { Component } from '@angular/core';
165
+ import { HlmResizableImports } from '@egose/shadcn-theme-ng/resizable';
166
+
167
+ @Component({
168
+ selector: 'app-vertical-resizable',
169
+ standalone: true,
170
+ imports: [HlmResizableImports],
171
+ template: `
172
+ <div hlmResizableGroup direction="vertical" class="tw:h-96 tw:rounded-lg tw:border">
173
+ <div hlmResizablePanel defaultSize="40" class="tw:p-4">Preview</div>
174
+ <hlm-resizable-handle withHandle />
175
+ <div hlmResizablePanel defaultSize="60" class="tw:p-4">Console</div>
176
+ </div>
177
+ `,
178
+ })
179
+ export class VerticalResizableComponent {}
180
+ ```
181
+
182
+ The group flips to `flex-col` automatically for vertical direction, and the handle renders horizontally with an `ns-resize` cursor.
183
+
184
+ ### 4. Constrained + collapsible panels
185
+
186
+ ```html
187
+ <div hlmResizableGroup direction="horizontal" class="tw:h-64 tw:rounded-lg tw:border">
188
+ <div hlmResizablePanel defaultSize="20" minSize="10" maxSize="40" collapsible class="tw:p-4">
189
+ Collapsible sidebar (10–40%)
190
+ </div>
191
+ <hlm-resizable-handle withHandle />
192
+ <div hlmResizablePanel defaultSize="80" minSize="30" class="tw:p-4">Main (≥30%)</div>
193
+ </div>
11
194
  ```
195
+
196
+ ### 5. Programmatic control via `setSize`
197
+
198
+ ```ts
199
+ import { Component } from '@angular/core';
200
+ import { HlmResizableImports } from '@egose/shadcn-theme-ng/resizable';
201
+
202
+ @Component({
203
+ selector: 'app-controlled-resizable',
204
+ standalone: true,
205
+ imports: [HlmResizableImports],
206
+ template: `
207
+ <div class="tw:mb-2 tw:flex tw:gap-2">
208
+ <button type="button" (click)="left.setSize(20)">Sidebar 20%</button>
209
+ <button type="button" (click)="left.setSize(50)">Split 50/50</button>
210
+ </div>
211
+ <div hlmResizableGroup direction="horizontal" class="tw:h-64 tw:rounded-lg tw:border">
212
+ <div hlmResizablePanel #left="hlmResizablePanel" defaultSize="30" class="tw:p-4">Nav</div>
213
+ <hlm-resizable-handle withHandle />
214
+ <div hlmResizablePanel defaultSize="70" class="tw:p-4">Content</div>
215
+ </div>
216
+ `,
217
+ })
218
+ export class ControlledResizableComponent {}
219
+ ```
220
+
221
+ ### 6. Layout events + three-pane IDE layout
222
+
223
+ ```ts
224
+ import { Component, signal } from '@angular/core';
225
+ import { HlmResizableImports } from '@egose/shadcn-theme-ng/resizable';
226
+
227
+ @Component({
228
+ selector: 'app-ide-resizable',
229
+ standalone: true,
230
+ imports: [HlmResizableImports],
231
+ template: `
232
+ <div
233
+ hlmResizableGroup
234
+ direction="horizontal"
235
+ class="tw:h-96 tw:rounded-lg tw:border"
236
+ (dragStart)="dragging.set(true)"
237
+ (dragEnd)="dragging.set(false)"
238
+ (layoutChange)="onLayout($event)"
239
+ >
240
+ <div hlmResizablePanel defaultSize="20" minSize="12" collapsible class="tw:p-4">Explorer</div>
241
+ <hlm-resizable-handle withHandle />
242
+ <div hlmResizablePanel defaultSize="55" minSize="30" class="tw:p-4">Editor</div>
243
+ <hlm-resizable-handle />
244
+ <div hlmResizablePanel defaultSize="25" minSize="15" collapsible class="tw:p-4">Outline</div>
245
+ </div>
246
+ @if (dragging()) {
247
+ <p class="tw:text-muted-foreground tw:text-sm">Resizing…</p>
248
+ }
249
+ `,
250
+ })
251
+ export class IdeResizableComponent {
252
+ readonly dragging = signal(false);
253
+
254
+ onLayout(layout: unknown): void {
255
+ console.log('panel layout:', layout);
256
+ }
257
+ }
258
+ ```
259
+
260
+ ## Accessibility notes
261
+
262
+ - The drag handle is keyboard-focusable via the underlying `BrnResizableHandle` — ensure handles remain in the tab order (do not set `tabindex="-1"` on them).
263
+ - A handle without `withHandle` is only a 1px strip visually; prefer `withHandle` when pointer users need a bigger grab target, and keep `disabled` handles out of confusing layouts (they still occupy space).
264
+ - Give each region a landmark or label (`role="region"` + `aria-label`, or headings) so screen-reader users understand the pane structure, since drag-to-resize itself is pointer/keyboard driven on the handle.
265
+
266
+ ## Theming / CSS variables
267
+
268
+ Class-driven only (flex direction, borders, cursors). Size the group with your own height/width utilities — panels size themselves as percentages of it.
269
+
270
+ ## Related subpaths
271
+
272
+ - `@egose/shadcn-theme-ng/card` — paneled content inside resizable regions
273
+ - `@egose/shadcn-theme-ng/scroll-area` — scrollable overflow inside fixed-size panels
274
+ - `@egose/shadcn-theme-ng/sidebar` — app-level collapsible navigation (alternative to a resizable nav pane)
275
+ - `@egose/shadcn-theme-ng/separator` — static visual dividers where resizing is not needed