@proyecto-viviana/ui 0.3.2 → 0.3.3

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 (76) hide show
  1. package/dist/components.css +1077 -1077
  2. package/dist/index.js +236 -249
  3. package/dist/index.js.map +3 -3
  4. package/dist/index.ssr.js +78 -81
  5. package/dist/index.ssr.js.map +3 -3
  6. package/dist/radio/index.d.ts +12 -27
  7. package/dist/radio/index.d.ts.map +1 -1
  8. package/dist/test-utils/index.d.ts +2 -2
  9. package/dist/test-utils/index.d.ts.map +1 -1
  10. package/package.json +13 -12
  11. package/src/alert/index.tsx +48 -0
  12. package/src/assets/favicon.png +0 -0
  13. package/src/assets/fire.gif +0 -0
  14. package/src/autocomplete/index.tsx +313 -0
  15. package/src/avatar/index.tsx +75 -0
  16. package/src/badge/index.tsx +43 -0
  17. package/src/breadcrumbs/index.tsx +207 -0
  18. package/src/button/Button.tsx +74 -0
  19. package/src/button/index.ts +2 -0
  20. package/src/button/types.ts +24 -0
  21. package/src/calendar/DateField.tsx +200 -0
  22. package/src/calendar/DatePicker.tsx +298 -0
  23. package/src/calendar/RangeCalendar.tsx +236 -0
  24. package/src/calendar/TimeField.tsx +196 -0
  25. package/src/calendar/index.tsx +223 -0
  26. package/src/checkbox/index.tsx +257 -0
  27. package/src/color/index.tsx +687 -0
  28. package/src/combobox/index.tsx +383 -0
  29. package/src/components.css +1077 -0
  30. package/src/custom/calendar-card/index.tsx +66 -0
  31. package/src/custom/chip/index.tsx +46 -0
  32. package/src/custom/conversation/index.tsx +105 -0
  33. package/src/custom/event-card/index.tsx +132 -0
  34. package/src/custom/header/index.tsx +33 -0
  35. package/src/custom/lateral-nav/index.tsx +88 -0
  36. package/src/custom/logo/index.tsx +58 -0
  37. package/src/custom/nav-header/index.tsx +42 -0
  38. package/src/custom/page-layout/index.tsx +29 -0
  39. package/src/custom/profile-card/index.tsx +64 -0
  40. package/src/custom/project-card/index.tsx +59 -0
  41. package/src/custom/timeline-item/index.tsx +105 -0
  42. package/src/dialog/Dialog.tsx +260 -0
  43. package/src/dialog/index.tsx +3 -0
  44. package/src/disclosure/index.tsx +307 -0
  45. package/src/gridlist/index.tsx +403 -0
  46. package/src/icon/icons/GitHubIcon.tsx +20 -0
  47. package/src/icon/index.tsx +48 -0
  48. package/src/index.ts +322 -0
  49. package/src/landmark/index.tsx +231 -0
  50. package/src/link/index.tsx +130 -0
  51. package/src/listbox/index.tsx +231 -0
  52. package/src/menu/index.tsx +297 -0
  53. package/src/meter/index.tsx +163 -0
  54. package/src/numberfield/index.tsx +482 -0
  55. package/src/popover/index.tsx +260 -0
  56. package/src/progress-bar/index.tsx +169 -0
  57. package/src/radio/index.tsx +173 -0
  58. package/src/searchfield/index.tsx +453 -0
  59. package/src/select/index.tsx +349 -0
  60. package/src/separator/index.tsx +141 -0
  61. package/src/slider/index.tsx +382 -0
  62. package/src/styles.css +450 -0
  63. package/src/switch/ToggleSwitch.tsx +112 -0
  64. package/src/switch/index.tsx +90 -0
  65. package/src/table/index.tsx +531 -0
  66. package/src/tabs/index.tsx +273 -0
  67. package/src/tag-group/index.tsx +240 -0
  68. package/src/test-utils/index.ts +40 -0
  69. package/src/textfield/index.tsx +211 -0
  70. package/src/theme.css +101 -0
  71. package/src/toast/index.tsx +324 -0
  72. package/src/toolbar/index.tsx +108 -0
  73. package/src/tooltip/index.tsx +197 -0
  74. package/src/tree/index.tsx +494 -0
  75. package/dist/index.jsx +0 -6658
  76. package/dist/index.jsx.map +0 -7
