@flaredev/cli 0.13.0 → 0.14.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 (33) hide show
  1. package/dist/index.js +72 -37
  2. package/package.json +2 -2
  3. package/templates/app/app/dashboard/error.tsx +18 -5
  4. package/templates/app/app/dashboard/roles/actions.ts +57 -0
  5. package/templates/app/app/dashboard/roles/page.tsx +56 -0
  6. package/templates/app/app/error.tsx +14 -5
  7. package/templates/app/components/account/sections.tsx +23 -3
  8. package/templates/app/components/auth/password-field.tsx +51 -32
  9. package/templates/app/components/auth/sign-in-flow.tsx +306 -306
  10. package/templates/app/components/dashboard/bulk-add-sheet.tsx +377 -0
  11. package/templates/app/components/dashboard/cell-value.ts +67 -0
  12. package/templates/app/components/dashboard/dashboard-sidebar.tsx +2 -1
  13. package/templates/app/components/dashboard/fields/field-widget.tsx +20 -16
  14. package/templates/app/components/dashboard/fields/file-field.tsx +23 -1
  15. package/templates/app/components/dashboard/import-dialog.tsx +1 -58
  16. package/templates/app/components/dashboard/resource-table.tsx +2 -0
  17. package/templates/app/components/dashboard/role-matrix.tsx +268 -0
  18. package/templates/app/components/dashboard/user-actions.tsx +55 -9
  19. package/templates/app/components/dashboard/user-menu.tsx +1 -1
  20. package/templates/app/components/ui/combobox.tsx +133 -0
  21. package/templates/app/db/flare-schema.ts +27 -1
  22. package/templates/app/lib/api.ts +4 -1
  23. package/templates/app/lib/dashboard.ts +15 -4
  24. package/templates/app/lib/permissions.ts +105 -0
  25. package/templates/app/lib/stale-chunk.ts +50 -0
  26. package/templates/app/migrations/0000_init.sql +9 -0
  27. package/templates/app/migrations/meta/0000_snapshot.json +57 -1
  28. package/templates/app/migrations/meta/_journal.json +1 -1
  29. package/templates/locks/cloudflare/pnpm-lock.yaml +8 -8
  30. package/templates/locks/next/pnpm-lock.yaml +4 -4
  31. package/templates/next/lib/dashboard.ts +15 -4
  32. package/templates/next/lib/permissions.ts +100 -0
  33. package/templates/next/prisma/schema/base.prisma +31 -0
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useId, useMemo, useState } from "react";
3
+ import { forwardRef, useId, useMemo, useState } from "react";
4
4
  import { CheckIcon, EyeIcon, EyeOffIcon } from "lucide-react";
5
5
  import { checkPassword, PASSWORD_MAX_LENGTH, PASSWORD_MIN_LENGTH } from "@/lib/password-rules";
6
6
  import { cn } from "@/lib/utils";
@@ -22,6 +22,40 @@ interface Props {
22
22
  avoid?: (string | null | undefined)[];
23
23
  }
24
24
 
25
+ /**
26
+ * A password input with the eye that reveals what you typed.
27
+ *
28
+ * The one place that knows how to do this, because a password field people cannot see
29
+ * into is where typos go to hide — and a form that offers the eye in one place and not
30
+ * the next teaches people not to trust it. {@link PasswordField} wraps this with the
31
+ * rules and the strength meter; forms that only need the box use it directly.
32
+ */
33
+ export const PasswordInput = forwardRef<HTMLInputElement, Omit<React.ComponentProps<typeof AuthInput>, "type">>(
34
+ function PasswordInput({ className, ...props }, ref) {
35
+ const [visible, setVisible] = useState(false);
36
+ return (
37
+ <div className="relative">
38
+ <AuthInput ref={ref} {...props} type={visible ? "text" : "password"} className={cn("pr-11", className)} />
39
+ <button
40
+ type="button"
41
+ onClick={() => setVisible(!visible)}
42
+ aria-pressed={visible}
43
+ aria-controls={props.id}
44
+ aria-label={visible ? "Hide password" : "Show password"}
45
+ title={visible ? "Hide password" : "Show password"}
46
+ // Not in the tab order: the input it belongs to is, and a keyboard user
47
+ // tabbing from the password to a toggle before the submit button is a worse
48
+ // path than the one they expect.
49
+ tabIndex={-1}
50
+ className="absolute inset-y-0 right-0 grid w-11 place-items-center text-foreground-muted hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand"
51
+ >
52
+ {visible ? <EyeOffIcon className="size-4.5" /> : <EyeIcon className="size-4.5" />}
53
+ </button>
54
+ </div>
55
+ );
56
+ },
57
+ );
58
+
25
59
  const BAR_COLOURS = ["bg-danger", "bg-danger", "bg-warning", "bg-success/70", "bg-success"];
26
60
 
27
61
  /**
@@ -46,43 +80,28 @@ export function PasswordField({
46
80
  const generatedId = useId();
47
81
  const fieldId = id ?? generatedId;
48
82
  const hintId = `${fieldId}-hint`;
49
- const [visible, setVisible] = useState(false);
50
83
  const check = useMemo(() => checkPassword(value, avoid), [value, avoid.join("\u0000")]);
51
84
  const started = value.length > 0;
52
85
 
53
86
  return (
54
87
  <div className="flex flex-col gap-2">
55
88
  <AuthLabel htmlFor={fieldId}>{label}</AuthLabel>
56
- <div className="relative">
57
- <AuthInput
58
- id={fieldId}
59
- name={name}
60
- type={visible ? "text" : "password"}
61
- autoComplete={autoComplete}
62
- autoFocus={autoFocus}
63
- required={required}
64
- placeholder={placeholder}
65
- value={value}
66
- onChange={(event) => onChange(event.target.value)}
67
- minLength={autoComplete === "new-password" ? PASSWORD_MIN_LENGTH : undefined}
68
- maxLength={PASSWORD_MAX_LENGTH}
69
- aria-describedby={showRules ? hintId : undefined}
70
- // A styling hook, so the field can go green without any state plumbing.
71
- data-has-passed-validation={showRules && check.valid ? "" : undefined}
72
- className={cn("pr-11", showRules && check.valid && "border-success/60")}
73
- />
74
- <button
75
- type="button"
76
- onClick={() => setVisible(!visible)}
77
- aria-pressed={visible}
78
- aria-controls={fieldId}
79
- aria-label={visible ? "Hide password" : "Show password"}
80
- title={visible ? "Hide password" : "Show password"}
81
- className="absolute inset-y-0 right-0 grid w-11 place-items-center text-foreground-muted hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand"
82
- >
83
- {visible ? <EyeOffIcon className="size-4.5" /> : <EyeIcon className="size-4.5" />}
84
- </button>
85
- </div>
89
+ <PasswordInput
90
+ id={fieldId}
91
+ name={name}
92
+ autoComplete={autoComplete}
93
+ autoFocus={autoFocus}
94
+ required={required}
95
+ placeholder={placeholder}
96
+ value={value}
97
+ onChange={(event) => onChange(event.target.value)}
98
+ minLength={autoComplete === "new-password" ? PASSWORD_MIN_LENGTH : undefined}
99
+ maxLength={PASSWORD_MAX_LENGTH}
100
+ aria-describedby={showRules ? hintId : undefined}
101
+ // A styling hook, so the field can go green without any state plumbing.
102
+ data-has-passed-validation={showRules && check.valid ? "" : undefined}
103
+ className={cn(showRules && check.valid && "border-success/60")}
104
+ />
86
105
 
87
106
  {showRules && (
88
107
  <div id={hintId} className="flex flex-col gap-2">