kasy-cli 1.40.0 → 1.40.1

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.
@@ -1,4 +1,13 @@
1
1
  {
2
+ "1.40.1": {
3
+ "modules": {
4
+ "components": {
5
+ "pt": "Refino do submenu do Kasy Menu em telas estreitas: o submenu agora abre como sanfona embaixo do próprio item (expande inline), em vez de empilhar uma nova folha por cima. Menos custo de interação e sem te jogar numa segunda tela; no desktop ele continua cascateando ao lado (flyout). Já vale na demo web e no app gerado.",
6
+ "en": "Kasy Menu submenu refinement on narrow screens: the submenu now opens as an accordion right beneath its own item (expands inline) instead of stacking a new sheet on top. Lower interaction cost and no jump to a second surface; on desktop it still cascades to the side (flyout). Live in the web demo and the generated app.",
7
+ "es": "Refinamiento del submenú del Kasy Menu en pantallas estrechas: el submenú ahora abre como acordeón justo debajo de su propio ítem (se expande inline), en vez de apilar una nueva hoja encima. Menor costo de interacción y sin saltar a una segunda superficie; en escritorio sigue desplegándose al costado (flyout). Disponible en la demo web y en la app generada."
8
+ }
9
+ }
10
+ },
2
11
  "1.40.0": {
3
12
  "modules": {
4
13
  "components": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kasy-cli",
3
- "version": "1.40.0",
3
+ "version": "1.40.1",
4
4
  "description": "CLI for scaffolding production-ready Flutter SaaS apps with Firebase, Supabase, or API REST backends.",
5
5
  "bin": {
6
6
  "kasy": "./bin/kasy.js"
@@ -274,7 +274,10 @@ class _KasyAccordionRow extends StatelessWidget {
274
274
  Expanded(
275
275
  child: Text(
276
276
  data.title,
277
- style: context.textTheme.titleMedium?.copyWith(
277
+ // Header of an inline row component: 16 / w600 (not the
278
+ // 18 screen-section step), matching the alert header.
279
+ style: context.textTheme.bodyLarge?.copyWith(
280
+ fontWeight: FontWeight.w600,
278
281
  color: context.colors.onSurface,
279
282
  ),
280
283
  ),
@@ -51,14 +51,17 @@ class KasyAlert extends StatelessWidget {
51
51
  Widget build(BuildContext context) {
52
52
  final _KasyAlertPalette palette = _resolvePalette(context, tone);
53
53
  final bool hasMessage = message != null && message!.trim().isNotEmpty;
54
+ // Inline component header: 16 / w600 (one step below an overlay/section
55
+ // title) so a compact alert card never wears a screen-section-sized title.
54
56
  final TextStyle titleStyle =
55
- context.textTheme.titleMedium?.copyWith(
57
+ context.textTheme.bodyLarge?.copyWith(
58
+ fontWeight: FontWeight.w600,
56
59
  color: emphasizeTitleWithTone
57
60
  ? palette.accent
58
61
  : context.colors.onSurface,
59
62
  ) ??
60
63
  TextStyle(
61
- fontSize: 17,
64
+ fontSize: 16,
62
65
  fontWeight: FontWeight.w600,
63
66
  color: emphasizeTitleWithTone
64
67
  ? palette.accent
@@ -280,8 +280,10 @@ class KasyDialog extends StatelessWidget {
280
280
  Text(
281
281
  message!,
282
282
  textAlign: titleCentered ? TextAlign.center : TextAlign.start,
283
+ // Supporting text uses the body token (14); the sheet uses the
284
+ // same, so dialog and sheet read identically (they are the same
285
+ // surface on desktop). No off-ladder 15.
283
286
  style: context.textTheme.bodyMedium?.copyWith(
284
- fontSize: 15,
285
287
  color: context.colors.onSurface.withValues(alpha: 0.6),
286
288
  height: 1.5,
287
289
  ),
@@ -96,7 +96,8 @@ class KasyMenuItem {
96
96
  final bool enabled;
97
97
 
98
98
  /// When set, the row opens a nested menu instead of acting; a trailing chevron
99
- /// is shown. Opens as a side popover on desktop, a nested sheet on mobile.
99
+ /// is shown. Where there's room it cascades as a side flyout (desktop); on a
100
+ /// narrow screen it expands inline beneath the row (accordion).
100
101
  final List<KasyMenuSection>? submenu;
101
102
 
102
103
  /// Whether tapping this row closes the menu. Null inherits the menu-level
@@ -359,17 +360,18 @@ class _KasyMenuAnchorState extends State<KasyMenuAnchor>
359
360
  }
360
361
 
361
362
  // Decide the open direction only. Position is handled by the LayerLink, so a
362
- // slightly-off estimate just flips the menu — it always stays glued. Measured
363
- // in the overlay's space (where the panel draws) to stay consistent.
363
+ // slightly-off estimate just flips the menu — it always stays glued. Mirrors
364
+ // [KasyDropDown]: measure against the viewport minus the safe-area insets
365
+ // (notch / home indicator) so the panel never opens under them.
364
366
  bool _resolveOpenUp() {
365
367
  final RenderBox? box = context.findRenderObject() as RenderBox?;
366
- final RenderBox? overlayBox =
367
- Overlay.of(context).context.findRenderObject() as RenderBox?;
368
- if (box == null || overlayBox == null) return false;
369
- final Offset topLeft = box.localToGlobal(Offset.zero, ancestor: overlayBox);
370
- final double spaceBelow =
371
- overlayBox.size.height - (topLeft.dy + box.size.height);
372
- final double spaceAbove = topLeft.dy;
368
+ if (box == null) return false;
369
+ final Offset topLeft = box.localToGlobal(Offset.zero);
370
+ final Size viewport = MediaQuery.sizeOf(context);
371
+ final EdgeInsets safe = MediaQuery.viewPaddingOf(context);
372
+ final double triggerBottom = topLeft.dy + box.size.height;
373
+ final double spaceBelow = viewport.height - safe.bottom - triggerBottom;
374
+ final double spaceAbove = topLeft.dy - safe.top;
373
375
  return spaceBelow < _estimatedHeight() + 8 && spaceAbove > spaceBelow;
374
376
  }
375
377
 
@@ -591,6 +593,9 @@ class _MenuItemRowState extends State<_MenuItemRow>
591
593
 
592
594
  // Whether the flyout opens to the left of the row (no room on the right).
593
595
  bool _openLeft = false;
596
+ // Whether the submenu is expanded inline beneath the row (the narrow-screen
597
+ // accordion fallback, when there's no room for a side cascade).
598
+ bool _inlineExpanded = false;
594
599
 
595
600
  bool get _hasSubmenu => widget.item.submenu != null;
596
601
 
@@ -615,13 +620,22 @@ class _MenuItemRowState extends State<_MenuItemRow>
615
620
  super.dispose();
616
621
  }
617
622
 
618
- void _openSubmenu() {
623
+ // Toggle the submenu. With room on a side it cascades as a flyout (desktop);
624
+ // on a narrow screen it expands inline beneath the row (the accordion the
625
+ // NN/g mobile-subnavigation guidance recommends for a handful of items —
626
+ // lowest interaction cost, no disorienting second surface).
627
+ void _toggleSubmenu() {
628
+ if (_portal.isShowing) {
629
+ _closeSubmenu();
630
+ return;
631
+ }
632
+ if (_inlineExpanded) {
633
+ setState(() => _inlineExpanded = false);
634
+ return;
635
+ }
619
636
  final _FlyoutSide side = _resolveSide();
620
637
  if (side == _FlyoutSide.none) {
621
- // No room for a side cascade (narrow phone): a 224px panel won't fit
622
- // beside the menu, so present the nested menu as a sheet instead of a
623
- // flyout clipped off-screen. The cascade still shows on wider layouts.
624
- _openSubmenuSheet();
638
+ setState(() => _inlineExpanded = true);
625
639
  return;
626
640
  }
627
641
  setState(() => _openLeft = side == _FlyoutSide.left);
@@ -635,28 +649,6 @@ class _MenuItemRowState extends State<_MenuItemRow>
635
649
  });
636
650
  }
637
651
 
638
- Future<void> _openSubmenuSheet() {
639
- final bool dark = Theme.of(context).brightness == Brightness.dark;
640
- return showModalBottomSheet<void>(
641
- context: context,
642
- useRootNavigator: true,
643
- backgroundColor: Colors.transparent,
644
- barrierColor: Colors.black.withValues(alpha: dark ? 0.6 : 0.45),
645
- builder: (_) => KasySheetSurface(
646
- child: KasyMenu(
647
- sections: widget.item.submenu!,
648
- closeOnSelect: widget.menuCloseOnSelect,
649
- // A row that closes the menu pops this sheet, then bubbles the close
650
- // up so the parent chain dismisses too.
651
- onClose: () {
652
- Navigator.of(context, rootNavigator: true).pop();
653
- _bubbleClose();
654
- },
655
- ),
656
- ),
657
- );
658
- }
659
-
660
652
  // Pick the side with room for the flyout, or [none] when neither fits (the
661
653
  // caller then falls back to a sheet). Position is the LayerLink's job — this
662
654
  // only decides the side, measured in the overlay's space.
@@ -677,11 +669,7 @@ class _MenuItemRowState extends State<_MenuItemRow>
677
669
 
678
670
  void _handleTap() {
679
671
  if (_hasSubmenu) {
680
- if (_portal.isShowing) {
681
- _closeSubmenu();
682
- } else {
683
- _openSubmenu();
684
- }
672
+ _toggleSubmenu();
685
673
  return;
686
674
  }
687
675
  // Close first (mirrors the call-site ordering that avoids a Navigator lock
@@ -818,13 +806,20 @@ class _MenuItemRowState extends State<_MenuItemRow>
818
806
  leading = SizedBox(width: iconSize);
819
807
  }
820
808
 
821
- // Suffix slot: submenu chevron, custom trailing, or a shortcut chip.
809
+ // Suffix slot: submenu chevron, custom trailing, or a shortcut chip. The
810
+ // chevron rotates a quarter turn down while the submenu is expanded inline,
811
+ // so the row reads like an accordion header.
822
812
  Widget? trailing;
823
813
  if (item.submenu != null) {
824
- trailing = Icon(
825
- KasyIcons.chevronRight,
826
- size: KasyIconSize.sm,
827
- color: resolvedLead,
814
+ trailing = AnimatedRotation(
815
+ turns: _inlineExpanded ? 0.25 : 0,
816
+ duration: const Duration(milliseconds: 150),
817
+ curve: Curves.easeOutCubic,
818
+ child: Icon(
819
+ KasyIcons.chevronRight,
820
+ size: KasyIconSize.sm,
821
+ color: resolvedLead,
822
+ ),
828
823
  );
829
824
  } else if (item.trailing != null) {
830
825
  trailing = item.trailing;
@@ -890,13 +885,42 @@ class _MenuItemRowState extends State<_MenuItemRow>
890
885
  ),
891
886
  );
892
887
 
893
- // A plain row needs no overlay machinery. A submenu row hosts its flyout
894
- // through an OverlayPortal and links the panel to itself for side anchoring.
888
+ // A plain row needs no overlay machinery.
895
889
  if (!_hasSubmenu) return hover;
890
+
891
+ // The trigger carries the LayerLink so a side cascade can anchor to it.
892
+ Widget child = CompositedTransformTarget(link: _link, child: hover);
893
+
894
+ // Narrow screens expand the submenu inline, indented beneath the row (so the
895
+ // nested items align under the title). Mutually exclusive with the cascade.
896
+ if (_inlineExpanded) {
897
+ final double indent = iconSize + iconGap;
898
+ child = Column(
899
+ mainAxisSize: MainAxisSize.min,
900
+ crossAxisAlignment: CrossAxisAlignment.stretch,
901
+ children: [
902
+ child,
903
+ Padding(
904
+ padding: EdgeInsets.only(left: indent),
905
+ child: KasyMenu(
906
+ sections: item.submenu!,
907
+ density: widget.compact
908
+ ? KasyMenuDensity.compact
909
+ : KasyMenuDensity.comfortable,
910
+ closeOnSelect: widget.menuCloseOnSelect,
911
+ onClose: _closeChain,
912
+ tapGroupId: widget.tapGroupId,
913
+ ),
914
+ ),
915
+ ],
916
+ );
917
+ }
918
+
919
+ // A submenu row hosts its side flyout through an OverlayPortal.
896
920
  return OverlayPortal(
897
921
  controller: _portal,
898
922
  overlayChildBuilder: _buildSubmenuOverlay,
899
- child: CompositedTransformTarget(link: _link, child: hover),
923
+ child: child,
900
924
  );
901
925
  }
902
926
  }
@@ -184,31 +184,6 @@ class KasyTextTheme extends ThemeExtension<KasyTextTheme> {
184
184
  /// Caption, hint, version label, footnote. 12 / w400.
185
185
  TextStyle get caption => bodySmall;
186
186
 
187
- // --- Overlay & component roles -----------------------------------------
188
- // The harmonic ladder for surfaces and components: an OVERLAY (a full
189
- // decision surface — dialog, bottom sheet) carries the largest title (20); an
190
- // inline COMPONENT header (alert, accordion) sits a step below (16) so it
191
- // reads as a header WITHIN the component, not a screen section; supporting
192
- // text everywhere is 14. These are deliberately STABLE across breakpoints —
193
- // only page/hero headings scale (Material/Apple model). Route every
194
- // dialog/sheet/alert/accordion through these so titles never drift per
195
- // component.
196
-
197
- /// Title of a modal/overlay surface (dialog, bottom sheet, OTP sheet).
198
- /// 20 / w600 — the largest title tier, for full decision surfaces.
199
- TextStyle get overlayTitle => titleLarge;
200
-
201
- /// Header of an inline component (alert, accordion). 16 / w600 — content
202
- /// emphasis, one clear step below an overlay/section title and above body, so
203
- /// a compact card never wears a screen-section-sized title.
204
- TextStyle get componentTitle =>
205
- bodyLarge.copyWith(fontWeight: FontWeight.w600);
206
-
207
- /// Supporting text / description inside an overlay or component (the message
208
- /// under a dialog/sheet/alert title, an accordion body). 14 / w400. Apply the
209
- /// muted colour and any line-height at the call site.
210
- TextStyle get supportingText => bodyMedium;
211
-
212
187
  /// Builds the full text theme for a given [device].
213
188
  ///
214
189
  /// Each slot resolves its size from the responsive [KasyTypeScale] ramp at the