@fraza/start 0.0.0-stage → 0.3.4

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 (59) hide show
  1. package/CHANGELOG.md +180 -0
  2. package/LICENSE +21 -0
  3. package/README.md +169 -2
  4. package/THIRD-PARTY-NOTICES.md +11 -0
  5. package/bin/agents-block.md +61 -0
  6. package/bin/cli.js +197 -0
  7. package/bin/project.js +281 -0
  8. package/bin/version.js +2 -0
  9. package/changelog.json +577 -0
  10. package/package.json +42 -4
  11. package/templates/app/(admin)/admin/[...admin]/layout.js +3 -0
  12. package/templates/app/(admin)/admin/[...admin]/loading.js +3 -0
  13. package/templates/app/(admin)/admin/[...admin]/page.js +1 -0
  14. package/templates/app/(admin)/admin/error.js +4 -0
  15. package/templates/app/(admin)/admin/layout.js +17 -0
  16. package/templates/app/(admin)/admin/not-found.js +2 -0
  17. package/templates/app/(admin)/admin/page.js +5 -0
  18. package/templates/app/(auth)/auth/[...auth]/page.js +1 -0
  19. package/templates/app/(auth)/auth/page.js +5 -0
  20. package/templates/app/(public)/[...slug]/page.js +29 -0
  21. package/templates/app/(public)/error.js +6 -0
  22. package/templates/app/(public)/layout.js +17 -0
  23. package/templates/app/(public)/not-found.js +20 -0
  24. package/templates/app/(public)/page.js +248 -0
  25. package/templates/app/.fraza/blocks.client.js +8 -0
  26. package/templates/app/.fraza/blocks.server.js +4 -0
  27. package/templates/app/.fraza/modules.client.js +6 -0
  28. package/templates/app/.fraza/modules.generated.js +3 -0
  29. package/templates/app/.fraza/public-pages.server.js +4 -0
  30. package/templates/app/.fraza/sources.css +2 -0
  31. package/templates/app/.fraza/templates.server.js +5 -0
  32. package/templates/app/global-error.js +5 -0
  33. package/templates/app/icon.svg +1 -0
  34. package/templates/app/layout.js +31 -0
  35. package/templates/app/za/[...path]/not-found.js +6 -0
  36. package/templates/app/za/[...path]/page.js +8 -0
  37. package/templates/app/za/api/[...path]/route.js +1 -0
  38. package/templates/app/za/media/[filename]/route.js +1 -0
  39. package/templates/components/fraza/blocks/README.md +207 -0
  40. package/templates/components/fraza/blocks/accordion/fields.js +19 -0
  41. package/templates/components/fraza/blocks/accordion/render.js +47 -0
  42. package/templates/components/fraza/blocks/accordion/template.js +28 -0
  43. package/templates/components/fraza/blocks/callout/fields.js +15 -0
  44. package/templates/components/fraza/blocks/callout/render.js +28 -0
  45. package/templates/components/fraza/blocks/callout/template.js +37 -0
  46. package/templates/components/fraza/blocks/icon-list/fields.js +20 -0
  47. package/templates/components/fraza/blocks/icon-list/render.js +44 -0
  48. package/templates/components/fraza/blocks/icon-list/template.js +89 -0
  49. package/templates/components/fraza/blocks/three-cards/fields.js +19 -0
  50. package/templates/components/fraza/blocks/three-cards/render.js +33 -0
  51. package/templates/components/fraza/blocks/three-cards/template.js +57 -0
  52. package/templates/components/fraza/templates/README.md +293 -0
  53. package/templates/components/fraza/templates/content.js +271 -0
  54. package/templates/components/fraza/templates/default.js +18 -0
  55. package/templates/components/fraza/templates/post-blogue.js +97 -0
  56. package/templates/instrumentation.js +21 -0
  57. package/templates/jsconfig.json +9 -0
  58. package/templates/styles/globals.css +25 -0
  59. package/templates/theme.js +25 -0