@@ -5,10 +5,10 @@
5
5
  * SSR-compatible - renders children and UI elements directly without render props.
6
6
  */
7
7
  import { type JSX } from 'solid-js';
8
- import { type RadioProps as HeadlessRadioProps } from '@proyecto-viviana/solidaria-components';
8
+ import { type RadioGroupProps as HeadlessRadioGroupProps, type RadioProps as HeadlessRadioProps } from '@proyecto-viviana/solidaria-components';
9
9
  export type RadioGroupOrientation = 'horizontal' | 'vertical';
10
10
  export type RadioGroupSize = 'sm' | 'md' | 'lg';
11
- export type RadioGroupProps = {
11
+ export interface RadioGroupProps extends Omit<HeadlessRadioGroupProps, 'class' | 'style'> {
12
12
  /** The size of the radio buttons. */
13
13
  size?: RadioGroupSize;
14
14
  /** Additional CSS class name. */
@@ -19,32 +19,10 @@ export type RadioGroupProps = {
19
19
  description?: string;
20
20
  /** Error message when invalid. */
21
21
  errorMessage?: string;
22
- /** The current value (controlled). */
23
- value?: string;
24
- /** The default value (uncontrolled). */
25
- defaultValue?: string;
26
- /** Callback when value changes. */
27
- onChange?: (value: string) => void;
28
- /** The orientation of the radio group. */
29
- orientation?: 'horizontal' | 'vertical';
30
- /** Whether the radio group is disabled. */
31
- isDisabled?: boolean;
32
- /** Whether the radio group is read only. */
33
- isReadOnly?: boolean;
34
- /** Whether the radio group is required. */
35
- isRequired?: boolean;
36
- /** Whether the radio group is invalid. */
37
- isInvalid?: boolean;
38
- /** The children of the component. */
39
- children?: JSX.Element;
40
- /** The name of the radio group (for form submission). */
41
- name?: string;
42
- };
43
- export interface RadioProps extends Omit<HeadlessRadioProps, 'class' | 'style' | 'children'> {
22
+ }
23
+ export interface RadioProps extends Omit<HeadlessRadioProps, 'class' | 'style'> {
44
24
  /** Additional CSS class name. */
45
25
  class?: string;
46
- /** The content/label for the radio button. */
47
- children?: JSX.Element;
48
26
  }
49
27
  /**
50
28
  * A radio group allows users to select a single option from a list of mutually exclusive options.
@@ -53,7 +31,14 @@ export interface RadioProps extends Omit<HeadlessRadioProps, 'class' | 'style' |
53
31
  */
54
32
  export declare function RadioGroup(props: RadioGroupProps): JSX.Element;
55
33
  /**
56
- * A radio button allows a user to select a single option from a list of mutually exclusive options.
34
+ * A radio button allows users to select a single option from a list.
35
+ * Must be used within a RadioGroup.
36
+ * SSR-compatible - renders static JSX without render prop children.
37
+ *
38
+ * Note: Unlike other styled components, Radio does not use render props for children.
39
+ * Instead, it relies on data attributes set by the headless Radio component for styling.
40
+ * However, since we need dynamic styling based on state, we accept that this component
41
+ * has some limitations compared to the render-props-based original implementation.
57
42
  *
58
43
  * Built on solidaria-components Radio for full accessibility support.
59
44
  */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/radio/index.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,KAAK,GAAG,EAA+C,MAAM,UAAU,CAAA;AAChF,OAAO,EAGL,KAAK,UAAU,IAAI,kBAAkB,EAGtC,MAAM,wCAAwC,CAAA;AAM/C,MAAM,MAAM,qBAAqB,GAAG,YAAY,GAAG,UAAU,CAAA;AAC7D,MAAM,MAAM,cAAc,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA;AAQ/C,MAAM,MAAM,eAAe,GAAG;IAC5B,qCAAqC;IACrC,IAAI,CAAC,EAAE,cAAc,CAAA;IACrB,iCAAiC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,2BAA2B;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,iCAAiC;IACjC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,kCAAkC;IAClC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,wCAAwC;IACxC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,mCAAmC;IACnC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC,0CAA0C;IAC1C,WAAW,CAAC,EAAE,YAAY,GAAG,UAAU,CAAA;IACvC,2CAA2C;IAC3C,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,4CAA4C;IAC5C,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,2CAA2C;IAC3C,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,0CAA0C;IAC1C,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,qCAAqC;IACrC,QAAQ,CAAC,EAAE,GAAG,CAAC,OAAO,CAAA;IACtB,yDAAyD;IACzD,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,CAAA;AAED,MAAM,WAAW,UAAW,SAAQ,IAAI,CAAC,kBAAkB,EAAE,OAAO,GAAG,OAAO,GAAG,UAAU,CAAC;IAC1F,iCAAiC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,GAAG,CAAC,OAAO,CAAA;CACvB;AA4BD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,GAAG,CAAC,OAAO,CA6C9D;AAMD;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,GAAG,CAAC,OAAO,CA8CpD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/radio/index.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,KAAK,GAAG,EAA+C,MAAM,UAAU,CAAA;AAChF,OAAO,EAGL,KAAK,eAAe,IAAI,uBAAuB,EAC/C,KAAK,UAAU,IAAI,kBAAkB,EAGtC,MAAM,wCAAwC,CAAA;AAM/C,MAAM,MAAM,qBAAqB,GAAG,YAAY,GAAG,UAAU,CAAA;AAC7D,MAAM,MAAM,cAAc,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA;AAQ/C,MAAM,WAAW,eAAgB,SAAQ,IAAI,CAAC,uBAAuB,EAAE,OAAO,GAAG,OAAO,CAAC;IACvF,qCAAqC;IACrC,IAAI,CAAC,EAAE,cAAc,CAAA;IACrB,iCAAiC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,2BAA2B;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,iCAAiC;IACjC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,kCAAkC;IAClC,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,UAAW,SAAQ,IAAI,CAAC,kBAAkB,EAAE,OAAO,GAAG,OAAO,CAAC;IAC7E,iCAAiC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AA4BD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,GAAG,CAAC,OAAO,CA2C9D;AAMD;;;;;;;;;;;GAWG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,GAAG,CAAC,OAAO,CAgCpD"}
@@ -1,4 +1,4 @@
1
- import userEvent from '@testing-library/user-event';
1
+ import userEvent, { type UserEvent } from '@testing-library/user-event';
2
2
  /**
3
3
  * Pointer map matching react-spectrum's test setup.
4
4
  * Ensures pointer events have realistic dimensions so they aren't mistaken for virtual clicks.
@@ -38,7 +38,7 @@ export declare const pointerMap: ({
38
38
  * - pointerMap - Realistic pointer event dimensions
39
39
  * - pointerEventsCheck: Never - Skip pointer-events CSS check (jsdom doesn't handle this well)
40
40
  */
41
- export declare function setupUser(): import("@testing-library/user-event").UserEvent;
41
+ export declare function setupUser(): UserEvent;
42
42
  export { render, screen, fireEvent, cleanup } from '@solidjs/testing-library';
43
43
  export { userEvent };
44
44
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/test-utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,6BAA6B,CAAC;AAGpD;;;GAGG;AACH,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAOtB,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,SAAS,oDAMxB;AAED,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AAC9E,OAAO,EAAE,SAAS,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/test-utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,EAAE,EAAE,KAAK,SAAS,EAA2B,MAAM,6BAA6B,CAAC;AAEjG;;;GAGG;AACH,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAOtB,CAAC;AASF;;;;;GAKG;AACH,wBAAgB,SAAS,IAAI,SAAS,CAQrC;AAED,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AAC9E,OAAO,EAAE,SAAS,EAAE,CAAC"}
package/package.json CHANGED
@@ -1,45 +1,46 @@
1
1
  {
2
2
  "name": "@proyecto-viviana/ui",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Styled UI components for SolidJS - inspired by React Spectrum",
5
5
  "type": "module",
6
- "main": "./dist/index.ssr.js",
6
+ "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",
8
8
  "types": "./dist/index.d.ts",
9
9
  "exports": {
10
10
  ".": {
11
11
  "types": "./dist/index.d.ts",
12
- "solid": "./dist/index.jsx",
12
+ "solid": "./src/index.ts",
13
+ "import": "./dist/index.js",
13
14
  "default": "./dist/index.js"
14
15
  },
15
16
  "./theme.css": {
16
17
  "import": "./dist/theme.css",
17
- "default": "./dist/theme.css"
18
+ "default": "./src/theme.css"
18
19
  },
19
20
  "./styles.css": {
20
21
  "import": "./dist/styles.css",
21
- "default": "./dist/styles.css"
22
+ "default": "./src/styles.css"
22
23
  },
23
24
  "./components.css": {
24
25
  "import": "./dist/components.css",
25
- "default": "./dist/components.css"
26
+ "default": "./src/components.css"
26
27
  }
27
28
  },
28
29
  "files": [
29
- "dist"
30
+ "dist",
31
+ "src"
30
32
  ],
31
33
  "sideEffects": [
32
34
  "*.css"
33
35
  ],
34
36
  "scripts": {
35
- "build": "deno task build",
37
+ "build": "tsup && rm -f tsconfig.build.tsbuildinfo && tsc -p tsconfig.build.json",
36
38
  "dev": "tsup --watch",
37
- "prepublishOnly": "deno task build"
39
+ "prepublishOnly": "echo 'Use deno task release from root'"
38
40
  },
39
41
  "dependencies": {
40
- "@proyecto-viviana/solid-stately": "^0.2.2",
41
- "@proyecto-viviana/solidaria": "^0.2.2",
42
- "@proyecto-viviana/solidaria-components": "^0.2.2"
42
+ "@proyecto-viviana/solidaria": "workspace:*",
43
+ "@proyecto-viviana/solidaria-components": "workspace:*"
43
44
  },
44
45
  "peerDependencies": {
45
46
  "solid-js": "^1.9.0"
@@ -0,0 +1,48 @@
1
+ import type { JSX } from 'solid-js'
2
+ import { Show } from 'solid-js'
3
+
4
+ export type AlertVariant = 'info' | 'success' | 'warning' | 'error'
5
+
6
+ export interface AlertProps {
7
+ children: JSX.Element
8
+ variant?: AlertVariant
9
+ title?: string
10
+ dismissible?: boolean
11
+ onDismiss?: () => void
12
+ class?: string
13
+ }
14
+
15
+ const variantStyles: Record<AlertVariant, string> = {
16
+ info: 'bg-primary-700 text-primary-200 border border-primary-500',
17
+ success: 'bg-success-600 text-success-100 border border-success-400',
18
+ warning: 'bg-warning-600 text-warning-100 border border-warning-400',
19
+ error: 'bg-danger-600 text-danger-100 border border-danger-400',
20
+ }
21
+
22
+ export function Alert(props: AlertProps) {
23
+ const variant = () => props.variant ?? 'info'
24
+
25
+ return (
26
+ <div
27
+ class={`flex items-center min-h-[50px] font-normal rounded-lg px-4 py-2 ${variantStyles[variant()]} ${props.class ?? ''}`}
28
+ role="alert"
29
+ >
30
+ <div class="flex items-center gap-3 flex-1">
31
+ <Show when={props.title}>
32
+ <span class="font-semibold font-jost">{props.title}</span>
33
+ <span class="opacity-50">|</span>
34
+ </Show>
35
+ <div class="flex-1">{props.children}</div>
36
+ <Show when={props.dismissible}>
37
+ <button
38
+ class="hover:opacity-70 transition-opacity ml-2"
39
+ onClick={props.onDismiss}
40
+ aria-label="Dismiss"
41
+ >
42
+
43
+ </button>
44
+ </Show>
45
+ </div>
46
+ </div>
47
+ )
48
+ }
Binary file
Binary file
@@ -0,0 +1,313 @@
1
+ /**
2
+ * SearchAutocomplete component for proyecto-viviana-ui
3
+ *
4
+ * A styled autocomplete component combining a search input with a
5
+ * filterable dropdown list of options.
6
+ */
7
+
8
+ import { type JSX, splitProps, createMemo, Show, For, createSignal } from 'solid-js'
9
+ import {
10
+ Autocomplete,
11
+ useAutocompleteInput,
12
+ useAutocompleteCollection,
13
+ useAutocompleteState,
14
+ } from '@proyecto-viviana/solidaria-components'
15
+
16
+ // ============================================
17
+ // TYPES
18
+ // ============================================
19
+
20
+ export type SearchAutocompleteSize = 'sm' | 'md' | 'lg'
21
+
22
+ export interface SearchAutocompleteItem {
23
+ id: string
24
+ name: string
25
+ [key: string]: unknown
26
+ }
27
+
28
+ export interface SearchAutocompleteProps<T extends SearchAutocompleteItem = SearchAutocompleteItem> {
29
+ /** The items to display in the dropdown. */
30
+ items: T[]
31
+ /** The size of the autocomplete. @default 'md' */
32
+ size?: SearchAutocompleteSize
33
+ /** Placeholder text for the input. */
34
+ placeholder?: string
35
+ /** Accessible label for the input. */
36
+ 'aria-label'?: string
37
+ /** Label text shown above the input. */
38
+ label?: string
39
+ /** Description text shown below the input. */
40
+ description?: string
41
+ /** The current input value (controlled). */
42
+ inputValue?: string
43
+ /** The default input value (uncontrolled). */
44
+ defaultInputValue?: string
45
+ /** Handler called when the input value changes. */
46
+ onInputChange?: (value: string) => void
47
+ /** Handler called when an item is selected. */
48
+ onSelect?: (item: T) => void
49
+ /** Additional CSS class name. */
50
+ class?: string
51
+ /** Whether the input is disabled. */
52
+ isDisabled?: boolean
53
+ /**
54
+ * Custom filter function. By default, filters by case-insensitive name match.
55
+ */
56
+ filter?: (textValue: string, inputValue: string) => boolean
57
+ /**
58
+ * Custom render function for items.
59
+ */
60
+ renderItem?: (item: T) => JSX.Element
61
+ /**
62
+ * Key to use for the display text. @default 'name'
63
+ */
64
+ textKey?: keyof T
65
+ }
66
+
67
+ // ============================================
68
+ // STYLES
69
+ // ============================================
70
+
71
+ const sizeStyles = {
72
+ sm: {
73
+ container: 'text-sm',
74
+ input: 'h-8 px-3 text-sm',
75
+ label: 'text-xs mb-1',
76
+ list: 'max-h-48',
77
+ item: 'px-3 py-1.5 text-sm',
78
+ },
79
+ md: {
80
+ container: 'text-base',
81
+ input: 'h-10 px-4 text-base',
82
+ label: 'text-sm mb-1.5',
83
+ list: 'max-h-64',
84
+ item: 'px-4 py-2 text-base',
85
+ },
86
+ lg: {
87
+ container: 'text-lg',
88
+ input: 'h-12 px-5 text-lg',
89
+ label: 'text-base mb-2',
90
+ list: 'max-h-80',
91
+ item: 'px-5 py-2.5 text-lg',
92
+ },
93
+ }
94
+
95
+ // ============================================
96
+ // INNER COMPONENTS
97
+ // ============================================
98
+
99
+ function AutocompleteInput(props: {
100
+ placeholder?: string
101
+ 'aria-label'?: string
102
+ isDisabled?: boolean
103
+ size: SearchAutocompleteSize
104
+ }) {
105
+ const ctx = useAutocompleteInput()
106
+ if (!ctx) return null
107
+
108
+ const styles = () => sizeStyles[props.size]
109
+
110
+ return (
111
+ <input
112
+ ref={ctx.inputRef}
113
+ type="text"
114
+ placeholder={props.placeholder}
115
+ aria-label={props['aria-label']}
116
+ disabled={props.isDisabled}
117
+ value={ctx.inputProps.value()}
118
+ onInput={(e) => ctx.inputProps.onChange(e.currentTarget.value)}
119
+ onKeyDown={ctx.inputProps.onKeyDown}
120
+ onFocus={ctx.inputProps.onFocus}
121
+ onBlur={ctx.inputProps.onBlur}
122
+ aria-activedescendant={ctx.inputProps['aria-activedescendant']()}
123
+ aria-controls={ctx.inputProps['aria-controls']}
124
+ aria-autocomplete={ctx.inputProps['aria-autocomplete']}
125
+ autocomplete={ctx.inputProps.autoComplete}
126
+ autocorrect={ctx.inputProps.autoCorrect}
127
+ spellcheck={ctx.inputProps.spellCheck !== 'false'}
128
+ class={[
129
+ 'w-full rounded-md border border-bg-200 bg-bg-50',
130
+ 'focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500',
131
+ 'placeholder:text-text-400',
132
+ 'disabled:opacity-50 disabled:cursor-not-allowed',
133
+ styles().input,
134
+ ].join(' ')}
135
+ />
136
+ )
137
+ }
138
+
139
+ function AutocompleteList<T extends SearchAutocompleteItem>(props: {
140
+ items: T[]
141
+ size: SearchAutocompleteSize
142
+ onSelect?: (item: T) => void
143
+ renderItem?: (item: T) => JSX.Element
144
+ textKey: keyof T
145
+ }) {
146
+ const ctx = useAutocompleteCollection()
147
+ const state = useAutocompleteState()
148
+ if (!ctx) return null
149
+
150
+ const styles = () => sizeStyles[props.size]
151
+
152
+ // Filter items based on input
153
+ const filteredItems = createMemo(() => {
154
+ if (!ctx.filter) return props.items
155
+ return props.items.filter((item) => {
156
+ const textValue = String(item[props.textKey] ?? item.name ?? '')
157
+ return ctx.filter!(textValue)
158
+ })
159
+ })
160
+
161
+ const handleSelect = (item: T) => {
162
+ props.onSelect?.(item)
163
+ state?.setInputValue(String(item[props.textKey] ?? item.name ?? ''))
164
+ }
165
+
166
+ return (
167
+ <Show when={filteredItems().length > 0}>
168
+ <ul
169
+ ref={ctx.collectionRef}
170
+ id={ctx.collectionProps.id}
171
+ role="listbox"
172
+ aria-label={ctx.collectionProps['aria-label']}
173
+ class={[
174
+ 'mt-1 w-full rounded-md border border-bg-200 bg-bg-50 shadow-lg',
175
+ 'overflow-auto',
176
+ styles().list,
177
+ ].join(' ')}
178
+ >
179
+ <For each={filteredItems()}>
180
+ {(item) => {
181
+ const itemId = `autocomplete-item-${item.id}`
182
+ const isFocused = () => state?.focusedNodeId() === itemId
183
+
184
+ return (
185
+ <li
186
+ id={itemId}
187
+ role="option"
188
+ aria-selected={isFocused()}
189
+ onClick={() => handleSelect(item)}
190
+ onMouseEnter={() => state?.setFocusedNodeId(itemId)}
191
+ onMouseLeave={() => {
192
+ if (state?.focusedNodeId() === itemId) {
193
+ state?.setFocusedNodeId(null)
194
+ }
195
+ }}
196
+ class={[
197
+ 'cursor-pointer transition-colors',
198
+ isFocused()
199
+ ? 'bg-primary-100 text-primary-900'
200
+ : 'hover:bg-bg-100',
201
+ styles().item,
202
+ ].join(' ')}
203
+ >
204
+ {props.renderItem ? props.renderItem(item) : String(item[props.textKey] ?? item.name)}
205
+ </li>
206
+ )
207
+ }}
208
+ </For>
209
+ </ul>
210
+ </Show>
211
+ )
212
+ }
213
+
214
+ // ============================================
215
+ // SEARCH AUTOCOMPLETE COMPONENT
216
+ // ============================================
217
+
218
+ /**
219
+ * A styled autocomplete component for searching and selecting from a list.
220
+ *
221
+ * @example
222
+ * ```tsx
223
+ * const items = [
224
+ * { id: '1', name: 'Apple' },
225
+ * { id: '2', name: 'Banana' },
226
+ * { id: '3', name: 'Cherry' },
227
+ * ];
228
+ *
229
+ * <SearchAutocomplete
230
+ * items={items}
231
+ * placeholder="Search fruits..."
232
+ * aria-label="Fruit search"
233
+ * onSelect={(item) => console.log('Selected:', item)}
234
+ * />
235
+ *
236
+ * // With custom filter
237
+ * <SearchAutocomplete
238
+ * items={items}
239
+ * filter={(textValue, inputValue) =>
240
+ * textValue.toLowerCase().startsWith(inputValue.toLowerCase())
241
+ * }
242
+ * onSelect={(item) => console.log('Selected:', item)}
243
+ * />
244
+ *
245
+ * // With label and description
246
+ * <SearchAutocomplete
247
+ * items={items}
248
+ * label="Search"
249
+ * description="Type to filter the list"
250
+ * placeholder="Start typing..."
251
+ * />
252
+ * ```
253
+ */
254
+ export function SearchAutocomplete<T extends SearchAutocompleteItem = SearchAutocompleteItem>(
255
+ props: SearchAutocompleteProps<T>
256
+ ): JSX.Element {
257
+ const [local, autocompleteProps] = splitProps(props, [
258
+ 'items',
259
+ 'size',
260
+ 'placeholder',
261
+ 'aria-label',
262
+ 'label',
263
+ 'description',
264
+ 'onSelect',
265
+ 'class',
266
+ 'isDisabled',
267
+ 'renderItem',
268
+ 'textKey',
269
+ ])
270
+
271
+ const size = () => local.size ?? 'md'
272
+ const textKey = () => local.textKey ?? 'name'
273
+ const styles = () => sizeStyles[size()]
274
+
275
+ // Default filter: case-insensitive contains
276
+ const defaultFilter = (textValue: string, inputValue: string) => {
277
+ if (!inputValue) return true
278
+ return textValue.toLowerCase().includes(inputValue.toLowerCase())
279
+ }
280
+
281
+ return (
282
+ <div class={['vui-search-autocomplete relative', styles().container, local.class].filter(Boolean).join(' ')}>
283
+ <Show when={local.label}>
284
+ <label class={['block font-medium text-text-700', styles().label].join(' ')}>
285
+ {local.label}
286
+ </label>
287
+ </Show>
288
+
289
+ <Autocomplete
290
+ {...autocompleteProps}
291
+ filter={autocompleteProps.filter ?? defaultFilter}
292
+ >
293
+ <AutocompleteInput
294
+ placeholder={local.placeholder}
295
+ aria-label={local['aria-label']}
296
+ isDisabled={local.isDisabled}
297
+ size={size()}
298
+ />
299
+ <AutocompleteList
300
+ items={local.items}
301
+ size={size()}
302
+ onSelect={local.onSelect}
303
+ renderItem={local.renderItem}
304
+ textKey={textKey() as keyof T}
305
+ />
306
+ </Autocomplete>
307
+
308
+ <Show when={local.description}>
309
+ <p class="mt-1 text-sm text-text-500">{local.description}</p>
310
+ </Show>
311
+ </div>
312
+ )
313
+ }
@@ -0,0 +1,75 @@
1
+ import { Show } from 'solid-js'
2
+
3
+ export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
4
+
5
+ export interface AvatarProps {
6
+ src?: string
7
+ alt?: string
8
+ size?: AvatarSize
9
+ fallback?: string
10
+ online?: boolean
11
+ class?: string
12
+ }
13
+
14
+ const sizeStyles: Record<AvatarSize, { container: string; text: string; indicator: string }> = {
15
+ xs: { container: 'w-6 h-6', text: 'text-xs', indicator: 'w-1.5 h-1.5' },
16
+ sm: { container: 'w-8 h-8', text: 'text-sm', indicator: 'w-2 h-2' },
17
+ md: { container: 'w-10 h-10', text: 'text-base', indicator: 'w-2.5 h-2.5' },
18
+ lg: { container: 'w-14 h-14', text: 'text-lg', indicator: 'w-3 h-3' },
19
+ xl: { container: 'w-20 h-20', text: 'text-xl', indicator: 'w-4 h-4' },
20
+ }
21
+
22
+ export function Avatar(props: AvatarProps) {
23
+ const size = () => props.size ?? 'md'
24
+ const styles = () => sizeStyles[size()]
25
+
26
+ const initials = () => {
27
+ if (props.fallback) return props.fallback.slice(0, 2).toUpperCase()
28
+ if (props.alt) return props.alt.slice(0, 2).toUpperCase()
29
+ return '?'
30
+ }
31
+
32
+ return (
33
+ <div class={`relative inline-block ${props.class ?? ''}`}>
34
+ <div
35
+ class={`${styles().container} rounded-full overflow-hidden bg-bg-200 flex items-center justify-center ring-2 ring-accent/50`}
36
+ >
37
+ <Show
38
+ when={props.src}
39
+ fallback={
40
+ <span class={`${styles().text} font-medium text-primary-300`}>
41
+ {initials()}
42
+ </span>
43
+ }
44
+ >
45
+ <img
46
+ src={props.src}
47
+ alt={props.alt ?? 'Avatar'}
48
+ class="w-full h-full object-cover"
49
+ />
50
+ </Show>
51
+ </div>
52
+ <Show when={props.online !== undefined}>
53
+ <span
54
+ class={`absolute bottom-0 right-0 ${styles().indicator} rounded-full ring-2 ring-bg-400 ${
55
+ props.online ? 'bg-success-400' : 'bg-bg-light'
56
+ }`}
57
+ />
58
+ </Show>
59
+ </div>
60
+ )
61
+ }
62
+
63
+ export interface AvatarGroupProps {
64
+ children: any
65
+ max?: number
66
+ size?: AvatarSize
67
+ }
68
+
69
+ export function AvatarGroup(props: AvatarGroupProps) {
70
+ return (
71
+ <div class="flex -space-x-2">
72
+ {props.children}
73
+ </div>
74
+ )
75
+ }
@@ -0,0 +1,43 @@
1
+ import type { JSX } from 'solid-js'
2
+ import { Show } from 'solid-js'
3
+
4
+ export type BadgeVariant = 'primary' | 'secondary' | 'accent' | 'success' | 'warning' | 'danger'
5
+ export type BadgeSize = 'sm' | 'md' | 'lg'
6
+
7
+ export interface BadgeProps {
8
+ children?: JSX.Element
9
+ count?: number
10
+ variant?: BadgeVariant
11
+ size?: BadgeSize
12
+ class?: string
13
+ }
14
+
15
+ const variantStyles: Record<BadgeVariant, string> = {
16
+ primary: 'bg-primary-500 text-white',
17
+ secondary: 'bg-bg-300 text-primary-300',
18
+ accent: 'bg-accent-300 text-black',
19
+ success: 'bg-success-400 text-white',
20
+ warning: 'bg-warning-400 text-black',
21
+ danger: 'bg-danger-400 text-white',
22
+ }
23
+
24
+ const sizeStyles: Record<BadgeSize, string> = {
25
+ sm: 'w-5 h-5 text-xs',
26
+ md: 'w-7 h-7 text-xs',
27
+ lg: 'w-9 h-9 text-sm',
28
+ }
29
+
30
+ export function Badge(props: BadgeProps) {
31
+ const variant = () => props.variant ?? 'accent'
32
+ const size = () => props.size ?? 'md'
33
+
34
+ return (
35
+ <div
36
+ class={`flex items-center justify-center rounded-full border-b border-white font-semibold ${variantStyles[variant()]} ${sizeStyles[size()]} ${props.class ?? ''}`}
37
+ >
38
+ <Show when={props.count !== undefined} fallback={props.children}>
39
+ <span>{props.count}</span>
40
+ </Show>
41
+ </div>
42
+ )
43
+ }