@mostrom/app-shell 0.1.3 → 0.1.5

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.
@@ -0,0 +1,130 @@
1
+ import { Button } from "@/components/ui/button"
2
+ import {
3
+ NavigationMenu,
4
+ NavigationMenuContent,
5
+ NavigationMenuItem,
6
+ NavigationMenuLink,
7
+ NavigationMenuList,
8
+ NavigationMenuTrigger,
9
+ } from "@/components/ui/navigation-menu"
10
+ import { UserIcon, BuildingIcon, CircleDollarSignIcon, LayoutGridIcon, SparklesIcon, RadioIcon } from "lucide-react"
11
+
12
+ const industries = [
13
+ {
14
+ title: "Individuals",
15
+ description: "Keep your finances organized.",
16
+ href: "#",
17
+ icon: (
18
+ <UserIcon
19
+ />
20
+ ),
21
+ },
22
+ {
23
+ title: "LLCs",
24
+ description: "Benefit from tax write-offs.",
25
+ href: "#",
26
+ icon: (
27
+ <BuildingIcon
28
+ />
29
+ ),
30
+ },
31
+ {
32
+ title: "Freelancers",
33
+ description: "For independent workers.",
34
+ href: "#",
35
+ icon: (
36
+ <CircleDollarSignIcon
37
+ />
38
+ ),
39
+ },
40
+ {
41
+ title: "Investors",
42
+ description: "Make and grow your money.",
43
+ href: "#",
44
+ icon: (
45
+ <LayoutGridIcon
46
+ />
47
+ ),
48
+ },
49
+ {
50
+ title: "Small businesses",
51
+ description: "We take care of your taxes.",
52
+ href: "#",
53
+ icon: (
54
+ <SparklesIcon
55
+ />
56
+ ),
57
+ },
58
+ {
59
+ title: "Crypto",
60
+ description: "For tech enthusiasts.",
61
+ href: "#",
62
+ icon: (
63
+ <CircleDollarSignIcon
64
+ />
65
+ ),
66
+ },
67
+ {
68
+ title: "Big companies",
69
+ description: "Run your finances easily.",
70
+ href: "#",
71
+ icon: (
72
+ <BuildingIcon
73
+ />
74
+ ),
75
+ },
76
+ {
77
+ title: "Investments",
78
+ description: "Launch your ideas worldwide.",
79
+ href: "#",
80
+ icon: (
81
+ <RadioIcon
82
+ />
83
+ ),
84
+ },
85
+ ]
86
+
87
+ export function Pattern() {
88
+ return (
89
+ <div className="flex items-center justify-center">
90
+ <NavigationMenu>
91
+ <NavigationMenuList>
92
+ <NavigationMenuItem>
93
+ <NavigationMenuTrigger>Industries</NavigationMenuTrigger>
94
+ <NavigationMenuContent>
95
+ <div className="w-[500px]">
96
+ <ul className="grid grid-cols-2 gap-1">
97
+ {industries.map((item) => (
98
+ <li key={item.title}>
99
+ <NavigationMenuLink
100
+ asChild
101
+ className="flex items-start gap-2 p-3"
102
+ >
103
+ <a href="#">
104
+ {item.icon}
105
+ <div className="flex flex-col gap-0.5">
106
+ <div className="text-sm leading-none font-medium">
107
+ {item.title}
108
+ </div>
109
+ <p className="text-muted-foreground text-xs leading-snug">
110
+ {item.description}
111
+ </p>
112
+ </div>
113
+ </a>
114
+ </NavigationMenuLink>
115
+ </li>
116
+ ))}
117
+ </ul>
118
+ <div className="mt-2 px-1 pb-1">
119
+ <Button className="w-full" asChild>
120
+ <a href="#">Learn more</a>
121
+ </Button>
122
+ </div>
123
+ </div>
124
+ </NavigationMenuContent>
125
+ </NavigationMenuItem>
126
+ </NavigationMenuList>
127
+ </NavigationMenu>
128
+ </div>
129
+ )
130
+ }
@@ -251,10 +251,10 @@ function FilterInput<T = unknown>({
251
251
  className?: string
252
252
  field?: FilterFieldConfig<T>
253
253
  }) {
254
- const context = useFilterContext()
255
254
  const [isValid, setIsValid] = useState(true)
256
255
  const [validationMessage, setValidationMessage] = useState("")
257
256
  const inputRef = useRef<HTMLInputElement>(null)
257
+ const context = useFilterContext()
258
258
 
259
259
  useEffect(() => {
260
260
  if (props.autoFocus) {
@@ -396,12 +396,6 @@ function FilterRemoveButton({
396
396
  }: FilterRemoveButtonProps) {
397
397
  const context = useFilterContext()
398
398
 
399
- const sizeMap = {
400
- sm: "sm" as const,
401
- default: "sm" as const,
402
- lg: "default" as const,
403
- }
404
-
405
399
  return (
406
400
  <Button
407
401
  variant="outline"
@@ -984,7 +978,6 @@ function FilterValueSelector<T = unknown>({
984
978
  operator,
985
979
  autoFocus,
986
980
  }: FilterValueSelectorProps<T>) {
987
- const context = useFilterContext()
988
981
 
989
982
  if (operator === "empty" || operator === "not_empty") {
990
983
  return null
@@ -1,7 +1,6 @@
1
1
  // ReUI Components
2
2
  export * from "./alert";
3
3
  export * from "./autocomplete";
4
- export * from "./badge";
5
4
  export * from "./date-selector";
6
5
  export * from "./filters";
7
6
  export * from "./frame";
@@ -3,7 +3,7 @@
3
3
  import { useMemo } from "react"
4
4
  import { cva, type VariantProps } from "class-variance-authority"
5
5
 
6
- import { cn } from "../../lib/utils"
6
+ import { cn } from "@/lib/utils"
7
7
  import { Label } from "@/components/ui/label"
8
8
  import { Separator } from "@/components/ui/separator"
9
9
 
@@ -23,6 +23,8 @@ export * from "./navigation-menu";
23
23
  export * from "./popover";
24
24
  export * from "./scroll-area";
25
25
  export * from "./select";
26
+ export * from "./simple-select";
27
+ export * from "./simple-input";
26
28
  export * from "./separator";
27
29
  export * from "./sheet";
28
30
  export * from "./space-between";
@@ -1,7 +1,7 @@
1
1
  import * as React from "react"
2
2
  import { Label as LabelPrimitive } from "radix-ui"
3
3
 
4
- import { cn } from "../../lib/utils"
4
+ import { cn } from "@/lib/utils"
5
5
 
6
6
  function Label({
7
7
  className,
@@ -1,10 +1,8 @@
1
- "use client"
2
-
3
1
  import * as React from "react"
4
2
  import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
5
3
  import { Select as SelectPrimitive } from "radix-ui"
6
4
 
7
- import { cn } from "../../lib/utils"
5
+ import { cn } from "@/lib/utils"
8
6
 
9
7
  function Select({
10
8
  ...props
@@ -1,7 +1,9 @@
1
+ "use client"
2
+
1
3
  import * as React from "react"
2
4
  import { Separator as SeparatorPrimitive } from "radix-ui"
3
5
 
4
- import { cn } from "../../lib/utils"
6
+ import { cn } from "@/lib/utils"
5
7
 
6
8
  function Separator({
7
9
  className,
@@ -0,0 +1,114 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "../../lib/utils"
5
+
6
+ /** Props for SimpleInput component - Cloudscape-compatible API */
7
+ export interface SimpleInputProps {
8
+ /** Current value of the input */
9
+ value: string
10
+ /** Callback when value changes - matches Cloudscape pattern */
11
+ onChange: (event: { detail: { value: string } }) => void
12
+ /** Input type */
13
+ type?: "text" | "number" | "search" | "email" | "url" | "password" | "tel"
14
+ /** Input mode for mobile keyboards */
15
+ inputMode?: "text" | "numeric" | "decimal" | "email" | "tel" | "url" | "search" | "none"
16
+ /** Placeholder text */
17
+ placeholder?: string
18
+ /** Whether the input is disabled */
19
+ disabled?: boolean
20
+ /** Whether the input is read-only */
21
+ readOnly?: boolean
22
+ /** Whether the input should auto-focus */
23
+ autoFocus?: boolean
24
+ /** Aria label for accessibility */
25
+ ariaLabel?: string
26
+ /** Additional className */
27
+ className?: string
28
+ /** Name attribute */
29
+ name?: string
30
+ /** Auto-complete attribute */
31
+ autoComplete?: string
32
+ /** Whether the input is invalid */
33
+ invalid?: boolean
34
+ /** Step for number inputs */
35
+ step?: number | string
36
+ /** Min value for number inputs */
37
+ min?: number | string
38
+ /** Max value for number inputs */
39
+ max?: number | string
40
+ }
41
+
42
+ /**
43
+ * SimpleInput - An input component with Cloudscape-compatible API.
44
+ *
45
+ * Provides a declarative API similar to Cloudscape Input while using
46
+ * the app-shell styling.
47
+ *
48
+ * @example
49
+ * ```tsx
50
+ * <SimpleInput
51
+ * value={name}
52
+ * onChange={({ detail }) => setName(detail.value)}
53
+ * placeholder="Enter your name"
54
+ * />
55
+ * ```
56
+ *
57
+ * @example Number input
58
+ * ```tsx
59
+ * <SimpleInput
60
+ * type="number"
61
+ * value={String(amount)}
62
+ * onChange={({ detail }) => setAmount(parseInt(detail.value, 10) || 0)}
63
+ * placeholder="0"
64
+ * />
65
+ * ```
66
+ */
67
+ export function SimpleInput({
68
+ value,
69
+ onChange,
70
+ type = "text",
71
+ inputMode,
72
+ placeholder,
73
+ disabled = false,
74
+ readOnly = false,
75
+ autoFocus = false,
76
+ ariaLabel,
77
+ className,
78
+ name,
79
+ autoComplete,
80
+ invalid = false,
81
+ step,
82
+ min,
83
+ max,
84
+ }: SimpleInputProps) {
85
+ const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
86
+ onChange({ detail: { value: event.target.value } })
87
+ }
88
+
89
+ return (
90
+ <input
91
+ type={type}
92
+ value={value}
93
+ onChange={handleChange}
94
+ placeholder={placeholder}
95
+ disabled={disabled}
96
+ readOnly={readOnly}
97
+ autoFocus={autoFocus}
98
+ aria-label={ariaLabel}
99
+ aria-invalid={invalid || undefined}
100
+ name={name}
101
+ autoComplete={autoComplete}
102
+ inputMode={inputMode}
103
+ step={step}
104
+ min={min}
105
+ max={max}
106
+ className={cn(
107
+ "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
108
+ "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
109
+ "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
110
+ className
111
+ )}
112
+ />
113
+ )
114
+ }
@@ -0,0 +1,310 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { CheckIcon, ChevronDownIcon, SearchIcon } from "lucide-react"
5
+ import { cn } from "../../lib/utils"
6
+
7
+ /** Option type for SimpleSelect */
8
+ export interface SelectOption {
9
+ /** Display label for the option */
10
+ label: string
11
+ /** Value of the option */
12
+ value: string
13
+ /** Optional description shown below the label */
14
+ description?: string
15
+ /** Whether the option is disabled */
16
+ disabled?: boolean
17
+ }
18
+
19
+ /** Grouped options type for SimpleSelect */
20
+ export interface SelectOptionGroup {
21
+ /** Group label */
22
+ label: string
23
+ /** Options in this group */
24
+ options: SelectOption[]
25
+ }
26
+
27
+ /** Props for SimpleSelect component */
28
+ export interface SimpleSelectProps {
29
+ /** Currently selected option */
30
+ selectedOption: SelectOption | null
31
+ /** Callback when selection changes - matches Cloudscape pattern */
32
+ onChange: (event: { detail: { selectedOption: SelectOption } }) => void
33
+ /** Options to display - can be flat array or grouped */
34
+ options: (SelectOption | SelectOptionGroup)[]
35
+ /** Placeholder text when no option is selected */
36
+ placeholder?: string
37
+ /** Whether the select is disabled */
38
+ disabled?: boolean
39
+ /** Enable filtering/search - "auto" enables search */
40
+ filteringType?: "auto" | "none"
41
+ /** Additional className for the trigger */
42
+ className?: string
43
+ /** Trigger size */
44
+ size?: "sm" | "default"
45
+ }
46
+
47
+ /** Type guard to check if an option is a group */
48
+ function isOptionGroup(option: SelectOption | SelectOptionGroup): option is SelectOptionGroup {
49
+ return "options" in option && Array.isArray(option.options)
50
+ }
51
+
52
+ /** Flatten grouped options into a flat list */
53
+ function flattenOptions(options: (SelectOption | SelectOptionGroup)[]): SelectOption[] {
54
+ return options.flatMap((opt) => (isOptionGroup(opt) ? opt.options : [opt]))
55
+ }
56
+
57
+ /**
58
+ * SimpleSelect - A select component with Cloudscape-compatible API.
59
+ *
60
+ * Provides a declarative API similar to Cloudscape Select while using
61
+ * custom dropdown implementation for flexibility.
62
+ *
63
+ * @example
64
+ * ```tsx
65
+ * <SimpleSelect
66
+ * selectedOption={selectedOption}
67
+ * onChange={({ detail }) => setSelectedOption(detail.selectedOption)}
68
+ * options={[
69
+ * { label: "Option 1", value: "1" },
70
+ * { label: "Option 2", value: "2" },
71
+ * ]}
72
+ * placeholder="Select an option"
73
+ * />
74
+ * ```
75
+ *
76
+ * @example With grouped options
77
+ * ```tsx
78
+ * <SimpleSelect
79
+ * selectedOption={selectedTimezone}
80
+ * onChange={({ detail }) => setSelectedTimezone(detail.selectedOption)}
81
+ * options={[
82
+ * { label: "Americas", options: [
83
+ * { label: "New York", value: "America/New_York" },
84
+ * { label: "Los Angeles", value: "America/Los_Angeles" },
85
+ * ]},
86
+ * { label: "Europe", options: [
87
+ * { label: "London", value: "Europe/London" },
88
+ * { label: "Paris", value: "Europe/Paris" },
89
+ * ]},
90
+ * ]}
91
+ * filteringType="auto"
92
+ * />
93
+ * ```
94
+ */
95
+ export function SimpleSelect({
96
+ selectedOption,
97
+ onChange,
98
+ options,
99
+ placeholder = "Select...",
100
+ disabled = false,
101
+ filteringType = "none",
102
+ className,
103
+ size = "default",
104
+ }: SimpleSelectProps) {
105
+ const [isOpen, setIsOpen] = React.useState(false)
106
+ const [filterText, setFilterText] = React.useState("")
107
+ const containerRef = React.useRef<HTMLDivElement>(null)
108
+ const inputRef = React.useRef<HTMLInputElement>(null)
109
+
110
+ const isSearchable = filteringType === "auto"
111
+
112
+ // Close dropdown when clicking outside
113
+ React.useEffect(() => {
114
+ function handleClickOutside(event: MouseEvent) {
115
+ if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
116
+ setIsOpen(false)
117
+ setFilterText("")
118
+ }
119
+ }
120
+
121
+ if (isOpen) {
122
+ document.addEventListener("mousedown", handleClickOutside)
123
+ return () => document.removeEventListener("mousedown", handleClickOutside)
124
+ }
125
+ }, [isOpen])
126
+
127
+ // Focus input when dropdown opens (for searchable selects)
128
+ React.useEffect(() => {
129
+ if (isOpen && isSearchable && inputRef.current) {
130
+ inputRef.current.focus()
131
+ }
132
+ }, [isOpen, isSearchable])
133
+
134
+ // Handle keyboard navigation
135
+ const handleKeyDown = (event: React.KeyboardEvent) => {
136
+ if (disabled) return
137
+
138
+ switch (event.key) {
139
+ case "Enter":
140
+ case " ":
141
+ if (!isOpen) {
142
+ event.preventDefault()
143
+ setIsOpen(true)
144
+ }
145
+ break
146
+ case "Escape":
147
+ setIsOpen(false)
148
+ setFilterText("")
149
+ break
150
+ case "ArrowDown":
151
+ if (!isOpen) {
152
+ event.preventDefault()
153
+ setIsOpen(true)
154
+ }
155
+ break
156
+ }
157
+ }
158
+
159
+ const handleSelect = (option: SelectOption) => {
160
+ onChange({ detail: { selectedOption: option } })
161
+ setIsOpen(false)
162
+ setFilterText("")
163
+ }
164
+
165
+ // Filter options based on search text
166
+ const getFilteredOptions = (): (SelectOption | SelectOptionGroup)[] => {
167
+ if (!filterText.trim()) return options
168
+
169
+ const searchLower = filterText.toLowerCase()
170
+
171
+ return options
172
+ .map((opt) => {
173
+ if (isOptionGroup(opt)) {
174
+ const filteredGroupOptions = opt.options.filter(
175
+ (o) =>
176
+ o.label.toLowerCase().includes(searchLower) ||
177
+ o.value.toLowerCase().includes(searchLower) ||
178
+ o.description?.toLowerCase().includes(searchLower)
179
+ )
180
+ if (filteredGroupOptions.length === 0) return null
181
+ return { ...opt, options: filteredGroupOptions }
182
+ }
183
+ const matches =
184
+ opt.label.toLowerCase().includes(searchLower) ||
185
+ opt.value.toLowerCase().includes(searchLower) ||
186
+ opt.description?.toLowerCase().includes(searchLower)
187
+ return matches ? opt : null
188
+ })
189
+ .filter(Boolean) as (SelectOption | SelectOptionGroup)[]
190
+ }
191
+
192
+ const filteredOptions = getFilteredOptions()
193
+ const allOptions = flattenOptions(options)
194
+
195
+ const renderOption = (option: SelectOption, index: number) => {
196
+ const isSelected = selectedOption?.value === option.value
197
+ return (
198
+ <div
199
+ key={`${option.value}-${index}`}
200
+ role="option"
201
+ aria-selected={isSelected}
202
+ className={cn(
203
+ "relative flex w-full cursor-pointer items-center rounded-sm py-1.5 pr-8 pl-2 text-sm outline-none select-none",
204
+ "hover:bg-accent hover:text-accent-foreground",
205
+ isSelected && "bg-accent/50",
206
+ option.disabled && "pointer-events-none opacity-50"
207
+ )}
208
+ onClick={() => !option.disabled && handleSelect(option)}
209
+ >
210
+ <span className="absolute right-2 flex size-3.5 items-center justify-center">
211
+ {isSelected && <CheckIcon className="size-4" />}
212
+ </span>
213
+ <div className="flex flex-col gap-0.5">
214
+ <span>{option.label}</span>
215
+ {option.description && (
216
+ <span className="text-muted-foreground text-xs">{option.description}</span>
217
+ )}
218
+ </div>
219
+ </div>
220
+ )
221
+ }
222
+
223
+ const renderOptions = () => {
224
+ if (filteredOptions.length === 0) {
225
+ return (
226
+ <div className="text-muted-foreground py-2 text-center text-sm">
227
+ {allOptions.length === 0 ? "No options available" : "No options found"}
228
+ </div>
229
+ )
230
+ }
231
+
232
+ return filteredOptions.map((opt, index) => {
233
+ if (isOptionGroup(opt)) {
234
+ return (
235
+ <div key={`group-${opt.label}-${index}`}>
236
+ <div className="text-muted-foreground px-2 py-1.5 text-xs font-medium">
237
+ {opt.label}
238
+ </div>
239
+ {opt.options.map((groupOpt, gIndex) => renderOption(groupOpt, gIndex))}
240
+ </div>
241
+ )
242
+ }
243
+ return renderOption(opt, index)
244
+ })
245
+ }
246
+
247
+ return (
248
+ <div ref={containerRef} className="relative w-full">
249
+ {/* Trigger Button */}
250
+ <button
251
+ type="button"
252
+ role="combobox"
253
+ aria-expanded={isOpen}
254
+ aria-haspopup="listbox"
255
+ disabled={disabled}
256
+ onClick={() => !disabled && setIsOpen(!isOpen)}
257
+ onKeyDown={handleKeyDown}
258
+ className={cn(
259
+ "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground",
260
+ "focus-visible:border-ring focus-visible:ring-ring/50",
261
+ "flex w-full items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm",
262
+ "whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px]",
263
+ "disabled:cursor-not-allowed disabled:opacity-50",
264
+ size === "default" ? "h-9" : "h-8",
265
+ className
266
+ )}
267
+ >
268
+ <span className={cn("truncate", !selectedOption && "text-muted-foreground")}>
269
+ {selectedOption?.label ?? placeholder}
270
+ </span>
271
+ <ChevronDownIcon className="size-4 shrink-0 opacity-50" />
272
+ </button>
273
+
274
+ {/* Dropdown */}
275
+ {isOpen && (
276
+ <div
277
+ role="listbox"
278
+ className={cn(
279
+ "bg-popover text-popover-foreground absolute z-50 mt-1 w-full min-w-[8rem] overflow-hidden rounded-md border shadow-md",
280
+ "animate-in fade-in-0 zoom-in-95"
281
+ )}
282
+ >
283
+ {/* Search Input */}
284
+ {isSearchable && (
285
+ <div className="border-b p-2">
286
+ <div className="relative">
287
+ <SearchIcon className="text-muted-foreground absolute left-2 top-1/2 size-4 -translate-y-1/2" />
288
+ <input
289
+ ref={inputRef}
290
+ type="text"
291
+ value={filterText}
292
+ onChange={(e) => setFilterText(e.target.value)}
293
+ placeholder="Search..."
294
+ className={cn(
295
+ "border-input placeholder:text-muted-foreground focus-visible:ring-ring/50",
296
+ "h-8 w-full rounded-md border bg-transparent py-1 pr-3 pl-8 text-sm outline-none",
297
+ "focus-visible:border-ring focus-visible:ring-[3px]"
298
+ )}
299
+ />
300
+ </div>
301
+ </div>
302
+ )}
303
+
304
+ {/* Options List */}
305
+ <div className="max-h-[300px] overflow-y-auto p-1">{renderOptions()}</div>
306
+ </div>
307
+ )}
308
+ </div>
309
+ )
310
+ }
@@ -1,9 +0,0 @@
1
- ---
2
- active: true
3
- iteration: 1
4
- max_iterations: 0
5
- completion_promise: null
6
- started_at: "2026-02-13T12:51:42Z"
7
- ---
8
-
9
- Without diverging from the drawer requirements in @docs/prompts/todo/drawer-fields.md ensure the drawer layout wise and style wise mimics this exact image docs/prompts/todo/Screenshot 2026-02-12 at 18.19.47.png and use playwright to take screenshots and do a comparison until the layouts match 100%