@@ -0,0 +1,19 @@
1
+ // Métadonnées du bloc « Accordéon » : un en-tête cliquable qui révèle un contenu.
2
+ // Lu sans React au démarrage (slash menu, defaults, validation CLI).
3
+ //
4
+ // Cet exemple montre un **rendu public interactif** : son `render.js` porte
5
+ // `'use client'` (voir le fichier). C'est le pendant interactif de `callout/`
6
+ // et `three-cards/`, qui sont eux des server components statiques.
7
+
8
+ export const meta = {
9
+ id: 'accordion',
10
+ label: 'Accordéon',
11
+ icon: 'ChevronDownIcon',
12
+ keywords: ['accordion', 'accordéon', 'toggle', 'faq', 'repliable', 'détails'],
13
+ category: 'Contenu',
14
+ };
15
+
16
+ export const defaults = {
17
+ title: '',
18
+ body: '',
19
+ };
@@ -0,0 +1,47 @@
1
+ 'use client';
2
+
3
+ // Rendu public du bloc « Accordéon ».
4
+ //
5
+ // Ce render est **interactif** : il porte `'use client'` et utilise `useState`
6
+ // pour ouvrir/fermer le contenu. Importé par le manifeste serveur
7
+ // `app/.fraza/blocks.server.js`, il s'y résout en référence client : React
8
+ // franchit la frontière serveur-client automatiquement à l'instanciation.
9
+ //
10
+ // Style du site : classes Tailwind et jetons du thème du projet. À adapter
11
+ // librement.
12
+
13
+ import { useState } from 'react';
14
+ // Une icône par fichier : le barrel des icônes entrerait en entier dans le
15
+ // bundle client du site.
16
+ import { ChevronDownIcon } from '@fraza/icons/ChevronDownIcon';
17
+
18
+ export default function AccordionRender({ block }) {
19
+ const [open, setOpen] = useState(false);
20
+ const title = block.title || 'Détails';
21
+
22
+ return (
23
+ // `id={block.anchor || undefined}` rend l'ancre du bloc (menu d'actions ▸
24
+ // Ancre) comme cible d'un lien `#slug`. Posée en inline plutôt que via le
25
+ // helper `anchorProps` du core : ce render est `'use client'` et
26
+ // `@fraza/core/blocks-registry/render` est un module serveur-strict.
27
+ <div id={block.anchor || undefined} className="my-4 rounded-card border border-line bg-surface">
28
+ <button
29
+ type="button"
30
+ onClick={() => setOpen((v) => !v)}
31
+ aria-expanded={open}
32
+ className="w-full flex items-center justify-between gap-3 rounded-card px-4 py-3 text-left font-medium text-fg transition-colors duration-120 ease-out hover:bg-surface-hover focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-canvas"
33
+ >
34
+ <span>{title}</span>
35
+ <ChevronDownIcon
36
+ aria-hidden="true"
37
+ className={`h-4 w-4 shrink-0 text-fg-muted transition-transform duration-200 ${open ? 'rotate-180' : ''}`}
38
+ />
39
+ </button>
40
+ {open && (
41
+ <div className="px-4 pb-4 pt-1 text-fg-secondary whitespace-pre-wrap leading-relaxed">
42
+ {block.body}
43
+ </div>
44
+ )}
45
+ </div>
46
+ );
47
+ }
@@ -0,0 +1,28 @@
1
+ 'use client';
2
+
3
+ // Vue éditeur du bloc « Accordéon » dans l'administration.
4
+ // Style neutre de l'administration, homogène quel que soit le style du site.
5
+
6
+ import { TextField, TextareaField, FrazaBlockShell } from '@fraza/core/blocks-registry/fields-ui';
7
+
8
+ export default function AccordionTemplate({ block, onChange, disabled }) {
9
+ return (
10
+ <FrazaBlockShell>
11
+ <TextField
12
+ label="Titre"
13
+ value={block.title}
14
+ onChange={(title) => onChange?.({ title })}
15
+ placeholder="En-tête cliquable..."
16
+ disabled={disabled}
17
+ />
18
+ <TextareaField
19
+ label="Contenu"
20
+ value={block.body}
21
+ onChange={(body) => onChange?.({ body })}
22
+ placeholder="Révélé au clic sur le site public..."
23
+ disabled={disabled}
24
+ rows={4}
25
+ />
26
+ </FrazaBlockShell>
27
+ );
28
+ }
@@ -0,0 +1,15 @@
1
+ // Métadonnées du bloc « Encadré » : un message et une couleur.
2
+ // Lu sans React au démarrage (slash menu, defaults, validation CLI).
3
+
4
+ export const meta = {
5
+ id: 'callout',
6
+ label: 'Encadré',
7
+ icon: 'InfoCircleIcon',
8
+ keywords: ['callout', 'encadré', 'note', 'info', 'warning'],
9
+ category: 'Contenu',
10
+ };
11
+
12
+ export const defaults = {
13
+ text: '',
14
+ variant: 'info',
15
+ };
@@ -0,0 +1,28 @@
1
+ // Rendu public du bloc « Encadré ». Server component par défaut.
2
+ // Style du site : classes Tailwind et jetons du thème du projet, sur le modèle
3
+ // des alertes de la charte (fond doux et bordure du statut, icône à gauche).
4
+ // À adapter librement.
5
+
6
+ import { WarningIcon } from '@fraza/icons/WarningIcon';
7
+ import { CloseCircleIcon } from '@fraza/icons/CloseCircleIcon';
8
+ import { InfoCircleIcon } from '@fraza/icons/InfoCircleIcon';
9
+ import { CheckIcon } from '@fraza/icons/CheckIcon';
10
+
11
+ const VARIANTS = {
12
+ info: { classes: 'bg-info-soft border-info-line text-info', Icon: InfoCircleIcon },
13
+ success: { classes: 'bg-success-soft border-success-line text-success', Icon: CheckIcon },
14
+ warning: { classes: 'bg-warning-soft border-warning-line text-warning', Icon: WarningIcon },
15
+ danger: { classes: 'bg-danger-soft border-danger-line text-danger', Icon: CloseCircleIcon },
16
+ };
17
+
18
+ export default function CalloutRender({ block }) {
19
+ const { classes, Icon } = VARIANTS[block.variant] || VARIANTS.info;
20
+ return (
21
+ // `id={block.anchor || undefined}` : ancre du bloc (menu d'actions ▸ Ancre),
22
+ // cible d'un lien interne `#slug`.
23
+ <div id={block.anchor || undefined} className={`my-6 flex gap-3 rounded-[10px] border px-4 py-3 ${classes}`}>
24
+ <Icon aria-hidden="true" className="mt-1 h-4 w-4 shrink-0" />
25
+ <p className="whitespace-pre-wrap leading-relaxed">{block.text}</p>
26
+ </div>
27
+ );
28
+ }
@@ -0,0 +1,37 @@
1
+ 'use client';
2
+
3
+ // Vue éditeur du bloc « Encadré » dans l'administration.
4
+ // Style neutre de l'administration : dimensions fixes, couleurs sobres, aucune
5
+ // classe propre au site. L'administration reste ainsi homogène, quel que soit
6
+ // le style du site public.
7
+
8
+ import { TextareaField, SelectField, FrazaBlockShell } from '@fraza/core/blocks-registry/fields-ui';
9
+
10
+ const VARIANT_OPTIONS = [
11
+ { value: 'info', label: 'Information' },
12
+ { value: 'success', label: 'Succès' },
13
+ { value: 'warning', label: 'Attention' },
14
+ { value: 'danger', label: 'Erreur' },
15
+ ];
16
+
17
+ export default function CalloutTemplate({ block, onChange, disabled }) {
18
+ return (
19
+ <FrazaBlockShell>
20
+ <SelectField
21
+ label="Variante"
22
+ value={block.variant}
23
+ onChange={(variant) => onChange?.({ variant })}
24
+ options={VARIANT_OPTIONS}
25
+ disabled={disabled}
26
+ />
27
+ <TextareaField
28
+ label="Message"
29
+ value={block.text}
30
+ onChange={(text) => onChange?.({ text })}
31
+ placeholder="Tape ton message ici..."
32
+ disabled={disabled}
33
+ rows={3}
34
+ />
35
+ </FrazaBlockShell>
36
+ );
37
+ }
@@ -0,0 +1,20 @@
1
+ // Métadonnées du bloc « Liste d'icônes » : une liste verticale d'éléments,
2
+ // chacun avec une icône à gauche, un texte, et un lien optionnel
3
+ // (tel:, mailto: ou URL).
4
+
5
+ export const meta = {
6
+ id: 'icon-list',
7
+ label: "Liste d'icônes",
8
+ icon: 'ChecklistIcon',
9
+ keywords: ['icone', 'icon', 'liste', 'list', 'contact', 'coordonnées', 'features'],
10
+ category: 'Contenu',
11
+ };
12
+
13
+ export const defaults = {
14
+ title: '',
15
+ iconColor: '', // '' = hérite de la couleur du texte
16
+ items: [
17
+ { icon: 'SmartphoneIcon', text: '', href: '' },
18
+ { icon: 'AtIcon', text: '', href: '' },
19
+ ],
20
+ };
@@ -0,0 +1,44 @@
1
+ // Rendu public du bloc « Liste d'icônes ».
2
+ // Server component : affiche une liste verticale icône + texte, avec lien
3
+ // optionnel (tel:/mailto:/URL). `<Icon name>` charge seulement l'icône choisie
4
+ // de @fraza/icons ; un nom inconnu n'affiche rien.
5
+
6
+ import { sanitizeUrl } from '@fraza/core/blocks-registry/url';
7
+ import { Icon } from '@fraza/core/shared/components/Icon';
8
+
9
+ export default function IconListRender({ block }) {
10
+ const items = Array.isArray(block.items) ? block.items : [];
11
+ const iconColor = block.iconColor || undefined;
12
+
13
+ return (
14
+ <section id={block.anchor || undefined} className="my-12 max-w-3xl mx-auto px-4">
15
+ {block.title && (
16
+ <h2 className="text-2xl font-bold tracking-[-0.03em] text-fg mb-6">{block.title}</h2>
17
+ )}
18
+ <ul className="flex flex-col gap-3">
19
+ {items.map((item, i) => {
20
+ // Même liste blanche que les liens du core : une adresse refusée
21
+ // (`javascript:`, `data:`…) rend l'élément sans lien.
22
+ const href = sanitizeUrl(item.href);
23
+ const content = (
24
+ <>
25
+ <Icon name={item.icon} className="w-5 h-5 shrink-0" style={iconColor ? { color: iconColor } : undefined} />
26
+ <span>{item.text}</span>
27
+ </>
28
+ );
29
+ return (
30
+ <li key={i} className="flex items-center gap-3">
31
+ {href ? (
32
+ <a href={href} className="flex items-center gap-3 rounded-md text-accent underline-offset-[3px] transition-colors duration-120 ease-out hover:text-accent-hover hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-canvas">
33
+ {content}
34
+ </a>
35
+ ) : (
36
+ content
37
+ )}
38
+ </li>
39
+ );
40
+ })}
41
+ </ul>
42
+ </section>
43
+ );
44
+ }
@@ -0,0 +1,89 @@
1
+ 'use client';
2
+
3
+ // Vue éditeur du bloc « Liste d'icônes » dans l'administration.
4
+ // Un titre et une couleur d'icône commune, puis une liste d'éléments (icône,
5
+ // texte, lien) à ajouter ou retirer. Style neutre de l'administration.
6
+
7
+ import { TextField, IconField, ColorField, FrazaBlockShell } from '@fraza/core/blocks-registry/fields-ui';
8
+ import { Button } from '@fraza/core/shared/components';
9
+ import { PlusIcon } from '@fraza/icons/PlusIcon';
10
+
11
+ export default function IconListTemplate({ block, onChange, disabled }) {
12
+ const items = Array.isArray(block.items) ? block.items : [];
13
+
14
+ const patchItem = (index, patch) =>
15
+ onChange?.({ items: items.map((it, i) => (i === index ? { ...it, ...patch } : it)) });
16
+ const addItem = () =>
17
+ onChange?.({ items: [...items, { icon: 'SmartphoneIcon', text: '', href: '' }] });
18
+ const removeItem = (index) =>
19
+ onChange?.({ items: items.filter((_, i) => i !== index) });
20
+
21
+ return (
22
+ <FrazaBlockShell>
23
+ <TextField
24
+ label="Titre de la section"
25
+ value={block.title}
26
+ onChange={(title) => onChange?.({ title })}
27
+ placeholder="Optionnel"
28
+ disabled={disabled}
29
+ />
30
+ <ColorField
31
+ label="Couleur des icônes"
32
+ value={block.iconColor}
33
+ onChange={(iconColor) => onChange?.({ iconColor })}
34
+ disabled={disabled}
35
+ />
36
+
37
+ <div className="flex flex-col gap-3 mt-3">
38
+ {items.map((item, i) => (
39
+ <div
40
+ key={i}
41
+ className="border border-line rounded-control p-3 bg-surface"
42
+ >
43
+ <div className="flex items-center justify-between mb-2">
44
+ <span className="text-[12.5px] font-medium text-fg-secondary">Élément {i + 1}</span>
45
+ <Button
46
+ variant="dangerSoft"
47
+ size="sm"
48
+ onClick={() => removeItem(i)}
49
+ disabled={disabled}
50
+ >
51
+ Retirer
52
+ </Button>
53
+ </div>
54
+ <IconField
55
+ label="Icône"
56
+ value={item.icon}
57
+ onChange={(icon) => patchItem(i, { icon })}
58
+ disabled={disabled}
59
+ />
60
+ <TextField
61
+ label="Texte"
62
+ value={item.text}
63
+ onChange={(text) => patchItem(i, { text })}
64
+ disabled={disabled}
65
+ />
66
+ <TextField
67
+ label="Lien (optionnel)"
68
+ value={item.href}
69
+ onChange={(href) => patchItem(i, { href })}
70
+ placeholder="tel:..., mailto:... ou https://..."
71
+ disabled={disabled}
72
+ />
73
+ </div>
74
+ ))}
75
+ </div>
76
+
77
+ <Button
78
+ variant="secondary"
79
+ size="sm"
80
+ icon={PlusIcon}
81
+ onClick={addItem}
82
+ disabled={disabled}
83
+ className="mt-3"
84
+ >
85
+ Ajouter un élément
86
+ </Button>
87
+ </FrazaBlockShell>
88
+ );
89
+ }
@@ -0,0 +1,19 @@
1
+ // Métadonnées du bloc « Trois cartes » : une rangée de 3 cartes, chacune avec
2
+ // une image, un titre et un texte.
3
+
4
+ export const meta = {
5
+ id: 'three-cards',
6
+ label: 'Trois cartes',
7
+ icon: 'LayersIcon',
8
+ keywords: ['cartes', 'colonnes', 'grid', 'three', 'features'],
9
+ category: 'Layout',
10
+ };
11
+
12
+ export const defaults = {
13
+ title: '',
14
+ cards: [
15
+ { title: '', text: '', image: null },
16
+ { title: '', text: '', image: null },
17
+ { title: '', text: '', image: null },
18
+ ],
19
+ };
@@ -0,0 +1,33 @@
1
+ // Rendu public du bloc « Trois cartes ». Server component par défaut.
2
+ // Style du site : grille Tailwind responsive.
3
+
4
+ export default function ThreeCardsRender({ block }) {
5
+ const cards = Array.isArray(block.cards) ? block.cards : [];
6
+ return (
7
+ // `id={block.anchor || undefined}` : ancre du bloc (menu d'actions ▸ Ancre),
8
+ // cible d'un lien interne `#slug`.
9
+ <section id={block.anchor || undefined} className="my-12 max-w-6xl mx-auto px-4">
10
+ {block.title && (
11
+ <h2 className="text-3xl font-bold tracking-[-0.03em] text-fg mb-8 text-center">{block.title}</h2>
12
+ )}
13
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
14
+ {cards.map((card, i) => (
15
+ <article key={i} className="rounded-card overflow-hidden bg-surface border border-line">
16
+ {card.image?.src && (
17
+ // eslint-disable-next-line @next/next/no-img-element
18
+ <img
19
+ src={card.image.src}
20
+ alt={card.image.alt || card.title || ''}
21
+ className="w-full aspect-[4/3] object-cover"
22
+ />
23
+ )}
24
+ <div className="p-5">
25
+ {card.title && <h3 className="text-xl font-semibold text-fg mb-2">{card.title}</h3>}
26
+ {card.text && <p className="text-fg-secondary leading-relaxed">{card.text}</p>}
27
+ </div>
28
+ </article>
29
+ ))}
30
+ </div>
31
+ </section>
32
+ );
33
+ }
@@ -0,0 +1,57 @@
1
+ 'use client';
2
+
3
+ // Vue éditeur du bloc « Trois cartes » dans l'administration.
4
+ // Affiche un titre et 3 cartes modifiables côte à côte, dans le style neutre de
5
+ // l'administration.
6
+
7
+ import { TextField, TextareaField, ImageField, FrazaBlockShell } from '@fraza/core/blocks-registry/fields-ui';
8
+
9
+ export default function ThreeCardsTemplate({ block, onChange, disabled }) {
10
+ const cards = Array.isArray(block.cards) ? block.cards : [];
11
+
12
+ function patchCard(index, patch) {
13
+ const next = cards.map((c, i) => (i === index ? { ...c, ...patch } : c));
14
+ onChange?.({ cards: next });
15
+ }
16
+
17
+ return (
18
+ <FrazaBlockShell>
19
+ <TextField
20
+ label="Titre de la section"
21
+ value={block.title}
22
+ onChange={(title) => onChange?.({ title })}
23
+ placeholder="Optionnel"
24
+ disabled={disabled}
25
+ />
26
+ <div className="grid grid-cols-3 gap-3 mt-3">
27
+ {[0, 1, 2].map((i) => {
28
+ const card = cards[i] || { title: '', text: '', image: null };
29
+ return (
30
+ <div key={i} className="border border-line rounded-control p-3 bg-surface">
31
+ <div className="text-[12.5px] font-medium text-fg-secondary mb-2">Carte {i + 1}</div>
32
+ <ImageField
33
+ label="Image"
34
+ value={card.image}
35
+ onChange={(image) => patchCard(i, { image })}
36
+ disabled={disabled}
37
+ />
38
+ <TextField
39
+ label="Titre"
40
+ value={card.title}
41
+ onChange={(title) => patchCard(i, { title })}
42
+ disabled={disabled}
43
+ />
44
+ <TextareaField
45
+ label="Texte"
46
+ value={card.text}
47
+ onChange={(text) => patchCard(i, { text })}
48
+ rows={2}
49
+ disabled={disabled}
50
+ />
51
+ </div>
52
+ );
53
+ })}
54
+ </div>
55
+ </FrazaBlockShell>
56
+ );
57
+ }