diff --git a/projects/docs/src/app/pages/docs/components/checkbox/checkbox.variants.ts b/projects/docs/src/app/pages/docs/components/checkbox/checkbox.variants.ts index 715b0b6..76a480b 100644 --- a/projects/docs/src/app/pages/docs/components/checkbox/checkbox.variants.ts +++ b/projects/docs/src/app/pages/docs/components/checkbox/checkbox.variants.ts @@ -10,19 +10,19 @@ import { UiCheckbox, UiLabel, UiButton, UiFormField, UiDescription } from 'ui'; template: `
- +
- +

@@ -31,23 +31,23 @@ import { UiCheckbox, UiLabel, UiButton, UiFormField, UiDescription } from 'ui';

- +
- @if (notificationsChecked()) { +

Enable notifications @@ -79,11 +79,11 @@ export class CheckboxDefaultExample { selector: 'checkbox-with-label-example', template: `

- +
`, @@ -101,11 +101,11 @@ export class CheckboxDefaultExample { template: `
- +

@@ -129,11 +129,11 @@ export class CheckboxWithDescriptionExample { template: `

- +
@@ -154,25 +154,23 @@ export class CheckboxDisabledExample { selector: 'checkbox-custom-styling-example', template: `
-
- - @if (checked()) { - -
-

- Enable notifications -

-

- You can enable or disable notifications at any time. -

-
+
+ +
+

+ Enable notifications +

+

+ You can enable or disable notifications at any time. +

+
`, standalone: true, imports: [UiCheckbox, NgIcon, UiFormField, UiDescription, UiLabel], @@ -198,66 +196,66 @@ export class CheckboxCustomStylingExample {
- +
- +
- +
- +
- +
- +
@@ -320,37 +318,37 @@ export const checkboxMeta: IComponentMeta = { description: 'A control that allows the user to toggle between checked and not checked.', installation: { import: `import { UiCheckbox } from '@workspace/ui/directives/checkbox';`, - usage: ` + usage: `` }, api: { props: [ { - name: 'uiCheckboxChecked', + name: 'checked', type: 'boolean', default: 'false', description: 'Whether the checkbox is checked.', required: false }, { - name: 'uiCheckboxIndeterminate', + name: 'indeterminate', type: 'boolean', default: 'false', description: 'Whether the checkbox is in an indeterminate state.', required: false }, { - name: 'uiCheckboxRequired', + name: 'required', type: 'boolean', default: 'false', description: 'Whether the checkbox is required.', required: false }, { - name: 'uiCheckboxDisabled', + name: 'disabled', type: 'boolean', default: 'false', description: 'Whether the checkbox is disabled.', @@ -366,12 +364,12 @@ export const checkboxMeta: IComponentMeta = { ], outputs: [ { - name: 'uiCheckboxCheckedChange', + name: 'checkedChange', type: 'boolean', description: 'Emitted when the checked state changes.' }, { - name: 'uiCheckboxIndeterminateChange', + name: 'indeterminateChange', type: 'boolean', description: 'Emitted when the indeterminate state changes.' } @@ -393,19 +391,19 @@ import { UiCheckbox, UiLabel, UiFormField, UiDescription } from '@workspace/ui/d template: \`
- +
- +

@@ -414,23 +412,23 @@ import { UiCheckbox, UiLabel, UiFormField, UiDescription } from '@workspace/ui/d

- +
- @if (notificationsChecked()) { +

Enable notifications @@ -467,11 +465,11 @@ import { UiCheckbox, UiLabel, NgIcon, UiFormField } from '@workspace/ui/directiv selector: 'checkbox-with-label-example', template: \`

- +
\`, @@ -497,11 +495,11 @@ import { UiCheckbox, UiLabel, UiFormField, UiDescription } from '@workspace/ui/d template: \`
- +

@@ -533,11 +531,11 @@ import { UiCheckbox, UiLabel, UiFormField } from '@workspace/ui/directives/check template: \`

- +
@@ -564,14 +562,14 @@ import { UiCheckbox, UiFormField, UiDescription } from '@workspace/ui/directives template: \`
- - @if (checked()) { +

Enable notifications @@ -614,66 +612,66 @@ import { UiCheckbox, UiLabel, UiButton, UiFormField } from '@workspace/ui/direct

- +
- +
- +
- +
- +
- +
diff --git a/projects/docs/src/app/pages/docs/components/components.routes.ts b/projects/docs/src/app/pages/docs/components/components.routes.ts index 224ba08..4700696 100644 --- a/projects/docs/src/app/pages/docs/components/components.routes.ts +++ b/projects/docs/src/app/pages/docs/components/components.routes.ts @@ -100,6 +100,10 @@ export const routes: Routes = [ path: 'skeleton', loadComponent: () => import('./skeleton/skeleton').then(m => m.Skeleton) }, + { + path: 'switch', + loadComponent: () => import('./switch/switch').then(m => m.Switch) + }, { path: '', redirectTo: 'alert', diff --git a/projects/docs/src/app/pages/docs/components/switch/switch.ts b/projects/docs/src/app/pages/docs/components/switch/switch.ts new file mode 100644 index 0000000..5f137d6 --- /dev/null +++ b/projects/docs/src/app/pages/docs/components/switch/switch.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +import { ComponentPreview } from '@components/component-preview/component-preview'; +import { switchVariants, switchMeta } from './switch.variants'; + +@Component({ + selector: 'docs-switch', + imports: [ComponentPreview], + template: ` + + + ` +}) +export class Switch { + switchMeta = switchMeta; + switchVariants = switchVariants; +} diff --git a/projects/docs/src/app/pages/docs/components/switch/switch.variants.ts b/projects/docs/src/app/pages/docs/components/switch/switch.variants.ts new file mode 100644 index 0000000..6890e4b --- /dev/null +++ b/projects/docs/src/app/pages/docs/components/switch/switch.variants.ts @@ -0,0 +1,269 @@ +import { Component, model } from '@angular/core'; +import { UiFormField, UiLabel, UiSwitch, UiSwitchThumb } from 'ui'; +import { IVariant, IComponentMeta } from '@components/component-preview/component-preview'; + +// Switch example components for dynamic rendering +@Component({ + selector: 'switch-default-example', + template: ` +
+ + +
+ `, + imports: [UiSwitch, UiSwitchThumb, UiFormField, UiLabel] +}) +export class SwitchDefaultExample { + checked = model(false); +} + +@Component({ + selector: 'switch-disabled-example', + template: ` +
+ + +
+ `, + imports: [UiSwitch, UiSwitchThumb, UiFormField, UiLabel] +}) +export class SwitchDisabledExample { + checked = model(false); +} + +@Component({ + selector: 'switch-checked-example', + template: ` +
+ + +
+ `, + imports: [UiSwitch, UiSwitchThumb, UiFormField, UiLabel] +}) +export class SwitchCheckedExample { + checked = model(true); +} + +@Component({ + selector: 'switch-form-example', + template: ` +
+
+

Email Notifications

+
+
+
+ +

+ Receive emails about new products, features, and more. +

+
+ +
+
+
+ +

+ Receive emails about your account security. +

+
+ +
+
+
+
+ `, + imports: [UiSwitch, UiSwitchThumb, UiFormField, UiLabel] +}) +export class SwitchFormExample { + marketingEmails = model(false); + securityEmails = model(true); +} + +// Component metadata for documentation +export const switchMeta: IComponentMeta = { + title: 'Switch', + description: 'A control that allows the user to toggle between checked and not checked.', + installation: { + package: `switch';`, + import: `import { UiSwitch, UiSwitchThumb } from '@workspace/ui/directives/switch'; +import { UiFormField } from '@workspace/ui/directives/form-field'; +import { UiLabel } from '@workspace/ui/directives/label';`, + usage: `
+ + +
` + }, + api: { + props: [ + { + name: 'checked', + type: 'boolean', + default: 'false', + description: 'Whether the switch is checked.' + }, + { + name: 'disabled', + type: 'boolean', + default: 'false', + description: 'Whether the switch is disabled.' + }, + { + name: 'class', + type: 'string', + description: 'Additional CSS classes to apply to the switch.' + } + ], + outputs: [ + { + name: 'checkedChange', + type: 'boolean', + description: 'Emitted when the checked state changes.' + } + ] + } +}; + +export const switchVariants: IVariant[] = [ + { + title: 'Default', + description: 'A basic switch component.', + code: `import { Component, model } from '@angular/core'; +import { UiSwitch, UiSwitchThumb } from '@workspace/ui/directives/switch'; +import { UiFormField } from '@workspace/ui/directives/form-field'; +import { UiLabel } from '@workspace/ui/directives/label'; + +@Component({ + selector: 'switch-default-example', + template: \` +
+ + +
+ \`, + imports: [UiSwitch, UiSwitchThumb, UiFormField, UiLabel] +}) +export class SwitchDefaultExample { + checked = model(false); +}`, + component: SwitchDefaultExample + }, + { + title: 'Disabled', + description: 'A switch in the disabled state.', + code: `import { Component, model } from '@angular/core'; +import { UiSwitch, UiSwitchThumb } from '@workspace/ui/directives/switch'; +import { UiFormField } from '@workspace/ui/directives/form-field'; +import { UiLabel } from '@workspace/ui/directives/label'; + +@Component({ + selector: 'switch-disabled-example', + template: \` +
+ + +
+ \`, + imports: [UiSwitch, UiSwitchThumb, UiFormField, UiLabel] +}) +export class SwitchDisabledExample { + checked = model(false); +}`, + component: SwitchDisabledExample + }, + { + title: 'Checked', + description: 'A switch in the checked state.', + code: `import { Component, model } from '@angular/core'; +import { UiSwitch, UiSwitchThumb } from '@workspace/ui/directives/switch'; +import { UiFormField } from '@workspace/ui/directives/form-field'; +import { UiLabel } from '@workspace/ui/directives/label'; + +@Component({ + selector: 'switch-checked-example', + template: \` +
+ + +
+ \`, + imports: [UiSwitch, UiSwitchThumb, UiFormField, UiLabel] +}) +export class SwitchCheckedExample { + checked = model(true); +}`, + component: SwitchCheckedExample + }, + { + title: 'Form', + description: 'A switch used in a form context with labels and descriptions.', + code: `import { Component, model } from '@angular/core'; +import { UiSwitch, UiSwitchThumb } from '@workspace/ui/directives/switch'; +import { UiFormField } from '@workspace/ui/directives/form-field'; +import { UiLabel } from '@workspace/ui/directives/label'; + +@Component({ + selector: 'switch-form-example', + template: \` +
+
+

Email Notifications

+
+
+
+ +

+ Receive emails about new products, features, and more. +

+
+ +
+
+
+ +

+ Receive emails about your account security. +

+
+ +
+
+
+
+ \`, + imports: [UiSwitch, UiSwitchThumb, UiFormField, UiLabel] +}) +export class SwitchFormExample { + marketingEmails = model(false); + securityEmails = model(true); +}`, + component: SwitchFormExample + } +]; diff --git a/projects/docs/src/app/shared/components/sidebar/sidebar.ts b/projects/docs/src/app/shared/components/sidebar/sidebar.ts index 90b674e..6357783 100644 --- a/projects/docs/src/app/shared/components/sidebar/sidebar.ts +++ b/projects/docs/src/app/shared/components/sidebar/sidebar.ts @@ -68,7 +68,7 @@ export class Sidebar { { name: 'Progress', path: 'progress' }, // { name: 'Select', path: 'select' }, { name: 'Separator', path: 'separator' }, - // { name: 'Switch', path: 'switch' }, + { name: 'Switch', path: 'switch' }, { name: 'Tabs', path: 'tabs' }, // { name: 'Toast', path: 'toast' }, { name: 'Tooltip', path: 'tooltip' }, diff --git a/projects/ui/src/directives/checkbox.ts b/projects/ui/src/directives/checkbox.ts index 02018f3..170bd99 100644 --- a/projects/ui/src/directives/checkbox.ts +++ b/projects/ui/src/directives/checkbox.ts @@ -15,14 +15,14 @@ const checkboxVariants = tv({ hostDirectives: [{ directive: NgpCheckbox, inputs: [ - 'ngpCheckboxChecked: uiCheckboxChecked', - 'ngpCheckboxIndeterminate: uiCheckboxIndeterminate', + 'ngpCheckboxChecked: checked', + 'ngpCheckboxIndeterminate: indeterminate', 'ngpCheckboxRequired: required', 'ngpCheckboxDisabled: disabled', ], outputs: [ - 'ngpCheckboxCheckedChange: uiCheckboxCheckedChange', - 'ngpCheckboxIndeterminateChange: uiCheckboxIndeterminateChange', + 'ngpCheckboxCheckedChange: checkedChange', + 'ngpCheckboxIndeterminateChange: indeterminateChange', ], }], }) diff --git a/projects/ui/src/directives/switch.ts b/projects/ui/src/directives/switch.ts new file mode 100644 index 0000000..2c63ad3 --- /dev/null +++ b/projects/ui/src/directives/switch.ts @@ -0,0 +1,49 @@ +import { computed, Directive, input } from '@angular/core'; +import { tv } from 'tailwind-variants'; +import { NgpSwitch, NgpSwitchThumb } from "ng-primitives/switch"; + +const switchVariants = tv({ + slots: { + switchRoot: 'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors data-[focus-visible]:outline-none data-[focus-visible]:ring-2 data-[focus-visible]:ring-ring data-[focus-visible]:ring-offset-2 data-[focus-visible]:ring-offset-background data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50 data-[checked]:bg-primary bg-input', + switchThumb: 'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[checked]:translate-x-5 translate-x-0' + } +}); + +const { switchThumb, switchRoot } = switchVariants(); + +@Directive({ + selector: '[uiSwitch]', + exportAs: 'uiSwitch', + host: { + '[class]': 'computedClass()' + }, + hostDirectives: [ + { + directive: NgpSwitch, + inputs: [ + 'ngpSwitchChecked:checked', + 'ngpSwitchDisabled:disabled' + ], + outputs: [ + 'ngpSwitchCheckedChange:checkedChange' + ], + }, + ], +}) +export class UiSwitch { + inputClass = input('', { alias: 'class' }); + computedClass = computed(() => switchRoot({ class: this.inputClass() })); +} + +@Directive({ + selector: '[uiSwitchThumb]', + exportAs: 'uiSwitchThumb', + host: { + '[class]': 'computedClass()' + }, + hostDirectives: [NgpSwitchThumb], +}) +export class UiSwitchThumb { + inputClass = input('', { alias: 'class' }); + computedClass = computed(() => switchThumb({ class: this.inputClass() })); +} \ No newline at end of file diff --git a/projects/ui/src/public-api.ts b/projects/ui/src/public-api.ts index 7170885..eb1c919 100644 --- a/projects/ui/src/public-api.ts +++ b/projects/ui/src/public-api.ts @@ -22,4 +22,5 @@ export * from './directives/form-field'; export * from './directives/checkbox'; export * from './directives/toggle'; export * from './directives/toggle-group'; -export * from './directives/skeleton'; \ No newline at end of file +export * from './directives/skeleton'; +export * from './directives/switch'; \ No newline at end of file diff --git a/registry.json b/registry.json index a3f36d9..0ff18e6 100644 --- a/registry.json +++ b/registry.json @@ -161,6 +161,19 @@ ], "dependencies": [] }, + { + "name": "switch", + "type": "registry:directive", + "title": "Switch Directive", + "description": "A switch directive for creating toggle switches with on/off states and proper accessibility", + "files": [ + { + "path": "directives/switch.ts", + "type": "registry:directive" + } + ], + "dependencies": [] + }, { "name": "avatar", "type": "registry:directive",